From de94baea97c7357d4899fae3770d8cee0a897e94 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 11:54:20 +0300 Subject: [PATCH 01/38] chore(transcode): vendor the OxideAV AC-3, DTS and AAC codecs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VuIO has no audio decoder for AC-3, E-AC-3 or DTS, so a TV without those licences plays the picture and no sound. Symphonia identifies those tracks (CODEC_ID_AC3 / CODEC_ID_EAC3 / CODEC_ID_DCA) and demuxes them fine — it just has no decoder. These four crates are that missing piece. Vendored rather than depended on. oxideav-dts is published to crates.io only as a yanked 0.0.1, so no registry dependency can ship DTS at all; the rest of the family moves fast enough that a floating version would change what a release decodes without anyone choosing it. The copies are verbatim — same crate names, same layout, no patches — so a diff against upstream stays meaningful and a refresh is a re-run of the script rather than a merge. Only three mechanical manifest changes: sibling deps become path deps, publishing is off (we do not own these names), and lints are allowed, because normalising 6 MB of foreign code to our clippy settings would destroy the diffability that is the point of vendoring verbatim. Upstream tests/ and benches/ are dropped, except the two fixtures the inline src/ tests include_bytes! — 17 KB total, and worth it: `cargo test -p oxideav-ac3` still checks the decoder we ship against a real bitstream. All 2330 upstream tests pass against the vendored copies. Costs no new external crates: thiserror, serde_json and bytemuck were already in the tree. Workspace members so the path deps resolve, but not default members, so their test and bench targets stay off the release path. Verified: cargo test -p oxideav-core -p oxideav-ac3 -p oxideav-dts -p oxideav-aac --lib → 869 + 459 + 280 + 722 passed, 0 failed. --- Cargo.toml | 11 + crates/vendor/oxideav-aac/Cargo.toml | 27 + crates/vendor/oxideav-aac/LICENSE | 21 + crates/vendor/oxideav-aac/README.md | 1252 +++ crates/vendor/oxideav-aac/VENDOR.toml | 9 + crates/vendor/oxideav-aac/src/adts.rs | 289 + crates/vendor/oxideav-aac/src/adts_crc.rs | 566 ++ crates/vendor/oxideav-aac/src/asc.rs | 819 ++ crates/vendor/oxideav-aac/src/bsac_arith.rs | 413 + crates/vendor/oxideav-aac/src/bsac_decode.rs | 861 ++ crates/vendor/oxideav-aac/src/bsac_layer.rs | 499 + crates/vendor/oxideav-aac/src/bsac_tables.rs | 1020 +++ crates/vendor/oxideav-aac/src/cce.rs | 1289 +++ crates/vendor/oxideav-aac/src/channel_map.rs | 751 ++ .../vendor/oxideav-aac/src/codec_decoder.rs | 972 ++ .../vendor/oxideav-aac/src/codec_encoder.rs | 333 + crates/vendor/oxideav-aac/src/crc.rs | 449 + crates/vendor/oxideav-aac/src/decode.rs | 1305 +++ .../oxideav-aac/src/decoded_spectrum.rs | 353 + crates/vendor/oxideav-aac/src/dequant.rs | 476 + .../vendor/oxideav-aac/src/element_decode.rs | 1482 +++ crates/vendor/oxideav-aac/src/encoder.rs | 2991 ++++++ crates/vendor/oxideav-aac/src/encoder_tns.rs | 421 + crates/vendor/oxideav-aac/src/ep_config.rs | 586 ++ crates/vendor/oxideav-aac/src/ep_fec.rs | 652 ++ crates/vendor/oxideav-aac/src/ep_frame.rs | 1390 +++ crates/vendor/oxideav-aac/src/ep_rs.rs | 434 + crates/vendor/oxideav-aac/src/error.rs | 1244 +++ .../oxideav-aac/src/extension_payload.rs | 856 ++ crates/vendor/oxideav-aac/src/filterbank.rs | 1549 ++++ crates/vendor/oxideav-aac/src/gain_control.rs | 908 ++ .../oxideav-aac/src/gain_control_data.rs | 305 + crates/vendor/oxideav-aac/src/hcr.rs | 677 ++ crates/vendor/oxideav-aac/src/hcr_decode.rs | 777 ++ crates/vendor/oxideav-aac/src/ics_body.rs | 837 ++ crates/vendor/oxideav-aac/src/ics_info.rs | 1100 +++ .../oxideav-aac/src/intensity_stereo.rs | 621 ++ crates/vendor/oxideav-aac/src/ipqf.rs | 298 + crates/vendor/oxideav-aac/src/latm.rs | 1902 ++++ crates/vendor/oxideav-aac/src/lib.rs | 641 ++ crates/vendor/oxideav-aac/src/ltp.rs | 976 ++ crates/vendor/oxideav-aac/src/ms_stereo.rs | 684 ++ crates/vendor/oxideav-aac/src/pce.rs | 440 + crates/vendor/oxideav-aac/src/pcm.rs | 198 + crates/vendor/oxideav-aac/src/pns.rs | 701 ++ crates/vendor/oxideav-aac/src/predictor.rs | 752 ++ crates/vendor/oxideav-aac/src/ps_data.rs | 705 ++ crates/vendor/oxideav-aac/src/ps_decoder.rs | 307 + crates/vendor/oxideav-aac/src/ps_decorr.rs | 519 ++ crates/vendor/oxideav-aac/src/ps_huffman.rs | 472 + crates/vendor/oxideav-aac/src/ps_hybrid.rs | 508 ++ crates/vendor/oxideav-aac/src/ps_map.rs | 282 + crates/vendor/oxideav-aac/src/ps_stereo.rs | 541 ++ crates/vendor/oxideav-aac/src/pulse_data.rs | 190 + .../vendor/oxideav-aac/src/raw_data_block.rs | 666 ++ crates/vendor/oxideav-aac/src/rvlc.rs | 407 + crates/vendor/oxideav-aac/src/sbr_decoder.rs | 1111 +++ crates/vendor/oxideav-aac/src/sbr_dequant.rs | 259 + crates/vendor/oxideav-aac/src/sbr_element.rs | 561 ++ .../vendor/oxideav-aac/src/sbr_env_adjust.rs | 1029 +++ crates/vendor/oxideav-aac/src/sbr_envelope.rs | 410 + .../vendor/oxideav-aac/src/sbr_extension.rs | 493 + .../vendor/oxideav-aac/src/sbr_freq_bands.rs | 756 ++ crates/vendor/oxideav-aac/src/sbr_grid.rs | 464 + crates/vendor/oxideav-aac/src/sbr_header.rs | 350 + crates/vendor/oxideav-aac/src/sbr_hf_gen.rs | 556 ++ crates/vendor/oxideav-aac/src/sbr_huffman.rs | 1011 +++ crates/vendor/oxideav-aac/src/sbr_limiter.rs | 170 + crates/vendor/oxideav-aac/src/sbr_lp.rs | 331 + .../vendor/oxideav-aac/src/sbr_noise_table.rs | 564 ++ crates/vendor/oxideav-aac/src/sbr_qmf.rs | 1005 ++ .../vendor/oxideav-aac/src/sbr_reconstruct.rs | 414 + .../vendor/oxideav-aac/src/sbr_time_grid.rs | 340 + crates/vendor/oxideav-aac/src/scalable.rs | 1541 ++++ .../oxideav-aac/src/scale_factor_data.rs | 1579 ++++ crates/vendor/oxideav-aac/src/section_data.rs | 631 ++ .../oxideav-aac/src/spectral_codebook.rs | 543 ++ .../vendor/oxideav-aac/src/spectral_data.rs | 927 ++ .../oxideav-aac/src/spectrum_huffman.rs | 5593 ++++++++++++ crates/vendor/oxideav-aac/src/ssr.rs | 549 ++ .../vendor/oxideav-aac/src/ssr_filterbank.rs | 454 + crates/vendor/oxideav-aac/src/swb_offset.rs | 1418 +++ crates/vendor/oxideav-aac/src/tns_coef.rs | 1239 +++ crates/vendor/oxideav-aac/src/tns_data.rs | 480 + crates/vendor/oxideav-aac/src/tns_frame.rs | 1035 +++ crates/vendor/oxideav-aac/src/tns_max.rs | 750 ++ crates/vendor/oxideav-ac3/Cargo.toml | 27 + crates/vendor/oxideav-ac3/LICENSE | 21 + crates/vendor/oxideav-ac3/README.md | 371 + crates/vendor/oxideav-ac3/VENDOR.toml | 9 + crates/vendor/oxideav-ac3/src/audblk.rs | 2934 ++++++ crates/vendor/oxideav-ac3/src/bsi.rs | 5027 ++++++++++ crates/vendor/oxideav-ac3/src/crc.rs | 554 ++ crates/vendor/oxideav-ac3/src/decoder.rs | 1010 ++ crates/vendor/oxideav-ac3/src/downmix.rs | 1154 +++ crates/vendor/oxideav-ac3/src/drc.rs | 418 + crates/vendor/oxideav-ac3/src/eac3/aht.rs | 635 ++ crates/vendor/oxideav-ac3/src/eac3/ahtenc.rs | 678 ++ crates/vendor/oxideav-ac3/src/eac3/audfrm.rs | 813 ++ crates/vendor/oxideav-ac3/src/eac3/bsi.rs | 3343 +++++++ crates/vendor/oxideav-ac3/src/eac3/chanmap.rs | 678 ++ crates/vendor/oxideav-ac3/src/eac3/decoder.rs | 868 ++ crates/vendor/oxideav-ac3/src/eac3/dsp.rs | 3109 +++++++ crates/vendor/oxideav-ac3/src/eac3/ecpl.rs | 2537 ++++++ crates/vendor/oxideav-ac3/src/eac3/ecplenc.rs | 773 ++ crates/vendor/oxideav-ac3/src/eac3/encoder.rs | 5603 ++++++++++++ crates/vendor/oxideav-ac3/src/eac3/mod.rs | 167 + crates/vendor/oxideav-ac3/src/eac3/spxenc.rs | 786 ++ .../src/eac3/tables/aht_codebooks.rs | 995 ++ .../vendor/oxideav-ac3/src/eac3/tables/mod.rs | 8 + crates/vendor/oxideav-ac3/src/encoder.rs | 8087 +++++++++++++++++ crates/vendor/oxideav-ac3/src/imdct.rs | 609 ++ crates/vendor/oxideav-ac3/src/lib.rs | 240 + crates/vendor/oxideav-ac3/src/mdct.rs | 304 + crates/vendor/oxideav-ac3/src/syncinfo.rs | 670 ++ crates/vendor/oxideav-ac3/src/tables.rs | 487 + crates/vendor/oxideav-ac3/src/wave_order.rs | 408 + .../tests/fixtures/sine440_stereo.ac3 | Bin 0 -> 12288 bytes crates/vendor/oxideav-core/Cargo.toml | 43 + crates/vendor/oxideav-core/LICENSE | 21 + crates/vendor/oxideav-core/README.md | 132 + crates/vendor/oxideav-core/VENDOR.toml | 9 + crates/vendor/oxideav-core/src/arena/mod.rs | 938 ++ crates/vendor/oxideav-core/src/arena/sync.rs | 873 ++ crates/vendor/oxideav-core/src/bits.rs | 1021 +++ .../vendor/oxideav-core/src/capabilities.rs | 252 + crates/vendor/oxideav-core/src/engine.rs | 126 + crates/vendor/oxideav-core/src/error.rs | 205 + crates/vendor/oxideav-core/src/execution.rs | 121 + crates/vendor/oxideav-core/src/filter.rs | 285 + crates/vendor/oxideav-core/src/format.rs | 2394 +++++ crates/vendor/oxideav-core/src/frame.rs | 649 ++ crates/vendor/oxideav-core/src/lib.rs | 67 + crates/vendor/oxideav-core/src/limits.rs | 183 + crates/vendor/oxideav-core/src/metadata.rs | 148 + crates/vendor/oxideav-core/src/options.rs | 480 + crates/vendor/oxideav-core/src/packet.rs | 275 + crates/vendor/oxideav-core/src/picture.rs | 282 + crates/vendor/oxideav-core/src/rational.rs | 621 ++ .../vendor/oxideav-core/src/registry/codec.rs | 1342 +++ .../oxideav-core/src/registry/container.rs | 361 + .../oxideav-core/src/registry/context.rs | 39 + .../oxideav-core/src/registry/filter.rs | 77 + .../vendor/oxideav-core/src/registry/mod.rs | 26 + .../vendor/oxideav-core/src/registry/slice.rs | 61 + .../oxideav-core/src/registry/source.rs | 577 ++ crates/vendor/oxideav-core/src/stream.rs | 922 ++ crates/vendor/oxideav-core/src/subtitle.rs | 178 + crates/vendor/oxideav-core/src/time.rs | 675 ++ crates/vendor/oxideav-core/src/vector.rs | 1441 +++ crates/vendor/oxideav-dts/Cargo.toml | 36 + crates/vendor/oxideav-dts/LICENSE | 21 + crates/vendor/oxideav-dts/README.md | 426 + crates/vendor/oxideav-dts/VENDOR.toml | 9 + crates/vendor/oxideav-dts/src/audio_array.rs | 1664 ++++ crates/vendor/oxideav-dts/src/audio_data.rs | 400 + crates/vendor/oxideav-dts/src/audio_header.rs | 653 ++ crates/vendor/oxideav-dts/src/audio_huff.rs | 3197 +++++++ crates/vendor/oxideav-dts/src/aux_data.rs | 770 ++ crates/vendor/oxideav-dts/src/bitreader.rs | 192 + crates/vendor/oxideav-dts/src/block_code.rs | 544 ++ crates/vendor/oxideav-dts/src/cos_mod.rs | 790 ++ crates/vendor/oxideav-dts/src/crc16.rs | 222 + crates/vendor/oxideav-dts/src/d10_tables.rs | 5157 +++++++++++ crates/vendor/oxideav-dts/src/d10_vq.rs | 824 ++ .../vendor/oxideav-dts/src/d6_block_book.rs | 560 ++ crates/vendor/oxideav-dts/src/dmix_coeff.rs | 344 + crates/vendor/oxideav-dts/src/drc_range.rs | 272 + crates/vendor/oxideav-dts/src/dsync.rs | 289 + crates/vendor/oxideav-dts/src/filter_bank.rs | 341 + crates/vendor/oxideav-dts/src/fir_coeff.rs | 453 + crates/vendor/oxideav-dts/src/header.rs | 4707 ++++++++++ .../vendor/oxideav-dts/src/inverse_adpcm.rs | 715 ++ crates/vendor/oxideav-dts/src/iter.rs | 2388 +++++ crates/vendor/oxideav-dts/src/join_scale.rs | 153 + .../vendor/oxideav-dts/src/joint_subband.rs | 674 ++ .../vendor/oxideav-dts/src/lfe_fir_coeff.rs | 448 + crates/vendor/oxideav-dts/src/lfe_interp.rs | 321 + crates/vendor/oxideav-dts/src/lfe_synth.rs | 648 ++ crates/vendor/oxideav-dts/src/lib.rs | 1167 +++ .../vendor/oxideav-dts/src/optional_info.rs | 209 + crates/vendor/oxideav-dts/src/qmf_assemble.rs | 1334 +++ .../oxideav-dts/src/qmf_multichannel.rs | 594 ++ crates/vendor/oxideav-dts/src/qmf_synth.rs | 448 + crates/vendor/oxideav-dts/src/registry.rs | 856 ++ crates/vendor/oxideav-dts/src/rev2_aux.rs | 702 ++ crates/vendor/oxideav-dts/src/side_info.rs | 1968 ++++ crates/vendor/oxideav-dts/src/step_size.rs | 582 ++ crates/vendor/oxideav-dts/src/subframe.rs | 985 ++ crates/vendor/oxideav-dts/src/subframe_pcm.rs | 2300 +++++ crates/vendor/oxideav-dts/src/sum_diff.rs | 622 ++ crates/vendor/oxideav-dts/src/test_util.rs | 80 + crates/vendor/oxideav-dts/src/unpack14.rs | 605 ++ .../tests/fixtures/dts_5_frames.bin | Bin 0 -> 5120 bytes scripts/vendor-oxideav.sh | 153 + 195 files changed, 164267 insertions(+) create mode 100644 crates/vendor/oxideav-aac/Cargo.toml create mode 100644 crates/vendor/oxideav-aac/LICENSE create mode 100644 crates/vendor/oxideav-aac/README.md create mode 100644 crates/vendor/oxideav-aac/VENDOR.toml create mode 100644 crates/vendor/oxideav-aac/src/adts.rs create mode 100644 crates/vendor/oxideav-aac/src/adts_crc.rs create mode 100644 crates/vendor/oxideav-aac/src/asc.rs create mode 100644 crates/vendor/oxideav-aac/src/bsac_arith.rs create mode 100644 crates/vendor/oxideav-aac/src/bsac_decode.rs create mode 100644 crates/vendor/oxideav-aac/src/bsac_layer.rs create mode 100644 crates/vendor/oxideav-aac/src/bsac_tables.rs create mode 100644 crates/vendor/oxideav-aac/src/cce.rs create mode 100644 crates/vendor/oxideav-aac/src/channel_map.rs create mode 100644 crates/vendor/oxideav-aac/src/codec_decoder.rs create mode 100644 crates/vendor/oxideav-aac/src/codec_encoder.rs create mode 100644 crates/vendor/oxideav-aac/src/crc.rs create mode 100644 crates/vendor/oxideav-aac/src/decode.rs create mode 100644 crates/vendor/oxideav-aac/src/decoded_spectrum.rs create mode 100644 crates/vendor/oxideav-aac/src/dequant.rs create mode 100644 crates/vendor/oxideav-aac/src/element_decode.rs create mode 100644 crates/vendor/oxideav-aac/src/encoder.rs create mode 100644 crates/vendor/oxideav-aac/src/encoder_tns.rs create mode 100644 crates/vendor/oxideav-aac/src/ep_config.rs create mode 100644 crates/vendor/oxideav-aac/src/ep_fec.rs create mode 100644 crates/vendor/oxideav-aac/src/ep_frame.rs create mode 100644 crates/vendor/oxideav-aac/src/ep_rs.rs create mode 100644 crates/vendor/oxideav-aac/src/error.rs create mode 100644 crates/vendor/oxideav-aac/src/extension_payload.rs create mode 100644 crates/vendor/oxideav-aac/src/filterbank.rs create mode 100644 crates/vendor/oxideav-aac/src/gain_control.rs create mode 100644 crates/vendor/oxideav-aac/src/gain_control_data.rs create mode 100644 crates/vendor/oxideav-aac/src/hcr.rs create mode 100644 crates/vendor/oxideav-aac/src/hcr_decode.rs create mode 100644 crates/vendor/oxideav-aac/src/ics_body.rs create mode 100644 crates/vendor/oxideav-aac/src/ics_info.rs create mode 100644 crates/vendor/oxideav-aac/src/intensity_stereo.rs create mode 100644 crates/vendor/oxideav-aac/src/ipqf.rs create mode 100644 crates/vendor/oxideav-aac/src/latm.rs create mode 100644 crates/vendor/oxideav-aac/src/lib.rs create mode 100644 crates/vendor/oxideav-aac/src/ltp.rs create mode 100644 crates/vendor/oxideav-aac/src/ms_stereo.rs create mode 100644 crates/vendor/oxideav-aac/src/pce.rs create mode 100644 crates/vendor/oxideav-aac/src/pcm.rs create mode 100644 crates/vendor/oxideav-aac/src/pns.rs create mode 100644 crates/vendor/oxideav-aac/src/predictor.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_data.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_decoder.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_decorr.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_huffman.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_hybrid.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_map.rs create mode 100644 crates/vendor/oxideav-aac/src/ps_stereo.rs create mode 100644 crates/vendor/oxideav-aac/src/pulse_data.rs create mode 100644 crates/vendor/oxideav-aac/src/raw_data_block.rs create mode 100644 crates/vendor/oxideav-aac/src/rvlc.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_decoder.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_dequant.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_element.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_env_adjust.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_envelope.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_extension.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_freq_bands.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_grid.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_header.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_hf_gen.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_huffman.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_limiter.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_lp.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_noise_table.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_qmf.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_reconstruct.rs create mode 100644 crates/vendor/oxideav-aac/src/sbr_time_grid.rs create mode 100644 crates/vendor/oxideav-aac/src/scalable.rs create mode 100644 crates/vendor/oxideav-aac/src/scale_factor_data.rs create mode 100644 crates/vendor/oxideav-aac/src/section_data.rs create mode 100644 crates/vendor/oxideav-aac/src/spectral_codebook.rs create mode 100644 crates/vendor/oxideav-aac/src/spectral_data.rs create mode 100644 crates/vendor/oxideav-aac/src/spectrum_huffman.rs create mode 100644 crates/vendor/oxideav-aac/src/ssr.rs create mode 100644 crates/vendor/oxideav-aac/src/ssr_filterbank.rs create mode 100644 crates/vendor/oxideav-aac/src/swb_offset.rs create mode 100644 crates/vendor/oxideav-aac/src/tns_coef.rs create mode 100644 crates/vendor/oxideav-aac/src/tns_data.rs create mode 100644 crates/vendor/oxideav-aac/src/tns_frame.rs create mode 100644 crates/vendor/oxideav-aac/src/tns_max.rs create mode 100644 crates/vendor/oxideav-ac3/Cargo.toml create mode 100644 crates/vendor/oxideav-ac3/LICENSE create mode 100644 crates/vendor/oxideav-ac3/README.md create mode 100644 crates/vendor/oxideav-ac3/VENDOR.toml create mode 100644 crates/vendor/oxideav-ac3/src/audblk.rs create mode 100644 crates/vendor/oxideav-ac3/src/bsi.rs create mode 100644 crates/vendor/oxideav-ac3/src/crc.rs create mode 100644 crates/vendor/oxideav-ac3/src/decoder.rs create mode 100644 crates/vendor/oxideav-ac3/src/downmix.rs create mode 100644 crates/vendor/oxideav-ac3/src/drc.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/aht.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/ahtenc.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/audfrm.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/bsi.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/chanmap.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/decoder.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/dsp.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/ecpl.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/ecplenc.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/encoder.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/mod.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/spxenc.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/tables/aht_codebooks.rs create mode 100644 crates/vendor/oxideav-ac3/src/eac3/tables/mod.rs create mode 100644 crates/vendor/oxideav-ac3/src/encoder.rs create mode 100644 crates/vendor/oxideav-ac3/src/imdct.rs create mode 100644 crates/vendor/oxideav-ac3/src/lib.rs create mode 100644 crates/vendor/oxideav-ac3/src/mdct.rs create mode 100644 crates/vendor/oxideav-ac3/src/syncinfo.rs create mode 100644 crates/vendor/oxideav-ac3/src/tables.rs create mode 100644 crates/vendor/oxideav-ac3/src/wave_order.rs create mode 100644 crates/vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3 create mode 100644 crates/vendor/oxideav-core/Cargo.toml create mode 100644 crates/vendor/oxideav-core/LICENSE create mode 100644 crates/vendor/oxideav-core/README.md create mode 100644 crates/vendor/oxideav-core/VENDOR.toml create mode 100644 crates/vendor/oxideav-core/src/arena/mod.rs create mode 100644 crates/vendor/oxideav-core/src/arena/sync.rs create mode 100644 crates/vendor/oxideav-core/src/bits.rs create mode 100644 crates/vendor/oxideav-core/src/capabilities.rs create mode 100644 crates/vendor/oxideav-core/src/engine.rs create mode 100644 crates/vendor/oxideav-core/src/error.rs create mode 100644 crates/vendor/oxideav-core/src/execution.rs create mode 100644 crates/vendor/oxideav-core/src/filter.rs create mode 100644 crates/vendor/oxideav-core/src/format.rs create mode 100644 crates/vendor/oxideav-core/src/frame.rs create mode 100644 crates/vendor/oxideav-core/src/lib.rs create mode 100644 crates/vendor/oxideav-core/src/limits.rs create mode 100644 crates/vendor/oxideav-core/src/metadata.rs create mode 100644 crates/vendor/oxideav-core/src/options.rs create mode 100644 crates/vendor/oxideav-core/src/packet.rs create mode 100644 crates/vendor/oxideav-core/src/picture.rs create mode 100644 crates/vendor/oxideav-core/src/rational.rs create mode 100644 crates/vendor/oxideav-core/src/registry/codec.rs create mode 100644 crates/vendor/oxideav-core/src/registry/container.rs create mode 100644 crates/vendor/oxideav-core/src/registry/context.rs create mode 100644 crates/vendor/oxideav-core/src/registry/filter.rs create mode 100644 crates/vendor/oxideav-core/src/registry/mod.rs create mode 100644 crates/vendor/oxideav-core/src/registry/slice.rs create mode 100644 crates/vendor/oxideav-core/src/registry/source.rs create mode 100644 crates/vendor/oxideav-core/src/stream.rs create mode 100644 crates/vendor/oxideav-core/src/subtitle.rs create mode 100644 crates/vendor/oxideav-core/src/time.rs create mode 100644 crates/vendor/oxideav-core/src/vector.rs create mode 100644 crates/vendor/oxideav-dts/Cargo.toml create mode 100644 crates/vendor/oxideav-dts/LICENSE create mode 100644 crates/vendor/oxideav-dts/README.md create mode 100644 crates/vendor/oxideav-dts/VENDOR.toml create mode 100644 crates/vendor/oxideav-dts/src/audio_array.rs create mode 100644 crates/vendor/oxideav-dts/src/audio_data.rs create mode 100644 crates/vendor/oxideav-dts/src/audio_header.rs create mode 100644 crates/vendor/oxideav-dts/src/audio_huff.rs create mode 100644 crates/vendor/oxideav-dts/src/aux_data.rs create mode 100644 crates/vendor/oxideav-dts/src/bitreader.rs create mode 100644 crates/vendor/oxideav-dts/src/block_code.rs create mode 100644 crates/vendor/oxideav-dts/src/cos_mod.rs create mode 100644 crates/vendor/oxideav-dts/src/crc16.rs create mode 100644 crates/vendor/oxideav-dts/src/d10_tables.rs create mode 100644 crates/vendor/oxideav-dts/src/d10_vq.rs create mode 100644 crates/vendor/oxideav-dts/src/d6_block_book.rs create mode 100644 crates/vendor/oxideav-dts/src/dmix_coeff.rs create mode 100644 crates/vendor/oxideav-dts/src/drc_range.rs create mode 100644 crates/vendor/oxideav-dts/src/dsync.rs create mode 100644 crates/vendor/oxideav-dts/src/filter_bank.rs create mode 100644 crates/vendor/oxideav-dts/src/fir_coeff.rs create mode 100644 crates/vendor/oxideav-dts/src/header.rs create mode 100644 crates/vendor/oxideav-dts/src/inverse_adpcm.rs create mode 100644 crates/vendor/oxideav-dts/src/iter.rs create mode 100644 crates/vendor/oxideav-dts/src/join_scale.rs create mode 100644 crates/vendor/oxideav-dts/src/joint_subband.rs create mode 100644 crates/vendor/oxideav-dts/src/lfe_fir_coeff.rs create mode 100644 crates/vendor/oxideav-dts/src/lfe_interp.rs create mode 100644 crates/vendor/oxideav-dts/src/lfe_synth.rs create mode 100644 crates/vendor/oxideav-dts/src/lib.rs create mode 100644 crates/vendor/oxideav-dts/src/optional_info.rs create mode 100644 crates/vendor/oxideav-dts/src/qmf_assemble.rs create mode 100644 crates/vendor/oxideav-dts/src/qmf_multichannel.rs create mode 100644 crates/vendor/oxideav-dts/src/qmf_synth.rs create mode 100644 crates/vendor/oxideav-dts/src/registry.rs create mode 100644 crates/vendor/oxideav-dts/src/rev2_aux.rs create mode 100644 crates/vendor/oxideav-dts/src/side_info.rs create mode 100644 crates/vendor/oxideav-dts/src/step_size.rs create mode 100644 crates/vendor/oxideav-dts/src/subframe.rs create mode 100644 crates/vendor/oxideav-dts/src/subframe_pcm.rs create mode 100644 crates/vendor/oxideav-dts/src/sum_diff.rs create mode 100644 crates/vendor/oxideav-dts/src/test_util.rs create mode 100644 crates/vendor/oxideav-dts/src/unpack14.rs create mode 100644 crates/vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin create mode 100755 scripts/vendor-oxideav.sh diff --git a/Cargo.toml b/Cargo.toml index e4cada3c..b18bb144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,17 @@ members = [ # Development tool, `publish = false`. Kept out of `vuio-cli` so the crate that # ships the binary does not carry its dependencies. "crates/vuio-bench", + + # The AC-3 / E-AC-3 / DTS decoders, vendored verbatim from github.com/OxideAV + # by `scripts/vendor-oxideav.sh`. Members so cargo resolves the path deps and + # `cargo test -p oxideav-ac3` works; deliberately not default members, so + # their own test and bench targets stay off the release path — the same + # reasoning that keeps `vuio-bench` out, and the reason a plain `cargo build` + # still compiles them (vuio-core depends on them) without building their tests. + "crates/vendor/oxideav-core", + "crates/vendor/oxideav-ac3", + "crates/vendor/oxideav-dts", + "crates/vendor/oxideav-aac", ] # `vuio-bench` is deliberately not a default member. Cargo unifies features across diff --git a/crates/vendor/oxideav-aac/Cargo.toml b/crates/vendor/oxideav-aac/Cargo.toml new file mode 100644 index 00000000..af5f3501 --- /dev/null +++ b/crates/vendor/oxideav-aac/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "oxideav-aac" +publish = false +version = "0.1.6" +edition = "2021" +rust-version = "1.80" +license = "MIT" +repository = "https://github.com/OxideAV/oxideav-aac" +authors = ["Mark Karpeles"] +description = "Pure-Rust AAC-LC decoder and encoder for oxideav — ADTS framing, Huffman books 1-11, IMDCT, M/S stereo, TNS, PNS" + +readme = "README.md" +homepage = "https://github.com/OxideAV/oxideav-aac" +keywords = ["multimedia", "audio", "aac", "codec", "pure-rust"] +categories = ["multimedia::encoding", "multimedia::audio"] + +[dependencies] +oxideav-core = { path = "../oxideav-core" } + +# Vendored verbatim — see scripts/vendor-oxideav.sh. Upstream does not build +# under this repository's `-D warnings`, and making it would mean carrying a +# patch set across every refresh. +[lints.rust] +warnings = "allow" + +[lints.clippy] +all = "allow" diff --git a/crates/vendor/oxideav-aac/LICENSE b/crates/vendor/oxideav-aac/LICENSE new file mode 100644 index 00000000..ffe2468a --- /dev/null +++ b/crates/vendor/oxideav-aac/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Karpelès Lab Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/vendor/oxideav-aac/README.md b/crates/vendor/oxideav-aac/README.md new file mode 100644 index 00000000..62f7fd22 --- /dev/null +++ b/crates/vendor/oxideav-aac/README.md @@ -0,0 +1,1252 @@ +# oxideav-aac + +[![CI](https://github.com/OxideAV/oxideav-aac/actions/workflows/ci.yml/badge.svg)](https://github.com/OxideAV/oxideav-aac/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/oxideav-aac.svg)](https://crates.io/crates/oxideav-aac) [![docs.rs](https://docs.rs/oxideav-aac/badge.svg)](https://docs.rs/oxideav-aac) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +A pure-Rust **AAC** (Advanced Audio Coding) codec for the +[oxideav](https://github.com/OxideAV/oxideav-workspace) framework. + +Every numeric constant, bit layout, and clause reference is sourced from +the staged ISO/IEC 13818-7 and ISO/IEC 14496-3 specifications under +`docs/audio/aac/`. + +## Status + +The crate implements the full AAC-LC decode chain end to end — from +ADTS bitstream parse through the per-tool reconstruction to interleaved +16-bit PCM — **plus the complete §4.6.18 SBR back-end (HE-AAC v1) and +the subpart-8 Parametric Stereo tool (HE-AAC v2)**, and **wires them +into the framework's runtime `Decoder` trait** (`register()` installs +an AAC decoder under id `"aac"`; see `codec_decoder` below). The PCM +is validated byte-exactly (within the 1-LSB IMDCT-rounding bound) +against the staged `expected.wav` corpus — **including the HE-AAC v1 +SBR fixture, which decodes 99.98% sample-exact with a max error of +1 LSB** at the doubled output rate, and the **HE-AAC v2 fixture, whose +PS stereo reconstruction lands at a 5e-5 per-channel error-to-signal +RMS** against the reference decode. The §4.6.18.4.3 **downsampled** +output mode (core-rate SBR, auto-selected from a core-rate +`extensionSamplingFrequency` ASC) and the §4.6.18.8 **low power** SBR +tool (real-valued filterbanks + aliasing detection/reduction) are +selectable on every decode entry point. +The crate also ships an **end-to-end AAC-LC encoder** (`encoder` / +`codec_encoder`): PCM → §4.6.11.3.1 forward-MDCT analysis with +§4.6.11.3.2 block switching (transient-driven +`ONLY_LONG → LONG_START → EIGHT_SHORT → LONG_STOP`, short frames +grouped per §4.5.2.3.4 on the band-envelope similarity of adjacent +windows), the exact §4.6.2 inverse quantizer under a masking-spread +psychoacoustics-lite model with a bidirectional rate loop, +measured-bit-cost codebook/section choice (a DP over section +boundaries priced with the real tuple writer), per-band §4.6.8.1 +M/S joint stereo (long frames per sfb, short frames per +`(window group, sfb)` under the pair's joint grouping), and +**every Table 1.19 default channel layout** — 1–6 and 8 (7.1) +channels as SCE / `common_window`-CPE / §4.5.2.1.3-conforming LFE +element plans — assembled into ADTS through the Phase-2 bit-exact +wire writers. Every stream is round-tripped through the crate's own +decoder (multitone 128 kbps at 0.016 err/sig RMS; staged-fixture +transcodes at 0.0008–0.003; multichannel layouts pinned with one +distinct tone per speaker); `register()` installs the encoder +alongside the decoder under id `"aac"`. + +### ISO/IEC 14496-26 conformance (normative corpus) + +`tests/iso_14496_26_conformance.rs` decodes members of the normative +MPEG-4 Audio conformance corpus end to end against their reference +waveforms (corpus located via `OXIDEAV_ISO_14496_26_DIR`, +skip-if-absent; sourcing, per-member checksums and the member-level +fetch recipe are in `docs/audio/aac/iso-14496-26-conformance.md` — the +ISO-copyright bitstreams are never committed). Measured state: + +* **ER AAC LD** — 15 vectors across 22.05/24/32/44.1/48 kHz at both + frame lengths: **47 003 / 47 004 access units decode** (the single + residual is `er_ad1103_22_ep0` AU 367, which the staged corpus + screen records as failing under every width hypothesis). PCM: the + LD-512 `er_ad1000*` family is reference-exact at err/sig ≈ 4.4e-5; + the LD-480 `er_ad1103np*` family lands at ≈ 1.3e-4 outside its + TNS/PNS access units (PNS noise phase is generator-defined; the + deployed LD TNS record is a still-untraced extra-spec wire — see + "Not yet supported"). The 32 kHz members pin the §4.5.4 + Tables 4.144/4.145 band tables end to end. +* **CCE** — the twelve `am05_*` vectors (AAC Main + one + `coupling_channel_element()` in every access unit): **1 370 / 1 370 + AUs decode**, and all six `am05_48` output channels match their + per-speaker references at ≈ 1e-4 err/sig — pinning the CCE gain + path (conformance-settled `cc_scale^(−ge)` exponent), the §4.6.6 + Main-profile predictor at its normative fixed-precision arithmetic, + M/S + intensity + TNS interplay, and the §8.5.2.2 PCE reorder. +* **SBR-CRC** — the four `al_sbr_{e,i}_32_*` vectors (the corpus's + only `EXT_SBR_DATA_CRC` carriers): **1 600 / 1 600 payload CRCs + verify** during a full decode, including the §4.5.2.8.1 pre-header + prefix (upsampling-only state) and the whole-payload coverage + region (`bs_fill_bits` included). + +### Bitstream parsing + +- **ADTS fixed header** (`adts`) — ISO/IEC 13818-7 §1.A.2: sync, + profile, sampling-frequency index, channel configuration, frame + length, raw-data-block count, CRC presence flag. +- **ADTS `error_check()` + SBR CRC verification** (`adts_crc`) — the + ISO/IEC 13818-7:2004 §8.1.1.1 protected-bit region walk (all 56 + header bits; the first 192 bits of every SCE / CPE / CCE / LFE with + the 3-bit `id_syn_ele` excluded and zero-padding of short elements; + the additional first-128-bits of every CPE's *second* + `individual_channel_stream`; all PCE / DSE bits) fed into the + ISO/IEC 11172-3 §2.4.3.1 CRC-16 (`0x8005`, all-ones init) that + 13818-7 §8.1.1.2 cites. Both frame forms verify: the Table 1.A.8 + single-raw-data-block `crc_check` and the Table 1.A.9 / 1.A.10 + multi-RDB split (headers + `raw_data_block_position` table under + one CRC, one CRC per block). Wired into + `StreamDecoder::decode_adts_frame` / `decode_all` and the runtime + `Decoder`; `protect_adts_frame` / `protect_adts_stream` produce the + protected form (a protected rewrite of a staged fixture decodes + byte-identically through a black-box validator binary — which, + notably, does not verify the CRC *value*, so the code convention is + pinned to the documented §2.4.3.1 parameters). The same module + hosts the SBR `bs_sbr_crc_bits` CRC-10 (`G10`, zero init) computed + over the Table 4.62 coverage region; the FIL walk verifies every + `EXT_SBR_DATA_CRC` payload. Corruption of any covered bit surfaces + `Error::AdtsCrcMismatch` / `Error::SbrCrcMismatch`; fill bits and + the beyond-window element bits are provably uncovered. +- **AudioSpecificConfig** (`asc`) — ISO/IEC 14496-3 §1.6.2.1 including + the §4.4.1 GASpecificConfig body for all General Audio object types, + the hierarchical SBR (AOT 5) / PS (AOT 29) wrappers, the + `extensionFlag` subtree, the `epConfig` field, and the Table 1.15 + trailing `syncExtensionType == 0x2b7` implicit-SBR probe. A + carrier-bounded `parse_bits_bounded` entry point is exposed for LATM + `StreamMuxConfig` callers. +- **program_config_element** (`pce`) — §4.4.1.1, used standalone and + inline inside `asc`. +- **raw_data_block()** walker (`raw_data_block`) — §4.4.2.1: visits each + `id_syn_ele` and stops at `END`. FIL / DSE / PCE bodies are fully + consumed; the channel-element body is composed by the modules below. +- **Channel-element body** (`ics_body`) — Table 4.50: `global_gain` → + `ics_info` → `section_data` → `scale_factor_data` → optional + `pulse_data` / `tns_data` / `gain_control_data`, surfacing the start + bit-offset for the spectral data. +- **spectral_data()** (`spectral_data`) — Table 4.56 wire walker and + bit-exact writer, dispatching onto the Huffman codebooks. +- **extension_payload()** (`extension_payload`) — §4.4.2.7 / Table 4.51 + parser + encoder for the `EXT_FILL`, `EXT_FILL_DATA`, and + `EXT_DYNAMIC_RANGE` branches. The two SBR-data extension types + decode through the `parse_with_sbr` entry (feeding the §4.6.18 + back-end); the plain `parse` entry without an SBR context rejects + them (`Error::UnsupportedExtensionSbr`). +- **Error-protection CRC generator** (`crc`) — §1.8.4.5: the full + family of MPEG-4 Audio CRC generation polynomials (`CRC4`..`CRC32`, + including the `CRC8` LATM `StreamMuxConfig()` `crcCheckSum` and the + 16-bit `x¹⁶+x¹⁵+x²+1`), a zero-init MSB-first shift-register + (`crc_bits` / `crc_bytes`) implementing the §1.8.4.5 + `M(x)·xᵏ = Q(x)·G(x) + R(x)` remainder with the normative + output-bit inversion ("written in a reversed manner, i.e. each bit + is inverted"), and the [`crc::stream_mux_config_crc`] LATM helper. + Cross-checked against an independent GF(2) long-division reference + and the codeword-divisibility property. The ADTS + `adts_error_check()` region-selection CRC uses a different code + convention (ISO/IEC 11172-3 §2.4.3.1, all-ones init, no output + inversion) and lives in the dedicated `adts_crc` module above. +- **RVLC error-resilient scalefactor coding** (`rvlc`, + `scale_factor_data::ErScaleFactorData`) — §4.6.16.2 the + reversible-variable-length-coding replacement for the §4.6.3 + noiseless coding of scalefactors, used when + `aacScalefactorDataResilienceFlag == 1`. The `rvlc` module + transcribes the symmetric (bit-palindrome) RVLC codebook + (Table 4.166, deltas `-7..=+7` with `±7` the `ESC_FLAG`), the eight + asymmetric *forbidden* codewords (Table 4.167) whose appearance is + surfaced as the §4.6.16.2.1 in-band error-detection event, and the + 54-entry RVLC-ESC Huffman codebook (Table 4.168) — every codebook + proven prefix-free and round-tripping, and independently + cross-validated against the staged packed binary-tree node tables. + `ErScaleFactorData::parse` / `::write` decode and re-encode the whole + Table 4.53 RVLC branch: the `sf_concealment` / `rev_global_gain` / + `length_of_rvlc_sf` (11 bits for `EIGHT_SHORT_SEQUENCE`, else 9) + header, the RVLC base-delta band loop (first PNS band keeping the + 9-bit PCM seed), the optional `sf_escapes_present` / + `length_of_rvlc_escapes` second pass folding each escape into its + `±ESC_FLAG` base (`+7 + esc` / `-7 - esc` per §4.6.16.2.1), and the + `dpcm_is_last_position` / `dpcm_noise_last_position` backward seeds. + Both `length_of_*` fields are validated against the bits actually + consumed. The reconstructed records share the non-resilient + `ScaleFactorData` shape, so the §4.6.2.3.2 forward DPCM + `accumulate()` pass consumes them unchanged — pinned by a test that + an RVLC stream and the Huffman stream carrying the same deltas + accumulate to identical absolute scalefactors. The + resilience-flag dispatch from `ics_body` is now wired (see the + error-resilient ICS body below); the RVLC bitstream path itself is + decoded end to end. +- **Error-resilient channel-element body** + (`ics_body::IcsBody::parse_er` / `::parse_with_ics_info_er`, + `section_data::SectionData::parse_er` / `::write_er`) — ISO/IEC + 14496-3 §4.4.6 Tables 4.50 / 4.52, the ER General-Audio object types + (AOTs 17 / 19 / 20 / 23). Drives all three resilience branches off + the `AacResilienceFlags` triplet: `section_data()` through the 5-bit + `sect_cb` branch (carrying the §4.6.16.4 virtual codebooks 16..=31, + whose `ESC_HCB` / `>= 16` runs take the fixed `sect_len_incr = 1` + single-band coding) when `aacSectionDataResilienceFlag` is set; + `scale_factor_data()` through the RVLC `ErScaleFactorData` branch + (its reconstruction mirrored into the shared `scale_factor_data` + field so the §4.6.2.3.2 accumulate pass is branch-agnostic, with the + RVLC seeds retained in `er_scale_factor_data`) when + `aacScalefactorDataResilienceFlag` is set; and the + `length_of_reordered_spectral_data` (14-bit) + + `length_of_longest_codeword` (6-bit) HCR length fields in + `reordered_spectral_lengths` when `aacSpectralDataResilienceFlag` is + set. The trailing `reordered_spectral_data()` (HCR) payload is the + caller's responsibility, exactly as `spectral_data()` is on the + non-resilient path. +- **HCR segmentation / pre-sorting scaffold** (`hcr`) — ISO/IEC + 14496-3 §4.6.16.3.3 / §4.6.16.3.5. The deterministic, header-only + half of Huffman codeword reordering: the Table 4.170 `maxCwLen` + table, the §4.6.16.3.3.1 `codebookPriority[32]` table + the + `assignedUnitNr` pre-sorting metric, the + `segmentWidth = min(maxCwLen, length_of_longest_codeword)` + derivation, the §4.6.16.3.2 length-field clamps, and the + `Segmentation` layout that instantiates PCW segments until the + `length_of_reordered_spectral_data` buffer is exhausted (folding the + trailing bits into the last segment). `ReorderPlan::build` then runs + the §4.6.16.3.3.4 `ReorderSpectralData()` writing scheme (PCWs + forward from each segment start, then the non-PCW set / trial loop + with the per-set `ToggleWriteDirection()` and the modulo-shift + `segment = (trial + codewordBase) % numberOfSegments`) to resolve, + for each codeword, the ordered global buffer bit positions + (MSB-first) that carry its bits — pinned by a bijection invariant + (every buffer bit covered exactly once). +- **HCR payload codec** (`hcr_decode`) — §4.6.16.3.3.4 / §4.6.16.3.4, + both directions of the `reordered_spectral_data()` payload itself. + `encode_reordered_spectral_data` enumerates the frame's codeword + units (the §4.5.2.3.2 unit: Huffman codeword + sign bits + escape + sequences, two or four lines) in the §4.6.16.3.3.1 pre-sorted order + — the unit-based window interleave (Table 4.169; the §4.5.2.3.5 + grouping interleave does not apply under HCR) stably ordered by + `assignedUnitNr` — encodes each unit and scatters the bits over the + segment grid via `ReorderPlan`. `decode_reordered_spectral_data` + inverts the walk without transmitted lengths: PCWs decode forward + from their own segment starts, then the non-PCW sets run the same + direction-toggling modulo-shift trial loop, each codeword consuming + segment free-region bits until its Huffman unit completes (prefix + codes make "incomplete" exactly detectable, so lengths are + discovered where the writer defined them). The §4.6.16.4 virtual + codebooks (16..=31) decode as book 11 with their own `maxCwLen` + segment widths. Round-tripped bit-exactly over long / eight-short + (two window groups), spectrum-less-band mixes, escape-bearing book + 11, virtual codebooks, and slack-padded buffers; corrupt payloads + surface errors, never panics. Threading the ER triplet from + `GASpecificConfig` through the stream drivers (the ER top-level + payloads) remains open, and an HCR-bearing conformance stream is + still wanted as an external cross-check. + +### Numeric reconstruction (AAC-LC tool chain) + +- **Spectrum Huffman codebooks 1..=11** (`spectrum_huffman`, + `spectral_codebook`) — the complete Annex 4.A spectrum book set, + including the ESC book 11, with §4.6.3.3 index↔tuple translation and + sign-bit / escape-sequence handling. +- **Inverse quantization + scalefactors** (`dequant`) — §4.6.1.3 + non-uniform inverse quantizer and §4.6.2.3.3 scalefactor gain. +- **Decoded spectrum** (`decoded_spectrum`) — §4.6.3.3 `quant_to_spec()` + de-interleaver plus the per-channel pipeline composing pulse fix-up → + scalefactor accumulation → inverse quantization + rescale → + de-interleave → TNS. +- **TNS** (`tns_data`, `tns_coef`, `tns_frame`, `tns_max`, + `swb_offset`) — §4.6.9 Temporal Noise Shaping: wire parse, + coefficient inverse-quantisation + conversion to LPC, the all-pole IIR + pass, and the per-frame region-slicing orchestration. +- **Filterbank** (`filterbank`) — §4.6.11 stateful per-channel IMDCT + with sine / KBD windows, all four `window_sequence` shapes, eight-short + internal overlap-add, and inter-frame overlap-add. Pinned by streaming + TDAC perfect-reconstruction tests. Covers **all four §4.5.1.1 + frame-length families**: the 1024/128- and 960/120-line + block-switching families (`N = 2048/256` and `1920/240`) and the + long-only ER AAC LD 512/480-line families (`N = 1024/960`), where + the `window_shape == 1` bit selects the §4.6.17.2.3 Table 4.171 + **low-overlap window** in place of KBD (power-complementarity and + streaming TDAC pinned per family). +- **Channel-pair / noise tools** — M/S stereo de-matrix (`ms_stereo`, + §4.6.8.1), intensity stereo (`intensity_stereo`, §4.6.8.2), and + Perceptual Noise Substitution (`pns`, §4.6.13). PNS produces + energy-exact bands; only the per-coefficient phase is RNG-defined per + §4.6.13.3, so its output is not byte-exact against any one decoder — + the staged `docs/audio/aac/pns-gen-rand-vector.md` analysis pins the + normative half (band selection, energy DPCM, measured-energy + normalisation, correlated-CPE same-vector rule — all implemented) + and shows the generator recurrence/seed/threading to be deliberately + unspecified, so cross-decoder PNS checks are energy-domain by + design. +- **Coupling channel element** (`cce`) — §4.6.8.3 / Table 4.8. The CCE + coupling header (`CouplingHeader`: `ind_sw_cce_flag`, + `num_coupled_elements`, the per-target `cc_target_is_cpe` / + `cc_target_tag_select` / `cc_l` / `cc_r` list with the Table 4.153 + shared-vs-split `num_gain_element_lists` derivation, `cc_domain` / + `gain_element_sign` / `gain_element_scale`) and the trailing gain-list + block (`CouplingGains`: per-target `common_gain_element` or per-`(g, + sfb)` `dpcm_gain_element` running-sum lists — the §4.6.8.3.3 + `ind_sw_cce_flag ⇒ common-gain-only` constraint and the embedded-SCE + `sfb_cb` `ZERO_HCB` skip — reusing the §4.A.1 scalefactor Huffman + codebook `hcod_sf`). `CouplingChannelElement` ties the whole Table 4.8 + element (header → embedded `individual_channel_stream(0,0)` body + + spectrum → gain lists) together, and `CouplingGains::cc_gain` computes + the §4.6.8.3.3 `couple_channel()` factor `cc_gain = cc_sign · + cc_scale^gain` (Table 4.154 `cc_scale_table`, implicit list-0 natural + scaling). `CouplingGains::couple_channel` applies the §4.6.8.3.3 + per-band scale-and-add — the spec's group / window-group / sfb / + coefficient loop multiplying the embedded-SCE spectrum by the + per-`(g, sfb)` `cc_gain` and adding it onto a target channel's + window-major spectrum (implicit list 0 in natural scaling, `ZERO_HCB` + bands skipped). **The cross-element application is wired into the + stream decoder**: the raw-data-block walk is two-pass (parse every + channel element, then decode), each CCE's embedded SCE is decoded + through a per-instance-tag `CceDecoder` slot (its own pulse / dequant + / PNS / TNS, plus its own persistent §4.6.11 filterbank for the + independently-switched case), and the `decode_coupling_channel()` + target walk matches `cc_target_is_cpe` / `cc_target_tag_select`, + assigns the Table 4.153 gain lists (shared / left / right / both), + and injects the scaled spectrum at the signalled `cc_domain` stage + (before / after the target's TNS; window-state match enforced) or — + for an independently switched CCE — the scaled time signal after the + target's filterbank. Validated end to end with writer-assembled CCE + streams against the filterbank-linearity identity + `decode([target, CCE]) = decode([target]) + cc_gain·decode([embedded])` + (natural scaling, gain-list ×2, independently-switched, and + CPE-left-only layouts, ≤ 2 LSB stacked-rounding deviation). A + writer-assembled CCE fixture cycling all three coupling shapes + (dpcm + sign split / ind-switched / shared natural, both domains, + PCE-declared `valid_cc_elements`) is staged in the docs corpus + (`aac-cce-writer-assembled`). The two long-standing §4.6.8.3.3 + wire questions are both settled: the **exponent** is negated — + `cc_gain = cc_sign · cc_scale^(−gain_element)` — confirmed by the + ISO/IEC 14496-26 `am05_*` conformance vectors (all three editions + print the positive exponent, which misses every coupled target by + ~1e-1 err/sig), and the **`gain_element_sign` split** follows the + 2001 / 13818-7:2004 `couple_channel()` text as ruled in + `docs/audio/aac/cce-gain-sign-split.md` §3 — `cc_sign` off **each + transmitted dpcm delta** (`1 − 2·(dpcm & 1)`), accumulator fed with + `dpcm >> 1`, and a `common_gain_element` **never** sign-split (the + 14496-3:2009 page prints two conflicting fragments; its + accumulated-value variant is an editorial defect of that edition). +- **Frequency-domain prediction** (`predictor`) — §4.6.6 MPEG-2 + backward-adaptive intra-channel predictor for the AAC **Main** object + type (AOT 1). A bank of second-order lattice predictors (one per MDCT + line up to the §4.6.6.2 `PRED_SFB_MAX` limit) reconstructs + `x_rec = x_est + y_rec` on the signalled bands. Implements the + §4.6.6.3.2.1 lattice `predict()` + LMS adaptation + (`α = 0.90625`, `a = b = 0.953125`), the §4.6.6.3.2.3 + `flt_round_inf()` 16-bit-float rounding applied to every stored state + variable and the predicted value, and the §4.6.6.3.3 reset (the 30 + Table 4.97 cyclic groups + the short-block reset-all). Wired into + `element_decode`: the bank runs every long frame *before* TNS (and is + mutually exclusive with LTP by object type), persisting the + backward-adaptive state across frames. +- **Long-Term Prediction** (`ltp`) — §4.6.7 long-window LTP: the + Table 4.98 coefficient codebook, the §4.6.7.3 `predict()` single-tap + time-domain predictor (`x_est(i) = ltp_coef·x_rec(i − ltp_lag)`) over + a per-channel `x_rec` reconstruction history, the windowed analysis + `MDCT(x_est)` (the §4.6.15.3.3 / §4.6.11.3.1 forward transform, now a + reusable `filterbank` primitive), and the per-sfb + `X_rec = X_est + Y_rec` combination on the bands flagged by + `ltp_long_used`. LTP is restricted to long windows for the AAC LTP + object type (§4.6.7.1, 2009 edition). The ISO/IEC 14496-3:**2001** + short-window synthesis (`LtpState::apply_short_2001`) is also + implemented per the 2001 §4.6.7.3 pseudo-code — per flagged + subwindow, `lag_w = ltp_lag + ltp_short_lag[w]`, the 256-point + windowed `MDCT(x_est)`, and the `X_rec = X_est + Y_rec` add on the + first 8 SFBs — with the one quantity the 2001 text never fixes + (the per-subwindow `x_rec` index origin; see the staged + `docs/audio/aac/short-window-ltp-blocked.md` §5) taken as an + explicit caller parameter rather than an invented convention. The + **ER AAC LD branch is implemented**: the 10-bit lag with the + `ltp_lag_update` repeat state (`ltp_prev_lag`) and the §4.6.7.3 + `M = N/2` lag offset, applied at the LD transform lengths. +- **TNS analysis filter** (`tns_coef::tns_ma_filter`, + `tns_frame::tns_analysis_frame`) — §4.6.7.4.1 / Figure 4.30: the + all-zero (moving-average, FIR) inverse of the §4.6.9.3 all-pole + synthesis filter, `y(n) = x(n) + Σ lpc[k]·x(n−k)`. Run over the same + per-window region walk as `tns_decode_frame`; analysis ∘ synthesis is + the identity over a shared region, which is the §4.6.7.4.1 + noise-shaping invariant. +- **Element decode driver** (`element_decode`) — `ElementDecoder` chains + the whole stack per element: `decode_sce` for SCE / LFE and + `decode_cpe` for a CPE (pulse → dequant → `quant_to_spec()` → M/S → + intensity → PNS → **LTP → TNS** → filterbank), carrying the + per-channel overlap-add tail **and the §4.6.7.3 LTP reconstruction + history** across frames. LTP runs in the §4.6.7.4.1 / Figure 4.30 + block order — long-term synthesis (with the all-zero TNS analysis + filter applied to `X_est`) *before* the §4.6.9 TNS synthesis filter, + so the single synthesis pass shapes the residual while undoing the + analysis on the LTP contribution. + +### Frame-length families — §4.5.1.1 / §4.6.17 (960, LD 512/480) + +All four `frameLengthFlag` frame geometries decode end to end, keyed +by `swb_offset::FrameFamily` (resolved from the AOT + flag; the +LATM/LOAS driver installs it per layer from the ASC, +`StreamDecoder::set_frame_family` serves raw callers — ADTS can only +carry the default 1024-line family): + +- **AAC-LC at 960/120 lines** (`frameLengthFlag == 1`) — the + bracketed "values for 1920/240" columns of Tables 4.129–4.141, the + `N = 1920/240` transform pair with all four window sequences, both + window shapes, grouping, TNS and the full joint-stereo/noise tool + chain; 960 PCM samples per channel per frame. Verified **bit-exact + against a black-box decoder binary** on writer-assembled streams, + and staged with mutation coverage as `aac-lc-960-writer-loas`. +- **ER AAC LD at 512/480 lines** (AOT 23, §4.6.17) — Tables + 4.142–4.147 (with the §4.5.1.1 nearest-defined-table rule for the + rates those tables omit), long-only frames (a non-`ONLY_LONG` + `window_sequence` is rejected, §4.6.17.2.2), the §4.6.17.2.3 + Table 4.171 **low-overlap window** on the `window_shape == 1` bit, + the §4.6.17.2.5 LD `TNS_MAX_BANDS` tables, the §4.6.7 **LD LTP** + branch (10-bit lag, `ltp_lag_update` repeat via a per-channel + `ltp_prev_lag`, `M = N/2` lag offset at the LD transform lengths), + and the Table 4.19 `er_raw_data_block()` element walk shared with + ER AAC LC; 512/480 PCM samples per channel per frame. The LD-512 + geometry (every swb band boundary, both window shapes, the + transform/overlap-add) is verified **bit-exact against two + independent black-box decoder binaries**; LD-480 against one (the + other binary decodes 480-line streams on the wrong 512-line + frequency grid — probed and documented in the fixture notes). + Staged fixtures: `aac-ld-512-writer-loas`, `aac-ld-480-writer-loas`. +- **LD TNS wire — RESOLVED against the conformance corpus**: the + divergence between the literal Table 4.54 / Table 4.155 field + widths and the deployed LD TNS wire was settled by the ISO/IEC + 14496-26 screen recorded in `docs/audio/aac/er-ld-tns-divergence.md` + §0 — the normative LD wire transmits `n_filt` in **1 bit** (the + reduced Table 4.155 column; the literal 2-bit keying hard-fails 792 + of the corpus's 2 017 TNS-bearing AUs). The LD families read the + 1 / 4 / 3 column via `TnsData::parse_family` / `write_family`; + because the corpus never transmits a `length` / `order` field + (`n_filt == 0` throughout, so 4 / 3 vs 6 / 5 is undetermined), the + §0.6 configurability recommendation is kept via the explicit-width + `TnsData::parse_widths` / `write_widths` entry points. +- An SBR payload on a 960-line or LD stream is rejected before its + body is parsed (`Error::SbrUnsupportedFrameFamily`) — the §4.6.18 + tool here is defined over the 1024-line core, and the §4.6.19 LD + SBR tool belongs to ELD (out of scope). + +### Scalable AAC (AOT 6) / ER AAC scalable (AOT 20) — §4.4.2.2 / §4.5.2.2 + +The AAC-only scalable combinations decode end to end (`scalable`): +one `aac_scalable_main_element()` plus up to seven extension +elements, each on its own elementary stream / LATM layer +(mono-only, stereo-only and mixed mono→stereo stacks, Table 4.87). + +- **Syntax** — Tables 4.13–4.18: the main/extension headers + (window geometry hoisted out of `ics_info()`, per-channel TNS on + the first mono and first stereo layer, per-channel LTP on the main + layer, the §4.6.8.1.4 *incremental* `ms_data()` over + `last_max_sfb_ms..max_sfb`, per-channel `diff_control_data_lr()` + with the Table 4.18 `ms_used` gating), and the Table 4.50 + `scale_flag == 1` ICS form (`IcsBody::parse_scale` — no inline + `ics_info()`, no tool dispatch trio). For AOT 20 the §4.4.6 + resilience triplet selects the ER wire branches per channel + (5-bit-`sect_cb` sections, RVLC scalefactors, inline HCR + `reordered_spectral_data()`); the element syntax itself is + unchanged (§4.5.2.4), pinned bit-identical to the AOT-6 decode of + the same spectra. `ScalableFrame::parse` / `::write` round-trip + the whole per-layer payload stack byte-exactly. +- **Layer combination** (§4.5.2.2.4 SIAQ, `ScalableDecoder`): the + dequantized spectra of all layers sum per output path under the + Table 4.91–4.93 per-band tool rules — a lower layer's PNS band + survives only while every higher layer decodes the band to zero + (§4.6.13.6), intensity accumulates the M/L channel with positions + from the highest layer, invalid combinations surface + `Error::ScalableLayerCombination` — then the §4.6.14.2.1 FSS merges + the combined mono spectrum into the stereo pair (`L/R += 2·M''` on + clear `diff_control_lr` bits, `M = M'' + M'` on M/S bands; long + and short windows), the cumulative-mask M/S butterfly, the + scalable-invariant intensity reconstruction + (`invert_intensity() = +1`), correlated PNS via `ms_used`, the + §4.6.9.5 / Table 4.158 **serial TNS** layout (first mono layer's + filter serves the low bands up to the highest mono `max_sfb`, + first stereo layer's filters serve L/R, with the lower-boundary + override rule) and the §4.6.11 filterbank. §4.6.7.5 **base-layer + LTP** runs on the lowest layer only, its reconstruction history + fed by a parallel first-layer-alone synthesis chain (pinned by a + history-isolation test). Both the 1024- and 960-line families. +- **Transport**: the LATM/LOAS driver recognises AOT-6/20 layers, + collects each program's layer payloads per access unit and decodes + them combined through a persistent per-program `ScalableDecoder` + (`ScalableConfig::from_layer_ascs` validates the layer stack; + `dependsOnCoreCoder == 1` — a CELP core — and TwinVQ lower layers + are out of scope, `Error::ScalableUnsupportedCore`). +- Single-layer scalable streams are pinned **bit-identical** to the + equivalent SCE / common-window CPE decodes; multi-layer stacks are + pinned against references composed from the crate's own + reconstruction primitives; every branch carries a deterministic + bit-flip / truncation battery (`tests/scalable_*.rs`). + +### Error protection (EP) tool — §1.8 + +The MPEG-4 Audio unequal-error-protection layer, from the out-of-band +configuration to the LOAS EP carrier (`ep_config` / `ep_fec` / +`ep_rs` / `ep_frame`): + +- **`ErrorProtectionSpecificConfig()`** (Table 1.49) — parse + + bit-exact write with reserved-field rejection, the §1.8.4.2 + `class_optional` expansion (pinned against the spec's own + Table 1.57/1.58 example), and ASC integration: `epConfig == 2 / 3` + now parse the inline config and the `directMapping` bit. +- **SRCPC** (§1.8.4.6) — the rate-1/4 systematic recursive + convolutional encoder (Figure 1.10 equations), the Table 1.61 + puncture family 8/8..8/32, §1.8.4.6.2 termination (the `u = d` + tail rule, proven identical to the whole Table 1.60 listing) and a + hard-decision 16-state Viterbi decoder correcting channel errors. +- **In-band header FEC** (§1.8.4.3 Table 1.59) — majority, BCH(7,4), + BCH(15,7), Golay(23,12), BCH(31,16) with the normative generators + and bounded-distance correction; CRC4 + terminated SRCPC 8/16 for + 17+ bits; the extended `header_protection` path. +- **Shortened Reed-Solomon** (§1.8.4.7) — `SRS(255−l, 255−2k−l)` + over the spec's GF(2⁸) (`m(x) = x⁸+x⁴+x³+x²+1`; the generated + antilog table is pinned against Table 1.62 rows), the part split + with zero-padded last part, lowest-order-first parity, and the + syndrome / Berlekamp-Massey / Chien / Forney correction chain + (`k` byte errors per part corrected, `k+1` rejected). +- **`ep_frame()`** (§1.8.2.2, `EpFrameCodec`) — the FEC-protected + `choice_of_pred` + `class_attrib()` header (in-band Table 1.55 + rate / Table 1.56 CRC escapes, `num_stuffing_bits`), per-class + §1.8.4.5 CRC (the family now reaches down to CRC1) + SRCPC / SRS + protection, §1.8.4.4 RS chains, the "until the end" class-length + recovery (§1.8.4.1), §1.8.4.9 class-reordered transmission, and + the §1.8.4.8 recursive interleaver (`k = m·D + min(m, d) + n`; + bitwise for SRCPC, bytewise for RS) in modes 0 / 1 / 2 with the + per-class mode-2 `interleave_switch`. Encode ↔ decode round-trips + across the configuration matrix; errors are corrected through the + whole frame; a full bit-flip battery never panics. Two + under-specified corners (an escaped rate on an RS class; byte-wise + interleave over a non-octet-aligned Y stream) are rejected rather + than guessed. +- **LOAS EP carrier** (§1.7) — the `EPAudioSyncStream()` BCH(36,18) + `headerParity` (§1.7.2.2.2 generator; generate + verify), + `EPMuxElement(1, 1)` (majority-protected `epUsePreviousMuxConfig`, + Golay-protected `epSpecificConfigLength`, Table 1.59-protected + inline config with threaded reuse), and + `LoasDecoder::decode_all_ep` — the recovered `ep_frame()` class + concatenation is the plain `AudioMuxElement()` bit stream + (§1.7.3.2.1: the sensitivity-category instances ride in syntax + order), so payloads (scalable programs included) ride the + existing decode paths. A writer-assembled EP stream decodes + **byte-identical** to its plain LOAS equivalent and survives + correctable channel errors. + +### SSR gain control (§4.6.12) — complete decode pipeline + +The §4.6.12 SSR (Scalable Sample Rate, AOT 3) gain-control tool is +implemented **end to end** — front-half filterbank, gain +reconstruction, and IPQF synthesis — and wired into the decode driver +(ADTS profile 2 routes every SCE / CPE channel through it), validated +independent of any external SSR implementation: + +- **Gain-control reconstruction** (`gain_control`) — §4.6.12.3.1–3. The + §4.6.12.3.1 gain-control data decoding (the Table 4.108 `AdjLoc()` = + `8·AC` and Table 4.109 `AdjLev()` = `AV − 4` tables, the `NADW` / + `ALOC` / `ALEV` ladder with the step-(3) `ALOC(0)=0` / `ALEV(0)` rule + and the step-(4) per-window-sequence endpoint), the §4.6.12.3.2 + gain-control function setting (the `M_{W,B,j}` index, the `FMD` + fragment-modification function with the `Inter(a,b,j)` geometric-blend + ramp, the per-sequence `GMF` composition threading the cross-frame + `PFMD`, and the inversion `AD(j) = 1/GMF(j)`), and the §4.6.12.3.3 + windowing + overlapping (`GainBandState::window_overlap` applies + `T = AD·U` then overlap-adds per `window_sequence` into the band sample + data `V_B`, threading the cross-frame `PT_B` tail). All four + `window_sequence` shapes are covered; the spec initial values + `PFMD ≡ 1.0` / `PT ≡ 0.0` are honoured, and the input-read vs + produced `PFMD` lengths (which differ per sequence) are tracked + separately with a persistent 256-entry carry. +- **IPQF synthesis filter** (`ipqf`) — §4.6.12.3.4. The Table 4.110 + length-96 prototype `Q(j)` (the symmetric `Q(j) = Q(95 − j)` half + mirrored to 96), the cosine modulation + `Q_B(j) = Q(j)·cos((2B+1)(2j−3)π/16)`, the 4× upsampling + `Ṽ_B(j) = V_B(j/4)`, and the streaming convolution + `AS(n) = Σ_B Σ_j Q_B(j)·Ṽ_B(n−j)` as a polyphase bank (`Ipqf`) that + retains a 24-deep per-band history across frames — pinned by an + impulse-response test against the direct §4.6.12.3.4 convolution + (`AS(n) = Q_0(n)`). +- **Per-channel driver** (`ssr`) — `SsrGainControl::decode_frame` + composes the four-band `GainBandState` and the `Ipqf` into one + persistent per-channel pipeline: the four per-band IMDCT outputs + `U_{W,B}` plus the decoded `gain_control_data()` → the §4.6.12.3.3 + per-band windowing/overlap → the §4.6.12.3.4 IPQF synthesis → the PCM + `AS(n)` (1024 samples/frame for the steady `ONLY_LONG` / `EIGHT_SHORT` + case). PQF band 0 is never gain-controlled. + +- **Front-half filterbank** (`ssr_filterbank`) — §4.6.12.1 / + 13818-7 §16.1, closing the previously docs-gapped spectrum→band + mapping: the frequency-ascending spectrum splits into four + *contiguous* PQF-band quarters (the PQF's band `B` covers the `B`-th + quarter, Annex C.2.1.1), the "even" bands — the spec's ordinal + 2nd/4th, i.e. 0-based 1 and 3, exactly the bands the ×4 decimation + spectrally inverts — are reversed, and each band runs a 256-line + (long) / 8 × 32-line (short) IMDCT under the quarter-scale + §4.6.11.3.2 window geometry (`N_l/N_s = 512/64`; the KBD windows are + generated with the α = 4 / α = 6 running-sum construction and pinned + against the normative Table 4.A.14 / 4.A.13 listings). The split + + reversal convention is pinned by a tone-placement test against the + Annex C.2.1.1 analysis-PQF definition. +- **Per-channel pipeline + driver wiring** (`ssr::SsrChannelDecoder`, + `element_decode`) — the complete spectrum → PCM chain (front half → + §4.6.12.3 gain compensation/overlap → IPQF), replacing the §4.6.11 + filterbank for AOT 3 in the decode driver (per-channel-slot state, + `gain_control_data()` from the channel body; note the §4.6.12.3.3 + variable frame lengths — 1472 / 576 PCM samples for `LONG_START` / + `LONG_STOP`). Validated by full round-trip tests against the Annex + C.2.1.1 analysis PQF: steady long frames and a complete + window-transition chain reconstruct at err/sig < 1e-3 (the PQF + pair's near-perfect-reconstruction bound, both window shapes), gain + ladders applied encoder-side cancel end to end, and an + ADTS-profile-2 stream decodes through the public `StreamDecoder` + (mono + stereo). + +### SBR bitstream decode (HE-AAC) + +The full SBR side-info path is now decoded from the `extension_payload` +SBR element down to the reconstructed quantized envelope / noise-floor +scalefactors — every numeric table sourced from the ISO/IEC 14496-3 +spec PDF (the §4.A normative Huffman grids and the §4.4.2.8 syntax +tables), independent of any external SBR table extraction. + +- **SBR Huffman codebooks** (`sbr_huffman`) — §4.A.6.1, all ten + normative envelope / noise codebooks (Tables 4.A.79–4.A.88) + transcribed from the spec codeword grids and validated complete + + prefix-free. `sbr_huff_dec()` reads MSB-first and returns the signed + DPCM delta (`index − LAV`); `env_tables()` / `noise_tables()` pick the + `(t_huff, f_huff)` pair from the §4.6.18.3 coupling / channel / + `bs_amp_res` selection (the freq-direction noise tables alias the + 3.0 dB envelope freq tables per Table 4.A.78 Note 2). +- **`sbr_header()`** (`sbr_header`) — §4.4.2.8 Table 4.63: the + fixed-width header plus the two optional extra blocks, with the + Table 4.63 Note 3 defaults (Tables 4.105–4.111) applied when an extra + flag is clear. `band_geometry_changed()` flags a §4.6.18.3.3 reset, + and `derive_bands()` chains into the band-setup pipeline below. +- **`sbr_grid()` / `sbr_dtdf()` / `sbr_invf()`** (`sbr_grid`) — + §4.4.2.8 Tables 4.69–4.71: all four `bs_frame_class` layouts (FIXFIX + / FIXVAR / VARFIX / VARVAR) with the envelope count, variable / + relative borders, `ptr_bits = ceil(log2(num_env + 1))` pointer, + reversed FIXVAR freq-res order, single-envelope FIXFIX `bs_amp_res` + override, and `bs_num_noise` derivation; the delta-direction flags; + and the per-noise-band 2-bit inverse-filtering modes. +- **`sbr_envelope()` / `sbr_noise()`** (`sbr_envelope`) — §4.4.2.8 + Tables 4.72–4.73: the raw `bs_data_*` delta arrays, with the + fixed-width absolute start value (5/6/7-bit per the coupling / + channel / `bs_amp_res` context; noise always 5-bit) and the + frequency- vs time-direction Huffman deltas, over `NHigh` / `NLow` + envelope bands and `NQ` noise bands. +- **Envelope / noise DPCM reconstruction** (`sbr_reconstruct`) — + §4.6.18.3.5: inverts the delta coding to the quantized scalefactors + `E_Q(k,l)` / `Q(k,l)`. Frequency deltas accumulate from the start + value; time deltas add to the reference envelope (previous in-frame, + or the prior frame's last envelope for `l == 0`) with the `i(k)` + high↔low band remap when the reference resolution differs; the + coupled second channel's `δ = 0.5` is applied as an integer ×2 on the + even transmitted values, threading cross-frame state. +- **Element framing** (`sbr_element`) — §4.4.2.8 Tables 4.65 / 4.66 / + 4.74: `SbrElement::parse_single` / `parse_pair` decode a whole SBR + data element in spec order — the optional `bs_data_extra` field, the + per-channel grid / dtdf / invf / envelope / noise blocks (coupled + shared-grid vs. independent-grid layouts, second coupled channel in + balance mode), the `sbr_sinusoidal_coding()` add-harmonic flags, and + the `bs_extended_data` block (id + raw body captured for a later PS + pass). The single-envelope FIXFIX `bs_amp_res` override is applied + before envelope decode. +- **`sbr_extension_data()`** (`sbr_extension`) — §4.4.2.8 Table 4.62: + the top-level walker that ties the header + element framing into a + whole SBR extension payload, in spec order — the optional 10-bit + `bs_sbr_crc_bits` (for the `EXT_SBR_DATA_CRC` type), the + `bs_header_flag` + `sbr_header()`, then `sbr_data(id_aac, bs_amp_res)` + dispatching onto `parse_single` (ID_SCE) / `parse_pair` (ID_CPE) with + the band tables derived from the active header at the SBR internal + rate (`FsSBR = 2·core`), and the trailing `bs_fill_bits` alignment + (`num_align_bits = (8·cnt − 4 − num_sbr_bits) % 8`). A clear + `bs_header_flag` reuses the threaded previous header (the + non-scalable core fixes `sbr_layer == SBR_NOT_SCALABLE`, so the flag + is always present). Reachable from the natural FIL entry point via + `extension_payload::ExtensionPayload::parse_with_sbr`, which routes + the SBR extension types here (the default `parse` still rejects them, + keeping the byte-exact AAC-LC corpus path untouched). + +The SBR *bitstream* side info is decoded end to end — CRC field, +header, element framing, band tables, and envelope / noise DPCM +reconstruction — and the **back-end DSP is now implemented too** (see +the next section). + +### SBR back-end (HE-AAC v1) — §4.6.18 + +The complete SBR reconstruction chain, from the core decoder's time +signal to dual-rate PCM, validated **99.98% sample-exact (max error +1 LSB)** against the staged HE-AAC v1 `expected.wav`: + +- **QMF filterbanks** (`sbr_qmf`) — §4.6.18.4 / Figures 4.42–4.44: the + Table 4.A.89 640-tap prototype window (transcribed digit-for-digit + from the spec PDF), the 32-band complex analysis bank, the 64-band + real-output synthesis bank (dual-rate), and the downsampled + 32-channel synthesis variant. Pinned by near-perfect-reconstruction + properties (< 1e-4 error ratios). +- **Dequantization + stereo decoding** (`sbr_dequant`) — §4.6.18.3.5: + `EOrig = 64·2^(E/a)`, `QOrig = 2^(6 − Q)`, and the coupled-pair pan + split with `panOffset = [24, 12]` (energy-sum-preserving). +- **Time / frequency grid** (`sbr_time_grid`) — §4.6.18.3.3: the + `tE` / `tQ` border vectors for all four frame classes, the + Table 4.174 `middleBorder` and the Table 4.176 `lA`. +- **HF generation** (`sbr_hf_gen`) — §4.6.18.6: the Figure 4.48 patch + construction, the covariance-method second-order inverse filtering + (`εInv = 1e-6`, `|α| ≥ 4` reset), the Table 4.175 chirp-factor + blend, and the patched `XHigh` generator. +- **Limiter band table** (`sbr_limiter`) — §4.6.18.3.2.3 / + Figure 4.41, fed by the patch borders (closing the previously + deferred limiter-table item). +- **Envelope adjustment** (`sbr_env_adjust` + `sbr_noise_table`) — + §4.6.18.7: mapping, `ECurr` estimation (both `bs_interpol_freq` + regimes), amplitude-domain gains (the spec PDF's typeset equations + carry square roots the plain text layer drops), the limiter / + boost compensation, `hSmooth` smoothing with cross-frame tails, the + Table 4.A.91 noise table with the running `fIndexNoise`, and the + sinusoid injection with the `(−1)^(m+kx)` alternation. +- **Frame driver** (`sbr_decoder`) — §4.6.18.5 / Figure 4.47: the + `tHFGen = 8`-slot `XLow` history, header-reset handling, the + `lTemp` splice of the previous frame's `Y'`, the coupled-pair invf + sharing, and the pure-upsampling path for SBR-less frames. +- **Stream wiring** (`decode`) — the ADTS `StreamDecoder` walks FIL + extension payloads via `extension_payload::parse_with_sbr`, attaches + each SBR payload to its preceding SCE / CPE, threads the + `sbr_header()` reuse state per element slot, and (once SBR-active) + emits every frame at the doubled rate — 2048 samples/channel — with + SBR-less frames upsampled so the output rate never flaps. The + runtime `Decoder` trait surfaces the dual-rate frames unchanged + (pinned byte-identical to the raw `StreamDecoder`). +- **Downsampled output mode** (§4.6.18.4.3) — selectable end to end: + `SbrDecoder::set_downsampled` / `StreamDecoder::set_sbr_downsampled` + / the `sbr_downsampled` codec option run the 32-channel synthesis + bank so an SBR-active stream is emitted at the *core* rate (1024 + samples per channel per frame; the SBR range above the core Nyquist + is discarded by construction, the bands below it are kept). The + LATM driver selects the mode automatically when an explicitly + signalled ASC carries `extensionSamplingFrequency == + samplingFrequency` (the §4.6.18.2.6 in-band core-rate declaration), + and PS composes (stereo through two downsampled banks). Validated + on the HE-AAC v1 fixture at **1.8e-4** per-channel err/sig RMS + against a band-limited 2:1 decimation of the reference decode + (v2 PS at 1.95e-4), byte-identical between the LATM and forced-ADTS + paths. +- **Low power SBR tool** (§4.6.18.8) — selectable end to end + (`SbrDecoder::set_low_power` / `StreamDecoder::set_sbr_low_power` / + the `sbr_low_power` codec option; composes with the downsampled + output): the §4.6.18.8.2 real-valued filterbank trio (`sbr_qmf`), + the §4.6.18.8.3 aliasing detection (`sbr_lp` + the reflection + coefficients in `sbr_hf_gen`: the Figure 4.53 degree walk, the + patch-carried `degPatched`, the Figure 4.54 gain groups), the + §4.6.18.8.4 ×2 energy estimation, and the §4.6.18.8.5 aliasing + reduction (`GLimBoost → GA`, exact group-energy restoration), + no-smoothing rule, modified real-valued sinusoid injection + (−0.00815 neighbour correction, first-16 rule, `kx − 1` / `kx + M` + spill) and modified `X` assembly. Validated on the HE-AAC v1 + fixture: sub-crossover content at **9e-5** err/sig RMS against the + reference with per-frame full-band energy within 0.05% (the + real-valued HF path is energy-normative, not phase-normative). A + PS payload in this mode is rejected (`Error::SbrLowPowerPs` — the + subpart-8 tool needs the complex QMF domain). All four mode + combinations survive a deterministic corruption battery + (`tests/sbr_mode_mutations.rs`). + +### SBR frequency band setup (HE-AAC) + +- **SBR frequency band tables** (`sbr_freq_bands`) — §4.6.18.3.2 the + static, header-only half of the Spectral Band Replication band setup, + computed directly from the closed-form spec algorithm (no QMF back-end + required): + - `k0` / `k2` — §4.6.18.3.2.1 the low and high QMF subband + boundaries. `k0 = startMin + offset(bs_start_freq)` with the + per-`FsSBR` `offset` table and the `startMin = NINT(c·128/FsSBR)` + thresholds; `k2` covers the `bs_stop_freq < 14` `stopDkSort` + accumulation path and the `bs_stop_freq == 14 / 15` + `min(64, 2·k0)` / `min(64, 3·k0)` shortcuts. + - `master_table` — §4.6.18.3.2.1 `fMaster`, both the Figure 4.39 + linear path (`bs_freq_scale == 0`, the `dk`/`vDk`/`k2Diff` + away-from-zero correction walk) and the Figure 4.40 warped path + (`bs_freq_scale > 0`, the `bands`/`warp` log-spaced regions with + the single-/two-region split at `k2/k0 > 2.2449` and the + `min(vDk1) < max(vDk0)` smoothing step). + - `HiLoTables::derive` — §4.6.18.3.2.2 the derived `fTableHigh`, + `fTableLow` (the `i(k) = 2k − (1−(−1)^NHigh)/2` decimation), and + `fTableNoise` (the `NQ = max(1, NINT(bs_noise_bands·log2(k2/kx)))` + band count plus its `i(k)` recursion), along with the `M` and + `k_x` outputs every later SBR stage keys off. + - The §4.6.18.3.6 requirements are enforced (`k2 > k0`, + `numBands > 0`, `vDk > 0`, `bs_xover_band < NMaster`), surfacing + `Error::SbrFreqBandInvalid` on violation. The §4.6.18.3.2.3 + limiter band table is out of scope here — its `bs_limiter_bands > + 0` path consumes the §4.6.18.6 patch borders that need the QMF + patching back-end. + +### Parametric Stereo (HE-AAC v2) — subpart 8 / Annex 8.A + +The complete §8.6.4 PS tool, reconstructing a stereo image from the +mono SBR signal, validated **5e-5 per-channel error-to-signal RMS** +against the staged HE-AAC v2 MP4 fixture (filterbank-rounding level): + +- **Bitstream** (`ps_data`, `ps_huffman`) — §8.4.2 Tables 8.9–8.14: + the persistent `enable_ps_header` configuration, FIX/VAR framing + (Table 8.29), per-envelope IID/ICC/IPD/OPD delta rows on all ten + Annex 8.B codebooks (each verified a complete prefix code; the six + IID/ICC books cross-checked leaf-for-leaf against the staged + `ps-huffbook-*.csv` trees), and the §8.5.2 time/frequency DPCM + resolution with range checks and modulo-8 phase wrap. +- **Hybrid filterbank** (`ps_hybrid`) — §8.6.4.3: both configurations + (71 / 91 sub-subbands) on the Table 8.37/8.38 13-tap prototypes, + with the Figure 8.20 merge/reorder, the odd-QMF-band inversion, and + the Annex 8.A.3 zero-delay alignment (6 look-ahead `XLow` slots + 6 + history slots). Analysis→synthesis reconstructs exactly. +- **De-correlation** (`ps_decorr`) — §8.6.4.5: the 3-link complex + all-pass chain behind `z⁻²·φ_fract`, the Table 8.40/8.41 centre + frequencies, the 14-/1-slot delays above `NR_ALLPASS_BANDS`, and + the transient duck (peak decay / smoothing / γ = 1.5) per stereo + band, with the Annex 8.A.3 partial + full resets. +- **Stereo processing** (`ps_stereo`, `ps_map`) — §8.6.4.6: Table + 8.25/8.26/8.28 dequantization (cross-validated against the staged + Q30 tables), mixing procedures Ra and Rb, IPD/OPD three-position + smoothing, the Table 8.48/8.49 `b(k)` maps + conjugate channels, + the Table 8.45/8.46 10↔20↔34 re-mappings, and the §8.6.4.6.4 + border interpolation with hold semantics. +- **Frame driver + wiring** (`ps_decoder`, `sbr_decoder`) — Annex + 8.A: inactive (mono) until the first header'd `ps_data()`, + parameter hold over payload-less frames, band-count switches, and + the per-frame de-correlator reset above `k_x + M`. A PS-carrying + SCE renders stereo through two synthesis banks in both the + SBR-processed and pure-upsampling paths, end to end through the + ADTS / LATM / raw `StreamDecoder` entries and the runtime + `Decoder`. + +### LATM / LOAS transport framing + +The §1.7 low-overhead transport layer is now decoded from the LOAS +sync frame down to the recovered MPEG-4 Audio access units — every +field sourced from the ISO/IEC 14496-3 §1.7 syntax tables. + +- **`StreamMuxConfig()`** (`latm::StreamMuxConfig`) — §1.7.3.1 + Table 1.42 plus `LatmGetValue()` (Table 1.43). Decodes the whole + multiplex configuration: the `audioMuxVersion` / `audioMuxVersionA` + version flags (with the `audioMuxVersion == 1` `taraBufferFullness` + and per-ASC length-prefix + `fillBits` extensions), + `allStreamsSameTimeFraming`, `numSubFrames` / `numProgram` / + per-program `numLayer`, and the per-`streamID[prog][lay]` + `LayerConfig` table — each layer carrying its inline + `AudioSpecificConfig()` (parsed via the `asc` module's + `parse_bits` / `parse_bits_bounded` entry points) or the resolved + `useSameConfig` inheritance, the `frameLengthType`, and the type-0 + `latmBufferFullness` / CELP-core `coreFrameOffset` or type-1 + `frameLength`. The `crcCheckSum` is recomputed against the + configuration prefix via the §1.8.4.5 `CRC8` generator and + validated. The reserved `audioMuxVersionA == 1` branch and the + CELP (`3`/`4`/`5`) / HVXC (`6`/`7`) `frameLengthType` values index + frame-length tables for object types this AAC-focused crate does not + decode, so they surface dedicated errors. +- **`AudioMuxElement()`** (`latm::AudioMuxElement`) — §1.7.3.1 + Tables 1.41 / 1.44 / 1.45. Recovers a whole multiplexed element: the + `muxConfigPresent` `useSameStreamMux` branch (inline + `StreamMuxConfig()` vs. inherited previous config), the per-subframe + `PayloadLengthInfo()` + `PayloadMux()` loop over `numSubFrames + 1` + frames (both the `allStreamsSameTimeFraming` program/layer walk and + the `numChunk` chunk layout with its `streamIndx` + `AuEndFlag`), the + `frameLengthType`-0 `MuxSlotLengthBytes` 8-bit-escape byte count and + the `frameLengthType`-1 fixed `(frameLength + 20) * 8` bits, the + `otherData` skip, and the trailing `ByteAlign()`. Each access unit is + returned as a `MuxPayload` carrying the raw §4.4.2.1 + `raw_data_block()` bytes. +- **`AudioSyncStream()` / `EPAudioSyncStream()`** + (`latm::AudioSyncStream`, `latm::EpAudioSyncHeader`) — §1.7.2.1 + Tables 1.36 / 1.37. `AudioSyncStream` scans a LOAS byte buffer for + the 11-bit `0x2B7` syncword, reads the 13-bit `audioMuxLengthBytes`, + and decodes the byte-aligned `AudioMuxElement(1)` body, exposing an + `Iterator` of `LoasFrame`s with the `StreamMuxConfig` threaded across + frames for `useSameStreamMux` inheritance. `EpAudioSyncHeader` + decodes the `EPAudioSyncStream` FEC header (`0x4DE1` syncword, + `futureUse`, `audioMuxLengthBytes`, `frameCounter`, `headerParity`) + and reports the byte-aligned `EPMuxElement` body offset. + +- **`LoasDecoder`** (`latm::LoasDecoder`) — the end-to-end LATM/LOAS → + PCM driver. `decode_all` walks the `AudioSyncStream`, and for every + recovered `MuxPayload` drives the payload's §4.4.2.1 `raw_data_block()` + through the shared `decode::StreamDecoder::decode_raw_data_block` core, + configuring the decode from the layer's `AudioSpecificConfig` (AOT / + `samplingFrequencyIndex` / resolved sample rate). One `StreamDecoder` + is held per `streamID[prog][lay]` so each multiplexed stream's + §4.6.11 overlap / §4.6.7 LTP / §4.6.6 predictor state threads + independently. An SBR-signalling ASC (explicit AOT-5 wrapper or + implicit AAC-LC-only) rides the same §4.6.18 auto-detect the ADTS + path uses and emits dual-rate output, and a PS payload synthesizes + stereo through the Annex 8.A tool. Pinned against the + `aac-latm-stream` fixture + (stereo, 44.1 kHz) to a §8 PCM-RMS error ratio of 0.0004, proven + bit-identical to a hand-fed `decode_raw_data_block` pass, and — for + a re-multiplexed HE-AAC v1 stream (both signalling modes) — + byte-identical to the ADTS decode. + +The runtime `Decoder` (`codec_decoder::AacDecoder`) auto-detects its +carrier on the first packet and routes LOAS packets through `LoasDecoder` +(see "Runtime `Decoder` registration" below). The `EPMuxElement()` EP-tool +payload de-interleave decodes via `LoasDecoder::decode_all_ep` (see +the EP section below). + +### Stream decode + PCM output + +- **Integer-PCM rendering** (`pcm`) — §4.6.11 filterbank `f64` time + signal → 16-bit signed PCM: `nint` (the §1.3 `NINT()` round-half- + away-from-zero operator), `to_s16` (round + saturate), `channel_to_s16`, + and `interleave_s16` (element-order interleave). The conversion is the + only output-rendering step (no resampler / dither), so it is fully + spec-determined. The **canonical channel reorder** for default + `channelConfiguration` layouts (Table 1.19, see `channel_map` below) is + applied to the per-channel buffers *before* this interleave. +- **Default-config channel reorder** (`channel_map`) — ISO/IEC 14496-3 + §1.6.3.5 / Table 1.19. A `raw_data_block()` lists its channel elements + in bitstream order, so the decoder produces channels in element order + (e.g. a 5.1 stream as `C, L, R, Ls, Rs, LFE` for `SCE, CPE, CPE, LFE`); + `channel_map::reorder_channels` permutes them into the canonical + interleaved order that `oxideav_core::ChannelLayout` adopts (the + WAVE_FORMAT_EXTENSIBLE / BS.775 convention — 5.1 becomes + `L, R, C, LFE, Ls, Rs`). The driver threads the signalled + `channelConfiguration` through `decode_raw_data_block` and applies the + reorder for every default config **1–7** — mono / stereo are identity + permutations and config 7 (the Table 1.19 7.1 arrangement: centre + + inner Lc/Rc centre-front pair + outer L/R front pair + surround pair + + LFE) rank-sorts to `L, R, C, LFE, Lc, Rc, Ls, Rs`. **Config 0 + (PCE-defined) layouts are also mapped**: `channel_map::pce_speaker_assignment` implements the + ISO/IEC 13818-7 §8.5.2.2 rules — the front list center-outward + (lone SCE = center, SCE pairs L-then-R, two front pairs = the + Table 42 inner Lc/Rc + outer L/R arrangement), the side list front + to back, the back list outside-in (outer pair = side surround, + inner = rear; a final unpaired SCE = rear center), one LFE — keyed + by `(element kind, instance tag)` so the block's element order is + irrelevant; unmappable shapes fall back to element order. The + decode driver captures an in-band PCE (§8.5.2.2 persistence), + `StreamDecoder::set_program_config` installs an out-of-band + (ASC-inline) one, and the LATM driver does so automatically. The + whole path is validated end to end in `tests/multichannel_mp4.rs` + (a minimal ISO 14496-12 sample-table + esds walk): the 5.1 + config-6 fixture at 2.4e-4–8.9e-4 per-channel err/sig RMS, the + **7.1 PCE fixture at 2e-5–2.9e-4**, and the **hexagonal custom + 6.0 PCE fixture at 2.2e-4–7.8e-4** — every speaker carries a + distinct source tone, so the per-channel ratios pin the mapping + (silent LFEs reproduced exactly). +- **Stream-level ADTS decode driver** (`decode`) — `StreamDecoder` walks + the §4.4.2.1 `raw_data_block()` of each ADTS frame above the + per-element driver, keying one `ElementDecoder` per + `(syntactic-element-id, element_instance_tag)` slot so every element's + §4.6.11 overlap / §4.6.7 LTP / §4.6.6 predictor state persists across + frames, and renders to element-order interleaved s16 PCM. `decode_all` + walks a whole raw-ADTS buffer (ID3v2-skip + `aac_frame_length` + framing). **The decoded PCM is validated against the staged + `expected.wav` corpus**: the two PNS-free ADTS fixtures + (`aac-lc-mono-8000-16kbps-adts`, `aac-lc-intensity-stereo`) are + **99.9% byte-exact** to the reference s16 output with a **max error of + 1 LSB** — the residual is purely the difference between this crate's + `f64` direct-sum IMDCT and a `float32` fast transform. The PNS-bearing + fixtures are compared in the PCM RMS domain (per the fixtures-doc §8), + where the error-to-signal RMS ratio stays below 0.1%; full + byte-exactness on those is precluded by the §4.6.13.3 spec-undefined + noise-phase RNG (energy is normative, phase is not). A + `coupling_channel_element()` (CCE) carried in the block is **decoded + and applied**: the block walk is two-pass, so the §4.6.8.3.3 + coupling contribution lands on the addressed SCE / CPE channels at + the signalled `cc_domain` stage whether the CCE precedes or follows + its targets (see the `cce` bullet above). Multi-`raw_data_block` + frames decode as consecutive 1024-sample blocks (per-block channel + render + time concatenation), and `decode_adts_frame` verifies the + whole §8.1.1 `error_check()` CRC layer when + `protection_absent == 0` (see `adts_crc` above). +- **Runtime `Decoder` registration** (`codec_decoder`) — `AacDecoder` + adapts the persistent `StreamDecoder` / `LoasDecoder` into the + framework's packet-in / frame-out `oxideav_core::Decoder` trait. It + **auto-detects the carrier** on the first packet — the `0xFFF` ADTS + syncword vs. the `0x2B7` LOAS `AudioSyncStream` syncword — and then + routes every later packet the same way: ADTS frames through + `StreamDecoder::decode_frame` (ID3v2-skip + `aac_frame_length` + framing; one or many frames per packet), LOAS packets through + `LoasDecoder::decode_all` (one or many sync frames per packet, with + the `StreamMuxConfig` and per-stream state threaded across packets). + `receive_frame` returns one interleaved-S16 `AudioFrame` (1024 + samples/channel) per decoded access unit, `flush` drains to `Eof`, + and `reset` drops both backends and re-arms carrier detection for a + clean post-seek restart. `register()` installs it under id `"aac"`, + claiming the MP4 object-type `0x40`, WAVEFORMATEX `0x00FF` / `0x1601`, + the `mp4a` / `aac ` FourCCs, and the Matroska `A_AAC` CodecID; the + probe scores a structurally-confirmed ADTS header at 1.0 and a bare + LOAS syncword at 0.9 to win shared tags. Both carrier outputs are + pinned byte-identical to their underlying `StreamDecoder` / + `LoasDecoder`. + +### AAC-LC encoder + +- **`encoder`** — the §4.5/§4.6-written-forward AAC-LC encode chain. + `StreamEncoder` consumes interleaved S16 PCM hop by hop + (`encode_frame` / `finish` / one-shot `encode_all`) and emits one + complete ADTS frame per 1024-sample hop with a 1024-sample encoder + delay. Per hop: an energy-jump transient detector over the + `[hist | cur]` subblock grid drives the §4.6.11.3.2 + `ONLY_LONG → LONG_START → EIGHT_SHORT → LONG_STOP` state machine; + the §4.6.11.3.1 forward MDCT runs under the same composite windows + the decoder synthesizes with (one 2048-point transform for long + sequences, eight 256-point transforms at `448 + j·128` for short); + `EIGHT_SHORT` frames merge envelope-alike adjacent windows into + §4.5.2.3.4 window groups (one scalefactor/section track per group, + §4.5.2.3.5 interleaved transmission order; the emitted 7-bit mask + is pinned as the exact inverse of the decoder-side derivation); + per-band scalefactors follow a masking-spread rule (band target + magnitude `42·(peak_b/peak_frame)^½`, sub-step bands culled to + `ZERO_HCB`) with the DPCM ±60 track threaded across window groups; + a bidirectional rate loop (±4 scalefactor ladder + ±1..3 fine pass) + fits each frame to the bitrate-derived byte budget; codebooks and + sections come from a **measured-bit-cost dynamic program** (every + candidate same-class run priced at its `section_data()` header + overhead plus the cheapest Table 4.95 book, actual codeword + + sign + escape bits measured with the real tuple writer — pinned + never-larger than the classic smallest-LAV + merge rule), and + long frames additionally price a §4.4.6.3 `pulse_data()` variant + (band outliers reduced to the rest-of-band floor, restored + bit-exactly by the decoder's §4.6.3.3 fix-up; kept only when the + measured channel stream is smaller). + Stereo pairs code per-band §4.6.8.1 M/S (`m=(l+r)/2`, `s=(l−r)/2` + where the transform concentrates band energy, emitted as + `ms_mask_present` 2 / 1+mask / 0) inside a `common_window` CPE — + long frames per sfb, short frames per `(window group, sfb)` under + the pair's **joint** grouping (decided once on the pair envelope; + independent decisions would desync the shared `ics_info`). + **Every Table 1.19 default channel layout encodes** — 1–6 and 8 + (7.1) channels: the element-plan `raw_data_block()` assembly (SCE + / `common_window` CPE / LFE with per-kind instance tags), + canonical-order input permuted by the exact inverse of the + decoder's §1.6.3.5 reorder, and §4.5.2.1.3-conforming LFE elements + (always ONLY_LONG / sine through the frame's block switching, no + TNS, only the lowest 12 spectral lines transmitted). + Round-trips through the crate's own decoder: multitone 128 kbps at + 0.016 err/sig RMS, staged-fixture transcodes at 0.0008–0.003, + identical-channel stereo at 1.02× the mono stream size (short-run + identical channels decode L exactly equal to R), multichannel + layouts pinned with one distinct tone per speaker, and the + wire-level window-sequence walk pins the exact + `LongStart → EightShort → LongStop` pattern around a percussive + burst; a deterministic bit-flip/truncation battery covers the + multichannel and grouped-short streams. +- **`codec_encoder`** — the frame-in / packet-out + `oxideav_core::Encoder` adaptor (`make_encoder`, honouring + `sample_rate` / `channels` (1–6, 8) / `bit_rate`, default + 64 kbps/channel); registered alongside the decoder under id + `"aac"`, and re-exported as `encoder::make_encoder` per the + workspace dual-API convention. + +### ER BSAC (AOT 22) — noiseless-coder bring-up (§4.4.2.6 / §4.5.2.6 / §4.6.4) + +The Bit-Sliced Arithmetic Coding decoder roster is implemented and +its front half is **conformance-pinned against the ISO/IEC 14496-26 +`er_bs*` corpus**; the spectral bit-slice probability *selection* +of the deployed encoder diverges from the printed spec and is the +component still open (see below): + +- **Numeric tables** (`bsac_tables`) — Tables 4.A.31–4.A.77 + transcribed from the staged spec PDF: the `cband_si_type` + parameter matrix, the scalefactor / `cband_si` / stereo / PNS + cumulative-frequency models, the Table 4.A.34 context-position + map, the Table 4.A.35/36 `min_p0`/`max_p0` budget clamps, and the + 22 spectral probability tables with the printed alias scheme + resolved. (The 2001 and 2009 editions print *different* alias + schemes — tables 11–22 onto 9/10 alternating with sub-MSB zero + rows from 7/8 in 2009, everything onto 10 with zero rows from 8 + in 2001 — plus one conflicting cell in table 7; both were + transcribed and tested.) +- **Arithmetic decoder** (`bsac_arith`) — the §4.5.2.6.2.7.4 + procedure exactly as listed (`decode_symbol` over 14-bit cumfreq + models, binary `decode_bit`, the `half[]` renorm schedule, 30-bit + init, zero-stuffing segment reader), round-tripped against a + spec-inverse test encoder over every model. +- **Layer geometry** (`bsac_layer`) — the §4.5.2.6.2.4/5 roster: + base sub-layer split, per-layer coding-band / spectral / sfb + coverage (the literal `end_sfb = sfb + 1` one-band lookahead, + corpus-confirmed), `layer_si_maxlen`, the rate-anchored + `layer_bit_offset` derivation with the overflow/underflow + redistribution, and the SBA `terminal_layer` marks. +- **Block decode + reconstruction** (`bsac_decode`) — the + `bsac_header()` / `general_header()` raw-bit parse, the full + layer walk (side info, first-pass spectra, the + `bsac_lower_spectra()` refinement, budget carry between layers), + bit-slice reassembly with interleaved sign decode, and the AAC + back end (§4.6.2 dequant, group de-interleave, §4.6.8 stereo + hooks, §4.6.9 TNS, §4.6.11 filterbank) behind a persistent + `BsacDecoder`. +- **What the corpus pins** (`tests/bsac_bringup.rs`, corpus-gated): + on `er_bs01_48_ep0` the silent access units decode + **sample-exact** against the reference waveform (headers, layer + roster, arithmetic side-info decode all in sync), and on content + frames a TDAC oracle (the §4.6.11 perfect-reconstruction + property recovers each frame's exact transmitted spectrum from + the reference PCM) confirms the decoded `cband_si` MSB plane and + scalefactor gains match the deployed encoder precisely. +- **The open divergence**: the §4.6.4.2.3 spectral-bit probability + selection as printed decodes the wrong sliced bits partway into + the first coding band (both editions' alias readings, several + structural variants, and every printed row under every position + mapping tried were tested against the oracle truth). A + constraint solver over the real stream proves a consistent + context→p0 dictionary *exists* — the symbol order, sign + interleave and context classes are right — but its values match + no printed row (the all-zero-context position demands + `p0 ∈ {0x3700..=0x3a00}`; every plausible row prints `0x3b00+` + there). A clean-room behavioural trace of the deployed p0 + selection (or the corrigendum text) is the standing docs ask; + `tests/bsac_bringup.rs` carries the divergence locator and the + solver instrument, and `tests/iso_14496_26_conformance.rs` + reports the structural decode rate (618/703 AUs on + `er_bs01_48_ep0`) without asserting PCM until the rule lands. + +## Not yet supported + +- **The deployed ER AAC LD `tns_data()` filter record.** The + ISO/IEC 14496-26 LD conformance bitstreams transmit an + extra-spec TNS record: the corpus-resolved 1-bit-`n_filt` reading + (`docs/audio/aac/er-ld-tns-divergence.md` §0, implemented here) + reconciles the *structure* — every AU parses to its boundary — but + the reference waveforms show the record carries a real + variable-length filter (per-AU record lengths ≈ 19–61 bits, in + 3-bit increments) whose layout matches no Table 4.54/4.155 + reading (a grammar search over length 4/6 × order 3/5 × 1–2 + filters × optional direction/compress fields, decoded *and* + applied, reconciles none of it). TNS-bearing LD AUs (~6 % of the + `er_ad1103*` family) therefore decode with wrong PCM until a + behavioural trace of the deployed record lands; the conformance + harness masks them (and bounds the LTP-setup vectors coarsely, + since LD LTP history includes those AUs). +- Encoder-side tool remainders — the end-to-end AAC-LC encoder (see + `encoder` below) covers block switching with §4.5.2.3.4 short-frame + grouping, M/S on both frame shapes, the scalefactor/quantizer rate + loop with measured-bit-cost codebook/section choice, every + Table 1.19 default channel layout, and opt-in §4.6.13 PNS emission + (`StreamEncoder::set_pns` — off by default because a single-frame + spectral statistic cannot tell true noise from noise-shaped + deterministic content such as sweeps; default-on awaits a + cross-frame tonality measure) and default-on §4.6.9 TNS emission + (`encoder_tns`: per-window Levinson-Durbin prediction-gain decision + under a time-domain temporal-envelope gate, PARCOR quantised on + the §4.6.9.3 4-bit arcsine grid, and the §4.6.7.4.1 all-zero + analysis pass derived from the *wire* coefficients so the + decoder's all-pole synthesis is its exact inverse) and opt-in + §4.6.8.2 intensity-stereo emission + (`StreamEncoder::set_intensity_stereo` — correlated high bands + transmitted once with codebook 15/14 + `is_pos` on the §4.6.8.1.4 + track; off by default because intensity coding discards the + pair's side information) and measured §4.4.6.3 `pulse_data()` + emission (long-frame outlier-over-floor bands, kept only when the + whole channel stream prices smaller; the decode-side fix-up + restores the identical quantized spectrum), keeps + PNS and IS long-frame-only (CPE PNS emits the §4.6.13.3 `ms_used` + correlated-noise signalling — shared random vector — for + both-channels-noise bands correlating above 0.5), and has no + PCE-driven custom layouts (7-channel and beyond-7.1 shapes; the + Table 1.19 defaults 1–6 and 8 all encode). +- SSR remainders — the §4.6.12 gain-control tool is now implemented + and wired **end to end** (front-half filterbank, gain + reconstruction, IPQF — see the "SSR gain control" section above), + and a writer-assembled AOT-3 fixture driving non-unity gain + ladders through all four window sequences (with the §4.6.12.3.3 + variable 1024/1472/576 frame lengths) is staged in the docs corpus + (`aac-ssr-gain-control-adts`; no encoder for AOT 3 exists anywhere, + so a captured conformance stream remains welcome — a black-box + validator binary reports SSR gain control unimplemented, so there + is no external oracle for the ladders). Still open: the 13818-7 + SSR-profile *bandwidth-scalable* output modes (decoding only 1–3 + PQF bands at a reduced rate) are not selectable — the decoder + always reconstructs the full-rate signal. (The Main frequency-domain predictor, + §4.6.6, is now + fully wired into `element_decode` for the AAC Main object type on long + windows — see `predictor` above. LTP, §4.6.7, is likewise wired in + with the §4.6.7.4.1 / Figure 4.30 TNS-analysis-in-loop ordering. + The ISO/IEC 14496-3:**2001** Table 4.55 short-window LTP *syntax* — + the per-short-window `ltp_short_used` / `ltp_short_lag_present` / + `ltp_short_lag` loop that the 2009 edition removed ("LTP is + restricted to long windows only", §4.6.7.1 2009) — is parsed and + re-encoded under the explicit `LtpEdition::Iso2001` selector + (`parse_ltp_data_edition` / `write_ltp_data_edition`; the two + editions are wire-incompatible there and nothing in-band signals + which one a stream follows). The short-window *synthesis* stays + unimplemented: the 2001 §4.6.7.3 text defines the `x_rec` buffer + arrangement once but never fixes the per-subframe index origin for + the eight windows, and no LTP fixture exists to disambiguate. The + ER AAC LD long-window LTP — 10-bit lag, `ltp_lag_update` repeat, + `M = N/2` — is implemented and exercised by the staged LD + fixtures.) +- SBR/PS remainders — the §4.6.18 SBR tool **and** the subpart-8 PS + tool are **implemented end to end** (see the sections above) and + wired into the ADTS / LATM `StreamDecoder` paths and the runtime + `Decoder`, validated against the HE-AAC v1 (99.98% sample-exact) + and HE-AAC v2 (5e-5 RMS) fixtures, with the 10-bit + `bs_sbr_crc_bits` of every `EXT_SBR_DATA_CRC` payload now + **verified** (§4.4.2.8.1 `G10`, zero init, over the Table 4.62 + region — see `adts_crc`; a derived type-14 fixture is staged as + `he-aac-v1-sbrcrc-adts`). The §4.6.18.4.3 downsampled-output mode + and the §4.6.18.8 low-power variant are now **both selectable end + to end** (see the SBR back-end section above). Still open: SBR is + defined here over the 1024-line core only — an SBR payload on a + 960-line or LD stream is rejected + (`Error::SbrUnsupportedFrameFamily`; the §4.6.19 LD SBR tool is + ELD's and stays out of scope) — and low-power PS is undefined by + design (the subpart-8 tool needs the complex QMF domain, so LP + + PS is rejected). The coupling-channel (CCE) tool is + decoded **and applied** end to end (`cce` + the two-pass stream walk; + see the tool-chain section above), validated against the + filterbank-linearity identity on writer-assembled CCE streams; a + third-party CCE-bearing conformance fixture would still be a welcome + external cross-check. +- **ER BSAC (AOT 22) PCM** — the noiseless-coder front half + (headers, layer geometry, arithmetic side-info decode) is + conformance-pinned, but the deployed encoder's spectral bit-slice + probability selection diverges from every reading of the printed + §4.6.4.2.3 / Table 4.A.34 selection (see the BSAC section above), + so reconstructed PCM does not yet match the reference waveforms. + Also out of scope until then: SBA-mode segment scheduling + (`sba_mode == 1`), BSAC LTP, BSAC PNS (the noise-energy PCM + conventions need a working spectral decode to pin), the + `zero_code` extended part (channel / SBR / MPEG-Surround + extensions), and the §4.5.2.6.1 multi-ES `bsac_payload()` + large-step-layer reassembly (the `er_bs02`-style carriage). +- Error-resilience remainders — the ER story is now wired end to end + for ER AAC LC (AOT 17), **ER AAC LTP (AOT 19)** and ER AAC LD + (AOT 23): the ER channel-element body + (`ics_body::IcsBody::parse_er`) selects all three §4.4.6 + resilience branches, the `reordered_spectral_data()` payload is + decoded and encoded (`hcr_decode`), and the §4.4.2.3 Table 4.19 + `er_raw_data_block()` driver + (`StreamDecoder::decode_er_raw_data_block`) walks the fixed + per-`channelConfiguration` element sequence for all three AOTs — + reachable from LATM (the LOAS driver routes AOT-17/19/23 layers + there with the ASC's resilience triplet and the ASC-resolved + §4.5.1.1 frame family). AOT 17 is pinned bit-identical to the + equivalent non-resilient decode of the same spectra + (`aac-er-hcr-loas`); AOT 19 — the §4.6.7 LTP tool over the + Table 4.19 walk (11-bit lag, `M = 0`, per-element `x_rec` history + across frames) — is pinned bit-identical to the equivalent AOT-4 + decode with LTP active (SCE and pair-LTP CPE, plain and HCR + spectra); AOT 23 is pinned by the staged LD fixtures (see the + frame-length families section above). **ER AAC scalable (AOT 20) + now decodes end to end** (see the scalable section above), and the + §1.8 EP tool — `ErrorProtectionSpecificConfig()`, SRCPC / RS / + interleaving, the `ep_frame()` codec and the LOAS `EPMuxElement` / + `EPAudioSyncStream` carrier — is implemented (see the EP section + above). Still open: the §4.5.2.4 Table 4.148/4.149 per-element + category *split* of the codec payloads themselves (reassembling an + er_raw_data_block whose bits arrive as separate + error-sensitivity-category instances under `epConfig == 1` / + `directMapping` — the ep_frame class concatenation covers the + in-order case), and an encoder-produced HCR conformance stream as + an external cross-check. +- LATM/LOAS transport framing (§1.7) — the `StreamMuxConfig()`, + `AudioMuxElement()`, `PayloadLengthInfo()`, `PayloadMux()`, + `LatmGetValue()`, `AudioSyncStream()` and `EPAudioSyncStream()` + bitstream walkers are now implemented and tested (see the + LATM / LOAS transport section above), with the `crcCheckSum` + recomputed against the §1.8.4.5 `CRC8` generator in the `crc` + module. The runtime `Decoder` LOAS entry point that routes the + recovered `MuxPayload` raw-data-blocks into a `StreamDecoder` is now + wired (`latm::LoasDecoder` + the `codec_decoder` carrier + auto-detection above). The `EPMuxElement()` EP-tool payload + de-interleave is now implemented (`LoasDecoder::decode_all_ep`; see + the EP section above). (ADTS `adts_error_check()` CRC validation — + the 192/128-bit region selection with the double-protection edge + cases plus the ISO/IEC 11172-3 §2.4.3.1 code — landed in the + dedicated `adts_crc` module; see the bitstream-parsing section.) + +## License + +MIT — see [LICENSE](./LICENSE). diff --git a/crates/vendor/oxideav-aac/VENDOR.toml b/crates/vendor/oxideav-aac/VENDOR.toml new file mode 100644 index 00000000..bd9fe575 --- /dev/null +++ b/crates/vendor/oxideav-aac/VENDOR.toml @@ -0,0 +1,9 @@ +# Written by scripts/vendor-oxideav.sh. Do not edit, and do not +# hand-edit the vendored sources beside it — change them upstream +# and re-run the script. +source = "https://github.com/OxideAV/oxideav-aac" +commit = "719f1f594aef3465ecf2d718685bc27f8e797423" +describe = "v0.1.6-77-g719f1f5" +version = "0.1.6" +vendored_at = "2026-08-24T08:53:00Z" +patches = [] diff --git a/crates/vendor/oxideav-aac/src/adts.rs b/crates/vendor/oxideav-aac/src/adts.rs new file mode 100644 index 00000000..4331d29e --- /dev/null +++ b/crates/vendor/oxideav-aac/src/adts.rs @@ -0,0 +1,289 @@ +//! ADTS — *Audio Data Transport Stream* — fixed-header parser. +//! +//! ISO/IEC 13818-7 §1.A.2.2.1 defines the ADTS fixed-header (28 bits) +//! and §1.A.2.2.2 the variable-header (28 bits), followed by an +//! optional 16-bit CRC and one or more `raw_data_block()` payloads. +//! The header layout, MSB first: +//! +//! | bits | field | +//! |------|------------------------------------------------| +//! | 12 | `syncword` — required `0xFFF` | +//! | 1 | `ID` — MPEG-4 (`0`) vs MPEG-2 (`1`) | +//! | 2 | `layer` — required `0b00` | +//! | 1 | `protection_absent` — `1` ⇒ no CRC follows | +//! | 2 | `profile_ObjectType` — ADTS profile field | +//! | 4 | `sampling_frequency_index` | +//! | 1 | `private_bit` | +//! | 3 | `channel_configuration` | +//! | 1 | `original_copy` | +//! | 1 | `home` | +//! | 1 | `copyright_identification_bit` | +//! | 1 | `copyright_identification_start` | +//! | 13 | `aac_frame_length` — total frame bytes | +//! | 11 | `adts_buffer_fullness` | +//! | 2 | `number_of_raw_data_blocks_in_frame` (N − 1) | +//! +//! followed by either: +//! +//! * `protection_absent == 1` ⇒ no CRC; payload starts at byte 7. +//! * `protection_absent == 0` ⇒ 16-bit CRC, payload starts at byte 9. +//! +//! Note the field ADTS calls `profile_ObjectType` is **one less** than +//! the `audioObjectType` defined in ISO/IEC 14496-3 Table 1.16. ADTS +//! `profile_ObjectType == 1` is therefore AAC LC (AOT 2). +//! +//! The `sampling_frequency_index` mapping follows ISO/IEC 14496-3 +//! Table 1.18; this module exposes [`AdtsHeader::sample_rate`] which +//! resolves the index to a frequency in Hz. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::{Error, Result}; + +/// Fixed sync pattern at the start of every ADTS frame. +pub const ADTS_SYNCWORD: u16 = 0x0FFF; + +/// Header byte count when `protection_absent == 1` (no CRC). +pub const ADTS_HEADER_BYTES_NO_CRC: usize = 7; + +/// Header byte count when `protection_absent == 0` (16-bit CRC after +/// the fixed/variable-header pair). +pub const ADTS_HEADER_BYTES_WITH_CRC: usize = 9; + +/// ISO/IEC 14496-3 Table 1.18 — `samplingFrequencyIndex`. +/// +/// Indices 13 and 14 are reserved. Index 15 signals an explicit +/// 24-bit rate in `AudioSpecificConfig` but is *not* legal in an +/// ADTS header (the ADTS field is 4 bits). +pub const ADTS_SAMPLE_RATES_HZ: [u32; 13] = [ + 96_000, 88_200, 64_000, 48_000, 44_100, 32_000, 24_000, 22_050, 16_000, 12_000, 11_025, 8_000, + 7_350, +]; + +/// Resolved ADTS fixed + variable header. See module docs for the +/// per-field bit layout. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AdtsHeader { + /// MPEG version indicator. `false` ⇒ MPEG-4 (`ID = 0`), `true` ⇒ + /// MPEG-2 (`ID = 1`). The decoder behaviour is otherwise + /// identical; the bit only affects which extensions are legal in + /// downstream `raw_data_block()` payloads (PNS / LTP are MPEG-4 + /// only). + pub mpeg_version_mpeg2: bool, + + /// `true` ⇒ no 16-bit CRC follows the variable header. The frame + /// payload starts at byte 7 instead of byte 9. + pub protection_absent: bool, + + /// 2-bit ADTS `profile_ObjectType` field as read from the wire. + /// This is `audioObjectType − 1` per ISO/IEC 13818-7 §1.A.2 (so + /// `0` = Main, `1` = LC, `2` = SSR, `3` = (LTP) reserved in + /// 13818-7 but used by 14496-3). + pub profile: u8, + + /// 4-bit `sampling_frequency_index`. Use [`AdtsHeader::sample_rate`] + /// for the resolved Hz value. + pub sampling_frequency_index: u8, + + /// 3-bit `channel_configuration`. ISO/IEC 14496-3 Table 1.19: + /// `0` ⇒ defined by an inline PCE in the payload, `1` ⇒ mono, + /// `2` ⇒ stereo, …, `7` ⇒ 7.1 surround. + pub channel_configuration: u8, + + /// 13-bit `aac_frame_length` — total frame size in bytes, + /// including the header itself and (if present) the CRC. + pub aac_frame_length: u16, + + /// 11-bit `adts_buffer_fullness`. `0x7FF` is the spec-mandated + /// "VBR / unknown" sentinel. Phase 1 does not enforce buffer + /// modelling. + pub adts_buffer_fullness: u16, + + /// Number of `raw_data_block()` payloads contained in this frame. + /// The wire field is `N − 1`; this is the resolved count (≥ 1). + pub number_of_raw_data_blocks_in_frame: u8, +} + +impl AdtsHeader { + /// Parse an ADTS fixed + variable header from the start of `data`. + /// On success returns the [`AdtsHeader`] and the byte offset where + /// the first `raw_data_block()` payload begins (7 if + /// `protection_absent == 1`, 9 otherwise). + /// + /// CRC validation is **deferred**: when `protection_absent == 0` + /// this routine confirms the CRC bytes are present (i.e. the + /// input is at least 9 bytes long) but does not verify the CRC + /// value itself. + pub fn parse(data: &[u8]) -> Result<(Self, usize)> { + if data.len() < ADTS_HEADER_BYTES_NO_CRC { + return Err(Error::UnexpectedEnd); + } + + let mut br = BitReader::new(data); + + // 12-bit syncword + let sync = br.read_u32(12).map_err(|_| Error::UnexpectedEnd)? as u16; + if sync != ADTS_SYNCWORD { + return Err(Error::AdtsSyncNotFound); + } + + // 1-bit ID, 2-bit layer, 1-bit protection_absent + let mpeg_version_mpeg2 = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let layer = br.read_u32(2).map_err(|_| Error::UnexpectedEnd)?; + if layer != 0 { + return Err(Error::AdtsLayerNonZero); + } + let protection_absent = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + + // 2-bit profile, 4-bit sampling_frequency_index, 1-bit private_bit + let profile = br.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; + let sampling_frequency_index = br.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; + if sampling_frequency_index >= 13 { + return Err(Error::AdtsReservedSampleRateIndex); + } + let _private_bit = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + + // 3-bit channel_configuration, 1-bit original_copy, 1-bit home + let channel_configuration = br.read_u32(3).map_err(|_| Error::UnexpectedEnd)? as u8; + let _original_copy = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let _home = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + + // 1-bit copyright_identification_bit, 1-bit copyright_identification_start + let _copyright_identification_bit = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let _copyright_identification_start = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; + + // 13-bit aac_frame_length, 11-bit adts_buffer_fullness, + // 2-bit number_of_raw_data_blocks_in_frame + let aac_frame_length = br.read_u32(13).map_err(|_| Error::UnexpectedEnd)? as u16; + let adts_buffer_fullness = br.read_u32(11).map_err(|_| Error::UnexpectedEnd)? as u16; + let raw_blocks_minus_one = br.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; + let number_of_raw_data_blocks_in_frame = raw_blocks_minus_one + 1; + + let payload_offset = if protection_absent { + ADTS_HEADER_BYTES_NO_CRC + } else { + ADTS_HEADER_BYTES_WITH_CRC + }; + + if (aac_frame_length as usize) < payload_offset { + return Err(Error::AdtsFrameLengthTooSmall); + } + + // When a CRC is present, confirm the trailing two bytes fit + // in `data`. We do not validate the CRC value in Phase 1. + if !protection_absent && data.len() < ADTS_HEADER_BYTES_WITH_CRC { + return Err(Error::UnexpectedEnd); + } + + Ok(( + AdtsHeader { + mpeg_version_mpeg2, + protection_absent, + profile, + sampling_frequency_index, + channel_configuration, + aac_frame_length, + adts_buffer_fullness, + number_of_raw_data_blocks_in_frame, + }, + payload_offset, + )) + } + + /// Resolved sample rate in Hz, from the + /// `sampling_frequency_index` via ISO/IEC 14496-3 Table 1.18. + pub fn sample_rate(&self) -> u32 { + // `parse` already rejects reserved indices, so the cast + // cannot index out of bounds for any successfully-parsed + // header. Defensive check kept regardless. + ADTS_SAMPLE_RATES_HZ + .get(self.sampling_frequency_index as usize) + .copied() + .unwrap_or(0) + } + + /// `audioObjectType` for the carried payload — the ADTS wire + /// field is one less than the `audioObjectType` defined by + /// ISO/IEC 14496-3 Table 1.16, so this returns `profile + 1`. + pub fn audio_object_type(&self) -> u8 { + self.profile + 1 + } + + /// Length in bytes of the `raw_data_block()` region that follows + /// the header (and CRC, if present): `aac_frame_length` minus + /// header overhead. + pub fn payload_len(&self) -> usize { + let header = if self.protection_absent { + ADTS_HEADER_BYTES_NO_CRC + } else { + ADTS_HEADER_BYTES_WITH_CRC + }; + (self.aac_frame_length as usize).saturating_sub(header) + } + + /// Serialise the fixed + variable header pair (7 bytes) — the + /// byte-exact inverse of [`AdtsHeader::parse`] for a + /// `protection_absent == 1` header. + /// + /// The four fields [`AdtsHeader::parse`] discards (`private_bit`, + /// `original_copy`, `home`, `copyright_identification_bit` / + /// `_start`) are written as `0`, matching what every fixture + /// header in the staged corpus carries. Encoders needing a CRC + /// (`protection_absent == 0`) must append the §1.A.2.2.3 16-bit + /// `crc_check` themselves after the returned 7 bytes; this + /// routine intentionally emits only the header pair so it stays + /// a pure function of the struct. + /// + /// Returns [`Error::AdtsEncodeInvalid`] when a field exceeds its + /// wire width or violates a normative constraint: + /// + /// * `profile > 3` (2-bit field), + /// * `sampling_frequency_index >= 13` (Table 1.18 reserved), + /// * `channel_configuration > 7` (3-bit field), + /// * `aac_frame_length >= 8192` (13-bit field) or smaller than + /// the header overhead itself, + /// * `adts_buffer_fullness > 0x7FF` (11-bit field), + /// * `number_of_raw_data_blocks_in_frame` outside `1..=4` + /// (2-bit `N − 1` field). + pub fn write(&self) -> Result<[u8; ADTS_HEADER_BYTES_NO_CRC]> { + if self.profile > 3 + || self.sampling_frequency_index >= 13 + || self.channel_configuration > 7 + || self.aac_frame_length >= (1 << 13) + || self.adts_buffer_fullness > 0x7FF + || !(1..=4).contains(&self.number_of_raw_data_blocks_in_frame) + { + return Err(Error::AdtsEncodeInvalid); + } + let overhead = if self.protection_absent { + ADTS_HEADER_BYTES_NO_CRC + } else { + ADTS_HEADER_BYTES_WITH_CRC + }; + if (self.aac_frame_length as usize) < overhead { + return Err(Error::AdtsEncodeInvalid); + } + + let mut bw = BitWriter::new(); + bw.write_u32(ADTS_SYNCWORD as u32, 12); + bw.write_bit(self.mpeg_version_mpeg2); + bw.write_u32(0, 2); // layer — required 0b00 + bw.write_bit(self.protection_absent); + bw.write_u32(self.profile as u32, 2); + bw.write_u32(self.sampling_frequency_index as u32, 4); + bw.write_bit(false); // private_bit + bw.write_u32(self.channel_configuration as u32, 3); + bw.write_bit(false); // original_copy + bw.write_bit(false); // home + bw.write_bit(false); // copyright_identification_bit + bw.write_bit(false); // copyright_identification_start + bw.write_u32(self.aac_frame_length as u32, 13); + bw.write_u32(self.adts_buffer_fullness as u32, 11); + bw.write_u32((self.number_of_raw_data_blocks_in_frame - 1) as u32, 2); + let bytes = bw.finish(); + debug_assert_eq!(bytes.len(), ADTS_HEADER_BYTES_NO_CRC); + let mut out = [0u8; ADTS_HEADER_BYTES_NO_CRC]; + out.copy_from_slice(&bytes); + Ok(out) + } +} diff --git a/crates/vendor/oxideav-aac/src/adts_crc.rs b/crates/vendor/oxideav-aac/src/adts_crc.rs new file mode 100644 index 00000000..15c274e5 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/adts_crc.rs @@ -0,0 +1,566 @@ +//! ADTS `error_check()` and SBR `bs_sbr_crc_bits` CRC verification. +//! +//! Two independent CRC mechanisms protect an AAC bitstream, each with +//! its own polynomial, initial value, and covered region: +//! +//! 1. the **ADTS `crc_check`** — a 16-bit CRC present when the ADTS +//! fixed header signals `protection_absent == 0`. The protected-bit +//! region is normatively described by ISO/IEC 13818-7:2004 §8.1.1.1 +//! (semantics of `adts_error_check()` and the multi-raw-data-block +//! split variants, Tables 1.A.8–1.A.10 of ISO/IEC 14496-3:2009); +//! the CRC code itself is cited by 13818-7 §8.1.1.2 to ISO/IEC +//! 11172-3 §2.4.3.1: generator polynomial +//! `G(x) = x¹⁶ + x¹⁵ + x² + 1` (`0x8005`), initial register value +//! all-ones (`0xFFFF`), bits fed MSB-first in order of appearance, +//! no final inversion. +//! 2. the **SBR extension CRC** (`bs_sbr_crc_bits`) — a 10-bit CRC +//! carried at the head of an `EXT_SBR_DATA_CRC` (extension type 14) +//! fill payload. ISO/IEC 14496-3:2009 §4.4.2.8.1 (repeated in +//! §4.5.2.8.1): generator polynomial +//! `G10(x) = x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1`, initial value **zero**, +//! covering every `sbr_extension_data()` bit after the CRC field up +//! to (but excluding) the trailing `bs_fill_bits` alignment — i.e. +//! `num_sbr_bits − 10` bits (Table 4.62). +//! +//! Both codes run on the same MSB-first shift register: for each +//! message bit, the feedback is the incoming bit XORed with the +//! register's top bit; on feedback the register (shifted left one) +//! is XORed with the low-order generator terms. No zero-augmentation +//! flush and no output inversion follow — the register value after +//! the last message bit is the checksum. With a zero initial value +//! this equals the polynomial remainder `M(x)·xᵏ mod G(x)`, matching +//! the §4.4.2.8.1 "remainder" wording for the SBR code; the ADTS code +//! differs only by its all-ones initialisation. (The §1.8.4.5 CRC +//! family implemented in [`crate::crc`] is a *different* convention — +//! zero init **plus** a normative output-bit inversion — and covers +//! the LATM `crcCheckSum` / EP-tool codes, not these two.) +//! +//! ## ADTS protected-bit region (13818-7:2004 §8.1.1.1) +//! +//! For `adts_error_check()` (single raw data block, +//! `number_of_raw_data_blocks_in_frame == 0` on the wire) the bits fed +//! into the CRC, in order of appearance, are: +//! +//! * **all 56 bits** of `adts_fixed_header()` + `adts_variable_header()`; +//! * the **first 192 bits** of every SCE / CPE / CCE / LFE channel +//! element — *excluding* the 3-bit `id_syn_ele`, zero-padded to 192 +//! when the element is shorter; +//! * **additionally** the first 128 bits of the *second* +//! `individual_channel_stream` of every CPE (zero-padded to 128; +//! when the second ICS starts before the element's 192nd bit the +//! overlap is protected twice, each time in order of appearance); +//! * **all** bits of every `program_config_element()` and +//! `data_stream_element()` (again excluding `id_syn_ele`). +//! +//! Fill elements, the END marker, and the `crc_check` field itself are +//! not covered. +//! +//! `adts_raw_data_block_error_check()` (multi-RDB form, one 16-bit CRC +//! after each `raw_data_block()`) covers the same per-element regions +//! scoped to its block, *without* re-including the headers; the +//! headers plus every 16-bit `raw_data_block_position` are covered +//! once by `adts_header_error_check()`. +//! +//! ## Provenance +//! +//! Region selection and code parameters are transcribed from the +//! staged format specifications (ISO/IEC 14496-3:2009 Tables +//! 1.A.5–1.A.10 / Table 4.62 / §4.4.2.8.1 and ISO/IEC 13818-7:2004 +//! §8.1.1) via the clean-room region analysis in +//! `docs/audio/aac/aac-crc-regions.md`. ISO/IEC 11172-3 itself is not +//! staged; the `0x8005` / `0xFFFF` shift-register parameters are the +//! §2.4.3.1 values as recorded there. + +use oxideav_core::bits::BitReader; + +use crate::cce::CouplingChannelElement; +use crate::ics_body::IcsBody; +use crate::raw_data_block::{Element, IdSynEle, Walker}; +use crate::spectral_data::SpectralData; +use crate::{Error, Result}; + +/// Low-order terms of the ADTS generator `x¹⁶ + x¹⁵ + x² + 1` +/// (ISO/IEC 11172-3 §2.4.3.1 via 13818-7:2004 §8.1.1.2). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub const ADTS_CRC_POLY: u32 = 0x8005; + +/// ADTS CRC initial register value (all ones). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub const ADTS_CRC_INIT: u32 = 0xFFFF; + +/// Low-order terms of the SBR generator `x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1` +/// (ISO/IEC 14496-3:2009 §4.4.2.8.1). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub const SBR_CRC_POLY: u32 = 0x0233; + +/// MSB-first CRC shift register (see module docs for the feedback +/// convention shared by the ADTS and SBR codes). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +#[derive(Debug, Clone, Copy)] +pub struct CrcRegister { + reg: u32, + poly: u32, + mask: u32, + top: u32, +} + +impl CrcRegister { + /// A register configured for the ADTS `crc_check` code: 16 bits, + /// generator `0x8005`, initial value `0xFFFF`. + pub fn adts() -> Self { + CrcRegister { + reg: ADTS_CRC_INIT, + poly: ADTS_CRC_POLY, + mask: 0xFFFF, + top: 0x8000, + } + } + + /// A register configured for the SBR `bs_sbr_crc_bits` code: 10 + /// bits, generator `G10` (`0x233`), initial value zero. + pub fn sbr() -> Self { + CrcRegister { + reg: 0, + poly: SBR_CRC_POLY, + mask: 0x03FF, + top: 0x0200, + } + } + + /// Feed one message bit (MSB-first order). + #[inline] + pub fn feed_bit(&mut self, bit: bool) { + let feedback = ((self.reg & self.top) != 0) ^ bit; + self.reg = (self.reg << 1) & self.mask; + if feedback { + self.reg ^= self.poly; + } + } + + /// Feed `n` zero bits (the §8.1.1.1 zero-padding of short + /// elements). + pub fn feed_zeros(&mut self, n: u64) { + for _ in 0..n { + self.feed_bit(false); + } + } + + /// Feed the bit range `[start_bit, end_bit)` of `data`, MSB-first + /// within each byte. Bits past the end of `data` are fed as zero + /// (a region that overruns its buffer only ever does so via the + /// normative zero-padding). + pub fn feed_bit_range(&mut self, data: &[u8], start_bit: u64, end_bit: u64) { + for pos in start_bit..end_bit { + let byte = (pos / 8) as usize; + let bit = data.get(byte).is_some_and(|b| b & (0x80 >> (pos % 8)) != 0); + self.feed_bit(bit); + } + } + + /// The current register value (the checksum once the whole + /// protected region has been fed). + pub fn value(&self) -> u16 { + self.reg as u16 + } +} + +/// Compute the 10-bit SBR CRC over the bit range `[start_bit, +/// end_bit)` of `data` — the `sbr_extension_data()` payload bits +/// after the `bs_sbr_crc_bits` field, before the `bs_fill_bits` +/// (ISO/IEC 14496-3:2009 Table 4.62 / §4.4.2.8.1). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn sbr_crc(data: &[u8], start_bit: u64, end_bit: u64) -> u16 { + let mut reg = CrcRegister::sbr(); + reg.feed_bit_range(data, start_bit, end_bit); + reg.value() +} + +/// One §8.1.1.1 protected region of a `raw_data_block()` payload: +/// the bit range `[start_bit, end_bit)` of the payload buffer, capped +/// and zero-padded to `pad_to` bits when a protection length applies +/// (192 for a channel element, 128 for a CPE's second ICS; `None` +/// feeds the whole range, the PCE / DSE case). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProtectedRegion { + /// First protected bit (absolute bit offset into the payload). + pub start_bit: u64, + /// One past the last payload bit of the region (the element end; + /// the fed length is additionally capped by `pad_to`). + pub end_bit: u64, + /// Normative protection length: feed `min(end_bit - start_bit, + /// pad_to)` payload bits, then zeros up to `pad_to`. + pub pad_to: Option, +} + +impl ProtectedRegion { + fn feed(&self, reg: &mut CrcRegister, payload: &[u8]) { + let len = self.end_bit.saturating_sub(self.start_bit); + match self.pad_to { + Some(pad) => { + let take = len.min(u64::from(pad)); + reg.feed_bit_range(payload, self.start_bit, self.start_bit + take); + reg.feed_zeros(u64::from(pad) - take); + } + None => reg.feed_bit_range(payload, self.start_bit, self.end_bit), + } + } +} + +/// Compute the single-RDB `adts_error_check()` CRC (ISO/IEC 14496-3 +/// Table 1.A.8, region per 13818-7:2004 §8.1.1.1): the 56 header bits +/// followed by every protected element region of the one +/// `raw_data_block()`. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn adts_single_crc(header: &[u8], payload: &[u8], regions: &[ProtectedRegion]) -> u16 { + let mut reg = CrcRegister::adts(); + reg.feed_bit_range(header, 0, 56); + for r in regions { + r.feed(&mut reg, payload); + } + reg.value() +} + +/// Compute the multi-RDB `adts_header_error_check()` CRC (Table +/// 1.A.9): the 56 header bits followed by every 16-bit +/// `raw_data_block_position`, in order. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn adts_header_crc(header: &[u8], positions: &[u16]) -> u16 { + let mut reg = CrcRegister::adts(); + reg.feed_bit_range(header, 0, 56); + for &p in positions { + for i in (0..16).rev() { + reg.feed_bit((p >> i) & 1 != 0); + } + } + reg.value() +} + +/// Compute one multi-RDB `adts_raw_data_block_error_check()` CRC +/// (Table 1.A.10): the protected element regions of a single +/// `raw_data_block()`, headers *not* re-included. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn adts_rdb_crc(payload: &[u8], regions: &[ProtectedRegion]) -> u16 { + let mut reg = CrcRegister::adts(); + for r in regions { + r.feed(&mut reg, payload); + } + reg.value() +} + +/// Walk one `raw_data_block()` off `reader` (parse-only — no +/// reconstruction) and collect its §8.1.1.1 protected regions in +/// order of appearance: per channel element the post-`id_syn_ele` +/// 192-bit window (SCE / CPE / CCE / LFE), per CPE additionally the +/// second ICS's 128-bit window, and the full body of every PCE / DSE. +/// +/// The reader is left positioned after the block's END marker (byte +/// aligned), exactly where a multi-RDB `adts_raw_data_block_error_ +/// check()` field or the next block begins. Returns the regions; an +/// exhausted payload before an explicit END terminates the block the +/// same way the decode driver treats it. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn collect_block_regions( + reader: &mut BitReader<'_>, + aot: u8, + fs: u8, +) -> Result> { + let mut regions = Vec::new(); + loop { + let elem_start = reader.bit_position(); + let Some(elem) = Walker::new(reader).next_element()? else { + return Ok(regions); + }; + match elem { + Element::ChannelElement { + kind: IdSynEle::Sce | IdSynEle::Lfe, + .. + } => { + let body = IcsBody::parse(reader, aot, fs, false)?; + let ics = body.ics_info.clone().ok_or(Error::ElementDecodeInvalid)?; + SpectralData::parse(reader, &ics, &body.section_data, fs)?; + regions.push(ProtectedRegion { + start_bit: elem_start + 3, + end_bit: reader.bit_position(), + pad_to: Some(192), + }); + } + Element::ChannelElement { + kind: IdSynEle::Cpe, + .. + } => { + let parsed = crate::decode::parse_cpe(reader, aot, fs)?; + let end = reader.bit_position(); + regions.push(ProtectedRegion { + start_bit: elem_start + 3, + end_bit: end, + pad_to: Some(192), + }); + regions.push(ProtectedRegion { + start_bit: parsed.second_ics_start_bit, + end_bit: end, + pad_to: Some(128), + }); + } + Element::ChannelElement { + kind: IdSynEle::Cce, + element_instance_tag, + } => { + CouplingChannelElement::parse_after_tag(reader, element_instance_tag, aot, fs)?; + regions.push(ProtectedRegion { + start_bit: elem_start + 3, + end_bit: reader.bit_position(), + pad_to: Some(192), + }); + } + Element::ChannelElement { .. } => return Err(Error::ElementDecodeInvalid), + Element::Data { .. } | Element::ProgramConfig(_) => { + regions.push(ProtectedRegion { + start_bit: elem_start + 3, + end_bit: reader.bit_position(), + pad_to: None, + }); + } + Element::Fill { .. } => {} + Element::End => return Ok(regions), + } + } +} + +/// Rewrite one `protection_absent == 1` single-raw-data-block ADTS +/// frame into its CRC-protected form: `protection_absent` cleared, +/// `aac_frame_length` grown by the 2 CRC bytes, and the Table 1.A.8 +/// `crc_check` computed over the §8.1.1.1 region inserted between the +/// header and the payload. Every other header bit (including the +/// fields [`AdtsHeader::parse`] does not surface) is preserved +/// verbatim. +/// +/// A frame that already carries a CRC is returned unchanged. A +/// multi-raw-data-block frame is rejected with +/// [`Error::NotImplemented`] (the Table 1.A.9/1.A.10 split form needs +/// a `raw_data_block_position` policy this helper does not invent). +pub fn protect_adts_frame(frame: &[u8]) -> Result> { + let (header, payload_offset) = crate::adts::AdtsHeader::parse(frame)?; + let frame_len = header.aac_frame_length as usize; + if frame_len < payload_offset || frame.len() < frame_len { + return Err(Error::UnexpectedEnd); + } + let frame = &frame[..frame_len]; + if !header.protection_absent { + return Ok(frame.to_vec()); + } + if header.number_of_raw_data_blocks_in_frame != 1 { + return Err(Error::NotImplemented); + } + let new_len = header.aac_frame_length + 2; + if new_len >= (1 << 13) { + return Err(Error::AdtsEncodeInvalid); + } + // Patch the header bytes in place: clear protection_absent (bit 0 + // of byte 1) and re-pack the 13-bit aac_frame_length (low 2 bits + // of byte 3, byte 4, top 3 bits of byte 5). + let mut h = [0u8; 7]; + h.copy_from_slice(&frame[..7]); + h[1] &= 0xFE; + h[3] = (h[3] & 0xFC) | ((new_len >> 11) as u8 & 0x03); + h[4] = (new_len >> 3) as u8; + h[5] = (h[5] & 0x1F) | (((new_len & 0x07) as u8) << 5); + + let payload = &frame[payload_offset..]; + let mut reader = BitReader::new(payload); + let regions = collect_block_regions( + &mut reader, + header.audio_object_type(), + header.sampling_frequency_index, + )?; + let crc = adts_single_crc(&h, payload, ®ions); + + let mut out = Vec::with_capacity(frame.len() + 2); + out.extend_from_slice(&h); + out.extend_from_slice(&crc.to_be_bytes()); + out.extend_from_slice(payload); + Ok(out) +} + +/// [`protect_adts_frame`] applied to every frame of a raw ADTS byte +/// stream (`aac_frame_length`-delimited walk to exhaustion). +pub fn protect_adts_stream(data: &[u8]) -> Result> { + let mut out = Vec::with_capacity(data.len()); + let mut pos = 0usize; + while pos + crate::adts::ADTS_HEADER_BYTES_NO_CRC <= data.len() { + let (header, _) = crate::adts::AdtsHeader::parse(&data[pos..])?; + let frame_len = header.aac_frame_length as usize; + if pos + frame_len > data.len() { + return Err(Error::UnexpectedEnd); + } + out.extend_from_slice(&protect_adts_frame(&data[pos..pos + frame_len])?); + pos += frame_len; + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Independent long-division reference: the MSB-feedback register + /// with initial value `I` over an `n`-bit message `M` computes, by + /// linearity, the remainder `(M(x)·xᵏ + I(x)·xⁿ) mod G(x)` — the + /// dividend is the k-zero-extended message with the init bits + /// XORed onto its leading `k` positions. + fn reference(poly_low: u32, k: u32, init: u32, bits: &[bool]) -> u32 { + let full = u64::from(poly_low) | (1u64 << k); + let mut dividend: Vec = bits.to_vec(); + dividend.extend(std::iter::repeat(false).take(k as usize)); + for (i, d) in dividend.iter_mut().enumerate().take(k as usize) { + *d ^= (init >> (k as usize - 1 - i)) & 1 != 0; + } + let mut reg: u64 = 0; + let topbit = 1u64 << k; + for &b in ÷nd { + reg = (reg << 1) | u64::from(b); + if reg & topbit != 0 { + reg ^= full; + } + } + (reg & ((1u64 << k) - 1)) as u32 + } + + fn to_bits(bytes: &[u8]) -> Vec { + bytes + .iter() + .flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1 != 0)) + .collect() + } + + #[test] + fn adts_register_matches_long_division_reference() { + for msg in [ + &[][..], + &[0x00][..], + &[0xFF, 0xF1][..], + &[0x12, 0x34, 0x56, 0x78, 0x9A][..], + &[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03][..], + ] { + let bits = to_bits(msg); + let mut reg = CrcRegister::adts(); + for &b in &bits { + reg.feed_bit(b); + } + assert_eq!( + u32::from(reg.value()), + reference(ADTS_CRC_POLY, 16, ADTS_CRC_INIT, &bits), + "message {msg:x?}" + ); + } + } + + #[test] + fn sbr_register_is_plain_remainder() { + // Zero init ⇒ the register equals M(x)·x¹⁰ mod G10(x). + for msg in [&[0x5Au8, 0x33][..], &[0xFF, 0x00, 0xAB, 0xCD][..]] { + let bits = to_bits(msg); + let mut reg = CrcRegister::sbr(); + for &b in &bits { + reg.feed_bit(b); + } + assert_eq!( + u32::from(reg.value()), + reference(SBR_CRC_POLY, 10, 0, &bits), + "message {msg:x?}" + ); + } + } + + #[test] + fn sbr_poly_matches_crate_crc10_generator() { + // §4.4.2.8.1's G10 is the same polynomial as the §1.8.4.5 + // CRC10 row; only the init / inversion conventions differ. + assert_eq!( + u64::from(SBR_CRC_POLY), + crate::crc::CrcPoly::Crc10.generator() + ); + assert_eq!( + u64::from(ADTS_CRC_POLY), + crate::crc::CrcPoly::Crc16.generator() + ); + } + + #[test] + fn empty_message_yields_init_for_adts() { + // No message bits: the register never moves. + let reg = CrcRegister::adts(); + assert_eq!(u32::from(reg.value()), ADTS_CRC_INIT); + assert_eq!(CrcRegister::sbr().value(), 0); + } + + #[test] + fn appending_checksum_cancels_the_register() { + // Defining property of the MSB-feedback register: feeding the + // message and then its own checksum drives the register to 0. + for msg in [&[0x53u8, 0x91, 0x2C][..], &[0xFF, 0xF9, 0x5C, 0x80][..]] { + let bits = to_bits(msg); + let mut reg = CrcRegister::adts(); + for &b in &bits { + reg.feed_bit(b); + } + let crc = reg.value(); + for i in (0..16).rev() { + reg.feed_bit((crc >> i) & 1 != 0); + } + assert_eq!(reg.value(), 0, "message {msg:x?}"); + } + } + + #[test] + fn region_pads_short_elements_with_zeros() { + // A 40-bit element padded to 192 must equal feeding the 40 + // payload bits + 152 explicit zeros. + let payload = [0xA5u8; 8]; + let region = ProtectedRegion { + start_bit: 3, + end_bit: 43, + pad_to: Some(192), + }; + let mut a = CrcRegister::adts(); + region.feed(&mut a, &payload); + let mut b = CrcRegister::adts(); + b.feed_bit_range(&payload, 3, 43); + b.feed_zeros(152); + assert_eq!(a.value(), b.value()); + } + + #[test] + fn region_caps_long_elements_at_pad_to() { + // A 300-bit element only contributes its first 192 bits. + let payload = [0x3Cu8; 64]; + let region = ProtectedRegion { + start_bit: 5, + end_bit: 305, + pad_to: Some(192), + }; + let mut a = CrcRegister::adts(); + region.feed(&mut a, &payload); + let mut b = CrcRegister::adts(); + b.feed_bit_range(&payload, 5, 5 + 192); + assert_eq!(a.value(), b.value()); + } + + #[test] + fn header_crc_covers_positions() { + let header = [0xFFu8, 0xF1, 0x50, 0x80, 0x2F, 0xFF, 0xFC]; + let a = adts_header_crc(&header, &[]); + let b = adts_header_crc(&header, &[0x1234]); + assert_ne!(a, b, "positions must alter the header CRC"); + } +} diff --git a/crates/vendor/oxideav-aac/src/asc.rs b/crates/vendor/oxideav-aac/src/asc.rs new file mode 100644 index 00000000..080b64b8 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/asc.rs @@ -0,0 +1,819 @@ +//! `AudioSpecificConfig` parser. +//! +//! ISO/IEC 14496-3 §1.6.2.1 Table 1.15 defines the canonical +//! `AudioSpecificConfig()` (ASC) as the out-of-band descriptor for +//! an MPEG-4 audio elementary stream. It carries +//! `audioObjectType`, `samplingFrequencyIndex` (and the 24-bit +//! escape rate when index is `0xf`), `channelConfiguration`, and a +//! per-AOT body (Table 1.17). +//! +//! Phase 1 parses the wrapper plus the body for **AOTs that route +//! to `GASpecificConfig`** (§4.4.1 Table 4.1) — the General Audio +//! branch covering all AAC variants: 1 (Main), 2 (LC), 3 (SSR), 4 +//! (LTP), 6 (scalable), 7 (TwinVQ), 17 (ER AAC LC), 19 (ER AAC +//! LTP), 20 (ER AAC scalable), 21 (ER TwinVQ), 22 (ER BSAC), 23 +//! (ER AAC LD). The hierarchical SBR (AOT 5) and PS (AOT 29) +//! outer-wrappers are also recognised: the parser reads the inner +//! `samplingFrequencyIndex` + (re-read) `audioObjectType` and +//! records `sbr_present` / `ps_present` so a later HE-AAC round can +//! drive SBR setup off the parsed ASC. +//! +//! All other AOTs return [`Error::UnsupportedAot`] so the spec +//! gap is explicit at the call site. +//! +//! ## What round 192 adds +//! +//! * The Table 1.15 trailing `syncExtensionType == 0x2b7` probe used +//! for *backward-compatible* implicit SBR / PS signalling in the +//! AudioSpecificConfig (§1.6.5, §1.6.6). After the per-AOT body +//! and `epConfig`, when `extensionAudioObjectType != 5` and the +//! carrier has `>= 16` bits remaining, the parser reads an 11-bit +//! `syncExtensionType` value: if it equals `0x2b7` it consumes a +//! nested `GetAudioObjectType()` and (when the resolved extension +//! AOT is `5`) the `sbrPresentFlag`, optional +//! `extensionSamplingFrequencyIndex` (with the same 24-bit escape +//! as the outer ASC), and a second 11-bit `syncExtensionType` +//! gated on `>= 12` further bits — if it equals `0x548` the +//! `psPresentFlag` follows. The AOT-22 (ER BSAC) extension branch +//! is also parsed: `sbrPresentFlag` (+ optional +//! `extensionSamplingFrequencyIndex`) then a mandatory 4-bit +//! `extensionChannelConfiguration`. The probe result lands in +//! [`AudioSpecificConfig::trailing_sbr_probe`] as +//! [`SbrExtensionProbe`]; when the probe resolves SBR or PS, +//! `asc.sbr_present` / `asc.ps_present` are updated to reflect +//! the implicit signalling. This entry point is exposed as +//! [`AudioSpecificConfig::parse_bits_bounded`] for carriers that +//! know the ASC bit length (LATM `StreamMuxConfig`, esds AudioObj +//! descriptor); the byte-slice [`AudioSpecificConfig::parse`] +//! computes the bound automatically. The original bit-level +//! [`AudioSpecificConfig::parse_bits`] keeps its no-probe +//! semantics so existing callers that pass a BitReader carrying +//! trailing carrier bytes are not surprised by a stray 11-bit +//! match. +//! +//! ## What is *not* parsed yet +//! +//! * `AOT 5` / `AOT 29` *implicit-extension* path **via the FIL +//! extension_payload**: when the outer AOT is 2 (LC) and the +//! SBR/PS extension is announced via the FIL `extension_payload` +//! inside the raw_data_block stream (not the ASC trailing probe), +//! the ASC alone does not carry the information — the decoder +//! must look at the FIL stream. Round 192 only resolves the +//! *ASC-side* implicit signalling (the Table 1.15 +//! `syncExtensionType == 0x2b7` probe). When neither signalling +//! form is present, the ASC parser correctly records +//! `sbr_present = false` / `ps_present = false` because no ASC +//! bit said otherwise. +//! +//! ## What round 177 adds +//! +//! * `GASpecificConfig` `extensionFlag == 1` body (Table 4.1): +//! AOT 22 (ER BSAC) emits a 5-bit `numOfSubFrame` + 11-bit +//! `layer_length`; AOTs 17 / 19 / 20 / 23 emit the 1-bit +//! `aacSectionDataResilienceFlag` + 1-bit +//! `aacScalefactorDataResilienceFlag` + 1-bit +//! `aacSpectralDataResilienceFlag` triplet; every AOT closes the +//! body with a 1-bit `extensionFlag3` (the Version 3 body behind it +//! is reserved per the spec's own "tbd in version 3" comment, so the +//! bit is surfaced but the body is rejected with +//! [`Error::UnsupportedAscExtensionFlag3`] when set). +//! * `epConfig` for ER object types (Table 1.15) — the 2-bit +//! `epConfig` field that follows the AOT body for AOTs 17, 19, 20, +//! 21, 22, 23, 24, 25, 26, 27, 39. `epConfig == 2` or +//! `epConfig == 3` further triggers the +//! `ErrorProtectionSpecificConfig()` body, which Phase 1 does not +//! parse — the ASC parser surfaces +//! [`Error::UnsupportedEpConfig`] in that case rather than +//! silently returning a partial ASC. + +use oxideav_core::bits::BitReader; + +use crate::adts::ADTS_SAMPLE_RATES_HZ; +use crate::pce::Pce; +use crate::{Error, Result}; + +/// Outer `audioObjectType` values for which the ASC body is +/// `GASpecificConfig` per Table 1.17. +const GA_AOTS: &[u8] = &[1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23]; + +/// AOTs that signal SBR (5) or SBR + PS (29) as an outer wrapper +/// around an inner GA AOT (typically 2 = LC). The ASC walks the +/// extension sample-rate/index and re-reads `GetAudioObjectType` +/// before dispatching to the inner body. +const SBR_AOT: u8 = 5; +const PS_AOT: u8 = 29; + +/// AOTs whose `GASpecificConfig` extension-flag body emits the 5-bit +/// `numOfSubFrame` + 11-bit `layer_length` pair (Table 4.1). +const GA_EXTENSION_NUM_OF_SUBFRAME_AOTS: &[u8] = &[22]; + +/// AOTs whose `GASpecificConfig` extension-flag body emits the three +/// error-resilience flags (Table 4.1). +const GA_EXTENSION_RESILIENCE_AOTS: &[u8] = &[17, 19, 20, 23]; + +/// AOTs whose ASC trailing body carries the 2-bit `epConfig` field +/// (Table 1.15 outer `switch (audioObjectType)` for the ER object +/// types). +const EP_CONFIG_AOTS: &[u8] = &[17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39]; + +/// Outer 11-bit `syncExtensionType` marker that introduces the Table +/// 1.15 trailing implicit-SBR signalling block. +pub const SYNC_EXTENSION_TYPE_SBR: u16 = 0x2b7; + +/// Inner 11-bit `syncExtensionType` marker that introduces the +/// `psPresentFlag` inside the SBR (`extensionAudioObjectType == 5`) +/// branch of the Table 1.15 trailing probe. +pub const SYNC_EXTENSION_TYPE_PS: u16 = 0x548; + +/// Width of the `syncExtensionType` field (Table 1.15). +pub const SYNC_EXTENSION_TYPE_BITS: u32 = 11; + +/// `extensionAudioObjectType` value that signals HE-AAC SBR inside +/// the trailing probe (Table 1.15). +pub const TRAILING_EXTENSION_AOT_SBR: u8 = 5; + +/// `extensionAudioObjectType` value that signals ER BSAC inside the +/// trailing probe (Table 1.15). +pub const TRAILING_EXTENSION_AOT_BSAC: u8 = 22; + +/// The raw `frameLengthFlag` of `GASpecificConfig` — ISO/IEC 14496-3 +/// §4.5.1.1 semantics. The flag's meaning is AOT-dependent: for every +/// GA AOT except AAC SSR and ER AAC LD it selects 1024 vs 960 IMDCT +/// lines; for ER AAC LD (AOT 23) the same flag selects 512 vs 480 +/// (use [`crate::swb_offset::FrameFamily::from_aot_and_flag`] to +/// resolve the actual frame geometry). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameLength { + /// `frameLengthFlag == 0` — 1024 lines (512 for ER AAC LD). + Long1024, + /// `frameLengthFlag == 1` — 960 lines (480 for ER AAC LD). + Long960, +} + +impl FrameLength { + /// Resolved sample count per output channel for the non-LD GA + /// AOTs. For ER AAC LD resolve through + /// [`crate::swb_offset::FrameFamily::from_aot_and_flag`] instead + /// (the same flag means 512/480 there). + pub fn samples(self) -> u32 { + match self { + FrameLength::Long1024 => 1024, + FrameLength::Long960 => 960, + } + } + + /// Resolve the §4.5.1.1 frame-length family for `aot`. + pub fn family(self, aot: u8) -> crate::swb_offset::FrameFamily { + crate::swb_offset::FrameFamily::from_aot_and_flag(aot, self == FrameLength::Long960) + } +} + +/// Parsed `GASpecificConfig` body (Table 4.1). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GaSpecificConfig { + /// Resolved frame length (1024 vs 960 lines). + pub frame_length: FrameLength, + /// `dependsOnCoreCoder` bit. `false` for plain AAC LC. + pub depends_on_core_coder: bool, + /// `coreCoderDelay` (14 bits, only present when + /// `dependsOnCoreCoder == 1`). + pub core_coder_delay: Option, + /// `extensionFlag` bit. Shall be `false` for AOTs 1, 2, 3, 4, + /// 6, 7; shall be `true` for AOTs 17, 19, 20, 21, 22, 23. + pub extension_flag: bool, + /// Inline `program_config_element()` (only present when the + /// surrounding ASC's `channelConfiguration == 0`). + pub pce: Option, + /// `layerNr` (3 bits, only present when AOT ∈ {6, 20}). + pub layer_nr: Option, + /// Parsed extension-flag body (only populated when + /// `extension_flag == true`). + pub extension_body: Option, +} + +/// Parsed body of the `if (extensionFlag)` branch of `GASpecificConfig` +/// (Table 4.1). Carries AOT-dependent subfields plus the always-present +/// `extensionFlag3` bit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GaExtensionBody { + /// `numOfSubFrame` (5 bits) + `layer_length` (11 bits). Only + /// present when `audioObjectType == 22` (ER BSAC). + pub bsac_layer: Option, + /// Error-resilience triplet. Only present when + /// `audioObjectType ∈ {17, 19, 20, 23}` (ER AAC LC / ER AAC LTP / + /// ER AAC scalable / ER AAC LD). + pub resilience: Option, + /// `extensionFlag3` (1 bit). Always present at the tail of the + /// extension-flag body. ISO/IEC 14496-3:2009 reserves the body + /// behind this flag with the comment "tbd in version 3"; Phase 1 + /// surfaces the bit but rejects the body itself with + /// [`Error::UnsupportedAscExtensionFlag3`] when the flag is set. + pub extension_flag3: bool, +} + +/// `numOfSubFrame` + `layer_length` pair from Table 4.1, only emitted +/// when the surrounding `audioObjectType == 22` (ER BSAC). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BsacLayerSpec { + /// 5-bit `numOfSubFrame` field. + pub num_of_sub_frame: u8, + /// 11-bit `layer_length` field. + pub layer_length: u16, +} + +/// `aacSection / Scalefactor / Spectral DataResilienceFlag` triplet from +/// Table 4.1, only emitted when the surrounding `audioObjectType ∈ +/// {17, 19, 20, 23}`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct AacResilienceFlags { + /// `aacSectionDataResilienceFlag`. Routes `section_data()` through + /// the §4.4.6 RVLC branch in a downstream round. + pub section_data: bool, + /// `aacScalefactorDataResilienceFlag`. Routes `scale_factor_data()` + /// through the §4.4.6 RVLC branch in a downstream round. + pub scalefactor_data: bool, + /// `aacSpectralDataResilienceFlag`. Routes `spectral_data()` through + /// the §4.4.6 HCR / reordered branch in a downstream round. + pub spectral_data: bool, +} + +/// Result of the Table 1.15 trailing `syncExtensionType == 0x2b7` +/// implicit-SBR / PS / BSAC-extension probe (§1.6.5). +/// +/// Only ever populated when the ASC parser reaches the trailing-bits +/// branch — i.e. the outer `audioObjectType` is **not** the +/// hierarchical SBR wrapper (5) or PS wrapper (29) (those already +/// emit `sbr_present` / `ps_present` from their explicit-signalling +/// path), at least 16 bits remain in the ASC carrier, and the next +/// 11 bits equal [`SYNC_EXTENSION_TYPE_SBR`] (`0x2b7`). +/// +/// `extension_audio_object_type` is the resolved nested AOT +/// (`GetAudioObjectType()` after the `0x2b7` sync). Round 192 +/// implements the bodies for `extension_audio_object_type == 5` +/// (HE-AAC SBR with the optional `0x548` PS sub-probe) and +/// `extension_audio_object_type == 22` (ER BSAC); any other resolved +/// extension AOT surfaces as +/// [`crate::Error::UnsupportedTrailingExtensionAot`] at parse time +/// (the body bit-layout is not defined by Table 1.15 for those). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SbrExtensionProbe { + /// Resolved `extensionAudioObjectType` immediately after the + /// 11-bit `syncExtensionType == 0x2b7` marker. Currently + /// constrained to `5` (HE-AAC) or `22` (ER BSAC). + pub extension_audio_object_type: u8, + /// `sbrPresentFlag` (1 bit). Present for both the `ext_aot == 5` + /// and `ext_aot == 22` branches. + pub sbr_present_flag: bool, + /// `extensionSamplingFrequencyIndex` (4 bits). Only present when + /// `sbr_present_flag == true`; when the wire value is `0xf` the + /// 24-bit `extensionSamplingFrequency` escape follows and the + /// resolved rate lands in + /// [`SbrExtensionProbe::extension_sample_rate`]. + pub extension_sampling_frequency_index: Option, + /// Resolved extension sample rate in Hz (Table 1.18 lookup, or + /// the 24-bit escape value when `extension_sampling_frequency_index + /// == Some(0xf)`). + pub extension_sample_rate: Option, + /// `psPresentFlag` (1 bit). Only present when the SBR (`ext_aot + /// == 5`) branch ran, at least 12 further bits were available, and + /// the second 11-bit `syncExtensionType` equalled + /// [`SYNC_EXTENSION_TYPE_PS`] (`0x548`). + pub ps_present_flag: Option, + /// `extensionChannelConfiguration` (4 bits). Only present when + /// the resolved extension AOT is `22` (ER BSAC). + pub extension_channel_configuration: Option, +} + +/// Parsed `AudioSpecificConfig`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AudioSpecificConfig { + /// Outer `audioObjectType` *as encoded on the wire* (before any + /// SBR/PS unwrap). For HE-AAC v1 signalled hierarchically this + /// is `5`; for HE-AAC v2 it is `29`. + pub outer_aot: u8, + /// Inner / effective `audioObjectType` after unwrapping the + /// AOT-5 (SBR) and AOT-29 (PS) hierarchical containers. For + /// plain AAC-LC this equals `outer_aot`. + pub aot: u8, + /// 4-bit `samplingFrequencyIndex` (the *core* index — for + /// hierarchical HE-AAC this is the inner AAC's index, half the + /// SBR output rate). + pub sampling_frequency_index: u8, + /// Resolved core sample rate. Resolves + /// [`AudioSpecificConfig::sampling_frequency_index`] via + /// Table 1.18, or reads the explicit 24-bit + /// `samplingFrequency` field when the index is `0xf`. + pub sample_rate: u32, + /// `channelConfiguration` (4 bits). `0` ⇔ defined by an inline + /// PCE inside `GASpecificConfig`. + pub channel_configuration: u8, + /// `true` ⇔ the ASC explicitly signalled SBR (outer AOT 5 or + /// 29). Does **not** capture implicit SBR signalling carried in + /// the FIL `extension_payload` of the AAC bitstream. + pub sbr_present: bool, + /// `true` ⇔ the ASC explicitly signalled PS (outer AOT 29). + pub ps_present: bool, + /// `extensionSamplingFrequencyIndex` (only present when + /// `outer_aot ∈ {5, 29}`). + pub extension_sampling_frequency_index: Option, + /// Resolved extension sample rate (SBR output rate). Present + /// when `extension_sampling_frequency_index` is set. + pub extension_sample_rate: Option, + /// `extensionChannelConfiguration` (only present when + /// `outer_aot ∈ {5, 29}` *and* the inner AOT is `22` = + /// ER BSAC). + pub extension_channel_configuration: Option, + /// Parsed body for the inner AOT. For GA AOTs this is + /// populated; for other AOTs (which Phase 1 rejects with + /// [`Error::UnsupportedAot`]) this is never returned. + pub ga_body: GaSpecificConfig, + /// `epConfig` (2 bits) for the ER object types listed in the + /// Table 1.15 outer `switch (audioObjectType)` (AOTs 17, 19, 20, + /// 21, 22, 23, 24, 25, 26, 27, 39). `None` for every other AOT. + /// When the field is `2` or `3`, the spec mandates parsing the + /// trailing `ErrorProtectionSpecificConfig()` body — Phase 1 + /// does **not** parse that body and surfaces + /// [`Error::UnsupportedEpConfig`] at the call site. + pub ep_config: Option, + + /// The parsed `ErrorProtectionSpecificConfig()` (§1.8.2.1 + /// Table 1.49) when `epConfig == 2 || epConfig == 3`. + pub error_protection: Option, + + /// `directMapping` (1 bit, Table 1.15) when `epConfig == 3`: the + /// §1.8.1 EP-class ↔ error-sensitivity-category-instance mapping + /// selector. + pub direct_mapping: Option, + /// Result of the Table 1.15 trailing `syncExtensionType == 0x2b7` + /// implicit-SBR probe (§1.6.5). Only ever populated when the + /// outer `audioObjectType` is not the explicit SBR (5) or PS + /// (29) wrapper, the carrier had at least 16 bits remaining + /// after the per-AOT body + `epConfig`, and the next 11 bits + /// equalled [`SYNC_EXTENSION_TYPE_SBR`]. When the probe resolves + /// SBR or PS, [`AudioSpecificConfig::sbr_present`] / + /// [`AudioSpecificConfig::ps_present`] are also updated to + /// reflect the implicit signalling. Only populated by + /// [`AudioSpecificConfig::parse`] (which knows the byte-slice + /// bound) and the new [`AudioSpecificConfig::parse_bits_bounded`] + /// entry point; the older + /// [`AudioSpecificConfig::parse_bits`] keeps its no-probe + /// semantics. + pub trailing_sbr_probe: Option, +} + +impl AudioSpecificConfig { + /// Parse an `AudioSpecificConfig` from `data`. Returns the + /// resolved ASC and the bit-length consumed (so the caller can + /// skip the rest of the carrier — `esds` payload, LATM + /// StreamMuxConfig, etc.). + /// + /// The byte-slice bound is also forwarded into the Table 1.15 + /// trailing `syncExtensionType == 0x2b7` implicit-SBR probe + /// (§1.6.5), so the bit-length returned here already reflects + /// any consumed trailing-probe fields. + pub fn parse(data: &[u8]) -> Result<(Self, u64)> { + let mut reader = BitReader::new(data); + let asc_bit_length = (data.len() as u64).saturating_mul(8); + let asc = Self::parse_bits_bounded(&mut reader, 0, asc_bit_length)?; + Ok((asc, reader.bit_position())) + } + + /// Parse from a pre-existing [`BitReader`] given the + /// `origin_bit_offset` (the absolute bit position of the start + /// of the ASC) and an explicit `asc_bit_length` (the total + /// bit-length of the ASC inside the carrier, as conveyed by + /// e.g. LATM `StreamMuxConfig`'s `audioSpecificConfig` length + /// field). The trailing Table 1.15 `syncExtensionType == 0x2b7` + /// probe consumes bits up to that bound. + pub fn parse_bits_bounded( + reader: &mut BitReader<'_>, + origin_bit_offset: u64, + asc_bit_length: u64, + ) -> Result { + let start_bit = reader.bit_position(); + let mut asc = Self::parse_bits_core(reader, origin_bit_offset)?; + let consumed = reader.bit_position().saturating_sub(start_bit); + // The Table 1.15 trailing-probe guard `extensionAudioObjectType + // != 5` translates into "skip the probe when the explicit + // hierarchical SBR (outer AOT 5) or PS (outer AOT 29) wrapper + // already established `extensionAudioObjectType == 5`". For + // every other outer AOT the spec defaults + // `extensionAudioObjectType = 0` (per §1.6.5), so the + // `!= 5` predicate is satisfied and the probe runs. + let already_hierarchical_sbr = asc.outer_aot == SBR_AOT || asc.outer_aot == PS_AOT; + if !already_hierarchical_sbr && consumed < asc_bit_length { + let remaining = asc_bit_length - consumed; + if let Some(probe) = parse_trailing_sbr_probe(reader, remaining)? { + if probe.extension_audio_object_type == TRAILING_EXTENSION_AOT_SBR { + if probe.sbr_present_flag { + asc.sbr_present = true; + asc.extension_sampling_frequency_index = + probe.extension_sampling_frequency_index; + asc.extension_sample_rate = probe.extension_sample_rate; + } + if probe.ps_present_flag == Some(true) { + asc.ps_present = true; + } + } else if probe.extension_audio_object_type == TRAILING_EXTENSION_AOT_BSAC { + if probe.sbr_present_flag { + asc.sbr_present = true; + asc.extension_sampling_frequency_index = + probe.extension_sampling_frequency_index; + asc.extension_sample_rate = probe.extension_sample_rate; + } + asc.extension_channel_configuration = probe.extension_channel_configuration; + } + asc.trailing_sbr_probe = Some(probe); + } + } + Ok(asc) + } + + /// Parse from a pre-existing [`BitReader`] given the + /// `origin_bit_offset` (the absolute bit position of the start + /// of the ASC). Used by carriers that embed an ASC inside a + /// wider bit-stream — LATM `StreamMuxConfig` being the obvious + /// case, where the ASC starts at a non-byte-aligned position + /// relative to the LATM packet's first bit. The + /// `origin_bit_offset` is forwarded into PCE parsing so the + /// Table 4.2 `byte_alignment()` note is honoured. + /// + /// This entry point does **not** invoke the Table 1.15 trailing + /// `syncExtensionType == 0x2b7` implicit-SBR probe: the + /// `BitReader` may carry trailing carrier bytes that are not + /// part of the ASC, and probing into them would mis-interpret + /// garbage as a `0x2b7` marker. Carriers that know the exact + /// ASC bit-length should call + /// [`AudioSpecificConfig::parse_bits_bounded`] instead. + pub fn parse_bits(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result { + Self::parse_bits_core(reader, origin_bit_offset) + } + + fn parse_bits_core(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result { + // Outer audioObjectType + samplingFrequencyIndex (+ escape) + let outer_aot = read_aot(reader)?; + let sampling_frequency_index = read_u8(reader, 4)?; + let core_sample_rate = if sampling_frequency_index == 0xf { + read_u32(reader, 24)? + } else { + resolve_sample_rate_index(sampling_frequency_index)? + }; + let channel_configuration = read_u8(reader, 4)?; + + // Hierarchical SBR / PS unwrap. + let mut sbr_present = false; + let mut ps_present = false; + let mut ext_sfi = None; + let mut ext_rate = None; + let mut ext_chan_cfg = None; + let mut effective_aot = outer_aot; + + if outer_aot == SBR_AOT || outer_aot == PS_AOT { + sbr_present = true; + if outer_aot == PS_AOT { + ps_present = true; + } + let sfi = read_u8(reader, 4)?; + let rate = if sfi == 0xf { + read_u32(reader, 24)? + } else { + resolve_sample_rate_index(sfi)? + }; + ext_sfi = Some(sfi); + ext_rate = Some(rate); + effective_aot = read_aot(reader)?; + if effective_aot == 22 { + ext_chan_cfg = Some(read_u8(reader, 4)?); + } + } + + // Body dispatch — Phase 1 only handles GA. + if !GA_AOTS.contains(&effective_aot) { + return Err(Error::UnsupportedAot(effective_aot)); + } + let ga_body = parse_ga_specific_config( + reader, + channel_configuration, + effective_aot, + origin_bit_offset, + )?; + + // Table 1.15 outer `switch (audioObjectType)` — `epConfig` + // for ER object types. `epConfig == 2 || epConfig == 3` + // triggers the `ErrorProtectionSpecificConfig()` body which + // Phase 1 does not parse. + let mut error_protection = None; + let mut direct_mapping = None; + let ep_config = if EP_CONFIG_AOTS.contains(&effective_aot) { + let v = read_u8(reader, 2)?; + // Table 1.15: epConfig 2 / 3 carry the inline + // ErrorProtectionSpecificConfig(); epConfig 3 additionally + // signals the §1.8.1 directMapping selector. + if v == 2 || v == 3 { + error_protection = Some(crate::ep_config::ErrorProtectionSpecificConfig::parse( + reader, + )?); + } + if v == 3 { + direct_mapping = Some(read_bit(reader)?); + } + Some(v) + } else { + None + }; + + Ok(AudioSpecificConfig { + outer_aot, + aot: effective_aot, + sampling_frequency_index, + sample_rate: core_sample_rate, + channel_configuration, + sbr_present, + ps_present, + extension_sampling_frequency_index: ext_sfi, + extension_sample_rate: ext_rate, + extension_channel_configuration: ext_chan_cfg, + ga_body, + ep_config, + error_protection, + direct_mapping, + trailing_sbr_probe: None, + }) + } + + /// Number of audio channels implied by the + /// `channelConfiguration` (Table 1.19); `0` means "defined by + /// PCE" and returns the PCE-derived count. + pub fn channel_count(&self) -> usize { + match self.channel_configuration { + 0 => self + .ga_body + .pce + .as_ref() + .map(Pce::channel_count) + .unwrap_or(0), + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, // 5.1 — LFE counts as one channel + 7 => 8, // 7.1 — LFE counts as one channel + _ => 0, + } + } +} + +fn parse_ga_specific_config( + reader: &mut BitReader<'_>, + channel_configuration: u8, + aot: u8, + origin_bit_offset: u64, +) -> Result { + // Table 4.1 — GASpecificConfig. + let frame_length_flag = read_bit(reader)?; + let frame_length = if frame_length_flag { + FrameLength::Long960 + } else { + FrameLength::Long1024 + }; + let depends_on_core_coder = read_bit(reader)?; + let core_coder_delay = if depends_on_core_coder { + Some(read_u32(reader, 14)? as u16) + } else { + None + }; + let extension_flag = read_bit(reader)?; + + let pce = if channel_configuration == 0 { + Some(Pce::parse(reader, origin_bit_offset)?) + } else { + None + }; + + let layer_nr = if aot == 6 || aot == 20 { + Some(read_u8(reader, 3)?) + } else { + None + }; + + let extension_body = if extension_flag { + Some(parse_ga_extension_body(reader, aot)?) + } else { + None + }; + + Ok(GaSpecificConfig { + frame_length, + depends_on_core_coder, + core_coder_delay, + extension_flag, + pce, + layer_nr, + extension_body, + }) +} + +/// Parse the `if (extensionFlag)` body of `GASpecificConfig()` per +/// Table 4.1. Subfield gating mirrors the AOT lists in the spec +/// listing exactly: `numOfSubFrame` / `layer_length` only for +/// `audioObjectType == 22`; the resilience triplet only for +/// `audioObjectType ∈ {17, 19, 20, 23}`; `extensionFlag3` always. +fn parse_ga_extension_body(reader: &mut BitReader<'_>, aot: u8) -> Result { + let bsac_layer = if GA_EXTENSION_NUM_OF_SUBFRAME_AOTS.contains(&aot) { + let num_of_sub_frame = read_u8(reader, 5)?; + let layer_length = read_u32(reader, 11)? as u16; + Some(BsacLayerSpec { + num_of_sub_frame, + layer_length, + }) + } else { + None + }; + + let resilience = if GA_EXTENSION_RESILIENCE_AOTS.contains(&aot) { + let section_data = read_bit(reader)?; + let scalefactor_data = read_bit(reader)?; + let spectral_data = read_bit(reader)?; + Some(AacResilienceFlags { + section_data, + scalefactor_data, + spectral_data, + }) + } else { + None + }; + + let extension_flag3 = read_bit(reader)?; + if extension_flag3 { + return Err(Error::UnsupportedAscExtensionFlag3); + } + + Ok(GaExtensionBody { + bsac_layer, + resilience, + extension_flag3, + }) +} + +/// Probe the Table 1.15 trailing `syncExtensionType == 0x2b7` / +/// `0x548` chain for implicit SBR / PS / BSAC-extension signalling +/// (§1.6.5, §1.6.6). +/// +/// Returns `Ok(None)` if any of the following holds (each is a +/// normative "no implicit signalling present" outcome — never an +/// error): +/// +/// * Fewer than `SYNC_EXTENSION_TYPE_BITS + 5 = 16` bits remain +/// (the spec's outer `bits_to_decode() >= 16` guard). +/// * The next 11 bits are not [`SYNC_EXTENSION_TYPE_SBR`] (0x2b7). +/// +/// When the outer 0x2b7 marker fires but the resolved +/// `extensionAudioObjectType` is neither `5` nor `22`, the parser +/// returns [`Error::UnsupportedTrailingExtensionAot`] — Table 1.15 +/// does not specify a body layout for any other extension AOT and +/// the bit-reader cannot advance. +/// +/// The `remaining_bits` parameter is the upper bound of bits the +/// probe is allowed to consume from the carrier (typically the +/// ASC's `bits_to_decode()`). The function never reads more than +/// `remaining_bits` bits; an UnexpectedEnd surfaces if a sub-field +/// extends past it. +fn parse_trailing_sbr_probe( + reader: &mut BitReader<'_>, + remaining_bits: u64, +) -> Result> { + // Outer §1.6.2.1 guard: at least 16 bits required to even + // attempt the probe (`syncExtensionType` + the minimum 5-bit + // `GetAudioObjectType()` base it gates). + if remaining_bits < (SYNC_EXTENSION_TYPE_BITS as u64 + 5) { + return Ok(None); + } + let sync = read_u32(reader, SYNC_EXTENSION_TYPE_BITS)? as u16; + if sync != SYNC_EXTENSION_TYPE_SBR { + return Ok(None); + } + + let extension_audio_object_type = read_aot(reader)?; + match extension_audio_object_type { + TRAILING_EXTENSION_AOT_SBR => parse_trailing_sbr_branch(reader, remaining_bits), + TRAILING_EXTENSION_AOT_BSAC => parse_trailing_bsac_branch(reader), + other => Err(Error::UnsupportedTrailingExtensionAot(other)), + } +} + +/// `extensionAudioObjectType == 5` body of the trailing probe: +/// `sbrPresentFlag` + optional `extensionSamplingFrequencyIndex` / +/// `extensionSamplingFrequency` + optional second `syncExtensionType +/// == 0x548` + `psPresentFlag` (Table 1.15). +fn parse_trailing_sbr_branch( + reader: &mut BitReader<'_>, + initial_remaining_bits: u64, +) -> Result> { + let sbr_present_flag = read_bit(reader)?; + let mut extension_sampling_frequency_index = None; + let mut extension_sample_rate = None; + let mut ps_present_flag = None; + if sbr_present_flag { + let sfi = read_u8(reader, 4)?; + let rate = if sfi == 0xf { + read_u32(reader, 24)? + } else { + resolve_sample_rate_index(sfi)? + }; + extension_sampling_frequency_index = Some(sfi); + extension_sample_rate = Some(rate); + // §1.6.2.1 inner guard: at least 12 further bits required + // to attempt the PS sub-probe (11-bit syncExtensionType + + // 1-bit psPresentFlag). + let consumed_so_far = SYNC_EXTENSION_TYPE_BITS as u64 + + 5 // GetAudioObjectType base + + 1 // sbrPresentFlag + + 4 // extensionSamplingFrequencyIndex + + if sfi == 0xf { 24 } else { 0 }; + let still_available = initial_remaining_bits.saturating_sub(consumed_so_far); + if still_available >= 12 { + let inner_sync = read_u32(reader, SYNC_EXTENSION_TYPE_BITS)? as u16; + if inner_sync == SYNC_EXTENSION_TYPE_PS { + ps_present_flag = Some(read_bit(reader)?); + } + } + } + Ok(Some(SbrExtensionProbe { + extension_audio_object_type: TRAILING_EXTENSION_AOT_SBR, + sbr_present_flag, + extension_sampling_frequency_index, + extension_sample_rate, + ps_present_flag, + extension_channel_configuration: None, + })) +} + +/// `extensionAudioObjectType == 22` body of the trailing probe: +/// `sbrPresentFlag` + optional `extensionSamplingFrequencyIndex` / +/// `extensionSamplingFrequency` + mandatory +/// `extensionChannelConfiguration` (Table 1.15). +fn parse_trailing_bsac_branch(reader: &mut BitReader<'_>) -> Result> { + let sbr_present_flag = read_bit(reader)?; + let mut extension_sampling_frequency_index = None; + let mut extension_sample_rate = None; + if sbr_present_flag { + let sfi = read_u8(reader, 4)?; + let rate = if sfi == 0xf { + read_u32(reader, 24)? + } else { + resolve_sample_rate_index(sfi)? + }; + extension_sampling_frequency_index = Some(sfi); + extension_sample_rate = Some(rate); + } + let extension_channel_configuration = Some(read_u8(reader, 4)?); + Ok(Some(SbrExtensionProbe { + extension_audio_object_type: TRAILING_EXTENSION_AOT_BSAC, + sbr_present_flag, + extension_sampling_frequency_index, + extension_sample_rate, + ps_present_flag: None, + extension_channel_configuration, + })) +} + +/// Table 1.16 — `GetAudioObjectType()`. 5-bit base, with the `31` +/// escape unlocking a 6-bit extension. +fn read_aot(reader: &mut BitReader<'_>) -> Result { + let base = read_u8(reader, 5)?; + if base == 31 { + let ext = read_u8(reader, 6)?; + // Per spec the result is `32 + audioObjectTypeExt`. AOTs + // above 41 are not defined in ISO/IEC 14496-3:2009; the + // parser preserves the wire value and the body dispatch + // will reject it. + let aot = 32u16 + ext as u16; + if aot > u8::MAX as u16 { + return Err(Error::UnsupportedAot(0)); + } + Ok(aot as u8) + } else { + Ok(base) + } +} + +fn resolve_sample_rate_index(idx: u8) -> Result { + if (idx as usize) >= ADTS_SAMPLE_RATES_HZ.len() { + return Err(Error::AdtsReservedSampleRateIndex); + } + Ok(ADTS_SAMPLE_RATES_HZ[idx as usize]) +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +fn read_u32(reader: &mut BitReader<'_>, n: u32) -> Result { + reader.read_u32(n).map_err(|_| Error::UnexpectedEnd) +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} diff --git a/crates/vendor/oxideav-aac/src/bsac_arith.rs b/crates/vendor/oxideav-aac/src/bsac_arith.rs new file mode 100644 index 00000000..0f914ee2 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/bsac_arith.rs @@ -0,0 +1,413 @@ +//! BSAC arithmetic decoder — ISO/IEC 14496-3:2009 §4.5.2.6.2.7. +//! +//! The ER BSAC noiseless coder replaces the AAC Huffman machinery +//! with a single arithmetic code over the whole +//! `bsac_raw_data_block()` (or, in SBA mode, over each segment). +//! The spec normatively lists the decoding procedure as C source +//! (§4.5.2.6.2.7.4); this module transcribes it exactly: +//! +//! * [`ArithDecoder::decode_symbol`] — the general multi-symbol +//! decode over a 14-bit cumulative-frequency model (`cband_si`, +//! scalefactors, stereo / PNS side info). +//! * [`ArithDecoder::decode_bit`] — the binary decode over a 14-bit +//! `p0` (spectral bit slices and sign bits). +//! +//! Both return the **estimated codeword length** (`est_cw_len`) the +//! spec defines — the renormalization shift that will be consumed +//! before the *next* symbol — which the §4.5.2.6.2.5 layer budget +//! (`available_len[]`) bookkeeping subtracts per decoded symbol. +//! +//! The register discipline follows the listing: `value` and `range` +//! are 32-bit quantities (held in `u64` here — the products +//! `range · cum_freq` stay under 2^30, so the arithmetic is +//! identical), `range` starts at 1 with `est_cw_len = 30`, and +//! renormalization scans the `half[]` table (2^29 … 2^14). +//! +//! Reads past the end of the segment buffer return the +//! §4.5.2.6.2.2.1 zero stuffing (a conforming stream never consumes +//! more than 32 such bits; the layer budgets bound all decode +//! loops, so the reader simply keeps yielding zeros). + +/// The §4.5.2.6.2.7.1 `half[]` table: 32-bit fixed-point values of +/// ½ at descending magnitudes (2^29 down to 2^14). +const HALF: [u64; 16] = [ + 0x2000_0000, + 0x1000_0000, + 0x0800_0000, + 0x0400_0000, + 0x0200_0000, + 0x0100_0000, + 0x0080_0000, + 0x0040_0000, + 0x0020_0000, + 0x0010_0000, + 0x0008_0000, + 0x0004_0000, + 0x0002_0000, + 0x0001_0000, + 0x0000_8000, + 0x0000_4000, +]; + +/// MSB-first bit reader over one arithmetic segment: a bit window +/// `[start_bit, end_bit)` of the frame buffer, followed by the +/// §4.5.2.6.2.2.1 zero stuffing (zeros for every read past the +/// window). +#[derive(Debug, Clone)] +pub struct SegmentReader<'a> { + data: &'a [u8], + /// Absolute next bit position within `data`. + pos: u64, + /// Absolute end of the segment window within `data`. + end: u64, + /// Bits consumed beyond `end` (the zero-stuffing tail). + overrun: u64, +} + +impl<'a> SegmentReader<'a> { + /// A reader over bits `[start_bit, end_bit)` of `data`. + /// `end_bit` is clamped to the buffer size. + pub fn new(data: &'a [u8], start_bit: u64, end_bit: u64) -> Self { + let cap = (data.len() as u64) * 8; + SegmentReader { + data, + pos: start_bit.min(cap), + end: end_bit.min(cap), + overrun: 0, + } + } + + /// Read `n` bits MSB-first (zeros past the window end). + fn read_bits(&mut self, n: u32) -> u64 { + let mut v = 0u64; + for _ in 0..n { + let bit = if self.pos < self.end { + let byte = self.data[(self.pos >> 3) as usize]; + u64::from((byte >> (7 - (self.pos & 7))) & 1) + } else { + self.overrun += 1; + 0 + }; + self.pos += 1; + v = (v << 1) | bit; + } + v + } + + /// Bits consumed past the segment window (the zero-stuffing + /// depth). A conforming stream stays at or under 32. + pub fn overrun(&self) -> u64 { + self.overrun + } +} + +/// The §4.5.2.6.2.7 arithmetic decoder registers. +#[derive(Debug, Clone)] +pub struct ArithDecoder { + value: u64, + range: u64, + est_cw_len: u32, +} + +impl Default for ArithDecoder { + fn default() -> Self { + Self::new() + } +} + +impl ArithDecoder { + /// §4.5.2.6.2.7.2 initialization: `value = 0`, `range = 1`, + /// `est_cw_len = 30`. Called at the start of every segment. + pub fn new() -> Self { + ArithDecoder { + value: 0, + range: 1, + est_cw_len: 30, + } + } + + /// Renormalize against `half[]`: the returned `est_cw_len` is + /// the shift consumed before the next symbol. + fn renormalize(&mut self) -> u32 { + let mut est = 0u32; + while est < HALF.len() as u32 && self.range < HALF[est as usize] { + est += 1; + } + self.est_cw_len = est; + est + } + + /// The renormalization shift the next decode will consume. + pub fn pending_est(&self) -> u32 { + self.est_cw_len + } + + /// §4.5.2.6.2.7.4 `decode_symbol()`: general arithmetic decode + /// over a cumulative-frequency model (14-bit fixed point, + /// strictly decreasing, last entry 0). Returns + /// `(symbol, est_cw_len)`. + pub fn decode_symbol( + &mut self, + reader: &mut SegmentReader<'_>, + cum_freq: &[u16], + ) -> (usize, u32) { + if self.est_cw_len > 0 { + self.range <<= self.est_cw_len; + self.value = (self.value << self.est_cw_len) | reader.read_bits(self.est_cw_len); + } + self.range >>= 14; + let cum = self.value.checked_div(self.range).unwrap_or(0); + // The listing's `for (sym = 0; cum_freq[sym] > cum; sym++)` + // — the last entry is 0 <= cum, so it terminates in range. + let mut sym = 0usize; + while sym + 1 < cum_freq.len() && u64::from(cum_freq[sym]) > cum { + sym += 1; + } + self.value -= self.range * u64::from(cum_freq[sym]); + let width = if sym > 0 { + u64::from(cum_freq[sym - 1]) - u64::from(cum_freq[sym]) + } else { + 16384 - u64::from(cum_freq[sym]) + }; + self.range *= width; + (sym, self.renormalize()) + } + + /// §4.5.2.6.2.7.4 `decode_symbol2()`: binary arithmetic decode + /// with `p0` the 14-bit probability of the "0" symbol. Returns + /// `(bit, est_cw_len)`. + pub fn decode_bit(&mut self, reader: &mut SegmentReader<'_>, p0: u16) -> (u8, u32) { + if self.est_cw_len > 0 { + self.range <<= self.est_cw_len; + self.value = (self.value << self.est_cw_len) | reader.read_bits(self.est_cw_len); + } + self.range >>= 14; + let p0 = u64::from(p0); + let bit; + if p0 * self.range <= self.value { + bit = 1; + self.value -= self.range * p0; + self.range *= 16384 - p0; + } else { + bit = 0; + self.range *= p0; + } + (bit, self.renormalize()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Spec-inverse arithmetic *encoder*, derived from the + /// §4.5.2.6.2.7.4 decoder listing: the decoder's `value` at + /// step `k` equals the stream prefix (as an integer) minus the + /// accumulated `range · cum_freq` subtractions shifted by the + /// renormalization schedule, so the codeword is + /// `Σ sub_k · 2^(A_L − A_k)` with `A_k` the bits consumed when + /// symbol `k` decodes (final `value` chosen 0). Test-only: it + /// exists to prove the decoder self-consistent on every model. + struct Encoder { + range: u64, + est: u32, + /// (subtrahend, alignment in bits when it applies). + subs: Vec<(u64, u64)>, + /// Bits consumed so far (A_k); starts at the 30-bit init. + align: u64, + } + + impl Encoder { + fn new() -> Self { + Encoder { + range: 1, + est: 30, + subs: Vec::new(), + align: 0, + } + } + + fn renorm(&mut self) { + let mut est = 0u32; + while est < HALF.len() as u32 && self.range < HALF[est as usize] { + est += 1; + } + self.est = est; + } + + fn push(&mut self, sub: u64, width: u64) { + self.range <<= self.est; + self.align += u64::from(self.est); + self.range >>= 14; + if sub > 0 { + self.subs.push((sub, self.align)); + } + self.range *= width; + self.renorm(); + } + + fn encode_symbol(&mut self, cum_freq: &[u16], sym: usize) { + let sub_base = u64::from(cum_freq[sym]); + let width = if sym > 0 { + u64::from(cum_freq[sym - 1]) - sub_base + } else { + 16384 - sub_base + }; + let rs_now = (self.range << self.est) >> 14; + self.push(rs_now * sub_base, width); + } + + fn encode_bit(&mut self, p0: u16, bit: u8) { + let p0 = u64::from(p0); + let rs_now = (self.range << self.est) >> 14; + if bit == 1 { + self.push(rs_now * p0, 16384 - p0); + } else { + self.push(0, p0); + } + } + + /// Assemble the codeword bytes (MSB-first bit order): the + /// integer `Σ sub_k · 2^(total_bits − A_k)` emitted as + /// `total_bits` bits (the final `value` is chosen 0, which + /// is always inside the final range). + fn finish(self) -> Vec { + let total_bits = self.align as usize; + // One accumulator slot per stream bit, MSB-first; + // sub_k's bit j lands at index `A_k - 1 - j`. + let mut acc = vec![0u32; total_bits]; + for (sub, align) in &self.subs { + let mut v = *sub; + let mut j = 0usize; + while v > 0 { + acc[*align as usize - 1 - j] += (v & 1) as u32; + v >>= 1; + j += 1; + } + } + // Carry-propagate from the LSB end. + let mut carry = 0u32; + for slot in acc.iter_mut().rev() { + let s = *slot + carry; + *slot = s & 1; + carry = s >> 1; + } + assert_eq!(carry, 0, "test encoder codeword overflow"); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + for (i, &b) in acc.iter().enumerate() { + if b != 0 { + out[i / 8] |= 1 << (7 - (i % 8)); + } + } + out + } + } + + fn roundtrip_symbols(model: &[u16], syms: &[usize]) { + let mut enc = Encoder::new(); + for &s in syms { + enc.encode_symbol(model, s); + } + let bytes = enc.finish(); + let mut rd = SegmentReader::new(&bytes, 0, (bytes.len() as u64) * 8); + let mut dec = ArithDecoder::new(); + for (i, &s) in syms.iter().enumerate() { + let (got, _est) = dec.decode_symbol(&mut rd, model); + assert_eq!(got, s, "symbol {i}"); + } + } + + fn roundtrip_bits(p0s: &[u16], bits: &[u8]) { + assert_eq!(p0s.len(), bits.len()); + let mut enc = Encoder::new(); + for (&p, &b) in p0s.iter().zip(bits) { + enc.encode_bit(p, b); + } + let bytes = enc.finish(); + let mut rd = SegmentReader::new(&bytes, 0, (bytes.len() as u64) * 8); + let mut dec = ArithDecoder::new(); + for (i, (&p, &b)) in p0s.iter().zip(bits).enumerate() { + let (got, _est) = dec.decode_bit(&mut rd, p); + assert_eq!(got, b, "bit {i}"); + } + } + + #[test] + fn symbol_roundtrip_over_every_model() { + use crate::bsac_tables::*; + let mut models: Vec<&[u16]> = vec![ + &MS_USED_MODEL, + &STEREO_INFO_MODEL, + &NOISE_FLAG_MODEL, + &NOISE_MODE_MODEL, + &CBAND_SI_MODEL_CBAND0, + ]; + models.extend(CBAND_SI_MODELS.iter().copied()); + models.extend(SCF_MODELS.iter().flatten().copied()); + let mut seed = 0xC0FFEEu32; + for model in models { + let mut syms = Vec::new(); + for _ in 0..40 { + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + syms.push((seed >> 11) as usize % model.len()); + } + roundtrip_symbols(model, &syms); + } + } + + #[test] + fn bit_roundtrip_over_spectral_probabilities() { + use crate::bsac_tables::spectral_p0; + let mut seed = 0xBEEFu32; + let mut p0s = Vec::new(); + let mut bits = Vec::new(); + for cband_si in [1u8, 4, 7, 9, 12, 15, 22] { + let plane = crate::bsac_tables::CBAND_SI_MSB_PLANE[cband_si as usize]; + for snf in 1..=plane { + for hbv in [0u32, 1, 3, 16] { + let rel = plane - snf; + if hbv != 0 && (rel == 0 || (rel < 31 && hbv >= (1 << rel))) { + continue; + } + for pos in [0usize, 7, 33, 64] { + let pos = if rel == 0 { pos.min(14) } else { pos }; + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + p0s.push(spectral_p0(cband_si, snf, hbv, pos)); + bits.push(((seed >> 13) & 1) as u8); + } + } + } + } + roundtrip_bits(&p0s, &bits); + } + + #[test] + fn mixed_symbol_and_bit_roundtrip() { + use crate::bsac_tables::{MS_USED_MODEL, SCF_MODELS, SIGN_P0}; + let scf = SCF_MODELS[3].unwrap(); + let mut enc = Encoder::new(); + enc.encode_symbol(scf, 5); + enc.encode_bit(SIGN_P0, 1); + enc.encode_symbol(&MS_USED_MODEL, 1); + enc.encode_bit(0x3f00, 0); + enc.encode_bit(0x0100, 1); + enc.encode_symbol(scf, 15); + let bytes = enc.finish(); + let mut rd = SegmentReader::new(&bytes, 0, (bytes.len() as u64) * 8); + let mut dec = ArithDecoder::new(); + assert_eq!(dec.decode_symbol(&mut rd, scf).0, 5); + assert_eq!(dec.decode_bit(&mut rd, SIGN_P0).0, 1); + assert_eq!(dec.decode_symbol(&mut rd, &MS_USED_MODEL).0, 1); + assert_eq!(dec.decode_bit(&mut rd, 0x3f00).0, 0); + assert_eq!(dec.decode_bit(&mut rd, 0x0100).0, 1); + assert_eq!(dec.decode_symbol(&mut rd, scf).0, 15); + } + + #[test] + fn zero_stuffing_supplies_zero_bits() { + let mut rd = SegmentReader::new(&[0xff], 0, 8); + assert_eq!(rd.read_bits(8), 0xff); + assert_eq!(rd.read_bits(8), 0); + assert_eq!(rd.overrun(), 8); + } +} diff --git a/crates/vendor/oxideav-aac/src/bsac_decode.rs b/crates/vendor/oxideav-aac/src/bsac_decode.rs new file mode 100644 index 00000000..9ab13d16 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/bsac_decode.rs @@ -0,0 +1,861 @@ +//! ER BSAC (AOT 22) decoder — ISO/IEC 14496-3:2009 §4.4.2.6 / +//! §4.5.2.6 / §4.6.4. +//! +//! Decodes a `bsac_raw_data_block()` end to end: the raw-bit +//! headers (Tables 4.34–4.36), the §4.5.2.6.2.5 layer roster, the +//! arithmetic-coded side information (`cband_si`, scalefactors, +//! stereo / PNS decisions) and the bit-sliced spectral data +//! (Tables 4.37–4.43 driving [`crate::bsac_arith`] over the +//! [`crate::bsac_tables`] models), then reconstructs PCM through +//! the standard AAC back end — §4.6.2 inverse quantization, the +//! §4.6.8.1 M/S and §4.6.8.2 intensity tools, §4.6.9 TNS and the +//! §4.6.11 filterbank — exactly as §4.6.4.1 prescribes ("the BSAC +//! noiseless coding module is an alternative to the AAC coding +//! module, with all other modules of the AAC-based coder remaining +//! unchanged"). +//! +//! Not yet covered (surfaced as [`Error::BsacUnsupportedTool`]): +//! long-term prediction (`ltp_data_present == 1`), the +//! `zero_code`-prefixed extended part (BSAC channel extension / +//! SBR / MPEG-Surround payloads), and perceptual noise +//! substitution (`pns_data_present == 1`) pending an external +//! vector to pin its arithmetic-PCM offset conventions. + +use crate::bsac_arith::{ArithDecoder, SegmentReader}; +use crate::bsac_layer::{BsacGeometry, LayerInfo, BSAC_FRAME_LEN}; +use crate::bsac_tables::{ + clamp_p0, context_position, spectral_p0, CBAND_SI_MODELS, CBAND_SI_MODEL_CBAND0, + CBAND_SI_MSB_PLANE, CBAND_SI_TYPES, MS_USED_MODEL, SCF_MODELS, SIGN_P0, STEREO_INFO_MODEL, +}; +use crate::dequant::{inverse_quantize, scale_factor_gain}; +use crate::filterbank::Filterbank; +use crate::ics_info::{IcsInfo, WindowSequence, WindowShape}; +use crate::ms_stereo::{apply_ms_stereo, ChannelPairSpectra, MsMaskPresent}; +use crate::pcm::channel_to_s16; +use crate::swb_offset::FrameFamily; +use crate::tns_data::TnsData; +use crate::tns_frame::tns_decode_frame_ics; +use crate::{Error, Result}; + +use oxideav_core::bits::BitReader; + +/// Parsed `bsac_header()` — Table 4.35. +#[derive(Debug, Clone)] +pub struct BsacHeader { + /// `frame_length` (11 bits) — whole frame length in bytes. + pub frame_length: usize, + /// `header_length` (4 bits) — header length escape field + /// (§4.5.2.6.2.2.3: values 1..=14 mean `(header_length + 7)` + /// bytes; 0 / 15 defer to the decoded header length). + pub header_length: u8, + /// `sba_mode` (1 bit) — segmented binary arithmetic coding. + pub sba_mode: bool, + /// `top_layer` (6 bits). + pub top_layer: usize, + /// `base_snf_thr` (2 bits). + pub base_snf_thr: u8, + /// `max_scalefactor[ch]` (8 bits each). + pub max_scalefactor: Vec, + /// `base_band` (5 bits). + pub base_band: usize, + /// `cband_si_type[ch]` (5 bits each). + pub cband_si_type: Vec, + /// `base_scf_model[ch]` (3 bits each). + pub base_scf_model: Vec, + /// `enh_scf_model[ch]` (3 bits each). + pub enh_scf_model: Vec, + /// `max_sfb_si_len[ch]` (4 bits each, raw — the +5 offset is + /// applied in the layer geometry). + pub max_sfb_si_len: Vec, +} + +/// Parsed `general_header()` — Table 4.36. +#[derive(Debug, Clone)] +pub struct GeneralHeader { + /// `window_sequence` (2 bits). + pub window_sequence: WindowSequence, + /// `window_shape` (1 bit). + pub window_shape: WindowShape, + /// `max_sfb` (4 bits short / 6 bits long). + pub max_sfb: usize, + /// `scale_factor_grouping` (7 bits, `EIGHT_SHORT` only). + pub scale_factor_grouping: u8, + /// `pns_data_present` (1 bit). + pub pns_data_present: bool, + /// `pns_start_sfb` (6 bits, when PNS is present). + pub pns_start_sfb: usize, + /// `ms_mask_present` (2 bits, `nch == 2` only): 0 independent, + /// 1 `ms_used` mask, 2 all ones, 3 `stereo_info` mask. + pub ms_mask_present: u8, + /// Per-channel §4.6.9 TNS record. + pub tns: Vec>, +} + +/// One decoded `bsac_raw_data_block()`: quantized spectra plus the +/// side information the AAC back end consumes. +#[derive(Debug, Clone)] +pub struct DecodedBlock { + /// The `bsac_header()`. + pub header: BsacHeader, + /// The `general_header()`. + pub general: GeneralHeader, + /// Signed quantized spectra, `[ch][g][group line]` in the + /// §4.5.2.6.2.6 (possibly interleaved) group order. + pub sample: Vec>>, + /// Absolute scalefactors, `[ch][g][sfb]` (`None` where no band + /// side info was decoded). + pub scf: Vec>>>, + /// `ms_used[g][sfb]` (derived: `stereo_info == 1` counts). + pub ms_used: Vec>, + /// `stereo_info[g][sfb]` (0 independent / 1 M/S / 2 IS in + /// phase / 3 IS out of phase). + pub stereo_info: Vec>, + /// Intensity position per `[g][sfb]` (`stereo_info >= 2`). + pub is_position: Vec>, + /// The layer geometry the block decoded under. + pub geometry: BsacGeometry, +} + +/// Per-(channel, group) bit-slice state. +#[derive(Debug, Clone, Default)] +struct LineState { + /// Decoded bit-plane mask: bit `p-1` set = the plane-`p` sliced + /// bit decoded 1. The magnitude equals the mask value. + mask: Vec, + /// Sign decoded (1 = negative). + sign_neg: Vec, + /// `sign_is_coded[]`. + sign_coded: Vec, + /// First-pass significance (`cur_snf`). + cur_snf: Vec, + /// Secondary-pass significance (`unc_snf`). + unc_snf: Vec, +} + +/// Which significance array a `bsac_spectral_data()` pass drives. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SnfKind { + /// The first coding pass (`bsac_layer_spectra`). + Cur, + /// The secondary passes (`bsac_lower_spectra` / + /// `bsac_higher_spectra`). + Unc, +} + +/// The whole-block arithmetic decode driver. +struct BlockCtx<'a> { + nch: usize, + header: BsacHeader, + general: GeneralHeader, + geo: BsacGeometry, + arith: ArithDecoder, + reader: SegmentReader<'a>, + /// Remaining budget of the current layer (bits). + avail: i64, + /// `cband_si[ch][g][cband]`. + cband_si: Vec>>, + /// Per-(ch, g) line state. + lines: Vec>, + scf: Vec>>>, + stereo_side_info_coded: Vec>, + ms_used: Vec>, + stereo_info: Vec>, + is_position: Vec>, +} + +impl<'a> BlockCtx<'a> { + fn layer_data_available(&self) -> bool { + self.avail > 0 + } + + fn decode_symbol(&mut self, model: &[u16]) -> usize { + let (sym, est) = self.arith.decode_symbol(&mut self.reader, model); + self.avail -= i64::from(est); + sym + } + + fn decode_bit(&mut self, p0: u16) -> u8 { + let (bit, est) = self.arith.decode_bit(&mut self.reader, p0); + self.avail -= i64::from(est); + bit + } + + /// Table 4.38 `layer_cband_si()`. + fn layer_cband_si(&mut self, layer: &LayerInfo) -> Result<()> { + let g = layer.group; + for ch in 0..self.nch { + let params = &CBAND_SI_TYPES[self.header.cband_si_type[ch] as usize]; + for cband in layer.start_cband..layer.end_cband { + let (model, largest): (&[u16], u8) = if cband == 0 { + (&CBAND_SI_MODEL_CBAND0, params.largest_cband0) + } else { + ( + CBAND_SI_MODELS[params.other_model as usize], + params.largest_other, + ) + }; + let si = self.decode_symbol(model); + if si > usize::from(largest) { + return Err(Error::BsacBitError); + } + self.cband_si[ch][g][cband] = si as u8; + // §4.5.2.6.2.5: cur_snf of the layer's new lines + // initializes to the coding band's MSB plane. + let plane = i32::from(CBAND_SI_MSB_PLANE[si]); + let start = cband * 32; + let end = (cband * 32 + 32).min(self.geo.group_len[g]); + for i in start..end { + self.lines[ch][g].cur_snf[i] = plane; + } + } + } + Ok(()) + } + + /// The scalefactor-model symbol for the current layer. + fn scf_symbol(&mut self, ch: usize, layer_idx: usize) -> Result { + let model_idx = if layer_idx < self.geo.slayer_size { + self.header.base_scf_model[ch] + } else { + self.header.enh_scf_model[ch] + } as usize; + match SCF_MODELS[model_idx] { + Some(model) => Ok(self.decode_symbol(model)), + // Model 0 is "not used" (Table 4.A.32): no symbol is + // coded; the differential is zero. + None => Ok(0), + } + } + + /// Table 4.39 `layer_sfb_si()`. + fn layer_sfb_si(&mut self, layer_idx: usize, layer: &LayerInfo) -> Result<()> { + let g = layer.group; + let pns = self.general.pns_data_present; + let msp = self.general.ms_mask_present; + for ch in 0..self.nch { + for sfb in layer.start_sfb..layer.end_sfb { + if self.nch == 1 { + if pns && sfb >= self.general.pns_start_sfb { + // PNS decode needs the noise-energy PCM + // conventions pinned by an external vector. + return Err(Error::BsacUnsupportedTool); + } + } else if !self.stereo_side_info_coded[g][sfb] { + if msp != 2 { + if msp == 1 { + let ms = self.decode_symbol(&MS_USED_MODEL); + self.ms_used[g][sfb] = ms == 1; + } else if msp == 3 { + let si = self.decode_symbol(&STEREO_INFO_MODEL) as u8; + self.stereo_info[g][sfb] = si; + self.ms_used[g][sfb] = si == 1; + } + if pns && sfb >= self.general.pns_start_sfb { + return Err(Error::BsacUnsupportedTool); + } + } + self.stereo_side_info_coded[g][sfb] = true; + } + // Per-channel scalefactor / intensity position. + if self.stereo_info[g][sfb] >= 2 && ch == 1 { + let idx = self.scf_symbol(ch, layer_idx)? as i32; + // §4.6.4.4.3 zig-zag: odd → −(idx+1)/2, even → + // idx/2. + self.is_position[g][sfb] = if idx % 2 == 1 { + -(idx + 1) / 2 + } else { + idx / 2 + }; + } else { + let diff = self.scf_symbol(ch, layer_idx)? as i32; + let scf = i32::from(self.header.max_scalefactor[ch]) - diff; + if !(0..=255).contains(&scf) { + return Err(Error::BsacBitError); + } + self.scf[ch][g][sfb] = Some(scf as u8); + } + } + } + Ok(()) + } + + /// Table 4.43 `bsac_spectral_data()` over `regions` + /// (`(group, start_index, end_index)`), down to (exclusive) + /// `thr_snf`, driving the selected significance array. + fn spectral_data(&mut self, regions: &[(usize, usize, usize)], thr_snf: i32, kind: SnfKind) { + if !self.layer_data_available() { + return; + } + // maxsnf over the region. + let mut maxsnf = 0i32; + for &(g, s, e) in regions { + for ch in 0..self.nch { + let st = &self.lines[ch][g]; + let arr = match kind { + SnfKind::Cur => &st.cur_snf, + SnfKind::Unc => &st.unc_snf, + }; + for &v in arr[s..e.min(arr.len())].iter() { + maxsnf = maxsnf.max(v); + } + } + } + let mut snf = maxsnf; + while snf > thr_snf { + for &(g, s, e) in regions { + let e = e.min(self.geo.group_len[g]); + for i in s..e { + for ch in 0..self.nch { + { + let st = &self.lines[ch][g]; + let v = match kind { + SnfKind::Cur => st.cur_snf[i], + SnfKind::Unc => st.unc_snf[i], + }; + if v < snf { + continue; + } + } + let cband_si = self.cband_si[ch][g][i / 32]; + let mask_i = self.lines[ch][g].mask[i]; + let sign_coded = self.lines[ch][g].sign_coded[i]; + if mask_i == 0 || sign_coded { + // Decode one sliced bit. + let hbv = mask_i >> snf; + let p0 = if hbv != 0 { + spectral_p0(cband_si, snf as u8, hbv, 0) + } else { + let a = i % 4; + let bit_at = |j: isize| -> u8 { + if j < 0 { + 0 + } else { + ((self.lines[ch][g].mask[j as usize] >> (snf - 1)) & 1) + as u8 + } + }; + let hb = |j: usize| -> u8 { + if j >= self.geo.group_len[g] { + 0 + } else { + u8::from(self.lines[ch][g].mask[j] >> snf != 0) + } + }; + let prev = [ + bit_at(i as isize - 3), + bit_at(i as isize - 2), + bit_at(i as isize - 1), + ]; + let base = i - a; + let flags = [hb(base), hb(base + 1), hb(base + 2), hb(base + 3)]; + spectral_p0( + cband_si, + snf as u8, + 0, + context_position(a, prev, flags), + ) + }; + let p0 = clamp_p0(p0, self.avail); + let bit = self.decode_bit(p0); + if bit != 0 { + self.lines[ch][g].mask[i] |= 1 << (snf - 1); + } + } + if self.lines[ch][g].mask[i] != 0 && !self.lines[ch][g].sign_coded[i] { + if !self.layer_data_available() { + return; + } + let sign = self.decode_bit(SIGN_P0); + self.lines[ch][g].sign_neg[i] = sign == 1; + self.lines[ch][g].sign_coded[i] = true; + } + { + let st = &mut self.lines[ch][g]; + match kind { + SnfKind::Cur => st.cur_snf[i] -= 1, + SnfKind::Unc => st.unc_snf[i] -= 1, + } + } + if !self.layer_data_available() { + return; + } + } + } + } + snf -= 1; + } + } +} + +/// Decode one `bsac_raw_data_block()` into quantized spectra + side +/// info. +/// +/// `fs` / `fs_index` — the sampling rate from the ASC; `nch` — the +/// channel count (1 or 2). +pub fn decode_bsac_raw_data_block( + frame: &[u8], + fs: u32, + fs_index: u8, + nch: usize, +) -> Result { + if !(1..=2).contains(&nch) || frame.is_empty() { + return Err(Error::BsacInvalidHeader); + } + let mut br = BitReader::new(frame); + fn rd(br: &mut BitReader<'_>, n: u32) -> Result { + br.read_u32(n).map_err(|_| Error::UnexpectedEnd) + } + + // Table 4.34 / 4.35: frame_length + bsac_header(). + let frame_length = rd(&mut br, 11)? as usize; + if frame_length > frame.len() || frame_length == 0 { + return Err(Error::BsacInvalidHeader); + } + let header_length = rd(&mut br, 4)? as u8; + let sba_mode = rd(&mut br, 1)? != 0; + let top_layer = rd(&mut br, 6)? as usize; + let base_snf_thr = rd(&mut br, 2)? as u8; + let mut max_scalefactor = Vec::with_capacity(nch); + for _ in 0..nch { + max_scalefactor.push(rd(&mut br, 8)? as u8); + } + let base_band = rd(&mut br, 5)? as usize; + let (mut cband_si_type, mut base_scf_model, mut enh_scf_model, mut max_sfb_si_len) = + (Vec::new(), Vec::new(), Vec::new(), Vec::new()); + for _ in 0..nch { + let t = rd(&mut br, 5)? as u8; + if usize::from(t) >= CBAND_SI_TYPES.len() { + return Err(Error::BsacInvalidHeader); + } + cband_si_type.push(t); + base_scf_model.push(rd(&mut br, 3)? as u8); + enh_scf_model.push(rd(&mut br, 3)? as u8); + max_sfb_si_len.push(rd(&mut br, 4)? as u8); + } + + // Table 4.36: general_header(). + let _reserved = rd(&mut br, 1)?; + let window_sequence = match rd(&mut br, 2)? { + 0 => WindowSequence::OnlyLong, + 1 => WindowSequence::LongStart, + 2 => WindowSequence::EightShort, + _ => WindowSequence::LongStop, + }; + let window_shape = if rd(&mut br, 1)? != 0 { + WindowShape::Kbd + } else { + WindowShape::Sine + }; + let short = window_sequence == WindowSequence::EightShort; + let (max_sfb, scale_factor_grouping) = if short { + let m = rd(&mut br, 4)? as usize; + let g = rd(&mut br, 7)? as u8; + (m, g) + } else { + (rd(&mut br, 6)? as usize, 0) + }; + let pns_data_present = rd(&mut br, 1)? != 0; + let pns_start_sfb = if pns_data_present { + rd(&mut br, 6)? as usize + } else { + 0 + }; + let ms_mask_present = if nch == 2 { rd(&mut br, 2)? as u8 } else { 0 }; + let mut tns = Vec::with_capacity(nch); + for _ in 0..nch { + if rd(&mut br, 1)? != 0 { + tns.push(Some( + TnsData::parse(&mut br, window_sequence).map_err(|_| Error::BsacInvalidHeader)?, + )); + } else { + tns.push(None); + } + // ltp_data_present. + if rd(&mut br, 1)? != 0 { + return Err(Error::BsacUnsupportedTool); + } + } + let consumed = br.bit_position() as i64; + // header_length escapes (§4.5.2.6.2.2.3): 1..=14 → (hl+7) + // bytes; 0 / 15 → the byte-aligned actual length. + let header_bits: i64 = if (1..=14).contains(&header_length) { + (i64::from(header_length) + 7) * 8 + } else { + (consumed + 7) / 8 * 8 + }; + if header_bits < (consumed + 7) / 8 * 8 || header_bits > (frame_length as i64) * 8 { + return Err(Error::BsacInvalidHeader); + } + + let geo = BsacGeometry::derive( + fs, + fs_index, + window_sequence, + scale_factor_grouping, + max_sfb, + nch, + top_layer, + base_band, + header_bits, + frame_length, + &cband_si_type, + &max_sfb_si_len, + )?; + + let header = BsacHeader { + frame_length, + header_length, + sba_mode, + top_layer, + base_snf_thr, + max_scalefactor, + base_band, + cband_si_type, + base_scf_model, + enh_scf_model, + max_sfb_si_len, + }; + let general = GeneralHeader { + window_sequence, + window_shape, + max_sfb, + scale_factor_grouping, + pns_data_present, + pns_start_sfb, + ms_mask_present, + tns, + }; + + let ngroups = geo.num_window_groups; + let mut ctx = BlockCtx { + nch, + geo, + arith: ArithDecoder::new(), + reader: SegmentReader::new(frame, 0, 0), + avail: 0, + cband_si: vec![Vec::new(); nch], + lines: vec![Vec::new(); nch], + scf: vec![vec![vec![None; max_sfb]; ngroups]; nch], + stereo_side_info_coded: vec![vec![false; max_sfb]; ngroups], + ms_used: vec![vec![false; max_sfb]; ngroups], + stereo_info: vec![vec![0u8; max_sfb]; ngroups], + is_position: vec![vec![0i32; max_sfb]; ngroups], + header, + general, + }; + for ch in 0..nch { + for g in 0..ngroups { + let len = ctx.geo.group_len[g]; + ctx.cband_si[ch].push(vec![0u8; len.div_ceil(32)]); + ctx.lines[ch].push(LineState { + mask: vec![0; len], + sign_neg: vec![false; len], + sign_coded: vec![false; len], + cur_snf: vec![0; len], + unc_snf: vec![0; len], + }); + } + } + + // §4.6.4.3.3: ms_mask_present == 2 sets every ms_used without + // decoding. + if nch == 2 && ctx.general.ms_mask_present == 2 { + for row in ctx.ms_used.iter_mut() { + row.fill(true); + } + } + + if ctx.header.sba_mode { + // SBA re-initializes the arithmetic code per segment; the + // segment split + higher-spectra scheduling lands with an + // SBA-bearing conformance vector. + return Err(Error::BsacUnsupportedTool); + } + // Non-SBA: one arithmetic segment from the header end to the + // frame end. + ctx.reader = SegmentReader::new(frame, header_bits as u64, (frame_length as u64) * 8); + ctx.arith = ArithDecoder::new(); + + let total_layers = ctx.geo.layers.len(); + // Suffix sums of the static layer budgets: the Table 4.33 + // `data_available()` gate — an enhancement layer decodes only + // while frame bits remain. + let mut suffix_avail = vec![0i64; total_layers + 1]; + for k in (0..total_layers).rev() { + suffix_avail[k] = suffix_avail[k + 1] + ctx.geo.layers[k].available_len; + } + // `prev_end[g]`: the highest end_index of any processed layer, + // per group — the §4.5.2.6.2.2 lower-spectra region. + let mut prev_end = vec![0usize; ngroups]; + let mut carry: i64 = -1; // segment start: 1 termination bit. + #[allow(clippy::needless_range_loop)] // ctx.geo.layers cannot be + // iterated while ctx is mutably borrowed inside the body. + for layer_idx in 0..total_layers { + let layer = ctx.geo.layers[layer_idx].clone(); + // Table 4.33: base sub-layers ride inside + // bsac_base_element() unconditionally; enhancement layers + // are gated on data_available(). + if layer_idx >= ctx.geo.slayer_size && carry + suffix_avail[layer_idx] <= 0 { + break; + } + ctx.avail = carry + layer.available_len; + // Side info. + ctx.layer_cband_si(&layer)?; + ctx.layer_sfb_si(layer_idx, &layer)?; + // First pass: the layer's new spectra. + let thr = if layer_idx < ctx.geo.slayer_size { + i32::from(ctx.header.base_snf_thr) + } else { + 0 + }; + let regions = [(layer.group, layer.start_index, layer.end_index)]; + ctx.spectral_data(®ions, thr, SnfKind::Cur); + // Store cur_snf → unc_snf for the layer's range. + for ch in 0..nch { + let st = &mut ctx.lines[ch][layer.group]; + let e = layer.end_index.min(st.cur_snf.len()); + for i in layer.start_index..e { + st.unc_snf[i] = st.cur_snf[i]; + } + } + // Secondary pass: refine every earlier line. + let lower: Vec<(usize, usize, usize)> = (0..ngroups) + .filter(|&g| prev_end[g] > 0) + .map(|g| (g, 0, prev_end[g])) + .collect(); + ctx.spectral_data(&lower, 0, SnfKind::Unc); + prev_end[layer.group] = prev_end[layer.group].max(layer.end_index); + carry = ctx.avail; + } + + // Assemble the signed samples. + let mut sample = vec![Vec::with_capacity(ngroups); nch]; + for (ch, sample_ch) in sample.iter_mut().enumerate().take(nch) { + for g in 0..ngroups { + let st = &ctx.lines[ch][g]; + let buf: Vec = st + .mask + .iter() + .zip(st.sign_neg.iter()) + .map(|(&m, &neg)| { + let v = m as i32; + if neg { + -v + } else { + v + } + }) + .collect(); + sample_ch.push(buf); + } + } + Ok(DecodedBlock { + header: ctx.header, + general: ctx.general, + sample, + scf: ctx.scf, + ms_used: ctx.ms_used, + stereo_info: ctx.stereo_info, + is_position: ctx.is_position, + geometry: ctx.geo, + }) +} + +/// Persistent ER BSAC stream decoder: one AU (`bsac_raw_data_block`) +/// in, one PCM frame out, carrying the §4.6.11 overlap-add state +/// across frames. +#[derive(Debug)] +pub struct BsacDecoder { + fs: u32, + fs_index: u8, + nch: usize, + filterbanks: Vec, +} + +impl BsacDecoder { + /// A decoder for `nch` channels at `fs` Hz (Table 1.18 index + /// `fs_index`). + pub fn new(fs: u32, fs_index: u8, nch: usize) -> Result { + if !(1..=2).contains(&nch) { + return Err(Error::BsacInvalidHeader); + } + Ok(BsacDecoder { + fs, + fs_index, + nch, + filterbanks: (0..nch).map(|_| Filterbank::new()).collect(), + }) + } + + /// Decode one access unit to interleaved 16-bit PCM + /// (1024 samples per channel). + pub fn decode_frame(&mut self, au: &[u8]) -> Result> { + let block = decode_bsac_raw_data_block(au, self.fs, self.fs_index, self.nch)?; + let spectra = reconstruct_spectra(&block, self.fs_index, self.nch)?; + let info = block_ics_info(&block, self.fs_index)?; + let mut channels = Vec::with_capacity(self.nch); + for (ch, mut spec) in spectra.into_iter().enumerate() { + if let Some(tns) = &block.general.tns[ch] { + tns_decode_frame_ics(&mut spec, tns, &info, 22, self.fs_index)?; + } + let time = self.filterbanks[ch].synthesize(&spec, &info)?; + channels.push(channel_to_s16(&time)); + } + let mut out = Vec::with_capacity(BSAC_FRAME_LEN * self.nch); + for i in 0..BSAC_FRAME_LEN { + for chan in &channels { + out.push(chan[i]); + } + } + Ok(out) + } + + /// Drop all cross-frame state (post-seek restart). + pub fn reset(&mut self) { + for fb in &mut self.filterbanks { + *fb = Filterbank::new(); + } + } +} + +/// The `IcsInfo` equivalent of a decoded block (drives the shared +/// TNS / filterbank / stereo primitives). +fn block_ics_info(block: &DecodedBlock, fs_index: u8) -> Result { + let short = block.general.window_sequence == WindowSequence::EightShort; + let num_swb = if short { + crate::ics_info::NUM_SWB_SHORT_WINDOW[fs_index as usize] + } else { + crate::ics_info::NUM_SWB_LONG_WINDOW[fs_index as usize] + }; + Ok(IcsInfo { + family: FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: block.general.window_sequence, + window_shape: block.general.window_shape, + max_sfb: block.general.max_sfb as u8, + scale_factor_grouping: if short { + Some(block.general.scale_factor_grouping) + } else { + None + }, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: if short { 8 } else { 1 }, + num_window_groups: block.geometry.num_window_groups as u8, + window_group_length: block.geometry.window_group_length.clone(), + num_swb, + }) +} + +/// Inverse-quantize + de-interleave one block into per-channel +/// window-major spectra, then run the §4.6.8.1 / §4.6.8.2 stereo +/// tools. +fn reconstruct_spectra(block: &DecodedBlock, fs_index: u8, nch: usize) -> Result>> { + let geo = &block.geometry; + let short = block.general.window_sequence == WindowSequence::EightShort; + let max_sfb = block.general.max_sfb; + let mut spectra = Vec::with_capacity(nch); + for ch in 0..nch { + let mut spec = vec![0.0f64; BSAC_FRAME_LEN]; + let mut window_base = 0usize; // first window of the group + for g in 0..geo.num_window_groups { + let wgl = geo.window_group_length[g] as usize; + let buf = &block.sample[ch][g]; + for sfb in 0..max_sfb { + let (s, e) = (geo.swb_offset[g][sfb], geo.swb_offset[g][sfb + 1]); + let Some(scf) = block.scf[ch][g][sfb] else { + continue; + }; + let gain = scale_factor_gain(scf); + for (gi, &q) in buf.iter().enumerate().take(e.min(buf.len())).skip(s) { + if q == 0 { + continue; + } + let x = inverse_quantize(q) * gain; + let out_idx = if short { + // §4.5.2.6.2.6: within a group, 4-line + // chunks interleave across the group's + // windows: group index + // `4·(chunk·wgl + w) + j` carries window + // `w`'s line `4·chunk + j`. + let chunk = gi / (4 * wgl); + let rem = gi % (4 * wgl); + let w = rem / 4; + let j = rem % 4; + (window_base + w) * 128 + chunk * 4 + j + } else { + gi + }; + spec[out_idx] = x; + } + } + window_base += wgl; + } + spectra.push(spec); + } + + if nch == 2 { + let info = block_ics_info(block, fs_index)?; + // Intensity stereo (stereo_info 2 / 3) reconstructs the + // right channel from the left before the M/S de-matrix + // (which skips intensity bands). + let ms_present = match block.general.ms_mask_present { + 0 => MsMaskPresent::AllZeros, + 2 => MsMaskPresent::AllOnes, + _ => MsMaskPresent::Mask, + }; + // Per-band codebook shadows for the shared primitives: + // intensity bands flag 15 (in phase) / 14 (out of phase) on + // the right channel. + let mut right_cb = vec![vec![1u8; max_sfb]; geo.num_window_groups]; + let mut is_pos = vec![vec![0i32; max_sfb]; geo.num_window_groups]; + let mut any_is = false; + for g in 0..geo.num_window_groups { + for sfb in 0..max_sfb { + match block.stereo_info[g][sfb] { + 2 => { + right_cb[g][sfb] = crate::section_data::INTENSITY_HCB; + is_pos[g][sfb] = block.is_position[g][sfb]; + any_is = true; + } + 3 => { + right_cb[g][sfb] = crate::section_data::INTENSITY_HCB2; + is_pos[g][sfb] = block.is_position[g][sfb]; + any_is = true; + } + _ => {} + } + } + } + if any_is { + let (left, right) = spectra.split_at_mut(1); + let mut pair = crate::intensity_stereo::IntensityPairSpectra { + left: &left[0], + right: &mut right[0], + right_sfb_cb: &right_cb, + is_pos: &is_pos, + }; + crate::intensity_stereo::apply_intensity_stereo( + &mut pair, + block.general.ms_mask_present != 0, + &block.ms_used, + &info, + fs_index, + )?; + } + let left_cb = vec![vec![1u8; max_sfb]; geo.num_window_groups]; + let (left, right) = spectra.split_at_mut(1); + let mut pair = ChannelPairSpectra { + left: &mut left[0], + right: &mut right[0], + left_sfb_cb: &left_cb, + right_sfb_cb: &right_cb, + }; + apply_ms_stereo(&mut pair, ms_present, &block.ms_used, &info, fs_index)?; + } + Ok(spectra) +} diff --git a/crates/vendor/oxideav-aac/src/bsac_layer.rs b/crates/vendor/oxideav-aac/src/bsac_layer.rs new file mode 100644 index 00000000..fd91b055 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/bsac_layer.rs @@ -0,0 +1,499 @@ +//! BSAC fine-grain scalability layer geometry — ISO/IEC +//! 14496-3:2009 §4.5.2.6.2.4 / §4.5.2.6.2.5. +//! +//! A `bsac_raw_data_block()` is a stack of scalability layers: the +//! base layer (split into `slayer_size` sub-layers, one per base +//! coding band) followed by `top_layer` enhancement layers of +//! ~1 kbit/s/ch each. Every layer covers a slice of the spectrum +//! (`layer_start_index .. layer_end_index` in its window group), a +//! run of 32-line coding bands, a run of scalefactor bands whose +//! side info it carries, and a bit budget (`available_len`) cut out +//! of the frame by the §4.5.2.6.2.5 `layer_bit_offset` derivation. +//! [`BsacGeometry::derive`] computes the whole roster from the +//! header fields, transcribing the spec pseudo-code (including its +//! evident loop-variable typos, noted inline). + +use crate::ics_info::WindowSequence; +use crate::swb_offset::{long_window_offsets, short_window_offsets}; +use crate::{Error, Result}; + +/// Frame length of the 1024-line family this decoder covers. +pub const BSAC_FRAME_LEN: usize = 1024; + +/// Short-window length. +const SHORT_LEN: usize = 128; + +/// §4.5.2.6.2.5: `max_cband0_si_len` — the fixed maximum length of +/// the 0th coding band's side information. +const MAX_CBAND0_SI_LEN: u32 = 11; + +/// One scalability layer's coverage and budget. +#[derive(Debug, Clone, Default)] +pub struct LayerInfo { + /// `layer_group[layer]` — the window group whose spectrum the + /// layer extends. + pub group: usize, + /// `layer_start_cband[layer]` .. `layer_end_cband[layer]`. + pub start_cband: usize, + /// Exclusive end coding band. + pub end_cband: usize, + /// `layer_start_index[layer]` .. `layer_end_index[layer]` + /// (group-local spectral lines). + pub start_index: usize, + /// Exclusive end line. + pub end_index: usize, + /// `layer_start_sfb[layer]` .. `layer_end_sfb[layer]`. + pub start_sfb: usize, + /// Exclusive end scalefactor band. + pub end_sfb: usize, + /// `layer_si_maxlen[layer]` in bits. + pub si_maxlen: u32, + /// `layer_bit_offset[layer]` — the layer's first bit within the + /// frame. + pub bit_offset: i64, + /// `available_len[layer]` in bits (before the segment-start + /// `-1` termination adjustment, which the decode driver + /// applies). + pub available_len: i64, + /// §4.6.4.6.3 `terminal_layer[layer]` — the layer ends an SBA + /// segment (always true for the last layer). + pub terminal: bool, +} + +/// The §4.5.2.6.2.4 / §4.5.2.6.2.5 derived geometry for one +/// `bsac_raw_data_block()`. +#[derive(Debug, Clone)] +pub struct BsacGeometry { + /// Number of window groups (1 for long sequences). + pub num_window_groups: usize, + /// Windows per group (sums to 8 for `EIGHT_SHORT`). + pub window_group_length: Vec, + /// Per-group scaled band offsets: `swb_offset[g][sfb] = + /// swb_offset_window[sfb] · window_group_length[g]`, length + /// `max_sfb + 1`. + pub swb_offset: Vec>, + /// Per-group group-buffer length (`1024` long, `wgl · 128` + /// short). + pub group_len: Vec, + /// `last_index[g]` — the spectral cap from `max_sfb`. + pub last_index: Vec, + /// Number of base sub-layers. + pub slayer_size: usize, + /// The header's `top_layer`. + pub top_layer: usize, + /// Per-layer coverage/budget, `slayer_size + top_layer` + /// entries. + pub layers: Vec, +} + +impl BsacGeometry { + /// Derive the whole layer roster. + /// + /// * `fs` / `fs_index` — sampling frequency (Hz / Table 1.18 + /// index). + /// * `window_sequence` + `scale_factor_grouping` + `max_sfb` — + /// from `general_header()`. + /// * `nch` — channels in the block (1 or 2). + /// * `top_layer` / `base_band` — from `bsac_header()`. + /// * `header_bits` — `layer_bit_offset[0]`, the total header + /// length in bits (byte-aligned). + /// * `frame_length` — the frame length in bytes. + /// * `cband_si_type` / `max_sfb_si_len` — per channel, from + /// `bsac_header()` (`max_sfb_si_len` raw, offset +5 applied + /// here). + #[allow(clippy::too_many_arguments)] + pub fn derive( + fs: u32, + fs_index: u8, + window_sequence: WindowSequence, + scale_factor_grouping: u8, + max_sfb: usize, + nch: usize, + top_layer: usize, + base_band: usize, + header_bits: i64, + frame_length: usize, + cband_si_type: &[u8], + max_sfb_si_len: &[u8], + ) -> Result { + // §4.5.2.6.2.4 grouping (identical to the AAC derivation). + let short = window_sequence == WindowSequence::EightShort; + let (num_window_groups, window_group_length) = if short { + let mut wgl: Vec = vec![1]; + for i in 0..7 { + if (scale_factor_grouping >> (6 - i)) & 1 == 0 { + wgl.push(1); + } else { + *wgl.last_mut().unwrap() += 1; + } + } + (wgl.len(), wgl) + } else { + (1, vec![1u8]) + }; + let window_offsets: &[u16] = if short { + short_window_offsets(fs_index)? + } else { + long_window_offsets(fs_index)? + }; + if max_sfb + 1 > window_offsets.len() { + return Err(Error::BsacInvalidHeader); + } + let mut swb_offset = Vec::with_capacity(num_window_groups); + let mut group_len = Vec::with_capacity(num_window_groups); + let mut last_index = Vec::with_capacity(num_window_groups); + for &wgl_u8 in window_group_length.iter().take(num_window_groups) { + let wgl = wgl_u8 as usize; + let offsets: Vec = (0..=max_sfb) + .map(|sfb| window_offsets[sfb] as usize * if short { wgl } else { 1 }) + .collect(); + last_index.push(offsets[max_sfb]); + swb_offset.push(offsets); + group_len.push(if short { + wgl * SHORT_LEN + } else { + BSAC_FRAME_LEN + }); + } + + // §4.5.2.6.2.5: slayer_size + per-group base band limit. + let mut end_index = vec![0usize; num_window_groups]; + let mut end_cband = vec![0usize; num_window_groups]; + let mut slayer_size = 0usize; + for g in 0..num_window_groups { + if short { + let wgl = window_group_length[g] as usize; + let mut ei = base_band * 4 * wgl; + if fs == 44_100 || fs == 48_000 { + if ei % 32 >= 16 { + ei = ei / 32 * 32 + 20; + } else if ei % 32 >= 4 { + ei = ei / 32 * 32 + 8; + } + } else if fs == 22_050 || fs == 24_000 || fs == 32_000 { + ei = ei / 16 * 16; + } else if fs == 11_025 || fs == 12_000 || fs == 16_000 { + ei = ei / 32 * 32; + } else { + ei = ei / 64 * 64; + } + end_index[g] = ei; + end_cband[g] = ei.div_ceil(32); + } else { + end_cband[g] = base_band; + } + slayer_size += end_cband[g]; + } + if slayer_size == 0 { + return Err(Error::BsacInvalidHeader); + } + + let total_layers = slayer_size + top_layer; + let mut layers = vec![LayerInfo::default(); total_layers]; + + // layer_group[]: base sub-layers walk the groups' cbands in + // order; enhancement layers cycle through the groups + // window-by-window (period `num_windows` — 8 for short, 1 + // for long; the spec writes the period-8 copy explicitly). + { + let mut layer = 0usize; + for (g, &nc) in end_cband.iter().enumerate().take(num_window_groups) { + for _ in 1..=nc { + layers[layer].group = g; + layer += 1; + } + } + let mut seq = Vec::new(); + for (g, &wgl) in window_group_length + .iter() + .enumerate() + .take(num_window_groups) + { + for _ in 0..wgl { + seq.push(g); + } + } + for (k, layer) in layers.iter_mut().enumerate().skip(slayer_size) { + layer.group = seq[(k - slayer_size) % seq.len()]; + } + } + + // Base sub-layers: one coding band each. + { + let mut layer = 0usize; + let mut end_index_run = vec![0usize; num_window_groups]; + for (g, &nc) in end_cband.iter().enumerate().take(num_window_groups) { + for cband in 0..nc { + layers[layer].start_cband = cband; + layers[layer].end_cband = cband + 1; + layers[layer].start_index = cband * 32; + layers[layer].end_index = (cband + 1) * 32; + end_index_run[g] = (cband + 1) * 32; + layer += 1; + } + } + // Enhancement layers extend the band limit at the + // rate-dependent §4.5.2.6.2.5 step. + let mut end_cband_run = end_cband.clone(); + let mut end_index_g = end_index_run; + for layer_info in layers.iter_mut().skip(slayer_size) { + let g = layer_info.group; + layer_info.start_index = end_index_g[g]; + let mut ei = end_index_g[g]; + if fs == 44_100 || fs == 48_000 { + if ei % 32 == 0 { + ei += 8; + } else { + ei += 12; + } + } else if fs == 22_050 || fs == 24_000 || fs == 32_000 { + ei += 16; + } else if fs == 11_025 || fs == 12_000 || fs == 16_000 { + ei += 32; + } else { + ei += 64; + } + if ei > last_index[g] { + ei = last_index[g]; + } + end_index_g[g] = ei; + layer_info.end_index = ei; + layer_info.start_cband = end_cband_run[g]; + end_cband_run[g] = ei.div_ceil(32); + layer_info.end_cband = end_cband_run[g]; + } + } + + // layer_start_sfb / layer_end_sfb (transcribed literally, + // `layer_end_sfb = sfb + 1` at the first band whose start + // offset reaches the layer's end index). + { + let mut end_sfb = vec![0usize; num_window_groups]; + for layer_info in layers.iter_mut() { + let g = layer_info.group; + layer_info.start_sfb = end_sfb[g]; + layer_info.end_sfb = max_sfb; + for (sfb, &off) in swb_offset[g].iter().enumerate().take(max_sfb) { + if layer_info.end_index <= off { + // Transcribed literally (`= sfb + 1`); the + // one-band lookahead is corpus-confirmed — + // the `= sfb` reading desyncs the arithmetic + // stream on frames that the `+ 1` reading + // decodes exactly. + layer_info.end_sfb = sfb + 1; + break; + } + } + end_sfb[g] = layer_info.end_sfb; + } + } + + // layer_si_maxlen. + for layer_info in layers.iter_mut() { + let mut si = 0u32; + for cband in layer_info.start_cband..layer_info.end_cband { + for &cst in cband_si_type.iter().take(nch) { + if cband == 0 { + si += MAX_CBAND0_SI_LEN; + } else { + si += u32::from( + crate::bsac_tables::CBAND_SI_TYPES + .get(cst as usize) + .ok_or(Error::BsacInvalidHeader)? + .max_len, + ); + } + } + } + for _sfb in layer_info.start_sfb..layer_info.end_sfb { + for &msl in max_sfb_si_len.iter().take(nch) { + si += u32::from(msl) + 5; + } + } + layer_info.si_maxlen = si; + } + + // layer_bit_offset: rate anchors for the enhancement + // layers, then top-down si-budget adjustments, the base + // sub-layer split, and the header overflow/underflow + // redistribution — §4.5.2.6.2.5, transcribed with the + // evident typos fixed (`slayer--` for `layer--`, `layer <=` + // for `m <=`). + let frame_bits = (frame_length as i64) * 8; + let mut bit_offset = vec![0i64; total_layers + 1]; + for (k, off) in bit_offset + .iter_mut() + .enumerate() + .take(total_layers + 1) + .skip(slayer_size) + { + let layer_bitrate = (nch as i64) * (((k - slayer_size) as i64) * 1000 + 16_000); + let mut v = layer_bitrate * (BSAC_FRAME_LEN as i64); + v = v / (fs as i64) / 8 * 8; + *off = v.min(frame_bits); + } + // The frame may carry more bytes than the top layer's + // nominal rate anchor (the encoder's bit reservoir); the + // stream end is the frame end, so the last boundary extends + // to it — the slack feeds the top layer's secondary + // (refinement) pass. + bit_offset[total_layers] = frame_bits; + for k in (slayer_size..total_layers).rev() { + let candidate = bit_offset[k + 1] - i64::from(layers[k].si_maxlen); + if candidate < bit_offset[k] { + bit_offset[k] = candidate; + } + } + for k in (0..slayer_size).rev() { + bit_offset[k] = bit_offset[k + 1] - i64::from(layers[k].si_maxlen); + } + let overflow = header_bits - bit_offset[0]; + bit_offset[0] = header_bits; + if overflow > 0 { + let mut overflow = overflow; + for k in (slayer_size..total_layers).rev() { + let mut layer_bit_size = bit_offset[k + 1] - bit_offset[k]; + layer_bit_size -= i64::from(layers[k].si_maxlen); + if layer_bit_size >= overflow { + layer_bit_size = overflow; + overflow = 0; + } else { + overflow -= layer_bit_size; + } + for off in bit_offset.iter_mut().take(k + 1).skip(1) { + *off += layer_bit_size; + } + if overflow <= 0 { + break; + } + } + } else { + let underflow = -overflow; + let share = underflow / (slayer_size as i64); + let extra = underflow % (slayer_size as i64); + for m in 1..slayer_size { + bit_offset[m] = bit_offset[m - 1] + i64::from(layers[m - 1].si_maxlen) + share; + if (m as i64) <= extra { + bit_offset[m] += 1; + } + } + } + for (k, layer_info) in layers.iter_mut().enumerate() { + layer_info.bit_offset = bit_offset[k]; + layer_info.available_len = bit_offset[k + 1] - bit_offset[k]; + } + + // §4.6.4.6.3 terminal_layer[]: a layer ends its segment when + // the next layer starts a different coding band run; the + // last layer always terminates. + for k in 0..total_layers { + layers[k].terminal = if k + 1 < total_layers { + layers[k].start_cband != layers[k + 1].start_cband + } else { + true + }; + } + + Ok(BsacGeometry { + num_window_groups, + window_group_length, + swb_offset, + group_len, + last_index, + slayer_size, + top_layer, + layers, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A 48 kHz mono long-window geometry with the header values of + /// a real conformance frame (`top_layer = 48`, `base_band = + /// 10`): the base splits into 10 sub-layers of one coding band, + /// enhancement layers extend by the 8/12-line 48 kHz step, and + /// the budgets partition the frame exactly. + #[test] + fn long_mono_layer_roster() { + let geo = BsacGeometry::derive( + 48_000, + 3, + WindowSequence::OnlyLong, + 0, + 40, + 1, + 48, + 10, + 72, + 171, + &[27], + &[0], + ) + .unwrap(); + assert_eq!(geo.slayer_size, 10); + assert_eq!(geo.layers.len(), 58); + // Base sub-layers: one 32-line cband each. + for (k, l) in geo.layers.iter().take(10).enumerate() { + assert_eq!(l.group, 0); + assert_eq!((l.start_cband, l.end_cband), (k, k + 1)); + assert_eq!((l.start_index, l.end_index), (32 * k, 32 * k + 32)); + } + // First enhancement layer starts at the base band limit. + assert_eq!(geo.layers[10].start_index, 320); + assert_eq!(geo.layers[10].end_index, 328); + assert_eq!(geo.layers[11].start_index, 328); + assert_eq!(geo.layers[11].end_index, 340); + // Budgets tile the frame: offsets ascend and the last layer + // ends at or before the frame end. + for w in geo.layers.windows(2) { + assert_eq!(w[0].bit_offset + w[0].available_len, w[1].bit_offset); + } + let last = geo.layers.last().unwrap(); + assert!(last.bit_offset + last.available_len <= 171 * 8); + assert_eq!(geo.layers[0].bit_offset, 72); + // sfb coverage is monotone and capped. + for l in &geo.layers { + assert!(l.start_sfb <= l.end_sfb && l.end_sfb <= 40); + } + // Non-SBA streams still mark segment boundaries; the last + // layer always terminates. + assert!(geo.layers.last().unwrap().terminal); + } + + /// The short-window 48 kHz band-limit rounding of + /// §4.5.2.6.2.5 (`% 32 >= 16 → +20`, `% 32 >= 4 → +8`). + #[test] + fn short_window_base_band_rounding() { + let geo = BsacGeometry::derive( + 48_000, + 3, + WindowSequence::EightShort, + 0, // 8 groups of 1 window + 14, + 1, + 8, + 10, + 72, + 400, + &[5], + &[2], + ) + .unwrap(); + assert_eq!(geo.num_window_groups, 8); + // base_band·4·1 = 40 → 40 % 32 = 8 (>= 4) → 32 + 8 = 40. + assert_eq!(geo.layers[0].end_index, 32); + // Each group contributes ceil(40/32) = 2 sub-layers. + assert_eq!(geo.slayer_size, 16); + for (k, l) in geo.layers.iter().take(16).enumerate() { + assert_eq!(l.group, k / 2); + assert_eq!(l.start_cband, k % 2); + } + // Enhancement layers cycle the 8 groups round-robin. + for k in 0..8 { + assert_eq!(geo.layers[16 + k].group, k); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/bsac_tables.rs b/crates/vendor/oxideav-aac/src/bsac_tables.rs new file mode 100644 index 00000000..11effaeb --- /dev/null +++ b/crates/vendor/oxideav-aac/src/bsac_tables.rs @@ -0,0 +1,1020 @@ +//! Numeric tables for the ER BSAC noiseless coder — ISO/IEC +//! 14496-3:2009 §4.A.5 (Tables 4.A.31–4.A.77), transcribed from the +//! staged specification PDF. +//! +//! Three table families live here: +//! +//! * **General arithmetic models** — 14-bit cumulative-frequency +//! arrays consumed by the §4.5.2.6.2.7.4 `decode_symbol()` +//! procedure: the scalefactor models (Tables 4.A.37–4.A.43, +//! selected via Table 4.A.32), the `cband_si` models (Tables +//! 4.A.44–4.A.50 for coding bands past the 0th, Table 4.A.51 for +//! the 0th, selected via Table 4.A.31), and the stereo / PNS +//! side-info models (Tables 4.A.52–4.A.55). Every array is +//! strictly decreasing and ends in 0 (the `cum_freq[sym] > cum` +//! symbol search walks it in order). +//! * **Binary probability tables** — the 22 spectral bit-slice +//! tables (Tables 4.A.56–4.A.77), each a set of `p0` rows (14-bit +//! probability of the "0" symbol) indexed by the significance +//! distance from the coding band's MSB plane, the neighbouring +//! lines' context (Table 4.A.34 position), and the line's own +//! decoded higher bits. Tables 11–22 are normative aliases of +//! tables 9 / 10 at higher MSB planes; tables 9 / 10 alias their +//! zero-context sub-MSB rows onto tables 7 / 8 (the spec states +//! the aliases verbatim). [`spectral_p0`] resolves the whole +//! scheme. +//! * **Context / clamp tables** — the Table 4.A.34 position map +//! ([`context_position`]), and the Table 4.A.35 / 4.A.36 +//! `min_p0` / `max_p0` clamps applied when a layer's remaining +//! budget drops under 14 bits ([`clamp_p0`]). + +/// Table 4.A.31 row: parameters of one `cband_si_type`. +#[derive(Debug, Clone, Copy)] +pub struct CbandSiTypeParams { + /// `max_cband_si_len` — the side-info bit allowance used by the + /// §4.5.2.6.2.5 `layer_si_maxlen` accumulation. + pub max_len: u8, + /// Largest decodable `cband_si` for the 0th coding band. + pub largest_cband0: u8, + /// Largest decodable `cband_si` for every other coding band. + pub largest_other: u8, + /// Index into [`CBAND_SI_MODELS`] for the non-0th coding bands + /// (the 0th band always uses [`CBAND_SI_MODEL_CBAND0`]). + pub other_model: u8, +} + +/// Table 4.A.31 — `cband_si_type` parameters (32 rows). +pub const CBAND_SI_TYPES: [CbandSiTypeParams; 32] = [ + CbandSiTypeParams { + max_len: 6, + largest_cband0: 6, + largest_other: 4, + other_model: 0, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 6, + largest_other: 6, + other_model: 1, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 8, + largest_other: 4, + other_model: 0, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 8, + largest_other: 6, + other_model: 1, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 8, + largest_other: 8, + other_model: 2, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 10, + largest_other: 4, + other_model: 0, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 10, + largest_other: 6, + other_model: 1, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 10, + largest_other: 8, + other_model: 2, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 10, + largest_other: 10, + other_model: 3, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 12, + largest_other: 4, + other_model: 0, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 12, + largest_other: 6, + other_model: 1, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 12, + largest_other: 8, + other_model: 2, + }, + CbandSiTypeParams { + max_len: 8, + largest_cband0: 12, + largest_other: 12, + other_model: 4, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 14, + largest_other: 4, + other_model: 0, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 14, + largest_other: 6, + other_model: 1, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 14, + largest_other: 8, + other_model: 2, + }, + CbandSiTypeParams { + max_len: 8, + largest_cband0: 14, + largest_other: 12, + other_model: 4, + }, + CbandSiTypeParams { + max_len: 9, + largest_cband0: 14, + largest_other: 14, + other_model: 5, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 15, + largest_other: 4, + other_model: 0, + }, + CbandSiTypeParams { + max_len: 5, + largest_cband0: 15, + largest_other: 6, + other_model: 1, + }, + CbandSiTypeParams { + max_len: 6, + largest_cband0: 15, + largest_other: 8, + other_model: 2, + }, + CbandSiTypeParams { + max_len: 8, + largest_cband0: 15, + largest_other: 12, + other_model: 4, + }, + CbandSiTypeParams { + max_len: 10, + largest_cband0: 15, + largest_other: 15, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 8, + largest_cband0: 16, + largest_other: 12, + other_model: 4, + }, + CbandSiTypeParams { + max_len: 10, + largest_cband0: 16, + largest_other: 16, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 9, + largest_cband0: 17, + largest_other: 14, + other_model: 5, + }, + CbandSiTypeParams { + max_len: 10, + largest_cband0: 17, + largest_other: 17, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 10, + largest_cband0: 18, + largest_other: 18, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 12, + largest_cband0: 19, + largest_other: 19, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 12, + largest_cband0: 20, + largest_other: 20, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 12, + largest_cband0: 21, + largest_other: 21, + other_model: 6, + }, + CbandSiTypeParams { + max_len: 12, + largest_cband0: 22, + largest_other: 22, + other_model: 6, + }, +]; + +/// Table 4.A.32 — largest differential value decodable under each +/// `scf_model` (model 0 is "not used": no scalefactor decoding). +pub const SCF_MODEL_LARGEST: [u8; 8] = [0, 3, 7, 15, 15, 31, 31, 63]; + +/// Table 4.A.33 — MSB plane per `cband_si` (0..=22). The MSB plane +/// is the highest bit-slice a coefficient in the coding band +/// carries; `cband_si == 0` means the band decodes to all zeros. +pub const CBAND_SI_MSB_PLANE: [u8; 23] = [ + 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, 15, +]; + +/// Table 4.A.35 — minimum `p0` when the layer's available length is +/// `1..=13` bits (index 0 unused). +pub const MIN_P0: [u16; 14] = [ + 0, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, + 0x0004, 0x0002, +]; + +/// Table 4.A.36 — maximum `p0` when the layer's available length is +/// `1..=13` bits (index 0 unused). +pub const MAX_P0: [u16; 14] = [ + 0, 0x2000, 0x3000, 0x3800, 0x3c00, 0x3e00, 0x3f00, 0x3f80, 0x3fc0, 0x3fe0, 0x3ff0, 0x3ff8, + 0x3ffc, 0x3ffe, +]; + +/// §4.6.4.2.3: clamp a spectral-bit `p0` onto the Table 4.A.35 / +/// 4.A.36 band when fewer than 14 bits remain in the layer. +pub fn clamp_p0(p0: u16, available_len: i64) -> u16 { + if (1..14).contains(&available_len) { + let i = available_len as usize; + p0.clamp(MIN_P0[i], MAX_P0[i]) + } else { + p0 + } +} + +/// The §4.5.2.6.2.2.13 sign-bit probability: `p0 = 0.5` as a 14-bit +/// fixed-point number. +pub const SIGN_P0: u16 = 0x2000; + +/// Scalefactor arithmetic model 1 (Table 4.A.37). +pub const SCF_MODEL_1: [u16; 4] = [0x0752, 0x03cd, 0x014d, 0x0000]; + +/// Scalefactor arithmetic model 2 (Table 4.A.38). +pub const SCF_MODEL_2: [u16; 8] = [ + 0x112f, 0x0de7, 0x0a8b, 0x07c1, 0x047a, 0x023a, 0x00d4, 0x0000, +]; + +/// Scalefactor arithmetic model 3 (Table 4.A.39). +pub const SCF_MODEL_3: [u16; 16] = [ + 0x1f67, 0x1c5f, 0x18d8, 0x1555, 0x1215, 0x0eb4, 0x0adc, 0x0742, 0x0408, 0x01e6, 0x00df, 0x0052, + 0x0032, 0x0023, 0x000c, 0x0000, +]; + +/// Scalefactor arithmetic model 4 (Table 4.A.40). +pub const SCF_MODEL_4: [u16; 16] = [ + 0x250f, 0x22b8, 0x2053, 0x1deb, 0x1b05, 0x186d, 0x15df, 0x12d9, 0x0f77, 0x0c01, 0x0833, 0x050d, + 0x0245, 0x008c, 0x0033, 0x0000, +]; + +/// Scalefactor arithmetic model 5 (Table 4.A.41). +pub const SCF_MODEL_5: [u16; 32] = [ + 0x08a8, 0x074e, 0x0639, 0x0588, 0x048c, 0x03cf, 0x032e, 0x0272, 0x01bc, 0x013e, 0x00e4, 0x0097, + 0x0069, 0x0043, 0x002f, 0x0029, 0x0020, 0x001b, 0x0018, 0x0015, 0x0012, 0x000f, 0x000d, 0x000c, + 0x000a, 0x0009, 0x0007, 0x0006, 0x0004, 0x0003, 0x0001, 0x0000, +]; + +/// Scalefactor arithmetic model 6 (Table 4.A.42). +pub const SCF_MODEL_6: [u16; 32] = [ + 0x0c2a, 0x099f, 0x0809, 0x06ec, 0x0603, 0x053d, 0x0491, 0x040e, 0x0394, 0x030a, 0x02a5, 0x0259, + 0x0202, 0x01bc, 0x0170, 0x0133, 0x0102, 0x00c9, 0x0097, 0x0073, 0x004f, 0x0037, 0x0022, 0x0016, + 0x000f, 0x000b, 0x0009, 0x0007, 0x0005, 0x0003, 0x0001, 0x0000, +]; + +/// Scalefactor arithmetic model 7 (Table 4.A.43). +pub const SCF_MODEL_7: [u16; 64] = [ + 0x3b5e, 0x3a90, 0x39d3, 0x387c, 0x3702, 0x3566, 0x33a7, 0x321c, 0x2f90, 0x2cf2, 0x29fe, 0x26fa, + 0x23e4, 0x20df, 0x1e0d, 0x1ac4, 0x1804, 0x159a, 0x131e, 0x10e7, 0x0e5b, 0x0c9c, 0x0b78, 0x0a21, + 0x08fd, 0x07b7, 0x06b5, 0x062c, 0x055d, 0x04f6, 0x04d4, 0x044b, 0x038e, 0x02e2, 0x029d, 0x0236, + 0x0225, 0x01f2, 0x01cf, 0x01ad, 0x019c, 0x0179, 0x0168, 0x0157, 0x0146, 0x0135, 0x0123, 0x0112, + 0x0101, 0x00f0, 0x00df, 0x00ce, 0x00bc, 0x00ab, 0x009a, 0x0089, 0x0078, 0x0067, 0x0055, 0x0044, + 0x0033, 0x0022, 0x0011, 0x0000, +]; + +/// cband_si arithmetic model 0 (Table 4.A.44). +pub const CBAND_SI_MODEL_0: [u16; 5] = [0x3ef6, 0x3b59, 0x1b12, 0x12a3, 0x0000]; + +/// cband_si arithmetic model 1 (Table 4.A.45). +pub const CBAND_SI_MODEL_1: [u16; 7] = [0x3d51, 0x33ae, 0x1cff, 0x0fb7, 0x07e4, 0x022b, 0x0000]; + +/// cband_si arithmetic model 2 (Table 4.A.46). +pub const CBAND_SI_MODEL_2: [u16; 9] = [ + 0x3a47, 0x2aec, 0x1e05, 0x1336, 0x0e7d, 0x0860, 0x05e0, 0x044a, 0x0000, +]; + +/// cband_si arithmetic model 3 (Table 4.A.47). +pub const CBAND_SI_MODEL_3: [u16; 11] = [ + 0x36be, 0x27ae, 0x20f4, 0x1749, 0x14d5, 0x0d46, 0x0ad3, 0x0888, 0x0519, 0x020b, 0x0000, +]; + +/// cband_si arithmetic model 4 (Table 4.A.48). +pub const CBAND_SI_MODEL_4: [u16; 13] = [ + 0x3983, 0x2e77, 0x2b03, 0x1ee8, 0x1df9, 0x1307, 0x11e4, 0x0b4d, 0x094c, 0x0497, 0x0445, 0x0040, + 0x0000, +]; + +/// cband_si arithmetic model 5 (Table 4.A.49). +pub const CBAND_SI_MODEL_5: [u16; 15] = [ + 0x306f, 0x249e, 0x1f56, 0x1843, 0x161a, 0x102d, 0x0f6c, 0x0c81, 0x0af2, 0x07a8, 0x071a, 0x0454, + 0x0413, 0x0016, 0x0000, +]; + +/// cband_si arithmetic model 6 (Table 4.A.50). +pub const CBAND_SI_MODEL_6: [u16; 23] = [ + 0x31af, 0x2001, 0x162d, 0x127e, 0x0f05, 0x0c34, 0x0b8f, 0x0a61, 0x0955, 0x0825, 0x07dd, 0x06a9, + 0x0688, 0x055b, 0x054b, 0x02f7, 0x0198, 0x0077, 0x0010, 0x000c, 0x0008, 0x0004, 0x0000, +]; + +/// cband_si arithmetic model for the 0th coding band (Table 4.A.51). +pub const CBAND_SI_MODEL_CBAND0: [u16; 23] = [ + 0x3ff8, 0x3ff0, 0x3fe8, 0x3fe0, 0x3fd7, 0x3f31, 0x3cd7, 0x3bc9, 0x3074, 0x2bcf, 0x231b, 0x13db, + 0x0d51, 0x0603, 0x044c, 0x0080, 0x0030, 0x0028, 0x0020, 0x0018, 0x0010, 0x0008, 0x0000, +]; + +/// MS_used model (Table 4.A.52). +pub const MS_USED_MODEL: [u16; 2] = [0x2ccd, 0x0000]; + +/// stereo_info model (Table 4.A.53). +pub const STEREO_INFO_MODEL: [u16; 4] = [0x3666, 0x1000, 0x0666, 0x0000]; + +/// noise_flag arithmetic model (Table 4.A.54). +pub const NOISE_FLAG_MODEL: [u16; 2] = [0x2000, 0x0000]; + +/// noise_mode arithmetic model (Table 4.A.55). +pub const NOISE_MODE_MODEL: [u16; 4] = [0x3000, 0x2000, 0x1000, 0x0000]; + +/// BSAC probability table 1 (MSB plane 1), MSB row (Table 4.A.56). +pub const PROB_T1_MSB: [u16; 15] = [ + 0x3900, 0x3a00, 0x2f00, 0x3b00, 0x2f00, 0x3700, 0x2c00, 0x3b00, 0x3000, 0x3600, 0x2d00, 0x3900, + 0x2f00, 0x3700, 0x2c00, +]; + +/// BSAC probability table 2 (MSB plane 1), MSB row (Table 4.A.57). +pub const PROB_T2_MSB: [u16; 15] = [ + 0x2800, 0x2800, 0x2500, 0x2900, 0x2600, 0x2700, 0x2300, 0x2a00, 0x2700, 0x2800, 0x2400, 0x2800, + 0x2500, 0x2600, 0x2200, +]; + +/// BSAC probability table 3 (MSB plane 2), MSB row (Table 4.A.58). +pub const PROB_T3_MSB: [u16; 15] = [ + 0x3d00, 0x3d00, 0x3300, 0x3d00, 0x3300, 0x3b00, 0x3300, 0x3d00, 0x3200, 0x3b00, 0x3100, 0x3e00, + 0x3700, 0x3c00, 0x3300, +]; + +/// BSAC probability table 3, MSB-1, zero higher bits (Table 4.A.58). +pub const PROB_T3_ZERO_1: [u16; 65] = [ + 0x3700, 0x3a00, 0x2800, 0x3b00, 0x2600, 0x2c00, 0x2400, 0x3a00, 0x2500, 0x2b00, 0x2400, 0x3100, + 0x2300, 0x2900, 0x2300, 0x3000, 0x2c00, 0x1d00, 0x2200, 0x1a00, 0x1c00, 0x1600, 0x2700, 0x2200, + 0x1a00, 0x1d00, 0x1900, 0x1c00, 0x1e00, 0x2c00, 0x2400, 0x1900, 0x1e00, 0x1f00, 0x1c00, 0x2b00, + 0x2400, 0x2900, 0x2700, 0x2400, 0x1300, 0x1a00, 0x2000, 0x1800, 0x2300, 0x2500, 0x1f00, 0x2c00, + 0x2300, 0x3600, 0x2800, 0x3100, 0x2500, 0x1400, 0x1200, 0x1800, 0x1400, 0x2100, 0x2200, 0x1000, + 0x1e00, 0x3000, 0x2600, 0x1200, 0x2200, +]; + +/// BSAC probability table 3, MSB-1, non-zero higher bits (Table 4.A.58). +pub const PROB_T3_NZ_1: [u16; 1] = [0x3100]; + +/// BSAC probability table 4 (MSB plane 2), MSB row (Table 4.A.59). +pub const PROB_T4_MSB: [u16; 15] = [ + 0x3900, 0x3a00, 0x2e00, 0x3a00, 0x2f00, 0x3400, 0x2a00, 0x3a00, 0x3000, 0x3500, 0x2c00, 0x3600, + 0x2b00, 0x3100, 0x2500, +]; + +/// BSAC probability table 4, MSB-1, zero higher bits (Table 4.A.59). +pub const PROB_T4_ZERO_1: [u16; 65] = [ + 0x1e00, 0x1d00, 0x1c00, 0x1d00, 0x1c00, 0x1d00, 0x1b00, 0x1d00, 0x1e00, 0x1e00, 0x1a00, 0x1e00, + 0x1c00, 0x1d00, 0x1b00, 0x1a00, 0x1a00, 0x1800, 0x1800, 0x1800, 0x1700, 0x1700, 0x1800, 0x1a00, + 0x1700, 0x1700, 0x1900, 0x1800, 0x1600, 0x1700, 0x1600, 0x1500, 0x1700, 0x1800, 0x1600, 0x1c00, + 0x1700, 0x1900, 0x1700, 0x1500, 0x1c00, 0x1500, 0x1600, 0x0f00, 0x1800, 0x1400, 0x1700, 0x1a00, + 0x1a00, 0x1e00, 0x1800, 0x1c00, 0x1b00, 0x1500, 0x1300, 0x1500, 0x1400, 0x1600, 0x1500, 0x1700, + 0x1600, 0x1b00, 0x1800, 0x1400, 0x1400, +]; + +/// BSAC probability table 4, MSB-1, non-zero higher bits (Table 4.A.59). +pub const PROB_T4_NZ_1: [u16; 1] = [0x3600]; + +/// BSAC probability table 5 (MSB plane 3), MSB row (Table 4.A.60). +pub const PROB_T5_MSB: [u16; 15] = [ + 0x3d00, 0x3d00, 0x3200, 0x3d00, 0x3300, 0x3d00, 0x3600, 0x3d00, 0x3500, 0x3c00, 0x3500, 0x3f00, + 0x3b00, 0x3f00, 0x3d00, +]; + +/// BSAC probability table 5, MSB-1, zero higher bits (Table 4.A.60). +pub const PROB_T5_ZERO_1: [u16; 65] = [ + 0x3c00, 0x3d00, 0x2b00, 0x3d00, 0x2900, 0x3500, 0x2c00, 0x3d00, 0x2b00, 0x3400, 0x2b00, 0x3800, + 0x2b00, 0x3700, 0x2a00, 0x3900, 0x3400, 0x2400, 0x2a00, 0x1c00, 0x1f00, 0x1600, 0x3500, 0x2500, + 0x1a00, 0x2a00, 0x2200, 0x2b00, 0x2a00, 0x3500, 0x2600, 0x1a00, 0x2600, 0x2500, 0x2700, 0x3500, + 0x2d00, 0x3800, 0x3200, 0x2e00, 0x1800, 0x1600, 0x2900, 0x2500, 0x3100, 0x2c00, 0x2300, 0x3600, + 0x3000, 0x3c00, 0x3300, 0x3b00, 0x3400, 0x1700, 0x1a00, 0x1c00, 0x1900, 0x2900, 0x2a00, 0x2400, + 0x2700, 0x3c00, 0x3600, 0x1d00, 0x3100, +]; + +/// BSAC probability table 5, MSB-1, non-zero higher bits (Table 4.A.60). +pub const PROB_T5_NZ_1: [u16; 1] = [0x3100]; + +/// BSAC probability table 5, MSB-2, zero higher bits (Table 4.A.60). +pub const PROB_T5_ZERO_2: [u16; 65] = [ + 0x3400, 0x3800, 0x2700, 0x3900, 0x2700, 0x2f00, 0x2200, 0x3800, 0x2500, 0x2d00, 0x2000, 0x3300, + 0x2000, 0x2900, 0x1e00, 0x2b00, 0x2300, 0x1a00, 0x1a00, 0x1b00, 0x1800, 0x1700, 0x1e00, 0x1c00, + 0x1b00, 0x1c00, 0x1b00, 0x1a00, 0x1800, 0x1d00, 0x1b00, 0x1800, 0x1900, 0x1b00, 0x1a00, 0x1d00, + 0x1e00, 0x1f00, 0x1b00, 0x1e00, 0x1200, 0x1400, 0x1a00, 0x1300, 0x1c00, 0x1b00, 0x1900, 0x2000, + 0x1e00, 0x3000, 0x2900, 0x2d00, 0x2500, 0x1300, 0x1700, 0x1400, 0x1300, 0x1e00, 0x1f00, 0x1100, + 0x1900, 0x2100, 0x1e00, 0x1500, 0x1a00, +]; + +/// BSAC probability table 5, MSB-2, non-zero higher bits (Table 4.A.60). +pub const PROB_T5_NZ_2: [u16; 3] = [0x2a00, 0x2b00, 0x2800]; + +/// BSAC probability table 6 (MSB plane 3), MSB row (Table 4.A.61). +pub const PROB_T6_MSB: [u16; 15] = [ + 0x3800, 0x3a00, 0x2d00, 0x3a00, 0x2d00, 0x3600, 0x2d00, 0x3a00, 0x2d00, 0x3600, 0x2b00, 0x3a00, + 0x2800, 0x3600, 0x2700, +]; + +/// BSAC probability table 6, MSB-1, zero higher bits (Table 4.A.61). +pub const PROB_T6_ZERO_1: [u16; 65] = [ + 0x2b00, 0x3000, 0x2500, 0x2f00, 0x2600, 0x2d00, 0x2400, 0x3000, 0x2500, 0x2b00, 0x2400, 0x2d00, + 0x2500, 0x2800, 0x2500, 0x2a00, 0x2900, 0x2300, 0x2200, 0x1e00, 0x1b00, 0x1900, 0x2600, 0x2300, + 0x1f00, 0x1d00, 0x2200, 0x1b00, 0x1800, 0x2100, 0x2100, 0x1d00, 0x1d00, 0x1f00, 0x1f00, 0x2900, + 0x2600, 0x2a00, 0x2100, 0x2300, 0x1800, 0x1a00, 0x1d00, 0x2000, 0x1c00, 0x1a00, 0x1e00, 0x2900, + 0x2800, 0x2f00, 0x2300, 0x2f00, 0x2600, 0x1d00, 0x1700, 0x1d00, 0x1c00, 0x1e00, 0x2100, 0x1700, + 0x2200, 0x2300, 0x2300, 0x1400, 0x1a00, +]; + +/// BSAC probability table 6, MSB-1, non-zero higher bits (Table 4.A.61). +pub const PROB_T6_NZ_1: [u16; 1] = [0x3000]; + +/// BSAC probability table 6, MSB-2, zero higher bits (Table 4.A.61). +pub const PROB_T6_ZERO_2: [u16; 65] = [ + 0x1900, 0x1900, 0x1900, 0x1b00, 0x1700, 0x1b00, 0x1a00, 0x1000, 0x1900, 0x1600, 0x1800, 0x1e00, + 0x1900, 0x1a00, 0x1700, 0x1b00, 0x1700, 0x1500, 0x1500, 0x1500, 0x1700, 0x1400, 0x1900, 0x1700, + 0x1600, 0x1600, 0x1200, 0x1300, 0x1200, 0x1600, 0x1500, 0x1500, 0x1300, 0x1600, 0x1600, 0x1c00, + 0x1400, 0x1700, 0x1600, 0x1400, 0x1400, 0x1400, 0x1500, 0x1400, 0x1300, 0x1300, 0x1500, 0x1800, + 0x1600, 0x1f00, 0x1a00, 0x1e00, 0x1800, 0x1700, 0x1600, 0x1600, 0x1300, 0x1400, 0x1300, 0x1100, + 0x1500, 0x1600, 0x1500, 0x1200, 0x1300, +]; + +/// BSAC probability table 6, MSB-2, non-zero higher bits (Table 4.A.61). +pub const PROB_T6_NZ_2: [u16; 3] = [0x2b00, 0x2800, 0x2700]; + +/// BSAC probability table 7 (MSB plane 4), MSB row (Table 4.A.62). +pub const PROB_T7_MSB: [u16; 15] = [ + 0x3d00, 0x3d00, 0x3500, 0x3e00, 0x3500, 0x3f00, 0x3b00, 0x3e00, 0x3200, 0x3f00, 0x3a00, 0x3f00, + 0x3d00, 0x3f00, 0x3b00, +]; + +/// BSAC probability table 7, MSB-1, zero higher bits (Table 4.A.62). +pub const PROB_T7_ZERO_1: [u16; 65] = [ + 0x3f00, 0x3f00, 0x3200, 0x3f00, 0x3500, 0x3e00, 0x3700, 0x3f00, 0x2d00, 0x3c00, 0x3000, 0x3f00, + 0x3700, 0x3e00, 0x3400, 0x3f00, 0x3900, 0x2600, 0x2f00, 0x1e00, 0x2400, 0x1500, 0x3700, 0x3100, + 0x1b00, 0x2600, 0x2300, 0x3a00, 0x3900, 0x3e00, 0x2b00, 0x2200, 0x2800, 0x2f00, 0x2500, 0x3e00, + 0x3700, 0x3e00, 0x3d00, 0x3900, 0x1a00, 0x3300, 0x2500, 0x2800, 0x3c00, 0x3800, 0x2c00, 0x3d00, + 0x3800, 0x3f00, 0x3b00, 0x3f00, 0x3a00, 0x1e00, 0x1b00, 0x1800, 0x1800, 0x3b00, 0x3a00, 0x1200, + 0x2f00, 0x3f00, 0x3b00, 0x1b00, 0x3500, +]; + +/// BSAC probability table 7, MSB-1, non-zero higher bits (Table 4.A.62). +pub const PROB_T7_NZ_1: [u16; 1] = [0x2f00]; + +/// BSAC probability table 7, MSB-2, zero higher bits (Table 4.A.62). +pub const PROB_T7_ZERO_2: [u16; 65] = [ + 0x3c00, 0x3e00, 0x3000, 0x3e00, 0x3100, 0x3a00, 0x3100, 0x3d00, 0x2c00, 0x3900, 0x2e00, 0x3c00, + 0x2d00, 0x3c00, 0x3100, 0x3d00, 0x3100, 0x2100, 0x2c00, 0x2600, 0x2800, 0x1d00, 0x2b00, 0x2800, + 0x2800, 0x2400, 0x2200, 0x2100, 0x2300, 0x2d00, 0x2500, 0x1f00, 0x2100, 0x2b00, 0x2700, 0x3200, + 0x2d00, 0x3400, 0x2a00, 0x3500, 0x1800, 0x1800, 0x1f00, 0x1e00, 0x2e00, 0x2a00, 0x2400, 0x3000, + 0x2b00, 0x3e00, 0x3d00, 0x3d00, 0x3a00, 0x1e00, 0x2b00, 0x2600, 0x1900, 0x3400, 0x3500, 0x1c00, + 0x2600, 0x3300, 0x2a00, 0x1c00, 0x2b00, +]; + +/// BSAC probability table 7, MSB-2, non-zero higher bits (Table 4.A.62). +pub const PROB_T7_NZ_2: [u16; 3] = [0x2800, 0x2900, 0x2400]; + +/// BSAC probability table 7, MSB-3 (others), zero higher bits (Table 4.A.62). +pub const PROB_T7_ZERO_3: [u16; 65] = [ + 0x3500, 0x3b00, 0x2900, 0x3b00, 0x2a00, 0x3100, 0x2700, 0x3b00, 0x2600, 0x2f00, 0x2400, 0x3400, + 0x2300, 0x2d00, 0x2000, 0x3300, 0x2700, 0x1c00, 0x2400, 0x1c00, 0x1c00, 0x1900, 0x2700, 0x2800, + 0x1b00, 0x1d00, 0x2000, 0x1b00, 0x1a00, 0x2300, 0x1d00, 0x1700, 0x1e00, 0x2400, 0x2100, 0x2b00, + 0x2100, 0x2800, 0x2000, 0x2300, 0x1b00, 0x1500, 0x1b00, 0x1400, 0x1a00, 0x1a00, 0x2000, 0x2a00, + 0x2200, 0x3700, 0x2f00, 0x3200, 0x2a00, 0x1700, 0x1700, 0x1600, 0x1900, 0x2500, 0x2300, 0x1500, + 0x1900, 0x2500, 0x2200, 0x1400, 0x1b00, +]; + +/// BSAC probability table 7, MSB-3 (others), non-zero higher bits (Table 4.A.62). +pub const PROB_T7_NZ_3: [u16; 7] = [0x2d00, 0x2500, 0x2300, 0x2500, 0x2500, 0x2600, 0x2400]; + +/// BSAC probability table 8 (MSB plane 4), MSB row (Table 4.A.63). +pub const PROB_T8_MSB: [u16; 15] = [ + 0x3b00, 0x3c00, 0x3400, 0x3c00, 0x3400, 0x3a00, 0x3000, 0x3c00, 0x3200, 0x3a00, 0x3100, 0x3c00, + 0x3000, 0x3900, 0x2f00, +]; + +/// BSAC probability table 8, MSB-1, zero higher bits (Table 4.A.63). +pub const PROB_T8_ZERO_1: [u16; 65] = [ + 0x3500, 0x3800, 0x2c00, 0x3900, 0x2c00, 0x3400, 0x2b00, 0x3800, 0x2e00, 0x3400, 0x2d00, 0x3600, + 0x2a00, 0x3300, 0x2800, 0x3100, 0x3100, 0x2600, 0x2900, 0x2000, 0x2300, 0x1f00, 0x2d00, 0x2600, + 0x2000, 0x2600, 0x2300, 0x2500, 0x2100, 0x2c00, 0x2400, 0x1d00, 0x2500, 0x2400, 0x2400, 0x3000, + 0x2800, 0x3000, 0x2900, 0x2200, 0x1e00, 0x1c00, 0x2500, 0x1d00, 0x2300, 0x2300, 0x2500, 0x3300, + 0x2c00, 0x3700, 0x2b00, 0x3400, 0x2c00, 0x1e00, 0x1c00, 0x2100, 0x1b00, 0x2900, 0x2a00, 0x1d00, + 0x2600, 0x3200, 0x2a00, 0x2000, 0x2400, +]; + +/// BSAC probability table 8, MSB-1, non-zero higher bits (Table 4.A.63). +pub const PROB_T8_NZ_1: [u16; 1] = [0x3200]; + +/// BSAC probability table 8, MSB-2, zero higher bits (Table 4.A.63). +pub const PROB_T8_ZERO_2: [u16; 65] = [ + 0x2900, 0x2e00, 0x2600, 0x2f00, 0x2600, 0x2d00, 0x2600, 0x2e00, 0x2500, 0x2b00, 0x2600, 0x2f00, + 0x2300, 0x2a00, 0x2300, 0x2800, 0x2800, 0x2100, 0x2400, 0x2000, 0x2000, 0x1b00, 0x2400, 0x1f00, + 0x1c00, 0x2100, 0x2200, 0x1d00, 0x1c00, 0x1f00, 0x1c00, 0x1900, 0x1e00, 0x2100, 0x2100, 0x2900, + 0x2200, 0x2300, 0x2100, 0x1c00, 0x1a00, 0x1a00, 0x2100, 0x2100, 0x1c00, 0x1c00, 0x1f00, 0x2700, + 0x2500, 0x2d00, 0x2700, 0x2a00, 0x2300, 0x1c00, 0x1d00, 0x1a00, 0x1a00, 0x1b00, 0x1d00, 0x1800, + 0x2000, 0x2300, 0x1f00, 0x1900, 0x1c00, +]; + +/// BSAC probability table 8, MSB-2, non-zero higher bits (Table 4.A.63). +pub const PROB_T8_NZ_2: [u16; 3] = [0x2b00, 0x2900, 0x2800]; + +/// BSAC probability table 8, MSB-3 (others), zero higher bits (Table 4.A.63). +pub const PROB_T8_ZERO_3: [u16; 65] = [ + 0x1c00, 0x1e00, 0x1b00, 0x1e00, 0x1c00, 0x1e00, 0x1900, 0x1a00, 0x1f00, 0x1f00, 0x1900, 0x2000, + 0x1a00, 0x1f00, 0x1700, 0x1b00, 0x1a00, 0x1900, 0x1800, 0x1900, 0x1800, 0x1600, 0x1900, 0x1a00, + 0x1900, 0x1700, 0x1800, 0x1700, 0x1800, 0x1600, 0x1700, 0x1400, 0x1600, 0x1800, 0x1a00, 0x1c00, + 0x1c00, 0x1c00, 0x1700, 0x1700, 0x1500, 0x1500, 0x1600, 0x1600, 0x1500, 0x1400, 0x1700, 0x1b00, + 0x1a00, 0x2300, 0x1c00, 0x1d00, 0x1a00, 0x1600, 0x1600, 0x1500, 0x1400, 0x1800, 0x1500, 0x1300, + 0x1700, 0x1900, 0x1600, 0x1400, 0x1400, +]; + +/// BSAC probability table 8, MSB-3 (others), non-zero higher bits (Table 4.A.63). +pub const PROB_T8_NZ_3: [u16; 7] = [0x2800, 0x2500, 0x2500, 0x2700, 0x2500, 0x2600, 0x2500]; + +/// BSAC probability table 9 (MSB plane 5), MSB row (Table 4.A.64). +pub const PROB_T9_MSB: [u16; 15] = [ + 0x3d00, 0x3e00, 0x3300, 0x3e00, 0x3500, 0x3e00, 0x3700, 0x3e00, 0x3400, 0x3e00, 0x3500, 0x3f00, + 0x3d00, 0x3f00, 0x3c00, +]; + +/// BSAC probability table 9, MSB-1, non-zero higher bits (Table 4.A.64). +pub const PROB_T9_NZ_1: [u16; 1] = [0x2e00]; + +/// BSAC probability table 9, MSB-2, non-zero higher bits (Table 4.A.64). +pub const PROB_T9_NZ_2: [u16; 3] = [0x2900, 0x2a00, 0x2700]; + +/// BSAC probability table 9, MSB-3, non-zero higher bits (Table 4.A.64). +pub const PROB_T9_NZ_3: [u16; 7] = [0x2d00, 0x2500, 0x2400, 0x2500, 0x2400, 0x2500, 0x2300]; + +/// BSAC probability table 9, others, non-zero higher bits (Table 4.A.64). +pub const PROB_T9_NZ_4: [u16; 16] = [ + 0x2800, 0x2500, 0x2300, 0x2300, 0x2200, 0x2200, 0x2200, 0x2200, 0x2200, 0x2200, 0x2200, 0x2100, + 0x2000, 0x2200, 0x2100, 0x2000, +]; + +/// BSAC probability table 10 (MSB plane 5), MSB row (Table 4.A.65). +pub const PROB_T10_MSB: [u16; 15] = [ + 0x3b00, 0x3c00, 0x3400, 0x3c00, 0x3200, 0x3900, 0x2e00, 0x3d00, 0x3400, 0x3900, 0x2f00, 0x3c00, + 0x2d00, 0x3700, 0x2d00, +]; + +/// BSAC probability table 10, MSB-1, non-zero higher bits (Table 4.A.65). +pub const PROB_T10_NZ_1: [u16; 1] = [0x3100]; + +/// BSAC probability table 10, MSB-2, non-zero higher bits (Table 4.A.65). +pub const PROB_T10_NZ_2: [u16; 3] = [0x2b00, 0x2a00, 0x2900]; + +/// BSAC probability table 10, MSB-3, non-zero higher bits (Table 4.A.65). +pub const PROB_T10_NZ_3: [u16; 7] = [0x2700, 0x2600, 0x2500, 0x2500, 0x2500, 0x2200, 0x2200]; + +/// BSAC probability table 10, others, non-zero higher bits (Table 4.A.65). +pub const PROB_T10_NZ_4: [u16; 16] = [ + 0x2200, 0x2300, 0x2300, 0x2300, 0x2200, 0x2300, 0x2200, 0x2300, 0x2200, 0x2200, 0x2200, 0x2200, + 0x2200, 0x2000, 0x2100, 0x2200, +]; + +/// The seven Table 4.A.44–4.A.50 `cband_si` models, indexed by the +/// Table 4.A.31 `other_model` column. +pub const CBAND_SI_MODELS: [&[u16]; 7] = [ + &CBAND_SI_MODEL_0, + &CBAND_SI_MODEL_1, + &CBAND_SI_MODEL_2, + &CBAND_SI_MODEL_3, + &CBAND_SI_MODEL_4, + &CBAND_SI_MODEL_5, + &CBAND_SI_MODEL_6, +]; + +/// The Table 4.A.37–4.A.43 scalefactor models, indexed by +/// `scf_model` (Table 4.A.32; model 0 has no table). +pub const SCF_MODELS: [Option<&[u16]>; 8] = [ + None, + Some(&SCF_MODEL_1), + Some(&SCF_MODEL_2), + Some(&SCF_MODEL_3), + Some(&SCF_MODEL_4), + Some(&SCF_MODEL_5), + Some(&SCF_MODEL_6), + Some(&SCF_MODEL_7), +]; + +/// Table 4.A.34 — position of the probability value inside a +/// zero-higher-bits row, from the neighbour context. +/// +/// * `a = i % 4` — the line's offset in its aligned 4-line group. +/// * `b`, `c`, `d` — the current-plane sliced bits already decoded +/// for lines `i-3`, `i-2`, `i-1` (only the in-group ones apply: +/// `d` from `a >= 1`, `c` from `a >= 2`, `b` from `a >= 3`). +/// * `e`, `f`, `g`, `h` — whether the higher bits of lines +/// `i-a+3`, `i-a+2`, `i-a+1`, `i-a` are non-zero. Flags of lines +/// at or after `i` are 0 by construction (their higher bits for +/// the *current* plane are what is being decoded), which is +/// exactly how the table's absent cells are shaped. +/// +/// Returns the row position `0..=64`. +pub fn context_position(a: usize, prev_bits: [u8; 3], group_higher_nonzero: [u8; 4]) -> usize { + debug_assert!(a < 4); + // `prev_bits = [b, c, d]` — the current-plane bits of lines + // i-3, i-2, i-1; `group_higher_nonzero = [h, g, f, e]` — the + // higher-bits-non-zero flags of the aligned group lines + // i-a .. i-a+3 in line order. + let [b, c, d] = prev_bits; + let [h, g, f, e] = group_higher_nonzero; + // Column index within the printed table: (h, g, f, e) walked as + // a 4-bit number h·8 + g·4 + f·2 + e. + let col = + (usize::from(h) << 3) | (usize::from(g) << 2) | (usize::from(f) << 1) | usize::from(e); + match a { + 0 => { + // h refers to line i itself: always 0 here. 8 columns. + const ROW: [usize; 8] = [0, 15, 22, 29, 32, 39, 42, 45]; + ROW[col & 7] + } + 1 => { + // g refers to line i: 0. Columns h∈{0,1} × f,e. + const ROW_D0: [[usize; 4]; 2] = [[1, 16, 23, 30], [46, 53, 56, 59]]; + const ROW_D1: [[usize; 4]; 2] = [[2, 17, 24, 31], [46, 53, 56, 59]]; + let h_i = usize::from(h); + let fe = col & 3; + if d == 0 { + ROW_D0[h_i][fe] + } else { + ROW_D1[h_i][fe] + } + } + 2 => { + // f refers to line i: 0. Columns (h, g) × e. + // Row selected by (c, d). + const ROWS: [[usize; 8]; 4] = [ + // (h,g,e) order: 000,001,010,011,100,101,110,111 + [3, 18, 33, 40, 47, 54, 60, 63], // c=0, d=0 + [4, 19, 33, 40, 48, 55, 60, 63], // c=0, d=1 + [5, 20, 34, 41, 47, 54, 60, 63], // c=1, d=0 + [6, 21, 34, 41, 48, 55, 60, 63], // c=1, d=1 + ]; + let row = ((c as usize) << 1) | d as usize; + let hge = ((usize::from(h)) << 2) | ((usize::from(g)) << 1) | usize::from(e); + ROWS[row][hge] + } + _ => { + // a == 3: e refers to line i: 0. Columns (h, g, f). + // Row selected by (b, c, d). + const ROWS: [[usize; 8]; 8] = [ + [7, 25, 35, 43, 49, 57, 61, 64], // 000 + [8, 25, 36, 43, 50, 57, 62, 64], // 001 + [9, 26, 35, 43, 51, 58, 61, 64], // 010 + [10, 26, 36, 43, 52, 58, 62, 64], // 011 + [11, 27, 37, 44, 49, 57, 61, 64], // 100 + [12, 27, 38, 44, 50, 57, 62, 64], // 101 + [13, 28, 37, 44, 51, 58, 61, 64], // 110 + [14, 28, 38, 44, 52, 58, 62, 64], // 111 + ]; + let row = ((b as usize) << 2) | ((c as usize) << 1) | d as usize; + let hgf = ((usize::from(h)) << 2) | ((usize::from(g)) << 1) | usize::from(f); + ROWS[row][hgf] + } + } +} + +/// One probability table's explicit rows (base tables 1..=10; the +/// aliased tables 11..=22 resolve onto 9 / 10 in [`spectral_p0`]). +struct ProbTable { + /// The MSB-plane row (15 positions — higher-bit flags are all + /// zero at the MSB by construction). + msb: &'static [u16], + /// Zero-higher-bits rows for `rel = 1..` (65 positions each). + /// Tables 9 / 10 leave this empty and alias tables 7 / 8. + zero: &'static [&'static [u16]], + /// Non-zero-higher-bits rows for `rel = 1..` (sizes + /// `min(2^rel - 1, 16)`). + nz: &'static [&'static [u16]], +} + +const PROB_TABLES: [ProbTable; 10] = [ + ProbTable { + msb: &PROB_T1_MSB, + zero: &[], + nz: &[], + }, + ProbTable { + msb: &PROB_T2_MSB, + zero: &[], + nz: &[], + }, + ProbTable { + msb: &PROB_T3_MSB, + zero: &[&PROB_T3_ZERO_1], + nz: &[&PROB_T3_NZ_1], + }, + ProbTable { + msb: &PROB_T4_MSB, + zero: &[&PROB_T4_ZERO_1], + nz: &[&PROB_T4_NZ_1], + }, + ProbTable { + msb: &PROB_T5_MSB, + zero: &[&PROB_T5_ZERO_1, &PROB_T5_ZERO_2], + nz: &[&PROB_T5_NZ_1, &PROB_T5_NZ_2], + }, + ProbTable { + msb: &PROB_T6_MSB, + zero: &[&PROB_T6_ZERO_1, &PROB_T6_ZERO_2], + nz: &[&PROB_T6_NZ_1, &PROB_T6_NZ_2], + }, + ProbTable { + msb: &PROB_T7_MSB, + zero: &[&PROB_T7_ZERO_1, &PROB_T7_ZERO_2, &PROB_T7_ZERO_3], + nz: &[&PROB_T7_NZ_1, &PROB_T7_NZ_2, &PROB_T7_NZ_3], + }, + ProbTable { + msb: &PROB_T8_MSB, + zero: &[&PROB_T8_ZERO_1, &PROB_T8_ZERO_2, &PROB_T8_ZERO_3], + nz: &[&PROB_T8_NZ_1, &PROB_T8_NZ_2, &PROB_T8_NZ_3], + }, + ProbTable { + msb: &PROB_T9_MSB, + zero: &[], + nz: &[&PROB_T9_NZ_1, &PROB_T9_NZ_2, &PROB_T9_NZ_3, &PROB_T9_NZ_4], + }, + ProbTable { + msb: &PROB_T10_MSB, + zero: &[], + nz: &[ + &PROB_T10_NZ_1, + &PROB_T10_NZ_2, + &PROB_T10_NZ_3, + &PROB_T10_NZ_4, + ], + }, +]; + +/// Resolve a `cband_si` (1..=22) to `(base probability table 1..=10, +/// MSB plane)` per Table 4.A.33 and the Table 4.A.66–4.A.77 alias +/// notes ("Same as BSAC probability Table 9/10, but MSB plane = M"). +fn resolve_table(cband_si: u8) -> (usize, u8) { + debug_assert!((1..=22).contains(&cband_si)); + let plane = CBAND_SI_MSB_PLANE[cband_si as usize]; + // 2009 alias scheme. NOTE: the 2001 edition prints a different + // scheme (tables 11..=22 all onto table 10, and the sub-MSB + // zero rows of 9/10 onto table 8) — both readings were tested + // against the 14496-26 conformance streams and neither matches + // the deployed encoder's selection; see the crate README's + // BSAC divergence note. + let base = match cband_si { + 1..=10 => cband_si, + 11 | 13 => 9, + 12 | 14 => 10, + _ => 9, // tables 15..=22 alias table 9 at planes 8..=15 + }; + (base as usize, plane) +} + +/// The spectral bit-slice `p0` — the probability of the "0" symbol +/// for one sliced bit, per §4.6.4.2.3. +/// +/// * `cband_si` — the coding band's side info (1..=22; 0 never +/// decodes spectral bits). +/// * `snf` — the significance (bit plane, 1-based) being decoded. +/// * `hbv` — the line's own decoded higher bits (the +/// `higher_bit_vector`, bits above `snf` as an integer). +/// * `pos` — the Table 4.A.34 context position (only consulted when +/// `hbv == 0`). +pub fn spectral_p0(cband_si: u8, snf: u8, hbv: u32, pos: usize) -> u16 { + let (base, plane) = resolve_table(cband_si); + let t = &PROB_TABLES[base - 1]; + debug_assert!(snf >= 1 && snf <= plane); + let rel = usize::from(plane - snf); + if hbv != 0 { + // Non-zero decoded higher bits: index by min(hbv, 16) - 1. + let rows = if t.nz.is_empty() { &[] } else { t.nz }; + let row = rows[rel.min(rows.len()) - 1]; + let idx = (hbv.min(16) as usize - 1).min(row.len() - 1); + row[idx] + } else if rel == 0 { + t.msb[pos.min(t.msb.len() - 1)] + } else { + // Zero rows: tables 9 / 10 (and the 11..=22 aliases on + // them) borrow tables 7 / 8 for the sub-MSB rows. + let (rows, cap) = if t.zero.is_empty() { + let borrowed = if base == 9 { + &PROB_TABLES[6] + } else { + &PROB_TABLES[7] + }; + (borrowed.zero, borrowed.zero.len()) + } else { + (t.zero, t.zero.len()) + }; + rows[rel.min(cap) - 1][pos] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cumulative_frequency_models_are_well_formed() { + let mut all: Vec<&[u16]> = vec![ + &MS_USED_MODEL, + &STEREO_INFO_MODEL, + &NOISE_FLAG_MODEL, + &NOISE_MODE_MODEL, + &CBAND_SI_MODEL_CBAND0, + ]; + all.extend(CBAND_SI_MODELS.iter().copied()); + all.extend(SCF_MODELS.iter().flatten().copied()); + for model in all { + assert!(model[0] < 0x4000, "cum freq must sit under 2^14"); + assert!( + model.windows(2).all(|w| w[0] > w[1]), + "cum freqs strictly decreasing" + ); + assert_eq!(*model.last().unwrap(), 0, "last cum freq is 0"); + } + } + + #[test] + fn model_sizes_cover_their_largest_symbols() { + for (i, p) in CBAND_SI_TYPES.iter().enumerate() { + assert!( + CBAND_SI_MODELS[p.other_model as usize].len() > p.largest_other as usize, + "type {i}: other model too small" + ); + assert!( + CBAND_SI_MODEL_CBAND0.len() > p.largest_cband0 as usize, + "type {i}: cband0 model too small" + ); + } + for (m, largest) in SCF_MODEL_LARGEST.iter().enumerate() { + if let Some(model) = SCF_MODELS[m] { + assert_eq!( + model.len(), + usize::from(*largest) + 1, + "scf model {m} size vs Table 4.A.32 largest" + ); + } + } + } + + /// Every Table 4.A.34 position 0..=64 is reachable, and every + /// reachable context yields a position <= 64. + #[test] + fn context_positions_cover_the_table() { + let mut seen = [false; 65]; + for a in 0..4usize { + for bits in 0..8u8 { + let (b, c, d) = ((bits >> 2) & 1, (bits >> 1) & 1, bits & 1); + // Only the in-group predecessors apply; zero the rest + // like the decoder does. + let (b, c, d) = match a { + 0 => (0, 0, 0), + 1 => (0, 0, d), + 2 => (0, c, d), + _ => (b, c, d), + }; + for flags in 0..16u8 { + let (h, g, f, _e) = ( + (flags >> 3) & 1, + (flags >> 2) & 1, + (flags >> 1) & 1, + flags & 1, + ); + // Flags at or after line i are structurally 0 + // (the last group line's flag e never survives + // the mask below). + let (h, g, f, e) = match a { + 0 => (0, 0, 0, 0), + 1 => (h, 0, 0, 0), + 2 => (h, g, 0, 0), + _ => (h, g, f, 0), + }; + let pos = context_position(a, [b, c, d], [h, g, f, e]); + assert!(pos <= 64); + seen[pos] = true; + } + } + } + // The e..h flags of *later* in-group lines can be non-zero + // too (their hbv from earlier planes) — walk the full flag + // space for coverage. + for a in 0..4usize { + for bits in 0..8u8 { + let (b, c, d) = ((bits >> 2) & 1, (bits >> 1) & 1, bits & 1); + for flags in 0..16u8 { + let (h, g, f, e) = ( + (flags >> 3) & 1, + (flags >> 2) & 1, + (flags >> 1) & 1, + flags & 1, + ); + let pos = context_position(a, [b, c, d], [h, g, f, e]); + assert!(pos <= 64); + seen[pos] = true; + } + } + } + assert!(seen.iter().all(|&s| s), "all 65 positions reachable"); + } + + /// [`spectral_p0`] resolves every `(cband_si, snf, hbv, pos)` + /// combination without panicking, always inside (0, 2^14). + #[test] + fn spectral_p0_covers_every_context() { + for cband_si in 1u8..=22 { + let plane = CBAND_SI_MSB_PLANE[cband_si as usize]; + for snf in 1..=plane { + let rel = plane - snf; + let max_hbv: u32 = if rel >= 31 { u32::MAX } else { (1 << rel) - 1 }; + for hbv in 0..=max_hbv.min(40) { + let poss: &[usize] = if rel == 0 { &[0, 7, 14] } else { &[0, 32, 64] }; + for &pos in poss { + let p0 = spectral_p0(cband_si, snf, hbv, pos); + assert!( + p0 > 0 && p0 < 0x4000, + "cband_si {cband_si} snf {snf} hbv {hbv} pos {pos}: {p0:#x}" + ); + } + } + } + } + } + + #[test] + fn p0_clamps_are_consistent() { + for len in 1..14usize { + assert!(MIN_P0[len] <= MAX_P0[len]); + } + assert_eq!(clamp_p0(0x3fff, 1), 0x2000); + assert_eq!(clamp_p0(0x0001, 1), 0x2000); + assert_eq!(clamp_p0(0x1234, 14), 0x1234); + } + + /// Spot-check transcription anchors against the printed spec + /// listings. + #[test] + fn transcription_anchors() { + assert_eq!(CBAND_SI_MODEL_0[0], 0x3ef6); // Table 4.A.44 + assert_eq!(CBAND_SI_MODEL_6[0], 0x31af); // Table 4.A.50 + assert_eq!(CBAND_SI_MODEL_CBAND0[0], 0x3ff8); // Table 4.A.51 + assert_eq!(MS_USED_MODEL[0], 0x2ccd); // Table 4.A.52 + assert_eq!(STEREO_INFO_MODEL, [0x3666, 0x1000, 0x0666, 0]); // 4.A.53 + assert_eq!(NOISE_FLAG_MODEL[0], 0x2000); // Table 4.A.54 + assert_eq!(SCF_MODEL_7[0], 0x3b5e); // Table 4.A.43 + assert_eq!(SCF_MODEL_7[63], 0); + assert_eq!(PROB_T1_MSB[0], 0x3900); // Table 4.A.56 + assert_eq!(PROB_T1_MSB[14], 0x2c00); + assert_eq!(PROB_T7_MSB[0], 0x3d00); // Table 4.A.62 + assert_eq!(PROB_T7_NZ_1[0], 0x2f00); // the 2F00 uppercase cell + assert_eq!(PROB_T9_NZ_4[15], 0x2000); // Table 4.A.64 last cell + assert_eq!(PROB_T10_NZ_4[15], 0x2200); // Table 4.A.65 last cell + } +} diff --git a/crates/vendor/oxideav-aac/src/cce.rs b/crates/vendor/oxideav-aac/src/cce.rs new file mode 100644 index 00000000..5d766554 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/cce.rs @@ -0,0 +1,1289 @@ +//! `coupling_channel_element()` — ISO/IEC 14496-3 §4.6.8.3 / Table 4.8. +//! +//! The coupling channel element (CCE, `id_syn_ele == 0b010`) carries an +//! embedded `single_channel_element()` whose decoded spectrum is scaled +//! by a list of *gain elements* and added onto one or more target +//! channels (SCE / CPE) signalled by the coupling header. This module +//! owns the **coupling header + gain-list** half of Table 4.8: +//! +//! ```text +//! coupling_channel_element() { +//! element_instance_tag; 4 uimsbf // consumed by the walker +//! ind_sw_cce_flag; 1 uimsbf +//! num_coupled_elements; 3 uimsbf +//! num_gain_element_lists = 0; +//! for (c = 0; c < num_coupled_elements+1; c++) { +//! num_gain_element_lists++; +//! cc_target_is_cpe[c]; 1 uimsbf +//! cc_target_tag_select[c]; 4 uimsbf +//! if (cc_target_is_cpe[c]) { +//! cc_l[c]; 1 uimsbf +//! cc_r[c]; 1 uimsbf +//! if (cc_l[c] && cc_r[c]) num_gain_element_lists++; +//! } +//! } +//! cc_domain; 1 uimsbf +//! gain_element_sign; 1 uimsbf +//! gain_element_scale; 2 uimsbf +//! individual_channel_stream(0,0); // the embedded SCE body +//! for (c=1; c> 1`), per the ISO/IEC 14496-3:2001 / +//! 13818-7:2004 `couple_channel()` text as ruled in +//! `docs/audio/aac/cce-gain-sign-split.md` §3. A `common_gain_element` +//! is **never** sign-split (`cc_sign = 1` forced in that branch — so an +//! independently switched CCE, which must use common gains only, always +//! couples in phase). The first coupled target (`list_index == 0`) is +//! not transmitted: its gains are all `0`, i.e. the CCE is added in its +//! natural scaling (`cc_gain == 1`). +//! +//! ## Provenance +//! +//! Table 4.8 syntax, the §4.6.8.3.3 `decode_coupling_channel()` / +//! `couple_channel()` pseudocode, the Table 4.153 shared-gain-list table, +//! and the Table 4.154 `cc_scale_table` are all from ISO/IEC 14496-3 +//! staged under `docs/audio/aac/`. The gain elements reuse the +//! §4.A.1 scalefactor Huffman codebook (codebook 12) via +//! [`crate::scale_factor_data::hcod_sf_decode`] / +//! [`crate::scale_factor_data::hcod_sf_encode`], exactly as the spec +//! directs ("gain_element values are differentially encoded using the +//! Huffman table for scalefactors"). + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::ics_body::IcsBody; +use crate::ics_info::IcsInfo; +use crate::scale_factor_data::{hcod_sf_decode, hcod_sf_encode}; +use crate::section_data::ZERO_HCB; +use crate::spectral_data::SpectralData; +use crate::{Error, Result}; + +/// Field width of `ind_sw_cce_flag` (Table 4.8). +pub const IND_SW_CCE_FLAG_BITS: u32 = 1; +/// Field width of `num_coupled_elements` (Table 4.8). +pub const NUM_COUPLED_ELEMENTS_BITS: u32 = 3; +/// Field width of `cc_target_tag_select` (Table 4.8). +pub const CC_TARGET_TAG_SELECT_BITS: u32 = 4; +/// Field width of `gain_element_scale` (Table 4.8). +pub const GAIN_ELEMENT_SCALE_BITS: u32 = 2; + +/// Table 4.154 — the four `cc_scale` amplitude resolutions selected by +/// the 2-bit `gain_element_scale`. `cc_scale = 2^(1/8 · 2^scale)`: +/// `2^(1/8)`, `2^(1/4)`, `2^(1/2)`, `2^1` (step sizes 0.75 / 1.5 / 3.0 / +/// 6.0 dB). +pub const CC_SCALE_TABLE: [f64; 4] = [ + 1.090_507_732_665_257_7, // 2^(1/8) + 1.189_207_115_002_721, // 2^(1/4) + std::f64::consts::SQRT_2, // 2^(1/2) + 2.0, // 2^1 +]; + +/// One coupled target of a CCE (Table 4.8 inner loop, one `c`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CoupledTarget { + /// `cc_target_is_cpe[c]` — the coupled target is a CPE (`true`) or a + /// SCE (`false`). + pub is_cpe: bool, + /// `cc_target_tag_select[c]` — the `element_instance_tag` of the + /// coupled SCE / CPE. + pub tag_select: u8, + /// `cc_l[c]` — a gain list applies to the CPE's left channel. Always + /// `false` for a SCE target. + pub cc_l: bool, + /// `cc_r[c]` — a gain list applies to the CPE's right channel. Always + /// `false` for a SCE target. + pub cc_r: bool, +} + +impl CoupledTarget { + /// The number of `num_gain_element_lists` slots this target + /// contributes (Table 4.8): one per target, plus a *second* slot for + /// a CPE target whose `cc_l && cc_r` (the shared-vs-split gain-list + /// distinction, Table 4.153). + fn gain_list_increment(&self) -> u32 { + if self.is_cpe && self.cc_l && self.cc_r { + 2 + } else { + 1 + } + } +} + +/// Parsed `coupling_channel_element()` header (everything before the +/// embedded `individual_channel_stream(0,0)`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CouplingHeader { + /// `ind_sw_cce_flag` — independently switched (`true`) vs dependently + /// switched (`false`). An independently switched CCE must only use + /// `common_gain_element` and is decoded to the time domain before + /// coupling (§4.6.8.3.3); a dependently switched CCE shares the + /// target window state and couples in the frequency domain. + pub ind_sw_cce_flag: bool, + /// `num_coupled_elements` — the number of coupled targets is + /// `num_coupled_elements + 1` (minimum value `0` ⇒ one target). + pub num_coupled_elements: u8, + /// The `num_coupled_elements + 1` coupled targets. + pub targets: Vec, + /// `cc_domain` — coupling performed before (`false`) or after + /// (`true`) TNS decoding of the coupled target channels. + pub cc_domain: bool, + /// `gain_element_sign` — the transmitted gain elements carry + /// in-phase / out-of-phase coupling information (`true`) or not + /// (`false`). + pub gain_element_sign: bool, + /// `gain_element_scale` — 2-bit index into [`CC_SCALE_TABLE`]. + pub gain_element_scale: u8, + /// `num_gain_element_lists` derived by the Table 4.8 loop. This is + /// the number of transmitted gain lists; the trailing gain loop runs + /// over `1 ..= num_gain_element_lists - 1` (list 0 is the implicit + /// natural-scaling target). + pub num_gain_element_lists: u32, +} + +impl CouplingHeader { + /// Parse the Table 4.8 coupling header. `reader` is positioned at + /// `ind_sw_cce_flag` (i.e. the caller — typically the + /// [`crate::raw_data_block`] walker — already consumed the 4-bit + /// `element_instance_tag`). + pub fn parse(reader: &mut BitReader<'_>) -> Result { + let ind_sw_cce_flag = read_bit(reader)?; + let num_coupled_elements = read_u8(reader, NUM_COUPLED_ELEMENTS_BITS)?; + + let mut num_gain_element_lists: u32 = 0; + let mut targets = Vec::with_capacity(usize::from(num_coupled_elements) + 1); + for _c in 0..(u32::from(num_coupled_elements) + 1) { + num_gain_element_lists += 1; + let is_cpe = read_bit(reader)?; + let tag_select = read_u8(reader, CC_TARGET_TAG_SELECT_BITS)?; + let (cc_l, cc_r) = if is_cpe { + let cc_l = read_bit(reader)?; + let cc_r = read_bit(reader)?; + if cc_l && cc_r { + num_gain_element_lists += 1; + } + (cc_l, cc_r) + } else { + (false, false) + }; + targets.push(CoupledTarget { + is_cpe, + tag_select, + cc_l, + cc_r, + }); + } + + let cc_domain = read_bit(reader)?; + let gain_element_sign = read_bit(reader)?; + let gain_element_scale = read_u8(reader, GAIN_ELEMENT_SCALE_BITS)?; + + Ok(CouplingHeader { + ind_sw_cce_flag, + num_coupled_elements, + targets, + cc_domain, + gain_element_sign, + gain_element_scale, + num_gain_element_lists, + }) + } + + /// Write the Table 4.8 coupling header (mirror of [`Self::parse`]), + /// **not** including the leading `element_instance_tag` (the caller / + /// frame assembler owns that, exactly as the walker consumes it on + /// the parse side). + /// + /// Rejects an inconsistent record: a `targets` count that disagrees + /// with `num_coupled_elements + 1`, a `gain_element_scale > 3`, or a + /// SCE target carrying a `cc_l` / `cc_r` flag. + pub fn write(&self, writer: &mut BitWriter) -> Result<()> { + if self.targets.len() != usize::from(self.num_coupled_elements) + 1 { + return Err(Error::CceInvalid); + } + if self.gain_element_scale > 3 { + return Err(Error::CceInvalid); + } + let mut derived_lists: u32 = 0; + for t in &self.targets { + if !t.is_cpe && (t.cc_l || t.cc_r) { + return Err(Error::CceInvalid); + } + derived_lists += t.gain_list_increment(); + } + if derived_lists != self.num_gain_element_lists { + return Err(Error::CceInvalid); + } + + writer.write_bit(self.ind_sw_cce_flag); + writer.write_u32( + u32::from(self.num_coupled_elements), + NUM_COUPLED_ELEMENTS_BITS, + ); + for t in &self.targets { + writer.write_bit(t.is_cpe); + writer.write_u32(u32::from(t.tag_select), CC_TARGET_TAG_SELECT_BITS); + if t.is_cpe { + writer.write_bit(t.cc_l); + writer.write_bit(t.cc_r); + } + } + writer.write_bit(self.cc_domain); + writer.write_bit(self.gain_element_sign); + writer.write_u32(u32::from(self.gain_element_scale), GAIN_ELEMENT_SCALE_BITS); + Ok(()) + } +} + +/// One decoded per-band coupling gain of a `dpcm_gain_element` list — +/// the §4.6.8.3.3 (2001 / 13818-7:2004) `couple_channel()` gain-decode +/// output for one `(g, sfb)`: the `cc_sign` out-of-phase flag split off +/// the transmitted DPCM delta, and the accumulated `gain_element` +/// exponent (see `docs/audio/aac/cce-gain-sign-split.md` §3). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct DpcmGain { + /// `cc_sign == −1` (out-of-phase coupling) for this band. Set from + /// the delta LSB (`dpcm & 1`) when `gain_element_sign == 1`; always + /// `false` when the sign bit is clear. + pub negative: bool, + /// The accumulated `gain_element[g][sfb]` exponent — + /// `a += dpcm >> 1` under `gain_element_sign == 1`, `a += dpcm` + /// otherwise. + pub gain: i32, +} + +/// The decoded gain list for one coupled target (Table 4.8 trailing +/// loop, one `c`). Either a single `common_gain_element` applied to +/// every band, or a per-`(g, sfb)` `dpcm_gain_element` list decoded by +/// the §4.6.8.3.3 forward running sum. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GainList { + /// `cge == 1`: one `common_gain_element` reused over every window + /// group and scalefactor band (§4.6.8.3.3). Never sign-split — the + /// 2001 / 13818-7 text forces `cc_sign = 1` in this branch. + Common(i32), + /// `cge == 0`: the per-band decoded gain grid, indexed + /// `gains[g][sfb]`. Only the non-`ZERO_HCB` bands are transmitted; + /// `ZERO_HCB` bands hold the running accumulator value with an + /// in-phase sign (no delta is read there). + Dpcm(Vec>), +} + +/// The whole trailing gain-list block of a CCE (Table 4.8), one +/// [`GainList`] per transmitted list (`1 ..= num_gain_element_lists`). +/// +/// The implicit list 0 (natural scaling) is **not** stored — the +/// [`Self::cc_gain`] helper returns `1.0` for it. +#[derive(Debug, Clone, PartialEq)] +pub struct CouplingGains { + /// The `gain_element_scale`-selected `cc_scale` from Table 4.154. + pub cc_scale: f64, + /// `gain_element_sign` from the coupling header (informational — + /// the in-phase / out-of-phase split is resolved per band at parse + /// time into [`DpcmGain::negative`], per the + /// `docs/audio/aac/cce-gain-sign-split.md` §3 ruling; the writer + /// keys off the [`CouplingHeader`] it is handed). + pub gain_element_sign: bool, + /// The transmitted gain lists, in `c = 1 ..= num_gain_element_lists` + /// order (`lists[0]` is the `c == 1` list). + pub lists: Vec, +} + +impl CouplingGains { + /// Parse the Table 4.8 trailing gain-list loop. `reader` is + /// positioned immediately after the embedded + /// `individual_channel_stream(0,0)`. + /// + /// * `header` — the already-parsed [`CouplingHeader`]. + /// * `num_window_groups` / `max_sfb` — from the embedded SCE's + /// `ics_info()`. + /// * `sfb_cb` — the embedded SCE's per-`(g, sfb)` section codebooks + /// ([`crate::section_data::SectionData::sfb_cb`]); the §4.6.8.3.3 + /// `Note` requires the CCE's *own* codebooks here, not the coupled + /// target's. + pub fn parse( + reader: &mut BitReader<'_>, + header: &CouplingHeader, + num_window_groups: usize, + max_sfb: usize, + sfb_cb: &[Vec], + ) -> Result { + let cc_scale = CC_SCALE_TABLE[usize::from(header.gain_element_scale & 0x3)]; + let mut lists = Vec::new(); + for _c in 1..header.num_gain_element_lists { + let cge = if header.ind_sw_cce_flag { + true + } else { + read_bit(reader)? + }; + if cge { + let common = i32::from(hcod_sf_decode(reader)?); + lists.push(GainList::Common(common)); + } else { + // An independently switched CCE must only use the common + // gain element (§4.6.8.3.3); a per-band list here is + // ill-formed. `cge` is already forced true above for that + // case, so reaching the else branch with ind_sw set is + // impossible, but guard against a hand-built record. + if header.ind_sw_cce_flag { + return Err(Error::CceInvalid); + } + // §4.6.8.3.3 (2001 / 13818-7:2004) gain-decode loop — + // under `gain_element_sign` the out-of-phase flag is + // split off **each transmitted delta** (`cc_sign = + // 1 − 2·(dpcm & 1)`) and the accumulator is fed with + // the remaining magnitude (`a += dpcm >> 1`, arithmetic + // shift); with the sign bit clear the delta accumulates + // whole. Ruled in + // `docs/audio/aac/cce-gain-sign-split.md` §3 (the + // 14496-3:2009 fragment that splits the *accumulated* + // value is an editorial defect of that edition). + let mut acc: i32 = 0; + let mut grid = vec![vec![DpcmGain::default(); max_sfb]; num_window_groups]; + for (g, row) in grid.iter_mut().enumerate() { + let cb_row = sfb_cb.get(g).ok_or(Error::CceInvalid)?; + for (sfb, cell) in row.iter_mut().enumerate() { + let cb = *cb_row.get(sfb).ok_or(Error::CceInvalid)?; + if cb != ZERO_HCB { + let dpcm = i32::from(hcod_sf_decode(reader)?); + if header.gain_element_sign { + acc += dpcm >> 1; + *cell = DpcmGain { + negative: (dpcm & 1) != 0, + gain: acc, + }; + } else { + acc += dpcm; + *cell = DpcmGain { + negative: false, + gain: acc, + }; + } + } else { + // ZERO_HCB band carries the running value but + // contributes no coupling (cc_gain unused). + *cell = DpcmGain { + negative: false, + gain: acc, + }; + } + } + } + lists.push(GainList::Dpcm(grid)); + } + } + Ok(CouplingGains { + cc_scale, + gain_element_sign: header.gain_element_sign, + lists, + }) + } + + /// Write the trailing gain-list loop (mirror of [`Self::parse`]). + /// `sfb_cb` must be the same embedded-SCE codebook grid the parse + /// consumed so the `ZERO_HCB` bands are skipped identically. + pub fn write( + &self, + writer: &mut BitWriter, + header: &CouplingHeader, + sfb_cb: &[Vec], + ) -> Result<()> { + if self.lists.len() + 1 != header.num_gain_element_lists as usize { + return Err(Error::CceInvalid); + } + for list in &self.lists { + match list { + GainList::Common(common) => { + if !header.ind_sw_cce_flag { + // common_gain_element_present[c] = 1 + writer.write_bit(true); + } + let dpcm = i8::try_from(*common).map_err(|_| Error::CceInvalid)?; + let (len, cw) = hcod_sf_encode(dpcm)?; + writer.write_u32(cw, u32::from(len)); + } + GainList::Dpcm(grid) => { + if header.ind_sw_cce_flag { + return Err(Error::CceInvalid); + } + // common_gain_element_present[c] = 0 + writer.write_bit(false); + // Exact inverse of the §4.6.8.3.3 gain-decode loop: + // under `gain_element_sign` each delta packs the + // out-of-phase flag into its LSB + // (`dpcm = ((gain − prev) << 1) | negative`, which + // `dpcm >> 1` / `dpcm & 1` recover for every signed + // delta); with the sign bit clear the delta is the + // plain gain difference and an out-of-phase band is + // unrepresentable (rejected). + let mut prev: i32 = 0; + for (g, row) in grid.iter().enumerate() { + let cb_row = sfb_cb.get(g).ok_or(Error::CceInvalid)?; + for (sfb, cell) in row.iter().enumerate() { + let cb = *cb_row.get(sfb).ok_or(Error::CceInvalid)?; + if cb != ZERO_HCB { + let delta = cell.gain - prev; + let dpcm = if header.gain_element_sign { + (delta << 1) | i32::from(cell.negative) + } else { + if cell.negative { + return Err(Error::CceInvalid); + } + delta + }; + let dpcm = i8::try_from(dpcm).map_err(|_| Error::CceInvalid)?; + let (len, cw) = hcod_sf_encode(dpcm)?; + writer.write_u32(cw, u32::from(len)); + prev = cell.gain; + } + } + } + } + } + } + Ok(()) + } + + /// The §4.6.8.3.3 `couple_channel()` per-band gain factor for a given + /// transmitted gain list and `(g, sfb)`. + /// + /// `list_index` is the §4.6.8.3.3 `couple_channel()` `gain_list_index` + /// (`0` = the implicit natural-scaling target → `cc_gain == 1.0`; + /// `1 ..= num_gain_element_lists - 1` index [`Self::lists`]). + /// + /// Returns `cc_gain = cc_sign · cc_scale^(−gain_element)`: + /// * for a [`GainList::Dpcm`] band, `cc_sign` and `gain_element` + /// are the per-band values the parse loop split off the DPCM + /// deltas (`docs/audio/aac/cce-gain-sign-split.md` §3 — the + /// 2001 / 13818-7:2004 `couple_channel()` gain decode); + /// * for a [`GainList::Common`] list, `cc_sign = 1` always — the + /// ruled text never sign-splits a `common_gain_element`, so an + /// independently switched CCE (common gains only) couples in + /// phase regardless of `gain_element_sign`. + /// + /// The **negated** exponent is the conformance-settled reading of + /// the §4.6.8.3.3 `cc_scale^gain_element` expression. All three + /// staged editions print a positive exponent, but the ISO/IEC + /// 14496-26 `am05_*` vectors (the only normative CCE bitstreams; + /// every AU carries `common_gain_element = −1` lists) reconstruct + /// their reference waveforms only with `cc_scale^(−ge)` — with the + /// printed positive exponent every coupled target channel misses by + /// ~1e-1 err/sig, with the negated form all six channels land at + /// ~1e-4. This resolves the question + /// `docs/audio/aac/cce-gain-sign-split.md` §4 left open (a + /// black-box validator had measured the negated exponent; the + /// conformance corpus now confirms it as the normative wire + /// convention). The §3 sign-split ruling is orthogonal (the + /// corpus's `gain_element_sign` is always 0) and is implemented in + /// the parse loop. + pub fn cc_gain(&self, list_index: usize, g: usize, sfb: usize) -> Result { + if list_index == 0 { + // The first coupled target's gains are not transmitted; the + // CCE adds in its natural scaling (gain = 0 ⇒ cc_gain = 1). + return Ok(1.0); + } + let list = self.lists.get(list_index - 1).ok_or(Error::CceInvalid)?; + let (cc_sign, gain) = match list { + GainList::Common(common) => (1.0, *common), + GainList::Dpcm(grid) => { + let cell = grid + .get(g) + .and_then(|row| row.get(sfb)) + .ok_or(Error::CceInvalid)?; + (if cell.negative { -1.0 } else { 1.0 }, cell.gain) + } + }; + Ok(cc_sign * self.cc_scale.powi(-gain)) + } + + /// §4.6.8.3.3 `couple_channel(source_spectrum, dest_spectrum, + /// gain_list_index)` — scale the CCE's embedded-SCE spectrum by the + /// `gain_list_index` gain list and **add** it onto one target + /// channel's window-major spectrum in place. + /// + /// This is the per-band scale-and-add the spec pseudocode defines: + /// + /// ```text + /// for (g = 0; g < num_window_groups; g++) + /// for (b = 0; b < window_group_length[g]; b++) + /// for (sfb = 0; sfb < max_sfb; sfb++) + /// if (sfb_cb[g][sfb] != ZERO_HCB) + /// for (i = swb_offset[sfb]; i < swb_offset[sfb+1]; i++) + /// dest[g][b][sfb][i] += cc_gain(idx,g,sfb) * source[g][b][sfb][i]; + /// ``` + /// + /// `cc_gain` per band is [`Self::cc_gain`] (`cc_sign · cc_scale^(−gain)`); + /// the implicit list 0 (`list_index == 0`) couples in natural scaling + /// (`cc_gain == 1`) onto every non-`ZERO_HCB` band. + /// + /// * `source` / `dest` — window-major spectra + /// (`num_windows × window_len`), identical geometry. `source` is the + /// decoded embedded-SCE spectrum; `dest` is the addressed SCE / CPE + /// channel's spectrum at the §4.6.8.3.3 `cc_domain` stage (before or + /// after TNS). + /// * `list_index` — the §4.6.8.3.3 `couple_channel()` `gain_list_index` + /// the [`CouplingHeader`] walk assigns to this target. + /// * `sfb_cb` — the **embedded SCE's** per-`(g, sfb)` section + /// codebooks, per the §4.6.8.3.3 Note (`sfb_cb` is the CCE's own + /// codebook data, not the coupled target's). Drives the `ZERO_HCB` + /// band skip and, for a `GainList::Dpcm` list, the gain lookup. + /// * `window_group_length` / `max_sfb` — the embedded SCE's + /// `ics_info()` group geometry. + /// * `offsets` — the `swb_offset` table for the embedded SCE's window + /// length (`window_len + 1` entries; `offsets[sfb]..offsets[sfb+1]` + /// is band `sfb`). + /// + /// Returns [`Error::CceInvalid`] on any geometry mismatch (source / + /// dest length, group / band shapes) so a malformed coupling does not + /// corrupt the target out of bounds. + #[allow(clippy::too_many_arguments)] + pub fn couple_channel( + &self, + source: &[f64], + dest: &mut [f64], + list_index: usize, + sfb_cb: &[Vec], + window_group_length: &[u8], + max_sfb: usize, + offsets: &[u16], + ) -> Result<()> { + if source.len() != dest.len() { + return Err(Error::CceInvalid); + } + if offsets.is_empty() { + return Err(Error::CceInvalid); + } + // The last `swb_offset` entry is the window length (the first + // coefficient past the last band). The window-major spectrum is + // `num_windows * window_len` long. + let window_len = usize::from(*offsets.last().expect("non-empty checked above")); + if window_len == 0 || source.len() % window_len != 0 { + return Err(Error::CceInvalid); + } + let num_swb = offsets.len() - 1; + if max_sfb > num_swb { + return Err(Error::CceInvalid); + } + if sfb_cb.len() != window_group_length.len() { + return Err(Error::CceInvalid); + } + + let mut window_base = 0usize; + for (g, &wgl) in window_group_length.iter().enumerate() { + let cb_row = sfb_cb.get(g).ok_or(Error::CceInvalid)?; + if cb_row.len() < max_sfb { + return Err(Error::CceInvalid); + } + let wgl = usize::from(wgl); + for sfb in 0..max_sfb { + if cb_row[sfb] == ZERO_HCB { + // §4.6.8.3.3: ZERO_HCB bands carry no coupling + // contribution (and, for a DPCM list, were not + // transmitted — the accumulator simply skipped them). + continue; + } + let start = usize::from(offsets[sfb]); + let end = usize::from(offsets[sfb + 1]); + let cc_gain = self.cc_gain(list_index, g, sfb)?; + for b in 0..wgl { + let base = (window_base + b) + .checked_mul(window_len) + .ok_or(Error::CceInvalid)?; + let dst_end = base + end; + if dst_end > dest.len() { + return Err(Error::CceInvalid); + } + for i in start..end { + dest[base + i] += cc_gain * source[base + i]; + } + } + } + window_base += wgl; + } + Ok(()) + } +} + +/// A fully-parsed `coupling_channel_element()` (Table 4.8): the coupling +/// header, the embedded `individual_channel_stream(0,0)` (body + +/// spectrum), and the trailing gain lists. +/// +/// This is the single entry point a `raw_data_block()` walker uses to +/// **consume a whole CCE** from the bitstream (advancing the reader past +/// it). The decode loop can then either drop the element (a CCE +/// contributes no output channel of its own) or, once the cross-element +/// coupling is wired, scale [`Self::spectral`] by [`Self::gains`] and add +/// it onto the addressed target channels (§4.6.8.3.3 `couple_channel()`). +#[derive(Debug, Clone, PartialEq)] +pub struct CouplingChannelElement { + /// `element_instance_tag` (4 bits) — the CCE's own instance tag. + pub element_instance_tag: u8, + /// The Table 4.8 coupling header. + pub header: CouplingHeader, + /// The embedded `individual_channel_stream(0,0)` body (Table 4.50), + /// up to but not including `spectral_data()`. + pub body: IcsBody, + /// `ics_info()` of the embedded SCE (cloned out of [`Self::body`] for + /// convenience; the embedded body always reads its own `ics_info`). + pub ics_info: IcsInfo, + /// The embedded SCE's `spectral_data()` (Table 4.56). + pub spectral: SpectralData, + /// The Table 4.8 trailing gain lists. + pub gains: CouplingGains, +} + +impl CouplingChannelElement { + /// Parse a whole `coupling_channel_element()` (Table 4.8). `reader` + /// is positioned at `element_instance_tag` (i.e. immediately after + /// the `raw_data_block()` walker read the 3-bit `id_syn_ele == CCE`). + /// + /// * `aot` — the surrounding ASC's effective `audioObjectType`. + /// * `fs_index` — the `samplingFrequencyIndex`. + /// + /// Walks, in spec order: the 4-bit instance tag, the + /// [`CouplingHeader`], the embedded `individual_channel_stream(0,0)` + /// ([`IcsBody`] + [`SpectralData`]), and the [`CouplingGains`] + /// gain-list loop keyed off the embedded SCE's `sfb_cb`. + pub fn parse(reader: &mut BitReader<'_>, aot: u8, fs_index: u8) -> Result { + let element_instance_tag = read_u8(reader, 4)?; + Self::parse_after_tag(reader, element_instance_tag, aot, fs_index) + } + + /// Parse a `coupling_channel_element()` whose 4-bit + /// `element_instance_tag` was already consumed by the surrounding + /// `raw_data_block()` walker (which returns the tag in its + /// `ChannelElement` event). `reader` is positioned at + /// `ind_sw_cce_flag`; `element_instance_tag` is the walker-supplied + /// tag. Otherwise identical to [`Self::parse`]. + pub fn parse_after_tag( + reader: &mut BitReader<'_>, + element_instance_tag: u8, + aot: u8, + fs_index: u8, + ) -> Result { + Self::parse_after_tag_family( + reader, + crate::swb_offset::FrameFamily::Lc1024, + element_instance_tag, + aot, + fs_index, + ) + } + + /// [`Self::parse_after_tag`] under an explicit §4.5.1.1 + /// frame-length family (a 960-line `raw_data_block()` may carry a + /// CCE like any other; the ER payloads — including LD — have no + /// CCE at all per §4.5.2.4, so the LD families never reach here). + pub fn parse_after_tag_family( + reader: &mut BitReader<'_>, + family: crate::swb_offset::FrameFamily, + element_instance_tag: u8, + aot: u8, + fs_index: u8, + ) -> Result { + let header = CouplingHeader::parse(reader)?; + // Embedded individual_channel_stream(0,0): common_window = 0 and + // scale_flag = 0 per Table 4.8. + let body = IcsBody::parse_family(reader, family, aot, fs_index, false)?; + let ics_info = body.ics_info.clone().ok_or(Error::CceInvalid)?; + let spectral = SpectralData::parse(reader, &ics_info, &body.section_data, fs_index)?; + let gains = CouplingGains::parse( + reader, + &header, + usize::from(ics_info.num_window_groups), + usize::from(ics_info.max_sfb), + &body.section_data.sfb_cb, + )?; + Ok(CouplingChannelElement { + element_instance_tag, + header, + body, + ics_info, + spectral, + gains, + }) + } +} + +/// Helper: read a 1-bit flag, mapping underflow to [`Error::UnexpectedEnd`]. +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} + +/// Helper: read an `n`-bit `uimsbf` field as a `u8`. +fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { + Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Table 4.154 values are `2^(2^scale / 8)`. + #[test] + fn cc_scale_table_matches_spec_resolutions() { + for (scale, &v) in CC_SCALE_TABLE.iter().enumerate() { + let expected = 2f64.powf((1u32 << scale) as f64 / 8.0); + assert!( + (v - expected).abs() < 1e-12, + "cc_scale[{scale}] = {v} != {expected}" + ); + } + } + + /// A header with a single SCE target derives `num_gain_element_lists + /// == 1` (only the implicit list 0 — no trailing gains). + #[test] + fn single_sce_target_has_one_gain_list() { + // ind_sw=0, num_coupled=0, target0: is_cpe=0 tag=0, + // cc_domain=0 sign=0 scale=0 + let mut writer = BitWriter::new(); + writer.write_bit(false); // ind_sw_cce_flag + writer.write_u32(0, 3); // num_coupled_elements + writer.write_bit(false); // cc_target_is_cpe[0] + writer.write_u32(0, 4); // cc_target_tag_select[0] + writer.write_bit(false); // cc_domain + writer.write_bit(false); // gain_element_sign + writer.write_u32(0, 2); // gain_element_scale + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + let h = CouplingHeader::parse(&mut reader).unwrap(); + assert_eq!(h.num_gain_element_lists, 1); + assert_eq!(h.targets.len(), 1); + assert!(!h.targets[0].is_cpe); + } + + /// A CPE target with `cc_l && cc_r` adds a second gain list slot + /// (Table 4.153: split left/right lists). + #[test] + fn cpe_target_with_both_channels_adds_a_list() { + let mut writer = BitWriter::new(); + writer.write_bit(false); // ind_sw_cce_flag + writer.write_u32(0, 3); // num_coupled_elements (=> 1 target) + writer.write_bit(true); // cc_target_is_cpe[0] + writer.write_u32(3, 4); // cc_target_tag_select[0] + writer.write_bit(true); // cc_l[0] + writer.write_bit(true); // cc_r[0] + writer.write_bit(false); // cc_domain + writer.write_bit(false); // gain_element_sign + writer.write_u32(1, 2); // gain_element_scale + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + let h = CouplingHeader::parse(&mut reader).unwrap(); + // 1 (target) + 1 (cc_l && cc_r) = 2. + assert_eq!(h.num_gain_element_lists, 2); + assert!(h.targets[0].is_cpe); + assert!(h.targets[0].cc_l && h.targets[0].cc_r); + assert_eq!(h.targets[0].tag_select, 3); + } + + /// The header round-trips through write → parse. + #[test] + fn header_round_trips() { + let h = CouplingHeader { + ind_sw_cce_flag: true, + num_coupled_elements: 1, + targets: vec![ + CoupledTarget { + is_cpe: false, + tag_select: 2, + cc_l: false, + cc_r: false, + }, + CoupledTarget { + is_cpe: true, + tag_select: 5, + cc_l: true, + cc_r: false, + }, + ], + cc_domain: true, + gain_element_sign: true, + gain_element_scale: 2, + num_gain_element_lists: 2, + }; + let mut writer = BitWriter::new(); + h.write(&mut writer).unwrap(); + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + let parsed = CouplingHeader::parse(&mut reader).unwrap(); + assert_eq!(parsed, h); + } + + /// `write` rejects a SCE target carrying a `cc_l` flag. + #[test] + fn write_rejects_sce_target_with_cc_flag() { + let h = CouplingHeader { + ind_sw_cce_flag: false, + num_coupled_elements: 0, + targets: vec![CoupledTarget { + is_cpe: false, + tag_select: 0, + cc_l: true, + cc_r: false, + }], + cc_domain: false, + gain_element_sign: false, + gain_element_scale: 0, + num_gain_element_lists: 1, + }; + let mut writer = BitWriter::new(); + assert_eq!(h.write(&mut writer), Err(Error::CceInvalid)); + } + + /// `cc_gain` for the implicit list 0 is the natural scaling 1.0. + #[test] + fn cc_gain_list_zero_is_unity() { + let gains = CouplingGains { + cc_scale: CC_SCALE_TABLE[3], + gain_element_sign: false, + lists: vec![], + }; + assert_eq!(gains.cc_gain(0, 0, 0).unwrap(), 1.0); + } + + /// `cc_gain` applies `cc_scale^(−gain)` (conformance-settled + /// exponent sign) for a common-gain list with the sign bit clear. + #[test] + fn cc_gain_common_no_sign() { + let gains = CouplingGains { + cc_scale: 2.0, // scale index 3 => 2^1 + gain_element_sign: false, + lists: vec![GainList::Common(3)], + }; + // gain = 3, cc_sign = 1 => 2^-3 = 1/8. + assert!((gains.cc_gain(1, 0, 0).unwrap() - 0.125).abs() < 1e-12); + } + + /// A `common_gain_element` is never sign-split, even when the + /// header's `gain_element_sign` is set — the 2001 / 13818-7:2004 + /// `couple_channel()` forces `cc_sign = 1` in the common branch + /// (`docs/audio/aac/cce-gain-sign-split.md` §3), which also makes + /// every independently switched CCE couple in phase. + #[test] + fn cc_gain_common_never_sign_split() { + let gains = CouplingGains { + cc_scale: 2.0, + gain_element_sign: true, + lists: vec![GainList::Common(3)], + }; + // gain_element = 3, cc_sign = +1 => +2^-3, not a split raw + // value. + assert!((gains.cc_gain(1, 0, 0).unwrap() - 0.125).abs() < 1e-12); + } + + /// The sign-split DPCM decode takes `cc_sign` from each **delta** + /// LSB and accumulates `dpcm >> 1` (§3 ruling): the worked + /// `[3, 3]` sequence from `cce-gain-sign-split.md` §2.2 must land + /// at `{−cc_scale^−1, −cc_scale^−2}` under the negated exponent + /// (per-band signs both negative, exponents 1 then 2) — not the + /// `{−1, +3}` split of the 2009 fragment-A misprint. + #[test] + fn cc_gain_dpcm_delta_split() { + let sfb_cb = vec![vec![2u8, 2u8]]; + let header = CouplingHeader { + ind_sw_cce_flag: false, + num_coupled_elements: 1, + targets: vec![ + CoupledTarget { + is_cpe: false, + tag_select: 0, + cc_l: false, + cc_r: false, + }, + CoupledTarget { + is_cpe: false, + tag_select: 1, + cc_l: false, + cc_r: false, + }, + ], + cc_domain: false, + gain_element_sign: true, + gain_element_scale: 3, // cc_scale = 2 + num_gain_element_lists: 2, + }; + // Transmit the deltas [3, 3] directly. + let mut writer = BitWriter::new(); + writer.write_bit(false); // common_gain_element_present = 0 + for _ in 0..2 { + let (len, cw) = hcod_sf_encode(3).unwrap(); + writer.write_u32(cw, u32::from(len)); + } + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + let gains = CouplingGains::parse(&mut reader, &header, 1, 2, &sfb_cb).unwrap(); + // delta 3 => negative (3 & 1), a += 1 twice => gains 1, 2. + assert_eq!( + gains.lists, + vec![GainList::Dpcm(vec![vec![ + DpcmGain { + negative: true, + gain: 1 + }, + DpcmGain { + negative: true, + gain: 2 + }, + ]])] + ); + assert!((gains.cc_gain(1, 0, 0).unwrap() + 0.5).abs() < 1e-12); + assert!((gains.cc_gain(1, 0, 1).unwrap() + 0.25).abs() < 1e-12); + } + + /// The sign-split writer is the exact inverse of the parse loop, + /// including negative deltas (arithmetic-shift packing) and an + /// interior `ZERO_HCB` skip. + #[test] + fn dpcm_sign_split_round_trips() { + let sfb_cb = vec![vec![2u8, ZERO_HCB, 4u8, 4u8]]; + let header = CouplingHeader { + ind_sw_cce_flag: false, + num_coupled_elements: 1, + targets: vec![ + CoupledTarget { + is_cpe: false, + tag_select: 0, + cc_l: false, + cc_r: false, + }, + CoupledTarget { + is_cpe: false, + tag_select: 1, + cc_l: false, + cc_r: false, + }, + ], + cc_domain: false, + gain_element_sign: true, + gain_element_scale: 1, + num_gain_element_lists: 2, + }; + let grid = vec![vec![ + DpcmGain { + negative: true, + gain: -2, + }, + // ZERO_HCB carry cell (not transmitted). + DpcmGain { + negative: false, + gain: -2, + }, + DpcmGain { + negative: false, + gain: 1, + }, + DpcmGain { + negative: true, + gain: 1, + }, + ]]; + let gains = CouplingGains { + cc_scale: CC_SCALE_TABLE[1], + gain_element_sign: true, + lists: vec![GainList::Dpcm(grid.clone())], + }; + let mut writer = BitWriter::new(); + gains.write(&mut writer, &header, &sfb_cb).unwrap(); + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + let parsed = CouplingGains::parse(&mut reader, &header, 1, 4, &sfb_cb).unwrap(); + assert_eq!(parsed.lists, vec![GainList::Dpcm(grid)]); + } + + /// An out-of-phase band under a clear `gain_element_sign` is + /// unrepresentable on the wire and must be rejected by the writer, + /// not silently dropped. + #[test] + fn write_rejects_negative_band_without_sign_bit() { + let sfb_cb = vec![vec![2u8]]; + let header = CouplingHeader { + ind_sw_cce_flag: false, + num_coupled_elements: 1, + targets: vec![ + CoupledTarget { + is_cpe: false, + tag_select: 0, + cc_l: false, + cc_r: false, + }, + CoupledTarget { + is_cpe: false, + tag_select: 1, + cc_l: false, + cc_r: false, + }, + ], + cc_domain: false, + gain_element_sign: false, + gain_element_scale: 0, + num_gain_element_lists: 2, + }; + let gains = CouplingGains { + cc_scale: CC_SCALE_TABLE[0], + gain_element_sign: false, + lists: vec![GainList::Dpcm(vec![vec![DpcmGain { + negative: true, + gain: 0, + }]])], + }; + let mut writer = BitWriter::new(); + assert_eq!( + gains.write(&mut writer, &header, &sfb_cb), + Err(Error::CceInvalid) + ); + } + + /// A dependently switched per-band DPCM list round-trips through + /// write → parse against a fixed `sfb_cb` grid, and the forward + /// accumulator reconstructs the absolute gains. + #[test] + fn dpcm_gain_list_round_trips() { + // One window group, three bands; band 1 is ZERO_HCB (skipped). + let sfb_cb = vec![vec![2u8, ZERO_HCB, 4u8]]; + let header = CouplingHeader { + ind_sw_cce_flag: false, + num_coupled_elements: 1, + targets: vec![ + CoupledTarget { + is_cpe: false, + tag_select: 0, + cc_l: false, + cc_r: false, + }, + CoupledTarget { + is_cpe: false, + tag_select: 1, + cc_l: false, + cc_r: false, + }, + ], + cc_domain: false, + gain_element_sign: false, + gain_element_scale: 0, + num_gain_element_lists: 2, + }; + // Absolute gains: band0 = +2 (dpcm +2), band1 carries acc (2, + // not transmitted), band2 = +5 (dpcm +3). + let grid = vec![vec![ + DpcmGain { + negative: false, + gain: 2, + }, + DpcmGain { + negative: false, + gain: 2, + }, + DpcmGain { + negative: false, + gain: 5, + }, + ]]; + let gains = CouplingGains { + cc_scale: CC_SCALE_TABLE[0], + gain_element_sign: false, + lists: vec![GainList::Dpcm(grid.clone())], + }; + let mut writer = BitWriter::new(); + gains.write(&mut writer, &header, &sfb_cb).unwrap(); + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + let parsed = CouplingGains::parse(&mut reader, &header, 1, 3, &sfb_cb).unwrap(); + assert_eq!(parsed.lists.len(), 1); + match &parsed.lists[0] { + GainList::Dpcm(g) => assert_eq!(g, &grid), + other => panic!("expected Dpcm, got {other:?}"), + } + } + + /// `couple_channel` for the implicit list 0 (natural scaling) adds + /// the source spectrum onto the target unchanged on every + /// non-`ZERO_HCB` band, and skips the `ZERO_HCB` band entirely. + #[test] + fn couple_channel_list_zero_adds_natural_scaling() { + // One window group, one window of length 8; two bands of width 4. + // Band 0 is a spectrum book (couples), band 1 is ZERO_HCB (skip). + let offsets = [0u16, 4, 8]; + let sfb_cb = vec![vec![2u8, ZERO_HCB]]; + let wgl = [1u8]; + let gains = CouplingGains { + cc_scale: CC_SCALE_TABLE[3], + gain_element_sign: false, + lists: vec![], + }; + let source = vec![1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + let mut dest = vec![10.0f64; 8]; + gains + .couple_channel(&source, &mut dest, 0, &sfb_cb, &wgl, 2, &offsets) + .unwrap(); + // Band 0 (indices 0..4): dest += 1*source. + assert_eq!(&dest[0..4], &[11.0, 12.0, 13.0, 14.0]); + // Band 1 (indices 4..8) is ZERO_HCB → untouched. + assert_eq!(&dest[4..8], &[10.0, 10.0, 10.0, 10.0]); + } + + /// `couple_channel` applies a non-unity common gain + /// (`cc_scale^(−gain)`) onto every coupled band. + #[test] + fn couple_channel_common_gain_scales() { + let offsets = [0u16, 4]; + let sfb_cb = vec![vec![2u8]]; + let wgl = [1u8]; + // gain element −1, sign clear, scale index 3 (cc_scale = 2) ⇒ + // cc_gain = 2^(−(−1)) = 2 (the am05 conformance vectors carry + // exactly this −1 common gain). + let gains = CouplingGains { + cc_scale: 2.0, + gain_element_sign: false, + lists: vec![GainList::Common(-1)], + }; + let source = vec![1.0f64, 2.0, 3.0, 4.0]; + let mut dest = vec![0.0f64; 4]; + gains + .couple_channel(&source, &mut dest, 1, &sfb_cb, &wgl, 1, &offsets) + .unwrap(); + assert_eq!(dest, vec![2.0, 4.0, 6.0, 8.0]); + } + + /// `couple_channel` walks the multi-window short-block grid: a window + /// group of length 2 applies the same per-sfb gain to both windows. + #[test] + fn couple_channel_multi_window_group() { + // num_windows = 2, window_len = 4, one group of length 2, one band. + let offsets = [0u16, 4]; + let sfb_cb = vec![vec![2u8]]; + let wgl = [2u8]; + let gains = CouplingGains { + cc_scale: 2.0, + gain_element_sign: false, + lists: vec![GainList::Common(0)], // cc_gain = 2^0 = 1 + }; + let source = vec![1.0f64, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0]; + let mut dest = vec![0.0f64; 8]; + gains + .couple_channel(&source, &mut dest, 1, &sfb_cb, &wgl, 1, &offsets) + .unwrap(); + // Both windows of the group are coupled at gain 1. + assert_eq!(dest, source); + } + + /// `couple_channel` per-band DPCM gains scale each band independently. + #[test] + fn couple_channel_dpcm_per_band_gains() { + let offsets = [0u16, 2, 4]; + let sfb_cb = vec![vec![2u8, 2u8]]; + let wgl = [1u8]; + // Absolute gains: band0 = 0 (cc_gain 1), band1 = −1 (cc_gain 2 + // under the conformance-settled negated exponent). + let gains = CouplingGains { + cc_scale: 2.0, + gain_element_sign: false, + lists: vec![GainList::Dpcm(vec![vec![ + DpcmGain { + negative: false, + gain: 0, + }, + DpcmGain { + negative: false, + gain: -1, + }, + ]])], + }; + let source = vec![3.0f64, 3.0, 3.0, 3.0]; + let mut dest = vec![0.0f64; 4]; + gains + .couple_channel(&source, &mut dest, 1, &sfb_cb, &wgl, 2, &offsets) + .unwrap(); + // Band 0 (0..2): ×1; band 1 (2..4): ×2. + assert_eq!(dest, vec![3.0, 3.0, 6.0, 6.0]); + } + + /// `couple_channel` rejects a source / dest length mismatch. + #[test] + fn couple_channel_rejects_length_mismatch() { + let offsets = [0u16, 4]; + let sfb_cb = vec![vec![2u8]]; + let gains = CouplingGains { + cc_scale: 2.0, + gain_element_sign: false, + lists: vec![], + }; + let source = vec![0.0f64; 4]; + let mut dest = vec![0.0f64; 8]; + assert_eq!( + gains.couple_channel(&source, &mut dest, 0, &sfb_cb, &[1u8], 1, &offsets), + Err(Error::CceInvalid) + ); + } + + /// An independently switched CCE forces `cge == 1`: no + /// `common_gain_element_present` bit is read, and the gain list is a + /// single common element per target. + #[test] + fn ind_sw_cce_uses_common_gain_only() { + let header = CouplingHeader { + ind_sw_cce_flag: true, + num_coupled_elements: 1, + targets: vec![ + CoupledTarget { + is_cpe: false, + tag_select: 0, + cc_l: false, + cc_r: false, + }, + CoupledTarget { + is_cpe: false, + tag_select: 1, + cc_l: false, + cc_r: false, + }, + ], + cc_domain: false, + gain_element_sign: false, + gain_element_scale: 0, + num_gain_element_lists: 2, + }; + let gains = CouplingGains { + cc_scale: CC_SCALE_TABLE[0], + gain_element_sign: false, + lists: vec![GainList::Common(1)], + }; + let mut writer = BitWriter::new(); + gains.write(&mut writer, &header, &[]).unwrap(); + let bytes = writer.into_bytes(); + let mut reader = BitReader::new(&bytes); + // No common_gain_element_present bit is present; parse must read + // exactly one hcod_sf codeword for the single list. + let parsed = CouplingGains::parse(&mut reader, &header, 1, 1, &[]).unwrap(); + assert_eq!(parsed.lists, vec![GainList::Common(1)]); + } +} diff --git a/crates/vendor/oxideav-aac/src/channel_map.rs b/crates/vendor/oxideav-aac/src/channel_map.rs new file mode 100644 index 00000000..93c74043 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/channel_map.rs @@ -0,0 +1,751 @@ +//! Canonical multichannel output ordering — ISO/IEC 14496-3 Table 1.19. +//! +//! A `raw_data_block()` lists its channel elements (SCE / CPE / LFE) in +//! **bitstream order**, and [`crate::decode::StreamDecoder`] decodes each +//! element's time signal into that same element order. For the default +//! `channelConfiguration` values 1–7 (Table 1.19) the spec fixes which +//! loudspeaker each element feeds, but the loudspeaker order is *not* the +//! order a downstream interleaved-PCM sink expects: a 5.1 decoder emits +//! its elements as `SCE(C), CPE(L,R), CPE(Ls,Rs), LFE` — speaker order +//! `[C, L, R, Ls, Rs, LFE]` — whereas the canonical interleaved layout +//! is `[L, R, C, LFE, Ls, Rs]` (the WAVE_FORMAT_EXTENSIBLE / BS.775 +//! convention that [`oxideav_core::ChannelLayout::Surround51`] adopts). +//! +//! This module owns the mapping from a `channelConfiguration` to: +//! +//! * the canonical [`ChannelLayout`] it denotes ([`layout_for_config`]), +//! and +//! * the **permutation** that reorders the element-order channel buffers +//! into that layout's canonical order ([`reorder_permutation`]). +//! +//! ## Element → speaker mapping (Table 1.19) +//! +//! Table 1.19's "channel to speaker mapping" column, read against the +//! "audio syntactic elements, listed in order received" column, gives the +//! per-element speaker assignment used here: +//! +//! | cfg | elements (in order) | element speaker order | +//! |-----|--------------------------------|----------------------------------| +//! | 1 | SCE | `[C]` | +//! | 2 | CPE | `[L, R]` | +//! | 3 | SCE, CPE | `[C, L, R]` | +//! | 4 | SCE, CPE, SCE | `[C, L, R, Cs]` | +//! | 5 | SCE, CPE, CPE | `[C, L, R, Ls, Rs]` | +//! | 6 | SCE, CPE, CPE, LFE | `[C, L, R, Ls, Rs, LFE]` | +//! +//! Each `ChannelPosition` in that element order is then matched to its +//! slot in the canonical layout (`ChannelLayout::positions()`), producing +//! the index permutation. The reorder is applied by the decode driver +//! before interleaving (see [`crate::decode`]). +//! +//! | 7 | SCE, CPE, CPE, CPE, LFE | `[C, Lc, Rc, L, R, Ls, Rs, LFE]` | +//! +//! Config 7 is the Table 1.19 7.1 arrangement (centre + inner +//! left/right *centre front* pair + outer left/right front pair + +//! surround pair + LFE); its canonical interleave follows the same +//! WAVE/BS.775 rank order as everything else, giving +//! `[L, R, C, LFE, Lc, Rc, Ls, Rs]`. `channelConfiguration == 0` +//! (custom layout) is handled by the §8.5.2.2 PCE mapping below +//! ([`pce_speaker_assignment`] / [`pce_reorder_permutation`]), driven +//! by the `program_config_element` the decoder captured; without an +//! active PCE the driver keeps bitstream element order. +//! +//! ## Clean-room provenance +//! +//! The element list and speaker mapping are transcribed from ISO/IEC +//! 14496-3:2009 §1.6.3.5 Table 1.19. The canonical interleaved order is +//! the WAVE_FORMAT_EXTENSIBLE / ITU-R BS.775 convention already encoded +//! in [`oxideav_core::ChannelLayout`]. + +use crate::pce::{ElementSelect, Pce}; +use oxideav_core::{ChannelLayout, ChannelPosition}; + +/// The canonical [`ChannelLayout`] denoted by a Table 1.19 +/// `channelConfiguration`, for the default values this crate reorders +/// (1–6). Returns `None` for `0` (PCE-defined), `7` (amendment-specific +/// 7.1), and any reserved value `≥ 8`. +#[must_use] +pub fn layout_for_config(channel_configuration: u8) -> Option { + Some(match channel_configuration { + 1 => ChannelLayout::Mono, + 2 => ChannelLayout::Stereo, + 3 => ChannelLayout::Surround30, + 4 => ChannelLayout::Surround40, + 5 => ChannelLayout::Surround50, + 6 => ChannelLayout::Surround51, + _ => return None, + }) +} + +/// The Table 1.19 per-element speaker order for a default +/// `channelConfiguration` — the loudspeaker each decoded channel feeds, +/// in the order the elements appear in the `raw_data_block()`. +/// +/// Returns `None` for `0` (PCE-defined — see +/// [`pce_speaker_assignment`]) and reserved values. +#[must_use] +pub fn element_speaker_order(channel_configuration: u8) -> Option<&'static [ChannelPosition]> { + use ChannelPosition::*; + Some(match channel_configuration { + 1 => &[FrontCenter], + 2 => &[FrontLeft, FrontRight], + 3 => &[FrontCenter, FrontLeft, FrontRight], + 4 => &[FrontCenter, FrontLeft, FrontRight, BackCenter], + 5 => &[FrontCenter, FrontLeft, FrontRight, SideLeft, SideRight], + 6 => &[ + FrontCenter, + FrontLeft, + FrontRight, + SideLeft, + SideRight, + LowFrequency, + ], + // Table 1.19 value 7 — 7+1: centre front; left, right CENTRE + // front (the inner pair); left, right OUTSIDE front; left, + // right surround rear (the same surround wording as configs + // 5/6, mapped to the side-surround positions this crate uses + // there); LFE. + 7 => &[ + FrontCenter, + FrontLeftOfCenter, + FrontRightOfCenter, + FrontLeft, + FrontRight, + SideLeft, + SideRight, + LowFrequency, + ], + _ => return None, + }) +} + +/// The permutation that reorders element-order channel buffers into the +/// canonical [`ChannelLayout`] order for a default `channelConfiguration`. +/// +/// The returned vector `perm` has one entry per output channel: output +/// slot `i` (in canonical layout order) is sourced from element-order +/// channel `perm[i]`. Applying it is `out[i] = channels[perm[i]]`. +/// +/// Returns `None` when no reordering is defined for this configuration +/// (`0` — PCE-defined — and reserved values); the caller keeps the +/// bitstream element order. An identity permutation (configs 1 and 2, +/// where element order already matches the canonical order) is +/// returned as `Some(vec![0, 1, …])` so the caller can still validate +/// the channel count. +#[must_use] +pub fn reorder_permutation(channel_configuration: u8) -> Option> { + let element_order = element_speaker_order(channel_configuration)?; + // Sort the element-order channels by their canonical WAVE/BS.775 + // interleave rank. For configs 1–6 this reproduces exactly the + // `ChannelLayout::positions()` order of `layout_for_config` (the + // named layouts list their speakers in mask order); config 7 has + // no named `ChannelLayout` but ranks the same way. + let mut perm: Vec = (0..element_order.len()).collect(); + let ranks: Vec = element_order + .iter() + .map(|&p| canonical_rank(p)) + .collect::>>()?; + perm.sort_by_key(|&i| ranks[i]); + Some(perm) +} + +/// Apply [`reorder_permutation`] to a set of element-order channel +/// buffers, returning the reordered set. When no permutation is defined +/// for `channel_configuration`, or the channel count does not match the +/// permutation length, the input order is preserved (returned unchanged). +/// +/// This is the entry point the decode driver calls once a frame's +/// element-order channels are assembled. +#[must_use] +pub fn reorder_channels(channel_configuration: u8, channels: Vec>) -> Vec> { + let Some(perm) = reorder_permutation(channel_configuration) else { + return channels; + }; + if perm.len() != channels.len() { + // Element count disagrees with the signalled configuration (a + // malformed or PCE-overridden stream); leave the order untouched + // rather than drop or duplicate a channel. + return channels; + } + // `perm[i]` is the source slot for output slot `i`. + apply_permutation(&perm, channels) +} + +// ===== PCE-defined layouts (`channelConfiguration == 0`) ===== +// +// ISO/IEC 13818-7 §8.5.2.2 (the PCE channel-configuration rules the +// 14496-3 GA payload inherits): the PCE carries a *list of front +// channels* "using the rule center outwards, left before right" (a +// center-channel SCE first, other SCEs in L/R pairs), then a list of +// *side channels* (CPEs or SCE pairs) "in the order of front to +// back", then a list of *back channels* "listed from outside in" +// (SCEs paired except that a final unpaired SCE is the rear center), +// then the LFE list. Each list references its elements by +// `*_element_is_cpe` + `*_element_tag_select`, so the mapping is by +// (element kind, instance tag), independent of the order the elements +// appear in the `raw_data_block()`. + +/// Which channel-element type a PCE list entry (or a decoded element) +/// is — the key half of the PCE (kind, tag) element reference. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PceElementKind { + /// `single_channel_element()`. + Sce, + /// `channel_pair_element()`. + Cpe, + /// `lfe_channel_element()`. + Lfe, +} + +/// Canonical interleave rank of a [`ChannelPosition`] — the +/// WAVE_FORMAT_EXTENSIBLE / BS.775 speaker-mask bit order this crate's +/// default-config reorder already targets. Lower rank interleaves +/// first. +fn canonical_rank(pos: ChannelPosition) -> Option { + use ChannelPosition::*; + Some(match pos { + FrontLeft => 0, + FrontRight => 1, + FrontCenter => 2, + LowFrequency => 3, + BackLeft => 4, + BackRight => 5, + FrontLeftOfCenter => 6, + FrontRightOfCenter => 7, + BackCenter => 8, + SideLeft => 9, + SideRight => 10, + _ => return None, + }) +} + +/// One PCE-addressed element with its speaker assignment: the +/// `(kind, instance tag)` reference and the position(s) its decoded +/// channel(s) feed, in the element's own channel order (`[left, +/// right]` for a CPE). +type PceAssignment = (PceElementKind, u8, Vec); + +/// Group a PCE element list into L/R pairs plus at most one unpaired +/// (center) SCE, preserving list order. CPEs are pairs by +/// construction; consecutive SCEs pair up left-then-right +/// (§8.5.2.2). Returns `(pairs, lone_sce_tag)` where each pair is +/// two `(is_cpe, tag)` halves (both halves of a CPE share its tag), +/// or `None` when the list leaves half an SCE pair over (an +/// ambiguous layout this crate leaves in element order). +#[allow(clippy::type_complexity)] +fn pair_up(list: &[ElementSelect], lone_first: bool) -> Option<(Vec<[(bool, u8); 2]>, Option)> { + let sce_count = list.iter().filter(|e| !e.is_cpe).count(); + // At most one SCE can be unpaired; §8.5.2.2 puts a front center + // first, while the back list's lone SCE (rear center) is last. + // Encoders are seen emitting the front center *last* too, so the + // rule keyed here is simply the parity: an odd SCE count means + // exactly one lone (center) SCE, taken at the position + // `lone_first` prefers when there is a choice. + let mut lone: Option = None; + let mut expect_lone = sce_count % 2 == 1; + let mut pairs: Vec<[(bool, u8); 2]> = Vec::new(); + let mut pending_sce: Option = None; + let sce_positions: Vec = (0..list.len()).filter(|&i| !list[i].is_cpe).collect(); + let lone_index = if expect_lone { + if lone_first { + sce_positions.first().copied() + } else { + sce_positions.last().copied() + } + } else { + None + }; + for (i, e) in list.iter().enumerate() { + if e.is_cpe { + pairs.push([(true, e.tag_select), (true, e.tag_select)]); + } else if expect_lone && Some(i) == lone_index { + lone = Some(e.tag_select); + expect_lone = false; + } else if let Some(left) = pending_sce.take() { + pairs.push([(false, left), (false, e.tag_select)]); + } else { + pending_sce = Some(e.tag_select); + } + } + if pending_sce.is_some() { + return None; // half an SCE pair left over + } + Some((pairs, lone)) +} + +/// Push one L/R pair's two assignment halves. +fn push_pair( + out: &mut Vec, + pair: [(bool, u8); 2], + left: ChannelPosition, + right: ChannelPosition, +) { + let [(l_cpe, l_tag), (r_cpe, r_tag)] = pair; + if l_cpe { + // One CPE carries both halves. + debug_assert!(r_cpe && l_tag == r_tag); + out.push((PceElementKind::Cpe, l_tag, vec![left, right])); + } else { + out.push((PceElementKind::Sce, l_tag, vec![left])); + out.push((PceElementKind::Sce, r_tag, vec![right])); + } +} + +/// Derive the §8.5.2.2 element→speaker assignment of a PCE-defined +/// layout. +/// +/// Returns `None` (caller keeps bitstream element order) for layouts +/// this crate cannot express in canonical positions: more than two +/// front pairs, more than one side pair, more than two back pairs, +/// more than one LFE, or a list shape §8.5.2.2 does not describe. +/// +/// Position choices, mirroring Table 42's named speakers: +/// +/// * front: the lone SCE (odd SCE count) is the front center; one +/// pair is the ordinary L/R; with two pairs, the first-listed +/// (inner — "center outwards") pair is the left/right *center* +/// front (`FrontLeftOfCenter` / `FrontRightOfCenter`) and the +/// second the outside L/R (the Table 42 index-7 arrangement). +/// * side: a single pair is the side surround `SideLeft`/`SideRight`. +/// * back: with two pairs ("listed from outside in") the first is +/// the side-most surround pair (`SideLeft`/`SideRight`) and the +/// second the rear `BackLeft`/`BackRight`; a single pair is the +/// rear `BackLeft`/`BackRight` when something else fixes the side +/// image (a side pair or a rear-center SCE), else the +/// `SideLeft`/`SideRight` surround pair of the 5.1-style layouts +/// (matching this crate's Table 1.19 config-5/6 mapping); a final +/// unpaired SCE is the `BackCenter`. +/// * every LFE-list entry is `LowFrequency` (at most one). +pub fn pce_speaker_assignment(pce: &Pce) -> Option> { + use ChannelPosition::*; + let mut out: Vec = Vec::new(); + + // Front list: center outwards. + let (front_pairs, front_center) = pair_up(&pce.front_elements, true)?; + if let Some(tag) = front_center { + out.push((PceElementKind::Sce, tag, vec![FrontCenter])); + } + match front_pairs.len() { + 0 => {} + 1 => push_pair(&mut out, front_pairs[0], FrontLeft, FrontRight), + 2 => { + push_pair( + &mut out, + front_pairs[0], + FrontLeftOfCenter, + FrontRightOfCenter, + ); + push_pair(&mut out, front_pairs[1], FrontLeft, FrontRight); + } + _ => return None, + } + + // Side list: front to back; only one distinct side position pair. + let (side_pairs, side_lone) = pair_up(&pce.side_elements, false)?; + if side_lone.is_some() || side_pairs.len() > 1 { + return None; + } + let have_side = side_pairs.len() == 1; + if have_side { + push_pair(&mut out, side_pairs[0], SideLeft, SideRight); + } + + // Back list: outside in; a final lone SCE is the rear center. + let (back_pairs, back_center) = pair_up(&pce.back_elements, false)?; + match back_pairs.len() { + 0 => {} + 1 => { + if have_side || back_center.is_some() { + push_pair(&mut out, back_pairs[0], BackLeft, BackRight); + } else { + // The single surround pair of a 5.1-style layout — + // the same SideLeft/SideRight this crate's Table 1.19 + // config-5/6 mapping uses. + push_pair(&mut out, back_pairs[0], SideLeft, SideRight); + } + } + 2 => { + if have_side { + return None; // three distinct surround pairs + } + push_pair(&mut out, back_pairs[0], SideLeft, SideRight); + push_pair(&mut out, back_pairs[1], BackLeft, BackRight); + } + _ => return None, + } + if let Some(tag) = back_center { + out.push((PceElementKind::Sce, tag, vec![BackCenter])); + } + + // LFE list. + match pce.lfe_element_tag_selects.len() { + 0 => {} + 1 => out.push(( + PceElementKind::Lfe, + pce.lfe_element_tag_selects[0], + vec![LowFrequency], + )), + _ => return None, // §8.5.2.3: no mapping for multiple LFEs + } + + // Every position must be distinct (and canonical-rankable). + let mut seen = [false; 11]; + for (_, _, positions) in &out { + for &p in positions { + let r = canonical_rank(p)?; + if seen[r] { + return None; + } + seen[r] = true; + } + } + Some(out) +} + +/// The permutation that reorders a PCE-defined frame's element-order +/// channel buffers into canonical interleave order. +/// +/// `elements` describes the decoded frame in bitstream order: one +/// `(kind, instance tag, channel count)` triple per channel element. +/// Every element must be referenced by the PCE exactly once with a +/// matching channel count, and the PCE's whole audio-element set must +/// appear in the frame; otherwise `None` is returned and the caller +/// keeps element order. +pub fn pce_reorder_permutation( + pce: &Pce, + elements: &[(PceElementKind, u8, usize)], +) -> Option> { + let mut assignment = pce_speaker_assignment(pce)?; + // Per decoded channel (element order): its canonical rank. + let mut ranks: Vec = Vec::new(); + for &(kind, tag, n_ch) in elements { + let idx = assignment + .iter() + .position(|&(k, t, _)| k == kind && t == tag)?; + let (_, _, positions) = assignment.swap_remove(idx); + if positions.len() != n_ch { + return None; // e.g. a PS-widened SCE — keep element order + } + for p in positions { + ranks.push(canonical_rank(p)?); + } + } + if !assignment.is_empty() { + return None; // PCE promises channels the frame did not carry + } + // Output slot i takes the source channel with the i-th smallest + // rank. Ranks are distinct by construction. + let mut perm: Vec = (0..ranks.len()).collect(); + perm.sort_by_key(|&i| ranks[i]); + Some(perm) +} + +/// Apply a permutation produced by [`pce_reorder_permutation`] to a +/// set of element-order channel buffers (same contract as +/// [`reorder_channels`]: `out[i] = channels[perm[i]]`). +#[must_use] +pub fn apply_permutation(perm: &[usize], channels: Vec>) -> Vec> { + if perm.len() != channels.len() { + return channels; + } + let mut slots: Vec>> = channels.into_iter().map(Some).collect(); + let mut out = Vec::with_capacity(perm.len()); + for &src in perm { + out.push( + slots[src] + .take() + .expect("permutation is a bijection over the channel slots"), + ); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::ChannelPosition::*; + + #[test] + fn mono_and_stereo_are_identity() { + assert_eq!(reorder_permutation(1), Some(vec![0])); + assert_eq!(reorder_permutation(2), Some(vec![0, 1])); + } + + #[test] + fn surround30_moves_center_to_third_slot() { + // element order [C, L, R] -> canonical [L, R, C] + assert_eq!(reorder_permutation(3), Some(vec![1, 2, 0])); + } + + #[test] + fn surround40_keeps_back_center_last() { + // element order [C, L, R, Cs] -> canonical [L, R, C, Cs] + assert_eq!(reorder_permutation(4), Some(vec![1, 2, 0, 3])); + } + + #[test] + fn surround50_orders_front_then_surround() { + // element order [C, L, R, Ls, Rs] -> canonical [L, R, C, Ls, Rs] + assert_eq!(reorder_permutation(5), Some(vec![1, 2, 0, 3, 4])); + } + + #[test] + fn surround51_interleaves_lfe_before_surround() { + // element order [C, L, R, Ls, Rs, LFE] -> canonical + // [L, R, C, LFE, Ls, Rs] + assert_eq!(reorder_permutation(6), Some(vec![1, 2, 0, 5, 3, 4])); + } + + #[test] + fn config_zero_and_reserved_are_unmapped() { + assert_eq!(reorder_permutation(0), None); + assert_eq!(reorder_permutation(8), None); + assert_eq!(reorder_permutation(15), None); + assert_eq!(layout_for_config(0), None); + // Config 7 reorders but denotes no named core layout. + assert_eq!(layout_for_config(7), None); + } + + #[test] + fn config_seven_lands_wave_rank_order() { + // element order [C, Lc, Rc, L, R, Ls, Rs, LFE] → canonical + // [L, R, C, LFE, Lc, Rc, Ls, Rs] (WAVE mask rank order). + assert_eq!(reorder_permutation(7), Some(vec![3, 4, 0, 7, 1, 2, 5, 6])); + } + + #[test] + fn permutation_matches_layout_positions() { + // The permutation must land each element on the layout slot whose + // ChannelPosition equals the element's Table 1.19 speaker. + for cfg in 1..=6u8 { + let perm = reorder_permutation(cfg).unwrap(); + let elem = element_speaker_order(cfg).unwrap(); + let layout = layout_for_config(cfg).unwrap(); + let canonical = layout.positions(); + assert_eq!(perm.len(), canonical.len(), "cfg {cfg} length"); + assert_eq!(canonical.len(), elem.len(), "cfg {cfg} element count"); + for (out_slot, &src) in perm.iter().enumerate() { + assert_eq!( + elem[src], canonical[out_slot], + "cfg {cfg}: output slot {out_slot} mismatched speaker" + ); + } + } + } + + #[test] + fn layout_channel_counts_agree_with_element_order() { + for cfg in 1..=6u8 { + let layout = layout_for_config(cfg).unwrap(); + let elem = element_speaker_order(cfg).unwrap(); + assert_eq!( + usize::from(layout.channel_count()), + elem.len(), + "cfg {cfg} channel count" + ); + } + } + + #[test] + fn reorder_channels_permutes_buffers() { + // 5.1 element order [C, L, R, Ls, Rs, LFE] tagged by a sentinel + // sample so we can see where each lands. + let channels: Vec> = vec![ + vec![0], // C + vec![1], // L + vec![2], // R + vec![3], // Ls + vec![4], // Rs + vec![5], // LFE + ]; + let out = reorder_channels(6, channels); + // canonical [L, R, C, LFE, Ls, Rs] = [1, 2, 0, 5, 3, 4] + let got: Vec = out.iter().map(|c| c[0]).collect(); + assert_eq!(got, vec![1, 2, 0, 5, 3, 4]); + } + + #[test] + fn reorder_channels_passthrough_on_unmapped_config() { + let channels: Vec> = vec![vec![9], vec![8]]; + let out = reorder_channels(0, channels.clone()); + assert_eq!(out, channels); + } + + #[test] + fn reorder_channels_passthrough_on_count_mismatch() { + // cfg 6 expects 6 channels; a 4-channel input is left untouched. + let channels: Vec> = vec![vec![0], vec![1], vec![2], vec![3]]; + let out = reorder_channels(6, channels.clone()); + assert_eq!(out, channels); + } + + // ===== §8.5.2.2 PCE-defined layouts ===== + + fn sce(tag: u8) -> ElementSelect { + ElementSelect { + is_cpe: false, + tag_select: tag, + } + } + fn cpe(tag: u8) -> ElementSelect { + ElementSelect { + is_cpe: true, + tag_select: tag, + } + } + fn pce_with( + front: Vec, + side: Vec, + back: Vec, + lfe: Vec, + ) -> Pce { + Pce { + element_instance_tag: 0, + object_type: 1, + sampling_frequency_index: 3, + front_elements: front, + side_elements: side, + back_elements: back, + lfe_element_tag_selects: lfe, + assoc_data_tag_selects: vec![], + valid_cc_elements: vec![], + mono_mixdown_element_number: None, + stereo_mixdown_element_number: None, + matrix_mixdown: None, + comment_field: vec![], + } + } + + #[test] + fn pce_5_1_matches_config_6_order() { + // front [SCE0(C), CPE0(L/R)], back [CPE1(Ls/Rs)], lfe [0] — + // the PCE spelling of the Table 1.19 config-6 layout. Element + // order SCE, CPE0, CPE1, LFE must permute exactly like + // config 6: [L, R, C, LFE, Ls, Rs]. + let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![cpe(1)], vec![0]); + use PceElementKind::*; + let perm = + pce_reorder_permutation(&pce, &[(Sce, 0, 1), (Cpe, 0, 2), (Cpe, 1, 2), (Lfe, 0, 1)]) + .expect("5.1 PCE maps"); + assert_eq!(perm, vec![1, 2, 0, 5, 3, 4]); + } + + #[test] + fn pce_7_1_two_back_pairs_outside_in() { + // The staged 7.1 fixture's PCE shape: front [SCE0, CPE0], + // back [CPE1, CPE2] ("outside in": CPE1 the side-most + // surround pair, CPE2 the rear pair), lfe [0]. Element order + // SCE, CPE0, CPE1, CPE2, LFE → canonical + // [FL FR FC LFE BL BR SL SR] = + // [Cpe0.l, Cpe0.r, Sce, Lfe, Cpe2.l, Cpe2.r, Cpe1.l, Cpe1.r]. + let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![cpe(1), cpe(2)], vec![0]); + use PceElementKind::*; + let perm = pce_reorder_permutation( + &pce, + &[ + (Sce, 0, 1), + (Cpe, 0, 2), + (Cpe, 1, 2), + (Cpe, 2, 2), + (Lfe, 0, 1), + ], + ) + .expect("7.1 PCE maps"); + assert_eq!(perm, vec![1, 2, 0, 7, 5, 6, 3, 4]); + } + + #[test] + fn pce_hexagonal_lone_sces_are_centers() { + // The staged hexagonal fixture's PCE: front [CPE0, SCE0] + // (the lone front SCE is the center wherever it is listed), + // back [CPE1, SCE1] (a final unpaired back SCE is the rear + // center — §8.5.2.2). Element order CPE0, SCE0, CPE1, SCE1 → + // canonical [FL FR FC BL BR BC]. + let pce = pce_with(vec![cpe(0), sce(0)], vec![], vec![cpe(1), sce(1)], vec![]); + use PceElementKind::*; + let perm = + pce_reorder_permutation(&pce, &[(Cpe, 0, 2), (Sce, 0, 1), (Cpe, 1, 2), (Sce, 1, 1)]) + .expect("hexagonal PCE maps"); + assert_eq!(perm, vec![0, 1, 2, 3, 4, 5], "already canonical order"); + + // The same layout with the block elements in a different + // order still lands canonically (mapping is by (kind, tag)). + let perm = + pce_reorder_permutation(&pce, &[(Sce, 1, 1), (Sce, 0, 1), (Cpe, 1, 2), (Cpe, 0, 2)]) + .unwrap(); + // element-order channels: [BC, FC, BL, BR, FL, FR] → + // canonical FL FR FC BL BR BC = sources [4, 5, 1, 2, 3, 0]. + assert_eq!(perm, vec![4, 5, 1, 2, 3, 0]); + } + + #[test] + fn pce_sce_pair_forms_lr() { + // Two SCEs in the front list (even count) form one L/R pair. + let pce = pce_with(vec![sce(0), sce(1)], vec![], vec![], vec![]); + let assign = pce_speaker_assignment(&pce).unwrap(); + assert_eq!( + assign, + vec![ + (PceElementKind::Sce, 0, vec![FrontLeft]), + (PceElementKind::Sce, 1, vec![FrontRight]), + ] + ); + } + + #[test] + fn pce_side_pair_moves_single_back_pair_to_rear() { + // side [CPE1] + back [CPE2]: the back pair is the rear + // BL/BR (the side pair holds SL/SR). + let pce = pce_with(vec![sce(0), cpe(0)], vec![cpe(1)], vec![cpe(2)], vec![]); + let assign = pce_speaker_assignment(&pce).unwrap(); + let find = |tag: u8| { + assign + .iter() + .find(|&&(k, t, _)| k == PceElementKind::Cpe && t == tag) + .map(|(_, _, p)| p.clone()) + .unwrap() + }; + assert_eq!(find(1), vec![SideLeft, SideRight]); + assert_eq!(find(2), vec![BackLeft, BackRight]); + } + + #[test] + fn pce_unmappable_layouts_fall_back() { + // Three front pairs: no canonical positions — None. + let pce = pce_with(vec![cpe(0), cpe(1), cpe(2)], vec![], vec![], vec![]); + assert!(pce_speaker_assignment(&pce).is_none()); + // Two LFEs: §8.5.2.3 defines no mapping. + let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![], vec![0, 1]); + assert!(pce_speaker_assignment(&pce).is_none()); + } + + #[test] + fn pce_permutation_rejects_mismatches() { + use PceElementKind::*; + let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![], vec![]); + // Channel-count mismatch (a PS-widened SCE): None. + assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 2), (Cpe, 0, 2)]).is_none()); + // An element the PCE does not reference: None. + assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 1), (Cpe, 0, 2), (Cpe, 5, 2)]).is_none()); + // A referenced element missing from the frame: None. + assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 1)]).is_none()); + } + + #[test] + fn every_speaker_in_canonical_appears_in_element_order() { + // Guards the bijection assumption reorder_channels relies on. + for cfg in 1..=6u8 { + let elem = element_speaker_order(cfg).unwrap(); + let layout = layout_for_config(cfg).unwrap(); + for &pos in layout.positions() { + assert!( + elem.contains(&pos), + "cfg {cfg}: canonical speaker {pos:?} missing from element order" + ); + } + } + // Sanity: a position only present in a higher layout is absent. + let elem5 = element_speaker_order(5).unwrap(); + assert!(!elem5.contains(&LowFrequency)); + } +} diff --git a/crates/vendor/oxideav-aac/src/codec_decoder.rs b/crates/vendor/oxideav-aac/src/codec_decoder.rs new file mode 100644 index 00000000..72b94f98 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/codec_decoder.rs @@ -0,0 +1,972 @@ +//! `oxideav_core::Decoder` wiring for AAC-LC carried in ADTS. +//! +//! The crate's [`decode::StreamDecoder`](crate::decode::StreamDecoder) +//! already walks one ADTS frame's §4.4.2.1 `raw_data_block()` to +//! interleaved 16-bit PCM end-to-end, carrying every channel element's +//! §4.6.11 overlap-add / §4.6.7 LTP / §4.6.6 predictor state across +//! frames. This module adapts that path into the framework's packet-in / +//! frame-out [`oxideav_core::Decoder`] trait so containers (the MP4 +//! `mp4a` object-type, the AVI / WAVEFORMATEX `0x00FF` raw-AAC tag, the +//! Matroska `A_AAC` CodecID, …) can route ADTS-framed AAC streams via the +//! registry. +//! +//! ## Trait-API adaptation +//! +//! The framework trait is *packet-in, frame-out*: +//! +//! * [`send_packet`](Decoder::send_packet) accepts one [`Packet`] whose +//! `data` is **one or more complete ADTS frames** — each an ADTS +//! fixed/variable header (+ optional 16-bit CRC) followed by its +//! `aac_frame_length`-delimited `raw_data_block()`. A leading ID3v2 tag +//! (the streaming-mux convention) is skipped. Every ADTS frame in the +//! packet is decoded in order against the persistent +//! [`StreamDecoder`](crate::decode::StreamDecoder), so the per-element +//! filterbank / LTP / predictor state threads across packet boundaries +//! exactly as it does across the frames of a contiguous stream. +//! * [`receive_frame`](Decoder::receive_frame) returns one +//! [`AudioFrame`] per decoded access unit: [`FRAME_LEN`] = 1024 +//! samples per channel for the default frame family (960 / 512 / +//! 480 under the §4.5.1.1 families a LATM-carried ASC can select, +//! 2048 for a dual-rate SBR frame), interleaved little-endian +//! `i16` in element order ([`SampleFormat::S16`]). +//! * [`flush`](Decoder::flush) marks end-of-stream so subsequent +//! `receive_frame` calls return [`Error::Eof`] once the pending queue +//! drains. +//! * [`reset`](Decoder::reset) drops the persistent +//! [`StreamDecoder`](crate::decode::StreamDecoder) (and with it all +//! §4.6.11 overlap / §4.6.7 LTP / §4.6.6 predictor memory) so the next +//! `send_packet` decodes as if it were the first — the trait contract +//! for a stateful, overlap-add codec after a container seek. +//! +//! ## Output format +//! +//! The decoder emits **interleaved** S16 PCM in `Frame::Audio`: +//! `data.len() == 1`, the single plane holding +//! `samples_per_channel * channels * 2` little-endian `i16` bytes in the +//! §4.4.2.1 element order an SCE/LFE contributes one channel, a CPE two. +//! The §4.6.11 [`pcm`](crate::pcm) output stage has already applied the +//! §1.3 `NINT()` round-half-away-from-zero and the 16-bit saturation, so +//! this layer only widens each `i16` to its two little-endian bytes. +//! +//! ## Registration +//! +//! [`register_codecs`] installs the codec under id `"aac"` and claims the +//! container tags an AAC stream is looked up under: the MP4 object-type +//! `0x40` (`Audio ISO/IEC 14496-3`), the WAVEFORMATEX `0x00FF` +//! (raw AAC) and `0x1601` (MPEG-4 ADTS AAC), the `mp4a` / `aac ` FourCCs, +//! and the Matroska `A_AAC` CodecID. A probe scores the ADTS syncword on +//! the first packet so a genuine ADTS stream out-ranks a non-ADTS +//! claimant on a shared tag. +//! +//! ## Provenance +//! +//! Every byte-layout and clause reference is from ISO/IEC 13818-7 / +//! 14496-3 staged under `docs/audio/aac/`; the trait adaptation composes +//! the crate's own [`decode::StreamDecoder`](crate::decode::StreamDecoder) +//! with the framework surface and reads no external decoder. + +use std::collections::VecDeque; + +use oxideav_core::{ + AudioFrame, CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag, + Confidence, Decoder, Error, Frame, Packet, ProbeContext, Result, SampleFormat, +}; + +use crate::adts::{AdtsHeader, ADTS_HEADER_BYTES_NO_CRC}; +use crate::decode::{DecodedFrame, StreamDecoder}; +use crate::latm::{LoasDecoder, AUDIO_SYNC_STREAM_SYNCWORD}; + +/// Codec id under which [`register_codecs`] installs this decoder. +pub const CODEC_ID_STR: &str = "aac"; + +/// MP4 object-type indicator for `Audio ISO/IEC 14496-3` (AAC). The OTI +/// every MP4 / ISO-BMFF `esds` AudioObject descriptor carries for an AAC +/// elementary stream. +pub const MP4_OBJECT_TYPE_AAC: u8 = 0x40; + +/// WAVEFORMATEX `wFormatTag` for raw AAC (`WAVE_FORMAT_RAW_AAC1`). +pub const WAVE_FORMAT_RAW_AAC1: u16 = 0x00FF; + +/// WAVEFORMATEX `wFormatTag` for MPEG-4 ADTS AAC (`WAVE_FORMAT_MPEG_ADTS_AAC`). +pub const WAVE_FORMAT_MPEG_ADTS_AAC: u16 = 0x1601; + +/// Build a boxed AAC [`Decoder`] from `params`. +/// +/// `params.sample_rate` and `params.channels` seed the returned +/// decoder's [`output_params`](AacDecoder)-equivalent stream description; +/// the real per-frame sample rate and channel count are re-derived from +/// each ADTS frame header on `send_packet`, so the values supplied here +/// are a hint only. The decoder is always built — AAC carries its full +/// configuration in-band (the ADTS header), so no parameter is mandatory. +pub fn make_decoder(params: &CodecParameters) -> Result> { + let sample_rate = params.sample_rate.unwrap_or(44_100); + let channels = params.channels.unwrap_or(2); + + let mut out_params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + out_params.sample_rate = Some(sample_rate); + out_params.channels = Some(channels); + out_params.sample_format = Some(SampleFormat::S16); + + let mut dec = AacDecoder::new(CodecId::new(CODEC_ID_STR), out_params); + // `{"sbr_downsampled": "true"}` selects the §4.6.18.4.3 + // downsampled SBR output mode: HE-AAC streams are emitted at the + // core sampling rate instead of the doubled SBR rate. + if let Some(v) = params.options.get("sbr_downsampled") { + dec.set_sbr_downsampled(matches!(v, "true" | "1")); + } + // `{"sbr_low_power": "true"}` selects the §4.6.18.8 low-power SBR + // tool (real-valued filterbanks; HE-AAC v2 PS streams are + // rejected in this mode). + if let Some(v) = params.options.get("sbr_low_power") { + dec.set_sbr_low_power(matches!(v, "true" | "1")); + } + Ok(Box::new(dec)) +} + +/// Packet-to-frame adaptor wrapping [`StreamDecoder`] in the framework +/// [`Decoder`] trait. +/// +/// State carried across packets: +/// +/// * `stream` — the persistent [`StreamDecoder`] whose per-element slots +/// thread the §4.6.11 overlap-add tail / §4.6.7 LTP history / §4.6.6 +/// predictor state across the frames of the stream. +/// * `pending` queues the [`AudioFrame`]s produced by the last +/// `send_packet` (one per decoded ADTS frame); `receive_frame` pops the +/// front. +/// * `eof` — set by [`Decoder::flush`]; once `pending` drains and `eof` +/// is set, `receive_frame` returns [`Error::Eof`]. +pub struct AacDecoder { + codec_id: CodecId, + output: CodecParameters, + stream: StreamDecoder, + loas: LoasDecoder, + /// The transport syntax detected from the first non-empty packet: + /// raw ADTS (`0xFFF` syncword) or LOAS `AudioSyncStream` (`0x2B7` + /// syncword). `None` until the first packet picks one; once set, every + /// later packet is routed the same way. + transport: Option, + pending: VecDeque, + eof: bool, + /// The caller-selected §4.6.18.4.3 downsampled SBR output mode, + /// kept so [`Decoder::reset`] re-applies it to the fresh backends. + sbr_downsampled: bool, + /// The caller-selected §4.6.18.8 low-power SBR mode, kept so + /// [`Decoder::reset`] re-applies it to the fresh backends. + sbr_low_power: bool, +} + +/// The carrier syntax an [`AacDecoder`] auto-detects on its first packet. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Transport { + /// Raw ADTS frames (`0xFFF` 12-bit syncword), routed through + /// [`StreamDecoder::decode_frame`]. + Adts, + /// LOAS `AudioSyncStream` (`0x2B7` 11-bit syncword), routed through + /// [`LoasDecoder::decode_all`]. + Loas, +} + +impl std::fmt::Debug for AacDecoder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AacDecoder") + .field("codec_id", &self.codec_id) + .field("transport", &self.transport) + .field("pending", &self.pending.len()) + .field("eof", &self.eof) + .finish() + } +} + +impl AacDecoder { + fn new(codec_id: CodecId, output: CodecParameters) -> Self { + Self { + codec_id, + output, + stream: StreamDecoder::new(), + loas: LoasDecoder::new(), + transport: None, + pending: VecDeque::new(), + eof: false, + sbr_downsampled: false, + sbr_low_power: false, + } + } + + /// Select the §4.6.18.4.3 downsampled SBR output mode on both + /// transport backends: HE-AAC (SBR-active) streams are synthesized + /// through the 32-channel QMF bank and emitted at the *core* + /// sampling rate (1024 samples per channel per block) instead of + /// the doubled SBR rate. Select before the first packet. Also + /// reachable at construction via the `sbr_downsampled` codec + /// option ([`make_decoder`]). + pub fn set_sbr_downsampled(&mut self, downsampled: bool) { + self.sbr_downsampled = downsampled; + self.stream.set_sbr_downsampled(downsampled); + self.loas.set_sbr_downsampled(downsampled); + } + + /// Select the §4.6.18.8 low-power SBR mode on both transport + /// backends (real-valued filterbanks + LP adjustment chain; + /// HE-AAC v2 PS streams are rejected in this mode). Select before + /// the first packet. Also reachable at construction via the + /// `sbr_low_power` codec option ([`make_decoder`]). + pub fn set_sbr_low_power(&mut self, low_power: bool) { + self.sbr_low_power = low_power; + self.stream.set_sbr_low_power(low_power); + self.loas.set_sbr_low_power(low_power); + } + + /// The parameter set this decoder advertises for its output stream. + /// Updated from each decoded ADTS frame header so a caller reading it + /// after the first packet sees the on-the-wire sample rate / channel + /// count rather than the at-construction hints. + pub fn output_params(&self) -> &CodecParameters { + &self.output + } + + /// Convert one [`DecodedFrame`]'s interleaved `i16` PCM to an + /// interleaved-S16 [`AudioFrame`] (single plane, little-endian). + fn decoded_to_audio(decoded: &DecodedFrame, pts: Option) -> AudioFrame { + let mut bytes = Vec::with_capacity(decoded.pcm.len() * 2); + for &s in &decoded.pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + AudioFrame { + // Per-channel sample count from the interleaved buffer: + // 1024 for the plain AAC path, 2048 for an SBR (HE-AAC) + // dual-rate frame (1024 again in the downsampled SBR + // mode). A fill-only frame (`channels == 0`) carries no + // samples. + samples: decoded.pcm.len().checked_div(decoded.channels).unwrap_or(0) as u32, + pts, + data: vec![bytes], + } + } + + /// Queue a decoded frame's PCM and refresh the advertised output + /// params; a fill-only frame (`channels == 0`) produces no audio. + fn queue_decoded(&mut self, decoded: &DecodedFrame, pts: Option) -> bool { + if decoded.channels > 0 { + self.output.sample_rate = Some(decoded.sample_rate); + self.output.channels = Some(decoded.channels as u16); + self.pending.push_back(Self::decoded_to_audio(decoded, pts)); + true + } else { + false + } + } + + /// Route an ADTS-framed packet (`data` already ID3-stripped) through + /// the [`StreamDecoder`], queuing one [`AudioFrame`] per ADTS frame. + fn send_adts(&mut self, data: &[u8], pts: Option) -> Result<()> { + let mut pos = 0usize; + let mut produced_any = false; + while pos + ADTS_HEADER_BYTES_NO_CRC <= data.len() { + let (header, payload_offset) = AdtsHeader::parse(&data[pos..]) + .map_err(|e| Error::other(format!("oxideav-aac: adts header: {e}")))?; + let frame_len = header.aac_frame_length as usize; + if frame_len < payload_offset || pos + frame_len > data.len() { + return Err(Error::other( + "oxideav-aac: ADTS frame length overruns packet", + )); + } + // decode_adts_frame re-parses the header and verifies the + // §8.1.1 error_check() CRC layer when protection is + // present (payload_offset only bounds the frame here). + let decoded = self + .stream + .decode_adts_frame(&data[pos..pos + frame_len]) + .map_err(|e| Error::other(format!("oxideav-aac: decode_adts_frame: {e}")))?; + produced_any |= self.queue_decoded(&decoded, pts); + pos += frame_len; + } + + if !produced_any && pos == 0 { + return Err(Error::other( + "oxideav-aac: packet held no complete ADTS frame", + )); + } + Ok(()) + } + + /// Route a LOAS `AudioSyncStream` packet (`data` already ID3-stripped) + /// through the [`LoasDecoder`], queuing one [`AudioFrame`] per + /// recovered access unit. A packet may carry one or several LOAS sync + /// frames; the persistent [`LoasDecoder`] threads the + /// `StreamMuxConfig` (and per-stream decode state) across packets. + fn send_loas(&mut self, data: &[u8], pts: Option) -> Result<()> { + let decoded_frames = self + .loas + .decode_all(data) + .map_err(|e| Error::other(format!("oxideav-aac: loas decode: {e}")))?; + let mut produced_any = false; + for decoded in &decoded_frames { + produced_any |= self.queue_decoded(decoded, pts); + } + if !produced_any && decoded_frames.is_empty() { + return Err(Error::other( + "oxideav-aac: packet held no complete LOAS sync frame", + )); + } + Ok(()) + } +} + +impl Decoder for AacDecoder { + fn codec_id(&self) -> &CodecId { + &self.codec_id + } + + fn send_packet(&mut self, packet: &Packet) -> Result<()> { + if self.eof { + return Err(Error::other("oxideav-aac: cannot send_packet after flush")); + } + + let data = skip_id3v2(&packet.data); + + // Pick the carrier from the first non-empty packet, then route + // every later packet the same way. + let transport = match self.transport { + Some(t) => t, + None => { + let Some(t) = detect_transport(data) else { + return Err(Error::other( + "oxideav-aac: packet has neither an ADTS nor a LOAS syncword", + )); + }; + self.transport = Some(t); + t + } + }; + + match transport { + Transport::Adts => self.send_adts(data, packet.pts), + Transport::Loas => self.send_loas(data, packet.pts), + } + } + + fn receive_frame(&mut self) -> Result { + if let Some(audio) = self.pending.pop_front() { + return Ok(Frame::Audio(audio)); + } + if self.eof { + return Err(Error::Eof); + } + Err(Error::NeedMore) + } + + fn flush(&mut self) -> Result<()> { + self.eof = true; + Ok(()) + } + + fn reset(&mut self) -> Result<()> { + // Drop every per-element overlap / LTP / predictor slot (for both + // carriers) so the next send_packet decodes from a clean state, + // and re-arm transport auto-detection. + self.stream = StreamDecoder::new(); + self.loas = LoasDecoder::new(); + self.stream.set_sbr_downsampled(self.sbr_downsampled); + self.loas.set_sbr_downsampled(self.sbr_downsampled); + self.stream.set_sbr_low_power(self.sbr_low_power); + self.loas.set_sbr_low_power(self.sbr_low_power); + self.transport = None; + self.pending.clear(); + self.eof = false; + Ok(()) + } +} + +/// Detect the AAC carrier syntax from the first bytes of a packet +/// (already ID3v2-stripped). +/// +/// * ADTS — 12-bit `0xFFF` syncword: `byte0 == 0xFF` and the top four +/// bits of `byte1` are set. +/// * LOAS `AudioSyncStream` — 11-bit `0x2B7` syncword: the first 11 bits +/// equal `0x2B7` (`byte0 == 0x56`, top three bits of `byte1` set). +/// +/// Returns `None` when neither syncword matches. +fn detect_transport(data: &[u8]) -> Option { + if data.len() < 2 { + return None; + } + if data[0] == 0xFF && (data[1] & 0xF0) == 0xF0 { + return Some(Transport::Adts); + } + // 0x2B7 = 0b010_1011_0111: byte0 = 0b0101_0110 = 0x56, byte1 top 3 = + // 0b111. Confirm via the 11-bit syncword constant. + let first11 = (u32::from(data[0]) << 3) | (u32::from(data[1]) >> 5); + if first11 == AUDIO_SYNC_STREAM_SYNCWORD { + return Some(Transport::Loas); + } + None +} + +/// Skip a leading ID3v2 tag (`"ID3"` + 6-byte header + syncsafe size + +/// optional footer) if present; otherwise return the input unchanged. +/// Mirrors [`crate::decode`]'s stream-level skip so a packet that carries +/// a leading tag (the streaming-mux convention) decodes cleanly. +fn skip_id3v2(data: &[u8]) -> &[u8] { + if data.len() < 10 || &data[..3] != b"ID3" { + return data; + } + let size = data[6..10] + .iter() + .fold(0usize, |acc, &b| (acc << 7) | usize::from(b & 0x7f)); + let footer = if data[5] & 0x10 != 0 { 10 } else { 0 }; + let total = 10 + size + footer; + if total >= data.len() { + data + } else { + &data[total..] + } +} + +/// Probe the [`ADTS syncword`](crate::adts::ADTS_SYNCWORD) on the first +/// packet to disambiguate the shared container tags. +/// +/// * Sync OK (and a parseable fixed header) → `1.0` (definitive ADTS AAC). +/// * Leading ID3v2 then sync OK → `1.0` (streaming-mux ADTS). +/// * Packet present but no ADTS sync → `0.2` (not us, but the same +/// tag also covers non-ADTS — raw `raw_data_block()` / LATM — carriage +/// we can still attempt, so don't refuse outright). +/// * No packet hint → `0.5`. +fn probe_aac(ctx: &ProbeContext) -> Confidence { + let Some(pkt) = ctx.packet else { + return 0.5; + }; + let pkt = skip_id3v2(pkt); + if pkt.len() < 2 { + return 0.2; + } + // 12-bit ADTS syncword 0xFFF: byte 0 == 0xFF and the top 4 bits of + // byte 1 are 1. `AdtsHeader::parse` confirms the rest of the fixed + // header is structurally valid before we commit to the definitive + // score. + if pkt[0] == 0xFF && (pkt[1] & 0xF0) == 0xF0 && AdtsHeader::parse(pkt).is_ok() { + return 1.0; + } + // 11-bit LOAS AudioSyncStream syncword 0x2B7. A bare syncword match + // is a strong-but-not-definitive AAC signal (the AudioMuxElement + // body is validated on the first decode), so score it just below the + // structurally-confirmed ADTS hit. + if detect_transport(pkt) == Some(Transport::Loas) { + return 0.9; + } + 0.2 +} + +/// Install the AAC decoder factory into `reg`. +/// +/// Claims the container tags an AAC elementary stream is routed under: +/// +/// * **MP4 object-type `0x40`** — the `esds` AudioObject descriptor OTI +/// for `Audio ISO/IEC 14496-3`. +/// * **WAVEFORMATEX `0x00FF`** (`WAVE_FORMAT_RAW_AAC1`) and **`0x1601`** +/// (`WAVE_FORMAT_MPEG_ADTS_AAC`) — the Win32 `mmreg.h` raw-AAC and +/// ADTS-AAC format tags used by AVI / WAVE carriage. +/// * **FourCCs `mp4a` / `aac `** and the **Matroska `A_AAC`** CodecID. +/// +/// The encoder factory is +/// [`crate::codec_encoder::make_encoder`] — the frame-in / +/// packet-out adaptor over the `encoder` module's PCM→ADTS +/// [`crate::encoder::StreamEncoder`]. +/// +/// The probe ([`probe_aac`]) scores the ADTS syncword so a genuine ADTS +/// stream out-ranks a non-ADTS claimant on any shared tag. +pub fn register_codecs(reg: &mut CodecRegistry) { + let info = CodecInfo::new(CodecId::new(CODEC_ID_STR)) + .capabilities( + CodecCapabilities::audio("aac") + .with_decode() + .with_encode() + .with_lossy(true), + ) + .decoder(make_decoder) + .encoder(crate::codec_encoder::make_encoder) + .probe(probe_aac) + .tags([ + CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC), + CodecTag::wave_format(WAVE_FORMAT_RAW_AAC1), + CodecTag::wave_format(WAVE_FORMAT_MPEG_ADTS_AAC), + CodecTag::fourcc(b"mp4a"), + CodecTag::fourcc(b"aac "), + CodecTag::matroska("A_AAC"), + ]); + reg.register(info); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decode::FRAME_LEN; + use oxideav_core::TimeBase; + + fn build_params(sample_rate: u32, channels: u16) -> CodecParameters { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(sample_rate); + p.channels = Some(channels); + p.sample_format = Some(SampleFormat::S16); + p + } + + /// Read a fixture's whole `input.aac` byte buffer, or `None` when the + /// workspace `docs/` tree is absent (standalone-crate CI checkouts). + fn fixture_bytes(name: &str) -> Option> { + let path = format!( + "{}/../../docs/audio/aac/fixtures/{name}/input.aac", + env!("CARGO_MANIFEST_DIR") + ); + if !std::path::Path::new(&path).exists() { + eprintln!("skip: staged ADTS fixture not present at {path}"); + return None; + } + Some(std::fs::read(&path).expect("read staged ADTS fixture")) + } + + /// Slice a raw-ADTS byte buffer into one packet per ADTS frame, the + /// way a demuxer would emit them on the wire. + fn split_into_packets(bytes: &[u8]) -> Vec { + let bytes = skip_id3v2(bytes); + let tb = TimeBase::new(1, 44_100); + let mut packets = Vec::new(); + let mut pos = 0usize; + let mut pts: i64 = 0; + while pos + ADTS_HEADER_BYTES_NO_CRC <= bytes.len() { + let Ok((header, _)) = AdtsHeader::parse(&bytes[pos..]) else { + break; + }; + let fl = header.aac_frame_length as usize; + if fl == 0 || pos + fl > bytes.len() { + break; + } + let mut pkt = Packet::new(0, tb, bytes[pos..pos + fl].to_vec()); + pkt.pts = Some(pts); + packets.push(pkt); + pts += FRAME_LEN as i64; + pos += fl; + } + packets + } + + /// Read a fixture's whole `input.` byte buffer, or `None` when + /// the workspace `docs/` tree is absent. + fn fixture_bytes_ext(name: &str, ext: &str) -> Option> { + let path = format!( + "{}/../../docs/audio/aac/fixtures/{name}/input.{ext}", + env!("CARGO_MANIFEST_DIR") + ); + if !std::path::Path::new(&path).exists() { + eprintln!("skip: staged fixture not present at {path}"); + return None; + } + Some(std::fs::read(&path).expect("read staged fixture")) + } + + #[test] + fn detect_transport_recognises_adts_and_loas() { + // ADTS: 0xFFF syncword. + assert_eq!(detect_transport(&[0xFF, 0xF1, 0x00]), Some(Transport::Adts)); + // LOAS AudioSyncStream: 0x2B7 in the first 11 bits → 0x56, top 3 + // bits of byte 1 set. + assert_eq!(detect_transport(&[0x56, 0xE0, 0x00]), Some(Transport::Loas)); + // Neither. + assert_eq!(detect_transport(&[0x00, 0x00]), None); + assert_eq!(detect_transport(&[0xFF]), None); + } + + #[test] + fn loas_packet_decodes_through_trait() { + let Some(buf) = fixture_bytes_ext("aac-latm-stream", "latm") else { + return; + }; + // Feed the whole LOAS buffer as one packet (a demuxer that hands + // the elementary stream in bulk). + let mut pkt = Packet::new(0, TimeBase::new(1, 44_100), buf.clone()); + pkt.pts = Some(0); + + let mut dec = make_decoder(&build_params(44_100, 2)).expect("decoder"); + dec.send_packet(&pkt).expect("send_packet (loas)"); + + let mut frames = 0usize; + let mut samples_total = 0usize; + while let Ok(Frame::Audio(a)) = dec.receive_frame() { + assert_eq!(a.samples as usize, FRAME_LEN); + // interleaved stereo → FRAME_LEN * 2 channels * 2 bytes. + assert_eq!(a.data[0].len(), FRAME_LEN * 2 * 2); + frames += 1; + samples_total += a.data[0].len() / 2; + } + assert!(frames > 0, "LOAS packet produced no frames"); + // 32 access units × 1024 × 2 channels. + assert_eq!(samples_total, 65_536); + } + + #[test] + fn loas_trait_matches_loas_decoder_pcm() { + let Some(buf) = fixture_bytes_ext("aac-latm-stream", "latm") else { + return; + }; + // Reference: bare LoasDecoder. + let mut reference = LoasDecoder::new(); + let ref_frames = reference.decode_all(&buf).expect("LoasDecoder"); + let mut ref_pcm: Vec = Vec::new(); + for f in &ref_frames { + ref_pcm.extend_from_slice(&f.pcm); + } + + // Trait path: one bulk packet. + let mut pkt = Packet::new(0, TimeBase::new(1, 44_100), buf); + pkt.pts = Some(0); + let mut dec = make_decoder(&build_params(44_100, 2)).expect("decoder"); + dec.send_packet(&pkt).expect("send_packet"); + let mut trait_pcm: Vec = Vec::new(); + while let Ok(Frame::Audio(a)) = dec.receive_frame() { + for c in a.data[0].chunks_exact(2) { + trait_pcm.push(i16::from_le_bytes([c[0], c[1]])); + } + } + assert_eq!(trait_pcm, ref_pcm, "LOAS trait diverged from LoasDecoder"); + } + + #[test] + fn probe_scores_loas_sync() { + // 0x2B7 syncword (0x56, top 3 bits of next byte set). + let pkt = [0x56u8, 0xE0, 0x00, 0x00]; + let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); + let ctx = ProbeContext::new(&tag).packet(&pkt); + assert!((probe_aac(&ctx) - 0.9).abs() < f32::EPSILON); + } + + #[test] + fn make_decoder_builds_and_reports_id() { + let dec = make_decoder(&build_params(44_100, 2)).expect("decoder builds"); + assert_eq!(dec.codec_id().as_str(), CODEC_ID_STR); + } + + #[test] + fn make_decoder_defaults_without_hints() { + let p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + let _ = make_decoder(&p).expect("default-params decoder builds"); + } + + #[test] + fn receive_without_packet_is_need_more() { + let mut dec = make_decoder(&build_params(44_100, 2)).unwrap(); + match dec.receive_frame() { + Err(Error::NeedMore) => {} + other => panic!("expected NeedMore, got {other:?}"), + } + } + + #[test] + fn mono_fixture_decodes_one_frame_per_packet() { + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let packets = split_into_packets(&buf); + assert!(!packets.is_empty(), "fixture yielded zero packets"); + + let mut dec = make_decoder(&build_params(8_000, 1)).expect("decoder"); + let mut frames = 0usize; + for pkt in &packets { + dec.send_packet(pkt).expect("send_packet"); + loop { + match dec.receive_frame() { + Ok(Frame::Audio(a)) => { + assert_eq!(a.samples as usize, FRAME_LEN); + assert_eq!(a.data.len(), 1, "interleaved single plane"); + // mono → FRAME_LEN samples * 1 channel * 2 bytes. + assert_eq!(a.data[0].len(), FRAME_LEN * 2); + assert_eq!(a.pts, pkt.pts); + frames += 1; + } + Ok(other) => panic!("expected Audio, got {other:?}"), + Err(Error::NeedMore) => break, + Err(e) => panic!("receive_frame: {e}"), + } + } + } + assert_eq!(frames, packets.len(), "one frame per packet"); + } + + #[test] + fn stereo_fixture_decodes_two_channel_planes() { + let Some(buf) = fixture_bytes("aac-lc-intensity-stereo") else { + return; + }; + let packets = split_into_packets(&buf); + let mut dec = make_decoder(&build_params(44_100, 2)).expect("decoder"); + dec.send_packet(&packets[0]).expect("send_packet 0"); + let Frame::Audio(a) = dec.receive_frame().expect("frame 0") else { + panic!("expected AudioFrame"); + }; + assert_eq!(a.samples as usize, FRAME_LEN); + // interleaved stereo → FRAME_LEN * 2 channels * 2 bytes. + assert_eq!(a.data[0].len(), FRAME_LEN * 2 * 2); + } + + #[test] + fn trait_decode_matches_stream_decoder_pcm() { + // The trait wrapper must produce byte-identical PCM to the + // StreamDecoder it adapts (same persistent state, same order). + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let mut reference = StreamDecoder::new(); + let ref_frames = reference.decode_all(&buf).expect("reference decode_all"); + let mut ref_pcm: Vec = Vec::new(); + for f in &ref_frames { + ref_pcm.extend_from_slice(&f.pcm); + } + + let packets = split_into_packets(&buf); + let mut dec = make_decoder(&build_params(8_000, 1)).expect("decoder"); + let mut trait_pcm: Vec = Vec::new(); + for pkt in &packets { + dec.send_packet(pkt).expect("send_packet"); + while let Ok(Frame::Audio(a)) = dec.receive_frame() { + for c in a.data[0].chunks_exact(2) { + trait_pcm.push(i16::from_le_bytes([c[0], c[1]])); + } + } + } + assert_eq!( + trait_pcm, ref_pcm, + "trait decode diverged from StreamDecoder" + ); + } + + #[test] + fn flush_then_receive_yields_eof_after_drain() { + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let packets = split_into_packets(&buf); + let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); + dec.send_packet(&packets[0]).unwrap(); + dec.flush().unwrap(); + let _ = dec.receive_frame().expect("pending frame drains"); + match dec.receive_frame() { + Err(Error::Eof) => {} + other => panic!("expected Eof, got {other:?}"), + } + } + + #[test] + fn send_after_flush_is_rejected() { + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let packets = split_into_packets(&buf); + let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); + dec.flush().unwrap(); + assert!(dec.send_packet(&packets[0]).is_err()); + } + + #[test] + fn reset_re_enables_send_and_restores_clean_state() { + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let packets = split_into_packets(&buf); + + // Decode the first frame fresh, capture its PCM. + let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); + dec.send_packet(&packets[0]).unwrap(); + let Frame::Audio(first_clean) = dec.receive_frame().unwrap() else { + panic!("audio"); + }; + + // Advance a few frames (building overlap state), flush, reset. + for pkt in packets.iter().take(4) { + dec.send_packet(pkt).unwrap(); + while let Ok(Frame::Audio(_)) = dec.receive_frame() {} + } + dec.flush().unwrap(); + dec.reset().unwrap(); + + // After reset the first frame decodes byte-identically again — + // proving the overlap / state was wiped. + dec.send_packet(&packets[0]).unwrap(); + let Frame::Audio(first_again) = dec.receive_frame().unwrap() else { + panic!("audio"); + }; + assert_eq!( + first_again.data, first_clean.data, + "reset did not restore the initial decode state" + ); + } + + #[test] + fn multi_frame_packet_emits_one_audio_frame_each() { + // A packet carrying two concatenated ADTS frames must yield two + // AudioFrames (the streaming case where a demuxer batches frames). + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let packets = split_into_packets(&buf); + assert!(packets.len() >= 2); + let mut joined = packets[0].data.clone(); + joined.extend_from_slice(&packets[1].data); + let mut pkt = Packet::new(0, TimeBase::new(1, 8_000), joined); + pkt.pts = Some(0); + + let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); + dec.send_packet(&pkt).unwrap(); + let mut n = 0usize; + while let Ok(Frame::Audio(_)) = dec.receive_frame() { + n += 1; + } + assert_eq!(n, 2, "two ADTS frames in one packet → two AudioFrames"); + } + + // ───────────────────── probe + registration ───────────────────── + + /// Pack a minimal, structurally-valid 7-byte ADTS fixed/variable + /// header (protection_absent, LC mono 44.1 kHz, `aac_frame_length` + /// covering just the header) MSB-first so the probe vector cannot + /// drift out of sync with `AdtsHeader::parse`. + fn synth_adts_header() -> [u8; 7] { + let mut bits: Vec = Vec::new(); + let mut push = |val: u32, n: u32| { + for i in (0..n).rev() { + bits.push(((val >> i) & 1) as u8); + } + }; + push(0xFFF, 12); // syncword + push(0, 1); // ID = MPEG-4 + push(0, 2); // layer + push(1, 1); // protection_absent + push(1, 2); // profile = LC (AOT 2) + push(4, 4); // sampling_frequency_index = 44100 + push(0, 1); // private_bit + push(1, 3); // channel_configuration = mono + push(0, 1); // original_copy + push(0, 1); // home + push(0, 1); // copyright_identification_bit + push(0, 1); // copyright_identification_start + push(7, 13); // aac_frame_length = 7 (header only) + push(0x7FF, 11); // adts_buffer_fullness = VBR sentinel + push(0, 2); // number_of_raw_data_blocks_in_frame - 1 + let mut out = [0u8; 7]; + for (i, chunk) in bits.chunks(8).enumerate() { + let mut b = 0u8; + for (j, &bit) in chunk.iter().enumerate() { + b |= bit << (7 - j); + } + out[i] = b; + } + out + } + + #[test] + fn probe_scores_adts_sync() { + let hdr = synth_adts_header(); + let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); + let ctx = ProbeContext::new(&tag).packet(&hdr); + // Confirm the test vector is a well-formed ADTS header first. + assert!(AdtsHeader::parse(&hdr).is_ok(), "test ADTS header invalid"); + assert!((probe_aac(&ctx) - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn probe_scores_low_for_non_adts() { + let pkt = [0x00u8, 0x00, 0x00, 0x00]; + let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); + let ctx = ProbeContext::new(&tag).packet(&pkt); + assert!(probe_aac(&ctx) < 0.5); + } + + #[test] + fn probe_default_without_packet() { + let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); + let ctx = ProbeContext::new(&tag); + assert!((probe_aac(&ctx) - 0.5).abs() < f32::EPSILON); + } + + #[test] + fn probe_uses_fixture_first_bytes() { + let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { + return; + }; + let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); + let ctx = ProbeContext::new(&tag).packet(&buf); + assert!((probe_aac(&ctx) - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn register_installs_decoder_factory() { + let mut reg = CodecRegistry::new(); + register_codecs(&mut reg); + assert!(reg.has_decoder(&CodecId::new(CODEC_ID_STR))); + let _ = reg + .first_decoder(&build_params(44_100, 2)) + .expect("registry-built decoder"); + } + + #[test] + fn register_claims_all_tags() { + let mut reg = CodecRegistry::new(); + register_codecs(&mut reg); + for tag in [ + CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC), + CodecTag::wave_format(WAVE_FORMAT_RAW_AAC1), + CodecTag::wave_format(WAVE_FORMAT_MPEG_ADTS_AAC), + CodecTag::fourcc(b"mp4a"), + CodecTag::fourcc(b"aac "), + CodecTag::matroska("A_AAC"), + ] { + let ctx = ProbeContext::new(&tag); + assert_eq!( + reg.resolve_tag_ref(&ctx).map(|c| c.as_str()), + Some(CODEC_ID_STR), + "tag {tag:?} did not resolve to aac", + ); + } + } + + /// The `sbr_downsampled` codec option: the HE-AAC v1 fixture + /// decodes at the core 22.05 kHz rate with 1024 samples per + /// channel per frame, and the mode survives `reset()`. + #[test] + fn sbr_downsampled_option_emits_core_rate() { + let Some(buf) = fixture_bytes("he-aac-v1-stereo-44100-32kbps-adts") else { + return; + }; + let packets = split_into_packets(&buf); + assert!(packets.len() > 2); + + let mut params = build_params(22_050, 2); + params.options.insert("sbr_downsampled", "true"); + let mut dec = make_decoder(¶ms).unwrap(); + + let run = |dec: &mut Box, pkts: &[Packet]| -> Vec { + let mut frames = Vec::new(); + for pkt in pkts { + dec.send_packet(pkt).unwrap(); + while let Ok(Frame::Audio(f)) = dec.receive_frame() { + frames.push(f); + } + } + frames + }; + let frames = run(&mut dec, &packets[..2]); + assert_eq!(frames.len(), 2); + for f in &frames { + assert_eq!(f.samples, 1024, "downsampled SBR frame length"); + assert_eq!(f.data[0].len(), 1024 * 2 * 2); + } + + // reset() keeps the selected mode. + dec.reset().unwrap(); + let frames2 = run(&mut dec, &packets[..2]); + assert_eq!(frames2.len(), 2); + assert_eq!(frames2[0].samples, 1024); + assert_eq!( + frames2[0].data[0], frames[0].data[0], + "post-reset decode differs" + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/codec_encoder.rs b/crates/vendor/oxideav-aac/src/codec_encoder.rs new file mode 100644 index 00000000..15ad7b28 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/codec_encoder.rs @@ -0,0 +1,333 @@ +//! `oxideav_core::Encoder` wiring for the AAC-LC encoder. +//! +//! Adapts [`crate::encoder::StreamEncoder`] (PCM → ADTS, see the +//! `encoder` module for the §4.5/§4.6 analysis chain) into the +//! framework's frame-in / packet-out [`oxideav_core::Encoder`] trait so +//! pipelines and muxers can drive AAC encoding via the registry. +//! +//! ## Trait-API adaptation +//! +//! * [`send_frame`](Encoder::send_frame) accepts [`Frame::Audio`] +//! frames carrying **interleaved little-endian `i16`** +//! (`SampleFormat::S16`, one data plane) at any per-frame sample +//! count. Samples buffer internally; every completed 1024-sample +//! hop becomes one ADTS frame. +//! * [`receive_packet`](Encoder::receive_packet) returns one +//! [`Packet`] per encoded ADTS frame ([`Error::NeedMore`] while the +//! buffer holds less than a hop). `pts` counts input samples +//! (time base `1/sample_rate`); the packet's `duration` is 1024. +//! Every AAC frame is independently decodable after the previous +//! frame's overlap, and each packet is flagged as a keyframe (the +//! ADTS stream is random-access at any frame boundary after a +//! 1-frame warmup). +//! * [`flush`](Encoder::flush) zero-pads the pending partial hop (if +//! any) into a final content frame and appends the encoder's +//! overlap-flush frame; subsequent `receive_packet` calls drain +//! those then return [`Error::Eof`]. +//! +//! ## Registration +//! +//! [`crate::codec_decoder::register_codecs`] installs +//! [`make_encoder`] alongside the decoder under codec id `"aac"`. +//! The historical direct factory path is also re-exported as +//! [`crate::encoder::make_encoder`]. + +use std::collections::VecDeque; + +use oxideav_core::{ + CodecId, CodecParameters, Encoder, Error, Frame, Packet, Result, SampleFormat, TimeBase, +}; + +use crate::codec_decoder::CODEC_ID_STR; +use crate::encoder::{EncoderConfig, StreamEncoder, FRAME_LEN}; + +/// Default target bitrate (bits/second) when `params.bit_rate` is +/// absent: 64 kbps per channel, the conventional "good quality" +/// AAC-LC operating point. +pub const DEFAULT_BITRATE_PER_CHANNEL: u32 = 64_000; + +/// Build a boxed AAC [`Encoder`] from `params`. +/// +/// Honoured parameters: +/// +/// * `sample_rate` (default 44 100) — must be an ISO/IEC 14496-3 +/// Table 1.18 rate with a §4.5.4 long-window band table +/// (96 000 … 8 000 Hz). +/// * `channels` (default 2) — any count with a Table 1.19 default +/// `channelConfiguration`: 1, 2, 3, 4, 5, 6 (5.1) or 8 (7.1); +/// input interleaved in the canonical [`crate::channel_map`] +/// order the decoder emits. 7 has no default configuration and is +/// rejected. +/// * `bit_rate` (default 64 kbps × channels) — the rate-loop target. +/// * `sample_format` — must be [`SampleFormat::S16`] (or unset). +/// +/// Anything unsupported surfaces as [`Error::Unsupported`] / +/// [`Error::invalid`] at construction time, per the registry's +/// init-time-fallback contract. +pub fn make_encoder(params: &CodecParameters) -> Result> { + let sample_rate = params.sample_rate.unwrap_or(44_100); + let channels = params.channels.unwrap_or(2); + if let Some(fmt) = params.sample_format { + if fmt != SampleFormat::S16 { + return Err(Error::unsupported( + "oxideav-aac encoder accepts interleaved S16 input only", + )); + } + } + if !(1..=6).contains(&channels) && channels != 8 { + return Err(Error::unsupported( + "oxideav-aac encoder supports the Table 1.19 default channel \ + configurations: 1-6 or 8 channels", + )); + } + let bitrate = params + .bit_rate + .map(|b| b.min(u64::from(u32::MAX)) as u32) + .unwrap_or(DEFAULT_BITRATE_PER_CHANNEL * u32::from(channels)); + let config = EncoderConfig { + sample_rate, + channels: channels as u8, + bitrate, + }; + let stream = StreamEncoder::new(config) + .map_err(|e| Error::invalid(format!("oxideav-aac encoder config: {e}")))?; + + let mut out_params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + out_params.sample_rate = Some(sample_rate); + out_params.channels = Some(channels); + out_params.sample_format = Some(SampleFormat::S16); + out_params.bit_rate = Some(u64::from(bitrate)); + + Ok(Box::new(AacEncoder { + codec_id: CodecId::new(CODEC_ID_STR), + out_params, + stream, + time_base: TimeBase::new(1, i64::from(sample_rate)), + pending_pcm: Vec::new(), + packets: VecDeque::new(), + samples_emitted: 0, + flushed: false, + })) +} + +/// Frame-to-packet adaptor wrapping [`StreamEncoder`] in the +/// framework [`Encoder`] trait. +struct AacEncoder { + codec_id: CodecId, + out_params: CodecParameters, + stream: StreamEncoder, + time_base: TimeBase, + /// Interleaved samples not yet forming a whole 1024-sample hop. + pending_pcm: Vec, + /// Encoded ADTS frames awaiting `receive_packet`. + packets: VecDeque, + /// Per-channel input samples consumed into emitted packets — + /// drives `pts`. + samples_emitted: i64, + flushed: bool, +} + +impl AacEncoder { + /// Encode every complete hop sitting in `pending_pcm`. + fn drain_hops(&mut self) -> Result<()> { + let ch = usize::from(self.out_params.channels.unwrap_or(1)).max(1); + let hop = FRAME_LEN * ch; + while self.pending_pcm.len() >= hop { + let chunk: Vec = self.pending_pcm.drain(..hop).collect(); + let bytes = self + .stream + .encode_frame(&chunk) + .map_err(|e| Error::invalid(format!("oxideav-aac encode: {e}")))?; + self.push_packet(bytes); + } + Ok(()) + } + + fn push_packet(&mut self, bytes: Vec) { + let pkt = Packet::new(0, self.time_base, bytes) + .with_pts(self.samples_emitted) + .with_duration(FRAME_LEN as i64) + .with_keyframe(true); + self.samples_emitted += FRAME_LEN as i64; + self.packets.push_back(pkt); + } +} + +impl Encoder for AacEncoder { + fn codec_id(&self) -> &CodecId { + &self.codec_id + } + + fn output_params(&self) -> &CodecParameters { + &self.out_params + } + + fn send_frame(&mut self, frame: &Frame) -> Result<()> { + if self.flushed { + return Err(Error::invalid("send_frame after flush")); + } + let audio = match frame { + Frame::Audio(a) => a, + _ => return Err(Error::invalid("oxideav-aac encoder accepts audio frames")), + }; + let plane = match audio.data.as_slice() { + [p] => p, + _ => { + return Err(Error::invalid( + "oxideav-aac encoder expects one interleaved S16 plane", + )) + } + }; + if plane.len() % 2 != 0 { + return Err(Error::invalid("odd byte count in S16 plane")); + } + self.pending_pcm.extend( + plane + .chunks_exact(2) + .map(|b| i16::from_le_bytes([b[0], b[1]])), + ); + self.drain_hops() + } + + fn receive_packet(&mut self) -> Result { + if let Some(pkt) = self.packets.pop_front() { + return Ok(pkt); + } + if self.flushed { + Err(Error::Eof) + } else { + Err(Error::NeedMore) + } + } + + fn flush(&mut self) -> Result<()> { + if self.flushed { + return Ok(()); + } + // Zero-pad any partial hop into a final content frame… + if !self.pending_pcm.is_empty() { + let chunk: Vec = std::mem::take(&mut self.pending_pcm); + let bytes = self + .stream + .encode_frame(&chunk) + .map_err(|e| Error::invalid(format!("oxideav-aac encode: {e}")))?; + self.push_packet(bytes); + } + // …then emit the overlap-flush frame. + let bytes = self + .stream + .finish() + .map_err(|e| Error::invalid(format!("oxideav-aac flush: {e}")))?; + self.push_packet(bytes); + self.flushed = true; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::AudioFrame; + + fn params(rate: u32, channels: u16, bitrate: Option) -> CodecParameters { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(rate); + p.channels = Some(channels); + p.sample_format = Some(SampleFormat::S16); + p.bit_rate = bitrate; + p + } + + fn tone_frame(samples: usize, channels: usize) -> Frame { + let mut bytes = Vec::with_capacity(samples * channels * 2); + for i in 0..samples { + let v = (8000.0 * (0.05 * i as f64).sin()) as i16; + for _ in 0..channels { + bytes.extend_from_slice(&v.to_le_bytes()); + } + } + Frame::Audio(AudioFrame { + samples: samples as u32, + pts: None, + data: vec![bytes], + }) + } + + #[test] + fn encoder_builds_and_reports_output_params() { + let enc = make_encoder(¶ms(44_100, 2, Some(128_000))).expect("builds"); + assert_eq!(enc.codec_id().as_str(), "aac"); + let out = enc.output_params(); + assert_eq!(out.sample_rate, Some(44_100)); + assert_eq!(out.channels, Some(2)); + assert_eq!(out.bit_rate, Some(128_000)); + } + + #[test] + fn encoder_rejects_unsupported_shapes() { + // 7 channels has no Table 1.19 default configuration; 6 + // (5.1) and 8 (7.1) do and build. + assert!(make_encoder(¶ms(44_100, 7, None)).is_err()); + assert!(make_encoder(¶ms(44_100, 9, None)).is_err()); + assert!(make_encoder(¶ms(44_100, 6, None)).is_ok()); + assert!(make_encoder(¶ms(44_100, 8, None)).is_ok()); + assert!(make_encoder(¶ms(44_055, 1, None)).is_err()); + let mut p = params(44_100, 2, None); + p.sample_format = Some(SampleFormat::F32); + assert!(make_encoder(&p).is_err()); + } + + #[test] + fn frames_in_packets_out_with_flush() { + let mut enc = make_encoder(¶ms(44_100, 1, Some(96_000))).unwrap(); + // 2.5 hops of input. + enc.send_frame(&tone_frame(2_560, 1)).unwrap(); + // Two whole hops → two packets. + let p0 = enc.receive_packet().unwrap(); + assert_eq!(p0.pts, Some(0)); + assert_eq!(p0.duration, Some(1024)); + assert!(p0.flags.keyframe); + assert!(p0.data.starts_with(&[0xFF])); + let p1 = enc.receive_packet().unwrap(); + assert_eq!(p1.pts, Some(1024)); + assert!(matches!(enc.receive_packet(), Err(Error::NeedMore))); + // Flush: the padded half hop + the overlap-flush frame. + enc.flush().unwrap(); + let p2 = enc.receive_packet().unwrap(); + assert_eq!(p2.pts, Some(2048)); + let p3 = enc.receive_packet().unwrap(); + assert_eq!(p3.pts, Some(3072)); + assert!(matches!(enc.receive_packet(), Err(Error::Eof))); + } + + #[test] + fn registry_round_trip_decodes_encoder_output() { + let mut enc = make_encoder(¶ms(44_100, 1, Some(128_000))).unwrap(); + let n = 4 * FRAME_LEN; + enc.send_frame(&tone_frame(n, 1)).unwrap(); + enc.flush().unwrap(); + let mut stream_bytes = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => stream_bytes.extend_from_slice(&p.data), + Err(Error::Eof) => break, + Err(e) => panic!("unexpected: {e}"), + } + } + + // Feed the whole ADTS stream to the registered decoder. + let mut dec = crate::codec_decoder::make_decoder(¶ms(44_100, 1, None)).unwrap(); + let pkt = Packet::new(0, TimeBase::new(1, 44_100), stream_bytes); + dec.send_packet(&pkt).unwrap(); + let mut decoded_samples = 0usize; + loop { + match dec.receive_frame() { + Ok(Frame::Audio(a)) => decoded_samples += a.samples as usize, + Ok(_) => panic!("non-audio frame"), + Err(_) => break, + } + } + // n/1024 content frames + 1 flush frame, 1024 samples each. + assert_eq!(decoded_samples, n + FRAME_LEN); + } +} diff --git a/crates/vendor/oxideav-aac/src/crc.rs b/crates/vendor/oxideav-aac/src/crc.rs new file mode 100644 index 00000000..813eeccc --- /dev/null +++ b/crates/vendor/oxideav-aac/src/crc.rs @@ -0,0 +1,449 @@ +//! Error-protection CRC generator — ISO/IEC 14496-3 §1.8.4.5. +//! +//! §1.8.4.5 defines a family of cyclic-redundancy-check codes used by +//! the MPEG-4 Audio error-protection (EP) tool and by the LATM +//! `StreamMuxConfig()` `crcCheckSum` field (§1.7.3.1, Table 1.42: +//! "This CRC uses the generation polynomial CRC8, as defined in +//! subclause 1.8.4.5 and covers the entire StreamMuxConfig() up to but +//! excluding the crcCheckPresent bit"). +//! +//! ## Generation polynomials (§1.8.4.5) +//! +//! Each `k`-bit CRC has a generator polynomial `G(x)` of degree `k`: +//! +//! | `k` | `G(x)` | +//! |------|-----------------------------------------------------------------| +//! | 4 | x⁴ + x³ + x² + 1 | +//! | 5 | x⁵ + x⁴ + x² + 1 | +//! | 6 | x⁶ + x⁵ + x⁴ + x² + x + 1 | +//! | 7 | x⁷ + x³ + x + 1 | +//! | 8 | x⁸ + x⁴ + x³ + x² + 1 | +//! | 9 | x⁹ + x⁴ + x³ + x² + x + 1 | +//! | 10 | x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1 | +//! | 11 | x¹¹ + x¹⁰ + x⁹ + x⁵ + x + 1 | +//! | 12 | x¹² + x¹¹ + x³ + x² + x + 1 | +//! | 13 | x¹³ + x¹² + x¹¹ + x⁸ + x⁷ + x⁴ + x² + 1 | +//! | 14 | x¹⁴ + x¹³ + x¹⁰ + x⁵ + x³ + x + 1 | +//! | 15 | x¹⁵ + x¹⁴ + x¹³ + x¹⁰ + x⁸ + x⁵ + x² + x + 1 | +//! | 16 | x¹⁶ + x¹⁵ + x² + 1 | +//! | 24 | x²⁴ + x²³ + x⁶ + x⁵ + x + 1 | +//! | 32 | x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1 | +//! +//! ## Encoding procedure (§1.8.4.5) +//! +//! With these polynomials the CRC encoding proceeds as follows. Let +//! `M(x)` be the information bits (highest order = first bit +//! transmitted) and `k` the number of CRC bits. Compute the remainder +//! `R(x)` satisfying +//! +//! ```text +//! M(x)·xᵏ = Q(x)·G(x) + R(x) +//! ``` +//! +//! i.e. `R(x)` is the degree-`(k−1)` remainder of the message shifted +//! left by `k` bits (`M(x)·xᵏ`) divided by `G(x)`, with a zero initial +//! register and no input reflection (MSB-first). The transmitted CRC +//! word is then +//! +//! ```text +//! W(x) = M(x)·xᵏ + R(x) +//! ``` +//! +//! with the normative final step: "The CRC bits are written in a +//! reversed manner, i. e. each bit is inverted." So the `k` remainder +//! bits are **bit-inverted** (one's complement) before transmission. +//! [`crc_bits`] returns the post-inversion value — the exact bits a +//! conforming bitstream carries in `crcCheckSum` — so a decoder +//! validates simply by recomputing over the protected region and +//! comparing for equality with the field it read off the wire. +//! +//! ## Scope +//! +//! This module is the §1.8.4.5 generator only. It does **not** apply +//! the §1.8.4.6 SRCPC convolutional FEC stage, nor does it implement +//! the ADTS (`adts_error_check()`) region selection, whose CRC is +//! cited by ISO/IEC 13818-7 to a different normative reference +//! (ISO/IEC 11172-3 §2.4.3.1) and is therefore not covered here. + +/// A CRC generation polynomial from ISO/IEC 14496-3 §1.8.4.5. +/// +/// Each variant fixes both the bit width `k` and the generator +/// polynomial `G(x)`. The polynomial is stored as the low `k` bits of +/// the generator (the implicit `xᵏ` leading term is dropped, as is +/// conventional for a shift-register CRC). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CrcPoly { + /// 1-bit CRC: x + 1 (§1.8.4.5, EP-tool class CRCs). + Crc1, + /// 2-bit CRC: x² + x + 1. + Crc2, + /// 3-bit CRC: x³ + x + 1. + Crc3, + /// 4-bit CRC: x⁴ + x³ + x² + 1. + Crc4, + /// 5-bit CRC: x⁵ + x⁴ + x² + 1. + Crc5, + /// 6-bit CRC: x⁶ + x⁵ + x⁴ + x² + x + 1. + Crc6, + /// 7-bit CRC: x⁷ + x³ + x + 1. + Crc7, + /// 8-bit CRC: x⁸ + x⁴ + x³ + x² + 1. Used by LATM + /// `StreamMuxConfig()` `crcCheckSum`. + Crc8, + /// 9-bit CRC: x⁹ + x⁴ + x³ + x² + x + 1. + Crc9, + /// 10-bit CRC: x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1. + Crc10, + /// 11-bit CRC: x¹¹ + x¹⁰ + x⁹ + x⁵ + x + 1. + Crc11, + /// 12-bit CRC: x¹² + x¹¹ + x³ + x² + x + 1. + Crc12, + /// 13-bit CRC: x¹³ + x¹² + x¹¹ + x⁸ + x⁷ + x⁴ + x² + 1. + Crc13, + /// 14-bit CRC: x¹⁴ + x¹³ + x¹⁰ + x⁵ + x³ + x + 1. + Crc14, + /// 15-bit CRC: x¹⁵ + x¹⁴ + x¹³ + x¹⁰ + x⁸ + x⁵ + x² + x + 1. + Crc15, + /// 16-bit CRC: x¹⁶ + x¹⁵ + x² + 1. + Crc16, + /// 24-bit CRC: x²⁴ + x²³ + x⁶ + x⁵ + x + 1. + Crc24, + /// 32-bit CRC: x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + + /// x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1. + Crc32, +} + +impl CrcPoly { + /// The CRC width `k` in bits. + pub const fn width(self) -> u32 { + match self { + CrcPoly::Crc1 => 1, + CrcPoly::Crc2 => 2, + CrcPoly::Crc3 => 3, + CrcPoly::Crc4 => 4, + CrcPoly::Crc5 => 5, + CrcPoly::Crc6 => 6, + CrcPoly::Crc7 => 7, + CrcPoly::Crc8 => 8, + CrcPoly::Crc9 => 9, + CrcPoly::Crc10 => 10, + CrcPoly::Crc11 => 11, + CrcPoly::Crc12 => 12, + CrcPoly::Crc13 => 13, + CrcPoly::Crc14 => 14, + CrcPoly::Crc15 => 15, + CrcPoly::Crc16 => 16, + CrcPoly::Crc24 => 24, + CrcPoly::Crc32 => 32, + } + } + + /// The generator polynomial `G(x)` as the low `k` bits (the + /// implicit leading `xᵏ` term is not stored). Bit `i` is set iff + /// the term `xⁱ` is present in `G(x)`. + /// + /// Derived directly from the §1.8.4.5 polynomial listing — e.g. + /// `Crc8` (x⁸ + x⁴ + x³ + x² + 1) drops the `x⁸` and keeps + /// `x⁴ + x³ + x² + x⁰`, i.e. bits 4, 3, 2, 0 ⇒ `0b0001_1101`. + pub const fn generator(self) -> u64 { + match self { + // x+1 → bit 0 + CrcPoly::Crc1 => bits(&[0]), + // x²+x+1 → bits 1,0 + CrcPoly::Crc2 => bits(&[1, 0]), + // x³+x+1 → bits 1,0 + CrcPoly::Crc3 => bits(&[1, 0]), + // x⁴+x³+x²+1 → bits 3,2,0 + CrcPoly::Crc4 => bits(&[3, 2, 0]), + // x⁵+x⁴+x²+1 → bits 4,2,0 + CrcPoly::Crc5 => bits(&[4, 2, 0]), + // x⁶+x⁵+x⁴+x²+x+1 → bits 5,4,2,1,0 + CrcPoly::Crc6 => bits(&[5, 4, 2, 1, 0]), + // x⁷+x³+x+1 → bits 3,1,0 + CrcPoly::Crc7 => bits(&[3, 1, 0]), + // x⁸+x⁴+x³+x²+1 → bits 4,3,2,0 + CrcPoly::Crc8 => bits(&[4, 3, 2, 0]), + // x⁹+x⁴+x³+x²+x+1 → bits 4,3,2,1,0 + CrcPoly::Crc9 => bits(&[4, 3, 2, 1, 0]), + // x¹⁰+x⁹+x⁵+x⁴+x+1 → bits 9,5,4,1,0 + CrcPoly::Crc10 => bits(&[9, 5, 4, 1, 0]), + // x¹¹+x¹⁰+x⁹+x⁵+x+1 → bits 10,9,5,1,0 + CrcPoly::Crc11 => bits(&[10, 9, 5, 1, 0]), + // x¹²+x¹¹+x³+x²+x+1 → bits 11,3,2,1,0 + CrcPoly::Crc12 => bits(&[11, 3, 2, 1, 0]), + // x¹³+x¹²+x¹¹+x⁸+x⁷+x⁴+x²+1 → bits 12,11,8,7,4,2,0 + CrcPoly::Crc13 => bits(&[12, 11, 8, 7, 4, 2, 0]), + // x¹⁴+x¹³+x¹⁰+x⁵+x³+x+1 → bits 13,10,5,3,1,0 + CrcPoly::Crc14 => bits(&[13, 10, 5, 3, 1, 0]), + // x¹⁵+x¹⁴+x¹³+x¹⁰+x⁸+x⁵+x²+x+1 → bits 14,13,10,8,5,2,1,0 + CrcPoly::Crc15 => bits(&[14, 13, 10, 8, 5, 2, 1, 0]), + // x¹⁶+x¹⁵+x²+1 → bits 15,2,0 + CrcPoly::Crc16 => bits(&[15, 2, 0]), + // x²⁴+x²³+x⁶+x⁵+x+1 → bits 23,6,5,1,0 + CrcPoly::Crc24 => bits(&[23, 6, 5, 1, 0]), + // x³²+x²⁶+x²³+x²²+x¹⁶+x¹²+x¹¹+x¹⁰+x⁸+x⁷+x⁵+x⁴+x²+x+1 + // → bits 26,23,22,16,12,11,10,8,7,5,4,2,1,0 + CrcPoly::Crc32 => bits(&[26, 23, 22, 16, 12, 11, 10, 8, 7, 5, 4, 2, 1, 0]), + } + } + + /// Mask of the low `k` bits: `(1 << k) - 1`. + const fn mask(self) -> u64 { + let k = self.width(); + if k >= 64 { + u64::MAX + } else { + (1u64 << k) - 1 + } + } +} + +/// Build a generator bitmask from a list of present term exponents +/// (each `< k`). Used by [`CrcPoly::generator`]. +const fn bits(exponents: &[u32]) -> u64 { + let mut acc = 0u64; + let mut i = 0; + while i < exponents.len() { + acc |= 1u64 << exponents[i]; + i += 1; + } + acc +} + +/// Compute the §1.8.4.5 CRC over `message_bits`, MSB-first. +/// +/// `message_bits` is the protected bit sequence `M(x)` in transmission +/// order: `message_bits[0]` is the highest-order coefficient (the +/// first bit transmitted). The returned value is the `k`-bit +/// `crcCheckSum` exactly as it appears on the wire — the degree-`(k−1)` +/// remainder of `M(x)·xᵏ ÷ G(x)` with the normative final one's +/// complement applied ("written in a reversed manner, i. e. each bit +/// is inverted"). Only the low `poly.width()` bits are significant. +pub fn crc_bits(poly: CrcPoly, message_bits: &[bool]) -> u64 { + let k = poly.width(); + let gen = poly.generator(); + let mask = poly.mask(); + let top = 1u64 << (k - 1); + + // Standard MSB-first shift register: zero init, no input + // reflection. Feeding the message bits and then `k` implicit zero + // bits (the `·xᵏ` shift) leaves the remainder R(x) in `reg`. + let mut reg: u64 = 0; + for &bit in message_bits { + let high = (reg & top) != 0; + reg = (reg << 1) & mask; + if high { + reg ^= gen; + } + if bit { + reg ^= 1; // fold the incoming message bit into x⁰ + } + } + // Flush k zero bits so the register holds M(x)·xᵏ mod G(x). + for _ in 0..k { + let high = (reg & top) != 0; + reg = (reg << 1) & mask; + if high { + reg ^= gen; + } + } + + // §1.8.4.5: "The CRC bits are written in a reversed manner, i. e. + // each bit is inverted." One's-complement the k remainder bits. + (!reg) & mask +} + +/// Convenience wrapper: compute the §1.8.4.5 CRC over a whole-byte +/// `message`, MSB-first within each byte. +/// +/// Equivalent to [`crc_bits`] fed `message.len() * 8` bits in +/// big-endian bit order. +pub fn crc_bytes(poly: CrcPoly, message: &[u8]) -> u64 { + let k = poly.width(); + let gen = poly.generator(); + let mask = poly.mask(); + let top = 1u64 << (k - 1); + + let mut reg: u64 = 0; + for &byte in message { + for i in (0..8).rev() { + let bit = (byte >> i) & 1 != 0; + let high = (reg & top) != 0; + reg = (reg << 1) & mask; + if high { + reg ^= gen; + } + if bit { + reg ^= 1; + } + } + } + for _ in 0..k { + let high = (reg & top) != 0; + reg = (reg << 1) & mask; + if high { + reg ^= gen; + } + } + (!reg) & mask +} + +/// Compute the LATM `StreamMuxConfig()` `crcCheckSum` (§1.7.3.1, +/// Table 1.42) over the protected bit region. +/// +/// Per Table 1.42 the CRC "uses the generation polynomial CRC8, as +/// defined in subclause 1.8.4.5 and covers the entire +/// StreamMuxConfig() up to but excluding the crcCheckPresent bit". +/// `config_bits` must therefore be exactly that prefix of the +/// `StreamMuxConfig()` bitstream (from `audioMuxVersion` through the +/// last bit before `crcCheckPresent`), in transmission (MSB-first) +/// order. The returned 8-bit value is the on-wire `crcCheckSum`; a +/// decoder validates by comparing it for equality against the field +/// it read. +pub fn stream_mux_config_crc(config_bits: &[bool]) -> u8 { + crc_bits(CrcPoly::Crc8, config_bits) as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Reference long-division CRC: compute the remainder of + /// `M(x)·xᵏ ÷ G(x)` over GF(2) directly, with the full `(k+1)`-bit + /// generator (leading `xᵏ` term included). Independent of the + /// shift-register implementation in `crc_bits`, so it cross-checks + /// the register arithmetic against the textbook polynomial-division + /// definition from §1.8.4.5. Returns the pre-inversion remainder. + fn reference_remainder(poly: CrcPoly, message_bits: &[bool]) -> u64 { + let k = poly.width(); + let full_gen = poly.generator() | (1u64 << k); // include xᵏ + // Build the dividend M(x)·xᵏ as a big sequence of bits. + let mut dividend: Vec = message_bits.to_vec(); + dividend.extend(std::iter::repeat(false).take(k as usize)); + + // Long division over GF(2), MSB-first, tracking a window of the + // most recent (k+1) bits implicitly via a running register. + let mut reg: u64 = 0; + let topbit = 1u64 << k; + for &bit in ÷nd { + reg = (reg << 1) | (bit as u64); + if reg & topbit != 0 { + reg ^= full_gen; + } + } + reg & ((1u64 << k) - 1) + } + + fn to_bits(bytes: &[u8]) -> Vec { + let mut v = Vec::with_capacity(bytes.len() * 8); + for &b in bytes { + for i in (0..8).rev() { + v.push((b >> i) & 1 != 0); + } + } + v + } + + #[test] + fn generator_masks_match_spec_exponents() { + // Spot-check the headline polynomials against §1.8.4.5. + assert_eq!(CrcPoly::Crc8.generator(), 0b0001_1101); // x⁴+x³+x²+1 + assert_eq!(CrcPoly::Crc16.generator(), (1 << 15) | (1 << 2) | 1); + assert_eq!(CrcPoly::Crc4.generator(), 0b1101); // x³+x²+1 + // Every generator must fit within its width and carry the x⁰ + // term (all listed polynomials have a constant 1). + for p in [ + CrcPoly::Crc4, + CrcPoly::Crc5, + CrcPoly::Crc6, + CrcPoly::Crc7, + CrcPoly::Crc8, + CrcPoly::Crc9, + CrcPoly::Crc10, + CrcPoly::Crc11, + CrcPoly::Crc12, + CrcPoly::Crc13, + CrcPoly::Crc14, + CrcPoly::Crc15, + CrcPoly::Crc16, + CrcPoly::Crc24, + CrcPoly::Crc32, + ] { + assert!(p.generator() & 1 == 1, "{p:?} missing x⁰ term"); + assert!(p.generator() <= p.mask(), "{p:?} generator exceeds width"); + } + } + + #[test] + fn crc_bits_matches_reference_long_division() { + let messages: [&[u8]; 5] = [ + &[], + &[0x00], + &[0xFF], + &[0x12, 0x34, 0x56, 0x78], + &[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03], + ]; + for p in [ + CrcPoly::Crc4, + CrcPoly::Crc8, + CrcPoly::Crc12, + CrcPoly::Crc16, + CrcPoly::Crc24, + CrcPoly::Crc32, + ] { + let mask = p.mask(); + for m in messages { + let bits = to_bits(m); + let got = crc_bits(p, &bits); + let expect = (!reference_remainder(p, &bits)) & mask; + assert_eq!(got, expect, "poly {p:?} message {m:x?}"); + } + } + } + + #[test] + fn crc_bytes_agrees_with_crc_bits() { + let m: &[u8] = &[0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0xFF]; + for p in [CrcPoly::Crc8, CrcPoly::Crc16, CrcPoly::Crc32] { + assert_eq!(crc_bytes(p, m), crc_bits(p, &to_bits(m))); + } + } + + #[test] + fn inversion_is_present() { + // The spec mandates the output bits be inverted. For an empty + // message the pre-inversion remainder is 0, so the on-wire CRC + // must be all-ones within the width. + for p in [CrcPoly::Crc4, CrcPoly::Crc8, CrcPoly::Crc16] { + assert_eq!(crc_bits(p, &[]), p.mask()); + } + } + + #[test] + fn appending_crc_makes_codeword_divisible_modulo_inversion() { + // A defining property: M(x)·xᵏ + R(x) is divisible by G(x). + // We store the *inverted* R(x), so re-derive R(x) and verify + // the codeword M·xᵏ + R divides cleanly. + let m: &[u8] = &[0x53, 0x91, 0x2C]; + for p in [CrcPoly::Crc8, CrcPoly::Crc16] { + let mut bits = to_bits(m); + let on_wire = crc_bits(p, &bits); + let r = (!on_wire) & p.mask(); // undo inversion → true R(x) + // Append the k remainder bits (MSB-first) to the message. + for i in (0..p.width()).rev() { + bits.push((r >> i) & 1 != 0); + } + // The remainder of the full codeword ÷ G(x) must be zero. + assert_eq!(reference_remainder(p, &bits), 0, "poly {p:?}"); + } + } + + #[test] + fn stream_mux_config_crc_is_crc8() { + let bits = to_bits(&[0x00, 0x10, 0x07, 0x00]); + assert_eq!( + stream_mux_config_crc(&bits) as u64, + crc_bits(CrcPoly::Crc8, &bits) + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/decode.rs b/crates/vendor/oxideav-aac/src/decode.rs new file mode 100644 index 00000000..8507f13a --- /dev/null +++ b/crates/vendor/oxideav-aac/src/decode.rs @@ -0,0 +1,1305 @@ +//! Stream-level ADTS decode driver — raw_data_block walk to interleaved +//! 16-bit PCM. +//! +//! [`crate::element_decode::ElementDecoder`] decodes *one* channel +//! element per call and carries that element's §4.6.11 overlap-add tail +//! across frames. This module is the layer above it: it walks the +//! §4.4.2.1 `raw_data_block()` of one ADTS frame +//! ([`crate::raw_data_block::Walker`]), dispatches each `id_syn_ele` +//! onto a per-element-slot [`ElementDecoder`] (keyed by `(syntactic +//! element id, element_instance_tag)` so each element's filterbank state +//! is independent), composes the channel-element bodies via +//! [`crate::ics_body`] / [`crate::spectral_data`], and renders the +//! frame's per-channel time signals to the element-order interleaved +//! 16-bit PCM layout via [`crate::pcm`]. +//! +//! Scope: AAC-LC (and the other General-Audio object types the +//! per-tool chain covers) carried in ADTS — including +//! multi-`raw_data_block` frames (each block renders one consecutive +//! 1024-sample hop) and the `error_check()` CRC layer (verified by +//! [`StreamDecoder::decode_adts_frame`] via [`crate::adts_crc`]) — +//! with the channel elements the staged-fixture encoders emit +//! (SCE / LFE / CPE, plus the consumed-and-ignored FIL / DSE / +//! PCE). A `coupling_channel_element()` (CCE) is parsed via +//! [`crate::cce::CouplingChannelElement`] **and applied**: the walk is +//! two-pass — every channel element of a block is parsed first, each +//! CCE's embedded `single_channel_element()` is decoded through its +//! per-instance-tag [`CceDecoder`] slot, and the §4.6.8.3.3 +//! `decode_coupling_channel()` target walk then injects the scaled +//! spectra (or, for an independently switched CCE, the time signal) +//! into the addressed SCE / CPE channels at the signalled `cc_domain` +//! stage. The CCE contributes no output channel of its own. SBR / PS +//! up-sampling ride the FIL extension walk (§4.6.18 back-end). +//! +//! ## Provenance +//! +//! The §4.4.2.1 `raw_data_block()` walk, the §4.4.2.3 `channel_pair_ +//! element()` `common_window` / `ms_mask_present` header, and the +//! §4.6.11 PCM output contract are from ISO/IEC 14496-3 / 13818-7 staged +//! under `docs/audio/aac/`. No part of the byte ordering or the element +//! dispatch comes from any external decoder. + +use std::collections::HashMap; + +use oxideav_core::bits::BitReader; + +use crate::adts::AdtsHeader; +use crate::asc::AacResilienceFlags; +use crate::cce::CouplingChannelElement; +use crate::channel_map::PceElementKind; +use crate::element_decode::{ + CceDecoder, ChannelInput, CouplingApply, CpeJointStereo, DecodedCce, ElementDecoder, +}; +use crate::extension_payload::{ExtensionPayload, ExtensionPayloadOrSbr}; +use crate::ics_body::IcsBody; +use crate::ics_info::IcsInfo; +use crate::ms_stereo::MsMaskPresent; +use crate::pce::Pce; +use crate::pcm::interleave_s16; +use crate::raw_data_block::{Element, IdSynEle, Walker}; +use crate::sbr_decoder::SbrDecoder; +use crate::sbr_extension::SbrExtensionData; +use crate::sbr_header::SbrHeader; +use crate::spectral_data::SpectralData; +use crate::swb_offset::FrameFamily; +use crate::{Error, Result}; + +/// Map a channel element's [`IdSynEle`] to its §8.5.2.2 PCE reference +/// kind. `None` for elements a PCE never addresses as an output +/// channel (CCE contributes no output channel here). +fn pce_kind(kind: IdSynEle) -> Option { + match kind { + IdSynEle::Sce => Some(PceElementKind::Sce), + IdSynEle::Cpe => Some(PceElementKind::Cpe), + IdSynEle::Lfe => Some(PceElementKind::Lfe), + _ => None, + } +} + +/// The §4.6.11 per-frame sample count for the default 1024-line +/// transform family. The other §4.5.1.1 families emit 960 / 512 / +/// 480 samples per frame per channel +/// ([`crate::swb_offset::FrameFamily::frame_len`]). +pub const FRAME_LEN: usize = 1024; + +/// One decoded ADTS frame: the interleaved 16-bit PCM plus the geometry +/// needed to interpret it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodedFrame { + /// Interleaved 16-bit PCM, `channels` samples per time index. For a + /// default `channelConfiguration` (Table 1.19, values 1–6) the + /// channels are in the canonical [`crate::channel_map`] output order + /// (e.g. 5.1 is `L, R, C, LFE, Ls, Rs`); for the unmapped configs + /// (`0` PCE-defined, `7`) they stay in `raw_data_block` element order + /// (an SCE/LFE contributes one channel, a CPE two). Length is + /// `FRAME_LEN * channels` for the plain AAC path, or + /// `2 * FRAME_LEN * channels` once the stream is SBR-active + /// (HE-AAC dual-rate output; `FRAME_LEN * channels` again when + /// the §4.6.18.4.3 downsampled SBR mode is selected). + pub pcm: Vec, + /// Number of interleaved channels this frame produced. + pub channels: usize, + /// The frame's sampling rate in Hz: the ADTS-signalled core rate, + /// doubled once the stream is SBR-active (kept at the core rate + /// in the §4.6.18.4.3 downsampled SBR mode). + pub sample_rate: u32, +} + +/// Stateful whole-stream ADTS decoder. +/// +/// Holds one [`ElementDecoder`] per `(element-id, instance-tag)` slot so +/// every channel element's §4.6.11 overlap-add tail, §4.6.7 LTP history, +/// and §4.6.6 predictor state persist across the frames of the stream. +/// Construct one [`StreamDecoder`] per stream and feed it ADTS frames in +/// order via [`Self::decode_frame`], or hand it the whole byte buffer +/// via [`Self::decode_all`]. +#[derive(Debug, Default)] +pub struct StreamDecoder { + decoders: HashMap<(u8, u8), ElementDecoder>, + /// One §4.6.8.3.3 CCE decoder per coupling-element instance tag + /// (its independently-switched filterbank overlap and PNS state + /// persist across frames). + cce_decoders: HashMap, + /// One §4.6.18 SBR back-end per channel-element slot (HE-AAC). + sbr: HashMap<(u8, u8), SbrDecoder>, + /// The threaded previous `sbr_header()` per slot (the + /// `bs_header_flag == 0` reuse path). + sbr_prev_header: HashMap<(u8, u8), SbrHeader>, + /// Latched once any frame carries SBR data: from then on every + /// frame is emitted at the SBR output rate (doubled, or the core + /// rate in downsampled mode) — SBR-less frames go through the + /// §4.6.18.5 pure-upsampling path so the output rate never flaps. + sbr_active: bool, + /// §4.6.18.4.3 downsampled SBR output mode: SBR frames are + /// synthesized through the 32-channel bank and emitted at the + /// *core* rate (1024 samples per channel per block). Installed on + /// every SBR back-end this decoder creates + /// ([`Self::set_sbr_downsampled`]). + sbr_downsampled: bool, + /// §4.6.18.8 low-power SBR mode: real-valued filterbanks and the + /// LP adjustment chain on every SBR back-end this decoder creates + /// ([`Self::set_sbr_low_power`]). + sbr_low_power: bool, + /// The active `program_config_element()` for + /// `channelConfiguration == 0` streams — captured from an in-band + /// PCE (§8.5.2.2: it takes effect at the block carrying it and + /// persists) or installed by [`Self::set_program_config`] when the + /// PCE rides inline in an out-of-band `AudioSpecificConfig`. + program_config: Option, + /// The §4.5.1.1 frame-length family every block of this stream + /// decodes under. ADTS cannot signal anything but the 1024-line + /// family (the default); a LATM / raw caller with an + /// `AudioSpecificConfig` installs the ASC-resolved family via + /// [`Self::set_frame_family`] before the first block. + family: FrameFamily, +} + +impl StreamDecoder { + /// A fresh stream decoder with no element state. + #[must_use] + pub fn new() -> Self { + StreamDecoder::default() + } + + /// Install the program configuration of a + /// `channelConfiguration == 0` stream whose + /// `program_config_element()` rides *outside* the AAC payload — + /// inline in the `AudioSpecificConfig` (the MP4 / LATM case, + /// [`crate::asc::GaSpecificConfig::pce`]) or an `adif_header()`. + /// An in-band PCE inside a later `raw_data_block()` replaces it + /// (§8.5.2.2 persistence). The active PCE drives the §8.5.2.2 + /// element→speaker canonical output reorder; without one, a + /// config-0 stream is emitted in bitstream element order. + pub fn set_program_config(&mut self, pce: Pce) { + self.program_config = Some(pce); + } + + /// Select the §4.6.18.4.3 downsampled SBR output mode: every SBR + /// back-end runs the 32-channel synthesis bank, so an SBR-active + /// stream is emitted at the *core* sampling rate (1024 samples per + /// channel per block) instead of the doubled `fs_sbr` rate. The + /// reconstructed SBR bands below the core Nyquist are kept; the + /// range above it is discarded by construction. An explicitly + /// signalled `AudioSpecificConfig` whose `extensionSamplingFrequency` + /// equals the core rate is the in-band request for this mode + /// (§4.6.18.2.6, `FsSBR` definition). + /// + /// Select the mode before decoding: back-ends already created for + /// earlier frames keep their rate (the QMF history is + /// rate-specific). + pub fn set_sbr_downsampled(&mut self, downsampled: bool) { + self.sbr_downsampled = downsampled; + } + + /// Install the §4.5.1.1 frame-length family (from + /// `GASpecificConfig.frameLengthFlag` + the AOT) for every later + /// block. Affects the SWB tables, transform lengths and the + /// per-frame PCM sample count (1024 / 960 / 512 / 480). ADTS + /// cannot signal anything but the default 1024-line family; a + /// LATM / raw caller with an `AudioSpecificConfig` selects the + /// ASC-resolved family before the first block (the per-element + /// state is keyed to the family at slot creation). + pub fn set_frame_family(&mut self, family: FrameFamily) { + self.family = family; + } + + /// The active §4.5.1.1 frame-length family. + pub fn frame_family(&self) -> FrameFamily { + self.family + } + + /// Select the §4.6.18.8 low-power SBR mode: every SBR back-end + /// runs the real-valued filterbanks with the LP adjustment chain + /// (×2 energy estimation, aliasing detection/reduction, modified + /// sinusoid injection, no gain smoothing). Composable with + /// [`Self::set_sbr_downsampled`]. An HE-AAC v2 (PS) stream is + /// rejected in this mode ([`crate::Error::SbrLowPowerPs`]) — the + /// subpart-8 tool needs the complex QMF domain. Select before + /// decoding. + pub fn set_sbr_low_power(&mut self, low_power: bool) { + self.sbr_low_power = low_power; + } + + /// Decode one ADTS frame's `raw_data_block()` payload to interleaved + /// 16-bit PCM. + /// + /// `header` is the parsed [`AdtsHeader`]; `payload` is the + /// `raw_data_block()` bytes (the frame body *after* the + /// fixed/variable header and the optional CRC — i.e. starting at the + /// header's `payload_offset`). The channel elements update this + /// decoder's per-slot state, so frames must be fed in stream order. + /// + /// A frame that yields no channel element (e.g. fill-only) returns a + /// [`DecodedFrame`] with `channels == 0` and an empty `pcm`. + pub fn decode_frame(&mut self, header: &AdtsHeader, payload: &[u8]) -> Result { + self.decode_raw_data_block( + header.audio_object_type(), + header.sampling_frequency_index, + header.sample_rate(), + header.channel_configuration, + header.number_of_raw_data_blocks_in_frame, + payload, + ) + } + + /// Decode one `raw_data_block()` payload to interleaved 16-bit PCM, + /// driven by an explicit `(audioObjectType, samplingFrequencyIndex, + /// sampleRate)` configuration rather than an ADTS header. + /// + /// This is the transport-independent core that [`Self::decode_frame`] + /// (ADTS) and the LATM/LOAS driver + /// ([`crate::latm::LoasDecoder`]) both call: each recovers the AAC + /// configuration from its own framing (the ADTS fixed header, or the + /// LATM `AudioSpecificConfig`) and hands the same §4.4.2.1 + /// `raw_data_block()` bytes here. `aot` is the §1.6.2.1 + /// `audioObjectType` (already escaped past the ADTS `profile + 1` + /// adjustment), `fs_index` is the Table 1.18 + /// `samplingFrequencyIndex`, `sample_rate` is the resolved rate the + /// returned [`DecodedFrame`] reports, `channel_configuration` is the + /// Table 1.19 default-layout selector that drives the §1.6.3.5 + /// element→speaker output reorder (see [`crate::channel_map`]), and + /// `num_raw_data_blocks` is the resolved block count `N` (ADTS carries + /// `N - 1`; LATM carries one block per payload, i.e. `N == 1`). + pub fn decode_raw_data_block( + &mut self, + aot: u8, + fs_index: u8, + sample_rate: u32, + channel_configuration: u8, + num_raw_data_blocks: u8, + payload: &[u8], + ) -> Result { + let fs = fs_index; + let family = self.family; + let mut reader = BitReader::new(payload); + + // Per channel-element outputs in element order: the decoded + // core time signals plus any SBR extension payload that + // followed the element in a FIL. + struct ElementOut { + key: (u8, u8), + kind: IdSynEle, + channels: Vec>, + sbr: Option>, + } + // A channel element parsed off the bitstream but not yet + // decoded. Decoding is deferred until the whole block is + // walked so §4.6.8.3.3 coupling channel elements — which may + // appear before or after the SCE / CPE targets they address — + // can contribute at the right stage of every target's chain. + struct ParsedSce { + body: IcsBody, + ics: IcsInfo, + spectral: SpectralData, + } + enum ParsedChannel { + Single(Box), + Pair(Box), + } + struct PendingElement { + key: (u8, u8), + kind: IdSynEle, + block: u8, + parsed: ParsedChannel, + sbr: Option>, + } + let mut pending: Vec = Vec::new(); + let mut cces: Vec<(u8, CouplingChannelElement)> = Vec::new(); + let fs_sbr = sample_rate.saturating_mul(2); + + // `num_raw_data_blocks` is the resolved count `N`. The walker + // returns `None` when the payload is exhausted before an explicit + // END (real-world encoders pad the frame but do not always + // round-trip a trailing END marker after the last element); treat + // that as end-of-block, the same as an `Element::End`. + 'blocks: for block in 0..num_raw_data_blocks { + while let Some(elem) = Walker::new(&mut reader).next_element_keep_fill()? { + match elem { + Element::ChannelElement { + kind: kind @ (IdSynEle::Sce | IdSynEle::Lfe), + element_instance_tag, + } => { + let body = IcsBody::parse_family(&mut reader, family, aot, fs, false)?; + let ics = body.ics_info.clone().ok_or(Error::ElementDecodeInvalid)?; + let spectral = + SpectralData::parse(&mut reader, &ics, &body.section_data, fs)?; + pending.push(PendingElement { + key: (kind_id(kind), element_instance_tag), + kind, + block, + parsed: ParsedChannel::Single(Box::new(ParsedSce { + body, + ics, + spectral, + })), + sbr: None, + }); + } + Element::ChannelElement { + kind: IdSynEle::Cpe, + element_instance_tag, + } => { + let parsed = parse_cpe_family(&mut reader, family, aot, fs)?; + pending.push(PendingElement { + key: (kind_id(IdSynEle::Cpe), element_instance_tag), + kind: IdSynEle::Cpe, + block, + parsed: ParsedChannel::Pair(Box::new(parsed)), + sbr: None, + }); + } + Element::ChannelElement { + kind: IdSynEle::Cce, + element_instance_tag, + } => { + // §4.6.8.3 / Table 4.8: parse the whole coupling + // channel element (header + embedded + // single_channel_element + gain lists). Its + // embedded spectrum is decoded once per block + // below and coupled onto the addressed SCE / CPE + // targets per §4.6.8.3.3. + let cce = CouplingChannelElement::parse_after_tag_family( + &mut reader, + family, + element_instance_tag, + aot, + fs, + )?; + cces.push((block, cce)); + } + Element::ChannelElement { kind, .. } => { + // Any other channel-element id has no decode path. + return Err(unsupported_element(kind)); + } + Element::Fill { payload_bytes } => { + // The FIL body was left unconsumed: walk the + // Table 4.51 extension_payload() chain, routing + // any SBR payload onto the preceding channel + // element (§4.4.2.7: an SBR FIL directly follows + // the SCE/CPE it extends). + let target = pending + .last() + .filter(|el| matches!(el.kind, IdSynEle::Sce | IdSynEle::Cpe)) + .map(|el| (el.kind, el.key)); + if let Some(ext) = + self.consume_fill(&mut reader, payload, payload_bytes, fs_sbr, target)? + { + if let Some(el) = pending.last_mut() { + el.sbr = Some(ext); + } + } + } + Element::Data { .. } => {} + Element::ProgramConfig(pce) => { + // §8.5.2.2: the configuration takes effect at + // the raw_data_block() containing the PCE and + // persists until a new PCE arrives. + self.program_config = Some(pce); + } + Element::End => continue 'blocks, + } + } + } + + // §4.6.8.3.3 — decode each CCE's embedded + // single_channel_element() into its cc_spectrum (and, for an + // independently switched CCE, its time signal), through the + // per-instance-tag persistent CCE decoder slot. + let mut decoded_cces: Vec = Vec::with_capacity(cces.len()); + for (_, cce) in &cces { + let dec = self + .cce_decoders + .entry(cce.element_instance_tag) + .or_insert_with(|| CceDecoder::new_family(family)); + decoded_cces.push(dec.decode(cce, aot, fs)?); + } + + // Decode the pending channel elements in element order, with + // each channel's coupling contributions injected at the + // §4.6.8.3.3 cc_domain stage. The elements stay tagged with + // their raw_data_block index: a multi-RDB ADTS frame carries N + // *consecutive* 1024-sample blocks of the same program, so + // each block renders its own channel set and the per-block PCM + // is concatenated in time below. + let mut elements: Vec<(u8, ElementOut)> = Vec::new(); + for pe in pending { + let channels = match &pe.parsed { + ParsedChannel::Single(sce) => { + let coupling = + coupling_for(&cces, &decoded_cces, pe.block, pe.kind, pe.key.1, 0); + let ch = ChannelInput { + body: &sce.body, + ics_info: &sce.ics, + spectral: &sce.spectral, + }; + let dec = self + .decoders + .entry(pe.key) + .or_insert_with(|| ElementDecoder::new_family(family)); + vec![dec.decode_sce_coupled(&ch, aot, fs, &coupling)?] + } + ParsedChannel::Pair(cpe) => { + let left_coupling = + coupling_for(&cces, &decoded_cces, pe.block, pe.kind, pe.key.1, 0); + let right_coupling = + coupling_for(&cces, &decoded_cces, pe.block, pe.kind, pe.key.1, 1); + let (left, right, joint) = cpe.channel_inputs(); + let dec = self + .decoders + .entry(pe.key) + .or_insert_with(|| ElementDecoder::new_family(family)); + let (l, r) = dec.decode_cpe_coupled( + &left, + &right, + joint, + aot, + fs, + &left_coupling, + &right_coupling, + )?; + vec![l, r] + } + }; + elements.push(( + pe.block, + ElementOut { + key: pe.key, + kind: pe.kind, + channels, + sbr: pe.sbr, + }, + )); + } + + // HE-AAC: once any frame carries SBR data the stream is emitted + // at the doubled rate; frames without SBR go through the pure + // upsampling path so the rate never flaps. + if elements.iter().any(|(_, e)| e.sbr.is_some()) { + self.sbr_active = true; + } + let out_rate = if self.sbr_active && !self.sbr_downsampled { + fs_sbr + } else { + sample_rate + }; + + // Render block by block; each block contributes one hop of + // interleaved PCM (all blocks of a frame must agree on the + // channel count). + let mut pcm: Vec = Vec::new(); + let mut frame_channels: Option = None; + for block in 0..num_raw_data_blocks { + let mut channels: Vec> = Vec::new(); + // Per decoded element: (kind, instance tag, contributed + // channel count) — the descriptor list the §8.5.2.2 PCE + // reorder keys on for `channelConfiguration == 0`. + let mut element_desc: Vec<(PceElementKind, u8, usize)> = Vec::new(); + for (_, el) in elements.iter().filter(|(b, _)| *b == block) { + if self.sbr_active { + let n_ch = el.channels.len(); + let dec = match self.sbr.entry(el.key) { + std::collections::hash_map::Entry::Occupied(e) => e.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => { + let mut d = SbrDecoder::new(fs_sbr, n_ch)?; + d.set_downsampled(self.sbr_downsampled)?; + d.set_low_power(self.sbr_low_power)?; + v.insert(d) + } + }; + let core: Vec<&[f64]> = el.channels.iter().map(Vec::as_slice).collect(); + let up = match &el.sbr { + Some(ext) => dec.process_frame(ext, &core)?, + None => dec.upsample_frame(&core)?, + }; + if let Some(kind) = pce_kind(el.kind) { + element_desc.push((kind, el.key.1, up.len())); + } + channels.extend(up); + } else { + if let Some(kind) = pce_kind(el.kind) { + element_desc.push((kind, el.key.1, el.channels.len())); + } + channels.extend(el.channels.iter().cloned()); + } + } + + // §1.6.3.5 / Table 1.19: a default `channelConfiguration` + // (1–7) fixes which loudspeaker each decoded element feeds. + // Reorder the element-order channel buffers into the + // canonical interleaved layout (a no-op for mono/stereo). A + // `channelConfiguration == 0` block is reordered by the + // active §8.5.2.2 PCE instead, when one is installed and it + // maps onto canonical positions; otherwise element order is + // kept. + let channels = + if channel_configuration == 0 { + match self.program_config.as_ref().and_then(|pce| { + crate::channel_map::pce_reorder_permutation(pce, &element_desc) + }) { + Some(perm) => crate::channel_map::apply_permutation(&perm, channels), + None => channels, + } + } else { + crate::channel_map::reorder_channels(channel_configuration, channels) + }; + + match frame_channels { + None => frame_channels = Some(channels.len()), + Some(n) if n != channels.len() => { + // The blocks of one ADTS frame carry the same + // program; a channel-count flip mid-frame is + // structurally inconsistent. + return Err(Error::ElementDecodeInvalid); + } + Some(_) => {} + } + pcm.extend(interleave_s16(&channels)?); + } + + Ok(DecodedFrame { + pcm, + channels: frame_channels.unwrap_or(0), + sample_rate: out_rate, + }) + } + + /// Walk a FIL element's Table 4.51 `extension_payload()` chain + /// (the body was left unconsumed by + /// [`Walker::next_element_keep_fill`]). `payload` is the byte + /// buffer `reader` was constructed over (needed to recompute the + /// §4.4.2.8.1 SBR CRC over its coverage region); `target` is the + /// preceding SCE / CPE this FIL would extend (its `id_syn_ele` + + /// slot key), or `None` when the FIL follows no channel element. + /// Returns the decoded SBR payload, if any; the threaded + /// `sbr_header()` reuse state is updated per slot. An + /// `EXT_SBR_DATA_CRC` payload whose recomputed CRC-10 disagrees + /// with the transmitted `bs_sbr_crc_bits` is rejected with + /// [`Error::SbrCrcMismatch`]. + fn consume_fill( + &mut self, + reader: &mut BitReader<'_>, + payload: &[u8], + payload_bytes: u32, + fs_sbr: u32, + target: Option<(IdSynEle, (u8, u8))>, + ) -> Result>> { + let mut remaining = payload_bytes; + let mut result = None; + while remaining > 0 { + match target { + None => { + // No preceding channel element: only the non-SBR + // payload types are meaningful here. + let p = ExtensionPayload::parse(reader, remaining)?; + let n = p.byte_length().max(1); + remaining = remaining.saturating_sub(n); + } + Some(_) if self.family != FrameFamily::Lc1024 => { + // The §4.6.18 SBR tool in this crate is defined + // over the 1024-line core frame (32-subband + // analysis / 2048-sample output); a 960-line or + // LD core cannot feed it, so an SBR extension + // type is rejected before its body is even + // parsed. Non-SBR payload types stay usable. + match ExtensionPayload::parse(reader, remaining) { + Ok(p) => { + let n = p.byte_length().max(1); + remaining = remaining.saturating_sub(n); + } + Err(Error::UnsupportedExtensionSbr(_)) => { + return Err(Error::SbrUnsupportedFrameFamily); + } + Err(e) => return Err(e), + } + } + Some((id_aac, slot)) => { + let prev = self.sbr_prev_header.get(&slot).copied(); + match ExtensionPayload::parse_with_sbr(reader, remaining, id_aac, fs_sbr, prev)? + { + ExtensionPayloadOrSbr::Payload(p) => { + let n = p.byte_length().max(1); + remaining = remaining.saturating_sub(n); + } + ExtensionPayloadOrSbr::Sbr(ext) => { + ext.verify_crc(payload)?; + self.sbr_prev_header.insert(slot, ext.header); + result = Some(ext); + remaining = 0; + } + ExtensionPayloadOrSbr::SbrPreHeader { crc, crc_region } => { + // §4.5.2.8.1: SBR payloads before the first + // sbr_header() — verify the CRC over the + // whole-payload region, then run upsampling + // and delay adjustment only (the None SBR + // slot below selects the §4.6.18.5 pure + // upsampling path). No header is threaded. + if let (Some(crc), Some((s, e))) = (crc, crc_region) { + if crate::adts_crc::sbr_crc(payload, s, e) != crc { + return Err(Error::SbrCrcMismatch); + } + } + self.sbr_active = true; + remaining = 0; + } + } + } + } + } + Ok(result) + } + + /// Decode one whole ADTS frame — fixed/variable header, the + /// optional `error_check()` CRC layer, and the `raw_data_block()` + /// payload(s) — to interleaved 16-bit PCM. + /// + /// `frame` must start at the ADTS syncword and carry at least + /// `aac_frame_length` bytes (trailing bytes are ignored). Unlike + /// [`Self::decode_frame`] (which receives the payload with the CRC + /// layer already stripped and therefore cannot verify it), this + /// entry point *verifies* the ISO/IEC 13818-7:2004 §8.1.1 CRCs + /// when `protection_absent == 0`: + /// + /// * single raw data block — the Table 1.A.8 `adts_error_check()` + /// 16-bit `crc_check` over the 56 header bits plus every + /// §8.1.1.1 protected element region; + /// * multiple raw data blocks — the Table 1.A.9 + /// `adts_header_error_check()` (headers + the 16-bit + /// `raw_data_block_position` table) followed by one Table 1.A.10 + /// `adts_raw_data_block_error_check()` per block, each read from + /// its byte-aligned slot after the block it protects. + /// + /// A mismatch surfaces [`Error::AdtsCrcMismatch`] before any + /// decoder state is touched. + pub fn decode_adts_frame(&mut self, frame: &[u8]) -> Result { + let (header, payload_offset) = AdtsHeader::parse(frame)?; + let frame_len = header.aac_frame_length as usize; + if frame_len < payload_offset || frame.len() < frame_len { + return Err(Error::UnexpectedEnd); + } + let frame = &frame[..frame_len]; + if header.protection_absent { + return self.decode_frame(&header, &frame[payload_offset..]); + } + let aot = header.audio_object_type(); + let fs = header.sampling_frequency_index; + if header.number_of_raw_data_blocks_in_frame == 1 { + // Table 1.A.8 adts_error_check(): one 16-bit crc_check at + // bytes 7..9 covering headers + the block's regions. + let crc = u16::from_be_bytes([frame[7], frame[8]]); + let payload = &frame[crate::adts::ADTS_HEADER_BYTES_WITH_CRC..]; + let mut reader = BitReader::new(payload); + let regions = crate::adts_crc::collect_block_regions(&mut reader, aot, fs)?; + if crate::adts_crc::adts_single_crc(&frame[..7], payload, ®ions) != crc { + return Err(Error::AdtsCrcMismatch); + } + return self.decode_frame(&header, payload); + } + // Multi-RDB form (Tables 1.A.9 / 1.A.10): N − 1 16-bit + // raw_data_block_position entries + the 16-bit header CRC, + // then each raw_data_block() followed by its own 16-bit CRC. + let n = usize::from(header.number_of_raw_data_blocks_in_frame); + let after_positions = 7 + 2 * (n - 1); + if frame.len() < after_positions + 2 { + return Err(Error::UnexpectedEnd); + } + let positions: Vec = (0..n - 1) + .map(|i| u16::from_be_bytes([frame[7 + 2 * i], frame[8 + 2 * i]])) + .collect(); + let header_crc = u16::from_be_bytes([frame[after_positions], frame[after_positions + 1]]); + if crate::adts_crc::adts_header_crc(&frame[..7], &positions) != header_crc { + return Err(Error::AdtsCrcMismatch); + } + let payload = &frame[after_positions + 2..]; + let mut reader = BitReader::new(payload); + // Verify each block's CRC, splicing the CRC fields out so the + // block walk below sees the contiguous raw_data_block() + // sequence it expects. + let mut clean = Vec::with_capacity(payload.len()); + for _ in 0..n { + let start_byte = (reader.bit_position() / 8) as usize; + let regions = crate::adts_crc::collect_block_regions(&mut reader, aot, fs)?; + let end_bit = reader.bit_position(); + if end_bit % 8 != 0 { + // A block that did not end on its §4.4.2.1 + // byte_alignment() cannot be followed by the + // byte-aligned CRC slot. + return Err(Error::UnexpectedEnd); + } + let rdb_crc = reader.read_u32(16).map_err(|_| Error::UnexpectedEnd)? as u16; + if crate::adts_crc::adts_rdb_crc(payload, ®ions) != rdb_crc { + return Err(Error::AdtsCrcMismatch); + } + clean.extend_from_slice(&payload[start_byte..(end_bit / 8) as usize]); + } + self.decode_raw_data_block( + aot, + fs, + header.sample_rate(), + header.channel_configuration, + header.number_of_raw_data_blocks_in_frame, + &clean, + ) + } + + /// Decode a whole raw-ADTS byte buffer to a vector of per-frame + /// interleaved PCM. + /// + /// Skips a leading ID3v2 tag if present, then walks consecutive ADTS + /// frames (`aac_frame_length`-delimited) to exhaustion, verifying + /// the `error_check()` CRC layer of every `protection_absent == 0` + /// frame (see [`Self::decode_adts_frame`]). A truncated trailing + /// frame (fewer bytes than its `aac_frame_length`) is rejected with + /// [`Error::UnexpectedEnd`]. + pub fn decode_all(&mut self, data: &[u8]) -> Result> { + let data = skip_id3v2(data); + let mut frames = Vec::new(); + let mut pos = 0usize; + while pos + crate::adts::ADTS_HEADER_BYTES_NO_CRC <= data.len() { + let (header, payload_offset) = AdtsHeader::parse(&data[pos..])?; + let frame_len = header.aac_frame_length as usize; + if frame_len < payload_offset || pos + frame_len > data.len() { + return Err(Error::UnexpectedEnd); + } + frames.push(self.decode_adts_frame(&data[pos..pos + frame_len])?); + pos += frame_len; + } + Ok(frames) + } + + /// Decode one §4.4.2.3 Table 4.19 `er_raw_data_block()` payload + /// (the ER General-Audio top-level payload) to interleaved 16-bit + /// PCM. + /// + /// The ER object types do not use the tagged `raw_data_block()` + /// element walk: the channel-element sequence is fixed by + /// `channelConfiguration` (1..=7). Each element body is parsed + /// through the error-resilient Table 4.50 branches selected by the + /// ASC's [`AacResilienceFlags`] triplet, and — when + /// `aacSpectralDataResilienceFlag` is set — the spectrum arrives + /// as the two HCR length fields plus the + /// `reordered_spectral_data()` payload decoded by + /// [`crate::hcr_decode::decode_reordered_spectral_data`]. + /// + /// Scope: the ER AAC LC (AOT 17), ER AAC LTP (AOT 19) and ER AAC + /// LD (AOT 23) object types — the three §4.4.2.3 Table 4.19 + /// payloads. ER AAC scalable (AOT 20) rides its own layered + /// `aac_scalable_main_element()` walk (see [`crate::scalable`]) + /// and is rejected here with [`Error::NotImplemented`]. For + /// AOT 19 the §4.6.7 LTP tool is live: `ics_info()` carries the + /// Table 4.55 non-LD `ltp_data()` branch (11-bit lag, `M = 0`), + /// and the per-element [`crate::element_decode::ElementDecoder`] + /// slots persist the §4.6.7.3 `x_rec` reconstruction history + /// across frames exactly as the non-ER AOT-4 walk does. The + /// trailing + /// `extension_payload()` loop is consumed permissively (ignored), + /// matching the FIL handling of the non-ER walk; `epConfig` 2 / 3 + /// physical-payload preprocessing (§4.5.2.4) is out of scope (the + /// ASC parser already rejects those configurations). + pub fn decode_er_raw_data_block( + &mut self, + aot: u8, + fs_index: u8, + sample_rate: u32, + channel_configuration: u8, + resilience: AacResilienceFlags, + payload: &[u8], + ) -> Result { + // AOT 17 (ER AAC LC), AOT 19 (ER AAC LTP) and AOT 23 (ER AAC + // LD) share the Table 4.19 er_raw_data_block(). LD differs in + // the 512/480-line frame family this decoder was configured + // with (§4.6.17) and its delta-coded ltp_data() branch; AOT 19 + // adds the plain §4.6.7 LTP tool (Table 4.55 non-LD branch) + // whose per-element reconstruction history the decoder slots + // below already thread. ER AAC scalable (AOT 20) uses the + // layered aac_scalable_main_element() walk instead and stays + // out of this entry point. + if aot != 17 && aot != 19 && aot != 23 { + return Err(Error::NotImplemented); + } + // An LD stream must run an LD family and vice versa — a + // mismatch means the caller never installed the ASC-resolved + // family, which would silently mis-decode every band. + if (aot == 23) != self.family.is_ld() { + return Err(Error::ElementDecodeInvalid); + } + let fs = fs_index; + let family = self.family; + // Table 4.19: the fixed element sequence per channelConfiguration. + let sequence: &[IdSynEle] = match channel_configuration { + 1 => &[IdSynEle::Sce], + 2 => &[IdSynEle::Cpe], + 3 => &[IdSynEle::Sce, IdSynEle::Cpe], + 4 => &[IdSynEle::Sce, IdSynEle::Cpe, IdSynEle::Sce], + 5 => &[IdSynEle::Sce, IdSynEle::Cpe, IdSynEle::Cpe], + 6 => &[IdSynEle::Sce, IdSynEle::Cpe, IdSynEle::Cpe, IdSynEle::Lfe], + 7 => &[ + IdSynEle::Sce, + IdSynEle::Cpe, + IdSynEle::Cpe, + IdSynEle::Cpe, + IdSynEle::Lfe, + ], + _ => return Err(Error::ElementDecodeInvalid), + }; + + let mut reader = BitReader::new(payload); + let mut channels: Vec> = Vec::new(); + for &kind in sequence { + let element_instance_tag = reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; + let key = (kind_id(kind), element_instance_tag); + match kind { + IdSynEle::Sce | IdSynEle::Lfe => { + let body = + IcsBody::parse_er_family(&mut reader, family, aot, fs, false, resilience)?; + let ics = body.ics_info.clone().ok_or(Error::ElementDecodeInvalid)?; + let spectral = + parse_er_spectral(&mut reader, &body, &ics, fs, resilience, false)?; + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + let dec = self + .decoders + .entry(key) + .or_insert_with(|| ElementDecoder::new_family(family)); + channels.push(dec.decode_sce(&ch, aot, fs)?); + } + IdSynEle::Cpe => { + let common_window = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let dec_out = if common_window { + // §4.4.2.3 shared ics_info + Table 4.4 ms_mask. + let ics = IcsInfo::parse_family(&mut reader, family, aot, fs, true)?; + let ms_bits = reader.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; + let ms_mask_present = MsMaskPresent::from_bits(ms_bits)?; + let mut ms_used: Vec> = Vec::new(); + if ms_mask_present == MsMaskPresent::Mask { + for _g in 0..usize::from(ics.num_window_groups) { + let mut row = Vec::with_capacity(usize::from(ics.max_sfb)); + for _sfb in 0..usize::from(ics.max_sfb) { + row.push(reader.read_bit().map_err(|_| Error::UnexpectedEnd)?); + } + ms_used.push(row); + } + } + let left_body = IcsBody::parse_with_ics_info_er( + &mut reader, + &ics, + aot, + false, + resilience, + )?; + let left_spectral = + parse_er_spectral(&mut reader, &left_body, &ics, fs, resilience, true)?; + let right_body = IcsBody::parse_with_ics_info_er( + &mut reader, + &ics, + aot, + false, + resilience, + )?; + let right_spectral = parse_er_spectral( + &mut reader, + &right_body, + &ics, + fs, + resilience, + true, + )?; + let left = ChannelInput { + body: &left_body, + ics_info: &ics, + spectral: &left_spectral, + }; + let right = ChannelInput { + body: &right_body, + ics_info: &ics, + spectral: &right_spectral, + }; + let joint = CpeJointStereo { + ms_mask_present, + ms_used, + }; + let dec = self + .decoders + .entry(key) + .or_insert_with(|| ElementDecoder::new_family(family)); + dec.decode_cpe(&left, &right, &joint, aot, fs)? + } else { + let left_body = IcsBody::parse_er_family( + &mut reader, + family, + aot, + fs, + false, + resilience, + )?; + let left_ics = left_body + .ics_info + .clone() + .ok_or(Error::ElementDecodeInvalid)?; + let left_spectral = parse_er_spectral( + &mut reader, + &left_body, + &left_ics, + fs, + resilience, + true, + )?; + let right_body = IcsBody::parse_er_family( + &mut reader, + family, + aot, + fs, + false, + resilience, + )?; + let right_ics = right_body + .ics_info + .clone() + .ok_or(Error::ElementDecodeInvalid)?; + let right_spectral = parse_er_spectral( + &mut reader, + &right_body, + &right_ics, + fs, + resilience, + true, + )?; + let left = ChannelInput { + body: &left_body, + ics_info: &left_ics, + spectral: &left_spectral, + }; + let right = ChannelInput { + body: &right_body, + ics_info: &right_ics, + spectral: &right_spectral, + }; + let dec = self + .decoders + .entry(key) + .or_insert_with(|| ElementDecoder::new_family(family)); + dec.decode_cpe(&left, &right, &CpeJointStereo::default(), aot, fs)? + }; + channels.push(dec_out.0); + channels.push(dec_out.1); + } + _ => return Err(Error::ElementDecodeInvalid), + } + } + // Trailing extension_payload() loop + byte_alignment(): consumed + // permissively (nothing this decoder acts on rides there yet). + + // Table 1.19 canonical output reorder, same as the non-ER walk. + let channels = crate::channel_map::reorder_channels(channel_configuration, channels); + let pcm = interleave_s16(&channels)?; + Ok(DecodedFrame { + pcm, + channels: channels.len(), + sample_rate, + }) + } + + // (the per-CPE parse lives in the free `parse_cpe` below so the + // two-pass §4.6.8.3.3 coupling walk can defer decoding) +} + +/// A parsed `channel_pair_element()` awaiting decode: both channels' +/// bodies + spectra and the Table 4.4 joint-stereo header. For the +/// `common_window == 1` form the shared `ics_info` is cloned into both +/// per-channel slots (the clone carries `ltp_data_pair`, so the +/// channel-1 LTP selection is unaffected). +pub(crate) struct ParsedCpe { + joint: CpeJointStereo, + left_body: IcsBody, + left_ics: IcsInfo, + left_spectral: SpectralData, + right_body: IcsBody, + right_ics: IcsInfo, + right_spectral: SpectralData, + /// Absolute bit position (in the reader's buffer) where the second + /// `individual_channel_stream()` begins — the anchor of the + /// 13818-7:2004 §8.1.1.1 128-bit second-ICS ADTS-CRC region. + pub(crate) second_ics_start_bit: u64, +} + +impl ParsedCpe { + /// Borrow the two [`ChannelInput`]s plus the joint-stereo header. + fn channel_inputs(&self) -> (ChannelInput<'_>, ChannelInput<'_>, &CpeJointStereo) { + ( + ChannelInput { + body: &self.left_body, + ics_info: &self.left_ics, + spectral: &self.left_spectral, + }, + ChannelInput { + body: &self.right_body, + ics_info: &self.right_ics, + spectral: &self.right_spectral, + }, + &self.joint, + ) + } +} + +/// Parse one CPE body (after the walker consumed its element-instance +/// tag): the §4.4.2.3 `common_window` fork, the Table 4.4 +/// `ms_mask_present` / `ms_used` joint-stereo header (shared form), and +/// both channels' `individual_channel_stream()` + `spectral_data()`. +pub(crate) fn parse_cpe(reader: &mut BitReader<'_>, aot: u8, fs: u8) -> Result { + parse_cpe_family(reader, FrameFamily::Lc1024, aot, fs) +} + +/// [`parse_cpe`] under an explicit §4.5.1.1 frame-length family. +pub(crate) fn parse_cpe_family( + reader: &mut BitReader<'_>, + family: FrameFamily, + aot: u8, + fs: u8, +) -> Result { + let common_window = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + if common_window { + // §4.4.2.3: shared ics_info, then the Table 4.4 ms_mask. + let ics = IcsInfo::parse_family(reader, family, aot, fs, true)?; + let ms_bits = reader.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; + let ms_mask_present = MsMaskPresent::from_bits(ms_bits)?; + let mut ms_used: Vec> = Vec::new(); + if ms_mask_present == MsMaskPresent::Mask { + for _g in 0..usize::from(ics.num_window_groups) { + let mut row = Vec::with_capacity(usize::from(ics.max_sfb)); + for _sfb in 0..usize::from(ics.max_sfb) { + row.push(reader.read_bit().map_err(|_| Error::UnexpectedEnd)?); + } + ms_used.push(row); + } + } + let left_body = IcsBody::parse_with_ics_info(reader, &ics, aot, false)?; + let left_spectral = SpectralData::parse(reader, &ics, &left_body.section_data, fs)?; + let second_ics_start_bit = reader.bit_position(); + let right_body = IcsBody::parse_with_ics_info(reader, &ics, aot, false)?; + let right_spectral = SpectralData::parse(reader, &ics, &right_body.section_data, fs)?; + Ok(ParsedCpe { + joint: CpeJointStereo { + ms_mask_present, + ms_used, + }, + left_body, + left_ics: ics.clone(), + left_spectral, + right_body, + right_ics: ics, + right_spectral, + second_ics_start_bit, + }) + } else { + // Non-shared CPE: each channel carries its own ics_info; no + // M/S mask, so the joint-stereo tools do not run. + let left_body = IcsBody::parse_family(reader, family, aot, fs, false)?; + let left_ics = left_body + .ics_info + .clone() + .ok_or(Error::ElementDecodeInvalid)?; + let left_spectral = SpectralData::parse(reader, &left_ics, &left_body.section_data, fs)?; + let second_ics_start_bit = reader.bit_position(); + let right_body = IcsBody::parse_family(reader, family, aot, fs, false)?; + let right_ics = right_body + .ics_info + .clone() + .ok_or(Error::ElementDecodeInvalid)?; + let right_spectral = SpectralData::parse(reader, &right_ics, &right_body.section_data, fs)?; + Ok(ParsedCpe { + joint: CpeJointStereo::default(), + left_body, + left_ics, + left_spectral, + right_body, + right_ics, + right_spectral, + second_ics_start_bit, + }) + } +} + +/// Parse one ER channel's spectrum: the plain Table 4.56 +/// `spectral_data()` when `aacSpectralDataResilienceFlag` is clear, or +/// the §4.6.16.3 `reordered_spectral_data()` payload (whose two length +/// fields the ER body already captured) decoded through +/// [`crate::hcr_decode::decode_reordered_spectral_data`]. +fn parse_er_spectral( + reader: &mut BitReader<'_>, + body: &IcsBody, + ics: &IcsInfo, + fs: u8, + resilience: AacResilienceFlags, + is_cpe: bool, +) -> Result { + if !resilience.spectral_data { + return SpectralData::parse(reader, ics, &body.section_data, fs); + } + let (len_reordered, len_longest) = body + .reordered_spectral_lengths + .ok_or(Error::ElementDecodeInvalid)?; + let len = crate::hcr::clamp_reordered_length(len_reordered, is_cpe); + // Gather the (not necessarily byte-aligned) payload bits. + let mut buf = vec![0u8; usize::from(len).div_ceil(8)]; + for i in 0..usize::from(len) { + if reader.read_bit().map_err(|_| Error::UnexpectedEnd)? { + buf[i / 8] |= 0x80 >> (i % 8); + } + } + crate::hcr_decode::decode_reordered_spectral_data( + &buf, + len, + len_longest, + ics, + &body.section_data, + fs, + ) +} + +/// §4.6.8.3.3 `decode_coupling_channel()` — collect the coupling +/// contributions addressed at one target channel. +/// +/// Walks every CCE of the same raw data block, replaying the spec's +/// target loop to assign `list_index` values: an SCE target consumes +/// one gain list; a CPE target consumes one shared list (`cc_l == cc_r +/// == 0`, applied to both channels), or one list per flagged channel. +/// `channel` selects the target channel of a CPE (`0` left, `1` +/// right); an SCE target only ever matches `channel == 0`. +fn coupling_for<'a>( + cces: &'a [(u8, CouplingChannelElement)], + decoded: &'a [DecodedCce], + block: u8, + kind: IdSynEle, + tag: u8, + channel: usize, +) -> Vec> { + let mut out = Vec::new(); + for ((cce_block, cce), dec) in cces.iter().zip(decoded.iter()) { + if *cce_block != block { + continue; + } + let mut list_index = 0usize; + for t in &cce.header.targets { + if !t.is_cpe { + if kind == IdSynEle::Sce && tag == t.tag_select && channel == 0 { + out.push(CouplingApply { + cce, + decoded: dec, + list_index, + }); + } + list_index += 1; + } else { + let addressed = kind == IdSynEle::Cpe && tag == t.tag_select; + if !t.cc_l && !t.cc_r { + // Table 4.153 shared list: both channels couple + // with the same gain list. + if addressed { + out.push(CouplingApply { + cce, + decoded: dec, + list_index, + }); + } + list_index += 1; + } + if t.cc_l { + if addressed && channel == 0 { + out.push(CouplingApply { + cce, + decoded: dec, + list_index, + }); + } + list_index += 1; + } + if t.cc_r { + if addressed && channel == 1 { + out.push(CouplingApply { + cce, + decoded: dec, + list_index, + }); + } + list_index += 1; + } + } + } + } + out +} + +/// Map a channel-element `id_syn_ele` to the slot key's first component +/// (the element decoders are keyed independently per syntactic-element +/// id so an SCE tag 0 and a CPE tag 0 never collide). +fn kind_id(kind: IdSynEle) -> u8 { + match kind { + IdSynEle::Sce => 0, + IdSynEle::Cpe => 1, + IdSynEle::Lfe => 3, + _ => 9, + } +} + +fn unsupported_element(kind: IdSynEle) -> Error { + // CCE (coupling) has no decode path; surface the element-decode + // failure mode rather than a parse error so the caller can tell a + // structural-OK-but-unsupported element apart from a malformed one. + let _ = kind; + Error::ElementDecodeInvalid +} + +/// Skip a leading ID3v2 tag (`"ID3"` + 6-byte header + syncsafe size + +/// optional footer) if present; otherwise return the input unchanged. +fn skip_id3v2(data: &[u8]) -> &[u8] { + if data.len() < 10 || &data[..3] != b"ID3" { + return data; + } + let size = data[6..10] + .iter() + .fold(0usize, |acc, &b| (acc << 7) | usize::from(b & 0x7f)); + let footer = if data[5] & 0x10 != 0 { 10 } else { 0 }; + let total = 10 + size + footer; + if total >= data.len() { + data + } else { + &data[total..] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn skip_id3v2_passes_through_non_id3() { + let data = [0xFFu8, 0xF1, 0x00, 0x00]; + assert_eq!(skip_id3v2(&data), &data); + } + + #[test] + fn skip_id3v2_strips_a_tag() { + // "ID3", ver 4.0, no flags, syncsafe size = 4 → 10 + 4 = 14 + // bytes of tag, then a sentinel payload byte. + let mut data = vec![b'I', b'D', b'3', 4, 0, 0, 0, 0, 0, 4]; + data.extend_from_slice(&[0; 4]); + data.push(0xAB); + assert_eq!(skip_id3v2(&data), &[0xABu8]); + } + + #[test] + fn skip_id3v2_keeps_tag_when_size_overruns() { + // A declared size larger than the buffer leaves the data as-is + // rather than panicking. + let data = vec![b'I', b'D', b'3', 4, 0, 0, 0x7f, 0x7f, 0x7f, 0x7f]; + assert_eq!(skip_id3v2(&data), &data[..]); + } + + #[test] + fn kind_id_separates_sce_and_cpe() { + assert_ne!(kind_id(IdSynEle::Sce), kind_id(IdSynEle::Cpe)); + assert_ne!(kind_id(IdSynEle::Lfe), kind_id(IdSynEle::Cpe)); + } +} diff --git a/crates/vendor/oxideav-aac/src/decoded_spectrum.rs b/crates/vendor/oxideav-aac/src/decoded_spectrum.rs new file mode 100644 index 00000000..2a040ea9 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/decoded_spectrum.rs @@ -0,0 +1,353 @@ +//! Per-channel "decoded spectrum" pipeline stage — ISO/IEC 14496-3 +//! §4.6.3.3 `quant_to_spec()` + the parse → dequant → scalefactor → +//! TNS composition. +//! +//! This module chains the per-tool reconstruction primitives into +//! the channel-level stage that ends one step short of the +//! filterbank (§4.6.11 IMDCT + window-overlap-add, a follow-up +//! round): +//! +//! 1. **Pulse fix-up** (§4.6.3.3) — when `pulse_data_present`, fold +//! the `±pulse_amp` corrections into `x_quant` via +//! [`crate::swb_offset::apply_pulse_data`] (long windows only, +//! per Table 4.50 Note 1). +//! 2. **Scalefactor accumulation** (§4.6.2.3.2 / §4.6.8.1.4 / +//! §4.6.13) — [`crate::scale_factor_data::accumulate`]. +//! 3. **Inverse quantization + rescaling** (§4.6.1.3 / §4.6.2.3.3) +//! — [`crate::dequant::rescale_spectrum`]. +//! 4. **De-interleaving** (§4.6.3.3 `quant_to_spec()`) — from the +//! §4.5.2.3.5 group-interleaved transmission order to the +//! window-major `spec[w][k]` layout that TNS and the filterbank +//! consume ([`quant_to_spec`]). +//! 5. **TNS** (§4.6.9) — when `tns_data_present`, +//! [`crate::tns_frame::tns_decode_frame`] over the de-interleaved +//! spectrum. +//! +//! ## §4.6.3.3 `quant_to_spec()` +//! +//! ```text +//! quant_to_spec() { +//! k = 0; +//! for (g = 0; g < num_window_groups; g++ ) { +//! j = 0; +//! for (sfb = 0; sfb < num_swb; sfb++) { +//! width = swb_offset[sfb+1] - swb_offset[sfb]; +//! for (win = 0; win < window_group_length[g]; win++) { +//! for (bin = 0; bin < width; bin++) { +//! spec[win+k][bin+j] = x_quant[g][win][sfb][bin]; +//! } +//! } +//! j += width; +//! } +//! k += window_group_length[g]; +//! } +//! } +//! ``` +//! +//! The interleaved source reads linearly in exactly the loop order +//! (`g`, `sfb`, `win`, `bin`) because §4.5.2.3.5 stores each +//! virtual scalefactor band as the concatenated per-window +//! scalefactor-window-band slices. For the long window sequences +//! (`num_window_groups == 1`, `window_group_length[0] == 1`) the +//! mapping degenerates to an identity copy. +//! +//! ## Scope +//! +//! Intensity stereo (§4.6.8.2), M/S (§4.6.8.1), and PNS (§4.6.13) +//! reconstruction are channel-*pair* / noise-synthesis tools that +//! slot between steps 4 and 5 (de-interleave → joint-stereo → TNS). +//! The M/S de-matrix is implemented as +//! [`crate::ms_stereo::apply_ms_stereo`], a CPE-level pass over the +//! two channels' de-interleaved spectra; this single-channel stage +//! does not invoke it (the caller runs it on the pair before each +//! channel's TNS). Intensity stereo and PNS synthesis remain +//! follow-ups, so intensity / `NOISE_HCB` bands still come out as +//! silence here. The Main-profile predictor (§4.6.7) and LTP +//! (§4.6.6) are likewise deferred. + +use crate::dequant::rescale_spectrum; +use crate::ics_body::IcsBody; +use crate::ics_info::IcsInfo; +use crate::scale_factor_data::accumulate; +use crate::spectral_data::SpectralData; +use crate::swb_offset::apply_pulse_data_family; +use crate::tns_frame::tns_decode_frame_ics; +use crate::{Error, Result}; + +/// §4.6.3.3 `quant_to_spec()` — de-interleave per-group +/// transmission-order coefficient buffers into the window-major +/// `spec[w][k]` layout (windows concatenated: +/// `spec[w * window_len + k]`). +/// +/// * `groups` — one buffer per window group in the §4.5.2.3.5 +/// interleaved order, each spanning the full group +/// (`window_group_length[g] × 128` short, `1024` long) — the +/// shape produced by [`SpectralData::parse`] and preserved by +/// [`rescale_spectrum`]. +/// * `ics_info` / `fs_index` — grouping and the Table 4.129-family +/// `swb_offset` table. +/// +/// Errors: +/// +/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] — `fs_index` +/// outside the SWB-table range. +/// * [`Error::QuantToSpecInvalid`] — group count or a group buffer +/// length disagreeing with the `ics_info` grouping, or a grouping +/// whose `window_group_length` sum is not `num_windows`. +pub fn quant_to_spec(groups: &[Vec], ics_info: &IcsInfo, fs_index: u8) -> Result> { + let window_len = ics_info.window_len()?; + let offsets = ics_info.swb_offsets(fs_index)?; + let num_swb = offsets.len() - 1; + let num_windows = ics_info.num_windows as usize; + let num_groups = ics_info.num_window_groups as usize; + + if groups.len() != num_groups + || ics_info.window_group_length.len() != num_groups + || ics_info + .window_group_length + .iter() + .map(|&w| w as usize) + .sum::() + != num_windows + { + return Err(Error::QuantToSpecInvalid); + } + + let mut spec = vec![0.0f64; num_windows * window_len]; + // `k` in the pseudocode: index of the group's first window. + let mut window_base = 0usize; + for (g, group) in groups.iter().enumerate() { + let wgl = ics_info.window_group_length[g] as usize; + if group.len() != wgl * window_len { + return Err(Error::QuantToSpecInvalid); + } + // The interleaved buffer reads linearly in (sfb, win, bin) + // order; `j` is the in-window coefficient offset of the + // current scalefactor window band. + let mut src = group.iter(); + let mut j = 0usize; + for sfb in 0..num_swb { + let width = (offsets[sfb + 1] - offsets[sfb]) as usize; + for win in 0..wgl { + let dst = (window_base + win) * window_len + j; + for bin in 0..width { + // group.len() == wgl * window_len == wgl * sum of + // widths, so the iterator yields exactly enough. + spec[dst + bin] = *src.next().expect("group length checked above"); + } + } + j += width; + } + window_base += wgl; + } + Ok(spec) +} + +/// Decode one channel's spectrum: pulse fix-up → scalefactor +/// accumulation → inverse quantization + rescaling → +/// `quant_to_spec()` → TNS. +/// +/// * `body` — the parsed Table 4.50 channel body +/// ([`IcsBody::parse`] / [`IcsBody::parse_with_ics_info`]). +/// * `ics_info` — the channel's `ics_info()`; pass +/// `body.ics_info.as_ref().unwrap()` for the inline form or the +/// externally-held shared `IcsInfo` for the CPE +/// `common_window == 1` form. +/// * `spectral` — the channel's parsed Table 4.56 spectrum +/// ([`SpectralData::parse`] resumed at +/// `body.spectral_data_bit_offset`). +/// * `aot` / `fs_index` — `audioObjectType` and +/// `samplingFrequencyIndex`, driving the TNS clamp tables and the +/// `swb_offset` selection. +/// +/// Returns the window-major decoded spectrum (`num_windows × +/// window_len` = 1024 coefficients, window `w` at +/// `spec[w * window_len ..]`) — the §4.6.11 filterbank's input. +/// +/// Errors propagate from the composed stages: see +/// [`apply_pulse_data`], [`accumulate`], [`rescale_spectrum`], +/// [`quant_to_spec`], and [`tns_decode_frame`]. +pub fn decode_channel_spectrum( + body: &IcsBody, + ics_info: &IcsInfo, + spectral: &SpectralData, + aot: u8, + fs_index: u8, +) -> Result> { + // 1. §4.6.3.3 pulse fix-up on the quantised spectrum (long + // windows only — the parser already rejects the EIGHT_SHORT + // combination, and a long sequence has exactly one group). + let x_quant: &SpectralData = &if let Some(pd) = &body.pulse_data { + let mut patched = spectral.clone(); + let group0 = patched.x_quant.first_mut().ok_or(Error::DequantInvalid)?; + apply_pulse_data_family(group0, ics_info.family, fs_index, pd)?; + patched + } else { + spectral.clone() + }; + + // 2. §4.6.2.3.2 scalefactor accumulation. + let scale_factors = accumulate( + &body.scale_factor_data, + &body.section_data.sfb_cb, + body.global_gain, + )?; + + // 3. §4.6.1.3 + §4.6.2.3.3 inverse quantization + rescaling. + let rescaled = rescale_spectrum( + x_quant, + &scale_factors, + &body.section_data.sfb_cb, + ics_info, + fs_index, + )?; + + // 4. §4.6.3.3 quant_to_spec() de-interleaving. + let mut spec = quant_to_spec(&rescaled, ics_info, fs_index)?; + + // 5. §4.6.9 TNS. + if let Some(tns) = &body.tns_data { + tns_decode_frame_ics(&mut spec, tns, ics_info, aot, fs_index)?; + } + Ok(spec) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + + fn long_ics_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], + } + } + + fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { + let num_window_groups = window_group_length.len() as u8; + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups, + window_group_length, + num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[4], + } + } + + // ===== quant_to_spec ===== + + #[test] + fn quant_to_spec_long_is_identity() { + let info = long_ics_info(10); + let group: Vec = (0..1024).map(|i| i as f64 * 0.5 - 100.0).collect(); + let spec = quant_to_spec(core::slice::from_ref(&group), &info, 4).unwrap(); + assert_eq!(spec, group); + } + + #[test] + fn quant_to_spec_short_deinterleaves_grouped_windows() { + // Grouping 5 + 3 at fs_index 4 (short band 0 is 4 wide, + // band 1 is 4 wide, ...). Place markers at known + // (g, sfb, win, bin) coordinates and verify their + // window-major destinations. + let info = short_ics_info(2, vec![5, 3]); + let mut g0 = vec![0.0f64; 5 * 128]; + let mut g1 = vec![0.0f64; 3 * 128]; + // (g=0, sfb=0, win=2, bin=1): interleaved index + // 0 + 2*4 + 1 = 9 -> spec window 2, coefficient 1. + g0[9] = 1.0; + // (g=0, sfb=1, win=4, bin=3): interleaved index + // 5*4 + 4*4 + 3 = 39 -> spec window 4, coefficient 4+3. + g0[39] = 2.0; + // (g=1, sfb=0, win=0, bin=0): -> spec window 5 (groups 0..4 + // are group 0), coefficient 0. + g1[0] = 3.0; + // (g=1, sfb=1, win=2, bin=2): interleaved index + // 3*4 + 2*4 + 2 = 22 -> spec window 7, coefficient 6. + g1[22] = 4.0; + let spec = quant_to_spec(&[g0, g1], &info, 4).unwrap(); + assert_eq!(spec.len(), 1024); + assert_eq!(spec[2 * 128 + 1], 1.0); + assert_eq!(spec[4 * 128 + 7], 2.0); + assert_eq!(spec[5 * 128], 3.0); + assert_eq!(spec[7 * 128 + 6], 4.0); + let placed = spec.iter().filter(|&&v| v != 0.0).count(); + assert_eq!(placed, 4); + } + + #[test] + fn quant_to_spec_short_full_table_round_trips_every_coefficient() { + // Tag every interleaved coefficient with a unique value and + // verify the de-interleave is a permutation reaching all + // 1024 slots. + let info = short_ics_info(14, vec![1, 2, 1, 4]); + let mut groups = Vec::new(); + let mut tag = 1.0f64; + for &wgl in &info.window_group_length { + let mut g = vec![0.0f64; wgl as usize * 128]; + for slot in g.iter_mut() { + *slot = tag; + tag += 1.0; + } + groups.push(g); + } + let spec = quant_to_spec(&groups, &info, 4).unwrap(); + let mut seen: Vec = spec.clone(); + seen.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let want: Vec = (1..=1024).map(|i| i as f64).collect(); + assert_eq!(seen, want); + } + + #[test] + fn quant_to_spec_rejects_shape_mismatches() { + let info = short_ics_info(2, vec![5, 3]); + // Wrong group count. + assert!(matches!( + quant_to_spec(&[vec![0.0; 5 * 128]], &info, 4), + Err(Error::QuantToSpecInvalid) + )); + // Wrong group buffer length. + assert!(matches!( + quant_to_spec(&[vec![0.0; 5 * 128], vec![0.0; 2 * 128]], &info, 4), + Err(Error::QuantToSpecInvalid) + )); + // Grouping that does not sum to num_windows. + let bad = short_ics_info(2, vec![5, 2]); + assert!(matches!( + quant_to_spec(&[vec![0.0; 5 * 128], vec![0.0; 2 * 128]], &bad, 4), + Err(Error::QuantToSpecInvalid) + )); + // Unsupported fs_index propagates. + let info = long_ics_info(2); + assert!(matches!( + quant_to_spec(&[vec![0.0; 1024]], &info, 12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/dequant.rs b/crates/vendor/oxideav-aac/src/dequant.rs new file mode 100644 index 00000000..34d46325 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/dequant.rs @@ -0,0 +1,476 @@ +//! §4.6.1.3 inverse quantization + §4.6.2.3.3 scalefactor +//! application — ISO/IEC 14496-3. +//! +//! The first numeric reconstruction stage after the Table 4.56 +//! `spectral_data()` wire walk: convert the quantised integer +//! spectrum `x_quant` into the rescaled real-valued spectrum +//! `x_rescal` that the downstream tools (TNS, filterbank) consume. +//! +//! ## §4.6.1.3 — inverse quantization +//! +//! The encoder's non-uniform quantizer is inverted per coefficient: +//! +//! ```text +//! x_invquant = Sign(x_quant) * |x_quant|^(4/3) +//! ``` +//! +//! The maximum allowed absolute amplitude for `x_quant` is 8191 +//! ([`crate::spectral_codebook::MAX_QUANT`]); the wire walker +//! already enforces it, so [`inverse_quantize`] accepts any `i32` +//! and leaves range policing to the parser. +//! +//! ## §4.6.2.3.3 — applying scalefactors +//! +//! Every scalefactor band is rescaled by the gain of its absolute +//! scalefactor: +//! +//! ```text +//! gain = 2^(0.25 * (sf[g][sfb] - SF_OFFSET)) SF_OFFSET = 100 +//! x_rescal[...] = x_invquant[...] * gain +//! ``` +//! +//! per the §4.6.2.3.3 pseudocode, with the same gain applied to all +//! grouped short windows of a (virtual) scalefactor band. The +//! band → coefficient mapping is the §4.5.2.3.4 +//! [`crate::spectral_data::sect_sfb_offset`] derivation, so the +//! whole operation runs directly over the §4.5.2.3.5 interleaved +//! transmission-order buffers produced by +//! [`crate::spectral_data::SpectralData::parse`]. +//! +//! Bands whose codebook carries no spectrum keep a `0.0` output: +//! `ZERO_HCB` bands and bands at or above `max_sfb` transmit +//! nothing (and the wire walker leaves their `x_quant` at 0), while +//! `NOISE_HCB` / intensity bands are reconstructed by the PNS / +//! intensity-stereo tools (§4.6.13 / §4.6.8) which are not part of +//! this stage — their [`AbsoluteScaleFactorEntry::NoiseNrg`] / +//! [`AbsoluteScaleFactorEntry::IsPos`] records are consumed (to +//! keep the wire-order lockstep) but produce no rescaled energy +//! here. + +use crate::ics_info::IcsInfo; +use crate::scale_factor_data::{AbsoluteScaleFactorEntry, AbsoluteScaleFactors}; +use crate::section_data::{Codebook, ZERO_HCB}; +use crate::spectral_data::{sect_sfb_offset, SpectralData}; +use crate::{Error, Result}; + +/// `SF_OFFSET` per §4.6.2.3.3 — the scalefactor that maps to unit +/// gain. "The constant SF_OFFSET must be set to 100." +pub const SF_OFFSET: i32 = 100; + +/// §4.6.1.3 inverse quantization of one coefficient: +/// `Sign(x_quant) · |x_quant|^(4/3)`. +#[inline] +pub fn inverse_quantize(x_quant: i32) -> f64 { + // |x|^(4/3) computed as |x| · |x|^(1/3): `cbrt` is correctly + // rounded, so perfect cubes (and 0 / ±1) invert exactly, and the + // general case avoids the representation error of the literal + // exponent 4/3. + let abs = f64::from(x_quant.unsigned_abs()); + let mag = abs * abs.cbrt(); + if x_quant < 0 { + -mag + } else { + mag + } +} + +/// §4.6.2.3.3 `get_scale_factor_gain()`: +/// `2^(0.25 · (sf − SF_OFFSET))`. +#[inline] +pub fn scale_factor_gain(sf: u8) -> f64 { + (0.25 * f64::from(i32::from(sf) - SF_OFFSET)).exp2() +} + +/// Run §4.6.1.3 inverse quantization and §4.6.2.3.3 scalefactor +/// application over one channel's quantised spectrum. +/// +/// * `spectral` — the per-group interleaved `x_quant` buffers from +/// [`SpectralData::parse`] (or the same shape with the §4.6.3.3 +/// pulse fix-up already folded in via +/// [`crate::swb_offset::apply_pulse_data`]). +/// * `scale_factors` — the absolute per-band records from +/// [`crate::scale_factor_data::accumulate`]. +/// * `sfb_cb` — the per-`(g, sfb)` codebook map from +/// [`crate::section_data::SectionData::parse`]. +/// * `ics_info` / `fs_index` — drive the §4.5.2.3.4 band → +/// coefficient mapping and the group buffer shapes. +/// +/// Returns the rescaled spectrum `x_rescal` in the same per-group +/// interleaved layout (and lengths) as the input `x_quant`. +/// +/// Errors: +/// +/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] / +/// [`Error::SpectralDataInvalid`] — propagated from +/// [`sect_sfb_offset`] (`fs_index` out of range, `max_sfb` above +/// `num_swb`). +/// * [`Error::DequantInvalid`] — structural mismatch between the +/// three inputs: group counts disagreeing with +/// `num_window_groups`, a group buffer length disagreeing with +/// `window_group_length[g] × 128` (or 1024 long), or a +/// scalefactor-entry sequence that does not match the +/// non-`ZERO_HCB` codebook classification of `sfb_cb` (including +/// the reserved codebook 12, which carries a scalefactor on the +/// wire but has no spectrum semantics to rescale). +pub fn rescale_spectrum( + spectral: &SpectralData, + scale_factors: &AbsoluteScaleFactors, + sfb_cb: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, +) -> Result>> { + let offsets = sect_sfb_offset(ics_info, fs_index)?; + let num_groups = ics_info.num_window_groups as usize; + if spectral.x_quant.len() != num_groups + || scale_factors.entries.len() != num_groups + || sfb_cb.len() != num_groups + { + return Err(Error::DequantInvalid); + } + + let mut out = Vec::with_capacity(num_groups); + for (g, group_offsets) in offsets.iter().enumerate() { + let x_quant = &spectral.x_quant[g]; + let window_len = ics_info.window_len().map_err(|_| Error::DequantInvalid)?; + let expected_len = if ics_info.window_sequence.is_eight_short() { + ics_info.window_group_length[g] as usize * window_len + } else { + window_len + }; + if x_quant.len() != expected_len || sfb_cb[g].len() != ics_info.max_sfb as usize { + return Err(Error::DequantInvalid); + } + + let mut rescal = vec![0.0f64; x_quant.len()]; + let mut entries = scale_factors.entries[g].iter(); + for (sfb, &cb) in sfb_cb[g].iter().enumerate() { + if cb == ZERO_HCB { + continue; + } + let entry = entries.next().ok_or(Error::DequantInvalid)?; + let kind = Codebook::from_value(cb); + match entry { + AbsoluteScaleFactorEntry::Sf(sf) + if matches!( + kind, + Codebook::Quad { .. } | Codebook::Pair { .. } | Codebook::Esc + ) => + { + let gain = scale_factor_gain(*sf); + let start = group_offsets[sfb] as usize; + let end = group_offsets[sfb + 1] as usize; + for k in start..end { + rescal[k] = inverse_quantize(x_quant[k]) * gain; + } + } + // PNS / intensity bands transmit no spectrum; their + // §4.6.13 / §4.6.8 reconstruction happens in the + // dedicated tools, not the rescale stage. Consume + // the record to keep the wire-order lockstep. + AbsoluteScaleFactorEntry::NoiseNrg(_) if kind.is_noise() => {} + AbsoluteScaleFactorEntry::IsPos(_) if kind.is_intensity() => {} + _ => return Err(Error::DequantInvalid), + } + } + if entries.next().is_some() { + return Err(Error::DequantInvalid); + } + out.push(rescal); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + use crate::section_data::{INTENSITY_HCB, NOISE_HCB}; + + fn long_ics_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], + } + } + + // ===== inverse_quantize ===== + + #[test] + fn inverse_quantize_pins_exact_cubes() { + // |x|^(4/3) is exact when |x| is a perfect cube: + // 8 = 2^3 -> 2^4, 27 = 3^3 -> 3^4, 64 = 4^3 -> 4^4, + // 729 = 3^6 -> 3^8, 4096 = 2^12 -> 2^16. + assert_eq!(inverse_quantize(0), 0.0); + assert_eq!(inverse_quantize(1), 1.0); + assert_eq!(inverse_quantize(-1), -1.0); + assert_eq!(inverse_quantize(8), 16.0); + assert_eq!(inverse_quantize(-8), -16.0); + assert_eq!(inverse_quantize(27), 81.0); + assert_eq!(inverse_quantize(-27), -81.0); + assert_eq!(inverse_quantize(64), 256.0); + assert_eq!(inverse_quantize(729), 6561.0); + assert_eq!(inverse_quantize(4096), 65536.0); + assert_eq!(inverse_quantize(-4096), -65536.0); + } + + #[test] + fn inverse_quantize_is_odd_and_monotonic_up_to_max_quant() { + let mut prev = 0.0; + for x in 1..=8191 { + let y = inverse_quantize(x); + assert!(y > prev, "monotonic at {x}"); + assert_eq!(inverse_quantize(-x), -y, "odd symmetry at {x}"); + prev = y; + } + // 8191^(4/3) is a bit above 8191 * 8191^(1/3) ~ 164k. + assert!(prev > 160_000.0 && prev < 170_000.0); + } + + // ===== scale_factor_gain ===== + + #[test] + fn scale_factor_gain_pins_exact_powers() { + // sf = SF_OFFSET -> 1; every +4 doubles, every -4 halves. + assert_eq!(scale_factor_gain(100), 1.0); + assert_eq!(scale_factor_gain(104), 2.0); + assert_eq!(scale_factor_gain(108), 4.0); + assert_eq!(scale_factor_gain(96), 0.5); + assert_eq!(scale_factor_gain(92), 0.25); + // sf = 0 -> 2^-25; sf = 255 -> 2^38.75. + assert_eq!(scale_factor_gain(0), (-25.0f64).exp2()); + assert_eq!(scale_factor_gain(255), 38.75f64.exp2()); + // Quarter-step: sf = 101 -> 2^0.25. + assert_eq!(scale_factor_gain(101), 0.25f64.exp2()); + } + + // ===== rescale_spectrum ===== + + /// One long window, two bands on a spectrum book: band gains are + /// applied per band over the swb ranges. + #[test] + fn rescale_applies_per_band_gain_over_swb_ranges() { + // fs_index 4 long: bands 0 and 1 are 4 coefficients each. + let info = long_ics_info(2); + let sfb_cb = vec![vec![1u8, 1]]; + let mut x_quant = vec![0i32; 1024]; + x_quant[..8].copy_from_slice(&[1, -1, 0, 8, -8, 1, 0, -1]); + let spectral = SpectralData { + x_quant: vec![x_quant], + }; + // Band 0 at sf 104 (gain 2), band 1 at sf 96 (gain 0.5). + let sf = AbsoluteScaleFactors { + entries: vec![vec![ + AbsoluteScaleFactorEntry::Sf(104), + AbsoluteScaleFactorEntry::Sf(96), + ]], + }; + let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].len(), 1024); + // Band 0: x_invquant * 2. + assert_eq!(out[0][..4], [2.0, -2.0, 0.0, 32.0]); + // Band 1: x_invquant * 0.5. + assert_eq!(out[0][4..8], [-8.0, 0.5, 0.0, -0.5]); + // Above max_sfb: all zero. + assert!(out[0][8..].iter().all(|&v| v == 0.0)); + } + + #[test] + fn rescale_leaves_noise_and_intensity_bands_at_zero() { + let info = long_ics_info(3); + let sfb_cb = vec![vec![NOISE_HCB, INTENSITY_HCB, 2]]; + let mut x_quant = vec![0i32; 1024]; + // Only band 2 (coefficients 8..12) carries spectrum. + x_quant[8..12].copy_from_slice(&[1, 1, -1, 0]); + let spectral = SpectralData { + x_quant: vec![x_quant], + }; + let sf = AbsoluteScaleFactors { + entries: vec![vec![ + AbsoluteScaleFactorEntry::NoiseNrg(-50), + AbsoluteScaleFactorEntry::IsPos(3), + AbsoluteScaleFactorEntry::Sf(100), + ]], + }; + let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); + assert!(out[0][..8].iter().all(|&v| v == 0.0)); + assert_eq!(out[0][8..12], [1.0, 1.0, -1.0, 0.0]); + } + + #[test] + fn rescale_skips_zero_hcb_bands_without_consuming_entries() { + let info = long_ics_info(3); + let sfb_cb = vec![vec![ZERO_HCB, 1, ZERO_HCB]]; + let mut x_quant = vec![0i32; 1024]; + x_quant[4..8].copy_from_slice(&[1, 0, 0, -1]); + let spectral = SpectralData { + x_quant: vec![x_quant], + }; + let sf = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::Sf(108)]], + }; + let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); + assert_eq!(out[0][4..8], [4.0, 0.0, 0.0, -4.0]); + assert!(out[0][..4].iter().all(|&v| v == 0.0)); + assert!(out[0][8..].iter().all(|&v| v == 0.0)); + } + + /// EIGHT_SHORT grouping: the same gain covers all grouped short + /// windows of a virtual band (§4.6.2.3.3 "all coefficients in + /// grouped scalefactor window bands ... same scalefactor"). + #[test] + fn rescale_short_grouped_band_shares_one_gain() { + let info = IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb: 1, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups: 2, + window_group_length: vec![5, 3], + num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[4], + }; + let sfb_cb = vec![vec![1u8], vec![1u8]]; + // fs 4 short band 0 is 4 wide; virtual band = wgl * 4. + let mut g0 = vec![0i32; 5 * 128]; + for (i, slot) in g0.iter_mut().take(20).enumerate() { + *slot = if i % 2 == 0 { 1 } else { -1 }; + } + let mut g1 = vec![0i32; 3 * 128]; + for slot in g1.iter_mut().take(12) { + *slot = 8; + } + let spectral = SpectralData { + x_quant: vec![g0, g1], + }; + let sf = AbsoluteScaleFactors { + entries: vec![ + vec![AbsoluteScaleFactorEntry::Sf(104)], + vec![AbsoluteScaleFactorEntry::Sf(96)], + ], + }; + let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); + for (i, &v) in out[0].iter().take(20).enumerate() { + let want = if i % 2 == 0 { 2.0 } else { -2.0 }; + assert_eq!(v, want, "g0[{i}]"); + } + assert!(out[0][20..].iter().all(|&v| v == 0.0)); + for (i, &v) in out[1].iter().take(12).enumerate() { + assert_eq!(v, 8.0, "g1[{i}]"); + } + assert!(out[1][12..].iter().all(|&v| v == 0.0)); + } + + #[test] + fn rescale_rejects_entry_codebook_mismatch() { + let info = long_ics_info(1); + let sfb_cb = vec![vec![1u8]]; + let spectral = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + // IsPos entry against a spectrum book. + let sf = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::IsPos(0)]], + }; + assert!(matches!( + rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), + Err(Error::DequantInvalid) + )); + } + + #[test] + fn rescale_rejects_reserved_codebook_12() { + let info = long_ics_info(1); + let sfb_cb = vec![vec![12u8]]; + let spectral = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + let sf = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], + }; + assert!(matches!( + rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), + Err(Error::DequantInvalid) + )); + } + + #[test] + fn rescale_rejects_surplus_and_missing_entries() { + let info = long_ics_info(1); + let sfb_cb = vec![vec![1u8]]; + let spectral = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + let missing = AbsoluteScaleFactors { + entries: vec![vec![]], + }; + assert!(matches!( + rescale_spectrum(&spectral, &missing, &sfb_cb, &info, 4), + Err(Error::DequantInvalid) + )); + let surplus = AbsoluteScaleFactors { + entries: vec![vec![ + AbsoluteScaleFactorEntry::Sf(100), + AbsoluteScaleFactorEntry::Sf(100), + ]], + }; + assert!(matches!( + rescale_spectrum(&spectral, &surplus, &sfb_cb, &info, 4), + Err(Error::DequantInvalid) + )); + } + + #[test] + fn rescale_rejects_wrong_group_buffer_length() { + let info = long_ics_info(1); + let sfb_cb = vec![vec![1u8]]; + let spectral = SpectralData { + x_quant: vec![vec![0i32; 512]], + }; + let sf = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], + }; + assert!(matches!( + rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), + Err(Error::DequantInvalid) + )); + } + + #[test] + fn rescale_rejects_group_count_mismatch() { + let info = long_ics_info(1); + let sfb_cb = vec![vec![1u8], vec![1u8]]; + let spectral = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + let sf = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], + }; + assert!(matches!( + rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), + Err(Error::DequantInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/element_decode.rs b/crates/vendor/oxideav-aac/src/element_decode.rs new file mode 100644 index 00000000..12a86d0c --- /dev/null +++ b/crates/vendor/oxideav-aac/src/element_decode.rs @@ -0,0 +1,1482 @@ +//! Channel-element decode driver — the §4.6 block-order chain that +//! turns a parsed `single_channel_element()` (SCE / LFE) or +//! `channel_pair_element()` (CPE) into PCM-domain samples. +//! +//! Every per-tool reconstruction primitive landed in earlier rounds; +//! what was missing was the element-level glue that runs them in the +//! ISO/IEC 14496-3 §4.6 block order and carries the per-channel +//! filterbank overlap state across frames. This module is that glue. +//! +//! ## §4.6 block order +//! +//! For a single channel the per-channel chain is (§4.6, Figure 4.1 / +//! the "Decoder block diagram"): +//! +//! 1. **Noiseless decoding** — `spectral_data()` (Table 4.56), already +//! parsed into [`crate::spectral_data::SpectralData`]. +//! 2. **Pulse fix-up** (§4.6.3.3) — fold the `±pulse_amp` corrections +//! into the quantised spectrum (long windows only, Table 4.50 +//! Note 1). +//! 3. **Inverse quantisation** (§4.6.1.3) + **scalefactor application** +//! (§4.6.2.3.3) — [`crate::dequant::rescale_spectrum`] over the +//! §4.6.2.3.2-accumulated absolute scalefactors. +//! 4. **De-interleave** (§4.6.3.3 `quant_to_spec()`) — group-interleaved +//! transmission order → window-major `spec[w][k]` +//! ([`crate::decoded_spectrum::quant_to_spec`]). +//! 5. **Joint stereo / noise** (CPE only, §4.6.8 / §4.6.13) — M/S +//! de-matrix (§4.6.8.1), then intensity stereo (§4.6.8.2), then PNS +//! (§4.6.13). The spec applies these *before* TNS (§4.6.13.5: noise +//! is injected prior to the TNS step) and on the de-interleaved +//! pre-TNS spectrum, which is exactly the +//! [`crate::ms_stereo::ChannelPairSpectra`] / +//! [`crate::intensity_stereo::IntensityPairSpectra`] / +//! [`crate::pns::PnsChannel`] contract. +//! 6. **TNS** (§4.6.9) — [`crate::tns_frame::tns_decode_frame`] in +//! place on the window-major spectrum. +//! 7. **Filterbank** (§4.6.11) — IMDCT + window + inter-frame +//! overlap-add ([`crate::filterbank::Filterbank::synthesize`]), +//! emitting `LONG_WINDOW_LEN` (1024) PCM samples per channel per +//! frame. +//! +//! Because the joint-stereo / noise tools (step 5) sit *between* +//! `quant_to_spec()` and TNS, the CPE path cannot reuse the +//! single-channel [`crate::decoded_spectrum::decode_channel_spectrum`] +//! (which runs TNS internally at the end of its own chain). This module +//! therefore composes the finer-grained primitives directly: +//! [`reconstruct_pre_pair`] runs steps 2–4 for one channel, the pair +//! tools run on both pre-TNS spectra, then [`finish_channel`] runs +//! steps 6–7 per channel. +//! +//! ## Scope +//! +//! * **LTP (§4.6.7)** is wired in for long windows: [`finish_channel`] +//! runs the §4.6.7.4.1 / Figure 4.30 block order — long-term +//! synthesis (with the all-zero TNS analysis filter on `X_est`) +//! *before* the §4.6.9 TNS synthesis filter — and advances the +//! per-channel [`crate::ltp::LtpState`] reconstruction history each +//! frame. Short-window LTP and the ER AAC LD `M = N/2` lag offset +//! remain out of scope (the predictor is left off for those, per the +//! §4.6.7.1 long-window restriction). +//! * **Frequency-domain prediction (§4.6.6)** is wired in for the AAC +//! Main object type (AOT 1): [`finish_channel`] runs the +//! §4.6.6.3.2.1 backward-adaptive predictor bank +//! ([`crate::predictor::PredictorBank`]) on every long frame *before* +//! §4.6.7 LTP / §4.6.9 TNS, adding `x_est + y_rec` on the signalled +//! bands and resetting the signalled group / the whole bank on a short +//! block. The per-channel bank persists across frames so the LMS +//! coefficients keep adapting. Prediction and LTP are mutually +//! exclusive by object type (AOT 1 carries no `ltp_data`), so only one +//! predictor ever fires per channel. +//! * **SSR gain control (§4.6.12)** is wired in for the SSR object +//! type (AOT 3): [`finish_channel`] replaces the §4.6.11 filterbank +//! with the per-channel [`crate::ssr::SsrChannelDecoder`] pipeline — +//! the §4.6.12.1 four-band front-half filterbank, the §4.6.12.3 gain +//! compensation/overlap driven by the frame's `gain_control_data()`, +//! and the IPQF synthesis. Note the §4.6.12.3.3 variable per-frame +//! output length (1472 / 576 for `LONG_START` / `LONG_STOP`). +//! * PNS output is RNG-defined per §4.6.13.3 (only the per-band L2 norm +//! is spec-determined); the driver uses the default +//! [`crate::pns::gen_rand_vector`] LCG, seeded once per decoder so the +//! noise is reproducible across a decode run. + +use crate::cce::CouplingChannelElement; +use crate::decoded_spectrum::quant_to_spec; +use crate::dequant::rescale_spectrum; +use crate::filterbank::Filterbank; +use crate::ics_body::IcsBody; +use crate::ics_info::IcsInfo; +use crate::intensity_stereo::{apply_intensity_stereo, IntensityPairSpectra}; +use crate::ltp::LtpState; +use crate::ms_stereo::{apply_ms_stereo, ChannelPairSpectra, MsMaskPresent}; +use crate::pns::{apply_pns, apply_pns_pair, gen_rand_vector, PnsChannel}; +use crate::predictor::PredictorBank; +use crate::scale_factor_data::{accumulate, AbsoluteScaleFactorEntry, AbsoluteScaleFactors}; +use crate::section_data::ZERO_HCB; +use crate::spectral_data::SpectralData; +use crate::ssr::SsrChannelDecoder; +use crate::swb_offset::apply_pulse_data; +use crate::tns_frame::{tns_analysis_frame_ics, tns_decode_frame_ics}; +use crate::{Error, Result}; + +/// One channel's parsed Table 4.50 body plus its Table 4.56 spectrum, +/// bundled so the element driver can take them by reference. +#[derive(Debug)] +pub struct ChannelInput<'a> { + /// The parsed `individual_channel_stream()` body + /// ([`IcsBody::parse`] / [`IcsBody::parse_with_ics_info`]). + pub body: &'a IcsBody, + /// The channel's `ics_info()`. For an SCE / LFE or a non-shared + /// CPE this is `body.ics_info`; for a `common_window == 1` CPE this + /// is the shared `ics_info` the caller parsed once. + pub ics_info: &'a IcsInfo, + /// The channel's parsed `spectral_data()` + /// ([`SpectralData::parse`]). + pub spectral: &'a SpectralData, +} + +/// Expand a wire-order [`AbsoluteScaleFactors`] into the band-indexed +/// `track[g][sfb]` layout (size `num_window_groups × max_sfb`) the +/// §4.6.8.2 / §4.6.13 synthesis passes consume. +/// +/// `accumulate()` returns one record per non-`ZERO_HCB` band in +/// wire (low-frequency-first) order; the joint-stereo / noise tools +/// instead index by `(g, sfb)`. This walks `sfb_cb[g][sfb]` in lock-step +/// with the wire records and scatters the requested track value into the +/// `(g, sfb)` slot, leaving non-matching bands at `default`. +/// +/// `pick` maps an [`AbsoluteScaleFactorEntry`] to the track value of +/// interest (`is_pos` or `noise_nrg`), or `None` for a record that +/// belongs to a different track (in which case the slot stays +/// `default`). +fn band_indexed_track( + abs: &AbsoluteScaleFactors, + sfb_cb: &[Vec], + max_sfb: usize, + default: i32, + pick: F, +) -> Result>> +where + F: Fn(&AbsoluteScaleFactorEntry) -> Option, +{ + if abs.entries.len() != sfb_cb.len() { + return Err(Error::ElementDecodeInvalid); + } + let mut out: Vec> = Vec::with_capacity(sfb_cb.len()); + for (group_records, group_cb) in abs.entries.iter().zip(sfb_cb.iter()) { + if group_cb.len() < max_sfb { + return Err(Error::ElementDecodeInvalid); + } + let mut row = vec![default; max_sfb]; + let mut rec = group_records.iter(); + for (sfb, &cb) in group_cb.iter().enumerate() { + if cb == ZERO_HCB { + continue; + } + // Every non-ZERO_HCB band consumes exactly one wire record, + // in lock-step with the accumulate() walk. + let entry = rec.next().ok_or(Error::ElementDecodeInvalid)?; + if sfb < max_sfb { + if let Some(v) = pick(entry) { + row[sfb] = v; + } + } + } + out.push(row); + } + Ok(out) +} + +/// Band-indexed `is_pos[g][sfb]` (§4.6.8.1.4), default `0` on +/// non-intensity bands. +pub(crate) fn is_pos_table( + abs: &AbsoluteScaleFactors, + sfb_cb: &[Vec], + max_sfb: usize, +) -> Result>> { + band_indexed_track(abs, sfb_cb, max_sfb, 0, |e| match e { + AbsoluteScaleFactorEntry::IsPos(p) => Some(i32::from(*p)), + _ => None, + }) +} + +/// Band-indexed `noise_nrg[g][sfb]` (§4.6.13.3), default `0` on +/// non-noise bands. +pub(crate) fn noise_nrg_table( + abs: &AbsoluteScaleFactors, + sfb_cb: &[Vec], + max_sfb: usize, +) -> Result>> { + band_indexed_track(abs, sfb_cb, max_sfb, 0, |e| match e { + AbsoluteScaleFactorEntry::NoiseNrg(n) => Some(*n), + _ => None, + }) +} + +/// Run §4.6 steps 2–4 for one channel: pulse fix-up → scalefactor +/// accumulation → inverse quantisation + rescaling → `quant_to_spec()`. +/// +/// Returns the window-major **pre-TNS** spectrum (the joint-stereo / +/// noise tools' input) alongside the accumulated absolute scalefactors +/// (so the caller can derive the band-indexed `is_pos` / `noise_nrg` +/// tracks without re-running the accumulator). +fn reconstruct_pre_pair( + ch: &ChannelInput<'_>, + fs_index: u8, +) -> Result<(Vec, AbsoluteScaleFactors)> { + // 2. §4.6.3.3 pulse fix-up on the quantised spectrum (long windows + // only — the parser already rejects pulse on EIGHT_SHORT, and a + // long sequence has exactly one group). + let x_quant: SpectralData = if let Some(pd) = &ch.body.pulse_data { + let mut patched = ch.spectral.clone(); + let group0 = patched.x_quant.first_mut().ok_or(Error::DequantInvalid)?; + apply_pulse_data(group0, fs_index, pd)?; + patched + } else { + ch.spectral.clone() + }; + + // 3a. §4.6.2.3.2 scalefactor accumulation. + let abs = accumulate( + &ch.body.scale_factor_data, + &ch.body.section_data.sfb_cb, + ch.body.global_gain, + )?; + + // 3b. §4.6.1.3 + §4.6.2.3.3 inverse quantisation + rescaling. + let rescaled = rescale_spectrum( + &x_quant, + &abs, + &ch.body.section_data.sfb_cb, + ch.ics_info, + fs_index, + )?; + + // 4. §4.6.3.3 quant_to_spec() de-interleaving. + let spec = quant_to_spec(&rescaled, ch.ics_info, fs_index)?; + Ok((spec, abs)) +} + +/// Run the §4.6.7.4.1 / §4.6.9 / §4.6.11 tail for one channel in the +/// Figure 4.30 block order: **LTP long-term synthesis** (§4.6.7) → +/// **TNS synthesis** (§4.6.9) → **filterbank** (§4.6.11), then update +/// the per-channel LTP reconstruction history (§4.6.7.3). +/// +/// Figure 4.30 places long-term synthesis *before* the TNS synthesis +/// filter; because the transmitted residual `Y_rec` in `spec` is in the +/// noise-shaped (pre-synthesis) domain, the LTP-predicted spectrum +/// `X_est` is first passed through the matching all-zero **TNS analysis +/// filter** ([`tns_analysis_frame`]) so the `X_rec = X_est + Y_rec` add +/// is like-for-like. The single TNS synthesis pass that follows then +/// shapes the residual while undoing the analysis on the LTP +/// contribution (the §4.6.7.4.1 inverse-filter relationship). +/// +/// `ltp` is the channel's parsed [`crate::ics_info::LtpData`] (from +/// `ics_info.ltp_data` for an SCE / CPE channel 0, or `ltp_data_pair` +/// for the shared-window CPE channel 1); `None` when +/// `ltp_data_present == 0`, in which case no prediction is added but the +/// history is still advanced so it stays continuous across frames. +#[allow(clippy::too_many_arguments)] +fn finish_channel( + spec: &mut [f64], + body: &IcsBody, + ics_info: &IcsInfo, + ltp: Option<&crate::ics_info::LtpData>, + aot: u8, + fs_index: u8, + fb: &mut Filterbank, + ltp_state: &mut LtpState, + predictor_bank: &mut Option, + ssr: &mut Option>, + coupling: &[CouplingApply<'_>], +) -> Result> { + // §4.6.6 MPEG-2 frequency-domain prediction (AAC Main, AOT 1 only). + // The backward-adaptive predictor bank is run on EVERY frame so its + // coefficients keep tracking the signal statistics, whether or not + // prediction is signalled this frame; a short block resets the whole + // bank. The bank is created lazily on the first Main frame. + if aot == 1 { + let bank = match predictor_bank { + Some(b) => b, + None => { + *predictor_bank = Some(PredictorBank::new(fs_index)?); + predictor_bank.as_mut().expect("just inserted") + } + }; + bank.apply_long(spec, ics_info, ics_info.predictor_data.as_ref(), fs_index)?; + } + + // §4.6.7 long-term synthesis (long windows only). The analysis + // filter applied to X_est mirrors this frame's TNS; an order-0 / + // filter-less TNS makes tns_analysis_frame a no-op, so a channel + // without TNS gets the plain X_est + Y_rec add. + if let Some(ltp) = ltp { + let prev_shape = fb.prev_shape(); + let tns = body.tns_data.as_ref(); + ltp_state.apply_long_with_analysis(spec, ics_info, ltp, prev_shape, fs_index, |x_est| { + if let Some(tns) = tns { + tns_analysis_frame_ics(x_est, tns, ics_info, aot, fs_index)?; + } + Ok(()) + })?; + } + + // §4.6.8.3.3 dependently-switched coupling with cc_domain == 0: + // the CCE spectra are scaled and added *before* the target's TNS + // decoding. + apply_freq_coupling(spec, ics_info, fs_index, coupling, false)?; + + // §4.6.9 TNS synthesis. + if let Some(tns) = &body.tns_data { + tns_decode_frame_ics(spec, tns, ics_info, aot, fs_index)?; + } + + // §4.6.8.3.3 dependently-switched coupling with cc_domain == 1: + // scaled and added *after* the target's TNS decoding. + apply_freq_coupling(spec, ics_info, fs_index, coupling, true)?; + + // §4.6.12 — the SSR object type (AOT 3) replaces the §4.6.11 + // filterbank with the four-band gain-control pipeline: the + // §4.6.12.1 front-half filterbank (band split + 256/32-line + // IMDCTs), the §4.6.12.3 gain compensation/overlap driven by this + // frame's gain_control_data(), and the IPQF synthesis. LTP and the + // §4.6.6 predictor are other object types' tools, so the state + // advance below does not apply. + let mut out = if aot == 3 { + let dec = ssr.get_or_insert_with(Default::default); + dec.decode_frame(spec, ics_info, body.gain_control_data.as_ref())? + } else { + // §4.6.11 filterbank → PCM, then advance the LTP history with + // this frame's output and aliased IMDCT tail (§4.6.7.3). + let out = fb.synthesize(spec, ics_info)?; + ltp_state.push_frame(&out, fb.aliased_tail()); + out + }; + + // §4.6.8.3.3 independently-switched coupling: the CCE was decoded + // all the way to the time domain and is scaled and added here. + apply_time_coupling(&mut out, coupling)?; + Ok(out) +} + +/// One §4.6.8.3.3 coupling contribution addressed at a single target +/// channel: the parsed CCE (gain lists + embedded-SCE geometry), its +/// decoded embedded spectrum / time signal, and the `list_index` the +/// `decode_coupling_channel()` target walk assigned to this channel. +#[derive(Debug, Clone, Copy)] +pub struct CouplingApply<'a> { + /// The parsed `coupling_channel_element()`. + pub cce: &'a CouplingChannelElement, + /// The CCE's decoded embedded `single_channel_element()` + /// ([`CceDecoder::decode`]). + pub decoded: &'a DecodedCce, + /// The §4.6.8.3.3 `couple_channel()` gain-list index for this + /// target channel. + pub list_index: usize, +} + +/// §4.6.8.3.3 — apply every *dependently switched* coupling +/// contribution whose `cc_domain` matches `after_tns` onto the target +/// spectrum in place. +/// +/// A dependently switched CCE "must have a window state that matches +/// all of the target SCE and CPE channels" — a `window_sequence` / +/// window-group-geometry mismatch is rejected with +/// [`Error::CceInvalid`] rather than mis-addressing bands. +fn apply_freq_coupling( + spec: &mut [f64], + ics_info: &IcsInfo, + fs_index: u8, + coupling: &[CouplingApply<'_>], + after_tns: bool, +) -> Result<()> { + for c in coupling { + if c.cce.header.ind_sw_cce_flag || c.cce.header.cc_domain != after_tns { + continue; + } + let cce_ics = &c.cce.ics_info; + if cce_ics.window_sequence != ics_info.window_sequence + || cce_ics.num_window_groups != ics_info.num_window_groups + || cce_ics.window_group_length != ics_info.window_group_length + { + return Err(Error::CceInvalid); + } + let offsets = cce_ics.swb_offsets(fs_index)?; + c.cce.gains.couple_channel( + &c.decoded.spectrum, + spec, + c.list_index, + &c.cce.body.section_data.sfb_cb, + &cce_ics.window_group_length, + usize::from(cce_ics.max_sfb), + offsets, + )?; + } + Ok(()) +} + +/// §4.6.8.3.3 — apply every *independently switched* coupling +/// contribution onto the target's time signal in place. An +/// independently switched CCE only carries `common_gain_element`s, so +/// the whole frame is scaled by one `cc_gain`. +fn apply_time_coupling(out: &mut [f64], coupling: &[CouplingApply<'_>]) -> Result<()> { + for c in coupling { + if !c.cce.header.ind_sw_cce_flag { + continue; + } + let time = c.decoded.time.as_deref().ok_or(Error::CceInvalid)?; + if time.len() != out.len() { + // The SSR variable-length frames cannot take a 1024-sample + // time coupling; surface the mismatch instead of adding a + // misaligned signal. + return Err(Error::CceInvalid); + } + let cc_gain = c.cce.gains.cc_gain(c.list_index, 0, 0)?; + for (o, &t) in out.iter_mut().zip(time.iter()) { + *o += cc_gain * t; + } + } + Ok(()) +} + +/// The decoded embedded `single_channel_element()` of one CCE +/// (§4.6.8.3.3 `cc_spectrum`), ready to be coupled onto targets. +#[derive(Debug, Clone)] +pub struct DecodedCce { + /// The fully decoded spectrum (pulse → dequant → `quant_to_spec()` + /// → PNS → the CCE's *own* TNS), window-major — the §4.6.8.3.3 + /// `cc_spectrum[]` buffer a dependently switched CCE couples from. + pub spectrum: Vec, + /// The time-domain signal (through the CCE's own §4.6.11 + /// filterbank) — present only for an independently switched CCE, + /// which §4.6.8.3.3 requires to be "decoded all the way to the + /// time domain … before it is scaled and added". + pub time: Option>, +} + +/// Stateful per-CCE-slot decoder for the embedded +/// `single_channel_element()` of a `coupling_channel_element()` +/// (§4.6.8.3.3). Keyed per `element_instance_tag` by the stream +/// driver so the independently-switched filterbank overlap and the +/// PNS generator persist across frames. +#[derive(Debug, Clone)] +pub struct CceDecoder { + /// The CCE's own §4.6.11 filterbank (independently-switched CCEs + /// synthesize to the time domain with their own window state). + fb: Filterbank, + /// §4.6.13.3 generator state for noise bands in the embedded SCE. + pns_state: u32, +} + +impl Default for CceDecoder { + fn default() -> Self { + Self::new() + } +} + +impl CceDecoder { + /// A fresh CCE decoder with zeroed filterbank overlap. + #[must_use] + pub fn new() -> Self { + Self::new_family(crate::swb_offset::FrameFamily::Lc1024) + } + + /// A fresh CCE decoder for an arbitrary §4.5.1.1 frame-length + /// family. + #[must_use] + pub fn new_family(family: crate::swb_offset::FrameFamily) -> Self { + CceDecoder { + fb: Filterbank::new_family(family), + pns_state: 0x0001_2345, + } + } + + /// Decode the CCE's embedded `single_channel_element()` to the + /// §4.6.8.3.3 `cc_spectrum[]` (and, for an independently switched + /// CCE, on to the time domain through this slot's persistent + /// filterbank). + pub fn decode( + &mut self, + cce: &CouplingChannelElement, + aot: u8, + fs_index: u8, + ) -> Result { + let ch = ChannelInput { + body: &cce.body, + ics_info: &cce.ics_info, + spectral: &cce.spectral, + }; + let (mut spec, abs) = reconstruct_pre_pair(&ch, fs_index)?; + + // §4.6.13 PNS on the embedded single channel. + let max_sfb = usize::from(cce.ics_info.max_sfb); + let noise_nrg = noise_nrg_table(&abs, &cce.body.section_data.sfb_cb, max_sfb)?; + let state = &mut self.pns_state; + let mut pns_chan = PnsChannel { + spec: &mut spec, + sfb_cb: &cce.body.section_data.sfb_cb, + noise_nrg: &noise_nrg, + }; + apply_pns(&mut pns_chan, &cce.ics_info, fs_index, |out| { + gen_rand_vector(out, state) + })?; + + // The CCE's own §4.6.9 TNS (the embedded ICS is decoded like + // any other; the target's TNS relationship is what cc_domain + // selects). + if let Some(tns) = &cce.body.tns_data { + tns_decode_frame_ics(&mut spec, tns, &cce.ics_info, aot, fs_index)?; + } + + // Independently switched: decode to the time domain through + // this slot's persistent filterbank. + let time = if cce.header.ind_sw_cce_flag { + Some(self.fb.synthesize(&spec, &cce.ics_info)?) + } else { + None + }; + Ok(DecodedCce { + spectrum: spec, + time, + }) + } +} + +/// The shared `channel_pair_element()` joint-stereo header (Table 4.4) +/// the caller reads after `common_window`. +/// +/// Only meaningful when `common_window == 1`. For +/// `common_window == 0` both channels carry their own `ics_info()` and +/// no M/S mask is transmitted, so the joint-stereo tools do not run. +#[derive(Debug, Clone)] +pub struct CpeJointStereo { + /// Decoded `ms_mask_present` (§4.6.8.1.1, Table 4.4): `00` + /// all-zeros, `01` per-band `ms_used` mask, `10` all-ones; `11` is + /// reserved (the caller rejects it before constructing this). + pub ms_mask_present: MsMaskPresent, + /// `ms_used[g][sfb]` when `ms_mask_present == 01`; empty otherwise. + /// Each row must cover `max_sfb`. + pub ms_used: Vec>, +} + +impl Default for CpeJointStereo { + /// The `common_window == 0` / no-joint-stereo default: all-zeros + /// M/S mask (an identity de-matrix) and no per-band `ms_used`. + fn default() -> Self { + CpeJointStereo { + ms_mask_present: MsMaskPresent::AllZeros, + ms_used: Vec::new(), + } + } +} + +/// Stateful per-element decoder: holds one [`Filterbank`] per channel +/// slot (so the inter-frame overlap-add tail and previous-block window +/// shape persist across frames) and the PNS generator state. +/// +/// Construct one [`ElementDecoder`] per channel element of the stream +/// (one for an SCE / LFE, one for a CPE) and call [`Self::decode_sce`] +/// / [`Self::decode_cpe`] once per frame. +#[derive(Debug, Clone)] +pub struct ElementDecoder { + /// Per-channel filterbanks. `[0]` for the SCE / LFE or the CPE's + /// first channel; `[1]` for the CPE's second channel. + filterbanks: [Filterbank; 2], + /// Per-channel §4.6.7.3 LTP reconstruction history, advanced once + /// per frame (whether or not LTP fired) so the predictor buffer + /// stays continuous. Same channel-slot indexing as `filterbanks`. + ltp_states: [LtpState; 2], + /// Per-channel §4.6.6 frequency-domain predictor bank (AAC Main, + /// AOT 1). `None` until the first Main frame creates the bank for the + /// stream's sampling rate; thereafter the backward-adaptive state + /// persists and is advanced every frame. Same channel-slot indexing + /// as `filterbanks`. + predictor_banks: [Option; 2], + /// Per-channel §4.6.12 SSR pipeline (AOT 3), replacing the §4.6.11 + /// filterbank for the SSR object type. `None` until the first SSR + /// frame; thereafter the gain-control / IPQF / window-shape state + /// persists across frames. Same channel-slot indexing as + /// `filterbanks`. + ssr_decoders: [Option>; 2], + /// §4.6.13.3 default generator state, advanced across every noise + /// band of every frame so the noise is reproducible per decode run. + pns_state: u32, +} + +impl Default for ElementDecoder { + fn default() -> Self { + Self::new() + } +} + +impl ElementDecoder { + /// A fresh element decoder with zeroed filterbank overlap and a + /// fixed PNS generator seed. + pub fn new() -> Self { + Self::new_family(crate::swb_offset::FrameFamily::Lc1024) + } + + /// A fresh element decoder whose per-channel filterbank and LTP + /// state run an arbitrary §4.5.1.1 frame-length family. + pub fn new_family(family: crate::swb_offset::FrameFamily) -> Self { + ElementDecoder { + filterbanks: [ + Filterbank::new_family(family), + Filterbank::new_family(family), + ], + ltp_states: [LtpState::new_family(family), LtpState::new_family(family)], + predictor_banks: [None, None], + ssr_decoders: [None, None], + // Any non-zero seed yields a non-degenerate sequence; the + // §4.6.13.3 normalisation makes the per-band energy + // independent of the seed, so this choice only fixes the + // (spec-undefined) per-coefficient phase. + pns_state: 0x0001_2345, + } + } + + /// A fresh element decoder with an explicit PNS generator seed. + /// Per §4.6.13.3 the seed only affects the noise *phase*, not the + /// (spec-determined) per-band energy. + pub fn with_pns_seed(seed: u32) -> Self { + ElementDecoder { + filterbanks: [Filterbank::new(), Filterbank::new()], + ltp_states: [LtpState::new(), LtpState::new()], + predictor_banks: [None, None], + ssr_decoders: [None, None], + pns_state: seed, + } + } + + /// Decode one single-channel element (SCE) or LFE channel to PCM. + /// + /// Runs the full §4.6 single-channel chain (pulse → dequant → + /// `quant_to_spec()` → PNS → TNS → filterbank). M/S and intensity + /// stereo are channel-*pair* tools and do not apply to an SCE; PNS + /// (§4.6.13) does, so a single-channel noise band is synthesised + /// here. + /// + /// Returns `LONG_WINDOW_LEN` (1024) PCM-domain samples for the + /// frame. + pub fn decode_sce(&mut self, ch: &ChannelInput<'_>, aot: u8, fs_index: u8) -> Result> { + self.decode_sce_coupled(ch, aot, fs_index, &[]) + } + + /// [`Self::decode_sce`] with §4.6.8.3.3 coupling contributions: + /// each [`CouplingApply`] is scaled and added at its signalled + /// stage (before / after TNS for a dependently switched CCE, on + /// the time signal for an independently switched one). + pub fn decode_sce_coupled( + &mut self, + ch: &ChannelInput<'_>, + aot: u8, + fs_index: u8, + coupling: &[CouplingApply<'_>], + ) -> Result> { + let (mut spec, abs) = reconstruct_pre_pair(ch, fs_index)?; + let max_sfb = ch.ics_info.max_sfb as usize; + + // §4.6.13 PNS on the single channel (no pair correlation). + let noise_nrg = noise_nrg_table(&abs, &ch.body.section_data.sfb_cb, max_sfb)?; + let state = &mut self.pns_state; + let mut pns_chan = PnsChannel { + spec: &mut spec, + sfb_cb: &ch.body.section_data.sfb_cb, + noise_nrg: &noise_nrg, + }; + apply_pns(&mut pns_chan, ch.ics_info, fs_index, |out| { + gen_rand_vector(out, state) + })?; + + let ltp = ltp_for_channel(ch.ics_info, false); + finish_channel( + &mut spec, + ch.body, + ch.ics_info, + ltp, + aot, + fs_index, + &mut self.filterbanks[0], + &mut self.ltp_states[0], + &mut self.predictor_banks[0], + &mut self.ssr_decoders[0], + coupling, + ) + } + + /// Decode one channel-pair element (CPE) to a `(left, right)` pair + /// of PCM frames. + /// + /// * `left` / `right` — the two channels' parsed bodies + spectra. + /// For the shared-info form both [`ChannelInput::ics_info`] point + /// at the same shared `ics_info`. + /// * `joint` — the Table 4.4 joint-stereo header + /// ([`CpeJointStereo`]); pass [`CpeJointStereo::default`] (mask + /// all-zeros, no `ms_used`) for a `common_window == 0` pair, where + /// no joint-stereo tools run. + /// + /// Runs the full §4.6 chain with the joint-stereo / noise tools in + /// block order: per-channel pulse → dequant → `quant_to_spec()`, + /// then M/S (§4.6.8.1) → intensity (§4.6.8.2) → PNS (§4.6.13) on the + /// pre-TNS pair, then per-channel TNS (§4.6.9) → filterbank + /// (§4.6.11). + /// + /// Both channels must share `window_sequence` (the `common_window` + /// geometry the §4.6.8 tools require) when any joint-stereo tool is + /// active; a mismatch surfaces as [`Error::ElementDecodeInvalid`]. + pub fn decode_cpe( + &mut self, + left: &ChannelInput<'_>, + right: &ChannelInput<'_>, + joint: &CpeJointStereo, + aot: u8, + fs_index: u8, + ) -> Result<(Vec, Vec)> { + self.decode_cpe_coupled(left, right, joint, aot, fs_index, &[], &[]) + } + + /// [`Self::decode_cpe`] with §4.6.8.3.3 coupling contributions, + /// one list per target channel (`cc_l` / `cc_r` and the shared + /// Table 4.153 layout decide which lists the stream driver builds + /// for each side). + #[allow(clippy::too_many_arguments)] + pub fn decode_cpe_coupled( + &mut self, + left: &ChannelInput<'_>, + right: &ChannelInput<'_>, + joint: &CpeJointStereo, + aot: u8, + fs_index: u8, + left_coupling: &[CouplingApply<'_>], + right_coupling: &[CouplingApply<'_>], + ) -> Result<(Vec, Vec)> { + // The §4.6.8 joint-stereo tools de-matrix the two channels + // band-for-band, so they require a shared window geometry. The + // shared-info CPE form guarantees this; reject a mismatch the + // non-shared form might present. + if left.ics_info.window_sequence != right.ics_info.window_sequence + || left.ics_info.num_window_groups != right.ics_info.num_window_groups + || left.ics_info.window_group_length != right.ics_info.window_group_length + { + return Err(Error::ElementDecodeInvalid); + } + // The joint-stereo geometry keys off the shared (here: left) + // ics_info's max_sfb; the pair tools validate both channels' + // sfb_cb against it. + let geom = left.ics_info; + let max_sfb = geom.max_sfb as usize; + + let (mut left_spec, left_abs) = reconstruct_pre_pair(left, fs_index)?; + let (mut right_spec, right_abs) = reconstruct_pre_pair(right, fs_index)?; + + // §4.6.8.1 M/S de-matrix (suppressed on intensity / noise bands + // by apply_ms_stereo itself). + let ms_used_slice: &[Vec] = if joint.ms_mask_present == MsMaskPresent::Mask { + validate_ms_used(&joint.ms_used, geom)?; + &joint.ms_used + } else { + &[] + }; + { + let mut pair = ChannelPairSpectra { + left: &mut left_spec, + right: &mut right_spec, + left_sfb_cb: &left.body.section_data.sfb_cb, + right_sfb_cb: &right.body.section_data.sfb_cb, + }; + apply_ms_stereo( + &mut pair, + joint.ms_mask_present, + ms_used_slice, + geom, + fs_index, + )?; + } + + // §4.6.8.2 intensity stereo: right derived from left on + // intensity bands. invert_intensity reads the per-band M/S mask + // only when ms_mask_present == 01 (Mask). + let right_is_pos = is_pos_table(&right_abs, &right.body.section_data.sfb_cb, max_sfb)?; + let is_mask = joint.ms_mask_present == MsMaskPresent::Mask; + { + let mut pair = IntensityPairSpectra { + left: &left_spec, + right: &mut right_spec, + right_sfb_cb: &right.body.section_data.sfb_cb, + is_pos: &right_is_pos, + }; + apply_intensity_stereo(&mut pair, is_mask, ms_used_slice, geom, fs_index)?; + } + + // §4.6.13 PNS with the shared-vector correlation rule. PNS and + // M/S are mutually exclusive per band (§4.6.13.5), so a noise + // band was skipped by the M/S de-matrix above; here it is filled. + let left_nrg = noise_nrg_table(&left_abs, &left.body.section_data.sfb_cb, max_sfb)?; + let right_nrg = noise_nrg_table(&right_abs, &right.body.section_data.sfb_cb, max_sfb)?; + let all_shared = joint.ms_mask_present == MsMaskPresent::AllOnes; + { + let mut left_chan = PnsChannel { + spec: &mut left_spec, + sfb_cb: &left.body.section_data.sfb_cb, + noise_nrg: &left_nrg, + }; + let mut right_chan = PnsChannel { + spec: &mut right_spec, + sfb_cb: &right.body.section_data.sfb_cb, + noise_nrg: &right_nrg, + }; + let state = &mut self.pns_state; + apply_pns_pair( + &mut left_chan, + &mut right_chan, + is_mask, + all_shared, + ms_used_slice, + geom, + fs_index, + |out| gen_rand_vector(out, state), + )?; + } + + // §4.6.7 LTP + §4.6.9 TNS + §4.6.11 filterbank, per channel. + // Channel 0 reads the first ltp_data; channel 1 of a shared- + // window CPE reads ltp_data_pair (the second ltp_data_present + // subtree, Table 4.4), falling back to its own ltp_data in the + // non-shared form where each channel carries separate side info. + let left_ltp = ltp_for_channel(left.ics_info, false); + let right_ltp = ltp_for_channel(right.ics_info, true); + let out_left = finish_channel( + &mut left_spec, + left.body, + left.ics_info, + left_ltp, + aot, + fs_index, + &mut self.filterbanks[0], + &mut self.ltp_states[0], + &mut self.predictor_banks[0], + &mut self.ssr_decoders[0], + left_coupling, + )?; + let out_right = finish_channel( + &mut right_spec, + right.body, + right.ics_info, + right_ltp, + aot, + fs_index, + &mut self.filterbanks[1], + &mut self.ltp_states[1], + &mut self.predictor_banks[1], + &mut self.ssr_decoders[1], + right_coupling, + )?; + Ok((out_left, out_right)) + } +} + +/// Select the parsed §4.6.7.2 [`crate::ics_info::LtpData`] that drives +/// one channel's long-term prediction, or `None` when LTP is off for +/// that channel this frame (`ltp_data_present == 0`). +/// +/// * `is_pair_slot == false` (SCE, CPE channel 0) reads the primary +/// `ltp_data` subtree. +/// * `is_pair_slot == true` (CPE channel 1) reads the second +/// `ltp_data_pair` subtree carried after `common_window == 1` +/// (Table 4.4). In the non-shared CPE form the second channel parses +/// its own `ics_info()` with the side info in `ltp_data` and +/// `ltp_data_pair == None`; the fall-through keeps that case working. +fn ltp_for_channel(ics_info: &IcsInfo, is_pair_slot: bool) -> Option<&crate::ics_info::LtpData> { + if is_pair_slot { + if let Some(pair) = ics_info.ltp_data_pair.as_ref() { + return Some(pair); + } + } + ics_info.ltp_data.as_ref() +} + +/// Validate that an `ms_used[g][sfb]` mask covers +/// `num_window_groups × max_sfb`. The pair tools re-check this, but +/// surfacing the element-level [`Error::ElementDecodeInvalid`] gives the +/// caller a single, element-scoped failure mode. +fn validate_ms_used(ms_used: &[Vec], ics_info: &IcsInfo) -> Result<()> { + let num_groups = ics_info.num_window_groups as usize; + let max_sfb = ics_info.max_sfb as usize; + if ms_used.len() != num_groups { + return Err(Error::ElementDecodeInvalid); + } + for row in ms_used { + if row.len() < max_sfb { + return Err(Error::ElementDecodeInvalid); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + use crate::scale_factor_data::{ScaleFactorData, ScaleFactorEntry}; + use crate::section_data::{Section, SectionData, INTENSITY_HCB, NOISE_HCB}; + + // ---- band-indexed track expansion ---- + + fn sfb_cb_one_group(cbs: &[u8]) -> Vec> { + vec![cbs.to_vec()] + } + + #[test] + fn band_indexed_track_scatters_by_wire_order() { + // Group 0: bands [ZERO, INTENSITY_HCB, NOISE_HCB, spectrum=2]. + // Wire records skip ZERO; so records are + // [IsPos, NoiseNrg, Sf] for sfb 1, 2, 3. + let sfb_cb = sfb_cb_one_group(&[ZERO_HCB, INTENSITY_HCB, NOISE_HCB, 2]); + let abs = AbsoluteScaleFactors { + entries: vec![vec![ + AbsoluteScaleFactorEntry::IsPos(7), + AbsoluteScaleFactorEntry::NoiseNrg(42), + AbsoluteScaleFactorEntry::Sf(120), + ]], + }; + let is_pos = is_pos_table(&abs, &sfb_cb, 4).unwrap(); + assert_eq!(is_pos[0], vec![0, 7, 0, 0]); + let nrg = noise_nrg_table(&abs, &sfb_cb, 4).unwrap(); + assert_eq!(nrg[0], vec![0, 0, 42, 0]); + } + + #[test] + fn band_indexed_track_rejects_record_shortfall() { + // Two non-ZERO bands but only one wire record. + let sfb_cb = sfb_cb_one_group(&[INTENSITY_HCB, NOISE_HCB]); + let abs = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::IsPos(1)]], + }; + assert!(matches!( + is_pos_table(&abs, &sfb_cb, 2), + Err(Error::ElementDecodeInvalid) + )); + } + + #[test] + fn band_indexed_track_rejects_group_count_mismatch() { + let sfb_cb = vec![vec![2u8], vec![2u8]]; + let abs = AbsoluteScaleFactors { + entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], + }; + assert!(matches!( + noise_nrg_table(&abs, &sfb_cb, 1), + Err(Error::ElementDecodeInvalid) + )); + } + + // ---- end-to-end element decode ---- + + fn long_ics_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], + } + } + + /// Build a minimal single-group long-window channel body whose + /// `section_data` assigns codebook `cb` to bands `0..max_sfb` and + /// whose `scale_factor_data` carries one DPCM record per non-ZERO + /// band. No pulse / TNS / gain-control tools. + fn make_body(max_sfb: u8, cb: u8, sf_deltas: &[i16]) -> IcsBody { + let sfb_cb = vec![vec![cb; max_sfb as usize]]; + let sections = vec![vec![Section { + codebook: cb, + start: 0, + end: max_sfb, + }]]; + let section_data = SectionData { sections, sfb_cb }; + // For a NOISE_HCB / INTENSITY band the record variant differs; + // make_body is only used with spectrum books (Dpcm) and the + // single-noise-band case below, where the first record is the + // 9-bit PNS PCM seed. + let entries: Vec = if cb == NOISE_HCB { + // The first noise band of the frame carries the 9-bit PCM + // seed; later noise bands carry Huffman DPCM deltas. + sf_deltas + .iter() + .enumerate() + .map(|(i, &d)| { + if i == 0 { + ScaleFactorEntry::NoisePcm(d as u16) + } else { + ScaleFactorEntry::NoiseDpcm(d as i8) + } + }) + .collect() + } else { + sf_deltas + .iter() + .map(|&d| ScaleFactorEntry::Dpcm(d as i8)) + .collect() + }; + let scale_factor_data = ScaleFactorData { + entries: vec![entries], + }; + IcsBody { + global_gain: 100, + ics_info: Some(long_ics_info(max_sfb)), + section_data, + scale_factor_data, + pulse_data_present: false, + pulse_data: None, + tns_data_present: false, + tns_data: None, + gain_control_data_present: false, + gain_control_data: None, + spectral_data_bit_offset: 0, + er_scale_factor_data: None, + reordered_spectral_lengths: None, + } + } + + /// A spectral-data block with `value` in every coefficient of bands + /// `0..max_sfb` (long window, fs_index 4: bands are 4 wide at the + /// low end). Just fills the full 1024-coefficient group buffer. + fn make_spectral(value: i32) -> SpectralData { + SpectralData { + x_quant: vec![vec![value; 1024]], + } + } + + #[test] + fn decode_sce_produces_finite_pcm() { + let body = make_body(4, 2, &[0, 0, 0, 0]); + let ics = body.ics_info.clone().unwrap(); + let spectral = make_spectral(3); + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + let mut dec = ElementDecoder::new(); + let pcm = dec.decode_sce(&ch, 2, 4).unwrap(); + assert_eq!(pcm.len(), 1024); + assert!(pcm.iter().all(|v| v.is_finite())); + // The first frame overlaps against a zero tail, so the right + // half of the windowed block is folded into the next frame. + // A constant non-zero spectrum yields non-silent PCM. + assert!(pcm.iter().any(|&v| v != 0.0)); + } + + #[test] + fn decode_sce_overlap_couples_frames() { + let body = make_body(4, 2, &[0, 0, 0, 0]); + let ics = body.ics_info.clone().unwrap(); + let spectral = make_spectral(3); + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + let mut dec = ElementDecoder::new(); + let f0 = dec.decode_sce(&ch, 2, 4).unwrap(); + let f1 = dec.decode_sce(&ch, 2, 4).unwrap(); + // The second frame carries the first frame's overlap tail, so + // for identical input the two frames differ only by the + // (now non-zero) overlap contribution at frame start. + assert_ne!(f0, f1); + } + + // ---- SSR (AOT 3) §4.6.12 routing ---- + + /// AOT 3 routes the channel through the §4.6.12 SSR pipeline + /// instead of the §4.6.11 filterbank: same body/spectrum, different + /// synthesis, and the SSR output is exactly what a hand-driven + /// [`SsrChannelDecoder`] produces from the same decoded spectrum — + /// frame after frame (state threads). + #[test] + fn decode_sce_ssr_matches_direct_pipeline_and_threads_state() { + let body = make_body(4, 2, &[0, 0, 0, 0]); + let ics = body.ics_info.clone().unwrap(); + let spectral = make_spectral(3); + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + let mut dec = ElementDecoder::new(); + let mut lc = ElementDecoder::new(); + let mut direct = SsrChannelDecoder::new(); + for frame in 0..3 { + let f_ssr = dec.decode_sce(&ch, 3, 4).unwrap(); + assert_eq!(f_ssr.len(), 1024); + assert!(f_ssr.iter().all(|v| v.is_finite())); + // Bit-identical to the direct §4.6.12 pipeline on the same + // decoded (post-TNS) spectrum. + let (spec, _) = reconstruct_pre_pair(&ch, 4).unwrap(); + let expect = direct.decode_frame(&spec, &ics, None).unwrap(); + assert_eq!(f_ssr, expect, "frame {frame}"); + // …and different from the §4.6.11 LC synthesis. + let f_lc = lc.decode_sce(&ch, 2, 4).unwrap(); + assert_ne!(f_lc, f_ssr, "frame {frame}"); + } + } + + /// A frame carrying `gain_control_data()` decodes through the + /// §4.6.12.3 gain compensation: its PCM differs from the same + /// frame without the ladder. + #[test] + fn decode_sce_ssr_gain_control_data_changes_output() { + use crate::gain_control_data::{GainAdjust, GainBand, GainControlData, GainWindow}; + // 40 active scalefactor bands so the spectrum reaches well past + // coefficient 256 — PQF band 1 (the gain-controlled one) must + // carry signal for the ladder to matter. + let plain = make_body(40, 2, &[0; 40]); + let mut gained = make_body(40, 2, &[0; 40]); + gained.gain_control_data_present = true; + gained.gain_control_data = Some(GainControlData { + max_band: 1, + bands: vec![GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 7, // AdjLev = 3 ⇒ ALEV = 8. + aloccode: 0, + }], + }], + }], + }); + let ics = plain.ics_info.clone().unwrap(); + let spectral = make_spectral(3); + let ch_plain = ChannelInput { + body: &plain, + ics_info: &ics, + spectral: &spectral, + }; + let ch_gained = ChannelInput { + body: &gained, + ics_info: &ics, + spectral: &spectral, + }; + let mut a = ElementDecoder::new(); + let mut b = ElementDecoder::new(); + let fa = a.decode_sce(&ch_plain, 3, 4).unwrap(); + let fb = b.decode_sce(&ch_gained, 3, 4).unwrap(); + assert_eq!(fa.len(), fb.len()); + assert_ne!(fa, fb, "gain ladder must alter the SSR synthesis"); + } + + /// A CPE decodes both channels through per-slot SSR pipelines. + #[test] + fn decode_cpe_ssr_both_channels() { + let left_body = make_body(4, 2, &[0, 0, 0, 0]); + let right_body = make_body(4, 2, &[0, 0, 0, 0]); + let ics = left_body.ics_info.clone().unwrap(); + let left_spec = make_spectral(5); + let right_spec = make_spectral(2); + let left = ChannelInput { + body: &left_body, + ics_info: &ics, + spectral: &left_spec, + }; + let right = ChannelInput { + body: &right_body, + ics_info: &ics, + spectral: &right_spec, + }; + let mut dec = ElementDecoder::new(); + let (l, r) = dec + .decode_cpe(&left, &right, &CpeJointStereo::default(), 3, 4) + .unwrap(); + assert_eq!(l.len(), 1024); + assert_eq!(r.len(), 1024); + assert!(l.iter().chain(r.iter()).all(|v| v.is_finite())); + assert_ne!(l, r); + } + + #[test] + fn decode_cpe_ms_reconstructs_left_right() { + // common_window: shared ics_info. Channel 0 = mid, channel 1 = + // side; ms_mask_present = all-ones (10). With a constant + // spectrum m, s the de-matrix gives l = m + s, r = m - s. + let left_body = make_body(4, 2, &[0, 0, 0, 0]); + let right_body = make_body(4, 2, &[0, 0, 0, 0]); + let ics = left_body.ics_info.clone().unwrap(); + let left_spec = make_spectral(5); + let right_spec = make_spectral(2); + let left = ChannelInput { + body: &left_body, + ics_info: &ics, + spectral: &left_spec, + }; + let right = ChannelInput { + body: &right_body, + ics_info: &ics, + spectral: &right_spec, + }; + let joint = CpeJointStereo { + ms_mask_present: MsMaskPresent::AllOnes, + ms_used: vec![], + }; + let mut dec = ElementDecoder::new(); + let (l, r) = dec.decode_cpe(&left, &right, &joint, 2, 4).unwrap(); + assert_eq!(l.len(), 1024); + assert_eq!(r.len(), 1024); + assert!(l.iter().all(|v| v.is_finite())); + assert!(r.iter().all(|v| v.is_finite())); + // The reconstructed channels differ (l = m+s, r = m-s with + // s != 0), so the PCM frames are not identical. + assert_ne!(l, r); + } + + #[test] + fn decode_cpe_mask_off_is_independent_channels() { + // ms_mask_present = all-zeros: M/S is a no-op, each channel + // passes through independently. + let left_body = make_body(4, 2, &[0, 0, 0, 0]); + let right_body = make_body(4, 2, &[0, 0, 0, 0]); + let ics = left_body.ics_info.clone().unwrap(); + let same = make_spectral(4); + let left = ChannelInput { + body: &left_body, + ics_info: &ics, + spectral: &same, + }; + let right = ChannelInput { + body: &right_body, + ics_info: &ics, + spectral: &same, + }; + let joint = CpeJointStereo::default(); + let mut dec = ElementDecoder::new(); + let (l, r) = dec.decode_cpe(&left, &right, &joint, 2, 4).unwrap(); + // Identical input, identical (independent) filterbanks → equal. + assert_eq!(l, r); + } + + #[test] + fn decode_cpe_rejects_window_sequence_mismatch() { + let left_body = make_body(4, 2, &[0, 0, 0, 0]); + let mut right_body = make_body(4, 2, &[0, 0, 0, 0]); + // Give the right channel a different window sequence. + let mut right_ics = right_body.ics_info.clone().unwrap(); + right_ics.window_sequence = WindowSequence::LongStop; + right_body.ics_info = Some(right_ics.clone()); + let left_ics = left_body.ics_info.clone().unwrap(); + let left_spec = make_spectral(1); + let right_spec = make_spectral(1); + let left = ChannelInput { + body: &left_body, + ics_info: &left_ics, + spectral: &left_spec, + }; + let right = ChannelInput { + body: &right_body, + ics_info: &right_ics, + spectral: &right_spec, + }; + let joint = CpeJointStereo::default(); + let mut dec = ElementDecoder::new(); + assert!(matches!( + dec.decode_cpe(&left, &right, &joint, 2, 4), + Err(Error::ElementDecodeInvalid) + )); + } + + #[test] + fn decode_sce_synthesizes_noise_band() { + // A NOISE_HCB band carries no spectrum (silence on entry); PNS + // fills it to the §4.6.13.3 target norm. With one noise band the + // decoded PCM must be non-silent. + let body = make_body(4, NOISE_HCB, &[10, 0, 0, 0]); + let ics = body.ics_info.clone().unwrap(); + // Noise bands carry no x_quant (spectrum-less); leave zeros. + let spectral = make_spectral(0); + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + let mut dec = ElementDecoder::new(); + let pcm = dec.decode_sce(&ch, 2, 4).unwrap(); + assert!(pcm.iter().all(|v| v.is_finite())); + assert!( + pcm.iter().any(|&v| v != 0.0), + "PNS-filled noise band should produce non-silent PCM" + ); + } + + // ---- §4.6.7.4.1 LTP wiring ---- + + use crate::ics_info::LtpData; + + /// Attach long-window LTP side info to a body's `ics_info`: the + /// `ltp_data_present` flag plus an `ltp_data` carrying `coef` / `lag` + /// and `long_used` bands. + fn with_ltp(mut body: IcsBody, coef: u8, lag: u16, long_used: Vec) -> IcsBody { + let mut ics = body.ics_info.clone().unwrap(); + ics.ltp_data_present = true; + ics.ltp_data = Some(LtpData { + lag_update: None, + lag: Some(lag), + coef, + long_used, + short: None, + }); + body.ics_info = Some(ics); + body + } + + #[test] + fn ltp_off_first_frame_zero_history_matches_no_ltp() { + // §4.6.7.3 init: with all-zero history the predictor is zero, so + // an LTP-flagged first frame must decode identically to one with + // LTP off (X_est == 0 ⇒ X_rec == Y_rec). + let plain = make_body(4, 2, &[0, 0, 0, 0]); + let ltp_body = with_ltp(make_body(4, 2, &[0, 0, 0, 0]), 7, 50, vec![true; 4]); + let spectral = make_spectral(3); + + let p_ics = plain.ics_info.clone().unwrap(); + let l_ics = ltp_body.ics_info.clone().unwrap(); + let plain_ch = ChannelInput { + body: &plain, + ics_info: &p_ics, + spectral: &spectral, + }; + let ltp_ch = ChannelInput { + body: <p_body, + ics_info: &l_ics, + spectral: &spectral, + }; + let f_plain = ElementDecoder::new().decode_sce(&plain_ch, 2, 4).unwrap(); + let f_ltp = ElementDecoder::new().decode_sce(<p_ch, 2, 4).unwrap(); + for (a, b) in f_plain.iter().zip(f_ltp.iter()) { + assert!((a - b).abs() < 1e-12, "first-frame LTP add must be zero"); + } + } + + #[test] + fn ltp_fires_on_second_frame_and_diverges() { + // After a non-silent first frame seeds the §4.6.7.3 history, the + // second frame's predictor is non-zero on the flagged bands, so + // an LTP-active decoder diverges from an LTP-off one — proof the + // driver wires predict() → MDCT → add into the chain. + let plain = make_body(4, 2, &[0, 0, 0, 0]); + let ltp_body = with_ltp(make_body(4, 2, &[0, 0, 0, 0]), 5, 30, vec![true; 4]); + let spectral = make_spectral(4); + let p_ics = plain.ics_info.clone().unwrap(); + let l_ics = ltp_body.ics_info.clone().unwrap(); + let plain_ch = ChannelInput { + body: &plain, + ics_info: &p_ics, + spectral: &spectral, + }; + let ltp_ch = ChannelInput { + body: <p_body, + ics_info: &l_ics, + spectral: &spectral, + }; + + let mut dec_plain = ElementDecoder::new(); + let mut dec_ltp = ElementDecoder::new(); + // Frame 0 — identical (zero history). + let _ = dec_plain.decode_sce(&plain_ch, 2, 4).unwrap(); + let _ = dec_ltp.decode_sce(<p_ch, 2, 4).unwrap(); + // Frame 1 — LTP now has non-zero history to predict from. + let f1_plain = dec_plain.decode_sce(&plain_ch, 2, 4).unwrap(); + let f1_ltp = dec_ltp.decode_sce(<p_ch, 2, 4).unwrap(); + assert!(f1_ltp.iter().all(|v| v.is_finite())); + let diff = f1_plain + .iter() + .zip(f1_ltp.iter()) + .any(|(a, b)| (a - b).abs() > 1e-9); + assert!(diff, "second-frame LTP should change the output"); + } + + #[test] + fn ltp_with_tns_stays_finite() { + // LTP active on a TNS-carrying channel exercises the + // §4.6.7.4.1 analysis-filter-in-loop path; the decode must stay + // finite across two frames. + use crate::tns_data::{TnsData, TnsFilter, TnsWindow}; + let mut body = with_ltp(make_body(20, 2, &[0i16; 20]), 4, 64, vec![true; 20]); + body.tns_data_present = true; + body.tns_data = Some(TnsData { + windows: vec![TnsWindow { + coef_res: false, + filters: vec![TnsFilter { + length: 10, + order: 3, + direction: false, + coef_compress: false, + coef: vec![1, 7, 2], + }], + }], + }); + let ics = body.ics_info.clone().unwrap(); + let spectral = make_spectral(3); + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + let mut dec = ElementDecoder::new(); + let f0 = dec.decode_sce(&ch, 2, 4).unwrap(); + let f1 = dec.decode_sce(&ch, 2, 4).unwrap(); + assert!(f0.iter().all(|v| v.is_finite())); + assert!(f1.iter().all(|v| v.is_finite())); + // Second frame predicts from a seeded history → not identical. + assert_ne!(f0, f1); + } + + /// Attach a §4.6.6 Main `predictor_data()` to a channel body's + /// `ics_info`, enabling prediction on bands `0..max_sfb`. + fn with_main_prediction(mut body: IcsBody, max_sfb: u8) -> IcsBody { + use crate::ics_info::PredictorData; + let ics = body.ics_info.as_mut().unwrap(); + ics.predictor_data_present = true; + ics.predictor_data = Some(PredictorData { + reset: false, + reset_group_number: None, + prediction_used: vec![true; max_sfb as usize], + }); + body + } + + #[test] + fn decode_sce_main_aot_runs_predictor() { + // AOT 1 (Main) with predictor_data_present must run the §4.6.6 + // backward-adaptive bank; decode must stay finite across frames + // and the predictor state must build up so successive frames + // diverge from the AOT-2 (LC, no predictor) decode of the same + // input. + let body = with_main_prediction(make_body(20, 2, &[0i16; 20]), 20); + let ics = body.ics_info.clone().unwrap(); + let spectral = make_spectral(3); + let ch = ChannelInput { + body: &body, + ics_info: &ics, + spectral: &spectral, + }; + + // Main (AOT 1): the predictor bank fires. + let mut main_dec = ElementDecoder::new(); + let mut main_frames = Vec::new(); + for _ in 0..6 { + let f = main_dec.decode_sce(&ch, 1, 4).unwrap(); + assert!(f.iter().all(|v| v.is_finite())); + main_frames.push(f); + } + + // LC (AOT 2): no §4.6.6 predictor, same input. + let lc_body = make_body(20, 2, &[0i16; 20]); + let lc_ics = lc_body.ics_info.clone().unwrap(); + let lc_ch = ChannelInput { + body: &lc_body, + ics_info: &lc_ics, + spectral: &spectral, + }; + let mut lc_dec = ElementDecoder::new(); + let mut lc_frames = Vec::new(); + for _ in 0..6 { + lc_frames.push(lc_dec.decode_sce(&lc_ch, 2, 4).unwrap()); + } + + // Once the lattice has adapted, the predicted spectrum diverges + // from the un-predicted one, so the late Main frames differ from + // their LC counterparts. + assert_ne!( + main_frames.last().unwrap(), + lc_frames.last().unwrap(), + "Main predictor produced no spectral change" + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/encoder.rs b/crates/vendor/oxideav-aac/src/encoder.rs new file mode 100644 index 00000000..807b3dcc --- /dev/null +++ b/crates/vendor/oxideav-aac/src/encoder.rs @@ -0,0 +1,2991 @@ +//! End-to-end AAC-LC encoder — ISO/IEC 14496-3 §4.5/§4.6 written +//! forward. +//! +//! This module drives the crate's Phase-2 bit-exact wire writers +//! ([`crate::ics_body::IcsBody::write`], +//! [`crate::section_data::SectionData::write`], +//! [`crate::scale_factor_data::ScaleFactorData::write`], +//! [`crate::spectral_data::SpectralData::write`], +//! [`crate::raw_data_block::FrameAssembler`], +//! [`crate::adts::AdtsHeader::write`]) from PCM input, producing an +//! ADTS stream the crate's own [`crate::decode::StreamDecoder`] +//! round-trips. +//! +//! ## What the wire format fixes vs. what the encoder chooses +//! +//! ISO/IEC 14496-3 normatively defines the *decoder*: the §4.6.2 +//! inverse quantizer `x = sign(q)·|q|^(4/3)·2^(0.25·(sf−100))`, the +//! §4.6.3 noiseless coding, and the §4.6.11 filterbank. Everything on +//! the analysis side — the psychoacoustic model, the +//! scalefactor/quantizer search, the codebook choice — is an encoder +//! degree of freedom; any choice that yields conforming syntax is a +//! conforming encoder. The choices here are deliberately simple and +//! fully derived from the normative decoder equations: +//! +//! * **Window decision (block switching, §4.6.11.3.2)** — an +//! energy-jump transient detector on each incoming hop drives the +//! `ONLY_LONG → LONG_START → EIGHT_SHORT → LONG_STOP` state +//! machine: a 128-sample subblock whose energy jumps ≥12× over the +//! running average of its predecessors (above an absolute floor) +//! marks the hop transient; the frame *before* the transient hop +//! becomes `LONG_START`, the transient hop's frame `EIGHT_SHORT` +//! (extended while transients continue), and the run exits through +//! `LONG_STOP`. All windows are the §4.6.11.3.2 sine shape. Within +//! an `EIGHT_SHORT` frame the §4.5.2.3.4 `scale_factor_grouping` +//! decision ([`decide_short_grouping`]) merges envelope-alike +//! adjacent windows into shared window groups (one scalefactor / +//! section track per group, §4.5.2.3.5 interleaved transmission +//! order) — the attack window's energy jump keeps it in its own +//! group. +//! * **Analysis filterbank** — the §4.6.11.3.1 forward MDCT (the +//! transform whose windowed overlap-add against the decoder's IMDCT +//! is unity — the same [`crate::filterbank::forward_mdct`] the +//! §4.6.7 LTP loop uses). Long frames run one 2048-point transform +//! under the sequence's composite window; `EIGHT_SHORT` frames run +//! eight 256-point transforms at offsets `448 + j·128` within the +//! window region. Frame `f` covers input samples +//! `[f·1024 − 1024, f·1024 + 1024)`; the leading frame is primed +//! with zeros, giving the standard 1024-sample encoder delay. +//! * **Quantizer** — the exact inverse of §4.6.2: +//! `q = round((|x| / 2^(0.25·(sf−100)))^(3/4))`, with +//! round-half-away-from-zero (the §1.3 `NINT` convention). +//! * **Psychoacoustics-lite** — a masking-spread rule: each +//! scalefactor band's `sf` is chosen so the band's *peak* +//! coefficient quantizes to a target magnitude +//! `M_b = M · (peak_b / peak_frame)^½` (`sf = 100 + 4·log2(peak_b) +//! − (16/3)·log2(M_b)`, the inversion of the dequant gain ladder). +//! The square-root spread interpolates between constant-SNR +//! (every band equally precise relative to itself — wasteful on +//! the leakage skirts of tonal signals) and a flat noise floor +//! (all precision on the loudest band): a band 40 dB below the +//! frame peak is quantized ~20 dB more coarsely, and a band whose +//! target falls below one quantizer step is culled to `ZERO_HCB` +//! outright — a first-order simultaneous-masking model. +//! * **Rate loop** — an outer loop adds a uniform offset to every +//! band's scalefactor (coarsening all quantizers by 1.5 dB per +//! step, the §4.6.2.3.3 quarter-step ladder ×2) until the assembled +//! frame fits the per-frame byte budget derived from the requested +//! bitrate. +//! * **Codebook / section choice** — measured bit cost: a dynamic +//! program over section boundaries picks, for every candidate run +//! of same-class bands, the single Table 4.95 book (1..=11) whose +//! *actual* coded size — Huffman codewords + sign bits + escapes, +//! measured with the real tuple writer — plus the `section_data()` +//! header overhead is minimal (see [`optimize_group_sections`]). +//! This subsumes the classic smallest-LAV-fit + merge-equal-books +//! rule and additionally exploits the signed/unsigned sibling +//! books and header-saving LAV upgrades. +//! * **Pulse escape (§4.4.6.3, measured)** — a long-frame band whose +//! few outlier lines force it onto a large-LAV book or into +//! §4.6.3.3 escape sequences is also priced with a Table 4.7 +//! `pulse_data()` variant: up to four outliers are reduced toward +//! zero to the magnitude floor of the rest of the band (4-bit +//! `amp` reach) and the `(offset, amp)` chain rides the pulse +//! record ([`extract_pulse_candidate`] picks the best-saving band +//! by per-band measured cost). The decoder's §4.6.3.3 fix-up +//! restores the *identical* quantized spectrum before +//! dequantization, so the choice is purely a noiseless-coding one +//! and is settled end-to-end by [`channel_wire_bits`] — the +//! variant is kept only when the whole channel stream measures +//! smaller. +//! * **Stereo** — a CPE with `common_window == 1` (one shared +//! `ics_info()`) and per-band §4.6.8.1 M/S coding: for each +//! scalefactor band the encoder forms `m = (l+r)/2`, +//! `s = (l−r)/2` (the exact forward matrix of the normative +//! `l = m+s` / `r = m−s` de-matrix) and selects M/S when it moves +//! the band's energy into one dominant channel — i.e. when +//! `min(e_m, e_s) ≤ (e_l + e_r) / 8` (the transformed pair is at +//! least ~9 dB lopsided, so the quiet one culls or codes cheaply). +//! The mask is emitted as `ms_mask_present = 2` when every band +//! flags (identical / phase-inverted channels), `1` + explicit +//! mask when mixed, `0` when no band benefits. `EIGHT_SHORT` +//! frames decide per `(window group, sfb)` under the pair's joint +//! grouping — the Table 4.5 `ms_used[g][sfb]` granularity +//! ([`ms_decide_short`]). +//! * **Intensity stereo (§4.6.8.2, opt-in)** — with +//! [`StreamEncoder::set_intensity_stereo`], a high-frequency +//! long-frame CPE band whose channels correlate above +//! [`IS_CORR_MIN`] is transmitted once: the right channel's band +//! becomes the intensity pseudo codebook (15 in-phase / 14 +//! out-of-phase) carrying only `is_pos = 2·log2(e_l/e_r)` on the +//! §4.6.8.1.4 DPCM track; the decoder derives +//! `r = ±0.5^(0.25·is_pos)·l` (§4.6.8.2.3). IS bands are excluded +//! from the M/S mask (per-band mutual exclusion; a set `ms_used` +//! bit would signal phase reversal instead). +//! * **TNS (§4.6.9, default on)** — per analysis window, the +//! [`crate::encoder_tns`] pass measures the prediction gain of an +//! LPC over the coverable spectral region (Levinson-Durbin on the +//! coefficient autocorrelation); a window whose gain clears the +//! threshold transmits one upward Table 4.54 filter (PARCOR +//! quantised on the §4.6.9.3 4-bit arcsine grid) and the spectrum +//! is passed through the §4.6.7.4.1 all-zero analysis filter +//! derived from the *wire* coefficients — the exact inverse of the +//! decoder's §4.6.9.3 all-pole synthesis, run per channel in the +//! L/R domain before the M/S forward matrix (mirroring the +//! decoder's M/S-then-TNS order). See [`StreamEncoder::set_tns`]. +//! * **PNS (§4.6.13, opt-in)** — with [`StreamEncoder::set_pns`], a +//! long-frame band whose energy is spread across most of its +//! coefficients (density `(Σ|x|)²/(width·Σx²)` above 0.4 — dense +//! noise measures `≈2/π`, `k` spectral lines `≈k/width`) is +//! transmitted as a `NOISE_HCB` band carrying only its energy +//! (`noise_nrg = round(4·log2‖band‖₂)`, the `2^(0.25·nrg)` ladder) +//! on the §4.6.13 DPCM track; the decoder re-synthesises the band +//! from its own generator at exactly that L2 norm. In a CPE the +//! decision runs per channel *before* the M/S matrix (mutual +//! exclusion, §4.6.13.5); a both-channels-noise band correlating +//! above [`PNS_CORR_MIN`] sets its `ms_used` bit — the §4.6.13.3 +//! correlated-noise signal (same random vector both channels), not +//! an M/S flag. Off by default: +//! a single-frame statistic cannot tell true noise from +//! noise-shaped deterministic content (sweeps, dense leakage +//! floors), which substitutes with the right energy but the wrong +//! waveform — the default-on decision awaits a cross-frame +//! tonality measure. +//! +//! ## Conformance envelope +//! +//! The assembled frame respects the wire-format hard limits: +//! scalefactors clamp to `0..=255` (8-bit `global_gain` seed) with +//! consecutive DPCM deltas in `−60..=+60` (Table 4.A.1's codeword +//! range), quantized magnitudes cap at +//! [`crate::spectral_codebook::MAX_QUANT`] (8191, the §4.6.3.3 ESC +//! ceiling), and `aac_frame_length` stays within its 13-bit field. + +use crate::adts::{AdtsHeader, ADTS_HEADER_BYTES_NO_CRC, ADTS_SAMPLE_RATES_HZ}; +use crate::encoder_tns::detect_and_apply_tns; +use crate::filterbank::{ + forward_mdct, long_sequence_window, short_window_j, SHORT_SEQ_HOP, SHORT_SEQ_START, +}; +use crate::ics_body::IcsBody; +use crate::ics_info::{ + IcsInfo, WindowSequence, WindowShape, NUM_SWB_LONG_WINDOW, NUM_SWB_SHORT_WINDOW, +}; +use crate::pulse_data::{Pulse, PulseData, MAX_PULSES}; +use crate::raw_data_block::{FrameAssembler, IdSynEle}; +use crate::scale_factor_data::{ + differentiate, AbsoluteScaleFactorEntry, AbsoluteScaleFactors, NOISE_OFFSET, +}; +use crate::section_data::{ + Section, SectionData, INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB, ZERO_HCB, +}; +use crate::spectral_codebook::MAX_QUANT; +use crate::spectral_data::SpectralData; +use crate::swb_offset::{ + long_window_offsets, short_window_offsets, LONG_WINDOW_LEN, SHORT_WINDOW_LEN, +}; +use crate::tns_data::TnsData; +use crate::{Error, Result}; + +use oxideav_core::bits::BitWriter; + +/// Historical direct-factory endpoint (the crate convention's +/// `::encoder::make_encoder` path) — re-exported from +/// [`crate::codec_encoder`]. +pub use crate::codec_encoder::make_encoder; + +/// Samples per channel per AAC frame (the 1024-line transform +/// family this crate implements). +pub const FRAME_LEN: usize = LONG_WINDOW_LEN as usize; + +/// The long transform length `N = 2048`. +const LONG_TRANSFORM_LEN: usize = 2 * FRAME_LEN; + +/// §4.6.2.3.3 `SF_OFFSET` — the scalefactor of unit gain. +const SF_OFFSET: i32 = 100; + +/// Table 4.53 DPCM delta bound (the Table 4.A.1 codeword range). +const MAX_SF_DELTA: i32 = 60; + +/// Target magnitude the *loudest* band's peak coefficient quantizes +/// to before the rate loop engages; quieter bands scale down with +/// the square-root masking spread. +const TARGET_PEAK_MAG: f64 = 42.0; + +/// Masking-spread exponent: a band's target magnitude is +/// `TARGET_PEAK_MAG · (peak_b / peak_frame)^SPREAD`. `0` would be +/// constant-SNR, `1` a flat noise floor; `½` splits the difference. +const SPREAD: f64 = 0.5; + +/// Cull threshold: a band whose spread target magnitude falls below +/// this fraction of one quantizer step carries no audible content +/// relative to the frame and is sent as `ZERO_HCB`. +const MIN_TARGET_MAG: f64 = 0.7; + +/// Upper bound on rate-loop iterations. Each iteration coarsens +/// every quantizer by 3 dB (sf offset +4), so 48 iterations span +/// ~144 dB — beyond that the frame is all-zero anyway. +const MAX_RATE_ITERATIONS: usize = 48; + +/// Deepest refinement the rate loop applies when a frame comes in +/// under budget: −32 scalefactors ≈ 24 dB of extra precision +/// (magnitudes ×2^6 over the [`TARGET_PEAK_MAG`] baseline). +const MAX_REFINE_OFFSET: i32 = 32; + +/// Minimum §4.5.4 band width (coefficients) for the §4.6.13 PNS +/// noise-likeness statistic to be meaningful; narrower bands always +/// code spectrally. +const PNS_MIN_WIDTH: usize = 8; + +/// PNS density floor on the `(Σ|x|)² / (width·Σx²)` statistic: dense +/// Gaussian-like noise measures `≈ 2/π ≈ 0.64` (`(E|x|)²/E[x²]`), +/// while `k` dominant spectral lines measure `≈ k/width` — a band +/// only counts as noise when its energy is spread across most of its +/// coefficients, so leakage skirts and harmonic combs keep spectral +/// coding. +const PNS_DENSITY_MIN: f64 = 0.4; + +/// Configuration for [`StreamEncoder`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EncoderConfig { + /// Output sample rate in Hz. Must be one of the ISO/IEC 14496-3 + /// Table 1.18 rates expressible in an ADTS header (index 0..=12). + pub sample_rate: u32, + /// Channel count. Every count with a Table 1.19 default + /// `channelConfiguration` is accepted: `1` (SCE) / `2` (CPE) / + /// `3` (SCE + CPE) / `4` (SCE + CPE + SCE) / `5` (SCE + 2 CPE) / + /// `6` (5.1: SCE + 2 CPE + LFE) / `8` (7.1: SCE + 3 CPE + LFE). + /// `7` has no default configuration (a PCE-defined layout, out + /// of scope) and is rejected. Input PCM is interleaved in the + /// canonical [`crate::channel_map`] order the decoder emits + /// (5.1 = `L R C LFE Ls Rs`), and the encoder derives the + /// bitstream element order from the Table 1.19 layout. + pub channels: u8, + /// Target bitrate in bits/second. Drives the per-frame byte + /// budget of the rate loop. The output is not strictly CBR — each + /// frame independently fits its budget — but averages at or below + /// this rate. + pub bitrate: u32, +} + +impl EncoderConfig { + /// Resolve `sample_rate` to its Table 1.18 + /// `sampling_frequency_index`. Index 12 (7350 Hz) is excluded: + /// the §4.5.4 scalefactor-band tables only cover indices + /// 0..=11 (7350 Hz content conventionally ships under index 11, + /// see the staged corpus notes). + fn fs_index(&self) -> Result { + ADTS_SAMPLE_RATES_HZ + .iter() + .position(|&r| r == self.sample_rate) + .filter(|&i| i < NUM_SWB_LONG_WINDOW.len()) + .map(|i| i as u8) + .ok_or(Error::EncoderInvalidConfig) + } + + /// Per-frame payload byte budget from the bitrate: + /// `bitrate · 1024 / sample_rate` bits, minus the 7-byte ADTS + /// header, floored at a minimum that always allows a syntactically + /// valid (silent) frame. + fn frame_budget_bytes(&self) -> usize { + let bits = (self.bitrate as u64 * FRAME_LEN as u64) / self.sample_rate.max(1) as u64; + let bytes = (bits / 8) as usize; + bytes.saturating_sub(ADTS_HEADER_BYTES_NO_CRC).max(16) + } + + /// The Table 1.19 default `channelConfiguration` for this channel + /// count (see [`EncoderConfig::channels`]). + fn channel_configuration(&self) -> Result { + match self.channels { + n @ 1..=6 => Ok(n), + 8 => Ok(7), + _ => Err(Error::EncoderInvalidConfig), + } + } +} + +/// One channel element of the §4.4.2.1 `raw_data_block()` plan, with +/// element-order channel indices. +#[derive(Debug, Clone, Copy)] +enum ElementPlan { + Sce(usize), + Cpe(usize, usize), + Lfe(usize), +} + +/// The Table 1.19 element sequence for a default +/// `channelConfiguration`, indexing element-order channels. +fn element_plan(channel_configuration: u8) -> Result> { + use ElementPlan::*; + Ok(match channel_configuration { + 1 => vec![Sce(0)], + 2 => vec![Cpe(0, 1)], + 3 => vec![Sce(0), Cpe(1, 2)], + 4 => vec![Sce(0), Cpe(1, 2), Sce(3)], + 5 => vec![Sce(0), Cpe(1, 2), Cpe(3, 4)], + 6 => vec![Sce(0), Cpe(1, 2), Cpe(3, 4), Lfe(5)], + 7 => vec![Sce(0), Cpe(1, 2), Cpe(3, 4), Cpe(5, 6), Lfe(7)], + _ => return Err(Error::EncoderInvalidConfig), + }) +} + +/// §4.5.2.1.3: only the lowest 12 spectral coefficients of an LFE +/// element may be non-zero. +const LFE_MAX_LINES: usize = 12; + +/// A streaming AAC-LC encoder producing one ADTS frame per +/// 1024-sample input hop. +/// +/// Feed interleaved `i16` PCM via [`StreamEncoder::encode_all`] (one +/// shot), or drive [`StreamEncoder::encode_frame`] hop by hop and +/// finish with [`StreamEncoder::finish`] to flush the analysis +/// overlap. The encoder delay is exactly [`FRAME_LEN`] samples: the +/// first decoded frame of the round-trip is silence, and decoded +/// frame `f ≥ 1` reconstructs input hop `f − 1`. +#[derive(Debug, Clone)] +pub struct StreamEncoder { + config: EncoderConfig, + fs_index: u8, + /// The Table 1.19 `channelConfiguration` derived from the channel + /// count (lands in the ADTS header and fixes the element plan). + channel_configuration: u8, + /// Canonical-input channel index feeding each *element-order* + /// channel slot — the inverse of the decoder's + /// [`crate::channel_map::reorder_permutation`], so encode ∘ + /// decode is channel-identity. + element_src: Vec, + /// Element-order slots that belong to an LFE element (§4.5.2.1.3 + /// restrictions apply: long-only analysis, no TNS, ≤ 12 lines). + lfe_slot: Vec, + /// Per-channel previous input hop (the left half of the next + /// analysis window), [`FRAME_LEN`] samples each, in *element + /// order*. Starts all-zero (the priming frame). + history: Vec>, + /// `window_sequence` of the previously emitted frame — drives + /// the §4.6.11.3.2 block-switching state machine. + prev_seq: WindowSequence, + /// The previous frame flagged a transient in what is now the + /// history hop, so this frame *must* be `EIGHT_SHORT_SEQUENCE` + /// (the `LONG_START → EIGHT_SHORT` contract). + short_pending: bool, + /// §4.6.13 PNS emission toggle — see [`StreamEncoder::set_pns`]. + pns_enabled: bool, + /// §4.6.9 TNS emission toggle — see [`StreamEncoder::set_tns`]. + tns_enabled: bool, + /// §4.6.8.2 intensity-stereo emission toggle — see + /// [`StreamEncoder::set_intensity_stereo`]. + is_enabled: bool, +} + +impl StreamEncoder { + /// Build an encoder for `config`. + /// + /// Errors with [`Error::EncoderInvalidConfig`] when the sample + /// rate is not a Table 1.18 ADTS rate, the channel count has no + /// Table 1.19 default configuration (see + /// [`EncoderConfig::channels`]), or the bitrate is 0. + pub fn new(config: EncoderConfig) -> Result { + let fs_index = config.fs_index()?; + let channel_configuration = config.channel_configuration()?; + if config.bitrate == 0 { + return Err(Error::EncoderInvalidConfig); + } + let ch = config.channels as usize; + // canonical[i] = element[perm[i]] on the decode side, so the + // element slot j sources canonical input channel i with + // perm[i] == j. + let perm = crate::channel_map::reorder_permutation(channel_configuration) + .ok_or(Error::EncoderInvalidConfig)?; + let mut element_src = vec![0usize; ch]; + for (i, &j) in perm.iter().enumerate() { + element_src[j] = i; + } + let mut lfe_slot = vec![false; ch]; + for elem in element_plan(channel_configuration)? { + if let ElementPlan::Lfe(slot) = elem { + lfe_slot[slot] = true; + } + } + Ok(Self { + config, + fs_index, + channel_configuration, + element_src, + lfe_slot, + history: vec![vec![0.0; FRAME_LEN]; ch], + prev_seq: WindowSequence::OnlyLong, + short_pending: false, + pns_enabled: false, + tns_enabled: true, + is_enabled: false, + }) + } + + /// Enable / disable §4.6.13 PNS emission (default **off**). + /// + /// When enabled, long frames transmit dense noise-like bands + /// (see the module docs) as `NOISE_HCB` energies instead of + /// spectra — a large bitrate win on noise content, validated + /// energy-exact through the decoder's §4.6.13.3 synthesis. In a + /// CPE the decision runs per channel on the pre-M/S spectra + /// (PNS and M/S are mutually exclusive per band, §4.6.13.5); + /// a band both channels noise-code whose content correlates + /// above [`PNS_CORR_MIN`] additionally sets its `ms_used` bit, + /// signalling the decoder to synthesise the *same* random + /// vector into both channels (§4.6.13.3 correlated noise). It + /// stays opt-in because a *single-frame* spectral statistic + /// cannot distinguish true noise from noise-shaped deterministic + /// content (a frequency sweep, a dense leakage floor): those + /// substitute with the right energy but the wrong waveform. + /// Turning the default on awaits a cross-frame tonality / + /// predictability measure. + pub fn set_pns(&mut self, enabled: bool) { + self.pns_enabled = enabled; + } + + /// Enable / disable §4.6.9 TNS emission (default **on**). + /// + /// When enabled, each analysis window whose spectrum shows a + /// prediction gain above the [`crate::encoder_tns::TNS_GAIN_MIN`] + /// threshold (a strongly non-flat temporal envelope) transmits a + /// Table 4.54 noise-shaping filter, and the spectrum is passed + /// through the matching §4.6.7.4.1 all-zero analysis filter + /// before quantisation. The decoder's §4.6.9.3 all-pole synthesis + /// pass is the exact inverse of the applied (wire-quantised) + /// filter, so TNS is transparent to the reconstruction while + /// confining quantisation noise under the signal's temporal + /// envelope. Safe to leave on: windows without a clear envelope + /// simply carry no filter. + pub fn set_tns(&mut self, enabled: bool) { + self.tns_enabled = enabled; + } + + /// Enable / disable §4.6.8.2 intensity-stereo emission (default + /// **off**). + /// + /// When enabled, a high-frequency scalefactor band of a + /// long-frame CPE whose two channels are strongly correlated + /// (normalised cross-correlation above + /// [`IS_CORR_MIN`]) is transmitted **once**: the left channel + /// carries its spectrum, the right channel's band becomes the + /// pseudo codebook `INTENSITY_HCB` (15, in-phase) or + /// `INTENSITY_HCB2` (14, out-of-phase) with an intensity + /// position `is_pos = 2·log2(e_l/e_r)` on the §4.6.8.1.4 DPCM + /// track, and the decoder derives + /// `r = ±0.5^(0.25·is_pos) · l` per §4.6.8.2.3. Such bands are + /// excluded from the M/S mask (M/S, IS and PNS are mutually + /// exclusive per band, and a set `ms_used` bit on an intensity + /// band would signal the §4.6.8.2.3 phase reversal instead). + /// Off by default: intensity coding discards the side + /// information of the pair (only the energy ratio survives), a + /// perceptual trade appropriate for low-rate coding but not for + /// transparent transcodes. + pub fn set_intensity_stereo(&mut self, enabled: bool) { + self.is_enabled = enabled; + } + + /// The configuration this encoder was built with. + pub fn config(&self) -> &EncoderConfig { + &self.config + } + + /// Encode one 1024-sample-per-channel hop of interleaved `i16` + /// PCM into one complete ADTS frame. + /// + /// `interleaved` must hold at most `1024 × channels` samples; a + /// shorter slice (the stream tail) is zero-padded. The analysis + /// window spans the previous hop and this one, so the emitted + /// frame carries the overlap-add contribution of both. + pub fn encode_frame(&mut self, interleaved: &[i16]) -> Result> { + let ch = self.config.channels as usize; + if interleaved.len() > FRAME_LEN * ch || interleaved.len() % ch != 0 { + return Err(Error::EncoderInvalidConfig); + } + // De-interleave onto the ±32768 axis the §4.6.11 output + // contract uses (no rescaling — the decoder's PCM stage + // rounds these values back to i16 directly), permuting the + // canonical input order into bitstream element order + // (`element_src` — the inverse of the decoder's Table 1.19 + // output reorder). + let mut cur: Vec> = vec![vec![0.0; FRAME_LEN]; ch]; + for (j, chan) in cur.iter_mut().enumerate() { + let src = self.element_src[j]; + for (n, slot) in chan.iter_mut().take(interleaved.len() / ch).enumerate() { + *slot = f64::from(interleaved[n * ch + src]); + } + } + let frame = self.encode_hop(&cur)?; + self.history = cur; + Ok(frame) + } + + /// Flush the final analysis overlap: emits one trailing ADTS + /// frame whose window covers the last real hop and a zero hop. + pub fn finish(&mut self) -> Result> { + let ch = self.config.channels as usize; + let zeros: Vec> = vec![vec![0.0; FRAME_LEN]; ch]; + let frame = self.encode_hop(&zeros)?; + self.history = zeros; + Ok(frame) + } + + /// One-shot convenience: encode a whole interleaved `i16` buffer + /// to a complete ADTS stream (`⌈n/1024⌉ + 1` frames — the `+1` + /// is the [`StreamEncoder::finish`] flush). + pub fn encode_all(&mut self, interleaved: &[i16]) -> Result> { + let ch = self.config.channels as usize; + if interleaved.len() % ch != 0 { + return Err(Error::EncoderInvalidConfig); + } + let mut out = Vec::new(); + let hop = FRAME_LEN * ch; + let mut chunks = interleaved.chunks(hop); + // Always emit at least one content frame (an empty input + // yields one silent frame + the flush frame). + let first = chunks.next().unwrap_or(&[]); + out.extend_from_slice(&self.encode_frame(first)?); + for chunk in chunks { + out.extend_from_slice(&self.encode_frame(chunk)?); + } + out.extend_from_slice(&self.finish()?); + Ok(out) + } + + /// Window `[history | cur]`, transform, quantize under the rate + /// loop, and wrap the raw data block in an ADTS header. + fn encode_hop(&mut self, cur: &[Vec]) -> Result> { + let ch = self.config.channels as usize; + + // §4.6.11.3.2 block-switching state machine. A transient in + // `cur` means the *next* frame (whose window's left half is + // `cur`) must be EIGHT_SHORT; this frame becomes the + // LONG_START lead-in (or stays short if a short run is + // already active). A pending short from the previous hop + // forces EIGHT_SHORT now; a short run with no continuation + // exits through LONG_STOP. + // LFE channels neither trigger nor follow block switching — + // §4.5.2.1.3 fixes their window_sequence to ONLY_LONG. + let transient = self + .history + .iter() + .zip(cur.iter()) + .zip(self.lfe_slot.iter()) + .any(|((h, c), &lfe)| !lfe && detect_transient(h, c)); + let seq = if self.short_pending { + WindowSequence::EightShort + } else if transient && self.prev_seq != WindowSequence::EightShort { + WindowSequence::LongStart + } else if self.prev_seq == WindowSequence::EightShort { + if transient { + WindowSequence::EightShort + } else { + WindowSequence::LongStop + } + } else { + WindowSequence::OnlyLong + }; + self.short_pending = transient; + + // Per-channel analysis transform for the chosen sequence + // (LFE channels always analyze ONLY_LONG — their own window + // chain stays long/sine per §4.5.2.1.3, independent of the + // frame's switching state). + let mut spectra: Vec> = Vec::with_capacity(ch); + for ((hist, chan), &lfe) in self + .history + .iter() + .zip(cur.iter()) + .zip(self.lfe_slot.iter()) + { + let ch_seq = if lfe { WindowSequence::OnlyLong } else { seq }; + spectra.push(analyze_channel(hist, chan, ch_seq)?); + } + self.prev_seq = seq; + + // §4.6.9 TNS: per-channel decision + analysis filtering, + // BEFORE the M/S forward matrix — the decoder applies TNS + // synthesis per channel *after* the M/S de-matrix + // (§4.6.9.3's place in the §4.6 tool chain), so the encoder's + // analysis pass runs in the L/R domain. The filtering mutates + // the spectra once, outside the rate loop (the filter choice + // is independent of the scalefactor offset). + let max_sfb = if seq == WindowSequence::EightShort { + NUM_SWB_SHORT_WINDOW[self.fs_index as usize] + } else { + NUM_SWB_LONG_WINDOW[self.fs_index as usize] + }; + let mut tns: Vec> = vec![None; ch]; + if self.tns_enabled { + for (j, (spec, slot)) in spectra.iter_mut().zip(tns.iter_mut()).enumerate() { + if self.lfe_slot[j] { + continue; // §4.5.2.1.3: no TNS on an LFE element + } + let permit = tns_temporal_permits(&self.history[j], &cur[j], seq); + *slot = detect_and_apply_tns(spec, seq, max_sfb, self.fs_index, &permit)?; + } + } + + // Rate loop: uniform scalefactor offset in ±4 steps (3 dB + // per step on the §4.6.2.3.3 quarter-step ladder). Coarsen + // until the raw data block fits the budget; when it already + // fits, refine (spend the remaining budget on precision) as + // long as the finer frame still fits, down to + // `-MAX_REFINE_OFFSET`. + let budget = self.config.frame_budget_bytes(); + let mut sf_offset = 0i32; + let mut raw_block = self.assemble_raw_block(seq, &spectra, &tns, sf_offset)?; + let mut iterations = 0usize; + if raw_block.len() > budget { + while raw_block.len() > budget && iterations < MAX_RATE_ITERATIONS { + sf_offset += 4; + raw_block = self.assemble_raw_block(seq, &spectra, &tns, sf_offset)?; + iterations += 1; + } + } else { + while sf_offset > -MAX_REFINE_OFFSET && iterations < MAX_RATE_ITERATIONS { + let finer = self.assemble_raw_block(seq, &spectra, &tns, sf_offset - 4)?; + if finer.len() > budget { + break; + } + sf_offset -= 4; + raw_block = finer; + iterations += 1; + } + } + // Fine pass: the ±4 ladder can leave up to 3 scalefactors of + // precision unspent at the budget boundary (a full −4 step + // un-zeros a whole swath of near-threshold coefficients at + // once). Try the intermediate offsets, finest first. + if raw_block.len() <= budget && sf_offset > -MAX_REFINE_OFFSET { + for fine in [3i32, 2, 1] { + let cand = self.assemble_raw_block(seq, &spectra, &tns, sf_offset - fine)?; + if cand.len() <= budget { + raw_block = cand; + break; + } + } + } + + // ADTS wrap. aac_frame_length is 13 bits; the budget floor + // (16 bytes) and MAX_RATE_ITERATIONS guarantee headroom for + // every realistic configuration, but validate regardless. + let frame_len = ADTS_HEADER_BYTES_NO_CRC + raw_block.len(); + if frame_len >= (1 << 13) { + return Err(Error::EncoderFrameOverflow); + } + let header = AdtsHeader { + mpeg_version_mpeg2: false, + protection_absent: true, + profile: 1, // AAC LC: profile_ObjectType = AOT − 1 = 1 + sampling_frequency_index: self.fs_index, + channel_configuration: self.channel_configuration, + aac_frame_length: frame_len as u16, + adts_buffer_fullness: 0x7FF, // VBR sentinel + number_of_raw_data_blocks_in_frame: 1, + }; + let mut out = Vec::with_capacity(frame_len); + out.extend_from_slice(&header.write()?); + out.extend_from_slice(&raw_block); + Ok(out) + } + + /// Assemble one `raw_data_block()` — the Table 1.19 element plan + /// for the configuration (SCE / `common_window` CPE / LFE + /// elements, then END) at the given rate-loop scalefactor offset. + /// + /// `tns` carries the per-channel §4.6.9 filter records decided + /// once per hop (the spectra arrive already analysis-filtered); + /// they land in each channel's `tns_data_present` / `tns_data` + /// wire slots. Element instance tags count up per element kind, + /// so the decoder's per-`(id, tag)` state slots stay distinct. + fn assemble_raw_block( + &self, + seq: WindowSequence, + spectra: &[Vec], + tns: &[Option], + sf_offset: i32, + ) -> Result> { + let mut asm = FrameAssembler::new(); + let plan = element_plan(self.channel_configuration)?; + let mut sce_tag = 0u8; + let mut cpe_tag = 0u8; + let mut lfe_tag = 0u8; + for elem in plan { + let mut body_bits = BitWriter::new(); + match elem { + ElementPlan::Sce(ch) => { + asm.push_channel_header(IdSynEle::Sce, sce_tag)?; + sce_tag += 1; + self.assemble_sce(&mut body_bits, &spectra[ch], &tns[ch], seq, sf_offset)?; + } + ElementPlan::Lfe(ch) => { + asm.push_channel_header(IdSynEle::Lfe, lfe_tag)?; + lfe_tag += 1; + self.assemble_lfe(&mut body_bits, &spectra[ch], sf_offset)?; + } + ElementPlan::Cpe(l, r) => { + asm.push_channel_header(IdSynEle::Cpe, cpe_tag)?; + cpe_tag += 1; + self.assemble_cpe( + &mut body_bits, + &spectra[l], + &spectra[r], + &tns[l], + &tns[r], + seq, + sf_offset, + )?; + } + } + let nbits = body_bits.bit_position(); + asm.push_channel_body_bits(&body_bits.finish(), nbits)?; + } + Ok(asm.push_end()) + } + + /// Quantize a spectrum for this frame: the grouped short-window + /// path for `EIGHT_SHORT`, the long path otherwise. + #[allow(clippy::too_many_arguments)] + fn quantize_seq( + &self, + spec: &[f64], + seq: WindowSequence, + sf_offset: i32, + peak: f64, + pns_bands: &[bool], + is_bands: &[IsBand], + ) -> Result { + if seq == WindowSequence::EightShort { + quantize_channel_short(spec, self.fs_index, sf_offset, peak) + } else { + quantize_channel( + spec, + seq, + self.fs_index, + sf_offset, + peak, + pns_bands, + is_bands, + ) + } + } + + /// One SCE body: quantize (with the mono blanket PNS allowance + /// when enabled on a long frame) and write + /// `individual_channel_stream(0)`. + fn assemble_sce( + &self, + body_bits: &mut BitWriter, + spec: &[f64], + tns: &Option, + seq: WindowSequence, + sf_offset: i32, + ) -> Result<()> { + // §4.6.13 PNS emission is opt-in (set_pns) and long-frame + // only; a lone channel grants a blanket per-band allowance + // (the noise-likeness test in quantize_group decides). + let pns_long = self.pns_enabled && seq != WindowSequence::EightShort; + let num_swb_long = NUM_SWB_LONG_WINDOW[self.fs_index as usize] as usize; + let peak = spec.iter().fold(0.0f64, |m, &v| m.max(v.abs())); + let pns_bands = if pns_long { + vec![true; num_swb_long] + } else { + Vec::new() + }; + let mut chan = self.quantize_seq(spec, seq, sf_offset, peak, &pns_bands, &[])?; + attach_tns(&mut chan, tns); + chan.body.write(body_bits, 2, self.fs_index, false)?; + chan.spectral.write( + body_bits, + &chan.info, + &chan.body.section_data, + self.fs_index, + ) + } + + /// One LFE body under the §4.5.2.1.3 restrictions: the element is + /// a plain `individual_channel_stream(0)`, always + /// `ONLY_LONG_SEQUENCE` with the sine window (the analysis stage + /// already ran this channel long), no TNS / PNS / prediction, and + /// only the lowest [`LFE_MAX_LINES`] spectral lines non-zero. + fn assemble_lfe(&self, body_bits: &mut BitWriter, spec: &[f64], sf_offset: i32) -> Result<()> { + let mut lfe_spec = spec.to_vec(); + for c in lfe_spec[LFE_MAX_LINES..].iter_mut() { + *c = 0.0; + } + let peak = lfe_spec.iter().fold(0.0f64, |m, &v| m.max(v.abs())); + let chan = self.quantize_seq( + &lfe_spec, + WindowSequence::OnlyLong, + sf_offset, + peak, + &[], + &[], + )?; + chan.body.write(body_bits, 2, self.fs_index, false)?; + chan.spectral.write( + body_bits, + &chan.info, + &chan.body.section_data, + self.fs_index, + ) + } + + /// One `common_window` CPE body: the per-band IS / PNS / M/S + /// decisions, joint quantization on the pair peak, and the + /// shared-`ics_info` wire assembly. + #[allow(clippy::too_many_arguments)] + fn assemble_cpe( + &self, + body_bits: &mut BitWriter, + l_spec: &[f64], + r_spec: &[f64], + tns_l: &Option, + tns_r: &Option, + seq: WindowSequence, + sf_offset: i32, + ) -> Result<()> { + let pns_long = self.pns_enabled && seq != WindowSequence::EightShort; + // §4.6.8.2: per-band intensity decision first (opt-in, + // long frames) — an IS band is transmitted once via + // the left channel and must not also be M/S-coded + // (mutual exclusion; a set ms_used bit on an + // intensity band signals the §4.6.8.2.3 phase + // reversal, not an M/S de-matrix). + let is_bands: Vec = if self.is_enabled && seq != WindowSequence::EightShort { + is_decide(l_spec, r_spec, self.fs_index)? + } else { + Vec::new() + }; + // §4.6.13 per-band PNS decision on the original l/r + // spectra (a noise band must not be M/S-transformed — + // mutual exclusion, §4.6.13.5 — and IS wins where the + // two overlap). + let pns = if pns_long { + pns_decide_pair(l_spec, r_spec, &is_bands, self.fs_index)? + } else { + PairPns::default() + }; + // §4.6.8.1: per-band M/S decision, then quantize the coding + // spectra (m/s on flagged bands, l/r elsewhere). Both coding + // channels share the pair's loudest peak for the masking + // spread, so the side channel's noise floor is judged + // against the pair, not against its own (often tiny) peak. + // The mask is one row per window group (`ms_used[g][sfb]`); + // long sequences have one group, EIGHT_SHORT frames decide + // per (group, sfb) under the jointly-decided grouping. + let (ms_rows, mut left, mut right); + if seq == WindowSequence::EightShort { + // A common_window CPE shares one ics_info, so the + // §4.5.2.3.4 grouping is decided ONCE on the pair + // envelope (per-coefficient L/R energy sum) and imposed + // on both channels — independent decisions could + // diverge and desync the shared wire layout. + let combined: Vec = l_spec + .iter() + .zip(r_spec.iter()) + .map(|(&l, &r)| (l * l + r * r).sqrt()) + .collect(); + let offsets = short_window_offsets(self.fs_index)?; + let num_swb = NUM_SWB_SHORT_WINDOW[self.fs_index as usize] as usize; + let wgl = decide_short_grouping(&combined, offsets, num_swb); + let rows = ms_decide_short(l_spec, r_spec, offsets, num_swb, &wgl); + let (code_l, code_r) = if rows.iter().flatten().any(|&b| b) { + apply_ms_short(l_spec, r_spec, &rows, offsets, &wgl) + } else { + (l_spec.to_vec(), r_spec.to_vec()) + }; + let pair_peak = code_l + .iter() + .chain(code_r.iter()) + .fold(0.0f64, |m, &v| m.max(v.abs())); + left = quantize_channel_short_grouped( + &code_l, + self.fs_index, + sf_offset, + pair_peak, + wgl.clone(), + )?; + right = + quantize_channel_short_grouped(&code_r, self.fs_index, sf_offset, pair_peak, wgl)?; + ms_rows = rows; + } else { + let mut ms_used = ms_decide(l_spec, r_spec, self.fs_index)?; + for (sfb, band) in is_bands.iter().enumerate() { + if band.is_some() { + if let Some(m) = ms_used.get_mut(sfb) { + *m = false; + } + } + } + // A band either channel will noise-code is excluded + // from the M/S transform; a both-channels-noise band + // whose content correlates re-sets its ms_used bit, + // which per §4.6.13.3 signals the decoder to draw the + // *same* random vector for both channels (correlated + // noise) rather than an M/S de-matrix. + for (sfb, m) in ms_used.iter_mut().enumerate() { + let l_n = pns.l_noise.get(sfb).copied().unwrap_or(false); + let r_n = pns.r_noise.get(sfb).copied().unwrap_or(false); + if l_n || r_n { + *m = pns.shared.get(sfb).copied().unwrap_or(false); + } + } + let ms_transform: Vec = ms_used + .iter() + .enumerate() + .map(|(sfb, &m)| { + m && !pns.l_noise.get(sfb).copied().unwrap_or(false) + && !pns.r_noise.get(sfb).copied().unwrap_or(false) + }) + .collect(); + let (code_l, code_r) = if ms_transform.iter().any(|&b| b) { + apply_ms(l_spec, r_spec, &ms_transform, self.fs_index)? + } else { + (l_spec.to_vec(), r_spec.to_vec()) + }; + let pair_peak = code_l + .iter() + .chain(code_r.iter()) + .fold(0.0f64, |m, &v| m.max(v.abs())); + left = self.quantize_seq(&code_l, seq, sf_offset, pair_peak, &pns.l_noise, &[])?; + right = + self.quantize_seq(&code_r, seq, sf_offset, pair_peak, &pns.r_noise, &is_bands)?; + ms_rows = vec![ms_used]; + } + attach_tns(&mut left, tns_l); + attach_tns(&mut right, tns_r); + + // §4.4.2.3: common_window = 1, one shared ics_info, + // then the two-bit ms_mask_present (+ mask when 1: one bit + // per (window group, sfb), group-major — Table 4.5). + body_bits.write_bit(true); + left.info.write(body_bits, 2, self.fs_index, true)?; + let any_ms = ms_rows.iter().flatten().any(|&b| b); + let all_ms = !ms_rows.is_empty() + && ms_rows.iter().all(|row| !row.is_empty()) + && ms_rows.iter().flatten().all(|&b| b); + if all_ms { + body_bits.write_u32(2, 2); // all ones, no mask bits + } else if any_ms { + body_bits.write_u32(1, 2); + for &b in ms_rows.iter().flatten() { + body_bits.write_bit(b); + } + } else { + body_bits.write_u32(0, 2); + } + for chan in [&left, &right] { + chan.body + .write_with_ics_info(body_bits, &chan.info, 2, false)?; + chan.spectral.write( + body_bits, + &chan.info, + &chan.body.section_data, + self.fs_index, + )?; + } + + Ok(()) + } +} + +/// Attach a channel's §4.6.9 TNS record to its wire body. +fn attach_tns(chan: &mut QuantizedChannel, slot: &Option) { + if let Some(t) = slot { + chan.body.tns_data_present = true; + chan.body.tns_data = Some(t.clone()); + } +} + +/// Subblock length of the transient detector — one short-window hop +/// (128 samples), so a detected attack aligns with the short-window +/// grid it triggers. +const TRANSIENT_SUBBLOCK: usize = SHORT_SEQ_HOP; + +/// Energy jump (×) a subblock must show over the running average of +/// the preceding subblocks to count as a transient attack. +const TRANSIENT_RATIO: f64 = 12.0; + +/// Absolute per-subblock energy floor below which an attack is +/// ignored (silence-to-quiet transitions don't warrant short +/// windows): a 128-sample block at ~±180 amplitude. +const TRANSIENT_FLOOR: f64 = 128.0 * 180.0 * 180.0; + +/// Minimum established (pre-attack) average subblock energy for the +/// detector to arm. Below this the context is effectively silence +/// and an onset codes acceptably with the long-window pair (its +/// left flank is silence — there is no signal to smear pre-echo +/// into), so the detector stays quiet rather than switching on +/// every stream/passage onset. +const TRANSIENT_ARM: f64 = TRANSIENT_FLOOR / TRANSIENT_RATIO; + +/// Detect a transient attack inside one channel's next hop. +/// +/// The 2048-sample context `[hist | cur]` is split into sixteen +/// 128-sample subblocks; an attack fires when a subblock **in the +/// `cur` half** has energy that (a) clears the absolute +/// [`TRANSIENT_FLOOR`], and (b) jumps [`TRANSIENT_RATIO`]× above the +/// **maximum** energy of the preceding eight subblocks (one hop of +/// context) — provided that maximum itself clears [`TRANSIENT_ARM`] +/// (an established signal level to jump *from*). Using the recent +/// max rather than a mean keeps beat nulls in steady multi-tone +/// content from arming spurious triggers, and keeps zeroed history +/// (stream start) from diluting the reference: an onset out of true +/// digital silence codes acceptably with the long-window pair (its +/// left flank is silence — there is nothing to smear pre-echo into), +/// so the detector deliberately stays quiet there. +fn detect_transient(hist: &[f64], cur: &[f64]) -> bool { + let energies: Vec = hist + .chunks(TRANSIENT_SUBBLOCK) + .chain(cur.chunks(TRANSIENT_SUBBLOCK)) + .map(|b| b.iter().map(|&v| v * v).sum()) + .collect(); + let hist_blocks = hist.len() / TRANSIENT_SUBBLOCK; + for (j, &e) in energies.iter().enumerate().skip(hist_blocks) { + let ctx = &energies[j.saturating_sub(hist_blocks.max(1))..j]; + let reference = ctx.iter().fold(0.0f64, |m, &v| m.max(v)); + if e > TRANSIENT_FLOOR && reference > TRANSIENT_ARM && e > TRANSIENT_RATIO * reference { + return true; + } + } + false +} + +/// TNS temporal-envelope gate: minimum `max / mean` subblock-energy +/// flatness ratio of a transform window's time region for TNS to be +/// considered on it. A steady tone (or dense steady multitone) +/// measures close to 1; a burst-and-decay envelope inside the window +/// measures well above. See [`tns_temporal_permits`]. +const TNS_TEMPORAL_RATIO: f64 = 3.0; + +/// Absolute per-window mean subblock energy floor below which the +/// TNS gate stays closed (silence / near-silence windows carry no +/// audible envelope to protect). One 128-sample subblock at ~±90 +/// amplitude. +const TNS_TEMPORAL_FLOOR: f64 = 128.0 * 90.0 * 90.0; + +/// §4.6.9.1 temporal gate for the encode-side TNS decision: per +/// transform window, `true` iff the window's raw time samples show a +/// strongly non-flat energy envelope. +/// +/// The window's time region (2048 samples for a long sequence; the +/// 256-sample `SHORT_SEQ_START + j·SHORT_SEQ_HOP` slice per short +/// window) is split into 16 subblocks whose energies are reduced to +/// the `max / mean` flatness ratio; the gate opens above +/// [`TNS_TEMPORAL_RATIO`] (with a [`TNS_TEMPORAL_FLOOR`] silence +/// guard). This is the *time-domain* half of the TNS decision — the +/// spectral prediction gain alone also fires on steady tonal windows +/// (their leakage skirts are highly predictable) where the temporal +/// envelope is flat and shaping buys nothing; measuring the envelope +/// directly on the input samples keeps TNS to the transient / +/// speech-like windows it exists for (§4.6.9.1's duality argument +/// run forward). +fn tns_temporal_permits(hist: &[f64], cur: &[f64], seq: WindowSequence) -> Vec { + let region = |i: usize| -> f64 { + if i < FRAME_LEN { + hist[i] + } else { + cur[i - FRAME_LEN] + } + }; + let flatness_permits = |base: usize, len: usize| -> bool { + let sub = len / 16; + let energies: Vec = (0..16) + .map(|j| { + (0..sub) + .map(|m| { + let v = region(base + j * sub + m); + v * v + }) + .sum() + }) + .collect(); + let mean = energies.iter().sum::() / 16.0; + let max = energies.iter().fold(0.0f64, |a, &b| a.max(b)); + // Normalise the floor to the subblock length (the constant is + // stated for a 128-sample subblock). + let floor = TNS_TEMPORAL_FLOOR * sub as f64 / 128.0; + mean > floor && max > TNS_TEMPORAL_RATIO * mean + }; + if seq == WindowSequence::EightShort { + (0..8) + .map(|j| { + flatness_permits( + SHORT_SEQ_START + j * SHORT_SEQ_HOP, + 2 * SHORT_WINDOW_LEN as usize, + ) + }) + .collect() + } else { + vec![flatness_permits(0, LONG_TRANSFORM_LEN)] + } +} + +/// Run the §4.6.11.3.1 analysis transform for one channel under the +/// chosen `window_sequence`, over the 2048-sample region +/// `[hist | cur]`. +/// +/// * Long sequences: one 2048-point MDCT under the +/// [`long_sequence_window`] (sine shape throughout — this encoder +/// never switches shapes, so left/right inheritance is trivial). +/// * `EIGHT_SHORT`: eight 256-point MDCTs at offsets +/// `448 + j·128` inside the region ([`SHORT_SEQ_START`] / +/// [`SHORT_SEQ_HOP`]), each under its [`short_window_j`]; +/// concatenated window-major (`8 × 128` coefficients). +fn analyze_channel(hist: &[f64], cur: &[f64], seq: WindowSequence) -> Result> { + debug_assert_eq!(hist.len(), FRAME_LEN); + debug_assert_eq!(cur.len(), FRAME_LEN); + let region = |i: usize| -> f64 { + if i < FRAME_LEN { + hist[i] + } else { + cur[i - FRAME_LEN] + } + }; + if seq == WindowSequence::EightShort { + let short_len = SHORT_WINDOW_LEN as usize; // 128 + let n_s = 2 * short_len; // 256 + let mut out = Vec::with_capacity(8 * short_len); + for j in 0..8 { + let w = short_window_j(j, WindowShape::Sine, WindowShape::Sine); + let base = SHORT_SEQ_START + j * SHORT_SEQ_HOP; + let seg: Vec = (0..n_s).map(|m| region(base + m) * w[m]).collect(); + out.extend_from_slice(&forward_mdct(&seg, n_s)); + } + Ok(out) + } else { + let w = long_sequence_window(seq, WindowShape::Sine, WindowShape::Sine)?; + let z: Vec = (0..LONG_TRANSFORM_LEN).map(|m| region(m) * w[m]).collect(); + Ok(forward_mdct(&z, LONG_TRANSFORM_LEN)) + } +} + +/// One quantized channel, ready for wire assembly. +struct QuantizedChannel { + info: IcsInfo, + body: IcsBody, + spectral: SpectralData, +} + +/// One band's §4.6.8.2 intensity-stereo decision: `None` codes the +/// band normally; `Some((codebook, is_pos))` transmits the right +/// channel's band as the intensity book (15 in-phase / 14 +/// out-of-phase) at the given position on the `0.5^(0.25·is_pos)` +/// gain ladder. +type IsBand = Option<(u8, i32)>; + +/// Lowest spectral line an intensity-coded band may start at: +/// intensity stereo exploits the ear's insensitivity to phase at +/// high frequencies (§4.6.8.2.1), so the bottom quarter of the +/// spectrum always keeps discrete coding. +const IS_MIN_SPECTRAL_LINE: usize = FRAME_LEN / 4; + +/// Minimum normalised cross-correlation `|Σ l·r| / sqrt(Σl²·Σr²)` +/// for a band to qualify for intensity coding. Deliberately strict: +/// a genuine intensity image (shared content at a per-channel gain) +/// measures ≈ 1.0, while the leakage skirts of two *different* +/// tones — deterministic, slowly-decaying magnitude profiles — were +/// measured correlating as high as 0.93 on synthetic two-tone +/// content; IS-coding those would substitute the wrong (if masked) +/// waveform for no bit win over the cull they get anyway. +pub const IS_CORR_MIN: f64 = 0.95; + +/// Relative peak floor for the intensity decision: a band whose +/// loudest coefficient (either channel) sits more than ~50 dB below +/// the pair's frame peak carries only leakage floor — the *distant* +/// skirts of any two windowed tones are smooth deterministic decays +/// that correlate near 1.0 regardless of the tones' relation +/// (measured 0.98 between two unrelated tones' far tails), so +/// correlation alone cannot vet an image down there, and a band that +/// quiet codes for almost nothing (or culls) discretely anyway. +const IS_PEAK_FLOOR_RATIO: f64 = 3e-3; + +/// §4.6.8.2 per-band intensity-stereo decision (encode side) for a +/// long-frame channel pair. +/// +/// A band qualifies when it lies above [`IS_MIN_SPECTRAL_LINE`], +/// both channels carry energy, and the normalised cross-correlation +/// clears [`IS_CORR_MIN`]. The transmitted position quantises the +/// energy ratio onto the §4.6.8.2.3 gain ladder — +/// `0.5^(0.25·is_pos) = sqrt(e_r/e_l)` ⇒ `is_pos = 2·log2(e_l/e_r)` +/// — and the codebook carries the phase: `INTENSITY_HCB` (15) when +/// the channels correlate positively, `INTENSITY_HCB2` (14) when +/// they anti-correlate. +fn is_decide(l_spec: &[f64], r_spec: &[f64], fs_index: u8) -> Result> { + let offsets = long_window_offsets(fs_index)?; + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + let frame_peak = l_spec + .iter() + .chain(r_spec.iter()) + .fold(0.0f64, |m, &v| m.max(v.abs())); + let peak_floor = frame_peak * IS_PEAK_FLOOR_RATIO; + let mut out: Vec = vec![None; num_swb]; + for (sfb, slot) in out.iter_mut().enumerate() { + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + if start < IS_MIN_SPECTRAL_LINE { + continue; + } + let mut e_l = 0.0f64; + let mut e_r = 0.0f64; + let mut dot = 0.0f64; + let mut band_peak = 0.0f64; + for k in start..end { + e_l += l_spec[k] * l_spec[k]; + e_r += r_spec[k] * r_spec[k]; + dot += l_spec[k] * r_spec[k]; + band_peak = band_peak.max(l_spec[k].abs()).max(r_spec[k].abs()); + } + if e_l <= 0.0 || e_r <= 0.0 || band_peak < peak_floor { + continue; + } + let corr = dot.abs() / (e_l * e_r).sqrt(); + if corr < IS_CORR_MIN { + continue; + } + let pos = (2.0 * (e_l / e_r).log2()).round(); + // Keep the position within a range the ±60-delta track can + // plausibly reach; a >±30 dB imbalance codes better discretely. + if !(-80.0..=80.0).contains(&pos) { + continue; + } + let cb = if dot >= 0.0 { + INTENSITY_HCB + } else { + INTENSITY_HCB2 + }; + *slot = Some((cb, pos as i32)); + } + Ok(out) +} + +/// Minimum normalised cross-correlation for a both-channels-noise +/// band to be flagged *correlated* (§4.6.13.3): the decoder then +/// draws the **same** random vector for both channels. Positive +/// correlation only — the shared vector reproduces positively +/// correlated noise, so anti-correlated noise stays on independent +/// draws. +pub const PNS_CORR_MIN: f64 = 0.5; + +/// Per-band §4.6.13 PNS decision for a channel pair (encode side). +#[derive(Debug, Default)] +struct PairPns { + /// Left channel per-band PNS allowance (noise-like content). + l_noise: Vec, + /// Right channel per-band PNS allowance. + r_noise: Vec, + /// Both channels noise **and** correlated above + /// [`PNS_CORR_MIN`] — emitted as a set `ms_used` bit + /// (§4.6.13.3 correlated-noise signalling). + shared: Vec, +} + +/// Decide the §4.6.13 noise bands of a long-frame channel pair on +/// the original (pre-M/S) spectra. +/// +/// Per band: each channel qualifies through the same +/// [`is_noise_like`] density statistic the mono path uses; a band +/// where **both** qualify additionally measures its normalised +/// cross-correlation — above [`PNS_CORR_MIN`] the band is flagged +/// `shared`, which the CPE assembler emits as a set `ms_used` bit so +/// the decoder synthesises the same random vector into both channels +/// (§4.6.13.3; no M/S de-matrix is performed on such a band — PNS +/// and M/S are mutually exclusive, §4.6.13.5). Bands claimed by +/// intensity stereo (`is_bands`) are skipped — M/S, IS and PNS are +/// pairwise exclusive on a band. +fn pns_decide_pair( + l_spec: &[f64], + r_spec: &[f64], + is_bands: &[IsBand], + fs_index: u8, +) -> Result { + let offsets = long_window_offsets(fs_index)?; + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + let mut out = PairPns { + l_noise: vec![false; num_swb], + r_noise: vec![false; num_swb], + shared: vec![false; num_swb], + }; + for sfb in 0..num_swb { + if is_bands.get(sfb).copied().flatten().is_some() { + continue; // intensity wins the band + } + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + let l_band = &l_spec[start..end]; + let r_band = &r_spec[start..end]; + let l_n = is_noise_like(l_band); + let r_n = is_noise_like(r_band); + out.l_noise[sfb] = l_n; + out.r_noise[sfb] = r_n; + if l_n && r_n { + let e_l: f64 = l_band.iter().map(|&v| v * v).sum(); + let e_r: f64 = r_band.iter().map(|&v| v * v).sum(); + let dot: f64 = l_band.iter().zip(r_band).map(|(&a, &b)| a * b).sum(); + if e_l > 0.0 && e_r > 0.0 && dot / (e_l * e_r).sqrt() >= PNS_CORR_MIN { + out.shared[sfb] = true; + } + } + } + Ok(out) +} + +/// §4.6.8.1 per-band M/S decision for a channel pair. +/// +/// A band selects M/S coding when the mid/side transform +/// (`m = (l+r)/2`, `s = (l−r)/2`) concentrates its energy: with +/// `e_m + e_s = (e_l + e_r)/2` (exact, by the transform's geometry), +/// requiring `min(e_m, e_s) ≤ (e_l + e_r)/8` means the quieter +/// transformed channel holds at most a quarter of the transformed +/// energy (≥ ~5 dB below its partner) — it will cull or code +/// cheaply while the dominant channel carries the band once instead +/// of twice. +fn ms_decide(l_spec: &[f64], r_spec: &[f64], fs_index: u8) -> Result> { + let offsets = long_window_offsets(fs_index)?; + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + let mut used = Vec::with_capacity(num_swb); + for sfb in 0..num_swb { + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + let mut e_lr = 0.0f64; + let mut e_m = 0.0f64; + let mut e_s = 0.0f64; + for k in start..end { + let (l, r) = (l_spec[k], r_spec[k]); + e_lr += l * l + r * r; + let m = 0.5 * (l + r); + let s = 0.5 * (l - r); + e_m += m * m; + e_s += s * s; + } + used.push(e_lr > 0.0 && e_m.min(e_s) <= e_lr / 8.0); + } + Ok(used) +} + +/// Forward M/S matrix: on flagged bands the coding pair is +/// `(m, s) = ((l+r)/2, (l−r)/2)` — the exact inverse of the +/// decoder's §4.6.8.1.3 `l = m+s` / `r = m−s` de-matrix — and the +/// identity elsewhere. +fn apply_ms( + l_spec: &[f64], + r_spec: &[f64], + ms_used: &[bool], + fs_index: u8, +) -> Result<(Vec, Vec)> { + let offsets = long_window_offsets(fs_index)?; + let mut code_l = l_spec.to_vec(); + let mut code_r = r_spec.to_vec(); + for (sfb, &used) in ms_used.iter().enumerate() { + if !used { + continue; + } + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + for k in start..end { + let m = 0.5 * (l_spec[k] + r_spec[k]); + let s = 0.5 * (l_spec[k] - r_spec[k]); + code_l[k] = m; + code_r[k] = s; + } + } + Ok((code_l, code_r)) +} + +/// §4.6.8.1 per-`(window group, sfb)` M/S decision for an +/// `EIGHT_SHORT_SEQUENCE` channel pair under the shared grouping +/// `wgl` — the same energy-concentration criterion as [`ms_decide`], +/// summed over every window of the group (the mask granularity the +/// Table 4.5 `ms_used[g][sfb]` wire provides). `l_spec` / `r_spec` +/// are window-major 8 × 128 buffers. +fn ms_decide_short( + l_spec: &[f64], + r_spec: &[f64], + offsets: &[u16], + num_swb: usize, + wgl: &[u8], +) -> Vec> { + let short_len = SHORT_WINDOW_LEN as usize; + let mut rows = Vec::with_capacity(wgl.len()); + let mut win_base = 0usize; + for &len in wgl { + let mut row = Vec::with_capacity(num_swb); + for sfb in 0..num_swb { + let mut e_lr = 0.0f64; + let mut e_m = 0.0f64; + let mut e_s = 0.0f64; + for w in win_base..win_base + len as usize { + for k in offsets[sfb] as usize..offsets[sfb + 1] as usize { + let (l, r) = (l_spec[w * short_len + k], r_spec[w * short_len + k]); + e_lr += l * l + r * r; + let m = 0.5 * (l + r); + let s = 0.5 * (l - r); + e_m += m * m; + e_s += s * s; + } + } + row.push(e_lr > 0.0 && e_m.min(e_s) <= e_lr / 8.0); + } + rows.push(row); + win_base += len as usize; + } + rows +} + +/// Forward M/S butterfly on the flagged `(group, sfb)` bands of a +/// window-major short-sequence pair — the short-frame counterpart of +/// [`apply_ms`], walking every window of a flagged group. +fn apply_ms_short( + l_spec: &[f64], + r_spec: &[f64], + rows: &[Vec], + offsets: &[u16], + wgl: &[u8], +) -> (Vec, Vec) { + let short_len = SHORT_WINDOW_LEN as usize; + let mut code_l = l_spec.to_vec(); + let mut code_r = r_spec.to_vec(); + let mut win_base = 0usize; + for (row, &len) in rows.iter().zip(wgl) { + for (sfb, &used) in row.iter().enumerate() { + if !used { + continue; + } + for w in win_base..win_base + len as usize { + for k in offsets[sfb] as usize..offsets[sfb + 1] as usize { + let i = w * short_len + k; + let m = 0.5 * (l_spec[i] + r_spec[i]); + let s = 0.5 * (l_spec[i] - r_spec[i]); + code_l[i] = m; + code_r[i] = s; + } + } + } + win_base += len as usize; + } + (code_l, code_r) +} + +/// §4.6.2 forward quantizer for one coefficient at scalefactor `sf`: +/// `q = sign(x) · NINT((|x| · 2^(−0.25·(sf−100)))^(3/4))`, the exact +/// inverse of the normative `|q|^(4/3) · 2^(0.25·(sf−100))` +/// (round-half-away-from-zero per §1.3 `NINT`). +fn quantize_coef(x: f64, sf: i32) -> i32 { + let gain = (0.25 * f64::from(sf - SF_OFFSET)).exp2(); + let mag = (x.abs() / gain).powf(0.75).round(); + let mag = mag.min(f64::from(MAX_QUANT)) as i32; + if x < 0.0 { + -mag + } else { + mag + } +} + +/// The masking-spread scalefactor for a band whose peak coefficient +/// is `peak` in a frame whose loudest band peaks at `frame_peak`: +/// solve `(peak / 2^(0.25·(sf−100)))^(3/4) = M_b` for `sf` with +/// `M_b = TARGET_PEAK_MAG · (peak/frame_peak)^SPREAD`, i.e. +/// `sf = 100 + 4·log2(peak) − (16/3)·log2(M_b)`. +/// +/// Returns `None` when the band's spread target falls below +/// [`MIN_TARGET_MAG`] — such a band quantizes to silence anyway and +/// is culled to `ZERO_HCB` by the caller. +fn band_scalefactor(peak: f64, frame_peak: f64, sf_offset: i32) -> Option { + if peak <= 0.0 || frame_peak <= 0.0 { + return None; + } + let target = TARGET_PEAK_MAG * (peak / frame_peak).powf(SPREAD); + if target < MIN_TARGET_MAG { + return None; + } + let sf = f64::from(SF_OFFSET) + 4.0 * peak.log2() - (16.0 / 3.0) * target.log2(); + Some((sf.round() as i32 + sf_offset).clamp(0, 255)) +} + +/// Smallest Table 4.95 spectrum codebook whose LAV covers `qmax`. +/// `1`/`3` are the 4-tuple books (LAV 1 / 2), `5`/`7`/`9` the pair +/// books (LAV 4 / 7 / 12), `11` the ESC book. +fn codebook_for(qmax: i32) -> u8 { + match qmax { + 0 => ZERO_HCB, + 1 => 1, + 2 => 3, + 3..=4 => 5, + 5..=7 => 7, + 8..=12 => 9, + _ => 11, + } +} + +/// Per-band quantization result for one window group. +struct GroupQuant { + x_quant: Vec, + sfb_cb: Vec, + sfs: Vec>, + /// §4.6.13 noise energies for PNS bands (`sfb_cb == NOISE_HCB`): + /// the band's target L2 norm on the `2^(0.25·noise_nrg)` ladder. + noise: Vec>, + /// §4.6.8.2 intensity positions for IS bands (`sfb_cb == 14/15`, + /// right channel of a CPE only): the position on the + /// `0.5^(0.25·is_pos)` gain ladder. + is_pos: Vec>, +} + +/// Quantize the `num_swb` scalefactor bands of one window group. +/// +/// `spec` is the group's coefficient buffer (1024 lines for a long +/// sequence, `window_group_length × 128` interleaved lines for a +/// short group); `offsets` the +/// matching §4.5.4 band-offset table. `prev_sf` threads the DPCM ±60 +/// clamp across groups in wire order — the §4.6.2.3.2 accumulator is +/// a single track for the whole channel. +/// +/// Pass 1 picks the masking-spread scalefactor per band; pass 2 +/// re-quantizes with the clamped value and derives the codebook. A +/// band whose coefficients all quantize to zero (or whose target is +/// culled) stays `ZERO_HCB` and transmits no scalefactor. +#[allow(clippy::too_many_arguments)] +fn quantize_group( + spec: &[f64], + offsets: &[u16], + num_swb: usize, + sf_offset: i32, + frame_peak: f64, + prev_sf: &mut Option, + pns_bands: &[bool], +) -> GroupQuant { + let mut x_quant = vec![0i32; spec.len()]; + let mut sfb_cb = vec![ZERO_HCB; num_swb]; + let mut sfs: Vec> = vec![None; num_swb]; + let mut noise: Vec> = vec![None; num_swb]; + for sfb in 0..num_swb { + let start = offsets[sfb] as usize; + let end = (offsets[sfb + 1] as usize).min(spec.len()); + let peak = spec[start..end].iter().fold(0.0f64, |m, &v| m.max(v.abs())); + let Some(mut sf) = band_scalefactor(peak, frame_peak, sf_offset) else { + continue; // culled: below the frame's masking floor + }; + // §4.6.13 PNS: a wide band with no dominant spectral line is + // transmitted as a noise energy instead of coefficients. The + // per-band allowance comes from the caller (blanket for mono, + // the pre-M/S pair decision for a CPE channel). + if pns_bands.get(sfb).copied().unwrap_or(false) && is_noise_like(&spec[start..end]) { + let nrg: f64 = spec[start..end].iter().map(|&x| x * x).sum(); + // Target L2 norm 2^(0.25·noise_nrg) == sqrt(nrg). + let noise_nrg = (4.0 * nrg.sqrt().log2()).round() as i32; + sfb_cb[sfb] = NOISE_HCB; + noise[sfb] = Some(noise_nrg); + continue; + } + if let Some(p) = *prev_sf { + sf = sf.clamp(p - MAX_SF_DELTA, p + MAX_SF_DELTA).clamp(0, 255); + } + // Raise sf until the band's peak fits the ESC ceiling (a + // +4 step scales magnitudes by 2^(-3/4)). + let mut qmax = quantize_coef(peak, sf).abs(); + while qmax >= MAX_QUANT && sf < 255 { + sf = (sf + 4).min(255); + qmax = quantize_coef(peak, sf).abs(); + } + if qmax == 0 { + continue; // all-zero band -> ZERO_HCB, no scalefactor + } + let mut band_max = 0i32; + for k in start..end { + let q = quantize_coef(spec[k], sf); + x_quant[k] = q; + band_max = band_max.max(q.abs()); + } + if band_max == 0 { + continue; + } + sfb_cb[sfb] = codebook_for(band_max); + sfs[sfb] = Some(sf); + *prev_sf = Some(sf); + } + GroupQuant { + x_quant, + sfb_cb, + sfs, + noise, + is_pos: vec![None; num_swb], + } +} + +/// Rewrite the right channel's IS-selected bands (§4.6.8.2 encode +/// side): the band's codebook becomes the transmitted intensity book +/// (15 in-phase / 14 out-of-phase), its coefficients are dropped +/// (intensity bands carry no spectral data — the decoder derives +/// them from the left channel), its spectrum scalefactor is retired, +/// and the intensity position lands on the §4.6.8.1.4 `is_pos` +/// track. +fn apply_is_overrides(group: &mut GroupQuant, is_bands: &[IsBand], offsets: &[u16]) { + for (sfb, band) in is_bands.iter().enumerate().take(group.sfb_cb.len()) { + let Some((cb, pos)) = band else { + continue; + }; + let start = offsets[sfb] as usize; + let end = (offsets[sfb + 1] as usize).min(group.x_quant.len()); + for q in &mut group.x_quant[start..end] { + *q = 0; + } + group.sfb_cb[sfb] = *cb; + group.sfs[sfb] = None; + group.noise[sfb] = None; + group.is_pos[sfb] = Some(*pos); + } +} + +/// §4.6.13 noise-likeness test on the `(Σ|x|)² / (width·Σx²)` +/// density statistic (see [`PNS_DENSITY_MIN`]): `true` only when the +/// band's energy is spread across most of its coefficients the way a +/// dense noise band's is. Bands narrower than [`PNS_MIN_WIDTH`] +/// never qualify (the statistic is meaningless on a handful of +/// coefficients). +fn is_noise_like(band: &[f64]) -> bool { + let width = band.len(); + if width < PNS_MIN_WIDTH { + return false; + } + let l1: f64 = band.iter().map(|&v| v.abs()).sum(); + let l2_sq: f64 = band.iter().map(|&v| v * v).sum(); + if l2_sq <= 0.0 { + return false; + } + (l1 * l1) / (width as f64 * l2_sq) > PNS_DENSITY_MIN +} + +/// Build the per-band absolute scalefactor / noise-energy records +/// and the frame's `global_gain`, then run the §4.6.2.3.2 / §4.6.13 +/// inverse DPCM ([`differentiate`]) to obtain the transmitted entry +/// set. +/// +/// `global_gain` is the first coded spectrum band's scalefactor +/// (making its delta 0). The §4.6.13 noise track is seeded at +/// `global_gain − NOISE_OFFSET − 256` with the first PNS band's +/// delta a 9-bit *unsigned* PCM (`0..=511`) and later noise deltas +/// Huffman `±60`; each requested `noise_nrg` is clamped into the +/// nearest feasible value on that track (a few 1.5 dB steps of +/// clamp at worst — noise energy is far less sensitive than a +/// spectral gain). +fn scalefactor_track(groups: &[GroupQuant]) -> (u8, AbsoluteScaleFactors) { + let global_gain = groups + .iter() + .flat_map(|g| g.sfs.iter().copied().flatten()) + .next() + .unwrap_or(SF_OFFSET) as u8; + let mut last_nrg = i32::from(global_gain) - NOISE_OFFSET - 256; + let mut first_noise = true; + // §4.6.8.1.4: the intensity-position track seeds at 0 and takes + // the same Huffman ±60 deltas as scalefactors; requested + // positions are clamped onto the feasible track like the noise + // energies above. + let mut last_is = 0i32; + let mut entries = Vec::with_capacity(groups.len()); + for g in groups { + let mut group_out = Vec::new(); + for sfb in 0..g.sfb_cb.len() { + if let Some(sf) = g.sfs[sfb] { + group_out.push(AbsoluteScaleFactorEntry::Sf(sf as u8)); + } else if let Some(nrg) = g.noise[sfb] { + let delta = nrg - last_nrg; + let clamped = if first_noise { + delta.clamp(0, 511) + } else { + delta.clamp(-MAX_SF_DELTA, MAX_SF_DELTA) + }; + first_noise = false; + last_nrg += clamped; + group_out.push(AbsoluteScaleFactorEntry::NoiseNrg(last_nrg)); + } else if let Some(pos) = g.is_pos[sfb] { + let delta = (pos - last_is).clamp(-MAX_SF_DELTA, MAX_SF_DELTA); + last_is += delta; + group_out.push(AbsoluteScaleFactorEntry::IsPos(last_is as i16)); + } + } + entries.push(group_out); + } + (global_gain, AbsoluteScaleFactors { entries }) +} + +/// Exact §4.6.3.3 wire cost, in bits, of coding one band's +/// coefficient range with spectrum book `cb` — Huffman codewords + +/// sign bits + escape sequences, measured by running the actual +/// [`crate::spectral_data`] tuple writer into a scratch buffer. +/// `None` when the book cannot carry the band (a magnitude beyond +/// the book's Table 4.95 LAV; book 11 escapes up to `MAX_QUANT`). +fn band_bits(cb: u8, coeffs: &[i32]) -> Option { + let row = crate::spectral_codebook::table_4_95(cb).ok()?; + let dim = row.dimension? as usize; + let mut bw = BitWriter::new(); + let mut k = 0; + while k + dim <= coeffs.len() { + crate::spectral_data::write_tuple(&mut bw, cb, dim, &coeffs[k..k + dim]).ok()?; + k += dim; + } + if k != coeffs.len() { + return None; // band width not a whole number of tuples + } + Some(bw.bit_position() as u32) +} + +/// The §4.4.2.7-adjacent `section_data()` header cost of one section +/// spanning `len` bands: 4 bits `sect_cb` plus the `sect_len_incr` +/// escape run (5-bit fields / escape 31 for long sequences, 3-bit / +/// escape 7 for `EIGHT_SHORT`). +fn section_header_bits(len: u32, long: bool) -> u32 { + let (esc, w) = if long { (31, 5) } else { (7, 3) }; + 4 + w * (len / esc + 1) +} + +/// A band's sectioning class — sections may only span bands of one +/// class (the special codebooks are semantic, not a coding choice, +/// and a `ZERO_HCB` band folded into a spectrum section would owe a +/// scalefactor the track never assigned). +#[derive(PartialEq, Eq, Clone, Copy)] +enum BandClass { + /// `ZERO_HCB` — no spectrum, no scalefactor. + Zero, + /// `NOISE_HCB` / intensity books — the codebook is fixed by the + /// tool decision; adjacent equal books merge. + Fixed(u8), + /// Spectrum bands (provisional book 1..=11) — the section book + /// is a free choice among every book that covers the run. + Spectral, +} + +/// Choose one window group's sections + codebooks by measured bit +/// cost (§4.6.3.1 leaves both entirely to the encoder). +/// +/// Dynamic program over section boundaries: for every candidate run +/// of same-class bands the cost is the [`section_header_bits`] +/// overhead plus — for spectral runs — the cheapest single Table +/// 4.95 book (1..=11, measured per band via [`band_bits`], covering +/// the whole run) summed over the run's bands. This subsumes the +/// classic "smallest LAV fit + merge equal books" rule and beats it +/// wherever a signed/unsigned sibling book codes the actual +/// distribution cheaper, or one step up in LAV lets two sections +/// merge for less than the saved header. +/// +/// `ranges` maps each band to its coefficient range inside the +/// group's (interleaved) buffer; `sfb_cb` carries the per-band class +/// in (provisional books on spectral bands) and the chosen books +/// out. +fn optimize_group_sections( + x_quant: &[i32], + ranges: &[(usize, usize)], + sfb_cb: &mut [u8], + long: bool, +) -> Result> { + let n = sfb_cb.len(); + if n == 0 { + return Ok(Vec::new()); + } + let class: Vec = sfb_cb + .iter() + .map(|&cb| match cb { + ZERO_HCB => BandClass::Zero, + NOISE_HCB | INTENSITY_HCB | INTENSITY_HCB2 => BandClass::Fixed(cb), + _ => BandClass::Spectral, + }) + .collect(); + // Per-band cost under each spectrum book (None = book can't + // carry the band). + let books: [u8; 11] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + let cost: Vec<[Option; 11]> = (0..n) + .map(|b| { + let mut row = [None; 11]; + if class[b] == BandClass::Spectral { + let (s, e) = ranges[b]; + for (i, &cb) in books.iter().enumerate() { + row[i] = band_bits(cb, &x_quant[s..e]); + } + } + row + }) + .collect(); + + // dp[b] = (bits, cut, book) for the cheapest sectioning of bands + // 0..b, where `cut` is the start of the final section and `book` + // its codebook. + let mut dp: Vec<(u64, usize, u8)> = vec![(u64::MAX, 0, 0); n + 1]; + dp[0] = (0, 0, 0); + for b in 1..=n { + for a in (0..b).rev() { + // The run a..b must be one class (and one fixed book). + if class[a] != class[b - 1] { + break; + } + let header = u64::from(section_header_bits((b - a) as u32, long)); + let run = match class[a] { + BandClass::Zero => Some((0u8, 0u64)), + BandClass::Fixed(cb) => { + if sfb_cb[a..b].iter().any(|&c| c != cb) { + None // e.g. mixed intensity phases + } else { + Some((cb, 0)) + } + } + BandClass::Spectral => { + let mut best: Option<(u8, u64)> = None; + for (i, &cb) in books.iter().enumerate() { + let mut sum = 0u64; + let mut ok = true; + for c in cost[a..b].iter() { + match c[i] { + Some(bits) => sum += u64::from(bits), + None => { + ok = false; + break; + } + } + } + if ok && best.map(|(_, s)| sum < s).unwrap_or(true) { + best = Some((cb, sum)); + } + } + best + } + }; + let Some((book, run_bits)) = run else { + continue; + }; + let total = dp[a].0.saturating_add(header + run_bits); + if total < dp[b].0 { + dp[b] = (total, a, book); + } + } + } + if dp[n].0 == u64::MAX { + return Err(Error::SpectralDataEncodeInvalid); + } + + // Walk the cuts back into sections and stamp the chosen books. + let mut bounds = Vec::new(); + let mut b = n; + while b > 0 { + let (_, a, book) = dp[b]; + bounds.push((a, b, book)); + b = a; + } + bounds.reverse(); + let mut sections = Vec::with_capacity(bounds.len()); + for (a, b, book) in bounds { + for cb in sfb_cb[a..b].iter_mut() { + *cb = book; + } + sections.push(Section { + codebook: book, + start: a as u8, + end: b as u8, + }); + } + Ok(sections) +} + +/// Wrap quantized groups + an `ics_info` into the wire record set. +/// The per-group band ranges come from the §4.5.2.3.4 +/// `sect_sfb_offset` derivation, so grouped short-window buffers +/// (band widths × `window_group_length`) resolve correctly. +fn finish_channel( + info: IcsInfo, + groups: Vec, + fs_index: u8, + pulse_data: Option, +) -> Result { + let (global_gain, abs) = scalefactor_track(&groups); + let long = info.window_sequence != WindowSequence::EightShort; + let per_group_offsets = crate::spectral_data::sect_sfb_offset(&info, fs_index)?; + let mut sections = Vec::with_capacity(groups.len()); + let mut sfb_cb = Vec::with_capacity(groups.len()); + let mut x_quant = Vec::with_capacity(groups.len()); + for (mut g, offsets) in groups.into_iter().zip(per_group_offsets.iter()) { + let ranges: Vec<(usize, usize)> = (0..g.sfb_cb.len()) + .map(|sfb| { + ( + (offsets[sfb] as usize).min(g.x_quant.len()), + (offsets[sfb + 1] as usize).min(g.x_quant.len()), + ) + }) + .collect(); + sections.push(optimize_group_sections( + &g.x_quant, + &ranges, + &mut g.sfb_cb, + long, + )?); + sfb_cb.push(g.sfb_cb); + x_quant.push(g.x_quant); + } + let scale_factor_data = differentiate(&abs, &sfb_cb, global_gain)?; + let body = IcsBody { + global_gain, + ics_info: Some(info.clone()), + section_data: SectionData { sections, sfb_cb }, + scale_factor_data, + pulse_data_present: pulse_data.is_some(), + pulse_data, + tns_data_present: false, + tns_data: None, + gain_control_data_present: false, + gain_control_data: None, + spectral_data_bit_offset: 0, + er_scale_factor_data: None, + reordered_spectral_lengths: None, + }; + let spectral = SpectralData { x_quant }; + Ok(QuantizedChannel { + info, + body, + spectral, + }) +} + +/// Exact wire size, in bits, of one channel's +/// `individual_channel_stream()` — the [`IcsBody`] side info +/// (sections, scalefactors, pulse / TNS dispatch) plus the +/// `spectral_data()` payload — measured by running the real writers +/// into a scratch buffer. Used to settle encoder tool decisions +/// (pulse escape) by measured cost, the same philosophy as +/// [`optimize_group_sections`]. +fn channel_wire_bits(chan: &QuantizedChannel, fs_index: u8) -> Result { + let mut bw = BitWriter::new(); + chan.body.write(&mut bw, 2, fs_index, false)?; + chan.spectral + .write(&mut bw, &chan.info, &chan.body.section_data, fs_index)?; + Ok(bw.bit_position()) +} + +/// Wire cost, in bits, of one Table 4.7 pulse record carrying `n` +/// pulses: 2-bit `number_pulse` + 6-bit `pulse_start_sfb` + 9 bits +/// per `(offset, amp)` entry (the `pulse_data_present` dispatch bit +/// itself is paid on both variants). +fn pulse_record_bits(n: usize) -> u64 { + 8 + 9 * n as u64 +} + +/// §4.4.6.3 / Table 4.7 pulse-escape candidate for one long-frame +/// quantized spectrum. +/// +/// The pulse tool pays off when a band's few outlier lines force the +/// whole band (and through sectioning, its neighbours) onto a large- +/// LAV codebook or into §4.6.3.3 escape sequences: transmitting the +/// outliers' excess as `(offset, amp)` fix-ups lets the residual +/// band code on the book the *rest* of its lines need. The decoder's +/// §4.6.3.3 reconstruction ([`crate::swb_offset::apply_pulse_data`], +/// `x_quant[k] ±= amp` on the transmitted sign) restores the exact +/// original quantized values, so the choice is purely one of +/// noiseless-coding cost. +/// +/// Candidate search, per spectrum-coded band (`ZERO_HCB` bands +/// transmit no scalefactor and `NOISE_HCB` / intensity bands no +/// coefficients — excluded): for every outlier count `j` in +/// `1..=`[`MAX_PULSES`], reduce the band's `j` largest-magnitude +/// lines to the magnitude floor set by its `(j+1)`-th largest +/// (clamped to the 4-bit `amp` reach, keeping the residual >= 1 so +/// the transmitted sign survives), price the reduced band at its +/// cheapest Table 4.95 book via [`band_bits`] plus the +/// [`pulse_record_bits`] overhead, and keep the best-measuring `j`. +/// The best band across the spectrum wins (Table 4.7's single +/// `pulse_start_sfb` + 5-bit offset deltas make one band the +/// realistic carrier; cross-band chains are almost never +/// addressable). Pulses must ascend with in-band gaps <= 31 and the +/// first offset within 31 of the band start. +/// +/// Returns the pulse record and the reduced spectrum, or `None` when +/// no band measures a saving. The caller re-prices the whole channel +/// with [`channel_wire_bits`] (capturing section-merge effects) and +/// keeps the variant only when the full stream measures smaller. +fn extract_pulse_candidate( + x_quant: &[i32], + sfb_cb: &[u8], + offsets: &[u16], +) -> Option<(PulseData, Vec)> { + // (measured saving, carrier sfb, [(line index, amp)]). + #[allow(clippy::type_complexity)] + let mut best: Option<(i64, usize, Vec<(usize, i32)>)> = None; + for sfb in 0..sfb_cb.len().min(offsets.len().saturating_sub(1)).min(64) { + match sfb_cb[sfb] { + ZERO_HCB | NOISE_HCB | INTENSITY_HCB | INTENSITY_HCB2 => continue, + _ => {} + } + let (s, e) = ( + offsets[sfb] as usize, + (offsets[sfb + 1] as usize).min(x_quant.len()), + ); + if e <= s { + continue; + } + let band = &x_quant[s..e]; + // Baseline: the band's cheapest book as-is. + let Some(base_cost) = cheapest_band_bits(band) else { + continue; + }; + // Magnitude-descending line order. + let mut by_mag: Vec = (0..band.len()).collect(); + by_mag.sort_by_key(|&i| std::cmp::Reverse(band[i].abs())); + for j in 1..=MAX_PULSES.min(band.len().saturating_sub(1)) { + let floor = band[by_mag[j]].abs().max(1); + // The j selected lines, ascending, with their amp. + let mut lines: Vec<(usize, i32)> = by_mag[..j] + .iter() + .map(|&i| (i, (band[i].abs() - floor).min(15))) + .filter(|&(_, amp)| amp >= 1) + .collect(); + if lines.len() < j { + continue; // an outlier is out of amp reach parity with the floor + } + lines.sort_by_key(|&(i, _)| i); + // Table 4.7 addressability. + if lines[0].0 > 0x1f { + continue; + } + if lines.windows(2).any(|w| w[1].0 - w[0].0 > 0x1f) { + continue; + } + let mut reduced = band.to_vec(); + for &(i, amp) in &lines { + if reduced[i] > 0 { + reduced[i] -= amp; + } else { + reduced[i] += amp; + } + } + let Some(cost) = cheapest_band_bits(&reduced) else { + continue; + }; + let saving = i64::from(base_cost) - i64::from(cost) - pulse_record_bits(j) as i64; + if saving > 0 && best.as_ref().map(|(bs, _, _)| saving > *bs).unwrap_or(true) { + best = Some(( + saving, + sfb, + lines.iter().map(|&(i, amp)| (s + i, amp)).collect(), + )); + } + } + } + let (_, start_sfb, lines) = best?; + let mut reduced = x_quant.to_vec(); + let mut pulses = Vec::with_capacity(lines.len()); + let mut prev_k = offsets[start_sfb] as usize; + for &(k, amp) in &lines { + pulses.push(Pulse { + offset: (k - prev_k) as u8, + amp: amp as u8, + }); + prev_k = k; + if reduced[k] > 0 { + reduced[k] -= amp; + } else { + reduced[k] += amp; + } + } + Some(( + PulseData { + pulse_start_sfb: start_sfb as u8, + pulses, + }, + reduced, + )) +} + +/// Cheapest single Table 4.95 book cost for one band's coefficients +/// (books 1..=11 via [`band_bits`]). +fn cheapest_band_bits(coeffs: &[i32]) -> Option { + (1u8..=11).filter_map(|cb| band_bits(cb, coeffs)).min() +} + +/// Quantize one channel's 1024-line long-sequence spectrum into a +/// complete `individual_channel_stream()` record set. `seq` must be +/// one of the three long sequences (it lands in the `ics_info`); +/// `frame_peak` anchors the masking spread and cull — the channel's +/// own peak for mono, the pair's loudest peak for a jointly-coded +/// CPE. `pns_bands` grants the per-band §4.6.13 allowance (empty = +/// PNS off); a non-empty `is_bands` (the right channel of an +/// intensity-coding CPE) rewrites the selected bands into §4.6.8.2 +/// intensity records after quantization. When the spectrum carries +/// escape-magnitude lines, a §4.4.6.3 `pulse_data()` variant is +/// priced against the plain coding and kept if it measures smaller +/// (the decode-side §4.6.3.3 fix-up restores the identical quantized +/// spectrum, so the choice never changes the reconstruction). +#[allow(clippy::too_many_arguments)] +fn quantize_channel( + spec: &[f64], + seq: WindowSequence, + fs_index: u8, + sf_offset: i32, + frame_peak: f64, + pns_bands: &[bool], + is_bands: &[IsBand], +) -> Result { + debug_assert_eq!(spec.len(), FRAME_LEN); + debug_assert!(seq != WindowSequence::EightShort); + let offsets = long_window_offsets(fs_index)?; + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + let mut prev_sf: Option = None; + let mut group = quantize_group( + spec, + offsets, + num_swb, + sf_offset, + frame_peak, + &mut prev_sf, + pns_bands, + ); + if !is_bands.is_empty() { + apply_is_overrides(&mut group, is_bands, offsets); + } + let pulse_candidate = extract_pulse_candidate(&group.x_quant, &group.sfb_cb, offsets); + let info = IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: seq, + window_shape: WindowShape::Sine, + max_sfb: num_swb as u8, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: num_swb as u8, + }; + // Measured pulse decision: price the reduced-spectrum + + // pulse_data() variant against the plain coding with the real + // writers and keep the smaller stream. + if let Some((pd, reduced)) = pulse_candidate { + let mut pulsed_group = GroupQuant { + x_quant: reduced, + sfb_cb: group.sfb_cb.clone(), + sfs: group.sfs.clone(), + noise: group.noise.clone(), + is_pos: group.is_pos.clone(), + }; + // Re-derive the reduced bands' provisional books (the DP + // re-decides anyway; this keeps the class metadata honest). + for sfb in 0..pulsed_group.sfb_cb.len() { + match pulsed_group.sfb_cb[sfb] { + ZERO_HCB | NOISE_HCB | INTENSITY_HCB | INTENSITY_HCB2 => continue, + _ => {} + } + let (s, e) = ( + offsets[sfb] as usize, + (offsets[sfb + 1] as usize).min(pulsed_group.x_quant.len()), + ); + let band_max = pulsed_group.x_quant[s..e] + .iter() + .map(|q| q.abs()) + .max() + .unwrap_or(0); + if band_max > 0 { + pulsed_group.sfb_cb[sfb] = codebook_for(band_max); + } + } + let plain = finish_channel(info.clone(), vec![group], fs_index, None)?; + let pulsed = finish_channel(info, vec![pulsed_group], fs_index, Some(pd))?; + return if channel_wire_bits(&pulsed, fs_index)? < channel_wire_bits(&plain, fs_index)? { + Ok(pulsed) + } else { + Ok(plain) + }; + } + finish_channel(info, vec![group], fs_index, None) +} + +/// Maximum mean per-band log-energy distance (natural log) between +/// two adjacent short windows for the §4.5.2.3.4 grouping decision +/// to merge them into one window group. `ln 4 ≈ 1.39` — the windows' +/// band envelopes agree within ~6 dB on average. +const GROUP_MERGE_LOG_DIST: f64 = 1.386; + +/// Energy floor (one squared unit coefficient) added to both sides +/// of the grouping log-ratio so empty bands compare as equal instead +/// of dividing by zero. +const GROUP_MERGE_EPS: f64 = 1.0; + +/// §4.5.2.3.4 `scale_factor_grouping` decision for one channel's +/// `EIGHT_SHORT_SEQUENCE` spectrum (8 × 128 window-major +/// coefficients): merge adjacent windows whose per-band energy +/// envelopes agree within [`GROUP_MERGE_LOG_DIST`] on average. +/// +/// Grouped windows share one scalefactor / section track — the whole +/// point of the tool (§4.6.2.3.2: "to achieve a most efficient +/// coding, several subsequent windows... can be grouped") — so the +/// merge criterion mirrors what sharing costs: windows with matching +/// band envelopes lose nothing to a common scalefactor, while an +/// attack window's jump keeps it in its own group. Returns the +/// `window_group_length` vector (summing to 8). +fn decide_short_grouping(spec: &[f64], offsets: &[u16], num_swb: usize) -> Vec { + let short_len = SHORT_WINDOW_LEN as usize; + let band_energy = |w: usize, sfb: usize| -> f64 { + let base = w * short_len; + spec[base + offsets[sfb] as usize..base + offsets[sfb + 1] as usize] + .iter() + .map(|&v| v * v) + .sum() + }; + let mut lengths: Vec = vec![1]; + for w in 1..8 { + let dist: f64 = (0..num_swb) + .map(|sfb| { + let a = band_energy(w - 1, sfb) + GROUP_MERGE_EPS; + let b = band_energy(w, sfb) + GROUP_MERGE_EPS; + (a / b).ln().abs() + }) + .sum::() + / num_swb.max(1) as f64; + if dist <= GROUP_MERGE_LOG_DIST { + *lengths.last_mut().expect("non-empty") += 1; + } else { + lengths.push(1); + } + } + lengths +} + +/// The 7-bit `scale_factor_grouping` mask for a `window_group_length` +/// vector: bit `6 − (w − 1)` is set when window `w` (1..=7) stays in +/// the previous window's group — the inverse of the §4.5.2.3.4 +/// derivation in [`crate::ics_info::derive_window_grouping`]. +fn grouping_mask(window_group_length: &[u8]) -> u8 { + let mut mask = 0u8; + let mut w = 0usize; + for &len in window_group_length { + for j in 0..len as usize { + if j > 0 { + mask |= 1 << (6 - (w - 1)); + } + w += 1; + } + } + mask +} + +/// Quantize one channel's `EIGHT_SHORT_SEQUENCE` spectrum (8 x 128 +/// window-major coefficients) into a complete record set. The +/// §4.5.2.3.4 grouping decision ([`decide_short_grouping`]) merges +/// envelope-alike adjacent windows into shared window groups — one +/// scalefactor / section track per group instead of eight — and each +/// group's coefficients are laid out in the §4.5.2.3.5 interleaved +/// `(sfb, window, bin)` transmission order. +fn quantize_channel_short( + spec: &[f64], + fs_index: u8, + sf_offset: i32, + frame_peak: f64, +) -> Result { + let offsets = short_window_offsets(fs_index)?; + let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; + let window_group_length = decide_short_grouping(spec, offsets, num_swb); + quantize_channel_short_grouped(spec, fs_index, sf_offset, frame_peak, window_group_length) +} + +/// [`quantize_channel_short`] under an *imposed* grouping — the +/// `common_window` CPE path must quantize both channels under one +/// shared `ics_info`, so the §4.5.2.3.4 grouping decision is made +/// once (jointly, on the pair envelope) and both channels' section / +/// scalefactor / interleave layouts follow it. +fn quantize_channel_short_grouped( + spec: &[f64], + fs_index: u8, + sf_offset: i32, + frame_peak: f64, + window_group_length: Vec, +) -> Result { + let short_len = SHORT_WINDOW_LEN as usize; + debug_assert_eq!(spec.len(), 8 * short_len); + let offsets = short_window_offsets(fs_index)?; + let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; + let mask = grouping_mask(&window_group_length); + let mut prev_sf: Option = None; + let mut groups = Vec::with_capacity(window_group_length.len()); + let mut win_base = 0usize; + for &len in &window_group_length { + let wgl = len as usize; + // §4.5.2.3.5 interleave: for each band, the group's windows' + // band coefficients ride consecutively. + let mut buf = Vec::with_capacity(wgl * short_len); + for sfb in 0..num_swb { + let (s, e) = (offsets[sfb] as usize, offsets[sfb + 1] as usize); + for w in 0..wgl { + let base = (win_base + w) * short_len; + buf.extend_from_slice(&spec[base + s..base + e]); + } + } + // The group's sect_sfb_offset table: band widths × wgl. + let mut scaled = Vec::with_capacity(num_swb + 1); + let mut acc = 0u16; + scaled.push(0u16); + for sfb in 0..num_swb { + acc += (offsets[sfb + 1] - offsets[sfb]) * len as u16; + scaled.push(acc); + } + groups.push(quantize_group( + &buf, + &scaled, + num_swb, + sf_offset, + frame_peak, + &mut prev_sf, + &[], // PNS stays long-frame-only for now + )); + win_base += wgl; + } + let info = IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb: num_swb as u8, + scale_factor_grouping: Some(mask), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups: window_group_length.len() as u8, + window_group_length, + num_swb: num_swb as u8, + }; + // §4.4.6.3: pulse_data is illegal on EIGHT_SHORT_SEQUENCE. + finish_channel(info, groups, fs_index, None) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dequant::{inverse_quantize, scale_factor_gain}; + + #[test] + fn quantize_coef_inverts_dequant_within_rounding() { + // For any q and sf, dequantizing then re-quantizing recovers + // q exactly (the quantizer is the exact inverse map). + for sf in [40i32, 100, 156, 200] { + for q in [-8190i32, -1000, -12, -1, 0, 1, 7, 40, 999, 8190] { + let x = inverse_quantize(q) * scale_factor_gain(sf as u8); + assert_eq!(quantize_coef(x, sf), q, "sf={sf} q={q}"); + } + } + } + + #[test] + fn band_scalefactor_hits_target_magnitude() { + // A frame-loudest peak quantized with its own + // band_scalefactor lands within rounding of TARGET_PEAK_MAG. + for peak in [1.0f64, 100.0, 3.2e4, 6.7e7] { + let sf = band_scalefactor(peak, peak, 0).expect("loudest band never culls"); + let q = quantize_coef(peak, sf).abs(); + let lo = (TARGET_PEAK_MAG / 2.0_f64.powf(0.375)).floor() as i32; + let hi = (TARGET_PEAK_MAG * 2.0_f64.powf(0.375)).ceil() as i32; + assert!( + (lo..=hi).contains(&q), + "peak={peak} sf={sf} q={q} not in [{lo},{hi}]" + ); + } + } + + #[test] + fn band_scalefactor_spreads_and_culls() { + let frame_peak = 1.0e6f64; + // A band 40 dB down gets a ~20 dB smaller target: the target + // is 42·(10^-2)^0.5 = 4.2, so its peak quantizes to ~4. + let sf = band_scalefactor(frame_peak * 1e-2, frame_peak, 0).unwrap(); + let q = quantize_coef(frame_peak * 1e-2, sf).abs(); + assert!((2..=8).contains(&q), "spread target off: q={q}"); + // A band ~90 dB down is culled outright (target < 0.7). + assert_eq!(band_scalefactor(frame_peak * 3.2e-5, frame_peak, 0), None); + // Zero-peak bands cull. + assert_eq!(band_scalefactor(0.0, frame_peak, 0), None); + } + + #[test] + fn codebook_selection_covers_table_4_95_lavs() { + assert_eq!(codebook_for(0), ZERO_HCB); + assert_eq!(codebook_for(1), 1); + assert_eq!(codebook_for(2), 3); + assert_eq!(codebook_for(4), 5); + assert_eq!(codebook_for(7), 7); + assert_eq!(codebook_for(12), 9); + assert_eq!(codebook_for(13), 11); + assert_eq!(codebook_for(8191), 11); + } + + #[test] + fn config_rejects_bad_parameters() { + assert!(StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: 1, + bitrate: 64_000, + }) + .is_ok()); + assert!(matches!( + StreamEncoder::new(EncoderConfig { + sample_rate: 44_056, // not a Table 1.18 rate + channels: 1, + bitrate: 64_000, + }), + Err(Error::EncoderInvalidConfig) + )); + // Every channel count with a Table 1.19 default + // configuration builds; 0 / 7 / 9 have none and reject. + for ok in [3u8, 4, 5, 6, 8] { + assert!( + StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: ok, + bitrate: 64_000 * u32::from(ok), + }) + .is_ok(), + "channels {ok}" + ); + } + for bad in [0u8, 7, 9] { + assert!( + matches!( + StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: bad, + bitrate: 64_000, + }), + Err(Error::EncoderInvalidConfig) + ), + "channels {bad}" + ); + } + assert!(matches!( + StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: 1, + bitrate: 0, + }), + Err(Error::EncoderInvalidConfig) + )); + } + + /// Deterministic band fill for the sectioning tests. + fn fill_band(buf: &mut [i32], range: (usize, usize), max: i32, seed: &mut u32) { + for slot in buf[range.0..range.1].iter_mut() { + *seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + *slot = ((*seed >> 8) % (2 * max + 1) as u32) as i32 - max; + } + } + + /// Total wire bits of one long group under given sections / + /// books: `section_data()` + `spectral_data()`, measured with + /// the real writers. + fn measure_group( + sections: Vec
, + sfb_cb: Vec, + x_quant: Vec, + num_swb: usize, + fs_index: u8, + ) -> u64 { + let info = IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb: num_swb as u8, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: num_swb as u8, + }; + let sd = SectionData { + sections: vec![sections], + sfb_cb: vec![sfb_cb], + }; + let spectral = SpectralData { + x_quant: vec![x_quant], + }; + let mut bw = BitWriter::new(); + sd.write(&mut bw, WindowSequence::OnlyLong, num_swb as u8) + .unwrap(); + spectral.write(&mut bw, &info, &sd, fs_index).unwrap(); + bw.bit_position() + } + + /// [`band_bits`] agrees with the real `spectral_data()` writer: + /// a one-section stream's spectral bits equal the summed band + /// costs. + #[test] + fn band_bits_matches_wire_writer() { + let fs_index = 4u8; + let offsets = long_window_offsets(fs_index).unwrap(); + let mut buf = vec![0i32; FRAME_LEN]; + let mut seed = 0xB17u32; + for sfb in 0..6 { + fill_band( + &mut buf, + (offsets[sfb] as usize, offsets[sfb + 1] as usize), + 7, + &mut seed, + ); + } + for cb in [7u8, 8, 9, 10, 11] { + let per_band: u32 = (0..6) + .map(|sfb| { + band_bits(cb, &buf[offsets[sfb] as usize..offsets[sfb + 1] as usize]).unwrap() + }) + .sum(); + let sections = vec![Section { + codebook: cb, + start: 0, + end: 6, + }]; + let wire = measure_group(sections.clone(), vec![cb; 6], buf.clone(), 6, fs_index); + let header = u64::from(section_header_bits(6, true)); + assert_eq!(wire, header + u64::from(per_band), "cb {cb}"); + } + // A signed book rejects magnitudes past its LAV; the quad + // books reject a pair-only width mismatch never (widths are + // multiples of 4), but LAV 1 caps at |1|. + assert!(band_bits(1, &[2, 0, 0, 0]).is_none()); + assert!(band_bits(3, &[3, 0, 0, 0]).is_none()); + } + + /// The measured-cost DP never codes a group larger than the + /// classic smallest-LAV + merge-equal-books sectioning, over a + /// spread of band shapes (zero runs, alternating magnitudes, + /// escape bands). + #[test] + fn optimizer_never_loses_to_naive_sections() { + let fs_index = 4u8; + let offsets = long_window_offsets(fs_index).unwrap(); + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + for (case, seed0) in [(0u32, 1u32), (1, 0xACE), (2, 0x5EED), (3, 77)] { + let mut buf = vec![0i32; FRAME_LEN]; + let mut seed = seed0; + for sfb in 0..num_swb { + let range = (offsets[sfb] as usize, offsets[sfb + 1] as usize); + let max = match case { + 0 => [0, 1, 1, 2, 0, 0, 4, 7, 1][sfb % 9], + 1 => [1, 12, 1, 30, 0, 2][sfb % 6], + 2 => (sfb as i32) % 5, + _ => [7, 7, 0, 0, 0, 12, 1, 1][sfb % 8], + }; + if max > 0 { + fill_band(&mut buf, range, max, &mut seed); + } + } + // Provisional per-band books (the DP input). + let provisional: Vec = (0..num_swb) + .map(|sfb| { + let band = &buf[offsets[sfb] as usize..offsets[sfb + 1] as usize]; + codebook_for(band.iter().map(|&v| v.abs()).max().unwrap_or(0)) + }) + .collect(); + // Naive: keep the smallest-LAV books, merge equal runs. + let mut naive_sections: Vec
= Vec::new(); + for (sfb, &cb) in provisional.iter().enumerate() { + match naive_sections.last_mut() { + Some(s) if s.codebook == cb => s.end = (sfb + 1) as u8, + _ => naive_sections.push(Section { + codebook: cb, + start: sfb as u8, + end: (sfb + 1) as u8, + }), + } + } + let naive = measure_group( + naive_sections, + provisional.clone(), + buf.clone(), + num_swb, + fs_index, + ); + // Optimized. + let ranges: Vec<(usize, usize)> = (0..num_swb) + .map(|sfb| (offsets[sfb] as usize, offsets[sfb + 1] as usize)) + .collect(); + let mut books = provisional; + let sections = optimize_group_sections(&buf, &ranges, &mut books, true).unwrap(); + // Every chosen book covers its bands (the writer would + // reject otherwise) and the wire is never larger. + let opt = measure_group(sections, books, buf, num_swb, fs_index); + assert!(opt <= naive, "case {case}: opt {opt} > naive {naive}"); + } + } + + /// [`decide_short_grouping`] merges alike windows and splits at + /// an attack; [`grouping_mask`] is the exact inverse of the + /// §4.5.2.3.4 mask derivation. + #[test] + fn short_grouping_decision_and_mask() { + let fs_index = 4u8; + let offsets = short_window_offsets(fs_index).unwrap(); + let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; + let short_len = SHORT_WINDOW_LEN as usize; + // Eight identical windows: one group of 8, mask all-ones. + let mut spec = vec![0.0f64; 8 * short_len]; + for w in 0..8 { + for k in 0..short_len { + spec[w * short_len + k] = 1000.0 * ((k as f64) * 0.37).sin(); + } + } + assert_eq!(decide_short_grouping(&spec, offsets, num_swb), vec![8]); + assert_eq!(grouping_mask(&[8]), 0x7F); + // A 60 dB attack at window 3 splits the run there. + for k in 0..short_len { + for w in 3..8 { + spec[w * short_len + k] *= 1000.0; + } + } + let lengths = decide_short_grouping(&spec, offsets, num_swb); + assert_eq!(lengths, vec![3, 5]); + assert_eq!(grouping_mask(&lengths), 0b110_1111); + // No grouping at all round-trips to mask 0. + assert_eq!(grouping_mask(&[1; 8]), 0); + // Every mask agrees with the decoder-side derivation. + for lengths in [vec![8u8], vec![3, 5], vec![1; 8], vec![2, 1, 4, 1]] { + let mask = grouping_mask(&lengths); + let (_, n, derived, _) = crate::ics_info::derive_window_grouping( + WindowSequence::EightShort, + Some(mask), + fs_index as usize, + ); + assert_eq!(derived, lengths); + assert_eq!(n as usize, lengths.len()); + } + } + + /// A grouped short channel's wire records round-trip through the + /// crate's own parsers: the ics_info grouping, the per-group + /// section spans, and the §4.5.2.3.5 interleaved spectrum come + /// back exactly. + #[test] + fn short_grouping_wire_roundtrip() { + use oxideav_core::bits::BitReader; + let fs_index = 4u8; + let short_len = SHORT_WINDOW_LEN as usize; + // Windows 0..3 carry pattern A, 3..8 a 40 dB louder pattern B + // (the grouping decision splits at the jump). + let mut spec = vec![0.0f64; 8 * short_len]; + for w in 0..8 { + let (gain, phase) = if w < 3 { + (300.0, 0.31) + } else { + (30000.0, 0.11) + }; + for k in 0..short_len { + spec[w * short_len + k] = gain * ((k as f64) * phase).sin(); + } + } + let frame_peak = spec.iter().fold(0.0f64, |m, &v| m.max(v.abs())); + let chan = quantize_channel_short(&spec, fs_index, 0, frame_peak).unwrap(); + assert_eq!(chan.info.window_group_length, vec![3, 5]); + + let mut bw = BitWriter::new(); + chan.body.write(&mut bw, 2, fs_index, false).unwrap(); + chan.spectral + .write(&mut bw, &chan.info, &chan.body.section_data, fs_index) + .unwrap(); + let bytes = bw.finish(); + let mut reader = BitReader::new(&bytes); + let body = IcsBody::parse(&mut reader, 2, fs_index, false).unwrap(); + let ics = body.ics_info.as_ref().unwrap(); + assert_eq!(ics.scale_factor_grouping, Some(0b110_1111)); + assert_eq!(ics.num_window_groups, 2); + assert_eq!(ics.window_group_length, vec![3, 5]); + let spectral = SpectralData::parse(&mut reader, ics, &body.section_data, fs_index).unwrap(); + assert_eq!(spectral, chan.spectral); + assert_eq!(body.section_data, chan.body.section_data); + assert_eq!(body.scale_factor_data, chan.body.scale_factor_data); + } + + /// The `common_window` CPE short path imposes ONE grouping on + /// both channels even when their independent decisions would + /// diverge — a divergent pair would desync the shared-`ics_info` + /// wire layout (verified: reverting to per-channel decisions + /// fails this test). The frame must decode consistently through + /// the stream decoder with the burst energy on the right + /// channels. + #[test] + fn cpe_short_grouping_is_joint() { + let fs_index = 4u8; + let short_len = SHORT_WINDOW_LEN as usize; + let offsets = short_window_offsets(fs_index).unwrap(); + let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; + // L jumps 60 dB at window 2, R at window 5: the independent + // groupings differ (the premise of the regression). + let mut l_spec = vec![0.0f64; 8 * short_len]; + let mut r_spec = vec![0.0f64; 8 * short_len]; + for w in 0..8 { + for k in 0..short_len { + let l_gain = if w < 2 { 30.0 } else { 30000.0 }; + let r_gain = if w < 5 { 30.0 } else { 30000.0 }; + l_spec[w * short_len + k] = l_gain * ((k as f64) * 0.23).sin(); + r_spec[w * short_len + k] = r_gain * ((k as f64) * 0.19).sin(); + } + } + let gl = decide_short_grouping(&l_spec, offsets, num_swb); + let gr = decide_short_grouping(&r_spec, offsets, num_swb); + assert_ne!(gl, gr, "premise: independent groupings diverge"); + + // Encode a stereo stream engineered to hit EIGHT_SHORT with + // per-channel attacks at different windows, and decode it + // with the crate's own decoder — a desynced CPE would fail + // to parse (or reconstruct garbage). + let n = 4 * FRAME_LEN; + let mut pcm = Vec::with_capacity(n * 2); + for i in 0..n { + let t = i as f64; + let in_l = (FRAME_LEN + 256..FRAME_LEN + 640).contains(&i); + let in_r = (FRAME_LEN + 640..FRAME_LEN + 1024).contains(&i); + let base = 400.0 * (0.09 * t).sin(); + let l = base + if in_l { 20000.0 * (0.5 * t).sin() } else { 0.0 }; + let r = base + + if in_r { + 20000.0 * (0.43 * t).sin() + } else { + 0.0 + }; + pcm.push(l.round() as i16); + pcm.push(r.round() as i16); + } + let mut enc = StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: 2, + bitrate: 192_000, + }) + .unwrap(); + let stream = enc.encode_all(&pcm).unwrap(); + let mut dec = crate::decode::StreamDecoder::new(); + let frames = dec.decode_all(&stream).unwrap(); + assert!(frames.len() >= 4); + // The burst energy must come back on the right channels. + let mut decoded = Vec::new(); + for f in &frames { + decoded.extend_from_slice(&f.pcm); + } + let aligned = &decoded[FRAME_LEN * 2..]; + let window_energy = |c: usize, range: core::ops::Range| -> f64 { + range.map(|i| f64::from(aligned[i * 2 + c]).powi(2)).sum() + }; + let l_burst = window_energy(0, FRAME_LEN + 256..FRAME_LEN + 640); + let r_quiet = window_energy(1, FRAME_LEN + 256..FRAME_LEN + 640); + assert!( + l_burst > 20.0 * r_quiet, + "left burst region not reconstructed: {l_burst:.0} vs {r_quiet:.0}" + ); + } + + /// Short-frame M/S: identical (and phase-inverted) channels + /// flag every `(group, sfb)` cell, independent channels flag + /// almost nothing, and an identical-channel transient stream + /// decodes with L exactly equal to R (all-M/S ⇒ `s ≡ 0` ⇒ the + /// de-matrix reproduces one channel twice). + #[test] + fn cpe_short_frame_ms_coding() { + let fs_index = 4u8; + let short_len = SHORT_WINDOW_LEN as usize; + let offsets = short_window_offsets(fs_index).unwrap(); + let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; + let mut spec = vec![0.0f64; 8 * short_len]; + let mut seed = 0x515u32; + for v in spec.iter_mut() { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + *v = f64::from(seed >> 16) - 32768.0; + } + let inverted: Vec = spec.iter().map(|&v| -v).collect(); + let wgl = vec![2u8, 3, 3]; + let same = ms_decide_short(&spec, &spec, offsets, num_swb, &wgl); + assert!(same.iter().flatten().all(|&b| b), "identical pair all-M/S"); + let anti = ms_decide_short(&spec, &inverted, offsets, num_swb, &wgl); + assert!(anti.iter().flatten().all(|&b| b), "inverted pair all-M/S"); + let mut other = vec![0.0f64; 8 * short_len]; + for v in other.iter_mut() { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + *v = f64::from(seed >> 16) - 32768.0; + } + let indep = ms_decide_short(&spec, &other, offsets, num_swb, &wgl); + let flagged = indep.iter().flatten().filter(|&&b| b).count(); + let total = indep.iter().flatten().count(); + assert!( + flagged * 4 < total, + "independent pair flagged {flagged}/{total}" + ); + // apply ∘ decide on the identical pair zeroes the side chain. + let (code_l, code_r) = apply_ms_short(&spec, &spec, &same, offsets, &wgl); + assert_eq!(code_l, spec); + assert!(code_r.iter().all(|&v| v == 0.0)); + + // End to end: identical channels with a percussive burst + // (short frames engaged) decode to L == R exactly. + let n = 4 * FRAME_LEN; + let mut pcm = Vec::with_capacity(n * 2); + for i in 0..n { + let t = i as f64; + let burst = (FRAME_LEN + 256..FRAME_LEN + 640).contains(&i); + let v = 500.0 * (0.07 * t).sin() + + if burst { + 18000.0 * (0.6 * t).sin() + } else { + 0.0 + }; + let s = v.round() as i16; + pcm.push(s); + pcm.push(s); + } + let mut enc = StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: 2, + bitrate: 128_000, + }) + .unwrap(); + let stream = enc.encode_all(&pcm).unwrap(); + let mut dec = crate::decode::StreamDecoder::new(); + let frames = dec.decode_all(&stream).unwrap(); + for (f, frame) in frames.iter().enumerate() { + for i in 0..(frame.pcm.len() / 2) { + assert_eq!( + frame.pcm[2 * i], + frame.pcm[2 * i + 1], + "frame {f} sample {i}: identical channels must decode identical" + ); + } + } + } + + #[test] + fn silent_input_yields_valid_minimal_frames() { + let mut enc = StreamEncoder::new(EncoderConfig { + sample_rate: 44_100, + channels: 1, + bitrate: 64_000, + }) + .unwrap(); + let stream = enc.encode_all(&[0i16; FRAME_LEN]).unwrap(); + // Two frames (content + flush), each parseable. + let (h0, off) = AdtsHeader::parse(&stream).unwrap(); + assert_eq!(h0.channel_configuration, 1); + assert_eq!(off, ADTS_HEADER_BYTES_NO_CRC); + let second = &stream[h0.aac_frame_length as usize..]; + let (h1, _) = AdtsHeader::parse(second).unwrap(); + assert_eq!( + h0.aac_frame_length as usize + h1.aac_frame_length as usize, + stream.len() + ); + } + + /// [`extract_pulse_candidate`] on a band with outlier lines: the + /// reduced spectrum plus the §4.6.3.3 fix-up + /// ([`crate::swb_offset::apply_pulse_data`]) restores the exact + /// original quantized values, and the measured per-band saving + /// is real (the reduced band's cheapest book costs at least the + /// pulse record less). + #[test] + fn pulse_candidate_reduction_is_exactly_invertible() { + let fs_index = 3u8; // 48 kHz + let offsets = long_window_offsets(fs_index).unwrap(); + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + let mut x_quant = vec![0i32; FRAME_LEN]; + let mut sfb_cb = vec![ZERO_HCB; num_swb]; + // Band 29 (32 lines at 48 kHz): a dense small-magnitude + // spectrum with two outliers (one negative) — the outliers + // alone force the whole band onto the ESC book. + let (s, e) = (offsets[29] as usize, offsets[30] as usize); + for (k, slot) in x_quant.iter_mut().enumerate().take(e).skip(s) { + *slot = 1 - ((k as i32) & 2); + } + x_quant[s + 1] = 17; + x_quant[s + 5] = -19; + sfb_cb[29] = codebook_for(19); + let (pd, reduced) = + extract_pulse_candidate(&x_quant, &sfb_cb, offsets).expect("outliers must qualify"); + assert_eq!(pd.pulse_start_sfb, 29); + assert_eq!(pd.pulses.len(), 2); + // The residual codes without the outliers' book demand + // (amp reach is 15, so 17 → 2 and −19 → −4, signs kept). + assert_eq!(reduced[s + 1], 2); + assert_eq!(reduced[s + 5], -4); + // Decode-side fix-up restores the original spectrum exactly. + let mut restored = reduced.clone(); + crate::swb_offset::apply_pulse_data(&mut restored, fs_index, &pd).unwrap(); + assert_eq!(restored, x_quant); + // The candidate's saving is real end to end. + let plain_bits = cheapest_band_bits(&x_quant[s..e]).unwrap() as u64; + let pulsed_bits = + cheapest_band_bits(&reduced[s..e]).unwrap() as u64 + pulse_record_bits(pd.pulses.len()); + assert!( + pulsed_bits < plain_bits, + "pulsed {pulsed_bits} vs plain {plain_bits}" + ); + } + + /// A spectrum-wide no-outlier profile yields no candidate, and a + /// candidate that does not measure smaller is dropped by the + /// [`quantize_channel`] decision (the emitted stream stays + /// pulse-free on flat content). + #[test] + fn pulse_candidate_requires_a_measured_win() { + let fs_index = 3u8; + let offsets = long_window_offsets(fs_index).unwrap(); + let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; + // Flat band: every line the same magnitude — no outlier. + let mut x_quant = vec![0i32; FRAME_LEN]; + let mut sfb_cb = vec![ZERO_HCB; num_swb]; + let (s, e) = (offsets[10] as usize, offsets[11] as usize); + x_quant[s..e].fill(3); + sfb_cb[10] = codebook_for(3); + assert!(extract_pulse_candidate(&x_quant, &sfb_cb, offsets).is_none()); + } + + /// End-to-end pulse emission through [`quantize_channel`]: a + /// long-frame spectrum whose loud band carries one outlier line + /// over a low floor selects the pulse variant, the wire record + /// round-trips, and the §4.6.3.3 fix-up on the transmitted + /// spectrum reproduces the pulse-free quantization exactly (the + /// reconstruction is bit-identical by construction). + #[test] + fn quantize_channel_emits_measured_pulse_data() { + let fs_index = 3u8; // 48 kHz + let offsets = long_window_offsets(fs_index).unwrap(); + // Craft a spectrum: wide band 29 carries a moderate peak + // (the frame peak lives in band 20 so band 29's masking + // target lands near the escape threshold) plus a dense low + // floor across the band. + let mut spec = vec![0.0f64; FRAME_LEN]; + let (s29, e29) = (offsets[29] as usize, offsets[30] as usize); + spec[offsets[20] as usize] = 1.0; // frame peak, own band + spec[s29 + 3] = 0.17; // the outlier line + for (k, slot) in spec.iter_mut().enumerate().take(e29).skip(s29) { + if k != s29 + 3 { + *slot = 0.0095; // the band floor + } + } + let chan = + quantize_channel(&spec, WindowSequence::OnlyLong, fs_index, 0, 1.0, &[], &[]).unwrap(); + let plain = quantize_group( + &spec, + offsets, + NUM_SWB_LONG_WINDOW[fs_index as usize] as usize, + 0, + 1.0, + &mut None, + &[], + ); + assert!( + chan.body.pulse_data_present, + "outlier-over-floor band must select the pulse variant" + ); + let pd = chan.body.pulse_data.as_ref().unwrap(); + assert!((1..=MAX_PULSES).contains(&pd.pulses.len())); + // The transmitted spectrum restores to the plain quantization. + let mut restored = chan.spectral.x_quant[0].clone(); + crate::swb_offset::apply_pulse_data(&mut restored, fs_index, pd).unwrap(); + assert_eq!(restored, plain.x_quant); + // And the pulse variant is the smaller stream. + let plain_chan = { + let info = chan.info.clone(); + finish_channel(info, vec![plain], fs_index, None).unwrap() + }; + assert!( + channel_wire_bits(&chan, fs_index).unwrap() + < channel_wire_bits(&plain_chan, fs_index).unwrap() + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/encoder_tns.rs b/crates/vendor/oxideav-aac/src/encoder_tns.rs new file mode 100644 index 00000000..a2510ebd --- /dev/null +++ b/crates/vendor/oxideav-aac/src/encoder_tns.rs @@ -0,0 +1,421 @@ +//! Encoder-side §4.6.9 Temporal Noise Shaping — decision, PARCOR +//! quantisation, and the analysis filtering pass. +//! +//! TNS is defined normatively from the decoder side only: §4.6.9.3 +//! specifies how transmitted filter coefficients are inverse-quantised +//! (`tns_decode_coef`), stepped up to LPC, and slid across the +//! spectrum as an **all-pole synthesis filter**. The encoder's job is +//! the inverse: pick a prediction filter over the spectral +//! coefficients, quantise it into the Table 4.54 wire fields, and run +//! the **all-zero analysis filter** (§4.6.7.4.1's `tns_ma_filter`, +//! the exact inverse of the synthesis filter over a shared region) on +//! the spectrum before quantisation, so the decoder's synthesis pass +//! reconstructs the original while shaping the quantisation noise in +//! time. +//! +//! Everything analysis-side (the autocorrelation, the Levinson-Durbin +//! recursion, the activation threshold) is an encoder degree of +//! freedom — any filter whose wire record is Table 4.54-conforming is +//! a conforming encode. The *applied* filter, however, must be +//! bit-identical to the one the decoder will derive from the wire, so +//! this module quantises the reflection coefficients first +//! ([`crate::tns_coef::tns_encode_coef`]) and then filters through +//! [`crate::tns_frame::tns_analysis_frame`], which re-derives the LPC +//! from the **wire** values exactly as `tns_decode_frame` does. The +//! encoder/decoder filter pair is therefore the §4.6.9.3 +//! analysis∘synthesis identity by construction. +//! +//! ## Decision rule +//! +//! Per transform window the encoder computes the autocorrelation of +//! the coverable spectral region (the same +//! `min(num_swb, TNS_MAX_BANDS, max_sfb)`-clamped region the §4.6.9.3 +//! walk will filter), runs Levinson-Durbin up to +//! [`TNS_ENC_MAX_ORDER`], and activates TNS only when the resulting +//! prediction gain `r(0) / err(order)` clears [`TNS_GAIN_MIN`]. A +//! high prediction gain over *frequency* coefficients means the +//! signal's *temporal* envelope inside the window is strongly +//! non-flat (the time/frequency duality TNS exploits, §4.6.9.1) — +//! exactly the windows where unshaped quantisation noise smears +//! audibly. Trailing reflection coefficients below +//! [`TNS_COEF_TRIM`] are trimmed to keep the order (and the 4-bit +//! coefficient payload) minimal. + +use crate::ics_info::WindowSequence; +use crate::swb_offset::{ + long_window_offsets, short_window_offsets, LONG_WINDOW_LEN, SHORT_WINDOW_LEN, +}; +use crate::tns_coef::tns_encode_coef; +use crate::tns_data::{num_windows, TnsData, TnsFilter, TnsWindow}; +use crate::tns_frame::tns_analysis_frame; +use crate::tns_max::{clamp_tns_band, tns_max_order, AOT_AAC_LC}; +use crate::Result; + +/// Encoder-side cap on the TNS filter order. Table 4.102 allows up +/// to 12 for AAC LC long windows (7 short), but each tap costs 4 +/// wire bits and the marginal gain past order 8 is small for a +/// first-order envelope model; the Levinson recursion below stops +/// early anyway once the prediction error stops shrinking. +pub const TNS_ENC_MAX_ORDER: usize = 8; + +/// Minimum §4.6.9.1 prediction gain (`r(0) / err`) for TNS to +/// activate on a window. Below ~1.4 the temporal envelope is close +/// enough to flat that the side-info bits outweigh the shaping win. +pub const TNS_GAIN_MIN: f64 = 1.4; + +/// Reflection-coefficient trim threshold: trailing PARCOR values +/// with `|k|` below this contribute negligible shaping and are +/// dropped to shorten the transmitted order. +pub const TNS_COEF_TRIM: f64 = 0.1; + +/// `coef_res` the encoder always transmits: `true` selects the 4-bit +/// (`coef_res_bits == 4`) resolution of §4.6.9.3, the finer of the +/// two grids. +const TNS_COEF_RES: bool = true; + +/// One window's TNS decision: the reflection coefficients that +/// survived the gain threshold and trim, ready for quantisation. +struct WindowDecision { + /// PARCOR reflection coefficients, order `parcor.len()`. + parcor: Vec, +} + +/// Autocorrelation `r[0..=max_lag]` of `region`. +fn autocorrelation(region: &[f64], max_lag: usize) -> Vec { + let n = region.len(); + (0..=max_lag.min(n.saturating_sub(1))) + .map(|lag| (0..n - lag).map(|i| region[i] * region[i + lag]).sum()) + .collect() +} + +/// Levinson-Durbin recursion on the autocorrelation `r`, returning +/// the reflection (PARCOR) coefficients and the final prediction +/// error. The per-step update matches the §4.6.9.3 step-up +/// ([`crate::tns_coef::lpc_step_up`]) convention — `a_m[i] = +/// a_{m-1}[i] + k_m · a_{m-1}[m-i]`, `a_m[m] = k_m` — so the +/// returned `k` values, once quantised and stepped up by the +/// decoder, reproduce this exact predictor. The analysis filter is +/// then `y(n) = x(n) + Σ a[i]·x(n-i)` (the §4.6.7.4.1 +/// `tns_ma_filter` polarity), i.e. `a[]` is the prediction-*error* +/// filter tail. +fn levinson(r: &[f64], max_order: usize) -> (Vec, f64) { + let mut err = r[0]; + if err <= 0.0 { + return (Vec::new(), err); + } + let order = max_order.min(r.len().saturating_sub(1)); + let mut a = vec![0.0f64; order + 1]; + a[0] = 1.0; + let mut k_out = Vec::with_capacity(order); + let mut b = vec![0.0f64; order + 1]; + for m in 1..=order { + // acc = r[m] + Σ_{i=1}^{m-1} a[i]·r[m-i] + let mut acc = r[m]; + for i in 1..m { + acc += a[i] * r[m - i]; + } + let k = -acc / err; + if !k.is_finite() || k.abs() >= 1.0 { + // Numerically degenerate (r not positive definite at + // this order) — stop with the taps found so far. + break; + } + // Step-up update, mirroring lpc_step_up so the decoder's + // reconstruction of `a` from the k's is this exact array. + for i in 1..m { + b[i] = a[i] + k * a[m - i]; + } + a[1..m].copy_from_slice(&b[1..m]); + a[m] = k; + k_out.push(k); + err *= 1.0 - k * k; + if err <= 0.0 { + break; + } + } + (k_out, err) +} + +/// Decide TNS for one window's coverable region. Returns `None` +/// when the prediction gain does not clear [`TNS_GAIN_MIN`] or the +/// trim leaves no taps. +fn decide_window(region: &[f64], max_order: usize) -> Option { + if region.len() < 2 * max_order.max(1) { + return None; + } + let r = autocorrelation(region, max_order); + if r[0] <= 0.0 { + return None; + } + let (mut parcor, err) = levinson(&r, max_order); + if parcor.is_empty() || err <= 0.0 { + return None; + } + let gain = r[0] / err; + if gain < TNS_GAIN_MIN { + return None; + } + while parcor.last().is_some_and(|k| k.abs() < TNS_COEF_TRIM) { + parcor.pop(); + } + if parcor.is_empty() { + return None; + } + Some(WindowDecision { parcor }) +} + +/// Detect and apply §4.6.9 TNS to one channel's analysis spectrum in +/// place. +/// +/// `spec` is the window-major forward-MDCT spectrum +/// (`num_windows × window_len`, the encoder's analysis output before +/// quantisation), `seq` / `max_sfb` / `fs_index` the surrounding +/// `ics_info()` parameters (the encoder transmits `max_sfb == +/// num_swb`). `permit` is the caller's per-window **temporal** gate +/// (length [`num_windows`], see below); for every permitted window +/// whose coverable region clears the [`TNS_GAIN_MIN`] +/// prediction-gain threshold, one upward filter covering the full +/// §4.6.9.3-clamped band range is quantised into Table 4.54 wire +/// fields; the whole-frame [`TnsData`] is then run through +/// [`tns_analysis_frame`] — deriving the LPC from the **wire** +/// coefficient values exactly as the decoder's `tns_decode_frame` +/// will — so the applied analysis filter and the decoder's synthesis +/// filter are exact inverses. +/// +/// ## Why a temporal gate +/// +/// Spectral prediction gain alone over-fires: a *steady tonal* +/// window also shows LPC gain over its MDCT coefficients (the smooth +/// leakage skirts around each spectral line are highly predictable) +/// even though its temporal envelope is flat — exactly the windows +/// where TNS buys nothing and merely re-shapes (and, at spectral +/// peaks, locally amplifies) the quantisation noise of a +/// peak-anchored rate allocation. The §4.6.9.1 duality says TNS pays +/// off when the *time-domain* envelope inside the window is strongly +/// non-flat, which the encoder can measure directly on its input +/// samples — so the caller derives `permit[w]` from the raw +/// subblock-energy flatness of window `w`'s time region (see +/// `StreamEncoder`'s hop driver) and this module only spends +/// prediction-gain analysis on permitted windows. +/// +/// Returns `Ok(None)` (spectrum untouched) when no window activates. +pub fn detect_and_apply_tns( + spec: &mut [f64], + seq: WindowSequence, + max_sfb: u8, + fs_index: u8, + permit: &[bool], +) -> Result> { + let nw = num_windows(seq); + let (window_len, offsets) = if seq.is_eight_short() { + (SHORT_WINDOW_LEN as usize, short_window_offsets(fs_index)?) + } else { + (LONG_WINDOW_LEN as usize, long_window_offsets(fs_index)?) + }; + let num_swb = offsets.len() - 1; + // The §4.6.9.3 region for a full-length (length == num_swb, + // bottom == 0) upward filter: [swb_offset[0], + // swb_offset[min(num_swb, TNS_MAX_BANDS, max_sfb)]). + let top = clamp_tns_band(num_swb as u8, max_sfb, AOT_AAC_LC, seq, fs_index)? as usize; + let end = offsets[top] as usize; + let max_order = TNS_ENC_MAX_ORDER.min(tns_max_order(AOT_AAC_LC, seq, fs_index)? as usize); + + let mut windows = Vec::with_capacity(nw); + let mut any = false; + for w in 0..nw { + let region = &spec[w * window_len..w * window_len + end]; + let decision = if permit.get(w).copied().unwrap_or(false) { + decide_window(region, max_order) + } else { + None + }; + let filters = match decision { + Some(d) => { + // Quantise PARCOR → wire coef[] (4-bit grid). The + // analysis pass below re-derives the LPC from these + // wire values, so the filter actually applied is the + // quantised one the decoder will invert. + let coef = tns_encode_coef(4, 0, &d.parcor)? + .into_iter() + .map(|c| c as u8) + .collect::>(); + any = true; + vec![TnsFilter { + length: num_swb as u8, + order: coef.len() as u8, + direction: false, + coef_compress: false, + coef, + }] + } + None => Vec::new(), + }; + windows.push(TnsWindow { + coef_res: TNS_COEF_RES, + filters, + }); + } + if !any { + return Ok(None); + } + let tns = TnsData { windows }; + tns_analysis_frame(spec, &tns, seq, max_sfb, AOT_AAC_LC, fs_index)?; + Ok(Some(tns)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::WindowSequence; + use crate::tns_frame::tns_decode_frame; + + /// Deterministic pseudo-noise in [-1, 1). + fn noise(n: usize, seed: u32) -> Vec { + let mut state = seed; + (0..n) + .map(|_| { + state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + (state as i32) as f64 / 2_147_483_648.0 + }) + .collect() + } + + /// Run `x` through the all-pole filter `1 / (1 + Σ a[i] z^-i)` + /// (the synthesis polarity), producing a strongly correlated + /// sequence whose optimal prediction-error filter is `a`. + fn all_pole(x: &[f64], a: &[f64]) -> Vec { + let mut y = vec![0.0f64; x.len()]; + for n in 0..x.len() { + let mut v = x[n]; + for (i, &ai) in a.iter().enumerate() { + let d = i + 1; + if n >= d { + v -= ai * y[n - d]; + } + } + y[n] = v; + } + y + } + + #[test] + fn levinson_recovers_ar1_reflection() { + // AR(1) with pole 0.8: prediction-error filter a = [-0.8], + // reflection k1 = -0.8. + let x = noise(4096, 0xC0FF_EE00); + let y = all_pole(&x, &[-0.8]); + let r = autocorrelation(&y, 4); + let (k, err) = levinson(&r, 4); + assert!(!k.is_empty()); + assert!( + (k[0] + 0.8).abs() < 0.05, + "k1 = {} should approximate -0.8", + k[0] + ); + // Prediction gain ≈ 1/(1 - 0.64) ≈ 2.8. + let gain = r[0] / err; + assert!(gain > 2.0, "gain {gain} too low for AR(1) 0.8"); + } + + #[test] + fn white_region_stays_untns() { + // A flat (white) region has prediction gain ≈ 1 — below the + // threshold — so no filter fires. + let region = noise(512, 0xDEAD_BEEF); + assert!(decide_window(®ion, TNS_ENC_MAX_ORDER).is_none()); + } + + #[test] + fn correlated_region_activates_and_analysis_whitens() { + // Long window, fs 48 kHz. Fill the coverable region with a + // strongly correlated AR process; TNS must fire, the analysis + // pass must reduce the region's energy (whitening), and the + // decoder's tns_decode_frame must restore the original + // spectrum exactly (the §4.6.9.3 analysis∘synthesis + // identity on the shared quantised filter). + let fs = 3u8; + let seq = WindowSequence::OnlyLong; + let n = LONG_WINDOW_LEN as usize; + let x = noise(n, 0x1234_5678); + let mut spec = all_pole(&x, &[-1.2, 0.5]); + // Scale to a realistic coefficient magnitude. + for v in spec.iter_mut() { + *v *= 1000.0; + } + let original = spec.clone(); + let max_sfb = (long_window_offsets(fs).unwrap().len() - 1) as u8; + + let tns = detect_and_apply_tns(&mut spec, seq, max_sfb, fs, &[true]) + .unwrap() + .expect("correlated spectrum must activate TNS"); + assert_eq!(tns.windows.len(), 1); + assert_eq!(tns.windows[0].filters.len(), 1); + let f = &tns.windows[0].filters[0]; + assert!(f.order >= 1); + assert_eq!(f.coef.len(), f.order as usize); + assert!(!f.direction); + + let e = |s: &[f64]| s.iter().map(|&v| v * v).sum::(); + assert!( + e(&spec) < 0.8 * e(&original), + "analysis should whiten: {} vs {}", + e(&spec), + e(&original) + ); + + // Round-trip: the decoder synthesis restores the original. + tns_decode_frame(&mut spec, &tns, seq, max_sfb, AOT_AAC_LC, fs).unwrap(); + for (a, b) in spec.iter().zip(original.iter()) { + assert!((a - b).abs() < 1e-6, "synthesis must invert analysis"); + } + } + + #[test] + fn short_windows_decide_independently() { + // Eight short windows: give window 3 a correlated region and + // leave the rest white — only window 3 fires. + let fs = 3u8; + let seq = WindowSequence::EightShort; + let wlen = SHORT_WINDOW_LEN as usize; + let mut spec = vec![0.0f64; 8 * wlen]; + for w in 0..8 { + let seed = 0x9E37_79B9u32.wrapping_add(w as u32); + let x = noise(wlen, seed); + let win = if w == 3 { + all_pole(&x, &[-1.4, 0.6]) + } else { + x + }; + for (i, v) in win.iter().enumerate() { + spec[w * wlen + i] = v * 500.0; + } + } + let max_sfb = (short_window_offsets(fs).unwrap().len() - 1) as u8; + let tns = detect_and_apply_tns(&mut spec, seq, max_sfb, fs, &[true; 8]) + .unwrap() + .expect("window 3 must activate"); + assert_eq!(tns.windows.len(), 8); + assert!(!tns.windows[3].filters.is_empty(), "window 3 fires"); + for w in [0usize, 1, 2, 4, 5, 6, 7] { + assert!( + tns.windows[w].filters.is_empty(), + "white window {w} must not fire" + ); + } + // Short-window field caps: order ≤ 7, length fits 4 bits. + let f = &tns.windows[3].filters[0]; + assert!(f.order <= 7); + assert!(f.length <= 15); + } + + #[test] + fn silent_spectrum_never_activates() { + let fs = 4u8; + let mut spec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let max_sfb = (long_window_offsets(fs).unwrap().len() - 1) as u8; + let tns = detect_and_apply_tns(&mut spec, WindowSequence::OnlyLong, max_sfb, fs, &[true]) + .unwrap(); + assert!(tns.is_none()); + assert!(spec.iter().all(|&v| v == 0.0)); + } +} diff --git a/crates/vendor/oxideav-aac/src/ep_config.rs b/crates/vendor/oxideav-aac/src/ep_config.rs new file mode 100644 index 00000000..14d7efc6 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ep_config.rs @@ -0,0 +1,586 @@ +//! `ErrorProtectionSpecificConfig()` — ISO/IEC 14496-3 §1.8.2.1 +//! Table 1.49, the out-of-band half of the §1.8 error-protection (EP) +//! tool, plus the §1.8.4.2 pre-defined-set derivation. +//! +//! The EP tool protects an access unit as a sequence of *classes* +//! (§1.8.1): each class carries a CRC (§1.8.4.5), an FEC — SRCPC +//! (§1.8.4.6) or shortened Reed-Solomon (§1.8.4.7) — and optional +//! interleaving (§1.8.4.8). Everything constant across frames rides +//! this configuration; the per-frame remainder (choice of pre-defined +//! set, escaped class parameters, stuffing count) rides the in-band +//! `ep_header()` (§1.8.2.2 / §1.8.4.3). +//! +//! The `class_optional` unwrapping (§1.8.4.2) expands every wire +//! pre-defined set with `N` optional classes into `2^N` transmission +//! sets — from "all optional classes present" (`j == 0`) down to +//! "none present"; [`ErrorProtectionSpecificConfig::expand`] is that +//! algorithm verbatim, and the in-band `choice_of_pred` indexes the +//! **expanded** list. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::crc::CrcPoly; +use crate::{Error, Result}; + +/// Per-class parameters of one wire pre-defined set (Table 1.49 inner +/// loop). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EpClass { + /// `length_escape` — `true` ⇒ the class length is signalled + /// in-band with `number_of_bits_for_length` bits (0 = the + /// §1.8.4.1 "until the end" class). + pub length_escape: bool, + /// `rate_escape` — `true` ⇒ the code rate is signalled in-band. + pub rate_escape: bool, + /// `crclen_escape` — `true` ⇒ the CRC length is signalled + /// in-band. + pub crclen_escape: bool, + /// `concatenate_flag` — present on the wire only when + /// `number_of_concatenated_frame != 1` (§1.8.4.4); `false` + /// otherwise. + pub concatenate_flag: bool, + /// `fec_type` (2 bits): `0` SRCPC; `1` RS (last / independent); + /// `2` RS concatenated with the next class. + pub fec_type: u8, + /// `termination_switch` — present iff `fec_type == 0` + /// (§1.8.4.6.2). + pub termination_switch: Option, + /// `interleave_switch` (2 bits) — present iff + /// `interleave_type == 2` (Table 1.64). + pub interleave_switch: Option, + /// `class_optional` — the §1.8.4.2 expansion flag. + pub class_optional: bool, + /// `number_of_bits_for_length` (4 bits) iff `length_escape`. + pub number_of_bits_for_length: Option, + /// `class_length` (16 bits) iff `!length_escape`. **Bits** for + /// SRCPC classes; must be a whole number of octets for RS classes + /// (§1.8.3.1 `fec_type`). + pub class_length: Option, + /// `class_rate` iff `!rate_escape` — 5 bits for SRCPC (0..=24 ⇒ + /// rate 8/8..8/32), 7 bits for RS (the number of correctable + /// bytes `k`, §1.8.4.7). + pub class_rate: Option, + /// `class_crclen` (5 bits) iff `!crclen_escape` — 0..=18 ⇒ CRC + /// length 0..=16 / 24 / 32 (§1.8.3.1). + pub class_crclen: Option, +} + +impl EpClass { + /// Resolve the §1.8.3.1 `class_crclen` code (0..=18) to a CRC bit + /// width. + pub fn crclen_bits(code: u8) -> Result { + Ok(match code { + 0..=16 => u32::from(code), + 17 => 24, + 18 => 32, + _ => return Err(Error::EpConfigInvalid), + }) + } + + /// The §1.8.4.5 generator for a CRC width produced by + /// [`EpClass::crclen_bits`] (widths 1..=16, 24, 32). + pub fn crc_poly(width: u32) -> Result> { + Ok(Some(match width { + 0 => return Ok(None), + 1 => CrcPoly::Crc1, + 2 => CrcPoly::Crc2, + 3 => CrcPoly::Crc3, + 4 => CrcPoly::Crc4, + 5 => CrcPoly::Crc5, + 6 => CrcPoly::Crc6, + 7 => CrcPoly::Crc7, + 8 => CrcPoly::Crc8, + 9 => CrcPoly::Crc9, + 10 => CrcPoly::Crc10, + 11 => CrcPoly::Crc11, + 12 => CrcPoly::Crc12, + 13 => CrcPoly::Crc13, + 14 => CrcPoly::Crc14, + 15 => CrcPoly::Crc15, + 16 => CrcPoly::Crc16, + 24 => CrcPoly::Crc24, + 32 => CrcPoly::Crc32, + _ => return Err(Error::EpConfigInvalid), + })) + } +} + +/// One pre-defined set (Table 1.49 outer loop): the class list plus +/// the §1.8.4.9 output reordering. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EpPredefinedSet { + /// The per-class parameter list. + pub classes: Vec, + /// `class_reordered_output` (§1.8.4.9). + pub class_reordered_output: bool, + /// `class_output_order[j]` (6 bits each) iff reordered: the j-th + /// EP-frame class is output as the `class_output_order[j]`-th + /// class to the audio decoder. + pub class_output_order: Vec, +} + +/// Parsed `ErrorProtectionSpecificConfig()` (Table 1.49). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ErrorProtectionSpecificConfig { + /// `interleave_type` (2 bits): 0 none, 1 intra-frame, 2 per-class + /// fine tuning; 3 reserved (rejected). + pub interleave_type: u8, + /// `bit_stuffing` (3 bits): 1 ⇒ `num_stuffing_bits` rides + /// `class_attrib()`. + pub bit_stuffing: u8, + /// `number_of_concatenated_frame` (3 bits): source frames per EP + /// frame; 0 is reserved (Table 1.54). + pub number_of_concatenated_frame: u8, + /// The wire pre-defined sets (before §1.8.4.2 expansion). + pub sets: Vec, + /// `header_protection`: extended in-band header FEC (§1.8.4.3). + pub header_protection: bool, + /// `header_rate` (5 bits) iff `header_protection`. + pub header_rate: Option, + /// `header_crclen` (5 bits) iff `header_protection`. + pub header_crclen: Option, +} + +impl ErrorProtectionSpecificConfig { + /// Parse a Table 1.49 configuration. + pub fn parse(reader: &mut BitReader<'_>) -> Result { + let number_of_predefined_set = read_u8(reader, 8)?; + let interleave_type = read_u8(reader, 2)?; + if interleave_type == 3 { + // §1.8.3.1: reserved. + return Err(Error::EpConfigInvalid); + } + let bit_stuffing = read_u8(reader, 3)?; + let number_of_concatenated_frame = read_u8(reader, 3)?; + if number_of_concatenated_frame == 0 { + // Table 1.54: codeword 000 is reserved. + return Err(Error::EpConfigInvalid); + } + let mut sets = Vec::with_capacity(usize::from(number_of_predefined_set)); + for _i in 0..number_of_predefined_set { + let number_of_class = read_u8(reader, 6)?; + let mut classes = Vec::with_capacity(usize::from(number_of_class)); + for _j in 0..number_of_class { + let length_escape = read_bit(reader)?; + let rate_escape = read_bit(reader)?; + let crclen_escape = read_bit(reader)?; + let concatenate_flag = if number_of_concatenated_frame != 1 { + read_bit(reader)? + } else { + false + }; + let fec_type = read_u8(reader, 2)?; + if fec_type == 3 { + return Err(Error::EpConfigInvalid); + } + let termination_switch = if fec_type == 0 { + Some(read_bit(reader)?) + } else { + None + }; + let interleave_switch = if interleave_type == 2 { + let v = read_u8(reader, 2)?; + // Table 1.64: width-28 intraclass interleaving is + // SRCPC-only. + if v == 2 && fec_type != 0 { + return Err(Error::EpConfigInvalid); + } + Some(v) + } else { + None + }; + let class_optional = read_bit(reader)?; + let (number_of_bits_for_length, class_length) = if length_escape { + (Some(read_u8(reader, 4)?), None) + } else { + (None, Some(read_u16(reader, 16)?)) + }; + let class_rate = if !rate_escape { + let bits = if fec_type != 0 { 7 } else { 5 }; + let v = read_u8(reader, bits)?; + if fec_type == 0 && v > 24 { + // §1.8.3.1: 0..=24 map onto 8/8..8/32. + return Err(Error::EpConfigInvalid); + } + Some(v) + } else { + None + }; + let class_crclen = if !crclen_escape { + let v = read_u8(reader, 5)?; + EpClass::crclen_bits(v)?; + Some(v) + } else { + None + }; + classes.push(EpClass { + length_escape, + rate_escape, + crclen_escape, + concatenate_flag, + fec_type, + termination_switch, + interleave_switch, + class_optional, + number_of_bits_for_length, + class_length, + class_rate, + class_crclen, + }); + } + let class_reordered_output = read_bit(reader)?; + let mut class_output_order = Vec::new(); + if class_reordered_output { + for _j in 0..number_of_class { + let v = read_u8(reader, 6)?; + if v >= number_of_class { + return Err(Error::EpConfigInvalid); + } + class_output_order.push(v); + } + // The order must be a permutation of 0..number_of_class. + let mut seen = vec![false; usize::from(number_of_class)]; + for &v in &class_output_order { + if core::mem::replace(&mut seen[usize::from(v)], true) { + return Err(Error::EpConfigInvalid); + } + } + } + sets.push(EpPredefinedSet { + classes, + class_reordered_output, + class_output_order, + }); + } + let header_protection = read_bit(reader)?; + let (header_rate, header_crclen) = if header_protection { + let rate = read_u8(reader, 5)?; + if rate > 24 { + return Err(Error::EpConfigInvalid); + } + let crclen = read_u8(reader, 5)?; + EpClass::crclen_bits(crclen)?; + (Some(rate), Some(crclen)) + } else { + (None, None) + }; + Ok(ErrorProtectionSpecificConfig { + interleave_type, + bit_stuffing, + number_of_concatenated_frame, + sets, + header_protection, + header_rate, + header_crclen, + }) + } + + /// Emit the Table 1.49 configuration — the bit-exact inverse of + /// [`ErrorProtectionSpecificConfig::parse`]. + pub fn write(&self, w: &mut BitWriter) -> Result<()> { + if self.sets.len() > 255 + || self.interleave_type > 2 + || self.number_of_concatenated_frame == 0 + || self.number_of_concatenated_frame > 7 + || self.bit_stuffing > 7 + { + return Err(Error::EpConfigInvalid); + } + w.write_u32(self.sets.len() as u32, 8); + w.write_u32(u32::from(self.interleave_type), 2); + w.write_u32(u32::from(self.bit_stuffing), 3); + w.write_u32(u32::from(self.number_of_concatenated_frame), 3); + for set in &self.sets { + if set.classes.len() > 63 { + return Err(Error::EpConfigInvalid); + } + w.write_u32(set.classes.len() as u32, 6); + for c in &set.classes { + w.write_bit(c.length_escape); + w.write_bit(c.rate_escape); + w.write_bit(c.crclen_escape); + if self.number_of_concatenated_frame != 1 { + w.write_bit(c.concatenate_flag); + } + if c.fec_type > 2 { + return Err(Error::EpConfigInvalid); + } + w.write_u32(u32::from(c.fec_type), 2); + if c.fec_type == 0 { + w.write_bit(c.termination_switch.ok_or(Error::EpConfigInvalid)?); + } + if self.interleave_type == 2 { + w.write_u32( + u32::from(c.interleave_switch.ok_or(Error::EpConfigInvalid)?), + 2, + ); + } + w.write_bit(c.class_optional); + if c.length_escape { + let n = c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?; + if n > 15 { + return Err(Error::EpConfigInvalid); + } + w.write_u32(u32::from(n), 4); + } else { + w.write_u32(u32::from(c.class_length.ok_or(Error::EpConfigInvalid)?), 16); + } + if !c.rate_escape { + let bits = if c.fec_type != 0 { 7 } else { 5 }; + w.write_u32(u32::from(c.class_rate.ok_or(Error::EpConfigInvalid)?), bits); + } + if !c.crclen_escape { + w.write_u32(u32::from(c.class_crclen.ok_or(Error::EpConfigInvalid)?), 5); + } + } + w.write_bit(set.class_reordered_output); + if set.class_reordered_output { + if set.class_output_order.len() != set.classes.len() { + return Err(Error::EpConfigInvalid); + } + for &v in &set.class_output_order { + w.write_u32(u32::from(v), 6); + } + } + } + w.write_bit(self.header_protection); + if self.header_protection { + w.write_u32( + u32::from(self.header_rate.ok_or(Error::EpConfigInvalid)?), + 5, + ); + w.write_u32( + u32::from(self.header_crclen.ok_or(Error::EpConfigInvalid)?), + 5, + ); + } + Ok(()) + } + + /// §1.8.4.2 — expand the `class_optional` flags into the + /// transmission pre-defined sets the in-band `choice_of_pred` + /// indexes. + /// + /// Each wire set with `N` optional classes yields `2^N` sets, from + /// "all optional classes present" (`j == 0`) to "none present" + /// (`j == 2^N − 1`); bit `k` of `j` clears the `k`-th optional + /// class. The expanded sets carry `class_optional == false` + /// throughout. + pub fn expand(&self) -> Result> { + let mut out = Vec::new(); + for set in &self.sets { + let opt_idx: Vec = set + .classes + .iter() + .enumerate() + .filter(|(_, c)| c.class_optional) + .map(|(i, _)| i) + .collect(); + let nco = opt_idx.len(); + if nco > 16 { + // 2^N sets would be unbounded; a conforming config + // never needs this many optional classes. + return Err(Error::EpConfigInvalid); + } + for j in 0u32..(1u32 << nco) { + let mut classes = Vec::with_capacity(set.classes.len()); + let mut kept_index = Vec::with_capacity(set.classes.len()); + for (i, c) in set.classes.iter().enumerate() { + let keep = match opt_idx.iter().position(|&o| o == i) { + Some(k) => j & (1 << k) == 0, + None => true, + }; + if keep { + let mut cc = c.clone(); + cc.class_optional = false; + classes.push(cc); + kept_index.push(i); + } + } + // The output order shrinks with the dropped classes: + // surviving entries keep their relative order. + let class_output_order = if set.class_reordered_output { + let mut order: Vec = Vec::with_capacity(classes.len()); + // Rank the surviving original output positions. + let mut kept_orders: Vec = kept_index + .iter() + .map(|&i| set.class_output_order[i]) + .collect(); + let mut sorted = kept_orders.clone(); + sorted.sort_unstable(); + for v in kept_orders.iter_mut() { + let rank = sorted.iter().position(|&s| s == *v).unwrap_or(0) as u8; + order.push(rank); + } + order + } else { + Vec::new() + }; + out.push(EpPredefinedSet { + classes, + class_reordered_output: set.class_reordered_output, + class_output_order, + }); + } + } + if out.is_empty() { + return Err(Error::EpConfigInvalid); + } + Ok(out) + } +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} + +fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { + Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +fn read_u16(reader: &mut BitReader<'_>, bits: u32) -> Result { + Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u16) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn simple_class(len: u16, rate: u8, crclen: u8) -> EpClass { + EpClass { + length_escape: false, + rate_escape: false, + crclen_escape: false, + concatenate_flag: false, + fec_type: 0, + termination_switch: Some(true), + interleave_switch: None, + class_optional: false, + number_of_bits_for_length: None, + class_length: Some(len), + class_rate: Some(rate), + class_crclen: Some(crclen), + } + } + + #[test] + fn roundtrip_two_sets() { + let cfg = ErrorProtectionSpecificConfig { + interleave_type: 0, + bit_stuffing: 0, + number_of_concatenated_frame: 1, + sets: vec![ + EpPredefinedSet { + classes: vec![simple_class(40, 8, 6), simple_class(100, 0, 0)], + class_reordered_output: false, + class_output_order: Vec::new(), + }, + EpPredefinedSet { + classes: vec![simple_class(24, 24, 8)], + class_reordered_output: false, + class_output_order: Vec::new(), + }, + ], + header_protection: false, + header_rate: None, + header_crclen: None, + }; + let mut w = BitWriter::new(); + cfg.write(&mut w).unwrap(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let parsed = ErrorProtectionSpecificConfig::parse(&mut r).unwrap(); + assert_eq!(parsed, cfg); + } + + /// The §1.8.4.2 example: pred #0 with optional classes A, C, E of + /// {A, B, C, D, E} and pred #1 with optional F of {F, G} expand + /// into the Table 1.58 ten sets. + #[test] + fn expansion_matches_table_1_58() { + // Give every class a distinct length so the expanded sets are + // recognisable. + let mk = |len: u16, opt: bool| EpClass { + class_optional: opt, + ..simple_class(len, 0, 0) + }; + let cfg = ErrorProtectionSpecificConfig { + interleave_type: 0, + bit_stuffing: 0, + number_of_concatenated_frame: 1, + sets: vec![ + EpPredefinedSet { + // A=1(opt) B=2 C=3(opt) D=4 E=5(opt) + classes: vec![ + mk(1, true), + mk(2, false), + mk(3, true), + mk(4, false), + mk(5, true), + ], + class_reordered_output: false, + class_output_order: Vec::new(), + }, + EpPredefinedSet { + // F=6(opt) G=7 + classes: vec![mk(6, true), mk(7, false)], + class_reordered_output: false, + class_output_order: Vec::new(), + }, + ], + header_protection: false, + header_rate: None, + header_crclen: None, + }; + let expanded = cfg.expand().unwrap(); + let lens: Vec> = expanded + .iter() + .map(|s| s.classes.iter().map(|c| c.class_length.unwrap()).collect()) + .collect(); + // Table 1.58 columns (A..G as 1..7). + assert_eq!( + lens, + vec![ + vec![1, 2, 3, 4, 5], // all present + vec![2, 3, 4, 5], // A absent + vec![1, 2, 4, 5], // C absent + vec![2, 4, 5], // A, C absent + vec![1, 2, 3, 4], // E absent + vec![2, 3, 4], // A, E absent + vec![1, 2, 4], // C, E absent + vec![2, 4], // A, C, E absent + vec![6, 7], // pred #1, F present + vec![7], // pred #1, F absent + ] + ); + } + + #[test] + fn reserved_fields_rejected() { + // interleave_type == 3. + let mut w = BitWriter::new(); + w.write_u32(1, 8); // number_of_predefined_set + w.write_u32(3, 2); // interleave_type (reserved) + w.write_u32(0, 3); + w.write_u32(1, 3); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert_eq!( + ErrorProtectionSpecificConfig::parse(&mut r).unwrap_err(), + Error::EpConfigInvalid + ); + + // number_of_concatenated_frame == 0 (Table 1.54 reserved). + let mut w = BitWriter::new(); + w.write_u32(1, 8); + w.write_u32(0, 2); + w.write_u32(0, 3); + w.write_u32(0, 3); // reserved + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert_eq!( + ErrorProtectionSpecificConfig::parse(&mut r).unwrap_err(), + Error::EpConfigInvalid + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/ep_fec.rs b/crates/vendor/oxideav-aac/src/ep_fec.rs new file mode 100644 index 00000000..feec9a7c --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ep_fec.rs @@ -0,0 +1,652 @@ +//! §1.8.4.6 SRCPC convolutional FEC and the §1.8.4.3 in-band-header +//! block codes of the MPEG-4 error-protection tool. +//! +//! ## SRCPC (§1.8.4.6) +//! +//! A systematic recursive convolutional code of rate 1/4 (Figure +//! 1.10) punctured to 8/8..8/32 (Table 1.61). Per input bit `u` with +//! state `(m1, m2, m3, m4)` and feedback `d = m4 ⊕ m2 ⊕ m1`: +//! +//! ```text +//! v1 = u +//! v2 = m3 ⊕ m2 ⊕ m1 ⊕ u +//! v3 = m3 ⊕ m1 ⊕ u +//! v4 = m3 ⊕ m2 ⊕ u +//! next state: (u ⊕ d, m1, m2, m3) +//! ``` +//! +//! Puncturing runs with period 8: bit `7 − (t mod 8)` of `Pr(i)` +//! decides whether `v(i+1)` at time `t` is emitted; surviving bits go +//! out in `v1..v4` order per time step. `Pr(0) == 0xFF` for every +//! rate, so the code stays systematic. Termination (§1.8.4.6.2) +//! appends four tail input bits `u = d` driving the state to zero +//! (the Table 1.60 tail-bit listing is the closed form of exactly +//! that rule — pinned by a test). +//! +//! Decoding is hard-decision Viterbi over the 16-state trellis +//! (§1.8.4.6.4); an error-free stream round-trips exactly, and up to +//! the code's correction capability transmission errors are repaired. +//! +//! ## In-band header FEC (§1.8.4.3, Table 1.59) +//! +//! The `choice_of_pred` / `class_attrib()` header parts are protected +//! by a length-selected block code: 3× repetition (1–2 bits), +//! BCH(7,4) (3–4), BCH(15,7) (5–7), Golay(23,12) (8–12), BCH(31,16) +//! (13–16), or — for 17+ bits — CRC4 + terminated SRCPC 8/16. The +//! parity of the polynomial codes is `R(x)` of +//! `M(x)·x^deg(G) = Q(x)G(x) + R(x)` with the §1.8.4.3 generators; +//! decode-side correction is bounded-distance (exhaustive syndrome +//! search up to the code's design correction capability). + +use crate::crc::{crc_bits, CrcPoly}; +use crate::{Error, Result}; + +/// Number of tail input bits appended by §1.8.4.6.2 termination. +pub const SRCPC_TAIL_BITS: usize = 4; + +/// The nine-step per-output-line puncture progression of Table 1.61 +/// (`00, 80, 88, A8, AA, EA, EE, FE, FF`): entry `j` keeps `j` of the +/// eight period positions. +const PUNCTURE_STEPS: [u8; 9] = [0x00, 0x80, 0x88, 0xA8, 0xAA, 0xEA, 0xEE, 0xFE, 0xFF]; + +/// The Table 1.61 puncture pattern `[Pr(0), Pr(1), Pr(2), Pr(3)]` for +/// `class_rate` 0..=24 (rate 8/8 .. 8/32). +pub fn puncture_pattern(class_rate: u8) -> Result<[u8; 4]> { + if class_rate > 24 { + return Err(Error::EpConfigInvalid); + } + let extra = usize::from(class_rate); + Ok([ + 0xFF, + PUNCTURE_STEPS[extra.min(8)], + PUNCTURE_STEPS[extra.saturating_sub(8).min(8)], + PUNCTURE_STEPS[extra.saturating_sub(16).min(8)], + ]) +} + +/// Number of coded bits the SRCPC emits for `n_info` information bits +/// at `class_rate` (0..=24), with or without the four termination +/// tail steps. +pub fn srcpc_coded_len(n_info: usize, class_rate: u8, terminated: bool) -> Result { + let p = puncture_pattern(class_rate)?; + let steps = n_info + if terminated { SRCPC_TAIL_BITS } else { 0 }; + let per_period: usize = p.iter().map(|&b| b.count_ones() as usize).sum(); + let full = steps / 8; + let mut len = full * per_period; + for t in (full * 8)..steps { + for &line in &p { + if line & (0x80 >> (t % 8)) != 0 { + len += 1; + } + } + } + Ok(len) +} + +/// The §1.8.4.6.1 encoder state `(m1, m2, m3, m4)` packed as bits +/// 0..=3 of a nibble. +#[inline] +fn step(state: u8, u: bool) -> (u8, [bool; 4]) { + let m1 = state & 1 != 0; + let m2 = state & 2 != 0; + let m3 = state & 4 != 0; + let m4 = state & 8 != 0; + let d = m4 ^ m2 ^ m1; + let v = [u, m3 ^ m2 ^ m1 ^ u, m3 ^ m1 ^ u, m3 ^ m2 ^ u]; + let next = (u8::from(u ^ d)) | (state << 1) & 0b1110; + (next, v) +} + +/// Feedback bit `d` for a state (drives the §1.8.4.6.2 tail inputs). +#[inline] +fn feedback(state: u8) -> bool { + let m1 = state & 1 != 0; + let m2 = state & 2 != 0; + let m4 = state & 8 != 0; + m4 ^ m2 ^ m1 +} + +/// SRCPC-encode `info` at `class_rate` (0..=24 ⇒ 8/8..8/32), +/// optionally terminated. The encoder always starts from the all-zero +/// state (§1.8.4.6.1). +pub fn srcpc_encode(info: &[bool], class_rate: u8, terminated: bool) -> Result> { + let p = puncture_pattern(class_rate)?; + let mut out = Vec::with_capacity(srcpc_coded_len(info.len(), class_rate, terminated)?); + let mut state = 0u8; + let mut t = 0usize; + let emit = |state: &mut u8, u: bool, t: usize, out: &mut Vec| { + let (next, v) = step(*state, u); + *state = next; + for (i, &line) in p.iter().enumerate() { + if line & (0x80 >> (t % 8)) != 0 { + out.push(v[i]); + } + } + }; + for &u in info { + emit(&mut state, u, t, &mut out); + t += 1; + } + if terminated { + for _ in 0..SRCPC_TAIL_BITS { + let u = feedback(state); + emit(&mut state, u, t, &mut out); + t += 1; + } + debug_assert_eq!(state, 0, "termination must return to state 0"); + } + Ok(out) +} + +/// Hard-decision Viterbi decode of an SRCPC stream (§1.8.4.6.4): +/// recovers `n_info` information bits from `coded`, correcting +/// transmission errors up to the punctured code's capability. +/// +/// `coded.len()` must equal +/// [`srcpc_coded_len`]`(n_info, class_rate, terminated)`. +pub fn srcpc_decode( + coded: &[bool], + n_info: usize, + class_rate: u8, + terminated: bool, +) -> Result> { + let p = puncture_pattern(class_rate)?; + let steps = n_info + if terminated { SRCPC_TAIL_BITS } else { 0 }; + if coded.len() != srcpc_coded_len(n_info, class_rate, terminated)? { + return Err(Error::EpFrameInvalid); + } + + const INF: u32 = u32::MAX / 2; + let mut metric = [INF; 16]; + metric[0] = 0; + // survivors[t][s] = (previous state, input bit) — tail steps have + // a forced input, still recorded uniformly. + let mut survivors: Vec<[(u8, bool); 16]> = Vec::with_capacity(steps); + + let mut pos = 0usize; + for t in 0..steps { + // The emitted lines at this step. + let mut lines: [bool; 4] = [false; 4]; + let mut n_lines = 0usize; + for (i, &line) in p.iter().enumerate() { + lines[i] = line & (0x80 >> (t % 8)) != 0; + if lines[i] { + n_lines += 1; + } + } + let received = &coded[pos..pos + n_lines]; + pos += n_lines; + + let mut next_metric = [INF; 16]; + let mut surv = [(0u8, false); 16]; + for s in 0u8..16 { + if metric[usize::from(s)] >= INF { + continue; + } + let inputs: &[bool] = if t >= n_info { + // Termination steps: the input is forced to d(state). + if feedback(s) { + &[true] + } else { + &[false] + } + } else { + &[false, true] + }; + for &u in inputs { + let (next, v) = step(s, u); + let mut m = metric[usize::from(s)]; + let mut ri = 0usize; + for (i, &on) in lines.iter().enumerate() { + if on { + if v[i] != received[ri] { + m += 1; + } + ri += 1; + } + } + let slot = usize::from(next); + if m < next_metric[slot] { + next_metric[slot] = m; + surv[slot] = (s, u); + } + } + } + metric = next_metric; + survivors.push(surv); + } + + // Terminated streams end in state 0; otherwise take the best. + let mut state: u8 = if terminated { + if metric[0] >= INF { + return Err(Error::EpFrameInvalid); + } + 0 + } else { + let (best, m) = metric + .iter() + .enumerate() + .min_by_key(|(_, &m)| m) + .map(|(s, &m)| (s as u8, m)) + .unwrap_or((0, INF)); + if m >= INF { + return Err(Error::EpFrameInvalid); + } + best + }; + + let mut bits = vec![false; steps]; + for t in (0..steps).rev() { + let (prev, u) = survivors[t][usize::from(state)]; + bits[t] = u; + state = prev; + } + bits.truncate(n_info); + Ok(bits) +} + +/// One §1.8.4.3 basic block code, selected by the protected length. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HeaderFec { + /// 1–2 bits: majority (each bit repeated 3 times). + Majority, + /// 3–4 bits: BCH(7,4), g = x³ + x + 1. + Bch7, + /// 5–7 bits: BCH(15,7), g = x⁸ + x⁷ + x⁶ + x⁴ + 1. + Bch15, + /// 8–12 bits: Golay(23,12), g = x¹¹ + x⁹ + x⁷ + x⁶ + x⁵ + x + 1. + Golay23, + /// 13–16 bits: BCH(31,16), + /// g = x¹⁵ + x¹¹ + x¹⁰ + x⁹ + x⁸ + x⁷ + x⁵ + x³ + x² + x + 1. + Bch31, + /// 17+ bits: CRC4 + terminated SRCPC 8/16. + Srcpc16, +} + +impl HeaderFec { + /// The Table 1.59 length-driven selection. + pub fn for_len(l: usize) -> Result { + Ok(match l { + 0 => return Err(Error::EpFrameInvalid), + 1..=2 => HeaderFec::Majority, + 3..=4 => HeaderFec::Bch7, + 5..=7 => HeaderFec::Bch15, + 8..=12 => HeaderFec::Golay23, + 13..=16 => HeaderFec::Bch31, + _ => HeaderFec::Srcpc16, + }) + } + + /// `(generator polynomial bits above x⁰ .. as u32 with implicit + /// leading term INCLUDED, parity bit count, correction capability)` + /// for the polynomial codes. + fn poly(self) -> Option<(u32, usize, usize)> { + match self { + // x³+x+1 → 0b1011, 3 parity bits, t = 1. + HeaderFec::Bch7 => Some((0b1011, 3, 1)), + // x⁸+x⁷+x⁶+x⁴+1 → 1_1101_0001, 8 parity bits, t = 2. + HeaderFec::Bch15 => Some((0b1_1101_0001, 8, 2)), + // x¹¹+x⁹+x⁷+x⁶+x⁵+x+1 → 1010_1110_0011, 11 parity, t = 3. + HeaderFec::Golay23 => Some((0b1010_1110_0011, 11, 3)), + // x¹⁵+x¹¹+x¹⁰+x⁹+x⁸+x⁷+x⁵+x³+x²+x+1, 15 parity, t = 3. + HeaderFec::Bch31 => Some((0b1000_1111_1010_1111, 15, 3)), + _ => None, + } + } + + /// Number of parity bits appended for `l` protected bits. + pub fn parity_bits(self, l: usize) -> Result { + Ok(match self { + HeaderFec::Majority => 2 * l, + HeaderFec::Srcpc16 => { + // CRC4 + terminated SRCPC 8/16 over (l + 4) info bits; + // parity = coded − l. + srcpc_coded_len(l + 4, 8, true)? - l + } + other => other.poly().map(|(_, p, _)| p).unwrap_or(0), + }) + } +} + +/// Polynomial-division parity `R(x)` of `M(x)·x^deg(G) mod G(x)`, +/// MSB-first over `info` (§1.8.4.3). +fn poly_parity(info: &[bool], gen: u32, parity: usize) -> Vec { + let top = 1u32 << parity; // the implicit leading term position + let mut reg: u32 = 0; + for &bit in info { + reg = (reg << 1) | u32::from(bit); + if reg & top != 0 { + reg ^= gen; + } + } + for _ in 0..parity { + reg <<= 1; + if reg & top != 0 { + reg ^= gen; + } + } + (0..parity) + .map(|i| reg & (1 << (parity - 1 - i)) != 0) + .collect() +} + +/// Encode a §1.8.4.3 header part: returns the parity bit sequence to +/// transmit after the `l` information bits (`Npred_parity` / +/// `Nattrib_parity`). +pub fn header_fec_encode(info: &[bool]) -> Result> { + let fec = HeaderFec::for_len(info.len())?; + Ok(match fec { + HeaderFec::Majority => { + let mut v = Vec::with_capacity(info.len() * 2); + v.extend_from_slice(info); + v.extend_from_slice(info); + v + } + HeaderFec::Srcpc16 => { + // CRC4 over the info, then terminated SRCPC 8/16 over + // info + CRC; the parity is everything past the + // systematic prefix of the coded stream... the coded + // stream is emitted interleaved per time step, so the + // whole codeword replaces info + parity: return the full + // codeword minus the leading l systematic copies is not + // separable. Instead the parity field carries the coded + // stream's non-systematic remainder: we transmit the + // complete coded stream in place of info+parity, so the + // parity here is the coded stream with the systematic + // prefix removed positionally. See `header_fec_decode`, + // which reassembles the same layout. + let crc = crc_bits(CrcPoly::Crc4, info); + let mut m: Vec = info.to_vec(); + for i in (0..4).rev() { + m.push(crc & (1 << i) != 0); + } + let coded = srcpc_encode(&m, 8, true)?; + // Systematic v1 bits occupy known positions; the parity + // field is the stream with those positions removed — the + // decoder re-merges them. + let mut parity = Vec::with_capacity(coded.len() - info.len()); + for (idx, chunk) in coded.chunks(2).enumerate() { + // rate 8/16 keeps v1 and v2 every step. + if idx < info.len() { + // chunk[0] is systematic (v1) — drop, it equals + // info[idx]. + parity.push(chunk[1]); + } else { + parity.push(chunk[0]); + parity.push(chunk[1]); + } + } + parity + } + other => { + let (gen, p, _) = other.poly().ok_or(Error::EpFrameInvalid)?; + poly_parity(info, gen, p) + } + }) +} + +/// Decode a §1.8.4.3 header part: `info` are the received (possibly +/// corrupted) information bits, `parity` the received parity bits. +/// Returns the corrected information bits; uncorrectable words +/// surface [`Error::EpFrameInvalid`]. +pub fn header_fec_decode(info: &[bool], parity: &[bool]) -> Result> { + let l = info.len(); + let fec = HeaderFec::for_len(l)?; + if parity.len() != fec.parity_bits(l)? { + return Err(Error::EpFrameInvalid); + } + match fec { + HeaderFec::Majority => { + let mut out = Vec::with_capacity(l); + for i in 0..l { + let votes = u8::from(info[i]) + u8::from(parity[i]) + u8::from(parity[l + i]); + out.push(votes >= 2); + } + Ok(out) + } + HeaderFec::Srcpc16 => { + // Re-merge the coded stream: v1 comes from `info` for the + // first l steps, both bits from `parity` afterwards. + let mut coded = Vec::with_capacity(l + parity.len()); + let mut pi = 0usize; + for &i_bit in info.iter().take(l) { + coded.push(i_bit); + coded.push(parity[pi]); + pi += 1; + } + coded.extend_from_slice(&parity[pi..]); + let decoded = srcpc_decode(&coded, l + 4, 8, true)?; + let (msg, crc_bits_rx) = decoded.split_at(l); + let want = crc_bits(CrcPoly::Crc4, msg); + let mut got = 0u64; + for &b in crc_bits_rx { + got = (got << 1) | u64::from(b); + } + if got != want { + return Err(Error::EpFrameInvalid); + } + Ok(msg.to_vec()) + } + other => { + let (gen, p, t) = other.poly().ok_or(Error::EpFrameInvalid)?; + let mut word: Vec = Vec::with_capacity(l + p); + word.extend_from_slice(info); + word.extend_from_slice(parity); + if poly_syndrome_ok(&word, gen, p) { + return Ok(info.to_vec()); + } + // Bounded-distance decoding: search error patterns of + // weight <= t over the (shortened) codeword. + let n = word.len(); + let mut positions: Vec = Vec::with_capacity(t); + if search_errors(&mut word, gen, p, t, 0, n, &mut positions) { + return Ok(word[..l].to_vec()); + } + Err(Error::EpFrameInvalid) + } + } +} + +/// `true` iff the codeword (info ‖ parity) has an all-zero syndrome +/// under `gen`. +fn poly_syndrome_ok(word: &[bool], gen: u32, parity: usize) -> bool { + let top = 1u32 << parity; + let mut reg: u32 = 0; + for &bit in word { + reg = (reg << 1) | u32::from(bit); + if reg & top != 0 { + reg ^= gen; + } + } + reg == 0 +} + +/// Recursive bounded-distance search: flip up to `budget` bits from +/// index `from` and test the syndrome. On success the corrected word +/// is left in `word` and `true` is returned. +fn search_errors( + word: &mut [bool], + gen: u32, + parity: usize, + budget: usize, + from: usize, + n: usize, + positions: &mut Vec, +) -> bool { + if budget == 0 { + return false; + } + for i in from..n { + word[i] = !word[i]; + positions.push(i); + if poly_syndrome_ok(word, gen, parity) + || search_errors(word, gen, parity, budget - 1, i + 1, n, positions) + { + return true; + } + positions.pop(); + word[i] = !word[i]; + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + fn prand_bits(n: usize, mut seed: u32) -> Vec { + let mut v = Vec::with_capacity(n); + for _ in 0..n { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + v.push(seed & 0x8000_0000 != 0); + } + v + } + + /// The Table 1.60 tail-bit listing is the closed form of the + /// `u = d` termination rule. + #[test] + fn termination_matches_table_1_60() { + // Table 1.60 rows: state (m4 m3 m2 m1) -> tail (un-3..un). + let table: [(u8, [u8; 4]); 16] = [ + (0b0000, [0, 0, 0, 0]), + (0b0001, [1, 1, 0, 1]), + (0b0010, [1, 0, 1, 0]), + (0b0011, [0, 1, 1, 1]), + (0b0100, [0, 1, 0, 0]), + (0b0101, [1, 0, 0, 1]), + (0b0110, [1, 1, 1, 0]), + (0b0111, [0, 0, 1, 1]), + (0b1000, [1, 0, 0, 0]), + (0b1001, [0, 1, 0, 1]), + (0b1010, [0, 0, 1, 0]), + (0b1011, [1, 1, 1, 1]), + (0b1100, [1, 1, 0, 0]), + (0b1101, [0, 0, 0, 1]), + (0b1110, [0, 1, 1, 0]), + (0b1111, [1, 0, 1, 1]), + ]; + for (packed, tail) in table { + // Repack (m4 m3 m2 m1) into the module's bit-0 = m1 layout. + let mut state = 0u8; + if packed & 0b0001 != 0 { + state |= 1; // m1 + } + if packed & 0b0010 != 0 { + state |= 2; // m2 + } + if packed & 0b0100 != 0 { + state |= 4; // m3 + } + if packed & 0b1000 != 0 { + state |= 8; // m4 + } + let mut s = state; + for (step_i, &want) in tail.iter().enumerate() { + let u = feedback(s); + assert_eq!(u8::from(u), want, "state {packed:04b} tail step {step_i}"); + let (next, _) = step(s, u); + s = next; + } + assert_eq!(s, 0, "state {packed:04b} did not terminate"); + } + } + + #[test] + fn puncture_patterns_match_table_1_61() { + // Spot rows straight from Table 1.61. + assert_eq!(puncture_pattern(0).unwrap(), [0xFF, 0x00, 0x00, 0x00]); // 8/8 + assert_eq!(puncture_pattern(3).unwrap(), [0xFF, 0xA8, 0x00, 0x00]); // 8/11 + assert_eq!(puncture_pattern(5).unwrap(), [0xFF, 0xEA, 0x00, 0x00]); // 8/13 + assert_eq!(puncture_pattern(8).unwrap(), [0xFF, 0xFF, 0x00, 0x00]); // 8/16 + assert_eq!(puncture_pattern(9).unwrap(), [0xFF, 0xFF, 0x80, 0x00]); // 8/17 + assert_eq!(puncture_pattern(16).unwrap(), [0xFF, 0xFF, 0xFF, 0x00]); // 8/24 + assert_eq!(puncture_pattern(17).unwrap(), [0xFF, 0xFF, 0xFF, 0x80]); // 8/25 + assert_eq!(puncture_pattern(24).unwrap(), [0xFF, 0xFF, 0xFF, 0xFF]); // 8/32 + } + + #[test] + fn srcpc_roundtrip_all_rates() { + for rate in [0u8, 1, 3, 8, 12, 17, 24] { + for terminated in [false, true] { + let info = prand_bits(97, 0xC0FFEE ^ u32::from(rate)); + let coded = srcpc_encode(&info, rate, terminated).unwrap(); + assert_eq!( + coded.len(), + srcpc_coded_len(info.len(), rate, terminated).unwrap() + ); + // Rate 8/8 is purely systematic. + if rate == 0 { + let systematic: Vec = coded + .iter() + .copied() + .take(if terminated { + info.len() + 4 + } else { + info.len() + }) + .collect(); + assert_eq!(&systematic[..info.len()], &info[..]); + } + let decoded = srcpc_decode(&coded, info.len(), rate, terminated).unwrap(); + assert_eq!(decoded, info, "rate {rate} terminated {terminated}"); + } + } + } + + #[test] + fn srcpc_corrects_errors() { + // Rate 8/16 (one parity bit per info bit), terminated: a few + // well-spread bit errors are corrected by the Viterbi pass. + let info = prand_bits(120, 0xDEAD); + let mut coded = srcpc_encode(&info, 8, true).unwrap(); + for &pos in &[10usize, 77, 150, 220] { + coded[pos] = !coded[pos]; + } + let decoded = srcpc_decode(&coded, info.len(), 8, true).unwrap(); + assert_eq!(decoded, info); + } + + #[test] + fn header_fec_roundtrip_all_classes() { + for l in [1usize, 2, 3, 4, 5, 7, 8, 12, 13, 16, 17, 30] { + let info = prand_bits(l, 0xBEEF ^ l as u32); + let parity = header_fec_encode(&info).unwrap(); + assert_eq!( + parity.len(), + HeaderFec::for_len(l).unwrap().parity_bits(l).unwrap(), + "len {l}" + ); + let decoded = header_fec_decode(&info, &parity).unwrap(); + assert_eq!(decoded, info, "len {l}"); + } + } + + #[test] + fn header_fec_corrects_errors() { + // Golay(23,12): three errors are correctable. + let info = prand_bits(12, 0x1234); + let parity = header_fec_encode(&info).unwrap(); + let mut rx_info = info.clone(); + let mut rx_parity = parity.clone(); + rx_info[3] = !rx_info[3]; + rx_info[9] = !rx_info[9]; + rx_parity[5] = !rx_parity[5]; + assert_eq!(header_fec_decode(&rx_info, &rx_parity).unwrap(), info); + + // Majority: one flip per repeated position corrects. + let info = prand_bits(2, 0x9); + let parity = header_fec_encode(&info).unwrap(); + let mut rx_info = info.clone(); + rx_info[0] = !rx_info[0]; + assert_eq!(header_fec_decode(&rx_info, &parity).unwrap(), info); + + // BCH(15,7): two errors. + let info = prand_bits(6, 0x77); + let parity = header_fec_encode(&info).unwrap(); + let mut rx_parity = parity.clone(); + rx_parity[0] = !rx_parity[0]; + rx_parity[6] = !rx_parity[6]; + assert_eq!(header_fec_decode(&info, &rx_parity).unwrap(), info); + } +} diff --git a/crates/vendor/oxideav-aac/src/ep_frame.rs b/crates/vendor/oxideav-aac/src/ep_frame.rs new file mode 100644 index 00000000..dec38662 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ep_frame.rs @@ -0,0 +1,1390 @@ +//! `ep_frame()` — ISO/IEC 14496-3 §1.8.2.2 (Tables 1.50–1.53) and the +//! §1.8.4 decoding machinery of the error-protection tool: the +//! FEC-protected in-band header (`choice_of_pred` + `class_attrib()`, +//! §1.8.4.3), per-class CRC (§1.8.4.5) + SRCPC (§1.8.4.6) / shortened +//! Reed-Solomon (§1.8.4.7) protection, the §1.8.4.8 recursive +//! interleaver (modes 0 / 1 / 2) and the §1.8.4.9 class-reordered +//! output. +//! +//! [`EpFrameCodec`] is built from a parsed +//! [`ErrorProtectionSpecificConfig`]; [`EpFrameCodec::encode`] turns a +//! class-partitioned access unit into one error-protected `ep_frame()` +//! and [`EpFrameCodec::decode`] inverts it, verifying every CRC and +//! correcting transmission errors through the FEC layers. The +//! concatenation of the decoded classes is the `epConfig == 0` payload +//! (§1.8.1); §1.8.4.9 output reordering is applied on the decode side. +//! +//! Implementation notes on the two spec points the staged text leaves +//! loose (kept conservative; both surface [`Error::EpFrameInvalid`] +//! rather than guessing): +//! +//! * an escaped (`rate_escape == 1`) rate on an RS class has no +//! in-band code table (Table 1.55 is the SRCPC puncture table), so +//! it is rejected; +//! * the byte-wise recursive interleaving of an RS class (§1.8.4.8.2) +//! is supported when the accumulated `Y` stream is a whole number of +//! octets (the matrix then works in byte cells exactly as Figure +//! 1.18 draws it); a non-aligned `Y` is rejected. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::crc::crc_bits; +use crate::ep_config::{EpClass, EpPredefinedSet, ErrorProtectionSpecificConfig}; +use crate::ep_fec::{ + header_fec_decode, header_fec_encode, srcpc_coded_len, srcpc_decode, srcpc_encode, +}; +use crate::ep_rs::{srs_decode, srs_encode}; +use crate::{Error, Result}; + +/// Table 1.55 — the 3-bit in-band `class_code_rate` codes mapped onto +/// the out-of-band `class_rate` scale (0..=24). +pub const INBAND_RATE_TO_CLASS_RATE: [u8; 8] = [0, 3, 4, 6, 8, 12, 16, 24]; + +/// Table 1.56 — the 3-bit in-band `class_crc_count` codes mapped onto +/// CRC bit counts. +pub const INBAND_CRC_BITS: [u32; 8] = [0, 6, 8, 10, 12, 14, 16, 32]; + +/// One frame's worth of class content plus the per-frame escaped +/// parameters. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EpFrameData { + /// Index into the §1.8.4.2 **expanded** pre-defined-set list. + pub choice_of_pred: usize, + /// Per-class information bits, in class-index order (their + /// concatenation is the `epConfig == 0` payload). + pub classes: Vec>, + /// In-band `class_code_rate` values (Table 1.55 codes) for + /// classes with `rate_escape == 1`; `None` on fixed-rate classes. + pub rate_codes: Vec>, + /// In-band `class_crc_count` values (Table 1.56 codes) for + /// classes with `crclen_escape == 1`; `None` on fixed-CRC classes. + pub crc_codes: Vec>, +} + +/// Resolved per-class parameters for one frame. +#[derive(Debug, Clone)] +struct ClassRt { + /// Information length in bits (`None` = "until the end"). + len_bits: Option, + /// Field width of the in-band `class_bit_count` (escaped classes). + len_field_bits: Option, + /// SRCPC `class_rate` (0..=24) or RS correctable-byte count. + rate: u8, + rate_escaped: bool, + /// CRC width in bits. + crc_bits: u32, + crc_escaped: bool, + fec_type: u8, + terminated: bool, + interleave_switch: u8, +} + +/// Codec for one EP-tool configuration. +#[derive(Debug, Clone)] +pub struct EpFrameCodec { + cfg: ErrorProtectionSpecificConfig, + sets: Vec, +} + +impl EpFrameCodec { + /// Build a codec from a parsed configuration (running the + /// §1.8.4.2 expansion once). + pub fn new(cfg: ErrorProtectionSpecificConfig) -> Result { + let sets = cfg.expand()?; + Ok(EpFrameCodec { cfg, sets }) + } + + /// The §1.8.4.2 expanded pre-defined sets (the `choice_of_pred` + /// index space). + pub fn sets(&self) -> &[EpPredefinedSet] { + &self.sets + } + + /// `Npred = ceil(log2(number of expanded sets))` (Table 1.51). + pub fn npred(&self) -> u32 { + let n = self.sets.len(); + if n <= 1 { + 0 + } else { + usize::BITS - (n - 1).leading_zeros() + } + } + + fn resolve_class(&self, c: &EpClass) -> Result { + let (len_bits, len_field_bits) = if c.length_escape { + let w = u32::from(c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?); + if w == 0 { + (None, None) // "until the end" + } else { + (None, Some(w)) + } + } else { + ( + Some(usize::from(c.class_length.ok_or(Error::EpConfigInvalid)?)), + None, + ) + }; + let rate = c.class_rate.unwrap_or(0); + let crc = match c.class_crclen { + Some(code) => EpClass::crclen_bits(code)?, + None => 0, + }; + Ok(ClassRt { + len_bits, + len_field_bits, + rate, + rate_escaped: c.rate_escape, + crc_bits: crc, + crc_escaped: c.crclen_escape, + fec_type: c.fec_type, + terminated: c.termination_switch.unwrap_or(false), + interleave_switch: c.interleave_switch.unwrap_or(0), + }) + } + + /// Coded bit length of one class's `ep_encoded_class` given its + /// resolved parameters and info length. RS chains are handled by + /// the caller (the chained parity rides the last member). + fn coded_len(&self, rt: &ClassRt, info_bits: usize) -> Result { + let with_crc = info_bits + rt.crc_bits as usize; + Ok(match rt.fec_type { + 0 => srcpc_coded_len(with_crc, rt.rate, rt.terminated)?, + 1 | 2 => { + if with_crc % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + let bytes = with_crc / 8; + let two_k = 2 * usize::from(rt.rate); + if two_k == 0 { + with_crc + } else { + if two_k >= 255 { + return Err(Error::EpConfigInvalid); + } + let parts = bytes.div_ceil(255 - two_k); + with_crc + 8 * two_k * parts + } + } + _ => return Err(Error::EpConfigInvalid), + }) + } + + /// Protect one class (CRC + FEC). RS chaining is resolved before + /// this call (the info of a chain arrives concatenated). + fn protect_class(&self, rt: &ClassRt, info: &[bool]) -> Result> { + // §1.8.4.5 CRC first (the crc module applies the normative + // output inversion). + let mut with_crc: Vec = info.to_vec(); + if rt.crc_bits > 0 { + let poly = EpClass::crc_poly(rt.crc_bits)?.ok_or(Error::EpFrameInvalid)?; + let crc = crc_bits(poly, info); + for i in (0..rt.crc_bits).rev() { + with_crc.push(crc & (1u64 << i) != 0); + } + } + match rt.fec_type { + 0 => srcpc_encode(&with_crc, rt.rate, rt.terminated), + 1 | 2 => { + if with_crc.len() % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + let bytes = bits_to_bytes(&with_crc); + let parity = srs_encode(&bytes, usize::from(rt.rate))?; + let mut out = with_crc; + out.extend(bytes_to_bits(&parity)); + Ok(out) + } + _ => Err(Error::EpConfigInvalid), + } + } + + /// Undo [`Self::protect_class`]: FEC-decode (with correction) and + /// verify + strip the CRC. + fn unprotect_class(&self, rt: &ClassRt, coded: &[bool], info_bits: usize) -> Result> { + let with_crc_len = info_bits + rt.crc_bits as usize; + let mut with_crc: Vec = match rt.fec_type { + 0 => srcpc_decode(coded, with_crc_len, rt.rate, rt.terminated)?, + 1 | 2 => { + if with_crc_len % 8 != 0 || coded.len() < with_crc_len { + return Err(Error::EpFrameInvalid); + } + let mut data = bits_to_bytes(&coded[..with_crc_len]); + let parity = bits_to_bytes(&coded[with_crc_len..]); + srs_decode(&mut data, &parity, usize::from(rt.rate))?; + bytes_to_bits(&data) + } + _ => return Err(Error::EpConfigInvalid), + }; + if rt.crc_bits > 0 { + let poly = EpClass::crc_poly(rt.crc_bits)?.ok_or(Error::EpFrameInvalid)?; + let rx_crc = with_crc.split_off(info_bits); + let want = crc_bits(poly, &with_crc); + let mut got = 0u64; + for &b in &rx_crc { + got = (got << 1) | u64::from(b); + } + if got != want { + return Err(Error::EpFrameInvalid); + } + } else { + with_crc.truncate(info_bits); + } + Ok(with_crc) + } + + /// Encode one frame to a byte-aligned `ep_frame()`. + pub fn encode(&self, frame: &EpFrameData) -> Result> { + let set = self + .sets + .get(frame.choice_of_pred) + .ok_or(Error::EpFrameInvalid)?; + let n = set.classes.len(); + if frame.classes.len() != n || frame.rate_codes.len() != n || frame.crc_codes.len() != n { + return Err(Error::EpFrameInvalid); + } + // Resolve runtime parameters (folding the in-band escapes in). + let mut rts = Vec::with_capacity(n); + for j in 0..n { + let mut rt = self.resolve_class(&set.classes[j])?; + if rt.rate_escaped { + if rt.fec_type != 0 { + return Err(Error::EpFrameInvalid); + } + let code = frame.rate_codes[j].ok_or(Error::EpFrameInvalid)?; + rt.rate = *INBAND_RATE_TO_CLASS_RATE + .get(usize::from(code)) + .ok_or(Error::EpFrameInvalid)?; + } else if frame.rate_codes[j].is_some() { + return Err(Error::EpFrameInvalid); + } + if rt.crc_escaped { + let code = frame.crc_codes[j].ok_or(Error::EpFrameInvalid)?; + rt.crc_bits = *INBAND_CRC_BITS + .get(usize::from(code)) + .ok_or(Error::EpFrameInvalid)?; + } else if frame.crc_codes[j].is_some() { + return Err(Error::EpFrameInvalid); + } + // Fixed-length classes must match the provided content. + if let Some(l) = rt.len_bits { + if frame.classes[j].len() != l { + return Err(Error::EpFrameInvalid); + } + } else if let Some(w) = rt.len_field_bits { + if frame.classes[j].len() >= (1usize << w) { + return Err(Error::EpFrameInvalid); + } + } + rts.push(rt); + } + + // ---- Protect the classes (§1.8.4.4 RS chains resolved by + // concatenating fec_type == 2 members with their successor). + let mut coded: Vec> = vec![Vec::new(); n]; + let mut j = 0usize; + while j < n { + if rts[j].fec_type == 2 { + // Chain: classes j..=last share one RS code. + let mut last = j; + while last < n && rts[last].fec_type == 2 { + last += 1; + } + if last >= n { + return Err(Error::EpFrameInvalid); + } + // §1.8.3.1: all chain members share class_rate. + #[allow(clippy::needless_range_loop)] + for m in j..=last { + if rts[m].rate != rts[last].rate || rts[m].crc_bits % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + } + // Per-class CRCs, then one RS over the concatenation. + let mut chain: Vec = Vec::new(); + let mut member_coded: Vec> = Vec::new(); + #[allow(clippy::needless_range_loop)] + for m in j..=last { + let mut with_crc = frame.classes[m].clone(); + if rts[m].crc_bits > 0 { + let poly = + EpClass::crc_poly(rts[m].crc_bits)?.ok_or(Error::EpFrameInvalid)?; + let crc = crc_bits(poly, &frame.classes[m]); + for i in (0..rts[m].crc_bits).rev() { + with_crc.push(crc & (1u64 << i) != 0); + } + } + if with_crc.len() % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + chain.extend_from_slice(&with_crc); + member_coded.push(with_crc); + } + let parity = srs_encode(&bits_to_bytes(&chain), usize::from(rts[last].rate))?; + // Every member transmits its own CRC-protected bits; + // the parity rides the chain's last member. + for (idx, m) in (j..=last).enumerate() { + coded[m] = member_coded[idx].clone(); + } + coded[last].extend(bytes_to_bits(&parity)); + j = last + 1; + } else { + coded[j] = self.protect_class(&rts[j], &frame.classes[j])?; + j += 1; + } + } + + // ---- In-band header bits. + let npred = self.npred(); + let mut pred_bits: Vec = Vec::new(); + for i in (0..npred).rev() { + pred_bits.push(frame.choice_of_pred & (1usize << i) != 0); + } + let mut attrib_bits: Vec = Vec::new(); + for jj in 0..n { + let k = if set.class_reordered_output { + usize::from(set.class_output_order[jj]) + } else { + jj + }; + if let Some(w) = rts[k].len_field_bits { + let v = frame.classes[k].len(); + for i in (0..w).rev() { + attrib_bits.push(v & (1usize << i) != 0); + } + } + if rts[k].rate_escaped { + let code = frame.rate_codes[k].ok_or(Error::EpFrameInvalid)?; + for i in (0..3).rev() { + attrib_bits.push(code & (1u8 << i) != 0); + } + } + if rts[k].crc_escaped { + let code = frame.crc_codes[k].ok_or(Error::EpFrameInvalid)?; + for i in (0..3).rev() { + attrib_bits.push(code & (1u8 << i) != 0); + } + } + } + + // The transmitted class order (Table 1.53). + let tx_order: Vec = (0..n) + .map(|jj| { + if set.class_reordered_output { + usize::from(set.class_output_order[jj]) + } else { + jj + } + }) + .collect(); + + match self.cfg.interleave_type { + 0 => self.assemble_mode0(&pred_bits, &attrib_bits, &tx_order, &coded, frame), + 1 | 2 => { + self.assemble_interleaved(&pred_bits, &attrib_bits, &tx_order, &rts, &coded, frame) + } + _ => Err(Error::EpConfigInvalid), + } + } + + /// interleave_type == 0: `ep_header()`, `ep_encoded_classes()`, + /// `stuffing_bits` (Table 1.50). + fn assemble_mode0( + &self, + pred_bits: &[bool], + attrib_bits: &[bool], + tx_order: &[usize], + coded: &[Vec], + frame: &EpFrameData, + ) -> Result> { + let mut bits: Vec = Vec::new(); + bits.extend_from_slice(pred_bits); + if !pred_bits.is_empty() { + bits.extend(self.header_parity(pred_bits)?); + } + // class_attrib() + num_stuffing_bits — the stuffing count + // depends on the total length, which the attrib field itself + // is part of; everything except the 3-bit count is fixed, so + // the count solves directly. + let mut fixed = bits.len() + attrib_bits.len(); + if self.cfg.bit_stuffing == 1 { + fixed += 3; + } + let attrib_parity_len = if attrib_bits.is_empty() && self.cfg.bit_stuffing != 1 { + 0 + } else { + // parity spans class_attrib() incl. num_stuffing_bits. + let l = attrib_bits.len() + if self.cfg.bit_stuffing == 1 { 3 } else { 0 }; + crate::ep_fec::HeaderFec::for_len(l)?.parity_bits(l)? + }; + fixed += attrib_parity_len; + let classes_len: usize = coded.iter().map(Vec::len).sum(); + let total_no_stuff = fixed + classes_len; + let nstuff = if self.cfg.bit_stuffing == 1 { + (8 - (total_no_stuff % 8)) % 8 + } else { + 0 + }; + let mut attrib_full: Vec = attrib_bits.to_vec(); + if self.cfg.bit_stuffing == 1 { + for i in (0..3).rev() { + attrib_full.push(nstuff & (1usize << i) != 0); + } + } + bits.extend_from_slice(&attrib_full); + if !attrib_full.is_empty() { + bits.extend(self.header_parity(&attrib_full)?); + } + for &k in tx_order { + bits.extend_from_slice(&coded[k]); + } + bits.resize(bits.len() + nstuff, false); + let _ = frame; + if self.cfg.bit_stuffing == 1 && bits.len() % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + Ok(bits_to_bytes_padded(&bits)) + } + + /// interleave_type == 1 / 2: the §1.8.4.8.2 multi-stage assembly. + fn assemble_interleaved( + &self, + pred_bits: &[bool], + attrib_bits: &[bool], + tx_order: &[usize], + rts: &[ClassRt], + coded: &[Vec], + frame: &EpFrameData, + ) -> Result> { + let mode2 = self.cfg.interleave_type == 2; + let n = tx_order.len(); + // Stuffing count: the total bit count is invariant under + // interleaving, so it solves exactly as in mode 0. + let mut fixed = pred_bits.len(); + if !pred_bits.is_empty() { + fixed += self.header_parity_len(pred_bits.len())?; + } + let attrib_l = attrib_bits.len() + if self.cfg.bit_stuffing == 1 { 3 } else { 0 }; + fixed += attrib_l; + if attrib_l > 0 { + fixed += self.header_parity_len(attrib_l)?; + } + let classes_len: usize = coded.iter().map(Vec::len).sum(); + let total_no_stuff = fixed + classes_len; + let nstuff = if self.cfg.bit_stuffing == 1 { + (8 - (total_no_stuff % 8)) % 8 + } else { + 0 + }; + + // ---- Class stage. + let mut buf_y: Vec = Vec::new(); + let mut buf_no: Vec = Vec::new(); + if mode2 { + // Forward pass: switch-3 (concatenate) and switch-0 + // (non-interleaved) classes. + for &k in tx_order.iter().take(n) { + match rts[k].interleave_switch { + 3 => buf_y.extend_from_slice(&coded[k]), + 0 => buf_no.extend_from_slice(&coded[k]), + _ => {} + } + } + } + for jj in (0..n).rev() { + let k = tx_order[jj]; + let sw = if mode2 { rts[k].interleave_switch } else { 1 }; + if mode2 && (sw == 0 || sw == 3) { + continue; + } + // Width selection (Tables 1.63 / 1.64). + let bytewise = rts[k].fec_type != 0; + let w_units = if mode2 && sw == 2 { + if bytewise { + return Err(Error::EpConfigInvalid); + } + 28 + } else if bytewise { + if coded[k].len() % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + coded[k].len() / 8 + } else if mode2 { + coded[k].len() + } else { + // Mode 1 SRCPC: 28 bits. + 28 + }; + buf_y = if bytewise { + if buf_y.len() % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + let x = bits_to_bytes(&coded[k]); + let y = bits_to_bytes(&buf_y); + bytes_to_bits(&interleave_units(&x, &y, w_units)?) + } else { + interleave_units(&coded[k], &buf_y, w_units)? + }; + } + buf_y.extend_from_slice(&buf_no); + buf_y.resize(buf_y.len() + nstuff, false); + + // ---- Header stages: class_attrib (+ its parity) then + // choice_of_pred (+ its parity), width = codeword length (or + // 28 for the SRCPC header case). + let mut attrib_full: Vec = attrib_bits.to_vec(); + if self.cfg.bit_stuffing == 1 { + for i in (0..3).rev() { + attrib_full.push(nstuff & (1usize << i) != 0); + } + } + if !attrib_full.is_empty() { + let mut x = attrib_full.clone(); + x.extend(self.header_parity(&attrib_full)?); + let w = self.header_width(attrib_full.len())?; + buf_y = interleave_units(&x, &buf_y, w)?; + } + if !pred_bits.is_empty() { + let mut x = pred_bits.to_vec(); + x.extend(self.header_parity(pred_bits)?); + let w = self.header_width(pred_bits.len())?; + buf_y = interleave_units(&x, &buf_y, w)?; + } + let _ = frame; + Ok(bits_to_bytes_padded(&buf_y)) + } + + /// Header parity via the Table 1.59 basic set, or the extended + /// §1.8.4.3 protection when configured and the part exceeds 16 + /// bits. + fn header_parity(&self, part: &[bool]) -> Result> { + if self.cfg.header_protection && part.len() > 16 { + let rate = self.cfg.header_rate.ok_or(Error::EpConfigInvalid)?; + let crc = EpClass::crclen_bits(self.cfg.header_crclen.ok_or(Error::EpConfigInvalid)?)?; + let mut with_crc = part.to_vec(); + if crc > 0 { + let poly = EpClass::crc_poly(crc)?.ok_or(Error::EpFrameInvalid)?; + let v = crc_bits(poly, part); + for i in (0..crc).rev() { + with_crc.push(v & (1u64 << i) != 0); + } + } + let coded = srcpc_encode(&with_crc, rate, true)?; + // The parity is the codeword past the systematic prefix + // is interleaved per-step; transmit the whole codeword + // minus the raw part positionally — same convention as + // ep_fec::header_fec_encode's SRCPC branch, generalised: + // here we simply append the full codeword after the part + // is *not* separately transmitted... To keep the wire + // shape "part then parity", the parity carries the coded + // stream with the leading systematic copies of the part + // removed positionally. + let mut parity = Vec::with_capacity(coded.len() - part.len()); + let p = crate::ep_fec::puncture_pattern(rate)?; + let mut pos = 0usize; + let steps = with_crc.len() + crate::ep_fec::SRCPC_TAIL_BITS; + for t in 0..steps { + for (i, &line) in p.iter().enumerate() { + if line & (0x80 >> (t % 8)) != 0 { + let bit = coded[pos]; + pos += 1; + let systematic_of_part = i == 0 && t < part.len(); + if !systematic_of_part { + parity.push(bit); + } + } + } + } + Ok(parity) + } else { + header_fec_encode(part) + } + } + + /// Bit length of [`Self::header_parity`] for an `l`-bit part. + fn header_parity_len(&self, l: usize) -> Result { + if self.cfg.header_protection && l > 16 { + let rate = self.cfg.header_rate.ok_or(Error::EpConfigInvalid)?; + let crc = EpClass::crclen_bits(self.cfg.header_crclen.ok_or(Error::EpConfigInvalid)?)? + as usize; + Ok(srcpc_coded_len(l + crc, rate, true)? - l) + } else { + crate::ep_fec::HeaderFec::for_len(l)?.parity_bits(l) + } + } + + /// Decode a header part protected by [`Self::header_parity`]. + fn header_unprotect(&self, part: &[bool], parity: &[bool]) -> Result> { + if self.cfg.header_protection && part.len() > 16 { + let rate = self.cfg.header_rate.ok_or(Error::EpConfigInvalid)?; + let crc = EpClass::crclen_bits(self.cfg.header_crclen.ok_or(Error::EpConfigInvalid)?)?; + // Re-merge the positional layout of header_parity. + let p = crate::ep_fec::puncture_pattern(rate)?; + let l = part.len(); + let with_crc_len = l + crc as usize; + let steps = with_crc_len + crate::ep_fec::SRCPC_TAIL_BITS; + let mut coded = Vec::with_capacity(l + parity.len()); + let mut pi = 0usize; + let mut ii = 0usize; + for t in 0..steps { + for (i, &line) in p.iter().enumerate() { + if line & (0x80 >> (t % 8)) != 0 { + if i == 0 && t < l { + coded.push(part[ii]); + ii += 1; + } else { + if pi >= parity.len() { + return Err(Error::EpFrameInvalid); + } + coded.push(parity[pi]); + pi += 1; + } + } + } + } + let decoded = srcpc_decode(&coded, with_crc_len, rate, true)?; + let (msg, rx_crc) = decoded.split_at(l); + if crc > 0 { + let poly = EpClass::crc_poly(crc)?.ok_or(Error::EpFrameInvalid)?; + let want = crc_bits(poly, msg); + let mut got = 0u64; + for &b in rx_crc { + got = (got << 1) | u64::from(b); + } + if got != want { + return Err(Error::EpFrameInvalid); + } + } + Ok(msg.to_vec()) + } else { + header_fec_decode(part, parity) + } + } + + /// Interleaver width for a header part (§1.8.4.8.2.1: the block + /// codeword length in bits, or 28 when SRCPC protects it). + fn header_width(&self, l: usize) -> Result { + if (self.cfg.header_protection && l > 16) + || matches!( + crate::ep_fec::HeaderFec::for_len(l)?, + crate::ep_fec::HeaderFec::Srcpc16 + ) + { + Ok(28) + } else { + Ok(l + self.header_parity_len(l)?) + } + } + + /// Decode one byte-aligned `ep_frame()`. + pub fn decode(&self, data: &[u8]) -> Result { + let total_bits = data.len() * 8; + let all_bits: Vec = (0..total_bits) + .map(|i| data[i / 8] & (0x80 >> (i % 8)) != 0) + .collect(); + + match self.cfg.interleave_type { + 0 => self.decode_mode0(&all_bits), + 1 | 2 => self.decode_interleaved(&all_bits), + _ => Err(Error::EpConfigInvalid), + } + } + + /// Read + verify the two header parts from a bit reader position. + fn read_headers(&self, bits: &[bool], pos: &mut usize) -> Result<(usize, usize, Vec)> { + // choice_of_pred (+ parity). + let npred = self.npred() as usize; + let choice = if npred > 0 { + let part = take(bits, pos, npred)?; + let parity = take(bits, pos, self.header_parity_len(npred)?)?; + let corrected = self.header_unprotect(&part, &parity)?; + let mut v = 0usize; + for &b in &corrected { + v = (v << 1) | usize::from(b); + } + v + } else { + 0 + }; + let set = self.sets.get(choice).ok_or(Error::EpFrameInvalid)?; + // class_attrib() length is fixed by the chosen set. + let mut attrib_l = 0usize; + for c in &set.classes { + if c.length_escape { + let w = usize::from(c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?); + attrib_l += w; // 0 for "until the end" + } + if c.rate_escape { + attrib_l += 3; + } + if c.crclen_escape { + attrib_l += 3; + } + } + if self.cfg.bit_stuffing == 1 { + attrib_l += 3; + } + let attrib = if attrib_l > 0 { + let part = take(bits, pos, attrib_l)?; + let parity = take(bits, pos, self.header_parity_len(attrib_l)?)?; + self.header_unprotect(&part, &parity)? + } else { + Vec::new() + }; + Ok((choice, attrib_l, attrib)) + } + + /// Parse the decoded `class_attrib()` bits into per-class in-band + /// values (`Table 1.52` order) + the stuffing count. + #[allow(clippy::type_complexity)] + fn parse_attrib( + &self, + choice: usize, + attrib: &[bool], + ) -> Result<(Vec>, Vec>, Vec>, usize)> { + let set = &self.sets[choice]; + let n = set.classes.len(); + let mut lens: Vec> = vec![None; n]; + let mut rates: Vec> = vec![None; n]; + let mut crcs: Vec> = vec![None; n]; + let mut pos = 0usize; + for jj in 0..n { + let k = if set.class_reordered_output { + usize::from(set.class_output_order[jj]) + } else { + jj + }; + let c = &set.classes[k]; + if c.length_escape { + let w = usize::from(c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?); + if w > 0 { + let v = take(attrib, &mut pos, w)?; + let mut acc = 0usize; + for &b in &v { + acc = (acc << 1) | usize::from(b); + } + lens[k] = Some(acc); + } + } + if c.rate_escape { + let v = take(attrib, &mut pos, 3)?; + let mut acc = 0u8; + for &b in &v { + acc = (acc << 1) | u8::from(b); + } + rates[k] = Some(acc); + } + if c.crclen_escape { + let v = take(attrib, &mut pos, 3)?; + let mut acc = 0u8; + for &b in &v { + acc = (acc << 1) | u8::from(b); + } + crcs[k] = Some(acc); + } + } + let nstuff = if self.cfg.bit_stuffing == 1 { + let v = take(attrib, &mut pos, 3)?; + let mut acc = 0usize; + for &b in &v { + acc = (acc << 1) | usize::from(b); + } + acc + } else { + 0 + }; + Ok((lens, rates, crcs, nstuff)) + } + + /// Resolve every class's runtime parameters + coded length; the + /// "until the end" class absorbs the remaining budget. + #[allow(clippy::too_many_arguments)] + fn resolve_frame( + &self, + choice: usize, + lens: &[Option], + rates: &[Option], + crcs: &[Option], + budget_bits: usize, + ) -> Result<(Vec, Vec, Vec)> { + let set = &self.sets[choice]; + let n = set.classes.len(); + let mut rts = Vec::with_capacity(n); + for j in 0..n { + let mut rt = self.resolve_class(&set.classes[j])?; + if rt.rate_escaped { + if rt.fec_type != 0 { + return Err(Error::EpFrameInvalid); + } + let code = rates[j].ok_or(Error::EpFrameInvalid)?; + rt.rate = *INBAND_RATE_TO_CLASS_RATE + .get(usize::from(code)) + .ok_or(Error::EpFrameInvalid)?; + } + if rt.crc_escaped { + let code = crcs[j].ok_or(Error::EpFrameInvalid)?; + rt.crc_bits = *INBAND_CRC_BITS + .get(usize::from(code)) + .ok_or(Error::EpFrameInvalid)?; + } + rts.push(rt); + } + // Info lengths: fixed, in-band, or until-the-end. + let mut info_lens: Vec> = Vec::with_capacity(n); + let mut open: Option = None; + for (j, rt) in rts.iter().enumerate() { + let l = match (rt.len_bits, rt.len_field_bits) { + (Some(l), _) => Some(l), + (None, Some(_)) => Some(lens[j].ok_or(Error::EpFrameInvalid)?), + (None, None) => { + if open.is_some() { + // Only one until-the-end class can exist. + return Err(Error::EpFrameInvalid); + } + open = Some(j); + None + } + }; + info_lens.push(l); + } + // Coded lengths of the closed classes (RS chains share their + // parity; compute chain-aware totals). + let mut coded_lens: Vec = vec![0; n]; + let mut consumed = 0usize; + let mut j = 0usize; + while j < n { + if rts[j].fec_type == 2 { + let mut last = j; + while last < n && rts[last].fec_type == 2 { + last += 1; + } + if last >= n { + return Err(Error::EpFrameInvalid); + } + if (j..=last).any(|m| info_lens[m].is_none()) { + // An until-the-end class inside an RS chain is + // not resolvable. + return Err(Error::EpFrameInvalid); + } + let mut chain_bits = 0usize; + for m in j..=last { + let with_crc = info_lens[m].unwrap_or(0) + rts[m].crc_bits as usize; + if with_crc % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + coded_lens[m] = with_crc; + chain_bits += with_crc; + } + let two_k = 2 * usize::from(rts[last].rate); + if two_k > 0 { + if two_k >= 255 { + return Err(Error::EpConfigInvalid); + } + let parts = (chain_bits / 8).div_ceil(255 - two_k); + coded_lens[last] += 8 * two_k * parts; + } + for &cl in coded_lens.iter().take(last + 1).skip(j) { + consumed += cl; + } + j = last + 1; + } else { + if let Some(l) = info_lens[j] { + coded_lens[j] = self.coded_len(&rts[j], l)?; + consumed += coded_lens[j]; + } + j += 1; + } + } + if let Some(open_j) = open { + let remaining = budget_bits + .checked_sub(consumed) + .ok_or(Error::EpFrameInvalid)?; + // Search the info length whose coded length fills the + // remainder exactly (§1.8.4.1: the boundary is known from + // the access-unit length). + let rt = &rts[open_j]; + let mut found = None; + // The coded length grows monotonically with the info + // length; scan candidates. + let max_info = remaining; + let mut lo = 0usize; + let mut hi = max_info; + while lo <= hi { + let mid = (lo + hi) / 2; + let cl = self.coded_len(rt, mid); + match cl { + Ok(cl) => match cl.cmp(&remaining) { + core::cmp::Ordering::Equal => { + found = Some(mid); + break; + } + core::cmp::Ordering::Less => lo = mid + 1, + core::cmp::Ordering::Greater => { + if mid == 0 { + break; + } + hi = mid - 1; + } + }, + Err(_) => { + // RS byte alignment: step to the next octet. + lo = mid + 1; + } + } + } + // The binary search can miss non-monotone byte-alignment + // gaps for RS classes; fall back to a linear scan near + // the boundary. + if found.is_none() { + for cand in 0..=max_info { + if let Ok(cl) = self.coded_len(rt, cand) { + if cl == remaining { + found = Some(cand); + break; + } + } + if cand > 4096 && rt.fec_type == 0 { + break; + } + } + } + let info = found.ok_or(Error::EpFrameInvalid)?; + info_lens[open_j] = Some(info); + coded_lens[open_j] = remaining; + } else if consumed != budget_bits { + return Err(Error::EpFrameInvalid); + } + let infos: Vec = info_lens.into_iter().map(|l| l.unwrap_or(0)).collect(); + Ok((rts, infos, coded_lens)) + } + + fn decode_mode0(&self, bits: &[bool]) -> Result { + let mut pos = 0usize; + let (choice, _attrib_l, attrib) = self.read_headers(bits, &mut pos)?; + let (lens, rates, crcs, nstuff) = self.parse_attrib(choice, &attrib)?; + let budget = bits + .len() + .checked_sub(pos + nstuff) + .ok_or(Error::EpFrameInvalid)?; + // Without bit stuffing the byte carrier can hold up to 7 + // slack bits that are not part of the frame; with stuffing the + // budget is exact. Try the exact budget first, then shrink. + let mut last_err = Error::EpFrameInvalid; + let slack_range = if self.cfg.bit_stuffing == 1 { 0 } else { 7 }; + for slack in 0..=slack_range { + let Some(b) = budget.checked_sub(slack) else { + break; + }; + match self.try_decode_classes(choice, &lens, &rates, &crcs, bits, pos, b) { + Ok(mut frame) => { + frame.choice_of_pred = choice; + return Ok(frame); + } + Err(e) => last_err = e, + } + } + Err(last_err) + } + + #[allow(clippy::too_many_arguments)] + fn try_decode_classes( + &self, + choice: usize, + lens: &[Option], + rates: &[Option], + crcs: &[Option], + bits: &[bool], + mut pos: usize, + budget: usize, + ) -> Result { + let set = &self.sets[choice]; + let n = set.classes.len(); + let (rts, infos, coded_lens) = self.resolve_frame(choice, lens, rates, crcs, budget)?; + // Slice the transmitted classes. + let mut coded: Vec> = vec![Vec::new(); n]; + for jj in 0..n { + let k = if set.class_reordered_output { + usize::from(set.class_output_order[jj]) + } else { + jj + }; + coded[k] = take(bits, &mut pos, coded_lens[k])?; + } + self.unprotect_all(&rts, &infos, coded, rates, crcs, choice) + } + + /// FEC/CRC-decode all classes (chain-aware) and assemble the + /// frame data. + fn unprotect_all( + &self, + rts: &[ClassRt], + infos: &[usize], + coded: Vec>, + rates: &[Option], + crcs: &[Option], + choice: usize, + ) -> Result { + let n = rts.len(); + let mut classes: Vec> = vec![Vec::new(); n]; + let mut j = 0usize; + while j < n { + if rts[j].fec_type == 2 { + let mut last = j; + while last < n && rts[last].fec_type == 2 { + last += 1; + } + if last >= n { + return Err(Error::EpFrameInvalid); + } + // Reassemble the chain: members' CRC-protected bits + + // the parity on the last member. + let mut chain: Vec = Vec::new(); + for (m, c) in coded.iter().enumerate().take(last + 1).skip(j) { + let with_crc = infos[m] + rts[m].crc_bits as usize; + if c.len() < with_crc { + return Err(Error::EpFrameInvalid); + } + chain.extend_from_slice(&c[..with_crc]); + } + let parity_bits = &coded[last][infos[last] + rts[last].crc_bits as usize..]; + let mut data = bits_to_bytes(&chain); + let parity = bits_to_bytes(parity_bits); + srs_decode(&mut data, &parity, usize::from(rts[last].rate))?; + let chain_bits = bytes_to_bits(&data); + let mut off = 0usize; + for m in j..=last { + let with_crc = infos[m] + rts[m].crc_bits as usize; + let seg = &chain_bits[off..off + with_crc]; + off += with_crc; + let mut info = seg[..infos[m]].to_vec(); + if rts[m].crc_bits > 0 { + let poly = + EpClass::crc_poly(rts[m].crc_bits)?.ok_or(Error::EpFrameInvalid)?; + let want = crc_bits(poly, &info); + let mut got = 0u64; + for &b in &seg[infos[m]..] { + got = (got << 1) | u64::from(b); + } + if got != want { + return Err(Error::EpFrameInvalid); + } + } + core::mem::swap(&mut classes[m], &mut info); + } + j = last + 1; + } else { + classes[j] = self.unprotect_class(&rts[j], &coded[j], infos[j])?; + j += 1; + } + } + Ok(EpFrameData { + choice_of_pred: choice, + classes, + rate_codes: rates.to_vec(), + crc_codes: crcs.to_vec(), + }) + } + + fn decode_interleaved(&self, bits: &[bool]) -> Result { + let mode2 = self.cfg.interleave_type == 2; + // Reverse the header stages: choice_of_pred first. + let npred = self.npred() as usize; + let mut stream: Vec = bits.to_vec(); + let choice = if npred > 0 { + let xl = npred + self.header_parity_len(npred)?; + let w = self.header_width(npred)?; + let (x, y) = deinterleave_units_bits(&stream, xl, w)?; + stream = y; + let corrected = self.header_unprotect(&x[..npred], &x[npred..])?; + let mut v = 0usize; + for &b in &corrected { + v = (v << 1) | usize::from(b); + } + v + } else { + 0 + }; + let set = self.sets.get(choice).ok_or(Error::EpFrameInvalid)?; + let mut attrib_l = 0usize; + for c in &set.classes { + if c.length_escape { + attrib_l += usize::from(c.number_of_bits_for_length.unwrap_or(0)); + } + if c.rate_escape { + attrib_l += 3; + } + if c.crclen_escape { + attrib_l += 3; + } + } + if self.cfg.bit_stuffing == 1 { + attrib_l += 3; + } + let attrib = if attrib_l > 0 { + let xl = attrib_l + self.header_parity_len(attrib_l)?; + let w = self.header_width(attrib_l)?; + let (x, y) = deinterleave_units_bits(&stream, xl, w)?; + stream = y; + self.header_unprotect(&x[..attrib_l], &x[attrib_l..])? + } else { + Vec::new() + }; + let (lens, rates, crcs, nstuff) = self.parse_attrib(choice, &attrib)?; + + // The class stream: everything minus trailing slack/stuffing. + let mut last_err = Error::EpFrameInvalid; + let slack_range = if self.cfg.bit_stuffing == 1 { 0 } else { 7 }; + for slack in 0..=slack_range { + let Some(budget) = stream.len().checked_sub(nstuff + slack) else { + break; + }; + match self.try_decode_interleaved_classes( + choice, + &lens, + &rates, + &crcs, + &stream[..budget], + mode2, + ) { + Ok(frame) => return Ok(frame), + Err(e) => last_err = e, + } + } + Err(last_err) + } + + fn try_decode_interleaved_classes( + &self, + choice: usize, + lens: &[Option], + rates: &[Option], + crcs: &[Option], + class_stream: &[bool], + mode2: bool, + ) -> Result { + let set = &self.sets[choice]; + let n = set.classes.len(); + let (rts, infos, coded_lens) = + self.resolve_frame(choice, lens, rates, crcs, class_stream.len())?; + let tx_order: Vec = (0..n) + .map(|jj| { + if set.class_reordered_output { + usize::from(set.class_output_order[jj]) + } else { + jj + } + }) + .collect(); + // Undo the class-stage interleaving: the encoder ran the + // reverse loop last-to-first, so decode unwinds first-to-last. + // In mode 2 the non-interleaved (switch-0) classes were + // appended AFTER the interleave stages — split them off the + // tail before unwinding. + let mut buf_no_len = 0usize; + if mode2 { + for (k, rt) in rts.iter().enumerate() { + if rt.interleave_switch == 0 { + buf_no_len += coded_lens[k]; + } + } + } + if buf_no_len > class_stream.len() { + return Err(Error::EpFrameInvalid); + } + let (inter_part, buf_no) = class_stream.split_at(class_stream.len() - buf_no_len); + let mut stream = inter_part.to_vec(); + let mut coded: Vec> = vec![Vec::new(); n]; + // Interleaved classes, in the encoder's reverse-of-reverse + // order (i.e. transmitted forward order). + for &k in tx_order.iter().take(n) { + let sw = if mode2 { rts[k].interleave_switch } else { 1 }; + if mode2 && (sw == 0 || sw == 3) { + continue; + } + let bytewise = rts[k].fec_type != 0; + let w_units = if mode2 && sw == 2 { + 28 + } else if bytewise { + if coded_lens[k] % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + coded_lens[k] / 8 + } else if mode2 { + coded_lens[k] + } else { + 28 + }; + if bytewise { + if stream.len() % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + let z = bits_to_bytes(&stream); + let (x, y) = deinterleave_units(&z, coded_lens[k] / 8, w_units)?; + coded[k] = bytes_to_bits(&x); + stream = bytes_to_bits(&y); + } else { + let (x, y) = deinterleave_units_bits(&stream, coded_lens[k], w_units)?; + coded[k] = x; + stream = y; + } + } + if mode2 { + // The remaining stream is the innermost BUF_Y: the + // switch-3 concatenated classes in forward order. + let mut pos = 0usize; + for &k in tx_order.iter().take(n) { + if rts[k].interleave_switch == 3 { + coded[k] = take(&stream, &mut pos, coded_lens[k])?; + } + } + if pos != stream.len() { + return Err(Error::EpFrameInvalid); + } + // The switch-0 classes ride the tail suffix. + let mut pos = 0usize; + for &k in tx_order.iter().take(n) { + if rts[k].interleave_switch == 0 { + coded[k] = take(buf_no, &mut pos, coded_lens[k])?; + } + } + if pos != buf_no.len() { + return Err(Error::EpFrameInvalid); + } + } else if !stream.is_empty() { + return Err(Error::EpFrameInvalid); + } + self.unprotect_all(&rts, &infos, coded, rates, crcs, choice) + } +} + +/// §1.8.4.8.1 recursive interleaver over generic units: X row-major, +/// Y filling the residual cells column-wise, output read column-major +/// with `k = m·D + min(m, d) + n`. +fn interleave_units(x: &[T], y: &[T], w: usize) -> Result> { + if w == 0 { + return Err(Error::EpFrameInvalid); + } + let total = x.len() + y.len(); + let d_rows = total / w; + let d = total - d_rows * w; + let col_height = |m: usize| d_rows + usize::from(m < d); + let k_of = |m: usize, n: usize| m * d_rows + m.min(d) + n; + + let dp = x.len() / w; + let dpr = x.len() - dp * w; + + let mut out = vec![T::default(); total]; + // X: row-major. + for (i, &v) in x.iter().enumerate() { + let m = i % w; + let n = i / w; + out[k_of(m, n)] = v; + } + // Y: column-wise into the residual cells. + let mut yi = 0usize; + for m in 0..w { + let start = dp + usize::from(m < dpr); + for n in start..col_height(m) { + if yi >= y.len() { + return Err(Error::EpFrameInvalid); + } + out[k_of(m, n)] = y[yi]; + yi += 1; + } + } + if yi != y.len() { + return Err(Error::EpFrameInvalid); + } + Ok(out) +} + +/// Inverse of [`interleave_units`] given `lx` and the width. +fn deinterleave_units(z: &[T], lx: usize, w: usize) -> Result<(Vec, Vec)> { + if w == 0 || lx > z.len() { + return Err(Error::EpFrameInvalid); + } + let total = z.len(); + let d_rows = total / w; + let d = total - d_rows * w; + let col_height = |m: usize| d_rows + usize::from(m < d); + let k_of = |m: usize, n: usize| m * d_rows + m.min(d) + n; + let dp = lx / w; + let dpr = lx - dp * w; + let mut x = vec![T::default(); lx]; + for (i, xv) in x.iter_mut().enumerate() { + let m = i % w; + let n = i / w; + *xv = z[k_of(m, n)]; + } + let mut y = Vec::with_capacity(total - lx); + for m in 0..w { + let start = dp + usize::from(m < dpr); + for n in start..col_height(m) { + y.push(z[k_of(m, n)]); + } + } + Ok((x, y)) +} + +fn deinterleave_units_bits(z: &[bool], lx: usize, w: usize) -> Result<(Vec, Vec)> { + deinterleave_units(z, lx, w) +} + +fn take(bits: &[bool], pos: &mut usize, n: usize) -> Result> { + if *pos + n > bits.len() { + return Err(Error::EpFrameInvalid); + } + let v = bits[*pos..*pos + n].to_vec(); + *pos += n; + Ok(v) +} + +fn bits_to_bytes(bits: &[bool]) -> Vec { + debug_assert_eq!(bits.len() % 8, 0); + bits.chunks(8) + .map(|c| c.iter().fold(0u8, |acc, &b| (acc << 1) | u8::from(b))) + .collect() +} + +fn bits_to_bytes_padded(bits: &[bool]) -> Vec { + let mut v = Vec::with_capacity(bits.len().div_ceil(8)); + for chunk in bits.chunks(8) { + let mut b = 0u8; + for (i, &bit) in chunk.iter().enumerate() { + if bit { + b |= 0x80 >> i; + } + } + v.push(b); + } + v +} + +fn bytes_to_bits(bytes: &[u8]) -> Vec { + let mut v = Vec::with_capacity(bytes.len() * 8); + for &b in bytes { + for i in 0..8 { + v.push(b & (0x80 >> i) != 0); + } + } + v +} + +/// Emit a parsed frame back through a [`BitWriter`] (whole bytes). +pub fn write_frame(w: &mut BitWriter, frame_bytes: &[u8]) { + for &b in frame_bytes { + w.write_u32(u32::from(b), 8); + } +} + +/// Convenience: read the remaining whole bytes of a reader. +pub fn read_remaining_bytes(reader: &mut BitReader<'_>, total_len: usize) -> Result> { + let pos = reader.bit_position() as usize; + if pos % 8 != 0 { + return Err(Error::EpFrameInvalid); + } + let mut out = Vec::with_capacity(total_len - pos / 8); + for _ in (pos / 8)..total_len { + out.push(reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)? as u8); + } + Ok(out) +} diff --git a/crates/vendor/oxideav-aac/src/ep_rs.rs b/crates/vendor/oxideav-aac/src/ep_rs.rs new file mode 100644 index 00000000..b57e934c --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ep_rs.rs @@ -0,0 +1,434 @@ +//! §1.8.4.7 shortened Reed-Solomon codes of the MPEG-4 +//! error-protection tool. +//! +//! `SRS(255−l, 255−2k−l)` over GF(2⁸) built on the primitive +//! polynomial `m(x) = x⁸ + x⁴ + x³ + x² + 1` (the Table 1.62 α-power +//! listing is exactly the antilog table of that polynomial — pinned +//! by tests against printed rows). The generator is +//! `g(x) = (x−α)(x−α²)…(x−α^2k)`; a class longer than `255−2k` octets +//! splits into parts (`l_i = 255−2k` except the zero-padded last), +//! each part's parity is `p(x) = x^2k·u(x) mod g(x)` with the +//! **lowest-order coefficient as the first octet** (§1.8.4.7), and +//! all parities are appended after the class data (Figure 1.11). +//! +//! Decoding runs the standard algebraic chain over the spec's field: +//! syndromes `S_j = r(α^j)`, Berlekamp-Massey for the error locator, +//! Chien search, Forney evaluation — correcting up to `k` byte errors +//! per part; an uncorrectable part surfaces +//! [`Error::EpFrameInvalid`]. + +use crate::{Error, Result}; + +/// GF(2⁸) tables for `m(x) = x⁸ + x⁴ + x³ + x² + 1` (0x11D). +struct Gf { + exp: [u8; 512], + log: [u8; 256], +} + +fn gf() -> &'static Gf { + use std::sync::OnceLock; + static GF: OnceLock = OnceLock::new(); + GF.get_or_init(|| { + let mut exp = [0u8; 512]; + let mut log = [0u8; 256]; + let mut v: u16 = 1; + #[allow(clippy::needless_range_loop)] + for i in 0..255 { + exp[i] = v as u8; + log[v as usize] = i as u8; + v <<= 1; + if v & 0x100 != 0 { + v ^= 0x11D; + } + } + for i in 255..512 { + exp[i] = exp[i - 255]; + } + Gf { exp, log } + }) +} + +#[inline] +fn gf_mul(a: u8, b: u8) -> u8 { + if a == 0 || b == 0 { + return 0; + } + let g = gf(); + g.exp[usize::from(g.log[usize::from(a)]) + usize::from(g.log[usize::from(b)])] +} + +#[inline] +fn gf_inv(a: u8) -> Result { + if a == 0 { + return Err(Error::EpFrameInvalid); + } + let g = gf(); + Ok(g.exp[255 - usize::from(g.log[usize::from(a)])]) +} + +/// α^i (`0 <= i`), the Table 1.62 antilog. +pub fn alpha_pow(i: usize) -> u8 { + gf().exp[i % 255] +} + +/// The §1.8.4.7 generator polynomial `g(x) = ∏_{i=1..2k} (x − α^i)`, +/// lowest-order coefficient first, length `2k + 1` (monic). +fn generator(two_k: usize) -> Vec { + let mut g = vec![0u8; two_k + 1]; + g[0] = 1; + let mut deg = 0usize; + for i in 1..=two_k { + let a = alpha_pow(i); + // g = g * (x + α^i) (− == + in GF(2^8)). + deg += 1; + for j in (1..=deg).rev() { + g[j] = g[j - 1] ^ gf_mul(g[j], a); + } + g[0] = gf_mul(g[0], a); + } + g +} + +/// Parity octets (`2k`, lowest order first) for one part `u` of at +/// most `255 − 2k` octets: `p(x) = x^2k · u(x) mod g(x)` with the +/// first octet of `u` as the lowest-order coefficient (§1.8.4.7). +fn part_parity(part: &[u8], two_k: usize) -> Vec { + let g = generator(two_k); + // Work highest-order-first for the long division: u(x)·x^2k has + // coefficients [0; 2k] ++ part (lowest first). Highest order is + // the LAST octet of `part`. + let mut rem = vec![0u8; two_k]; // remainder, highest order at [0] + for &coeff in part.iter().rev() { + let factor = rem[0] ^ coeff; + // Shift left by one (multiply by x) and subtract factor·g. + for i in 0..two_k { + let next = if i + 1 < two_k { rem[i + 1] } else { 0 }; + rem[i] = next ^ gf_mul(factor, g[two_k - 1 - i]); + } + } + // rem[0] is the highest-order remainder coefficient; the wire + // wants lowest order first. + rem.reverse(); + rem +} + +/// §1.8.4.7 part split of a class of `len` octets under `2k` parity +/// octets per part: every part is `255 − 2k` long except the last +/// (`len mod (255 − 2k)`, zero-padded for the computation). +fn part_lengths(len: usize, two_k: usize) -> Result> { + let cap = 255 - two_k; + if cap == 0 || len == 0 { + return Err(Error::EpFrameInvalid); + } + let n = len.div_ceil(cap); + let mut parts = Vec::with_capacity(n); + for i in 0..n { + if i + 1 < n { + parts.push(cap); + } else { + let last = len - cap * (n - 1); + parts.push(last); + } + } + Ok(parts) +} + +/// SRS-encode a class: returns the parity octets to append after the +/// class data (all parts' parities in part order, Figure 1.11). +/// +/// `k` is the per-codeword correction capability (`class_rate` for +/// `fec_type == 1 / 2`); `k == 0` yields no parity. +pub fn srs_encode(class_data: &[u8], k: usize) -> Result> { + if k == 0 { + return Ok(Vec::new()); + } + let two_k = 2 * k; + if two_k >= 255 { + return Err(Error::EpConfigInvalid); + } + let parts = part_lengths(class_data.len(), two_k)?; + let cap = 255 - two_k; + let mut out = Vec::with_capacity(two_k * parts.len()); + let mut pos = 0usize; + for (i, &plen) in parts.iter().enumerate() { + let mut part = class_data[pos..pos + plen].to_vec(); + pos += plen; + if i + 1 == parts.len() && plen < cap { + // §1.8.4.7: zero-pad the short last part for the + // computation only. + part.resize(cap, 0); + } + out.extend_from_slice(&part_parity(&part, two_k)); + } + Ok(out) +} + +/// SRS-decode a class in place: `class_data` are the received data +/// octets, `parity` the received parity octets ([`srs_encode`] +/// layout). Corrects up to `k` byte errors per part (errors in the +/// parity octets included); an uncorrectable part is +/// [`Error::EpFrameInvalid`]. +pub fn srs_decode(class_data: &mut [u8], parity: &[u8], k: usize) -> Result<()> { + if k == 0 { + return Ok(()); + } + let two_k = 2 * k; + if two_k >= 255 { + return Err(Error::EpConfigInvalid); + } + let parts = part_lengths(class_data.len(), two_k)?; + if parity.len() != two_k * parts.len() { + return Err(Error::EpFrameInvalid); + } + let cap = 255 - two_k; + let mut pos = 0usize; + for (i, &plen) in parts.iter().enumerate() { + // Codeword c(x): parity (lowest orders 0..2k) then data + // (orders 2k..). Build lowest-order-first. + let mut cw = vec![0u8; 255]; + cw[..two_k].copy_from_slice(&parity[i * two_k..(i + 1) * two_k]); + let part = &class_data[pos..pos + plen]; + for (j, &b) in part.iter().enumerate() { + cw[two_k + j] = b; + } + // (zero padding of a short last part occupies the top orders + // implicitly.) + let corrected = rs_correct(&mut cw, k)?; + let _ = corrected; + // Verify the padding stayed zero (errors located there would + // mean a miscorrection for a conforming stream). + for j in plen..cap { + if cw[two_k + j] != 0 { + return Err(Error::EpFrameInvalid); + } + } + class_data[pos..pos + plen].copy_from_slice(&cw[two_k..two_k + plen]); + pos += plen; + } + Ok(()) +} + +/// Correct one 255-octet codeword (lowest-order coefficient first) in +/// place; returns the number of corrected byte errors. +fn rs_correct(cw: &mut [u8], k: usize) -> Result { + let two_k = 2 * k; + // Syndromes S_j = c(α^j), j = 1..=2k. + let mut synd = vec![0u8; two_k]; + let mut any = false; + for (j, s) in synd.iter_mut().enumerate() { + let a = alpha_pow(j + 1); + let mut acc = 0u8; + // Horner from the highest order down. + for &c in cw.iter().rev() { + acc = gf_mul(acc, a) ^ c; + } + *s = acc; + any |= acc != 0; + } + if !any { + return Ok(0); + } + + // Berlekamp-Massey for the error locator Λ(x) (lowest order + // first, Λ(0) = 1). + let mut lambda = vec![0u8; two_k + 1]; + let mut prev = vec![0u8; two_k + 1]; + lambda[0] = 1; + prev[0] = 1; + let mut l = 0usize; + let mut m = 1usize; + let mut b = 1u8; + for n in 0..two_k { + // Discrepancy. + let mut delta = synd[n]; + for i in 1..=l { + delta ^= gf_mul(lambda[i], synd[n - i]); + } + if delta == 0 { + m += 1; + } else if 2 * l <= n { + let t = lambda.clone(); + let coef = gf_mul(delta, gf_inv(b)?); + for i in 0..=two_k { + if i >= m && prev[i - m] != 0 { + lambda[i] ^= gf_mul(coef, prev[i - m]); + } + } + prev = t; + l = n + 1 - l; + b = delta; + m = 1; + } else { + let coef = gf_mul(delta, gf_inv(b)?); + for i in 0..=two_k { + if i >= m && prev[i - m] != 0 { + lambda[i] ^= gf_mul(coef, prev[i - m]); + } + } + m += 1; + } + } + if l > k { + return Err(Error::EpFrameInvalid); + } + + // Chien search: error at position p iff Λ(α^{-p}) == 0. + let mut err_pos = Vec::with_capacity(l); + for p in 0..255usize { + let x = alpha_pow((255 - p) % 255); // α^{-p} + let mut acc = 0u8; + for i in (0..=l).rev() { + acc = gf_mul(acc, x) ^ lambda[i]; + } + if acc == 0 { + err_pos.push(p); + } + } + if err_pos.len() != l { + return Err(Error::EpFrameInvalid); + } + + // Forney: error magnitudes from the evaluator + // Ω(x) = S(x)·Λ(x) mod x^{2k}. + let mut omega = vec![0u8; two_k]; + for i in 0..two_k { + let mut acc = 0u8; + for j in 0..=i.min(l) { + if lambda[j] != 0 && i >= j { + acc ^= gf_mul(lambda[j], synd[i - j]); + } + } + omega[i] = acc; + } + // Λ'(x): formal derivative (odd-power terms). Forney with the + // first syndrome at j = 1: e_p = Ω(X_p⁻¹) / Λ'(X_p⁻¹). + for &p in &err_pos { + let x_inv = alpha_pow((255 - p) % 255); + // Ω(x_inv), Horner highest order down. + let mut om = 0u8; + for i in (0..two_k).rev() { + om = gf_mul(om, x_inv) ^ omega[i]; + } + // Λ'(x_inv) = Σ_{i odd, i <= l} Λ_i · x_inv^{i−1}. + let mut dl = 0u8; + for i in (1..=l).step_by(2) { + dl ^= gf_mul(lambda[i], gf_pow(x_inv, i - 1)); + } + if dl == 0 { + return Err(Error::EpFrameInvalid); + } + let magnitude = gf_mul(om, gf_inv(dl)?); + cw[p] ^= magnitude; + } + + // Re-verify. + for j in 1..=two_k { + let a = alpha_pow(j); + let mut acc = 0u8; + for &c in cw.iter().rev() { + acc = gf_mul(acc, a) ^ c; + } + if acc != 0 { + return Err(Error::EpFrameInvalid); + } + } + Ok(l) +} + +/// `x^i` in GF(2⁸). +fn gf_pow(x: u8, i: usize) -> u8 { + let mut acc = 1u8; + for _ in 0..i { + acc = gf_mul(acc, x); + } + acc +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Spot-check the generated antilog table against printed rows of + /// Table 1.62. + #[test] + fn alpha_table_matches_table_1_62() { + assert_eq!(alpha_pow(0), 0b0000_0001); + assert_eq!(alpha_pow(1), 0b0000_0010); + assert_eq!(alpha_pow(8), 0b0001_1101); + assert_eq!(alpha_pow(63), 0b1010_0001); + assert_eq!(alpha_pow(64), 0b0101_1111); + assert_eq!(alpha_pow(127), 0b1100_1100); + assert_eq!(alpha_pow(128), 0b1000_0101); + assert_eq!(alpha_pow(175), 0b1111_1111); + assert_eq!(alpha_pow(191), 0b0100_0001); + assert_eq!(alpha_pow(254), 0b1000_1110); + } + + fn prand_bytes(n: usize, mut seed: u32) -> Vec { + let mut v = Vec::with_capacity(n); + for _ in 0..n { + seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); + v.push((seed >> 16) as u8); + } + v + } + + #[test] + fn srs_roundtrip_clean() { + for (len, k) in [(10usize, 2usize), (100, 4), (300, 8), (251, 2), (600, 1)] { + let data = prand_bytes(len, 0xA5A5 ^ len as u32); + let parity = srs_encode(&data, k).unwrap(); + let n_parts = len.div_ceil(255 - 2 * k); + assert_eq!(parity.len(), 2 * k * n_parts, "len {len} k {k}"); + let mut rx = data.clone(); + srs_decode(&mut rx, &parity, k).unwrap(); + assert_eq!(rx, data, "len {len} k {k}"); + } + } + + #[test] + fn srs_corrects_byte_errors() { + let data = prand_bytes(120, 0x5EED); + let k = 4; + let parity = srs_encode(&data, k).unwrap(); + // Up to k errors in the data part. + let mut rx = data.clone(); + rx[3] ^= 0x41; + rx[57] ^= 0xFF; + rx[100] ^= 0x01; + rx[119] ^= 0x80; + srs_decode(&mut rx, &parity, k).unwrap(); + assert_eq!(rx, data); + + // Errors in the parity octets are located and ignored for the + // data reconstruction. + let mut rx = data.clone(); + let mut bad_parity = parity.clone(); + bad_parity[0] ^= 0x10; + bad_parity[5] ^= 0x22; + srs_decode(&mut rx, &bad_parity, k).unwrap(); + assert_eq!(rx, data); + + // k + 1 errors are uncorrectable. + let mut rx = data.clone(); + for (i, b) in rx.iter_mut().enumerate().take(k + 1) { + *b ^= 0x11 + i as u8; + } + assert!(srs_decode(&mut rx, &parity, k).is_err()); + } + + #[test] + fn srs_multi_part_correction() { + // 300 octets with k = 8 → parts of 239 + 61; errors in both + // parts correct independently. + let data = prand_bytes(300, 0x77); + let k = 8; + let parity = srs_encode(&data, k).unwrap(); + let mut rx = data.clone(); + for &p in &[0usize, 100, 238, 239, 250, 299] { + rx[p] ^= 0x5A; + } + srs_decode(&mut rx, &parity, k).unwrap(); + assert_eq!(rx, data); + } +} diff --git a/crates/vendor/oxideav-aac/src/error.rs b/crates/vendor/oxideav-aac/src/error.rs new file mode 100644 index 00000000..4ce425c1 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/error.rs @@ -0,0 +1,1244 @@ +//! Crate-local error type. + +/// Errors returned by `oxideav-aac` Phase 1 surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + /// Decode / encode body is not implemented yet (Phase 1 skeleton). + NotImplemented, + + /// ADTS sync pattern (`syncword = 0xFFF`, 12 bits) not found at the + /// expected position. ISO/IEC 13818-7 §1.A.2.2.1. + AdtsSyncNotFound, + + /// ADTS layer field must be `00` per ISO/IEC 13818-7 §1.A.2.2.1 + /// (the *Layer* field is reserved for MPEG-1/2 layer signalling + /// and is required zero in ADTS). Decoder rejects non-zero. + AdtsLayerNonZero, + + /// Reserved `sampling_frequency_index` value (13 or 14). ISO/IEC + /// 14496-3 Table 1.18 marks indices 13 and 14 as reserved; index + /// 15 signals an explicit 24-bit rate (only present in + /// `AudioSpecificConfig`, never in an ADTS header — the ADTS + /// field is 4 bits so the legal range is 0..=12). + AdtsReservedSampleRateIndex, + + /// ADTS `aac_frame_length` is smaller than the fixed header + /// itself (7 bytes without CRC, 9 bytes with CRC). Such a frame + /// is malformed and cannot wrap any payload. + AdtsFrameLengthTooSmall, + + /// An in-memory [`crate::adts::AdtsHeader`] cannot be + /// serialised: a field exceeds its ADTS wire width or violates + /// a normative constraint (reserved sampling-frequency index, + /// `aac_frame_length` below the header overhead, raw-data-block + /// count outside `1..=4`). + AdtsEncodeInvalid, + + /// [`crate::encoder::StreamEncoder`] configuration is invalid: + /// the sample rate is not a Table 1.18 ADTS rate, the channel + /// count is not 1 or 2, the bitrate is 0, or an input slice + /// exceeds the per-frame hop / is not a whole number of + /// interleaved sample tuples. + EncoderInvalidConfig, + + /// The assembled encoder frame exceeds the 13-bit ADTS + /// `aac_frame_length` ceiling even after the rate loop. + EncoderFrameOverflow, + + /// The bit-reader hit end-of-stream while parsing. + UnexpectedEnd, + + /// Encountered an `id_syn_ele` value the walker cannot advance + /// past in Phase 1. Carries the raw 3-bit value (0..=7) — the + /// caller can map it back to ISO/IEC 14496-3 Table 4.71 names. + /// Phase 1 can step past FIL (`0b110`), DSE (`0b100`), and PCE + /// (`0b101`); the channel elements (SCE/CPE/CCE/LFE) still + /// require body parsing that is deferred. + UnsupportedElementSkip(u8), + + /// `AudioSpecificConfig` carried an `audioObjectType` whose + /// body Phase 1 does not parse. The General Audio AOTs handled + /// by Phase 1 are 1 (Main), 2 (LC), 3 (SSR), 4 (LTP), 6 + /// (scalable), 7 (TwinVQ), 17 (ER AAC LC), 19 (ER AAC LTP), 20 + /// (ER AAC scalable), 21 (ER TwinVQ), 22 (ER BSAC), 23 (ER AAC + /// LD); SBR (5) and PS (29) hierarchical wrappers are + /// unwrapped before this check. Any other AOT — CELP, HVXC, + /// SSC, USAC, ELD, ALS, SLS, … — currently surfaces here. + UnsupportedAot(u8), + + /// [`crate::ics_info::IcsInfo::parse`] was called with a + /// `sampling_frequency_index` outside the standard 0..=11 + /// range covered by the `NUM_SWB_{LONG,SHORT}_WINDOW` tables. + /// The 24-bit explicit-rate escape (`samplingFrequencyIndex + /// == 0xf`) does not select an SWB table directly — the caller + /// must resolve the explicit rate to the nearest standard + /// index before invoking the ics_info parser. + IcsInfoUnsupportedSampleRateIndex(u8), + + /// An `EIGHT_SHORT_SEQUENCE` (or any short-window geometry) was + /// requested for an ER AAC LD frame family. §4.6.17.2.2: the low + /// delay coder has no block switching, so the 512/480-line + /// families define no short-window tables at all — a stream + /// signalling a non-`ONLY_LONG` window sequence under AOT 23 is + /// malformed. + LdShortWindow, + + /// An SBR extension payload arrived on a stream running a + /// non-1024-line §4.5.1.1 frame family. The §4.6.18 SBR tool in + /// this crate covers the 1024-line core (32-subband QMF analysis, + /// 2048-sample dual-rate output); SBR over a 960-line core (and + /// the §4.6.19 LD SBR tool) is out of scope. + SbrUnsupportedFrameFamily, + + /// [`crate::ics_info::IcsInfo::write`] was handed an in-memory + /// [`crate::ics_info::IcsInfo`] whose field combination cannot + /// be represented on the wire under ISO/IEC 14496-3 Table 4.6 / + /// Table 4.55. Examples: `max_sfb` exceeds its field width + /// (`> 15` for `EIGHT_SHORT_SEQUENCE`, `> 63` otherwise); + /// `scale_factor_grouping == None` for `EIGHT_SHORT_SEQUENCE` or + /// `Some(_)` for any other window sequence; a predictor / LTP + /// body slot is populated while the dispatching + /// `predictor_data_present` bit is zero, or vice versa; a + /// non-Main AOT has `predictor_data` set instead of `ltp_data`; + /// the paired-channel `ltp_data_present_pair` slot is populated + /// while `common_window == false`; a `prediction_used[]` / + /// `long_used[]` length differs from the spec-cap + /// (`min(max_sfb, PRED_SFB_MAX[fs_index])` or + /// `min(max_sfb, MAX_LTP_LONG_SFB)`); or a numeric field + /// (`ltp_coef`, `ltp_lag`, `reset_group_number`) exceeds the + /// width of its wire slot. A conforming AAC encoder never builds + /// such a structure; this surfaces caller bugs at the boundary + /// between psychoacoustic / windowing-decision code and bitstream + /// emission. + IcsInfoEncodeInvalid, + + /// [`crate::section_data::SectionData::parse`] read a section + /// run-length (`sect_len`) that would extend a section past + /// `max_sfb`. ISO/IEC 13818-7 §6.3 Table 17 terminates the + /// per-group loop at `k < max_sfb`; a conforming encoder never + /// emits a `sect_len` that overshoots, so this signals a + /// malformed `section_data()`. + SectionDataOverrun, + + /// [`crate::section_data::SectionData::write`] was handed an + /// in-memory [`crate::section_data::SectionData`] whose + /// per-group section list violates an invariant the encoder + /// cannot represent on the wire — non-contiguous bands + /// (`start != 0`, `end[i] != start[i+1]`, or last `end != + /// max_sfb`), a `sect_cb` greater than the 4-bit field, or a + /// zero-length section that the §6.3 escape cannot terminate + /// while preserving parser round-trip. A conforming AAC encoder + /// never builds such a structure; this surfaces caller bugs at + /// the boundary between scalefactor-grouping and section + /// emission. + SectionDataEncodeInvalid, + + /// [`crate::pulse_data::PulseData::write`] was handed an + /// in-memory [`crate::pulse_data::PulseData`] whose field set + /// cannot be represented on the wire under ISO/IEC 14496-3 + /// §4.4.6.3 Table 4.7. Examples: `pulses` is empty (the loop + /// bound is `number_pulse + 1 >= 1`) or exceeds the 2-bit + /// `number_pulse` field cap (`pulses.len() > 4`); + /// `pulse_start_sfb > 0x3f` (6-bit overflow); a `Pulse::offset > + /// 0x1f` (5-bit overflow) or `Pulse::amp > 0x0f` (4-bit + /// overflow). A conforming AAC encoder never builds such a + /// structure; this surfaces caller bugs at the boundary between + /// the pulse-selection psychoacoustic stage and bitstream + /// emission. + PulseDataEncodeInvalid, + + /// [`crate::tns_data::TnsData::write`] was handed an in-memory + /// [`crate::tns_data::TnsData`] whose field combination cannot + /// be represented on the wire under ISO/IEC 14496-3 §4.4.6 / + /// Table 4.54 (with the §4.6.9.2 Table 4.155 size switch). + /// Examples: `windows.len()` differs from `num_windows` for the + /// surrounding `window_sequence` (1 for long sequences, 8 for + /// `EIGHT_SHORT_SEQUENCE`); per-window `filters.len()` exceeds + /// the `n_filt` field cap (1 on `EIGHT_SHORT_SEQUENCE`, 3 + /// otherwise); a filter's `length` exceeds the `length` field + /// cap (15 / 63); a filter's `order` exceeds the `order` field + /// cap (7 / 31); the `coef[]` length differs from `order`; a + /// coefficient magnitude exceeds the `(1 << coef_bits) - 1` + /// cap (where `coef_bits = (3 + coef_res) - coef_compress`); a + /// zero-`order` filter carries a non-default `direction` / + /// `coef_compress` that would silently be dropped on the wire + /// (those fields are not transmitted when `order == 0`). A + /// conforming AAC encoder never builds such a structure; this + /// surfaces caller bugs at the boundary between the TNS + /// psychoacoustic-decision stage and bitstream emission. + TnsDataEncodeInvalid, + + /// [`crate::scale_factor_data::ScaleFactorData::write`] was + /// handed an in-memory record set whose shape cannot be + /// represented on the wire under ISO/IEC 14496-3 §4.4.6 / + /// Table 4.53 (non-resilient branch). Examples: the outer + /// `entries.len()` does not match the supplied `sfb_cb.len()`; + /// a group's entry count differs from the non-`ZERO_HCB` band + /// count of the matching `sfb_cb` group; an entry variant + /// does not match its band's codebook classification + /// (e.g. [`crate::scale_factor_data::ScaleFactorEntry::Intensity`] + /// paired with a spectrum band, or + /// [`crate::scale_factor_data::ScaleFactorEntry::NoisePcm`] re-used + /// after the §4.4.6 frame-scope `noise_pcm_flag` has already + /// cleared, or + /// [`crate::scale_factor_data::ScaleFactorEntry::NoiseDpcm`] used + /// on the first PNS band of the frame); a DPCM delta falls + /// outside `-60..=+60` (Table 4.150); or a `NoisePcm` magnitude + /// exceeds the 9-bit field cap (`> 0x1ff`). A conforming AAC + /// encoder never builds such a structure; this surfaces caller + /// bugs at the boundary between the rate-allocation / + /// scalefactor-quantisation stage and bitstream emission. + ScaleFactorDataEncodeInvalid, + + /// An RVLC encode primitive ([`crate::rvlc::rvlc_encode`] / + /// [`crate::rvlc::rvlc_esc_encode`]) was handed a value outside + /// its codebook domain: a Table 4.166 RVLC delta outside + /// `-7..=+7`, or a Table 4.168 escape magnitude index outside + /// `0..=53` (ISO/IEC 14496-3 §4.6.16.2). A conforming + /// error-resilient encoder never builds such a value; this + /// surfaces caller bugs at the scalefactor-quantisation / + /// emission boundary. + RvlcEncodeInvalid, + + /// [`crate::rvlc::rvlc_decode`] read a Table 4.167 *asymmetric* + /// (forbidden) codeword from the error-resilient + /// `scale_factor_data()` RVLC part (ISO/IEC 14496-3 §4.6.16.2.1). + /// Because the RVLC code tree leaves some nodes unused, hitting + /// one is an in-band *error-detection* event — the stream's RVLC + /// scalefactor data is corrupt. + RvlcForbiddenCodeword, + + /// [`crate::rvlc::rvlc_esc_decode`] walked the full 20-bit + /// Table 4.168 RVLC-ESC depth without matching any codeword + /// (ISO/IEC 14496-3 §4.6.16.2). The escape part of the + /// error-resilient `scale_factor_data()` is corrupt. + RvlcEscInvalid, + + /// The error-resilient `scale_factor_data()` RVLC branch + /// (ISO/IEC 14496-3 Table 4.53 / §4.6.16.2) violated a + /// structural invariant: the decoded RVLC part did not consume + /// exactly `length_of_rvlc_sf` bits, the escape part did not + /// consume exactly `length_of_rvlc_escapes` bits, an escape was + /// signalled for a non-`ESC_FLAG` band, or an in-memory record + /// set handed to the writer cannot be represented (variant / + /// codebook mismatch, escape magnitude out of range, or the + /// `rev_global_gain` / DPCM-last seeds out of their field caps). + RvlcScaleFactorDataInvalid, + + /// [`crate::pce::Pce::write`] was handed an in-memory + /// [`crate::pce::Pce`] whose field combination cannot be + /// represented on the wire under ISO/IEC 14496-3 §4.4.1.1 / + /// Table 4.2. Examples: `element_instance_tag > 0x0f` (4-bit + /// field cap); `object_type > 0x03` (2-bit field cap); + /// `sampling_frequency_index > 0x0f` (4-bit field cap); + /// `front_elements.len() > 0x0f`, `side_elements.len() > 0x0f`, + /// or `back_elements.len() > 0x0f` (4-bit `num_*` field caps); + /// `lfe_element_tag_selects.len() > 0x03` (2-bit `num_lfe` + /// field cap); `assoc_data_tag_selects.len() > 0x07` (3-bit + /// `num_assoc` field cap); `valid_cc_elements.len() > 0x0f` + /// (4-bit `num_valid_cc` field cap); a `tag_select` inside any + /// per-element list exceeds the 4-bit cap; a `matrix_mixdown` + /// `idx > 0x03` (2-bit field cap); `mono_mixdown_element_number` + /// or `stereo_mixdown_element_number` `> 0x0f` (4-bit caps); or + /// `comment_field.len() > 0xff` (8-bit `comment_field_bytes` + /// length prefix). A conforming AAC encoder never builds such a + /// structure; this surfaces caller bugs at the boundary between + /// channel-layout selection and bitstream emission. + PceEncodeInvalid, + + /// [`crate::raw_data_block::FrameAssembler`] was handed an + /// element whose field combination cannot be represented on the + /// wire under ISO/IEC 14496-3 §4.4.2.1. Examples: + /// [`crate::raw_data_block::FrameAssembler::push_channel_header`] + /// was called with an `IdSynEle` other than `SCE` / `CPE` / `CCE` + /// / `LFE` (those have their own dedicated `push_*` entry points + /// because each carries a bespoke wire layout — FIL goes through + /// [`crate::raw_data_block::FrameAssembler::push_fill`], DSE + /// through + /// [`crate::raw_data_block::FrameAssembler::push_data`], END + /// through + /// [`crate::raw_data_block::FrameAssembler::push_end`], and PCE + /// has no writer yet); a channel-element `element_instance_tag` + /// or DSE `element_instance_tag` exceeds the 4-bit field cap + /// (`> 0x0f`); a FIL payload exceeds the 269-byte ceiling + /// (`15 + 255 − 1`) imposed by the §4.4.2.7 8-bit `esc_count` + /// field; a DSE payload exceeds the 510-byte ceiling + /// (`255 + 255`) imposed by the §4.4.2.5 8-bit `esc_count` + /// field; or + /// [`crate::raw_data_block::FrameAssembler::push_channel_body_bits`] + /// was called with `bit_count > bits.len() * 8`. Long fill / + /// data payloads (above the per-element ceilings) split + /// naturally across multiple back-to-back FIL / DSE elements + /// with the same `tag`; that splitting is the caller's + /// responsibility, not the assembler's. + RawDataBlockEncodeInvalid, + + /// `epConfig` (from the Table 1.15 outer `switch (audioObjectType)` + /// for the ER object types) selected value `2` or `3`, which + /// mandates parsing the trailing `ErrorProtectionSpecificConfig()` + /// body. Phase 1 does not parse the error-protection + /// configuration; the carried `u8` is the literal 2-bit + /// `epConfig` field value as read from the wire. `epConfig == 0` + /// (no EP) and `epConfig == 1` (EP defined by EP class mapping + /// table only — no trailing body) are accepted and surfaced via + /// [`crate::asc::AudioSpecificConfig::ep_config`]. + UnsupportedEpConfig(u8), + + /// An `ErrorProtectionSpecificConfig()` (§1.8.2.1 Table 1.49) + /// carries a reserved or inconsistent field: `interleave_type == + /// 3`, `number_of_concatenated_frame == 0` (Table 1.54), `fec_type + /// == 3`, an SRCPC `class_rate > 24`, a `class_crclen > 18`, a + /// width-28 intraclass `interleave_switch` on an RS class + /// (Table 1.64), or a `class_output_order` that is not a + /// permutation. + EpConfigInvalid, + + /// An EP-tool frame (`ep_frame()`, §1.8.2.2) violates its + /// configuration: a `choice_of_pred` beyond the expanded set + /// list, a class overrunning the frame, a failed class CRC, an + /// uncorrectable FEC codeword, or a malformed EPMuxElement / + /// EPAudioSyncStream carrier. + EpFrameInvalid, + + /// A scalable-AAC (§4.4.2.2 / §4.5.2.2) layer configuration or + /// per-layer payload violates a normative shape: an empty or + /// over-long layer list (one main + at most 7 extension layers, + /// §4.5.2.2.4), a mono layer following a stereo layer + /// (Table 4.87), a payload count that does not match the + /// configured layer count, a reserved `ms_mask_present == 3` + /// (§4.6.8.1.2), an LD frame family (the scalable object types + /// are defined over the 1024/960-line families only), or a + /// per-layer element that overruns its payload. + ScalableInvalid, + + /// The scalable configuration signals a non-AAC lower layer — + /// `dependsOnCoreCoder == 1` (a CELP core, §4.5.2.2.5) or a + /// TwinVQ layer (§4.5.2.2.6). This crate decodes the AAC-only + /// scalable combinations (§4.5.2.2.4); the CELP / TwinVQ + /// base-layer codecs belong to other subparts. + ScalableUnsupportedCore, + + /// An invalid per-band tool combination between two scalable + /// layers per Tables 4.91–4.93 (e.g. a plain-coded band followed + /// by a PNS band in the next layer, or an intensity band on top + /// of a plain-coded stereo band). + ScalableLayerCombination, + + /// `extensionFlag3` was set to `1` inside the `GASpecificConfig` + /// `extensionFlag` body (Table 4.1). ISO/IEC 14496-3:2009 reserves + /// the body behind this flag with the comment "tbd in version 3"; + /// since the body bit-layout is not defined, Phase 1 cannot + /// advance the bit-reader and rejects the ASC. + UnsupportedAscExtensionFlag3, + + /// The Table 1.15 trailing `syncExtensionType == 0x2b7` probe + /// resolved an `extensionAudioObjectType` whose body bit-layout + /// is not specified by ISO/IEC 14496-3:2009 §1.6.2.1. The carrier + /// only spells out two values: `5` (HE-AAC SBR with the optional + /// `0x548` PS sub-probe) and `22` (ER BSAC with mandatory + /// `extensionChannelConfiguration`); any other extension AOT + /// resolved by `GetAudioObjectType()` inside the probe surfaces + /// here. The carried `u8` is the resolved extension AOT. + UnsupportedTrailingExtensionAot(u8), + + /// `extension_payload()` dispatched on an `extension_type` value + /// whose body needs the SBR back-end this crate does not yet + /// provide. The carried `u8` is the literal 4-bit + /// `extension_type` value as read from the wire — one of + /// `0b1101` (`EXT_SBR_DATA`) or `0b1110` (`EXT_SBR_DATA_CRC`) + /// per ISO/IEC 13818-7 Table 40. + UnsupportedExtensionSbr(u8), + + /// `extension_payload()` dispatched on a reserved + /// `extension_type` value (any 4-bit value not in + /// `{0b0000, 0b0001, 0b1011, 0b1101, 0b1110}`). ISO/IEC + /// 14496-3 Table 4.59 and ISO/IEC 13818-7 Table 40 list these + /// values as "reserved"; this crate has no body layout to + /// advance the bit-reader by. + UnsupportedExtensionType(u8), + + /// [`crate::extension_payload::ExtensionPayload`] parse / write + /// hit a structural invariant violation: + /// + /// * The dispatching FIL `cnt` is 0 (no room for the 4-bit + /// `extension_type` field). + /// * For `EXT_FILL` (parser / writer): an `other_bits` byte + /// buffer whose length does not match the + /// `8 * (cnt - 1) + 4` body-bits ceiling. + /// * For `EXT_FILL_DATA` (parser): a `fill_nibble` that is not + /// normatively `0b0000`, or a `fill_byte` that is not + /// normatively `0b10100101`. + /// * For `EXT_DYNAMIC_RANGE` (parser): the Table 4.52 derived + /// byte count `n` disagrees with the dispatching FIL `cnt`. + /// * For `EXT_DYNAMIC_RANGE` (writer): a numeric field + /// overflows its Table 4.52 cap (`pce_instance_tag > 0x0f`, + /// `drc_tag_reserved_bits > 0x0f`, `drc_band_incr > 0x0f`, + /// `drc_bands_reserved_bits > 0x0f`, `prog_ref_level > + /// 0x7f`, `dyn_rng_ctl > 0x7f`), an internal + /// shape-mismatch (`band_top.len() != 1 + band_incr`, + /// `bands.len() != drc_num_bands`), or an + /// `excluded_channels.exclude_mask.len()` that is not a + /// positive multiple of 7 (Table 4.53 emits exclusion bits + /// in fixed groups of 7). + ExtensionPayloadInvalid, + + /// [`crate::gain_control_data::GainControlData::write`] was + /// handed an in-memory + /// [`crate::gain_control_data::GainControlData`] whose field + /// combination cannot be represented on the wire under ISO/IEC + /// 14496-3 §4.4.6.5 / Table 4.12. Examples: `max_band > 0x03` + /// (2-bit field cap); `bands.len() != max_band` (the outer + /// band-loop count must match the dispatched wire value); + /// `band.windows.len()` differs from the per-`window_sequence` + /// count (1 for `OnlyLong`, 2 for `LongStart` / `LongStop`, 8 for + /// `EightShort`); a per-`(bd, wd)` `adjustments.len() > 7` + /// (3-bit `adjust_num` field cap); a `GainAdjust::alevcode > + /// 0x0f` (4-bit field cap); or a `GainAdjust::aloccode` exceeds + /// the per-slot width-derived cap (5 bits for `OnlyLong wd=0`, + /// 4 bits for `LongStart / LongStop wd=0`, 2 bits for + /// `EightShort` and the `wd=1` slot of `LongStart`, 5 bits for + /// the `wd=1` slot of `LongStop`). A conforming AAC SSR encoder + /// never builds such a structure; this surfaces caller bugs at + /// the boundary between the SSR PQF gain-control psychoacoustic + /// stage and bitstream emission. + GainControlDataEncodeInvalid, + + /// [`crate::scale_factor_data::differentiate`] was handed an + /// [`crate::scale_factor_data::AbsoluteScaleFactors`] whose + /// shape or numeric values cannot be encoded back to a + /// well-formed `scale_factor_data()` block. Examples: outer + /// length differs from `sfb_cb.len()`; a group's + /// per-band-classification list differs from the matching + /// `sfb_cb` group; the spectrum-track delta `sf - last_sf` + /// falls outside Table 4.150's `-60..=+60`; the intensity-track + /// delta `is_pos - last_is` falls outside `-60..=+60`; the + /// PNS-track delta `nrg - last_nrg` (for PNS bands after the + /// first) falls outside `-60..=+60`; or the first PNS band's + /// initial seed magnitude (`first_nrg - (global_gain - + /// NOISE_OFFSET - 256)`) does not fit the 9-bit Table 4.53 + /// `dpcm_noise_nrg` uimsbf field (`0..=511`). A conforming AAC + /// rate-allocation stage never produces such a structure; this + /// surfaces caller bugs at the boundary between absolute + /// scalefactor quantisation and DPCM differential coding. + ScaleFactorAccumulatorInvalid, + + /// [`crate::spectral_codebook::table_4_95`] (or any other + /// public accessor in that module) was called with a `codebook` + /// value `> 31`. ISO/IEC 14496-3 Table 4.95 only defines rows + /// `0..=31`. + SpectralCodebookOutOfRange(u8), + + /// [`crate::spectral_codebook::decode_index_to_tuple`] / + /// [`crate::spectral_codebook::encode_tuple_to_index`] / + /// [`crate::spectral_codebook::apply_sign_bits`] / + /// [`crate::spectral_codebook::derive_sign_bits`] was called + /// with a codebook whose Table 4.95 row carries no + /// `unsigned_cb` / `dimension` / `lav` (`0`, `12`, `13`, `14`, + /// `15`). Those are non-spectral books (`ZERO_HCB`, reserved, + /// PNS, intensity stereo); §4.6.3.3 does not translate any + /// codeword index for them. + SpectralCodebookHasNoTuple(u8), + + /// [`crate::spectral_codebook::decode_index_to_tuple`] was + /// called with a codeword index `idx >= mod^dim` where `mod = + /// lav + 1` (unsigned) or `2 * lav + 1` (signed). A conforming + /// Huffman decoder never produces such an index; this surfaces + /// an incoherence between the Huffman tree and Table 4.95. + SpectralCodebookIndexOutOfRange(u8), + + /// [`crate::spectral_codebook::encode_tuple_to_index`] / + /// [`crate::spectral_codebook::derive_sign_bits`] was called + /// with a tuple shorter than the codebook's dimension, or with + /// an entry outside the codebook's representable range + /// (`0..=lav` unsigned, `-lav..=+lav` signed). A conforming AAC + /// encoder never produces such a tuple. + SpectralCodebookTupleOutOfRange(u8), + + /// [`crate::spectral_codebook::apply_sign_bits`] was called + /// with a `signs` slice whose length disagrees with the count + /// of non-zero coefficients in the unsigned-codebook tuple, or + /// with a non-empty `signs` slice on a signed codebook. + SpectralCodebookSignBitsMismatch(u8), + + /// [`crate::spectral_codebook::decode_esc_value`] / + /// [`crate::spectral_codebook::encode_esc_value`] was called + /// with arguments outside the §4.6.3.3 ESC range: `prefix_len > + /// 9`, `escape_word` not fitting `(prefix_len + 4)` bits, a + /// decoded value exceeding `MAX_QUANT` (`8191`), or an encoder + /// value `< 16` (which is in-band, not ESC-encoded). + SpectralCodebookEscOutOfRange, + + /// [`crate::tns_coef::tns_decode_coef`] / + /// [`crate::tns_coef::tns_encode_coef`] / + /// [`crate::tns_coef::iqfac`] / [`crate::tns_coef::iqfac_m`] / + /// [`crate::tns_coef::sign_extend_coef`] / + /// [`crate::tns_coef::pack_coef`] was called with an argument + /// outside the §4.6.9.3 / §C.6 legal range. Examples: + /// `coef_res_bits` not in `{3, 4}` (the spec's `coef_res[w] + 3` + /// envelope); `coef_compress > 1` (a 1-bit wire flag); a wire + /// `coef[i]` value that does not fit in `coef_res2 = + /// coef_res_bits - coef_compress` bits; a `pack_coef` `value` + /// outside `-(1 << (coef_res2-1))..=(1 << (coef_res2-1)) - 1`; + /// or an encode-side PARCOR coefficient `|r| > 1.0` (or NaN / + /// ±∞) — `arcsin` is undefined outside `[-1, 1]`. + TnsCoefOutOfRange, + + /// [`crate::tns_frame::tns_decode_frame`] was called with a + /// frame-level argument combination that violates the §4.6.9.3 + /// `tns_decode_frame()` preconditions: the `spec` buffer length + /// differs from `num_windows × window_len` (8 × 128 for + /// `EIGHT_SHORT_SEQUENCE`, 1 × 1024 otherwise); the + /// [`crate::tns_data::TnsData`] window count disagrees with the + /// `window_sequence`; or a filter's `coef` vector is shorter than + /// the `TNS_MAX_ORDER`-clamped `tns_order` it must supply. A + /// [`crate::tns_data::TnsData`] produced by + /// [`crate::tns_data::TnsData::parse`] under the same + /// `window_sequence` never trips the structural checks — this + /// surfaces caller-fabricated structures. + TnsFrameInvalid, + + /// [`crate::spectral_data::SpectralData::parse`] (or the + /// [`crate::spectral_data::sect_sfb_offset`] helper) found a + /// structural violation of Table 4.56 / §4.5.2.3.4: `max_sfb` + /// exceeding `num_swb` for the active window sequence, a + /// [`crate::section_data::SectionData`] whose group count + /// disagrees with the [`crate::ics_info::IcsInfo`], a section + /// carrying the reserved codebook 12 into `spectral_data()`, + /// or a section span that is not a whole number of + /// `QUAD_LEN` / `PAIR_LEN` n-tuples. + SpectralDataInvalid, + + /// [`crate::spectral_data::SpectralData::write`] was handed a + /// coefficient buffer that cannot be represented on the wire: + /// per-group buffer lengths disagreeing with + /// `window_group_length[g] × window_len`, a non-zero coefficient + /// inside a `ZERO_HCB` / `NOISE_HCB` / intensity section (or + /// above `max_sfb`), or a magnitude exceeding the section + /// codebook's LAV (`MAX_QUANT` = 8191 for the ESC book). + SpectralDataEncodeInvalid, + + /// [`crate::dequant::rescale_spectrum`] found a structural + /// mismatch between its inputs: group counts disagreeing with + /// `num_window_groups`, a per-group `x_quant` buffer length + /// disagreeing with the `ics_info` grouping, or an + /// [`crate::scale_factor_data::AbsoluteScaleFactorEntry`] + /// sequence that does not match the non-`ZERO_HCB` codebook + /// classification of `sfb_cb` (including the reserved codebook + /// 12, which has no spectrum semantics to rescale). Inputs + /// produced by the wire parsers plus + /// [`crate::scale_factor_data::accumulate`] under one shared + /// `ics_info` / `section_data` never trip this — it surfaces + /// caller-fabricated structures. + DequantInvalid, + + /// [`crate::decoded_spectrum::quant_to_spec`] was handed a group + /// buffer set whose shape disagrees with the `ics_info` + /// grouping: wrong group count, a group buffer length that is + /// not `window_group_length[g] × window_len`, or a + /// `window_group_length[]` whose sum is not `num_windows`. + QuantToSpecInvalid, + + /// [`crate::filterbank::Filterbank::synthesize`] was handed a + /// window-major spectrum whose length disagrees with the + /// [`crate::ics_info::IcsInfo`] `window_sequence`: a long + /// sequence (`ONLY_LONG` / `LONG_START` / `LONG_STOP`) requires + /// exactly [`crate::swb_offset::LONG_WINDOW_LEN`] (1024) + /// coefficients, an `EIGHT_SHORT` sequence requires `8 ×` + /// [`crate::swb_offset::SHORT_WINDOW_LEN`] (1024 total). The + /// §4.6.11.3.1 IMDCT cannot run against any other length. + FilterbankInvalid, + + /// [`crate::ms_stereo::apply_ms_stereo`] was handed a channel + /// pair whose shapes disagree with the shared + /// [`crate::ics_info::IcsInfo`]: the two window-major spectra + /// have different lengths, a length that is not + /// `num_windows × window_len`, an `ms_used` mask whose group + /// count is not `num_window_groups` (or a per-group row shorter + /// than `max_sfb`), or a per-channel `sfb_cb` whose group/band + /// extents do not cover `max_sfb`. The §4.6.8.1.3 de-matrix is + /// undefined without a consistent group/band geometry across + /// both channels. + MsStereoInvalid, + /// [`crate::intensity_stereo::apply_intensity_stereo`] was handed a + /// channel pair whose shapes disagree with the shared + /// [`crate::ics_info::IcsInfo`]: the two window-major spectra have + /// different lengths, a length that is not + /// `num_windows × window_len`, an `ms_used` mask whose group count + /// is not `num_window_groups` (or a per-group row shorter than + /// `max_sfb`), a right-channel `sfb_cb` that does not cover + /// `max_sfb`, or an `is_pos[g][sfb]` table whose group/band extents + /// do not cover every intensity-coded band. The §4.6.8.2.3 scale + /// `is_intensity · invert_intensity · 0.5^(0.25·is_pos)` is + /// undefined without a consistent group/band geometry and an + /// intensity-stereo position for every intensity band. + IntensityStereoInvalid, + /// [`crate::pns::apply_pns`] / [`crate::pns::apply_pns_pair`] was + /// handed a channel (or pair) whose shapes disagree with the + /// [`crate::ics_info::IcsInfo`]: a window-major spectrum whose + /// length is not `num_windows × window_len`, a + /// `window_group_length` whose sum is not `num_windows`, a + /// `max_sfb` beyond the active window's band count, a `sfb_cb` or + /// `noise_nrg` table whose group/band extents do not cover + /// `max_sfb`, two paired channels with differing window geometry, + /// or (for the pair) an `ms_used` mask whose group count is not + /// `num_window_groups` (or a per-group row shorter than `max_sfb`). + /// The §4.6.13.3 noise synthesis is undefined without a consistent + /// group/band geometry and a `noise_nrg` for every noise band. + PnsInvalid, + /// [`crate::ltp::LtpState::apply_long`] was handed §4.6.7 Long-Term + /// Prediction inputs that are mutually inconsistent: an `ltp_coef` + /// index outside the Table 4.98 codebook (`> 7`), an active + /// `ltp_long_used` mask with no transmitted `ltp_lag`, or a channel + /// spectrum whose length is not `LONG_WINDOW_LEN` (1024). The + /// §4.6.7.3 `X_rec = X_est + Y_rec` combination is undefined without + /// a valid predictor coefficient, lag, and long-window spectrum. + LtpInvalid, + /// [`crate::element_decode`] was asked to decode a channel element + /// whose component shapes are mutually inconsistent: a channel-pair + /// element (`CPE`) whose two channels disagree on `window_sequence` + /// (so the shared `common_window` geometry the §4.6.8 joint-stereo + /// tools require is violated), an `ms_used` row that does not cover + /// `num_window_groups × max_sfb`, or a per-channel + /// `AbsoluteScaleFactors` whose wire-order record count does not + /// match its `sfb_cb` non-`ZERO_HCB` band count when expanded to the + /// band-indexed `is_pos[g][sfb]` / `noise_nrg[g][sfb]` layout the + /// §4.6.8.2 / §4.6.13 synthesis passes consume. The element-level + /// §4.6 block-order chain (de-quantise → M/S → intensity → PNS → + /// TNS → filterbank) cannot run without a consistent geometry across + /// the composed stages. + ElementDecodeInvalid, + /// [`crate::predictor::PredictorBank`] was handed §4.6.6 + /// frequency-domain-prediction inputs that are mutually inconsistent: + /// a long-window scalefactor-band offset table too short to cover + /// `PRED_SFB_MAX` for the sampling rate, a reconstructed spectrum + /// shorter than the per-line predictor bank, or a + /// `predictor_reset_group_number` outside the Table 4.97 range + /// (`1 ..= 30`; the values `0` and `31` are reserved). The + /// §4.6.6.3.2.1 `x_rec = x_est + y_rec` reconstruction and the + /// §4.6.6.3.3 reset are undefined without a full predictor bank and a + /// valid reset group. + PredictorInvalid, + /// SBR frequency-band-table derivation + /// ([`crate::sbr_freq_bands`], §4.6.18.3.2) was handed parameters + /// that violate a normative constraint: + /// + /// * `bs_start_freq` / `bs_stop_freq` outside their 4-bit ranges + /// (`0 ..= 15` each), an unsupported `FsSBR` (no offset / + /// `startMin` / `stopMin` row in §4.6.18.3.2.1), or + /// `bs_freq_scale` / `bs_alter_scale` / `bs_noise_bands` + /// outside their signalled ranges. + /// * A derived geometry that breaks a §4.6.18.3.6 requirement: + /// `k2 <= k0` (`fMaster` undefined), `numBands <= 0`, + /// `k2 - k0` over the per-rate subband-count cap, `k_x > 32`, + /// `k_x + M > 64`, or `bs_xover_band >= NMaster`. + /// + /// The §4.6.18.3.2.1 master table and the §4.6.18.3.2.2 derived + /// high / low / noise tables are undefined for such inputs. + SbrFreqBandInvalid, + /// SBR envelope / noise Huffman decode ([`crate::sbr_huffman`], + /// §4.A.6.1 `sbr_huff_dec()`) could not match a codeword: either no + /// table entry matched within the maximum SBR codeword length, or + /// the bitstream ran out before a codeword completed. Both signal a + /// corrupt or truncated SBR extension payload. + SbrHuffInvalid, + /// Parametric Stereo `ps_data()` parse ([`crate::ps_data`] / + /// [`crate::ps_huffman`], ISO/IEC 14496-3:2009 §8.4.2 Table 8.9): + /// a PS Huffman codeword failed to match within the Annex 8.B + /// maximum length, the bitstream ran out mid-element, a reserved + /// `iid_mode` / `icc_mode` was signalled, or a differentially + /// decoded IID/ICC index left its Table 8.24/8.27 range. All + /// signal a corrupt or truncated PS extension payload. + PsDataInvalid, + /// SBR time-frequency grid parse ([`crate::sbr_grid`], §4.4.2.8 + /// Tables 4.69–4.71) failed: the bitstream ran out mid-grid, or a + /// frame class signalled an envelope count outside the + /// §4.6.18.3.6 limit ([`crate::sbr_grid::SBR_MAX_NUM_ENV`]). Both + /// signal a corrupt SBR data element. + SbrGridInvalid, + /// SBR QMF filterbank ([`crate::sbr_qmf`], §4.6.18.4) was handed a + /// slot buffer of the wrong length: the analysis bank consumes + /// exactly 32 time samples per slot, the synthesis bank exactly 64 + /// complex subband samples (32 for the downsampled variant). + SbrQmfInvalid, + /// The §4.6.18.8 low-power SBR tool operates on real-valued + /// subband signals, so the subpart-8 Parametric Stereo tool — + /// whose de-correlation and phase parameters need the + /// complex-valued QMF domain — cannot run on top of it. Decode + /// HE-AAC v2 streams with the high-quality (complex) SBR mode. + SbrLowPowerPs, + /// Integer-PCM rendering ([`crate::pcm`], §4.6.11 output → + /// §1.3 `NINT()`-rounded 16-bit word) was handed per-channel time + /// signals of disagreeing length. [`crate::pcm::interleave_s16`] + /// requires every channel buffer to carry the same per-frame sample + /// count (the §4.6.11 transform length) so the interleave is + /// well-defined. + PcmInvalid, + /// LATM `StreamMuxConfig()` ([`crate::latm`], ISO/IEC 14496-3 + /// §1.7.3 Table 1.42) signalled `audioMuxVersion == 1` with + /// `audioMuxVersionA == 1`, which the spec marks reserved-for- + /// future-extensions (`/* tbd */`). No syntax is defined for that + /// branch, so the multiplex cannot be parsed. + LatmAudioMuxVersionAReserved, + /// LATM `StreamMuxConfig()` ([`crate::latm`], §1.7.3 Table 1.42) + /// signalled a per-layer `frameLengthType` this decoder does not + /// carry payload framing for. Only `0` (variable-length, byte + /// count in `PayloadLengthInfo()`) and `1` (fixed `frameLength` + /// bits) are supported; the CELP (`3`/`4`/`5`) and HVXC + /// (`6`/`7`) types index frame-length tables this AAC-focused + /// decoder does not implement. Carries the offending value. + LatmUnsupportedFrameLengthType(u8), + /// LATM multiplex configuration ([`crate::latm`], §1.7.3) exceeded + /// one of the spec signalling caps: `numProgram > 15`, + /// `numLayer > 7`, `numChunk > 15`, `streamCnt > 15`, or + /// `numSubFrames` produced more PayloadMux frames than the bound. + /// The fields are bit-limited on the wire so this only fires on a + /// derived-count overflow or an internal inconsistency. + LatmConfigOutOfRange, + /// LATM `AudioMuxElement()` ([`crate::latm`], §1.7.3 Table 1.41) + /// with `muxConfigPresent == 1` set `useSameStreamMux == 1` (apply + /// previous configuration) but no `StreamMuxConfig()` had been + /// decoded yet on this stream. The first in-band element must + /// carry the configuration. + LatmNoPreviousMuxConfig, + /// LATM transport ([`crate::latm`], §1.7.3 Table 1.42) carried a + /// `crcCheckSum` whose recomputed §1.8.4.5 `CRC8` value did not + /// match the transmitted byte, indicating a corrupt + /// `StreamMuxConfig()`. + LatmCrcMismatch, + /// LOAS `AudioSyncStream()` / `EPAudioSyncStream()` + /// ([`crate::latm`], §1.7.2 Tables 1.36 / 1.37) sync search failed: + /// the `0x2B7` / `0x4DE1` syncword was not found, or the + /// `audioMuxLengthBytes` payload ran past the end of the buffer. + LoasSyncInvalid, + /// **No longer emitted.** A LATM/LOAS `AudioSpecificConfig` that + /// signals SBR ([`crate::latm::LoasDecoder`]) now decodes through + /// the shared §4.6.18 SBR back-end instead of being pre-rejected + /// (a PS-signalling stream decodes its HE-AAC v1 layer). The + /// variant is kept so existing `match` arms stay valid. + LatmSbrUnsupported, + /// `coupling_channel_element()` parse / reconstruction + /// ([`crate::cce`], ISO/IEC 14496-3 §4.6.8.3 / Table 4.8) was handed + /// a structurally inconsistent CCE: + /// + /// * a `num_coupled_elements` / `cc_target_is_cpe` / `cc_l` / `cc_r` + /// combination that derives a `num_gain_element_lists` other than + /// the count of transmitted gain lists, + /// * an `ind_sw_cce_flag == 1` (independently switched) element that + /// carries a per-band `dpcm_gain_element` list instead of the + /// §4.6.8.3.3-required single `common_gain_element` per target, or + /// * a coupled-target geometry (`num_window_groups` / `max_sfb` / + /// `swb_offset`) whose gain list does not cover the embedded + /// `single_channel_element()`'s band layout. + /// + /// The §4.6.8.3.3 `couple_channel()` scaling-and-add is undefined for + /// such inputs. + CceInvalid, + + /// An ADTS frame with `protection_absent == 0` carried a + /// `crc_check` (or, in the multi-raw-data-block form, an + /// `adts_header_error_check()` / `adts_raw_data_block_error_check()` + /// field) that does not match the CRC recomputed over the + /// ISO/IEC 13818-7:2004 §8.1.1.1 protected-bit region with the + /// ISO/IEC 11172-3 §2.4.3.1 code (16 bits, generator `0x8005`, + /// all-ones init). The protected header / element bits are + /// corrupt. + AdtsCrcMismatch, + + /// An `EXT_SBR_DATA_CRC` fill extension carried a + /// `bs_sbr_crc_bits` value that does not match the 10-bit CRC + /// (generator `G10 = x¹⁰+x⁹+x⁵+x⁴+x+1`, zero init — ISO/IEC + /// 14496-3:2009 §4.4.2.8.1) recomputed over the + /// `sbr_extension_data()` payload bits after the CRC field + /// (Table 4.62, `num_sbr_bits − 10` bits before `bs_fill_bits`). + /// The SBR side info is corrupt. + SbrCrcMismatch, + + /// A `bsac_header()` / `general_header()` field is out of its + /// legal range (ISO/IEC 14496-3:2009 §4.5.2.6.2.2.4/5): a + /// `cband_si_type` past Table 4.A.31, a `max_sfb` past the + /// §4.5.4 band table, a zero base-layer coverage, or a + /// `frame_length` too small for the headers. + BsacInvalidHeader, + + /// The arithmetic-decoded BSAC side information violates a + /// normative bound (§4.6.4.5 "bit_error_is_generated"): a + /// `cband_si` above the Table 4.A.31 largest value, or a + /// stereo / noise decision outside its model. + BsacBitError, + + /// The `bsac_raw_data_block()` uses a tool this decoder does + /// not implement yet (long-term prediction, or the extended + /// part's channel / SBR / SAC extensions). + BsacUnsupportedTool, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::NotImplemented => { + write!(f, "oxideav-aac: feature not implemented in Phase 1") + } + Error::AdtsSyncNotFound => { + write!(f, "ADTS sync word (0xFFF) not found") + } + Error::AdtsLayerNonZero => { + write!(f, "ADTS layer field must be 0") + } + Error::AdtsReservedSampleRateIndex => { + write!( + f, + "ADTS sampling_frequency_index is reserved (13, 14, or 15)" + ) + } + Error::AdtsFrameLengthTooSmall => { + write!(f, "ADTS aac_frame_length is smaller than the header") + } + Error::AdtsEncodeInvalid => { + write!( + f, + "ADTS header field exceeds its wire width or violates a normative constraint" + ) + } + Error::EncoderInvalidConfig => { + write!(f, "AAC encoder configuration or input slice is invalid") + } + Error::EncoderFrameOverflow => { + write!( + f, + "encoded AAC frame exceeds the 13-bit aac_frame_length ceiling" + ) + } + Error::UnexpectedEnd => { + write!(f, "unexpected end of bitstream") + } + Error::UnsupportedElementSkip(id) => { + write!( + f, + "raw_data_block walker cannot advance past id_syn_ele {} in Phase 1", + id + ) + } + Error::UnsupportedAot(aot) => { + write!( + f, + "AudioSpecificConfig audioObjectType {} is not handled in Phase 1", + aot + ) + } + Error::IcsInfoUnsupportedSampleRateIndex(idx) => { + write!( + f, + "ics_info sampling_frequency_index {} is outside the 0..=11 SWB-table range", + idx + ) + } + Error::SbrUnsupportedFrameFamily => { + write!( + f, + "SBR extension on a non-1024-line frame family: the §4.6.18 tool covers the 1024-line core only" + ) + } + Error::LdShortWindow => { + write!( + f, + "ER AAC LD: the 512/480-line families are long-only (§4.6.17.2.2) — no short-window geometry exists" + ) + } + Error::IcsInfoEncodeInvalid => { + write!( + f, + "ics_info encode: in-memory IcsInfo violates a Table 4.6 / 4.55 wire-field invariant" + ) + } + Error::SectionDataOverrun => { + write!( + f, + "section_data sect_len overruns max_sfb (malformed bitstream)" + ) + } + Error::SectionDataEncodeInvalid => { + write!( + f, + "section_data encode: per-group sections must be contiguous [0, max_sfb), sect_cb < 16, sect_len > 0" + ) + } + Error::PulseDataEncodeInvalid => { + write!( + f, + "pulse_data encode: pulses.len() in 1..=4, pulse_start_sfb < 64, pulse_offset < 32, pulse_amp < 16" + ) + } + Error::TnsDataEncodeInvalid => { + write!( + f, + "tns_data encode: in-memory TnsData violates a Table 4.54 / 4.155 wire-field invariant" + ) + } + Error::ScaleFactorDataEncodeInvalid => { + write!( + f, + "scale_factor_data encode: in-memory record set violates a Table 4.53 / 4.150 wire-field invariant" + ) + } + Error::RvlcEncodeInvalid => { + write!( + f, + "rvlc encode: value outside the Table 4.166 (-7..=+7) / Table 4.168 (0..=53) codebook domain" + ) + } + Error::RvlcForbiddenCodeword => { + write!( + f, + "rvlc decode: read a Table 4.167 asymmetric (forbidden) codeword — RVLC scalefactor data is corrupt (§4.6.16.2.1)" + ) + } + Error::RvlcEscInvalid => { + write!( + f, + "rvlc-esc decode: 20-bit Table 4.168 walk matched no codeword — RVLC escape data is corrupt (§4.6.16.2)" + ) + } + Error::RvlcScaleFactorDataInvalid => { + write!( + f, + "error-resilient scale_factor_data: RVLC branch violates a Table 4.53 / §4.6.16.2 structural invariant" + ) + } + Error::PceEncodeInvalid => { + write!( + f, + "pce encode: in-memory Pce violates a Table 4.2 wire-field invariant" + ) + } + Error::RawDataBlockEncodeInvalid => { + write!( + f, + "raw_data_block encode: element field violates a §4.4.2.1 / §4.4.2.5 / §4.4.2.7 wire-field invariant" + ) + } + Error::GainControlDataEncodeInvalid => { + write!( + f, + "gain_control_data encode: in-memory GainControlData violates a Table 4.12 wire-field invariant" + ) + } + Error::ScaleFactorAccumulatorInvalid => { + write!( + f, + "scale_factor accumulator: absolute-to-DPCM differentiation produced a delta outside Table 4.150 / Table 4.53 ranges" + ) + } + Error::UnsupportedEpConfig(value) => { + write!( + f, + "AudioSpecificConfig epConfig {} requires ErrorProtectionSpecificConfig parsing (Phase 1 supports only epConfig 0 and 1)", + value + ) + } + Error::EpConfigInvalid => { + write!( + f, + "ErrorProtectionSpecificConfig: reserved or inconsistent field (Table 1.49 / 1.54 / 1.64)" + ) + } + Error::EpFrameInvalid => { + write!( + f, + "EP-tool frame violates its configuration (ep_frame() vs ErrorProtectionSpecificConfig)" + ) + } + Error::ScalableInvalid => { + write!( + f, + "scalable AAC: layer configuration or per-layer payload violates the §4.4.2.2 / §4.5.2.2 shape" + ) + } + Error::ScalableUnsupportedCore => { + write!( + f, + "scalable AAC: CELP core / TwinVQ lower layers are out of scope (AAC-only combinations per §4.5.2.2.4)" + ) + } + Error::ScalableLayerCombination => { + write!( + f, + "scalable AAC: invalid per-band tool combination between layers (Tables 4.91-4.93)" + ) + } + Error::UnsupportedAscExtensionFlag3 => { + write!( + f, + "GASpecificConfig extensionFlag3 body is reserved (\"tbd in version 3\") and cannot be parsed" + ) + } + Error::UnsupportedTrailingExtensionAot(aot) => { + write!( + f, + "AudioSpecificConfig trailing syncExtensionType=0x2b7 probe resolved extensionAudioObjectType {} (only 5 and 22 have a Table 1.15 body)", + aot + ) + } + Error::UnsupportedExtensionSbr(value) => { + write!( + f, + "extension_payload extension_type 0x{:x} selects EXT_SBR_DATA / EXT_SBR_DATA_CRC; SBR back-end is not implemented", + value + ) + } + Error::UnsupportedExtensionType(value) => { + write!( + f, + "extension_payload extension_type 0x{:x} is reserved (no body layout defined)", + value + ) + } + Error::ExtensionPayloadInvalid => { + write!( + f, + "extension_payload: Table 4.51 / 4.52 / 4.53 / 4.59 wire-field invariant violated" + ) + } + Error::SpectralCodebookOutOfRange(cb) => { + write!( + f, + "spectral codebook {} is outside Table 4.95 (legal range 0..=31)", + cb + ) + } + Error::SpectralCodebookHasNoTuple(cb) => { + write!( + f, + "spectral codebook {} is non-spectral (Table 4.95 row carries no dim / lav)", + cb + ) + } + Error::SpectralCodebookIndexOutOfRange(cb) => { + write!( + f, + "spectral codebook {}: codeword index out of Table 4.95 range", + cb + ) + } + Error::SpectralCodebookTupleOutOfRange(cb) => { + write!( + f, + "spectral codebook {}: tuple length or value outside Table 4.95 dimension / lav", + cb + ) + } + Error::SpectralCodebookSignBitsMismatch(cb) => { + write!( + f, + "spectral codebook {}: sign-bit count disagrees with non-zero coefficients in tuple", + cb + ) + } + Error::SpectralCodebookEscOutOfRange => { + write!( + f, + "spectral codebook 11/16..=31 ESC sequence: prefix_len, escape_word, or magnitude outside §4.6.3.3 range" + ) + } + Error::TnsCoefOutOfRange => { + write!( + f, + "tns_coef: coef_res_bits / coef_compress / wire coef / PARCOR value outside §4.6.9.3 / §C.6 legal range" + ) + } + Error::TnsFrameInvalid => { + write!( + f, + "tns_decode_frame: spec length, TnsData window count, or per-filter coef length violates a §4.6.9.3 precondition" + ) + } + Error::SpectralDataInvalid => { + write!( + f, + "spectral_data: max_sfb / section layout / codebook violates a Table 4.56 or §4.5.2.3.4 structural constraint" + ) + } + Error::SpectralDataEncodeInvalid => { + write!( + f, + "spectral_data encode: coefficient buffer shape, zero-section content, or magnitude range cannot be represented per Table 4.56" + ) + } + Error::DequantInvalid => { + write!( + f, + "rescale_spectrum: x_quant / scalefactor-entry / sfb_cb layout violates a §4.6.1.3 / §4.6.2.3.3 precondition" + ) + } + Error::QuantToSpecInvalid => { + write!( + f, + "quant_to_spec: group buffer shape disagrees with the §4.5.2.3.4 ics_info grouping" + ) + } + Error::FilterbankInvalid => { + write!( + f, + "filterbank: window-major spectrum length disagrees with the §4.6.11 window_sequence" + ) + } + Error::MsStereoInvalid => { + write!( + f, + "M/S stereo: channel-pair spectra / ms_used / sfb_cb shapes disagree with the §4.6.8.1 ics_info geometry" + ) + } + Error::IntensityStereoInvalid => { + write!( + f, + "intensity stereo: channel-pair spectra / ms_used / right sfb_cb / is_pos shapes disagree with the §4.6.8.2 ics_info geometry" + ) + } + Error::PnsInvalid => { + write!( + f, + "PNS: channel spectrum / sfb_cb / noise_nrg / ms_used shapes disagree with the §4.6.13 ics_info geometry" + ) + } + Error::LtpInvalid => { + write!( + f, + "LTP: ltp_coef index, ltp_lag presence, or long-window spectrum length disagree with the §4.6.7 decoding process" + ) + } + Error::ElementDecodeInvalid => { + write!( + f, + "element decode: channel-element component shapes (window_sequence pairing, ms_used extent, or scalefactor-record count) are mutually inconsistent for the §4.6 block-order chain" + ) + } + Error::PredictorInvalid => { + write!( + f, + "predictor: long-window offset table, spectrum length, or reset-group number disagree with the §4.6.6 frequency-domain prediction process" + ) + } + Error::SbrFreqBandInvalid => { + write!( + f, + "SBR frequency bands: bs_start_freq/bs_stop_freq/bs_freq_scale, FsSBR, or the derived k0/k2 geometry violate a §4.6.18.3.2 / §4.6.18.3.6 constraint" + ) + } + Error::SbrHuffInvalid => { + write!( + f, + "SBR Huffman decode: no §4.A.6.1 codeword matched (corrupt or truncated SBR envelope/noise payload)" + ) + } + Error::PsDataInvalid => { + write!( + f, + "PS ps_data(): §8.4.2 Table 8.9 parse failed (unmatched Annex 8.B codeword, truncated payload, reserved iid/icc mode, or out-of-range index)" + ) + } + Error::SbrGridInvalid => { + write!( + f, + "SBR grid: §4.4.2.8 sbr_grid/sbr_dtdf/sbr_invf ran out of bits or signalled an out-of-range envelope count" + ) + } + Error::SbrQmfInvalid => { + write!( + f, + "SBR QMF: §4.6.18.4 filterbank slot buffer has the wrong length (analysis takes 32 samples, synthesis 64 complex bands, downsampled 32)" + ) + } + Error::SbrLowPowerPs => { + write!( + f, + "SBR low power: the §4.6.18.8 real-valued tool cannot carry the complex-domain subpart-8 PS tool; use the high-quality SBR mode for HE-AAC v2" + ) + } + Error::PcmInvalid => { + write!( + f, + "PCM interleave: per-channel time signals disagree in length" + ) + } + Error::LatmAudioMuxVersionAReserved => { + write!( + f, + "LATM StreamMuxConfig: audioMuxVersionA == 1 is reserved (§1.7.3 Table 1.42 /* tbd */ branch)" + ) + } + Error::LatmUnsupportedFrameLengthType(t) => { + write!( + f, + "LATM StreamMuxConfig: frameLengthType {t} (CELP/HVXC table-indexed framing) is unsupported; only 0 and 1 are carried" + ) + } + Error::LatmConfigOutOfRange => { + write!( + f, + "LATM StreamMuxConfig: a multiplex count (numProgram/numLayer/numChunk/streamCnt/numSubFrames) exceeded the §1.7.3 signalling cap" + ) + } + Error::LatmNoPreviousMuxConfig => { + write!( + f, + "LATM AudioMuxElement: useSameStreamMux == 1 but no previous StreamMuxConfig() has been decoded" + ) + } + Error::LatmCrcMismatch => { + write!( + f, + "LATM StreamMuxConfig: recomputed §1.8.4.5 CRC8 does not match the transmitted crcCheckSum" + ) + } + Error::LoasSyncInvalid => { + write!( + f, + "LOAS AudioSyncStream: §1.7.2 0x2B7/0x4DE1 syncword not found or audioMuxLengthBytes overruns the buffer" + ) + } + Error::LatmSbrUnsupported => { + write!( + f, + "LATM AudioSpecificConfig signalled SBR/PS, which the core LATM PCM driver does not decode" + ) + } + Error::CceInvalid => { + write!( + f, + "coupling_channel_element() has an inconsistent gain-list / target geometry (§4.6.8.3)" + ) + } + Error::AdtsCrcMismatch => { + write!( + f, + "ADTS crc_check mismatch: recomputed §8.1.1.1-region CRC-16 disagrees with the transmitted value" + ) + } + Error::SbrCrcMismatch => { + write!( + f, + "SBR bs_sbr_crc_bits mismatch: recomputed §4.4.2.8.1 CRC-10 disagrees with the transmitted value" + ) + } + Error::BsacInvalidHeader => { + write!( + f, + "bsac_header()/general_header() field out of range (§4.5.2.6.2.2.4)" + ) + } + Error::BsacBitError => { + write!( + f, + "BSAC arithmetic side info violates a normative bound (§4.6.4.5 bit error)" + ) + } + Error::BsacUnsupportedTool => { + write!( + f, + "bsac_raw_data_block() uses a tool this decoder does not implement (LTP / extended part)" + ) + } + } + } +} + +impl std::error::Error for Error {} diff --git a/crates/vendor/oxideav-aac/src/extension_payload.rs b/crates/vendor/oxideav-aac/src/extension_payload.rs new file mode 100644 index 00000000..86311f86 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/extension_payload.rs @@ -0,0 +1,856 @@ +//! `extension_payload()` parser + encoder primitive — ISO/IEC +//! 14496-3 §4.4.2.7 / Table 4.51 plus the DRC +//! `dynamic_range_info()` body (Table 4.52) and the +//! `excluded_channels()` helper (Table 4.53), with +//! `extension_type` values per Table 4.59 (and ISO/IEC 13818-7 +//! Table 40, which extends the 14496-3 table with the SBR-data +//! values). +//! +//! `extension_payload()` is the structured body inside a FIL +//! element (`fill_element()`). The outer FIL surfaces a byte +//! count `cnt`; the `extension_payload(cnt)` reads exactly `cnt` +//! bytes — the first 4 bits select an `extension_type`, the +//! remaining bits carry the type-specific body. Three of the four +//! well-known `extension_type` values have fully fixed-width +//! Table 4.51 / 4.52 layouts and are implemented here: +//! +//! * `EXT_FILL` (`0b0000`) — bitstream filler. Body is +//! `8 * (cnt - 1) + 4` `other_bits`. No normative value +//! constraint per Table 4.51's `default` branch. +//! * `EXT_FILL_DATA` (`0b0001`) — bitstream data as filler. +//! Body is a 4-bit `fill_nibble` (normatively `0b0000`) +//! followed by `cnt - 1` × 8-bit `fill_byte` (each normatively +//! `0b10100101`). +//! * `EXT_DYNAMIC_RANGE` (`0b1011`) — dynamic range control. +//! Body is the Table 4.52 `dynamic_range_info()` block (see +//! [`DynamicRangeInfo`]). +//! +//! The SBR-data extension types defined by ISO/IEC 13818-7 Table 40 +//! are surfaced as [`Error::UnsupportedExtensionSbr`] by the default +//! [`ExtensionPayload::parse`] (so the byte-exact AAC-LC decode path +//! stays untouched). The dedicated [`ExtensionPayload::parse_with_sbr`] +//! entry instead routes them into the §4.4.2.8 +//! [`crate::sbr_extension::SbrExtensionData`] side-info walker (the SBR +//! back-end DSP is still not applied): +//! +//! * `EXT_SBR_DATA` (`0b1101`). +//! * `EXT_SBR_DATA_CRC` (`0b1110`). +//! +//! All other (reserved) values surface as +//! [`Error::UnsupportedExtensionType`] carrying the literal 4-bit +//! value as read from the wire. +//! +//! ## Why a parser / writer pair, and why now +//! +//! The Phase 1 `raw_data_block()` walker (round 121) recognises +//! FIL but skips its payload bytes opaque. Round 160's +//! `FrameAssembler::push_fill` accepts an opaque payload byte +//! slice. Neither side decodes or encodes the structured +//! `extension_payload()` body — and the FIL element is where the +//! DRC metadata (per-band gain factors), encoder-identifier fill +//! bytes, and the SBR enhancement bytes ride. This module is +//! the §4.4.2.7 wire-level decode/encode for the three non-SBR +//! extension types whose body layouts are fully specified by +//! fixed-width fields (no Huffman, no spectral context). The +//! intent is that downstream rounds plug this module into +//! `FrameAssembler::push_fill` / +//! `Walker::next_element` to surface a typed `extension_payload` +//! per FIL element. +//! +//! ## Returned byte count +//! +//! Per Table 4.51, `extension_payload()` returns the byte count +//! it consumed. Table 4.52's `dynamic_range_info()` returns its +//! own byte count starting from `n = 1` (the leading byte +//! containing the 4-bit `extension_type` nibble plus four of the +//! body's "presence" flags); each subsequent 8-bit-wide field set +//! is `n++`. [`ExtensionPayload::parse`] and [`ExtensionPayload::write`] +//! both expose this byte count via the returned +//! [`ExtensionPayload::bytes_consumed`] / [`ExtensionPayload::byte_length`] +//! accessors. +//! +//! ## What this module does *not* cover +//! +//! * No application of the DRC `(dyn_rng_sgn, dyn_rng_ctl)` gain +//! factors to the reconstructed audio. §4.5.2.13 specifies the +//! companding curve; this module surfaces the raw fields only. +//! * No semantic validation of `pce_instance_tag` against the +//! surrounding PCE (the surrounding PCE may not be known at +//! parse time — e.g. when the DRC FIL precedes the PCE in +//! independent-program multiplexes). +//! * The SBR-data extension types (Table 40 +//! `EXT_SBR_DATA` / `EXT_SBR_DATA_CRC`) are surfaced as +//! [`Error::UnsupportedExtensionSbr`] — their bodies are the +//! `sbr_extension_data()` syntax which needs the QMF / patching +//! back-end. This module's writer / parser deliberately does +//! *not* consume bits for these types so a future SBR round can +//! take over without a wire-format incompatibility. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::{Error, Result}; + +/// Width in bits of the wire `extension_type` field. ISO/IEC +/// 14496-3 Table 4.51. +pub const EXTENSION_TYPE_BITS: u32 = 4; + +/// Symbolic `extension_type` values per ISO/IEC 14496-3 Table 4.59 +/// plus the ISO/IEC 13818-7 Table 40 SBR-data extensions. +/// +/// Every variant maps to a single 4-bit wire value; the raw value +/// is exposed via [`ExtensionType::as_u8`] for round-tripping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExtensionType { + /// `EXT_FILL` (`0b0000`) — bitstream filler. + Fill, + /// `EXT_FILL_DATA` (`0b0001`) — bitstream data as filler. + /// Normative payload: 4-bit `fill_nibble == 0b0000` followed + /// by `cnt - 1` × 8-bit `fill_byte == 0b10100101`. + FillData, + /// `EXT_DYNAMIC_RANGE` (`0b1011`) — dynamic range control. + /// Body is the Table 4.52 `dynamic_range_info()` block. + DynamicRange, + /// `EXT_SBR_DATA` (`0b1101`) — SBR enhancement (ISO/IEC + /// 13818-7 Table 40). This crate does not parse the + /// `sbr_extension_data()` body yet. + SbrData, + /// `EXT_SBR_DATA_CRC` (`0b1110`) — SBR enhancement with CRC + /// (ISO/IEC 13818-7 Table 40). This crate does not parse the + /// `sbr_extension_data()` body yet. + SbrDataCrc, +} + +impl ExtensionType { + /// Map a 4-bit wire value (`0..=15`) to the corresponding + /// [`ExtensionType`], or surface a structural error. + /// + /// Returns: + /// + /// * [`Error::UnsupportedExtensionSbr`] for `0b1101` + /// (`EXT_SBR_DATA`) and `0b1110` (`EXT_SBR_DATA_CRC`) — the + /// bodies are the SBR `sbr_extension_data()` syntax which + /// this crate does not parse. + /// * [`Error::UnsupportedExtensionType`] carrying the raw + /// 4-bit value for any other value not in + /// `{0b0000, 0b0001, 0b1011, 0b1101, 0b1110}`. Table 4.59 / + /// Table 40 list these as "reserved". + pub fn from_bits(value: u8) -> Result { + match value { + 0b0000 => Ok(ExtensionType::Fill), + 0b0001 => Ok(ExtensionType::FillData), + 0b1011 => Ok(ExtensionType::DynamicRange), + 0b1101 | 0b1110 => Err(Error::UnsupportedExtensionSbr(value)), + other if other <= 0x0f => Err(Error::UnsupportedExtensionType(other)), + // unreachable in practice — `read_u32(4)` produces 0..=15 + _ => Err(Error::UnsupportedExtensionType(value)), + } + } + + /// Like [`Self::from_bits`] but maps the two SBR wire values to + /// their [`ExtensionType`] variants instead of an error, so the + /// [`ExtensionPayload::parse_with_sbr`] entry can dispatch them into + /// the SBR side-info walker. Reserved values still error. + pub fn from_bits_allow_sbr(value: u8) -> Result { + match value { + 0b0000 => Ok(ExtensionType::Fill), + 0b0001 => Ok(ExtensionType::FillData), + 0b1011 => Ok(ExtensionType::DynamicRange), + 0b1101 => Ok(ExtensionType::SbrData), + 0b1110 => Ok(ExtensionType::SbrDataCrc), + other => Err(Error::UnsupportedExtensionType(other)), + } + } + + /// Convert back to the 4-bit wire value used by Table 4.51. + pub fn as_u8(self) -> u8 { + match self { + ExtensionType::Fill => 0b0000, + ExtensionType::FillData => 0b0001, + ExtensionType::DynamicRange => 0b1011, + ExtensionType::SbrData => 0b1101, + ExtensionType::SbrDataCrc => 0b1110, + } + } +} + +/// Normative `fill_byte` literal per ISO/IEC 14496-3 §4.4.2.7 / +/// Table 4.51 (`must be '10100101'`). Surfaced as a public constant +/// so callers and tests can refer to the same magic value. +pub const FILL_DATA_BYTE: u8 = 0b1010_0101; + +/// Normative `fill_nibble` literal per ISO/IEC 14496-3 §4.4.2.7 / +/// Table 4.51 (`must be '0000'`). +pub const FILL_DATA_NIBBLE: u8 = 0b0000; + +/// Parsed `extension_payload()` body (Table 4.51 dispatch). +/// +/// The body always carries the byte count it consumed (the `n` +/// returned by Table 4.51) so the surrounding FIL `cnt` can be +/// decremented in lockstep with the spec. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExtensionPayload { + /// `EXT_FILL` — opaque filler. Carries the raw `8 * (cnt - 1) + 4` + /// "other_bits" packed MSB-first into a byte vector. The last + /// byte's low 4 bits are unused if `cnt > 0` (since the body + /// is not a whole number of bytes). + Fill { + /// Total bytes consumed by this `extension_payload`, + /// including the 4-bit `extension_type` nibble (so the + /// useful body is `8 * (cnt - 1) + 4` bits). + cnt: u32, + /// `other_bits` packed MSB-first. Empty when `cnt == 1` + /// (a 4-bit-only EXT_FILL whose body is 4 unused bits). + other_bits: Vec, + }, + /// `EXT_FILL_DATA` — normative-pattern filler. Carries the + /// byte count (FIL `cnt`) so the body length is implicit. + FillData { + /// Total bytes consumed (the FIL `cnt`). + cnt: u32, + }, + /// `EXT_DYNAMIC_RANGE` — DRC metadata per Table 4.52. + DynamicRange(DynamicRangeInfo), +} + +/// The result of [`ExtensionPayload::parse_with_sbr`]: either a standard +/// (non-SBR) extension payload, or a decoded SBR side-info element. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExtensionPayloadOrSbr { + /// A non-SBR extension payload (`EXT_FILL` / `EXT_FILL_DATA` / + /// `EXT_DYNAMIC_RANGE`). + Payload(ExtensionPayload), + /// A decoded `sbr_extension_data()` (`EXT_SBR_DATA` / + /// `EXT_SBR_DATA_CRC`). Boxed because the SBR side-info element is + /// much larger than the other variants. + Sbr(Box), + /// An SBR payload received **before any `sbr_header()`** — the + /// stream opens with `bs_header_flag == 0` payloads and no header + /// has been threaded yet. Per ISO/IEC 14496-3:2009 §4.5.2.8.1 + /// ("As long as no SBR header part is present, the SBR decoder + /// performs upsampling and delay adjustment only") the `sbr_data()` + /// body cannot be parsed (its band tables come from the missing + /// header), so the payload is skipped whole; the caller should run + /// the §4.6.18.5 pure-upsampling path for the covered element. The + /// ISO/IEC 14496-26 `al_sbr_{e,i}_32_*` conformance vectors open + /// this way. + /// + /// `crc` / `crc_region` carry the `EXT_SBR_DATA_CRC` checksum and + /// its covered bit range (everything after the 10-bit CRC field up + /// to the end of the fill payload, per the §4.5.2.8.1 coverage + /// statement — with no parsed `sbr_data()` the `bs_fill_bits` + /// boundary is unknowable, and the whole-payload region is the + /// normative coverage); `None` for the plain `EXT_SBR_DATA` type. + SbrPreHeader { + /// Transmitted `bs_sbr_crc_bits`, when the CRC variant. + crc: Option, + /// Covered `[start, end)` bit range in the parse buffer. + crc_region: Option<(u64, u64)>, + }, +} + +/// Parsed `dynamic_range_info()` body (Table 4.52). All fields are +/// surfaced verbatim from the wire; the §4.5.2.13 companding curve +/// that maps `(dyn_rng_sgn, dyn_rng_ctl)` pairs to dB attenuations +/// is *not* applied here. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DynamicRangeInfo { + /// Optional PCE element-tag selector. `Some((tag, reserved))` + /// when `pce_tag_present == 1`. Both fields are 4 bits. + pub pce_tag: Option, + /// Optional excluded-channels list. `Some(_)` when + /// `excluded_chns_present == 1`. + pub excluded_channels: Option, + /// Optional per-band partitioning. `Some(_)` when + /// `drc_bands_present == 1`. When `None`, the spec sets + /// `drc_num_bands = 1` and there is a single + /// `(dyn_rng_sgn[0], dyn_rng_ctl[0])` pair below. + pub drc_bands: Option, + /// Optional 7-bit `prog_ref_level` reference level + /// (`Some((level, reserved))` when `prog_ref_level_present + /// == 1`). `reserved` is the trailing 1-bit reserved field. + pub prog_ref_level: Option, + /// Per-band `(dyn_rng_sgn, dyn_rng_ctl)` records, in wire + /// order. Length equals the resolved `drc_num_bands` + /// (`drc_bands.is_none()` ⇒ 1; otherwise + /// `1 + drc_bands.band_incr`). + pub bands: Vec, +} + +/// 4-bit `pce_instance_tag` + 4-bit `drc_tag_reserved_bits` pair +/// per Table 4.52. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PceTagFields { + /// `pce_instance_tag` — selects the surrounding PCE this DRC + /// applies to. 4 bits. + pub pce_instance_tag: u8, + /// `drc_tag_reserved_bits` — 4 bits, value not constrained by + /// the spec. + pub reserved: u8, +} + +/// `excluded_channels()` body (Table 4.53). Carries the resolved +/// `exclude_mask[]` bits packed MSB-first into a `Vec`. The +/// wire length is implied by the trailing +/// `additional_excluded_chns[n-1] == 0` flag — every 7 +/// `exclude_mask` bits are followed by a 1-bit continuation flag, +/// repeating until the continuation flag reads 0. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExcludedChannels { + /// `exclude_mask[i]` for `i = 0..(7 * n_groups)`, where + /// `n_groups` is the number of 8-bit-wide groups consumed. + pub exclude_mask: Vec, +} + +/// `drc_band_incr` + `drc_bands_reserved_bits` + `drc_band_top[]` +/// payload per Table 4.52. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DrcBands { + /// `drc_band_incr` — 4 bits. Resolved + /// `drc_num_bands = 1 + drc_band_incr`. + pub band_incr: u8, + /// `drc_bands_reserved_bits` — 4 bits, value not constrained + /// by the spec. + pub reserved: u8, + /// `drc_band_top[i]` — 8 bits per band. Length equals + /// `1 + band_incr`. + pub band_top: Vec, +} + +/// 7-bit `prog_ref_level` + 1-bit `prog_ref_level_reserved_bits` +/// pair per Table 4.52. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProgRefLevelFields { + /// `prog_ref_level` — 7 bits. Reference level for downstream + /// loudness normalisation. + pub level: u8, + /// `prog_ref_level_reserved_bits` — 1 bit. + pub reserved: bool, +} + +/// Per-band `(dyn_rng_sgn, dyn_rng_ctl)` pair per Table 4.52. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DrcBandRecord { + /// `dyn_rng_sgn[i]` — 1-bit sign. `true` ⇒ negative gain (cut). + pub dyn_rng_sgn: bool, + /// `dyn_rng_ctl[i]` — 7-bit magnitude in 0.25 dB steps per + /// §4.5.2.13. Surfaced verbatim here. + pub dyn_rng_ctl: u8, +} + +impl ExtensionPayload { + /// Parse an `extension_payload(cnt)` from `reader`. + /// + /// `cnt` is the FIL element's payload byte count after the + /// §4.4.2.7 `esc_count` escape resolution (the same value the + /// existing [`crate::raw_data_block::Walker`] computes via + /// `read_fill_count`). `cnt == 0` is rejected as + /// [`Error::ExtensionPayloadInvalid`] — Table 4.51's + /// `extension_type` field itself is 4 bits, so a zero-byte FIL + /// has no room for it. + pub fn parse(reader: &mut BitReader<'_>, cnt: u32) -> Result { + if cnt == 0 { + return Err(Error::ExtensionPayloadInvalid); + } + let raw = read_u8(reader, EXTENSION_TYPE_BITS)?; + let ty = ExtensionType::from_bits(raw)?; + match ty { + ExtensionType::Fill => parse_fill(reader, cnt), + ExtensionType::FillData => parse_fill_data(reader, cnt), + ExtensionType::DynamicRange => parse_dynamic_range(reader, cnt), + // `from_bits` already converted these to errors. + ExtensionType::SbrData | ExtensionType::SbrDataCrc => unreachable!(), + } + } + + /// Parse an `extension_payload(cnt)`, routing the two SBR extension + /// types (`EXT_SBR_DATA` / `EXT_SBR_DATA_CRC`) into the + /// [`crate::sbr_extension::SbrExtensionData`] side-info walker rather + /// than rejecting them. + /// + /// Unlike [`Self::parse`] (which surfaces + /// [`Error::UnsupportedExtensionSbr`] for the SBR types so the + /// byte-exact AAC-LC decode path stays untouched), this entry decodes + /// the SBR bitstream side info: the §4.4.2.8 `sbr_extension_data()` + /// header + element framing keyed off the surrounding channel + /// element. The SBR back-end DSP (QMF / HF patching / envelope + /// adjustment) is still not applied — this only recovers the decoded + /// side info. + /// + /// * `id_aac` — the AAC core element this FIL follows + /// ([`crate::raw_data_block::IdSynEle::Sce`] / `Cpe`); selects the + /// single- vs pair-element `sbr_data()` dispatch. + /// * `fs_sbr` — the SBR internal sample rate (twice the core rate). + /// * `prev_header` — the threaded previous `sbr_header()` for the + /// `bs_header_flag == 0` reuse path (`None` on the first payload). + /// + /// A non-SBR extension type returns + /// [`ExtensionPayloadOrSbr::Payload`] with the same body + /// [`Self::parse`] would produce. + pub fn parse_with_sbr( + reader: &mut BitReader<'_>, + cnt: u32, + id_aac: crate::raw_data_block::IdSynEle, + fs_sbr: u32, + prev_header: Option, + ) -> Result { + if cnt == 0 { + return Err(Error::ExtensionPayloadInvalid); + } + let nibble_start = reader.bit_position(); + let raw = read_u8(reader, EXTENSION_TYPE_BITS)?; + let ty = ExtensionType::from_bits_allow_sbr(raw)?; + match ty { + ExtensionType::Fill => Ok(ExtensionPayloadOrSbr::Payload(parse_fill(reader, cnt)?)), + ExtensionType::FillData => Ok(ExtensionPayloadOrSbr::Payload(parse_fill_data( + reader, cnt, + )?)), + ExtensionType::DynamicRange => Ok(ExtensionPayloadOrSbr::Payload(parse_dynamic_range( + reader, cnt, + )?)), + ExtensionType::SbrData | ExtensionType::SbrDataCrc => { + let crc_flag = ty == ExtensionType::SbrDataCrc; + if prev_header.is_none() { + // Peek the CRC field + bs_header_flag without + // committing: a header-less payload before the + // first sbr_header() cannot be parsed (§4.5.2.8.1 + // — upsampling and delay adjustment only), so the + // payload is skipped whole with its CRC surfaced. + let crc = if crc_flag { + Some(reader.read_u32(10).map_err(|_| Error::UnexpectedEnd)? as u16) + } else { + None + }; + let region_start = reader.bit_position(); + let header_flag = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + if !header_flag { + let end = nibble_start + u64::from(cnt) * 8; + let mut pos = reader.bit_position(); + while pos < end { + let step = (end - pos).min(32) as u32; + reader.read_u32(step).map_err(|_| Error::UnexpectedEnd)?; + pos += u64::from(step); + } + return Ok(ExtensionPayloadOrSbr::SbrPreHeader { + crc, + crc_region: crc.map(|_| (region_start, end)), + }); + } + // A header is present after all: re-parse through + // the normal path from the header flag onward. + let sbr = crate::sbr_extension::SbrExtensionData::parse_after_prefix( + reader, + id_aac, + crc, + nibble_start, + fs_sbr, + Some(cnt), + None, + )?; + return Ok(ExtensionPayloadOrSbr::Sbr(Box::new(sbr))); + } + let sbr = crate::sbr_extension::SbrExtensionData::parse( + reader, + id_aac, + crc_flag, + fs_sbr, + Some(cnt), + prev_header, + )?; + Ok(ExtensionPayloadOrSbr::Sbr(Box::new(sbr))) + } + } + } + + /// Encode an `extension_payload()` body onto `writer` — the + /// bit-exact inverse of [`ExtensionPayload::parse`]. + /// + /// Returns the byte count consumed (matching Table 4.51's + /// returned `n`). Surfaces caller-side field violations as + /// [`Error::ExtensionPayloadInvalid`]. + pub fn write(&self, writer: &mut BitWriter) -> Result { + match self { + ExtensionPayload::Fill { cnt, other_bits } => write_fill(writer, *cnt, other_bits), + ExtensionPayload::FillData { cnt } => write_fill_data(writer, *cnt), + ExtensionPayload::DynamicRange(drc) => write_dynamic_range(writer, drc), + } + } + + /// Total byte count this `extension_payload` consumed on the + /// wire — Table 4.51's returned `n`. + pub fn byte_length(&self) -> u32 { + match self { + ExtensionPayload::Fill { cnt, .. } => *cnt, + ExtensionPayload::FillData { cnt } => *cnt, + ExtensionPayload::DynamicRange(drc) => drc.byte_length(), + } + } +} + +impl DynamicRangeInfo { + /// Byte count this DRC body consumes — Table 4.52's returned + /// `n`, including the 4-bit `extension_type` nibble that the + /// outer `extension_payload()` writes immediately before the + /// DRC body. + pub fn byte_length(&self) -> u32 { + // Start from n = 1 (the leading byte containing the 4-bit + // extension_type + 4 presence flags). + let mut n: u32 = 1; + if self.pce_tag.is_some() { + n += 1; + } + if let Some(ex) = &self.excluded_channels { + // Each group is 7 mask bits + 1 continuation bit = 1 byte. + n += excluded_group_count(ex.exclude_mask.len()) as u32; + } + if let Some(b) = &self.drc_bands { + // drc_band_incr + reserved = 1 byte, then 1 byte per + // drc_band_top entry. + n += 1 + b.band_top.len() as u32; + } + if self.prog_ref_level.is_some() { + n += 1; + } + // 1 byte per (dyn_rng_sgn + dyn_rng_ctl). + n += self.bands.len() as u32; + n + } + + /// Resolved `drc_num_bands` per Table 4.52. Always equals + /// `bands.len()`. + pub fn num_bands(&self) -> usize { + self.bands.len() + } +} + +// =================================================================== +// EXT_FILL parser / writer +// =================================================================== + +fn parse_fill(reader: &mut BitReader<'_>, cnt: u32) -> Result { + // Table 4.51 default branch: + // for (i = 0; i < 8*(cnt-1) + 4; i++) other_bits[i]; + // 4 of those bits are already consumed (the extension_type + // nibble — except wait, no: the 8*(cnt-1)+4 count is the bits + // AFTER the extension_type. Re-reading the spec carefully — + // Table 4.51 reads extension_type FIRST, then enters the + // switch; the default branch's loop counts the body AFTER the + // type nibble. The total bits consumed is then + // 4 + 8*(cnt-1) + 4 = 8 * cnt — consistent with returning cnt. + let body_bits = 8u32 + .checked_mul(cnt.saturating_sub(1)) + .ok_or(Error::ExtensionPayloadInvalid)? + .checked_add(4) + .ok_or(Error::ExtensionPayloadInvalid)?; + let other_bits = read_packed_bits(reader, body_bits)?; + Ok(ExtensionPayload::Fill { cnt, other_bits }) +} + +fn write_fill(writer: &mut BitWriter, cnt: u32, other_bits: &[u8]) -> Result { + if cnt == 0 { + return Err(Error::ExtensionPayloadInvalid); + } + let body_bits = 8u32 + .checked_mul(cnt.saturating_sub(1)) + .ok_or(Error::ExtensionPayloadInvalid)? + .checked_add(4) + .ok_or(Error::ExtensionPayloadInvalid)?; + let expected_bytes = (body_bits as usize).div_ceil(8); + if other_bits.len() != expected_bytes { + return Err(Error::ExtensionPayloadInvalid); + } + writer.write_u32(ExtensionType::Fill.as_u8() as u32, EXTENSION_TYPE_BITS); + write_packed_bits(writer, other_bits, body_bits)?; + Ok(cnt) +} + +// =================================================================== +// EXT_FILL_DATA parser / writer +// =================================================================== + +fn parse_fill_data(reader: &mut BitReader<'_>, cnt: u32) -> Result { + // Table 4.51: + // fill_nibble; 4 bits /* must be '0000' */ + // for (i = 0; i < cnt - 1; i++) + // fill_byte[i]; 8 bits /* must be '10100101' */ + let nibble = read_u8(reader, 4)?; + if nibble != FILL_DATA_NIBBLE { + return Err(Error::ExtensionPayloadInvalid); + } + let body_bytes = cnt.saturating_sub(1) as usize; + for _ in 0..body_bytes { + let b = read_u8(reader, 8)?; + if b != FILL_DATA_BYTE { + return Err(Error::ExtensionPayloadInvalid); + } + } + Ok(ExtensionPayload::FillData { cnt }) +} + +fn write_fill_data(writer: &mut BitWriter, cnt: u32) -> Result { + if cnt == 0 { + return Err(Error::ExtensionPayloadInvalid); + } + writer.write_u32(ExtensionType::FillData.as_u8() as u32, EXTENSION_TYPE_BITS); + writer.write_u32(FILL_DATA_NIBBLE as u32, 4); + let body_bytes = cnt.saturating_sub(1) as usize; + for _ in 0..body_bytes { + writer.write_u32(FILL_DATA_BYTE as u32, 8); + } + Ok(cnt) +} + +// =================================================================== +// EXT_DYNAMIC_RANGE parser / writer +// =================================================================== + +fn parse_dynamic_range(reader: &mut BitReader<'_>, cnt: u32) -> Result { + let pce_tag_present = read_bit(reader)?; + let pce_tag = if pce_tag_present { + let pce_instance_tag = read_u8(reader, 4)?; + let reserved = read_u8(reader, 4)?; + Some(PceTagFields { + pce_instance_tag, + reserved, + }) + } else { + None + }; + + let excluded_chns_present = read_bit(reader)?; + let excluded_channels = if excluded_chns_present { + Some(parse_excluded_channels(reader)?) + } else { + None + }; + + let drc_bands_present = read_bit(reader)?; + let drc_bands = if drc_bands_present { + let band_incr = read_u8(reader, 4)?; + let reserved = read_u8(reader, 4)?; + let num_bands = 1usize + band_incr as usize; + let mut band_top = Vec::with_capacity(num_bands); + for _ in 0..num_bands { + band_top.push(read_u8(reader, 8)?); + } + Some(DrcBands { + band_incr, + reserved, + band_top, + }) + } else { + None + }; + + let prog_ref_level_present = read_bit(reader)?; + let prog_ref_level = if prog_ref_level_present { + let level = read_u8(reader, 7)?; + let reserved = read_bit(reader)?; + Some(ProgRefLevelFields { level, reserved }) + } else { + None + }; + + let num_bands = drc_bands + .as_ref() + .map(|b| 1 + b.band_incr as usize) + .unwrap_or(1); + let mut bands = Vec::with_capacity(num_bands); + for _ in 0..num_bands { + let dyn_rng_sgn = read_bit(reader)?; + let dyn_rng_ctl = read_u8(reader, 7)?; + bands.push(DrcBandRecord { + dyn_rng_sgn, + dyn_rng_ctl, + }); + } + + let drc = DynamicRangeInfo { + pce_tag, + excluded_channels, + drc_bands, + prog_ref_level, + bands, + }; + if drc.byte_length() != cnt { + // The dispatching FIL `cnt` and the derived Table 4.52 `n` + // must agree byte-for-byte — Table 4.52 normatively + // returns the byte count to the caller. A mismatch + // indicates a malformed bitstream. + return Err(Error::ExtensionPayloadInvalid); + } + Ok(ExtensionPayload::DynamicRange(drc)) +} + +fn write_dynamic_range(writer: &mut BitWriter, drc: &DynamicRangeInfo) -> Result { + // Caller-side invariant checks (every numeric field cap from + // Table 4.52). + if let Some(p) = &drc.pce_tag { + if p.pce_instance_tag > 0x0f || p.reserved > 0x0f { + return Err(Error::ExtensionPayloadInvalid); + } + } + if let Some(b) = &drc.drc_bands { + if b.band_incr > 0x0f || b.reserved > 0x0f { + return Err(Error::ExtensionPayloadInvalid); + } + if b.band_top.len() != 1 + b.band_incr as usize { + return Err(Error::ExtensionPayloadInvalid); + } + } + if let Some(p) = &drc.prog_ref_level { + if p.level > 0x7f { + return Err(Error::ExtensionPayloadInvalid); + } + } + let expected_bands = drc + .drc_bands + .as_ref() + .map(|b| 1 + b.band_incr as usize) + .unwrap_or(1); + if drc.bands.len() != expected_bands { + return Err(Error::ExtensionPayloadInvalid); + } + for r in &drc.bands { + if r.dyn_rng_ctl > 0x7f { + return Err(Error::ExtensionPayloadInvalid); + } + } + + writer.write_u32( + ExtensionType::DynamicRange.as_u8() as u32, + EXTENSION_TYPE_BITS, + ); + + writer.write_bit(drc.pce_tag.is_some()); + if let Some(p) = &drc.pce_tag { + writer.write_u32(p.pce_instance_tag as u32, 4); + writer.write_u32(p.reserved as u32, 4); + } + + writer.write_bit(drc.excluded_channels.is_some()); + if let Some(ex) = &drc.excluded_channels { + write_excluded_channels(writer, ex)?; + } + + writer.write_bit(drc.drc_bands.is_some()); + if let Some(b) = &drc.drc_bands { + writer.write_u32(b.band_incr as u32, 4); + writer.write_u32(b.reserved as u32, 4); + for &top in &b.band_top { + writer.write_u32(top as u32, 8); + } + } + + writer.write_bit(drc.prog_ref_level.is_some()); + if let Some(p) = &drc.prog_ref_level { + writer.write_u32(p.level as u32, 7); + writer.write_bit(p.reserved); + } + + for r in &drc.bands { + writer.write_bit(r.dyn_rng_sgn); + writer.write_u32(r.dyn_rng_ctl as u32, 7); + } + + Ok(drc.byte_length()) +} + +// =================================================================== +// excluded_channels() helper (Table 4.53) +// =================================================================== + +fn parse_excluded_channels(reader: &mut BitReader<'_>) -> Result { + // Table 4.53: each iteration reads 7 exclude_mask bits + 1 + // additional_excluded_chns continuation flag = 1 byte. Stop + // when the continuation flag reads 0. + let mut exclude_mask = Vec::new(); + loop { + for _ in 0..7 { + exclude_mask.push(read_bit(reader)?); + } + let cont = read_bit(reader)?; + if !cont { + break; + } + } + Ok(ExcludedChannels { exclude_mask }) +} + +fn write_excluded_channels(writer: &mut BitWriter, ex: &ExcludedChannels) -> Result<()> { + if ex.exclude_mask.is_empty() || ex.exclude_mask.len() % 7 != 0 { + // Table 4.53 emits exclude_mask bits in fixed groups of 7; + // any non-multiple-of-7 length cannot round-trip through + // [`parse_excluded_channels`]. + return Err(Error::ExtensionPayloadInvalid); + } + let groups = ex.exclude_mask.len() / 7; + for g in 0..groups { + for i in 0..7 { + writer.write_bit(ex.exclude_mask[g * 7 + i]); + } + // The continuation flag is 1 for every group except the + // last, which carries 0 to terminate. + let last = g + 1 == groups; + writer.write_bit(!last); + } + Ok(()) +} + +/// Resolved byte count for an `excluded_channels()` body carrying +/// the given total `exclude_mask` bit count. Exposed so callers can +/// pre-size `cnt` without round-tripping through +/// [`DynamicRangeInfo::byte_length`]. +pub fn excluded_group_count(exclude_mask_len: usize) -> usize { + // The spec emits 7-bit groups; the byte count equals the group + // count (each group is 7 mask bits + 1 continuation bit). + exclude_mask_len.div_ceil(7) +} + +// =================================================================== +// Helpers +// =================================================================== + +fn read_packed_bits(reader: &mut BitReader<'_>, n_bits: u32) -> Result> { + let n_bytes = (n_bits as usize).div_ceil(8); + let mut out = vec![0u8; n_bytes]; + let mut remaining = n_bits; + let mut idx = 0; + while remaining >= 8 { + out[idx] = read_u8(reader, 8)?; + idx += 1; + remaining -= 8; + } + if remaining > 0 { + // Pack the trailing partial byte into the top bits of the + // last output byte. + let partial = read_u8(reader, remaining)?; + out[idx] = partial << (8 - remaining); + } + Ok(out) +} + +fn write_packed_bits(writer: &mut BitWriter, bytes: &[u8], n_bits: u32) -> Result<()> { + let mut remaining = n_bits; + let mut idx = 0; + while remaining >= 8 { + writer.write_u32(bytes[idx] as u32, 8); + idx += 1; + remaining -= 8; + } + if remaining > 0 { + // The trailing partial byte stores its bits in the top + // `remaining` bits; recover them with a right-shift. + let partial = bytes[idx] >> (8 - remaining); + writer.write_u32(partial as u32, remaining); + } + Ok(()) +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} diff --git a/crates/vendor/oxideav-aac/src/filterbank.rs b/crates/vendor/oxideav-aac/src/filterbank.rs new file mode 100644 index 00000000..caf9865c --- /dev/null +++ b/crates/vendor/oxideav-aac/src/filterbank.rs @@ -0,0 +1,1549 @@ +//! §4.6.11 Filterbank and block switching — the inverse modified +//! discrete cosine transform (IMDCT), the analysis/synthesis windows +//! (sine and Kaiser-Bessel-derived), and the overlap-add that maps a +//! window-major decoded spectrum back to the time domain. +//! +//! This is the last stage of the per-channel decode chain +//! ([`crate::decoded_spectrum::decode_channel_spectrum`]) for a +//! single channel: it consumes the `num_windows × window_len = 1024` +//! window-major coefficients and emits 1024 PCM-domain samples per +//! frame after overlap-adding against the previous frame's tail. +//! +//! Spec basis (ISO/IEC 14496-3:2001, §4.6.11): +//! +//! * §4.6.11.3.1 — the IMDCT +//! `x[n] = (2/N) · Σ_k spec[k] · cos((2π/N)·(n + n0)·(k + 1/2))` +//! for `0 ≤ n < N`, with `n0 = (N/2 + 1)/2`. `N` is the +//! *transform* window length (2048 for long sequences, 256 for each +//! of the eight short windows). The crate carries the spectrum at +//! `N/2` resolution (1024 long, 128 short) as +//! [`crate::swb_offset::LONG_WINDOW_LEN`] / +//! [`crate::swb_offset::SHORT_WINDOW_LEN`]. +//! * §4.6.11.3.2 — windowing and block switching. The sine window is +//! `W_SIN(n) = sin((π/N)·(n + 1/2))`; the KBD window is the +//! normalized running sum of the Kaiser-Bessel kernel `W'(n, α)` +//! with `α = 4` for the long transform and `α = 6` for the short +//! transform. The four `window_sequence` shapes +//! (`ONLY_LONG`, `LONG_START`, `EIGHT_SHORT`, `LONG_STOP`) compose +//! left/right window halves; the left half's shape is inherited +//! from the *previous* block's `window_shape`. +//! * §4.6.11.3.3 — the inter-block overlap-add +//! `out[n] = z[i][n] + z[i-1][n + N/2]` for `0 ≤ n < N/2`, +//! `N = 2048`, valid for all four sequences. +//! +//! The frame-length-960 (`N = 1920 / 240`) variant of the spec is +//! out of scope: the rest of the crate's `swb_offset` tables and +//! transmission-order machinery are wired to the 1024-coefficient +//! layout, so this module mirrors that and only implements the 2048 +//! transform family. + +use crate::ics_info::{IcsInfo, WindowSequence, WindowShape}; +use crate::swb_offset::{FrameFamily, LONG_WINDOW_LEN, SHORT_WINDOW_LEN}; +use crate::Error; + +/// `N` for a long-sequence transform (§4.6.11.3.1): 2 × +/// [`LONG_WINDOW_LEN`]. +const LONG_TRANSFORM_LEN: usize = 2 * LONG_WINDOW_LEN as usize; // 2048 +/// `N` for a single short-sequence transform: 2 × +/// [`SHORT_WINDOW_LEN`]. +const SHORT_TRANSFORM_LEN: usize = 2 * SHORT_WINDOW_LEN as usize; // 256 +/// `M = N_l / N_s` = number of short windows in an `EIGHT_SHORT` +/// sequence. +const NUM_SHORT_WINDOWS: usize = 8; +/// `N_l` — the long transform length, used as the frame's PCM stride. +const N_L: usize = LONG_TRANSFORM_LEN; // 2048 +/// `N_s` — the short transform length. +const N_S: usize = SHORT_TRANSFORM_LEN; // 256 + +/// Result of [`Filterbank::synthesize`]: one frame of +/// `LONG_WINDOW_LEN` (1024) PCM-domain samples for a single channel. +type Result = core::result::Result; + +/// §4.6.11.3.1 — inverse MDCT for a length-`n_transform` window. +/// +/// `spec` holds the `N/2` transmitted coefficients; the returned +/// vector holds the `N` time-domain values +/// `x[n] = (2/N) · Σ_k spec[k] · cos((2π/N)·(n + n0)·(k + 1/2))`. +/// +/// `n0 = (N/2 + 1)/2` is the §4.6.11.3.1 phase offset. The `2/N` +/// scale and the half-coefficient phase are the only normalization +/// the spec attaches to the inverse transform; the energy-correcting +/// window then follows in the per-sequence windowing step. +pub(crate) fn imdct(spec: &[f64], n_transform: usize) -> Vec { + let half = n_transform / 2; + debug_assert_eq!(spec.len(), half); + let n0 = (half + 1) as f64 / 2.0; + let scale = 2.0 / n_transform as f64; + let phase_step = 2.0 * core::f64::consts::PI / n_transform as f64; + let mut out = vec![0.0f64; n_transform]; + for (n, slot) in out.iter_mut().enumerate() { + let np = n as f64 + n0; + let mut acc = 0.0f64; + for (k, &c) in spec.iter().enumerate() { + acc += c * (phase_step * np * (k as f64 + 0.5)).cos(); + } + *slot = scale * acc; + } + out +} + +/// §4.6.15.3.3 / §4.6.11.3.1 — the forward (analysis) MDCT for a +/// length-`n_transform` window. +/// +/// `time` holds the `N` windowed time-domain values `z[n]`; the +/// returned vector holds the `N/2` spectral coefficients +/// `X[k] = 2 · Σ_n z[n] · cos((2π/N)·(n + n0)·(k + 1/2))`, +/// `0 ≤ k < N/2`, with the §4.6.11.3.1 phase `n0 = (N/2 + 1)/2`. +/// +/// This is the exact analysis pair of [`imdct`]: the IMDCT carries the +/// `2/N` scale, the analysis here carries the matching factor `2`, so +/// the windowed-and-overlap-added round trip is unity for a +/// power-complementary §4.6.11.3.2 window. The same transform is the +/// `MDCT(x_est)` of the §4.6.7.3 Long-Term-Prediction loop. +pub(crate) fn forward_mdct(time: &[f64], n_transform: usize) -> Vec { + let half = n_transform / 2; + debug_assert_eq!(time.len(), n_transform); + let n0 = (half + 1) as f64 / 2.0; + let step = 2.0 * core::f64::consts::PI / n_transform as f64; + (0..half) + .map(|k| { + 2.0 * time + .iter() + .enumerate() + .map(|(n, &t)| t * (step * (n as f64 + n0) * (k as f64 + 0.5)).cos()) + .sum::() + }) + .collect() +} + +/// §4.6.11.3.2 — build the `ONLY_LONG_SEQUENCE` analysis window +/// `[W_LEFT_l | W_RIGHT_l]` at the family's long transform length, +/// with the family's window style (the LD families map +/// `window_shape == 1` to the §4.6.17.2.3 low-overlap window). +/// +/// Exposed for the §4.6.7.3 LTP loop, which windows the predicted time +/// signal `x_est` with the current long window before the analysis +/// [`forward_mdct`]. (LTP is restricted to long windows, §4.6.7.1.) +pub(crate) fn long_only_window_family( + family: FrameFamily, + left_shape: WindowShape, + right_shape: WindowShape, +) -> Vec { + let n_l = family.long_transform_len(); + let halves = window_halves_style( + n_l, + left_shape, + right_shape, + WindowStyle::for_family(family), + ); + let half_l = n_l / 2; + let mut w = vec![0.0f64; n_l]; + w[..half_l].copy_from_slice(&halves.left); + for (m, &rv) in halves.right.iter().enumerate() { + w[half_l + m] = rv; + } + w +} + +/// §4.6.11.3.2 — assemble the length-2048 window for any of the +/// three long-transform sequences. The window is shared between the +/// decoder's synthesis ([`Filterbank::long_window`] delegates here) +/// and the encoder's analysis (the §4.6.11 filterbank is its own +/// transpose up to the TDAC fold, so the same window applies on both +/// sides). Returns [`Error::FilterbankInvalid`] for +/// `EIGHT_SHORT_SEQUENCE` — use [`short_window_j`] per short window +/// instead. +pub(crate) fn long_sequence_window( + sequence: WindowSequence, + left_shape: WindowShape, + right_shape: WindowShape, +) -> Result> { + long_sequence_window_n(N_L, N_S, sequence, left_shape, right_shape) +} + +/// §4.6.11.3.2 — the [`long_sequence_window`] construction generalized +/// to an arbitrary `(n_l, n_s)` transform family. The SSR gain-control +/// filterbank (§4.6.12.1) runs the same window geometry at +/// `(512, 64)` — one quarter of the standard family — per band. +pub(crate) fn long_sequence_window_n( + n_l: usize, + n_s: usize, + sequence: WindowSequence, + left_shape: WindowShape, + right_shape: WindowShape, +) -> Result> { + let kind = match sequence { + WindowSequence::OnlyLong => LongKind::OnlyLong, + WindowSequence::LongStart => LongKind::Start, + WindowSequence::LongStop => LongKind::Stop, + WindowSequence::EightShort => return Err(Error::FilterbankInvalid), + }; + Ok(build_long_window_n(n_l, n_s, left_shape, right_shape, kind)) +} + +/// §4.6.11.3.2 c) — the length-256 window of short window `j` +/// (`0..8`) inside an `EIGHT_SHORT_SEQUENCE` frame: window 0's left +/// half inherits the previous block's shape, all other halves use +/// this block's shape. +pub(crate) fn short_window_j( + j: usize, + left_shape: WindowShape, + right_shape: WindowShape, +) -> Vec { + short_window_n(N_S, j, left_shape, right_shape) +} + +/// §4.6.11.3.2 c) — [`short_window_j`] generalized to an arbitrary +/// short-transform length `n_s` (64 for the SSR §4.6.12.1 per-band +/// family). +pub(crate) fn short_window_n( + n_s: usize, + j: usize, + left_shape: WindowShape, + right_shape: WindowShape, +) -> Vec { + let this_left = if j == 0 { left_shape } else { right_shape }; + let halves = window_halves(n_s, this_left, right_shape); + let mut w = vec![0.0f64; n_s]; + w[..n_s / 2].copy_from_slice(&halves.left); + for (m, &rv) in halves.right.iter().enumerate() { + w[n_s / 2 + m] = rv; + } + w +} + +/// §4.6.11.3.2 c) — offset of short window 0 inside the 2048-sample +/// frame window region: `(N_l − N_s)/4 = 448`. +pub(crate) const SHORT_SEQ_START: usize = (N_L - N_S) / 4; + +/// §4.6.11.3.2 c) — hop between successive short windows: +/// `N_s/2 = 128`. +pub(crate) const SHORT_SEQ_HOP: usize = N_S / 2; + +/// Modified Bessel function of the first kind, order 0, via its power +/// series `I0(x) = Σ_k ((x/2)^k / k!)^2` (§4.6.11.3.2). The series +/// converges quickly for the `x = π·α` arguments the KBD window uses +/// (`α ∈ {4, 6}`), so a fixed term cap with an early-out on negligible +/// terms is exact to f64 precision. +fn bessel_i0(x: f64) -> f64 { + let half_x = x / 2.0; + let mut term = 1.0f64; // k = 0 term: (half_x^0 / 0!)^2 = 1 + let mut sum = 1.0f64; + let mut k = 1.0f64; + loop { + // term_k = term_{k-1} · (half_x / k)^2 + term *= (half_x / k) * (half_x / k); + sum += term; + if term <= sum * 1e-18 { + break; + } + k += 1.0; + if k > 256.0 { + break; + } + } + sum +} + +/// §4.6.11.3.2 — the Kaiser-Bessel kernel +/// `W'(n, α) = I0(π·α·sqrt(1 − ((n − N/4)/(N/4))^2)) / I0(π·α)` +/// for `0 ≤ n ≤ N/2`, evaluated over `0..=half` (`half = N/2`). +fn kbd_kernel(half: usize, alpha: f64) -> Vec { + let quarter = half as f64 / 2.0; // N/4 + let denom = bessel_i0(core::f64::consts::PI * alpha); + (0..=half) + .map(|n| { + let t = (n as f64 - quarter) / quarter; + let radicand = (1.0 - t * t).max(0.0); + bessel_i0(core::f64::consts::PI * alpha * radicand.sqrt()) / denom + }) + .collect() +} + +/// §4.6.11.3.2 — the left half of the KBD window: +/// `W_KBD_LEFT(n) = sqrt( Σ_{p=0..n} W'(p) / Σ_{p=0..N/2} W'(p) )` +/// for `0 ≤ n < N/2`. Returns the `half = N/2` left-half samples. +/// +/// `alpha` is 4 for the long transform and 6 for the short transform. +fn kbd_left(half: usize, alpha: f64) -> Vec { + let kernel = kbd_kernel(half, alpha); + let total: f64 = kernel.iter().sum(); + let mut running = 0.0f64; + let mut out = Vec::with_capacity(half); + for &w in kernel.iter().take(half) { + running += w; + out.push((running / total).sqrt()); + } + out +} + +/// §4.6.11.3.2 — the sine window left half +/// `W_SIN_LEFT(n) = sin((π/N)·(n + 1/2))`, `0 ≤ n < N/2`. Returns the +/// `half = N/2` samples. +fn sine_left(half: usize) -> Vec { + let n_transform = (2 * half) as f64; + (0..half) + .map(|n| (core::f64::consts::PI / n_transform * (n as f64 + 0.5)).sin()) + .collect() +} + +/// One transform's analysis/synthesis window halves, each `half = N/2` +/// long. The right half of a sine/KBD window is the mirror of its +/// left half (`W_RIGHT(n) = W_LEFT(N − 1 − n)`), so we store left +/// halves and index the right half by mirror at apply time. +struct WindowHalves { + /// Left half, indices `0..half`. + left: Vec, + /// Right half, indices `0..half`; element `m` is the window value + /// at transform position `half + m`. + right: Vec, +} + +/// §4.6.17.2.3 Table 4.171 — the ER AAC LD *low-overlap* window's +/// left half. Over the full length-`N` window: +/// +/// ```text +/// W(i) = 0 i in [0, 3N/16) +/// sin(π(i − 3N/16 + 0.5) / (N/4)) i in [3N/16, 5N/16) +/// 1 i in [5N/16, 11N/16) +/// sin(π(i − 9N/16 + 0.5) / (N/4)) i in [11N/16, 13N/16) +/// 0 i in [13N/16, N) +/// ``` +/// +/// The two sine segments' arguments sum to π at mirrored positions +/// (`i` and `N − 1 − i`), so the right half is the exact spatial +/// mirror of this left half — the same mirror convention every other +/// window shape uses — and the TDAC partners inside the rise region +/// have arguments summing to π/2, making the window +/// power-complementary (`sin² + cos² = 1`), as §4.6.11.3.2 requires +/// for perfect reconstruction. +fn low_overlap_left(half: usize) -> Vec { + let n = 2 * half; // full window length N (1024 or 960) + let rise_start = 3 * n / 16; + let rise_end = 5 * n / 16; + let quarter = n as f64 / 4.0; + (0..half) + .map(|i| { + if i < rise_start { + 0.0 + } else if i < rise_end { + (core::f64::consts::PI * (i as f64 - rise_start as f64 + 0.5) / quarter).sin() + } else { + 1.0 + } + }) + .collect() +} + +/// Which window family the `window_shape` bit selects between — +/// §4.6.11.3.2 (sine / KBD) for the general families, §4.6.17.2.3 +/// Table 4.171 (sine / low-overlap) for ER AAC LD. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum WindowStyle { + /// `window_shape == 1` selects the Kaiser-Bessel-derived window. + Standard, + /// `window_shape == 1` selects the §4.6.17.2.3 low-overlap + /// window (ER AAC LD). + LowDelay, +} + +impl WindowStyle { + /// The style a [`FrameFamily`] mandates. + pub(crate) fn for_family(family: FrameFamily) -> Self { + if family.is_ld() { + WindowStyle::LowDelay + } else { + WindowStyle::Standard + } + } +} + +/// Build the left half for the requested `shape` at transform length +/// `n_transform` under a [`WindowStyle`]. +/// +/// The KBD kernel alpha follows the transform's *role*: the long +/// transform of a family uses `α = 4`, the short transform `α = 6`. +/// §4.6.11.3.2 states this for the 2048/256 (1920/240) family; for the +/// SSR 512/64 family the same pair reproduces the normative +/// Table 4.A.14 / Table 4.A.13 window listings (each printed value +/// matches the α = 4 / α = 6 running-sum construction to the tables' +/// print precision — pinned by the `ssr_kbd_*` tests below). Under +/// [`WindowStyle::LowDelay`] the `window_shape == 1` bit selects the +/// §4.6.17.2.3 low-overlap window instead of KBD. +fn half_window_style(n_transform: usize, shape: WindowShape, style: WindowStyle) -> Vec { + let half = n_transform / 2; + match (shape, style) { + (WindowShape::Sine, _) => sine_left(half), + (WindowShape::Kbd, WindowStyle::LowDelay) => low_overlap_left(half), + (WindowShape::Kbd, WindowStyle::Standard) => { + let alpha = match n_transform { + // Long transforms: 2048 (1920) per §4.6.11.3.2; 512 per + // the Table 4.A.14 SSR window fit. + 2048 | 1920 | 512 => 4.0, + // Short transforms: 256 (240) per §4.6.11.3.2; 64 per + // the Table 4.A.13 SSR window fit. + _ => 6.0, + }; + kbd_left(half, alpha) + } + } +} + +/// §4.6.11.3.2 — assemble a transform's window from a `left` shape +/// (inherited from the previous block) and a `right` shape (this +/// block's `window_shape`). For a sine/KBD window the right half is +/// the spatial mirror of that shape's *left* half, so we build the +/// `right`-shape left half and reverse it. +fn window_halves( + n_transform: usize, + left_shape: WindowShape, + right_shape: WindowShape, +) -> WindowHalves { + window_halves_style(n_transform, left_shape, right_shape, WindowStyle::Standard) +} + +/// [`window_halves`] with an explicit [`WindowStyle`]. +fn window_halves_style( + n_transform: usize, + left_shape: WindowShape, + right_shape: WindowShape, + style: WindowStyle, +) -> WindowHalves { + let left = half_window_style(n_transform, left_shape, style); + let mut right = half_window_style(n_transform, right_shape, style); + right.reverse(); + WindowHalves { left, right } +} + +/// The stateful per-channel §4.6.11 filterbank. One instance per +/// decoded channel; [`Filterbank::synthesize`] is called once per +/// frame and carries the overlap-add tail (`z[i-1][n + N/2]`) plus the +/// previous block's `window_shape` (which determines the left-half +/// shape of the next block, §4.6.11.3.2) across calls. +#[derive(Clone, Debug)] +pub struct Filterbank { + /// The §4.5.1.1 frame-length family this filterbank synthesizes + /// (transform lengths, overlap length, and — for LD — the + /// §4.6.17.2.3 window style). Fixed at construction; a frame + /// whose `ics_info.family` disagrees is rejected. + family: FrameFamily, + /// `z[i-1][N/2 .. N]` — the right half of the previous frame's + /// windowed time signal, added to the left half of this frame's + /// windowed signal (§4.6.11.3.3). `family.frame_len()` long. + overlap: Vec, + /// `window_shape` of the previous block, governing the left-half + /// window shape of the next block. [`None`] before the first + /// frame: per §4.6.11.3.2 the first block's left and right halves + /// share its own `window_shape`. + prev_shape: Option, +} + +impl Default for Filterbank { + fn default() -> Self { + Self::new() + } +} + +impl Filterbank { + /// A fresh filterbank with a zeroed overlap buffer and no + /// previous-block shape (so the first frame uses its own + /// `window_shape` for both halves, per §4.6.11.3.2). + pub fn new() -> Self { + Self::new_family(FrameFamily::Lc1024) + } + + /// A fresh filterbank for an arbitrary §4.5.1.1 [`FrameFamily`]: + /// the 1024 / 960 block-switching families or the long-only LD + /// 512 / 480 families (whose `window_shape == 1` selects the + /// §4.6.17.2.3 low-overlap window in place of KBD). + pub fn new_family(family: FrameFamily) -> Self { + Filterbank { + family, + overlap: vec![0.0f64; family.frame_len()], + prev_shape: None, + } + } + + /// The [`FrameFamily`] this filterbank was constructed for. + pub fn family(&self) -> FrameFamily { + self.family + } + + /// §4.6.7.3 — the current frame's *aliased half window* + /// `x_rec(0 … N/2 − 1)`: the right half of the just-synthesized + /// frame's windowed (pre-overlap-add) time signal `z[i][N/2 … N]`. + /// + /// After a [`Self::synthesize`] call the internal overlap buffer + /// holds exactly this tail (it is reused as the *next* frame's + /// overlap-add term, §4.6.11.3.3). The LTP reconstruction history + /// ([`crate::ltp::LtpState`]) needs the same vector — its + /// `x_rec(0 … N/2 − 1)` region — so the element driver reads it here + /// after each synthesis and feeds it to + /// [`crate::ltp::LtpState::push_frame`]. Before the first frame this + /// is the zero buffer, matching the §4.6.7.3 zero initialisation. + pub fn aliased_tail(&self) -> &[f64] { + &self.overlap + } + + /// §4.6.11.3.2 — the previous block's `window_shape`, which governs + /// the left-half shape of the *next* block's analysis/synthesis + /// window. [`None`] before the first frame (the first block uses its + /// own shape for both halves). + /// + /// The §4.6.7.4.1 LTP analysis MDCT must window `x_est` with the + /// same composite long window the filterbank uses for this frame, so + /// the element driver reads the previous shape here before + /// synthesizing. + pub fn prev_shape(&self) -> Option { + self.prev_shape + } + + /// §4.6.11 — synthesize one frame of `LONG_WINDOW_LEN` (1024) PCM + /// samples from `spec`, the window-major decoded spectrum produced + /// by [`crate::decoded_spectrum::decode_channel_spectrum`]. + /// + /// `spec` must be: + /// + /// * `LONG_WINDOW_LEN` (1024) coefficients for `ONLY_LONG`, + /// `LONG_START`, `LONG_STOP`; + /// * `8 × SHORT_WINDOW_LEN` (1024 total) for `EIGHT_SHORT`, + /// laid out window-major: window `w` at `spec[w * 128 ..]`. + /// + /// The result is the §4.6.11.3.3 overlap-added output; the method + /// updates the internal overlap tail and previous-block shape for + /// the next call. + /// + /// Errors: [`Error::FilterbankInvalid`] if `spec.len()` disagrees + /// with `ics_info.window_sequence`. + pub fn synthesize(&mut self, spec: &[f64], ics_info: &IcsInfo) -> Result> { + if ics_info.family != self.family { + return Err(Error::FilterbankInvalid); + } + let z = self.windowed_signal(spec, ics_info)?; + debug_assert_eq!(z.len(), self.family.long_transform_len()); + + // §4.6.11.3.3 overlap-add: out[n] = z[i][n] + z[i-1][n + N/2]. + let half = self.family.frame_len(); + let out: Vec = z[..half] + .iter() + .zip(self.overlap.iter()) + .map(|(&zn, &on)| zn + on) + .collect(); + + // Retain z[i][N/2 .. N] as next frame's z[i-1][n + N/2]. + self.overlap.clear(); + self.overlap.extend_from_slice(&z[half..]); + + // §4.6.11.3.2: the left-half shape of the *next* block is this + // block's window_shape. + self.prev_shape = Some(ics_info.window_shape); + Ok(out) + } + + /// §4.6.11.3.1 + §4.6.11.3.2 — produce the full-length (`N_l = + /// 2048`) windowed time signal `z[i][n]` for this frame, before + /// the inter-block overlap-add. Dispatches on `window_sequence`. + fn windowed_signal(&self, spec: &[f64], ics_info: &IcsInfo) -> Result> { + let left_shape = self.prev_shape.unwrap_or(ics_info.window_shape); + let right_shape = ics_info.window_shape; + match ics_info.window_sequence { + WindowSequence::OnlyLong => { + self.long_windowed(spec, left_shape, right_shape, LongKind::OnlyLong) + } + WindowSequence::LongStart => { + self.long_windowed(spec, left_shape, right_shape, LongKind::Start) + } + WindowSequence::LongStop => { + self.long_windowed(spec, left_shape, right_shape, LongKind::Stop) + } + WindowSequence::EightShort => self.short_windowed(spec, left_shape, right_shape), + } + } + + /// §4.6.11.3.2 a)/b)/d) — the three long-transform sequences. Each + /// runs a single length-2048 IMDCT and applies a composite window + /// whose left half (`ONLY_LONG`, `LONG_START`) or right half + /// (`LONG_STOP`) is the full long half-window, and whose other + /// half is shaped by the start/stop transition (a short half-window + /// flanked by a flat `1.0` plateau and a zero region). + fn long_windowed( + &self, + spec: &[f64], + left_shape: WindowShape, + right_shape: WindowShape, + kind: LongKind, + ) -> Result> { + if spec.len() != self.family.frame_len() { + return Err(Error::FilterbankInvalid); + } + let x = imdct(spec, self.family.long_transform_len()); + let w = self.long_window(left_shape, right_shape, kind)?; + let z: Vec = x.iter().zip(w.iter()).map(|(&xv, &wv)| xv * wv).collect(); + Ok(z) + } + + /// §4.6.11.3.2 — assemble the length-2048 window vector for a + /// long-transform sequence. + /// + /// * `OnlyLong` (a): `[W_LEFT_l | W_RIGHT_l]`. + /// * `Start` (b): left half is `W_LEFT_l`; the right half is a + /// flat `1.0` plateau over `[N_l/2, (3N_l − N_s)/4)`, the short + /// right half-window over `[(3N_l − N_s)/4, (3N_l + N_s)/4)`, and + /// `0.0` over `[(3N_l + N_s)/4, N_l)`. + /// * `Stop` (d): the left half is `0.0` over `[0, (N_l − N_s)/4)`, + /// the short left half-window over `[(N_l − N_s)/4, (N_l + + /// N_s)/4)`, and a flat `1.0` plateau over `[(N_l + N_s)/4, + /// N_l/2)`; the right half is `W_RIGHT_l`. + fn long_window( + &self, + left_shape: WindowShape, + right_shape: WindowShape, + kind: LongKind, + ) -> Result> { + let n_l = self.family.long_transform_len(); + match self.family.short_transform_len() { + Some(n_s) => Ok(build_long_window_style( + n_l, + n_s, + left_shape, + right_shape, + kind, + WindowStyle::for_family(self.family), + )), + // LD: long-only — Start / Stop transitions do not exist + // (§4.6.17.2.2), so only the OnlyLong composite is legal. + None => match kind { + LongKind::OnlyLong => { + let halves = + window_halves_style(n_l, left_shape, right_shape, WindowStyle::LowDelay); + let half_l = n_l / 2; + let mut w = vec![0.0f64; n_l]; + w[..half_l].copy_from_slice(&halves.left); + for (m, &rv) in halves.right.iter().enumerate() { + w[half_l + m] = rv; + } + Ok(w) + } + _ => Err(Error::LdShortWindow), + }, + } + } +} + +/// §4.6.11.3.2 — [`build_long_window`] generalized to an arbitrary +/// `(n_l, n_s)` transform family; every breakpoint is the spec's +/// `N_l`/`N_s` expression evaluated at the caller's lengths (the +/// standard family passes `(2048, 256)`, the SSR §4.6.12.1 per-band +/// family `(512, 64)`). +fn build_long_window_n( + n_l: usize, + n_s: usize, + left_shape: WindowShape, + right_shape: WindowShape, + kind: LongKind, +) -> Vec { + build_long_window_style( + n_l, + n_s, + left_shape, + right_shape, + kind, + WindowStyle::Standard, + ) +} + +/// [`build_long_window_n`] with an explicit [`WindowStyle`] (the LD +/// families map `window_shape == 1` to the §4.6.17.2.3 low-overlap +/// window; the LD long-only path never reaches the Start / Stop +/// composites, but the parameterization keeps the construction +/// uniform). +fn build_long_window_style( + n_l: usize, + n_s: usize, + left_shape: WindowShape, + right_shape: WindowShape, + kind: LongKind, + style: WindowStyle, +) -> Vec { + let long = window_halves_style(n_l, left_shape, right_shape, style); + let short = window_halves_style(n_s, left_shape, right_shape, style); + let half_l = n_l / 2; + let mut w = vec![0.0f64; n_l]; + + // Left half is always the plain long left half for OnlyLong / + // Start; Stop replaces it with the start-transition mirror. + match kind { + LongKind::OnlyLong | LongKind::Start => { + w[..half_l].copy_from_slice(&long.left); + } + LongKind::Stop => { + // 0.0 over [0, (N_l − N_s)/4); short left half over + // [(N_l − N_s)/4, (N_l + N_s)/4); 1.0 over + // [(N_l + N_s)/4, N_l/2). + let a = (n_l - n_s) / 4; + for (m, &sv) in short.left.iter().enumerate() { + w[a + m] = sv; + } + for slot in w.iter_mut().take(half_l).skip(a + n_s / 2) { + *slot = 1.0; + } + } + } + + match kind { + LongKind::OnlyLong => { + for (m, &rv) in long.right.iter().enumerate() { + w[half_l + m] = rv; + } + } + LongKind::Start => { + // 1.0 over [N_l/2, (3N_l − N_s)/4); short right half + // over [(3N_l − N_s)/4, (3N_l + N_s)/4); 0.0 after. + let b = (3 * n_l - n_s) / 4; + for slot in w.iter_mut().take(b).skip(half_l) { + *slot = 1.0; + } + for (m, &rv) in short.right.iter().enumerate() { + w[b + m] = rv; + } + // [(3N_l + N_s)/4, N_l) stays 0.0 from the vec init. + } + LongKind::Stop => { + for (m, &rv) in long.right.iter().enumerate() { + w[half_l + m] = rv; + } + } + } + w +} + +impl Filterbank { + /// §4.6.11.3.2 c) — the `EIGHT_SHORT` sequence: eight length-256 + /// IMDCTs, each windowed with a short window, then overlapped and + /// added into the 2048-sample frame with leading/trailing zeros. + /// + /// Window-shape inheritance (§4.6.11.3.2): the *first* short + /// window's left half uses the previous block's shape; every + /// later short window's left half — and every short window's right + /// half — uses this block's `window_shape`. + fn short_windowed( + &self, + spec: &[f64], + left_shape: WindowShape, + right_shape: WindowShape, + ) -> Result> { + let n_s = self + .family + .short_transform_len() + .ok_or(Error::LdShortWindow)?; + let n_l = self.family.long_transform_len(); + let short_len = n_s / 2; // 128 (120) + if spec.len() != NUM_SHORT_WINDOWS * short_len { + return Err(Error::FilterbankInvalid); + } + + // Per-window windowed length-N_s time signals. + let mut windowed: Vec> = Vec::with_capacity(NUM_SHORT_WINDOWS); + for j in 0..NUM_SHORT_WINDOWS { + let coeffs = &spec[j * short_len..(j + 1) * short_len]; + let x = imdct(coeffs, n_s); + // W_0 left half inherits the previous block's shape; all + // other windows' left halves use this block's shape. + let this_left = if j == 0 { left_shape } else { right_shape }; + let halves = window_halves(n_s, this_left, right_shape); + let mut z = vec![0.0f64; n_s]; + for n in 0..n_s / 2 { + z[n] = x[n] * halves.left[n]; + } + for n in n_s / 2..n_s { + z[n] = x[n] * halves.right[n - n_s / 2]; + } + windowed.push(z); + } + + // §4.6.11.3.2 c) overlap-add of the eight short windows into a + // N_l-sample frame. Short window `j` starts at offset + // `(N_l − N_s)/4 + j·N_s/2` (each successive short window is + // hopped by N_s/2 = 128 (120) samples) — the spec's piecewise + // z_{i,n} is exactly this 50%-overlap-add with the first + // window placed at (N_l − N_s)/4 = 448 (420). + let mut z = vec![0.0f64; n_l]; + let start = (n_l - n_s) / 4; // 448 (420) + let hop = n_s / 2; // 128 (120) + for (j, win) in windowed.iter().enumerate() { + let base = start + j * hop; + for (n, &v) in win.iter().enumerate() { + z[base + n] += v; + } + } + Ok(z) + } +} + +/// Discriminates the three long-transform `window_sequence` shapes +/// inside [`Filterbank::long_window`]. +#[derive(Clone, Copy)] +enum LongKind { + OnlyLong, + Start, + Stop, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::IcsInfo; + + fn long_info(shape: WindowShape, seq: WindowSequence) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: seq, + window_shape: shape, + max_sfb: 49, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: 49, + } + } + + fn short_info(shape: WindowShape) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: shape, + max_sfb: 14, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups: 8, + window_group_length: vec![1; 8], + num_swb: 14, + } + } + + #[test] + fn sine_window_endpoints() { + // W_SIN_LEFT(n) = sin((π/N)(n + 1/2)); for N = 2048 the first + // sample is sin(π·0.5/2048) and the last left-half sample is + // sin(π·1023.5/2048) ≈ sin(π/2 · 0.9995…). + let left = sine_left(1024); + assert_eq!(left.len(), 1024); + let expect0 = (core::f64::consts::PI * 0.5 / 2048.0).sin(); + assert!((left[0] - expect0).abs() < 1e-15); + // The window rises monotonically to ~1.0 at the centre. + assert!(left[1023] > 0.9999 && left[1023] <= 1.0); + for w in 1..1024 { + assert!(left[w] > left[w - 1]); + } + } + + #[test] + fn sine_window_unit_power_overlap() { + // The sine window satisfies the Princen-Bradley condition: + // W(n)^2 + W(n + N/2)^2 = 1 for a symmetric sine window. Build + // a full OnlyLong sine window and check the squared-sum of the + // overlapping halves is 1. + let half = sine_left(1024); + for n in 0..1024 { + // Right half mirrors the left: W(N-1-n) = W_left(n). + let wl = half[n]; + let wr = half[1023 - n]; // W(1024 + n) = W_left(1023 - n) + let s = wl * wl + wr * wr; + assert!((s - 1.0).abs() < 1e-12, "n={n} sum={s}"); + } + } + + #[test] + fn kbd_window_unit_power_overlap() { + // The KBD window is constructed precisely so that + // W(n)^2 + W(n + N/2)^2 = 1 (it is the canonical + // perfect-reconstruction window). Verify against the long α=4 + // KBD window. + let left = kbd_left(1024, 4.0); + assert_eq!(left.len(), 1024); + for n in 0..1024 { + let wl = left[n]; + let wr = left[1023 - n]; + let s = wl * wl + wr * wr; + assert!((s - 1.0).abs() < 1e-12, "n={n} sum={s}"); + } + // KBD is monotonically increasing on its left half. + for n in 1..1024 { + assert!(left[n] >= left[n - 1]); + } + } + + #[test] + fn bessel_i0_known_values() { + // I0(0) = 1; I0(1) ≈ 1.2660658777520084; + // I0(2) ≈ 2.2795853023360673 (standard tabulated values). + assert!((bessel_i0(0.0) - 1.0).abs() < 1e-15); + assert!((bessel_i0(1.0) - 1.266_065_877_752_008_4).abs() < 1e-12); + assert!((bessel_i0(2.0) - 2.279_585_302_336_067_3).abs() < 1e-12); + } + + #[test] + fn imdct_dc_coefficient() { + // A single non-zero spec[0] is a pure cosine basis function. + // For N=8, half=4, n0=(4+1)/2=2.5: x[n] = (2/8)·cos((2π/8)(n+2.5)(0.5)). + let n = 8usize; + let spec = [1.0, 0.0, 0.0, 0.0]; + let x = imdct(&spec, n); + let scale = 2.0 / 8.0; + let n0 = 2.5; + for (idx, &xv) in x.iter().enumerate() { + let expect = + scale * (2.0 * core::f64::consts::PI / 8.0 * (idx as f64 + n0) * 0.5).cos(); + assert!((xv - expect).abs() < 1e-15, "n={idx}"); + } + } + + /// Time-domain aliasing cancellation (TDAC): for a windowed MDCT/ + /// IMDCT pair, two consecutive identical frames overlap-add to + /// reconstruct the windowed input exactly in the steady state. We + /// drive the filterbank with the production analysis [`forward_mdct`] + /// of a known signal and confirm perfect reconstruction over the + /// second frame. (The analysis/synthesis pair is unity for a + /// power-complementary §4.6.11.3.2 window.) + use super::forward_mdct; + + /// The full symmetric (sine) `OnlyLong` window, length `N`. + fn long_sine_window() -> Vec { + let left = sine_left(1024); + let mut w = vec![0.0; LONG_TRANSFORM_LEN]; + w[..1024].copy_from_slice(&left); + for m in 0..1024 { + w[1024 + m] = left[1023 - m]; + } + w + } + + #[test] + fn tdac_perfect_reconstruction_sine_long() { + // Streaming time-domain aliasing cancellation. A long input is + // analysed by a 50%-overlap forward MDCT (analysis window = + // sine), each frame carried through the decoder's IMDCT + + // synthesis window + overlap-add. For a power-complementary + // window the central frames reconstruct the input exactly. + // + // The forward analysis used here is the transpose of the + // decoder's §4.6.11.3.1 IMDCT basis with NO scale (the IMDCT + // carries the 2/N), so the analysis/synthesis pair satisfies + // TDAC for the sine window. + let n = LONG_TRANSFORM_LEN; // 2048 + let hop = n / 2; // 1024 + let win = long_sine_window(); + + // A long deterministic input; reconstruct the central hop. + let total = 5 * hop; + let input: Vec = (0..total) + .map(|i| (0.013 * i as f64).sin() + 0.5 * (0.07 * i as f64).cos()) + .collect(); + + let info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); + let mut fb = Filterbank::new(); + + // Run four overlapping analysis frames (starts 0, 1024, 2048, + // 3072), feeding each frame's MDCT to the filterbank. Collect + // the decoder's per-frame outputs. + let mut outputs = Vec::new(); + for f in 0..4 { + let base = f * hop; + let frame: Vec = (0..n) + .map(|m| { + let idx = base + m; + if idx < total { + input[idx] * win[m] + } else { + 0.0 + } + }) + .collect(); + let spec = forward_mdct(&frame, n); + outputs.push(fb.synthesize(&spec, &info).unwrap()); + } + + // The decoder output for frame f covers input samples + // [f·hop, f·hop + hop). The steady-state frames f = 1, 2 + // reconstruct the input (their window region is fully covered + // by both the analysis-window taper and the overlap from the + // neighbouring frames). + for (f, out) in outputs.iter().enumerate().take(3).skip(1) { + let base = f * hop; + for k in 0..hop { + let recon = out[k]; + let expect = input[base + k]; + assert!( + (recon - expect).abs() < 1e-9, + "frame={f} k={k} recon={recon} expect={expect}" + ); + } + } + } + + /// Family-parameterized streaming TDAC harness: analyse a + /// deterministic input with the 50%-overlap forward MDCT under + /// the family's own long window, run the decoder filterbank, and + /// require exact reconstruction on the steady-state frames. + fn tdac_long_family(family: crate::swb_offset::FrameFamily, shape: WindowShape) { + let n = family.long_transform_len(); + let hop = n / 2; + let style = WindowStyle::for_family(family); + let win = { + let left = half_window_style(n, shape, style); + let mut w = vec![0.0; n]; + w[..hop].copy_from_slice(&left); + for m in 0..hop { + w[hop + m] = left[hop - 1 - m]; + } + w + }; + let total = 5 * hop; + let input: Vec = (0..total) + .map(|i| (0.017 * i as f64).sin() + 0.4 * (0.043 * i as f64).cos()) + .collect(); + let mut info = long_info(shape, WindowSequence::OnlyLong); + info.family = family; + info.num_swb = 40; // geometry-irrelevant here + let mut fb = Filterbank::new_family(family); + let mut outputs = Vec::new(); + for f in 0..4 { + let base = f * hop; + let frame: Vec = (0..n) + .map(|m| { + let idx = base + m; + if idx < total { + input[idx] * win[m] + } else { + 0.0 + } + }) + .collect(); + let spec = forward_mdct(&frame, n); + let out = fb.synthesize(&spec, &info).unwrap(); + assert_eq!(out.len(), family.frame_len()); + outputs.push(out); + } + for (f, out) in outputs.iter().enumerate().take(3).skip(1) { + let base = f * hop; + for k in 0..hop { + assert!( + (out[k] - input[base + k]).abs() < 1e-9, + "{:?} {:?} frame={f} k={k}", + family, + shape + ); + } + } + } + + #[test] + fn tdac_lc960_sine_and_kbd() { + tdac_long_family(crate::swb_offset::FrameFamily::Lc960, WindowShape::Sine); + tdac_long_family(crate::swb_offset::FrameFamily::Lc960, WindowShape::Kbd); + } + + #[test] + fn tdac_ld512_sine_and_low_overlap() { + // Under the LD families the window_shape == 1 bit selects the + // §4.6.17.2.3 low-overlap window (Table 4.171). + tdac_long_family(crate::swb_offset::FrameFamily::Ld512, WindowShape::Sine); + tdac_long_family(crate::swb_offset::FrameFamily::Ld512, WindowShape::Kbd); + } + + #[test] + fn tdac_ld480_sine_and_low_overlap() { + tdac_long_family(crate::swb_offset::FrameFamily::Ld480, WindowShape::Sine); + tdac_long_family(crate::swb_offset::FrameFamily::Ld480, WindowShape::Kbd); + } + + #[test] + fn low_overlap_window_regions_and_pr() { + // §4.6.17.2.3: zeros over [0, 3N/16), sine rise over + // [3N/16, 5N/16), flat 1.0 over [5N/16, N/2) on the left + // half; power-complementary at the TDAC partners. + for n in [1024usize, 960] { + let half = n / 2; + let left = low_overlap_left(half); + assert_eq!(left.len(), half); + for (i, &v) in left.iter().enumerate().take(3 * n / 16) { + assert_eq!(v, 0.0, "N={n} i={i}"); + } + for (i, &v) in left.iter().enumerate().take(half).skip(5 * n / 16) { + assert_eq!(v, 1.0, "N={n} i={i}"); + } + // Monotone rise inside [3N/16, 5N/16). + for i in 3 * n / 16 + 1..5 * n / 16 { + assert!(left[i] > left[i - 1], "N={n} i={i}"); + } + // Princen-Bradley: W(n)² + W(N/2−1−n)² = 1 over the half. + for i in 0..half { + let s = left[i] * left[i] + left[half - 1 - i] * left[half - 1 - i]; + assert!((s - 1.0).abs() < 1e-12, "N={n} i={i} s={s}"); + } + } + } + + #[test] + fn ld_filterbank_rejects_non_only_long() { + use crate::swb_offset::FrameFamily; + let mut fb = Filterbank::new_family(FrameFamily::Ld512); + let mut info = long_info(WindowShape::Sine, WindowSequence::LongStart); + info.family = FrameFamily::Ld512; + let spec = vec![0.0; 512]; + assert!(matches!( + fb.synthesize(&spec, &info), + Err(Error::LdShortWindow) + )); + } + + #[test] + fn family_mismatch_rejected() { + use crate::swb_offset::FrameFamily; + let mut fb = Filterbank::new_family(FrameFamily::Lc1024); + let mut info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); + info.family = FrameFamily::Lc960; + let spec = vec![0.0; 960]; + assert!(matches!( + fb.synthesize(&spec, &info), + Err(Error::FilterbankInvalid) + )); + } + + #[test] + fn eight_short_lc960_tdac() { + // The 960-family EIGHT_SHORT: 8 × 120-line windows (240-point + // transforms) at start 420, hop 120. A steady sine input + // through forward-MDCT analysis per short window must + // reconstruct inside the fully-overlapped interior region of + // the frame's central section. + use crate::swb_offset::FrameFamily; + let family = FrameFamily::Lc960; + let n_s = 240usize; + let hop = 120usize; + let start = 420usize; + let win = { + let left = half_window_style(n_s, WindowShape::Sine, WindowStyle::Standard); + let mut w = vec![0.0; n_s]; + w[..hop].copy_from_slice(&left); + for m in 0..hop { + w[hop + m] = left[hop - 1 - m]; + } + w + }; + // Input signal over the frame's 1920-sample window region. + let input: Vec = (0..1920).map(|i| (0.05 * i as f64).sin() * 0.7).collect(); + // Analyse the eight short windows. + let mut spec = Vec::with_capacity(8 * hop); + for j in 0..8 { + let base = start + j * hop; + let frame: Vec = (0..n_s).map(|m| input[base + m] * win[m]).collect(); + spec.extend(forward_mdct(&frame, n_s)); + } + let mut info = short_info(WindowShape::Sine); + info.family = family; + info.num_swb = 14; + let mut fb = Filterbank::new_family(family); + // Prime the overlap with the previous frame's tail = zeros; the + // first output frame covers window-region samples [0, 960). + let out = fb.synthesize(&spec, &info).unwrap(); + assert_eq!(out.len(), 960); + // Interior of the short-window train that lands in the first + // output half: [start + hop, 960) = [540, 960) is covered by + // two overlapping short windows each (TDAC-complete). + for k in 540..960 { + assert!( + (out[k] - input[k]).abs() < 1e-9, + "k={k} out={} in={}", + out[k], + input[k] + ); + } + } + + #[test] + fn tdac_perfect_reconstruction_kbd_long() { + // Same streaming TDAC check with the KBD (α=4) long window. + let n = LONG_TRANSFORM_LEN; + let hop = n / 2; + let win = { + let left = kbd_left(1024, 4.0); + let mut w = vec![0.0; n]; + w[..1024].copy_from_slice(&left); + for m in 0..1024 { + w[1024 + m] = left[1023 - m]; + } + w + }; + let total = 5 * hop; + let input: Vec = (0..total) + .map(|i| 0.3 * (0.02 * i as f64).cos() - 0.6 * (0.05 * i as f64).sin()) + .collect(); + let info = long_info(WindowShape::Kbd, WindowSequence::OnlyLong); + let mut fb = Filterbank::new(); + let mut outputs = Vec::new(); + for f in 0..4 { + let base = f * hop; + let frame: Vec = (0..n) + .map(|m| { + let idx = base + m; + if idx < total { + input[idx] * win[m] + } else { + 0.0 + } + }) + .collect(); + let spec = forward_mdct(&frame, n); + outputs.push(fb.synthesize(&spec, &info).unwrap()); + } + for (f, out) in outputs.iter().enumerate().take(3).skip(1) { + let base = f * hop; + for k in 0..hop { + assert!((out[k] - input[base + k]).abs() < 1e-9, "frame={f} k={k}"); + } + } + } + + #[test] + fn eight_short_internal_tdac() { + // §4.6.11.3.2 c): the eight short windows overlap-add inside + // the frame with a 128-sample hop, the first window placed at + // offset (N_l − N_s)/4 = 448. Drive the eight short MDCTs from + // a streaming short-window analysis of a continuous input and + // confirm the frame's interior reconstructs that input over + // the fully-overlapped central short windows. + let n_s = SHORT_TRANSFORM_LEN; // 256 + let hop = n_s / 2; // 128 + let sine_short = { + let left = sine_left(hop); + let mut w = vec![0.0; n_s]; + w[..hop].copy_from_slice(&left); + for m in 0..hop { + w[hop + m] = left[hop - 1 - m]; + } + w + }; + // A continuous input long enough to cover all eight short + // windows once placed at start=448, hop=128: last window starts + // at 448 + 7·128 = 1344, ends at 1600. + let total = N_L; + let input: Vec = (0..total) + .map(|i| (0.05 * i as f64).sin() + 0.4 * (0.11 * i as f64).cos()) + .collect(); + let start = (N_L - N_S) / 4; // 448 + + // Build the eight short windows' MDCTs from the windowed input + // segments at the same offsets the decoder overlaps them. + let mut spec = Vec::with_capacity(NUM_SHORT_WINDOWS * SHORT_WINDOW_LEN as usize); + for j in 0..NUM_SHORT_WINDOWS { + let base = start + j * hop; + let seg: Vec = (0..n_s).map(|m| input[base + m] * sine_short[m]).collect(); + let s = forward_mdct(&seg, n_s); + spec.extend_from_slice(&s); + } + + let info = short_info(WindowShape::Sine); + let mut fb = Filterbank::new(); + let out = fb.synthesize(&spec, &info).unwrap(); + + // The output frame is z[0:1024]; overlap with the (zero) prior + // frame leaves the interior intact. The central short windows + // j=1..6 are fully overlapped by their neighbours, so the + // reconstructed signal equals the input over their shared + // central hops: input indices [start + hop, start + 7·hop). + // The decoder output covers input [0, 1024); the short-window + // region [start, 1600) is partly past 1024, so check the + // covered central hops [start+hop, 1024). + for idx in (start + hop)..1024 { + assert!( + (out[idx] - input[idx]).abs() < 1e-9, + "idx={idx} out={} input={}", + out[idx], + input[idx] + ); + } + } + + #[test] + fn synthesize_long_length_and_shape() { + let info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); + let mut fb = Filterbank::new(); + let spec = vec![0.25f64; LONG_WINDOW_LEN as usize]; + let out = fb.synthesize(&spec, &info).unwrap(); + assert_eq!(out.len(), LONG_WINDOW_LEN as usize); + assert!(out.iter().all(|v| v.is_finite())); + } + + #[test] + fn synthesize_eight_short_length() { + let info = short_info(WindowShape::Sine); + let mut fb = Filterbank::new(); + let spec = vec![0.1f64; NUM_SHORT_WINDOWS * SHORT_WINDOW_LEN as usize]; + let out = fb.synthesize(&spec, &info).unwrap(); + assert_eq!(out.len(), LONG_WINDOW_LEN as usize); + assert!(out.iter().all(|v| v.is_finite())); + } + + #[test] + fn synthesize_rejects_wrong_length() { + let info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); + let mut fb = Filterbank::new(); + let spec = vec![0.0f64; 512]; + assert!(matches!( + fb.synthesize(&spec, &info), + Err(Error::FilterbankInvalid) + )); + let sinfo = short_info(WindowShape::Sine); + let mut fb2 = Filterbank::new(); + let bad = vec![0.0f64; 1000]; + assert!(matches!( + fb2.synthesize(&bad, &sinfo), + Err(Error::FilterbankInvalid) + )); + } + + #[test] + fn start_window_plateau_and_zero_regions() { + // LONG_START: left half is the long left window, then a flat + // 1.0 plateau, then the short right half, then zeros. + let fb = Filterbank::new(); + let w = fb + .long_window(WindowShape::Sine, WindowShape::Sine, LongKind::Start) + .unwrap(); + assert_eq!(w.len(), N_L); + // Plateau region [1024, 1472) is all 1.0. + for v in w.iter().take(1472).skip(1024) { + assert!((*v - 1.0).abs() < 1e-15); + } + // Tail [1600, 2048) is all 0.0. (3N_l + N_s)/4 = 1600. + for v in w.iter().take(N_L).skip(1600) { + assert_eq!(*v, 0.0); + } + // The short-right transition [1472, 1600) falls from 1 to 0. + assert!(w[1472] > w[1599]); + } + + #[test] + fn stop_window_zero_and_plateau_regions() { + // LONG_STOP: leading zeros, short left half, 1.0 plateau, then + // the long right window. + let fb = Filterbank::new(); + let w = fb + .long_window(WindowShape::Sine, WindowShape::Sine, LongKind::Stop) + .unwrap(); + assert_eq!(w.len(), N_L); + // Leading [0, 448) zeros. (N_l − N_s)/4 = 448. + for v in w.iter().take(448) { + assert_eq!(*v, 0.0); + } + // Plateau [576, 1024) all 1.0. (N_l + N_s)/4 = 576. + for v in w.iter().take(1024).skip(576) { + assert!((*v - 1.0).abs() < 1e-15); + } + // The short-left transition [448, 576) rises from 0 to 1. + assert!(w[448] < w[575]); + } + + #[test] + fn first_frame_uses_own_shape_for_left_half() { + // Before any frame, prev_shape is None, so the first frame's + // left half uses its own window_shape (KBD here). Confirm the + // left half equals the KBD left window, not the sine one. + let info = long_info(WindowShape::Kbd, WindowSequence::OnlyLong); + let fb = Filterbank::new(); + let w = fb + .windowed_signal(&vec![0.0; LONG_WINDOW_LEN as usize], &info) + .unwrap(); + // All-zero spectrum → zero time signal regardless, so instead + // inspect the window directly. + let _ = w; + let win = fb + .long_window(WindowShape::Kbd, WindowShape::Kbd, LongKind::OnlyLong) + .unwrap(); + let kbd = kbd_left(1024, 4.0); + for n in 0..1024 { + assert!((win[n] - kbd[n]).abs() < 1e-15); + } + } + + /// Table 4.A.13 — the normative Kaiser-Bessel window for the AAC + /// SSR object type `EIGHT_SHORT_SEQUENCE` (`N = 64`): all 32 + /// tabulated left-half values, transcribed from the spec PDF. The + /// running-sum KBD construction with the short-transform `α = 6` + /// reproduces every entry to the table's print precision. + #[test] + fn ssr_kbd_short_window_matches_table_4_a_13() { + // Verbatim table transcription — keep every printed digit, + // including redundant trailing zeros. + #[allow(clippy::excessive_precision)] + const TABLE_4_A_13: [(usize, f64); 32] = [ + (0, 0.0000875914060105), + (1, 0.0009321760265333), + (2, 0.0032114611466596), + (3, 0.0081009893216786), + (4, 0.0171240286619181), + (5, 0.0320720743527833), + (6, 0.0548307856028528), + (7, 0.0871361822564870), + (8, 0.1302923415174603), + (9, 0.1848955425508276), + (10, 0.2506163195331889), + (11, 0.3260874142923209), + (12, 0.4089316830907141), + (13, 0.4959414909423747), + (14, 0.5833939894958904), + (15, 0.6674601983218376), + (16, 0.7446454751465113), + (17, 0.8121892962974020), + (18, 0.8683559394406505), + (19, 0.9125649996381605), + (20, 0.9453396205809574), + (21, 0.9680864942677585), + (22, 0.9827581789763112), + (23, 0.9914756203467121), + (24, 0.9961964092194694), + (25, 0.9984956609571091), + (26, 0.9994855586984285), + (27, 0.9998533730714648), + (28, 0.9999671864476404), + (29, 0.9999948432453556), + (30, 0.9999995655238333), + (31, 0.9999999961638728), + ]; + let left = half_window_style(64, WindowShape::Kbd, WindowStyle::Standard); + assert_eq!(left.len(), 32); + for &(i, expect) in &TABLE_4_A_13 { + assert!( + (left[i] - expect).abs() < 1e-8, + "Table 4.A.13 w({i}): got {} expect {expect}", + left[i] + ); + } + // Discriminator: the long-transform α = 4 does NOT fit. + let alt = kbd_left(32, 4.0); + assert!((alt[0] - TABLE_4_A_13[0].1).abs() > 1e-4); + } + + /// Table 4.A.14 — the normative Kaiser-Bessel window for the SSR + /// object type's other window sequences (`N = 512`): a spread of + /// tabulated left-half values transcribed from the spec PDF. The + /// running-sum KBD construction with the long-transform `α = 4` + /// reproduces each to the table's print precision. + #[test] + fn ssr_kbd_long_window_matches_table_4_a_14() { + // Verbatim table transcription — keep every printed digit, + // including redundant trailing zeros. + #[allow(clippy::excessive_precision)] + const TABLE_4_A_14_SPREAD: [(usize, f64); 15] = [ + (0, 0.0005851230124487), + (1, 0.0009642149851497), + (2, 0.0013558207534965), + (16, 0.0116765080854300), + (32, 0.0405466983507029), + (64, 0.1811734433685097), + (96, 0.4325622561631607), + (128, 0.7110428359000029), + (160, 0.9058173183656508), + (192, 0.9845850806232530), + (224, 0.9992757396582338), + (240, 0.9999442511639580), + (250, 0.9999962619864214), + (254, 0.9999995351446231), + (255, 0.9999998288155155), + ]; + let left = half_window_style(512, WindowShape::Kbd, WindowStyle::Standard); + assert_eq!(left.len(), 256); + for &(i, expect) in &TABLE_4_A_14_SPREAD { + assert!( + (left[i] - expect).abs() < 1e-8, + "Table 4.A.14 w({i}): got {} expect {expect}", + left[i] + ); + } + // Discriminator: the short-transform α = 6 does NOT fit. + let alt = kbd_left(256, 6.0); + assert!((alt[0] - TABLE_4_A_14_SPREAD[0].1).abs() > 1e-4); + } + + /// The generalized `(n_l, n_s)` long-window builder reproduces the + /// standard-family construction exactly, and the SSR family's + /// breakpoints land at the quarter-scaled positions. + #[test] + fn generalized_long_window_matches_standard_and_scales() { + for kind in [LongKind::OnlyLong, LongKind::Start, LongKind::Stop] { + let std = Filterbank::new() + .long_window(WindowShape::Sine, WindowShape::Sine, kind) + .unwrap(); + let gen = build_long_window_n(2048, 256, WindowShape::Sine, WindowShape::Sine, kind); + assert_eq!(std, gen); + } + // SSR LONG_START at (512, 64): 1.0 plateau over [256, 368), + // short descent over [368, 400), zero over [400, 512). + let w = build_long_window_n( + 512, + 64, + WindowShape::Sine, + WindowShape::Sine, + LongKind::Start, + ); + assert_eq!(w.len(), 512); + for v in w.iter().take(368).skip(256) { + assert!((*v - 1.0).abs() < 1e-15); + } + assert!(w[368] < 1.0 && w[368] > w[399]); + for v in w.iter().skip(400) { + assert_eq!(*v, 0.0); + } + // SSR LONG_STOP mirrors: zero over [0, 112), ascent [112, 144), + // plateau [144, 256). + let w = build_long_window_n( + 512, + 64, + WindowShape::Sine, + WindowShape::Sine, + LongKind::Stop, + ); + for v in w.iter().take(112) { + assert_eq!(*v, 0.0); + } + for v in w.iter().take(256).skip(144) { + assert!((*v - 1.0).abs() < 1e-15); + } + } + + /// The SSR-family windows are TDAC power-complementary at every + /// steady overlap: `w(n)² + w(n + N/2)²` over the flanks sums to 1 + /// for the 512 `ONLY_LONG` window (both shapes), which is the + /// §4.6.11 perfect-reconstruction condition the §4.6.12.3.3 + /// per-band overlap relies on. + #[test] + fn ssr_only_long_window_is_power_complementary() { + for shape in [WindowShape::Sine, WindowShape::Kbd] { + let w = build_long_window_n(512, 64, shape, shape, LongKind::OnlyLong); + for n in 0..256 { + let s = w[n] * w[n] + w[n + 256] * w[n + 256]; + assert!( + (s - 1.0).abs() < 1e-10, + "{shape:?} w²({n}) + w²({}) = {s}", + n + 256 + ); + } + } + } +} diff --git a/crates/vendor/oxideav-aac/src/gain_control.rs b/crates/vendor/oxideav-aac/src/gain_control.rs new file mode 100644 index 00000000..e9813adb --- /dev/null +++ b/crates/vendor/oxideav-aac/src/gain_control.rs @@ -0,0 +1,908 @@ +//! SSR gain-control reconstruction — ISO/IEC 14496-3 §4.6.12. +//! +//! This is the §4.6.12 *back-end* of the SSR (Scalable Sample Rate, +//! AOT 3) gain-control tool, the counterpart to the +//! [`crate::gain_control_data`] wire parser. Where that module reads +//! the Table 4.12 `(max_band, adjust_num, alevcode, aloccode)` side +//! info off the bitstream, this module turns that side info plus the +//! per-band IMDCT output into the reconstructed PCM time signal: +//! +//! 1. **Gain-control data decoding** (§4.6.12.3.1) — +//! [`BandGainFunction::reconstruct`] maps the wire codes to the +//! `NADW` / `ALOC` / `ALEV` ladder via the Table 4.108 `AdjLoc()` +//! and Table 4.109 `AdjLev()` tables. +//! 2. **Gain-control function setting** (§4.6.12.3.2) — the same call +//! builds the `FMD` fragment-modification function, threads the +//! cross-frame `PFMD`, composes the per-sequence `GMF` gain +//! modification function, and inverts it to the gain-control +//! function `AD(j) = 1/GMF(j)`. +//! 3. **Gain-control windowing & overlapping** (§4.6.12.3.3) — +//! [`GainBandState::window_overlap`] applies `AD` to the band +//! spectrum `U`, then overlap-adds against the previous frame's +//! tail `PT` to produce the band sample data `V`. +//! +//! The IPQF synthesis filter (§4.6.12.3.4) that recombines the four +//! `V` bands into the output PCM lives in the `ipqf` module. +//! +//! ## Per-band, per-frame state +//! +//! Two quantities thread across frames, *per IPQF band*: +//! +//! * `PFMD_B(j)` — the previous frame's fragment-modification function, +//! used to scale the left half of this frame's `GMF` (§4.6.12.3.2 +//! step 3). Its initial value is `1.0` (spec note). +//! * `PT_B(j)` — the previous frame's gain-controlled block sample +//! data tail, overlap-added into this frame's `V` (§4.6.12.3.3 +//! step 2). Its initial value is `0.0` (spec note). +//! +//! [`GainBandState`] carries both for one band; the four-band decoder +//! holds a `[GainBandState; 4]`. +//! +//! ## Provenance +//! +//! Every table and formula is from ISO/IEC 14496-3:2001 §4.6.12 +//! (Tables 4.108 / 4.109, the §4.6.12.3.1–3 equations) staged under +//! `docs/audio/aac/`. No external SSR implementation was consulted. + +use crate::gain_control_data::{GainBand, GainControlData}; +use crate::ics_info::WindowSequence; + +/// `AdjLoc(AC)` — ISO/IEC 14496-3 Table 4.108. The 32 tabulated +/// values are exactly `8 · AC` for `AC ∈ 0..=31`. +#[must_use] +pub fn adj_loc(ac: u8) -> u32 { + 8 * u32::from(ac) +} + +/// `AdjLev(AV)` — ISO/IEC 14496-3 Table 4.109. The 16 tabulated +/// values are exactly `AV − 4` for `AV ∈ 0..=15`. +#[must_use] +pub fn adj_lev(av: u8) -> i32 { + i32::from(av) - 4 +} + +/// Number of gain-control windows `N(window_sequence)` — the per-band +/// window count over which the gain ladder is transmitted (Table 4.12 +/// / §4.6.12.3.1). Long sequences carry one or two windows; the short +/// sequence carries eight. +#[must_use] +pub fn num_windows(seq: WindowSequence) -> usize { + match seq { + WindowSequence::OnlyLong => 1, + WindowSequence::LongStart | WindowSequence::LongStop => 2, + WindowSequence::EightShort => 8, + } +} + +/// `ALOC_{W,B}(NADW + 1)` — the §4.6.12.3.1 step (4) endpoint location +/// for the gain ladder of window `w` under `seq`. +/// +/// ```text +/// 256, W == 0 if ONLY_LONG_SEQUENCE +/// 112, W == 0 +/// if LONG_START_SEQUENCE +/// 32, W == 1 +/// ALOC(NADW+1) = +/// 32, 0..=7 if EIGHT_SHORT_SEQUENCE +/// +/// 112, W == 0 +/// if LONG_STOP_SEQUENCE +/// 256, W == 1 +/// ``` +#[must_use] +pub fn endpoint_aloc(seq: WindowSequence, w: usize) -> u32 { + match seq { + WindowSequence::OnlyLong => 256, + WindowSequence::LongStart => { + if w == 0 { + 112 + } else { + 32 + } + } + WindowSequence::EightShort => 32, + WindowSequence::LongStop => { + if w == 0 { + 112 + } else { + 256 + } + } + } +} + +/// The §4.6.12.3.2 upper bound (inclusive) on `j` for the `FMD` +/// fragment-modification function of window `w` under `seq`. This is +/// the largest sample index over which `M`/`FMD` are defined. +#[must_use] +fn fmd_last_j(seq: WindowSequence, w: usize) -> usize { + match seq { + WindowSequence::OnlyLong => 255, + WindowSequence::LongStart => { + if w == 0 { + 111 + } else { + 31 + } + } + WindowSequence::EightShort => 31, + WindowSequence::LongStop => { + if w == 0 { + 111 + } else { + 255 + } + } + } +} + +/// The §4.6.12.3.1 reconstructed gain ladder for one `(window, band)` +/// slot: the `ALOC` / `ALEV` arrays indexed `0..=NADW+1`. +#[derive(Debug, Clone, PartialEq)] +struct Ladder { + /// `ALOC_{W,B}(m)`, `0 ≤ m ≤ NADW + 1`. + aloc: Vec, + /// `ALEV_{W,B}(m)`, `0 ≤ m ≤ NADW + 1`. Each entry is a power of + /// two `2^AdjLev(...)` (or the unit endpoint / `NADW == 0` value). + alev: Vec, +} + +impl Ladder { + /// Reconstruct the §4.6.12.3.1 ladder for window `w` of band `b` + /// (1-based spec band) from the per-window wire record. + /// + /// `window` carries the `adjust_num[B][W]` ladder entries (the + /// `(alevcode, aloccode)` pairs) for this `(b, w)` slot. + fn reconstruct( + window: &crate::gain_control_data::GainWindow, + seq: WindowSequence, + w: usize, + ) -> Self { + let nadw = window.adjustments.len(); + // ALOC / ALEV have NADW + 2 entries: indices 0..=NADW+1. + let mut aloc = Vec::with_capacity(nadw + 2); + let mut alev = Vec::with_capacity(nadw + 2); + + // Step (3): ALOC(0) = 0; ALEV(0) = 1 if NADW == 0 else ALEV(1). + // ALEV(0) is back-patched once ALEV(1) is known. + aloc.push(0); + alev.push(1.0); // placeholder; patched below for NADW > 0. + + // Steps (1)/(2): the transmitted ladder entries, m = 1..=NADW. + for adj in &window.adjustments { + aloc.push(adj_loc(adj.aloccode)); + alev.push(2f64.powi(adj_lev(adj.alevcode))); + } + + // Step (4): the endpoint, m = NADW + 1. + aloc.push(endpoint_aloc(seq, w)); + alev.push(1.0); + + // Patch ALEV(0): equals ALEV(1) when NADW > 0. + if nadw > 0 { + alev[0] = alev[1]; + } + + Ladder { aloc, alev } + } + + /// `M_{W,B,j} = max{ m : ALOC(m) ≤ j }` (§4.6.12.3.2 step 1). + /// + /// `ALOC` is monotonically increasing with `ALOC(0) = 0`, so for + /// any `j ≥ 0` at least `m = 0` qualifies; the answer is the index + /// of the last `ALOC` entry not exceeding `j`. + fn m_at(&self, j: u32) -> usize { + let mut m = 0usize; + for (idx, &loc) in self.aloc.iter().enumerate() { + if loc <= j { + m = idx; + } else { + break; + } + } + m + } +} + +/// `Inter(a, b, j) = 2^(((8 − j)·log2(a) + j·log2(b)) / 8)` +/// (§4.6.12.3.2) — the geometric interpolation between gain levels +/// `a` and `b` over the eight-sample ramp `0 ≤ j ≤ 8`. With `a`, `b` +/// powers of two the exponent is the linear blend of their `log2`s. +#[must_use] +fn inter(a: f64, b: f64, j: u32) -> f64 { + let la = a.log2(); + let lb = b.log2(); + let jf = j as f64; + let exp = ((8.0 - jf) * la + jf * lb) / 8.0; + 2f64.powf(exp) +} + +/// The fully-reconstructed §4.6.12.3.2 gain-control function for one +/// band of one frame: the per-window `AD_{W,B}(j) = 1 / GMF_{W,B}(j)` +/// arrays plus the `PFMD_B(j)` to thread into the next frame. +#[derive(Debug, Clone, PartialEq)] +pub struct BandGainFunction { + /// `AD_{W,B}(j)` per window. For long sequences the single (or + /// `w == 0`) window spans `0..512`; `EIGHT_SHORT_SEQUENCE` has + /// eight windows each spanning `0..64`. + pub ad: Vec>, + /// `PFMD_B(j)` for the next frame (§4.6.12.3.2 step 3). + pub pfmd_next: Vec, +} + +/// Build the §4.6.12.3.1–2 fragment-modification function `FMD_{W,B}` +/// for window `w` of one band. +fn fmd_window(ladder: &Ladder, seq: WindowSequence, w: usize) -> Vec { + let last = fmd_last_j(seq, w); + let mut fmd = vec![0.0f64; last + 1]; + for (j, slot) in fmd.iter_mut().enumerate() { + let m = ladder.m_at(j as u32); + let loc_m = ladder.aloc[m]; + let alev_m = ladder.alev[m]; + let alev_m1 = ladder.alev[m + 1]; + // FMD(j) = Inter(ALEV(M), ALEV(M+1), j − ALOC(M)) if + // ALOC(M) ≤ j ≤ ALOC(M) + 7, else ALEV(M+1). + let jj = j as u32; + *slot = if jj <= loc_m + 7 { + inter(alev_m, alev_m1, jj - loc_m) + } else { + alev_m1 + }; + } + fmd +} + +/// `ALEV_{W,B}(0)` for window `w` — the front gain used in the +/// §4.6.12.3.2 step-3 `GMF` composition. Reconstructs only the head of +/// the ladder. +fn alev0(band: &GainBand, w: usize) -> f64 { + let window = &band.windows[w]; + if window.adjustments.is_empty() { + 1.0 + } else { + 2f64.powi(adj_lev(window.adjustments[0].alevcode)) + } +} + +/// Compose the §4.6.12.3.2 step-3 gain-modification function `GMF` for +/// a non-`EIGHT_SHORT` band and thread `PFMD`. +fn gmf_long( + fmd: &[Vec], + band: &GainBand, + pfmd_prev: &[f64], + seq: WindowSequence, +) -> (Vec, Vec) { + // GMF spans 0..512 for the long sequences. + let mut gmf = vec![0.0f64; 512]; + let pfmd_next: Vec; + match seq { + WindowSequence::OnlyLong => { + let a0 = alev0(band, 0); + for (j, slot) in gmf.iter_mut().enumerate() { + *slot = if j <= 255 { + a0 * pfmd_prev[j] + } else { + fmd[0][j - 256] + }; + } + // PFMD_B(j) = FMD_0,B(j), 0 ≤ j ≤ 255. + pfmd_next = fmd[0][..256].to_vec(); + } + WindowSequence::LongStart => { + let a0 = alev0(band, 0); + let a1 = alev0(band, 1); + for (j, slot) in gmf.iter_mut().enumerate() { + *slot = if j <= 255 { + a0 * a1 * pfmd_prev[j] + } else if j <= 367 { + a1 * fmd[0][j - 256] + } else if j <= 399 { + fmd[1][j - 368] + } else { + 1.0 + }; + } + // PFMD_B(j) = FMD_1,B(j), 0 ≤ j ≤ 31. + pfmd_next = fmd[1][..32].to_vec(); + } + WindowSequence::LongStop => { + let a0 = alev0(band, 0); + let a1 = alev0(band, 1); + for (j, slot) in gmf.iter_mut().enumerate() { + *slot = if j <= 111 { + 1.0 + } else if j <= 143 { + a0 * a1 * pfmd_prev[j - 112] + } else if j <= 255 { + a1 * fmd[0][j - 144] + } else { + fmd[1][j - 256] + }; + } + // PFMD_B(j) = FMD_1,B(j), 0 ≤ j ≤ 255. + pfmd_next = fmd[1][..256].to_vec(); + } + WindowSequence::EightShort => unreachable!("gmf_long called for short sequence"), + } + (gmf, pfmd_next) +} + +/// Compose the §4.6.12.3.2 step-3 `EIGHT_SHORT_SEQUENCE` gain +/// modification: eight 64-sample `GMF` windows, threading `PFMD`. +fn gmf_short(fmd: &[Vec], band: &GainBand, pfmd_prev: &[f64]) -> (Vec>, Vec) { + let mut gmf: Vec> = Vec::with_capacity(8); + for w in 0..8 { + let a0 = alev0(band, w); + let mut g = vec![0.0f64; 64]; + for (j, slot) in g.iter_mut().enumerate() { + *slot = if j <= 31 { + if w == 0 { + a0 * pfmd_prev[j] + } else { + a0 * fmd[w - 1][j] + } + } else { + fmd[w][j - 32] + }; + } + gmf.push(g); + } + // PFMD_B(j) = FMD_7,B(j), 0 ≤ j ≤ 31. + let pfmd_next = fmd[7][..32].to_vec(); + (gmf, pfmd_next) +} + +impl BandGainFunction { + /// Reconstruct the §4.6.12.3.1–2 gain-control function `AD` for one + /// band of one frame. + /// + /// * `band` — the band's per-window ladder records (the + /// `bands[b - 1]` entry of the wire [`GainControlData`], spec band + /// `b ∈ 1..=3`). + /// * `seq` — the frame's `window_sequence`. + /// * `pfmd_prev` — `PFMD_B(j)` carried from the previous frame + /// (initial `1.0`). Length is 256 for the long sequences, 32 for + /// the short sequence. + /// + /// Returns the per-window `AD_{W,B}(j) = 1 / GMF_{W,B}(j)` arrays + /// and the `pfmd_next` to thread into the next frame. + #[must_use] + pub fn reconstruct(band: &GainBand, seq: WindowSequence, pfmd_prev: &[f64]) -> Self { + let n_win = num_windows(seq); + // Per-window FMD. + let fmd: Vec> = (0..n_win) + .map(|w| { + let ladder = Ladder::reconstruct(&band.windows[w], seq, w); + fmd_window(&ladder, seq, w) + }) + .collect(); + + match seq { + WindowSequence::EightShort => { + let (gmf, pfmd_next) = gmf_short(&fmd, band, pfmd_prev); + let ad = gmf + .iter() + .map(|g| g.iter().map(|&v| 1.0 / v).collect()) + .collect(); + BandGainFunction { ad, pfmd_next } + } + _ => { + let (gmf, pfmd_next) = gmf_long(&fmd, band, pfmd_prev, seq); + let ad = vec![gmf.iter().map(|&v| 1.0 / v).collect()]; + BandGainFunction { ad, pfmd_next } + } + } + } + + /// An identity gain function (`AD ≡ 1`) for a band with no gain + /// control active — the §4.6.12.3.3 `B == 0` case (band 0 never + /// carries a ladder) and any band beyond `max_band`. + /// + /// `seq` selects the window layout: one 512-sample window for the + /// long sequences, eight 64-sample windows for the short sequence. + #[must_use] + pub fn identity(seq: WindowSequence) -> Self { + match seq { + WindowSequence::EightShort => BandGainFunction { + ad: vec![vec![1.0; 64]; 8], + pfmd_next: vec![1.0; 32], + }, + _ => BandGainFunction { + ad: vec![vec![1.0; 512]], + pfmd_next: vec![1.0; 256], + }, + } + } +} + +/// One IPQF band's cross-frame gain-control state (§4.6.12.3.2–3): the +/// `PFMD_B(j)` fragment-modification carry and the `PT_B(j)` +/// gain-controlled block sample data tail. +/// +/// Construct with [`GainBandState::new`] (spec initial values: `PFMD ≡ +/// 1.0`, `PT ≡ 0.0`), then call [`GainBandState::window_overlap`] once +/// per frame; it returns the 256-sample-stride band sample data `V_B` +/// for this frame and advances both carries. +#[derive(Debug, Clone, PartialEq)] +pub struct GainBandState { + /// `PFMD_B(j)` — 256 entries (only the first + /// [`pfmd_len`]`(seq)` are read by the next frame). + pfmd: Vec, + /// `PT_B(j)` — 256 entries (only the written prefix is meaningful + /// for the next frame's overlap). + pt: Vec, +} + +impl Default for GainBandState { + fn default() -> Self { + Self::new() + } +} + +impl GainBandState { + /// A fresh band state with the §4.6.12 spec initial values: + /// `PFMD_B(j) = 1.0` and `PT_B(j) = 0.0`. + #[must_use] + pub fn new() -> Self { + GainBandState { + pfmd: vec![1.0; 256], + pt: vec![0.0; 256], + } + } + + /// The §4.6.12.3.3 gain-control windowing + overlapping for one band + /// of one frame. + /// + /// * `band` — this band's wire ladder (`None` for band 0 or a band + /// beyond `max_band`: gain control is inactive and `T = U`). + /// * `u` — the band spectrum data `U_{W,B}(j)`, the non-overlapped + /// per-band IMDCT output. For the long sequences this is a single + /// 512-sample window; for `EIGHT_SHORT_SEQUENCE` it is eight + /// 64-sample windows concatenated (window `w` at `u[64·w .. 64·w + + /// 64]`). + /// * `seq` — the frame's `window_sequence`. + /// + /// Returns the band sample data `V_B(j)` (the variable-length + /// per-frame fragment: 256 for `ONLY_LONG` / `EIGHT_SHORT`, 368 for + /// `LONG_START`, 144 for `LONG_STOP`) and updates the `PFMD` / `PT` + /// carries in place. + #[must_use] + pub fn window_overlap( + &mut self, + band: Option<&GainBand>, + u: &[f64], + seq: WindowSequence, + ) -> Vec { + // (1) windowing: T = AD · U (or T = U when gain control is off). + // The produced `pfmd_next` is the 32- or 256-entry prefix the + // next frame reads; write it into the persistent 256-buffer so + // the buffer never shrinks (any branch can read its prefix). + let t = match band { + Some(b) => { + let g = BandGainFunction::reconstruct(b, seq, &self.pfmd); + self.store_pfmd(&g.pfmd_next); + apply_gain(&g.ad, u, seq) + } + None => { + // Band 0 / inactive: T = U, PFMD threads as the identity. + let g = BandGainFunction::identity(seq); + self.store_pfmd(&g.pfmd_next); + u.to_vec() + } + }; + + // (2) overlapping: produce V_B and update PT_B. + self.overlap(&t, seq) + } + + /// Write the produced `PFMD` prefix into the persistent 256-entry + /// buffer (the buffer never shrinks, so any following frame can read + /// the prefix it needs). + fn store_pfmd(&mut self, produced: &[f64]) { + self.pfmd[..produced.len()].copy_from_slice(produced); + } + + /// The §4.6.12.3.3 step-(2) overlap for the gain-controlled block + /// sample data `t` (`T_{W,B}` concatenated window-major). + fn overlap(&mut self, t: &[f64], seq: WindowSequence) -> Vec { + match seq { + WindowSequence::OnlyLong => { + // V(j) = PT(j) + T0(j), 0..256; PT(j) = T0(j+256), 0..256. + let v = add_slices(&self.pt[..256], &t[..256]); + self.pt[..256].copy_from_slice(&t[256..512]); + v + } + WindowSequence::LongStart => { + // V(j) = PT(j) + T0(j), 0..256; + // V(j+256) = T0(j+256), 0..112; ⇒ V spans 0..368. + // PT(j) = T0(j+368), 0..32. + let mut v = vec![0.0f64; 368]; + add_into(&mut v[..256], &self.pt[..256], &t[..256]); + v[256..368].copy_from_slice(&t[256..368]); + self.pt[..32].copy_from_slice(&t[368..400]); + v + } + WindowSequence::EightShort => { + // V(j) = PT(j) + T0(j), W==0, 0..32; + // V(32W+j) = T_{W-1}(j+32) + T_W(j), 1..=7, 0..32; + // PT(j) = T7(j+32), 0..32. ⇒ V spans 0..256. + let mut v = vec![0.0f64; 256]; + // Window w occupies t[64·w .. 64·w + 64]. + add_into(&mut v[..32], &self.pt[..32], &t[..32]); + for w in 1..=7 { + let prev = &t[64 * (w - 1) + 32..64 * (w - 1) + 64]; + let cur = &t[64 * w..64 * w + 32]; + add_into(&mut v[32 * w..32 * w + 32], prev, cur); + } + self.pt[..32].copy_from_slice(&t[64 * 7 + 32..64 * 7 + 64]); + v + } + WindowSequence::LongStop => { + // V(j) = PT(j) + T0(j+112), 0..32; + // V(j+32) = T0(j+144), 0..112; ⇒ V spans 0..144. + // PT(j) = T0(j+256), 0..256. + let mut v = vec![0.0f64; 144]; + add_into(&mut v[..32], &self.pt[..32], &t[112..144]); + v[32..144].copy_from_slice(&t[144..256]); + self.pt[..256].copy_from_slice(&t[256..512]); + v + } + } + } +} + +/// Element-wise sum of two equal-length slices into a fresh `Vec`. +fn add_slices(a: &[f64], b: &[f64]) -> Vec { + a.iter().zip(b.iter()).map(|(&x, &y)| x + y).collect() +} + +/// Element-wise `dst[i] = a[i] + b[i]` over equal-length slices. +fn add_into(dst: &mut [f64], a: &[f64], b: &[f64]) { + for (d, (&x, &y)) in dst.iter_mut().zip(a.iter().zip(b.iter())) { + *d = x + y; + } +} + +/// Apply the §4.6.12.3.3 step-(1) gain `T_{W,B}(j) = AD_{W,B}(j) · +/// U_{W,B}(j)` window-major, returning the concatenated `T`. +fn apply_gain(ad: &[Vec], u: &[f64], seq: WindowSequence) -> Vec { + match seq { + WindowSequence::EightShort => { + let mut t = vec![0.0f64; u.len()]; + for (w, ad_w) in ad.iter().enumerate() { + for (j, &g) in ad_w.iter().enumerate() { + let idx = 64 * w + j; + t[idx] = g * u[idx]; + } + } + t + } + _ => ad[0].iter().zip(u.iter()).map(|(&g, &x)| g * x).collect(), + } +} + +/// The §4.6.12.3.2 `PFMD_B` **input** length a frame of `seq` reads +/// from the previous frame. +/// +/// The step-3 `GMF` composition reads `PFMD_B(j)` over `0..256` for +/// `ONLY_LONG` / `LONG_START` (their left half spans the full 256), but +/// only `0..32` for `LONG_STOP` (the `112 ≤ j ≤ 143` region) and +/// `EIGHT_SHORT` (the `W == 0`, `0 ≤ j ≤ 31` region). A +/// [`GainBandState`] keeps the full 256-entry buffer, so any branch can +/// always read the prefix it needs. +#[must_use] +pub fn pfmd_len(seq: WindowSequence) -> usize { + match seq { + WindowSequence::OnlyLong | WindowSequence::LongStart => 256, + WindowSequence::LongStop | WindowSequence::EightShort => 32, + } +} + +/// The §4.6.12.3.2 `PFMD_B` **output** length a frame of `seq` produces +/// for the next frame. +/// +/// `ONLY_LONG` / `LONG_STOP` emit `FMD(0..256)` (256 entries); +/// `LONG_START` / `EIGHT_SHORT` emit `FMD(0..32)` (32 entries). In a +/// legal `window_sequence` chain the produced length always matches +/// what the following frame's [`pfmd_len`] reads (`LONG_START` → +/// `EIGHT_SHORT`, `EIGHT_SHORT` → `LONG_STOP`, etc.). +#[must_use] +pub fn pfmd_produced_len(seq: WindowSequence) -> usize { + match seq { + WindowSequence::OnlyLong | WindowSequence::LongStop => 256, + WindowSequence::LongStart | WindowSequence::EightShort => 32, + } +} + +/// Look up the band's wire ladder from a [`GainControlData`] record for +/// spec band `b ∈ 1..=3`, or `None` when `b > max_band` (the band is +/// not gain-controlled, so its gain function is the identity). +#[must_use] +pub fn band_record(gcd: &GainControlData, b: usize) -> Option<&GainBand> { + if b == 0 || b > gcd.max_band as usize { + None + } else { + gcd.bands.get(b - 1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adj_loc_is_eight_times() { + assert_eq!(adj_loc(0), 0); + assert_eq!(adj_loc(1), 8); + assert_eq!(adj_loc(15), 120); + assert_eq!(adj_loc(31), 248); + } + + #[test] + fn adj_lev_is_offset_minus_four() { + assert_eq!(adj_lev(0), -4); + assert_eq!(adj_lev(4), 0); + assert_eq!(adj_lev(15), 11); + } + + #[test] + fn endpoint_aloc_per_sequence() { + assert_eq!(endpoint_aloc(WindowSequence::OnlyLong, 0), 256); + assert_eq!(endpoint_aloc(WindowSequence::LongStart, 0), 112); + assert_eq!(endpoint_aloc(WindowSequence::LongStart, 1), 32); + assert_eq!(endpoint_aloc(WindowSequence::EightShort, 3), 32); + assert_eq!(endpoint_aloc(WindowSequence::LongStop, 0), 112); + assert_eq!(endpoint_aloc(WindowSequence::LongStop, 1), 256); + } + + #[test] + fn inter_endpoints_are_exact() { + // Inter(a, b, 0) == a, Inter(a, b, 8) == b. + assert!((inter(2.0, 8.0, 0) - 2.0).abs() < 1e-12); + assert!((inter(2.0, 8.0, 8) - 8.0).abs() < 1e-12); + // Geometric midpoint at j == 4: sqrt(a·b). + assert!((inter(2.0, 8.0, 4) - (2.0f64 * 8.0).sqrt()).abs() < 1e-12); + } + + #[test] + fn empty_ladder_gives_unit_gain() { + // A band with an all-empty (adjust_num == 0) ladder produces + // AD ≡ 1 everywhere (GMF ≡ 1). + let band = GainBand { + windows: vec![crate::gain_control_data::GainWindow::default()], + }; + let pfmd = vec![1.0f64; 256]; + let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); + assert_eq!(g.ad.len(), 1); + assert_eq!(g.ad[0].len(), 512); + for &v in &g.ad[0] { + assert!((v - 1.0).abs() < 1e-12, "expected unit gain, got {v}"); + } + // PFMD threads forward as FMD_0 == 1. + assert!(g.pfmd_next.iter().all(|&v| (v - 1.0).abs() < 1e-12)); + } + + #[test] + fn identity_matches_empty_ladder() { + let band = GainBand { + windows: vec![crate::gain_control_data::GainWindow::default(); 8], + }; + let pfmd = vec![1.0f64; 32]; + let recon = BandGainFunction::reconstruct(&band, WindowSequence::EightShort, &pfmd); + let ident = BandGainFunction::identity(WindowSequence::EightShort); + assert_eq!(recon.ad.len(), ident.ad.len()); + for (r, i) in recon.ad.iter().zip(ident.ad.iter()) { + for (&rv, &iv) in r.iter().zip(i.iter()) { + assert!((rv - iv).abs() < 1e-12); + } + } + } + + #[test] + fn overlap_only_long_is_tdac_add() { + // Identity gain (band 0 / inactive): T == U. A 512-sample U; + // first frame V(j) = 0 + U(j) (PT starts 0); PT becomes + // U(256..512). Second frame with the same U: V(j) = + // U(256+j) + U(j). + let mut st = GainBandState::new(); + let u: Vec = (0..512).map(|j| (j as f64) * 0.01).collect(); + let v0 = st.window_overlap(None, &u, WindowSequence::OnlyLong); + assert_eq!(v0.len(), 256); + for j in 0..256 { + assert!((v0[j] - u[j]).abs() < 1e-12); + } + let v1 = st.window_overlap(None, &u, WindowSequence::OnlyLong); + for j in 0..256 { + assert!((v1[j] - (u[256 + j] + u[j])).abs() < 1e-12); + } + } + + #[test] + fn overlap_lengths_per_sequence() { + let u_long = vec![1.0f64; 512]; + let u_short = vec![1.0f64; 512]; // eight 64-sample windows. + assert_eq!( + GainBandState::new() + .window_overlap(None, &u_long, WindowSequence::OnlyLong) + .len(), + 256 + ); + assert_eq!( + GainBandState::new() + .window_overlap(None, &u_long, WindowSequence::LongStart) + .len(), + 368 + ); + assert_eq!( + GainBandState::new() + .window_overlap(None, &u_short, WindowSequence::EightShort) + .len(), + 256 + ); + assert_eq!( + GainBandState::new() + .window_overlap(None, &u_long, WindowSequence::LongStop) + .len(), + 144 + ); + } + + #[test] + fn overlap_eight_short_overlaps_adjacent_windows() { + // Identity gain. Each short window is constant c_w. The overlap + // V(32W+j) = T_{W-1}(j+32) + T_W(j) = c_{W-1} + c_W for the + // overlapped region, and PT becomes c_7. + let mut st = GainBandState::new(); + let mut u = vec![0.0f64; 512]; + for w in 0..8 { + for j in 0..64 { + u[64 * w + j] = (w as f64) + 1.0; + } + } + let v = st.window_overlap(None, &u, WindowSequence::EightShort); + assert_eq!(v.len(), 256); + // First segment: PT(0)=0 + T0 = 1. + assert!((v[0] - 1.0).abs() < 1e-12); + // Segment W=1: T0 + T1 = 1 + 2 = 3. + assert!((v[32] - 3.0).abs() < 1e-12); + // Segment W=7: T6 + T7 = 7 + 8 = 15. + assert!((v[32 * 7] - 15.0).abs() < 1e-12); + // PT now holds T7 = 8. + assert!((st.pt[0] - 8.0).abs() < 1e-12); + } + + #[test] + fn gain_then_overlap_scales_band() { + use crate::gain_control_data::{GainAdjust, GainWindow}; + // A constant band U ≡ 1.0; a single gain change makes AD ≠ 1 in + // the [256..) region (where the FMD lands). The V output picks + // up AD·U in that region. + let band = GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 6, // AdjLev=2 ⇒ ALEV=4 ⇒ AD=1/4 in ramp. + aloccode: 0, // ALOC=0. + }], + }], + }; + let mut st = GainBandState::new(); + let u = vec![1.0f64; 512]; + let v = st.window_overlap(Some(&band), &u, WindowSequence::OnlyLong); + assert_eq!(v.len(), 256); + // V is finite and the gain has been applied (not all 1.0). + assert!(v.iter().all(|x| x.is_finite())); + } + + #[test] + fn single_gain_change_scales_segment() { + use crate::gain_control_data::{GainAdjust, GainWindow}; + // One gain change at aloccode=2 (ALOC=16), alevcode=6 + // (AdjLev=2 ⇒ ALEV=4). NADW=1. + // ALOC = [0, 16, 256], ALEV = [4, 4, 1] (ALEV(0)=ALEV(1)=4). + // For j in 0..16, M=0, ALOC(0)=0, ramp Inter(4,4,j)=4 over the + // first 8 then flat ALEV(1)=4 ⇒ FMD=4 throughout 0..16. + let band = GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 6, + aloccode: 2, + }], + }], + }; + let pfmd = vec![1.0f64; 256]; + let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); + // GMF(256) = FMD_0(0) = ALEV at j=0 region. Since ALOC(1)=16, + // M(0)=0, ALEV(0)=4, ALEV(1)=4 ⇒ FMD(0)=4 ⇒ AD = 1/4. + assert!((g.ad[0][256] - 0.25).abs() < 1e-9, "AD={}", g.ad[0][256]); + // Beyond ALOC(NADW+1)=256 region: at j large, M=1 (ALOC(1)=16), + // ALEV(1)=4, ALEV(2)=1, j-16 > 7 ⇒ FMD = ALEV(2) = 1 ⇒ AD=1. + assert!((g.ad[0][511] - 1.0).abs() < 1e-9, "AD={}", g.ad[0][511]); + } + + /// A band carrying a ladder reconstructs a finite, strictly-positive + /// `AD` over the full window for every `window_sequence` — the + /// `GMF`/`AD` reciprocal pair is well-defined (no zero or infinity). + #[test] + fn ad_is_finite_positive_all_sequences() { + use crate::gain_control_data::{GainAdjust, GainWindow}; + for &seq in &[ + WindowSequence::OnlyLong, + WindowSequence::LongStart, + WindowSequence::LongStop, + WindowSequence::EightShort, + ] { + let n_win = num_windows(seq); + // Each window carries one mid-range gain change. + let windows = (0..n_win) + .map(|_| GainWindow { + adjustments: vec![GainAdjust { + alevcode: 7, // AdjLev=3 ⇒ ALEV=8. + aloccode: 1, // ALOC=8. + }], + }) + .collect(); + let band = GainBand { windows }; + let pfmd = vec![1.0f64; pfmd_len(seq)]; + let g = BandGainFunction::reconstruct(&band, seq, &pfmd); + for win in &g.ad { + for &v in win { + assert!(v.is_finite() && v > 0.0, "AD={v} for {seq:?}"); + } + } + // PFMD threads with the right produced length. + assert_eq!(g.pfmd_next.len(), pfmd_produced_len(seq)); + } + } + + /// The §4.6.12.3.2 inversion is exact: `AD(j) · GMF(j) == 1`. We + /// recover `GMF` as `1/AD` and confirm it round-trips to `AD`. + #[test] + fn ad_times_gmf_is_one() { + use crate::gain_control_data::{GainAdjust, GainWindow}; + let band = GainBand { + windows: vec![GainWindow { + adjustments: vec![ + GainAdjust { + alevcode: 8, + aloccode: 2, + }, + GainAdjust { + alevcode: 2, + aloccode: 10, + }, + ], + }], + }; + let pfmd = vec![1.0f64; 256]; + let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); + for &ad in &g.ad[0] { + let gmf = 1.0 / ad; + assert!((ad * gmf - 1.0).abs() < 1e-12); + } + } + + /// Pre-stream defaults: a first frame with `PFMD ≡ 1.0` and a band + /// whose only gain change sits at `ALOC = 0` scales the left-half + /// `GMF` region by `ALEV(0)` (the §4.6.12.3.2 step-3 `ONLY_LONG` + /// branch `ALEV(0)·PFMD`). + #[test] + fn long_left_half_scaled_by_alev0() { + use crate::gain_control_data::{GainAdjust, GainWindow}; + // alevcode=7 ⇒ AdjLev=3 ⇒ ALEV=8; aloccode=0 ⇒ ALOC=0. + // ALEV(0)=ALEV(1)=8. GMF(j) for j in 0..256 = ALEV(0)·PFMD(j) + // = 8·1 = 8 ⇒ AD = 1/8. + let band = GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 7, + aloccode: 0, + }], + }], + }; + let pfmd = vec![1.0f64; 256]; + let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); + for &ad in &g.ad[0][..256] { + assert!((ad - 0.125).abs() < 1e-12, "AD={ad}"); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/gain_control_data.rs b/crates/vendor/oxideav-aac/src/gain_control_data.rs new file mode 100644 index 00000000..549dc829 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/gain_control_data.rs @@ -0,0 +1,305 @@ +//! `gain_control_data()` parser + encoder primitive — ISO/IEC 14496-3 +//! §4.4.6.5 / Table 4.12. +//! +//! `gain_control_data()` is the wire record of the SSR (Scalable +//! Sample Rate, AOT 3) gain-control tool. SSR splits each AAC frame +//! through a 4-band polyphase quadrature filterbank (PQF) **before** +//! the MDCT, and applies a per-band, per-window gain-adjustment +//! ladder to attenuate pre-echo artefacts. The decoder reads the +//! ladder out of `gain_control_data()` and reverses it after the +//! per-band IMDCTs. The block rides inside an +//! `individual_channel_stream()` between `tns_data()` and +//! `spectral_data()`, gated by the dispatching +//! `gain_control_data_present` flag (Tables 4.44 / 4.50). +//! +//! ## Wire layout (Table 4.12) +//! +//! ```text +//! gain_control_data() { +//! max_band; 2 bits +//! for (bd = 1; bd <= max_band; bd++) { +//! for (wd = 0; wd < N(window_sequence); wd++) { +//! adjust_num[bd][wd]; 3 bits +//! for (ad = 0; ad < adjust_num[bd][wd]; ad++) { +//! alevcode[bd][wd][ad]; 4 bits +//! aloccode[bd][wd][ad]; W(seq, wd) bits +//! } +//! } +//! } +//! } +//! ``` +//! +//! Per Table 4.12 the per-window count `N(window_sequence)` and the +//! per-`(window_sequence, wd)` `aloccode` width `W(seq, wd)` are: +//! +//! | `window_sequence` | N | `W(seq, wd=0)` | `W(seq, wd≥1)` | +//! |--------------------------|---|----------------|----------------| +//! | `ONLY_LONG_SEQUENCE` | 1 | 5 | n/a | +//! | `LONG_START_SEQUENCE` | 2 | 4 | 2 | +//! | `EIGHT_SHORT_SEQUENCE` | 8 | 2 | 2 | +//! | `LONG_STOP_SEQUENCE` | 2 | 4 | 5 | +//! +//! `alevcode` is always 4 bits; `adjust_num` is always 3 bits (so +//! per `(bd, wd)` slot the ladder length is `0..=7`). +//! +//! The outer band loop iterates `1..=max_band` (note the **`bd = +//! 1`** start — band 0 carries no gain ladder by spec). +//! `max_band ∈ 0..=3` (2-bit field); when `max_band == 0` the body +//! collapses to just the 2-bit field. Per the §4.6.12 SSR backend +//! the legal `max_band` for a decoder targeting 4-band PQF output +//! is `0..=3`; the wire-format itself does not constrain values +//! further than the field width. +//! +//! ## What this module covers +//! +//! * [`GainControlData::parse`] — read a Table 4.12 block from a +//! [`BitReader`], surfacing the raw wire fields without applying +//! the §4.6.12 SSR gain-reconstruction (the actual ladder +//! application needs the SSR PQF backend, which is not part of +//! Phase 2). +//! * [`GainControlData::write`] — the inverse: serialise a +//! [`GainControlData`] onto a [`BitWriter`] in bit-exact +//! Table 4.12 form. Caller-side field overflow surfaces as +//! [`Error::GainControlDataEncodeInvalid`]. +//! +//! ## What this module does *not* cover +//! +//! * The §4.6.12 ladder-application loop (per-window gain envelope +//! reconstruction from `(alevcode, aloccode)` pairs into +//! sample-domain attenuation factors) is deferred until the SSR +//! PQF / IMDCT back-end lands. +//! * The normative §4.6.12 constraint that the SSR profile's +//! `gain_control_data_present` flag is **0** for AOTs other than +//! 3 (SSR) is the responsibility of the dispatching +//! `individual_channel_stream()` (not yet wired up); the parser +//! and writer here surface the literal Table 4.12 bytes +//! regardless of the surrounding AOT so future round work has +//! access to the raw decoded record. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::ics_info::WindowSequence; +use crate::{Error, Result}; + +/// Width in bits of the `max_band` field. Table 4.12. +pub const MAX_BAND_BITS: u32 = 2; + +/// Width in bits of the `adjust_num` field. Table 4.12. +pub const ADJUST_NUM_BITS: u32 = 3; + +/// Width in bits of the `alevcode` field. Table 4.12. +pub const ALEVCODE_BITS: u32 = 4; + +/// Maximum value of the `max_band` field (2-bit width cap). +pub const MAX_BAND_CAP: u8 = 0x03; + +/// Maximum value of the `adjust_num` field (3-bit width cap). Each +/// per-`(bd, wd)` slot can carry between 0 and 7 ladder entries. +pub const MAX_ADJUST_NUM: u8 = 0x07; + +/// Maximum value of the `alevcode` field (4-bit width cap). +pub const MAX_ALEVCODE: u8 = 0x0f; + +/// Per-window count `N(window_sequence)` from Table 4.12. +/// +/// * `ONLY_LONG_SEQUENCE` → 1 +/// * `LONG_START_SEQUENCE` → 2 +/// * `EIGHT_SHORT_SEQUENCE` → 8 +/// * `LONG_STOP_SEQUENCE` → 2 +pub fn num_windows(window_sequence: WindowSequence) -> usize { + match window_sequence { + WindowSequence::OnlyLong => 1, + WindowSequence::LongStart => 2, + WindowSequence::EightShort => 8, + WindowSequence::LongStop => 2, + } +} + +/// Width in bits of the `aloccode` field at the given +/// `(window_sequence, wd)` position per Table 4.12. +/// +/// * `ONLY_LONG_SEQUENCE` — always 5 (only `wd == 0` is reached). +/// * `LONG_START_SEQUENCE` — 4 if `wd == 0`, else 2. +/// * `EIGHT_SHORT_SEQUENCE` — always 2. +/// * `LONG_STOP_SEQUENCE` — 4 if `wd == 0`, else 5. +/// +/// Returns `0` for `wd` indices outside the per-sequence range — the +/// caller is responsible for honouring [`num_windows`] when stepping +/// the inner loop. +pub fn aloccode_bits(window_sequence: WindowSequence, wd: usize) -> u32 { + match window_sequence { + WindowSequence::OnlyLong => { + if wd == 0 { + 5 + } else { + 0 + } + } + WindowSequence::LongStart => match wd { + 0 => 4, + 1 => 2, + _ => 0, + }, + WindowSequence::EightShort => { + if wd < 8 { + 2 + } else { + 0 + } + } + WindowSequence::LongStop => match wd { + 0 => 4, + 1 => 5, + _ => 0, + }, + } +} + +/// Single `(alevcode, aloccode)` ladder entry within one +/// `(bd, wd)` slot. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GainAdjust { + /// `alevcode[bd][wd][ad]` — 4-bit unsigned level code. + pub alevcode: u8, + /// `aloccode[bd][wd][ad]` — unsigned location code; field width + /// is selected by [`aloccode_bits`] from the surrounding + /// `window_sequence` and `wd` index. + pub aloccode: u8, +} + +/// Per-window ladder for a single `(bd, wd)` slot. The vector length +/// is the wire `adjust_num[bd][wd]` value (`0..=7`). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct GainWindow { + /// Ladder entries in wire order. `len()` equals + /// `adjust_num[bd][wd]`. + pub adjustments: Vec, +} + +/// Per-band collection of per-window ladders for one `bd` value. +/// +/// `windows.len()` must equal [`num_windows`] for the surrounding +/// `window_sequence`. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct GainBand { + /// Per-`wd` ladder entries. `windows[wd]` is the + /// `(bd, wd)` slot. + pub windows: Vec, +} + +/// Parsed `gain_control_data()` block (Table 4.12). +/// +/// `bands.len()` equals `max_band` (the **wire** field value); the +/// per-spec `bd = 1..=max_band` outer loop maps onto `bands[bd - 1]`. +/// `bands` is empty when `max_band == 0` (the body collapses to a +/// bare 2-bit zero). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct GainControlData { + /// `max_band` per Table 4.12 — 2-bit field, `0..=3`. The length + /// of `bands` is `max_band`. + pub max_band: u8, + /// Per-band ladders, indexed `bands[bd - 1]` for spec band + /// `bd ∈ 1..=max_band`. `bands.len() == max_band as usize`. + pub bands: Vec, +} + +impl GainControlData { + /// Parse a `gain_control_data()` block from `reader`, using + /// `window_sequence` to choose the per-window count and the + /// `aloccode` field widths. + /// + /// Returns [`Error::UnexpectedEnd`] on bit-reader underflow. + /// Never returns an encode-side variant — every field of + /// Table 4.12 is fixed-width and unconditionally well-formed up + /// to bit-position arithmetic. + pub fn parse(reader: &mut BitReader<'_>, window_sequence: WindowSequence) -> Result { + let max_band = read_u8(reader, MAX_BAND_BITS)?; + let n_win = num_windows(window_sequence); + let mut bands = Vec::with_capacity(max_band as usize); + for _bd in 1..=max_band as usize { + let mut windows = Vec::with_capacity(n_win); + for wd in 0..n_win { + let adjust_num = read_u8(reader, ADJUST_NUM_BITS)?; + let aloc_bits = aloccode_bits(window_sequence, wd); + let mut adjustments = Vec::with_capacity(adjust_num as usize); + for _ad in 0..adjust_num as usize { + let alevcode = read_u8(reader, ALEVCODE_BITS)?; + let aloccode = read_u8(reader, aloc_bits)?; + adjustments.push(GainAdjust { alevcode, aloccode }); + } + windows.push(GainWindow { adjustments }); + } + bands.push(GainBand { windows }); + } + Ok(GainControlData { max_band, bands }) + } + + /// Encode `gain_control_data()` onto `writer`, the bit-exact + /// inverse of [`GainControlData::parse`]. + /// + /// Returns [`Error::GainControlDataEncodeInvalid`] if any of the + /// following caller-side invariants are violated: + /// + /// * `max_band > MAX_BAND_CAP` (2-bit `max_band` overflow). + /// * `bands.len() != max_band as usize` (the outer band-loop + /// count must match the dispatched wire value). + /// * Any `band.windows.len() != num_windows(window_sequence)` + /// (the per-band window count must match the wire dispatch). + /// * Any `window.adjustments.len() > MAX_ADJUST_NUM as usize` + /// (3-bit `adjust_num` overflow). + /// * Any `GainAdjust::alevcode > MAX_ALEVCODE` (4-bit overflow). + /// * Any `GainAdjust::aloccode` exceeds the + /// `(1 << aloccode_bits(seq, wd)) - 1` cap for its slot. + pub fn write(&self, writer: &mut BitWriter, window_sequence: WindowSequence) -> Result<()> { + if self.max_band > MAX_BAND_CAP { + return Err(Error::GainControlDataEncodeInvalid); + } + if self.bands.len() != self.max_band as usize { + return Err(Error::GainControlDataEncodeInvalid); + } + let n_win = num_windows(window_sequence); + for band in &self.bands { + if band.windows.len() != n_win { + return Err(Error::GainControlDataEncodeInvalid); + } + for (wd, window) in band.windows.iter().enumerate() { + if window.adjustments.len() > MAX_ADJUST_NUM as usize { + return Err(Error::GainControlDataEncodeInvalid); + } + let aloc_bits = aloccode_bits(window_sequence, wd); + let aloc_cap: u32 = if aloc_bits == 0 { + 0 + } else { + (1u32 << aloc_bits) - 1 + }; + for adj in &window.adjustments { + if adj.alevcode > MAX_ALEVCODE { + return Err(Error::GainControlDataEncodeInvalid); + } + if adj.aloccode as u32 > aloc_cap { + return Err(Error::GainControlDataEncodeInvalid); + } + } + } + } + + writer.write_u32(self.max_band as u32, MAX_BAND_BITS); + for band in &self.bands { + for (wd, window) in band.windows.iter().enumerate() { + let adjust_num = window.adjustments.len() as u32; + writer.write_u32(adjust_num, ADJUST_NUM_BITS); + let aloc_bits = aloccode_bits(window_sequence, wd); + for adj in &window.adjustments { + writer.write_u32(adj.alevcode as u32, ALEVCODE_BITS); + writer.write_u32(adj.aloccode as u32, aloc_bits); + } + } + } + Ok(()) + } +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} diff --git a/crates/vendor/oxideav-aac/src/hcr.rs b/crates/vendor/oxideav-aac/src/hcr.rs new file mode 100644 index 00000000..32db4aa0 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/hcr.rs @@ -0,0 +1,677 @@ +//! Huffman codeword reordering (HCR) for AAC spectral data — ISO/IEC +//! 14496-3 §4.6.16.3. +//! +//! HCR is the error-resilience tool selected by +//! `aacSpectralDataResilienceFlag` (the Table 4.50 spectral branch +//! parsed by [`crate::ics_body::IcsBody::parse_er`]): the +//! `reordered_spectral_data()` block carries the same Huffman +//! codewords as a non-resilient `spectral_data()`, but the *priority* +//! codewords (PCWs) are placed at known segment boundaries so a bit +//! error inside one codeword cannot propagate into them. +//! +//! ## What this module covers (round 375) +//! +//! The deterministic, header-only **scaffolding** of HCR — the parts +//! that depend only on the two transmitted length fields, the active +//! `section_data()` codebooks, and the window geometry, *not* on the +//! reordered bit payload itself: +//! +//! * The §4.6.16.3.3.1 pre-sorting **priority metric** +//! ([`codebook_priority`], [`assigned_unit_nr`]) — the +//! `codebookPriority[32]` table and the `assignedUnitNr` formula +//! that determines which codewords become PCWs. +//! * The §4.6.16.3.3.2 **segment width / instantiation** math +//! ([`MAX_CW_LEN`], [`segment_width`], [`Segmentation::new`]) — the +//! `segmentWidth = min(maxCwLen, length_of_longest_codeword)` +//! derivation and the segment count / last-segment-remainder rule +//! sized by `length_of_reordered_spectral_data`. +//! +//! The full §4.6.16.3.4 reordered-payload **decode** (the PCW / +//! non-PCW `WriteCodewordToSegment` trial loop inverted to recover the +//! codeword bit positions) keys off this scaffold and is a later +//! milestone; it needs an HCR-bearing conformance stream to validate +//! bit-exactly. +//! +//! ## Provenance +//! +//! Every constant and formula here is from ISO/IEC 14496-3 +//! §4.6.16.3.3 / §4.6.16.3.5 (Table 4.170, the `codebookPriority[32]` +//! and `assignedUnitNr` listings) staged under `docs/audio/aac/`. The +//! `maxCwLen` column of Table 4.170 is a numeric data table; the +//! pre-sorting metric is the spec's own arithmetic. No external HCR +//! implementation was consulted. + +/// `maxCwLen[cb]` — the maximum Huffman codeword length, in bits, for +/// each spectral codebook (ISO/IEC 14496-3 Table 4.170). +/// +/// Indexed by the raw `sect_cb` value (`0..=31`): the base §4.A.1 +/// books are `0..=11`, the §4.6.16.4 virtual codebooks (used only in +/// the error-resilient `section_data()` 5-bit branch) are `16..=31`. +/// Codebook `0` (`ZERO_HCB`) and the reserved gaps `12..=15` carry no +/// codeword, so their entry is `0`. +pub const MAX_CW_LEN: [u8; 32] = [ + 0, // 0 ZERO_HCB + 11, // 1 + 9, // 2 + 20, // 3 + 16, // 4 + 13, // 5 + 11, // 6 + 14, // 7 + 12, // 8 + 17, // 9 + 14, // 10 + 49, // 11 ESC_HCB + 0, // 12 reserved + 0, // 13 NOISE_HCB (no spectral codeword) + 0, // 14 INTENSITY_HCB2 (no spectral codeword) + 0, // 15 INTENSITY_HCB (no spectral codeword) + 14, // 16 virtual + 17, // 17 virtual + 21, // 18 virtual + 21, // 19 virtual + 25, // 20 virtual + 25, // 21 virtual + 29, // 22 virtual + 29, // 23 virtual + 29, // 24 virtual + 29, // 25 virtual + 33, // 26 virtual + 33, // 27 virtual + 33, // 28 virtual + 37, // 29 virtual + 37, // 30 virtual + 41, // 31 virtual +]; + +/// `codebookPriority[32]` — the §4.6.16.3.3.1 pre-sorting priority +/// assigned to each codebook (ISO/IEC 14496-3 §4.6.16.3.3.1). +/// +/// Higher values are pre-sorted earlier (become PCWs). The `x` +/// entries in the spec — codebooks `0` (`ZERO_HCB`) and the reserved +/// `12..=15` — carry no spectral codeword and never participate in +/// reordering, so they are mapped to `0`. +/// +/// The spec listing is: +/// `{x,21,21,20,20,19,19,18,18,17,17,0,x,x,x,x,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1}`. +pub const CODEBOOK_PRIORITY: [u8; 32] = [ + 0, // 0 (x — no codeword) + 21, // 1 + 21, // 2 + 20, // 3 + 20, // 4 + 19, // 5 + 19, // 6 + 18, // 7 + 18, // 8 + 17, // 9 + 17, // 10 + 0, // 11 ESC_HCB + 0, // 12 (x) + 0, // 13 (x) + 0, // 14 (x) + 0, // 15 (x) + 16, // 16 + 15, // 17 + 14, // 18 + 13, // 19 + 12, // 20 + 11, // 21 + 10, // 22 + 9, // 23 + 8, // 24 + 7, // 25 + 6, // 26 + 5, // 27 + 4, // 28 + 3, // 29 + 2, // 30 + 1, // 31 +]; + +/// The §4.6.16.3.3.1 pre-sorting priority of a raw codebook value. +/// +/// Returns `0` for a codebook that carries no spectral codeword +/// (`ZERO_HCB`, the reserved `12..=15`, and any value `>= 32`). +#[must_use] +pub fn codebook_priority(cb: u8) -> u8 { + CODEBOOK_PRIORITY.get(cb as usize).copied().unwrap_or(0) +} + +/// `maxCwLen` for a raw codebook value (Table 4.170). Returns `0` for +/// a codebook that carries no spectral codeword or an out-of-range +/// value. +#[must_use] +pub fn max_cw_len(cb: u8) -> u8 { + MAX_CW_LEN.get(cb as usize).copied().unwrap_or(0) +} + +/// The §4.6.16.3.3.1 `assignedUnitNr` metric for one unit (a group of +/// four spectral lines = two 2-D or one 4-D codeword). +/// +/// ```text +/// assignedUnitNr = ( codebookPriority[cb] * maxNrOfLinesInWindow +/// + nrOfFirstLineInUnit ) * maxNrOfWindows + window +/// ``` +/// +/// * `cb` — the codebook of the unit (its priority drives the +/// energy-based second pre-sorting step). +/// * `max_lines_in_window` — `1024` for one long window, `128` for +/// eight short windows. +/// * `nr_of_first_line_in_unit` — the first spectral line index of +/// the unit (a multiple of 4: `0..=1020` long, `0..=124` short). +/// * `max_windows` — `1` long, `8` short. +/// * `window` — `0` long, `0..=7` short. +/// +/// Units sorted ascending by this number give the pre-sorted codeword +/// order (PCWs first). +#[must_use] +pub fn assigned_unit_nr( + cb: u8, + max_lines_in_window: u32, + nr_of_first_line_in_unit: u32, + max_windows: u32, + window: u32, +) -> u32 { + (u32::from(codebook_priority(cb)) * max_lines_in_window + nr_of_first_line_in_unit) + * max_windows + + window +} + +/// The §4.6.16.3.3.2 per-codebook segment width: +/// `segmentWidth = min(maxCwLen, length_of_longest_codeword)`. +/// +/// `length_of_longest_codeword` is the transmitted 6-bit field +/// (clamped to `49` for the reserved `50..=63` per §4.6.16.3.2). A +/// codebook with no codeword (`maxCwLen == 0`) yields a zero-width +/// segment. +#[must_use] +pub fn segment_width(cb: u8, length_of_longest_codeword: u8) -> u8 { + max_cw_len(cb).min(clamp_longest_codeword(length_of_longest_codeword)) +} + +/// Clamp the transmitted `length_of_longest_codeword` to its valid +/// range per §4.6.16.3.2: values `50..=63` are reserved and a current +/// decoder replaces them with `49`. +#[must_use] +pub fn clamp_longest_codeword(length_of_longest_codeword: u8) -> u8 { + length_of_longest_codeword.min(49) +} + +/// Clamp the transmitted `length_of_reordered_spectral_data` to its +/// valid range per §4.6.16.3.2. +/// +/// The maximum is `6144` bits for an SCE / CCE / LFE and `12288` bits +/// for a CPE; larger values are reserved and a current decoder +/// replaces them with the valid maximum. +#[must_use] +pub fn clamp_reordered_length(length_of_reordered_spectral_data: u16, is_cpe: bool) -> u16 { + let max = if is_cpe { 12288 } else { 6144 }; + length_of_reordered_spectral_data.min(max) +} + +/// The §4.6.16.3.3.2 segment layout for one `reordered_spectral_data()` +/// block: the per-segment bit widths derived from the active codebooks +/// and the two transmitted length fields. +/// +/// "Segments are instantiated until the available buffer is +/// exhausted, whereas the size of this buffer is given by +/// `length_of_reordered_spectral_data`. The remaining bits at the end +/// of the buffer increase the size of the last segment." +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Segmentation { + /// Per-segment bit width, in segment-instantiation order. The + /// final entry absorbs any remaining buffer bits, so it may exceed + /// its codebook's `segmentWidth`. + pub segment_bits: Vec, + /// `length_of_reordered_spectral_data` (clamped) — the total + /// buffer size in bits. `segment_bits` sums to exactly this. + pub total_bits: u32, +} + +impl Segmentation { + /// Build the segment layout from the pre-sorted PCW segment widths + /// and the (clamped) reordered-buffer length. + /// + /// `pcw_segment_widths` is the ordered list of + /// `segmentWidth = min(maxCwLen, length_of_longest_codeword)` for + /// each priority codeword in pre-sorted order. Segments are taken + /// in order while their cumulative width fits the buffer; once the + /// next full segment would overflow (or the list is exhausted), the + /// remaining buffer bits are folded into the last instantiated + /// segment. + /// + /// Returns an empty layout (`segment_bits` empty, `total_bits` as + /// given) when the buffer is zero-length. + #[must_use] + pub fn new(pcw_segment_widths: &[u8], total_bits: u32) -> Self { + let mut segment_bits: Vec = Vec::new(); + if total_bits == 0 { + return Segmentation { + segment_bits, + total_bits, + }; + } + + let mut used: u32 = 0; + for &w in pcw_segment_widths { + let w = u32::from(w); + if used + w > total_bits { + // The next full segment would overrun the buffer; stop + // instantiating new segments. The remainder folds into + // the last one below. + break; + } + segment_bits.push(w); + used += w; + } + + // The remaining bits at the end of the buffer increase the size + // of the last segment (§4.6.16.3.3.2). If no segment fit at all + // (every width exceeds the whole buffer, or the width list is + // empty), the whole buffer is one segment. + let remainder = total_bits - used; + if remainder > 0 { + if let Some(last) = segment_bits.last_mut() { + *last += remainder; + } else { + segment_bits.push(remainder); + } + } + + Segmentation { + segment_bits, + total_bits, + } + } + + /// `numberOfSegments` — the count of instantiated segments. + #[must_use] + pub fn number_of_segments(&self) -> usize { + self.segment_bits.len() + } + + /// The global bit offset of segment `i`'s first bit within the + /// reordered buffer (the running sum of preceding segment widths). + #[must_use] + pub(crate) fn segment_start(&self, i: usize) -> u32 { + self.segment_bits[..i].iter().sum() + } +} + +/// Write direction within a segment (§4.6.16.3.3.3). PCWs and +/// odd-numbered sets use [`Direction::Forward`] (left-to-right); +/// the direction toggles from set to set. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// Left-to-right: fill from the leftmost remaining bit of the + /// segment's free region. + Forward, + /// Right-to-left: fill from the rightmost remaining bit. + Backward, +} + +impl Direction { + /// Toggle the write direction (`ToggleWriteDirection()`). + #[must_use] + pub fn toggled(self) -> Self { + match self { + Direction::Forward => Direction::Backward, + Direction::Backward => Direction::Forward, + } + } +} + +/// The fully-resolved bit placement of every codeword in a +/// `reordered_spectral_data()` block — for each codeword, the ordered +/// list of global bit positions (within the reordered buffer) that +/// carry its bits, most-significant-bit first. +/// +/// This is the inverse of the §4.6.16.3.3.4 `ReorderSpectralData()` +/// writing scheme: it runs the same PCW-then-non-PCW set / trial loop +/// to determine *where* each codeword's bits land, so a decoder that +/// already knows each codeword's bit length (PCWs are decoded first +/// from the segment starts, then the non-PCW lengths become known) can +/// gather a codeword's scattered bits back into a contiguous codeword +/// for Huffman decoding. +/// +/// The bit lengths themselves come from Huffman-decoding the codewords +/// in place (the §4.6.16.3.4 decode references the ordinary +/// §4.6.3.3 spectral decode); this structure only resolves geometry +/// once those lengths are known. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReorderPlan { + /// `codeword_bits[c]` — the global buffer bit positions of + /// codeword `c`, in codeword-MSB-first order. + pub codeword_bits: Vec>, +} + +impl ReorderPlan { + /// Resolve the bit placement for `codeword_lengths` codewords over + /// `seg` segments, following the §4.6.16.3.3.4 writing scheme. + /// + /// * `codeword_lengths[c]` — the bit length of codeword `c`, in + /// pre-sorted order (PCWs first). `codeword_lengths.len()` is + /// `numberOfCodewords`. + /// * `seg` — the [`Segmentation`] giving `numberOfSegments` and the + /// per-segment bit widths. + /// + /// `numberOfSets = ceil(numberOfCodewords / numberOfSegments)`. The + /// first `numberOfSegments` codewords are the PCWs (set 0), each + /// written forward from its own segment's start; the rest are + /// non-PCWs distributed by the set / trial loop with the per-set + /// direction toggle. + /// + /// Returns `None` if the codewords do not fit the buffer (the sum + /// of `codeword_lengths` exceeds `total_bits`, or a segment + /// overflows) — a conforming stream always fits by construction. + #[must_use] + pub fn build(codeword_lengths: &[u32], seg: &Segmentation) -> Option { + let num_segments = seg.number_of_segments(); + let num_codewords = codeword_lengths.len(); + if num_segments == 0 { + return if num_codewords == 0 { + Some(ReorderPlan { + codeword_bits: Vec::new(), + }) + } else { + None + }; + } + + // Per-segment free-region cursors. Segment `s` spans local bits + // `[0, width)`. `low[s]` counts bits consumed from the low end + // (forward writes), `high[s]` counts bits consumed from the high + // end (backward writes). The free region is the local-bit range + // `[low[s], width - high[s])`; free bit count is + // `width - low[s] - high[s]`. + let widths: Vec = seg.segment_bits.clone(); + let mut low: Vec = vec![0; num_segments]; + let mut high: Vec = vec![0; num_segments]; + let seg_start: Vec = (0..num_segments).map(|s| seg.segment_start(s)).collect(); + + let mut codeword_bits: Vec> = vec![Vec::new(); num_codewords]; + // remainingBitsInCodeword[] + let mut remaining: Vec = codeword_lengths.to_vec(); + + // Inlined `WriteCodewordToSegment(cw, sg, dir)`: write up to + // `remaining[cw]` bits of codeword `cw` into the free region of + // segment `sg` in `dir`, recording the global bit positions + // MSB-first. Returns bits written. + macro_rules! write_cw_to_seg { + ($cw:expr, $sg:expr, $dir:expr) => {{ + let cw = $cw; + let sg = $sg; + let dir = $dir; + let free = widths[sg] - low[sg] - high[sg]; + let n = remaining[cw].min(free); + for _ in 0..n { + let local = match dir { + Direction::Forward => { + let l = low[sg]; + low[sg] += 1; + l + } + Direction::Backward => { + // Outermost free bit from the right. + let l = widths[sg] - 1 - high[sg]; + high[sg] += 1; + l + } + }; + codeword_bits[cw].push(seg_start[sg] + local); + } + remaining[cw] -= n; + n + }}; + } + + // First step: write PCWs (set 0). Codeword `i` → segment `i`, + // forward. + for codeword in 0..num_segments.min(num_codewords) { + write_cw_to_seg!(codeword, codeword, Direction::Forward); + } + + // numberOfSets = ceil(numberOfCodewords / numberOfSegments). + let num_sets = num_codewords.div_ceil(num_segments); + + // Second step: write non-PCWs (sets 1..num_sets). + let mut write_direction = Direction::Forward; + for set in 1..num_sets { + write_direction = write_direction.toggled(); + for trial in 0..num_segments { + for codeword_base in 0..num_segments { + let segment = (trial + codeword_base) % num_segments; + let codeword = codeword_base + set * num_segments; + if codeword >= num_codewords { + continue; + } + if remaining[codeword] > 0 { + write_cw_to_seg!(codeword, segment, write_direction); + } + } + } + } + + // Every codeword must be fully placed for a conforming stream. + if remaining.iter().any(|&r| r > 0) { + return None; + } + + Some(ReorderPlan { codeword_bits }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn max_cw_len_table_spot_checks() { + assert_eq!(max_cw_len(0), 0); + assert_eq!(max_cw_len(1), 11); + assert_eq!(max_cw_len(3), 20); + assert_eq!(max_cw_len(11), 49); + // No-codeword books. + assert_eq!(max_cw_len(13), 0); + assert_eq!(max_cw_len(15), 0); + // Virtual codebooks. + assert_eq!(max_cw_len(16), 14); + assert_eq!(max_cw_len(31), 41); + assert_eq!(max_cw_len(200), 0); + } + + #[test] + fn codebook_priority_table_spot_checks() { + assert_eq!(codebook_priority(0), 0); + assert_eq!(codebook_priority(1), 21); + assert_eq!(codebook_priority(2), 21); + assert_eq!(codebook_priority(10), 17); + assert_eq!(codebook_priority(11), 0); + assert_eq!(codebook_priority(16), 16); + assert_eq!(codebook_priority(31), 1); + assert_eq!(codebook_priority(99), 0); + } + + #[test] + fn assigned_unit_nr_long_window() { + // Long window: max_lines=1024, max_windows=1, window=0. + // unit at line 0, cb 1 (priority 21): 21*1024 + 0 = 21504. + assert_eq!(assigned_unit_nr(1, 1024, 0, 1, 0), 21504); + // Same line, higher cb 11 (priority 0): just the line offset. + assert_eq!(assigned_unit_nr(11, 1024, 0, 1, 0), 0); + // A higher-priority codebook sorts ahead of a lower one at the + // same line. + assert!(assigned_unit_nr(1, 1024, 4, 1, 0) > assigned_unit_nr(31, 1024, 4, 1, 0)); + } + + #[test] + fn assigned_unit_nr_short_window_interleaves_window() { + // Short window: max_lines=128, max_windows=8. Two units at the + // same line + codebook but different windows order by window. + let a = assigned_unit_nr(5, 128, 8, 8, 0); + let b = assigned_unit_nr(5, 128, 8, 8, 3); + assert_eq!(b - a, 3); + } + + #[test] + fn segment_width_is_min_of_maxcwlen_and_longest() { + // cb 3 maxCwLen 20, longest 16 → 16. + assert_eq!(segment_width(3, 16), 16); + // cb 2 maxCwLen 9, longest 16 → 9. + assert_eq!(segment_width(2, 16), 9); + // longest in reserved range clamps to 49. + assert_eq!(segment_width(11, 60), 49); + } + + #[test] + fn clamp_reordered_length_per_element_kind() { + assert_eq!(clamp_reordered_length(7000, false), 6144); + assert_eq!(clamp_reordered_length(7000, true), 7000); + assert_eq!(clamp_reordered_length(20000, true), 12288); + assert_eq!(clamp_reordered_length(100, false), 100); + } + + #[test] + fn segmentation_folds_remainder_into_last_segment() { + // Three PCW segments of width 10, 10, 10; buffer of 35 bits. + // All three fit (30 bits); the trailing 5 bits fold into the + // last segment → 10, 10, 15. + let seg = Segmentation::new(&[10, 10, 10], 35); + assert_eq!(seg.segment_bits, vec![10, 10, 15]); + assert_eq!(seg.number_of_segments(), 3); + assert_eq!(seg.segment_bits.iter().sum::(), 35); + } + + #[test] + fn segmentation_stops_before_overrun() { + // Widths 10, 10, 10 but only 25 bits of buffer: two full + // segments fit (20 bits); the third would overrun, so the + // remaining 5 bits fold into the second segment → 10, 15. + let seg = Segmentation::new(&[10, 10, 10], 25); + assert_eq!(seg.segment_bits, vec![10, 15]); + assert_eq!(seg.segment_bits.iter().sum::(), 25); + } + + #[test] + fn segmentation_single_segment_when_first_width_exceeds_buffer() { + // First width 50 > buffer 30: no full segment fits; the whole + // buffer becomes one segment. + let seg = Segmentation::new(&[50, 50], 30); + assert_eq!(seg.segment_bits, vec![30]); + assert_eq!(seg.number_of_segments(), 1); + } + + #[test] + fn segmentation_zero_buffer_is_empty() { + let seg = Segmentation::new(&[10, 10], 0); + assert!(seg.segment_bits.is_empty()); + assert_eq!(seg.number_of_segments(), 0); + assert_eq!(seg.total_bits, 0); + } + + #[test] + fn segmentation_exact_fit_no_remainder() { + let seg = Segmentation::new(&[8, 8, 8], 24); + assert_eq!(seg.segment_bits, vec![8, 8, 8]); + assert_eq!(seg.segment_bits.iter().sum::(), 24); + } + + // ---- ReorderPlan: bit-placement geometry ---- + + /// Assert the placement is a bijection over `[0, total_bits)`: every + /// codeword bit position is distinct and the union covers every + /// buffer bit exactly once (true whenever the codewords fully fill + /// the buffer). + fn assert_bijective(plan: &ReorderPlan, total_bits: u32) { + let mut seen = vec![false; total_bits as usize]; + let mut count = 0u32; + for cw in &plan.codeword_bits { + for &p in cw { + assert!(p < total_bits, "position {p} out of range"); + assert!(!seen[p as usize], "position {p} written twice"); + seen[p as usize] = true; + count += 1; + } + } + assert_eq!(count, total_bits, "not every buffer bit was covered"); + } + + #[test] + fn reorder_pcws_start_at_segment_boundaries() { + // 3 segments of 8 bits, 3 PCWs each exactly 8 bits long → each + // codeword fills its own segment, forward, starting at the + // segment boundary. + let seg = Segmentation::new(&[8, 8, 8], 24); + let plan = ReorderPlan::build(&[8, 8, 8], &seg).unwrap(); + assert_eq!(plan.codeword_bits[0], (0..8).collect::>()); + assert_eq!(plan.codeword_bits[1], (8..16).collect::>()); + assert_eq!(plan.codeword_bits[2], (16..24).collect::>()); + assert_bijective(&plan, 24); + } + + #[test] + fn reorder_nonpcws_fill_gaps_with_direction_toggle() { + // 2 segments of 10 bits = 20-bit buffer. Codewords: + // PCWs (set 0): cw0=4 bits, cw1=4 bits (start of each segment). + // Set 1 (backward): cw2=6, cw3=6 — fill the remaining 6 bits of + // each segment from the right. + let seg = Segmentation::new(&[10, 10], 20); + let plan = ReorderPlan::build(&[4, 4, 6, 6], &seg).unwrap(); + // cw0 forward at segment 0 start. + assert_eq!(plan.codeword_bits[0], vec![0, 1, 2, 3]); + // cw1 forward at segment 1 start (offset 10). + assert_eq!(plan.codeword_bits[1], vec![10, 11, 12, 13]); + // cw2 is the first non-PCW: set 1, trial 0, codeword_base 0 → + // segment 0, backward → bits 9,8,7,6,5,4. + assert_eq!(plan.codeword_bits[2], vec![9, 8, 7, 6, 5, 4]); + // cw3 → segment 1, backward → bits 19,18,17,16,15,14. + assert_eq!(plan.codeword_bits[3], vec![19, 18, 17, 16, 15, 14]); + assert_bijective(&plan, 20); + } + + #[test] + fn reorder_codeword_spanning_multiple_segments() { + // 3 segments of 5 bits = 15-bit buffer. PCWs cw0,cw1,cw2 each 3 + // bits (forward from each segment start, 2 free bits left each). + // Set 1 (backward): cw3=4, cw4=4, cw5=4 — each is longer than one + // segment's 2-bit remainder, so it spans into the next segment + // across trials (modulo shift). + let seg = Segmentation::new(&[5, 5, 5], 15); + let plan = ReorderPlan::build(&[3, 3, 3, 2, 2, 2], &seg).unwrap(); + // Total bits placed equals buffer size, bijective. + assert_bijective(&plan, 15); + // PCWs at segment starts. + assert_eq!(plan.codeword_bits[0], vec![0, 1, 2]); + assert_eq!(plan.codeword_bits[1], vec![5, 6, 7]); + assert_eq!(plan.codeword_bits[2], vec![10, 11, 12]); + } + + #[test] + fn reorder_partial_codeword_continues_next_trial() { + // 2 segments of 6 bits = 12-bit buffer. PCWs cw0=2, cw1=2 leave + // 4 free bits per segment. Set 1 backward: cw2=6, cw3=2. cw2 (6 + // bits) into segment 0's 4 free bits (trial 0) writes 4 bits; + // the remaining 2 spill into segment 1 on trial 1. cw3 (2 bits) + // goes into segment 1 trial 0. + let seg = Segmentation::new(&[6, 6], 12); + let plan = ReorderPlan::build(&[2, 2, 6, 2], &seg).unwrap(); + assert_bijective(&plan, 12); + assert_eq!(plan.codeword_bits[2].len(), 6); + assert_eq!(plan.codeword_bits[3].len(), 2); + } + + #[test] + fn reorder_rejects_overfull_buffer() { + // Codewords summing past the buffer don't fit. + let seg = Segmentation::new(&[8, 8], 16); + assert!(ReorderPlan::build(&[8, 8, 4], &seg).is_none()); + } + + #[test] + fn reorder_empty_block() { + let seg = Segmentation::new(&[], 0); + let plan = ReorderPlan::build(&[], &seg).unwrap(); + assert!(plan.codeword_bits.is_empty()); + } +} diff --git a/crates/vendor/oxideav-aac/src/hcr_decode.rs b/crates/vendor/oxideav-aac/src/hcr_decode.rs new file mode 100644 index 00000000..d95025d4 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/hcr_decode.rs @@ -0,0 +1,777 @@ +//! `reordered_spectral_data()` payload codec — ISO/IEC 14496-3 +//! §4.6.16.3.3 / §4.6.16.3.4: the Huffman-codeword-reordering (HCR) +//! bitstream payload, both directions. +//! +//! The [`crate::hcr`] module owns the deterministic geometry half of +//! the tool (the Table 4.170 `maxCwLen` table, the pre-sorting metric, +//! the [`crate::hcr::Segmentation`] layout, and the +//! [`crate::hcr::ReorderPlan`] writing-scheme walk). This module binds +//! that geometry to the actual spectral payload: +//! +//! * [`encode_reordered_spectral_data`] — the §4.6.16.3.3.4 +//! `ReorderSpectralData()` encoder: enumerate the frame's codewords +//! in §4.6.16.3.3.1 pre-sorted order, Huffman-encode each (the +//! codeword plus sign bits plus escape sequences: the §4.5.2.3.2 HCR +//! codeword unit), and scatter the bits over the segment grid with the +//! PCW-then-non-PCW set / trial loop. +//! * [`decode_reordered_spectral_data`] — the §4.6.16.3.4 decode: the +//! inverse walk. Codeword lengths are *not* transmitted; the PCWs +//! are decoded first, each from the start of its own segment (the +//! §4.6.16.3.3.2 `segmentWidth ≥` every same-book codeword +//! guarantees they fit), then the non-PCW sets are decoded through +//! the same trial loop the writer used — a codeword consumes bits +//! from a segment's free region (in the set's direction) until its +//! Huffman unit completes or the segment exhausts, in which case its +//! remainder continues in the next trial's segment. Because every +//! spectrum codebook is a complete prefix code, an incomplete bit +//! prefix is exactly distinguishable (bit-source underflow) from a +//! completed codeword, so the decoder discovers each codeword's +//! length precisely where the writer defined it. +//! +//! ## Codeword enumeration and pre-sorting (§4.6.16.3.3.1) +//! +//! A *unit* covers four spectral lines of one window: one 4-D codeword +//! or two 2-D codewords in natural (ascending-frequency) order. Unit +//! groups are collected ascending in spectral direction with the +//! windows of one spectral region in temporal order (the unit-based +//! window interleaving of Table 4.169 — §4.5.2.3.5 grouping interleave +//! does *not* apply under HCR), then stably ordered by the +//! `assignedUnitNr` metric (codebook priority first). The §4.6.16.4 +//! virtual codebooks 16..=31 carry ordinary codebook-11 spectrum (their +//! `maxCwLen` differs for the segment widths only). +//! +//! ## Provenance +//! +//! Everything follows the §4.6.16.3 text and pseudocode plus the +//! §4.5.2.3.2 codeword-unit definition ("the whole data necessary to +//! decode two or four lines … includes Huffman codeword, sign bits, +//! and escape sequences"), staged under `docs/audio/aac/`. No external +//! HCR implementation was consulted. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::hcr::{assigned_unit_nr, segment_width, Direction, Segmentation}; +use crate::ics_info::IcsInfo; +use crate::section_data::SectionData; +use crate::spectral_data::{ + decode_codeword, read_and_apply_signs, read_escape_sequence, write_tuple, SpectralData, +}; +#[cfg(test)] +use crate::swb_offset::{long_window_offsets, short_window_offsets}; +use crate::swb_offset::{LONG_WINDOW_LEN, SHORT_WINDOW_LEN}; +use crate::{Error, Result}; + +/// The `ESC_FLAG` magnitude of the escape book (§4.6.3.3). +const ESC_FLAG: i32 = 16; + +/// One HCR codeword in pre-sorted order: the §4.5.2.3.2 unit of "the +/// whole data necessary to decode two or four lines". +#[derive(Debug, Clone, Copy)] +struct HcrCodeword { + /// The section codebook (1..=11 spectrum books, or a §4.6.16.4 + /// virtual codebook 16..=31) — drives the segment width. + sect_cb: u8, + /// The codebook whose Huffman tables encode the lines (the virtual + /// codebooks decode as book 11). + decode_cb: u8, + /// Tuple dimension: 4 (books 1..=4) or 2. + dim: usize, + /// Window group index. + group: usize, + /// Index within the group's transmission-order buffer of the first + /// line of this codeword. + buf_index: usize, +} + +/// Enumerate the frame's spectral codewords in §4.6.16.3.3.1 +/// pre-sorted order. +/// +/// Walks every window of every group over the active scalefactor bands +/// (`sfb_cb[g][sfb]`), skipping the spectrum-less books (`ZERO`, +/// `NOISE`, intensity), and sorts stably by the `assignedUnitNr` +/// metric. `buf_index` targets the §4.5.2.3.5 transmission-order group +/// buffer layout [`SpectralData`] uses (sfb-major, window-in-group, +/// line), which keeps the rest of the decode chain unchanged. +fn enumerate_presorted( + ics_info: &IcsInfo, + section_data: &SectionData, + fs_index: u8, +) -> Result> { + let short = ics_info.window_sequence.is_eight_short(); + let window_len = ics_info.window_len()?; + let offsets = ics_info.swb_offsets(fs_index)?; + let max_lines = window_len as u32; + let max_windows: u32 = if short { 8 } else { 1 }; + let max_sfb = usize::from(ics_info.max_sfb); + if max_sfb > offsets.len() - 1 { + return Err(Error::SpectralDataInvalid); + } + if section_data.sfb_cb.len() != usize::from(ics_info.num_window_groups) { + return Err(Error::SpectralDataInvalid); + } + + let mut cws: Vec<(u32, HcrCodeword)> = Vec::new(); + let mut window_base = 0usize; // absolute index of the group's first window + for (g, cb_row) in section_data.sfb_cb.iter().enumerate() { + if cb_row.len() < max_sfb { + return Err(Error::SpectralDataInvalid); + } + let wgl = usize::from(ics_info.window_group_length[g]); + // Transmission-order offset of band `sfb` for window-in-group + // `b`: sum over earlier bands of `wgl · width`, plus + // `b · width(sfb)`. + let mut band_base = 0usize; + for sfb in 0..max_sfb { + let start = usize::from(offsets[sfb]); + let end = usize::from(offsets[sfb + 1]); + let width = end - start; + let cb = cb_row[sfb]; + let spec = classify_hcr(cb)?; + if let Some((decode_cb, dim)) = spec { + for b in 0..wgl { + let window = (window_base + b) as u32; + for line_off in (0..width).step_by(dim) { + let line = (start + line_off) as u32; + // A unit covers four lines; both 2-D codewords + // of one unit share its assignedUnitNr and keep + // their natural order (stable sort below). + let unit_line = line & !3; + let key = assigned_unit_nr(cb, max_lines, unit_line, max_windows, window); + cws.push(( + key, + HcrCodeword { + sect_cb: cb, + decode_cb, + dim, + group: g, + buf_index: band_base + b * width + line_off, + }, + )); + } + } + } + band_base += wgl * width; + } + window_base += wgl; + } + + cws.sort_by_key(|&(key, _)| key); + Ok(cws.into_iter().map(|(_, cw)| cw).collect()) +} + +/// Classify a section codebook for HCR: `None` for the spectrum-less +/// books, `(decode_cb, dim)` for the spectrum books, an error for the +/// reserved book 12. +fn classify_hcr(cb: u8) -> Result> { + match cb { + 0 | 13 | 14 | 15 => Ok(None), + 1..=4 => Ok(Some((cb, 4))), + 5..=11 => Ok(Some((cb, 2))), + // §4.6.16.4 virtual codebooks: ordinary book-11 spectrum with + // a limited value range (the limit shapes maxCwLen only). + 16..=31 => Ok(Some((11, 2))), + _ => Err(Error::SpectralDataInvalid), + } +} + +/// Encode one codeword unit (Huffman codeword + sign bits + escape +/// sequences) to a fresh bit vector. +fn encode_codeword_bits(cw: &HcrCodeword, values: &[i32]) -> Result<(Vec, u32)> { + let mut w = BitWriter::new(); + write_tuple(&mut w, cw.decode_cb, cw.dim, values)?; + let bits = w.bit_position() as u32; + Ok((w.finish(), bits)) +} + +/// §4.6.16.3.3.4 `ReorderSpectralData()` — encode a frame's spectrum +/// as a `reordered_spectral_data()` payload. +/// +/// Returns `(payload_bytes, length_of_reordered_spectral_data, +/// length_of_longest_codeword)`. The payload length is exactly the sum +/// of the codeword lengths (the writer transmits no slack), stored +/// MSB-first. +/// +/// `spectral` must use the same transmission-order layout +/// [`SpectralData::write`] consumes; `section_data.sfb_cb` may carry +/// §4.6.16.4 virtual codebooks (16..=31). +pub fn encode_reordered_spectral_data( + spectral: &SpectralData, + ics_info: &IcsInfo, + section_data: &SectionData, + fs_index: u8, +) -> Result<(Vec, u16, u8)> { + let cws = enumerate_presorted(ics_info, section_data, fs_index)?; + if spectral.x_quant.len() != usize::from(ics_info.num_window_groups) { + return Err(Error::SpectralDataEncodeInvalid); + } + + // Encode every codeword unit to its bit string. + let mut encoded: Vec<(Vec, u32)> = Vec::with_capacity(cws.len()); + let mut longest = 0u32; + for cw in &cws { + let buf = spectral + .x_quant + .get(cw.group) + .ok_or(Error::SpectralDataEncodeInvalid)?; + let vals = buf + .get(cw.buf_index..cw.buf_index + cw.dim) + .ok_or(Error::SpectralDataEncodeInvalid)?; + let e = encode_codeword_bits(cw, vals)?; + longest = longest.max(e.1); + encoded.push(e); + } + if longest > 49 { + // §4.6.16.3.2: valid lengths are 0..=49; the codeword units of + // the spectrum books never exceed this by construction. + return Err(Error::SpectralDataEncodeInvalid); + } + let total_bits: u32 = encoded.iter().map(|e| e.1).sum(); + if total_bits > 12288 { + return Err(Error::SpectralDataEncodeInvalid); + } + + // Segment grid + the writing-scheme bit placement. + let widths: Vec = cws + .iter() + .map(|cw| segment_width(cw.sect_cb, longest as u8)) + .collect(); + let seg = Segmentation::new(&widths, total_bits); + let lengths: Vec = encoded.iter().map(|e| e.1).collect(); + let plan = + crate::hcr::ReorderPlan::build(&lengths, &seg).ok_or(Error::SpectralDataEncodeInvalid)?; + + // Scatter the codeword bits to their planned buffer positions. + let mut out = vec![0u8; (total_bits as usize).div_ceil(8)]; + for (c, (bytes, len)) in encoded.iter().enumerate() { + for bit in 0..*len { + let set = bytes[(bit / 8) as usize] & (0x80 >> (bit % 8)) != 0; + if set { + let pos = plan.codeword_bits[c][bit as usize]; + out[(pos / 8) as usize] |= 0x80 >> (pos % 8); + } + } + } + Ok((out, total_bits as u16, longest as u8)) +} + +/// The per-segment cursor pair of the §4.6.16.3.3.3 walk: bits +/// consumed from the low (forward) and high (backward) ends. +struct SegCursor { + start: u32, + width: u32, + low: u32, + high: u32, +} + +impl SegCursor { + fn free(&self) -> u32 { + self.width - self.low - self.high + } + + /// Collect the segment's free bits in `dir` order (the order the + /// writer would have placed a codeword's bits). + fn free_bits(&self, payload: &[u8], dir: Direction) -> Vec { + let read = |local: u32| { + let pos = self.start + local; + payload[(pos / 8) as usize] & (0x80 >> (pos % 8)) != 0 + }; + match dir { + Direction::Forward => (self.low..self.width - self.high).map(read).collect(), + Direction::Backward => (self.low..self.width - self.high).rev().map(read).collect(), + } + } + + /// Consume `n` bits from the `dir` end. + fn consume(&mut self, n: u32, dir: Direction) { + match dir { + Direction::Forward => self.low += n, + Direction::Backward => self.high += n, + } + } +} + +/// The in-flight decode state of one codeword: the bits gathered so +/// far and, once complete, the decoded lines. +struct CodewordState { + bits: Vec, + done: bool, + values: [i32; 4], +} + +/// Try to decode a whole codeword unit from `bits`. Returns +/// `Ok(Some((consumed_bits, values)))` when the unit completes within +/// `bits`, `Ok(None)` when more bits are needed (bit-source +/// underflow), or a hard error for a genuinely invalid unit. +fn try_decode_unit(cw: &HcrCodeword, bits: &[bool]) -> Result> { + // Pack MSB-first. + let mut bytes = vec![0u8; bits.len().div_ceil(8)]; + for (i, &b) in bits.iter().enumerate() { + if b { + bytes[i / 8] |= 0x80 >> (i % 8); + } + } + let mut r = BitReader::new(&bytes); + // Mirror the SpectralData::parse per-tuple sequence: hcod → sign + // bits → escape sequences. + let step = (|| -> Result<[i32; 4]> { + let idx = decode_codeword(&mut r, cw.decode_cb)?; + let tuple = crate::spectral_codebook::decode_index_to_tuple(cw.decode_cb, idx)?; + let mut tuple = read_and_apply_signs(&mut r, cw.decode_cb, cw.dim, tuple)?; + if cw.decode_cb == 11 { + for v in tuple.iter_mut().take(cw.dim) { + if v.abs() == ESC_FLAG { + let mag = read_escape_sequence(&mut r)? as i32; + *v = if *v < 0 { -mag } else { mag }; + } + } + } + Ok(tuple) + })(); + match step { + Ok(tuple) => { + let consumed = r.bit_position() as u32; + if consumed as usize > bits.len() { + // The packed byte buffer is padded to a byte boundary; + // a "completion" that consumed padding bits is phantom + // — the genuine continuation bits arrive in a later + // trial's segment. + return Ok(None); + } + Ok(Some((consumed, tuple))) + } + Err(Error::UnexpectedEnd) => Ok(None), + Err(e) => Err(e), + } +} + +/// §4.6.16.3.4 — decode a `reordered_spectral_data()` payload back to +/// the transmission-order [`SpectralData`]. +/// +/// * `payload` — the reordered buffer, MSB-first; +/// `length_of_reordered_spectral_data` (already clamped per +/// §4.6.16.3.2 by the caller if reserved) selects the bit count. +/// * `length_of_longest_codeword` — the transmitted 6-bit field +/// (clamped internally per §4.6.16.3.2). +/// +/// The decode runs the exact §4.6.16.3.3.4 walk with the codeword +/// lengths discovered by Huffman completion; see the module notes. +pub fn decode_reordered_spectral_data( + payload: &[u8], + length_of_reordered_spectral_data: u16, + length_of_longest_codeword: u8, + ics_info: &IcsInfo, + section_data: &SectionData, + fs_index: u8, +) -> Result { + let total_bits = u32::from(length_of_reordered_spectral_data); + if (payload.len() as u32) * 8 < total_bits { + return Err(Error::UnexpectedEnd); + } + let cws = enumerate_presorted(ics_info, section_data, fs_index)?; + + // Segment grid, exactly as the writer derived it. + let widths: Vec = cws + .iter() + .map(|cw| segment_width(cw.sect_cb, length_of_longest_codeword)) + .collect(); + let seg = Segmentation::new(&widths, total_bits); + let num_segments = seg.number_of_segments(); + if num_segments == 0 { + if cws.is_empty() { + return empty_spectral(ics_info); + } + return Err(Error::SpectralDataInvalid); + } + + let mut cursors: Vec = (0..num_segments) + .map(|s| SegCursor { + start: seg.segment_start(s), + width: seg.segment_bits[s], + low: 0, + high: 0, + }) + .collect(); + let mut states: Vec = cws + .iter() + .map(|_| CodewordState { + bits: Vec::new(), + done: false, + values: [0; 4], + }) + .collect(); + + // Feed a codeword from one segment: append free bits, try to + // complete; consume what the codeword actually used (all free bits + // if it is still incomplete). + let feed = |state: &mut CodewordState, + cw: &HcrCodeword, + cursor: &mut SegCursor, + dir: Direction| + -> Result<()> { + if state.done || cursor.free() == 0 { + return Ok(()); + } + let already = state.bits.len() as u32; + let fresh = cursor.free_bits(payload, dir); + state.bits.extend_from_slice(&fresh); + match try_decode_unit(cw, &state.bits)? { + Some((consumed, values)) => { + if consumed < already { + return Err(Error::SpectralDataInvalid); + } + cursor.consume(consumed - already, dir); + state.bits.truncate(consumed as usize); + state.values = values; + state.done = true; + } + None => { + // Uses every free bit of this segment and continues. + cursor.consume(fresh.len() as u32, dir); + } + } + Ok(()) + }; + + // First step: decode PCWs (set 0), codeword i forward from segment i. + for i in 0..num_segments.min(cws.len()) { + feed(&mut states[i], &cws[i], &mut cursors[i], Direction::Forward)?; + if !states[i].done { + // A PCW always fits its own segment (§4.6.16.3.3.2); not + // completing means the stream is corrupt. + return Err(Error::SpectralDataInvalid); + } + } + + // Second step: the non-PCW sets with the per-set direction toggle + // and the modulo-shift trial loop. + let num_sets = cws.len().div_ceil(num_segments); + let mut direction = Direction::Forward; + for set in 1..num_sets { + direction = direction.toggled(); + for trial in 0..num_segments { + for codeword_base in 0..num_segments { + let segment = (trial + codeword_base) % num_segments; + let codeword = codeword_base + set * num_segments; + if codeword >= cws.len() { + continue; + } + feed( + &mut states[codeword], + &cws[codeword], + &mut cursors[segment], + direction, + )?; + } + } + // §4.6.16.3.3.3: after at most N trials every codeword of the + // set is complete on a conforming stream. + for base in 0..num_segments { + let codeword = base + set * num_segments; + if codeword < cws.len() && !states[codeword].done { + return Err(Error::SpectralDataInvalid); + } + } + } + + // Scatter the decoded lines into the transmission-order buffers. + let mut spectral = empty_spectral(ics_info)?; + for (cw, state) in cws.iter().zip(states.iter()) { + let buf = spectral + .x_quant + .get_mut(cw.group) + .ok_or(Error::SpectralDataInvalid)?; + let dst = buf + .get_mut(cw.buf_index..cw.buf_index + cw.dim) + .ok_or(Error::SpectralDataInvalid)?; + dst.copy_from_slice(&state.values[..cw.dim]); + } + Ok(spectral) +} + +/// An all-zero transmission-order [`SpectralData`] with the group +/// buffer geometry of `ics_info`. +fn empty_spectral(ics_info: &IcsInfo) -> Result { + let short = ics_info.window_sequence.is_eight_short(); + let x_quant = ics_info + .window_group_length + .iter() + .map(|&wgl| { + let len = if short { + usize::from(wgl) * SHORT_WINDOW_LEN as usize + } else { + LONG_WINDOW_LEN as usize + }; + vec![0i32; len] + }) + .collect(); + Ok(SpectralData { x_quant }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape, NUM_SWB_LONG_WINDOW}; + use crate::section_data::Section; + + const FS: u8 = 4; // 44.1 kHz + + fn long_ics(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: NUM_SWB_LONG_WINDOW[FS as usize], + } + } + + /// An `EIGHT_SHORT` ics_info with two groups (3 + 5 windows). + fn short_ics(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups: 2, + window_group_length: vec![3, 5], + num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[FS as usize], + } + } + + fn section_data_for(sfb_cb_rows: Vec>) -> SectionData { + let sections = sfb_cb_rows + .iter() + .map(|row| { + // One section per band keeps the geometry simple. + row.iter() + .enumerate() + .map(|(sfb, &cb)| Section { + codebook: cb, + start: sfb as u8, + end: sfb as u8 + 1, + }) + .collect() + }) + .collect(); + SectionData { + sections, + sfb_cb: sfb_cb_rows, + } + } + + /// Deterministic pseudo-random value in `-max..=max`. + fn prand(state: &mut u32, max: i32) -> i32 { + *state = state.wrapping_mul(1664525).wrapping_add(1013904223); + let span = 2 * max + 1; + ((*state >> 8) % span as u32) as i32 - max + } + + /// Fill the active bands of a transmission-order spectrum with + /// bounded pseudo-random values per the band's codebook LAV. + fn fill_spectrum(ics: &IcsInfo, sd: &SectionData, seed: u32) -> SpectralData { + let mut state = seed; + let mut spectral = empty_spectral(ics).unwrap(); + let short = ics.window_sequence.is_eight_short(); + let offsets = if short { + short_window_offsets(FS).unwrap() + } else { + long_window_offsets(FS).unwrap() + }; + for (g, row) in sd.sfb_cb.iter().enumerate() { + let wgl = usize::from(ics.window_group_length[g]); + let mut base = 0usize; + for (sfb, &cb) in row.iter().enumerate().take(usize::from(ics.max_sfb)) { + let width = usize::from(offsets[sfb + 1] - offsets[sfb]); + let max = match cb { + 0 | 13 | 14 | 15 => 0, + 1 | 2 => 1, + 3 | 4 => 2, + 5 | 6 => 4, + 7 | 8 => 7, + 9 | 10 => 12, + // ESC book: exercise escapes with magnitudes > 16. + 11 => 40, + _ => 15, + }; + if max > 0 { + for i in 0..wgl * width { + spectral.x_quant[g][base + i] = prand(&mut state, max); + } + } + base += wgl * width; + } + } + spectral + } + + /// Round-trip: encode → decode reproduces the exact quantized + /// spectrum, across a codebook mix that forces multiple sets and + /// non-PCW segment spanning (long window). + #[test] + fn round_trips_long_window_mixed_codebooks() { + let ics = long_ics(12); + let sd = section_data_for(vec![vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11]]); + let spectral = fill_spectrum(&ics, &sd, 0xC0FFEE); + let (payload, len_bits, longest) = + encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); + assert!(len_bits > 0 && longest > 0); + let back = + decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); + assert_eq!(back.x_quant, spectral.x_quant); + } + + /// Round-trip with ZERO_HCB holes and an intensity band mixed in + /// (no spectrum transmitted for those bands). + #[test] + fn round_trips_with_spectrumless_bands() { + let ics = long_ics(10); + let sd = section_data_for(vec![vec![3, 0, 5, 15, 11, 0, 9, 1, 0, 7]]); + let spectral = fill_spectrum(&ics, &sd, 0xBADF00D); + let (payload, len_bits, longest) = + encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); + let back = + decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); + assert_eq!(back.x_quant, spectral.x_quant); + } + + /// Eight-short round-trip with two window groups: the §4.6.16.3.3.1 + /// unit-based window interleave (not the §4.5.2.3.5 grouping + /// interleave) must be applied consistently on both sides. + #[test] + fn round_trips_eight_short_two_groups() { + let ics = short_ics(8); + let sd = section_data_for(vec![ + vec![1, 3, 5, 7, 9, 11, 2, 4], + vec![11, 9, 7, 5, 3, 1, 4, 2], + ]); + let spectral = fill_spectrum(&ics, &sd, 0x5EED); + let (payload, len_bits, longest) = + encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); + let back = + decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); + assert_eq!(back.x_quant, spectral.x_quant); + } + + /// The payload survives trailing slack: a buffer longer than the + /// codeword bits (larger transmitted length) still decodes — the + /// slack widens the last segment, exactly as §4.6.16.3.3.2 + /// specifies. + #[test] + fn decodes_with_trailing_slack_bits() { + let ics = long_ics(6); + let sd = section_data_for(vec![vec![2, 4, 6, 8, 10, 11]]); + let spectral = fill_spectrum(&ics, &sd, 0xABCDEF); + let (payload, len_bits, longest) = + encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); + // Re-plan with 16 slack bits: the writer must scatter into the + // wider grid and the decoder must follow. + let slack_bits = len_bits + 16; + let cws = enumerate_presorted(&ics, &sd, FS).unwrap(); + let widths: Vec = cws + .iter() + .map(|cw| segment_width(cw.sect_cb, longest)) + .collect(); + let seg = Segmentation::new(&widths, u32::from(slack_bits)); + let mut encoded = Vec::new(); + for cw in &cws { + let vals = &spectral.x_quant[cw.group][cw.buf_index..cw.buf_index + cw.dim]; + encoded.push(encode_codeword_bits(cw, vals).unwrap()); + } + let lengths: Vec = encoded.iter().map(|e| e.1).collect(); + let plan = crate::hcr::ReorderPlan::build(&lengths, &seg).unwrap(); + let mut wide = vec![0u8; (slack_bits as usize).div_ceil(8)]; + for (c, (bytes, len)) in encoded.iter().enumerate() { + for bit in 0..*len { + if bytes[(bit / 8) as usize] & (0x80 >> (bit % 8)) != 0 { + let pos = plan.codeword_bits[c][bit as usize]; + wide[(pos / 8) as usize] |= 0x80 >> (pos % 8); + } + } + } + let back = + decode_reordered_spectral_data(&wide, slack_bits, longest, &ics, &sd, FS).unwrap(); + assert_eq!(back.x_quant, spectral.x_quant); + let _ = payload; + } + + /// Virtual codebooks (16..=31) decode as book 11 with their own + /// segment widths. + #[test] + fn round_trips_virtual_codebooks() { + let ics = long_ics(6); + // VCB 17 pairs with small magnitudes; VCB 31 with escapes. + let sd = section_data_for(vec![vec![17, 31, 1, 16, 20, 11]]); + let mut spectral = empty_spectral(&ics).unwrap(); + let offsets = long_window_offsets(FS).unwrap(); + let mut state = 0x1234u32; + for sfb in 0..6usize { + let (a, b) = (usize::from(offsets[sfb]), usize::from(offsets[sfb + 1])); + let max = match sfb { + 0 | 3 => 3, // VCB 17 / 16: modest values + 1 | 4 => 30, // VCB 31 / 20: escapes + 2 => 1, // book 1 quads + _ => 40, // book 11 + }; + for i in a..b { + spectral.x_quant[0][i] = prand(&mut state, max); + } + } + let (payload, len_bits, longest) = + encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); + let back = + decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); + assert_eq!(back.x_quant, spectral.x_quant); + } + + /// A corrupt payload surfaces an error, not a panic: flip bits in + /// the PCW region. + #[test] + fn corrupt_payload_errors_cleanly() { + let ics = long_ics(8); + let sd = section_data_for(vec![vec![1, 2, 3, 4, 5, 6, 7, 8]]); + let spectral = fill_spectrum(&ics, &sd, 0xFEED); + let (mut payload, len_bits, longest) = + encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); + for byte in payload.iter_mut().take(4) { + *byte ^= 0xFF; + } + // Either decodes to different values or errors — it must not + // panic, and it must not silently return the original. + if let Ok(back) = decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS) + { + assert_ne!(back.x_quant, spectral.x_quant); + } + } + + /// Pre-sorting puts the ESC-book codewords first (priority 0) and + /// the book-1/2 codewords last (priority 21). + #[test] + fn presort_orders_esc_first() { + let ics = long_ics(3); + let sd = section_data_for(vec![vec![1, 11, 5]]); + let cws = enumerate_presorted(&ics, &sd, FS).unwrap(); + assert!(!cws.is_empty()); + assert_eq!(cws.first().unwrap().sect_cb, 11); + assert_eq!(cws.last().unwrap().sect_cb, 1); + } +} diff --git a/crates/vendor/oxideav-aac/src/ics_body.rs b/crates/vendor/oxideav-aac/src/ics_body.rs new file mode 100644 index 00000000..1c7fee37 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ics_body.rs @@ -0,0 +1,837 @@ +//! `individual_channel_stream()` body walker — ISO/IEC 14496-3 §4.4.6 / +//! Table 4.50. +//! +//! This module composes the existing per-tool parsers / writers +//! (`global_gain`, [`crate::ics_info`], [`crate::section_data`], +//! [`crate::scale_factor_data`], [`crate::pulse_data`], +//! [`crate::tns_data`], [`crate::gain_control_data`]) into the +//! Table 4.50 channel-element body, **up to but not including** +//! `spectral_data()`. +//! +//! ## Why "up to but not including" +//! +//! `spectral_data()` (Table 4.56) is the per-band Huffman-coded +//! quantised MDCT-coefficient block. Its walker lives in the +//! dedicated [`crate::spectral_data`] module (round 281): this body +//! walker stops at the bit position immediately after +//! `gain_control_data()` (or the dispatching +//! `gain_control_data_present` bit when the tool is omitted) and +//! surfaces that position as [`IcsBody::spectral_data_bit_offset`], +//! from which [`crate::spectral_data::SpectralData::parse`] consumes +//! the spectrum in place — see `tests/spectral_data.rs` for the +//! sequential composition. Keeping the two stages separate mirrors +//! the CPE shared-`ics_info` split: the caller owns the reader and +//! decides when to hand off. +//! +//! This is consistent with the round-200 README ("Phase 2 in +//! progress + channel-element body walker still pending") — the +//! Walker in [`crate::raw_data_block`] emits a `ChannelElement` +//! event but does not consume the body, so the caller has to +//! re-bind a [`BitReader`] to the body region and call this module +//! to parse the structural per-tool layout. +//! +//! ## Table 4.50 layout (the non-scalable branch) +//! +//! ```text +//! individual_channel_stream(common_window, scale_flag) { +//! global_gain; 8 uimsbf +//! if (!common_window && !scale_flag) { +//! ics_info(); +//! } +//! section_data(); +//! scale_factor_data(); +//! if (!scale_flag) { +//! pulse_data_present; 1 uimsbf +//! if (pulse_data_present) pulse_data(); +//! tns_data_present; 1 uimsbf +//! if (tns_data_present) tns_data(); +//! gain_control_data_present; 1 uimsbf +//! if (gain_control_data_present) gain_control_data(); +//! } +//! if (!aacSpectralDataResilienceFlag) { +//! spectral_data(); // NOT covered here +//! } else { +//! length_of_reordered_spectral_data; +//! length_of_longest_codeword; +//! reordered_spectral_data(); // NOT covered here +//! } +//! } +//! ``` +//! +//! Per Table 4.50 the `common_window` flag is set by the surrounding +//! `channel_pair_element()` (Table 4.4) when the two channels of the +//! CPE share the `ics_info()`; in that case the *first* call to +//! `individual_channel_stream()` reads the shared `ics_info()` (the +//! caller of this module does that — by, say, invoking +//! [`crate::ics_info::IcsInfo::parse`] directly — and then calls +//! [`IcsBody::parse_with_ics_info`]). For the single-channel form +//! (SCE / LFE) `common_window == false` and the body reads its own +//! `ics_info()` inline; the caller invokes [`IcsBody::parse`] and the +//! module both reads `ics_info()` and surfaces it. +//! +//! `scale_flag` is set by scalable streams (AOT 6) when the +//! `aac_scalable_main_header()` carries side-info that already +//! dispatched the pulse / TNS / gain-control tools. Phase 2 does not +//! yet support the scalable extension; this module rejects +//! `scale_flag == true` with [`crate::Error::NotImplemented`] so the +//! existing SCE / CPE / LFE callers keep their bit-exact round-trip. +//! +//! ## What this module covers +//! +//! * [`IcsBody::parse`] — reads `global_gain`, the inline +//! `ics_info()`, `section_data()`, `scale_factor_data()`, the three +//! `*_present` dispatch bits, and the dispatched +//! `pulse_data()` / `tns_data()` / `gain_control_data()` bodies. +//! * [`IcsBody::parse_with_ics_info`] — same minus the `ics_info()` +//! read; the caller supplies the parsed [`crate::ics_info::IcsInfo`] +//! that the CPE-shared-info path already produced. +//! * [`IcsBody::write`] — the symmetric writer that round-trips the +//! parsed `IcsBody` back to a bit-exact Table 4.50 prefix +//! (everything up to and including `gain_control_data_present` / +//! its body). The `spectral_data()` portion is the caller's +//! responsibility (typically `push_channel_body_bits` on a +//! [`crate::raw_data_block::FrameAssembler`]). +//! * [`IcsBody::write_with_ics_info`] — same minus the `ics_info()` +//! write. +//! +//! Field validity: +//! +//! * Pulse-data is only legal when `window_sequence != EIGHT_SHORT` +//! per Table 4.50 / Table 4.7; the parser surfaces +//! [`crate::Error::PulseDataEncodeInvalid`] on a violation, the +//! writer rejects the same shape before emitting. +//! * Gain-control-data is only legal when `audioObjectType == 3` +//! (SSR) per the §4.6.12 normative constraint; the parser does not +//! enforce this (it surfaces the dispatching bit verbatim so a +//! non-SSR stream with the bit set still round-trips) but the +//! writer does, to keep the FrameAssembler emitting only +//! conforming streams. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::asc::AacResilienceFlags; +use crate::gain_control_data::GainControlData; +use crate::ics_info::{IcsInfo, WindowSequence}; +use crate::pulse_data::PulseData; +use crate::scale_factor_data::{ErScaleFactorData, ScaleFactorData}; +use crate::section_data::SectionData; +use crate::swb_offset::FrameFamily; +use crate::tns_data::TnsData; +use crate::{Error, Result}; + +/// Field width of `global_gain` (Table 4.50). +pub const GLOBAL_GAIN_BITS: u32 = 8; + +/// AOT value for AAC SSR (the only AOT that uses +/// `gain_control_data()`). +pub const AOT_AAC_SSR: u8 = 3; + +/// Parsed `individual_channel_stream()` body per Table 4.50, up to +/// but not including `spectral_data()`. +/// +/// The trailing `spectral_data()` block is the caller's +/// responsibility — see the module docs for the rationale. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IcsBody { + /// `global_gain` — 8-bit `uimsbf` per Table 4.50. The seed for + /// the §4.6.2.3.2 scalefactor DPCM accumulator + /// (`last_sf = global_gain` at the top of every frame). + pub global_gain: u8, + /// `ics_info()` (Table 4.6). `None` only when + /// [`IcsBody::parse_with_ics_info`] / [`IcsBody::write_with_ics_info`] + /// were used and the caller's `IcsInfo` is held outside this + /// struct (CPE shared-info form). + pub ics_info: Option, + /// `section_data()` (ISO/IEC 13818-7 §6.3 Table 17). + pub section_data: SectionData, + /// `scale_factor_data()` (Table 4.53, non-resilient branch). + pub scale_factor_data: ScaleFactorData, + /// `pulse_data_present` (1 bit). When `true`, [`Self::pulse_data`] + /// carries the dispatched Table 4.7 record. + pub pulse_data_present: bool, + /// `pulse_data()` (Table 4.7). Populated when + /// `pulse_data_present == true`. + pub pulse_data: Option, + /// `tns_data_present` (1 bit). When `true`, [`Self::tns_data`] + /// carries the dispatched Table 4.54 record. + pub tns_data_present: bool, + /// `tns_data()` (Table 4.54). Populated when + /// `tns_data_present == true`. + pub tns_data: Option, + /// `gain_control_data_present` (1 bit). When `true`, + /// [`Self::gain_control_data`] carries the dispatched Table 4.12 + /// record. + pub gain_control_data_present: bool, + /// `gain_control_data()` (Table 4.12). Populated when + /// `gain_control_data_present == true`. + pub gain_control_data: Option, + /// Bit position of the *first* `spectral_data()` bit, measured + /// from the start of this `individual_channel_stream()` body + /// (i.e. the bit reader's position when [`IcsBody::parse`] was + /// invoked is `0` here). Useful for callers that need to slice + /// the spectrum block out of a parent buffer or hand it to a + /// spectral-data parser without re-walking the body. + pub spectral_data_bit_offset: u64, + /// The error-resilient `scale_factor_data()` record (RVLC branch, + /// Table 4.53) when the body was parsed via [`IcsBody::parse_er`] / + /// [`IcsBody::parse_with_ics_info_er`] with + /// `aacScalefactorDataResilienceFlag == 1`. `None` on the + /// non-resilient path. The reconstructed absolute-delta records + /// are mirrored into [`Self::scale_factor_data`] so the shared + /// §4.6.2.3.2 accumulate pass consumes the body unchanged + /// regardless of which branch produced it; this field preserves + /// the extra RVLC backward seeds (`rev_global_gain`, + /// `dpcm_*_last_position`). + pub er_scale_factor_data: Option, + /// The §4.4.2.7 Table 4.50 spectral-resilience length fields, + /// present only when the body was parsed via the ER path with + /// `aacSpectralDataResilienceFlag == 1`: + /// `(length_of_reordered_spectral_data, length_of_longest_codeword)`. + /// The `reordered_spectral_data()` (HCR) payload that follows is + /// the caller's responsibility (same contract as the non-resilient + /// `spectral_data()` block); these two counts size that payload. + pub reordered_spectral_lengths: Option<(u16, u8)>, +} + +impl IcsBody { + /// Parse a Table 4.50 channel-element body whose `ics_info()` is + /// inline (the single-channel `SCE` / `LFE` form, or the + /// non-shared `CPE` form). + /// + /// * `reader` — positioned at the first bit of the + /// `individual_channel_stream()` body (i.e. at `global_gain`). + /// * `audio_object_type` — the surrounding ASC's effective AOT + /// (post SBR/PS unwrap). Drives the Table 4.6 / 4.55 predictor + /// branch and the SSR-only `gain_control_data` gate. + /// * `sampling_frequency_index` — the surrounding ASC's + /// `samplingFrequencyIndex` (the *core* index for hierarchical + /// SBR / PS). + /// * `scale_flag` — Table 4.50's outer `scale_flag` (set by + /// scalable AAC, AOT 6). The Phase 2 surface rejects + /// `scale_flag == true` with [`Error::NotImplemented`]. + /// + /// Errors propagate from the underlying per-tool parsers: + /// [`Error::UnexpectedEnd`] on bit-reader underflow, + /// [`Error::IcsInfoUnsupportedSampleRateIndex`] on an out-of-range + /// `fs_index`, [`Error::SectionDataOverrun`] on a non-conforming + /// `section_data()`, [`Error::PulseDataEncodeInvalid`] when the + /// stream sets `pulse_data_present == 1` on an + /// `EIGHT_SHORT_SEQUENCE` (Table 4.50 Note 1). + pub fn parse( + reader: &mut BitReader<'_>, + audio_object_type: u8, + sampling_frequency_index: u8, + scale_flag: bool, + ) -> Result { + // CPE-shared-info form is handled by parse_with_ics_info; the + // public `parse` always reads its own ics_info, which matches + // the SCE / LFE / non-common-window CPE case. + Self::parse_family( + reader, + FrameFamily::Lc1024, + audio_object_type, + sampling_frequency_index, + scale_flag, + ) + } + + /// [`IcsBody::parse`] under an explicit §4.5.1.1 frame-length + /// family (the inline `ics_info()` is parsed with + /// [`IcsInfo::parse_family`], so the 960 / LD band geometry and + /// the LD `ONLY_LONG` constraint apply). + pub fn parse_family( + reader: &mut BitReader<'_>, + family: FrameFamily, + audio_object_type: u8, + sampling_frequency_index: u8, + scale_flag: bool, + ) -> Result { + Self::parse_inner( + reader, + family, + audio_object_type, + sampling_frequency_index, + false, + scale_flag, + ) + } + + /// Parse a Table 4.50 channel-element body whose `ics_info()` was + /// already consumed by the surrounding shared-info `CPE` form. + /// + /// The supplied `ics_info` drives the same `num_window_groups` / + /// `max_sfb` / `window_sequence` dependencies the inline path + /// would otherwise derive. + /// + /// `scale_flag` semantics mirror [`IcsBody::parse`]. The returned + /// `IcsBody::ics_info` is `None` — the caller holds the shared + /// `IcsInfo` outside the per-channel body. + pub fn parse_with_ics_info( + reader: &mut BitReader<'_>, + ics_info: &IcsInfo, + audio_object_type: u8, + scale_flag: bool, + ) -> Result { + if scale_flag { + return Err(Error::NotImplemented); + } + let start = reader.bit_position(); + let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; + let section_data = SectionData::parse( + reader, + ics_info.window_sequence, + ics_info.num_window_groups, + ics_info.max_sfb, + )?; + let scale_factor_data = ScaleFactorData::parse(reader, §ion_data.sfb_cb)?; + + let tools = parse_tools(reader, ics_info, audio_object_type, start)?; + + Ok(IcsBody { + global_gain, + ics_info: None, + section_data, + scale_factor_data, + pulse_data_present: tools.pulse_data_present, + pulse_data: tools.pulse_data, + tns_data_present: tools.tns_data_present, + tns_data: tools.tns_data, + gain_control_data_present: tools.gain_control_data_present, + gain_control_data: tools.gain_control_data, + spectral_data_bit_offset: tools.spectral_data_bit_offset, + er_scale_factor_data: None, + reordered_spectral_lengths: None, + }) + } + + fn parse_inner( + reader: &mut BitReader<'_>, + family: FrameFamily, + audio_object_type: u8, + sampling_frequency_index: u8, + common_window: bool, + scale_flag: bool, + ) -> Result { + if scale_flag { + return Err(Error::NotImplemented); + } + let start = reader.bit_position(); + let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; + // Table 4.50: `if (!common_window && !scale_flag) ics_info();` + // — `parse` is the !common_window path (the caller of + // `parse_with_ics_info` covers the other branch). + let ics_info = IcsInfo::parse_family( + reader, + family, + audio_object_type, + sampling_frequency_index, + common_window, + )?; + let section_data = SectionData::parse( + reader, + ics_info.window_sequence, + ics_info.num_window_groups, + ics_info.max_sfb, + )?; + let scale_factor_data = ScaleFactorData::parse(reader, §ion_data.sfb_cb)?; + + let tools = parse_tools(reader, &ics_info, audio_object_type, start)?; + + Ok(IcsBody { + global_gain, + ics_info: Some(ics_info), + section_data, + scale_factor_data, + pulse_data_present: tools.pulse_data_present, + pulse_data: tools.pulse_data, + tns_data_present: tools.tns_data_present, + tns_data: tools.tns_data, + gain_control_data_present: tools.gain_control_data_present, + gain_control_data: tools.gain_control_data, + spectral_data_bit_offset: tools.spectral_data_bit_offset, + er_scale_factor_data: None, + reordered_spectral_lengths: None, + }) + } + + /// Parse an **error-resilient** Table 4.50 channel-element body + /// (the ER General Audio object types — AOTs 17 / 19 / 20 / 23 — + /// whose ASC carries the [`AacResilienceFlags`] triplet). + /// + /// Differs from [`IcsBody::parse`] in three spec-driven ways + /// (Table 4.50 / Table 4.52 / Table 4.53): + /// + /// * `section_data()` takes the [`SectionData::parse_er`] branch + /// when `resilience.section_data` is set (5-bit `sect_cb`). + /// * `scale_factor_data()` takes the RVLC + /// [`ErScaleFactorData::parse`] branch when + /// `resilience.scalefactor_data` is set; the reconstructed + /// absolute-delta records are mirrored into + /// [`Self::scale_factor_data`] and the RVLC seeds are retained + /// in [`Self::er_scale_factor_data`]. + /// * the trailing `spectral_data()` is replaced — when + /// `resilience.spectral_data` is set — by the + /// `length_of_reordered_spectral_data` (14-bit) + + /// `length_of_longest_codeword` (6-bit) pair captured in + /// [`Self::reordered_spectral_lengths`]; the + /// `reordered_spectral_data()` (HCR) payload that follows is the + /// caller's responsibility, exactly as `spectral_data()` is on + /// the non-resilient path. + pub fn parse_er( + reader: &mut BitReader<'_>, + audio_object_type: u8, + sampling_frequency_index: u8, + scale_flag: bool, + resilience: AacResilienceFlags, + ) -> Result { + Self::parse_er_family( + reader, + FrameFamily::Lc1024, + audio_object_type, + sampling_frequency_index, + scale_flag, + resilience, + ) + } + + /// [`IcsBody::parse_er`] under an explicit §4.5.1.1 frame-length + /// family — the ER AAC LD (AOT 23) payloads ride the same + /// Table 4.19 `er_raw_data_block()` as ER AAC LC, differing only + /// in the 512/480-line geometry this parameter selects. + pub fn parse_er_family( + reader: &mut BitReader<'_>, + family: FrameFamily, + audio_object_type: u8, + sampling_frequency_index: u8, + scale_flag: bool, + resilience: AacResilienceFlags, + ) -> Result { + if scale_flag { + return Err(Error::NotImplemented); + } + let start = reader.bit_position(); + let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; + let ics_info = IcsInfo::parse_family( + reader, + family, + audio_object_type, + sampling_frequency_index, + false, + )?; + let mut body = Self::finish_er_shared( + reader, + global_gain, + &ics_info, + audio_object_type, + resilience, + start, + )?; + body.ics_info = Some(ics_info); + Ok(body) + } + + /// Parse an error-resilient Table 4.50 body whose `ics_info()` was + /// already consumed by the surrounding shared-info CPE form. + /// + /// ER analogue of [`IcsBody::parse_with_ics_info`]; the resilience + /// branch semantics match [`IcsBody::parse_er`]. The returned + /// `ics_info` is `None` (the caller holds the shared `IcsInfo`). + pub fn parse_with_ics_info_er( + reader: &mut BitReader<'_>, + ics_info: &IcsInfo, + audio_object_type: u8, + scale_flag: bool, + resilience: AacResilienceFlags, + ) -> Result { + if scale_flag { + return Err(Error::NotImplemented); + } + let start = reader.bit_position(); + let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; + Self::finish_er_shared( + reader, + global_gain, + ics_info, + audio_object_type, + resilience, + start, + ) + } + + /// Shared tail of the ER parse: `section_data()` (ER branch) → + /// `scale_factor_data()` (RVLC branch) → tool dispatch → spectral + /// resilience length fields. `ics_info` carries the geometry; the + /// returned `IcsBody::ics_info` is `None` (the inline caller sets + /// it afterwards from its owned value). + fn finish_er_shared( + reader: &mut BitReader<'_>, + global_gain: u8, + ics_info: &IcsInfo, + audio_object_type: u8, + resilience: AacResilienceFlags, + start: u64, + ) -> Result { + let section_data = if resilience.section_data { + SectionData::parse_er( + reader, + ics_info.window_sequence, + ics_info.num_window_groups, + ics_info.max_sfb, + )? + } else { + SectionData::parse( + reader, + ics_info.window_sequence, + ics_info.num_window_groups, + ics_info.max_sfb, + )? + }; + + let (scale_factor_data, er_scale_factor_data) = if resilience.scalefactor_data { + let er = + ErScaleFactorData::parse(reader, §ion_data.sfb_cb, ics_info.window_sequence)?; + (er.data.clone(), Some(er)) + } else { + (ScaleFactorData::parse(reader, §ion_data.sfb_cb)?, None) + }; + + let tools = parse_tools(reader, ics_info, audio_object_type, start)?; + + // Table 4.50 ER spectral branch: when + // aacSpectralDataResilienceFlag is set, the body carries the + // two HCR length fields in place of starting spectral_data(). + let reordered_spectral_lengths = if resilience.spectral_data { + let len_reordered = reader.read_u32(14).map_err(|_| Error::UnexpectedEnd)? as u16; + let len_longest = reader.read_u32(6).map_err(|_| Error::UnexpectedEnd)? as u8; + Some((len_reordered, len_longest)) + } else { + None + }; + + let spectral_data_bit_offset = reader.bit_position() - start; + + Ok(IcsBody { + global_gain, + ics_info: None, + section_data, + scale_factor_data, + pulse_data_present: tools.pulse_data_present, + pulse_data: tools.pulse_data, + tns_data_present: tools.tns_data_present, + tns_data: tools.tns_data, + gain_control_data_present: tools.gain_control_data_present, + gain_control_data: tools.gain_control_data, + spectral_data_bit_offset, + er_scale_factor_data, + reordered_spectral_lengths, + }) + } + + /// Parse a Table 4.50 body with `scale_flag == 1` — the + /// `individual_channel_stream(1, 1)` form the scalable payloads + /// (Tables 4.13 / 4.14, AOTs 6 / 20) embed. + /// + /// Per Table 4.50 the scale-flag form reads neither `ics_info()` + /// (the window geometry lives in the `aac_scalable_main_header()`) + /// nor the pulse / TNS / gain-control dispatch trio (TNS rides in + /// the scalable headers; pulse and SSR gain control do not exist + /// in the scalable object types): the body is `global_gain` → + /// `section_data()` → `scale_factor_data()` → the spectral branch. + /// + /// * `ics_info` — the per-layer geometry (the header-transmitted + /// `window_sequence` / `window_shape` / grouping with **this + /// layer's** `max_sfb`). + /// * `resilience` — the ASC triplet for AOT 20 (ER AAC scalable); + /// pass `AacResilienceFlags::default()` for AOT 6. The branches + /// behave exactly as in [`IcsBody::parse_er`]: 5-bit `sect_cb` + /// `section_data()`, RVLC `scale_factor_data()`, and the HCR + /// length fields in place of `spectral_data()`. + /// + /// The trailing `spectral_data()` / `reordered_spectral_data()` is + /// the caller's responsibility, as on every other parse path. + pub fn parse_scale( + reader: &mut BitReader<'_>, + ics_info: &IcsInfo, + resilience: AacResilienceFlags, + ) -> Result { + let start = reader.bit_position(); + let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; + let section_data = if resilience.section_data { + SectionData::parse_er( + reader, + ics_info.window_sequence, + ics_info.num_window_groups, + ics_info.max_sfb, + )? + } else { + SectionData::parse( + reader, + ics_info.window_sequence, + ics_info.num_window_groups, + ics_info.max_sfb, + )? + }; + let (scale_factor_data, er_scale_factor_data) = if resilience.scalefactor_data { + let er = + ErScaleFactorData::parse(reader, §ion_data.sfb_cb, ics_info.window_sequence)?; + (er.data.clone(), Some(er)) + } else { + (ScaleFactorData::parse(reader, §ion_data.sfb_cb)?, None) + }; + // Table 4.50: `if (!scale_flag) { pulse/tns/gain dispatch }` — + // all three tools are skipped on the scale-flag form. + let reordered_spectral_lengths = if resilience.spectral_data { + let len_reordered = reader.read_u32(14).map_err(|_| Error::UnexpectedEnd)? as u16; + let len_longest = reader.read_u32(6).map_err(|_| Error::UnexpectedEnd)? as u8; + Some((len_reordered, len_longest)) + } else { + None + }; + let spectral_data_bit_offset = reader.bit_position() - start; + Ok(IcsBody { + global_gain, + ics_info: None, + section_data, + scale_factor_data, + pulse_data_present: false, + pulse_data: None, + tns_data_present: false, + tns_data: None, + gain_control_data_present: false, + gain_control_data: None, + spectral_data_bit_offset, + er_scale_factor_data, + reordered_spectral_lengths, + }) + } + + /// Write a Table 4.50 `scale_flag == 1` body — the inverse of + /// [`IcsBody::parse_scale`], emitting `global_gain` → + /// `section_data()` → `scale_factor_data()` (→ the HCR length + /// fields when `resilience.spectral_data` is set). The trailing + /// spectrum block is the caller's responsibility. + pub fn write_scale( + &self, + writer: &mut BitWriter, + ics_info: &IcsInfo, + resilience: AacResilienceFlags, + ) -> Result<()> { + writer.write_u32(u32::from(self.global_gain), GLOBAL_GAIN_BITS); + if resilience.section_data { + self.section_data + .write_er(writer, ics_info.window_sequence, ics_info.max_sfb)?; + } else { + self.section_data + .write(writer, ics_info.window_sequence, ics_info.max_sfb)?; + } + if resilience.scalefactor_data { + let er = self + .er_scale_factor_data + .as_ref() + .ok_or(Error::ElementDecodeInvalid)?; + er.write(writer, &self.section_data.sfb_cb, ics_info.window_sequence)?; + } else { + self.scale_factor_data + .write(writer, &self.section_data.sfb_cb)?; + } + if resilience.spectral_data { + let (len_reordered, len_longest) = self + .reordered_spectral_lengths + .ok_or(Error::ElementDecodeInvalid)?; + writer.write_u32(u32::from(len_reordered), 14); + writer.write_u32(u32::from(len_longest), 6); + } + Ok(()) + } + + /// Write a Table 4.50 body whose `ics_info()` is inline. + /// + /// Mirrors [`IcsBody::parse`] — emits `global_gain`, `ics_info()`, + /// `section_data()`, `scale_factor_data()`, then the three + /// dispatching bits and their optional bodies. The trailing + /// `spectral_data()` is the caller's responsibility. + /// + /// Returns [`Error::IcsInfoEncodeInvalid`] if [`Self::ics_info`] + /// is `None` (use [`IcsBody::write_with_ics_info`] for the + /// CPE-shared-info case); other errors propagate from the + /// per-tool writers (e.g. [`Error::PulseDataEncodeInvalid`] when + /// `pulse_data_present == true` on `EIGHT_SHORT_SEQUENCE`). + pub fn write( + &self, + writer: &mut BitWriter, + audio_object_type: u8, + sampling_frequency_index: u8, + scale_flag: bool, + ) -> Result<()> { + if scale_flag { + return Err(Error::NotImplemented); + } + let ics_info = self.ics_info.as_ref().ok_or(Error::IcsInfoEncodeInvalid)?; + writer.write_u32(u32::from(self.global_gain), GLOBAL_GAIN_BITS); + ics_info.write(writer, audio_object_type, sampling_frequency_index, false)?; + self.section_data + .write(writer, ics_info.window_sequence, ics_info.max_sfb)?; + self.scale_factor_data + .write(writer, &self.section_data.sfb_cb)?; + self.write_tools(writer, ics_info, audio_object_type) + } + + /// Write a Table 4.50 body whose `ics_info()` was emitted + /// separately by the surrounding shared-info `CPE` form. + /// + /// The supplied `ics_info` drives the same per-tool field + /// dispatch the inline path would. The in-memory + /// [`Self::ics_info`] field is ignored (and is expected to be + /// `None` for round-trip consistency). + pub fn write_with_ics_info( + &self, + writer: &mut BitWriter, + ics_info: &IcsInfo, + audio_object_type: u8, + scale_flag: bool, + ) -> Result<()> { + if scale_flag { + return Err(Error::NotImplemented); + } + writer.write_u32(u32::from(self.global_gain), GLOBAL_GAIN_BITS); + self.section_data + .write(writer, ics_info.window_sequence, ics_info.max_sfb)?; + self.scale_factor_data + .write(writer, &self.section_data.sfb_cb)?; + self.write_tools(writer, ics_info, audio_object_type) + } + + fn write_tools( + &self, + writer: &mut BitWriter, + ics_info: &IcsInfo, + audio_object_type: u8, + ) -> Result<()> { + // pulse_data_present + body. + writer.write_bit(self.pulse_data_present); + if self.pulse_data_present { + // Table 4.50 Note 1: pulse_data is illegal on + // EIGHT_SHORT_SEQUENCE (the pulse-escape fix-up needs the + // long-window swb_offset_long table). + if ics_info.window_sequence == WindowSequence::EightShort { + return Err(Error::PulseDataEncodeInvalid); + } + let pd = self + .pulse_data + .as_ref() + .ok_or(Error::PulseDataEncodeInvalid)?; + pd.write(writer)?; + } else if self.pulse_data.is_some() { + // Slot populated while the dispatching bit is clear. + return Err(Error::PulseDataEncodeInvalid); + } + + // tns_data_present + body (family-aware widths — the LD + // families emit the reduced 1 / 4 / 3-bit column, mirroring + // the parse side). + writer.write_bit(self.tns_data_present); + if self.tns_data_present { + let td = self.tns_data.as_ref().ok_or(Error::TnsDataEncodeInvalid)?; + td.write_family(writer, ics_info.family, ics_info.window_sequence)?; + } else if self.tns_data.is_some() { + return Err(Error::TnsDataEncodeInvalid); + } + + // gain_control_data_present + body. The §4.6.12 normative + // constraint: AOT 3 (SSR) only. + writer.write_bit(self.gain_control_data_present); + if self.gain_control_data_present { + if audio_object_type != AOT_AAC_SSR { + return Err(Error::GainControlDataEncodeInvalid); + } + let gc = self + .gain_control_data + .as_ref() + .ok_or(Error::GainControlDataEncodeInvalid)?; + gc.write(writer, ics_info.window_sequence)?; + } else if self.gain_control_data.is_some() { + return Err(Error::GainControlDataEncodeInvalid); + } + Ok(()) + } +} + +/// Helper: read an 8-bit `uimsbf` field. +fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { + Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +/// Internal carrier for the pulse / tns / gain_control walk result. +struct ToolDispatch { + pulse_data_present: bool, + pulse_data: Option, + tns_data_present: bool, + tns_data: Option, + gain_control_data_present: bool, + gain_control_data: Option, + spectral_data_bit_offset: u64, +} + +/// Helper: walk the pulse / tns / gain_control dispatch trio after +/// `scale_factor_data()` and return the resulting slots plus the +/// `spectral_data_bit_offset` (measured from `start`). +fn parse_tools( + reader: &mut BitReader<'_>, + ics_info: &IcsInfo, + _audio_object_type: u8, + start: u64, +) -> Result { + let pulse_data_present = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let pulse_data = if pulse_data_present { + // Table 4.50 Note 1: pulse_data is illegal on + // EIGHT_SHORT_SEQUENCE. A conforming stream never sets the + // flag in that case; surface the violation so callers can + // reject the stream rather than crash downstream. + if ics_info.window_sequence == WindowSequence::EightShort { + return Err(Error::PulseDataEncodeInvalid); + } + Some(PulseData::parse(reader)?) + } else { + None + }; + + let tns_data_present = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let tns_data = if tns_data_present { + // Family-aware widths: the ER AAC LD families read the + // reduced 1 / 4 / 3-bit Table 4.155 column (the + // corpus-resolved AOT-23 wire — see + // docs/audio/aac/er-ld-tns-divergence.md §0); everything + // else takes the literal window_sequence dispatch. + Some(TnsData::parse_family( + reader, + ics_info.family, + ics_info.window_sequence, + )?) + } else { + None + }; + + let gain_control_data_present = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let gain_control_data = if gain_control_data_present { + // Per §4.6.12 the gain_control_data tool is AOT-3 (SSR) only; + // a conforming stream never sets the flag on any other AOT. + // The parser surfaces the literal bits regardless of AOT — + // the AOT-validity check is enforced on the writer side so + // we can ingest hostile streams without panicking, and the + // emitter side keeps us from emitting non-conforming streams. + Some(GainControlData::parse(reader, ics_info.window_sequence)?) + } else { + None + }; + + let spectral_data_bit_offset = reader.bit_position() - start; + Ok(ToolDispatch { + pulse_data_present, + pulse_data, + tns_data_present, + tns_data, + gain_control_data_present, + gain_control_data, + spectral_data_bit_offset, + }) +} diff --git a/crates/vendor/oxideav-aac/src/ics_info.rs b/crates/vendor/oxideav-aac/src/ics_info.rs new file mode 100644 index 00000000..d8b3e955 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ics_info.rs @@ -0,0 +1,1100 @@ +//! `ics_info()` parser — ISO/IEC 14496-3 §4.4.6 Table 4.6. +//! +//! `ics_info()` carries the per-channel window-shape / window-sequence +//! decision plus the `max_sfb` (number of scalefactor bands actually +//! coded), scale-factor grouping mask for `EIGHT_SHORT_SEQUENCE`, and +//! either the MPEG-2 frequency-domain predictor side-info (AOT 1 +//! Main) or the LTP `ltp_data_present` flag(s) (every other GA AOT +//! that's not 3 = SSR — SSR uses `gain_control_data()` instead of +//! prediction). +//! +//! This parser is the **start** of Phase 2 (channel-element body +//! parsing). It does not consume `global_gain`, `section_data()`, +//! `scale_factor_data()`, `pulse_data()`, `tns_data()`, +//! `gain_control_data()`, or `spectral_data()` — those land in +//! later Phase 2 rounds. `ltp_data()` (Table 4.55) **is** parsed +//! when `ltp_data_present == 1`, because it is dispatched from +//! inside the Table 4.6 syntax itself; deferring it would leave +//! `IcsInfo` in an indeterminate bit-position. +//! +//! ## Derived values +//! +//! Beyond the literal wire fields the parser surfaces the +//! §4.5.2.3.4 / §4.5.2.6.2.4 derivations: +//! +//! * `num_windows` — `8` for `EIGHT_SHORT_SEQUENCE`, `1` otherwise. +//! * `num_window_groups` — `1` for long sequences; for +//! `EIGHT_SHORT_SEQUENCE` it is the number of groups implied by +//! the 7-bit `scale_factor_grouping` mask. The first short +//! window always starts a new group; for windows 1..=7 a `1` bit +//! at position `6 − i` (so bit 6 controls grouping of window 1, +//! …, bit 0 controls window 7) merges window `i+1` into the +//! current group, a `0` opens a new group. This matches the +//! spec's `bit_set(scale_factor_grouping, 6 − i)` pseudo-code. +//! * `window_group_length[g]` — number of short windows in group +//! `g`. Sum is always 8. +//! * `num_swb` — `num_swb_long_window[fs_index]` for long, or +//! `num_swb_short_window[fs_index]` for `EIGHT_SHORT_SEQUENCE`. +//! Sample-rate count tables ([`NUM_SWB_LONG_WINDOW`], +//! [`NUM_SWB_SHORT_WINDOW`]) cover the 12 valid ADTS +//! `sampling_frequency_index` values 0..=11. +//! +//! ## What is *not* in this round +//! +//! * `swb_offset_long_window[]` / `swb_offset_short_window[]` +//! tables — only the *count* of scalefactor bands is needed to +//! step through `ics_info()`. Spectral decoding (Phase 2 mid) +//! will pull in the offset tables. +//! * `sect_sfb_offset[g][section]` — derived from the offset +//! tables, not from `ics_info` proper; landed alongside +//! `section_data()` in a later round. +//! * The `aac_section_data_resilience_flag` / +//! `aac_scalefactor_data_resilience_flag` / +//! `aac_spectral_data_resilience_flag` extension chain (ER AOTs). +//! Surfaced by `GASpecificConfig` `extensionFlag == 1` parsing +//! that itself is a Phase 1 follow-up. +//! +//! ## Predictor / LTP dispatch (Table 4.6) +//! +//! When `window_sequence != EIGHT_SHORT_SEQUENCE`, an extra +//! `predictor_data_present` bit follows `max_sfb`. The branch +//! taken when that bit is 1 depends on `audioObjectType`: +//! +//! * `audioObjectType == 1` (Main) — read `predictor_reset` (1 bit); +//! if set, read `predictor_reset_group_number` (5 bits); then read +//! `prediction_used[sfb]` for `sfb in 0..min(max_sfb, PRED_SFB_MAX)`. +//! `PRED_SFB_MAX` is sample-rate dependent (see +//! [`PRED_SFB_MAX`]). +//! * Any other AOT (LC, SSR, LTP, scalable, TwinVQ, ER variants) — +//! read `ltp_data_present` (1 bit); if set, parse `ltp_data()` +//! per Table 4.55. If the surrounding element is a CPE with +//! `common_window == 1`, a *second* `ltp_data_present` (+ +//! optional `ltp_data()`) follows for the paired channel. +//! +//! The spec attaches a normative caveat: for plain LC streams the +//! `predictor_data_present` bit is required to be 0 by ISO/IEC +//! 14496-3 §1.5.1.1 (AOT 2 does not own a predictor). The parser +//! enforces nothing here — it surfaces whatever the wire said and +//! lets a higher-layer validator decide. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::swb_offset::{long_window_offsets_family, short_window_offsets_family, FrameFamily}; +use crate::{Error, Result}; + +/// Sentinel for the `EIGHT_SHORT_SEQUENCE` window-sequence value. +/// Exposed as a `pub const` so consumers can compare without +/// matching against [`WindowSequence`]. +pub const EIGHT_SHORT_SEQUENCE: u8 = 2; + +/// `window_sequence` enumeration — ISO/IEC 14496-3 §4.5.2.3.1.1 / +/// Table 4.128. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum WindowSequence { + /// `0` — one 1024-sample (or 960-sample if `frameLengthFlag`) + /// MDCT covering the full frame. + OnlyLong = 0, + /// `1` — long MDCT with a start window on the right half. + /// Always preceded by `OnlyLong` and followed by + /// `EightShort` in a transient-onset transition. + LongStart = 1, + /// `2` — eight 128-sample MDCTs; `scale_factor_grouping` and + /// `num_window_groups` are meaningful here. + EightShort = 2, + /// `3` — long MDCT with a stop window on the left half. Tail + /// of an `EightShort` burst. + LongStop = 3, +} + +impl WindowSequence { + /// Map a 2-bit wire value (0..=3) to the corresponding variant. + pub fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0 => WindowSequence::OnlyLong, + 1 => WindowSequence::LongStart, + 2 => WindowSequence::EightShort, + _ => WindowSequence::LongStop, + } + } + + /// `true` ⇔ `EIGHT_SHORT_SEQUENCE`. + pub fn is_eight_short(self) -> bool { + matches!(self, WindowSequence::EightShort) + } +} + +/// `window_shape` enumeration — ISO/IEC 14496-3 §4.5.2.3.1.1. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum WindowShape { + /// `0` — sine window. Default for AAC-LC. + Sine = 0, + /// `1` — Kaiser-Bessel-derived (KBD) window. + Kbd = 1, +} + +impl WindowShape { + /// Map a 1-bit wire value (0..=1) to the variant. + pub fn from_bit(bit: bool) -> Self { + if bit { + WindowShape::Kbd + } else { + WindowShape::Sine + } + } +} + +/// `predictor_data()` body (Table 4.6, Main branch). Only the Main +/// AOT (`audioObjectType == 1`) ever instantiates this. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PredictorData { + /// `predictor_reset` bit. + pub reset: bool, + /// `predictor_reset_group_number` (5 bits) — only present when + /// `reset == true`. Identifies which group of predictors to + /// re-initialise this frame. + pub reset_group_number: Option, + /// `prediction_used[sfb]` for `sfb in 0..min(max_sfb, + /// PRED_SFB_MAX[fs_index])`. Each entry is a single bit. + pub prediction_used: Vec, +} + +/// `ltp_data()` body (Table 4.55). +/// +/// Two variants are distinguished by `audioObjectType == 23` +/// (`ER_AAC_LD`), which carries a delta-coded `ltp_lag_update` / +/// `ltp_lag` pair instead of an unconditional 11-bit `ltp_lag`. +/// For `EIGHT_SHORT_SEQUENCE` in the non-LD branch, +/// `ltp_long_used[]` is **absent** per the 2009 edition — the +/// parser emits an empty `long_used` vec in that case. The 2001 +/// edition instead carries a per-short-window +/// `ltp_short_used` / `ltp_short_lag_present` / `ltp_short_lag` +/// loop there (see [`LtpEdition`] and [`LtpShortWindow`]); those +/// records land in `short`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LtpData { + /// `ltp_lag_update` bit. Only present for `audioObjectType == + /// 23` (LD); `None` for every other AOT. + pub lag_update: Option, + /// `ltp_lag`. For LD this is 10 bits and may be absent when + /// `lag_update == false`; for non-LD this is 11 bits and is + /// always present. + pub lag: Option, + /// `ltp_coef` (3 bits) — index into the 8-entry LTP + /// coefficient codebook. + pub coef: u8, + /// `ltp_long_used[sfb]` for `sfb in 0..min(max_sfb, + /// MAX_LTP_LONG_SFB)`. Empty when the non-LD AOT is using + /// `EIGHT_SHORT_SEQUENCE` (both editions omit the long loop in + /// that case). + pub long_used: Vec, + /// ISO/IEC 14496-3:2001 Table 4.55 per-short-window LTP + /// records — `Some(v)` (with `v.len() == num_windows == 8`) + /// only when the non-LD `EIGHT_SHORT_SEQUENCE` branch is + /// parsed / written under [`LtpEdition::Iso2001`]. Always + /// `None` for long window sequences, the LD branch, and the + /// 2009 edition (which removed short-window LTP — §4.6.7.1 + /// "LTP is restricted to long windows only"). + pub short: Option>, +} + +/// One short window's LTP record from the ISO/IEC 14496-3:2001 +/// Table 4.55 `EIGHT_SHORT_SEQUENCE` branch. +/// +/// Wire layout (2001 edition only): `ltp_short_used[w]` (1 bit); +/// if set, `ltp_short_lag_present[w]` (1 bit); if *that* is set, +/// `ltp_short_lag[w]` (4 bits). Per §4.6.7.2 (2001) the 4-bit +/// field is "a 4-bit number specifying the relative delay for +/// each short window to ltp_lag from −8 to 7" — this crate reads +/// it as a 4-bit two's-complement integer (the standard MPEG +/// reading of an n-bit field whose documented range is +/// −2^(n−1)..2^(n−1)−1). When `ltp_short_lag_present == 0` the +/// relative delay is 0 per §4.6.7.3 (2001). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LtpShortWindow { + /// `ltp_short_used[w]` — whether LTP contributes to this short + /// window at all. + pub used: bool, + /// `ltp_short_lag_present[w]` — whether the 4-bit relative lag + /// was actually transmitted. Only meaningful when `used`; + /// always `false` otherwise. Kept distinct from `lag == 0` so a + /// re-encode reproduces the exact wire bits. + pub lag_present: bool, + /// The relative delay for this window, `−8..=7`, added to the + /// frame's `ltp_lag`. `0` when `lag_present == false`. + pub lag: i8, +} + +/// Which edition of the ISO/IEC 14496-3 Table 4.55 `ltp_data()` +/// syntax to apply for the non-LD `EIGHT_SHORT_SEQUENCE` branch. +/// +/// The 2001 edition transmits a per-short-window +/// `ltp_short_used` / `ltp_short_lag_present` / `ltp_short_lag` +/// loop after `ltp_coef`; the 2009 edition removed short-window +/// LTP entirely (§4.6.7.1: "LTP is restricted to long windows +/// only") and transmits nothing there. The two forms are +/// wire-incompatible for `EIGHT_SHORT_SEQUENCE` frames with +/// `ltp_data_present == 1`, and the bitstream itself does not +/// signal which edition the encoder followed, so the choice is an +/// out-of-band caller decision. Long window sequences and the LD +/// branch are identical in both editions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum LtpEdition { + /// ISO/IEC 14496-3:2009 Table 4.55 — no short-window LTP + /// fields (the form every contemporary stream follows). + #[default] + Iso2009, + /// ISO/IEC 14496-3:2001 Table 4.55 — per-short-window + /// `ltp_short_used[w]` loop for `EIGHT_SHORT_SEQUENCE`. + Iso2001, +} + +/// Per-Table 4.55 maximum number of scalefactor bands carrying +/// `ltp_long_used[]`. ISO/IEC 14496-3 §4.6.7.2. +pub const MAX_LTP_LONG_SFB: usize = 40; + +/// Number of short windows in an `EIGHT_SHORT_SEQUENCE` frame — +/// `num_windows == 8` per ISO/IEC 14496-3 §4.5.2.3.4, and the +/// iteration count of the 2001-edition Table 4.55 short-window +/// LTP loop. +pub const SHORT_WINDOWS_PER_FRAME: usize = 8; + +/// Per-Table 4.6 / Table 62 (ISO/IEC 13818-7 §13.3.1) +/// sample-rate-dependent `PRED_SFB_MAX` constant. Indexed by +/// ADTS `sampling_frequency_index` 0..=11. +/// +/// | idx | rate (Hz) | PRED_SFB_MAX | +/// |-------|--------------|--------------| +/// | 0 | 96 000 | 33 | +/// | 1 | 88 200 | 33 | +/// | 2 | 64 000 | 38 | +/// | 3 | 48 000 | 40 | +/// | 4 | 44 100 | 40 | +/// | 5 | 32 000 | 40 | +/// | 6 | 24 000 | 41 | +/// | 7 | 22 050 | 41 | +/// | 8 | 16 000 | 37 | +/// | 9 | 12 000 | 37 | +/// | 10 | 11 025 | 37 | +/// | 11 | 8 000 | 34 | +pub const PRED_SFB_MAX: [u8; 12] = [33, 33, 38, 40, 40, 40, 41, 41, 37, 37, 37, 34]; + +/// `num_swb_long_window[fs_index]` for the canonical 1024-line +/// long window — ISO/IEC 14496-3 Tables 4.129 / 4.131 / 4.132 / +/// 4.134 / 4.136 / 4.138 / 4.140, distilled to the count column. +/// +/// | idx | rate (Hz) | num_swb | source | +/// |-------|--------------|---------|----------------| +/// | 0 | 96 000 | 41 | Table 4.140 | +/// | 1 | 88 200 | 41 | Table 4.140 | +/// | 2 | 64 000 | 47 | Table 4.138 | +/// | 3 | 48 000 | 49 | Table 4.129 | +/// | 4 | 44 100 | 49 | Table 4.129 | +/// | 5 | 32 000 | 51 | Table 4.131 | +/// | 6 | 24 000 | 47 | Table 4.136 | +/// | 7 | 22 050 | 47 | Table 4.136 | +/// | 8 | 16 000 | 43 | Table 4.134 | +/// | 9 | 12 000 | 43 | Table 4.134 | +/// | 10 | 11 025 | 43 | Table 4.134 | +/// | 11 | 8 000 | 40 | Table 4.132 | +pub const NUM_SWB_LONG_WINDOW: [u8; 12] = [41, 41, 47, 49, 49, 51, 47, 47, 43, 43, 43, 40]; + +/// `num_swb_short_window[fs_index]` for the canonical 128-line +/// short window — ISO/IEC 14496-3 Tables 4.130 / 4.133 / 4.135 / +/// 4.137 / 4.139 / 4.141. +/// +/// | idx | rate (Hz) | num_swb | source | +/// |-------|--------------|---------|----------------| +/// | 0 | 96 000 | 12 | Table 4.141 | +/// | 1 | 88 200 | 12 | Table 4.141 | +/// | 2 | 64 000 | 12 | Table 4.139 | +/// | 3 | 48 000 | 14 | Table 4.130 | +/// | 4 | 44 100 | 14 | Table 4.130 | +/// | 5 | 32 000 | 14 | Table 4.130 | +/// | 6 | 24 000 | 15 | Table 4.137 | +/// | 7 | 22 050 | 15 | Table 4.137 | +/// | 8 | 16 000 | 15 | Table 4.135 | +/// | 9 | 12 000 | 15 | Table 4.135 | +/// | 10 | 11 025 | 15 | Table 4.135 | +/// | 11 | 8 000 | 15 | Table 4.133 | +pub const NUM_SWB_SHORT_WINDOW: [u8; 12] = [12, 12, 12, 14, 14, 14, 15, 15, 15, 15, 15, 15]; + +/// Parsed `ics_info()` (Table 4.6) plus the §4.5.2.3.4 derivations +/// that depend on it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IcsInfo { + /// The §4.5.1.1 frame-length family this `ics_info()` was parsed + /// under (`frameLengthFlag` + AOT). Governs every derived band + /// geometry: the `num_swb` below, the SWB offset tables the + /// numeric chain reads, and the §4.6.11 transform lengths. + pub family: FrameFamily, + /// `ics_reserved_bit` — spec mandates `0`; the parser surfaces + /// the wire value without enforcement (some encoders set it + /// even though they shouldn't). + pub ics_reserved_bit: bool, + /// `window_sequence` (2 bits, Table 4.128). + pub window_sequence: WindowSequence, + /// `window_shape` (1 bit, Table 4.129 reference). + pub window_shape: WindowShape, + /// `max_sfb` — 4 bits in the `EIGHT_SHORT_SEQUENCE` branch, + /// 6 bits in every other branch. + pub max_sfb: u8, + /// `scale_factor_grouping` (7 bits) — only present when + /// `window_sequence == EIGHT_SHORT_SEQUENCE`. Bit `6 − i` + /// controls whether window `i + 1` joins the current group + /// (`1`) or opens a new group (`0`) for `i in 0..7`. + pub scale_factor_grouping: Option, + /// `predictor_data_present` (1 bit) — only present when + /// `window_sequence != EIGHT_SHORT_SEQUENCE`. + pub predictor_data_present: bool, + /// Main-AOT `predictor_data()` body (Table 4.6 Main branch). + /// Populated when `predictor_data_present == true` and + /// `audioObjectType == 1`. + pub predictor_data: Option, + /// First `ltp_data_present` bit — read when + /// `predictor_data_present == true` and `audioObjectType != + /// 1`. `false` if not read. + pub ltp_data_present: bool, + /// Channel's own `ltp_data()` body — populated when + /// `ltp_data_present == true`. + pub ltp_data: Option, + /// `common_window`-paired channel `ltp_data_present` bit — + /// only read when the caller passed `common_window == true` + /// AND `predictor_data_present == true` AND `audioObjectType + /// != 1`. `None` if not present. + pub ltp_data_present_pair: Option, + /// `ltp_data()` body for the paired channel — populated when + /// `ltp_data_present_pair == Some(true)`. + pub ltp_data_pair: Option, + + // Derived fields (§4.5.2.3.4) — populated unconditionally. + /// Number of MDCT windows in this frame (`8` for short, `1` + /// otherwise). + pub num_windows: u8, + /// Number of window-groups after scale-factor grouping. Always + /// `1` for long sequences; for `EIGHT_SHORT_SEQUENCE` it is in + /// `1..=8` per the [`Self::scale_factor_grouping`] mask. + pub num_window_groups: u8, + /// Number of windows in each group; `window_group_length[g]` + /// for `g in 0..num_window_groups`. Sum is always + /// `num_windows`. + pub window_group_length: Vec, + /// Total scalefactor window bands for this frame — + /// `NUM_SWB_LONG_WINDOW[fs_index]` for long sequences, + /// `NUM_SWB_SHORT_WINDOW[fs_index]` for short sequences. + pub num_swb: u8, +} + +impl IcsInfo { + /// Parse a single `ics_info()` from the bit-reader. + /// + /// * `audio_object_type` — the surrounding ASC's effective + /// `audioObjectType` (post SBR/PS unwrap). Used to pick + /// between the Main / LTP predictor branches. + /// * `sampling_frequency_index` — the surrounding ASC's + /// `samplingFrequencyIndex` (the *core* index for hierarchical + /// SBR/PS — ics_info follows the inner AAC framerate, not the + /// SBR output rate). Must be in `0..=11` (the 24-bit + /// explicit-rate escape from §1.6.2.1 is not supported here + /// because the SWB tables are indexed by the standard 12 + /// rates). + /// * `common_window` — `true` ⇔ the surrounding element is a + /// `channel_pair_element()` with the shared-info form + /// (`common_window == 1` per Table 4.5); controls whether + /// the second `ltp_data_present` (+ optional second + /// `ltp_data()`) is consumed. + pub fn parse( + reader: &mut BitReader<'_>, + audio_object_type: u8, + sampling_frequency_index: u8, + common_window: bool, + ) -> Result { + Self::parse_family( + reader, + FrameFamily::Lc1024, + audio_object_type, + sampling_frequency_index, + common_window, + ) + } + + /// [`IcsInfo::parse`] under an explicit §4.5.1.1 frame-length + /// family. The wire layout of `ics_info()` itself is + /// family-independent; the family drives the derived band counts + /// (`num_swb` comes from the family's own SWB tables) and the LD + /// constraint checks: an ER AAC LD stream has no block switching + /// (§4.6.17.2.2), so any `window_sequence` other than + /// `ONLY_LONG_SEQUENCE` under an LD family surfaces + /// [`Error::LdShortWindow`]. + pub fn parse_family( + reader: &mut BitReader<'_>, + family: FrameFamily, + audio_object_type: u8, + sampling_frequency_index: u8, + common_window: bool, + ) -> Result { + let fs_index = sampling_frequency_index as usize; + if fs_index >= NUM_SWB_LONG_WINDOW.len() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex( + sampling_frequency_index, + )); + } + + let ics_reserved_bit = read_bit(reader)?; + let window_sequence_bits = read_u8(reader, 2)?; + let window_sequence = WindowSequence::from_bits(window_sequence_bits); + let window_shape = WindowShape::from_bit(read_bit(reader)?); + + if family.is_ld() && window_sequence != WindowSequence::OnlyLong { + return Err(Error::LdShortWindow); + } + + let mut scale_factor_grouping = None; + let mut predictor_data_present = false; + let mut predictor_data = None; + let mut ltp_data_present = false; + let mut ltp_data = None; + let mut ltp_data_present_pair = None; + let mut ltp_data_pair = None; + + let max_sfb; + if window_sequence.is_eight_short() { + max_sfb = read_u8(reader, 4)?; + scale_factor_grouping = Some(read_u8(reader, 7)?); + } else { + max_sfb = read_u8(reader, 6)?; + predictor_data_present = read_bit(reader)?; + if predictor_data_present { + if audio_object_type == 1 { + // Main predictor side info. + let reset = read_bit(reader)?; + let reset_group_number = if reset { + Some(read_u8(reader, 5)?) + } else { + None + }; + let pred_sfb_max = PRED_SFB_MAX[fs_index] as u16; + let n = core::cmp::min(max_sfb as u16, pred_sfb_max) as usize; + let mut prediction_used = Vec::with_capacity(n); + for _ in 0..n { + prediction_used.push(read_bit(reader)?); + } + predictor_data = Some(PredictorData { + reset, + reset_group_number, + prediction_used, + }); + } else { + // LTP / other GA AOTs — Table 4.6 nests a + // dedicated `ltp_data_present` bit inside the + // `predictor_data_present` branch, so an AU can + // signal the branch with the channel's own LTP + // off (e.g. only the common_window pair bit + // follows). Corpus-confirmed by the ISO/IEC + // 14496-26 `er_ad1000*`/`er_ad1103*` LD vectors, + // which desynchronise without this bit. + ltp_data_present = read_bit(reader)?; + if ltp_data_present { + ltp_data = Some(parse_ltp_data( + reader, + audio_object_type, + window_sequence, + max_sfb, + )?); + } + if common_window { + let pair_flag = read_bit(reader)?; + ltp_data_present_pair = Some(pair_flag); + if pair_flag { + ltp_data_pair = Some(parse_ltp_data( + reader, + audio_object_type, + window_sequence, + max_sfb, + )?); + } + } + } + } else if common_window && audio_object_type != 1 { + // Spec note: when predictor_data_present == 0, the + // second ltp_data_present bit is also not + // transmitted (Table 4.6 only enters the LTP + // branch when predictor_data_present == 1). The + // pair-channel flag therefore stays absent. + } + } + + // §4.5.2.3.4 derivations. + let (num_windows, num_window_groups, window_group_length, num_swb) = + derive_window_grouping_family( + family, + window_sequence, + scale_factor_grouping, + sampling_frequency_index, + )?; + + Ok(IcsInfo { + family, + ics_reserved_bit, + window_sequence, + window_shape, + max_sfb, + scale_factor_grouping, + predictor_data_present, + predictor_data, + ltp_data_present, + ltp_data, + ltp_data_present_pair, + ltp_data_pair, + num_windows, + num_window_groups, + window_group_length, + num_swb, + }) + } + + /// The active per-window spectral length for this frame's + /// `window_sequence` under the frame's [`FrameFamily`]: the + /// family's short-window length (128 / 120) for + /// `EIGHT_SHORT_SEQUENCE`, the family's frame length + /// (1024 / 960 / 512 / 480) otherwise. The parser guarantees an + /// LD family never carries a short sequence, so the LD lookup + /// error is unreachable through parsed values. + pub fn window_len(&self) -> Result { + if self.window_sequence.is_eight_short() { + self.family.short_window_len().ok_or(Error::LdShortWindow) + } else { + Ok(self.family.frame_len()) + } + } + + /// The active `swb_offset` table for this frame's + /// `window_sequence` under the frame's [`FrameFamily`] at + /// `fs_index` — the short-window table for `EIGHT_SHORT_SEQUENCE`, + /// the long-window table otherwise. + pub fn swb_offsets(&self, fs_index: u8) -> Result<&'static [u16]> { + if self.window_sequence.is_eight_short() { + short_window_offsets_family(self.family, fs_index) + } else { + long_window_offsets_family(self.family, fs_index) + } + } + + /// Encode `ics_info()` onto `writer`, the inverse of + /// [`IcsInfo::parse`]. + /// + /// The writer mirrors Table 4.6 verbatim — `ics_reserved_bit` + /// (1 bit), `window_sequence` (2 bits), `window_shape` (1 bit), + /// then either `max_sfb` (4 bits) + `scale_factor_grouping` + /// (7 bits) for `EIGHT_SHORT_SEQUENCE`, or `max_sfb` (6 bits) + + /// `predictor_data_present` (1 bit) plus the per-AOT + /// predictor / LTP body for every other window sequence. + /// + /// The `audio_object_type` / `sampling_frequency_index` / + /// `common_window` parameters must match the values the parser + /// was (or would be) invoked with. They drive the branch the + /// encoder takes for the Main vs LTP predictor body and the + /// `prediction_used[]` cap (`PRED_SFB_MAX[fs_index]` for AOT 1). + /// + /// Returns [`Error::IcsInfoEncodeInvalid`] if the in-memory + /// [`IcsInfo`] violates a wire-field invariant: + /// + /// * `max_sfb` exceeds its field width + /// (`> 15` for `EIGHT_SHORT_SEQUENCE`, `> 63` otherwise). + /// * `scale_factor_grouping` is `None` for `EIGHT_SHORT_SEQUENCE`, + /// `Some(_)` otherwise, or its value exceeds 7 bits. + /// * `predictor_data_present == true` for `EIGHT_SHORT_SEQUENCE` + /// (Table 4.6 omits the bit on the short branch). + /// * `predictor_data` is `Some` while `audio_object_type != 1`, + /// or `None` while the predictor bit is set with AOT 1. + /// * Predictor `reset_group_number` doesn't match + /// `reset.is_some()` parity, or exceeds 5 bits. + /// * Predictor `prediction_used.len()` differs from `min(max_sfb, + /// PRED_SFB_MAX[fs_index])`. + /// * LTP body fields (lag width, `coef`, `long_used[]` length) do + /// not satisfy Table 4.55 (delegated to [`write_ltp_data`]). + /// * The paired-channel LTP slot is populated while + /// `common_window == false`, or while `predictor_data_present + /// == false`, or while `audio_object_type == 1`. + /// * `sampling_frequency_index` is outside `0..=11`. + pub fn write( + &self, + writer: &mut BitWriter, + audio_object_type: u8, + sampling_frequency_index: u8, + common_window: bool, + ) -> Result<()> { + let fs_index = sampling_frequency_index as usize; + if fs_index >= NUM_SWB_LONG_WINDOW.len() { + return Err(Error::IcsInfoEncodeInvalid); + } + // §4.6.17.2.2 — an LD-family ics_info can only carry + // ONLY_LONG_SEQUENCE (no block switching exists for LD). + if self.family.is_ld() && self.window_sequence != WindowSequence::OnlyLong { + return Err(Error::IcsInfoEncodeInvalid); + } + + writer.write_bit(self.ics_reserved_bit); + writer.write_u32(self.window_sequence as u32 & 0b11, 2); + writer.write_u32(self.window_shape as u32 & 0b1, 1); + + if self.window_sequence.is_eight_short() { + if self.max_sfb > 0x0f { + return Err(Error::IcsInfoEncodeInvalid); + } + let mask = self + .scale_factor_grouping + .ok_or(Error::IcsInfoEncodeInvalid)?; + if mask > 0x7f { + return Err(Error::IcsInfoEncodeInvalid); + } + // EIGHT_SHORT branch has neither predictor_data_present + // nor any LTP body — reject populated slots before they + // silently round-trip into a non-conforming stream. + if self.predictor_data_present + || self.predictor_data.is_some() + || self.ltp_data_present + || self.ltp_data.is_some() + || self.ltp_data_present_pair.is_some() + || self.ltp_data_pair.is_some() + { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_u32(self.max_sfb as u32, 4); + writer.write_u32(mask as u32, 7); + } else { + if self.max_sfb > 0x3f { + return Err(Error::IcsInfoEncodeInvalid); + } + if self.scale_factor_grouping.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_u32(self.max_sfb as u32, 6); + writer.write_bit(self.predictor_data_present); + + if self.predictor_data_present { + if audio_object_type == 1 { + // Main predictor side info. + if self.ltp_data_present + || self.ltp_data.is_some() + || self.ltp_data_present_pair.is_some() + || self.ltp_data_pair.is_some() + { + return Err(Error::IcsInfoEncodeInvalid); + } + let pd = self + .predictor_data + .as_ref() + .ok_or(Error::IcsInfoEncodeInvalid)?; + // reset_group_number parity matches reset bit. + if pd.reset != pd.reset_group_number.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + let pred_sfb_max = PRED_SFB_MAX[fs_index] as u16; + let expected = core::cmp::min(self.max_sfb as u16, pred_sfb_max) as usize; + if pd.prediction_used.len() != expected { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_bit(pd.reset); + if let Some(g) = pd.reset_group_number { + if g > 0x1f { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_u32(g as u32, 5); + } + for &b in &pd.prediction_used { + writer.write_bit(b); + } + } else { + // LTP / non-Main branch — Table 4.6 nests a + // dedicated `ltp_data_present` bit (mirror of the + // parse side). + if self.predictor_data.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_bit(self.ltp_data_present); + if self.ltp_data_present { + let ltp = self.ltp_data.as_ref().ok_or(Error::IcsInfoEncodeInvalid)?; + write_ltp_data( + writer, + ltp, + audio_object_type, + self.window_sequence, + self.max_sfb, + )?; + } else if self.ltp_data.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + if common_window { + let pair_flag = self + .ltp_data_present_pair + .ok_or(Error::IcsInfoEncodeInvalid)?; + writer.write_bit(pair_flag); + if pair_flag { + let ltp2 = self + .ltp_data_pair + .as_ref() + .ok_or(Error::IcsInfoEncodeInvalid)?; + write_ltp_data( + writer, + ltp2, + audio_object_type, + self.window_sequence, + self.max_sfb, + )?; + } else if self.ltp_data_pair.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + } else if self.ltp_data_present_pair.is_some() || self.ltp_data_pair.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + } + } else { + // predictor_data_present == 0: no predictor / LTP body + // is emitted at all (Table 4.6 only enters either + // branch under the predictor bit). Reject populated + // slots so a stale in-memory structure cannot + // silently desync from the wire. + if self.predictor_data.is_some() + || self.ltp_data_present + || self.ltp_data.is_some() + || self.ltp_data_present_pair.is_some() + || self.ltp_data_pair.is_some() + { + return Err(Error::IcsInfoEncodeInvalid); + } + } + } + + Ok(()) + } +} + +/// `ltp_data()` per Table 4.55 (2009 edition). Public to allow +/// standalone unit tests; in normal use it is invoked indirectly +/// via [`IcsInfo::parse`]. Equivalent to +/// [`parse_ltp_data_edition`] with [`LtpEdition::Iso2009`] and the +/// spec's `num_windows == 8` for `EIGHT_SHORT_SEQUENCE`. +pub fn parse_ltp_data( + reader: &mut BitReader<'_>, + audio_object_type: u8, + window_sequence: WindowSequence, + max_sfb: u8, +) -> Result { + parse_ltp_data_edition( + reader, + audio_object_type, + window_sequence, + max_sfb, + LtpEdition::Iso2009, + ) +} + +/// `ltp_data()` per Table 4.55, edition-selectable. +/// +/// [`LtpEdition::Iso2009`] behaves exactly like +/// [`parse_ltp_data`]. [`LtpEdition::Iso2001`] additionally reads +/// the per-short-window `ltp_short_used[w]` / +/// `ltp_short_lag_present[w]` / `ltp_short_lag[w]` loop (8 +/// iterations — `num_windows` for `EIGHT_SHORT_SEQUENCE` is +/// always 8, §4.5.2.3.4) when the non-LD branch sees a short +/// window sequence; the records land in [`LtpData::short`]. The +/// LD branch and all long window sequences are edition-invariant. +pub fn parse_ltp_data_edition( + reader: &mut BitReader<'_>, + audio_object_type: u8, + window_sequence: WindowSequence, + max_sfb: u8, + edition: LtpEdition, +) -> Result { + if audio_object_type == 23 { + // ER_AAC_LD branch. + let lag_update = read_bit(reader)?; + let lag = if lag_update { + Some(read_u16(reader, 10)?) + } else { + None + }; + let coef = read_u8(reader, 3)?; + let n = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); + let mut long_used = Vec::with_capacity(n); + for _ in 0..n { + long_used.push(read_bit(reader)?); + } + Ok(LtpData { + lag_update: Some(lag_update), + lag, + coef, + long_used, + short: None, + }) + } else { + let lag = read_u16(reader, 11)?; + let coef = read_u8(reader, 3)?; + let mut short = None; + let long_used = if window_sequence.is_eight_short() { + if edition == LtpEdition::Iso2001 { + // 2001 Table 4.55: for (w = 0; w < num_windows; w++) + // { ltp_short_used[w]; if set → + // ltp_short_lag_present[w]; if set → + // ltp_short_lag[w] (4 bits). } + let mut v = Vec::with_capacity(SHORT_WINDOWS_PER_FRAME); + for _ in 0..SHORT_WINDOWS_PER_FRAME { + let used = read_bit(reader)?; + let (lag_present, lag) = if used { + let lag_present = read_bit(reader)?; + let lag = if lag_present { + // 4-bit two's-complement −8..=7 (see + // LtpShortWindow docs). + let raw = read_u8(reader, 4)?; + ((raw << 4) as i8) >> 4 + } else { + 0 + }; + (lag_present, lag) + } else { + (false, 0) + }; + v.push(LtpShortWindow { + used, + lag_present, + lag, + }); + } + short = Some(v); + } + Vec::new() + } else { + let n = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); + let mut v = Vec::with_capacity(n); + for _ in 0..n { + v.push(read_bit(reader)?); + } + v + }; + Ok(LtpData { + lag_update: None, + lag: Some(lag), + coef, + long_used, + short, + }) + } +} + +/// Encode an `ltp_data()` (Table 4.55) body onto `writer`, the +/// inverse of [`parse_ltp_data`]. +/// +/// Mirrors the parser's two branches: +/// +/// * `audio_object_type == 23` (ER AAC LD) — write `ltp_lag_update` +/// (1 bit); if set, write `ltp_lag` (10 bits); then `ltp_coef` +/// (3 bits); then `ltp_long_used[sfb]` for `sfb in 0..min(max_sfb, +/// MAX_LTP_LONG_SFB)`. +/// * Every other AOT — write `ltp_lag` (11 bits, always), `ltp_coef` +/// (3 bits), then `ltp_long_used[]` *unless* the surrounding +/// `ics_info()` says `EIGHT_SHORT_SEQUENCE` (the spec omits the +/// loop in that case). +/// +/// Returns [`Error::IcsInfoEncodeInvalid`] when the in-memory +/// [`LtpData`] is inconsistent with the AOT or `window_sequence` +/// context (e.g. `lag_update == Some(_)` for a non-LD AOT, missing +/// `lag` for an LD `lag_update == true` slot, `coef > 7`, `lag` +/// exceeding its field width, or `long_used.len()` not matching +/// `min(max_sfb, MAX_LTP_LONG_SFB)` in the loop branch). +pub fn write_ltp_data( + writer: &mut BitWriter, + ltp: &LtpData, + audio_object_type: u8, + window_sequence: WindowSequence, + max_sfb: u8, +) -> Result<()> { + write_ltp_data_edition( + writer, + ltp, + audio_object_type, + window_sequence, + max_sfb, + LtpEdition::Iso2009, + ) +} + +/// Encode an `ltp_data()` (Table 4.55) body, edition-selectable — +/// the inverse of [`parse_ltp_data_edition`]. +/// +/// Under [`LtpEdition::Iso2001`] a non-LD `EIGHT_SHORT_SEQUENCE` +/// body must carry `short == Some(v)` with `v.len() == 8` +/// ([`SHORT_WINDOWS_PER_FRAME`]) and each [`LtpShortWindow`] +/// internally consistent (`!used ⇒ !lag_present`, +/// `!lag_present ⇒ lag == 0`, `lag ∈ −8..=7`); under +/// [`LtpEdition::Iso2009`] `short` must be `None` everywhere. +/// All other validation matches [`write_ltp_data`]. +pub fn write_ltp_data_edition( + writer: &mut BitWriter, + ltp: &LtpData, + audio_object_type: u8, + window_sequence: WindowSequence, + max_sfb: u8, + edition: LtpEdition, +) -> Result<()> { + if ltp.coef > 0x07 { + return Err(Error::IcsInfoEncodeInvalid); + } + // `short` is only representable on the wire in the 2001 + // non-LD EIGHT_SHORT branch; reject it anywhere else so an + // in-memory record can't silently drop fields. + let short_branch = audio_object_type != 23 + && window_sequence.is_eight_short() + && edition == LtpEdition::Iso2001; + if !short_branch && ltp.short.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + if audio_object_type == 23 { + let lag_update = ltp.lag_update.ok_or(Error::IcsInfoEncodeInvalid)?; + writer.write_bit(lag_update); + if lag_update { + let lag = ltp.lag.ok_or(Error::IcsInfoEncodeInvalid)?; + if lag > 0x3ff { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_u32(lag as u32, 10); + } else if ltp.lag.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_u32(ltp.coef as u32, 3); + let expected = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); + if ltp.long_used.len() != expected { + return Err(Error::IcsInfoEncodeInvalid); + } + for &b in <p.long_used { + writer.write_bit(b); + } + } else { + if ltp.lag_update.is_some() { + return Err(Error::IcsInfoEncodeInvalid); + } + let lag = ltp.lag.ok_or(Error::IcsInfoEncodeInvalid)?; + if lag > 0x7ff { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_u32(lag as u32, 11); + writer.write_u32(ltp.coef as u32, 3); + if window_sequence.is_eight_short() { + if !ltp.long_used.is_empty() { + return Err(Error::IcsInfoEncodeInvalid); + } + if edition == LtpEdition::Iso2001 { + // 2001 Table 4.55 per-short-window loop. + let short = ltp.short.as_ref().ok_or(Error::IcsInfoEncodeInvalid)?; + if short.len() != SHORT_WINDOWS_PER_FRAME { + return Err(Error::IcsInfoEncodeInvalid); + } + for w in short { + // Internal consistency: an unused window has no + // further fields; an absent lag means rel 0. + if !w.used && (w.lag_present || w.lag != 0) { + return Err(Error::IcsInfoEncodeInvalid); + } + if !w.lag_present && w.lag != 0 { + return Err(Error::IcsInfoEncodeInvalid); + } + if !(-8..=7).contains(&w.lag) { + return Err(Error::IcsInfoEncodeInvalid); + } + writer.write_bit(w.used); + if w.used { + writer.write_bit(w.lag_present); + if w.lag_present { + // 4-bit two's complement. + writer.write_u32((w.lag as u32) & 0x0f, 4); + } + } + } + } + } else { + let expected = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); + if ltp.long_used.len() != expected { + return Err(Error::IcsInfoEncodeInvalid); + } + for &b in <p.long_used { + writer.write_bit(b); + } + } + } + Ok(()) +} + +/// Compute (`num_windows`, `num_window_groups`, +/// `window_group_length`, `num_swb`) per ISO/IEC 14496-3 +/// §4.5.2.3.4. Exposed publicly so encoder-side code or +/// pre-section_data setup can compute the same derivations +/// without re-parsing an `ics_info`. +pub fn derive_window_grouping( + window_sequence: WindowSequence, + scale_factor_grouping: Option, + fs_index: usize, +) -> (u8, u8, Vec, u8) { + if !window_sequence.is_eight_short() { + return (1, 1, vec![1], NUM_SWB_LONG_WINDOW[fs_index]); + } + derive_short_grouping(scale_factor_grouping, NUM_SWB_SHORT_WINDOW[fs_index]) +} + +/// [`derive_window_grouping`] under an explicit §4.5.1.1 frame-length +/// family: `num_swb` is read from the family's own SWB offset tables +/// ([`crate::swb_offset::long_window_offsets_family`] / +/// [`crate::swb_offset::short_window_offsets_family`]), so the 960 / +/// LD band counts come out right. Errors surface for rates a family +/// table does not define and for a short-window request under an LD +/// family. +pub fn derive_window_grouping_family( + family: FrameFamily, + window_sequence: WindowSequence, + scale_factor_grouping: Option, + fs_index: u8, +) -> Result<(u8, u8, Vec, u8)> { + if !window_sequence.is_eight_short() { + let num_swb = (long_window_offsets_family(family, fs_index)?.len() - 1) as u8; + return Ok((1, 1, vec![1], num_swb)); + } + let num_swb = (short_window_offsets_family(family, fs_index)?.len() - 1) as u8; + Ok(derive_short_grouping(scale_factor_grouping, num_swb)) +} + +/// Shared `EIGHT_SHORT_SEQUENCE` §4.5.2.3.4 grouping walk. +fn derive_short_grouping(scale_factor_grouping: Option, num_swb: u8) -> (u8, u8, Vec, u8) { + // EIGHT_SHORT_SEQUENCE: scale_factor_grouping must be present + // per Table 4.6. derive_window_grouping treats a missing mask + // as the "no grouping" form (one group per window) so it + // remains a pure function; callers that go through + // IcsInfo::parse always supply the mask. + let mask = scale_factor_grouping.unwrap_or(0); + let mut groups: Vec = vec![1]; + for i in 0..7u32 { + // bit_set(mask, 6 - i) — most-right bit is bit 0. + let bit = (mask >> (6 - i as u8)) & 1; + if bit == 0 { + groups.push(1); + } else { + let last = groups.last_mut().expect("at least one group"); + *last += 1; + } + } + let num_window_groups = groups.len() as u8; + (8, num_window_groups, groups, num_swb) +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +fn read_u16(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 16); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u16) +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} diff --git a/crates/vendor/oxideav-aac/src/intensity_stereo.rs b/crates/vendor/oxideav-aac/src/intensity_stereo.rs new file mode 100644 index 00000000..ed361aac --- /dev/null +++ b/crates/vendor/oxideav-aac/src/intensity_stereo.rs @@ -0,0 +1,621 @@ +//! §4.6.8.2 Intensity Stereo (IS) decoding — ISO/IEC 14496-3. +//! +//! Intensity stereo is the second joint-channel tool of a channel +//! pair (the first being M/S, [`crate::ms_stereo`]). Where M/S +//! reconstructs both channels from a mid/side basis, intensity stereo +//! derives the **right** channel entirely from the **left** channel +//! by a single per-band real scale, exploiting the ear's reduced +//! sensitivity to phase at high frequencies. The left channel is +//! untouched. +//! +//! ## §4.6.8.2.3 decoding process +//! +//! Intensity stereo is signalled by the pseudo codebooks +//! `INTENSITY_HCB` (15, in-phase) and `INTENSITY_HCB2` (14, +//! out-of-phase) appearing in the **right** channel's `sfb_cb` (their +//! use in a left channel is illegal). For each intensity-coded band a +//! transmitted *intensity stereo position* `is_pos[g][sfb]` replaces +//! the right channel's scalefactor; the §4.6.8.2.3 reconstruction is +//! +//! ```text +//! is_intensity(g,sfb) = +1 if right sfb_cb == INTENSITY_HCB (15) +//! -1 if right sfb_cb == INTENSITY_HCB2 (14) +//! 0 otherwise +//! invert_intensity(g,sfb)= 1 - 2*ms_used[g][sfb] if ms_mask_present == 1 +//! (and aot != AAC scalable) +//! +1 otherwise +//! scale = is_intensity(g,sfb) * invert_intensity(g,sfb) +//! * 0.5^(0.25 * is_pos[g][sfb]); +//! for (i = 0; i < swb_offset[sfb+1]-swb_offset[sfb]; i++) +//! r_spec[g][b][sfb][i] = scale * l_spec[g][b][sfb][i]; +//! ``` +//! +//! The `0.5^(0.25·is_pos)` magnitude is the same per-quarter-step gain +//! ladder as the §4.6.2.3.3 scalefactor gain `2^(0.25·(sf−100))` (the +//! intensity position plays the role of a scalefactor difference); the +//! `is_intensity` factor carries the in/out-of-phase sign of the +//! codebook and `invert_intensity` flips it when the band's `ms_used` +//! bit is set under a per-band M/S mask (`ms_mask_present == 1`). This +//! is a deterministic algebraic reconstruction — no rounding tables and +//! no RNG — so an intensity-coded band comes out byte-exact. +//! +//! ## Mutual exclusion (§4.6.8.1.3 note / §4.6.8.2.3 / §4.6.13.3) +//! +//! M/S, intensity stereo, and PNS are mutually exclusive on any one +//! `(group, sfb)`. This tool only ever rewrites the right channel of a +//! band whose **right** `sfb_cb` is an intensity book; M/S already +//! skips those bands (it consults the right channel's intensity status +//! via `is_intensity`). A band that is `NOISE_HCB` cannot also be an +//! intensity book, so no extra noise guard is needed here — the +//! `is_intensity` predicate is `0` for every non-intensity codebook +//! and the band is left as the inverse-quantised passthrough. +//! +//! ## Decoder block order +//! +//! Per §4.6 the channel-pair / noise tools run inverse-quant → M/S → +//! PNS → intensity → TNS on the **de-interleaved, window-major** +//! spectrum produced by [`crate::decoded_spectrum::quant_to_spec`] +//! (`spec[w * window_len + k]`), so this pass runs after +//! [`crate::ms_stereo::apply_ms_stereo`] and before the §4.6.9 TNS +//! filter. The per-band coefficient extent +//! `swb_offset[sfb+1]-swb_offset[sfb]` and the +//! `(group, in-group window) → absolute window` mapping match the rest +//! of the pipeline. +//! +//! ## Scope +//! +//! This module is the intensity-stereo / left-to-right derivation only. +//! The dependently-switched coupling channel contribution of the +//! "intensity stereo / coupling" tool (§4.6.8.2.1, fed by a CCE) and +//! PNS synthesis (§4.6.13) are separate follow-ups. The +//! `is_position[g][sfb]` track itself is produced upstream by the +//! §4.6.8.1.4 DPCM accumulator +//! ([`crate::scale_factor_data::accumulate`]). + +use crate::ics_info::IcsInfo; +use crate::section_data::{INTENSITY_HCB, INTENSITY_HCB2}; +#[cfg(test)] +use crate::swb_offset::{ + long_window_offsets, short_window_offsets, LONG_WINDOW_LEN, SHORT_WINDOW_LEN, +}; +use crate::{Error, Result}; + +/// §4.6.8.2.3 `is_intensity(g,sfb)` — the in/out-of-phase sign of an +/// intensity band, keyed on the **right** channel codebook. +/// +/// `+1` for `INTENSITY_HCB` (15, in-phase), `-1` for `INTENSITY_HCB2` +/// (14, out-of-phase), `0` for any non-intensity codebook (the band is +/// not intensity-coded and is left untouched). +pub fn is_intensity(right_cb: u8) -> i32 { + match right_cb { + INTENSITY_HCB => 1, + INTENSITY_HCB2 => -1, + _ => 0, + } +} + +/// §4.6.8.2.3 `invert_intensity(g,sfb)` — the phase-reversal factor. +/// +/// Returns `1 - 2*ms_used` (i.e. `+1` when `ms_used == false`, `-1` +/// when `true`) under a per-band M/S mask (`ms_mask_present == 1`) for +/// a non-scalable GA decoder; `+1` otherwise. Because M/S and +/// intensity are mutually exclusive on a band, a set `ms_used` bit on +/// an intensity band carries the §4.6.8.2.3 phase reversal rather than +/// an M/S de-matrix. +pub fn invert_intensity(per_band_mask: bool, ms_used: bool) -> i32 { + if per_band_mask { + 1 - 2 * (ms_used as i32) + } else { + 1 + } +} + +/// §4.6.8.2.3 `0.5^(0.25 * is_pos)` — the intensity-position gain. +/// +/// The same per-quarter-step ladder as the §4.6.2.3.3 scalefactor gain +/// but on a base of `1/2`; a larger position attenuates the derived +/// right channel. +pub fn intensity_gain(is_pos: i32) -> f64 { + 0.5f64.powf(0.25 * is_pos as f64) +} + +/// A channel pair's de-interleaved spectra plus the right-channel +/// codebooks and intensity positions the §4.6.8.2.3 derivation needs. +/// +/// `left` / `right` are the window-major decoded spectra +/// (`num_windows × window_len`) produced by +/// [`crate::decoded_spectrum::quant_to_spec`]. `left` is read only; +/// for every intensity-coded band `right` is overwritten with +/// `scale · left`. Bands whose right `sfb_cb` is not an intensity book +/// are left exactly as they arrive (the inverse-quantised passthrough). +/// +/// `right_sfb_cb` is the right channel's `sfb_cb[g][sfb]` (from its +/// [`crate::section_data::SectionData`]); it both selects which bands +/// are intensity-coded and supplies the in/out-of-phase sign. +/// +/// `is_pos` is the right channel's absolute `is_pos[g][sfb]` track +/// (§4.6.8.1.4, from [`crate::scale_factor_data::accumulate`]). Only +/// the entries at intensity-coded `(g, sfb)` are consulted. +#[derive(Debug)] +pub struct IntensityPairSpectra<'a> { + /// First ("left") channel spectrum — read only. + pub left: &'a [f64], + /// Second ("right") channel spectrum — derived from `left` on + /// intensity bands, untouched elsewhere. + pub right: &'a mut [f64], + /// Right channel `sfb_cb[g][sfb]`. + pub right_sfb_cb: &'a [Vec], + /// Right channel absolute `is_pos[g][sfb]` (§4.6.8.1.4). + pub is_pos: &'a [Vec], +} + +/// Apply the §4.6.8.2.3 intensity-stereo left→right derivation in place. +/// +/// * `pair` — the channel-pair spectra, right-channel codebooks, and +/// intensity positions ([`IntensityPairSpectra`]). +/// * `ms_mask_present` — `true` ⇔ the CPE carries a per-band `ms_used` +/// mask (`ms_mask_present == 1`); selects the §4.6.8.2.3 +/// `invert_intensity` phase-reversal branch. +/// * `ms_used` — `ms_used[g][sfb]` (one row per window group, each at +/// least `max_sfb` long). Consulted only when `ms_mask_present` is +/// `true`; pass an empty slice otherwise. +/// * `ics_info` — the shared `common_window` `ics_info()`; supplies +/// `num_window_groups`, `window_group_length`, `max_sfb`, and the +/// window geometry. +/// * `fs_index` — `samplingFrequencyIndex`, selecting the `swb_offset` +/// table. +/// +/// Returns [`Error::IntensityStereoInvalid`] if the buffer / mask / +/// `sfb_cb` / `is_pos` shapes disagree with `ics_info` (see the variant +/// docs). When no band is intensity-coded the right buffer is left +/// untouched. +pub fn apply_intensity_stereo( + pair: &mut IntensityPairSpectra<'_>, + ms_mask_present: bool, + ms_used: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, +) -> Result<()> { + let IntensityPairSpectra { + left, + right, + right_sfb_cb, + is_pos, + } = pair; + + let window_len = ics_info.window_len()?; + let offsets = ics_info.swb_offsets(fs_index)?; + let num_swb = offsets.len() - 1; + let num_windows = ics_info.num_windows as usize; + let num_groups = ics_info.num_window_groups as usize; + let max_sfb = ics_info.max_sfb as usize; + + // Geometry consistency: both channels share the common_window + // ics_info, so both spectra are num_windows × window_len. + let expected = num_windows * window_len; + if left.len() != expected || right.len() != expected { + return Err(Error::IntensityStereoInvalid); + } + if ics_info.window_group_length.len() != num_groups + || ics_info + .window_group_length + .iter() + .map(|&w| w as usize) + .sum::() + != num_windows + { + return Err(Error::IntensityStereoInvalid); + } + // max_sfb must not exceed the band count of the active window. + if max_sfb > num_swb { + return Err(Error::IntensityStereoInvalid); + } + // The right channel drives both the intensity predicate and the + // is_pos lookup, so both tables must cover every (g, sfb). + if right_sfb_cb.len() != num_groups || is_pos.len() != num_groups { + return Err(Error::IntensityStereoInvalid); + } + for g in 0..num_groups { + if right_sfb_cb[g].len() < max_sfb || is_pos[g].len() < max_sfb { + return Err(Error::IntensityStereoInvalid); + } + } + // A per-band mask needs a full ms_used[g][sfb]; otherwise it is + // ignored. + if ms_mask_present { + if ms_used.len() != num_groups { + return Err(Error::IntensityStereoInvalid); + } + for row in ms_used { + if row.len() < max_sfb { + return Err(Error::IntensityStereoInvalid); + } + } + } + + let mut window_base = 0usize; + for g in 0..num_groups { + let wgl = ics_info.window_group_length[g] as usize; + for sfb in 0..max_sfb { + let sign = is_intensity(right_sfb_cb[g][sfb]); + if sign == 0 { + // Not an intensity band: leave the right channel as the + // inverse-quantised / M/S-reconstructed passthrough. + continue; + } + let used = ms_mask_present && ms_used[g][sfb]; + let inv = invert_intensity(ms_mask_present, used); + let scale = sign as f64 * inv as f64 * intensity_gain(is_pos[g][sfb]); + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + for b in 0..wgl { + let base = (window_base + b) * window_len; + for i in start..end { + right[base + i] = scale * left[base + i]; + } + } + } + window_base += wgl; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + use crate::section_data::{NOISE_HCB, ZERO_HCB}; + + const FS_44100: u8 = 4; + const SPECTRUM_CB: u8 = 2; + + fn long_ics_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[FS_44100 as usize], + } + } + + fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { + let num_window_groups = window_group_length.len() as u8; + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups, + window_group_length, + num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[FS_44100 as usize], + } + } + + fn plain_cb(num_groups: usize, max_sfb: usize) -> Vec> { + vec![vec![SPECTRUM_CB; max_sfb]; num_groups] + } + + fn zero_pos(num_groups: usize, max_sfb: usize) -> Vec> { + vec![vec![0i32; max_sfb]; num_groups] + } + + #[allow(clippy::too_many_arguments)] + fn run( + left: &[f64], + right: &mut [f64], + ms_mask_present: bool, + ms_used: &[Vec], + right_sfb_cb: &[Vec], + is_pos: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, + ) -> Result<()> { + let mut pair = IntensityPairSpectra { + left, + right, + right_sfb_cb, + is_pos, + }; + apply_intensity_stereo(&mut pair, ms_mask_present, ms_used, ics_info, fs_index) + } + + #[test] + fn is_intensity_sign() { + assert_eq!(is_intensity(INTENSITY_HCB), 1); + assert_eq!(is_intensity(INTENSITY_HCB2), -1); + assert_eq!(is_intensity(SPECTRUM_CB), 0); + assert_eq!(is_intensity(NOISE_HCB), 0); + assert_eq!(is_intensity(ZERO_HCB), 0); + } + + #[test] + fn invert_intensity_branches() { + // No per-band mask: always +1 regardless of ms_used. + assert_eq!(invert_intensity(false, false), 1); + assert_eq!(invert_intensity(false, true), 1); + // Per-band mask: 1 - 2*ms_used. + assert_eq!(invert_intensity(true, false), 1); + assert_eq!(invert_intensity(true, true), -1); + } + + #[test] + fn intensity_gain_quarter_ladder() { + // 0.5^0 = 1. + assert!((intensity_gain(0) - 1.0).abs() < 1e-12); + // 0.5^(0.25*4) = 0.5^1 = 0.5. + assert!((intensity_gain(4) - 0.5).abs() < 1e-12); + // 0.5^(0.25*8) = 0.25. + assert!((intensity_gain(8) - 0.25).abs() < 1e-12); + // negative position amplifies: 0.5^(-1) = 2. + assert!((intensity_gain(-4) - 2.0).abs() < 1e-12); + } + + #[test] + fn in_phase_pos_zero_copies_left() { + // INTENSITY_HCB, is_pos = 0, no mask → scale = +1: right == left. + let info = long_ics_info(3); + let max_sfb = 3; + let off = long_window_offsets(FS_44100).unwrap(); + let n = LONG_WINDOW_LEN as usize; + let mut left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + for (k, (l, r)) in left.iter_mut().zip(right.iter_mut()).enumerate() { + if k >= off[0] as usize && k < off[3] as usize { + *l = (k as f64) * 0.5 - 3.0; + *r = 999.0; // garbage to be overwritten + } + } + let mut cb = plain_cb(1, max_sfb); + cb[0][1] = INTENSITY_HCB; // make sfb 1 intensity-coded + let pos = zero_pos(1, max_sfb); + + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + + // sfb 1 derived from left; sfb 0 and 2 untouched (still garbage). + for (r, l) in right + .iter() + .zip(left.iter()) + .take(off[2] as usize) + .skip(off[1] as usize) + { + assert!((r - l).abs() < 1e-12); + } + for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { + assert_eq!(r, 999.0); + } + for &r in right.iter().take(off[3] as usize).skip(off[2] as usize) { + assert_eq!(r, 999.0); + } + } + + #[test] + fn out_of_phase_negates() { + // INTENSITY_HCB2 (sign -1), is_pos = 0, no mask → scale = -1. + let info = long_ics_info(2); + let off = long_window_offsets(FS_44100).unwrap(); + let n = LONG_WINDOW_LEN as usize; + let mut left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { + *l = 7.0; + } + let mut cb = plain_cb(1, 2); + cb[0][0] = INTENSITY_HCB2; + let pos = zero_pos(1, 2); + + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + + for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { + assert!((r + 7.0).abs() < 1e-12); + } + } + + #[test] + fn position_scales_gain() { + // is_pos = 4 → gain 0.5; in-phase, no mask → right = 0.5 * left. + let info = long_ics_info(1); + let off = long_window_offsets(FS_44100).unwrap(); + let n = LONG_WINDOW_LEN as usize; + let mut left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { + *l = 16.0; + } + let mut cb = plain_cb(1, 1); + cb[0][0] = INTENSITY_HCB; + let mut pos = zero_pos(1, 1); + pos[0][0] = 4; + + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + + for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { + assert!((r - 8.0).abs() < 1e-12); + } + } + + #[test] + fn ms_used_inverts_phase_under_mask() { + // INTENSITY_HCB (sign +1) with a set ms_used bit under a + // per-band mask flips to -1: right = -left. + let info = long_ics_info(1); + let off = long_window_offsets(FS_44100).unwrap(); + let n = LONG_WINDOW_LEN as usize; + let mut left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { + *l = 3.0; + } + let mut cb = plain_cb(1, 1); + cb[0][0] = INTENSITY_HCB; + let pos = zero_pos(1, 1); + let ms_used = vec![vec![true]]; + + run( + &left, &mut right, true, &ms_used, &cb, &pos, &info, FS_44100, + ) + .unwrap(); + + for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { + assert!((r + 3.0).abs() < 1e-12); + } + } + + #[test] + fn ms_used_ignored_without_mask() { + // Same set ms_used bit but ms_mask_present == false → +1 (the + // mask is not consulted; invert_intensity is +1). + let info = long_ics_info(1); + let off = long_window_offsets(FS_44100).unwrap(); + let n = LONG_WINDOW_LEN as usize; + let mut left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { + *l = 3.0; + } + let mut cb = plain_cb(1, 1); + cb[0][0] = INTENSITY_HCB; + let pos = zero_pos(1, 1); + + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + + for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { + assert!((r - 3.0).abs() < 1e-12); + } + } + + #[test] + fn non_intensity_bands_untouched() { + // No intensity codebook anywhere → right is left exactly as-is. + let info = long_ics_info(4); + let n = LONG_WINDOW_LEN as usize; + let left = vec![1.0f64; n]; + let mut right = vec![42.0f64; n]; + let cb = plain_cb(1, 4); // all spectrum books + let pos = zero_pos(1, 4); + + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + + assert!(right.iter().all(|&v| v == 42.0)); + } + + #[test] + fn short_window_grouping() { + // Two groups of 4 + 4 short windows; intensity on sfb 0 of + // group 1. Every window of that group derives right from left. + let wgl = vec![4u8, 4u8]; + let info = short_ics_info(2, wgl.clone()); + let off = short_window_offsets(FS_44100).unwrap(); + let wlen = SHORT_WINDOW_LEN as usize; + let n = 8 * wlen; + let mut left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + for w in 0..8 { + for k in (off[0] as usize)..(off[1] as usize) { + left[w * wlen + k] = (w as f64) + 1.0; + } + } + let mut cb = plain_cb(2, 2); + cb[1][0] = INTENSITY_HCB; // group 1 sfb 0 in-phase + let pos = zero_pos(2, 2); + + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + + // Group 0 (windows 0..4) untouched, group 1 (windows 4..8) + // derived as right = left (in-phase, pos 0). + for w in 0..4 { + for k in (off[0] as usize)..(off[1] as usize) { + assert_eq!(right[w * wlen + k], 0.0); + } + } + for w in 4..8 { + for k in (off[0] as usize)..(off[1] as usize) { + assert!((right[w * wlen + k] - left[w * wlen + k]).abs() < 1e-12); + } + } + } + + #[test] + fn rejects_length_mismatch() { + let info = long_ics_info(1); + let left = vec![0.0f64; 10]; + let mut right = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let cb = plain_cb(1, 1); + let pos = zero_pos(1, 1); + let e = run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100); + assert_eq!(e, Err(Error::IntensityStereoInvalid)); + } + + #[test] + fn rejects_short_is_pos() { + let info = long_ics_info(3); + let n = LONG_WINDOW_LEN as usize; + let left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + let cb = plain_cb(1, 3); + let pos = zero_pos(1, 2); // too short + let e = run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100); + assert_eq!(e, Err(Error::IntensityStereoInvalid)); + } + + #[test] + fn rejects_missing_ms_used_under_mask() { + let info = long_ics_info(1); + let n = LONG_WINDOW_LEN as usize; + let left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + let cb = plain_cb(1, 1); + let pos = zero_pos(1, 1); + // ms_mask_present true but ms_used empty → reject. + let e = run(&left, &mut right, true, &[], &cb, &pos, &info, FS_44100); + assert_eq!(e, Err(Error::IntensityStereoInvalid)); + } + + #[test] + fn rejects_max_sfb_over_num_swb() { + let mut info = long_ics_info(1); + info.max_sfb = 99; // exceeds long-window band count + let n = LONG_WINDOW_LEN as usize; + let left = vec![0.0f64; n]; + let mut right = vec![0.0f64; n]; + let cb = vec![vec![SPECTRUM_CB; 99]]; + let pos = vec![vec![0i32; 99]]; + let e = run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100); + assert_eq!(e, Err(Error::IntensityStereoInvalid)); + } + + #[test] + fn no_intensity_band_leaves_right_untouched() { + // Empty/degenerate: max_sfb 0 → nothing to scan. + let mut info = long_ics_info(0); + info.max_sfb = 0; + let n = LONG_WINDOW_LEN as usize; + let left = vec![1.0f64; n]; + let mut right = vec![5.0f64; n]; + let cb: Vec> = vec![vec![]]; + let pos: Vec> = vec![vec![]]; + run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); + assert!(right.iter().all(|&v| v == 5.0)); + } +} diff --git a/crates/vendor/oxideav-aac/src/ipqf.rs b/crates/vendor/oxideav-aac/src/ipqf.rs new file mode 100644 index 00000000..5ce71771 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ipqf.rs @@ -0,0 +1,298 @@ +//! IPQF — the SSR inverse polyphase quadrature filter (ISO/IEC +//! 14496-3 §4.6.12.3.4). +//! +//! The IPQF is the final stage of the SSR (AOT 3) gain-control tool: it +//! recombines the four per-band gain-controlled sample streams `V_B` +//! (produced by [`crate::gain_control::GainBandState::window_overlap`]) +//! into a single full-rate PCM time signal `AS(n)`, cancelling the +//! aliasing the encoder's PQF analysis introduced. +//! +//! ## Synthesis filter (§4.6.12.3.4) +//! +//! The four bands are interpolated 4× (one band sample every fourth +//! output sample) and cosine-modulated through a length-96 prototype +//! filter: +//! +//! ```text +//! Ṽ_B(j) = V_B(k) if j == 4k, else 0 (4× upsample) +//! +//! Q_B(j) = Q(j) · cos( (2B+1)(2j−3)π / 16 ), 0 ≤ j ≤ 95 +//! +//! AS(n) = Σ_{B=0}^{3} Σ_{j=0}^{95} Q_B(j) · Ṽ_B(n − j) +//! ``` +//! +//! The length-96 prototype `Q(j)` is symmetric: `Q(0..=47)` are the +//! Table 4.110 values; `Q(48..=95)` mirror them as `Q(j) = Q(95 − j)`. +//! +//! Because the `Ṽ_B` interpolation places a band sample only at the +//! multiples of four, the inner sum over `j` touches band `B`'s history +//! at the strided positions `j ≡ n (mod 4)`. The synthesizer is run as +//! a streaming polyphase bank: it keeps a 96-tap (24 band-sample) ring +//! of recent `V_B` history per band so each call produces the next +//! block of `AS(n)` from the new band samples and the carried tail. +//! +//! ## Provenance +//! +//! The prototype coefficients are Table 4.110 of ISO/IEC 14496-3:2001 +//! (a numeric data table) staged under `docs/audio/aac/`; the +//! modulation and upsampling equations are the §4.6.12.3.4 normative +//! formulas. No external SSR / PQF implementation was consulted. + +use core::f64::consts::PI; + +/// The number of IPQF bands (§4.6.12.1): four uniform frequency bands. +pub const NUM_BANDS: usize = 4; + +/// The prototype filter length (§4.6.12.3.4): `Q(0..=95)`. +pub const PROTO_LEN: usize = 96; + +/// `Q(0)..=Q(47)` — the first half of the §4.6.12.3.4 prototype filter, +/// ISO/IEC 14496-3 Table 4.110. The second half `Q(48)..=Q(95)` is the +/// mirror `Q(j) = Q(95 − j)` (see [`prototype`]). +/// +/// The literals are the f64-exact shortest round-trip forms of the +/// Table 4.110 decimal values (the table prints ~17 significant +/// digits, more than an `f64` can distinguish; these are the canonical +/// shortest forms that decode to the identical bit pattern). +pub const Q_HALF: [f64; 48] = [ + 9.765529100757551e-5, + 1.3809589379038567e-4, + 9.840074925662353e-5, + -8.667154478233572e-5, + -4.6217998911921346e-4, + -1.0211814095158174e-3, + -1.6772149340010668e-3, + -2.253333895141108e-3, + -2.4987888343213967e-3, + -2.139081596676188e-3, + -9.559539745459777e-4, + 1.1172111530118943e-3, + 3.909130912734858e-3, + 6.963570342011867e-3, + 9.559544215947834e-3, + 1.081576654002136e-2, + 9.87705149917153e-3, + 6.156256729132736e-3, + -4.179394606362971e-4, + -9.212874309770764e-3, + -1.883077587336902e-2, + -2.7226498457701823e-2, + -3.2022840857588906e-2, + -3.099633252775461e-2, + -2.2656858741499447e-2, + -6.803111385896335e-3, + 1.5085400948280744e-2, + 3.975099338827274e-2, + 6.244536362943674e-2, + 7.762232774872133e-2, + 7.996833849613293e-2, + 6.561549306847558e-2, + 3.331365830088269e-2, + -1.4691563058190206e-2, + -7.230789047533415e-2, + -1.2993222541703875e-1, + -1.7551641029040532e-1, + -1.9626543957670528e-1, + -1.807333067021503e-1, + -1.2097653136035738e-1, + -1.4377370758549035e-2, + 1.3522730742860303e-1, + 3.1737852699301633e-1, + 5.159002179848223e-1, + 7.108002037976138e-1, + 8.80906324884448e-1, + 1.0068321641150089e0, + 1.0737914947736096e0, +]; + +/// The full length-96 prototype filter `Q(0..=95)` (§4.6.12.3.4): the +/// [`Q_HALF`] first half plus its mirror `Q(j) = Q(95 − j)`. +#[must_use] +pub fn prototype() -> [f64; PROTO_LEN] { + let mut q = [0.0f64; PROTO_LEN]; + q[..48].copy_from_slice(&Q_HALF); + for j in 48..PROTO_LEN { + q[j] = Q_HALF[95 - j]; + } + q +} + +/// The §4.6.12.3.4 synthesis-filter coefficient +/// `Q_B(j) = Q(j) · cos((2B+1)(2j−3)π/16)` for band `b`, tap `j`. +#[must_use] +fn synthesis_coef(q: &[f64; PROTO_LEN], b: usize, j: usize) -> f64 { + let angle = (2.0 * b as f64 + 1.0) * (2.0 * j as f64 - 3.0) * PI / 16.0; + q[j] * angle.cos() +} + +/// Streaming IPQF synthesizer: holds the per-band `V_B` history needed +/// to evaluate the length-96 `AS(n)` convolution across frame +/// boundaries. +/// +/// The interpolation `Ṽ_B(j) = V_B(j/4)` means tap `j` of the +/// convolution reads band sample `(n − j)/4` for `j ≡ n (mod 4)`. The +/// prototype spans `j ∈ 0..=95`, so the bank needs the last +/// `ceil(96/4) = 24` band samples per band; a 24-deep ring per band is +/// retained between [`Ipqf::synthesize`] calls. +#[derive(Debug, Clone)] +pub struct Ipqf { + /// The precomputed `Q_B(j)` matrix, `[band][tap]`. + coefs: [[f64; PROTO_LEN]; NUM_BANDS], + /// Per-band history of the most recent band samples, newest last. + /// Holds at least [`HISTORY`] entries once primed. + history: [Vec; NUM_BANDS], +} + +/// The number of past band samples the length-96 prototype reaches: +/// `ceil(PROTO_LEN / NUM_BANDS) = 24`. +const HISTORY: usize = PROTO_LEN.div_ceil(NUM_BANDS); + +impl Default for Ipqf { + fn default() -> Self { + Self::new() + } +} + +impl Ipqf { + /// A fresh synthesizer with the prototype-derived coefficients and + /// zero-initialised history (the spec's implicit pre-stream + /// silence). + #[must_use] + pub fn new() -> Self { + let q = prototype(); + let mut coefs = [[0.0f64; PROTO_LEN]; NUM_BANDS]; + for (b, band) in coefs.iter_mut().enumerate() { + for (j, slot) in band.iter_mut().enumerate() { + *slot = synthesis_coef(&q, b, j); + } + } + let history = core::array::from_fn(|_| vec![0.0f64; HISTORY]); + Ipqf { coefs, history } + } + + /// Synthesize `AS(n)` for `len` band-sample steps from the four + /// per-band input streams `bands[B]`. + /// + /// Each `bands[B]` supplies the next `len` band samples `V_B`. The + /// returned vector holds `NUM_BANDS · len` output samples — the + /// IPQF interpolates each band sample to four full-rate positions, + /// so `len` band steps produce `4·len` PCM samples. + /// + /// The convolution `AS(n) = Σ_B Σ_j Q_B(j)·Ṽ_B(n − j)` is evaluated + /// at every output position `n`, reading band `B`'s history at the + /// strided positions; the per-band history rings advance one band + /// sample per step. + /// + /// # Panics + /// + /// Panics if any `bands[B]` has fewer than `len` samples. + #[must_use] + pub fn synthesize(&mut self, bands: &[&[f64]; NUM_BANDS], len: usize) -> Vec { + let mut out = Vec::with_capacity(NUM_BANDS * len); + for step in 0..len { + // Push the new band sample of each band onto its ring. + for (hist, band) in self.history.iter_mut().zip(bands.iter()) { + hist.push(band[step]); + } + // For this band step we emit NUM_BANDS output samples + // n = 4·step + p, p = 0..NUM_BANDS. Output position n reads + // Ṽ_B(n − j): non-zero only when (n − j) ≡ 0 (mod 4), i.e. + // the band sample index is (n − j)/4. The newest band sample + // sits at history end (index L−1) and is the p-aligned + // phase, so tap j = 4·t + p reads the t-th most recent + // band sample. + for p in 0..NUM_BANDS { + let mut acc = 0.0f64; + for (coefs, hist) in self.coefs.iter().zip(self.history.iter()) { + let l = hist.len(); + let mut j = p; + while j < PROTO_LEN { + let t = j / NUM_BANDS; // how many band samples back + if t < l { + acc += coefs[j] * hist[l - 1 - t]; + } + j += NUM_BANDS; + } + } + out.push(acc); + } + // Trim the rings to the needed depth to bound memory. + for hist in &mut self.history { + let l = hist.len(); + if l > HISTORY { + hist.drain(0..l - HISTORY); + } + } + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn prototype_is_symmetric() { + let q = prototype(); + for j in 0..PROTO_LEN { + assert!((q[j] - q[95 - j]).abs() < 1e-15, "Q({j}) != Q({})", 95 - j); + } + // Spot-check the documented endpoints. + assert!((q[0] - 9.765529100757551e-5).abs() < 1e-18); + assert!((q[47] - 1.0737914947736096e0).abs() < 1e-15); + assert!((q[48] - 1.0737914947736096e0).abs() < 1e-15); + assert!((q[95] - 9.765529100757551e-5).abs() < 1e-18); + } + + #[test] + fn silence_produces_silence() { + let mut ipqf = Ipqf::new(); + let z = vec![0.0f64; 16]; + let bands: [&[f64]; NUM_BANDS] = [&z, &z, &z, &z]; + let out = ipqf.synthesize(&bands, 16); + assert_eq!(out.len(), NUM_BANDS * 16); + assert!(out.iter().all(|&x| x == 0.0)); + } + + #[test] + fn output_length_is_four_times_band_steps() { + let mut ipqf = Ipqf::new(); + let s: Vec = (0..10).map(|i| i as f64).collect(); + let bands: [&[f64]; NUM_BANDS] = [&s, &s, &s, &s]; + let out = ipqf.synthesize(&bands, 10); + assert_eq!(out.len(), 40); + assert!(out.iter().all(|x| x.is_finite())); + } + + #[test] + fn synthesis_coef_first_band_zero_tap() { + // Q_0(0) = Q(0)·cos((1)(−3)π/16). + let q = prototype(); + let expect = q[0] * ((-3.0) * PI / 16.0).cos(); + assert!((synthesis_coef(&q, 0, 0) - expect).abs() < 1e-15); + } + + #[test] + fn impulse_response_matches_direct_convolution() { + // Feed an impulse into band 0 and verify the streamed output + // equals the direct §4.6.12.3.4 convolution for the first + // several output samples: AS(n) = Σ_j Q_0(j)·Ṽ_0(n−j), with + // Ṽ_0(0)=1 (impulse) and 0 elsewhere ⇒ AS(n) = Q_0(n). + let q = prototype(); + let mut ipqf = Ipqf::new(); + let mut b0 = vec![0.0f64; 30]; + b0[0] = 1.0; // first band sample = 1 ⇒ Ṽ_0(0)=1. + let z = vec![0.0f64; 30]; + let bands: [&[f64]; NUM_BANDS] = [&b0, &z, &z, &z]; + let out = ipqf.synthesize(&bands, 30); + // AS(n) for n = 0..PROTO_LEN should equal Q_0(n). + for (n, &got) in out.iter().take(PROTO_LEN).enumerate() { + let expect = synthesis_coef(&q, 0, n); + assert!( + (got - expect).abs() < 1e-12, + "AS({n}) = {got} != Q_0({n}) = {expect}" + ); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/latm.rs b/crates/vendor/oxideav-aac/src/latm.rs new file mode 100644 index 00000000..fbae51bb --- /dev/null +++ b/crates/vendor/oxideav-aac/src/latm.rs @@ -0,0 +1,1902 @@ +//! LATM / LOAS transport framing — ISO/IEC 14496-3 §1.7. +//! +//! LATM (Low-overhead MPEG-4 Audio Transport Multiplex) is the +//! multiplex layer that packs one or more MPEG-4 Audio payloads plus +//! their [`AudioSpecificConfig`] (ASC) into a single multiplexed +//! element ([`AudioMuxElement`], §1.7.3.1 Table 1.41). LOAS +//! (Low Overhead Audio Stream) is the synchronization layer above it +//! ([`AudioSyncStream`], §1.7.2.1 Table 1.36), which prefixes each +//! multiplexed element with a `0x2B7` syncword and a 13-bit byte +//! length so the multiplex can be recovered from a transmission +//! channel that carries no framing of its own. +//! +//! This module decodes the transport structure end to end for the AAC +//! case — the configuration ([`StreamMuxConfig`], Table 1.42), the +//! per-subframe payload lengths ([`PayloadLengthInfo`], Table 1.44), +//! and the multiplexed AAC access units ([`PayloadMux`], Table 1.45) — +//! and hands the recovered raw-data-block byte slices to the +//! [`crate::decode::StreamDecoder`] / [`crate::raw_data_block`] layer. +//! +//! ## Scope +//! +//! The decode path supports the configurations that carry AAC: +//! `audioMuxVersion ∈ {0, 1}` (the `audioMuxVersion == 1` +//! `taraBufferFullness` / per-ASC length-prefix extensions are parsed), +//! `allStreamsSameTimeFraming` in both states, and the per-layer +//! `frameLengthType` values `0` (variable-length, byte count carried +//! in `PayloadLengthInfo()`) and `1` (fixed `frameLength` bits in +//! `StreamMuxConfig()`). The CELP (`3`/`4`/`5`) and HVXC (`6`/`7`) +//! frame-length-table-indexed types are surfaced as +//! [`Error::LatmUnsupportedFrameLengthType`] — they index frame-length +//! tables for object types this AAC-focused crate does not decode. The +//! `audioMuxVersionA == 1` reserved branch is +//! [`Error::LatmAudioMuxVersionAReserved`]. The `EPMuxElement()` +//! error-protected variant (Table 1.40) and the +//! `EPAudioSyncStream()` FEC header (Table 1.37) are parsed at the +//! framing level but the EP-tool payload de-interleave is out of +//! scope. + +use crate::asc::AudioSpecificConfig; +use crate::crc; +use crate::{Error, Result}; +use oxideav_core::bits::BitReader; + +/// §1.7.2.1 Table 1.36 `AudioSyncStream()` syncword (`0x2B7`, 11 bits). +pub const AUDIO_SYNC_STREAM_SYNCWORD: u32 = 0x2B7; + +/// §1.7.2.1 Table 1.37 `EPAudioSyncStream()` syncword (`0x4DE1`, +/// 16 bits). +pub const EP_AUDIO_SYNC_STREAM_SYNCWORD: u32 = 0x4DE1; + +/// §1.7.2.2.1: "The maximum byte-distance between two syncwords is +/// 8192 bytes", encoded in the 13-bit `audioMuxLengthBytes` field. +pub const MAX_AUDIO_MUX_LENGTH_BYTES: u32 = (1 << 13) - 1; + +/// §1.7.3 signalling caps: `numProgram` is 4-bit (max program index +/// 15), `numLayer` is 3-bit (max layer index 7), `streamIndx` is +/// 4-bit (max 15 streams), `numChunk` is 4-bit. +const MAX_PROGRAM_INDEX: u32 = 15; +const MAX_LAYER_INDEX: u32 = 7; +const MAX_STREAM_COUNT: usize = 16; + +/// One decoded scalable layer of a [`StreamMuxConfig`] program. +/// +/// Mirrors the per-`streamID[prog][lay]` state the Table 1.42 loop +/// builds: the parsed [`AudioSpecificConfig`] (or `None` when +/// `useSameConfig` pointed at an earlier layer's config), the +/// `frameLengthType`, and the framing parameter that type selects +/// (`latmBufferFullness` for type 0, `frameLength` bits for type 1). +#[derive(Debug, Clone)] +pub struct LayerConfig { + /// `progSIndx` — the program this layer belongs to. + pub prog: u8, + /// `laySIndx` — the layer index within the program. + pub lay: u8, + /// `streamID[prog][lay]` — the flat stream counter assigned in + /// transmission order. + pub stream_id: u8, + /// The layer's [`AudioSpecificConfig`]. `None` ⇔ `useSameConfig` + /// was set, meaning "apply the ASC most recently transmitted in a + /// previous layer or program" (§1.7.3.2.3). [`StreamMuxConfig`] + /// resolves this into [`LayerConfig::effective_asc`] on parse, so + /// callers always have a concrete config there. + pub asc: Option, + /// The effective ASC after resolving `useSameConfig` back to the + /// most recently transmitted config. Always populated. + pub effective_asc: AudioSpecificConfig, + /// `frameLengthType[streamID]` (§1.7.3.1 Table 1.42). + pub frame_length_type: u8, + /// `latmBufferFullness[streamID]` — present (8-bit) only for + /// `frameLengthType == 0`. + pub latm_buffer_fullness: Option, + /// `coreFrameOffset` — present (6-bit) only for + /// `frameLengthType == 0`, `!allStreamsSameTimeFraming`, and a + /// CELP-core / AAC-enhancement layer pairing. + pub core_frame_offset: Option, + /// `frameLength[streamID]` — present (9-bit) only for + /// `frameLengthType == 1`. The fixed payload length is + /// `(frameLength + 20) * 8` bits per §1.7.3.2.3. + pub frame_length: Option, +} + +impl LayerConfig { + /// §1.7.3.2.3: for `frameLengthType == 1` the fixed payload bit + /// length is `(frameLength + 20) * 8`. Returns `None` for every + /// other frame-length type (their length is carried in + /// `PayloadLengthInfo()` or is table-indexed). + pub fn fixed_payload_bits(&self) -> Option { + if self.frame_length_type == 1 { + self.frame_length + .map(|fl| (u32::from(fl) + 20).saturating_mul(8)) + } else { + None + } + } +} + +/// Decoded `StreamMuxConfig()` — ISO/IEC 14496-3 §1.7.3.1 Table 1.42. +/// +/// Carries the whole multiplex configuration: the version flags, the +/// time-framing mode, the per-program / per-layer [`LayerConfig`] +/// table, the `otherData` length, and the optional `crcCheckSum`. +#[derive(Debug, Clone)] +pub struct StreamMuxConfig { + /// `audioMuxVersion` (1 bit). + pub audio_mux_version: u8, + /// `audioMuxVersionA` (1 bit; `0` unless `audioMuxVersion == 1` + /// signalled it). A `1` here is the reserved `/* tbd */` branch, + /// rejected on parse. + pub audio_mux_version_a: u8, + /// `taraBufferFullness` — present only for `audioMuxVersion == 1`. + pub tara_buffer_fullness: Option, + /// `allStreamsSameTimeFraming` (1 bit). + pub all_streams_same_time_framing: bool, + /// `numSubFrames` (6 bits). `numSubFrames + 1` PayloadMux frames + /// are multiplexed. + pub num_sub_frames: u8, + /// `numProgram` (4 bits). `numProgram + 1` programs. + pub num_program: u8, + /// `numLayer[prog]` (3 bits) for each program — `num_layer[p] + 1` + /// layers in program `p`. + pub num_layer: Vec, + /// The flat per-stream layer table, in transmission order. + pub layers: Vec, + /// `otherDataPresent` (1 bit). + pub other_data_present: bool, + /// `otherDataLenBits` — the decoded length of the trailing + /// `otherData` field (in bits). `0` when `!otherDataPresent`. + pub other_data_len_bits: u32, + /// `crcCheckPresent` (1 bit). + pub crc_check_present: bool, + /// `crcCheckSum` (8 bits) when present. + pub crc_check_sum: Option, +} + +impl StreamMuxConfig { + /// `streamID[prog][lay]` lookup, mirroring the Table 1.42 + /// `streamID` assignment (`prog`-major, `lay`-minor flat counter). + pub fn stream_id(&self, prog: u8, lay: u8) -> Option { + self.layers + .iter() + .find(|l| l.prog == prog && l.lay == lay) + .map(|l| l.stream_id) + } + + /// The [`LayerConfig`] for a given flat `streamID`. + pub fn layer(&self, stream_id: u8) -> Option<&LayerConfig> { + self.layers.iter().find(|l| l.stream_id == stream_id) + } + + /// Parse a `StreamMuxConfig()` from `reader` (Table 1.42). + /// + /// `data` is the byte slice that backs `reader` (the same slice it + /// was constructed over); it is used only to re-read the config + /// prefix for CRC recomputation when `crcCheckPresent` is set. + /// + /// The reader is positioned at the `audioMuxVersion` bit and is + /// advanced to the bit after the configuration (the `crcCheckSum`, + /// or the last config bit when no CRC is present). The optional + /// `crcCheckSum` is recomputed against the configuration prefix and + /// validated; a mismatch is [`Error::LatmCrcMismatch`]. + pub fn parse(reader: &mut BitReader<'_>, data: &[u8]) -> Result { + let start_bit = reader.bit_position(); + + let audio_mux_version = read_u8(reader, 1)?; + let audio_mux_version_a = if audio_mux_version == 1 { + read_u8(reader, 1)? + } else { + 0 + }; + + if audio_mux_version_a != 0 { + // The Table 1.42 `else { /* tbd */ }` branch — no defined + // syntax. + return Err(Error::LatmAudioMuxVersionAReserved); + } + + let tara_buffer_fullness = if audio_mux_version == 1 { + Some(latm_get_value(reader)?) + } else { + None + }; + + let all_streams_same_time_framing = read_bit(reader)?; + let num_sub_frames = read_u8(reader, 6)?; + let num_program = read_u8(reader, 4)?; + if u32::from(num_program) > MAX_PROGRAM_INDEX { + return Err(Error::LatmConfigOutOfRange); + } + + let mut num_layer: Vec = Vec::with_capacity(usize::from(num_program) + 1); + let mut layers: Vec = Vec::new(); + // The "most recently transmitted" ASC, threaded across layers + // for `useSameConfig` resolution (§1.7.3.2.3). + let mut last_asc: Option = None; + let mut stream_cnt: u32 = 0; + + for prog in 0..=u32::from(num_program) { + let n_layer = read_u8(reader, 3)?; + if u32::from(n_layer) > MAX_LAYER_INDEX { + return Err(Error::LatmConfigOutOfRange); + } + num_layer.push(n_layer); + + for lay in 0..=u32::from(n_layer) { + if stream_cnt as usize >= MAX_STREAM_COUNT { + return Err(Error::LatmConfigOutOfRange); + } + let stream_id = stream_cnt as u8; + stream_cnt += 1; + + // useSameConfig — never present for the (0,0) layer. + let use_same_config = if prog == 0 && lay == 0 { + false + } else { + read_bit(reader)? + }; + + let asc = if use_same_config { + None + } else if audio_mux_version == 0 { + // audioMuxVersion == 0: the ASC has no explicit + // length prefix; it is parsed in place and its + // bit-length is implied by the ASC syntax. + let asc = AudioSpecificConfig::parse_bits(reader, start_bit)?; + Some(asc) + } else { + // audioMuxVersion == 1: `ascLen = LatmGetValue(); + // ascLen -= AudioSpecificConfig(); fillBits(ascLen)`. + // The ASC is length-prefixed, so we know the exact + // bit bound and can apply the §1.6.5 trailing + // implicit-SBR probe. + let asc_len = latm_get_value(reader)?; + let asc_start = reader.bit_position(); + let asc = AudioSpecificConfig::parse_bits_bounded( + reader, + asc_start, + u64::from(asc_len), + )?; + let consumed = reader.bit_position().saturating_sub(asc_start); + // fillBits = ascLen - (bits the ASC consumed). + let fill = u64::from(asc_len).saturating_sub(consumed); + if fill > 0 { + skip_bits(reader, fill)?; + } + Some(asc) + }; + + // Resolve useSameConfig into a concrete effective ASC. + let effective_asc = if let Some(a) = &asc { + last_asc = Some(a.clone()); + a.clone() + } else { + last_asc.clone().ok_or(Error::LatmNoPreviousMuxConfig)? + }; + + let frame_length_type = read_u8(reader, 3)?; + let mut latm_buffer_fullness = None; + let mut core_frame_offset = None; + let mut frame_length = None; + + match frame_length_type { + 0 => { + latm_buffer_fullness = Some(read_u8(reader, 8)?); + if !all_streams_same_time_framing { + // The CELP-core / AAC-enhancement pairing + // (§1.7.3.1 Table 1.42): AOT 6/20 (AAC SSR + // / ER AAC Scalable) layered above AOT 8/24 + // (CELP / ER CELP). + let this_aot = effective_asc.aot; + let prev_aot = layers.last().map(|l| l.effective_asc.aot); + let pairs = (this_aot == 6 || this_aot == 20) + && matches!(prev_aot, Some(8) | Some(24)); + if pairs { + core_frame_offset = Some(read_u8(reader, 6)?); + } + } + } + 1 => { + frame_length = Some(read_u16(reader, 9)?); + } + other => { + // `2` is reserved; `3`/`4`/`5` are CELP and + // `6`/`7` are HVXC, all table-indexed framing + // this AAC-focused decoder does not carry. + return Err(Error::LatmUnsupportedFrameLengthType(other)); + } + } + + layers.push(LayerConfig { + prog: prog as u8, + lay: lay as u8, + stream_id, + asc, + effective_asc, + frame_length_type, + latm_buffer_fullness, + core_frame_offset, + frame_length, + }); + } + } + + // otherDataPresent / otherDataLenBits. + let other_data_present = read_bit(reader)?; + let other_data_len_bits = if other_data_present { + if audio_mux_version == 1 { + latm_get_value(reader)? + } else { + // do { otherDataLenBits *= 256; esc; tmp(8); + // otherDataLenBits += tmp; } while (esc); + let mut acc: u32 = 0; + loop { + acc = acc.wrapping_mul(256); + let esc = read_bit(reader)?; + let tmp = read_u8(reader, 8)?; + acc = acc.wrapping_add(u32::from(tmp)); + if !esc { + break; + } + } + acc + } + } else { + 0 + }; + + // crcCheckPresent / crcCheckSum. The CRC covers the whole + // StreamMuxConfig() from `audioMuxVersion` up to but excluding + // crcCheckPresent — capture that prefix before reading the + // flag. + let crc_end_bit = reader.bit_position(); + let crc_check_present = read_bit(reader)?; + let crc_check_sum = if crc_check_present { + let sum = read_u8(reader, 8)?; + // Recompute over the config prefix and validate. + let prefix = read_back_bits(data, start_bit, crc_end_bit)?; + let expected = crc::stream_mux_config_crc(&prefix); + if expected != sum { + return Err(Error::LatmCrcMismatch); + } + Some(sum) + } else { + None + }; + + Ok(StreamMuxConfig { + audio_mux_version, + audio_mux_version_a, + tara_buffer_fullness, + all_streams_same_time_framing, + num_sub_frames, + num_program, + num_layer, + layers, + other_data_present, + other_data_len_bits, + crc_check_present, + crc_check_sum, + }) + } +} + +/// §1.7.3 signalling cap: `numChunk` is 4-bit (max chunk index 15). +const MAX_NUM_CHUNK_INDEX: u32 = 15; + +/// One recovered MPEG-4 Audio payload from a [`PayloadMux`] — the raw +/// access-unit bytes for a single `(subframe, prog, lay)` slot. For an +/// AAC layer these bytes are the §4.4.2.1 `raw_data_block()` that the +/// [`crate::decode::StreamDecoder`] / [`crate::raw_data_block`] layer +/// consumes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MuxPayload { + /// Subframe index (`0 ..= numSubFrames`). + pub sub_frame: u8, + /// `prog` — the program this payload belongs to. + pub prog: u8, + /// `lay` — the layer within the program. + pub lay: u8, + /// `streamID[prog][lay]`. + pub stream_id: u8, + /// The raw payload bytes (one complete access unit for + /// `frameLengthType == 0`). + pub data: Vec, +} + +/// `MuxSlotLengthBytes[streamID]` decoded for one payload slot of a +/// [`PayloadLengthInfo`] (Table 1.44). For `frameLengthType == 0` this +/// is the running 8-bit-escape byte count; the bit length for +/// `frameLengthType == 1` comes from the layer's fixed `frameLength`. +#[derive(Debug, Clone, Copy)] +struct SlotLength { + prog: u8, + lay: u8, + stream_id: u8, + /// Payload length in **bits**. For type-0 this is `bytes * 8`; for + /// type-1 it is `(frameLength + 20) * 8`. + bits: u32, +} + +/// Decoded `AudioMuxElement()` — ISO/IEC 14496-3 §1.7.3.1 Table 1.41. +/// +/// Holds the (possibly inherited) [`StreamMuxConfig`] and the recovered +/// per-subframe payloads. Parsing supports `audioMuxVersionA == 0` +/// (the only defined branch) and `allStreamsSameTimeFraming` in both +/// states; non-same-time-framing uses the `numChunk` chunk layout of +/// Tables 1.44 / 1.45. +#[derive(Debug, Clone)] +pub struct AudioMuxElement { + /// `useSameStreamMux` (only present when `muxConfigPresent`). When + /// `true`, [`AudioMuxElement::config`] was inherited from the + /// previous element rather than parsed here. + pub use_same_stream_mux: bool, + /// The active multiplex configuration for this element. + pub config: StreamMuxConfig, + /// The recovered payloads in transmission order. + pub payloads: Vec, +} + +impl AudioMuxElement { + /// Parse an `AudioMuxElement()` (Table 1.41) from `reader`. + /// + /// `data` is the byte slice backing `reader` (forwarded to + /// [`StreamMuxConfig::parse`] for CRC recomputation). + /// `mux_config_present` is the `muxConfigPresent` flag the calling + /// layer supplies (LOAS [`AudioSyncStream`] passes `1`; an + /// out-of-band-configured transport passes `0`). `prev_config` is + /// the configuration decoded on the previous element, used when + /// `useSameStreamMux` is set or when `muxConfigPresent == 0`. + pub fn parse( + reader: &mut BitReader<'_>, + data: &[u8], + mux_config_present: bool, + prev_config: Option<&StreamMuxConfig>, + ) -> Result { + let (use_same_stream_mux, config) = if mux_config_present { + let use_same = read_bit(reader)?; + if use_same { + let cfg = prev_config.cloned().ok_or(Error::LatmNoPreviousMuxConfig)?; + (true, cfg) + } else { + (false, StreamMuxConfig::parse(reader, data)?) + } + } else { + // Out-of-band StreamMuxConfig(): apply the previous one. + let cfg = prev_config.cloned().ok_or(Error::LatmNoPreviousMuxConfig)?; + (false, cfg) + }; + + if config.audio_mux_version_a != 0 { + return Err(Error::LatmAudioMuxVersionAReserved); + } + + let mut payloads = Vec::new(); + for sub_frame in 0..=u32::from(config.num_sub_frames) { + let slots = payload_length_info(reader, &config)?; + payload_mux(reader, &config, sub_frame as u8, &slots, &mut payloads)?; + } + + // otherData: skip otherDataLenBits bits. + if config.other_data_present { + skip_bits(reader, u64::from(config.other_data_len_bits))?; + } + + // ByteAlign(). + reader.align_to_byte(); + + Ok(AudioMuxElement { + use_same_stream_mux, + config, + payloads, + }) + } +} + +/// `PayloadLengthInfo()` — §1.7.3.1 Table 1.44. Returns the decoded +/// per-slot payload bit-lengths in the order `PayloadMux()` will emit +/// them. +fn payload_length_info( + reader: &mut BitReader<'_>, + config: &StreamMuxConfig, +) -> Result> { + let mut slots = Vec::new(); + if config.all_streams_same_time_framing { + for prog in 0..=u32::from(config.num_program) { + let n_layer = config.num_layer[prog as usize]; + for lay in 0..=u32::from(n_layer) { + let stream_id = config + .stream_id(prog as u8, lay as u8) + .ok_or(Error::LatmConfigOutOfRange)?; + let layer = config.layer(stream_id).ok_or(Error::LatmConfigOutOfRange)?; + let bits = slot_bits(reader, layer)?; + slots.push(SlotLength { + prog: prog as u8, + lay: lay as u8, + stream_id, + bits, + }); + } + } + } else { + let num_chunk = read_u8(reader, 4)?; + if u32::from(num_chunk) > MAX_NUM_CHUNK_INDEX { + return Err(Error::LatmConfigOutOfRange); + } + for _ in 0..=u32::from(num_chunk) { + let stream_indx = read_u8(reader, 4)?; + let layer = config + .layer(stream_indx) + .ok_or(Error::LatmConfigOutOfRange)?; + let prog = layer.prog; + let lay = layer.lay; + let stream_id = layer.stream_id; + let frame_length_type = layer.frame_length_type; + let bits = slot_bits(reader, layer)?; + // For frameLengthType == 0 in the chunk layout the spec + // appends an AuEndFlag bit after MuxSlotLengthBytes. + if frame_length_type == 0 { + let _au_end_flag = read_bit(reader)?; + } + slots.push(SlotLength { + prog, + lay, + stream_id, + bits, + }); + } + } + Ok(slots) +} + +/// Decode the payload bit-length for one slot per its +/// `frameLengthType` (Table 1.44 inner body): the 8-bit-escape running +/// `MuxSlotLengthBytes` for type 0, or the fixed `(frameLength+20)*8` +/// for type 1. CELP/HVXC `MuxSlotLengthCoded` table indices are out of +/// scope and were already rejected when the config was parsed. +fn slot_bits(reader: &mut BitReader<'_>, layer: &LayerConfig) -> Result { + match layer.frame_length_type { + 0 => { + let mut bytes: u32 = 0; + loop { + let tmp = read_u8(reader, 8)?; + bytes = bytes.wrapping_add(u32::from(tmp)); + if tmp != 255 { + break; + } + } + Ok(bytes.saturating_mul(8)) + } + 1 => layer + .fixed_payload_bits() + .ok_or(Error::LatmConfigOutOfRange), + other => Err(Error::LatmUnsupportedFrameLengthType(other)), + } +} + +/// `PayloadMux()` — §1.7.3.1 Table 1.45. Reads each slot's payload +/// bytes in the same order `PayloadLengthInfo()` emitted them, pushing +/// one [`MuxPayload`] per slot. Payloads are byte-extracted; the spec +/// guarantees `frameLengthType == 0` payloads are an integer number of +/// bytes, and `AudioMuxElement()` byte-aligns the reader at each +/// subframe boundary in the common AAC case. +fn payload_mux( + reader: &mut BitReader<'_>, + config: &StreamMuxConfig, + sub_frame: u8, + slots: &[SlotLength], + out: &mut Vec, +) -> Result<()> { + // Walk in the order PayloadLengthInfo built the slots, which is the + // same program/layer (or chunk) order PayloadMux uses. + let _ = config; + for slot in slots { + let data = read_payload_bytes(reader, slot.bits)?; + out.push(MuxPayload { + sub_frame, + prog: slot.prog, + lay: slot.lay, + stream_id: slot.stream_id, + data, + }); + } + Ok(()) +} + +/// Read `bits` bits of payload as a byte vector. The common AAC case +/// (`frameLengthType == 0`, byte-aligned reader) is a fast `read_bytes` +/// path; a non-byte-multiple length or non-aligned reader falls back to +/// bit-by-bit assembly (MSB-first), with the trailing partial byte +/// left-justified. +fn read_payload_bytes(reader: &mut BitReader<'_>, bits: u32) -> Result> { + if bits % 8 == 0 && reader.is_byte_aligned() { + let n = (bits / 8) as usize; + return reader.read_bytes(n).map_err(|_| Error::UnexpectedEnd); + } + let full = bits / 8; + let rem = bits % 8; + let mut out = Vec::with_capacity((full + u32::from(rem != 0)) as usize); + for _ in 0..full { + out.push(read_u8(reader, 8)?); + } + if rem > 0 { + let v = read_u8(reader, rem)?; + out.push(v << (8 - rem)); + } + Ok(out) +} + +/// §1.7.3.1 Table 1.43 `LatmGetValue()`: a variable-length unsigned +/// integer carried as `bytesForValue` (2 bits) followed by +/// `bytesForValue + 1` bytes, big-endian. +pub fn latm_get_value(reader: &mut BitReader<'_>) -> Result { + let bytes_for_value = read_u8(reader, 2)?; + let mut value: u32 = 0; + for _ in 0..=u32::from(bytes_for_value) { + value = value.wrapping_mul(256); + let byte = read_u8(reader, 8)?; + value = value.wrapping_add(u32::from(byte)); + } + Ok(value) +} + +/// One decoded LOAS sync frame — ISO/IEC 14496-3 §1.7.2.1. +/// +/// Carries the framed `audioMuxLengthBytes` length, the recovered +/// [`AudioMuxElement`], and (for `EPAudioSyncStream`) the FEC header +/// fields. The byte offset of the frame within the LOAS buffer is also +/// recorded so callers can resume the sync search. +#[derive(Debug, Clone)] +pub struct LoasFrame { + /// `audioMuxLengthBytes` (13 bits) — the byte length of the framed + /// multiplexed element. + pub audio_mux_length_bytes: u16, + /// The recovered multiplexed element. + pub element: AudioMuxElement, + /// `frameCounter` (5 bits) — present only for `EPAudioSyncStream`. + pub frame_counter: Option, + /// Byte offset of the syncword within the LOAS buffer. + pub offset: usize, + /// Byte offset of the first byte after this sync frame. + pub next_offset: usize, +} + +/// LOAS `AudioSyncStream()` walker — ISO/IEC 14496-3 §1.7.2.1 +/// Table 1.36. +/// +/// Scans `data` for the 11-bit `0x2B7` syncword, then for each frame +/// reads the 13-bit `audioMuxLengthBytes` and decodes the byte-aligned +/// `AudioMuxElement(1)` over the next `audioMuxLengthBytes` bytes. The +/// syncword is searched on byte boundaries (AudioSyncStream frames are +/// byte-aligned per §1.7.2.2.1). +#[derive(Debug)] +pub struct AudioSyncStream<'a> { + data: &'a [u8], + pos: usize, + /// The most recently decoded [`StreamMuxConfig`], threaded across + /// frames for `useSameStreamMux` inheritance. + prev_config: Option, +} + +impl<'a> AudioSyncStream<'a> { + /// Create a walker over a LOAS `AudioSyncStream()` byte buffer. + pub fn new(data: &'a [u8]) -> Self { + AudioSyncStream { + data, + pos: 0, + prev_config: None, + } + } + + /// Decode the next `AudioSyncStream()` sync frame, advancing past + /// it. Returns `Ok(None)` at end of stream (no further syncword). + /// + /// On a successful decode the frame's [`StreamMuxConfig`] is + /// retained so a subsequent frame carrying `useSameStreamMux` can + /// inherit it. + pub fn next_frame(&mut self) -> Result> { + let Some(sync_off) = self.find_syncword(AUDIO_SYNC_STREAM_SYNCWORD, 11) else { + self.pos = self.data.len(); + return Ok(None); + }; + + // Read audioMuxLengthBytes (13 bits) starting after the 11-bit + // syncword. + let mut reader = BitReader::new(&self.data[sync_off..]); + reader.skip(11).map_err(|_| Error::LoasSyncInvalid)?; + let audio_mux_length_bytes = + reader.read_u32(13).map_err(|_| Error::LoasSyncInvalid)? as u16; + + // The AudioMuxElement(1) follows; it is byte-aligned because + // 11 + 13 = 24 bits = 3 whole bytes. + debug_assert_eq!(reader.bit_position(), 24); + let element_byte_start = sync_off + 3; + let element_byte_end = element_byte_start + usize::from(audio_mux_length_bytes); + if element_byte_end > self.data.len() { + return Err(Error::LoasSyncInvalid); + } + let element_bytes = &self.data[element_byte_start..element_byte_end]; + let mut elem_reader = BitReader::new(element_bytes); + let element = AudioMuxElement::parse( + &mut elem_reader, + element_bytes, + true, + self.prev_config.as_ref(), + )?; + + self.prev_config = Some(element.config.clone()); + self.pos = element_byte_end; + + Ok(Some(LoasFrame { + audio_mux_length_bytes, + element, + frame_counter: None, + offset: sync_off, + next_offset: element_byte_end, + })) + } + + /// Search for an `n`-bit syncword on byte boundaries from the + /// current position. Returns the byte offset of the syncword's + /// first byte, or `None` if not found before end of buffer. The + /// 11-bit `0x2B7` and 16-bit `0x4DE1` syncwords both begin on a + /// byte boundary in their respective frame layouts. + fn find_syncword(&self, syncword: u32, n: u32) -> Option { + let bytes_needed = n.div_ceil(8) as usize; + let mut off = self.pos; + while off + bytes_needed <= self.data.len() { + let mut r = BitReader::new(&self.data[off..]); + if let Ok(v) = r.read_u32(n) { + if v == syncword { + return Some(off); + } + } + off += 1; + } + None + } +} + +impl Iterator for AudioSyncStream<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + match self.next_frame() { + Ok(Some(frame)) => Some(Ok(frame)), + Ok(None) => None, + Err(e) => { + // Stop iterating after surfacing the error. + self.pos = self.data.len(); + Some(Err(e)) + } + } + } +} + +/// Decoded `EPAudioSyncStream()` FEC header — ISO/IEC 14496-3 §1.7.2.1 +/// Table 1.37. +/// +/// Parses the 16-bit `0x4DE1` syncword, the 4-bit `futureUse`, the +/// 13-bit `audioMuxLengthBytes`, the 5-bit `frameCounter`, and the +/// 18-bit `headerParity`. The body is an `EPMuxElement(1, 1)` whose +/// EP-tool de-interleave is out of scope; this struct captures the +/// header so callers can frame the stream and recover the (byte-aligned) +/// element body bounds. +#[derive(Debug, Clone)] +pub struct EpAudioSyncHeader { + /// `futureUse` (4 bits). + pub future_use: u8, + /// `audioMuxLengthBytes` (13 bits). + pub audio_mux_length_bytes: u16, + /// `frameCounter` (5 bits). + pub frame_counter: u8, + /// `headerParity` (18 bits). + pub header_parity: u32, + /// Byte offset of the syncword. + pub offset: usize, + /// Byte offset of the first byte of the `EPMuxElement(1, 1)` body + /// (the header is `16 + 4 + 13 + 5 + 18 = 56` bits = 7 bytes, so the + /// body is byte-aligned). + pub body_offset: usize, +} + +impl EpAudioSyncHeader { + /// Parse one `EPAudioSyncStream()` FEC header from `data` starting + /// at `pos`, scanning for the `0x4DE1` syncword on byte boundaries. + /// Returns `Ok(None)` if no syncword is found. + pub fn parse(data: &[u8], pos: usize) -> Result> { + let walker = AudioSyncStream { + data, + pos, + prev_config: None, + }; + let Some(sync_off) = walker.find_syncword(EP_AUDIO_SYNC_STREAM_SYNCWORD, 16) else { + return Ok(None); + }; + let mut reader = BitReader::new(&data[sync_off..]); + reader.skip(16).map_err(|_| Error::LoasSyncInvalid)?; // syncword + let future_use = read_u8(&mut reader, 4)?; + let audio_mux_length_bytes = read_u16(&mut reader, 13)?; + let frame_counter = read_u8(&mut reader, 5)?; + let header_parity = reader.read_u32(18).map_err(|_| Error::UnexpectedEnd)?; + debug_assert_eq!(reader.bit_position(), 56); + Ok(Some(EpAudioSyncHeader { + future_use, + audio_mux_length_bytes, + frame_counter, + header_parity, + offset: sync_off, + body_offset: sync_off + 7, + })) + } +} + +/// Generator polynomial of the `EPAudioSyncStream()` `headerParity` +/// BCH(36,18) code (§1.7.2.2.2): +/// x¹⁸+x¹⁷+x¹⁶+x¹⁵+x⁹+x⁷+x⁶+x³+x²+x+1, stored without the leading +/// x¹⁸ term. +const EP_SYNC_BCH_GEN: u32 = (1 << 17) + | (1 << 16) + | (1 << 15) + | (1 << 9) + | (1 << 7) + | (1 << 6) + | (1 << 3) + | (1 << 2) + | (1 << 1) + | 1; + +/// Compute the §1.7.2.2.2 `headerParity` — the 18 parity bits of the +/// shortened BCH(36,18) over `audioMuxLengthBytes` (13 bits) followed +/// by `frameCounter` (5 bits), `R(x)` of `M(x)·x¹⁸ mod G(x)` per +/// §1.8.4.3. +pub fn ep_sync_header_parity(audio_mux_length_bytes: u16, frame_counter: u8) -> u32 { + let msg: u32 = + (u32::from(audio_mux_length_bytes & 0x1FFF) << 5) | u32::from(frame_counter & 0x1F); + let mut reg: u32 = 0; + let top = 1u32 << 17; + let feed = |reg: &mut u32, bit: bool| { + let high = *reg & top != 0; + *reg = (*reg << 1) & 0x3FFFF; + if high { + *reg ^= EP_SYNC_BCH_GEN; + } + if bit { + *reg ^= 1; + } + }; + for i in (0..18).rev() { + feed(&mut reg, msg & (1 << i) != 0); + } + for _ in 0..18 { + let high = reg & top != 0; + reg = (reg << 1) & 0x3FFFF; + if high { + reg ^= EP_SYNC_BCH_GEN; + } + } + reg +} + +impl EpAudioSyncHeader { + /// Verify the §1.7.2.2.2 BCH(36,18) `headerParity` against the + /// received `audioMuxLengthBytes` / `frameCounter`. + pub fn parity_ok(&self) -> bool { + ep_sync_header_parity(self.audio_mux_length_bytes, self.frame_counter) == self.header_parity + } +} + +/// Threaded cross-frame state of an `EPMuxElement()` stream: the +/// active EP-tool configuration and the previous `StreamMuxConfig`. +#[derive(Debug, Default)] +pub struct EpMuxState { + /// The active `ErrorProtectionSpecificConfig()` (threaded across + /// `epUsePreviousMuxConfig == 1` elements). + pub ep_config: Option, + /// The previous `StreamMuxConfig` for `useSameStreamMux`. + pub prev_config: Option, +} + +/// A decoded `EPMuxElement(1, 1)` (§1.7.3.1 Table 1.40): the EP-tool +/// configuration in force plus the recovered (error-corrected) +/// `AudioMuxElement()`. +#[derive(Debug)] +pub struct EpMuxElement { + /// `epUsePreviousMuxConfig` (majority-decoded). + pub use_previous_mux_config: bool, + /// The recovered inner `AudioMuxElement()`. + pub element: AudioMuxElement, +} + +impl EpMuxElement { + /// Parse an `EPMuxElement(epDataPresent = 1, muxConfigPresent = 1)` + /// from `data` (the whole element, byte-aligned), threading + /// `state` across elements. + /// + /// Layout per Table 1.40: `epUsePreviousMuxConfig` + its 2-bit + /// repetition parity (majority decides, §1.7.3.2.1); when clear, + /// the 10-bit `epSpecificConfigLength` protected by the Table 1.59 + /// Golay(23,12) 11-bit parity, then + /// `ErrorProtectionSpecificConfig()` + its Table 1.59 parity; + /// `ByteAlign()`; then `EPAudioMuxElement(1)` — the EP-tool + /// `ep_frame()` whose decoded class concatenation is the plain + /// `AudioMuxElement(1)` bit stream (the §1.7.3.2.1 sensitivity + /// category instances ride in syntax order). + pub fn parse(data: &[u8], state: &mut EpMuxState) -> Result { + let mut reader = BitReader::new(data); + // epUsePreviousMuxConfig + 2-bit repetition parity. + let b0 = read_bit(&mut reader)?; + let b1 = read_bit(&mut reader)?; + let b2 = read_bit(&mut reader)?; + let use_prev = (u8::from(b0) + u8::from(b1) + u8::from(b2)) >= 2; + if !use_prev { + // epSpecificConfigLength (10) + Golay parity (11). + let mut len_bits_field = [false; 10]; + for b in len_bits_field.iter_mut() { + *b = read_bit(&mut reader)?; + } + let mut parity = [false; 11]; + for b in parity.iter_mut() { + *b = read_bit(&mut reader)?; + } + let corrected = crate::ep_fec::header_fec_decode(&len_bits_field, &parity)?; + let mut cfg_len = 0usize; + for &b in &corrected { + cfg_len = (cfg_len << 1) | usize::from(b); + } + // ErrorProtectionSpecificConfig() (self-terminating) + + // Table 1.59 parity over its bits. + let cfg_start = reader.bit_position(); + let epsc = crate::ep_config::ErrorProtectionSpecificConfig::parse(&mut reader)?; + let consumed = (reader.bit_position() - cfg_start) as usize; + // `epSpecificConfigLength` indicates the size of the + // config; validate in bits (with a byte-unit fallback — + // the staged text does not name the unit). + if cfg_len != consumed && cfg_len != consumed.div_ceil(8) { + return Err(Error::EpFrameInvalid); + } + let cfg_bits = read_back_bits(data, cfg_start, cfg_start + consumed as u64)?; + let parity_len = crate::ep_fec::HeaderFec::for_len(consumed)?.parity_bits(consumed)?; + let mut cfg_parity = Vec::with_capacity(parity_len); + for _ in 0..parity_len { + cfg_parity.push(read_bit(&mut reader)?); + } + let corrected_cfg = crate::ep_fec::header_fec_decode(&cfg_bits, &cfg_parity)?; + if corrected_cfg != cfg_bits { + // The FEC corrected config bits: re-parse from the + // corrected sequence. + let mut bytes = vec![0u8; corrected_cfg.len().div_ceil(8)]; + for (i, &b) in corrected_cfg.iter().enumerate() { + if b { + bytes[i / 8] |= 0x80 >> (i % 8); + } + } + let mut r2 = BitReader::new(&bytes); + state.ep_config = Some(crate::ep_config::ErrorProtectionSpecificConfig::parse( + &mut r2, + )?); + } else { + state.ep_config = Some(epsc); + } + } + // ByteAlign(). + reader.align_to_byte(); + let epsc = state.ep_config.clone().ok_or(Error::EpFrameInvalid)?; + let codec = crate::ep_frame::EpFrameCodec::new(epsc)?; + let body = crate::ep_frame::read_remaining_bytes(&mut reader, data.len())?; + let frame = codec.decode(&body)?; + // The class concatenation is the AudioMuxElement(1) bits. + let mut au_bits: Vec = Vec::new(); + for c in &frame.classes { + au_bits.extend_from_slice(c); + } + let mut au_bytes = vec![0u8; au_bits.len().div_ceil(8)]; + for (i, &b) in au_bits.iter().enumerate() { + if b { + au_bytes[i / 8] |= 0x80 >> (i % 8); + } + } + let mut au_reader = BitReader::new(&au_bytes); + let element = + AudioMuxElement::parse(&mut au_reader, &au_bytes, true, state.prev_config.as_ref())?; + state.prev_config = Some(element.config.clone()); + Ok(EpMuxElement { + use_previous_mux_config: use_prev, + element, + }) + } +} + +// ---- bit helpers ----------------------------------------------------- + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +fn read_u16(reader: &mut BitReader<'_>, n: u32) -> Result { + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u16) +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} + +fn skip_bits(reader: &mut BitReader<'_>, n: u64) -> Result<()> { + // BitReader::skip takes a u32; chunk for safety on large fill runs. + let mut remaining = n; + while remaining > 0 { + let chunk = remaining.min(u64::from(u32::MAX)) as u32; + reader.skip(chunk).map_err(|_| Error::UnexpectedEnd)?; + remaining -= u64::from(chunk); + } + Ok(()) +} + +/// Re-read the bits of an already-consumed `[from_bit, to_bit)` range +/// of `data` as a `Vec` in MSB-first transmission order, for CRC +/// recomputation. A fresh reader is created over the backing buffer so +/// the original reader's position is untouched. +fn read_back_bits(data: &[u8], from_bit: u64, to_bit: u64) -> Result> { + debug_assert!(to_bit >= from_bit); + let count = (to_bit - from_bit) as usize; + let mut scratch = BitReader::new(data); + skip_bits(&mut scratch, from_bit)?; + let mut out = Vec::with_capacity(count); + for _ in 0..count { + out.push(scratch.read_bit().map_err(|_| Error::UnexpectedEnd)?); + } + Ok(out) +} + +// ---- LOAS → PCM decode driver ---------------------------------------- + +use std::collections::HashMap; + +use crate::decode::{DecodedFrame, StreamDecoder}; + +/// Whole-stream LATM/LOAS → PCM decoder. +/// +/// Walks a LOAS `AudioSyncStream()` byte buffer ([`AudioSyncStream`]), +/// and for every recovered access unit ([`MuxPayload`]) drives the +/// payload's §4.4.2.1 `raw_data_block()` through the +/// [`crate::decode::StreamDecoder`] core +/// ([`StreamDecoder::decode_raw_data_block`]) using the configuration the +/// LATM `StreamMuxConfig` carried in the layer's +/// [`AudioSpecificConfig`]. +/// +/// The LATM multiplex can carry several streams (`streamID[prog][lay]`); +/// each is given its own [`StreamDecoder`] so the per-stream filterbank +/// overlap-add tail, LTP history, and predictor state thread across the +/// frames of that stream independently. For the common single-program / +/// single-layer AAC case there is exactly one stream. +/// +/// ## Scope +/// +/// Targets the core (AAC-LC / Main / LTP) tool chain the +/// [`StreamDecoder`] covers, **plus §4.6.18 SBR (HE-AAC v1)** — the +/// shared `decode_raw_data_block` core auto-detects the `EXT_SBR_DATA` +/// FIL payloads in-band and doubles the output rate (or keeps the core +/// rate in the §4.6.18.4.3 downsampled mode), and a PS payload renders +/// stereo through the subpart-8 tool (HE-AAC v2). The +/// `audioObjectType` carried by the ASC must be a General Audio +/// type whose `raw_data_block()` the core driver understands; otherwise +/// the underlying decode surfaces its own element-level error. +#[derive(Debug, Default)] +pub struct LoasDecoder { + /// One [`StreamDecoder`] per `streamID`, so each multiplexed stream's + /// inter-frame state stays independent. + streams: HashMap, + /// One §4.5.2.2 [`crate::scalable::ScalableDecoder`] per *program* + /// for the scalable object types (AOTs 6 / 20), whose layers ride + /// separate `streamID`s but decode to one combined output. + scalable: HashMap, + /// Per-program buffer collecting the current subframe's scalable + /// layer payloads (in layer order) until the stack is complete. + scalable_pending: HashMap>>, + /// Caller-forced §4.6.18.4.3 downsampled SBR output (see + /// [`Self::set_sbr_downsampled`]); an explicitly signalled ASC + /// whose extension sampling frequency equals the core rate selects + /// the mode per stream regardless. + sbr_downsampled: bool, + /// Caller-forced §4.6.18.8 low-power SBR mode (see + /// [`Self::set_sbr_low_power`]). + sbr_low_power: bool, +} + +impl LoasDecoder { + /// A fresh LOAS decoder with no per-stream state. + #[must_use] + pub fn new() -> Self { + LoasDecoder::default() + } + + /// Force the §4.6.18.4.3 downsampled SBR output mode on every + /// stream decoder this LOAS driver creates: SBR-active streams are + /// emitted at the core sampling rate. Independent of the forced + /// mode, a layer whose explicitly signalled `AudioSpecificConfig` + /// carries `extensionSamplingFrequency == samplingFrequency` + /// selects the mode by itself (the SBR output rate the ASC + /// declares *is* the core rate). Select before decoding. + pub fn set_sbr_downsampled(&mut self, downsampled: bool) { + self.sbr_downsampled = downsampled; + } + + /// Force the §4.6.18.8 low-power SBR mode on every stream decoder + /// this LOAS driver creates (real-valued filterbanks + the LP + /// adjustment chain; PS streams are rejected in this mode). Select + /// before decoding. + pub fn set_sbr_low_power(&mut self, low_power: bool) { + self.sbr_low_power = low_power; + } + + /// Decode a whole LOAS `AudioSyncStream()` byte buffer to a vector of + /// per-access-unit interleaved PCM frames, in transmission order. + /// + /// Each [`LoasFrame`]'s `AudioMuxElement` may carry several + /// subframes / payloads; every payload is decoded and pushed in the + /// order [`AudioMuxElement::payloads`] presents them. A frame that + /// yields no channel element (fill-only) still contributes its + /// (empty) [`DecodedFrame`]. + pub fn decode_all(&mut self, data: &[u8]) -> Result> { + let mut out = Vec::new(); + let mut walker = AudioSyncStream::new(data); + while let Some(frame) = walker.next_frame()? { + for payload in &frame.element.payloads { + // A scalable (AOT 6 / 20) layer joins its program's + // pending stack; the stack decodes as one combined + // access unit when the last layer arrives (§4.5.2.2: + // one elementary stream per layer, one output). + let config = &frame.element.config; + let layer = config + .layer(payload.stream_id) + .ok_or(Error::LatmConfigOutOfRange)?; + if layer.effective_asc.aot == 6 || layer.effective_asc.aot == 20 { + if let Some(decoded) = self.push_scalable_payload(config, payload)? { + out.push(decoded); + } + continue; + } + let decoded = self.decode_payload(config, payload)?; + out.push(decoded); + } + } + Ok(out) + } + + /// Feed one scalable-program layer payload; returns the combined + /// [`DecodedFrame`] when the payload completes the program's layer + /// stack for the current access unit, `None` while the stack is + /// still filling. + /// + /// Layers must arrive in layer order within each access unit + /// (which is how `AudioMuxElement()` multiplexes them under + /// `allStreamsSameTimeFraming`); an out-of-order layer surfaces + /// [`Error::ScalableInvalid`]. + pub fn push_scalable_payload( + &mut self, + config: &StreamMuxConfig, + payload: &MuxPayload, + ) -> Result> { + let layer = config + .layer(payload.stream_id) + .ok_or(Error::LatmConfigOutOfRange)?; + let prog = layer.prog; + let n_layers = usize::from( + *config + .num_layer + .get(usize::from(prog)) + .ok_or(Error::LatmConfigOutOfRange)?, + ) + 1; + let pending = self.scalable_pending.entry(prog).or_default(); + if usize::from(layer.lay) != pending.len() { + self.scalable_pending.remove(&prog); + return Err(Error::ScalableInvalid); + } + pending.push(payload.data.clone()); + if pending.len() < n_layers { + return Ok(None); + } + let payloads = self.scalable_pending.remove(&prog).unwrap_or_default(); + + // Resolve the program's ScalableConfig from the layer ASCs. + let mut ascs: Vec<&crate::asc::AudioSpecificConfig> = Vec::with_capacity(n_layers); + for lay in 0..n_layers { + let sid = config + .stream_id(prog, lay as u8) + .ok_or(Error::LatmConfigOutOfRange)?; + let lc = config.layer(sid).ok_or(Error::LatmConfigOutOfRange)?; + ascs.push(&lc.effective_asc); + } + let cfg = crate::scalable::ScalableConfig::from_layer_ascs(&ascs)?; + // Reuse the persistent decoder while the configuration holds; + // a mid-stream StreamMuxConfig change rebuilds it (the + // overlap/LTP state is geometry-shaped). + let rebuild = !matches!(self.scalable.get(&prog), Some(d) if d.config() == &cfg); + if rebuild { + self.scalable + .insert(prog, crate::scalable::ScalableDecoder::new(cfg)?); + } + let dec = self.scalable.get_mut(&prog).expect("just inserted"); + let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect(); + dec.decode_frame(&refs).map(Some) + } + + /// Decode a whole `EPAudioSyncStream()` byte buffer (§1.7.2.1 + /// Table 1.37) to per-access-unit PCM frames: every `0x4DE1` sync + /// frame's BCH(36,18)-verified header is walked, its + /// `EPMuxElement(1, 1)` is EP-decoded ([`EpMuxElement::parse`] — + /// FEC-corrected, CRC-checked, de-interleaved) and the recovered + /// `AudioMuxElement()` payloads decode exactly as on the plain + /// LOAS path (scalable programs included). + pub fn decode_all_ep(&mut self, data: &[u8]) -> Result> { + let mut out = Vec::new(); + let mut ep_state = EpMuxState::default(); + let mut pos = 0usize; + while let Some(header) = EpAudioSyncHeader::parse(data, pos)? { + if !header.parity_ok() { + return Err(Error::EpFrameInvalid); + } + let body_end = header + .body_offset + .checked_add(usize::from(header.audio_mux_length_bytes)) + .ok_or(Error::UnexpectedEnd)?; + if body_end > data.len() { + return Err(Error::UnexpectedEnd); + } + let mux = EpMuxElement::parse(&data[header.body_offset..body_end], &mut ep_state)?; + for payload in &mux.element.payloads { + let config = &mux.element.config; + let layer = config + .layer(payload.stream_id) + .ok_or(Error::LatmConfigOutOfRange)?; + if layer.effective_asc.aot == 6 || layer.effective_asc.aot == 20 { + if let Some(decoded) = self.push_scalable_payload(config, payload)? { + out.push(decoded); + } + continue; + } + out.push(self.decode_payload(config, payload)?); + } + pos = body_end; + } + Ok(out) + } + + /// Decode one recovered [`MuxPayload`] to PCM, routing it to the + /// per-`streamID` [`StreamDecoder`] and configuring the decode from + /// the payload's layer [`AudioSpecificConfig`]. + pub fn decode_payload( + &mut self, + config: &StreamMuxConfig, + payload: &MuxPayload, + ) -> Result { + let layer = config + .layer(payload.stream_id) + .ok_or(Error::LatmConfigOutOfRange)?; + let asc = &layer.effective_asc; + // The scalable object types decode per *program*, not per + // stream: route through the layer-stack collector. While a + // multi-layer stack is still filling, an empty frame (0 + // channels) is returned — [`Self::decode_all`] instead calls + // [`Self::push_scalable_payload`] directly and skips these. + if asc.aot == 6 || asc.aot == 20 { + let sample_rate = asc.sample_rate; + return Ok(self + .push_scalable_payload(config, payload)? + .unwrap_or(DecodedFrame { + pcm: Vec::new(), + channels: 0, + sample_rate, + })); + } + // An SBR-signalling ASC (explicit AOT 5 wrapper or the implicit + // trailing probe) needs no pre-rejection: the shared + // `decode_raw_data_block` core auto-detects the `EXT_SBR_DATA` + // FIL payloads in-band and doubles the output rate (§4.6.18). + // The decode runs at the *core* configuration (`asc.aot` is the + // unwrapped core object type, `asc.sample_rate` the core rate); + // a PS payload renders stereo through the subpart-8 tool. + // §4.5.1.1 — resolve the frame-length family from the layer's + // ASC (`frameLengthFlag` semantics depend on the AOT: 1024/960 + // lines for the general GA types, 512/480 for ER AAC LD). + let family = crate::swb_offset::FrameFamily::from_aot_and_flag( + asc.aot, + asc.ga_body.frame_length == crate::asc::FrameLength::Long960, + ); + let dec = self.streams.entry(payload.stream_id).or_insert_with({ + let force_down = self.sbr_downsampled; + let force_lp = self.sbr_low_power; + move || { + let mut d = StreamDecoder::new(); + d.set_sbr_downsampled(force_down); + d.set_sbr_low_power(force_lp); + d.set_frame_family(family); + d + } + }); + // A mid-stream StreamMuxConfig replacement can change the + // layer's frame family; the per-element overlap/LTP state is + // family-shaped, so a mismatched decoder is rebuilt from + // scratch rather than fed the wrong geometry. + if dec.frame_family() != family { + let mut d = StreamDecoder::new(); + d.set_sbr_downsampled(self.sbr_downsampled); + d.set_sbr_low_power(self.sbr_low_power); + d.set_frame_family(family); + *dec = d; + } + // §4.6.18.2.6: FsSBR is twice the core rate; an explicit SBR + // ASC whose extensionSamplingFrequency equals the core rate is + // therefore declaring the §4.6.18.4.3 downsampled output. + if asc.sbr_present && asc.extension_sample_rate == Some(asc.sample_rate) { + dec.set_sbr_downsampled(true); + } + // A channelConfiguration-0 layer carries its layout in the + // ASC's inline program_config_element(); install it so the + // §8.5.2.2 canonical output reorder applies (an in-band PCE in + // a later raw_data_block() still supersedes it). + if asc.channel_configuration == 0 { + if let Some(pce) = &asc.ga_body.pce { + dec.set_program_config(pce.clone()); + } + } + // The ER General-Audio object types use the §4.4.2.3 Table 4.19 + // fixed-sequence er_raw_data_block() instead of the tagged + // element walk; route AOT 17 (ER AAC LC), AOT 19 (ER AAC LTP — + // the §4.6.7 LTP tool over the same Table 4.19 walk) and + // AOT 23 (ER AAC LD, §4.6.17 — the 512/480-line family + // installed above) there with the ASC's resilience triplet. + if asc.aot == 17 || asc.aot == 19 || asc.aot == 23 { + let resilience = asc + .ga_body + .extension_body + .as_ref() + .and_then(|ext| ext.resilience) + .unwrap_or_default(); + return dec.decode_er_raw_data_block( + asc.aot, + asc.sampling_frequency_index, + asc.sample_rate, + asc.channel_configuration, + resilience, + &payload.data, + ); + } + // LATM carries exactly one raw_data_block() per payload. + dec.decode_raw_data_block( + asc.aot, + asc.sampling_frequency_index, + asc.sample_rate, + asc.channel_configuration, + 1, + &payload.data, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitWriter; + + /// AAC-LC, 44.1 kHz (samplingFrequencyIndex 4), stereo + /// (channelConfiguration 2): AOT=2 (5 bits `00010`), freqIdx=4 + /// (`0100`), chanConfig=2 (`0010`), then GASpecificConfig + /// `frameLengthFlag=0 dependsOnCoreCoder=0 extensionFlag=0` + /// (`000`). 16 bits total = `0x12 0x10`. + const AAC_LC_ASC: [u8; 2] = [0x12, 0x10]; + + /// Append the §1.7.3 AAC-LC ASC bit-for-bit into `w`. + fn write_aac_lc_asc(w: &mut BitWriter) { + // 16 bits, MSB-first, exactly as AAC_LC_ASC encodes. + w.write_u32(u32::from(u16::from_be_bytes(AAC_LC_ASC)), 16); + } + + #[test] + fn latm_get_value_single_byte() { + // bytesForValue = 0 -> one byte. value = 0xFF. + let mut w = BitWriter::new(); + w.write_u32(0, 2); // bytesForValue + w.write_u32(0xFF, 8); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert_eq!(latm_get_value(&mut r).unwrap(), 0xFF); + } + + #[test] + fn latm_get_value_multi_byte() { + // bytesForValue = 2 -> three bytes, big-endian: 0x010203. + let mut w = BitWriter::new(); + w.write_u32(2, 2); + w.write_u32(0x01, 8); + w.write_u32(0x02, 8); + w.write_u32(0x03, 8); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert_eq!(latm_get_value(&mut r).unwrap(), 0x01_02_03); + } + + /// Build a minimal `audioMuxVersion == 0` AAC-LC StreamMuxConfig: + /// one program, one layer, allStreamsSameTimeFraming, + /// frameLengthType 0, latmBufferFullness 0xFF, no otherData, no + /// CRC. + fn build_min_smc() -> Vec { + let mut w = BitWriter::new(); + w.write_bit(false); // audioMuxVersion = 0 + w.write_bit(true); // allStreamsSameTimeFraming = 1 + w.write_u32(0, 6); // numSubFrames = 0 + w.write_u32(0, 4); // numProgram = 0 + w.write_u32(0, 3); // numLayer = 0 + // (prog 0, lay 0): no useSameConfig bit; ASC inline. + write_aac_lc_asc(&mut w); + w.write_u32(0, 3); // frameLengthType = 0 + w.write_u32(0xFF, 8); // latmBufferFullness = 0xFF + w.write_bit(false); // otherDataPresent = 0 + w.write_bit(false); // crcCheckPresent = 0 + w.finish() + } + + #[test] + fn stream_mux_config_minimal_aac_lc() { + let bytes = build_min_smc(); + let mut r = BitReader::new(&bytes); + let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); + assert_eq!(smc.audio_mux_version, 0); + assert_eq!(smc.audio_mux_version_a, 0); + assert!(smc.all_streams_same_time_framing); + assert_eq!(smc.num_sub_frames, 0); + assert_eq!(smc.num_program, 0); + assert_eq!(smc.num_layer, vec![0]); + assert_eq!(smc.layers.len(), 1); + let lay = &smc.layers[0]; + assert_eq!(lay.stream_id, 0); + assert_eq!(lay.frame_length_type, 0); + assert_eq!(lay.latm_buffer_fullness, Some(0xFF)); + assert_eq!(lay.effective_asc.aot, 2); + assert_eq!(lay.effective_asc.sampling_frequency_index, 4); + assert_eq!(lay.effective_asc.channel_configuration, 2); + assert!(!smc.other_data_present); + assert!(!smc.crc_check_present); + assert_eq!(smc.stream_id(0, 0), Some(0)); + } + + /// Push the low `n` bits of `v` (MSB-first) onto a bool vector, + /// mirroring `BitWriter::write_u32` so the test can hold the config + /// prefix as bits for an independent CRC recomputation. + fn push_bits(out: &mut Vec, v: u32, n: u32) { + for i in (0..n).rev() { + out.push((v >> i) & 1 == 1); + } + } + + #[test] + fn stream_mux_config_with_valid_crc() { + // Build the config prefix as a bit vector, compute its CRC, then + // emit prefix + crcCheckPresent + crcCheckSum. + let mut prefix: Vec = Vec::new(); + push_bits(&mut prefix, 0, 1); // audioMuxVersion = 0 + push_bits(&mut prefix, 1, 1); // allStreamsSameTimeFraming + push_bits(&mut prefix, 0, 6); // numSubFrames + push_bits(&mut prefix, 0, 4); // numProgram + push_bits(&mut prefix, 0, 3); // numLayer + push_bits(&mut prefix, u32::from(u16::from_be_bytes(AAC_LC_ASC)), 16); + push_bits(&mut prefix, 0, 3); // frameLengthType + push_bits(&mut prefix, 0xFF, 8); // latmBufferFullness + push_bits(&mut prefix, 0, 1); // otherDataPresent + let sum = crc::stream_mux_config_crc(&prefix); + + let mut w = BitWriter::new(); + for &b in &prefix { + w.write_bit(b); + } + w.write_bit(true); // crcCheckPresent + w.write_u32(u32::from(sum), 8); // crcCheckSum + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); + assert!(smc.crc_check_present); + assert_eq!(smc.crc_check_sum, Some(sum)); + } + + #[test] + fn stream_mux_config_bad_crc_rejected() { + let mut w = BitWriter::new(); + w.write_bit(false); + w.write_bit(true); + w.write_u32(0, 6); + w.write_u32(0, 4); + w.write_u32(0, 3); + write_aac_lc_asc(&mut w); + w.write_u32(0, 3); + w.write_u32(0xFF, 8); + w.write_bit(false); + w.write_bit(true); // crcCheckPresent + w.write_u32(0x00, 8); // deliberately wrong crcCheckSum + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(matches!( + StreamMuxConfig::parse(&mut r, &bytes), + Err(Error::LatmCrcMismatch) + )); + } + + #[test] + fn stream_mux_config_two_layers_use_same_config() { + // One program, two layers; the second layer sets + // useSameConfig, so it must inherit the first layer's ASC. + let mut w = BitWriter::new(); + w.write_bit(false); // audioMuxVersion = 0 + w.write_bit(true); // allStreamsSameTimeFraming + w.write_u32(0, 6); // numSubFrames + w.write_u32(0, 4); // numProgram = 0 + w.write_u32(1, 3); // numLayer = 1 -> two layers + // layer 0: no useSameConfig bit; inline ASC. + write_aac_lc_asc(&mut w); + w.write_u32(0, 3); // frameLengthType 0 + w.write_u32(0xFF, 8); // latmBufferFullness + // layer 1: useSameConfig = 1. + w.write_bit(true); // useSameConfig + w.write_u32(0, 3); // frameLengthType 0 + w.write_u32(0xFF, 8); // latmBufferFullness + w.write_bit(false); // otherDataPresent + w.write_bit(false); // crcCheckPresent + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); + assert_eq!(smc.layers.len(), 2); + assert!(smc.layers[0].asc.is_some()); + assert!(smc.layers[1].asc.is_none()); + // The inherited effective ASC matches the first layer. + assert_eq!( + smc.layers[1].effective_asc.aot, + smc.layers[0].effective_asc.aot + ); + assert_eq!(smc.stream_id(0, 1), Some(1)); + } + + #[test] + fn stream_mux_config_unsupported_frame_length_type() { + // frameLengthType = 3 (CELP) must be rejected. + let mut w = BitWriter::new(); + w.write_bit(false); + w.write_bit(true); + w.write_u32(0, 6); + w.write_u32(0, 4); + w.write_u32(0, 3); + write_aac_lc_asc(&mut w); + w.write_u32(3, 3); // frameLengthType = 3 (CELP) + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(matches!( + StreamMuxConfig::parse(&mut r, &bytes), + Err(Error::LatmUnsupportedFrameLengthType(3)) + )); + } + + #[test] + fn stream_mux_config_version1_reserved_a_rejected() { + // audioMuxVersion = 1, audioMuxVersionA = 1 -> reserved. + let mut w = BitWriter::new(); + w.write_bit(true); // audioMuxVersion = 1 + w.write_bit(true); // audioMuxVersionA = 1 + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(matches!( + StreamMuxConfig::parse(&mut r, &bytes), + Err(Error::LatmAudioMuxVersionAReserved) + )); + } + + #[test] + fn stream_mux_config_frame_length_type1_fixed_bits() { + // frameLengthType = 1, frameLength = 100 -> (100+20)*8 bits. + let mut w = BitWriter::new(); + w.write_bit(false); + w.write_bit(true); + w.write_u32(0, 6); + w.write_u32(0, 4); + w.write_u32(0, 3); + write_aac_lc_asc(&mut w); + w.write_u32(1, 3); // frameLengthType = 1 + w.write_u32(100, 9); // frameLength = 100 + w.write_bit(false); // otherDataPresent + w.write_bit(false); // crcCheckPresent + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); + let lay = &smc.layers[0]; + assert_eq!(lay.frame_length_type, 1); + assert_eq!(lay.frame_length, Some(100)); + assert_eq!(lay.fixed_payload_bits(), Some((100 + 20) * 8)); + } + + /// Write the minimal `audioMuxVersion == 0` AAC-LC StreamMuxConfig + /// (one prog, one layer, frameLengthType 0, no CRC) into `w` + /// without finishing — for embedding inside an AudioMuxElement. + fn write_min_smc_into(w: &mut BitWriter) { + w.write_bit(false); // audioMuxVersion = 0 + w.write_bit(true); // allStreamsSameTimeFraming + w.write_u32(0, 6); // numSubFrames = 0 + w.write_u32(0, 4); // numProgram = 0 + w.write_u32(0, 3); // numLayer = 0 + write_aac_lc_asc(w); + w.write_u32(0, 3); // frameLengthType = 0 + w.write_u32(0xFF, 8); // latmBufferFullness + w.write_bit(false); // otherDataPresent + w.write_bit(false); // crcCheckPresent + } + + #[test] + fn audio_mux_element_in_band_single_payload() { + // muxConfigPresent=1, useSameStreamMux=0, inline minimal SMC, + // one subframe carrying a 4-byte payload. + let payload: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF]; + let mut w = BitWriter::new(); + w.write_bit(false); // useSameStreamMux = 0 + write_min_smc_into(&mut w); + // PayloadLengthInfo: MuxSlotLengthBytes = 4 (single byte, < 255). + w.write_u32(4, 8); + // PayloadMux: 4 payload bytes. + for &b in &payload { + w.write_byte(b); + } + // otherDataPresent was 0; ByteAlign() pads. + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let ame = AudioMuxElement::parse(&mut r, &bytes, true, None).unwrap(); + assert!(!ame.use_same_stream_mux); + assert_eq!(ame.payloads.len(), 1); + let p = &ame.payloads[0]; + assert_eq!(p.sub_frame, 0); + assert_eq!(p.prog, 0); + assert_eq!(p.lay, 0); + assert_eq!(p.stream_id, 0); + assert_eq!(p.data, payload.to_vec()); + } + + #[test] + fn audio_mux_element_escape_length() { + // MuxSlotLengthBytes with one 0xFF escape: 255 + 3 = 258 bytes. + let len = 258usize; + let payload: Vec = (0..len).map(|i| (i & 0xFF) as u8).collect(); + let mut w = BitWriter::new(); + w.write_bit(false); // useSameStreamMux + write_min_smc_into(&mut w); + w.write_u32(255, 8); // escape + w.write_u32(3, 8); // + 3 = 258 + for &b in &payload { + w.write_byte(b); + } + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let ame = AudioMuxElement::parse(&mut r, &bytes, true, None).unwrap(); + assert_eq!(ame.payloads.len(), 1); + assert_eq!(ame.payloads[0].data, payload); + } + + #[test] + fn audio_mux_element_use_same_stream_mux_inherits() { + // First element carries the config; second sets + // useSameStreamMux and inherits it. + let mut w0 = BitWriter::new(); + w0.write_bit(false); // useSameStreamMux = 0 + write_min_smc_into(&mut w0); + w0.write_u32(2, 8); // 2-byte payload + w0.write_byte(0x11); + w0.write_byte(0x22); + let bytes0 = w0.finish(); + let mut r0 = BitReader::new(&bytes0); + let first = AudioMuxElement::parse(&mut r0, &bytes0, true, None).unwrap(); + + let mut w1 = BitWriter::new(); + w1.write_bit(true); // useSameStreamMux = 1 + w1.write_u32(3, 8); // 3-byte payload + w1.write_byte(0xAA); + w1.write_byte(0xBB); + w1.write_byte(0xCC); + let bytes1 = w1.finish(); + let mut r1 = BitReader::new(&bytes1); + let second = AudioMuxElement::parse(&mut r1, &bytes1, true, Some(&first.config)).unwrap(); + assert!(second.use_same_stream_mux); + assert_eq!(second.payloads.len(), 1); + assert_eq!(second.payloads[0].data, vec![0xAA, 0xBB, 0xCC]); + } + + #[test] + fn audio_mux_element_use_same_without_prev_rejected() { + let mut w = BitWriter::new(); + w.write_bit(true); // useSameStreamMux = 1, but no prev config + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(matches!( + AudioMuxElement::parse(&mut r, &bytes, true, None), + Err(Error::LatmNoPreviousMuxConfig) + )); + } + + #[test] + fn audio_mux_element_multiple_subframes() { + // numSubFrames = 1 -> two PayloadMux frames, each a separate + // PayloadLengthInfo + payload. + let mut w = BitWriter::new(); + w.write_bit(false); // useSameStreamMux + // StreamMuxConfig with numSubFrames = 1. + w.write_bit(false); // audioMuxVersion = 0 + w.write_bit(true); // allStreamsSameTimeFraming + w.write_u32(1, 6); // numSubFrames = 1 + w.write_u32(0, 4); // numProgram = 0 + w.write_u32(0, 3); // numLayer = 0 + write_aac_lc_asc(&mut w); + w.write_u32(0, 3); // frameLengthType = 0 + w.write_u32(0xFF, 8); // latmBufferFullness + w.write_bit(false); // otherDataPresent + w.write_bit(false); // crcCheckPresent + // subframe 0: 2 bytes. + w.write_u32(2, 8); + w.write_byte(0x01); + w.write_byte(0x02); + // subframe 1: 1 byte. + w.write_u32(1, 8); + w.write_byte(0x03); + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let ame = AudioMuxElement::parse(&mut r, &bytes, true, None).unwrap(); + assert_eq!(ame.payloads.len(), 2); + assert_eq!(ame.payloads[0].sub_frame, 0); + assert_eq!(ame.payloads[0].data, vec![0x01, 0x02]); + assert_eq!(ame.payloads[1].sub_frame, 1); + assert_eq!(ame.payloads[1].data, vec![0x03]); + } + + /// Build the byte body of a minimal in-band AudioMuxElement(1) + /// carrying `payload` (one subframe, frameLengthType 0). The + /// returned bytes are exactly the `audioMuxLengthBytes` body that a + /// LOAS frame wraps. + fn build_min_audio_mux_element(payload: &[u8]) -> Vec { + let mut w = BitWriter::new(); + w.write_bit(false); // useSameStreamMux = 0 + write_min_smc_into(&mut w); + // MuxSlotLengthBytes for payload.len() (< 255). + assert!(payload.len() < 255); + w.write_u32(payload.len() as u32, 8); + for &b in payload { + w.write_byte(b); + } + w.finish() + } + + #[test] + fn audio_sync_stream_single_frame() { + let payload: [u8; 5] = [0x21, 0x00, 0x03, 0x40, 0x80]; + let body = build_min_audio_mux_element(&payload); + + // AudioSyncStream frame: 0x2B7 (11 bits) + audioMuxLengthBytes + // (13 bits) + body. 11 + 13 = 24 bits = 3 bytes, so the body is + // byte-aligned. + let mut w = BitWriter::new(); + w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); + w.write_u32(body.len() as u32, 13); + w.write_bytes(&body); + let stream = w.finish(); + + let mut walker = AudioSyncStream::new(&stream); + let frame = walker.next_frame().unwrap().unwrap(); + assert_eq!(frame.offset, 0); + assert_eq!(usize::from(frame.audio_mux_length_bytes), body.len()); + assert_eq!(frame.element.payloads.len(), 1); + assert_eq!(frame.element.payloads[0].data, payload.to_vec()); + // No more frames. + assert!(walker.next_frame().unwrap().is_none()); + } + + #[test] + fn audio_sync_stream_skips_leading_garbage() { + let payload: [u8; 2] = [0xAB, 0xCD]; + let body = build_min_audio_mux_element(&payload); + let mut w = BitWriter::new(); + w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); + w.write_u32(body.len() as u32, 13); + w.write_bytes(&body); + let frame_bytes = w.finish(); + + // Prepend non-syncword garbage bytes. + let mut stream = vec![0x00, 0xAA, 0x55]; + stream.extend_from_slice(&frame_bytes); + + let mut walker = AudioSyncStream::new(&stream); + let frame = walker.next_frame().unwrap().unwrap(); + assert_eq!(frame.offset, 3); + assert_eq!(frame.element.payloads[0].data, payload.to_vec()); + } + + #[test] + fn audio_sync_stream_two_frames_via_iterator() { + let p0: [u8; 2] = [0x10, 0x20]; + let p1: [u8; 3] = [0x30, 0x40, 0x50]; + + let build = |payload: &[u8]| { + // First frame carries config inline; second uses + // useSameStreamMux to inherit it. + let body = build_min_audio_mux_element(payload); + let mut w = BitWriter::new(); + w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); + w.write_u32(body.len() as u32, 13); + w.write_bytes(&body); + w.finish() + }; + + let mut stream = build(&p0); + // Second frame: useSameStreamMux = 1 body. + let body1 = { + let mut w = BitWriter::new(); + w.write_bit(true); // useSameStreamMux = 1 + w.write_u32(p1.len() as u32, 8); // MuxSlotLengthBytes + for &b in &p1 { + w.write_byte(b); + } + w.finish() + }; + let mut w1 = BitWriter::new(); + w1.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); + w1.write_u32(body1.len() as u32, 13); + w1.write_bytes(&body1); + stream.extend_from_slice(&w1.finish()); + + let frames: Vec<_> = AudioSyncStream::new(&stream) + .collect::>>() + .unwrap(); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].element.payloads[0].data, p0.to_vec()); + assert_eq!(frames[1].element.payloads[0].data, p1.to_vec()); + // The second frame inherited the first frame's config. + assert!(frames[1].element.use_same_stream_mux); + } + + #[test] + fn audio_sync_stream_truncated_body_rejected() { + let payload: [u8; 4] = [0x01, 0x02, 0x03, 0x04]; + let body = build_min_audio_mux_element(&payload); + let mut w = BitWriter::new(); + w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); + // Claim a longer body than is present. + w.write_u32((body.len() + 10) as u32, 13); + w.write_bytes(&body); + let stream = w.finish(); + + let mut walker = AudioSyncStream::new(&stream); + assert!(matches!(walker.next_frame(), Err(Error::LoasSyncInvalid))); + } + + #[test] + fn ep_audio_sync_header_parse() { + // 0x4DE1 (16) + futureUse(4)=0x5 + audioMuxLengthBytes(13)=100 + // + frameCounter(5)=7 + headerParity(18)=0x12345. + let mut w = BitWriter::new(); + w.write_u32(EP_AUDIO_SYNC_STREAM_SYNCWORD, 16); + w.write_u32(0x5, 4); + w.write_u32(100, 13); + w.write_u32(7, 5); + w.write_u32(0x12345, 18); + // A few body bytes (not parsed). + w.write_bytes(&[0xAA, 0xBB]); + let stream = w.finish(); + + let hdr = EpAudioSyncHeader::parse(&stream, 0).unwrap().unwrap(); + assert_eq!(hdr.offset, 0); + assert_eq!(hdr.future_use, 0x5); + assert_eq!(hdr.audio_mux_length_bytes, 100); + assert_eq!(hdr.frame_counter, 7); + assert_eq!(hdr.header_parity, 0x12345); + assert_eq!(hdr.body_offset, 7); + } + + #[test] + fn ep_audio_sync_header_not_found() { + let stream = [0x00u8, 0x11, 0x22, 0x33]; + assert!(EpAudioSyncHeader::parse(&stream, 0).unwrap().is_none()); + } +} diff --git a/crates/vendor/oxideav-aac/src/lib.rs b/crates/vendor/oxideav-aac/src/lib.rs new file mode 100644 index 00000000..19e83f87 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/lib.rs @@ -0,0 +1,641 @@ +//! # oxideav-aac +//! +//! Pure-Rust AAC (Advanced Audio Coding) parsing — currently **Phase 1** +//! of the post-r111 orphan-rebuild lineage. Decode and encode bodies are +//! *not* wired up yet; this crate's public surface is limited to: +//! +//! * The [`adts`] module — ISO/IEC 13818-7 §1.A.2 *Audio Data Transport +//! Stream* fixed-header parser (sync, profile, sampling-frequency +//! index, channel configuration, frame length, raw-data-block count, +//! CRC presence flag). +//! * The [`asc`] module — ISO/IEC 14496-3 §1.6.2.1 *AudioSpecificConfig* +//! parser, including the §4.4.1 *GASpecificConfig* body for all +//! General Audio audio-object types (AOTs 1, 2, 3, 4, 6, 7, 17, 19, +//! 20, 21, 22, 23) and the hierarchical SBR (AOT 5) / PS (AOT 29) +//! outer-wrapper unwrap. Embeds an inline +//! [`pce::Pce`](pce::Pce) when `channelConfiguration == 0`. +//! **Round 177** extends the GA body with the `extensionFlag == 1` +//! subtree (Table 4.1: AOT 22's `numOfSubFrame` + `layer_length`; +//! the AOT-17 / 19 / 20 / 23 resilience triplet; the +//! always-present `extensionFlag3` tail bit) and the Table 1.15 +//! trailing `epConfig` 2-bit field for every ER AOT in +//! {17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39}. `epConfig == 2` +//! or `3` (which mandate `ErrorProtectionSpecificConfig()` parsing) +//! surface as [`Error::UnsupportedEpConfig`]; an `extensionFlag3 +//! == 1` body — whose layout is reserved by the spec — surfaces as +//! [`Error::UnsupportedAscExtensionFlag3`]. +//! **Round 192** adds the Table 1.15 trailing +//! `syncExtensionType == 0x2b7` implicit-SBR probe (§1.6.5): +//! when the outer AOT is **not** the explicit SBR (5) or PS (29) +//! wrapper and the carrier has at least 16 bits remaining, +//! [`AudioSpecificConfig::parse`] now reads an 11-bit +//! `syncExtensionType` field. On a `0x2b7` match it consumes the +//! nested `GetAudioObjectType()` plus either the SBR branch +//! (`sbrPresentFlag`, optional `extensionSamplingFrequencyIndex`, +//! then a second 11-bit `syncExtensionType == 0x548` gating a +//! 1-bit `psPresentFlag`) or the BSAC branch (`sbrPresentFlag`, +//! optional `extensionSamplingFrequencyIndex`, mandatory 4-bit +//! `extensionChannelConfiguration`). The probe result is exposed +//! as [`asc::AudioSpecificConfig::trailing_sbr_probe`] and the +//! implicitly-signalled SBR / PS / extension-sample-rate values +//! are also propagated to the top-level `sbr_present` / +//! `ps_present` / `extension_sampling_frequency_index` / +//! `extension_sample_rate` / `extension_channel_configuration` +//! fields. A carrier-bounded entry point +//! [`asc::AudioSpecificConfig::parse_bits_bounded`] is exposed so +//! LATM `StreamMuxConfig` (and any future esds AudioObj +//! descriptor) callers can pass the exact ASC bit length; +//! [`asc::AudioSpecificConfig::parse_bits`] preserves its +//! no-probe semantics for callers that hold a `BitReader` +//! carrying trailing carrier bytes. +//! * The [`pce`] module — ISO/IEC 14496-3 §4.4.1.1 *program_config_element* +//! parser. Used both standalone (inside [`raw_data_block`]) and inline +//! inside [`asc`]. +//! * The [`raw_data_block`] module — ISO/IEC 14496-3 §4.4.2.1 syntactic +//! *raw_data_block()* walker that visits each `id_syn_ele` in order +//! and stops cleanly at `END (0b111)`. Per-element bodies for +//! SCE / CPE / CCE / LFE are **not** parsed yet — the walker emits an +//! element-header event and the consumer is responsible for +//! advancing the bit-reader past the body (subsequent rounds will +//! internalise this). PCE is fully parsed. **Round 160** added the +//! matching encoder-side [`raw_data_block::FrameAssembler`] — the +//! bit-exact inverse, with a typed push-API +//! (`push_channel_header` / `push_channel_body_bits` / `push_fill` / +//! `push_data` / `push_pce` / `push_end`) that composes the existing +//! per-tool writers (`IcsInfo::write`, `SectionData::write`, …) into +//! a complete byte stream. **Round 165** adds +//! [`pce::Pce::write`] (the bit-exact inverse of the round-126 +//! `Pce::parse`) and the matching +//! [`raw_data_block::FrameAssembler::push_pce`] entry point, closing +//! the last per-element writer gap in the `raw_data_block()` frame +//! assembler. +//! * The [`ics_info`] module — ISO/IEC 14496-3 §4.4.6 / Table 4.6 +//! *ics_info()* parser. The first piece of Phase 2 +//! (channel-element body parsing) — surfaces the window-sequence / +//! shape, `max_sfb`, `scale_factor_grouping`, the Main predictor +//! side-info (AOT 1), and the LTP `ltp_data()` body +//! (Table 4.55) when the wire bit selects it, plus the +//! §4.5.2.3.4 derivations (`num_windows`, `num_window_groups`, +//! `window_group_length[]`, `num_swb`). **Round 140** added the +//! matching `IcsInfo::write` encoder primitive (and a public +//! `write_ltp_data` helper) — the second encode-side syntax-element +//! writer in the crate. Self-roundtrip (`write` → `parse`) is +//! bit-perfect across every branch the parser handles, including +//! the Main predictor + Table 4.55 LTP body for both the non-LD +//! and the ER-AAC-LD forms. +//! * The [`section_data`] module — ISO/IEC 14496-3 §4.4.6 / ISO/IEC +//! 13818-7 §6.3 Table 17 *section_data()* parser, **plus** (round +//! 137) the matching `SectionData::write` encoder primitive. The +//! parser assigns a Huffman codebook (`sect_cb`) to each run of +//! scalefactor bands per window group via run-length escape +//! coding, building the per-group `sfb_cb[g][sfb]` map that +//! `scale_factor_data()` (next round) consumes. The encoder is its +//! inverse: given the same `(window_sequence, max_sfb)` context it +//! emits a bit-exact Table 17 stream. Self-roundtrip +//! (`write` → `parse`) is bit-perfect across the long, EIGHT_SHORT, +//! single-escape, double-escape, and exact-multiple-of-`sect_esc_val` +//! branches. No Huffman decode yet — every field is fixed-width. +//! * The [`pulse_data`] module — ISO/IEC 14496-3 §4.4.6.3 / Table 4.7 +//! *pulse_data()* parser **and** encoder primitive (**new in round +//! 142**). The parser reads the 2-bit `number_pulse`, 6-bit +//! `pulse_start_sfb`, and `number_pulse + 1` `(5-bit pulse_offset, +//! 4-bit pulse_amp)` records into [`pulse_data::PulseData`]; the +//! writer serialises the same structure back bit-for-bit. Every +//! field is fixed-width — no Huffman tables, no `swb_offset` +//! dependence, and no surrounding-element state. The §4.6.13 +//! reconstruction loop (`k += swb_offset[pulse_start_sfb] + +//! pulse_offset[j]; x_quant[…] ±= pulse_amp[j]`) is **not** +//! performed; it needs `swb_offset_long_window[]` and the +//! post-Huffman `x_quant` array that arrive with `spectral_data()`. +//! * The [`scale_factor_data`] module — ISO/IEC 14496-3 §4.4.6 / +//! Table 4.53 (non-resilient branch) plus §4.6.3 / Table 4.A.1 +//! *scale_factor_data()* parser **and** encoder primitive +//! (round 149, the fifth encode-side syntax-element writer in +//! the crate). Carries the AAC scalefactor Huffman codebook +//! (codebook 12) — 121 entries indexed `0..=120` with +//! `index_offset = -60`, producing DPCM deltas in `-60..=+60`. The +//! parser walks the per-`(g, sfb)` non-`ZERO_HCB` subsequence +//! driven by [`section_data::SectionData::sfb_cb`] and dispatches +//! between `hcod_sf[]` (ordinary spectrum / PNS-after-first / both +//! intensity codebooks) and the 9-bit `dpcm_noise_nrg` PCM seed +//! (first PNS band of the frame). The writer serialises the same +//! structure back bit-for-bit and validates the in-memory record +//! variants against the codebook map. +//! +//! **Round 152** adds the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM +//! accumulator pair [`scale_factor_data::accumulate`] (decoder +//! side) / [`scale_factor_data::differentiate`] (encoder side) +//! that converts between transmitted DPCM deltas and absolute +//! per-band quantities. Three independent tracks: spectrum +//! scalefactors (seed `last_sf = global_gain`, range `0..=255`), +//! intensity stereo positions (seed `last_is = 0`), and PNS noise +//! energies (seed `last_nrg = global_gain - NOISE_OFFSET - 256`, +//! first PNS band carries a 9-bit `uimsbf` literal). The §4.4.6 +//! error-resilient branch (`aacScalefactorDataResilienceFlag == +//! 1`, RVLC with `rev_global_gain`, `sf_concealment`, +//! `length_of_rvlc_sf`) is still **not** implemented; ER AAC-LD / +//! scalable profiles that flip the resilience flag will need a +//! sibling `scale_factor_data_rvlc()` module. +//! * The [`tns_data`] module — ISO/IEC 14496-3 §4.4.6 / Table 4.54 +//! *tns_data()* parser **and** encoder primitive (**new in round +//! 146**). The parser walks every transform window of the +//! surrounding `window_sequence` and reads `n_filt[w]` +//! (1 or 2 bits per Table 4.155), an optional `coef_res[w]` +//! (when `n_filt[w] > 0`), then per-filter `length` (4 or 6 bits), +//! `order` (3 or 5 bits), and — when `order > 0` — `direction`, +//! `coef_compress`, and `order` × `coef[i]` magnitudes whose width +//! is `(3 + coef_res) − coef_compress` per §4.6.9.3. The writer +//! serialises the same structure back bit-for-bit. The §4.6.9.3 +//! `tns_decode_coef` LPC reconstruction (signed conversion, +//! `iqfac` arcsine inverse-quantisation, Levinson-style conversion +//! to LPC) lives in [`tns_coef`], as does the §4.6.9.3 +//! `tns_ar_filter` all-pole pass over a strided spectrum region. +//! What remains owed is the §4.6.9 `tns_decode_frame` orchestration +//! that slices the per-window spectrum by `swb_offset` / +//! `direction` / `length` and dispatches the filter — that walker +//! belongs with the per-AOT IMDCT reconstruction driver. +//! * The [`gain_control_data`] module — ISO/IEC 14496-3 §4.4.6.5 / +//! Table 4.12 *gain_control_data()* parser **and** encoder +//! primitive (**new in round 183**). Carries the SSR (AOT 3) +//! PQF-band gain-control ladder: 2-bit `max_band`, then for each +//! `bd ∈ 1..=max_band` a per-window `(3-bit adjust_num) + +//! adjust_num × (4-bit alevcode + W(seq, wd)-bit aloccode)` ladder +//! with the per-`window_sequence` window count `N ∈ {1, 2, 8, 2}` +//! and the per-`(seq, wd)` `aloccode` width table from Table 4.12 +//! (5 / 4-2 / 2 / 4-5). The §4.6.12 ladder-application loop that +//! reconstructs sample-domain attenuation factors is **not** +//! performed; it needs the SSR PQF / IMDCT back-end. +//! * The [`swb_offset`] module — ISO/IEC 14496-3 §4.5.4.1 / Tables +//! 4.129–4.141 *swb_offset_long_window[]* and +//! *swb_offset_short_window[]* lookup tables, **new in round 194**. +//! The per-band lowest-coefficient index for each of the 12 valid +//! `samplingFrequencyIndex` values is exposed as +//! [`swb_offset::SWB_OFFSET_LONG_WINDOW`] (each slot +//! `num_swb + 1` entries with trailing 1024 sentinel) and +//! [`swb_offset::SWB_OFFSET_SHORT_WINDOW`] (each slot +//! `num_swb + 1` entries with trailing 128 sentinel). Public +//! accessors [`swb_offset::long_window_offsets`] and +//! [`swb_offset::short_window_offsets`] bounds-check +//! `fs_index`. [`swb_offset::apply_pulse_data`] applies the +//! §4.6.13 pulse-escape reconstruction to a long-window +//! `x_quant` slice — the first reconstruction-layer entry point in +//! the crate, consuming a parsed [`pulse_data::PulseData`] block +//! and folding the `±pulse_amp` fix-up into the quantised +//! spectrum at the running coefficient index `k = swb_offset[fs][ +//! pulse_start_sfb] + Σ pulse_offset[i]`. The 960-line frame +//! variant (Tables 4.142–4.147) is **not** covered. +//! * The [`tns_max`] module — ISO/IEC 14496-3 §4.6.9.4 Tables +//! 4.102 / 4.103 decoder-side `TNS_MAX_ORDER` / `TNS_MAX_BANDS` +//! clamp tables and §4.6.17.2.5 Tables 4.119 / 4.120 LD-specific +//! `TNS_MAX_BANDS` tables, **new in round 200**. The accessors +//! [`tns_max::tns_max_order`] and [`tns_max::tns_max_bands`] +//! surface the per-AOT / per-window-sequence / per-`fs_index` +//! caps; [`tns_max::tns_max_bands_ld_480`] and +//! [`tns_max::tns_max_bands_ld_512`] handle the LD frame-size +//! split. The clamp helpers [`tns_max::clamp_tns_order`] and +//! [`tns_max::clamp_tns_band`] fold the §4.6.9.3 three-way +//! `min(band, TNS_MAX_BANDS, max_sfb)` and +//! `min(order, TNS_MAX_ORDER)` pseudocode into one call so the +//! eventual TNS reconstruction layer can consume them without +//! re-deriving the AOT dispatch. The Table 4.103 dispatch splits +//! AOT 3 (AAC SSR) into the PQF-filterbank columns; every other +//! AOT uses the non-PQF columns. +//! * The [`ics_body`] module — ISO/IEC 14496-3 §4.4.6 / Table 4.50 +//! `individual_channel_stream()` body walker, **new in round 207**. +//! Composes the existing per-tool parsers / writers (`global_gain`, +//! [`ics_info`], [`section_data`], [`scale_factor_data`], optional +//! [`pulse_data`] / [`tns_data`] / [`gain_control_data`]) into the +//! complete Table 4.50 channel-element body, **up to but not +//! including** `spectral_data()`. Surfaces the parsed structure plus +//! the `spectral_data_bit_offset` so the caller (e.g. a future +//! spectrum parser, or a frame-assembler that hands off the +//! spectrum-bit-slice via `push_channel_body_bits`) can resume the +//! walk at the right boundary. The shared-info `CPE` form +//! ([`ics_body::IcsBody::parse_with_ics_info`] / +//! [`ics_body::IcsBody::write_with_ics_info`]) accepts the +//! externally-held [`ics_info::IcsInfo`] for the per-channel body. +//! Table 4.50 Note 1's "pulse_data illegal on +//! `EIGHT_SHORT_SEQUENCE`" and the §4.6.12 "gain_control_data is +//! AOT-3 (SSR) only" normative constraints are enforced on the +//! writer side; the parser surfaces literal bits to keep hostile +//! streams from panicking. `scale_flag == true` (scalable AAC, AOT +//! 6) rejects with [`Error::NotImplemented`]. +//! * The [`spectral_codebook`] module — ISO/IEC 14496-3 §4.6.3.1 / +//! Table 4.95 Spectrum Huffman codebook parameter table plus the +//! §4.6.3.3 codeword-index → spectral-tuple translation, the +//! §4.6.3.3 sign-bit fix-up, and the §4.6.3.3 ESC sequence handler +//! for codebook 11 (and the extension books 16..=31), **new in +//! round 213**. `TABLE_4_95: [Table495Row; 32]` carries the four +//! normative columns (`unsigned_cb`, `dimension`, `lav`, +//! `esc_threshold`) for every codebook in `0..=31`; `table_4_95` +//! is the safe accessor. +//! [`spectral_codebook::decode_index_to_tuple`] is the §4.6.3.3 +//! pseudocode that translates a Huffman codeword index `idx` to a +//! `dim`-tuple of quantised spectral coefficients; +//! [`spectral_codebook::encode_tuple_to_index`] is its inverse. +//! The sign-bit fix-up +//! [`spectral_codebook::apply_sign_bits`] / +//! [`spectral_codebook::derive_sign_bits`] folds the +//! per-non-zero-coefficient sign bits the spec emits after an +//! unsigned-codebook codeword onto / from a signed tuple. The +//! ESC sequence [`spectral_codebook::decode_esc_value`] / +//! [`spectral_codebook::encode_esc_value`] expands codebook-11 +//! coefficients at the LAV cap into the §4.6.3.3 escape sequence +//! (`2^(N + 4) + escape_word`, capped at +//! [`spectral_codebook::MAX_QUANT`] = 8191 per §4.6.1.3). The +//! Huffman tables themselves (Tables 4.A.3 through 4.A.12) are +//! still owed — see [`spectrum_huffman`] for the first one. The +//! §4.4.6 `spectral_data()` wire walker that loops over +//! scalefactor bands and dispatches on the per-band codebook is +//! also **not** wired up; this module is the per-codeword +//! translation layer it will sit on top of. +//! * The [`spectrum_huffman`] module — the **wire layer** for the +//! §4.6.3 / Annex 4.A Huffman codebooks (**new in round 219**). +//! Round 219 landed the first of the eleven spectrum books: +//! **Table 4.A.2** (Spectrum Huffman Codebook 1, signed 4-tuple, +//! `LAV = 1`, 81 entries indexed `0..=80`, maximum codeword +//! length 11 bits; the zero-tuple at index 40 carries the +//! single-bit codeword `0`). Round 226 added Codebook 2 +//! (Table 4.A.3, same signed 4-tuple universe, 9-bit max), round +//! 231 added Codebook 3 (Table 4.A.4, the first **unsigned** book, +//! `LAV = 2`, 16-bit max; the zero magnitude tuple migrates to +//! index 0). Round 234 added Codebook 4 (Table 4.A.5, the second +//! unsigned dim-4 book, 12-bit max; the shortest codeword +//! `0b0000` parks at index 40 while index 0 carries a 4-bit +//! `0b0111`). Round 238 adds Codebook 5 (Table 4.A.6, the first +//! **pair** book: `unsigned = 0`, `dim = 2`, `LAV = 4` → `9^2 = +//! 81` entries, 13-bit max; the §4.6.3.3 polynomial puts the +//! zero-tuple `(0, 0)` at the centre index 40 — also the location +//! of the single-bit `0` shortest codeword — while the four +//! `(±4, ±4)` lattice corners take the four 13-bit codewords at +//! indices 0 / 8 / 72 / 80). Public API per book: +//! `HCODN_NUM_ENTRIES` = 81, +//! `HCODN_MAX_LEN` (codebook-specific), `hcodN_encode(idx) -> +//! (length, codeword)` (right-aligned in `u16`), `hcodN_decode` +//! reads MSB-first from a [`oxideav_core::bits::BitReader`] and +//! returns the codeword index, and `hcodN_write` is a convenience +//! wrapper over the encode + writer-emit pair. Every book is a +//! complete prefix code over `HCODN_MAX_LEN` bits, exhaustively +//! verified at unit-test time. Round 250 added Codebook 8 +//! (Table 4.A.9, the second **unsigned pair** book sharing +//! Codebook 7's `unsigned = 1`, `dim = 2`, `LAV = 7` → 64-entry +//! universe; 10-bit max; the zero-tuple at index 0 carries a +//! 5-bit `0b01110` and the shortest 3-bit `0` codeword migrates +//! to the interior tuple `(1, 1)` at index 9). Codebooks 9..=11 +//! (Tables 4.A.10 … 4.A.12) reuse the same module shape and are +//! owed in subsequent rounds; the `spectral_data()` driver that +//! dispatches per-band onto the chosen codebook arrives once all +//! eleven are in place. +//! * The [`dequant`] module — ISO/IEC 14496-3 §4.6.1.3 inverse +//! quantization (`Sign(x_quant) · |x_quant|^(4/3)`) and §4.6.2.3.3 +//! scalefactor application (`gain = 2^(0.25 · (sf − SF_OFFSET))`, +//! `SF_OFFSET = 100`), **new in round 284** — the first numeric +//! reconstruction stage after the wire walk. +//! [`dequant::rescale_spectrum`] applies both band-wise over the +//! §4.5.2.3.4 `sect_sfb_offset` ranges in the §4.5.2.3.5 +//! interleaved transmission order. +//! * The [`decoded_spectrum`] module — the §4.6.3.3 +//! `quant_to_spec()` de-interleaver (transmission order → +//! window-major `spec[w][k]`) and +//! [`decoded_spectrum::decode_channel_spectrum`], the per-channel +//! pipeline stage (pulse fix-up → scalefactor accumulation → +//! inverse quantization + rescaling → de-interleave → TNS), +//! **new in round 284**. Ends one step short of the §4.6.11 +//! filterbank. +//! * The [`extension_payload`] module — ISO/IEC 14496-3 §4.4.2.7 / +//! Table 4.51 *extension_payload()* parser **and** encoder +//! primitive (**new in round 187**). Implements the three +//! non-SBR `extension_type` branches whose body layouts are +//! fully specified by fixed-width fields: `EXT_FILL` (`0b0000`) +//! — the Table 4.51 default branch surfacing the +//! `8 * (cnt - 1) + 4` `other_bits` as a packed byte buffer; +//! `EXT_FILL_DATA` (`0b0001`) — the normative-pattern filler +//! with `fill_nibble == 0b0000` and `fill_byte == +//! 0b1010_0101`; and `EXT_DYNAMIC_RANGE` (`0b1011`) — the +//! Table 4.52 `dynamic_range_info()` block (optional +//! `pce_instance_tag`, optional Table 4.53 `excluded_channels()` +//! exclude-mask list, optional per-band partitioning, optional +//! `prog_ref_level`, and per-band `(dyn_rng_sgn, dyn_rng_ctl)` +//! records). The two SBR-data values from ISO/IEC 13818-7 +//! Table 40 (`EXT_SBR_DATA` `0b1101` and `EXT_SBR_DATA_CRC` +//! `0b1110`) surface as [`Error::UnsupportedExtensionSbr`] — +//! their bodies are `sbr_extension_data()` which needs the QMF / +//! patching back-end this crate does not yet provide. The +//! §4.5.2.13 DRC companding-curve application is **not** +//! performed; the raw `(dyn_rng_sgn, dyn_rng_ctl)` records are +//! surfaced verbatim for a later round. +//! +//! The decode path is fully wired: [`register`] installs an AAC +//! [`Decoder`](oxideav_core::Decoder) (id `"aac"`) via the +//! [`codec_decoder`] module, adapting the [`decode::StreamDecoder`] into +//! the framework's packet-in / frame-out trait. The encode path still +//! has no rate-control back-end — the bit-exact wire writers exist but +//! no `Encoder` is registered. +//! +//! ## Provenance +//! +//! Every numeric +//! constant, bit layout, and clause reference in this crate is sourced +//! from the staged ISO/IEC 13818-7 and ISO/IEC 14496-3 PDFs under +//! `docs/audio/aac/`. The fixture descriptions in +//! `docs/audio/aac/aac-fixtures-and-traces.md` were consulted as a +//! cross-reference against the spec wording. +//! +//! ## Status (Phase 1 + Phase 2 begin) +//! +//! * ADTS fixed header parsing: **complete** (sync + 7-byte body). +//! * ADTS CRC validation: deferred; the parser surfaces the +//! `protection_absent` flag but does not validate the trailing +//! 16-bit CRC when present. +//! * `raw_data_block()` walker: iterates `id_syn_ele` and stops at +//! `END`; FIL / DSE / PCE bodies are fully consumed. SCE / CPE / +//! CCE / LFE bodies now compose through the new [`ics_body`] +//! walker (Table 4.50): `global_gain` → [`ics_info`] → +//! [`section_data`] → [`scale_factor_data`] → optional +//! [`pulse_data`] / [`tns_data`] / [`gain_control_data`]. The +//! trailing channel-stream tool, Table 4.56 `spectral_data()`, is +//! covered by the [`spectral_data`] walker: `ics_body` surfaces +//! the start bit-offset and [`spectral_data::SpectralData::parse`] +//! consumes the spectrum from that position, completing the +//! Table 4.50 body. Driving that pair from the `raw_data_block()` +//! walker (plus the CPE `common_window` / `ms_mask_present` +//! header) is the remaining wiring; the `tests/docs_adts_corpus.rs` +//! driver demonstrates the full composition over the staged ADTS +//! fixture corpus. +//! * Numeric reconstruction (round 284): a parsed channel body now +//! decodes to a window-major real-valued spectrum via +//! [`decoded_spectrum::decode_channel_spectrum`] — §4.6.3.3 pulse +//! fix-up, §4.6.2.3.2 scalefactor accumulation, §4.6.1.3 inverse +//! quantization, §4.6.2.3.3 rescaling, §4.6.3.3 `quant_to_spec()`, +//! §4.6.9 TNS. The §4.6.11 filterbank (round 289) turns that +//! spectrum into PCM-domain samples. M/S (§4.6.8.1) stereo +//! reconstruction is [`ms_stereo::apply_ms_stereo`] (round 293), a +//! CPE-level de-matrix over the channel pair before TNS. Intensity +//! stereo (§4.6.8.2) reconstruction is +//! [`intensity_stereo::apply_intensity_stereo`] (round 300), the +//! deterministic left→right derivation +//! `r = is_intensity·invert_intensity·0.5^(0.25·is_pos)·l` that runs +//! after M/S and before TNS. PNS (§4.6.13) synthesis is +//! [`pns::apply_pns`] / [`pns::apply_pns_pair`] (round 307), the +//! noise-band fill `scale = 2^(0.25·noise_nrg)/sqrt(Σ spec²)` whose +//! per-band L2 norm is the spec-determined `2^(0.25·noise_nrg)` (only +//! the per-coefficient phase is RNG-defined, so the band energy — not +//! the exact samples — is byte-exact). The §4.6 element-level decode +//! driver [`element_decode::ElementDecoder`] (round 311) chains the +//! whole stack per channel element: `decode_sce` for SCE / LFE and +//! `decode_cpe` for a CPE run pulse → dequant → `quant_to_spec()` → +//! M/S → intensity → PNS → TNS → §4.6.11 filterbank to PCM, carrying +//! the per-channel overlap-add tail across frames. The stream-level +//! [`decode::StreamDecoder`] walks the §4.4.2.1 `raw_data_block()` +//! above that driver and renders to element-order interleaved 16-bit +//! PCM via the §4.6.11 [`pcm`] output stage (the §1.3 `NINT()` +//! round-half-away-from-zero + saturation). The decoded PCM is +//! validated against the staged `expected.wav` corpus: the two +//! PNS-free ADTS fixtures are 99.9 % byte-exact (max error 1 LSB — +//! the residual is the `f64` direct-sum vs a `float32` fast-transform +//! IMDCT difference), and the PNS-bearing fixtures match in the PCM +//! RMS domain below 0.1 % error-to-signal (full byte-exactness is +//! precluded only by the §4.6.13.3 spec-undefined noise phase). + +#![warn(missing_debug_implementations)] +#![warn(missing_docs)] + +use oxideav_core::RuntimeContext; + +pub mod adts; +pub mod adts_crc; +pub mod asc; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod bsac_arith; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod bsac_decode; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod bsac_layer; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod bsac_tables; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod cce; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod channel_map; +pub mod codec_decoder; +pub mod codec_encoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod crc; +pub mod decode; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod decoded_spectrum; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod dequant; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod element_decode; +pub mod encoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod encoder_tns; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod extension_payload; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod filterbank; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod gain_control; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod gain_control_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod hcr; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod hcr_decode; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ics_body; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ics_info; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod intensity_stereo; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ipqf; +pub mod latm; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ltp; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ms_stereo; +pub mod pce; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod pcm; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod pns; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod predictor; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_decoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_decorr; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_huffman; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_hybrid; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_map; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ps_stereo; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod pulse_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod raw_data_block; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod rvlc; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_decoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_dequant; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_element; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_env_adjust; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_envelope; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_extension; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_freq_bands; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_grid; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_header; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_hf_gen; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_huffman; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_limiter; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_lp; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_noise_table; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_qmf; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_reconstruct; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod sbr_time_grid; +// internal — exposed for tests/fuzz; not part of the stable API +pub mod scalable; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ep_config; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ep_fec; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ep_rs; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ep_frame; +#[doc(hidden)] +pub mod scale_factor_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod section_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod spectral_codebook; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod spectral_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod spectrum_huffman; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ssr; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ssr_filterbank; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod swb_offset; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod tns_coef; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod tns_data; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod tns_frame; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod tns_max; + +mod error; + +pub use error::Error; + +/// Result alias used throughout the crate. +pub type Result = core::result::Result; + +/// Codec-registry entry point. Installs the AAC +/// [`Decoder`](oxideav_core::Decoder) (id `"aac"`) — the ADTS-framed +/// AAC-LC decode chain wired through [`codec_decoder::register_codecs`], +/// claiming the MP4 object-type / WAVEFORMATEX / FourCC / Matroska tags +/// an AAC elementary stream is routed under. No encoder is wired yet +/// (the crate has the bit-exact wire writers but no rate-control +/// encoder back-end). +pub fn register(ctx: &mut RuntimeContext) { + codec_decoder::register_codecs(&mut ctx.codecs); +} + +oxideav_core::register!("aac", register); diff --git a/crates/vendor/oxideav-aac/src/ltp.rs b/crates/vendor/oxideav-aac/src/ltp.rs new file mode 100644 index 00000000..caa6a30d --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ltp.rs @@ -0,0 +1,976 @@ +//! Long-Term Prediction (LTP) synthesis — ISO/IEC 14496-3 §4.6.7. +//! +//! LTP is a forward-adaptive, single-tap time-domain predictor that +//! reduces inter-frame redundancy for signals with a clear pitch. +//! Because the predictor coefficients are transmitted as side +//! information (`ltp_data()`, Table 4.55, parsed by +//! [`crate::ics_info::LtpData`]), the decoder applies the predictor +//! without the round-off sensitivity of the backward-adaptive MPEG-2 +//! frequency-domain predictor (§4.6.6). +//! +//! ## Scope of this module +//! +//! This module implements the §4.6.7.3 **long-window** decoding +//! process, which is the only window family LTP supports for the AAC +//! LTP audio object type (§4.6.7.1 restricts LTP to long windows for +//! bitstream compatibility with MPEG-2 AAC). The three long sequences +//! (`ONLY_LONG_SEQUENCE`, `LONG_START_SEQUENCE`, `LONG_STOP_SEQUENCE`) +//! are handled; `EIGHT_SHORT_SEQUENCE` is a no-op here (LTP is disabled +//! and the per-window predictors are reset, §4.6.7.3 / the short-block +//! reset note). +//! +//! The decode steps, transcribed from the §4.6.7.3 pseudo code: +//! +//! ```text +//! x_est = predict(); // 1-tap time-domain prediction +//! X_est = MDCT(x_est); // windowed analysis transform +//! for (sfb = 0; sfb < num_sfb; sfb++) +//! if (ltp_data_present && ltp_long_used[sfb]) +//! X_rec = X_est + Y_rec; // add predicted spectrum +//! else +//! X_rec = Y_rec; // pass the transmitted spectrum +//! ``` +//! +//! * `predict()` forms `x_est(i) = ltp_coef · x_rec(i − M − ltp_lag)`, +//! `i = 0 … N−1`, with `M = 0` for every non-LD AOT and `M = N/2` +//! for ER AAC LD (§4.6.7.3; the LD lag is 10-bit with the +//! `ltp_lag_update` repeat, §4.6.7.2). `x_rec` is the per-channel +//! reconstruction history (see [`LtpState`]). +//! * `MDCT(x_est)` windows `x_est` with the current frame's §4.6.11 +//! long window and applies the §4.6.15.3.3 analysis transform +//! ([`crate::filterbank::forward_mdct`]). +//! * `Y_rec` is the decoded (de-interleaved, inverse-quantised) +//! spectrum; `X_est + Y_rec` replaces it on the sfb that carry +//! `ltp_long_used == 1`. +//! +//! Per §4.6.7.4.1 (Figure 4.30) the LTP add precedes TNS synthesis in +//! the decode chain, so the spectrum passed in / out here is the +//! pre-TNS reconstructed spectrum. + +use crate::filterbank::{forward_mdct, long_only_window_family, short_window_j}; +use crate::ics_info::{IcsInfo, LtpData, WindowSequence, WindowShape}; +#[cfg(test)] +use crate::swb_offset::long_window_offsets; +#[cfg(test)] +use crate::swb_offset::LONG_WINDOW_LEN; +use crate::swb_offset::{short_window_offsets, FrameFamily, SHORT_WINDOW_LEN}; +use crate::Error; + +type Result = core::result::Result; + +/// The short transform length `N_s = 2 · 128 = 256` (§4.6.11.3.1). +const SHORT_TRANSFORM_LEN: usize = 2 * SHORT_WINDOW_LEN as usize; + +/// ISO/IEC 14496-3:2001 §4.6.7.3 — the number of scalefactor bands a +/// short-window LTP contribution covers ("for (sfb = 0; sfb < 8; +/// sfb++)": the first 8 SFBs of each predicted subwindow only). +pub const LTP_SHORT_MAX_SFB: usize = 8; + +/// Table 4.98 — the 8-entry LTP coefficient codebook. `ltp_coef` +/// (3 bits) indexes this table; the value is the single-tap predictor +/// gain applied in [`LtpState::predict_long`]. +pub const LTP_COEF: [f64; 8] = [ + 0.570829, 0.696616, 0.813004, 0.911304, 0.984900, 1.067894, 1.194601, 1.369533, +]; + +/// Map a 3-bit `ltp_coef` index to its Table 4.98 gain. +/// +/// Errors: [`Error::LtpInvalid`] if `index > 7`. +pub fn ltp_coefficient(index: u8) -> Result { + LTP_COEF + .get(index as usize) + .copied() + .ok_or(Error::LtpInvalid) +} + +/// Per-channel LTP reconstruction-history buffer (§4.6.7.3). +/// +/// The predictor reads `x_rec(i − M − ltp_lag)`; the buffer therefore +/// has to retain enough past output to cover the maximum lag +/// (`ltp_lag ≤ 2047`) plus the current transform window. The layout, +/// per §4.6.7.3: +/// +/// * `x_rec(0 … N/2 − 1)` — the last aliased half window from the +/// current frame's IMDCT (the pre-overlap-add windowed tail); +/// * `x_rec(N/2 … N − 1)` — always all zeros; +/// * `x_rec(i < 0)` — the previous fully reconstructed time-domain +/// output of the decoder. +/// +/// [`Self::history`] stores the `i < 0` region in chronological order +/// (oldest first), so `x_rec(j)` for `j < 0` is +/// `history[history.len() + j]`. [`Self::aliased_tail`] stores +/// `x_rec(0 … N/2 − 1)`. At the start of decoding the whole buffer is +/// zero, matching the §4.6.7.3 initialisation. +#[derive(Clone, Debug, Default)] +pub struct LtpState { + /// The §4.5.1.1 frame-length family this channel decodes under. + /// Sets the transform length `N`, the aliased-tail length `N/2`, + /// the §4.6.7.3 LD lag offset `M = N/2`, and the history depth. + family: FrameFamily, + /// Previously reconstructed decoder output (the `i < 0` region), + /// oldest sample first. Capped at [`Self::history_cap`] samples. + history: Vec, + /// `x_rec(0 … N/2 − 1)` — the current frame's aliased IMDCT half + /// window, `family.frame_len()` samples. Empty before the first + /// frame (treated as zeros). + aliased_tail: Vec, + /// §4.6.7.2 (ER AAC LD) `ltp_prev_lag` — the last transmitted + /// `ltp_lag`, repeated when a frame signals + /// `ltp_lag_update == 0`. Zero before any lag was transmitted. + prev_lag: u16, +} + +impl LtpState { + /// Maximum 11-bit `ltp_lag` (§4.6.7.2), used to size the history + /// buffer so the deepest possible prediction still has data. + const MAX_LAG: usize = 2047; + + /// A fresh, all-zero LTP state (§4.6.7.3 initialisation) for the + /// 1024-line family. + pub fn new() -> Self { + Self::default() + } + + /// A fresh, all-zero LTP state for an arbitrary §4.5.1.1 family. + /// For the LD families this arms the §4.6.7.3 `M = N/2` lag + /// offset, the 10-bit lag range and the `ltp_prev_lag` repeat + /// mechanism (§4.6.17.2.6 scales the delay buffer with the frame, + /// 2048 / 1920 samples for N = 512 / 480). + pub fn new_family(family: FrameFamily) -> Self { + LtpState { + family, + ..Self::default() + } + } + + /// §4.6.7.3 — the LD lag offset `M`: `N/2` (== the frame length, + /// since `N` is the transform window length `2 × frame_len`) for + /// ER AAC LD, `0` otherwise. + fn lag_offset(&self) -> usize { + if self.family.is_ld() { + self.family.frame_len() + } else { + 0 + } + } + + /// Resolve this frame's effective `ltp_lag` and update the + /// `ltp_prev_lag` repeat state (§4.6.7.2, ER AAC LD): a + /// transmitted lag becomes the new `ltp_prev_lag`; an absent lag + /// (LD `ltp_lag_update == 0`) repeats the previous one. Non-LD + /// streams always transmit the 11-bit lag, so the repeat arm is + /// only reachable for LD. + fn resolve_lag(&mut self, ltp: &LtpData) -> Result { + match ltp.lag { + Some(lag) => { + self.prev_lag = lag; + Ok(lag) + } + None => { + if ltp.lag_update == Some(false) { + Ok(self.prev_lag) + } else { + // A missing lag without the LD repeat signal is a + // malformed in-memory record. + Err(Error::LtpInvalid) + } + } + } + } + + /// Number of past-output samples to retain. The predictor needs + /// `M + ltp_lag` samples before index 0 (`M = N/2` for LD), and + /// the deepest window read is `N − 1`, so `MAX_LAG + M + N` past + /// samples always suffice; the non-LD families keep the historic + /// `MAX_LAG + N` depth. + fn history_cap(&self) -> usize { + Self::MAX_LAG + self.lag_offset() + self.family.long_transform_len() + } + + /// Read `x_rec(j)` for any integer index `j` per the §4.6.7.3 + /// buffer arrangement. Out-of-range indices (deeper than the + /// retained history, or `j ≥ N`) read as zero, matching the + /// zero-initialised buffer. + fn x_rec(&self, j: isize) -> f64 { + let half = self.family.frame_len() as isize; // N/2 + if j < 0 { + // Previous fully reconstructed output, chronological. + let idx = self.history.len() as isize + j; + if idx < 0 { + 0.0 + } else { + self.history[idx as usize] + } + } else if j < half { + // Aliased IMDCT half window. + self.aliased_tail.get(j as usize).copied().unwrap_or(0.0) + } else { + // x_rec(N/2 … N−1) is always zero. + 0.0 + } + } + + /// §4.6.7.3 `predict()` — form the predicted time-domain signal + /// `x_est(i) = ltp_coef · x_rec(i − M − ltp_lag)`, `i = 0 … N−1`, + /// with `M = N/2` for the ER AAC LD families and `M = 0` + /// otherwise. + fn predict_long(&self, lag: u16, coef: f64) -> Vec { + let shift = lag as isize + self.lag_offset() as isize; + (0..self.family.long_transform_len() as isize) + .map(|i| coef * self.x_rec(i - shift)) + .collect() + } + + /// Update the history after a frame is fully reconstructed. + /// + /// * `output` — this frame's `LONG_WINDOW_LEN` (1024) PCM samples, + /// i.e. the §4.6.11.3.3 overlap-added output, which become the + /// `i < 0` region for subsequent frames. + /// * `aliased_tail` — this frame's `x_rec(0 … N/2 − 1)`, the + /// pre-overlap-add windowed IMDCT tail of length + /// `LONG_WINDOW_LEN`. + /// + /// Call once per frame, after synthesis, regardless of whether LTP + /// was active, so the predictor history stays continuous. + pub fn push_frame(&mut self, output: &[f64], aliased_tail: &[f64]) { + self.history.extend_from_slice(output); + let cap = self.history_cap(); + if self.history.len() > cap { + let excess = self.history.len() - cap; + self.history.drain(0..excess); + } + self.aliased_tail.clear(); + self.aliased_tail.extend_from_slice(aliased_tail); + } + + /// §4.6.7.3 — apply long-window LTP to one channel's reconstructed + /// spectrum in place. + /// + /// * `spec` — the `LONG_WINDOW_LEN` (1024) decoded coefficients + /// `Y_rec`, modified to `X_rec` on the predicted bands. + /// * `ics_info` — provides `window_sequence`, `window_shape` and + /// `max_sfb`; LTP only acts on the three long sequences. + /// * `ltp` — the parsed §4.6.7.2 side info for this channel. + /// * `prev_shape` — the previous block's `window_shape`, governing + /// the left half of this block's analysis window + /// (§4.6.11.3.2). `None` before the first frame, in which case + /// the block's own shape is used for both halves. + /// * `fs_index` — the sampling-frequency index, selecting the + /// §4.5.4 long-window scalefactor-band offsets. + /// + /// When LTP is inactive (short sequence, or `ltp.long_used` all + /// false) the spectrum is left untouched. Errors: + /// [`Error::LtpInvalid`] for an out-of-range `ltp_coef`, a missing + /// `ltp_lag`, or a spectrum length that is not `LONG_WINDOW_LEN`; + /// the [`Error`] surfaced by [`long_window_offsets`] for a bad + /// `fs_index`. + pub fn apply_long( + &mut self, + spec: &mut [f64], + ics_info: &IcsInfo, + ltp: &LtpData, + prev_shape: Option, + fs_index: u8, + ) -> Result<()> { + self.apply_long_with_analysis(spec, ics_info, ltp, prev_shape, fs_index, |_| Ok(())) + } + + /// §4.6.7.3 + §4.6.7.4.1 — the LTP long-window add with the + /// Figure 4.30 **TNS analysis filter** inserted between + /// `X_est = MDCT(x_est)` and the per-sfb `X_rec = X_est + Y_rec`. + /// + /// When TNS is active on the channel, the transmitted residual + /// `Y_rec` carried in `spec` lives in the noise-shaped (pre-TNS- + /// synthesis) domain. The LTP-predicted spectrum `X_est` is a clean + /// MDCT, so it has to be pushed through the same all-zero TNS + /// analysis filter before it can be added like-for-like. `analyze` + /// applies that filter in place to the freshly transformed `X_est` + /// (length `LONG_WINDOW_LEN`); pass a no-op closure when the channel + /// carries no TNS (which is what [`Self::apply_long`] does). + /// + /// The subsequent §4.6.9 TNS *synthesis* pass over the combined + /// `X_rec` (run by the element driver after this add) undoes the + /// analysis on the LTP contribution while shaping the residual, + /// per the §4.6.7.4.1 inverse-filter relationship. + /// + /// All other semantics match [`Self::apply_long`]. + pub fn apply_long_with_analysis( + &mut self, + spec: &mut [f64], + ics_info: &IcsInfo, + ltp: &LtpData, + prev_shape: Option, + fs_index: u8, + analyze: F, + ) -> Result<()> + where + F: FnOnce(&mut [f64]) -> Result<()>, + { + // §4.6.7.3 / short-block note: prediction is disabled for + // EIGHT_SHORT_SEQUENCE in the long-window LTP path. + if ics_info.window_sequence == WindowSequence::EightShort { + return Ok(()); + } + // The channel state and the frame side info must agree on the + // §4.5.1.1 family (transform length, LD lag offset). + if ics_info.family != self.family { + return Err(Error::LtpInvalid); + } + if spec.len() != self.family.frame_len() { + return Err(Error::LtpInvalid); + } + // The effective lag (with the LD ltp_prev_lag repeat) must be + // resolved on EVERY LTP-bearing frame — even one that flags no + // bands — so the repeat state tracks the wire exactly. + let lag = self.resolve_lag(ltp)?; + // No bands flagged → nothing to add. + if !ltp.long_used.iter().any(|&u| u) { + return Ok(()); + } + + let coef = ltp_coefficient(ltp.coef)?; + + // predict() → MDCT(x_est). + let n_transform = self.family.long_transform_len(); + let x_est = self.predict_long(lag, coef); + let left_shape = prev_shape.unwrap_or(ics_info.window_shape); + let window = long_only_window_family(self.family, left_shape, ics_info.window_shape); + let z: Vec = x_est + .iter() + .zip(window.iter()) + .map(|(&x, &w)| x * w) + .collect(); + let mut x_est_spec = forward_mdct(&z, n_transform); + + // §4.6.7.4.1 / Figure 4.30: TNS analysis filter on X_est. + analyze(&mut x_est_spec)?; + + // Per-sfb: X_rec = X_est + Y_rec where ltp_long_used[sfb]. + let offsets = crate::swb_offset::long_window_offsets_family(self.family, fs_index)?; + let num_sfb = ics_info.max_sfb as usize; + for sfb in 0..num_sfb { + if !ltp.long_used.get(sfb).copied().unwrap_or(false) { + continue; + } + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + for c in start..end.min(spec.len()) { + spec[c] += x_est_spec[c]; + } + } + Ok(()) + } + + /// ISO/IEC 14496-3:**2001** §4.6.7.3 — short-window LTP synthesis + /// for one channel's `EIGHT_SHORT_SEQUENCE` spectrum, in place. + /// + /// This is the reconstruction counterpart of the 2001-edition + /// `ltp_data()` short branch (`ltp_short_used[w]` / + /// `ltp_short_lag[w]`, parsed under + /// [`crate::ics_info::LtpEdition::Iso2001`]). The 2009 edition + /// **removed** short-window LTP entirely (§4.6.7.1 "LTP is + /// restricted to long windows only"), so this entry point is never + /// reached by the 2009 decode chain; it exists for 2001-edition + /// streams. Per the 2001 pseudo-code, for each of the eight + /// subwindows `w` flagged `ltp_short_used[w]`: + /// + /// ```text + /// x_est = predict(); // lag = ltp_lag + ltp_short_lag[w] + /// X_est = MDCT(x_est); // the 256-point short transform + /// for (sfb = 0; sfb < 8; sfb++) // first 8 SFBs only + /// X_rec = X_est + Y_rec; + /// ``` + /// + /// with the same Table 4.98 `ltp_coef` for every subwindow, and + /// `ltp_short_lag[w] ∈ −8..=7` a per-window *relative* delay added + /// to the frame's 11-bit `ltp_lag` (`0` when + /// `ltp_short_lag_present[w] == 0`). A negative combined lag + /// (possible only when `ltp_lag < 8`) is floored at `0` — the + /// history holds no future samples. + /// + /// ## The `window_origins` parameter — a documented spec ambiguity + /// + /// §4.6.7.3 (2001) states the `x_rec` buffer arrangement once, in + /// terms of a single long transform, and never respecifies the + /// **index origin of each subwindow** into that shared history — + /// i.e. which absolute history position subwindow `w`'s + /// `x_est(0)` reads from (see the staged analysis + /// `docs/audio/aac/short-window-ltp-blocked.md` §5; no encoder + /// emits this syntax and no reference decode exists to pin it). + /// Rather than invent a convention, this routine takes the + /// per-subwindow origin explicitly: subwindow `w` predicts + /// `x_est(i) = ltp_coef · x_rec(window_origins[w] + i − lag_w)` + /// for `i = 0..256`. When a fixture (or errata) eventually fixes + /// the origin rule, the caller encodes it here without touching + /// the pinned math. + /// + /// Errors: [`Error::LtpInvalid`] when `ics_info` is not + /// `EIGHT_SHORT_SEQUENCE`, `spec` is not the 8 × 128 window-major + /// short spectrum, `ltp.short` is missing / not 8 entries, the + /// frame `ltp_lag` is absent, or `ltp_coef` is out of range. + pub fn apply_short_2001( + &self, + spec: &mut [f64], + ics_info: &IcsInfo, + ltp: &LtpData, + prev_shape: Option, + fs_index: u8, + window_origins: &[isize; 8], + ) -> Result<()> { + if ics_info.window_sequence != WindowSequence::EightShort { + return Err(Error::LtpInvalid); + } + let wlen = SHORT_WINDOW_LEN as usize; + if spec.len() != 8 * wlen { + return Err(Error::LtpInvalid); + } + let Some(short) = ltp.short.as_ref() else { + return Err(Error::LtpInvalid); + }; + if short.len() != 8 { + return Err(Error::LtpInvalid); + } + if !short.iter().any(|s| s.used) { + return Ok(()); + } + let coef = ltp_coefficient(ltp.coef)?; + let lag = ltp.lag.ok_or(Error::LtpInvalid)? as isize; + let offsets = short_window_offsets(fs_index)?; + let num_sfb = LTP_SHORT_MAX_SFB + .min(ics_info.max_sfb as usize) + .min(offsets.len() - 1); + let left_shape = prev_shape.unwrap_or(ics_info.window_shape); + + for (w, sw) in short.iter().enumerate() { + if !sw.used { + continue; + } + // lag_w = ltp_lag + ltp_short_lag[w], floored at 0. + let lag_w = (lag + isize::from(sw.lag)).max(0); + let origin = window_origins[w]; + let x_est: Vec = (0..SHORT_TRANSFORM_LEN as isize) + .map(|i| coef * self.x_rec(origin + i - lag_w)) + .collect(); + // Window subwindow w (window 0's left half inherits the + // previous block's shape, §4.6.11.3.2) and run the + // 256-point analysis transform. + let window = short_window_j(w, left_shape, ics_info.window_shape); + let z: Vec = x_est + .iter() + .zip(window.iter()) + .map(|(&x, &wv)| x * wv) + .collect(); + let x_est_spec = forward_mdct(&z, SHORT_TRANSFORM_LEN); + // X_rec = X_est + Y_rec on the first 8 SFBs. + let base = w * wlen; + for sfb in 0..num_sfb { + let start = offsets[sfb] as usize; + let end = (offsets[sfb + 1] as usize).min(wlen); + for c in start..end { + spec[base + c] += x_est_spec[c]; + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::WindowSequence; + + fn long_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: true, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: 49, + } + } + + fn ltp_with(coef: u8, lag: u16, long_used: Vec) -> LtpData { + LtpData { + lag_update: None, + lag: Some(lag), + coef, + long_used, + short: None, + } + } + + #[test] + fn table_4_98_coefficients() { + // Table 4.98 endpoints and a mid value. + assert_eq!(ltp_coefficient(0).unwrap(), 0.570829); + assert_eq!(ltp_coefficient(4).unwrap(), 0.984900); + assert_eq!(ltp_coefficient(7).unwrap(), 1.369533); + assert!(ltp_coefficient(8).is_err()); + } + + #[test] + fn short_sequence_is_noop() { + let mut st = LtpState::new(); + let mut info = long_info(40); + info.window_sequence = WindowSequence::EightShort; + let ltp = ltp_with(0, 100, vec![true; 40]); + let mut spec = vec![1.0f64; LONG_WINDOW_LEN as usize]; + st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); + assert!(spec.iter().all(|&v| v == 1.0)); + } + + #[test] + fn no_bands_flagged_is_noop() { + let mut st = LtpState::new(); + // Seed history so a predictor would otherwise fire. + let out = vec![0.5f64; LONG_WINDOW_LEN as usize]; + let tail = vec![0.25f64; LONG_WINDOW_LEN as usize]; + st.push_frame(&out, &tail); + let info = long_info(40); + let ltp = ltp_with(0, 100, vec![false; 40]); + let mut spec = vec![1.0f64; LONG_WINDOW_LEN as usize]; + st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); + assert!(spec.iter().all(|&v| v == 1.0)); + } + + #[test] + fn zero_history_predicts_zero() { + // §4.6.7.3 initialisation: x_rec all zero ⇒ x_est all zero ⇒ + // X_est all zero ⇒ spectrum unchanged even with bands flagged. + let mut st = LtpState::new(); + let info = long_info(40); + let ltp = ltp_with(7, 50, vec![true; 40]); + let mut spec = vec![2.0f64; LONG_WINDOW_LEN as usize]; + st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); + for &v in &spec { + assert!((v - 2.0).abs() < 1e-12, "got {v}"); + } + } + + #[test] + fn predict_long_applies_lag_and_gain() { + // Drive the predictor from a known history. With lag L and the + // i<0 region holding a DC level d, x_est(i) = coef·d for all i + // whose source index i−L < 0 (i.e. i < L). Verify a handful of + // sample values directly via the private predictor. + let mut st = LtpState::new(); + let d = 1.0f64; + st.history = vec![d; st.history_cap()]; + let coef = ltp_coefficient(2).unwrap(); // 0.813004 + let lag = 64u16; + let x_est = st.predict_long(lag, coef); + // i=0: source index −64 (in history) ⇒ coef·d. + assert!((x_est[0] - coef * d).abs() < 1e-12); + // i=63: source −1 ⇒ coef·d. + assert!((x_est[63] - coef * d).abs() < 1e-12); + // i=64: source 0 ⇒ aliased_tail (empty) ⇒ 0. + assert!(x_est[64].abs() < 1e-12); + } + + #[test] + fn x_rec_regions_are_distinct() { + let mut st = LtpState::new(); + st.history = vec![3.0; 10]; + st.aliased_tail = vec![7.0; LONG_WINDOW_LEN as usize]; + // i<0 region: most-recent past = 3.0. + assert_eq!(st.x_rec(-1), 3.0); + // Beyond retained history reads zero. + assert_eq!(st.x_rec(-100), 0.0); + // 0..N/2 is the aliased tail. + assert_eq!(st.x_rec(0), 7.0); + assert_eq!(st.x_rec(LONG_WINDOW_LEN as isize - 1), 7.0); + // N/2..N is always zero. + assert_eq!(st.x_rec(LONG_WINDOW_LEN as isize), 0.0); + } + + #[test] + fn push_frame_caps_history() { + let mut st = LtpState::new(); + for _ in 0..4 { + let out = vec![1.0f64; LONG_WINDOW_LEN as usize]; + let tail = vec![0.0f64; LONG_WINDOW_LEN as usize]; + st.push_frame(&out, &tail); + } + assert!(st.history.len() <= st.history_cap()); + } + + // ===== ISO/IEC 14496-3:2001 §4.6.7.3 short-window LTP ===== + + use crate::ics_info::LtpShortWindow; + + fn short_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: true, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups: 8, + window_group_length: vec![1; 8], + num_swb: 14, + } + } + + fn short_ltp(coef: u8, lag: u16, windows: [Option; 8]) -> LtpData { + LtpData { + lag_update: None, + lag: Some(lag), + coef, + long_used: vec![], + short: Some( + windows + .iter() + .map(|w| match w { + Some(l) => LtpShortWindow { + used: true, + lag_present: *l != 0, + lag: *l, + }, + None => LtpShortWindow { + used: false, + lag_present: false, + lag: 0, + }, + }) + .collect(), + ), + } + } + + /// Natural subwindow-grid origins for the tests: subwindow w's + /// x_est(0) reads history position w·128 (one convention among + /// those the 2001 text admits — the routine deliberately takes + /// the origins from the caller; see the method docs). + fn grid_origins() -> [isize; 8] { + core::array::from_fn(|w| (w as isize) * SHORT_WINDOW_LEN as isize) + } + + #[test] + fn short_2001_rejects_bad_shapes() { + let st = LtpState::new(); + let ltp = short_ltp(0, 100, [Some(0); 8]); + let origins = grid_origins(); + // Long sequence rejected. + let mut spec = vec![0.0f64; 8 * SHORT_WINDOW_LEN as usize]; + let info = long_info(40); + assert!(st + .apply_short_2001(&mut spec, &info, <p, None, 3, &origins) + .is_err()); + // Wrong spectrum length rejected. + let sinfo = short_info(8); + let mut bad = vec![0.0f64; 100]; + assert!(st + .apply_short_2001(&mut bad, &sinfo, <p, None, 3, &origins) + .is_err()); + // Missing short records rejected. + let mut no_short = short_ltp(0, 100, [Some(0); 8]); + no_short.short = None; + assert!(st + .apply_short_2001(&mut spec, &sinfo, &no_short, None, 3, &origins) + .is_err()); + } + + #[test] + fn short_2001_no_used_window_is_noop() { + let mut st = LtpState::new(); + st.history = vec![1.0; st.history_cap()]; + st.aliased_tail = vec![0.5; LONG_WINDOW_LEN as usize]; + let info = short_info(8); + let ltp = short_ltp(3, 64, [None; 8]); + let mut spec = vec![2.0f64; 8 * SHORT_WINDOW_LEN as usize]; + st.apply_short_2001(&mut spec, &info, <p, None, 3, &grid_origins()) + .unwrap(); + assert!(spec.iter().all(|&v| v == 2.0)); + } + + #[test] + fn short_2001_zero_history_predicts_zero() { + let st = LtpState::new(); + let info = short_info(8); + let ltp = short_ltp(7, 64, [Some(0); 8]); + let mut spec = vec![1.5f64; 8 * SHORT_WINDOW_LEN as usize]; + st.apply_short_2001(&mut spec, &info, <p, None, 3, &grid_origins()) + .unwrap(); + for &v in &spec { + assert!((v - 1.5).abs() < 1e-12); + } + } + + #[test] + fn short_2001_only_used_windows_and_first_8_sfbs_change() { + // Non-trivial history; flag only subwindow 2. Its first-8-sfb + // region gains X_est energy, its upper bands stay untouched, + // and every other subwindow is untouched entirely. + let mut st = LtpState::new(); + st.history = (0..st.history_cap()) + .map(|i| ((i % 37) as f64) / 17.0 - 1.0) + .collect(); + st.aliased_tail = vec![0.25; LONG_WINDOW_LEN as usize]; + let fs = 3u8; + let info = short_info(14); + let mut flags = [None; 8]; + flags[2] = Some(0); + let ltp = short_ltp(4, 200, flags); + let wlen = SHORT_WINDOW_LEN as usize; + let mut spec = vec![0.0f64; 8 * wlen]; + st.apply_short_2001(&mut spec, &info, <p, None, fs, &grid_origins()) + .unwrap(); + + let offsets = short_window_offsets(fs).unwrap(); + let cutoff = offsets[LTP_SHORT_MAX_SFB] as usize; + // Subwindow 2, first 8 sfbs: changed. + let low = &spec[2 * wlen..2 * wlen + cutoff]; + assert!( + low.iter().any(|&v| v.abs() > 1e-9), + "flagged region changed" + ); + // Subwindow 2 above sfb 8: untouched. + assert!(spec[2 * wlen + cutoff..3 * wlen].iter().all(|&v| v == 0.0)); + // All other subwindows: untouched. + for w in [0usize, 1, 3, 4, 5, 6, 7] { + assert!( + spec[w * wlen..(w + 1) * wlen].iter().all(|&v| v == 0.0), + "unflagged subwindow {w} must stay silent" + ); + } + } + + #[test] + fn short_2001_relative_lag_shifts_the_source() { + // Same frame lag, different ltp_short_lag: the predictor must + // read a shifted history slice, so the two X_est contributions + // differ. History is an impulse train so any shift changes + // the windowed segment. + let mut st = LtpState::new(); + st.history = (0..st.history_cap()) + .map(|i| if i % 64 == 0 { 1.0 } else { 0.0 }) + .collect(); + st.aliased_tail = vec![0.0; LONG_WINDOW_LEN as usize]; + let info = short_info(8); + let wlen = SHORT_WINDOW_LEN as usize; + let run = |short_lag: i8| -> Vec { + let mut flags = [None; 8]; + flags[0] = Some(short_lag); + let ltp = short_ltp(4, 300, flags); + let mut spec = vec![0.0f64; 8 * wlen]; + st.apply_short_2001(&mut spec, &info, <p, None, 3, &grid_origins()) + .unwrap(); + spec[..wlen].to_vec() + }; + let a = run(0); + let b = run(7); + let c = run(-8); + assert!(a.iter().zip(&b).any(|(x, y)| (x - y).abs() > 1e-9)); + assert!(a.iter().zip(&c).any(|(x, y)| (x - y).abs() > 1e-9)); + } + + #[test] + fn short_2001_origin_convention_is_callers_choice() { + // The documented §4.6.7.3 (2001) ambiguity: the same frame + // under two different origin conventions produces different + // contributions — pinning that the routine faithfully defers + // the choice rather than hard-coding one. + let mut st = LtpState::new(); + st.history = (0..st.history_cap()) + .map(|i| ((i * 7919) % 251) as f64 / 125.0 - 1.0) + .collect(); + st.aliased_tail = vec![0.0; LONG_WINDOW_LEN as usize]; + let info = short_info(8); + let wlen = SHORT_WINDOW_LEN as usize; + let mut flags = [None; 8]; + flags[5] = Some(0); + let ltp = short_ltp(2, 500, flags); + let run = |origins: [isize; 8]| -> Vec { + let mut spec = vec![0.0f64; 8 * wlen]; + st.apply_short_2001(&mut spec, &info, <p, None, 3, &origins) + .unwrap(); + spec + }; + let grid = run(grid_origins()); + let zeroed = run([0; 8]); + assert!(grid.iter().zip(&zeroed).any(|(x, y)| (x - y).abs() > 1e-9)); + } + + #[test] + fn nonzero_history_modifies_flagged_bands_only() { + // With nonzero history, a flagged sfb gains X_est energy while + // an unflagged sfb is untouched. fs_index 3 (48 kHz) long + // offsets: sfb 0 = [0,4), so flag sfb 0 only and check bins + // 0..4 changed but a high bin is unchanged. + let mut st = LtpState::new(); + st.history = vec![1.0; st.history_cap()]; + st.aliased_tail = vec![0.5; LONG_WINDOW_LEN as usize]; + let mut used = vec![false; 40]; + used[0] = true; + let info = long_info(40); + let ltp = ltp_with(5, 30, used); + let baseline = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let mut spec = baseline.clone(); + st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); + let offsets = long_window_offsets(3).unwrap(); + let sfb0_end = offsets[1] as usize; + let changed = (0..sfb0_end).any(|c| (spec[c] - baseline[c]).abs() > 1e-9); + assert!(changed, "flagged sfb 0 should change"); + // A bin well above sfb 0 must be unchanged. + assert!((spec[sfb0_end + 50] - baseline[sfb0_end + 50]).abs() < 1e-12); + } + + // ---- ER AAC LD (§4.6.7.3 M = N/2, §4.6.7.2 ltp_prev_lag) ---- + + fn ld_info(family: FrameFamily, max_sfb: u8) -> IcsInfo { + let mut info = long_info(max_sfb); + info.family = family; + info.num_swb = 36; + info + } + + fn ld_ltp(coef: u8, lag: Option, long_used: Vec) -> LtpData { + LtpData { + lag_update: Some(lag.is_some()), + lag, + coef, + long_used, + short: None, + } + } + + #[test] + fn ld_predict_reads_with_m_offset() { + // Place a single impulse in the history and verify the LD + // predictor reads it at i = M + lag − depth… i.e. that + // x_est(i) = coef · x_rec(i − M − lag) with M = frame_len. + let mut st = LtpState::new_family(FrameFamily::Ld512); + // history: 2000 zeros with an impulse 100 samples back + // (x_rec(−100) = 1.0). + let mut hist = vec![0.0f64; 2000]; + let hlen = hist.len(); + hist[hlen - 100] = 1.0; + st.history = hist; + let coef = ltp_coefficient(0).unwrap(); + // lag = 40, M = 512: x_est(i) = coef·x_rec(i − 552); the + // impulse at x_rec(−100) lands at i = 452. + let x_est = st.predict_long(40, coef); + assert_eq!(x_est.len(), 1024); // N = 1024 for LD512 + for (i, &v) in x_est.iter().enumerate() { + if i == 452 { + assert!((v - coef).abs() < 1e-15, "impulse at {i}: {v}"); + } else { + assert_eq!(v, 0.0, "unexpected non-zero at {i}"); + } + } + } + + #[test] + fn ld_480_predict_geometry() { + let mut st = LtpState::new_family(FrameFamily::Ld480); + let mut hist = vec![0.0f64; 2000]; + let hlen = hist.len(); + hist[hlen - 1] = 1.0; // x_rec(−1) = 1.0 + st.history = hist; + let coef = ltp_coefficient(3).unwrap(); + // M = 480, lag = 0: impulse lands at i = 479. + let x_est = st.predict_long(0, coef); + assert_eq!(x_est.len(), 960); + assert!((x_est[479] - coef).abs() < 1e-15); + assert_eq!(x_est[480], 0.0); + } + + #[test] + fn ld_prev_lag_repeat() { + // Frame 1 transmits lag 123 (ltp_lag_update == 1); frame 2 + // repeats it (ltp_lag_update == 0, no lag on the wire). Both + // frames must predict identically from the same history. + let mut info = ld_info(FrameFamily::Ld512, 36); + info.num_swb = 36; + let mut st = LtpState::new_family(FrameFamily::Ld512); + st.history = (0..2048).map(|i| ((i * 37) % 101) as f64 * 0.01).collect(); + st.aliased_tail = vec![0.0; 512]; + + let with_lag = ld_ltp(2, Some(123), vec![true; 36]); + let repeat = ld_ltp(2, None, vec![true; 36]); + + let mut spec_a = vec![0.0f64; 512]; + let mut st_a = st.clone(); + st_a.apply_long(&mut spec_a, &info, &with_lag, None, 3) + .unwrap(); + + // Same state, but resolve the transmitted lag first and then + // decode a repeat frame — must produce the same contribution. + let mut st_b = st.clone(); + let mut warmup = vec![0.0f64; 512]; + st_b.apply_long(&mut warmup, &info, &with_lag, None, 3) + .unwrap(); + let mut spec_b = vec![0.0f64; 512]; + st_b.apply_long(&mut spec_b, &info, &repeat, None, 3) + .unwrap(); + + assert!(spec_a.iter().any(|&v| v != 0.0), "LTP must contribute"); + for (a, b) in spec_a.iter().zip(spec_b.iter()) { + assert!((a - b).abs() < 1e-12); + } + } + + #[test] + fn ld_repeat_without_prior_lag_uses_zero() { + // ltp_lag_update == 0 before any transmitted lag: the + // §4.6.7.3 zero-initialised state gives ltp_prev_lag = 0. + let info = ld_info(FrameFamily::Ld512, 36); + let mut st = LtpState::new_family(FrameFamily::Ld512); + st.aliased_tail = vec![0.0; 512]; + let repeat = ld_ltp(2, None, vec![true; 36]); + let mut spec = vec![0.0f64; 512]; + st.apply_long(&mut spec, &info, &repeat, None, 3).unwrap(); + // Zero history → zero contribution, but no error. + assert!(spec.iter().all(|&v| v == 0.0)); + } + + #[test] + fn ld_family_mismatch_rejected() { + let info = ld_info(FrameFamily::Ld512, 36); + let mut st = LtpState::new(); // Lc1024 state + let ltp = ld_ltp(0, Some(1), vec![true; 36]); + let mut spec = vec![0.0f64; 512]; + assert!(matches!( + st.apply_long(&mut spec, &info, <p, None, 3), + Err(Error::LtpInvalid) + )); + } + + #[test] + fn missing_lag_without_repeat_signal_rejected() { + let info = long_info(40); + let mut st = LtpState::new(); + let ltp = LtpData { + lag_update: None, + lag: None, + coef: 0, + long_used: vec![true; 40], + short: None, + }; + let mut spec = vec![0.0f64; 1024]; + assert!(matches!( + st.apply_long(&mut spec, &info, <p, None, 3), + Err(Error::LtpInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/ms_stereo.rs b/crates/vendor/oxideav-aac/src/ms_stereo.rs new file mode 100644 index 00000000..6f73bc9c --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ms_stereo.rs @@ -0,0 +1,684 @@ +//! §4.6.8.1 M/S (mid/side) stereo de-matrix — ISO/IEC 14496-3. +//! +//! M/S joint channel coding operates on a channel pair. On a +//! per-spectral-coefficient basis the decoder reconstructs the +//! left/right vector by either the identity matrix (M/S off for the +//! band) or the inverse M/S matrix (M/S on): +//! +//! ```text +//! [ l ] [ 1 0 ] [ l ] [ l ] [ 1 1 ] [ m ] +//! [ r ] = [ 0 1 ] [ r ] or [ r ] = [ 1 -1 ] [ s ] +//! ``` +//! +//! With `m` carried in the left slot and `s` in the right slot, the +//! §4.6.8.1.3 decoding pseudo code is the in-place de-matrix: +//! +//! ```text +//! if (mask_present >= 1) { +//! for (g=0; g Result { + match bits { + 0 => Ok(MsMaskPresent::AllZeros), + 1 => Ok(MsMaskPresent::Mask), + 2 => Ok(MsMaskPresent::AllOnes), + _ => Err(Error::MsStereoInvalid), + } + } + + /// The wire value (`0`/`1`/`2`) — the inverse of [`Self::from_bits`]. + pub fn to_bits(self) -> u8 { + match self { + MsMaskPresent::AllZeros => 0, + MsMaskPresent::Mask => 1, + MsMaskPresent::AllOnes => 2, + } + } + + /// `mask_present >= 1` — whether the §4.6.8.1.3 outer guard is + /// entered at all. + pub fn is_active(self) -> bool { + !matches!(self, MsMaskPresent::AllZeros) + } +} + +/// `true` ⇔ the band's right-channel codebook is an intensity book +/// (`INTENSITY_HCB` / `INTENSITY_HCB2`) — the §4.6.8.2.3 +/// `is_intensity(g,sfb)` predicate restricted to "is it intensity at +/// all" (the M/S guard only needs the boolean, not the ±1 sign). +fn is_intensity_cb(cb: u8) -> bool { + cb == INTENSITY_HCB || cb == INTENSITY_HCB2 +} + +/// `true` ⇔ the band's codebook is `NOISE_HCB` — the §4.6.13.3 +/// `is_noise(g,sfb)` predicate. +fn is_noise_cb(cb: u8) -> bool { + cb == NOISE_HCB +} + +/// A channel pair's de-interleaved spectra plus the per-channel +/// codebook assignments the M/S de-matrix needs. +/// +/// `left` / `right` are the window-major decoded spectra +/// (`num_windows × window_len`) produced by +/// [`crate::decoded_spectrum::quant_to_spec`], **pre-TNS**. On entry +/// `left` holds the mid (`m`) and `right` the side (`s`) for every +/// M/S-active band; [`apply_ms_stereo`] overwrites them with the +/// reconstructed left/right channels. +/// +/// `left_sfb_cb` / `right_sfb_cb` are each channel's `sfb_cb[g][sfb]` +/// (from its [`crate::section_data::SectionData`]); they drive the +/// intensity (right) / noise (either) exclusions. +#[derive(Debug)] +pub struct ChannelPairSpectra<'a> { + /// First ("left") channel spectrum — mid on entry, left on return. + pub left: &'a mut [f64], + /// Second ("right") channel spectrum — side on entry, right on return. + pub right: &'a mut [f64], + /// Left channel `sfb_cb[g][sfb]`. + pub left_sfb_cb: &'a [Vec], + /// Right channel `sfb_cb[g][sfb]`. + pub right_sfb_cb: &'a [Vec], +} + +/// Apply the §4.6.8.1.3 M/S de-matrix in place to a channel pair. +/// +/// * `pair` — the channel-pair spectra and per-channel codebooks +/// ([`ChannelPairSpectra`]). +/// * `ms_mask_present` — the decoded CPE [`MsMaskPresent`]. +/// * `ms_used` — `ms_used[g][sfb]` (one row per window group, each +/// at least `max_sfb` long). Ignored when `ms_mask_present` is +/// [`MsMaskPresent::AllZeros`] or [`MsMaskPresent::AllOnes`]; pass +/// an empty slice in those cases. +/// * `ics_info` — the shared `common_window` `ics_info()`; supplies +/// `num_window_groups`, `window_group_length`, `max_sfb`, and the +/// window geometry. +/// * `fs_index` — `samplingFrequencyIndex`, selecting the +/// `swb_offset` table. +/// +/// When `ms_mask_present` is [`MsMaskPresent::AllZeros`] the buffers +/// are left untouched (identity matrix on every band). +/// +/// Returns [`Error::MsStereoInvalid`] if the buffer / mask / `sfb_cb` +/// shapes disagree with `ics_info` (see the variant docs). +pub fn apply_ms_stereo( + pair: &mut ChannelPairSpectra<'_>, + ms_mask_present: MsMaskPresent, + ms_used: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, +) -> Result<()> { + let ChannelPairSpectra { + left, + right, + left_sfb_cb, + right_sfb_cb, + } = pair; + let window_len = ics_info.window_len()?; + let offsets = ics_info.swb_offsets(fs_index)?; + let num_swb = offsets.len() - 1; + let num_windows = ics_info.num_windows as usize; + let num_groups = ics_info.num_window_groups as usize; + let max_sfb = ics_info.max_sfb as usize; + + // Geometry consistency: both channels share the common_window + // ics_info, so both spectra are num_windows × window_len. + let expected = num_windows * window_len; + if left.len() != expected || right.len() != expected { + return Err(Error::MsStereoInvalid); + } + if ics_info.window_group_length.len() != num_groups + || ics_info + .window_group_length + .iter() + .map(|&w| w as usize) + .sum::() + != num_windows + { + return Err(Error::MsStereoInvalid); + } + // max_sfb must not exceed the band count of the active window. + if max_sfb > num_swb { + return Err(Error::MsStereoInvalid); + } + if left_sfb_cb.len() != num_groups || right_sfb_cb.len() != num_groups { + return Err(Error::MsStereoInvalid); + } + // The per-band exclusions read sfb_cb[g][sfb] for sfb < max_sfb. + for cb in left_sfb_cb.iter().chain(right_sfb_cb.iter()) { + if cb.len() < max_sfb { + return Err(Error::MsStereoInvalid); + } + } + // `Mask` needs a full ms_used[g][sfb]; the other modes ignore it. + if ms_mask_present == MsMaskPresent::Mask { + if ms_used.len() != num_groups { + return Err(Error::MsStereoInvalid); + } + for row in ms_used { + if row.len() < max_sfb { + return Err(Error::MsStereoInvalid); + } + } + } + + if !ms_mask_present.is_active() { + // mask_present == 0: identity matrix everywhere, nothing to do. + return Ok(()); + } + let all_ones = ms_mask_present == MsMaskPresent::AllOnes; + + let mut window_base = 0usize; + for g in 0..num_groups { + let wgl = ics_info.window_group_length[g] as usize; + for sfb in 0..max_sfb { + let band_on = all_ones || ms_used[g][sfb]; + if !band_on { + continue; + } + // §4.6.8.1.3: intensity is keyed on the right channel, + // noise on either channel; both suppress M/S. + if is_intensity_cb(right_sfb_cb[g][sfb]) + || is_noise_cb(left_sfb_cb[g][sfb]) + || is_noise_cb(right_sfb_cb[g][sfb]) + { + continue; + } + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + for b in 0..wgl { + let base = (window_base + b) * window_len; + for i in start..end { + let l = left[base + i]; + let r = right[base + i]; + left[base + i] = l + r; + right[base + i] = l - r; + } + } + } + window_base += wgl; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + use crate::section_data::ZERO_HCB; + + const FS_44100: u8 = 4; + + fn long_ics_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[FS_44100 as usize], + } + } + + fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { + let num_window_groups = window_group_length.len() as u8; + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups, + window_group_length, + num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[FS_44100 as usize], + } + } + + /// `sfb_cb` rows defaulting to a real spectrum book (here `2`). + fn plain_cb(num_groups: usize, max_sfb: usize) -> Vec> { + vec![vec![2u8; max_sfb]; num_groups] + } + + /// Positional wrapper over [`apply_ms_stereo`] that bundles the + /// channel pair into a [`ChannelPairSpectra`], keeping the test + /// bodies terse. + #[allow(clippy::too_many_arguments)] + fn run( + left: &mut [f64], + right: &mut [f64], + ms_mask_present: MsMaskPresent, + ms_used: &[Vec], + left_sfb_cb: &[Vec], + right_sfb_cb: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, + ) -> Result<()> { + let mut pair = ChannelPairSpectra { + left, + right, + left_sfb_cb, + right_sfb_cb, + }; + apply_ms_stereo(&mut pair, ms_mask_present, ms_used, ics_info, fs_index) + } + + #[test] + fn from_bits_roundtrip() { + for (bits, m) in [ + (0u8, MsMaskPresent::AllZeros), + (1, MsMaskPresent::Mask), + (2, MsMaskPresent::AllOnes), + ] { + assert_eq!(MsMaskPresent::from_bits(bits).unwrap(), m); + assert_eq!(m.to_bits(), bits); + } + assert!(matches!( + MsMaskPresent::from_bits(3), + Err(Error::MsStereoInvalid) + )); + } + + #[test] + fn all_zeros_is_identity() { + let ics = long_ics_info(4); + let mut l = vec![1.0f64; 1024]; + let mut r = vec![2.0f64; 1024]; + let cb = plain_cb(1, 4); + run( + &mut l, + &mut r, + MsMaskPresent::AllZeros, + &[], + &cb, + &cb, + &ics, + FS_44100, + ) + .unwrap(); + assert!(l.iter().all(|&x| x == 1.0)); + assert!(r.iter().all(|&x| x == 2.0)); + } + + #[test] + fn all_ones_dematrixes_every_band() { + // max_sfb = 2; long-window band 0 = bins 0..4, band 1 = 4..8. + let ics = long_ics_info(2); + let offsets = long_window_offsets(FS_44100).unwrap(); + assert_eq!(offsets[0], 0); + let mut l = vec![0.0f64; 1024]; + let mut r = vec![0.0f64; 1024]; + // m = 3, s = 1 in the first two bands → l' = 4, r' = 2. + let band_end = offsets[2] as usize; + for x in l.iter_mut().take(band_end) { + *x = 3.0; + } + for x in r.iter_mut().take(band_end) { + *x = 1.0; + } + let cb = plain_cb(1, 2); + run( + &mut l, + &mut r, + MsMaskPresent::AllOnes, + &[], + &cb, + &cb, + &ics, + FS_44100, + ) + .unwrap(); + for i in 0..band_end { + assert_eq!(l[i], 4.0, "l'[{i}]"); + assert_eq!(r[i], 2.0, "r'[{i}]"); + } + // Bands at/above max_sfb are untouched. + assert_eq!(l[band_end], 0.0); + assert_eq!(r[band_end], 0.0); + } + + #[test] + fn mask_gates_per_band() { + let ics = long_ics_info(2); + let offsets = long_window_offsets(FS_44100).unwrap(); + let b0 = offsets[1] as usize; // end of band 0 + let b1 = offsets[2] as usize; // end of band 1 + let mut l = vec![0.0f64; 1024]; + let mut r = vec![0.0f64; 1024]; + for x in l.iter_mut().take(b1) { + *x = 5.0; + } + for x in r.iter_mut().take(b1) { + *x = 1.0; + } + let cb = plain_cb(1, 2); + // band 0 on, band 1 off. + let ms_used = vec![vec![true, false]]; + run( + &mut l, + &mut r, + MsMaskPresent::Mask, + &ms_used, + &cb, + &cb, + &ics, + FS_44100, + ) + .unwrap(); + // band 0 de-matrixed: l'=6, r'=4. + for i in 0..b0 { + assert_eq!(l[i], 6.0); + assert_eq!(r[i], 4.0); + } + // band 1 untouched. + for i in b0..b1 { + assert_eq!(l[i], 5.0); + assert_eq!(r[i], 1.0); + } + } + + #[test] + fn intensity_band_excluded() { + let ics = long_ics_info(1); + let offsets = long_window_offsets(FS_44100).unwrap(); + let b0 = offsets[1] as usize; + let mut l = vec![3.0f64; 1024]; + let mut r = vec![1.0f64; 1024]; + let left_cb = plain_cb(1, 1); + // Right channel band 0 is intensity (15) → no de-matrix. + let mut right_cb = plain_cb(1, 1); + right_cb[0][0] = INTENSITY_HCB; + run( + &mut l, + &mut r, + MsMaskPresent::AllOnes, + &[], + &left_cb, + &right_cb, + &ics, + FS_44100, + ) + .unwrap(); + for i in 0..b0 { + assert_eq!(l[i], 3.0); + assert_eq!(r[i], 1.0); + } + } + + #[test] + fn noise_band_excluded_from_either_channel() { + let ics = long_ics_info(1); + let offsets = long_window_offsets(FS_44100).unwrap(); + let b0 = offsets[1] as usize; + // Left channel band 0 is noise (13) → no de-matrix even though + // the right channel is a real spectrum. + let mut left_cb = plain_cb(1, 1); + left_cb[0][0] = NOISE_HCB; + let right_cb = plain_cb(1, 1); + let mut l = vec![3.0f64; 1024]; + let mut r = vec![1.0f64; 1024]; + run( + &mut l, + &mut r, + MsMaskPresent::AllOnes, + &[], + &left_cb, + &right_cb, + &ics, + FS_44100, + ) + .unwrap(); + for i in 0..b0 { + assert_eq!(l[i], 3.0); + assert_eq!(r[i], 1.0); + } + } + + #[test] + fn short_window_grouping_applies_per_window() { + // Two groups: lengths [3, 5] summing to 8 short windows. + let ics = short_ics_info(2, vec![3, 5]); + let offsets = short_window_offsets(FS_44100).unwrap(); + let win = SHORT_WINDOW_LEN as usize; + let bandlen = offsets[1] as usize; // band 0 width + let mut l = vec![0.0f64; 8 * win]; + let mut r = vec![0.0f64; 8 * win]; + // Seed band 0 of every window with m=2, s=1. + for w in 0..8 { + for i in 0..bandlen { + l[w * win + i] = 2.0; + r[w * win + i] = 1.0; + } + } + let cb = plain_cb(2, 2); + // group 0 band 0 on; everything else off. + let ms_used = vec![vec![true, false], vec![false, false]]; + run( + &mut l, + &mut r, + MsMaskPresent::Mask, + &ms_used, + &cb, + &cb, + &ics, + FS_44100, + ) + .unwrap(); + // Group 0 = windows 0,1,2 → de-matrixed (l'=3, r'=1). + for w in 0..3 { + for i in 0..bandlen { + assert_eq!(l[w * win + i], 3.0, "win {w} band0"); + assert_eq!(r[w * win + i], 1.0); + } + } + // Group 1 = windows 3..8 → untouched. + for w in 3..8 { + for i in 0..bandlen { + assert_eq!(l[w * win + i], 2.0, "win {w} band0"); + assert_eq!(r[w * win + i], 1.0); + } + } + } + + #[test] + fn shape_mismatch_rejected() { + let ics = long_ics_info(2); + let cb = plain_cb(1, 2); + let mut l = vec![0.0f64; 512]; // wrong length + let mut r = vec![0.0f64; 1024]; + assert!(matches!( + run( + &mut l, + &mut r, + MsMaskPresent::AllOnes, + &[], + &cb, + &cb, + &ics, + FS_44100, + ), + Err(Error::MsStereoInvalid) + )); + } + + #[test] + fn mask_mode_requires_full_ms_used() { + let ics = long_ics_info(3); + let cb = plain_cb(1, 3); + let mut l = vec![0.0f64; 1024]; + let mut r = vec![0.0f64; 1024]; + // ms_used row too short for max_sfb = 3. + let ms_used = vec![vec![true, false]]; + assert!(matches!( + run( + &mut l, + &mut r, + MsMaskPresent::Mask, + &ms_used, + &cb, + &cb, + &ics, + FS_44100, + ), + Err(Error::MsStereoInvalid) + )); + } + + #[test] + fn dematrix_is_exactly_invertible_for_integers() { + // l' = m+s, r' = m-s recovers (m,s) = ((l'+r')/2,(l'-r')/2). + let ics = long_ics_info(1); + let offsets = long_window_offsets(FS_44100).unwrap(); + let b0 = offsets[1] as usize; + let cb = plain_cb(1, 1); + let mut l = vec![0.0f64; 1024]; + let mut r = vec![0.0f64; 1024]; + for i in 0..b0 { + l[i] = (i as f64) * 0.5 - 7.0; // m + r[i] = 3.0 - (i as f64) * 0.25; // s + } + let m: Vec = l[..b0].to_vec(); + let s: Vec = r[..b0].to_vec(); + run( + &mut l, + &mut r, + MsMaskPresent::AllOnes, + &[], + &cb, + &cb, + &ics, + FS_44100, + ) + .unwrap(); + for i in 0..b0 { + assert_eq!(l[i], m[i] + s[i]); + assert_eq!(r[i], m[i] - s[i]); + } + } + + #[test] + fn zero_hcb_bands_still_dematrix() { + // A ZERO_HCB band carries no transmitted spectrum but is not + // intensity/noise, so the M/S guard does not exclude it (its + // coefficients are simply 0 on both sides → stays 0). + let ics = long_ics_info(1); + let offsets = long_window_offsets(FS_44100).unwrap(); + let b0 = offsets[1] as usize; + let mut left_cb = plain_cb(1, 1); + left_cb[0][0] = ZERO_HCB; + let mut right_cb = plain_cb(1, 1); + right_cb[0][0] = ZERO_HCB; + let mut l = vec![0.0f64; 1024]; + let mut r = vec![0.0f64; 1024]; + run( + &mut l, + &mut r, + MsMaskPresent::AllOnes, + &[], + &left_cb, + &right_cb, + &ics, + FS_44100, + ) + .unwrap(); + for i in 0..b0 { + assert_eq!(l[i], 0.0); + assert_eq!(r[i], 0.0); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/pce.rs b/crates/vendor/oxideav-aac/src/pce.rs new file mode 100644 index 00000000..eb0cbbd3 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/pce.rs @@ -0,0 +1,440 @@ +//! `program_config_element()` parser. +//! +//! ISO/IEC 14496-3 §4.4.1.1 Table 4.2 (identical to ISO/IEC 13818-7 +//! §8.5 Table 25 modulo the field rename `profile` → `object_type`). +//! A PCE describes a custom channel layout — element ordering, per- +//! element CPE/SCE selection, mix-down hints, and a free-form +//! comment field. It is emitted either: +//! +//! * **As the first element of a `raw_data_block()`** (`id_syn_ele == +//! PCE`), when the `channelConfiguration` is one of 1..=7 *and* the +//! encoder wants to override the implicit element layout. +//! * **Inline in [`AudioSpecificConfig`](crate::asc::AudioSpecificConfig)** +//! when `channelConfiguration == 0`. In that case the PCE has no +//! surrounding `id_syn_ele` prefix and the byte-alignment Note 1 +//! on Table 4.2 applies *relative to the start of the +//! `AudioSpecificConfig`*, not to the absolute byte position in +//! the bitstream. +//! +//! Phase 1 retains the entire PCE structure verbatim (every wire +//! field is preserved) so a later round can validate channel layouts +//! and matrix mix-down semantics without re-parsing. +//! +//! ## Byte alignment +//! +//! The `byte_alignment()` call inside Table 4.2 follows the final +//! `valid_cc_element_tag_select[i]` loop. The position to align *to* +//! depends on the call site: +//! +//! * **Standalone PCE in `raw_data_block()`** ⇒ align to the next +//! absolute byte boundary of the bit-reader. +//! * **PCE inline in `AudioSpecificConfig`** ⇒ align to the next byte +//! boundary *relative to the start of the ASC*. Since the ASC +//! itself usually starts on a byte boundary in the carrying +//! container (`esds` payload, LATM `StreamMuxConfig`, etc.), the +//! two definitions usually coincide; they differ only when the +//! ASC was started at a non-zero bit offset inside a larger +//! bit-stream. [`Pce::parse`] takes a `relative_origin_bit` +//! parameter that the caller passes when the ASC origin is not at +//! the bit-reader's current zero — see +//! [`AudioSpecificConfig`](crate::asc::AudioSpecificConfig) for +//! the ASC-origin handling. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::{Error, Result}; + +/// One entry in a per-element list (`front_element_*`, `side_*`, +/// `back_*`) of a PCE. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ElementSelect { + /// `true` ⇔ the element at this slot is a CPE (channel-pair + /// element); `false` ⇔ SCE (single-channel element). Matches the + /// `*_element_is_cpe[i]` wire bit. + pub is_cpe: bool, + /// 4-bit `*_element_tag_select[i]` — the + /// `element_instance_tag` value the matching SCE/CPE will carry + /// inside the `raw_data_block()`. + pub tag_select: u8, +} + +/// One entry in the `valid_cc_element_*` list of a PCE. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CcElementSelect { + /// `true` ⇔ the coupling channel element is independently + /// switched (`cc_element_is_ind_sw[i] == 1`). + pub is_ind_sw: bool, + /// 4-bit `valid_cc_element_tag_select[i]` — the tag the matching + /// CCE will carry inside the `raw_data_block()`. + pub tag_select: u8, +} + +/// Parsed `program_config_element()` (Table 4.2). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pce { + /// 4-bit `element_instance_tag`. + pub element_instance_tag: u8, + /// 2-bit `object_type` (ISO/IEC 14496-3) — synonym of `profile` + /// in ISO/IEC 13818-7. `0` = Main, `1` = LC, `2` = SSR, `3` = + /// LTP. Note this is **the same scheme as ADTS** (one less than + /// the `audioObjectType` defined by Table 1.16). + pub object_type: u8, + /// 4-bit `sampling_frequency_index` (Table 1.18). The PCE is + /// allowed to override the surrounding context's + /// `samplingFrequencyIndex`; in practice the wire value usually + /// matches the ASC / ADTS value. + pub sampling_frequency_index: u8, + /// `num_front_channel_elements` × [`ElementSelect`]. + pub front_elements: Vec, + /// `num_side_channel_elements` × [`ElementSelect`]. + pub side_elements: Vec, + /// `num_back_channel_elements` × [`ElementSelect`]. + pub back_elements: Vec, + /// `num_lfe_channel_elements` × `lfe_element_tag_select[i]` + /// (4-bit each). + pub lfe_element_tag_selects: Vec, + /// `num_assoc_data_elements` × `assoc_data_element_tag_select[i]` + /// (4-bit each). + pub assoc_data_tag_selects: Vec, + /// `num_valid_cc_elements` × [`CcElementSelect`]. + pub valid_cc_elements: Vec, + /// `mono_mixdown_element_number` (4 bits) if + /// `mono_mixdown_present == 1`. + pub mono_mixdown_element_number: Option, + /// `stereo_mixdown_element_number` (4 bits) if + /// `stereo_mixdown_present == 1`. + pub stereo_mixdown_element_number: Option, + /// `(matrix_mixdown_idx, pseudo_surround_enable)` if + /// `matrix_mixdown_idx_present == 1`. + pub matrix_mixdown: Option<(u8, bool)>, + /// `comment_field_bytes` raw bytes (after the `byte_alignment()` + /// and the 8-bit `comment_field_bytes` length prefix). + pub comment_field: Vec, +} + +impl Pce { + /// Parse a PCE starting at the current bit-reader position. The + /// `byte_alignment()` clause inside Table 4.2 will align the + /// reader to the next byte boundary whose *absolute* bit + /// position is a multiple of 8 plus `origin_bit_offset`. Pass + /// `0` for a standalone PCE inside a `raw_data_block()`; pass + /// the ASC origin bit-position for a PCE inline in + /// `AudioSpecificConfig` (see module docs). + pub fn parse(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result { + // 4 + 2 + 4 = 10 bits of header + let element_instance_tag = read_u8(reader, 4)?; + let object_type = read_u8(reader, 2)?; + let sampling_frequency_index = read_u8(reader, 4)?; + + // Element counts. + let n_front = read_u8(reader, 4)? as usize; + let n_side = read_u8(reader, 4)? as usize; + let n_back = read_u8(reader, 4)? as usize; + let n_lfe = read_u8(reader, 2)? as usize; + let n_assoc = read_u8(reader, 3)? as usize; + let n_cc = read_u8(reader, 4)? as usize; + + // Mix-down presence + bodies. + let mono_mixdown_present = read_bit(reader)?; + let mono_mixdown_element_number = if mono_mixdown_present { + Some(read_u8(reader, 4)?) + } else { + None + }; + let stereo_mixdown_present = read_bit(reader)?; + let stereo_mixdown_element_number = if stereo_mixdown_present { + Some(read_u8(reader, 4)?) + } else { + None + }; + let matrix_mixdown_idx_present = read_bit(reader)?; + let matrix_mixdown = if matrix_mixdown_idx_present { + let idx = read_u8(reader, 2)?; + let pseudo = read_bit(reader)?; + Some((idx, pseudo)) + } else { + None + }; + + // Element lists. + let front_elements = read_element_selects(reader, n_front)?; + let side_elements = read_element_selects(reader, n_side)?; + let back_elements = read_element_selects(reader, n_back)?; + + let mut lfe_element_tag_selects = Vec::with_capacity(n_lfe); + for _ in 0..n_lfe { + lfe_element_tag_selects.push(read_u8(reader, 4)?); + } + let mut assoc_data_tag_selects = Vec::with_capacity(n_assoc); + for _ in 0..n_assoc { + assoc_data_tag_selects.push(read_u8(reader, 4)?); + } + let mut valid_cc_elements = Vec::with_capacity(n_cc); + for _ in 0..n_cc { + let is_ind_sw = read_bit(reader)?; + let tag_select = read_u8(reader, 4)?; + valid_cc_elements.push(CcElementSelect { + is_ind_sw, + tag_select, + }); + } + + // §4.4.1.1 Note 1: byte_alignment() relative to the PCE's + // origin reference. The "next byte boundary" is determined + // by the absolute reader position minus `origin_bit_offset` + // — when the offset is 0, this collapses to the standard + // `align_to_byte()`. + align_relative_to_origin(reader, origin_bit_offset)?; + + let comment_field_bytes = read_u8(reader, 8)? as usize; + let mut comment_field = Vec::with_capacity(comment_field_bytes); + for _ in 0..comment_field_bytes { + comment_field.push(read_u8(reader, 8)?); + } + + Ok(Pce { + element_instance_tag, + object_type, + sampling_frequency_index, + front_elements, + side_elements, + back_elements, + lfe_element_tag_selects, + assoc_data_tag_selects, + valid_cc_elements, + mono_mixdown_element_number, + stereo_mixdown_element_number, + matrix_mixdown, + comment_field, + }) + } + + /// Total channel count implied by this PCE — sums one channel + /// for each SCE entry and two channels for each CPE entry across + /// front/side/back lists, plus one channel per LFE entry. (CCEs + /// are coupling buses and do not contribute to the output + /// channel count.) + pub fn channel_count(&self) -> usize { + let count_list = |list: &[ElementSelect]| -> usize { + list.iter().map(|e| if e.is_cpe { 2 } else { 1 }).sum() + }; + count_list(&self.front_elements) + + count_list(&self.side_elements) + + count_list(&self.back_elements) + + self.lfe_element_tag_selects.len() + } + + /// Encode this PCE into `writer` per ISO/IEC 14496-3 §4.4.1.1 + /// Table 4.2 — the bit-exact inverse of [`Pce::parse`]. The + /// emitted layout matches the parser exactly: + /// + /// `element_instance_tag(4) + object_type(2) + + /// sampling_frequency_index(4) + num_front(4) + num_side(4) + + /// num_back(4) + num_lfe(2) + num_assoc(3) + num_valid_cc(4) + + /// mono_mixdown_present(1) [+ mono_mixdown_element_number(4)] + + /// stereo_mixdown_present(1) [+ stereo_mixdown_element_number(4)] + + /// matrix_mixdown_idx_present(1) [+ matrix_mixdown_idx(2) + + /// pseudo_surround_enable(1)] + front[i].is_cpe(1) + + /// front[i].tag_select(4) ... + side[...] + back[...] + + /// lfe[i].tag_select(4) + assoc[i].tag_select(4) + + /// cc[i].is_ind_sw(1) + cc[i].tag_select(4) + byte_alignment() + + /// comment_field_bytes(8) + comment_field bytes` + /// + /// `origin_bit_offset` controls the Table 4.2 Note 1 + /// `byte_alignment()` semantics — pass `0` for a standalone PCE + /// inside a `raw_data_block()` (align to the absolute byte + /// boundary of the writer), and the ASC origin bit-position for + /// a PCE inline in [`AudioSpecificConfig`](crate::asc::AudioSpecificConfig) + /// (align relative to the ASC origin). Note that when the writer + /// itself starts at bit 0 (the standalone case) the absolute and + /// the origin-relative alignments coincide. + /// + /// Returns [`Error::PceEncodeInvalid`] when any wire field + /// overflows its bit-width — see [`Error::PceEncodeInvalid`] for + /// the exhaustive list. + pub fn write(&self, writer: &mut BitWriter, origin_bit_offset: u64) -> Result<()> { + // ----- Header fields ----- + // 4-bit element_instance_tag, 2-bit object_type, 4-bit + // sampling_frequency_index. + if self.element_instance_tag > 0x0f + || self.object_type > 0x03 + || self.sampling_frequency_index > 0x0f + { + return Err(Error::PceEncodeInvalid); + } + + // ----- Element counts: validate against field widths first + // so a single overflow surfaces before any bits leak onto + // the wire. + if self.front_elements.len() > 0x0f + || self.side_elements.len() > 0x0f + || self.back_elements.len() > 0x0f + || self.lfe_element_tag_selects.len() > 0x03 + || self.assoc_data_tag_selects.len() > 0x07 + || self.valid_cc_elements.len() > 0x0f + { + return Err(Error::PceEncodeInvalid); + } + + // ----- Per-element tag_select field-width checks (4 bits + // each) and mix-down field-width checks. Doing the checks + // up-front avoids emitting a partial PCE on a downstream + // overflow. + for e in self + .front_elements + .iter() + .chain(self.side_elements.iter()) + .chain(self.back_elements.iter()) + { + if e.tag_select > 0x0f { + return Err(Error::PceEncodeInvalid); + } + } + for &t in self + .lfe_element_tag_selects + .iter() + .chain(self.assoc_data_tag_selects.iter()) + { + if t > 0x0f { + return Err(Error::PceEncodeInvalid); + } + } + for cc in &self.valid_cc_elements { + if cc.tag_select > 0x0f { + return Err(Error::PceEncodeInvalid); + } + } + if let Some(n) = self.mono_mixdown_element_number { + if n > 0x0f { + return Err(Error::PceEncodeInvalid); + } + } + if let Some(n) = self.stereo_mixdown_element_number { + if n > 0x0f { + return Err(Error::PceEncodeInvalid); + } + } + if let Some((idx, _)) = self.matrix_mixdown { + if idx > 0x03 { + return Err(Error::PceEncodeInvalid); + } + } + if self.comment_field.len() > 0xff { + return Err(Error::PceEncodeInvalid); + } + + // ----- Emit header ----- + writer.write_u32(self.element_instance_tag as u32, 4); + writer.write_u32(self.object_type as u32, 2); + writer.write_u32(self.sampling_frequency_index as u32, 4); + writer.write_u32(self.front_elements.len() as u32, 4); + writer.write_u32(self.side_elements.len() as u32, 4); + writer.write_u32(self.back_elements.len() as u32, 4); + writer.write_u32(self.lfe_element_tag_selects.len() as u32, 2); + writer.write_u32(self.assoc_data_tag_selects.len() as u32, 3); + writer.write_u32(self.valid_cc_elements.len() as u32, 4); + + // ----- Mix-down presence + bodies ----- + match self.mono_mixdown_element_number { + Some(n) => { + writer.write_bit(true); + writer.write_u32(n as u32, 4); + } + None => writer.write_bit(false), + } + match self.stereo_mixdown_element_number { + Some(n) => { + writer.write_bit(true); + writer.write_u32(n as u32, 4); + } + None => writer.write_bit(false), + } + match self.matrix_mixdown { + Some((idx, pseudo)) => { + writer.write_bit(true); + writer.write_u32(idx as u32, 2); + writer.write_bit(pseudo); + } + None => writer.write_bit(false), + } + + // ----- Element lists ----- + for e in &self.front_elements { + writer.write_bit(e.is_cpe); + writer.write_u32(e.tag_select as u32, 4); + } + for e in &self.side_elements { + writer.write_bit(e.is_cpe); + writer.write_u32(e.tag_select as u32, 4); + } + for e in &self.back_elements { + writer.write_bit(e.is_cpe); + writer.write_u32(e.tag_select as u32, 4); + } + for &t in &self.lfe_element_tag_selects { + writer.write_u32(t as u32, 4); + } + for &t in &self.assoc_data_tag_selects { + writer.write_u32(t as u32, 4); + } + for cc in &self.valid_cc_elements { + writer.write_bit(cc.is_ind_sw); + writer.write_u32(cc.tag_select as u32, 4); + } + + // ----- Table 4.2 Note 1 byte_alignment() — relative to the + // PCE's origin reference. The pad is `(8 - from_origin % 8) % + // 8` where `from_origin = writer.bit_position() - + // origin_bit_offset`. For a standalone PCE inside + // `raw_data_block()` the caller passes `origin_bit_offset = + // 0` and this collapses to absolute alignment; for an + // ASC-inline PCE the caller passes the ASC origin so the + // alignment is computed relative to the start of the ASC. + let cur = writer.bit_position(); + let from_origin = cur.saturating_sub(origin_bit_offset); + let pad = (8 - (from_origin % 8)) % 8; + if pad > 0 { + writer.write_u32(0, pad as u32); + } + + // ----- comment_field ----- + writer.write_u32(self.comment_field.len() as u32, 8); + for &b in &self.comment_field { + writer.write_byte(b); + } + Ok(()) + } +} + +fn read_element_selects(reader: &mut BitReader<'_>, n: usize) -> Result> { + let mut out = Vec::with_capacity(n); + for _ in 0..n { + let is_cpe = read_bit(reader)?; + let tag_select = read_u8(reader, 4)?; + out.push(ElementSelect { is_cpe, tag_select }); + } + Ok(out) +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} + +/// Align the reader to the next byte boundary measured from +/// `origin_bit_offset`. Equivalent to `align_to_byte()` when +/// `origin_bit_offset == 0`. +fn align_relative_to_origin(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result<()> { + let cur = reader.bit_position(); + let from_origin = cur.saturating_sub(origin_bit_offset); + let pad = (8 - (from_origin % 8)) % 8; + if pad == 0 { + return Ok(()); + } + reader.skip(pad as u32).map_err(|_| Error::UnexpectedEnd)?; + Ok(()) +} diff --git a/crates/vendor/oxideav-aac/src/pcm.rs b/crates/vendor/oxideav-aac/src/pcm.rs new file mode 100644 index 00000000..4e87df3e --- /dev/null +++ b/crates/vendor/oxideav-aac/src/pcm.rs @@ -0,0 +1,198 @@ +//! §4.6.11 time-domain output → integer PCM rendering. +//! +//! The §4.6.11 filterbank ([`crate::filterbank`]) emits one channel's +//! reconstructed time signal as `f64` samples already scaled to the +//! 16-bit full-scale amplitude domain (the `2/N` IMDCT normalisation +//! plus the §4.6.2.3.3 scalefactor gain land the dequantised, windowed, +//! overlap-added output directly on the `±32768` axis). This module +//! turns that floating-point time signal into the integer-PCM +//! representation a sink consumes, and interleaves a frame's channels. +//! +//! Two operations live here, both fully spec-determined: +//! +//! 1. **Rounding to the nearest integer.** ISO/IEC 14496-3 §1.3 defines +//! the `NINT()` nearest-integer operator as *"Returns the nearest +//! integer value to the real-valued argument. Half-integer values are +//! rounded away from zero."* [`nint`] implements exactly that +//! (`floor(x + 0.5)` for `x ≥ 0`, `ceil(x - 0.5)` for `x < 0`), which +//! is the same tie-breaking rule the spec's `//` rounded-division and +//! every other `NINT`-quoting clause use. +//! +//! 2. **Saturation to the output word.** A 16-bit signed sink represents +//! `-32768 ..= 32767`; a sample whose magnitude overshoots that range +//! (possible only on a clipped / full-scale input) saturates to the +//! nearest representable extreme rather than wrapping. [`to_s16`] +//! clamps after rounding. +//! +//! The conversion is *the only* output-rendering step the crate applies: +//! there is no resampler, no dither, and no channel remap. The integer +//! samples are produced in the filterbank's own time order; the optional +//! [`interleave_s16`] helper packs a frame's per-channel buffers into the +//! element-order interleaved layout a multi-channel sink expects. +//! +//! ## Provenance +//! +//! Every constant and rule here is from ISO/IEC 14496-3 (the §1.3 +//! arithmetic-operator definitions and the §4.6.11 filterbank output +//! contract) staged under `docs/audio/aac/`. The full-scale `±32768` +//! amplitude domain is the filterbank's documented output scale (the +//! `2/N` IMDCT factor of [`crate::filterbank`]); this module adds only +//! the spec's `NINT()` rounding and the integer-word saturation. + +use crate::{Error, Result}; + +/// The most negative value a 16-bit signed PCM word can hold. +pub const S16_MIN: i32 = -32768; +/// The most positive value a 16-bit signed PCM word can hold. +pub const S16_MAX: i32 = 32767; + +/// ISO/IEC 14496-3 §1.3 `NINT()` — round a real value to the nearest +/// integer, with half-integers rounded **away from zero**. +/// +/// `NINT(2.5) == 3`, `NINT(-2.5) == -3`, `NINT(2.4) == 2`, +/// `NINT(-2.4) == -2`. A non-finite input (`NaN` / `±∞`) has no nearest +/// integer; it returns `0.0` so a downstream cast cannot trap (the +/// filterbank never emits non-finite output for a well-formed stream, +/// but a hostile bitstream must not be able to poison the PCM cast). +#[must_use] +pub fn nint(x: f64) -> f64 { + if !x.is_finite() { + return 0.0; + } + if x >= 0.0 { + (x + 0.5).floor() + } else { + (x - 0.5).ceil() + } +} + +/// Render one filterbank time-domain sample to a saturating 16-bit +/// signed PCM word. +/// +/// Applies the §1.3 [`nint`] rounding then clamps to +/// [`S16_MIN`]`..=`[`S16_MAX`]. The clamp is a no-op for the +/// well-below-full-scale output of a dequantised LC stream; it only +/// engages on a clipped / full-scale signal whose rounded magnitude +/// would overflow the 16-bit word. +#[must_use] +pub fn to_s16(sample: f64) -> i16 { + nint(sample).clamp(S16_MIN as f64, S16_MAX as f64) as i16 +} + +/// Render a whole channel's time signal to 16-bit PCM in place order, +/// returning a fresh `Vec` of the same length. +#[must_use] +pub fn channel_to_s16(samples: &[f64]) -> Vec { + samples.iter().copied().map(to_s16).collect() +} + +/// Interleave a frame's per-channel time signals into the element-order +/// interleaved 16-bit PCM layout a multi-channel sink consumes. +/// +/// `channels[c][n]` is channel `c`'s sample `n`; the output is +/// `out[n * num_channels + c] = to_s16(channels[c][n])`. Every channel +/// buffer must be the same length (the §4.6.11 per-frame sample count, +/// `1024` for the 1024-line transform family); a length disagreement is +/// rejected with [`Error::PcmInvalid`]. An empty channel list yields an +/// empty buffer. +pub fn interleave_s16(channels: &[Vec]) -> Result> { + if channels.is_empty() { + return Ok(Vec::new()); + } + let frame_len = channels[0].len(); + if channels.iter().any(|c| c.len() != frame_len) { + return Err(Error::PcmInvalid); + } + let num_channels = channels.len(); + let mut out = Vec::with_capacity(frame_len * num_channels); + for n in 0..frame_len { + for ch in channels { + out.push(to_s16(ch[n])); + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nint_rounds_half_away_from_zero() { + // §1.3: half-integers round away from zero. + assert_eq!(nint(2.5), 3.0); + assert_eq!(nint(-2.5), -3.0); + assert_eq!(nint(0.5), 1.0); + assert_eq!(nint(-0.5), -1.0); + assert_eq!(nint(1.5), 2.0); + assert_eq!(nint(-1.5), -2.0); + } + + #[test] + fn nint_rounds_non_halves_to_nearest() { + assert_eq!(nint(2.4), 2.0); + assert_eq!(nint(2.6), 3.0); + assert_eq!(nint(-2.4), -2.0); + assert_eq!(nint(-2.6), -3.0); + assert_eq!(nint(0.0), 0.0); + assert_eq!(nint(-0.0), 0.0); + } + + #[test] + fn nint_non_finite_is_zero() { + assert_eq!(nint(f64::NAN), 0.0); + assert_eq!(nint(f64::INFINITY), 0.0); + assert_eq!(nint(f64::NEG_INFINITY), 0.0); + } + + #[test] + fn to_s16_saturates() { + assert_eq!(to_s16(0.0), 0); + assert_eq!(to_s16(100.4), 100); + assert_eq!(to_s16(100.5), 101); + assert_eq!(to_s16(-100.5), -101); + // Beyond full scale clamps, not wraps. + assert_eq!(to_s16(40000.0), S16_MAX as i16); + assert_eq!(to_s16(-40000.0), S16_MIN as i16); + // The exact extremes round-trip. + assert_eq!(to_s16(32767.0), 32767); + assert_eq!(to_s16(-32768.0), -32768); + // 32767.5 rounds away from zero to 32768 then clamps to 32767. + assert_eq!(to_s16(32767.5), 32767); + // -32768.5 rounds to -32769 then clamps to -32768. + assert_eq!(to_s16(-32768.5), -32768); + } + + #[test] + fn channel_to_s16_maps_each_sample() { + let got = channel_to_s16(&[0.0, 1.4, 1.5, -1.5, 50000.0]); + assert_eq!(got, vec![0, 1, 2, -2, S16_MAX as i16]); + } + + #[test] + fn interleave_two_channels() { + let l = vec![0.0, 10.0, 20.0]; + let r = vec![1.0, 11.0, 21.0]; + let got = interleave_s16(&[l, r]).unwrap(); + assert_eq!(got, vec![0, 1, 10, 11, 20, 21]); + } + + #[test] + fn interleave_single_channel_is_identity_order() { + let mono = vec![3.4, 3.5, -3.5]; + let got = interleave_s16(&[mono]).unwrap(); + assert_eq!(got, vec![3, 4, -4]); + } + + #[test] + fn interleave_empty_is_empty() { + assert!(interleave_s16(&[]).unwrap().is_empty()); + } + + #[test] + fn interleave_rejects_length_mismatch() { + let l = vec![0.0, 1.0]; + let r = vec![0.0, 1.0, 2.0]; + assert!(matches!(interleave_s16(&[l, r]), Err(Error::PcmInvalid))); + } +} diff --git a/crates/vendor/oxideav-aac/src/pns.rs b/crates/vendor/oxideav-aac/src/pns.rs new file mode 100644 index 00000000..742ceb79 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/pns.rs @@ -0,0 +1,701 @@ +//! §4.6.13 Perceptual Noise Substitution (PNS) synthesis — ISO/IEC +//! 14496-3. +//! +//! PNS replaces the Huffman-coded / inverse-quantised spectrum of a +//! noise-like scalefactor band with a freshly generated random vector +//! scaled to a transmitted target energy. It is the third channel / +//! noise tool in the §4.6 decode chain (after M/S and before intensity +//! stereo), signalled by the pseudo codebook `NOISE_HCB` (13) in a +//! band's `sfb_cb`. Because no spectral coefficients are transmitted +//! for such a band (during Huffman decoding `NOISE_HCB` is treated +//! exactly like `ZERO_HCB`, §4.6.13.5), the in-band coefficients arrive +//! as silence and this tool fills them. +//! +//! ## §4.6.13.3 decoding process +//! +//! The energy of a noise band is carried by `noise_nrg[g][sfb]` — a +//! value coded *exactly like a scalefactor* (Huffman-DPCM, with the +//! first PNS band of the frame sent as a 9-bit literal) on its own DPCM +//! track seeded at `global_gain - NOISE_OFFSET - 256` +//! (`NOISE_OFFSET == 90`). That accumulation is the upstream job of +//! [`crate::scale_factor_data::accumulate`]; this module consumes the +//! absolute `noise_nrg[g][sfb]` it produces. +//! +//! The per-band synthesis (ISO/IEC 14496-3:2009 §4.6.13.3 pseudo code) +//! is: +//! +//! ```text +//! size = swb_offset[sfb+1] - swb_offset[sfb]; +//! gen_rand_vector(&spec[..], size); /* random vector */ +//! nrg = 0; for i in 0..size { nrg += spec[i]*spec[i]; } +//! sqrt_nrg = sqrt(nrg); +//! scale *= 2.0^(0.25 * noise_nrg[g][sfb]) / sqrt_nrg; +//! for i in 0..size { spec[i] *= scale; } +//! ``` +//! +//! The 2009 revision normalises by the **measured** energy of the +//! generated vector (`sqrt_nrg = sqrt(Σ spec²)`) rather than by an +//! assumed per-sample average energy `MEAN_NRG` (the 2001 form). This +//! removes the dependence on any particular generator's variance: the +//! band that comes out has L2 norm +//! +//! ```text +//! ‖spec‖₂ = sqrt(Σ (spec[i]·scale)²) +//! = scale · sqrt_nrg +//! = 2.0^(0.25 · noise_nrg[g][sfb]) +//! ``` +//! +//! exactly, independent of which random vector was drawn (as long as it +//! is non-zero). The target energy is therefore **spec-determined and +//! deterministic**; only the per-coefficient *phase* of the band is a +//! function of the generator, which the standard deliberately leaves +//! open ("a suitable random number generator can be realized using one +//! multiplication/accumulation per random value", §4.6.13.3). The +//! `2.0^(0.25·noise_nrg)` energy ladder is the same per-quarter-step +//! gain as the §4.6.2.3.3 scalefactor gain. +//! +//! ## Generator +//! +//! [`gen_rand_vector`] is the default generator: a 32-bit +//! multiply-accumulate LCG mapped to signed `f64` values in +//! `[-1.0, 1.0)`, one multiply-accumulate per coefficient as the spec +//! suggests. Because the final scaling normalises away the generator's +//! amplitude, only its *zero-sum-of-squares-avoidance* matters for +//! correctness (a band of length ≥ 1 from this generator always has a +//! non-zero sum of squares). [`apply_pns`] takes the generator as a +//! closure so a caller can substitute a different (e.g. bit-exact +//! reference) source without changing the synthesis maths. +//! +//! The staged clean-room analysis +//! (`docs/audio/aac/pns-gen-rand-vector.md`) pins down exactly how +//! far the standard constrains this tool: the band selection, the +//! noise-energy DPCM, the `2^(0.25·noise_nrg)` target energy, the +//! 2009 measured-energy normalisation, and the correlated-CPE +//! same-vector rule are all normative (and implemented here), while +//! the generator's recurrence, seed, word→coefficient mapping, and +//! state-threading order are **deliberately unspecified** — the spec +//! demands only signed values with a non-zero sum of squares, one +//! multiply-accumulate each. Byte-exact PCM against any *particular* +//! reference decoder's PNS output would require replicating that +//! decoder's exact generator and threading order, which no document +//! can pin; cross-decoder PNS validation is therefore an +//! energy-domain comparison by design (the fixtures-doc §8 check), +//! with the per-bin noise *phase* implementation-defined. The LCG +//! below realises the doc's example recurrence +//! (`state·1664525 + 1013904223`), one of the admissible family. +//! +//! ## §4.6.13.3 channel-pair correlation (`ms_used`) +//! +//! For a channel pair, if the **same** `(group, sfb)` is `NOISE_HCB` in +//! **both** channels and the band's `ms_used` bit is set (or +//! `ms_mask_present == 2`), the *same* random vector is used for both +//! channels (correlated noise); otherwise each channel draws its own +//! (independent noise). No M/S de-matrix is applied to such a band — +//! PNS and M/S are mutually exclusive (§4.6.13.5), so a set `ms_used` +//! bit on a both-channels-noise band selects the shared vector rather +//! than an M/S reconstruction. [`apply_pns_pair`] implements this. +//! +//! ## Decoder block order +//! +//! Per §4.6 the noise components are injected into the output spectrum +//! **prior to** the §4.6.9 TNS step (§4.6.13.5), on the de-interleaved, +//! window-major spectrum produced by +//! [`crate::decoded_spectrum::quant_to_spec`] +//! (`spec[w * window_len + k]`). The per-band coefficient extent +//! `swb_offset[sfb+1]-swb_offset[sfb]` and the +//! `(group, in-group window) → absolute window` mapping match the rest +//! of the pipeline. +//! +//! ## Scope +//! +//! This module is the §4.6.13.3 forward (`global_gain`-seeded) noise +//! synthesis only. The RVLC backward-DPCM noise-energy decode +//! (§4.6.13.3, error-resilient profiles) and the scalable-coder +//! integration (§4.6.13.6) are separate follow-ups; the `noise_nrg` +//! track itself is produced upstream by +//! [`crate::scale_factor_data::accumulate`]. + +use crate::ics_info::IcsInfo; +use crate::section_data::NOISE_HCB; +#[cfg(test)] +use crate::swb_offset::{long_window_offsets, LONG_WINDOW_LEN}; +use crate::{Error, Result}; + +/// §4.6.13.3 `is_noise(group,sfb)` — the noise-band predicate. +/// +/// `true` ⇔ the band's `sfb_cb` is `NOISE_HCB` (13); the band carries a +/// `noise_nrg` energy in place of a scalefactor and no spectral +/// coefficients, and is filled by this tool. +pub fn is_noise(cb: u8) -> bool { + cb == NOISE_HCB +} + +/// §4.6.13.3 `2.0^(0.25 * noise_nrg)` — the target L2 norm of a noise +/// band. +/// +/// The same per-quarter-step gain ladder as the §4.6.2.3.3 scalefactor +/// gain `2^(0.25·(sf−100))`; the absolute `noise_nrg[g][sfb]` plays the +/// role of a scalefactor for the noise energy. After +/// measured-energy normalisation a synthesised band has exactly this +/// L2 norm. +pub fn noise_target_norm(noise_nrg: i32) -> f64 { + 2.0f64.powf(0.25 * noise_nrg as f64) +} + +/// Default `gen_rand_vector(addr, size)` (§4.6.13.3): fill `out` with +/// `out.len()` signed pseudo-random values in `[-1.0, 1.0)` using one +/// multiply-accumulate per value. +/// +/// `state` is the generator's running 32-bit register; pass the same +/// `&mut state` across calls within a frame so independent bands draw +/// independent vectors. The amplitude is irrelevant to the final +/// output — [`apply_pns`] normalises by the measured energy — so this +/// only needs to produce a vector whose sum of squares is non-zero, +/// which it always does for a non-empty band. +/// +/// The recurrence is a 32-bit linear congruential step +/// (`state = state * 1664525 + 1013904223`, both wrapping) whose high +/// bits are mapped to a signed fraction. This is one of the "suitable +/// random number generators" the spec admits; callers needing a +/// different source pass their own closure to [`apply_pns`]. +pub fn gen_rand_vector(out: &mut [f64], state: &mut u32) { + for v in out.iter_mut() { + // One multiply-accumulate per random value (§4.6.13.3). + *state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + // Map the 32-bit register to a signed fraction in [-1, 1). + // (state as i32) ranges over the full signed 32-bit interval; + // dividing by 2^31 yields [-1.0, 1.0). + *v = (*state as i32) as f64 / 2_147_483_648.0; + } +} + +/// Synthesise one PNS band in place from a pre-filled random vector. +/// +/// `band` is the generated random vector (length `size`); on return it +/// holds the energy-normalised noise band whose L2 norm is exactly +/// `2.0^(0.25 · noise_nrg)` (§4.6.13.3, 2009 measured-energy form). +/// +/// If the random vector is all-zero (sum of squares zero) the band +/// cannot be normalised; per §4.6.13.3 a suitable generator yields a +/// non-zero sum of squares, so this leaves an all-zero band untouched +/// (the only energy-preserving choice) rather than dividing by zero. +fn normalise_band(band: &mut [f64], noise_nrg: i32) { + let nrg: f64 = band.iter().map(|&x| x * x).sum(); + if nrg <= 0.0 { + return; + } + let sqrt_nrg = nrg.sqrt(); + let scale = noise_target_norm(noise_nrg) / sqrt_nrg; + for x in band.iter_mut() { + *x *= scale; + } +} + +/// A single channel's de-interleaved spectrum plus the per-band +/// codebooks and noise energies the §4.6.13.3 synthesis needs. +/// +/// `spec` is the window-major decoded spectrum +/// (`num_windows × window_len`) produced by +/// [`crate::decoded_spectrum::quant_to_spec`]; noise bands arrive as +/// silence and are overwritten in place. `sfb_cb[g][sfb]` selects which +/// bands are noise-coded; `noise_nrg[g][sfb]` is the absolute energy +/// (§4.6.13.3) for each noise band (consulted only where +/// `sfb_cb == NOISE_HCB`). +#[derive(Debug)] +pub struct PnsChannel<'a> { + /// Window-major channel spectrum; noise bands overwritten in place. + pub spec: &'a mut [f64], + /// Per-band `sfb_cb[g][sfb]`. + pub sfb_cb: &'a [Vec], + /// Absolute `noise_nrg[g][sfb]` (§4.6.13.3). + pub noise_nrg: &'a [Vec], +} + +/// Validate that a channel's spectrum / `sfb_cb` / `noise_nrg` shapes +/// agree with `ics_info`, returning the window geometry. +fn channel_geometry<'a>( + spec_len: usize, + sfb_cb: &[Vec], + noise_nrg: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, +) -> Result<(usize, &'a [u16])> { + let window_len = ics_info.window_len()?; + let offsets = ics_info.swb_offsets(fs_index)?; + let num_swb = offsets.len() - 1; + let num_windows = ics_info.num_windows as usize; + let num_groups = ics_info.num_window_groups as usize; + let max_sfb = ics_info.max_sfb as usize; + + if spec_len != num_windows * window_len { + return Err(Error::PnsInvalid); + } + if ics_info.window_group_length.len() != num_groups + || ics_info + .window_group_length + .iter() + .map(|&w| w as usize) + .sum::() + != num_windows + { + return Err(Error::PnsInvalid); + } + if max_sfb > num_swb { + return Err(Error::PnsInvalid); + } + if sfb_cb.len() != num_groups || noise_nrg.len() != num_groups { + return Err(Error::PnsInvalid); + } + for g in 0..num_groups { + if sfb_cb[g].len() < max_sfb || noise_nrg[g].len() < max_sfb { + return Err(Error::PnsInvalid); + } + } + Ok((window_len, offsets)) +} + +/// Apply the §4.6.13.3 noise substitution to one channel in place. +/// +/// For every `(group, sfb)` whose `sfb_cb` is `NOISE_HCB`, every window +/// of the group has its in-band coefficients replaced by a fresh random +/// vector (drawn via `rng`) scaled to L2 norm +/// `2.0^(0.25 · noise_nrg[g][sfb])`. Non-noise bands are left exactly +/// as they arrive. +/// +/// * `chan` — the channel spectrum, codebooks, and noise energies +/// ([`PnsChannel`]). +/// * `ics_info` — supplies `num_window_groups`, `window_group_length`, +/// `max_sfb`, and the window geometry. +/// * `fs_index` — `samplingFrequencyIndex`, selecting the `swb_offset` +/// table. +/// * `rng` — `gen_rand_vector(out)` fills `out` with a fresh random +/// vector; called once per `(group, window, noise sfb)`. Use a +/// stateful closure over [`gen_rand_vector`] for the default +/// generator. +/// +/// Returns [`Error::PnsInvalid`] if the buffer / `sfb_cb` / `noise_nrg` +/// shapes disagree with `ics_info` (see the variant docs). When no band +/// is noise-coded the spectrum is left untouched. +pub fn apply_pns( + chan: &mut PnsChannel<'_>, + ics_info: &IcsInfo, + fs_index: u8, + mut rng: F, +) -> Result<()> +where + F: FnMut(&mut [f64]), +{ + let PnsChannel { + spec, + sfb_cb, + noise_nrg, + } = chan; + + let (window_len, offsets) = + channel_geometry(spec.len(), sfb_cb, noise_nrg, ics_info, fs_index)?; + let num_groups = ics_info.num_window_groups as usize; + let max_sfb = ics_info.max_sfb as usize; + + let mut window_base = 0usize; + for g in 0..num_groups { + let wgl = ics_info.window_group_length[g] as usize; + for sfb in 0..max_sfb { + if !is_noise(sfb_cb[g][sfb]) { + continue; + } + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + let nrg = noise_nrg[g][sfb]; + for b in 0..wgl { + let base = (window_base + b) * window_len; + let band = &mut spec[base + start..base + end]; + rng(band); + normalise_band(band, nrg); + } + } + window_base += wgl; + } + + Ok(()) +} + +/// Apply the §4.6.13.3 noise substitution to a channel pair in place, +/// honouring the shared-random-vector correlation rule. +/// +/// Both channels share the `common_window` `ics_info`. For each +/// `(group, sfb)`: +/// +/// * If `NOISE_HCB` in **both** channels and the band's `ms_used` bit +/// is set (`ms_mask_present == true`, i.e. `ms_mask_present == 1`), +/// **or** `all_shared` is set (`ms_mask_present == 2`, "all bands +/// shared"), the **same** random vector is generated once and scaled +/// independently into each channel (correlated noise; no M/S +/// de-matrix — PNS and M/S are mutually exclusive, §4.6.13.5). +/// * Otherwise each noise band draws an independent vector. +/// +/// `rng(out)` fills `out` with a fresh random vector. +/// +/// * `left` / `right` — the two channels' spectra, codebooks, and noise +/// energies. +/// * `ms_mask_present` — `true` when a per-band `ms_used` mask is +/// present (`ms_mask_present == 1`). +/// * `all_shared` — `true` when `ms_mask_present == 2` (every band's +/// noise vector is shared); when set, `ms_used` is not consulted. +/// * `ms_used` — `ms_used[g][sfb]`; consulted only when `ms_mask_present` +/// is `true` and `all_shared` is `false`. Pass an empty slice +/// otherwise. +/// * `ics_info` / `fs_index` — shared window geometry and `swb_offset` +/// table. +/// +/// Returns [`Error::PnsInvalid`] on any shape mismatch. +#[allow(clippy::too_many_arguments)] +pub fn apply_pns_pair( + left: &mut PnsChannel<'_>, + right: &mut PnsChannel<'_>, + ms_mask_present: bool, + all_shared: bool, + ms_used: &[Vec], + ics_info: &IcsInfo, + fs_index: u8, + mut rng: F, +) -> Result<()> +where + F: FnMut(&mut [f64]), +{ + let (window_len, offsets) = channel_geometry( + left.spec.len(), + left.sfb_cb, + left.noise_nrg, + ics_info, + fs_index, + )?; + // Right channel must match the same geometry. + let (rwindow_len, _) = channel_geometry( + right.spec.len(), + right.sfb_cb, + right.noise_nrg, + ics_info, + fs_index, + )?; + if rwindow_len != window_len { + return Err(Error::PnsInvalid); + } + + let num_groups = ics_info.num_window_groups as usize; + let max_sfb = ics_info.max_sfb as usize; + + if ms_mask_present && !all_shared { + if ms_used.len() != num_groups { + return Err(Error::PnsInvalid); + } + for row in ms_used { + if row.len() < max_sfb { + return Err(Error::PnsInvalid); + } + } + } + + let mut window_base = 0usize; + for g in 0..num_groups { + let wgl = ics_info.window_group_length[g] as usize; + for sfb in 0..max_sfb { + let l_noise = is_noise(left.sfb_cb[g][sfb]); + let r_noise = is_noise(right.sfb_cb[g][sfb]); + if !l_noise && !r_noise { + continue; + } + let start = offsets[sfb] as usize; + let end = offsets[sfb + 1] as usize; + let size = end - start; + // Shared vector only when both channels are noise on this + // band AND the band signals correlation. The `ms_used` + // lookup is gated on `ms_mask_present` (and validated + // above), so the `.get()` chain only matters defensively. + let band_ms_used = ms_used + .get(g) + .and_then(|row| row.get(sfb)) + .copied() + .unwrap_or(false); + let shared = l_noise && r_noise && (all_shared || (ms_mask_present && band_ms_used)); + for b in 0..wgl { + let base = (window_base + b) * window_len; + if shared { + // One random vector, scaled independently into both + // channels (§4.6.13.3 correlated-noise path). + let mut vec = vec![0.0f64; size]; + rng(&mut vec); + let lband = &mut left.spec[base + start..base + end]; + lband.copy_from_slice(&vec); + normalise_band(lband, left.noise_nrg[g][sfb]); + let rband = &mut right.spec[base + start..base + end]; + rband.copy_from_slice(&vec); + normalise_band(rband, right.noise_nrg[g][sfb]); + } else { + if l_noise { + let lband = &mut left.spec[base + start..base + end]; + rng(lband); + normalise_band(lband, left.noise_nrg[g][sfb]); + } + if r_noise { + let rband = &mut right.spec[base + start..base + end]; + rng(rband); + normalise_band(rband, right.noise_nrg[g][sfb]); + } + } + } + } + window_base += wgl; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + use crate::section_data::{INTENSITY_HCB, ZERO_HCB}; + + const FS_48000: u8 = 3; + + /// Build a minimal long-window `IcsInfo` (one group of one window, + /// `max_sfb` bands) for synthesis tests at fs_index 3 (48 kHz). + fn long_ics(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[FS_48000 as usize], + } + } + + /// L2 norm of a slice. + fn norm(s: &[f64]) -> f64 { + s.iter().map(|&x| x * x).sum::().sqrt() + } + + #[test] + fn noise_target_norm_is_quarter_step_ladder() { + assert!((noise_target_norm(0) - 1.0).abs() < 1e-12); + // +4 in noise_nrg doubles the target norm (2^(0.25*4) = 2). + assert!((noise_target_norm(4) - 2.0).abs() < 1e-12); + assert!((noise_target_norm(-4) - 0.5).abs() < 1e-12); + } + + #[test] + fn gen_rand_vector_is_signed_and_nonzero_energy() { + let mut state = 1u32; + let mut v = vec![0.0f64; 16]; + gen_rand_vector(&mut v, &mut state); + // Values lie in [-1, 1) and the sum of squares is non-zero. + assert!(v.iter().all(|&x| (-1.0..1.0).contains(&x))); + assert!(v.iter().map(|&x| x * x).sum::() > 0.0); + // Signed: at least one negative and one positive over 16 draws. + assert!(v.iter().any(|&x| x < 0.0)); + assert!(v.iter().any(|&x| x > 0.0)); + } + + #[test] + fn synthesised_band_has_exact_target_norm() { + // fs_index 3 (48 kHz) long window, single noise band at sfb 0. + let fs = 3u8; + let ics = long_ics(1); + let offsets = long_window_offsets(fs).unwrap(); + let mut spec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let sfb_cb = vec![vec![NOISE_HCB]]; + let noise_nrg = vec![vec![8i32]]; // target norm = 2^(0.25*8) = 4.0 + let mut state = 12345u32; + let mut chan = PnsChannel { + spec: &mut spec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + apply_pns(&mut chan, &ics, fs, |out| gen_rand_vector(out, &mut state)).unwrap(); + let start = offsets[0] as usize; + let end = offsets[1] as usize; + let got = norm(&spec[start..end]); + assert!((got - 4.0).abs() < 1e-9, "band L2 norm {got} != target 4.0"); + // Coefficients outside the noise band are untouched (silent). + assert!(spec[end..].iter().all(|&x| x == 0.0)); + } + + #[test] + fn non_noise_bands_left_untouched() { + let fs = 3u8; + let ics = long_ics(2); + let offsets = long_window_offsets(fs).unwrap(); + let mut spec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + // Seed band 1 (a non-noise band) with a recognisable value. + let b1 = offsets[1] as usize; + spec[b1] = 7.5; + let sfb_cb = vec![vec![NOISE_HCB, ZERO_HCB]]; + let noise_nrg = vec![vec![0i32, 0i32]]; + let mut state = 1u32; + let mut chan = PnsChannel { + spec: &mut spec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + apply_pns(&mut chan, &ics, fs, |out| gen_rand_vector(out, &mut state)).unwrap(); + assert_eq!(spec[b1], 7.5, "non-noise band must be untouched"); + // The noise band (band 0) was filled. + assert!(norm(&spec[offsets[0] as usize..b1]) > 0.0); + } + + #[test] + fn shape_mismatch_is_rejected() { + let fs = 3u8; + let ics = long_ics(1); + let mut spec = vec![0.0f64; 10]; // wrong length + let sfb_cb = vec![vec![NOISE_HCB]]; + let noise_nrg = vec![vec![0i32]]; + let mut chan = PnsChannel { + spec: &mut spec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + let r = apply_pns(&mut chan, &ics, fs, |_| {}); + assert!(matches!(r, Err(Error::PnsInvalid))); + } + + #[test] + fn pair_shared_vector_correlates_noise() { + // Both channels noise at sfb 0 with the SAME noise_nrg and the + // ms_used bit set → shared random vector → the two bands are + // identical (correlated noise), since equal target norms scale + // the same source vector by the same factor. + let fs = 3u8; + let ics = long_ics(1); + let offsets = long_window_offsets(fs).unwrap(); + let mut lspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let mut rspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let sfb_cb = vec![vec![NOISE_HCB]]; + let noise_nrg = vec![vec![4i32]]; + let ms_used = vec![vec![true]]; + let mut state = 999u32; + { + let mut l = PnsChannel { + spec: &mut lspec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + let mut r = PnsChannel { + spec: &mut rspec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + apply_pns_pair(&mut l, &mut r, true, false, &ms_used, &ics, fs, |out| { + gen_rand_vector(out, &mut state) + }) + .unwrap(); + } + let start = offsets[0] as usize; + let end = offsets[1] as usize; + for i in start..end { + assert!( + (lspec[i] - rspec[i]).abs() < 1e-12, + "shared-vector bands must be identical at {i}" + ); + } + assert!((norm(&lspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); + } + + #[test] + fn pair_independent_when_mask_absent() { + // Both channels noise but ms_mask_present == false → independent + // vectors → the two bands differ (overwhelmingly likely for a + // 96-bin band; we assert they are not bit-identical). + let fs = 3u8; + let ics = long_ics(1); + let offsets = long_window_offsets(fs).unwrap(); + let mut lspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let mut rspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let sfb_cb = vec![vec![NOISE_HCB]]; + let noise_nrg = vec![vec![4i32]]; + let mut state = 7u32; + { + let mut l = PnsChannel { + spec: &mut lspec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + let mut r = PnsChannel { + spec: &mut rspec, + sfb_cb: &sfb_cb, + noise_nrg: &noise_nrg, + }; + apply_pns_pair(&mut l, &mut r, false, false, &[], &ics, fs, |out| { + gen_rand_vector(out, &mut state) + }) + .unwrap(); + } + let start = offsets[0] as usize; + let end = offsets[1] as usize; + let identical = (start..end).all(|i| lspec[i] == rspec[i]); + assert!(!identical, "independent draws must not be bit-identical"); + // Both still hit the exact target norm. + assert!((norm(&lspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); + assert!((norm(&rspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); + } + + #[test] + fn pair_one_channel_noise_ignores_ms_used() { + // Only the right channel is noise at sfb 0; ms_used is set but + // must be ignored (§4.6.13.3) — the left band stays silent and + // only the right is filled, independently. + let fs = 3u8; + let ics = long_ics(1); + let offsets = long_window_offsets(fs).unwrap(); + let mut lspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let mut rspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; + let lcb = vec![vec![ZERO_HCB]]; + let rcb = vec![vec![NOISE_HCB]]; + let lnrg = vec![vec![0i32]]; + let rnrg = vec![vec![4i32]]; + let ms_used = vec![vec![true]]; + let mut state = 42u32; + { + let mut l = PnsChannel { + spec: &mut lspec, + sfb_cb: &lcb, + noise_nrg: &lnrg, + }; + let mut r = PnsChannel { + spec: &mut rspec, + sfb_cb: &rcb, + noise_nrg: &rnrg, + }; + apply_pns_pair(&mut l, &mut r, true, false, &ms_used, &ics, fs, |out| { + gen_rand_vector(out, &mut state) + }) + .unwrap(); + } + let start = offsets[0] as usize; + let end = offsets[1] as usize; + assert!( + lspec[start..end].iter().all(|&x| x == 0.0), + "non-noise left band must stay silent" + ); + assert!((norm(&rspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); + } + + #[test] + fn is_noise_predicate() { + assert!(is_noise(NOISE_HCB)); + assert!(!is_noise(ZERO_HCB)); + assert!(!is_noise(INTENSITY_HCB)); + assert!(!is_noise(1)); + } +} diff --git a/crates/vendor/oxideav-aac/src/predictor.rs b/crates/vendor/oxideav-aac/src/predictor.rs new file mode 100644 index 00000000..bc73f800 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/predictor.rs @@ -0,0 +1,752 @@ +//! MPEG-2 frequency-domain prediction — ISO/IEC 14496-3 §4.6.6 +//! (carried over from ISO/IEC 13818-7). +//! +//! Frequency-domain prediction is the backward-adaptive intra-channel +//! predictor of the AAC **Main** object type. It exploits the +//! auto-correlation between the spectral components of consecutive +//! frames: for every MDCT line up to the §4.6.6.2 `PRED_SFB_MAX` limit +//! there is one second-order, backward-adaptive lattice predictor. The +//! predictor coefficients are derived from previously reconstructed +//! values on both encoder and decoder, so no coefficients are +//! transmitted — only the per-frame / per-sfb on/off side information +//! ([`crate::ics_info::PredictorData`], Table 4.6) controls whether the +//! reconstructed prediction error or the reconstructed spectral value is +//! carried. +//! +//! ## Scope of this module +//! +//! Prediction is only ever applied on the three long window sequences +//! (`ONLY_LONG_SEQUENCE`, `LONG_START_SEQUENCE`, `LONG_STOP_SEQUENCE`); +//! an `EIGHT_SHORT_SEQUENCE` disables prediction and resets every +//! predictor (§4.6.6.3.2.1 / §4.6.6.3.3). This module implements: +//! +//! * the §4.6.6.3.2.1 lattice `predict()` (estimate + LMS adaptation); +//! * the §4.6.6.3.2.3 `flt_round_inf()` 16-bit-float rounding used on +//! every stored state variable and on the predicted value; +//! * the §4.6.6.3.2.1 per-frame reconstruction loop +//! `x_rec = x_est + y_rec` on the predicted bands; +//! * the §4.6.6.3.3 predictor reset (cyclic group reset + short-block +//! reset-all), with the 30 reset groups of Table 4.97. +//! +//! The decode steps, transcribed from the §4.6.6.3.2.1 pseudo code: +//! +//! ```text +//! if (ONLY_LONG || LONG_START || LONG_STOP) { +//! for (sfb = 0; sfb < PRED_SFB_MAX; sfb++) { +//! for (c = swb[sfb]; c < swb[sfb+1]; c++) { +//! x_est[c] = predict(); // lattice estimate +//! if (predictor_data_present && prediction_used[sfb]) +//! x_rec[c] = x_est[c] + y_rec[c]; +//! else +//! x_rec[c] = y_rec[c]; +//! } +//! } +//! } else { +//! reset_all_predictors(); +//! } +//! ``` +//! +//! Each per-coefficient predictor is run **every** frame (whether or not +//! its band is active) so its coefficients keep tracking the signal +//! statistics (§4.6.6.3.2.1, "all the predictors are run all the time"). +//! The post-processing reset of the signalled group then follows +//! (§4.6.6.3.3, "after the normal predictor processing ... has been +//! carried out"). +//! +//! Per §4.6.6 the predicted value `x_est` is rounded to a 16-bit float +//! before use ([`flt_round_inf`]), the six saved state variables `r0, +//! r1, COR1, COR2, VAR1, VAR2` are stored as *truncated* 16-msb floats +//! ([`flt_trunc`]), and the `b / VAR_m` ratio is quantized through the +//! §4.6.6.3.2.4 `make_inv_tables()` lookup pair (7-bit-mantissa +//! nearest-even reciprocal). All three fixed-precision forms are +//! transcribed from the printed listings; the ISO/IEC 14496-26 +//! `am05_*` conformance vectors (AAC Main with long prediction runs) +//! are the empirical anchor. + +use crate::ics_info::{IcsInfo, PredictorData, WindowSequence, PRED_SFB_MAX}; +use crate::swb_offset::long_window_offsets; +use crate::Error; + +type Result = core::result::Result; + +/// §4.6.6.3.2.1 LMS adaptation time constant `α = 0.90625`. +pub const ALPHA: f32 = 0.90625; + +/// §4.6.6.3.2.1 attenuation factor `a = 0.953125`. +pub const A: f32 = 0.953125; + +/// §4.6.6.3.2.1 attenuation factor `b = 0.953125`. +pub const B: f32 = 0.953125; + +/// Number of cyclic reset groups (Table 4.97). Predictor `i` belongs to +/// reset group `(i mod 30) + 1` (the group numbers are 1-based and the +/// values `0` and `31` are reserved, §4.6.6.3.3). +pub const NUM_RESET_GROUPS: usize = 30; + +/// §4.6.6.3.2.3 — round a single-precision float toward infinity to a +/// 16-bit float (a 7-bit mantissa: the 16 most-significant bits of the +/// IEEE-754 storage word). +/// +/// This is the bit-exact transcription of the spec `flt_round_inf()` +/// pseudo code: the low 16 bits of the mantissa are discarded, and if +/// the most-significant discarded bit (`0x00008000`) was set, half an +/// lsb of the retained representation is added so the result rounds +/// toward (away from zero) infinity rather than truncating. The +/// add/subtract dance reproduces the spec's "add 1 lsb and elided one" +/// trick using only float arithmetic on the exponent/sign field. +pub fn flt_round_inf(pf: f32) -> f32 { + let bits = pf.to_bits(); + // Most-significant discarded mantissa bit. + let flg = bits & 0x0000_8000; + // Truncate to the 16 msb (clears the low 16 mantissa bits). + let truncated = bits & 0xffff_0000; + let mut result = f32::from_bits(truncated); + if flg != 0 { + // Build "1 lsb" of the 16-bit representation from the retained + // exponent + sign, then add it (carrying the elided leading + // one) and subtract the elided one again — exactly the spec's + // round-half-toward-infinity sequence. + let exp_sign = truncated & 0xff80_0000; + let one_lsb = exp_sign | 0x0001_0000; + result += f32::from_bits(one_lsb); + result -= f32::from_bits(exp_sign); + } + result +} + +/// §4.6.6.3.2.2 — truncate a single-precision float to its 16 most +/// significant storage bits (a 7-bit mantissa), the storage format of +/// the six saved predictor state variables ("saved as *truncated* +/// IEEE floating-point numbers" — truncation, not rounding). +#[inline] +pub fn flt_trunc(pf: f32) -> f32 { + f32::from_bits(pf.to_bits() & 0xffff_0000) +} + +/// §4.6.6.3.2.4 `flt_round_even()` — round to an 8-bit mantissa, +/// nearest-even, via the printed `frexp`-based listing. Used when +/// building the `b / VAR` inverse tables. +fn flt_round_even(pf: f32) -> f32 { + if pf == 0.0 { + return 0.0; + } + // frexp: pf = mant · 2^exp with mant in [0.5, 1). + let bits = pf.to_bits(); + let biased = ((bits >> 23) & 0xff) as i32; + let exp = biased - 126; + let scale = 2f32.powi(8 - exp); + let tmp = pf * scale; + let mut a = tmp as i64; + if (tmp - a as f32) >= 0.5 { + a += 1; + } + if (tmp - a as f32) == 0.5 { + a &= -2; + } + a as f32 / scale +} + +/// §4.6.6.3.2.4 `make_inv_tables()` — the two lookup tables through +/// which the `b / VAR_m` ratio is computed: `MNT_TABLE[m]` holds +/// `flt_round_even(b / (1.m))` for each 7-bit mantissa prefix, and +/// `EXP_TABLE[e]` holds `1 / 2^(e-127)` for exponent fields whose +/// value exceeds 1.0 (zero otherwise, exactly as the printed listing +/// guards it). `b_over_var` composes them at the state's stored +/// (truncated) precision. +fn mnt_table(i: usize) -> f32 { + let f = f32::from_bits(0x3f80_0000 + ((i as u32) << 16)); + flt_round_even(B / f) +} + +fn exp_table(i: usize) -> f32 { + let f = f32::from_bits((i as u32) << 23); + if f > 1.0 { + 1.0 / f + } else { + 0.0 + } +} + +/// `b / VAR` computed via the §4.6.6.3.2.4 table pair, keyed by the +/// truncated state's 7 mantissa msbs and its exponent field. +#[inline] +fn b_over_var(var: f32) -> f32 { + let bits = var.to_bits(); + let mant7 = ((bits >> 16) & 0x7f) as usize; + let exp = ((bits >> 23) & 0xff) as usize; + MNT_TABLE[mant7] * EXP_TABLE[exp] +} + +/// Precomputed §4.6.6.3.2.4 tables (see [`b_over_var`]). +static MNT_TABLE: std::sync::LazyLock<[f32; 128]> = + std::sync::LazyLock::new(|| core::array::from_fn(mnt_table)); +static EXP_TABLE: std::sync::LazyLock<[f32; 256]> = + std::sync::LazyLock::new(|| core::array::from_fn(exp_table)); + +/// State of one second-order backward-adaptive lattice predictor +/// (§4.6.6.3.2.1), i.e. one MDCT line. +/// +/// The six saved variables of §4.6.6.3.2.2 (`r0, r1, COR1, COR2, VAR1, +/// VAR2`) are stored as 16-bit-truncated floats. [`Self::new`] applies +/// the §4.6.6.3.3 initialisation `r0 = r1 = 0, COR1 = COR2 = 0, +/// VAR1 = VAR2 = 1`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Predictor { + /// `r_q,0(n-1)` — first basic element's delayed register. + r0: f32, + /// `r_q,1(n-1)` — second basic element's delayed register. + r1: f32, + /// `COR1(n-1)` — first element's running correlation estimate. + cor1: f32, + /// `COR2(n-1)` — second element's running correlation estimate. + cor2: f32, + /// `VAR1(n-1)` — first element's running variance estimate. + var1: f32, + /// `VAR2(n-1)` — second element's running variance estimate. + var2: f32, +} + +impl Default for Predictor { + fn default() -> Self { + Self::new() + } +} + +impl Predictor { + /// §4.6.6.3.3 predictor initialisation: `r0 = r1 = 0, + /// COR1 = COR2 = 0, VAR1 = VAR2 = 1`. + pub fn new() -> Self { + Self { + r0: 0.0, + r1: 0.0, + cor1: 0.0, + cor2: 0.0, + var1: 1.0, + var2: 1.0, + } + } + + /// §4.6.6.3.3 reset — re-initialise to the start-of-decoding state. + pub fn reset(&mut self) { + *self = Self::new(); + } + + /// `b · k_m(n) = COR_m(n-1) · (b / VAR_m(n-1))` for `m = 1, 2` + /// (§4.6.6.3.2.1), with the `b / VAR` factor quantized through the + /// §4.6.6.3.2.4 table pair — the normative fixed-precision form + /// (`VAR_m` is initialised to `1` and never decays to `0`). + fn coefficients(&self) -> (f32, f32) { + ( + self.cor1 * b_over_var(self.var1), + self.cor2 * b_over_var(self.var2), + ) + } + + /// §4.6.6.3.2.1 `predict()` — form the estimate `x_est(n)` from the + /// current state, **without** advancing it. + /// + /// The two cascaded basic elements compute + /// `x_est,m(n) = b · k_m(n) · r_q,m-1(n-1)` and + /// `x_est(n) = x_est,1(n) + x_est,2(n)`, where `r_q,0(n-1) = r0` and + /// `r_q,1(n-1) = r1`. The result is rounded to a 16-bit float per + /// §4.6.6.3.2.2 before use. + pub fn predict(&self) -> f32 { + let (bk1, bk2) = self.coefficients(); + let x_est1 = bk1 * self.r0; + let x_est2 = bk2 * self.r1; + flt_round_inf(x_est1 + x_est2) + } + + /// §4.6.6.3.2.1 — advance the predictor by one frame given the + /// reconstructed spectral value `x_rec(n)` of this line, updating the + /// LMS correlation / variance estimates and the lattice registers. + /// + /// This realises the §4.6.6.3.2.1 recursion: + /// + /// ```text + /// e_q,0(n) = r_q,0(n) = x_rec(n) (for adaptation) + /// x_est,1(n) = b·k1(n)·r_q,0(n-1) + /// e_q,1(n) = e_q,0(n) − x_est,1(n) + /// r_q,1(n) = a·(r_q,0(n-1) − b·k1(n)·e_q,0(n)) + /// x_est,2(n) = b·k2(n)·r_q,1(n-1) + /// COR_m(n) = α·COR_m(n-1) + r_q,m-1(n-1)·e_q,m-1(n) + /// VAR_m(n) = α·VAR_m(n-1) + 0.5·(r_q,m-1²(n-1) + e_q,m-1²(n)) + /// r_q,0(n) = a·x_rec(n) + /// ``` + /// + /// Every stored variable is rounded to a 16-bit float (§4.6.6.3.2.2). + pub fn update(&mut self, x_rec: f32) { + // Only element 1's coefficient enters the lattice register + // update / second-element error; k2 only affects the estimate + // (computed in `predict`). `b·k1` uses the same §4.6.6.3.2.4 + // table-quantized `b / VAR` factor as the estimate path. + let bk1 = self.cor1 * b_over_var(self.var1); + + // Element 1: e_q,0(n) = r_q,0(n) = x_rec(n). + let e0 = x_rec; + let r0_prev = self.r0; + let r1_prev = self.r1; + + // x_est,1(n) = b·k1·r_q,0(n-1); e_q,1(n) = e_q,0(n) − x_est,1(n). + let x_est1 = bk1 * r0_prev; + let e1 = e0 - x_est1; + + // Adapt element 1: COR1, VAR1 use r_q,0(n-1) and e_q,0(n). + let cor1 = ALPHA * self.cor1 + r0_prev * e0; + let var1 = ALPHA * self.var1 + 0.5 * (r0_prev * r0_prev + e0 * e0); + + // Adapt element 2: COR2, VAR2 use r_q,1(n-1) and e_q,1(n). + let cor2 = ALPHA * self.cor2 + r1_prev * e1; + let var2 = ALPHA * self.var2 + 0.5 * (r1_prev * r1_prev + e1 * e1); + + // New lattice registers. + // r_q,1(n) = a·(r_q,0(n-1) − b·k1(n)·e_q,0(n)). + let r1_new = A * (r0_prev - bk1 * e0); + // r_q,0(n) = a·x_rec(n). + let r0_new = A * x_rec; + + // §4.6.6.3.2.2: the six saved state variables are stored as + // *truncated* 16-msb floats (truncation, not the round-to- + // infinity used for x_est). + self.r0 = flt_trunc(r0_new); + self.r1 = flt_trunc(r1_new); + self.cor1 = flt_trunc(cor1); + self.cor2 = flt_trunc(cor2); + self.var1 = flt_trunc(var1); + self.var2 = flt_trunc(var2); + } +} + +/// A per-channel bank of §4.6.6 frequency-domain predictors, one for +/// every MDCT line up to the §4.6.6.2 `PRED_SFB_MAX` coefficient limit. +/// +/// The bank lives for the whole channel decode (across frames), carrying +/// the backward-adaptive state. Construct one per channel with +/// [`PredictorBank::new`] and call [`PredictorBank::apply_long`] each +/// frame (§4.6.6.3.2.1) — including frames where prediction is off, so +/// the LMS coefficients keep adapting. +#[derive(Clone, Debug)] +pub struct PredictorBank { + /// One predictor per coefficient index `0 .. num_predictors`. + predictors: Vec, +} + +impl PredictorBank { + /// Build a fresh bank for the `fs_index` sampling-frequency index. + /// + /// The number of predictors is `swb_offset_long_window[fs_index] + /// [PRED_SFB_MAX[fs_index]]`, i.e. the first MDCT line **above** the + /// last predictable scalefactor band (§4.6.6.2 / Table 4.96). All + /// predictors start in the §4.6.6.3.3 initial state. + /// + /// Errors: the [`Error`] from [`long_window_offsets`] for a bad + /// `fs_index`, or [`Error::PredictorInvalid`] if the long-window + /// offset table is too short to cover `PRED_SFB_MAX`. + pub fn new(fs_index: u8) -> Result { + let offsets = long_window_offsets(fs_index)?; + let pred_sfb_max = PRED_SFB_MAX[fs_index as usize] as usize; + let num_predictors = offsets + .get(pred_sfb_max) + .copied() + .ok_or(Error::PredictorInvalid)? as usize; + Ok(Self { + predictors: vec![Predictor::new(); num_predictors], + }) + } + + /// Number of per-line predictors in the bank. + pub fn len(&self) -> usize { + self.predictors.len() + } + + /// Whether the bank carries no predictors. + pub fn is_empty(&self) -> bool { + self.predictors.is_empty() + } + + /// §4.6.6.3.3 — reset every predictor in the bank (the + /// `reset_all_predictors()` path taken on a short block). + pub fn reset_all(&mut self) { + for p in &mut self.predictors { + p.reset(); + } + } + + /// §4.6.6.3.3 — reset the predictors of one cyclic reset group. + /// + /// `group` is the 1-based `predictor_reset_group_number` (Table 4.97, + /// valid range `1 ..= 30`). Predictor `i` belongs to group + /// `(i mod 30) + 1`, so the members of group `g` are the lines + /// `g-1, g-1+30, g-1+60, …`. + /// + /// Errors: [`Error::PredictorInvalid`] if `group` is `0` or `> 30` + /// (the reserved values of §4.6.6.3.3). + pub fn reset_group(&mut self, group: u8) -> Result<()> { + if group == 0 || group as usize > NUM_RESET_GROUPS { + return Err(Error::PredictorInvalid); + } + let start = (group - 1) as usize; + let mut idx = start; + while idx < self.predictors.len() { + self.predictors[idx].reset(); + idx += NUM_RESET_GROUPS; + } + Ok(()) + } + + /// §4.6.6.3.2.1 — apply frequency-domain prediction to one channel's + /// reconstructed long-window spectrum in place, then advance and (if + /// signalled) reset the predictor bank. + /// + /// * `spec` — the decoded coefficients `y_rec` (the reconstructed + /// quantised prediction error or spectral value), modified to + /// `x_rec` on the predicted bands. Length must be at least the + /// bank's predictor count. + /// * `ics_info` — provides `window_sequence` (prediction only acts on + /// the three long sequences; a short sequence resets the whole bank + /// and leaves the spectrum untouched) and `max_sfb` (bands at or + /// above `max_sfb` carry `prediction_used = 0`). + /// * `pred` — the parsed §4.6.6.3.1 `predictor_data()` side info, or + /// `None` when `predictor_data_present == 0` (prediction off this + /// frame, but the bank is still run to keep adapting). + /// * `fs_index` — selects the §4.5.4 long-window scalefactor-band + /// offsets. + /// + /// Returns `true` if prediction modified the spectrum, `false` + /// otherwise (short block, or no active band). + /// + /// Errors: the [`Error`] from [`long_window_offsets`] for a bad + /// `fs_index`; [`Error::PredictorInvalid`] if `spec` is shorter than + /// the predictor bank or the reset-group number is reserved. + pub fn apply_long( + &mut self, + spec: &mut [f64], + ics_info: &IcsInfo, + pred: Option<&PredictorData>, + fs_index: u8, + ) -> Result { + // Short block: disable prediction and reset every predictor + // (§4.6.6.3.2.1 else-branch / §4.6.6.3.3). + if ics_info.window_sequence == WindowSequence::EightShort { + self.reset_all(); + return Ok(false); + } + + if spec.len() < self.predictors.len() { + return Err(Error::PredictorInvalid); + } + + // Family-aware long-window offsets (the §4.6.6 predictor is a + // long-window tool; the 960-line family shares every band + // start below the PRED_SFB_MAX region with the 1024 table, so + // the bank sizing from `new` stays valid). + let offsets = ics_info.swb_offsets(fs_index)?; + let pred_sfb_max = PRED_SFB_MAX[fs_index as usize] as usize; + let max_sfb = ics_info.max_sfb as usize; + + let mut modified = false; + let num_predictors = self.predictors.len(); + // §4.6.6.3.2.1 — run every predictor every frame; only the + // reconstruction differs by `prediction_used[sfb]`. + for sfb in 0..pred_sfb_max { + let fc = offsets[sfb] as usize; + let lc = (offsets[sfb + 1] as usize).min(num_predictors); + if fc >= lc { + continue; + } + // A band at/above max_sfb has prediction_used = 0 (the bits + // are not transmitted, §4.6.6.2). + let active = sfb < max_sfb + && pred.is_some_and(|p| p.prediction_used.get(sfb).copied().unwrap_or(false)); + for (p, y) in self.predictors[fc..lc] + .iter_mut() + .zip(spec[fc..lc].iter_mut()) + { + // §13.3.2.2 (ISO/IEC 13818-7): "The predicted value + // xest will be rounded to a 16-bit floating point + // representation prior to being used in ANY + // calculation" — the rounding applies to x_est + // itself, not to the x_est + y_rec sum. Rounding the + // sum instead leaves a small persistent bias on + // predicted bands (measured against the ISO/IEC + // 14496-26 am05_48 vector's reference waveform: the + // prediction-bearing channel pair decodes ~5e-3 + // err/sig with the sum-rounding form and ~1e-4 with + // this one). + let x_est = flt_round_inf(p.predict()); + let y_rec = *y as f32; + let x_rec = if active { + modified = true; + x_est + y_rec + } else { + y_rec + }; + *y = x_rec as f64; + p.update(x_rec); + } + } + + // §4.6.6.3.3 — the signalled group reset is applied *after* the + // normal per-frame processing. + if let Some(p) = pred { + if p.reset { + if let Some(group) = p.reset_group_number { + self.reset_group(group)?; + } + } + } + + Ok(modified) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::{WindowSequence, WindowShape}; + + /// Build a minimal long-window `IcsInfo` for predictor tests. + fn long_ics(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: 49, + } + } + + #[test] + fn flt_round_inf_clears_low_16_bits_when_no_rounding() { + // A value whose low 16 mantissa bits are already zero is a + // fixed point of the rounding. + let v = 1.5_f32; // 0x3FC00000, low 16 bits zero. + assert_eq!(flt_round_inf(v).to_bits() & 0x0000_ffff, 0); + assert_eq!(flt_round_inf(v), v); + } + + #[test] + fn flt_round_inf_result_always_has_zero_low_bits() { + for &v in &[0.0_f32, 1.0, -1.0, 3.5_f32, -2.6_f32, 1e-8, 1e8, 0.953125] { + let r = flt_round_inf(v); + assert_eq!( + r.to_bits() & 0x0000_ffff, + 0, + "flt_round_inf({v}) left low mantissa bits set" + ); + } + } + + #[test] + fn flt_round_inf_rounds_toward_infinity() { + // Construct a positive value with the round bit (0x8000) set and + // a larger magnitude in the remaining discarded bits: the result + // must be >= the truncation. + let bits = 1.0_f32.to_bits() | 0x0000_8001; + let v = f32::from_bits(bits); + let truncated = f32::from_bits(bits & 0xffff_0000); + let r = flt_round_inf(v); + assert!(r > truncated, "expected round-up: {r} vs trunc {truncated}"); + assert_eq!(r.to_bits() & 0x0000_ffff, 0); + } + + #[test] + fn fresh_predictor_predicts_zero() { + // With r0 = r1 = 0, the estimate is 0 regardless of COR/VAR. + let p = Predictor::new(); + assert_eq!(p.predict(), 0.0); + } + + #[test] + fn predictor_initial_state_matches_spec() { + let p = Predictor::new(); + assert_eq!(p.r0, 0.0); + assert_eq!(p.r1, 0.0); + assert_eq!(p.cor1, 0.0); + assert_eq!(p.cor2, 0.0); + assert_eq!(p.var1, 1.0); + assert_eq!(p.var2, 1.0); + } + + #[test] + fn update_then_reset_returns_to_initial() { + let mut p = Predictor::new(); + for _ in 0..16 { + p.update(0.7); + } + assert_ne!(p, Predictor::new()); + p.reset(); + assert_eq!(p, Predictor::new()); + } + + #[test] + fn update_advances_lattice_register() { + // After feeding x_rec, r0 should become flt_round_inf(a·x_rec). + let mut p = Predictor::new(); + let x = 2.0_f32; + p.update(x); + assert_eq!(p.r0, flt_round_inf(A * x)); + } + + #[test] + fn bank_size_covers_pred_sfb_max() { + // fs_index 4 (44100 Hz): PRED_SFB_MAX = 40, swb[40] = 672. + let bank = PredictorBank::new(4).unwrap(); + assert_eq!(bank.len(), 672); + assert!(!bank.is_empty()); + } + + #[test] + fn bank_size_24khz() { + // fs_index 6 (24000 Hz): PRED_SFB_MAX = 41, swb[41] = 652. + let bank = PredictorBank::new(6).unwrap(); + assert_eq!(bank.len(), 652); + } + + #[test] + fn reset_group_rejects_reserved_numbers() { + let mut bank = PredictorBank::new(4).unwrap(); + assert!(matches!(bank.reset_group(0), Err(Error::PredictorInvalid))); + assert!(matches!(bank.reset_group(31), Err(Error::PredictorInvalid))); + assert!(bank.reset_group(1).is_ok()); + assert!(bank.reset_group(30).is_ok()); + } + + #[test] + fn reset_group_only_touches_its_members() { + let mut bank = PredictorBank::new(4).unwrap(); + // Dirty every predictor. + for p in &mut bank.predictors { + p.update(0.5); + } + let before: Vec = bank.predictors.clone(); + bank.reset_group(1).unwrap(); + // Group 1 members are lines 0, 30, 60, … — those must be fresh, + // every other line unchanged. + for (i, p) in bank.predictors.iter().enumerate() { + if i % NUM_RESET_GROUPS == 0 { + assert_eq!(*p, Predictor::new(), "line {i} should be reset"); + } else { + assert_eq!(*p, before[i], "line {i} should be untouched"); + } + } + } + + #[test] + fn short_block_resets_and_leaves_spectrum_untouched() { + let mut bank = PredictorBank::new(4).unwrap(); + for p in &mut bank.predictors { + p.update(0.3); + } + let mut ics = long_ics(40); + ics.window_sequence = WindowSequence::EightShort; + let mut spec = vec![1.0_f64; 1024]; + let original = spec.clone(); + let modified = bank.apply_long(&mut spec, &ics, None, 4).unwrap(); + assert!(!modified); + assert_eq!(spec, original); + // Every predictor is back to the initial state. + for p in &bank.predictors { + assert_eq!(*p, Predictor::new()); + } + } + + #[test] + fn prediction_off_leaves_spectrum_but_advances_state() { + // predictor_data_present == 0: spectrum untouched, but predictors + // still run (so they adapt). With a fresh bank, x_est = 0 so the + // spectrum is unchanged either way; verify state advanced. + let mut bank = PredictorBank::new(4).unwrap(); + let ics = long_ics(40); + let mut spec = vec![2.0_f64; 1024]; + let original = spec.clone(); + let modified = bank.apply_long(&mut spec, &ics, None, 4).unwrap(); + assert!(!modified); + assert_eq!(spec, original, "prediction-off must not alter the spectrum"); + // Predictors over the active range advanced (r0 = a·x_rec). + assert_ne!(bank.predictors[0], Predictor::new()); + } + + #[test] + fn active_band_modifies_spectrum_on_second_frame() { + // Frame 1 primes the lattice; frame 2 produces a non-zero + // estimate that is added on the active band. + let mut bank = PredictorBank::new(4).unwrap(); + let mut ics = long_ics(40); + ics.predictor_data_present = true; + let pred = PredictorData { + reset: false, + reset_group_number: None, + // Enable prediction on sfb 0 only. + prediction_used: { + let mut v = vec![false; 40]; + v[0] = true; + v + }, + }; + // Prime the lattice over several frames so the LMS correlation + // and the delayed register r0 build up (a single frame leaves + // COR1 = r0_prev·e0 = 0 because r0_prev starts at zero). + for _ in 0..6 { + let mut spec = vec![0.0_f64; 1024]; + for (c, s) in spec.iter_mut().enumerate().take(8) { + *s = (c as f64) + 1.0; + } + bank.apply_long(&mut spec, &ics, Some(&pred), 4).unwrap(); + } + // Next frame: an active band should now add a non-zero estimate. + let mut spec2 = vec![1.0_f64; 1024]; + let y_rec = spec2.clone(); + let modified = bank.apply_long(&mut spec2, &ics, Some(&pred), 4).unwrap(); + assert!(modified); + // At least one coefficient in sfb 0 changed from its y_rec. + let band0_changed = (0..4).any(|c| spec2[c] != y_rec[c]); + assert!(band0_changed, "active band 0 spectrum did not change"); + } + + #[test] + fn reset_after_processing_clears_signalled_group() { + let mut bank = PredictorBank::new(4).unwrap(); + let mut ics = long_ics(40); + ics.predictor_data_present = true; + let pred = PredictorData { + reset: true, + reset_group_number: Some(1), + prediction_used: vec![true; 40], + }; + let mut spec = vec![3.0_f64; 1024]; + bank.apply_long(&mut spec, &ics, Some(&pred), 4).unwrap(); + // Group 1 lines were reset *after* processing, so they are fresh. + assert_eq!(bank.predictors[0], Predictor::new()); + assert_eq!(bank.predictors[NUM_RESET_GROUPS], Predictor::new()); + // A non-group-1 line still carries adapted state. + assert_ne!(bank.predictors[1], Predictor::new()); + } + + #[test] + fn spec_shorter_than_bank_is_rejected() { + let mut bank = PredictorBank::new(4).unwrap(); + let ics = long_ics(40); + let mut spec = vec![0.0_f64; 100]; + assert!(matches!( + bank.apply_long(&mut spec, &ics, None, 4), + Err(Error::PredictorInvalid) + )); + } + + #[test] + fn bad_fs_index_propagates_error() { + assert!(PredictorBank::new(13).is_err()); + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_data.rs b/crates/vendor/oxideav-aac/src/ps_data.rs new file mode 100644 index 00000000..078ecf17 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_data.rs @@ -0,0 +1,705 @@ +//! `ps_data()` — Parametric Stereo bitstream element, ISO/IEC +//! 14496-3:2009 §8.4.2 Tables 8.9–8.14 (+ §8.5.2 semantics). +//! +//! PS conveys the stereo image of an HE-AAC v2 stream as per-band +//! Inter-channel Intensity Differences (IID), Inter-channel +//! Coherences (ICC) and optional Inter-channel / Overall Phase +//! Differences (IPD/OPD), carried inside the SBR `sbr_extension()` +//! container (`bs_extension_id == EXTENSION_ID_PS`, Annex 8.A). +//! +//! ## Header persistence +//! +//! The one-bit `enable_ps_header` gates the configuration block +//! (`enable_iid` / `iid_mode` / `enable_icc` / `icc_mode` / +//! `enable_ext`); when clear, **the latest transmitted configuration +//! persists** (§8.5.2). [`PsData::parse`] therefore takes the previous +//! frame's [`PsConfig`] and returns `Ok(None)` for a headerless +//! element with no prior configuration — per §8.6.5.1 the decoder +//! outputs the mono signal in both channels until a decodable +//! `ps_data()` arrives. +//! +//! ## Differential decode +//! +//! IID/ICC/IPD/OPD parameters are DPCM-coded per envelope, either over +//! frequency (`*_dt[e] == 0`, band `b` relative to band `b-1`, the +//! first band relative to index 0) or over time (`*_dt[e] == 1`, +//! relative to the same band of envelope `e-1`, envelope 0 relative to +//! the previous frame's last envelope). [`PsData::resolve`] applies +//! the accumulation against a caller-threaded [`PsIndexState`] and +//! range-checks the result against the Table 8.24 / 8.27 index ranges +//! (IPD/OPD indices accumulate modulo 8 on the Table 8.31 phase +//! ladder, so they cannot leave their range). `num_env == 0` signals +//! that the previous parameters are held (§8.5.2 / Table 8.50–8.52); +//! `resolve` then produces no envelopes and leaves the state +//! untouched. +//! +//! All truth from ISO/IEC 14496-3:2009 subpart 8 staged under +//! `docs/audio/aac/`. + +use oxideav_core::bits::BitReader; + +use crate::ps_huffman::{ + ps_huff_dec, HUFF_ICC_DF, HUFF_ICC_DT, HUFF_IID_DF, HUFF_IID_DT, HUFF_IID_FINE_DF, + HUFF_IID_FINE_DT, HUFF_IPD_DF, HUFF_IPD_DT, HUFF_OPD_DF, HUFF_OPD_DT, +}; +use crate::{Error, Result}; + +/// `nr_iid_par_tab[iid_mode]` / `nr_icc_par_tab[icc_mode]` — Tables +/// 8.24 / 8.27 (modes 6 and 7 are reserved). +const NR_PAR_TAB: [usize; 6] = [10, 20, 34, 10, 20, 34]; + +/// `nr_ipdopd_par_tab[iid_mode]` — Table 8.24. +const NR_IPDOPD_PAR_TAB: [usize; 6] = [5, 11, 17, 5, 11, 17]; + +/// `num_env_tab[frame_class][num_env_idx]` — Table 8.29. +const NUM_ENV_TAB: [[usize; 4]; 2] = [[0, 1, 2, 4], [1, 2, 3, 4]]; + +/// The persistent `ps_data()` configuration (the `enable_ps_header` +/// block of Table 8.9): which parameters are transmitted and on which +/// band/quantization grid (Tables 8.24 / 8.27). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PsConfig { + /// `enable_iid`. + pub enable_iid: bool, + /// `iid_mode` (0..=5; 6/7 reserved). Meaningful when `enable_iid`. + pub iid_mode: u8, + /// `enable_icc`. + pub enable_icc: bool, + /// `icc_mode` (0..=5; 6/7 reserved). Meaningful when `enable_icc`. + pub icc_mode: u8, + /// `enable_ext` — whether the extension layer (IPD/OPD) may be + /// present. + pub enable_ext: bool, +} + +impl PsConfig { + /// Number of IID parameters per envelope (Table 8.24). + #[must_use] + pub fn nr_iid_par(&self) -> usize { + if self.enable_iid { + NR_PAR_TAB[usize::from(self.iid_mode)] + } else { + 0 + } + } + + /// Number of ICC parameters per envelope (Table 8.27). + #[must_use] + pub fn nr_icc_par(&self) -> usize { + if self.enable_icc { + NR_PAR_TAB[usize::from(self.icc_mode)] + } else { + 0 + } + } + + /// Number of IPD/OPD parameters per envelope (Table 8.24 — coupled + /// to the IID configuration). + #[must_use] + pub fn nr_ipdopd_par(&self) -> usize { + if self.enable_iid { + NR_IPDOPD_PAR_TAB[usize::from(self.iid_mode)] + } else { + 0 + } + } + + /// `iid_quant` — Table 8.24: modes 3..=5 use the fine (±15, + /// Table 8.26) grid, modes 0..=2 the default (±7, Table 8.25). + #[must_use] + pub fn iid_quant_fine(&self) -> bool { + self.iid_mode >= 3 + } + + /// The Table 8.24 IID index bound: 7 (default grid) or 15 (fine). + #[must_use] + pub fn iid_bound(&self) -> i32 { + if self.iid_quant_fine() { + 15 + } else { + 7 + } + } +} + +/// One parsed `ps_data()` element: the effective configuration plus +/// the raw (still differential) parameter deltas of each envelope. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PsData { + /// `enable_ps_header` — whether this element carried a fresh + /// configuration block. + pub header_present: bool, + /// The effective configuration (fresh or inherited). + pub config: PsConfig, + /// `frame_class` — `false` = FIX_BORDERS, `true` = VAR_BORDERS. + pub frame_class: bool, + /// `num_env` (Table 8.29). `0` = hold the previous parameters. + pub num_env: usize, + /// `border_position[e]` (5 bits each) when VAR_BORDERS. + pub border_position: Vec, + /// `iid_dt[e]` — time (`true`) vs frequency differential. + pub iid_dt: Vec, + /// Raw IID deltas per envelope (`nr_iid_par` each). + pub iid_deltas: Vec>, + /// `icc_dt[e]`. + pub icc_dt: Vec, + /// Raw ICC deltas per envelope (`nr_icc_par` each). + pub icc_deltas: Vec>, + /// `enable_ipdopd` (extension layer, Table 8.10); `false` when no + /// extension was present. + pub enable_ipdopd: bool, + /// `ipd_dt[e]`. + pub ipd_dt: Vec, + /// Raw IPD deltas per envelope (`nr_ipdopd_par` each). + pub ipd_deltas: Vec>, + /// `opd_dt[e]`. + pub opd_dt: Vec, + /// Raw OPD deltas per envelope. + pub opd_deltas: Vec>, +} + +impl PsData { + /// Parse one `ps_data()` element (Table 8.9). + /// + /// `prev_config` is the configuration in force from the last + /// element that carried `enable_ps_header == 1`. Returns + /// `Ok(None)` when the element carries no header and no previous + /// configuration exists (§8.6.5.1: output mono until then) — + /// the payload bits are consumed either way. + pub fn parse( + reader: &mut BitReader<'_>, + prev_config: Option<&PsConfig>, + ) -> Result> { + let header_present = read_flag(reader)?; + let config = if header_present { + let enable_iid = read_flag(reader)?; + let mut iid_mode = 0u8; + if enable_iid { + iid_mode = read(reader, 3)? as u8; + if iid_mode > 5 { + return Err(Error::PsDataInvalid); + } + } + let enable_icc = read_flag(reader)?; + let mut icc_mode = 0u8; + if enable_icc { + icc_mode = read(reader, 3)? as u8; + if icc_mode > 5 { + return Err(Error::PsDataInvalid); + } + } + let enable_ext = read_flag(reader)?; + PsConfig { + enable_iid, + iid_mode, + enable_icc, + icc_mode, + enable_ext, + } + } else { + match prev_config { + Some(c) => *c, + // §8.6.5.1: not yet decodable — a conformant stream + // starts with a header'd element; consume nothing more + // and signal "mono until a header arrives". + None => return Ok(None), + } + }; + + let frame_class = read_flag(reader)?; + let num_env_idx = read(reader, 2)? as usize; + let num_env = NUM_ENV_TAB[usize::from(frame_class)][num_env_idx]; + + let mut border_position = Vec::new(); + if frame_class { + for _ in 0..num_env { + border_position.push(read(reader, 5)? as u8); + } + } + + let nr_iid = config.nr_iid_par(); + let mut iid_dt = Vec::with_capacity(num_env); + let mut iid_deltas = Vec::with_capacity(num_env); + if config.enable_iid { + let fine = config.iid_quant_fine(); + for _ in 0..num_env { + let dt = read_flag(reader)?; + iid_dt.push(dt); + let table: &[(u8, u32)] = match (fine, dt) { + (false, false) => &HUFF_IID_DF, + (false, true) => &HUFF_IID_DT, + (true, false) => &HUFF_IID_FINE_DF, + (true, true) => &HUFF_IID_FINE_DT, + }; + let lav = if fine { 30 } else { 14 }; + let mut row = Vec::with_capacity(nr_iid); + for _ in 0..nr_iid { + row.push(ps_huff_dec(reader, table, lav)?); + } + iid_deltas.push(row); + } + } + + let nr_icc = config.nr_icc_par(); + let mut icc_dt = Vec::with_capacity(num_env); + let mut icc_deltas = Vec::with_capacity(num_env); + if config.enable_icc { + for _ in 0..num_env { + let dt = read_flag(reader)?; + icc_dt.push(dt); + let table: &[(u8, u32)] = if dt { &HUFF_ICC_DT } else { &HUFF_ICC_DF }; + let mut row = Vec::with_capacity(nr_icc); + for _ in 0..nr_icc { + row.push(ps_huff_dec(reader, table, 7)?); + } + icc_deltas.push(row); + } + } + + // Extension layer (Tables 8.9/8.10): byte-counted, id-tagged. + let mut enable_ipdopd = false; + let mut ipd_dt = Vec::new(); + let mut ipd_deltas = Vec::new(); + let mut opd_dt = Vec::new(); + let mut opd_deltas = Vec::new(); + if config.enable_ext { + let mut cnt = read(reader, 4)?; + if cnt == 15 { + cnt += read(reader, 8)?; + } + let mut num_bits_left = i64::from(8 * cnt); + let nr_ipdopd = config.nr_ipdopd_par(); + while num_bits_left > 7 { + let id = read(reader, 2)?; + num_bits_left -= 2; + if id == 0 { + // ps_extension(0): optional IPD/OPD + reserved bit. + let start = reader.bit_position(); + enable_ipdopd = read_flag(reader)?; + if enable_ipdopd { + for _ in 0..num_env { + let dt_i = read_flag(reader)?; + ipd_dt.push(dt_i); + let t: &[(u8, u32)] = if dt_i { &HUFF_IPD_DT } else { &HUFF_IPD_DF }; + let mut row = Vec::with_capacity(nr_ipdopd); + for _ in 0..nr_ipdopd { + row.push(ps_huff_dec(reader, t, 0)?); + } + ipd_deltas.push(row); + let dt_o = read_flag(reader)?; + opd_dt.push(dt_o); + let t: &[(u8, u32)] = if dt_o { &HUFF_OPD_DT } else { &HUFF_OPD_DF }; + let mut row = Vec::with_capacity(nr_ipdopd); + for _ in 0..nr_ipdopd { + row.push(ps_huff_dec(reader, t, 0)?); + } + opd_deltas.push(row); + } + } + let _reserved_ps = read_flag(reader)?; + num_bits_left -= (reader.bit_position() - start) as i64; + } else { + // Unknown extension id: the remaining block is fill. + skip_bits(reader, num_bits_left)?; + num_bits_left = 0; + } + } + if num_bits_left < 0 { + return Err(Error::PsDataInvalid); + } + // fill_bits. + skip_bits(reader, num_bits_left)?; + } + + Ok(Some(PsData { + header_present, + config, + frame_class, + num_env, + border_position, + iid_dt, + iid_deltas, + icc_dt, + icc_deltas, + enable_ipdopd, + ipd_dt, + ipd_deltas, + opd_dt, + opd_deltas, + })) + } +} + +/// Cross-frame differential state: the absolute parameter indices of +/// the previous frame's last envelope, plus the band counts they were +/// decoded at (a mode change forces frequency-differential coding on +/// the first envelope, §8.5.2). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PsIndexState { + /// Last-envelope absolute IID indices. + pub iid: Vec, + /// Last-envelope absolute ICC indices. + pub icc: Vec, + /// Last-envelope absolute IPD indices (0..8). + pub ipd: Vec, + /// Last-envelope absolute OPD indices (0..8). + pub opd: Vec, +} + +/// The resolved (absolute-index) parameters of one `ps_data()` +/// element: `num_env` rows per enabled parameter kind. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PsIndices { + /// Absolute IID indices per envelope (Table 8.25/8.26 domain). + pub iid: Vec>, + /// Absolute ICC indices per envelope (Table 8.28 domain, 0..=7). + pub icc: Vec>, + /// Absolute IPD indices per envelope (Table 8.31 ladder, 0..8). + pub ipd: Vec>, + /// Absolute OPD indices per envelope. + pub opd: Vec>, +} + +impl PsData { + /// Resolve the differential deltas to absolute indices against + /// `state` (§8.5.2 `iid_par[e][b]` accumulation), updating `state` + /// to this element's last envelope. Time-differential envelope 0 + /// references the previous frame's last envelope; when the + /// previous state has a different parameter count (mode change — + /// the spec forces `*_dt[0] == 0` there) a zero history is used + /// for robustness. IID/ICC results are range-checked; IPD/OPD + /// accumulate modulo 8. + pub fn resolve(&self, state: &mut PsIndexState) -> Result { + let mut out = PsIndices::default(); + if self.num_env == 0 { + // Parameters held (§8.6.4.6.5); state unchanged. + return Ok(out); + } + let bound = self.config.iid_bound(); + out.iid = resolve_kind( + &self.iid_deltas, + &self.iid_dt, + &mut state.iid, + self.config.nr_iid_par(), + Some((-bound, bound)), + )?; + out.icc = resolve_kind( + &self.icc_deltas, + &self.icc_dt, + &mut state.icc, + self.config.nr_icc_par(), + Some((0, 7)), + )?; + if self.enable_ipdopd { + out.ipd = resolve_kind( + &self.ipd_deltas, + &self.ipd_dt, + &mut state.ipd, + self.config.nr_ipdopd_par(), + None, + )?; + out.opd = resolve_kind( + &self.opd_deltas, + &self.opd_dt, + &mut state.opd, + self.config.nr_ipdopd_par(), + None, + )?; + } else { + // §8.5.2: no IPD/OPD data → parameters are index 0. + state.ipd.clear(); + state.opd.clear(); + } + Ok(out) + } +} + +/// Accumulate one parameter kind's deltas to absolute indices. +/// `range = None` selects the modulo-8 phase accumulation (Table +/// 8.31); `Some((lo, hi))` the range-checked linear accumulation. +fn resolve_kind( + deltas: &[Vec], + dt: &[bool], + state: &mut Vec, + nr_par: usize, + range: Option<(i32, i32)>, +) -> Result>> { + if deltas.is_empty() { + // Parameter kind disabled this frame; reset its history so a + // later re-enable starts from the defaults (§8.5.2 index 0). + state.clear(); + return Ok(Vec::new()); + } + let mut rows: Vec> = Vec::with_capacity(deltas.len()); + for (e, row) in deltas.iter().enumerate() { + let mut abs = Vec::with_capacity(nr_par); + if dt[e] { + // Time differential: reference envelope e-1 (or the + // previous frame's last envelope; zeros on a mode change). + let prev_row: &[i32] = if e > 0 { + &rows[e - 1] + } else if state.len() == nr_par { + state + } else { + &[] + }; + for (b, &d) in row.iter().enumerate().take(nr_par) { + let prev = prev_row.get(b).copied().unwrap_or(0); + abs.push(accumulate(prev, d, range)?); + } + } else { + // Frequency differential: band b references band b-1, + // band 0 references index 0. + let mut prev = 0i32; + for &d in row { + prev = accumulate(prev, d, range)?; + abs.push(prev); + } + } + rows.push(abs); + } + *state = rows.last().cloned().unwrap_or_default(); + Ok(rows) +} + +#[inline] +fn accumulate(prev: i32, delta: i32, range: Option<(i32, i32)>) -> Result { + match range { + Some((lo, hi)) => { + let v = prev + delta; + if v < lo || v > hi { + return Err(Error::PsDataInvalid); + } + Ok(v) + } + None => Ok((prev + delta).rem_euclid(8)), + } +} + +#[inline] +fn read(reader: &mut BitReader<'_>, n: u32) -> Result { + reader.read_u32(n).map_err(|_| Error::PsDataInvalid) +} + +#[inline] +fn read_flag(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::PsDataInvalid) +} + +#[inline] +fn skip_bits(reader: &mut BitReader<'_>, mut n: i64) -> Result<()> { + while n > 0 { + let step = n.min(32) as u32; + read(reader, step)?; + n -= i64::from(step); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitWriter; + + /// Write the 1-bit codeword for delta 0 in the coarse IID (`0`), + /// ICC (`0`) tables. + fn write_zero_deltas(w: &mut BitWriter, n: usize) { + for _ in 0..n { + w.write_bit(false); + } + } + + /// Minimal header'd element: IID mode 0 (10 bands), ICC mode 0, + /// no ext, FIX_BORDERS, 1 envelope, all-zero freq deltas. + fn build_min() -> Vec { + let mut w = BitWriter::new(); + w.write_bit(true); // enable_ps_header + w.write_bit(true); // enable_iid + w.write_u32(0, 3); // iid_mode = 0 + w.write_bit(true); // enable_icc + w.write_u32(0, 3); // icc_mode = 0 + w.write_bit(false); // enable_ext + w.write_bit(false); // frame_class = FIX + w.write_u32(1, 2); // num_env_idx = 1 -> num_env = 1 + w.write_bit(false); // iid_dt[0] = freq + write_zero_deltas(&mut w, 10); + w.write_bit(false); // icc_dt[0] = freq + write_zero_deltas(&mut w, 10); + w.finish() + } + + #[test] + fn parses_minimal_headered_element() { + let bytes = build_min(); + let mut r = BitReader::new(&bytes); + let ps = PsData::parse(&mut r, None).unwrap().unwrap(); + assert!(ps.header_present); + assert!(ps.config.enable_iid); + assert_eq!(ps.config.nr_iid_par(), 10); + assert_eq!(ps.config.nr_icc_par(), 10); + assert!(!ps.config.iid_quant_fine()); + assert_eq!(ps.num_env, 1); + assert_eq!(ps.iid_deltas[0], vec![0; 10]); + assert_eq!(ps.icc_deltas[0], vec![0; 10]); + + let mut st = PsIndexState::default(); + let idx = ps.resolve(&mut st).unwrap(); + assert_eq!(idx.iid[0], vec![0; 10]); + assert_eq!(idx.icc[0], vec![0; 10]); + assert_eq!(st.iid, vec![0; 10]); + } + + #[test] + fn headerless_without_prior_config_is_mono_signal() { + let mut w = BitWriter::new(); + w.write_bit(false); // enable_ps_header = 0 + w.write_bit(false); + w.write_u32(0, 2); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(PsData::parse(&mut r, None).unwrap().is_none()); + } + + #[test] + fn headerless_inherits_previous_config() { + // First frame with header, then a headerless frame reusing it. + let bytes = build_min(); + let mut r = BitReader::new(&bytes); + let ps0 = PsData::parse(&mut r, None).unwrap().unwrap(); + + let mut w = BitWriter::new(); + w.write_bit(false); // enable_ps_header = 0 + w.write_bit(false); // frame_class + w.write_u32(1, 2); // num_env = 1 + w.write_bit(true); // iid_dt[0] = time + for _ in 0..10 { + w.write_bit(false); // coarse dt zero-delta codeword `0` + } + w.write_bit(true); // icc_dt[0] = time + for _ in 0..10 { + w.write_bit(false); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ps1 = PsData::parse(&mut r, Some(&ps0.config)).unwrap().unwrap(); + assert!(!ps1.header_present); + assert_eq!(ps1.config, ps0.config); + assert!(ps1.iid_dt[0]); + } + + /// Frequency-differential accumulation: deltas +1 per band ramp + /// the index; time-differential carries envelope-to-envelope. + #[test] + fn differential_accumulation_freq_then_time() { + let mut w = BitWriter::new(); + w.write_bit(true); // header + w.write_bit(true); // enable_iid + w.write_u32(0, 3); // iid_mode 0 + w.write_bit(false); // enable_icc = 0 + w.write_bit(false); // enable_ext = 0 + w.write_bit(false); // FIX + w.write_u32(2, 2); // num_env = 2 + // env 0: freq deltas +1 ×7 then -1 ×3 + // (coarse df: +1 = `100`, -1 = `101`). + w.write_bit(false); + for _ in 0..7 { + w.write_u32(0b100, 3); + } + for _ in 0..3 { + w.write_u32(0b101, 3); + } + // env 1: time deltas -1 ×10 (coarse dt: -1 = `10`). + w.write_bit(true); + for _ in 0..10 { + w.write_u32(0b10, 2); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ps = PsData::parse(&mut r, None).unwrap().unwrap(); + let mut st = PsIndexState::default(); + let idx = ps.resolve(&mut st).unwrap(); + // env 0 freq ramp: +1 ×7 then -1 ×3 → 1..7 then 6,5,4. + assert_eq!(idx.iid[0], vec![1, 2, 3, 4, 5, 6, 7, 6, 5, 4]); + // env 1 subtracts 1 per band from env 0. + assert_eq!(idx.iid[1], vec![0, 1, 2, 3, 4, 5, 6, 5, 4, 3]); + // State carries env 1 forward. + assert_eq!(st.iid, idx.iid[1]); + // ICC disabled: no rows, history cleared. + assert!(idx.icc.is_empty()); + assert!(st.icc.is_empty()); + } + + /// A frequency ramp that leaves the Table 8.24 index range is + /// rejected. + #[test] + fn out_of_range_iid_rejected() { + let mut w = BitWriter::new(); + w.write_bit(true); // header + w.write_bit(true); // enable_iid + w.write_u32(0, 3); // iid_mode 0 (bound ±7) + w.write_bit(false); // enable_icc + w.write_bit(false); // enable_ext + w.write_bit(false); // FIX + w.write_u32(1, 2); // num_env = 1 + w.write_bit(false); // freq + for _ in 0..10 { + w.write_u32(0b100, 3); // +1 each → crosses +7 at band 7 + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ps = PsData::parse(&mut r, None).unwrap().unwrap(); + let mut st = PsIndexState::default(); + assert!(matches!(ps.resolve(&mut st), Err(Error::PsDataInvalid))); + } + + /// VAR_BORDERS carries 5-bit border positions; the extension + /// layer decodes IPD/OPD with modulo-8 accumulation. + #[test] + fn var_borders_and_ipdopd_extension() { + let mut w = BitWriter::new(); + w.write_bit(true); // header + w.write_bit(true); // enable_iid + w.write_u32(0, 3); // iid_mode 0 → nr_ipdopd_par = 5 + w.write_bit(false); // enable_icc + w.write_bit(true); // enable_ext + w.write_bit(true); // frame_class = VAR + w.write_u32(0, 2); // num_env_idx 0 → num_env = 1 (VAR column) + w.write_u32(15, 5); // border_position[0] + w.write_bit(false); // iid_dt[0] = freq + for _ in 0..10 { + w.write_bit(false); // zero deltas + } + // Extension: ps_extension_size counts whole bytes. Body: + // id(2) + enable_ipdopd(1) + ipd_dt(1) + 5×ipd deltas + + // opd_dt(1) + 5×opd deltas + reserved(1) then fill. Zero + // phase deltas are the 1-bit codeword `1`. + let mut body = BitWriter::new(); + body.write_u32(0, 2); // ps_extension_id = 0 + body.write_bit(true); // enable_ipdopd + body.write_bit(false); // ipd_dt[0] = freq + for _ in 0..5 { + body.write_bit(true); // delta 0 + } + body.write_bit(false); // opd_dt[0] + for _ in 0..5 { + body.write_bit(true); + } + body.write_bit(false); // reserved_ps + let body_bytes = body.finish(); // padded to whole bytes = fill + w.write_u32(body_bytes.len() as u32, 4); // ps_extension_size + for &b in &body_bytes { + w.write_u32(u32::from(b), 8); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ps = PsData::parse(&mut r, None).unwrap().unwrap(); + assert!(ps.frame_class); + assert_eq!(ps.border_position, vec![15]); + assert!(ps.enable_ipdopd); + assert_eq!(ps.ipd_deltas[0], vec![0; 5]); + let mut st = PsIndexState::default(); + let idx = ps.resolve(&mut st).unwrap(); + assert_eq!(idx.ipd[0], vec![0; 5]); + assert_eq!(idx.opd[0], vec![0; 5]); + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_decoder.rs b/crates/vendor/oxideav-aac/src/ps_decoder.rs new file mode 100644 index 00000000..4a428cec --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_decoder.rs @@ -0,0 +1,307 @@ +//! PS frame driver — ISO/IEC 14496-3:2009 Annex 8.A (combination of +//! the SBR tool with the parametric stereo tool). +//! +//! Composes the whole §8.6.4 chain per stereo frame: `ps_data()` +//! parse (with the persistent header configuration), differential +//! index resolution, the hybrid analysis of the Annex 8.A.3 `Xinput` +//! matrix (32 SBR slots + 6 look-ahead slots from `XLow`), +//! de-correlation with the per-frame partial reset above the +//! SBR-generated spectrum (`kmax = k_x + M + 7` hybrid channels for +//! 10/20 stereo bands, `+ 27` for 34 — the split-region offsets), the +//! §8.6.4.6 stereo mixing, and the hybrid synthesis back to two +//! 64-band QMF matrices ready for the final synthesis filterbanks. +//! +//! Per §8.6.5.1 the decoder stays *inactive* (mono output duplicated +//! by the caller) until the first `ps_data()` that carries +//! `enable_ps_header == 1` arrives; per Annex 8.A.3 a frame with no +//! `ps_data()` after activation holds the previous parameters, and a +//! *missing previous* `ps_data()` forces a full de-correlator reset. +//! Table 8.44 picks the stereo band count from the IID/ICC modes +//! (either at 34 bands → 34, else 20); a switch re-maps the retained +//! mixing coefficients (Table 8.47) and resets the hybrid / +//! de-correlator state. +//! +//! All truth from ISO/IEC 14496-3:2009 subpart 8 + Annex 8.A staged +//! under `docs/audio/aac/`. + +use oxideav_core::bits::BitReader; + +use crate::ps_data::{PsConfig, PsData, PsIndexState}; +use crate::ps_decorr::PsDecorr; +use crate::ps_hybrid::{synthesize, HybridConfig, PsHybrid}; +use crate::ps_stereo::PsStereo; +use crate::sbr_qmf::Complex; +use crate::Result; + +/// A stereo pair of 64-band QMF matrices (`NUM_QMF_SLOTS` slots). +pub type QmfPair = (Vec<[Complex; 64]>, Vec<[Complex; 64]>); + +/// The Annex 8.A PS decoder: one instance per SBR channel element. +#[derive(Debug)] +pub struct PsDecoder { + /// Persistent `enable_ps_header` configuration (§8.5.2). + config: Option, + /// Cross-frame differential-index state. + idx_state: PsIndexState, + hybrid: PsHybrid, + decorr: PsDecorr, + stereo: PsStereo, + /// Whether the previous frame carried a `ps_data()` element + /// (Annex 8.A.3 full-reset rule). + prev_frame_had_ps: bool, + /// Whether a decodable (header-carrying) `ps_data()` has arrived. + active: bool, +} + +impl Default for PsDecoder { + fn default() -> Self { + PsDecoder::new() + } +} + +impl PsDecoder { + /// A fresh, inactive PS decoder (20-band configuration until the + /// first header says otherwise). + #[must_use] + pub fn new() -> Self { + PsDecoder { + config: None, + idx_state: PsIndexState::default(), + hybrid: PsHybrid::new(HybridConfig::Bands1020), + decorr: PsDecorr::new(HybridConfig::Bands1020), + stereo: PsStereo::new(20), + prev_frame_had_ps: false, + active: false, + } + } + + /// Whether a decodable `ps_data()` has been received — before + /// this, the caller outputs the mono signal on both channels. + #[must_use] + pub fn active(&self) -> bool { + self.active + } + + /// Process one stereo frame. + /// + /// * `payload` — the raw `sbr_extension()` body bytes carrying + /// `ps_data()` (already stripped of the 2-bit extension id), or + /// `None` when this frame transmitted no PS data (parameters + /// hold). + /// * `x_input` — the Annex 8.A.3 `Xinput` matrix: + /// `NUM_QMF_SLOTS + LOOKAHEAD` slots of 64 QMF bands (the + /// look-ahead tail needs only the split bands populated). + /// * `kx_plus_m` — `k_x + M` (§4.6.18.3.2.2): the first QMF band + /// above the SBR-generated spectrum, for the per-frame partial + /// de-correlator reset (pass 32 for a pure-upsampled frame). + /// + /// Returns `Ok(None)` while inactive (§8.6.5.1 — the caller + /// duplicates the mono synthesis), otherwise the left/right QMF + /// matrices for two independent §4.6.18.4.2 synthesis banks. + pub fn process( + &mut self, + payload: Option<&[u8]>, + x_input: &[[Complex; 64]], + kx_plus_m: usize, + ) -> Result> { + // Parse (and activate on the first header'd element). + let parsed: Option = match payload { + Some(bytes) => { + let mut reader = BitReader::new(bytes); + PsData::parse(&mut reader, self.config.as_ref())? + } + None => None, + }; + if let Some(ps) = &parsed { + self.config = Some(ps.config); + self.active = true; + } + let Some(config) = self.config else { + // Not yet decodable: mono until a header arrives. + self.prev_frame_had_ps = payload.is_some(); + return Ok(None); + }; + if !self.active { + self.prev_frame_had_ps = payload.is_some(); + return Ok(None); + } + + // Table 8.44: 34 stereo bands iff either parameter kind runs + // on the 34-band grid; disabled kinds count as 20. + let bands34 = (config.enable_iid && config.iid_mode % 3 == 2) + || (config.enable_icc && config.icc_mode % 3 == 2); + let hcfg = if bands34 { + HybridConfig::Bands34 + } else { + HybridConfig::Bands1020 + }; + if hcfg != self.hybrid.config() { + // Table 8.47: instantaneous filterbank switch, coefficient + // re-map, de-correlator reset. + self.hybrid.reset(hcfg); + self.decorr = PsDecorr::new(hcfg); + self.stereo.switch_bands(if bands34 { 34 } else { 20 }); + } + + // Annex 8.A.3 resets: full when the previous frame had no + // ps_data(); otherwise partial above the SBR spectrum. + if !self.prev_frame_had_ps { + self.decorr.reset_bands(0); + } else { + let off = if bands34 { 27 } else { 7 }; + let kmax = (kx_plus_m + off).min(hcfg.nr_bands()); + self.decorr.reset_bands(kmax); + } + + // The hold element for a frame with no (new) parameters. + let ps = parsed.unwrap_or_else(|| hold_element(config)); + let idx = ps.resolve(&mut self.idx_state)?; + + // Hybrid analysis → de-correlation → stereo mixing → + // hybrid synthesis. + let s = self.hybrid.analyze(x_input)?; + let d = self.decorr.process(&s)?; + let (l, r) = self.stereo.process(&ps, &idx, hcfg, &s, &d)?; + let l_qmf = synthesize(hcfg, &l); + let r_qmf = synthesize(hcfg, &r); + + self.prev_frame_had_ps = payload.is_some(); + Ok(Some((l_qmf, r_qmf))) + } +} + +/// A `num_env == 0` element holding the previous parameters +/// (§8.6.4.6.5 / Table 8.50–8.52). +fn hold_element(config: PsConfig) -> PsData { + PsData { + header_present: false, + config, + frame_class: false, + num_env: 0, + border_position: Vec::new(), + iid_dt: Vec::new(), + iid_deltas: Vec::new(), + icc_dt: Vec::new(), + icc_deltas: Vec::new(), + enable_ipdopd: false, + ipd_dt: Vec::new(), + ipd_deltas: Vec::new(), + opd_dt: Vec::new(), + opd_deltas: Vec::new(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ps_hybrid::{LOOKAHEAD, NUM_QMF_SLOTS}; + use oxideav_core::bits::BitWriter; + + /// Build a header'd one-envelope ps_data payload: coarse IID with + /// a uniform index, ICC index 0 everywhere (freq differential). + fn payload(iid_idx: i32) -> Vec { + let mut w = BitWriter::new(); + w.write_bit(true); // enable_ps_header + w.write_bit(true); // enable_iid + w.write_u32(0, 3); // iid_mode 0 + w.write_bit(true); // enable_icc + w.write_u32(0, 3); // icc_mode 0 + w.write_bit(false); // enable_ext + w.write_bit(false); // FIX + w.write_u32(1, 2); // num_env = 1 + w.write_bit(false); // iid_dt = freq + let (len, code) = crate::ps_huffman::HUFF_IID_DF[(iid_idx + 14) as usize]; + w.write_u32(code, u32::from(len)); + let (l0, c0) = crate::ps_huffman::HUFF_IID_DF[14]; + for _ in 1..10 { + w.write_u32(c0, u32::from(l0)); + } + w.write_bit(false); // icc_dt = freq + let (li, ci) = crate::ps_huffman::HUFF_ICC_DF[7]; + for _ in 0..10 { + w.write_u32(ci, u32::from(li)); + } + w.finish() + } + + fn x_input_ones() -> Vec<[Complex; 64]> { + (0..NUM_QMF_SLOTS + LOOKAHEAD) + .map(|_| [Complex::new(1.0, 0.0); 64]) + .collect() + } + + /// Inactive until a header'd element arrives; then the stereo + /// output appears and a hold frame keeps producing it. + #[test] + fn activation_and_hold() { + let mut dec = PsDecoder::new(); + let x = x_input_ones(); + // No payload → inactive. + assert!(dec.process(None, &x, 32).unwrap().is_none()); + // Headerless payload with no prior config → still inactive. + let mut w = BitWriter::new(); + w.write_bit(false); // enable_ps_header = 0 + w.write_bit(false); // frame_class + w.write_u32(0, 2); // num_env_idx → num_env = 0 + let headerless = w.finish(); + assert!(dec.process(Some(&headerless), &x, 32).unwrap().is_none()); + // Header'd element → active, stereo out. + let p = payload(7); // +25 dB left + let out = dec.process(Some(&p), &x, 32).unwrap(); + let (l, r) = out.expect("active after header"); + assert_eq!(l.len(), NUM_QMF_SLOTS); + assert_eq!(r.len(), NUM_QMF_SLOTS); + // Hold frame (no payload) keeps producing stereo. + assert!(dec.process(None, &x, 32).unwrap().is_some()); + } + + /// A large positive IID tilts the energy to the left channel + /// (steady state, after a couple of frames of interpolation). + #[test] + fn iid_tilts_energy_left() { + let mut dec = PsDecoder::new(); + let x = x_input_ones(); + let p = payload(7); + let mut l_e = 0.0f64; + let mut r_e = 0.0f64; + for f in 0..4 { + let out = dec.process(Some(&p), &x, 32).unwrap().unwrap(); + if f >= 2 { + for n in 0..NUM_QMF_SLOTS { + for k in 0..64 { + l_e += out.0[n][k].norm_sqr(); + r_e += out.1[n][k].norm_sqr(); + } + } + } + } + // 25 dB IID → power ratio 10^2.5 ≈ 316; allow generous slack + // for the decorrelated component and filter transients. + assert!(l_e > 50.0 * r_e, "left {l_e} not dominant over right {r_e}"); + } + + /// IID 0 + ICC 1 reproduces the mono signal identically on both + /// channels in steady state (h11 = h12 = 1, h21 = h22 = 0). + #[test] + fn neutral_cues_give_dual_mono() { + let mut dec = PsDecoder::new(); + let x = x_input_ones(); + let p = payload(0); + let mut last = None; + for _ in 0..3 { + last = dec.process(Some(&p), &x, 32).unwrap(); + } + let (l, r) = last.unwrap(); + for n in 0..NUM_QMF_SLOTS { + for k in 0..64 { + let d = l[n][k] - r[n][k]; + assert!(d.norm_sqr() < 1e-20, "slot {n} band {k}"); + // And the mono signal passes through: DC input in + // every QMF band re-appears (the hybrid partition is + // exact). + } + } + let d = l[16][10] - Complex::new(1.0, 0.0); + assert!(d.norm_sqr() < 1e-18, "mono pass-through broken: {d:?}"); + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_decorr.rs b/crates/vendor/oxideav-aac/src/ps_decorr.rs new file mode 100644 index 00000000..f0229468 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_decorr.rs @@ -0,0 +1,519 @@ +//! PS de-correlation — ISO/IEC 14496-3:2009 §8.6.4.5. +//! +//! The stereo reconstruction mixes the mono hybrid signal `s_k(n)` +//! with a de-correlated version `d_k(n)` of itself. Per §8.6.4.5.2 +//! the first `NR_ALLPASS_BANDS` hybrid channels run through a chain +//! of `NR_ALLPASS_LINKS = 3` complex all-pass sections behind a +//! 2-slot delay and a fractional-delay rotation: +//! +//! ```text +//! H_k(z) = z⁻² · φ_fract(k) · Π_m (Q(k,m)·z^(−d(m)) − a(m)·g(k)) +//! / (1 − a(m)·g(k)·Q(k,m)·z^(−d(m))) +//! ``` +//! +//! with `a(m) = {0.65143905753106, 0.56471812200776, 0.48954165955695}`, +//! `d(m) = {3, 4, 5}` (Table 8.39), the unit rotations +//! `Q(k,m) = exp(−iπ·q(m)·fcenter(k))` (`q = {0.43, 0.75, 0.347}`, +//! Table 8.42), `φ_fract(k) = exp(−iπ·q_φ·fcenter(k))` (`q_φ = 0.39`), +//! and the frequency-dependent decay +//! `g(k) = max(0, 1 − DECAY_SLOPE·(k − DECAY_CUTOFF))`. The centre +//! frequencies `fcenter(k)` come from Table 8.40 / 8.41 for the split +//! region and the closed forms `k + 1/2 − 7` / `k + 1/2 − 27` above +//! it. Bands `NR_ALLPASS_BANDS..` use a plain delay: 14 slots up to +//! `SHORT_DELAY_BAND`, 1 slot above. +//! +//! §8.6.4.5.3–5.4 duck the de-correlated signal at transients: the +//! per-stereo-band input power is peak-decayed +//! (`α = 0.76592833836465`, Table 8.43), both the power and the +//! peak-minus-power difference are smoothed with the one-pole +//! `H_smooth` (`a_smooth = 0.25`), and wherever +//! `γ·PSmoothPeakDecayDiff > PSmoothNrg` (`γ = 1.5`) the output is +//! scaled by their ratio. +//! +//! [`PsDecorr`] carries every filter/delay/detector state across +//! frames and exposes the Annex 8.A.3 resets: `reset_bands(kmax)` +//! zeroes the state of hybrid channels `k ≥ kmax` each stereo frame +//! (the region above the SBR-generated spectrum), and a full reset +//! covers the "no `ps_data()` in the previous frame" rule. +//! +//! All truth from ISO/IEC 14496-3:2009 §8.6.4.5 / Annex 8.A staged +//! under `docs/audio/aac/`. + +use crate::ps_hybrid::HybridConfig; +use crate::ps_map::parameter_map; +use crate::sbr_qmf::Complex; +use crate::{Error, Result}; + +/// `DECAY_SLOPE` (§8.6.4.5.1). +const DECAY_SLOPE: f64 = 0.05; + +/// `a(m)` — all-pass filter coefficients (Table 8.39). +const A: [f64; 3] = [0.65143905753106, 0.56471812200776, 0.48954165955695]; + +/// `d(m)` — all-pass link delays (Table 8.39). +const D: [usize; 3] = [3, 4, 5]; + +/// `q(m)` — fractional delay lengths (Table 8.42). +const Q_FRACT: [f64; 3] = [0.43, 0.75, 0.347]; + +/// `q_φ` — fractional delay constant (§8.6.4.5.2). +const Q_PHI: f64 = 0.39; + +/// Peak decay factor `α` (Table 8.43). +const PEAK_DECAY: f64 = 0.76592833836465; + +/// Smoothing coefficient `a_smooth` (§8.6.4.5.1). +const A_SMOOTH: f64 = 0.25; + +/// Transient impact factor `γ` (§8.6.4.5.3). +const GAMMA: f64 = 1.5; + +/// Long delay for the non-all-pass mid bands (§8.6.4.5.2). +const LONG_DELAY: usize = 14; + +/// Table 8.40 — `fcenter_20(k)` for the split region (k = 0..10). +const F_CENTER_20: [f64; 10] = [ + -3.0 / 8.0, + -1.0 / 8.0, + 1.0 / 8.0, + 3.0 / 8.0, + 5.0 / 8.0, + 7.0 / 8.0, + 5.0 / 4.0, + 7.0 / 4.0, + 9.0 / 4.0, + 11.0 / 4.0, +]; + +/// Table 8.41 — `fcenter_34(k)` for the split region (k = 0..32). +const F_CENTER_34: [f64; 32] = [ + 1.0 / 12.0, + 3.0 / 12.0, + 5.0 / 12.0, + 7.0 / 12.0, + 9.0 / 12.0, + 11.0 / 12.0, + 13.0 / 12.0, + 15.0 / 12.0, + 17.0 / 12.0, + -5.0 / 12.0, + -3.0 / 12.0, + -1.0 / 12.0, + 17.0 / 8.0, + 19.0 / 8.0, + 5.0 / 8.0, + 7.0 / 8.0, + 9.0 / 8.0, + 11.0 / 8.0, + 13.0 / 8.0, + 15.0 / 8.0, + 9.0 / 4.0, + 11.0 / 4.0, + 13.0 / 4.0, + 7.0 / 4.0, + 17.0 / 4.0, + 11.0 / 4.0, + 13.0 / 4.0, + 15.0 / 4.0, + 17.0 / 4.0, + 19.0 / 4.0, + 21.0 / 4.0, + 15.0 / 4.0, +]; + +/// The §8.6.4.5.1 configuration constants that depend on the stereo +/// band count. +#[derive(Debug, Clone, Copy)] +struct DecorrConsts { + nr_par_bands: usize, + nr_bands: usize, + decay_cutoff: usize, + nr_allpass_bands: usize, + short_delay_band: usize, +} + +fn consts(config: HybridConfig) -> DecorrConsts { + match config { + HybridConfig::Bands1020 => DecorrConsts { + nr_par_bands: 20, + nr_bands: 71, + decay_cutoff: 10, + nr_allpass_bands: 30, + short_delay_band: 42, + }, + HybridConfig::Bands34 => DecorrConsts { + nr_par_bands: 34, + nr_bands: 91, + decay_cutoff: 32, + nr_allpass_bands: 50, + short_delay_band: 62, + }, + } +} + +/// `fcenter(k)` for the all-pass region (§8.6.4.5.2). +fn f_center(config: HybridConfig, k: usize) -> f64 { + match config { + HybridConfig::Bands1020 => { + if k < F_CENTER_20.len() { + F_CENTER_20[k] + } else { + k as f64 + 0.5 - 7.0 + } + } + HybridConfig::Bands34 => { + if k < F_CENTER_34.len() { + F_CENTER_34[k] + } else { + k as f64 + 0.5 - 27.0 + } + } + } +} + +/// Per-band all-pass state: the z⁻² input delay plus one direct-form +/// ring per link (`w[n] = u[n] + a·g·Q·w[n−d]`, +/// `v[n] = Q·w[n−d] − a·g·w[n]`). +#[derive(Debug, Clone)] +struct AllpassState { + /// z⁻² input history (index 0 = one slot ago). + in2: [Complex; 2], + /// Ring buffers for the three links (lengths 3, 4, 5). + w: [Vec; 3], + /// Ring positions. + pos: [usize; 3], +} + +impl AllpassState { + fn new() -> Self { + AllpassState { + in2: [Complex::default(); 2], + w: [ + vec![Complex::default(); D[0]], + vec![Complex::default(); D[1]], + vec![Complex::default(); D[2]], + ], + pos: [0; 3], + } + } + + fn reset(&mut self) { + self.in2 = [Complex::default(); 2]; + for (w, d) in self.w.iter_mut().zip(D) { + w.iter_mut().for_each(|c| *c = Complex::default()); + debug_assert_eq!(w.len(), d); + } + self.pos = [0; 3]; + } +} + +/// The §8.6.4.5 de-correlator (one instance per PS decoder). +#[derive(Debug, Clone)] +pub struct PsDecorr { + config: HybridConfig, + /// All-pass state per band `k < NR_ALLPASS_BANDS`. + allpass: Vec, + /// Pre-computed `φ_fract(k)` per all-pass band. + phi_fract: Vec, + /// Pre-computed `Q(k,m)·1` per all-pass band and link. + q_fract: Vec<[Complex; 3]>, + /// `g_DecaySlope(k)` per all-pass band. + g_decay: Vec, + /// Delay lines for the non-all-pass bands (14 or 1 slots each). + delay: Vec>, + /// Ring positions for `delay`. + delay_pos: Vec, + /// Transient detector state per stereo band. + peak_decay_nrg: Vec, + smooth_nrg: Vec, + smooth_peak_diff: Vec, +} + +impl PsDecorr { + /// A fresh de-correlator for `config`. + #[must_use] + pub fn new(config: HybridConfig) -> Self { + let c = consts(config); + let mut phi_fract = Vec::with_capacity(c.nr_allpass_bands); + let mut q_fract = Vec::with_capacity(c.nr_allpass_bands); + let mut g_decay = Vec::with_capacity(c.nr_allpass_bands); + for k in 0..c.nr_allpass_bands { + let f = f_center(config, k); + let arg = -core::f64::consts::PI * Q_PHI * f; + let (s, co) = arg.sin_cos(); + phi_fract.push(Complex::new(co, s)); + let mut qs = [Complex::default(); 3]; + for (m, q) in qs.iter_mut().enumerate() { + let arg = -core::f64::consts::PI * Q_FRACT[m] * f; + let (s, co) = arg.sin_cos(); + *q = Complex::new(co, s); + } + q_fract.push(qs); + let g = if k > c.decay_cutoff { + (1.0 - DECAY_SLOPE * (k as f64 - c.decay_cutoff as f64)).max(0.0) + } else { + 1.0 + }; + g_decay.push(g); + } + let mut delay = Vec::with_capacity(c.nr_bands - c.nr_allpass_bands); + for k in c.nr_allpass_bands..c.nr_bands { + let d = if k < c.short_delay_band { + LONG_DELAY + } else { + 1 + }; + delay.push(vec![Complex::default(); d]); + } + PsDecorr { + config, + allpass: vec![AllpassState::new(); c.nr_allpass_bands], + phi_fract, + q_fract, + g_decay, + delay_pos: vec![0; c.nr_bands - c.nr_allpass_bands], + delay, + peak_decay_nrg: vec![0.0; c.nr_par_bands], + smooth_nrg: vec![0.0; c.nr_par_bands], + smooth_peak_diff: vec![0.0; c.nr_par_bands], + } + } + + /// Annex 8.A.3 partial reset: zero the filter state of hybrid + /// channels `k ≥ kmax` (the region above the SBR-generated + /// spectrum), or the whole bank with `kmax = 0` (the "no + /// `ps_data()` in the previous frame" full reset). + pub fn reset_bands(&mut self, kmax: usize) { + let c = consts(self.config); + for k in kmax..c.nr_allpass_bands { + self.allpass[k].reset(); + } + for k in kmax.max(c.nr_allpass_bands)..c.nr_bands { + let i = k - c.nr_allpass_bands; + self.delay[i] + .iter_mut() + .for_each(|v| *v = Complex::default()); + self.delay_pos[i] = 0; + } + } + + /// De-correlate one stereo frame of hybrid slots (each + /// `nr_bands()` wide). Returns `d_k(n)` with the transient + /// attenuation applied; all state advances. + pub fn process(&mut self, s: &[Vec]) -> Result>> { + let c = consts(self.config); + let b_k = parameter_map(self.config); + if s.iter().any(|row| row.len() != c.nr_bands) { + return Err(Error::PsDataInvalid); + } + let mut out = vec![vec![Complex::default(); c.nr_bands]; s.len()]; + for (n, row) in s.iter().enumerate() { + // §8.6.4.5.3 transient detection at this slot. + let mut p = vec![0.0f64; c.nr_par_bands]; + for (k, v) in row.iter().enumerate() { + p[usize::from(b_k[k])] += v.norm_sqr(); + } + let mut g_ratio = vec![1.0f64; c.nr_par_bands]; + for i in 0..c.nr_par_bands { + let peak = if PEAK_DECAY * self.peak_decay_nrg[i] < p[i] { + p[i] + } else { + PEAK_DECAY * self.peak_decay_nrg[i] + }; + self.peak_decay_nrg[i] = peak; + self.smooth_nrg[i] += A_SMOOTH * (p[i] - self.smooth_nrg[i]); + self.smooth_peak_diff[i] += A_SMOOTH * (peak - p[i] - self.smooth_peak_diff[i]); + if GAMMA * self.smooth_peak_diff[i] > self.smooth_nrg[i] { + g_ratio[i] = self.smooth_nrg[i] / (GAMMA * self.smooth_peak_diff[i]); + } + } + + // §8.6.4.5.2 all-pass chain for the low bands. + for k in 0..c.nr_allpass_bands { + let st = &mut self.allpass[k]; + // z⁻² then φ_fract rotation. + let delayed = st.in2[1]; + st.in2[1] = st.in2[0]; + st.in2[0] = row[k]; + let mut u = self.phi_fract[k] * delayed; + // Three all-pass links. + let g = self.g_decay[k]; + for m in 0..3 { + let coef = A[m] * g; + let q = self.q_fract[k][m]; + let pos = st.pos[m]; + let w_d = st.w[m][pos]; + // w[n] = u[n] + a·g·Q·w[n−d] + let w_n = u + q * w_d * coef; + // v[n] = Q·w[n−d] − a·g·w[n] + u = q * w_d - w_n * coef; + st.w[m][pos] = w_n; + st.pos[m] = (pos + 1) % D[m]; + } + out[n][k] = u * g_ratio[usize::from(b_k[k])]; + } + + // Plain delays above. + for k in c.nr_allpass_bands..c.nr_bands { + let i = k - c.nr_allpass_bands; + let pos = self.delay_pos[i]; + let v = self.delay[i][pos]; + self.delay[i][pos] = row[k]; + self.delay_pos[i] = (pos + 1) % self.delay[i].len(); + out[n][k] = v * g_ratio[usize::from(b_k[k])]; + } + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ps_hybrid::HybridConfig; + + fn noise_slot(seed: u64, n: usize, nb: usize) -> Vec { + (0..nb) + .map(|k| { + let mut h = seed + .wrapping_mul(6364136223846793005) + .wrapping_add((n * 128 + k) as u64); + h ^= h >> 33; + h = h.wrapping_mul(0xff51afd7ed558ccd); + h ^= h >> 33; + Complex::new( + (h & 0xFFFF) as f64 / 65535.0 - 0.5, + ((h >> 16) & 0xFFFF) as f64 / 65535.0 - 0.5, + ) + }) + .collect() + } + + /// The all-pass chain preserves energy per band in steady state + /// (stationary input keeps the transient ratio at 1, and each + /// section is unit-magnitude on the unit circle). + #[test] + fn allpass_preserves_energy_on_stationary_noise() { + let config = HybridConfig::Bands1020; + let mut dec = PsDecorr::new(config); + let nb = config.nr_bands(); + let mut in_e = vec![0.0f64; nb]; + let mut out_e = vec![0.0f64; nb]; + for f in 0..40 { + let s: Vec> = (0..32).map(|n| noise_slot(3, f * 32 + n, nb)).collect(); + let d = dec.process(&s).unwrap(); + if f >= 8 { + for n in 0..32 { + for k in 0..nb { + in_e[k] += s[n][k].norm_sqr(); + out_e[k] += d[n][k].norm_sqr(); + } + } + } + } + for k in 0..nb { + let ratio = out_e[k] / in_e[k]; + assert!( + (0.85..1.15).contains(&ratio), + "band {k}: energy ratio {ratio}" + ); + } + } + + /// The upper bands are pure delays: 14 slots in the mid region, + /// 1 slot at the top. + #[test] + fn upper_bands_are_pure_delays() { + let config = HybridConfig::Bands1020; + let mut dec = PsDecorr::new(config); + let nb = config.nr_bands(); + // Stationary-amplitude signal so the transient ratio stays 1: + // an impulse *train* in every band with period > delay would + // still trip the detector, so use a constant rotating phasor + // instead and check the delay relation on the waveform. + let mut frames: Vec>> = Vec::new(); + for f in 0..3 { + let s: Vec> = (0..32) + .map(|n| { + let t = (f * 32 + n) as f64; + (0..nb) + .map(|k| { + let arg = 0.1 * t + k as f64; + let (si, co) = arg.sin_cos(); + Complex::new(co, si) + }) + .collect() + }) + .collect(); + frames.push(s); + } + let mut all_in: Vec> = Vec::new(); + let mut all_out: Vec> = Vec::new(); + for s in &frames { + let d = dec.process(s).unwrap(); + all_in.extend_from_slice(s); + all_out.extend_from_slice(&d); + } + // Mid band k=35 (30..42): 14-slot delay. Top band k=50: 1. + for (k, delay) in [(35usize, 14usize), (50, 1)] { + for n in 40..96 { + let d = all_out[n][k] - all_in[n - delay][k]; + assert!( + d.norm_sqr() < 1e-20, + "band {k} slot {n}: not a {delay}-delay" + ); + } + } + } + + /// After a loud burst cuts to silence the peak tracker holds while + /// the smoothed power decays, so the de-correlated tail (still + /// flowing out of the 14-slot delay line) is ducked (G < 1). A + /// constant-level signal, by contrast, keeps `peak == P`, the + /// difference at zero, and G exactly 1 — the steady test above + /// already pins that via the exact delay identity. + #[test] + fn transient_tail_is_ducked() { + let config = HybridConfig::Bands1020; + let nb = config.nr_bands(); + let loud: Vec> = (0..32).map(|_| vec![Complex::new(1.0, 0.0); nb]).collect(); + let quiet: Vec> = (0..32).map(|_| vec![Complex::default(); nb]).collect(); + let mut dec = PsDecorr::new(config); + dec.process(&loud).unwrap(); + let d = dec.process(&quiet).unwrap(); + // Band 35 is a pure 14-slot delay (b(35) = 18): during the + // first 14 silence slots the delayed loud samples (|·| = 1) + // are still emerging, scaled by G(18, n). By slot 5 the + // recurrences (α peak decay vs a_smooth power decay, γ = 1.5) + // put G well under 0.8; at slot 0 G is still 1. + let first = d[0][35].norm_sqr(); + let later = d[5][35].norm_sqr(); + assert!((first - 1.0).abs() < 1e-12, "slot 0 should be unducked"); + assert!(later < 0.64, "slot 5 should be ducked: {later}"); + // And the duck deepens monotonically over the tail. + let even_later = d[10][35].norm_sqr(); + assert!(even_later < later); + } + + /// reset_bands zeroes the tail region state only. + #[test] + fn partial_reset_clears_upper_state() { + let config = HybridConfig::Bands1020; + let nb = config.nr_bands(); + let mut dec = PsDecorr::new(config); + let s: Vec> = (0..32).map(|n| noise_slot(9, n, nb)).collect(); + dec.process(&s).unwrap(); + dec.reset_bands(40); + let zeros: Vec> = (0..32).map(|_| vec![Complex::default(); nb]).collect(); + let d = dec.process(&zeros).unwrap(); + // Bands >= 40 were reset: zero input → zero output. + for (n, row) in d.iter().enumerate().take(14) { + for (k, v) in row.iter().enumerate().skip(40) { + assert_eq!(*v, Complex::default(), "slot {n} band {k}"); + } + } + // A low band still rings from its surviving state. + let rings = (0..8).any(|n| d[n][3].norm_sqr() > 0.0); + assert!(rings, "low-band state should survive a partial reset"); + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_huffman.rs b/crates/vendor/oxideav-aac/src/ps_huffman.rs new file mode 100644 index 00000000..50e1cec2 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_huffman.rs @@ -0,0 +1,472 @@ +//! Parametric Stereo Huffman codebooks + `ps_huff_dec()` — ISO/IEC +//! 14496-3:2009 Annex 8.B (Tables 8.B.17–8.B.21). +//! +//! The `ps_data()` element (§8.4.2 Table 8.9) entropy-codes its IID / +//! ICC / IPD / OPD parameters as DPCM deltas with ten canonical +//! Huffman codebooks, selected by parameter kind, quantization grid +//! (`iid_quant`, Table 8.24) and coding direction (time vs frequency +//! differential, the `*_dt[e]` flags): +//! +//! | parameter | grid | direction | table | +//! |-----------|--------|-----------|-------| +//! | IID | coarse | freq | [`HUFF_IID_DF`] (8.B.18) | +//! | IID | coarse | time | [`HUFF_IID_DT`] (8.B.18) | +//! | IID | fine | freq | [`HUFF_IID_FINE_DF`] (8.B.17) | +//! | IID | fine | time | [`HUFF_IID_FINE_DT`] (8.B.17) | +//! | ICC | — | freq | [`HUFF_ICC_DF`] (8.B.19) | +//! | ICC | — | time | [`HUFF_ICC_DT`] (8.B.19) | +//! | IPD | — | freq | [`HUFF_IPD_DF`] (8.B.20) | +//! | IPD | — | time | [`HUFF_IPD_DT`] (8.B.20) | +//! | OPD | — | freq | [`HUFF_OPD_DF`] (8.B.21) | +//! | OPD | — | time | [`HUFF_OPD_DT`] (8.B.21) | +//! +//! ## Codeword representation +//! +//! Same shape as [`crate::sbr_huffman`]: each table is `[(u8, u32); N]` +//! `(code_length_bits, codeword)` pairs indexed by the Huffman table +//! index, MSB-first prefix codes. [`ps_huff_dec`] accumulates bits and +//! returns `index - lav` (the signed delta). The IID/ICC tables carry +//! their LAV in the index layout (`LAV = (N-1)/2`); IPD/OPD deltas are +//! phase-index differences taken modulo 8 by the caller, so their +//! tables decode with `lav = 0`. +//! +//! ## Provenance +//! +//! All ten tables are transcribed from the normative codeword grids in +//! ISO/IEC 14496-3:2009 Annex 8.B staged under `docs/audio/aac/`. All +//! six IID/ICC tables were additionally cross-checked leaf-for-leaf +//! against the staged `docs/audio/aac/sbr-tables/ps-huffbook-*.csv` +//! decode-tree data at transcription time; every table satisfies the +//! complete-prefix-code invariant (Kraft sum exactly 1). + +use crate::{Error, Result}; + +/// Longest PS codeword across all Annex 8.B tables (`huff_iid_dt[0]` +/// reaches 20 bits). +pub const PS_HUFF_MAX_CODE_LEN: u32 = 20; + +/// `huff_iid_df[1]` — Table 8.B.17 (fine grid, frequency direction). +/// Index `i` decodes the delta `i - 30`. +pub const HUFF_IID_FINE_DF: [(u8, u32); 61] = [ + (18, 0b011111111010110100), // -30 + (18, 0b011111111010110101), // -29 + (18, 0b011111110101110110), // -28 + (18, 0b011111110101110111), // -27 + (18, 0b011111110101110100), // -26 + (18, 0b011111110101110101), // -25 + (18, 0b011111111010001010), // -24 + (18, 0b011111111010001011), // -23 + (18, 0b011111111010001000), // -22 + (17, 0b01111111010000000), // -21 + (18, 0b011111111010110110), // -20 + (17, 0b01111111010000010), // -19 + (17, 0b01111111010111000), // -18 + (16, 0b0111111101000010), // -17 + (16, 0b0111111110101110), // -16 + (15, 0b011111110101111), // -15 + (14, 0b01111111010001), // -14 + (14, 0b01111111101001), // -13 + (13, 0b0111111101001), // -12 + (12, 0b011111101010), // -11 + (12, 0b011111111011), // -10 + (11, 0b01111111011), // -9 + (10, 0b0111111011), // -8 + (10, 0b0111111111), // -7 + (8, 0b01111100), // -6 + (7, 0b0111100), // -5 + (6, 0b011100), // -4 + (5, 0b01100), // -3 + (4, 0b0000), // -2 + (3, 0b001), // -1 + (1, 0b1), // +0 + (3, 0b010), // +1 + (4, 0b0001), // +2 + (5, 0b01101), // +3 + (6, 0b011101), // +4 + (7, 0b0111101), // +5 + (8, 0b01111101), // +6 + (9, 0b011111100), // +7 + (10, 0b0111111100), // +8 + (11, 0b01111111100), // +9 + (11, 0b01111110100), // +10 + (12, 0b011111101011), // +11 + (13, 0b0111111101010), // +12 + (14, 0b01111111101010), // +13 + (14, 0b01111111010110), // +14 + (15, 0b011111111010000), // +15 + (16, 0b0111111110101111), // +16 + (16, 0b0111111101000011), // +17 + (17, 0b01111111010111001), // +18 + (17, 0b01111111010000011), // +19 + (18, 0b011111111010110111), // +20 + (17, 0b01111111010000001), // +21 + (18, 0b011111111010001001), // +22 + (18, 0b011111111010001110), // +23 + (18, 0b011111111010001111), // +24 + (18, 0b011111111010001100), // +25 + (18, 0b011111111010001101), // +26 + (18, 0b011111111010110010), // +27 + (18, 0b011111111010110011), // +28 + (18, 0b011111111010110000), // +29 + (18, 0b011111111010110001), // +30 +]; + +/// `huff_iid_dt[1]` — Table 8.B.17 (fine grid, time direction). +/// Index `i` decodes the delta `i - 30`. +pub const HUFF_IID_FINE_DT: [(u8, u32); 61] = [ + (16, 0b0100111011010100), // -30 + (16, 0b0100111011010101), // -29 + (16, 0b0100111011001110), // -28 + (16, 0b0100111011001111), // -27 + (16, 0b0100111011001100), // -26 + (16, 0b0100111011010110), // -25 + (16, 0b0100111011011000), // -24 + (16, 0b0100111101000110), // -23 + (16, 0b0100111101100000), // -22 + (15, 0b010011100011000), // -21 + (15, 0b010011100011001), // -20 + (15, 0b010011101100100), // -19 + (15, 0b010011101100101), // -18 + (15, 0b010011101101101), // -17 + (15, 0b010011110110001), // -16 + (14, 0b01001110110111), // -15 + (14, 0b01001111010110), // -14 + (13, 0b0100111000111), // -13 + (13, 0b0100111101001), // -12 + (13, 0b0100111101101), // -11 + (12, 0b010011101110), // -10 + (12, 0b010011110111), // -9 + (11, 0b01001111000), // -8 + (10, 0b0100111001), // -7 + (9, 0b010011010), // -6 + (9, 0b010011111), // -5 + (7, 0b0100000), // -4 + (6, 0b010001), // -3 + (5, 0b01010), // -2 + (3, 0b011), // -1 + (1, 0b1), // +0 + (2, 0b00), // +1 + (5, 0b01011), // +2 + (6, 0b010010), // +3 + (7, 0b0100001), // +4 + (8, 0b01001100), // +5 + (9, 0b010011011), // +6 + (10, 0b0100111010), // +7 + (11, 0b01001111001), // +8 + (11, 0b01001110000), // +9 + (12, 0b010011101111), // +10 + (12, 0b010011100010), // +11 + (13, 0b0100111101010), // +12 + (13, 0b0100111011000), // +13 + (14, 0b01001111010111), // +14 + (14, 0b01001111010000), // +15 + (15, 0b010011110110010), // +16 + (15, 0b010011110100010), // +17 + (15, 0b010011100011010), // +18 + (15, 0b010011100011011), // +19 + (16, 0b0100111101100110), // +20 + (16, 0b0100111101100111), // +21 + (16, 0b0100111101100001), // +22 + (16, 0b0100111101000111), // +23 + (16, 0b0100111011011001), // +24 + (16, 0b0100111011010111), // +25 + (16, 0b0100111011001101), // +26 + (16, 0b0100111011010010), // +27 + (16, 0b0100111011010011), // +28 + (16, 0b0100111011010000), // +29 + (16, 0b0100111011010001), // +30 +]; + +/// `huff_iid_df[0]` — Table 8.B.18 (coarse grid, frequency direction). +/// Index `i` decodes the delta `i - 14`. +pub const HUFF_IID_DF: [(u8, u32); 29] = [ + (17, 0b11111111111111011), // -14 + (17, 0b11111111111111100), // -13 + (17, 0b11111111111111101), // -12 + (17, 0b11111111111111010), // -11 + (16, 0b1111111111111100), // -10 + (15, 0b111111111111100), // -9 + (13, 0b1111111111101), // -8 + (10, 0b1111111110), // -7 + (9, 0b111111110), // -6 + (7, 0b1111110), // -5 + (6, 0b111100), // -4 + (5, 0b11101), // -3 + (4, 0b1101), // -2 + (3, 0b101), // -1 + (1, 0b0), // +0 + (3, 0b100), // +1 + (4, 0b1100), // +2 + (5, 0b11100), // +3 + (6, 0b111101), // +4 + (6, 0b111110), // +5 + (8, 0b11111110), // +6 + (11, 0b11111111110), // +7 + (13, 0b1111111111100), // +8 + (14, 0b11111111111100), // +9 + (14, 0b11111111111101), // +10 + (15, 0b111111111111101), // +11 + (17, 0b11111111111111110), // +12 + (18, 0b111111111111111110), // +13 + (18, 0b111111111111111111), // +14 +]; + +/// `huff_iid_dt[0]` — Table 8.B.18 (coarse grid, time direction). +/// Index `i` decodes the delta `i - 14`. +pub const HUFF_IID_DT: [(u8, u32); 29] = [ + (19, 0b1111111111111111001), // -14 + (19, 0b1111111111111111010), // -13 + (19, 0b1111111111111111011), // -12 + (20, 0b11111111111111111000), // -11 + (20, 0b11111111111111111001), // -10 + (20, 0b11111111111111111010), // -9 + (17, 0b11111111111111101), // -8 + (15, 0b111111111111110), // -7 + (12, 0b111111111110), // -6 + (10, 0b1111111110), // -5 + (8, 0b11111110), // -4 + (6, 0b111110), // -3 + (4, 0b1110), // -2 + (2, 0b10), // -1 + (1, 0b0), // +0 + (3, 0b110), // +1 + (5, 0b11110), // +2 + (7, 0b1111110), // +3 + (9, 0b111111110), // +4 + (11, 0b11111111110), // +5 + (13, 0b1111111111110), // +6 + (14, 0b11111111111110), // +7 + (17, 0b11111111111111100), // +8 + (19, 0b1111111111111111000), // +9 + (20, 0b11111111111111111011), // +10 + (20, 0b11111111111111111100), // +11 + (20, 0b11111111111111111101), // +12 + (20, 0b11111111111111111110), // +13 + (20, 0b11111111111111111111), // +14 +]; + +/// `huff_icc_df` — Table 8.B.19 (frequency direction). +/// Index `i` decodes the delta `i - 7`. +pub const HUFF_ICC_DF: [(u8, u32); 15] = [ + (14, 0b11111111111111), // -7 + (14, 0b11111111111110), // -6 + (12, 0b111111111110), // -5 + (10, 0b1111111110), // -4 + (7, 0b1111110), // -3 + (5, 0b11110), // -2 + (3, 0b110), // -1 + (1, 0b0), // +0 + (2, 0b10), // +1 + (4, 0b1110), // +2 + (6, 0b111110), // +3 + (8, 0b11111110), // +4 + (9, 0b111111110), // +5 + (11, 0b11111111110), // +6 + (13, 0b1111111111110), // +7 +]; + +/// `huff_icc_dt` — Table 8.B.19 (time direction). +/// Index `i` decodes the delta `i - 7`. +pub const HUFF_ICC_DT: [(u8, u32); 15] = [ + (14, 0b11111111111110), // -7 + (13, 0b1111111111110), // -6 + (11, 0b11111111110), // -5 + (9, 0b111111110), // -4 + (7, 0b1111110), // -3 + (5, 0b11110), // -2 + (3, 0b110), // -1 + (1, 0b0), // +0 + (2, 0b10), // +1 + (4, 0b1110), // +2 + (6, 0b111110), // +3 + (8, 0b11111110), // +4 + (10, 0b1111111110), // +5 + (12, 0b111111111110), // +6 + (14, 0b11111111111111), // +7 +]; + +/// `huff_ipd_df` — Table 8.B.20 (frequency direction). Decodes the +/// raw phase-index delta `0..8` (`lav = 0`). +pub const HUFF_IPD_DF: [(u8, u32); 8] = [ + (1, 0b1), // 0 + (3, 0b000), // 1 + (4, 0b0110), // 2 + (4, 0b0100), // 3 + (4, 0b0010), // 4 + (4, 0b0011), // 5 + (4, 0b0101), // 6 + (4, 0b0111), // 7 +]; + +/// `huff_ipd_dt` — Table 8.B.20 (time direction). Decodes the raw +/// phase-index delta `0..8` (`lav = 0`). +pub const HUFF_IPD_DT: [(u8, u32); 8] = [ + (1, 0b1), // 0 + (3, 0b010), // 1 + (4, 0b0010), // 2 + (5, 0b00011), // 3 + (5, 0b00010), // 4 + (4, 0b0000), // 5 + (4, 0b0011), // 6 + (3, 0b011), // 7 +]; + +/// `huff_opd_df` — Table 8.B.21 (frequency direction). Decodes the +/// raw phase-index delta `0..8` (`lav = 0`). +pub const HUFF_OPD_DF: [(u8, u32); 8] = [ + (1, 0b1), // 0 + (3, 0b001), // 1 + (4, 0b0110), // 2 + (4, 0b0100), // 3 + (5, 0b01111), // 4 + (5, 0b01110), // 5 + (4, 0b0101), // 6 + (3, 0b000), // 7 +]; + +/// `huff_opd_dt` — Table 8.B.21 (time direction). Decodes the raw +/// phase-index delta `0..8` (`lav = 0`). +pub const HUFF_OPD_DT: [(u8, u32); 8] = [ + (1, 0b1), // 0 + (3, 0b010), // 1 + (4, 0b0001), // 2 + (5, 0b00111), // 3 + (5, 0b00110), // 4 + (4, 0b0000), // 5 + (4, 0b0010), // 6 + (3, 0b011), // 7 +]; + +/// Decode one PS Huffman codeword from `reader` against `table`, +/// returning `index - lav` (the signed DPCM delta). +/// +/// Reads bits MSB-first, accumulating a codeword until it matches an +/// entry `(length, codeword)`. Returns [`Error::PsDataInvalid`] if no +/// codeword of length up to [`PS_HUFF_MAX_CODE_LEN`] matches (a +/// corrupt or truncated `ps_data()` payload). +pub fn ps_huff_dec( + reader: &mut oxideav_core::bits::BitReader<'_>, + table: &[(u8, u32)], + lav: i32, +) -> Result { + let mut codeword: u32 = 0; + let mut len: u32 = 0; + loop { + codeword = (codeword << 1) | reader.read_u32(1).map_err(|_| Error::PsDataInvalid)?; + len += 1; + for (idx, &(clen, ccode)) in table.iter().enumerate() { + if u32::from(clen) == len && ccode == codeword { + return Ok(idx as i32 - lav); + } + } + if len >= PS_HUFF_MAX_CODE_LEN { + return Err(Error::PsDataInvalid); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::{BitReader, BitWriter}; + + /// Every table: codewords fit their declared length, the code is + /// prefix-free, and it is *complete* (Kraft sum exactly 1) — the + /// invariants the Annex 8.B grids must satisfy. + fn check_table(table: &[(u8, u32)]) { + let mut kraft_num: u64 = 0; // sum of 2^(max_len - len) + for &(len, code) in table { + assert!(len >= 1 && u32::from(len) <= PS_HUFF_MAX_CODE_LEN); + assert!( + u64::from(code) < (1u64 << len), + "codeword 0x{code:08X} overflows its {len}-bit length" + ); + kraft_num += 1u64 << (PS_HUFF_MAX_CODE_LEN - u32::from(len)); + } + assert_eq!( + kraft_num, + 1u64 << PS_HUFF_MAX_CODE_LEN, + "code is not complete" + ); + for (a, &(la, ca)) in table.iter().enumerate() { + for (b, &(lb, cb)) in table.iter().enumerate() { + if a == b || lb < la { + continue; + } + assert!(cb >> (lb - la) != ca, "prefix conflict {a} vs {b}"); + } + } + } + + #[test] + fn all_tables_are_complete_prefix_codes() { + check_table(&HUFF_IID_FINE_DF); + check_table(&HUFF_IID_FINE_DT); + check_table(&HUFF_IID_DF); + check_table(&HUFF_IID_DT); + check_table(&HUFF_ICC_DF); + check_table(&HUFF_ICC_DT); + check_table(&HUFF_IPD_DF); + check_table(&HUFF_IPD_DT); + check_table(&HUFF_OPD_DF); + check_table(&HUFF_OPD_DT); + } + + /// Round-trip every index of every table through ps_huff_dec. + #[test] + fn every_codeword_decodes_to_its_index() { + let cases: [(&[(u8, u32)], i32); 10] = [ + (&HUFF_IID_FINE_DF, 30), + (&HUFF_IID_FINE_DT, 30), + (&HUFF_IID_DF, 14), + (&HUFF_IID_DT, 14), + (&HUFF_ICC_DF, 7), + (&HUFF_ICC_DT, 7), + (&HUFF_IPD_DF, 0), + (&HUFF_IPD_DT, 0), + (&HUFF_OPD_DF, 0), + (&HUFF_OPD_DT, 0), + ]; + for (table, lav) in cases { + for (idx, &(len, code)) in table.iter().enumerate() { + let mut w = BitWriter::new(); + w.write_u32(code, u32::from(len)); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let got = ps_huff_dec(&mut r, table, lav).unwrap(); + assert_eq!(got, idx as i32 - lav); + assert_eq!(r.bit_position(), u64::from(len)); + } + } + } + + /// The zero delta is always the 1-bit codeword `1` for IID/ICC + /// (Table 8.B.17–8.B.19 anchor `0 → 1`, except the coarse tables' + /// `0 → 0`) — pin the two anchors that differ. + #[test] + fn zero_delta_anchors() { + // Fine IID: delta 0 = codeword 1 (1 bit). + assert_eq!(HUFF_IID_FINE_DF[30], (1, 0b1)); + // Coarse IID: delta 0 = codeword 0 (1 bit). + assert_eq!(HUFF_IID_DF[14], (1, 0b0)); + // ICC: delta 0 = codeword 0 (1 bit). + assert_eq!(HUFF_ICC_DF[7], (1, 0b0)); + // IPD/OPD: delta 0 = codeword 1 (1 bit). + assert_eq!(HUFF_IPD_DF[0], (1, 0b1)); + assert_eq!(HUFF_OPD_DT[0], (1, 0b1)); + } + + /// A truncated payload (the reader running dry mid-codeword) + /// surfaces the parse error rather than spinning. + #[test] + fn unmatched_bits_error() { + // In HUFF_IID_DT the shortest all-ones codeword is 20 bits, so + // 8 one-bits cannot complete a codeword; the reader runs dry. + let bytes = [0xFFu8; 1]; + let mut r = BitReader::new(&bytes); + assert!(matches!( + ps_huff_dec(&mut r, &HUFF_IID_DT, 14), + Err(Error::PsDataInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_hybrid.rs b/crates/vendor/oxideav-aac/src/ps_hybrid.rs new file mode 100644 index 00000000..2f982320 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_hybrid.rs @@ -0,0 +1,508 @@ +//! PS hybrid filterbank — ISO/IEC 14496-3:2009 §8.6.4.3 / Annex 8.A.3. +//! +//! Parametric Stereo needs a finer frequency resolution at the bottom +//! of the spectrum than the 64-band QMF provides, so the lowest QMF +//! subbands are split further by 13-tap prototype filters (Tables +//! 8.36–8.38), producing the *hybrid* sub-subband domain: +//! +//! * **10/20 stereo bands** — QMF band 0 split by 8 (Type A, complex +//! modulated) with the outer sub-subband pairs merged to 6 channels, +//! QMF bands 1 and 2 split by 2 (Type B, cosine modulated); 71 +//! hybrid channels total (`6 + 2 + 2 + 61`). +//! * **34 stereo bands** — QMF band 0 split by 12, band 1 by 8, bands +//! 2–4 by 4 (all Type A); 91 hybrid channels (`12+8+4+4+4 + 59`). +//! +//! ```text +//! Type A: G_q^p[n] = g^p[n] · exp(j·2π/Q^p·(q+1/2)·(n−6)) +//! Type B: G_q^p[n] = g^p[n] · cos(2π·q/Q^p·(n−6)) +//! ``` +//! +//! The prototypes are linear-phase with a 6-slot delay; per Annex +//! 8.A.3 the SBR combination feeds the filterbank 6 *look-ahead* QMF +//! slots (`XLow` beyond the current frame), so the hybrid output is +//! time-aligned with the QMF input at **zero net delay**: the unsplit +//! bands pass straight through and the split bands consume the +//! look-ahead. Filtering is the convolution +//! `y[n] = Σ_m G[m] · x[n+6−m]`, needing 6 history slots per split +//! band which [`PsHybrid`] threads across frames. +//! +//! ## Channel ordering (Figures 8.20 / 8.22) +//! +//! For the 10/20 configuration QMF band 0's eight Type-A outputs `q` +//! (sub-subband centres `(q+1/2)·π/8`, `q ≥ 4` the negative-frequency +//! mirrors) merge and reorder to six hybrid channels: +//! `s0 = q6, s1 = q7, s2 = q0, s3 = q1, s4 = q2+q5, s5 = q3+q4`. +//! QMF band 1's two Type-B outputs land **swapped** (`s6 = q1, +//! s7 = q0` — odd QMF bands are spectrally inverted), band 2's in +//! order (`s8 = q0, s9 = q1`). The 34-band configuration keeps every +//! split output in filter order (Figure 8.22). +//! +//! The synthesis (§8.6.4.7 / Figures 8.21, 8.23) is a plain adder: +//! sub-subbands of a split QMF band sum back into that band. Because +//! each prototype's sub-filters sum to a pure 6-slot delay (the +//! Type-A modulation phases cancel off-centre, the Type-B prototypes +//! vanish at the surviving off-centre taps), analysis followed by +//! synthesis reconstructs the input exactly — pinned by the tests. +//! +//! All truth from ISO/IEC 14496-3:2009 §8.6.4.3 / Annex 8.A staged +//! under `docs/audio/aac/`. + +use crate::sbr_qmf::Complex; +use crate::{Error, Result}; + +/// QMF slots per PS stereo frame in the SBR combination +/// (`numQMFSlots = numTimeSlots · RATE`, Annex 8.A.3, 1024 framing). +pub const NUM_QMF_SLOTS: usize = 32; + +/// Look-ahead slots supplied by the SBR low-band buffer (Annex 8.A.3). +pub const LOOKAHEAD: usize = 6; + +/// Prototype filter length (§8.6.4.3). +const PROTO_LEN: usize = 13; + +/// Table 8.37 — `g⁰[n]`, `Q⁰ = 8` (10/20 stereo bands, QMF band 0). +const G0_Q8: [f64; PROTO_LEN] = [ + 0.00746082949812, + 0.02270420949825, + 0.04546865930473, + 0.07266113929591, + 0.09885108575264, + 0.11793710567217, + 0.125, + 0.11793710567217, + 0.09885108575264, + 0.07266113929591, + 0.04546865930473, + 0.02270420949825, + 0.00746082949812, +]; + +/// Table 8.37 — `g^{1,2}[n]`, `Q^{1,2} = 2` (10/20 bands, QMF 1–2). +const G12_Q2: [f64; PROTO_LEN] = [ + 0.0, + 0.01899487526049, + 0.0, + -0.07293139167538, + 0.0, + 0.30596630545168, + 0.5, + 0.30596630545168, + 0.0, + -0.07293139167538, + 0.0, + 0.01899487526049, + 0.0, +]; + +/// Table 8.38 — `g⁰[n]`, `Q⁰ = 12` (34 stereo bands, QMF band 0). +const G0_Q12: [f64; PROTO_LEN] = [ + 0.04081179924692, + 0.03812810994926, + 0.05144908135699, + 0.06399831151592, + 0.07428313801106, + 0.08100347892914, + 0.08333333333333, + 0.08100347892914, + 0.07428313801106, + 0.06399831151592, + 0.05144908135699, + 0.03812810994926, + 0.04081179924692, +]; + +/// Table 8.38 — `g¹[n]`, `Q¹ = 8` (34 bands, QMF band 1). +const G1_Q8: [f64; PROTO_LEN] = [ + 0.01565675600122, + 0.03752716391991, + 0.05417891378782, + 0.08417044116767, + 0.10307344158036, + 0.12222452249753, + 0.125, + 0.12222452249753, + 0.10307344158036, + 0.08417044116767, + 0.05417891378782, + 0.03752716391991, + 0.01565675600122, +]; + +/// Table 8.38 — `g^{2,3,4}[n]`, `Q^{2,3,4} = 4` (34 bands, QMF 2–4). +const G234_Q4: [f64; PROTO_LEN] = [ + -0.05908211155639, + -0.04871498374946, + 0.0, + 0.07778723915851, + 0.16486303567403, + 0.23279856662996, + 0.25, + 0.23279856662996, + 0.16486303567403, + 0.07778723915851, + 0.0, + -0.04871498374946, + -0.05908211155639, +]; + +/// The two §8.6.4.3 hybrid configurations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HybridConfig { + /// 10 or 20 stereo bands: 71 hybrid channels, QMF bands 0–2 split. + Bands1020, + /// 34 stereo bands: 91 hybrid channels, QMF bands 0–4 split. + Bands34, +} + +impl HybridConfig { + /// `NR_BANDS` — hybrid channel count (§8.6.4.5.1). + #[must_use] + pub fn nr_bands(&self) -> usize { + match self { + HybridConfig::Bands1020 => 71, + HybridConfig::Bands34 => 91, + } + } + + /// Number of QMF bands that are split. + fn split_bands(&self) -> usize { + match self { + HybridConfig::Bands1020 => 3, + HybridConfig::Bands34 => 5, + } + } + + /// Split factor `Q^p` per split QMF band. + fn q(&self, p: usize) -> usize { + match self { + HybridConfig::Bands1020 => [8, 2, 2][p], + HybridConfig::Bands34 => [12, 8, 4, 4, 4][p], + } + } + + /// Prototype `g^p` per split QMF band. + fn proto(&self, p: usize) -> &'static [f64; PROTO_LEN] { + match self { + HybridConfig::Bands1020 => [&G0_Q8, &G12_Q2, &G12_Q2][p], + HybridConfig::Bands34 => [&G0_Q12, &G1_Q8, &G234_Q4, &G234_Q4, &G234_Q4][p], + } + } + + /// Whether split band `p` uses the Type-A (complex) modulation. + fn type_a(&self, p: usize) -> bool { + match self { + HybridConfig::Bands1020 => p == 0, + HybridConfig::Bands34 => true, + } + } +} + +/// One channel's hybrid analysis/synthesis state: the 6 history slots +/// per split QMF band that the 13-tap convolution reaches into before +/// the current frame. +#[derive(Debug, Clone)] +pub struct PsHybrid { + config: HybridConfig, + /// `history[p][j]` — the previous frame's QMF slots `26..32` for + /// split band `p` (`j = 0` is the oldest). + history: Vec<[Complex; LOOKAHEAD]>, +} + +impl PsHybrid { + /// A fresh filterbank for `config` (zero history). + #[must_use] + pub fn new(config: HybridConfig) -> Self { + PsHybrid { + config, + history: vec![[Complex::default(); LOOKAHEAD]; config.split_bands()], + } + } + + /// The active configuration. + #[must_use] + pub fn config(&self) -> HybridConfig { + self.config + } + + /// Switch configuration (a §8.6.4.6.1 stereo-band change resets + /// the filter state instantaneously). + pub fn reset(&mut self, config: HybridConfig) { + self.config = config; + self.history = vec![[Complex::default(); LOOKAHEAD]; config.split_bands()]; + } + + /// Hybrid analysis of one stereo frame. + /// + /// `x` is the Annex 8.A.3 `Xinput` matrix: at least + /// `NUM_QMF_SLOTS + LOOKAHEAD` slots of 64 QMF bands (the trailing + /// 6 slots only need bands `0..split_bands` populated). Returns + /// `NUM_QMF_SLOTS` slots of `nr_bands()` hybrid channels, and + /// advances the cross-frame history. + pub fn analyze(&mut self, x: &[[Complex; 64]]) -> Result>> { + if x.len() < NUM_QMF_SLOTS + LOOKAHEAD { + return Err(Error::PsDataInvalid); + } + let nb = self.config.nr_bands(); + let split = self.config.split_bands(); + let mut out = vec![vec![Complex::default(); nb]; NUM_QMF_SLOTS]; + + for p in 0..split { + // Extended buffer: 6 history slots + the frame + look-ahead. + let mut buf = [Complex::default(); LOOKAHEAD + NUM_QMF_SLOTS + LOOKAHEAD]; + buf[..LOOKAHEAD].copy_from_slice(&self.history[p]); + for (j, slot) in x.iter().enumerate().take(NUM_QMF_SLOTS + LOOKAHEAD) { + buf[LOOKAHEAD + j] = slot[p]; + } + let q_cnt = self.config.q(p); + let g = self.config.proto(p); + let type_a = self.config.type_a(p); + for q in 0..q_cnt { + // G_q[m] for m = 0..13. + let mut filt = [Complex::default(); PROTO_LEN]; + for (m, f) in filt.iter_mut().enumerate() { + let arg = if type_a { + 2.0 * core::f64::consts::PI / q_cnt as f64 + * (q as f64 + 0.5) + * (m as f64 - 6.0) + } else { + 2.0 * core::f64::consts::PI * q as f64 / q_cnt as f64 * (m as f64 - 6.0) + }; + let (s, c) = arg.sin_cos(); + *f = if type_a { + Complex::new(g[m] * c, g[m] * s) + } else { + Complex::new(g[m] * c, 0.0) + }; + } + for (n, row) in out.iter_mut().enumerate() { + // y[n] = Σ_m G[m]·x[n+6−m]; buf[j] = x[j−6]. + let mut acc = Complex::default(); + for (m, &f) in filt.iter().enumerate() { + acc += f * buf[n + 12 - m]; + } + accumulate_channel(&self.config, p, q, acc, row); + } + } + // Next frame's x[−6..0] are this frame's slots 26..32. + for j in 0..LOOKAHEAD { + self.history[p][j] = x[NUM_QMF_SLOTS - LOOKAHEAD + j][p]; + } + } + + // Unsplit QMF bands pass through at zero delay. + for (n, row) in out.iter_mut().enumerate() { + for k in split..64 { + row[hybrid_offset(&self.config) + k - split] = x[n][k]; + } + } + Ok(out) + } +} + +/// First hybrid channel index of the unsplit QMF region. +fn hybrid_offset(config: &HybridConfig) -> usize { + match config { + HybridConfig::Bands1020 => 10, + HybridConfig::Bands34 => 32, + } +} + +/// Route split-band filter output `q` of QMF band `p` into its hybrid +/// channel (Figures 8.20 / 8.22), merging where the 10/20 +/// configuration combines sub-subbands. +fn accumulate_channel(config: &HybridConfig, p: usize, q: usize, v: Complex, row: &mut [Complex]) { + match config { + HybridConfig::Bands1020 => match p { + 0 => { + // s0=q6, s1=q7, s2=q0, s3=q1, s4=q2+q5, s5=q3+q4. + let k = match q { + 6 => 0, + 7 => 1, + 0 => 2, + 1 => 3, + 2 | 5 => 4, + _ => 5, // 3 | 4 + }; + row[k] += v; + } + 1 => { + // Spectrally inverted odd QMF band: s6=q1, s7=q0. + row[if q == 0 { 7 } else { 6 }] += v; + } + _ => { + // Band 2 in order: s8=q0, s9=q1. + row[8 + q] += v; + } + }, + HybridConfig::Bands34 => { + // Figure 8.22: filter order, bands packed consecutively. + let base = [0usize, 12, 20, 24, 28][p]; + row[base + q] += v; + } + } +} + +/// Hybrid synthesis (§8.6.4.7): sum each split QMF band's sub-subbands +/// back into the band; copy the unsplit region. `rows` are +/// `nr_bands()`-wide hybrid slots; returns 64-band QMF slots. +#[must_use] +pub fn synthesize(config: HybridConfig, rows: &[Vec]) -> Vec<[Complex; 64]> { + let split = config.split_bands(); + let off = hybrid_offset(&config); + let mut out = Vec::with_capacity(rows.len()); + for row in rows { + let mut slot = [Complex::default(); 64]; + // Per-band sub-subband spans in the hybrid row. + let spans: &[(usize, usize)] = match config { + HybridConfig::Bands1020 => &[(0, 6), (6, 8), (8, 10)], + HybridConfig::Bands34 => &[(0, 12), (12, 20), (20, 24), (24, 28), (28, 32)], + }; + for (p, &(lo, hi)) in spans.iter().enumerate() { + for v in &row[lo..hi] { + slot[p] += *v; + } + } + for k in split..64 { + slot[k] = row[off + k - split]; + } + out.push(slot); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame_from(f: impl Fn(usize, usize) -> Complex) -> Vec<[Complex; 64]> { + (0..NUM_QMF_SLOTS + LOOKAHEAD) + .map(|n| { + let mut s = [Complex::default(); 64]; + for (k, cell) in s.iter_mut().enumerate() { + *cell = f(n, k); + } + s + }) + .collect() + } + + /// Deterministic pseudo-random complex signal. + fn noise(seed: u64) -> impl Fn(usize, usize) -> Complex { + move |n, k| { + let mut h = seed + .wrapping_mul(6364136223846793005) + .wrapping_add((n * 64 + k) as u64); + h ^= h >> 33; + h = h.wrapping_mul(0xff51afd7ed558ccd); + h ^= h >> 33; + let re = (h & 0xFFFF) as f64 / 65535.0 - 0.5; + let im = ((h >> 16) & 0xFFFF) as f64 / 65535.0 - 0.5; + Complex::new(re, im) + } + } + + /// Analysis followed by synthesis reconstructs the input exactly + /// (both configurations, across a frame boundary so the history + /// path is exercised). + #[test] + fn perfect_reconstruction_both_configs() { + for config in [HybridConfig::Bands1020, HybridConfig::Bands34] { + let mut fb = PsHybrid::new(config); + // Two consecutive frames of one continuous signal: frame f + // covers absolute slots 32f .. 32f+38. + for f in 0..3 { + let sig = noise(7); + let x = frame_from(|n, k| sig(32 * f + n, k)); + let hyb = fb.analyze(&x).unwrap(); + assert_eq!(hyb.len(), NUM_QMF_SLOTS); + assert_eq!(hyb[0].len(), config.nr_bands()); + let back = synthesize(config, &hyb); + // The split-band path reaches 6 slots into history, + // which is zero for the first frame's first slots — + // skip the warm-up region of frame 0. + let start = if f == 0 { LOOKAHEAD } else { 0 }; + for n in start..NUM_QMF_SLOTS { + for k in 0..64 { + let d = back[n][k] - x[n][k]; + assert!( + d.norm_sqr() < 1e-24, + "cfg {config:?} frame {f} slot {n} band {k}: {d:?}" + ); + } + } + } + } + } + + /// A complex exponential at the centre of QMF-band-0 sub-subband + /// `q = 0` (frequency π/8·(0+1/2) = π/16) concentrates in hybrid + /// channel `s2` of the 10/20 configuration — pinning the Figure + /// 8.20 reorder (positive low frequencies land on s2/s3, negative + /// on s1/s0). + #[test] + fn band0_positive_low_frequency_lands_on_s2() { + let mut fb = PsHybrid::new(HybridConfig::Bands1020); + let omega = core::f64::consts::PI / 16.0; + let x = frame_from(|n, k| { + if k == 0 { + let (s, c) = (omega * n as f64).sin_cos(); + Complex::new(c, s) + } else { + Complex::default() + } + }); + let hyb = fb.analyze(&x).unwrap(); + // Steady-state slot (history warm-up over). + let row = &hyb[20]; + let energies: Vec = (0..10).map(|k| row[k].norm_sqr()).collect(); + let max_k = (0..10) + .max_by(|&a, &b| energies[a].partial_cmp(&energies[b]).unwrap()) + .unwrap(); + assert_eq!(max_k, 2, "energies: {energies:?}"); + } + + /// The negative mirror (−π/16) lands on s1 (`q = 7`). + #[test] + fn band0_negative_low_frequency_lands_on_s1() { + let mut fb = PsHybrid::new(HybridConfig::Bands1020); + let omega = -core::f64::consts::PI / 16.0; + let x = frame_from(|n, k| { + if k == 0 { + let (s, c) = (omega * n as f64).sin_cos(); + Complex::new(c, s) + } else { + Complex::default() + } + }); + let hyb = fb.analyze(&x).unwrap(); + let row = &hyb[20]; + let energies: Vec = (0..10).map(|k| row[k].norm_sqr()).collect(); + let max_k = (0..10) + .max_by(|&a, &b| energies[a].partial_cmp(&energies[b]).unwrap()) + .unwrap(); + assert_eq!(max_k, 1, "energies: {energies:?}"); + } + + /// Unsplit bands pass through unchanged at zero delay. + #[test] + fn unsplit_bands_pass_through() { + let mut fb = PsHybrid::new(HybridConfig::Bands1020); + let sig = noise(11); + let x = frame_from(&sig); + let hyb = fb.analyze(&x).unwrap(); + for n in 0..NUM_QMF_SLOTS { + for k in 3..64 { + let d = hyb[n][10 + k - 3] - x[n][k]; + assert!(d.norm_sqr() < 1e-30); + } + } + } + + /// Short input is rejected. + #[test] + fn short_input_rejected() { + let mut fb = PsHybrid::new(HybridConfig::Bands34); + let x = vec![[Complex::default(); 64]; NUM_QMF_SLOTS]; + assert!(fb.analyze(&x).is_err()); + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_map.rs b/crates/vendor/oxideav-aac/src/ps_map.rs new file mode 100644 index 00000000..51ed0d6d --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_map.rs @@ -0,0 +1,282 @@ +//! PS parameter-band maps — ISO/IEC 14496-3:2009 §8.6.4.6.1 +//! (Tables 8.45 / 8.46 / 8.48 / 8.49). +//! +//! The stereo cues are defined per *stereo band* `b` (20 or 34 of +//! them), while the signal lives in 71 or 91 *hybrid channels* `k`. +//! [`parameter_map`] is `b(k)` — which stereo band governs each hybrid +//! channel — and [`conjugate_flags`] marks the negative-frequency +//! sub-subbands whose mixing coefficients apply conjugated +//! (the `*`-marked rows of Tables 8.48 / 8.49). +//! +//! [`map_10_to_20`], [`MAP_20_TO_34`] and [`MAP_34_TO_20`] convert +//! parameter vectors between band counts (§8.6.4.6.1): 10→20 +//! duplicates every parameter; 20→34 and 34→20 follow Tables 8.45 and +//! 8.46, averaging in *ANSI-C integer arithmetic* on the index +//! representation (the same tables are reused with float arithmetic +//! for the `h`-coefficient hand-over when the stereo-band count +//! switches mid-stream). +//! +//! In the 34-band configuration `b(k)` is deliberately non-monotonic +//! over the split region: the short 13-tap sub-filters of QMF bands +//! 1–4 have pass-bands reaching into neighbouring QMF bands (e.g. +//! hybrid channel 14, the third sub-subband of QMF band 1, sits at +//! 5/8 of a QMF bandwidth — inside stereo band 4), exactly as the +//! Table 8.41 centre-frequency ladder describes. +//! +//! All truth from ISO/IEC 14496-3:2009 subpart 8 staged under +//! `docs/audio/aac/`. + +use crate::ps_hybrid::HybridConfig; + +/// Table 8.48 — `b(k)` for the 20-stereo-band configuration +/// (71 hybrid channels). +const B_K_20: [u8; 71] = [ + 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, // sub-QMF (k0/k1 conjugate) + 8, 9, 10, 11, 12, 13, // QMF 3..8 + 14, 14, // 9-10 + 15, 15, 15, // 11-13 + 16, 16, 16, 16, // 14-17 + 17, 17, 17, 17, 17, // 18-22 + 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, // 23-34 + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, // 35-63 +]; + +/// Table 8.49 — `b(k)` for the 34-stereo-band configuration +/// (91 hybrid channels). +const B_K_34: [u8; 91] = [ + 0, 1, 2, 3, 4, 5, 6, 6, 7, 2, 1, 0, // QMF band 0 (k9..k11 conjugate) + 10, 10, 4, 5, 6, 7, 8, 9, // QMF band 1 + 10, 11, 12, 9, // QMF band 2 + 14, 11, 12, 13, // QMF band 3 + 14, 15, 16, 13, // QMF band 4 + 16, // QMF 5 + 17, // 6 + 18, // 7 + 19, // 8 + 20, // 9 + 21, // 10 + 22, 22, // 11-12 + 23, 23, // 13-14 + 24, 24, // 15-16 + 25, 25, // 17-18 + 26, 26, // 19-20 + 27, 27, 27, // 21-23 + 28, 28, 28, // 24-26 + 29, 29, 29, // 27-29 + 30, 30, 30, // 30-32 + 31, 31, 31, 31, // 33-36 + 32, 32, 32, 32, // 37-40 + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 33, // 41-63 +]; + +/// `b(k)` — stereo band per hybrid channel (Tables 8.48 / 8.49). +#[must_use] +pub fn parameter_map(config: HybridConfig) -> &'static [u8] { + match config { + HybridConfig::Bands1020 => &B_K_20, + HybridConfig::Bands34 => &B_K_34, + } +} + +/// The `*`-marked hybrid channels of Tables 8.48 / 8.49 — the +/// negative-frequency sub-subbands whose `h` coefficients apply +/// complex-conjugated when phase parameters are enabled. +#[must_use] +pub fn conjugate_flags(config: HybridConfig) -> &'static [usize] { + match config { + HybridConfig::Bands1020 => &[0, 1], + HybridConfig::Bands34 => &[9, 10, 11], + } +} + +/// §8.6.4.6.1 — map a 10-band parameter vector to 20 bands by +/// duplication (Table 8.45: `20idx_k ← 10idx_{k/2}`). +#[must_use] +pub fn map_10_to_20(v: &[i32]) -> Vec { + (0..20).map(|k| v[k / 2]).collect() +} + +/// Table 8.45 — 20→34 source per 34-band entry: `Single(i)` copies +/// `idx_i`, `Avg(i, j)` takes `(idx_i + idx_j) / 2` (integer +/// arithmetic on indices). +#[derive(Debug, Clone, Copy)] +pub enum MapSrc { + /// Copy one source band. + Single(usize), + /// Average two source bands. + Avg(usize, usize), + /// Average four source bands (only 34→20's `idx18`). + Avg4(usize, usize, usize, usize), + /// Weighted `(2·a + b)/3`. + W21(usize, usize), + /// Weighted `(a + 2·b)/3`. + W12(usize, usize), +} + +/// Table 8.45 — mapping from 20 to 34 parameters. +pub const MAP_20_TO_34: [MapSrc; 34] = [ + MapSrc::Single(0), + MapSrc::Avg(0, 1), + MapSrc::Single(1), + MapSrc::Single(2), + MapSrc::Avg(2, 3), + MapSrc::Single(3), + MapSrc::Single(4), + MapSrc::Single(4), + MapSrc::Single(5), + MapSrc::Single(5), + MapSrc::Single(6), + MapSrc::Single(7), + MapSrc::Single(8), + MapSrc::Single(8), + MapSrc::Single(9), + MapSrc::Single(9), + MapSrc::Single(10), + MapSrc::Single(11), + MapSrc::Single(12), + MapSrc::Single(13), + MapSrc::Single(14), + MapSrc::Single(14), + MapSrc::Single(15), + MapSrc::Single(15), + MapSrc::Single(16), + MapSrc::Single(16), + MapSrc::Single(17), + MapSrc::Single(17), + MapSrc::Single(18), + MapSrc::Single(18), + MapSrc::Single(18), + MapSrc::Single(18), + MapSrc::Single(19), + MapSrc::Single(19), +]; + +/// Table 8.46 — mapping from 34 down to 20 parameters. +pub const MAP_34_TO_20: [MapSrc; 20] = [ + MapSrc::W21(0, 1), + MapSrc::W12(1, 2), + MapSrc::W21(3, 4), + MapSrc::W12(4, 5), + MapSrc::Avg(6, 7), + MapSrc::Avg(8, 9), + MapSrc::Single(10), + MapSrc::Single(11), + MapSrc::Avg(12, 13), + MapSrc::Avg(14, 15), + MapSrc::Single(16), + MapSrc::Single(17), + MapSrc::Single(18), + MapSrc::Single(19), + MapSrc::Avg(20, 21), + MapSrc::Avg(22, 23), + MapSrc::Avg(24, 25), + MapSrc::Avg(26, 27), + MapSrc::Avg4(28, 29, 30, 31), + MapSrc::Avg(32, 33), +]; + +impl MapSrc { + /// Apply to an integer index vector (ANSI-C truncating division). + #[must_use] + pub fn apply_i32(&self, v: &[i32]) -> i32 { + match *self { + MapSrc::Single(i) => v[i], + MapSrc::Avg(i, j) => (v[i] + v[j]) / 2, + MapSrc::Avg4(i, j, k, l) => (v[i] + v[j] + v[k] + v[l]) / 4, + MapSrc::W21(i, j) => (2 * v[i] + v[j]) / 3, + MapSrc::W12(i, j) => (v[i] + 2 * v[j]) / 3, + } + } + + /// Apply to a float vector (the `h`-coefficient hand-over on a + /// stereo-band-count switch, §8.6.4.6.1). + #[must_use] + pub fn apply_f64(&self, v: &[f64]) -> f64 { + match *self { + MapSrc::Single(i) => v[i], + MapSrc::Avg(i, j) => (v[i] + v[j]) / 2.0, + MapSrc::Avg4(i, j, k, l) => (v[i] + v[j] + v[k] + v[l]) / 4.0, + MapSrc::W21(i, j) => (2.0 * v[i] + v[j]) / 3.0, + MapSrc::W12(i, j) => (v[i] + 2.0 * v[j]) / 3.0, + } + } +} + +/// Map an index vector of `n` parameters (10, 20 or 34) to the target +/// stereo-band count (20 or 34), per §8.6.4.6.1: 10→20 duplication, +/// 20→34 via Table 8.45, 34→20 via Table 8.46, 10→34 via 20. +#[must_use] +pub fn map_indices(v: &[i32], target: usize) -> Vec { + match (v.len(), target) { + (n, t) if n == t => v.to_vec(), + (10, 20) => map_10_to_20(v), + (20, 34) => MAP_20_TO_34.iter().map(|m| m.apply_i32(v)).collect(), + (10, 34) => { + let v20 = map_10_to_20(v); + MAP_20_TO_34.iter().map(|m| m.apply_i32(&v20)).collect() + } + (34, 20) => MAP_34_TO_20.iter().map(|m| m.apply_i32(v)).collect(), + // Shorter vectors (IPD/OPD's nr_ipdopd_par = 5/11/17) are + // handled by the caller; anything else passes through. + _ => v.to_vec(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn b_k_tables_are_consistent() { + assert_eq!(B_K_20.len(), 71); + assert_eq!(B_K_34.len(), 91); + assert!(B_K_20.iter().all(|&b| b < 20)); + assert!(B_K_34.iter().all(|&b| b < 34)); + // Every stereo band is hit at least once. + for b in 0..20u8 { + assert!(B_K_20.contains(&b), "20-band {b} unused"); + } + for b in 0..34u8 { + assert!(B_K_34.contains(&b), "34-band {b} unused"); + } + // The unsplit QMF region of the 20-band table: k=10..16 map + // QMF bands 3..9 one-to-one (Table 8.48 rows 10..15 + 16-17). + assert_eq!(&B_K_20[10..16], &[8, 9, 10, 11, 12, 13]); + // Table 8.49 spot rows: the QMF band 1 sub-subbands reach into + // stereo bands 4..10. + assert_eq!(&B_K_34[12..20], &[10, 10, 4, 5, 6, 7, 8, 9]); + } + + #[test] + fn index_mapping_round_trips_shape() { + let v10: Vec = (0..10).collect(); + let v20 = map_indices(&v10, 20); + assert_eq!( + v20, + vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9] + ); + let v34 = map_indices(&v20, 34); + assert_eq!(v34.len(), 34); + // Table 8.45 first rows: idx0, (idx0+idx1)/2, idx1, idx2, ... + assert_eq!(v34[0], 0); + assert_eq!(v34[1], (v20[0] + v20[1]) / 2); + assert_eq!(v34[2], v20[1]); + let back = map_indices(&v34, 20); + assert_eq!(back.len(), 20); + // A constant vector survives every mapping exactly. + let c34 = map_indices(&[5i32; 20], 34); + assert_eq!(c34, vec![5i32; 34]); + let c20 = map_indices(&[5i32; 34], 20); + assert_eq!(c20, vec![5i32; 20]); + } + + #[test] + fn ansi_c_integer_average_truncates_toward_zero() { + // (-3 + 2)/2 = -0 in C (truncation), not -1 (flooring). + let v = vec![-3i32, 2]; + assert_eq!(MapSrc::Avg(0, 1).apply_i32(&v), 0); + assert_eq!(MapSrc::W21(0, 1).apply_i32(&v), -1); // (-6+2)/3 + } +} diff --git a/crates/vendor/oxideav-aac/src/ps_stereo.rs b/crates/vendor/oxideav-aac/src/ps_stereo.rs new file mode 100644 index 00000000..74261e38 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ps_stereo.rs @@ -0,0 +1,541 @@ +//! PS stereo processing — ISO/IEC 14496-3:2009 §8.6.4.6. +//! +//! Converts the mono hybrid signal `s_k(n)` and its de-correlation +//! `d_k(n)` into left/right hybrid signals through the 2×2 mixing +//! +//! ```text +//! l_k(n) = H11(k,n)·s_k(n) + H21(k,n)·d_k(n) +//! r_k(n) = H12(k,n)·s_k(n) + H22(k,n)·d_k(n) +//! ``` +//! +//! Per parameter position (envelope border) the vectors `h11..h22` +//! are derived per stereo band from the dequantized cues: +//! +//! * IID: `c(b) = 10^(iid(b)/20)` on the Table 8.25 (default) or +//! 8.26 (fine) dB grid; +//! * ICC: `ρ(b)` on the Table 8.28 grid, driving **mixing procedure +//! Ra** (`icc_mode 0..2`: scale factors `c1 = √(2/(1+c²))`, +//! `c2 = √2·c/√(1+c²)`, rotation `α = ½·arccos(ρ)`, +//! `β = α·(c1−c2)/√2`) or **Rb** (`icc_mode 3..5`: `ρ` floored at +//! 0.05, `α = ½·arctan(2cρ/(c²−1))` with the `c = 1` and +//! modulo-π/2 corrections, `μ`/`γ` per §8.6.4.6.2.2); +//! * IPD/OPD (§8.6.4.6.3.2, when enabled): the three-position +//! smoothing `φ = ∠(¼e^(j·prev2) + ½e^(j·prev1) + e^(j·cur))` on +//! the Table 8.31 `π/4` ladder, applied as `e^(jφ1)` on `h11/h21` +//! and `e^(jφ2)` (`φ2 = φ_opd − φ_ipd`) on `h12/h22`; the +//! `*`-marked negative-frequency hybrid channels take the complex +//! conjugate. +//! +//! Between borders the four H matrices are linearly interpolated +//! (§8.6.4.6.4), the first region interpolating from the previous +//! frame's final coefficients (zeros on the very first frame), the +//! region after the last border holding. FIX_BORDERS positions are +//! `⌊32·(e+1)/num_env⌋ − 1`; VAR_BORDERS come from the bitstream. +//! `num_env == 0` holds the previous frame's coefficients for the +//! whole frame (§8.6.4.6.5). A stereo-band-count switch (Table 8.47) +//! re-maps the retained coefficients through Tables 8.45 / 8.46. +//! +//! All truth from ISO/IEC 14496-3:2009 §8.6.4.6 staged under +//! `docs/audio/aac/`. + +use crate::ps_data::{PsData, PsIndices}; +use crate::ps_hybrid::{HybridConfig, NUM_QMF_SLOTS}; +use crate::ps_map::{conjugate_flags, map_indices, parameter_map, MAP_20_TO_34, MAP_34_TO_20}; +use crate::sbr_qmf::Complex; +use crate::{Error, Result}; + +/// Table 8.25 — default IID quantization grid, dB, index −7..7. +const IID_DB_COARSE: [f64; 15] = [ + -25.0, -18.0, -14.0, -10.0, -7.0, -4.0, -2.0, 0.0, 2.0, 4.0, 7.0, 10.0, 14.0, 18.0, 25.0, +]; + +/// Table 8.26 — fine IID quantization grid, dB, index −15..15. +const IID_DB_FINE: [f64; 31] = [ + -50.0, -45.0, -40.0, -35.0, -30.0, -25.0, -22.0, -19.0, -16.0, -13.0, -10.0, -8.0, -6.0, -4.0, + -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 13.0, 16.0, 19.0, 22.0, 25.0, 30.0, 35.0, 40.0, 45.0, + 50.0, +]; + +/// Table 8.28 — ICC quantization grid `ρ`. +const ICC_RHO: [f64; 8] = [1.0, 0.937, 0.84118, 0.60092, 0.36764, 0.0, -0.589, -1.0]; + +/// One stereo band's mixing coefficients. +type H4 = [Complex; 4]; // h11, h12, h21, h22 + +/// A stereo pair of hybrid-domain frames. +pub type HybridPair = (Vec>, Vec>); + +/// §8.6.4.6 stereo processor: per-band coefficient state across +/// frames plus the IPD/OPD smoothing history. +#[derive(Debug, Clone)] +pub struct PsStereo { + /// Stereo band count in force (20 or 34). + n_bands: usize, + /// `H(·, n_{−1})` — coefficients at the previous frame's last + /// slot, per stereo band. + h_prev: Vec

, + /// IPD/OPD angle history: `[.., e−1]` and `[.., e]` positions + /// (radians), per stereo band. + ipd_hist: [Vec; 2], + opd_hist: [Vec; 2], +} + +impl PsStereo { + /// Fresh state (first frame interpolates from zero coefficients). + #[must_use] + pub fn new(n_bands: usize) -> Self { + PsStereo { + n_bands, + h_prev: vec![[Complex::default(); 4]; n_bands], + ipd_hist: [vec![0.0; n_bands], vec![0.0; n_bands]], + opd_hist: [vec![0.0; n_bands], vec![0.0; n_bands]], + } + } + + /// Table 8.47 — switch the stereo band count, re-mapping the + /// retained coefficients through Table 8.45 / 8.46 and resetting + /// the phase-smoothing history. + pub fn switch_bands(&mut self, n_bands: usize) { + if n_bands == self.n_bands { + return; + } + let map = |vals: Vec| -> Vec { + if n_bands == 34 { + MAP_20_TO_34.iter().map(|m| m.apply_f64(&vals)).collect() + } else { + MAP_34_TO_20.iter().map(|m| m.apply_f64(&vals)).collect() + } + }; + let mut new_h = vec![[Complex::default(); 4]; n_bands]; + for c in 0..4 { + let re: Vec = self.h_prev.iter().map(|h| h[c].re).collect(); + let im: Vec = self.h_prev.iter().map(|h| h[c].im).collect(); + let re = map(re); + let im = map(im); + for (b, h) in new_h.iter_mut().enumerate() { + h[c] = Complex::new(re[b], im[b]); + } + } + self.h_prev = new_h; + self.n_bands = n_bands; + self.ipd_hist = [vec![0.0; n_bands], vec![0.0; n_bands]]; + self.opd_hist = [vec![0.0; n_bands], vec![0.0; n_bands]]; + } + + /// The stereo band count in force. + #[must_use] + pub fn n_bands(&self) -> usize { + self.n_bands + } + + /// Process one stereo frame: mix `s` (mono hybrid) and `d` + /// (de-correlated hybrid) into `(l, r)` hybrid signals per the + /// resolved parameters. `config` must agree with `n_bands`. + pub fn process( + &mut self, + ps: &PsData, + idx: &PsIndices, + config: HybridConfig, + s: &[Vec], + d: &[Vec], + ) -> Result { + let nb = self.n_bands; + let expected = match config { + HybridConfig::Bands1020 => 20, + HybridConfig::Bands34 => 34, + }; + if expected != nb || s.len() != NUM_QMF_SLOTS || d.len() != NUM_QMF_SLOTS { + return Err(Error::PsDataInvalid); + } + let b_k = parameter_map(config); + let conj_k = conjugate_flags(config); + let nr_hyb = config.nr_bands(); + if s.iter().chain(d.iter()).any(|row| row.len() != nr_hyb) { + return Err(Error::PsDataInvalid); + } + + // Per-slot H matrices, per stereo band. + let mut h_slots = vec![vec![[Complex::default(); 4]; nb]; NUM_QMF_SLOTS]; + + if ps.num_env == 0 { + // §8.6.4.6.5: hold the previous coefficients all frame. + for slot in h_slots.iter_mut() { + slot.copy_from_slice(&self.h_prev); + } + } else { + // Envelope borders n_e. + let borders: Vec = if ps.frame_class { + ps.border_position + .iter() + .map(|&b| usize::from(b).min(NUM_QMF_SLOTS - 1)) + .collect() + } else { + (0..ps.num_env) + .map(|e| NUM_QMF_SLOTS * (e + 1) / ps.num_env - 1) + .collect() + }; + + let mut h_from = self.h_prev.clone(); + let mut n_from: isize = -1; // "border" behind slot 0 + for (e, &n_e) in borders.iter().enumerate() { + let h_to = self.envelope_h(ps, idx, e)?; + // §8.6.4.6.4: first region divides by n_0 with + // multiplier n; later regions by (n_e − n_{e−1}). + let (den, base) = if e == 0 { + (n_e.max(1) as f64, 0isize) + } else { + (((n_e as isize - n_from).max(1)) as f64, n_from) + }; + let lo = ((n_from + 1).max(0)) as usize; + let hi = n_e.min(NUM_QMF_SLOTS - 1); + for (n, slot) in h_slots.iter_mut().enumerate().take(hi + 1).skip(lo) { + let t = (n as isize - base) as f64 / den; + for (b, cell) in slot.iter_mut().enumerate() { + for c in 0..4 { + cell[c] = h_from[b][c] + (h_to[b][c] - h_from[b][c]) * t; + } + } + } + h_from = h_to; + n_from = n_e as isize; + } + // Region after the last border: hold. + let lo = ((n_from + 1).max(0)) as usize; + for slot in h_slots.iter_mut().skip(lo) { + slot.copy_from_slice(&h_from); + } + self.h_prev = h_from; + } + + // Mix. + let mut l = vec![vec![Complex::default(); nr_hyb]; NUM_QMF_SLOTS]; + let mut r = vec![vec![Complex::default(); nr_hyb]; NUM_QMF_SLOTS]; + for n in 0..NUM_QMF_SLOTS { + for k in 0..nr_hyb { + let b = usize::from(b_k[k]); + let mut h = h_slots[n][b]; + if conj_k.contains(&k) { + for c in h.iter_mut() { + *c = c.conj(); + } + } + l[n][k] = h[0] * s[n][k] + h[2] * d[n][k]; + r[n][k] = h[1] * s[n][k] + h[3] * d[n][k]; + } + } + Ok((l, r)) + } + + /// Derive `h11..h22` per stereo band for envelope `e` + /// (§8.6.4.6.2 + §8.6.4.6.3), advancing the phase history. + fn envelope_h(&mut self, ps: &PsData, idx: &PsIndices, e: usize) -> Result> { + let nb = self.n_bands; + + // Map the parameter vectors to the stereo band count; a + // disabled parameter kind is index 0 (§8.5.2 defaults). + let iid = match idx.iid.get(e) { + Some(v) => map_indices(v, nb), + None => vec![0; nb], + }; + let icc = match idx.icc.get(e) { + Some(v) => map_indices(v, nb), + None => vec![0; nb], + }; + if iid.len() != nb || icc.len() != nb { + return Err(Error::PsDataInvalid); + } + + let fine = ps.config.iid_quant_fine(); + let rb = ps.config.icc_mode >= 3; + + let mut out = vec![[Complex::default(); 4]; nb]; + for b in 0..nb { + let iid_db = if fine { + *IID_DB_FINE + .get((iid[b] + 15) as usize) + .ok_or(Error::PsDataInvalid)? + } else { + *IID_DB_COARSE + .get((iid[b] + 7) as usize) + .ok_or(Error::PsDataInvalid)? + }; + let c = 10f64.powf(iid_db / 20.0); + let rho = *ICC_RHO.get(icc[b] as usize).ok_or(Error::PsDataInvalid)?; + + let (h11, h12, h21, h22) = if rb { mix_rb(c, rho) } else { mix_ra(c, rho) }; + out[b] = [ + Complex::new(h11, 0.0), + Complex::new(h12, 0.0), + Complex::new(h21, 0.0), + Complex::new(h22, 0.0), + ]; + } + + if ps.enable_ipdopd { + // Zero-extended, band-count-mapped phase indices. + let nr = ps.config.nr_ipdopd_par(); + let native = if ps.config.iid_mode % 3 == 0 { + 10 + } else if ps.config.iid_mode % 3 == 1 { + 20 + } else { + 34 + }; + let extend = |v: Option<&Vec>| -> Vec { + let mut full = vec![0i32; native]; + if let Some(v) = v { + full[..nr.min(v.len())].copy_from_slice(&v[..nr.min(v.len())]); + } + map_indices(&full, nb) + .iter() + .map(|&i| f64::from(i) * core::f64::consts::FRAC_PI_4) + .collect() + }; + let ipd_cur = extend(idx.ipd.get(e)); + let opd_cur = extend(idx.opd.get(e)); + for b in 0..nb { + let sm = |h: &[Vec; 2], cur: f64| -> f64 { + let mut acc = Complex::default(); + for (w, ang) in [(0.25, h[0][b]), (0.5, h[1][b]), (1.0, cur)] { + let (si, co) = ang.sin_cos(); + acc += Complex::new(co * w, si * w); + } + acc.im.atan2(acc.re) + }; + let phi_opd = sm(&self.opd_hist, opd_cur[b]); + let phi_ipd = sm(&self.ipd_hist, ipd_cur[b]); + let phi1 = phi_opd; + let phi2 = phi_opd - phi_ipd; + let (s1, c1) = phi1.sin_cos(); + let (s2, c2) = phi2.sin_cos(); + let r1 = Complex::new(c1, s1); + let r2 = Complex::new(c2, s2); + out[b][0] = out[b][0] * r1; + out[b][2] = out[b][2] * r1; + out[b][1] = out[b][1] * r2; + out[b][3] = out[b][3] * r2; + } + // Advance the history. + self.ipd_hist[0] = core::mem::take(&mut self.ipd_hist[1]); + self.ipd_hist[1] = ipd_cur; + self.opd_hist[0] = core::mem::take(&mut self.opd_hist[1]); + self.opd_hist[1] = opd_cur; + } + Ok(out) + } +} + +/// §8.6.4.6.2.1 mixing procedure Ra. +fn mix_ra(c: f64, rho: f64) -> (f64, f64, f64, f64) { + let denom = (1.0 + c * c).sqrt(); + let c1 = core::f64::consts::SQRT_2 / denom; + let c2 = core::f64::consts::SQRT_2 * c / denom; + let alpha = 0.5 * rho.clamp(-1.0, 1.0).acos(); + let beta = alpha * (c1 - c2) / core::f64::consts::SQRT_2; + ( + (alpha + beta).cos() * c2, + (beta - alpha).cos() * c1, + (alpha + beta).sin() * c2, + (beta - alpha).sin() * c1, + ) +} + +/// §8.6.4.6.2.2 mixing procedure Rb. +fn mix_rb(c: f64, rho: f64) -> (f64, f64, f64, f64) { + let rho = rho.max(0.05); + let mut alpha = if (c - 1.0).abs() < 1e-12 { + core::f64::consts::FRAC_PI_4 + } else { + 0.5 * (2.0 * c * rho / (c * c - 1.0)).atan() + }; + // Modulo correction into [0, π/2). + alpha -= (alpha / core::f64::consts::FRAC_PI_2).floor() * core::f64::consts::FRAC_PI_2; + let mu = 1.0 + (4.0 * rho * rho - 4.0) / (c + 1.0 / c).powi(2); + let gamma = ((1.0 - mu) / (1.0 + mu)).max(0.0).sqrt().atan(); + let s2 = core::f64::consts::SQRT_2; + ( + s2 * alpha.cos() * gamma.cos(), + s2 * alpha.sin() * gamma.cos(), + -s2 * alpha.sin() * gamma.sin(), + s2 * alpha.cos() * gamma.sin(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ps_data::PsIndexState; + use oxideav_core::bits::{BitReader, BitWriter}; + + /// Build a one-envelope FIX ps_data with the given uniform IID / + /// ICC index (coarse grid, 10 pars each) and resolve it. + fn ps_with(iid_idx: i32, icc_idx: i32) -> (PsData, PsIndices) { + let mut w = BitWriter::new(); + w.write_bit(true); // header + w.write_bit(true); // enable_iid + w.write_u32(0, 3); // iid_mode 0 + w.write_bit(true); // enable_icc + w.write_u32(0, 3); // icc_mode 0 + w.write_bit(false); // enable_ext + w.write_bit(false); // FIX + w.write_u32(1, 2); // num_env = 1 + w.write_bit(false); // iid freq + for b in 0..10 { + // First band carries the index, the rest delta 0. + let (len, code) = crate::ps_huffman::HUFF_IID_DF[(iid_idx + 14) as usize]; + if b == 0 { + w.write_u32(code, u32::from(len)); + } else { + let (l0, c0) = crate::ps_huffman::HUFF_IID_DF[14]; + w.write_u32(c0, u32::from(l0)); + } + } + w.write_bit(false); // icc freq + for b in 0..10 { + let (len, code) = crate::ps_huffman::HUFF_ICC_DF[(icc_idx + 7) as usize]; + if b == 0 { + w.write_u32(code, u32::from(len)); + } else { + let (l0, c0) = crate::ps_huffman::HUFF_ICC_DF[7]; + w.write_u32(c0, u32::from(l0)); + } + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ps = PsData::parse(&mut r, None).unwrap().unwrap(); + let mut st = PsIndexState::default(); + let idx = ps.resolve(&mut st).unwrap(); + (ps, idx) + } + + fn ones(nr: usize) -> Vec> { + (0..NUM_QMF_SLOTS) + .map(|_| vec![Complex::new(1.0, 0.0); nr]) + .collect() + } + + fn zeros(nr: usize) -> Vec> { + (0..NUM_QMF_SLOTS) + .map(|_| vec![Complex::default(); nr]) + .collect() + } + + /// ICC = 1 (index 0) makes α = β = 0: the mix is pure IID panning + /// `l = c2·s`, `r = c1·s`, `d` unused. Pin the exact §8.6.4.6.2.1 + /// scale factors at the last slot (interpolation complete). + #[test] + fn pure_iid_panning_matches_scale_factors() { + let config = HybridConfig::Bands1020; + let (ps, idx) = ps_with(7, 0); // +25 dB + let mut st = PsStereo::new(20); + let s = ones(config.nr_bands()); + let d = zeros(config.nr_bands()); + let (l, r) = st.process(&ps, &idx, config, &s, &d).unwrap(); + let c = 10f64.powf(25.0 / 20.0); + let c1 = core::f64::consts::SQRT_2 / (1.0 + c * c).sqrt(); + let c2 = c * c1; + // Hybrid channel 20 (stereo band 16), final slot: h fully + // interpolated to the envelope value. + let n = NUM_QMF_SLOTS - 1; + assert!((l[n][20].re - c2).abs() < 1e-12, "{} vs {c2}", l[n][20].re); + assert!((r[n][20].re - c1).abs() < 1e-12); + assert!(l[n][20].im.abs() < 1e-15 && r[n][20].im.abs() < 1e-15); + // Left is 25 dB louder. + let ratio = 20.0 * (l[n][20].re / r[n][20].re).log10(); + assert!((ratio - 25.0).abs() < 1e-9); + } + + /// ICC = −1 (index 7) with IID 0: α = π/2, the channels are the + /// anti-phase de-correlated pair `l = d`, `r = −d` (§8.6.4.6.2.1). + #[test] + fn full_anticorrelation_uses_decorrelated_signal() { + let config = HybridConfig::Bands1020; + let (ps, idx) = ps_with(0, 7); + let mut st = PsStereo::new(20); + let s = ones(config.nr_bands()); + let d = ones(config.nr_bands()); + let (l, r) = st.process(&ps, &idx, config, &s, &d).unwrap(); + let n = NUM_QMF_SLOTS - 1; + // h11 = cos(π/2) = 0, h21 = sin(π/2) = 1 → l = d. + assert!((l[n][20].re - 1.0).abs() < 1e-12); + // h12 = cos(−π/2) = 0, h22 = sin(−π/2) = −1 → r = −d. + assert!((r[n][20].re + 1.0).abs() < 1e-12); + } + + /// The first region interpolates from zero (fresh state) to the + /// envelope coefficients linearly in n/n_0. + #[test] + fn first_region_interpolates_from_zero() { + let config = HybridConfig::Bands1020; + let (ps, idx) = ps_with(0, 0); // IID 0 dB, ICC 1 → h11 = h12 = 1 + let mut st = PsStereo::new(20); + let s = ones(config.nr_bands()); + let d = zeros(config.nr_bands()); + let (l, _r) = st.process(&ps, &idx, config, &s, &d).unwrap(); + // num_env = 1, FIX → n_0 = 31; H(n) = n/31 · h. + for (n, row) in l.iter().enumerate() { + let expect = n as f64 / 31.0; + assert!( + (row[20].re - expect).abs() < 1e-12, + "slot {n}: {} vs {expect}", + row[20].re + ); + } + // A second identical frame is flat at the full value. + let (l2, _) = st.process(&ps, &idx, config, &s, &d).unwrap(); + for row in &l2 { + assert!((row[20].re - 1.0).abs() < 1e-12); + } + } + + /// num_env = 0 holds the previous coefficients for the whole + /// frame. + #[test] + fn zero_envelopes_hold_previous_coefficients() { + let config = HybridConfig::Bands1020; + let (ps, idx) = ps_with(7, 0); + let mut st = PsStereo::new(20); + let s = ones(config.nr_bands()); + let d = zeros(config.nr_bands()); + let _ = st.process(&ps, &idx, config, &s, &d).unwrap(); + // Hold frame: num_env = 0 (frame_class FIX, num_env_idx 0). + let mut hold = ps.clone(); + hold.num_env = 0; + let empty = PsIndices::default(); + let (l, r) = st.process(&hold, &empty, config, &s, &d).unwrap(); + let c = 10f64.powf(25.0 / 20.0); + let c1 = core::f64::consts::SQRT_2 / (1.0 + c * c).sqrt(); + for row in &l { + assert!((row[20].re - c * c1).abs() < 1e-12); + } + for row in &r { + assert!((row[20].re - c1).abs() < 1e-12); + } + } + + /// Rb at c = 1, ρ = 1: α = π/4, μ = 1, γ = 0 → an energy- + /// preserving 45° rotation of the mono signal (`h11 = h12 = 1`, + /// `h21 = h22 = 0`). + #[test] + fn rb_identity_point() { + let (h11, h12, h21, h22) = mix_rb(1.0, 1.0); + assert!((h11 - 1.0).abs() < 1e-12); + assert!((h12 - 1.0).abs() < 1e-12); + assert!(h21.abs() < 1e-12); + assert!(h22.abs() < 1e-12); + } + + /// Ra preserves total energy: |h11|² + |h12|² + |h21|² + |h22|² + /// = c1² + c2² = 2 for every cue combination. + #[test] + fn ra_energy_invariant() { + for iid in -7..=7 { + for &rho in &ICC_RHO { + let c = 10f64.powf(IID_DB_COARSE[(iid + 7) as usize] / 20.0); + let (a, b, x, y) = mix_ra(c, rho); + let e = a * a + b * b + x * x + y * y; + assert!((e - 2.0).abs() < 1e-12, "iid {iid} rho {rho}: {e}"); + } + } + } +} diff --git a/crates/vendor/oxideav-aac/src/pulse_data.rs b/crates/vendor/oxideav-aac/src/pulse_data.rs new file mode 100644 index 00000000..e6292b0e --- /dev/null +++ b/crates/vendor/oxideav-aac/src/pulse_data.rs @@ -0,0 +1,190 @@ +//! `pulse_data()` parser + encoder primitive — ISO/IEC 14496-3 +//! §4.4.6.3 / Table 4.7. +//! +//! `pulse_data()` is the optional "pulse escape" tool inside +//! `individual_channel_stream()`: when the encoder finds it cheaper +//! to replace a small number (1..=4) of quantised spectral +//! coefficients with smaller ones plus a fix-up record than to spend +//! the bits on the literal escape codeword, it writes a `pulse_data()` +//! block that the decoder uses to restore the original amplitudes +//! after Huffman decoding. The pulse escape is dispatched by the +//! one-bit `pulse_data_present` flag immediately after +//! `scale_factor_data()` (Table 4.44 / Table 4.50). +//! +//! ## Wire layout (Table 4.7) +//! +//! ```text +//! pulse_data() { +//! number_pulse; 2 bits +//! pulse_start_sfb; 6 bits +//! for (i = 0; i < number_pulse + 1; i++) { +//! pulse_offset[i]; 5 bits +//! pulse_amp[i]; 4 bits +//! } +//! } +//! ``` +//! +//! Every field is fixed-width. The actual pulse count on the wire +//! is `number_pulse + 1` (so 1..=4 pulses, never zero), encoded in +//! 2 bits as `0..=3`. +//! +//! ## What this module covers +//! +//! * [`PulseData::parse`] — read a Table 4.7 block from a +//! [`BitReader`], surfacing the raw wire fields without applying +//! the §4.6.13 reconstruction (the spectral fix-up itself needs +//! `swb_offset_long_window[]` + the post-Huffman `x_quant` array, +//! neither of which exists in Phase 2 yet). +//! * [`PulseData::write`] — the inverse: serialise a [`PulseData`] +//! onto a [`BitWriter`] in bit-exact Table 4.7 form. Surfaces +//! field-overflow as [`Error::PulseDataEncodeInvalid`]. +//! +//! ## What this module does *not* cover +//! +//! * The §4.6.13 reconstruction loop (`k += +//! swb_offset[pulse_start_sfb]; k += pulse_offset[j]; x_quant[…] ±= +//! pulse_amp[j]`) is deferred until `swb_offset` tables land with +//! `spectral_data()`. +//! * The normative constraint that `pulse_data_present` *must* be 0 +//! when `window_sequence == EIGHT_SHORT_SEQUENCE` (§4.4.6.3 last +//! paragraph) is the responsibility of the dispatching +//! `individual_channel_stream()` (which has not landed yet); the +//! parser and writer here intentionally surface the literal Table 4.7 +//! bytes regardless of the surrounding window sequence so that +//! future round work has access to the raw decoded record. +//! * No validation against `swb_offset_long_window[fs_index]` — the +//! parser cannot tell whether `pulse_start_sfb` is in-range for a +//! given sample rate without the offset table; the encoder cannot +//! tell whether a pulse position lands inside the represented +//! coefficient grid. These are §4.6.13 reconstruction concerns, +//! not Table 4.7 wire-format concerns. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::{Error, Result}; + +/// Per-pulse `(offset, amp)` record. Both fields are unsigned. +/// +/// * `offset` — 5 bits. `pulse_offset[i]` per Table 4.7. Read by +/// the decoder as a delta added to the running coefficient index +/// `k` (initialised to `swb_offset[pulse_start_sfb]` before the +/// loop). +/// * `amp` — 4 bits. `pulse_amp[i]` per Table 4.7. Unsigned +/// magnitude added to (or subtracted from, depending on the sign +/// of the existing `x_quant` coefficient) the reconstructed +/// spectral coefficient. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pulse { + /// `pulse_offset[i]` — 5-bit unsigned delta. + pub offset: u8, + /// `pulse_amp[i]` — 4-bit unsigned magnitude. + pub amp: u8, +} + +/// Width in bits of the wire `pulse_offset` field. ISO/IEC 14496-3 +/// Table 4.7. +pub const PULSE_OFFSET_BITS: u32 = 5; + +/// Width in bits of the wire `pulse_amp` field. ISO/IEC 14496-3 +/// Table 4.7. +pub const PULSE_AMP_BITS: u32 = 4; + +/// Maximum pulse count expressible in the 2-bit `number_pulse` +/// field. The wire value runs `0..=3`; the actual pulse count is +/// `number_pulse + 1`, so `MAX_PULSES == 4`. +pub const MAX_PULSES: usize = 4; + +/// Parsed `pulse_data()` block (Table 4.7). +/// +/// `pulses` always carries 1..=4 entries (since `number_pulse + 1 +/// >= 1`); this is enforced by the writer and produced by the +/// parser. An empty `pulses` vector is rejected by [`PulseData::write`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PulseData { + /// `pulse_start_sfb` — 6-bit index of the lowest scalefactor + /// band that carries a pulse fix-up. + pub pulse_start_sfb: u8, + /// `pulses[i]` — the `(offset, amp)` records, in wire order. + /// Length is in `1..=MAX_PULSES`; the wire `number_pulse` field + /// is `pulses.len() - 1`. + pub pulses: Vec, +} + +impl PulseData { + /// Parse a `pulse_data()` from `reader`. + /// + /// Returns [`Error::UnexpectedEnd`] on bit-reader underflow. + /// Never returns a structural-error variant because every field + /// of Table 4.7 is fixed-width and unconditionally well-formed + /// up to bit-position arithmetic. + pub fn parse(reader: &mut BitReader<'_>) -> Result { + let number_pulse = read_u8(reader, 2)?; + let pulse_start_sfb = read_u8(reader, 6)?; + let count = number_pulse as usize + 1; + let mut pulses = Vec::with_capacity(count); + for _ in 0..count { + let offset = read_u8(reader, PULSE_OFFSET_BITS)?; + let amp = read_u8(reader, PULSE_AMP_BITS)?; + pulses.push(Pulse { offset, amp }); + } + Ok(PulseData { + pulse_start_sfb, + pulses, + }) + } + + /// Wire `number_pulse` value (always `pulses.len() - 1`). + /// Returns `0` for an empty `pulses` vector, which is itself + /// rejected by [`PulseData::write`] — the accessor exists so the + /// writer doesn't have to inline the subtraction with a saturating + /// path of its own. + pub fn number_pulse(&self) -> u8 { + self.pulses.len().saturating_sub(1) as u8 + } + + /// Encode `pulse_data()` onto `writer`, the inverse of + /// [`PulseData::parse`]. + /// + /// The writer mirrors Table 4.7 verbatim — 2-bit `number_pulse` + /// (where the wire value is `pulses.len() - 1`), 6-bit + /// `pulse_start_sfb`, then `(5-bit pulse_offset + 4-bit + /// pulse_amp)` per entry. + /// + /// Returns [`Error::PulseDataEncodeInvalid`] if: + /// + /// * `pulses.is_empty()` — Table 4.7's loop bound is + /// `number_pulse + 1`, so the smallest legal pulse count is 1. + /// A zero-pulse block has no wire representation that round- + /// trips through [`PulseData::parse`]. + /// * `pulses.len() > MAX_PULSES` (the 2-bit field maxes at 4). + /// * `pulse_start_sfb > 0x3f` (6-bit field overflow). + /// * Any `Pulse::offset > 0x1f` (5-bit field overflow). + /// * Any `Pulse::amp > 0x0f` (4-bit field overflow). + pub fn write(&self, writer: &mut BitWriter) -> Result<()> { + if self.pulses.is_empty() || self.pulses.len() > MAX_PULSES { + return Err(Error::PulseDataEncodeInvalid); + } + if self.pulse_start_sfb > 0x3f { + return Err(Error::PulseDataEncodeInvalid); + } + for p in &self.pulses { + if p.offset > 0x1f || p.amp > 0x0f { + return Err(Error::PulseDataEncodeInvalid); + } + } + + let number_pulse = (self.pulses.len() - 1) as u32; + writer.write_u32(number_pulse, 2); + writer.write_u32(self.pulse_start_sfb as u32, 6); + for p in &self.pulses { + writer.write_u32(p.offset as u32, PULSE_OFFSET_BITS); + writer.write_u32(p.amp as u32, PULSE_AMP_BITS); + } + Ok(()) + } +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} diff --git a/crates/vendor/oxideav-aac/src/raw_data_block.rs b/crates/vendor/oxideav-aac/src/raw_data_block.rs new file mode 100644 index 00000000..727ee1c1 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/raw_data_block.rs @@ -0,0 +1,666 @@ +//! `raw_data_block()` syntactic walker. +//! +//! ISO/IEC 14496-3 §4.4.2.1 defines `raw_data_block()` as a sequence +//! of *syntactic elements*, each prefixed by a 3-bit `id_syn_ele` +//! identifier (Table 4.71). The element types are: +//! +//! | id (binary) | id (decimal) | name | role | +//! |-------------|--------------|------|--------------------------------------------| +//! | `0b000` | 0 | SCE | single-channel element (mono) | +//! | `0b001` | 1 | CPE | channel-pair element | +//! | `0b010` | 2 | CCE | coupling channel element | +//! | `0b011` | 3 | LFE | low-frequency-effects element | +//! | `0b100` | 4 | DSE | data stream element | +//! | `0b101` | 5 | PCE | program config element | +//! | `0b110` | 6 | FIL | fill element (padding / extension payload) | +//! | `0b111` | 7 | END | block terminator | +//! +//! After the terminating `END`, ISO/IEC 14496-3 §4.4.2.1 requires the +//! decoder to byte-align the bit-reader before the next +//! `raw_data_block()` begins. The walker performs that alignment so +//! the next call after `END` resumes on a fresh byte boundary. +//! +//! ## Phase 1 scope +//! +//! This module is the **syntactic skeleton** — the walker emits an +//! [`Element`] per `id_syn_ele` it encounters and stops at `END`. +//! Per-element bodies are handled as follows: +//! +//! * **SCE / CPE / CCE / LFE**: the walker reads the mandatory 4-bit +//! `element_instance_tag` and then *stops body parsing*. The +//! consumer must advance the [`BitReader`](oxideav_core::bits::BitReader) +//! past the channel-element body itself; Phase 2 will absorb that +//! logic. The emitted [`Element::ChannelElement`] carries the +//! element kind and its tag. +//! * **FIL**: parsed as ISO/IEC 14496-3 §4.4.2.7 — 4-bit +//! `count`, optional 8-bit `esc_count` escape (when `count == 15`, +//! the real byte count is `count + esc_count − 1`), then *count* +//! bytes of `extension_payload` which are skipped without +//! interpretation. The emitted [`Element::Fill`] reports the byte +//! length skipped. +//! * **DSE**: parsed as ISO/IEC 14496-3 §4.4.2.5 — 4-bit +//! `element_instance_tag`, 1-bit `data_byte_align_flag`, 8-bit +//! `count`, optional 8-bit `esc_count`, byte-align (if flag set), +//! then *count* bytes of `data_stream_byte[]`. +//! * **PCE**: parsed via [`crate::pce::Pce::parse`] with an +//! `origin_bit_offset` of `0` (the standalone-in-`raw_data_block` +//! form has no enclosing ASC, so the Table 4.2 `byte_alignment()` +//! resolves to the absolute byte boundary). The walker emits +//! [`Element::ProgramConfig`] carrying the resolved +//! [`crate::pce::Pce`]. +//! * **END**: emits [`Element::End`] and byte-aligns the reader. +//! Subsequent calls return `None`. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::pce::Pce; +use crate::{Error, Result}; + +/// Syntactic element identifier — the 3-bit `id_syn_ele` field +/// defined in ISO/IEC 14496-3 Table 4.71. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum IdSynEle { + /// `0b000` — single-channel element. + Sce = 0, + /// `0b001` — channel-pair element. + Cpe = 1, + /// `0b010` — coupling channel element. + Cce = 2, + /// `0b011` — low-frequency-effects element. + Lfe = 3, + /// `0b100` — data stream element. + Dse = 4, + /// `0b101` — program config element. + Pce = 5, + /// `0b110` — fill element. + Fil = 6, + /// `0b111` — raw-data-block terminator. + End = 7, +} + +impl IdSynEle { + /// Map a 3-bit wire value (0..=7) to the corresponding variant. + pub fn from_bits(bits: u8) -> Self { + match bits & 0b111 { + 0 => IdSynEle::Sce, + 1 => IdSynEle::Cpe, + 2 => IdSynEle::Cce, + 3 => IdSynEle::Lfe, + 4 => IdSynEle::Dse, + 5 => IdSynEle::Pce, + 6 => IdSynEle::Fil, + _ => IdSynEle::End, + } + } + + /// Short upper-case name as used in the spec table and the + /// AAC_TRACE fixture corpus (`SCE`, `CPE`, `CCE`, `LFE`, `DSE`, + /// `PCE`, `FIL`, `END`). + pub fn name(self) -> &'static str { + match self { + IdSynEle::Sce => "SCE", + IdSynEle::Cpe => "CPE", + IdSynEle::Cce => "CCE", + IdSynEle::Lfe => "LFE", + IdSynEle::Dse => "DSE", + IdSynEle::Pce => "PCE", + IdSynEle::Fil => "FIL", + IdSynEle::End => "END", + } + } +} + +/// An event emitted by [`Walker::next_element`]. +/// +/// The walker emits exactly one event per `id_syn_ele` it consumes +/// and stops at `END`. For non-`End` events the bit-reader position +/// after the call reflects the bytes the walker itself consumed +/// (header + any per-element bookkeeping it parses); see the +/// per-variant docs for which bytes have been skipped. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Element { + /// SCE / CPE / CCE / LFE — channel element. The walker has + /// consumed the 3-bit `id_syn_ele` and the 4-bit + /// `element_instance_tag`. The channel-element body (`ics_info`, + /// section data, scale factors, spectral data, …) starts at the + /// current bit-reader position and is **not** parsed in Phase 1. + ChannelElement { + /// The channel element variant (`Sce`, `Cpe`, `Cce`, or + /// `Lfe`). + kind: IdSynEle, + /// The 4-bit `element_instance_tag` read from the wire. + element_instance_tag: u8, + }, + /// FIL — fill element. The walker has consumed the 3-bit + /// `id_syn_ele`, the 4-bit `count`, the optional 8-bit + /// `esc_count`, and the resulting *count* `extension_payload` + /// bytes. + Fill { + /// Total `extension_payload` bytes skipped (`count` after + /// optional escape expansion). + payload_bytes: u32, + }, + /// DSE — data stream element. The walker has consumed the + /// header (3-bit `id_syn_ele`, 4-bit `element_instance_tag`, + /// 1-bit `data_byte_align_flag`, 8-bit `count`, optional 8-bit + /// `esc_count`, optional byte-align) and the resulting *count* + /// `data_stream_byte[]` values. + Data { + /// The 4-bit `element_instance_tag` read from the wire. + element_instance_tag: u8, + /// `true` ⇔ a `data_byte_align_flag == 1` was processed and + /// the bit-reader was byte-aligned before the payload. + byte_align_flag: bool, + /// Total `data_stream_byte[]` bytes skipped (`count` after + /// optional escape expansion). + payload_bytes: u32, + }, + /// PCE — program config element. The walker has consumed the + /// 3-bit `id_syn_ele` and the entire PCE body per + /// [`Pce::parse`] (`origin_bit_offset = 0` — see + /// [`crate::pce`] for the standalone vs ASC-embedded handling + /// of the trailing `byte_alignment()`). + ProgramConfig(Pce), + /// END (`0b111`) — the raw-data-block terminator. The walker + /// has consumed the 3-bit `id_syn_ele` and byte-aligned the + /// bit-reader (ISO/IEC 14496-3 §4.4.2.1). + End, +} + +/// Walker over a `raw_data_block()` payload. +/// +/// Drive the walker by calling [`Walker::next_element`] in a loop +/// until it returns either an [`Element::End`] event or `None` +/// (input exhausted before reaching `END`). See the [module +/// docs](self) for the per-element body-skipping rules and what +/// the walker currently does not parse. +pub struct Walker<'a, 'b> { + reader: &'b mut BitReader<'a>, + finished: bool, +} + +impl<'a, 'b> core::fmt::Debug for Walker<'a, 'b> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Walker") + .field("finished", &self.finished) + .field("bit_position", &self.reader.bit_position()) + .finish() + } +} + +impl<'a, 'b> Walker<'a, 'b> { + /// Bind a walker to an existing [`BitReader`] positioned at the + /// first byte of a `raw_data_block()` payload. + pub fn new(reader: &'b mut BitReader<'a>) -> Self { + Self { + reader, + finished: false, + } + } + + /// Read the next syntactic element. Returns `Ok(Some(_))` for + /// every non-terminating element, `Ok(Some(Element::End))` once + /// (and the walker becomes `finished`), and `Ok(None)` for any + /// further calls after `End`. + /// + /// Errors out with [`Error::UnsupportedElementSkip`] when the + /// next `id_syn_ele` would require body parsing Phase 1 has + /// not landed yet. As of this round, PCE is fully parsed + /// (round 126) and FIL / DSE are skipped (round 121); only the + /// channel-element bodies (SCE/CPE/CCE/LFE) remain deferred, + /// and even those return [`Element::ChannelElement`] for the + /// header — the caller must advance the bit-reader past the + /// body itself if more than one element is needed in a single + /// `raw_data_block()`. + pub fn next_element(&mut self) -> Result> { + self.next_element_impl(true) + } + + /// [`Self::next_element`], except a FIL element's + /// `extension_payload` body is **left unconsumed**: the returned + /// [`Element::Fill`] reports the byte count and the bit-reader + /// stays at the first extension-payload bit, so the caller can + /// parse the Table 4.51 `extension_payload()` chain itself (e.g. + /// to route an `EXT_SBR_DATA` payload into the SBR decoder). The + /// caller **must** consume exactly `payload_bytes` bytes worth of + /// bits before the next call. + pub fn next_element_keep_fill(&mut self) -> Result> { + self.next_element_impl(false) + } + + fn next_element_impl(&mut self, consume_fill: bool) -> Result> { + if self.finished { + return Ok(None); + } + + let id_bits = self.reader.read_u32(3).map_err(|_| Error::UnexpectedEnd)? as u8; + let id = IdSynEle::from_bits(id_bits); + + match id { + IdSynEle::Sce | IdSynEle::Cpe | IdSynEle::Cce | IdSynEle::Lfe => { + let element_instance_tag = + self.reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; + Ok(Some(Element::ChannelElement { + kind: id, + element_instance_tag, + })) + } + IdSynEle::Fil => { + let payload_bytes = self.read_fill_count()?; + if consume_fill { + self.skip_bytes(payload_bytes)?; + } + Ok(Some(Element::Fill { payload_bytes })) + } + IdSynEle::Dse => { + let element_instance_tag = + self.reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; + let byte_align_flag = self.reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let payload_bytes = self.read_data_count()?; + if byte_align_flag { + self.reader.align_to_byte(); + } + self.skip_bytes(payload_bytes)?; + Ok(Some(Element::Data { + element_instance_tag, + byte_align_flag, + payload_bytes, + })) + } + IdSynEle::Pce => { + // Standalone PCE inside a raw_data_block: align the + // PCE's byte_alignment() to the absolute byte + // boundary (origin_bit_offset == 0). The ASC-inline + // variant uses the surrounding ASC origin instead. + let pce = Pce::parse(self.reader, 0)?; + Ok(Some(Element::ProgramConfig(pce))) + } + IdSynEle::End => { + self.reader.align_to_byte(); + self.finished = true; + Ok(Some(Element::End)) + } + } + } + + /// `true` once an [`Element::End`] event has been returned. + pub fn is_finished(&self) -> bool { + self.finished + } + + /// Fill-element byte-count read per ISO/IEC 14496-3 §4.4.2.7. + /// 4-bit `count`; if `count == 15`, an 8-bit `esc_count` follows + /// and the resulting count is `count + esc_count − 1`. + fn read_fill_count(&mut self) -> Result { + let count = self.reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)?; + if count == 15 { + let esc = self.reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)?; + // §4.4.2.7: `cnt = esc_count + 15 - 1`. + Ok(esc + 15 - 1) + } else { + Ok(count) + } + } + + /// Data-stream-element byte-count read per ISO/IEC 14496-3 + /// §4.4.2.5. 8-bit `count`; if `count == 255`, an 8-bit + /// `esc_count` follows and the resulting count is + /// `count + esc_count`. + fn read_data_count(&mut self) -> Result { + let count = self.reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)?; + if count == 255 { + let esc = self.reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)?; + Ok(count + esc) + } else { + Ok(count) + } + } + + /// Skip `n` whole bytes via the bit-reader. + fn skip_bytes(&mut self, n: u32) -> Result<()> { + // Multiplication is safe within u32 because §4.4.2.5 caps + // `count` at 2 × 255 = 510 and §4.4.2.7 caps `cnt` at + // 15 + 255 − 1 = 269, well below `u32::MAX / 8`. + let bits = n.saturating_mul(8); + self.reader.skip(bits).map_err(|_| Error::UnexpectedEnd) + } +} + +// =================================================================== +// raw_data_block() frame assembler — encoder primitive +// =================================================================== +// +// Round 160 lands the symmetric encoder side: a [`FrameAssembler`] +// that composes the existing typed writers into a complete +// `raw_data_block()` byte stream per ISO/IEC 14496-3 §4.4.2.1, the +// inverse of [`Walker`]. The assembler accepts: +// +// * [`FrameAssembler::push_channel_header`] — emits the 3-bit +// `id_syn_ele` (`SCE` / `CPE` / `CCE` / `LFE`) + 4-bit +// `element_instance_tag`. The channel-element *body* +// (`ics_info` → `section_data` → `scale_factor_data` → optional +// `pulse_data` / `tns_data` / `gain_control_data` → `spectral_data`) +// is not internalised yet; the caller is responsible for serialising +// it via the existing per-tool writers (`IcsInfo::write`, +// `SectionData::write`, `ScaleFactorData::write`, `PulseData::write`, +// `TnsData::write`, …). [`FrameAssembler::push_channel_body_bits`] +// appends a pre-serialised body as a bit-slice immediately after a +// channel header. +// +// * [`FrameAssembler::push_fill`] — emits a FIL element per §4.4.2.7, +// including the 8-bit `esc_count` escape when `payload_bytes >= 15` +// (resulting wire `count = 15` + `esc_count = payload_bytes - 15 + 1` +// — the inverse of `read_fill_count`'s `cnt = esc_count + 15 - 1`). +// +// * [`FrameAssembler::push_data`] — emits a DSE element per §4.4.2.5, +// honouring `data_byte_align_flag` (which, when set, byte-aligns +// *before* the payload bytes per §4.4.2.5) and the 8-bit `esc_count` +// escape when `payload_bytes >= 255` (resulting wire `count = 255` + +// `esc_count = payload_bytes - 255` — the inverse of +// `read_data_count`'s `cnt = count + esc_count`). +// +// * [`FrameAssembler::push_end`] — emits the 3-bit `END` terminator +// and byte-aligns to the next byte boundary per §4.4.2.1. +// +// PCE encoding is deferred — [`Pce`] has no `write` primitive yet, and +// adding one is a separate round's worth of work (Tables 4.4 / 4.5 +// front/side/back/lfe element selects, mono / stereo / matrix +// mix-down hints, comment field, plus the relative-origin +// `byte_alignment()` per Table 4.2 Note 1). +// +// The §4.4.2.1 normative constraint that exactly one `END` element +// terminates the block (and that no further elements may follow) is +// enforced by the type-state: [`FrameAssembler::push_end`] consumes +// `self` and returns the finished [`Vec`] (calling any other +// `push_*` after END is a compile-time error). + +/// Encoder-side frame assembler for `raw_data_block()` per ISO/IEC +/// 14496-3 §4.4.2.1 — the bit-exact inverse of [`Walker`]. +/// +/// Construct via [`FrameAssembler::new`] or +/// [`FrameAssembler::with_capacity`], push elements with the +/// `push_*` family in wire order, then finish with +/// [`FrameAssembler::push_end`] which consumes the assembler and +/// returns the byte-aligned frame. END is mandatory — dropping a +/// non-finished assembler discards the in-progress frame. +/// +/// ## Composition with the existing typed writers +/// +/// Channel-element *headers* are emitted by +/// [`FrameAssembler::push_channel_header`]. The channel-element +/// *body* — `ics_info` → `section_data` → `scale_factor_data` → +/// optional `pulse_data` / `tns_data` / `gain_control_data` → +/// `spectral_data` — has no single round-160 writer. Callers +/// serialise the body separately via the existing tool writers +/// ([`crate::ics_info::IcsInfo::write`], +/// [`crate::section_data::SectionData::write`], +/// [`crate::scale_factor_data::ScaleFactorData::write`], +/// [`crate::pulse_data::PulseData::write`], +/// [`crate::tns_data::TnsData::write`]) into an auxiliary +/// [`BitWriter`] and append the resulting bits to the frame via +/// [`FrameAssembler::push_channel_body_bits`]. This keeps the +/// frame-level concern (element ordering + sync + alignment + +/// fill/data escapes + END) separate from the channel-element-level +/// concern (per-tool bit layouts), which round 160 already covers +/// for everything except `gain_control_data` / `spectral_data`. +/// +/// ## Why this is `Phase 2`, not `Phase 1` +/// +/// The Phase 1 [`Walker`] *consumes* a `raw_data_block()` byte slice +/// produced by an external encoder (typically extracted from an ADTS +/// frame or an MP4 audio sample). Phase 2 adds the inverse — the +/// assembler that *produces* the byte slice that the Phase 1 walker +/// can read back. Together they form a complete §4.4.2.1 +/// parse / write cycle for every element type with a bit-exact +/// inverse already in the crate (channel headers, FIL, DSE, END). +pub struct FrameAssembler { + writer: BitWriter, +} + +impl core::fmt::Debug for FrameAssembler { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FrameAssembler") + .field("bit_position", &self.writer.bit_position()) + .finish() + } +} + +impl Default for FrameAssembler { + fn default() -> Self { + Self::new() + } +} + +impl FrameAssembler { + /// Start a new, empty `raw_data_block()` assembler. + pub fn new() -> Self { + Self { + writer: BitWriter::new(), + } + } + + /// Start a new assembler whose underlying byte buffer is + /// pre-reserved for at least `cap` bytes. + pub fn with_capacity(cap: usize) -> Self { + Self { + writer: BitWriter::with_capacity(cap), + } + } + + /// Current bit position (relative to the start of the frame). + /// Useful for sizing channel-element bodies. + pub fn bit_position(&self) -> u64 { + self.writer.bit_position() + } + + /// Emit a channel-element header — the 3-bit `id_syn_ele` (one of + /// `SCE` / `CPE` / `CCE` / `LFE`) followed by the 4-bit + /// `element_instance_tag` per ISO/IEC 14496-3 §4.4.2.1. + /// + /// The channel-element body itself is the caller's responsibility + /// — see [`FrameAssembler::push_channel_body_bits`] for the + /// post-header append. + /// + /// Returns [`Error::RawDataBlockEncodeInvalid`] when: + /// + /// * `kind` is not one of `SCE` / `CPE` / `CCE` / `LFE` (this + /// helper is for channel elements only — use + /// [`FrameAssembler::push_fill`] / [`FrameAssembler::push_data`] + /// / [`FrameAssembler::push_end`] for the other element types, + /// each of which has its own bespoke wire layout). + /// * `element_instance_tag > 0x0f` (4-bit field overflow). + pub fn push_channel_header(&mut self, kind: IdSynEle, element_instance_tag: u8) -> Result<()> { + match kind { + IdSynEle::Sce | IdSynEle::Cpe | IdSynEle::Cce | IdSynEle::Lfe => {} + _ => return Err(Error::RawDataBlockEncodeInvalid), + } + if element_instance_tag > 0x0f { + return Err(Error::RawDataBlockEncodeInvalid); + } + self.writer.write_u32(kind as u32, 3); + self.writer.write_u32(element_instance_tag as u32, 4); + Ok(()) + } + + /// Append `bit_count` raw bits from `bits` (read MSB-first) to + /// the frame — the channel-element body that follows a + /// [`FrameAssembler::push_channel_header`]. + /// + /// `bits` is interpreted as an MSB-first packed bit-buffer (the + /// same byte layout [`BitWriter::finish`] / [`BitReader::new`] + /// already use throughout the crate). The low `(8 - bit_count % + /// 8) % 8` bits of the last byte are not consumed and may carry + /// arbitrary content. + /// + /// Returns [`Error::RawDataBlockEncodeInvalid`] when `bit_count` + /// exceeds `bits.len() * 8`. + pub fn push_channel_body_bits(&mut self, bits: &[u8], bit_count: u64) -> Result<()> { + if bit_count > (bits.len() as u64).saturating_mul(8) { + return Err(Error::RawDataBlockEncodeInvalid); + } + let mut remaining = bit_count; + let mut byte_idx = 0usize; + // Whole bytes first. + while remaining >= 8 { + self.writer.write_byte(bits[byte_idx]); + byte_idx += 1; + remaining -= 8; + } + // Trailing partial byte: take the high `remaining` bits of + // the next source byte. + if remaining > 0 { + let last = bits[byte_idx]; + let high = (last as u32) >> (8 - remaining); + self.writer.write_u32(high, remaining as u32); + } + Ok(()) + } + + /// Emit a FIL element per ISO/IEC 14496-3 §4.4.2.7 — the 3-bit + /// `id_syn_ele` (`0b110`), the 4-bit `count`, the optional 8-bit + /// `esc_count` escape (when `payload_bytes >= 15`), then the + /// `payload_bytes` of `extension_payload`. + /// + /// Escape arithmetic: the parser's `cnt = esc_count + 15 - 1` + /// (see [`Walker::read_fill_count`]) inverts to `esc_count = + /// payload_bytes - 15 + 1 = payload_bytes - 14`, so the largest + /// representable payload is `15 + 255 - 1 = 269` bytes. Larger + /// fill payloads must be split across multiple FIL elements (as + /// AAC's bit-reservoir code path does in practice for long fill + /// runs). + /// + /// Returns [`Error::RawDataBlockEncodeInvalid`] when: + /// + /// * `payload.len() > 269` (Table 4.57 + escape arithmetic + /// ceiling), or + /// * `payload.len()` exceeds the `bit_count` capacity of the + /// surrounding writer (in practice `u32::MAX`). + pub fn push_fill(&mut self, payload: &[u8]) -> Result<()> { + let n = payload.len(); + if n > 269 { + return Err(Error::RawDataBlockEncodeInvalid); + } + self.writer.write_u32(IdSynEle::Fil as u32, 3); + if n < 15 { + self.writer.write_u32(n as u32, 4); + } else { + self.writer.write_u32(15, 4); + // §4.4.2.7: parser reconstructs `cnt = esc_count + 15 - + // 1`. The inverse, given `cnt == n`, is + // `esc_count = n - 15 + 1 = n - 14`. The 8-bit + // `esc_count` field caps `n` at `15 + 255 - 1 = 269`, + // which we already rejected above when violated. + let esc = (n as u32) - 14; + self.writer.write_u32(esc, 8); + } + // Per §4.4.2.7 the payload is *not* required to be + // byte-aligned — `extension_payload()` is itself a + // bit-level item — but the walker treats it as `count` + // whole bytes, mirroring how every conforming encoder we + // care about ever emits it. The assembler therefore writes + // the payload as bytes too. + for &b in payload { + self.writer.write_byte(b); + } + Ok(()) + } + + /// Emit a DSE element per ISO/IEC 14496-3 §4.4.2.5 — the 3-bit + /// `id_syn_ele` (`0b100`), the 4-bit `element_instance_tag`, the + /// 1-bit `data_byte_align_flag`, the 8-bit `count`, the optional + /// 8-bit `esc_count` escape (when `payload_bytes >= 255`), + /// optionally byte-align (if the flag was set), then the + /// `payload_bytes` of `data_stream_byte[]`. + /// + /// Escape arithmetic: the parser's `cnt = count + esc_count` (see + /// [`Walker::read_data_count`]) inverts to `esc_count = + /// payload_bytes - 255`, so the largest representable payload is + /// `255 + 255 = 510` bytes. Larger data payloads must be split + /// across multiple DSE elements with the same `tag`. + /// + /// Returns [`Error::RawDataBlockEncodeInvalid`] when: + /// + /// * `element_instance_tag > 0x0f` (4-bit field overflow), or + /// * `payload.len() > 510` (the escape arithmetic ceiling above). + pub fn push_data( + &mut self, + element_instance_tag: u8, + byte_align_flag: bool, + payload: &[u8], + ) -> Result<()> { + if element_instance_tag > 0x0f { + return Err(Error::RawDataBlockEncodeInvalid); + } + let n = payload.len(); + if n > 510 { + return Err(Error::RawDataBlockEncodeInvalid); + } + self.writer.write_u32(IdSynEle::Dse as u32, 3); + self.writer.write_u32(element_instance_tag as u32, 4); + self.writer.write_bit(byte_align_flag); + if n < 255 { + self.writer.write_u32(n as u32, 8); + } else { + // §4.4.2.5: parser reconstructs `cnt = count + + // esc_count`. The inverse, given `cnt == n` and the + // escape trigger `count == 255`, is `esc_count = n - + // 255`. The 8-bit `esc_count` field caps `n` at + // `255 + 255 = 510`, which we already rejected above + // when violated. + self.writer.write_u32(255, 8); + let esc = (n as u32) - 255; + self.writer.write_u32(esc, 8); + } + if byte_align_flag { + self.writer.align_to_byte(); + } + for &b in payload { + self.writer.write_byte(b); + } + Ok(()) + } + + /// Emit a PCE element per ISO/IEC 14496-3 §4.4.1.1 / Table 4.2 + /// — the 3-bit `id_syn_ele` (`0b101`) followed by the full + /// `program_config_element()` body produced by [`Pce::write`]. + /// + /// The Table 4.2 Note 1 `byte_alignment()` call inside the PCE + /// body is *relative to the start of the PCE body* (i.e. the bit + /// position immediately after the 3-bit `id_syn_ele`). For the + /// standalone-in-`raw_data_block()` form the PCE-relative origin + /// is the parser's `origin_bit_offset = 0` (see [`Pce::parse`]) + /// — since [`Pce::write`] reproduces that exact arithmetic, this + /// helper simply passes `0` and the writer's own + /// `bit_position` becomes the alignment reference. Bit-exact + /// inverse of [`Walker::next_element`]'s + /// [`Element::ProgramConfig`] branch. + /// + /// Returns [`Error::PceEncodeInvalid`] propagated from + /// [`Pce::write`] when any wire field overflows its bit-width. + pub fn push_pce(&mut self, pce: &Pce) -> Result<()> { + self.writer.write_u32(IdSynEle::Pce as u32, 3); + // §4.4.1.1 Note 1: the Table 4.2 byte_alignment() is + // measured from the start of the PCE body, which is the + // current writer position *after* the id_syn_ele prefix. + // The Phase 1 standalone-in-raw_data_block parser hands + // origin_bit_offset = 0 to `Pce::parse`, which collapses to + // absolute byte alignment of the reader. The writer mirrors + // that exact collapse by passing 0 here — the alignment + // pad inside `Pce::write` will then align to the next + // absolute byte boundary of the underlying BitWriter. + pce.write(&mut self.writer, 0) + } + + /// Emit the terminating `END` element per ISO/IEC 14496-3 + /// §4.4.2.1 — the 3-bit `id_syn_ele` (`0b111`), then a pad-to- + /// byte-boundary that the [`Walker`] mirrors via + /// [`BitReader::align_to_byte`]. Consumes the assembler and + /// returns the finished byte buffer; the final byte is always + /// fully populated. + pub fn push_end(mut self) -> Vec { + self.writer.write_u32(IdSynEle::End as u32, 3); + self.writer.align_to_byte(); + self.writer.finish() + } +} diff --git a/crates/vendor/oxideav-aac/src/rvlc.rs b/crates/vendor/oxideav-aac/src/rvlc.rs new file mode 100644 index 00000000..a881d5d1 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/rvlc.rs @@ -0,0 +1,407 @@ +//! Reversible Variable Length Coding (RVLC) codebooks — ISO/IEC +//! 14496-3 §4.6.16.2 (error-resilient AAC scalefactor coding). +//! +//! RVLC is the error-resilient plug-in replacement for the §4.6.3 +//! noiseless coding of scalefactors. Instead of the Table 4.A.1 +//! Huffman codebook (codebook 12, indices `0..=120`, DPCM range +//! `-60..=+60`), the error-resilient `scale_factor_data()` branch +//! (Table 4.53) codes the scalefactor / intensity-position / +//! noise-energy DPCM deltas with the **RVLC codebook** (Table 4.166) +//! — a small *symmetric* (palindromic) prefix code covering only the +//! deltas `-7..=+7`. The value `±7` is the `ESC_FLAG`: it signals +//! that an escape magnitude (Huffman-coded with the separate RVLC-ESC +//! codebook, Table 4.168) is to be *added to +7* (positive ESC) or +//! *subtracted from -7* (negative ESC) to recover the true delta. +//! +//! Two properties make RVLC error-resilient, and both are exercised +//! by this module: +//! +//! 1. **Symmetry / reversibility.** Every Table 4.166 codeword is a +//! bit-palindrome, so the same codebook decodes a stream forwards +//! *and* backwards. The encoder transmits `rev_global_gain` (the +//! last scalefactor) and `length_of_rvlc_sf` (the bit length of +//! the RVLC part) so a decoder that hits a bit error mid-stream +//! can restart from the far end. This module provides the forward +//! primitive — the §4.6.2.3.2 note that "the decoding process of +//! the RVLC words is the same as for the Huffman codewords" +//! means a clean stream decodes identically forwards, so the +//! backward path is a recovery-only concern handled at the +//! `scale_factor_data` driver level. +//! 2. **Sparse code → error detection.** The 4-bit-deep code tree +//! has unused (asymmetric) leaves: Table 4.167 lists eight +//! *forbidden* codewords that a conforming encoder never emits. +//! Hitting one signals a bit error. [`rvlc_decode`] surfaces them +//! as [`Error::RvlcForbiddenCodeword`]. +//! +//! ## Provenance / cross-check +//! +//! The two codebooks are transcribed verbatim from the human-readable +//! ISO/IEC 14496-3:2009 normative tables: +//! +//! * [`RVLC_CB`] — Table 4.166 (`index`, `length`, `codeword`). +//! * [`RVLC_FORBIDDEN`] — Table 4.167 (asymmetric / forbidden +//! `length`, `codeword`). +//! * [`RVLC_ESC_CB`] — Table 4.168 (RVLC escape Huffman, 54 entries). +//! +//! Each was independently cross-validated against the packed +//! binary-tree node tables staged under +//! `docs/audio/aac/tables/rvlc-codewords-huff-tree.csv` and +//! `rvlc-escape-huff-tree.csv`: decoding the tree recovers exactly +//! the 15 Table 4.166 codewords (leaf − 7 == index) plus the 8 +//! Table 4.167 forbidden leaves, confirming both transcriptions. + +use oxideav_core::bits::BitReader; + +use crate::{Error, Result}; + +// ============================================================================= +// Table 4.166 — RVLC codebook +// ============================================================================= + +/// The `ESC_FLAG` magnitude. A decoded RVLC delta of `±7` does not +/// stand for the literal value `±7` when escapes are present; it +/// flags that an escape magnitude follows (§4.6.16.2.1). +pub const RVLC_ESC_FLAG: i8 = 7; + +/// Number of entries in the Table 4.166 RVLC codebook (deltas +/// `-7..=+7`, 15 entries). +pub const RVLC_CB_NUM_ENTRIES: usize = 15; + +/// Maximum Table 4.166 codeword length (9 bits — the `±6` codewords). +pub const RVLC_CB_MAX_LEN: u32 = 9; + +/// Table 4.166 — `(value, length_in_bits, codeword)` for the RVLC +/// codebook. `value` is the signed DPCM delta in `-7..=+7`; +/// `codeword` is right-aligned within the `u32` (MSB at bit +/// `length - 1`). Every codeword is a bit-palindrome (the symmetry +/// property §4.6.16.2.1 relies on). +const RVLC_CB: [(i8, u8, u32); RVLC_CB_NUM_ENTRIES] = [ + (-7, 7, 65), // 1000001 + (-6, 9, 257), // 100000001 + (-5, 8, 129), // 10000001 + (-4, 6, 33), // 100001 + (-3, 5, 17), // 10001 + (-2, 4, 9), // 1001 + (-1, 3, 5), // 101 + (0, 1, 0), // 0 + (1, 3, 7), // 111 + (2, 5, 27), // 11011 + (3, 6, 51), // 110011 + (4, 7, 107), // 1101011 + (5, 8, 195), // 11000011 + (6, 9, 427), // 110101011 + (7, 7, 99), // 1100011 +]; + +/// Table 4.167 — the eight *asymmetric* (forbidden) codewords as +/// `(length_in_bits, codeword)`. A conforming encoder never emits +/// these; a decode that lands on one signals a bit error +/// (§4.6.16.2.1 "some error detection is possible … because not all +/// nodes of the coding tree are used as codewords"). +const RVLC_FORBIDDEN: [(u8, u32); 8] = [ + (6, 50), // 110010 + (7, 96), // 1100000 + (9, 256), // 100000000 + (8, 194), // 11000010 + (7, 98), // 1100010 + (6, 52), // 110100 + (9, 426), // 110101010 + (8, 212), // 11010100 +]; + +/// Encode a signed RVLC delta in `-7..=+7` to its Table 4.166 +/// codeword. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u32` (MSB at bit `length - 1`). Out-of-range `value` +/// produces [`Error::RvlcEncodeInvalid`]. +/// +/// The inverse of [`rvlc_decode`]. +pub fn rvlc_encode(value: i8) -> Result<(u8, u32)> { + for &(v, len, cw) in &RVLC_CB { + if v == value { + return Ok((len, cw)); + } + } + Err(Error::RvlcEncodeInvalid) +} + +/// Decode one Table 4.166 RVLC codeword from `reader`, returning the +/// signed delta in `-7..=+7`. +/// +/// Read MSB-first one bit at a time and prefix-match against the +/// codebook. If the accumulated bit pattern matches one of the +/// Table 4.167 forbidden codewords, return +/// [`Error::RvlcForbiddenCodeword`] (an error-detection event, not a +/// reader fault). Returns [`Error::UnexpectedEnd`] on reader +/// underflow. +/// +/// A delta of `±7` is the `ESC_FLAG` (see [`RVLC_ESC_FLAG`]); the +/// caller decides whether an escape magnitude follows based on the +/// stream's `sf_escapes_present` flag. +pub fn rvlc_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=RVLC_CB_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for &(value, entry_len, entry_cw) in &RVLC_CB { + if u32::from(entry_len) == len && entry_cw == acc { + return Ok(value); + } + } + for &(f_len, f_cw) in &RVLC_FORBIDDEN { + if u32::from(f_len) == len && f_cw == acc { + return Err(Error::RvlcForbiddenCodeword); + } + } + } + // Every 9-bit prefix is either a valid codeword, a forbidden + // codeword, or a prefix of one of those; the RVLC tree is fully + // populated to depth 9, so a 9-bit walk always terminates in one + // of the two arms above. The guard keeps the return type `!`-free. + Err(Error::RvlcForbiddenCodeword) +} + +// ============================================================================= +// Table 4.168 — RVLC escape Huffman codebook +// ============================================================================= + +/// Number of entries in the Table 4.168 RVLC-ESC Huffman codebook +/// (54 entries, indices `0..=53`). +pub const RVLC_ESC_NUM_ENTRIES: usize = 54; + +/// Maximum Table 4.168 codeword length (20 bits). +pub const RVLC_ESC_MAX_LEN: u32 = 20; + +/// Table 4.168 — `(length_in_bits, codeword)` per escape index +/// `0..=53`. `codeword` is right-aligned in the `u32`. +/// +/// The escape *index* is the magnitude added to the `ESC_FLAG`: a +/// positive escape recovers `+7 + index`, a negative escape recovers +/// `-7 - index` (§4.6.16.2.1). Indices `0` and `1` (the two +/// shortest, 2-bit codewords) correspond to magnitudes 0 and 1. +const RVLC_ESC_CB: [(u8, u32); RVLC_ESC_NUM_ENTRIES] = [ + (2, 2), // 0 + (2, 0), // 1 + (3, 6), // 2 + (3, 2), // 3 + (4, 14), // 4 + (5, 31), // 5 + (5, 15), // 6 + (5, 13), // 7 + (6, 61), // 8 + (6, 29), // 9 + (6, 25), // 10 + (6, 24), // 11 + (7, 120), // 12 + (7, 56), // 13 + (8, 242), // 14 + (8, 114), // 15 + (9, 486), // 16 + (9, 230), // 17 + (10, 974), // 18 + (10, 463), // 19 + (11, 1950), // 20 + (11, 1951), // 21 + (11, 925), // 22 + (12, 1848), // 23 + (14, 7399), // 24 + (13, 3698), // 25 + (15, 14797), // 26 + (20, 473482), // 27 + (20, 473483), // 28 + (20, 473484), // 29 + (20, 473485), // 30 + (20, 473486), // 31 + (20, 473487), // 32 + (20, 473488), // 33 + (20, 473489), // 34 + (20, 473490), // 35 + (20, 473491), // 36 + (20, 473492), // 37 + (20, 473493), // 38 + (20, 473494), // 39 + (20, 473495), // 40 + (20, 473496), // 41 + (20, 473497), // 42 + (20, 473498), // 43 + (20, 473499), // 44 + (20, 473500), // 45 + (20, 473501), // 46 + (20, 473502), // 47 + (20, 473503), // 48 + (19, 236736), // 49 + (19, 236737), // 50 + (19, 236738), // 51 + (19, 236739), // 52 + (19, 236740), // 53 +]; + +/// Encode an RVLC escape magnitude (`0..=53`) to its Table 4.168 +/// Huffman codeword. +/// +/// Returns `(length_in_bits, codeword)` right-aligned in the `u32`. +/// An out-of-range magnitude produces [`Error::RvlcEncodeInvalid`]. +/// +/// The inverse of [`rvlc_esc_decode`]. +pub fn rvlc_esc_encode(magnitude: u8) -> Result<(u8, u32)> { + RVLC_ESC_CB + .get(magnitude as usize) + .copied() + .ok_or(Error::RvlcEncodeInvalid) +} + +/// Decode one Table 4.168 RVLC-ESC Huffman codeword from `reader`, +/// returning the escape magnitude index `0..=53`. +/// +/// Read MSB-first one bit at a time and prefix-match. Returns +/// [`Error::UnexpectedEnd`] on reader underflow and +/// [`Error::RvlcEscInvalid`] if the 20-bit walk matches no entry +/// (a bit error inside the escape part). +pub fn rvlc_esc_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=RVLC_ESC_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in RVLC_ESC_CB.iter().enumerate() { + if u32::from(entry_len) == len && entry_cw == acc { + return Ok(idx as u8); + } + } + } + Err(Error::RvlcEscInvalid) +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitWriter; + + fn encode_rvlc(value: i8) -> Vec { + let (len, cw) = rvlc_encode(value).unwrap(); + let mut w = BitWriter::new(); + w.write_u32(cw, u32::from(len)); + // Pad to a byte so the BitReader has whole bytes to read. + w.align_to_byte_zero(); + w.finish() + } + + #[test] + fn rvlc_codebook_roundtrips_every_value() { + for value in -7..=7 { + let bytes = encode_rvlc(value); + let mut r = BitReader::new(&bytes); + assert_eq!(rvlc_decode(&mut r).unwrap(), value, "value {value}"); + } + } + + #[test] + fn rvlc_codewords_are_palindromes() { + // The §4.6.16.2.1 symmetry property: every codeword reads the + // same forwards and backwards. This is what enables backward + // decoding of the RVLC part. + for &(value, len, cw) in &RVLC_CB { + let mut forward = 0u32; + for i in 0..len { + let bit = (cw >> i) & 1; + forward = (forward << 1) | bit; + } + assert_eq!(forward, cw, "value {value} codeword not a palindrome"); + } + } + + #[test] + fn rvlc_codebook_is_prefix_free() { + for &(_, li, ci) in &RVLC_CB { + for &(_, lj, cj) in &RVLC_CB { + if (li, ci) == (lj, cj) { + continue; + } + // ci is a prefix of cj iff the high `li` bits of cj + // (a `lj`-bit codeword) equal ci. + if li <= lj { + let shifted = cj >> (lj - li); + assert_ne!(shifted, ci, "({li},{ci}) is a prefix of ({lj},{cj})"); + } + } + } + } + + #[test] + fn forbidden_codewords_are_detected() { + for &(len, cw) in &RVLC_FORBIDDEN { + let mut w = BitWriter::new(); + w.write_u32(cw, u32::from(len)); + w.align_to_byte_zero(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!( + matches!(rvlc_decode(&mut r), Err(Error::RvlcForbiddenCodeword)), + "forbidden codeword ({len},{cw}) not detected" + ); + } + } + + #[test] + fn forbidden_codewords_disjoint_from_valid() { + for &(fl, fc) in &RVLC_FORBIDDEN { + for &(_, vl, vc) in &RVLC_CB { + assert!( + !(fl == vl && fc == vc), + "forbidden ({fl},{fc}) collides with a valid codeword" + ); + } + } + } + + #[test] + fn rvlc_esc_codebook_roundtrips_every_magnitude() { + for magnitude in 0u8..RVLC_ESC_NUM_ENTRIES as u8 { + let (len, cw) = rvlc_esc_encode(magnitude).unwrap(); + let mut w = BitWriter::new(); + w.write_u32(cw, u32::from(len)); + w.align_to_byte_zero(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert_eq!( + rvlc_esc_decode(&mut r).unwrap(), + magnitude, + "mag {magnitude}" + ); + } + } + + #[test] + fn rvlc_esc_codebook_is_prefix_free() { + for &(li, ci) in &RVLC_ESC_CB { + for &(lj, cj) in &RVLC_ESC_CB { + if (li, ci) == (lj, cj) { + continue; + } + if li <= lj { + let shifted = cj >> (lj - li); + assert_ne!(shifted, ci, "esc ({li},{ci}) is a prefix of ({lj},{cj})"); + } + } + } + } + + #[test] + fn rvlc_encode_rejects_out_of_range() { + assert!(matches!(rvlc_encode(8), Err(Error::RvlcEncodeInvalid))); + assert!(matches!(rvlc_encode(-8), Err(Error::RvlcEncodeInvalid))); + assert!(matches!( + rvlc_esc_encode(RVLC_ESC_NUM_ENTRIES as u8), + Err(Error::RvlcEncodeInvalid) + )); + } + + #[test] + fn esc_flag_is_seven() { + // Table 4.166 maps +7 and -7 to the shortest of the + // extreme magnitudes; the ESC_FLAG constant must agree. + assert_eq!(RVLC_ESC_FLAG, 7); + assert!(rvlc_encode(RVLC_ESC_FLAG).is_ok()); + assert!(rvlc_encode(-RVLC_ESC_FLAG).is_ok()); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_decoder.rs b/crates/vendor/oxideav-aac/src/sbr_decoder.rs new file mode 100644 index 00000000..3c6db4b1 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_decoder.rs @@ -0,0 +1,1111 @@ +//! SBR frame driver — ISO/IEC 14496-3 §4.6.18.5 "SBR tool overview". +//! +//! Composes the whole SBR back-end for one channel element (SCE or +//! CPE): the §4.6.18.4.1 analysis QMF of the core decoder output, the +//! `XLow` buffer with its `tHFGen = 8`-slot cross-frame history, the +//! §4.6.18.6 HF generator, the §4.6.18.7 envelope adjuster, the +//! §4.6.18.5 output matrix `X` assembly (the `lTemp` splice of the +//! previous frame's `Y'` against the current `XLow` / `Y`), and the +//! §4.6.18.4.2 64-band synthesis QMF producing `numTimeSlots·RATE·64 = +//! 2048` output samples per 1024-sample core frame (dual-rate SBR). +//! [`SbrDecoder::set_downsampled`] selects the §4.6.18.4.3 downsampled +//! output mode instead: the 32-channel synthesis bank keeps the output +//! at the core rate (1024 samples per frame), discarding the assembled +//! `X` subbands above the core Nyquist. +//! +//! [`SbrDecoder::process_frame`] drives a parsed +//! [`crate::sbr_extension::SbrExtensionData`]; +//! [`SbrDecoder::upsample_frame`] is the §4.6.18.5 "pure upsampling +//! without SBR processing" path used when a frame carries no SBR +//! payload, keeping the selected output rate and the QMF state +//! continuous. +//! +//! ## Provenance +//! +//! The buffer geometry (`tHFGen = 8`, `tHFAdj = 2`, `lf = +//! numTimeSlots·RATE = 32`), the `XLow` history splice, the `lTemp` +//! output splice, and the reset rules are from the §4.6.18.5 text and +//! Figure 4.47 of the staged spec. No part of this implementation is +//! derived from any external decoder. + +use crate::ps_decoder::PsDecoder; +use crate::ps_hybrid::LOOKAHEAD; +use crate::sbr_dequant::{dequant_coupled, dequant_single, DequantizedSbr}; +use crate::sbr_element::EXTENSION_ID_PS; +use crate::sbr_env_adjust::{adjust, EnvAdjustState, EnvParams}; +use crate::sbr_extension::SbrExtensionData; +use crate::sbr_freq_bands::{k0 as derive_k0, k2 as derive_k2, master_table, HiLoTables}; +use crate::sbr_header::SbrHeader; +use crate::sbr_hf_gen::{ + build_patches, chirp_factors, generate_hf, reflection_coefficient, Patches, T_HF_ADJ, T_HF_GEN, +}; +use crate::sbr_limiter::limiter_table; +use crate::sbr_lp::{aliasing_degree, deg_patched}; +use crate::sbr_qmf::{ + AnalysisQmf, Complex, DownsampledSynthesisQmf, RealAnalysisQmf, RealDownsampledSynthesisQmf, + RealSynthesisQmf, SynthesisQmf, +}; +use crate::sbr_reconstruct::{EnvelopeScalefactors, NoiseScalefactors}; +use crate::sbr_time_grid::derive_time_grid; +use crate::{Error, Result}; + +/// `numTimeSlots` for the 1024-sample core frame (§4.6.18.2.6). +pub const NUM_TIME_SLOTS: i32 = 16; + +/// `RATE = 2` (§4.6.18.2.5). +pub const RATE: i32 = 2; + +/// Slots per frame at the SBR rate (`lf = numTimeSlots · RATE`). +const LF: usize = (NUM_TIME_SLOTS * RATE) as usize; + +/// Total `XLow` / `XHigh` / `Y` columns (`lf + tHFGen`). +const COLS: usize = LF + T_HF_GEN; + +/// The synthesis filterbank of one output channel: the §4.6.18.4.2 +/// 64-band dual-rate bank, or the §4.6.18.4.3 32-channel downsampled +/// bank that keeps the output at the core rate (fed the first 32 +/// subbands of the assembled `X` matrix; the SBR content above the +/// core Nyquist is discarded by construction). +#[derive(Debug)] +enum SynthesisBank { + /// §4.6.18.4.2 — 64 output samples per slot (2× rate). + Dual(SynthesisQmf), + /// §4.6.18.4.3 — 32 output samples per slot (core rate). + Down(DownsampledSynthesisQmf), + /// §4.6.18.8.2.3 — the real-valued low-power dual-rate bank. + RealDual(RealSynthesisQmf), + /// §4.6.18.8.2.4 — the real-valued low-power core-rate bank. + RealDown(RealDownsampledSynthesisQmf), +} + +impl SynthesisBank { + fn new(downsampled: bool, low_power: bool) -> Self { + match (low_power, downsampled) { + (false, false) => SynthesisBank::Dual(SynthesisQmf::new()), + (false, true) => SynthesisBank::Down(DownsampledSynthesisQmf::new()), + (true, false) => SynthesisBank::RealDual(RealSynthesisQmf::new()), + (true, true) => SynthesisBank::RealDown(RealDownsampledSynthesisQmf::new()), + } + } + + /// Output samples per QMF slot (64 dual-rate, 32 downsampled). + fn samples_per_slot(&self) -> usize { + match self { + SynthesisBank::Dual(_) | SynthesisBank::RealDual(_) => 64, + SynthesisBank::Down(_) | SynthesisBank::RealDown(_) => 32, + } + } + + /// Synthesize one assembled `X` column, appending the slot's output + /// samples to `out`. The real (low-power) banks consume the real + /// parts — the LP signal path never populates the imaginary parts. + fn push_slot(&mut self, x: &[Complex; 64], out: &mut Vec) -> Result<()> { + match self { + SynthesisBank::Dual(s) => out.extend_from_slice(&s.push_slot(x)?), + SynthesisBank::Down(s) => out.extend_from_slice(&s.push_slot(&x[..32])?), + SynthesisBank::RealDual(s) => { + let mut re = [0.0f64; 64]; + for (r, c) in re.iter_mut().zip(x.iter()) { + *r = c.re; + } + out.extend_from_slice(&s.push_slot(&re)?); + } + SynthesisBank::RealDown(s) => { + let mut re = [0.0f64; 32]; + for (r, c) in re.iter_mut().zip(x.iter()) { + *r = c.re; + } + out.extend_from_slice(&s.push_slot(&re)?); + } + } + Ok(()) + } +} + +/// The analysis filterbank of one core channel: the §4.6.18.4.1 +/// complex bank, or the §4.6.18.8.2.2 real-valued low-power bank +/// (whose output rides the same `Complex` slots with zero imaginary +/// parts, so the HF generator and adjuster formulas apply unchanged). +#[derive(Debug)] +enum AnalysisBank { + Complex(AnalysisQmf), + Real(RealAnalysisQmf), +} + +impl AnalysisBank { + fn new(low_power: bool) -> Self { + if low_power { + AnalysisBank::Real(RealAnalysisQmf::new()) + } else { + AnalysisBank::Complex(AnalysisQmf::new()) + } + } + + fn push_slot(&mut self, samples: &[f64]) -> Result<[Complex; 32]> { + match self { + AnalysisBank::Complex(a) => a.push_slot(samples), + AnalysisBank::Real(a) => { + let w = a.push_slot(samples)?; + let mut out = [Complex::default(); 32]; + for (o, &r) in out.iter_mut().zip(w.iter()) { + o.re = r; + } + Ok(out) + } + } + } +} + +/// Per-channel cross-frame state. +#[derive(Debug)] +struct ChannelState { + analysis: AnalysisBank, + synthesis: SynthesisBank, + /// The previous frame's last `tHFGen` analysis slots (`W'`). + w_hist: Vec<[Complex; 32]>, + /// The previous frame's `Y` buffer (spec absolute columns). + y_prev: Vec<[Complex; 64]>, + /// `tE'(LE')` — the previous frame's trailing envelope border. + t_e_last_prev: i32, + /// The previous frame's `kx` / `M` (for the `lTemp` splice). + k_x_prev: i32, + m_prev: i32, + env_state: EnvAdjustState, + prev_invf: Vec, + prev_bw: Vec, + prev_env: Option, + prev_noise: Option, +} + +impl ChannelState { + fn new(downsampled: bool, low_power: bool) -> Self { + ChannelState { + analysis: AnalysisBank::new(low_power), + synthesis: SynthesisBank::new(downsampled, low_power), + w_hist: vec![[Complex::default(); 32]; T_HF_GEN], + y_prev: vec![[Complex::default(); 64]; COLS], + t_e_last_prev: NUM_TIME_SLOTS, + k_x_prev: 0, + m_prev: 0, + env_state: EnvAdjustState::new(), + prev_invf: Vec::new(), + prev_bw: Vec::new(), + prev_env: None, + prev_noise: None, + } + } + + /// Run the analysis QMF over one 1024-sample core frame and build + /// the `XLow` buffer: columns `0..tHFGen` are the previous frame's + /// trailing slots (`W'`), columns `tHFGen..` the current `W`. + fn analyze(&mut self, core: &[f64]) -> Result> { + if core.len() != 1024 { + return Err(Error::SbrQmfInvalid); + } + let mut x_low = Vec::with_capacity(COLS); + x_low.extend_from_slice(&self.w_hist); + for slot in 0..LF { + let w = self.analysis.push_slot(&core[slot * 32..(slot + 1) * 32])?; + x_low.push(w); + } + self.w_hist.clear(); + self.w_hist.extend_from_slice(&x_low[COLS - T_HF_GEN..]); + Ok(x_low) + } +} + +/// One SBR decoder per channel element (SCE: 1 channel, CPE: 2). +#[derive(Debug)] +pub struct SbrDecoder { + fs_sbr: u32, + header: Option, + bands: Option, + patches: Option, + f_table_lim: Vec, + /// §4.6.18.4.3 downsampled output mode: the synthesis runs the + /// 32-channel bank and every frame yields 1024 samples per channel + /// at the *core* rate instead of 2048 at `fs_sbr`. + downsampled: bool, + /// §4.6.18.8 low-power mode: real-valued filterbanks, ×2 energy + /// estimation, aliasing detection/reduction, modified sinusoid + /// injection. PS payloads are rejected ([`Error::SbrLowPowerPs`]). + low_power: bool, + /// `k0` of the active band setup (the first `fMaster` subband; + /// the §4.6.18.8.3 reflection coefficients cover `0 ≤ k < k0`). + k0: i32, + /// Set once the first frame is processed (mode switches are then + /// rejected — the QMF synthesis state is rate-specific). + started: bool, + channels: Vec, + /// Annex 8.A parametric stereo state, created when a + /// single-channel element first carries a PS extension. Holds the + /// PS decoder plus the second (right-channel) synthesis bank; the + /// channel's own bank renders the left channel. + ps: Option, +} + +/// PS decoder + right-channel synthesis bank (Annex 8.A). +#[derive(Debug)] +struct PsState { + dec: PsDecoder, + synthesis_r: SynthesisBank, +} + +impl SbrDecoder { + /// A fresh SBR decoder. `fs_sbr` is the SBR internal rate (twice + /// the core rate); `num_channels` is 1 (SCE) or 2 (CPE). + pub fn new(fs_sbr: u32, num_channels: usize) -> Result { + if num_channels == 0 || num_channels > 2 || fs_sbr == 0 { + return Err(Error::SbrFreqBandInvalid); + } + Ok(SbrDecoder { + fs_sbr, + header: None, + bands: None, + patches: None, + f_table_lim: Vec::new(), + downsampled: false, + low_power: false, + k0: 0, + started: false, + channels: (0..num_channels) + .map(|_| ChannelState::new(false, false)) + .collect(), + ps: None, + }) + } + + /// Select the §4.6.18.4.3 downsampled output mode: the SBR-processed + /// subband signals are synthesized through the 32-channel QMF bank, + /// so the output stays at the *core* coder rate (1024 samples per + /// channel per frame) instead of the dual `fs_sbr` rate. The SBR + /// range above the core Nyquist (assembled `X` subbands 32..64) is + /// discarded by construction; the reconstructed bands below it are + /// kept, so the mode is still an SBR decode, not a plain core decode. + /// + /// Must be selected before the first frame is processed — the QMF + /// synthesis history is rate-specific ([`Error::SbrQmfInvalid`] + /// otherwise). + pub fn set_downsampled(&mut self, downsampled: bool) -> Result<()> { + if self.started { + return Err(Error::SbrQmfInvalid); + } + if self.downsampled != downsampled { + self.downsampled = downsampled; + self.rebuild_banks(); + } + Ok(()) + } + + /// `true` ⇔ the §4.6.18.4.3 downsampled output mode is selected. + #[must_use] + pub fn is_downsampled(&self) -> bool { + self.downsampled + } + + /// Select the §4.6.18.8 low-power SBR mode: the whole signal path + /// runs on real-valued subband signals (the §4.6.18.8.2 real + /// filterbanks), the envelope adjuster applies the §4.6.18.8.4 + /// energy correction and §4.6.18.8.5 aliasing reduction / modified + /// sinusoid injection, and gain smoothing is disabled. Composable + /// with [`Self::set_downsampled`]. A PS payload on a low-power + /// decoder is rejected with [`Error::SbrLowPowerPs`] — the + /// subpart-8 tool needs the complex QMF domain. + /// + /// Must be selected before the first frame is processed + /// ([`Error::SbrQmfInvalid`] otherwise). + pub fn set_low_power(&mut self, low_power: bool) -> Result<()> { + if self.started { + return Err(Error::SbrQmfInvalid); + } + if self.low_power != low_power { + self.low_power = low_power; + self.rebuild_banks(); + } + Ok(()) + } + + /// `true` ⇔ the §4.6.18.8 low-power mode is selected. + #[must_use] + pub fn is_low_power(&self) -> bool { + self.low_power + } + + /// Re-instantiate every filterbank for the current mode pair + /// (only legal before the first frame). + fn rebuild_banks(&mut self) { + for ch in &mut self.channels { + ch.analysis = AnalysisBank::new(self.low_power); + ch.synthesis = SynthesisBank::new(self.downsampled, self.low_power); + } + if let Some(ps) = &mut self.ps { + ps.synthesis_r = SynthesisBank::new(self.downsampled, self.low_power); + } + } + + /// §4.6.18.5 pure upsampling: no SBR data for this frame — run the + /// analysis / synthesis pair with the high 32 bands zero, keeping + /// the output rate steady and the QMF state continuous. + /// + /// `core` holds one 1024-sample time signal per channel; returns + /// 2048 samples per channel (1024 in the §4.6.18.4.3 downsampled + /// mode). + pub fn upsample_frame(&mut self, core: &[&[f64]]) -> Result>> { + if core.len() != self.channels.len() { + return Err(Error::SbrQmfInvalid); + } + self.started = true; + let mut out = Vec::with_capacity(core.len()); + let n_ch = self.channels.len(); + for (ch, core_ch) in self.channels.iter_mut().zip(core.iter()) { + let x_low = ch.analyze(core_ch)?; + let mut x_cols: Vec<[Complex; 64]> = Vec::with_capacity(LF); + for l in 0..LF { + let mut x = [Complex::default(); 64]; + x[..32].copy_from_slice(&x_low[l + T_HF_ADJ]); + x_cols.push(x); + } + let sps = ch.synthesis.samples_per_slot(); + // A PS-active stream holds its stereo parameters over a + // frame without SBR/PS payload (Annex 8.A.3); the whole + // 32-band spectrum counts as SBR-covered for the partial + // reset. + let mut emitted = false; + if n_ch == 1 { + if let Some(ps) = self.ps.as_mut() { + let x_input = build_x_input(&x_cols, &x_low); + if let Some((lq, rq)) = ps.dec.process(None, &x_input, 32)? { + let mut pcm_l = Vec::with_capacity(LF * sps); + let mut pcm_r = Vec::with_capacity(LF * sps); + for l in 0..LF { + ch.synthesis.push_slot(&lq[l], &mut pcm_l)?; + ps.synthesis_r.push_slot(&rq[l], &mut pcm_r)?; + } + out.push(pcm_l); + out.push(pcm_r); + emitted = true; + } + } + } + if !emitted { + let mut pcm = Vec::with_capacity(LF * sps); + for x in &x_cols { + ch.synthesis.push_slot(x, &mut pcm)?; + } + out.push(pcm); + } + // No Y for this frame; the next frame's lTemp splice sees + // an empty previous envelope span. + ch.y_prev + .iter_mut() + .for_each(|c| *c = [Complex::default(); 64]); + ch.t_e_last_prev = NUM_TIME_SLOTS; + } + Ok(out) + } + + /// Decode one SBR frame: `ext` is the parsed `sbr_extension_data()` + /// for this element, `core` one 1024-sample signal per channel. + /// Returns 2048 samples per channel at the SBR rate (1024 per + /// channel at the core rate in the §4.6.18.4.3 downsampled mode). + pub fn process_frame( + &mut self, + ext: &SbrExtensionData, + core: &[&[f64]], + ) -> Result>> { + let n_ch = self.channels.len(); + if core.len() != n_ch || ext.element.channels.len() != n_ch { + return Err(Error::SbrFreqBandInvalid); + } + self.started = true; + + // §4.6.18.3.3 reset: first header, or a transmitted header that + // changes the band geometry. + let reset = match &self.header { + None => true, + Some(prev) => prev.band_geometry_changed(&ext.header), + }; + if reset { + let k0v = derive_k0(self.fs_sbr, ext.header.start_freq)?; + let k2v = derive_k2(self.fs_sbr, ext.header.stop_freq, k0v)?; + let f_master = master_table(k0v, k2v, ext.header.freq_scale, ext.header.alter_scale)?; + let bands = + HiLoTables::derive(&f_master, ext.header.xover_band, ext.header.noise_bands)?; + let patches = build_patches(&f_master, k0v, bands.k_x, bands.m, self.fs_sbr)?; + self.f_table_lim = limiter_table( + &bands, + &patches.borders(bands.k_x), + ext.header.limiter_bands, + )?; + self.bands = Some(bands); + self.patches = Some(patches); + self.k0 = k0v; + for ch in &mut self.channels { + ch.prev_invf.clear(); + ch.prev_bw.clear(); + ch.prev_env = None; + ch.prev_noise = None; + } + } + self.header = Some(ext.header); + let bands = self.bands.as_ref().ok_or(Error::SbrFreqBandInvalid)?; + let patches = self.patches.as_ref().ok_or(Error::SbrFreqBandInvalid)?; + + let coupling = ext.element.coupling; + + // Reconstruct the quantized scalefactors per transmitted + // channel, then dequantize (jointly for a coupled pair). + let mut recon: Vec<(EnvelopeScalefactors, NoiseScalefactors)> = Vec::with_capacity(n_ch); + for (c, sbr_ch) in ext.element.channels.iter().enumerate() { + let st = &self.channels[c]; + let env = EnvelopeScalefactors::reconstruct( + &sbr_ch.envelope, + &sbr_ch.grid, + &sbr_ch.dtdf, + bands, + coupling, + c == 1, + if reset { None } else { st.prev_env.as_ref() }, + )?; + let noise = NoiseScalefactors::reconstruct( + &sbr_ch.noise, + &sbr_ch.grid, + &sbr_ch.dtdf, + bands.n_q(), + coupling, + c == 1, + if reset { None } else { st.prev_noise.as_ref() }, + )?; + recon.push((env, noise)); + } + + let dequant: Vec = if coupling && n_ch == 2 { + let amp_res = effective_amp_res(&ext.header, &ext.element.channels[0].grid); + let (l, r) = + dequant_coupled(&recon[0].0, &recon[0].1, &recon[1].0, &recon[1].1, amp_res); + vec![l, r] + } else { + (0..n_ch) + .map(|c| { + let amp_res = effective_amp_res(&ext.header, &ext.element.channels[c].grid); + dequant_single(&recon[c].0, &recon[c].1, amp_res) + }) + .collect() + }; + + let mut out = Vec::with_capacity(n_ch); + for c in 0..n_ch { + let sbr_ch = &ext.element.channels[c]; + let grid = derive_time_grid(&sbr_ch.grid, NUM_TIME_SLOTS)?; + + // Coupling: the second channel transmits no sbr_invf() + // (Table 4.66) — it shares the first channel's + // inverse-filtering modes. + let invf_modes = if coupling && c == 1 { + &ext.element.channels[0].invf.invf_mode + } else { + &sbr_ch.invf.invf_mode + }; + + let ch = &mut self.channels[c]; + + // Chirp factors (per noise band). + let bw = chirp_factors(invf_modes, &ch.prev_invf, &ch.prev_bw); + + // Analysis + XLow (with tHFGen history). + let x_low = ch.analyze(core[c])?; + + // HF generation over the envelope span. + let l_range = (RATE * grid.t_e[0])..(RATE * grid.t_e[grid.t_e.len() - 1]); + let x_high = generate_hf(&x_low, patches, &bw, bands, l_range, LF)?; + + // §4.6.18.8.3 aliasing detection (low power): reflection + // coefficients over the low band, the Figure 4.53 degree + // walk, and the patch carry onto the SBR range. + let dp = if self.low_power { + let k0_cnt = usize::try_from(self.k0).map_err(|_| Error::SbrFreqBandInvalid)?; + let mut refl = Vec::with_capacity(k0_cnt); + for k in 0..k0_cnt.min(32) { + refl.push(reflection_coefficient(&x_low, k, LF)?); + } + let deg = aliasing_degree(&refl); + Some(deg_patched(°, patches, bands.k_x, bands.m)?) + } else { + None + }; + + // Envelope adjustment. + let freq_res: Vec = sbr_ch.grid.freq_res.clone(); + let params = EnvParams { + bands, + f_table_lim: &self.f_table_lim, + t_e: &grid.t_e, + t_q: &grid.t_q, + freq_res: &freq_res, + l_a: grid.l_a, + e_orig: &dequant[c].e_orig, + q_orig: &dequant[c].q_orig, + add_harmonic: &sbr_ch.add_harmonic, + interpol_freq: ext.header.interpol_freq, + smoothing_mode: ext.header.smoothing_mode, + limiter_gains: ext.header.limiter_gains, + reset, + low_power: self.low_power, + deg_patched: dp.as_deref(), + }; + let y = adjust(&x_high, ¶ms, &mut ch.env_state)?; + + // §4.6.18.5 X assembly. + let l_temp = (RATE * ch.t_e_last_prev - NUM_TIME_SLOTS * RATE).max(0) as usize; + let mut x_cols: Vec<[Complex; 64]> = Vec::with_capacity(LF); + for l in 0..LF { + let mut x = [Complex::default(); 64]; + let (kx_cur, m_cur, y_col) = if l < l_temp { + (ch.k_x_prev, ch.m_prev, &ch.y_prev[l + T_HF_ADJ + LF]) + } else { + (bands.k_x, bands.m, &y[l + T_HF_ADJ]) + }; + let kx_u = kx_cur.max(0) as usize; + for (k, cell) in x.iter_mut().enumerate().take(kx_u.min(32)) { + *cell = x_low[l + T_HF_ADJ][k]; + } + let hi = (kx_cur + m_cur).max(0) as usize; + // §4.6.18.8.5: the low-power sinusoid spill extends the + // Y range one subband above the SBR range (≤ 63)… + let hi = if self.low_power { + (hi + 1).min(64) + } else { + hi.min(64) + }; + if kx_u < hi { + x[kx_u..hi].copy_from_slice(&y_col[kx_u..hi]); + } + // …and adds Y(kx − 1) onto the lowband subband rather + // than replacing it. + if self.low_power && (1..=32).contains(&kx_u) { + x[kx_u - 1] += y_col[kx_u - 1]; + } + x_cols.push(x); + } + + // Annex 8.A: a single-channel element carrying an + // EXTENSION_ID_PS payload renders stereo through the PS + // tool (the element's own bank = left, the PS state's = + // right). Until the first decodable ps_data() the mono + // path below stays in effect. + let ps_payload = if n_ch == 1 { + ext.element + .extension + .as_ref() + .filter(|e| e.id == EXTENSION_ID_PS) + .map(|e| e.data.as_slice()) + } else { + None + }; + if ps_payload.is_some() && self.low_power { + // §4.6.18.8: the real-valued tool cannot host the + // complex-domain PS processing. + return Err(Error::SbrLowPowerPs); + } + if ps_payload.is_some() && self.ps.is_none() { + self.ps = Some(PsState { + dec: PsDecoder::new(), + synthesis_r: SynthesisBank::new(self.downsampled, self.low_power), + }); + } + let sps = ch.synthesis.samples_per_slot(); + let mut emitted = false; + if n_ch == 1 { + if let Some(ps) = self.ps.as_mut() { + let x_input = build_x_input(&x_cols, &x_low); + let kx_plus_m = (bands.k_x + bands.m).max(0) as usize; + if let Some((lq, rq)) = ps.dec.process(ps_payload, &x_input, kx_plus_m)? { + let mut pcm_l = Vec::with_capacity(LF * sps); + let mut pcm_r = Vec::with_capacity(LF * sps); + for l in 0..LF { + ch.synthesis.push_slot(&lq[l], &mut pcm_l)?; + ps.synthesis_r.push_slot(&rq[l], &mut pcm_r)?; + } + out.push(pcm_l); + out.push(pcm_r); + emitted = true; + } + } + } + if !emitted { + let mut pcm = Vec::with_capacity(LF * sps); + for x in &x_cols { + ch.synthesis.push_slot(x, &mut pcm)?; + } + out.push(pcm); + } + + // Thread cross-frame state. + ch.y_prev = y; + ch.t_e_last_prev = grid.t_e[grid.t_e.len() - 1]; + ch.k_x_prev = bands.k_x; + ch.m_prev = bands.m; + ch.prev_invf = invf_modes.clone(); + ch.prev_bw = bw; + let (env, noise) = recon[c].clone(); + ch.prev_env = Some(env); + ch.prev_noise = Some(noise); + } + Ok(out) + } +} + +/// Assemble the Annex 8.A.3 `Xinput` matrix: the 32 assembled `X` +/// columns followed by `LOOKAHEAD` slots taken from `XLow` beyond the +/// frame (`XLow(k, l + tHFAdj)`, `k < 5` — the split bands the hybrid +/// filterbank consumes ahead of time). +fn build_x_input(x_cols: &[[Complex; 64]], x_low: &[[Complex; 32]]) -> Vec<[Complex; 64]> { + let mut v = Vec::with_capacity(LF + LOOKAHEAD); + v.extend_from_slice(x_cols); + for l in LF..LF + LOOKAHEAD { + let mut col = [Complex::default(); 64]; + col[..5].copy_from_slice(&x_low[l + T_HF_ADJ][..5]); + v.push(col); + } + v +} + +/// The effective `bs_amp_res` after the single-envelope FIXFIX +/// override (§4.4.2.8 Table 4.69 Note). +fn effective_amp_res(header: &SbrHeader, grid: &crate::sbr_grid::SbrGrid) -> bool { + if grid.amp_res_override { + false + } else { + header.amp_res + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sbr_element::{SbrChannel, SbrElement}; + use crate::sbr_envelope::{SbrEnvelopeData, SbrNoiseData}; + use crate::sbr_grid::{FrameClass, SbrDtdf, SbrGrid, SbrInvf}; + + fn sine(freq: f64, n: usize, offset: usize) -> Vec { + (0..n) + .map(|t| (2.0 * core::f64::consts::PI * freq * (t + offset) as f64).sin()) + .collect() + } + + /// Pure upsampling reproduces a 2×-upsampled, delayed sine across + /// frame boundaries. + #[test] + fn upsample_frames_are_continuous() { + let mut dec = SbrDecoder::new(44_100, 1).unwrap(); + let freq = 0.02; + let mut out = Vec::new(); + for f in 0..4 { + let core = sine(freq, 1024, f * 1024); + let o = dec.upsample_frame(&[&core]).unwrap(); + assert_eq!(o[0].len(), 2048); + out.extend_from_slice(&o[0]); + } + // Steady-state fit against the ideal upsampled sine. + let ideal = |t: f64, d: f64| (2.0 * core::f64::consts::PI * freq * (t - d) / 2.0).sin(); + let mut best = f64::INFINITY; + for delay in 0..1500usize { + let mut err = 0.0; + let mut sig = 0.0; + for (t, &o) in out.iter().enumerate().skip(2500) { + let e = o - ideal(t as f64, delay as f64); + err += e * e; + sig += o * o; + } + best = best.min(err / sig.max(1e-30)); + } + assert!(best < 1e-4, "upsample error ratio {best}"); + } + + /// Build a minimal single-channel SBR extension: one FIXFIX + /// envelope, frequency-direction start values, flat noise floor. + fn synthetic_ext(fs_sbr: u32, env_start: i32, noise_q: i32) -> SbrExtensionData { + let header = SbrHeader { + amp_res: true, + start_freq: 5, + stop_freq: 3, + xover_band: 0, + reserved: 0, + header_extra_1: false, + header_extra_2: false, + freq_scale: 2, + alter_scale: true, + noise_bands: 2, + limiter_bands: 2, + limiter_gains: 2, + interpol_freq: true, + smoothing_mode: true, + }; + let bands = header.derive_bands(fs_sbr).unwrap(); + let n_high = bands.n_high(); + let n_q = bands.n_q(); + let grid = SbrGrid { + frame_class: FrameClass::FixFix, + num_env: 1, + num_noise: 1, + freq_res: vec![true], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: true, + }; + let dtdf = SbrDtdf { + df_env: vec![false], + df_noise: vec![false], + }; + let invf = SbrInvf { + invf_mode: vec![0; n_q], + }; + let mut env_row = vec![0i32; n_high]; + env_row[0] = env_start; + let envelope = SbrEnvelopeData { + data: vec![env_row], + }; + let noise = SbrNoiseData { + data: vec![{ + let mut r = vec![0i32; n_q]; + r[0] = noise_q; + r + }], + }; + SbrExtensionData { + crc: None, + crc_region: None, + header_present: true, + header, + element: SbrElement { + coupling: false, + channels: vec![SbrChannel { + grid, + dtdf, + invf, + envelope, + noise, + add_harmonic: vec![], + }], + extension: None, + }, + num_sbr_bits: 0, + } + } + + /// A full synthetic SBR frame produces finite 2048-sample output + /// with energy in the SBR band, and threads state across frames + /// (header reuse, no reset). + #[test] + fn synthetic_sbr_frame_produces_high_band() { + let fs_sbr = 44_100; + let ext = synthetic_ext(fs_sbr, 10, 6); + let mut dec = SbrDecoder::new(fs_sbr, 1).unwrap(); + // A mid-band core tone so the patch sources carry signal. + let freq = 0.11; + let mut all = Vec::new(); + for f in 0..3 { + let core = sine(freq, 1024, f * 1024); + let out = dec.process_frame(&ext, &[&core]).unwrap(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].len(), 2048); + assert!(out[0].iter().all(|v| v.is_finite())); + all.extend_from_slice(&out[0]); + } + // The output must carry energy (base band at least). + let energy: f64 = all.iter().map(|v| v * v).sum(); + assert!(energy > 1.0, "energy {energy}"); + // Deterministic: a second decoder over the same input matches + // bit-exactly. + let mut dec2 = SbrDecoder::new(fs_sbr, 1).unwrap(); + let mut all2 = Vec::new(); + for f in 0..3 { + let core = sine(freq, 1024, f * 1024); + all2.extend_from_slice(&dec2.process_frame(&ext, &[&core]).unwrap()[0]); + } + assert_eq!(all, all2); + } + + /// The high band actually receives patched content: with a strong + /// envelope target the spectrum above kx·(fs/128) is non-silent, + /// and it scales with the envelope scalefactor. + #[test] + fn envelope_scalefactor_controls_high_band_level() { + let fs_sbr = 44_100; + let mut quiet = SbrDecoder::new(fs_sbr, 1).unwrap(); + let mut loud = SbrDecoder::new(fs_sbr, 1).unwrap(); + let ext_quiet = synthetic_ext(fs_sbr, 2, 10); + let ext_loud = synthetic_ext(fs_sbr, 12, 10); + let freq = 0.09; + let mut hi_q = 0.0f64; + let mut hi_l = 0.0f64; + for f in 0..3 { + let core = sine(freq, 1024, f * 1024); + let oq = quiet.process_frame(&ext_quiet, &[&core]).unwrap(); + let ol = loud.process_frame(&ext_loud, &[&core]).unwrap(); + if f > 0 { + // High-pass both outputs with a crude difference filter + // to weight the HF region, then compare energies. + for w in oq[0].windows(2) { + hi_q += (w[1] - w[0]) * (w[1] - w[0]); + } + for w in ol[0].windows(2) { + hi_l += (w[1] - w[0]) * (w[1] - w[0]); + } + } + } + assert!(hi_l > hi_q * 4.0, "loud {hi_l} vs quiet {hi_q}"); + } + + /// Downsampled pure upsampling is the identity at the core rate + /// (up to the analysis+synthesis delay), and each frame yields + /// 1024 samples. + #[test] + fn downsampled_upsample_is_identity_at_core_rate() { + let mut dec = SbrDecoder::new(44_100, 1).unwrap(); + dec.set_downsampled(true).unwrap(); + assert!(dec.is_downsampled()); + let freq = 0.02; + let mut input_all = Vec::new(); + let mut out = Vec::new(); + for f in 0..4 { + let core = sine(freq, 1024, f * 1024); + input_all.extend_from_slice(&core); + let o = dec.upsample_frame(&[&core]).unwrap(); + assert_eq!(o[0].len(), 1024); + out.extend_from_slice(&o[0]); + } + // Mode switches after the first frame are rejected. + assert!(dec.set_downsampled(false).is_err()); + let mut best = (f64::INFINITY, 0usize); + for delay in 0..1024usize { + let mut err = 0.0; + let mut sig = 0.0; + for t in 1500..out.len() { + if t < delay { + continue; + } + let e = out[t] - input_all[t - delay]; + err += e * e; + sig += out[t] * out[t]; + } + let ratio = err / sig.max(1e-30); + if ratio < best.0 { + best = (ratio, delay); + } + } + assert!( + best.0 < 1e-4, + "identity error ratio {} at {}", + best.0, + best.1 + ); + } + + /// With the whole SBR range inside the first 32 QMF bands, the + /// dual-rate output is band-limited below the core Nyquist, so the + /// downsampled decode must match a straight 2:1 decimation of the + /// dual-rate decode (same synthetic SBR frames, delay-searched). + #[test] + fn downsampled_matches_decimated_dual_rate() { + // The synthetic header at 44.1 kHz derives kx 14, M 15 — + // kx + M = 29 ≤ 32, so no SBR content crosses the core Nyquist. + let fs_sbr = 44_100; + let ext = synthetic_ext(fs_sbr, 8, 6); + let bands = ext.header.derive_bands(fs_sbr).unwrap(); + assert!( + bands.k_x + bands.m <= 32, + "test premise: SBR range within 32 bands (kx {} M {})", + bands.k_x, + bands.m + ); + let mut dual = SbrDecoder::new(fs_sbr, 1).unwrap(); + let mut down = SbrDecoder::new(fs_sbr, 1).unwrap(); + down.set_downsampled(true).unwrap(); + let freq = 0.055; + let mut out_dual = Vec::new(); + let mut out_down = Vec::new(); + for f in 0..6 { + let core = sine(freq, 1024, f * 1024); + out_dual.extend_from_slice(&dual.process_frame(&ext, &[&core]).unwrap()[0]); + let o = down.process_frame(&ext, &[&core]).unwrap(); + assert_eq!(o[0].len(), 1024); + out_down.extend_from_slice(&o[0]); + } + // out_down[n] ≈ out_dual[2n − d] for some fixed integer d + // (either parity): search d, then gate the steady-state error. + let mut best = (f64::INFINITY, 0usize); + for d in 0..1400usize { + let mut err = 0.0; + let mut sig = 0.0; + for (n, &od) in out_down.iter().enumerate().skip(1200) { + let idx = 2 * n; + if idx < d || idx - d >= out_dual.len() { + continue; + } + let e = od - out_dual[idx - d]; + err += e * e; + sig += od * od; + } + let ratio = err / sig.max(1e-30); + if ratio < best.0 { + best = (ratio, d); + } + } + assert!( + best.0 < 1e-3, + "decimation mismatch ratio {} at delay {}", + best.0, + best.1 + ); + } + + /// The §4.6.18.8 low-power mode reconstructs the same synthetic + /// SBR frame as the high-quality mode to a moderate tolerance + /// (the LP tool is a real-valued approximation), stays finite and + /// deterministic, and composes with the downsampled output. + #[test] + fn low_power_tracks_high_quality() { + let fs_sbr = 44_100; + let ext = synthetic_ext(fs_sbr, 8, 6); + let mut hq = SbrDecoder::new(fs_sbr, 1).unwrap(); + let mut lp = SbrDecoder::new(fs_sbr, 1).unwrap(); + lp.set_low_power(true).unwrap(); + assert!(lp.is_low_power()); + let freq = 0.055; + let mut out_hq = Vec::new(); + let mut out_lp = Vec::new(); + for f in 0..6 { + let core = sine(freq, 1024, f * 1024); + out_hq.extend_from_slice(&hq.process_frame(&ext, &[&core]).unwrap()[0]); + let o = lp.process_frame(&ext, &[&core]).unwrap(); + assert_eq!(o[0].len(), 2048); + assert!(o[0].iter().all(|v| v.is_finite())); + out_lp.extend_from_slice(&o[0]); + } + assert!(lp.set_low_power(false).is_err(), "mode locked after start"); + // Energy tracks the HQ reconstruction (the two banks share the + // prototype and delay). + let e_hq: f64 = out_hq.iter().skip(4096).map(|v| v * v).sum(); + let e_lp: f64 = out_lp.iter().skip(4096).map(|v| v * v).sum(); + assert!( + e_lp > 0.5 * e_hq && e_lp < 2.0 * e_hq, + "LP {e_lp} vs HQ {e_hq}" + ); + // The real-valued HF processing does not reproduce the complex + // path's subband phases, so the comparison is energy-domain: + // the core tone's amplitude (quadrature probe at the upsampled + // frequency) must match tightly, and the per-block energy + // envelope must track. + let probe = |x: &[f64]| -> f64 { + let w = 2.0 * core::f64::consts::PI * freq / 2.0; + let (mut cs, mut sn) = (0.0f64, 0.0f64); + let n0 = 4096; + for (t, &v) in x.iter().enumerate().skip(n0) { + cs += v * (w * t as f64).cos(); + sn += v * (w * t as f64).sin(); + } + let n = (x.len() - n0) as f64; + 2.0 / n * (cs * cs + sn * sn).sqrt() + }; + let (a_hq, a_lp) = (probe(&out_hq), probe(&out_lp)); + assert!( + (a_lp - a_hq).abs() < 0.05 * a_hq, + "core tone amplitude LP {a_lp} vs HQ {a_hq}" + ); + for (block_hq, block_lp) in out_hq + .chunks_exact(1024) + .zip(out_lp.chunks_exact(1024)) + .skip(4) + { + let e_h: f64 = block_hq.iter().map(|v| v * v).sum(); + let e_l: f64 = block_lp.iter().map(|v| v * v).sum(); + assert!( + e_l > 0.4 * e_h && e_l < 2.5 * e_h, + "block energy LP {e_l} vs HQ {e_h}" + ); + } + + // Determinism. + let mut lp2 = SbrDecoder::new(fs_sbr, 1).unwrap(); + lp2.set_low_power(true).unwrap(); + let mut out_lp2 = Vec::new(); + for f in 0..6 { + let core = sine(freq, 1024, f * 1024); + out_lp2.extend_from_slice(&lp2.process_frame(&ext, &[&core]).unwrap()[0]); + } + assert_eq!(out_lp, out_lp2); + + // LP + downsampled: 1024 samples per frame, finite. + let mut lpd = SbrDecoder::new(fs_sbr, 1).unwrap(); + lpd.set_low_power(true).unwrap(); + lpd.set_downsampled(true).unwrap(); + let core = sine(freq, 1024, 0); + let o = lpd.process_frame(&ext, &[&core]).unwrap(); + assert_eq!(o[0].len(), 1024); + assert!(o[0].iter().all(|v| v.is_finite())); + } + + /// Low-power pure upsampling is still the identity at 2× rate + /// (real analysis + real synthesis pair). + #[test] + fn low_power_upsample_is_identity() { + let mut dec = SbrDecoder::new(44_100, 1).unwrap(); + dec.set_low_power(true).unwrap(); + let freq = 0.02; + let mut out = Vec::new(); + for f in 0..4 { + let core = sine(freq, 1024, f * 1024); + let o = dec.upsample_frame(&[&core]).unwrap(); + assert_eq!(o[0].len(), 2048); + out.extend_from_slice(&o[0]); + } + let ideal = |t: f64, d: f64| (2.0 * core::f64::consts::PI * freq * (t - d) / 2.0).sin(); + let mut best = f64::INFINITY; + for delay in 0..1500usize { + let mut err = 0.0; + let mut sig = 0.0; + for (t, &o) in out.iter().enumerate().skip(2500) { + let e = o - ideal(t as f64, delay as f64); + err += e * e; + sig += o * o; + } + best = best.min(err / sig.max(1e-30)); + } + assert!(best < 1e-4, "LP upsample error ratio {best}"); + } + + /// A PS payload on a low-power decoder is rejected — the + /// subpart-8 tool needs the complex QMF domain. + #[test] + fn low_power_rejects_ps() { + use crate::sbr_element::SbrExtension; + let fs_sbr = 44_100; + let mut ext = synthetic_ext(fs_sbr, 8, 6); + ext.element.extension = Some(SbrExtension { + id: EXTENSION_ID_PS, + data: vec![0u8; 4], + }); + let mut lp = SbrDecoder::new(fs_sbr, 1).unwrap(); + lp.set_low_power(true).unwrap(); + let core = sine(0.05, 1024, 0); + assert!(matches!( + lp.process_frame(&ext, &[&core]), + Err(Error::SbrLowPowerPs) + )); + } + + /// A channel-count / buffer-length mismatch is rejected. + #[test] + fn shape_mismatches_rejected() { + let mut dec = SbrDecoder::new(44_100, 1).unwrap(); + let core = vec![0.0; 512]; + assert!(dec.upsample_frame(&[&core]).is_err()); + let ext = synthetic_ext(44_100, 0, 6); + let short = vec![0.0; 1024]; + assert!(dec.process_frame(&ext, &[&short[..], &short[..]]).is_err()); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_dequant.rs b/crates/vendor/oxideav-aac/src/sbr_dequant.rs new file mode 100644 index 00000000..88b559b1 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_dequant.rs @@ -0,0 +1,259 @@ +//! SBR envelope / noise-floor dequantization — ISO/IEC 14496-3 +//! §4.6.18.3.5 "Dequantization and stereo decoding". +//! +//! Converts the reconstructed *quantized* scalefactors +//! ([`crate::sbr_reconstruct`]'s `E_Q(k,l)` / `Q(k,l)`) into the linear +//! energy values `EOrig(k,l)` / `QOrig(k,l)` the envelope adjuster +//! (§4.6.18.7) consumes: +//! +//! * Single channel (or an uncoupled pair, `bs_coupling == 0`): +//! `EOrig = 64 · 2^(E/a)` with `a = 2` for `bs_amp_res = 0` (1.5 dB +//! steps) and `a = 1` for `bs_amp_res = 1` (3.0 dB steps); +//! `QOrig = 2^(NOISE_FLOOR_OFFSET − Q)` with +//! `NOISE_FLOOR_OFFSET = 6` (§4.6.18.2.5). +//! * Coupled pair (`bs_coupling == 1`): channel 0 carries the +//! level average and channel 1 the pan ratio; +//! `panOffset = [24, 12]` (§4.6.18.2.6) recentres the ratio. The +//! left / right split divides the doubled average +//! `64·2^(E0/a + 1)` by `1 + 2^(±(panOffset − E1)/a)` (and the +//! noise analogue with `panOffset(1) = 12`), which preserves +//! `ELeft + ERight = 2 · (64·2^(E0/a))`. +//! +//! ## Provenance +//! +//! Every formula and constant is from the §4.6.18.3.5 text and the +//! §4.6.18.2.5 / §4.6.18.2.6 constant lists of the staged spec. No part +//! of this implementation is derived from any external decoder. + +use crate::sbr_reconstruct::{EnvelopeScalefactors, NoiseScalefactors}; + +/// `NOISE_FLOOR_OFFSET = 6` (§4.6.18.2.5). +pub const NOISE_FLOOR_OFFSET: f64 = 6.0; + +/// `panOffset = [24, 12]` indexed by `bs_amp_res` (§4.6.18.2.6). +#[inline] +#[must_use] +pub fn pan_offset(amp_res: bool) -> f64 { + if amp_res { + 12.0 + } else { + 24.0 + } +} + +/// The §4.6.18.3.5 amplitude-resolution divisor `a`: `2` for +/// `bs_amp_res = 0` (1.5 dB), `1` for `bs_amp_res = 1` (3.0 dB). +#[inline] +#[must_use] +pub fn amp_divisor(amp_res: bool) -> f64 { + if amp_res { + 1.0 + } else { + 2.0 + } +} + +/// Dequantized (linear-energy) envelope and noise-floor scalefactors +/// for one channel. +#[derive(Debug, Clone, PartialEq)] +pub struct DequantizedSbr { + /// `EOrig[l][k]` — linear envelope energies, one band vector per + /// envelope (band count follows the envelope's frequency + /// resolution). + pub e_orig: Vec>, + /// `QOrig[l][k]` — linear noise-floor energies, one `NQ`-band + /// vector per noise floor. + pub q_orig: Vec>, +} + +/// §4.6.18.3.5 single-channel dequantization: +/// `EOrig = 64·2^(E/a)`, `QOrig = 2^(NOISE_FLOOR_OFFSET − Q)`. +#[must_use] +pub fn dequant_single( + env: &EnvelopeScalefactors, + noise: &NoiseScalefactors, + amp_res: bool, +) -> DequantizedSbr { + let a = amp_divisor(amp_res); + let e_orig = env + .eq + .iter() + .map(|l| { + l.iter() + .map(|&e| 64.0 * (f64::from(e) / a).exp2()) + .collect() + }) + .collect(); + let q_orig = noise + .q + .iter() + .map(|l| { + l.iter() + .map(|&q| (NOISE_FLOOR_OFFSET - f64::from(q)).exp2()) + .collect() + }) + .collect(); + DequantizedSbr { e_orig, q_orig } +} + +/// §4.6.18.3.5 coupled-pair dequantization. +/// +/// `ch0` carries the level average (`E0` / `Q0`), `ch1` the pan ratio +/// (`E1` / `Q1`). Returns the `(left, right)` linear energies. +#[must_use] +pub fn dequant_coupled( + env0: &EnvelopeScalefactors, + noise0: &NoiseScalefactors, + env1: &EnvelopeScalefactors, + noise1: &NoiseScalefactors, + amp_res: bool, +) -> (DequantizedSbr, DequantizedSbr) { + let a = amp_divisor(amp_res); + let pan = pan_offset(amp_res); + + let mut left_e = Vec::with_capacity(env0.eq.len()); + let mut right_e = Vec::with_capacity(env0.eq.len()); + for (l0, l1) in env0.eq.iter().zip(env1.eq.iter()) { + let mut le = Vec::with_capacity(l0.len()); + let mut re = Vec::with_capacity(l0.len()); + for (&e0, &e1) in l0.iter().zip(l1.iter()) { + // 64·2^(E0/a + 1) split by the pan ratio. + let avg2 = 64.0 * (f64::from(e0) / a + 1.0).exp2(); + let ratio = ((pan - f64::from(e1)) / a).exp2(); + le.push(avg2 / (1.0 + ratio)); + re.push(avg2 / (1.0 + 1.0 / ratio)); + } + left_e.push(le); + right_e.push(re); + } + + // Noise floors always use panOffset(1) = 12 (§4.6.18.3.5: the + // noise formulas are written with panOffset(1) regardless of + // bs_amp_res). + let noise_pan = pan_offset(true); + let mut left_q = Vec::with_capacity(noise0.q.len()); + let mut right_q = Vec::with_capacity(noise0.q.len()); + for (l0, l1) in noise0.q.iter().zip(noise1.q.iter()) { + let mut lq = Vec::with_capacity(l0.len()); + let mut rq = Vec::with_capacity(l0.len()); + for (&q0, &q1) in l0.iter().zip(l1.iter()) { + let avg2 = (NOISE_FLOOR_OFFSET - f64::from(q0) + 1.0).exp2(); + let ratio = (noise_pan - f64::from(q1)).exp2(); + lq.push(avg2 / (1.0 + ratio)); + rq.push(avg2 / (1.0 + 1.0 / ratio)); + } + left_q.push(lq); + right_q.push(rq); + } + + ( + DequantizedSbr { + e_orig: left_e, + q_orig: left_q, + }, + DequantizedSbr { + e_orig: right_e, + q_orig: right_q, + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn env(eq: Vec>) -> EnvelopeScalefactors { + let n = eq.len(); + EnvelopeScalefactors { + eq, + freq_res: vec![true; n], + } + } + + fn noise(q: Vec>) -> NoiseScalefactors { + NoiseScalefactors { q } + } + + /// `EOrig = 64·2^(E/a)`: exact powers for both amplitude + /// resolutions. + #[test] + fn single_channel_envelope_powers() { + let e = env(vec![vec![0, 2, 4]]); + let q = noise(vec![vec![6]]); + // bs_amp_res = 1 → a = 1: 64·2^E. + let d = dequant_single(&e, &q, true); + assert_eq!(d.e_orig[0], vec![64.0, 256.0, 1024.0]); + // bs_amp_res = 0 → a = 2: 64·2^(E/2). + let d = dequant_single(&e, &q, false); + assert_eq!(d.e_orig[0], vec![64.0, 128.0, 256.0]); + } + + /// `QOrig = 2^(6 − Q)`: Q = 6 is unity, each +1 halves. + #[test] + fn single_channel_noise_powers() { + let e = env(vec![vec![0]]); + let q = noise(vec![vec![0, 6, 8]]); + let d = dequant_single(&e, &q, true); + assert_eq!(d.q_orig[0], vec![64.0, 1.0, 0.25]); + } + + /// A balanced pan (`E1 == panOffset`) splits the energy equally: + /// both channels get exactly the mono dequantization. + #[test] + fn coupled_balanced_pan_is_symmetric() { + for amp_res in [false, true] { + let e0 = env(vec![vec![4, 8]]); + let q0 = noise(vec![vec![3]]); + let e1 = env(vec![vec![ + pan_offset(amp_res) as i32, + pan_offset(amp_res) as i32, + ]]); + let q1 = noise(vec![vec![12]]); + let (l, r) = dequant_coupled(&e0, &q0, &e1, &q1, amp_res); + let mono = dequant_single(&e0, &q0, amp_res); + for k in 0..2 { + assert!((l.e_orig[0][k] - mono.e_orig[0][k]).abs() < 1e-12); + assert!((r.e_orig[0][k] - mono.e_orig[0][k]).abs() < 1e-12); + } + assert!((l.q_orig[0][0] - mono.q_orig[0][0]).abs() < 1e-12); + assert!((r.q_orig[0][0] - mono.q_orig[0][0]).abs() < 1e-12); + } + } + + /// The coupled split preserves the pair sum: + /// `ELeft + ERight = 2·(64·2^(E0/a))` for every pan value, and the + /// same for the noise floors. + #[test] + fn coupled_split_preserves_energy_sum() { + for amp_res in [false, true] { + for e1v in [0, 5, 11, 17, 24] { + let e0 = env(vec![vec![6]]); + let q0 = noise(vec![vec![4]]); + let e1 = env(vec![vec![e1v]]); + let q1 = noise(vec![vec![(e1v % 12) * 2]]); + let (l, r) = dequant_coupled(&e0, &q0, &e1, &q1, amp_res); + let mono = dequant_single(&e0, &q0, amp_res); + let sum = l.e_orig[0][0] + r.e_orig[0][0]; + assert!( + (sum - 2.0 * mono.e_orig[0][0]).abs() < 1e-9, + "amp_res {amp_res} pan {e1v}: {sum}" + ); + let qsum = l.q_orig[0][0] + r.q_orig[0][0]; + assert!((qsum - 2.0 * mono.q_orig[0][0]).abs() < 1e-9); + } + } + } + + /// A pan below the offset weights the left channel heavier (E1 + /// counts down from left-dominant to right-dominant). + #[test] + fn coupled_pan_direction() { + let e0 = env(vec![vec![6]]); + let q0 = noise(vec![vec![4]]); + let e1 = env(vec![vec![2]]); + let q1 = noise(vec![vec![2]]); + let (l, r) = dequant_coupled(&e0, &q0, &e1, &q1, true); + assert!(l.e_orig[0][0] < r.e_orig[0][0]); + assert!(l.q_orig[0][0] < r.q_orig[0][0]); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_element.rs b/crates/vendor/oxideav-aac/src/sbr_element.rs new file mode 100644 index 00000000..e0ef6689 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_element.rs @@ -0,0 +1,561 @@ +//! SBR element framing — `sbr_single_channel_element()` / +//! `sbr_channel_pair_element()` and `sbr_sinusoidal_coding()` — +//! ISO/IEC 14496-3 §4.4.2.8, Tables 4.65, 4.66, 4.74. +//! +//! These wrappers tie the per-channel grid / dtdf / invf / envelope / +//! noise parses together into a whole SBR data element, in the exact +//! order the spec syntax tables prescribe: +//! +//! * `sbr_single_channel_element()` (Table 4.65): one optional +//! `bs_data_extra` reserved field, then `sbr_grid(0)`, `sbr_dtdf(0)`, +//! `sbr_invf(0)`, `sbr_envelope(0,0)`, `sbr_noise(0,0)`, the optional +//! `sbr_sinusoidal_coding(0)`, and the optional extended-data block. +//! * `sbr_channel_pair_element()` (Table 4.66): the two +//! coupling-dependent layouts. When `bs_coupling` is set, a single +//! shared grid drives both channels' envelopes / noise (with the +//! second channel coded in *balance* mode); otherwise each channel +//! carries its own grid. Either way the parse order is fixed by the +//! table. +//! +//! `sbr_sinusoidal_coding()` (Table 4.74) reads one +//! `bs_add_harmonic[ch][n]` flag per high-resolution band (`NHigh`). +//! +//! The element-level `bs_amp_res` may be forced to `0` by a +//! single-envelope FIXFIX grid ([`crate::sbr_grid::SbrGrid::amp_res_override`]); +//! this wrapper applies that override before decoding the envelopes so +//! the start-value widths and codebook selection match the spec's +//! in-order `bs_amp_res` mutation. +//! +//! The extended-data block (`bs_extended_data` … `sbr_extension`) is +//! recognized and its size is consumed, but the only standardized +//! `sbr_extension` payload (PS, `bs_extension_id == EXTENSION_ID_PS`) +//! is not yet decoded — its bits are skipped as fill so the element +//! parse stays byte-aligned. The raw extension bytes are surfaced for a +//! later PS pass. +//! +//! All of this is fixed-/variable-width syntax driven by the grid and +//! the band tables; the Huffman content lives in [`crate::sbr_huffman`]. + +use crate::sbr_envelope::{SbrEnvelopeData, SbrNoiseData}; +use crate::sbr_freq_bands::HiLoTables; +use crate::sbr_grid::{SbrDtdf, SbrGrid, SbrInvf}; +use crate::{Error, Result}; +use oxideav_core::bits::BitReader; + +/// `bs_extension_id` value that signals a Parametric Stereo payload +/// inside `sbr_extension()` (§4.4.2.8 / Table 4.A.x). PS itself is not +/// decoded here yet. +pub const EXTENSION_ID_PS: u8 = 2; + +/// One channel's fully-parsed SBR side info. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrChannel { + /// The time-frequency grid. + pub grid: SbrGrid, + /// The delta-direction flags. + pub dtdf: SbrDtdf, + /// The inverse-filtering modes (one per noise band). + pub invf: SbrInvf, + /// Raw envelope deltas. + pub envelope: SbrEnvelopeData, + /// Raw noise-floor deltas. + pub noise: SbrNoiseData, + /// `bs_add_harmonic[n]` — one flag per high-resolution band + /// (`NHigh`); empty when `bs_add_harmonic_flag` was clear. + pub add_harmonic: Vec, +} + +/// A parsed SBR data element (single channel or channel pair). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrElement { + /// `bs_coupling` (always `false` for a single channel element). + pub coupling: bool, + /// One or two channels of side info. + pub channels: Vec, + /// Raw bytes of an `sbr_extension()` payload, if `bs_extended_data` + /// was set. Reserved for a later PS decode; `None` when no extended + /// data was present. + pub extension: Option, +} + +/// Raw `sbr_extension()` content carried past this parse. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrExtension { + /// `bs_extension_id`. + pub id: u8, + /// The extension body bytes (everything after the 2-bit id, up to + /// the byte-aligned fill). + pub data: Vec, +} + +/// `sbr_sinusoidal_coding()` (Table 4.74): `NHigh` add-harmonic flags. +fn parse_sinusoidal(reader: &mut BitReader<'_>, n_high: usize) -> Result> { + let mut v = Vec::with_capacity(n_high); + for _ in 0..n_high { + v.push(read_flag(reader)?); + } + Ok(v) +} + +/// Parse one channel's `sbr_grid` → `sbr_dtdf` → `sbr_invf` block (the +/// shared prefix of both element types). +fn parse_grid_dtdf_invf( + reader: &mut BitReader<'_>, + n_q: usize, +) -> Result<(SbrGrid, SbrDtdf, SbrInvf)> { + let grid = SbrGrid::parse(reader)?; + let dtdf = SbrDtdf::parse(reader, grid.num_env, grid.num_noise)?; + let invf = SbrInvf::parse(reader, n_q)?; + Ok((grid, dtdf, invf)) +} + +impl SbrElement { + /// Parse `sbr_single_channel_element()` (Table 4.65). + /// + /// `bands` is the derived band table for the active header; `n_q` is + /// its noise-band count. `bs_amp_res` is the header amplitude + /// resolution (it may be overridden by a single-envelope FIXFIX + /// grid). + pub fn parse_single( + reader: &mut BitReader<'_>, + bands: &HiLoTables, + amp_res: bool, + ) -> Result { + let n_q = bands.n_q(); + // bs_data_extra (1 bit) → optional bs_reserved (4). + if read_flag(reader)? { + read(reader, 4)?; + } + + let (grid, dtdf, invf) = parse_grid_dtdf_invf(reader, n_q)?; + let eff_amp = amp_res && !grid.amp_res_override; + + let envelope = SbrEnvelopeData::parse(reader, &grid, &dtdf, bands, false, false, eff_amp)?; + let noise = SbrNoiseData::parse(reader, &grid, &dtdf, n_q, false, false, eff_amp)?; + + let add_harmonic = if read_flag(reader)? { + parse_sinusoidal(reader, bands.n_high())? + } else { + Vec::new() + }; + + let extension = parse_extended_data(reader)?; + + Ok(SbrElement { + coupling: false, + channels: vec![SbrChannel { + grid, + dtdf, + invf, + envelope, + noise, + add_harmonic, + }], + extension, + }) + } + + /// Parse `sbr_channel_pair_element()` (Table 4.66), both the coupled + /// and the independent layouts. + pub fn parse_pair( + reader: &mut BitReader<'_>, + bands: &HiLoTables, + amp_res: bool, + ) -> Result { + let n_q = bands.n_q(); + // bs_data_extra (1 bit) → two bs_reserved (4 each). + if read_flag(reader)? { + read(reader, 4)?; + read(reader, 4)?; + } + + let coupling = read_flag(reader)?; + + let channels = if coupling { + // Shared grid; second channel coded in balance mode. Parse + // order (Table 4.66, coupling): grid(0), dtdf(0), dtdf(1), + // invf(0). + let grid = SbrGrid::parse(reader)?; + let dtdf0 = SbrDtdf::parse(reader, grid.num_env, grid.num_noise)?; + let dtdf1 = SbrDtdf::parse(reader, grid.num_env, grid.num_noise)?; + let invf0 = SbrInvf::parse(reader, n_q)?; + let eff_amp = amp_res && !grid.amp_res_override; + + // Order (Table 4.66, coupling): env0, noise0, env1, noise1. + let env0 = SbrEnvelopeData::parse(reader, &grid, &dtdf0, bands, true, false, eff_amp)?; + let noise0 = SbrNoiseData::parse(reader, &grid, &dtdf0, n_q, true, false, eff_amp)?; + let env1 = SbrEnvelopeData::parse(reader, &grid, &dtdf1, bands, true, true, eff_amp)?; + let noise1 = SbrNoiseData::parse(reader, &grid, &dtdf1, n_q, true, true, eff_amp)?; + + let (h0, h1) = parse_pair_harmonics(reader, bands)?; + vec![ + SbrChannel { + grid: grid.clone(), + dtdf: dtdf0, + invf: invf0, + envelope: env0, + noise: noise0, + add_harmonic: h0, + }, + SbrChannel { + grid, + dtdf: dtdf1, + invf: SbrInvf { + invf_mode: Vec::new(), + }, + envelope: env1, + noise: noise1, + add_harmonic: h1, + }, + ] + } else { + // Independent grids per channel. + let grid0 = SbrGrid::parse(reader)?; + let grid1 = SbrGrid::parse(reader)?; + let dtdf0 = SbrDtdf::parse(reader, grid0.num_env, grid0.num_noise)?; + let dtdf1 = SbrDtdf::parse(reader, grid1.num_env, grid1.num_noise)?; + let invf0 = SbrInvf::parse(reader, n_q)?; + let invf1 = SbrInvf::parse(reader, n_q)?; + + let eff0 = amp_res && !grid0.amp_res_override; + let eff1 = amp_res && !grid1.amp_res_override; + + // Order (Table 4.66, no coupling): env0, env1, noise0, noise1. + let env0 = SbrEnvelopeData::parse(reader, &grid0, &dtdf0, bands, false, false, eff0)?; + let env1 = SbrEnvelopeData::parse(reader, &grid1, &dtdf1, bands, false, true, eff1)?; + let noise0 = SbrNoiseData::parse(reader, &grid0, &dtdf0, n_q, false, false, eff0)?; + let noise1 = SbrNoiseData::parse(reader, &grid1, &dtdf1, n_q, false, true, eff1)?; + + let (h0, h1) = parse_pair_harmonics(reader, bands)?; + vec![ + SbrChannel { + grid: grid0, + dtdf: dtdf0, + invf: invf0, + envelope: env0, + noise: noise0, + add_harmonic: h0, + }, + SbrChannel { + grid: grid1, + dtdf: dtdf1, + invf: invf1, + envelope: env1, + noise: noise1, + add_harmonic: h1, + }, + ] + }; + + let extension = parse_extended_data(reader)?; + + Ok(SbrElement { + coupling, + channels, + extension, + }) + } +} + +/// The two `bs_add_harmonic_flag[ch]` blocks of a channel pair +/// (Table 4.66): each optionally followed by an `sbr_sinusoidal_coding` +/// of `NHigh` flags. +fn parse_pair_harmonics( + reader: &mut BitReader<'_>, + bands: &HiLoTables, +) -> Result<(Vec, Vec)> { + let h0 = if read_flag(reader)? { + parse_sinusoidal(reader, bands.n_high())? + } else { + Vec::new() + }; + let h1 = if read_flag(reader)? { + parse_sinusoidal(reader, bands.n_high())? + } else { + Vec::new() + }; + Ok((h0, h1)) +} + +/// The shared `if (bs_extended_data) { … }` block (Tables 4.65 / 4.66). +/// +/// Reads `bs_extension_size` (4 bits, extended by `bs_esc_count` when +/// `== 15`), then for the duration of the block reads `bs_extension_id` +/// (2 bits) and captures the body as raw bytes. The standardized PS +/// payload is not decoded here; the bytes are returned for a later +/// pass. +fn parse_extended_data(reader: &mut BitReader<'_>) -> Result> { + if !read_flag(reader)? { + return Ok(None); + } + let mut cnt = read(reader, 4)?; + if cnt == 15 { + cnt += read(reader, 8)?; + } + let mut num_bits_left = (8 * cnt) as i64; + // The while-loop in the spec reads one bs_extension_id then hands + // the rest to sbr_extension(); we capture the first id and then + // *every remaining bit* of the block. The extension payload (e.g. + // ps_data(), Table 8.A.1) is a bitstream that is NOT byte-aligned + // within the block — its final sub-byte shares a byte with the + // bs_fill_bits — so a whole-byte capture would truncate up to 7 + // trailing payload bits. The re-packed buffer is zero-padded to a + // byte; the payload parser consumes exactly the bits it needs and + // ignores the rest as fill. + let mut id = 0u8; + let mut data: Vec = Vec::new(); + if num_bits_left > 7 { + id = read(reader, 2)? as u8; + num_bits_left -= 2; + let mut w = oxideav_core::bits::BitWriter::new(); + while num_bits_left >= 8 { + w.write_u32(read(reader, 8)?, 8); + num_bits_left -= 8; + } + if num_bits_left > 0 { + let n = num_bits_left as u32; + w.write_u32(read(reader, n)?, n); + num_bits_left = 0; + } + data = w.finish(); + } + // bs_fill_bits: consume the remaining (< 8) bits of a block too + // short to carry an id. + if num_bits_left > 0 { + read(reader, num_bits_left as u32)?; + } + Ok(Some(SbrExtension { id, data })) +} + +#[inline] +fn read(reader: &mut BitReader<'_>, n: u32) -> Result { + reader.read_u32(n).map_err(|_| Error::SbrGridInvalid) +} + +#[inline] +fn read_flag(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::SbrGridInvalid) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sbr_freq_bands::{k0, k2, master_table, HiLoTables}; + use crate::sbr_grid::FrameClass; + use crate::sbr_huffman::{env_tables, noise_tables, SbrHuffContext}; + use oxideav_core::bits::{BitReader, BitWriter}; + + fn bands_44100() -> HiLoTables { + let k0v = k0(88_200, 5).unwrap(); + let k2v = k2(88_200, 5, k0v).unwrap(); + let fm = master_table(k0v, k2v, 0, false).unwrap(); + HiLoTables::derive(&fm, 1, 2).unwrap() + } + + fn push_code(w: &mut BitWriter, table: &[(u8, u32)], idx: usize) { + let (len, code) = table[idx]; + w.write_u32(code, len as u32); + } + + /// Write a minimal single-channel SBR element: no data_extra, a + /// FIXFIX single high-res envelope (forces amp_res=0), freq-coded + /// envelope + noise, no sinusoidal, no extended data. + fn write_minimal_sce(bands: &HiLoTables) -> Vec { + let n_high = bands.n_high(); + let n_q = bands.n_q(); + let mut w = BitWriter::new(); + w.write_bit(false); // bs_data_extra + // sbr_grid: FIXFIX, 2^0 = 1 env, freq_res high. + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(0, 2); // 1 env + w.write_bit(true); // freq_res[0] = high + // sbr_dtdf: 1 env flag + 1 noise flag, both freq (0). + w.write_bit(false); // df_env[0] + w.write_bit(false); // df_noise[0] + // sbr_invf: n_q 2-bit modes. + for _ in 0..n_q { + w.write_u32(1, 2); + } + // sbr_envelope: amp_res override → false; level start = 7 bits. + let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + w.write_u32(33, 7); // start value + for i in 1..n_high { + push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); + } + // sbr_noise: 5-bit start + (n_q-1) f deltas. + let ((_nt, _ntl), (nf, nfl)) = noise_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + w.write_u32(10, 5); + for i in 1..n_q { + push_code(&mut w, nf, (i + nfl as usize) % nf.len()); + } + w.write_bit(false); // bs_add_harmonic_flag[0] + w.write_bit(false); // bs_extended_data + w.finish() + } + + #[test] + fn single_channel_element_round_trips_structure() { + let bands = bands_44100(); + let bytes = write_minimal_sce(&bands); + let mut r = BitReader::new(&bytes); + // Header amp_res = true, but the single-env FIXFIX overrides it. + let el = SbrElement::parse_single(&mut r, &bands, true).unwrap(); + assert!(!el.coupling); + assert_eq!(el.channels.len(), 1); + let ch = &el.channels[0]; + assert_eq!(ch.grid.frame_class, FrameClass::FixFix); + assert_eq!(ch.grid.num_env, 1); + assert!(ch.grid.amp_res_override); + assert_eq!(ch.envelope.data[0].len(), bands.n_high()); + assert_eq!(ch.envelope.data[0][0], 33); + assert_eq!(ch.noise.data[0].len(), bands.n_q()); + assert_eq!(ch.noise.data[0][0], 10); + assert!(ch.add_harmonic.is_empty()); + assert!(el.extension.is_none()); + assert_eq!(ch.invf.invf_mode.len(), bands.n_q()); + } + + #[test] + fn single_channel_with_sinusoidal_and_extension() { + let bands = bands_44100(); + let n_high = bands.n_high(); + let n_q = bands.n_q(); + let mut w = BitWriter::new(); + w.write_bit(false); // data_extra + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(0, 2); + w.write_bit(true); + w.write_bit(false); // df_env + w.write_bit(false); // df_noise + for _ in 0..n_q { + w.write_u32(0, 2); + } + let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + w.write_u32(20, 7); + for i in 1..n_high { + push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); + } + let ((_nt, _ntl), (nf, nfl)) = noise_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + w.write_u32(5, 5); + for i in 1..n_q { + push_code(&mut w, nf, (i + nfl as usize) % nf.len()); + } + // Sinusoidal: flag set, then n_high bools (alternating). + w.write_bit(true); + for n in 0..n_high { + w.write_bit(n % 2 == 0); + } + // Extended data: id = PS, one body byte 0xA5, then byte-align. + w.write_bit(true); // bs_extended_data + w.write_u32(1, 4); // bs_extension_size = 1 byte (8 bits) + // 8 bits = id(2) + 6 fill bits; body has no full byte. + w.write_u32(EXTENSION_ID_PS as u32, 2); + w.write_u32(0, 6); // fill + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let el = SbrElement::parse_single(&mut r, &bands, false).unwrap(); + let ch = &el.channels[0]; + assert_eq!(ch.add_harmonic.len(), n_high); + assert!(ch.add_harmonic[0]); + assert!(!ch.add_harmonic[1]); + let ext = el.extension.unwrap(); + assert_eq!(ext.id, EXTENSION_ID_PS); + } + + #[test] + fn channel_pair_independent_grids() { + let bands = bands_44100(); + let n_high = bands.n_high(); + let n_q = bands.n_q(); + let mut w = BitWriter::new(); + w.write_bit(false); // data_extra + w.write_bit(false); // bs_coupling = 0 (independent) + // grid0: FIXFIX 1 env high. + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(0, 2); + w.write_bit(true); + // grid1: FIXFIX 1 env high. + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(0, 2); + w.write_bit(true); + // dtdf0, dtdf1 (1 env + 1 noise each), all freq. + w.write_bit(false); + w.write_bit(false); + w.write_bit(false); + w.write_bit(false); + // invf0, invf1. + for _ in 0..n_q { + w.write_u32(2, 2); + } + for _ in 0..n_q { + w.write_u32(3, 2); + } + let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + let ((_nt, _ntl), (nf, nfl)) = noise_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + // env0, env1. + for &start in &[30u32, 40] { + w.write_u32(start, 7); + for i in 1..n_high { + push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); + } + } + // noise0, noise1. + for &start in &[8u32, 9] { + w.write_u32(start, 5); + for i in 1..n_q { + push_code(&mut w, nf, (i + nfl as usize) % nf.len()); + } + } + w.write_bit(false); // harmonic flag ch0 + w.write_bit(false); // harmonic flag ch1 + w.write_bit(false); // extended data + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let el = SbrElement::parse_pair(&mut r, &bands, false).unwrap(); + assert!(!el.coupling); + assert_eq!(el.channels.len(), 2); + assert_eq!(el.channels[0].envelope.data[0][0], 30); + assert_eq!(el.channels[1].envelope.data[0][0], 40); + assert_eq!(el.channels[0].noise.data[0][0], 8); + assert_eq!(el.channels[1].noise.data[0][0], 9); + assert_eq!(el.channels[0].invf.invf_mode, vec![2u8; n_q]); + assert_eq!(el.channels[1].invf.invf_mode, vec![3u8; n_q]); + } + + #[test] + fn truncated_element_errors() { + let bands = bands_44100(); + let bytes = [0u8; 0]; + let mut r = BitReader::new(&bytes); + assert!(matches!( + SbrElement::parse_single(&mut r, &bands, true), + Err(Error::SbrGridInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_env_adjust.rs b/crates/vendor/oxideav-aac/src/sbr_env_adjust.rs new file mode 100644 index 00000000..a1396126 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_env_adjust.rs @@ -0,0 +1,1029 @@ +//! SBR HF adjustment (envelope adjuster) — ISO/IEC 14496-3 §4.6.18.7. +//! +//! Takes the HF-generated subband matrix `XHigh` and produces the +//! output matrix `Y` over the `M` SBR subbands starting at `kx`: +//! +//! * **Mapping** (§4.6.18.7.2) — `EOrigMapped` / `QMapped` to QMF +//! resolution, the `SIndexMapped` sinusoid placement (band middle, +//! `δStep` start gate against `lA` and the previous frame's +//! sinusoids) and the `SMapped` band flags. +//! * **Current envelope estimation** (§4.6.18.7.3) — `ECurr` by +//! squared-magnitude averaging, per subband (`bs_interpol_freq = 1`) +//! or per envelope band. +//! * **Additional-component levels** (§4.6.18.7.4) — `QM` / `SM` +//! (amplitude domain, i.e. with the square root of the energy +//! ratios). +//! * **Gain** (§4.6.18.7.5) — `G`, the limiter (`GMax` from the +//! `fTableLim` band ratios and `limGain`), the noise-level limit +//! `QM_Lim`, and the boost compensation `GBoost` capped at +//! `1.584893192`. +//! * **Assembly** (§4.6.18.7.6) — the `hSmooth` gain/noise smoothing +//! over `hSL` columns, `W1 = GFilt·XHigh`, the Table 4.A.91 noise +//! mix `W2`, and the `φsin` sinusoid injection with the +//! `(−1)^(m+kx)` imaginary alternation, producing `Y`. +//! +//! **Low-power mode** (§4.6.18.8, `EnvParams::low_power`): the energy +//! estimation carries the §4.6.18.8.4 factor 2 (real-valued subband +//! signals hold half the energy of the complex representation), gain +//! smoothing is disabled regardless of `bs_smoothing_mode`, the +//! §4.6.18.8.5 aliasing reduction re-computes the limiter/boost gains +//! over the Figure 4.54 groups (driven by the caller-supplied +//! `degPatched`), the Table 4.A.91 noise mix keeps only its real +//! part, and the sinusoid injection follows the §4.6.18.8.5 modified +//! equations — real-valued `ψm` with the `−0.00815·(−1)^(m+kx)` +//! neighbour correction, applied to the first 16 sinusoids per time +//! segment, spilling into subbands `kx − 1` and `kx + M`. +//! +//! Cross-frame state (`EnvAdjustState`) carries the previous frame's +//! last-envelope `SIndexMapped`, `lA` / `LE`, the `GTemp` / `QTemp` +//! smoothing tails, and the running `indexNoise` / `indexSine`. +//! +//! ## Provenance +//! +//! Every formula (including the square roots the §4.6.18.7.4–7.5 +//! equations carry) was read from the staged ISO/IEC 14496-3:2009 spec +//! PDF's typeset equations. No part of this implementation is derived +//! from any external decoder. + +use crate::sbr_freq_bands::HiLoTables; +use crate::sbr_hf_gen::T_HF_ADJ; +use crate::sbr_lp::{aliasing_reduction, gain_groups}; +use crate::sbr_noise_table::NOISE_TABLE; +use crate::sbr_qmf::Complex; +use crate::{Error, Result}; + +/// `limGain = [0.70795, 1.0, 1.41254, 1e10]` (§4.6.18.7.5). +pub const LIM_GAIN: [f64; 4] = [0.70795, 1.0, 1.41254, 1e10]; + +/// `ε0 = 1e-12` (§4.6.18.7.5). +pub const EPS0: f64 = 1e-12; + +/// `ε = 1` (§4.6.18.2.5) — the division-by-zero guard in the gain. +pub const EPS: f64 = 1.0; + +/// The `GBoost` cap `1.584893192` (§4.6.18.7.5). +pub const MAX_BOOST: f64 = 1.584893192; + +/// The `GMax` cap `10^5` (§4.6.18.7.5). +pub const G_MAX_CAP: f64 = 1e5; + +/// `hSmooth` — the §4.6.18.7.6 smoothing filter. +pub const H_SMOOTH: [f64; 5] = [ + 0.33333333333333, + 0.30150283239582, + 0.21816949906249, + 0.11516383427084, + 0.03183050093751, +]; + +/// `φRe,sin = [1, 0, −1, 0]`, `φIm,sin = [0, 1, 0, −1]` (§4.6.18.7.6). +pub const PHI_SIN: [(f64, f64); 4] = [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)]; + +/// Per-frame inputs to the envelope adjuster (one channel). +#[derive(Debug)] +pub struct EnvParams<'a> { + /// Derived frequency band tables (`M`, `kx`, high/low/noise). + pub bands: &'a HiLoTables, + /// The §4.6.18.3.2.3 limiter band table `fTableLim(0..=NL)`. + pub f_table_lim: &'a [i32], + /// Envelope time borders `tE(0..=LE)` (slots). + pub t_e: &'a [i32], + /// Noise-floor time borders `tQ(0..=LQ)` (slots). + pub t_q: &'a [i32], + /// Per-envelope frequency resolution `r(l)` (`true` = high). + pub freq_res: &'a [bool], + /// Table 4.176 `lA` (`-1` = none). + pub l_a: i32, + /// Dequantized envelope energies `EOrig[l][k]`. + pub e_orig: &'a [Vec], + /// Dequantized noise-floor energies `QOrig[l][k]`. + pub q_orig: &'a [Vec], + /// `bs_add_harmonic` flags (`NHigh` entries; empty = none). + pub add_harmonic: &'a [bool], + /// `bs_interpol_freq`. + pub interpol_freq: bool, + /// `bs_smoothing_mode` (`true` ⇒ `hSL = 0`). + pub smoothing_mode: bool, + /// `bs_limiter_gains` (`0..=3`, indexes [`LIM_GAIN`]). + pub limiter_gains: u8, + /// The §4.6.18.3.3 reset flag (header band geometry changed). + pub reset: bool, + /// §4.6.18.8 low-power mode: ×2 energy estimation, no gain + /// smoothing, aliasing reduction, real-only noise, and the + /// modified sinusoid injection. + pub low_power: bool, + /// The §4.6.18.8.3 `degPatched` (`kx`-relative, `M` entries) — + /// required when `low_power` is set. + pub deg_patched: Option<&'a [f64]>, +} + +/// Cross-frame envelope-adjuster state for one channel. +#[derive(Debug, Clone, Default)] +pub struct EnvAdjustState { + /// Previous frame's last-envelope `SIndexMapped` (per SBR subband, + /// `kx`-relative) plus its `kx`, for the `δStep` gate. + s_index_prev: Vec, + k_x_prev: i32, + /// Previous frame's `lA` and `LE` (for `lAPrev`). + l_a_prev_frame: i32, + l_e_prev: i32, + /// Previous frame's trailing `hSL` columns of `GTemp` / `QTemp`. + g_temp_tail: Vec>, + q_temp_tail: Vec>, + /// Running noise / sine phase indices. + index_noise: usize, + index_sine: usize, + started: bool, +} + +impl EnvAdjustState { + /// Fresh state (first frame / after a stream reset). + #[must_use] + pub fn new() -> Self { + Self::default() + } +} + +/// Run the §4.6.18.7 HF adjustment for one channel's SBR frame. +/// +/// `x_high` is the slot-major HF-generator output (spec absolute +/// columns, i.e. spec index `i + tHFAdj` is a direct column index). +/// Returns `Y` in the same layout, filled for the SBR range and the +/// frame's envelope span; other cells are zero. +pub fn adjust( + x_high: &[[Complex; 64]], + p: &EnvParams<'_>, + st: &mut EnvAdjustState, +) -> Result> { + let m_cnt = usize::try_from(p.bands.m).map_err(|_| Error::SbrFreqBandInvalid)?; + let k_x = p.bands.k_x; + let l_e = p + .t_e + .len() + .checked_sub(1) + .ok_or(Error::SbrFreqBandInvalid)?; + if l_e == 0 + || p.freq_res.len() != l_e + || p.e_orig.len() != l_e + || p.q_orig.len() + 1 != p.t_q.len() + || p.f_table_lim.len() < 2 + || usize::from(p.limiter_gains) >= LIM_GAIN.len() + { + return Err(Error::SbrFreqBandInvalid); + } + + let rate = 2i32; // RATE (§4.6.18.2.5) + let i0 = rate * p.t_e[0]; + let i_end = rate * p.t_e[l_e]; + let n_cols = usize::try_from(i_end - i0).map_err(|_| Error::SbrFreqBandInvalid)?; + if i0 < 0 + || usize::try_from(i_end).map_err(|_| Error::SbrFreqBandInvalid)? + T_HF_ADJ > x_high.len() + { + return Err(Error::SbrFreqBandInvalid); + } + + if p.reset || !st.started { + st.index_noise = 0; + st.index_sine = 0; + st.s_index_prev.clear(); + st.g_temp_tail.clear(); + st.q_temp_tail.clear(); + st.l_a_prev_frame = -1; + st.l_e_prev = 0; + st.started = true; + } + + // lAPrev: 0 if the previous frame's transient sat on its trailing + // border, else -1. + let l_a_prev = if st.l_a_prev_frame == st.l_e_prev { + 0i32 + } else { + -1 + }; + + // ---- §4.6.18.7.2 mapping ------------------------------------- + // Envelope band table per resolution. + let f_of = |high: bool| -> &Vec { + if high { + &p.bands.f_table_high + } else { + &p.bands.f_table_low + } + }; + // Band index of QMF subband `k` in border table `f`. + let band_of = |f: &[i32], k: i32| -> Result { + for i in 0..f.len() - 1 { + if f[i] <= k && k < f[i + 1] { + return Ok(i); + } + } + Err(Error::SbrFreqBandInvalid) + }; + + let mut e_map = vec![vec![0.0f64; m_cnt]; l_e]; // EOrigMapped[l][m] + let mut q_map = vec![vec![0.0f64; m_cnt]; l_e]; // QMapped[l][m] + let mut s_index = vec![vec![false; m_cnt]; l_e]; // SIndexMapped[l][m] + let mut s_map = vec![vec![false; m_cnt]; l_e]; // SMapped[l][m] + + let n_high = p.bands.n_high(); + for l in 0..l_e { + let f = f_of(p.freq_res[l]); + if p.e_orig[l].len() + 1 != f.len() { + return Err(Error::SbrFreqBandInvalid); + } + // k(l): the noise floor whose span contains envelope l. + let mut kq = None; + for q in 0..p.t_q.len() - 1 { + if p.t_q[q] <= p.t_e[l] && p.t_e[l + 1] <= p.t_q[q + 1] { + kq = Some(q); + break; + } + } + let kq = kq.ok_or(Error::SbrFreqBandInvalid)?; + if p.q_orig[kq].len() + 1 != p.bands.f_table_noise.len() { + return Err(Error::SbrFreqBandInvalid); + } + for m in 0..m_cnt { + let k = k_x + i32::try_from(m).map_err(|_| Error::SbrFreqBandInvalid)?; + e_map[l][m] = p.e_orig[l][band_of(f, k)?]; + q_map[l][m] = p.q_orig[kq][band_of(&p.bands.f_table_noise, k)?]; + } + + // SIndexMapped: sinusoid in the middle subband of each + // high-resolution band, gated by δStep. + if !p.add_harmonic.is_empty() { + if p.add_harmonic.len() != n_high { + return Err(Error::SbrFreqBandInvalid); + } + for (i, &on) in p.add_harmonic.iter().enumerate() { + if !on { + continue; + } + let mid = (p.bands.f_table_high[i + 1] + p.bands.f_table_high[i]) / 2; + let m_rel = mid - k_x; + if m_rel < 0 || m_rel as usize >= m_cnt { + continue; + } + // δStep: on from lA, or already ringing in the + // previous frame's last envelope. + let prev_on = { + let prev_rel = mid - st.k_x_prev; + prev_rel >= 0 + && st + .s_index_prev + .get(prev_rel as usize) + .copied() + .unwrap_or(false) + }; + if (l as i32) >= p.l_a || prev_on { + s_index[l][m_rel as usize] = true; + } + } + } + // SMapped: any sinusoid within the envelope band. + for i in 0..f.len() - 1 { + let any = ((f[i] - k_x).max(0)..(f[i + 1] - k_x).max(0)) + .any(|j| (j as usize) < m_cnt && s_index[l][j as usize]); + if any { + for j in (f[i] - k_x).max(0)..(f[i + 1] - k_x).max(0) { + if (j as usize) < m_cnt { + s_map[l][j as usize] = true; + } + } + } + } + } + + // ---- §4.6.18.7.3 current envelope ---------------------------- + // §4.6.18.8.4: the real-valued low-power signals carry half the + // energy of the complex representation — the estimation doubles. + let e_scale = if p.low_power { 2.0 } else { 1.0 }; + let mut e_curr = vec![vec![0.0f64; m_cnt]; l_e]; + for (l, e_curr_l) in e_curr.iter_mut().enumerate() { + let lo = (rate * p.t_e[l] + T_HF_ADJ as i32) as usize; + let hi = (rate * p.t_e[l + 1] + T_HF_ADJ as i32) as usize; + let width = (hi - lo) as f64; + if p.interpol_freq { + for (m, e) in e_curr_l.iter_mut().enumerate() { + let k = (k_x as usize) + m; + let sum: f64 = x_high[lo..hi].iter().map(|col| col[k].norm_sqr()).sum(); + *e = e_scale * sum / width; + } + } else { + let f = f_of(p.freq_res[l]); + for pband in 0..f.len() - 1 { + let kl = f[pband]; + let kh = f[pband + 1] - 1; + let mut sum = 0.0; + for j in kl..=kh { + sum += x_high[lo..hi] + .iter() + .map(|col| col[j as usize].norm_sqr()) + .sum::(); + } + let avg = e_scale * sum / (width * f64::from(kh - kl + 1)); + for j in kl..=kh { + let m_rel = j - k_x; + if m_rel >= 0 && (m_rel as usize) < m_cnt { + e_curr_l[m_rel as usize] = avg; + } + } + } + } + } + + // ---- §4.6.18.7.4 / 7.5 gain, limiter, boost ------------------ + let lim_gain = LIM_GAIN[usize::from(p.limiter_gains)]; + let n_l = p.f_table_lim.len() - 1; + + let mut g_lim_boost = vec![vec![0.0f64; m_cnt]; l_e]; + let mut q_m_lim_boost = vec![vec![0.0f64; m_cnt]; l_e]; + let mut s_m_boost = vec![vec![0.0f64; m_cnt]; l_e]; + + for l in 0..l_e { + let li = l as i32; + let delta_l = if li == p.l_a || li == l_a_prev { + 0.0 + } else { + 1.0 + }; + + // QM / SM (amplitude domain). + let mut q_m = vec![0.0f64; m_cnt]; + let mut s_m = vec![0.0f64; m_cnt]; + let mut g = vec![0.0f64; m_cnt]; + for m in 0..m_cnt { + let e_o = e_map[l][m]; + let q = q_map[l][m]; + q_m[m] = (e_o * q / (1.0 + q)).sqrt(); + s_m[m] = if s_index[l][m] { + (e_o / (1.0 + q)).sqrt() + } else { + 0.0 + }; + g[m] = if s_map[l][m] { + ((e_o / (EPS + e_curr[l][m])) * (q / (1.0 + q))).sqrt() + } else { + (e_o / ((EPS + e_curr[l][m]) * (1.0 + delta_l * q))).sqrt() + }; + } + + // Limiter-band maxima. + let mut g_max = vec![0.0f64; m_cnt]; + for k in 0..n_l { + let lo = (p.f_table_lim[k] - k_x).max(0) as usize; + let hi = ((p.f_table_lim[k + 1] - k_x).max(0) as usize).min(m_cnt); + let num: f64 = EPS0 + e_map[l][lo..hi].iter().sum::(); + let den: f64 = EPS0 + e_curr[l][lo..hi].iter().sum::(); + let gmax = ((num / den).sqrt() * lim_gain).min(G_MAX_CAP); + for gm in &mut g_max[lo..hi] { + *gm = gmax; + } + } + + // QM_Lim / GLim. + let mut q_m_lim = vec![0.0f64; m_cnt]; + let mut g_lim = vec![0.0f64; m_cnt]; + for m in 0..m_cnt { + q_m_lim[m] = if g[m] > 0.0 { + q_m[m].min(q_m[m] * g_max[m] / g[m]) + } else { + q_m[m] + }; + g_lim[m] = g[m].min(g_max[m]); + } + + // Boost per limiter band. + for k in 0..n_l { + let lo = (p.f_table_lim[k] - k_x).max(0) as usize; + let hi = ((p.f_table_lim[k + 1] - k_x).max(0) as usize).min(m_cnt); + let mut num = EPS0; + let mut den = EPS0; + for i in lo..hi { + num += e_map[l][i]; + let delta_s = if s_m[i] != 0.0 || li == p.l_a || li == l_a_prev { + 0.0 + } else { + 1.0 + }; + den += e_curr[l][i] * g_lim[i] * g_lim[i] + + s_m[i] * s_m[i] + + delta_s * q_m_lim[i] * q_m_lim[i]; + } + let boost = (num / den).sqrt().min(MAX_BOOST); + for i in lo..hi { + g_lim_boost[l][i] = g_lim[i] * boost; + q_m_lim_boost[l][i] = q_m_lim[i] * boost; + s_m_boost[l][i] = s_m[i] * boost; + } + } + } + + // ---- §4.6.18.8.5 aliasing reduction (low power) -------------- + // GA replaces GLimBoost in the assembly below. + if p.low_power { + let dp = p.deg_patched.ok_or(Error::SbrFreqBandInvalid)?; + if dp.len() != m_cnt { + return Err(Error::SbrFreqBandInvalid); + } + for l in 0..l_e { + let groups = gain_groups(dp, &s_map[l], k_x); + aliasing_reduction(&mut g_lim_boost[l], &e_curr[l], dp, &groups, k_x)?; + } + } + + // ---- §4.6.18.7.6 assembly ------------------------------------ + // §4.6.18.8.5: the low-power tool never smooths, regardless of + // bs_smoothing_mode. + let h_sl: usize = if p.smoothing_mode || p.low_power { + 0 + } else { + 4 + }; + + // GTemp / QTemp with the hSL-column prefix. + let mut g_temp = vec![vec![0.0f64; m_cnt]; n_cols + h_sl]; + let mut q_temp = vec![vec![0.0f64; m_cnt]; n_cols + h_sl]; + for j in 0..h_sl { + if st.g_temp_tail.len() == h_sl && st.g_temp_tail[j].len() == m_cnt { + g_temp[j].clone_from(&st.g_temp_tail[j]); + q_temp[j].clone_from(&st.q_temp_tail[j]); + } else { + // Reset (or first frame): prefix = first column values. + g_temp[j].clone_from(&g_lim_boost[0]); + q_temp[j].clone_from(&q_m_lim_boost[0]); + } + } + // Envelope of column i (spec index space i0..i_end). + let env_of = |i: i32| -> usize { + let mut l = l_e - 1; + for e in 0..l_e { + if i >= rate * p.t_e[e] && i < rate * p.t_e[e + 1] { + l = e; + break; + } + } + l + }; + for c in 0..n_cols { + let l = env_of(i0 + c as i32); + g_temp[c + h_sl].clone_from(&g_lim_boost[l]); + q_temp[c + h_sl].clone_from(&q_m_lim_boost[l]); + } + + // §4.6.18.8.5: the modified sinusoid equations apply to the first + // 16 sinusoids (in increasing frequency order) of every time + // segment; later sinusoids keep the original (real-part) term. + let lp_first16: Vec> = if p.low_power { + s_index + .iter() + .map(|row| { + let mut count = 0usize; + row.iter() + .map(|&on| { + if on { + count += 1; + count <= 16 + } else { + false + } + }) + .collect() + }) + .collect() + } else { + Vec::new() + }; + + let mut y = vec![[Complex::default(); 64]; x_high.len()]; + let mut f_index_noise = 0usize; + let mut f_index_sine = 0usize; + for c in 0..n_cols { + let i = i0 + c as i32; + let l = env_of(i); + let li = l as i32; + let col = (i + T_HF_ADJ as i32) as usize; + let smooth_gain = li != p.l_a && li != l_a_prev && h_sl != 0; + f_index_sine = (st.index_sine + c) % 4; + let (sin_re, sin_im) = PHI_SIN[f_index_sine]; + for m in 0..m_cnt { + let k = (k_x as usize) + m; + // GFilt. + let g_filt = if smooth_gain { + (0..=h_sl) + .map(|j| g_temp[c + h_sl - j][m] * H_SMOOTH[j]) + .sum::() + } else { + g_temp[c + h_sl][m] + }; + // QFilt: zero on transient envelopes and sinusoid bands. + let q_filt = if li == p.l_a || li == l_a_prev || s_m_boost[l][m] != 0.0 { + 0.0 + } else if h_sl != 0 { + (0..=h_sl) + .map(|j| q_temp[c + h_sl - j][m] * H_SMOOTH[j]) + .sum::() + } else { + q_temp[c + h_sl][m] + }; + + // W1 = GFilt · XHigh. + let w1 = x_high[col][k] * g_filt; + + // W2 = W1 + QFilt · V(fIndexNoise). The low-power tool + // ignores every imaginary part (§4.6.18.8.1). + f_index_noise = (st.index_noise + c * m_cnt + m + 1) % 512; + let (v_re, v_im) = NOISE_TABLE[f_index_noise]; + let mut out = if p.low_power { + Complex::new(w1.re + q_filt * v_re, 0.0) + } else { + Complex::new(w1.re + q_filt * v_re, w1.im + q_filt * v_im) + }; + + // Y = W2 + ψ (sinusoids; the low-power injection runs as + // a separate per-column pass below). + if !p.low_power && s_index[l][m] { + let s = s_m_boost[l][m]; + let alt = if (m + k_x as usize) % 2 == 1 { + -1.0 + } else { + 1.0 + }; + out.re += s * sin_re; + out.im += s * alt * sin_im; + } + y[col][k] = out; + } + + if p.low_power { + // §4.6.18.8.5 sinusoid injection: real-valued ψm with the + // −0.00815·(−1)^(m+kx) neighbour correction, over targets + // m ∈ −1..=M — spilling into the lowband subband kx − 1 + // and the subband kx + M just above the SBR range. + let phi_re_at = |off: i64| -> f64 { + let idx = (st.index_sine as i64 + c as i64 + off).rem_euclid(4) as usize; + PHI_SIN[idx].0 + }; + let f0 = phi_re_at(0); + let fm1 = phi_re_at(-1); + let fp1 = phi_re_at(1); + let first16 = &lp_first16[l]; + // ψRe of the (first-16) sinusoid in band m, else 0. + let s16 = |m: i64| -> f64 { + if m >= 0 && (m as usize) < m_cnt && first16[m as usize] { + s_m_boost[l][m as usize] + } else { + 0.0 + } + }; + for t in -1..=(m_cnt as i64) { + let band = i64::from(k_x) + t; + if !(0..64).contains(&band) { + continue; + } + let alt = if band.rem_euclid(2) == 1 { -1.0 } else { 1.0 }; + let psi = s16(t) * f0 - 0.00815 * alt * (s16(t - 1) * fm1 + s16(t + 1) * fp1); + if psi != 0.0 { + y[col][band as usize].re += psi; + } + } + // Sinusoids beyond the sixteenth keep the original term + // (real part only). + for (m, &on) in s_index[l].iter().enumerate() { + if on && !first16[m] { + y[col][(k_x as usize) + m].re += s_m_boost[l][m] * f0; + } + } + } + } + + // ---- thread cross-frame state -------------------------------- + st.index_noise = if n_cols > 0 { + f_index_noise + } else { + st.index_noise + }; + st.index_sine = if n_cols > 0 { + (f_index_sine + 1) % 4 + } else { + st.index_sine + }; + st.g_temp_tail = g_temp[n_cols..].to_vec(); + st.q_temp_tail = q_temp[n_cols..].to_vec(); + st.s_index_prev = s_index[l_e - 1].clone(); + st.k_x_prev = k_x; + st.l_a_prev_frame = p.l_a; + st.l_e_prev = l_e as i32; + + Ok(y) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bands() -> HiLoTables { + HiLoTables { + f_table_high: vec![8, 10, 12, 14, 16], + f_table_low: vec![8, 12, 16], + f_table_noise: vec![8, 16], + m: 8, + k_x: 8, + } + } + + fn flat_x_high(amp: f64, cols: usize) -> Vec<[Complex; 64]> { + let mut x = vec![[Complex::default(); 64]; cols]; + for (ci, col) in x.iter_mut().enumerate() { + for (k, cell) in col.iter_mut().enumerate().take(16).skip(8) { + // A deterministic unit-magnitude phase pattern. + let ph = (ci * 7 + k) as f64 * 0.37; + *cell = Complex::new(amp * ph.cos(), amp * ph.sin()); + } + } + x + } + + #[allow(clippy::too_many_arguments)] + fn params<'a>( + b: &'a HiLoTables, + lim: &'a [i32], + t_e: &'a [i32], + t_q: &'a [i32], + freq_res: &'a [bool], + e_orig: &'a [Vec], + q_orig: &'a [Vec], + add: &'a [bool], + ) -> EnvParams<'a> { + EnvParams { + bands: b, + f_table_lim: lim, + t_e, + t_q, + freq_res, + l_a: -1, + e_orig, + q_orig, + add_harmonic: add, + interpol_freq: true, + smoothing_mode: true, + limiter_gains: 3, + reset: false, + low_power: false, + deg_patched: None, + } + } + + /// A flat XHigh with EOrig = G²·|X|² reproduces gain G on every + /// sample (no noise, no sinusoids, limiter wide open). + #[test] + fn flat_gain_reproduces_target_envelope() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let amp = 100.0; + let target_gain = 3.0; + // EOrig is an energy: G = sqrt(EOrig / (ε + |X|²)). + let e_target = target_gain * target_gain * (amp * amp + EPS); + let e_orig = vec![vec![e_target; 4]]; + let q_orig = vec![vec![0.0]]; + let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); + let x = flat_x_high(amp, 40); + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + // The boost ratio uses the raw energies (no ε), so the exact + // applied gain is target·sqrt((amp² + ε)/amp²); pin to 1e-3. + for c in 0..32usize { + let col = c + T_HF_ADJ; + for k in 8..16 { + let g = (y[col][k].norm_sqr() / x[col][k].norm_sqr()).sqrt(); + assert!( + (g - target_gain).abs() < 1e-3 * target_gain, + "col {col} k {k}: gain {g}" + ); + } + } + } + + /// Per-envelope gains switch exactly at the tE border. + #[test] + fn gain_switches_at_envelope_border() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 8, 16]; + let t_q = [0, 8, 16]; + let fr = [true, true]; + let amp = 50.0; + let e0 = 4.0 * (amp * amp + EPS); + let e1 = 25.0 * (amp * amp + EPS); + let e_orig = vec![vec![e0; 4], vec![e1; 4]]; + let q_orig = vec![vec![0.0], vec![0.0]]; + let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); + let x = flat_x_high(amp, 40); + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + // Slots 0..16 → gain 2; slots 16..32 → gain 5. + let g_at = |c: usize| { + let col = c + T_HF_ADJ; + (y[col][9].norm_sqr() / x[col][9].norm_sqr()).sqrt() + }; + assert!((g_at(3) - 2.0).abs() < 1e-2); + assert!((g_at(15) - 2.0).abs() < 1e-2); + assert!((g_at(16) - 5.0).abs() < 2e-2); + assert!((g_at(31) - 5.0).abs() < 2e-2); + } + + /// The limiter clamps a runaway per-subband gain to the + /// limiter-band average, and the boost compensates the band's + /// total energy (up to the 1.584893192 cap). + #[test] + fn limiter_clamps_and_boost_compensates() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + // Band 0 demands a huge gain (XHigh is tiny there), bands 1..4 + // are ordinary. limiter_gains = 1 → limGain = 1.0. + let amp = 10.0; + let mut x = flat_x_high(amp, 40); + for col in x.iter_mut() { + for cell in &mut col[8..10] { + *cell = *cell * 1e-6; + } + } + let e_orig = vec![vec![ + 400.0 * (amp * amp), + 400.0 * (amp * amp), + 400.0 * (amp * amp), + 400.0 * (amp * amp), + ]]; + let q_orig = vec![vec![0.0]]; + let mut p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); + p.limiter_gains = 1; + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + // Unclamped G in the dead band would be ≈ 2e7; the limiter-band + // average cap is far smaller, so the dead band's output stays + // bounded by GMax·|X| ≪ 1 with boost ≤ MAX_BOOST. + for c in 0..32usize { + let col = c + T_HF_ADJ; + assert!(y[col][8].norm_sqr() < 1.0); + // The healthy bands keep a finite, boosted gain. + assert!(y[col][12].norm_sqr().is_finite()); + } + } + + /// A pure noise band (XHigh = 0, QOrig ≫) synthesises Table 4.A.91 + /// noise at the QM level, and the running index threads across + /// frames. + #[test] + fn noise_floor_synthesis_and_index_threading() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let e_orig = vec![vec![64.0; 4]]; + let q_orig = vec![vec![1.0]]; // QMapped = 1 → QM = sqrt(64/2) + let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); + let x = vec![[Complex::default(); 64]; 40]; + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + // First sample: fIndexNoise = 0·8 + 0 + 1 = 1. + let qm = (64.0f64 * 1.0 / 2.0).sqrt(); + // Boost over the limiter band: num = Σ EOrig = 8·64, den = + // Σ QM² = 8·32 → GBoost = √2 (below the cap). + let expect = qm * 2.0f64.sqrt(); + let (v_re, v_im) = NOISE_TABLE[1]; + let got = y[T_HF_ADJ][8]; + assert!((got.re - expect * v_re).abs() < 1e-9, "{got:?}"); + assert!((got.im - expect * v_im).abs() < 1e-9); + // Last index this frame: (31·8 + 7 + 1) mod 512 = 256. + assert_eq!(st.index_noise, 256); + // Second frame continues from 256. + let y2 = adjust(&x, &p, &mut st).unwrap(); + let (v_re2, v_im2) = NOISE_TABLE[257]; + let got2 = y2[T_HF_ADJ][8]; + assert!((got2.re - expect * v_re2).abs() < 1e-9); + assert!((got2.im - expect * v_im2).abs() < 1e-9); + } + + /// An additional sinusoid lands in the middle subband of its + /// high-res band with the [1, 0, −1, 0] / (−1)^(m+kx) pattern and + /// the cross-frame indexSine advance. + #[test] + fn sinusoid_injection_pattern() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let e_orig = vec![vec![64.0; 4]]; + let q_orig = vec![vec![0.0]]; + // Harmonic in high band 1 → mid subband (10 + 12)/2 = 11. + let add = [false, true, false, false]; + let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &add); + let x = vec![[Complex::default(); 64]; 40]; + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + let s = 64.0f64.sqrt() * MAX_BOOST; // SM boosted (ECurr = 0) + // m + kx = 11 (odd) → imaginary part sign-flipped. + // c = 0: φ = (1, 0); c = 1: φ = (0, 1) → im = −s. + let y0 = y[T_HF_ADJ][11]; + let y1 = y[T_HF_ADJ + 1][11]; + assert!((y0.re - s).abs() < 1e-9 && y0.im.abs() < 1e-12, "{y0:?}"); + assert!(y1.re.abs() < 1e-12 && (y1.im + s).abs() < 1e-9, "{y1:?}"); + // Other bands carry no sinusoid. + assert_eq!(y[T_HF_ADJ][9], Complex::default()); + // indexSine advances past the frame: (31 % 4 + 1) % 4 = 0. + assert_eq!(st.index_sine, 0); + // Next frame: still ringing (prev SIndexMapped carries over) + // even though l_a stays -1. + let y2 = adjust(&x, &p, &mut st).unwrap(); + assert!(y2[T_HF_ADJ][11].norm_sqr() > 0.0); + } + + /// Smoothing mode 0 (hSL = 4) filters a gain step across the + /// carry, and the second frame consumes the previous tail. + #[test] + fn smoothing_carries_across_frames() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let amp = 10.0; + let x = flat_x_high(amp, 40); + let e_lo = vec![vec![1.0 * (amp * amp + EPS); 4]]; + let e_hi = vec![vec![100.0 * (amp * amp + EPS); 4]]; + let q_orig = vec![vec![0.0]]; + let mut p1 = params(&b, &lim, &t_e, &t_q, &fr, &e_lo, &q_orig, &[]); + p1.smoothing_mode = false; + let mut st = EnvAdjustState::new(); + let _ = adjust(&x, &p1, &mut st).unwrap(); + assert_eq!(st.g_temp_tail.len(), 4); + // Second frame jumps to gain 10; the first output columns are + // still pulled down by the smoothing history (gain < 10). + let mut p2 = params(&b, &lim, &t_e, &t_q, &fr, &e_hi, &q_orig, &[]); + p2.smoothing_mode = false; + let y = adjust(&x, &p2, &mut st).unwrap(); + let g0 = (y[T_HF_ADJ][9].norm_sqr() / x[T_HF_ADJ][9].norm_sqr()).sqrt(); + let g_late = (y[T_HF_ADJ + 20][9].norm_sqr() / x[T_HF_ADJ + 20][9].norm_sqr()).sqrt(); + assert!(g0 < 6.0, "g0 = {g0}"); + assert!((g_late - 10.0).abs() < 0.1, "g_late = {g_late}"); + } + + /// Low-power mode requires `deg_patched`, doubles the energy + /// estimation (§4.6.18.8.4: with EOrig = G²·(2|X|² + ε) the flat + /// gain lands on G), and produces a purely real Y. + #[test] + fn low_power_energy_doubling_and_real_output() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let amp = 100.0; + let target_gain = 3.0; + let e_target = target_gain * target_gain * (2.0 * amp * amp + EPS); + let e_orig = vec![vec![e_target; 4]]; + let q_orig = vec![vec![0.0]]; + let dp = [0.0f64; 8]; + let mut p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); + p.low_power = true; + // deg_patched is mandatory in low-power mode. + let x = flat_x_high(amp, 40); + let mut st = EnvAdjustState::new(); + assert!(adjust(&x, &p, &mut st).is_err()); + p.deg_patched = Some(&dp); + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + for c in 0..32usize { + let col = c + T_HF_ADJ; + for cell in &y[col][8..16] { + assert_eq!(cell.im, 0.0, "LP Y must be real"); + } + } + // Noise-free path: the applied gain is uniform on the real + // part; ECurr = 2·|X|², so G = √(EOrig/(ε + 2·amp²)) = 3. + let g = (y[T_HF_ADJ][12].re / x[T_HF_ADJ][12].re).abs(); + let expect = (e_target / (EPS + 2.0 * amp * amp)).sqrt(); + assert!( + (g - expect).abs() < 1e-3 * expect, + "LP gain {g} vs expected {expect}" + ); + } + + /// The §4.6.18.8.5 sinusoid injection: real-valued main term on + /// the φRe cycle, the −0.00815 neighbour corrections one subband + /// away (with the (−1)^band alternation), and no imaginary part. + #[test] + fn low_power_sinusoid_injection_pattern() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let e_orig = vec![vec![64.0; 4]]; + let q_orig = vec![vec![0.0]]; + // Harmonic in high band 1 → mid subband (10 + 12)/2 = 11. + let add = [false, true, false, false]; + let dp = [0.0f64; 8]; + let mut p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &add); + p.low_power = true; + p.deg_patched = Some(&dp); + let x = vec![[Complex::default(); 64]; 40]; + let mut st = EnvAdjustState::new(); + let y = adjust(&x, &p, &mut st).unwrap(); + let s = 64.0f64.sqrt() * MAX_BOOST; // SM boosted (ECurr = 0) + + // c = 0: φRe(0) = 1 → main term s in band 11; φRe(±1) = 0 → + // no neighbour corrections. + assert!((y[T_HF_ADJ][11].re - s).abs() < 1e-9); + assert_eq!(y[T_HF_ADJ][11].im, 0.0); + assert_eq!(y[T_HF_ADJ][10].re, 0.0); + assert_eq!(y[T_HF_ADJ][12].re, 0.0); + + // c = 1: φRe(1) = 0 → no main term; band 10 sees the m+1 + // neighbour at i+1 (φRe(2) = −1): ψ = −0.00815·(+1)·(−s); + // band 12 sees the m−1 neighbour at i−1 (φRe(0) = 1): + // ψ = −0.00815·(+1)·(s). + let col1 = T_HF_ADJ + 1; + assert!(y[col1][11].re.abs() < 1e-12); + assert!( + (y[col1][10].re - 0.00815 * s).abs() < 1e-9, + "{}", + y[col1][10].re + ); + assert!( + (y[col1][12].re + 0.00815 * s).abs() < 1e-9, + "{}", + y[col1][12].re + ); + // Everything stays real. + for col in y.iter() { + for cell in col.iter() { + assert_eq!(cell.im, 0.0); + } + } + } + + /// LP mode never smooths: a gain step lands instantly even with + /// bs_smoothing_mode = 0, and the aliasing reduction equalizes a + /// full-degree group while preserving its output energy. + #[test] + fn low_power_no_smoothing_and_aliasing_reduction() { + let b = bands(); + let lim = [8, 16]; + let t_e = [0, 16]; + let t_q = [0, 16]; + let fr = [true]; + let amp = 10.0; + let x = flat_x_high(amp, 40); + let e_lo = vec![vec![2.0 * (amp * amp) + EPS; 4]]; + let e_hi = vec![vec![100.0 * (2.0 * (amp * amp) + EPS); 4]]; + let q_orig = vec![vec![0.0]]; + let dp = [0.0f64; 8]; + let mut p1 = params(&b, &lim, &t_e, &t_q, &fr, &e_lo, &q_orig, &[]); + p1.smoothing_mode = false; // requests smoothing… + p1.low_power = true; // …which LP overrides + p1.deg_patched = Some(&dp); + let mut st = EnvAdjustState::new(); + let _ = adjust(&x, &p1, &mut st).unwrap(); + // No smoothing tail is carried in LP mode. + assert!(st.g_temp_tail.is_empty()); + let mut p2 = params(&b, &lim, &t_e, &t_q, &fr, &e_hi, &q_orig, &[]); + p2.smoothing_mode = false; + p2.low_power = true; + p2.deg_patched = Some(&dp); + let y = adjust(&x, &p2, &mut st).unwrap(); + let g0 = (y[T_HF_ADJ][9].re / x[T_HF_ADJ][9].re).abs(); + assert!((g0 - 10.0).abs() < 0.5, "gain step not instant: {g0}"); + + // With a full-degree dp the group gains equalize but keep the + // envelope's output energy (checked via the flat spectrum). + let dp_full = [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]; + let e_skew = vec![vec![ + 1.0 * (2.0 * amp * amp + EPS), + 4.0 * (2.0 * amp * amp + EPS), + 9.0 * (2.0 * amp * amp + EPS), + 16.0 * (2.0 * amp * amp + EPS), + ]]; + let mut p3 = params(&b, &lim, &t_e, &t_q, &fr, &e_skew, &q_orig, &[]); + p3.low_power = true; + p3.deg_patched = Some(&dp_full); + let mut st3 = EnvAdjustState::new(); + let y3 = adjust(&x, &p3, &mut st3).unwrap(); + // Adjacent grouped subbands carry (near-)equal gains. + let g_at = |k: usize| (y3[T_HF_ADJ + 4][k].re / x[T_HF_ADJ + 4][k].re).abs(); + assert!( + (g_at(9) - g_at(10)).abs() < 1e-6 * g_at(9), + "grouped gains differ: {} vs {}", + g_at(9), + g_at(10) + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_envelope.rs b/crates/vendor/oxideav-aac/src/sbr_envelope.rs new file mode 100644 index 00000000..8d39c196 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_envelope.rs @@ -0,0 +1,410 @@ +//! `sbr_envelope()` / `sbr_noise()` raw decode — ISO/IEC 14496-3 +//! §4.4.2.8, Tables 4.72–4.73. +//! +//! These two elements carry the SBR spectral-envelope scalefactors and +//! noise-floor scalefactors as **delta values** (`bs_data_env` / +//! `bs_data_noise`). For each envelope (resp. noise floor) the delta +//! direction comes from `sbr_dtdf()` ([`crate::sbr_grid::SbrDtdf`]): +//! +//! * delta-in-**frequency** (`bs_df_* == 0`): the first band carries an +//! absolute *start value* read as a fixed-width field, and the +//! remaining bands are frequency-direction Huffman deltas (`f_huff`). +//! * delta-in-**time** (`bs_df_* == 1`): every band is a +//! time-direction Huffman delta (`t_huff`) relative to the +//! corresponding band of the previous envelope / noise floor. +//! +//! The start-value field widths (Table 4.72 / 4.73) depend on the +//! coupling / channel / amplitude-resolution context: +//! +//! | element | context | width | +//! |---------|---------|-------| +//! | envelope | coupling && ch, amp_res | 5 | +//! | envelope | coupling && ch, !amp_res | 6 | +//! | envelope | level, amp_res | 6 | +//! | envelope | level, !amp_res | 7 | +//! | noise | (any) | 5 | +//! +//! The per-envelope band count is `num_env_bands[bs_freq_res]` — the +//! high-resolution band count `NHigh` when the envelope's freq-res flag +//! is set, otherwise the low-resolution count `NLow` +//! ([`crate::sbr_freq_bands::HiLoTables`]). The noise band count is +//! `NQ` for every noise floor. +//! +//! This module produces the **raw** delta arrays exactly as written on +//! the wire; the §4.6.18.3.5 DPCM accumulation across bands / time and +//! the §4.6.18 dequantization to linear energies are downstream. + +use crate::sbr_grid::{SbrDtdf, SbrGrid}; +use crate::sbr_huffman::{env_tables, noise_tables, sbr_huff_dec, SbrHuffContext}; +use crate::{Error, Result}; +use oxideav_core::bits::BitReader; + +/// Raw `bs_data_env` for one channel: one delta vector per envelope. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrEnvelopeData { + /// `bs_data_env[env][band]` — the raw delta (or, at band 0 of a + /// frequency-coded envelope, the absolute start value). One inner + /// vector per envelope; its length is the envelope's band count. + pub data: Vec>, +} + +/// Raw `bs_data_noise` for one channel: one delta vector per noise +/// floor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrNoiseData { + /// `bs_data_noise[noise][band]` — raw delta / start value. One + /// inner vector per noise floor; each has `NQ` entries. + pub data: Vec>, +} + +/// The number of envelope bands for a given freq-resolution flag: +/// `NHigh` (high res) or `NLow` (low res). +fn num_env_bands(bands: &crate::sbr_freq_bands::HiLoTables, high_res: bool) -> usize { + if high_res { + bands.n_high() + } else { + bands.n_low() + } +} + +impl SbrEnvelopeData { + /// Parse `sbr_envelope()` (Table 4.72) for one channel. + /// + /// * `grid` / `dtdf` are this channel's already-parsed grid and + /// delta-direction flags. + /// * `bands` supplies `NHigh` / `NLow` for the per-envelope band + /// counts. + /// * `coupling` is the element `bs_coupling`; `ch` is the channel + /// index within the element; `amp_res` is the *effective* + /// amplitude resolution (after any single-envelope FIXFIX + /// override). + pub fn parse( + reader: &mut BitReader<'_>, + grid: &SbrGrid, + dtdf: &SbrDtdf, + bands: &crate::sbr_freq_bands::HiLoTables, + coupling: bool, + ch: bool, + amp_res: bool, + ) -> Result { + let ctx = SbrHuffContext { + coupling, + ch, + amp_res, + }; + let ((t_huff, t_lav), (f_huff, f_lav)) = env_tables(ctx); + + // Start-value width per Table 4.72. + let start_bits = if coupling && ch { + if amp_res { + 5 + } else { + 6 + } + } else if amp_res { + 6 + } else { + 7 + }; + + let mut data = Vec::with_capacity(grid.num_env); + for env in 0..grid.num_env { + let n = num_env_bands(bands, grid.freq_res[env]); + let mut row = Vec::with_capacity(n); + if !dtdf.df_env[env] { + // Delta in frequency: band 0 is the absolute start + // value, bands 1.. are f_huff deltas. + let start = read(reader, start_bits)? as i32; + row.push(start); + for _ in 1..n { + row.push(sbr_huff_dec(reader, f_huff, f_lav)?); + } + } else { + // Delta in time: every band is a t_huff delta. + for _ in 0..n { + row.push(sbr_huff_dec(reader, t_huff, t_lav)?); + } + } + data.push(row); + } + Ok(SbrEnvelopeData { data }) + } +} + +impl SbrNoiseData { + /// Parse `sbr_noise()` (Table 4.73) for one channel. + /// + /// `num_noise_bands` is `NQ` + /// ([`crate::sbr_freq_bands::HiLoTables::n_q`]). The other + /// arguments mirror [`SbrEnvelopeData::parse`]; the noise start + /// value is always a 5-bit field (Table 4.73), regardless of + /// `amp_res`. + pub fn parse( + reader: &mut BitReader<'_>, + grid: &SbrGrid, + dtdf: &SbrDtdf, + num_noise_bands: usize, + coupling: bool, + ch: bool, + amp_res: bool, + ) -> Result { + let ctx = SbrHuffContext { + coupling, + ch, + amp_res, + }; + let ((t_huff, t_lav), (f_huff, f_lav)) = noise_tables(ctx); + + let mut data = Vec::with_capacity(grid.num_noise); + for noise in 0..grid.num_noise { + let mut row = Vec::with_capacity(num_noise_bands); + if !dtdf.df_noise[noise] { + // Delta in frequency: band 0 is a 5-bit absolute start + // value, bands 1.. are f_huff deltas. + let start = read(reader, 5)? as i32; + row.push(start); + for _ in 1..num_noise_bands { + row.push(sbr_huff_dec(reader, f_huff, f_lav)?); + } + } else { + for _ in 0..num_noise_bands { + row.push(sbr_huff_dec(reader, t_huff, t_lav)?); + } + } + data.push(row); + } + Ok(SbrNoiseData { data }) + } +} + +#[inline] +fn read(reader: &mut BitReader<'_>, n: u32) -> Result { + reader.read_u32(n).map_err(|_| Error::SbrHuffInvalid) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sbr_freq_bands::{k0, k2, master_table, HiLoTables}; + use crate::sbr_grid::FrameClass; + use oxideav_core::bits::{BitReader, BitWriter}; + + fn bands_44100() -> HiLoTables { + // The known-good 44.1 kHz linear geometry from sbr_freq_bands. + let k0v = k0(88_200, 5).unwrap(); + let k2v = k2(88_200, 5, k0v).unwrap(); + let fm = master_table(k0v, k2v, 0, false).unwrap(); + HiLoTables::derive(&fm, 1, 2).unwrap() + } + + /// Push the MSB-first codeword for one table entry into a writer. + fn push_code(w: &mut BitWriter, table: &[(u8, u32)], idx: usize) { + let (len, code) = table[idx]; + w.write_u32(code, len as u32); + } + + #[test] + fn envelope_freq_direction_one_env() { + let bands = bands_44100(); + // FIXFIX, single env, high-res freq, delta-in-frequency. + let grid = SbrGrid { + frame_class: FrameClass::FixFix, + num_env: 1, + num_noise: 1, + freq_res: vec![true], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: true, + }; + let dtdf = SbrDtdf { + df_env: vec![false], // frequency direction + df_noise: vec![false], + }; + // amp_res = false → level start width 7 bits. + let n = bands.n_high(); + let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + let mut w = BitWriter::new(); + w.write_u32(40, 7); // start value + // remaining n-1 bands: pick a few known indices. + let chosen: Vec = (1..n) + .map(|i| (i + f_lav as usize) % f_huff.len()) + .collect(); + for &idx in &chosen { + push_code(&mut w, f_huff, idx); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ev = SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, false, false, false).unwrap(); + assert_eq!(ev.data.len(), 1); + assert_eq!(ev.data[0].len(), n); + assert_eq!(ev.data[0][0], 40); + for (band, &idx) in chosen.iter().enumerate() { + assert_eq!(ev.data[0][band + 1], idx as i32 - f_lav); + } + } + + #[test] + fn envelope_time_direction() { + let bands = bands_44100(); + let grid = SbrGrid { + frame_class: FrameClass::FixFix, + num_env: 1, + num_noise: 1, + freq_res: vec![false], // low res + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: false, + }; + let dtdf = SbrDtdf { + df_env: vec![true], // time direction → no start value + df_noise: vec![false], + }; + let n = bands.n_low(); + let ((t_huff, t_lav), (_f, _fl)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + let mut w = BitWriter::new(); + let chosen: Vec = (0..n).map(|i| (i * 2 + 1) % t_huff.len()).collect(); + for &idx in &chosen { + push_code(&mut w, t_huff, idx); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ev = SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, false, false, false).unwrap(); + assert_eq!(ev.data[0].len(), n); + for (band, &idx) in chosen.iter().enumerate() { + assert_eq!(ev.data[0][band], idx as i32 - t_lav); + } + } + + #[test] + fn noise_freq_and_time() { + let bands = bands_44100(); + let nq = bands.n_q(); + let grid = SbrGrid { + frame_class: FrameClass::FixVar, + num_env: 2, + num_noise: 2, + freq_res: vec![true, true], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: false, + }; + let dtdf = SbrDtdf { + df_env: vec![false, false], + df_noise: vec![false, true], // floor 0 freq, floor 1 time + }; + let ((t_huff, t_lav), (f_huff, f_lav)) = noise_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + let mut w = BitWriter::new(); + // Floor 0: 5-bit start + (nq-1) f deltas. + w.write_u32(12, 5); + let f_chosen: Vec = (1..nq) + .map(|i| (i + f_lav as usize) % f_huff.len()) + .collect(); + for &idx in &f_chosen { + push_code(&mut w, f_huff, idx); + } + // Floor 1: nq t deltas. + let t_chosen: Vec = (0..nq) + .map(|i| (i + t_lav as usize) % t_huff.len()) + .collect(); + for &idx in &t_chosen { + push_code(&mut w, t_huff, idx); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let nd = SbrNoiseData::parse(&mut r, &grid, &dtdf, nq, false, false, false).unwrap(); + assert_eq!(nd.data.len(), 2); + assert_eq!(nd.data[0].len(), nq); + assert_eq!(nd.data[0][0], 12); + for (band, &idx) in f_chosen.iter().enumerate() { + assert_eq!(nd.data[0][band + 1], idx as i32 - f_lav); + } + for (band, &idx) in t_chosen.iter().enumerate() { + assert_eq!(nd.data[1][band], idx as i32 - t_lav); + } + } + + #[test] + fn coupling_balance_start_width() { + // Coupled second channel at 3.0 dB → balance start width 5. + let bands = bands_44100(); + let grid = SbrGrid { + frame_class: FrameClass::FixFix, + num_env: 1, + num_noise: 1, + freq_res: vec![true], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: false, + }; + let dtdf = SbrDtdf { + df_env: vec![false], + df_noise: vec![false], + }; + let n = bands.n_high(); + let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { + coupling: true, + ch: true, + amp_res: true, + }); + let mut w = BitWriter::new(); + w.write_u32(7, 5); // 5-bit balance start + for i in 1..n { + push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let ev = SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, true, true, true).unwrap(); + assert_eq!(ev.data[0][0], 7); + } + + #[test] + fn truncated_envelope_errors() { + let bands = bands_44100(); + let grid = SbrGrid { + frame_class: FrameClass::FixFix, + num_env: 1, + num_noise: 1, + freq_res: vec![true], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: false, + }; + let dtdf = SbrDtdf { + df_env: vec![false], + df_noise: vec![false], + }; + let bytes = [0u8; 0]; + let mut r = BitReader::new(&bytes); + assert!(matches!( + SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, false, false, false), + Err(Error::SbrHuffInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_extension.rs b/crates/vendor/oxideav-aac/src/sbr_extension.rs new file mode 100644 index 00000000..0129b473 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_extension.rs @@ -0,0 +1,493 @@ +//! `sbr_extension_data()` top-level walker — ISO/IEC 14496-3 §4.4.2.8 +//! Table 4.62. +//! +//! This is the glue between [`crate::extension_payload`] and the SBR +//! side-info parsers: it consumes a whole SBR extension payload from a +//! `fill_element()`'s `extension_payload()` body, in the exact spec +//! order: +//! +//! ```text +//! sbr_extension_data(id_aac, crc_flag) { +//! num_sbr_bits = 0; +//! if (crc_flag) { bs_sbr_crc_bits; 10 uimsbf num_sbr_bits += 10; } +//! // sbr_layer != SBR_STEREO_ENHANCE for a non-scalable core: +//! bs_header_flag; 1 uimsbf num_sbr_bits += 1; +//! if (bs_header_flag) num_sbr_bits += sbr_header(); +//! num_sbr_bits += sbr_data(id_aac, bs_amp_res); +//! num_align_bits = (8*cnt - 4 - num_sbr_bits) % 8; +//! bs_fill_bits; num_align_bits uimsbf +//! } +//! ``` +//! +//! `sbr_data(id_aac, bs_amp_res)` dispatches on the AAC element type the +//! SBR payload extends: an `ID_SCE` core element pairs with +//! `sbr_single_channel_element()` ([`SbrElement::parse_single`]), an +//! `ID_CPE` core element with `sbr_channel_pair_element()` +//! ([`SbrElement::parse_pair`]). The band tables both need are derived +//! from the active [`SbrHeader`] at the SBR *internal* sample rate +//! `fs_sbr` (twice the AAC core rate) via [`SbrHeader::derive_bands`]. +//! +//! ## Header reuse +//! +//! When `bs_header_flag == 0` the payload reuses the most recent +//! transmitted `sbr_header()`. The first SBR payload of a stream must +//! carry a header (`bs_header_flag == 1`); a clear flag with no prior +//! header is an ill-formed stream ([`Error::SbrFreqBandInvalid`]). The +//! caller threads the returned [`SbrExtensionData::header`] back in as +//! `prev_header` on the next payload so the reuse chain is continuous. +//! +//! ## Scope +//! +//! This decodes the SBR *bitstream* side info end to end (CRC field + +//! header + grid / dtdf / invf / envelope / noise / add-harmonic + +//! extended-data block). The SBR back-end DSP (dequantization to linear +//! energies, the QMF analysis / synthesis filterbanks, HF generation / +//! patching, the limiter, and the envelope adjustment that produces +//! up-sampled PCM) is **not** part of this walker — it keys off the +//! band tables and scalefactors this produces. The `bs_sbr_crc_bits` +//! value is captured along with its §4.4.2.8.1 coverage region (the +//! `num_sbr_bits − 10` payload bits after the CRC field); callers that +//! own the payload buffer verify it via +//! [`SbrExtensionData::verify_crc`] (the decode drivers do). +//! +//! ## Clean-room provenance +//! +//! The Table 4.62 syntax, the `num_align_bits = (8·cnt − 4 − +//! num_sbr_bits) % 8` fill computation, and the `sbr_data` dispatch on +//! `id_aac` are transcribed from ISO/IEC 14496-3:2009 §4.4.2.8 staged +//! under `docs/audio/aac/`. The non-scalable core fixes the helper +//! `sbr_layer` to `SBR_NOT_SCALABLE` (Table 4.62 Note 1), so the +//! `bs_header_flag` is always present. + +use oxideav_core::bits::BitReader; + +use crate::raw_data_block::IdSynEle; +use crate::sbr_element::SbrElement; +use crate::sbr_header::SbrHeader; +use crate::{Error, Result}; + +/// Field width of `bs_sbr_crc_bits` (Table 4.62). +pub const SBR_CRC_BITS: u32 = 10; + +/// A fully-parsed `sbr_extension_data()` payload (Table 4.62). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrExtensionData { + /// `bs_sbr_crc_bits` (10-bit) when `crc_flag` was set (the + /// `EXT_SBR_DATA_CRC` extension type); `None` for the plain + /// `EXT_SBR_DATA` type. Verify with [`Self::verify_crc`]. + pub crc: Option, + /// The protected bit range `[start, end)` — absolute positions in + /// the buffer the parsing [`BitReader`] was constructed over — + /// covering every `sbr_extension_data()` bit after the CRC field + /// up to the end of `sbr_data()` (the §4.4.2.8.1 coverage region, + /// `num_sbr_bits − 10` bits). `None` when no CRC was present. + pub crc_region: Option<(u64, u64)>, + /// `bs_header_flag` — whether this payload transmitted a fresh + /// `sbr_header()`. + pub header_present: bool, + /// The active SBR header for this payload: the freshly parsed one + /// when `header_present`, otherwise the reused `prev_header`. The + /// caller threads this forward as the next payload's `prev_header`. + pub header: SbrHeader, + /// The decoded SBR data element (single channel or channel pair), + /// dispatched on the core element's `id_aac`. + pub element: SbrElement, + /// The number of SBR side-info bits consumed before the trailing + /// `bs_fill_bits` (the spec's `num_sbr_bits`). Useful for callers + /// validating against the `extension_payload()` byte count. + pub num_sbr_bits: u64, +} + +impl SbrExtensionData { + /// Parse an `sbr_extension_data(id_aac, crc_flag)` payload (Table + /// 4.62) from `reader`, positioned at the first SBR bit (i.e. the + /// caller — [`crate::extension_payload`] — has already consumed the + /// 4-bit `extension_type`). + /// + /// * `id_aac` — the AAC core element this SBR payload extends: only + /// [`IdSynEle::Sce`] / [`IdSynEle::Cpe`] are valid (an SBR payload + /// only attaches to a channel element). Any other id is rejected + /// with [`Error::SbrFreqBandInvalid`]. + /// * `crc_flag` — `true` for the `EXT_SBR_DATA_CRC` extension type + /// (a 10-bit `bs_sbr_crc_bits` field precedes the header), `false` + /// for plain `EXT_SBR_DATA`. + /// * `fs_sbr` — the SBR *internal* sample rate (twice the AAC core + /// `samplingFrequencyIndex` rate). Drives [`SbrHeader::derive_bands`]. + /// * `cnt` — the `extension_payload()` byte count `cnt` (Table 4.51), + /// used to size the trailing `bs_fill_bits` alignment. Pass `None` + /// to skip the fill consumption (when the caller bounds the reader + /// itself); the fill is then left in the reader. + /// * `prev_header` — the most recent transmitted header for the reuse + /// path; `None` on the stream's first SBR payload. A clear + /// `bs_header_flag` with `prev_header == None` is ill-formed. + pub fn parse( + reader: &mut BitReader<'_>, + id_aac: IdSynEle, + crc_flag: bool, + fs_sbr: u32, + cnt: Option, + prev_header: Option, + ) -> Result { + let start = reader.bit_position(); + + let crc = if crc_flag { + Some(read(reader, SBR_CRC_BITS)? as u16) + } else { + None + }; + let region_start = reader.bit_position(); + + // Non-scalable core ⇒ sbr_layer == SBR_NOT_SCALABLE, so the + // bs_header_flag is always present (Table 4.62 Note 1). + let header_present = read_flag(reader)?; + Self::finish( + reader, + id_aac, + crc, + start, + region_start, + header_present, + prev_header, + fs_sbr, + cnt, + ) + } + + /// [`SbrExtensionData::parse`] for a caller that has already + /// consumed the `extension_type` nibble, the optional 10-bit CRC + /// field, **and** a set `bs_header_flag` (the pre-header probe in + /// [`crate::extension_payload::ExtensionPayload::parse_with_sbr`]). + /// `nibble_start` is the bit position of the `extension_type` + /// nibble, from which the CRC coverage region and the Table 4.62 + /// `num_sbr_bits` accounting are reconstructed. + #[allow(clippy::too_many_arguments)] + pub fn parse_after_prefix( + reader: &mut BitReader<'_>, + id_aac: IdSynEle, + crc: Option, + nibble_start: u64, + fs_sbr: u32, + cnt: Option, + prev_header: Option, + ) -> Result { + let start = nibble_start + 4; + let region_start = start + + if crc.is_some() { + u64::from(SBR_CRC_BITS) + } else { + 0 + }; + Self::finish( + reader, + id_aac, + crc, + start, + region_start, + true, + prev_header, + fs_sbr, + cnt, + ) + } + + /// Shared tail of the two parse entries: `sbr_header()` (when + /// present), band derivation, `sbr_data()`, and the Table 4.62 + /// `bs_fill_bits` alignment. + #[allow(clippy::too_many_arguments)] + fn finish( + reader: &mut BitReader<'_>, + id_aac: IdSynEle, + crc: Option, + start: u64, + region_start: u64, + header_present: bool, + prev_header: Option, + fs_sbr: u32, + cnt: Option, + ) -> Result { + let header = if header_present { + SbrHeader::parse(reader)? + } else { + // Reuse the previous transmitted header. A stream that + // opens with header-less SBR payloads is the §4.5.2.8.1 + // "upsampling and delay adjustment only" state — the + // parse_with_sbr caller intercepts that case before + // reaching here, so a missing header at this point is a + // caller-contract violation. + prev_header.ok_or(Error::SbrFreqBandInvalid)? + }; + + // sbr_data(id_aac, bs_amp_res): the band tables are derived from + // the active header at the SBR internal rate; the element type is + // selected by the core element id_aac. + let bands = header.derive_bands(fs_sbr)?; + let element = match id_aac { + IdSynEle::Sce => SbrElement::parse_single(reader, &bands, header.amp_res)?, + IdSynEle::Cpe => SbrElement::parse_pair(reader, &bands, header.amp_res)?, + _ => return Err(Error::SbrFreqBandInvalid), + }; + + let region_end = reader.bit_position(); + let num_sbr_bits = region_end - start; + + // num_align_bits = (8*cnt - 4 - num_sbr_bits) % 8. The `- 4` + // accounts for the extension_type nibble the caller already read; + // when cnt is known, consume the trailing bs_fill_bits so the + // reader lands on the next extension_payload element. + let mut crc_end = region_end; + if let Some(cnt) = cnt { + let total = u64::from(cnt) * 8; + let consumed = num_sbr_bits + 4; // + the extension_type nibble + if total < consumed { + return Err(Error::SbrFreqBandInvalid); + } + let align = (total - consumed) % 8; + if align > 0 { + read(reader, align as u32)?; + } + // §4.5.2.8.1: "The checksum shall be calculated covering + // the whole SBR data range including possible + // bs_fill_bits" — the coverage extends past the end of + // sbr_data() through the alignment padding to the end of + // the fill payload. Confirmed against the ISO/IEC + // 14496-26 `al_sbr_*` type-14 vectors, whose + // header-bearing payloads carry non-zero bs_fill_bits and + // only verify over the padded region. + // (`start` is 4 bits past the extension_type nibble; the + // grouping avoids u64 underflow when a caller parses a + // nibble-less buffer from position 0.) + crc_end = start + (total - 4); + } + + Ok(SbrExtensionData { + crc, + crc_region: crc.map(|_| (region_start, crc_end)), + header_present, + header, + element, + num_sbr_bits, + }) + } + + /// Verify the `bs_sbr_crc_bits` checksum against the §4.5.2.8.1 + /// coverage region (every payload bit after the CRC field to the + /// end of the fill payload — "the whole SBR data range including + /// possible bs_fill_bits"). + /// + /// `data` must be the same byte buffer the parsing [`BitReader`] + /// was constructed over ([`Self::crc_region`] holds absolute bit + /// positions into it). A payload without a CRC field (plain + /// `EXT_SBR_DATA`) verifies vacuously. Returns + /// [`Error::SbrCrcMismatch`] when the recomputed 10-bit `G10` + /// (zero-init) CRC disagrees with the transmitted value. + pub fn verify_crc(&self, data: &[u8]) -> Result<()> { + if let (Some(crc), Some((start, end))) = (self.crc, self.crc_region) { + if crate::adts_crc::sbr_crc(data, start, end) != crc { + return Err(Error::SbrCrcMismatch); + } + } + Ok(()) + } +} + +#[inline] +fn read(reader: &mut BitReader<'_>, n: u32) -> Result { + reader.read_u32(n).map_err(|_| Error::SbrFreqBandInvalid) +} + +#[inline] +fn read_flag(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::SbrFreqBandInvalid) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sbr_freq_bands::HiLoTables; + use crate::sbr_grid::FrameClass; + use crate::sbr_huffman::{env_tables, noise_tables, SbrHuffContext}; + use oxideav_core::bits::BitWriter; + + const FS_SBR: u32 = 88_200; // 44.1 kHz core, doubled. + + /// A header carrying explicit extra-1 params (freq_scale 0, + /// alter_scale false, noise_bands 2) so the derived band geometry is + /// deterministic; extra-2 absent. + fn write_header(w: &mut BitWriter, amp_res: bool) { + w.write_bit(amp_res); // bs_amp_res + w.write_u32(5, 4); // bs_start_freq + w.write_u32(0, 4); // bs_stop_freq + w.write_u32(1, 3); // bs_xover_band + w.write_u32(0, 2); // bs_reserved + w.write_bit(true); // bs_header_extra_1 + w.write_bit(false); // bs_header_extra_2 + w.write_u32(0, 2); // bs_freq_scale + w.write_bit(false); // bs_alter_scale + w.write_u32(2, 2); // bs_noise_bands + } + + /// The band tables a `write_header(_, _)`-built header derives. + fn header_bands() -> HiLoTables { + let mut w = BitWriter::new(); + write_header(&mut w, false); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let h = SbrHeader::parse(&mut r).unwrap(); + h.derive_bands(FS_SBR).unwrap() + } + + fn push_code(w: &mut BitWriter, table: &[(u8, u32)], idx: usize) { + let (len, code) = table[idx]; + w.write_u32(code, len as u32); + } + + /// Minimal single-channel SBR element body (FIXFIX single env, freq + /// deltas, no sinusoidal / extended data). Mirrors the + /// `sbr_element` test helper but inline so the band geometry comes + /// from the header we just wrote. + fn write_minimal_sce(w: &mut BitWriter, bands: &HiLoTables) { + let n_high = bands.n_high(); + let n_q = bands.n_q(); + w.write_bit(false); // bs_data_extra + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(0, 2); // 2^0 = 1 env + w.write_bit(true); // freq_res[0] high + w.write_bit(false); // df_env[0] + w.write_bit(false); // df_noise[0] + for _ in 0..n_q { + w.write_u32(1, 2); // invf modes + } + let (_, (f_huff, f_lav)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + w.write_u32(33, 7); // env start value (amp_res override → 7-bit) + for i in 1..n_high { + push_code(w, f_huff, (i + f_lav as usize) % f_huff.len()); + } + let (_, (nf, nfl)) = noise_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + w.write_u32(10, 5); // noise start + for i in 1..n_q { + push_code(w, nf, (i + nfl as usize) % nf.len()); + } + w.write_bit(false); // bs_add_harmonic_flag + w.write_bit(false); // bs_extended_data + } + + #[test] + fn parses_header_plus_single_channel() { + let bands = header_bands(); + let mut w = BitWriter::new(); + w.write_bit(true); // bs_header_flag + write_header(&mut w, true); + write_minimal_sce(&mut w, &bands); + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let sbr = + SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, None, None).unwrap(); + assert!(sbr.header_present); + assert!(sbr.crc.is_none()); + assert_eq!(sbr.header.start_freq, 5); + assert_eq!(sbr.header.freq_scale, 0); + assert!(!sbr.element.coupling); + assert_eq!(sbr.element.channels.len(), 1); + assert_eq!(sbr.element.channels[0].envelope.data[0][0], 33); + assert_eq!(sbr.element.channels[0].noise.data[0][0], 10); + } + + #[test] + fn crc_flag_reads_ten_bit_field() { + let bands = header_bands(); + let mut w = BitWriter::new(); + w.write_u32(0x2A5, SBR_CRC_BITS); // bs_sbr_crc_bits + w.write_bit(true); // bs_header_flag + write_header(&mut w, true); + write_minimal_sce(&mut w, &bands); + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let sbr = SbrExtensionData::parse(&mut r, IdSynEle::Sce, true, FS_SBR, None, None).unwrap(); + assert_eq!(sbr.crc, Some(0x2A5)); + assert!(sbr.header_present); + } + + #[test] + fn header_reuse_when_flag_clear() { + // A prior header is reused when bs_header_flag == 0. + let bands = header_bands(); + let prev = { + let mut w = BitWriter::new(); + write_header(&mut w, true); + let bytes = w.finish(); + SbrHeader::parse(&mut BitReader::new(&bytes)).unwrap() + }; + let mut w = BitWriter::new(); + w.write_bit(false); // bs_header_flag clear + write_minimal_sce(&mut w, &bands); + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let sbr = SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, None, Some(prev)) + .unwrap(); + assert!(!sbr.header_present); + assert_eq!(sbr.header, prev); + assert_eq!(sbr.element.channels.len(), 1); + } + + #[test] + fn header_clear_without_prior_is_error() { + let mut w = BitWriter::new(); + w.write_bit(false); // bs_header_flag clear, no prior header + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(matches!( + SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, None, None), + Err(Error::SbrFreqBandInvalid) + )); + } + + #[test] + fn non_channel_id_aac_is_rejected() { + let mut w = BitWriter::new(); + w.write_bit(true); + write_header(&mut w, true); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert!(matches!( + SbrExtensionData::parse(&mut r, IdSynEle::Lfe, false, FS_SBR, None, None), + Err(Error::SbrFreqBandInvalid) + )); + } + + #[test] + fn fill_bits_consumed_when_cnt_given() { + // Pad the payload to a known byte count and confirm the walker + // consumes the trailing bs_fill_bits so the reader is byte-aligned + // at `cnt` bytes (minus the extension_type nibble the caller owns). + let bands = header_bands(); + let mut w = BitWriter::new(); + w.write_bit(true); + write_header(&mut w, true); + write_minimal_sce(&mut w, &bands); + let mut body = w.finish(); + // cnt counts whole bytes of the extension_payload including its + // 4-bit type nibble; add two trailing fill bytes so there is a + // non-trivial bs_fill_bits to swallow and the reader has the bits. + let cnt = (body.len() + 2) as u32; + body.extend_from_slice(&[0u8, 0u8]); + let mut r = BitReader::new(&body); + let before = r.bit_position(); + let sbr = + SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, Some(cnt), None).unwrap(); + let consumed = r.bit_position() - before; + // Total consumed (+ the 4-bit type nibble) must be a multiple of 8. + assert_eq!((consumed + 4) % 8, 0); + assert_eq!(sbr.element.channels.len(), 1); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs b/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs new file mode 100644 index 00000000..168e2746 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs @@ -0,0 +1,756 @@ +//! SBR frequency band tables — ISO/IEC 14496-3 §4.6.18.3.2. +//! +//! Spectral Band Replication groups the QMF subbands in frequency by a +//! family of *frequency band tables*. Everything is derived from one +//! **master** table `fMaster`, which is in turn fixed by two QMF subband +//! boundaries — the low boundary `k0` and the high boundary `k2` — and +//! the header data elements `bs_freq_scale` / `bs_alter_scale`. +//! +//! This module implements the *static* (header-only) half of the band +//! setup, i.e. everything that does **not** depend on the §4.6.18.6 QMF +//! patching / high-frequency-generation back-end: +//! +//! * [`k0`] — §4.6.18.3.2.1 low boundary `k0 = startMin + +//! offset(bs_start_freq)`, with the per-`FsSBR` `offset` table and the +//! `startMin = NINT(c · 128 / FsSBR)` thresholds. +//! * [`k2`] — §4.6.18.3.2.1 high boundary, including the +//! `bs_stop_freq < 14` `stopDkSort` accumulation path and the +//! `bs_stop_freq == 14 / 15` `min(64, 2·k0)` / `min(64, 3·k0)` +//! shortcuts. +//! * [`master_table`] — §4.6.18.3.2.1 `fMaster` (Figure 4.39 for +//! `bs_freq_scale == 0`, Figure 4.40 for `bs_freq_scale > 0`). +//! * [`HiLoTables::derive`] — §4.6.18.3.2.2 `fTableHigh`, `fTableLow`, +//! `fTableNoise`, plus the `M` / `k_x` outputs every later SBR stage +//! keys off. +//! +//! ## Scope +//! +//! * The §4.6.18.3.2.3 limiter band table `fTableLim` is **not** here: +//! for `bs_limiter_bands > 0` it consumes the `patchBorders` / +//! `patchNumSubbands` produced by §4.6.18.6, which needs the QMF +//! patching back-end this crate does not have yet. The +//! `bs_limiter_bands == 0` single-band case +//! (`{fTableLow(0), fTableLow(NLow)}`) is trivially derivable from +//! [`HiLoTables`] and is left to the limiter pass. +//! * The actual envelope decode, noise-floor decode, and QMF synthesis +//! are downstream of these tables. +//! +//! ## Operators +//! +//! The spec's `INT()` truncates toward zero and `NINT()` rounds to the +//! nearest integer with halves away from zero (ISO/IEC 14496-3 §4.6.18, +//! reusing the §4 `INT` / `NINT` definitions). The arguments here are +//! always non-negative, so `INT` is a plain floor and the `NINT` helper +//! adds `0.5` before truncating. + +use crate::{Error, Result}; + +/// §4.6.18.3.2.1 nearest-integer operator (`NINT`): round to the nearest +/// integer, halves away from zero. All call sites in this module pass a +/// finite, non-negative argument. +#[inline] +fn nint(x: f64) -> i32 { + // Halves away from zero: for x >= 0 this is floor(x + 0.5); the sign + // branch keeps the helper correct for any finite input. + if x >= 0.0 { + (x + 0.5).floor() as i32 + } else { + (x - 0.5).ceil() as i32 + } +} + +/// §4 `INT` operator: truncation toward zero. The arguments in this +/// module are always non-negative, so this is a plain `floor`. +#[inline] +fn int_trunc(x: f64) -> i32 { + x.trunc() as i32 +} + +/// The `offset(bs_start_freq)` row for an `FsSBR` value, per the +/// §4.6.18.3.2.1 `offset` table. Returns `None` for an `FsSBR` outside +/// the tabulated set (the spec only defines rows for the standard SBR +/// internal sample rates). +fn offset_row(fs_sbr: u32) -> Option<&'static [i32; 16]> { + // FsSBR is twice the core sample rate; the table is keyed by the + // SBR internal rate directly. + const OFF_16: [i32; 16] = [-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]; + const OFF_22: [i32; 16] = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13]; + const OFF_24: [i32; 16] = [-5, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16]; + const OFF_32: [i32; 16] = [-6, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16]; + const OFF_44: [i32; 16] = [-4, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16, 20]; + const OFF_64: [i32; 16] = [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16, 20, 24]; + + match fs_sbr { + 16000 => Some(&OFF_16), + 22050 => Some(&OFF_22), + 24000 => Some(&OFF_24), + 32000 => Some(&OFF_32), + // `44100 <= FsSBR <= 64000` shares one row. + 44100 | 48000 | 64000 => Some(&OFF_44), + // `FsSBR > 64000`. + 88200 | 96000 | 128000 | 176400 | 192000 => Some(&OFF_64), + _ => None, + } +} + +/// §4.6.18.3.2.1 `startMin = NINT(c · 128 / FsSBR)`, with the three +/// `c ∈ {3000, 4000, 5000}` bands keyed by `FsSBR`. +fn start_min(fs_sbr: u32) -> i32 { + let fs = fs_sbr as f64; + let c = if fs_sbr < 32000 { + 3000.0 + } else if fs_sbr < 64000 { + 4000.0 + } else { + 5000.0 + }; + nint(c * 128.0 / fs) +} + +/// §4.6.18.3.2.1 `stopMin = NINT(c · 128 / FsSBR)`, with the three +/// `c ∈ {6000, 8000, 10000}` bands keyed by `FsSBR`. +fn stop_min(fs_sbr: u32) -> i32 { + let fs = fs_sbr as f64; + let c = if fs_sbr < 32000 { + 6000.0 + } else if fs_sbr < 64000 { + 8000.0 + } else { + 10000.0 + }; + nint(c * 128.0 / fs) +} + +/// §4.6.18.3.2.1 low boundary `k0`. +/// +/// `k0 = startMin + offset(bs_start_freq)`. `bs_start_freq` is a 4-bit +/// header field (`0 ..= 15`); `fs_sbr` must be one of the tabulated SBR +/// internal sample rates. Returns [`Error::SbrFreqBandInvalid`] for an +/// out-of-range `bs_start_freq` or an unsupported `fs_sbr`. +pub fn k0(fs_sbr: u32, bs_start_freq: u8) -> Result { + let row = offset_row(fs_sbr).ok_or(Error::SbrFreqBandInvalid)?; + let idx = bs_start_freq as usize; + if idx >= row.len() { + return Err(Error::SbrFreqBandInvalid); + } + Ok(start_min(fs_sbr) + row[idx]) +} + +/// §4.6.18.3.2.1 high boundary `k2`. +/// +/// For `0 <= bs_stop_freq < 14` this is +/// `min(64, stopMin + Σ_{i Result { + if bs_stop_freq > 15 { + return Err(Error::SbrFreqBandInvalid); + } + let val = match bs_stop_freq { + 14 => (2 * k0_val).min(64), + 15 => (3 * k0_val).min(64), + _ => { + let stop_min_v = stop_min(fs_sbr); + if stop_min_v <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let ratio = 64.0 / stop_min_v as f64; + // stopDk(p), 0 <= p <= 12 -> 13 entries. + let mut stop_dk = [0i32; 13]; + for (p, slot) in stop_dk.iter_mut().enumerate() { + let hi = nint(stop_min_v as f64 * ratio.powf((p as f64 + 1.0) / 13.0)); + let lo = nint(stop_min_v as f64 * ratio.powf(p as f64 / 13.0)); + *slot = hi - lo; + } + stop_dk.sort_unstable(); + // stopMin + Σ_{i=0}^{bs_stop_freq-1} stopDkSort(i). + let mut acc = stop_min_v; + for &dk in stop_dk.iter().take(bs_stop_freq as usize) { + acc += dk; + } + acc.min(64) + } + }; + Ok(val) +} + +/// §4.6.18.3.2.1 master frequency band table `fMaster`. +/// +/// Implements Figure 4.39 (`bs_freq_scale == 0`) and Figure 4.40 +/// (`bs_freq_scale > 0`). The returned vector is `fMaster(0..=NMaster)`, +/// so `NMaster == len() - 1`. `fMaster` is only defined for `k2 > k0`; +/// `numBands > 0` and the §4.6.18.3.6 `vDk > 0` requirements are checked. +/// +/// * `bs_freq_scale ∈ {0, 1, 2, 3}` (0 = no warping/linear, +/// 1/2/3 select `bands ∈ {12, 10, 8}`). +/// * `bs_alter_scale ∈ {0, 1}`. +pub fn master_table( + k0_val: i32, + k2_val: i32, + bs_freq_scale: u8, + bs_alter_scale: bool, +) -> Result> { + if k2_val <= k0_val || bs_freq_scale > 3 { + return Err(Error::SbrFreqBandInvalid); + } + + if bs_freq_scale == 0 { + master_linear(k0_val, k2_val, bs_alter_scale) + } else { + master_warped(k0_val, k2_val, bs_freq_scale, bs_alter_scale) + } +} + +/// Figure 4.39 — `fMaster` for `bs_freq_scale == 0`. +fn master_linear(k0_val: i32, k2_val: i32, bs_alter_scale: bool) -> Result> { + let dk; + let num_bands; + if !bs_alter_scale { + dk = 1; + // numBands = 2 * INT( (k2 - k0) / (dk * 2) ) + num_bands = 2 * int_trunc((k2_val - k0_val) as f64 / (dk as f64 * 2.0)); + } else { + dk = 2; + // numBands = 2 * NINT( (k2 - k0) / (dk * 2) ) + num_bands = 2 * nint((k2_val - k0_val) as f64 / (dk as f64 * 2.0)); + } + if num_bands <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let num_bands = num_bands as usize; + + let mut v_dk = vec![dk; num_bands]; + let k2_achieved = k0_val + num_bands as i32 * dk; + let mut k2_diff = k2_val - k2_achieved; + + if k2_diff != 0 { + // incr / k start, then walk while k2Diff != 0. + let (incr, mut k): (i32, isize) = if k2_diff < 0 { + (1, 0) + } else { + (-1, num_bands as isize - 1) + }; + while k2_diff != 0 { + v_dk[k as usize] -= incr; + k += incr as isize; + k2_diff += incr; + } + } + + // fMaster(0) = k0; fMaster(k) = fMaster(k-1) + vDk[k-1]. + let mut f_master = Vec::with_capacity(num_bands + 1); + f_master.push(k0_val); + for &d in &v_dk { + // §4.6.18.3.6: numBands > 0 is checked above; the away-from-zero + // walk above can drive a vDk entry to 0 only on malformed input. + if d <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let next = *f_master.last().unwrap() + d; + f_master.push(next); + } + Ok(f_master) +} + +/// Figure 4.40 — `fMaster` for `bs_freq_scale > 0`. +fn master_warped( + k0_val: i32, + k2_val: i32, + bs_freq_scale: u8, + bs_alter_scale: bool, +) -> Result> { + // temp1 = {12, 10, 8}; bands = temp1[bs_freq_scale - 1]. + let bands = [12.0, 10.0, 8.0][(bs_freq_scale - 1) as usize]; + // temp2 = {1.0, 1.3}; warp = temp2[bs_alter_scale]. + let warp = if bs_alter_scale { 1.3 } else { 1.0 }; + + let two_regions; + let k1; + if (k2_val as f64) / (k0_val as f64) > 2.2449 { + two_regions = true; + k1 = 2 * k0_val; + } else { + two_regions = false; + k1 = k2_val; + } + + // Lower region. + let v_k0 = warped_region(k0_val, k1, bands, 1.0)?; + let num_bands0 = v_k0.len() - 1; + + if !two_regions { + return Ok(v_k0); + } + + // Upper region with warping. The §4.6.18.3.6 "min(vDk1) < max(vDk0)" + // smoothing step is part of warped_region_upper. + let max_v_dk0 = max_step(&v_k0); + let v_k1 = warped_region_upper(k1, k2_val, bands, warp, max_v_dk0)?; + let num_bands1 = v_k1.len() - 1; + + // fMaster: vk0[0..=numBands0] then vk1[1..=numBands1]. + let mut f_master = Vec::with_capacity(num_bands0 + num_bands1 + 1); + f_master.extend_from_slice(&v_k0); + f_master.extend_from_slice(&v_k1[1..]); + Ok(f_master) +} + +/// Largest forward step `vDk[k] = vk[k+1] - vk[k]` of a `vk` vector. +fn max_step(v_k: &[i32]) -> i32 { + v_k.windows(2).map(|w| w[1] - w[0]).max().unwrap_or(0) +} + +/// Figure 4.40 lower-region builder: produces `vk0` (or, for the +/// `twoRegions == 0` case, the whole `fMaster`). +/// +/// `numBands0 = 2 * NINT( bands * log(k1/k0) / (2 * log(2) * warp) )` +/// (the lower region always passes `warp = 1`), then +/// `vDk0[k] = NINT(k0 * (k1/k0)^((k+1)/numBands0)) − NINT(k0 * +/// (k1/k0)^(k/numBands0))`, sorted ascending, cumulatively summed from +/// `k0`. +fn warped_region(k_lo: i32, k_hi: i32, bands: f64, warp: f64) -> Result> { + let ratio = k_hi as f64 / k_lo as f64; + let num_bands = 2 * nint(bands * ratio.ln() / (2.0 * 2.0_f64.ln() * warp)); + if num_bands <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let num_bands = num_bands as usize; + + let mut v_dk = vec![0i32; num_bands]; + for (k, slot) in v_dk.iter_mut().enumerate() { + let hi = nint(k_lo as f64 * ratio.powf((k as f64 + 1.0) / num_bands as f64)); + let lo = nint(k_lo as f64 * ratio.powf(k as f64 / num_bands as f64)); + *slot = hi - lo; + } + v_dk.sort_unstable(); + + let mut v_k = Vec::with_capacity(num_bands + 1); + v_k.push(k_lo); + for &d in &v_dk { + // §4.6.18.3.6: vDk0(i) > 0 ∀ i. + if d <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let next = *v_k.last().unwrap() + d; + v_k.push(next); + } + Ok(v_k) +} + +/// Figure 4.40 upper-region builder with the `min(vDk1) < max(vDk0)` +/// smoothing branch. +fn warped_region_upper( + k1: i32, + k2_val: i32, + bands: f64, + warp: f64, + max_v_dk0: i32, +) -> Result> { + let ratio = k2_val as f64 / k1 as f64; + // numBands1 = 2 * NINT(bands * log(k2/k1) / (2 * log(2) * warp)) + let num_bands1 = 2 * nint(bands * ratio.ln() / (2.0 * 2.0_f64.ln() * warp)); + if num_bands1 <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let num_bands1 = num_bands1 as usize; + + let mut v_dk1 = vec![0i32; num_bands1]; + for (k, slot) in v_dk1.iter_mut().enumerate() { + let hi = nint(k1 as f64 * ratio.powf((k as f64 + 1.0) / num_bands1 as f64)); + let lo = nint(k1 as f64 * ratio.powf(k as f64 / num_bands1 as f64)); + *slot = hi - lo; + } + + // if min(vDk1) < max(vDk0): sort, then redistribute `change` from the + // largest to the smallest entry (capped at half the spread). + if v_dk1.iter().copied().min().unwrap_or(0) < max_v_dk0 { + v_dk1.sort_unstable(); + let mut change = max_v_dk0 - v_dk1[0]; + let half = int_trunc((v_dk1[num_bands1 - 1] - v_dk1[0]) as f64 / 2.0); + if change > half { + change = half; + } + v_dk1[0] += change; + v_dk1[num_bands1 - 1] -= change; + } + v_dk1.sort_unstable(); + + let mut v_k1 = Vec::with_capacity(num_bands1 + 1); + v_k1.push(k1); + for &d in &v_dk1 { + // §4.6.18.3.6: vDk1(i) > 0 ∀ i. + if d <= 0 { + return Err(Error::SbrFreqBandInvalid); + } + let next = *v_k1.last().unwrap() + d; + v_k1.push(next); + } + Ok(v_k1) +} + +/// §4.6.18.3.2.2 derived high / low / noise frequency band tables, plus +/// the `M` (number of QMF subbands covered by SBR) and `k_x` (first SBR +/// subband) outputs that the envelope / noise / patching stages key off. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HiLoTables { + /// `fTableHigh(0..=NHigh)` — high-resolution envelope band borders. + pub f_table_high: Vec, + /// `fTableLow(0..=NLow)` — low-resolution envelope band borders. + pub f_table_low: Vec, + /// `fTableNoise(0..=NQ)` — noise-floor band borders. + pub f_table_noise: Vec, + /// `M = fTableHigh(NHigh) − fTableHigh(0)` — number of QMF subbands + /// covered by SBR. + pub m: i32, + /// `k_x = fTableHigh(0)` — index of the first QMF subband in the SBR + /// range. + pub k_x: i32, +} + +impl HiLoTables { + /// `NHigh = len(fTableHigh) - 1`. + #[inline] + pub fn n_high(&self) -> usize { + self.f_table_high.len() - 1 + } + + /// `NLow = len(fTableLow) - 1`. + #[inline] + pub fn n_low(&self) -> usize { + self.f_table_low.len() - 1 + } + + /// `NQ = len(fTableNoise) - 1`. + #[inline] + pub fn n_q(&self) -> usize { + self.f_table_noise.len() - 1 + } + + /// §4.6.18.3.2.2 derive `fTableHigh` / `fTableLow` / `fTableNoise` + /// from a master table. + /// + /// `f_master` is `fMaster(0..=NMaster)` (i.e. [`master_table`]'s + /// output). `bs_xover_band` must satisfy `bs_xover_band < NMaster` + /// (§4.6.18.3.6). `bs_noise_bands ∈ {0, 1, 2, 3}`. + pub fn derive(f_master: &[i32], bs_xover_band: u8, bs_noise_bands: u8) -> Result { + if f_master.len() < 2 || bs_noise_bands > 3 { + return Err(Error::SbrFreqBandInvalid); + } + let n_master = f_master.len() - 1; + let xover = bs_xover_band as usize; + // bs_xover_band < NMaster (§4.6.18.3.6). + if xover >= n_master { + return Err(Error::SbrFreqBandInvalid); + } + + // NHigh = NMaster - bs_xover_band. + let n_high = n_master - xover; + // fTableHigh(k) = fMaster(k + bs_xover_band), 0 <= k <= NHigh. + let f_table_high: Vec = f_master[xover..=n_master].to_vec(); + debug_assert_eq!(f_table_high.len(), n_high + 1); + + // M = fTableHigh(NHigh) - fTableHigh(0); k_x = fTableHigh(0). + let k_x = f_table_high[0]; + let m = f_table_high[n_high] - k_x; + + // NLow = INT(NHigh/2) + (NHigh - 2*INT(NHigh/2)). + let half = n_high / 2; + let n_low = half + (n_high - 2 * half); + + // fTableLow(k) = fTableHigh(i(k)): + // i(0) = 0; i(k) = 2*k - ((1 - (-1)^NHigh)/2) for k != 0. + let parity = (1 - if n_high % 2 == 0 { 1 } else { -1 }) / 2; // 0 if NHigh even, 1 if odd + let mut f_table_low = Vec::with_capacity(n_low + 1); + for k in 0..=n_low { + let i_k = if k == 0 { + 0 + } else { + (2 * k as isize - parity as isize) as usize + }; + f_table_low.push(*f_table_high.get(i_k).ok_or(Error::SbrFreqBandInvalid)?); + } + + // NQ = max(1, NINT(bs_noise_bands * log2(k2/k_x))), where + // k2 == fTableLow(NLow) (the high boundary of the SBR range). + let k2_range = f_table_low[n_low]; + let n_q = if bs_noise_bands == 0 { + 1usize + } else { + let val = nint(bs_noise_bands as f64 * ((k2_range as f64 / k_x as f64).log2())); + val.max(1) as usize + }; + + // fTableNoise(0) = fTableLow(0); for k != 0: + // i(k) = i(k-1) + INT((NLow - i(k-1)) / (NQ + 1 - k)). + let mut f_table_noise = Vec::with_capacity(n_q + 1); + let mut i_prev: usize = 0; + f_table_noise.push(f_table_low[0]); + for k in 1..=n_q { + let denom = (n_q + 1 - k) as f64; + let step = int_trunc((n_low - i_prev) as f64 / denom); + i_prev += step as usize; + f_table_noise.push(*f_table_low.get(i_prev).ok_or(Error::SbrFreqBandInvalid)?); + } + + Ok(HiLoTables { + f_table_high, + f_table_low, + f_table_noise, + m, + k_x, + }) + } +} + +#[cfg(test)] +mod tests { + //! Truth is the ISO/IEC 14496-3 §4.6.18.3.2 closed-form algorithm + //! (Figures 4.39 / 4.40 and the §4.6.18.3.2.2 derivations). Each + //! expected value below is computed by hand from those formulas for + //! a specific `(FsSBR, bs_start_freq, bs_stop_freq, bs_freq_scale, + //! …)` parameter set; no external decoder is consulted. + + use super::*; + + #[test] + fn nint_rounds_half_away_from_zero() { + assert_eq!(nint(2.5), 3); + assert_eq!(nint(2.4), 2); + assert_eq!(nint(2.6), 3); + assert_eq!(nint(0.5), 1); + assert_eq!(nint(3.0), 3); + } + + #[test] + fn start_stop_min_44100() { + // 44.1 kHz core -> FsSBR = 88200 (> 64000): c = 5000 / 10000. + // startMin = NINT(5000 * 128 / 88200) = NINT(7.256...) = 7. + assert_eq!(start_min(88200), 7); + // stopMin = NINT(10000 * 128 / 88200) = NINT(14.51...) = 15. + assert_eq!(stop_min(88200), 15); + } + + #[test] + fn start_min_band_thresholds() { + // FsSBR < 32000 -> c = 3000. FsSBR = 24000: + // NINT(3000 * 128 / 24000) = NINT(16.0) = 16. + assert_eq!(start_min(24000), 16); + // 32000 <= FsSBR < 64000 -> c = 4000. FsSBR = 44100: + // NINT(4000 * 128 / 44100) = NINT(11.61...) = 12. + assert_eq!(start_min(44100), 12); + } + + #[test] + fn k0_24khz_start_freq_5() { + // FsSBR = 24000 -> startMin = 16, offset row OFF_24. + // offset(5) = 1 -> k0 = 17. + assert_eq!(k0(24000, 5).unwrap(), 17); + // offset(0) = -5 -> k0 = 11. + assert_eq!(k0(24000, 0).unwrap(), 11); + } + + #[test] + fn k0_rejects_bad_inputs() { + // bs_start_freq out of 0..=15 (would need a 5-bit field). + assert_eq!(k0(24000, 16), Err(Error::SbrFreqBandInvalid)); + // Unsupported FsSBR. + assert_eq!(k0(11025, 0), Err(Error::SbrFreqBandInvalid)); + } + + #[test] + fn k2_shortcuts() { + // bs_stop_freq == 14 -> min(64, 2*k0). + assert_eq!(k2(88200, 14, 10).unwrap(), 20); + assert_eq!(k2(88200, 14, 40).unwrap(), 64); // capped + // bs_stop_freq == 15 -> min(64, 3*k0). + assert_eq!(k2(88200, 15, 10).unwrap(), 30); + assert_eq!(k2(88200, 15, 30).unwrap(), 64); // capped + } + + #[test] + fn k2_accumulation_bs_stop_freq_0() { + // bs_stop_freq == 0 -> empty sum -> k2 = min(64, stopMin). + // FsSBR = 88200 -> stopMin = 15. + assert_eq!(k2(88200, 0, 7).unwrap(), 15); + } + + #[test] + fn k2_accumulation_is_monotone() { + // As bs_stop_freq grows, k2 is non-decreasing (stopDkSort >= 0 + // and the sum accumulates) and capped at 64. + let mut prev = k2(88200, 0, 7).unwrap(); + for bsf in 1..14 { + let cur = k2(88200, bsf, 7).unwrap(); + assert!(cur >= prev, "k2 dropped at bs_stop_freq={bsf}"); + assert!(cur <= 64); + prev = cur; + } + } + + #[test] + fn master_linear_simple() { + // bs_freq_scale = 0, bs_alter_scale = 0 -> dk = 1, every band + // width 1. k0 = 5, k2 = 13 -> numBands = 2*INT(8/2) = 8, + // k2Achieved = 13, k2Diff = 0 -> fMaster = 5..=13. + let fm = master_table(5, 13, 0, false).unwrap(); + assert_eq!(fm, vec![5, 6, 7, 8, 9, 10, 11, 12, 13]); + } + + #[test] + fn master_linear_with_remainder() { + // k0 = 5, k2 = 12 -> numBands = 2*INT(7/2) = 6, + // k2Achieved = 11, k2Diff = 1 > 0 -> incr = -1, k starts at 5: + // bump the last band by +1. fMaster spans 5..=12, 6 bands. + let fm = master_table(5, 12, 0, false).unwrap(); + assert_eq!(*fm.first().unwrap(), 5); + assert_eq!(*fm.last().unwrap(), 12); + assert_eq!(fm.len(), 7); // numBands + 1 + // Strictly increasing (all vDk > 0). + assert!(fm.windows(2).all(|w| w[1] > w[0])); + } + + #[test] + fn master_linear_alter_scale_dk2() { + // bs_alter_scale = 1 -> dk = 2. + // k0 = 4, k2 = 16 -> numBands = 2*NINT(12/4) = 6, + // k2Achieved = 4 + 6*2 = 16, k2Diff = 0 -> 6 bands of width 2. + let fm = master_table(4, 16, 0, true).unwrap(); + assert_eq!(fm, vec![4, 6, 8, 10, 12, 14, 16]); + } + + #[test] + fn master_rejects_k2_le_k0() { + assert_eq!( + master_table(20, 20, 0, false), + Err(Error::SbrFreqBandInvalid) + ); + assert_eq!( + master_table(20, 10, 1, false), + Err(Error::SbrFreqBandInvalid) + ); + } + + #[test] + fn master_warped_single_region_monotone() { + // k2/k0 = 28/14 = 2.0 <= 2.2449 -> single region. + // bs_freq_scale = 1 -> bands = 12. The §4.6.18.3.6 `vDk0(i) > 0` + // requirement holds for this range, so the table is well-defined, + // strictly increasing, and spans [k0, k2]. + let fm = master_table(14, 28, 1, false).unwrap(); + assert_eq!(*fm.first().unwrap(), 14); + assert_eq!(*fm.last().unwrap(), 28); + assert!(fm.windows(2).all(|w| w[1] > w[0])); + } + + #[test] + fn master_warped_two_region_monotone() { + // k2/k0 = 32/12 ≈ 2.667 > 2.2449 -> two regions, k1 = 2*k0 = 24. + // bs_freq_scale = 2 -> bands = 10 (the §4.6.18.3.6 `vDk > 0` + // requirement holds for both regions at this geometry). + let fm = master_table(12, 32, 2, false).unwrap(); + assert_eq!(*fm.first().unwrap(), 12); + assert_eq!(*fm.last().unwrap(), 32); + assert!(fm.windows(2).all(|w| w[1] > w[0])); + // Crossover region boundary k1 = 2*k0 = 24 must be a border. + assert!(fm.contains(&24)); + } + + #[test] + fn derive_high_low_noise_geometry() { + // Build a clean linear master, then derive. + // k0 = 5, k2 = 13 -> fMaster = 5..=13 (NMaster = 8). + let fm = master_table(5, 13, 0, false).unwrap(); + let t = HiLoTables::derive(&fm, 2, 2).unwrap(); + + // NHigh = NMaster - xover = 8 - 2 = 6. + assert_eq!(t.n_high(), 6); + // fTableHigh = fMaster[2..=8] = 7..=13. + assert_eq!(t.f_table_high, vec![7, 8, 9, 10, 11, 12, 13]); + // k_x = 7, M = 13 - 7 = 6. + assert_eq!(t.k_x, 7); + assert_eq!(t.m, 6); + + // NHigh = 6 (even): NLow = INT(6/2) + (6 - 2*3) = 3. + assert_eq!(t.n_low(), 3); + // parity = 0 (NHigh even): i(k) = 2k. fTableLow = high[0,2,4,6]. + assert_eq!(t.f_table_low, vec![7, 9, 11, 13]); + + // fTableLow(0) is always the first noise border; tables strictly + // increasing; last border == k2 of the range. + assert_eq!(t.f_table_noise[0], 7); + assert_eq!(*t.f_table_noise.last().unwrap(), 13); + assert!(t.f_table_noise.windows(2).all(|w| w[1] > w[0])); + } + + #[test] + fn derive_odd_nhigh_parity() { + // Force an odd NHigh. k0 = 5, k2 = 12 -> fMaster has 7 entries + // (NMaster = 6); xover = 1 -> NHigh = 5 (odd). + let fm = master_table(5, 12, 0, false).unwrap(); + let t = HiLoTables::derive(&fm, 1, 1).unwrap(); + assert_eq!(t.n_high(), 5); + // NHigh odd: NLow = INT(5/2) + (5 - 2*2) = 2 + 1 = 3. + assert_eq!(t.n_low(), 3); + // parity = 1: i(0)=0, i(k) = 2k - 1 -> high[0,1,3,5]. + let h = &t.f_table_high; + assert_eq!(t.f_table_low, vec![h[0], h[1], h[3], h[5]]); + } + + #[test] + fn derive_noise_bands_zero_single_band() { + let fm = master_table(5, 13, 0, false).unwrap(); + let t = HiLoTables::derive(&fm, 2, 0).unwrap(); + // bs_noise_bands == 0 -> NQ = 1 (two borders). + assert_eq!(t.n_q(), 1); + assert_eq!(t.f_table_noise.len(), 2); + assert_eq!(t.f_table_noise[0], t.f_table_low[0]); + assert_eq!( + *t.f_table_noise.last().unwrap(), + *t.f_table_low.last().unwrap() + ); + } + + #[test] + fn derive_rejects_xover_ge_nmaster() { + let fm = master_table(5, 13, 0, false).unwrap(); // NMaster = 8 + assert_eq!( + HiLoTables::derive(&fm, 8, 1), + Err(Error::SbrFreqBandInvalid) + ); + assert_eq!( + HiLoTables::derive(&fm, 9, 1), + Err(Error::SbrFreqBandInvalid) + ); + } + + #[test] + fn end_to_end_44100_typical() { + // An HE-AAC 44.1 kHz config wired end-to-end from FsSBR through + // the derived tables: + // FsSBR = 88200, bs_start_freq = 5, bs_stop_freq = 5, + // bs_freq_scale = 0 (linear), bs_alter_scale = 0, + // bs_xover_band = 1, bs_noise_bands = 2. + // Linear scale (bs_freq_scale == 0) is chosen here because it is + // well-defined for every k0/k2 pair; the warped scale is exercised + // by the dedicated single/two-region tests, which pick geometries + // that satisfy the §4.6.18.3.6 `vDk0(i) > 0` requirement. + let k0v = k0(88200, 5).unwrap(); + let k2v = k2(88200, 5, k0v).unwrap(); + assert!(k2v > k0v); + let fm = master_table(k0v, k2v, 0, false).unwrap(); + let t = HiLoTables::derive(&fm, 1, 2).unwrap(); + // k_x = fTableHigh(0) = fMaster(bs_xover_band); M spans from there + // to the top of the master table. The geometry is self-consistent. + assert_eq!(t.k_x, fm[1]); + assert_eq!(t.m, fm[fm.len() - 1] - fm[1]); + // §4.6.18.3.6: k_x <= 32 and k_x + M <= 64. + assert!(t.k_x <= 32); + assert!(t.k_x + t.m <= 64); + // Every derived table is strictly increasing. + assert!(t.f_table_high.windows(2).all(|w| w[1] > w[0])); + assert!(t.f_table_low.windows(2).all(|w| w[1] > w[0])); + assert!(t.f_table_noise.windows(2).all(|w| w[1] > w[0])); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_grid.rs b/crates/vendor/oxideav-aac/src/sbr_grid.rs new file mode 100644 index 00000000..5992b0ba --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_grid.rs @@ -0,0 +1,464 @@ +//! `sbr_grid()` / `sbr_dtdf()` / `sbr_invf()` — ISO/IEC 14496-3 +//! §4.4.2.8, Tables 4.69–4.71. +//! +//! The SBR time-frequency grid describes how a frame's QMF time slots +//! are partitioned into SBR *envelopes* and *noise floors*, and which +//! frequency resolution (high / low) each envelope uses. It is the +//! variable-length heart of an SBR data element: `sbr_envelope()` and +//! `sbr_noise()` are sized entirely by the grid (`bs_num_env` envelopes +//! and `bs_num_noise` noise floors). +//! +//! Four frame classes (Table 4.69) describe the slot layout: +//! +//! * `FIXFIX` (0) — a fixed number of equal-length envelopes +//! (`bs_num_env = 2^bs_num_env_raw`, the raw value being a 2-bit +//! field). A single envelope forces `bs_amp_res = 0`. All envelopes +//! share one transmitted frequency resolution. +//! * `FIXVAR` (1) — a fixed leading border plus a variable trailing +//! border list; envelopes are counted by `bs_num_rel_1 + 1` and the +//! frequency-resolution flags are transmitted in reverse order. +//! * `VARFIX` (2) — a variable leading border plus a fixed trailing +//! border; envelopes counted by `bs_num_rel_0 + 1`, freq-res in +//! forward order. +//! * `VARVAR` (3) — both borders variable; envelopes counted by +//! `bs_num_rel_0 + bs_num_rel_1 + 1`. +//! +//! For the variable classes the *envelope-count pointer* `bs_pointer` +//! is read as `ptr_bits = ceil(log2(bs_num_env + 1))` bits (Table 4.69 +//! Note 2: a true float log, not a truncated one). +//! +//! After the class-specific body, `bs_num_noise = (bs_num_env > 1) ? 2 +//! : 1`. +//! +//! `sbr_dtdf()` (Table 4.70) reads one delta-direction flag per +//! envelope (`bs_df_env`) and per noise floor (`bs_df_noise`): +//! `false` = delta in frequency (the first band is an absolute start +//! value), `true` = delta in time. +//! +//! `sbr_invf()` (Table 4.71) reads a 2-bit inverse-filtering mode per +//! noise band (`NQ`, taken from the derived noise band table). +//! +//! All three are fixed-/variable-width *syntax* only — no Huffman — so +//! they are fully recoverable from the spec tables. The actual border +//! reconstruction, envelope dequantization, and QMF synthesis are +//! downstream of this parse. + +use crate::{Error, Result}; +use oxideav_core::bits::BitReader; + +/// SBR frame class (`bs_frame_class`, Table 4.69 switch). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameClass { + /// `FIXFIX` (0) — fixed start, fixed stop; equal-length envelopes. + FixFix, + /// `FIXVAR` (1) — fixed start, variable stop. + FixVar, + /// `VARFIX` (2) — variable start, fixed stop. + VarFix, + /// `VARVAR` (3) — variable start, variable stop. + VarVar, +} + +impl FrameClass { + fn from_bits(v: u32) -> Self { + match v & 0b11 { + 0 => FrameClass::FixFix, + 1 => FrameClass::FixVar, + 2 => FrameClass::VarFix, + _ => FrameClass::VarVar, + } + } + + /// The 2-bit `bs_frame_class` wire value. + pub fn to_bits(self) -> u32 { + match self { + FrameClass::FixFix => 0, + FrameClass::FixVar => 1, + FrameClass::VarFix => 2, + FrameClass::VarVar => 3, + } + } +} + +/// The maximum number of SBR envelopes per frame (§4.6.18.3.6). Used to +/// bound the variable border lists so a corrupt grid cannot allocate +/// without limit. +pub const SBR_MAX_NUM_ENV: usize = 5; + +/// A parsed `sbr_grid()` (Table 4.69) for one channel. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrGrid { + /// `bs_frame_class`. + pub frame_class: FrameClass, + /// `bs_num_env[ch]` — number of envelopes in this frame. + pub num_env: usize, + /// `bs_num_noise[ch]` — number of noise floors (`1` or `2`). + pub num_noise: usize, + /// `bs_freq_res[ch][env]` — per-envelope frequency-resolution flag + /// (`true` = high resolution). Length is [`Self::num_env`]. + pub freq_res: Vec, + /// `bs_var_bord_0[ch]` — variable leading border (VARFIX / VARVAR), + /// else `0`. + pub var_bord_0: u8, + /// `bs_var_bord_1[ch]` — variable trailing border (FIXVAR / + /// VARVAR), else `0`. + pub var_bord_1: u8, + /// `bs_rel_bord_0[ch][..]` — relative leading borders (VARFIX / + /// VARVAR). Each element is the *raw* 2-bit value; the reconstructed + /// border is `2·raw + 2`. + pub rel_bord_0: Vec, + /// `bs_rel_bord_1[ch][..]` — relative trailing borders (FIXVAR / + /// VARVAR). Raw 2-bit values; reconstructed `2·raw + 2`. + pub rel_bord_1: Vec, + /// `bs_pointer[ch]` — the envelope-count pointer for the variable + /// classes (`0` for FIXFIX). + pub pointer: u32, + /// Whether this grid forced `bs_amp_res = 0` (single-envelope + /// FIXFIX). The caller applies this override to the element-level + /// `bs_amp_res`. + pub amp_res_override: bool, +} + +/// `ptr_bits = ceil(log2(num_env + 1))` (Table 4.69 Note 2: a true +/// float division / log, not a truncated one). For `num_env + 1` a +/// power of two this is exactly `log2`; otherwise it rounds up. +fn ptr_bits(num_env: usize) -> u32 { + let n = (num_env + 1) as u32; + // ceil(log2(n)): the position of the highest set bit, plus one if n + // is not itself a power of two. + if n <= 1 { + 0 + } else { + let floor_log2 = 31 - n.leading_zeros(); + if n.is_power_of_two() { + floor_log2 + } else { + floor_log2 + 1 + } + } +} + +impl SbrGrid { + /// Parse `sbr_grid()` (Table 4.69) for channel `ch` from `reader`. + /// + /// `num_env` is bounded by [`SBR_MAX_NUM_ENV`]; a value beyond it + /// (only reachable for a corrupt VARVAR grid) yields + /// [`Error::SbrGridInvalid`]. + pub fn parse(reader: &mut BitReader<'_>) -> Result { + let frame_class = FrameClass::from_bits(read(reader, 2)?); + let mut var_bord_0 = 0u8; + let mut var_bord_1 = 0u8; + let mut rel_bord_0: Vec = Vec::new(); + let mut rel_bord_1: Vec = Vec::new(); + let mut pointer = 0u32; + let mut amp_res_override = false; + + let (num_env, freq_res) = match frame_class { + FrameClass::FixFix => { + let raw = read(reader, 2)?; + let num_env = 1usize << raw; // bs_num_env = 2^tmp. + check_num_env(num_env)?; + if num_env == 1 { + amp_res_override = true; // bs_amp_res = 0. + } + let fr0 = read_flag(reader)?; + // All envelopes share bs_freq_res[ch][0]. + let freq_res = vec![fr0; num_env]; + (num_env, freq_res) + } + FrameClass::FixVar => { + var_bord_1 = read(reader, 2)? as u8; + let num_rel_1 = read(reader, 2)? as usize; + let num_env = num_rel_1 + 1; + check_num_env(num_env)?; + for _ in 0..num_env - 1 { + rel_bord_1.push(read(reader, 2)? as u8); + } + pointer = read(reader, ptr_bits(num_env))?; + // Frequency-resolution flags transmitted in reverse: + // bs_freq_res[ch][num_env - 1 - env]. + let mut freq_res = vec![false; num_env]; + for env in 0..num_env { + freq_res[num_env - 1 - env] = read_flag(reader)?; + } + (num_env, freq_res) + } + FrameClass::VarFix => { + var_bord_0 = read(reader, 2)? as u8; + let num_rel_0 = read(reader, 2)? as usize; + let num_env = num_rel_0 + 1; + check_num_env(num_env)?; + for _ in 0..num_env - 1 { + rel_bord_0.push(read(reader, 2)? as u8); + } + pointer = read(reader, ptr_bits(num_env))?; + // Forward order. + let mut freq_res = Vec::with_capacity(num_env); + for _ in 0..num_env { + freq_res.push(read_flag(reader)?); + } + (num_env, freq_res) + } + FrameClass::VarVar => { + var_bord_0 = read(reader, 2)? as u8; + var_bord_1 = read(reader, 2)? as u8; + let num_rel_0 = read(reader, 2)? as usize; + let num_rel_1 = read(reader, 2)? as usize; + let num_env = num_rel_0 + num_rel_1 + 1; + check_num_env(num_env)?; + for _ in 0..num_rel_0 { + rel_bord_0.push(read(reader, 2)? as u8); + } + for _ in 0..num_rel_1 { + rel_bord_1.push(read(reader, 2)? as u8); + } + pointer = read(reader, ptr_bits(num_env))?; + let mut freq_res = Vec::with_capacity(num_env); + for _ in 0..num_env { + freq_res.push(read_flag(reader)?); + } + (num_env, freq_res) + } + }; + + let num_noise = if num_env > 1 { 2 } else { 1 }; + + Ok(SbrGrid { + frame_class, + num_env, + num_noise, + freq_res, + var_bord_0, + var_bord_1, + rel_bord_0, + rel_bord_1, + pointer, + amp_res_override, + }) + } +} + +/// `sbr_dtdf()` (Table 4.70) — the delta-coding direction flags for a +/// channel's envelopes and noise floors. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrDtdf { + /// `bs_df_env[ch][env]` — `false` = delta in frequency (absolute + /// start band), `true` = delta in time. Length = `num_env`. + pub df_env: Vec, + /// `bs_df_noise[ch][noise]` — same convention. Length = `num_noise`. + pub df_noise: Vec, +} + +impl SbrDtdf { + /// Parse `sbr_dtdf()` (Table 4.70). `num_env` / `num_noise` come + /// from the channel's already-parsed [`SbrGrid`]. + pub fn parse(reader: &mut BitReader<'_>, num_env: usize, num_noise: usize) -> Result { + let mut df_env = Vec::with_capacity(num_env); + for _ in 0..num_env { + df_env.push(read_flag(reader)?); + } + let mut df_noise = Vec::with_capacity(num_noise); + for _ in 0..num_noise { + df_noise.push(read_flag(reader)?); + } + Ok(SbrDtdf { df_env, df_noise }) + } +} + +/// `sbr_invf()` (Table 4.71) — the 2-bit inverse-filtering mode per +/// noise band. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SbrInvf { + /// `bs_invf_mode[ch][n]` — one mode (0..=3) per noise band (`NQ`). + pub invf_mode: Vec, +} + +impl SbrInvf { + /// Parse `sbr_invf()` (Table 4.71). `num_noise_bands` is `NQ` from + /// the derived noise band table + /// ([`crate::sbr_freq_bands::HiLoTables::n_q`]). + pub fn parse(reader: &mut BitReader<'_>, num_noise_bands: usize) -> Result { + let mut invf_mode = Vec::with_capacity(num_noise_bands); + for _ in 0..num_noise_bands { + invf_mode.push(read(reader, 2)? as u8); + } + Ok(SbrInvf { invf_mode }) + } +} + +#[inline] +fn read(reader: &mut BitReader<'_>, n: u32) -> Result { + reader.read_u32(n).map_err(|_| Error::SbrGridInvalid) +} + +#[inline] +fn read_flag(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::SbrGridInvalid) +} + +#[inline] +fn check_num_env(num_env: usize) -> Result<()> { + if num_env == 0 || num_env > SBR_MAX_NUM_ENV { + Err(Error::SbrGridInvalid) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitWriter; + + #[test] + fn ptr_bits_matches_ceil_log2() { + // ceil(log2(n+1)) for n = num_env. + assert_eq!(ptr_bits(1), 1); // ceil(log2 2) = 1 + assert_eq!(ptr_bits(2), 2); // ceil(log2 3) = 2 + assert_eq!(ptr_bits(3), 2); // ceil(log2 4) = 2 + assert_eq!(ptr_bits(4), 3); // ceil(log2 5) = 3 + assert_eq!(ptr_bits(5), 3); // ceil(log2 6) = 3 + } + + #[test] + fn fixfix_single_env_forces_amp_res() { + let mut w = BitWriter::new(); + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(0, 2); // 2^0 = 1 envelope + w.write_bit(true); // freq_res[0] + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let g = SbrGrid::parse(&mut r).unwrap(); + assert_eq!(g.frame_class, FrameClass::FixFix); + assert_eq!(g.num_env, 1); + assert_eq!(g.num_noise, 1); + assert_eq!(g.freq_res, vec![true]); + assert!(g.amp_res_override); + } + + #[test] + fn fixfix_four_env_shares_freq_res() { + let mut w = BitWriter::new(); + w.write_u32(FrameClass::FixFix.to_bits(), 2); + w.write_u32(2, 2); // 2^2 = 4 envelopes + w.write_bit(false); // freq_res[0] shared by all + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let g = SbrGrid::parse(&mut r).unwrap(); + assert_eq!(g.num_env, 4); + assert_eq!(g.num_noise, 2); + assert_eq!(g.freq_res, vec![false; 4]); + assert!(!g.amp_res_override); + } + + #[test] + fn fixvar_reverses_freq_res() { + // num_rel_1 = 2 → num_env = 3. Frequency-resolution flags are + // transmitted as bs_freq_res[num_env-1-env]. + let mut w = BitWriter::new(); + w.write_u32(FrameClass::FixVar.to_bits(), 2); + w.write_u32(1, 2); // var_bord_1 + w.write_u32(2, 2); // num_rel_1 = 2 → num_env = 3 + w.write_u32(0, 2); // rel_bord_1[0] + w.write_u32(3, 2); // rel_bord_1[1] + // ptr_bits(3) = 2. + w.write_u32(1, 2); // pointer + // freq_res transmitted reversed: index 2, then 1, then 0. + w.write_bit(true); // -> freq_res[2] + w.write_bit(false); // -> freq_res[1] + w.write_bit(true); // -> freq_res[0] + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let g = SbrGrid::parse(&mut r).unwrap(); + assert_eq!(g.frame_class, FrameClass::FixVar); + assert_eq!(g.num_env, 3); + assert_eq!(g.var_bord_1, 1); + assert_eq!(g.rel_bord_1, vec![0, 3]); + assert_eq!(g.pointer, 1); + assert_eq!(g.freq_res, vec![true, false, true]); + } + + #[test] + fn varfix_forward_freq_res() { + let mut w = BitWriter::new(); + w.write_u32(FrameClass::VarFix.to_bits(), 2); + w.write_u32(2, 2); // var_bord_0 + w.write_u32(1, 2); // num_rel_0 = 1 → num_env = 2 + w.write_u32(3, 2); // rel_bord_0[0] + // ptr_bits(2) = 2. + w.write_u32(0, 2); // pointer + w.write_bit(false); // freq_res[0] + w.write_bit(true); // freq_res[1] + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let g = SbrGrid::parse(&mut r).unwrap(); + assert_eq!(g.frame_class, FrameClass::VarFix); + assert_eq!(g.num_env, 2); + assert_eq!(g.var_bord_0, 2); + assert_eq!(g.rel_bord_0, vec![3]); + assert_eq!(g.freq_res, vec![false, true]); + } + + #[test] + fn varvar_both_borders() { + let mut w = BitWriter::new(); + w.write_u32(FrameClass::VarVar.to_bits(), 2); + w.write_u32(1, 2); // var_bord_0 + w.write_u32(2, 2); // var_bord_1 + w.write_u32(1, 2); // num_rel_0 = 1 + w.write_u32(1, 2); // num_rel_1 = 1 → num_env = 3 + w.write_u32(0, 2); // rel_bord_0[0] + w.write_u32(3, 2); // rel_bord_1[0] + // ptr_bits(3) = 2. + w.write_u32(2, 2); // pointer + w.write_bit(true); + w.write_bit(false); + w.write_bit(true); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let g = SbrGrid::parse(&mut r).unwrap(); + assert_eq!(g.frame_class, FrameClass::VarVar); + assert_eq!(g.num_env, 3); + assert_eq!(g.num_noise, 2); + assert_eq!(g.var_bord_0, 1); + assert_eq!(g.var_bord_1, 2); + assert_eq!(g.rel_bord_0, vec![0]); + assert_eq!(g.rel_bord_1, vec![3]); + assert_eq!(g.pointer, 2); + assert_eq!(g.freq_res, vec![true, false, true]); + } + + #[test] + fn dtdf_reads_per_env_and_noise() { + let mut w = BitWriter::new(); + w.write_bit(true); // df_env[0] + w.write_bit(false); // df_env[1] + w.write_bit(true); // df_noise[0] + w.write_bit(false); // df_noise[1] + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let d = SbrDtdf::parse(&mut r, 2, 2).unwrap(); + assert_eq!(d.df_env, vec![true, false]); + assert_eq!(d.df_noise, vec![true, false]); + } + + #[test] + fn invf_reads_two_bits_per_band() { + let mut w = BitWriter::new(); + w.write_u32(0, 2); + w.write_u32(1, 2); + w.write_u32(2, 2); + w.write_u32(3, 2); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let inv = SbrInvf::parse(&mut r, 4).unwrap(); + assert_eq!(inv.invf_mode, vec![0, 1, 2, 3]); + } + + #[test] + fn truncated_grid_errors() { + let bytes = [0u8; 0]; + let mut r = BitReader::new(&bytes); + assert!(matches!(SbrGrid::parse(&mut r), Err(Error::SbrGridInvalid))); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_header.rs b/crates/vendor/oxideav-aac/src/sbr_header.rs new file mode 100644 index 00000000..d074f904 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_header.rs @@ -0,0 +1,350 @@ +//! `sbr_header()` parser — ISO/IEC 14496-3 §4.4.2.8, Table 4.63. +//! +//! The SBR header carries the static per-stream parameters that drive +//! the §4.6.18.3.2 frequency-band setup ([`crate::sbr_freq_bands`]): +//! the start / stop frequency indices, the crossover band, and the two +//! optional "extra" header blocks (`bs_header_extra_1` / +//! `bs_header_extra_2`). Per Table 4.63 Note 3, when an extra-header +//! flag is clear the underlying elements take their **default** values, +//! disregarding any previously transmitted value: +//! +//! | element | width | default (Tables 4.105–4.111) | +//! |---------|-------|------------------------------| +//! | `bs_freq_scale` | 2 | 2 (10 bands/octave) | +//! | `bs_alter_scale` | 1 | 1 (grouping / extra-wide) | +//! | `bs_noise_bands` | 2 | 2 (2 bands/octave) | +//! | `bs_limiter_bands` | 2 | 2 (2.0 bands/octave) | +//! | `bs_limiter_gains` | 2 | 2 (3 dB max gain) | +//! | `bs_interpol_freq` | 1 | 1 (interpolation on) | +//! | `bs_smoothing_mode`| 1 | 1 (smoothing off) | +//! +//! `bs_amp_res` (the envelope amplitude resolution: 0 = 1.5 dB, +//! 1 = 3.0 dB) is carried in the header but may be overridden to 0 by +//! `sbr_grid()` for a single-envelope `FIXFIX` frame — that override is +//! applied downstream, not here. +//! +//! The header is a fixed-width bit layout with no Huffman or +//! variable-length content, so it is fully recoverable from the spec +//! syntax table alone. + +use crate::{Error, Result}; +use oxideav_core::bits::BitReader; + +/// Default `bs_freq_scale` when `bs_header_extra_1 == 0` (Table 4.105: +/// 10 bands/octave). +pub const DEFAULT_FREQ_SCALE: u8 = 2; +/// Default `bs_alter_scale` when `bs_header_extra_1 == 0` (Table 4.106). +pub const DEFAULT_ALTER_SCALE: bool = true; +/// Default `bs_noise_bands` when `bs_header_extra_1 == 0` (Table 4.107: +/// 2 bands/octave). +pub const DEFAULT_NOISE_BANDS: u8 = 2; +/// Default `bs_limiter_bands` when `bs_header_extra_2 == 0` (Table +/// 4.108: 2.0 bands/octave). +pub const DEFAULT_LIMITER_BANDS: u8 = 2; +/// Default `bs_limiter_gains` when `bs_header_extra_2 == 0` (Table +/// 4.109: 3 dB max gain). +pub const DEFAULT_LIMITER_GAINS: u8 = 2; +/// Default `bs_interpol_freq` when `bs_header_extra_2 == 0` (Table +/// 4.110: interpolation on). +pub const DEFAULT_INTERPOL_FREQ: bool = true; +/// Default `bs_smoothing_mode` when `bs_header_extra_2 == 0` (Table +/// 4.111: smoothing off). +pub const DEFAULT_SMOOTHING_MODE: bool = true; + +/// A parsed `sbr_header()` (Table 4.63). +/// +/// The two `bs_header_extra_*` flags are recorded so a re-encoder can +/// reproduce the exact bit layout, but the underlying parameters are +/// already resolved to their effective values (the Table 4.63 Note 3 +/// defaults are filled in when an extra flag is clear). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SbrHeader { + /// `bs_amp_res` — envelope amplitude resolution (false = 1.5 dB, + /// true = 3.0 dB). May be forced to false by a single-envelope + /// `FIXFIX` grid downstream. + pub amp_res: bool, + /// `bs_start_freq` — 4-bit index into the §4.6.18.3.2.1 `offset` + /// table that sets the low QMF boundary `k0`. + pub start_freq: u8, + /// `bs_stop_freq` — 4-bit index that sets the high QMF boundary + /// `k2`. + pub stop_freq: u8, + /// `bs_xover_band` — 3-bit index into the master frequency table + /// where the SBR range begins (`bs_xover_band < NMaster`). + pub xover_band: u8, + /// `bs_reserved` — 2 reserved bits (kept for faithful re-encode). + pub reserved: u8, + /// `bs_header_extra_1` — whether the optional header part 1 was + /// transmitted. + pub header_extra_1: bool, + /// `bs_header_extra_2` — whether the optional header part 2 was + /// transmitted. + pub header_extra_2: bool, + /// `bs_freq_scale` — master-table warping selector (Table 4.105). + pub freq_scale: u8, + /// `bs_alter_scale` — master-table alteration flag (Table 4.106). + pub alter_scale: bool, + /// `bs_noise_bands` — noise-band density selector (Table 4.107). + pub noise_bands: u8, + /// `bs_limiter_bands` — limiter-band density selector (Table 4.108). + pub limiter_bands: u8, + /// `bs_limiter_gains` — limiter max-gain selector (Table 4.109). + pub limiter_gains: u8, + /// `bs_interpol_freq` — frequency-interpolation flag (Table 4.110). + pub interpol_freq: bool, + /// `bs_smoothing_mode` — smoothing flag (Table 4.111). + pub smoothing_mode: bool, +} + +impl SbrHeader { + /// Parse `sbr_header()` (Table 4.63) from `reader`, filling in the + /// Table 4.63 Note 3 default values for any extra-header block that + /// is not present. + /// + /// The `bs_reserved` field is read but not validated (Table 4.63 + /// leaves its value unconstrained). Returns [`Error::SbrHuffInvalid`] + /// only if `reader` runs out of bits — there is no Huffman content + /// here, so this maps the bit-exhaustion error onto the SBR error + /// surface. + pub fn parse(reader: &mut BitReader<'_>) -> Result { + let amp_res = read_bit(reader)?; + let start_freq = read_u8(reader, 4)?; + let stop_freq = read_u8(reader, 4)?; + let xover_band = read_u8(reader, 3)?; + let reserved = read_u8(reader, 2)?; + let header_extra_1 = read_bit(reader)?; + let header_extra_2 = read_bit(reader)?; + + let (freq_scale, alter_scale, noise_bands) = if header_extra_1 { + (read_u8(reader, 2)?, read_bit(reader)?, read_u8(reader, 2)?) + } else { + (DEFAULT_FREQ_SCALE, DEFAULT_ALTER_SCALE, DEFAULT_NOISE_BANDS) + }; + + let (limiter_bands, limiter_gains, interpol_freq, smoothing_mode) = if header_extra_2 { + ( + read_u8(reader, 2)?, + read_u8(reader, 2)?, + read_bit(reader)?, + read_bit(reader)?, + ) + } else { + ( + DEFAULT_LIMITER_BANDS, + DEFAULT_LIMITER_GAINS, + DEFAULT_INTERPOL_FREQ, + DEFAULT_SMOOTHING_MODE, + ) + }; + + Ok(SbrHeader { + amp_res, + start_freq, + stop_freq, + xover_band, + reserved, + header_extra_1, + header_extra_2, + freq_scale, + alter_scale, + noise_bands, + limiter_bands, + limiter_gains, + interpol_freq, + smoothing_mode, + }) + } + + /// Whether this header differs from `other` in any field that + /// affects the §4.6.18.3.2 frequency-band geometry + /// (`bs_start_freq`, `bs_stop_freq`, `bs_xover_band`, + /// `bs_freq_scale`, `bs_alter_scale`, `bs_noise_bands`). + /// + /// SBR decoders only need to recompute the master / derived band + /// tables when one of these "reset" parameters changes; the limiter + /// / interpolation / smoothing parameters do not alter the band + /// geometry. This mirrors the §4.6.18.3.3 header-change reset. + pub fn band_geometry_changed(&self, other: &SbrHeader) -> bool { + self.start_freq != other.start_freq + || self.stop_freq != other.stop_freq + || self.xover_band != other.xover_band + || self.freq_scale != other.freq_scale + || self.alter_scale != other.alter_scale + || self.noise_bands != other.noise_bands + } + + /// Compute the §4.6.18.3.2 derived frequency-band tables + /// ([`crate::sbr_freq_bands::HiLoTables`]) for this header at the + /// given SBR internal sample rate `fs_sbr` (twice the AAC core + /// rate). + /// + /// This chains [`crate::sbr_freq_bands::k0`] / + /// [`crate::sbr_freq_bands::k2`] / + /// [`crate::sbr_freq_bands::master_table`] / + /// [`crate::sbr_freq_bands::HiLoTables::derive`] with this header's + /// parameters; it returns [`Error::SbrFreqBandInvalid`] for any + /// §4.6.18.3.6-violating geometry. + pub fn derive_bands(&self, fs_sbr: u32) -> Result { + let k0 = crate::sbr_freq_bands::k0(fs_sbr, self.start_freq)?; + let k2 = crate::sbr_freq_bands::k2(fs_sbr, self.stop_freq, k0)?; + let f_master = + crate::sbr_freq_bands::master_table(k0, k2, self.freq_scale, self.alter_scale)?; + crate::sbr_freq_bands::HiLoTables::derive(&f_master, self.xover_band, self.noise_bands) + } +} + +#[inline] +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::SbrHuffInvalid) +} + +#[inline] +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + reader + .read_u32(n) + .map(|v| v as u8) + .map_err(|_| Error::SbrHuffInvalid) +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitWriter; + + /// Build a minimal header bitstream with both extra flags clear. + fn pack_minimal(amp_res: bool, start: u8, stop: u8, xover: u8, reserved: u8) -> Vec { + let mut w = BitWriter::new(); + w.write_bit(amp_res); + w.write_u32(start as u32, 4); + w.write_u32(stop as u32, 4); + w.write_u32(xover as u32, 3); + w.write_u32(reserved as u32, 2); + w.write_bit(false); // bs_header_extra_1 + w.write_bit(false); // bs_header_extra_2 + w.finish() + } + + #[test] + fn minimal_header_uses_defaults() { + let bytes = pack_minimal(true, 5, 6, 4, 0b10); + let mut r = BitReader::new(&bytes); + let h = SbrHeader::parse(&mut r).unwrap(); + assert!(h.amp_res); + assert_eq!(h.start_freq, 5); + assert_eq!(h.stop_freq, 6); + assert_eq!(h.xover_band, 4); + assert_eq!(h.reserved, 0b10); + assert!(!h.header_extra_1); + assert!(!h.header_extra_2); + // Table 4.63 Note 3 defaults. + assert_eq!(h.freq_scale, DEFAULT_FREQ_SCALE); + assert_eq!(h.alter_scale, DEFAULT_ALTER_SCALE); + assert_eq!(h.noise_bands, DEFAULT_NOISE_BANDS); + assert_eq!(h.limiter_bands, DEFAULT_LIMITER_BANDS); + assert_eq!(h.limiter_gains, DEFAULT_LIMITER_GAINS); + assert_eq!(h.interpol_freq, DEFAULT_INTERPOL_FREQ); + assert_eq!(h.smoothing_mode, DEFAULT_SMOOTHING_MODE); + } + + #[test] + fn full_header_round_trip_values() { + let mut w = BitWriter::new(); + w.write_bit(false); // amp_res + w.write_u32(3, 4); // start_freq + w.write_u32(9, 4); // stop_freq + w.write_u32(2, 3); // xover_band + w.write_u32(0, 2); // reserved + w.write_bit(true); // header_extra_1 + w.write_bit(true); // header_extra_2 + w.write_u32(1, 2); // freq_scale + w.write_bit(false); // alter_scale + w.write_u32(3, 2); // noise_bands + w.write_u32(0, 2); // limiter_bands + w.write_u32(1, 2); // limiter_gains + w.write_bit(false); // interpol_freq + w.write_bit(false); // smoothing_mode + let bytes = w.finish(); + + let mut r = BitReader::new(&bytes); + let h = SbrHeader::parse(&mut r).unwrap(); + assert!(!h.amp_res); + assert_eq!(h.start_freq, 3); + assert_eq!(h.stop_freq, 9); + assert_eq!(h.xover_band, 2); + assert!(h.header_extra_1); + assert!(h.header_extra_2); + assert_eq!(h.freq_scale, 1); + assert!(!h.alter_scale); + assert_eq!(h.noise_bands, 3); + assert_eq!(h.limiter_bands, 0); + assert_eq!(h.limiter_gains, 1); + assert!(!h.interpol_freq); + assert!(!h.smoothing_mode); + } + + #[test] + fn truncated_header_errors() { + let bytes = [0x00u8]; // 8 bits, not enough for the 16-bit fixed prefix + let mut r = BitReader::new(&bytes); + assert!(matches!( + SbrHeader::parse(&mut r), + Err(Error::SbrHuffInvalid) + )); + } + + #[test] + fn band_geometry_change_detection() { + let a = pack_minimal(true, 5, 6, 4, 0); + let mut ra = BitReader::new(&a); + let ha = SbrHeader::parse(&mut ra).unwrap(); + + // Same geometry → no change. + let mut rb = BitReader::new(&a); + let hb = SbrHeader::parse(&mut rb).unwrap(); + assert!(!ha.band_geometry_changed(&hb)); + + // Different start_freq → change. + let c = pack_minimal(true, 7, 6, 4, 0); + let mut rc = BitReader::new(&c); + let hc = SbrHeader::parse(&mut rc).unwrap(); + assert!(ha.band_geometry_changed(&hc)); + } + + #[test] + fn derive_bands_matches_freq_band_module() { + // A representative 44.1 kHz core → fs_sbr = 88200. Use a header + // with the linear master scale (bs_freq_scale = 0), which is + // well-defined for every k0/k2 pair, and the known-good + // §4.6.18.3.6 geometry from the sbr_freq_bands end-to-end test: + // bs_start_freq = 5, bs_stop_freq = 5, bs_xover_band = 1, + // bs_alter_scale = 0, bs_noise_bands = 2. + let fs_sbr = 88_200; + let mut w = BitWriter::new(); + w.write_bit(false); // amp_res + w.write_u32(5, 4); // start_freq + w.write_u32(5, 4); // stop_freq + w.write_u32(1, 3); // xover_band + w.write_u32(0, 2); // reserved + w.write_bit(true); // header_extra_1 (so freq_scale is explicit) + w.write_bit(false); // header_extra_2 + w.write_u32(0, 2); // freq_scale = 0 (linear) + w.write_bit(false); // alter_scale = 0 + w.write_u32(2, 2); // noise_bands = 2 + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let h = SbrHeader::parse(&mut r).unwrap(); + assert_eq!(h.freq_scale, 0); + let bands = h.derive_bands(fs_sbr).unwrap(); + + let k0 = crate::sbr_freq_bands::k0(fs_sbr, h.start_freq).unwrap(); + let k2 = crate::sbr_freq_bands::k2(fs_sbr, h.stop_freq, k0).unwrap(); + let fm = crate::sbr_freq_bands::master_table(k0, k2, h.freq_scale, h.alter_scale).unwrap(); + let direct = + crate::sbr_freq_bands::HiLoTables::derive(&fm, h.xover_band, h.noise_bands).unwrap(); + assert_eq!(bands.f_table_high, direct.f_table_high); + assert_eq!(bands.f_table_low, direct.f_table_low); + assert_eq!(bands.f_table_noise, direct.f_table_noise); + assert_eq!(bands.m, direct.m); + assert_eq!(bands.k_x, direct.k_x); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_hf_gen.rs b/crates/vendor/oxideav-aac/src/sbr_hf_gen.rs new file mode 100644 index 00000000..927f323f --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_hf_gen.rs @@ -0,0 +1,556 @@ +//! SBR high-frequency generation — ISO/IEC 14496-3 §4.6.18.6. +//! +//! Builds the `XHigh` subband matrix from the analysis-filterbank +//! output `XLow`: +//! +//! * **Patch construction** (§4.6.18.6.3 / Figure 4.48) — the +//! `numPatches` / `patchStartSubband` / `patchNumSubbands` decision +//! that maps consecutive low-band source ranges onto the SBR range, +//! driven by `goalSb = NINT(2.048e6 / FsSBR)` and the `fMaster` +//! grid, with the trailing small-patch trim. +//! * **Inverse filtering** (§4.6.18.6.2) — the covariance-method +//! second-order linear prediction per low subband (`φk(i,j)` over +//! `numTimeSlots·RATE + 6` samples, `d(k)` with `εInv = 1e-6`, the +//! `α0(k)` / `α1(k)` solution, and the `|α| ≥ 4` reset), plus the +//! Table 4.175 `newBw` transition function and the `bwArray` chirp +//! blend (`0.75/0.25` attack, `0.90625/0.09375` decay, `< 0.015625` +//! flush to zero). +//! * **HF generator** (§4.6.18.6.3) — `XHigh(k, l + tHFAdj) = +//! XLow(p, …) + bw·α0(p)·XLow(p, l−1+…) + bw²·α1(p)·XLow(p, l−2+…)` +//! over the patch mapping, with the chirp factor selected by the +//! noise-floor band `g(k)`. +//! +//! Both `XLow` and `XHigh` are stored slot-major (`x[slot][band]`) +//! with the slot axis carrying the spec's absolute column index (the +//! `tHFGen`-slot history precedes the current frame, so spec index +//! `l + tHFAdj` is a direct column index). +//! +//! ## Provenance +//! +//! Every formula, constant, and branch is from the §4.6.18.6 text, +//! Table 4.175, and the Figure 4.48 flowchart of the staged spec. No +//! part of this implementation is derived from any external decoder. + +use crate::sbr_freq_bands::HiLoTables; +use crate::sbr_qmf::Complex; +use crate::{Error, Result}; + +/// `tHFAdj = 2` — the envelope-adjuster offset (§4.6.18.5). +pub const T_HF_ADJ: usize = 2; + +/// `tHFGen = 8` — the HF-generator offset (§4.6.18.5). +pub const T_HF_GEN: usize = 8; + +/// The §4.6.18.6.2 relaxation parameter `εInv`. +pub const EPS_INV: f64 = 1e-6; + +/// §4.6.18.3.6: `numPatches ≤ 5`. +pub const MAX_PATCHES: usize = 5; + +/// The §4.6.18.6.3 / Figure 4.48 patch layout. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Patches { + /// `patchStartSubband(i)` — first source QMF subband of patch `i`. + pub start: Vec, + /// `patchNumSubbands(i)` — subband count of patch `i`. + pub num: Vec, +} + +impl Patches { + /// `numPatches`. + #[inline] + #[must_use] + pub fn num_patches(&self) -> usize { + self.num.len() + } + + /// The §4.6.18.3.2.3 patch borders: `patchBorders(0) = kx`, + /// `patchBorders(k) = patchBorders(k-1) + patchNumSubbands(k-1)`. + #[must_use] + pub fn borders(&self, k_x: i32) -> Vec { + let mut b = Vec::with_capacity(self.num.len() + 1); + b.push(k_x); + for &n in &self.num { + b.push(b[b.len() - 1] + n as i32); + } + b + } +} + +/// Figure 4.48 — patch construction. +/// +/// `f_master` is the §4.6.18.3.2.1 master table (`fMaster(0..=NMaster)`), +/// `k0` its first subband, `k_x` / `m` the SBR range, and `fs_sbr` the +/// SBR internal rate driving `goalSb = NINT(2.048e6 / FsSBR)`. +pub fn build_patches(f_master: &[i32], k0: i32, k_x: i32, m: i32, fs_sbr: u32) -> Result { + if f_master.len() < 2 || fs_sbr == 0 { + return Err(Error::SbrFreqBandInvalid); + } + let n_master = f_master.len() - 1; + + let mut msb = k0; + let mut usb = k_x; + let mut start = Vec::new(); + let mut num = Vec::new(); + + // goalSb = NINT(2.048e6 / Fs). + let goal_sb = ((2.0 * 2.048e6 / f64::from(fs_sbr) + 1.0) / 2.0).floor() as i32; + // k: the first master index at/after goalSb (NMaster if goalSb is + // past the SBR stop border). + let mut k = if goal_sb < k_x + m { + let mut kk = 0usize; + for (i, &f) in f_master.iter().enumerate() { + if f < goal_sb { + kk = i + 1; + } else { + break; + } + } + kk + } else { + n_master + }; + + let mut sb; + let mut guard = 0usize; + loop { + guard += 1; + if guard > 64 { + return Err(Error::SbrFreqBandInvalid); + } + // Walk j downward from k until the patch source fits under the + // first master subband: sb <= k0 - 1 + msb - odd. + let mut j = k; + let odd = loop { + if j >= f_master.len() { + return Err(Error::SbrFreqBandInvalid); + } + sb = f_master[j]; + let odd = (sb - 2 + k0).rem_euclid(2); + if sb <= k0 - 1 + msb - odd { + break odd; + } + if j == 0 { + return Err(Error::SbrFreqBandInvalid); + } + j -= 1; + }; + + let n = (sb - usb).max(0); + let s = k0 - odd - n; + if n > 0 { + if s < 0 || start.len() >= MAX_PATCHES { + return Err(Error::SbrFreqBandInvalid); + } + start.push(s as usize); + num.push(n as usize); + usb = sb; + msb = sb; + } else { + msb = k_x; + } + + if f_master[k] - sb < 3 { + k = n_master; + } + if sb == k_x + m { + break; + } + } + + // Trailing small-patch trim: drop a final patch narrower than 3 + // subbands when more than one patch was built. + if num.len() > 1 && *num.last().unwrap() < 3 { + num.pop(); + start.pop(); + } + + Ok(Patches { start, num }) +} + +/// Table 4.175 — `newBw(bs_invf_mode´, bs_invf_mode)`. Row is the +/// previous frame's mode, column the current one (both `0..=3` for +/// Off / Low / Intermediate / Strong). +#[must_use] +pub fn new_bw(prev_mode: u8, cur_mode: u8) -> f64 { + const TABLE: [[f64; 4]; 4] = [ + [0.0, 0.6, 0.9, 0.98], + [0.6, 0.75, 0.9, 0.98], + [0.0, 0.75, 0.9, 0.98], + [0.0, 0.75, 0.9, 0.98], + ]; + TABLE[usize::from(prev_mode.min(3))][usize::from(cur_mode.min(3))] +} + +/// §4.6.18.6.2 chirp-factor update: one `bwArray` entry per noise +/// band. `prev_invf` / `prev_bw` are the previous SBR frame's values +/// (all zero for the first frame). +#[must_use] +pub fn chirp_factors(cur_invf: &[u8], prev_invf: &[u8], prev_bw: &[f64]) -> Vec { + cur_invf + .iter() + .enumerate() + .map(|(i, &cur)| { + let prev_mode = prev_invf.get(i).copied().unwrap_or(0); + let bw_prev = prev_bw.get(i).copied().unwrap_or(0.0); + let nb = new_bw(prev_mode, cur); + let temp = if nb < bw_prev { + 0.75 * nb + 0.25 * bw_prev + } else { + 0.90625 * nb + 0.09375 * bw_prev + }; + if temp < 0.015625 { + 0.0 + } else { + temp + } + }) + .collect() +} + +/// §4.6.18.6.2 covariance-method prediction coefficients +/// `(α0(k), α1(k))` for low subband `k`. +/// +/// `x_low` is slot-major with the spec's absolute column index (the +/// covariance windows over `n − i + tHFAdj` for +/// `0 ≤ n < n_slots_frame + 6`), so `x_low` must carry at least +/// `n_slots_frame + 6 + tHFAdj` columns. +pub fn prediction_coefficients( + x_low: &[[Complex; 32]], + k: usize, + n_slots_frame: usize, +) -> Result<(Complex, Complex)> { + if k >= 32 || x_low.len() < n_slots_frame + 6 + T_HF_ADJ { + return Err(Error::SbrFreqBandInvalid); + } + // φk(i, j) = Σ_n XLow(k, n - i + tHFAdj) · XLow*(k, n - j + tHFAdj). + let phi = |i: usize, j: usize| -> Complex { + let mut acc = Complex::default(); + for n in 0..(n_slots_frame + 6) { + let a = x_low[n + T_HF_ADJ - i][k]; + let b = x_low[n + T_HF_ADJ - j][k]; + acc += a * b.conj(); + } + acc + }; + let phi01 = phi(0, 1); + let phi02 = phi(0, 2); + let phi11 = phi(1, 1); + let phi12 = phi(1, 2); + let phi22 = phi(2, 2); + + // d(k) = φ(2,2)·φ(1,1) − |φ(1,2)|² / (1 + εInv). φ(1,1) / φ(2,2) + // are real by construction. + let d = phi22.re * phi11.re - phi12.norm_sqr() / (1.0 + EPS_INV); + + let alpha1 = if d != 0.0 { + let numer = phi01 * phi12 - phi02 * phi11.re; + Complex::new(numer.re / d, numer.im / d) + } else { + Complex::default() + }; + let alpha0 = if phi11.re != 0.0 { + let numer = phi01 + alpha1 * phi12.conj(); + Complex::new(-numer.re / phi11.re, -numer.im / phi11.re) + } else { + Complex::default() + }; + + // If either magnitude reaches 4, both coefficients reset to zero. + if alpha0.norm_sqr() >= 16.0 || alpha1.norm_sqr() >= 16.0 { + return Ok((Complex::default(), Complex::default())); + } + Ok((alpha0, alpha1)) +} + +/// §4.6.18.8.3 reflection coefficient for the low-power SBR aliasing +/// detection: `ref(k) = min(max(−φk(0,1)/φk(1,1), −1), 1)` when +/// `φk(1,1) ≠ 0`, else `0`, with the covariance sums of §4.6.18.6.2 +/// (over the same `numTimeSlots·RATE + 6` window). The low-power tool +/// operates on real-valued subband signals, so the real parts carry +/// the whole covariance. +pub fn reflection_coefficient( + x_low: &[[Complex; 32]], + k: usize, + n_slots_frame: usize, +) -> Result { + if k >= 32 || x_low.len() < n_slots_frame + 6 + T_HF_ADJ { + return Err(Error::SbrFreqBandInvalid); + } + let mut phi01 = 0.0f64; + let mut phi11 = 0.0f64; + for n in 0..(n_slots_frame + 6) { + let a = x_low[n + T_HF_ADJ][k].re; + let b = x_low[n + T_HF_ADJ - 1][k].re; + phi01 += a * b; + phi11 += b * b; + } + Ok(if phi11 != 0.0 { + (-phi01 / phi11).clamp(-1.0, 1.0) + } else { + 0.0 + }) +} + +/// §4.6.18.6.3 — generate `XHigh` from `XLow` over the patch mapping. +/// +/// * `x_low` — slot-major analysis output (spec absolute columns). +/// * `patches` — the Figure 4.48 layout. +/// * `bw_array` — the per-noise-band chirp factors. +/// * `bands` — the derived frequency tables (`fTableNoise`, `k_x`). +/// * `l_range` — the spec's `RATE·tE(0) .. RATE·tE(LE)` column range +/// (exclusive end, *before* the `tHFAdj` offset). +/// * `n_slots_frame` — `numTimeSlots · RATE` (covariance length). +/// +/// Returns `XHigh` with the same slot-major layout and column count as +/// `x_low` (bands outside the patched range stay zero). +pub fn generate_hf( + x_low: &[[Complex; 32]], + patches: &Patches, + bw_array: &[f64], + bands: &HiLoTables, + l_range: core::ops::Range, + n_slots_frame: usize, +) -> Result> { + let k_x = bands.k_x; + let mut x_high = vec![[Complex::default(); 64]; x_low.len()]; + + // α cache per source subband (a subband may feed several patches). + let mut alphas: [Option<(Complex, Complex)>; 32] = [None; 32]; + + // g(k): the noise band containing QMF subband k. + let g_of = |k: i32| -> Result { + let nb = &bands.f_table_noise; + for i in 0..nb.len() - 1 { + if nb[i] <= k && k < nb[i + 1] { + return Ok(i); + } + } + Err(Error::SbrFreqBandInvalid) + }; + + let mut k_off = 0usize; + for (i, (&p_start, &p_num)) in patches.start.iter().zip(patches.num.iter()).enumerate() { + let _ = i; + for x in 0..p_num { + let k = k_x as usize + x + k_off; + let p = p_start + x; + if k >= 64 || p >= 32 { + return Err(Error::SbrFreqBandInvalid); + } + let (a0, a1) = match alphas[p] { + Some(a) => a, + None => { + let a = prediction_coefficients(x_low, p, n_slots_frame)?; + alphas[p] = Some(a); + a + } + }; + let bw = *bw_array + .get(g_of(k as i32)?) + .ok_or(Error::SbrFreqBandInvalid)?; + let bw2 = bw * bw; + for l in l_range.clone() { + let c = usize::try_from(l).map_err(|_| Error::SbrFreqBandInvalid)? + T_HF_ADJ; + if c >= x_low.len() || c < 2 { + return Err(Error::SbrFreqBandInvalid); + } + x_high[c][k] = + x_low[c][p] + (a0 * bw) * x_low[c - 1][p] + (a1 * bw2) * x_low[c - 2][p]; + } + } + k_off += p_num; + } + Ok(x_high) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Table 4.175 spot values. + #[test] + fn new_bw_table() { + assert_eq!(new_bw(0, 0), 0.0); + assert_eq!(new_bw(0, 1), 0.6); + assert_eq!(new_bw(1, 0), 0.6); + assert_eq!(new_bw(1, 1), 0.75); + assert_eq!(new_bw(2, 0), 0.0); + assert_eq!(new_bw(2, 1), 0.75); + assert_eq!(new_bw(3, 3), 0.98); + assert_eq!(new_bw(0, 2), 0.9); + } + + /// Chirp blend: rising values take the 0.90625/0.09375 mix, + /// falling values the 0.75/0.25 mix, and tiny results flush to 0. + #[test] + fn chirp_blend_and_flush() { + // First frame: prev all zero. newBw(0, 3) = 0.98 rising: + // 0.90625·0.98 = 0.888125. + let bw = chirp_factors(&[3], &[0], &[0.0]); + assert!((bw[0] - 0.888125).abs() < 1e-12); + // Falling: newBw(3, 0) = 0.0 < prev 0.888125: + // 0.25·0.888125 = 0.22203125. + let bw2 = chirp_factors(&[0], &[3], &bw); + assert!((bw2[0] - 0.22203125).abs() < 1e-12); + // Repeated Off decays geometrically to below 0.015625 → 0. + let mut cur = bw2; + for _ in 0..4 { + cur = chirp_factors(&[0], &[0], &cur); + } + assert_eq!(cur[0], 0.0); + } + + /// §4.6.18.8.3 reflection coefficient: a constant subband signal + /// has φ(0,1) = φ(1,1) → ref = −1; an alternating-sign signal has + /// φ(0,1) = −φ(1,1) → ref = +1; silence → 0; and the clamp holds. + #[test] + fn reflection_coefficient_orientations() { + let n = 32usize; + let cols = n + 6 + T_HF_ADJ; + let mut x = vec![[Complex::default(); 32]; cols]; + for (c, col) in x.iter_mut().enumerate() { + col[3] = Complex::new(1.0, 0.0); // constant + col[4] = Complex::new(if c % 2 == 0 { 1.0 } else { -1.0 }, 0.0); // alternating + } + assert_eq!(reflection_coefficient(&x, 3, n).unwrap(), -1.0); + assert_eq!(reflection_coefficient(&x, 4, n).unwrap(), 1.0); + assert_eq!(reflection_coefficient(&x, 5, n).unwrap(), 0.0); + assert!(reflection_coefficient(&x, 32, n).is_err()); + } + + /// Figure 4.48 on a hand-walked geometry: fMaster = 8..=24 step 2, + /// k0 = kx = 8, M = 16, goalSb past the range. + #[test] + fn patch_construction_hand_walked() { + let f_master: Vec = (0..=8).map(|i| 8 + 2 * i).collect(); + // fs_sbr small enough that goalSb = NINT(2.048e6/fs) ≥ 24. + let p = build_patches(&f_master, 8, 8, 16, 85_000).unwrap(); + // Iter 1: sb = 14 → patch (start 2, num 6); + // iter 2: sb = 20 → patch (2, 6); iter 3: sb = 24 → (4, 4). + assert_eq!(p.start, vec![2, 2, 4]); + assert_eq!(p.num, vec![6, 6, 4]); + assert_eq!(p.borders(8), vec![8, 14, 20, 24]); + } + + /// The patch trim drops a trailing patch narrower than 3 subbands. + #[test] + fn patch_trim_drops_small_tail() { + // fMaster reaching kx + M = 22 with a final 2-wide step. + let f_master = vec![8, 10, 12, 14, 16, 20, 22]; + let p = build_patches(&f_master, 8, 8, 14, 85_000).unwrap(); + // Walk: msb=8,usb=8 → sb=14 (odd 0) num 6 start 2; + // then sb=20? 20 ≤ 7+14-0=21 → num 6 start 2; then sb=22: + // 22 ≤ 7+20-0=27 → num 2 start 6 → trimmed. + assert_eq!(p.num, vec![6, 6]); + assert_eq!(p.start, vec![2, 2]); + } + + /// Patch invariants on a spec-derived master table (44.1 kHz + /// HE-AAC geometry). + #[test] + fn patch_invariants_on_derived_master() { + let fs_sbr = 44_100; + let k0 = crate::sbr_freq_bands::k0(fs_sbr, 5).unwrap(); + let k2 = crate::sbr_freq_bands::k2(fs_sbr, 5, k0).unwrap(); + let fm = crate::sbr_freq_bands::master_table(k0, k2, 2, true).unwrap(); + let bands = HiLoTables::derive(&fm, 0, 2).unwrap(); + let p = build_patches(&fm, k0, bands.k_x, bands.m, fs_sbr).unwrap(); + assert!(p.num_patches() >= 1 && p.num_patches() <= MAX_PATCHES); + for (&s, &n) in p.start.iter().zip(p.num.iter()) { + assert!(n > 0); + // Source range lies below the first master subband. + assert!((s + n) as i32 <= k0); + } + // Borders start at kx and stay within kx + M. + let borders = p.borders(bands.k_x); + assert_eq!(borders[0], bands.k_x); + assert!(*borders.last().unwrap() <= bands.k_x + bands.m); + } + + /// Build a slot-major XLow whose band `k` carries an exact + /// second-order recursion `x[n] = a1·x[n-1] + a2·x[n-2]`. + fn ar2_xlow(k: usize, a1: Complex, a2: Complex, cols: usize) -> Vec<[Complex; 32]> { + let mut x = vec![[Complex::default(); 32]; cols]; + x[0][k] = Complex::new(1.0, 0.3); + x[1][k] = Complex::new(0.2, -0.5); + for n in 2..cols { + let v = a1 * x[n - 1][k] + a2 * x[n - 2][k]; + x[n][k] = v; + } + x + } + + /// The covariance method recovers an exact AR(2) recursion: + /// α0 = −a1, α1 = −a2. + #[test] + fn prediction_recovers_ar2() { + let a1 = Complex::new(0.9, 0.1); + let a2 = Complex::new(-0.5, 0.05); + let x = ar2_xlow(3, a1, a2, 40); + let (al0, al1) = prediction_coefficients(&x, 3, 32).unwrap(); + // The εInv = 1e-6 relaxation perturbs the exact solution by + // O(εInv), so the recovery is pinned to that scale. + assert!((al0 + a1).norm_sqr() < 1e-10, "{al0:?}"); + assert!((al1 + a2).norm_sqr() < 1e-10, "{al1:?}"); + } + + /// |α| ≥ 4 resets both coefficients. + #[test] + fn prediction_resets_large_coefficients() { + // An unstable recursion with |a1| > 4 forces the reset. + let a1 = Complex::new(4.5, 0.0); + let a2 = Complex::new(0.0, 0.0); + let mut x = vec![[Complex::default(); 32]; 40]; + x[0][0] = Complex::new(1e-6, 0.0); + for n in 1..40 { + let v = a1 * x[n - 1][0]; + x[n][0] = v; + } + let _ = a2; + let (al0, al1) = prediction_coefficients(&x, 0, 32).unwrap(); + assert_eq!(al0, Complex::default()); + assert_eq!(al1, Complex::default()); + } + + fn tiny_bands() -> HiLoTables { + HiLoTables { + f_table_high: vec![8, 12, 16], + f_table_low: vec![8, 16], + f_table_noise: vec![8, 16], + m: 8, + k_x: 8, + } + } + + /// bw = 0 copies the source band; bw = 1 on a perfectly + /// predictable source whitens it to (near) zero. + #[test] + fn generate_copies_and_whitens() { + let a1 = Complex::new(0.8, 0.2); + let a2 = Complex::new(-0.4, 0.0); + let x = ar2_xlow(2, a1, a2, 40); + let patches = Patches { + start: vec![2], + num: vec![8], + }; + let bands = tiny_bands(); + // bw = 0: XHigh(k) == XLow(p) on the generated range. Patch + // maps source 2..10 → 8..16; k = 8 comes from p = 2. + let hi = generate_hf(&x, &patches, &[0.0], &bands, 0..32, 32).unwrap(); + for l in 0..32usize { + let c = l + T_HF_ADJ; + assert_eq!(hi[c][8], x[c][2]); + } + // bw = 1: the inverse filter cancels the AR(2) recursion (to + // the O(εInv) accuracy of the relaxed covariance solution). + let hi = generate_hf(&x, &patches, &[1.0], &bands, 0..32, 32).unwrap(); + let sig: f64 = (0..32).map(|l| x[l + T_HF_ADJ][2].norm_sqr()).sum(); + let res: f64 = (0..32).map(|l| hi[l + T_HF_ADJ][8].norm_sqr()).sum(); + assert!(res < 1e-10 * sig, "residual {res} vs signal {sig}"); + // Un-patched bands stay zero. + for col in &hi { + assert_eq!(col[20], Complex::default()); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_huffman.rs b/crates/vendor/oxideav-aac/src/sbr_huffman.rs new file mode 100644 index 00000000..a5c12497 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_huffman.rs @@ -0,0 +1,1011 @@ +//! SBR Huffman codebooks + `sbr_huff_dec()` — ISO/IEC 14496-3 +//! Annex 4.A.6.1 (Tables 4.A.78–4.A.88). +//! +//! Spectral Band Replication codes its envelope scalefactors and +//! noise-floor values as DPCM deltas entropy-coded with one of ten +//! canonical Huffman codebooks. The codebook is selected per the +//! §4.6.18.3 `sbr_envelope()` / `sbr_noise()` switch on the coupling +//! flag, the channel index, the amplitude resolution (`bs_amp_res`), +//! and the time/frequency direction (`bs_df_*`): +//! +//! | direction | amp_res | coupling | which | table | +//! |-----------|---------|----------|-------|-------| +//! | time | 0 (1.5 dB) | level | env | [`T_HUFFMAN_ENV_1_5DB`] | +//! | freq | 0 (1.5 dB) | level | env | [`F_HUFFMAN_ENV_1_5DB`] | +//! | time | 0 (1.5 dB) | balance | env | [`T_HUFFMAN_ENV_BAL_1_5DB`] | +//! | freq | 0 (1.5 dB) | balance | env | [`F_HUFFMAN_ENV_BAL_1_5DB`] | +//! | time | 1 (3.0 dB) | level | env | [`T_HUFFMAN_ENV_3_0DB`] | +//! | freq | 1 (3.0 dB) | level | env | [`F_HUFFMAN_ENV_3_0DB`] | +//! | time | 1 (3.0 dB) | balance | env | [`T_HUFFMAN_ENV_BAL_3_0DB`] | +//! | freq | 1 (3.0 dB) | balance | env | [`F_HUFFMAN_ENV_BAL_3_0DB`] | +//! | time | dc | level | noise | [`T_HUFFMAN_NOISE_3_0DB`] | +//! | time | dc | balance | noise | [`T_HUFFMAN_NOISE_BAL_3_0DB`] | +//! +//! Per Table 4.A.78 Note 2, the *frequency*-direction noise codebooks +//! `f_huffman_noise_3_0dB` / `f_huffman_noise_bal_3_0dB` are identical +//! to the 3.0 dB envelope freq codebooks `f_huffman_env_3_0dB` / +//! `f_huffman_env_bal_3_0dB`, so they are not duplicated here — the +//! [`noise_tables`] selector aliases them. +//! +//! ## Codeword representation +//! +//! Each table is `[(u8, u32); N]` indexed by the Huffman table index, +//! where the tuple is `(code_length_bits, codeword)`. Codewords are +//! MSB-first prefix codes (the most-significant of the `length` low +//! bits is read first). [`sbr_huff_dec`] reads one bit at a time, +//! accumulating MSB-first, and returns the first table index whose +//! `(length, codeword)` matches, with the table's largest-absolute- +//! value (LAV) subtracted so the result is the signed DPCM delta. +//! +//! ## Provenance +//! +//! All ten tables are transcribed directly from the normative +//! codeword grids in ISO/IEC 14496-3:2009 Annex 4.A (Tables 4.A.79 +//! through 4.A.88). Each table was validated for completeness (every +//! index 0..=2·LAV present), self-consistency (every codeword fits in +//! its declared bit length), and the prefix-free property (no codeword +//! is a prefix of another) at extraction time. + +use crate::{Error, Result}; + +/// `t_huffman_env_1_5dB` — ISO/IEC 14496-3 Table 4.A.79 (LAV = 60). +/// +/// 121 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 60`. +pub const T_HUFFMAN_ENV_1_5DB: [(u8, u32); 121] = [ + (18, 0x0003FFD6), + (18, 0x0003FFD7), + (18, 0x0003FFD8), + (18, 0x0003FFD9), + (18, 0x0003FFDA), + (18, 0x0003FFDB), + (19, 0x0007FFB8), + (19, 0x0007FFB9), + (19, 0x0007FFBA), + (19, 0x0007FFBB), + (19, 0x0007FFBC), + (19, 0x0007FFBD), + (19, 0x0007FFBE), + (19, 0x0007FFBF), + (19, 0x0007FFC0), + (19, 0x0007FFC1), + (19, 0x0007FFC2), + (19, 0x0007FFC3), + (19, 0x0007FFC4), + (19, 0x0007FFC5), + (19, 0x0007FFC6), + (19, 0x0007FFC7), + (19, 0x0007FFC8), + (19, 0x0007FFC9), + (19, 0x0007FFCA), + (19, 0x0007FFCB), + (19, 0x0007FFCC), + (19, 0x0007FFCD), + (19, 0x0007FFCE), + (19, 0x0007FFCF), + (19, 0x0007FFD0), + (19, 0x0007FFD1), + (19, 0x0007FFD2), + (19, 0x0007FFD3), + (17, 0x0001FFE6), + (18, 0x0003FFD4), + (16, 0x0000FFF0), + (17, 0x0001FFE9), + (18, 0x0003FFD5), + (17, 0x0001FFE7), + (16, 0x0000FFF1), + (16, 0x0000FFEC), + (16, 0x0000FFED), + (16, 0x0000FFEE), + (15, 0x00007FF4), + (14, 0x00003FF9), + (14, 0x00003FF7), + (13, 0x00001FFA), + (13, 0x00001FF9), + (12, 0x00000FFB), + (11, 0x000007FC), + (10, 0x000003FC), + (9, 0x000001FD), + (8, 0x000000FD), + (7, 0x0000007D), + (6, 0x0000003D), + (5, 0x0000001D), + (4, 0x0000000D), + (3, 0x00000005), + (2, 0x00000001), + (2, 0x00000000), + (3, 0x00000004), + (4, 0x0000000C), + (5, 0x0000001C), + (6, 0x0000003C), + (7, 0x0000007C), + (8, 0x000000FC), + (9, 0x000001FC), + (10, 0x000003FD), + (12, 0x00000FFA), + (13, 0x00001FF8), + (14, 0x00003FF6), + (14, 0x00003FF8), + (15, 0x00007FF5), + (16, 0x0000FFEF), + (17, 0x0001FFE8), + (16, 0x0000FFF2), + (19, 0x0007FFD4), + (19, 0x0007FFD5), + (19, 0x0007FFD6), + (19, 0x0007FFD7), + (19, 0x0007FFD8), + (19, 0x0007FFD9), + (19, 0x0007FFDA), + (19, 0x0007FFDB), + (19, 0x0007FFDC), + (19, 0x0007FFDD), + (19, 0x0007FFDE), + (19, 0x0007FFDF), + (19, 0x0007FFE0), + (19, 0x0007FFE1), + (19, 0x0007FFE2), + (19, 0x0007FFE3), + (19, 0x0007FFE4), + (19, 0x0007FFE5), + (19, 0x0007FFE6), + (19, 0x0007FFE7), + (19, 0x0007FFE8), + (19, 0x0007FFE9), + (19, 0x0007FFEA), + (19, 0x0007FFEB), + (19, 0x0007FFEC), + (19, 0x0007FFED), + (19, 0x0007FFEE), + (19, 0x0007FFEF), + (19, 0x0007FFF0), + (19, 0x0007FFF1), + (19, 0x0007FFF2), + (19, 0x0007FFF3), + (19, 0x0007FFF4), + (19, 0x0007FFF5), + (19, 0x0007FFF6), + (19, 0x0007FFF7), + (19, 0x0007FFF8), + (19, 0x0007FFF9), + (19, 0x0007FFFA), + (19, 0x0007FFFB), + (19, 0x0007FFFC), + (19, 0x0007FFFD), + (19, 0x0007FFFE), + (19, 0x0007FFFF), +]; + +/// `f_huffman_env_1_5dB` — ISO/IEC 14496-3 Table 4.A.80 (LAV = 60). +/// +/// 121 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 60`. +pub const F_HUFFMAN_ENV_1_5DB: [(u8, u32); 121] = [ + (19, 0x0007FFE7), + (19, 0x0007FFE8), + (20, 0x000FFFD2), + (20, 0x000FFFD3), + (20, 0x000FFFD4), + (20, 0x000FFFD5), + (20, 0x000FFFD6), + (20, 0x000FFFD7), + (20, 0x000FFFD8), + (19, 0x0007FFDA), + (20, 0x000FFFD9), + (20, 0x000FFFDA), + (20, 0x000FFFDB), + (20, 0x000FFFDC), + (19, 0x0007FFDB), + (20, 0x000FFFDD), + (19, 0x0007FFDC), + (19, 0x0007FFDD), + (20, 0x000FFFDE), + (18, 0x0003FFE4), + (20, 0x000FFFDF), + (20, 0x000FFFE0), + (20, 0x000FFFE1), + (19, 0x0007FFDE), + (20, 0x000FFFE2), + (20, 0x000FFFE3), + (20, 0x000FFFE4), + (19, 0x0007FFDF), + (20, 0x000FFFE5), + (19, 0x0007FFE0), + (18, 0x0003FFE8), + (19, 0x0007FFE1), + (18, 0x0003FFE0), + (18, 0x0003FFE9), + (17, 0x0001FFEF), + (18, 0x0003FFE5), + (17, 0x0001FFEC), + (17, 0x0001FFED), + (17, 0x0001FFEE), + (16, 0x0000FFF4), + (16, 0x0000FFF3), + (16, 0x0000FFF0), + (15, 0x00007FF7), + (15, 0x00007FF6), + (14, 0x00003FFA), + (13, 0x00001FFA), + (13, 0x00001FF9), + (12, 0x00000FFA), + (12, 0x00000FF8), + (11, 0x000007F9), + (10, 0x000003FB), + (9, 0x000001FC), + (9, 0x000001FA), + (8, 0x000000FB), + (7, 0x0000007C), + (6, 0x0000003C), + (5, 0x0000001C), + (4, 0x0000000C), + (3, 0x00000005), + (2, 0x00000001), + (2, 0x00000000), + (3, 0x00000004), + (4, 0x0000000D), + (5, 0x0000001D), + (6, 0x0000003D), + (8, 0x000000FA), + (8, 0x000000FC), + (9, 0x000001FB), + (10, 0x000003FA), + (11, 0x000007F8), + (11, 0x000007FA), + (11, 0x000007FB), + (12, 0x00000FF9), + (12, 0x00000FFB), + (13, 0x00001FF8), + (13, 0x00001FFB), + (14, 0x00003FF8), + (14, 0x00003FF9), + (16, 0x0000FFF1), + (16, 0x0000FFF2), + (17, 0x0001FFEA), + (17, 0x0001FFEB), + (18, 0x0003FFE1), + (18, 0x0003FFE2), + (18, 0x0003FFEA), + (18, 0x0003FFE3), + (18, 0x0003FFE6), + (18, 0x0003FFE7), + (18, 0x0003FFEB), + (20, 0x000FFFE6), + (19, 0x0007FFE2), + (20, 0x000FFFE7), + (20, 0x000FFFE8), + (20, 0x000FFFE9), + (20, 0x000FFFEA), + (20, 0x000FFFEB), + (20, 0x000FFFEC), + (19, 0x0007FFE3), + (20, 0x000FFFED), + (20, 0x000FFFEE), + (20, 0x000FFFEF), + (20, 0x000FFFF0), + (19, 0x0007FFE4), + (20, 0x000FFFF1), + (18, 0x0003FFEC), + (20, 0x000FFFF2), + (20, 0x000FFFF3), + (19, 0x0007FFE5), + (19, 0x0007FFE6), + (20, 0x000FFFF4), + (20, 0x000FFFF5), + (20, 0x000FFFF6), + (20, 0x000FFFF7), + (20, 0x000FFFF8), + (20, 0x000FFFF9), + (20, 0x000FFFFA), + (20, 0x000FFFFB), + (20, 0x000FFFFC), + (20, 0x000FFFFD), + (20, 0x000FFFFE), + (20, 0x000FFFFF), +]; + +/// `t_huffman_env_bal_1_5dB` — ISO/IEC 14496-3 Table 4.A.81 (LAV = 24). +/// +/// 49 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 24`. +pub const T_HUFFMAN_ENV_BAL_1_5DB: [(u8, u32); 49] = [ + (16, 0x0000FFE4), + (16, 0x0000FFE5), + (16, 0x0000FFE6), + (16, 0x0000FFE7), + (16, 0x0000FFE8), + (16, 0x0000FFE9), + (16, 0x0000FFEA), + (16, 0x0000FFEB), + (16, 0x0000FFEC), + (16, 0x0000FFED), + (16, 0x0000FFEE), + (16, 0x0000FFEF), + (16, 0x0000FFF0), + (16, 0x0000FFF1), + (16, 0x0000FFF2), + (16, 0x0000FFF3), + (16, 0x0000FFF4), + (16, 0x0000FFE2), + (12, 0x00000FFC), + (11, 0x000007FC), + (9, 0x000001FE), + (7, 0x0000007E), + (5, 0x0000001E), + (3, 0x00000006), + (1, 0x00000000), + (2, 0x00000002), + (4, 0x0000000E), + (6, 0x0000003E), + (8, 0x000000FE), + (11, 0x000007FD), + (12, 0x00000FFD), + (15, 0x00007FF0), + (16, 0x0000FFE3), + (16, 0x0000FFF5), + (16, 0x0000FFF6), + (16, 0x0000FFF7), + (16, 0x0000FFF8), + (16, 0x0000FFF9), + (16, 0x0000FFFA), + (17, 0x0001FFF6), + (17, 0x0001FFF7), + (17, 0x0001FFF8), + (17, 0x0001FFF9), + (17, 0x0001FFFA), + (17, 0x0001FFFB), + (17, 0x0001FFFC), + (17, 0x0001FFFD), + (17, 0x0001FFFE), + (17, 0x0001FFFF), +]; + +/// `f_huffman_env_bal_1_5dB` — ISO/IEC 14496-3 Table 4.A.82 (LAV = 24). +/// +/// 49 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 24`. +pub const F_HUFFMAN_ENV_BAL_1_5DB: [(u8, u32); 49] = [ + (18, 0x0003FFE2), + (18, 0x0003FFE3), + (18, 0x0003FFE4), + (18, 0x0003FFE5), + (18, 0x0003FFE6), + (18, 0x0003FFE7), + (18, 0x0003FFE8), + (18, 0x0003FFE9), + (18, 0x0003FFEA), + (18, 0x0003FFEB), + (18, 0x0003FFEC), + (18, 0x0003FFED), + (18, 0x0003FFEE), + (18, 0x0003FFEF), + (18, 0x0003FFF0), + (16, 0x0000FFF7), + (17, 0x0001FFF0), + (14, 0x00003FFC), + (11, 0x000007FE), + (11, 0x000007FC), + (8, 0x000000FE), + (7, 0x0000007E), + (4, 0x0000000E), + (2, 0x00000002), + (1, 0x00000000), + (3, 0x00000006), + (5, 0x0000001E), + (6, 0x0000003E), + (9, 0x000001FE), + (11, 0x000007FD), + (12, 0x00000FFE), + (15, 0x00007FFA), + (16, 0x0000FFF6), + (18, 0x0003FFF1), + (18, 0x0003FFF2), + (18, 0x0003FFF3), + (18, 0x0003FFF4), + (18, 0x0003FFF5), + (18, 0x0003FFF6), + (18, 0x0003FFF7), + (18, 0x0003FFF8), + (18, 0x0003FFF9), + (18, 0x0003FFFA), + (18, 0x0003FFFB), + (18, 0x0003FFFC), + (18, 0x0003FFFD), + (18, 0x0003FFFE), + (19, 0x0007FFFE), + (19, 0x0007FFFF), +]; + +/// `t_huffman_env_3_0dB` — ISO/IEC 14496-3 Table 4.A.83 (LAV = 31). +/// +/// 63 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 31`. +pub const T_HUFFMAN_ENV_3_0DB: [(u8, u32); 63] = [ + (18, 0x0003FFED), + (18, 0x0003FFEE), + (19, 0x0007FFDE), + (19, 0x0007FFDF), + (19, 0x0007FFE0), + (19, 0x0007FFE1), + (19, 0x0007FFE2), + (19, 0x0007FFE3), + (19, 0x0007FFE4), + (19, 0x0007FFE5), + (19, 0x0007FFE6), + (19, 0x0007FFE7), + (19, 0x0007FFE8), + (19, 0x0007FFE9), + (19, 0x0007FFEA), + (19, 0x0007FFEB), + (19, 0x0007FFEC), + (17, 0x0001FFF4), + (16, 0x0000FFF7), + (16, 0x0000FFF9), + (16, 0x0000FFF8), + (14, 0x00003FFB), + (14, 0x00003FFA), + (14, 0x00003FF8), + (13, 0x00001FFA), + (12, 0x00000FFC), + (11, 0x000007FC), + (8, 0x000000FE), + (6, 0x0000003E), + (4, 0x0000000E), + (2, 0x00000002), + (1, 0x00000000), + (3, 0x00000006), + (5, 0x0000001E), + (7, 0x0000007E), + (9, 0x000001FE), + (11, 0x000007FD), + (13, 0x00001FFB), + (14, 0x00003FF9), + (14, 0x00003FFC), + (15, 0x00007FFA), + (16, 0x0000FFF6), + (17, 0x0001FFF5), + (18, 0x0003FFEC), + (19, 0x0007FFED), + (19, 0x0007FFEE), + (19, 0x0007FFEF), + (19, 0x0007FFF0), + (19, 0x0007FFF1), + (19, 0x0007FFF2), + (19, 0x0007FFF3), + (19, 0x0007FFF4), + (19, 0x0007FFF5), + (19, 0x0007FFF6), + (19, 0x0007FFF7), + (19, 0x0007FFF8), + (19, 0x0007FFF9), + (19, 0x0007FFFA), + (19, 0x0007FFFB), + (19, 0x0007FFFC), + (19, 0x0007FFFD), + (19, 0x0007FFFE), + (19, 0x0007FFFF), +]; + +/// `f_huffman_env_3_0dB` — ISO/IEC 14496-3 Table 4.A.84 (LAV = 31). +/// +/// 63 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 31`. +pub const F_HUFFMAN_ENV_3_0DB: [(u8, u32); 63] = [ + (20, 0x000FFFF0), + (20, 0x000FFFF1), + (20, 0x000FFFF2), + (20, 0x000FFFF3), + (20, 0x000FFFF4), + (20, 0x000FFFF5), + (20, 0x000FFFF6), + (18, 0x0003FFF3), + (19, 0x0007FFF5), + (19, 0x0007FFEE), + (19, 0x0007FFEF), + (19, 0x0007FFF6), + (18, 0x0003FFF4), + (18, 0x0003FFF2), + (20, 0x000FFFF7), + (19, 0x0007FFF0), + (17, 0x0001FFF5), + (18, 0x0003FFF0), + (17, 0x0001FFF4), + (16, 0x0000FFF7), + (16, 0x0000FFF6), + (15, 0x00007FF8), + (14, 0x00003FFB), + (12, 0x00000FFD), + (11, 0x000007FD), + (10, 0x000003FD), + (9, 0x000001FD), + (8, 0x000000FD), + (6, 0x0000003E), + (4, 0x0000000E), + (2, 0x00000002), + (1, 0x00000000), + (3, 0x00000006), + (5, 0x0000001E), + (8, 0x000000FC), + (9, 0x000001FC), + (10, 0x000003FC), + (11, 0x000007FC), + (12, 0x00000FFC), + (13, 0x00001FFC), + (14, 0x00003FFA), + (15, 0x00007FF9), + (15, 0x00007FFA), + (16, 0x0000FFF8), + (16, 0x0000FFF9), + (17, 0x0001FFF6), + (17, 0x0001FFF7), + (18, 0x0003FFF5), + (18, 0x0003FFF6), + (18, 0x0003FFF1), + (20, 0x000FFFF8), + (19, 0x0007FFF1), + (19, 0x0007FFF2), + (19, 0x0007FFF3), + (20, 0x000FFFF9), + (19, 0x0007FFF7), + (19, 0x0007FFF4), + (20, 0x000FFFFA), + (20, 0x000FFFFB), + (20, 0x000FFFFC), + (20, 0x000FFFFD), + (20, 0x000FFFFE), + (20, 0x000FFFFF), +]; + +/// `t_huffman_env_bal_3_0dB` — ISO/IEC 14496-3 Table 4.A.85 (LAV = 12). +/// +/// 25 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 12`. +pub const T_HUFFMAN_ENV_BAL_3_0DB: [(u8, u32); 25] = [ + (13, 0x00001FF2), + (13, 0x00001FF3), + (13, 0x00001FF4), + (13, 0x00001FF5), + (13, 0x00001FF6), + (13, 0x00001FF7), + (13, 0x00001FF8), + (12, 0x00000FF8), + (8, 0x000000FE), + (7, 0x0000007E), + (4, 0x0000000E), + (3, 0x00000006), + (1, 0x00000000), + (2, 0x00000002), + (5, 0x0000001E), + (6, 0x0000003E), + (9, 0x000001FE), + (13, 0x00001FF9), + (13, 0x00001FFA), + (13, 0x00001FFB), + (13, 0x00001FFC), + (13, 0x00001FFD), + (13, 0x00001FFE), + (14, 0x00003FFE), + (14, 0x00003FFF), +]; + +/// `f_huffman_env_bal_3_0dB` — ISO/IEC 14496-3 Table 4.A.86 (LAV = 12). +/// +/// 25 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 12`. +pub const F_HUFFMAN_ENV_BAL_3_0DB: [(u8, u32); 25] = [ + (13, 0x00001FF7), + (13, 0x00001FF8), + (13, 0x00001FF9), + (13, 0x00001FFA), + (13, 0x00001FFB), + (14, 0x00003FF8), + (14, 0x00003FF9), + (11, 0x000007FC), + (8, 0x000000FE), + (7, 0x0000007E), + (4, 0x0000000E), + (2, 0x00000002), + (1, 0x00000000), + (3, 0x00000006), + (5, 0x0000001E), + (6, 0x0000003E), + (9, 0x000001FE), + (12, 0x00000FFA), + (13, 0x00001FF6), + (14, 0x00003FFA), + (14, 0x00003FFB), + (14, 0x00003FFC), + (14, 0x00003FFD), + (14, 0x00003FFE), + (14, 0x00003FFF), +]; + +/// `t_huffman_noise_3_0dB` — ISO/IEC 14496-3 Table 4.A.87 (LAV = 31). +/// +/// 63 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 31`. +pub const T_HUFFMAN_NOISE_3_0DB: [(u8, u32); 63] = [ + (13, 0x00001FCE), + (13, 0x00001FCF), + (13, 0x00001FD0), + (13, 0x00001FD1), + (13, 0x00001FD2), + (13, 0x00001FD3), + (13, 0x00001FD4), + (13, 0x00001FD5), + (13, 0x00001FD6), + (13, 0x00001FD7), + (13, 0x00001FD8), + (13, 0x00001FD9), + (13, 0x00001FDA), + (13, 0x00001FDB), + (13, 0x00001FDC), + (13, 0x00001FDD), + (13, 0x00001FDE), + (13, 0x00001FDF), + (13, 0x00001FE0), + (13, 0x00001FE1), + (13, 0x00001FE2), + (13, 0x00001FE3), + (13, 0x00001FE4), + (13, 0x00001FE5), + (13, 0x00001FE6), + (13, 0x00001FE7), + (11, 0x000007F2), + (8, 0x000000FD), + (6, 0x0000003E), + (4, 0x0000000E), + (3, 0x00000006), + (1, 0x00000000), + (2, 0x00000002), + (5, 0x0000001E), + (8, 0x000000FC), + (10, 0x000003F8), + (13, 0x00001FCC), + (13, 0x00001FE8), + (13, 0x00001FE9), + (13, 0x00001FEA), + (13, 0x00001FEB), + (13, 0x00001FEC), + (13, 0x00001FCD), + (13, 0x00001FED), + (13, 0x00001FEE), + (13, 0x00001FEF), + (13, 0x00001FF0), + (13, 0x00001FF1), + (13, 0x00001FF2), + (13, 0x00001FF3), + (13, 0x00001FF4), + (13, 0x00001FF5), + (13, 0x00001FF6), + (13, 0x00001FF7), + (13, 0x00001FF8), + (13, 0x00001FF9), + (13, 0x00001FFA), + (13, 0x00001FFB), + (13, 0x00001FFC), + (13, 0x00001FFD), + (13, 0x00001FFE), + (14, 0x00003FFE), + (14, 0x00003FFF), +]; + +/// `t_huffman_noise_bal_3_0dB` — ISO/IEC 14496-3 Table 4.A.88 (LAV = 12). +/// +/// 25 entries `(code_length_bits, codeword)` indexed by the Huffman +/// table index; the decoded value is `index - 12`. +pub const T_HUFFMAN_NOISE_BAL_3_0DB: [(u8, u32); 25] = [ + (8, 0x000000EC), + (8, 0x000000ED), + (8, 0x000000EE), + (8, 0x000000EF), + (8, 0x000000F0), + (8, 0x000000F1), + (8, 0x000000F2), + (8, 0x000000F3), + (8, 0x000000F4), + (8, 0x000000F5), + (5, 0x0000001C), + (2, 0x00000002), + (1, 0x00000000), + (3, 0x00000006), + (6, 0x0000003A), + (8, 0x000000F6), + (8, 0x000000F7), + (8, 0x000000F8), + (8, 0x000000F9), + (8, 0x000000FA), + (8, 0x000000FB), + (8, 0x000000FC), + (8, 0x000000FD), + (8, 0x000000FE), + (8, 0x000000FF), +]; + +/// Resolution / coupling context that picks an envelope or noise +/// codebook pair, per the §4.6.18.3 `sbr_envelope()` / `sbr_noise()` +/// table-selection pseudo-code. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SbrHuffContext { + /// `bs_coupling` — the channel pair is coupled (balance coding). + pub coupling: bool, + /// Channel index within the element (`0` or `1`); only relevant + /// when `coupling` is set (the second coupled channel carries the + /// balance values). + pub ch: bool, + /// `bs_amp_res` — `false` = 1.5 dB resolution, `true` = 3.0 dB. + pub amp_res: bool, +} + +/// One SBR Huffman codebook ready for [`sbr_huff_dec`]: the table +/// slice and its largest-absolute-value (`lav`) offset. +pub type SbrHuffCodebook = (&'static [(u8, u32)], i32); + +/// Returns the `(t_huff, f_huff)` envelope codebook pair for a given +/// `sbr_envelope()` context, per the §4.6.18.3 selection pseudo-code +/// (Table 4.72 surrounding text). `t_huff` is the time-direction +/// table, `f_huff` the frequency-direction table; each is returned as +/// `(slice, lav)`. +pub fn env_tables(ctx: SbrHuffContext) -> (SbrHuffCodebook, SbrHuffCodebook) { + // The balance tables are only ever selected for the *second* + // channel of a coupled pair; otherwise the level tables apply. + if ctx.coupling && ctx.ch { + if ctx.amp_res { + ( + (&T_HUFFMAN_ENV_BAL_3_0DB, 12), + (&F_HUFFMAN_ENV_BAL_3_0DB, 12), + ) + } else { + ( + (&T_HUFFMAN_ENV_BAL_1_5DB, 24), + (&F_HUFFMAN_ENV_BAL_1_5DB, 24), + ) + } + } else if ctx.amp_res { + ((&T_HUFFMAN_ENV_3_0DB, 31), (&F_HUFFMAN_ENV_3_0DB, 31)) + } else { + ((&T_HUFFMAN_ENV_1_5DB, 60), (&F_HUFFMAN_ENV_1_5DB, 60)) + } +} + +/// Returns the `(t_huff, f_huff)` noise codebook pair for a given +/// `sbr_noise()` context, per the §4.6.18.3 selection pseudo-code +/// (Table 4.73 surrounding text). Noise floors are always coded at the +/// 3.0 dB resolution (`bs_amp_res` is "don't care" for noise). Per +/// Table 4.A.78 Note 2 the frequency-direction noise codebooks reuse +/// the 3.0 dB *envelope* frequency codebooks. +pub fn noise_tables(ctx: SbrHuffContext) -> (SbrHuffCodebook, SbrHuffCodebook) { + if ctx.coupling && ctx.ch { + ( + (&T_HUFFMAN_NOISE_BAL_3_0DB, 12), + // f_huffman_noise_bal_3_0dB == f_huffman_env_bal_3_0dB. + (&F_HUFFMAN_ENV_BAL_3_0DB, 12), + ) + } else { + ( + (&T_HUFFMAN_NOISE_3_0DB, 31), + // f_huffman_noise_3_0dB == f_huffman_env_3_0dB. + (&F_HUFFMAN_ENV_3_0DB, 31), + ) + } +} + +/// The longest codeword across every SBR Huffman table is 20 bits +/// (`f_huffman_env_1_5dB` / `f_huffman_env_3_0dB`). `sbr_huff_dec` +/// refuses to read past this many bits without a match (a malformed +/// bitstream would otherwise loop until the reader runs dry). +pub const SBR_HUFF_MAX_CODE_LEN: u32 = 20; + +/// `sbr_huff_dec()` — ISO/IEC 14496-3 Annex 4.A.6.1. +/// +/// Reads bits MSB-first from `reader`, accumulating a codeword, until +/// it matches an entry `(length, codeword)` of `table`. Returns the +/// matching table index minus `lav`, i.e. the signed DPCM delta the +/// envelope / noise reconstruction adds to the running value. +/// +/// Returns [`Error::SbrHuffInvalid`] if no codeword of length up to +/// [`SBR_HUFF_MAX_CODE_LEN`] matches (a corrupt or truncated payload). +pub fn sbr_huff_dec( + reader: &mut oxideav_core::bits::BitReader<'_>, + table: &[(u8, u32)], + lav: i32, +) -> Result { + let mut codeword: u32 = 0; + let mut len: u32 = 0; + loop { + codeword = (codeword << 1) | reader.read_u32(1).map_err(|_| Error::SbrHuffInvalid)?; + len += 1; + for (idx, &(clen, ccode)) in table.iter().enumerate() { + if u32::from(clen) == len && ccode == codeword { + return Ok(idx as i32 - lav); + } + } + if len >= SBR_HUFF_MAX_CODE_LEN { + return Err(Error::SbrHuffInvalid); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitReader; + + /// Every table is complete, codewords fit their declared length, + /// and the table is prefix-free — the canonical-Huffman invariants + /// the spec grids must satisfy. + fn check_table(table: &[(u8, u32)]) { + for &(len, code) in table { + assert!(len >= 1 && len <= SBR_HUFF_MAX_CODE_LEN as u8); + // codeword fits in its declared bit length. + assert!( + code < (1u32 << len), + "codeword 0x{code:08X} overflows its {len}-bit length" + ); + } + // Prefix-free: no codeword is a prefix of another. With + // `lb >= la` (the shorter or equal code is `ca`), truncating + // the longer code `cb` to `la` bits must not equal `ca` — + // covering both the equal-length collision and the strict + // prefix case in one comparison. + for (a, &(la, ca)) in table.iter().enumerate() { + for (b, &(lb, cb)) in table.iter().enumerate() { + if a == b || lb < la { + continue; + } + let shifted = cb >> (lb - la); + assert!(shifted != ca, "prefix conflict between index {a} and {b}"); + } + } + } + + #[test] + fn all_tables_valid() { + check_table(&T_HUFFMAN_ENV_1_5DB); + check_table(&F_HUFFMAN_ENV_1_5DB); + check_table(&T_HUFFMAN_ENV_BAL_1_5DB); + check_table(&F_HUFFMAN_ENV_BAL_1_5DB); + check_table(&T_HUFFMAN_ENV_3_0DB); + check_table(&F_HUFFMAN_ENV_3_0DB); + check_table(&T_HUFFMAN_ENV_BAL_3_0DB); + check_table(&F_HUFFMAN_ENV_BAL_3_0DB); + check_table(&T_HUFFMAN_NOISE_3_0DB); + check_table(&T_HUFFMAN_NOISE_BAL_3_0DB); + } + + #[test] + fn table_sizes_match_lav() { + assert_eq!(T_HUFFMAN_ENV_1_5DB.len(), 121); + assert_eq!(F_HUFFMAN_ENV_1_5DB.len(), 121); + assert_eq!(T_HUFFMAN_ENV_BAL_1_5DB.len(), 49); + assert_eq!(F_HUFFMAN_ENV_BAL_1_5DB.len(), 49); + assert_eq!(T_HUFFMAN_ENV_3_0DB.len(), 63); + assert_eq!(F_HUFFMAN_ENV_3_0DB.len(), 63); + assert_eq!(T_HUFFMAN_ENV_BAL_3_0DB.len(), 25); + assert_eq!(F_HUFFMAN_ENV_BAL_3_0DB.len(), 25); + assert_eq!(T_HUFFMAN_NOISE_3_0DB.len(), 63); + assert_eq!(T_HUFFMAN_NOISE_BAL_3_0DB.len(), 25); + } + + /// Encode each codeword MSB-first into a byte buffer and confirm + /// `sbr_huff_dec` decodes back to `index - lav`. + fn roundtrip(table: &[(u8, u32)], lav: i32) { + for (idx, &(len, code)) in table.iter().enumerate() { + // Pack the codeword MSB-first, then pad to a byte so the + // reader has whole bytes to consume. + let mut bits: Vec = Vec::new(); + for b in (0..len).rev() { + bits.push(((code >> b) & 1) as u8); + } + let mut bytes = vec![0u8; len.div_ceil(8) as usize]; + for (i, &bit) in bits.iter().enumerate() { + if bit != 0 { + bytes[i / 8] |= 1 << (7 - (i % 8)); + } + } + let mut reader = BitReader::new(&bytes); + let got = sbr_huff_dec(&mut reader, table, lav).unwrap(); + assert_eq!(got, idx as i32 - lav, "table index {idx}"); + } + } + + #[test] + fn roundtrip_all() { + roundtrip(&T_HUFFMAN_ENV_1_5DB, 60); + roundtrip(&F_HUFFMAN_ENV_1_5DB, 60); + roundtrip(&T_HUFFMAN_ENV_BAL_1_5DB, 24); + roundtrip(&F_HUFFMAN_ENV_BAL_1_5DB, 24); + roundtrip(&T_HUFFMAN_ENV_3_0DB, 31); + roundtrip(&F_HUFFMAN_ENV_3_0DB, 31); + roundtrip(&T_HUFFMAN_ENV_BAL_3_0DB, 12); + roundtrip(&F_HUFFMAN_ENV_BAL_3_0DB, 12); + roundtrip(&T_HUFFMAN_NOISE_3_0DB, 31); + roundtrip(&T_HUFFMAN_NOISE_BAL_3_0DB, 12); + } + + #[test] + fn context_selectors() { + // Mono / level path picks the level tables. + let ((tt, tl), (ft, fl)) = env_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + assert_eq!(tl, 60); + assert_eq!(fl, 60); + assert_eq!(tt.len(), 121); + assert_eq!(ft.len(), 121); + + // Coupled second channel at 3.0 dB picks the balance tables. + let ((tt, tl), (_ft, _fl)) = env_tables(SbrHuffContext { + coupling: true, + ch: true, + amp_res: true, + }); + assert_eq!(tl, 12); + assert_eq!(tt.len(), 25); + + // Noise freq-direction reuses the 3.0 dB envelope freq table + // (Table 4.A.78 Note 2): same contents, same LAV. + let ((_nt, _nl), (nf, nfl)) = noise_tables(SbrHuffContext { + coupling: false, + ch: false, + amp_res: false, + }); + assert_eq!(nf, &F_HUFFMAN_ENV_3_0DB[..]); + assert_eq!(nfl, 31); + // Coupled noise balance freq-direction reuses the 3.0 dB + // envelope balance freq table. + let ((_nt, _nl), (nf, nfl)) = noise_tables(SbrHuffContext { + coupling: true, + ch: true, + amp_res: false, + }); + assert_eq!(nf, &F_HUFFMAN_ENV_BAL_3_0DB[..]); + assert_eq!(nfl, 12); + } + + #[test] + fn truncated_payload_errors() { + // An empty buffer can never complete a codeword — the first + // bit read fails and maps to SbrHuffInvalid rather than the raw + // bitreader error. + let bytes: [u8; 0] = []; + let mut reader = BitReader::new(&bytes); + assert!(matches!( + sbr_huff_dec(&mut reader, &T_HUFFMAN_ENV_1_5DB, 60), + Err(Error::SbrHuffInvalid) + )); + } + + /// The noise balance table's longest codeword followed by the + /// shortest exercises the bit-at-a-time accumulation past a byte + /// boundary. + #[test] + fn decode_across_byte_boundary() { + // f_huffman_env_1_5dB index 0 is an 18-bit codeword; decode it + // then immediately decode index 60's 2-bit codeword from the + // same stream. + let (l0, c0) = F_HUFFMAN_ENV_1_5DB[0]; + let (l1, c1) = F_HUFFMAN_ENV_1_5DB[60]; + let total = l0 as u32 + l1 as u32; + let combined = (u64::from(c0) << l1) | u64::from(c1); + let nbytes = total.div_ceil(8) as usize; + let mut bytes = vec![0u8; nbytes]; + for b in 0..total { + let bit = (combined >> (total - 1 - b)) & 1; + if bit != 0 { + bytes[(b / 8) as usize] |= 1 << (7 - (b % 8)); + } + } + let mut reader = BitReader::new(&bytes); + assert_eq!( + sbr_huff_dec(&mut reader, &F_HUFFMAN_ENV_1_5DB, 60).unwrap(), + -60 + ); + assert_eq!( + sbr_huff_dec(&mut reader, &F_HUFFMAN_ENV_1_5DB, 60).unwrap(), + 0 + ); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_limiter.rs b/crates/vendor/oxideav-aac/src/sbr_limiter.rs new file mode 100644 index 00000000..f9ea0dfe --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_limiter.rs @@ -0,0 +1,170 @@ +//! SBR limiter frequency band table — ISO/IEC 14496-3 §4.6.18.3.2.3 / +//! Figure 4.41. +//! +//! `fTableLim` partitions the SBR range into the bands over which the +//! §4.6.18.7.5 gain limiter averages: either exactly one band +//! (`bs_limiter_bands == 0`) or approximately 1.2 / 2 / 3 bands per +//! octave. The table is a subset of the union of `fTableLow` and the +//! §4.6.18.6 patch borders; the Figure 4.41 walk merges neighbours +//! closer than `0.49 / limBands` octaves, always preferring to keep a +//! patch border over an envelope border (both being patch borders +//! keeps both). +//! +//! ## Provenance +//! +//! The construction is the Figure 4.41 flowchart of the staged spec, +//! with the `limiterBandsPerOctave = {1.2, 2, 3}` selector. No part of +//! this implementation is derived from any external decoder. + +use crate::sbr_freq_bands::HiLoTables; +use crate::{Error, Result}; + +/// §4.6.18.3.2.3 / Figure 4.41 — build `fTableLim`. +/// +/// * `bands` — the derived frequency tables (`fTableLow`, `k_x`, `m`). +/// * `patch_borders` — the §4.6.18.6 patch borders +/// ([`crate::sbr_hf_gen::Patches::borders`], starting at `k_x`). +/// * `bs_limiter_bands` — the 2-bit header field (`0..=3`). +/// +/// Returns the border vector `fTableLim(0..=NL)`. +pub fn limiter_table( + bands: &HiLoTables, + patch_borders: &[i32], + bs_limiter_bands: u8, +) -> Result> { + let f_low = &bands.f_table_low; + if f_low.len() < 2 || bs_limiter_bands > 3 { + return Err(Error::SbrFreqBandInvalid); + } + + // bs_limiter_bands == 0: one band over the whole SBR range. + if bs_limiter_bands == 0 { + return Ok(vec![f_low[0], f_low[f_low.len() - 1]]); + } + + // limiterBandsPerOctave = {1.2, 2, 3}. + let lim_bands = [1.2f64, 2.0, 3.0][usize::from(bs_limiter_bands - 1)]; + + // limTable = fTableLow ∪ interior patch borders, sorted. + let num_patches = patch_borders.len().saturating_sub(1); + let mut lim_table: Vec = f_low.clone(); + if num_patches > 1 { + lim_table.extend_from_slice(&patch_borders[1..num_patches]); + } + lim_table.sort_unstable(); + + // nrLim = NLow + numPatches - 1 (the last index of limTable). + let mut k = 1usize; + while k < lim_table.len() { + if lim_table[k] < 1 || lim_table[k - 1] < 1 { + return Err(Error::SbrFreqBandInvalid); + } + let n_octaves = (f64::from(lim_table[k]) / f64::from(lim_table[k - 1])).log2(); + if n_octaves * lim_bands < 0.49 { + if lim_table[k] == lim_table[k - 1] { + // Duplicate border: drop one copy. + lim_table.remove(k); + } else if !patch_borders.contains(&lim_table[k]) { + // The upper border is droppable (an envelope border). + lim_table.remove(k); + } else if !patch_borders.contains(&lim_table[k - 1]) { + // The upper border is a patch border; drop the lower + // envelope border instead. + lim_table.remove(k - 1); + } else { + // Both are patch borders: keep both. + k += 1; + } + } else { + k += 1; + } + } + + Ok(lim_table) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bands(f_low: Vec) -> HiLoTables { + let k_x = f_low[0]; + let m = f_low[f_low.len() - 1] - k_x; + HiLoTables { + f_table_high: f_low.clone(), + f_table_low: f_low, + f_table_noise: vec![k_x, k_x + m], + m, + k_x, + } + } + + /// bs_limiter_bands == 0 → exactly one band over the SBR range. + #[test] + fn zero_limiter_bands_is_one_band() { + let b = bands(vec![8, 12, 16, 20, 24]); + let t = limiter_table(&b, &[8, 16, 24], 0).unwrap(); + assert_eq!(t, vec![8, 24]); + } + + /// A single patch adds no interior borders: wide envelope bands + /// pass through untouched. + #[test] + fn single_patch_keeps_envelope_borders() { + let b = bands(vec![8, 12, 16, 20, 24]); + let t = limiter_table(&b, &[8, 24], 3).unwrap(); + assert_eq!(t, vec![8, 12, 16, 20, 24]); + } + + /// A patch border duplicating an envelope border collapses to one + /// entry. + #[test] + fn duplicate_border_removed() { + let b = bands(vec![8, 12, 16, 20, 24]); + // Interior patch border at 16 duplicates fLow's 16. + let t = limiter_table(&b, &[8, 16, 24], 3).unwrap(); + assert_eq!(t, vec![8, 12, 16, 20, 24]); + } + + /// A close pair drops the envelope border and keeps the patch + /// border. + #[test] + fn close_pair_keeps_patch_border() { + // fLow has 15 next to the interior patch border 16: + // log2(16/15)·3 ≈ 0.28 < 0.49 → merge, dropping 15. + let b = bands(vec![8, 12, 15, 20, 24]); + let t = limiter_table(&b, &[8, 16, 24], 3).unwrap(); + assert!(t.contains(&16) && !t.contains(&15), "{t:?}"); + // Borders stay sorted, spanning the SBR range. + assert_eq!(t.first(), Some(&8)); + assert_eq!(t.last(), Some(&24)); + assert!(t.windows(2).all(|w| w[0] < w[1])); + } + + /// A close envelope pair (no patch border involved) drops the + /// upper border. + #[test] + fn close_envelope_pair_drops_upper() { + // 20 and 21 are ~0.07 octaves apart → merged; neither is a + // patch border so the upper (21) goes. + let b = bands(vec![8, 14, 20, 21, 28]); + let t = limiter_table(&b, &[8, 28], 2).unwrap(); + assert_eq!(t, vec![8, 14, 20, 28]); + } + + /// The coarsest per-octave setting (1.2) merges more bands than + /// the finest (3). + #[test] + fn coarser_setting_merges_more() { + let b = bands(vec![8, 9, 10, 12, 14, 17, 20, 24]); + let pb = [8, 24]; + let t1 = limiter_table(&b, &pb, 1).unwrap(); + let t3 = limiter_table(&b, &pb, 3).unwrap(); + assert!(t1.len() <= t3.len(), "{t1:?} vs {t3:?}"); + for t in [&t1, &t3] { + assert_eq!(t.first(), Some(&8)); + assert_eq!(t.last(), Some(&24)); + assert!(t.windows(2).all(|w| w[0] < w[1])); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_lp.rs b/crates/vendor/oxideav-aac/src/sbr_lp.rs new file mode 100644 index 00000000..1dcb8455 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_lp.rs @@ -0,0 +1,331 @@ +//! Low-power SBR aliasing detection and reduction — ISO/IEC 14496-3 +//! §4.6.18.8.3 / §4.6.18.8.5. +//! +//! The low-power SBR tool processes real-valued subband signals, so +//! the per-subband gains applied by the §4.6.18.7 envelope adjuster +//! can introduce audible aliasing between adjacent QMF subbands. This +//! module implements the countermeasure: +//! +//! * **Aliasing degree** (§4.6.18.8.3 / Figure 4.53) — from the +//! per-subband reflection coefficients +//! [`crate::sbr_hf_gen::reflection_coefficient`], the `deg` vector +//! marking low-band subband pairs whose spectral orientation makes +//! gain steps alias. +//! * **Patched degree** — `degPatched`, the low-band degrees carried +//! onto the SBR range through the §4.6.18.6 patch mapping (zero at +//! every patch start and beyond the patch coverage). +//! * **Gain grouping** (Figure 4.54) — the per-envelope `FGroup` +//! start/stop index pairs bracketing runs of aliasing-prone, +//! sinusoid-free subbands. +//! * **Aliasing reduction** (§4.6.18.8.5) — the `GLimBoost → GA` gain +//! re-calculation: per group, a target gain from the group energies, +//! the `α(m)`-weighted blend, and the exact energy-restoring +//! normalization. +//! +//! ## Provenance +//! +//! Every formula and branch is from the §4.6.18.8.3 / §4.6.18.8.5 text +//! and the Figure 4.53 / 4.54 flowcharts of the staged spec. No part +//! of this implementation is derived from any external decoder. + +use crate::sbr_env_adjust::EPS0; +use crate::sbr_hf_gen::Patches; +use crate::{Error, Result}; + +/// §4.6.18.8.3 / Figure 4.53 — the aliasing degree `deg(k)` of every +/// low-band subband, from the reflection coefficients `ref(k)` +/// (`0 ≤ k < k0`). Entries 0 and 1 are always zero (the flowchart +/// starts at `k = 2` after forcing `ref(0) = 0`, `deg(1) = 0`). +#[must_use] +pub fn aliasing_degree(refl: &[f64]) -> Vec { + let k0 = refl.len(); + let mut deg = vec![0.0f64; k0]; + let mut refl = refl.to_vec(); + if !refl.is_empty() { + refl[0] = 0.0; + } + let mut k = 2usize; + while k < k0 { + deg[k] = 0.0; + // Even subbands alias on a negative reflection, odd subbands + // on a positive one; other orientations are alias-free. + let sign = if k % 2 == 0 && refl[k] < 0.0 { + 1.0 + } else if k % 2 == 1 && refl[k] > 0.0 { + -1.0 + } else { + k += 1; + continue; + }; + if sign * refl[k - 1] < 0.0 { + deg[k] = 1.0; + if sign * refl[k - 2] > 0.0 { + deg[k - 1] = 1.0 - refl[k - 1] * refl[k - 1]; + } + } else if sign * refl[k - 2] > 0.0 { + deg[k] = 1.0 - refl[k - 1] * refl[k - 1]; + } + k += 1; + } + deg +} + +/// §4.6.18.8.3 — `degPatched(k)` over the SBR range, `kx`-relative +/// (`m` entries): each patch carries the source subband's degree, the +/// first subband of every patch (`x == 0`) and the region beyond the +/// patch coverage are zero. +pub fn deg_patched(deg: &[f64], patches: &Patches, k_x: i32, m: i32) -> Result> { + let m_cnt = usize::try_from(m).map_err(|_| Error::SbrFreqBandInvalid)?; + if k_x < 0 { + return Err(Error::SbrFreqBandInvalid); + } + let mut dp = vec![0.0f64; m_cnt]; + let mut k_off = 0usize; + for (&start, &num) in patches.start.iter().zip(patches.num.iter()) { + for x in 0..num { + let rel = k_off + x; + if rel >= m_cnt { + break; + } + let p = start + x; + dp[rel] = if x == 0 { + 0.0 + } else { + deg.get(p).copied().ok_or(Error::SbrFreqBandInvalid)? + }; + } + k_off += num; + } + Ok(dp) +} + +/// Figure 4.54 — the gain groups of one SBR envelope: `(start, stop)` +/// absolute-QMF-subband pairs (`stop` exclusive), bracketing runs +/// where the *next* subband boundary is aliasing-prone +/// (`degPatched(k+1) ≠ 0`) and no sinusoid is mapped. +/// +/// `dp` is the `kx`-relative `degPatched` (length `M`), `s_mapped` the +/// envelope's `SMapped` row (length `M`). +#[must_use] +pub fn gain_groups(dp: &[f64], s_mapped: &[bool], k_x: i32) -> Vec<(usize, usize)> { + let m_cnt = dp.len().min(s_mapped.len()); + let kx = k_x.max(0) as usize; + let mut groups: Vec<(usize, usize)> = Vec::new(); + let mut open: Option = None; + // k walks kx .. kx + M − 1 (exclusive), exactly the flowchart loop. + for rel in 0..m_cnt.saturating_sub(1) { + let k = kx + rel; + if dp[rel + 1] != 0.0 && !s_mapped[rel] { + if open.is_none() { + open = Some(k); + } + } else if let Some(start) = open.take() { + // Close the group: past the current subband when it is + // sinusoid-free, before it otherwise. + let stop = if s_mapped[rel] { k } else { k + 1 }; + groups.push((start, stop)); + } + } + if let Some(start) = open { + groups.push((start, kx + m_cnt)); + } + groups +} + +/// §4.6.18.8.5 — recompute the limiter/boost gains `GLimBoost` of one +/// envelope into the aliasing-reduced `GA`, in place. +/// +/// `g` is the envelope's `GLimBoost` row and `e_curr` its `ECurr` row +/// (both `kx`-relative, length `M`); `dp` the `kx`-relative +/// `degPatched`; `groups` the Figure 4.54 gain groups (absolute +/// subband indices). Subbands outside every group keep `GLimBoost`. +pub fn aliasing_reduction( + g: &mut [f64], + e_curr: &[f64], + dp: &[f64], + groups: &[(usize, usize)], + k_x: i32, +) -> Result<()> { + let m_cnt = g.len(); + if e_curr.len() != m_cnt || dp.len() != m_cnt || k_x < 0 { + return Err(Error::SbrFreqBandInvalid); + } + let kx = k_x as usize; + for &(start, stop) in groups { + if start < kx || stop > kx + m_cnt || start >= stop { + return Err(Error::SbrFreqBandInvalid); + } + let lo = start - kx; + let hi = stop - kx; + // ETotal: the group energy the GLimBoost gains would produce. + let mut e_total = 0.0f64; + let mut e_curr_sum = 0.0f64; + for i in lo..hi { + e_total += g[i] * g[i] * e_curr[i]; + e_curr_sum += e_curr[i]; + } + // GTarget²: the group-equalized gain. + let g_target2 = e_total / (EPS0 + e_curr_sum); + // α(m)-weighted blend into G²ARtemp. + let mut g_ar2 = vec![0.0f64; hi - lo]; + for i in lo..hi { + let alpha = if i + 1 < m_cnt { + dp[i].max(dp[i + 1]) + } else { + dp[i] + }; + g_ar2[i - lo] = alpha * g_target2 + (1.0 - alpha) * g[i] * g[i]; + } + // Restore the exact group output energy. + let mut e_total_new = 0.0f64; + for (i, &ga2) in (lo..hi).zip(g_ar2.iter()) { + e_total_new += ga2 * e_curr[i]; + } + let scale2 = e_total / (EPS0 + e_total_new); + for (i, &ga2) in (lo..hi).zip(g_ar2.iter()) { + g[i] = (ga2 * scale2).sqrt(); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Figure 4.53 hand-walked: an even subband with a negative + /// reflection whose left neighbour also reflects negatively is a + /// full-degree alias pair, and a positive `ref(k−2)` marks the + /// neighbour too. + #[test] + fn aliasing_degree_even_subband_cases() { + // k = 2: sign = 1 (ref[2] < 0); sign·ref[1] < 0 → deg[2] = 1; + // sign·ref[0] forced 0 → no deg[1] update. + let deg = aliasing_degree(&[0.9, -0.6, -0.5, 0.0]); + assert_eq!(deg, vec![0.0, 0.0, 1.0, 0.0]); + + // k = 4: sign = 1; ref[3] = −0.4 < 0 → deg[4] = 1, and ref[2] + // = 0.5 > 0 marks the neighbour: deg[3] = 1 − 0.4² = 0.84 + // (k = 2 and k = 3 fire on neither orientation). + let deg = aliasing_degree(&[0.0, 0.0, 0.5, -0.4, -0.3]); + assert_eq!(deg[4], 1.0); + assert!((deg[3] - 0.84).abs() < 1e-12); + assert_eq!(°[..3], &[0.0, 0.0, 0.0]); + + // k = 2 with ref[1] < 0 and ref[0]... ref[0] is forced to 0 by + // the flowchart even when transmitted non-zero. + let deg = aliasing_degree(&[0.9, 0.2, -0.5, 0.0]); + assert_eq!(deg[2], 0.0, "no alias: sign·ref[1] > 0, ref[0] forced 0"); + } + + /// Figure 4.53 odd-subband orientation: positive reflection at an + /// odd `k` with a positive left neighbour (sign = −1 → + /// sign·ref[k−1] < 0) is a full-degree alias, and `ref(k−2) < 0` + /// marks the neighbour. + #[test] + fn aliasing_degree_odd_subband_cases() { + // k = 3: ref[3] > 0 → sign = −1; −ref[2] < 0 (ref[2] > 0) → + // deg[3] = 1; −ref[1] > 0 (ref[1] < 0) → deg[2] = 1 − ref[2]². + let deg = aliasing_degree(&[0.0, -0.8, 0.3, 0.7]); + assert_eq!(deg[3], 1.0); + assert!((deg[2] - (1.0 - 0.09)).abs() < 1e-12); + + // Odd-k else-branch: k = 2 fires first (ref[2] < 0, ref[1] < + // 0 → deg[2] = 1), then k = 3: −ref[2] = 0.3 ≥ 0 → else; + // −ref[1] = 0.6 > 0 → deg[3] = 1 − ref[2]² = 0.91. + let deg = aliasing_degree(&[0.0, -0.6, -0.3, 0.7]); + assert_eq!(deg[2], 1.0); + assert!((deg[3] - 0.91).abs() < 1e-12); + } + + /// `degPatched`: the source degrees ride the patch mapping, patch + /// starts and uncovered tail are zero. + #[test] + fn deg_patched_rides_patches() { + let patches = Patches { + start: vec![2, 4], + num: vec![3, 2], + }; + // deg over the low band 0..k0. + let deg = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]; + // M = 7: patches cover 5 subbands, tail of 2 stays zero. + let dp = deg_patched(°, &patches, 10, 7).unwrap(); + // Patch 0 (src 2..5): x=0 → 0, then deg[3], deg[4]. + // Patch 1 (src 4..6): x=0 → 0, then deg[5]. + assert_eq!(dp, vec![0.0, 0.3, 0.4, 0.0, 0.5, 0.0, 0.0]); + } + + /// Figure 4.54 hand-walk: a run of alias-prone boundaries opens a + /// group at its first subband and closes it past the last one; a + /// mapped sinusoid closes the group *before* the sinusoid subband; + /// a run reaching the loop end closes at `kx + M`. + #[test] + fn gain_groups_hand_walk() { + let kx = 8; + // dp[1], dp[2] non-zero → boundaries after subbands 0 and 1. + let dp = [0.0, 1.0, 0.5, 0.0, 0.0, 0.0]; + let sm = [false; 6]; + assert_eq!(gain_groups(&dp, &sm, kx), vec![(8, 11)]); + + // A sinusoid at rel 1 blocks the group from covering it: the + // open condition fails at rel 1, and the close lands at k + // (the sinusoid subband) rather than k + 1. + let sm = [false, true, false, false, false, false]; + let dp = [0.0, 1.0, 1.0, 1.0, 0.0, 0.0]; + assert_eq!(gain_groups(&dp, &sm, kx), vec![(8, 9), (10, 12)]); + + // A run whose alias boundaries reach the end of the SBR range + // closes at kx + M. + let sm = [false; 6]; + let dp = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0]; + assert_eq!(gain_groups(&dp, &sm, kx), vec![(11, 14)]); + } + + /// §4.6.18.8.5: the group output energy under GA equals the + /// GLimBoost energy exactly (the ETotal/ETotalNew normalization), + /// and a full-degree group equalizes the gains. + #[test] + fn aliasing_reduction_preserves_group_energy() { + let kx = 8; + let e_curr = [4.0, 1.0, 9.0, 2.0]; + let mut g = [3.0, 0.5, 1.0, 2.0]; + let dp = [0.0, 1.0, 1.0, 1.0]; + let groups = vec![(8usize, 12usize)]; + let e_before: f64 = g + .iter() + .zip(e_curr.iter()) + .map(|(gi, ei)| gi * gi * ei) + .sum(); + aliasing_reduction(&mut g, &e_curr, &dp, &groups, kx).unwrap(); + let e_after: f64 = g + .iter() + .zip(e_curr.iter()) + .map(|(gi, ei)| gi * gi * ei) + .sum(); + assert!( + (e_after - e_before).abs() < 1e-6 * e_before, + "group energy {e_after} vs {e_before}" + ); + // α = 1 on every interior subband → gains equalize to the + // target (the last subband blends with α = dp[3] = 1 too). + for w in g.windows(2) { + assert!((w[0] - w[1]).abs() < 1e-9, "gains not equalized: {g:?}"); + } + } + + /// Subbands outside every group keep their GLimBoost value. + #[test] + fn aliasing_reduction_leaves_ungrouped_gains() { + let kx = 0; + let e_curr = [1.0, 1.0, 1.0, 1.0]; + let mut g = [1.0, 2.0, 3.0, 4.0]; + let dp = [0.0, 0.6, 0.0, 0.0]; + let groups = vec![(0usize, 2usize)]; + aliasing_reduction(&mut g, &e_curr, &dp, &groups, kx).unwrap(); + assert_eq!(g[2], 3.0); + assert_eq!(g[3], 4.0); + // The grouped pair's energy is preserved. + let e = g[0] * g[0] + g[1] * g[1]; + assert!((e - 5.0).abs() < 1e-9, "group energy {e}"); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_noise_table.rs b/crates/vendor/oxideav-aac/src/sbr_noise_table.rs new file mode 100644 index 00000000..c25a7728 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_noise_table.rs @@ -0,0 +1,564 @@ +//! SBR noise table `V` — ISO/IEC 14496-3 Table 4.A.91. +//! +//! The 512-entry complex pseudo-noise sequence +//! `(φRe,noise(i), φIm,noise(i))` that the §4.6.18.7.6 HF assembly adds +//! at the `QFilt` level, indexed by the running +//! `fIndexNoise = (indexNoise + (i − RATE·tE(0))·M + m + 1) mod 512`. +//! +//! Transcribed digit-for-digit from the Table 4.A.91 grid of the +//! staged ISO/IEC 14496-3:2009 spec PDF (`docs/audio/aac/`); the +//! staged `sbr-tables/sbr-random-phase.csv` data table cross-checks +//! every present value to 1e-9 (the CSV is missing one of the 1024 +//! scalars, so the spec grid is the authoritative source here). +//! +//! ## Provenance +//! +//! Numeric data only, from the staged spec PDF. No part of this table +//! or its indexing is derived from any external decoder +//! implementation. + +/// Table 4.A.91 — `V(0, i) + i·V(1, i)` as `(re, im)` pairs. +#[rustfmt::skip] +pub const NOISE_TABLE: [(f64, f64); 512] = [ + (-0.99948153278296, -0.59483417516607), + (0.97113454393991, -0.67528515225647), + (0.14130051758487, -0.95090983575689), + (-0.47005496701697, -0.37340549728647), + (0.80705063769351, 0.29653668284408), + (-0.38981478896926, 0.89572605717087), + (-0.01053049862020, -0.66959058036166), + (-0.91266367957293, -0.11522938140034), + (0.54840422910309, 0.75221367176302), + (0.40009252867955, -0.98929400334421), + (-0.99867974711855, -0.88147068645358), + (-0.95531076805040, 0.90908757154593), + (-0.45725933317144, -0.56716323646760), + (-0.72929675029275, -0.98008272727324), + (0.75622801399036, 0.20950329995549), + (0.07069442601050, -0.78247898470706), + (0.74496252926055, -0.91169004445807), + (-0.96440182703856, -0.94739918296622), + (0.30424629369539, -0.49438267012479), + (0.66565033746925, 0.64652935542491), + (0.91697008020594, 0.17514097332009), + (-0.70774918760427, 0.52548653416543), + (-0.70051415345560, -0.45340028808763), + (-0.99496513054797, -0.90071908066973), + (0.98164490790123, -0.77463155528697), + (-0.54671580548181, -0.02570928536004), + (-0.01689629065389, 0.00287506445732), + (-0.86110349531986, 0.42548583726477), + (-0.98892980586032, -0.87881132267556), + (0.51756627678691, 0.66926784710139), + (-0.99635026409640, -0.58107730574765), + (-0.99969370862163, 0.98369989360250), + (0.55266258627194, 0.59449057465591), + (0.34581177741673, 0.94879421061866), + (0.62664209577999, -0.74402970906471), + (-0.77149701404973, -0.33883658042801), + (-0.91592244254432, 0.03687901376713), + (-0.76285492357887, -0.91371867919124), + (0.79788337195331, -0.93180971199849), + (0.54473080610200, -0.11919206037186), + (-0.85639281671058, 0.42429854760451), + (-0.92882402971423, 0.27871809078609), + (-0.11708371046774, -0.99800843444966), + (0.21356749817493, -0.90716295627033), + (-0.76191692573909, 0.99768118356265), + (0.98111043100884, -0.95854459734407), + (-0.85913269895572, 0.95766566168880), + (-0.93307242253692, 0.49431757696466), + (0.30485754879632, -0.70540034357529), + (0.85289650925190, 0.46766131791044), + (0.91328082618125, -0.99839597361769), + (-0.05890199924154, 0.70741827819497), + (0.28398686150148, 0.34633555702188), + (0.95258164539612, -0.54893416026939), + (-0.78566324168507, -0.75568541079691), + (-0.95789495447877, -0.20423194696966), + (0.82411158711197, 0.96654618432562), + (-0.65185446735885, -0.88734990773289), + (-0.93643603134666, 0.99870790442385), + (0.91427159529618, -0.98290505544444), + (-0.70395684036886, 0.58796798221039), + (0.00563771969365, 0.61768196727244), + (0.89065051931895, 0.52783352697585), + (-0.68683707712762, 0.80806944710339), + (0.72165342518718, -0.69259857349564), + (-0.62928247730667, 0.13627037407335), + (0.29938434065514, -0.46051329682246), + (-0.91781958879280, -0.74012716684186), + (0.99298717043688, 0.40816610075661), + (0.82368298622748, -0.74036047190173), + (-0.98512833386833, -0.99972330709594), + (-0.95915368242257, -0.99237800466040), + (-0.21411126572790, -0.93424819052545), + (-0.68821476106884, -0.26892306315457), + (0.91851997982317, 0.09358228901785), + (-0.96062769559127, 0.36099095133739), + (0.51646184922287, -0.71373332873917), + (0.61130721139669, 0.46950141175917), + (0.47336129371299, -0.27333178296162), + (0.90998308703519, 0.96715662938132), + (0.44844799194357, 0.99211574628306), + (0.66614891079092, 0.96590176169121), + (0.74922239129237, -0.89879858826087), + (-0.99571588506485, 0.52785521494349), + (0.97401082477563, -0.16855870075190), + (0.72683747733879, -0.48060774432251), + (0.95432193457128, 0.68849603408441), + (-0.72962208425191, -0.76608443420917), + (-0.85359479233537, 0.88738125901579), + (-0.81412430338535, -0.97480768049637), + (-0.87930772356786, 0.74748307690436), + (-0.71573331064977, -0.98570608178923), + (0.83524300028228, 0.83702537075163), + (-0.48086065601423, -0.98848504923531), + (0.97139128574778, 0.80093621198236), + (0.51992825347895, 0.80247631400510), + (-0.00848591195325, -0.76670128000486), + (-0.70294374303036, 0.55359910445577), + (-0.95894428168140, -0.43265504344783), + (0.97079252950321, 0.09325857238682), + (-0.92404293670797, 0.85507704027855), + (-0.69506469500450, 0.98633412625459), + (0.26559203620024, 0.73314307966524), + (0.28038443336943, 0.14537913654427), + (-0.74138124825523, 0.99310339807762), + (-0.01752795995444, -0.82616635284178), + (-0.55126773094930, -0.98898543862153), + (0.97960898850996, -0.94021446752851), + (-0.99196309146936, 0.67019017358456), + (-0.67684928085260, 0.12631491649378), + (0.09140039465500, -0.20537731453108), + (-0.71658965751996, -0.97788200391224), + (0.81014640078925, 0.53722648362443), + (0.40616991671205, -0.26469008598449), + (-0.67680188682972, 0.94502052337695), + (0.86849774348749, -0.18333598647899), + (-0.99500381284851, -0.02634122068550), + (0.84329189340667, 0.10406957462213), + (-0.09215968531446, 0.69540012101253), + (0.99956173327206, -0.12358542001404), + (-0.79732779473535, -0.91582524736159), + (0.96349973642406, 0.96640458041000), + (-0.79942778496547, 0.64323902822857), + (-0.11566039853896, 0.28587846253726), + (-0.39922954514662, 0.94129601616966), + (0.99089197565987, -0.92062625581587), + (0.28631285179909, -0.91035047143603), + (-0.83302725605608, -0.67330410892084), + (0.95404443402072, 0.49162765398743), + (-0.06449863579434, 0.03250560813135), + (-0.99575054486311, 0.42389784469507), + (-0.65501142790847, 0.82546114655624), + (-0.81254441908887, -0.51627234660629), + (-0.99646369485481, 0.84490533520752), + (0.00287840603348, 0.64768261158166), + (0.70176989408455, -0.20453028573322), + (0.96361882270190, 0.40706967140989), + (-0.68883758192426, 0.91338958840772), + (-0.34875585502238, 0.71472290693300), + (0.91980081243087, 0.66507455644919), + (-0.99009048343881, 0.85868021604848), + (0.68865791458395, 0.55660316809678), + (-0.99484402129368, -0.20052559254934), + (0.94214511408023, -0.99696425367461), + (-0.67414626793544, 0.49548221180078), + (-0.47339353684664, -0.85904328834047), + (0.14323651387360, -0.94145598222488), + (-0.29268293575672, 0.05759224927952), + (0.43793861458754, -0.78904969892724), + (-0.36345126374441, 0.64874435357162), + (-0.08750604656825, 0.97686944362527), + (-0.96495267812511, -0.53960305946511), + (0.55526940659947, 0.78891523734774), + (0.73538215752630, 0.96452072373404), + (-0.30889773919437, -0.80664389776860), + (0.03574995626194, -0.97325616900959), + (0.98720684660488, 0.48409133691962), + (-0.81689296271203, -0.90827703628298), + (0.67866860118215, 0.81284503870856), + (-0.15808569732583, 0.85279555024382), + (0.80723395114371, -0.24717418514605), + (0.47788757329038, -0.46333147839295), + (0.96367554763201, 0.38486749303242), + (-0.99143875716818, -0.24945277239809), + (0.83081876925833, -0.94780851414763), + (-0.58753191905341, 0.01290772389163), + (0.95538108220960, -0.85557052096538), + (-0.96490920476211, -0.64020970923102), + (-0.97327101028521, 0.12378128133110), + (0.91400366022124, 0.57972471346930), + (-0.99925837363824, 0.71084847864067), + (-0.86875903507313, -0.20291699203564), + (-0.26240034795124, -0.68264554369108), + (-0.24664412953388, -0.87642273115183), + (0.02416275806869, 0.27192914288905), + (0.82068619590515, -0.85087787994476), + (0.88547373760759, -0.89636802901469), + (-0.18173078152226, -0.26152145156800), + (0.09355476558534, 0.54845123045604), + (-0.54668414224090, 0.95980774020221), + (0.37050990604091, -0.59910140383171), + (-0.70373594262891, 0.91227665827081), + (-0.34600785879594, -0.99441426144200), + (-0.68774481731008, -0.30238837956299), + (-0.26843291251234, 0.83115668004362), + (0.49072334613242, -0.45359708737775), + (0.38975993093975, 0.95515358099121), + (-0.97757125224150, 0.05305894580606), + (-0.17325552859616, -0.92770672250494), + (0.99948035025744, 0.58285545563426), + (-0.64946246527458, 0.68645507104960), + (-0.12016920576437, -0.57147322153312), + (-0.58947456517751, -0.34847132454388), + (-0.41815140454465, 0.16276422358861), + (0.99885650204884, 0.11136095490444), + (-0.56649614128386, -0.90494866361587), + (0.94138021032330, 0.35281916733018), + (-0.75725076534641, 0.53650549640587), + (0.20541973692630, -0.94435144369918), + (0.99980371023351, 0.79835913565599), + (0.29078277605775, 0.35393777921520), + (-0.62858772103030, 0.38765693387102), + (0.43440904467688, -0.98546330463232), + (-0.98298583762390, 0.21021524625209), + (0.19513029146934, -0.94239832251867), + (-0.95476662400101, 0.98364554179143), + (0.93379635304810, -0.70881994583682), + (-0.85235410573336, -0.08342347966410), + (-0.86425093011245, -0.45795025029466), + (0.38879779059045, 0.97274429344593), + (0.92045124735495, -0.62433652524220), + (0.89162532251878, 0.54950955570563), + (-0.36834336949252, 0.96458298020975), + (0.93891760988045, -0.89968353740388), + (0.99267657565094, -0.03757034316958), + (-0.94063471614176, 0.41332338538963), + (0.99740224117019, -0.16830494996370), + (-0.35899413170555, -0.46633226649613), + (0.05237237274947, -0.25640361602661), + (0.36703583957424, -0.38653265641875), + (0.91653180367913, -0.30587628726597), + (0.69000803499316, 0.90952171386132), + (-0.38658751133527, 0.99501571208985), + (-0.29250814029851, 0.37444994344615), + (-0.60182204677608, 0.86779651036123), + (-0.97418588163217, 0.96468523666475), + (0.88461574003963, 0.57508405276414), + (0.05198933055162, 0.21269661669964), + (-0.53499621979720, 0.97241553731237), + (-0.49429560226497, 0.98183865291903), + (-0.98935142339139, -0.40249159006933), + (-0.98081380091130, -0.72856895534041), + (-0.27338148835532, 0.99950922447209), + (0.06310802338302, -0.54539587529618), + (-0.20461677199539, -0.14209977628489), + (0.66223843141647, 0.72528579940326), + (-0.84764345483665, 0.02372316801261), + (-0.89039863483811, 0.88866581484602), + (0.95903308477986, 0.76744927173873), + (0.73504123909879, -0.03747203173192), + (-0.31744434966056, -0.36834111883652), + (-0.34110827591623, 0.40211222807691), + (0.47803883714199, -0.39423219786288), + (0.98299195879514, 0.01989791390047), + (-0.30963073129751, -0.18076720599336), + (0.99992588229018, -0.26281872094289), + (-0.93149731080767, -0.98313162570490), + (0.99923472302773, -0.80142993767554), + (-0.26024169633417, -0.75999759855752), + (-0.35712514743563, 0.19298963768574), + (-0.99899084509530, 0.74645156992493), + (0.86557171579452, 0.55593866696299), + (0.33408042438752, 0.86185953874709), + (0.99010736374716, 0.04602397576623), + (-0.66694269691195, -0.91643611810148), + (0.64016792079480, 0.15649530836856), + (0.99570534804836, 0.45844586038111), + (-0.63431466947340, 0.21079116459234), + (-0.07706847005931, -0.89581437101329), + (0.98590090577724, 0.88241721133981), + (0.80099335254678, -0.36851896710853), + (0.78368131392666, 0.45506999802597), + (0.08707806671691, 0.80938994918745), + (-0.86811883080712, 0.39347308654705), + (-0.39466529740375, -0.66809432114456), + (0.97875325649683, -0.72467840967746), + (-0.95038560288864, 0.89563219587625), + (0.17005239424212, 0.54683053962658), + (-0.76910792026848, -0.96226617549298), + (0.99743281016846, 0.42697157037567), + (0.95437383549973, 0.97002324109952), + (0.99578905365569, -0.54106826257356), + (0.28058259829990, -0.85361420634036), + (0.85256524470573, -0.64567607735589), + (-0.50608540105128, -0.65846015480300), + (-0.97210735183243, -0.23095213067791), + (0.95424048234441, -0.99240147091219), + (-0.96926570524023, 0.73775654896574), + (0.30872163214726, 0.41514960556126), + (-0.24523839572639, 0.63206633394807), + (-0.33813265086024, -0.38661779441897), + (-0.05826828420146, -0.06940774188029), + (-0.22898461455054, 0.97054853316316), + (-0.18509915019881, 0.47565762892084), + (-0.10488238045009, -0.87769947402394), + (-0.71886586182037, 0.78030982480538), + (0.99793873738654, 0.90041310491497), + (0.57563307626120, -0.91034337352097), + (0.28909646383717, 0.96307783970534), + (0.42188998312520, 0.48148651230437), + (0.93335049681047, -0.43537023883588), + (-0.97087374418267, 0.86636445711364), + (0.36722871286923, 0.65291654172961), + (-0.81093025665696, 0.08778370229363), + (-0.26240603062237, -0.92774095379098), + (0.83996497984604, 0.55839849139647), + (-0.99909615720225, -0.96024605713970), + (0.74649464155061, 0.12144893606462), + (-0.74774595569805, -0.26898062008959), + (0.95781667469567, -0.79047927052628), + (0.95472308713099, -0.08588776019550), + (0.48708332746299, 0.99999041579432), + (0.46332038247497, 0.10964126185063), + (-0.76497004940162, 0.89210929242238), + (0.57397389364339, 0.35289703373760), + (0.75374316974495, 0.96705214651335), + (-0.59174397685714, -0.89405370422752), + (0.75087906691890, -0.29612672982396), + (-0.98607857336230, 0.25034911730023), + (-0.40761056640505, -0.90045573444695), + (0.66929266740477, 0.98629493401748), + (-0.97463695257310, -0.00190223301301), + (0.90145509409859, 0.99781390365446), + (-0.87259289048043, 0.99233587353666), + (-0.91529461447692, -0.15698707534206), + (-0.03305738840705, -0.37205262859764), + (0.07223051368337, -0.88805001733626), + (0.99498012188353, 0.97094358113387), + (-0.74904939500519, 0.99985483641521), + (0.04585228574211, 0.99812337444082), + (-0.89054954257993, -0.31791913188064), + (-0.83782144651251, 0.97637632547466), + (0.33454804933804, -0.86231516800408), + (-0.99707579362824, 0.93237990079441), + (-0.22827527843994, 0.18874759397997), + (0.67248046289143, -0.03646211390569), + (-0.05146538187944, -0.92599700120679), + (0.99947295749905, 0.93625229707912), + (0.66951124390363, 0.98905825623893), + (-0.99602956559179, -0.44654715757688), + (0.82104905483590, 0.99540741724928), + (0.99186510988782, 0.72023001312947), + (-0.65284592392918, 0.52186723253637), + (0.93885443798188, -0.74895312615259), + (0.96735248738388, 0.90891816978629), + (-0.22225968841114, 0.57124029781228), + (-0.44132783753414, -0.92688840659280), + (-0.85694974219574, 0.88844532719844), + (0.91783042091762, -0.46356892383970), + (0.72556974415690, -0.99899555770747), + (-0.99711581834508, 0.58211560180426), + (0.77638976371966, 0.94321834873819), + (0.07717324253925, 0.58638399856595), + (-0.56049829194163, 0.82522301569036), + (0.98398893639988, 0.39467440420569), + (0.47546946844938, 0.68613044836811), + (0.65675089314631, 0.18331637134880), + (0.03273375457980, -0.74933109564108), + (-0.38684144784738, 0.51337349030406), + (-0.97346267944545, -0.96549364384098), + (-0.53282156061942, -0.91423265091354), + (0.99817310731176, 0.61133572482148), + (-0.50254500772635, -0.88829338134294), + (0.01995873238855, 0.85223515096765), + (0.99930381973804, 0.94578896296649), + (0.82907767600783, -0.06323442598128), + (-0.58660709669728, 0.96840773806582), + (-0.17573736667267, -0.48166920859485), + (0.83434292401346, -0.13023450646997), + (0.05946491307025, 0.20511047074866), + (0.81505484574602, -0.94685947861369), + (-0.44976380954860, 0.40894572671545), + (-0.89746474625671, 0.99846578838537), + (0.39677256130792, -0.74854668609359), + (-0.07588948563079, 0.74096214084170), + (0.76343198951445, 0.41746629422634), + (-0.74490104699626, 0.94725911744610), + (0.64880119792759, 0.41336660830571), + (0.62319537462542, -0.93098313552599), + (0.42215817594807, -0.07712787385208), + (0.02704554141885, -0.05417518053666), + (0.80001773566818, 0.91542195141039), + (-0.79351832348816, -0.36208897989136), + (0.63872359151636, 0.08128252493444), + (0.52890520960295, 0.60048872455592), + (0.74238552914587, 0.04491915291044), + (0.99096131449250, -0.19451182854402), + (-0.80412329643109, -0.88513818199457), + (-0.64612616129736, 0.72198674804544), + (0.11657770663191, -0.83662833815041), + (-0.95053182488101, -0.96939905138082), + (-0.62228872928622, 0.82767262846661), + (0.03004475787316, -0.99738896333384), + (-0.97987214341034, 0.36526129686425), + (-0.99986980746200, -0.36021610299715), + (0.89110648599879, -0.97894250343044), + (0.10407960510582, 0.77357793811619), + (0.95964737821728, -0.35435818285502), + (0.50843233159162, 0.96107691266205), + (0.17006334670615, -0.76854025314829), + (0.25872675063360, 0.99893303933816), + (-0.01115998681937, 0.98496019742444), + (-0.79598702973261, 0.97138411318894), + (-0.99264708948101, -0.99542822402536), + (-0.99829663752818, 0.01877138824311), + (-0.70801016548184, 0.33680685948117), + (-0.70467057786826, 0.93272777501857), + (0.99846021905254, -0.98725746254433), + (-0.63364968534650, -0.16473594423746), + (-0.16258217500792, -0.95939125400802), + (-0.43645594360633, -0.94805030113284), + (-0.99848471702976, 0.96245166923809), + (-0.16796458968998, -0.98987511890470), + (-0.87979225745213, -0.71725725041680), + (0.44183099021786, -0.93568974498761), + (0.93310180125532, -0.99913308068246), + (-0.93941931782002, -0.56409379640356), + (-0.88590003188677, 0.47624600491382), + (0.99971463703691, -0.83889954253462), + (-0.75376385639978, 0.00814643438625), + (0.93887685615875, -0.11284528204636), + (0.85126435782309, 0.52349251543547), + (0.39701421446381, 0.81779634174316), + (-0.37024464187437, -0.87071656222959), + (-0.36024828242896, 0.34655735648287), + (-0.93388812549209, -0.84476541096429), + (-0.65298804552119, -0.18439575450921), + (0.11960319006843, 0.99899346780168), + (0.94292565553160, 0.83163906518293), + (0.75081145286948, -0.35533223142265), + (0.56721979748394, -0.24076836414499), + (0.46857766746029, -0.30140233457198), + (0.97312313923635, -0.99548191630031), + (-0.38299976567017, 0.98516909715427), + (0.41025800019463, 0.02116736935734), + (0.09638062008048, 0.04411984381457), + (-0.85283249275397, 0.91475563922421), + (0.88866808958124, -0.99735267083226), + (-0.48202429536989, -0.96805608884164), + (0.27572582416567, 0.58634753335832), + (-0.65889129659168, 0.58835634138583), + (0.98838086953732, 0.99994349600236), + (-0.20651349620689, 0.54593044066355), + (-0.62126416356920, -0.59893681700392), + (0.20320105410437, -0.86879180355289), + (-0.97790548600584, 0.96290806999242), + (0.11112534735126, 0.21484763313301), + (-0.41368337314182, 0.28216837680365), + (0.24133038992960, 0.51294362630238), + (-0.66393410674885, -0.08249679629081), + (-0.53697829178752, -0.97649903936228), + (-0.97224737889348, 0.22081333579837), + (0.87392477144549, -0.12796173740361), + (0.19050361015753, 0.01602615387195), + (-0.46353441212724, -0.95249041539006), + (-0.07064096339021, -0.94479803205886), + (-0.92444085484466, -0.10457590187436), + (-0.83822593578728, -0.01695043208885), + (0.75214681811150, -0.99955681042665), + (-0.42102998829339, 0.99720941999394), + (-0.72094786237696, -0.35008961934255), + (0.78843311019251, 0.52851398958271), + (0.97394027897442, -0.26695944086561), + (0.99206463477946, -0.57010120849429), + (0.76789609461795, -0.76519356730966), + (-0.82002421836409, -0.73530179553767), + (0.81924990025724, 0.99698425250579), + (-0.26719850873357, 0.68903369776193), + (-0.43311260380975, 0.85321815947490), + (0.99194979673836, 0.91876249766422), + (-0.80692001248487, -0.32627540663214), + (0.43080003649976, -0.21919095636638), + (0.67709491937357, -0.95478075822906), + (0.56151770568316, -0.70693811747778), + (0.10831862810749, -0.08628837174592), + (0.91229417540436, -0.65987351408410), + (-0.48972893932274, 0.56289246362686), + (-0.89033658689697, -0.71656563987082), + (0.65269447475094, 0.65916004833932), + (0.67439478141121, -0.81684380846796), + (-0.47770832416973, -0.16789556203025), + (-0.99715979260878, -0.93565784007648), + (-0.90889593602546, 0.62034397054380), + (-0.06618622548177, -0.23812217221359), + (0.99430266919728, 0.18812555317553), + (0.97686402381843, -0.28664534366620), + (0.94813650221268, -0.97506640027128), + (-0.95434497492853, -0.79607978501983), + (-0.49104783137150, 0.32895214359663), + (0.99881175120751, 0.88993983831354), + (0.50449166760303, -0.85995072408434), + (0.47162891065108, -0.18680204049569), + (-0.62081581361840, 0.75000676218956), + (-0.43867015250812, 0.99998069244322), + (0.98630563232075, -0.53578899600662), + (-0.61510362277374, -0.89515019899997), + (-0.03841517601843, -0.69888815681179), + (-0.30102157304644, -0.07667808922205), + (0.41881284182683, 0.02188098922282), + (-0.86135454941237, 0.98947480909359), + (0.67226861393788, -0.13494389011014), + (-0.70737398842068, -0.76547349325992), + (0.94044946687963, 0.09026201157416), + (-0.82386352534327, 0.08924768823676), + (-0.32070666698656, 0.50143421908753), + (0.57593163224487, -0.98966422921509), + (-0.36326018419965, 0.07440243123228), + (0.99979044674350, -0.14130287347405), + (-0.92366023326932, -0.97979298068180), + (-0.44607178518598, -0.54233252016394), + (0.44226800932956, 0.71326756742752), + (0.03671907158312, 0.63606389366675), + (0.52175424682195, -0.85396826735705), + (-0.94701139690956, -0.01826348194255), + (-0.98759606946049, 0.82288714303073), + (0.87434794743625, 0.89399495655433), + (-0.93412041758744, 0.41374052024363), + (0.96063943315511, 0.93116709541280), + (0.97534253457837, 0.86150930812689), + (0.99642466504163, 0.70190043427512), + (-0.94705089665984, -0.29580042814306), + (0.91599807087376, -0.98147830385781), +]; + +#[cfg(test)] +mod tests { + use super::NOISE_TABLE; + + /// Table 4.A.91 spot values (first, last, and an interior row). + #[test] + fn spot_values() { + assert_eq!(NOISE_TABLE[0], (-0.99948153278296, -0.59483417516607)); + assert_eq!(NOISE_TABLE[1], (0.97113454393991, -0.67528515225647)); + assert_eq!(NOISE_TABLE[511], (0.91599807087376, -0.98147830385781)); + } + + /// The sequence is a bounded pseudo-noise table: every component + /// stays within (-1, 1] and the sequence is zero-mean to within a + /// few percent of full scale. + #[test] + fn bounded_and_roughly_zero_mean() { + let mut sum_re = 0.0; + let mut sum_im = 0.0; + for &(re, im) in NOISE_TABLE.iter() { + assert!(re.abs() <= 1.0 && im.abs() <= 1.0); + sum_re += re; + sum_im += im; + } + assert!((sum_re / 512.0).abs() < 0.05); + assert!((sum_im / 512.0).abs() < 0.05); + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_qmf.rs b/crates/vendor/oxideav-aac/src/sbr_qmf.rs new file mode 100644 index 00000000..a1b6bedb --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_qmf.rs @@ -0,0 +1,1005 @@ +//! SBR QMF filterbanks — ISO/IEC 14496-3 §4.6.18.4. +//! +//! The complex-exponential-modulated filterbank pair of the SBR tool: +//! +//! * [`AnalysisQmf`] — §4.6.18.4.1 / Figure 4.42: splits the core +//! decoder's time-domain output into 32 complex-valued subband +//! signals (oversampled by two relative to a real QMF bank), one +//! 32-sample slot at a time. +//! * [`SynthesisQmf`] — §4.6.18.4.2 / Figure 4.43: recombines 64 +//! complex subbands into 64 real time-domain samples per slot (the +//! dual-rate output of the SBR tool). +//! * [`DownsampledSynthesisQmf`] — §4.6.18.4.3 / Figure 4.44: the +//! 32-channel variant that keeps the output at the core rate. +//! +//! The low-power SBR tool (§4.6.18.8) replaces the complex banks with +//! real-valued ones (§4.6.18.8.2): +//! +//! * [`RealAnalysisQmf`] — §4.6.18.8.2.2 / Figure 4.50: 32 real-valued, +//! critically sampled subband signals. +//! * [`RealSynthesisQmf`] — §4.6.18.8.2.3 / Figure 4.51: the 64-subband +//! real synthesis bank (dual-rate output). +//! * [`RealDownsampledSynthesisQmf`] — §4.6.18.8.2.4 / Figure 4.52: the +//! 32-channel real variant at the core rate. +//! +//! The 640-tap prototype window `c[i]` is Table 4.A.89, transcribed +//! from the staged ISO/IEC 14496-3:2009 spec PDF (`docs/audio/aac/`). +//! The table prints `c[639]` with nine decimals (`-0.000552528`); every +//! other entry carries ten. The transcription preserves the printed +//! digits verbatim, including the mirror structure +//! `|c[i]| == |c[640 - i]|` that the tests pin. +//! +//! ## Provenance +//! +//! Every constant and loop bound below comes from the §4.6.18.4 text +//! and the Figure 4.42 / 4.43 / 4.44 flowcharts of the staged spec. +//! No part of this implementation is derived from any external decoder. + +use crate::{Error, Result}; + +/// A complex number, as used by the SBR subband domain (§4.6.18.2.2: +/// the subband samples are complex-valued). +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Complex { + /// Real part. + pub re: f64, + /// Imaginary part. + pub im: f64, +} + +impl Complex { + /// `re + i·im`. + #[inline] + #[must_use] + pub fn new(re: f64, im: f64) -> Self { + Complex { re, im } + } + + /// The complex conjugate. + #[inline] + #[must_use] + pub fn conj(self) -> Self { + Complex { + re: self.re, + im: -self.im, + } + } + + /// Squared magnitude `re² + im²`. + #[inline] + #[must_use] + pub fn norm_sqr(self) -> f64 { + self.re * self.re + self.im * self.im + } +} + +impl core::ops::Add for Complex { + type Output = Complex; + #[inline] + fn add(self, rhs: Complex) -> Complex { + Complex::new(self.re + rhs.re, self.im + rhs.im) + } +} + +impl core::ops::Sub for Complex { + type Output = Complex; + #[inline] + fn sub(self, rhs: Complex) -> Complex { + Complex::new(self.re - rhs.re, self.im - rhs.im) + } +} + +impl core::ops::Mul for Complex { + type Output = Complex; + #[inline] + fn mul(self, rhs: Complex) -> Complex { + Complex::new( + self.re * rhs.re - self.im * rhs.im, + self.re * rhs.im + self.im * rhs.re, + ) + } +} + +impl core::ops::Mul for Complex { + type Output = Complex; + #[inline] + fn mul(self, rhs: f64) -> Complex { + Complex::new(self.re * rhs, self.im * rhs) + } +} + +impl core::ops::AddAssign for Complex { + #[inline] + fn add_assign(&mut self, rhs: Complex) { + self.re += rhs.re; + self.im += rhs.im; + } +} + +/// Table 4.A.89 — the 640 coefficients `c[i]` of the QMF bank window, +/// shared by the analysis and both synthesis filterbanks. +#[rustfmt::skip] +pub const QMF_WINDOW: [f64; 640] = [ + 0.0000000000, -0.0005525286, -0.0005617692, -0.0004947518, + -0.0004875227, -0.0004893791, -0.0005040714, -0.0005226564, + -0.0005466565, -0.0005677802, -0.0005870930, -0.0006132747, + -0.0006312493, -0.0006540333, -0.0006777690, -0.0006941614, + -0.0007157736, -0.0007255043, -0.0007440941, -0.0007490598, + -0.0007681371, -0.0007724848, -0.0007834332, -0.0007779869, + -0.0007803664, -0.0007801449, -0.0007757977, -0.0007630793, + -0.0007530001, -0.0007319357, -0.0007215391, -0.0006917937, + -0.0006650415, -0.0006341594, -0.0005946118, -0.0005564576, + -0.0005145572, -0.0004606325, -0.0004095121, -0.0003501175, + -0.0002896981, -0.0002098337, -0.0001446380, -0.0000617334, + 0.0000134949, 0.0001094383, 0.0002043017, 0.0002949531, + 0.0004026540, 0.0005107388, 0.0006239376, 0.0007458025, + 0.0008608443, 0.0009885988, 0.0011250155, 0.0012577884, + 0.0013902494, 0.0015443219, 0.0016868083, 0.0018348265, + 0.0019841140, 0.0021461583, 0.0023017254, 0.0024625616, + 0.0026201758, 0.0027870464, 0.0029469447, 0.0031125420, + 0.0032739613, 0.0034418874, 0.0036008268, 0.0037603922, + 0.0039207432, 0.0040819753, 0.0042264269, 0.0043730719, + 0.0045209852, 0.0046606460, 0.0047932560, 0.0049137603, + 0.0050393022, 0.0051407353, 0.0052461166, 0.0053471681, + 0.0054196775, 0.0054876040, 0.0055475714, 0.0055938023, + 0.0056220643, 0.0056455196, 0.0056389199, 0.0056266114, + 0.0055917128, 0.0055404363, 0.0054753783, 0.0053838975, + 0.0052715758, 0.0051382275, 0.0049839687, 0.0048109469, + 0.0046039530, 0.0043801861, 0.0041251642, 0.0038456408, + 0.0035401246, 0.0032091885, 0.0028446757, 0.0024508540, + 0.0020274176, 0.0015784682, 0.0010902329, 0.0005832264, + 0.0000276045, -0.0005464280, -0.0011568135, -0.0018039472, + -0.0024826723, -0.0031933778, -0.0039401124, -0.0047222596, + -0.0055337211, -0.0063792293, -0.0072615816, -0.0081798233, + -0.0091325329, -0.0101150215, -0.0111315548, -0.0121849995, + 0.0132718220, 0.0143904666, 0.0155405553, 0.0167324712, + 0.0179433381, 0.0191872431, 0.0204531793, 0.0217467550, + 0.0230680169, 0.0244160992, 0.0257875847, 0.0271859429, + 0.0286072173, 0.0300502657, 0.0315017608, 0.0329754081, + 0.0344620948, 0.0359697560, 0.0374812850, 0.0390053679, + 0.0405349170, 0.0420649094, 0.0436097542, 0.0451488405, + 0.0466843027, 0.0482165720, 0.0497385755, 0.0512556155, + 0.0527630746, 0.0542452768, 0.0557173648, 0.0571616450, + 0.0585915683, 0.0599837480, 0.0613455171, 0.0626857808, + 0.0639715898, 0.0652247106, 0.0664367512, 0.0676075985, + 0.0687043828, 0.0697630244, 0.0707628710, 0.0717002673, + 0.0725682583, 0.0733620255, 0.0741003642, 0.0747452558, + 0.0753137336, 0.0758008358, 0.0761992479, 0.0764992170, + 0.0767093490, 0.0768173975, 0.0768230011, 0.0767204924, + 0.0765050718, 0.0761748321, 0.0757305756, 0.0751576255, + 0.0744664394, 0.0736406005, 0.0726774642, 0.0715826364, + 0.0703533073, 0.0689664013, 0.0674525021, 0.0657690668, + 0.0639444805, 0.0619602779, 0.0598166570, 0.0575152691, + 0.0550460034, 0.0524093821, 0.0495978676, 0.0466303305, + 0.0434768782, 0.0401458278, 0.0366418116, 0.0329583930, + 0.0290824006, 0.0250307561, 0.0207997072, 0.0163701258, + 0.0117623832, 0.0069636862, 0.0019765601, -0.0032086896, + -0.0085711749, -0.0141288827, -0.0198834129, -0.0258227288, + -0.0319531274, -0.0382776572, -0.0447806821, -0.0514804176, + -0.0583705326, -0.0654409853, -0.0726943300, -0.0801372934, + -0.0877547536, -0.0955533352, -0.1035329531, -0.1116826931, + -0.1200077984, -0.1285002850, -0.1371551761, -0.1459766491, + -0.1549607071, -0.1640958855, -0.1733808172, -0.1828172548, + -0.1923966745, -0.2021250176, -0.2119735853, -0.2219652696, + -0.2320690870, -0.2423016884, -0.2526480309, -0.2631053299, + -0.2736634040, -0.2843214189, -0.2950716717, -0.3059098575, + -0.3168278913, -0.3278113727, -0.3388722693, -0.3499914122, + 0.3611589903, 0.3723795546, 0.3836350013, 0.3949211761, + 0.4062317676, 0.4175696896, 0.4289119920, 0.4402553754, + 0.4515996535, 0.4629308085, 0.4742453214, 0.4855253091, + 0.4967708254, 0.5079817500, 0.5191234970, 0.5302240895, + 0.5412553448, 0.5522051258, 0.5630789140, 0.5738524131, + 0.5845403235, 0.5951123086, 0.6055783538, 0.6159109932, + 0.6261242695, 0.6361980107, 0.6461269695, 0.6559016302, + 0.6655139880, 0.6749663190, 0.6842353293, 0.6933282376, + 0.7022388719, 0.7109410426, 0.7194462634, 0.7277448900, + 0.7358211758, 0.7436827863, 0.7513137456, 0.7587080760, + 0.7658674865, 0.7727780881, 0.7794287519, 0.7858353120, + 0.7919735841, 0.7978466413, 0.8034485751, 0.8087695004, + 0.8138191270, 0.8185776004, 0.8230419890, 0.8272275347, + 0.8311038457, 0.8346937361, 0.8379717337, 0.8409541392, + 0.8436238281, 0.8459818469, 0.8480315777, 0.8497805198, + 0.8511971524, 0.8523047035, 0.8531020949, 0.8535720573, + 0.8537385600, 0.8535720573, 0.8531020949, 0.8523047035, + 0.8511971524, 0.8497805198, 0.8480315777, 0.8459818469, + 0.8436238281, 0.8409541392, 0.8379717337, 0.8346937361, + 0.8311038457, 0.8272275347, 0.8230419890, 0.8185776004, + 0.8138191270, 0.8087695004, 0.8034485751, 0.7978466413, + 0.7919735841, 0.7858353120, 0.7794287519, 0.7727780881, + 0.7658674865, 0.7587080760, 0.7513137456, 0.7436827863, + 0.7358211758, 0.7277448900, 0.7194462634, 0.7109410426, + 0.7022388719, 0.6933282376, 0.6842353293, 0.6749663190, + 0.6655139880, 0.6559016302, 0.6461269695, 0.6361980107, + 0.6261242695, 0.6159109932, 0.6055783538, 0.5951123086, + 0.5845403235, 0.5738524131, 0.5630789140, 0.5522051258, + 0.5412553448, 0.5302240895, 0.5191234970, 0.5079817500, + 0.4967708254, 0.4855253091, 0.4742453214, 0.4629308085, + 0.4515996535, 0.4402553754, 0.4289119920, 0.4175696896, + 0.4062317676, 0.3949211761, 0.3836350013, 0.3723795546, + -0.3611589903, -0.3499914122, -0.3388722693, -0.3278113727, + -0.3168278913, -0.3059098575, -0.2950716717, -0.2843214189, + -0.2736634040, -0.2631053299, -0.2526480309, -0.2423016884, + -0.2320690870, -0.2219652696, -0.2119735853, -0.2021250176, + -0.1923966745, -0.1828172548, -0.1733808172, -0.1640958855, + -0.1549607071, -0.1459766491, -0.1371551761, -0.1285002850, + -0.1200077984, -0.1116826931, -0.1035329531, -0.0955533352, + -0.0877547536, -0.0801372934, -0.0726943300, -0.0654409853, + -0.0583705326, -0.0514804176, -0.0447806821, -0.0382776572, + -0.0319531274, -0.0258227288, -0.0198834129, -0.0141288827, + -0.0085711749, -0.0032086896, 0.0019765601, 0.0069636862, + 0.0117623832, 0.0163701258, 0.0207997072, 0.0250307561, + 0.0290824006, 0.0329583930, 0.0366418116, 0.0401458278, + 0.0434768782, 0.0466303305, 0.0495978676, 0.0524093821, + 0.0550460034, 0.0575152691, 0.0598166570, 0.0619602779, + 0.0639444805, 0.0657690668, 0.0674525021, 0.0689664013, + 0.0703533073, 0.0715826364, 0.0726774642, 0.0736406005, + 0.0744664394, 0.0751576255, 0.0757305756, 0.0761748321, + 0.0765050718, 0.0767204924, 0.0768230011, 0.0768173975, + 0.0767093490, 0.0764992170, 0.0761992479, 0.0758008358, + 0.0753137336, 0.0747452558, 0.0741003642, 0.0733620255, + 0.0725682583, 0.0717002673, 0.0707628710, 0.0697630244, + 0.0687043828, 0.0676075985, 0.0664367512, 0.0652247106, + 0.0639715898, 0.0626857808, 0.0613455171, 0.0599837480, + 0.0585915683, 0.0571616450, 0.0557173648, 0.0542452768, + 0.0527630746, 0.0512556155, 0.0497385755, 0.0482165720, + 0.0466843027, 0.0451488405, 0.0436097542, 0.0420649094, + 0.0405349170, 0.0390053679, 0.0374812850, 0.0359697560, + 0.0344620948, 0.0329754081, 0.0315017608, 0.0300502657, + 0.0286072173, 0.0271859429, 0.0257875847, 0.0244160992, + 0.0230680169, 0.0217467550, 0.0204531793, 0.0191872431, + 0.0179433381, 0.0167324712, 0.0155405553, 0.0143904666, + -0.0132718220, -0.0121849995, -0.0111315548, -0.0101150215, + -0.0091325329, -0.0081798233, -0.0072615816, -0.0063792293, + -0.0055337211, -0.0047222596, -0.0039401124, -0.0031933778, + -0.0024826723, -0.0018039472, -0.0011568135, -0.0005464280, + 0.0000276045, 0.0005832264, 0.0010902329, 0.0015784682, + 0.0020274176, 0.0024508540, 0.0028446757, 0.0032091885, + 0.0035401246, 0.0038456408, 0.0041251642, 0.0043801861, + 0.0046039530, 0.0048109469, 0.0049839687, 0.0051382275, + 0.0052715758, 0.0053838975, 0.0054753783, 0.0055404363, + 0.0055917128, 0.0056266114, 0.0056389199, 0.0056455196, + 0.0056220643, 0.0055938023, 0.0055475714, 0.0054876040, + 0.0054196775, 0.0053471681, 0.0052461166, 0.0051407353, + 0.0050393022, 0.0049137603, 0.0047932560, 0.0046606460, + 0.0045209852, 0.0043730719, 0.0042264269, 0.0040819753, + 0.0039207432, 0.0037603922, 0.0036008268, 0.0034418874, + 0.0032739613, 0.0031125420, 0.0029469447, 0.0027870464, + 0.0026201758, 0.0024625616, 0.0023017254, 0.0021461583, + 0.0019841140, 0.0018348265, 0.0016868083, 0.0015443219, + 0.0013902494, 0.0012577884, 0.0011250155, 0.0009885988, + 0.0008608443, 0.0007458025, 0.0006239376, 0.0005107388, + 0.0004026540, 0.0002949531, 0.0002043017, 0.0001094383, + 0.0000134949, -0.0000617334, -0.0001446380, -0.0002098337, + -0.0002896981, -0.0003501175, -0.0004095121, -0.0004606325, + -0.0005145572, -0.0005564576, -0.0005946118, -0.0006341594, + -0.0006650415, -0.0006917937, -0.0007215391, -0.0007319357, + -0.0007530001, -0.0007630793, -0.0007757977, -0.0007801449, + -0.0007803664, -0.0007779869, -0.0007834332, -0.0007724848, + -0.0007681371, -0.0007490598, -0.0007440941, -0.0007255043, + -0.0007157736, -0.0006941614, -0.0006777690, -0.0006540333, + -0.0006312493, -0.0006132747, -0.0005870930, -0.0005677802, + -0.0005466565, -0.0005226564, -0.0005040714, -0.0004893791, + -0.0004875227, -0.0004947518, -0.0005617692, -0.000552528, +]; + +/// §4.6.18.4.1 / Figure 4.42 — the 32-band complex analysis QMF bank. +/// +/// One instance carries the 320-sample input history `x` of one +/// channel; [`AnalysisQmf::push_slot`] consumes the next 32 time-domain +/// samples and produces the 32 complex subband samples `W[k][l]` of one +/// QMF slot. +#[derive(Debug, Clone)] +pub struct AnalysisQmf { + /// The Figure 4.42 input history; a higher index is an older sample. + x: Vec, + /// Precomputed modulation matrix + /// `2·exp(i·π/64·(k + 0.5)·(2n − 0.5))`, row-major `[k][n]`. + m: Vec, +} + +impl Default for AnalysisQmf { + fn default() -> Self { + Self::new() + } +} + +impl AnalysisQmf { + /// A fresh analysis bank with an all-zero history. + #[must_use] + pub fn new() -> Self { + let mut m = Vec::with_capacity(32 * 64); + for k in 0..32 { + for n in 0..64 { + let arg = core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 0.5); + m.push(Complex::new(2.0 * arg.cos(), 2.0 * arg.sin())); + } + } + AnalysisQmf { + x: vec![0.0; 320], + m, + } + } + + /// Run one Figure 4.42 loop: shift in 32 new time samples (oldest + /// first within `samples`) and return the 32 complex subband + /// samples `W[k]` for this slot. + pub fn push_slot(&mut self, samples: &[f64]) -> Result<[Complex; 32]> { + if samples.len() != 32 { + return Err(Error::SbrQmfInvalid); + } + // Shift the history by 32 (discarding the oldest 32) and store + // the new samples in positions 0..=31. Figure 4.42 fills + // `x[31] .. x[0]` from consecutive input samples, so the newest + // input sample lands at index 0 (a higher index is older). + self.x.copy_within(0..288, 32); + for (n, s) in samples.iter().enumerate() { + self.x[31 - n] = *s; + } + // z[n] = x[n] · c[2n]; u[n] = Σ_{j=0..=4} z[n + 64j]. + let mut u = [0.0f64; 64]; + for (n, un) in u.iter_mut().enumerate() { + let mut acc = 0.0; + for j in 0..5 { + let idx = n + j * 64; + acc += self.x[idx] * QMF_WINDOW[2 * idx]; + } + *un = acc; + } + // W[k] = Σ_n u[n] · 2·exp(i·π/64·(k + 0.5)(2n − 0.5)). + let mut w = [Complex::default(); 32]; + for (k, wk) in w.iter_mut().enumerate() { + let row = &self.m[k * 64..(k + 1) * 64]; + let mut acc = Complex::default(); + for (n, cell) in row.iter().enumerate() { + acc += *cell * u[n]; + } + *wk = acc; + } + Ok(w) + } +} + +/// §4.6.18.4.2 / Figure 4.43 — the 64-band real-output synthesis QMF +/// bank (dual-rate SBR output). +#[derive(Debug, Clone)] +pub struct SynthesisQmf { + /// The Figure 4.43 synthesis history `v`. + v: Vec, + /// Precomputed `exp(i·π/128·(k + 0.5)·(2n − 255)) / 64`, row-major + /// `[n][k]` (transposed for the inner sum over `k`). + n_mat: Vec, +} + +impl Default for SynthesisQmf { + fn default() -> Self { + Self::new() + } +} + +impl SynthesisQmf { + /// A fresh synthesis bank with an all-zero history. + #[must_use] + pub fn new() -> Self { + let mut n_mat = Vec::with_capacity(128 * 64); + for n in 0..128 { + for k in 0..64 { + let arg = + core::f64::consts::PI / 128.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 255.0); + n_mat.push(Complex::new(arg.cos() / 64.0, arg.sin() / 64.0)); + } + } + SynthesisQmf { + v: vec![0.0; 1280], + n_mat, + } + } + + /// Run one Figure 4.43 loop: consume the 64 complex subband samples + /// `X[k]` of one slot and return the 64 real output samples. + pub fn push_slot(&mut self, bands: &[Complex]) -> Result<[f64; 64]> { + if bands.len() != 64 { + return Err(Error::SbrQmfInvalid); + } + // Shift v by 128 (discard the oldest 128 samples). + self.v.copy_within(0..1152, 128); + // v[n] = Σ_k Real(X[k]/64 · exp(i·π/128·(k + 0.5)(2n − 255))). + for n in 0..128 { + let row = &self.n_mat[n * 64..(n + 1) * 64]; + let mut acc = 0.0; + for (k, cell) in row.iter().enumerate() { + let x = bands[k]; + acc += x.re * cell.re - x.im * cell.im; + } + self.v[n] = acc; + } + // Extract g from v, window by c, and sum the ten taps. + let mut out = [0.0f64; 64]; + for (k, o) in out.iter_mut().enumerate() { + let mut acc = 0.0; + for n in 0..5 { + // g[128n + k] = v[256n + k]; w = g·c. + acc += self.v[256 * n + k] * QMF_WINDOW[128 * n + k]; + // g[128n + 64 + k] = v[256n + 192 + k]. + acc += self.v[256 * n + 192 + k] * QMF_WINDOW[128 * n + 64 + k]; + } + *o = acc; + } + Ok(out) + } +} + +/// §4.6.18.4.3 / Figure 4.44 — the 32-channel downsampled synthesis QMF +/// bank (output at the core rate). +#[derive(Debug, Clone)] +pub struct DownsampledSynthesisQmf { + /// The Figure 4.44 synthesis history `v`. + v: Vec, + /// Precomputed `exp(i·π/64·(k + 0.5)·(2n − 127.5)) / 64`, row-major + /// `[n][k]`. + n_mat: Vec, +} + +impl Default for DownsampledSynthesisQmf { + fn default() -> Self { + Self::new() + } +} + +impl DownsampledSynthesisQmf { + /// A fresh downsampled synthesis bank with an all-zero history. + #[must_use] + pub fn new() -> Self { + let mut n_mat = Vec::with_capacity(64 * 32); + for n in 0..64 { + for k in 0..32 { + let arg = + core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 127.5); + n_mat.push(Complex::new(arg.cos() / 64.0, arg.sin() / 64.0)); + } + } + DownsampledSynthesisQmf { + v: vec![0.0; 640], + n_mat, + } + } + + /// Run one Figure 4.44 loop: consume the 32 complex subband samples + /// `X[k]` of one slot and return the 32 real output samples. + pub fn push_slot(&mut self, bands: &[Complex]) -> Result<[f64; 32]> { + if bands.len() != 32 { + return Err(Error::SbrQmfInvalid); + } + // Shift v by 64 (discard the oldest 64 samples). + self.v.copy_within(0..576, 64); + // v[n] = Σ_k Real(X[k]/64 · exp(i·π/64·(k + 0.5)(2n − 127.5))). + for n in 0..64 { + let row = &self.n_mat[n * 32..(n + 1) * 32]; + let mut acc = 0.0; + for (k, cell) in row.iter().enumerate() { + let x = bands[k]; + acc += x.re * cell.re - x.im * cell.im; + } + self.v[n] = acc; + } + // g extraction, every-other-coefficient windowing, ten-tap sum. + let mut out = [0.0f64; 32]; + for (k, o) in out.iter_mut().enumerate() { + let mut acc = 0.0; + for n in 0..5 { + // g[64n + k] = v[128n + k]; w[n] = g[n]·c[2n]. + acc += self.v[128 * n + k] * QMF_WINDOW[2 * (64 * n + k)]; + // g[64n + 32 + k] = v[128n + 96 + k]. + acc += self.v[128 * n + 96 + k] * QMF_WINDOW[2 * (64 * n + 32 + k)]; + } + *o = acc; + } + Ok(out) + } +} + +/// §4.6.18.8.2.2 / Figure 4.50 — the 32-band real-valued analysis QMF +/// bank of the low-power SBR tool (critically sampled). +#[derive(Debug, Clone)] +pub struct RealAnalysisQmf { + /// The Figure 4.50 input history; a higher index is an older sample. + x: Vec, + /// Precomputed modulation matrix + /// `2·cos(π/64·(k + 0.5)·(2n − 96))`, row-major `[k][n]`. + m: Vec, +} + +impl Default for RealAnalysisQmf { + fn default() -> Self { + Self::new() + } +} + +impl RealAnalysisQmf { + /// A fresh real-valued analysis bank with an all-zero history. + #[must_use] + pub fn new() -> Self { + let mut m = Vec::with_capacity(32 * 64); + for k in 0..32 { + for n in 0..64 { + let arg = core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 96.0); + m.push(2.0 * arg.cos()); + } + } + RealAnalysisQmf { + x: vec![0.0; 320], + m, + } + } + + /// Run one Figure 4.50 loop: shift in 32 new time samples (oldest + /// first within `samples`) and return the 32 real subband samples + /// `W[k]` for this slot. + pub fn push_slot(&mut self, samples: &[f64]) -> Result<[f64; 32]> { + if samples.len() != 32 { + return Err(Error::SbrQmfInvalid); + } + // As Figure 4.42: newest input sample lands at index 0. + self.x.copy_within(0..288, 32); + for (n, s) in samples.iter().enumerate() { + self.x[31 - n] = *s; + } + // z[n] = x[n] · c[2n]; u[n] = Σ_{j=0..=4} z[n + 64j]. + let mut u = [0.0f64; 64]; + for (n, un) in u.iter_mut().enumerate() { + let mut acc = 0.0; + for j in 0..5 { + let idx = n + j * 64; + acc += self.x[idx] * QMF_WINDOW[2 * idx]; + } + *un = acc; + } + // W[k] = Σ_n u[n] · 2·cos(π/64·(k + 0.5)(2n − 96)). + let mut w = [0.0f64; 32]; + for (k, wk) in w.iter_mut().enumerate() { + let row = &self.m[k * 64..(k + 1) * 64]; + let mut acc = 0.0; + for (n, cell) in row.iter().enumerate() { + acc += *cell * u[n]; + } + *wk = acc; + } + Ok(w) + } +} + +/// §4.6.18.8.2.3 / Figure 4.51 — the 64-subband real-valued synthesis +/// QMF bank (dual-rate low-power SBR output). +#[derive(Debug, Clone)] +pub struct RealSynthesisQmf { + /// The Figure 4.51 synthesis history `v`. + v: Vec, + /// Precomputed `cos(π/128·(k + 0.5)·(2n − 64)) / 32`, row-major + /// `[n][k]`. + n_mat: Vec, +} + +impl Default for RealSynthesisQmf { + fn default() -> Self { + Self::new() + } +} + +impl RealSynthesisQmf { + /// A fresh real synthesis bank with an all-zero history. + #[must_use] + pub fn new() -> Self { + let mut n_mat = Vec::with_capacity(128 * 64); + for n in 0..128 { + for k in 0..64 { + let arg = + core::f64::consts::PI / 128.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 64.0); + n_mat.push(arg.cos() / 32.0); + } + } + RealSynthesisQmf { + v: vec![0.0; 1280], + n_mat, + } + } + + /// Run one Figure 4.51 loop: consume the 64 real subband samples + /// `X[k]` of one slot and return the 64 real output samples. + pub fn push_slot(&mut self, bands: &[f64]) -> Result<[f64; 64]> { + if bands.len() != 64 { + return Err(Error::SbrQmfInvalid); + } + // Shift v by 128 (discard the oldest 128 samples). + self.v.copy_within(0..1152, 128); + // v[n] = Σ_k X[k]/32 · cos(π/128·(k + 0.5)(2n − 64)). + for n in 0..128 { + let row = &self.n_mat[n * 64..(n + 1) * 64]; + let mut acc = 0.0; + for (k, cell) in row.iter().enumerate() { + acc += bands[k] * *cell; + } + self.v[n] = acc; + } + // g extraction (as Figure 4.51), full-window multiply, ten-tap + // sum. + let mut out = [0.0f64; 64]; + for (k, o) in out.iter_mut().enumerate() { + let mut acc = 0.0; + for n in 0..5 { + // g[128n + k] = v[256n + k]; w = g·c. + acc += self.v[256 * n + k] * QMF_WINDOW[128 * n + k]; + // g[128n + 64 + k] = v[256n + 192 + k]. + acc += self.v[256 * n + 192 + k] * QMF_WINDOW[128 * n + 64 + k]; + } + *o = acc; + } + Ok(out) + } +} + +/// §4.6.18.8.2.4 / Figure 4.52 — the 32-channel downsampled real-valued +/// synthesis QMF bank (core-rate low-power SBR output). +#[derive(Debug, Clone)] +pub struct RealDownsampledSynthesisQmf { + /// The Figure 4.52 synthesis history `v`. + v: Vec, + /// Precomputed `cos(π/64·(k + 0.5)·(2n − 32)) / 32`, row-major + /// `[n][k]`. + n_mat: Vec, +} + +impl Default for RealDownsampledSynthesisQmf { + fn default() -> Self { + Self::new() + } +} + +impl RealDownsampledSynthesisQmf { + /// A fresh downsampled real synthesis bank with an all-zero history. + #[must_use] + pub fn new() -> Self { + let mut n_mat = Vec::with_capacity(64 * 32); + for n in 0..64 { + for k in 0..32 { + let arg = core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 32.0); + n_mat.push(arg.cos() / 32.0); + } + } + RealDownsampledSynthesisQmf { + v: vec![0.0; 640], + n_mat, + } + } + + /// Run one Figure 4.52 loop: consume the 32 real subband samples + /// `X[k]` of one slot and return the 32 real output samples. + pub fn push_slot(&mut self, bands: &[f64]) -> Result<[f64; 32]> { + if bands.len() != 32 { + return Err(Error::SbrQmfInvalid); + } + // Shift v by 64 (discard the oldest 64 samples). + self.v.copy_within(0..576, 64); + // v[n] = Σ_k X[k]/32 · cos(π/64·(k + 0.5)(2n − 32)). + for n in 0..64 { + let row = &self.n_mat[n * 32..(n + 1) * 32]; + let mut acc = 0.0; + for (k, cell) in row.iter().enumerate() { + acc += bands[k] * *cell; + } + self.v[n] = acc; + } + // g extraction (as Figure 4.52), every-other-coefficient + // windowing, ten-tap sum. + let mut out = [0.0f64; 32]; + for (k, o) in out.iter_mut().enumerate() { + let mut acc = 0.0; + for n in 0..5 { + // g[64n + k] = v[128n + k]; w[n] = g[n]·c[2n]. + acc += self.v[128 * n + k] * QMF_WINDOW[2 * (64 * n + k)]; + // g[64n + 32 + k] = v[128n + 96 + k]. + acc += self.v[128 * n + 96 + k] * QMF_WINDOW[2 * (64 * n + 32 + k)]; + } + *o = acc; + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Table 4.A.89 spot values, straight from the printed table. + #[test] + fn window_spot_values() { + assert_eq!(QMF_WINDOW[0], 0.0); + assert_eq!(QMF_WINDOW[1], -0.0005525286); + assert_eq!(QMF_WINDOW[128], 0.0132718220); + assert_eq!(QMF_WINDOW[320], 0.8537385600); + assert_eq!(QMF_WINDOW[512], -0.0132718220); + // The table prints c[639] with nine decimals. + assert_eq!(QMF_WINDOW[639], -0.000552528); + } + + /// The printed table mirrors around index 320: + /// `|c[i]| == |c[640 - i]|` for every interior index (the last + /// entry only to the table's own nine printed decimals). + #[test] + fn window_mirror_structure() { + for i in 1..320usize { + let a = QMF_WINDOW[i].abs(); + let b = QMF_WINDOW[640 - i].abs(); + assert!((a - b).abs() < 1e-9, "mirror mismatch at {i}: {a} vs {b}"); + } + } + + /// Silence in → silence out, and slot-length validation. + #[test] + fn analysis_silence_and_shape() { + let mut a = AnalysisQmf::new(); + assert!(matches!(a.push_slot(&[0.0; 16]), Err(Error::SbrQmfInvalid))); + for _ in 0..4 { + let w = a.push_slot(&[0.0; 32]).unwrap(); + assert!(w.iter().all(|c| c.re == 0.0 && c.im == 0.0)); + } + let mut s = SynthesisQmf::new(); + assert!(matches!( + s.push_slot(&[Complex::default(); 32]), + Err(Error::SbrQmfInvalid) + )); + let out = s.push_slot(&[Complex::default(); 64]).unwrap(); + assert!(out.iter().all(|&x| x == 0.0)); + } + + /// A pure low-frequency sine through analysis → 64-band synthesis + /// (upper 32 bands zero) reconstructs the 2×-upsampled sine to + /// within the filterbank's near-perfect-reconstruction bound. + #[test] + fn analysis_synthesis_upsamples_a_sine() { + let mut a = AnalysisQmf::new(); + let mut s = SynthesisQmf::new(); + let freq = 0.03; // cycles per input sample, well inside band 1 + let slots = 96; + let mut output = Vec::new(); + for slot in 0..slots { + let mut input = [0.0f64; 32]; + for (n, v) in input.iter_mut().enumerate() { + let t = (slot * 32 + n) as f64; + *v = (2.0 * core::f64::consts::PI * freq * t).sin(); + } + let w = a.push_slot(&input).unwrap(); + let mut x = [Complex::default(); 64]; + x[..32].copy_from_slice(&w); + output.extend_from_slice(&s.push_slot(&x).unwrap()); + } + // Search the analysis+synthesis delay (in output samples) by + // matching against the ideal upsampled sine, then measure the + // steady-state error. + let ideal = + |t: f64, delay: f64| (2.0 * core::f64::consts::PI * freq * (t - delay) / 2.0).sin(); + let mut best = (f64::INFINITY, 0usize); + for delay in 0..1200usize { + let mut err = 0.0; + let mut sig = 0.0; + for (t, &out) in output.iter().enumerate().skip(1400) { + let e = out - ideal(t as f64, delay as f64); + err += e * e; + sig += out * out; + } + let ratio = err / sig.max(1e-30); + if ratio < best.0 { + best = (ratio, delay); + } + } + assert!( + best.0 < 1e-4, + "reconstruction error ratio {} at delay {}", + best.0, + best.1 + ); + } + + /// The downsampled synthesis bank reconstructs the input at the + /// core rate (identity up to the filterbank delay). + #[test] + fn analysis_downsampled_synthesis_is_identity() { + let mut a = AnalysisQmf::new(); + let mut s = DownsampledSynthesisQmf::new(); + let freq = 0.04; + let slots = 96; + let mut input_all = Vec::new(); + let mut output = Vec::new(); + for slot in 0..slots { + let mut input = [0.0f64; 32]; + for (n, v) in input.iter_mut().enumerate() { + let t = (slot * 32 + n) as f64; + *v = (2.0 * core::f64::consts::PI * freq * t).sin() + + 0.5 * (2.0 * core::f64::consts::PI * 2.3 * freq * t).cos(); + } + input_all.extend_from_slice(&input); + let w = a.push_slot(&input).unwrap(); + output.extend_from_slice(&s.push_slot(&w).unwrap()); + } + let mut best = (f64::INFINITY, 0usize); + for delay in 0..640usize { + let mut err = 0.0; + let mut sig = 0.0; + for t in 800..output.len() { + if t < delay { + continue; + } + let e = output[t] - input_all[t - delay]; + err += e * e; + sig += output[t] * output[t]; + } + let ratio = err / sig.max(1e-30); + if ratio < best.0 { + best = (ratio, delay); + } + } + assert!( + best.0 < 1e-4, + "identity error ratio {} at delay {}", + best.0, + best.1 + ); + } + + /// The analysis bank is linear: analysis(a + b) == analysis(a) + + /// analysis(b) slot by slot. + #[test] + fn analysis_is_linear() { + let mut qa = AnalysisQmf::new(); + let mut qb = AnalysisQmf::new(); + let mut qs = AnalysisQmf::new(); + for slot in 0..8 { + let mut a = [0.0f64; 32]; + let mut b = [0.0f64; 32]; + let mut sum = [0.0f64; 32]; + for n in 0..32 { + let t = (slot * 32 + n) as f64; + a[n] = (0.11 * t).sin(); + b[n] = (0.031 * t + 1.0).cos(); + sum[n] = a[n] + b[n]; + } + let wa = qa.push_slot(&a).unwrap(); + let wb = qb.push_slot(&b).unwrap(); + let ws = qs.push_slot(&sum).unwrap(); + for k in 0..32 { + let d = ws[k] - (wa[k] + wb[k]); + assert!(d.norm_sqr() < 1e-18); + } + } + } + + /// The real-valued LP bank pair (§4.6.18.8.2.2 + §4.6.18.8.2.4) + /// reconstructs the input at the core rate: real-QMF aliasing + /// between adjacent subbands cancels in the matched synthesis. + #[test] + fn real_analysis_downsampled_synthesis_is_identity() { + let mut a = RealAnalysisQmf::new(); + let mut s = RealDownsampledSynthesisQmf::new(); + let freq = 0.037; + let slots = 96; + let mut input_all = Vec::new(); + let mut output = Vec::new(); + for slot in 0..slots { + let mut input = [0.0f64; 32]; + for (n, v) in input.iter_mut().enumerate() { + let t = (slot * 32 + n) as f64; + *v = (2.0 * core::f64::consts::PI * freq * t).sin() + + 0.5 * (2.0 * core::f64::consts::PI * 2.9 * freq * t).cos(); + } + input_all.extend_from_slice(&input); + let w = a.push_slot(&input).unwrap(); + output.extend_from_slice(&s.push_slot(&w).unwrap()); + } + let mut best = (f64::INFINITY, 0usize); + for delay in 0..640usize { + let mut err = 0.0; + let mut sig = 0.0; + for (t, &o) in output.iter().enumerate().skip(900) { + if t < delay { + continue; + } + let e = o - input_all[t - delay]; + err += e * e; + sig += o * o; + } + let ratio = err / sig.max(1e-30); + if ratio < best.0 { + best = (ratio, delay); + } + } + assert!( + best.0 < 1e-4, + "identity error ratio {} at delay {}", + best.0, + best.1 + ); + } + + /// Real analysis → 64-band real synthesis (top half zero) + /// reconstructs the 2×-upsampled input (§4.6.18.8.2.3). + #[test] + fn real_analysis_synthesis_upsamples_a_sine() { + let mut a = RealAnalysisQmf::new(); + let mut s = RealSynthesisQmf::new(); + let freq = 0.043; + let slots = 96; + let mut output = Vec::new(); + for slot in 0..slots { + let mut input = [0.0f64; 32]; + for (n, v) in input.iter_mut().enumerate() { + let t = (slot * 32 + n) as f64; + *v = (2.0 * core::f64::consts::PI * freq * t).sin(); + } + let w = a.push_slot(&input).unwrap(); + let mut x = [0.0f64; 64]; + x[..32].copy_from_slice(&w); + output.extend_from_slice(&s.push_slot(&x).unwrap()); + } + let ideal = + |t: f64, delay: f64| (2.0 * core::f64::consts::PI * freq * (t - delay) / 2.0).sin(); + let mut best = (f64::INFINITY, 0usize); + for delay in 0..1200usize { + let mut err = 0.0; + let mut sig = 0.0; + for (t, &out) in output.iter().enumerate().skip(1600) { + let e = out - ideal(t as f64, delay as f64); + err += e * e; + sig += out * out; + } + let ratio = err / sig.max(1e-30); + if ratio < best.0 { + best = (ratio, delay); + } + } + assert!( + best.0 < 1e-4, + "reconstruction error ratio {} at delay {}", + best.0, + best.1 + ); + } + + /// The real analysis output is the real part structure of the + /// complex bank only in aggregate — but silence and shape checks + /// hold exactly, and the bank is linear. + #[test] + fn real_banks_silence_shape_linearity() { + let mut a = RealAnalysisQmf::new(); + assert!(matches!(a.push_slot(&[0.0; 16]), Err(Error::SbrQmfInvalid))); + for _ in 0..4 { + let w = a.push_slot(&[0.0; 32]).unwrap(); + assert!(w.iter().all(|&c| c == 0.0)); + } + let mut s = RealSynthesisQmf::new(); + assert!(matches!(s.push_slot(&[0.0; 32]), Err(Error::SbrQmfInvalid))); + assert!(s.push_slot(&[0.0; 64]).unwrap().iter().all(|&x| x == 0.0)); + let mut d = RealDownsampledSynthesisQmf::new(); + assert!(matches!(d.push_slot(&[0.0; 64]), Err(Error::SbrQmfInvalid))); + assert!(d.push_slot(&[0.0; 32]).unwrap().iter().all(|&x| x == 0.0)); + + // Linearity. + let mut qa = RealAnalysisQmf::new(); + let mut qb = RealAnalysisQmf::new(); + let mut qs = RealAnalysisQmf::new(); + for slot in 0..8 { + let mut va = [0.0f64; 32]; + let mut vb = [0.0f64; 32]; + let mut sum = [0.0f64; 32]; + for n in 0..32 { + let t = (slot * 32 + n) as f64; + va[n] = (0.13 * t).sin(); + vb[n] = (0.029 * t + 0.4).cos(); + sum[n] = va[n] + vb[n]; + } + let wa = qa.push_slot(&va).unwrap(); + let wb = qb.push_slot(&vb).unwrap(); + let ws = qs.push_slot(&sum).unwrap(); + for k in 0..32 { + assert!((ws[k] - (wa[k] + wb[k])).abs() < 1e-9); + } + } + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_reconstruct.rs b/crates/vendor/oxideav-aac/src/sbr_reconstruct.rs new file mode 100644 index 00000000..b41e8970 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_reconstruct.rs @@ -0,0 +1,414 @@ +//! SBR envelope / noise-floor DPCM reconstruction — ISO/IEC 14496-3 +//! §4.6.18.3.5. +//! +//! [`crate::sbr_envelope`] yields the **raw** transmitted values +//! `bs_data_env` / `bs_data_noise`, which are delta-coded (the spec's +//! `E_Delta(k,l)`). This module inverts the §4.6.18.3.5 delta coding to +//! recover the quantized scalefactors `E_Q(k,l)` (and the noise-floor +//! `Q(k,l)`). +//! +//! The spec defines `E_Delta` in terms of `E_Q`; inverting: +//! +//! * **frequency direction** (`bs_df_env(l) == 0`): +//! - `E_Q(0,l) = bs_data_env(0,l) / δ` +//! - `E_Q(k,l) = E_Q(k-1,l) + bs_data_env(k,l) / δ`, `k ≥ 1` +//! * **time direction** (`bs_df_env(l) == 1`): +//! - `E_Q(k,l) = g_E(k,l) + bs_data_env(k,l) / δ` +//! +//! where `δ = 0.5` for the second channel of a coupled pair (so the +//! transmitted balance values carry a factor of 2 — i.e. they must be +//! even, per §4.6.18.3.6) and `δ = 1` otherwise. In the integer +//! quantized domain the divide-by-δ is a multiply-by-`1/δ` (× 2 for the +//! coupled second channel); the transmitted values are even there, so +//! the result stays integral. +//! +//! `g_E(k,l)` is the "previous envelope, same band" reference for a +//! time delta: +//! +//! * for `l ≥ 1` it is `E_Q(k, l-1)` of the *current* frame, +//! * for `l == 0` it is `E'_Q(k, L'_E − 1)` — the last envelope of the +//! *previous* frame. +//! +//! When the frequency resolution of the reference envelope differs from +//! the current envelope (`r(l) ≠ g(l)`), the band index must be +//! re-mapped between the high- and low-resolution band tables via the +//! `i(k)` relation: +//! +//! * `r(l) = 1, g(l) = 0` (current high, ref low): for current +//! high-band `k`, the reference low-band `i` satisfies +//! `fTableLow(i) ≤ fTableHigh(k) < fTableLow(i+1)`. +//! * `r(l) = 0, g(l) = 1` (current low, ref high): for current +//! low-band `k`, the reference high-band `i` satisfies +//! `fTableHigh(i) = fTableLow(k)`. +//! +//! Noise floors follow the identical scheme over `NQ` bands, except a +//! noise floor is always at the (single) noise-band resolution, so no +//! resolution remap is ever needed. + +use crate::sbr_envelope::{SbrEnvelopeData, SbrNoiseData}; +use crate::sbr_freq_bands::HiLoTables; +use crate::sbr_grid::{SbrDtdf, SbrGrid}; +use crate::{Error, Result}; + +/// Reconstructed quantized envelope scalefactors `E_Q(k,l)` for one +/// channel: one band vector per envelope. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvelopeScalefactors { + /// `E_Q[l][k]` — the quantized envelope scalefactor for envelope + /// `l`, band `k`. + pub eq: Vec>, + /// Per-envelope frequency-resolution flag `r(l)` (copied from the + /// grid) — needed by the next frame for a cross-frame time delta. + pub freq_res: Vec, +} + +/// Reconstructed quantized noise-floor scalefactors `Q(k,l)` for one +/// channel: one `NQ`-band vector per noise floor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoiseScalefactors { + /// `Q[l][k]` — the quantized noise-floor scalefactor for noise + /// floor `l`, band `k`. + pub q: Vec>, +} + +/// The `1/δ` integer multiplier: `2` for the coupled second channel, +/// `1` otherwise. +#[inline] +fn inv_delta(coupling: bool, ch: bool) -> i32 { + if coupling && ch { + 2 + } else { + 1 + } +} + +/// `i(k)` for `r(l) = 1, g(l) = 0` (current high-res band `k` → ref +/// low-res band): the largest `i` with `fTableLow(i) ≤ fTableHigh(k)`. +fn high_to_low(bands: &HiLoTables, k: usize) -> usize { + let target = bands.f_table_high[k]; + let mut i = 0usize; + while i + 1 < bands.f_table_low.len() && bands.f_table_low[i + 1] <= target { + i += 1; + } + i +} + +/// `i(k)` for `r(l) = 0, g(l) = 1` (current low-res band `k` → ref +/// high-res band): the `i` with `fTableHigh(i) = fTableLow(k)`. +fn low_to_high(bands: &HiLoTables, k: usize) -> usize { + let target = bands.f_table_low[k]; + bands + .f_table_high + .iter() + .position(|&v| v == target) + .unwrap_or(0) +} + +/// Map a reference-envelope band array `prev` (at resolution +/// `prev_high`) onto the current envelope's band `k` (at resolution +/// `cur_high`), per the §4.6.18.3.5 `i(k)` relation. +fn ref_band(bands: &HiLoTables, prev: &[i32], cur_high: bool, prev_high: bool, k: usize) -> i32 { + let idx = if cur_high == prev_high { + k + } else if cur_high { + // r=1, g=0 + high_to_low(bands, k) + } else { + // r=0, g=1 + low_to_high(bands, k) + }; + prev.get(idx).copied().unwrap_or(0) +} + +impl EnvelopeScalefactors { + /// Reconstruct `E_Q(k,l)` from the raw `bs_data_env`. + /// + /// `prev` is the previous frame's reconstructed envelopes (its last + /// envelope is `g_E` for an `l == 0` time delta); pass `None` for + /// the first frame after a reset (in which case a time-coded first + /// envelope is treated as if the reference were all-zero, which the + /// §4.6.18.3.5 reset rule forbids on the wire anyway). + pub fn reconstruct( + env: &SbrEnvelopeData, + grid: &SbrGrid, + dtdf: &SbrDtdf, + bands: &HiLoTables, + coupling: bool, + ch: bool, + prev: Option<&EnvelopeScalefactors>, + ) -> Result { + let inv = inv_delta(coupling, ch); + let mut eq: Vec> = Vec::with_capacity(grid.num_env); + + for l in 0..grid.num_env { + let cur_high = grid.freq_res[l]; + let n = if cur_high { + bands.n_high() + } else { + bands.n_low() + }; + let raw = &env.data[l]; + if raw.len() != n { + return Err(Error::SbrGridInvalid); + } + let mut row = vec![0i32; n]; + + if !dtdf.df_env[l] { + // Frequency direction. + row[0] = raw[0] * inv; + for k in 1..n { + row[k] = row[k - 1] + raw[k] * inv; + } + } else { + // Time direction: reference is the previous envelope of + // this frame (l-1), or the last envelope of the previous + // frame for l == 0. + let (prev_row, prev_high): (Vec, bool) = if l >= 1 { + (eq[l - 1].clone(), grid.freq_res[l - 1]) + } else if let Some(p) = prev { + let last = p.eq.len().saturating_sub(1); + ( + p.eq.get(last).cloned().unwrap_or_default(), + *p.freq_res.get(last).unwrap_or(&cur_high), + ) + } else { + (vec![0i32; n], cur_high) + }; + for k in 0..n { + let g = ref_band(bands, &prev_row, cur_high, prev_high, k); + row[k] = g + raw[k] * inv; + } + } + eq.push(row); + } + + Ok(EnvelopeScalefactors { + eq, + freq_res: grid.freq_res.clone(), + }) + } +} + +impl NoiseScalefactors { + /// Reconstruct `Q(k,l)` from the raw `bs_data_noise` over `NQ` + /// bands. Noise floors share one resolution, so there is no + /// `i(k)` remap. + pub fn reconstruct( + noise: &SbrNoiseData, + grid: &SbrGrid, + dtdf: &SbrDtdf, + num_noise_bands: usize, + coupling: bool, + ch: bool, + prev: Option<&NoiseScalefactors>, + ) -> Result { + let inv = inv_delta(coupling, ch); + let mut q: Vec> = Vec::with_capacity(grid.num_noise); + + for l in 0..grid.num_noise { + let raw = &noise.data[l]; + if raw.len() != num_noise_bands { + return Err(Error::SbrGridInvalid); + } + let mut row = vec![0i32; num_noise_bands]; + if !dtdf.df_noise[l] { + row[0] = raw[0] * inv; + for k in 1..num_noise_bands { + row[k] = row[k - 1] + raw[k] * inv; + } + } else { + let prev_row: Vec = if l >= 1 { + q[l - 1].clone() + } else if let Some(p) = prev { + p.q.last() + .cloned() + .unwrap_or_else(|| vec![0i32; num_noise_bands]) + } else { + vec![0i32; num_noise_bands] + }; + for k in 0..num_noise_bands { + let g = prev_row.get(k).copied().unwrap_or(0); + row[k] = g + raw[k] * inv; + } + } + q.push(row); + } + + Ok(NoiseScalefactors { q }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sbr_freq_bands::{k0, k2, master_table, HiLoTables}; + use crate::sbr_grid::FrameClass; + + fn bands_44100() -> HiLoTables { + let k0v = k0(88_200, 5).unwrap(); + let k2v = k2(88_200, 5, k0v).unwrap(); + let fm = master_table(k0v, k2v, 0, false).unwrap(); + HiLoTables::derive(&fm, 1, 2).unwrap() + } + + fn single_env_grid(high: bool) -> (SbrGrid, SbrDtdf) { + ( + SbrGrid { + frame_class: FrameClass::FixFix, + num_env: 1, + num_noise: 1, + freq_res: vec![high], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: false, + }, + SbrDtdf { + df_env: vec![false], + df_noise: vec![false], + }, + ) + } + + #[test] + fn freq_direction_accumulates() { + let bands = bands_44100(); + let (grid, dtdf) = single_env_grid(true); + let n = bands.n_high(); + // raw = [10, 1, 2, -1, ...] → cumulative sums. + let mut raw = vec![10i32]; + for k in 1..n { + raw.push(if k % 2 == 0 { 2 } else { -1 }); + } + let env = SbrEnvelopeData { + data: vec![raw.clone()], + }; + let rec = EnvelopeScalefactors::reconstruct(&env, &grid, &dtdf, &bands, false, false, None) + .unwrap(); + // Expected: cumulative sum. + let mut acc = 10; + assert_eq!(rec.eq[0][0], 10); + for (k, &delta) in raw.iter().enumerate().skip(1) { + acc += delta; + assert_eq!(rec.eq[0][k], acc); + } + } + + #[test] + fn coupled_second_channel_doubles_delta() { + let bands = bands_44100(); + let (grid, dtdf) = single_env_grid(true); + let n = bands.n_high(); + let mut raw = vec![4i32]; + raw.extend(std::iter::repeat_n(2, n - 1)); + let env = SbrEnvelopeData { data: vec![raw] }; + // coupling && ch → inv_delta = 2. + let rec = EnvelopeScalefactors::reconstruct(&env, &grid, &dtdf, &bands, true, true, None) + .unwrap(); + assert_eq!(rec.eq[0][0], 8); // 4 * 2 + assert_eq!(rec.eq[0][1], 12); // 8 + 2*2 + } + + #[test] + fn time_direction_uses_prev_envelope_in_frame() { + let bands = bands_44100(); + let n = bands.n_high(); + // Two high-res envelopes: env0 freq-coded, env1 time-coded. + let grid = SbrGrid { + frame_class: FrameClass::FixVar, + num_env: 2, + num_noise: 2, + freq_res: vec![true, true], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: false, + }; + let dtdf = SbrDtdf { + df_env: vec![false, true], // env1 is time-coded + df_noise: vec![false, false], + }; + let mut raw0 = vec![20i32]; // env0 start = 20, flat thereafter + raw0.extend(std::iter::repeat_n(0, n - 1)); + let raw1 = vec![1i32; n]; // env1 = env0 + 1 per band + let env = SbrEnvelopeData { + data: vec![raw0, raw1], + }; + let rec = EnvelopeScalefactors::reconstruct(&env, &grid, &dtdf, &bands, false, false, None) + .unwrap(); + for k in 0..n { + assert_eq!(rec.eq[0][k], 20); + assert_eq!(rec.eq[1][k], 21); // 20 + 1 + } + } + + #[test] + fn time_direction_cross_frame() { + let bands = bands_44100(); + let n = bands.n_high(); + let (grid, _) = single_env_grid(true); + // Previous frame: a single high-res envelope all = 30. + let prev = EnvelopeScalefactors { + eq: vec![vec![30i32; n]], + freq_res: vec![true], + }; + // Current frame: single time-coded envelope, deltas all +2. + let dtdf = SbrDtdf { + df_env: vec![true], + df_noise: vec![false], + }; + let env = SbrEnvelopeData { + data: vec![vec![2i32; n]], + }; + let rec = EnvelopeScalefactors::reconstruct( + &env, + &grid, + &dtdf, + &bands, + false, + false, + Some(&prev), + ) + .unwrap(); + for k in 0..n { + assert_eq!(rec.eq[0][k], 32); // 30 + 2 + } + } + + #[test] + fn resolution_remap_high_to_low_is_monotone() { + // high_to_low must be non-decreasing and in-range for every + // high-res band. + let bands = bands_44100(); + let mut prev = 0usize; + for k in 0..=bands.n_high() { + let i = high_to_low(&bands, k); + assert!(i < bands.f_table_low.len()); + assert!(i >= prev); + prev = i; + } + } + + #[test] + fn noise_reconstruct_accumulates() { + let bands = bands_44100(); + let nq = bands.n_q(); + let (grid, dtdf) = single_env_grid(true); + let raw = (0..nq) + .map(|k| if k == 0 { 5 } else { 1 }) + .collect::>(); + let noise = SbrNoiseData { data: vec![raw] }; + let rec = + NoiseScalefactors::reconstruct(&noise, &grid, &dtdf, nq, false, false, None).unwrap(); + let mut acc = 5; + assert_eq!(rec.q[0][0], 5); + for k in 1..nq { + acc += 1; + assert_eq!(rec.q[0][k], acc); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/sbr_time_grid.rs b/crates/vendor/oxideav-aac/src/sbr_time_grid.rs new file mode 100644 index 00000000..de3140fd --- /dev/null +++ b/crates/vendor/oxideav-aac/src/sbr_time_grid.rs @@ -0,0 +1,340 @@ +//! SBR time / frequency grid derivation — ISO/IEC 14496-3 §4.6.18.3.3. +//! +//! Turns a parsed [`crate::sbr_grid::SbrGrid`] into the envelope and +//! noise-floor time border vectors `tE(l)` / `tQ(l)` (in SBR time +//! slots) plus the `lA` "transient envelope" index of Table 4.176: +//! +//! * `absBordLead` / `absBordTrail` — the leading / trailing SBR frame +//! borders per frame class (`bs_var_bord_*` offsets for the variable +//! sides). +//! * `nRelLead` / `nRelTrail` and the relative-border vectors — +//! `NINT(numTimeSlots / LE)` uniform spacing for FIXFIX, the +//! reconstructed `2·bs_rel_bord + 2` values for the variable sides. +//! * `tQ` — one or two noise floors, the two-floor split at +//! `tE(middleBorder)` with `middleBorder` from Table 4.174. +//! * `lA` — Table 4.176 (`-1` when no transient envelope is +//! signalled), consumed by the §4.6.18.7.5 gain calculation. +//! +//! ## Provenance +//! +//! Every branch below is from the §4.6.18.3.3 text and Tables 4.174 / +//! 4.176 of the staged spec. No part of this implementation is derived +//! from any external decoder. + +use crate::sbr_grid::{FrameClass, SbrGrid}; +use crate::{Error, Result}; + +/// The derived §4.6.18.3.3 time grid for one channel's SBR frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimeGrid { + /// `tE(0..=LE)` — envelope time borders in SBR time slots. The + /// start border of segment `l` is inclusive, the stop border + /// exclusive. + pub t_e: Vec, + /// `tQ(0..=LQ)` — noise-floor time borders (a subset of `t_e`). + pub t_q: Vec, + /// `lA` per Table 4.176: the envelope index where a newly started + /// sinusoid begins (and where the §4.6.18.7.5 `δ(l)` noise gate + /// opens); `-1` when none is signalled. + pub l_a: i32, +} + +/// Derive the §4.6.18.3.3 time grid from a parsed `sbr_grid()`. +/// +/// `num_time_slots` is the §4.6.18.2.6 `numTimeSlots` (16 for the +/// 1024-sample core frame this crate decodes). Border vectors that are +/// not strictly increasing, or that leave the +/// `[0, num_time_slots + 8]` range, are rejected with +/// [`Error::SbrGridInvalid`] (a malformed variable-border grid). +pub fn derive_time_grid(grid: &SbrGrid, num_time_slots: i32) -> Result { + let le = grid.num_env; + if le == 0 { + return Err(Error::SbrGridInvalid); + } + + // Leading / trailing absolute borders. + let abs_bord_lead = match grid.frame_class { + FrameClass::FixFix | FrameClass::FixVar => 0, + FrameClass::VarFix | FrameClass::VarVar => i32::from(grid.var_bord_0), + }; + let abs_bord_trail = match grid.frame_class { + FrameClass::FixFix | FrameClass::VarFix => num_time_slots, + FrameClass::FixVar | FrameClass::VarVar => i32::from(grid.var_bord_1) + num_time_slots, + }; + + // Relative-border counts. + let n_rel_lead = match grid.frame_class { + FrameClass::FixFix => le - 1, + FrameClass::FixVar => 0, + FrameClass::VarFix | FrameClass::VarVar => grid.rel_bord_0.len(), + }; + let n_rel_trail = match grid.frame_class { + FrameClass::FixFix | FrameClass::VarFix => 0, + FrameClass::FixVar | FrameClass::VarVar => grid.rel_bord_1.len(), + }; + if n_rel_lead + n_rel_trail + 1 != le { + return Err(Error::SbrGridInvalid); + } + + // relBordLead(l): FIXFIX splits the frame uniformly with + // NINT(numTimeSlots / LE); the variable classes carry + // 2·bs_rel_bord_0 + 2. + let rel_lead = |l: usize| -> i32 { + match grid.frame_class { + FrameClass::FixFix => nint_ratio(num_time_slots, le as i32), + _ => 2 * i32::from(grid.rel_bord_0[l]) + 2, + } + }; + // relBordTrail(l): 2·bs_rel_bord_1 + 2. + let rel_trail = |l: usize| -> i32 { 2 * i32::from(grid.rel_bord_1[l]) + 2 }; + + // tE(l). + let mut t_e = Vec::with_capacity(le + 1); + for l in 0..=le { + let border = if l == 0 { + abs_bord_lead + } else if l == le { + abs_bord_trail + } else if l <= n_rel_lead { + let mut b = abs_bord_lead; + for i in 0..l { + b += rel_lead(i); + } + b + } else { + let mut b = abs_bord_trail; + for i in 0..(le - l) { + b -= rel_trail(i); + } + b + }; + t_e.push(border); + } + + // §4.6.18.3.3 border sanity: strictly increasing, within the + // addressable slot range (the XLow / XHigh buffers extend + // tHFGen = 8 slots past the frame). + for w in t_e.windows(2) { + if w[1] <= w[0] { + return Err(Error::SbrGridInvalid); + } + } + if t_e[0] < 0 || t_e[le] > num_time_slots + 8 { + return Err(Error::SbrGridInvalid); + } + + // tQ: one floor spans the frame; two floors split at + // tE(middleBorder) (Table 4.174). + let t_q = if le == 1 { + vec![t_e[0], t_e[1]] + } else { + let middle = middle_border(grid.frame_class, grid.pointer, le)?; + if middle == 0 || middle >= le { + return Err(Error::SbrGridInvalid); + } + vec![t_e[0], t_e[middle], t_e[le]] + }; + if grid.num_noise != t_q.len() - 1 { + return Err(Error::SbrGridInvalid); + } + + // lA (Table 4.176). + let l_a = match grid.frame_class { + FrameClass::FixFix => -1, + FrameClass::FixVar | FrameClass::VarVar => { + if grid.pointer == 0 { + -1 + } else { + le as i32 + 1 - grid.pointer as i32 + } + } + FrameClass::VarFix => { + if grid.pointer > 1 { + grid.pointer as i32 - 1 + } else { + -1 + } + } + }; + + Ok(TimeGrid { t_e, t_q, l_a }) +} + +/// Table 4.174 — the `middleBorder` envelope index that splits the two +/// noise floors. +fn middle_border(class: FrameClass, pointer: u32, le: usize) -> Result { + let le_i = le as i32; + let v = match class { + FrameClass::FixFix => le_i / 2, + FrameClass::VarFix => match pointer { + 0 => 1, + 1 => le_i - 1, + _ => pointer as i32 - 1, + }, + FrameClass::FixVar | FrameClass::VarVar => match pointer { + 0 | 1 => le_i - 1, + _ => le_i + 1 - pointer as i32, + }, + }; + if v < 0 { + return Err(Error::SbrGridInvalid); + } + Ok(v as usize) +} + +/// §1.3 `NINT()` of the ratio `num / den` (round half away from zero; +/// both operands are positive here). +#[inline] +fn nint_ratio(num: i32, den: i32) -> i32 { + (2 * num + den) / (2 * den) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixfix(num_env: usize) -> SbrGrid { + SbrGrid { + frame_class: FrameClass::FixFix, + num_env, + num_noise: if num_env > 1 { 2 } else { 1 }, + freq_res: vec![true; num_env], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![], + pointer: 0, + amp_res_override: num_env == 1, + } + } + + /// FIXFIX splits the frame uniformly: LE ∈ {1, 2, 4} over 16 slots. + #[test] + fn fixfix_uniform_borders() { + assert_eq!(derive_time_grid(&fixfix(1), 16).unwrap().t_e, vec![0, 16]); + assert_eq!( + derive_time_grid(&fixfix(2), 16).unwrap().t_e, + vec![0, 8, 16] + ); + assert_eq!( + derive_time_grid(&fixfix(4), 16).unwrap().t_e, + vec![0, 4, 8, 12, 16] + ); + } + + /// FIXFIX noise floors: LE = 1 has one floor over the frame; LE > 1 + /// splits at tE(LE/2); lA is always -1. + #[test] + fn fixfix_noise_floors_and_la() { + let g1 = derive_time_grid(&fixfix(1), 16).unwrap(); + assert_eq!(g1.t_q, vec![0, 16]); + assert_eq!(g1.l_a, -1); + let g4 = derive_time_grid(&fixfix(4), 16).unwrap(); + assert_eq!(g4.t_q, vec![0, 8, 16]); + assert_eq!(g4.l_a, -1); + } + + /// FIXVAR counts envelopes back from the variable trailing border. + #[test] + fn fixvar_borders_from_trail() { + let grid = SbrGrid { + frame_class: FrameClass::FixVar, + num_env: 2, + num_noise: 2, + freq_res: vec![true; 2], + var_bord_0: 0, + var_bord_1: 3, + rel_bord_0: vec![], + rel_bord_1: vec![1], // reconstructed 2·1 + 2 = 4 + pointer: 0, + amp_res_override: false, + }; + let g = derive_time_grid(&grid, 16).unwrap(); + // absBordTrail = 3 + 16 = 19; tE(1) = 19 - 4 = 15. + assert_eq!(g.t_e, vec![0, 15, 19]); + // middleBorder (pointer = 0) = LE - 1 = 1. + assert_eq!(g.t_q, vec![0, 15, 19]); + assert_eq!(g.l_a, -1); + // pointer = 1 → lA = LE + 1 - 1 = 2. + let g = derive_time_grid(&SbrGrid { pointer: 1, ..grid }, 16).unwrap(); + assert_eq!(g.l_a, 2); + } + + /// VARFIX counts envelopes forward from the variable leading + /// border; lA fires only for pointer > 1. + #[test] + fn varfix_borders_from_lead() { + let grid = SbrGrid { + frame_class: FrameClass::VarFix, + num_env: 2, + num_noise: 2, + freq_res: vec![false; 2], + var_bord_0: 2, + var_bord_1: 0, + rel_bord_0: vec![0], // reconstructed 2 + rel_bord_1: vec![], + pointer: 2, + amp_res_override: false, + }; + let g = derive_time_grid(&grid, 16).unwrap(); + assert_eq!(g.t_e, vec![2, 4, 16]); + // middleBorder (pointer = 2) = pointer - 1 = 1. + assert_eq!(g.t_q, vec![2, 4, 16]); + // lA = pointer - 1 = 1. + assert_eq!(g.l_a, 1); + let g = derive_time_grid(&SbrGrid { pointer: 1, ..grid }, 16).unwrap(); + assert_eq!(g.l_a, -1); + } + + /// VARVAR mixes both variable sides. + #[test] + fn varvar_mixed_borders() { + let grid = SbrGrid { + frame_class: FrameClass::VarVar, + num_env: 3, + num_noise: 2, + freq_res: vec![true; 3], + var_bord_0: 1, + var_bord_1: 2, + rel_bord_0: vec![2], // 6 + rel_bord_1: vec![3], // 8 + pointer: 0, + amp_res_override: false, + }; + let g = derive_time_grid(&grid, 16).unwrap(); + // lead: 1, 1+6 = 7; trail: 18, 18-8 = 10. + assert_eq!(g.t_e, vec![1, 7, 10, 18]); + // middleBorder (pointer = 0) = LE - 1 = 2 → tQ splits at 10. + assert_eq!(g.t_q, vec![1, 10, 18]); + } + + /// Non-monotonic borders are rejected. + #[test] + fn non_monotonic_borders_rejected() { + let grid = SbrGrid { + frame_class: FrameClass::FixVar, + num_env: 2, + num_noise: 2, + freq_res: vec![true; 2], + var_bord_0: 0, + var_bord_1: 0, + rel_bord_0: vec![], + rel_bord_1: vec![3], // tE(1) = 16 - 8 = 8 … fine + pointer: 0, + amp_res_override: false, + }; + assert!(derive_time_grid(&grid, 16).is_ok()); + let bad = SbrGrid { + var_bord_1: 0, + rel_bord_1: vec![3, 3, 3], + num_env: 4, + freq_res: vec![true; 4], + ..grid + }; + // tE = [0, 16-24, …] — not increasing. + assert!(matches!( + derive_time_grid(&bad, 16), + Err(Error::SbrGridInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/scalable.rs b/crates/vendor/oxideav-aac/src/scalable.rs new file mode 100644 index 00000000..667fb6cb --- /dev/null +++ b/crates/vendor/oxideav-aac/src/scalable.rs @@ -0,0 +1,1541 @@ +//! Scalable AAC — ISO/IEC 14496-3 §4.4.2.2 (Tables 4.13–4.18) syntax +//! and the §4.5.2.2 / §4.6.14.2 AAC-only layer-combination decode for +//! the AAC scalable (AOT 6) and ER AAC scalable (AOT 20) object types. +//! +//! ## Payload shape +//! +//! A scalable program is one `aac_scalable_main_element()` (ASME, +//! Table 4.13 — layer 0) plus up to seven +//! `aac_scalable_extension_element()`s (ASEE, Table 4.14 — layers +//! 1..8), each riding its own elementary stream / LATM layer. Every +//! element is a header followed by one `individual_channel_stream(1,1)` +//! per channel (the Table 4.50 `scale_flag == 1` form: no inline +//! `ics_info()`, no pulse / TNS / gain-control dispatch — see +//! [`IcsBody::parse_scale`]), a trailing `extension_payload()` loop and +//! `byte_alignment()`. +//! +//! * `aac_scalable_main_header()` (Table 4.15, the AAC-only branch — +//! `core_flag == 0`, `tvq_layer_present == 0`): `ics_reserved_bit`, +//! `window_sequence`, `window_shape`, `max_sfb` (+ +//! `scale_factor_grouping` on `EIGHT_SHORT_SEQUENCE`), the stereo +//! `ms_mask_present` / `ms_data()`, then per channel +//! `tns_data_present` / `tns_data()` and `ltp_data_present` / +//! `ltp_data()`. +//! * `aac_scalable_extension_header()` (Table 4.16): `max_sfb`, the +//! stereo `ms_mask_present` / `ms_data()` (Table 4.60 — transmitted +//! for the **additional** bands `last_max_sfb_ms..max_sfb` only, +//! §4.6.8.1.4), per-channel `tns_data_present` / `tns_data()` on the +//! *first stereo layer after mono layers* only (`mono_stereo_flag`, +//! §4.6.9.5), and per-channel `diff_control_data_lr()` (Table 4.18) +//! on every stereo layer of a mixed mono/stereo configuration. +//! +//! ## Layer combination (§4.5.2.2.4, Figure 4.4) +//! +//! The Scalable Inverse AAC Quantization module (SIAQ) adds the +//! dequantized spectra of all layers per output path; the per-band +//! tool interactions follow Tables 4.91–4.93: +//! +//! * mono→mono / stereo→stereo plain bands: **sum**; +//! * PNS bands: a lower layer's noise band survives only while every +//! higher layer decodes the band to all-zero (§4.6.13.6); a higher +//! layer's PNS **replaces** a lower PNS band; PNS on top of real +//! coefficients (and vice versa within a channel pair) is invalid; +//! * intensity bands: only the left/mid channel accumulates across +//! IS→IS layers, positions come from the highest layer; IS over a +//! plain stereo band (or plain over IS) replaces the band with the +//! highest layer's content per Table 4.92; +//! * at the mono→stereo transition the combined mono spectrum `M''` +//! enters M/S-coded bands as `M = M'' + M'` and L/R-coded bands via +//! the §4.6.14.2 FSS: `L/R += 2·M''` where the per-channel +//! `diff_control_lr` bit is `0` (untransmitted bands default to +//! `1`); a mono PNS band never crosses the transition (Table 4.93). +//! +//! M/S (§4.6.8.1.4: one cumulative mask across layers), intensity +//! (§4.6.8.2.3 — `invert_intensity() = +1` for the scalable AOT) and +//! PNS (§4.6.13.6 — `ms_used` still signals noise correlation) are +//! then applied on the combined spectra, followed by the §4.6.9.5 +//! serial TNS layout (Table 4.158: the first mono layer's filter data +//! serves the `M` region up to the highest mono `max_sfb`, the first +//! stereo layer's filters serve L / R; an L/R filter reaching below +//! the mono boundary overrides the M filter) and the §4.6.11 +//! filterbank. +//! +//! §4.6.7.5 LTP: prediction runs only on the lowest GA layer, its +//! reconstruction history is the time-domain output of the first +//! layer decoded **alone** — the driver keeps a parallel base-layer +//! synthesis chain for exactly that; intensity / PNS bands of the +//! base layer take precedence over prediction (§4.6.7.5 / §4.6.7.4.2). +//! +//! CELP-core (`dependsOnCoreCoder == 1`) and TwinVQ lower layers are +//! other subparts' codecs and are rejected +//! ([`Error::ScalableUnsupportedCore`]). + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::asc::AacResilienceFlags; +use crate::decoded_spectrum::quant_to_spec; +use crate::dequant::rescale_spectrum; +use crate::extension_payload::ExtensionPayload; +use crate::filterbank::Filterbank; +use crate::ics_body::IcsBody; +use crate::ics_info::{ + derive_window_grouping_family, parse_ltp_data, write_ltp_data, IcsInfo, LtpData, + WindowSequence, WindowShape, +}; +use crate::intensity_stereo::{apply_intensity_stereo, IntensityPairSpectra}; +use crate::ltp::LtpState; +use crate::ms_stereo::{apply_ms_stereo, ChannelPairSpectra, MsMaskPresent}; +use crate::pns::{apply_pns, apply_pns_pair, gen_rand_vector, PnsChannel}; +use crate::scale_factor_data::{accumulate, AbsoluteScaleFactors}; +use crate::section_data::{INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB}; +use crate::spectral_data::SpectralData; +use crate::swb_offset::FrameFamily; +use crate::tns_data::TnsData; +use crate::tns_frame::{tns_analysis_frame_ics, tns_decode_frame_ics}; +use crate::{Error, Result}; + +/// Maximum number of coding layers (§4.5.2.2.4: one AAC main layer +/// plus up to 7 AAC extension layers). +pub const MAX_LAYERS: usize = 8; + +/// Static configuration of a scalable program, resolved from the +/// per-layer `AudioSpecificConfig`s. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScalableConfig { + /// `audioObjectType` — 6 (AAC scalable) or 20 (ER AAC scalable). + pub aot: u8, + /// Table 1.18 `samplingFrequencyIndex` (all layers share it in + /// the AAC-only combinations — §4.5.2.2.4 runs one filterbank). + pub fs_index: u8, + /// Resolved sampling rate in Hz. + pub sample_rate: u32, + /// §4.5.1.1 frame-length family — `Lc1024` or `Lc960` + /// (`frameLengthFlag`); the LD families are not scalable shapes. + pub family: FrameFamily, + /// The ASC resilience triplet for AOT 20; all-false for AOT 6. + pub resilience: AacResilienceFlags, + /// `this_layer_stereo` per layer, in layer order (§4.5.2.2.1.1). + /// Derived from each layer's `channelConfiguration` (1 or 2). + pub layer_stereo: Vec, +} + +impl ScalableConfig { + /// Validate the §4.5.2.2 shape: 1..=8 layers, no mono layer after + /// a stereo layer (Table 4.87), a non-LD family, a scalable AOT. + pub fn validate(&self) -> Result<()> { + if self.aot != 6 && self.aot != 20 { + return Err(Error::ScalableInvalid); + } + if self.layer_stereo.is_empty() || self.layer_stereo.len() > MAX_LAYERS { + return Err(Error::ScalableInvalid); + } + if self.family.is_ld() { + return Err(Error::ScalableInvalid); + } + // Table 4.87: AAC mono may feed mono or stereo; AAC stereo + // feeds stereo only. + let mut seen_stereo = false; + for &s in &self.layer_stereo { + if seen_stereo && !s { + return Err(Error::ScalableInvalid); + } + seen_stereo |= s; + } + Ok(()) + } + + /// `mono_layer_flag` (§4.5.2.2.1.1): any mono layer present. + pub fn mono_layer_flag(&self) -> bool { + self.layer_stereo.iter().any(|&s| !s) + } + + /// Index of the first stereo layer, if any. + pub fn first_stereo_layer(&self) -> Option { + self.layer_stereo.iter().position(|&s| s) + } + + /// `mono_stereo_flag` for layer `lay` (§4.5.2.2.1.1): at least one + /// mono layer exists and `lay` is the first stereo layer. + pub fn mono_stereo_flag(&self, lay: usize) -> bool { + self.mono_layer_flag() && self.first_stereo_layer() == Some(lay) + } + + /// Build a [`ScalableConfig`] from the per-layer + /// `AudioSpecificConfig`s of a LATM program (§1.7.3: one layer per + /// `streamID[prog][lay]`, in layer order). + /// + /// Shape rules enforced here: every layer carries the same + /// scalable AOT (6 / 20), the same `samplingFrequencyIndex` and + /// the same `frameLengthFlag`; each `channelConfiguration` is 1 + /// (mono) or 2 (stereo); a `dependsOnCoreCoder == 1` layer (CELP + /// core, §4.5.2.2.5) is rejected with + /// [`Error::ScalableUnsupportedCore`]; the AOT-20 resilience + /// triplet comes from the first layer and must match on every + /// layer. + pub fn from_layer_ascs(ascs: &[&crate::asc::AudioSpecificConfig]) -> Result { + let first = ascs.first().ok_or(Error::ScalableInvalid)?; + if first.aot != 6 && first.aot != 20 { + return Err(Error::ScalableInvalid); + } + let family = FrameFamily::from_aot_and_flag( + first.aot, + first.ga_body.frame_length == crate::asc::FrameLength::Long960, + ); + let resilience = |asc: &crate::asc::AudioSpecificConfig| { + asc.ga_body + .extension_body + .as_ref() + .and_then(|ext| ext.resilience) + .unwrap_or_default() + }; + let res0 = resilience(first); + let mut layer_stereo = Vec::with_capacity(ascs.len()); + for asc in ascs { + if asc.aot != first.aot + || asc.sampling_frequency_index != first.sampling_frequency_index + || asc.ga_body.frame_length != first.ga_body.frame_length + || resilience(asc) != res0 + { + return Err(Error::ScalableInvalid); + } + if asc.ga_body.depends_on_core_coder { + return Err(Error::ScalableUnsupportedCore); + } + layer_stereo.push(match asc.channel_configuration { + 1 => false, + 2 => true, + _ => return Err(Error::ScalableInvalid), + }); + } + let cfg = ScalableConfig { + aot: first.aot, + fs_index: first.sampling_frequency_index, + sample_rate: first.sample_rate, + family, + resilience: res0, + layer_stereo, + }; + cfg.validate()?; + Ok(cfg) + } + + /// Number of output channels (2 iff any layer is stereo). + pub fn output_channels(&self) -> usize { + if self.layer_stereo.iter().any(|&s| s) { + 2 + } else { + 1 + } + } + + fn channels_of_layer(&self, lay: usize) -> usize { + if self.layer_stereo[lay] { + 2 + } else { + 1 + } + } +} + +/// One channel of one layer: the `individual_channel_stream(1,1)` +/// body plus its decoded spectrum. +#[derive(Debug, Clone)] +pub struct ScalableChannel { + /// The Table 4.50 `scale_flag == 1` body + /// ([`IcsBody::parse_scale`]). + pub body: IcsBody, + /// The channel's quantized spectrum — from `spectral_data()` or, + /// for AOT 20 with `aacSpectralDataResilienceFlag`, from the + /// §4.6.16.3 `reordered_spectral_data()` payload. + pub spectral: SpectralData, +} + +/// One parsed layer of a scalable frame (main or extension element). +#[derive(Debug, Clone)] +pub struct ScalableLayer { + /// Per-layer geometry: the main header's `window_sequence` / + /// `window_shape` / grouping with **this layer's** `max_sfb`. + pub ics: IcsInfo, + /// `ms_mask_present` for a stereo layer ([`MsMaskPresent::AllZeros`] + /// for mono layers, whose headers carry no mask). + pub ms_mask_present: MsMaskPresent, + /// The layer's newly transmitted `ms_used` rows (Table 4.60): + /// `num_window_groups` rows covering `last_max_sfb_ms..max_sfb`. + /// Empty unless `ms_mask_present == 1`. + pub ms_used_new: Vec>, + /// Per-channel `tns_data()`; populated only on layers whose header + /// carries TNS bits (the main layer; the `mono_stereo_flag` + /// extension layer). + pub tns: Vec>, + /// Per-channel `ltp_data()` (main layer only, §4.6.7.5). + pub ltp: Vec>, + /// Per-channel long-window `diff_control_lr` bits in transmission + /// order (Table 4.18: bands `last_max_sfb_ms..min(last_mono_max_sfb, + /// max_sfb)` whose cumulative `ms_used` is clear). Empty when the + /// header carries none. + pub diff_lr_long: Vec>, + /// Per-channel short-window `diff_control_lr[win][0]` bits (first + /// stereo layer only). `None` when absent. + pub diff_lr_short: Vec>, + /// The per-channel ICS bodies + spectra. + pub channels: Vec, +} + +/// A fully parsed scalable frame: every layer element plus the +/// cumulative cross-layer tables. +#[derive(Debug, Clone)] +pub struct ScalableFrame { + /// The per-layer parsed elements, in layer order. + pub layers: Vec, + /// Cumulative `ms_used[g][sfb]` over `max_total_sfb` bands + /// (§4.6.8.1.4 — one mask across all layers, each layer + /// transmitting only its additional bands). + pub ms_used: Vec>, + /// Cumulative per-channel long-window `diff_control_lr[sfb]` + /// (§4.6.14.2.1; `None` = untransmitted = `1`). + pub diff_lr_long: [Vec>; 2], + /// Per-channel short-window `diff_control_lr[win][0]` (first + /// stereo layer; `None` when the frame is long-window or has no + /// stereo transition). + pub diff_lr_short: [Option<[bool; 8]>; 2], + /// Highest `max_sfb` across all layers. + pub max_total_sfb: u8, + /// Highest `max_sfb` across the mono layers (`0` when none). + pub max_mono_sfb: u8, +} + +impl ScalableFrame { + /// Parse one frame's per-layer payloads (one byte buffer per + /// layer, layer 0 first) into the element structures. + pub fn parse(cfg: &ScalableConfig, payloads: &[&[u8]]) -> Result { + cfg.validate()?; + if payloads.len() != cfg.layer_stereo.len() { + return Err(Error::ScalableInvalid); + } + + let mut layers: Vec = Vec::with_capacity(payloads.len()); + // Base geometry from the main header (window sequence / shape / + // grouping are frame-global; only max_sfb varies per layer). + let mut base_ics: Option = None; + let mut ms_used: Vec> = Vec::new(); + let mut diff_lr_long: [Vec>; 2] = [Vec::new(), Vec::new()]; + let mut diff_lr_short: [Option<[bool; 8]>; 2] = [None, None]; + let mut last_max_sfb_ms: u8 = 0; // previous *stereo* layer's max_sfb + let mut max_mono_sfb: u8 = 0; + let mut max_total_sfb: u8 = 0; + + for (lay, payload) in payloads.iter().enumerate() { + let stereo = cfg.layer_stereo[lay]; + let n_ch = cfg.channels_of_layer(lay); + let mut reader = BitReader::new(payload); + + let (ics, ms_mask_present, ms_used_new, tns, ltp, dl_long, dl_short); + if lay == 0 { + // ---- Table 4.15 aac_scalable_main_header() (AAC-only). + let ics_reserved_bit = read_bit(&mut reader)?; + let ws = WindowSequence::from_bits(read_u8(&mut reader, 2)?); + let shape = WindowShape::from_bit(read_bit(&mut reader)?); + let (msfb, sfg) = if ws.is_eight_short() { + let m = read_u8(&mut reader, 4)?; + let g = read_u8(&mut reader, 7)?; + (m, Some(g)) + } else { + (read_u8(&mut reader, 6)?, None) + }; + let (num_windows, num_window_groups, window_group_length, num_swb) = + derive_window_grouping_family(cfg.family, ws, sfg, cfg.fs_index)?; + let info = IcsInfo { + family: cfg.family, + ics_reserved_bit, + window_sequence: ws, + window_shape: shape, + max_sfb: msfb, + scale_factor_grouping: sfg, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows, + num_window_groups, + window_group_length, + num_swb, + }; + if msfb > num_swb { + return Err(Error::ScalableInvalid); + } + let groups = usize::from(num_window_groups); + ms_used = vec![Vec::new(); groups]; + + let (mask, new_rows) = if stereo { + parse_ms_data(&mut reader, groups, 0, msfb)? + } else { + (MsMaskPresent::AllZeros, Vec::new()) + }; + merge_ms_rows(&mut ms_used, mask, &new_rows, 0, msfb); + + // Note: `mono_stereo_flag` cannot be set on the main + // layer of an AAC-only configuration (a stereo main + // layer means no mono layer exists), so the + // `tns_channel_mono_layer` bit never occurs here. + let mut tns_v: Vec> = Vec::with_capacity(n_ch); + let mut ltp_v: Vec> = Vec::with_capacity(n_ch); + for _ch in 0..n_ch { + // Table 4.15 per-channel loop: TNS then (AAC-only + // branch) LTP. + if read_bit(&mut reader)? { + tns_v.push(Some(TnsData::parse(&mut reader, ws)?)); + } else { + tns_v.push(None); + } + if read_bit(&mut reader)? { + ltp_v.push(Some(parse_ltp_data(&mut reader, cfg.aot, ws, msfb)?)); + } else { + ltp_v.push(None); + } + } + ics = info; + ms_mask_present = mask; + ms_used_new = new_rows; + tns = tns_v; + ltp = ltp_v; + dl_long = Vec::new(); + dl_short = vec![None; n_ch]; + base_ics = Some(ics.clone()); + } else { + // ---- Table 4.16 aac_scalable_extension_header(). + let base = base_ics.as_ref().ok_or(Error::ScalableInvalid)?; + let ws = base.window_sequence; + let msfb = if ws.is_eight_short() { + read_u8(&mut reader, 4)? + } else { + read_u8(&mut reader, 6)? + }; + if msfb > base.num_swb { + return Err(Error::ScalableInvalid); + } + let groups = usize::from(base.num_window_groups); + let (mask, new_rows) = if stereo { + parse_ms_data(&mut reader, groups, last_max_sfb_ms, msfb)? + } else { + (MsMaskPresent::AllZeros, Vec::new()) + }; + merge_ms_rows(&mut ms_used, mask, &new_rows, last_max_sfb_ms, msfb); + + let tns_v: Vec> = if cfg.mono_stereo_flag(lay) { + let mut v = Vec::with_capacity(2); + for _ch in 0..2 { + if read_bit(&mut reader)? { + v.push(Some(TnsData::parse(&mut reader, ws)?)); + } else { + v.push(None); + } + } + v + } else { + vec![None; n_ch] + }; + + // Table 4.18 diff_control_data_lr(), one per channel. + let mut dl_long_v: Vec> = Vec::new(); + let mut dl_short_v: Vec> = vec![None; n_ch]; + if cfg.mono_layer_flag() && stereo { + for ch in 0..2usize { + if ws != WindowSequence::EightShort { + let hi = core::cmp::min(max_mono_sfb, msfb); + let mut bits = Vec::new(); + for sfb in last_max_sfb_ms..hi { + let on = ms_used + .first() + .and_then(|row| row.get(usize::from(sfb))) + .copied() + .unwrap_or(false); + if !on { + let b = read_bit(&mut reader)?; + bits.push(b); + if usize::from(sfb) >= diff_lr_long[ch].len() { + diff_lr_long[ch].resize(usize::from(sfb) + 1, None); + } + diff_lr_long[ch][usize::from(sfb)] = Some(b); + } + } + dl_long_v.push(bits); + } else { + dl_long_v.push(Vec::new()); + if last_max_sfb_ms == 0 { + // Only in the first stereo layer. + let mut w = [false; 8]; + for slot in w.iter_mut() { + *slot = read_bit(&mut reader)?; + } + dl_short_v[ch] = Some(w); + diff_lr_short[ch] = Some(w); + } + } + } + } + + let mut info = base.clone(); + info.max_sfb = msfb; + ics = info; + ms_mask_present = mask; + ms_used_new = new_rows; + tns = tns_v; + ltp = vec![None; n_ch]; + dl_long = dl_long_v; + dl_short = dl_short_v; + } + + // ---- Per-channel individual_channel_stream(1, 1). + let mut channels: Vec = Vec::with_capacity(n_ch); + for _ch in 0..n_ch { + let body = IcsBody::parse_scale(&mut reader, &ics, cfg.resilience)?; + let spectral = if cfg.resilience.spectral_data { + let (len_reordered, len_longest) = body + .reordered_spectral_lengths + .ok_or(Error::ScalableInvalid)?; + let len = crate::hcr::clamp_reordered_length(len_reordered, stereo); + let mut buf = vec![0u8; usize::from(len).div_ceil(8)]; + for i in 0..usize::from(len) { + if read_bit(&mut reader)? { + buf[i / 8] |= 0x80 >> (i % 8); + } + } + crate::hcr_decode::decode_reordered_spectral_data( + &buf, + len, + len_longest, + &ics, + &body.section_data, + cfg.fs_index, + )? + } else { + SpectralData::parse(&mut reader, &ics, &body.section_data, cfg.fs_index)? + }; + channels.push(ScalableChannel { body, spectral }); + } + + // ---- Trailing extension_payload() loop + byte_alignment(). + let total_bits = (payload.len() as u64) * 8; + let mut cnt = (total_bits.saturating_sub(reader.bit_position())) / 8; + while cnt >= 1 { + let p = ExtensionPayload::parse(&mut reader, cnt as u32)?; + let used = u64::from(p.byte_length()); + if used == 0 || used > cnt { + return Err(Error::ScalableInvalid); + } + cnt -= used; + } + if reader.bit_position() > total_bits { + return Err(Error::ScalableInvalid); + } + + // ---- Cumulative bookkeeping. + if stereo { + last_max_sfb_ms = ics.max_sfb; + } else { + max_mono_sfb = core::cmp::max(max_mono_sfb, ics.max_sfb); + } + max_total_sfb = core::cmp::max(max_total_sfb, ics.max_sfb); + + layers.push(ScalableLayer { + ics, + ms_mask_present, + ms_used_new, + tns, + ltp, + diff_lr_long: dl_long, + diff_lr_short: dl_short, + channels, + }); + } + + // Pad the cumulative mask rows to max_total_sfb. + for row in &mut ms_used { + if row.len() < usize::from(max_total_sfb) { + row.resize(usize::from(max_total_sfb), false); + } + } + + Ok(ScalableFrame { + layers, + ms_used, + diff_lr_long, + diff_lr_short, + max_total_sfb, + max_mono_sfb, + }) + } + + /// Re-emit the frame as one byte-aligned payload per layer — the + /// bit-exact inverse of [`ScalableFrame::parse`] (no trailing + /// extension payloads are emitted). + pub fn write(&self, cfg: &ScalableConfig) -> Result>> { + cfg.validate()?; + if self.layers.len() != cfg.layer_stereo.len() { + return Err(Error::ScalableInvalid); + } + let mut out = Vec::with_capacity(self.layers.len()); + let mut last_max_sfb_ms: u8 = 0; + let mut max_mono_sfb: u8 = 0; + for (lay, layer) in self.layers.iter().enumerate() { + let stereo = cfg.layer_stereo[lay]; + let n_ch = cfg.channels_of_layer(lay); + if layer.channels.len() != n_ch { + return Err(Error::ScalableInvalid); + } + let mut w = BitWriter::new(); + let ics = &layer.ics; + if lay == 0 { + w.write_bit(ics.ics_reserved_bit); + w.write_u32(u32::from(ics.window_sequence as u8), 2); + w.write_bit(matches!(ics.window_shape, WindowShape::Kbd)); + if ics.window_sequence.is_eight_short() { + w.write_u32(u32::from(ics.max_sfb), 4); + w.write_u32( + u32::from(ics.scale_factor_grouping.ok_or(Error::ScalableInvalid)?), + 7, + ); + } else { + w.write_u32(u32::from(ics.max_sfb), 6); + } + if stereo { + write_ms_data( + &mut w, + layer.ms_mask_present, + &layer.ms_used_new, + 0, + ics.max_sfb, + )?; + } + for ch in 0..n_ch { + let tns = layer.tns.get(ch).ok_or(Error::ScalableInvalid)?; + w.write_bit(tns.is_some()); + if let Some(t) = tns { + t.write(&mut w, ics.window_sequence)?; + } + let ltp = layer.ltp.get(ch).ok_or(Error::ScalableInvalid)?; + w.write_bit(ltp.is_some()); + if let Some(l) = ltp { + write_ltp_data(&mut w, l, cfg.aot, ics.window_sequence, ics.max_sfb)?; + } + } + } else { + if ics.window_sequence.is_eight_short() { + w.write_u32(u32::from(ics.max_sfb), 4); + } else { + w.write_u32(u32::from(ics.max_sfb), 6); + } + if stereo { + write_ms_data( + &mut w, + layer.ms_mask_present, + &layer.ms_used_new, + last_max_sfb_ms, + ics.max_sfb, + )?; + } + if cfg.mono_stereo_flag(lay) { + for ch in 0..2usize { + let tns = layer.tns.get(ch).ok_or(Error::ScalableInvalid)?; + w.write_bit(tns.is_some()); + if let Some(t) = tns { + t.write(&mut w, ics.window_sequence)?; + } + } + } + if cfg.mono_layer_flag() && stereo { + for ch in 0..2usize { + if ics.window_sequence != WindowSequence::EightShort { + let bits = layer.diff_lr_long.get(ch).ok_or(Error::ScalableInvalid)?; + let mut it = bits.iter(); + let hi = core::cmp::min(max_mono_sfb, ics.max_sfb); + for sfb in last_max_sfb_ms..hi { + let on = self + .ms_used + .first() + .and_then(|row| row.get(usize::from(sfb))) + .copied() + .unwrap_or(false); + if !on { + w.write_bit(*it.next().ok_or(Error::ScalableInvalid)?); + } + } + if it.next().is_some() { + return Err(Error::ScalableInvalid); + } + } else if last_max_sfb_ms == 0 { + let bits = layer + .diff_lr_short + .get(ch) + .and_then(|b| *b) + .ok_or(Error::ScalableInvalid)?; + for b in bits { + w.write_bit(b); + } + } + } + } + } + + for chan in &layer.channels { + chan.body.write_scale(&mut w, ics, cfg.resilience)?; + if cfg.resilience.spectral_data { + let (buf, len, _longest) = crate::hcr_decode::encode_reordered_spectral_data( + &chan.spectral, + ics, + &chan.body.section_data, + cfg.fs_index, + )?; + // The body writer emitted the stored length fields; + // they must match the re-encoded payload. + let (stored_len, _stored_longest) = chan + .body + .reordered_spectral_lengths + .ok_or(Error::ScalableInvalid)?; + if stored_len != len { + return Err(Error::ScalableInvalid); + } + for i in 0..usize::from(len) { + w.write_bit(buf[i / 8] & (0x80 >> (i % 8)) != 0); + } + } else { + chan.spectral + .write(&mut w, ics, &chan.body.section_data, cfg.fs_index)?; + } + } + // byte_alignment() + let pos = w.bit_position(); + for _ in 0..((8 - (pos % 8)) % 8) { + w.write_bit(false); + } + out.push(w.finish()); + + if stereo { + last_max_sfb_ms = ics.max_sfb; + } else { + max_mono_sfb = core::cmp::max(max_mono_sfb, ics.max_sfb); + } + } + Ok(out) + } +} + +/// Parse a stereo layer's `ms_mask_present` + Table 4.60 `ms_data()` +/// covering bands `lo..hi` (the §4.6.8.1.4 incremental range). +fn parse_ms_data( + reader: &mut BitReader<'_>, + groups: usize, + lo: u8, + hi: u8, +) -> Result<(MsMaskPresent, Vec>)> { + let bits = read_u8(reader, 2)?; + // §4.6.8.1.2: `11` is reserved. + let mask = MsMaskPresent::from_bits(bits).map_err(|_| Error::ScalableInvalid)?; + let mut rows = Vec::new(); + if mask == MsMaskPresent::Mask { + for _g in 0..groups { + let mut row = Vec::new(); + for _sfb in lo..hi { + row.push(read_bit(reader)?); + } + rows.push(row); + } + } + Ok((mask, rows)) +} + +/// Emit `ms_mask_present` + the incremental `ms_data()` rows. +fn write_ms_data( + w: &mut BitWriter, + mask: MsMaskPresent, + rows: &[Vec], + lo: u8, + hi: u8, +) -> Result<()> { + w.write_u32(u32::from(mask.to_bits()), 2); + if mask == MsMaskPresent::Mask { + let span = usize::from(hi.saturating_sub(lo)); + for row in rows { + if row.len() != span { + return Err(Error::ScalableInvalid); + } + for &b in row { + w.write_bit(b); + } + } + } + Ok(()) +} + +/// Fold a layer's transmitted mask into the cumulative `ms_used` +/// (§4.6.8.1.4). `AllOnes` sets the whole incremental range; `Mask` +/// scatters the transmitted rows; `AllZeros` leaves the range clear. +fn merge_ms_rows( + ms_used: &mut [Vec], + mask: MsMaskPresent, + rows: &[Vec], + lo: u8, + hi: u8, +) { + for (g, row) in ms_used.iter_mut().enumerate() { + if row.len() < usize::from(hi) { + row.resize(usize::from(hi), false); + } + for sfb in lo..hi { + let v = match mask { + MsMaskPresent::AllZeros => false, + MsMaskPresent::AllOnes => true, + MsMaskPresent::Mask => rows + .get(g) + .and_then(|r| r.get(usize::from(sfb - lo))) + .copied() + .unwrap_or(false), + }; + if v { + row[usize::from(sfb)] = true; + } + } + } +} + +fn read_bit(reader: &mut BitReader<'_>) -> Result { + reader.read_bit().map_err(|_| Error::UnexpectedEnd) +} + +fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { + Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +// --------------------------------------------------------------------------- +// Layer combination + decode driver (§4.5.2.2.4 / §4.6.14.2 / §4.6.9.5) +// --------------------------------------------------------------------------- + +/// Per-band combination state across stereo layers (Tables 4.92/4.93). +#[derive(Debug, Clone, Copy, Default)] +struct BandState { + covered_l: bool, + covered_r: bool, + noise_l: Option, + noise_r: Option, + /// `(in_phase, is_position)` — set while the band is + /// intensity-coded; the position comes from the highest IS layer. + intensity: Option<(bool, i32)>, +} + +/// The window-major slices of one `(g, sfb)` band. +fn band_slices(ics: &IcsInfo, fs: u8, g: usize, sfb: usize) -> Result> { + let window_len = ics.window_len()?; + let offsets = ics.swb_offsets(fs)?; + let lo = *offsets.get(sfb).ok_or(Error::ScalableInvalid)? as usize; + let hi = *offsets.get(sfb + 1).ok_or(Error::ScalableInvalid)? as usize; + let mut window_base = 0usize; + let mut out = Vec::new(); + for (gg, &wgl) in ics.window_group_length.iter().enumerate() { + if gg == g { + for b in 0..usize::from(wgl) { + let base = (window_base + b) * window_len; + out.push((base + lo, base + hi)); + } + return Ok(out); + } + window_base += usize::from(wgl); + } + Err(Error::ScalableInvalid) +} + +/// `true` iff every coefficient of the band is exactly zero in `spec` +/// (§4.6.13.6 "all spectral coefficients … are decoded to zero"). +fn band_is_zero(spec: &[f64], slices: &[(usize, usize)]) -> bool { + slices + .iter() + .all(|&(a, b)| spec[a..b].iter().all(|&v| v == 0.0)) +} + +fn add_band(dst: &mut [f64], src: &[f64], slices: &[(usize, usize)], gain: f64) { + for &(a, b) in slices { + for i in a..b { + dst[i] += gain * src[i]; + } + } +} + +fn copy_band(dst: &mut [f64], src: &[f64], slices: &[(usize, usize)]) { + for &(a, b) in slices { + dst[a..b].copy_from_slice(&src[a..b]); + } +} + +fn zero_band(dst: &mut [f64], slices: &[(usize, usize)]) { + for &(a, b) in slices { + for v in &mut dst[a..b] { + *v = 0.0; + } + } +} + +/// One reconstructed layer: window-major dequantized spectra plus the +/// per-channel band tables. +struct LayerRecon { + /// `[channel]` window-major spectra. + specs: Vec>, + /// `[channel]` band-indexed `noise_nrg[g][sfb]`. + noise: Vec>>, + /// Right-channel band-indexed `is_pos[g][sfb]` (stereo layers). + is_pos: Option>>, +} + +/// §4.6.9.5: the lowest sfb any of this `tns_data()`'s filters +/// reaches (the filters run downward from `max_sfb`), minimised over +/// windows. Used for the Table 4.158 serial-filter override rule. +fn tns_lower_boundary(tns: &TnsData, max_sfb: u8) -> u8 { + let mut lowest = max_sfb; + for w in &tns.windows { + let total: u32 = w.filters.iter().map(|f| u32::from(f.length)).sum(); + let bottom = u32::from(max_sfb).saturating_sub(total) as u8; + lowest = core::cmp::min(lowest, bottom); + } + lowest +} + +/// Spectral-domain output of the layer-combination pipeline: one +/// combined spectrum per output channel, ready for the filterbank. +struct CombinedSpectra { + chans: Vec>, +} + +/// Stateful decoder for one scalable program (§4.5.2.2). +/// +/// Feed one payload per layer per frame ([`ScalableDecoder::decode_frame`]); +/// the per-channel §4.6.11 overlap-add tails and the §4.6.7.5 +/// base-layer LTP history persist across frames. +#[derive(Debug)] +pub struct ScalableDecoder { + cfg: ScalableConfig, + /// Output-path filterbanks (1 or 2). + out_fbs: Vec, + /// Base-layer filterbanks for the §4.6.7.5 LTP history (used only + /// when more than one layer is configured). + base_fbs: Vec, + /// §4.6.7.5 base-layer LTP reconstruction state per base channel. + base_ltp: Vec, + /// §4.6.13.3 generator state for the output run. + pns_state: u32, + /// Independent generator state for the base-layer history run. + base_pns_state: u32, +} + +impl ScalableDecoder { + /// Build a decoder for the given configuration. + pub fn new(cfg: ScalableConfig) -> Result { + cfg.validate()?; + if cfg.aot == 6 + && (cfg.resilience.section_data + || cfg.resilience.scalefactor_data + || cfg.resilience.spectral_data) + { + return Err(Error::ScalableInvalid); + } + let n_out = cfg.output_channels(); + let n_base = cfg.channels_of_layer(0); + Ok(ScalableDecoder { + out_fbs: (0..n_out) + .map(|_| Filterbank::new_family(cfg.family)) + .collect(), + base_fbs: (0..n_base) + .map(|_| Filterbank::new_family(cfg.family)) + .collect(), + base_ltp: (0..n_base) + .map(|_| LtpState::new_family(cfg.family)) + .collect(), + pns_state: 0x0001_2345, + base_pns_state: 0x0001_2345, + cfg, + }) + } + + /// The static configuration. + pub fn config(&self) -> &ScalableConfig { + &self.cfg + } + + /// Decode one frame (one payload per layer, layer 0 first) to + /// interleaved 16-bit PCM. + pub fn decode_frame(&mut self, payloads: &[&[u8]]) -> Result { + let chans = self.decode_frame_channels(payloads)?; + let pcm = crate::pcm::interleave_s16(&chans)?; + Ok(crate::decode::DecodedFrame { + pcm, + channels: chans.len(), + sample_rate: self.cfg.sample_rate, + }) + } + + /// Decode one frame to per-channel `f64` time signals (`L, R` or + /// mono), each `family.frame_len()` samples. + pub fn decode_frame_channels(&mut self, payloads: &[&[u8]]) -> Result>> { + let frame = ScalableFrame::parse(&self.cfg, payloads)?; + let fs = self.cfg.fs_index; + + // ---- Per-layer reconstruction (SIAQ inverse quantisation). + let mut recon: Vec = Vec::with_capacity(frame.layers.len()); + for layer in &frame.layers { + let mut specs = Vec::new(); + let mut noise = Vec::new(); + let mut abs_all: Vec = Vec::new(); + for chan in &layer.channels { + let abs = accumulate( + &chan.body.scale_factor_data, + &chan.body.section_data.sfb_cb, + chan.body.global_gain, + )?; + let rescaled = rescale_spectrum( + &chan.spectral, + &abs, + &chan.body.section_data.sfb_cb, + &layer.ics, + fs, + )?; + let spec = quant_to_spec(&rescaled, &layer.ics, fs)?; + noise.push(crate::element_decode::noise_nrg_table( + &abs, + &chan.body.section_data.sfb_cb, + usize::from(layer.ics.max_sfb), + )?); + specs.push(spec); + abs_all.push(abs); + } + let is_pos = if layer.channels.len() == 2 { + Some(crate::element_decode::is_pos_table( + &abs_all[1], + &layer.channels[1].body.section_data.sfb_cb, + usize::from(layer.ics.max_sfb), + )?) + } else { + None + }; + recon.push(LayerRecon { + specs, + noise, + is_pos, + }); + } + + // ---- §4.6.7.5 base-layer LTP (prediction on layer 0 only; + // IS / PNS bands of the base layer take precedence). + let single_layer = frame.layers.len() == 1; + { + let layer0 = &frame.layers[0]; + let n_base = layer0.channels.len(); + for ch in 0..n_base { + if let Some(ltp) = &layer0.ltp[ch] { + let mut masked = ltp.clone(); + let sfb_cb_own = &layer0.channels[ch].body.section_data.sfb_cb; + let sfb_cb_right = &layer0.channels[n_base - 1].body.section_data.sfb_cb; + for (sfb, used) in masked.long_used.iter_mut().enumerate() { + let noise_band = sfb_cb_own + .first() + .and_then(|row| row.get(sfb)) + .is_some_and(|&cb| cb == NOISE_HCB); + let is_band = n_base == 2 + && sfb_cb_right + .first() + .and_then(|row| row.get(sfb)) + .is_some_and(|&cb| cb == INTENSITY_HCB || cb == INTENSITY_HCB2); + if noise_band || is_band { + *used = false; + } + } + let fb = if single_layer { + &self.out_fbs[ch] + } else { + &self.base_fbs[ch] + }; + let prev_shape = fb.prev_shape(); + let tns = layer0.tns[ch].clone(); + let ics0 = &layer0.ics; + let aot = self.cfg.aot; + let spec0 = &mut recon[0].specs[ch]; + self.base_ltp[ch].apply_long_with_analysis( + spec0, + ics0, + &masked, + prev_shape, + fs, + |x_est| { + if let Some(tns) = &tns { + tns_analysis_frame_ics(x_est, tns, ics0, aot, fs)?; + } + Ok(()) + }, + )?; + } + } + } + + // ---- Full combination run → output channels. + let n_layers = frame.layers.len(); + let mut pns_state = self.pns_state; + let combined = combine_layers(&self.cfg, &frame, &recon, n_layers, &mut pns_state)?; + self.pns_state = pns_state; + let mut out: Vec> = Vec::with_capacity(combined.chans.len()); + for (ch, spec) in combined.chans.iter().enumerate() { + out.push(self.out_fbs[ch].synthesize(spec, &frame.layers[0].ics)?); + } + + // ---- §4.6.7.5 LTP history: the time-domain output of the + // first GA layer decoded alone. + if single_layer { + for (ch, o) in out.iter().enumerate() { + let tail = self.out_fbs[ch].aliased_tail().to_vec(); + self.base_ltp[ch].push_frame(o, &tail); + } + } else { + let mut base_pns = self.base_pns_state; + let base = combine_layers(&self.cfg, &frame, &recon, 1, &mut base_pns)?; + self.base_pns_state = base_pns; + for (ch, spec) in base.chans.iter().enumerate() { + let o = self.base_fbs[ch].synthesize(spec, &frame.layers[0].ics)?; + let tail = self.base_fbs[ch].aliased_tail().to_vec(); + self.base_ltp[ch].push_frame(&o, &tail); + } + } + Ok(out) + } +} + +/// Run the §4.5.2.2.4 layer combination over the first `n_layers` +/// layers: SIAQ accumulation with the Table 4.91–4.93 per-band rules, +/// the §4.6.14.2 FSS mono→stereo merge, cumulative M/S (§4.6.8.1.4), +/// intensity (§4.6.8.2.3), PNS (§4.6.13.6) and the §4.6.9.5 serial +/// TNS. Returns the combined spectra ready for the filterbank. +fn combine_layers( + cfg: &ScalableConfig, + frame: &ScalableFrame, + recon: &[LayerRecon], + n_layers: usize, + pns_state: &mut u32, +) -> Result { + let fs = cfg.fs_index; + let base_ics = &frame.layers[0].ics; + let window_len = base_ics.window_len()?; + let num_windows = usize::from(base_ics.num_windows); + let spec_len = num_windows * window_len; + let num_groups = usize::from(base_ics.num_window_groups); + + // Coverage bounds inside this sub-run. + let stereo_present = (0..n_layers).any(|l| cfg.layer_stereo[l]); + let max_mono: u8 = (0..n_layers) + .filter(|&l| !cfg.layer_stereo[l]) + .map(|l| frame.layers[l].ics.max_sfb) + .max() + .unwrap_or(0); + let max_total: u8 = (0..n_layers) + .map(|l| frame.layers[l].ics.max_sfb) + .max() + .unwrap_or(0); + + // The synthetic geometry every final band op runs under. + let mut ics_total = base_ics.clone(); + ics_total.max_sfb = max_total; + + // Precompute band slices. + let mut slices: Vec>> = Vec::with_capacity(num_groups); + for g in 0..num_groups { + let mut per_sfb = Vec::with_capacity(usize::from(max_total)); + for sfb in 0..usize::from(max_total) { + per_sfb.push(band_slices(&ics_total, fs, g, sfb)?); + } + slices.push(per_sfb); + } + + // ---- Stage 1: mono prefix (Table 4.91). + let mut m_acc = vec![0.0f64; spec_len]; + let mut m_noise: Vec>> = vec![vec![None; usize::from(max_total)]; num_groups]; + let mut m_covered: Vec> = vec![vec![false; usize::from(max_total)]; num_groups]; + for (l, rec) in recon.iter().enumerate().take(n_layers) { + if cfg.layer_stereo[l] { + continue; + } + let layer = &frame.layers[l]; + let spec = &rec.specs[0]; + let sfb_cb = &layer.channels[0].body.section_data.sfb_cb; + for g in 0..num_groups { + for sfb in 0..usize::from(layer.ics.max_sfb) { + let cb = sfb_cb[g][sfb]; + let sl = &slices[g][sfb]; + if cb == NOISE_HCB { + if m_covered[g][sfb] && m_noise[g][sfb].is_none() { + // Table 4.91: No Tool → PNS is invalid. + return Err(Error::ScalableLayerCombination); + } + // First coverage or PNS → PNS (layer N+1 wins). + m_noise[g][sfb] = Some(rec.noise[0][g][sfb]); + } else { + if m_noise[g][sfb].is_some() && !band_is_zero(spec, sl) { + // §4.6.13.6: non-zero higher-layer content + // cancels the noise substitution. + m_noise[g][sfb] = None; + } + add_band(&mut m_acc, spec, sl, 1.0); + } + m_covered[g][sfb] = true; + } + } + } + + // ---- Stage 2: stereo layers (Table 4.92). + let mut l_acc = vec![0.0f64; spec_len]; + let mut r_acc = vec![0.0f64; spec_len]; + let mut st: Vec> = + vec![vec![BandState::default(); usize::from(max_total)]; num_groups]; + for (l, rec) in recon.iter().enumerate().take(n_layers) { + if !cfg.layer_stereo[l] { + continue; + } + let layer = &frame.layers[l]; + let (lspec, rspec) = (&rec.specs[0], &rec.specs[1]); + let lcb_t = &layer.channels[0].body.section_data.sfb_cb; + let rcb_t = &layer.channels[1].body.section_data.sfb_cb; + for g in 0..num_groups { + for sfb in 0..usize::from(layer.ics.max_sfb) { + let sl = &slices[g][sfb]; + let lcb = lcb_t[g][sfb]; + let rcb = rcb_t[g][sfb]; + let s = &mut st[g][sfb]; + let is_band = rcb == INTENSITY_HCB || rcb == INTENSITY_HCB2; + if is_band { + let pos = rec.is_pos.as_ref().map(|t| t[g][sfb]).unwrap_or(0); + let in_phase = rcb == INTENSITY_HCB; + if s.intensity.is_some() { + // IS → IS: sum the M/L channel, take the + // positions from layer N+1. + add_band(&mut l_acc, lspec, sl, 1.0); + } else if s.noise_l.is_some() || s.noise_r.is_some() { + // PNS → IS: layer N+1 only. + s.noise_l = None; + s.noise_r = None; + copy_band(&mut l_acc, lspec, sl); + zero_band(&mut r_acc, sl); + } else if s.covered_l || s.covered_r { + // No Tool / MS → IS: invalid (Table 4.92). + return Err(Error::ScalableLayerCombination); + } else { + copy_band(&mut l_acc, lspec, sl); + } + s.intensity = Some((in_phase, pos)); + s.covered_l = true; + s.covered_r = true; + continue; + } + if s.intensity.is_some() { + if lcb == NOISE_HCB || rcb == NOISE_HCB { + // IS → PNS: invalid (Table 4.92). + return Err(Error::ScalableLayerCombination); + } + // IS → No Tool / MS: layer N+1 only. + s.intensity = None; + copy_band(&mut l_acc, lspec, sl); + copy_band(&mut r_acc, rspec, sl); + s.covered_l = true; + s.covered_r = true; + continue; + } + // Per-channel plain / noise handling. + let ms_band = frame + .ms_used + .get(g) + .and_then(|row| row.get(sfb)) + .copied() + .unwrap_or(false); + let l_zero = band_is_zero(lspec, sl); + let r_zero = band_is_zero(rspec, sl); + // Table 4.93: a plain-coded mono band cannot turn + // into a stereo PNS band (No Tool → PNS is invalid). + let mono_plain = m_covered[g][sfb] && m_noise[g][sfb].is_none(); + // Left channel. + if lcb == NOISE_HCB { + if s.covered_l && s.noise_l.is_none() { + return Err(Error::ScalableLayerCombination); + } + if !s.covered_l && mono_plain { + return Err(Error::ScalableLayerCombination); + } + s.noise_l = Some(rec.noise[0][g][sfb]); + } else { + if s.noise_l.is_some() { + let cancels = if ms_band { + !(l_zero && r_zero) + } else { + !l_zero + }; + if cancels { + s.noise_l = None; + } + } + add_band(&mut l_acc, lspec, sl, 1.0); + } + s.covered_l = true; + // Right channel. + if rcb == NOISE_HCB { + if s.covered_r && s.noise_r.is_none() { + return Err(Error::ScalableLayerCombination); + } + if !s.covered_r && mono_plain { + return Err(Error::ScalableLayerCombination); + } + s.noise_r = Some(rec.noise[1][g][sfb]); + } else { + if s.noise_r.is_some() { + let cancels = if ms_band { + !(l_zero && r_zero) + } else { + !r_zero + }; + if cancels { + s.noise_r = None; + } + } + add_band(&mut r_acc, rspec, sl, 1.0); + } + s.covered_r = true; + } + } + } + + if !stereo_present { + // ---- Mono-only output: PNS, then serial TNS (M source). + let mut sfb_cb: Vec> = vec![vec![1u8; usize::from(max_total)]; num_groups]; + let mut noise_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; + for g in 0..num_groups { + for sfb in 0..usize::from(max_total) { + if let Some(nrg) = m_noise[g][sfb] { + sfb_cb[g][sfb] = NOISE_HCB; + noise_tab[g][sfb] = nrg; + } + } + } + { + let mut chan = PnsChannel { + spec: &mut m_acc, + sfb_cb: &sfb_cb, + noise_nrg: &noise_tab, + }; + apply_pns(&mut chan, &ics_total, fs, |out| { + gen_rand_vector(out, pns_state) + })?; + } + // First mono layer's TNS serves the M output (Table 4.158). + let first_mono = (0..n_layers).find(|&l| !cfg.layer_stereo[l]); + if let Some(l0) = first_mono { + if let Some(tns) = frame.layers[l0].tns.first().and_then(|t| t.as_ref()) { + tns_decode_frame_ics(&mut m_acc, tns, &frame.layers[l0].ics, cfg.aot, fs)?; + } + } + return Ok(CombinedSpectra { chans: vec![m_acc] }); + } + + // ---- Stage 3: mono → stereo merge (Table 4.93 + §4.6.14.2.1). + let has_mono = (0..n_layers).any(|l| !cfg.layer_stereo[l]); + if has_mono { + let short = base_ics.window_sequence.is_eight_short(); + if !short { + for g in 0..num_groups { + for sfb in 0..usize::from(max_mono) { + let s = &st[g][sfb]; + if s.intensity.is_some() || s.noise_l.is_some() || s.noise_r.is_some() { + // Mono content never crosses into an IS / PNS + // band (Table 4.93). + continue; + } + if m_noise[g][sfb].is_some() { + // A mono PNS band never crosses the transition. + continue; + } + let sl = &slices[g][sfb]; + let ms_band = frame.ms_used[g].get(sfb).copied().unwrap_or(false); + if ms_band { + // M = M'' + M' (§4.5.2.2.4). + add_band(&mut l_acc, &m_acc, sl, 1.0); + } else { + // §4.6.14.2.1 FSS: `+ 2·M''` where the bit is 0. + if frame.diff_lr_long[0].get(sfb).copied().flatten() == Some(false) { + add_band(&mut l_acc, &m_acc, sl, 2.0); + } + if frame.diff_lr_long[1].get(sfb).copied().flatten() == Some(false) { + add_band(&mut r_acc, &m_acc, sl, 2.0); + } + } + } + } + } else { + // §4.6.14.2.1 short windows: diff_control_lr[win][0] + // covers every band up to the mono coverage per window. + let offsets = ics_total.swb_offsets(fs)?; + let hi_coef = usize::from(offsets[usize::from(max_mono)]); + let mut window_of_group: Vec = Vec::with_capacity(num_windows); + for (g, &wgl) in base_ics.window_group_length.iter().enumerate() { + for _ in 0..wgl { + window_of_group.push(g); + } + } + for w in 0..num_windows { + let g = window_of_group[w]; + let base = w * window_len; + for sfb in 0..usize::from(max_mono) { + let s = &st[g][sfb]; + if s.intensity.is_some() || s.noise_l.is_some() || s.noise_r.is_some() { + continue; + } + if m_noise[g][sfb].is_some() { + continue; + } + let a = base + usize::from(offsets[sfb]); + let b = base + core::cmp::min(usize::from(offsets[sfb + 1]), hi_coef); + let ms_band = frame.ms_used[g].get(sfb).copied().unwrap_or(false); + if ms_band { + for i in a..b { + l_acc[i] += m_acc[i]; + } + } else { + if frame.diff_lr_short[0].map(|bits| bits[w]) == Some(false) { + for i in a..b { + l_acc[i] += 2.0 * m_acc[i]; + } + } + if frame.diff_lr_short[1].map(|bits| bits[w]) == Some(false) { + for i in a..b { + r_acc[i] += 2.0 * m_acc[i]; + } + } + } + } + } + } + } + + // ---- Stage 4: synthetic band tables → M/S → IS → PNS. + let mut synth_l: Vec> = vec![vec![1u8; usize::from(max_total)]; num_groups]; + let mut synth_r: Vec> = vec![vec![1u8; usize::from(max_total)]; num_groups]; + let mut noise_l_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; + let mut noise_r_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; + let mut is_pos_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; + for g in 0..num_groups { + for sfb in 0..usize::from(max_total) { + let s = &st[g][sfb]; + if let Some((in_phase, pos)) = s.intensity { + synth_r[g][sfb] = if in_phase { + INTENSITY_HCB + } else { + INTENSITY_HCB2 + }; + is_pos_tab[g][sfb] = pos; + continue; + } + if let Some(nrg) = s.noise_l { + synth_l[g][sfb] = NOISE_HCB; + noise_l_tab[g][sfb] = nrg; + } + if let Some(nrg) = s.noise_r { + synth_r[g][sfb] = NOISE_HCB; + noise_r_tab[g][sfb] = nrg; + } + } + } + + { + let mut pair = ChannelPairSpectra { + left: &mut l_acc, + right: &mut r_acc, + left_sfb_cb: &synth_l, + right_sfb_cb: &synth_r, + }; + apply_ms_stereo( + &mut pair, + MsMaskPresent::Mask, + &frame.ms_used, + &ics_total, + fs, + )?; + } + { + let mut pair = IntensityPairSpectra { + left: &l_acc, + right: &mut r_acc, + right_sfb_cb: &synth_r, + is_pos: &is_pos_tab, + }; + // §4.6.8.2.3: invert_intensity() == +1 for the scalable AOT, + // so the ms_used phase-reversal branch is disabled. + apply_intensity_stereo(&mut pair, false, &[], &ics_total, fs)?; + } + { + let mut left = PnsChannel { + spec: &mut l_acc, + sfb_cb: &synth_l, + noise_nrg: &noise_l_tab, + }; + let mut right = PnsChannel { + spec: &mut r_acc, + sfb_cb: &synth_r, + noise_nrg: &noise_r_tab, + }; + // §4.6.13.6: the cumulative ms_used still signals noise + // correlation across the channel pair. + apply_pns_pair( + &mut left, + &mut right, + true, + false, + &frame.ms_used, + &ics_total, + fs, + |out| gen_rand_vector(out, pns_state), + )?; + } + + // ---- Stage 5: §4.6.9.5 serial TNS (Table 4.158). + let first_mono = (0..n_layers).find(|&l| !cfg.layer_stereo[l]); + let first_stereo = (0..n_layers).find(|&l| cfg.layer_stereo[l]); + let tns_m: Option<(&TnsData, &IcsInfo)> = first_mono.and_then(|l| { + frame.layers[l] + .tns + .first() + .and_then(|t| t.as_ref()) + .map(|t| (t, &frame.layers[l].ics)) + }); + for (ch, acc) in [&mut l_acc, &mut r_acc].into_iter().enumerate() { + let tns_ch: Option<(&TnsData, &IcsInfo)> = first_stereo.and_then(|l| { + frame.layers[l] + .tns + .get(ch) + .and_then(|t| t.as_ref()) + .map(|t| (t, &frame.layers[l].ics)) + }); + match (tns_ch, tns_m) { + (Some((t, ics)), Some((tm, ics_m))) => { + // Serial L/M (R/M) layout: the M filter first (it + // covers the low bands, stopping at the highest mono + // max_sfb), then the channel filter — unless the + // channel filter reaches below the mono boundary, in + // which case the M filter is skipped. + if tns_lower_boundary(t, ics.max_sfb) >= max_mono { + tns_decode_frame_ics(acc, tm, ics_m, cfg.aot, fs)?; + } + tns_decode_frame_ics(acc, t, ics, cfg.aot, fs)?; + } + (Some((t, ics)), None) => { + tns_decode_frame_ics(acc, t, ics, cfg.aot, fs)?; + } + (None, Some((tm, ics_m))) => { + tns_decode_frame_ics(acc, tm, ics_m, cfg.aot, fs)?; + } + (None, None) => {} + } + } + + Ok(CombinedSpectra { + chans: vec![l_acc, r_acc], + }) +} diff --git a/crates/vendor/oxideav-aac/src/scale_factor_data.rs b/crates/vendor/oxideav-aac/src/scale_factor_data.rs new file mode 100644 index 00000000..d247ae9d --- /dev/null +++ b/crates/vendor/oxideav-aac/src/scale_factor_data.rs @@ -0,0 +1,1579 @@ +//! `scale_factor_data()` parser + encoder primitive — ISO/IEC 14496-3 +//! §4.4.6 / Table 4.53 (non-resilient branch) plus §4.6.3 / Table 4.A.1 +//! ("Scalefactor Huffman Codebook" — codebook 12). +//! +//! `scale_factor_data()` is the third tool inside +//! `individual_channel_stream()` (after `global_gain` and +//! `section_data()`, before `pulse_data_present` / +//! `pulse_data()`). For every `(g, sfb)` whose +//! [`section_data`](crate::section_data) classifier picked a non-zero +//! codebook, this tool emits one differentially-coded value (a DPCM +//! delta in the range `-60..=+60`) using the 121-entry Table 4.A.1 +//! Huffman codebook. The exception is the **first** Perceptual Noise +//! Substitution (PNS) band of the frame, whose energy delta is sent +//! as a literal 9-bit signed value — every subsequent PNS band falls +//! back to the Huffman path. +//! +//! ## Wire layout (Table 4.53, non-resilient branch) +//! +//! ```text +//! scale_factor_data() { +//! noise_pcm_flag = 1 +//! for (g = 0; g < num_window_groups; g++) { +//! for (sfb = 0; sfb < max_sfb; sfb++) { +//! if (sfb_cb[g][sfb] != ZERO_HCB) { +//! if (is_intensity(g, sfb)) { +//! hcod_sf[dpcm_is_position[g][sfb]]; 1..19 bits +//! } else if (is_noise(g, sfb)) { +//! if (noise_pcm_flag) { +//! noise_pcm_flag = 0 +//! dpcm_noise_nrg[g][sfb]; 9 bits (PCM) +//! } else { +//! hcod_sf[dpcm_noise_nrg[g][sfb]]; 1..19 bits +//! } +//! } else { +//! hcod_sf[dpcm_sf[g][sfb]]; 1..19 bits +//! } +//! } +//! } +//! } +//! } +//! ``` +//! +//! Three observations the parser and writer both rely on: +//! +//! 1. The outer `(g, sfb)` traversal is **driven by** +//! [`section_data::SectionData::sfb_cb`](crate::section_data::SectionData::sfb_cb) +//! — the parser must already know which bands carry a value before +//! it can decide between "skip", "Huffman value", or "9-bit PCM +//! energy". The wire stream carries no per-band header that would +//! let it self-synchronise. +//! 2. The DPCM range is `-60..=+60` (Table 4.150). The Huffman +//! codebook (Table 4.A.1) has 121 entries indexed `0..=120`; an +//! `index_offset` of `-60` recovers the signed delta. The codeword +//! for index 60 (delta 0) is the single bit `0`. +//! 3. `noise_pcm_flag` is **frame-scoped** (not group-scoped): it +//! starts at `1` at the top of `scale_factor_data()` and clears the +//! first time a PNS band is emitted, regardless of which window +//! group or scalefactor band that is. +//! +//! ## What this module covers +//! +//! * [`ScaleFactorData::parse`] — read a non-resilient Table 4.53 +//! block given the surrounding `sfb_cb[g][sfb]` map. Surfaces the +//! raw transmitted `dpcm_sf` / `dpcm_is_position` deltas and the +//! `dpcm_noise_nrg` magnitudes verbatim. +//! * [`ScaleFactorData::write`] — the inverse: serialise a +//! [`ScaleFactorData`] bit-for-bit. Surfaces caller-side structural +//! bugs (delta out of range, PCM energy out of range, missing / +//! surplus per-band entry versus the `sfb_cb` map) as +//! [`Error::ScaleFactorDataEncodeInvalid`]. +//! * [`accumulate`] — the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM +//! accumulator (decoder side). Runs the three independent tracks +//! forward: spectrum scalefactors (`last_sf = global_gain`), +//! intensity stereo positions (`last_is = 0`), and PNS noise +//! energies (`last_nrg = global_gain - NOISE_OFFSET - 256`). +//! Returns absolute `(sf, is_pos, noise_nrg)` per band. +//! * [`differentiate`] — the symmetric inverse (encoder side). Takes +//! absolute per-band quantities from rate-allocation and produces +//! the [`ScaleFactorData`] the bit-exact writer expects. Validates +//! that every spectrum / intensity / PNS-subsequent delta fits +//! Table 4.150's `-60..=+60`, and that the first PNS band's +//! seed fits the 9-bit `uimsbf` Table 4.53 field. +//! * [`hcod_sf_encode`] / [`hcod_sf_decode`] — public Table 4.A.1 +//! accessors for callers (Auditor harnesses, fixture cross-checks) +//! that need the codebook directly without going through the full +//! `scale_factor_data()` driver. +//! +//! ## Three-track DPCM (spec ambiguity, resolved per §4.6.8 / §4.6.13) +//! +//! The §4.6.2.3.2 illustrative pseudocode declares **one** accumulator +//! `last_sf = global_gain` and lumps PNS (`NOISE_HCB`) bands into it +//! alongside spectrum bands. This pseudocode predates MPEG-4's PNS +//! feature (it is identical in 13818-7 §11.3.2 where no PNS exists) +//! and conflicts with the surrounding §4.6.8.1.4 + §4.6.13 wording, +//! which states explicitly that "differential decoding is done +//! separately between scalefactors, intensity stereo positions and +//! noise energies" with each track having its own running register +//! and its own initial-condition seed. +//! +//! This module implements the three-track interpretation: +//! intensity bands seed at `last_is = 0`, PNS bands seed at +//! `last_nrg = global_gain - NOISE_OFFSET - 256` (with the first +//! PNS band's 9-bit literal added directly to `last_nrg`), spectrum +//! bands seed at `last_sf = global_gain`. The §4.6.2.3.2 pseudocode's +//! single-track form is not used because the §4.6.8 / §4.6.13 +//! prose-level requirement of independence cannot be honoured under +//! a single track that mixes spectrum and PNS deltas. +//! +//! ## What this module does *not* cover +//! +//! * The §4.4.6 error-resilient branch (`aacScalefactorDataResilienceFlag +//! == 1` → RVLC with `rev_global_gain`, `length_of_rvlc_sf`, +//! `sf_concealment`, `length_of_rvlc_escapes`, etc.) — the in-memory +//! structure here is the non-resilient flavour. ER AAC-LD / scalable +//! profiles that flip the resilience flag will need a sibling +//! `scale_factor_data_rvlc()` module. +//! * The §4.6.2.3.3 / §4.6.8 / §4.6.13 reconstruction steps that +//! actually *consume* the absolute values: `get_scale_factor_gain +//! = 2^(0.25 * (sf - SF_OFFSET))`, the IS rescaling sign-flip per +//! `ms_used`, and the PNS random-vector energy rescaling. Those +//! are per-AOT IMDCT back-end concerns that need spectral-context +//! state this module does not own. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::ics_info::WindowSequence; +use crate::section_data::{INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB, ZERO_HCB}; +use crate::{Error, Result}; + +// ============================================================================= +// Table 4.A.1 — Scalefactor Huffman Codebook (codebook 12) +// ============================================================================= +// +// Per Table 4.150, the codebook covers indices 0..=120 with +// `index_offset = -60`, producing DPCM values in `-60..=+60`. The +// table is reproduced verbatim from ISO/IEC 14496-3 §4.A.1 / Table +// 4.A.1 with every length / codeword cross-checked against the +// 13818-7 §11.3.2 / Table 11.3 listing (the two specifications carry +// the same table for backwards bitstream compatibility). +// +// Format: `(length_in_bits, codeword_value)`. Codewords are stored +// right-aligned (the MSB of the wire codeword sits at bit +// `length - 1`), exactly as the Table 4.A.1 hexadecimal column +// presents them. + +/// `index_offset` for the scalefactor codebook per Table 4.150 +/// (`-60`, surfaced as a signed type because the DPCM range is +/// `-60..=+60`). +pub const SF_INDEX_OFFSET: i8 = -60; + +/// `dpcm_noise_nrg` PCM seed width — Table 4.53 `dpcm_noise_nrg` +/// row (9 bits, `uimsbf` in the spec which the §4.6.13 decoder +/// re-interprets as a signed 9-bit delta). +pub const NOISE_PCM_BITS: u32 = 9; + +/// Number of entries in Table 4.A.1 (`121`, indices `0..=120`). +pub const HCOD_SF_NUM_ENTRIES: usize = 121; + +/// Maximum codeword length emitted by Table 4.A.1 (19 bits). +pub const HCOD_SF_MAX_LEN: u32 = 19; + +/// Table 4.A.1 — `(length_in_bits, codeword)` per index `0..=120`. +/// +/// Codewords are right-aligned within the `u32`. To emit one bit-for- +/// bit, write `codeword` as `length` bits MSB-first. +const HCOD_SF: [(u8, u32); HCOD_SF_NUM_ENTRIES] = [ + (18, 0x3ffe8), // 0 + (18, 0x3ffe6), // 1 + (18, 0x3ffe7), // 2 + (18, 0x3ffe5), // 3 + (19, 0x7fff5), // 4 + (19, 0x7fff1), // 5 + (19, 0x7ffed), // 6 + (19, 0x7fff6), // 7 + (19, 0x7ffee), // 8 + (19, 0x7ffef), // 9 + (19, 0x7fff0), // 10 + (19, 0x7fffc), // 11 + (19, 0x7fffd), // 12 + (19, 0x7ffff), // 13 + (19, 0x7fffe), // 14 + (19, 0x7fff7), // 15 + (19, 0x7fff8), // 16 + (19, 0x7fffb), // 17 + (19, 0x7fff9), // 18 + (18, 0x3ffe4), // 19 + (19, 0x7fffa), // 20 + (18, 0x3ffe3), // 21 + (17, 0x1ffef), // 22 + (17, 0x1fff0), // 23 + (16, 0x0fff5), // 24 + (17, 0x1ffee), // 25 + (16, 0x0fff2), // 26 + (16, 0x0fff3), // 27 + (16, 0x0fff4), // 28 + (16, 0x0fff1), // 29 + (15, 0x07ff6), // 30 + (15, 0x07ff7), // 31 + (14, 0x03ff9), // 32 + (14, 0x03ff5), // 33 + (14, 0x03ff7), // 34 + (14, 0x03ff3), // 35 + (14, 0x03ff6), // 36 + (14, 0x03ff2), // 37 + (13, 0x01ff7), // 38 + (13, 0x01ff5), // 39 + (12, 0x00ff9), // 40 + (12, 0x00ff7), // 41 + (12, 0x00ff6), // 42 + (11, 0x007f9), // 43 + (12, 0x00ff4), // 44 + (11, 0x007f8), // 45 + (10, 0x003f9), // 46 + (10, 0x003f7), // 47 + (10, 0x003f5), // 48 + (9, 0x001f8), // 49 + (9, 0x001f7), // 50 + (8, 0x000fa), // 51 + (8, 0x000f8), // 52 + (8, 0x000f6), // 53 + (7, 0x00079), // 54 + (6, 0x0003a), // 55 + (6, 0x00038), // 56 + (5, 0x0001a), // 57 + (4, 0x0000b), // 58 + (3, 0x00004), // 59 + (1, 0x00000), // 60 — delta 0, single bit `0` + (4, 0x0000a), // 61 + (4, 0x0000c), // 62 + (5, 0x0001b), // 63 + (6, 0x00039), // 64 + (6, 0x0003b), // 65 + (7, 0x00078), // 66 + (7, 0x0007a), // 67 + (8, 0x000f7), // 68 + (8, 0x000f9), // 69 + (9, 0x001f6), // 70 + (9, 0x001f9), // 71 + (10, 0x003f4), // 72 + (10, 0x003f6), // 73 + (10, 0x003f8), // 74 + (11, 0x007f5), // 75 + (11, 0x007f4), // 76 + (11, 0x007f6), // 77 + (11, 0x007f7), // 78 + (12, 0x00ff5), // 79 + (12, 0x00ff8), // 80 + (13, 0x01ff4), // 81 + (13, 0x01ff6), // 82 + (13, 0x01ff8), // 83 + (14, 0x03ff8), // 84 + (14, 0x03ff4), // 85 + (16, 0x0fff0), // 86 + (15, 0x07ff4), // 87 + (16, 0x0fff6), // 88 + (15, 0x07ff5), // 89 + (18, 0x3ffe2), // 90 + (19, 0x7ffd9), // 91 + (19, 0x7ffda), // 92 + (19, 0x7ffdb), // 93 + (19, 0x7ffdc), // 94 + (19, 0x7ffdd), // 95 + (19, 0x7ffde), // 96 + (19, 0x7ffd8), // 97 + (19, 0x7ffd2), // 98 + (19, 0x7ffd3), // 99 + (19, 0x7ffd4), // 100 + (19, 0x7ffd5), // 101 + (19, 0x7ffd6), // 102 + (19, 0x7fff2), // 103 + (19, 0x7ffdf), // 104 + (19, 0x7ffe7), // 105 + (19, 0x7ffe8), // 106 + (19, 0x7ffe9), // 107 + (19, 0x7ffea), // 108 + (19, 0x7ffeb), // 109 + (19, 0x7ffe6), // 110 + (19, 0x7ffe0), // 111 + (19, 0x7ffe1), // 112 + (19, 0x7ffe2), // 113 + (19, 0x7ffe3), // 114 + (19, 0x7ffe4), // 115 + (19, 0x7ffe5), // 116 + (19, 0x7ffd7), // 117 + (19, 0x7ffec), // 118 + (19, 0x7fff4), // 119 + (19, 0x7fff3), // 120 +]; + +/// Encode a signed DPCM delta in `-60..=+60` to the wire Huffman +/// codeword for Table 4.A.1. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u32` (MSB at bit `length - 1`). Out-of-range `dpcm` +/// produces [`Error::ScaleFactorDataEncodeInvalid`]. +/// +/// The inverse of [`hcod_sf_decode`]. +pub fn hcod_sf_encode(dpcm: i8) -> Result<(u8, u32)> { + let idx = (dpcm as i32) - (SF_INDEX_OFFSET as i32); + if !(0..HCOD_SF_NUM_ENTRIES as i32).contains(&idx) { + return Err(Error::ScaleFactorDataEncodeInvalid); + } + Ok(HCOD_SF[idx as usize]) +} + +/// Decode one Table 4.A.1 Huffman codeword from `reader`, returning +/// the signed DPCM delta in `-60..=+60`. +/// +/// The decoder is a straight prefix-match: read one bit at a time, +/// look it up in a flat table. The table is small (121 entries, max +/// length 19 bits) so a single linear scan per bit-extend is +/// sufficient and avoids the cost / complexity of a multi-level +/// lookup acceleration table. Returns [`Error::UnexpectedEnd`] on +/// reader underflow. +/// +/// The codebook is a **complete** prefix code (Kraft equality: +/// `Σ 2^(19-L_i) = 2^19`), so every fully-read 19-bit sequence is +/// guaranteed to match some entry — the bottom of the loop is +/// unreachable provided `reader` produces 19 bits without +/// underflowing. A purely-defensive `unreachable!()` guards the +/// loop fall-through; it has been verified at compile-time as +/// dead code by the [`hcod_sf_decode_is_complete`](#) regression +/// test that exhaustively walks all `2^19` 19-bit prefixes. +pub fn hcod_sf_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD_SF_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + // Linear scan: cost is bounded by HCOD_SF_NUM_ENTRIES * 19. + for (idx, &(entry_len, entry_cw)) in HCOD_SF.iter().enumerate() { + if u32::from(entry_len) == len && entry_cw == acc { + return Ok((idx as i8) + SF_INDEX_OFFSET); + } + } + } + // Unreachable: the codebook is a complete prefix code over + // 19 bits (Kraft equality = 524288), so the inner loop must + // hit for at least one `len <= 19`. The guard is here so the + // compiler doesn't infer a non-`!` return path. + unreachable!("HCOD_SF is a complete 19-bit prefix code; the 19-bit walk must match"); +} + +// ============================================================================= +// Per-band record +// ============================================================================= + +/// One transmitted per-band record. +/// +/// The variant is selected by [`crate::section_data::SectionData::sfb_cb`]: +/// `Dpcm` for ordinary spectrum books (1..=11, plus PNS book 13 +/// after the first), `Intensity` for books 14 / 15, `NoisePcm` for +/// the **first** PNS band of the frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScaleFactorEntry { + /// `hcod_sf[dpcm_sf[g][sfb]]` — Huffman DPCM delta for a band + /// whose codebook is a non-zero spectrum book (1..=11). + Dpcm(i8), + /// `hcod_sf[dpcm_is_position[g][sfb]]` — Huffman DPCM delta for + /// an intensity-stereo band (codebook 14 or 15). + Intensity(i8), + /// `dpcm_noise_nrg[g][sfb]` 9-bit PCM seed — emitted **only** + /// for the first PNS band (codebook 13) of the frame. The value + /// is the raw 9-bit wire bits (the §4.6.13 reconstruction + /// converts the unsigned wire pattern to a signed `-256..=+255` + /// energy delta). + NoisePcm(u16), + /// `hcod_sf[dpcm_noise_nrg[g][sfb]]` — Huffman DPCM delta for a + /// PNS band after the first. + NoiseDpcm(i8), +} + +/// Parsed `scale_factor_data()` payload (non-resilient branch). +/// +/// `entries` is grouped per window group: `entries[g][i]` is the +/// `i`-th transmitted per-band record for group `g`, in wire +/// (low-frequency-first) order. The mapping back to scalefactor +/// bands is recovered by walking +/// [`SectionData::sfb_cb`](crate::section_data::SectionData::sfb_cb) +/// and skipping `ZERO_HCB` bands — the same walk the parser +/// performed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScaleFactorData { + /// `entries[g]` — the per-band records of window group `g` in + /// wire order. `entries.len()` equals `sfb_cb.len()` + /// (`num_window_groups`). + pub entries: Vec>, +} + +impl ScaleFactorData { + /// Parse a non-resilient `scale_factor_data()` from `reader`. + /// + /// * `reader` — positioned at the first bit of the + /// `scale_factor_data()` block (immediately after + /// `section_data()`). + /// * `sfb_cb` — the per-`(g, sfb)` codebook map produced by + /// [`section_data::SectionData::parse`](crate::section_data::SectionData::parse). + /// Outer length is `num_window_groups`; each inner slice is + /// `max_sfb` entries. + /// + /// Returns [`Error::UnexpectedEnd`] on reader underflow. The + /// codebook is a complete 19-bit prefix code so a fully-read + /// Huffman value is guaranteed to match an entry. + pub fn parse(reader: &mut BitReader<'_>, sfb_cb: &[Vec]) -> Result { + let mut noise_pcm_flag = true; + let mut entries: Vec> = Vec::with_capacity(sfb_cb.len()); + for group in sfb_cb { + let mut group_entries: Vec = Vec::new(); + for &cb in group { + if cb == ZERO_HCB { + continue; + } + let entry = if is_intensity(cb) { + let dpcm = hcod_sf_decode(reader)?; + ScaleFactorEntry::Intensity(dpcm) + } else if is_noise(cb) { + if noise_pcm_flag { + noise_pcm_flag = false; + let pcm = reader + .read_u32(NOISE_PCM_BITS) + .map_err(|_| Error::UnexpectedEnd)? + as u16; + ScaleFactorEntry::NoisePcm(pcm) + } else { + let dpcm = hcod_sf_decode(reader)?; + ScaleFactorEntry::NoiseDpcm(dpcm) + } + } else { + let dpcm = hcod_sf_decode(reader)?; + ScaleFactorEntry::Dpcm(dpcm) + }; + group_entries.push(entry); + } + entries.push(group_entries); + } + Ok(ScaleFactorData { entries }) + } + + /// Encode `scale_factor_data()` onto `writer`, the inverse of + /// [`ScaleFactorData::parse`]. + /// + /// * `writer` — receives the bit-exact Table 4.53 stream. + /// * `sfb_cb` — the same codebook map the matching parse call + /// would receive. Drives the variant the writer expects at + /// each band. + /// + /// Returns [`Error::ScaleFactorDataEncodeInvalid`] if: + /// + /// * `self.entries.len()` does not equal `sfb_cb.len()`. + /// * A group's `entries` count does not match the number of + /// non-zero-codebook bands in the matching `sfb_cb` group. + /// * The variant at index `i` does not match the codebook + /// classification of the `i`-th non-zero band + /// (e.g. [`ScaleFactorEntry::Intensity`] paired with a + /// spectrum book, or [`ScaleFactorEntry::NoisePcm`] paired + /// with a non-PNS band, or — for the second PNS band onward — + /// [`ScaleFactorEntry::NoisePcm`] re-used after + /// `noise_pcm_flag` has cleared). + /// * A `Dpcm` / `Intensity` / `NoiseDpcm` delta falls outside + /// `-60..=+60`. + /// * A `NoisePcm` value exceeds the 9-bit field cap + /// (`> 0x1ff`). + pub fn write(&self, writer: &mut BitWriter, sfb_cb: &[Vec]) -> Result<()> { + if self.entries.len() != sfb_cb.len() { + return Err(Error::ScaleFactorDataEncodeInvalid); + } + let mut noise_pcm_flag = true; + for (group_entries, group_cb) in self.entries.iter().zip(sfb_cb.iter()) { + // Walk both in lockstep: the entries list and the + // non-zero subsequence of sfb_cb must match position-by- + // position. Surfacing a mismatch is the same error + // regardless of cause (length vs variant mismatch). + let mut entry_iter = group_entries.iter(); + for &cb in group_cb { + if cb == ZERO_HCB { + continue; + } + let entry = entry_iter + .next() + .ok_or(Error::ScaleFactorDataEncodeInvalid)?; + match (entry, cb) { + (ScaleFactorEntry::Intensity(dpcm), cb) if is_intensity(cb) => { + let (len, cw) = hcod_sf_encode(*dpcm)?; + writer.write_u32(cw, u32::from(len)); + } + (ScaleFactorEntry::NoisePcm(pcm), cb) if is_noise(cb) => { + if !noise_pcm_flag { + // PNS seed already consumed earlier; + // a second NoisePcm is wire-illegal. + return Err(Error::ScaleFactorDataEncodeInvalid); + } + if u32::from(*pcm) >= (1u32 << NOISE_PCM_BITS) { + return Err(Error::ScaleFactorDataEncodeInvalid); + } + noise_pcm_flag = false; + writer.write_u32(u32::from(*pcm), NOISE_PCM_BITS); + } + (ScaleFactorEntry::NoiseDpcm(dpcm), cb) if is_noise(cb) => { + if noise_pcm_flag { + // First PNS band of the frame must use + // the 9-bit PCM seed, not the Huffman + // delta — caller skipped the seed. + return Err(Error::ScaleFactorDataEncodeInvalid); + } + let (len, cw) = hcod_sf_encode(*dpcm)?; + writer.write_u32(cw, u32::from(len)); + } + (ScaleFactorEntry::Dpcm(dpcm), cb) if !is_intensity(cb) && !is_noise(cb) => { + let (len, cw) = hcod_sf_encode(*dpcm)?; + writer.write_u32(cw, u32::from(len)); + } + _ => return Err(Error::ScaleFactorDataEncodeInvalid), + } + } + // Extra entries beyond the non-zero codebook subsequence + // would silently shift the wire layout — reject. + if entry_iter.next().is_some() { + return Err(Error::ScaleFactorDataEncodeInvalid); + } + } + Ok(()) + } +} + +// ============================================================================= +// §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM accumulators +// ============================================================================= +// +// `scale_factor_data()` transmits *differential* values. Recovering the +// absolute per-band quantities the per-AOT IMDCT / intensity-stereo / +// PNS back-ends consume requires accumulating the DPCM deltas against +// initial-condition seeds. There are **three** independent tracks: +// +// 1. **Spectrum scalefactors** (codebooks 1..=11): per ISO/IEC 14496-3 +// §4.6.2.3.2 / ISO/IEC 13818-7 §11.3.2, accumulator initial value +// `last_sf = global_gain`; per-band `sf[g][sfb] = dpcm_sf + +// last_sf; last_sf = sf[g][sfb]`. Range `0..=255` (clause note; +// the 13818-7 wording matches). +// +// 2. **Intensity stereo positions** (codebooks 14, 15): per +// §4.6.8.1.4, initial `last_is = 0`; per-band `is_pos[g][sfb] = +// dpcm_is_position + last_is; last_is = is_pos[g][sfb]`. The +// §4.6.8.1.4 text is explicit that intensity-position differential +// decoding is "done separately" from the scalefactor track, with +// the seed starting at zero rather than `global_gain`. +// +// 3. **PNS noise energies** (codebook 13): per §4.6.13, initial +// `last_nrg = global_gain - NOISE_OFFSET - 256` (`NOISE_OFFSET == +// 90`); the first PNS band carries a 9-bit `uimsbf` literal +// `dpcm_noise_nrg` (added to `last_nrg` directly), each +// subsequent PNS band carries a Huffman delta in `-60..=+60`. +// Per-band `noise_nrg[g][sfb] = dpcm_noise_nrg + last_nrg; +// last_nrg = noise_nrg[g][sfb]`. The §4.6.13 text is explicit +// that PNS energies are "done separately" from both other tracks. +// +// The three-track presentation in §4.6.8 / §4.6.13 takes precedence +// over the §4.6.2.3.2 illustrative pseudocode (which predates PNS +// in 13818-7 and conflates the spectrum + PNS tracks under a single +// `last_sf` register). The "done separately" wording in §4.6.8.1.4 +// and §4.6.13 is unambiguous; this crate honours it. +// +// `accumulate(sfd, sfb_cb, global_gain)` runs all three tracks +// forward (decoder side) to recover absolute `(sf, is_pos, +// noise_nrg)`. `differentiate(abs, sfb_cb, global_gain)` is its +// inverse (encoder side, fed by the rate-allocation stage's +// absolute-value output). + +/// `NOISE_OFFSET` per §4.6.13 — added to the PNS energy seed to +/// position the running `last_nrg` register relative to +/// `global_gain`. +pub const NOISE_OFFSET: i32 = 90; + +/// One absolute per-band record, the result of running the §4.6.2.3.2 +/// / §4.6.8.1.4 / §4.6.13 DPCM accumulators forward over a +/// [`ScaleFactorData`] together with `global_gain`. +/// +/// The variant matches the [`ScaleFactorEntry`] variant of the +/// corresponding transmitted record but carries the absolute value +/// the per-AOT back-end consumes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AbsoluteScaleFactorEntry { + /// Absolute spectrum-band scalefactor `sf[g][sfb] ∈ 0..=255` — + /// the gain applied to the spectral coefficients of this + /// scalefactor band per §4.6.2.3.3. + Sf(u8), + /// Absolute intensity stereo position `is_pos[g][sfb] ∈ + /// -60..=+60` accumulated — the value the §4.6.8.2 IS decoder + /// consumes. The track seeds at 0 and accumulates `-60..=+60` + /// deltas, so the absolute value's reachable range is in + /// principle unbounded; conforming streams keep it within the + /// signed 8-bit window. + IsPos(i16), + /// Absolute noise energy `noise_nrg[g][sfb]` — the value the + /// §4.6.13 noise-substitution back-end consumes. Tracked as + /// `i32` because the seed is `global_gain - NOISE_OFFSET - 256` + /// (which can be negative for small `global_gain`) and the + /// running accumulator may dip negative before the first PNS + /// band lands a positive 9-bit delta. + NoiseNrg(i32), +} + +/// Absolute per-band quantities recovered by running the §4.6.2.3.2 +/// / §4.6.8.1.4 / §4.6.13 DPCM accumulators forward over a +/// [`ScaleFactorData`]. +/// +/// Outer length equals `sfb_cb.len()` (`num_window_groups`); inner +/// `entries[g]` length matches the `entries[g]` of the source +/// [`ScaleFactorData`] (the non-`ZERO_HCB` band count of the +/// matching `sfb_cb[g]`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AbsoluteScaleFactors { + /// `entries[g]` — the per-band absolute records of window group + /// `g` in wire (low-frequency-first) order. Variant order + /// follows the per-band codebook classification in the matching + /// `sfb_cb[g]`, skipping `ZERO_HCB` bands. + pub entries: Vec>, +} + +/// Run the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM accumulators +/// forward over `sfd` to recover absolute scalefactors, intensity +/// stereo positions, and PNS noise energies (decoder side). +/// +/// * `sfd` — the transmitted DPCM record set returned by +/// [`ScaleFactorData::parse`]. +/// * `sfb_cb` — the per-`(g, sfb)` codebook map produced by +/// [`crate::section_data::SectionData::parse`]. +/// * `global_gain` — the 8-bit `global_gain` element transmitted +/// immediately before `section_data()` in +/// `individual_channel_stream()`. +/// +/// Returns [`Error::ScaleFactorAccumulatorInvalid`] if the +/// per-group entry layout in `sfd` does not match the non-`ZERO_HCB` +/// codebook classification of the matching `sfb_cb` group, or if a +/// Sf-track running value escapes the `0..=255` spec range (Note +/// after §4.6.2.3.2 pseudocode). +pub fn accumulate( + sfd: &ScaleFactorData, + sfb_cb: &[Vec], + global_gain: u8, +) -> Result { + if sfd.entries.len() != sfb_cb.len() { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + let mut last_sf: i32 = i32::from(global_gain); + let mut last_is: i32 = 0; + let mut last_nrg: i32 = i32::from(global_gain) - NOISE_OFFSET - 256; + let mut noise_pcm_flag = true; + let mut out: Vec> = Vec::with_capacity(sfb_cb.len()); + for (group_entries, group_cb) in sfd.entries.iter().zip(sfb_cb.iter()) { + let mut entry_iter = group_entries.iter(); + let mut group_out: Vec = Vec::new(); + for &cb in group_cb { + if cb == ZERO_HCB { + continue; + } + let entry = entry_iter + .next() + .ok_or(Error::ScaleFactorAccumulatorInvalid)?; + let abs_entry = match (entry, cb) { + (ScaleFactorEntry::Intensity(dpcm), cb) if is_intensity(cb) => { + last_is += i32::from(*dpcm); + AbsoluteScaleFactorEntry::IsPos(last_is as i16) + } + (ScaleFactorEntry::NoisePcm(pcm), cb) if is_noise(cb) => { + if !noise_pcm_flag { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + noise_pcm_flag = false; + last_nrg += i32::from(*pcm); + AbsoluteScaleFactorEntry::NoiseNrg(last_nrg) + } + (ScaleFactorEntry::NoiseDpcm(dpcm), cb) if is_noise(cb) => { + if noise_pcm_flag { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + last_nrg += i32::from(*dpcm); + AbsoluteScaleFactorEntry::NoiseNrg(last_nrg) + } + (ScaleFactorEntry::Dpcm(dpcm), cb) if !is_intensity(cb) && !is_noise(cb) => { + last_sf += i32::from(*dpcm); + if !(0..=255).contains(&last_sf) { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + AbsoluteScaleFactorEntry::Sf(last_sf as u8) + } + _ => return Err(Error::ScaleFactorAccumulatorInvalid), + }; + group_out.push(abs_entry); + } + if entry_iter.next().is_some() { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + out.push(group_out); + } + Ok(AbsoluteScaleFactors { entries: out }) +} + +/// Run the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM accumulators +/// backward (encoder side): convert absolute per-band quantities +/// produced by rate-allocation into the transmitted DPCM record set +/// the bit-exact `scale_factor_data()` writer expects. +/// +/// This is the symmetric inverse of [`accumulate`]: +/// `accumulate(differentiate(abs, sfb_cb, gg)?, sfb_cb, gg) == abs` +/// on every well-formed input. +/// +/// * `abs` — the absolute per-band records from rate-allocation +/// (`Sf` for spectrum bands, `IsPos` for intensity bands, +/// `NoiseNrg` for PNS bands). +/// * `sfb_cb` — per-band codebook map from `section_data()`. +/// * `global_gain` — the 8-bit element the wire stream carries +/// immediately before `section_data()` (a free parameter the +/// encoder picks; conforming choice is the first spectrum band's +/// absolute `sf` to make the first delta `0`). +/// +/// Returns [`Error::ScaleFactorAccumulatorInvalid`] if outer / inner +/// shape disagrees with `sfb_cb`, if an entry variant does not match +/// its band's codebook, if a spectrum / intensity / PNS-subsequent +/// delta `cur - prev` falls outside Table 4.150's `-60..=+60`, or +/// if the first PNS band's initial `dpcm_noise_nrg` magnitude does +/// not fit the 9-bit `uimsbf` Table 4.53 field (`0..=511`). +pub fn differentiate( + abs: &AbsoluteScaleFactors, + sfb_cb: &[Vec], + global_gain: u8, +) -> Result { + if abs.entries.len() != sfb_cb.len() { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + let mut last_sf: i32 = i32::from(global_gain); + let mut last_is: i32 = 0; + let mut last_nrg: i32 = i32::from(global_gain) - NOISE_OFFSET - 256; + let mut noise_pcm_flag = true; + let mut out: Vec> = Vec::with_capacity(sfb_cb.len()); + for (group_abs, group_cb) in abs.entries.iter().zip(sfb_cb.iter()) { + let mut abs_iter = group_abs.iter(); + let mut group_out: Vec = Vec::new(); + for &cb in group_cb { + if cb == ZERO_HCB { + continue; + } + let abs_entry = abs_iter + .next() + .ok_or(Error::ScaleFactorAccumulatorInvalid)?; + let entry = match (abs_entry, cb) { + (AbsoluteScaleFactorEntry::IsPos(cur), cb) if is_intensity(cb) => { + let delta = i32::from(*cur) - last_is; + if !(-60..=60).contains(&delta) { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + last_is = i32::from(*cur); + ScaleFactorEntry::Intensity(delta as i8) + } + (AbsoluteScaleFactorEntry::NoiseNrg(cur), cb) if is_noise(cb) => { + if noise_pcm_flag { + let delta = *cur - last_nrg; + if !(0..=511).contains(&delta) { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + noise_pcm_flag = false; + last_nrg = *cur; + ScaleFactorEntry::NoisePcm(delta as u16) + } else { + let delta = *cur - last_nrg; + if !(-60..=60).contains(&delta) { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + last_nrg = *cur; + ScaleFactorEntry::NoiseDpcm(delta as i8) + } + } + (AbsoluteScaleFactorEntry::Sf(cur), cb) if !is_intensity(cb) && !is_noise(cb) => { + let delta = i32::from(*cur) - last_sf; + if !(-60..=60).contains(&delta) { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + last_sf = i32::from(*cur); + ScaleFactorEntry::Dpcm(delta as i8) + } + _ => return Err(Error::ScaleFactorAccumulatorInvalid), + }; + group_out.push(entry); + } + if abs_iter.next().is_some() { + return Err(Error::ScaleFactorAccumulatorInvalid); + } + out.push(group_out); + } + Ok(ScaleFactorData { entries: out }) +} + +// ============================================================================= +// Error-resilient `scale_factor_data()` — Table 4.53 RVLC branch (§4.6.16.2) +// ============================================================================= +// +// When the GASpecificConfig sets `aacScalefactorDataResilienceFlag == 1`, +// `scale_factor_data()` takes the RVLC branch: the Table 4.A.1 Huffman +// codebook is replaced by the Table 4.166 symmetric RVLC codebook (see +// [`crate::rvlc`]) and three extra wire fields wrap the band loop so a +// decoder can recover from bit errors by decoding backwards: +// +// * `sf_concealment` (1 bit) — concealment hint, decode-irrelevant +// for an error-free stream (§4.6.16.2.2). +// * `rev_global_gain` (8 bits) — the *last* scalefactor, the start +// value for backward DPCM decoding. +// * `length_of_rvlc_sf` (11 bits if `EIGHT_SHORT_SEQUENCE` else 9) — +// the bit length of the RVLC part (the band loop + the optional +// `dpcm_is_last_position`), used to seek to the backward start. +// * `sf_escapes_present` (1 bit) + `length_of_rvlc_escapes` (8 bits) +// — the optional escape sub-stream, present iff any band's RVLC +// delta reached the ESC_FLAG (`±7`). +// * `dpcm_is_last_position` (RVLC, present iff intensity used) — the +// symmetric backward seed for the intensity-position track. +// * `dpcm_noise_last_position` (9 bits, present iff PNS used) — the +// symmetric backward seed for the PNS-energy track. +// +// Forward decoding is the focus here. Per §4.6.2.3.2, "the decoding +// process of the RVLC words is the same as for the Huffman +// codewords" — so once the RVLC deltas are recovered (and folded with +// their escapes), the *same* [`accumulate`] three-track DPCM forward +// pass reconstructs the absolute scalefactors. The `rev_global_gain` +// / `dpcm_*_last_position` seeds and the `length_of_*` fields are the +// backward-recovery scaffolding; this module surfaces them verbatim +// (and validates the two length fields against the bits actually +// consumed, an in-band conformance check) so a future recovery path +// can use them, but forward decode keys off `global_gain` exactly as +// the non-resilient branch does. +// +// Escape folding (§4.6.16.2.1): a base RVLC delta of `+7` means the +// true delta is `+7 + esc`; a base delta of `-7` means `-7 - esc`, +// where `esc` is the Table 4.168 escape magnitude. The escapes are a +// *separate pass* over the same band walk, after the whole RVLC part. + +/// A parsed error-resilient `scale_factor_data()` block — Table 4.53 +/// RVLC branch (`aacScalefactorDataResilienceFlag == 1`). +/// +/// `data` carries the *reconstructed* per-band DPCM records (RVLC base +/// delta with any escape already folded in), so it feeds [`accumulate`] +/// unchanged. The remaining fields are the §4.6.16.2 backward-decoding +/// scaffolding, surfaced verbatim. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ErScaleFactorData { + /// `sf_concealment` (1 bit) — concealment hint; not needed to + /// decode an error-free stream. + pub sf_concealment: bool, + /// `rev_global_gain` (8 bits) — last scalefactor, the backward + /// DPCM start value. + pub rev_global_gain: u8, + /// The reconstructed per-band records (escapes folded in), + /// identical in shape to the non-resilient + /// [`ScaleFactorData`] so [`accumulate`] consumes it directly. + pub data: ScaleFactorData, + /// `dpcm_is_last_position` — backward seed for the intensity + /// track. `Some` iff at least one intensity band was present. + pub dpcm_is_last_position: Option, + /// `dpcm_noise_last_position` (9-bit `uimsbf`) — backward seed + /// for the PNS track. `Some` iff at least one PNS band was + /// present. + pub dpcm_noise_last_position: Option, +} + +/// `length_of_rvlc_sf` field width — 11 bits for +/// `EIGHT_SHORT_SEQUENCE`, 9 bits otherwise (§4.6.16.2.2). +fn length_of_rvlc_sf_bits(window_sequence: WindowSequence) -> u32 { + if window_sequence.is_eight_short() { + 11 + } else { + 9 + } +} + +/// `length_of_rvlc_escapes` field width — always 8 bits +/// (§4.6.16.2.2). +const LENGTH_OF_RVLC_ESCAPES_BITS: u32 = 8; + +/// Fold a Table 4.168 escape magnitude into a base RVLC `±ESC_FLAG` +/// delta (§4.6.16.2.1): a positive base recovers `+7 + esc`, a +/// negative base recovers `-7 - esc`. Both extremes stay within the +/// `-60..=+60` DPCM range, so the result fits `i8`. +fn fold_escape(base: i8, esc_magnitude: u8) -> i8 { + if base >= 0 { + crate::rvlc::RVLC_ESC_FLAG + esc_magnitude as i8 + } else { + -crate::rvlc::RVLC_ESC_FLAG - esc_magnitude as i8 + } +} + +impl ErScaleFactorData { + /// Parse an error-resilient `scale_factor_data()` from `reader` + /// (Table 4.53, RVLC branch). + /// + /// * `reader` — positioned at the first bit of the block (the + /// `sf_concealment` flag). + /// * `sfb_cb` — the per-`(g, sfb)` codebook map from + /// [`section_data`](crate::section_data). + /// * `window_sequence` — selects the `length_of_rvlc_sf` field + /// width (11 vs 9 bits). + /// + /// The two `length_of_*` fields are validated against the bits + /// actually consumed; a mismatch surfaces + /// [`Error::RvlcScaleFactorDataInvalid`] (an in-band conformance + /// check). A forbidden RVLC codeword surfaces + /// [`Error::RvlcForbiddenCodeword`]; reader underflow surfaces + /// [`Error::UnexpectedEnd`]. + pub fn parse( + reader: &mut BitReader<'_>, + sfb_cb: &[Vec], + window_sequence: WindowSequence, + ) -> Result { + let sf_concealment = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)? != 0; + let rev_global_gain = reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)? as u8; + let len_rvlc_sf = u64::from( + reader + .read_u32(length_of_rvlc_sf_bits(window_sequence)) + .map_err(|_| Error::UnexpectedEnd)?, + ); + + // ---- RVLC part (Table 4.53): base deltas + ESC bookkeeping. + let rvlc_start = reader.bit_position(); + let mut intensity_used = false; + let mut noise_used = false; + // base[g] mirrors entries[g]; esc_band flags which records + // need an escape fold in the second pass. + let mut base: Vec> = Vec::with_capacity(sfb_cb.len()); + // `(group, index_within_group)` of each record that is at + // ESC_FLAG and must read an escape (in band-walk order). The + // first-PNS-PCM record is never escaped. + let mut esc_records: Vec<(usize, usize)> = Vec::new(); + for (g, group) in sfb_cb.iter().enumerate() { + let mut group_entries: Vec = Vec::new(); + for &cb in group { + if cb == ZERO_HCB { + continue; + } + let idx_in_group = group_entries.len(); + let entry = if is_intensity(cb) { + intensity_used = true; + let d = crate::rvlc::rvlc_decode(reader)?; + if d.abs() == crate::rvlc::RVLC_ESC_FLAG { + esc_records.push((g, idx_in_group)); + } + ScaleFactorEntry::Intensity(d) + } else if is_noise(cb) { + if !noise_used { + noise_used = true; + let pcm = reader + .read_u32(NOISE_PCM_BITS) + .map_err(|_| Error::UnexpectedEnd)? + as u16; + ScaleFactorEntry::NoisePcm(pcm) + } else { + let d = crate::rvlc::rvlc_decode(reader)?; + if d.abs() == crate::rvlc::RVLC_ESC_FLAG { + esc_records.push((g, idx_in_group)); + } + ScaleFactorEntry::NoiseDpcm(d) + } + } else { + let d = crate::rvlc::rvlc_decode(reader)?; + if d.abs() == crate::rvlc::RVLC_ESC_FLAG { + esc_records.push((g, idx_in_group)); + } + ScaleFactorEntry::Dpcm(d) + }; + group_entries.push(entry); + } + base.push(group_entries); + } + + // `dpcm_is_last_position` (RVLC) closes the RVLC part if any + // intensity band was present. + let mut is_last_base: Option = None; + if intensity_used { + let d = crate::rvlc::rvlc_decode(reader)?; + is_last_base = Some(d); + } + + // The RVLC part length must match the transmitted field. + let rvlc_consumed = reader.bit_position() - rvlc_start; + if rvlc_consumed != len_rvlc_sf { + return Err(Error::RvlcScaleFactorDataInvalid); + } + + // ---- Escape part (Table 4.53): optional second pass. + let sf_escapes_present = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)? != 0; + let mut is_last_esc: Option = None; + if sf_escapes_present { + let len_escapes = u64::from( + reader + .read_u32(LENGTH_OF_RVLC_ESCAPES_BITS) + .map_err(|_| Error::UnexpectedEnd)?, + ); + let esc_start = reader.bit_position(); + // Read one escape per recorded ESC_FLAG band, in walk order, + // and fold it into the base delta. + for &(g, i) in &esc_records { + let mag = crate::rvlc::rvlc_esc_decode(reader)?; + let folded = match base[g][i] { + ScaleFactorEntry::Dpcm(d) => ScaleFactorEntry::Dpcm(fold_escape(d, mag)), + ScaleFactorEntry::Intensity(d) => { + ScaleFactorEntry::Intensity(fold_escape(d, mag)) + } + ScaleFactorEntry::NoiseDpcm(d) => { + ScaleFactorEntry::NoiseDpcm(fold_escape(d, mag)) + } + // A NoisePcm record is never recorded as an escape. + ScaleFactorEntry::NoisePcm(_) => { + return Err(Error::RvlcScaleFactorDataInvalid); + } + }; + base[g][i] = folded; + } + // `dpcm_is_last_position` escape closes the escape part. + if let Some(d) = is_last_base { + if d.abs() == crate::rvlc::RVLC_ESC_FLAG { + let mag = crate::rvlc::rvlc_esc_decode(reader)?; + is_last_esc = Some(mag); + } + } + let esc_consumed = reader.bit_position() - esc_start; + if esc_consumed != len_escapes { + return Err(Error::RvlcScaleFactorDataInvalid); + } + } + + // ---- PNS backward seed. + // + // Table 4.53 resets `noise_used = 0` immediately before + // `sf_escapes_present` and re-derives it inside the escape + // loop's `if (!noise_used)` arm. The terminal + // `if (noise_used) dpcm_noise_last_position` therefore fires + // *only* when both a PNS band is present **and** + // `sf_escapes_present == 1` (the escape loop — and its + // `noise_used = 1` — is wholly inside `if (sf_escapes_present)`). + // A PNS frame with no escapes carries no `dpcm_noise_last_position`. + let dpcm_noise_last_position = if noise_used && sf_escapes_present { + Some( + reader + .read_u32(NOISE_PCM_BITS) + .map_err(|_| Error::UnexpectedEnd)? as u16, + ) + } else { + None + }; + + // Fold the intensity-last backward seed (with its escape). + let dpcm_is_last_position = is_last_base.map(|d| { + let folded = match is_last_esc { + Some(mag) => fold_escape(d, mag), + None => d, + }; + i16::from(folded) + }); + + Ok(ErScaleFactorData { + sf_concealment, + rev_global_gain, + data: ScaleFactorData { entries: base }, + dpcm_is_last_position, + dpcm_noise_last_position, + }) + } + + /// Encode an error-resilient `scale_factor_data()` onto `writer`, + /// the inverse of [`ErScaleFactorData::parse`]. + /// + /// The records in `self.data` carry the *final* DPCM deltas + /// (escapes already folded). The writer re-splits each delta whose + /// magnitude exceeds `±6` into a base `±7` RVLC codeword plus a + /// Table 4.168 escape magnitude, regenerates the `length_of_*` + /// fields from the bits emitted, and sets `sf_escapes_present` + /// when any escape is needed. + /// + /// Returns [`Error::RvlcScaleFactorDataInvalid`] on a structural + /// mismatch (record/codebook shape, escape magnitude out of the + /// Table 4.168 domain, or a backward-seed field overflow). + pub fn write( + &self, + writer: &mut BitWriter, + sfb_cb: &[Vec], + window_sequence: WindowSequence, + ) -> Result<()> { + if self.data.entries.len() != sfb_cb.len() { + return Err(Error::RvlcScaleFactorDataInvalid); + } + + writer.write_u32(u32::from(self.sf_concealment), 1); + writer.write_u32(u32::from(self.rev_global_gain), 8); + + // Build the RVLC part into a scratch writer first so its bit + // length is known for `length_of_rvlc_sf`. Collect the escape + // magnitudes (in walk order) for the second pass. + let mut rvlc_part = BitWriter::new(); + let mut escapes: Vec = Vec::new(); + let mut intensity_used = false; + let mut noise_used = false; + + for (group_entries, group_cb) in self.data.entries.iter().zip(sfb_cb.iter()) { + let mut entry_iter = group_entries.iter(); + for &cb in group_cb { + if cb == ZERO_HCB { + continue; + } + let entry = entry_iter.next().ok_or(Error::RvlcScaleFactorDataInvalid)?; + match (entry, cb) { + (ScaleFactorEntry::Intensity(d), cb) if is_intensity(cb) => { + intensity_used = true; + write_rvlc_delta(&mut rvlc_part, *d, &mut escapes)?; + } + (ScaleFactorEntry::NoisePcm(pcm), cb) if is_noise(cb) => { + if noise_used { + return Err(Error::RvlcScaleFactorDataInvalid); + } + if u32::from(*pcm) >= (1u32 << NOISE_PCM_BITS) { + return Err(Error::RvlcScaleFactorDataInvalid); + } + noise_used = true; + rvlc_part.write_u32(u32::from(*pcm), NOISE_PCM_BITS); + } + (ScaleFactorEntry::NoiseDpcm(d), cb) if is_noise(cb) => { + if !noise_used { + return Err(Error::RvlcScaleFactorDataInvalid); + } + write_rvlc_delta(&mut rvlc_part, *d, &mut escapes)?; + } + (ScaleFactorEntry::Dpcm(d), cb) if !is_intensity(cb) && !is_noise(cb) => { + write_rvlc_delta(&mut rvlc_part, *d, &mut escapes)?; + } + _ => return Err(Error::RvlcScaleFactorDataInvalid), + } + } + if entry_iter.next().is_some() { + return Err(Error::RvlcScaleFactorDataInvalid); + } + } + + // `dpcm_is_last_position` (RVLC) closes the RVLC part. + let mut is_last_escape: Option = None; + match (intensity_used, self.dpcm_is_last_position) { + (true, Some(d)) => { + let d8 = i8::try_from(d).map_err(|_| Error::RvlcScaleFactorDataInvalid)?; + let mut tail: Vec = Vec::new(); + write_rvlc_delta(&mut rvlc_part, d8, &mut tail)?; + is_last_escape = tail.into_iter().next(); + } + (true, None) | (false, Some(_)) => { + // Intensity presence must agree with the seed presence. + return Err(Error::RvlcScaleFactorDataInvalid); + } + (false, None) => {} + } + + let len_rvlc_sf = rvlc_part.bit_position(); + let field_bits = length_of_rvlc_sf_bits(window_sequence); + if len_rvlc_sf >= (1u64 << field_bits) { + return Err(Error::RvlcScaleFactorDataInvalid); + } + writer.write_u32(len_rvlc_sf as u32, field_bits); + append_bits(writer, len_rvlc_sf, &rvlc_part.finish()); + + // ---- Escape part. + let any_escape = !escapes.is_empty() || is_last_escape.is_some(); + writer.write_u32(u32::from(any_escape), 1); + if any_escape { + let mut esc_part = BitWriter::new(); + for &mag in &escapes { + let (len, cw) = crate::rvlc::rvlc_esc_encode(mag)?; + esc_part.write_u32(cw, u32::from(len)); + } + if let Some(mag) = is_last_escape { + let (len, cw) = crate::rvlc::rvlc_esc_encode(mag)?; + esc_part.write_u32(cw, u32::from(len)); + } + let len_escapes = esc_part.bit_position(); + if len_escapes >= (1u64 << LENGTH_OF_RVLC_ESCAPES_BITS) { + return Err(Error::RvlcScaleFactorDataInvalid); + } + writer.write_u32(len_escapes as u32, LENGTH_OF_RVLC_ESCAPES_BITS); + append_bits(writer, len_escapes, &esc_part.finish()); + } + + // ---- PNS backward seed. + // + // Per Table 4.53 the terminal `dpcm_noise_last_position` is + // present only when a PNS band exists **and** + // `sf_escapes_present == 1` (the spec re-derives `noise_used` + // inside the escape loop, which only runs when escapes are + // present). So the seed must be `Some` exactly when + // `noise_used && any_escape`, and `None` otherwise — any other + // combination cannot be represented on the wire. + let expect_noise_seed = noise_used && any_escape; + match (expect_noise_seed, self.dpcm_noise_last_position) { + (true, Some(pcm)) => { + if u32::from(pcm) >= (1u32 << NOISE_PCM_BITS) { + return Err(Error::RvlcScaleFactorDataInvalid); + } + writer.write_u32(u32::from(pcm), NOISE_PCM_BITS); + } + (false, None) => {} + _ => return Err(Error::RvlcScaleFactorDataInvalid), + } + Ok(()) + } +} + +/// Split a final DPCM delta into an RVLC base codeword plus (if the +/// magnitude exceeds `±6`) an escape magnitude pushed onto `escapes`. +/// The base RVLC codeword is emitted onto `part`. +fn write_rvlc_delta(part: &mut BitWriter, delta: i8, escapes: &mut Vec) -> Result<()> { + let flag = crate::rvlc::RVLC_ESC_FLAG; // 7 + if delta.abs() < flag { + // Fits the RVLC codebook directly (no escape, magnitude ≤ 6). + let (len, cw) = crate::rvlc::rvlc_encode(delta)?; + part.write_u32(cw, u32::from(len)); + } else { + // Magnitude ≥ 7: base codeword is the signed ESC_FLAG, the + // remainder is the escape magnitude. + let (base, mag) = if delta >= 0 { + (flag, (delta - flag) as u8) + } else { + (-flag, (-delta - flag) as u8) + }; + let (len, cw) = crate::rvlc::rvlc_encode(base)?; + part.write_u32(cw, u32::from(len)); + if mag as usize >= crate::rvlc::RVLC_ESC_NUM_ENTRIES { + return Err(Error::RvlcScaleFactorDataInvalid); + } + escapes.push(mag); + } + Ok(()) +} + +/// Append the first `total` bits of `bytes` (a zero-padded +/// `BitWriter::finish()` output) to `dst`, MSB-first, preserving bit +/// position. The trailing zero pad bits past `total` are ignored. +fn append_bits(dst: &mut BitWriter, total: u64, bytes: &[u8]) { + let mut remaining = total; + let mut byte_idx = 0usize; + while remaining >= 8 { + dst.write_u32(u32::from(bytes[byte_idx]), 8); + byte_idx += 1; + remaining -= 8; + } + if remaining > 0 { + // The final partial byte is MSB-aligned in the finished buffer. + let last = bytes[byte_idx]; + let value = u32::from(last) >> (8 - remaining); + dst.write_u32(value, remaining as u32); + } +} + +/// Internal: `cb` is an intensity codebook (14 or 15). +fn is_intensity(cb: u8) -> bool { + cb == INTENSITY_HCB || cb == INTENSITY_HCB2 +} + +/// Internal: `cb` is the PNS codebook (13). +fn is_noise(cb: u8) -> bool { + cb == NOISE_HCB +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Spot-check a handful of Table 4.A.1 rows the way the spec + /// presents them: `index 60 → 1 bit, codeword 0`; `index 59 → + /// 3 bits, codeword 4`; `index 61 → 4 bits, codeword 0xa`. + #[test] + fn hcod_sf_table_known_rows() { + assert_eq!(HCOD_SF[60], (1, 0x0)); + assert_eq!(HCOD_SF[59], (3, 0x4)); + assert_eq!(HCOD_SF[61], (4, 0xa)); + assert_eq!(HCOD_SF[0], (18, 0x3ffe8)); + assert_eq!(HCOD_SF[120], (19, 0x7fff3)); + } + + /// The codebook is prefix-free (no codeword is a prefix of any + /// other). Verified once here as a regression guard against typos + /// in the Table 4.A.1 transcription above. + #[test] + fn hcod_sf_table_is_prefix_free() { + for (i, &(li, vi)) in HCOD_SF.iter().enumerate() { + for (j, &(lj, vj)) in HCOD_SF.iter().enumerate() { + if i == j || lj < li { + continue; + } + let lo = u32::from(lj - li); + let prefix = vj >> lo; + assert_ne!( + prefix, vi, + "entry {} (L={}, v={:x}) is prefix of entry {} (L={}, v={:x})", + i, li, vi, j, lj, vj + ); + } + } + } + + /// `index_offset = -60`: encoding `dpcm = 0` selects index 60, + /// the single-bit `0` codeword. + #[test] + fn encode_dpcm_zero_is_single_bit() { + let (len, cw) = hcod_sf_encode(0).unwrap(); + assert_eq!(len, 1); + assert_eq!(cw, 0); + } + + /// Boundary values: `-60` and `+60` are the endpoints of the + /// DPCM range; anything outside is rejected. + #[test] + fn encode_dpcm_boundaries() { + assert!(hcod_sf_encode(-60).is_ok()); + assert!(hcod_sf_encode(60).is_ok()); + assert_eq!( + hcod_sf_encode(-61), + Err(Error::ScaleFactorDataEncodeInvalid) + ); + assert_eq!(hcod_sf_encode(61), Err(Error::ScaleFactorDataEncodeInvalid)); + } + + /// Every entry of the table round-trips: encode then decode + /// recovers the original DPCM value. + #[test] + fn hcod_sf_roundtrip_every_entry() { + for dpcm in -60i8..=60 { + let (len, cw) = hcod_sf_encode(dpcm).unwrap(); + let mut bw = BitWriter::new(); + bw.write_u32(cw, u32::from(len)); + let bits_written = bw.bit_position(); + let buf = bw.finish(); + let mut br = BitReader::new(&buf); + let recovered = hcod_sf_decode(&mut br).unwrap(); + assert_eq!(recovered, dpcm); + // Reader must consume exactly `len` bits. + assert_eq!(br.bit_position(), bits_written); + } + } + + // ------------------------------------------------------------------------- + // Error-resilient (RVLC) `scale_factor_data()` — Table 4.53 / §4.6.16.2 + // ------------------------------------------------------------------------- + + /// Round-trip an ER block (no intensity / no PNS, all spectrum + /// bands within the RVLC ±6 range — no escapes) and confirm the + /// writer regenerates exactly what the parser read back. + #[test] + fn er_roundtrip_spectrum_only_no_escapes() { + // Two groups, codebook 2 (spectrum) on every band. + let sfb_cb = vec![vec![2u8, 2, 2], vec![2u8, 2]]; + let block = ErScaleFactorData { + sf_concealment: true, + rev_global_gain: 137, + data: ScaleFactorData { + entries: vec![ + vec![ + ScaleFactorEntry::Dpcm(0), + ScaleFactorEntry::Dpcm(3), + ScaleFactorEntry::Dpcm(-5), + ], + vec![ScaleFactorEntry::Dpcm(6), ScaleFactorEntry::Dpcm(-6)], + ], + }, + dpcm_is_last_position: None, + dpcm_noise_last_position: None, + }; + let mut w = BitWriter::new(); + block + .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) + .unwrap(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong).unwrap(); + assert_eq!(parsed, block); + } + + /// A delta whose magnitude exceeds ±6 must round-trip through the + /// base-`±7` + escape split (§4.6.16.2.1) and set + /// `sf_escapes_present`. + #[test] + fn er_roundtrip_with_escapes() { + let sfb_cb = vec![vec![3u8, 3, 3]]; + let block = ErScaleFactorData { + sf_concealment: false, + rev_global_gain: 200, + data: ScaleFactorData { + entries: vec![vec![ + ScaleFactorEntry::Dpcm(7), // +7 + 0 escape + ScaleFactorEntry::Dpcm(-20), // -7 - 13 escape + ScaleFactorEntry::Dpcm(60), // +7 + 53 escape (max) + ]], + }, + dpcm_is_last_position: None, + dpcm_noise_last_position: None, + }; + let mut w = BitWriter::new(); + block + .write(&mut w, &sfb_cb, WindowSequence::EightShort) + .unwrap(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::EightShort).unwrap(); + assert_eq!(parsed, block); + } + + /// An ER block with both intensity and PNS bands round-trips, + /// exercising `dpcm_is_last_position`, the first-PNS 9-bit PCM + /// seed, a subsequent PNS RVLC delta, and `dpcm_noise_last_position`. + /// The Table 4.53 terminal `dpcm_noise_last_position` is present + /// only when `sf_escapes_present == 1`, so this block carries an + /// escape (`NoiseDpcm(10)` → base +7 + magnitude 3). + #[test] + fn er_roundtrip_intensity_and_pns() { + // band codebooks: spectrum(2), intensity(15), pns(13), pns(13). + let sfb_cb = vec![vec![2u8, INTENSITY_HCB2, NOISE_HCB, NOISE_HCB]]; + let block = ErScaleFactorData { + sf_concealment: true, + rev_global_gain: 100, + data: ScaleFactorData { + entries: vec![vec![ + ScaleFactorEntry::Dpcm(2), + ScaleFactorEntry::Intensity(-3), + ScaleFactorEntry::NoisePcm(0x1a5), // 9-bit PCM seed + ScaleFactorEntry::NoiseDpcm(10), // escape → sf_escapes_present + ]], + }, + dpcm_is_last_position: Some(5), + dpcm_noise_last_position: Some(0x0c2), + }; + let mut w = BitWriter::new(); + block + .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) + .unwrap(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong).unwrap(); + assert_eq!(parsed, block); + } + + /// A PNS frame whose deltas all fit the RVLC ±6 range emits no + /// escapes (`sf_escapes_present == 0`), so per Table 4.53 the + /// terminal `dpcm_noise_last_position` is **absent** — the parser + /// recovers `None` for it. The writer rejects a `Some` seed in + /// that escapeless case as unrepresentable. + #[test] + fn er_pns_without_escapes_has_no_noise_seed() { + let sfb_cb = vec![vec![NOISE_HCB, NOISE_HCB]]; + let block = ErScaleFactorData { + sf_concealment: false, + rev_global_gain: 80, + data: ScaleFactorData { + entries: vec![vec![ + ScaleFactorEntry::NoisePcm(0x010), + ScaleFactorEntry::NoiseDpcm(3), // within ±6 → no escape + ]], + }, + dpcm_is_last_position: None, + dpcm_noise_last_position: None, + }; + let mut w = BitWriter::new(); + block + .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) + .unwrap(); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong).unwrap(); + assert_eq!(parsed, block); + assert_eq!(parsed.dpcm_noise_last_position, None); + + // A Some seed in the escapeless case is unrepresentable. + let bad = ErScaleFactorData { + dpcm_noise_last_position: Some(0x0aa), + ..block + }; + let mut bw = BitWriter::new(); + assert!(matches!( + bad.write(&mut bw, &sfb_cb, WindowSequence::OnlyLong), + Err(Error::RvlcScaleFactorDataInvalid) + )); + } + + /// The headline §4.6.2.3.2 equivalence: an RVLC-coded scalefactor + /// stream and the Huffman-coded stream carrying the *same* DPCM + /// deltas accumulate to identical absolute scalefactors. This is + /// what "the decoding process of the RVLC words is the same as + /// for the Huffman codewords" means in practice. + #[test] + fn er_forward_decode_matches_huffman_path() { + let sfb_cb = vec![vec![2u8, 2, 2, 2]]; + let global_gain = 120u8; + // Identical DPCM records for both paths. + let entries = vec![vec![ + ScaleFactorEntry::Dpcm(0), + ScaleFactorEntry::Dpcm(5), + ScaleFactorEntry::Dpcm(-30), // forces an escape on the RVLC side + ScaleFactorEntry::Dpcm(2), + ]]; + let sfd = ScaleFactorData { + entries: entries.clone(), + }; + + // Huffman path: write + parse + accumulate. + let mut hw = BitWriter::new(); + sfd.write(&mut hw, &sfb_cb).unwrap(); + let hbytes = hw.finish(); + let mut hr = BitReader::new(&hbytes); + let hsfd = ScaleFactorData::parse(&mut hr, &sfb_cb).unwrap(); + let habs = accumulate(&hsfd, &sfb_cb, global_gain).unwrap(); + + // RVLC path: write + parse the ER block, then accumulate the + // reconstructed records with the SAME global_gain. + let er = ErScaleFactorData { + sf_concealment: false, + rev_global_gain: 0, + data: ScaleFactorData { entries }, + dpcm_is_last_position: None, + dpcm_noise_last_position: None, + }; + let mut ew = BitWriter::new(); + er.write(&mut ew, &sfb_cb, WindowSequence::OnlyLong) + .unwrap(); + let ebytes = ew.finish(); + let mut er_reader = BitReader::new(&ebytes); + let parsed_er = + ErScaleFactorData::parse(&mut er_reader, &sfb_cb, WindowSequence::OnlyLong).unwrap(); + let eabs = accumulate(&parsed_er.data, &sfb_cb, global_gain).unwrap(); + + assert_eq!(habs, eabs, "RVLC forward decode must equal Huffman path"); + } + + /// A corrupted RVLC bit pattern that lands on a Table 4.167 + /// forbidden codeword surfaces the in-band error-detection event. + #[test] + fn er_forbidden_codeword_is_detected() { + // Forbidden codeword (6 bits, 0b110010) followed by padding. + let mut w = BitWriter::new(); + w.write_u32(0, 1); // sf_concealment + w.write_u32(0, 8); // rev_global_gain + w.write_u32(6, 9); // length_of_rvlc_sf == 6 bits + w.write_u32(0b110010, 6); // the forbidden codeword + let bytes = w.finish(); + let sfb_cb = vec![vec![2u8]]; + let mut r = BitReader::new(&bytes); + assert!(matches!( + ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong), + Err(Error::RvlcForbiddenCodeword) + )); + } + + /// A `length_of_rvlc_sf` that disagrees with the bits actually + /// consumed is an in-band conformance failure. + #[test] + fn er_length_mismatch_rejected() { + // Build a valid block then corrupt the length field. + let sfb_cb = vec![vec![2u8, 2]]; + let block = ErScaleFactorData { + sf_concealment: false, + rev_global_gain: 50, + data: ScaleFactorData { + entries: vec![vec![ScaleFactorEntry::Dpcm(1), ScaleFactorEntry::Dpcm(-1)]], + }, + dpcm_is_last_position: None, + dpcm_noise_last_position: None, + }; + let mut w = BitWriter::new(); + block + .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) + .unwrap(); + let mut bytes = w.finish(); + // The length_of_rvlc_sf field sits at bit offset 9 (after the + // 1-bit sf_concealment + 8-bit rev_global_gain), 9 bits wide. + // Flip its low bit to desync the consumed-bit check. + // bits 9..18 → spans bytes 1 (bits 1..8) and 2 (bits 0..1). + bytes[2] ^= 0x40; // flip a bit inside the length field region + let mut r = BitReader::new(&bytes); + // Either a length mismatch or a forbidden codeword — both are + // valid in-band rejections of the corrupted stream. + let res = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong); + assert!(res.is_err(), "corrupted length field must be rejected"); + } +} diff --git a/crates/vendor/oxideav-aac/src/section_data.rs b/crates/vendor/oxideav-aac/src/section_data.rs new file mode 100644 index 00000000..51b97cbf --- /dev/null +++ b/crates/vendor/oxideav-aac/src/section_data.rs @@ -0,0 +1,631 @@ +//! `section_data()` parser — ISO/IEC 14496-3 §4.4.6 / ISO/IEC +//! 13818-7 §6.3 Table 17. +//! +//! `section_data()` is the second tool inside +//! `individual_channel_stream()` (after `global_gain` and +//! `ics_info()`, before `scale_factor_data()`). It assigns one +//! Huffman codebook (`sect_cb`) to each *run* of scalefactor bands +//! (a "section") within each window group, using run-length coding +//! with an escape mechanism for sections longer than the field can +//! hold in one increment. +//! +//! This parser depends only on values already produced by +//! [`crate::ics_info::IcsInfo`]: +//! +//! * `num_window_groups` — the outer loop bound. +//! * `max_sfb` — the inner loop terminator (`while (k < max_sfb)`). +//! * `window_sequence == EIGHT_SHORT_SEQUENCE` — selects the +//! 3-bit (`sect_esc_val = 7`) versus 5-bit (`sect_esc_val = 31`) +//! `sect_len_incr` field width. +//! +//! Crucially it carries **no Huffman codebook of its own**: every +//! field is fixed-width (`sect_cb` is 4 bits, `sect_len_incr` is +//! 3 or 5 bits), so the parser is a pure bit-walker. The Huffman +//! codebooks the `sect_cb` values *select* (the spectrum books 1-11 +//! plus the scalefactor book) are consumed by later tools +//! (`scale_factor_data()`, `spectral_data()`), not here. +//! +//! ## Run-length escape coding (Table 17) +//! +//! For each window group `g`, starting at scalefactor band `k = 0`: +//! +//! 1. Read `sect_cb[g][i]` (4 bits). +//! 2. Set `sect_len = 0`. Read `sect_len_incr` (3 or 5 bits). +//! While the value read equals `sect_esc_val`, add `sect_esc_val` +//! to `sect_len` and read the next `sect_len_incr`. When a +//! non-escape value is read, add it to `sect_len` and stop. +//! 3. The section covers bands `[k, k + sect_len)`. Record +//! `sect_start[g][i] = k`, `sect_end[g][i] = k + sect_len`, and +//! `sfb_cb[g][sfb] = sect_cb[g][i]` for every band in the run. +//! 4. Advance `k += sect_len`, `i += 1`. Repeat while `k < max_sfb`. +//! +//! `num_sec[g]` is the final value of `i` for the group. +//! +//! ## What is *not* in this round +//! +//! * No Huffman decode. The codebook indices are surfaced verbatim; +//! the spectrum / scalefactor decoders consume them later. +//! * No `is_intensity()` / PNS classification. The +//! [`Codebook`] enum exposes the semantic role of each value +//! (`Intensity`, `IntensityInPhase`, `Noise`, `Esc`, …) for the +//! benefit of `scale_factor_data()` / `spectral_data()`, but +//! `section_data()` itself only records the raw `u8`. +//! * No validation that `sfb_cb` is fully populated to `max_sfb` in +//! pathological streams — the parser surfaces a +//! [`Error::SectionDataOverrun`] when a section would extend past +//! `max_sfb` (which a conforming encoder never emits) and +//! otherwise trusts the run lengths. +//! +//! ## Encode side (Phase 2: first writer primitive) +//! +//! [`SectionData::write`] is the inverse of [`SectionData::parse`]: +//! given the same `window_sequence` / `num_window_groups` / `max_sfb` +//! context the parser was invoked with, it emits the bit-exact +//! Table 17 syntax that the parser reads back. This is the AAC +//! crate's first encoder primitive — a bounded syntax-element +//! writer with no Huffman tables of its own, so the surface lives +//! entirely in the fixed-width `sect_cb` / `sect_len_incr` field +//! pair. +//! +//! The encode-side rule for the §6.3 escape is the inverse of the +//! decode-side accumulation: +//! +//! 1. While the remaining `sect_len` is **greater than or equal to** +//! `sect_esc_val`, emit a `sect_len_incr` of `sect_esc_val` and +//! subtract `sect_esc_val` from the remaining length. The +//! "greater than or equal to" boundary is what forces a trailing +//! non-escape `sect_len_incr == 0` after a length that lands +//! exactly on a multiple of `sect_esc_val` — the parser loop +//! keeps reading while `incr == sect_esc_val`, so the writer +//! must terminate the run with a non-escape value (which can be +//! zero) so the parser sees a `break` condition. +//! 2. Emit the residual `sect_len` (which is now strictly less than +//! `sect_esc_val`) as a single non-escape `sect_len_incr`. +//! +//! [`SectionData::write`] validates that the supplied sections form +//! a contiguous run `0 → max_sfb` per group and that every +//! `sect_cb` and `sect_len` fits the wire field; encoder bugs upstream +//! that violate either invariant surface as +//! [`Error::SectionDataEncodeInvalid`]. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::ics_info::WindowSequence; +use crate::{Error, Result}; + +/// `ZERO_HCB` — section carries neither scalefactor nor spectral +/// data; the band is silent. ISO/IEC 13818-7 §9.2.2 / §11.3.2. +pub const ZERO_HCB: u8 = 0; + +/// `FIRST_PAIR_HCB` — the first codebook whose dimension is 2 +/// (a 2-tuple); books `< FIRST_PAIR_HCB` are 4-tuple (QUAD) books. +/// ISO/IEC 13818-7 §9.2.2. +pub const FIRST_PAIR_HCB: u8 = 5; + +/// `ESC_HCB` — the spectrum escape codebook (book 11). Values whose +/// magnitude reaches the LAV use the §9.3 escape sequence for the +/// actual coefficient. ISO/IEC 13818-7 §9.2.2. +pub const ESC_HCB: u8 = 11; + +/// `NOISE_HCB` — Perceptual Noise Substitution codebook (value 13). +/// An MPEG-4 extension (ISO/IEC 14496-3; the base ISO/IEC 13818-7 +/// Table 59 marks value 13 *reserved* and adds PNS in its Annex B +/// Table B.1 extended `scale_factor_data()`). When a band's +/// `sfb_cb == NOISE_HCB` the band is noise-filled and its +/// "scalefactor" position carries the PNS energy delta instead. +pub const NOISE_HCB: u8 = 13; + +/// `INTENSITY_HCB2` — out-of-phase intensity-stereo codebook +/// (value 14). ISO/IEC 13818-7 §9.2.2 / Table 59. +pub const INTENSITY_HCB2: u8 = 14; + +/// `INTENSITY_HCB` — in-phase intensity-stereo codebook (value 15). +/// ISO/IEC 13818-7 §9.2.2 / Table 59. +pub const INTENSITY_HCB: u8 = 15; + +/// Semantic classification of a 4-bit `sect_cb` value, per ISO/IEC +/// 13818-7 Table 59 (extended by the MPEG-4 PNS codebook 13). +/// +/// `section_data()` records the raw `u8` in [`Section::codebook`]; +/// this enum is a *view* over that value so downstream tools +/// (`scale_factor_data()` for the `is_intensity` / PNS branch, +/// `spectral_data()` for the dimension / signed / escape branch) +/// can dispatch without re-deriving the classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Codebook { + /// `0` — `ZERO_HCB`: silent band, no scalefactor, no spectrum. + Zero, + /// `1..=4` — 4-tuple (QUAD) spectrum book. `signed` is `false` + /// for books 1-2 (`unsigned_cb == 0`) and `true` for 3-4. + Quad { + /// Codebook number (1..=4). + number: u8, + /// `true` ⇔ the book is *unsigned* (`unsigned_cb[i] == 1`). + unsigned: bool, + }, + /// `5..=10` — 2-tuple (PAIR) spectrum book. + Pair { + /// Codebook number (5..=10). + number: u8, + /// `true` ⇔ the book is *unsigned* (`unsigned_cb[i] == 1`). + unsigned: bool, + }, + /// `11` — `ESC_HCB`: 2-tuple unsigned escape book. + Esc, + /// `12` — reserved (ISO/IEC 13818-7 Table 59). + Reserved12, + /// `13` — `NOISE_HCB`: Perceptual Noise Substitution (MPEG-4). + Noise, + /// `14` — `INTENSITY_HCB2`: out-of-phase intensity stereo. + IntensityOutOfPhase, + /// `15` — `INTENSITY_HCB`: in-phase intensity stereo. + IntensityInPhase, +} + +impl Codebook { + /// Classify a raw 4-bit `sect_cb` value (0..=15). + /// + /// `unsigned_cb[]` per ISO/IEC 13818-7 Table 59: books 1, 2 are + /// signed (`unsigned == false`); books 3, 4, 5*, 6*, 7, 8, 9, + /// 10, 11 are unsigned. (*Books 5 and 6 are 2-tuple signed in + /// Table 59 — see the per-number mapping below.) + pub fn from_value(value: u8) -> Self { + match value & 0x0f { + 0 => Codebook::Zero, + // QUAD books (dimension 4): 1, 2 signed; 3, 4 unsigned. + n @ 1..=4 => Codebook::Quad { + number: n, + unsigned: matches!(n, 3 | 4), + }, + // PAIR books (dimension 2): 5, 6 signed; 7, 8, 9, 10 + // unsigned. + n @ 5..=10 => Codebook::Pair { + number: n, + unsigned: matches!(n, 7..=10), + }, + 11 => Codebook::Esc, + 12 => Codebook::Reserved12, + 13 => Codebook::Noise, + 14 => Codebook::IntensityOutOfPhase, + 15 => Codebook::IntensityInPhase, + _ => unreachable!("masked to 0..=15"), + } + } + + /// `true` ⇔ this codebook is an intensity-stereo book + /// (`INTENSITY_HCB` or `INTENSITY_HCB2`). Mirrors the spec + /// `is_intensity()` helper used by `scale_factor_data()`. + pub fn is_intensity(self) -> bool { + matches!( + self, + Codebook::IntensityInPhase | Codebook::IntensityOutOfPhase + ) + } + + /// `true` ⇔ this is the PNS noise codebook (`NOISE_HCB`). + pub fn is_noise(self) -> bool { + matches!(self, Codebook::Noise) + } + + /// `true` ⇔ this is `ZERO_HCB` (band carries no data). + pub fn is_zero(self) -> bool { + matches!(self, Codebook::Zero) + } +} + +/// One contiguous run of scalefactor bands sharing a codebook, as +/// produced by Table 17. `start`/`end` are scalefactor-band indices +/// (`end` is one past the last band, matching `sect_end`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Section { + /// `sect_cb[g][i]` — the raw 4-bit codebook value for this run. + pub codebook: u8, + /// `sect_start[g][i]` — first scalefactor band in the section. + pub start: u8, + /// `sect_end[g][i]` — one past the last band (`start + + /// sect_len`). + pub end: u8, +} + +impl Section { + /// Length of the section in scalefactor bands (`sect_len`). + pub fn len(self) -> u8 { + self.end - self.start + } + + /// `true` ⇔ the section spans zero bands. A conforming encoder + /// never emits a zero-length section, but the accessor is + /// provided so the `clippy::len_without_is_empty` lint is + /// satisfied and callers can defensively check. + pub fn is_empty(self) -> bool { + self.end == self.start + } + + /// Semantic [`Codebook`] classification of [`Self::codebook`]. + pub fn codebook_kind(self) -> Codebook { + Codebook::from_value(self.codebook) + } +} + +/// Parsed `section_data()` for one `individual_channel_stream()`. +/// +/// The per-group section lists plus the flattened `sfb_cb[g][sfb]` +/// map are surfaced; `scale_factor_data()` (next round) consumes +/// `sfb_cb` to decide which bands carry a transmitted scalefactor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SectionData { + /// `sect[g]` — the ordered sections of window group `g`. The + /// outer index runs `0..num_window_groups`; `sect[g].len()` is + /// `num_sec[g]`. + pub sections: Vec>, + /// `sfb_cb[g][sfb]` — the codebook assigned to scalefactor band + /// `sfb` of group `g`, for `sfb in 0..max_sfb`. Flattened per + /// group; the outer index runs `0..num_window_groups`. + pub sfb_cb: Vec>, +} + +impl SectionData { + /// Parse a `section_data()` from the bit-reader. + /// + /// * `reader` — positioned immediately after `ics_info()` (well, + /// after `global_gain` + `ics_info()` in the full ICS, but + /// `section_data()` starts right where the caller leaves the + /// reader). + /// * `window_sequence` — from the surrounding `ics_info()`; + /// selects the 3-bit vs 5-bit `sect_len_incr` field. + /// * `num_window_groups` — from the surrounding `ics_info()` + /// derivations (`1` for long sequences). + /// * `max_sfb` — from the surrounding `ics_info()`. + /// + /// Returns [`Error::SectionDataOverrun`] if a section run would + /// extend past `max_sfb` (non-conforming stream), and + /// [`Error::UnexpectedEnd`] on bit-reader underflow. + pub fn parse( + reader: &mut BitReader<'_>, + window_sequence: WindowSequence, + num_window_groups: u8, + max_sfb: u8, + ) -> Result { + // Table 17: sect_esc_val and sect_len_incr field width. + let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { + ((1u32 << 3) - 1, 3u32) // 7, 3-bit field + } else { + ((1u32 << 5) - 1, 5u32) // 31, 5-bit field + }; + + let mut sections: Vec> = Vec::with_capacity(num_window_groups as usize); + let mut sfb_cb: Vec> = Vec::with_capacity(num_window_groups as usize); + + for _g in 0..num_window_groups { + let mut group_sections: Vec
= Vec::new(); + let mut group_sfb_cb: Vec = vec![ZERO_HCB; max_sfb as usize]; + + let mut k: u32 = 0; + let max = max_sfb as u32; + while k < max { + let sect_cb = read_u8(reader, 4)?; + + // sect_len accumulation with escape coding. + let mut sect_len: u32 = 0; + loop { + let incr = reader + .read_u32(len_bits) + .map_err(|_| Error::UnexpectedEnd)?; + if incr == sect_esc_val { + sect_len += sect_esc_val; + // Re-read another sect_len_incr. + continue; + } + sect_len += incr; + break; + } + + let start = k; + let end = k + sect_len; + if end > max { + return Err(Error::SectionDataOverrun); + } + for sfb in start..end { + group_sfb_cb[sfb as usize] = sect_cb; + } + group_sections.push(Section { + codebook: sect_cb, + start: start as u8, + end: end as u8, + }); + k = end; + } + + sections.push(group_sections); + sfb_cb.push(group_sfb_cb); + } + + Ok(SectionData { sections, sfb_cb }) + } + + /// Parse the error-resilient `section_data()` branch + /// (`aacSectionDataResilienceFlag == 1`, Table 4.52). + /// + /// Two differences from the non-resilient [`SectionData::parse`]: + /// + /// * `sect_cb[g][i]` is read as a **5-bit** field (so it can carry + /// the §4.6.16.4 virtual codebooks 16..=31, the per-band VCB11 + /// range derived from `ESC_HCB`) rather than 4 bits. + /// * The `sect_len_incr` escape loop only runs when + /// `sect_cb < 11 || (sect_cb > 11 && sect_cb < 16)`; for + /// `sect_cb == 11` (`ESC_HCB`) or `sect_cb >= 16` (a virtual + /// codebook) the section length is fixed at `sect_len_incr = 1` + /// (one band) with no field on the wire. This is the Table 4.52 + /// `else { sect_len_incr = 1; }` branch. + /// + /// The recovered `sfb_cb[g][sfb]` therefore carries the raw 5-bit + /// `sect_cb` value (which may exceed `0x0f`); downstream tools that + /// only understand the base §4.A.1 books must map a virtual `>= 16` + /// codebook back onto `ESC_HCB` before dispatching — the value is + /// preserved here so that mapping can stay one layer up. + /// + /// Returns [`Error::SectionDataOverrun`] on a run past `max_sfb` + /// and [`Error::UnexpectedEnd`] on bit-reader underflow. + pub fn parse_er( + reader: &mut BitReader<'_>, + window_sequence: WindowSequence, + num_window_groups: u8, + max_sfb: u8, + ) -> Result { + // Table 4.52: sect_esc_val / sect_len_incr field width are the + // same as the non-resilient branch; only sect_cb widens to 5 + // bits and the escape loop is gated by the codebook value. + let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { + ((1u32 << 3) - 1, 3u32) + } else { + ((1u32 << 5) - 1, 5u32) + }; + + let mut sections: Vec> = Vec::with_capacity(num_window_groups as usize); + let mut sfb_cb: Vec> = Vec::with_capacity(num_window_groups as usize); + + for _g in 0..num_window_groups { + let mut group_sections: Vec
= Vec::new(); + let mut group_sfb_cb: Vec = vec![ZERO_HCB; max_sfb as usize]; + + let mut k: u32 = 0; + let max = max_sfb as u32; + while k < max { + let sect_cb = read_u8(reader, 5)?; + + let mut sect_len: u32 = 0; + if er_uses_escape_coding(sect_cb) { + loop { + let incr = reader + .read_u32(len_bits) + .map_err(|_| Error::UnexpectedEnd)?; + if incr == sect_esc_val { + sect_len += sect_esc_val; + continue; + } + sect_len += incr; + break; + } + } else { + // Table 4.52 `else { sect_len_incr = 1; }` — one band, + // no field on the wire. + sect_len = 1; + } + + let start = k; + let end = k + sect_len; + if end > max { + return Err(Error::SectionDataOverrun); + } + for sfb in start..end { + group_sfb_cb[sfb as usize] = sect_cb; + } + group_sections.push(Section { + codebook: sect_cb, + start: start as u8, + end: end as u8, + }); + k = end; + } + + sections.push(group_sections); + sfb_cb.push(group_sfb_cb); + } + + Ok(SectionData { sections, sfb_cb }) + } + + /// Encode the error-resilient `section_data()` branch, the inverse + /// of [`SectionData::parse_er`]. + /// + /// `sect_cb` is emitted as a 5-bit field; the `sect_len_incr` + /// escape sequence is emitted only for codebooks that use escape + /// coding (`< 11`, or `12..=15`). A `sect_cb == 11` / `>= 16` + /// section must span exactly one band (the Table 4.52 fixed + /// `sect_len_incr = 1`); a longer such section is rejected with + /// [`Error::SectionDataEncodeInvalid`]. + pub fn write_er( + &self, + writer: &mut BitWriter, + window_sequence: WindowSequence, + max_sfb: u8, + ) -> Result<()> { + let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { + (7u32, 3u32) + } else { + (31u32, 5u32) + }; + + for group_sections in &self.sections { + if group_sections.is_empty() { + if max_sfb != 0 { + return Err(Error::SectionDataEncodeInvalid); + } + continue; + } + if group_sections[0].start != 0 { + return Err(Error::SectionDataEncodeInvalid); + } + for w in group_sections.windows(2) { + if w[0].end != w[1].start { + return Err(Error::SectionDataEncodeInvalid); + } + } + if group_sections.last().unwrap().end != max_sfb { + return Err(Error::SectionDataEncodeInvalid); + } + + for section in group_sections { + // sect_cb is 5 bits in the ER branch. + if section.codebook > 0x1f { + return Err(Error::SectionDataEncodeInvalid); + } + let sect_len = section.len() as u32; + if sect_len == 0 { + return Err(Error::SectionDataEncodeInvalid); + } + + writer.write_u32(section.codebook as u32, 5); + + if er_uses_escape_coding(section.codebook) { + let mut remaining = sect_len; + while remaining >= sect_esc_val { + writer.write_u32(sect_esc_val, len_bits); + remaining -= sect_esc_val; + } + writer.write_u32(remaining, len_bits); + } else { + // Fixed sect_len_incr = 1 — the section must be a + // single band and carries no length field. + if sect_len != 1 { + return Err(Error::SectionDataEncodeInvalid); + } + } + } + } + + Ok(()) + } + + /// `num_sec[g]` — number of sections in window group `g`. + /// Returns `0` for an out-of-range group index. + pub fn num_sec(&self, group: usize) -> usize { + self.sections.get(group).map_or(0, Vec::len) + } + + /// Encode `section_data()` onto `writer`, inverse of + /// [`SectionData::parse`]. + /// + /// * `writer` — receives the bit-exact Table 17 stream. The + /// writer position advances by `4 + (3|5) × (n_increments)` bits + /// per section (per the chosen `sect_esc_val` branch). + /// * `window_sequence` — must match the value the surrounding + /// `ics_info()` carries; selects 3-bit / 5-bit `sect_len_incr`. + /// * `max_sfb` — the band count the parser will be told. Every + /// per-group section list must cover bands `[0, max_sfb)` + /// exactly without gaps or overlaps. + /// + /// Returns [`Error::SectionDataEncodeInvalid`] if: + /// + /// * `self.sections.len()` doesn't equal the implicit + /// `num_window_groups` (taken from `self.sections.len()`). + /// `num_window_groups` itself isn't a parameter — it's read + /// off `self.sections` so a caller who constructed + /// [`SectionData`] in-memory cannot accidentally desync. + /// * Any group's section list isn't contiguous from band `0` + /// to band `max_sfb` (start of first section != 0; end of + /// last section != `max_sfb`; or section `[i].end != + /// sections[i+1].start`). + /// * A `sect_cb` exceeds the 4-bit field width. + /// * A `sect_len` of `0` appears (a conforming encoder never + /// emits empty sections, and the §6.3 escape can't terminate + /// a zero-length run with the parser's `break` semantics). + pub fn write( + &self, + writer: &mut BitWriter, + window_sequence: WindowSequence, + max_sfb: u8, + ) -> Result<()> { + let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { + (7u32, 3u32) // (1 << 3) - 1, 3-bit field + } else { + (31u32, 5u32) // (1 << 5) - 1, 5-bit field + }; + + for group_sections in &self.sections { + // Empty section list is only valid when max_sfb == 0: + // the parser's `while k < max_sfb` loop never enters. + if group_sections.is_empty() { + if max_sfb != 0 { + return Err(Error::SectionDataEncodeInvalid); + } + continue; + } + + // Contiguity: first section starts at 0, sections chain + // end[i] == start[i+1], last ends at max_sfb. + if group_sections[0].start != 0 { + return Err(Error::SectionDataEncodeInvalid); + } + for w in group_sections.windows(2) { + if w[0].end != w[1].start { + return Err(Error::SectionDataEncodeInvalid); + } + } + if group_sections.last().unwrap().end != max_sfb { + return Err(Error::SectionDataEncodeInvalid); + } + + for section in group_sections { + // sect_cb is 4 bits; reject any out-of-range value. + if section.codebook > 0x0f { + return Err(Error::SectionDataEncodeInvalid); + } + let sect_len = section.len() as u32; + // A conforming encoder never emits a zero-length + // section; the §6.3 termination relies on a non- + // escape final increment, and the parser's outer + // `while k < max_sfb` would then re-enter the loop + // expecting another sect_cb. Reject up front. + if sect_len == 0 { + return Err(Error::SectionDataEncodeInvalid); + } + + writer.write_u32(section.codebook as u32, 4); + + // §6.3 escape: while remaining >= sect_esc_val, + // emit sect_esc_val and subtract. The trailing + // non-escape increment (which is in [0, sect_esc_val) + // by construction) terminates the run. This is what + // forces a literal `0` after a length that's an + // exact multiple of sect_esc_val (e.g. sect_len=31 + // long branch → emit 31, then 0). + let mut remaining = sect_len; + while remaining >= sect_esc_val { + writer.write_u32(sect_esc_val, len_bits); + remaining -= sect_esc_val; + } + writer.write_u32(remaining, len_bits); + } + } + + Ok(()) + } +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} + +/// Table 4.52 escape-coding gate for the error-resilient +/// `section_data()` branch. +/// +/// Escape coding (`sect_len_incr` loop) runs when +/// `sect_cb < 11 || (sect_cb > 11 && sect_cb < 16)`. For +/// `sect_cb == 11` (`ESC_HCB`) and `sect_cb >= 16` (the §4.6.16.4 +/// virtual codebooks) the spec fixes `sect_len_incr = 1` and emits no +/// length field, so the section spans exactly one band. +fn er_uses_escape_coding(sect_cb: u8) -> bool { + sect_cb < 11 || (sect_cb > 11 && sect_cb < 16) +} diff --git a/crates/vendor/oxideav-aac/src/spectral_codebook.rs b/crates/vendor/oxideav-aac/src/spectral_codebook.rs new file mode 100644 index 00000000..24d71b3d --- /dev/null +++ b/crates/vendor/oxideav-aac/src/spectral_codebook.rs @@ -0,0 +1,543 @@ +//! Spectrum Huffman codebook parameters and the §4.6.3.3 index → +//! n-tuple translation. +//! +//! ISO/IEC 14496-3 §4.6.3 / Table 4.95 enumerates the AAC spectrum +//! Huffman codebooks. Each codebook is identified by a number `i ∈ +//! 0..=11` (plus the §4.6.3.1 non-spectral books 12..=15 and the +//! ISO/IEC 14496-3 Annex 4.6.3.3 extension books 16..=31) and carries +//! four parameters used by the §4.6.3.3 spectrum-translation +//! pseudocode: +//! +//! | column | meaning | +//! |----------------|---------| +//! | `unsigned_cb` | `0` ⇔ codeword indices encode a signed centred range `-LAV..=+LAV`; `1` ⇔ unsigned `0..=LAV` with explicit sign bits | +//! | `dimension` | `2` (PAIR books) or `4` (QUAD books) — number of spectral coefficients per codeword | +//! | `LAV` | largest absolute value the book can represent directly (without the ESC sequence) | +//! | spec table | which `Table 4.A.x` lists the Huffman codes (not consumed by this module — see "Scope" below) | +//! +//! ## What this module covers +//! +//! * The [`Table495Row`] struct — the four normative columns of +//! Table 4.95 for one codebook number. +//! * The [`TABLE_4_95`] static — the row for every codebook number +//! in `0..=31`, sourced from ISO/IEC 14496-3:2001(E) §4.6.3.1 +//! Table 4.95. Rows for `12` (reserved), `13` (PNS), `14` +//! (out-of-phase intensity), and `15` (in-phase intensity) carry +//! `None` for `unsigned_cb`, `dimension`, and `lav` — those four +//! indices do not carry spectral data so the §4.6.3.3 translation +//! does not apply. +//! * [`table_4_95`] — a safe accessor that returns the row for a +//! given codebook number (0..=31). +//! * [`decode_index_to_tuple`] — the §4.6.3.3 pseudocode that +//! translates a Huffman codeword index `idx` (the first column of +//! Table 4.A.2 through Table 4.A.12) into a `dim`-tuple of +//! quantised spectral coefficients. For unsigned books, the +//! returned tuple carries non-negative magnitudes whose signs are +//! restored by the per-coefficient sign bits that follow the +//! codeword on the wire. +//! * [`encode_tuple_to_index`] — the inverse of +//! `decode_index_to_tuple`. Given a `dim`-tuple of quantised +//! coefficients valid for the codebook (i.e. respecting the +//! `signed`/`unsigned` convention and the LAV cap), returns the +//! matching codeword index that an encoder would emit before the +//! Huffman compression layer. +//! * [`apply_sign_bits`] — folds the per-non-zero-coefficient sign +//! bits from §4.6.3.3 onto an unsigned-codebook decoded tuple. +//! * [`derive_sign_bits`] — the inverse: extracts the sign bits an +//! encoder must emit for an unsigned-codebook signed tuple. +//! * [`decode_esc_value`] — the §4.6.3.3 escape sequence for +//! codebook 11 (`ESC_HCB`). Given an `escape_prefix` length (the +//! run of 1-bits before the separator 0) and the `(N + 4)`-bit +//! `escape_word`, returns the absolute magnitude +//! `2^(N + 4) + escape_word`. +//! * [`encode_esc_value`] — the inverse: given an absolute magnitude +//! `>= LAV = 16`, returns the `(prefix_len, escape_word_bits, +//! escape_word)` triple an encoder must emit. +//! * [`MAX_QUANT`] = `8191` — the maximum absolute amplitude any +//! spectrum codebook 11 can represent, per §4.6.1.3. +//! +//! ## What this module does *not* cover +//! +//! * The Huffman tables themselves (Tables 4.A.2 through 4.A.12 + +//! the AAC-LD / ER variants). Those translate a codeword +//! *bit-pattern* into the `idx` consumed by [`decode_index_to_tuple`]. +//! The Huffman trees are a separate clean-room transcription that +//! will land in a follow-up round. +//! * The §4.4.6 `spectral_data()` wire walker — the function that +//! loops over scalefactor bands and dispatches per-band onto the +//! appropriate codebook. That walker will sit on top of this +//! module and the (forthcoming) Huffman tables. +//! * Codebooks 16..=31 — the Table 4.95 tail (rows 16..=31, all +//! reusing Table 4.A.12 with different ESC thresholds) are +//! surfaced in [`TABLE_4_95`] for completeness but the §4.6.3.3 +//! index translation for these books needs the ESC threshold +//! plumbed through the ESC sequence; the parser-facing accessors +//! in this round handle the standard `0..=11` range and reject +//! `12..=31` with [`Error::SpectralCodebookOutOfRange`]. The +//! per-row LAV value already differs for `16..=31` because each +//! row carries its own ESC threshold; the row data is correct, +//! only the wire decoder is unwired. + +use crate::section_data::Codebook; +use crate::{Error, Result}; + +/// Maximum absolute amplitude for a quantised spectral coefficient +/// (`x_quant`). ISO/IEC 14496-3 §4.6.1.3. +pub const MAX_QUANT: i32 = 8191; + +/// One row of ISO/IEC 14496-3 Table 4.95 (Spectrum Huffman codebook +/// parameters). Carries the `unsigned_cb`, dimension, and LAV +/// columns; the "Codebook listed in Table" column is encoded as a +/// `Some(table_index)` (e.g. `Some(2)` for Table 4.A.2) when the +/// row references a Huffman codebook listing, and `None` for the +/// non-spectral books (0 / 12..=15). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Table495Row { + /// Column 2 — `unsigned_cb[i]`. `None` for non-spectral books + /// (0 / 12..=15). + pub unsigned: Option, + /// Column 3 — dimension (`2` or `4`). `None` for non-spectral + /// books. + pub dimension: Option, + /// Column 4 — Largest Absolute Value the codebook can encode + /// directly. For codebook `0` the value is `0` (the band carries + /// no data so the maximum encoded magnitude is trivially 0). For + /// `12..=15` the column is `None`. For `11` and `16..=31` the + /// row carries the LAV after the ESC sequence is consumed; the + /// in-band LAV (15) is fixed by the Huffman codebook shape — see + /// [`Self::esc_threshold`] for the per-row ESC value. + pub lav: Option, + /// Column 4 trailing parenthesis — the per-row ESC threshold. + /// `Some(8191)` for codebook 11, `Some(15)` for codebook 16 + /// (the "w/o ESC" row — the threshold is the in-band cap), and + /// `Some(31)..=Some(2047)` for codebooks 17..=31. `None` for + /// codebooks 1..=10 (no ESC sequence — the LAV is fully covered + /// by the in-band Huffman table) and for the non-spectral books. + pub esc_threshold: Option, + /// Column 5 — the Table 4.A.x number that lists the Huffman + /// codes. `Some(2)..=Some(12)` for codebooks 1..=11; the + /// extension books 16..=31 all reuse Table 4.A.12 so the value + /// is `Some(12)` for each of those. `None` for codebook 0 and + /// 12..=15. + pub huffman_table: Option, +} + +impl Table495Row { + /// `true` ⇔ the row carries `unsigned_cb == 1`. Convenience + /// accessor that defaults to `false` for non-spectral books + /// (where the column is `None`). + pub fn is_unsigned(self) -> bool { + matches!(self.unsigned, Some(true)) + } + + /// `true` ⇔ the codebook carries an ESC sequence (codebook 11 + /// and the extension books 16..=31). + pub fn has_esc(self) -> bool { + self.esc_threshold.is_some() + } +} + +/// Helper to build a row for a spectral codebook (`1..=11` and +/// `16..=31`). +const fn spec_row( + unsigned: bool, + dimension: u8, + lav: u32, + esc: Option, + table: u8, +) -> Table495Row { + Table495Row { + unsigned: Some(unsigned), + dimension: Some(dimension), + lav: Some(lav), + esc_threshold: esc, + huffman_table: Some(table), + } +} + +/// Helper for non-spectral rows (`0`, `12..=15`). +const fn nonspec_row() -> Table495Row { + Table495Row { + unsigned: None, + dimension: None, + lav: None, + esc_threshold: None, + huffman_table: None, + } +} + +/// ISO/IEC 14496-3 §4.6.3.1 Table 4.95 — Spectrum Huffman codebook +/// parameters. Index by codebook number (`0..=31`). +/// +/// Cross-check with ISO/IEC 14496-3:2001(E) page 113. Row-by-row: +/// +/// | i | unsigned | dim | LAV | ESC | table | +/// |----|----------|-----|-----|-----|-------| +/// | 0 | — | — | 0 | — | — | +/// | 1 | 0 | 4 | 1 | — | 4.A.2 | +/// | 2 | 0 | 4 | 1 | — | 4.A.3 | +/// | 3 | 1 | 4 | 2 | — | 4.A.4 | +/// | 4 | 1 | 4 | 2 | — | 4.A.5 | +/// | 5 | 0 | 2 | 4 | — | 4.A.6 | +/// | 6 | 0 | 2 | 4 | — | 4.A.7 | +/// | 7 | 1 | 2 | 7 | — | 4.A.8 | +/// | 8 | 1 | 2 | 7 | — | 4.A.9 | +/// | 9 | 1 | 2 | 12 | — | 4.A.10| +/// | 10 | 1 | 2 | 12 | — | 4.A.11| +/// | 11 | 1 | 2 | 16 | 8191| 4.A.12| +/// | 12 | — | — | — | — | reserved | +/// | 13 | — | — | — | — | PNS | +/// | 14 | — | — | — | — | intensity out-of-phase | +/// | 15 | — | — | — | — | intensity in-phase | +/// | 16 | 1 | 2 | 16 | 15 | 4.A.12 | +/// | 17 | 1 | 2 | 16 | 31 | 4.A.12 | +/// | 18 | 1 | 2 | 16 | 47 | 4.A.12 | +/// | 19 | 1 | 2 | 16 | 63 | 4.A.12 | +/// | 20 | 1 | 2 | 16 | 95 | 4.A.12 | +/// | 21 | 1 | 2 | 16 | 127 | 4.A.12 | +/// | 22 | 1 | 2 | 16 | 159 | 4.A.12 | +/// | 23 | 1 | 2 | 16 | 191 | 4.A.12 | +/// | 24 | 1 | 2 | 16 | 223 | 4.A.12 | +/// | 25 | 1 | 2 | 16 | 255 | 4.A.12 | +/// | 26 | 1 | 2 | 16 | 319 | 4.A.12 | +/// | 27 | 1 | 2 | 16 | 383 | 4.A.12 | +/// | 28 | 1 | 2 | 16 | 511 | 4.A.12 | +/// | 29 | 1 | 2 | 16 | 767 | 4.A.12 | +/// | 30 | 1 | 2 | 16 | 1023| 4.A.12 | +/// | 31 | 1 | 2 | 16 | 2047| 4.A.12 | +pub const TABLE_4_95: [Table495Row; 32] = [ + // 0: ZERO_HCB + Table495Row { + unsigned: None, + dimension: None, + lav: Some(0), + esc_threshold: None, + huffman_table: None, + }, + // 1..=4 (QUAD) + spec_row(false, 4, 1, None, 2), + spec_row(false, 4, 1, None, 3), + spec_row(true, 4, 2, None, 4), + spec_row(true, 4, 2, None, 5), + // 5..=10 (PAIR) + spec_row(false, 2, 4, None, 6), + spec_row(false, 2, 4, None, 7), + spec_row(true, 2, 7, None, 8), + spec_row(true, 2, 7, None, 9), + spec_row(true, 2, 12, None, 10), + spec_row(true, 2, 12, None, 11), + // 11: ESC + spec_row(true, 2, 16, Some(8191), 12), + // 12..=15: non-spectral + nonspec_row(), + nonspec_row(), + nonspec_row(), + nonspec_row(), + // 16: w/o ESC 15 (ESC threshold equals in-band LAV — the row + // exists but the ESC sequence is never invoked because the LAV + // cap is also 15). + spec_row(true, 2, 16, Some(15), 12), + // 17..=31: ESC books with increasing thresholds + spec_row(true, 2, 16, Some(31), 12), + spec_row(true, 2, 16, Some(47), 12), + spec_row(true, 2, 16, Some(63), 12), + spec_row(true, 2, 16, Some(95), 12), + spec_row(true, 2, 16, Some(127), 12), + spec_row(true, 2, 16, Some(159), 12), + spec_row(true, 2, 16, Some(191), 12), + spec_row(true, 2, 16, Some(223), 12), + spec_row(true, 2, 16, Some(255), 12), + spec_row(true, 2, 16, Some(319), 12), + spec_row(true, 2, 16, Some(383), 12), + spec_row(true, 2, 16, Some(511), 12), + spec_row(true, 2, 16, Some(767), 12), + spec_row(true, 2, 16, Some(1023), 12), + spec_row(true, 2, 16, Some(2047), 12), +]; + +/// Safe accessor for [`TABLE_4_95`]. Returns +/// [`Error::SpectralCodebookOutOfRange`] for `codebook > 31`. +pub fn table_4_95(codebook: u8) -> Result { + if (codebook as usize) >= TABLE_4_95.len() { + return Err(Error::SpectralCodebookOutOfRange(codebook)); + } + Ok(TABLE_4_95[codebook as usize]) +} + +/// Translate a Huffman codeword index `idx` to a `dim`-tuple of +/// quantised spectral coefficients, per ISO/IEC 14496-3 §4.6.3.3. +/// +/// The output buffer is the first `dim` entries of the returned +/// fixed-size array; the unused trailing entries are zero. For +/// `dim == 2` the meaningful entries are `[y, z]`; for `dim == 4` +/// they are `[w, x, y, z]`. The spec ordering is preserved +/// (low-frequency first within the n-tuple). +/// +/// `codebook` must be one of: +/// +/// * `1..=11` — standard spectrum books. The full §4.6.3.3 path is +/// exercised; ESC handling for `11` is not performed *inside* this +/// call (the caller dispatches on [`Table495Row::has_esc`] and +/// invokes [`decode_esc_value`] for each coefficient at the LAV +/// cap). +/// * `0` is rejected with [`Error::SpectralCodebookHasNoTuple`] +/// because the band carries no spectrum data. +/// * `12..=15` are rejected with +/// [`Error::SpectralCodebookHasNoTuple`] (non-spectral books). +/// * `16..=31` are accepted for the pseudocode mechanics but with +/// the same ESC-handling caveat as `11`. +/// +/// An out-of-range `idx` (which can only happen when the caller's +/// Huffman tree is incoherent — a conforming Huffman decoder always +/// emits an in-range index) surfaces as +/// [`Error::SpectralCodebookIndexOutOfRange`]. The legal range is +/// `0..mod^dim` where `mod = lav + 1` (unsigned) or `2 * lav + 1` +/// (signed). +pub fn decode_index_to_tuple(codebook: u8, idx: u32) -> Result<[i32; 4]> { + let row = table_4_95(codebook)?; + let dim = row + .dimension + .ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; + let lav = row.lav.ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; + let unsigned = row.is_unsigned(); + + let (modulus, offset) = if unsigned { + (lav as i64 + 1, 0i64) + } else { + (2 * lav as i64 + 1, lav as i64) + }; + + // Range check: idx must be < modulus^dim. + let mut max = 1i64; + for _ in 0..dim { + max = max.saturating_mul(modulus); + } + if (idx as i64) >= max { + return Err(Error::SpectralCodebookIndexOutOfRange(codebook)); + } + + let mut out = [0i32; 4]; + let mut remaining = idx as i64; + if dim == 4 { + // §4.6.3.3 pseudocode: + // w = INT(idx / mod^3) - off + // x = INT(idx / mod^2) - off (after removing the w slice) + // y = INT(idx / mod^1) - off (after removing the x slice) + // z = idx - off (the leftover scaled by mod^0) + let m2 = modulus * modulus; + let m3 = m2 * modulus; + let w = remaining / m3 - offset; + remaining -= (w + offset) * m3; + let x = remaining / m2 - offset; + remaining -= (x + offset) * m2; + let y = remaining / modulus - offset; + remaining -= (y + offset) * modulus; + let z = remaining - offset; + out[0] = w as i32; + out[1] = x as i32; + out[2] = y as i32; + out[3] = z as i32; + } else { + // dim == 2: only y and z (in the lower two slots). + let y = remaining / modulus - offset; + remaining -= (y + offset) * modulus; + let z = remaining - offset; + out[0] = y as i32; + out[1] = z as i32; + } + Ok(out) +} + +/// Inverse of [`decode_index_to_tuple`]: given a `dim`-tuple of +/// quantised coefficients, returns the codeword index that maps to +/// it under the §4.6.3.3 translation. +/// +/// `tuple` is the first `dim` entries of the input slice (`[w, x, +/// y, z]` for `dim == 4`, `[y, z]` for `dim == 2`); the unused +/// trailing entries are ignored. For unsigned codebooks every entry +/// must be in `0..=lav`; for signed codebooks every entry must be in +/// `-lav..=+lav`. Any value outside the valid range surfaces as +/// [`Error::SpectralCodebookTupleOutOfRange`]. +pub fn encode_tuple_to_index(codebook: u8, tuple: &[i32]) -> Result { + let row = table_4_95(codebook)?; + let dim = row + .dimension + .ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; + let lav = row.lav.ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; + let unsigned = row.is_unsigned(); + + if tuple.len() < dim as usize { + return Err(Error::SpectralCodebookTupleOutOfRange(codebook)); + } + + let (modulus, offset) = if unsigned { + (lav as i64 + 1, 0i64) + } else { + (2 * lav as i64 + 1, lav as i64) + }; + let lav_i = lav as i32; + + let mut acc: i64 = 0; + for &v in tuple.iter().take(dim as usize) { + let valid = if unsigned { + (0..=lav_i).contains(&v) + } else { + (-lav_i..=lav_i).contains(&v) + }; + if !valid { + return Err(Error::SpectralCodebookTupleOutOfRange(codebook)); + } + acc = acc * modulus + (v as i64 + offset); + } + Ok(acc as u32) +} + +/// Apply the §4.6.3.3 sign-bit fix-up to an unsigned-codebook +/// decoded tuple. +/// +/// On the wire, an unsigned codebook (codebooks 3, 4, 7, 8, 9, 10, +/// 11, 16..=31) emits non-negative magnitudes; the actual sign of +/// each *non-zero* coefficient is carried in a separate sign bit +/// that immediately follows the Huffman codeword. The bit ordering +/// matches the spec's "lower frequency first" rule: for a QUAD book, +/// the sign for `w` (if `w != 0`) is first, then `x`, then `y`, +/// then `z`; for a PAIR book the order is `y`, then `z`. +/// +/// `signs` must contain exactly one bit per non-zero coefficient in +/// `tuple`, in the spec-defined order. A `1` bit makes the +/// coefficient negative; a `0` leaves it positive. +/// +/// On signed codebooks this is a no-op (signed books already carry +/// their sign in the codeword index). The caller is expected to +/// guard on [`Table495Row::is_unsigned`]; if invoked on a signed +/// codebook the function returns the input unchanged. +/// +/// Returns [`Error::SpectralCodebookSignBitsMismatch`] when +/// `signs.len()` disagrees with the count of non-zero coefficients +/// in the unsigned-codebook tuple. +pub fn apply_sign_bits(codebook: u8, mut tuple: [i32; 4], signs: &[bool]) -> Result<[i32; 4]> { + let row = table_4_95(codebook)?; + if !row.is_unsigned() { + // Signed codebooks already carry the sign in the codeword + // index; this is a no-op. We still accept `signs.is_empty()` + // and reject any non-empty `signs` to keep the API symmetric + // — a caller that incorrectly sent sign bits for a signed + // codebook is a bug worth surfacing. + if !signs.is_empty() { + return Err(Error::SpectralCodebookSignBitsMismatch(codebook)); + } + return Ok(tuple); + } + let dim = row + .dimension + .ok_or(Error::SpectralCodebookHasNoTuple(codebook))? as usize; + let nonzero = tuple.iter().take(dim).filter(|&&v| v != 0).count(); + if signs.len() != nonzero { + return Err(Error::SpectralCodebookSignBitsMismatch(codebook)); + } + let mut sign_it = signs.iter(); + for entry in tuple.iter_mut().take(dim) { + if *entry != 0 { + let neg = *sign_it.next().expect("count match"); + if neg { + *entry = -*entry; + } + } + } + Ok(tuple) +} + +/// Inverse of [`apply_sign_bits`]: given a signed tuple decoded +/// from an unsigned codebook, returns the sign-bit sequence the +/// encoder must emit (one bit per non-zero coefficient, low-to-high +/// frequency). +/// +/// On signed codebooks returns an empty sign-bit vector. +pub fn derive_sign_bits(codebook: u8, tuple: &[i32]) -> Result> { + let row = table_4_95(codebook)?; + let dim = row + .dimension + .ok_or(Error::SpectralCodebookHasNoTuple(codebook))? as usize; + if tuple.len() < dim { + return Err(Error::SpectralCodebookTupleOutOfRange(codebook)); + } + if !row.is_unsigned() { + return Ok(Vec::new()); + } + let mut bits = Vec::with_capacity(dim); + for &v in tuple.iter().take(dim) { + if v != 0 { + bits.push(v < 0); + } + } + Ok(bits) +} + +/// Decode a §4.6.3.3 ESC sequence to its absolute magnitude. +/// +/// The ESC sequence is emitted whenever a codebook-11 Huffman +/// codeword decodes to a 2-tuple coefficient at the in-band cap +/// (magnitude `16`). It consists of: +/// +/// 1. `escape_prefix` — a run of `N` consecutive `1` bits. +/// 2. `escape_separator` — a single `0` bit. +/// 3. `escape_word` — `N + 4` bits, big-endian, carrying the +/// unsigned word value. +/// +/// The decoded absolute magnitude is `2^(N + 4) + escape_word`. +/// +/// `prefix_len` must be in `0..=24`. §4.6.2 caps the *encoder-side* +/// magnitude at [`MAX_QUANT`] (8191, i.e. `N ≤ 8`), but the decode +/// side deliberately accepts larger escape codes: the normative +/// ISO/IEC 14496-26 ER AAC LD conformance vectors transmit escapes +/// far past the cap (`er_ad1103np_22_ep0` AU 508 carries magnitude +/// 9283 at `N == 9`; `er_ad1103np_24_ep0` AU 1551 carries 783 966 at +/// `N == 15`), and their reference waveforms require the value to be +/// decoded, not rejected. The `> 24` bound keeps a hostile all-ones +/// prefix run from consuming unbounded input (and the u32 magnitude +/// in `i32` range) while admitting every observed conformance +/// magnitude with headroom. `escape_word` must fit `(N + 4)` bits. +/// Out-of-range arguments surface as +/// [`Error::SpectralCodebookEscOutOfRange`]. +pub fn decode_esc_value(prefix_len: u32, escape_word: u32) -> Result { + if prefix_len > 24 { + return Err(Error::SpectralCodebookEscOutOfRange); + } + let word_bits = prefix_len + 4; + if escape_word >= (1u32 << word_bits) { + return Err(Error::SpectralCodebookEscOutOfRange); + } + Ok((1u32 << word_bits) + escape_word) +} + +/// Inverse of [`decode_esc_value`]: given an absolute magnitude +/// `>= 16` (the ESC threshold for codebook 11), returns the +/// `(prefix_len, escape_word)` pair the encoder must emit. +/// +/// The mapping is `prefix_len = floor(log2(value)) - 4` and +/// `escape_word = value - 2^(prefix_len + 4)`. Values in +/// `0..=15` cannot be ESC-encoded (they are in-band) and surface as +/// [`Error::SpectralCodebookEscOutOfRange`]. Values greater than +/// [`MAX_QUANT`] also surface there. +pub fn encode_esc_value(value: u32) -> Result<(u32, u32)> { + if value < 16 || value as i32 > MAX_QUANT { + return Err(Error::SpectralCodebookEscOutOfRange); + } + // floor(log2(value)) — value is in 16..=8191, so log2 is in + // 4..=12, and prefix_len = log2 - 4 is in 0..=8. + let log = 31 - value.leading_zeros(); + let prefix_len = log - 4; + let escape_word = value - (1u32 << (prefix_len + 4)); + Ok((prefix_len, escape_word)) +} + +/// Bridge to the existing [`Codebook`] enum: classifies a `sect_cb` +/// value (`0..=15`) into a semantic category. The wire-form +/// `sect_cb` field is 4 bits in the standard branch and 5 bits in +/// the ER-AAC resilience branch (Table 17), so [`Codebook`] only +/// covers `0..=15`; this re-export is a convenience so callers can +/// reach the existing classifier without importing +/// [`crate::section_data`] directly. +pub fn classify(sect_cb: u8) -> Codebook { + Codebook::from_value(sect_cb) +} diff --git a/crates/vendor/oxideav-aac/src/spectral_data.rs b/crates/vendor/oxideav-aac/src/spectral_data.rs new file mode 100644 index 00000000..20e13e36 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/spectral_data.rs @@ -0,0 +1,927 @@ +//! `spectral_data()` wire walker — ISO/IEC 14496-3 Table 4.56. +//! +//! This module is the §4.4.6 driver that the round-259 README named +//! as the next step after the Codebook 1..=11 table set completed: +//! it loops over the window groups and sections established by +//! [`crate::ics_info`] / [`crate::section_data`] and recovers the +//! quantised spectral coefficients `x_quant` by dispatching, per +//! section, onto the [`crate::spectrum_huffman`] codeword decoders +//! and the [`crate::spectral_codebook`] index/sign/ESC translation +//! helpers. +//! +//! ## Table 4.56 layout +//! +//! ```text +//! spectral_data() { +//! for (g = 0; g < num_window_groups; g++) { +//! for (i = 0; i < num_sec[g]; i++) { +//! if (sect_cb[g][i] != ZERO_HCB && +//! sect_cb[g][i] != NOISE_HCB && +//! sect_cb[g][i] != INTENSITY_HCB && +//! sect_cb[g][i] != INTENSITY_HCB2) { +//! for (k = sect_sfb_offset[g][sect_start[g][i]]; +//! k < sect_sfb_offset[g][sect_end[g][i]];) { +//! if (sect_cb[g][i] < FIRST_PAIR_HCB) { +//! hcod[sect_cb[g][i]][w][x][y][z]; // 1..16 vlclbf +//! if (unsigned_cb[sect_cb[g][i]]) +//! quad_sign_bits; // 0..4 bslbf +//! k += QUAD_LEN; +//! } else { +//! hcod[sect_cb[g][i]][y][z]; // 1..15 vlclbf +//! if (unsigned_cb[sect_cb[g][i]]) +//! pair_sign_bits; // 0..2 bslbf +//! k += PAIR_LEN; +//! if (sect_cb[g][i] == ESC_HCB) { +//! if (y == ESC_FLAG) hcod_esc_y; // 5..21 vlclbf +//! if (z == ESC_FLAG) hcod_esc_z; // 5..21 vlclbf +//! } +//! } +//! } +//! } +//! } +//! } +//! } +//! ``` +//! +//! ## `sect_sfb_offset` — §4.5.2.3.4 +//! +//! The loop bounds come from the per-group coefficient offsets +//! `sect_sfb_offset[g][sfb]` derived in §4.5.2.3.4: +//! +//! * For the three long window sequences (`num_window_groups == 1`, +//! `window_group_length[0] == 1`) the offsets are simply +//! `swb_offset_long_window[fs_index][sfb]` for +//! `sfb ∈ 0..=max_sfb`. +//! * For `EIGHT_SHORT_SEQUENCE` each group `g` spans +//! `window_group_length[g]` grouped short windows whose spectral +//! data is interleaved scalefactor-band by scalefactor-band +//! (§4.5.2.3.5), so each *virtual* scalefactor band is +//! `window_group_length[g]` times the Table 4.130-family +//! scalefactor-window-band width: +//! `sect_sfb_offset[g][i+1] = sect_sfb_offset[g][i] + +//! (swb_offset_short[i+1] − swb_offset_short[i]) × +//! window_group_length[g]`. +//! +//! [`sect_sfb_offset`] exposes that derivation so follow-up tools +//! (the §4.6.3.3 `quant_to_spec()` deinterleaver, intensity / PNS +//! reconstruction) can reuse it. +//! +//! ## Coefficient storage +//! +//! [`SpectralData::x_quant`] holds one buffer per window group, in +//! the §4.5.2.3.5 *transmission* order: groups sequential, and +//! within a group the coefficients of all grouped short windows +//! interleaved per scalefactor band ("virtual" scalefactor bands). +//! Each group buffer is allocated at the full group span — +//! `window_group_length[g] × 128` for `EIGHT_SHORT_SEQUENCE`, +//! `1024` otherwise — with the bands above `max_sfb` (and every +//! `ZERO_HCB` / `NOISE_HCB` / intensity band) left at `0`, matching +//! the §4.5.2.3 "all spectral data associated with Huffman codebook +//! zero are omitted [and zeroed]" rule. De-interleaving into the +//! `spec[w][k]` window-major layout consumed by TNS / the filterbank +//! (the §4.6.3.3 `quant_to_spec()` pseudocode) is a follow-up tool. +//! +//! ## §4.6.3.3 per-codeword translation +//! +//! * The Huffman codeword index is translated to the n-tuple via +//! [`crate::spectral_codebook::decode_index_to_tuple`]. +//! * For unsigned codebooks (3, 4, 7..=11) the +//! `quad_sign_bits` / `pair_sign_bits` field follows the codeword +//! — one bit per non-zero coefficient, low frequency first, `1` = +//! negative — applied via +//! [`crate::spectral_codebook::apply_sign_bits`]. +//! * For the ESC codebook (11) a decoded magnitude of `16` +//! (`ESC_FLAG`) is not a literal value: a `hcod_esc_y` / +//! `hcod_esc_z` escape sequence follows the sign bits (in `y`, +//! `z` order) — an `escape_prefix` of `N` ones, a zero +//! `escape_separator`, and an `(N + 4)`-bit `escape_word` — +//! decoding to `2^(N+4) + escape_word` via +//! [`crate::spectral_codebook::decode_esc_value`], with the sign +//! carried by the already-parsed sign bit. §4.6.1.3 caps the +//! magnitude at `MAX_QUANT` (8191), so `N ≤ 8` on a conforming +//! stream. +//! +//! [`SpectralData::write`] is the symmetric encoder: it re-derives +//! the codeword index via +//! [`crate::spectral_codebook::encode_tuple_to_index`] (clamping +//! ESC-book magnitudes ≥ 16 to the in-band `ESC_FLAG`), emits the +//! sign bits via [`crate::spectral_codebook::derive_sign_bits`], and +//! appends the escape sequences via +//! [`crate::spectral_codebook::encode_esc_value`], producing a +//! bit-exact inverse of [`SpectralData::parse`]. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::ics_info::IcsInfo; +use crate::section_data::{Codebook, Section, SectionData}; +use crate::spectral_codebook::{ + apply_sign_bits, decode_esc_value, decode_index_to_tuple, derive_sign_bits, encode_esc_value, + encode_tuple_to_index, table_4_95, MAX_QUANT, +}; +use crate::spectrum_huffman::{ + hcod10_decode, hcod10_write, hcod11_decode, hcod11_write, hcod1_decode, hcod1_write, + hcod2_decode, hcod2_write, hcod3_decode, hcod3_write, hcod4_decode, hcod4_write, hcod5_decode, + hcod5_write, hcod6_decode, hcod6_write, hcod7_decode, hcod7_write, hcod8_decode, hcod8_write, + hcod9_decode, hcod9_write, +}; +#[cfg(test)] +use crate::swb_offset::{long_window_offsets, short_window_offsets}; +use crate::{Error, Result}; + +/// `QUAD_LEN` — coefficients per codeword for the dim-4 books +/// (1..=4), per Table 4.56 / Table 4.151. +pub const QUAD_LEN: usize = 4; + +/// `PAIR_LEN` — coefficients per codeword for the dim-2 books +/// (5..=11), per Table 4.56 / Table 4.151. +pub const PAIR_LEN: usize = 2; + +/// `ESC_FLAG` — the in-band ESC-book magnitude (16) that signals a +/// following `hcod_esc_y` / `hcod_esc_z` escape sequence +/// (§4.6.3.3). +pub const ESC_FLAG: i32 = 16; + +/// Derive `sect_sfb_offset[g][sfb]` (`sfb ∈ 0..=max_sfb`) per the +/// §4.5.2.3.4 pseudocode — the offset of the first coefficient of +/// each (virtual) scalefactor band within window group `g`'s +/// interleaved coefficient stream. +/// +/// Returns one `max_sfb + 1`-entry offset vector per window group. +/// For the long window sequences this is a single group mirroring +/// `swb_offset_long_window[fs_index]`; for `EIGHT_SHORT_SEQUENCE` +/// each group scales the Table 4.130-family band widths by +/// `window_group_length[g]`. +/// +/// Errors: +/// +/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] — `fs_index` +/// outside the `0..=11` SWB-table range. +/// * [`Error::SpectralDataInvalid`] — `max_sfb` exceeds the +/// `num_swb` of the active window sequence (the §4.5.2.3.4 loops +/// index `swb_offset[max_sfb]`, which only exists up to +/// `num_swb`). +pub fn sect_sfb_offset(ics_info: &IcsInfo, fs_index: u8) -> Result>> { + let max_sfb = ics_info.max_sfb as usize; + if ics_info.window_sequence.is_eight_short() { + let swb = ics_info.swb_offsets(fs_index)?; + // `swb` has num_swb + 1 entries; band widths exist for + // sfb < num_swb only. + if max_sfb + 1 > swb.len() { + return Err(Error::SpectralDataInvalid); + } + let mut per_group = Vec::with_capacity(ics_info.num_window_groups as usize); + for g in 0..ics_info.num_window_groups as usize { + let wgl = u32::from(ics_info.window_group_length[g]); + let mut offsets = Vec::with_capacity(max_sfb + 1); + let mut offset = 0u32; + offsets.push(offset); + for i in 0..max_sfb { + let width = u32::from(swb[i + 1] - swb[i]) * wgl; + offset += width; + offsets.push(offset); + } + per_group.push(offsets); + } + Ok(per_group) + } else { + let swb = ics_info.swb_offsets(fs_index)?; + if max_sfb + 1 > swb.len() { + return Err(Error::SpectralDataInvalid); + } + let offsets = swb[..=max_sfb].iter().map(|&o| u32::from(o)).collect(); + Ok(vec![offsets]) + } +} + +/// Quantised spectral coefficients recovered from (or destined for) +/// a Table 4.56 `spectral_data()` block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpectralData { + /// `x_quant`, one buffer per window group, in the §4.5.2.3.5 + /// transmission (interleaved) order. `x_quant[g].len() == + /// window_group_length[g] × 128` for `EIGHT_SHORT_SEQUENCE`, + /// `1024` otherwise; bands at or above `max_sfb` and bands whose + /// section codebook carries no spectrum (`ZERO_HCB`, + /// `NOISE_HCB`, intensity) are `0`. + pub x_quant: Vec>, +} + +impl SpectralData { + /// Length of one window group's coefficient buffer. + fn group_len(ics_info: &IcsInfo, g: usize) -> usize { + // The parser rejects LD + EIGHT_SHORT, so window_len() cannot + // fail on a parsed ics_info; fall back to the family frame + // length defensively. + let window_len = ics_info + .window_len() + .unwrap_or_else(|_| ics_info.family.frame_len()); + if ics_info.window_sequence.is_eight_short() { + ics_info.window_group_length[g] as usize * window_len + } else { + window_len + } + } + + /// Parse a Table 4.56 `spectral_data()` block. + /// + /// * `reader` — positioned at the first `spectral_data()` bit + /// (the position [`crate::ics_body::IcsBody`] surfaces as + /// `spectral_data_bit_offset`). + /// * `ics_info` — the channel's parsed `ics_info()` (drives + /// `num_window_groups` / `window_group_length` / + /// `window_sequence`). + /// * `section_data` — the channel's parsed `section_data()` + /// (drives the per-section codebook dispatch and the + /// `sect_start` / `sect_end` loop bounds). + /// * `fs_index` — `samplingFrequencyIndex` selecting the + /// Table 4.129-family `swb_offset` tables. + /// + /// Errors: + /// + /// * [`Error::UnexpectedEnd`] — bit-reader underflow inside a + /// codeword, sign-bit field, or escape sequence. + /// * [`Error::SpectralDataInvalid`] — structural violations: see + /// [`sect_sfb_offset`], a `section_data` group count that + /// disagrees with `ics_info`, a section carrying the reserved + /// codebook 12, or a section span that is not a whole number + /// of n-tuples. + /// * [`Error::SpectralCodebookEscOutOfRange`] — an escape + /// sequence whose decoded magnitude exceeds `MAX_QUANT` + /// (8191) per §4.6.1.3. + pub fn parse( + reader: &mut BitReader<'_>, + ics_info: &IcsInfo, + section_data: &SectionData, + fs_index: u8, + ) -> Result { + let offsets = sect_sfb_offset(ics_info, fs_index)?; + let num_groups = ics_info.num_window_groups as usize; + if section_data.sections.len() != num_groups { + return Err(Error::SpectralDataInvalid); + } + + let mut x_quant = Vec::with_capacity(num_groups); + for (g, group_offsets) in offsets.iter().enumerate() { + let mut buf = vec![0i32; Self::group_len(ics_info, g)]; + for sec in §ion_data.sections[g] { + let (cb, dim) = match section_codebook(sec)? { + Some(pair) => pair, + None => continue, + }; + let start = group_offsets[sec.start as usize] as usize; + let end = group_offsets[sec.end as usize] as usize; + debug_assert!(end <= buf.len(), "offsets bounded by group span"); + let mut k = start; + while k < end { + if k + dim > end { + return Err(Error::SpectralDataInvalid); + } + let idx = decode_codeword(reader, cb)?; + let tuple = decode_index_to_tuple(cb, idx)?; + let tuple = read_and_apply_signs(reader, cb, dim, tuple)?; + for (j, &v) in tuple.iter().take(dim).enumerate() { + buf[k + j] = if cb == 11 && v.abs() == ESC_FLAG { + // §4.6.3.3: escape sequences follow the + // sign bits, in y then z order; the sign + // bit already parsed applies to the + // escaped magnitude. + let mag = read_escape_sequence(reader)? as i32; + if v < 0 { + -mag + } else { + mag + } + } else { + v + }; + } + k += dim; + } + } + x_quant.push(buf); + } + Ok(SpectralData { x_quant }) + } + + /// Write a Table 4.56 `spectral_data()` block — the bit-exact + /// inverse of [`SpectralData::parse`] under the same `ics_info` + /// / `section_data` / `fs_index`. + /// + /// Errors: + /// + /// * [`Error::SpectralDataInvalid`] — same structural checks as + /// the parser. + /// * [`Error::SpectralDataEncodeInvalid`] — group buffer count + /// or lengths disagreeing with the `ics_info` grouping, or a + /// non-zero coefficient in a band that transmits no spectrum + /// (`ZERO_HCB` / `NOISE_HCB` / intensity sections, or at and + /// above `max_sfb`). + /// * [`Error::SpectralCodebookTupleOutOfRange`] — a coefficient + /// magnitude exceeding the section codebook's LAV (for the + /// ESC book, propagated as + /// [`Error::SpectralCodebookEscOutOfRange`] above + /// `MAX_QUANT`). + pub fn write( + &self, + writer: &mut BitWriter, + ics_info: &IcsInfo, + section_data: &SectionData, + fs_index: u8, + ) -> Result<()> { + let offsets = sect_sfb_offset(ics_info, fs_index)?; + let num_groups = ics_info.num_window_groups as usize; + if section_data.sections.len() != num_groups { + return Err(Error::SpectralDataInvalid); + } + if self.x_quant.len() != num_groups { + return Err(Error::SpectralDataEncodeInvalid); + } + + for (g, group_offsets) in offsets.iter().enumerate() { + let buf = &self.x_quant[g]; + if buf.len() != Self::group_len(ics_info, g) { + return Err(Error::SpectralDataEncodeInvalid); + } + // Bands that transmit no spectrum must hold zeros: + // everything not covered by a spectrum-carrying section. + let mut covered = vec![false; buf.len()]; + for sec in §ion_data.sections[g] { + if section_codebook(sec)?.is_none() { + continue; + } + let start = group_offsets[sec.start as usize] as usize; + let end = group_offsets[sec.end as usize] as usize; + covered[start..end].fill(true); + } + if buf.iter().zip(covered.iter()).any(|(&v, &c)| v != 0 && !c) { + return Err(Error::SpectralDataEncodeInvalid); + } + + for sec in §ion_data.sections[g] { + let (cb, dim) = match section_codebook(sec)? { + Some(pair) => pair, + None => continue, + }; + let start = group_offsets[sec.start as usize] as usize; + let end = group_offsets[sec.end as usize] as usize; + let mut k = start; + while k < end { + if k + dim > end { + return Err(Error::SpectralDataInvalid); + } + write_tuple(writer, cb, dim, &buf[k..k + dim])?; + k += dim; + } + } + } + Ok(()) + } +} + +/// Classify a section for the Table 4.56 dispatch: `Ok(None)` for +/// the codebooks that transmit no spectral data (`ZERO_HCB`, +/// `NOISE_HCB`, `INTENSITY_HCB`, `INTENSITY_HCB2`), +/// `Ok(Some((cb, dim)))` for the spectrum books 1..=11, and +/// [`Error::SpectralDataInvalid`] for the reserved codebook 12 +/// (which the Table 4.56 condition does not exclude but which has +/// no Huffman table to dispatch onto). +fn section_codebook(sec: &Section) -> Result> { + match sec.codebook_kind() { + Codebook::Zero + | Codebook::Noise + | Codebook::IntensityInPhase + | Codebook::IntensityOutOfPhase => Ok(None), + Codebook::Quad { number, .. } => Ok(Some((number, QUAD_LEN))), + Codebook::Pair { number, .. } => Ok(Some((number, PAIR_LEN))), + Codebook::Esc => Ok(Some((11, PAIR_LEN))), + Codebook::Reserved12 => Err(Error::SpectralDataInvalid), + } +} + +/// Dispatch one `hcod[cb]` codeword decode onto the per-book +/// decoder (Tables 4.A.2 … 4.A.12). +pub(crate) fn decode_codeword(reader: &mut BitReader<'_>, cb: u8) -> Result { + match cb { + 1 => hcod1_decode(reader), + 2 => hcod2_decode(reader), + 3 => hcod3_decode(reader), + 4 => hcod4_decode(reader), + 5 => hcod5_decode(reader), + 6 => hcod6_decode(reader), + 7 => hcod7_decode(reader), + 8 => hcod8_decode(reader), + 9 => hcod9_decode(reader), + 10 => hcod10_decode(reader), + 11 => hcod11_decode(reader), + _ => Err(Error::SpectralDataInvalid), + } +} + +/// Dispatch one `hcod[cb]` codeword write onto the per-book writer. +fn write_codeword(writer: &mut BitWriter, cb: u8, idx: u32) -> Result<()> { + match cb { + 1 => hcod1_write(writer, idx), + 2 => hcod2_write(writer, idx), + 3 => hcod3_write(writer, idx), + 4 => hcod4_write(writer, idx), + 5 => hcod5_write(writer, idx), + 6 => hcod6_write(writer, idx), + 7 => hcod7_write(writer, idx), + 8 => hcod8_write(writer, idx), + 9 => hcod9_write(writer, idx), + 10 => hcod10_write(writer, idx), + 11 => hcod11_write(writer, idx), + _ => Err(Error::SpectralDataInvalid), + } +} + +/// For unsigned codebooks, read the `quad_sign_bits` / +/// `pair_sign_bits` field (one bit per non-zero coefficient, low +/// frequency first, `1` = negative) and apply it to the magnitude +/// tuple per §4.6.3.3. Signed codebooks pass through unchanged. +pub(crate) fn read_and_apply_signs( + reader: &mut BitReader<'_>, + cb: u8, + dim: usize, + tuple: [i32; 4], +) -> Result<[i32; 4]> { + let row = table_4_95(cb)?; + if !row.is_unsigned() { + return Ok(tuple); + } + let nonzero = tuple.iter().take(dim).filter(|&&v| v != 0).count(); + let mut signs = Vec::with_capacity(nonzero); + for _ in 0..nonzero { + signs.push(reader.read_bit().map_err(|_| Error::UnexpectedEnd)?); + } + apply_sign_bits(cb, tuple, &signs) +} + +/// Read one `hcod_esc_y` / `hcod_esc_z` escape sequence per +/// §4.6.3.3: an `escape_prefix` of `N` ones, a zero +/// `escape_separator`, and an `(N + 4)`-bit `escape_word`, decoding +/// to `2^(N+4) + escape_word`. §4.6.2 caps the *encoded* magnitude +/// at `MAX_QUANT` (`N ≤ 8`), but the decoder accepts up to `N == 24` +/// — the ISO/IEC 14496-26 ER AAC LD conformance vectors transmit +/// escapes up to `N == 15` (magnitude 783 966) whose reference +/// waveforms require the decoded value (see +/// [`crate::spectral_codebook::decode_esc_value`]); a longer prefix +/// run is rejected without consuming further bits. +pub(crate) fn read_escape_sequence(reader: &mut BitReader<'_>) -> Result { + let mut prefix_len = 0u32; + while reader.read_bit().map_err(|_| Error::UnexpectedEnd)? { + prefix_len += 1; + if prefix_len > 24 { + return Err(Error::SpectralCodebookEscOutOfRange); + } + } + let escape_word = reader + .read_u32(prefix_len + 4) + .map_err(|_| Error::UnexpectedEnd)?; + decode_esc_value(prefix_len, escape_word) +} + +/// Write one n-tuple: the Huffman codeword, the sign bits (unsigned +/// books), and the escape sequences (ESC book, magnitudes ≥ 16). +pub(crate) fn write_tuple( + writer: &mut BitWriter, + cb: u8, + dim: usize, + coeffs: &[i32], +) -> Result<()> { + // Build the in-band tuple: for the ESC book, magnitudes >= 16 + // are clamped to the ESC_FLAG (signed, so the sign survives for + // derive_sign_bits); §4.6.1.3 bounds the true magnitude at + // MAX_QUANT. + let mut tuple = [0i32; 4]; + for (slot, &v) in tuple.iter_mut().zip(coeffs.iter()) { + if cb == 11 && v.abs() >= ESC_FLAG { + if v.abs() > MAX_QUANT { + return Err(Error::SpectralCodebookEscOutOfRange); + } + *slot = v.signum() * ESC_FLAG; + } else { + *slot = v; + } + } + + let row = table_4_95(cb)?; + let index_tuple: Vec = if row.is_unsigned() { + tuple.iter().take(dim).map(|v| v.abs()).collect() + } else { + tuple[..dim].to_vec() + }; + let idx = encode_tuple_to_index(cb, &index_tuple)?; + write_codeword(writer, cb, idx)?; + + if row.is_unsigned() { + for neg in derive_sign_bits(cb, &tuple[..dim])? { + writer.write_bit(neg); + } + } + + if cb == 11 { + for (&clamped, &v) in tuple.iter().zip(coeffs.iter()).take(dim) { + if clamped.abs() == ESC_FLAG { + let (prefix_len, escape_word) = encode_esc_value(v.unsigned_abs())?; + for _ in 0..prefix_len { + writer.write_bit(true); + } + writer.write_bit(false); + writer.write_u32(escape_word, prefix_len + 4); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::WindowSequence; + use crate::section_data::ZERO_HCB; + + /// Build a long-window IcsInfo for fs_index 4 (44.1 kHz) with + /// the given max_sfb. + fn long_ics_info(max_sfb: u8) -> IcsInfo { + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::OnlyLong, + window_shape: crate::ics_info::WindowShape::Sine, + max_sfb, + scale_factor_grouping: None, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 1, + num_window_groups: 1, + window_group_length: vec![1], + num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], + } + } + + /// Build an EIGHT_SHORT IcsInfo for fs_index 4 with the given + /// grouping. + fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { + let num_window_groups = window_group_length.len() as u8; + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: WindowSequence::EightShort, + window_shape: crate::ics_info::WindowShape::Sine, + max_sfb, + scale_factor_grouping: Some(0), + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: 8, + num_window_groups, + window_group_length, + num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[4], + } + } + + fn one_section(num_groups: usize, codebook: u8, max_sfb: u8) -> SectionData { + let sections = (0..num_groups) + .map(|_| { + vec![Section { + codebook, + start: 0, + end: max_sfb, + }] + }) + .collect::>(); + let sfb_cb = (0..num_groups) + .map(|_| vec![codebook; max_sfb as usize]) + .collect::>(); + SectionData { sections, sfb_cb } + } + + fn round_trip( + data: &SpectralData, + ics_info: &IcsInfo, + section_data: &SectionData, + fs_index: u8, + ) -> SpectralData { + let mut writer = BitWriter::new(); + data.write(&mut writer, ics_info, section_data, fs_index) + .expect("write"); + let bytes = writer.finish(); + let mut reader = BitReader::new(&bytes); + SpectralData::parse(&mut reader, ics_info, section_data, fs_index).expect("parse") + } + + #[test] + fn sect_sfb_offset_long_mirrors_swb_table() { + let info = long_ics_info(10); + let offsets = sect_sfb_offset(&info, 4).expect("offsets"); + assert_eq!(offsets.len(), 1); + let swb = long_window_offsets(4).expect("table"); + assert_eq!(offsets[0].len(), 11); + for (i, &o) in offsets[0].iter().enumerate() { + assert_eq!(o, u32::from(swb[i])); + } + } + + #[test] + fn sect_sfb_offset_short_scales_by_group_length() { + // Grouping 5 + 3: each virtual band is wgl × the Table + // 4.130 band width. + let info = short_ics_info(4, vec![5, 3]); + let offsets = sect_sfb_offset(&info, 4).expect("offsets"); + assert_eq!(offsets.len(), 2); + let swb = short_window_offsets(4).expect("table"); + for (g, wgl) in [(0usize, 5u32), (1, 3)] { + for i in 0..4 { + let width = u32::from(swb[i + 1] - swb[i]) * wgl; + assert_eq!(offsets[g][i + 1] - offsets[g][i], width); + } + } + } + + #[test] + fn sect_sfb_offset_rejects_max_sfb_above_num_swb() { + let mut info = long_ics_info(50); + info.max_sfb = 50; // num_swb for fs 4 long is 49. + assert!(matches!( + sect_sfb_offset(&info, 4), + Err(Error::SpectralDataInvalid) + )); + } + + #[test] + fn all_zero_sections_consume_no_bits() { + let info = long_ics_info(10); + let sd = one_section(1, ZERO_HCB, 10); + let mut reader = BitReader::new(&[0xff, 0xff]); + let parsed = SpectralData::parse(&mut reader, &info, &sd, 4).expect("parse"); + assert_eq!(reader.bit_position(), 0); + assert_eq!(parsed.x_quant.len(), 1); + assert_eq!(parsed.x_quant[0].len(), 1024); + assert!(parsed.x_quant[0].iter().all(|&v| v == 0)); + } + + #[test] + fn quad_signed_book_round_trip() { + let info = long_ics_info(2); + let sd = one_section(1, 1, 2); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + // fs 4 long bands 0..2 cover coefficients 0..8. + data.x_quant[0][..8].copy_from_slice(&[1, -1, 0, 1, -1, 0, 0, 1]); + assert_eq!(round_trip(&data, &info, &sd, 4), data); + } + + #[test] + fn unsigned_pair_book_round_trip_with_signs() { + let info = long_ics_info(2); + let sd = one_section(1, 7, 2); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + data.x_quant[0][..8].copy_from_slice(&[7, -7, 0, 3, -1, 2, 0, -5]); + assert_eq!(round_trip(&data, &info, &sd, 4), data); + } + + #[test] + fn esc_book_round_trip_with_escapes() { + let info = long_ics_info(2); + let sd = one_section(1, 11, 2); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + // In-band, half-ESC, full-ESC, extreme magnitudes. + data.x_quant[0][..8].copy_from_slice(&[15, -15, 16, -16, 8191, -8191, 0, 100]); + assert_eq!(round_trip(&data, &info, &sd, 4), data); + } + + #[test] + fn esc_magnitude_16_uses_escape_sequence_00000() { + // §4.6.3.3 worked example: an escape_sequence of 00000 + // decodes as 16. Pin the wire layout for the tuple (16, 0): + // index 16*17+0 = 272 → 9-bit 0x1c2, one sign bit (0), then + // prefix-less escape 0 0000. + let info = long_ics_info(1); + let sd = one_section(1, 11, 1); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + data.x_quant[0][..4].copy_from_slice(&[16, 0, 0, 0]); + let mut writer = BitWriter::new(); + data.write(&mut writer, &info, &sd, 4).expect("write"); + // Band 0 at fs 4 long spans 4 coefficients = 2 pair tuples: + // (16, 0) then (0, 0). Codeword 0x1c2 (9 bits), sign 0, + // escape 00000 (5 bits), then (0,0) codeword 0b0000 (4 + // bits). Total 9 + 1 + 5 + 4 = 19 bits. + assert_eq!(writer.bit_position(), 19); + let bytes = writer.finish(); + let mut reader = BitReader::new(&bytes); + let parsed = SpectralData::parse(&mut reader, &info, &sd, 4).expect("parse"); + assert_eq!(reader.bit_position(), 19); + assert_eq!(parsed, data); + } + + #[test] + fn short_grouped_round_trip() { + // Two groups (5 + 3 windows); codebook 2 (signed quad) over + // 4 virtual bands per group. + let info = short_ics_info(4, vec![5, 3]); + let sd = one_section(2, 2, 4); + let offsets = sect_sfb_offset(&info, 4).expect("offsets"); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 5 * 128], vec![0i32; 3 * 128]], + }; + for (g, group_offsets) in offsets.iter().enumerate() { + let end = group_offsets[4] as usize; + for k in 0..end { + data.x_quant[g][k] = match k % 3 { + 0 => 1, + 1 => -1, + _ => 0, + }; + } + } + assert_eq!(round_trip(&data, &info, &sd, 4), data); + } + + #[test] + fn parse_rejects_reserved_codebook_12() { + let info = long_ics_info(2); + let sd = one_section(1, 12, 2); + let mut reader = BitReader::new(&[0x00; 8]); + assert!(matches!( + SpectralData::parse(&mut reader, &info, &sd, 4), + Err(Error::SpectralDataInvalid) + )); + } + + #[test] + fn parse_rejects_group_count_mismatch() { + let info = long_ics_info(2); + let sd = one_section(2, 1, 2); // two groups vs long's one + let mut reader = BitReader::new(&[0x00; 8]); + assert!(matches!( + SpectralData::parse(&mut reader, &info, &sd, 4), + Err(Error::SpectralDataInvalid) + )); + } + + #[test] + fn parse_rejects_truncated_codeword() { + let info = long_ics_info(2); + let sd = one_section(1, 9, 2); + // Codebook 9 max codeword is 15 bits; an all-ones byte is a + // prefix of longer codewords, so a 1-byte buffer underflows. + let mut reader = BitReader::new(&[0xff]); + assert!(matches!( + SpectralData::parse(&mut reader, &info, &sd, 4), + Err(Error::UnexpectedEnd) + )); + } + + #[test] + fn write_rejects_nonzero_outside_sections() { + let info = long_ics_info(2); + let sd = one_section(1, ZERO_HCB, 2); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + data.x_quant[0][0] = 1; + let mut writer = BitWriter::new(); + assert!(matches!( + data.write(&mut writer, &info, &sd, 4), + Err(Error::SpectralDataEncodeInvalid) + )); + } + + #[test] + fn write_rejects_nonzero_above_max_sfb() { + let info = long_ics_info(2); + let sd = one_section(1, 1, 2); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + data.x_quant[0][1023] = 1; + let mut writer = BitWriter::new(); + assert!(matches!( + data.write(&mut writer, &info, &sd, 4), + Err(Error::SpectralDataEncodeInvalid) + )); + } + + #[test] + fn write_rejects_magnitude_above_lav() { + let info = long_ics_info(2); + let sd = one_section(1, 1, 2); // codebook 1, LAV 1 + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + data.x_quant[0][0] = 2; + let mut writer = BitWriter::new(); + assert!(matches!( + data.write(&mut writer, &info, &sd, 4), + Err(Error::SpectralCodebookTupleOutOfRange(1)) + )); + } + + #[test] + fn write_rejects_esc_magnitude_above_max_quant() { + let info = long_ics_info(2); + let sd = one_section(1, 11, 2); + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + data.x_quant[0][0] = MAX_QUANT + 1; + let mut writer = BitWriter::new(); + assert!(matches!( + data.write(&mut writer, &info, &sd, 4), + Err(Error::SpectralCodebookEscOutOfRange) + )); + } + + #[test] + fn write_rejects_wrong_group_buffer_length() { + let info = long_ics_info(2); + let sd = one_section(1, 1, 2); + let data = SpectralData { + x_quant: vec![vec![0i32; 512]], + }; + let mut writer = BitWriter::new(); + assert!(matches!( + data.write(&mut writer, &info, &sd, 4), + Err(Error::SpectralDataEncodeInvalid) + )); + } + + #[test] + fn escape_prefix_run_past_24_rejected() { + // A run of >24 ones exceeds the decoder-side tolerance bound + // (the ISO conformance vectors reach N == 15; the cap guards + // hostile all-ones input). + let mut reader = BitReader::new(&[0xff, 0xff, 0xff, 0xff]); + assert!(matches!( + read_escape_sequence(&mut reader), + Err(Error::SpectralCodebookEscOutOfRange) + )); + } + + #[test] + fn escape_sequence_examples_from_spec() { + // §4.6.3.3: 00000 → 16, 01111 → 31, 1000000 → 32, + // 1011111 → 63. + for (bits, len, expect) in [ + (0b00000u32, 5u32, 16u32), + (0b01111, 5, 31), + (0b1000000, 7, 32), + (0b1011111, 7, 63), + ] { + let mut writer = BitWriter::new(); + writer.write_u32(bits, len); + let bytes = writer.finish(); + let mut reader = BitReader::new(&bytes); + assert_eq!(read_escape_sequence(&mut reader).expect("esc"), expect); + assert_eq!(reader.bit_position(), u64::from(len)); + } + } + + #[test] + fn multi_section_mixed_codebooks_round_trip() { + // Bands 0..2 on book 1 (quad), 2..4 zero, 4..6 on book 11. + let info = long_ics_info(6); + let sections = vec![vec![ + Section { + codebook: 1, + start: 0, + end: 2, + }, + Section { + codebook: ZERO_HCB, + start: 2, + end: 4, + }, + Section { + codebook: 11, + start: 4, + end: 6, + }, + ]]; + let sfb_cb = vec![vec![1, 1, ZERO_HCB, ZERO_HCB, 11, 11]]; + let sd = SectionData { sections, sfb_cb }; + let mut data = SpectralData { + x_quant: vec![vec![0i32; 1024]], + }; + // fs 4 long: bands are 4 wide here, so 0..8 book 1, 8..16 + // zero, 16..24 book 11. + data.x_quant[0][..8].copy_from_slice(&[1, 0, -1, 0, 0, 1, 1, -1]); + data.x_quant[0][16..24].copy_from_slice(&[20, -3, 0, 0, 1000, -16, 15, 0]); + assert_eq!(round_trip(&data, &info, &sd, 4), data); + } +} diff --git a/crates/vendor/oxideav-aac/src/spectrum_huffman.rs b/crates/vendor/oxideav-aac/src/spectrum_huffman.rs new file mode 100644 index 00000000..09d96306 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/spectrum_huffman.rs @@ -0,0 +1,5593 @@ +//! Spectrum Huffman codebook **wire** layer — ISO/IEC 14496-3 +//! §4.6.3 + Annex 4.A (Tables 4.A.2 … 4.A.12). +//! +//! Round 213 landed [`crate::spectral_codebook`] — the §4.6.3.3 index ↔ +//! n-tuple translation, the §4.6.3 sign-bit fix-up, and the codebook-11 +//! ESC sequence. That module does **not** carry the Huffman codeword +//! tables themselves: it operates on the *index* the wire bitstream +//! decodes to, leaving the codeword ↔ index mapping for this module +//! to own. +//! +//! Round 219 landed the first of the eleven spectrum Huffman +//! codebooks — **Table 4.A.2, "Spectrum Huffman Codebook 1"**. Round +//! 226 added the second — **Table 4.A.3, "Spectrum Huffman Codebook +//! 2"**. Round 231 added the third — **Table 4.A.4, "Spectrum Huffman +//! Codebook 3"**. Round 234 added the fourth — **Table 4.A.5, "Spectrum +//! Huffman Codebook 4"**. Round 238 added the fifth — **Table 4.A.6, +//! "Spectrum Huffman Codebook 5"** — the first **pair** (`dim = 2`) +//! book and the first book to widen its codewords to 13 bits. Round +//! 241 adds the sixth — **Table 4.A.7, "Spectrum Huffman Codebook +//! 6"** — the second pair book, sharing the Codebook 5 Table 4.95 +//! row shape (`signed`, `dim = 2`, `LAV = 4`) but tightening the +//! codeword ceiling back down to 11 bits. Round 244 adds the +//! seventh — **Table 4.A.8, "Spectrum Huffman Codebook 7"** — the +//! first **unsigned pair** book (Table 4.95 row 7: `unsigned_cb = 1`, +//! `dim = 2`, `LAV = 7`), widening the per-coefficient magnitude +//! range to `0..=7` and parking the §4.6.3.3 zero-tuple `(0, 0)` at +//! index 0 with a single-bit `0` codeword. Round 250 adds the +//! eighth — **Table 4.A.9, "Spectrum Huffman Codebook 8"** — the +//! second **unsigned pair** book, sharing the Codebook 7 Table 4.95 +//! row shape (`unsigned_cb = 1`, `dim = 2`, `LAV = 7` → 64 entries +//! indexed `0..=63`) but tightening the codeword ceiling down to +//! 10 bits and migrating the shortest codeword off the §4.6.3.3 +//! zero-tuple `(0, 0)` at index 0 (which now carries a 5-bit +//! `0b01110`) onto the interior tuple `(1, 1)` at index 9 (which +//! carries the 3-bit `0b000`). Round 253 adds the ninth — **Table +//! 4.A.10, "Spectrum Huffman Codebook 9"** — the first +//! **expanded-LAV unsigned pair** book (Table 4.95 row 9: +//! `unsigned_cb = 1`, `dim = 2`, `LAV = 12`), exercising the +//! §4.6.3.3 universe expansion to a `(12 + 1)^2 = 13^2 = 169`-entry +//! lattice indexed `0..=168` with each `(y, z)` coefficient in +//! `0..=12`. Codebook 9 parks the §4.6.3.3 zero-tuple `(0, 0)` at +//! index 0 with a single-bit `0` codeword (matching the head- +//! placement of Codebook 7) and pins the far corner `(12, 12)` at +//! index 168 with a 15-bit `0x7fff` — the widest codeword among +//! the non-ESC spectrum books. +//! Codebooks 1 and 2 share the same Table 4.95 +//! row shape (`signed`, `dim = 4`, `LAV = 1` → `3^4 = 81` entries +//! indexed `0..=80`); Codebooks 3 and 4 share the unsigned dim-4 +//! shape (Table 4.95 rows 3 and 4 both: `unsigned_cb = 1`, `dim = 4`, +//! `LAV = 2` → `3^4 = 81` entries indexed `0..=80`, with sign bits +//! following the Huffman codeword for every non-zero coefficient per +//! §4.6.3.3); Codebooks 5 and 6 share the signed pair shape +//! (Table 4.95 rows 5 and 6 both: `unsigned_cb = 0`, `dim = 2`, +//! `LAV = 4` → `(2 * 4 + 1)^2 = 9^2 = 81` entries indexed `0..=80`, +//! each tuple coefficient in `-4..=+4`, signed-book so no sign-bit +//! suffix is required after the codeword). Codebook 7 is the first +//! unsigned pair book (Table 4.95 row 7: `unsigned_cb = 1`, `dim = 2`, +//! `LAV = 7` → `(7 + 1)^2 = 8^2 = 64` entries indexed `0..=63`, each +//! tuple coefficient in `0..=7`, sign-bit suffix follows the codeword +//! for each non-zero coefficient per §4.6.3.3); Codebook 8 shares +//! the same unsigned dim-2 LAV-7 shape (Table 4.95 row 8 column-for- +//! column matches row 7 except for the `Codebook listed in Table` +//! cell pointing at Table 4.A.9). Codebook 9 (Table 4.95 row 9) +//! widens the per-coefficient ceiling to `LAV = 12` — the §4.6.3.3 +//! universe grows from `8 × 8 = 64` to `13 × 13 = 169` entries — +//! making it the largest of the non-ESC spectrum books. +//! Round 255 adds the tenth — **Table 4.A.11, "Spectrum Huffman +//! Codebook 10"** — the second **expanded-LAV unsigned pair** book +//! (Table 4.95 row 10: `unsigned_cb = 1`, `dim = 2`, `LAV = 12` → +//! 169 entries indexed `0..=168`, the same `13 × 13` universe +//! Codebook 9 covers). Codebook 10 trades Codebook 9's +//! zero-tuple-at-the-1-bit-head distribution for a flatter codeword +//! profile: the zero-tuple `(0, 0)` at index 0 now carries a 6-bit +//! `0b100010` (`0x22`), the shortest slot (4 bits, codeword `0b0000`) +//! migrates onto the interior `(1, 1)` tuple at index 14, and the +//! codeword ceiling pulls down from Codebook 9's 15 bits to **12 +//! bits** — matching the head-displacement pattern Codebook 8 uses +//! relative to Codebook 7 (one row lifted from the 1-bit slot, +//! shortest codeword moved off the zero-tuple) but at the wider +//! `LAV = 12` universe. +//! Round 259 adds the eleventh — **Table 4.A.12, "Spectrum Huffman +//! Codebook 11"** — the only **ESC** spectrum book (Table 4.95 +//! row 11: `unsigned_cb = 1`, `dim = 2`, `LAV = 16` with an ESC +//! threshold of `8191` — the §4.6.1.3 `x_quant` ceiling). The +//! §4.6.3.3 in-band universe widens to a `17 × 17 = 289`-entry +//! lattice indexed `0..=288` with each `(y, z)` coefficient in +//! `0..=16`; a coefficient value of `16` in either slot is the +//! §4.6.3.3 `escape_flag` whose actual magnitude is reconstructed +//! from the `escape_sequence` (`escape_prefix` of N `1`s, a `0` +//! `escape_separator`, and an `(N + 4)`-bit `escape_word`) bridged +//! by [`crate::spectral_codebook::decode_esc_value`] / +//! [`crate::spectral_codebook::encode_esc_value`] — both already +//! landed in round 213, separate from the Huffman codeword this +//! module carries. Codebook 11 parks the zero-tuple `(0, 0)` at +//! index 0 with the shortest 4-bit codeword `0b0000`, shares that +//! 4-bit floor with the interior `(1, 1)` pair at index 18 (the +//! second 4-bit slot, codeword `0b0001`), pins the half-ESC tuples +//! `(0, 16)` and `(16, 0)` to 10-bit `0x38e` (index 16) and 9-bit +//! `0x1c2` (index 272), and parks the full-ESC corner `(16, 16)` +//! at index 288 with the surprisingly short 5-bit `0b00100` +//! (`0x04`) — the wire layout extends with two sign bits and two +//! escape sequences for that corner, so the Huffman codeword +//! itself stays short. The codeword ceiling matches Codebook 10's +//! 12 bits — exactly six rows reach it (indices 12, 14, 15, 255, +//! 269, 270) — because Codebook 11 pushes its tail distribution +//! out of the Huffman table and into the §4.6.3 ESC sequence. +//! With Codebook 11 the per-codebook AAC spectrum Huffman tables +//! are complete (Tables 4.A.2 through 4.A.12 all land in this +//! module); the next step is the §4.4.6 `spectral_data()` wire +//! walker that loops over scalefactor bands and dispatches per-band +//! onto the codebook chosen by `section_data()`. +//! +//! ## Codebook 1 invariants (Table 4.A.2) +//! +//! | property | value | source | +//! |------------------------|-----------|------------------------------| +//! | dimension | 4 | Table 4.95 row 1, column 3 | +//! | `unsigned_cb` | 0 (signed)| Table 4.95 row 1, column 2 | +//! | LAV | 1 | Table 4.95 row 1, column 4 | +//! | entry count | `3^4 = 81`| `(2 * 1 + 1)^4` per §4.6.3.3 | +//! | maximum codeword length| 11 bits | Table 4.A.2 column 2 maximum | +//! | shortest codeword | 1 bit | Table 4.A.2 row 40 (index 40)| +//! | shortest codeword value| `0` | Table 4.A.2 row 40 | +//! | Kraft equality | 2048 = 2¹¹| see [`hcod1_is_complete`] | +//! +//! Index 40 is `(w, x, y, z) = (0, 0, 0, 0)` per §4.6.3.3 — the +//! zero-tuple gets the single-bit codeword because zero-tuples are +//! the modal spectrum n-tuple in any non-silent frame. +//! +//! ## Wire representation in memory +//! +//! Codewords are stored right-aligned within a `u16`: the MSB of the +//! wire codeword sits at bit `length − 1`, the LSB at bit `0`. To emit +//! bit-for-bit, [`hcod1_encode`] returns `(length, codeword)` and the +//! caller passes them straight to +//! [`oxideav_core::bits::BitWriter::write_u32`]. +//! +//! ## Codebook 2 invariants (Table 4.A.3) +//! +//! | property | value | source | +//! |------------------------|-----------|------------------------------| +//! | dimension | 4 | Table 4.95 row 2, column 3 | +//! | `unsigned_cb` | 0 (signed)| Table 4.95 row 2, column 2 | +//! | LAV | 1 | Table 4.95 row 2, column 4 | +//! | entry count | `3^4 = 81`| `(2 * 1 + 1)^4` per §4.6.3.3 | +//! | maximum codeword length| 9 bits | Table 4.A.3 column 2 maximum | +//! | shortest codeword | 3 bits | Table 4.A.3 row 40 (index 40)| +//! | shortest codeword value| `0` | Table 4.A.3 row 40 | +//! | Kraft equality | 512 = 2⁹ | see [`hcod2_is_complete`] | +//! +//! Codebook 2 covers the same `3^4 = 81` signed 4-tuple universe as +//! Codebook 1, with each coefficient in `(-1, 0, +1)`. The encoder +//! chooses between the two books per-section based on +//! `section_data()`'s `sect_cb` field; the choice reflects which book +//! gives the shorter overall bit count for the section's tuple +//! statistics. Index 40 is `(w, x, y, z) = (0, 0, 0, 0)` in both +//! books; in Codebook 2 it carries the 3-bit codeword `0b000` +//! (vs the single bit `0` in Codebook 1). +//! +//! ## Codebook 3 invariants (Table 4.A.4) +//! +//! | property | value | source | +//! |------------------------|------------|------------------------------| +//! | dimension | 4 | Table 4.95 row 3, column 3 | +//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 3, column 2 | +//! | LAV | 2 | Table 4.95 row 3, column 4 | +//! | entry count | `3^4 = 81` | `(2 + 1)^4` per §4.6.3.3 | +//! | maximum codeword length| 16 bits | Table 4.A.4 column 2 maximum | +//! | shortest codeword | 1 bit | Table 4.A.4 row 0 (index 0) | +//! | shortest codeword value| `0` | Table 4.A.4 row 0 | +//! | Kraft equality | 65536 = 2¹⁶| see [`hcod3_is_complete`] | +//! +//! Codebook 3 is the first *unsigned* spectrum book: the Huffman +//! codeword conveys the magnitude n-tuple (each coefficient in +//! `0..=LAV = 0..=2`) and each non-zero coefficient is followed by a +//! single sign bit per §4.6.3.3 (the sign bits travel in +//! low-frequency-first order: `w`, `x`, `y`, `z`). The zero-tuple +//! `(0, 0, 0, 0)` is at *index 0* (not 40 as in the signed books) +//! because the unsigned modulus-3 polynomial puts all-zero at the +//! origin; it carries the single bit codeword `0`. The §4.6.3.3 +//! sign-bit suffix is exposed by +//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +//! and is *not* part of the Huffman codeword itself — this module's +//! `hcod3_encode` / `hcod3_decode` cover the codeword only. +//! +//! ## Codebook 4 invariants (Table 4.A.5) +//! +//! | property | value | source | +//! |------------------------|------------|------------------------------| +//! | dimension | 4 | Table 4.95 row 4, column 3 | +//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 4, column 2 | +//! | LAV | 2 | Table 4.95 row 4, column 4 | +//! | entry count | `3^4 = 81` | `(2 + 1)^4` per §4.6.3.3 | +//! | maximum codeword length| 12 bits | Table 4.A.5 column 2 maximum | +//! | shortest codeword | 4 bits | Table 4.A.5 row 40 (index 40)| +//! | shortest codeword value| `0` | Table 4.A.5 row 40 | +//! | Kraft equality | 4096 = 2¹² | see [`hcod4_is_complete`] | +//! +//! Codebook 4 shares Codebook 3's unsigned dim-4 LAV-2 tuple universe +//! (Table 4.95 row 4 is identical to row 3 except for the `Codebook +//! listed in Table` column) but uses a different per-row Huffman +//! length tuning for a different encoder target-statistics. Where +//! Codebook 3 puts the zero-tuple at index 0 with a single-bit +//! codeword and lets the magnitude-2 tuples climb to a 16-bit +//! maximum, Codebook 4 puts the zero-tuple at the *same* §4.6.3.3 +//! polynomial position (index 0 maps the unsigned `(0, 0, 0, 0)` +//! tuple via the `((w*3 + x)*3 + y)*3 + z` evaluation with no offset) +//! — but the codeword assignment lifts the zero-tuple to a 4-bit +//! codeword (`0b0111`) and parks the *shortest* codeword (4 bits +//! `0b0000`) at **index 40** instead. The maximum codeword length is +//! **12 bits** (vs 16 for Codebook 3), and two distinct rows reach +//! that length: index 62 (`0xfff`) and index 74 (`0xffe`). The +//! shorter overall code length distribution makes Codebook 4 a +//! better fit for sections whose magnitude statistics are flatter +//! across the `(0, 0, 0, 0) .. (2, 2, 2, 2)` range than Codebook 3's +//! zero-heavy target. The §4.6.3.3 sign-bit suffix is again exposed +//! by [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) +//! / [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +//! and is *not* part of the Huffman codeword itself — this module's +//! `hcod4_encode` / `hcod4_decode` cover the codeword only. +//! +//! ## Codebook 5 invariants (Table 4.A.6) +//! +//! | property | value | source | +//! |------------------------|------------|------------------------------| +//! | dimension | 2 (pair) | Table 4.95 row 5, column 3 | +//! | `unsigned_cb` | 0 (signed) | Table 4.95 row 5, column 2 | +//! | LAV | 4 | Table 4.95 row 5, column 4 | +//! | entry count | `9^2 = 81` | `(2 * 4 + 1)^2` per §4.6.3.3 | +//! | maximum codeword length| 13 bits | Table 4.A.6 column 2 maximum | +//! | shortest codeword | 1 bit | Table 4.A.6 row 40 (index 40)| +//! | shortest codeword value| `0` | Table 4.A.6 row 40 | +//! | Kraft equality | 8192 = 2¹³ | see [`hcod5_is_complete`] | +//! +//! Codebook 5 is the first **pair** book — the §4.6.3.3 translation +//! consumes two coefficients per Huffman codeword (`(y, z)`) rather +//! than four (`(w, x, y, z)`) — and the first book to widen the +//! per-coefficient quantised range to `-4..=+4` (LAV = 4). The pair +//! universe stays at 81 entries because `(2 * 4 + 1)^2 = 9^2 = 81` +//! coincides with the dim-4 LAV-1 / LAV-2 universes of Codebooks +//! 1..=4. Index 40 carries the §4.6.3.3 zero-tuple `(0, 0)` — the +//! `(modulus = 9, offset = 4)` polynomial evaluation puts the +//! origin at the centre of the index range, not at the edges as in +//! the unsigned books (Codebooks 3 and 4 placed `(0, 0, 0, 0)` at +//! index 0). The shortest codeword (1 bit `0`) parks at index 40 +//! — the same zero-tuple position as Codebook 1 (whose dim-4 origin +//! also lands at the row-40 centre via the same signed-book +//! polynomial). The maximum codeword length is **13 bits** — one +//! more than Codebook 4's 12-bit ceiling and three less than +//! Codebook 3's 16-bit reach — and exactly four rows occupy the +//! 13-bit ceiling: indices 0, 8, 72, and 80 (the four corners +//! `(-4, -4)`, `(-4, +4)`, `(+4, -4)`, `(+4, +4)` of the +//! `9 × 9` signed pair lattice). Because Codebook 5 is **signed**, +//! the §4.6.3.3 sign-bit suffix is *not* emitted after the +//! codeword — every coefficient's sign is baked into the index +//! itself via the `offset = LAV = 4` shift. +//! +//! ## Codebook 6 invariants (Table 4.A.7) +//! +//! | property | value | source | +//! |------------------------|------------|------------------------------| +//! | dimension | 2 (pair) | Table 4.95 row 6, column 3 | +//! | `unsigned_cb` | 0 (signed) | Table 4.95 row 6, column 2 | +//! | LAV | 4 | Table 4.95 row 6, column 4 | +//! | entry count | `9^2 = 81` | `(2 * 4 + 1)^2` per §4.6.3.3 | +//! | maximum codeword length| 11 bits | Table 4.A.7 column 2 maximum | +//! | shortest codeword | 4 bits | Table 4.A.7 row 40 (index 40)| +//! | shortest codeword value| `0` | Table 4.A.7 row 40 | +//! | Kraft equality | 2048 = 2¹¹| see [`hcod6_is_complete`] | +//! +//! ## Codebook 7 invariants (Table 4.A.8) +//! +//! | property | value | source | +//! |------------------------|------------|------------------------------| +//! | dimension | 2 (pair) | Table 4.95 row 7, column 3 | +//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 7, column 2 | +//! | LAV | 7 | Table 4.95 row 7, column 4 | +//! | entry count | `8^2 = 64` | `(7 + 1)^2` per §4.6.3.3 | +//! | maximum codeword length| 12 bits | Table 4.A.8 column 2 maximum | +//! | shortest codeword | 1 bit | Table 4.A.8 row 0 (index 0) | +//! | shortest codeword value| `0` | Table 4.A.8 row 0 | +//! | Kraft equality | 4096 = 2¹²| see [`hcod7_is_complete`] | +//! +//! Codebook 7 is the first **unsigned pair** spectrum book — the +//! §4.6.3.3 translation consumes two coefficients per Huffman codeword +//! (`(y, z)`) with each coefficient in `0..=LAV = 0..=7`. The pair +//! universe has `(7 + 1)^2 = 64` entries indexed `0..=63`, a notable +//! drop from the 81-entry universe of Codebooks 1..=6 — the higher +//! per-coefficient ceiling (LAV = 7 vs LAV = 1, 2, 4 in the earlier +//! books) trades dimensionality for range. Like the unsigned dim-4 +//! books (Codebooks 3 and 4) the zero-tuple sits at *index 0* (not +//! 40 as in the signed books); the unsigned polynomial +//! `idx = y * (LAV + 1) + z = y * 8 + z` puts all-zero at the origin +//! and the maximum tuple `(7, 7)` at index 63. The single-bit +//! codeword `0` parks at index 0 — the same shortest-codeword position +//! as Codebook 3. The §4.6.3.3 sign-bit suffix applies after every +//! non-zero coefficient (the sign bits travel in low-frequency-first +//! order: `y`, `z`); the suffix is exposed by +//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +//! and is *not* part of the Huffman codeword itself — this module's +//! `hcod7_encode` / `hcod7_decode` cover the codeword only. +//! +//! ## Codebook 8 invariants (Table 4.A.9) +//! +//! | property | value | source | +//! |------------------------|------------|------------------------------| +//! | dimension | 2 (pair) | Table 4.95 row 8, column 3 | +//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 8, column 2 | +//! | LAV | 7 | Table 4.95 row 8, column 4 | +//! | entry count | `8^2 = 64` | `(7 + 1)^2` per §4.6.3.3 | +//! | maximum codeword length| 10 bits | Table 4.A.9 column 2 maximum | +//! | shortest codeword | 3 bits | Table 4.A.9 row 9 (index 9) | +//! | shortest codeword value| `0` | Table 4.A.9 row 9 | +//! | Kraft equality | 1024 = 2¹⁰| see [`hcod8_is_complete`] | +//! +//! Codebook 8 shares Codebook 7's unsigned pair tuple universe +//! (Table 4.95 row 8 is identical to row 7 except for the `Codebook +//! listed in Table` column) but uses a different per-row Huffman +//! length tuning. Where Codebook 7 pins the §4.6.3.3 zero-tuple +//! `(0, 0)` to index 0 with the single-bit codeword `0` and lets +//! the upper-right quadrant of the lattice climb to a 12-bit +//! ceiling, Codebook 8 lifts the zero-tuple at index 0 to a 5-bit +//! `0b01110` and migrates the shortest codeword (3 bits `0b000`) to +//! **index 9** — the unsigned-polynomial position of the interior +//! tuple `(y, z) = (1, 1)` (`idx = 1 * 8 + 1 = 9`). The maximum +//! codeword length is **10 bits**; exactly four rows reach the +//! ceiling: indices 7 (`0x3fe`), 47 (`0x3fc`), 56 (`0x3fd`), and +//! 63 (`0x3ff`) — the rarest pair magnitudes (one or two +//! coefficients at the LAV cap). The flatter, lower-ceiling +//! codeword distribution makes Codebook 8 a better fit for sections +//! whose magnitude statistics put weight on the `(1, 1)` interior +//! rather than the `(0, 0)` zero-tuple corner Codebook 7 +//! optimises. Because Codebook 8 is unsigned, the §4.6.3.3 sign-bit +//! suffix follows the Huffman codeword on the wire — one sign bit +//! per non-zero coefficient, low-frequency-first — and is exposed +//! by [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) +//! / [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +//! and is *not* part of the Huffman codeword itself — this module's +//! `hcod8_encode` / `hcod8_decode` cover the codeword only. +//! +//! ## Codebook 9 invariants (Table 4.A.10) +//! +//! | property | value | source | +//! |------------------------|---------------|-------------------------------| +//! | dimension | 2 (pair) | Table 4.95 row 9, column 3 | +//! | `unsigned_cb` | 1 (unsigned) | Table 4.95 row 9, column 2 | +//! | LAV | 12 | Table 4.95 row 9, column 4 | +//! | entry count | `13^2 = 169` | `(12 + 1)^2` per §4.6.3.3 | +//! | maximum codeword length| 15 bits | Table 4.A.10 column 2 maximum | +//! | shortest codeword | 1 bit | Table 4.A.10 row 0 (index 0) | +//! | shortest codeword value| `0` | Table 4.A.10 row 0 | +//! | Kraft equality | 32768 = 2¹⁵ | see [`hcod9_is_complete`] | +//! +//! Codebook 9 is the first **expanded-LAV pair** spectrum book — it +//! steps away from the `8 × 8` unsigned pair lattice Codebooks 7 and +//! 8 share and widens the per-coefficient ceiling from `7` to `12`, +//! producing a `13 × 13 = 169`-entry universe indexed `0..=168` with +//! each `(y, z)` coefficient in `0..=12`. The §4.6.3.3 unsigned +//! polynomial `idx = y * (LAV + 1) + z = y * 13 + z` parks the +//! zero-tuple `(0, 0)` at index 0 — the same head placement +//! Codebook 7 uses — and pins the maximum tuple `(12, 12)` at index +//! 168 (the far corner of the `13 × 13` unsigned lattice). The +//! single-bit codeword `0` lives at index 0, matching the +//! shortest-slot placement Codebook 7 also uses for its zero-tuple. +//! The maximum codeword length is **15 bits** — a 5-bit jump up +//! from Codebook 8's 10-bit ceiling and the widest non-ESC spectrum +//! codeword in the entire Annex 4.A book set — reflecting the +//! `169 / 64 ≈ 2.6×` universe expansion that widens the +//! distribution's tail. Exactly four rows reach the 15-bit ceiling: +//! indices 142 (`0x7ffc`), 154 (`0x7ffd`), 155 (`0x7ffe`), and 168 +//! (`0x7fff`) — the rarest pair magnitudes, sitting near the +//! `LAV = 12` cap. The table is a **complete** 15-bit prefix code +//! (Kraft equality `Σ 2^(15 − L) = 32768 = 2¹⁵`), exhaustively +//! verified by walking every 15-bit prefix and asserting each maps +//! to exactly one entry. Because Codebook 9 is unsigned, the +//! §4.6.3.3 sign-bit suffix follows the Huffman codeword on the +//! wire — one sign bit per non-zero coefficient, low-frequency- +//! first — and is exposed by +//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +//! and is *not* part of the Huffman codeword itself — this module's +//! `hcod9_encode` / `hcod9_decode` cover the codeword only. +//! +//! ## Codebook 10 invariants (Table 4.A.11) +//! +//! | property | value | source | +//! |------------------------|---------------|-------------------------------| +//! | dimension | 2 (pair) | Table 4.95 row 10, column 3 | +//! | `unsigned_cb` | 1 (unsigned) | Table 4.95 row 10, column 2 | +//! | LAV | 12 | Table 4.95 row 10, column 4 | +//! | entry count | `13^2 = 169` | `(12 + 1)^2` per §4.6.3.3 | +//! | maximum codeword length| 12 bits | Table 4.A.11 column 2 maximum | +//! | shortest codeword | 4 bits | Table 4.A.11 row 14 (index 14)| +//! | shortest codeword value| `0` | Table 4.A.11 row 14 | +//! | Kraft equality | 4096 = 2¹² | see [`hcod10_is_complete`] | +//! +//! Codebook 10 shares Codebook 9's expanded-LAV unsigned pair tuple +//! universe (Table 4.95 row 10 is identical to row 9 except for the +//! `Codebook listed in Table` column pointing at Table 4.A.11) but +//! uses a different per-row Huffman length tuning for a different +//! encoder target-statistics. Where Codebook 9 parks the §4.6.3.3 +//! zero-tuple `(0, 0)` at index 0 with the single-bit `0` codeword +//! and lets the four rarest pair magnitudes climb to a 15-bit +//! ceiling, Codebook 10 keeps the zero-tuple at index 0 (the +//! §4.6.3.3 polynomial position is fixed by the tuple) but its +//! codeword swells to 6 bits (`0x22`), the shortest 4-bit slot +//! migrates onto the interior `(1, 1)` tuple at index 14 with +//! codeword `0b0000`, and the codeword ceiling pulls down to +//! **12 bits**. Exactly three rows reach the 4-bit floor (indices +//! 14, 15, 27 with codewords `0x0`, `0x1`, `0x2`) and exactly eight +//! rows reach the 12-bit ceiling (indices 12, 129, 142, 155, 165, +//! 166, 167, 168 with codewords `0xffd`, `0xffa`, `0xff9`, `0xffb`, +//! `0xff8`, `0xffe`, `0xffc`, `0xfff`) — the four corners and four +//! near-edges of the `13 × 13` unsigned lattice. The flatter, +//! pull-down distribution makes Codebook 10 a better fit for +//! sections whose magnitude statistics put more weight in the +//! `(1..=4, 1..=4)` interior than Codebook 9's +//! more-zero-tuple-heavy target. The encoder chooses between the +//! two books per-section via `section_data()`'s `sect_cb` field; +//! the §4.6.3.3 sign-bit suffix follows the Huffman codeword on the +//! wire — one sign bit per non-zero coefficient, low-frequency- +//! first — and is exposed by +//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +//! and is *not* part of the Huffman codeword itself — this module's +//! `hcod10_encode` / `hcod10_decode` cover the codeword only. +//! +//! Codebook 6 shares Codebook 5's signed pair tuple universe +//! (Table 4.95 row 6 is identical to row 5 except for the `Codebook +//! listed in Table` column) but uses a different per-row Huffman +//! length tuning. Where Codebook 5 parks the single bit `0` at +//! index 40 and lets the four lattice corners reach a 13-bit +//! ceiling, Codebook 6 lifts the zero-tuple at index 40 to a 4-bit +//! `0b0000` and pulls the ceiling back to **11 bits**. Exactly four +//! rows reach the 11-bit ceiling: indices 0 (`0x7fe`), 8 (`0x7fd`), +//! 72 (`0x7ff`), and 80 (`0x7fc`) — the four `(±4, ±4)` corners of +//! the `9 × 9` signed pair lattice, the same four corner positions +//! Codebook 5 also pinned to its 13-bit ceiling. The shorter, +//! flatter codeword distribution makes Codebook 6 a better fit +//! for sections whose magnitude statistics put more weight in the +//! `(±1, ±1) .. (±3, ±3)` interior than Codebook 5's +//! more-zero-tuple-heavy target. The encoder chooses between the +//! two books per-section via `section_data()`'s `sect_cb` field; +//! the §4.6.3.3 sign bits remain inside the index for both books +//! because both are signed (`unsigned_cb = 0`). +//! +//! * The §4.6.3.3 index → n-tuple translation. That sits in +//! [`crate::spectral_codebook::decode_index_to_tuple`] / +//! [`crate::spectral_codebook::encode_tuple_to_index`]. +//! * The ESC sequence (codebook 11 and the extension books 16..=31). +//! That sits in [`crate::spectral_codebook::decode_esc_value`] / +//! [`crate::spectral_codebook::encode_esc_value`]. +//! * The §4.6.3 sign-bit suffix for unsigned codebooks. Codebook 1 +//! is *signed* so no sign bits follow the codeword; the +//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) +//! path is exercised by unsigned codebooks (3, 4, 7..=11, 16..=31). +//! * The `spectral_data()` driver that loops over scalefactor bands +//! and dispatches per-band onto the codebook chosen by +//! `section_data()`. That driver will land once codebooks 2..=11 +//! are in place. + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::{Error, Result}; + +// ============================================================================= +// Table 4.A.2 — Spectrum Huffman Codebook 1 +// ============================================================================= +// +// 81 entries, indices 0..=80. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the wire +// codeword at bit `length − 1`). Reproduced verbatim from ISO/IEC +// 14496-3:2001(E) §4.A.1 Table 4.A.2 (page 193). +// +// The codebook is a complete prefix code: Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹. +// This is exhaustively verified at compile time by the +// `hcod1_is_complete` regression test (which walks every 11-bit +// prefix and asserts each maps to exactly one index). + +/// Number of entries in Table 4.A.2 (`81`, indices `0..=80`). +pub const HCOD1_NUM_ENTRIES: usize = 81; + +/// Maximum codeword length emitted by Table 4.A.2 (11 bits). +pub const HCOD1_MAX_LEN: u32 = 11; + +/// Table 4.A.2 — `(length_in_bits, codeword)` per index `0..=80`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD1: [(u8, u16); HCOD1_NUM_ENTRIES] = [ + (11, 0x7f8), // 0 + (9, 0x1f1), // 1 + (11, 0x7fd), // 2 + (10, 0x3f5), // 3 + (7, 0x68), // 4 + (10, 0x3f0), // 5 + (11, 0x7f7), // 6 + (9, 0x1ec), // 7 + (11, 0x7f5), // 8 + (10, 0x3f1), // 9 + (7, 0x72), // 10 + (10, 0x3f4), // 11 + (7, 0x74), // 12 + (5, 0x11), // 13 + (7, 0x76), // 14 + (9, 0x1eb), // 15 + (7, 0x6c), // 16 + (10, 0x3f6), // 17 + (11, 0x7fc), // 18 + (9, 0x1e1), // 19 + (11, 0x7f1), // 20 + (9, 0x1f0), // 21 + (7, 0x61), // 22 + (9, 0x1f6), // 23 + (11, 0x7f2), // 24 + (9, 0x1ea), // 25 + (11, 0x7fb), // 26 + (9, 0x1f2), // 27 + (7, 0x69), // 28 + (9, 0x1ed), // 29 + (7, 0x77), // 30 + (5, 0x17), // 31 + (7, 0x6f), // 32 + (9, 0x1e6), // 33 + (7, 0x64), // 34 + (9, 0x1e5), // 35 + (7, 0x67), // 36 + (5, 0x15), // 37 + (7, 0x62), // 38 + (5, 0x12), // 39 + (1, 0x000), // 40 — zero-tuple, single bit `0` + (5, 0x14), // 41 + (7, 0x65), // 42 + (5, 0x16), // 43 + (7, 0x6d), // 44 + (9, 0x1e9), // 45 + (7, 0x63), // 46 + (9, 0x1e4), // 47 + (7, 0x6b), // 48 + (5, 0x13), // 49 + (7, 0x71), // 50 + (9, 0x1e3), // 51 + (7, 0x70), // 52 + (9, 0x1f3), // 53 + (11, 0x7fe), // 54 + (9, 0x1e7), // 55 + (11, 0x7f3), // 56 + (9, 0x1ef), // 57 + (7, 0x60), // 58 + (9, 0x1ee), // 59 + (11, 0x7f0), // 60 + (9, 0x1e2), // 61 + (11, 0x7fa), // 62 + (10, 0x3f3), // 63 + (7, 0x6a), // 64 + (9, 0x1e8), // 65 + (7, 0x75), // 66 + (5, 0x10), // 67 + (7, 0x73), // 68 + (9, 0x1f4), // 69 + (7, 0x6e), // 70 + (10, 0x3f7), // 71 + (11, 0x7f6), // 72 + (9, 0x1e0), // 73 + (11, 0x7f9), // 74 + (10, 0x3f2), // 75 + (7, 0x66), // 76 + (9, 0x1f5), // 77 + (11, 0x7ff), // 78 + (9, 0x1f7), // 79 + (11, 0x7f4), // 80 +]; + +/// Encode a Codebook 1 codeword index (`0..=80`) to the wire Huffman +/// codeword from Table 4.A.2. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=80` (the 81-entry `3^4` enumeration of every legal +/// signed 4-tuple with each coefficient in `-1..=+1`). +/// +/// The inverse of [`hcod1_decode`]. +pub fn hcod1_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD1 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(1))?; + Ok(*entry) +} + +/// Decode one Codebook 1 Huffman codeword from `reader`, returning +/// the codeword index in `0..=80`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 81-entry table. The table is +/// small (max codeword length 11 bits, 81 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 11 bits (Kraft +/// equality `Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹`), so any 11-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 11 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod1_is_complete` regression test that exhaustively +/// walks all `2¹¹` 11-bit prefixes. +pub fn hcod1_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD1_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD1.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD1 is a complete 11-bit prefix code. The + // `hcod1_is_complete` regression test verifies every 11-bit + // prefix maps to exactly one entry. + unreachable!("HCOD1 is a complete 11-bit prefix code; the 11-bit walk must match"); +} + +/// Write a Codebook 1 codeword to `writer` by index. +/// +/// Convenience over `hcod1_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. +pub fn hcod1_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod1_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.3 — Spectrum Huffman Codebook 2 +// ============================================================================= +// +// 81 entries, indices 0..=80. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the wire +// codeword at bit `length − 1`). Transcribed verbatim from ISO/IEC +// 14496-3:2001(E) §4.A.1 Table 4.A.3 (page 194). +// +// The codebook is a complete prefix code: Σᵢ 2^(9 − Lᵢ) = 512 = 2⁹. +// This is exhaustively verified by the `hcod2_is_complete` regression +// test (which walks every 9-bit prefix and asserts each maps to +// exactly one index). +// +// The signed-tuple universe is identical to Codebook 1's (3^4 = 81 +// signed 4-tuples with each element in `-1..=+1`); the §4.6.3.3 index +// translation in [`crate::spectral_codebook`] is reused as-is. + +/// Number of entries in Table 4.A.3 (`81`, indices `0..=80`). +pub const HCOD2_NUM_ENTRIES: usize = 81; + +/// Maximum codeword length emitted by Table 4.A.3 (9 bits). +pub const HCOD2_MAX_LEN: u32 = 9; + +/// Table 4.A.3 — `(length_in_bits, codeword)` per index `0..=80`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD2: [(u8, u16); HCOD2_NUM_ENTRIES] = [ + (9, 0x1f3), // 0 + (7, 0x6f), // 1 + (9, 0x1fd), // 2 + (8, 0xeb), // 3 + (6, 0x23), // 4 + (8, 0xea), // 5 + (9, 0x1f7), // 6 + (8, 0xe8), // 7 + (9, 0x1fa), // 8 + (8, 0xf2), // 9 + (6, 0x2d), // 10 + (7, 0x70), // 11 + (6, 0x20), // 12 + (5, 0x06), // 13 + (6, 0x2b), // 14 + (7, 0x6e), // 15 + (6, 0x28), // 16 + (8, 0xe9), // 17 + (9, 0x1f9), // 18 + (7, 0x66), // 19 + (8, 0xf8), // 20 + (8, 0xe7), // 21 + (6, 0x1b), // 22 + (8, 0xf1), // 23 + (9, 0x1f4), // 24 + (7, 0x6b), // 25 + (9, 0x1f5), // 26 + (8, 0xec), // 27 + (6, 0x2a), // 28 + (7, 0x6c), // 29 + (6, 0x2c), // 30 + (5, 0x0a), // 31 + (6, 0x27), // 32 + (7, 0x67), // 33 + (6, 0x1a), // 34 + (8, 0xf5), // 35 + (6, 0x24), // 36 + (5, 0x08), // 37 + (6, 0x1f), // 38 + (5, 0x09), // 39 + (3, 0x000), // 40 — zero-tuple, 3-bit codeword `0` + (5, 0x07), // 41 + (6, 0x1d), // 42 + (5, 0x0b), // 43 + (6, 0x30), // 44 + (8, 0xef), // 45 + (6, 0x1c), // 46 + (7, 0x64), // 47 + (6, 0x1e), // 48 + (5, 0x0c), // 49 + (6, 0x29), // 50 + (8, 0xf3), // 51 + (6, 0x2f), // 52 + (8, 0xf0), // 53 + (9, 0x1fc), // 54 + (7, 0x71), // 55 + (9, 0x1f2), // 56 + (8, 0xf4), // 57 + (6, 0x21), // 58 + (8, 0xe6), // 59 + (8, 0xf7), // 60 + (7, 0x68), // 61 + (9, 0x1f8), // 62 + (8, 0xee), // 63 + (6, 0x22), // 64 + (7, 0x65), // 65 + (6, 0x31), // 66 + (4, 0x02), // 67 + (6, 0x26), // 68 + (8, 0xed), // 69 + (6, 0x25), // 70 + (7, 0x6a), // 71 + (9, 0x1fb), // 72 + (7, 0x72), // 73 + (9, 0x1fe), // 74 + (7, 0x69), // 75 + (6, 0x2e), // 76 + (8, 0xf6), // 77 + (9, 0x1ff), // 78 + (7, 0x6d), // 79 + (9, 0x1f6), // 80 +]; + +/// Encode a Codebook 2 codeword index (`0..=80`) to the wire Huffman +/// codeword from Table 4.A.3. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the +/// codebook number `2`; the legal range is `0..=80` (the 81-entry +/// `3^4` enumeration of every legal signed 4-tuple with each +/// coefficient in `-1..=+1` — the same universe as Codebook 1). +/// +/// The inverse of [`hcod2_decode`]. +pub fn hcod2_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD2 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(2))?; + Ok(*entry) +} + +/// Decode one Codebook 2 Huffman codeword from `reader`, returning +/// the codeword index in `0..=80`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 81-entry table. The table is +/// small (max codeword length 9 bits, 81 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 9 bits (Kraft +/// equality `Σᵢ 2^(9 − Lᵢ) = 512 = 2⁹`), so any 9-bit prefix fully +/// read from `reader` is guaranteed to match exactly one entry — the +/// bottom of the loop is unreachable when `reader` produces 9 bits +/// without underflowing. A purely defensive `unreachable!()` guards +/// the loop fall-through; it is verified dead by the +/// `hcod2_is_complete` regression test that exhaustively walks all +/// `2⁹` 9-bit prefixes. +pub fn hcod2_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD2_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD2.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD2 is a complete 9-bit prefix code. The + // `hcod2_is_complete` regression test verifies every 9-bit + // prefix maps to exactly one entry. + unreachable!("HCOD2 is a complete 9-bit prefix code; the 9-bit walk must match"); +} + +/// Write a Codebook 2 codeword to `writer` by index. +/// +/// Convenience over `hcod2_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. +pub fn hcod2_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod2_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.4 — Spectrum Huffman Codebook 3 +// ============================================================================= +// +// 81 entries, indices 0..=80. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the +// wire codeword at bit `length − 1`). Transcribed verbatim from +// ISO/IEC 14496-3:2009(E) §4.A.1 Table 4.A.4. +// +// The codebook is a complete prefix code: Σᵢ 2^(16 − Lᵢ) = 65536 = 2¹⁶. +// This is exhaustively verified by the `hcod3_is_complete` regression +// test (which walks every 16-bit prefix and asserts each maps to +// exactly one index). +// +// Codebook 3 is the first *unsigned* spectrum book: each tuple +// coefficient is a non-negative magnitude in `0..=LAV = 0..=2`, and +// the §4.6.3.3 sign-bit suffix carries the sign of each non-zero +// coefficient outside the Huffman codeword. The §4.6.3.3 index ↔ +// 4-tuple translation lives in +// [`crate::spectral_codebook::decode_index_to_tuple`] / +// [`crate::spectral_codebook::encode_tuple_to_index`]; the sign-bit +// suffix lives in +// [`crate::spectral_codebook::apply_sign_bits`] / +// [`crate::spectral_codebook::derive_sign_bits`]. + +/// Number of entries in Table 4.A.4 (`81`, indices `0..=80`). +pub const HCOD3_NUM_ENTRIES: usize = 81; + +/// Maximum codeword length emitted by Table 4.A.4 (16 bits). +pub const HCOD3_MAX_LEN: u32 = 16; + +/// Table 4.A.4 — `(length_in_bits, codeword)` per index `0..=80`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD3: [(u8, u16); HCOD3_NUM_ENTRIES] = [ + (1, 0x0000), // 0 — zero-tuple, single bit `0` + (4, 0x0009), // 1 + (8, 0x00ef), // 2 + (4, 0x000b), // 3 + (5, 0x0019), // 4 + (8, 0x00f0), // 5 + (9, 0x01eb), // 6 + (9, 0x01e6), // 7 + (10, 0x03f2), // 8 + (4, 0x000a), // 9 + (6, 0x0035), // 10 + (9, 0x01ef), // 11 + (6, 0x0034), // 12 + (6, 0x0037), // 13 + (9, 0x01e9), // 14 + (9, 0x01ed), // 15 + (9, 0x01e7), // 16 + (10, 0x03f3), // 17 + (9, 0x01ee), // 18 + (10, 0x03ed), // 19 + (13, 0x1ffa), // 20 + (9, 0x01ec), // 21 + (9, 0x01f2), // 22 + (11, 0x07f9), // 23 + (11, 0x07f8), // 24 + (10, 0x03f8), // 25 + (12, 0x0ff8), // 26 + (4, 0x0008), // 27 + (6, 0x0038), // 28 + (10, 0x03f6), // 29 + (6, 0x0036), // 30 + (7, 0x0075), // 31 + (10, 0x03f1), // 32 + (10, 0x03eb), // 33 + (10, 0x03ec), // 34 + (12, 0x0ff4), // 35 + (5, 0x0018), // 36 + (7, 0x0076), // 37 + (11, 0x07f4), // 38 + (6, 0x0039), // 39 + (7, 0x0074), // 40 + (10, 0x03ef), // 41 + (9, 0x01f3), // 42 + (9, 0x01f4), // 43 + (11, 0x07f6), // 44 + (9, 0x01e8), // 45 + (10, 0x03ea), // 46 + (13, 0x1ffc), // 47 + (8, 0x00f2), // 48 + (9, 0x01f1), // 49 + (12, 0x0ffb), // 50 + (10, 0x03f5), // 51 + (11, 0x07f3), // 52 + (12, 0x0ffc), // 53 + (8, 0x00ee), // 54 + (10, 0x03f7), // 55 + (15, 0x7ffe), // 56 + (9, 0x01f0), // 57 + (11, 0x07f5), // 58 + (15, 0x7ffd), // 59 + (13, 0x1ffb), // 60 + (14, 0x3ffa), // 61 + (16, 0xffff), // 62 + (8, 0x00f1), // 63 + (10, 0x03f0), // 64 + (14, 0x3ffc), // 65 + (9, 0x01ea), // 66 + (10, 0x03ee), // 67 + (14, 0x3ffb), // 68 + (12, 0x0ff6), // 69 + (12, 0x0ffa), // 70 + (15, 0x7ffc), // 71 + (11, 0x07f2), // 72 + (12, 0x0ff5), // 73 + (16, 0xfffe), // 74 + (10, 0x03f4), // 75 + (11, 0x07f7), // 76 + (15, 0x7ffb), // 77 + (12, 0x0ff7), // 78 + (12, 0x0ff9), // 79 + (15, 0x7ffa), // 80 +]; + +/// Encode a Codebook 3 codeword index (`0..=80`) to the wire Huffman +/// codeword from Table 4.A.4. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the +/// codebook number `3`; the legal range is `0..=80` (the 81-entry +/// `3^4` enumeration of every legal unsigned 4-tuple with each +/// coefficient in `0..=LAV = 0..=2`). +/// +/// The inverse of [`hcod3_decode`]. The sign-bit suffix for each +/// non-zero coefficient is *not* part of the returned codeword — the +/// caller emits sign bits separately per +/// [`crate::spectral_codebook::derive_sign_bits`]. +pub fn hcod3_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD3 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(3))?; + Ok(*entry) +} + +/// Decode one Codebook 3 Huffman codeword from `reader`, returning +/// the codeword index in `0..=80`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 81-entry table. The table is +/// small (max codeword length 16 bits, 81 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 16 bits (Kraft +/// equality `Σᵢ 2^(16 − Lᵢ) = 65536 = 2¹⁶`), so any 16-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 16 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod3_is_complete` regression test that exhaustively +/// walks all `2¹⁶` 16-bit prefixes. +/// +/// The sign-bit suffix for non-zero coefficients is *not* consumed +/// here — the caller pairs the returned index with the §4.6.3.3 +/// translation and then reads exactly one sign bit per non-zero +/// coefficient in low-frequency-first order. +pub fn hcod3_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD3_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD3.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD3 is a complete 16-bit prefix code. The + // `hcod3_is_complete` regression test verifies every 16-bit + // prefix maps to exactly one entry. + unreachable!("HCOD3 is a complete 16-bit prefix code; the 16-bit walk must match"); +} + +/// Write a Codebook 3 codeword to `writer` by index. +/// +/// Convenience over `hcod3_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. The +/// caller is responsible for emitting the §4.6.3.3 sign bits for +/// every non-zero coefficient after this call. +pub fn hcod3_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod3_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.5 — Spectrum Huffman Codebook 4 +// ============================================================================= +// +// 81 entries, indices 0..=80. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the +// wire codeword at bit `length − 1`). Transcribed verbatim from +// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.5. +// +// The codebook is a complete prefix code: Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹². +// This is exhaustively verified by the `hcod4_is_complete` regression +// test (which walks every 12-bit prefix and asserts each maps to +// exactly one index). +// +// Codebook 4 shares Codebook 3's unsigned dim-4 LAV-2 tuple universe +// (Table 4.95 row 4 = row 3 except for the source-table column); +// the §4.6.3.3 index ↔ 4-tuple translation in +// [`crate::spectral_codebook`] is reused as-is. The §4.6.3.3 sign-bit +// suffix lives in [`crate::spectral_codebook::apply_sign_bits`] / +// [`crate::spectral_codebook::derive_sign_bits`]. + +/// Number of entries in Table 4.A.5 (`81`, indices `0..=80`). +pub const HCOD4_NUM_ENTRIES: usize = 81; + +/// Maximum codeword length emitted by Table 4.A.5 (12 bits). +pub const HCOD4_MAX_LEN: u32 = 12; + +/// Table 4.A.5 — `(length_in_bits, codeword)` per index `0..=80`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD4: [(u8, u16); HCOD4_NUM_ENTRIES] = [ + (4, 0x007), // 0 + (5, 0x016), // 1 + (8, 0x0f6), // 2 + (5, 0x018), // 3 + (4, 0x008), // 4 + (8, 0x0ef), // 5 + (9, 0x1ef), // 6 + (8, 0x0f3), // 7 + (11, 0x7f8), // 8 + (5, 0x019), // 9 + (5, 0x017), // 10 + (8, 0x0ed), // 11 + (5, 0x015), // 12 + (4, 0x001), // 13 + (8, 0x0e2), // 14 + (8, 0x0f0), // 15 + (7, 0x070), // 16 + (10, 0x3f0), // 17 + (9, 0x1ee), // 18 + (8, 0x0f1), // 19 + (11, 0x7fa), // 20 + (8, 0x0ee), // 21 + (8, 0x0e4), // 22 + (10, 0x3f2), // 23 + (11, 0x7f6), // 24 + (10, 0x3ef), // 25 + (11, 0x7fd), // 26 + (4, 0x005), // 27 + (5, 0x014), // 28 + (8, 0x0f2), // 29 + (4, 0x009), // 30 + (4, 0x004), // 31 + (8, 0x0e5), // 32 + (8, 0x0f4), // 33 + (8, 0x0e8), // 34 + (10, 0x3f4), // 35 + (4, 0x006), // 36 + (4, 0x002), // 37 + (8, 0x0e7), // 38 + (4, 0x003), // 39 + (4, 0x000), // 40 — shortest codeword in Codebook 4 + (7, 0x06b), // 41 + (8, 0x0e3), // 42 + (7, 0x069), // 43 + (9, 0x1f3), // 44 + (8, 0x0eb), // 45 + (8, 0x0e6), // 46 + (10, 0x3f6), // 47 + (7, 0x06e), // 48 + (7, 0x06a), // 49 + (9, 0x1f4), // 50 + (10, 0x3ec), // 51 + (9, 0x1f0), // 52 + (10, 0x3f9), // 53 + (8, 0x0f5), // 54 + (8, 0x0ec), // 55 + (11, 0x7fb), // 56 + (8, 0x0ea), // 57 + (7, 0x06f), // 58 + (10, 0x3f7), // 59 + (11, 0x7f9), // 60 + (10, 0x3f3), // 61 + (12, 0xfff), // 62 + (8, 0x0e9), // 63 + (7, 0x06d), // 64 + (10, 0x3f8), // 65 + (7, 0x06c), // 66 + (7, 0x068), // 67 + (9, 0x1f5), // 68 + (10, 0x3ee), // 69 + (9, 0x1f2), // 70 + (11, 0x7f4), // 71 + (11, 0x7f7), // 72 + (10, 0x3f1), // 73 + (12, 0xffe), // 74 + (10, 0x3ed), // 75 + (9, 0x1f1), // 76 + (11, 0x7f5), // 77 + (11, 0x7fe), // 78 + (10, 0x3f5), // 79 + (11, 0x7fc), // 80 +]; + +/// Encode a Codebook 4 codeword index (`0..=80`) to the wire Huffman +/// codeword from Table 4.A.5. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the +/// codebook number `4`; the legal range is `0..=80` (the 81-entry +/// `3^4` enumeration of every legal unsigned 4-tuple with each +/// coefficient in `0..=LAV = 0..=2` — the same universe as Codebook +/// 3). +/// +/// The inverse of [`hcod4_decode`]. The sign-bit suffix for each +/// non-zero coefficient is *not* part of the returned codeword — the +/// caller emits sign bits separately per +/// [`crate::spectral_codebook::derive_sign_bits`]. +pub fn hcod4_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD4 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(4))?; + Ok(*entry) +} + +/// Decode one Codebook 4 Huffman codeword from `reader`, returning +/// the codeword index in `0..=80`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 81-entry table. The table is +/// small (max codeword length 12 bits, 81 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 12 bits (Kraft +/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 12 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod4_is_complete` regression test that exhaustively +/// walks all `2¹²` 12-bit prefixes. +/// +/// The sign-bit suffix for non-zero coefficients is *not* consumed +/// here — the caller pairs the returned index with the §4.6.3.3 +/// translation and then reads exactly one sign bit per non-zero +/// coefficient in low-frequency-first order. +pub fn hcod4_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD4_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD4.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD4 is a complete 12-bit prefix code. The + // `hcod4_is_complete` regression test verifies every 12-bit + // prefix maps to exactly one entry. + unreachable!("HCOD4 is a complete 12-bit prefix code; the 12-bit walk must match"); +} + +/// Write a Codebook 4 codeword to `writer` by index. +/// +/// Convenience over `hcod4_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. The +/// caller is responsible for emitting the §4.6.3.3 sign bits for +/// every non-zero coefficient after this call. +pub fn hcod4_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod4_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.6 — Spectrum Huffman Codebook 5 +// ============================================================================= +// +// 81 entries, indices 0..=80. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the +// wire codeword at bit `length − 1`). Transcribed verbatim from +// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.6. +// +// The codebook is a complete prefix code: Σᵢ 2^(13 − Lᵢ) = 8192 = 2¹³. +// This is exhaustively verified by the `hcod5_is_complete` regression +// test (which walks every 13-bit prefix and asserts each maps to +// exactly one index). +// +// Codebook 5 is the first **pair** spectrum book (Table 4.95 row 5: +// `unsigned_cb = 0`, `dim = 2`, `LAV = 4`). Per §4.6.3.3 the +// index↔tuple translation evaluates `idx = (y + LAV) * 9 + (z + LAV)` +// so the signed pair lattice spans `(-4, -4) .. (+4, +4)` and the +// zero-tuple `(0, 0)` lands at the centre row index 40. The +// [`crate::spectral_codebook`] §4.6.3.3 dispatcher already handles +// the dim=2 path; this module owns only the codeword wire layer. +// Because Codebook 5 is signed, no sign-bit suffix follows the +// codeword on the wire — the index alone fully specifies the +// signed pair. + +/// Number of entries in Table 4.A.6 (`81`, indices `0..=80`). +pub const HCOD5_NUM_ENTRIES: usize = 81; + +/// Maximum codeword length emitted by Table 4.A.6 (13 bits). +pub const HCOD5_MAX_LEN: u32 = 13; + +/// Table 4.A.6 — `(length_in_bits, codeword)` per index `0..=80`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD5: [(u8, u16); HCOD5_NUM_ENTRIES] = [ + (13, 0x1fff), // 0 — (y, z) = (-4, -4); one of the four 13-bit corners + (12, 0xff7), // 1 + (11, 0x7f4), // 2 + (11, 0x7e8), // 3 + (10, 0x3f1), // 4 + (11, 0x7ee), // 5 + (11, 0x7f9), // 6 + (12, 0xff8), // 7 + (13, 0x1ffd), // 8 — (y, z) = (-4, +4); 13-bit corner + (12, 0xffd), // 9 + (11, 0x7f1), // 10 + (10, 0x3e8), // 11 + (9, 0x1e8), // 12 + (8, 0xf0), // 13 + (9, 0x1ec), // 14 + (10, 0x3ee), // 15 + (11, 0x7f2), // 16 + (12, 0xffa), // 17 + (12, 0xff4), // 18 + (10, 0x3ef), // 19 + (9, 0x1f2), // 20 + (8, 0xe8), // 21 + (7, 0x70), // 22 + (8, 0xec), // 23 + (9, 0x1f0), // 24 + (10, 0x3ea), // 25 + (11, 0x7f3), // 26 + (11, 0x7eb), // 27 + (9, 0x1eb), // 28 + (8, 0xea), // 29 + (5, 0x1a), // 30 + (4, 0x8), // 31 + (5, 0x19), // 32 + (8, 0xee), // 33 + (9, 0x1ef), // 34 + (11, 0x7ed), // 35 + (10, 0x3f0), // 36 + (8, 0xf2), // 37 + (7, 0x73), // 38 + (4, 0xb), // 39 + (1, 0x0), // 40 — (y, z) = (0, 0); single-bit zero codeword + (4, 0xa), // 41 + (7, 0x71), // 42 + (8, 0xf3), // 43 + (11, 0x7e9), // 44 + (11, 0x7ef), // 45 + (9, 0x1ee), // 46 + (8, 0xef), // 47 + (5, 0x18), // 48 + (4, 0x9), // 49 + (5, 0x1b), // 50 + (8, 0xeb), // 51 + (9, 0x1e9), // 52 + (11, 0x7ec), // 53 + (11, 0x7f6), // 54 + (10, 0x3eb), // 55 + (9, 0x1f3), // 56 + (8, 0xed), // 57 + (7, 0x72), // 58 + (8, 0xe9), // 59 + (9, 0x1f1), // 60 + (10, 0x3ed), // 61 + (11, 0x7f7), // 62 + (12, 0xff6), // 63 + (11, 0x7f0), // 64 + (10, 0x3e9), // 65 + (9, 0x1ed), // 66 + (8, 0xf1), // 67 + (9, 0x1ea), // 68 + (10, 0x3ec), // 69 + (11, 0x7f8), // 70 + (12, 0xff9), // 71 + (13, 0x1ffc), // 72 — (y, z) = (+4, -4); 13-bit corner + (12, 0xffc), // 73 + (12, 0xff5), // 74 + (11, 0x7ea), // 75 + (10, 0x3f3), // 76 + (10, 0x3f2), // 77 + (11, 0x7f5), // 78 + (12, 0xffb), // 79 + (13, 0x1ffe), // 80 — (y, z) = (+4, +4); 13-bit corner +]; + +/// Encode a Codebook 5 codeword index (`0..=80`) to the wire Huffman +/// codeword from Table 4.A.6. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the +/// codebook number `5`; the legal range is `0..=80` (the 81-entry +/// `9^2` enumeration of every legal signed 2-tuple with each +/// coefficient in `-LAV..=+LAV = -4..=+4`). +/// +/// The inverse of [`hcod5_decode`]. Because Codebook 5 is signed, +/// no sign-bit suffix follows the codeword on the wire — the +/// `offset = LAV = 4` shift inside the §4.6.3.3 translation already +/// encodes every coefficient's sign into the index. +pub fn hcod5_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD5 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(5))?; + Ok(*entry) +} + +/// Decode one Codebook 5 Huffman codeword from `reader`, returning +/// the codeword index in `0..=80`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 81-entry table. The table is +/// small (max codeword length 13 bits, 81 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 13 bits (Kraft +/// equality `Σᵢ 2^(13 − Lᵢ) = 8192 = 2¹³`), so any 13-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 13 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod5_is_complete` regression test that exhaustively +/// walks all `2¹³` 13-bit prefixes. +/// +/// No sign-bit suffix is read here — Codebook 5 is signed, so every +/// coefficient's sign is already baked into the index via the +/// `offset = LAV = 4` §4.6.3.3 polynomial. +pub fn hcod5_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD5_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD5.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD5 is a complete 13-bit prefix code. The + // `hcod5_is_complete` regression test verifies every 13-bit + // prefix maps to exactly one entry. + unreachable!("HCOD5 is a complete 13-bit prefix code; the 13-bit walk must match"); +} + +/// Write a Codebook 5 codeword to `writer` by index. +/// +/// Convenience over `hcod5_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. No +/// sign bits follow on the wire (Codebook 5 is signed). +pub fn hcod5_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod5_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.7 — Spectrum Huffman Codebook 6 +// ============================================================================= +// +// 81 entries, indices 0..=80. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the wire +// codeword at bit `length − 1`). Transcribed verbatim from ISO/IEC +// 14496-3:2001(E) §4.A.1 Table 4.A.7. +// +// The codebook is a complete prefix code: Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹. +// This is exhaustively verified by the `hcod6_is_complete` regression +// test (which walks every 11-bit prefix and asserts each maps to +// exactly one index). +// +// Codebook 6 is the second signed pair spectrum book (Table 4.95 row 6: +// `unsigned_cb = 0`, `dim = 2`, `LAV = 4` → `9^2 = 81` entries, each +// coefficient in `-4..=+4`). The §4.6.3.3 polynomial places the +// zero-tuple `(0, 0)` at the centre of the index range (index 40); +// the four `(±4, ±4)` lattice corners sit at indices 0, 8, 72, 80. +// Because Codebook 6 is signed, no sign-bit suffix follows the +// codeword on the wire. + +/// Number of entries in Table 4.A.7 (`81`, indices `0..=80`). +pub const HCOD6_NUM_ENTRIES: usize = 81; + +/// Maximum codeword length emitted by Table 4.A.7 (11 bits). +pub const HCOD6_MAX_LEN: u32 = 11; + +/// Table 4.A.7 — `(length_in_bits, codeword)` per index `0..=80`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD6: [(u8, u16); HCOD6_NUM_ENTRIES] = [ + (11, 0x7fe), // 0 — (y, z) = (-4, -4) + (10, 0x3fd), // 1 + (9, 0x1f1), // 2 + (9, 0x1eb), // 3 + (9, 0x1f4), // 4 + (9, 0x1ea), // 5 + (9, 0x1f0), // 6 + (10, 0x3fc), // 7 + (11, 0x7fd), // 8 — (y, z) = (-4, +4) + (10, 0x3f6), // 9 + (9, 0x1e5), // 10 + (8, 0xea), // 11 + (7, 0x6c), // 12 + (7, 0x71), // 13 + (7, 0x68), // 14 + (8, 0xf0), // 15 + (9, 0x1e6), // 16 + (10, 0x3f7), // 17 + (9, 0x1f3), // 18 + (8, 0xef), // 19 + (6, 0x32), // 20 + (6, 0x27), // 21 + (6, 0x28), // 22 + (6, 0x26), // 23 + (6, 0x31), // 24 + (8, 0xeb), // 25 + (9, 0x1f7), // 26 + (9, 0x1e8), // 27 + (7, 0x6f), // 28 + (6, 0x2e), // 29 + (4, 0x8), // 30 + (4, 0x4), // 31 + (4, 0x6), // 32 + (6, 0x29), // 33 + (7, 0x6b), // 34 + (9, 0x1ee), // 35 + (9, 0x1ef), // 36 + (7, 0x72), // 37 + (6, 0x2d), // 38 + (4, 0x2), // 39 + (4, 0x0), // 40 — zero-tuple (y, z) = (0, 0), 4-bit `0b0000` + (4, 0x3), // 41 + (6, 0x2f), // 42 + (7, 0x73), // 43 + (9, 0x1fa), // 44 + (9, 0x1e7), // 45 + (7, 0x6e), // 46 + (6, 0x2b), // 47 + (4, 0x7), // 48 + (4, 0x1), // 49 + (4, 0x5), // 50 + (6, 0x2c), // 51 + (7, 0x6d), // 52 + (9, 0x1ec), // 53 + (9, 0x1f9), // 54 + (8, 0xee), // 55 + (6, 0x30), // 56 + (6, 0x24), // 57 + (6, 0x2a), // 58 + (6, 0x25), // 59 + (6, 0x33), // 60 + (8, 0xec), // 61 + (9, 0x1f2), // 62 + (10, 0x3f8), // 63 + (9, 0x1e4), // 64 + (8, 0xed), // 65 + (7, 0x6a), // 66 + (7, 0x70), // 67 + (7, 0x69), // 68 + (7, 0x74), // 69 + (8, 0xf1), // 70 + (10, 0x3fa), // 71 + (11, 0x7ff), // 72 — (y, z) = (+4, -4) + (10, 0x3f9), // 73 + (9, 0x1f6), // 74 + (9, 0x1ed), // 75 + (9, 0x1f8), // 76 + (9, 0x1e9), // 77 + (9, 0x1f5), // 78 + (10, 0x3fb), // 79 + (11, 0x7fc), // 80 — (y, z) = (+4, +4) +]; + +/// Encode a Codebook 6 codeword index (`0..=80`) to the wire Huffman +/// codeword from Table 4.A.7. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=80` (the 81-entry `9^2` enumeration of every legal +/// signed pair with each coefficient in `-4..=+4`). +/// +/// The inverse of [`hcod6_decode`]. Because Codebook 6 is signed, +/// each tuple coefficient's sign is already encoded in the index via +/// the §4.6.3.3 `offset = LAV = 4` shift — no sign-bit suffix is +/// emitted after the codeword. +pub fn hcod6_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD6 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(6))?; + Ok(*entry) +} + +/// Decode one Codebook 6 Huffman codeword from `reader`, returning +/// the codeword index in `0..=80`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 81-entry table. The table is +/// small (max codeword length 11 bits, 81 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 11 bits (Kraft +/// equality `Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹`), so any 11-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 11 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod6_is_complete` regression test that exhaustively +/// walks all `2¹¹` 11-bit prefixes. +/// +/// No sign-bit suffix is read here — Codebook 6 is signed, so every +/// `(y, z)` pair carries its sign inside the §4.6.3.3 index via the +/// `offset = LAV = 4` shift. +pub fn hcod6_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD6_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD6.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD6 is a complete 11-bit prefix code. The + // `hcod6_is_complete` regression test verifies every 11-bit + // prefix maps to exactly one entry. + unreachable!("HCOD6 is a complete 11-bit prefix code; the 11-bit walk must match"); +} + +/// Write a Codebook 6 codeword to `writer` by index. +/// +/// Convenience over `hcod6_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. No +/// sign bits follow on the wire (Codebook 6 is signed). +pub fn hcod6_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod6_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.8 — Spectrum Huffman Codebook 7 +// ============================================================================= +// +// 64 entries, indices 0..=63. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the +// wire codeword at bit `length − 1`). Transcribed verbatim from +// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.8. +// +// The codebook is a complete prefix code: Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹². +// This is exhaustively verified by the `hcod7_is_complete` regression +// test (which walks every 12-bit prefix and asserts each maps to +// exactly one entry). +// +// Codebook 7 is the first unsigned pair spectrum book (Table 4.95 row 7: +// `unsigned_cb = 1`, `dim = 2`, `LAV = 7` → `8^2 = 64` entries, each +// coefficient in `0..=7`). The §4.6.3.3 polynomial +// `idx = y * (LAV + 1) + z = y * 8 + z` places the zero-tuple `(0, 0)` +// at index 0 (the origin of the unsigned dim-2 lattice) and the maximum +// tuple `(7, 7)` at index 63 (the far corner). Because Codebook 7 is +// unsigned, a sign-bit suffix follows the Huffman codeword for every +// non-zero coefficient per §4.6.3.3 — the suffix is delivered by +// `crate::spectral_codebook::apply_sign_bits` / +// `crate::spectral_codebook::derive_sign_bits`, separate from the +// Huffman codeword carried here. + +/// Number of entries in Table 4.A.8 (`64`, indices `0..=63`). +pub const HCOD7_NUM_ENTRIES: usize = 64; + +/// Maximum codeword length emitted by Table 4.A.8 (12 bits). +pub const HCOD7_MAX_LEN: u32 = 12; + +/// Table 4.A.8 — `(length_in_bits, codeword)` per index `0..=63`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD7: [(u8, u16); HCOD7_NUM_ENTRIES] = [ + (1, 0x000), // 0 — zero-tuple (y, z) = (0, 0), 1-bit `0` + (3, 0x005), // 1 + (6, 0x037), // 2 + (7, 0x074), // 3 + (8, 0x0f2), // 4 + (9, 0x1eb), // 5 + (10, 0x3ed), // 6 + (11, 0x7f7), // 7 + (3, 0x004), // 8 + (4, 0x00c), // 9 + (6, 0x035), // 10 + (7, 0x071), // 11 + (8, 0x0ec), // 12 + (8, 0x0ee), // 13 + (9, 0x1ee), // 14 + (9, 0x1f5), // 15 + (6, 0x036), // 16 + (6, 0x034), // 17 + (7, 0x072), // 18 + (8, 0x0ea), // 19 + (8, 0x0f1), // 20 + (9, 0x1e9), // 21 + (9, 0x1f3), // 22 + (10, 0x3f5), // 23 + (7, 0x073), // 24 + (7, 0x070), // 25 + (8, 0x0eb), // 26 + (8, 0x0f0), // 27 + (9, 0x1f1), // 28 + (9, 0x1f0), // 29 + (10, 0x3ec), // 30 + (10, 0x3fa), // 31 + (8, 0x0f3), // 32 + (8, 0x0ed), // 33 + (9, 0x1e8), // 34 + (9, 0x1ef), // 35 + (10, 0x3ef), // 36 + (10, 0x3f1), // 37 + (10, 0x3f9), // 38 + (11, 0x7fb), // 39 + (9, 0x1ed), // 40 + (8, 0x0ef), // 41 + (9, 0x1ea), // 42 + (9, 0x1f2), // 43 + (10, 0x3f3), // 44 + (10, 0x3f8), // 45 + (11, 0x7f9), // 46 + (11, 0x7fc), // 47 + (10, 0x3ee), // 48 + (9, 0x1ec), // 49 + (9, 0x1f4), // 50 + (10, 0x3f4), // 51 + (10, 0x3f7), // 52 + (11, 0x7f8), // 53 + (12, 0xffd), // 54 + (12, 0xffe), // 55 + (11, 0x7f6), // 56 + (10, 0x3f0), // 57 + (10, 0x3f2), // 58 + (10, 0x3f6), // 59 + (11, 0x7fa), // 60 + (11, 0x7fd), // 61 + (12, 0xffc), // 62 + (12, 0xfff), // 63 — far corner (y, z) = (7, 7) +]; + +/// Encode a Codebook 7 codeword index (`0..=63`) to the wire Huffman +/// codeword from Table 4.A.8. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=63` (the 64-entry `8^2` enumeration of every legal +/// unsigned pair with each coefficient in `0..=7`). +/// +/// The inverse of [`hcod7_decode`]. Because Codebook 7 is unsigned, +/// callers transmit one sign bit after the codeword for each non-zero +/// coefficient via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried +/// here. +pub fn hcod7_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD7 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(7))?; + Ok(*entry) +} + +/// Decode one Codebook 7 Huffman codeword from `reader`, returning +/// the codeword index in `0..=63`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 64-entry table. The table is +/// small (max codeword length 12 bits, 64 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 12 bits (Kraft +/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 12 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod7_is_complete` regression test that exhaustively +/// walks all `2¹²` 12-bit prefixes. +/// +/// The §4.6.3.3 sign-bit suffix lies outside this routine — for +/// unsigned Codebook 7 the caller consumes one sign bit per non-zero +/// coefficient after the Huffman codeword via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). +pub fn hcod7_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD7_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD7.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD7 is a complete 12-bit prefix code. The + // `hcod7_is_complete` regression test verifies every 12-bit + // prefix maps to exactly one entry. + unreachable!("HCOD7 is a complete 12-bit prefix code; the 12-bit walk must match"); +} + +/// Write a Codebook 7 codeword to `writer` by index. +/// +/// Convenience over `hcod7_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 63`. The +/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one +/// suffix bit per non-zero coefficient, low-frequency-first). +pub fn hcod7_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod7_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ============================================================================= +// Table 4.A.9 — Spectrum Huffman Codebook 8 +// ============================================================================= +// +// 64 entries, indices 0..=63. Each row is `(length_in_bits, +// codeword)` with `codeword` right-aligned in a `u16` (MSB of the +// wire codeword at bit `length − 1`). Transcribed verbatim from +// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.9 (page 198). +// +// The codebook is a complete prefix code: Σᵢ 2^(10 − Lᵢ) = 1024 = 2¹⁰. +// This is exhaustively verified by the `hcod8_is_complete` regression +// test (which walks every 10-bit prefix and asserts each maps to +// exactly one entry). +// +// Codebook 8 is the second unsigned pair spectrum book — it shares +// Codebook 7's Table 4.95 row shape (row 8 column-for-column matches +// row 7 except for the `Codebook listed in Table` cell pointing at +// Table 4.A.9): `unsigned_cb = 1`, `dim = 2`, `LAV = 7` → `(7 + 1)^2 +// = 8^2 = 64` entries, each coefficient in `0..=7`. The §4.6.3.3 +// unsigned polynomial `idx = y * (LAV + 1) + z = y * 8 + z` places +// the zero-tuple `(0, 0)` at index 0, the interior `(1, 1)` at +// index 9, and the far corner `(7, 7)` at index 63 — the same head +// and far-corner placements Codebook 7 also uses for its unsigned +// dim-2 universe. The Huffman-length tuning differs: Codebook 8 +// lifts the zero-tuple off the single-bit codeword (now 5 bits at +// index 0) and migrates the shortest codeword (3 bits `0b000`) to +// the interior tuple `(1, 1)` at index 9. The maximum codeword +// length is 10 bits; exactly four rows reach the ceiling +// (indices 7, 47, 56, 63). +// +// Because Codebook 8 is unsigned, a sign-bit suffix follows the +// Huffman codeword for every non-zero coefficient per §4.6.3.3 — +// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` +// / `crate::spectral_codebook::derive_sign_bits`, separate from the +// Huffman codeword carried here. + +/// Number of entries in Table 4.A.9 (`64`, indices `0..=63`). +pub const HCOD8_NUM_ENTRIES: usize = 64; + +/// Maximum codeword length emitted by Table 4.A.9 (10 bits). +pub const HCOD8_MAX_LEN: u32 = 10; + +/// Table 4.A.9 — `(length_in_bits, codeword)` per index `0..=63`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD8: [(u8, u16); HCOD8_NUM_ENTRIES] = [ + (5, 0x00e), // 0 — zero-tuple (y, z) = (0, 0) + (4, 0x005), // 1 + (5, 0x010), // 2 + (6, 0x030), // 3 + (7, 0x06f), // 4 + (8, 0x0f1), // 5 + (9, 0x1fa), // 6 + (10, 0x3fe), // 7 + (4, 0x003), // 8 + (3, 0x000), // 9 — interior (y, z) = (1, 1), shortest 3-bit `0` + (4, 0x004), // 10 + (5, 0x012), // 11 + (6, 0x02c), // 12 + (7, 0x06a), // 13 + (7, 0x075), // 14 + (8, 0x0f8), // 15 + (5, 0x00f), // 16 + (4, 0x002), // 17 + (4, 0x006), // 18 + (5, 0x014), // 19 + (6, 0x02e), // 20 + (7, 0x069), // 21 + (7, 0x072), // 22 + (8, 0x0f5), // 23 + (6, 0x02f), // 24 + (5, 0x011), // 25 + (5, 0x013), // 26 + (6, 0x02a), // 27 + (6, 0x032), // 28 + (7, 0x06c), // 29 + (8, 0x0ec), // 30 + (8, 0x0fa), // 31 + (7, 0x071), // 32 + (6, 0x02b), // 33 + (6, 0x02d), // 34 + (6, 0x031), // 35 + (7, 0x06d), // 36 + (7, 0x070), // 37 + (8, 0x0f2), // 38 + (9, 0x1f9), // 39 + (8, 0x0ef), // 40 + (7, 0x068), // 41 + (6, 0x033), // 42 + (7, 0x06b), // 43 + (7, 0x06e), // 44 + (8, 0x0ee), // 45 + (8, 0x0f9), // 46 + (10, 0x3fc), // 47 + (9, 0x1f8), // 48 + (7, 0x074), // 49 + (7, 0x073), // 50 + (8, 0x0ed), // 51 + (8, 0x0f0), // 52 + (8, 0x0f6), // 53 + (9, 0x1f6), // 54 + (9, 0x1fd), // 55 + (10, 0x3fd), // 56 + (8, 0x0f3), // 57 + (8, 0x0f4), // 58 + (8, 0x0f7), // 59 + (9, 0x1f7), // 60 + (9, 0x1fb), // 61 + (9, 0x1fc), // 62 + (10, 0x3ff), // 63 — far corner (y, z) = (7, 7) +]; + +/// Encode a Codebook 8 codeword index (`0..=63`) to the wire Huffman +/// codeword from Table 4.A.9. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=63` (the 64-entry `8^2` enumeration of every legal +/// unsigned pair with each coefficient in `0..=7`). +/// +/// The inverse of [`hcod8_decode`]. Because Codebook 8 is unsigned, +/// callers transmit one sign bit after the codeword for each non-zero +/// coefficient via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried +/// here. +pub fn hcod8_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD8 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(8))?; + Ok(*entry) +} + +/// Decode one Codebook 8 Huffman codeword from `reader`, returning +/// the codeword index in `0..=63`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 64-entry table. The table is +/// small (max codeword length 10 bits, 64 entries) so a single +/// linear scan per bit-extend is cheaper than the storage and +/// build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 10 bits (Kraft +/// equality `Σᵢ 2^(10 − Lᵢ) = 1024 = 2¹⁰`), so any 10-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 10 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod8_is_complete` regression test that exhaustively +/// walks all `2¹⁰` 10-bit prefixes. +/// +/// The §4.6.3.3 sign-bit suffix lies outside this routine — for +/// unsigned Codebook 8 the caller consumes one sign bit per non-zero +/// coefficient after the Huffman codeword via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). +pub fn hcod8_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD8_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD8.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD8 is a complete 10-bit prefix code. The + // `hcod8_is_complete` regression test verifies every 10-bit + // prefix maps to exactly one entry. + unreachable!("HCOD8 is a complete 10-bit prefix code; the 10-bit walk must match"); +} + +/// Write a Codebook 8 codeword to `writer` by index. +/// +/// Convenience over `hcod8_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 63`. The +/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one +/// suffix bit per non-zero coefficient, low-frequency-first). +pub fn hcod8_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod8_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ===================================================================== +// Codebook 9 — Table 4.A.10 +// ===================================================================== +// +// Codebook 9 is the first expanded-LAV unsigned pair spectrum book — +// Table 4.95 row 9 declares `unsigned_cb = 1`, `dim = 2`, `LAV = 12`, +// so the §4.6.3.3 universe shifts to `(12 + 1)^2 = 13^2 = 169` +// entries indexed `0..=168` with each `(y, z)` coefficient in +// `0..=12`. That is a substantial step up from Codebooks 7 and 8's +// shared `8 × 8 = 64`-entry unsigned pair lattice — the `169 / 64 ≈ +// 2.6×` universe expansion widens the distribution's tail and lifts +// the codeword ceiling from Codebook 8's 10 bits to **15 bits**, the +// widest non-ESC codeword in the entire Annex 4.A book set. The +// §4.6.3.3 unsigned polynomial `idx = y * (LAV + 1) + z = y * 13 + z` +// places the zero-tuple `(0, 0)` at index 0 and the maximum tuple +// `(12, 12)` at index 168 (`12 * 13 + 12 = 168`). The single-bit +// codeword `0` parks at index 0 — the same shortest-codeword head +// placement Codebook 7 uses for its zero-tuple. Exactly four rows +// reach the 15-bit ceiling (indices 142, 154, 155, 168) — the +// rarest pair magnitudes near the `LAV = 12` cap. +// +// Because Codebook 9 is unsigned, a sign-bit suffix follows the +// Huffman codeword for every non-zero coefficient per §4.6.3.3 — +// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` +// / `crate::spectral_codebook::derive_sign_bits`, separate from the +// Huffman codeword carried here. + +/// Number of entries in Table 4.A.10 (`169`, indices `0..=168`). +pub const HCOD9_NUM_ENTRIES: usize = 169; + +/// Maximum codeword length emitted by Table 4.A.10 (15 bits). +pub const HCOD9_MAX_LEN: u32 = 15; + +/// Table 4.A.10 — `(length_in_bits, codeword)` per index `0..=168`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD9: [(u8, u16); HCOD9_NUM_ENTRIES] = [ + (1, 0x0000), // 0 — zero-tuple (y, z) = (0, 0), shortest 1-bit `0` + (3, 0x0005), // 1 + (6, 0x0037), // 2 + (8, 0x00e7), // 3 + (9, 0x01de), // 4 + (10, 0x03ce), // 5 + (10, 0x03d9), // 6 + (11, 0x07c8), // 7 + (11, 0x07cd), // 8 + (12, 0x0fc8), // 9 + (12, 0x0fdd), // 10 + (13, 0x1fe4), // 11 + (13, 0x1fec), // 12 + (3, 0x0004), // 13 + (4, 0x000c), // 14 — interior (y, z) = (1, 1) + (6, 0x0035), // 15 + (7, 0x0072), // 16 + (8, 0x00ea), // 17 + (8, 0x00ed), // 18 + (9, 0x01e2), // 19 + (10, 0x03d1), // 20 + (10, 0x03d3), // 21 + (10, 0x03e0), // 22 + (11, 0x07d8), // 23 + (12, 0x0fcf), // 24 + (12, 0x0fd5), // 25 + (6, 0x0036), // 26 + (6, 0x0034), // 27 + (7, 0x0071), // 28 + (8, 0x00e8), // 29 + (8, 0x00ec), // 30 + (9, 0x01e1), // 31 + (10, 0x03cf), // 32 + (10, 0x03dd), // 33 + (10, 0x03db), // 34 + (11, 0x07d0), // 35 + (12, 0x0fc7), // 36 + (12, 0x0fd4), // 37 + (12, 0x0fe4), // 38 + (8, 0x00e6), // 39 + (7, 0x0070), // 40 + (8, 0x00e9), // 41 + (9, 0x01dd), // 42 + (9, 0x01e3), // 43 + (10, 0x03d2), // 44 + (10, 0x03dc), // 45 + (11, 0x07cc), // 46 + (11, 0x07ca), // 47 + (11, 0x07de), // 48 + (12, 0x0fd8), // 49 + (12, 0x0fea), // 50 + (13, 0x1fdb), // 51 + (9, 0x01df), // 52 + (8, 0x00eb), // 53 + (9, 0x01dc), // 54 + (9, 0x01e6), // 55 + (10, 0x03d5), // 56 + (10, 0x03de), // 57 + (11, 0x07cb), // 58 + (11, 0x07dd), // 59 + (11, 0x07dc), // 60 + (12, 0x0fcd), // 61 + (12, 0x0fe2), // 62 + (12, 0x0fe7), // 63 + (13, 0x1fe1), // 64 + (10, 0x03d0), // 65 + (9, 0x01e0), // 66 + (9, 0x01e4), // 67 + (10, 0x03d6), // 68 + (11, 0x07c5), // 69 + (11, 0x07d1), // 70 + (11, 0x07db), // 71 + (12, 0x0fd2), // 72 + (11, 0x07e0), // 73 + (12, 0x0fd9), // 74 + (12, 0x0feb), // 75 + (13, 0x1fe3), // 76 + (13, 0x1fe9), // 77 + (11, 0x07c4), // 78 + (9, 0x01e5), // 79 + (10, 0x03d7), // 80 + (11, 0x07c6), // 81 + (11, 0x07cf), // 82 + (11, 0x07da), // 83 + (12, 0x0fcb), // 84 + (12, 0x0fda), // 85 + (12, 0x0fe3), // 86 + (12, 0x0fe9), // 87 + (13, 0x1fe6), // 88 + (13, 0x1ff3), // 89 + (13, 0x1ff7), // 90 + (11, 0x07d3), // 91 + (10, 0x03d8), // 92 + (10, 0x03e1), // 93 + (11, 0x07d4), // 94 + (11, 0x07d9), // 95 + (12, 0x0fd3), // 96 + (12, 0x0fde), // 97 + (13, 0x1fdd), // 98 + (13, 0x1fd9), // 99 + (13, 0x1fe2), // 100 + (13, 0x1fea), // 101 + (13, 0x1ff1), // 102 + (13, 0x1ff6), // 103 + (11, 0x07d2), // 104 + (10, 0x03d4), // 105 + (10, 0x03da), // 106 + (11, 0x07c7), // 107 + (11, 0x07d7), // 108 + (11, 0x07e2), // 109 + (12, 0x0fce), // 110 + (12, 0x0fdb), // 111 + (13, 0x1fd8), // 112 + (13, 0x1fee), // 113 + (14, 0x3ff0), // 114 + (13, 0x1ff4), // 115 + (14, 0x3ff2), // 116 + (11, 0x07e1), // 117 + (10, 0x03df), // 118 + (11, 0x07c9), // 119 + (11, 0x07d6), // 120 + (12, 0x0fca), // 121 + (12, 0x0fd0), // 122 + (12, 0x0fe5), // 123 + (12, 0x0fe6), // 124 + (13, 0x1feb), // 125 + (13, 0x1fef), // 126 + (14, 0x3ff3), // 127 + (14, 0x3ff4), // 128 + (14, 0x3ff5), // 129 + (12, 0x0fe0), // 130 + (11, 0x07ce), // 131 + (11, 0x07d5), // 132 + (12, 0x0fc6), // 133 + (12, 0x0fd1), // 134 + (12, 0x0fe1), // 135 + (13, 0x1fe0), // 136 + (13, 0x1fe8), // 137 + (13, 0x1ff0), // 138 + (14, 0x3ff1), // 139 + (14, 0x3ff8), // 140 + (14, 0x3ff6), // 141 + (15, 0x7ffc), // 142 + (12, 0x0fe8), // 143 + (11, 0x07df), // 144 + (12, 0x0fc9), // 145 + (12, 0x0fd7), // 146 + (12, 0x0fdc), // 147 + (13, 0x1fdc), // 148 + (13, 0x1fdf), // 149 + (13, 0x1fed), // 150 + (13, 0x1ff5), // 151 + (14, 0x3ff9), // 152 + (14, 0x3ffb), // 153 + (15, 0x7ffd), // 154 + (15, 0x7ffe), // 155 + (13, 0x1fe7), // 156 + (12, 0x0fcc), // 157 + (12, 0x0fd6), // 158 + (12, 0x0fdf), // 159 + (13, 0x1fde), // 160 + (13, 0x1fda), // 161 + (13, 0x1fe5), // 162 + (13, 0x1ff2), // 163 + (14, 0x3ffa), // 164 + (14, 0x3ff7), // 165 + (14, 0x3ffc), // 166 + (14, 0x3ffd), // 167 + (15, 0x7fff), // 168 — far corner (y, z) = (12, 12) +]; + +/// Encode a Codebook 9 codeword index (`0..=168`) to the wire Huffman +/// codeword from Table 4.A.10. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=168` (the 169-entry `13^2` enumeration of every +/// legal unsigned pair with each coefficient in `0..=12`). +/// +/// The inverse of [`hcod9_decode`]. Because Codebook 9 is unsigned, +/// callers transmit one sign bit after the codeword for each non-zero +/// coefficient via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried +/// here. +pub fn hcod9_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD9 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(9))?; + Ok(*entry) +} + +/// Decode one Codebook 9 Huffman codeword from `reader`, returning +/// the codeword index in `0..=168`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 169-entry table. The table is +/// small enough (max codeword length 15 bits, 169 entries) that a +/// single linear scan per bit-extend is cheaper than the storage +/// and build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 15 bits (Kraft +/// equality `Σᵢ 2^(15 − Lᵢ) = 32768 = 2¹⁵`), so any 15-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 15 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod9_is_complete` regression test that exhaustively +/// walks all `2¹⁵` 15-bit prefixes. +/// +/// The §4.6.3.3 sign-bit suffix lies outside this routine — for +/// unsigned Codebook 9 the caller consumes one sign bit per non-zero +/// coefficient after the Huffman codeword via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). +pub fn hcod9_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD9_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD9.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD9 is a complete 15-bit prefix code. The + // `hcod9_is_complete` regression test verifies every 15-bit + // prefix maps to exactly one entry. + unreachable!("HCOD9 is a complete 15-bit prefix code; the 15-bit walk must match"); +} + +/// Write a Codebook 9 codeword to `writer` by index. +/// +/// Convenience over `hcod9_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 168`. The +/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one +/// suffix bit per non-zero coefficient, low-frequency-first). +pub fn hcod9_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod9_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ===================================================================== +// Codebook 10 — Table 4.A.11 +// ===================================================================== +// +// Codebook 10 is the second expanded-LAV unsigned pair spectrum book — +// Table 4.95 row 10 mirrors Codebook 9's row 9 column-for-column +// (`unsigned_cb = 1`, `dim = 2`, `LAV = 12`) so the §4.6.3.3 universe +// is the same `13 × 13 = 169`-entry lattice indexed `0..=168` with +// each `(y, z)` coefficient in `0..=12`. The §4.6.3.3 unsigned +// polynomial `idx = y * (LAV + 1) + z = y * 13 + z` places the +// zero-tuple `(0, 0)` at index 0 and the maximum tuple `(12, 12)` at +// index 168 (`12 * 13 + 12 = 168`). Where Codebook 9 parks the +// single-bit codeword `0` on the zero-tuple at index 0, Codebook 10 +// lifts the zero-tuple to a 6-bit `0b100010` (`0x22`) and migrates +// the shortest codeword (4 bits) onto the interior `(1, 1)` at +// index 14 with codeword `0b0000` — the same head-displacement +// pattern Codebook 8 uses to relocate its shortest slot off the +// zero-tuple. Exactly three rows reach the 4-bit floor (indices +// 14, 15, 27 with codewords `0x0`, `0x1`, `0x2`), reflecting an +// encoder target whose magnitude statistics are denser around +// `(±1, ±1) .. (±2, ±2)` than Codebook 9's zero-heavy distribution. +// The maximum codeword length is **12 bits** — a 3-bit pull-down +// from Codebook 9's 15-bit ceiling — and exactly eight rows reach +// that 12-bit ceiling (indices 12, 129, 142, 155, 165, 166, 167, +// 168 with codewords `0xffd, 0xffa, 0xff9, 0xffb, 0xff8, 0xffe, +// 0xffc, 0xfff`), the rarest pair magnitudes near the `LAV = 12` +// cap. +// +// Because Codebook 10 is unsigned, a sign-bit suffix follows the +// Huffman codeword for every non-zero coefficient per §4.6.3.3 — +// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` +// / `crate::spectral_codebook::derive_sign_bits`, separate from the +// Huffman codeword carried here. + +/// Number of entries in Table 4.A.11 (`169`, indices `0..=168`). +pub const HCOD10_NUM_ENTRIES: usize = 169; + +/// Maximum codeword length emitted by Table 4.A.11 (12 bits). +pub const HCOD10_MAX_LEN: u32 = 12; + +/// Table 4.A.11 — `(length_in_bits, codeword)` per index `0..=168`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +const HCOD10: [(u8, u16); HCOD10_NUM_ENTRIES] = [ + (6, 0x0022), // 0 — zero-tuple (y, z) = (0, 0) + (5, 0x0008), // 1 + (6, 0x001d), // 2 + (6, 0x0026), // 3 + (7, 0x005f), // 4 + (8, 0x00d3), // 5 + (9, 0x01cf), // 6 + (10, 0x03d0), // 7 + (10, 0x03d7), // 8 + (10, 0x03ed), // 9 + (11, 0x07f0), // 10 + (11, 0x07f6), // 11 + (12, 0x0ffd), // 12 + (5, 0x0007), // 13 + (4, 0x0000), // 14 — interior (y, z) = (1, 1), shortest 4-bit `0b0000` + (4, 0x0001), // 15 + (5, 0x0009), // 16 + (6, 0x0020), // 17 + (7, 0x0054), // 18 + (7, 0x0060), // 19 + (8, 0x00d5), // 20 + (8, 0x00dc), // 21 + (9, 0x01d4), // 22 + (10, 0x03cd), // 23 + (10, 0x03de), // 24 + (11, 0x07e7), // 25 + (6, 0x001c), // 26 + (4, 0x0002), // 27 + (5, 0x0006), // 28 + (5, 0x000c), // 29 + (6, 0x001e), // 30 + (6, 0x0028), // 31 + (7, 0x005b), // 32 + (8, 0x00cd), // 33 + (8, 0x00d9), // 34 + (9, 0x01ce), // 35 + (9, 0x01dc), // 36 + (10, 0x03d9), // 37 + (10, 0x03f1), // 38 + (6, 0x0025), // 39 + (5, 0x000b), // 40 + (5, 0x000a), // 41 + (5, 0x000d), // 42 + (6, 0x0024), // 43 + (7, 0x0057), // 44 + (7, 0x0061), // 45 + (8, 0x00cc), // 46 + (8, 0x00dd), // 47 + (9, 0x01cc), // 48 + (9, 0x01de), // 49 + (10, 0x03d3), // 50 + (10, 0x03e7), // 51 + (7, 0x005d), // 52 + (6, 0x0021), // 53 + (6, 0x001f), // 54 + (6, 0x0023), // 55 + (6, 0x0027), // 56 + (7, 0x0059), // 57 + (7, 0x0064), // 58 + (8, 0x00d8), // 59 + (8, 0x00df), // 60 + (9, 0x01d2), // 61 + (9, 0x01e2), // 62 + (10, 0x03dd), // 63 + (10, 0x03ee), // 64 + (8, 0x00d1), // 65 + (7, 0x0055), // 66 + (6, 0x0029), // 67 + (7, 0x0056), // 68 + (7, 0x0058), // 69 + (7, 0x0062), // 70 + (8, 0x00ce), // 71 + (8, 0x00e0), // 72 + (8, 0x00e2), // 73 + (9, 0x01da), // 74 + (10, 0x03d4), // 75 + (10, 0x03e3), // 76 + (11, 0x07eb), // 77 + (9, 0x01c9), // 78 + (7, 0x005e), // 79 + (7, 0x005a), // 80 + (7, 0x005c), // 81 + (7, 0x0063), // 82 + (8, 0x00ca), // 83 + (8, 0x00da), // 84 + (9, 0x01c7), // 85 + (9, 0x01ca), // 86 + (9, 0x01e0), // 87 + (10, 0x03db), // 88 + (10, 0x03e8), // 89 + (11, 0x07ec), // 90 + (9, 0x01e3), // 91 + (8, 0x00d2), // 92 + (8, 0x00cb), // 93 + (8, 0x00d0), // 94 + (8, 0x00d7), // 95 + (8, 0x00db), // 96 + (9, 0x01c6), // 97 + (9, 0x01d5), // 98 + (9, 0x01d8), // 99 + (10, 0x03ca), // 100 + (10, 0x03da), // 101 + (11, 0x07ea), // 102 + (11, 0x07f1), // 103 + (9, 0x01e1), // 104 + (8, 0x00d4), // 105 + (8, 0x00cf), // 106 + (8, 0x00d6), // 107 + (8, 0x00de), // 108 + (8, 0x00e1), // 109 + (9, 0x01d0), // 110 + (9, 0x01d6), // 111 + (10, 0x03d1), // 112 + (10, 0x03d5), // 113 + (10, 0x03f2), // 114 + (11, 0x07ee), // 115 + (11, 0x07fb), // 116 + (10, 0x03e9), // 117 + (9, 0x01cd), // 118 + (9, 0x01c8), // 119 + (9, 0x01cb), // 120 + (9, 0x01d1), // 121 + (9, 0x01d7), // 122 + (9, 0x01df), // 123 + (10, 0x03cf), // 124 + (10, 0x03e0), // 125 + (10, 0x03ef), // 126 + (11, 0x07e6), // 127 + (11, 0x07f8), // 128 + (12, 0x0ffa), // 129 + (10, 0x03eb), // 130 + (9, 0x01dd), // 131 + (9, 0x01d3), // 132 + (9, 0x01d9), // 133 + (9, 0x01db), // 134 + (10, 0x03d2), // 135 + (10, 0x03cc), // 136 + (10, 0x03dc), // 137 + (10, 0x03ea), // 138 + (11, 0x07ed), // 139 + (11, 0x07f3), // 140 + (11, 0x07f9), // 141 + (12, 0x0ff9), // 142 + (11, 0x07f2), // 143 + (10, 0x03ce), // 144 + (9, 0x01e4), // 145 + (10, 0x03cb), // 146 + (10, 0x03d8), // 147 + (10, 0x03d6), // 148 + (10, 0x03e2), // 149 + (10, 0x03e5), // 150 + (11, 0x07e8), // 151 + (11, 0x07f4), // 152 + (11, 0x07f5), // 153 + (11, 0x07f7), // 154 + (12, 0x0ffb), // 155 + (11, 0x07fa), // 156 + (10, 0x03ec), // 157 + (10, 0x03df), // 158 + (10, 0x03e1), // 159 + (10, 0x03e4), // 160 + (10, 0x03e6), // 161 + (10, 0x03f0), // 162 + (11, 0x07e9), // 163 + (11, 0x07ef), // 164 + (12, 0x0ff8), // 165 + (12, 0x0ffe), // 166 + (12, 0x0ffc), // 167 + (12, 0x0fff), // 168 — far corner (y, z) = (12, 12) +]; + +/// Encode a Codebook 10 codeword index (`0..=168`) to the wire Huffman +/// codeword from Table 4.A.11. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=168` (the 169-entry `13^2` enumeration of every +/// legal unsigned pair with each coefficient in `0..=12`). +/// +/// The inverse of [`hcod10_decode`]. Because Codebook 10 is unsigned, +/// callers transmit one sign bit after the codeword for each non-zero +/// coefficient via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried +/// here. +pub fn hcod10_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD10 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(10))?; + Ok(*entry) +} + +/// Decode one Codebook 10 Huffman codeword from `reader`, returning +/// the codeword index in `0..=168`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 169-entry table. The table is +/// small enough (max codeword length 12 bits, 169 entries) that a +/// single linear scan per bit-extend is cheaper than the storage +/// and build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 12 bits (Kraft +/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 12 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod10_is_complete` regression test that +/// exhaustively walks all `2¹²` 12-bit prefixes. +/// +/// The §4.6.3.3 sign-bit suffix lies outside this routine — for +/// unsigned Codebook 10 the caller consumes one sign bit per +/// non-zero coefficient after the Huffman codeword via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). +pub fn hcod10_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD10_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD10.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD10 is a complete 12-bit prefix code. The + // `hcod10_is_complete` regression test verifies every 12-bit + // prefix maps to exactly one entry. + unreachable!("HCOD10 is a complete 12-bit prefix code; the 12-bit walk must match"); +} + +/// Write a Codebook 10 codeword to `writer` by index. +/// +/// Convenience over `hcod10_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 168`. The +/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one +/// suffix bit per non-zero coefficient, low-frequency-first). +pub fn hcod10_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod10_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +// ===================================================================== +// Codebook 11 — Table 4.A.12 +// ===================================================================== +// +// Codebook 11 is the only AAC spectrum book that carries an **escape +// (ESC) sequence**. Table 4.95 row 11 declares `unsigned_cb = 1`, +// `dim = 2`, `LAV = 16` and an ESC threshold of `8191` — the §4.6.1.3 +// `x_quant` ceiling. The in-band Huffman universe is therefore the +// `(LAV + 1)^dim = 17^2 = 289`-entry lattice indexed `0..=288` with +// each `(y, z)` coefficient in `0..=16`. A coefficient value of `16` +// in either `y` or `z` is **not** a literal 16: per §4.6.3.3 it is the +// `escape_flag` that signals an `escape_sequence` follows the +// Huffman codeword (and any sign-bit suffix). The +// `escape_sequence` is a unary `escape_prefix` of N `1` bits, a +// `0` `escape_separator`, and an `(N + 4)`-bit unsigned `escape_word`, +// whose reconstructed magnitude is `2^(N + 4) + escape_word`. The +// ESC bridge sits in [`crate::spectral_codebook::decode_esc_value`] +// / [`crate::spectral_codebook::encode_esc_value`] and is **not** +// part of the Huffman codeword carried here; this module's +// `hcod11_encode` / `hcod11_decode` cover the codeword only. +// +// The §4.6.3.3 unsigned polynomial `idx = y * (LAV + 1) + z = y * 17 +// + z` parks the zero-tuple `(0, 0)` at index 0 with the 4-bit +// codeword `0b0000` — the shortest slot. The interior pair `(1, 1)` +// lives at index `1 * 17 + 1 = 18` and shares the 4-bit floor with +// the zero-tuple (codeword `0b0001`). The far corner `(16, 16)` — +// both coefficients flagged as ESC — lives at index `16 * 17 + 16 = +// 288` with the 5-bit `0b00100` (`0x04`); the `(0, 16)` half-ESC +// tuple lives at index 16 with the 10-bit `0x38e`; the `(16, 0)` +// half-ESC tuple lives at index `16 * 17 = 272` with the 9-bit +// `0x1c2`. The maximum codeword length is **12 bits** — matching +// Codebook 10's ceiling — and exactly six rows reach that 12-bit +// ceiling (indices 12, 14, 15, 255, 269, 270 with codewords +// `0xffb`, `0xffa`, `0xffe`, `0xffd`, `0xffc`, `0xfff`). Exactly two +// rows reach the 4-bit floor: indices 0 and 18 (the zero-tuple and +// the interior `(1, 1)` pair). The codeword-length histogram is +// `{4: 2, 5: 6, 6: 7, 7: 16, 8: 59, 9: 55, 10: 95, 11: 43, 12: 6}`. +// +// Because Codebook 11 is unsigned, a sign-bit suffix follows the +// Huffman codeword for every non-zero coefficient per §4.6.3.3 — +// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` +// / `crate::spectral_codebook::derive_sign_bits`, separate from the +// Huffman codeword carried here. The §4.6.3.3 wire layout for an +// in-band coefficient pair is: `` then `0..=2` +// sign bits (one per non-zero coefficient). When `y` or `z` is at +// the ESC threshold (`= 16`), the wire layout extends with the +// `escape_sequence` bridge per §4.6.3 (handled outside this +// module). + +/// Number of entries in Table 4.A.12 (`289`, indices `0..=288`). +pub const HCOD11_NUM_ENTRIES: usize = 289; + +/// Maximum codeword length emitted by Table 4.A.12 (12 bits). +pub const HCOD11_MAX_LEN: u32 = 12; + +/// Table 4.A.12 — `(length_in_bits, codeword)` per index `0..=288`. +/// +/// Codewords are right-aligned within the `u16`. To emit one +/// bit-for-bit, write `codeword` as `length` bits MSB-first. +/// +/// A coefficient value of `16` in either slot of the decoded +/// `(y, z)` pair is the §4.6.3.3 `escape_flag` — the actual +/// magnitude is reconstructed by the +/// [`crate::spectral_codebook::decode_esc_value`] bridge from the +/// `escape_sequence` that follows the Huffman codeword (and any +/// sign-bit suffix) on the wire. +const HCOD11: [(u8, u16); HCOD11_NUM_ENTRIES] = [ + (4, 0x0000), // 0 — zero-tuple (y, z) = (0, 0) + (5, 0x0006), // 1 + (6, 0x0019), // 2 + (7, 0x003d), // 3 + (8, 0x009c), // 4 + (8, 0x00c6), // 5 + (9, 0x01a7), // 6 + (10, 0x0390), // 7 + (10, 0x03c2), // 8 + (10, 0x03df), // 9 + (11, 0x07e6), // 10 + (11, 0x07f3), // 11 + (12, 0x0ffb), // 12 + (11, 0x07ec), // 13 + (12, 0x0ffa), // 14 + (12, 0x0ffe), // 15 + (10, 0x038e), // 16 — (y, z) = (0, 16) — z at ESC threshold + (5, 0x0005), // 17 + (4, 0x0001), // 18 — interior (y, z) = (1, 1), shortest 4-bit `0b0000` + (5, 0x0008), // 19 + (6, 0x0014), // 20 + (7, 0x0037), // 21 + (7, 0x0042), // 22 + (8, 0x0092), // 23 + (8, 0x00af), // 24 + (9, 0x0191), // 25 + (9, 0x01a5), // 26 + (9, 0x01b5), // 27 + (10, 0x039e), // 28 + (10, 0x03c0), // 29 + (10, 0x03a2), // 30 + (10, 0x03cd), // 31 + (11, 0x07d6), // 32 + (8, 0x00ae), // 33 + (6, 0x0017), // 34 + (5, 0x0007), // 35 + (5, 0x0009), // 36 + (6, 0x0018), // 37 + (7, 0x0039), // 38 + (7, 0x0040), // 39 + (8, 0x008e), // 40 + (8, 0x00a3), // 41 + (8, 0x00b8), // 42 + (9, 0x0199), // 43 + (9, 0x01ac), // 44 + (9, 0x01c1), // 45 + (10, 0x03b1), // 46 + (10, 0x0396), // 47 + (10, 0x03be), // 48 + (10, 0x03ca), // 49 + (8, 0x009d), // 50 + (7, 0x003c), // 51 + (6, 0x0015), // 52 + (6, 0x0016), // 53 + (6, 0x001a), // 54 + (7, 0x003b), // 55 + (7, 0x0044), // 56 + (8, 0x0091), // 57 + (8, 0x00a5), // 58 + (8, 0x00be), // 59 + (9, 0x0196), // 60 + (9, 0x01ae), // 61 + (9, 0x01b9), // 62 + (10, 0x03a1), // 63 + (10, 0x0391), // 64 + (10, 0x03a5), // 65 + (10, 0x03d5), // 66 + (8, 0x0094), // 67 + (8, 0x009a), // 68 + (7, 0x0036), // 69 + (7, 0x0038), // 70 + (7, 0x003a), // 71 + (7, 0x0041), // 72 + (8, 0x008c), // 73 + (8, 0x009b), // 74 + (8, 0x00b0), // 75 + (8, 0x00c3), // 76 + (9, 0x019e), // 77 + (9, 0x01ab), // 78 + (9, 0x01bc), // 79 + (10, 0x039f), // 80 + (10, 0x038f), // 81 + (10, 0x03a9), // 82 + (10, 0x03cf), // 83 + (8, 0x0093), // 84 + (8, 0x00bf), // 85 + (7, 0x003e), // 86 + (7, 0x003f), // 87 + (7, 0x0043), // 88 + (7, 0x0045), // 89 + (8, 0x009e), // 90 + (8, 0x00a7), // 91 + (8, 0x00b9), // 92 + (9, 0x0194), // 93 + (9, 0x01a2), // 94 + (9, 0x01ba), // 95 + (9, 0x01c3), // 96 + (10, 0x03a6), // 97 + (10, 0x03a7), // 98 + (10, 0x03bb), // 99 + (10, 0x03d4), // 100 + (8, 0x009f), // 101 + (9, 0x01a0), // 102 + (8, 0x008f), // 103 + (8, 0x008d), // 104 + (8, 0x0090), // 105 + (8, 0x0098), // 106 + (8, 0x00a6), // 107 + (8, 0x00b6), // 108 + (8, 0x00c4), // 109 + (9, 0x019f), // 110 + (9, 0x01af), // 111 + (9, 0x01bf), // 112 + (10, 0x0399), // 113 + (10, 0x03bf), // 114 + (10, 0x03b4), // 115 + (10, 0x03c9), // 116 + (10, 0x03e7), // 117 + (8, 0x00a8), // 118 + (9, 0x01b6), // 119 + (8, 0x00ab), // 120 + (8, 0x00a4), // 121 + (8, 0x00aa), // 122 + (8, 0x00b2), // 123 + (8, 0x00c2), // 124 + (8, 0x00c5), // 125 + (9, 0x0198), // 126 + (9, 0x01a4), // 127 + (9, 0x01b8), // 128 + (10, 0x038c), // 129 + (10, 0x03a4), // 130 + (10, 0x03c4), // 131 + (10, 0x03c6), // 132 + (10, 0x03dd), // 133 + (10, 0x03e8), // 134 + (8, 0x00ad), // 135 + (10, 0x03af), // 136 + (9, 0x0192), // 137 + (8, 0x00bd), // 138 + (8, 0x00bc), // 139 + (9, 0x018e), // 140 + (9, 0x0197), // 141 + (9, 0x019a), // 142 + (9, 0x01a3), // 143 + (9, 0x01b1), // 144 + (10, 0x038d), // 145 + (10, 0x0398), // 146 + (10, 0x03b7), // 147 + (10, 0x03d3), // 148 + (10, 0x03d1), // 149 + (10, 0x03db), // 150 + (11, 0x07dd), // 151 + (8, 0x00b4), // 152 + (10, 0x03de), // 153 + (9, 0x01a9), // 154 + (9, 0x019b), // 155 + (9, 0x019c), // 156 + (9, 0x01a1), // 157 + (9, 0x01aa), // 158 + (9, 0x01ad), // 159 + (9, 0x01b3), // 160 + (10, 0x038b), // 161 + (10, 0x03b2), // 162 + (10, 0x03b8), // 163 + (10, 0x03ce), // 164 + (10, 0x03e1), // 165 + (10, 0x03e0), // 166 + (11, 0x07d2), // 167 + (11, 0x07e5), // 168 + (8, 0x00b7), // 169 + (11, 0x07e3), // 170 + (9, 0x01bb), // 171 + (9, 0x01a8), // 172 + (9, 0x01a6), // 173 + (9, 0x01b0), // 174 + (9, 0x01b2), // 175 + (9, 0x01b7), // 176 + (10, 0x039b), // 177 + (10, 0x039a), // 178 + (10, 0x03ba), // 179 + (10, 0x03b5), // 180 + (10, 0x03d6), // 181 + (11, 0x07d7), // 182 + (10, 0x03e4), // 183 + (11, 0x07d8), // 184 + (11, 0x07ea), // 185 + (8, 0x00ba), // 186 + (11, 0x07e8), // 187 + (10, 0x03a0), // 188 + (9, 0x01bd), // 189 + (9, 0x01b4), // 190 + (10, 0x038a), // 191 + (9, 0x01c4), // 192 + (10, 0x0392), // 193 + (10, 0x03aa), // 194 + (10, 0x03b0), // 195 + (10, 0x03bc), // 196 + (10, 0x03d7), // 197 + (11, 0x07d4), // 198 + (11, 0x07dc), // 199 + (11, 0x07db), // 200 + (11, 0x07d5), // 201 + (11, 0x07f0), // 202 + (8, 0x00c1), // 203 + (11, 0x07fb), // 204 + (10, 0x03c8), // 205 + (10, 0x03a3), // 206 + (10, 0x0395), // 207 + (10, 0x039d), // 208 + (10, 0x03ac), // 209 + (10, 0x03ae), // 210 + (10, 0x03c5), // 211 + (10, 0x03d8), // 212 + (10, 0x03e2), // 213 + (10, 0x03e6), // 214 + (11, 0x07e4), // 215 + (11, 0x07e7), // 216 + (11, 0x07e0), // 217 + (11, 0x07e9), // 218 + (11, 0x07f7), // 219 + (9, 0x0190), // 220 + (11, 0x07f2), // 221 + (10, 0x0393), // 222 + (9, 0x01be), // 223 + (9, 0x01c0), // 224 + (10, 0x0394), // 225 + (10, 0x0397), // 226 + (10, 0x03ad), // 227 + (10, 0x03c3), // 228 + (10, 0x03c1), // 229 + (10, 0x03d2), // 230 + (11, 0x07da), // 231 + (11, 0x07d9), // 232 + (11, 0x07df), // 233 + (11, 0x07eb), // 234 + (11, 0x07f4), // 235 + (11, 0x07fa), // 236 + (9, 0x0195), // 237 + (11, 0x07f8), // 238 + (10, 0x03bd), // 239 + (10, 0x039c), // 240 + (10, 0x03ab), // 241 + (10, 0x03a8), // 242 + (10, 0x03b3), // 243 + (10, 0x03b9), // 244 + (10, 0x03d0), // 245 + (10, 0x03e3), // 246 + (10, 0x03e5), // 247 + (11, 0x07e2), // 248 + (11, 0x07de), // 249 + (11, 0x07ed), // 250 + (11, 0x07f1), // 251 + (11, 0x07f9), // 252 + (11, 0x07fc), // 253 + (9, 0x0193), // 254 + (12, 0x0ffd), // 255 + (10, 0x03dc), // 256 + (10, 0x03b6), // 257 + (10, 0x03c7), // 258 + (10, 0x03cc), // 259 + (10, 0x03cb), // 260 + (10, 0x03d9), // 261 + (10, 0x03da), // 262 + (11, 0x07d3), // 263 + (11, 0x07e1), // 264 + (11, 0x07ee), // 265 + (11, 0x07ef), // 266 + (11, 0x07f5), // 267 + (11, 0x07f6), // 268 + (12, 0x0ffc), // 269 + (12, 0x0fff), // 270 + (9, 0x019d), // 271 + (9, 0x01c2), // 272 — (y, z) = (16, 0) — y at ESC threshold + (8, 0x00b5), // 273 + (8, 0x00a1), // 274 + (8, 0x0096), // 275 + (8, 0x0097), // 276 + (8, 0x0095), // 277 + (8, 0x0099), // 278 + (8, 0x00a0), // 279 + (8, 0x00a2), // 280 + (8, 0x00ac), // 281 + (8, 0x00a9), // 282 + (8, 0x00b1), // 283 + (8, 0x00b3), // 284 + (8, 0x00bb), // 285 + (8, 0x00c0), // 286 + (9, 0x018f), // 287 + (5, 0x0004), // 288 — far corner (y, z) = (16, 16) — both at ESC threshold +]; + +/// Encode a Codebook 11 codeword index (`0..=288`) to the wire Huffman +/// codeword from Table 4.A.12. +/// +/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned +/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` +/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal +/// range is `0..=288` (the 289-entry `17^2` enumeration of every +/// legal unsigned pair with each coefficient in `0..=16` where `16` +/// is the §4.6.3.3 escape flag). +/// +/// The inverse of [`hcod11_decode`]. Because Codebook 11 is unsigned, +/// callers transmit one sign bit after the codeword for each non-zero +/// coefficient via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / +/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) +/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried +/// here. When either coefficient is `16`, the ESC sequence from +/// [`encode_esc_value`](crate::spectral_codebook::encode_esc_value) +/// follows the sign-bit suffix; the ESC bridge is also outside this +/// module. +pub fn hcod11_encode(idx: u32) -> Result<(u8, u16)> { + let entry = HCOD11 + .get(idx as usize) + .ok_or(Error::SpectralCodebookIndexOutOfRange(11))?; + Ok(*entry) +} + +/// Decode one Codebook 11 Huffman codeword from `reader`, returning +/// the codeword index in `0..=288`. +/// +/// The decoder is a straight prefix-match: read one bit at a time +/// (MSB-first), look it up in a flat 289-entry table. The table is +/// small enough (max codeword length 12 bits, 289 entries) that a +/// single linear scan per bit-extend is cheaper than the storage +/// and build-time cost of a multi-level lookup acceleration table. +/// Returns [`Error::UnexpectedEnd`] on reader underflow. +/// +/// The codebook is a **complete** prefix code over 12 bits (Kraft +/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix +/// fully read from `reader` is guaranteed to match exactly one +/// entry — the bottom of the loop is unreachable when `reader` +/// produces 12 bits without underflowing. A purely defensive +/// `unreachable!()` guards the loop fall-through; it is verified +/// dead by the `hcod11_is_complete` regression test that +/// exhaustively walks all `2¹²` 12-bit prefixes. +/// +/// The §4.6.3.3 sign-bit suffix and the ESC sequence (when either +/// coefficient is `16`) lie outside this routine — for unsigned +/// Codebook 11 the caller consumes one sign bit per non-zero +/// coefficient after the Huffman codeword via +/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) +/// and dispatches onto the +/// [`decode_esc_value`](crate::spectral_codebook::decode_esc_value) +/// bridge when the §4.6.3.3 index translation surfaces a `16` in +/// either slot. +pub fn hcod11_decode(reader: &mut BitReader<'_>) -> Result { + let mut acc: u32 = 0; + for len in 1..=HCOD11_MAX_LEN { + let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; + acc = (acc << 1) | bit; + for (idx, &(entry_len, entry_cw)) in HCOD11.iter().enumerate() { + if u32::from(entry_len) == len && u32::from(entry_cw) == acc { + return Ok(idx as u32); + } + } + } + // Unreachable: HCOD11 is a complete 12-bit prefix code. The + // `hcod11_is_complete` regression test verifies every 12-bit + // prefix maps to exactly one entry. + unreachable!("HCOD11 is a complete 12-bit prefix code; the 12-bit walk must match"); +} + +/// Write a Codebook 11 codeword to `writer` by index. +/// +/// Convenience over `hcod11_encode` + manual `write_u32`. Returns +/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 288`. The +/// §4.6.3.3 sign-bit suffix and the ESC sequence are the caller's +/// responsibility — the suffix is one bit per non-zero coefficient +/// emitted low-frequency-first, and the ESC sequence is appended +/// after the sign bits for each coefficient whose value reaches the +/// `16` flag. +pub fn hcod11_write(writer: &mut BitWriter, idx: u32) -> Result<()> { + let (len, cw) = hcod11_encode(idx)?; + writer.write_u32(u32::from(cw), u32::from(len)); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ------------------------------------------------------------------- + // Table-shape invariants + // ------------------------------------------------------------------- + + #[test] + fn hcod1_has_exactly_81_entries() { + // 3^4 = 81 (signed LAV=1 → mod = 2*1+1 = 3, dim = 4). + assert_eq!(HCOD1.len(), HCOD1_NUM_ENTRIES); + assert_eq!(HCOD1_NUM_ENTRIES, 81); + } + + #[test] + fn hcod1_max_length_is_11_bits() { + let max = HCOD1.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD1_MAX_LEN); + assert_eq!(HCOD1_MAX_LEN, 11); + } + + #[test] + fn hcod1_min_length_is_one_bit_at_index_40() { + // The zero-tuple (w, x, y, z) = (0, 0, 0, 0) at index 40 + // gets the single bit `0`. Every other index has length >= 5. + for (idx, &(len, cw)) in HCOD1.iter().enumerate() { + if idx == 40 { + assert_eq!(len, 1, "index 40 must be 1-bit"); + assert_eq!(cw, 0, "index 40 codeword must be `0`"); + } else { + assert!( + len >= 5, + "every non-zero-tuple index must have length >= 5; idx={} len={}", + idx, + len + ); + } + } + } + + #[test] + fn hcod1_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD1.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + // ------------------------------------------------------------------- + // Kraft equality / completeness + // ------------------------------------------------------------------- + + #[test] + fn hcod1_kraft_sum_is_two_to_the_eleven() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD1_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD1 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 2048); + } + + #[test] + fn hcod1_is_complete() { + // Walk every 11-bit prefix, decode it via the same path the + // production decoder uses, and confirm every prefix yields + // exactly one entry. Bonus: confirm the decoded index round- + // trips back to the same codeword via `hcod1_encode`. + for prefix in 0u32..(1u32 << HCOD1_MAX_LEN) { + let bytes = [(prefix >> 3) as u8, ((prefix & 0x7) << 5) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod1_decode(&mut br).expect("11-bit prefix must decode"); + let (len, cw) = hcod1_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD1_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + // ------------------------------------------------------------------- + // Encoder API + // ------------------------------------------------------------------- + + #[test] + fn encode_zero_tuple_is_single_zero_bit() { + // Index 40 = the zero 4-tuple → 1-bit `0` codeword. + let (len, cw) = hcod1_encode(40).unwrap(); + assert_eq!(len, 1); + assert_eq!(cw, 0); + } + + #[test] + fn encode_first_entry_matches_table() { + // Spec PDF Table 4.A.2 row 0: length 11, codeword 0x7f8. + let (len, cw) = hcod1_encode(0).unwrap(); + assert_eq!(len, 11); + assert_eq!(cw, 0x7f8); + } + + #[test] + fn encode_last_entry_matches_table() { + // Spec PDF Table 4.A.2 row 80: length 11, codeword 0x7f4. + let (len, cw) = hcod1_encode(80).unwrap(); + assert_eq!(len, 11); + assert_eq!(cw, 0x7f4); + } + + #[test] + fn encode_rejects_out_of_range_index() { + assert!(matches!( + hcod1_encode(81), + Err(Error::SpectralCodebookIndexOutOfRange(1)) + )); + assert!(matches!( + hcod1_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(1)) + )); + } + + // ------------------------------------------------------------------- + // Decoder API + // ------------------------------------------------------------------- + + #[test] + fn decode_single_zero_bit_yields_index_40() { + // One byte starting with `0` followed by anything → idx 40. + let bytes = [0b0111_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod1_decode(&mut br).unwrap(); + assert_eq!(idx, 40); + // Only one bit consumed; the remaining 7 are untouched. + assert_eq!(br.bit_position(), 1); + } + + #[test] + fn decode_first_entry_round_trip() { + // Index 0 → length 11, codeword 0x7f8 = 0b111_1111_1000. + // Pack into 2 bytes left-aligned: 0xff, 0x00. + let bytes = [0xff, 0x00]; + let mut br = BitReader::new(&bytes); + let idx = hcod1_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + assert_eq!(br.bit_position(), 11); + } + + #[test] + fn decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod1_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + // ------------------------------------------------------------------- + // Writer API + // ------------------------------------------------------------------- + + #[test] + fn write_then_decode_round_trips_every_index() { + for idx in 0..HCOD1_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod1_write(&mut w, idx).unwrap(); + // Pad to byte boundary if needed so BitReader can consume. + let (len, _) = hcod1_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod1_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod1_write(&mut w, 81), + Err(Error::SpectralCodebookIndexOutOfRange(1)) + )); + } + + // ------------------------------------------------------------------- + // Codebook 2 — Table 4.A.3 + // ------------------------------------------------------------------- + + #[test] + fn hcod2_has_exactly_81_entries() { + // 3^4 = 81 (signed LAV=1 → mod = 2*1+1 = 3, dim = 4) — same + // tuple universe as Codebook 1. + assert_eq!(HCOD2.len(), HCOD2_NUM_ENTRIES); + assert_eq!(HCOD2_NUM_ENTRIES, 81); + } + + #[test] + fn hcod2_max_length_is_9_bits() { + let max = HCOD2.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD2_MAX_LEN); + assert_eq!(HCOD2_MAX_LEN, 9); + } + + #[test] + fn hcod2_min_length_is_three_bits_at_index_40() { + // The zero-tuple (w, x, y, z) = (0, 0, 0, 0) at index 40 + // gets a 3-bit codeword `0b000` (vs the 1-bit `0` of + // Codebook 1). Every other index has length >= 4. + for (idx, &(len, cw)) in HCOD2.iter().enumerate() { + if idx == 40 { + assert_eq!(len, 3, "index 40 must be 3-bit"); + assert_eq!(cw, 0, "index 40 codeword must be `0`"); + } else { + assert!( + len >= 4, + "every non-zero-tuple index must have length >= 4; idx={} len={}", + idx, + len + ); + } + } + } + + #[test] + fn hcod2_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD2.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod2_kraft_sum_is_two_to_the_nine() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD2_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD2 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 512); + } + + #[test] + fn hcod2_is_complete() { + // Walk every 9-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod2_encode`. + for prefix in 0u32..(1u32 << HCOD2_MAX_LEN) { + // Pack `prefix` (9 bits) left-aligned into two bytes: + // [bits 8..1] [bit 0 << 7 | rest]. + let bytes = [(prefix >> 1) as u8, ((prefix & 0x1) << 7) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod2_decode(&mut br).expect("9-bit prefix must decode"); + let (len, cw) = hcod2_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD2_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn encode_zero_tuple_is_three_zero_bits_in_codebook_2() { + // Index 40 = the zero 4-tuple → 3-bit `000` codeword. + let (len, cw) = hcod2_encode(40).unwrap(); + assert_eq!(len, 3); + assert_eq!(cw, 0); + } + + #[test] + fn hcod2_encode_first_entry_matches_table() { + // Spec PDF Table 4.A.3 row 0: length 9, codeword 0x1f3. + let (len, cw) = hcod2_encode(0).unwrap(); + assert_eq!(len, 9); + assert_eq!(cw, 0x1f3); + } + + #[test] + fn hcod2_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.3 row 80: length 9, codeword 0x1f6. + let (len, cw) = hcod2_encode(80).unwrap(); + assert_eq!(len, 9); + assert_eq!(cw, 0x1f6); + } + + #[test] + fn hcod2_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod2_encode(81), + Err(Error::SpectralCodebookIndexOutOfRange(2)) + )); + assert!(matches!( + hcod2_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(2)) + )); + } + + #[test] + fn hcod2_decode_three_zero_bits_yields_index_40() { + // Three leading `0` bits → idx 40. Remaining 5 bits untouched. + let bytes = [0b0001_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod2_decode(&mut br).unwrap(); + assert_eq!(idx, 40); + assert_eq!(br.bit_position(), 3); + } + + #[test] + fn hcod2_decode_first_entry_round_trip() { + // Index 0 → length 9, codeword 0x1f3 = 0b1_1111_0011. + // Pack into 2 bytes left-aligned: 0xf9, 0x80. + // 0x1f3 << 7 = 0xf980 (16-bit big-endian). + let bytes = [0xf9, 0x80]; + let mut br = BitReader::new(&bytes); + let idx = hcod2_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + assert_eq!(br.bit_position(), 9); + } + + #[test] + fn hcod2_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod2_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod2_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD2_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod2_write(&mut w, idx).unwrap(); + let (len, _) = hcod2_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod2_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod2_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod2_write(&mut w, 81), + Err(Error::SpectralCodebookIndexOutOfRange(2)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebooks 1 and 2 share the same tuple universe but + // never share a codeword for the same index (different lengths + // and codewords for index 40 — 1 bit `0` vs 3 bits `0b000`). + // ------------------------------------------------------------------- + + #[test] + fn codebook_1_and_2_disagree_on_zero_tuple_codeword_length() { + let (l1, _) = hcod1_encode(40).unwrap(); + let (l2, _) = hcod2_encode(40).unwrap(); + // Both books carry the zero-tuple at index 40 but use + // different codeword lengths: 1 bit for Codebook 1, 3 bits + // for Codebook 2. + assert_eq!(l1, 1); + assert_eq!(l2, 3); + assert_ne!(l1, l2); + } + + // ------------------------------------------------------------------- + // Codebook 3 — Table 4.A.4 + // ------------------------------------------------------------------- + + #[test] + fn hcod3_has_exactly_81_entries() { + // 3^4 = 81 (unsigned LAV=2 → mod = lav+1 = 3, dim = 4). + assert_eq!(HCOD3.len(), HCOD3_NUM_ENTRIES); + assert_eq!(HCOD3_NUM_ENTRIES, 81); + } + + #[test] + fn hcod3_max_length_is_16_bits() { + let max = HCOD3.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD3_MAX_LEN); + assert_eq!(HCOD3_MAX_LEN, 16); + } + + #[test] + fn hcod3_min_length_is_one_bit_at_index_0() { + // Unsigned books put the all-zero magnitude n-tuple at + // index 0 (vs index 40 for the signed books); it carries the + // single bit `0`. Every other index has length >= 4. + for (idx, &(len, cw)) in HCOD3.iter().enumerate() { + if idx == 0 { + assert_eq!(len, 1, "index 0 must be 1-bit"); + assert_eq!(cw, 0, "index 0 codeword must be `0`"); + } else { + assert!( + len >= 4, + "every non-zero-tuple index must have length >= 4; idx={} len={}", + idx, + len + ); + } + } + } + + #[test] + fn hcod3_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD3.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod3_kraft_sum_is_two_to_the_sixteen() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD3_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD3 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 65536); + } + + #[test] + fn hcod3_is_complete() { + // Walk every 16-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod3_encode`. + for prefix in 0u32..(1u32 << HCOD3_MAX_LEN) { + // `prefix` already fits in 16 bits: pack left-aligned + // into two bytes (high byte first). + let bytes = [(prefix >> 8) as u8, (prefix & 0xff) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod3_decode(&mut br).expect("16-bit prefix must decode"); + let (len, cw) = hcod3_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD3_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#06x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod3_encode_zero_tuple_is_single_zero_bit() { + // Index 0 = the zero 4-tuple `(0, 0, 0, 0)` in the unsigned + // book → 1-bit `0` codeword. + let (len, cw) = hcod3_encode(0).unwrap(); + assert_eq!(len, 1); + assert_eq!(cw, 0); + } + + #[test] + fn hcod3_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.4 row 80: length 15, codeword 0x7ffa. + let (len, cw) = hcod3_encode(80).unwrap(); + assert_eq!(len, 15); + assert_eq!(cw, 0x7ffa); + } + + #[test] + fn hcod3_encode_index_62_is_the_only_full_16_bit_codeword_0xffff() { + // Spec PDF Table 4.A.4 row 62: length 16, codeword 0xffff + // (the all-ones 16-bit pattern). Verify by spot-check that + // this is the unique row with codeword 0xffff. + let (len, cw) = hcod3_encode(62).unwrap(); + assert_eq!(len, 16); + assert_eq!(cw, 0xffff); + let count_matching = HCOD3.iter().filter(|&&(_, c)| c == 0xffff).count(); + assert_eq!(count_matching, 1); + } + + #[test] + fn hcod3_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod3_encode(81), + Err(Error::SpectralCodebookIndexOutOfRange(3)) + )); + assert!(matches!( + hcod3_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(3)) + )); + } + + #[test] + fn hcod3_decode_single_zero_bit_yields_index_0() { + // Leading `0` bit → idx 0 (the unsigned book's zero-tuple). + let bytes = [0b0111_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod3_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + // Only one bit consumed; the remaining 7 are untouched. + assert_eq!(br.bit_position(), 1); + } + + #[test] + fn hcod3_decode_full_16_bit_codeword_round_trips() { + // Index 62 → length 16, codeword 0xffff. Pack as two bytes. + let bytes = [0xff, 0xff]; + let mut br = BitReader::new(&bytes); + let idx = hcod3_decode(&mut br).unwrap(); + assert_eq!(idx, 62); + assert_eq!(br.bit_position(), 16); + } + + #[test] + fn hcod3_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod3_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod3_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD3_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod3_write(&mut w, idx).unwrap(); + let (len, _) = hcod3_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod3_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod3_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod3_write(&mut w, 81), + Err(Error::SpectralCodebookIndexOutOfRange(3)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebook 3 zero-tuple sits at a different index + // than Codebooks 1 / 2 because unsigned books use a different + // index origin from signed books. + // ------------------------------------------------------------------- + + #[test] + fn codebook_3_zero_tuple_lives_at_index_zero_not_forty() { + // The zero magnitude 4-tuple `(0, 0, 0, 0)`: + // - signed book (mod = 3, offset = LAV = 1): polynomial + // evaluates to (0+1)*27 + (0+1)*9 + (0+1)*3 + (0+1) = 40. + // - unsigned book (mod = 3, offset = 0): polynomial + // evaluates to (0)*27 + (0)*9 + (0)*3 + (0) = 0. + // So the zero-tuple lives at index 40 in HCOD1 / HCOD2 and + // at index 0 in HCOD3. Both still carry a 1-bit codeword in + // their respective books (Codebook 1 + 3); Codebook 2 trades + // the 1-bit zero-tuple for a 3-bit one to free up the short + // codes for the non-zero tuples its target statistics prefer. + let (l1, cw1) = hcod1_encode(40).unwrap(); + let (l3, cw3) = hcod3_encode(0).unwrap(); + assert_eq!(l1, 1); + assert_eq!(cw1, 0); + assert_eq!(l3, 1); + assert_eq!(cw3, 0); + } + + // ------------------------------------------------------------------- + // Codebook 4 — Table 4.A.5 + // ------------------------------------------------------------------- + + #[test] + fn hcod4_has_exactly_81_entries() { + // 3^4 = 81 (unsigned LAV=2 → mod = lav+1 = 3, dim = 4) — same + // tuple universe as Codebook 3. + assert_eq!(HCOD4.len(), HCOD4_NUM_ENTRIES); + assert_eq!(HCOD4_NUM_ENTRIES, 81); + } + + #[test] + fn hcod4_max_length_is_12_bits() { + let max = HCOD4.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD4_MAX_LEN); + assert_eq!(HCOD4_MAX_LEN, 12); + } + + #[test] + fn hcod4_min_length_is_four_bits_at_index_40() { + // The shortest codeword in Codebook 4 is 4 bits, parked at + // index 40 with the all-zero pattern `0b0000`. Every other + // index has length >= 4 (Codebook 4's distribution has a + // dense 4-bit head: indices 0, 4, 13, 27, 30, 31, 36, 37, 39, + // 40 all share length 4). + let (len_40, cw_40) = (HCOD4[40].0, HCOD4[40].1); + assert_eq!(len_40, 4, "index 40 must be 4-bit"); + assert_eq!(cw_40, 0, "index 40 codeword must be `0b0000`"); + for (idx, &(len, _)) in HCOD4.iter().enumerate() { + assert!( + len >= 4, + "every index must have length >= 4; idx={} len={}", + idx, + len + ); + } + } + + #[test] + fn hcod4_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD4.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod4_kraft_sum_is_two_to_the_twelve() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD4_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD4 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 4096); + } + + #[test] + fn hcod4_is_complete() { + // Walk every 12-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod4_encode`. + for prefix in 0u32..(1u32 << HCOD4_MAX_LEN) { + // Pack `prefix` (12 bits) left-aligned into two bytes: + // high byte = bits 11..4, low byte = (bits 3..0) << 4. + let bytes = [(prefix >> 4) as u8, ((prefix & 0xf) << 4) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod4_decode(&mut br).expect("12-bit prefix must decode"); + let (len, cw) = hcod4_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD4_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod4_encode_index_40_is_4_bit_zero_codeword() { + // Spec PDF Table 4.A.5 row 40: length 4, codeword 0 (the + // shortest codeword in the table). + let (len, cw) = hcod4_encode(40).unwrap(); + assert_eq!(len, 4); + assert_eq!(cw, 0); + } + + #[test] + fn hcod4_encode_first_entry_matches_table() { + // Spec PDF Table 4.A.5 row 0: length 4, codeword 0x7. + let (len, cw) = hcod4_encode(0).unwrap(); + assert_eq!(len, 4); + assert_eq!(cw, 0x7); + } + + #[test] + fn hcod4_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.5 row 80: length 11, codeword 0x7fc. + let (len, cw) = hcod4_encode(80).unwrap(); + assert_eq!(len, 11); + assert_eq!(cw, 0x7fc); + } + + #[test] + fn hcod4_encode_indices_62_and_74_are_the_full_12_bit_codewords() { + // Spec PDF Table 4.A.5 row 62: length 12, codeword 0xfff. + // Spec PDF Table 4.A.5 row 74: length 12, codeword 0xffe. + // These are the only two 12-bit rows in Codebook 4. + let (len_62, cw_62) = hcod4_encode(62).unwrap(); + assert_eq!((len_62, cw_62), (12, 0xfff)); + let (len_74, cw_74) = hcod4_encode(74).unwrap(); + assert_eq!((len_74, cw_74), (12, 0xffe)); + let count_12_bit = HCOD4.iter().filter(|&&(l, _)| l == 12).count(); + assert_eq!(count_12_bit, 2); + } + + #[test] + fn hcod4_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod4_encode(81), + Err(Error::SpectralCodebookIndexOutOfRange(4)) + )); + assert!(matches!( + hcod4_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(4)) + )); + } + + #[test] + fn hcod4_decode_four_zero_bits_yields_index_40() { + // Leading `0b0000` → idx 40 (Codebook 4's shortest codeword). + // Remaining 4 bits of the byte untouched. + let bytes = [0b0000_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod4_decode(&mut br).unwrap(); + assert_eq!(idx, 40); + assert_eq!(br.bit_position(), 4); + } + + #[test] + fn hcod4_decode_full_12_bit_codeword_round_trips_index_62() { + // Index 62 → length 12, codeword 0xfff = 0b1111_1111_1111. + // Pack left-aligned into 2 bytes: 0xff, 0xf0. + let bytes = [0xff, 0xf0]; + let mut br = BitReader::new(&bytes); + let idx = hcod4_decode(&mut br).unwrap(); + assert_eq!(idx, 62); + assert_eq!(br.bit_position(), 12); + } + + #[test] + fn hcod4_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod4_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod4_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD4_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod4_write(&mut w, idx).unwrap(); + let (len, _) = hcod4_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod4_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod4_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod4_write(&mut w, 81), + Err(Error::SpectralCodebookIndexOutOfRange(4)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebook 3 and Codebook 4 share the unsigned dim-4 + // LAV-2 tuple universe (same Table 4.95 row shape) but assign + // different codewords for the same tuple — Codebook 3 gives the + // zero-tuple the single-bit codeword `0`; Codebook 4 lifts it to + // a 4-bit `0b0111` and parks the 4-bit `0b0000` shortest at + // index 40 instead. + // ------------------------------------------------------------------- + + #[test] + fn codebook_3_and_4_disagree_on_zero_tuple_codeword() { + let (l3, cw3) = hcod3_encode(0).unwrap(); + let (l4, cw4) = hcod4_encode(0).unwrap(); + assert_eq!((l3, cw3), (1, 0)); + assert_eq!((l4, cw4), (4, 0x7)); + // Codebook 4's shortest codeword sits at a different index + // (40) with a different value (`0b0000`). + let (l40, cw40) = hcod4_encode(40).unwrap(); + assert_eq!((l40, cw40), (4, 0)); + } + + // ------------------------------------------------------------------- + // Codebook 5 (Table 4.A.6) — signed dim-2 LAV-4 pair book + // ------------------------------------------------------------------- + + #[test] + fn hcod5_has_exactly_81_entries() { + // 9^2 = 81 (signed LAV=4 → mod = 2*4+1 = 9, dim = 2). + assert_eq!(HCOD5.len(), HCOD5_NUM_ENTRIES); + assert_eq!(HCOD5_NUM_ENTRIES, 81); + } + + #[test] + fn hcod5_max_length_is_13_bits() { + let max = HCOD5.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD5_MAX_LEN); + assert_eq!(HCOD5_MAX_LEN, 13); + } + + #[test] + fn hcod5_min_length_is_one_bit_at_index_40() { + // The shortest codeword in Codebook 5 is the single bit `0` + // at index 40 — the §4.6.3.3 zero-tuple `(0, 0)` for a + // signed pair book with LAV = 4 lands at the centre of the + // index range, not at the edges. + let (len_40, cw_40) = (HCOD5[40].0, HCOD5[40].1); + assert_eq!(len_40, 1, "index 40 must be 1-bit"); + assert_eq!(cw_40, 0, "index 40 codeword must be `0`"); + let count_1_bit = HCOD5.iter().filter(|&&(l, _)| l == 1).count(); + assert_eq!(count_1_bit, 1, "exactly one 1-bit codeword"); + } + + #[test] + fn hcod5_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD5.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod5_kraft_sum_is_two_to_the_thirteen() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD5_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD5 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 8192); + } + + #[test] + fn hcod5_is_complete() { + // Walk every 13-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod5_encode`. + for prefix in 0u32..(1u32 << HCOD5_MAX_LEN) { + // Pack `prefix` (13 bits) left-aligned into two bytes: + // high byte = bits 12..5, low byte = (bits 4..0) << 3. + let bytes = [(prefix >> 5) as u8, ((prefix & 0x1f) << 3) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod5_decode(&mut br).expect("13-bit prefix must decode"); + let (len, cw) = hcod5_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD5_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#06x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod5_encode_index_40_is_single_zero_bit() { + // Spec PDF Table 4.A.6 row 40: length 1, codeword 0 — the + // §4.6.3.3 zero-tuple `(0, 0)`. + let (len, cw) = hcod5_encode(40).unwrap(); + assert_eq!(len, 1); + assert_eq!(cw, 0); + } + + #[test] + fn hcod5_encode_first_entry_matches_table() { + // Spec PDF Table 4.A.6 row 0: length 13, codeword 0x1fff — + // the lower-left corner `(-4, -4)` of the signed pair lattice. + let (len, cw) = hcod5_encode(0).unwrap(); + assert_eq!(len, 13); + assert_eq!(cw, 0x1fff); + } + + #[test] + fn hcod5_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.6 row 80: length 13, codeword 0x1ffe — + // the upper-right corner `(+4, +4)` of the signed pair lattice. + let (len, cw) = hcod5_encode(80).unwrap(); + assert_eq!(len, 13); + assert_eq!(cw, 0x1ffe); + } + + #[test] + fn hcod5_encode_four_13_bit_rows_are_the_lattice_corners() { + // The four 13-bit codewords sit at indices 0, 8, 72, 80 — the + // four `(±4, ±4)` corners of the signed `9 × 9` pair lattice. + let expected = [ + (0u32, 0x1fffu16), // (-4, -4) + (8u32, 0x1ffdu16), // (-4, +4) + (72u32, 0x1ffcu16), // (+4, -4) + (80u32, 0x1ffeu16), // (+4, +4) + ]; + let observed: Vec<_> = HCOD5 + .iter() + .enumerate() + .filter_map(|(i, &(l, cw))| if l == 13 { Some((i as u32, cw)) } else { None }) + .collect(); + assert_eq!(observed.len(), 4); + for (e, o) in expected.iter().zip(observed.iter()) { + assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); + } + } + + #[test] + fn hcod5_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod5_encode(81), + Err(Error::SpectralCodebookIndexOutOfRange(5)) + )); + assert!(matches!( + hcod5_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(5)) + )); + } + + #[test] + fn hcod5_decode_single_zero_bit_yields_index_40() { + // Leading bit `0` → idx 40 (the zero-tuple `(0, 0)`). + // Remaining 7 bits of the byte untouched. + let bytes = [0b0111_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod5_decode(&mut br).unwrap(); + assert_eq!(idx, 40); + assert_eq!(br.bit_position(), 1); + } + + #[test] + fn hcod5_decode_full_13_bit_codeword_round_trips_index_0() { + // Index 0 → length 13, codeword 0x1fff = 0b1_1111_1111_1111. + // Pack left-aligned into 2 bytes: 0xff, 0xf8. + let bytes = [0xff, 0xf8]; + let mut br = BitReader::new(&bytes); + let idx = hcod5_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + assert_eq!(br.bit_position(), 13); + } + + #[test] + fn hcod5_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod5_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod5_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD5_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod5_write(&mut w, idx).unwrap(); + let (len, _) = hcod5_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod5_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod5_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod5_write(&mut w, 81), + Err(Error::SpectralCodebookIndexOutOfRange(5)) + )); + } + + // ------------------------------------------------------------------- + // Codebook 6 (Table 4.A.7) — signed dim-2 LAV-4 pair book + // ------------------------------------------------------------------- + + #[test] + fn hcod6_has_exactly_81_entries() { + // 9^2 = 81 (signed LAV=4 → mod = 2*4+1 = 9, dim = 2) — same + // tuple universe as Codebook 5. + assert_eq!(HCOD6.len(), HCOD6_NUM_ENTRIES); + assert_eq!(HCOD6_NUM_ENTRIES, 81); + } + + #[test] + fn hcod6_max_length_is_11_bits() { + let max = HCOD6.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD6_MAX_LEN); + assert_eq!(HCOD6_MAX_LEN, 11); + } + + #[test] + fn hcod6_min_length_is_four_bits_at_index_40() { + // The shortest codeword in Codebook 6 is 4 bits, parked at + // index 40 (the §4.6.3.3 zero-tuple `(0, 0)` for a signed + // pair book with LAV=4) with the all-zero pattern `0b0000`. + // Every other index has length >= 4 (Codebook 6's + // distribution has a dense 4-bit head: indices 30, 31, 32, + // 39, 40, 41, 48, 49, 50 all share length 4). + let (len_40, cw_40) = (HCOD6[40].0, HCOD6[40].1); + assert_eq!(len_40, 4, "index 40 must be 4-bit"); + assert_eq!(cw_40, 0, "index 40 codeword must be `0b0000`"); + for (idx, &(len, _)) in HCOD6.iter().enumerate() { + assert!( + len >= 4, + "every index must have length >= 4; idx={} len={}", + idx, + len + ); + } + } + + #[test] + fn hcod6_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD6.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod6_kraft_sum_is_two_to_the_eleven() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD6_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD6 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 2048); + } + + #[test] + fn hcod6_is_complete() { + // Walk every 11-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod6_encode`. + for prefix in 0u32..(1u32 << HCOD6_MAX_LEN) { + // Pack `prefix` (11 bits) left-aligned into two bytes: + // high byte = bits 10..3, low byte = (bits 2..0) << 5. + let bytes = [(prefix >> 3) as u8, ((prefix & 0x7) << 5) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod6_decode(&mut br).expect("11-bit prefix must decode"); + let (len, cw) = hcod6_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD6_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod6_encode_index_40_is_4_bit_zero_codeword() { + // Spec PDF Table 4.A.7 row 40: length 4, codeword 0 — the + // §4.6.3.3 zero-tuple `(0, 0)`. + let (len, cw) = hcod6_encode(40).unwrap(); + assert_eq!(len, 4); + assert_eq!(cw, 0); + } + + #[test] + fn hcod6_encode_first_entry_matches_table() { + // Spec PDF Table 4.A.7 row 0: length 11, codeword 0x7fe — + // the lower-left corner `(-4, -4)` of the signed pair lattice. + let (len, cw) = hcod6_encode(0).unwrap(); + assert_eq!(len, 11); + assert_eq!(cw, 0x7fe); + } + + #[test] + fn hcod6_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.7 row 80: length 11, codeword 0x7fc — + // the upper-right corner `(+4, +4)` of the signed pair lattice. + let (len, cw) = hcod6_encode(80).unwrap(); + assert_eq!(len, 11); + assert_eq!(cw, 0x7fc); + } + + #[test] + fn hcod6_encode_four_11_bit_rows_are_the_lattice_corners() { + // The four 11-bit codewords sit at indices 0, 8, 72, 80 — the + // four `(±4, ±4)` corners of the signed `9 × 9` pair lattice. + let expected = [ + (0u32, 0x7feu16), // (-4, -4) + (8u32, 0x7fdu16), // (-4, +4) + (72u32, 0x7ffu16), // (+4, -4) + (80u32, 0x7fcu16), // (+4, +4) + ]; + let observed: Vec<_> = HCOD6 + .iter() + .enumerate() + .filter_map(|(i, &(l, cw))| if l == 11 { Some((i as u32, cw)) } else { None }) + .collect(); + assert_eq!(observed.len(), 4); + for (e, o) in expected.iter().zip(observed.iter()) { + assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); + } + } + + #[test] + fn hcod6_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod6_encode(81), + Err(Error::SpectralCodebookIndexOutOfRange(6)) + )); + assert!(matches!( + hcod6_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(6)) + )); + } + + #[test] + fn hcod6_decode_four_zero_bits_yields_index_40() { + // Leading `0b0000` → idx 40 (the zero-tuple `(0, 0)`). + // Remaining 4 bits of the byte untouched. + let bytes = [0b0000_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod6_decode(&mut br).unwrap(); + assert_eq!(idx, 40); + assert_eq!(br.bit_position(), 4); + } + + #[test] + fn hcod6_decode_full_11_bit_codeword_round_trips_index_72() { + // Index 72 → length 11, codeword 0x7ff = 0b111_1111_1111. + // Pack left-aligned into 2 bytes: 0xff, 0xe0. + let bytes = [0xff, 0xe0]; + let mut br = BitReader::new(&bytes); + let idx = hcod6_decode(&mut br).unwrap(); + assert_eq!(idx, 72); + assert_eq!(br.bit_position(), 11); + } + + #[test] + fn hcod6_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod6_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod6_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD6_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod6_write(&mut w, idx).unwrap(); + let (len, _) = hcod6_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod6_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod6_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod6_write(&mut w, 81), + Err(Error::SpectralCodebookIndexOutOfRange(6)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebooks 5 and 6 share the signed pair tuple + // universe (Table 4.95 rows 5 and 6 are identical except for the + // `Codebook listed in Table` column) but assign different codewords + // for the same tuple — Codebook 5 gives the zero-tuple the single- + // bit codeword `0`; Codebook 6 lifts it to a 4-bit `0b0000` and + // pulls the ceiling back from 13 down to 11 bits. + // ------------------------------------------------------------------- + + #[test] + fn codebook_5_and_6_disagree_on_zero_tuple_codeword() { + let (l5, cw5) = hcod5_encode(40).unwrap(); + let (l6, cw6) = hcod6_encode(40).unwrap(); + assert_eq!((l5, cw5), (1, 0)); + assert_eq!((l6, cw6), (4, 0)); + } + + #[test] + fn codebook_5_and_6_agree_on_lattice_corner_indices() { + // Both books pin the four (±4, ±4) lattice corners to their + // respective maximum-length codewords — Codebook 5 at 13 bits, + // Codebook 6 at 11 bits — but at the same four index positions. + let corners: Vec = [0, 8, 72, 80].to_vec(); + let cb5_max_idx: Vec = HCOD5 + .iter() + .enumerate() + .filter_map(|(i, &(l, _))| { + if u32::from(l) == HCOD5_MAX_LEN { + Some(i) + } else { + None + } + }) + .collect(); + let cb6_max_idx: Vec = HCOD6 + .iter() + .enumerate() + .filter_map(|(i, &(l, _))| { + if u32::from(l) == HCOD6_MAX_LEN { + Some(i) + } else { + None + } + }) + .collect(); + assert_eq!(cb5_max_idx, corners); + assert_eq!(cb6_max_idx, corners); + } + + // ------------------------------------------------------------------- + // Codebook 7 (Table 4.A.8): unsigned pair, dim=2, LAV=7, + // 64 entries indexed 0..=63 (8^2 lattice). Zero-tuple `(0, 0)` at + // index 0 carries the single-bit codeword `0`. Maximum codeword + // length 12 bits. Complete prefix code: Kraft sum = 4096 = 2^12. + // ------------------------------------------------------------------- + + #[test] + fn hcod7_has_exactly_64_entries() { + // 8^2 = 64 (unsigned LAV=7 → mod = 7+1 = 8, dim = 2). + assert_eq!(HCOD7.len(), HCOD7_NUM_ENTRIES); + assert_eq!(HCOD7_NUM_ENTRIES, 64); + } + + #[test] + fn hcod7_max_length_is_12_bits() { + let max = HCOD7.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD7_MAX_LEN); + assert_eq!(HCOD7_MAX_LEN, 12); + } + + #[test] + fn hcod7_min_length_is_one_bit_at_index_0() { + // The shortest codeword in Codebook 7 is 1 bit, parked at + // index 0 (the §4.6.3.3 zero-tuple `(0, 0)` for an unsigned + // pair book with LAV=7) with the codeword `0`. Index 0 is the + // only 1-bit entry; every other index has length >= 3. + let (len_0, cw_0) = (HCOD7[0].0, HCOD7[0].1); + assert_eq!(len_0, 1, "index 0 must be 1-bit"); + assert_eq!(cw_0, 0, "index 0 codeword must be `0`"); + let single_bit_entries: usize = HCOD7.iter().filter(|&&(len, _)| len == 1).count(); + assert_eq!(single_bit_entries, 1, "exactly one 1-bit codeword"); + for (idx, &(len, _)) in HCOD7.iter().enumerate().skip(1) { + assert!( + len >= 3, + "every index > 0 must have length >= 3; idx={} len={}", + idx, + len + ); + } + } + + #[test] + fn hcod7_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD7.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod7_kraft_sum_is_two_to_the_twelve() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD7_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD7 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 4096); + } + + #[test] + fn hcod7_is_complete() { + // Walk every 12-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod7_encode`. + for prefix in 0u32..(1u32 << HCOD7_MAX_LEN) { + // Pack `prefix` (12 bits) left-aligned into two bytes: + // high byte = bits 11..4, low byte = (bits 3..0) << 4. + let bytes = [(prefix >> 4) as u8, ((prefix & 0xf) << 4) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod7_decode(&mut br).expect("12-bit prefix must decode"); + let (len, cw) = hcod7_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD7_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod7_encode_index_0_is_1_bit_zero_codeword() { + // Spec PDF Table 4.A.8 row 0: length 1, codeword 0 — the + // §4.6.3.3 zero-tuple `(0, 0)`. + let (len, cw) = hcod7_encode(0).unwrap(); + assert_eq!(len, 1); + assert_eq!(cw, 0); + } + + #[test] + fn hcod7_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.8 row 63: length 12, codeword 0xfff — + // the far corner `(7, 7)` of the unsigned `8 × 8` pair lattice. + let (len, cw) = hcod7_encode(63).unwrap(); + assert_eq!(len, 12); + assert_eq!(cw, 0xfff); + } + + #[test] + fn hcod7_encode_four_12_bit_rows_match_table() { + // Exactly four rows reach the 12-bit ceiling in Table 4.A.8: + // indices 54, 55, 62, 63 with codewords ffd, ffe, ffc, fff. + let expected = [ + (54u32, 0xffdu16), + (55u32, 0xffeu16), + (62u32, 0xffcu16), + (63u32, 0xfffu16), + ]; + let observed: Vec<_> = HCOD7 + .iter() + .enumerate() + .filter_map(|(i, &(l, cw))| if l == 12 { Some((i as u32, cw)) } else { None }) + .collect(); + assert_eq!(observed.len(), 4); + for (e, o) in expected.iter().zip(observed.iter()) { + assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); + } + } + + #[test] + fn hcod7_encode_index_8_is_first_y1_row() { + // Index 8 = (y, z) = (1, 0) via `y * 8 + z`. Table 4.A.8 row + // 8: length 3, codeword 4. + let (len, cw) = hcod7_encode(8).unwrap(); + assert_eq!(len, 3); + assert_eq!(cw, 4); + } + + #[test] + fn hcod7_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod7_encode(64), + Err(Error::SpectralCodebookIndexOutOfRange(7)) + )); + assert!(matches!( + hcod7_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(7)) + )); + } + + #[test] + fn hcod7_decode_single_zero_bit_yields_index_0() { + // Leading `0` → idx 0 (the zero-tuple `(0, 0)`). Remaining 7 + // bits of the byte untouched. + let bytes = [0b0111_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod7_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + assert_eq!(br.bit_position(), 1); + } + + #[test] + fn hcod7_decode_full_12_bit_codeword_round_trips_index_63() { + // Index 63 → length 12, codeword 0xfff = 0b1111_1111_1111. + // Pack left-aligned into 2 bytes: 0xff, 0xf0. + let bytes = [0xff, 0xf0]; + let mut br = BitReader::new(&bytes); + let idx = hcod7_decode(&mut br).unwrap(); + assert_eq!(idx, 63); + assert_eq!(br.bit_position(), 12); + } + + #[test] + fn hcod7_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod7_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod7_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD7_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod7_write(&mut w, idx).unwrap(); + let (len, _) = hcod7_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod7_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod7_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod7_write(&mut w, 64), + Err(Error::SpectralCodebookIndexOutOfRange(7)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebook 7 is the first unsigned **pair** book. + // It shares Codebook 3's "zero-tuple at index 0 with the shortest + // codeword" placement (both are unsigned books with the §4.6.3.3 + // polynomial origin at index 0) but at dim=2 vs dim=4, and with + // a 12-bit ceiling vs Codebook 3's 16-bit ceiling. + // ------------------------------------------------------------------- + + #[test] + fn codebook_3_and_7_both_park_zero_tuple_at_index_0() { + // Both unsigned books map the §4.6.3.3 origin to index 0 and + // hand it the shortest available codeword (length 1, value 0). + let (l3, cw3) = hcod3_encode(0).unwrap(); + let (l7, cw7) = hcod7_encode(0).unwrap(); + assert_eq!((l3, cw3), (1, 0)); + assert_eq!((l7, cw7), (1, 0)); + } + + #[test] + fn codebook_7_entry_count_is_64_vs_81_for_dim4_books() { + // Dim-4 unsigned (HCB3/HCB4 with LAV=2): (2+1)^4 = 81. + // Dim-2 unsigned (HCB7 with LAV=7): (7+1)^2 = 64. The + // dim-2 → dim-4 split affects the §4.6.3.3 universe size. + assert_eq!(HCOD3.len(), 81); + assert_eq!(HCOD4.len(), 81); + assert_eq!(HCOD7.len(), 64); + } + + // ------------------------------------------------------------------- + // Codebook 8 (Table 4.A.9): unsigned pair, dim=2, LAV=7, + // 64 entries indexed 0..=63 (8^2 lattice). The §4.6.3.3 zero-tuple + // `(0, 0)` at index 0 carries a 5-bit `0b01110` (not the shortest); + // the shortest 3-bit codeword `0` parks at index 9 (= (1, 1)). + // Maximum codeword length 10 bits. Complete prefix code: Kraft + // sum = 1024 = 2^10. + // ------------------------------------------------------------------- + + #[test] + fn hcod8_has_exactly_64_entries() { + // 8^2 = 64 (unsigned LAV=7 → mod = 7+1 = 8, dim = 2). Shares + // the universe size with Codebook 7. + assert_eq!(HCOD8.len(), HCOD8_NUM_ENTRIES); + assert_eq!(HCOD8_NUM_ENTRIES, 64); + } + + #[test] + fn hcod8_max_length_is_10_bits() { + let max = HCOD8.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD8_MAX_LEN); + assert_eq!(HCOD8_MAX_LEN, 10); + } + + #[test] + fn hcod8_min_length_is_three_bits_at_index_9() { + // The shortest codeword in Codebook 8 is 3 bits, parked at + // index 9 (the §4.6.3.3 interior tuple `(y, z) = (1, 1)` for + // an unsigned pair book with LAV=7) with the codeword `0`. + // Index 9 is the only 3-bit entry; every other index has + // length >= 4. + let (len_9, cw_9) = (HCOD8[9].0, HCOD8[9].1); + assert_eq!(len_9, 3, "index 9 must be 3-bit"); + assert_eq!(cw_9, 0, "index 9 codeword must be `0`"); + let three_bit_entries: usize = HCOD8.iter().filter(|&&(len, _)| len == 3).count(); + assert_eq!(three_bit_entries, 1, "exactly one 3-bit codeword"); + for (idx, &(len, _)) in HCOD8.iter().enumerate() { + if idx == 9 { + continue; + } + assert!( + len >= 4, + "every index != 9 must have length >= 4; idx={} len={}", + idx, + len + ); + } + } + + #[test] + fn hcod8_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD8.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod8_kraft_sum_is_two_to_the_ten() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD8_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD8 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 1024); + } + + #[test] + fn hcod8_is_complete() { + // Walk every 10-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod8_encode`. + for prefix in 0u32..(1u32 << HCOD8_MAX_LEN) { + // Pack `prefix` (10 bits) left-aligned into two bytes: + // high byte = bits 9..2, low byte = (bits 1..0) << 6. + let bytes = [(prefix >> 2) as u8, ((prefix & 0x3) << 6) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod8_decode(&mut br).expect("10-bit prefix must decode"); + let (len, cw) = hcod8_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` bits + // of `prefix`. + let lead = prefix >> (HCOD8_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod8_encode_index_0_is_5_bit_zero_tuple_codeword() { + // Spec PDF Table 4.A.9 row 0: length 5, codeword 0xe — the + // §4.6.3.3 zero-tuple `(0, 0)` lifted off the shortest slot. + let (len, cw) = hcod8_encode(0).unwrap(); + assert_eq!(len, 5); + assert_eq!(cw, 0xe); + } + + #[test] + fn hcod8_encode_index_9_is_3_bit_zero_codeword() { + // Spec PDF Table 4.A.9 row 9: length 3, codeword 0 — the + // shortest codeword, parked on `(1, 1)` (= y * 8 + z = 9). + let (len, cw) = hcod8_encode(9).unwrap(); + assert_eq!(len, 3); + assert_eq!(cw, 0); + } + + #[test] + fn hcod8_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.9 row 63: length 10, codeword 0x3ff — + // the far corner `(7, 7)` of the unsigned `8 × 8` pair lattice. + let (len, cw) = hcod8_encode(63).unwrap(); + assert_eq!(len, 10); + assert_eq!(cw, 0x3ff); + } + + #[test] + fn hcod8_encode_four_10_bit_rows_match_table() { + // Exactly four rows reach the 10-bit ceiling in Table 4.A.9: + // indices 7, 47, 56, 63 with codewords 3fe, 3fc, 3fd, 3ff. + let expected = [ + (7u32, 0x3feu16), + (47u32, 0x3fcu16), + (56u32, 0x3fdu16), + (63u32, 0x3ffu16), + ]; + let observed: Vec<_> = HCOD8 + .iter() + .enumerate() + .filter_map(|(i, &(l, cw))| if l == 10 { Some((i as u32, cw)) } else { None }) + .collect(); + assert_eq!(observed.len(), 4); + for (e, o) in expected.iter().zip(observed.iter()) { + assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); + } + } + + #[test] + fn hcod8_encode_index_8_is_first_y1_row() { + // Index 8 = (y, z) = (1, 0) via `y * 8 + z`. Table 4.A.9 row + // 8: length 4, codeword 0x3. + let (len, cw) = hcod8_encode(8).unwrap(); + assert_eq!(len, 4); + assert_eq!(cw, 0x3); + } + + #[test] + fn hcod8_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod8_encode(64), + Err(Error::SpectralCodebookIndexOutOfRange(8)) + )); + assert!(matches!( + hcod8_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(8)) + )); + } + + #[test] + fn hcod8_decode_three_zero_bits_yields_index_9() { + // Leading `0b000` → idx 9 (the interior tuple `(1, 1)`). + // Remaining 5 bits of the byte untouched. + let bytes = [0b0001_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod8_decode(&mut br).unwrap(); + assert_eq!(idx, 9); + assert_eq!(br.bit_position(), 3); + } + + #[test] + fn hcod8_decode_full_10_bit_codeword_round_trips_index_63() { + // Index 63 → length 10, codeword 0x3ff = 0b1111_1111_11. + // Pack left-aligned into 2 bytes: 0xff, 0xc0. + let bytes = [0xff, 0xc0]; + let mut br = BitReader::new(&bytes); + let idx = hcod8_decode(&mut br).unwrap(); + assert_eq!(idx, 63); + assert_eq!(br.bit_position(), 10); + } + + #[test] + fn hcod8_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod8_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod8_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD8_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod8_write(&mut w, idx).unwrap(); + let (len, _) = hcod8_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod8_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod8_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod8_write(&mut w, 64), + Err(Error::SpectralCodebookIndexOutOfRange(8)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebooks 7 and 8 share the unsigned pair tuple + // universe (Table 4.95 rows 7 and 8 are identical except for the + // `Codebook listed in Table` column) but assign different codewords + // for the same `(y, z)` tuple. Where Codebook 7 pins the zero-tuple + // to the 1-bit slot, Codebook 8 lifts it to a 5-bit codeword and + // hands the 3-bit shortest-codeword slot to the `(1, 1)` interior. + // ------------------------------------------------------------------- + + #[test] + fn codebook_7_and_8_share_universe_size_but_disagree_on_shortest_slot() { + assert_eq!(HCOD7_NUM_ENTRIES, HCOD8_NUM_ENTRIES); + assert_eq!(HCOD7_NUM_ENTRIES, 64); + // Codebook 7: zero-tuple at index 0 takes the 1-bit slot. + let (l7_0, _) = hcod7_encode(0).unwrap(); + assert_eq!(l7_0, 1); + // Codebook 8: zero-tuple at index 0 takes 5 bits; the 3-bit + // shortest slot lives on the (1, 1) interior at index 9. + let (l8_0, _) = hcod8_encode(0).unwrap(); + let (l8_9, cw8_9) = hcod8_encode(9).unwrap(); + assert_eq!(l8_0, 5); + assert_eq!((l8_9, cw8_9), (3, 0)); + } + + #[test] + fn codebook_8_far_corner_matches_codebook_7_far_corner_index() { + // Both unsigned dim-2 LAV-7 books park `(7, 7)` at index 63 + // (the §4.6.3.3 unsigned polynomial puts the far corner at + // the highest index). Only the codeword length / value + // differs: Codebook 7 → 12-bit 0xfff; Codebook 8 → 10-bit 0x3ff. + let (l7, cw7) = hcod7_encode(63).unwrap(); + let (l8, cw8) = hcod8_encode(63).unwrap(); + assert_eq!((l7, cw7), (12, 0xfff)); + assert_eq!((l8, cw8), (10, 0x3ff)); + } + + // ------------------------------------------------------------------- + // Codebook 9 — Table 4.A.10 + // ------------------------------------------------------------------- + + #[test] + fn hcod9_has_exactly_169_entries() { + // 13^2 = 169 (unsigned LAV=12 → mod = lav+1 = 13, dim = 2). + assert_eq!(HCOD9.len(), HCOD9_NUM_ENTRIES); + assert_eq!(HCOD9_NUM_ENTRIES, 169); + } + + #[test] + fn hcod9_max_length_is_15_bits() { + let max = HCOD9.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD9_MAX_LEN); + assert_eq!(HCOD9_MAX_LEN, 15); + } + + #[test] + fn hcod9_min_length_is_one_bit_at_index_0() { + // Unsigned books put the all-zero magnitude pair tuple at + // index 0; Codebook 9 carries it as the single bit `0`. + // Every other index has length >= 3. + for (idx, &(len, cw)) in HCOD9.iter().enumerate() { + if idx == 0 { + assert_eq!(len, 1, "index 0 must be 1-bit"); + assert_eq!(cw, 0, "index 0 codeword must be `0`"); + } else { + assert!( + len >= 3, + "every non-zero index must have length >= 3; idx={} len={}", + idx, + len + ); + } + } + } + + #[test] + fn hcod9_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD9.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod9_kraft_sum_is_two_to_the_fifteen() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD9_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD9 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 32768); + } + + #[test] + fn hcod9_is_complete() { + // Walk every 15-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod9_encode`. + for prefix in 0u32..(1u32 << HCOD9_MAX_LEN) { + // Pack `prefix` (15 bits) left-aligned into two bytes: + // high byte = bits 14..7, low byte = (bits 6..0) << 1. + let bytes = [(prefix >> 7) as u8, ((prefix & 0x7f) << 1) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod9_decode(&mut br).expect("15-bit prefix must decode"); + let (len, cw) = hcod9_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` + // bits of `prefix`. + let lead = prefix >> (HCOD9_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#06x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod9_encode_index_0_is_one_bit_zero_codeword() { + // Spec PDF Table 4.A.10 row 0: length 1, codeword 0 — the + // §4.6.3.3 zero-tuple `(0, 0)` carries the shortest possible + // codeword. + let (len, cw) = hcod9_encode(0).unwrap(); + assert_eq!(len, 1); + assert_eq!(cw, 0); + } + + #[test] + fn hcod9_encode_first_few_rows_match_spec() { + // Spec PDF Table 4.A.10 spot checks: indices 1, 13, 14. + // Row 1: length 3, codeword 0x5; row 13: length 3, codeword + // 0x4 (the only other 3-bit row); row 14: length 4, + // codeword 0xc (interior `(y, z) = (1, 1)` since `idx = + // 1 * 13 + 1 = 14`). + assert_eq!(hcod9_encode(1).unwrap(), (3, 0x5)); + assert_eq!(hcod9_encode(13).unwrap(), (3, 0x4)); + assert_eq!(hcod9_encode(14).unwrap(), (4, 0xc)); + } + + #[test] + fn hcod9_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.10 row 168: length 15, codeword 0x7fff + // — the far corner `(12, 12)` of the unsigned `13 × 13` + // pair lattice. + let (len, cw) = hcod9_encode(168).unwrap(); + assert_eq!(len, 15); + assert_eq!(cw, 0x7fff); + } + + #[test] + fn hcod9_encode_four_15_bit_rows_match_table() { + // Exactly four rows reach the 15-bit ceiling in Table 4.A.10: + // indices 142, 154, 155, 168 with codewords 7ffc, 7ffd, + // 7ffe, 7fff. + let expected = [ + (142u32, 0x7ffcu16), + (154u32, 0x7ffdu16), + (155u32, 0x7ffeu16), + (168u32, 0x7fffu16), + ]; + let observed: Vec<_> = HCOD9 + .iter() + .enumerate() + .filter_map(|(i, &(l, cw))| if l == 15 { Some((i as u32, cw)) } else { None }) + .collect(); + assert_eq!(observed.len(), 4); + for (e, o) in expected.iter().zip(observed.iter()) { + assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); + } + } + + #[test] + fn hcod9_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod9_encode(169), + Err(Error::SpectralCodebookIndexOutOfRange(9)) + )); + assert!(matches!( + hcod9_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(9)) + )); + } + + #[test] + fn hcod9_decode_single_zero_bit_yields_index_0() { + // Leading `0` → idx 0 (the zero-tuple). + // Remaining 7 bits of the byte untouched. + let bytes = [0b0111_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod9_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + assert_eq!(br.bit_position(), 1); + } + + #[test] + fn hcod9_decode_full_15_bit_codeword_round_trips_index_168() { + // Index 168 → length 15, codeword 0x7fff = 0b111_1111_1111_1111. + // Pack left-aligned into 2 bytes: 0xff, 0xfe. + let bytes = [0xff, 0xfe]; + let mut br = BitReader::new(&bytes); + let idx = hcod9_decode(&mut br).unwrap(); + assert_eq!(idx, 168); + assert_eq!(br.bit_position(), 15); + } + + #[test] + fn hcod9_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod9_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod9_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD9_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod9_write(&mut w, idx).unwrap(); + let (len, _) = hcod9_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod9_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod9_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod9_write(&mut w, 169), + Err(Error::SpectralCodebookIndexOutOfRange(9)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebook 9 expands the unsigned pair universe. + // Codebooks 7 and 8 share the `8 × 8 = 64`-entry `LAV = 7` + // lattice; Codebook 9 widens the per-coefficient ceiling to + // `LAV = 12`, producing the `13 × 13 = 169`-entry lattice and + // lifting the codeword ceiling from 10 (HCOD8) to 15 bits. + // ------------------------------------------------------------------- + + #[test] + fn codebook_9_universe_size_grows_to_169_from_codebook_8_64() { + assert_eq!(HCOD7_NUM_ENTRIES, 64); + assert_eq!(HCOD8_NUM_ENTRIES, 64); + assert_eq!(HCOD9_NUM_ENTRIES, 169); + // 169 / 64 ≈ 2.64 — the §4.6.3.3 universe more than doubles. + const { assert!(HCOD9_NUM_ENTRIES > 2 * HCOD8_NUM_ENTRIES) }; + } + + #[test] + fn codebook_9_zero_tuple_shares_codebook_7_head_placement() { + // Both Codebook 7 and Codebook 9 are unsigned pair books + // that park the §4.6.3.3 zero-tuple `(0, 0)` at index 0 with + // the single-bit `0` codeword — the shortest-possible slot. + // Codebook 8 lifts the zero-tuple off the 1-bit slot (it + // becomes 5 bits at index 0 and the 3-bit shortest moves to + // the (1, 1) interior at index 9). + let (l7_0, cw7_0) = hcod7_encode(0).unwrap(); + let (l9_0, cw9_0) = hcod9_encode(0).unwrap(); + assert_eq!((l7_0, cw7_0), (1, 0)); + assert_eq!((l9_0, cw9_0), (1, 0)); + } + + #[test] + fn codebook_9_far_corner_index_matches_lav_12_polynomial() { + // The §4.6.3.3 unsigned polynomial puts the max pair tuple + // `(LAV, LAV)` at index `LAV * (LAV + 1) + LAV`. For + // Codebook 9 with `LAV = 12` that's `12 * 13 + 12 = 168`, + // which carries the 15-bit codeword `0x7fff` — the widest + // codeword in any non-ESC spectrum book. + let (l9, cw9) = hcod9_encode(168).unwrap(); + assert_eq!((l9, cw9), (15, 0x7fff)); + // Compare to Codebook 8's far corner (LAV = 7) at index + // 63: that's only 10 bits wide. + let (l8, cw8) = hcod8_encode(63).unwrap(); + assert_eq!((l8, cw8), (10, 0x3ff)); + // Codebook 9's ceiling is 5 bits wider than Codebook 8's. + assert_eq!(HCOD9_MAX_LEN - HCOD8_MAX_LEN, 5); + } + + // ------------------------------------------------------------------- + // Codebook 10 — Table 4.A.11 + // ------------------------------------------------------------------- + + #[test] + fn hcod10_has_exactly_169_entries() { + // 13^2 = 169 (unsigned LAV=12 → mod = lav+1 = 13, dim = 2). + assert_eq!(HCOD10.len(), HCOD10_NUM_ENTRIES); + assert_eq!(HCOD10_NUM_ENTRIES, 169); + } + + #[test] + fn hcod10_max_length_is_12_bits() { + let max = HCOD10.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD10_MAX_LEN); + assert_eq!(HCOD10_MAX_LEN, 12); + } + + #[test] + fn hcod10_min_length_is_four_bits_at_interior_tuple_index_14() { + // Codebook 10 lifts the zero-tuple off the shortest slot (it + // sits at 6 bits at index 0) and parks the 4-bit shortest + // codeword on the interior `(1, 1)` tuple at index 14, the + // same head-displacement pattern Codebook 8 uses. + // Exactly three rows reach 4 bits: indices 14, 15, 27. + let mut four_bit_indices = Vec::new(); + for (idx, &(len, _)) in HCOD10.iter().enumerate() { + if len == 4 { + four_bit_indices.push(idx); + } + assert!( + len >= 4, + "every row must have length >= 4; idx={} len={}", + idx, + len + ); + } + assert_eq!(four_bit_indices, vec![14, 15, 27]); + } + + #[test] + fn hcod10_zero_tuple_lives_at_index_0_with_six_bit_codeword() { + // Codebook 10 places the §4.6.3.3 zero-tuple `(0, 0)` at + // index 0 via the unsigned polynomial idx = 0 * 13 + 0 = 0, + // but the Huffman row carries a 6-bit `0b100010` (`0x22`) + // codeword — not the 1-bit `0` that Codebook 9 uses. + let (len, cw) = HCOD10[0]; + assert_eq!(len, 6); + assert_eq!(cw, 0x22); + } + + #[test] + fn hcod10_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD10.iter().enumerate() { + let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; + assert!( + u32::from(cw) <= max, + "idx={}: codeword {:#x} does not fit {} bits", + idx, + cw, + len + ); + } + } + + #[test] + fn hcod10_kraft_sum_is_two_to_the_twelve() { + // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. + let lmax = HCOD10_MAX_LEN; + let mut sum: u64 = 0; + for &(len, _) in &HCOD10 { + sum += 1u64 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); + assert_eq!(sum, 4096); + } + + #[test] + fn hcod10_is_complete() { + // Walk every 12-bit prefix, decode it via the production + // decoder, and confirm every prefix yields exactly one entry. + // Bonus: confirm the decoded index round-trips back to the + // same codeword via `hcod10_encode`. + for prefix in 0u32..(1u32 << HCOD10_MAX_LEN) { + // Pack `prefix` (12 bits) left-aligned into two bytes: + // high byte = bits 11..4, low byte = (bits 3..0) << 4. + let bytes = [(prefix >> 4) as u8, ((prefix & 0xf) << 4) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod10_decode(&mut br).expect("12-bit prefix must decode"); + let (len, cw) = hcod10_encode(idx).expect("decoded index must round-trip"); + // The decoded codeword should match the leading `len` + // bits of `prefix`. + let lead = prefix >> (HCOD10_MAX_LEN - u32::from(len)); + assert_eq!( + u32::from(cw), + lead, + "round-trip prefix={:#05x} idx={} len={} cw={:#x}", + prefix, + idx, + len, + cw + ); + } + } + + #[test] + fn hcod10_encode_index_0_is_six_bit_codeword_0x22() { + // Spec PDF Table 4.A.11 row 0: length 6, codeword 0x22 — the + // §4.6.3.3 zero-tuple `(0, 0)` does NOT carry the shortest + // possible codeword in Codebook 10. + let (len, cw) = hcod10_encode(0).unwrap(); + assert_eq!(len, 6); + assert_eq!(cw, 0x22); + } + + #[test] + fn hcod10_encode_shortest_codewords_match_spec() { + // Spec PDF Table 4.A.11 spot checks: the three 4-bit rows are + // indices 14, 15, 27 with codewords 0, 1, 2. + assert_eq!(hcod10_encode(14).unwrap(), (4, 0x0)); + assert_eq!(hcod10_encode(15).unwrap(), (4, 0x1)); + assert_eq!(hcod10_encode(27).unwrap(), (4, 0x2)); + } + + #[test] + fn hcod10_encode_last_entry_matches_table() { + // Spec PDF Table 4.A.11 row 168: length 12, codeword 0xfff — + // the far corner `(12, 12)` of the unsigned `13 × 13` pair + // lattice. + let (len, cw) = hcod10_encode(168).unwrap(); + assert_eq!(len, 12); + assert_eq!(cw, 0xfff); + } + + #[test] + fn hcod10_encode_eight_12_bit_rows_match_table() { + // Exactly eight rows reach the 12-bit ceiling in + // Table 4.A.11. Their indices and codewords are pinned here. + let expected = [ + (12u32, 0x0ffdu16), + (129u32, 0x0ffau16), + (142u32, 0x0ff9u16), + (155u32, 0x0ffbu16), + (165u32, 0x0ff8u16), + (166u32, 0x0ffeu16), + (167u32, 0x0ffcu16), + (168u32, 0x0fffu16), + ]; + let observed: Vec<_> = HCOD10 + .iter() + .enumerate() + .filter_map(|(i, &(l, cw))| if l == 12 { Some((i as u32, cw)) } else { None }) + .collect(); + assert_eq!(observed.len(), 8); + for (e, o) in expected.iter().zip(observed.iter()) { + assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); + } + } + + #[test] + fn hcod10_encode_rejects_out_of_range_index() { + assert!(matches!( + hcod10_encode(169), + Err(Error::SpectralCodebookIndexOutOfRange(10)) + )); + assert!(matches!( + hcod10_encode(0xffff_ffff), + Err(Error::SpectralCodebookIndexOutOfRange(10)) + )); + } + + #[test] + fn hcod10_decode_four_bit_zero_codeword_yields_index_14() { + // Index 14 → length 4, codeword 0 = 0b0000. Pack + // left-aligned in a single byte: top 4 bits = 0, bottom 4 + // bits arbitrary. + let bytes = [0b0000_1111u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod10_decode(&mut br).unwrap(); + assert_eq!(idx, 14); + assert_eq!(br.bit_position(), 4); + } + + #[test] + fn hcod10_decode_full_12_bit_codeword_round_trips_index_168() { + // Index 168 → length 12, codeword 0xfff = 0b1111_1111_1111. + // Pack left-aligned: high byte = 0xff (bits 11..4), low byte + // = (0xf << 4) = 0xf0 (bits 3..0 in the top of the low byte). + let bytes = [0xff, 0xf0]; + let mut br = BitReader::new(&bytes); + let idx = hcod10_decode(&mut br).unwrap(); + assert_eq!(idx, 168); + assert_eq!(br.bit_position(), 12); + } + + #[test] + fn hcod10_decode_propagates_unexpected_end() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + assert_eq!(hcod10_decode(&mut br), Err(Error::UnexpectedEnd)); + } + + #[test] + fn hcod10_write_then_decode_round_trips_every_index() { + for idx in 0..HCOD10_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod10_write(&mut w, idx).unwrap(); + let (len, _) = hcod10_encode(idx).unwrap(); + let mut w2 = w; + let pad = (8 - (u32::from(len) % 8)) % 8; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let decoded = hcod10_decode(&mut br).unwrap(); + assert_eq!( + decoded, idx, + "round-trip mismatch at idx={} (encoded as {} bits)", + idx, len + ); + } + } + + #[test] + fn hcod10_write_rejects_out_of_range_index() { + let mut w = BitWriter::new(); + assert!(matches!( + hcod10_write(&mut w, 169), + Err(Error::SpectralCodebookIndexOutOfRange(10)) + )); + } + + // ------------------------------------------------------------------- + // Cross-check: Codebooks 9 and 10 share the unsigned dim-2 LAV-12 + // universe (169 entries each) but differ in codeword distribution. + // Codebook 9's ceiling is 15 bits with the zero-tuple at the + // 1-bit head; Codebook 10's ceiling pulls down to 12 bits and + // lifts the zero-tuple off the head — the shortest 4-bit slot + // sits on the interior `(1, 1)` tuple at index 14. + // ------------------------------------------------------------------- + + #[test] + fn codebook_10_matches_codebook_9_universe_size() { + assert_eq!(HCOD9_NUM_ENTRIES, 169); + assert_eq!(HCOD10_NUM_ENTRIES, 169); + } + + #[test] + fn codebook_10_ceiling_is_3_bits_below_codebook_9() { + // Codebook 9's ceiling is 15 bits; Codebook 10's ceiling + // is 12 bits — a 3-bit pull-down reflecting the flatter + // codeword distribution targeted by Codebook 10's + // encoder-statistics tuning. + assert_eq!(HCOD9_MAX_LEN, 15); + assert_eq!(HCOD10_MAX_LEN, 12); + assert_eq!(HCOD9_MAX_LEN - HCOD10_MAX_LEN, 3); + } + + #[test] + fn codebook_10_lifts_zero_tuple_off_codebook_9_head_placement() { + // Codebook 9 parks the §4.6.3.3 zero-tuple at index 0 with + // the 1-bit `0` codeword (shortest possible slot). Codebook + // 10 keeps the zero-tuple at index 0 (the §4.6.3.3 polynomial + // index is fixed by the tuple, not the codebook) but the + // codeword swells to 6 bits — the shortest 4-bit slot + // migrates onto the interior `(1, 1)` tuple at index 14. + let (l9_0, cw9_0) = hcod9_encode(0).unwrap(); + let (l10_0, cw10_0) = hcod10_encode(0).unwrap(); + let (l10_14, cw10_14) = hcod10_encode(14).unwrap(); + assert_eq!((l9_0, cw9_0), (1, 0)); + assert_eq!((l10_0, cw10_0), (6, 0x22)); + assert_eq!((l10_14, cw10_14), (4, 0)); + } + + #[test] + fn codebook_10_far_corner_matches_codebook_9_far_corner_index() { + // Both codebooks share LAV = 12, so the §4.6.3.3 unsigned + // polynomial parks `(12, 12)` at index 12 * 13 + 12 = 168. + // Codeword shapes differ: Codebook 9 → 15-bit 0x7fff; + // Codebook 10 → 12-bit 0xfff. + let (l9, cw9) = hcod9_encode(168).unwrap(); + let (l10, cw10) = hcod10_encode(168).unwrap(); + assert_eq!((l9, cw9), (15, 0x7fff)); + assert_eq!((l10, cw10), (12, 0xfff)); + } + + // ------------------------------------------------------------------- + // Codebook 11 invariants (Table 4.A.12) + // ------------------------------------------------------------------- + + #[test] + fn hcod11_has_exactly_289_entries() { + // 17^2 = 289 (unsigned LAV=16 → mod = 17, dim = 2). + assert_eq!(HCOD11.len(), HCOD11_NUM_ENTRIES); + assert_eq!(HCOD11_NUM_ENTRIES, 289); + } + + #[test] + fn hcod11_max_length_is_12_bits() { + let max = HCOD11.iter().map(|&(len, _)| len).max().unwrap(); + assert_eq!(u32::from(max), HCOD11_MAX_LEN); + assert_eq!(HCOD11_MAX_LEN, 12); + } + + #[test] + fn hcod11_min_length_is_four_bits_at_zero_tuple_and_interior_pair() { + // The 4-bit floor is shared by exactly two rows: index 0 + // (the zero-tuple (0, 0)) and index 18 (the interior (1, 1) + // pair, since 1 * 17 + 1 = 18). The zero-tuple carries + // 0b0000 and (1, 1) carries 0b0001. + let mut min: u32 = u32::MAX; + let mut min_indices: Vec = Vec::new(); + for (idx, &(len, _)) in HCOD11.iter().enumerate() { + let l = u32::from(len); + if l < min { + min = l; + min_indices.clear(); + min_indices.push(idx); + } else if l == min { + min_indices.push(idx); + } + } + assert_eq!(min, 4); + assert_eq!(min_indices, vec![0, 18]); + } + + #[test] + fn hcod11_zero_tuple_lives_at_index_0_with_four_bit_codeword() { + // The §4.6.3.3 unsigned polynomial idx = y * 17 + z places + // the zero-tuple (0, 0) at index 0; Codebook 11 hands it + // the shortest 4-bit codeword 0b0000. + let (len, cw) = HCOD11[0]; + assert_eq!(len, 4); + assert_eq!(cw, 0x0000); + } + + #[test] + fn hcod11_interior_one_one_tuple_lives_at_index_18_with_four_bit_codeword() { + // 1 * 17 + 1 = 18 → the second 4-bit slot, codeword 0b0001. + let (len, cw) = HCOD11[18]; + assert_eq!(len, 4); + assert_eq!(cw, 0x0001); + } + + #[test] + fn hcod11_far_corner_lives_at_index_288_with_five_bit_codeword() { + // (16, 16) at 16 * 17 + 16 = 288 — both coefficients flagged + // as ESC. Codebook 11 spends only 5 bits on this far corner + // (codeword 0b00100), keeping the in-band codeword short + // because the wire layout extends with two escape sequences + // and (where the magnitudes are non-zero) two sign bits. + let (len, cw) = HCOD11[288]; + assert_eq!(len, 5); + assert_eq!(cw, 0x0004); + } + + #[test] + fn hcod11_codewords_fit_their_declared_length() { + for (idx, &(len, cw)) in HCOD11.iter().enumerate() { + assert!( + u32::from(cw) < (1u32 << u32::from(len)), + "row {idx}: codeword 0x{cw:x} >= 2^{len}", + ); + } + } + + #[test] + fn hcod11_kraft_sum_is_two_to_the_twelve() { + // Σ 2^(L_max - L) = 2^L_max ⇔ complete prefix code. + let lmax = HCOD11_MAX_LEN; + let mut sum: u32 = 0; + for &(len, _) in &HCOD11 { + sum += 1u32 << (lmax - u32::from(len)); + } + assert_eq!(sum, 1u32 << lmax); + assert_eq!(sum, 4096); + } + + #[test] + fn hcod11_is_complete() { + // Exhaustively walk every 12-bit prefix and verify each + // matches exactly one entry. This is the strongest + // possible check that the table is a complete prefix code + // and that `hcod11_decode`'s `unreachable!()` is dead. + for prefix in 0u32..(1u32 << HCOD11_MAX_LEN) { + let bytes = [((prefix >> 4) & 0xff) as u8, ((prefix & 0xf) << 4) as u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod11_decode(&mut br).expect("12-bit prefix must decode"); + let (len, cw) = hcod11_encode(idx).expect("decoded index must round-trip"); + // The decoded prefix must match the leading `len` bits + // of our 12-bit walk. + let lead = prefix >> (HCOD11_MAX_LEN - u32::from(len)); + assert_eq!( + lead, + u32::from(cw), + "prefix 0b{prefix:012b} decoded idx={idx} → codeword ({len}, 0x{cw:x})", + ); + } + } + + #[test] + fn hcod11_twelve_bit_ceiling_hits_exactly_six_indices() { + // Indices 12, 14, 15, 255, 269, 270 are the only rows whose + // codeword length reaches the 12-bit ceiling. + let ceiling: Vec = HCOD11 + .iter() + .enumerate() + .filter_map(|(i, &(len, _))| if len == 12 { Some(i) } else { None }) + .collect(); + assert_eq!(ceiling, vec![12, 14, 15, 255, 269, 270]); + assert_eq!(HCOD11[12], (12, 0x0ffb)); + assert_eq!(HCOD11[14], (12, 0x0ffa)); + assert_eq!(HCOD11[15], (12, 0x0ffe)); + assert_eq!(HCOD11[255], (12, 0x0ffd)); + assert_eq!(HCOD11[269], (12, 0x0ffc)); + assert_eq!(HCOD11[270], (12, 0x0fff)); + } + + #[test] + fn hcod11_half_esc_rows_match_spec() { + // Index 16 corresponds to (y, z) = (0, 16), index 272 to + // (16, 0). Both are half-ESC tuples — exactly one + // coefficient at the §4.6.3.3 escape flag. + assert_eq!(HCOD11[16], (10, 0x038e)); + assert_eq!(HCOD11[272], (9, 0x01c2)); + } + + #[test] + fn hcod11_encode_rejects_out_of_range_indices() { + for bad in [289u32, 290, 300, 1000, u32::MAX] { + assert!(matches!( + hcod11_encode(bad), + Err(Error::SpectralCodebookIndexOutOfRange(11)) + )); + } + } + + #[test] + fn hcod11_write_rejects_out_of_range_indices() { + let mut w = BitWriter::new(); + for bad in [289u32, 1000, u32::MAX] { + assert!(matches!( + hcod11_write(&mut w, bad), + Err(Error::SpectralCodebookIndexOutOfRange(11)) + )); + } + } + + #[test] + fn hcod11_decode_index_0_zero_bits() { + // Index 0 → 4-bit `0`. Padding to a byte boundary with zeros + // keeps the wire byte at 0x00. + let bytes = [0x00u8]; + let mut br = BitReader::new(&bytes); + let idx = hcod11_decode(&mut br).unwrap(); + assert_eq!(idx, 0); + assert_eq!(br.bit_position(), 4u64); + } + + #[test] + fn hcod11_decode_index_270_full_12_bit_far_codeword() { + // Index 270 → 12-bit 0xfff packed left-aligned: high byte = + // 0xff (bits 11..4), low byte = (0xf << 4) = 0xf0 (bits + // 3..0 in the high nibble of the low byte). + let bytes = [0xffu8, 0xf0]; + let mut br = BitReader::new(&bytes); + let idx = hcod11_decode(&mut br).unwrap(); + assert_eq!(idx, 270); + assert_eq!(br.bit_position(), 12u64); + } + + #[test] + fn hcod11_writer_round_trip_pins_every_index() { + // Writer → reader round-trip for every legal index. Each + // index must produce the exact bit-stream the encode + // function claims, and decode must recover the original + // index using exactly `len` bits. + for idx in 0..HCOD11_NUM_ENTRIES as u32 { + let mut w = BitWriter::new(); + hcod11_write(&mut w, idx).unwrap(); + let (len, _) = hcod11_encode(idx).unwrap(); + let pad = (8 - (u32::from(len) % 8)) % 8; + let mut w2 = w; + if pad > 0 { + w2.write_u32(0, pad); + } + let bytes = w2.into_bytes(); + let mut br = BitReader::new(&bytes); + let got = hcod11_decode(&mut br).unwrap(); + assert_eq!(got, idx, "round-trip mismatch at idx={idx}"); + assert_eq!( + br.bit_position(), + u64::from(len), + "bit consumption mismatch at idx={idx}", + ); + } + } + + #[test] + fn hcod11_decoder_returns_unexpected_end_on_truncation() { + let bytes: [u8; 0] = []; + let mut br = BitReader::new(&bytes); + let err = hcod11_decode(&mut br).unwrap_err(); + assert_eq!(err, Error::UnexpectedEnd); + } + + #[test] + fn hcod11_max_len_constant_matches_table_data() { + let mut observed_max = 0u32; + for idx in 0..HCOD11_NUM_ENTRIES as u32 { + let (len, _) = hcod11_encode(idx).unwrap(); + observed_max = observed_max.max(u32::from(len)); + } + assert_eq!(observed_max, HCOD11_MAX_LEN); + } + + #[test] + fn hcod11_ceiling_matches_codebook_10_ceiling() { + // Codebook 10 caps at 12 bits; Codebook 11 also caps at 12 + // bits — the universe widens (169 → 289 entries) but the + // codeword ceiling stays the same because the ESC sequence + // soaks up the tail-distribution rather than spending + // longer Huffman codewords on it. + assert_eq!(HCOD10_MAX_LEN, HCOD11_MAX_LEN); + assert_eq!(HCOD11_MAX_LEN, 12); + } + + #[test] + fn hcod11_universe_is_69_entries_wider_than_codebook_10() { + // 289 - 169 = 120 extra rows = (17 + 17 - 1) extra entries + // along the ESC border `y == 16 || z == 16`. + assert_eq!(HCOD11_NUM_ENTRIES - HCOD10_NUM_ENTRIES, 120); + assert_eq!(HCOD11_NUM_ENTRIES, 289); + assert_eq!(HCOD10_NUM_ENTRIES, 169); + } +} diff --git a/crates/vendor/oxideav-aac/src/ssr.rs b/crates/vendor/oxideav-aac/src/ssr.rs new file mode 100644 index 00000000..ea0d30e8 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ssr.rs @@ -0,0 +1,549 @@ +//! SSR per-channel gain-control + IPQF back-end driver (ISO/IEC +//! 14496-3 §4.6.12). +//! +//! [`SsrGainControl`] composes the four-band gain-control state +//! ([`crate::gain_control::GainBandState`]) and the IPQF synthesizer +//! ([`crate::ipqf::Ipqf`]) into one persistent per-channel pipeline. +//! Per frame it consumes the four per-band IMDCT outputs `U_{W,B}` plus +//! the `gain_control_data()` side info and returns the reconstructed +//! PCM time signal `AS(n)`: +//! +//! ```text +//! for each PQF band B in 0..4: +//! V_B = GainBandState[B].window_overlap(ladder[B], U_B, seq) §4.6.12.3.3 +//! AS = IPQF.synthesize([V_0, V_1, V_2, V_3]) §4.6.12.3.4 +//! ``` +//! +//! ## Front half +//! +//! [`SsrGainControl`] runs the §4.6.12.3.3–4 *back half* of the SSR +//! tool: the per-band gain windowing/overlap and the IPQF synthesis, +//! from caller-supplied non-overlapped `U_{W,B}` columns. The +//! §4.6.12.1 *front half* — splitting the transmitted spectrum into +//! the four PQF-band coefficient columns, the even-band spectral +//! reversal, and the per-band 256-line (long) / 32-line (short) +//! IMDCTs + windows — lives in [`crate::ssr_filterbank`]; +//! [`SsrChannelDecoder`] chains the two into the complete +//! spectrum → PCM pipeline. +//! +//! ## Provenance +//! +//! Composes the §4.6.12.1 front half ([`crate::ssr_filterbank`]) and +//! the §4.6.12.3.1–4 stages implemented in [`crate::gain_control`] and +//! [`crate::ipqf`]; no new tables. No external SSR implementation was +//! consulted — the full-pipeline tests below validate against the +//! Annex C.2.1.1 analysis PQF and the §4.6.11 TDAC property. + +use crate::gain_control::{band_record, GainBandState}; +use crate::gain_control_data::GainControlData; +use crate::ics_info::{IcsInfo, WindowSequence}; +use crate::ipqf::{Ipqf, NUM_BANDS}; +use crate::ssr_filterbank::SsrSynthesis; +use crate::Result; + +/// One channel's persistent SSR gain-control + IPQF state: the four +/// per-band [`GainBandState`] carries plus the streaming [`Ipqf`]. +#[derive(Debug, Clone)] +pub struct SsrGainControl { + /// Per-PQF-band gain-control cross-frame state (`PFMD` / `PT`). + bands: [GainBandState; NUM_BANDS], + /// The streaming IPQF synthesizer (cross-frame band history). + ipqf: Ipqf, +} + +impl Default for SsrGainControl { + fn default() -> Self { + Self::new() + } +} + +impl SsrGainControl { + /// A fresh per-channel SSR pipeline with the §4.6.12 spec initial + /// state (`PFMD ≡ 1.0`, `PT ≡ 0.0`, zero IPQF history). + #[must_use] + pub fn new() -> Self { + SsrGainControl { + bands: core::array::from_fn(|_| GainBandState::new()), + ipqf: Ipqf::new(), + } + } + + /// Reconstruct one frame of PCM `AS(n)` from the four per-band IMDCT + /// outputs and the frame's gain-control side info. + /// + /// * `u` — the four non-overlapped per-band IMDCT outputs + /// `U_{W,B}`. `u[B]` is the band-`B` column: a single 512-sample + /// window for the long sequences, or eight 64-sample windows + /// concatenated for `EIGHT_SHORT_SEQUENCE`. + /// * `gcd` — the decoded `gain_control_data()` (`None` ⇒ no gain + /// control active this frame, every band runs `T = U`). + /// * `seq` — the frame's `window_sequence`. + /// + /// Returns `NUM_BANDS · |V_B|` PCM samples (`4 · 256 = 1024` for the + /// steady `ONLY_LONG` / `EIGHT_SHORT` case). + #[must_use] + pub fn decode_frame( + &mut self, + u: &[Vec; NUM_BANDS], + gcd: Option<&GainControlData>, + seq: WindowSequence, + ) -> Vec { + // §4.6.12.3.3 — per-band gain windowing + overlap → V_B. + let mut v: [Vec; NUM_BANDS] = core::array::from_fn(|_| Vec::new()); + for (b, slot) in v.iter_mut().enumerate() { + // Spec band index is 1..=3 for gain-controlled bands; PQF + // band 0 never carries a ladder (§4.6.12.3.3 `B == 0`). + let ladder = gcd.and_then(|g| band_record(g, b)); + *slot = self.bands[b].window_overlap(ladder, &u[b], seq); + } + + // §4.6.12.3.4 — IPQF synthesis. All four V_B share the same + // per-frame length by construction. + let len = v[0].len(); + debug_assert!(v.iter().all(|vb| vb.len() == len)); + let band_refs: [&[f64]; NUM_BANDS] = core::array::from_fn(|b| v[b].as_slice()); + self.ipqf.synthesize(&band_refs, len) + } +} + +/// One channel's *complete* §4.6.12 SSR reconstruction pipeline: the +/// §4.6.12.1 front-half filterbank ([`SsrSynthesis`] — band split, +/// even-band reversal, per-band 256/32-line IMDCTs + windows) chained +/// into the §4.6.12.3 gain-control + IPQF back end +/// ([`SsrGainControl`]). +/// +/// This is the SSR (AOT 3) replacement for the per-channel §4.6.11 +/// [`crate::filterbank::Filterbank`]: it consumes the same decoded +/// 1024-line spectrum (window-major for `EIGHT_SHORT_SEQUENCE`, after +/// TNS) and produces the frame's PCM time signal `AS(n)`. +#[derive(Debug, Clone, Default)] +pub struct SsrChannelDecoder { + /// §4.6.12.1 front half (carries the previous block's + /// `window_shape`). + synth: SsrSynthesis, + /// §4.6.12.3 back half (carries `PFMD` / `PT` / IPQF history). + gain: SsrGainControl, +} + +impl SsrChannelDecoder { + /// A fresh SSR channel pipeline with the spec initial state. + #[must_use] + pub fn new() -> Self { + SsrChannelDecoder::default() + } + + /// Decode one frame: 1024-line spectrum (+ this frame's + /// `gain_control_data()`, if any) → PCM `AS(n)`. + /// + /// The output length follows the §4.6.12.3.3 band fragment length + /// times the four-band IPQF interpolation: 1024 samples for + /// `ONLY_LONG` / `EIGHT_SHORT`, 1472 for `LONG_START`, 576 for + /// `LONG_STOP` (a `START`/`STOP` pair still totals 2048, so stream + /// timing is preserved). + pub fn decode_frame( + &mut self, + spec: &[f64], + ics_info: &IcsInfo, + gcd: Option<&GainControlData>, + ) -> Result> { + let u = self.synth.windowed_bands(spec, ics_info)?; + Ok(self.gain.decode_frame(&u, gcd, ics_info.window_sequence)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Four bands of constant-zero U give silence out. + #[test] + fn zero_bands_give_silence() { + let mut ssr = SsrGainControl::new(); + let u: [Vec; NUM_BANDS] = core::array::from_fn(|_| vec![0.0f64; 512]); + let pcm = ssr.decode_frame(&u, None, WindowSequence::OnlyLong); + assert_eq!(pcm.len(), 1024); + assert!(pcm.iter().all(|&x| x == 0.0)); + } + + /// A steady ONLY_LONG stream produces 1024 PCM samples per frame and + /// the pipeline is finite + deterministic. + #[test] + fn only_long_frame_is_1024_pcm() { + let mut ssr = SsrGainControl::new(); + let u: [Vec; NUM_BANDS] = + core::array::from_fn(|b| (0..512).map(|j| ((b * 512 + j) as f64) * 1e-3).collect()); + let pcm0 = ssr.decode_frame(&u, None, WindowSequence::OnlyLong); + assert_eq!(pcm0.len(), 1024); + assert!(pcm0.iter().all(|x| x.is_finite())); + // A second identical frame also yields 1024 and threads state. + let pcm1 = ssr.decode_frame(&u, None, WindowSequence::OnlyLong); + assert_eq!(pcm1.len(), 1024); + // The first and second frames differ (the overlap tail carries). + assert!(pcm0 != pcm1); + } + + /// Gain control with `max_band == 0` (the bare 2-bit field, no + /// ladders) is the identity: same PCM as `None`. + #[test] + fn max_band_zero_matches_no_gain() { + let u: [Vec; NUM_BANDS] = core::array::from_fn(|b| { + (0..512) + .map(|j| ((b + 1) as f64 * (j as f64 + 1.0)).sin()) + .collect() + }); + let gcd = GainControlData { + max_band: 0, + bands: Vec::new(), + }; + let mut a = SsrGainControl::new(); + let mut b = SsrGainControl::new(); + let pa = a.decode_frame(&u, Some(&gcd), WindowSequence::OnlyLong); + let pb = b.decode_frame(&u, None, WindowSequence::OnlyLong); + assert_eq!(pa.len(), pb.len()); + for (x, y) in pa.iter().zip(pb.iter()) { + assert!((x - y).abs() < 1e-12); + } + } + + /// EIGHT_SHORT bands (eight 64-sample windows each) also reconstruct + /// 1024 PCM samples per frame. + #[test] + fn eight_short_frame_is_1024_pcm() { + let mut ssr = SsrGainControl::new(); + let u: [Vec; NUM_BANDS] = + core::array::from_fn(|_| (0..512).map(|j| (j as f64 * 0.01).cos()).collect()); + let pcm = ssr.decode_frame(&u, None, WindowSequence::EightShort); + assert_eq!(pcm.len(), 1024); + assert!(pcm.iter().all(|x| x.is_finite())); + } +} + +/// Full-pipeline round-trip tests: the Annex C.2.1.1 analysis PQF + +/// the §4.6.11.3.2 (quarter-scale) analysis windows + forward MDCTs +/// mirror the encoder; [`SsrChannelDecoder`] must reconstruct the +/// input within the PQF pair's near-perfect-reconstruction bound. +#[cfg(test)] +mod round_trip_tests { + use super::*; + use crate::filterbank::{forward_mdct, long_sequence_window_n, short_window_n}; + use crate::gain_control::{band_record, pfmd_len, BandGainFunction}; + use crate::gain_control_data::{GainAdjust, GainBand, GainWindow}; + use crate::ics_info::{IcsInfo, WindowShape}; + use crate::ssr_filterbank::pqf_test_support::{pqf_analysis, PQF_CASCADE_DELAY}; + use crate::ssr_filterbank::{SSR_LONG_TRANSFORM, SSR_SHORT_TRANSFORM}; + use core::f64::consts::PI; + + /// A minimal [`IcsInfo`] carrying just what the SSR pipeline reads. + fn ics(shape: WindowShape, seq: WindowSequence) -> IcsInfo { + let short = seq == WindowSequence::EightShort; + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: seq, + window_shape: shape, + max_sfb: 0, + scale_factor_grouping: if short { Some(0) } else { None }, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: if short { 8 } else { 1 }, + num_window_groups: if short { 8 } else { 1 }, + window_group_length: if short { vec![1; 8] } else { vec![1] }, + num_swb: 0, + } + } + + /// A broadband deterministic test signal exciting all four PQF + /// bands: four tones (one per band quarter) plus a slow envelope. + fn test_signal(len: usize) -> Vec { + (0..len) + .map(|n| { + let t = n as f64; + let env = 0.6 + 0.4 * (2.0 * PI * t / 3000.0).sin(); + env * ((0.05 * t).sin() + + 0.7 * (0.9 * t).sin() + + 0.5 * (1.8 * t).sin() + + 0.4 * (2.9 * t).sin()) + }) + .collect() + } + + /// Encoder-mirror state: per-band position of the next frame's + /// window origin (in band samples) plus the previous block's + /// window shape and the per-band `PFMD` gain threading. + struct MirrorEncoder { + /// Absolute band-sample position `P_f` where this frame's `V` + /// starts. + p: usize, + prev_shape: Option, + /// Per-band `PFMD` carry for the encoder-side GMF (256 + /// entries, prefix-read like the decoder's). + pfmd: [Vec; NUM_BANDS], + } + + impl MirrorEncoder { + fn new() -> Self { + MirrorEncoder { + p: 0, + prev_shape: None, + pfmd: core::array::from_fn(|_| vec![1.0f64; 256]), + } + } + + /// Encode one frame: window the four band signals at the + /// §4.6.12.3.3-mirror positions, apply the §4.6.12.3.2 `GMF` + /// (identity when `gcd` is `None`), forward-MDCT each band, + /// reverse the even (0-based 1 and 3) bands and assemble the + /// 1024-line spectrum. Advances the band position by the + /// frame's `V` length. + fn encode_frame( + &mut self, + bands: &[Vec; NUM_BANDS], + seq: WindowSequence, + shape: WindowShape, + gcd: Option<&GainControlData>, + ) -> Vec { + let left = self.prev_shape.unwrap_or(shape); + let mut spec = vec![0.0f64; 1024]; + + // Per-band GMF (1/AD) for this frame, threading PFMD the + // same way the decoder does. + let gmf: [Vec>; NUM_BANDS] = core::array::from_fn(|b| { + let record = gcd.and_then(|g| band_record(g, b)); + let f = match record { + Some(rec) => { + BandGainFunction::reconstruct(rec, seq, &self.pfmd[b][..pfmd_len(seq)]) + } + None => BandGainFunction::identity(seq), + }; + self.pfmd[b][..f.pfmd_next.len()].copy_from_slice(&f.pfmd_next); + f.ad.iter() + .map(|w| w.iter().map(|&a| 1.0 / a).collect()) + .collect() + }); + + match seq { + WindowSequence::EightShort => { + // Window w over band samples [p + 32w, p + 32w + 64). + for w in 0..8 { + let win = short_window_n(SSR_SHORT_TRANSFORM, w, left, shape); + for (b, band) in bands.iter().enumerate() { + let z: Vec = (0..SSR_SHORT_TRANSFORM) + .map(|n| band[self.p + 32 * w + n] * gmf[b][w][n] * win[n]) + .collect(); + let mut coeffs = forward_mdct(&z, SSR_SHORT_TRANSFORM); + if b % 2 == 1 { + coeffs.reverse(); + } + spec[128 * w + 32 * b..128 * w + 32 * b + 32].copy_from_slice(&coeffs); + } + } + self.p += 256; + } + _ => { + // Long window over [p, p+512) (LONG_STOP: the + // window origin sits 112 band samples *before* the + // frame's V start, mirroring §4.6.12.3.3). + let origin = match seq { + WindowSequence::LongStop => self.p - 112, + _ => self.p, + }; + let win = long_sequence_window_n( + SSR_LONG_TRANSFORM, + SSR_SHORT_TRANSFORM, + seq, + left, + shape, + ) + .unwrap(); + for (b, band) in bands.iter().enumerate() { + let z: Vec = (0..SSR_LONG_TRANSFORM) + .map(|n| band[origin + n] * gmf[b][0][n] * win[n]) + .collect(); + let mut coeffs = forward_mdct(&z, SSR_LONG_TRANSFORM); + if b % 2 == 1 { + coeffs.reverse(); + } + spec[256 * b..256 * b + 256].copy_from_slice(&coeffs); + } + self.p += match seq { + WindowSequence::OnlyLong => 256, + WindowSequence::LongStart => 368, + WindowSequence::LongStop => 144, + WindowSequence::EightShort => unreachable!(), + }; + } + } + self.prev_shape = Some(shape); + spec + } + } + + /// Round-trip error-to-signal RMS of `y` (decoder output) against + /// `x` delayed by the PQF cascade, over `[skip, n)`. + fn err_ratio(x: &[f64], y: &[f64], skip: usize) -> f64 { + let n = y.len().min(x.len().saturating_sub(PQF_CASCADE_DELAY)); + let (mut err, mut sig) = (0.0f64, 0.0f64); + for i in skip..n { + // y(i) reconstructs x(i - delay): compare shifted. + let d = y[i] - x[i - PQF_CASCADE_DELAY]; + err += d * d; + sig += x[i - PQF_CASCADE_DELAY] * x[i - PQF_CASCADE_DELAY]; + } + (err / sig).sqrt() + } + + /// Steady `ONLY_LONG` frames round-trip through the complete + /// §4.6.12 pipeline within the PQF pair's reconstruction bound, + /// for both window shapes. + #[test] + fn full_pipeline_round_trips_only_long() { + let frames = 20usize; + let x = test_signal(4 * 256 * (frames + 3)); + let bands = pqf_analysis(&x); + for shape in [WindowShape::Sine, WindowShape::Kbd] { + let mut enc = MirrorEncoder::new(); + let mut dec = SsrChannelDecoder::new(); + let info = ics(shape, WindowSequence::OnlyLong); + let mut y = Vec::new(); + for _ in 0..frames { + let spec = enc.encode_frame(&bands, WindowSequence::OnlyLong, shape, None); + y.extend(dec.decode_frame(&spec, &info, None).unwrap()); + } + assert_eq!(y.len(), 1024 * frames); + let ratio = err_ratio(&x, &y, 2048); + assert!(ratio < 1e-3, "{shape:?} round-trip err/sig = {ratio}"); + } + } + + /// A full window-sequence transition chain (`ONLY_LONG → + /// LONG_START → EIGHT_SHORT ×2 → LONG_STOP → ONLY_LONG`) + /// round-trips, with the §4.6.12.3.3 variable per-frame output + /// lengths (1024 / 1472 / 1024 / 576) preserving stream timing. + #[test] + fn full_pipeline_round_trips_window_transitions() { + use WindowSequence::{EightShort, LongStart, LongStop, OnlyLong}; + let chain = [ + OnlyLong, OnlyLong, OnlyLong, LongStart, EightShort, EightShort, LongStop, OnlyLong, + OnlyLong, LongStart, EightShort, LongStop, OnlyLong, OnlyLong, + ]; + let x = test_signal(4 * 256 * (chain.len() + 3)); + let bands = pqf_analysis(&x); + let mut enc = MirrorEncoder::new(); + let mut dec = SsrChannelDecoder::new(); + let mut y = Vec::new(); + let mut expect_len = 0usize; + for &seq in &chain { + let spec = enc.encode_frame(&bands, seq, WindowShape::Sine, None); + let out = dec + .decode_frame(&spec, &ics(WindowShape::Sine, seq), None) + .unwrap(); + expect_len += match seq { + OnlyLong | EightShort => 1024, + LongStart => 1472, + LongStop => 576, + }; + y.extend(out); + } + assert_eq!(y.len(), expect_len); + let ratio = err_ratio(&x, &y, 2048); + assert!( + ratio < 1e-3, + "transition-chain round-trip err/sig = {ratio}" + ); + } + + /// Gain ladders cancel end to end: the encoder applies the + /// §4.6.12.3.2 `GMF`, the decoder its inverse `AD`, and the + /// round-trip stays close to the input — while decoding the same + /// stream *without* the gain data leaves the gain modification in + /// the output (large error). Pins the orientation of the whole + /// §4.6.12.3 gain path against the front half. + #[test] + fn gain_ladders_cancel_in_round_trip() { + let frames = 16usize; + let x = test_signal(4 * 256 * (frames + 3)); + let bands = pqf_analysis(&x); + + // Ladders on bands 1..=3 (spec 2nd..4th), one gain change per + // window: modest ±1-exponent steps at varied positions. + let gcd = GainControlData { + max_band: 3, + bands: vec![ + GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 5, // AdjLev = 1 ⇒ ALEV = 2. + aloccode: 4, // ALOC = 32. + }], + }], + }, + GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 3, // AdjLev = −1 ⇒ ALEV = 1/2. + aloccode: 12, // ALOC = 96. + }], + }], + }, + GainBand { + windows: vec![GainWindow { + adjustments: vec![GainAdjust { + alevcode: 6, // AdjLev = 2 ⇒ ALEV = 4. + aloccode: 20, // ALOC = 160. + }], + }], + }, + ], + }; + + let mut enc = MirrorEncoder::new(); + let mut dec = SsrChannelDecoder::new(); + let mut dec_plain = SsrChannelDecoder::new(); + let info = ics(WindowShape::Sine, WindowSequence::OnlyLong); + let mut y = Vec::new(); + let mut y_plain = Vec::new(); + for _ in 0..frames { + let spec = enc.encode_frame( + &bands, + WindowSequence::OnlyLong, + WindowShape::Sine, + Some(&gcd), + ); + y.extend(dec.decode_frame(&spec, &info, Some(&gcd)).unwrap()); + y_plain.extend(dec_plain.decode_frame(&spec, &info, None).unwrap()); + } + let ratio = err_ratio(&x, &y, 2048); + // Gain steps re-introduce a little aliasing at the transition + // ramps (the §4.6.12.3.2 Inter() ramp bounds it); the + // compensated round trip must stay small… + assert!(ratio < 0.02, "gain-compensated err/sig = {ratio}"); + // …while dropping the gain data leaves the modification in. + let ratio_plain = err_ratio(&x, &y_plain, 2048); + assert!( + ratio_plain > 5.0 * ratio, + "uncompensated err/sig = {ratio_plain} vs compensated {ratio}" + ); + } + + /// Per-sequence output lengths of [`SsrChannelDecoder`]. + #[test] + fn decode_frame_output_lengths() { + let spec = vec![0.5f64; 1024]; + let mut dec = SsrChannelDecoder::new(); + for (seq, len) in [ + (WindowSequence::OnlyLong, 1024), + (WindowSequence::LongStart, 1472), + (WindowSequence::EightShort, 1024), + (WindowSequence::LongStop, 576), + ] { + let out = dec + .decode_frame(&spec, &ics(WindowShape::Sine, seq), None) + .unwrap(); + assert_eq!(out.len(), len, "{seq:?}"); + } + } +} diff --git a/crates/vendor/oxideav-aac/src/ssr_filterbank.rs b/crates/vendor/oxideav-aac/src/ssr_filterbank.rs new file mode 100644 index 00000000..4e184556 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/ssr_filterbank.rs @@ -0,0 +1,454 @@ +//! SSR front-half filterbank — ISO/IEC 14496-3 §4.6.12.1 (matching +//! ISO/IEC 13818-7 §16.1): the spectrum → PQF-band de-interleave, the +//! even-band spectral reversal, and the per-band 256/32-line IMDCTs +//! with the quarter-scale §4.6.11.3.2 windows. +//! +//! When the gain control tool is active (the SSR object type, AOT 3), +//! the §4.6.11 filterbank configuration changes (§4.6.12.1): +//! +//! * the IMDCT is 256 lines instead of 1024 (one per PQF band) for the +//! long window sequences, and 32 lines instead of 128 (eight per +//! band) for `EIGHT_SHORT_SEQUENCE`; +//! * "the filter bank tool outputs a total of 2048 non-overlapped +//! values per frame" — four bands × 512 windowed samples, handed to +//! the §4.6.12.3.3 gain-control windowing/overlap stage as +//! `U_{W,B}(j)`; +//! * "the order of the MDCT coefficients in each even PQF band must be +//! reversed … exchanging the higher frequency MDCT coefficients with +//! the lower frequency MDCT coefficients". +//! +//! ## The spectrum → band arrangement +//! +//! The PQF splits the input into "four equal width frequency bands" +//! (Annex C.2.1.1), band `B` covering the `B`-th quarter of the +//! spectrum in ascending frequency (its modulator is centred on +//! `(2B+1)π/8`). The transmitted spectrum keeps the ordinary +//! ascending-frequency coefficient order (the §4.5.2.3 scalefactor-band +//! machinery runs on it unchanged), so band `B`'s 256 (long) / 32 +//! (short, per window) coefficient column is the contiguous quarter +//! `spec[256·B ..][..256]` / `spec[128·w + 32·B ..][..32]`. +//! +//! ## Which bands are "even" +//! +//! The §4.6.12.2 definitions count IPQF bands ordinally — `max_band` +//! is defined over "the 2nd / 3rd / 4th IPQF band" — so the "even PQF +//! band[s]" whose coefficients are reversed are the 2nd and 4th, i.e. +//! 0-based bands 1 and 3. This is also forced by the filterbank +//! mathematics: decimating band `B` by four spectrally inverts the +//! odd-indexed (0-based) bands, so exactly those bands need the +//! reversal for the assembled spectrum to be frequency-ascending. The +//! `tone_lands_at_its_spectral_bin` test pins this against the Annex +//! C.2.1.1 analysis PQF: a pure tone encoded through the PQF → MDCT → +//! reversal chain peaks at its global spectral bin only under this +//! convention (bands 1 and 3 mirror without it). +//! +//! ## Provenance +//! +//! Transform sizes, output layout and the reversal rule are the +//! §4.6.12.1 / §16.1 prose; the window geometry is §4.6.11.3.2 +//! evaluated at the `(512, 64)` family with the KBD windows pinned +//! against Tables 4.A.13 / 4.A.14; the validation PQF is the Annex +//! C.2.1.1 formula. All from the spec PDFs staged under +//! `docs/audio/aac/`. No external SSR implementation was consulted. + +use crate::filterbank::{imdct, long_sequence_window_n, short_window_n}; +use crate::ics_info::{IcsInfo, WindowSequence, WindowShape}; +use crate::ipqf::NUM_BANDS; +use crate::Error; + +type Result = core::result::Result; + +/// The SSR per-band long transform length (§4.6.12.1: 256 lines → +/// `N = 512`). +pub const SSR_LONG_TRANSFORM: usize = 512; +/// The SSR per-band short transform length (§4.6.12.1: 32 lines → +/// `N = 64`). +pub const SSR_SHORT_TRANSFORM: usize = 64; +/// Spectral lines per band for the long window sequences. +pub const BAND_LINES_LONG: usize = SSR_LONG_TRANSFORM / 2; // 256 +/// Spectral lines per band per short window. +pub const BAND_LINES_SHORT: usize = SSR_SHORT_TRANSFORM / 2; // 32 +/// Short windows in an `EIGHT_SHORT_SEQUENCE`. +const NUM_SHORT_WINDOWS: usize = 8; +/// Non-overlapped windowed samples each band contributes per frame +/// (§4.6.12.1: `4 × 512 = 2048` total). +pub const BAND_SAMPLES_PER_FRAME: usize = SSR_LONG_TRANSFORM; + +/// §4.6.12.1 — split the frame's 1024 decoded spectral coefficients +/// into the four PQF-band coefficient columns, applying the even-band +/// (0-based 1 and 3, see the module notes) spectral reversal. +/// +/// * Long sequences: `spec` is the 1024-line frequency-ascending +/// spectrum; band `B`'s column is `spec[256·B ..][..256]`, reversed +/// for bands 1 and 3. +/// * `EIGHT_SHORT_SEQUENCE`: `spec` is window-major (window `w` at +/// `spec[128·w ..][..128]`); band `B`'s column concatenates the +/// eight per-window quarters `spec[128·w + 32·B ..][..32]` (each +/// reversed for bands 1 and 3), so it is itself window-major. +/// +/// Errors with [`Error::FilterbankInvalid`] if `spec` is not 1024 +/// coefficients. +pub fn split_bands(spec: &[f64], seq: WindowSequence) -> Result<[Vec; NUM_BANDS]> { + if spec.len() != NUM_BANDS * BAND_LINES_LONG { + return Err(Error::FilterbankInvalid); + } + let mut bands: [Vec; NUM_BANDS] = + core::array::from_fn(|_| Vec::with_capacity(BAND_LINES_LONG)); + match seq { + WindowSequence::EightShort => { + for w in 0..NUM_SHORT_WINDOWS { + let win = + &spec[w * (NUM_BANDS * BAND_LINES_SHORT)..][..NUM_BANDS * BAND_LINES_SHORT]; + for (b, band) in bands.iter_mut().enumerate() { + let col = &win[b * BAND_LINES_SHORT..][..BAND_LINES_SHORT]; + if b % 2 == 1 { + band.extend(col.iter().rev()); + } else { + band.extend_from_slice(col); + } + } + } + } + _ => { + for (b, band) in bands.iter_mut().enumerate() { + let col = &spec[b * BAND_LINES_LONG..][..BAND_LINES_LONG]; + if b % 2 == 1 { + band.extend(col.iter().rev()); + } else { + band.extend_from_slice(col); + } + } + } + } + Ok(bands) +} + +/// The stateful SSR front-half synthesis for one channel: the +/// §4.6.12.1 band split + per-band IMDCT + quarter-scale §4.6.11.3.2 +/// windowing, producing the non-overlapped `U_{W,B}(j)` columns the +/// §4.6.12.3.3 gain-control stage consumes. +/// +/// Carries the previous block's `window_shape` across frames (the left +/// half of every window inherits it, §4.6.11.3.2 — the SSR family +/// keeps the standard inheritance rule). +#[derive(Debug, Clone, Default)] +pub struct SsrSynthesis { + /// `window_shape` of the previous block; `None` before the first + /// frame (the first block uses its own shape for both halves). + prev_shape: Option, +} + +impl SsrSynthesis { + /// A fresh front half with no previous-block shape. + #[must_use] + pub fn new() -> Self { + SsrSynthesis::default() + } + + /// Produce the four per-band non-overlapped windowed columns + /// `U_{W,B}` for one frame. + /// + /// `spec` is the frame's decoded 1024-line spectrum (window-major + /// for `EIGHT_SHORT_SEQUENCE`). Each returned column holds + /// [`BAND_SAMPLES_PER_FRAME`] (512) samples: a single windowed + /// 512-sample block for the long sequences, or eight windowed + /// 64-sample blocks concatenated window-major for + /// `EIGHT_SHORT_SEQUENCE` — exactly the `u` layout + /// [`crate::gain_control::GainBandState::window_overlap`] expects. + pub fn windowed_bands( + &mut self, + spec: &[f64], + ics_info: &IcsInfo, + ) -> Result<[Vec; NUM_BANDS]> { + let left_shape = self.prev_shape.unwrap_or(ics_info.window_shape); + let right_shape = ics_info.window_shape; + let seq = ics_info.window_sequence; + let cols = split_bands(spec, seq)?; + + let mut out: [Vec; NUM_BANDS] = core::array::from_fn(|_| Vec::new()); + match seq { + WindowSequence::EightShort => { + // Eight per-band 32-line IMDCTs, each windowed with the + // 64-sample short window (window 0's left half inherits + // the previous block's shape). No intra-sequence + // overlap-add here: §4.6.12.3.3 performs it after the + // gain is applied. + for (band, col) in out.iter_mut().zip(cols.iter()) { + let mut u = Vec::with_capacity(BAND_SAMPLES_PER_FRAME); + for w in 0..NUM_SHORT_WINDOWS { + let lines = &col[w * BAND_LINES_SHORT..][..BAND_LINES_SHORT]; + let x = imdct(lines, SSR_SHORT_TRANSFORM); + let win = short_window_n(SSR_SHORT_TRANSFORM, w, left_shape, right_shape); + u.extend(x.iter().zip(win.iter()).map(|(&xv, &wv)| xv * wv)); + } + band.extend_from_slice(&u); + } + } + _ => { + let win = long_sequence_window_n( + SSR_LONG_TRANSFORM, + SSR_SHORT_TRANSFORM, + seq, + left_shape, + right_shape, + )?; + for (band, col) in out.iter_mut().zip(cols.iter()) { + let x = imdct(col, SSR_LONG_TRANSFORM); + band.extend(x.iter().zip(win.iter()).map(|(&xv, &wv)| xv * wv)); + } + } + } + + self.prev_shape = Some(right_shape); + Ok(out) + } +} + +/// Test-side mirror of the encoder PQF (Annex C.2.1.1), shared by the +/// front-half tests here and the full round-trip tests in +/// [`crate::ssr`]. +#[cfg(test)] +pub(crate) mod pqf_test_support { + use super::NUM_BANDS; + use crate::ipqf::{prototype, PROTO_LEN}; + use core::f64::consts::PI; + + /// Annex C.2.1.1 — the encoder-side PQF analysis coefficients + /// `h_i(n) = (1/4)·cos((2i+1)(2n+5)π/16)·Q(n)`, `0 ≤ n ≤ 95`, + /// with `Q` the Table 4.110 prototype (test-side mirror of the + /// §4.6.12.3.4 IPQF). + pub(crate) fn analysis_coefs() -> [[f64; PROTO_LEN]; NUM_BANDS] { + let q = prototype(); + core::array::from_fn(|i| { + core::array::from_fn(|n| { + 0.25 * ((2.0 * i as f64 + 1.0) * (2.0 * n as f64 + 5.0) * PI / 16.0).cos() * q[n] + }) + }) + } + + /// Critically-sampled PQF analysis: band sample + /// `X_B(m) = Σ_n h_B(n)·x(4m + 3 − n)` — each band sample consumes + /// one block of four new input samples (the `+3` reads up to the + /// newest sample of block `m`; the resulting analysis+synthesis + /// cascade delay is [`PQF_CASCADE_DELAY`] full-rate samples). + pub(crate) fn pqf_analysis(x: &[f64]) -> [Vec; NUM_BANDS] { + let h = analysis_coefs(); + let m_len = x.len() / NUM_BANDS; + core::array::from_fn(|b| { + (0..m_len) + .map(|m| { + let mut acc = 0.0f64; + for (n, &hn) in h[b].iter().enumerate() { + let idx = 4 * m as isize + 3 - n as isize; + if idx >= 0 { + if let Some(&xv) = x.get(idx as usize) { + acc += hn * xv; + } + } + } + acc + }) + .collect() + }) + } + + /// Full-rate delay of the Annex C.2.1.1 analysis → §4.6.12.3.4 + /// synthesis cascade with the `+3` analysis alignment (measured by + /// the near-perfect-reconstruction test). + pub(crate) const PQF_CASCADE_DELAY: usize = 92; +} + +#[cfg(test)] +mod tests { + use super::pqf_test_support::{pqf_analysis, PQF_CASCADE_DELAY}; + use super::*; + use crate::filterbank::forward_mdct; + use crate::ipqf::Ipqf; + use core::f64::consts::PI; + + /// The analysis PQF and the IPQF are a near-perfect-reconstruction + /// pair: white input round-trips within the prototype's stopband + /// leakage (measured ≈ 2.9e-4 err/sig) at a flat 92-sample delay. + #[test] + fn pqf_ipqf_cascade_is_near_perfect_reconstruction() { + // Deterministic pseudo-random input. + let mut state = 0x1234_5678u32; + let mut rnd = || { + state = state.wrapping_mul(1664525).wrapping_add(1013904223); + (state >> 8) as f64 / (1u32 << 24) as f64 - 0.5 + }; + let x: Vec = (0..4000).map(|_| rnd()).collect(); + let bands = pqf_analysis(&x); + let refs: [&[f64]; NUM_BANDS] = core::array::from_fn(|b| bands[b].as_slice()); + let mut ipqf = Ipqf::new(); + let y = ipqf.synthesize(&refs, bands[0].len()); + + let (mut err, mut sig) = (0.0f64, 0.0f64); + for n in 500..2500 { + let d = y[n + PQF_CASCADE_DELAY] - x[n]; + err += d * d; + sig += x[n] * x[n]; + } + let ratio = (err / sig).sqrt(); + assert!(ratio < 1e-3, "cascade err/sig = {ratio}"); + // Discriminator: a wrong delay is nowhere near. + let mut err_bad = 0.0f64; + for n in 500..2500 { + let d = y[n + PQF_CASCADE_DELAY + 4] - x[n]; + err_bad += d * d; + } + assert!((err_bad / sig).sqrt() > 0.1); + } + + /// §4.6.12.1 — a pure tone at global spectral bin `k`, encoded + /// through the Annex C.2.1.1 PQF → per-band windowed MDCT → + /// even-band reversal → contiguous quarters, peaks at bin `k`. + /// Without the reversal, the band-1 / band-3 tones mirror inside + /// their quarter — this pins both the split arrangement and the + /// reversal convention (0-based bands 1 and 3). + #[test] + fn tone_lands_at_its_spectral_bin() { + let win: Vec = (0..SSR_LONG_TRANSFORM) + .map(|n| (PI / SSR_LONG_TRANSFORM as f64 * (n as f64 + 0.5)).sin()) + .collect(); + // One tone per PQF band. + for &k_target in &[100usize, 300, 550, 800] { + let f = (k_target as f64 + 0.5) * PI / 1024.0; + let x: Vec = (0..8192).map(|n| (f * n as f64).sin()).collect(); + let bands = pqf_analysis(&x); + + // Steady ONLY_LONG frame over band samples [768, 1280). + let mut spec = vec![0.0f64; 1024]; + let mut spec_unreversed = vec![0.0f64; 1024]; + for b in 0..NUM_BANDS { + let z: Vec = (0..SSR_LONG_TRANSFORM) + .map(|n| bands[b][768 + n] * win[n]) + .collect(); + let mut coeffs = forward_mdct(&z, SSR_LONG_TRANSFORM); + spec_unreversed[256 * b..256 * b + 256].copy_from_slice(&coeffs); + if b % 2 == 1 { + coeffs.reverse(); + } + spec[256 * b..256 * b + 256].copy_from_slice(&coeffs); + } + let peak = |s: &[f64]| { + (0..s.len()) + .max_by(|&a, &b| s[a].abs().partial_cmp(&s[b].abs()).unwrap()) + .unwrap() + }; + let got = peak(&spec); + assert!( + got.abs_diff(k_target) <= 2, + "tone k={k_target} peaked at {got}" + ); + let got_unrev = peak(&spec_unreversed); + if k_target / 256 % 2 == 1 { + // Bands 1 and 3 mirror without the reversal. + let band = k_target / 256; + let mirrored = 256 * band + (255 - (k_target - 256 * band)); + assert!( + got_unrev.abs_diff(mirrored) <= 2, + "unreversed tone k={k_target} peaked at {got_unrev}, expected ≈{mirrored}" + ); + } + } + } + + /// `split_bands` long layout: contiguous ascending quarters, bands + /// 1 and 3 reversed. + #[test] + fn split_bands_long_layout() { + let spec: Vec = (0..1024).map(|i| i as f64).collect(); + let bands = split_bands(&spec, WindowSequence::OnlyLong).unwrap(); + for (b, band) in bands.iter().enumerate() { + assert_eq!(band.len(), 256); + if b % 2 == 0 { + assert_eq!(band[0], (256 * b) as f64); + assert_eq!(band[255], (256 * b + 255) as f64); + } else { + assert_eq!(band[0], (256 * b + 255) as f64); + assert_eq!(band[255], (256 * b) as f64); + } + } + } + + /// `split_bands` short layout: per short window, per-band 32-line + /// quarters (window-major columns), bands 1 and 3 reversed within + /// each window. + #[test] + fn split_bands_short_layout() { + let spec: Vec = (0..1024).map(|i| i as f64).collect(); + let bands = split_bands(&spec, WindowSequence::EightShort).unwrap(); + for (b, band) in bands.iter().enumerate() { + assert_eq!(band.len(), 256); + for w in 0..8 { + let base = (128 * w + 32 * b) as f64; + if b % 2 == 0 { + assert_eq!(band[32 * w], base); + assert_eq!(band[32 * w + 31], base + 31.0); + } else { + assert_eq!(band[32 * w], base + 31.0); + assert_eq!(band[32 * w + 31], base); + } + } + } + } + + /// Bad spectrum length is rejected. + #[test] + fn split_bands_rejects_bad_length() { + assert!(split_bands(&[0.0; 512], WindowSequence::OnlyLong).is_err()); + } + + /// A minimal [`IcsInfo`] for the front-half tests. + fn test_ics_info(shape: WindowShape, seq: WindowSequence) -> IcsInfo { + let short = seq == WindowSequence::EightShort; + IcsInfo { + family: crate::swb_offset::FrameFamily::Lc1024, + ics_reserved_bit: false, + window_sequence: seq, + window_shape: shape, + max_sfb: 0, + scale_factor_grouping: if short { Some(0) } else { None }, + predictor_data_present: false, + predictor_data: None, + ltp_data_present: false, + ltp_data: None, + ltp_data_present_pair: None, + ltp_data_pair: None, + num_windows: if short { 8 } else { 1 }, + num_window_groups: if short { 8 } else { 1 }, + window_group_length: if short { vec![1; 8] } else { vec![1] }, + num_swb: 0, + } + } + + /// `windowed_bands` output geometry: four 512-sample columns for + /// every window sequence, and the long-start column goes silent + /// after the §4.6.11.3.2 zero region (scaled: `[400, 512)`). + #[test] + fn windowed_bands_geometry() { + let spec = vec![1.0f64; 1024]; + for seq in [ + WindowSequence::OnlyLong, + WindowSequence::LongStart, + WindowSequence::EightShort, + WindowSequence::LongStop, + ] { + let mut synth = SsrSynthesis::new(); + let info = test_ics_info(WindowShape::Sine, seq); + let u = synth.windowed_bands(&spec, &info).unwrap(); + for band in &u { + assert_eq!(band.len(), BAND_SAMPLES_PER_FRAME); + assert!(band.iter().all(|v| v.is_finite())); + } + if seq == WindowSequence::LongStart { + for band in &u { + for &v in &band[400..] { + assert_eq!(v, 0.0, "LONG_START zero region"); + } + } + } + } + } +} diff --git a/crates/vendor/oxideav-aac/src/swb_offset.rs b/crates/vendor/oxideav-aac/src/swb_offset.rs new file mode 100644 index 00000000..7eb868a0 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/swb_offset.rs @@ -0,0 +1,1418 @@ +//! Scalefactor-band offset tables — ISO/IEC 14496-3 §4.5.4.1 / Tables +//! 4.129–4.141. +//! +//! Each `swb_offset_long_window[fs_index]` / `swb_offset_short_window[fs_index]` +//! table lists the *index of the lowest spectral coefficient* of each +//! scalefactor band, plus a trailing sentinel at the spectrum length +//! (1024 for long, 128 for short). The per-band width is therefore +//! `offset[i + 1] - offset[i]`, and the total entry count is +//! `num_swb + 1`. +//! +//! ## What this module covers +//! +//! * [`SWB_OFFSET_LONG_WINDOW`] — 13-entry lookup of long-window +//! offset slices, keyed by `samplingFrequencyIndex` (Table 1.18). +//! Slots `0..=11` cover the 12 sampling rates that have defined +//! SWB tables; slot `12` (7350 Hz) is an empty slice (no SWB +//! table is defined). Sourced from Tables 4.129 (44.1 / 48 kHz, +//! fs 3/4), 4.131 (32 kHz, fs 5), 4.132 (8 kHz, fs 11), 4.134 +//! (11.025 / 12 / 16 kHz, fs 8/9/10), 4.136 (22.05 / 24 kHz, fs 6/7), +//! 4.138 (64 kHz, fs 2), 4.140 (88.2 / 96 kHz, fs 0/1). +//! * [`SWB_OFFSET_SHORT_WINDOW`] — 13-entry lookup of 128-line +//! short-window offset slices (same fs-index layout as the long +//! table). Sourced from Tables 4.130 (32 / 44.1 / 48 kHz, +//! fs 3/4/5), 4.133 (8 kHz, fs 11), 4.135 (11.025 / 12 / 16 kHz, +//! fs 8/9/10), 4.137 (22.05 / 24 kHz, fs 6/7), 4.139 (64 kHz, +//! fs 2), 4.141 (88.2 / 96 kHz, fs 0/1). +//! * [`long_window_offsets`] / [`short_window_offsets`] — safe +//! bounds-checked accessors. +//! * [`apply_pulse_data`] — the §4.6.13 pulse-escape reconstruction +//! loop. Given a quantised long-window spectrum `x_quant` and a +//! parsed [`crate::pulse_data::PulseData`] block, applies the +//! per-pulse offset / amplitude fix-up in place. +//! +//! * [`FrameFamily`] + [`long_window_offsets_family`] / +//! [`short_window_offsets_family`] — the §4.5.1.1 frame-length +//! families: the 960/120-line variant (`frameLengthFlag == 1`, the +//! bracketed "values for 1920 / 240" columns of Tables 4.129–4.141) +//! and the ER AAC LD 512/480-line variants (§4.6.17.2.1, Tables +//! 4.142–4.147 with the §4.5.1.1 nearest-defined-table rule for +//! rates those tables omit). +//! +//! ## What this module does *not* cover +//! +//! * `sampling_frequency_index == 12` (7350 Hz) has no +//! scalefactor-band table in the spec; accessors return +//! [`Error::IcsInfoUnsupportedSampleRateIndex`] for that index. +//! * The 24-bit explicit-rate escape (`samplingFrequencyIndex +//! == 0xf`) does not select an SWB table directly — the caller must +//! resolve the explicit rate to the nearest standard index before +//! invoking these accessors. + +use crate::pulse_data::PulseData; +use crate::{Error, Result}; + +/// Total number of spectral coefficients in a long-window frame +/// (1024). The sentinel of every long-window table equals this value. +pub const LONG_WINDOW_LEN: u16 = 1024; + +/// Total number of spectral coefficients in a short-window frame +/// (128). The sentinel of every short-window table equals this value. +pub const SHORT_WINDOW_LEN: u16 = 128; + +/// `swb_offset_long_window[3]` / `swb_offset_long_window[4]` — Table +/// 4.129 (44.1 and 48 kHz, 49 SWB). 50 entries (49 bands + sentinel). +const SWB_OFFSET_LONG_44100_48000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, + 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, + 736, 768, 800, 832, 864, 896, 928, 1024, +]; + +/// `swb_offset_long_window[5]` — Table 4.131 (32 kHz, 51 SWB). 52 +/// entries. +const SWB_OFFSET_LONG_32000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, + 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, + 736, 768, 800, 832, 864, 896, 928, 960, 992, 1024, +]; + +/// `swb_offset_long_window[11]` — Table 4.132 (8 kHz, 40 SWB). 41 +/// entries. +const SWB_OFFSET_LONG_8000: &[u16] = &[ + 0, 12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 172, 188, 204, 220, 236, 252, 268, + 288, 308, 328, 348, 372, 396, 420, 448, 476, 508, 544, 580, 620, 664, 712, 764, 820, 880, 944, + 1024, +]; + +/// `swb_offset_long_window[8]` / `[9]` / `[10]` — Table 4.134 +/// (11.025, 12 and 16 kHz, 43 SWB). 44 entries. +const SWB_OFFSET_LONG_11025_12000_16000: &[u16] = &[ + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 100, 112, 124, 136, 148, 160, 172, 184, 196, 212, + 228, 244, 260, 280, 300, 320, 344, 368, 396, 424, 456, 492, 532, 572, 616, 664, 716, 772, 832, + 896, 960, 1024, +]; + +/// `swb_offset_long_window[6]` / `[7]` — Table 4.136 (22.05 and 24 kHz, +/// 47 SWB). 48 entries. +const SWB_OFFSET_LONG_22050_24000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, 136, + 148, 160, 172, 188, 204, 220, 240, 260, 284, 308, 336, 364, 396, 432, 468, 508, 552, 600, 652, + 704, 768, 832, 896, 960, 1024, +]; + +/// `swb_offset_long_window[2]` — Table 4.138 (64 kHz, 47 SWB). 48 +/// entries. +const SWB_OFFSET_LONG_64000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 100, 112, 124, 140, + 156, 172, 192, 216, 240, 268, 304, 344, 384, 424, 464, 504, 544, 584, 624, 664, 704, 744, 784, + 824, 864, 904, 944, 984, 1024, +]; + +/// `swb_offset_long_window[0]` / `[1]` — Table 4.140 (88.2 and 96 kHz, +/// 41 SWB). 42 entries. +const SWB_OFFSET_LONG_88200_96000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, + 144, 156, 172, 188, 212, 240, 276, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, +]; + +/// `swb_offset_short_window[3]` / `[4]` / `[5]` — Table 4.130 +/// (32, 44.1, 48 kHz, 14 SWB). 15 entries. +const SWB_OFFSET_SHORT_32000_44100_48000: &[u16] = + &[0, 4, 8, 12, 16, 20, 28, 36, 44, 56, 68, 80, 96, 112, 128]; + +/// `swb_offset_short_window[11]` — Table 4.133 (8 kHz, 15 SWB). 16 +/// entries. +const SWB_OFFSET_SHORT_8000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 60, 72, 88, 108, 128, +]; + +/// `swb_offset_short_window[8]` / `[9]` / `[10]` — Table 4.135 +/// (11.025, 12, 16 kHz, 15 SWB). 16 entries. +const SWB_OFFSET_SHORT_11025_12000_16000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 60, 72, 88, 108, 128, +]; + +/// `swb_offset_short_window[6]` / `[7]` — Table 4.137 (22.05, 24 kHz, +/// 15 SWB). 16 entries. +const SWB_OFFSET_SHORT_22050_24000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 64, 76, 92, 108, 128, +]; + +/// `swb_offset_short_window[2]` — Table 4.139 (64 kHz, 12 SWB). 13 +/// entries. +const SWB_OFFSET_SHORT_64000: &[u16] = &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 128]; + +/// `swb_offset_short_window[0]` / `[1]` — Table 4.141 (88.2, 96 kHz, +/// 12 SWB). 13 entries. +const SWB_OFFSET_SHORT_88200_96000: &[u16] = &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 128]; + +/// `swb_offset_long_window` keyed by `samplingFrequencyIndex` +/// (ISO/IEC 14496-3 Table 1.18). Slot `12` (7350 Hz) carries an empty +/// slice — no SWB table is defined for that rate. +/// +/// Each slot is `num_swb + 1` entries long (the trailing entry is the +/// spectrum-length sentinel `1024`). +pub const SWB_OFFSET_LONG_WINDOW: [&[u16]; 13] = [ + SWB_OFFSET_LONG_88200_96000, // 0 = 96 kHz + SWB_OFFSET_LONG_88200_96000, // 1 = 88.2 kHz + SWB_OFFSET_LONG_64000, // 2 = 64 kHz + SWB_OFFSET_LONG_44100_48000, // 3 = 48 kHz + SWB_OFFSET_LONG_44100_48000, // 4 = 44.1 kHz + SWB_OFFSET_LONG_32000, // 5 = 32 kHz + SWB_OFFSET_LONG_22050_24000, // 6 = 24 kHz + SWB_OFFSET_LONG_22050_24000, // 7 = 22.05 kHz + SWB_OFFSET_LONG_11025_12000_16000, // 8 = 16 kHz + SWB_OFFSET_LONG_11025_12000_16000, // 9 = 12 kHz + SWB_OFFSET_LONG_11025_12000_16000, // 10 = 11.025 kHz + SWB_OFFSET_LONG_8000, // 11 = 8 kHz + &[], // 12 = 7350 Hz (no SWB table) +]; + +/// `swb_offset_short_window` keyed by `samplingFrequencyIndex` +/// (ISO/IEC 14496-3 Table 1.18). Slot `12` (7350 Hz) carries an +/// empty slice. +/// +/// Each slot is `num_swb + 1` entries long (the trailing entry is the +/// short-spectrum-length sentinel `128`). +pub const SWB_OFFSET_SHORT_WINDOW: [&[u16]; 13] = [ + SWB_OFFSET_SHORT_88200_96000, // 0 = 96 kHz + SWB_OFFSET_SHORT_88200_96000, // 1 = 88.2 kHz + SWB_OFFSET_SHORT_64000, // 2 = 64 kHz + SWB_OFFSET_SHORT_32000_44100_48000, // 3 = 48 kHz + SWB_OFFSET_SHORT_32000_44100_48000, // 4 = 44.1 kHz + SWB_OFFSET_SHORT_32000_44100_48000, // 5 = 32 kHz + SWB_OFFSET_SHORT_22050_24000, // 6 = 24 kHz + SWB_OFFSET_SHORT_22050_24000, // 7 = 22.05 kHz + SWB_OFFSET_SHORT_11025_12000_16000, // 8 = 16 kHz + SWB_OFFSET_SHORT_11025_12000_16000, // 9 = 12 kHz + SWB_OFFSET_SHORT_11025_12000_16000, // 10 = 11.025 kHz + SWB_OFFSET_SHORT_8000, // 11 = 8 kHz + &[], // 12 = 7350 Hz (no SWB table) +]; + +/// Look up `swb_offset_long_window[fs_index]`. +/// +/// Returns the slice of `num_swb + 1` per-band lowest-coefficient +/// indices (with the trailing `1024` sentinel) for the requested +/// `samplingFrequencyIndex`. +/// +/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] if `fs_index` +/// is outside `0..=11`. Index 12 (7350 Hz) has no defined long-window +/// SWB table. +pub fn long_window_offsets(fs_index: u8) -> Result<&'static [u16]> { + let idx = fs_index as usize; + if idx >= SWB_OFFSET_LONG_WINDOW.len() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + let slice = SWB_OFFSET_LONG_WINDOW[idx]; + if slice.is_empty() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + Ok(slice) +} + +/// Look up `swb_offset_short_window[fs_index]`. +/// +/// Returns the slice of `num_swb + 1` per-band lowest-coefficient +/// indices (with the trailing `128` sentinel) for the requested +/// `samplingFrequencyIndex`. +/// +/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] if `fs_index` +/// is outside `0..=11`. Index 12 (7350 Hz) has no defined short-window +/// SWB table. +pub fn short_window_offsets(fs_index: u8) -> Result<&'static [u16]> { + let idx = fs_index as usize; + if idx >= SWB_OFFSET_SHORT_WINDOW.len() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + let slice = SWB_OFFSET_SHORT_WINDOW[idx]; + if slice.is_empty() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + Ok(slice) +} + +// --------------------------------------------------------------------------- +// Frame-length families — §4.5.1.1 `frameLengthFlag` / §4.6.17.2.1. +// --------------------------------------------------------------------------- + +/// The four spectral-line frame families a General-Audio payload can +/// select — ISO/IEC 14496-3 §4.5.1.1 (`frameLengthFlag`) and +/// §4.6.17.2.1 (the ER AAC LD frame sizes). +/// +/// * For every GA AOT except AAC SSR and ER AAC LD, +/// `frameLengthFlag == 0` selects the 1024/128-line IMDCT family +/// and `frameLengthFlag == 1` the 960/120-line family. +/// * For ER AAC LD (AOT 23), `frameLengthFlag == 0` selects a single +/// 512-line IMDCT and `frameLengthFlag == 1` a single 480-line +/// IMDCT; there is no block switching (§4.6.17.2.2), hence no +/// short-window geometry at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum FrameFamily { + /// 1024 spectral lines per long frame, 128 per short window + /// (`frameLengthFlag == 0`, all GA AOTs except SSR / LD). + #[default] + Lc1024, + /// 960 spectral lines per long frame, 120 per short window + /// (`frameLengthFlag == 1`). + Lc960, + /// ER AAC LD, 512 spectral lines (`frameLengthFlag == 0`); + /// long-only. + Ld512, + /// ER AAC LD, 480 spectral lines (`frameLengthFlag == 1`); + /// long-only. + Ld480, +} + +impl FrameFamily { + /// Resolve the family from the stream's `audioObjectType` and + /// `frameLengthFlag` per §4.5.1.1. + pub fn from_aot_and_flag(aot: u8, frame_length_flag: bool) -> Self { + match (aot == 23, frame_length_flag) { + (false, false) => FrameFamily::Lc1024, + (false, true) => FrameFamily::Lc960, + (true, false) => FrameFamily::Ld512, + (true, true) => FrameFamily::Ld480, + } + } + + /// Spectral lines per long window == PCM samples per frame per + /// channel (1024 / 960 / 512 / 480). + pub fn frame_len(self) -> usize { + match self { + FrameFamily::Lc1024 => 1024, + FrameFamily::Lc960 => 960, + FrameFamily::Ld512 => 512, + FrameFamily::Ld480 => 480, + } + } + + /// `N_l` — the long IMDCT transform length (`2 × frame_len`): + /// 2048 / 1920 / 1024 / 960. + pub fn long_transform_len(self) -> usize { + 2 * self.frame_len() + } + + /// Spectral lines per short window (128 / 120), or [`None`] for + /// the long-only LD families (§4.6.17.2.2 — no block switching). + pub fn short_window_len(self) -> Option { + match self { + FrameFamily::Lc1024 => Some(128), + FrameFamily::Lc960 => Some(120), + FrameFamily::Ld512 | FrameFamily::Ld480 => None, + } + } + + /// `N_s` — the short IMDCT transform length (256 / 240), or + /// [`None`] for the LD families. + pub fn short_transform_len(self) -> Option { + self.short_window_len().map(|w| 2 * w) + } + + /// `true` for the ER AAC LD families (§4.6.17): long-only frames, + /// low-overlap window in place of KBD, LD LTP lag semantics. + pub fn is_ld(self) -> bool { + matches!(self, FrameFamily::Ld512 | FrameFamily::Ld480) + } +} + +// --------------------------------------------------------------------------- +// 960/120-line tables — the bracketed "values for 1920 / 240" columns +// of Tables 4.129–4.141. +// --------------------------------------------------------------------------- +// +// Each long table prints the 1920-transform variant as bracketed +// values on the shared rows: the band starts are identical to the +// 2048-transform column and only the tail changes — the sentinel +// becomes 960 and any offsets at or above 960 are dropped (`(-)`). +// Each short table only re-brackets the sentinel (`128 (120)`). + +/// `swb_offset_long_window[3]` / `[4]` for the 960-line family — +/// Table 4.129 bracketed column (44.1 / 48 kHz, 49 SWB). 50 entries. +const SWB_OFFSET_LONG_960_44100_48000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, + 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, + 736, 768, 800, 832, 864, 896, 928, 960, +]; + +/// `swb_offset_long_window[5]` for the 960-line family — Table 4.131 +/// bracketed column (32 kHz; the 992 / 1024 rows are `(-)`, so 49 +/// SWB). 50 entries. +const SWB_OFFSET_LONG_960_32000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, + 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, + 736, 768, 800, 832, 864, 896, 928, 960, +]; + +/// `swb_offset_long_window[11]` for the 960-line family — Table 4.132 +/// bracketed column (8 kHz, 40 SWB). 41 entries. +const SWB_OFFSET_LONG_960_8000: &[u16] = &[ + 0, 12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 172, 188, 204, 220, 236, 252, 268, + 288, 308, 328, 348, 372, 396, 420, 448, 476, 508, 544, 580, 620, 664, 712, 764, 820, 880, 944, + 960, +]; + +/// `swb_offset_long_window[8]` / `[9]` / `[10]` for the 960-line +/// family — Table 4.134 bracketed column (11.025 / 12 / 16 kHz; the +/// 1024 row is `(-)`, so 42 SWB). 43 entries. +const SWB_OFFSET_LONG_960_11025_12000_16000: &[u16] = &[ + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 100, 112, 124, 136, 148, 160, 172, 184, 196, 212, + 228, 244, 260, 280, 300, 320, 344, 368, 396, 424, 456, 492, 532, 572, 616, 664, 716, 772, 832, + 896, 960, +]; + +/// `swb_offset_long_window[6]` / `[7]` for the 960-line family — +/// Table 4.136 bracketed column (22.05 / 24 kHz; the 1024 row is +/// `(-)`, so 46 SWB). 47 entries. +const SWB_OFFSET_LONG_960_22050_24000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, 136, + 148, 160, 172, 188, 204, 220, 240, 260, 284, 308, 336, 364, 396, 432, 468, 508, 552, 600, 652, + 704, 768, 832, 896, 960, +]; + +/// `swb_offset_long_window[2]` for the 960-line family — Table 4.138 +/// bracketed column (64 kHz, `num_swb 47 (46)`: the 984 row brackets +/// to 960 and the 1024 row is `(-)`). 47 entries. +const SWB_OFFSET_LONG_960_64000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 100, 112, 124, 140, + 156, 172, 192, 216, 240, 268, 304, 344, 384, 424, 464, 504, 544, 584, 624, 664, 704, 744, 784, + 824, 864, 904, 944, 960, +]; + +/// `swb_offset_long_window[0]` / `[1]` for the 960-line family — +/// Table 4.140 bracketed column (88.2 / 96 kHz; the 1024 row is +/// `(-)`, so 40 SWB). 41 entries. +const SWB_OFFSET_LONG_960_88200_96000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, + 144, 156, 172, 188, 212, 240, 276, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, +]; + +/// Table 4.130 bracketed column — 120-line short window at 32 / 44.1 / +/// 48 kHz (14 SWB). 15 entries. +const SWB_OFFSET_SHORT_120_32000_44100_48000: &[u16] = + &[0, 4, 8, 12, 16, 20, 28, 36, 44, 56, 68, 80, 96, 112, 120]; + +/// Table 4.133 bracketed column — 120-line short window at 8 kHz +/// (15 SWB). 16 entries. +const SWB_OFFSET_SHORT_120_8000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 60, 72, 88, 108, 120, +]; + +/// Table 4.135 bracketed column — 120-line short window at 11.025 / +/// 12 / 16 kHz (15 SWB). 16 entries. +const SWB_OFFSET_SHORT_120_11025_12000_16000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 60, 72, 88, 108, 120, +]; + +/// Table 4.137 bracketed column — 120-line short window at 22.05 / +/// 24 kHz (15 SWB). 16 entries. +const SWB_OFFSET_SHORT_120_22050_24000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 64, 76, 92, 108, 120, +]; + +/// Table 4.139 bracketed column — 120-line short window at 64 kHz +/// (12 SWB). 13 entries. +const SWB_OFFSET_SHORT_120_64000: &[u16] = &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 120]; + +/// Table 4.141 bracketed column — 120-line short window at 88.2 / +/// 96 kHz (12 SWB). 13 entries. +const SWB_OFFSET_SHORT_120_88200_96000: &[u16] = + &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 120]; + +/// 960-line long-window offset tables keyed by +/// `samplingFrequencyIndex` (the bracketed Tables 4.129–4.140 +/// columns). Same slot layout as [`SWB_OFFSET_LONG_WINDOW`]. +pub const SWB_OFFSET_LONG_WINDOW_960: [&[u16]; 13] = [ + SWB_OFFSET_LONG_960_88200_96000, // 0 = 96 kHz + SWB_OFFSET_LONG_960_88200_96000, // 1 = 88.2 kHz + SWB_OFFSET_LONG_960_64000, // 2 = 64 kHz + SWB_OFFSET_LONG_960_44100_48000, // 3 = 48 kHz + SWB_OFFSET_LONG_960_44100_48000, // 4 = 44.1 kHz + SWB_OFFSET_LONG_960_32000, // 5 = 32 kHz + SWB_OFFSET_LONG_960_22050_24000, // 6 = 24 kHz + SWB_OFFSET_LONG_960_22050_24000, // 7 = 22.05 kHz + SWB_OFFSET_LONG_960_11025_12000_16000, // 8 = 16 kHz + SWB_OFFSET_LONG_960_11025_12000_16000, // 9 = 12 kHz + SWB_OFFSET_LONG_960_11025_12000_16000, // 10 = 11.025 kHz + SWB_OFFSET_LONG_960_8000, // 11 = 8 kHz + &[], // 12 = 7350 Hz (no SWB table) +]; + +/// 120-line short-window offset tables keyed by +/// `samplingFrequencyIndex` (the bracketed Tables 4.130–4.141 +/// columns). Same slot layout as [`SWB_OFFSET_SHORT_WINDOW`]. +pub const SWB_OFFSET_SHORT_WINDOW_120: [&[u16]; 13] = [ + SWB_OFFSET_SHORT_120_88200_96000, // 0 = 96 kHz + SWB_OFFSET_SHORT_120_88200_96000, // 1 = 88.2 kHz + SWB_OFFSET_SHORT_120_64000, // 2 = 64 kHz + SWB_OFFSET_SHORT_120_32000_44100_48000, // 3 = 48 kHz + SWB_OFFSET_SHORT_120_32000_44100_48000, // 4 = 44.1 kHz + SWB_OFFSET_SHORT_120_32000_44100_48000, // 5 = 32 kHz + SWB_OFFSET_SHORT_120_22050_24000, // 6 = 24 kHz + SWB_OFFSET_SHORT_120_22050_24000, // 7 = 22.05 kHz + SWB_OFFSET_SHORT_120_11025_12000_16000, // 8 = 16 kHz + SWB_OFFSET_SHORT_120_11025_12000_16000, // 9 = 12 kHz + SWB_OFFSET_SHORT_120_11025_12000_16000, // 10 = 11.025 kHz + SWB_OFFSET_SHORT_120_8000, // 11 = 8 kHz + &[], // 12 = 7350 Hz (no SWB table) +]; + +// --------------------------------------------------------------------------- +// ER AAC LD tables — §4.5.4 Tables 4.142–4.147 (window lengths 960 +// and 1024, i.e. LD frame sizes 480 and 512). +// --------------------------------------------------------------------------- + +/// Table 4.143 — LD 512-line frame at 44.1 / 48 kHz (36 SWB). 37 +/// entries. +const SWB_OFFSET_LD_512_44100_48000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 68, 76, 84, 92, 100, 112, 124, + 136, 148, 164, 184, 208, 236, 268, 300, 332, 364, 396, 428, 460, 512, +]; + +/// Table 4.145 — LD 512-line frame at 32 kHz (37 SWB). 38 entries. +const SWB_OFFSET_LD_512_32000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, + 144, 160, 176, 192, 212, 236, 260, 288, 320, 352, 384, 416, 448, 480, 512, +]; + +/// Table 4.147 — LD 512-line frame at 22.05 / 24 kHz (31 SWB). 32 +/// entries. +const SWB_OFFSET_LD_512_22050_24000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 80, 92, 104, 120, 140, 164, 192, 224, + 256, 288, 320, 352, 384, 416, 448, 480, 512, +]; + +/// Table 4.142 — LD 480-line frame at 44.1 / 48 kHz (35 SWB). 36 +/// entries. +const SWB_OFFSET_LD_480_44100_48000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, + 144, 156, 172, 188, 212, 240, 272, 304, 336, 368, 400, 432, 480, +]; + +/// Table 4.144 — LD 480-line frame at 32 kHz (37 SWB). 38 entries. +const SWB_OFFSET_LD_480_32000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 124, + 136, 148, 164, 180, 200, 224, 256, 288, 320, 352, 384, 416, 448, 480, +]; + +/// Table 4.146 — LD 480-line frame at 22.05 / 24 kHz (30 SWB). 31 +/// entries. +const SWB_OFFSET_LD_480_22050_24000: &[u16] = &[ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 80, 92, 104, 120, 140, 164, 192, 224, + 256, 288, 320, 352, 384, 416, 448, 480, +]; + +/// Map a `samplingFrequencyIndex` onto the LD table column that +/// covers it. +/// +/// Tables 4.142–4.147 only define the 48 / 44.1 / 32 / 24 / 22.05 kHz +/// rates. Per §4.5.1.1 ("if in a certain sampling frequency dependent +/// table a sampling frequency stated in the right column of Table +/// 4.82 is not defined, the nearest defined table shall be used"), +/// every higher rate resolves to the 48 kHz table (48 000 is the +/// nearest defined rate for 96 / 88.2 / 64 kHz) and every lower rate +/// to the 22.05 kHz table (22 050 is the nearest defined rate for +/// 16 / 12 / 11.025 / 8 kHz). +fn ld_table_slot(fs_index: u8) -> Result { + match fs_index { + 0..=4 => Ok(0), // 96 / 88.2 / 64 / 48 / 44.1 kHz → 44.1/48 table + 5 => Ok(1), // 32 kHz + 6..=11 => Ok(2), // 24 / 22.05 kHz + nearest-rule lower rates + other => Err(Error::IcsInfoUnsupportedSampleRateIndex(other)), + } +} + +/// LD 512-line tables in [`ld_table_slot`] order. +const SWB_OFFSET_LD_512: [&[u16]; 3] = [ + SWB_OFFSET_LD_512_44100_48000, + SWB_OFFSET_LD_512_32000, + SWB_OFFSET_LD_512_22050_24000, +]; + +/// LD 480-line tables in [`ld_table_slot`] order. +const SWB_OFFSET_LD_480: [&[u16]; 3] = [ + SWB_OFFSET_LD_480_44100_48000, + SWB_OFFSET_LD_480_32000, + SWB_OFFSET_LD_480_22050_24000, +]; + +/// Family-aware `swb_offset_long_window[fs_index]` lookup. +/// +/// Dispatches on the [`FrameFamily`]: `Lc1024` reads the Tables +/// 4.129–4.140 primary columns (== [`long_window_offsets`]), `Lc960` +/// their bracketed 1920-transform columns, and the LD families the +/// dedicated Tables 4.142–4.147 (with the §4.5.1.1 nearest-defined- +/// table rule for rates those tables omit). +pub fn long_window_offsets_family(family: FrameFamily, fs_index: u8) -> Result<&'static [u16]> { + match family { + FrameFamily::Lc1024 => long_window_offsets(fs_index), + FrameFamily::Lc960 => { + let idx = fs_index as usize; + let slice = SWB_OFFSET_LONG_WINDOW_960 + .get(idx) + .copied() + .unwrap_or(&[][..]); + if slice.is_empty() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + Ok(slice) + } + FrameFamily::Ld512 => Ok(SWB_OFFSET_LD_512[ld_table_slot(fs_index)?]), + FrameFamily::Ld480 => Ok(SWB_OFFSET_LD_480[ld_table_slot(fs_index)?]), + } +} + +/// Family-aware `swb_offset_short_window[fs_index]` lookup. +/// +/// `Lc1024` reads the Tables 4.130–4.141 primary columns +/// (== [`short_window_offsets`]), `Lc960` their bracketed +/// 240-transform columns. The LD families have no short windows at +/// all (§4.6.17.2.2 — no block switching), so the lookup itself is +/// invalid and surfaces [`Error::LdShortWindow`]. +pub fn short_window_offsets_family(family: FrameFamily, fs_index: u8) -> Result<&'static [u16]> { + match family { + FrameFamily::Lc1024 => short_window_offsets(fs_index), + FrameFamily::Lc960 => { + let idx = fs_index as usize; + let slice = SWB_OFFSET_SHORT_WINDOW_120 + .get(idx) + .copied() + .unwrap_or(&[][..]); + if slice.is_empty() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + Ok(slice) + } + FrameFamily::Ld512 | FrameFamily::Ld480 => Err(Error::LdShortWindow), + } +} + +/// Apply the §4.6.13 pulse-escape reconstruction to a long-window +/// quantised spectrum. +/// +/// The decoder pseudocode in ISO/IEC 14496-3 §4.6.13 is: +/// +/// ```text +/// if (pulse_data_present) { +/// k = swb_offset_long_window[fs_index][pulse_start_sfb]; +/// for (i = 0; i < number_pulse + 1; i++) { +/// k += pulse_offset[i]; +/// if (x_quant[k] > 0) +/// x_quant[k] += pulse_amp[i]; +/// else +/// x_quant[k] -= pulse_amp[i]; +/// } +/// } +/// ``` +/// +/// `x_quant` is the per-coefficient quantised spectrum from +/// `spectral_data()`; pulse fix-ups overwrite the residual the encoder +/// shaved off the literal escape codeword. +/// +/// ## Inputs +/// +/// * `x_quant` — `&mut [i32]`, length must be at least +/// [`LONG_WINDOW_LEN`] (1024). Note: §4.4.6.3 normatively forbids +/// `pulse_data_present` on `EIGHT_SHORT_SEQUENCE` frames, so the +/// only window-sequence context this loop runs on is long +/// (long / long_start / long_stop). The short-window spectrum is +/// never touched. +/// * `fs_index` — `samplingFrequencyIndex` (Table 1.18, 0..=11). Selects +/// `swb_offset_long_window[fs_index]`. +/// * `pulse_data` — the parsed [`PulseData`] block. `pulses` must be in +/// `1..=4`; `pulse_start_sfb` must be in +/// `0..long_window_offsets(fs_index).len() - 1` (i.e. addressable +/// without going past the last real band). +/// +/// ## Errors +/// +/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] if `fs_index` has no +/// long-window SWB table. +/// * [`Error::PulseDataEncodeInvalid`] if: +/// * `pulse_data.pulses.is_empty()` or `> 4` (Table 4.7 cap), +/// * `pulse_data.pulse_start_sfb` indexes past the last real +/// scalefactor band (`>= long_offsets.len() - 1`), +/// * the running coefficient index `k` reaches or exceeds +/// [`LONG_WINDOW_LEN`] (the per-pulse offset accumulation runs off +/// the end of the spectrum). All three checks correspond to +/// conditions a conforming AAC encoder will never produce — this +/// surfaces malformed bitstreams or caller bugs. +/// * `x_quant.len() < LONG_WINDOW_LEN` panics in debug, saturates the +/// slice length in release. (The caller is expected to pass a +/// correctly-sized buffer; misuse here is a programming error, not +/// a wire-format violation.) +pub fn apply_pulse_data(x_quant: &mut [i32], fs_index: u8, pulse_data: &PulseData) -> Result<()> { + apply_pulse_data_family(x_quant, FrameFamily::Lc1024, fs_index, pulse_data) +} + +/// [`apply_pulse_data`] generalized to any [`FrameFamily`]: the band +/// start `k` is read from the family's own long-window table and the +/// running index is bounded by the family's long spectrum length. +pub fn apply_pulse_data_family( + x_quant: &mut [i32], + family: FrameFamily, + fs_index: u8, + pulse_data: &PulseData, +) -> Result<()> { + // A corrupted stream can pair a pulse_data_present flag with a + // group buffer shorter than the family frame length (e.g. a + // flipped window_sequence bit) — reject rather than assert. + if x_quant.len() < family.frame_len() { + return Err(Error::PulseDataEncodeInvalid); + } + + if pulse_data.pulses.is_empty() || pulse_data.pulses.len() > crate::pulse_data::MAX_PULSES { + return Err(Error::PulseDataEncodeInvalid); + } + + let offsets = long_window_offsets_family(family, fs_index)?; + let start_sfb = pulse_data.pulse_start_sfb as usize; + // Last entry of the offsets slice is the sentinel; bands are + // addressable at indices 0..offsets.len() - 1. + if start_sfb >= offsets.len() - 1 { + return Err(Error::PulseDataEncodeInvalid); + } + + let mut k = offsets[start_sfb] as usize; + let len = x_quant.len().min(family.frame_len()); + for pulse in &pulse_data.pulses { + k += pulse.offset as usize; + if k >= len { + return Err(Error::PulseDataEncodeInvalid); + } + let amp = pulse.amp as i32; + if x_quant[k] > 0 { + x_quant[k] += amp; + } else { + x_quant[k] -= amp; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn num_swb_long_window() -> [u8; 12] { + // Mirror NUM_SWB_LONG_WINDOW in src/ics_info.rs without + // referencing it from outside the module under test. + [41, 41, 47, 49, 49, 51, 47, 47, 43, 43, 43, 40] + } + + fn num_swb_short_window() -> [u8; 12] { + [12, 12, 12, 14, 14, 14, 15, 15, 15, 15, 15, 15] + } + + #[test] + fn long_offset_lengths_match_num_swb() { + let counts = num_swb_long_window(); + for fs_index in 0..12_u8 { + let offsets = long_window_offsets(fs_index).unwrap(); + assert_eq!( + offsets.len(), + counts[fs_index as usize] as usize + 1, + "fs_index {} long-window offset table length", + fs_index, + ); + } + } + + #[test] + fn short_offset_lengths_match_num_swb() { + let counts = num_swb_short_window(); + for fs_index in 0..12_u8 { + let offsets = short_window_offsets(fs_index).unwrap(); + assert_eq!( + offsets.len(), + counts[fs_index as usize] as usize + 1, + "fs_index {} short-window offset table length", + fs_index, + ); + } + } + + #[test] + fn long_tables_start_at_zero_and_end_at_1024() { + for fs_index in 0..12_u8 { + let offsets = long_window_offsets(fs_index).unwrap(); + assert_eq!(offsets[0], 0, "fs_index {} first offset", fs_index); + assert_eq!( + *offsets.last().unwrap(), + LONG_WINDOW_LEN, + "fs_index {} sentinel", + fs_index + ); + } + } + + #[test] + fn short_tables_start_at_zero_and_end_at_128() { + for fs_index in 0..12_u8 { + let offsets = short_window_offsets(fs_index).unwrap(); + assert_eq!(offsets[0], 0, "fs_index {} first offset", fs_index); + assert_eq!( + *offsets.last().unwrap(), + SHORT_WINDOW_LEN, + "fs_index {} sentinel", + fs_index + ); + } + } + + #[test] + fn long_offsets_are_strictly_monotonic() { + for fs_index in 0..12_u8 { + let offsets = long_window_offsets(fs_index).unwrap(); + for w in offsets.windows(2) { + assert!( + w[0] < w[1], + "fs_index {} non-monotonic at {} -> {}", + fs_index, + w[0], + w[1] + ); + } + } + } + + #[test] + fn short_offsets_are_strictly_monotonic() { + for fs_index in 0..12_u8 { + let offsets = short_window_offsets(fs_index).unwrap(); + for w in offsets.windows(2) { + assert!( + w[0] < w[1], + "fs_index {} non-monotonic at {} -> {}", + fs_index, + w[0], + w[1] + ); + } + } + } + + #[test] + fn fs_index_7350_returns_unsupported() { + assert!(matches!( + long_window_offsets(12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + assert!(matches!( + short_window_offsets(12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + } + + #[test] + fn fs_index_out_of_range_returns_unsupported() { + assert!(matches!( + long_window_offsets(13), + Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) + )); + assert!(matches!( + long_window_offsets(15), + Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) + )); + assert!(matches!( + short_window_offsets(15), + Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) + )); + } + + #[test] + fn table_4_129_spot_check_48k() { + // Table 4.129 — 44.1 / 48 kHz long window. 50 entries. + let offsets = long_window_offsets(3).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[1], 4); + assert_eq!(offsets[10], 40); + assert_eq!(offsets[11], 48); + assert_eq!(offsets[24], 196); + assert_eq!(offsets[49], 1024); + assert_eq!(offsets.len(), 50); + } + + #[test] + fn table_4_131_spot_check_32k() { + // Table 4.131 — 32 kHz long window. 52 entries. + let offsets = long_window_offsets(5).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[50], 992); + assert_eq!(offsets[51], 1024); + assert_eq!(offsets.len(), 52); + } + + #[test] + fn table_4_132_spot_check_8k() { + // Table 4.132 — 8 kHz long window. 41 entries. + let offsets = long_window_offsets(11).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[1], 12); + assert_eq!(offsets[20], 268); + assert_eq!(offsets[40], 1024); + assert_eq!(offsets.len(), 41); + } + + #[test] + fn table_4_134_spot_check_16k() { + // Table 4.134 — 11.025 / 12 / 16 kHz long window. 44 entries. + let offsets = long_window_offsets(8).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[1], 8); + assert_eq!(offsets[21], 212); + assert_eq!(offsets[22], 228); + assert_eq!(offsets[43], 1024); + assert_eq!(offsets.len(), 44); + // Same table also covers 12 kHz (fs 9) and 11.025 kHz (fs 10). + assert_eq!(long_window_offsets(9).unwrap(), offsets); + assert_eq!(long_window_offsets(10).unwrap(), offsets); + } + + #[test] + fn table_4_136_spot_check_24k() { + // Table 4.136 — 22.05 / 24 kHz long window. 48 entries. + let offsets = long_window_offsets(6).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[11], 44); + assert_eq!(offsets[12], 52); + assert_eq!(offsets[23], 148); + assert_eq!(offsets[24], 160); + assert_eq!(offsets[47], 1024); + assert_eq!(offsets.len(), 48); + assert_eq!(long_window_offsets(7).unwrap(), offsets); + } + + #[test] + fn table_4_138_spot_check_64k() { + // Table 4.138 — 64 kHz long window. 48 entries. + let offsets = long_window_offsets(2).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[14], 56); + assert_eq!(offsets[15], 64); + assert_eq!(offsets[22], 140); + assert_eq!(offsets[46], 984); + assert_eq!(offsets[47], 1024); + assert_eq!(offsets.len(), 48); + } + + #[test] + fn table_4_140_spot_check_96k() { + // Table 4.140 — 88.2 / 96 kHz long window. 42 entries. + let offsets = long_window_offsets(0).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[20], 108); + assert_eq!(offsets[21], 120); + assert_eq!(offsets[41], 1024); + assert_eq!(offsets.len(), 42); + assert_eq!(long_window_offsets(1).unwrap(), offsets); + } + + #[test] + fn table_4_130_spot_check_48k_short() { + // Table 4.130 — 32 / 44.1 / 48 kHz short window. 15 entries. + let offsets = short_window_offsets(3).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[5], 20); + assert_eq!(offsets[6], 28); + assert_eq!(offsets[14], 128); + assert_eq!(offsets.len(), 15); + // Shared with 44.1 and 32 kHz. + assert_eq!(short_window_offsets(4).unwrap(), offsets); + assert_eq!(short_window_offsets(5).unwrap(), offsets); + } + + #[test] + fn table_4_133_spot_check_8k_short() { + // Table 4.133 — 8 kHz short window. 16 entries. + let offsets = short_window_offsets(11).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[7], 28); + assert_eq!(offsets[8], 36); + assert_eq!(offsets[15], 128); + assert_eq!(offsets.len(), 16); + } + + #[test] + fn table_4_135_spot_check_16k_short() { + // Table 4.135 — 11.025 / 12 / 16 kHz short window. 16 entries. + let offsets = short_window_offsets(8).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[8], 32); + assert_eq!(offsets[9], 40); + assert_eq!(offsets[15], 128); + assert_eq!(offsets.len(), 16); + assert_eq!(short_window_offsets(9).unwrap(), offsets); + assert_eq!(short_window_offsets(10).unwrap(), offsets); + } + + #[test] + fn table_4_137_spot_check_24k_short() { + // Table 4.137 — 22.05 / 24 kHz short window. 16 entries. + let offsets = short_window_offsets(6).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[7], 28); + assert_eq!(offsets[8], 36); + assert_eq!(offsets[11], 64); + assert_eq!(offsets[15], 128); + assert_eq!(offsets.len(), 16); + assert_eq!(short_window_offsets(7).unwrap(), offsets); + } + + #[test] + fn table_4_139_spot_check_64k_short() { + // Table 4.139 — 64 kHz short window. 13 entries. + let offsets = short_window_offsets(2).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[6], 24); + assert_eq!(offsets[7], 32); + assert_eq!(offsets[11], 92); + assert_eq!(offsets[12], 128); + assert_eq!(offsets.len(), 13); + } + + #[test] + fn table_4_141_spot_check_96k_short() { + // Table 4.141 — 88.2 / 96 kHz short window. 13 entries. + let offsets = short_window_offsets(0).unwrap(); + assert_eq!(offsets[0], 0); + assert_eq!(offsets[7], 32); + assert_eq!(offsets[12], 128); + assert_eq!(offsets.len(), 13); + assert_eq!(short_window_offsets(1).unwrap(), offsets); + } + + #[test] + fn apply_pulse_data_single_positive_pulse_48k() { + use crate::pulse_data::{Pulse, PulseData}; + // 48 kHz long, swb_offset_long[3] = 12, then a single pulse + // with offset=5 (k = 12 + 5 = 17), amp=3, on a positive + // x_quant: x_quant[17] += 3. + let mut x_quant = vec![0_i32; 1024]; + x_quant[17] = 7; + let pd = PulseData { + pulse_start_sfb: 3, + pulses: vec![Pulse { offset: 5, amp: 3 }], + }; + apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); + assert_eq!(x_quant[17], 10); + } + + #[test] + fn apply_pulse_data_single_negative_pulse_48k() { + use crate::pulse_data::{Pulse, PulseData}; + // x_quant <= 0 (incl. 0): amp is subtracted. + let mut x_quant = vec![0_i32; 1024]; + x_quant[17] = -7; + let pd = PulseData { + pulse_start_sfb: 3, + pulses: vec![Pulse { offset: 5, amp: 3 }], + }; + apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); + assert_eq!(x_quant[17], -10); + } + + #[test] + fn apply_pulse_data_zero_coefficient_subtracts_amp() { + use crate::pulse_data::{Pulse, PulseData}; + // Zero is not > 0, so it falls into the else branch and amp + // is subtracted (matching the §4.6.13 pseudocode). + let mut x_quant = vec![0_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 0, + pulses: vec![Pulse { offset: 1, amp: 4 }], + }; + apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); + assert_eq!(x_quant[1], -4); + } + + #[test] + fn apply_pulse_data_four_pulses_accumulate_k() { + use crate::pulse_data::{Pulse, PulseData}; + // 48 kHz long, swb_offset_long[10] = 40. Four pulses with + // offsets 1/2/3/4 land at k = 41, 43, 46, 50. All four target + // coefficients are set positive so each is incremented by its + // amplitude. + let mut x_quant = vec![1_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 10, + pulses: vec![ + Pulse { offset: 1, amp: 1 }, + Pulse { offset: 2, amp: 2 }, + Pulse { offset: 3, amp: 3 }, + Pulse { offset: 4, amp: 4 }, + ], + }; + apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); + assert_eq!(x_quant[41], 2); + assert_eq!(x_quant[43], 3); + assert_eq!(x_quant[46], 4); + assert_eq!(x_quant[50], 5); + } + + #[test] + fn apply_pulse_data_overrun_rejected() { + use crate::pulse_data::{Pulse, PulseData}; + // Pulse offset that drives k past 1024 is rejected. + let mut x_quant = vec![0_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 48, // 48 kHz, swb_offset_long[48] = 928 + pulses: vec![Pulse { offset: 31, amp: 0 }; 4], // 928 + 4*31 = 1052 + }; + assert!(matches!( + apply_pulse_data(&mut x_quant, 3, &pd), + Err(Error::PulseDataEncodeInvalid) + )); + } + + #[test] + fn apply_pulse_data_start_sfb_past_last_band_rejected() { + use crate::pulse_data::{Pulse, PulseData}; + // 48 kHz long has 49 SWB; addressable band indices are 0..=48. + let mut x_quant = vec![0_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 49, + pulses: vec![Pulse { offset: 1, amp: 1 }], + }; + assert!(matches!( + apply_pulse_data(&mut x_quant, 3, &pd), + Err(Error::PulseDataEncodeInvalid) + )); + } + + #[test] + fn apply_pulse_data_empty_pulses_rejected() { + use crate::pulse_data::PulseData; + let mut x_quant = vec![0_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 0, + pulses: vec![], + }; + assert!(matches!( + apply_pulse_data(&mut x_quant, 3, &pd), + Err(Error::PulseDataEncodeInvalid) + )); + } + + #[test] + fn apply_pulse_data_too_many_pulses_rejected() { + use crate::pulse_data::{Pulse, PulseData}; + let mut x_quant = vec![0_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 0, + pulses: vec![Pulse { offset: 1, amp: 1 }; 5], + }; + assert!(matches!( + apply_pulse_data(&mut x_quant, 3, &pd), + Err(Error::PulseDataEncodeInvalid) + )); + } + + #[test] + fn apply_pulse_data_unsupported_fs_index_rejected() { + use crate::pulse_data::{Pulse, PulseData}; + let mut x_quant = vec![0_i32; 1024]; + let pd = PulseData { + pulse_start_sfb: 0, + pulses: vec![Pulse { offset: 1, amp: 1 }], + }; + assert!(matches!( + apply_pulse_data(&mut x_quant, 12, &pd), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + } + + #[test] + fn long_widths_match_num_swb_sums() { + // sum of per-band widths must equal LONG_WINDOW_LEN for every + // table. + for fs_index in 0..12_u8 { + let offsets = long_window_offsets(fs_index).unwrap(); + let total: u32 = offsets.windows(2).map(|w| (w[1] - w[0]) as u32).sum(); + assert_eq!(total, LONG_WINDOW_LEN as u32); + } + } + + #[test] + fn short_widths_match_num_swb_sums() { + for fs_index in 0..12_u8 { + let offsets = short_window_offsets(fs_index).unwrap(); + let total: u32 = offsets.windows(2).map(|w| (w[1] - w[0]) as u32).sum(); + assert_eq!(total, SHORT_WINDOW_LEN as u32); + } + } + + // -- FrameFamily geometry ------------------------------------------------ + + #[test] + fn family_resolution_follows_4_5_1_1() { + assert_eq!( + FrameFamily::from_aot_and_flag(2, false), + FrameFamily::Lc1024 + ); + assert_eq!(FrameFamily::from_aot_and_flag(2, true), FrameFamily::Lc960); + assert_eq!(FrameFamily::from_aot_and_flag(17, true), FrameFamily::Lc960); + assert_eq!( + FrameFamily::from_aot_and_flag(23, false), + FrameFamily::Ld512 + ); + assert_eq!(FrameFamily::from_aot_and_flag(23, true), FrameFamily::Ld480); + } + + #[test] + fn family_lengths() { + assert_eq!(FrameFamily::Lc1024.frame_len(), 1024); + assert_eq!(FrameFamily::Lc1024.long_transform_len(), 2048); + assert_eq!(FrameFamily::Lc1024.short_window_len(), Some(128)); + assert_eq!(FrameFamily::Lc1024.short_transform_len(), Some(256)); + assert_eq!(FrameFamily::Lc960.frame_len(), 960); + assert_eq!(FrameFamily::Lc960.long_transform_len(), 1920); + assert_eq!(FrameFamily::Lc960.short_window_len(), Some(120)); + assert_eq!(FrameFamily::Lc960.short_transform_len(), Some(240)); + assert_eq!(FrameFamily::Ld512.frame_len(), 512); + assert_eq!(FrameFamily::Ld512.long_transform_len(), 1024); + assert_eq!(FrameFamily::Ld512.short_window_len(), None); + assert_eq!(FrameFamily::Ld480.frame_len(), 480); + assert_eq!(FrameFamily::Ld480.long_transform_len(), 960); + assert_eq!(FrameFamily::Ld480.short_window_len(), None); + assert!(FrameFamily::Ld512.is_ld()); + assert!(FrameFamily::Ld480.is_ld()); + assert!(!FrameFamily::Lc1024.is_ld()); + assert!(!FrameFamily::Lc960.is_ld()); + } + + #[test] + fn lc1024_family_lookup_matches_legacy_accessors() { + for fs_index in 0..12_u8 { + assert_eq!( + long_window_offsets_family(FrameFamily::Lc1024, fs_index).unwrap(), + long_window_offsets(fs_index).unwrap() + ); + assert_eq!( + short_window_offsets_family(FrameFamily::Lc1024, fs_index).unwrap(), + short_window_offsets(fs_index).unwrap() + ); + } + } + + #[test] + fn lc960_long_tables_are_the_bracketed_columns() { + // Tables 4.129–4.140: the 1920-transform column shares every + // band start with the 2048 column; the sentinel becomes 960 + // and any offsets >= 960 are dropped. So each 960 table must + // be a strict prefix of its 1024 sibling with the sentinel + // replaced by 960. + for fs_index in 0..12_u8 { + let long1024 = long_window_offsets(fs_index).unwrap(); + let long960 = long_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); + let n = long960.len(); + assert_eq!(*long960.last().unwrap(), 960, "fs {} sentinel", fs_index); + assert_eq!( + &long960[..n - 1], + &long1024[..n - 1], + "fs {} shared band starts", + fs_index + ); + // Everything dropped from the 1024 table must be >= 960. + for &off in &long1024[n - 1..] { + assert!(off >= 960, "fs {} dropped offset {}", fs_index, off); + } + // Strictly monotonic, starts at zero. + assert_eq!(long960[0], 0); + for w in long960.windows(2) { + assert!(w[0] < w[1], "fs {} non-monotonic", fs_index); + } + } + } + + #[test] + fn lc960_expected_num_swb() { + // Bracket-derived band counts: 44.1/48 keeps all 49 bands + // (only the sentinel shrinks); 32 kHz drops from 51 to 49; + // 64 kHz prints `47 (46)` in Table 4.138; the rest drop + // exactly the bands whose start would be >= 960. + let expected: [usize; 12] = [40, 40, 46, 49, 49, 49, 46, 46, 42, 42, 42, 40]; + for fs_index in 0..12_u8 { + let long960 = long_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); + assert_eq!( + long960.len() - 1, + expected[fs_index as usize], + "fs {} num_swb", + fs_index + ); + } + } + + #[test] + fn lc960_short_tables_only_rescale_the_sentinel() { + for fs_index in 0..12_u8 { + let short128 = short_window_offsets(fs_index).unwrap(); + let short120 = short_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); + assert_eq!(short120.len(), short128.len(), "fs {}", fs_index); + let n = short120.len(); + assert_eq!(&short120[..n - 1], &short128[..n - 1]); + assert_eq!(short120[n - 1], 120); + for w in short120.windows(2) { + assert!(w[0] < w[1], "fs {} non-monotonic", fs_index); + } + } + } + + #[test] + fn ld_tables_match_spec_counts_and_sentinels() { + // Table 4.143 / 4.145 / 4.147 — LD 512: 36 / 37 / 31 SWB. + for (fs, num) in [(3u8, 36usize), (4, 36), (5, 37), (6, 31), (7, 31)] { + let t = long_window_offsets_family(FrameFamily::Ld512, fs).unwrap(); + assert_eq!(t.len() - 1, num, "LD512 fs {}", fs); + assert_eq!(t[0], 0); + assert_eq!(*t.last().unwrap(), 512); + for w in t.windows(2) { + assert!(w[0] < w[1]); + } + } + // Table 4.142 / 4.144 / 4.146 — LD 480: 35 / 37 / 30 SWB. + for (fs, num) in [(3u8, 35usize), (4, 35), (5, 37), (6, 30), (7, 30)] { + let t = long_window_offsets_family(FrameFamily::Ld480, fs).unwrap(); + assert_eq!(t.len() - 1, num, "LD480 fs {}", fs); + assert_eq!(t[0], 0); + assert_eq!(*t.last().unwrap(), 480); + for w in t.windows(2) { + assert!(w[0] < w[1]); + } + } + } + + #[test] + fn ld_512_spot_checks() { + // Table 4.143 spot rows: swb 16 -> 68, swb 21 -> 112, + // swb 27 -> 208, swb 35 -> 460. + let t = long_window_offsets_family(FrameFamily::Ld512, 3).unwrap(); + assert_eq!(t[16], 68); + assert_eq!(t[21], 112); + assert_eq!(t[27], 208); + assert_eq!(t[35], 460); + // Table 4.145 spot rows: swb 15 -> 64, swb 20 -> 108, + // swb 30 -> 288, swb 36 -> 480. + let t = long_window_offsets_family(FrameFamily::Ld512, 5).unwrap(); + assert_eq!(t[15], 64); + assert_eq!(t[20], 108); + assert_eq!(t[30], 288); + assert_eq!(t[36], 480); + // Table 4.147 spot rows: swb 12 -> 52, swb 18 -> 120, + // swb 25 -> 320, swb 30 -> 480. + let t = long_window_offsets_family(FrameFamily::Ld512, 6).unwrap(); + assert_eq!(t[12], 52); + assert_eq!(t[18], 120); + assert_eq!(t[25], 320); + assert_eq!(t[30], 480); + } + + #[test] + fn ld_480_spot_checks() { + // Table 4.142 spot rows: swb 15 -> 64, swb 20 -> 108, + // swb 27 -> 212, swb 34 -> 432. + let t = long_window_offsets_family(FrameFamily::Ld480, 4).unwrap(); + assert_eq!(t[15], 64); + assert_eq!(t[20], 108); + assert_eq!(t[27], 212); + assert_eq!(t[34], 432); + // Table 4.144 spot rows: swb 17 -> 72, swb 23 -> 124, + // swb 29 -> 224, swb 36 -> 448. + let t = long_window_offsets_family(FrameFamily::Ld480, 5).unwrap(); + assert_eq!(t[17], 72); + assert_eq!(t[23], 124); + assert_eq!(t[29], 224); + assert_eq!(t[36], 448); + // Table 4.146 spot rows: swb 12 -> 52, swb 16 -> 92, + // swb 22 -> 224, swb 29 -> 448. + let t = long_window_offsets_family(FrameFamily::Ld480, 7).unwrap(); + assert_eq!(t[12], 52); + assert_eq!(t[16], 92); + assert_eq!(t[22], 224); + assert_eq!(t[29], 448); + } + + #[test] + fn ld_nearest_defined_table_rule() { + // §4.5.1.1: rates the LD tables omit resolve to the nearest + // defined rate — 96/88.2/64 kHz to the 48 kHz table, 16 kHz + // and below to the 22.05 kHz table. + let t48 = long_window_offsets_family(FrameFamily::Ld512, 3).unwrap(); + for fs in [0u8, 1, 2, 4] { + assert_eq!( + long_window_offsets_family(FrameFamily::Ld512, fs).unwrap(), + t48 + ); + } + let t22 = long_window_offsets_family(FrameFamily::Ld512, 7).unwrap(); + for fs in [6u8, 8, 9, 10, 11] { + assert_eq!( + long_window_offsets_family(FrameFamily::Ld512, fs).unwrap(), + t22 + ); + } + assert!(matches!( + long_window_offsets_family(FrameFamily::Ld512, 12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + } + + #[test] + fn ld_short_lookup_is_rejected() { + assert!(matches!( + short_window_offsets_family(FrameFamily::Ld512, 3), + Err(Error::LdShortWindow) + )); + assert!(matches!( + short_window_offsets_family(FrameFamily::Ld480, 3), + Err(Error::LdShortWindow) + )); + } + + #[test] + fn family_widths_sum_to_family_lengths() { + for family in [FrameFamily::Lc960, FrameFamily::Ld512, FrameFamily::Ld480] { + for fs_index in 0..12_u8 { + let long = long_window_offsets_family(family, fs_index).unwrap(); + assert_eq!( + *long.last().unwrap() as usize, + family.frame_len(), + "{:?} fs {} long sentinel", + family, + fs_index + ); + } + } + for fs_index in 0..12_u8 { + let short = short_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); + assert_eq!(*short.last().unwrap(), 120); + } + } + + #[test] + fn apply_pulse_data_family_uses_family_bounds() { + use crate::pulse_data::{Pulse, PulseData}; + // LD512 at 48 kHz: swb_offset[35] == 460 is the last band. + // A pulse landing at 460 + 40 = 500 stays inside the 512-line + // spectrum, while the same pulse under a 1024-line check + // would also pass — so also verify the overrun at >= 512. + let mut x_quant = vec![1_i32; 512]; + let pd = PulseData { + pulse_start_sfb: 35, + pulses: vec![Pulse { offset: 31, amp: 2 }], + }; + apply_pulse_data_family(&mut x_quant, FrameFamily::Ld512, 3, &pd).unwrap(); + assert_eq!(x_quant[491], 3); + + let mut x_quant = vec![1_i32; 512]; + let pd = PulseData { + pulse_start_sfb: 35, + pulses: vec![Pulse { offset: 31, amp: 2 }; 2], // 460+62 = 522 >= 512 + }; + assert!(matches!( + apply_pulse_data_family(&mut x_quant, FrameFamily::Ld512, 3, &pd), + Err(Error::PulseDataEncodeInvalid) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/tns_coef.rs b/crates/vendor/oxideav-aac/src/tns_coef.rs new file mode 100644 index 00000000..b2542358 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/tns_coef.rs @@ -0,0 +1,1239 @@ +//! TNS coefficient inverse-quantisation and LPC step-up — ISO/IEC +//! 14496-3 §4.6.9.3 (`tns_decode_coef` pseudo-code) plus the +//! ISO/IEC 14496-3:2001 §C.6 encoder-side quantisation companion. +//! +//! Temporal Noise Shaping carries one all-pole filter per scalefactor +//! region. The wire `coef[i]` slots produced by [`crate::tns_data`] +//! hold the per-coefficient *quantised reflection (PARCOR)* index in +//! `coef_bits` (2..=4) of unsigned magnitude with the high bit acting +//! as a sign flag (signed-magnitude / two's-complement padding). The +//! decoder reconstructs the floating-point reflection coefficient +//! `rq[i]` by: +//! +//! 1. Sign-extending the truncated wire value to a normal signed int. +//! 2. Inverse-quantising with `sin(index / iqfac)` where `iqfac` +//! depends on the sign of `index` (`iqfac` for non-negative, +//! `iqfac_m` for negative — the half-bit offset matches the +//! encoder's rounded quantisation). +//! 3. Running the §4.6.9.3 *conversion-to-LPC* step-up loop that +//! converts the order-`order` PARCOR array into an order-`order` +//! direct-form LPC `a[]` vector with `a[0] = 1`. +//! +//! The encoder side (§C.6) inverts steps (1) and (2): given the +//! floating-point reflection coefficients computed by Levinson-Durbin, +//! quantise via `NINT(arcsin(r) * iqfac)`, where `iqfac` again branches +//! on the sign of `r`. The step-up loop is unchanged — both encoder and +//! decoder run it to derive the same `a[]` array that drives the +//! `tns_ar_filter()` / inverse FIR pass. +//! +//! ## §4.6.9.3 pseudocode (transcribed for cross-check) +//! +//! ```text +//! tns_decode_coef( order, coef_res_bits, coef_compress, coef[], a[] ) +//! { +//! sgn_mask[] = { 0x2, 0x4, 0x8 }; +//! neg_mask[] = { ~0x3, ~0x7, ~0xf }; +//! +//! coef_res2 = coef_res_bits - coef_compress; +//! s_mask = sgn_mask[ coef_res2 - 2 ]; +//! n_mask = neg_mask[ coef_res2 - 2 ]; +//! +//! for (i = 0; i < order; i++) +//! tmp[i] = (coef[i] & s_mask) ? (coef[i] | n_mask) : coef[i]; +//! +//! iqfac = ((1 << (coef_res_bits-1)) - 0.5) / (π/2.0); +//! iqfac_m = ((1 << (coef_res_bits-1)) + 0.5) / (π/2.0); +//! for (i = 0; i < order; i++) { +//! tmp2[i] = sin( tmp[i] / ((tmp[i] >= 0) ? iqfac : iqfac_m) ); +//! } +//! +//! a[0] = 1; +//! for (m = 1; m <= order; m++) { +//! for (i = 1; i < m; i++) +//! b[i] = a[i] + tmp2[m-1] * a[m-i]; +//! for (i = 1; i < m; i++) +//! a[i] = b[i]; +//! a[m] = tmp2[m-1]; +//! } +//! } +//! ``` +//! +//! The `sgn_mask` / `neg_mask` pair encode the two's-complement +//! sign-extension of a `coef_res2`-bit field (`coef_res2 ∈ {2, 3, 4}`). +//! `s_mask` is `1 << (coef_res2 - 1)` (the MSB of the truncated field) +//! and `n_mask` is `~((1 << coef_res2) - 1)` (the bits that need to be +//! filled with 1 to extend a negative value into a normal signed int). +//! +//! ## What this module covers +//! +//! * [`iqfac`] / [`iqfac_m`] — the §4.6.9.3 quantiser scale factors +//! `((1 << (n-1)) ± 0.5) / (π/2)`. Exposed as standalone helpers so +//! the §C.6 encoder path can re-use the same constants. +//! * [`sign_extend_coef`] — inverse of `(coef & s_mask) ? coef | +//! n_mask : coef`. Takes a wire `coef` (held in the low `coef_res2` +//! bits as transmitted) and a `coef_res2 ∈ {2, 3, 4}` field width; +//! returns the matching signed integer. +//! * [`tns_decode_coef`] — the full §4.6.9.3 path: wire `coef[]` → +//! floating-point `tmp2[]` (the inverse-quantised PARCOR +//! coefficients). +//! * [`tns_encode_coef`] — the §C.6 inverse: floating-point reflection +//! coefficients → wire `coef[]` values ready for [`crate::tns_data`]. +//! * [`lpc_step_up`] — the §4.6.9.3 *conversion-to-LPC* loop. Takes a +//! slice of inverse-quantised PARCOR `tmp2[]` values and returns the +//! `order + 1` direct-form LPC `a[]` vector with `a[0] = 1.0`. +//! * [`tns_decode_coef_to_lpc`] — convenience wrapper that runs +//! [`tns_decode_coef`] followed by [`lpc_step_up`]; the +//! reconstruction loop will call this once per `(window, filter)` +//! pair. +//! * [`tns_ar_filter`] — the §4.6.9.3 `tns_ar_filter()` all-pole IIR +//! pass. Operates in place over a strided region of the dequantised +//! spectrum (`start` / `size` / `inc`) driven by the `lpc[]` array +//! from [`lpc_step_up`]. Filter state is zero-seeded per +//! invocation, exactly as the spec mandates. +//! +//! ## What this module does *not* cover +//! +//! * The §4.6.9 `tns_decode_frame()` orchestration that dispatches +//! `tns_decode_coef_to_lpc` / `tns_ar_filter` per filter per window. +//! That orchestration is the responsibility of the eventual +//! `individual_channel_stream()` reconstruction driver. +//! * The §4.6.17.3.4 ER AAC LD `int_tns_decode_coef()` integer +//! variant. The LD path uses a fixed-point arithmetic surface that +//! we do not need until the AAC LD reconstruction path is wired. +//! * The §C.6 Levinson-Durbin / autocorrelation reflection-coefficient +//! derivation. The encoder gets floating-point PARCOR coefficients +//! from some upstream LPC estimator (a standard speech-coding +//! procedure); this module accepts the already-derived `r[]` array +//! and quantises it. +//! +//! ## Numerical contract +//! +//! Both `iqfac` and `iqfac_m` are exactly representable as `f64` for +//! every legal `coef_res_bits ∈ {3, 4}`: +//! +//! | coef_res_bits | iqfac (≈) | iqfac_m (≈) | +//! |---------------|-------------------------|-------------------------| +//! | 3 | `3.5 / (π/2) ≈ 2.228...`| `4.5 / (π/2) ≈ 2.864...`| +//! | 4 | `7.5 / (π/2) ≈ 4.774...`| `8.5 / (π/2) ≈ 5.411...`| +//! +//! The encoder's `NINT(arcsin(r) * iqfac)` rounding is implemented via +//! `f64::round` (round-half-away-from-zero, matching the spec's `NINT` +//! convention). A reflection coefficient `r = 0.0` quantises to +//! `index = 0` (the `iqfac` branch is taken because `r >= 0`); the +//! decoder then reconstructs `sin(0 / iqfac) = 0.0`. Likewise the +//! sentinel `r = 1.0` rounds to the field maximum (`6` for +//! `coef_res2 = 3`, `7` for `coef_res2 = 4` after `coef_compress = 0`) +//! and `r = -1.0` rounds to the field minimum (`-7` / `-8`); both +//! recover via `sin(±π/2) = ±1.0` to within IEEE-754 round-off +//! (`|round-trip error| < 1e-15`). All in-range PARCOR values quantise +//! cleanly without saturation; an out-of-range `r` (`|r| > 1.0`) is +//! rejected by [`tns_encode_coef`] with +//! [`Error::TnsCoefOutOfRange`] because `arcsin` is undefined there. +//! +//! The signed-magnitude wire fold preserves round-trip: every legal +//! sign-extended index `i` in `[-(1 << (coef_res2-1)), +//! (1 << (coef_res2-1)) - 1]` maps back to a unique `coef_res2`-bit +//! pattern. The step-up loop is exact (no quantisation), so the same +//! quantised PARCOR array always yields bit-identical LPC coefficients. + +use core::f64::consts::PI; + +use crate::{Error, Result}; + +/// Half-π. Cached so the [`iqfac`] / [`iqfac_m`] arithmetic matches +/// the spec's literal `π/2.0` division. +const HALF_PI: f64 = PI / 2.0; + +/// `iqfac` per §4.6.9.3. Branches on a *non-negative* index / PARCOR +/// value. Defined as `((1 << (coef_res_bits-1)) - 0.5) / (π/2)`. +/// +/// Returns [`Error::TnsCoefOutOfRange`] when `coef_res_bits` lies +/// outside `3..=4` (the legal `coef_res[w] + 3` values per +/// §4.6.9.3 and §C.6, where `coef_res[w] ∈ {0, 1}` is the wire flag). +pub fn iqfac(coef_res_bits: u32) -> Result { + if !(3..=4).contains(&coef_res_bits) { + return Err(Error::TnsCoefOutOfRange); + } + let scale = (1u32 << (coef_res_bits - 1)) as f64 - 0.5; + Ok(scale / HALF_PI) +} + +/// `iqfac_m` per §4.6.9.3. Branches on a *negative* index / PARCOR +/// value. Defined as `((1 << (coef_res_bits-1)) + 0.5) / (π/2)`. +/// +/// Errors as [`iqfac`]. +pub fn iqfac_m(coef_res_bits: u32) -> Result { + if !(3..=4).contains(&coef_res_bits) { + return Err(Error::TnsCoefOutOfRange); + } + let scale = (1u32 << (coef_res_bits - 1)) as f64 + 0.5; + Ok(scale / HALF_PI) +} + +/// Sign-extend a wire `coef` value held in the low `coef_res2` bits +/// into a normal signed integer. +/// +/// `coef_res2 = coef_res_bits - coef_compress` per §4.6.9.3 and is +/// always in `{2, 3, 4}`. The spec's `sgn_mask = 1 << +/// (coef_res2 - 1)` selects the MSB of the truncated field; if that +/// bit is set, the spec ORs in `neg_mask = ~((1 << coef_res2) - 1)` +/// to fill the upper bits with 1 (two's-complement sign extension). +/// +/// Returns [`Error::TnsCoefOutOfRange`] when `coef_res2` lies outside +/// `2..=4` or when `coef` does not fit in `coef_res2` bits. +pub fn sign_extend_coef(coef: u32, coef_res2: u32) -> Result { + if !(2..=4).contains(&coef_res2) { + return Err(Error::TnsCoefOutOfRange); + } + let field_mask = (1u32 << coef_res2) - 1; + if coef & !field_mask != 0 { + return Err(Error::TnsCoefOutOfRange); + } + let sgn_mask = 1u32 << (coef_res2 - 1); + if coef & sgn_mask != 0 { + // Negative — OR with the bits above the field. + let neg_mask = !field_mask; + Ok((coef | neg_mask) as i32) + } else { + Ok(coef as i32) + } +} + +/// Inverse of [`sign_extend_coef`]: pack a signed integer back into a +/// `coef_res2`-bit wire field. Used by the encoder to emit the wire +/// `coef[i]` slot. +/// +/// Returns [`Error::TnsCoefOutOfRange`] when `coef_res2` is outside +/// `2..=4`, or when `value` is outside the field-representable range +/// `-(1 << (coef_res2-1))..=(1 << (coef_res2-1)) - 1`. +pub fn pack_coef(value: i32, coef_res2: u32) -> Result { + if !(2..=4).contains(&coef_res2) { + return Err(Error::TnsCoefOutOfRange); + } + let half = 1i32 << (coef_res2 - 1); + if !(-half..half).contains(&value) { + return Err(Error::TnsCoefOutOfRange); + } + let field_mask = (1u32 << coef_res2) - 1; + Ok((value as u32) & field_mask) +} + +/// Run §4.6.9.3 `tns_decode_coef`: sign-extend the wire `coef[]`, +/// then inverse-quantise via `sin(tmp[i] / iqfac_branch)` to recover +/// the floating-point PARCOR (reflection-coefficient) array. +/// +/// * `coef_res_bits` is the spec's `coef_res[w] + 3`, i.e. either +/// `3` (`coef_res = 0`) or `4` (`coef_res = 1`). +/// * `coef_compress` is the per-filter flag (0 or 1). The §4.6.9.3 +/// field width on the wire is `coef_res2 = coef_res_bits - +/// coef_compress` bits per coefficient. +/// * `coef` is the per-coefficient wire slice produced by +/// [`crate::tns_data::TnsData::parse`], length `order`. Every entry +/// must fit in `coef_res2` bits (the parser already enforces this, +/// so a runtime overflow here means the caller fabricated an +/// in-memory [`crate::tns_data::TnsFilter`]). +/// +/// The returned `Vec` has the same length as `coef` and contains +/// the §4.6.9.3 `tmp2[]` array (PARCOR coefficients in `[-1, 1]`). +/// +/// Returns [`Error::TnsCoefOutOfRange`] on invalid `coef_res_bits` +/// (`!= 3 && != 4`), `coef_compress > 1`, or a `coef[i]` that does +/// not fit `coef_res2` bits. +pub fn tns_decode_coef(coef_res_bits: u32, coef_compress: u32, coef: &[u32]) -> Result> { + if coef_compress > 1 { + return Err(Error::TnsCoefOutOfRange); + } + let coef_res2 = coef_res_bits + .checked_sub(coef_compress) + .ok_or(Error::TnsCoefOutOfRange)?; + let iq = iqfac(coef_res_bits)?; + let iq_m = iqfac_m(coef_res_bits)?; + + let mut out = Vec::with_capacity(coef.len()); + for &c in coef { + let signed = sign_extend_coef(c, coef_res2)?; + let divisor = if signed >= 0 { iq } else { iq_m }; + out.push((signed as f64 / divisor).sin()); + } + Ok(out) +} + +/// Encoder-side inverse of [`tns_decode_coef`] per §C.6: quantise a +/// PARCOR reflection-coefficient array `r[]` into the wire `coef[]` +/// slots [`crate::tns_data::TnsFilter::coef`] consumes. +/// +/// The quantisation rule is `index = NINT(arcsin(r) * iqfac_branch)`, +/// where the `iqfac_branch` selector is keyed on the *sign of `r`* +/// (not the index): non-negative `r` uses [`iqfac`], strictly +/// negative `r` uses [`iqfac_m`]. After rounding, the encoder clamps +/// `index` to the `coef_res2`-bit signed-magnitude range +/// `-(1 << (coef_res2-1))..=(1 << (coef_res2-1)) - 1` and folds it +/// through [`pack_coef`]. +/// +/// Returns [`Error::TnsCoefOutOfRange`] on invalid `coef_res_bits` / +/// `coef_compress`, or on a `|r| > 1.0` value (`arcsin` is undefined +/// outside `[-1, 1]`). +pub fn tns_encode_coef(coef_res_bits: u32, coef_compress: u32, r: &[f64]) -> Result> { + if coef_compress > 1 { + return Err(Error::TnsCoefOutOfRange); + } + let coef_res2 = coef_res_bits + .checked_sub(coef_compress) + .ok_or(Error::TnsCoefOutOfRange)?; + let iq = iqfac(coef_res_bits)?; + let iq_m = iqfac_m(coef_res_bits)?; + let half = 1i32 << (coef_res2 - 1); + let max_idx = half - 1; + let min_idx = -half; + + let mut out = Vec::with_capacity(r.len()); + for &value in r { + if !(-1.0..=1.0).contains(&value) { + return Err(Error::TnsCoefOutOfRange); + } + let scale = if value >= 0.0 { iq } else { iq_m }; + // NINT = round-half-away-from-zero; f64::round matches this. + let raw = (value.asin() * scale).round() as i32; + let clamped = raw.clamp(min_idx, max_idx); + out.push(pack_coef(clamped, coef_res2)?); + } + Ok(out) +} + +/// §4.6.9.3 *conversion to LPC coefficients* — the "step-up procedure" +/// that converts an order-`N` PARCOR array `tmp2[]` (output of +/// [`tns_decode_coef`]) into the order-`N` direct-form LPC vector +/// `a[]` of length `N + 1` with `a[0] = 1.0`. +/// +/// The loop is: +/// +/// ```text +/// a[0] = 1 +/// for (m = 1; m <= order; m++) { +/// for (i = 1; i < m; i++) +/// b[i] = a[i] + tmp2[m-1] * a[m-i]; +/// for (i = 1; i < m; i++) +/// a[i] = b[i]; +/// a[m] = tmp2[m-1]; +/// } +/// ``` +/// +/// `parcor.len()` is the filter order; the returned vector has +/// `parcor.len() + 1` entries. An empty `parcor` slice produces the +/// degenerate `[1.0]` (no filtering — every filter with `order == 0` +/// is skipped by the §4.6.9.3 outer loop). +pub fn lpc_step_up(parcor: &[f64]) -> Vec { + let order = parcor.len(); + // `a` is the running LPC coefficient array. Length is `order + 1` + // throughout; only the first `m + 1` slots are meaningful at the + // start of iteration `m` (the remainder is zeroed and overwritten + // by later iterations). + let mut a = vec![0.0_f64; order + 1]; + a[0] = 1.0; + // Scratch `b[]` matches the spec's pseudocode literally. Allocated + // once and reused across iterations; only the low `m` slots are + // consulted per `m`. + let mut b = vec![0.0_f64; order + 1]; + for m in 1..=order { + let k = parcor[m - 1]; + for i in 1..m { + b[i] = a[i] + k * a[m - i]; + } + // Copy the m-1 newly-derived `b[1..m]` slots back into a; + // clippy prefers `copy_from_slice` here over a manual loop. + a[1..m].copy_from_slice(&b[1..m]); + a[m] = k; + } + a +} + +/// Convenience wrapper that runs [`tns_decode_coef`] then +/// [`lpc_step_up`] in one call. +/// +/// Returns the `order + 1` LPC `a[]` vector (`a[0] = 1.0`) the +/// §4.6.9.3 `tns_ar_filter()` loop consumes. +pub fn tns_decode_coef_to_lpc( + coef_res_bits: u32, + coef_compress: u32, + coef: &[u32], +) -> Result> { + let parcor = tns_decode_coef(coef_res_bits, coef_compress, coef)?; + Ok(lpc_step_up(&parcor)) +} + +/// §4.6.9.3 `tns_ar_filter()` — the simple all-pole (auto-regressive) +/// IIR filter that TNS slides across a strided region of the +/// dequantised MDCT spectrum, in place. +/// +/// The §4.6.9.3 pseudocode defines the filter by the recurrence +/// +/// ```text +/// y(n) = x(n) - lpc[1]*y(n-1) - ... - lpc[order]*y(n-order) +/// ``` +/// +/// with these spec-mandated properties: +/// +/// * the filter state (`y(n-1) .. y(n-order)`) is **initialised to +/// zero** at every invocation; +/// * the output overwrites the input (**in-place operation**); +/// * `size` samples are processed, stepping to the next sample by the +/// index increment `inc` (`+1` upward, `−1` downward). +/// +/// `lpc` is the direct-form `a[]` array produced by [`lpc_step_up`] / +/// [`tns_decode_coef_to_lpc`]: `lpc[0] == 1.0` and `lpc[1..=order]` +/// are the predictor taps. The filter order is `lpc.len() - 1`; a +/// `lpc` of length 1 (order 0) leaves the spectrum untouched. +/// +/// `spectrum` is the full per-window coefficient buffer. `start` is +/// the index of the first sample to process — for an upward filter +/// (`inc = 1`) this is the §4.6.9.3 `start = swb_offset[bottom]`; for +/// a downward filter (`inc = -1`) the §4.6.9.3 `tns_decode_frame` +/// outer loop has already set `start = end - 1`, so the same `start` +/// argument is the top of the region and the walk proceeds toward +/// lower indices. +/// +/// The recurrence is evaluated literally: because the output is +/// written over the input and the filter reads back its own previous +/// *outputs* (`y`), the per-tap history is a small ring of the last +/// `order` produced samples, seeded with zeros. +/// +/// Returns [`Error::TnsCoefOutOfRange`] when: +/// +/// * `lpc` is empty (no `a[0]`), +/// * `inc` is neither `+1` nor `-1`, +/// * the strided walk of `size` samples starting at `start` with step +/// `inc` would leave the bounds of `spectrum` (an out-of-range +/// `start`/`size`/`inc` triple the caller fabricated; the +/// §4.6.9.3 `size = end - start <= 0` guard and the `swb_offset` +/// clamping in `tns_decode_frame` keep legitimate callers in range). +pub fn tns_ar_filter( + spectrum: &mut [f64], + start: usize, + size: usize, + inc: i32, + lpc: &[f64], +) -> Result<()> { + if lpc.is_empty() { + return Err(Error::TnsCoefOutOfRange); + } + if inc != 1 && inc != -1 { + return Err(Error::TnsCoefOutOfRange); + } + let order = lpc.len() - 1; + if size == 0 || order == 0 { + // Nothing to shape: an order-0 filter (`lpc == [1.0]`) is the + // identity, and a zero-length region is a no-op. Still + // bounds-check the (degenerate) walk so a bad `start` is + // rejected consistently. + if size > 0 { + walk_bounds_check(spectrum.len(), start, size, inc)?; + } + return Ok(()); + } + + walk_bounds_check(spectrum.len(), start, size, inc)?; + + // Filter-state ring: the last `order` *output* samples y(n-1) .. + // y(n-order). Index `0` is the most recent output; the ring is + // shifted by one each iteration. Seeded with zeros per §4.6.9.3. + let mut history = vec![0.0_f64; order]; + + let mut idx = start as isize; + for _ in 0..size { + let x = spectrum[idx as usize]; + // y(n) = x(n) - Σ_{k=1..order} lpc[k] * y(n-k) + let mut y = x; + for k in 1..=order { + y -= lpc[k] * history[k - 1]; + } + spectrum[idx as usize] = y; + // Shift the history ring: y becomes the new y(n-1). + for k in (1..order).rev() { + history[k] = history[k - 1]; + } + history[0] = y; + idx += inc as isize; + } + Ok(()) +} + +/// §4.6.7.4.1 TNS **analysis** filter — the all-zero (moving-average, +/// FIR) inverse of the §4.6.9.3 [`tns_ar_filter`] all-pole synthesis +/// filter, applied in place over a strided region. +/// +/// Figure 4.30 puts an additional TNS analysis filter in the LTP loop: +/// because TNS is applied to a *reconstructed* spectrum, the +/// LTP-predicted spectrum `X_est` has to be pushed through the same +/// noise-shaping the residual carries before it can be added to the +/// transmitted residual `Y_rec` (which sits in the pre-synthesis, +/// noise-shaped domain). That forward filter is the exact inverse of +/// the synthesis recurrence: where [`tns_ar_filter`] computes +/// +/// ```text +/// y(n) = x(n) - Σ_{k=1..order} lpc[k] * y(n-k) (all-pole) +/// ``` +/// +/// the analysis filter computes +/// +/// ```text +/// y(n) = x(n) + Σ_{k=1..order} lpc[k] * x(n-k) (all-zero) +/// ``` +/// +/// reading back its own *inputs* (`x`) rather than its outputs. Running +/// the analysis filter and then the synthesis filter over the same +/// region with the same `lpc` is the identity, which is the §4.6.7.4.1 +/// requirement: the analysis step in the LTP loop is undone by the +/// §4.6.9 TNS synthesis step that follows the `X_est + Y_rec` add. +/// +/// Argument and error semantics mirror [`tns_ar_filter`] exactly: the +/// filter state is seeded with zeros at every invocation, the output +/// overwrites the input in place, and `size` samples are processed +/// stepping by `inc ∈ {-1, +1}`. `lpc[0]` is the implicit `1.0`; +/// `lpc[1..=order]` are the predictor taps. An order-0 filter +/// (`lpc == [1.0]`) is the identity. +pub fn tns_ma_filter( + spectrum: &mut [f64], + start: usize, + size: usize, + inc: i32, + lpc: &[f64], +) -> Result<()> { + if lpc.is_empty() { + return Err(Error::TnsCoefOutOfRange); + } + if inc != 1 && inc != -1 { + return Err(Error::TnsCoefOutOfRange); + } + let order = lpc.len() - 1; + if size == 0 || order == 0 { + if size > 0 { + walk_bounds_check(spectrum.len(), start, size, inc)?; + } + return Ok(()); + } + + walk_bounds_check(spectrum.len(), start, size, inc)?; + + // Filter-state ring: the last `order` *input* samples x(n-1) .. + // x(n-order). Index `0` is the most recent input. Seeded with zeros, + // matching the all-pole filter's zero-initialised state so the two + // are mutual inverses over the region. + let mut history = vec![0.0_f64; order]; + + let mut idx = start as isize; + for _ in 0..size { + let x = spectrum[idx as usize]; + // y(n) = x(n) + Σ_{k=1..order} lpc[k] * x(n-k) + let mut y = x; + for k in 1..=order { + y += lpc[k] * history[k - 1]; + } + spectrum[idx as usize] = y; + // Shift the history ring: x becomes the new x(n-1). + for k in (1..order).rev() { + history[k] = history[k - 1]; + } + history[0] = x; + idx += inc as isize; + } + Ok(()) +} + +/// Bounds-check the §4.6.9.3 strided walk: `size` samples starting at +/// `start`, stepping by `inc ∈ {-1, +1}`, must all land inside a +/// buffer of `len` elements. Returns [`Error::TnsCoefOutOfRange`] +/// otherwise. +fn walk_bounds_check(len: usize, start: usize, size: usize, inc: i32) -> Result<()> { + if start >= len { + return Err(Error::TnsCoefOutOfRange); + } + // Last visited index = start + (size-1)*inc. Validate it stays in + // `0..len` without overflowing. + let span = (size - 1) as isize; + let last = start as isize + span * inc as isize; + if last < 0 || last >= len as isize { + return Err(Error::TnsCoefOutOfRange); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---------- iqfac / iqfac_m ---------- + + #[test] + fn iqfac_matches_spec_formula_for_legal_widths() { + // coef_res_bits = 3 (coef_res = 0): scale = 4 - 0.5 = 3.5 + let want3 = 3.5_f64 / HALF_PI; + let want4 = 7.5_f64 / HALF_PI; + assert!((iqfac(3).unwrap() - want3).abs() < 1e-15); + assert!((iqfac(4).unwrap() - want4).abs() < 1e-15); + } + + #[test] + fn iqfac_m_matches_spec_formula_for_legal_widths() { + let want3 = 4.5_f64 / HALF_PI; + let want4 = 8.5_f64 / HALF_PI; + assert!((iqfac_m(3).unwrap() - want3).abs() < 1e-15); + assert!((iqfac_m(4).unwrap() - want4).abs() < 1e-15); + } + + #[test] + fn iqfac_rejects_widths_outside_3_to_4() { + assert!(matches!(iqfac(0), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(iqfac(2), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(iqfac(5), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(iqfac_m(0), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(iqfac_m(2), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(iqfac_m(5), Err(Error::TnsCoefOutOfRange))); + } + + #[test] + fn iqfac_m_is_always_greater_than_iqfac() { + // The +0.5 vs -0.5 offset guarantees `iqfac_m > iqfac` for + // every coef_res_bits — this is what biases the round-to-zero + // of negative reflection coefficients toward the next-larger + // magnitude (so they don't underflow toward zero). + for n in [3, 4] { + assert!(iqfac_m(n).unwrap() > iqfac(n).unwrap()); + } + } + + // ---------- sign extension ---------- + + #[test] + fn sign_extend_4bit_covers_signed_range() { + // coef_res2 = 4 ⇒ signed range -8..=7. Walk every wire pattern. + let expected: [i32; 16] = [0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1]; + for wire in 0_u32..16 { + assert_eq!( + sign_extend_coef(wire, 4).unwrap(), + expected[wire as usize], + "wire {wire:04b}", + ); + } + } + + #[test] + fn sign_extend_3bit_covers_signed_range() { + // coef_res2 = 3 ⇒ signed range -4..=3. + let expected: [i32; 8] = [0, 1, 2, 3, -4, -3, -2, -1]; + for wire in 0_u32..8 { + assert_eq!(sign_extend_coef(wire, 3).unwrap(), expected[wire as usize]); + } + } + + #[test] + fn sign_extend_2bit_covers_signed_range() { + // coef_res2 = 2 ⇒ signed range -2..=1. + let expected: [i32; 4] = [0, 1, -2, -1]; + for wire in 0_u32..4 { + assert_eq!(sign_extend_coef(wire, 2).unwrap(), expected[wire as usize]); + } + } + + #[test] + fn sign_extend_rejects_out_of_range_field_width() { + assert!(matches!( + sign_extend_coef(0, 1), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + sign_extend_coef(0, 5), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn sign_extend_rejects_wire_value_that_overflows_field() { + // 4-bit field: a wire value of 16 (0b10000) doesn't fit. + assert!(matches!( + sign_extend_coef(16, 4), + Err(Error::TnsCoefOutOfRange) + )); + // 2-bit field: 4 doesn't fit. + assert!(matches!( + sign_extend_coef(4, 2), + Err(Error::TnsCoefOutOfRange) + )); + } + + // ---------- pack_coef (encoder-side) ---------- + + #[test] + fn pack_coef_round_trips_through_sign_extend_4bit() { + for value in -8_i32..=7 { + let packed = pack_coef(value, 4).unwrap(); + assert_eq!(sign_extend_coef(packed, 4).unwrap(), value); + } + } + + #[test] + fn pack_coef_round_trips_through_sign_extend_3bit() { + for value in -4_i32..=3 { + let packed = pack_coef(value, 3).unwrap(); + assert_eq!(sign_extend_coef(packed, 3).unwrap(), value); + } + } + + #[test] + fn pack_coef_round_trips_through_sign_extend_2bit() { + for value in -2_i32..=1 { + let packed = pack_coef(value, 2).unwrap(); + assert_eq!(sign_extend_coef(packed, 2).unwrap(), value); + } + } + + #[test] + fn pack_coef_rejects_out_of_field_value() { + // 4-bit signed range is -8..=7; 8 and -9 reject. + assert!(matches!(pack_coef(8, 4), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(pack_coef(-9, 4), Err(Error::TnsCoefOutOfRange))); + // 2-bit signed range is -2..=1; 2 and -3 reject. + assert!(matches!(pack_coef(2, 2), Err(Error::TnsCoefOutOfRange))); + assert!(matches!(pack_coef(-3, 2), Err(Error::TnsCoefOutOfRange))); + } + + // ---------- tns_decode_coef ---------- + + #[test] + fn decode_zero_wire_yields_zero_parcor() { + let parcor = tns_decode_coef(4, 0, &[0, 0, 0]).unwrap(); + assert_eq!(parcor.len(), 3); + for v in parcor { + assert!(v.abs() < 1e-15); + } + } + + #[test] + fn decode_field_extrema_yield_near_unity_magnitudes() { + // coef_res_bits=4, coef_compress=0 ⇒ coef_res2=4 ⇒ signed range + // -8..=7. The extreme positive index is 7; it should decode to + // sin(7 / iqfac) = sin(7 / (7.5 / (π/2))) ≈ sin(0.4666... · π/2). + // The extreme negative index is -8 ⇒ sin(-8 / iqfac_m). + let pos = tns_decode_coef(4, 0, &[7]).unwrap()[0]; + let neg = tns_decode_coef(4, 0, &[8]).unwrap()[0]; // wire 8 = -8 after sign-extend + let want_pos = (7.0_f64 / (7.5 / HALF_PI)).sin(); + let want_neg = (-8.0_f64 / (8.5 / HALF_PI)).sin(); + assert!((pos - want_pos).abs() < 1e-15); + assert!((neg - want_neg).abs() < 1e-15); + // Both magnitudes are in [-1, 1] — PARCOR coefficient validity. + assert!(pos.abs() <= 1.0); + assert!(neg.abs() <= 1.0); + } + + #[test] + fn decode_negative_branch_uses_iqfac_m() { + // Wire value 0xF in a 4-bit field sign-extends to -1. + // Decoded value must use iqfac_m (not iqfac): sin(-1 / iqfac_m). + let got = tns_decode_coef(4, 0, &[0xF]).unwrap()[0]; + let want = (-1.0_f64 / iqfac_m(4).unwrap()).sin(); + assert!((got - want).abs() < 1e-15); + } + + #[test] + fn decode_3bit_branch_uses_coef_res_bits_3() { + // coef_res_bits = 3 always — coef_compress doesn't change the + // iqfac arithmetic (only coef_res2 changes for sign extension). + let got_long = tns_decode_coef(3, 0, &[1]).unwrap()[0]; + let want_long = (1.0_f64 / iqfac(3).unwrap()).sin(); + assert!((got_long - want_long).abs() < 1e-15); + // coef_compress = 1 ⇒ coef_res2 = 2; signed range -2..=1. + // Wire 1 ⇒ +1 after sign-extend, then sin(1 / iqfac(3)). + let got_short = tns_decode_coef(3, 1, &[1]).unwrap()[0]; + assert!((got_short - want_long).abs() < 1e-15); + } + + #[test] + fn decode_rejects_oversized_wire_value_for_compress_path() { + // coef_res_bits=4, coef_compress=1 ⇒ coef_res2=3 (signed -4..=3); + // wire 8 (0b1000) does not fit a 3-bit field. + assert!(matches!( + tns_decode_coef(4, 1, &[8]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn decode_rejects_invalid_coef_res_bits() { + assert!(matches!( + tns_decode_coef(5, 0, &[0]), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + tns_decode_coef(2, 0, &[0]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn decode_rejects_invalid_coef_compress() { + assert!(matches!( + tns_decode_coef(4, 2, &[0]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn decode_empty_input_yields_empty_output() { + let parcor = tns_decode_coef(4, 0, &[]).unwrap(); + assert!(parcor.is_empty()); + } + + // ---------- tns_encode_coef ---------- + + #[test] + fn encode_zero_parcor_yields_zero_wire() { + let wire = tns_encode_coef(4, 0, &[0.0, 0.0, 0.0]).unwrap(); + assert_eq!(wire, vec![0, 0, 0]); + } + + #[test] + fn encode_unity_parcor_saturates_to_field_max() { + // r = 1.0 ⇒ arcsin = π/2; index = round(π/2 * iqfac(4)) = + // round(π/2 * (7.5 / (π/2))) = round(7.5) = 8, clamped to 7 + // (the 4-bit signed field maximum). Sign-extends back to +7. + let wire = tns_encode_coef(4, 0, &[1.0]).unwrap(); + assert_eq!(wire, vec![7]); + // r = -1.0 ⇒ arcsin = -π/2; index = round(-π/2 * iqfac_m(4)) = + // round(-π/2 * (8.5 / (π/2))) = round(-8.5) = -9, clamped to + // -8 (the 4-bit signed field minimum). Wire pattern is + // 0b1000 = 8. + let wire_neg = tns_encode_coef(4, 0, &[-1.0]).unwrap(); + assert_eq!(wire_neg, vec![8]); + } + + #[test] + fn encode_rejects_parcor_outside_minus_one_to_plus_one() { + assert!(matches!( + tns_encode_coef(4, 0, &[1.0001]), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + tns_encode_coef(4, 0, &[-1.0001]), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + tns_encode_coef(4, 0, &[f64::NAN]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn encode_rejects_invalid_coef_res_bits() { + assert!(matches!( + tns_encode_coef(5, 0, &[0.5]), + Err(Error::TnsCoefOutOfRange) + )); + } + + // ---------- round-trip ---------- + + #[test] + fn round_trip_every_4bit_wire_value_through_decode_then_encode() { + // For every 4-bit wire input, decode to PARCOR then re-encode + // and confirm we land on the same wire pattern. This is the + // fundamental invariant that §C.6 NINT(arcsin(sin(x))) returns + // the input integer when the magnitude is in-range. + for wire in 0_u32..16 { + let parcor = tns_decode_coef(4, 0, &[wire]).unwrap(); + let back = tns_encode_coef(4, 0, &parcor).unwrap(); + assert_eq!(back, vec![wire], "wire {wire:04b} round-trip"); + } + } + + #[test] + fn round_trip_every_3bit_wire_value_through_decode_then_encode() { + for wire in 0_u32..8 { + let parcor = tns_decode_coef(3, 0, &[wire]).unwrap(); + let back = tns_encode_coef(3, 0, &parcor).unwrap(); + assert_eq!(back, vec![wire], "wire {wire:03b} round-trip"); + } + } + + #[test] + fn round_trip_with_coef_compress_for_both_res_settings() { + // coef_res_bits = 4, coef_compress = 1 ⇒ coef_res2 = 3. + // Sign-extension uses the 3-bit field but iqfac/iqfac_m use + // coef_res_bits = 4. Confirm round-trip lands on the same + // 3-bit wire pattern. + for wire in 0_u32..8 { + let parcor = tns_decode_coef(4, 1, &[wire]).unwrap(); + let back = tns_encode_coef(4, 1, &parcor).unwrap(); + assert_eq!(back, vec![wire], "coef_res=1 compress=1 wire {wire:03b}"); + } + // coef_res_bits = 3, coef_compress = 1 ⇒ coef_res2 = 2. + for wire in 0_u32..4 { + let parcor = tns_decode_coef(3, 1, &[wire]).unwrap(); + let back = tns_encode_coef(3, 1, &parcor).unwrap(); + assert_eq!(back, vec![wire], "coef_res=0 compress=1 wire {wire:02b}"); + } + } + + // ---------- lpc_step_up ---------- + + #[test] + fn step_up_zero_order_returns_unit_a() { + let a = lpc_step_up(&[]); + assert_eq!(a, vec![1.0]); + } + + #[test] + fn step_up_first_order_matches_hand_arithmetic() { + // order = 1: a[0] = 1, a[1] = k. No inner-loop iterations. + let a = lpc_step_up(&[0.5]); + assert_eq!(a, vec![1.0, 0.5]); + } + + #[test] + fn step_up_second_order_matches_hand_arithmetic() { + // order = 2 with parcor [k1, k2]: + // m=1: a = [1, k1] + // m=2: b[1] = a[1] + k2 * a[1] = k1 * (1 + k2) + // a[1] = b[1]; a[2] = k2 + // ⇒ a = [1, k1*(1 + k2), k2] + let (k1, k2) = (0.3, 0.4); + let a = lpc_step_up(&[k1, k2]); + assert_eq!(a.len(), 3); + assert!((a[0] - 1.0).abs() < 1e-15); + assert!((a[1] - k1 * (1.0 + k2)).abs() < 1e-15); + assert!((a[2] - k2).abs() < 1e-15); + } + + #[test] + fn step_up_third_order_matches_hand_arithmetic() { + // order = 3: + // m=1: a = [1, k1, 0, 0] + // m=2: a = [1, k1*(1+k2), k2, 0] + // m=3: b[1] = a[1] + k3 * a[2] = k1*(1+k2) + k3*k2 + // b[2] = a[2] + k3 * a[1] = k2 + k3*k1*(1+k2) + // a[3] = k3 + let (k1, k2, k3) = (0.2, 0.3, -0.4); + let a = lpc_step_up(&[k1, k2, k3]); + let want = [ + 1.0, + k1 * (1.0 + k2) + k3 * k2, + k2 + k3 * k1 * (1.0 + k2), + k3, + ]; + for i in 0..4 { + assert!( + (a[i] - want[i]).abs() < 1e-15, + "i={i} got {} want {}", + a[i], + want[i], + ); + } + } + + #[test] + fn step_up_a0_always_one() { + // a[0] = 1 for every PARCOR sequence — invariant of the + // step-up loop init. + for parcor in [ + vec![0.5], + vec![-0.5], + vec![0.1, -0.2], + vec![0.3, -0.4, 0.5, -0.6, 0.7, -0.8, 0.9, -0.95], + ] { + let a = lpc_step_up(&parcor); + assert_eq!(a.len(), parcor.len() + 1); + assert!((a[0] - 1.0).abs() < 1e-15); + } + } + + #[test] + fn step_up_last_coefficient_is_last_parcor() { + // The §4.6.9.3 loop's final iteration sets a[m] = tmp2[m-1] at + // m = order. So a[order] must equal parcor[order-1] for every + // order. (The intermediate a[i] entries pick up the cross + // terms.) + for parcor in [vec![0.3], vec![0.3, -0.5], vec![0.1, 0.2, 0.3, 0.4]] { + let a = lpc_step_up(&parcor); + let last_idx = parcor.len(); + assert_eq!(a[last_idx], *parcor.last().unwrap()); + } + } + + // ---------- combined wrapper ---------- + + #[test] + fn decode_to_lpc_combines_decode_and_step_up() { + let wire = [3, 5, 0xF]; // mixed positive / negative + let parcor = tns_decode_coef(4, 0, &wire).unwrap(); + let want = lpc_step_up(&parcor); + let got = tns_decode_coef_to_lpc(4, 0, &wire).unwrap(); + assert_eq!(got, want); + } + + #[test] + fn decode_to_lpc_propagates_decode_errors() { + assert!(matches!( + tns_decode_coef_to_lpc(5, 0, &[0]), + Err(Error::TnsCoefOutOfRange) + )); + } + + // ---------- tns_ar_filter ---------- + + /// Reference implementation of the §4.6.9.3 recurrence written the + /// straightforward (non-ring-buffer) way, for cross-checking the + /// production `tns_ar_filter`. Operates on a contiguous copy. + fn ref_ar_filter(x: &[f64], lpc: &[f64]) -> Vec { + let order = lpc.len() - 1; + let mut y = vec![0.0_f64; x.len()]; + for n in 0..x.len() { + let mut acc = x[n]; + for k in 1..=order { + if n >= k { + acc -= lpc[k] * y[n - k]; + } + } + y[n] = acc; + } + y + } + + #[test] + fn ar_filter_order0_is_identity() { + let mut spec = [1.0, 2.0, 3.0, 4.0]; + let before = spec; + // lpc = [1.0] ⇒ order 0. + tns_ar_filter(&mut spec, 0, 4, 1, &[1.0]).unwrap(); + assert_eq!(spec, before); + } + + #[test] + fn ar_filter_order1_matches_recurrence_upward() { + // y(n) = x(n) - lpc[1]*y(n-1). + let lpc = [1.0, 0.5]; + let x = [1.0, 0.0, 0.0, 0.0, 0.0]; + let want = ref_ar_filter(&x, &lpc); + let mut spec = x; + tns_ar_filter(&mut spec, 0, 5, 1, &lpc).unwrap(); + for (g, w) in spec.iter().zip(want.iter()) { + assert!((g - w).abs() < 1e-12, "got {g} want {w}"); + } + // Hand-check: unit impulse through y(n)+0.5 y(n-1) = x gives + // y = 1, -0.5, 0.25, -0.125, 0.0625. + let hand = [1.0, -0.5, 0.25, -0.125, 0.0625]; + for (g, h) in spec.iter().zip(hand.iter()) { + assert!((g - h).abs() < 1e-12); + } + } + + #[test] + fn ar_filter_order3_matches_reference() { + let lpc = [1.0, -0.4, 0.2, 0.1]; + let x = [0.7, -1.3, 2.1, 0.0, -0.5, 1.1, 0.9, -0.2]; + let want = ref_ar_filter(&x, &lpc); + let mut spec = x; + tns_ar_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); + for (g, w) in spec.iter().zip(want.iter()) { + assert!((g - w).abs() < 1e-12, "got {g} want {w}"); + } + } + + #[test] + fn ar_filter_downward_walks_high_to_low() { + // direction = 1 ⇒ inc = -1, start = end - 1. The §4.6.9.3 + // filter then processes the region top-to-bottom. Cross-check + // by reversing the region, filtering forward, and reversing + // back. + let lpc = [1.0, 0.3, -0.15]; + let region = [0.5, -0.2, 0.9, 1.4, -0.7]; + // Place region inside a larger buffer with sentinel padding to + // confirm only the targeted span is touched. + let mut spec = vec![100.0, 0.5, -0.2, 0.9, 1.4, -0.7, 200.0]; + let start = 5; // end-1, where end = 6 (one past last region idx) + let size = 5; + tns_ar_filter(&mut spec, start, size, -1, &lpc).unwrap(); + + // Reference: process region in reverse order (high→low). + let mut rev: Vec = region.iter().rev().copied().collect(); + let want_rev = ref_ar_filter(&rev, &lpc); + rev.copy_from_slice(&want_rev); + let want: Vec = rev.into_iter().rev().collect(); + + assert_eq!(spec[0], 100.0, "lower sentinel untouched"); + assert_eq!(spec[6], 200.0, "upper sentinel untouched"); + for (i, w) in want.iter().enumerate() { + assert!( + (spec[1 + i] - w).abs() < 1e-12, + "idx {i}: {} vs {w}", + spec[1 + i] + ); + } + } + + #[test] + fn ar_filter_only_touches_targeted_region() { + let lpc = [1.0, 0.5]; + let mut spec = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; + // Filter only indices 2..=4 (size 3, upward). + tns_ar_filter(&mut spec, 2, 3, 1, &lpc).unwrap(); + assert_eq!(spec[0], 1.0); + assert_eq!(spec[1], 2.0); + assert_eq!(spec[5], 6.0); + // Region recomputed independently. + let want = ref_ar_filter(&[3.0, 4.0, 5.0], &lpc); + for i in 0..3 { + assert!((spec[2 + i] - want[i]).abs() < 1e-12); + } + } + + #[test] + fn ar_filter_zero_size_is_noop() { + let mut spec = [1.0, 2.0, 3.0]; + let before = spec; + tns_ar_filter(&mut spec, 0, 0, 1, &[1.0, 0.5]).unwrap(); + assert_eq!(spec, before); + } + + #[test] + fn ar_filter_rejects_empty_lpc() { + let mut spec = [1.0, 2.0]; + assert!(matches!( + tns_ar_filter(&mut spec, 0, 2, 1, &[]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn ar_filter_rejects_bad_inc() { + let mut spec = [1.0, 2.0]; + assert!(matches!( + tns_ar_filter(&mut spec, 0, 2, 0, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + tns_ar_filter(&mut spec, 0, 2, 2, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn ar_filter_rejects_out_of_bounds_walk() { + let mut spec = [1.0, 2.0, 3.0]; + // start in range but size overruns the top. + assert!(matches!( + tns_ar_filter(&mut spec, 1, 5, 1, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + // downward walk underruns below 0. + assert!(matches!( + tns_ar_filter(&mut spec, 1, 3, -1, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + // start past the end. + assert!(matches!( + tns_ar_filter(&mut spec, 3, 1, 1, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + } + + #[test] + fn ar_filter_end_to_end_from_wire_coef() { + // Decode a wire TNS filter to LPC, then shape a spectrum. + // Confirms the lpc_step_up output drives tns_ar_filter without + // any glue. coef_res_bits = 4, coef_compress = 0, order 2. + let wire = [3_u32, 0xE]; // one positive, one negative reflection + let lpc = tns_decode_coef_to_lpc(4, 0, &wire).unwrap(); + assert_eq!(lpc.len(), 3); + assert_eq!(lpc[0], 1.0); + let x = [0.3, -0.9, 1.2, 0.4, -0.6, 0.1]; + let want = ref_ar_filter(&x, &lpc); + let mut spec = x; + tns_ar_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); + for (g, w) in spec.iter().zip(want.iter()) { + assert!((g - w).abs() < 1e-12); + } + } + + // ---------- tns_ma_filter (analysis / all-zero) ---------- + + /// Reference all-zero (analysis) filter for an upward, in-order + /// region: y(n) = x(n) + Σ lpc[k]·x(n-k), zero-seeded history. + fn ref_ma_filter(x: &[f64], lpc: &[f64]) -> Vec { + let order = lpc.len() - 1; + let mut y = vec![0.0; x.len()]; + for n in 0..x.len() { + let mut acc = x[n]; + for k in 1..=order { + if n >= k { + acc += lpc[k] * x[n - k]; + } + } + y[n] = acc; + } + y + } + + #[test] + fn ma_filter_order_zero_is_identity() { + let mut spec = [0.3, -0.9, 1.2, 0.4]; + let before = spec; + let n = spec.len(); + tns_ma_filter(&mut spec, 0, n, 1, &[1.0]).unwrap(); + assert_eq!(spec, before); + } + + #[test] + fn ma_filter_matches_reference_upward() { + let lpc = [1.0, 0.5, -0.25]; + let x = [0.3, -0.9, 1.2, 0.4, -0.6, 0.1]; + let want = ref_ma_filter(&x, &lpc); + let mut spec = x; + tns_ma_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); + for (g, w) in spec.iter().zip(want.iter()) { + assert!((g - w).abs() < 1e-12, "got {g} want {w}"); + } + } + + #[test] + fn ma_then_ar_is_identity() { + // §4.6.7.4.1: the analysis filter followed by the synthesis + // filter (same region, same lpc) reconstructs the input exactly. + let lpc = tns_decode_coef_to_lpc(4, 0, &[3, 0xE]).unwrap(); + let x = [0.7, -0.2, 1.1, -1.3, 0.05, 0.9, -0.4]; + let mut spec = x; + tns_ma_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); + tns_ar_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); + for (g, w) in spec.iter().zip(x.iter()) { + assert!((g - w).abs() < 1e-12, "ma∘ar not identity: {g} vs {w}"); + } + } + + #[test] + fn ma_then_ar_is_identity_downward() { + // Same inverse relationship for the downward (direction=1) walk. + let lpc = [1.0, -0.4, 0.2]; + let x = [0.7, -0.2, 1.1, -1.3, 0.05]; + let mut spec = x; + let end = x.len(); + tns_ma_filter(&mut spec, end - 1, end, -1, &lpc).unwrap(); + tns_ar_filter(&mut spec, end - 1, end, -1, &lpc).unwrap(); + for (g, w) in spec.iter().zip(x.iter()) { + assert!((g - w).abs() < 1e-12); + } + } + + #[test] + fn ma_filter_rejects_bad_args() { + let mut spec = [1.0, 2.0, 3.0]; + assert!(matches!( + tns_ma_filter(&mut spec, 0, 1, 2, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + tns_ma_filter(&mut spec, 1, 5, 1, &[1.0, 0.5]), + Err(Error::TnsCoefOutOfRange) + )); + assert!(matches!( + tns_ma_filter(&mut spec, 0, 1, 1, &[]), + Err(Error::TnsCoefOutOfRange) + )); + } +} diff --git a/crates/vendor/oxideav-aac/src/tns_data.rs b/crates/vendor/oxideav-aac/src/tns_data.rs new file mode 100644 index 00000000..1c289adc --- /dev/null +++ b/crates/vendor/oxideav-aac/src/tns_data.rs @@ -0,0 +1,480 @@ +//! `tns_data()` parser + encoder primitive — ISO/IEC 14496-3 +//! §4.4.6 / Table 4.54 (syntax) and §4.6.9 / Table 4.155 (field-size +//! switching). +//! +//! Temporal Noise Shaping is an in-MDCT prediction tool that shapes +//! the temporal envelope of quantisation noise inside each transform +//! window. The encoder emits one or more all-pole filters per +//! window, each covering a contiguous range of scalefactor bands. +//! The decoder reverses the filtering after Huffman decoding but +//! before IMDCT. `tns_data()` is the wire record of those filters. +//! It rides inside an `individual_channel_stream()` between +//! `pulse_data()` and `gain_control_data()` / `spectral_data()`, +//! gated by the dispatching `tns_data_present` flag (Tables 4.44 / +//! 4.50). +//! +//! ## Wire layout (Table 4.54) +//! +//! ```text +//! tns_data() { +//! for (w = 0; w < num_windows; w++) { +//! n_filt[w]; 1..2 bits (Table 4.155) +//! if (n_filt[w]) +//! coef_res[w]; 1 bit +//! for (filt = 0; filt < n_filt[w]; filt++) { +//! length[w][filt]; 4 or 6 bits (Table 4.155) +//! order[w][filt]; 3 or 5 bits (Table 4.155) +//! if (order[w][filt]) { +//! direction[w][filt]; 1 bit +//! coef_compress[w][filt]; 1 bit +//! for (i = 0; i < order[w][filt]; i++) +//! coef[w][filt][i]; 2..4 bits (see below) +//! } +//! } +//! } +//! } +//! ``` +//! +//! Two `window_sequence`-dependent field-width pairs control the +//! per-window dispatch (§4.6.9.2 Table 4.155): +//! +//! | name | EIGHT_SHORT (128-line) | other window sizes | +//! |-----------|-------------------------|--------------------| +//! | `n_filt` | 1 bit | 2 bits | +//! | `length` | 4 bits | 6 bits | +//! | `order` | 3 bits | 5 bits | +//! +//! Per-filter `coef[i]` width is determined by `coef_res[w]` and +//! `coef_compress[w][filt]` per §4.6.9.3 `tns_decode_coef`: +//! +//! ```text +//! coef_res_bits = coef_res[w] ? 4 : 3 +//! coef_bits = coef_res_bits - coef_compress[w][filt] +//! ∈ {2, 3, 4} +//! ``` +//! +//! `coef_res` is **only** present on the wire when at least one +//! filter is emitted for the window (`n_filt[w] > 0`); zero-filter +//! windows simply skip the bit. +//! +//! `num_windows` is supplied by the surrounding `ics_info()`: `8` for +//! `EIGHT_SHORT_SEQUENCE`, `1` for every other window sequence +//! (§4.5.2.3.4). +//! +//! ## What this module covers +//! +//! * [`TnsData::parse`] — read a Table 4.54 block from a +//! [`BitReader`], surfacing every wire field literally. Per-filter +//! `coef[]` widths are computed from the freshly-read `coef_res` +//! and `coef_compress` flags exactly as §4.6.9.3 prescribes. +//! * [`TnsData::write`] — the inverse: serialise a [`TnsData`] onto +//! a [`BitWriter`] in bit-exact Table 4.54 form. Surfaces caller- +//! side structural bugs (field overflow, length mismatch between +//! `order` and the `coef` slice, out-of-range `coef` value) as +//! [`Error::TnsDataEncodeInvalid`]. +//! +//! ## What this module does *not* cover +//! +//! * The §4.6.9.3 `tns_decode_coef` LPC reconstruction (signed-magnitude +//! conversion, `iqfac` arcsine inverse-quantisation, Levinson-style +//! conversion to LPC coefficients) is **not** performed here — it +//! needs a floating-point or fixed-point spectral context that +//! arrives with the per-AOT IMDCT back-end. +//! * The §4.6.9.3 `tns_ar_filter` all-pole filtering pass over the +//! spectrum is similarly deferred. +//! * The §4.6.9.4 `TNS_MAX_ORDER` and `TNS_MAX_BANDS` clamp tables +//! (Tables 4.156 / 4.157) are not consulted by the wire encoder +//! or parser. The parser surfaces the literal wire `order` / +//! `length` regardless of whether they exceed the AOT-and-sample- +//! rate-dependent caps; the decoder's reconstruction loop is the +//! layer that applies `min(order, TNS_MAX_ORDER)` and +//! `min(bands, TNS_MAX_BANDS, max_sfb)`. +//! * The normative constraint that `tns_data_present == 0` for the +//! ER AAC LD `gain_control_data` path (Table 4.50) is the +//! responsibility of the dispatching `individual_channel_stream()` +//! (which has not landed yet). + +use oxideav_core::bits::{BitReader, BitWriter}; + +use crate::ics_info::WindowSequence; +use crate::swb_offset::FrameFamily; +use crate::{Error, Result}; + +/// One TNS noise-shaping filter inside a single transform window. +/// +/// Fields are the literal Table 4.54 wire values. The `coef` slot +/// holds the unsigned magnitudes as transmitted (each entry occupies +/// `coef_bits` per §4.6.9.3 — `coef_res_bits − coef_compress`); +/// signed-magnitude conversion is performed by `tns_decode_coef()` +/// at decode time and is *not* applied here. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TnsFilter { + /// `length[w][filt]` — number of scalefactor bands covered by this + /// filter (4 bits on `EIGHT_SHORT_SEQUENCE`, 6 bits otherwise). + pub length: u8, + /// `order[w][filt]` — all-pole filter order (3 bits on + /// `EIGHT_SHORT_SEQUENCE`, 5 bits otherwise). When `order == 0` + /// no `direction` / `coef_compress` / `coef[]` are emitted. + pub order: u8, + /// `direction[w][filt]` — slide direction across the spectrum: + /// `false` = upward, `true` = downward. Absent on the wire when + /// `order == 0`; the [`TnsData::parse`] caller-side default is + /// `false` in that case. + pub direction: bool, + /// `coef_compress[w][filt]` — when `true` the MSB of every + /// transmitted coefficient is omitted, shrinking each `coef[i]` + /// from `coef_res_bits` to `coef_res_bits − 1`. Absent on the + /// wire when `order == 0`. + pub coef_compress: bool, + /// `coef[w][filt][i]` for `i in 0..order` — unsigned magnitudes + /// as transmitted. Length **must** equal `order`. Each entry + /// is in `0..(1 << coef_bits)` where + /// `coef_bits = (3 + coef_res as u32) − coef_compress as u32`. + pub coef: Vec, +} + +/// Per-window TNS payload. Always carries `coef_res` even when +/// `filters.is_empty()`; the [`TnsData::write`] code path omits the +/// wire bit in that case but the field is meaningful for callers +/// that round-trip a structurally identical block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TnsWindow { + /// `coef_res[w]` — `false` selects a 3-bit `coef_res_bits`, + /// `true` selects a 4-bit `coef_res_bits` per §4.6.9.3. When + /// `filters.is_empty()` the bit is **not** transmitted; both the + /// parser and the writer treat the stored value as a don't-care + /// in that case. + pub coef_res: bool, + /// The filters for this window, in wire order. `n_filt[w]` on + /// the wire is `filters.len()` and is capped by the field width + /// (1 bit on `EIGHT_SHORT_SEQUENCE`, 2 bits otherwise → 0..=1 + /// vs 0..=3). + pub filters: Vec, +} + +/// Parsed `tns_data()` block (Table 4.54). +/// +/// `windows` always carries exactly `num_windows` entries (8 for +/// `EIGHT_SHORT_SEQUENCE`, 1 otherwise). The [`TnsData::write`] code +/// path validates this against the surrounding [`WindowSequence`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TnsData { + /// One entry per transform window. Length must match the + /// surrounding [`WindowSequence`]: 8 for `EightShort`, 1 + /// otherwise. + pub windows: Vec, +} + +/// `n_filt` field width for `EIGHT_SHORT_SEQUENCE` per Table 4.155. +pub const N_FILT_BITS_SHORT: u32 = 1; +/// `n_filt` field width for any non-`EIGHT_SHORT_SEQUENCE` per Table +/// 4.155. +pub const N_FILT_BITS_LONG: u32 = 2; +/// `length` field width for `EIGHT_SHORT_SEQUENCE` per Table 4.155. +pub const LENGTH_BITS_SHORT: u32 = 4; +/// `length` field width for any non-`EIGHT_SHORT_SEQUENCE` per Table +/// 4.155. +pub const LENGTH_BITS_LONG: u32 = 6; +/// `order` field width for `EIGHT_SHORT_SEQUENCE` per Table 4.155. +pub const ORDER_BITS_SHORT: u32 = 3; +/// `order` field width for any non-`EIGHT_SHORT_SEQUENCE` per Table +/// 4.155. +pub const ORDER_BITS_LONG: u32 = 5; +/// `coef_res` field width per Table 4.54. +pub const COEF_RES_BITS: u32 = 1; +/// `direction` field width per Table 4.54. +pub const DIRECTION_BITS: u32 = 1; +/// `coef_compress` field width per Table 4.54. +pub const COEF_COMPRESS_BITS: u32 = 1; + +/// `(n_filt_bits, length_bits, order_bits)` triple for the given +/// `window_sequence`. The selection rule is §4.6.9.2 Table 4.155 — +/// the 128-line `EIGHT_SHORT_SEQUENCE` shrinks every field by one +/// or two bits versus the other window sizes. +pub fn field_widths(seq: WindowSequence) -> (u32, u32, u32) { + if seq.is_eight_short() { + (N_FILT_BITS_SHORT, LENGTH_BITS_SHORT, ORDER_BITS_SHORT) + } else { + (N_FILT_BITS_LONG, LENGTH_BITS_LONG, ORDER_BITS_LONG) + } +} + +/// `(n_filt_bits, length_bits, order_bits)` triple for the given +/// frame family and `window_sequence`. +/// +/// For the ER AAC LD families (§4.6.17, 512/480-line long-only +/// frames) the normative ISO/IEC 14496-26 conformance bitstreams +/// transmit the *reduced* Table 4.155 column — `n_filt` in **1 bit** +/// — even though the literal table keying (window size ≠ 128) selects +/// the 2-bit column. The resolution is corpus-empirical: across all +/// 173 `er_ad*_ep0` conformance vectors, every TNS-bearing access +/// unit parses with the 1-bit width and the 2-bit reading +/// desynchronises `spectral_data()` (792 hard failures of 2 017 TNS +/// records). `length` / `order` take the rest of the same reduced +/// column (4 / 3 bits); the corpus never transmits either field +/// (`n_filt == 0` throughout), so those two widths follow the only +/// hypothesis with a consistent selection mechanism. See +/// `docs/audio/aac/er-ld-tns-divergence.md` §0 (resolution of issue +/// #292). +/// +/// Every non-LD family keeps the literal Table 4.155 dispatch of +/// [`field_widths`]. +pub fn field_widths_family(family: FrameFamily, seq: WindowSequence) -> (u32, u32, u32) { + if family.is_ld() { + (N_FILT_BITS_SHORT, LENGTH_BITS_SHORT, ORDER_BITS_SHORT) + } else { + field_widths(seq) + } +} + +/// `num_windows` for the given `window_sequence` per §4.5.2.3.4: +/// `8` for `EIGHT_SHORT_SEQUENCE`, `1` otherwise. +pub fn num_windows(seq: WindowSequence) -> usize { + if seq.is_eight_short() { + 8 + } else { + 1 + } +} + +/// Per-filter `coef_bits` width per §4.6.9.3: +/// +/// ```text +/// coef_res_bits = 3 + (coef_res ? 1 : 0) +/// coef_bits = coef_res_bits - (coef_compress ? 1 : 0) +/// ``` +/// +/// Result is in `{2, 3, 4}`. +pub fn coef_bits(coef_res: bool, coef_compress: bool) -> u32 { + let coef_res_bits = 3 + u32::from(coef_res); + coef_res_bits - u32::from(coef_compress) +} + +impl TnsData { + /// Parse a `tns_data()` from `reader`, given the surrounding + /// `window_sequence` (which selects the per-window field widths + /// and `num_windows`). + /// + /// Returns [`Error::UnexpectedEnd`] on bit-reader underflow. + /// Returns [`Error::TnsDataEncodeInvalid`] when a `coef[i]` is + /// large enough to indicate a parser/spec mismatch (which cannot + /// happen for a conforming stream — every `coef[i]` is bounded + /// by its field width — but the check guards round-trip + /// invariants for hostile inputs). + pub fn parse(reader: &mut BitReader<'_>, window_sequence: WindowSequence) -> Result { + Self::parse_family(reader, FrameFamily::Lc1024, window_sequence) + } + + /// [`TnsData::parse`] under an explicit §4.5.1.1 frame family. + /// + /// The family selects the per-window field widths via + /// [`field_widths_family`]: the ER AAC LD families read the + /// reduced 1 / 4 / 3-bit column (the corpus-resolved AOT-23 wire, + /// `docs/audio/aac/er-ld-tns-divergence.md` §0), every other + /// family follows the literal Table 4.155 `window_sequence` + /// dispatch. + pub fn parse_family( + reader: &mut BitReader<'_>, + family: FrameFamily, + window_sequence: WindowSequence, + ) -> Result { + Self::parse_widths( + reader, + field_widths_family(family, window_sequence), + window_sequence, + ) + } + + /// [`TnsData::parse`] under an **explicit** + /// `(n_filt_bits, length_bits, order_bits)` width triple. + /// + /// This is the configurability hook + /// `docs/audio/aac/er-ld-tns-divergence.md` §0.6 recommends: the + /// LD `n_filt` width is corpus-settled at 1 bit, but the LD + /// `length` / `order` widths are only *preferred* at 4 / 3 (the + /// rest of the reduced Table 4.155 column) — the ISO/IEC 14496-26 + /// corpus transmits `n_filt == 0` in every LD TNS record, so it + /// cannot discriminate 4 / 3 from 6 / 5. A caller confronted with + /// evidence for a mixed wire (e.g. 1 / 6 / 5) can drive this entry + /// point directly instead of forking [`Self::parse_family`]'s + /// dispatch. Each width must be `1..=8` + /// ([`Error::TnsDataEncodeInvalid`] otherwise — the widths are + /// caller configuration, not wire data). + pub fn parse_widths( + reader: &mut BitReader<'_>, + widths: (u32, u32, u32), + window_sequence: WindowSequence, + ) -> Result { + let (n_filt_bits, length_bits, order_bits) = widths; + if !widths_valid(widths) { + return Err(Error::TnsDataEncodeInvalid); + } + let nw = num_windows(window_sequence); + let mut windows = Vec::with_capacity(nw); + for _ in 0..nw { + let n_filt = read_u8(reader, n_filt_bits)?; + let coef_res = if n_filt > 0 { + reader.read_bit().map_err(|_| Error::UnexpectedEnd)? + } else { + false + }; + let mut filters = Vec::with_capacity(n_filt as usize); + for _ in 0..n_filt { + let length = read_u8(reader, length_bits)?; + let order = read_u8(reader, order_bits)?; + let (direction, coef_compress, coef) = if order > 0 { + let direction = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let coef_compress = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; + let bits = coef_bits(coef_res, coef_compress); + let mut coef = Vec::with_capacity(order as usize); + for _ in 0..order { + coef.push(read_u8(reader, bits)?); + } + (direction, coef_compress, coef) + } else { + (false, false, Vec::new()) + }; + filters.push(TnsFilter { + length, + order, + direction, + coef_compress, + coef, + }); + } + windows.push(TnsWindow { coef_res, filters }); + } + Ok(TnsData { windows }) + } + + /// Encode `tns_data()` onto `writer`, the inverse of + /// [`TnsData::parse`]. + /// + /// Returns [`Error::TnsDataEncodeInvalid`] if: + /// + /// * `windows.len()` differs from [`num_windows`] for + /// `window_sequence` (1 for long sequences, 8 for + /// `EIGHT_SHORT_SEQUENCE`). + /// * `filters.len()` exceeds the `n_filt` field cap + /// (`(1 << n_filt_bits) - 1`) — 1 on `EIGHT_SHORT_SEQUENCE`, + /// 3 otherwise. + /// * Any `length` exceeds the `length` field cap + /// (`(1 << length_bits) - 1`) — 15 on `EIGHT_SHORT_SEQUENCE`, + /// 63 otherwise. + /// * Any `order` exceeds the `order` field cap — 7 on + /// `EIGHT_SHORT_SEQUENCE`, 31 otherwise. + /// * A filter's `coef.len()` differs from its `order`. + /// * A filter's `coef[i]` exceeds the `(1 << coef_bits) - 1` + /// field cap (where `coef_bits = (3 + coef_res) - coef_compress`). + /// * A filter has populated `direction` / `coef_compress` / + /// `coef` slots while `order == 0` (those fields are not + /// transmitted on the wire and a non-default value would not + /// round-trip). + pub fn write(&self, writer: &mut BitWriter, window_sequence: WindowSequence) -> Result<()> { + self.write_family(writer, FrameFamily::Lc1024, window_sequence) + } + + /// [`TnsData::write`] under an explicit §4.5.1.1 frame family — + /// the bit-exact inverse of [`TnsData::parse_family`]. The LD + /// families emit the reduced 1 / 4 / 3-bit widths, capping + /// `filters.len()` at 1, `length` at 15 and `order` at 7 per + /// window. + pub fn write_family( + &self, + writer: &mut BitWriter, + family: FrameFamily, + window_sequence: WindowSequence, + ) -> Result<()> { + self.write_widths( + writer, + field_widths_family(family, window_sequence), + window_sequence, + ) + } + + /// [`TnsData::write`] under an **explicit** + /// `(n_filt_bits, length_bits, order_bits)` width triple — the + /// bit-exact inverse of [`TnsData::parse_widths`] (see there for + /// why the widths are caller-configurable). Field caps derive from + /// the given widths; each width must be `1..=8`. + pub fn write_widths( + &self, + writer: &mut BitWriter, + widths: (u32, u32, u32), + window_sequence: WindowSequence, + ) -> Result<()> { + let (n_filt_bits, length_bits, order_bits) = widths; + if !widths_valid(widths) { + return Err(Error::TnsDataEncodeInvalid); + } + let nw = num_windows(window_sequence); + if self.windows.len() != nw { + return Err(Error::TnsDataEncodeInvalid); + } + let n_filt_max = (1u32 << n_filt_bits) - 1; + let length_max = (1u32 << length_bits) - 1; + let order_max = (1u32 << order_bits) - 1; + for w in &self.windows { + if (w.filters.len() as u32) > n_filt_max { + return Err(Error::TnsDataEncodeInvalid); + } + for f in &w.filters { + if u32::from(f.length) > length_max || u32::from(f.order) > order_max { + return Err(Error::TnsDataEncodeInvalid); + } + if f.order as usize != f.coef.len() { + return Err(Error::TnsDataEncodeInvalid); + } + if f.order == 0 && (f.direction || f.coef_compress) { + // Non-default direction/compress would silently be + // dropped on the wire (the spec emits neither field + // when order == 0); reject to keep round-trip + // identity. + return Err(Error::TnsDataEncodeInvalid); + } + if f.order > 0 { + let bits = coef_bits(w.coef_res, f.coef_compress); + let coef_max = (1u32 << bits) - 1; + for c in &f.coef { + if u32::from(*c) > coef_max { + return Err(Error::TnsDataEncodeInvalid); + } + } + } + } + } + + for w in &self.windows { + writer.write_u32(w.filters.len() as u32, n_filt_bits); + if !w.filters.is_empty() { + writer.write_bit(w.coef_res); + } + for f in &w.filters { + writer.write_u32(u32::from(f.length), length_bits); + writer.write_u32(u32::from(f.order), order_bits); + if f.order > 0 { + writer.write_bit(f.direction); + writer.write_bit(f.coef_compress); + let bits = coef_bits(w.coef_res, f.coef_compress); + for c in &f.coef { + writer.write_u32(u32::from(*c), bits); + } + } + } + } + Ok(()) + } +} + +/// A caller-supplied width triple is sane when every field fits the +/// `u8`-backed record (`1..=8` bits). +fn widths_valid((n_filt_bits, length_bits, order_bits): (u32, u32, u32)) -> bool { + (1..=8).contains(&n_filt_bits) + && (1..=8).contains(&length_bits) + && (1..=8).contains(&order_bits) +} + +fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { + debug_assert!(n <= 8); + Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) +} diff --git a/crates/vendor/oxideav-aac/src/tns_frame.rs b/crates/vendor/oxideav-aac/src/tns_frame.rs new file mode 100644 index 00000000..16564a37 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/tns_frame.rs @@ -0,0 +1,1035 @@ +//! §4.6.9.3 `tns_decode_frame()` — per-frame Temporal Noise Shaping +//! orchestration. +//! +//! This module chains the three TNS building blocks that previous +//! rounds landed into the spec's outer per-frame loop: +//! +//! * [`crate::tns_data`] — the Table 4.54 wire parser that yields the +//! per-window `n_filt` / `coef_res` and per-filter `length` / +//! `order` / `direction` / `coef_compress` / `coef[]` fields; +//! * [`crate::tns_coef::tns_decode_coef_to_lpc`] — the §4.6.9.3 +//! `tns_decode_coef()` inverse-quantisation + conversion-to-LPC +//! step-up; +//! * [`crate::tns_coef::tns_ar_filter`] — the §4.6.9.3 +//! `tns_ar_filter()` all-pole IIR pass over a strided spectral +//! region. +//! +//! The orchestration follows the §4.6.9.3 pseudocode: +//! +//! ```text +//! tns_decode_frame() +//! { +//! for (w = 0; w < num_windows; w++) { +//! bottom = num_swb; +//! for (f = 0; f < n_filt[w]; f++) { +//! top = bottom; +//! bottom = max( top - length[w][f], 0 ); +//! tns_order = min( order[w][f], TNS_MAX_ORDER ); +//! if (!tns_order) continue; +//! tns_decode_coef( tns_order, coef_res[w]+3, +//! coef_compress[w][f], coef[w][f], lpc[] ); +//! start = swb_offset[min(bottom, TNS_MAX_BANDS, max_sfb)]; +//! end = swb_offset[min(top, TNS_MAX_BANDS, max_sfb)]; +//! if ((size = end - start) <= 0) continue; +//! if (direction[w][f]) { inc = -1; start = end - 1; } +//! else { inc = 1; } +//! tns_ar_filter( &spec[w][start], size, inc, lpc[], tns_order ); +//! } +//! } +//! } +//! ``` +//! +//! Filter regions are sliced top-down: the first transmitted filter +//! covers the topmost `length[w][0]` scalefactor bands (counting down +//! from `num_swb`), the next filter covers the `length[w][1]` bands +//! immediately below, and so on, with `bottom` clamped at band 0. The +//! band → coefficient-index mapping goes through the +//! [`crate::swb_offset`] tables, with each lookup index clamped by the +//! three-way `min(band, TNS_MAX_BANDS, max_sfb)` +//! ([`crate::tns_max::clamp_tns_band`]); the filter order is clamped +//! by `TNS_MAX_ORDER` ([`crate::tns_max::clamp_tns_order`]). Both +//! caps are object-type-dependent (Tables 4.102 / 4.103). +//! +//! Scope: the canonical 1024-line long / 8 × 128-line short frames +//! that the [`crate::swb_offset`] tables cover. The ER AAC LD +//! 480/512-line frames (Tables 4.119 / 4.120 band caps, dedicated +//! `swb_offset` tables) remain deferred until the LD reconstruction +//! path is wired, matching the standing `int_tns_decode_coef()` +//! deferral. + +use crate::ics_info::IcsInfo; +use crate::ics_info::WindowSequence; +#[cfg(test)] +use crate::swb_offset::{long_window_offsets, short_window_offsets}; +use crate::swb_offset::{long_window_offsets_family, short_window_offsets_family, FrameFamily}; +use crate::tns_coef::{tns_ar_filter, tns_decode_coef_to_lpc, tns_ma_filter}; +use crate::tns_data::{num_windows, TnsData}; +use crate::tns_max::{clamp_tns_band_family, clamp_tns_order}; +use crate::{Error, Result}; + +/// Apply §4.6.9.3 `tns_decode_frame()` to one channel's dequantised +/// spectrum, in place. +/// +/// ## Inputs +/// +/// * `spec` — the channel's full-frame coefficient buffer, windows +/// concatenated in order: `num_windows × window_len` samples, i.e. +/// `8 × 128 = 1024` for `EIGHT_SHORT_SEQUENCE` and `1 × 1024` +/// otherwise. Window `w` occupies +/// `spec[w * window_len .. (w + 1) * window_len]` (the pseudocode's +/// `spec[w][..]`). +/// * `tns` — the parsed [`TnsData`] block for this channel. Its +/// window count must match `window_sequence` (which +/// [`TnsData::parse`] guarantees when called under the same +/// sequence). +/// * `window_sequence` — the surrounding `ics_info()` window +/// sequence; selects `num_windows`, `window_len`, the +/// [`crate::swb_offset`] table, and the short/long columns of +/// Tables 4.102 / 4.103. +/// * `max_sfb` — the surrounding `ics_info()` field; third operand of +/// the §4.6.9.3 band clamp. +/// * `aot` — `audioObjectType` (Table 1.17); selects the +/// `TNS_MAX_ORDER` row and the PQF / non-PQF `TNS_MAX_BANDS` +/// columns. +/// * `fs_index` — `samplingFrequencyIndex` (Table 1.18, `0..=11`); +/// selects the `swb_offset` table and the `TNS_MAX_BANDS` row. +/// +/// ## Errors +/// +/// * [`Error::TnsFrameInvalid`] — `spec.len()` is not +/// `num_windows × window_len`; `tns.windows.len()` disagrees with +/// `window_sequence`; or a filter's `coef` vector is shorter than +/// its `TNS_MAX_ORDER`-clamped `tns_order` (a fabricated +/// structure — the wire parser always emits `coef.len() == order`). +/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] — `fs_index` has no +/// `swb_offset` / `TNS_MAX_BANDS` entry (`>= 12`). +/// * [`Error::TnsCoefOutOfRange`] — a wire `coef[i]` magnitude does +/// not fit `coef_res2 = coef_res_bits − coef_compress` bits +/// (propagated from [`tns_decode_coef_to_lpc`]). +/// +/// An order-0 filter and an empty (clamped-away) region are +/// well-defined no-ops per the pseudocode's `continue` arms; a +/// `TnsData` with no filters at all leaves `spec` untouched. +pub fn tns_decode_frame( + spec: &mut [f64], + tns: &TnsData, + window_sequence: WindowSequence, + max_sfb: u8, + aot: u8, + fs_index: u8, +) -> Result<()> { + tns_frame_filter( + spec, + tns, + FrameFamily::Lc1024, + window_sequence, + max_sfb, + aot, + fs_index, + TnsFilterKind::Synthesis, + ) +} + +/// [`tns_decode_frame`] driven by a parsed [`IcsInfo`] — the frame's +/// §4.5.1.1 family, `window_sequence` and `max_sfb` all come from the +/// side info, so the 960 / LD geometries (window lengths, family SWB +/// tables and the §4.6.17.2.5 LD `TNS_MAX_BANDS`) are selected +/// consistently with the rest of the channel decode. +pub fn tns_decode_frame_ics( + spec: &mut [f64], + tns: &TnsData, + ics_info: &IcsInfo, + aot: u8, + fs_index: u8, +) -> Result<()> { + tns_frame_filter( + spec, + tns, + ics_info.family, + ics_info.window_sequence, + ics_info.max_sfb, + aot, + fs_index, + TnsFilterKind::Synthesis, + ) +} + +/// §4.6.7.4.1 TNS **analysis** pass — the same per-window / per-filter +/// region walk as [`tns_decode_frame`], but applying the all-zero +/// [`tns_ma_filter`] (the inverse of the §4.6.9.3 all-pole synthesis +/// filter) instead. +/// +/// Figure 4.30 requires this forward filter inside the LTP loop: the +/// LTP-predicted spectrum `X_est = MDCT(x_est)` must be moved into the +/// noise-shaped residual domain (the domain the transmitted `Y_rec` +/// lives in, *before* TNS synthesis) so that `X_rec = X_est + Y_rec` +/// adds like-for-like. The subsequent §4.6.9 TNS synthesis pass over +/// `X_rec` then undoes the analysis on the LTP contribution while +/// shaping the residual, exactly as the all-pole filter inverts the +/// all-zero one over a shared region. +/// +/// Inputs, scope and errors mirror [`tns_decode_frame`]; the only +/// difference is the filter polarity. When `tns` carries no filters +/// (or only order-0 / empty-region filters) the spectrum is untouched, +/// so a channel without TNS needs no analysis pass. +pub fn tns_analysis_frame( + spec: &mut [f64], + tns: &TnsData, + window_sequence: WindowSequence, + max_sfb: u8, + aot: u8, + fs_index: u8, +) -> Result<()> { + tns_frame_filter( + spec, + tns, + FrameFamily::Lc1024, + window_sequence, + max_sfb, + aot, + fs_index, + TnsFilterKind::Analysis, + ) +} + +/// [`tns_analysis_frame`] driven by a parsed [`IcsInfo`] (see +/// [`tns_decode_frame_ics`] for the family selection). +pub fn tns_analysis_frame_ics( + spec: &mut [f64], + tns: &TnsData, + ics_info: &IcsInfo, + aot: u8, + fs_index: u8, +) -> Result<()> { + tns_frame_filter( + spec, + tns, + ics_info.family, + ics_info.window_sequence, + ics_info.max_sfb, + aot, + fs_index, + TnsFilterKind::Analysis, + ) +} + +/// Which TNS filter polarity [`tns_frame_filter`] applies over each +/// region: the §4.6.9.3 all-pole synthesis filter (the normal decode +/// path) or the §4.6.7.4.1 all-zero analysis filter (the LTP loop). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum TnsFilterKind { + /// All-pole [`tns_ar_filter`] — §4.6.9.3 decode. + Synthesis, + /// All-zero [`tns_ma_filter`] — §4.6.7.4.1 LTP-loop analysis. + Analysis, +} + +/// Shared §4.6.9.3 region walk for both TNS polarities. Identical band +/// clamping, coefficient decode and region selection; only the final +/// per-region filter call differs (`kind`). +#[allow(clippy::too_many_arguments)] +fn tns_frame_filter( + spec: &mut [f64], + tns: &TnsData, + family: FrameFamily, + window_sequence: WindowSequence, + max_sfb: u8, + aot: u8, + fs_index: u8, + kind: TnsFilterKind, +) -> Result<()> { + let windows = num_windows(window_sequence); + let (window_len, offsets) = if window_sequence.is_eight_short() { + ( + family.short_window_len().ok_or(Error::LdShortWindow)?, + short_window_offsets_family(family, fs_index)?, + ) + } else { + ( + family.frame_len(), + long_window_offsets_family(family, fs_index)?, + ) + }; + + if tns.windows.len() != windows { + return Err(Error::TnsFrameInvalid); + } + if spec.len() != windows * window_len { + return Err(Error::TnsFrameInvalid); + } + + // `num_swb + 1` entries per swb_offset table; the top band index + // (the pseudocode's initial `bottom = num_swb`) is the sentinel + // slot, so every clamped lookup below stays in bounds. + let num_swb = offsets.len() - 1; + + for (w, tns_window) in tns.windows.iter().enumerate() { + let coef_res_bits = 3 + u32::from(tns_window.coef_res); + let window_spec = &mut spec[w * window_len..(w + 1) * window_len]; + + let mut bottom = num_swb; + for filter in &tns_window.filters { + let top = bottom; + bottom = top.saturating_sub(filter.length as usize); + + let tns_order = clamp_tns_order(filter.order, aot, window_sequence, fs_index)? as usize; + if tns_order == 0 { + continue; + } + if filter.coef.len() < tns_order { + return Err(Error::TnsFrameInvalid); + } + + // tns_decode_coef( tns_order, coef_res[w]+3, + // coef_compress[w][f], coef[w][f], lpc[] ) + // — only the first `tns_order` transmitted magnitudes + // participate when the wire `order` exceeded the cap. + let coef: Vec = filter.coef[..tns_order] + .iter() + .map(|&c| u32::from(c)) + .collect(); + let lpc = + tns_decode_coef_to_lpc(coef_res_bits, u32::from(filter.coef_compress), &coef)?; + + // Band indices are at most `num_swb` (bottom/top start + // there and only decrease), so the u8 narrowing is exact: + // every standard table has num_swb <= 51. + let start_band = clamp_tns_band_family( + bottom as u8, + max_sfb, + family, + aot, + window_sequence, + fs_index, + )?; + let end_band = + clamp_tns_band_family(top as u8, max_sfb, family, aot, window_sequence, fs_index)?; + let start = offsets[start_band as usize] as usize; + let end = offsets[end_band as usize] as usize; + if end <= start { + continue; + } + let size = end - start; + + let (filter_start, inc) = if filter.direction { + (end - 1, -1) + } else { + (start, 1) + }; + match kind { + TnsFilterKind::Synthesis => { + tns_ar_filter(window_spec, filter_start, size, inc, &lpc)?; + } + TnsFilterKind::Analysis => { + tns_ma_filter(window_spec, filter_start, size, inc, &lpc)?; + } + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tns_data::{TnsFilter, TnsWindow}; + use crate::tns_max::{tns_max_bands, tns_max_order, AOT_AAC_LC, AOT_AAC_MAIN}; + + /// 48 kHz — long-window table has 49 SWBs (sentinel 1024), short + /// has 14 (sentinel 128). + const FS_48K: u8 = 3; + + fn ramp(len: usize) -> Vec { + (0..len).map(|i| (i % 97) as f64 * 0.25 - 12.0).collect() + } + + fn long_window(filters: Vec, coef_res: bool) -> TnsData { + TnsData { + windows: vec![TnsWindow { coef_res, filters }], + } + } + + fn no_filter_window() -> TnsWindow { + TnsWindow { + coef_res: false, + filters: vec![], + } + } + + // ===== no-op paths ===== + + #[test] + fn empty_tns_data_leaves_spectrum_untouched() { + let mut spec = ramp(1024); + let want = spec.clone(); + let tns = long_window(vec![], false); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + #[test] + fn order_zero_filter_is_a_no_op() { + let mut spec = ramp(1024); + let want = spec.clone(); + let tns = long_window( + vec![TnsFilter { + length: 49, + order: 0, + direction: false, + coef_compress: false, + coef: vec![], + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + #[test] + fn zero_length_region_is_a_no_op() { + // length = 0 → bottom == top → end == start → `continue` arm. + let mut spec = ramp(1024); + let want = spec.clone(); + let tns = long_window( + vec![TnsFilter { + length: 0, + order: 2, + direction: false, + coef_compress: false, + coef: vec![1, 2], + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + // ===== single-filter long window: composition equivalence ===== + + /// The orchestrator must produce exactly the manual composition + /// `tns_decode_coef_to_lpc` + `tns_ar_filter` over the region the + /// §4.6.9.3 band arithmetic selects. + #[test] + fn single_upward_filter_matches_manual_composition() { + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; // 49 + let length = 10_u8; + let coef: Vec = vec![1, 7, 2]; // 3-bit wire magnitudes + let order = coef.len() as u8; + + let mut spec = ramp(1024); + let mut want = spec.clone(); + + // Manual composition. max_sfb = num_swb and TNS_MAX_BANDS for + // LC long @48k >= 40, so top clamps to min(49, cap, 49) and + // bottom to min(39, cap, 49). + let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + let top = num_swb.min(cap).min(num_swb); + let bottom = (num_swb - length as usize).min(cap).min(num_swb); + let start = offsets[bottom] as usize; + let end = offsets[top] as usize; + let coef_u32: Vec = coef.iter().map(|&c| u32::from(c)).collect(); + let lpc = tns_decode_coef_to_lpc(3, 0, &coef_u32).unwrap(); + tns_ar_filter(&mut want, start, end - start, 1, &lpc).unwrap(); + + let tns = long_window( + vec![TnsFilter { + length, + order, + direction: false, + coef_compress: false, + coef, + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + // The filter genuinely changed something inside the region. + assert_ne!(spec[start..end], ramp(1024)[start..end]); + } + + #[test] + fn downward_filter_matches_manual_composition() { + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; + let length = 8_u8; + let coef: Vec = vec![3, 14, 9]; // 4-bit wire magnitudes + let order = coef.len() as u8; + + let mut spec = ramp(1024); + let mut want = spec.clone(); + + let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + let top = num_swb.min(cap); + let bottom = (num_swb - length as usize).min(cap); + let start = offsets[bottom] as usize; + let end = offsets[top] as usize; + let coef_u32: Vec = coef.iter().map(|&c| u32::from(c)).collect(); + let lpc = tns_decode_coef_to_lpc(4, 0, &coef_u32).unwrap(); + // direction = 1 → inc = -1, start = end - 1. + tns_ar_filter(&mut want, end - 1, end - start, -1, &lpc).unwrap(); + + let tns = long_window( + vec![TnsFilter { + length, + order, + direction: true, + coef_compress: false, + coef, + }], + true, // coef_res = 1 → coef_res_bits = 4 + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + // ===== region slicing ===== + + #[test] + fn filter_region_counts_down_from_top_band_and_leaves_rest_untouched() { + // LC long @48 kHz: TNS_MAX_BANDS = 40 < num_swb = 49, so the + // §4.6.9.3 three-way min clamps both region ends. length = 15 + // → bottom = 34, top = 49→40: the live region is + // swb_offset[34]..swb_offset[40]; bands 40..49 are clamped + // away entirely. + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; + let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + assert_eq!(cap, 40); + let length = 15_u8; + let bottom = num_swb - length as usize; // 34, below the cap + let start = offsets[bottom] as usize; + let end = offsets[cap] as usize; + + let mut spec = ramp(1024); + let before = spec.clone(); + let tns = long_window( + vec![TnsFilter { + length, + order: 1, + direction: false, + coef_compress: false, + coef: vec![2], + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + // Everything below swb_offset[bottom] is untouched. + assert_eq!(spec[..start], before[..start]); + // Everything above the TNS_MAX_BANDS clamp is untouched too. + assert_eq!(spec[end..], before[end..]); + // The surviving clamped region was genuinely filtered. + assert_ne!(spec[start..end], before[start..end]); + } + + #[test] + fn second_filter_covers_bands_below_the_first() { + // Two filters: f0 covers the top 14 bands, f1 the 7 bands + // below them. Verify against a manual two-pass composition. + // With TNS_MAX_BANDS = 40 < num_swb = 49 the f0 region top + // clamps to band 40 while its bottom (35) survives, and the + // f1 region (28..35) lies entirely below the cap. + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; + let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + let (len0, len1) = (14_u8, 7_u8); + + let mut spec = ramp(1024); + let mut want = spec.clone(); + + let top0 = num_swb; + let bottom0 = top0 - len0 as usize; + let lpc0 = tns_decode_coef_to_lpc(3, 0, &[4]).unwrap(); + let s0 = offsets[bottom0.min(cap)] as usize; + let e0 = offsets[top0.min(cap)] as usize; + tns_ar_filter(&mut want, s0, e0 - s0, 1, &lpc0).unwrap(); + + let top1 = bottom0; + let bottom1 = top1 - len1 as usize; + let lpc1 = tns_decode_coef_to_lpc(3, 0, &[7, 1]).unwrap(); + let s1 = offsets[bottom1.min(cap)] as usize; + let e1 = offsets[top1.min(cap)] as usize; + tns_ar_filter(&mut want, e1 - 1, e1 - s1, -1, &lpc1).unwrap(); + + let tns = long_window( + vec![ + TnsFilter { + length: len0, + order: 1, + direction: false, + coef_compress: false, + coef: vec![4], + }, + TnsFilter { + length: len1, + order: 2, + direction: true, + coef_compress: false, + coef: vec![7, 1], + }, + ], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + // The two regions are disjoint and both genuinely filtered. + assert!(s1 < e1 && e1 == s0 && s0 < e0); + } + + #[test] + fn length_overrun_saturates_bottom_at_band_zero() { + // length = 63 (max 6-bit wire value) > num_swb → bottom = 0, + // region = whole clamped spectrum. + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; + let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + + let mut spec = ramp(1024); + let mut want = spec.clone(); + let lpc = tns_decode_coef_to_lpc(3, 0, &[5]).unwrap(); + let end = offsets[num_swb.min(cap)] as usize; + tns_ar_filter(&mut want, 0, end, 1, &lpc).unwrap(); + + let tns = long_window( + vec![TnsFilter { + length: 63, + order: 1, + direction: false, + coef_compress: false, + coef: vec![5], + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + // ===== clamps ===== + + #[test] + fn max_sfb_clamps_the_filter_region_top() { + // max_sfb = 20 → end = swb_offset[20]; coefficients above it + // must stay untouched even though the filter nominally covers + // the top 30 bands. + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; + let max_sfb = 20_u8; + let end = offsets[max_sfb as usize] as usize; + + let mut spec = ramp(1024); + let before = spec.clone(); + let tns = long_window( + vec![TnsFilter { + length: 30, + order: 1, + direction: false, + coef_compress: false, + coef: vec![6], + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + max_sfb, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec[end..], before[end..]); + // bottom = 49 - 30 = 19 < max_sfb → a 1-band region survives + // the clamp and is filtered. + let start = offsets[(num_swb - 30).min(max_sfb as usize)] as usize; + assert_ne!(spec[start..end], before[start..end]); + } + + #[test] + fn fully_clamped_region_is_a_no_op() { + // bottom = 49 - 5 = 44 > max_sfb = 10 → both ends clamp to + // swb_offset[10] → size = 0 → continue. + let mut spec = ramp(1024); + let want = spec.clone(); + let tns = long_window( + vec![TnsFilter { + length: 5, + order: 1, + direction: false, + coef_compress: false, + coef: vec![3], + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 10, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + #[test] + fn wire_order_is_clamped_by_tns_max_order() { + // AOT LC long → TNS_MAX_ORDER = 12. A wire order of 15 must + // use only the first 12 transmitted magnitudes. + let cap = tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + assert_eq!(cap, 12); + let coef: Vec = (0..15).map(|i| (i % 8) as u8).collect(); + + let offsets = long_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; + let band_cap = + tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; + let length = 12_u8; + let bottom = num_swb - length as usize; + let start = offsets[bottom.min(band_cap)] as usize; + let end = offsets[num_swb.min(band_cap)] as usize; + + let mut spec = ramp(1024); + let mut want = spec.clone(); + let coef_u32: Vec = coef[..cap].iter().map(|&c| u32::from(c)).collect(); + let lpc = tns_decode_coef_to_lpc(3, 0, &coef_u32).unwrap(); + assert_eq!(lpc.len(), cap + 1); + tns_ar_filter(&mut want, start, end - start, 1, &lpc).unwrap(); + + let tns = long_window( + vec![TnsFilter { + length, + order: 15, + direction: false, + coef_compress: false, + coef, + }], + false, + ); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, want); + } + + #[test] + fn aac_main_long_window_allows_order_up_to_20() { + // Same wire order 15, but AOT Main (TNS_MAX_ORDER = 20 long): + // all 15 magnitudes participate, so the output differs from + // the LC-clamped run. + let coef: Vec = (0..15).map(|i| ((i * 3) % 8) as u8).collect(); + let mk = |aot: u8| { + let mut spec = ramp(1024); + let tns = long_window( + vec![TnsFilter { + length: 12, + order: 15, + direction: false, + coef_compress: false, + coef: coef.clone(), + }], + false, + ); + tns_decode_frame(&mut spec, &tns, WindowSequence::OnlyLong, 49, aot, FS_48K).unwrap(); + spec + }; + assert_ne!(mk(AOT_AAC_MAIN), mk(AOT_AAC_LC)); + } + + // ===== short windows ===== + + #[test] + fn short_sequence_filters_only_the_targeted_window() { + // 8 × 128 frame; a single filter on window 3 must leave the + // other 7 windows byte-identical. + let offsets = short_window_offsets(FS_48K).unwrap(); + let num_swb = offsets.len() - 1; // 14 + let mut windows: Vec = (0..8).map(|_| no_filter_window()).collect(); + windows[3] = TnsWindow { + coef_res: false, + filters: vec![TnsFilter { + length: num_swb as u8, + order: 2, + direction: false, + coef_compress: false, + coef: vec![1, 6], + }], + }; + let tns = TnsData { windows }; + + let mut spec = ramp(1024); + let before = spec.clone(); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::EightShort, + num_swb as u8, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec[..3 * 128], before[..3 * 128]); + assert_eq!(spec[4 * 128..], before[4 * 128..]); + assert_ne!(spec[3 * 128..4 * 128], before[3 * 128..4 * 128]); + + // And window 3 matches the manual composition on its slice. + let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::EightShort, FS_48K).unwrap() as usize; + let end = offsets[num_swb.min(cap)] as usize; + let lpc = tns_decode_coef_to_lpc(3, 0, &[1, 6]).unwrap(); + let mut want_w3 = before[3 * 128..4 * 128].to_vec(); + tns_ar_filter(&mut want_w3, 0, end, 1, &lpc).unwrap(); + assert_eq!(spec[3 * 128..4 * 128], want_w3); + } + + // ===== validation ===== + + #[test] + fn rejects_spectrum_length_mismatch() { + let mut spec = ramp(512); + let tns = long_window(vec![], false); + assert!(matches!( + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K + ), + Err(Error::TnsFrameInvalid) + )); + } + + #[test] + fn rejects_window_count_mismatch() { + // 1 TnsWindow under EIGHT_SHORT_SEQUENCE (needs 8). + let mut spec = ramp(1024); + let tns = long_window(vec![], false); + assert!(matches!( + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::EightShort, + 14, + AOT_AAC_LC, + FS_48K + ), + Err(Error::TnsFrameInvalid) + )); + } + + #[test] + fn rejects_coef_shorter_than_clamped_order() { + let mut spec = ramp(1024); + let tns = long_window( + vec![TnsFilter { + length: 10, + order: 3, + direction: false, + coef_compress: false, + coef: vec![1], // < clamped order 3 + }], + false, + ); + assert!(matches!( + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K + ), + Err(Error::TnsFrameInvalid) + )); + } + + #[test] + fn rejects_unsupported_fs_index() { + let mut spec = ramp(1024); + let tns = long_window(vec![], false); + assert!(matches!( + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + 12 + ), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + } + + #[test] + fn propagates_coef_out_of_range_from_decode() { + // coef_compress = 1 with coef_res = 0 → coef_res2 = 2 bits; + // a magnitude of 4 overflows the field. + let mut spec = ramp(1024); + let tns = long_window( + vec![TnsFilter { + length: 10, + order: 1, + direction: false, + coef_compress: true, + coef: vec![4], + }], + false, + ); + assert!(matches!( + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K + ), + Err(Error::TnsCoefOutOfRange) + )); + } + + // ===== §4.6.7.4.1 analysis pass ===== + + #[test] + fn analysis_then_synthesis_is_identity() { + // The LTP-loop analysis filter (all-zero) followed by the §4.6.9 + // synthesis filter (all-pole), over the same frame, reconstructs + // the spectrum exactly — the §4.6.7.4.1 invariant that lets the + // single TNS synthesis pass after the LTP add undo the analysis + // on X_est while shaping the residual. + let tns = long_window( + vec![ + TnsFilter { + length: 12, + order: 3, + direction: false, + coef_compress: false, + coef: vec![1, 7, 2], + }, + TnsFilter { + length: 8, + order: 2, + direction: true, + coef_compress: false, + coef: vec![6, 3], + }, + ], + false, + ); + let original = ramp(1024); + let mut spec = original.clone(); + tns_analysis_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + // The analysis pass actually changed the spectrum. + assert_ne!(spec, original); + tns_decode_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + for (g, w) in spec.iter().zip(original.iter()) { + assert!((g - w).abs() < 1e-9, "analysis∘synthesis drift: {g} vs {w}"); + } + } + + #[test] + fn analysis_no_filters_is_noop() { + let tns = TnsData { + windows: vec![no_filter_window()], + }; + let original = ramp(1024); + let mut spec = original.clone(); + tns_analysis_frame( + &mut spec, + &tns, + WindowSequence::OnlyLong, + 49, + AOT_AAC_LC, + FS_48K, + ) + .unwrap(); + assert_eq!(spec, original); + } +} diff --git a/crates/vendor/oxideav-aac/src/tns_max.rs b/crates/vendor/oxideav-aac/src/tns_max.rs new file mode 100644 index 00000000..db07fcc7 --- /dev/null +++ b/crates/vendor/oxideav-aac/src/tns_max.rs @@ -0,0 +1,750 @@ +//! Maximum TNS filter order and bandwidth lookup tables — +//! ISO/IEC 14496-3 §4.6.9.4 Tables 4.102 / 4.103 (general AAC) +//! and §4.6.17.2.5 Tables 4.119 / 4.120 (AAC LD). +//! +//! ## What this module covers +//! +//! TNS (Temporal Noise Shaping) places caps on two per-filter wire +//! quantities that the decoder must clamp during reconstruction +//! (the dispatching `tns_data()` parser, [`crate::tns_data`], +//! surfaces wire values *literally* — clamping happens here): +//! +//! * `TNS_MAX_ORDER` (Table 4.102) — the upper bound for +//! `order[w][filt]` as a function of audio-object type, window +//! sequence, and whether the surrounding stream's sampling rate +//! exceeds 32 kHz. +//! * `TNS_MAX_BANDS` (Table 4.103) — the upper bound for the +//! `bottom` and `top` band indices a TNS filter touches, as a +//! function of audio-object type and `samplingFrequencyIndex`. +//! Two AOT families dispatch differently: AOT 3 (AAC SSR) uses the +//! polyphase-quadrature-filterbank columns; every other GA AOT +//! uses the non-PQF columns. +//! * `TNS_MAX_BANDS` for AAC LD (Tables 4.119 / 4.120) — a separate +//! pair of tables keyed by the AAC LD frame size (480 vs 512 +//! samples) and sampling rate, used by AOT 23 (ER AAC LD). +//! +//! ## How the spec applies these caps +//! +//! Per §4.6.9.3 the TNS reconstruction loop clamps the wire `order` +//! and the per-filter band range with: +//! +//! ```text +//! tns_order = min(order[w][f], TNS_MAX_ORDER); +//! start = swb_offset[min(bottom, TNS_MAX_BANDS, max_sfb)]; +//! end = swb_offset[min(top, TNS_MAX_BANDS, max_sfb)]; +//! ``` +//! +//! [`tns_max_order`] and [`tns_max_bands`] return those caps; the +//! [`clamp_tns_order`] / [`clamp_tns_band`] helpers fold the +//! `min` chain into one call so the eventual reconstruction layer +//! consumes them without re-deriving the dispatch from the AOT. +//! +//! ## AOT dispatch +//! +//! The Table 4.102 row map: +//! +//! | AOT | name | row | +//! |-----------|-----------------------|--------------------| +//! | 1 | AAC Main | first row | +//! | 2 | AAC LC | second row | +//! | 3 | AAC SSR | third row | +//! | 4, 17, 19, 20, 21, 22, 23 | other GA + ER variants using TNS | fourth row ("other AOT using TNS") | +//! +//! AOT 6 (AAC Scalable) and AOT 7 (TwinVQ) are GA dispatch targets +//! per [`crate::asc::GA_AOTS`] but do not use the AAC TNS surface +//! verbatim — AOT 6 wraps an inner AAC layer (which picks its own +//! row), and AOT 7 is a different frequency-domain codec entirely. +//! The accessor surfaces them as the "other" row when invoked, since +//! the field-width dispatch in [`crate::tns_data`] does not gate on +//! AOT in any case. +//! +//! The Table 4.103 column map: +//! +//! | AOT | columns used | +//! |------|---------------------------------------| +//! | 1, 2, 4, 6, 7, 17, 19, 20, 21, 22 | columns 1 (long) / 2 (short) — "without PQF filterbank" | +//! | 3 | columns 3 (long) / 4 (short) — "with PQF filterbank" | +//! +//! AOT 23 (ER AAC LD) does **not** use Table 4.103 at all; its +//! `TNS_MAX_BANDS` cap comes from the §4.6.17.2.5 LD-specific tables +//! [`TNS_MAX_BANDS_LD_480`] / [`TNS_MAX_BANDS_LD_512`] keyed by the +//! AAC LD frame size (480 vs 512 samples). The crate's frame-size +//! tracking is the responsibility of the dispatching +//! `individual_channel_stream()` layer (not landed yet); the +//! accessor here exposes both tables as a stand-alone surface so +//! the eventual LD reconstruction loop can pick the right one. +//! +//! ## What this module does *not* cover +//! +//! * No wire-format I/O. The clamps are decoder-side reconstruction +//! constraints; the literal `length` / `order` values are still +//! written and read by [`crate::tns_data`] without clamping. +//! * No actual TNS LPC reconstruction. That belongs in the +//! per-AOT IMDCT back-end (not yet present in this crate). +//! * No ER AAC ELD (AOT 39) TNS cap. ELD uses its own MDCT length +//! (480 / 512 like AAC LD) and an ELD-specific reconstruction +//! path; the spec subclause for that cap lives in §4.6.20 and is +//! deferred until ELD-specific machinery lands. +//! * No xHE-AAC / USAC (AOT 42) caps. USAC's TNS is governed by +//! ISO/IEC 23003-3 which is out of scope for this crate. + +use crate::ics_info::WindowSequence; +use crate::{Error, Result}; + +/// AOT 1 — AAC Main. +pub const AOT_AAC_MAIN: u8 = 1; +/// AOT 2 — AAC LC (Low Complexity). +pub const AOT_AAC_LC: u8 = 2; +/// AOT 3 — AAC SSR (Scalable Sampling Rate). Uses the PQF-filterbank +/// columns of Table 4.103. +pub const AOT_AAC_SSR: u8 = 3; +/// AOT 4 — AAC LTP (Long-Term Prediction). +pub const AOT_AAC_LTP: u8 = 4; +/// AOT 23 — ER AAC LD (Low Delay). Uses the §4.6.17.2.5 LD-specific +/// `TNS_MAX_BANDS` tables, not Table 4.103. +pub const AOT_ER_AAC_LD: u8 = 23; + +/// Sample-rate index threshold for the "short window / long window +/// >32 kHz / long window ≤32 kHz" partition in Table 4.102. +/// +/// `samplingFrequencyIndex` 0..=4 cover 96000 / 88200 / 64000 / +/// 48000 / 44100 Hz — all > 32 kHz. Index 5 is exactly 32 kHz which +/// the table's `<= 32kHz` column also covers. Indices 6..=11 cover +/// 24000 / 22050 / 16000 / 12000 / 11025 / 8000 Hz — all ≤ 32 kHz. +/// Index 12 (7350 Hz) is also ≤ 32 kHz. +const FS_INDEX_FIRST_LE_32K: u8 = 5; + +/// `TNS_MAX_BANDS` lookup for AOTs that use the "without PQF +/// filterbank" columns of Table 4.103 with **long** windows. Indexed +/// by `samplingFrequencyIndex` 0..=11 — slot 12 (7350 Hz) is not +/// covered by the table. +const TNS_MAX_BANDS_LONG_NON_PQF: [u8; 12] = [31, 31, 34, 40, 42, 51, 46, 46, 42, 42, 42, 39]; + +/// `TNS_MAX_BANDS` lookup for AOTs that use the "without PQF +/// filterbank" columns of Table 4.103 with **short** windows. +const TNS_MAX_BANDS_SHORT_NON_PQF: [u8; 12] = [9, 9, 10, 14, 14, 14, 14, 14, 14, 14, 14, 14]; + +/// `TNS_MAX_BANDS` lookup for AOT 3 (AAC SSR) — the "with PQF +/// filterbank" columns of Table 4.103 — with **long** windows. +const TNS_MAX_BANDS_LONG_PQF: [u8; 12] = [28, 28, 27, 26, 26, 26, 29, 29, 23, 23, 23, 19]; + +/// `TNS_MAX_BANDS` lookup for AOT 3 (AAC SSR) — the "with PQF +/// filterbank" columns of Table 4.103 — with **short** windows. +const TNS_MAX_BANDS_SHORT_PQF: [u8; 12] = [7, 7, 7, 6, 6, 6, 7, 7, 8, 8, 8, 7]; + +/// `TNS_MAX_BANDS` for the AAC LD coder when the frame is 480 +/// samples per ISO/IEC 14496-3 Table 4.119. Indexed by +/// `samplingFrequencyIndex` 0..=11; entries marked `None` mean the +/// rate is not covered by the table (Table 4.119 only specifies +/// 48000, 44100, 32000, 24000, 22050 Hz — fs indices 3, 4, 5, 6, 7). +pub const TNS_MAX_BANDS_LD_480: [Option; 12] = [ + None, // 0 = 96000 + None, // 1 = 88200 + None, // 2 = 64000 + Some(31), // 3 = 48000 + Some(32), // 4 = 44100 + Some(37), // 5 = 32000 + Some(30), // 6 = 24000 + Some(30), // 7 = 22050 + None, // 8 = 16000 + None, // 9 = 12000 + None, // 10 = 11025 + None, // 11 = 8000 +]; + +/// `TNS_MAX_BANDS` for the AAC LD coder when the frame is 512 +/// samples per ISO/IEC 14496-3 Table 4.120. Indexed by +/// `samplingFrequencyIndex` 0..=11. +pub const TNS_MAX_BANDS_LD_512: [Option; 12] = [ + None, // 0 = 96000 + None, // 1 = 88200 + None, // 2 = 64000 + Some(31), // 3 = 48000 + Some(32), // 4 = 44100 + Some(37), // 5 = 32000 + Some(31), // 6 = 24000 + Some(31), // 7 = 22050 + None, // 8 = 16000 + None, // 9 = 12000 + None, // 10 = 11025 + None, // 11 = 8000 +]; + +/// Look up `TNS_MAX_ORDER` per ISO/IEC 14496-3 Table 4.102. +/// +/// `aot` is the `audioObjectType` value driving the stream +/// (1 = Main, 2 = LC, 3 = SSR; all other AOTs fall into the +/// "other AOT using TNS" row of the table). `window_sequence` is +/// the per-frame `ics_info()` value; `EIGHT_SHORT_SEQUENCE` +/// dispatches the `short windows` column, every other sequence +/// dispatches one of the two `long windows` columns. `fs_index` is +/// `samplingFrequencyIndex` (Table 1.18) and partitions the long- +/// window dispatch between `> 32 kHz` (fs 0..=4) and `<= 32 kHz` +/// (fs 5..=12) per the Table 4.102 header. +/// +/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when +/// `fs_index >= 13`. +pub fn tns_max_order(aot: u8, window_sequence: WindowSequence, fs_index: u8) -> Result { + if fs_index >= 13 { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + + if window_sequence.is_eight_short() { + // Every AOT row collapses to 7 for short windows. + return Ok(7); + } + + let above_32k = fs_index < FS_INDEX_FIRST_LE_32K; + Ok(match aot { + AOT_AAC_MAIN => 20, + AOT_AAC_LC => 12, + AOT_AAC_SSR => 12, + _ => { + if above_32k { + 20 + } else { + 12 + } + } + }) +} + +/// Look up `TNS_MAX_BANDS` per ISO/IEC 14496-3 Table 4.103. +/// +/// `aot` selects the table column pair: AOT 3 (AAC SSR) uses the +/// "with PQF filterbank" columns; every other AOT uses the +/// "without PQF filterbank" columns. `window_sequence` distinguishes +/// the long-window column (every sequence except +/// `EIGHT_SHORT_SEQUENCE`) from the short-window column. +/// +/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when +/// `fs_index >= 12` (Table 4.103 does not cover fs 12 = 7350 Hz). +/// +/// For AOT 23 (ER AAC LD) this accessor returns the non-PQF Table +/// 4.103 entry as a syntactic fallback; callers in an LD stream +/// should use [`tns_max_bands_ld_480`] or [`tns_max_bands_ld_512`] +/// directly per the LD frame size in [`crate::asc`]. +pub fn tns_max_bands(aot: u8, window_sequence: WindowSequence, fs_index: u8) -> Result { + let idx = fs_index as usize; + if idx >= TNS_MAX_BANDS_LONG_NON_PQF.len() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + + let table = match (aot, window_sequence.is_eight_short()) { + (AOT_AAC_SSR, false) => &TNS_MAX_BANDS_LONG_PQF, + (AOT_AAC_SSR, true) => &TNS_MAX_BANDS_SHORT_PQF, + (_, false) => &TNS_MAX_BANDS_LONG_NON_PQF, + (_, true) => &TNS_MAX_BANDS_SHORT_NON_PQF, + }; + Ok(table[idx]) +} + +/// [`tns_max_bands`] under an explicit §4.5.1.1 frame-length family. +/// +/// * `Lc1024` / `Lc960` read Table 4.157 (its values are per sampling +/// rate, not per frame length; the §4.6.9.3 three-way `min` with +/// `max_sfb` keeps any 960-family band-count difference in bounds). +/// * `Ld512` / `Ld480` read the §4.6.17.2.5 LD tables (Tables 4.173 / +/// 4.172), with the §4.5.1.1 nearest-defined-table rule for the +/// rates those tables omit: 96 / 88.2 / 64 kHz resolve to the +/// 48 kHz entry, 16 kHz and below to the 22.05 kHz entry. +pub fn tns_max_bands_family( + family: crate::swb_offset::FrameFamily, + aot: u8, + window_sequence: WindowSequence, + fs_index: u8, +) -> Result { + use crate::swb_offset::FrameFamily; + match family { + FrameFamily::Lc1024 | FrameFamily::Lc960 => tns_max_bands(aot, window_sequence, fs_index), + FrameFamily::Ld512 | FrameFamily::Ld480 => { + if window_sequence.is_eight_short() { + return Err(Error::LdShortWindow); + } + // §4.5.1.1 nearest-defined-table rule (the LD tables only + // cover fs 3..=7). + let slot = match fs_index { + 0..=3 => 3, + 4..=7 => fs_index, + 8..=11 => 7, + other => return Err(Error::IcsInfoUnsupportedSampleRateIndex(other)), + }; + if family == FrameFamily::Ld512 { + tns_max_bands_ld_512(slot) + } else { + tns_max_bands_ld_480(slot) + } + } + } +} + +/// Look up `TNS_MAX_BANDS` for an AAC LD stream with a 480-sample +/// frame, per ISO/IEC 14496-3 Table 4.119. +/// +/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when +/// `fs_index >= 12`, and the same error when `fs_index` lies in the +/// table's covered range (0..=11) but the entry is `None` (i.e. the +/// sampling rate is not one of the five LD rates 48 / 44.1 / 32 / 24 / +/// 22.05 kHz). +pub fn tns_max_bands_ld_480(fs_index: u8) -> Result { + let idx = fs_index as usize; + if idx >= TNS_MAX_BANDS_LD_480.len() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + TNS_MAX_BANDS_LD_480[idx].ok_or(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)) +} + +/// Look up `TNS_MAX_BANDS` for an AAC LD stream with a 512-sample +/// frame, per ISO/IEC 14496-3 Table 4.120. +/// +/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when +/// `fs_index >= 12`, and the same error when the table entry is +/// `None` (Table 4.120 only covers fs indices 3, 4, 5, 6, 7). +pub fn tns_max_bands_ld_512(fs_index: u8) -> Result { + let idx = fs_index as usize; + if idx >= TNS_MAX_BANDS_LD_512.len() { + return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); + } + TNS_MAX_BANDS_LD_512[idx].ok_or(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)) +} + +/// Clamp a raw `order[w][filt]` wire value by `TNS_MAX_ORDER` per +/// §4.6.9.3: +/// +/// ```text +/// tns_order = min(order[w][f], TNS_MAX_ORDER); +/// ``` +/// +/// Returns the clamped order, or +/// [`Error::IcsInfoUnsupportedSampleRateIndex`] when the cap lookup +/// rejects `fs_index`. +pub fn clamp_tns_order( + order: u8, + aot: u8, + window_sequence: WindowSequence, + fs_index: u8, +) -> Result { + let cap = tns_max_order(aot, window_sequence, fs_index)?; + Ok(order.min(cap)) +} + +/// Clamp a TNS filter band-index (the `bottom` or `top` operand of +/// the swb_offset lookup) by `min(band, TNS_MAX_BANDS, max_sfb)` per +/// §4.6.9.3: +/// +/// ```text +/// start = swb_offset[min(bottom, TNS_MAX_BANDS, max_sfb)]; +/// end = swb_offset[min(top, TNS_MAX_BANDS, max_sfb)]; +/// ``` +/// +/// `max_sfb` is the surrounding `ics_info()` field. Returns the +/// three-way `min`. Errors mirror [`tns_max_bands`]. +pub fn clamp_tns_band( + band: u8, + max_sfb: u8, + aot: u8, + window_sequence: WindowSequence, + fs_index: u8, +) -> Result { + let cap = tns_max_bands(aot, window_sequence, fs_index)?; + Ok(band.min(cap).min(max_sfb)) +} + +/// [`clamp_tns_band`] under an explicit §4.5.1.1 frame-length family +/// (the `TNS_MAX_BANDS` operand comes from [`tns_max_bands_family`]). +pub fn clamp_tns_band_family( + band: u8, + max_sfb: u8, + family: crate::swb_offset::FrameFamily, + aot: u8, + window_sequence: WindowSequence, + fs_index: u8, +) -> Result { + let cap = tns_max_bands_family(family, aot, window_sequence, fs_index)?; + Ok(band.min(cap).min(max_sfb)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics_info::WindowSequence; + + // ===== Table 4.102 — TNS_MAX_ORDER ===== + + #[test] + fn order_short_window_is_7_for_every_aot() { + // Every row of Table 4.102 collapses to 7 in the short-window + // column. Cover the four AOTs the table calls out by name + // plus a representative ER AOT (17 = ER AAC LC). + for aot in [AOT_AAC_MAIN, AOT_AAC_LC, AOT_AAC_SSR, AOT_AAC_LTP, 17] { + for fs in 0..=12_u8 { + assert_eq!( + tns_max_order(aot, WindowSequence::EightShort, fs).unwrap(), + 7, + "AOT {aot} fs {fs} short windows", + ); + } + } + } + + #[test] + fn order_aac_main_long_window_is_20_for_all_rates() { + for fs in 0..=12_u8 { + for ws in [ + WindowSequence::OnlyLong, + WindowSequence::LongStart, + WindowSequence::LongStop, + ] { + assert_eq!(tns_max_order(AOT_AAC_MAIN, ws, fs).unwrap(), 20); + } + } + } + + #[test] + fn order_aac_lc_long_window_is_12_for_all_rates() { + for fs in 0..=12_u8 { + assert_eq!( + tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, fs).unwrap(), + 12, + ); + } + } + + #[test] + fn order_aac_ssr_long_window_is_12_for_all_rates() { + for fs in 0..=12_u8 { + assert_eq!( + tns_max_order(AOT_AAC_SSR, WindowSequence::OnlyLong, fs).unwrap(), + 12, + ); + } + } + + #[test] + fn order_other_aot_long_window_splits_at_32k_threshold() { + // "other AOT using TNS": > 32 kHz → 20, ≤ 32 kHz → 12. + // fs indices 0..=4 (96/88.2/64/48/44.1 kHz) take the high + // column; 5..=12 (32 / 24 / 22.05 / 16 / 12 / 11.025 / 8 / + // 7.35 kHz) take the low column. + for aot in [AOT_AAC_LTP, 17, 19, 20, 21, 22, 23] { + for fs in 0..=4_u8 { + assert_eq!( + tns_max_order(aot, WindowSequence::OnlyLong, fs).unwrap(), + 20, + "AOT {aot} fs {fs} long > 32 kHz", + ); + } + for fs in 5..=12_u8 { + assert_eq!( + tns_max_order(aot, WindowSequence::OnlyLong, fs).unwrap(), + 12, + "AOT {aot} fs {fs} long <= 32 kHz", + ); + } + } + } + + #[test] + fn order_rejects_out_of_range_fs_index() { + assert!(matches!( + tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, 13), + Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) + )); + assert!(matches!( + tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, 15), + Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) + )); + } + + // ===== Table 4.103 — TNS_MAX_BANDS ===== + + #[test] + fn bands_long_non_pqf_matches_table_row_by_row() { + // Each row of Table 4.103, column 1 ("without PQF filterbank, + // long windows"), per the Table 1.18 fs-index ordering. + let expected: [(u8, u8); 12] = [ + (0, 31), // 96000 + (1, 31), // 88200 + (2, 34), // 64000 + (3, 40), // 48000 + (4, 42), // 44100 + (5, 51), // 32000 + (6, 46), // 24000 + (7, 46), // 22050 + (8, 42), // 16000 + (9, 42), // 12000 + (10, 42), // 11025 + (11, 39), // 8000 + ]; + for (fs, expected_bands) in expected { + assert_eq!( + tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, fs).unwrap(), + expected_bands, + "fs {fs} long non-PQF", + ); + } + } + + #[test] + fn bands_short_non_pqf_matches_table_row_by_row() { + let expected: [(u8, u8); 12] = [ + (0, 9), + (1, 9), + (2, 10), + (3, 14), + (4, 14), + (5, 14), + (6, 14), + (7, 14), + (8, 14), + (9, 14), + (10, 14), + (11, 14), + ]; + for (fs, expected_bands) in expected { + assert_eq!( + tns_max_bands(AOT_AAC_LC, WindowSequence::EightShort, fs).unwrap(), + expected_bands, + "fs {fs} short non-PQF", + ); + } + } + + #[test] + fn bands_long_pqf_aac_ssr_matches_table_row_by_row() { + let expected: [(u8, u8); 12] = [ + (0, 28), + (1, 28), + (2, 27), + (3, 26), + (4, 26), + (5, 26), + (6, 29), + (7, 29), + (8, 23), + (9, 23), + (10, 23), + (11, 19), + ]; + for (fs, expected_bands) in expected { + assert_eq!( + tns_max_bands(AOT_AAC_SSR, WindowSequence::OnlyLong, fs).unwrap(), + expected_bands, + "fs {fs} long PQF", + ); + } + } + + #[test] + fn bands_short_pqf_aac_ssr_matches_table_row_by_row() { + let expected: [(u8, u8); 12] = [ + (0, 7), + (1, 7), + (2, 7), + (3, 6), + (4, 6), + (5, 6), + (6, 7), + (7, 7), + (8, 8), + (9, 8), + (10, 8), + (11, 7), + ]; + for (fs, expected_bands) in expected { + assert_eq!( + tns_max_bands(AOT_AAC_SSR, WindowSequence::EightShort, fs).unwrap(), + expected_bands, + "fs {fs} short PQF", + ); + } + } + + #[test] + fn bands_dispatches_long_start_and_stop_to_long_column() { + // §4.6.9.4 contrast is short vs long; LongStart and LongStop + // are long-window sequences (the analysis transform produces a + // 1024-line spectrum just like OnlyLong), so they must use the + // long-windows column. + for ws in [WindowSequence::LongStart, WindowSequence::LongStop] { + assert_eq!(tns_max_bands(AOT_AAC_LC, ws, 4).unwrap(), 42); + assert_eq!(tns_max_bands(AOT_AAC_SSR, ws, 4).unwrap(), 26); + } + } + + #[test] + fn bands_rejects_fs_12_and_above() { + // Table 4.103 does not list 7350 Hz (fs 12) as a row. + assert!(matches!( + tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, 12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + assert!(matches!( + tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, 13), + Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) + )); + assert!(matches!( + tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, 15), + Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) + )); + } + + #[test] + fn bands_aot_other_treats_as_non_pqf() { + // AOT 17 / 19 / 20 / 21 / 22 / 23 (ER variants) are *not* + // SSR, so they take the non-PQF columns identical to AOT 2. + for aot in [AOT_AAC_LTP, 17, 19, 20, 21, 22, 23] { + assert_eq!( + tns_max_bands(aot, WindowSequence::OnlyLong, 4).unwrap(), + 42, + "AOT {aot} long non-PQF", + ); + assert_eq!( + tns_max_bands(aot, WindowSequence::EightShort, 4).unwrap(), + 14, + "AOT {aot} short non-PQF", + ); + } + } + + // ===== Tables 4.119 / 4.120 — AAC LD ===== + + #[test] + fn ld_480_matches_table_4_119_row_by_row() { + assert_eq!(tns_max_bands_ld_480(3).unwrap(), 31); // 48000 + assert_eq!(tns_max_bands_ld_480(4).unwrap(), 32); // 44100 + assert_eq!(tns_max_bands_ld_480(5).unwrap(), 37); // 32000 + assert_eq!(tns_max_bands_ld_480(6).unwrap(), 30); // 24000 + assert_eq!(tns_max_bands_ld_480(7).unwrap(), 30); // 22050 + } + + #[test] + fn ld_512_matches_table_4_120_row_by_row() { + assert_eq!(tns_max_bands_ld_512(3).unwrap(), 31); // 48000 + assert_eq!(tns_max_bands_ld_512(4).unwrap(), 32); // 44100 + assert_eq!(tns_max_bands_ld_512(5).unwrap(), 37); // 32000 + // The 512-sample row for 24 kHz / 22.05 kHz is 31 (one + // higher than the 480 row); this is the row-by-row + // contrast that justifies the two tables existing. + assert_eq!(tns_max_bands_ld_512(6).unwrap(), 31); // 24000 + assert_eq!(tns_max_bands_ld_512(7).unwrap(), 31); // 22050 + } + + #[test] + fn ld_480_rejects_uncovered_rates() { + // Table 4.119 covers fs 3..=7 only. Every other slot is None. + for fs in [0_u8, 1, 2, 8, 9, 10, 11] { + assert!(matches!( + tns_max_bands_ld_480(fs), + Err(Error::IcsInfoUnsupportedSampleRateIndex(_)) + )); + } + } + + #[test] + fn ld_512_rejects_uncovered_rates() { + for fs in [0_u8, 1, 2, 8, 9, 10, 11] { + assert!(matches!( + tns_max_bands_ld_512(fs), + Err(Error::IcsInfoUnsupportedSampleRateIndex(_)) + )); + } + } + + #[test] + fn ld_accessors_reject_out_of_range_fs_index() { + assert!(matches!( + tns_max_bands_ld_480(12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + assert!(matches!( + tns_max_bands_ld_480(15), + Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) + )); + assert!(matches!( + tns_max_bands_ld_512(13), + Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) + )); + } + + // ===== Clamp helpers ===== + + #[test] + fn clamp_order_floors_to_cap() { + // AAC LC at 48 kHz long: cap is 12. A wire order of 20 + // (decoder MUST clamp per §4.6.9.3) becomes 12. + assert_eq!( + clamp_tns_order(20, AOT_AAC_LC, WindowSequence::OnlyLong, 3).unwrap(), + 12, + ); + // Wire order under the cap is returned unchanged. + assert_eq!( + clamp_tns_order(5, AOT_AAC_LC, WindowSequence::OnlyLong, 3).unwrap(), + 5, + ); + // Equal-to-cap order is preserved (not clamped to one less). + assert_eq!( + clamp_tns_order(12, AOT_AAC_LC, WindowSequence::OnlyLong, 3).unwrap(), + 12, + ); + // Short windows always cap at 7 regardless of AOT. + assert_eq!( + clamp_tns_order(31, AOT_AAC_MAIN, WindowSequence::EightShort, 3).unwrap(), + 7, + ); + } + + #[test] + fn clamp_order_propagates_fs_error() { + assert!(matches!( + clamp_tns_order(5, AOT_AAC_LC, WindowSequence::OnlyLong, 13), + Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) + )); + } + + #[test] + fn clamp_band_takes_three_way_min() { + // AAC LC at 44.1 kHz long: TNS_MAX_BANDS = 42. With + // max_sfb = 49 (per Table 4.129) and a wire band of 50, the + // three-way min is 42 (the TNS_MAX_BANDS cap wins). + assert_eq!( + clamp_tns_band(50, 49, AOT_AAC_LC, WindowSequence::OnlyLong, 4).unwrap(), + 42, + ); + // With max_sfb = 30 the second min collapses to 30 (the + // ics_info `max_sfb` cap wins). + assert_eq!( + clamp_tns_band(50, 30, AOT_AAC_LC, WindowSequence::OnlyLong, 4).unwrap(), + 30, + ); + // With a wire band under both caps, the band itself wins. + assert_eq!( + clamp_tns_band(10, 49, AOT_AAC_LC, WindowSequence::OnlyLong, 4).unwrap(), + 10, + ); + } + + #[test] + fn clamp_band_propagates_fs_error() { + assert!(matches!( + clamp_tns_band(5, 49, AOT_AAC_LC, WindowSequence::OnlyLong, 12), + Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) + )); + } + + // ===== Sanity: table lengths cover fs 0..=11 ===== + + #[test] + fn every_non_ld_table_has_12_entries() { + assert_eq!(TNS_MAX_BANDS_LONG_NON_PQF.len(), 12); + assert_eq!(TNS_MAX_BANDS_SHORT_NON_PQF.len(), 12); + assert_eq!(TNS_MAX_BANDS_LONG_PQF.len(), 12); + assert_eq!(TNS_MAX_BANDS_SHORT_PQF.len(), 12); + } + + #[test] + fn every_ld_table_has_12_entries() { + assert_eq!(TNS_MAX_BANDS_LD_480.len(), 12); + assert_eq!(TNS_MAX_BANDS_LD_512.len(), 12); + } +} diff --git a/crates/vendor/oxideav-ac3/Cargo.toml b/crates/vendor/oxideav-ac3/Cargo.toml new file mode 100644 index 00000000..1385406e --- /dev/null +++ b/crates/vendor/oxideav-ac3/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "oxideav-ac3" +publish = false +version = "0.0.10" +edition = "2021" +rust-version = "1.80" +license = "MIT" +repository = "https://github.com/OxideAV/oxideav-ac3" +authors = ["Mark Karpeles"] +description = "Pure-Rust AC-3 (Dolby Digital) audio decoder" + +readme = "README.md" +homepage = "https://github.com/OxideAV/oxideav-ac3" +keywords = ["multimedia", "audio", "ac3", "dolby", "codec"] +categories = ["multimedia::encoding", "multimedia::audio"] + +[dependencies] +oxideav-core = { path = "../oxideav-core" } + +# Vendored verbatim — see scripts/vendor-oxideav.sh. Upstream does not build +# under this repository's `-D warnings`, and making it would mean carrying a +# patch set across every refresh. +[lints.rust] +warnings = "allow" + +[lints.clippy] +all = "allow" diff --git a/crates/vendor/oxideav-ac3/LICENSE b/crates/vendor/oxideav-ac3/LICENSE new file mode 100644 index 00000000..ffe2468a --- /dev/null +++ b/crates/vendor/oxideav-ac3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Karpelès Lab Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/vendor/oxideav-ac3/README.md b/crates/vendor/oxideav-ac3/README.md new file mode 100644 index 00000000..fcf5990a --- /dev/null +++ b/crates/vendor/oxideav-ac3/README.md @@ -0,0 +1,371 @@ +# oxideav-ac3 + +[![CI](https://github.com/OxideAV/oxideav-ac3/actions/workflows/ci.yml/badge.svg)](https://github.com/OxideAV/oxideav-ac3/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/oxideav-ac3.svg)](https://crates.io/crates/oxideav-ac3) [![docs.rs](https://docs.rs/oxideav-ac3/badge.svg)](https://docs.rs/oxideav-ac3) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +Pure-Rust **AC-3 (Dolby Digital)** + **E-AC-3 (Enhanced AC-3 / Dolby +Digital Plus)** audio decoder + encoder — elementary streams per +ATSC A/52:2018 (= ETSI TS 102 366). Zero C dependencies. + +Part of the [oxideav](https://github.com/OxideAV/oxideav-workspace) +framework but usable standalone. + +## Architecture + +The pipeline follows the spec's natural ordering; each module owns one +slice of §5..§7 (base AC-3) or §E (E-AC-3): + +1. [`syncinfo`] — sync word 0x0B77, crc1, fscod, frmsizecod, + frame-length lookup (§5.3.1 / §5.4.1 / Table 5.18). +2. [`bsi`] — Bit Stream Information: bsid, bsmod, acmod → channel layout + + lfeon + dialnorm + the optional timecode / Annex D alternate-syntax + metadata blocks (§5.4.2). +3. [`audblk`] — per-block exponent decode (§7.1), parametric bit + allocation (§7.2 with §7.2.2.6 delta-bit-allocation), mantissa decode + (§7.3), channel coupling (§7.4), rematrixing (§7.5), dynamic-range + compression (§7.7). +4. [`imdct`] + [`mdct`] — §7.9.4 FFT-backed 512-point IMDCT and + 256-point short-block pair, plus the forward transforms the encoder + uses. +5. [`downmix`] — §7.8 LoRo + §7.8.2 LtRt downmix matrices for every + source acmod, with Annex D / E-AC-3 mix-level extension routing. +6. [`wave_order`] — channel reorder for front-centre-bearing layouts + (`acmod ∈ {3, 5, 7}`). +7. [`encoder`] — base AC-3 encoder. +8. [`eac3`] — Annex E decoder + encoder. +9. [`crc`] — §7.10.1 CRC-16 over poly 0x8005, shared between the encoder + and the opt-in `decoder::verify_packet_crc` residue check. +10. [`drc`] — §6.1.9 / §7.6 / §7.7 dynamic-range-control + dialogue- + normalisation control surface (`DrcSettings`: partial-compression + cut/boost, heavy-compression "RF mode", dialnorm playback target). + +## Capabilities + +### AC-3 decoder + +- Sync frame + BSI parse (§5.3 / §5.4). All §5.4.2 metadata words — + bit-stream mode, compression gain, dialogue normalisation, mix + levels, Dolby Surround mode, timecodes, copyright/original flags, + language code, audio-production info, the Annex D alternate-syntax + informational blocks, and the `addbsi` trailer — are parsed and + surfaced as typed accessors (advisory metadata; the PCM path is + unchanged). +- Audio-block parse (§5.4.3), exponent decode (§7.1) + parametric bit + allocation (§7.2), mantissa decode (§7.3) with bap=0 dither (§7.3.4), + delta bit allocation (§7.2.2.6). +- IMDCT synthesis (§7.9) — both 512-point long-block and 256-point + short-block paths. +- Channel coupling (§7.4) + rematrix (§7.5) + dynrng (§7.7). +- **Dynamic-range-control + dialogue-normalisation control surface** + (§6.1.9 / §7.6 / §7.7, [`drc`]). The mandatory §7.7.1 full-`dynrng` + decode is the default ("line out"); a listener-facing + [`DrcSettings`] steers the §7.7.1.2 *partial-compression* cut/boost + factors (apply a fraction of each gain reduction / increase, with + independent directions), §7.7.2 *heavy compression* ("RF mode" — + substitutes the BSI `compr` word, ±48 dB, falling back to `dynrng` + when a frame carries no `compr` per §7.7.2.1), and §7.6 dialogue + normalisation (an opt-in playback scalar `10^((target − dialnorm)/20)` + toward a chosen headroom target). Build a configured decoder with + `decoder::make_decoder_with_drc(params, DrcSettings)`. Applies to both + the AC-3 and E-AC-3 paths. +- Downmix (§7.8) — LoRo and LtRt 2-channel. +- Bitstream → WAV channel reorder for multichannel layouts. + +### AC-3 encoder + +- Multichannel encode — 1/0, 2/0, 2/0+LFE (2.1), 3/0, 2/2, 3/2, 3/2.1 + (5.1) and other acmod layouts, with per-channel D15/D25/D45 exponent + strategy selection (§7.1.3), 5-fbw channel coupling within the + §5.4.3.12 narrow-coupling validity envelope, a §8.2.2 transient + detector (4th-order Butterworth 8 kHz split for short-block + switching), per-channel `fsnroffst[ch]` tuning (§5.4.3.40), per-block + SNR-offset bit-pool redistribution, and §7.10.1 dual-CRC emission. +- **Bitstream-metadata surface** (`encoder::MetadataParams` / + `make_encoder_with_metadata`, or the registry options `dialnorm`, + `compr`, `dynrng`, `bsmod`, `cmixlev`, `surmixlev`, `dsurmod`, + `langcod`, `mixlevel`+`roomtyp`, `copyright`, `origbs`): every + §5.4.2 BSI advisory word plus the §5.4.3.3-4 per-block `dynrng` + dynamic-range word. Round-tripped through the typed `bsi::parse` + surface and black-box validated: the external decoder binary + reproduces the authored `dynrng` / `compr` gains exactly + (Δ0.000 dB at −12 dB words) and a 10 dB `dialnorm` delta measures + −10.03 dB under target-level normalisation. + +### E-AC-3 (Annex E) + +- Decoder — BSI, audfrm (Tables E1.2 / E1.3), audblk DSP, the §3.4 + Adaptive Hybrid Transform on fbw / LFE / coupling channels, §3.6 + spectral extension with the §3.6.4.2.3 SPXATTEN border notch, and + §3.7.2 transient pre-noise processing. All three §2.3.2.3 SNR-offset + strategies decode: the frame-level `snroffststr == 0` pair plus the + §2.3.3.27 per-block modes `0x1` (one shared `blkfsnroffst`) and `0x2` + (independent per-coupling/channel/LFE fine offsets), each gated by the + per-block `snroffste` reuse flag. Enhanced coupling + (`ecplinu == 1`, §E.2.3.3.16-26 / §E.3.5.5) decodes end-to-end: the + audblk parser reads the strategy + per-channel amplitude/angle/chaos + coordinates, decodes the enhanced-coupling channel through the shared + exponent / bit-allocation / mantissa path, and a deferred second pass + reconstructs the non-aliased complex carrier `Z[k]` from the + previous / current / next blocks (§E.3.5.5.1), processes the per-bin + amplitudes + de-correlated angles, and emits each coupled channel's + transform coefficients via the §E.3.5.5.4 complex product — replacing + the standard §7.4 decouple. Block 0's "previous block" carrier source + is threaded across the frame boundary from the prior frame's last + enhanced-coupling block (carried on `EcplState`, §E.3.5.5.1), so the + prior-frame edge no longer collapses to a zero carrier; the frame's + last block's "next block" still uses a zero carrier (it lives in a + not-yet-decoded frame — streaming lookahead is out of scope). Three + enhanced-coupling conformance defects fixed in r406, pinned by the + new encoder round-trips: `chincpl[ch]` is read directly after + `ecplinu` (BEFORE the standard/enhanced strategy split — the prior + order desynced every multichannel ecpl frame, invisibly in 2/0 where + the flags are implicit); `ecplparam1e/2e == 0` now REUSE the + previously transmitted amplitudes / angle+chaos values per + §2.3.3.21-22 (previously each block's coordinate set was replaced + wholesale, silencing every band of a reusing channel); and the + resolved banding structure masks its entries up to and including + `max(ecpl_begin_subbnd, 8)` per §E.2.3.3.19 — the Table E2.14 + default carries a merge bit at sub-band 9, so a default-banded + region beginning there previously made `necplbnd` disagree with the + §E.3.5.5.1 band walk by one band (a coordinate-count desync). The + §E.3.3.2 `nrematbd` derivation now folds in enhanced coupling: a 2/0 + `ecplinu` block sizes its rematrix-flag field from the raw `ecplbegf` + code (0/1/2/<5 → 0/1/2/3 bands, else 4) rather than `cplbegf`, keeping + the bit cursor aligned on enhanced-coupling 2/0 frames. Standard coupling + now applies the §E.2.3.3.15 **default coupling banding structure** + (`defcplbndstrc[]`, Table E2.12, indexed by absolute sub-band) when + `cplbndstrce == 0` in a frame's first coupling block, instead of leaving + every sub-band un-merged — the prior all-zeros behaviour collapsed a + 7-subband region to 7 bands instead of 3, corrupting the §7.4 + coupling-coordinate scatter on every basic stereo-coupled frame. Three + corpus stereo fixtures (`eac3-stereo-48000-192kbps`, `eac3-256-coeff-block`, + `eac3-from-ac3-bitstream-recombination`) jumped from ~8-14 dB to ~91 dB + PSNR and are now CI-gated at an 80 dB floor (`Tier::MinPsnr` in + `tests/docs_corpus.rs`). Dependent-substream channel combination follows + the §E.3.8.2 replace-or-extend rule: each dep coded channel is routed by + its Table E2.5 location (or natural `acmod` order when `chanmape == 0`), + *replacing* the matching independent-substream channel in place when the + location is shared (e.g. a dep substream re-coding Center / LFE, or L/R + via a custom `chanmap`) and *extending* the output only for genuinely new + locations — so a real greater-than-5.1 broadcast program reassembles + spatially correctly rather than duplicating and decorrelating the shared + channels a blind append would have appended. +- Encoder — independent + dependent substream pairs for 1.0 / 2.0 / 5.1 + / 7.1 layouts, with adaptive / frame-based exponent strategies. + **Bitstream-metadata surface** (`eac3::Eac3Metadata` / + `eac3::make_encoder_with_metadata` + registry options): fixed-BSI + `dialnorm` and `compr`, the per-block `dynrng` word (every block of + every substream), the Table E1.2 **mixing-metadata block** + (`dmixmod`, LtRt/LoRo centre + surround mix levels, `lfemixlevcod`, + `pgmscl`, `extpgmscl`) and the §E.2.3.1.62+ **informational block** + (`bsmod`, copyright/original, 2/0 `dsurmod`+`dheadphonmod`, + ≥6-channel `dsurexmod`, audio-production info, `sourcefscod`) — + emitted on the independent substream (a 7.1 pair's dependent + substream keeps the blocks absent while sharing dialnorm/compr). + Round-tripped through the typed Annex E `bsi::parse` surface (which + now also surfaces `bsmod`); black-box validated: the external + decoder binary accepts the block-bearing syntax (decode level + within 0.004 dB of a block-less encode) and reproduces the authored + `dynrng` gain exactly. Metadata-bearing AC-3 and E-AC-3 streams are + swept through the corruption families in `tests/robustness.rs`. + **Spectral extension is now available on the encoder side** + (`eac3::make_encoder_with_spx(params, SpxParams)`, §E.2.3.3 / §E.3.6): + every fbw channel is coded only up to the SPX begin frequency + (default tc# 109 ≈ 10.2 kHz at 48 kHz) and the decoder regenerates + the extension region (default up to tc# 229 ≈ 21.5 kHz) from a + translated copy of the channel's own low band, noise-blended and + scaled by per-band coordinates the encoder derives from the + §3.6.4.3 energy-matching rule (`spxco = rms(original HF band) / + (rms(translated band)·32)`, quantised through the §E.2.3.3.11-13 + exponent/mantissa/master-coordinate forms). Coordinates refresh on + the exponent anchor blocks and are reused between (`spxcoe`); + geometry (begin/end/copy-start codes, noise blend, default + Table E2.11 vs explicit band structure) is configurable via + `SpxParams`; the freed high-frequency bits are re-spent on the coded + low band by the SNR-offset tuner. Optional extras: §3.6.4.2.3 + **attenuation** (`spxattene`/`chinspxatten`/`spxattencod` in audfrm, + with the border/wrap notch folded into the encoder's coordinate + computation so band energies still match) and **adaptive copy-start** + (per-frame `spxstrtf` re-selection that scores every candidate by + coordinate saturation — a spectrum with a hole above the first copy + sub-band would otherwise pin a band's coordinate at the 0.875 + ceiling). Validated three ways: in-tree round-trips gate per-SPX-band + decoded energy within ±3 dB of the original (mono / stereo / 5.1 / + narrow non-default geometry), default-vs-explicit band structure + decodes bit-identically, and an external decoder binary + cross-validates the emitted syntax + banded energies (±4 dB) for the + plain, mono, and attenuated variants. SPX-encoded streams are also + swept through the truncation / bit-flip / garbage corruption + families. Both entry points build the same encoder: the typed + `make_encoder_with_spx` and the registry path via + `CodecParameters::options` `spx*` keys (`spx`, `spx_begf`/`endf`/ + `strtf`/`blnd`, `spx_atten`, `spx_adaptive_copy_start`, + `spx_explicit_band_structure`) — pinned byte-identical. Stationary + coordinate refreshes are thrifted (`spxcoe = 0`) with a mid-frame + level-step gate proving per-span refresh still tracks moving spectra. + **Mixed per-channel membership** (§E.2.3.3.3) is supported via + `SpxParams::channel_mask` / the `spx_chmask` option: excluded + channels emit `chinspx[ch] = 0`, keep their `chbwcod`, and are + waveform-coded to full bandwidth while member channels stop at the + SPX begin frequency — the SNR tuner budgets every channel at its own + coded bandwidth (per-channel `end_mant` plumbed through + `tune_snroffst_with_plan_ends` / `overhead_bits_for_ends` / + `mantissa_bits_total_ends`). The mixed split is validated in-tree + (SPX band-energy contract on the member channel AND full-bandwidth + HF fidelity on the excluded one) and through the external decoder. + + **The Adaptive Hybrid Transform is now on the encoder side too** + (`eac3::make_encoder_with_aht(params)` / the `aht` option, §3.4): + every fbw channel — and the LFE (`lfeahtinu`) — moves to a single + block-0 exponent anchor (`nchregs[ch] == nlferegs == 1`, per-bin + 6-block-max exponents), long transforms are forced, and block 0 + front-loads the §3.4.4 mantissa stream: `chgaqmod` + gain words + + per-bin codewords against the §3.4.3.1 `hebap[]` (the shared + psd/excitation/mask pipeline with the Table E3.1 pointer table). + Quantiser stack per Tables E3.2/E3.5/E3.6: minimum-Euclidean- + distance VQ over Tables E4.1..E4.7 for `hebap` 1..7, and + gain-adaptive quantisation for `hebap` >= 8 — the per-channel + `gaqmod` (all four Table E3.3 modes, incl. the 5-bit composite gain + triplets) is chosen by exact bit accounting, with per-bin Gk in + {1, 2, 4} splitting short small-mantissa codewords from + tag + dead-zone large escapes. The SNR-offset tuner costs AHT + channels by their exact front-loaded payload and binary-searches + the monotone `csnroffst·16 + fsnroffst` axis. Measured on a + stationary stereo two-tone (in-tree decode): 42.0 dB @ 96 kbps + rising to 75.6 dB @ 448 kbps versus a flat ~23.9 dB for the + standard path (+18 to +52 dB); on a multitone+noise bed fixture + +7 to +22 dB. Black-box: mono / stereo / 5.1 AHT streams decode + through an external decoder binary at 28.1 / 28.1 / 33.4 dB + (vs 22.3 dB non-AHT baseline through the same harness). + `examples/eac3_rate_curves.rs` prints the full + standard/AHT/SPX/enhanced-coupling rate ladder; + `aht_quality_scales_with_rate` gates the curve shape in CI. + + **Enhanced coupling is now on the encoder side too** + (`eac3::make_encoder_with_ecpl(params, EcplParams)` / the `ecpl`, + `ecpl_begf`, `ecpl_endf` options; §E.2.3.3.16-26 / §E.3.5.5) — the + last Annex E encoder tool. Every fbw channel of the independent + substream is coupled: below the begin frequency (default tc# 37 ≈ + 3.5 kHz) channels are waveform-coded as usual; above it a single + shared **carrier** channel is coded through the standard coupling- + channel exponent / bit-allocation / mantissa path (cplexpstr anchors + on blocks 0/3, `cplabsexp` + D15 groups, implicit first-block + `cplleake` with zero leak inits, mantissas interleaved after the + first coupled channel) and each coupled channel is rebuilt from it + via per-band Table E3.10 amplitude + Table E3.11 angle coordinates + (chaos 0, `ecpltrans` 0 — deterministic decode). The carrier is the + first coupled channel's MDCT scaled per band ~3 dB above the loudest + coupled channel — phase-locked to channel 0, whose angle is + spec-fixed to 0 and never transmitted, with the margin letting the + 1.0 amplitude ceiling absorb band-level carrier coding loss. + Coordinates are measured in the §E.3.5.5.1 complex analysis domain + against the carrier the decoder will actually reconstruct (each bin + through the final exponent + bap quantiser; the previous frame's + carried quantised last block at the frame head, zero after the + tail), refreshed on blocks 0/3 with §2.3.3.21-22 reuse thrift when + the block-3 refresh quantises identically. **Chaos coordinates** + (Table E3.12, on by default via `EcplParams::chaos` / the + `ecpl_chaos` option) are derived from the measured per-band + coherence — the incoherent fraction maps onto the 8-step grid, + engaging the decoder's §E.3.5.5.3 per-bin random de-correlation for + content the shared carrier cannot represent, with the transmitted + amplitude pre-divided by the decoder's `1 + 0.38·chaosval` + modification (chaos backs off when the pre-compensated amplitude + would exceed the 1.0 ceiling — band energy wins over width). A + partial-coherence stereo fixture gates the effect: decoded + in-region inter-channel coherence 0.914 chaos-less → 0.796 with + chaos (source 0.706), band energies still matched. Validated in-tree: + stereo band-energy round-trip (±3 dB per signal band, ±1.5 dB coded + low band, 20 dB waveform floor), a stereo **quadrature** fixture + (channel 1's tones 90° off the carrier — pins the angle path at an + 18 dB waveform floor; a broken angle path collapses to ~3 dB), + 5.1 with explicit `chincpl` bits, a 7.1 indep(coupled)+dep(plain) + pair walk, registry-vs-typed byte-identical construction, and the + corruption families in `tests/robustness.rs`. Measured interior + PSNR 25.4-30.4 dB across channels. Black-box cross-validation is + **not possible for this tool**: the external validator binary + reports enhanced coupling as not implemented and mutes (probed + r406) — our decoder is ahead of the validator here, so validation + is round-trip + spec-text only. **SPX and enhanced coupling can be + co-active** (`make_encoder_with_spx_ecpl` / options `spx=1` + + `ecpl=1`) per §3.6.1: channels are waveform-coded below the + coupling begin, carried by the shared carrier + coordinates from + there to the SPX begin frequency (the coupling region is + SPX-bounded — `ecplendf` is not transmitted, §E.2.3.3.17), and + SPX-synthesized above it; a stereo three-region round-trip gates + all three regions' energies (coupling bands ±3 dB, SPX bands + ±3.5 dB, coded low band ±1.5 dB). AHT remains mutually exclusive + with both. + + Three spec-fidelity notes from this work: (1) GAQ dequantisation now + uses the literal Table E3.5/E3.6 characteristics — the `Gk = 2` + large mantissa is an `(m-1)`-bit codeword (2^(m-1) output points), + and all scalar quantisers apply the exact Q15 `y = x + ax + b` + remap. (2) The §3.4.5 IDCT's printed leading constant `2` measures + as `√2` against an independent production decoder (a pure-DC and a + 40 Hz-modulated fixture both fit `external = ours(2·Σ)/√2` with + ~89 dB residual); with `√2` the DC basis weight is exactly 1, and + both transforms follow the deployed constant. (3) §E.3.5.5.1's + step-3 overlap-add omits the §7.9.4.1 step-6 headroom-restoring + factor of 2 (step 2 references only "steps 1 to 5") — taken + literally the analysis→synthesis chain returns exactly half the + original coefficients, so every enhanced-coupling channel would + decode 6 dB low and the loudest coupled channel would need the + unrepresentable amplitude 2.0; the Table E3.10 ceiling of exactly + 1.0 pins the intended identity at unity, and the factor of 2 is + applied in the carrier reconstruction. All three notes are codified + as clean-room errata entries (`docs/audio/ac3/ac3-errata.md` E2 / + E1 / E3 respectively; E3 also records that the ETSI TS 102 366 + V1.4.1 copy omits the enhanced-coupling channel-processing clause + entirely, so A/52:2018 is the operative text). The E3 correction is + regression-pinned in both directions: a decode-side least-squares + identity fit (corrected chain gain 1.0 vs exactly 0.5 as printed) + and a full encode→decode bitstream round-trip gating the aggregate + coupling-region energy within ±1.5 dB of unity (the as-printed + reading sits at −6.02 dB). A real Dolby-encoded `ecplinu = 1` + stream remains a recorded fixture GAP + (`docs/audio/ac3/fixtures/eac3-ecpl-enhanced-coupling/GAP.md`), so + ecpl validation stays in-tree round-trip + spec-text. + +### CRC + +§7.10.1 CRC-16 (poly 0x8005), shared between the encoder (forward +generation, augmented form for crc2) and the opt-in decoder residue +check. + +## Conformance corpus + +`tests/docs_corpus.rs` decodes the AC-3 / E-AC-3 fixture set under +`docs/audio/ac3/fixtures/` (each a raw elementary stream paired with a +reference PCM decode) and scores per-channel PSNR. The decode is +floating-point in the IMDCT, so it is not bit-exact against the +reference, but it is **deterministic** (identical PSNR run-to-run). + +Fixtures whose decode is known-good are gated at a `Tier::MinPsnr` +floor so a regression fails CI; the rest log deltas without gating: + +| Tier | AC-3 | E-AC-3 | +| --- | --- | --- | +| `MinPsnr` (CI-gated) | 11 fixtures — mono / stereo / 2/1 / 3/0 / 3/2 (±LFE) at 48 / 44.1 / 32 kHz, 32-448 kbps, ~86-92 dB (80 dB floor, 96 kbps mono at 78), plus the torture-grade `ac3-low-bitrate-32kbps-mono` (~62 dB — the 32 kbps lossy / onset-overlap floor — at a loose 50 dB gross-regression floor) | 6 fixtures — stereo + 5.1 + 256-coeff at ~91 dB (80 dB floor), plus the low-rate `eac3-low-bitrate-32kbps` (~66 dB, 60 dB floor) and `eac3-low-rate-stereo-64kbps` (~72 dB, 65 dB floor) as gross-regression guards | + +Every corpus fixture is now CI-gated; none remain `ReportOnly`. The +decoder is additionally fuzzed for panic-safety against +truncation / bit-flip / sync-prefixed-garbage corruption of every +fixture (`tests/robustness.rs`). + +## Installation + +```toml +[dependencies] +oxideav-core = "0.1" +oxideav-codec = "0.1" +oxideav-ac3 = "0.0" +``` + +## Codec ID + +- Codecs: `"ac3"` (decoder + encoder) and `"eac3"` (decoder + encoder); + output sample format `S16` interleaved. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/crates/vendor/oxideav-ac3/VENDOR.toml b/crates/vendor/oxideav-ac3/VENDOR.toml new file mode 100644 index 00000000..a3d6bb19 --- /dev/null +++ b/crates/vendor/oxideav-ac3/VENDOR.toml @@ -0,0 +1,9 @@ +# Written by scripts/vendor-oxideav.sh. Do not edit, and do not +# hand-edit the vendored sources beside it — change them upstream +# and re-run the script. +source = "https://github.com/OxideAV/oxideav-ac3" +commit = "8acf106d50d58f359946c086d1b393a060eaf5f6" +describe = "v0.0.10-23-g8acf106" +version = "0.0.10" +vendored_at = "2026-08-24T08:52:59Z" +patches = [] diff --git a/crates/vendor/oxideav-ac3/src/audblk.rs b/crates/vendor/oxideav-ac3/src/audblk.rs new file mode 100644 index 00000000..a881ef40 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/audblk.rs @@ -0,0 +1,2934 @@ +//! AC-3 audio-block parser + DSP pipeline (§5.4.3, §7). +//! +//! This module walks `audblk()` bit-by-bit, running the full decoder +//! data-flow per Figure 6.1: unpack side-info → decode exponents → bit +//! allocation → unpack mantissas → decouple → rematrix → IMDCT → +//! window+overlap-add. Decoder state that must persist across audio +//! blocks inside a syncframe (and between syncframes for the +//! overlap-add delay line) lives on the `Ac3State` struct handed in by +//! the top-level decoder. +//! +//! The code is intentionally "big function per DSP stage" so each stage +//! matches a section of the spec 1:1. +//! +//! ## Scope +//! +//! - Full-bandwidth channels (fbw) + LFE are decoded. +//! - Coupling is supported for 2-channel streams (Table 7.24 coupling +//! sub-bands, 7.4.3 coupling-coordinate reconstruction). +//! - Rematrixing (§7.5) is applied in 2/0 mode. +//! - Dynamic range compression (§7.7 dynrng) scales the transform +//! coefficients. +//! - 512-point IMDCT with KBD window + 50% overlap-add (§7.9.4.1, +//! §7.9.5). The 256-point short-block pair (§7.9.4.2) is wired +//! through `crate::imdct::imdct_256_pair_fft`; block-switching +//! correctness is gated by the `transient_bursts_stereo.ac3` PSNR +//! test (the sine fixture never exercises `blksw=1`). + +use oxideav_core::bits::BitReader; +use oxideav_core::{Error, Result}; + +use crate::bsi::Bsi; +use crate::syncinfo::SyncInfo; +use crate::tables::{ + BAPTAB, BNDSZ, BNDTAB, DBPBTAB, FASTDEC, FASTGAIN, FLOORTAB, HTH, LATAB, MANT_LEVEL_11, + MANT_LEVEL_15, MANT_LEVEL_3, MANT_LEVEL_5, MANT_LEVEL_7, MASKTAB, QUANTIZATION_BITS, SLOWDEC, + SLOWGAIN, WINDOW, +}; + +/// Maximum fbw channels (3/2 mode). +pub const MAX_FBW: usize = 5; +/// Total channel slots: fbw (5) + coupling pseudo-channel (1) + lfe (1) = 7. +pub const MAX_CHANNELS: usize = 7; +/// Number of transform coefficients per block. +pub const N_COEFFS: usize = 256; +/// Audio blocks per syncframe. +pub const BLOCKS_PER_FRAME: usize = 6; +/// New samples per block per channel after overlap-add. +pub const SAMPLES_PER_BLOCK: usize = 256; + +/// Snapshot of every side-info field decoded out of an `audblk()` +/// element per §5.4.3. This is purely the "parse" half of the pipeline +/// — no exponents, no mantissas, no DSP state. It gives tests and the +/// downstream §7 stages a single inspectable record of what the +/// bit-stream actually said, keyed to the spec clause numbers. +/// +/// Every field here cites its §5.4.3.x subsection in the doc comment +/// so an auditor can verify the parser against the spec table by table. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AudBlkSideInfo { + // ---- §5.4.3.1 blksw[ch] / §5.4.3.2 dithflag[ch] ---- + /// `blksw[ch]` — per-channel block-switch flag (§5.4.3.1, 1 bit). + pub blksw: [bool; MAX_FBW], + /// `dithflag[ch]` — per-channel dither flag (§5.4.3.2, 1 bit). + pub dithflag: [bool; MAX_FBW], + // ---- §5.4.3.3-6 dynamic range control ---- + /// `dynrnge` — dynamic-range word present (§5.4.3.3, 1 bit). + pub dynrnge: bool, + /// `dynrng` — 8-bit dynamic-range gain word (§5.4.3.4). Only + /// meaningful when `dynrnge == true`. + pub dynrng: u8, + /// `dynrng2e` — dual-mono ch2 dynamic-range present (§5.4.3.5, + /// 1 bit). Only present when `acmod == 0`. + pub dynrng2e: bool, + /// `dynrng2` — dual-mono ch2 dynamic-range word (§5.4.3.6, 8 bits). + pub dynrng2: u8, + // ---- §5.4.3.7-18 coupling strategy + coordinates ---- + /// `cplstre` — coupling strategy present in this block (§5.4.3.7). + pub cplstre: bool, + /// `cplinu` — coupling in use (§5.4.3.8). Valid only when `cplstre`. + pub cplinu: bool, + /// `chincpl[ch]` — channel is part of the coupling group + /// (§5.4.3.9). Valid only when `cplinu`. + pub chincpl: [bool; MAX_FBW], + /// `phsflginu` — coupling phase flags in use for 2/0 (§5.4.3.10). + pub phsflginu: bool, + /// `cplbegf` — coupling begin frequency code (§5.4.3.11, 4 bits). + pub cplbegf: u8, + /// `cplendf` — coupling end frequency code (§5.4.3.12, 4 bits). + pub cplendf: u8, + /// `cplbndstrc[sbnd]` — coupling band structure (§5.4.3.13). + pub cplbndstrc: [bool; 18], + /// `cplcoe[ch]` — coupling-coordinates-present flag per channel + /// (§5.4.3.14). + pub cplcoe: [bool; MAX_FBW], + // ---- §5.4.3.19-20 rematrix ---- + /// `rematstr` — rematrix strategy present (§5.4.3.19). + pub rematstr: bool, + /// Number of rematrix bands actually carried in `rematflg` per + /// §5.4.3.19 rules and Table — 2/3/4 depending on `cplbegf`. + pub rematflg_count: u8, + /// `rematflg[rbnd]` — per-band rematrix flag (§5.4.3.20). + pub rematflg: [bool; 4], + // ---- §5.4.3.21-24 exponent strategy ---- + /// `cplexpstr` — coupling exponent strategy (§5.4.3.21, 2 bits). + /// 0=reuse, 1=D15, 2=D25, 3=D45. + pub cplexpstr: u8, + /// `chexpstr[ch]` — full-bandwidth exponent strategy per channel + /// (§5.4.3.22, 2 bits). + pub chexpstr: [u8; MAX_FBW], + /// `lfeexpstr` — LFE exponent strategy (§5.4.3.23, 1 bit). + pub lfeexpstr: u8, + /// `chbwcod[ch]` — channel bandwidth code (§5.4.3.24, 6 bits). + /// Only meaningful when `chexpstr[ch] != 0 && !chincpl[ch]`. + pub chbwcod: [u8; MAX_FBW], + // ---- §5.4.3.30-46 bit-allocation parametric side-info ---- + /// `baie` — bit-allocation-info exists (§5.4.3.30). + pub baie: bool, + /// `snroffste` — SNR-offset block-level flag (§5.4.3.36). + pub snroffste: bool, + /// `cplleake` — coupling-leak-init flag (§5.4.3.44). Only when + /// `cplinu`. + pub cplleake: bool, + // ---- §5.4.3.47 delta bit allocation ---- + /// `deltbaie` — delta-bit-allocation info exists (§5.4.3.47). + pub deltbaie: bool, + // ---- §5.4.3.58-59 skip field ---- + /// `skiple` — skip-field-exists flag (§5.4.3.58). + pub skiple: bool, + /// `skipl` — number of skip *bytes* (§5.4.3.59, 9 bits). + pub skipl: u16, +} + +/// Per-channel persistent state: exponents, bit-allocation pointers, +/// bandwidth, gain-range, and the 256-sample overlap-add delay line. +#[derive(Clone)] +pub struct ChannelState { + pub exp: [u8; N_COEFFS], + pub bap: [u8; N_COEFFS], + pub psd: [i16; N_COEFFS], + pub bndpsd: [i16; 50], + pub mask: [i16; 50], + pub deltba: [i16; 50], + pub end_mant: usize, + /// 256-sample tail from last block's MDCT, ready to be added into + /// this block's output. + pub delay: [f32; SAMPLES_PER_BLOCK], + /// Raw dequantized transform coefficients for this block. + pub coeffs: [f32; N_COEFFS], + pub blksw: bool, + pub dithflag: bool, + /// Whether this channel is coupled (set from chincpl[]). + pub in_coupling: bool, + /// Dynrng gain multiplier (linear). + pub dynrng: f32, + /// E-AC-3 spectral extension (§E.3.6): whether this channel + /// regenerates high-frequency transform coefficients via SPX this + /// block. Base AC-3 never sets this (no SPX in the base layer), so + /// the SPX synthesis step in [`dsp_block`] is a no-op for AC-3. + pub in_spx: bool, + /// Per-band SPX coordinate `spxco[ch][bnd]` (§E.3.6.3). Persisted + /// across blocks so a `spxcoe[ch] == 0` block can reuse the prior + /// coordinates. + pub spx_coord: [f32; 18], + /// Per-band SPX noise / signal blend factors `nblendfact` / + /// `sblendfact` (§E.3.6.4.2.1). Recomputed when new coordinates + /// (and hence a new `spxblnd`) arrive; reused otherwise. + pub spx_nblend: [f32; 18], + pub spx_sblend: [f32; 18], + /// E-AC-3 spectral-extension attenuation (§3.6.4.2.3, §2.3.2.24-25). + /// `spx_atten_active` mirrors the frame-level `chinspxatten[ch]` bit + /// (a frame-scoped flag — the spec carries it in audfrm, not audblk, + /// so it stays constant across the 6 blocks of a syncframe). When + /// set, the SPX synthesis applies a 5-tap notch filter at the + /// baseband/extension border (and at every wrap point during the + /// translation copy) using row `spx_atten_code` of Table E3.14. + pub spx_atten_active: bool, + /// `spxattencod[ch]` — 5-bit index into Table E3.14 + /// (`SPX_ATTEN_TABLE`). Only meaningful when `spx_atten_active`. + pub spx_atten_code: u8, +} + +impl Default for ChannelState { + fn default() -> Self { + Self::new() + } +} + +impl ChannelState { + pub fn new() -> Self { + Self { + exp: [24; N_COEFFS], + bap: [0; N_COEFFS], + psd: [0; N_COEFFS], + bndpsd: [0; 50], + mask: [0; 50], + deltba: [0; 50], + end_mant: 0, + delay: [0.0; SAMPLES_PER_BLOCK], + coeffs: [0.0; N_COEFFS], + blksw: false, + dithflag: false, + in_coupling: false, + dynrng: 1.0, + in_spx: false, + spx_coord: [0.0; 18], + spx_nblend: [0.0; 18], + spx_sblend: [0.0; 18], + spx_atten_active: false, + spx_atten_code: 0, + } + } +} + +/// Per-frame decoder state that survives across audio blocks and across +/// syncframes (delay lines). +#[derive(Clone)] +pub struct Ac3State { + /// [0..nfchans] = fbw channels, index MAX_FBW = coupling pseudo-channel, + /// index MAX_FBW+1 = LFE. + pub channels: [ChannelState; MAX_CHANNELS], + + // ---- Coupling state (§5.4.3.7 ff, §7.4) ---- + pub cpl_in_use: bool, + pub phsflginu: bool, + pub cpl_begf: u8, + pub cpl_endf: u8, + pub cpl_begf_mant: usize, // 37 + 12*cplbegf + pub cpl_endf_mant: usize, // 37 + 12*(cplendf+3) + pub cpl_nsubbnd: usize, + pub cpl_nbnd: usize, + /// cplbndstrc[sbnd], 1 when subband merges into previous band. + pub cpl_bndstrc: [bool; 18], + /// cplco[ch][bnd] linear coupling coordinate. + pub cpl_coord: [[f32; 18]; MAX_FBW], + pub cpl_coord_valid: [bool; MAX_FBW], + pub cpl_phsflg: [bool; 18], + + // ---- Rematrix ---- + pub rematflg: [bool; 4], + + // ---- Bit-allocation parameters ---- + pub sdcycod: u8, + pub fdcycod: u8, + pub sgaincod: u8, + pub dbpbcod: u8, + pub floorcod: u8, + pub snroffst_coarse: u8, + pub cpl_fsnroffst: u8, + pub cpl_fgaincod: u8, + pub cpl_fleak: u8, + pub cpl_sleak: u8, + pub fsnroffst: [u8; MAX_FBW], + pub fgaincod: [u8; MAX_FBW], + pub lfefsnroffst: u8, + pub lfefgaincod: u8, + + // ---- Delta bit allocation state (§5.4.3.47-57, §7.2.2.6) ---- + /// Per-channel deltba state: number of segments + per-segment offset/length/value. + /// Each fbw channel has its own segment list; index `MAX_FBW` is the + /// coupling channel. `deltnseg` of 0 means no delta-band processing. + /// State is initialized to all-zero at the top of every syncframe (§7.2.2.6 + /// "initialize the cpldeltnseg and deltnseg[ch] delta bit allocation + /// variables to 0 at the beginning of each syncframe") and updated by the + /// per-block parser when `deltbae[ch] == 1` (new info follows) or cleared + /// when `deltbae[ch] == 2` (perform no delta alloc this block). When + /// `deltbae[ch] == 0` (reuse) or `deltbaie == 0` for blk > 0, the previous + /// values are kept. + pub deltnseg: [usize; MAX_FBW + 1], + pub deltoffst: [[u8; 8]; MAX_FBW + 1], + pub deltlen: [[u8; 8]; MAX_FBW + 1], + pub deltba: [[u8; 8]; MAX_FBW + 1], + + // ---- E-AC-3 spectral extension region state (§E.3.6) ---- + /// Whether SPX is in use in the current block (`spxinu`). When false + /// the SPX synthesis step in [`dsp_block`] does nothing. + pub spx_in_use: bool, + /// `spxstrtf` copy-start sub-band index → first copied tc# is + /// `spx_bandtable(spxstrtf)`. + pub spx_strtf: u8, + /// First / one-past-last SPX sub-band (`spx_begin_subbnd` / + /// `spx_end_subbnd`, §E.2.3.3.5-6). + pub spx_begin_subbnd: usize, + pub spx_end_subbnd: usize, + /// SPX sub-band → band grouping (`spxbndstrc[]`, §E.2.3.3.8). Index + /// is the absolute sub-band number; `true` means "merge into the + /// previous band". Persisted so a `spxbndstrce == 0` block reuses + /// the prior structure. + pub spx_bndstrc: [bool; 18], + /// Number of SPX bands and per-band size in transform coefficients + /// (`nspxbnds` / `spxbndsztab[]`), derived from the sub-band range + /// and `spx_bndstrc`. + pub spx_nbnds: usize, + pub spx_bndsztab: [usize; 18], + /// 32-bit LFSR driving the SPX noise generator (§E.3.6.4.2). The + /// spec leaves the noise sequence non-normative ("any reasonably + /// random sequence"); a fixed seed keeps decodes reproducible. + pub spx_noise_lfsr: u32, + + /// Bit position immediately after the BSI (start of block 0 bits). + pub audblk_start_bits: u64, + /// Which block we are currently parsing (0..6). + pub blkidx: usize, + /// 16-bit LFSR state driving the `bap=0` dither replacement + /// (§7.3.4). Persisted across audio blocks and syncframes so the + /// dither sequence has a smooth long-period character. + pub dither_lfsr_state: u32, + /// Monotonically increasing syncframe counter used for trace gating + /// (e.g. `AC3_TRACE_FRAME=14`). Incremented at the top of every + /// `decode_frame` call. Not part of the spec — diagnostic only. + pub frame_counter: u64, + + /// E-AC-3 enhanced coupling (§E.3.5.5): when set, the §7.4 standard + /// decouple step in [`dsp_block`] is skipped because each coupled + /// channel's transform coefficients in `channels[ch].coeffs` were + /// already reconstructed from the enhanced-coupling carrier `Z[k]` + /// (the §E.3.5.5.4 complex product) by the E-AC-3 dsp layer before + /// `dsp_block` runs. Base AC-3 and standard E-AC-3 coupling leave + /// this `false` so the normal decouple applies. + pub skip_decouple: bool, + + /// E-AC-3 enhanced-coupling cross-frame synthesis state (§E.3.5.5.3 + /// random de-correlation sources). The non-transient random arrays are + /// "generated once … and the same for every block of every frame", and + /// the transient random generator advances across blocks/frames — both + /// lifetimes outlive a single syncframe, so the state lives here. Base + /// AC-3 and standard E-AC-3 coupling never touch it. + pub ecpl_state: crate::eac3::ecpl::EcplState, + + /// §6.1.9 / §7.7 dynamic-range control settings. Steers how the + /// per-block `dynrng` word (and, in RF mode, the frame-level `compr` + /// word) is turned into the linear coefficient gain. [`Default`] is + /// line-out — the mandatory §7.7.1 full-`dynrng` decode — so existing + /// behaviour is unchanged unless a caller opts in via the decoder's + /// DRC API. + pub drc: crate::drc::DrcSettings, +} + +impl Default for Ac3State { + fn default() -> Self { + Self::new() + } +} + +impl Ac3State { + pub fn new() -> Self { + Self { + channels: std::array::from_fn(|_| ChannelState::new()), + cpl_in_use: false, + phsflginu: false, + cpl_begf: 0, + cpl_endf: 0, + cpl_begf_mant: 0, + cpl_endf_mant: 0, + cpl_nsubbnd: 0, + cpl_nbnd: 0, + cpl_bndstrc: [false; 18], + cpl_coord: [[0.0; 18]; MAX_FBW], + cpl_coord_valid: [false; MAX_FBW], + cpl_phsflg: [false; 18], + rematflg: [false; 4], + sdcycod: 0, + fdcycod: 0, + sgaincod: 0, + dbpbcod: 0, + floorcod: 0, + snroffst_coarse: 0, + cpl_fsnroffst: 0, + cpl_fgaincod: 0, + cpl_fleak: 0, + cpl_sleak: 0, + fsnroffst: [0; MAX_FBW], + fgaincod: [0; MAX_FBW], + lfefsnroffst: 0, + lfefgaincod: 0, + deltnseg: [0; MAX_FBW + 1], + deltoffst: [[0; 8]; MAX_FBW + 1], + deltlen: [[0; 8]; MAX_FBW + 1], + deltba: [[0; 8]; MAX_FBW + 1], + spx_in_use: false, + spx_strtf: 0, + spx_begin_subbnd: 0, + spx_end_subbnd: 0, + spx_bndstrc: [false; 18], + spx_nbnds: 0, + spx_bndsztab: [0; 18], + spx_noise_lfsr: 0x4A5B_6C7D, + audblk_start_bits: 0, + blkidx: 0, + // Non-zero seed so the LFSR doesn't get stuck on all-zeros. + // Arbitrary fixed value keeps decodes byte-reproducible. + dither_lfsr_state: 0x1234, + frame_counter: 0, + skip_decouple: false, + ecpl_state: crate::eac3::ecpl::EcplState::new(), + drc: crate::drc::DrcSettings::default(), + } + } +} + +/// Parse (but do not DSP) one syncframe's 6 audio blocks, returning +/// the per-block [`AudBlkSideInfo`] snapshots. Used by tests and +/// introspection tools; the decoder itself calls [`decode_frame`] +/// which fuses parse + DSP. +/// +/// Side-info capture mirrors `decode_frame`'s bit-cursor: after each +/// block's side-info `parse_audblk_into` itself consumes the mantissa +/// region via [`unpack_mantissas`], so the cursor naturally lands on +/// block N+1's bits without a second walk here (a previous version +/// double-consumed the mantissas, which made every block N>0 read its +/// side-info bits from somewhere inside the previous block's mantissa +/// region). +/// Blocks whose parse fails (e.g. due to our current bit-allocation +/// approximation consuming a few mantissa bits too many) yield a +/// `Default::default()` snapshot and subsequent blocks restart from +/// the last good cursor — matching the decoder's graceful-degradation +/// policy for the §7 stages. +pub fn parse_frame_side_info( + si: &SyncInfo, + bsi: &Bsi, + frame_bytes: &[u8], +) -> Result<[AudBlkSideInfo; BLOCKS_PER_FRAME]> { + let mut state = Ac3State::new(); + let post_sync = &frame_bytes[5..]; + let mut br = BitReader::new(post_sync); + br.skip(bsi.bits_consumed as u32)?; + let mut out: [AudBlkSideInfo; BLOCKS_PER_FRAME] = Default::default(); + for blk in 0..BLOCKS_PER_FRAME { + state.blkidx = blk; + let mut side = AudBlkSideInfo::default(); + if parse_audblk_into(&mut state, si, bsi, &mut br, &mut side).is_ok() { + out[blk] = side; + } else { + // Parse error — stop side-info capture here. Downstream + // blocks are unreachable without a known bit-position. + break; + } + } + Ok(out) +} + +/// Decode one syncframe of 6 audio blocks into interleaved f32 samples. +/// Output length = 1536 × nchans. +pub fn decode_frame( + state: &mut Ac3State, + si: &SyncInfo, + bsi: &Bsi, + frame_bytes: &[u8], + out: &mut [f32], +) -> Result<()> { + // Slice starting at the beginning of BSI (byte 5 of syncframe). + let post_sync = &frame_bytes[5..]; + let mut br = BitReader::new(post_sync); + // Consume the BSI bits so we start exactly at audio block 0. + br.skip(bsi.bits_consumed as u32)?; + state.audblk_start_bits = br.bit_position(); + state.blkidx = 0; + // §7.2.2.6 / A/52 §5.4.3.47: the cpldeltnseg and deltnseg[ch] delta + // bit-allocation segment counts must be initialised to 0 at the + // start of every syncframe so a `deltbaie == 0` / `deltbae == reuse` + // block-0 inherits "no delta", not stale segments left over from the + // previous frame's dba. Without this reset a frame whose block 0 + // reuses (deltbae == 0) a prior frame's segments applies a phantom + // mask offset, perturbing bap[] and desynchronising mantissa unpack. + for d in state.deltnseg.iter_mut() { + *d = 0; + } + + let nchans = bsi.nchans as usize; // output channel count (fbw + lfe) + let nfchans = bsi.nfchans as usize; + + for blk in 0..BLOCKS_PER_FRAME { + state.blkidx = blk; + // Tolerate bit-exhaustion in later blocks: if parsing or mantissa + // unpack runs out of bits, zero-fill this block's coefficients and + // keep going. This matches the spec's graceful-degradation + // guidance for corrupt streams and also compensates for the + // current bit-allocation approximation producing slightly more + // mantissas than the encoder actually wrote. + if parse_audblk(state, si, bsi, &mut br).is_err() { + for ch in 0..MAX_CHANNELS { + for v in state.channels[ch].coeffs.iter_mut() { + *v = 0.0; + } + } + } + if std::env::var("AC3_TRACE_BITPOS").is_ok() { + // Round-12 diagnostic: verify per-block bit-cursor lands inside + // the syncframe (frame_bits ≈ 6104 for a 192 kbps frame; CRC + // and a few padding bits sit between the last block's end and + // the frame end). If `end_pos` ever exceeds `frame_bits` the + // parser is over-consuming and every later block's side-info + // bits will be misread. + eprintln!( + "TRACE-BITPOS frame={} blk={} end_pos={} frame_bits={}", + state.frame_counter, + blk, + br.bit_position(), + (frame_bytes.len() as u64 - 5) * 8 + ); + } + dsp_block(state, si, bsi); + // Write this block's SAMPLES_PER_BLOCK samples per channel + // interleaved into `out` starting at block offset. + let base = blk * SAMPLES_PER_BLOCK * nchans; + for n in 0..SAMPLES_PER_BLOCK { + for ch in 0..nfchans { + let s = state.channels[ch].coeffs[n]; + out[base + n * nchans + ch] = s; + } + if bsi.lfeon { + let s = state.channels[MAX_FBW + 1].coeffs[n]; + out[base + n * nchans + nfchans] = s; + } + } + } + // Diagnostic frame counter (gated by `AC3_TRACE_FRAME=N`). Increment + // *after* the frame so frame 0 == first decoded frame; not part of + // the spec. + state.frame_counter = state.frame_counter.saturating_add(1); + Ok(()) +} + +fn parse_audblk(state: &mut Ac3State, si: &SyncInfo, bsi: &Bsi, br: &mut BitReader) -> Result<()> { + let mut side = AudBlkSideInfo::default(); + parse_audblk_into(state, si, bsi, br, &mut side) +} + +/// Parse one `audblk()` element into [`Ac3State`] (for DSP) and +/// [`AudBlkSideInfo`] (for tests / introspection). Every bit field +/// cites its §5.4.3.x clause; the pseudo-code in Table 5.3 was the +/// authoritative reference for bit-order. Consumes exactly as many +/// bits as the spec prescribes, up to the end of the skip field; the +/// tail-end mantissas are then parsed by [`unpack_mantissas`]. +pub(crate) fn parse_audblk_into( + state: &mut Ac3State, + si: &SyncInfo, + bsi: &Bsi, + br: &mut BitReader, + side: &mut AudBlkSideInfo, +) -> Result<()> { + let _ = si; + let nfchans = bsi.nfchans as usize; + let acmod = bsi.acmod; + let blk = state.blkidx; + + // §5.4.3.1 blksw[ch] — per-channel block-switch flag (1 bit each). + for ch in 0..nfchans { + let v = br.read_u32(1)? != 0; + state.channels[ch].blksw = v; + side.blksw[ch] = v; + } + // §5.4.3.2 dithflag[ch] — per-channel dither flag (1 bit each). + for ch in 0..nfchans { + let v = br.read_u32(1)? != 0; + state.channels[ch].dithflag = v; + side.dithflag[ch] = v; + } + + // The frame-level §5.4.2.10 heavy-compression words, consulted only + // when the DRC control surface is in RF mode (§7.7.2.1). `compr` + // drives ch1 (and every fbw channel for acmod != 0); `compr_ch2` + // drives ch2 in 1+1 dual mono. + let compr_ch1 = bsi.compr.map(|c| c.raw()); + let compr_ch2 = bsi.compr_ch2.map(|c| c.raw()); + // §5.4.3.3 dynrnge — dynamic-range word present (1 bit). + let dynrnge = br.read_u32(1)? != 0; + side.dynrnge = dynrnge; + if dynrnge { + // §5.4.3.4 dynrng — 8-bit dynamic-range gain word. The DRC + // control surface (§7.7.1.2 partial compression / §7.7.2 heavy + // compression) maps the raw word to the applied linear gain; + // line-out (the default) reproduces the bare §7.7.1.2 word. + let dynrng = br.read_u32(8)? as u8; + side.dynrng = dynrng; + let g = state.drc.resolve_block_gain(dynrng, compr_ch1); + for ch in 0..nfchans { + state.channels[ch].dynrng = g; + } + } else if blk == 0 { + // §7.7.1.2 — block 0 with no dynrng word uses '0000 0000' (0 dB). + // In RF mode that 0 dB dynrng still yields to the frame's compr + // word, so route the block-0 default through the same resolver. + let g = state.drc.resolve_block_gain(0x00, compr_ch1); + for ch in 0..nfchans { + state.channels[ch].dynrng = g; + } + } + // §5.4.3.5 dynrng2e — dual-mono ch2 dynamic-range present (1 bit), + // §5.4.3.6 dynrng2 — dual-mono ch2 dynamic-range word (8 bits). + if acmod == 0 { + let dynrng2e = br.read_u32(1)? != 0; + side.dynrng2e = dynrng2e; + if dynrng2e { + let d2 = br.read_u32(8)? as u8; + side.dynrng2 = d2; + state.channels[1].dynrng = state.drc.resolve_block_gain(d2, compr_ch2); + } else if blk == 0 { + state.channels[1].dynrng = state.drc.resolve_block_gain(0x00, compr_ch2); + } + } + + // §5.4.3.7 cplstre — coupling strategy present (1 bit). + let cplstre = br.read_u32(1)? != 0; + side.cplstre = cplstre; + if cplstre { + // §5.4.3.8 cplinu — coupling in use (1 bit). + state.cpl_in_use = br.read_u32(1)? != 0; + side.cplinu = state.cpl_in_use; + if state.cpl_in_use { + // §5.4.3.9 chincpl[ch] — per-channel coupling membership (1 bit). + for ch in 0..nfchans { + let v = br.read_u32(1)? != 0; + state.channels[ch].in_coupling = v; + side.chincpl[ch] = v; + } + // §5.4.3.10 phsflginu — phase flags in use (only in 2/0 mode). + state.phsflginu = if acmod == 0x2 { + br.read_u32(1)? != 0 + } else { + false + }; + side.phsflginu = state.phsflginu; + // §5.4.3.11 cplbegf (4 bits), §5.4.3.12 cplendf (4 bits). + state.cpl_begf = br.read_u32(4)? as u8; + state.cpl_endf = br.read_u32(4)? as u8; + side.cplbegf = state.cpl_begf; + side.cplendf = state.cpl_endf; + // Per A/52 §5.4.3.12 the upper sub-band index is `cplendf+2`, + // so the spec's validity envelope is `cplbegf <= cplendf+2` + // (equivalently `ncplsubnd = 3 + cplendf - cplbegf >= 1`). + // The earlier strict `cplendf < cplbegf` rejection bombed out + // of valid 5.0 (acmod=7 lfeon=0) frames whose bitstreams pick + // narrow-coupling configs like (cplbegf=11, cplendf=10), which + // place coupling on sub-bands 11..=12 (tc bins 169..193) — a + // perfectly legal 2-sub-band coupling channel. Using signed + // arithmetic also dodges the usize underflow that the previous + // branch would have hit before the explicit check. + let nsub = 3i32 + state.cpl_endf as i32 - state.cpl_begf as i32; + if nsub < 1 { + return Err(Error::invalid( + "ac3: §5.4.3.11/12 cplbegf > cplendf+2 — malformed coupling range", + )); + } + state.cpl_nsubbnd = nsub as usize; + // §5.4.3.13 cplbndstrc[sbnd] — 1 bit per subband for sbnd >= 1. + state.cpl_bndstrc[0] = false; + for bnd in 1..state.cpl_nsubbnd { + let v = br.read_u32(1)? != 0; + state.cpl_bndstrc[bnd] = v; + side.cplbndstrc[bnd] = v; + } + // Mantissa-domain coupling range: bins [37 + 12*cplbegf, + // 37 + 12*(cplendf+3)) per §7.4.2. + state.cpl_begf_mant = 37 + 12 * state.cpl_begf as usize; + state.cpl_endf_mant = 37 + 12 * (state.cpl_endf as usize + 3); + // Derive ncplbnd by merging sub-bands whose cplbndstrc=1. + let mut n = state.cpl_nsubbnd; + for bnd in 1..state.cpl_nsubbnd { + if state.cpl_bndstrc[bnd] { + n -= 1; + } + } + state.cpl_nbnd = n; + } + } + + // §5.4.3.14 cplcoe[ch], §5.4.3.15 mstrcplco[ch], §5.4.3.16 cplcoexp, + // §5.4.3.17 cplcomant, §5.4.3.18 phsflg[bnd]. + if state.cpl_in_use { + let mut any = false; + for ch in 0..nfchans { + if state.channels[ch].in_coupling { + // §5.4.3.14 cplcoe[ch] — coupling coordinates present (1 bit). + let cplcoe = br.read_u32(1)? != 0; + side.cplcoe[ch] = cplcoe; + if cplcoe { + any = true; + // §5.4.3.15 mstrcplco[ch] — master coupling coord (2 bits). + let mstrcplco = br.read_u32(2)? as i32; + for bnd in 0..state.cpl_nbnd { + // §5.4.3.16 cplcoexp[ch][bnd] — 4 bits. + let cplcoexp = br.read_u32(4)? as i32; + // §5.4.3.17 cplcomant[ch][bnd] — 4 bits. + let cplcomant = br.read_u32(4)? as i32; + let mant = if cplcoexp == 15 { + cplcomant as f32 / 16.0 + } else { + (cplcomant + 16) as f32 / 32.0 + }; + let shift = cplcoexp + 3 * mstrcplco; + state.cpl_coord[ch][bnd] = mant * 2f32.powi(-shift); + } + state.cpl_coord_valid[ch] = true; + } + } + } + // §5.4.3.18 phsflg[bnd] — only when 2/0, phsflginu, and at + // least one channel emitted coupling coordinates this block. + if acmod == 0x2 && state.phsflginu && any { + for bnd in 0..state.cpl_nbnd { + state.cpl_phsflg[bnd] = br.read_u32(1)? != 0; + } + } + } + + // §5.4.3.19 rematstr — rematrix strategy (only in 2/0 mode). + // §5.4.3.20 rematflg[rbnd] — per-band rematrix flag. + if acmod == 0x2 { + let rematstr = br.read_u32(1)? != 0; + side.rematstr = rematstr; + if rematstr { + let n_remat = remat_band_count(state.cpl_in_use, state.cpl_begf); + side.rematflg_count = n_remat as u8; + for rbnd in 0..n_remat { + let v = br.read_u32(1)? != 0; + state.rematflg[rbnd] = v; + side.rematflg[rbnd] = v; + } + } + if std::env::var("AC3_TRACE_REMAT").is_ok() { + eprintln!( + "TRACE-REMAT blk={} rematstr={} rematflg={:?}", + blk, rematstr, state.rematflg + ); + } + } + + // §5.4.3.21 cplexpstr — coupling exponent strategy (2 bits). + // §5.4.3.22 chexpstr[ch] — fbw channel exponent strategy (2 bits). + // §5.4.3.23 lfeexpstr — LFE exponent strategy (1 bit). + // §5.4.3.24 chbwcod[ch] — channel bandwidth code (6 bits). + let mut cplexpstr = 0u8; + let mut chexpstr = [0u8; MAX_FBW]; + let mut lfeexpstr = 0u8; + if state.cpl_in_use { + cplexpstr = br.read_u32(2)? as u8; + } + side.cplexpstr = cplexpstr; + for ch in 0..nfchans { + chexpstr[ch] = br.read_u32(2)? as u8; + side.chexpstr[ch] = chexpstr[ch]; + } + if bsi.lfeon { + lfeexpstr = br.read_u32(1)? as u8; + } + side.lfeexpstr = lfeexpstr; + // chbwcod — only for non-coupled independent fbw channels with new exponents. + let mut chbwcod = [0u8; MAX_FBW]; + for ch in 0..nfchans { + if chexpstr[ch] != 0 && !state.channels[ch].in_coupling { + chbwcod[ch] = br.read_u32(6)? as u8; + side.chbwcod[ch] = chbwcod[ch]; + if chbwcod[ch] > 60 { + return Err(Error::invalid("ac3: chbwcod > 60")); + } + } + } + + // --- unpack coupling exponents --- + if state.cpl_in_use && cplexpstr != 0 { + let cplabsexp = br.read_u32(4)? as i32; + let cpl_start = state.cpl_begf_mant; + let cpl_end = state.cpl_endf_mant; + let grpsize = match cplexpstr { + 1 => 1, + 2 => 2, + 3 => 4, + _ => 1, + }; + let ncplgrps = (cpl_end - cpl_start) / (grpsize * 3); + // Absolute exponent for coupling: cplabsexp << 1 (from 4-bit range 0..15 to full 5-bit 0..30). + let mut raw_exp = vec![0i32; ncplgrps * 3]; + decode_exponents( + br, + cplabsexp << 1, + ncplgrps, + cplexpstr as usize, + &mut raw_exp, + )?; + let ch_idx = MAX_FBW; + // Offset for coupling per spec: cplexp[n + cplstrtmant] = exp[n+1], + // i.e. the absolute exponent is used as a reference only and the + // actual exponents start at index 1. + for (i, e) in raw_exp.iter().enumerate() { + let idx = cpl_start + i * grpsize; + for j in 0..grpsize { + if idx + j < N_COEFFS { + state.channels[ch_idx].exp[idx + j] = (*e).clamp(0, 24) as u8; + } + } + } + if std::env::var("AC3_TRACE_CPL").is_ok() { + eprintln!( + "TRACE-CPL blk={} cplexpstr={} cplabsexp={} (<<1={}) ncplgrps={} grpsize={}", + blk, + cplexpstr, + cplabsexp, + cplabsexp << 1, + ncplgrps, + grpsize + ); + eprintln!( + "TRACE-CPL raw_exp first 12: {:?}", + &raw_exp[..raw_exp.len().min(12)] + ); + eprintln!( + "TRACE-CPL placed cpl exp[{}..{}]: {:?}", + cpl_start, + cpl_end, + &state.channels[ch_idx].exp[cpl_start..cpl_end.min(cpl_start + 30)] + ); + } + } + + // --- unpack fbw channel exponents + gainrng --- + for ch in 0..nfchans { + if chexpstr[ch] != 0 { + let strt = 0usize; + let end = if state.channels[ch].in_coupling { + state.cpl_begf_mant + } else { + 37 + 3 * (chbwcod[ch] as usize + 12) + }; + state.channels[ch].end_mant = end; + + // reason: kept as a compile-time-disabled debug probe that is flipped + // to `true` locally when inspecting exponent strategy bit alignment. + #[allow(clippy::overly_complex_bool_expr)] + if false && blk == 0 { + eprintln!( + "ch{} exps start at bit {}, end_mant={}, strategy={}", + ch, + br.bit_position(), + end, + chexpstr[ch] + ); + } + let absexp = br.read_u32(4)? as i32; + let grpsize = match chexpstr[ch] { + 1 => 1, + 2 => 2, + 3 => 4, + _ => 1, + }; + // Guard against `end == 0` (rare: a fully-coupled channel + // whose cpl_begf_mant lands at 0 has no independent + // exponents). Without this guard the `(end - 1) / k` term + // would underflow under debug arithmetic and panic the + // decoder mid-frame. + let nchgrps = if end == 0 { + 0usize + } else { + match chexpstr[ch] { + 1 => (end - 1) / 3, + 2 => (end - 1 + 3) / 6, + 3 => (end - 1 + 9) / 12, + _ => 0, + } + }; + let mut raw_exp = vec![0i32; nchgrps * 3]; + decode_exponents(br, absexp, nchgrps, chexpstr[ch] as usize, &mut raw_exp)?; + // Place: exp[0] = absexp; exp[i*grpsize + 1 + j] = raw_exp[i] + state.channels[ch].exp[strt] = absexp.clamp(0, 24) as u8; + for (i, e) in raw_exp.iter().enumerate() { + let base = i * grpsize + 1; + for j in 0..grpsize { + if base + j < end { + state.channels[ch].exp[base + j] = (*e).clamp(0, 24) as u8; + } + } + } + let _gainrng = br.read_u32(2)?; + } else if blk == 0 { + return Err(Error::invalid("ac3: chexpstr=0 in block 0")); + } + } + + // --- unpack LFE exponents --- + if bsi.lfeon { + let lfe_ch = MAX_FBW + 1; + state.channels[lfe_ch].end_mant = 7; + if lfeexpstr != 0 { + let absexp = br.read_u32(4)? as i32; + let nlfegrps = 2usize; + let mut raw_exp = vec![0i32; nlfegrps * 3]; + decode_exponents(br, absexp, nlfegrps, 1, &mut raw_exp)?; + state.channels[lfe_ch].exp[0] = absexp.clamp(0, 24) as u8; + for (i, e) in raw_exp.iter().enumerate() { + if i + 1 < 7 { + state.channels[lfe_ch].exp[i + 1] = (*e).clamp(0, 24) as u8; + } + } + } + } + + // §5.4.3.30 baie — bit-allocation info exists (1 bit). + // §5.4.3.31-35 sdcycod/fdcycod/sgaincod/dbpbcod/floorcod parametric + // masking words, present iff baie. + let baie = br.read_u32(1)? != 0; + side.baie = baie; + if baie { + state.sdcycod = br.read_u32(2)? as u8; + state.fdcycod = br.read_u32(2)? as u8; + state.sgaincod = br.read_u32(2)? as u8; + state.dbpbcod = br.read_u32(2)? as u8; + state.floorcod = br.read_u32(3)? as u8; + } + // §5.4.3.36 snroffste — SNR-offset block flag (1 bit). + let snroffste = br.read_u32(1)? != 0; + side.snroffste = snroffste; + if snroffste { + // §5.4.3.37 csnroffst — coarse SNR offset (6 bits). + state.snroffst_coarse = br.read_u32(6)? as u8; + if state.cpl_in_use { + // §5.4.3.38 cplfsnroffst (4 bits), §5.4.3.39 cplfgaincod (3 bits). + state.cpl_fsnroffst = br.read_u32(4)? as u8; + state.cpl_fgaincod = br.read_u32(3)? as u8; + } + for ch in 0..nfchans { + // §5.4.3.40 fsnroffst[ch] (4 bits), §5.4.3.41 fgaincod[ch] (3 bits). + state.fsnroffst[ch] = br.read_u32(4)? as u8; + state.fgaincod[ch] = br.read_u32(3)? as u8; + } + if bsi.lfeon { + // §5.4.3.42 lfefsnroffst (4 bits), §5.4.3.43 lfefgaincod (3 bits). + state.lfefsnroffst = br.read_u32(4)? as u8; + state.lfefgaincod = br.read_u32(3)? as u8; + } + } + // §5.4.3.44 cplleake — coupling leak init flag (1 bit). + // §5.4.3.45 cplfleak (3 bits), §5.4.3.46 cplsleak (3 bits). + if state.cpl_in_use { + let cplleake = br.read_u32(1)? != 0; + side.cplleake = cplleake; + if cplleake { + state.cpl_fleak = br.read_u32(3)? as u8; + state.cpl_sleak = br.read_u32(3)? as u8; + } + } + + // §5.4.3.47 deltbaie — delta bit allocation info exists (1 bit). + // §5.4.3.48-57 — per-channel + coupling delta bit allocation segments. + // §7.2.2.6 — apply per-band ±6 dB mask offsets BEFORE final bit + // allocation. Critical for transient blocks where the encoder uses + // dba to lift the masking floor in low-energy bands so they don't + // get assigned mantissa bits the encoder needs for the burst peak. + let deltbaie = br.read_u32(1)? != 0; + side.deltbaie = deltbaie; + if deltbaie { + let cpl_idx = MAX_FBW; + let mut cpldeltbae = 0u32; + if state.cpl_in_use { + cpldeltbae = br.read_u32(2)?; + } + let mut deltbae = [0u32; MAX_FBW]; + for ch in 0..nfchans { + deltbae[ch] = br.read_u32(2)?; + } + // Per Table 5.16: 0=reuse, 1=new info, 2=no delta this block, 3=reserved. + if state.cpl_in_use { + match cpldeltbae { + 1 => { + let nseg = (br.read_u32(3)? + 1) as usize; + state.deltnseg[cpl_idx] = nseg.min(8); + for seg in 0..state.deltnseg[cpl_idx] { + state.deltoffst[cpl_idx][seg] = br.read_u32(5)? as u8; + state.deltlen[cpl_idx][seg] = br.read_u32(4)? as u8; + state.deltba[cpl_idx][seg] = br.read_u32(3)? as u8; + } + } + 2 => { + // "perform no delta alloc" — clear segments for this block. + state.deltnseg[cpl_idx] = 0; + } + _ => { + // 0 = reuse previous; 3 = reserved (treat as reuse to stay + // robust against malformed streams). + } + } + } + for ch in 0..nfchans { + match deltbae[ch] { + 1 => { + let nseg = (br.read_u32(3)? + 1) as usize; + state.deltnseg[ch] = nseg.min(8); + for seg in 0..state.deltnseg[ch] { + state.deltoffst[ch][seg] = br.read_u32(5)? as u8; + state.deltlen[ch][seg] = br.read_u32(4)? as u8; + state.deltba[ch][seg] = br.read_u32(3)? as u8; + } + } + 2 => { + state.deltnseg[ch] = 0; + } + _ => {} + } + } + } else if blk == 0 { + // §5.4.3.47 spec: "If deltbaie is '0' in block 0, then cpldeltbae + // and deltbae[ch] are set to the binary value '10', and no delta + // bit allocation is applied." This means clear all segments. + for ch in 0..MAX_FBW + 1 { + state.deltnseg[ch] = 0; + } + } + + // §5.4.3.58 skiple — skip-length-exists flag (1 bit). + // §5.4.3.59 skipl — skip length in *bytes* (9 bits). + // §5.4.3.60 skipfld — `skipl × 8` skip-data bits. + let skiple = br.read_u32(1)? != 0; + side.skiple = skiple; + if skiple { + let skipl = br.read_u32(9)?; + side.skipl = skipl as u16; + br.skip(skipl * 8)?; + } + + // --- run bit allocation per channel --- + for ch in 0..nfchans { + let end = state.channels[ch].end_mant; + run_bit_allocation( + state, + ch, + 0, + end, + si.fscod, + state.fsnroffst[ch], + state.fgaincod[ch], + false, + ); + } + if state.cpl_in_use { + let start = state.cpl_begf_mant; + let end = state.cpl_endf_mant; + run_bit_allocation( + state, + MAX_FBW, + start, + end, + si.fscod, + state.cpl_fsnroffst, + state.cpl_fgaincod, + true, + ); + } + if bsi.lfeon { + let lfe_ch = MAX_FBW + 1; + run_bit_allocation( + state, + lfe_ch, + 0, + 7, + si.fscod, + state.lfefsnroffst, + state.lfefgaincod, + false, + ); + } + + // --- unpack mantissas --- + if std::env::var("AC3_DEBUG_FULL").is_ok() && blk == 0 { + let mut histo = [0u32; 16]; + for ch in 0..nfchans { + let end = state.channels[ch].end_mant; + for bin in 0..end { + histo[state.channels[ch].bap[bin] as usize] += 1; + } + } + if state.cpl_in_use { + for bin in state.cpl_begf_mant..state.cpl_endf_mant { + histo[state.channels[MAX_FBW].bap[bin] as usize] += 1; + } + } + eprintln!("block 0 bap histogram (incl cpl): {:?}", histo); + eprintln!( + "ch_cpl bap[begf..end]: {:?}", + &state.channels[MAX_FBW].bap + [state.cpl_begf_mant..state.cpl_endf_mant.min(state.cpl_begf_mant + 30)] + ); + eprintln!( + "ch_cpl exp[begf..end]: {:?}", + &state.channels[MAX_FBW].exp + [state.cpl_begf_mant..state.cpl_endf_mant.min(state.cpl_begf_mant + 30)] + ); + eprintln!("ch0 bap[0..133]: {:?}", &state.channels[0].bap[0..133]); + eprintln!("ch1 bap[0..133]: {:?}", &state.channels[1].bap[0..133]); + eprintln!("ch1 exp[0..133]: {:?}", &state.channels[1].exp[0..133]); + eprintln!("ch0 exp[0..40]: {:?}", &state.channels[0].exp[0..40]); + eprintln!("ch0 psd[0..10]: {:?}", &state.channels[0].psd[0..10]); + eprintln!("ch0 bndpsd[0..10]: {:?}", &state.channels[0].bndpsd[0..10]); + eprintln!( + "cpl in_use: {} begf: {} endf: {} begf_mant: {} endf_mant: {} nsubbnd: {} nbnd: {}", + state.cpl_in_use, + state.cpl_begf, + state.cpl_endf, + state.cpl_begf_mant, + state.cpl_endf_mant, + state.cpl_nsubbnd, + state.cpl_nbnd + ); + eprintln!( + "ch0 end_mant: {}, ch1 end_mant: {}", + state.channels[0].end_mant, state.channels[1].end_mant + ); + eprintln!( + "snroffst: csnr={} cpl_fsnr={} cpl_fgain={} cpl_fleak={} cpl_sleak={}", + state.snroffst_coarse, + state.cpl_fsnroffst, + state.cpl_fgaincod, + state.cpl_fleak, + state.cpl_sleak + ); + eprintln!( + "sdcy={} fdcy={} sgain={} dbpb={} floor={}", + state.sdcycod, state.fdcycod, state.sgaincod, state.dbpbcod, state.floorcod + ); + eprintln!("ch0 psd[0..20]: {:?}", &state.channels[0].psd[0..20]); + eprintln!("ch0 bndpsd[0..20]: {:?}", &state.channels[0].bndpsd[0..20]); + eprintln!("ch0 mask[0..20]: {:?}", &state.channels[0].mask[0..20]); + let total_bits: u32 = histo + .iter() + .enumerate() + .map(|(b, n)| match b { + 0 => 0, + 1 => 5 * n / 3 + (if n % 3 != 0 { 5 } else { 0 }), + 2 => 7 * n / 3 + (if n % 3 != 0 { 7 } else { 0 }), + 3 => 3 * n, + 4 => 7 * n / 2 + (if n % 2 != 0 { 7 } else { 0 }), + 5 => 4 * n, + _ => crate::tables::QUANTIZATION_BITS[b] as u32 * n, + }) + .sum(); + eprintln!( + "block 0 pre-mantissa bit pos {}, estimated mantissa bits {}", + br.bit_position(), + total_bits + ); + } + unpack_mantissas(state, bsi, br)?; + + Ok(()) +} + +/// Number of rematrix bands (Table 5.15). +// +// reason: the two `4` arms are spec-faithful — Table 5.15 lists nrematbnd=4 +// for both "coupling not in use" and "cplbegf > 2". Collapsing them would +// obscure the table structure for future audits. +#[allow(clippy::if_same_then_else)] +pub(crate) fn remat_band_count(cplinu: bool, cplbegf: u8) -> usize { + if !cplinu { + 4 + } else if cplbegf > 2 { + 4 + } else if cplbegf > 0 { + 3 + } else { + 2 + } +} + +/// E-AC-3 number-of-rematrix-bands `nrematbd` per §E.3.3.2, which folds +/// in both spectral extension and enhanced coupling. +/// +/// The §E.3.3.2 pseudo-code decision tree, transcribed verbatim: +/// +/// ```text +/// if (cplinu) { +/// if (ecplinu) { // enhanced coupling +/// if (ecplbegf == 0) nrematbd = 0 +/// else if (ecplbegf == 1) nrematbd = 1 +/// else if (ecplbegf == 2) nrematbd = 2 +/// else if (ecplbegf < 5) nrematbd = 3 +/// else nrematbd = 4 +/// } else { // standard coupling +/// if (cplbegf == 0) nrematbd = 2 +/// else if (cplbegf < 3) nrematbd = 3 +/// else nrematbd = 4 +/// } +/// } else if (spxinu) { +/// if (spxbegf < 2) nrematbd = 3 else nrematbd = 4 +/// } else { +/// nrematbd = 4 +/// } +/// ``` +/// +/// The standard-coupling arm is identical to [`remat_band_count`]. The +/// enhanced-coupling arm is new (an `ecplinu` 2/0 block uses `ecplbegf`, +/// not `cplbegf`, to size the rematrix-flag field — and uniquely admits +/// `nrematbd = 0`, suppressing the field entirely). `spx_in_use` / +/// `spx_begin_subbnd` come from the SPX strategy block; `spxbegf < 2` is +/// equivalent to `spx_begin_subbnd < 4`. `ecplbegf` is the raw 4-bit +/// `ecplbegf` code carried on [`crate::eac3::ecpl::EcplStrategy`]. +pub(crate) fn remat_band_count_spx( + cplinu: bool, + cplbegf: u8, + ecpl_in_use: bool, + ecplbegf: u8, + spx_in_use: bool, + spx_begin_subbnd: usize, +) -> usize { + if cplinu { + if ecpl_in_use { + // §E.3.3.2 enhanced-coupling arm — thresholds the raw ecplbegf. + match ecplbegf { + 0 => 0, + 1 => 1, + 2 => 2, + 3 | 4 => 3, + _ => 4, + } + } else { + remat_band_count(true, cplbegf) + } + } else if spx_in_use { + if spx_begin_subbnd < 4 { + 3 + } else { + 4 + } + } else { + 4 + } +} + +/// Decode a grouped exponent run (§7.1.3). +pub(crate) fn decode_exponents( + br: &mut BitReader, + absexp: i32, + ngrps: usize, + _expstr: usize, + out: &mut [i32], +) -> Result<()> { + // Spec §7.1.3 pseudo-code: unpack mapped values, convert to dexp + // (subtract 2), then prefix-sum with the seeding absolute exponent. + let mut prev = absexp; + for grp in 0..ngrps { + let gexp = br.read_u32(7)? as i32; + let m1 = gexp / 25; + let m2 = (gexp % 25) / 5; + let m3 = (gexp % 25) % 5; + let dexp0 = m1 - 2; + let dexp1 = m2 - 2; + let dexp2 = m3 - 2; + let e0 = prev + dexp0; + let e1 = e0 + dexp1; + let e2 = e1 + dexp2; + out[grp * 3] = e0; + out[grp * 3 + 1] = e1; + out[grp * 3 + 2] = e2; + prev = e2; + } + // Post-chain, clamp each exponent to the valid 0..=24 range. Encoders + // that overshoot (e.g. when encoding an exactly-zero channel after + // rematrix) rely on this for sane decoder output. + for v in out.iter_mut() { + *v = (*v).clamp(0, 24); + } + Ok(()) +} + +/// Parametric bit allocation (§7.2.2) for a single channel range. +/// `start`..`end` is the mantissa-bin range. +#[allow(clippy::too_many_arguments)] +pub(crate) fn run_bit_allocation( + state: &mut Ac3State, + ch: usize, + start: usize, + end: usize, + fscod: u8, + fsnroffst: u8, + fgaincod: u8, + is_coupling: bool, +) { + if end <= start { + return; + } + // 1) Map exponents into PSD (§7.2.2.2). + for bin in start..end { + let e = state.channels[ch].exp[bin] as i32; + state.channels[ch].psd[bin] = (3072 - (e << 7)) as i16; + } + // 2) PSD integration (§7.2.2.3). + let bndstrt = MASKTAB[start] as usize; + let bndend = MASKTAB[end - 1] as usize + 1; + { + let mut j = start; + let mut k = bndstrt; + loop { + let lastbin = (BNDTAB[k] as usize + BNDSZ[k] as usize).min(end); + state.channels[ch].bndpsd[k] = state.channels[ch].psd[j]; + j += 1; + while j < lastbin { + let a = state.channels[ch].bndpsd[k] as i32; + let b = state.channels[ch].psd[j] as i32; + state.channels[ch].bndpsd[k] = logadd(a, b) as i16; + j += 1; + } + k += 1; + if end <= lastbin { + break; + } + } + } + + // 3) Excitation / masking (§7.2.2.4-7.2.2.5). + let sdecay = SLOWDEC[state.sdcycod as usize]; + let fdecay = FASTDEC[state.fdcycod as usize]; + let sgain = SLOWGAIN[state.sgaincod as usize]; + let dbknee = DBPBTAB[state.dbpbcod as usize]; + let floor = FLOORTAB[state.floorcod as usize]; + let fgain = FASTGAIN[fgaincod as usize]; + let snroffset = (((state.snroffst_coarse as i32 - 15) << 4) + fsnroffst as i32) << 2; + + let (fastleak_init, slowleak_init) = if is_coupling { + ((state.cpl_fleak as i32) << 8, (state.cpl_sleak as i32) << 8) + } else { + (0i32, 0i32) + }; + let mut fastleak = fastleak_init + 768; + let mut slowleak = slowleak_init + 768; + + let mut excite = [0i32; 50]; + let mut lowcomp = 0i32; + let mut begin; + + if is_coupling { + begin = bndstrt; + for bin in bndstrt..bndend { + fastleak -= fdecay; + fastleak = fastleak.max(state.channels[ch].bndpsd[bin] as i32 - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(state.channels[ch].bndpsd[bin] as i32 - sgain); + excite[bin] = fastleak.max(slowleak); + } + let _ = begin; + } else if bndstrt == 0 { + // fbw channel path (and LFE with same start=0) + let lfe_last = end == 7; + let bpsd = |i: usize| -> i32 { state.channels[ch].bndpsd[i.min(49)] as i32 }; + if 0 < bndend { + lowcomp = calc_lowcomp(lowcomp, bpsd(0), bpsd(1), 0); + excite[0] = bpsd(0) - fgain - lowcomp; + } + if 1 < bndend { + lowcomp = calc_lowcomp(lowcomp, bpsd(1), bpsd(2), 1); + excite[1] = bpsd(1) - fgain - lowcomp; + } + begin = 7.min(bndend); + for bin in 2..7.min(bndend) { + if !(lfe_last && bin == 6) { + lowcomp = calc_lowcomp(lowcomp, bpsd(bin), bpsd(bin + 1), bin); + } + fastleak = bpsd(bin) - fgain; + slowleak = bpsd(bin) - sgain; + excite[bin] = fastleak - lowcomp; + if !(lfe_last && bin == 6) && bpsd(bin) <= bpsd(bin + 1) { + begin = bin + 1; + break; + } + } + for bin in begin..22.min(bndend) { + if !(lfe_last && bin == 6) { + lowcomp = calc_lowcomp(lowcomp, bpsd(bin), bpsd(bin + 1), bin); + } + fastleak -= fdecay; + fastleak = fastleak.max(bpsd(bin) - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bpsd(bin) - sgain); + excite[bin] = (fastleak - lowcomp).max(slowleak); + } + // 22..bndend path (with coupling-channel-style rule). + if bndend > 22 { + for bin in 22..bndend { + fastleak -= fdecay; + fastleak = fastleak.max(bpsd(bin) - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bpsd(bin) - sgain); + excite[bin] = fastleak.max(slowleak); + } + } + } else { + // Shouldn't really hit this path for non-coupling in our data, but + // cover it: behave like decoupled fbw starting at bndstrt. + begin = bndstrt; + for bin in begin..bndend { + fastleak -= fdecay; + fastleak = fastleak.max(state.channels[ch].bndpsd[bin] as i32 - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(state.channels[ch].bndpsd[bin] as i32 - sgain); + excite[bin] = fastleak.max(slowleak); + } + } + + // Compute masking curve (§7.2.2.5). + let mut mask = [0i32; 50]; + let hth_row = &HTH[fscod as usize]; + for bin in bndstrt..bndend { + let mut exc = excite[bin]; + if (state.channels[ch].bndpsd[bin] as i32) < dbknee { + exc += (dbknee - state.channels[ch].bndpsd[bin] as i32) >> 2; + } + mask[bin] = exc.max(hth_row[bin] as i32); + } + + // Apply delta bit allocation (§7.2.2.6). Per-band ±6 dB mask offsets + // signalled by the encoder. Critical for transient blocks: the §7.2.2.6 + // mechanism BOOSTs the masking floor (less bits assigned) in low-energy + // bands during a burst frame, freeing bit budget for the burst peak. + // Without applying these offsets, our decoder ends up with a different + // mask shape than the encoder used — and therefore a different bap[] + // assignment, which causes our mantissa unpacking to read wrong-width + // codes from the bitstream. The downstream symptom is wildly wrong + // coefficients in burst frames (PSNR ≈ 5–15 dB). The dba code below + // is the literal §7.2.2.6 pseudocode; the per-block deltba state is + // maintained by the parser per Table 5.16 semantics. + // LFE has no delta bit allocation per spec syntax (Table 5.3 lists + // only cpldeltbae + per-fbw deltbae[ch]). Skip when ch is the LFE + // pseudo-channel (index MAX_FBW+1 in our channel array). + let is_lfe = !is_coupling && ch > MAX_FBW; + let dba_idx = if is_coupling { MAX_FBW } else { ch }; + if !is_lfe && state.deltnseg[dba_idx] > 0 { + let mut band = 0usize; + for seg in 0..state.deltnseg[dba_idx] { + band += state.deltoffst[dba_idx][seg] as usize; + let dba_raw = state.deltba[dba_idx][seg] as i32; + let delta = if dba_raw >= 4 { + (dba_raw - 3) << 7 + } else { + (dba_raw - 4) << 7 + }; + let len = state.deltlen[dba_idx][seg] as usize; + for _ in 0..len { + if band < 50 { + mask[band] += delta; + } + band += 1; + } + } + } + + // Persist masking curve onto the channel state for diagnostics. + for bin in bndstrt..bndend { + state.channels[ch].mask[bin] = mask[bin] as i16; + } + + // 4) Compute bit allocation pointers (§7.2.2.7). + { + let mut i = start; + let mut j = MASKTAB[start] as usize; + loop { + let lastbin = (BNDTAB[j] as usize + BNDSZ[j] as usize).min(end); + let mut m = mask[j]; + m -= snroffset; + m -= floor; + if m < 0 { + m = 0; + } + m &= 0x1fe0; + m += floor; + while i < lastbin { + let addr = ((state.channels[ch].psd[i] as i32 - m) >> 5).clamp(0, 63) as usize; + state.channels[ch].bap[i] = BAPTAB[addr]; + i += 1; + } + if i >= end { + break; + } + j += 1; + } + } + + // ---- Diagnostic trace (gated by `AC3_TRACE_FRAME=N` and `AC3_TRACE_BLK=B`) ---- + // Dumps bndpsd / excite / mask / bap for the requested frame+block. Used + // to compare against the validator binary's decode of the same fixture. + // Cheap when the env vars aren't set. + let trace_frame = std::env::var("AC3_TRACE_FRAME") + .ok() + .and_then(|s| s.parse::().ok()); + let trace_blk = std::env::var("AC3_TRACE_BLK") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + if let Some(tf) = trace_frame { + if tf == state.frame_counter && state.blkidx == trace_blk { + let label = if is_coupling { + "cpl".to_string() + } else if ch == MAX_FBW + 1 { + "lfe".to_string() + } else { + format!("ch{ch}") + }; + eprintln!( + "TRACE frame={} blk={} {} bndstrt={} bndend={} start={} end={} fgain={:#x} sgain={:#x} fdecay={:#x} sdecay={:#x} dbknee={:#x} floor={:#x} snroffset={:#x}", + state.frame_counter, + state.blkidx, + label, + bndstrt, + bndend, + start, + end, + fgain, + sgain, + fdecay, + sdecay, + dbknee, + floor, + snroffset + ); + eprintln!( + "TRACE exp[0..bndend]: {:?}", + &state.channels[ch].exp[start..start + bndend.min(20)] + ); + eprintln!( + "TRACE psd[0..bndend]: {:?}", + &state.channels[ch].psd[start..start + bndend.min(20)] + ); + eprintln!( + "TRACE bndpsd[bndstrt..bndend]: {:?}", + &state.channels[ch].bndpsd[bndstrt..bndend] + ); + eprintln!( + "TRACE excite[bndstrt..bndend]: {:?}", + &excite[bndstrt..bndend] + ); + eprintln!("TRACE mask[bndstrt..bndend]: {:?}", &mask[bndstrt..bndend]); + eprintln!( + "TRACE bap[start..end]: {:?}", + &state.channels[ch].bap[start..end.min(start + 30)] + ); + // Re-derive the lowcomp progression for the first 7 bins so we + // can audit calc_lowcomp by hand against the spec table. + if bndstrt == 0 && !is_coupling { + let mut lc = 0i32; + let bp = |i: usize| state.channels[ch].bndpsd[i.min(49)] as i32; + eprint!("TRACE lowcomp progression: "); + for bin in 0..7.min(bndend) { + if bin + 1 < 50 { + lc = calc_lowcomp(lc, bp(bin), bp(bin + 1), bin); + } + eprint!("[bin={} lc={}] ", bin, lc); + } + eprintln!(); + } + } + } +} + +/// Log-addition (§7.2.2.3 logadd). +fn logadd(a: i32, b: i32) -> i32 { + let c = a - b; + let addr = ((c.abs() >> 1) as usize).min(255); + if c >= 0 { + a + LATAB[addr] as i32 + } else { + b + LATAB[addr] as i32 + } +} + +/// calc_lowcomp (§7.2.2.4). +fn calc_lowcomp(a: i32, b0: i32, b1: i32, bin: usize) -> i32 { + let mut a = a; + if bin < 7 { + if b0 + 256 == b1 { + a = 384; + } else if b0 > b1 { + a = (a - 64).max(0); + } + } else if bin < 20 { + if b0 + 256 == b1 { + a = 320; + } else if b0 > b1 { + a = (a - 64).max(0); + } + } else { + a = (a - 128).max(0); + } + a +} + +/// 16-bit Galois LFSR used to generate dither for `bap=0` mantissas +/// (§7.3.4). The spec says "any reasonably random sequence may be +/// used" — we use the classic x^16 + x^14 + x^13 + x^11 + 1 polynomial +/// because it has a maximal 65535-sample period and the output looks +/// like white noise to within a few bits. Seed is arbitrary but +/// fixed so decodes are deterministic. +pub(crate) fn dither_lfsr(state: &mut u32) -> f32 { + // Advance the 16-bit LFSR one step and return a uniform value in + // the range `[-0.707, 0.707)` — the spec's "optimum" scaling + // (0.707 ≈ 1/√2). Uses the classic Fibonacci taps at bits + // 15, 13, 12, 10 of a 16-bit state. + let bit = ((*state >> 15) ^ (*state >> 13) ^ (*state >> 12) ^ (*state >> 10)) & 1; + *state = ((*state << 1) | bit) & 0xFFFF; + // Center around zero: bit15 of the 16-bit state becomes the sign, + // lower 15 bits provide magnitude. + let signed = (*state as i32).wrapping_sub(0x8000) as f32 / 32768.0; + signed * 0.707 +} + +/// Unpack + dequantize mantissas for all channels (§7.3). +/// Populates ChannelState.coeffs with dequantized transform coefficients. +pub(crate) fn unpack_mantissas(state: &mut Ac3State, bsi: &Bsi, br: &mut BitReader) -> Result<()> { + let nfchans = bsi.nfchans as usize; + // Zero any leftover coefficient slots so stale data from prior blocks + // can never bleed into the IMDCT input. Unpacked mantissas overwrite + // bins 0..end_mant, decoupling overwrites bins in the coupling range, + // but all other bins (e.g. end_mant..N_COEFFS on an uncoupled or + // narrow-band channel) must read as exactly zero. + for ch in 0..MAX_CHANNELS { + for v in state.channels[ch].coeffs.iter_mut() { + *v = 0.0; + } + } + let mut got_cplchan = false; + // Grouped-mantissa buffers per bap: values 1,2,4 have triples/pairs + // shared across channels in frequency order. The spec says groups + // are *shared across exponent sets*, meaning once a group is started + // for bap=1 (5-bit triple), subsequent mantissas of bap=1 consume + // from that group, even if a different channel emits them. We + // implement this per-bap buffer state. + let mut grp1: [f32; 3] = [0.0; 3]; + let mut grp1_n = 0usize; // remaining in buffer + let mut grp2: [f32; 3] = [0.0; 3]; + let mut grp2_n = 0usize; + let mut grp4: [f32; 2] = [0.0; 2]; + let mut grp4_n = 0usize; + + // Optional per-block mantissa trace gated by `AC3_TRACE_FRAME=N` and + // `AC3_TRACE_BLK=B` plus `AC3_TRACE_MANT=1`. Used by maintainers when + // chasing bit-stream alignment issues; off by default to keep the + // hot path clean. + let trace_mant_frame = std::env::var("AC3_TRACE_FRAME") + .ok() + .and_then(|s| s.parse::().ok()); + let trace_mant_blk = std::env::var("AC3_TRACE_BLK") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let trace_mant_on = std::env::var("AC3_TRACE_MANT").is_ok(); + for ch in 0..nfchans { + let end = state.channels[ch].end_mant; + let dith = state.channels[ch].dithflag; + let trace_this = trace_mant_on + && trace_mant_frame == Some(state.frame_counter) + && state.blkidx == trace_mant_blk; + if trace_this { + eprintln!( + "TRACE-MANT ch{} mantissa decode (end={}, dith={}):", + ch, end, dith + ); + } + for bin in 0..end { + let bap = state.channels[ch].bap[bin]; + let bit_pos_before = if trace_this { br.bit_position() } else { 0 }; + let val = fetch_mantissa( + br, + bap, + &mut grp1, + &mut grp1_n, + &mut grp2, + &mut grp2_n, + &mut grp4, + &mut grp4_n, + false, + )?; + // Dither for bap=0 mantissas (§7.3.4): when dithflag is + // set, replace the zero-level mantissa with an LFSR-driven + // pseudo-random value scaled by 0.707 before the standard + // `>> exponent` coefficient reconstruction. This fills + // inaudible masked bands with near-noise instead of + // silence, preventing coloration of subsequent DSP stages + // (especially rematrix and the IMDCT post-chain). + let final_val = if bap == 0 && dith { + dither_lfsr(&mut state.dither_lfsr_state) + } else { + val + }; + let e = state.channels[ch].exp[bin] as i32; + state.channels[ch].coeffs[bin] = final_val * 2f32.powi(-e); + if trace_this && bin < 32 { + eprintln!( + "TRACE-MANT ch{} bin={:3} bap={:2} exp={:2} bit_pos_before={} mant={:.5} dither_used={} coeff={:.5e}", + ch, + bin, + bap, + e, + bit_pos_before, + val, + bap == 0 && dith, + state.channels[ch].coeffs[bin] + ); + } + } + if state.cpl_in_use && state.channels[ch].in_coupling && !got_cplchan { + let start = state.cpl_begf_mant; + let end_c = state.cpl_endf_mant; + let cplc = MAX_FBW; + for bin in start..end_c { + let bap = state.channels[cplc].bap[bin]; + // Coupling-channel mantissas are never dithered — the + // spec explicitly says dither is applied after a channel + // is extracted from the coupling channel (§7.3.4 para 1). + let val = fetch_mantissa( + br, + bap, + &mut grp1, + &mut grp1_n, + &mut grp2, + &mut grp2_n, + &mut grp4, + &mut grp4_n, + false, + )?; + let e = state.channels[cplc].exp[bin] as i32; + state.channels[cplc].coeffs[bin] = val * 2f32.powi(-e); + } + got_cplchan = true; + } + } + if bsi.lfeon { + let lfe_ch = MAX_FBW + 1; + let dith = state.channels[lfe_ch].dithflag; + for bin in 0..7 { + let bap = state.channels[lfe_ch].bap[bin]; + let val = fetch_mantissa( + br, + bap, + &mut grp1, + &mut grp1_n, + &mut grp2, + &mut grp2_n, + &mut grp4, + &mut grp4_n, + false, + )?; + let final_val = if bap == 0 && dith { + dither_lfsr(&mut state.dither_lfsr_state) + } else { + val + }; + let e = state.channels[lfe_ch].exp[bin] as i32; + state.channels[lfe_ch].coeffs[bin] = final_val * 2f32.powi(-e); + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn fetch_mantissa( + br: &mut BitReader, + bap: u8, + g1: &mut [f32; 3], + g1n: &mut usize, + g2: &mut [f32; 3], + g2n: &mut usize, + g4: &mut [f32; 2], + g4n: &mut usize, + dithflag: bool, +) -> Result { + let _ = dithflag; + match bap { + 0 => { + // Dither replacement for bap=0 is handled by the caller + // (unpack_mantissas) after we return. Here we just signal + // "no bits consumed" by returning 0. + Ok(0.0) + } + 1 => { + if *g1n == 0 { + let code = br.read_u32(5)? as i32; + let m1 = code / 9; + let m2 = (code % 9) / 3; + let m3 = code % 3; + g1[0] = MANT_LEVEL_3[m1.clamp(0, 2) as usize]; + g1[1] = MANT_LEVEL_3[m2.clamp(0, 2) as usize]; + g1[2] = MANT_LEVEL_3[m3.clamp(0, 2) as usize]; + *g1n = 3; + } + let v = g1[3 - *g1n]; + *g1n -= 1; + Ok(v) + } + 2 => { + if *g2n == 0 { + let code = br.read_u32(7)? as i32; + let m1 = code / 25; + let m2 = (code % 25) / 5; + let m3 = code % 5; + g2[0] = MANT_LEVEL_5[m1.clamp(0, 4) as usize]; + g2[1] = MANT_LEVEL_5[m2.clamp(0, 4) as usize]; + g2[2] = MANT_LEVEL_5[m3.clamp(0, 4) as usize]; + *g2n = 3; + } + let v = g2[3 - *g2n]; + *g2n -= 1; + Ok(v) + } + 3 => { + let code = br.read_u32(3)? as usize; + Ok(MANT_LEVEL_7[code.min(6)]) + } + 4 => { + if *g4n == 0 { + let code = br.read_u32(7)? as i32; + let m1 = code / 11; + let m2 = code % 11; + g4[0] = MANT_LEVEL_11[m1.clamp(0, 10) as usize]; + g4[1] = MANT_LEVEL_11[m2.clamp(0, 10) as usize]; + *g4n = 2; + } + let v = g4[2 - *g4n]; + *g4n -= 1; + Ok(v) + } + 5 => { + let code = br.read_u32(4)? as usize; + Ok(MANT_LEVEL_15[code.min(14)]) + } + b if (6..=15).contains(&b) => { + let nbits = QUANTIZATION_BITS[b as usize] as u32; + let raw = br.read_u32(nbits)? as i32; + // Sign-extend the top bit as a signed two's-complement fraction. + let shift = 32 - nbits; + let signed = (raw << shift) >> shift; + // Normalize to (-1, 1): divide by 2^(nbits-1). + let scale = 2f32.powi(-(nbits as i32 - 1)); + Ok(signed as f32 * scale) + } + _ => Ok(0.0), + } +} + +/// Lowest transform-coefficient number of SPX sub-band `subbnd` per +/// Table E3.13. Sub-bands 0..=16 carry 12 coefficients each starting at +/// tc# 25; the entry for sub-band 17 (tc# 229) is the one-past-last +/// marker used when `spxendf == 7`. +#[inline] +fn spx_bandtable(subbnd: usize) -> usize { + 25 + 12 * subbnd +} + +/// Table E3.14 — Spectral Extension Attenuation Table `spxattentab[][]` +/// (§3.6.4.2.3). Indexed by the 5-bit `spxattencod[ch]` codeword +/// (rows 0..=31), each row holds the first 3 attenuation values of a +/// 5-tap symmetric notch filter applied at the baseband / extension +/// border (and at every wrap point during the §3.6.4.1 translation +/// copy). The 5-tap kernel is `[T[0], T[1], T[2], T[1], T[0]]` — +/// the last two taps are derived by symmetry per spec text: +/// +/// > "The first 3 attenuation values of the filter are determined by +/// > lookup into Table E3.14 with index `spxattencod[ch]`. The last +/// > two attenuation values of the filter are determined by symmetry +/// > and are not explicitly stored in the table." +#[allow(clippy::excessive_precision)] // spec text values; f32 round suffices +pub(crate) const SPX_ATTEN_TABLE: [[f32; 3]; 32] = [ + [0.954_841_604, 0.911_722_489, 0.870_550_563], + [0.911_722_489, 0.831_237_896, 0.757_858_283], + [0.870_550_563, 0.757_858_283, 0.659_753_955], + [0.831_237_896, 0.690_956_440, 0.574_349_177], + [0.793_700_526, 0.629_960_525, 0.500_000_000], + [0.757_858_283, 0.574_349_177, 0.435_275_282], + [0.723_634_619, 0.523_647_061, 0.378_929_142], + [0.690_956_440, 0.477_420_802, 0.329_876_978], + [0.659_753_955, 0.435_275_282, 0.287_174_589], + [0.629_960_525, 0.396_850_263, 0.250_000_000], + [0.601_512_518, 0.361_817_309, 0.217_637_641], + [0.574_349_177, 0.329_876_978, 0.189_464_571], + [0.548_412_490, 0.300_756_259, 0.164_938_489], + [0.523_647_061, 0.274_206_245, 0.143_587_294], + [0.500_000_000, 0.250_000_000, 0.125_000_000], + [0.477_420_802, 0.227_930_622, 0.108_818_820], + [0.455_861_244, 0.207_809_474, 0.094_732_285], + [0.435_275_282, 0.189_464_571, 0.082_469_244], + [0.415_618_948, 0.172_739_110, 0.071_793_647], + [0.396_850_263, 0.157_490_131, 0.062_500_000], + [0.378_929_142, 0.143_587_294, 0.054_409_410], + [0.361_817_309, 0.130_911_765, 0.047_366_143], + [0.345_478_220, 0.119_355_200, 0.041_234_622], + [0.329_876_978, 0.108_818_820, 0.035_896_824], + [0.314_980_262, 0.099_212_566, 0.031_250_000], + [0.300_756_259, 0.090_454_327, 0.027_204_705], + [0.287_174_589, 0.082_469_244, 0.023_683_071], + [0.274_206_245, 0.075_189_065, 0.020_617_311], + [0.261_823_531, 0.068_551_561, 0.017_948_412], + [0.250_000_000, 0.062_500_000, 0.015_625_000], + [0.238_710_401, 0.056_982_656, 0.013_602_353], + [0.227_930_622, 0.051_952_369, 0.011_841_536], +]; + +/// Apply the §3.6.4.2.3 5-tap symmetric notch filter to a 5-bin window +/// centred on the band-border bin. The kernel taps are +/// `[T[0], T[1], T[2], T[1], T[0]]` where `T = SPX_ATTEN_TABLE[code]`. +/// The window starts at `filtbin` (which the caller positions at +/// `border_bin - 2`); bins outside `0..N_COEFFS` are skipped so this is +/// safe near the array tail. +#[inline] +fn apply_spx_atten_notch(coeffs: &mut [f32; N_COEFFS], filtbin: usize, code: u8) { + let row = SPX_ATTEN_TABLE[(code & 0x1F) as usize]; + let taps = [row[0], row[1], row[2], row[1], row[0]]; + for (i, tap) in taps.iter().enumerate() { + let idx = filtbin + i; + if idx < N_COEFFS { + coeffs[idx] *= *tap; + } + } +} + +/// One step of the SPX pseudo-random noise generator (§E.3.6.4.2). The +/// spec only requires a "zero-mean, unity-variance" sequence and leaves +/// the exact generator non-normative — AC-3 / E-AC-3 are lossy and the +/// corpus is PSNR-compared, so a deterministic LFSR-derived value is +/// adequate. Returns a value in roughly `[-1, 1)` scaled to ~unit +/// variance (a sign-balanced uniform on (-√3, √3) has unit variance). +#[inline] +fn spx_noise(lfsr: &mut u32) -> f32 { + // 32-bit xorshift — long period, cheap, deterministic. + let mut x = *lfsr; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *lfsr = x; + // Map to (-1, 1) then scale to unit variance: a uniform on (-1, 1) + // has variance 1/3, so multiply by √3 ≈ 1.7320508. + let u = (x as f32 / u32::MAX as f32) * 2.0 - 1.0; + u * 1.732_050_8 +} + +/// §E.3.6.4.1 transform-coefficient translation plan, shared between +/// the decoder ([`apply_spectral_extension`]) and the encoder +/// (`eac3::spxenc` — which must predict exactly which LF bin the +/// decoder will copy into each SPX bin to compute the §3.6.4.3 +/// energy-matching coordinates). +/// +/// Returns: +/// * `map` — for each SPX-region bin `i` (absolute tc# = +/// `spx_begin_tc + i`), the LF source tc# the translation copies +/// from. The copy cursor starts at `copystart` and wraps back to it +/// per the spec's dual wrap checks: the pre-band +/// `copyindex + bandsize > copyend` test AND the per-bin +/// `copyindex == copyend` test. +/// * `wrapflag[bnd]` — true when band `bnd >= 1` wrapped (its start is +/// a copy boundary), driving the §3.6.4.2.3 border-notch re-filter +/// sites. Band 0's border site (the baseband / extension border +/// itself) is unconditional and not flagged here. +pub(crate) fn spx_translation_plan( + copystart: usize, + copyend: usize, + nbnds: usize, + bndsztab: &[usize; 18], +) -> (Vec, [bool; 18]) { + let total: usize = bndsztab[..nbnds].iter().sum(); + let mut map = Vec::with_capacity(total); + let mut wrapflag = [false; 18]; + let mut copyindex = copystart; + for bnd in 0..nbnds { + let bandsize = bndsztab[bnd]; + let mut wrapped = false; + if copyindex + bandsize > copyend { + copyindex = copystart; + wrapped = true; + } + for _ in 0..bandsize { + if copyindex == copyend { + copyindex = copystart; + wrapped = true; + } + map.push(copyindex); + copyindex += 1; + } + if bnd > 0 && wrapped { + wrapflag[bnd] = true; + } + } + (map, wrapflag) +} + +/// E-AC-3 spectral extension high-frequency regeneration (§E.3.6). +/// +/// For each fbw channel with `in_spx == true` this synthesizes the SPX +/// region `[spx_begin .. spx_end)` (transform-coefficient indices) from +/// the channel's own low-frequency coefficients: +/// +/// 1. **Translation** (§E.3.6.4.1) — copy LF coefficients from the copy +/// region `[copystart .. copyend)` (`copystart = spxbandtable[spxstrtf]`, +/// `copyend = spxbandtable[spx_begin]`) into the SPX region, +/// wrapping the copy cursor when it reaches `copyend`. +/// 2. **Banded RMS energy** (§E.3.6.4.2.2) of the translated bins. +/// 3. **Noise blending** (§E.3.6.4.2.4) — `tc = tc·sblend + noise·rms·nblend` +/// per band, using the precomputed `spx_nblend` / `spx_sblend`. +/// 4. **Coordinate scaling** (§E.3.6.4.3) — `tc *= spxco·32` per band. +/// +/// Band sizing (`spx_nbnds` / `spx_bndsztab`), the per-band coordinates +/// (`spx_coord`) and blend factors are computed during the audblk parse +/// (see `eac3::dsp`); this routine consumes that prepared state. +fn apply_spectral_extension(state: &mut Ac3State, nfchans: usize) { + if !state.spx_in_use { + return; + } + let nbnds = state.spx_nbnds; + if nbnds == 0 { + return; + } + let copystart = spx_bandtable(state.spx_strtf as usize); + let copyend = spx_bandtable(state.spx_begin_subbnd); + let spx_begin_tc = copyend; + let spx_end_tc = spx_bandtable(state.spx_end_subbnd); + if copyend <= copystart || spx_end_tc <= spx_begin_tc { + return; + } + + // 1. Transform coefficient translation plan (§E.3.6.4.1) — + // channel-independent, so compute it once. `wrapflag[bnd]` is + // true when the band-relative copy cursor had to wrap back to + // `copystart` before consuming this band's `bandsize` samples — + // i.e. the band straddles a copy boundary. The spec applies the + // §3.6.4.2.3 border notch filter at every such wrap point AND at + // the baseband / extension border itself (the bin straddling + // `spx_begin_tc`). The plan is shared with the encoder + // (`eac3::spxenc`) so both sides agree bin-for-bin. + let (copy_map, wrapflag) = spx_translation_plan(copystart, copyend, nbnds, &state.spx_bndsztab); + + for ch in 0..nfchans { + if !state.channels[ch].in_spx { + continue; + } + + // Execute the translation copy. + for (i, &src) in copy_map.iter().enumerate() { + let insertindex = spx_begin_tc + i; + if insertindex < N_COEFFS && src < N_COEFFS { + state.channels[ch].coeffs[insertindex] = state.channels[ch].coeffs[src]; + } + } + + // 1b. §3.6.4.2.3 Transform Coefficient Band Border Filtering — + // after the §3.6.4.1 translation copy AND BEFORE the + // §3.6.4.2.2 banded RMS / §3.6.4.2.4 noise scaling. The + // 5-tap symmetric notch filter sits centred on the first + // extension bin, attenuating the 2 bins below and 2 bins + // above the border (filter starts at `spx_begin_tc - 2`). + // The same filter re-applies at each band-internal wrap + // point flagged above (filter starts at the band's start + // minus 2 bins). + if state.channels[ch].spx_atten_active { + let code = state.channels[ch].spx_atten_code; + // Baseband / extension region border. + let border = spx_begin_tc; + if border >= 2 { + apply_spx_atten_notch(&mut state.channels[ch].coeffs, border - 2, code); + } + // Wrap points at band starts (bnd >= 1). + let mut band_start = spx_begin_tc; + for bnd in 0..nbnds { + if bnd > 0 && wrapflag[bnd] && band_start >= 2 { + apply_spx_atten_notch(&mut state.channels[ch].coeffs, band_start - 2, code); + } + band_start += state.spx_bndsztab[bnd]; + } + } + + // 2. Banded RMS energy of the translated coefficients + // (§E.3.6.4.2.2), 3. noise blend (§E.3.6.4.2.4), and + // 4. coordinate scaling (§E.3.6.4.3) — fused per band. + let mut spxmant = spx_begin_tc; + for bnd in 0..nbnds { + let bandsize = state.spx_bndsztab[bnd]; + let band_lo = spxmant; + let band_hi = (spxmant + bandsize).min(N_COEFFS); + + // Banded RMS. + let mut accum = 0.0f64; + for bin in band_lo..band_hi { + let v = state.channels[ch].coeffs[bin] as f64; + accum += v * v; + } + let rms = if bandsize > 0 { + (accum / bandsize as f64).sqrt() as f32 + } else { + 0.0 + }; + + let nblend = state.channels[ch].spx_nblend[bnd]; + let sblend = state.channels[ch].spx_sblend[bnd]; + let nscale = rms * nblend; + let coord = state.channels[ch].spx_coord[bnd]; + + for bin in band_lo..band_hi { + let tctemp = state.channels[ch].coeffs[bin]; + let ntemp = spx_noise(&mut state.spx_noise_lfsr); + let blended = tctemp * sblend + ntemp * nscale; + // §E.3.6.4.3 final scale by spxco·32. + state.channels[ch].coeffs[bin] = blended * coord * 32.0; + } + + spxmant += bandsize; + } + + // The SPX region is now populated; extend the channel's mantissa + // count so dynrng + IMDCT process the regenerated bins. + state.channels[ch].end_mant = state.channels[ch].end_mant.max(spx_end_tc); + } +} + +/// Apply DSP stages to the current block: decouple, rematrix, dynrng, +/// IMDCT, window + overlap-add. Populates `channels[ch].coeffs[0..256]` +/// with time-domain PCM samples ready for emission. +pub(crate) fn dsp_block(state: &mut Ac3State, _si: &SyncInfo, bsi: &Bsi) { + let nfchans = bsi.nfchans as usize; + let acmod = bsi.acmod; + + // --- Decoupling (§7.4) --- + // Enhanced coupling (§E.3.5.5) reconstructs each coupled channel's + // transform coefficients from the complex carrier `Z[k]` (the + // §E.3.5.5.4 product) *before* `dsp_block` is called, so `coeffs[bin]` + // already holds the per-channel result. The standard scalar decouple + // (`cpl_coord · cplchan`) must NOT run in that case — it would + // overwrite the carrier-derived coefficients. `skip_decouple` carries + // that gate; base AC-3 and standard E-AC-3 coupling leave it `false`. + if state.cpl_in_use && !state.skip_decouple { + let cpl_ch = MAX_FBW; + let start = state.cpl_begf_mant; + let end = state.cpl_endf_mant; + // Build subband->band lookup. The §7.4.2 coupling region spans at + // most 18 sub-bands (bins 37..253, 12 bins each), so `cpl_bndstrc` + // / `cpl_coord` / `cpl_phsflg` / `sbnd2bnd` are all sized 18. A + // malformed frame can decode `cpl_nsubbnd > 18` (e.g. an + // enhanced-coupling span of up to 22 sub-bands threaded into the + // standard decouple path); clamp the walk so it degrades to the + // valid prefix instead of indexing out of bounds. + let nsub = state.cpl_nsubbnd.min(18); + let mut sbnd2bnd = [0usize; 18]; + let mut bnd = 0usize; + for sbnd in 0..nsub { + if sbnd > 0 && !state.cpl_bndstrc[sbnd] { + bnd += 1; + } + sbnd2bnd[sbnd] = bnd; + } + for ch in 0..nfchans { + if !state.channels[ch].in_coupling { + continue; + } + for sbnd_off in 0..nsub { + let band = sbnd2bnd[sbnd_off]; + let coord = state.cpl_coord[ch][band] * 8.0; + let base = start + sbnd_off * 12; + let limit = (base + 12).min(end); + for bin in base..limit { + let mut v = state.channels[cpl_ch].coeffs[bin] * coord; + // phase flag for right channel in 2/0. + if acmod == 0x2 && ch == 1 && state.cpl_phsflg[band] { + v = -v; + } + state.channels[ch].coeffs[bin] = v; + } + } + } + } + + // --- Rematrixing (§7.5) --- + // + // Per Tables 7.25 / 7.26 / 7.27 / 7.28, the upper edge of the LAST + // rematrix band is NOT a fixed constant — it tracks the lower edge + // of the coupling region whenever coupling is in use: + // + // • cplinu == 0 : 4 bands, last ends at bin 252 (Table 7.25) + // • cplinu == 1, cplbegf > 2: 4 bands, last ends at A = 36 + 12*cplbegf + // • cplinu == 1, 2 ≥ cplbegf > 0: 3 bands, last ends at A + // • cplinu == 1, cplbegf = 0: 2 bands, last ends at bin 36 + // + // A previous formulation hard-coded the last band's high coefficient at + // bin 252 even when coupling was active. On 2/0 frames whose bitstream + // enables rematrixing AND coupling above bin 132 (cplbegf=8 in our + // transient fixture), this bled the L+R / L-R operation into the + // coupling region — bins that had just been re-derived from the + // coupling pseudo-channel via cplco coords. The downstream symptom + // was a steady PSNR drift across the 6 audblks of every burst-onset + // frame: rematrix scrambled the post-decouple coefficients, the + // IMDCT rendered the wrong waveform, and overlap-add carried the + // error into the next block. Fixing the upper edge to the spec's + // cplbegf-dependent boundary restores burst-frame PSNR. + if acmod == 0x2 { + let last_high = if state.cpl_in_use { + // A = 36 + 12 * cplbegf per Tables 7.26 / 7.27. + // For cplbegf = 0 (Table 7.28), the last band actually ends + // at bin 36 — but with only 2 rematrix bands it never reaches + // band index 3, so the value of `last_high` for index 3 is + // unused. Compute A unconditionally. + 36 + 12 * state.cpl_begf as usize + } else { + 252 // Table 7.25 fixed last band high. + }; + // Convert to exclusive upper bound for our `lo..hi` ranges. + let last_hi_excl = last_high + 1; + let remat_bands: [(usize, usize); 4] = + [(13, 25), (25, 37), (37, 61), (61, last_hi_excl.max(61))]; + let n = remat_band_count(state.cpl_in_use, state.cpl_begf); + for (i, (lo, hi)) in remat_bands.iter().take(n).enumerate() { + if !state.rematflg[i] { + continue; + } + let end_lo = state.channels[0].end_mant.min(*hi); + let end_hi = state.channels[1].end_mant.min(*hi); + let end = end_lo.min(end_hi); + for bin in *lo..end { + let l = state.channels[0].coeffs[bin]; + let r = state.channels[1].coeffs[bin]; + state.channels[0].coeffs[bin] = l + r; + state.channels[1].coeffs[bin] = l - r; + } + } + } + + // --- Spectral extension synthesis (§E.3.6) --- + // + // For channels using SPX (`in_spx`, E-AC-3 only) the coded + // coefficients stop at the SPX begin frequency; this step + // regenerates the high-frequency band [spx_begin .. spx_end) by + // copying low-frequency coefficients, blending with banded noise, + // and scaling by the per-band SPX coordinates. It runs AFTER + // decouple + rematrix (which reshape the low-frequency copy region) + // and BEFORE dynrng + IMDCT so the synthesized bins are gain-scaled + // and transformed together with the baseband. Base AC-3 never sets + // `in_spx`, so this is a no-op there. + apply_spectral_extension(state, nfchans); + + // --- Dynrng scaling --- + for ch in 0..nfchans { + let g = state.channels[ch].dynrng; + if (g - 1.0).abs() > 1e-6 { + let end = state.channels[ch].end_mant; + for bin in 0..end { + state.channels[ch].coeffs[bin] *= g; + } + } + } + + // --- IMDCT + window + overlap-add for every output channel --- + // Uses the §7.9.4 FFT-backed decomposition: pre-twiddle → N/4-point + // complex IFFT (N/8 for short blocks) → post-twiddle → de-interleave. + // Matches the direct-form reference within f32 precision on the long + // path; the short path is validated by the validator-fixture RMS gate. + let trace_frame_dsp = std::env::var("AC3_TRACE_FRAME") + .ok() + .and_then(|s| s.parse::().ok()); + let trace_blk_dsp = std::env::var("AC3_TRACE_BLK") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + for ch in 0..nfchans { + let mut coeffs = [0.0f32; 256]; + coeffs.copy_from_slice(&state.channels[ch].coeffs); + if let Some(tf) = trace_frame_dsp { + if tf == state.frame_counter && state.blkidx == trace_blk_dsp { + let max_abs = coeffs.iter().fold(0.0f32, |a, &b| a.max(b.abs())); + let nonzero = coeffs.iter().filter(|&&v| v != 0.0).count(); + eprintln!( + "TRACE-DSP ch{} pre-IMDCT max|coeff|={:.6e} nonzero_bins={} blksw={} dynrng={}", + ch, max_abs, nonzero, state.channels[ch].blksw, state.channels[ch].dynrng + ); + eprint!("TRACE-DSP ch{} coeff[0..16]: ", ch); + for v in &coeffs[..16] { + eprint!("{:.4e} ", v); + } + eprintln!(); + eprint!("TRACE-DSP ch{} coeff[16..32]: ", ch); + for v in &coeffs[16..32] { + eprint!("{:.4e} ", v); + } + eprintln!(); + } + } + let mut time = [0.0f32; 512]; + if state.channels[ch].blksw { + crate::imdct::imdct_256_pair_fft(&coeffs, &mut time); + } else { + crate::imdct::imdct_512_fft(&coeffs, &mut time); + } + // Apply window. + for n in 0..256 { + time[n] *= WINDOW[n]; + time[511 - n] *= WINDOW[n]; + } + // Overlap-add: pcm[n] = time[n] + delay[n]; delay[n] = time[256+n] + let mut out_pcm = [0.0f32; 256]; + for n in 0..256 { + // Per §7.9.4.1 overlap-add: pcm[n] = 2 * (x[n] + delay[n]). + out_pcm[n] = 2.0 * (time[n] + state.channels[ch].delay[n]); + state.channels[ch].delay[n] = time[256 + n]; + } + state.channels[ch].coeffs[..256].copy_from_slice(&out_pcm); + } + if bsi.lfeon { + let ch = MAX_FBW + 1; + let mut coeffs = [0.0f32; 256]; + coeffs.copy_from_slice(&state.channels[ch].coeffs); + let mut time = [0.0f32; 512]; + // LFE is always long-block (spec §5.4.3.3). + crate::imdct::imdct_512_fft(&coeffs, &mut time); + for n in 0..256 { + time[n] *= WINDOW[n]; + time[511 - n] *= WINDOW[n]; + } + let mut out_pcm = [0.0f32; 256]; + for n in 0..256 { + // Per §7.9.4.1 overlap-add: pcm[n] = 2 * (x[n] + delay[n]). + out_pcm[n] = 2.0 * (time[n] + state.channels[ch].delay[n]); + state.channels[ch].delay[n] = time[256 + n]; + } + state.channels[ch].coeffs[..256].copy_from_slice(&out_pcm); + } +} + +// Naive reference 512-point IMDCT (§7.9.4.1). Given N/2=256 transform +// coefficients, produces 512 time-domain samples prior to windowing. +// +// IMDCT formula: x[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos( (π/(2N)) * (2n+1+N/2) * (2k+1) ). +// +// This is the DFT-style reference implementation — not fast, but +// correct and matches the spec's prescribed output polarity / +// scaling so that window+overlap-add reproduces the original PCM. + +// --------------------------------------------------------------------- +// `imdct_256_pair`: DEPRECATED reference — NOT the canonical short-block +// IMDCT. Kept behind `cfg(test)` for regression inspection only. +// +// The forward MDCT spec at §8.2.3.2 has an α parameter that picks the +// phase offset: α=-1 for the first short transform, α=0 for the long +// transform, α=+1 for the second short transform. This function tries +// to reconstruct the per-half direct-form from that spec, with X1 using +// phase `π/(2N)·(2n+1)·(2k+1)` (no `+N/2` shift) and X2 using the +// standard `π/(2N)·(2n+1+N/2)·(2k+1)`. In practice this DOES NOT match +// the §7.9.4.2 FFT decomposition output — the two disagree with ~40% +// residual on random input (see `imdct::tests::short_block_direct_form_disagrees`). +// The FFT path is the canonical one per the spec; keep this around only +// so a future audit can bisect which side is wrong. +#[cfg(test)] +fn imdct_256_pair(x: &[f32; 256], out: &mut [f32; 512]) { + use std::f32::consts::PI; + let n: usize = 256; + let scale = -1.0f32; + let mut x1 = [0.0f32; 128]; + let mut x2 = [0.0f32; 128]; + for k in 0..128 { + x1[k] = x[2 * k]; + x2[k] = x[2 * k + 1]; + } + // First short transform: phase offset (1+α)=0 → pure cos(π/(2N)*(2n+1)*(2k+1)). + for nn in 0..n { + let mut s = 0.0f32; + for k in 0..128 { + let phase = PI / (2.0 * n as f32) * ((2 * nn + 1) as f32) * ((2 * k + 1) as f32); + s += x1[k] * phase.cos(); + } + out[nn] = scale * s; + } + // Second short transform: phase offset (1+α)=2 → standard IMDCT with +N/2. + for nn in 0..n { + let mut s = 0.0f32; + for k in 0..128 { + let phase = + PI / (2.0 * n as f32) * ((2 * nn + 1 + n / 2) as f32) * ((2 * k + 1) as f32); + s += x2[k] * phase.cos(); + } + out[256 + nn] = scale * s; + } +} + +pub fn imdct_512(x: &[f32; 256], out: &mut [f32; 512]) { + // Direct reference implementation of the 512-point IMDCT described + // in §7.9.4.1 of A/52:2018. The spec provides a fast FFT-based + // decomposition with pre/post-twiddle, but the mathematical + // definition — a sum of cosines over the 256 transform bins — + // produces identical output and is easier to audit. + // + // x[n] = sum_{k=0..N/2-1} X[k] * cos( π/(2N) * (2n+1+N/2) * (2k+1) ) + // + // The AC-3 reconstruction chain scales by `2 * (x + delay)` in the + // overlap-add step (spec pseudocode at end of §7.9.4.1), so the + // IMDCT itself needs no explicit `2/N` normalisation; pairing that + // with AC-3's windowing produces full-scale PCM for a full-scale + // transform coefficient. + use std::f32::consts::PI; + let n: usize = 512; + // The AC-3 encoder applies an explicit `-2/N` scale to the forward + // MDCT (§8.2.3.2). Our decoder undoes that via the IMDCT scale + + // the `2*(x + delay)` overlap-add (§7.9.4.1 step 6). Calibrated + // empirically against the validator binary on the 440 Hz @ 192 kbps + // fixture: peak validator-decoded 2897 int16, our output 2895 with + // `scale = -1.0`. The sign flip cancels the encoder's `-2/N` sign so + // positive-amplitude input reconstructs as positive-amplitude PCM. + let scale = -1.0f32; + for nn in 0..n { + let mut s = 0.0f32; + for k in 0..256 { + let phase = + PI / (2.0 * n as f32) * ((2 * nn + 1 + n / 2) as f32) * ((2 * k + 1) as f32); + s += x[k] * phase.cos(); + } + out[nn] = scale * s; + } +} + +#[cfg(test)] +mod short_block_tests { + use super::*; + + /// Regression / bisection fixture. The naive direct-form short-block + /// IMDCT (derived from §8.2.3.2's α=-1/+1 phase offsets) does NOT + /// match the §7.9.4.2 FFT decomposition used in production. We + /// assert the disagreement explicitly here — if a future fix makes + /// the two align, that's a signal that BOTH the direct form AND the + /// FFT path changed together, and the test can then be tightened + /// into a proper equality gate. Until then the FFT path is + /// considered canonical: its PCM output is byte-equivalent to the + /// reference S16LE produced by black-box validator-binary decode of + /// transient fixtures (cross-validated in the transient-fixture + /// integration tests). + #[test] + fn short_block_direct_form_diverges_from_fft() { + // LCG-based deterministic "random" input — no rand dependency. + let mut x = [0.0f32; 256]; + let mut s: u32 = 0x1234_5678; + for v in x.iter_mut() { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + *v = (s as i32 as f32) / (i32::MAX as f32); + } + let mut d = [0.0f32; 512]; + let mut f = [0.0f32; 512]; + imdct_256_pair(&x, &mut d); + crate::imdct::imdct_256_pair_fft(&x, &mut f); + let sse: f32 = d.iter().zip(f.iter()).map(|(a, b)| (a - b).powi(2)).sum(); + let rmse = (sse / 512.0).sqrt(); + // Currently observed: RMSE ≈ 4-5 on ~unit-magnitude random input. + // Assert the divergence exists (>0.5) so this test fails loudly if + // someone accidentally makes both paths compute the same thing. + assert!( + rmse > 0.5, + "direct form and FFT path now match (rmse={rmse:.3}) — promote short_block_direct_form_diverges_from_fft to equality" + ); + } +} + +#[cfg(test)] +mod spx_tests { + use super::*; + + /// Table E3.13: spx sub-band `s` begins at transform coefficient + /// `25 + 12·s`. Sub-band 17 is the one-past-last marker at tc# 229. + #[test] + fn spx_bandtable_matches_table_e3_13() { + assert_eq!(spx_bandtable(0), 25); + assert_eq!(spx_bandtable(1), 37); + assert_eq!(spx_bandtable(2), 49); + assert_eq!(spx_bandtable(9), 133); + assert_eq!(spx_bandtable(16), 217); + assert_eq!(spx_bandtable(17), 229); + } + + /// §E.3.6.4.1 translation plan — no-wrap case: the copy region is + /// at least as large as the SPX region, so the cursor never wraps + /// and the map is a straight run from `copystart`. + #[test] + fn spx_translation_plan_no_wrap() { + // copy region [25, 133) = 108 bins; SPX region of 2×12-bin + // bands = 24 bins — fits without wrapping. + let mut sztab = [0usize; 18]; + sztab[0] = 12; + sztab[1] = 12; + let (map, wrap) = spx_translation_plan(25, 133, 2, &sztab); + assert_eq!(map.len(), 24); + for (i, &src) in map.iter().enumerate() { + assert_eq!(src, 25 + i); + } + assert!(wrap.iter().all(|&w| !w), "no band may wrap"); + } + + /// §E.3.6.4.1 translation plan — wrap case with the spec's + /// PRE-BAND check: when the next band would run past `copyend`, + /// the cursor resets to `copystart` BEFORE the band starts (it does + /// not consume the tail of the copy region first). + #[test] + fn spx_translation_plan_pre_band_wrap() { + // Copy region [25, 43) = 18 bins; three 12-bin bands. + // Band 0: bins 25..37 (12 taken, cursor at 37). + // Band 1: 37 + 12 > 43 → pre-band wrap → bins 25..37 again. + // Band 2: 37 + 12 > 43 → pre-band wrap → bins 25..37 again. + let mut sztab = [0usize; 18]; + sztab[0] = 12; + sztab[1] = 12; + sztab[2] = 12; + let (map, wrap) = spx_translation_plan(25, 43, 3, &sztab); + assert_eq!(map.len(), 36); + assert_eq!(&map[0..12], &(25..37).collect::>()[..]); + assert_eq!(&map[12..24], &(25..37).collect::>()[..]); + assert_eq!(&map[24..36], &(25..37).collect::>()[..]); + assert!(!wrap[0], "band 0 border site is unconditional, not flagged"); + assert!(wrap[1] && wrap[2], "bands 1/2 wrap"); + } + + /// §E.3.6.4.1 translation plan — wrap case with the spec's PER-BIN + /// check: a merged (24-bin) band larger than the copy region wraps + /// mid-band via the `copyindex == copyend` test. + #[test] + fn spx_translation_plan_per_bin_wrap() { + // Copy region [25, 41) = 16 bins; one 24-bin merged band. + // Pre-band check fires (24 > 16) → start at 25; after 16 bins + // the per-bin check wraps the cursor back to 25 for the last 8. + let mut sztab = [0usize; 18]; + sztab[0] = 24; + let (map, wrap) = spx_translation_plan(25, 41, 1, &sztab); + assert_eq!(map.len(), 24); + assert_eq!(&map[0..16], &(25..41).collect::>()[..]); + assert_eq!(&map[16..24], &(25..33).collect::>()[..]); + assert!( + !wrap[0], + "band 0 wraps are not flagged (border site is unconditional)" + ); + } + + /// §E.3.3.2 `nrematbd` decision tree — every arm of the spec + /// pseudo-code. The enhanced-coupling arm (`cplinu && ecplinu`) sizes + /// from the raw `ecplbegf` code and is the one that 2/0 ecpl frames + /// reach; the rest reproduce the SPX / standard-coupling / no-coupling + /// behaviour already exercised by the parse loop. + #[test] + fn nrematbd_e3_3_2_decision_tree() { + // No coupling, no SPX → always 4. + assert_eq!(remat_band_count_spx(false, 0, false, 0, false, 0), 4); + assert_eq!(remat_band_count_spx(false, 9, true, 7, false, 0), 4); + + // SPX without coupling: 3 for spx_begin_subbnd < 4 (spxbegf < 2), + // else 4. ecpl flags are ignored when cplinu == false. + assert_eq!(remat_band_count_spx(false, 0, false, 0, true, 3), 3); + assert_eq!(remat_band_count_spx(false, 0, false, 0, true, 4), 4); + assert_eq!(remat_band_count_spx(false, 0, true, 9, true, 2), 3); + + // Standard coupling (cplinu && !ecplinu): identical to + // `remat_band_count(true, cplbegf)`. The ecplbegf argument is + // ignored on this arm. + assert_eq!(remat_band_count_spx(true, 0, false, 9, false, 0), 2); + assert_eq!(remat_band_count_spx(true, 1, false, 9, false, 0), 3); + assert_eq!(remat_band_count_spx(true, 2, false, 9, false, 0), 3); + assert_eq!(remat_band_count_spx(true, 3, false, 9, false, 0), 4); + assert_eq!(remat_band_count_spx(true, 9, false, 9, false, 0), 4); + + // Enhanced coupling (cplinu && ecplinu): thresholds the raw + // ecplbegf 0/1/2/<5/else → 0/1/2/3/4. cplbegf is ignored. + assert_eq!(remat_band_count_spx(true, 9, true, 0, false, 0), 0); + assert_eq!(remat_band_count_spx(true, 9, true, 1, false, 0), 1); + assert_eq!(remat_band_count_spx(true, 9, true, 2, false, 0), 2); + assert_eq!(remat_band_count_spx(true, 9, true, 3, false, 0), 3); + assert_eq!(remat_band_count_spx(true, 9, true, 4, false, 0), 3); + assert_eq!(remat_band_count_spx(true, 9, true, 5, false, 0), 4); + assert_eq!(remat_band_count_spx(true, 0, true, 15, false, 0), 4); + // SPX co-active with enhanced coupling: the cplinu arm wins (SPX + // only matters when coupling is off), so ecplbegf still drives it. + assert_eq!(remat_band_count_spx(true, 0, true, 0, true, 2), 0); + assert_eq!(remat_band_count_spx(true, 0, true, 5, true, 9), 4); + } + + /// §E.3.6.3 spectral-extension coordinate decode. For exponent < 15 + /// the mantissa is `(spxcomant + 4) / 8`, shifted right by + /// `spxcoexp + 3·mstrspxco`. For exponent == 15 it's `spxcomant / 4`. + /// These mirror the encoder-side math the parse in `eac3::dsp` runs; + /// duplicated here as an independent oracle. + #[test] + fn spx_coordinate_decode_formula() { + // Mirrors the §E.3.6.3 decode the parse in `eac3::dsp` runs. + fn spxco(coexp: i32, comant: i32, mstr: i32) -> f32 { + let temp = if coexp == 15 { + comant as f32 / 4.0 + } else { + (comant as f32 + 4.0) / 8.0 + }; + let shift = coexp + 3 * mstr; + temp * 2f32.powi(-shift) + } + // exp = 0, mant = 3, mstr = 0 → (3+4)/8 = 0.875, no shift. + assert!((spxco(0, 3, 0) - 0.875).abs() < 1e-6); + // exp = 2, mant = 1, mstr = 1 → (1+4)/8 = 0.625, >> (2 + 3) = 5. + assert!((spxco(2, 1, 1) - 0.625 / 32.0).abs() < 1e-7); + // exp = 15 limiting case → mant/4 (= 0.5 for mant 2), no shift + // beyond the 15 exponent. + assert!((spxco(15, 2, 0) - 2.0 / 4.0 / 2f32.powi(15)).abs() < 1e-9); + } + + /// §E.3.6.2 band sizing. With the default Table E2.11 banding and a + /// full sub-band range (begin=2, end=17) the merge bits at sub-bands + /// 8/10/12/14/16 fold pairs of 12-coefficient sub-bands into 24-wide + /// bands. We replicate the pseudo-code here against a hand-built + /// `Ac3State` and check the resulting band-size table. + #[test] + fn spx_band_sizing_default_banding() { + // begin=2, end=8 (sub-bands 2..7 active): merge bit only at 8 + // doesn't appear in range (8 == end excluded), so 6 bands of 12. + let begin = 2usize; + let end = 8usize; + let mut bndstrc = [false; 18]; + bndstrc[8] = true; // default merge bit, out of [begin+1, end) here. + let (n, sztab) = derive_bands(begin, end, &bndstrc); + assert_eq!(n, 6); + assert!(sztab[..6].iter().all(|&s| s == 12)); + + // begin=2, end=17 with default merges at 8,10,12,14,16: the + // merge bits land inside [3,17) and combine the second of each + // pair. nspxbnds = 15 sub-bands → 10 bands (5 of width 24, + // 5 of width 12) by the pseudo-code. + let mut bndstrc = [false; 18]; + for &b in &[8usize, 10, 12, 14, 16] { + bndstrc[b] = true; + } + let (n, sztab) = derive_bands(2, 17, &bndstrc); + // 15 sub-bands, 5 merges → 10 bands. + assert_eq!(n, 10); + // Total coefficients == 15 sub-bands × 12 == 180. + let total: usize = sztab[..n].iter().sum(); + assert_eq!(total, 180); + } + + // Local re-implementation of the §E.3.6.2 nspxbnds / spxbndsztab + // pseudo-code, used only by the test above. + fn derive_bands(begin: usize, end: usize, bndstrc: &[bool; 18]) -> (usize, [usize; 18]) { + let mut n = 1usize; + let mut t = [0usize; 18]; + t[0] = 12; + for bnd in (begin + 1)..end { + if !bndstrc[bnd] { + t[n] = 12; + n += 1; + } else { + t[n - 1] += 12; + } + } + (n, t) + } + + /// End-to-end synthesis check for `apply_spectral_extension`. Build a + /// channel whose low-frequency copy region carries a known ramp, set + /// up one SPX band with a pure-signal blend (sblend=1, nblend=0, + /// coord=1/32 so the ·32 scale cancels), and verify the SPX region is + /// populated with copied values (not left silent) and that `end_mant` + /// extends to the SPX end. + #[test] + fn spx_synthesis_copies_and_scales() { + let mut state = Ac3State::new(); + let ch = 0usize; + // SPX geometry: copy from sub-band 0 (tc 25), begin sub-band 2 + // (tc 49), end sub-band 4 (tc 73). One band of 24 (sub-bands + // 2+3 merged via bndstrc[3]). + state.spx_in_use = true; + state.channels[ch].in_spx = true; + state.spx_strtf = 0; // copystart = 25 + state.spx_begin_subbnd = 2; // copyend / spx_begin = 49 + state.spx_end_subbnd = 4; // spx_end = 73 + state.spx_bndstrc = [false; 18]; + state.spx_bndstrc[3] = true; // merge sub-band 3 into the band + state.spx_nbnds = 1; + state.spx_bndsztab = [0; 18]; + state.spx_bndsztab[0] = 24; + // Pure-signal blend, unity-after-·32 coord. + state.channels[ch].spx_sblend[0] = 1.0; + state.channels[ch].spx_nblend[0] = 0.0; + state.channels[ch].spx_coord[0] = 1.0 / 32.0; + state.channels[ch].end_mant = 49; // coded mantissas stop at SPX begin. + + // Fill the copy region [25, 49) with a known non-zero ramp. + for bin in 25..49 { + state.channels[ch].coeffs[bin] = (bin - 25) as f32 + 1.0; + } + // SPX region [49, 73) starts silent. + for bin in 49..73 { + state.channels[ch].coeffs[bin] = 0.0; + } + + apply_spectral_extension(&mut state, 1); + + // The SPX region must now be non-silent (copied + scaled). + let nonzero = (49..73) + .filter(|&b| state.channels[ch].coeffs[b] != 0.0) + .count(); + assert!( + nonzero >= 23, + "SPX region should be populated, got {nonzero} non-zero bins" + ); + // With sblend=1, nblend=0, coord·32=1, the first SPX bin equals + // the first copied coefficient (copyindex starts at copystart=25). + assert!( + (state.channels[ch].coeffs[49] - state.channels[ch].coeffs[25]).abs() < 1e-4, + "first SPX bin {} should equal first copy bin {}", + state.channels[ch].coeffs[49], + state.channels[ch].coeffs[25], + ); + // end_mant extends to the SPX end so dynrng + IMDCT cover it. + assert_eq!(state.channels[ch].end_mant, 73); + } + + /// `apply_spectral_extension` must be a no-op for a channel not in + /// SPX (and for base AC-3, which never sets `spx_in_use`). + #[test] + fn spx_synthesis_noop_when_disabled() { + let mut state = Ac3State::new(); + for bin in 49..73 { + state.channels[0].coeffs[bin] = 0.0; + } + state.spx_in_use = false; + apply_spectral_extension(&mut state, 1); + assert!((49..73).all(|b| state.channels[0].coeffs[b] == 0.0)); + } + + /// Spot-check three rows of Table E3.14 against the spec values. The + /// scaling rows (0, 14, 29) cover the table's value-doubling + /// progression (each ~2× step in `binindex=0` halves at the next + /// power-of-two row) so a transcription typo on any of them stands + /// out immediately. + #[test] + fn spx_atten_table_matches_spec() { + // Compare against the spec's full 9-decimal-digit values held in + // f64 (f32 literals would clip and trip `excessive_precision`). + // f32 precision is ~7 digits so we compare within 1e-6 absolute. + let check = |row: usize, col: usize, spec: f64| { + let got = SPX_ATTEN_TABLE[row][col] as f64; + assert!( + (got - spec).abs() < 1e-6, + "row {row} col {col}: got {got}, spec {spec}", + ); + }; + // Row 0. + check(0, 0, 0.954_841_604); + check(0, 1, 0.911_722_489); + check(0, 2, 0.870_550_563); + // Row 14 (half-attenuation reference: T[0]=0.5). + check(14, 0, 0.5); + check(14, 1, 0.25); + check(14, 2, 0.125); + // Row 29 (quarter-attenuation reference: T[0]=0.25). + check(29, 0, 0.25); + check(29, 1, 0.0625); + check(29, 2, 0.015_625); + // 32 rows total per the 5-bit `spxattencod[ch]` field. + assert_eq!(SPX_ATTEN_TABLE.len(), 32); + } + + /// `apply_spx_atten_notch` is the 5-tap symmetric filter + /// `[T[0], T[1], T[2], T[1], T[0]]`. Drive it on a constant-1 buffer + /// and read back the filtered bins — they must equal the kernel. + #[test] + fn spx_atten_notch_kernel_is_symmetric() { + let mut coeffs = [0.0f32; N_COEFFS]; + for v in coeffs.iter_mut().take(50) { + *v = 1.0; + } + // Apply at filtbin=10 with code=14 (T = [0.5, 0.25, 0.125]). + apply_spx_atten_notch(&mut coeffs, 10, 14); + assert!((coeffs[10] - 0.5).abs() < 1e-6); + assert!((coeffs[11] - 0.25).abs() < 1e-6); + assert!((coeffs[12] - 0.125).abs() < 1e-6); + assert!((coeffs[13] - 0.25).abs() < 1e-6); // mirror + assert!((coeffs[14] - 0.5).abs() < 1e-6); // mirror + // Outside the 5-tap window, coefficients are untouched. + assert_eq!(coeffs[9], 1.0); + assert_eq!(coeffs[15], 1.0); + } + + /// `apply_spx_atten_notch` masks the 5-bit code so a malformed + /// 6-or-7-bit value doesn't index out of bounds. + #[test] + fn spx_atten_notch_masks_code_to_5_bits() { + let mut coeffs = [1.0f32; N_COEFFS]; + // 0x3F & 0x1F == 31 → row 31. Should not panic. + apply_spx_atten_notch(&mut coeffs, 0, 0x3F); + assert!((coeffs[0] - SPX_ATTEN_TABLE[31][0]).abs() < 1e-6); + } + + /// With `spx_atten_active == true` and `spxattencod = 14` (the + /// half-attenuation row), the 5 bins centred on the baseband / + /// extension border (i.e. starting at `spx_begin_tc - 2`) must + /// be attenuated by `[0.5, 0.25, 0.125, 0.25, 0.5]` AFTER the + /// translation copy and BEFORE the noise/coord blend. We isolate + /// the filter contribution by setting blend factors to a pure-pass + /// (sblend=1, nblend=0, coord=1/32). + #[test] + fn spx_synthesis_applies_border_notch_when_chinspxatten() { + let mut state = Ac3State::new(); + let ch = 0usize; + state.spx_in_use = true; + state.channels[ch].in_spx = true; + state.spx_strtf = 0; // copystart = 25 + state.spx_begin_subbnd = 2; // copyend / spx_begin = 49 + state.spx_end_subbnd = 4; // spx_end = 73 + state.spx_bndstrc = [false; 18]; + state.spx_bndstrc[3] = true; + state.spx_nbnds = 1; + state.spx_bndsztab = [0; 18]; + state.spx_bndsztab[0] = 24; + state.channels[ch].spx_sblend[0] = 1.0; + state.channels[ch].spx_nblend[0] = 0.0; + state.channels[ch].spx_coord[0] = 1.0 / 32.0; + state.channels[ch].end_mant = 49; + // Drive the whole low-frequency region with a constant signal + // so the copy + notch is easy to read out. + for bin in 0..49 { + state.channels[ch].coeffs[bin] = 1.0; + } + // Enable the §3.6.4.2.3 notch with the half-attenuation row. + state.channels[ch].spx_atten_active = true; + state.channels[ch].spx_atten_code = 14; + + apply_spectral_extension(&mut state, 1); + + // Border is at spx_begin_tc = 49 → filter window [47, 51]. + let expected = [0.5_f32, 0.25, 0.125, 0.25, 0.5]; + for (i, exp) in expected.iter().enumerate() { + let bin = 47 + i; + // Bins 47, 48 are in the baseband (constant=1, scaled by tap). + // Bins 49, 50, 51 are in the SPX region (copied=1, then scaled + // by tap, then scaled by sblend=1 * coord=1/32 * 32 = 1). + assert!( + (state.channels[ch].coeffs[bin] - exp).abs() < 1e-4, + "border bin {bin} = {} expected {exp}", + state.channels[ch].coeffs[bin] + ); + } + // Untouched neighbour: bin 46 (below the filter window). + assert!((state.channels[ch].coeffs[46] - 1.0).abs() < 1e-6); + } + + /// With `spx_atten_active == false` the SPX synthesis is byte- + /// identical to the round-100 baseline — the border bins stay at the + /// copied value (no attenuation applied). + #[test] + fn spx_synthesis_no_atten_when_chinspxatten_off() { + let mut state = Ac3State::new(); + let ch = 0usize; + state.spx_in_use = true; + state.channels[ch].in_spx = true; + state.spx_strtf = 0; + state.spx_begin_subbnd = 2; + state.spx_end_subbnd = 4; + state.spx_bndstrc = [false; 18]; + state.spx_bndstrc[3] = true; + state.spx_nbnds = 1; + state.spx_bndsztab = [0; 18]; + state.spx_bndsztab[0] = 24; + state.channels[ch].spx_sblend[0] = 1.0; + state.channels[ch].spx_nblend[0] = 0.0; + state.channels[ch].spx_coord[0] = 1.0 / 32.0; + state.channels[ch].end_mant = 49; + for bin in 0..49 { + state.channels[ch].coeffs[bin] = 1.0; + } + state.channels[ch].spx_atten_active = false; + + apply_spectral_extension(&mut state, 1); + + // Border bins are NOT attenuated. + for bin in 47..=51 { + assert!( + (state.channels[ch].coeffs[bin] - 1.0).abs() < 1e-4, + "no-atten border bin {bin} should stay at 1.0, got {}", + state.channels[ch].coeffs[bin] + ); + } + } + + /// §3.6.4.2.3 wrap-point filtering: when band 1's copy cursor wraps + /// back to `copystart`, a second 5-tap notch must apply at the start + /// of band 1 (`band_start - 2`). Construct a geometry where the + /// copy region is smaller than one band so the second band guarantees + /// a wrap, and verify a second attenuated 5-bin window appears. + #[test] + fn spx_synthesis_applies_wrap_notch_on_band_boundary() { + let mut state = Ac3State::new(); + let ch = 0usize; + state.spx_in_use = true; + state.channels[ch].in_spx = true; + // Copy region = sub-bands 0..1 only (12 bins: [25, 37)). Two SPX + // bands of 12 each starting at sub-band 1 → spx region [37, 61). + // Each SPX band consumes 12 bins from a 12-bin copy region, so + // the second band MUST wrap. + state.spx_strtf = 0; // copystart = 25 + state.spx_begin_subbnd = 1; // copyend = 37, spx_begin = 37 + state.spx_end_subbnd = 3; // spx_end = 61 + state.spx_bndstrc = [false; 18]; + state.spx_nbnds = 2; + state.spx_bndsztab = [0; 18]; + state.spx_bndsztab[0] = 12; + state.spx_bndsztab[1] = 12; + state.channels[ch].spx_sblend[0] = 1.0; + state.channels[ch].spx_nblend[0] = 0.0; + state.channels[ch].spx_coord[0] = 1.0 / 32.0; + state.channels[ch].spx_sblend[1] = 1.0; + state.channels[ch].spx_nblend[1] = 0.0; + state.channels[ch].spx_coord[1] = 1.0 / 32.0; + state.channels[ch].end_mant = 37; + for bin in 0..49 { + state.channels[ch].coeffs[bin] = 1.0; + } + state.channels[ch].spx_atten_active = true; + state.channels[ch].spx_atten_code = 14; // [0.5, 0.25, 0.125] + + apply_spectral_extension(&mut state, 1); + + // Band 1 starts at SPX bin 49 (37 + 12). Wrap notch is centred + // on the first bin of band 1 → filter window [47, 51]. + // Border notch (always-applied) is centred on spx_begin_tc=37 + // → filter window [35, 39]. + let expected = [0.5_f32, 0.25, 0.125, 0.25, 0.5]; + for (i, exp) in expected.iter().enumerate() { + let bin = 35 + i; + assert!( + (state.channels[ch].coeffs[bin] - exp).abs() < 1e-4, + "border-notch bin {bin} expected {exp} got {}", + state.channels[ch].coeffs[bin] + ); + } + for (i, exp) in expected.iter().enumerate() { + let bin = 47 + i; + assert!( + (state.channels[ch].coeffs[bin] - exp).abs() < 1e-4, + "wrap-notch bin {bin} expected {exp} got {}", + state.channels[ch].coeffs[bin] + ); + } + // Untouched between the two notches: bin 40..46 stay at 1.0. + for bin in 40..=46 { + assert!( + (state.channels[ch].coeffs[bin] - 1.0).abs() < 1e-4, + "bin {bin} between notches should be 1.0, got {}", + state.channels[ch].coeffs[bin] + ); + } + } +} diff --git a/crates/vendor/oxideav-ac3/src/bsi.rs b/crates/vendor/oxideav-ac3/src/bsi.rs new file mode 100644 index 00000000..8f3c623e --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/bsi.rs @@ -0,0 +1,5027 @@ +//! AC-3 Bit Stream Information — `bsi()` (§5.3.2 / §5.4.2). +//! +//! The BSI immediately follows the 5-byte syncinfo and describes the +//! service characteristics: stream identification, channel layout, +//! dialogue normalization, compression, language, timecode, etc. +//! +//! This module parses the base (bsid ≤ 8) layout. Annex E (E-AC-3, +//! bsid=16) is a separate syntax not handled here — that's a future +//! `oxideav-eac3` crate. + +use oxideav_core::bits::BitReader; +use oxideav_core::{Error, Result}; + +use crate::tables::acmod_nfchans; + +/// Largest `bsid` value accepted by the base AC-3 BSI parser. Streams +/// at higher `bsid` values use the Annex E (E-AC-3) syntax — the +/// top-level decoder dispatches them to [`crate::eac3::decoder`]. +/// +/// The spec mandates muting for `bsid > 8` in pure AC-3 decoders +/// (§5.4.2.7) but accepts up to 10 as a small safety margin for +/// near-compatible streams (legacy bsid=9..=10 variants of base AC-3 +/// that still parse the same syntax). bsid 11..=16 is canonical +/// E-AC-3 territory. +pub const MAX_BSID_BASE: u8 = 10; + +/// Parsed BSI — just the fields a decoder actually needs. Optional +/// service-metadata (compression gain, language code, timecodes, +/// `addbsi`) is also surfaced for chain consumers but does not drive +/// the decoder PCM path. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bsi { + /// bsid — bit stream identification. Spec mandates decoders built + /// to A/52 mute for `bsid > 8` (Annex E E-AC-3 is a different + /// syntax). We surface the raw value and let the decoder decide. + pub bsid: u8, + pub bsmod: u8, + pub acmod: u8, + /// Number of full-bandwidth channels per Table 5.8. + pub nfchans: u8, + /// `true` when the low-frequency-effects channel is on. + pub lfeon: bool, + /// Total channel count — `nfchans + lfeon`. + pub nchans: u8, + /// Dialogue normalization, 1..=31 dB below reference. 0 is reserved + /// and spec says to treat it as 31. For a typed surface that + /// exposes the `1..=31`-dB-below-reference semantics + the §7.6 + /// reproduction-gain derivation, use + /// [`Bsi::dialogue_normalization`]. + pub dialnorm: u8, + /// §5.4.2.16 dialogue normalization for Ch2 in 1+1 dual-mono + /// streams (`acmod == 0`). `None` outside `acmod == 0`. Stored as + /// the same post-remap `1..=31` codepoint that + /// [`Bsi::dialnorm`] carries — the reserved `0` wire codepoint + /// is collapsed to `31` per §5.4.2.8 (the spec note on + /// §5.4.2.16 reads "This 5-bit code has the same meaning as + /// dialnorm"). + /// + /// For the typed surface, see + /// [`Bsi::dialogue_normalization_ch2`]. + pub dialnorm_ch2: Option, + /// Center mix-level coefficient code (cmixlev) for acmod with 3 + /// front channels; 0xFF when absent. + pub cmixlev: u8, + /// §5.4.2.4 center mix level (Table 5.9), surfaced as a typed + /// [`CenterMixLevel`]. `Some` only when the encoder emitted the + /// 2-bit codeword — i.e. the stream has 3 front channels + /// (`(acmod & 0x1) != 0 && acmod != 0x1`, equivalently `acmod ∈ {3, + /// 5, 7}`); `None` for every other channel mode where the wire + /// field is definitionally absent. Equivalent to the typed view of + /// [`Bsi::cmixlev`] where the `0xFF` "absent" sentinel becomes + /// `None`; the raw `cmixlev` field stays authoritative for + /// bit-stream round-trip and the typed surface is a thin + /// convenience over it. Lets a §7.8 downmix consumer pick the + /// per-codepoint center-channel attenuation (0.707 / 0.595 / 0.500) + /// without re-walking Table 5.9, and the spec's reserved-code + /// fallback ("the decoder should still reproduce audio. The + /// intermediate value of cmixlev (-4.5 dB) may be used in this + /// case") is exposed as + /// [`CenterMixLevel::coefficient_with_reserved_fallback`]. + pub center_mix: Option, + /// Surround mix-level coefficient code (surmixlev); 0xFF when absent. + pub surmixlev: u8, + /// §5.4.2.5 surround mix level (Table 5.10), surfaced as a typed + /// [`SurroundMixLevel`]. `Some` only when the encoder emitted the + /// 2-bit codeword — i.e. the stream has a surround channel + /// (`(acmod & 0x4) != 0`, equivalently `acmod ∈ {4, 5, 6, 7}`); + /// `None` for every other channel mode where the wire field is + /// definitionally absent. Equivalent to the typed view of + /// [`Bsi::surmixlev`] where the `0xFF` "absent" sentinel becomes + /// `None`; the raw `surmixlev` field stays authoritative for + /// bit-stream round-trip and the typed surface is a thin + /// convenience over it. Lets a §7.8 downmix consumer pick the + /// per-codepoint surround-channel attenuation (0.707 / 0.500 / 0) + /// without re-walking Table 5.10, and the spec's reserved-code + /// fallback ("the decoder should still reproduce audio. The + /// intermediate value of surmixlev (-6 dB) may be used in this + /// case") is exposed as + /// [`SurroundMixLevel::coefficient_with_reserved_fallback`]. + pub surround_mix: Option, + /// Dolby-Surround flag for 2/0 stereo streams; 0xFF when absent. + pub dsurmod: u8, + /// §5.4.2.6 Dolby Surround mode (Table 5.11), surfaced as a typed + /// [`DolbySurroundMode`]. `Some` only when `acmod == 2` (2/0 stereo + /// — the only channel layout that carries the codeword on the wire); + /// `None` otherwise. Equivalent to the typed view of [`Bsi::dsurmod`] + /// where the `0xFF` "absent" sentinel becomes `None`; the raw + /// `dsurmod` field stays public for bit-stream round-trip and the + /// typed surface is a thin convenience over it. Lets a Pro Logic-aware + /// receiver arm its matrix decoder without consulting a magic-number + /// sentinel — paired with [`Bsi::dolby_surround_mode`]. + pub dolby_surround_mode: Option, + /// Annex D §2.3 "alternate bit stream syntax" mix-level extensions. + /// `Some` only when `bsid == 6` AND the encoder set `xbsi1e == 1`; + /// `None` otherwise. The four 3-bit codewords (`ltrtcmixlev` / + /// `ltrtsurmixlev` / `lorocmixlev` / `lorosurmixlev`) refine the + /// 2-bit `cmixlev` / `surmixlev` defaults specifically for the + /// LtRt vs LoRo downmix targets — see [`crate::downmix`]. + pub annex_d_mix_levels: Option, + /// Annex D §2.3.1.2 preferred-stereo-downmix-mode (`dmixmod`); 0xFF + /// when absent. `00` = not indicated, `01` = LtRt preferred, + /// `10` = LoRo preferred, `11` = reserved. + pub dmixmod: u8, + /// Annex D §2.3.1.2 preferred stereo downmix mode (Table D2.2), + /// surfaced as a typed [`StereoDownmixPreference`]. `Some` only + /// when `bsid == 6` AND the encoder set `xbsi1e == 1`; `None` + /// otherwise (base §5.3.2 timecode syntax reuses the bit slot for + /// `timecod*e/timecod*`). Equivalent to the typed view of [`Bsi::dmixmod`] + /// where the `0xFF` "absent" sentinel becomes `None`; the raw + /// `dmixmod` field stays authoritative for bit-stream round-trip + /// and the typed surface is a thin convenience over it. Lets a + /// §3.1.1 auto-mode two-channel-downmix router pick LtRt vs + /// LoRo without consulting a magic-number sentinel. + pub dmixmod_preference: Option, + /// Heavy compression gain word (`compr`, §5.4.2.10 / §7.7.2.2). For + /// 1+1 dual-mono (`acmod == 0`) this is the Ch1 word; Ch2 is + /// surfaced separately as [`Bsi::compr_ch2`]. `Some` when + /// `compre == 1` in the bitstream; `None` when the encoder did not + /// emit a heavy-compression word for this syncframe (the spec's + /// "use `dynrng` instead for this frame" branch). + pub compr: Option, + /// Ch2 heavy compression gain word for 1+1 dual-mono only. `None` + /// outside `acmod == 0`, or inside `acmod == 0` when `compr2e == 0`. + pub compr_ch2: Option, + /// Annex D §2.3.1.8 Dolby Surround EX mode (`dsurexmod`, 2 bits, + /// Table D2.7). `Some` only when `bsid == 6` and the `xbsi2e` block + /// is present; `None` otherwise. Per the spec note the field's + /// semantics are only defined for `acmod ∈ {6, 7}` (2/2 or 3/2) — + /// the parser still surfaces the raw decoded variant for other + /// `acmod` values so a caller can decide whether to honour the + /// hint (encoders treat reserved-combination codes as advisory). + pub dsurexmod: Option, + /// Annex D §2.3.1.9 Dolby Headphone mode (`dheadphonmod`, 2 bits, + /// Table D2.8). `Some` only when `bsid == 6` and the `xbsi2e` + /// block is present; `None` otherwise. Per the spec note the + /// field's semantics are only defined for `acmod == 2` (2/0 + /// stereo); the parser still surfaces the raw decoded variant for + /// other `acmod` values. + pub dheadphonmod: Option, + /// Annex D §2.3.1.10 A/D converter type (`adconvtyp`, 1 bit, Table + /// D2.9). `Some` only when `bsid == 6` and the `xbsi2e` block is + /// present; `None` otherwise. `Standard` = generic 24-bit PCM + /// converter; `Hdcd` = HDCD-encoded source. + pub adconvtyp: Option, + /// Annex D §2.3.1.11-12 reserved-for-future-assignment + encoder- + /// private trailer of the `xbsi2` block. `Some` only when `bsid == + /// 6` AND the encoder set `xbsi2e == 1`; `None` otherwise (the §5.3.2 + /// base syntax reuses the bit slot for `timecod2e/timecod2` and the + /// trailer is definitionally absent). See [`ExtraBsi2`] — the + /// surface exposes the raw 8-bit `xbsi2` codepoint, the 1-bit + /// `encinfo` flag, and an `is_spec_reserved_value()` predicate that + /// flags non-conformant `xbsi2 != 0x00` encoder output. + pub extra_bsi: Option, + /// §5.4.2.11-12 deprecated 8-bit `langcod` slot, surfaced as a + /// typed [`LanguageCode`]. `Some` only when the encoder set + /// `langcode == 1` in the bitstream; `None` when `langcode == 0` + /// (no `langcod` word follows). Per §5.4.2.12 the slot is an + /// "8 bit reserved value that shall be set to `0xFF` if present" + /// — the original 1995 mapping to a table-lookup language id was + /// removed in 2001, and modern delivery systems carry the ISO + /// 639-2 language code in the signaling layer instead. Surfacing + /// the raw byte plus the spec-mandated `is_spec_reserved_value()` + /// predicate lets a probe / archive tool flag legacy streams that + /// still carry a non-`0xFF` deprecated value without re-parsing + /// the BSI. The decoder PCM path is unchanged — the word does not + /// affect audio reproduction. + pub language_code: Option, + /// §5.4.2.19-20 Ch2 deprecated `langcod2` slot, surfaced as a + /// typed [`LanguageCode`]. `Some` only when `acmod == 0` (1+1 + /// dual-mono) AND the encoder set `langcod2e == 1`; `None` + /// otherwise. Same semantics as [`Self::language_code`] but + /// routed to the Ch2 reproduction chain — the §5.4.2.20 spec + /// note reads "See lancod, Section 5.4.2.12 above" so the + /// wire-conformance check is identical. + pub language_code_ch2: Option, + /// §5.4.2.13-15 audio production information for the main channel + /// (Ch1 in a 1+1 dual-mono stream). `Some` only when `audprodie == + /// 1` in the bitstream; `None` otherwise. Carries the `mixlevel` + /// (peak mixing-session SPL hint per §5.4.2.14) and the `roomtyp` + /// (mixing-room calibration per §5.4.2.15 / Table 5.12). The base + /// AC-3 decoder does not act on these fields ("not typically used + /// within the AC-3 decoder, but may be used by other parts of the + /// audio reproduction equipment") — surfacing them lets a chain + /// consumer route the hint without re-parsing the BSI. + pub audio_production: Option, + /// §5.4.2.21-23 audio production information for Ch2 in a 1+1 + /// dual-mono stream (`acmod == 0` AND `audprodi2e == 1`). `None` + /// outside 1+1 mode or when `audprodi2e == 0`. Same semantics as + /// [`Bsi::audio_production`] but routed to the Ch2 reproduction + /// chain. + pub audio_production_ch2: Option, + /// §5.4.2.27 low-resolution timecode half. `Some` only when the + /// base syntax is in use (`bsid != 6` — the alternate Annex D + /// syntax reuses these wire bits for the `xbsi1` block) AND the + /// encoder set `timecod1e == 1` in the bitstream. Covers hours + + /// minutes + 8-second increments per §5.4.2.27; combine with + /// [`Self::timecod2`] for a full ~521 µs-resolution offset. + /// + /// Per Annex D §1 / §3.2 the timecode "does not affect the + /// decoding process in legacy decoders"; surfacing it lets a chain + /// consumer recover a playback offset for editorial workflows that + /// pre-date out-of-band timecode. + pub timecod1: Option, + /// §5.4.2.28 high-resolution timecode half. `Some` only when the + /// base syntax is in use (`bsid != 6`) AND the encoder set + /// `timecod2e == 1`. Covers residual seconds + frames + + /// fractional-frames per §5.4.2.28; can stand alone (sync to + /// out-of-band wall-clock) or pair with [`Self::timecod1`] for the + /// full 28-bit code. + pub timecod2: Option, + /// §5.4.2.26 Table 5.13 presence pattern. Always present — + /// [`TimeCodePresence::NotPresent`] when both flags are clear (or + /// when the alternate Annex D syntax is in use, in which case the + /// `timecod*e` slots carry `xbsi*e` instead and the timecode is + /// definitionally absent). + pub timecode_presence: TimeCodePresence, + /// §5.4.2.24-25 distribution-control hint pair (`copyrightb` + + /// `origbs`). Always present — every base AC-3 syncframe carries + /// both 1-bit fields unconditionally per the BSI bit layout + /// (`bit_stream_info()` syntax in §5.3.2). The decoder PCM path + /// does not consult these bits; surfacing them lets a chain + /// consumer enforce a distribution / archive policy without + /// re-parsing the BSI. + pub copyright_info: CopyrightInfo, + /// §5.4.2.29-31 additional bit-stream information payload. `Some` + /// when the encoder set `addbsie == 1`; `None` when `addbsie == 0`. + /// The decoder per §5.4.2.30 "is not required to interpret this + /// information, and thus shall skip over this number of bytes" — + /// surfacing the payload bytes lets a chain consumer recover an + /// encoder-private metadata block (Dolby reserved-payload routing, + /// OAMD packetisation, encoder watermark) without re-parsing the + /// BSI. See [`AdditionalBitStreamInfo`]. + pub addbsi: Option, + /// Absolute bit position (in bits, measured from the first byte of + /// `bsi()` input) where the BSI ended. Callers use this to skip + /// straight to the audio-block area. + pub bits_consumed: u64, +} + +impl Bsi { + /// Decode the raw `bsmod` value into a typed [`BitStreamMode`] + /// per Table 5.7. `bsmod == 0b111` is overloaded by `acmod` and + /// returns either [`BitStreamMode::VoiceOver`] (acmod=0b001) or + /// [`BitStreamMode::Karaoke`] (acmod ∈ {0b010..=0b111}); the + /// `bsmod==0b111 && acmod==0b000` combination is not defined by + /// the spec and maps to [`BitStreamMode::Reserved`]. + /// + /// This is a thin convenience over [`Bsi::bsmod`] + [`Bsi::acmod`] + /// — the raw fields stay authoritative and an unmatched value + /// never panics here. A player can use the typed result to drive + /// service-routing (e.g. mute the dialogue-only `Dialogue` track + /// when also playing a main service, or surface the + /// `VisuallyImpaired` track to a screen-reader bus). + pub fn service_type(&self) -> BitStreamMode { + BitStreamMode::from_bsmod_acmod(self.bsmod, self.acmod) + } + + /// Typed view over [`Bsi::dialnorm`] per §5.4.2.8. The wrapper + /// exposes both `db()` (signed, in `-31..=-1` dB) and + /// `reproduction_gain_linear()` (the §7.6 playback-gain + /// derivation) so a reproduction system can apply the dialnorm + /// without re-parsing the BSI. + /// + /// Because [`Bsi::dialnorm`] has already been remapped (the + /// reserved `0` codepoint becomes `31`), the returned + /// [`DialNorm::is_reserved_wire_codepoint`] always reports + /// `false` on the value built from this accessor — callers who + /// need to detect the reserved-wire-code path should consult the + /// raw [`Bsi::dialnorm`] value directly. + pub fn dialogue_normalization(&self) -> DialNorm { + DialNorm::from_wire(self.dialnorm) + } + + /// Typed view over [`Bsi::dialnorm_ch2`] per §5.4.2.16 — the + /// Ch2 mirror in 1+1 dual-mono streams. `None` outside + /// `acmod == 0`. + pub fn dialogue_normalization_ch2(&self) -> Option { + self.dialnorm_ch2.map(DialNorm::from_wire) + } + + /// Typed view over [`Bsi::dmixmod_preference`] — the Annex D + /// §2.3.1.2 preferred stereo downmix mode. `Some` only when + /// `bsid == 6` and the encoder set `xbsi1e == 1`; `None` + /// otherwise. A §3.1.1 auto-mode two-channel-downmix router + /// should consult this hint to pick LtRt vs LoRo and fall back + /// to the §7.8 LoRo defaults when this returns `None` (or when + /// the hint reports + /// [`StereoDownmixPreference::is_not_indicated`]). + pub fn stereo_downmix_preference(&self) -> Option { + self.dmixmod_preference + } + + /// Typed view over [`Bsi::dolby_surround_mode`] — the §5.4.2.6 + /// base-syntax Dolby Surround mode (Table 5.11). `Some` only when + /// `acmod == 2` (2/0 stereo); `None` otherwise. A Pro Logic-aware + /// receiver can consult this hint to arm its matrix decoder for a + /// program that was matrix-encoded for surround-from-stereo + /// recovery. The decoder PCM path is unchanged — per §5.4.2.6 the + /// field "is not used by the AC-3 decoder, but may be used by + /// other portions of the audio reproduction equipment". + pub fn dolby_surround_mode(&self) -> Option { + self.dolby_surround_mode + } + + /// Typed view over [`Bsi::center_mix`] — the §5.4.2.4 center mix + /// level (Table 5.9). `Some` only when the stream has 3 front + /// channels (`acmod ∈ {3, 5, 7}`); `None` otherwise. A §7.8 + /// downmix consumer can use the returned + /// [`CenterMixLevel::coefficient_with_reserved_fallback`] to pick + /// the per-codepoint center-channel attenuation without re-walking + /// Table 5.9 or consulting the raw [`Bsi::cmixlev`] sentinel. + pub fn center_mix(&self) -> Option { + self.center_mix + } + + /// Typed view over [`Bsi::surround_mix`] — the §5.4.2.5 surround + /// mix level (Table 5.10). `Some` only when the stream has a + /// surround channel (`acmod ∈ {4, 5, 6, 7}`); `None` otherwise. A + /// §7.8 downmix consumer can use the returned + /// [`SurroundMixLevel::coefficient_with_reserved_fallback`] to pick + /// the per-codepoint surround-channel attenuation without + /// re-walking Table 5.10 or consulting the raw [`Bsi::surmixlev`] + /// sentinel. + pub fn surround_mix(&self) -> Option { + self.surround_mix + } + + /// Typed view over [`Bsi::language_code`] — the §5.4.2.11-12 + /// deprecated 8-bit `langcod` slot. `Some` only when the encoder + /// set `langcode == 1` in the bitstream; `None` when + /// `langcode == 0` (no `langcod` word follows). Per §5.4.2.12 the + /// slot is an "8 bit reserved value that shall be set to `0xFF` + /// if present" — use [`LanguageCode::is_spec_reserved_value`] to + /// flag legacy streams that still carry a non-`0xFF` value. + pub fn language_code(&self) -> Option { + self.language_code + } + + /// Typed view over [`Bsi::language_code_ch2`] — the §5.4.2.19-20 + /// Ch2 `langcod2` slot in 1+1 dual-mono streams. `Some` only when + /// `acmod == 0` AND `langcod2e == 1`; `None` otherwise. Same + /// per-§5.4.2.20 semantics as [`Self::language_code`]. + pub fn language_code_ch2(&self) -> Option { + self.language_code_ch2 + } +} + +/// Service-type classification of an AC-3 bit stream — Table 5.7 +/// "Bit Stream Mode". The encoding is keyed on `bsmod`; the `'111'` +/// codepoint is overloaded and resolves with `acmod`'s help. +/// +/// Spec §5.4.2.2: `bsmod` indicates whether the bit stream carries a +/// main audio service (CM, ME, karaoke), an associated service +/// (VI, HI, D, C, E, VO), or — for the unused `bsmod==0b111` +/// /`acmod==0b000` combination — nothing defined. +/// +/// Routing recommendations (from §5.4.2.2 and Table 5.7): +/// +/// * **Main** services (`CompleteMain` / `MusicAndEffects` / `Karaoke`): +/// the primary playback target. A receiver normally selects exactly +/// one main service at a time. +/// * **Associated** services may be mixed *on top of* a main service +/// (e.g. `VisuallyImpaired` and `HearingImpaired` are descriptive +/// narration / cleaned-dialogue mixes intended to substitute or +/// augment the main mix; `Commentary` / `Emergency` / `VoiceOver` +/// typically mix on top of a separate main). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BitStreamMode { + /// `bsmod=0b000` — main audio service: complete main (CM). + CompleteMain, + /// `bsmod=0b001` — main audio service: music and effects (ME). + MusicAndEffects, + /// `bsmod=0b010` — associated service: visually impaired (VI). + VisuallyImpaired, + /// `bsmod=0b011` — associated service: hearing impaired (HI). + HearingImpaired, + /// `bsmod=0b100` — associated service: dialogue (D). + Dialogue, + /// `bsmod=0b101` — associated service: commentary (C). + Commentary, + /// `bsmod=0b110` — associated service: emergency (E). + Emergency, + /// `bsmod=0b111` + `acmod=0b001` (mono) — associated service: + /// voice over (VO). + VoiceOver, + /// `bsmod=0b111` + `acmod ∈ {0b010..=0b111}` — main audio + /// service: karaoke. + Karaoke, + /// `bsmod=0b111` + `acmod=0b000` — undefined by Table 5.7 + /// (`bsmod==0b111` collides with the 1+1 dual-mono `acmod`). + /// Decoders should treat this as malformed metadata, not error. + Reserved, +} + +impl BitStreamMode { + /// Resolve a `(bsmod, acmod)` pair into a typed service-type per + /// Table 5.7. Only the low 3 bits of each input are consulted. + pub fn from_bsmod_acmod(bsmod: u8, acmod: u8) -> Self { + match bsmod & 0x7 { + 0b000 => BitStreamMode::CompleteMain, + 0b001 => BitStreamMode::MusicAndEffects, + 0b010 => BitStreamMode::VisuallyImpaired, + 0b011 => BitStreamMode::HearingImpaired, + 0b100 => BitStreamMode::Dialogue, + 0b101 => BitStreamMode::Commentary, + 0b110 => BitStreamMode::Emergency, + 0b111 => match acmod & 0x7 { + 0b000 => BitStreamMode::Reserved, + 0b001 => BitStreamMode::VoiceOver, + _ => BitStreamMode::Karaoke, + }, + _ => unreachable!(), + } + } + + /// `true` for a main audio service (CM, ME, or karaoke). A + /// receiver picking a default playback target should normally + /// route a main service first. + pub fn is_main(self) -> bool { + matches!( + self, + BitStreamMode::CompleteMain | BitStreamMode::MusicAndEffects | BitStreamMode::Karaoke + ) + } + + /// `true` for an associated service (VI / HI / D / C / E / VO). + /// These are typically mixed on top of a separately-decoded main + /// service. + pub fn is_associated(self) -> bool { + matches!( + self, + BitStreamMode::VisuallyImpaired + | BitStreamMode::HearingImpaired + | BitStreamMode::Dialogue + | BitStreamMode::Commentary + | BitStreamMode::Emergency + | BitStreamMode::VoiceOver + ) + } + + /// Short ASCII mnemonic per Table 5.7 (e.g. "CM", "ME", "VI", + /// "HI", "D", "C", "E", "VO", "K"). Stable for UI / logging. + /// Returns "?" for [`BitStreamMode::Reserved`]. + pub fn mnemonic(self) -> &'static str { + match self { + BitStreamMode::CompleteMain => "CM", + BitStreamMode::MusicAndEffects => "ME", + BitStreamMode::VisuallyImpaired => "VI", + BitStreamMode::HearingImpaired => "HI", + BitStreamMode::Dialogue => "D", + BitStreamMode::Commentary => "C", + BitStreamMode::Emergency => "E", + BitStreamMode::VoiceOver => "VO", + BitStreamMode::Karaoke => "K", + BitStreamMode::Reserved => "?", + } + } +} + +/// §5.4.2.8 dialogue normalization word — the 5-bit `dialnorm` +/// codepoint, lifted into a typed surface. +/// +/// Per spec the 5-bit value indicates "how far the average dialogue +/// level is below digital 100 percent": valid codepoints `1..=31` +/// map to `-1 dB`..=`-31 dB`. The `0` codepoint is reserved; a +/// spec-compliant decoder treats it as `31` (the `-31 dB` floor). +/// +/// Per §7.6 the `dialnorm` value is **not** consumed inside the +/// AC-3 decoder itself — it is forwarded to the reproduction +/// system's volume controller, which combines it with the +/// listener's chosen playback SPL. With `dialnorm` advertised, a +/// system volume control calibrated in dB SPL stays consistent +/// across programs of different mixing loudness (the spec example +/// describes a listener set to 67 dB SPL receiving a -25 dB +/// program then a -15 dB commercial — the system gain +/// auto-adjusts so the dialogue stays at 67 dB SPL across the +/// boundary). The §7.6 prose closes with "It is mandatory that +/// the dialnorm value and the user selected volume setting both +/// be used to set the reproduction system gain." +/// +/// `oxideav-ac3`'s decoder PCM path does not apply the value +/// (the field is forwarded raw on [`crate::bsi::Bsi::dialnorm`]) +/// — surfacing the typed value lets a downstream volume +/// controller carry out the §7.6 normalisation without +/// re-parsing the BSI. +/// +/// For 1+1 dual-mono streams (`acmod == 0`) the bitstream carries +/// a second copy of the word for Ch2; see +/// [`crate::bsi::Bsi::dialnorm_ch2`] / [`Bsi::dialogue_normalization_ch2`] +/// for that surface. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DialNorm { + /// Stored as the post-remap codepoint in `1..=31`. The `0` + /// wire codepoint is collapsed to `31` per §5.4.2.8; the + /// `wire_value` accessor recovers the on-the-wire byte if + /// needed. + raw: u8, + /// Records whether the on-the-wire codepoint was the reserved + /// `0` value (which the parser remaps to `31`). Lets a + /// careful consumer distinguish "encoder emitted the + /// reserved code" from a legitimate `31` codepoint. + was_reserved: bool, +} + +impl DialNorm { + /// Wrap a 5-bit wire codepoint. The reserved `0` codepoint is + /// remapped to `31` per §5.4.2.8; the original wire value is + /// preserved for [`Self::wire_value`] and + /// [`Self::is_reserved_wire_codepoint`]. + /// + /// Only the low 5 bits of `wire` are consulted. + pub fn from_wire(wire: u8) -> Self { + let masked = wire & 0x1F; + if masked == 0 { + Self { + raw: 31, + was_reserved: true, + } + } else { + Self { + raw: masked, + was_reserved: false, + } + } + } + + /// Post-remap codepoint in `1..=31`. This is the value the + /// reproduction system should use for §7.6 normalisation — + /// the reserved `0` wire codepoint has already been collapsed + /// to `31`. + pub fn codepoint(self) -> u8 { + self.raw + } + + /// On-the-wire 5-bit codepoint as it appeared in the + /// bitstream (`0..=31`). Recovers `0` for the reserved code; + /// callers re-emitting the BSI byte-for-byte should use this + /// rather than [`Self::codepoint`]. + pub fn wire_value(self) -> u8 { + if self.was_reserved { + 0 + } else { + self.raw + } + } + + /// `true` when the on-the-wire codepoint was the reserved + /// `0` value (which the parser remapped to `31` per + /// §5.4.2.8). Use to flag malformed encoders without + /// rejecting the stream — per the spec text "If the reserved + /// value of 0 is received, the decoder shall use -31 dB." + pub fn is_reserved_wire_codepoint(self) -> bool { + self.was_reserved + } + + /// Dialogue level below digital 100 percent, in dB. Returns + /// a negative integer in `-31..=-1` per §5.4.2.8 (the spec's + /// "interpreted as -1 dB to -31 dB" wording — codepoint `N` + /// maps to `-N dB`). + pub fn db(self) -> i8 { + -(self.raw as i8) + } + + /// Magnitude of the dialogue level below full scale, in dB + /// (`1..=31`). Equivalent to `-self.db()` — kept as a + /// separate accessor since the §7.6 prose phrases the value + /// both ways ("headroom in dB above the subjective dialogue + /// level" / "how many dB the subjective dialogue level is + /// below digital 100 percent"). + pub fn level_below_full_scale_db(self) -> u8 { + self.raw + } + + /// Linear-domain attenuation factor — multiply a full-scale + /// digital signal by this to land at the dialogue reference + /// level. Equivalent to `10^(dialnorm.db() / 20.0)`. Range: + /// `10^(-31/20) ≈ 0.0282` at codepoint 31 (the `-31 dB` + /// floor) up to `10^(-1/20) ≈ 0.891` at codepoint 1 + /// (the `-1 dB` ceiling). + /// + /// This is the *reverse* of the gain the reproduction system + /// applies — the spec mandates the system *boost* the signal + /// by `(listener_target_db + dialnorm.db()) dB`, not attenuate + /// it by `-dialnorm.db() dB`. Use + /// [`Self::reproduction_gain_linear`] for the playback gain + /// derivation. + pub fn attenuation_linear(self) -> f32 { + 10.0f32.powf(self.db() as f32 / 20.0) + } + + /// Linear playback gain to bring dialogue at the encoded + /// level to the listener's target dialogue SPL — per §7.6 + /// "reproduction system gain becomes a function of both the + /// listeners desired reproduction sound pressure level for + /// dialogue, and the dialnorm value". + /// + /// Given a listener target dialogue level (in dB SPL) and an + /// assumed full-scale reproduction SPL (`reference_full_scale_db`), + /// the playback gain in dB is + /// `listener_target_db - (reference_full_scale_db + self.db())` + /// — equivalent to + /// `listener_target_db - reference_full_scale_db + level_below_full_scale_db`. + /// Returned as a linear multiplier. + /// + /// Example (from §7.6): `listener_target_db = 67`, + /// `reference_full_scale_db = 105` (typical cinema + /// calibration), `level_below_full_scale_db = 25` → + /// `67 - 105 + 25 = -13 dB` of attenuation from full scale, + /// matching the spec example's "full scale digital signals + /// reproduce at a sound pressure level of 92 dB". + pub fn reproduction_gain_linear( + self, + listener_target_db: f32, + reference_full_scale_db: f32, + ) -> f32 { + let gain_db = + listener_target_db - reference_full_scale_db + self.level_below_full_scale_db() as f32; + 10.0f32.powf(gain_db / 20.0) + } +} + +/// Heavy compression gain word per Table 7.30 + §7.7.2.2. +/// +/// The wire field is 8 bits, split as `X0 X1 X2 X3 . Y4 Y5 Y6 Y7`: +/// +/// * The upper nibble `X` is a 4-bit signed integer in the range +/// `-8..=+7` (transmitted MSB-first). It contributes a gain of +/// `(X + 1) * 6.02 dB` — i.e. an arithmetic shift on the PCM +/// sample. The 16 `X` codepoints span `+48.16 dB` (`X=7`) down to +/// `-42.14 dB` (`X=-8`). +/// * The lower nibble `Y` is an unsigned fractional value with an +/// implicit leading `1`, read as `0.1 Y4 Y5 Y6 Y7` in base 2 — i.e. +/// `(16 + Y) / 32`, ranging from `16/32 = 0.5` to `31/32`. It +/// represents a linear *attenuation* between `0` dB and `-6.02` dB. +/// +/// The combined linear gain is `linear = 2^(X+1) * (16 + Y) / 32`; +/// the combined dB gain runs from `-48.16 dB` (`X=-8`, `Y=0`, +/// linear `0.5 * 0.5 = 0.25`) up to `+47.89 dB` (`X=7`, `Y=15`, +/// linear `256 * 31/32`). +/// +/// Per §7.7.2 the `compr` element is intended to bound the **peak** +/// playback level for downstream feeds with restricted dynamic range +/// (RF modulators, hotel-room feeds, etc.). Decoders that have been +/// instructed to "compress on" SHOULD apply `compr` when present, and +/// fall back to `dynrng` for syncframes that omit it (§7.7.2.1). +/// `oxideav-ac3`'s current PCM path does neither — both `compr` and +/// `dynrng` are left for the application to apply downstream — but +/// surfacing the typed value here lets a player implement the policy +/// without re-parsing the BSI. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CompressionGain { + raw: u8, +} + +impl CompressionGain { + /// Wrap the 8-bit wire value verbatim. Every byte pattern is valid + /// per Table 7.30 (all 256 codepoints map to a defined gain). + pub fn from_byte(raw: u8) -> Self { + Self { raw } + } + + /// Underlying 8-bit wire value — `X0 X1 X2 X3 Y4 Y5 Y6 Y7` packed + /// MSB-first. + pub fn raw(self) -> u8 { + self.raw + } + + /// Signed `X` field, in `-8..=+7`. Per the §7.7.2.2 description + /// the four upper bits encode `X` as a 4-bit signed integer + /// (two's-complement convention: `0b1111 → -1`, `0b1000 → -8`). + pub fn x(self) -> i8 { + let x4 = (self.raw >> 4) & 0xF; + // Sign-extend the 4-bit field. + if x4 & 0x8 != 0 { + (x4 as i16 - 16) as i8 + } else { + x4 as i8 + } + } + + /// Unsigned `Y` field, in `0..=15`. Combined with the implicit + /// leading `1`, it represents `(16 + Y) / 32` per §7.7.2.2. + pub fn y(self) -> u8 { + self.raw & 0xF + } + + /// Linear-domain gain coefficient — multiply the decoded PCM by + /// this scalar. Equals `2^(X+1) * (16 + Y) / 32`. + pub fn linear(self) -> f32 { + let x_shift = (self.x() as i32) + 1; // -7..=+8 + let y_frac = (16.0 + self.y() as f32) / 32.0; // 0.5..=31/32 + // 2^x_shift via direct floating multiply: x_shift fits in i32 well + // within f32 exponent range (-7..=+8). + let two_pow = 2.0f32.powi(x_shift); + two_pow * y_frac + } + + /// dB-domain gain — `20 * log10(linear())`. Range + /// `-48.16 dB ..= +47.89 dB` per Table 7.30 + §7.7.2.2. + pub fn decibels(self) -> f32 { + 20.0 * self.linear().log10() + } +} + +/// Annex D §2.3.1.8 Dolby Surround EX mode (Table D2.7). +/// +/// Surfaced on [`Bsi::dsurexmod`] when `bsid == 6` and the `xbsi2e` +/// block is present. The spec note constrains the meaningful range of +/// the field to `acmod ∈ {6, 7}` (2/2 and 3/2 — the only layouts that +/// carry a stereo surround pair); for other `acmod` values the field +/// is "reserved" but encoders still emit one of the four codepoints, +/// so the parser surfaces the raw decoded variant and leaves the +/// caller to honour the spec gating. +/// +/// "Dolby Pro Logic IIx" is a back-compatible matrix decoder that +/// recovers a 5.1 or 6.1/7.1 program from a Dolby Surround EX-encoded +/// stream; "Dolby Pro Logic IIz" is the matrix variant that recovers a +/// front-height pair. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DolbySurroundExMode { + /// `'00'` — encoding not indicated. + NotIndicated, + /// `'01'` — explicitly NOT Dolby Surround EX, Pro Logic IIx, or + /// Pro Logic IIz encoded. + NotEncoded, + /// `'10'` — Dolby Surround EX or Pro Logic IIx encoded. + SurroundExOrProLogicIIx, + /// `'11'` — Dolby Pro Logic IIz encoded. + ProLogicIIz, +} + +impl DolbySurroundExMode { + /// Decode the 2-bit wire value verbatim per Table D2.7. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => DolbySurroundExMode::NotIndicated, + 1 => DolbySurroundExMode::NotEncoded, + 2 => DolbySurroundExMode::SurroundExOrProLogicIIx, + _ => DolbySurroundExMode::ProLogicIIz, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + DolbySurroundExMode::NotIndicated => 0, + DolbySurroundExMode::NotEncoded => 1, + DolbySurroundExMode::SurroundExOrProLogicIIx => 2, + DolbySurroundExMode::ProLogicIIz => 3, + } + } +} + +/// Annex D §2.3.1.9 Dolby Headphone mode (Table D2.8). +/// +/// Surfaced on [`Bsi::dheadphonmod`] when `bsid == 6` and the `xbsi2e` +/// block is present. The spec note constrains the meaningful range of +/// the field to `acmod == 2` (2/0 stereo); for other `acmod` values +/// the field is "reserved" but the parser still surfaces the raw +/// decoded variant. +/// +/// The `'11'` reserved codepoint is mapped to [`DolbyHeadphoneMode::Reserved`]; +/// per the spec a decoder receiving the reserved code "should still +/// reproduce audio" and is encouraged to treat it as `NotIndicated`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DolbyHeadphoneMode { + /// `'00'` — encoding not indicated. + NotIndicated, + /// `'01'` — explicitly NOT Dolby Headphone encoded. + NotEncoded, + /// `'10'` — Dolby Headphone encoded. + Encoded, + /// `'11'` — reserved (treat as [`NotIndicated`](Self::NotIndicated) + /// per §2.3.1.9; the decoder must still reproduce audio). + Reserved, +} + +impl DolbyHeadphoneMode { + /// Decode the 2-bit wire value verbatim per Table D2.8. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => DolbyHeadphoneMode::NotIndicated, + 1 => DolbyHeadphoneMode::NotEncoded, + 2 => DolbyHeadphoneMode::Encoded, + _ => DolbyHeadphoneMode::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + DolbyHeadphoneMode::NotIndicated => 0, + DolbyHeadphoneMode::NotEncoded => 1, + DolbyHeadphoneMode::Encoded => 2, + DolbyHeadphoneMode::Reserved => 3, + } + } +} + +/// Base-syntax §5.4.2.6 Dolby Surround mode (Table 5.11). A 2-bit +/// advisory carried in 2/0 stereo streams (`acmod == 2`) that flags +/// whether the program was matrix-encoded for a Dolby Surround +/// (Pro Logic-decodable) playback path. +/// +/// Surfaced on [`Bsi::dolby_surround_mode`] when `acmod == 2`; the +/// parser returns `None` for every other channel mode because the +/// `dsurmod` slot is not on the wire there (§5.3.2 only emits the +/// 2-bit codeword inside the `acmod == 0x2` guard). The Annex E +/// informational-metadata block (§E.2.3.1.x) carries the same field +/// under the same `acmod == 2` guard, so the Annex E `Bsi` reuses +/// this enum verbatim — single source of truth for both syntaxes. +/// +/// Per §5.4.2.6: "This information is not used by the AC-3 decoder, +/// but may be used by other portions of the audio reproduction +/// equipment. If `dsurmod` is set to the reserved code, the decoder +/// should still reproduce audio. The reserved code may be +/// interpreted as 'not indicated'." A receiver that has a Pro Logic +/// matrix decoder available can pre-arm it when this surface reports +/// [`DolbySurroundMode::Encoded`]. +/// +/// This is the base-syntax cousin of the Annex D §2.3.1.8 +/// [`DolbySurroundExMode`] (which extends the same advisory pattern +/// to 6/7-channel `acmod` layouts for the EX / Pro Logic IIx / IIz +/// matrix variants). The two surfaces are independent — a 2/0 stream +/// can only carry `dsurmod`, a 5.1 stream can only carry `dsurexmod`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DolbySurroundMode { + /// `'00'` — encoding not indicated. + NotIndicated, + /// `'01'` — explicitly NOT Dolby Surround encoded. + NotEncoded, + /// `'10'` — Dolby Surround encoded (matrix-encoded for a + /// Pro Logic-decodable playback path). + Encoded, + /// `'11'` — reserved (per §5.4.2.6 the decoder must keep + /// reproducing audio and may interpret this codepoint as + /// [`NotIndicated`](Self::NotIndicated)). + Reserved, +} + +impl DolbySurroundMode { + /// Decode the 2-bit wire value verbatim per Table 5.11. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => DolbySurroundMode::NotIndicated, + 1 => DolbySurroundMode::NotEncoded, + 2 => DolbySurroundMode::Encoded, + _ => DolbySurroundMode::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + DolbySurroundMode::NotIndicated => 0, + DolbySurroundMode::NotEncoded => 1, + DolbySurroundMode::Encoded => 2, + DolbySurroundMode::Reserved => 3, + } + } + + /// `true` when the field collapses to "no useful information" per + /// the §5.4.2.6 spec note ("the reserved code may be interpreted + /// as 'not indicated'") — both [`NotIndicated`](Self::NotIndicated) + /// and [`Reserved`](Self::Reserved) fold into one branch for a + /// receiver that wants to apply a matrix-decode default. + pub fn is_not_indicated(self) -> bool { + matches!( + self, + DolbySurroundMode::NotIndicated | DolbySurroundMode::Reserved + ) + } + + /// `true` only for [`Encoded`](Self::Encoded) — a Pro Logic-aware + /// receiver can use this predicate to arm its matrix decoder. + pub fn is_dolby_surround_encoded(self) -> bool { + matches!(self, DolbySurroundMode::Encoded) + } +} + +/// §5.4.2.4 center mix level (Table 5.9). A 2-bit codeword carried +/// only when the stream has 3 front channels (`acmod ∈ {3, 5, 7}`) +/// that picks the nominal down-mix attenuation applied to the centre +/// channel when it is folded into the left / right channels by a +/// stereo downmix. +/// +/// Surfaced on [`Bsi::center_mix`] when the wire codeword is present; +/// `None` for every other channel layout because the 2-bit slot is +/// not on the wire there (§5.3.2 only emits the codeword inside the +/// `(acmod & 0x1) != 0 && acmod != 0x1` guard). +/// +/// Per §5.4.2.4: "If `cmixlev` is set to the reserved code, decoders +/// should still reproduce audio. The intermediate value of `cmixlev` +/// (-4.5 dB) may be used in this case." That fallback is applied by +/// [`Self::coefficient_with_reserved_fallback`] — callers who want +/// to detect "encoder explicitly emitted the reserved code" should +/// match on the [`Reserved`](Self::Reserved) variant directly. +/// +/// Annex E (E-AC-3) removes this 2-bit slot in favour of the refined +/// 3-bit `ltrtcmixlev` / `lorocmixlev` codewords (Tables D2.3 / +/// D2.5), so this enum is base-AC-3 only and is not mirrored on the +/// Annex E `Bsi`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CenterMixLevel { + /// `'00'` — 0.707 (≈ -3.0 dB). + Minus3Db, + /// `'01'` — 0.595 (≈ -4.5 dB). Per §5.4.2.4 this intermediate + /// value is also the spec's recommended fallback when the + /// [`Reserved`](Self::Reserved) codepoint is received. + Minus4Point5Db, + /// `'10'` — 0.500 (≈ -6.0 dB). + Minus6Db, + /// `'11'` — reserved. Per §5.4.2.4 the decoder must still + /// reproduce audio and may treat this codepoint as equivalent to + /// [`Minus4Point5Db`](Self::Minus4Point5Db); + /// [`Self::coefficient_with_reserved_fallback`] applies that + /// substitution. + Reserved, +} + +impl CenterMixLevel { + /// Decode the 2-bit wire value verbatim per Table 5.9. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => CenterMixLevel::Minus3Db, + 1 => CenterMixLevel::Minus4Point5Db, + 2 => CenterMixLevel::Minus6Db, + _ => CenterMixLevel::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + CenterMixLevel::Minus3Db => 0, + CenterMixLevel::Minus4Point5Db => 1, + CenterMixLevel::Minus6Db => 2, + CenterMixLevel::Reserved => 3, + } + } + + /// Linear attenuation coefficient per Table 5.9 — `0.707` for + /// `'00'`, `0.595` for `'01'`, `0.500` for `'10'`, and `None` for + /// the `'11'` reserved codepoint (the spec leaves the choice to + /// the decoder; see + /// [`Self::coefficient_with_reserved_fallback`] for the standard + /// "intermediate value" substitution). + pub fn coefficient(self) -> Option { + match self { + CenterMixLevel::Minus3Db => Some(0.707), + CenterMixLevel::Minus4Point5Db => Some(0.595), + CenterMixLevel::Minus6Db => Some(0.500), + CenterMixLevel::Reserved => None, + } + } + + /// Linear attenuation coefficient with the §5.4.2.4 reserved-code + /// substitution applied — for the [`Reserved`](Self::Reserved) + /// codepoint, returns the intermediate value `0.595` (-4.5 dB) so + /// a downmix consumer can pick the centre-channel gain in a + /// single call. + pub fn coefficient_with_reserved_fallback(self) -> f32 { + match self { + CenterMixLevel::Minus3Db => 0.707, + CenterMixLevel::Minus4Point5Db | CenterMixLevel::Reserved => 0.595, + CenterMixLevel::Minus6Db => 0.500, + } + } + + /// `true` only for [`Reserved`](Self::Reserved) — lets a probe + /// tool flag streams that picked the reserved codepoint without + /// re-walking Table 5.9. + pub fn is_reserved(self) -> bool { + matches!(self, CenterMixLevel::Reserved) + } +} + +/// §5.4.2.5 surround mix level (Table 5.10). A 2-bit codeword carried +/// only when the stream has a surround channel (`acmod ∈ {4, 5, 6, +/// 7}`) that picks the nominal down-mix attenuation applied to the +/// surround channel(s) when they are folded into the left / right +/// channels by a stereo downmix. +/// +/// Surfaced on [`Bsi::surround_mix`] when the wire codeword is +/// present; `None` for every other channel layout because the 2-bit +/// slot is not on the wire there (§5.3.2 only emits the codeword +/// inside the `(acmod & 0x4) != 0` guard). +/// +/// Per §5.4.2.5: "If `surmixlev` is set to the reserved code, the +/// decoder should still reproduce audio. The intermediate value of +/// `surmixlev` (-6 dB) may be used in this case." That fallback is +/// applied by [`Self::coefficient_with_reserved_fallback`] — callers +/// who want to detect "encoder explicitly emitted the reserved code" +/// should match on the [`Reserved`](Self::Reserved) variant +/// directly. +/// +/// Annex E (E-AC-3) removes this 2-bit slot in favour of the refined +/// 3-bit `ltrtsurmixlev` / `lorosurmixlev` codewords (Tables D2.4 / +/// D2.6), so this enum is base-AC-3 only and is not mirrored on the +/// Annex E `Bsi`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SurroundMixLevel { + /// `'00'` — 0.707 (≈ -3.0 dB). + Minus3Db, + /// `'01'` — 0.500 (≈ -6.0 dB). Per §5.4.2.5 this intermediate + /// value is also the spec's recommended fallback when the + /// [`Reserved`](Self::Reserved) codepoint is received. + Minus6Db, + /// `'10'` — 0 (surround channels muted in the downmix). + Mute, + /// `'11'` — reserved. Per §5.4.2.5 the decoder must still + /// reproduce audio and may treat this codepoint as equivalent to + /// [`Minus6Db`](Self::Minus6Db); + /// [`Self::coefficient_with_reserved_fallback`] applies that + /// substitution. + Reserved, +} + +impl SurroundMixLevel { + /// Decode the 2-bit wire value verbatim per Table 5.10. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => SurroundMixLevel::Minus3Db, + 1 => SurroundMixLevel::Minus6Db, + 2 => SurroundMixLevel::Mute, + _ => SurroundMixLevel::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + SurroundMixLevel::Minus3Db => 0, + SurroundMixLevel::Minus6Db => 1, + SurroundMixLevel::Mute => 2, + SurroundMixLevel::Reserved => 3, + } + } + + /// Linear attenuation coefficient per Table 5.10 — `0.707` for + /// `'00'`, `0.500` for `'01'`, `0.000` for `'10'`, and `None` for + /// the `'11'` reserved codepoint (the spec leaves the choice to + /// the decoder; see + /// [`Self::coefficient_with_reserved_fallback`] for the standard + /// "intermediate value" substitution). + pub fn coefficient(self) -> Option { + match self { + SurroundMixLevel::Minus3Db => Some(0.707), + SurroundMixLevel::Minus6Db => Some(0.500), + SurroundMixLevel::Mute => Some(0.000), + SurroundMixLevel::Reserved => None, + } + } + + /// Linear attenuation coefficient with the §5.4.2.5 reserved-code + /// substitution applied — for the [`Reserved`](Self::Reserved) + /// codepoint, returns the intermediate value `0.500` (-6 dB) so + /// a downmix consumer can pick the surround-channel gain in a + /// single call. + pub fn coefficient_with_reserved_fallback(self) -> f32 { + match self { + SurroundMixLevel::Minus3Db => 0.707, + SurroundMixLevel::Minus6Db | SurroundMixLevel::Reserved => 0.500, + SurroundMixLevel::Mute => 0.000, + } + } + + /// `true` only for [`Reserved`](Self::Reserved) — lets a probe + /// tool flag streams that picked the reserved codepoint without + /// re-walking Table 5.10. + pub fn is_reserved(self) -> bool { + matches!(self, SurroundMixLevel::Reserved) + } + + /// `true` only for [`Mute`](Self::Mute) — surround channels are + /// dropped from the stereo downmix. Lets a downmix consumer + /// short-circuit the surround mix-in step without computing the + /// coefficient. + pub fn is_mute(self) -> bool { + matches!(self, SurroundMixLevel::Mute) + } +} + +/// Annex D §2.3.1.10 A/D converter type (Table D2.9). A single bit: +/// `'0'` indicates a generic / standard PCM A/D converter; `'1'` +/// indicates an HDCD-encoded source (HDCD packs a "hidden" 4 bits in +/// the 16-bit PCM LSBs, and downstream equipment may decode them for a +/// 20-bit dynamic range). The AC-3 decoder treats both identically. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AdConverterType { + /// `'0'` — Standard (generic 24-bit PCM). + Standard, + /// `'1'` — HDCD-encoded source. + Hdcd, +} + +impl AdConverterType { + /// Decode the 1-bit wire value verbatim per Table D2.9. + pub fn from_code(code: u8) -> Self { + if code & 0x1 == 0 { + AdConverterType::Standard + } else { + AdConverterType::Hdcd + } + } + + /// Raw 1-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + AdConverterType::Standard => 0, + AdConverterType::Hdcd => 1, + } + } +} + +/// Annex D §2.3.1.2 preferred stereo downmix mode (Table D2.2). +/// +/// Surfaced on [`Bsi::dmixmod_preference`] when `bsid == 6` and the +/// `xbsi1e` block is present; mirrored on +/// [`crate::eac3::Bsi::dmixmod_preference`] when the Annex E +/// `mixmdate == 1` mixing-metadata block is present and `acmod > 2`. +/// `None` outside those gates — base AC-3 streams with the §5.3.2 +/// timecode syntax (`bsid != 6`) cannot carry this hint, and the +/// Annex D / Annex E spec note states the field is meaningful only +/// for the multi-channel audio coding modes (3/0, 2/1, 3/1, 2/2, +/// 3/2); for 1+1 / 1/0 / 2/0 the wire field is reserved and not +/// transmitted. +/// +/// Per §2.3.1.2 / §3.1.1 a compliant two-channel-downmix decoder +/// "should allow the end user to specify which two-channel downmix +/// is chosen" with an "automatic selection of either Lt/Rt or Lo/Ro +/// based on the preferred downmix mode parameter dmixmod" as one of +/// the three options — so the typed value is consulted by an +/// auto-mode downmix router. The Reserved codepoint is per spec to +/// be treated as `NotIndicated` ("the decoder should still reproduce +/// audio. The reserved code may be interpreted as 'not indicated'"). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StereoDownmixPreference { + /// `'00'` — not indicated by the encoder. The downmix router + /// falls back to the LoRo equations from the original §7.8 + /// specification (which the AC-3 decoder defaults to when no + /// preference is signalled). + NotIndicated, + /// `'01'` — Lt/Rt downmix preferred. A matrix-encoded stereo + /// pair suitable for Dolby Pro Logic / Pro Logic II / IIx + /// recovery of the surround field; consult the `ltrtcmixlev` + /// / `ltrtsurmixlev` codewords for the per-channel gains. + LtRtPreferred, + /// `'10'` — Lo/Ro downmix preferred. A non-matrix-encoded + /// stereo pair suitable for conventional two-speaker playback; + /// consult the `lorocmixlev` / `lorosurmixlev` codewords for + /// the per-channel gains. + LoRoPreferred, + /// `'11'` — reserved. Per spec the decoder should treat the + /// reserved codepoint as equivalent to + /// [`NotIndicated`](Self::NotIndicated) and "should still + /// reproduce audio"; we surface it as its own variant so a + /// chain consumer can distinguish "encoder explicitly emitted + /// the reserved code" from "encoder emitted 'not indicated'". + Reserved, +} + +impl StereoDownmixPreference { + /// Decode the 2-bit wire value verbatim per Table D2.2. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => StereoDownmixPreference::NotIndicated, + 1 => StereoDownmixPreference::LtRtPreferred, + 2 => StereoDownmixPreference::LoRoPreferred, + _ => StereoDownmixPreference::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + StereoDownmixPreference::NotIndicated => 0, + StereoDownmixPreference::LtRtPreferred => 1, + StereoDownmixPreference::LoRoPreferred => 2, + StereoDownmixPreference::Reserved => 3, + } + } + + /// Whether the encoder signalled an explicit Lt/Rt preference. + /// + /// Equivalent to `matches!(self, Self::LtRtPreferred)` but + /// expressed as a method so a downmix router can short-circuit + /// on the typed predicate. + pub fn prefers_lt_rt(self) -> bool { + matches!(self, StereoDownmixPreference::LtRtPreferred) + } + + /// Whether the encoder signalled an explicit Lo/Ro preference. + /// + /// Equivalent to `matches!(self, Self::LoRoPreferred)` but + /// expressed as a method so a downmix router can short-circuit + /// on the typed predicate. + pub fn prefers_lo_ro(self) -> bool { + matches!(self, StereoDownmixPreference::LoRoPreferred) + } + + /// Whether the codepoint should be treated as "not indicated" + /// — covers both the explicit [`NotIndicated`](Self::NotIndicated) + /// codepoint and the [`Reserved`](Self::Reserved) codepoint + /// (per §2.3.1.2: "the reserved code may be interpreted as + /// 'not indicated'"). Lets an auto-mode downmix router collapse + /// both into the "fall back to §7.8 LoRo defaults" branch with + /// a single check. + pub fn is_not_indicated(self) -> bool { + matches!( + self, + StereoDownmixPreference::NotIndicated | StereoDownmixPreference::Reserved + ) + } +} + +/// §5.4.2.15 / Table 5.12 mixing-room type. A 2-bit code describing +/// the calibration of the mixing room used during the final audio +/// mixing session. +/// +/// Per spec the value "is not typically used by the AC-3 decoder, but +/// may be used by other parts of the audio reproduction equipment". +/// The reserved code may be interpreted as "not indicated"; we keep +/// it as its own variant so a careful consumer can still distinguish +/// "encoder explicitly left the field blank" from "encoder emitted an +/// invalid codepoint". +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RoomType { + /// `'00'` — not indicated. + NotIndicated, + /// `'01'` — large room, X-curve monitor calibration. + LargeXCurve, + /// `'10'` — small room, flat monitor calibration. + SmallFlat, + /// `'11'` — reserved (treat as + /// [`NotIndicated`](Self::NotIndicated) per §5.4.2.15; the + /// decoder must still reproduce audio). + Reserved, +} + +impl RoomType { + /// Decode the 2-bit wire value verbatim per Table 5.12. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => RoomType::NotIndicated, + 1 => RoomType::LargeXCurve, + 2 => RoomType::SmallFlat, + _ => RoomType::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire. + pub fn raw(self) -> u8 { + match self { + RoomType::NotIndicated => 0, + RoomType::LargeXCurve => 1, + RoomType::SmallFlat => 2, + RoomType::Reserved => 3, + } + } +} + +/// §5.4.2.11-12 deprecated language-code word — the optional 8-bit +/// `langcod` slot that follows `langcode == 1` in the §5.3.2 base +/// AC-3 BSI syntax (and its Ch2 `langcod2` mirror at §5.4.2.19-20 in +/// 1+1 dual-mono streams). +/// +/// The original 1995 A/52 specification defined `langcod` as an 8-bit +/// table-lookup index into a language identifier table; the 2001 +/// revision retired the table-lookup semantics. Per the current +/// §5.4.2.12 wire-conformance rule the slot "is an 8 bit reserved +/// value that shall be set to `0xFF` if present" — modern delivery +/// systems carry the ISO 639-2 language code in the signaling layer, +/// and the in-stream slot is kept only for bitstream-format +/// backwards-compatibility. +/// +/// A typed surface over the raw byte lets a probe / archive tool flag +/// legacy streams that still carry a non-`0xFF` value (and may +/// therefore be addressable to the obsolete 1995 lookup table) — via +/// [`Self::is_spec_reserved_value`] — without re-parsing the BSI. +/// The AC-3 decoder ignores this word; the slot exists in the BSI +/// surface only as an informational hint. +/// +/// Per §5.4.2.12 the field only exists when `langcode == 1`; the +/// typed surface lives behind `Bsi::language_code: +/// Option` so the absence (`langcode == 0`) case +/// short-circuits to `None`. The Ch2 mirror at §5.4.2.20 reads "See +/// lancod, Section 5.4.2.12 above" so the same type backs both +/// slots. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LanguageCode { + raw: u8, +} + +impl LanguageCode { + /// Wrap the 8-bit wire byte verbatim. No remap or rejection — + /// every value `0x00..=0xFF` is representable, and the + /// spec-conformance check is exposed separately as + /// [`Self::is_spec_reserved_value`]. + pub fn from_raw(raw: u8) -> Self { + LanguageCode { raw } + } + + /// Raw 8-bit code as it appeared on the wire. For a + /// spec-conforming stream this is always `0xFF`; legacy 1995-era + /// streams may carry a different value pointing into the retired + /// language-id table. + pub fn raw(self) -> u8 { + self.raw + } + + /// `true` when the carried byte equals the §5.4.2.12 mandated + /// reserved value (`0xFF`). For a spec-conforming stream this + /// predicate is always `true` when the slot is present; a probe + /// tool can branch on `false` to surface a non-conforming legacy + /// stream to a chain-of-custody log. + pub fn is_spec_reserved_value(self) -> bool { + self.raw == 0xFF + } +} + +/// §5.4.2.13-15 audio production information block — the +/// `audprodie==1` payload (and its Ch2 `audprodi2e==1` mirror in 1+1 +/// dual-mono streams). Carries a peak mixing-level hint and the +/// mixing-room calibration. +/// +/// Neither field affects AC-3 PCM decoding, but a downstream +/// SPL-calibrated reproduction chain (cinema / mastering monitor) +/// can use them to re-target the playback level back to the absolute +/// SPL the mixing engineer was monitoring at. Per §5.4.2.14 the peak +/// mixing level is `80 + mixlevel` dB SPL, in the documented range +/// 80..=111 dB SPL. +/// +/// The Annex E (E-AC-3) `infomdata` informational block reuses the +/// same two fields with identical semantics (§E.2.3.1.x) so the type +/// is shared between the AC-3 and E-AC-3 BSI surfaces. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AudioProductionInfo { + /// Raw 5-bit `mixlevel` codepoint, in the spec-documented range + /// 0..=31. The peak mixing-session SPL is + /// `80 + mixlevel` dB SPL — use [`Self::peak_mix_level_db_spl`] + /// for the resolved value. + pub mixlevel: u8, + /// Typed `roomtyp` decode (Table 5.12). + pub roomtyp: RoomType, +} + +impl AudioProductionInfo { + /// Resolve the [`Self::mixlevel`] codepoint into its absolute + /// peak SPL value per §5.4.2.14: the peak mixing level is + /// `80 + mixlevel` dB SPL, i.e. in the range 80..=111 dB SPL for + /// a 5-bit codepoint. + pub fn peak_mix_level_db_spl(self) -> u32 { + 80 + (self.mixlevel as u32 & 0x1F) + } +} + +/// §5.4.2.27 base-syntax `timecod1` field — the **low-resolution** half +/// of the 28-bit SMPTE-style time code. Surfaced on +/// [`Bsi::timecod1`] only when the base syntax is in use (`bsid != 6`, +/// equivalently when the alternate Annex D syntax is *not* selected) +/// AND the encoder set `timecod1e == 1`. +/// +/// The 14 wire bits split per §5.4.2.27 as `H H H H H . M M M M M M . +/// S S S` (MSB-first): +/// +/// * 5-bit `hours` field — valid range `0..=23` (§5.4.2.27 says values +/// 24..=31 are illegal but spec-compliant decoders should still +/// reproduce audio; the parser accepts the raw codepoint and lets +/// the caller decide). +/// * 6-bit `minutes` field — valid range `0..=59`. +/// * 3-bit `eight_second_increments` field — valid range `0..=7`, +/// each step representing 8 seconds (i.e. `0, 8, 16, 24, 32, 40, +/// 48, 56` seconds within the current minute). +/// +/// The combined resolution is 8 seconds and the addressable range is +/// 24 hours (`24 × 3600 = 86 400 s`). The high-resolution remainder +/// lives in [`TimeCode2`]. +/// +/// Per §5.4.2.26 and Annex D §1 these slots have "never been applied +/// for their originally anticipated purpose" — modern delivery uses +/// out-of-band timecode (e.g. PTP, SMPTE 12M MTC) — but legacy AC-3 +/// streams may still carry them, and a careful consumer can recover a +/// frame-accurate playback offset for editorial workflows. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TimeCode1 { + raw: u16, +} + +impl TimeCode1 { + /// Wrap the 14-bit wire value verbatim. Only the low 14 bits of + /// `raw` are consulted; the parser hands the field in already + /// masked. + pub fn from_raw(raw: u16) -> Self { + Self { raw: raw & 0x3FFF } + } + + /// Underlying 14-bit wire value — `HHHHH MMMMMM SSS` packed + /// MSB-first. + pub fn raw(self) -> u16 { + self.raw + } + + /// 5-bit `hours` field, in `0..=31` (spec-valid range `0..=23`). + pub fn hours(self) -> u8 { + ((self.raw >> 9) & 0x1F) as u8 + } + + /// 6-bit `minutes` field, in `0..=63` (spec-valid range `0..=59`). + pub fn minutes(self) -> u8 { + ((self.raw >> 3) & 0x3F) as u8 + } + + /// 3-bit `eight_second_increments` field, in `0..=7`. Each step + /// represents 8 seconds within the current minute. + pub fn eight_second_increments(self) -> u8 { + (self.raw & 0x7) as u8 + } + + /// Total whole-second offset within the 24-hour day represented by + /// this half — `hours·3600 + minutes·60 + eight_second_increments·8`. + /// Maxes at `23·3600 + 59·60 + 7·8 = 86 336 s` for spec-valid input; + /// the raw 5+6+3 bit ranges can push the result up to `122 296 s` + /// when the encoder emits out-of-range values (the parser still + /// passes those through verbatim). + pub fn seconds_in_day(self) -> u32 { + (self.hours() as u32) * 3600 + + (self.minutes() as u32) * 60 + + (self.eight_second_increments() as u32) * 8 + } + + /// `true` when every field is inside its spec-documented range + /// (`hours ≤ 23`, `minutes ≤ 59`). The `eight_second_increments` + /// field cannot overflow its spec range (its 3-bit width caps it at + /// 7). Use this to flag malformed encoders without rejecting the + /// stream — per §5.4.2.27 a decoder need not act on the timecode. + pub fn is_spec_valid(self) -> bool { + self.hours() <= 23 && self.minutes() <= 59 + } +} + +/// §5.4.2.28 base-syntax `timecod2` field — the **high-resolution** +/// half of the 28-bit SMPTE-style time code. Surfaced on +/// [`Bsi::timecod2`] only when the base syntax is in use (`bsid != 6`) +/// AND the encoder set `timecod2e == 1`. +/// +/// The 14 wire bits split per §5.4.2.28 as `S S S . F F F F F . f f f +/// f f f` (MSB-first): +/// +/// * 3-bit `seconds` field — valid range `0..=7`, the residual whole +/// seconds beyond the [`TimeCode1::eight_second_increments`] +/// quantum (i.e. `tc1.eight_second_increments·8 + tc2.seconds` +/// recovers the absolute second-within-minute). +/// * 5-bit `frames` field — valid range `0..=29` (assumes a 30 fps +/// reference per §5.4.2.26 "one frame = 1/30th of a second"; the +/// parser accepts codepoints up to 31). +/// * 6-bit `frame_fractions` field — valid range `0..=63`, each step +/// representing 1/64 of a frame. +/// +/// The combined resolution is `1 / (30 × 64) ≈ 521 µs` and the +/// addressable range covers 8 seconds (the quantum of +/// [`TimeCode1::eight_second_increments`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TimeCode2 { + raw: u16, +} + +impl TimeCode2 { + /// Wrap the 14-bit wire value verbatim. Only the low 14 bits of + /// `raw` are consulted. + pub fn from_raw(raw: u16) -> Self { + Self { raw: raw & 0x3FFF } + } + + /// Underlying 14-bit wire value — `SSS FFFFF ffffff` packed + /// MSB-first. + pub fn raw(self) -> u16 { + self.raw + } + + /// 3-bit `seconds` field, in `0..=7`. Combine with + /// [`TimeCode1::eight_second_increments`] for the absolute + /// second-within-minute (`tc1.eight_second_increments · 8 + + /// tc2.seconds`). + pub fn seconds(self) -> u8 { + ((self.raw >> 11) & 0x7) as u8 + } + + /// 5-bit `frames` field, in `0..=31` (spec-valid range `0..=29` + /// for the 30 fps reference assumed by §5.4.2.26). + pub fn frames(self) -> u8 { + ((self.raw >> 6) & 0x1F) as u8 + } + + /// 6-bit `frame_fractions` field, in `0..=63`. Each step represents + /// 1/64 of a frame; at the 30 fps reference that is `≈ 521 µs`. + pub fn frame_fractions(self) -> u8 { + (self.raw & 0x3F) as u8 + } + + /// `true` when the `frames` field is inside its spec-documented + /// 30 fps range (`≤ 29`). The 3-bit `seconds` and 6-bit + /// `frame_fractions` fields cannot exceed their spec ranges. + pub fn is_spec_valid(self) -> bool { + self.frames() <= 29 + } +} + +/// §5.4.2.26 Table 5.13 presence pattern for the +/// `timecod2e, timecod1e` pair. A receiver typically inspects +/// [`Bsi::timecode_presence`] before deciding whether to consult +/// [`Bsi::timecod1`] / [`Bsi::timecod2`]. +/// +/// Wire codes per Table 5.13: +/// +/// * `(timecod2e=0, timecod1e=0)` → [`Self::NotPresent`] +/// * `(timecod2e=0, timecod1e=1)` → [`Self::FirstHalfOnly`] +/// * `(timecod2e=1, timecod1e=0)` → [`Self::SecondHalfOnly`] +/// * `(timecod2e=1, timecod1e=1)` → [`Self::BothHalves`] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TimeCodePresence { + /// Neither half present — `timecod2e=0, timecod1e=0`. + NotPresent, + /// First (low-resolution) half only — `timecod2e=0, timecod1e=1`. + /// Resolves coarse playback offset to 8-second granularity. + FirstHalfOnly, + /// Second (high-resolution) half only — `timecod2e=1, timecod1e=0`. + /// Resolves to ≈ 521 µs but only within the implicit `0..=8 s` + /// quantum — typically paired with out-of-band sync to pin the + /// minute / hour position. + SecondHalfOnly, + /// Both halves present — `timecod2e=1, timecod1e=1`. Full 28-bit + /// SMPTE-style timecode addressing 24 h at ≈ 521 µs resolution. + BothHalves, +} + +impl TimeCodePresence { + /// Resolve the `(timecod2e, timecod1e)` pair into a presence + /// pattern per Table 5.13. Only the low bit of each input is + /// consulted. + pub fn from_flags(timecod2e: bool, timecod1e: bool) -> Self { + match (timecod2e, timecod1e) { + (false, false) => TimeCodePresence::NotPresent, + (false, true) => TimeCodePresence::FirstHalfOnly, + (true, false) => TimeCodePresence::SecondHalfOnly, + (true, true) => TimeCodePresence::BothHalves, + } + } +} + +/// §5.4.2.24-25 distribution-control hint pair — the `copyrightb` +/// (Copyright Bit) + `origbs` (Original Bit Stream) flags. Both are +/// 1-bit fields placed back-to-back in every BSI's mandatory section +/// (§5.3.2 — they live just after the optional `audprodie` / `roomtyp2` +/// chain and just before the `timecod*e` / `xbsi*e` slots, with no +/// per-acmod gate). +/// +/// Per spec text: +/// +/// * `copyrightb == 1` — the bitstream is indicated as +/// copyright-protected (§5.4.2.24). `0` — not indicated as +/// protected. +/// * `origbs == 1` — this is an original bitstream (§5.4.2.25). `0` — +/// this is a copy of another bitstream. +/// +/// The decoder does not act on either bit; surfacing them lets a chain +/// consumer enforce a distribution / archival policy (e.g. refuse to +/// re-encode a `copyrightb == 1` stream, or tag a `origbs == 0` copy +/// for downstream-only routing) without re-parsing the BSI. +/// +/// On the Annex E (E-AC-3) side the same `copyrightb` / `origbs` pair +/// is carried inside the §E.2.3.1.62 informational-metadata block +/// (gated by `infomdate == 1`) and surfaces as +/// `eac3::Bsi::copyright_info` — see [`crate::eac3::bsi::Bsi`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CopyrightInfo { + copyrightb: bool, + origbs: bool, +} + +impl CopyrightInfo { + /// Build a [`CopyrightInfo`] from the raw 1-bit `copyrightb` / + /// `origbs` values. Inputs are taken as booleans so the call site + /// stays clean — the BSI parser passes the bit-shift result of + /// each `read_u32(1)?` cast through `!= 0`. + pub fn from_bits(copyrightb: bool, origbs: bool) -> Self { + Self { copyrightb, origbs } + } + + /// `true` when the encoder set the `copyrightb` bit + /// (§5.4.2.24 — "the information in the bit stream is indicated as + /// protected by copyright"). + pub fn is_copyright_protected(self) -> bool { + self.copyrightb + } + + /// `true` when the encoder set the `origbs` bit (§5.4.2.25 — + /// "this is an original bit stream"). `false` indicates this is a + /// copy of another bitstream. + pub fn is_original_bitstream(self) -> bool { + self.origbs + } + + /// Raw 1-bit `copyrightb` codepoint, useful for re-emission / + /// bit-exact mirroring of the wire field. + pub fn copyrightb_bit(self) -> u8 { + u8::from(self.copyrightb) + } + + /// Raw 1-bit `origbs` codepoint, useful for re-emission / + /// bit-exact mirroring of the wire field. + pub fn origbs_bit(self) -> u8 { + u8::from(self.origbs) + } +} + +/// §5.4.2.29-31 additional bit-stream information (`addbsi`) payload. +/// +/// Carries between 1 and 64 bytes of encoder-defined trailing data, +/// gated on `addbsie == 1`. The bit-stream syntax (§5.3.2 / Table 5.1) +/// places the field right before the audio blocks at the end of +/// `bit_stream_info()`; on Annex E (E-AC-3) streams the same field +/// closes Table E1.2's BSI walk at the same logical position. +/// +/// Per §5.4.2.30 — "the decoder is not required to interpret this +/// information, and thus shall skip over this number of bytes" — the +/// PCM decode is unchanged; surfacing the payload bytes lets a chain +/// consumer reach an encoder-private metadata block without +/// re-walking the BSI. +/// +/// The wire format is: +/// +/// ```text +/// addbsie 1 bit // 1 = field present +/// if (addbsie) { +/// addbsil 6 bits // 0..=63 ⇒ 1..=64 payload bytes +/// addbsi (addbsil + 1) × 8 bits +/// } +/// ``` +/// +/// The payload bytes are stored verbatim in transmission order — bit 7 +/// of the first byte is the bit immediately after the `addbsil` field. +/// The bit-stream cursor is not required to be byte-aligned at the +/// start of `addbsi` (and in practice rarely is, since `addbsi` follows +/// a 6-bit length field rather than padding); the bytes here are the +/// MSB-first bit-stream view in 8-bit groups, matching the wire-order +/// reads §5.4.2.31 prescribes. +/// +/// The `addbsil` codepoint is preserved verbatim so a caller can +/// distinguish between an empty payload (`addbsil == 0`, payload = `[0]` +/// — a single byte) and a long payload at the codepoint endpoint +/// (`addbsil == 63`, payload = 64 bytes). The length-byte relationship +/// is `payload.len() == addbsil + 1`. +/// +/// On the Annex E (E-AC-3) side the same payload is surfaced on +/// `eac3::Bsi::addbsi` — see [`crate::eac3::bsi::Bsi`] — using the same +/// type. The base + Annex E syntax tables (§5.3.2 / Table E1.2) carry +/// `addbsie + addbsil + addbsi` verbatim, so a single typed surface +/// covers both. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AdditionalBitStreamInfo { + addbsil: u8, + payload: Vec, +} + +impl AdditionalBitStreamInfo { + /// Build an [`AdditionalBitStreamInfo`] from the raw 6-bit + /// `addbsil` codepoint + the (addbsil + 1)-byte payload. Returns + /// `None` when `addbsil >= 64` (the wire field is 6 bits, so any + /// caller-supplied value above `63` is outside the codepoint range) + /// or when `payload.len() != addbsil as usize + 1` (the spec is + /// strict about the length-byte relationship — a violation here + /// would not round-trip back through the bit-stream parser). + pub fn from_addbsil_and_payload(addbsil: u8, payload: Vec) -> Option { + if addbsil > 63 { + return None; + } + if payload.len() != addbsil as usize + 1 { + return None; + } + Some(Self { addbsil, payload }) + } + + /// Raw 6-bit `addbsil` codepoint (§5.4.2.30). Range `0..=63`, + /// indicating `1..=64` payload bytes (the codepoint is the byte + /// count *minus one*). + pub fn addbsil(&self) -> u8 { + self.addbsil + } + + /// Number of payload bytes — `addbsil + 1`. Always within `1..=64` + /// per the §5.4.2.30 codepoint range. + pub fn len(&self) -> usize { + self.addbsil as usize + 1 + } + + /// Convenience: `false` always per the spec — the field is at + /// least 1 byte whenever it exists. Provided for Clippy + /// `len_without_is_empty` and for caller idiomatic checks. + pub fn is_empty(&self) -> bool { + false + } + + /// Borrowed view of the payload bytes in wire order (bit 7 of byte + /// 0 is the bit immediately after `addbsil`). + pub fn payload(&self) -> &[u8] { + &self.payload + } + + /// Total wire-field width in bits — `7 + 8 × (addbsil + 1)`. Useful + /// for callers that need to mirror the BSI verbatim back into a + /// bit-stream writer (6 bits for `addbsil` + 8 × payload bytes + + /// 1 bit for the `addbsie` flag that gates the block). + pub fn wire_bits(&self) -> u32 { + 7 + 8 * (self.addbsil as u32 + 1) + } +} + +/// Annex D §2.3.1.11-12 reserved-for-future-assignment + encoder-private +/// trailer of the `xbsi2` block. The §2.3 alternate bit-stream syntax +/// reserves 9 bits at the tail of the `xbsi2e == 1` block: +/// +/// ```text +/// xbsi2 8 bits // §2.3.1.11 — reserved for future assignment; +/// // encoders shall set to all 0s. +/// encinfo 1 bit // §2.3.1.12 — reserved for encoder-private use; +/// // decoders do not interpret. +/// ``` +/// +/// Both fields sit definitionally after the §2.3.1.8-10 informational +/// metadata (`dsurexmod` / `dheadphonmod` / `adconvtyp`) inside the +/// `xbsi2e == 1` block — see §D Table D2.1 syntax. They are decoder +/// no-ops by spec but a conformant decoder still has to walk the bits, +/// and a chain consumer that re-emits the stream verbatim needs the raw +/// codepoints to round-trip the BSI without loss. +/// +/// Surfacing the typed pair lets: +/// +/// * a conformance probe verify that `xbsi2 == 0x00` per §2.3.1.11 +/// (a non-zero codepoint flags either a future-spec extension or a +/// non-conformant encoder) without re-parsing the BSI, +/// * an encoder-watermark / encoder-identification consumer recover the +/// §2.3.1.12 `encinfo` bit without consulting a magic-number sentinel, +/// * a verbatim re-encoder route the BSI back into a bit-stream writer +/// bit-exactly without re-walking the wire. +/// +/// `xbsi2` is preserved as a raw `u8` rather than parsed into a +/// codepoint enum — §2.3.1.11 deliberately leaves the bits unassigned, +/// so any partition would be premature. The single accessor +/// [`Self::is_spec_reserved_value`] flags whether the carried byte +/// matches the spec-conformance `0x00` value. +/// +/// On the Annex E (E-AC-3) side this block does not exist — the Annex E +/// BSI never carries an `xbsi2e == 1` slot — so the typed surface stays +/// on the base BSI struct only. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExtraBsi2 { + xbsi2: u8, + encinfo: bool, +} + +impl ExtraBsi2 { + /// Build an [`ExtraBsi2`] from the raw 8-bit `xbsi2` codepoint and + /// the 1-bit `encinfo` flag. No validation — both fields are + /// reserved (`xbsi2` for future assignment, `encinfo` for encoder + /// private use) so any 8-bit + 1-bit combination is wire-legal. + pub fn from_raw(xbsi2: u8, encinfo: bool) -> Self { + Self { xbsi2, encinfo } + } + /// Raw 8-bit `xbsi2` codepoint (§2.3.1.11). Per spec encoders shall + /// set this to `0x00`; a non-zero codepoint flags either a + /// future-spec extension or a non-conformant encoder. + pub fn xbsi2(&self) -> u8 { + self.xbsi2 + } + /// 1-bit `encinfo` flag (§2.3.1.12). Reserved for encoder-private + /// use; the decoder does not interpret it. + pub fn encinfo(&self) -> bool { + self.encinfo + } + /// `true` when the carried `xbsi2` byte matches the spec-mandated + /// `0x00` wire-conformance value (§2.3.1.11). Lets a probe / archive + /// tool route streams that carry a non-conformant codepoint without + /// re-parsing the BSI. The `encinfo` bit is excluded from the + /// check — it is reserved for encoder-private use and any value is + /// wire-legal. + pub fn is_spec_reserved_value(&self) -> bool { + self.xbsi2 == 0x00 + } + /// Total wire-field width in bits — `9` (8 bits for `xbsi2` + 1 bit + /// for `encinfo`). Useful for callers that need to mirror the BSI + /// verbatim back into a bit-stream writer; does **not** include the + /// gating `xbsi2e` flag that sits ahead of the informational + /// metadata block. + pub fn wire_bits(&self) -> u32 { + 9 + } +} + +/// Annex D §2.3.1.3-6 alternate-syntax mix-level codewords. Each is a +/// 3-bit value; Tables D2.3 / D2.4 / D2.5 / D2.6 map them to linear +/// gains via [`annex_d_lt_rt_clev`] / [`annex_d_lt_rt_slev`] / +/// [`annex_d_lo_ro_clev`] / [`annex_d_lo_ro_slev`]. +/// +/// These supersede the body-spec 2-bit `cmixlev` / `surmixlev` defaults +/// for the LtRt / LoRo downmix targets specifically — the body fields +/// are still parsed (they sit ahead of the xbsi1 block in the bit +/// stream) but a §7.8 downmix on a `bsid == 6` Annex D stream should +/// prefer the Annex D refinements. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AnnexDMixLevels { + /// `ltrtcmixlev` (Table D2.3). Defined for acmod ∈ {3, 5, 7}. + pub ltrtcmixlev: u8, + /// `ltrtsurmixlev` (Table D2.4). Codes 000..010 are reserved → use + /// 0.841. Defined for acmod ∈ {4, 5, 6, 7}. + pub ltrtsurmixlev: u8, + /// `lorocmixlev` (Table D2.5). Defined for acmod ∈ {3, 5, 7}. + pub lorocmixlev: u8, + /// `lorosurmixlev` (Table D2.6). Codes 000..010 are reserved → use + /// 0.841. Defined for acmod ∈ {4, 5, 6, 7}. + pub lorosurmixlev: u8, +} + +/// Map an Annex D 3-bit "center-channel" mix-level code to a linear +/// gain per Tables D2.3 / D2.5 (the two tables are identical). +/// +/// Code → gain (dB): +/// `000` 1.414 (+3.0), `001` 1.189 (+1.5), `010` 1.000 ( 0.0), +/// `011` 0.841 (−1.5), `100` 0.707 (−3.0), `101` 0.595 (−4.5), +/// `110` 0.500 (−6.0), `111` 0.000 (−∞). +pub fn annex_d_center_mix_gain(code: u8) -> f32 { + match code & 0x7 { + 0 => 1.414, + 1 => 1.189, + 2 => 1.000, + 3 => 0.841, + 4 => 0.707, + 5 => 0.595, + 6 => 0.500, + _ => 0.000, + } +} + +/// Map an Annex D 3-bit "surround-channel" mix-level code to a linear +/// gain per Tables D2.4 / D2.6 (identical). Codes `000..010` are +/// reserved; per §2.3.1.4 / §2.3.1.6 the decoder shall substitute +/// 0.841 (the next defined code). +pub fn annex_d_surround_mix_gain(code: u8) -> f32 { + match code & 0x7 { + 0..=3 => 0.841, // 000/001/010 reserved → 0.841; 011 = 0.841 + 4 => 0.707, + 5 => 0.595, + 6 => 0.500, + _ => 0.000, + } +} + +/// Parse the BSI starting at the beginning of `data`. The slice *must* +/// point at the byte immediately following `syncinfo` (i.e. byte 5 of +/// the syncframe). +/// +/// On success the returned `Bsi` describes the stream and carries the +/// exact number of bits the parser consumed, so the caller can resume +/// an MSB-first `BitReader` at the right place for the first audio +/// block. +pub fn parse(data: &[u8]) -> Result { + let mut br = BitReader::new(data); + + let bsid = br.read_u32(5)? as u8; + let bsmod = br.read_u32(3)? as u8; + let acmod = br.read_u32(3)? as u8; + let nfchans = acmod_nfchans(acmod); + + // cmixlev — only present when there are 3 front channels, i.e. + // the two LSBs of acmod include '1' for centre *and* acmod!=1 + // (the spec's "if ((acmod & 0x1) && (acmod != 0x1))" guard). + let (cmixlev, center_mix) = if (acmod & 0x1) != 0 && acmod != 0x1 { + let raw = br.read_u32(2)? as u8; + (raw, Some(CenterMixLevel::from_code(raw))) + } else { + (0xFF, None) + }; + + // surmixlev — present when a surround channel exists (acmod & 0x4). + let (surmixlev, surround_mix) = if (acmod & 0x4) != 0 { + let raw = br.read_u32(2)? as u8; + (raw, Some(SurroundMixLevel::from_code(raw))) + } else { + (0xFF, None) + }; + + // dsurmod — present only in 2/0 mode (acmod == 0x2). + let (dsurmod, dolby_surround_mode) = if acmod == 0x2 { + let raw = br.read_u32(2)? as u8; + (raw, Some(DolbySurroundMode::from_code(raw))) + } else { + (0xFF, None) + }; + + let lfeon = br.read_u32(1)? != 0; + let nchans = nfchans + u8::from(lfeon); + + let dialnorm_raw = br.read_u32(5)? as u8; + // §5.4.2.8: dialnorm=0 is reserved; decoder shall use -31 dB. + let dialnorm = if dialnorm_raw == 0 { 31 } else { dialnorm_raw }; + + // Optional service metadata (§5.4.2.9 ff). `compr` is surfaced + // (Table 7.30); `audprodie` carries the §5.4.2.13-15 mixing-room + // hints and is surfaced as a typed [`AudioProductionInfo`]. The + // §5.4.2.11-12 `langcod` slot — once a table-lookup language id, + // now a wire-conformance reserved `0xFF` per the 2001 revision — + // is surfaced as a typed [`LanguageCode`] so a probe / archive + // tool can flag legacy non-conforming streams without re-parsing + // the BSI. + let compre = br.read_u32(1)? != 0; + let compr = if compre { + Some(CompressionGain::from_byte(br.read_u32(8)? as u8)) + } else { + None + }; + let langcode_flag = br.read_u32(1)? != 0; + let language_code = if langcode_flag { + Some(LanguageCode::from_raw(br.read_u32(8)? as u8)) + } else { + None + }; + let audprodie = br.read_u32(1)? != 0; + let audio_production = if audprodie { + let mixlevel = br.read_u32(5)? as u8; + let roomtyp_raw = br.read_u32(2)? as u8; + Some(AudioProductionInfo { + mixlevel, + roomtyp: RoomType::from_code(roomtyp_raw), + }) + } else { + None + }; + + // 1+1 mode (dual mono) carries a second copy of the metadata for Ch2. + let (dialnorm_ch2, compr_ch2, language_code_ch2, audio_production_ch2) = if acmod == 0 { + // §5.4.2.16 — dialnorm2 has the same meaning as dialnorm; the + // `0` codepoint is reserved and remaps to `31` per §5.4.2.8. + let dialnorm2_raw = br.read_u32(5)? as u8; + let dialnorm2 = if dialnorm2_raw == 0 { + 31 + } else { + dialnorm2_raw + }; + let compr2e = br.read_u32(1)? != 0; + let c2 = if compr2e { + Some(CompressionGain::from_byte(br.read_u32(8)? as u8)) + } else { + None + }; + // §5.4.2.19-20 Ch2 `langcod2` slot — same reserved-`0xFF` + // wire-conformance semantics as the Ch1 `langcod`. + let langcod2e = br.read_u32(1)? != 0; + let lc2 = if langcod2e { + Some(LanguageCode::from_raw(br.read_u32(8)? as u8)) + } else { + None + }; + let audprodi2e = br.read_u32(1)? != 0; + let ap2 = if audprodi2e { + let mixlevel2 = br.read_u32(5)? as u8; + let roomtyp2_raw = br.read_u32(2)? as u8; + Some(AudioProductionInfo { + mixlevel: mixlevel2, + roomtyp: RoomType::from_code(roomtyp2_raw), + }) + } else { + None + }; + (Some(dialnorm2), c2, lc2, ap2) + } else { + (None, None, None, None) + }; + + let copyrightb = br.read_u32(1)? != 0; + let origbs = br.read_u32(1)? != 0; + let copyright_info = CopyrightInfo::from_bits(copyrightb, origbs); + + // §5.3.2 base syntax has `timecod1e/timecod2e` here; Annex D + // §2.3 / Table D2.1 reuses the same two 1+14-bit slots as + // `xbsi1e/xbsi2e` and is identified by `bsid == 6` (§2.1). + // Both shapes occupy the same fixed 30 bits maximum so the + // surrounding parse is unchanged. + let ( + annex_d_mix_levels, + dmixmod, + dmixmod_preference, + dsurexmod, + dheadphonmod, + adconvtyp, + extra_bsi, + timecod1, + timecod2, + timecode_presence, + ) = if bsid == 6 { + // Annex D xbsi1 block. + let xbsi1e = br.read_u32(1)? != 0; + let (mix, dmm, dmm_pref) = if xbsi1e { + let dmm = br.read_u32(2)? as u8; + let ltrtc = br.read_u32(3)? as u8; + let ltrts = br.read_u32(3)? as u8; + let loroc = br.read_u32(3)? as u8; + let loros = br.read_u32(3)? as u8; + ( + Some(AnnexDMixLevels { + ltrtcmixlev: ltrtc, + ltrtsurmixlev: ltrts, + lorocmixlev: loroc, + lorosurmixlev: loros, + }), + dmm, + Some(StereoDownmixPreference::from_code(dmm)), + ) + } else { + (None, 0xFFu8, None) + }; + // xbsi2 block — §2.3.1.7-12. 14 bits total: dsurexmod(2) + + // dheadphonmod(2) + adconvtyp(1) + xbsi2(8) + encinfo(1). The + // last two are reserved-for-future-assignment / encoder-private + // respectively; they are captured into [`ExtraBsi2`] so a chain + // consumer can verify spec-conformance (`xbsi2 == 0x00`) and + // recover the encoder-private `encinfo` bit without re-walking + // the BSI. + let xbsi2e = br.read_u32(1)? != 0; + let (dsex, dhpm, adcv, xbsi2_tail) = if xbsi2e { + let dsex_raw = br.read_u32(2)? as u8; + let dhpm_raw = br.read_u32(2)? as u8; + let adcv_raw = br.read_u32(1)? as u8; + let xbsi2_raw = br.read_u32(8)? as u8; + let encinfo_raw = br.read_u32(1)? != 0; + ( + Some(DolbySurroundExMode::from_code(dsex_raw)), + Some(DolbyHeadphoneMode::from_code(dhpm_raw)), + Some(AdConverterType::from_code(adcv_raw)), + Some(ExtraBsi2::from_raw(xbsi2_raw, encinfo_raw)), + ) + } else { + (None, None, None, None) + }; + // Annex D syntax replaces both `timecod*` slots with + // `xbsi*e` blocks — by definition the timecode is absent. + ( + mix, + dmm, + dmm_pref, + dsex, + dhpm, + adcv, + xbsi2_tail, + None, + None, + TimeCodePresence::NotPresent, + ) + } else { + // §5.3.2 base syntax — timecod1/timecod2 surfaced as typed + // [`TimeCode1`] / [`TimeCode2`] when the encoder set the + // respective `timecod*e` flag. Both halves are independently + // gated per §5.4.2.26 Table 5.13. + let timecod1e = br.read_u32(1)? != 0; + let tc1 = if timecod1e { + Some(TimeCode1::from_raw(br.read_u32(14)? as u16)) + } else { + None + }; + let timecod2e = br.read_u32(1)? != 0; + let tc2 = if timecod2e { + Some(TimeCode2::from_raw(br.read_u32(14)? as u16)) + } else { + None + }; + let presence = TimeCodePresence::from_flags(timecod2e, timecod1e); + ( + None, 0xFFu8, None, None, None, None, None, tc1, tc2, presence, + ) + }; + + // addbsi — §5.4.2.29-31 trailer of 1..=64 encoder-defined bytes. + // The decoder PCM path does not consult these bits ("the decoder is + // not required to interpret this information") so the payload is + // surfaced verbatim for chain consumers (encoder-private metadata, + // OAMD packetisation, distribution-tagging) and the cursor is + // advanced exactly `7 + 8 × (addbsil + 1)` bits. + let addbsie = br.read_u32(1)? != 0; + let addbsi = if addbsie { + let addbsil = br.read_u32(6)? as u8; // 0..=63, meaning 1..=64 bytes + let nbytes = addbsil as usize + 1; + let mut payload = Vec::with_capacity(nbytes); + for _ in 0..nbytes { + payload.push(br.read_u32(8)? as u8); + } + // `from_addbsil_and_payload` returns `Some` here unconditionally + // — the 6-bit field cannot exceed 63 and the payload length is + // built to match `addbsil + 1` exactly — but route through the + // safe constructor so the invariant is checked rather than + // asserted. + AdditionalBitStreamInfo::from_addbsil_and_payload(addbsil, payload) + } else { + None + }; + + let bits_consumed = br.bit_position(); + + if bsid > 10 { + // Per spec, base decoders mute for bsid > 8; we accept ≤10 as + // a small safety margin for near-compatible streams and defer + // a hard rejection to the decoder loop so probing still + // succeeds. + return Err(Error::Unsupported(format!( + "ac3: bsid {bsid} > 8 — Annex E E-AC-3 bitstream needs a separate parser" + ))); + } + + Ok(Bsi { + bsid, + bsmod, + acmod, + nfchans, + lfeon, + nchans, + dialnorm, + dialnorm_ch2, + cmixlev, + center_mix, + surmixlev, + surround_mix, + dsurmod, + dolby_surround_mode, + annex_d_mix_levels, + dmixmod, + dmixmod_preference, + compr, + compr_ch2, + language_code, + language_code_ch2, + dsurexmod, + dheadphonmod, + adconvtyp, + extra_bsi, + audio_production, + audio_production_ch2, + timecod1, + timecod2, + timecode_presence, + copyright_info, + addbsi, + bits_consumed, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a minimal BSI byte sequence for 2/0 stereo, LFE off, no + /// optional fields. acmod=2 → surmixlev/cmixlev absent, dsurmod + /// present. + /// + /// bsid=8 (5 bits) : 0b01000 + /// bsmod=0 (3 bits) : 0b000 + /// acmod=2 (3 bits) : 0b010 + /// dsurmod=0 (2) : 0b00 + /// lfeon=0 (1) : 0 + /// dialnorm=27 (5) : 0b11011 + /// compre=0 : 0 + /// langcode=0 : 0 + /// audprodie=0 : 0 + /// copyrightb=0 : 0 + /// origbs=0 : 0 + /// timecod1e=0 : 0 + /// timecod2e=0 : 0 + /// addbsie=0 : 0 + /// + /// Total = 5+3+3+2+1+5+1+1+1+1+1+1+1+1 = 27 bits → 4 bytes with 5 + /// trailing pad bits. + #[test] + fn parses_minimal_2_0_stereo_bsi() { + // Build via a BitWriter-style manual pack. + let bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), + (2, 0b00), + (1, 0), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let mut out = vec![0u8; 8]; + let mut bitpos = 0usize; + for (n, v) in bits.iter().copied() { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = bitpos / 8; + let shift = 7 - (bitpos % 8); + out[byte] |= bit << shift; + bitpos += 1; + } + } + + let b = parse(&out).unwrap(); + assert_eq!(b.bsid, 8); + assert_eq!(b.bsmod, 0); + assert_eq!(b.acmod, 2); + assert_eq!(b.nfchans, 2); + assert!(!b.lfeon); + assert_eq!(b.nchans, 2); + assert_eq!(b.dialnorm, 27); + assert_eq!(b.dsurmod, 0); + assert_eq!(b.cmixlev, 0xFF); + assert_eq!(b.surmixlev, 0xFF); + assert!(b.annex_d_mix_levels.is_none()); + assert_eq!(b.dmixmod, 0xFF); + assert_eq!(b.bits_consumed, bitpos as u64); + } + + #[test] + fn dialnorm_zero_remaps_to_31() { + // bsid=8, bsmod=0, acmod=1 (1/0 mono — no cmix / surmix / dsurmod), + // lfeon=0, dialnorm=0 → should remap. + let bits: [(u8, u32); 11] = [ + (5, 8), + (3, 0), + (3, 1), + (1, 0), // lfeon + (5, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let mut last4 = vec![0u8; 8]; + let mut bitpos = 0usize; + for (n, v) in bits.iter().copied() { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = bitpos / 8; + let shift = 7 - (bitpos % 8); + last4[byte] |= bit << shift; + bitpos += 1; + } + } + // Need addbsie + timecodes bits too — add three trailing zero bits + // to cover (timecod1e, timecod2e, addbsie) — wait, already in list. + // Actually this packs 5+3+3+1+5+... re-count: + // 5+3+3+1+5+1+1+1+1+1+1 = 23 bits. Missing nothing structural? + // For acmod=1 there's no cmix, surmix, dsurmod. After lfeon and + // dialnorm it goes compre/langcode/audprodie/copyrightb/origbs/ + // timecod1e/timecod2e/addbsie = 8 flags but we have 6. Add two + // more zero bits so the addbsie fires false. + let bits2: [(u8, u32); 2] = [(1, 0), (1, 0)]; + for (n, v) in bits2.iter().copied() { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = bitpos / 8; + let shift = 7 - (bitpos % 8); + last4[byte] |= bit << shift; + bitpos += 1; + } + } + + let b = parse(&last4).unwrap(); + assert_eq!(b.dialnorm, 31); + assert_eq!(b.nfchans, 1); + assert_eq!(b.nchans, 1); + } + + /// Annex D §2 / Table D2.1 — `bsid == 6` activates the alternate + /// syntax: the body's `timecod1e/timecod2e` slots become + /// `xbsi1e/xbsi2e`. Verify the xbsi1 mix-level fields surface on + /// [`Bsi::annex_d_mix_levels`] / [`Bsi::dmixmod`]. + #[test] + fn parses_annex_d_bsid_6_xbsi1_mix_levels() { + // 3/2 (acmod=7), lfe on. cmixlev = 0b00 (0.707), surmixlev = 0b00 + // (0.707). dialnorm=27. No compre / langcode / audprodie / + // copyrightb / origbs. xbsi1e = 1 with: + // dmixmod = 0b01 (LtRt preferred) + // ltrtcmixlev = 0b011 (0.841) + // ltrtsurmixlev = 0b100 (0.707) + // lorocmixlev = 0b100 (0.707) + // lorosurmixlev = 0b101 (0.595) + // xbsi2e = 0. addbsie = 0. + // + // Bit layout: + // bsid=6 (5) 00110 + // bsmod=0 (3) 000 + // acmod=7 (3) 111 + // cmixlev (2) 00 + // surmixlev (2) 00 + // lfeon (1) 1 + // dialnorm (5) 11011 + // compre (1) 0 + // langcode (1) 0 + // audprodie (1) 0 + // copyrightb (1) 0 + // origbs (1) 0 + // xbsi1e (1) 1 + // dmixmod (2) 01 + // ltrtcmixlev (3) 011 + // ltrtsurmixlev (3) 100 + // lorocmixlev (3) 100 + // lorosurmixlev (3) 101 + // xbsi2e (1) 0 + // addbsie (1) 0 + let bits: &[(u8, u32)] = &[ + (5, 6), + (3, 0), + (3, 7), + (2, 0), + (2, 0), + (1, 1), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 1), + (2, 0b01), + (3, 0b011), + (3, 0b100), + (3, 0b100), + (3, 0b101), + (1, 0), + (1, 0), + ]; + let mut out = vec![0u8; 8]; + let mut bitpos = 0usize; + for (n, v) in bits.iter().copied() { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = bitpos / 8; + let shift = 7 - (bitpos % 8); + out[byte] |= bit << shift; + bitpos += 1; + } + } + + let b = parse(&out).unwrap(); + assert_eq!(b.bsid, 6); + assert_eq!(b.acmod, 7); + assert!(b.lfeon); + assert_eq!(b.dmixmod, 0b01); + let mix = b.annex_d_mix_levels.expect("xbsi1 set → mix levels"); + assert_eq!(mix.ltrtcmixlev, 0b011); + assert_eq!(mix.ltrtsurmixlev, 0b100); + assert_eq!(mix.lorocmixlev, 0b100); + assert_eq!(mix.lorosurmixlev, 0b101); + assert_eq!(b.bits_consumed, bitpos as u64); + } + + /// `bsid == 6` with `xbsi1e == 0` should leave the mix-level + /// payload absent. The xbsi2e slot still needs to be consumed. + #[test] + fn parses_annex_d_bsid_6_no_xbsi1() { + // 2/0 (acmod=2), no LFE. cmixlev absent (acmod & 1 == 0). + // surmixlev absent (acmod & 4 == 0). dsurmod=0 (2 bits). + // dialnorm=20. xbsi1e=0. xbsi2e=0. addbsie=0. + let bits: &[(u8, u32)] = &[ + (5, 6), + (3, 0), + (3, 2), + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 20), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e + (1, 0), // xbsi2e + (1, 0), // addbsie + ]; + let mut out = vec![0u8; 8]; + let mut bitpos = 0usize; + for (n, v) in bits.iter().copied() { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = bitpos / 8; + let shift = 7 - (bitpos % 8); + out[byte] |= bit << shift; + bitpos += 1; + } + } + + let b = parse(&out).unwrap(); + assert_eq!(b.bsid, 6); + assert!(b.annex_d_mix_levels.is_none()); + assert_eq!(b.dmixmod, 0xFF); + assert_eq!(b.bits_consumed, bitpos as u64); + } + + /// Table D2.3 / D2.5 — the 3-bit center mix-level codewords map to + /// the exact gains the spec tabulates. + #[test] + fn annex_d_center_mix_gain_matches_table_d2_3() { + let expected: [(u8, f32); 8] = [ + (0b000, 1.414), + (0b001, 1.189), + (0b010, 1.000), + (0b011, 0.841), + (0b100, 0.707), + (0b101, 0.595), + (0b110, 0.500), + (0b111, 0.000), + ]; + for (code, gain) in expected { + assert!( + (annex_d_center_mix_gain(code) - gain).abs() < 1e-6, + "code 0b{code:03b}: want {gain}, got {}", + annex_d_center_mix_gain(code) + ); + } + } + + /// Table D2.4 / D2.6 — the 3-bit surround mix-level codewords. The + /// reserved codes `000/001/010` substitute 0.841 per spec. + #[test] + fn annex_d_surround_mix_gain_substitutes_reserved_with_0_841() { + // Reserved codes all map to 0.841. + for code in 0u8..=2 { + let g = annex_d_surround_mix_gain(code); + assert!( + (g - 0.841).abs() < 1e-6, + "reserved code 0b{code:03b} should resolve to 0.841, got {g}" + ); + } + let expected: [(u8, f32); 5] = [ + (0b011, 0.841), + (0b100, 0.707), + (0b101, 0.595), + (0b110, 0.500), + (0b111, 0.000), + ]; + for (code, gain) in expected { + assert!( + (annex_d_surround_mix_gain(code) - gain).abs() < 1e-6, + "code 0b{code:03b}: want {gain}, got {}", + annex_d_surround_mix_gain(code) + ); + } + } + + /// Table 5.7 — every `bsmod` codepoint except `0b111` resolves to a + /// fixed service type independent of `acmod`. Spot-check each row + /// with a couple of `acmod` values to confirm the resolver doesn't + /// peek at `acmod` when `bsmod != 0b111`. + #[test] + fn bsmod_table_5_7_fixed_codepoints() { + use BitStreamMode::*; + let rows: [(u8, BitStreamMode); 7] = [ + (0b000, CompleteMain), + (0b001, MusicAndEffects), + (0b010, VisuallyImpaired), + (0b011, HearingImpaired), + (0b100, Dialogue), + (0b101, Commentary), + (0b110, Emergency), + ]; + for (bsmod, want) in rows { + for acmod in 0u8..=7 { + let got = BitStreamMode::from_bsmod_acmod(bsmod, acmod); + assert_eq!( + got, want, + "bsmod=0b{bsmod:03b} acmod=0b{acmod:03b}: want {want:?}, got {got:?}" + ); + } + } + } + + /// Table 5.7 — `bsmod==0b111` is overloaded: acmod=0b001 → VoiceOver, + /// acmod ∈ {0b010..=0b111} → Karaoke, acmod=0b000 (the 1+1 dual-mono + /// slot) → Reserved (no Table 5.7 row defines it). + #[test] + fn bsmod_0b111_resolves_with_acmod() { + assert_eq!( + BitStreamMode::from_bsmod_acmod(0b111, 0b000), + BitStreamMode::Reserved + ); + assert_eq!( + BitStreamMode::from_bsmod_acmod(0b111, 0b001), + BitStreamMode::VoiceOver + ); + for acmod in 0b010u8..=0b111 { + assert_eq!( + BitStreamMode::from_bsmod_acmod(0b111, acmod), + BitStreamMode::Karaoke, + "acmod=0b{acmod:03b}" + ); + } + } + + /// `is_main` / `is_associated` partition Table 5.7 cleanly. CM, ME, + /// and karaoke are main; VI/HI/D/C/E/VO are associated; the unused + /// `bsmod=0b111 acmod=0b000` cell is neither. + #[test] + fn main_vs_associated_partition() { + use BitStreamMode::*; + let main = [CompleteMain, MusicAndEffects, Karaoke]; + let assoc = [ + VisuallyImpaired, + HearingImpaired, + Dialogue, + Commentary, + Emergency, + VoiceOver, + ]; + for m in main { + assert!(m.is_main(), "{m:?} should be main"); + assert!(!m.is_associated(), "{m:?} should not be associated"); + } + for a in assoc { + assert!(a.is_associated(), "{a:?} should be associated"); + assert!(!a.is_main(), "{a:?} should not be main"); + } + assert!(!Reserved.is_main()); + assert!(!Reserved.is_associated()); + } + + /// Mnemonics are stable per Table 5.7 — used in CLI / log output. + /// "?" is reserved for the Reserved case so downstream code can + /// rely on a single sentinel for "no service type". + #[test] + fn mnemonics_are_table_5_7_short_forms() { + use BitStreamMode::*; + let rows: [(BitStreamMode, &str); 10] = [ + (CompleteMain, "CM"), + (MusicAndEffects, "ME"), + (VisuallyImpaired, "VI"), + (HearingImpaired, "HI"), + (Dialogue, "D"), + (Commentary, "C"), + (Emergency, "E"), + (VoiceOver, "VO"), + (Karaoke, "K"), + (Reserved, "?"), + ]; + for (mode, mnem) in rows { + assert_eq!(mode.mnemonic(), mnem, "{mode:?}"); + } + } + + /// `Bsi::service_type()` round-trips the raw bsmod/acmod into the + /// typed enum. Reuses the minimal 2/0 stereo fixture (acmod=2, + /// bsmod=0) and a custom 1/0 mono bsmod=0b111 builder to cover + /// both the simple and overloaded branches end-to-end through the + /// `Bsi` accessor. + #[test] + fn bsi_service_type_accessor_routes_through_table_5_7() { + // The minimal 2/0 stereo fixture sets bsmod=0, acmod=2 → + // CompleteMain. Re-built locally so the test stays + // self-contained. + let stereo_bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), + (2, 0b00), + (1, 0), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let stereo_bytes = pack_bits(&stereo_bits); + let bsi = parse(&stereo_bytes).expect("parse minimal 2/0"); + assert_eq!(bsi.bsmod, 0b000); + assert_eq!(bsi.acmod, 0b010); + assert_eq!(bsi.service_type(), BitStreamMode::CompleteMain); + + // 1/0 mono BSI with bsmod=0b111 + acmod=0b001 → VoiceOver. + // acmod=1 means no cmix / no surmix / no dsurmod optional fields. + // bsid=8 (5) : 0b01000 + // bsmod=0b111 (3) : 0b111 + // acmod=0b001 (3) : 0b001 + // lfeon=0 (1) : 0 + // dialnorm=27 (5) : 0b11011 + // compre=0 (1) : 0 + // langcode=0 (1) : 0 + // audprodie=0 (1) : 0 + // copyrightb=0 (1) : 0 + // origbs=0 (1) : 0 + // timecod1e=0 (1) : 0 + // timecod2e=0 (1) : 0 + // addbsie=0 (1) : 0 + let voiceover_bits: [(u8, u32); 13] = [ + (5, 0b01000), + (3, 0b111), + (3, 0b001), + (1, 0), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let voiceover_bytes = pack_bits(&voiceover_bits); + let bsi = parse(&voiceover_bytes).expect("parse 1/0 voiceover"); + assert_eq!(bsi.bsmod, 0b111); + assert_eq!(bsi.acmod, 0b001); + assert_eq!(bsi.service_type(), BitStreamMode::VoiceOver); + } + + /// MSB-first bit packer matching the AC-3 `BitReader` order — used + /// by the Table 5.7 service-type tests to build synthetic BSIs. + fn pack_bits(bits: &[(u8, u32)]) -> Vec { + let total_bits: usize = bits.iter().map(|(n, _)| *n as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8) + 1]; + let mut bitpos = 0usize; + for (n, v) in bits.iter().copied() { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = bitpos / 8; + let shift = 7 - (bitpos % 8); + out[byte] |= bit << shift; + bitpos += 1; + } + } + out + } + + // --------------------------------------------------------------- + // Heavy compression gain (`compr`) — Table 7.30 / §7.7.2.2. + // --------------------------------------------------------------- + + /// X is a 4-bit signed integer with values in `-8..=+7`. Walk every + /// `X` codepoint with `Y = 0b1111` (max-Y) and assert the decoded + /// `(X, Y)` round-trip matches the bit layout described in §7.7.2.2. + #[test] + fn compression_gain_x_field_sign_extends_correctly() { + // (raw_x_nibble, expected signed value) — every codepoint. + let cases = [ + (0b0000u8, 0i8), + (0b0001, 1), + (0b0010, 2), + (0b0011, 3), + (0b0100, 4), + (0b0101, 5), + (0b0110, 6), + (0b0111, 7), + (0b1000, -8), + (0b1001, -7), + (0b1010, -6), + (0b1011, -5), + (0b1100, -4), + (0b1101, -3), + (0b1110, -2), + (0b1111, -1), + ]; + for (xn, x) in cases { + // Y = 0b1010 (arbitrary) — verify X decoding is independent of Y. + let cg = CompressionGain::from_byte((xn << 4) | 0b1010); + assert_eq!(cg.x(), x, "X mismatch for raw nibble {xn:#06b}"); + assert_eq!(cg.y(), 0b1010); + assert_eq!(cg.raw(), (xn << 4) | 0b1010); + } + } + + /// Table 7.30 row checks: the dB gain of each `(X, Y=0)` codepoint + /// must match the table's "Gain Indicated" column to within 0.005 dB. + /// At `Y=0`, the contribution from `Y` is exactly `-6.02 dB`, so the + /// table's "X alone = (X+1)*6.02 dB" sums with the Y attenuation to + /// `linear = 2^(X+1) * 0.5`, i.e. `(X+1)*6.02 - 6.02 = X*6.02 dB`. + /// Therefore the dB at `Y=0` equals `X * 6.02` (Table 7.30 minus + /// 6.02 dB across the board). + /// + /// Equivalently the table's headline rows (e.g. `X=7 → +48.16 dB`) + /// describe the X contribution **without** the Y attenuation; the + /// effective decoder gain when `Y = 0b1111` (`(16+15)/32 = 31/32 ≈ + /// -0.28 dB`) drops the headline by 0.276 dB. This test checks both + /// the headline (max-Y) and the bottom (Y=0) of every X row. + #[test] + fn compression_gain_table_7_30_db_endpoints() { + // (X, Y=15 dB ≈ headline - 0.276; Y=0 dB = headline - 6.02). + let cases = [ + (7i8, 48.16f32), + (6, 42.14), + (5, 36.12), + (4, 30.10), + (3, 24.08), + (2, 18.06), + (1, 12.04), + (0, 6.02), + (-1, 0.0), + (-2, -6.02), + (-3, -12.04), + (-4, -18.06), + (-5, -24.08), + (-6, -30.10), + (-7, -36.12), + (-8, -42.14), + ]; + for (x, headline_db) in cases { + // Pack X into the upper nibble (two's-complement 4-bit). + let xn = (x as i16 & 0xF) as u8; + // Y = 0b1111 → top of row, dB ≈ headline - 0.276. + let max_y = CompressionGain::from_byte((xn << 4) | 0b1111); + let max_y_db = max_y.decibels(); + assert!( + (max_y_db - (headline_db - 0.276)).abs() < 0.01, + "X={x} Y=15: got {max_y_db:.3} dB, want {:.3} dB", + headline_db - 0.276 + ); + // Y = 0b0000 → bottom of row, dB = headline - 6.02. + let min_y = CompressionGain::from_byte(xn << 4); + let min_y_db = min_y.decibels(); + assert!( + (min_y_db - (headline_db - 6.02)).abs() < 0.01, + "X={x} Y=0: got {min_y_db:.3} dB, want {:.3} dB", + headline_db - 6.02 + ); + } + } + + /// Y is a 4-bit unsigned mantissa with an implicit leading 1, read + /// as `(16 + Y) / 32`. Spot-check the four boundary values per + /// §7.7.2.2 ("Y can represent values between 0.111112 (or 31/32) and + /// 0.100002 (or 1/2)"). + #[test] + fn compression_gain_y_field_is_fractional_with_leading_one() { + // With X = -1 (= 0b1111, gain = 0 dB), linear = 1.0 * (16+Y)/32. + let cases = [ + (0u8, 16.0 / 32.0), // 0.5 + (1, 17.0 / 32.0), // 0.53125 + (15, 31.0 / 32.0), // 0.96875 + (8, 24.0 / 32.0), // 0.75 + ]; + for (y, expected) in cases { + let cg = CompressionGain::from_byte(0b1111_0000 | y); + let lin = cg.linear(); + assert!( + (lin - expected).abs() < 1e-6, + "X=-1 Y={y}: got linear={lin}, want {expected}" + ); + } + } + + /// Combined-range sanity per §7.7.2.2: + /// "The combination of X and Y values allows compr to indicate gain + /// changes from 48.16 – 0.28 = +47.89 dB, to –42.14 – 6.02 = + /// –48.16 dB." + #[test] + fn compression_gain_extreme_codepoints_match_spec_range() { + let top = CompressionGain::from_byte(0b0111_1111); // X=7, Y=15 + let bottom = CompressionGain::from_byte(0b1000_0000); // X=-8, Y=0 + + assert_eq!(top.x(), 7); + assert_eq!(top.y(), 15); + // Linear = 2^8 * 31/32 = 248. + assert!((top.linear() - 248.0).abs() < 1e-3); + // dB = 20*log10(248) ≈ +47.884 dB. + assert!((top.decibels() - 47.884).abs() < 0.01); + + assert_eq!(bottom.x(), -8); + assert_eq!(bottom.y(), 0); + // Linear = 2^-7 * 0.5 = 1/256. + assert!((bottom.linear() - 1.0 / 256.0).abs() < 1e-6); + // dB = 20*log10(1/256) ≈ -48.165 dB. + assert!((bottom.decibels() - (-48.165)).abs() < 0.01); + } + + /// `parse()` surfaces `compr` as `Some(CompressionGain)` when the + /// `compre` flag is set, and `None` otherwise. Build a 1/0 mono + /// BSI with `compre=1` and `compr=0b0001_0000` (X=1, Y=0, linear + /// `2^2 * 0.5 = 2.0`, ≈ +6.02 dB), then verify the parser routes + /// the byte verbatim into the typed surface. + #[test] + fn parse_surfaces_compr_when_compre_set() { + // 1/0 mono (acmod=1) → no cmixlev / surmixlev / dsurmod. + // bsid=8, bsmod=0, acmod=1, lfeon=0, dialnorm=27, + // compre=1, compr=0b0001_0000, langcode=0, audprodie=0, + // copyrightb=0, origbs=0, timecod1e=0, timecod2e=0, + // addbsie=0. + let bits: [(u8, u32); 13] = [ + (5, 8), + (3, 0), + (3, 1), + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 1), // compre + (8, 0b0001_0000), + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + addbsie folded as separate bits below + ]; + let mut bytes = pack_bits(&bits); + // Append one more zero bit for addbsie. + bytes.push(0); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.acmod, 1); + let cg = bsi.compr.expect("compre=1 should surface compr"); + assert_eq!(cg.raw(), 0b0001_0000); + assert_eq!(cg.x(), 1); + assert_eq!(cg.y(), 0); + assert!((cg.linear() - 2.0).abs() < 1e-6); + // 1+1 mode is acmod==0; for acmod==1 the Ch2 word stays None. + assert!(bsi.compr_ch2.is_none()); + } + + /// `parse()` leaves `compr` as `None` when `compre == 0`. + #[test] + fn parse_leaves_compr_none_when_compre_clear() { + // Reuse the minimal 2/0 BSI from `parses_minimal_2_0_stereo_bsi` + // — it has compre=0 by construction. + let bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), + (2, 0b00), + (1, 0), + (5, 27), + (1, 0), // compre + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let bytes = pack_bits(&bits); + let bsi = parse(&bytes).unwrap(); + assert!(bsi.compr.is_none()); + assert!(bsi.compr_ch2.is_none()); + } + + /// 1+1 dual-mono (`acmod == 0`) carries a second `compr2` word for + /// Ch2 with identical Table 7.30 semantics per §5.4.2.18 ("This + /// 8-bit word has the same meaning as compr, except that it applies + /// to the second audio channel"). Build a 1+1 BSI with `compre=1` + /// (X=-1, Y=15 ≈ -0.276 dB on Ch1) and `compr2e=1` (X=-8, Y=0 ≈ + /// -48.16 dB on Ch2) and verify both surface independently. + #[test] + fn parse_surfaces_compr_ch2_in_dual_mono() { + // acmod=0 (1+1 dual mono): no cmix/surmix/dsurmod, lfeon possible. + // bsid=8, bsmod=0, acmod=0, lfeon=0, dialnorm=27, + // compre=1, compr=0b1111_1111, + // langcode=0, audprodie=0, + // /* 1+1 second block */ + // dialnorm2=27, compr2e=1, compr2=0b1000_0000, + // langcod2e=0, audprodi2e=0, + // copyrightb=0, origbs=0, timecod1e=0, timecod2e=0, addbsie=0. + let bits: [(u8, u32); 18] = [ + (5, 8), + (3, 0), + (3, 0), + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 1), // compre + (8, 0b1111_1111), + (1, 0), // langcode + (1, 0), // audprodie + (5, 27), + (1, 1), // compr2e + (8, 0b1000_0000), + (1, 0), // langcod2e + (1, 0), // audprodi2e + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + ]; + let mut bytes = pack_bits(&bits); + bytes.push(0); // addbsie + pad + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.acmod, 0); + let c1 = bsi.compr.expect("compre=1"); + assert_eq!(c1.raw(), 0b1111_1111); + assert!((c1.decibels() - (-0.276)).abs() < 0.01); + let c2 = bsi.compr_ch2.expect("compr2e=1"); + assert_eq!(c2.raw(), 0b1000_0000); + assert!((c2.decibels() - (-48.165)).abs() < 0.01); + } + + // --------------------------------------------------------------- + // §5.4.2.8 / §5.4.2.16 — dialogue normalization typed surface. + // --------------------------------------------------------------- + + /// Every legal wire codepoint `1..=31` maps to itself unchanged + /// and to `-N dB` per §5.4.2.8 ("interpreted as -1 dB to -31 dB"). + #[test] + fn dialnorm_decodes_every_legal_wire_codepoint() { + for wire in 1u8..=31u8 { + let dn = DialNorm::from_wire(wire); + assert_eq!(dn.codepoint(), wire); + assert_eq!(dn.wire_value(), wire); + assert!(!dn.is_reserved_wire_codepoint()); + assert_eq!(dn.db(), -(wire as i8)); + assert_eq!(dn.level_below_full_scale_db(), wire); + } + } + + /// The reserved `0` wire codepoint remaps to `31` per §5.4.2.8 + /// ("If the reserved value of 0 is received, the decoder shall + /// use -31 dB"). The remap is observable via + /// [`DialNorm::is_reserved_wire_codepoint`] so a careful consumer + /// can distinguish a legitimate `31` codepoint from the reserved- + /// remap path; [`DialNorm::wire_value`] recovers the original `0` + /// for byte-exact re-emission. + #[test] + fn dialnorm_zero_wire_codepoint_remaps_to_31_with_reserved_flag() { + let dn = DialNorm::from_wire(0); + assert_eq!(dn.codepoint(), 31); + assert_eq!(dn.wire_value(), 0); + assert!(dn.is_reserved_wire_codepoint()); + assert_eq!(dn.db(), -31); + assert_eq!(dn.level_below_full_scale_db(), 31); + // A "real" 31 codepoint reports the same dB / codepoint but is + // distinguishable from the reserved path. + let dn31 = DialNorm::from_wire(31); + assert_eq!(dn31.codepoint(), 31); + assert_eq!(dn31.wire_value(), 31); + assert!(!dn31.is_reserved_wire_codepoint()); + assert_ne!(dn, dn31); + } + + /// Bits above the 5-bit field are masked off — `from_wire` consumes + /// only the low 5 bits per the BSI bit-reader contract. + #[test] + fn dialnorm_only_consumes_low_5_bits() { + let dn = DialNorm::from_wire(0b1110_1011); // low5=01011=11 + assert_eq!(dn.codepoint(), 11); + assert_eq!(dn.db(), -11); + } + + /// Linear attenuation matches `10^(dB/20)` per the standard + /// dB-to-linear conversion. `-1 dB` ≈ 0.8913, `-31 dB` ≈ 0.02818, + /// `-25 dB` ≈ 0.05623. + #[test] + fn dialnorm_attenuation_linear_matches_dbgain() { + let cases: [(u8, f32); 3] = [(1, 0.8913), (25, 0.0562), (31, 0.0282)]; + for (wire, expected) in cases { + let got = DialNorm::from_wire(wire).attenuation_linear(); + assert!( + (got - expected).abs() < 1e-3, + "wire={wire}: got {got}, expected {expected}" + ); + } + } + + /// §7.6 worked example — listener target 67 dB SPL, reference + /// full-scale 105 dB SPL, dialnorm = -25 dB → playback gain + /// `67 - 105 + 25 = -13 dB` (so full-scale digital reproduces at + /// `105 - 13 = 92 dB SPL`, matching the spec text "full scale + /// digital signals reproduce at a sound pressure level of 92 dB"). + /// Linear gain `10^(-13/20) ≈ 0.2239`. + #[test] + fn dialnorm_reproduction_gain_matches_spec_7_6_example() { + let dn = DialNorm::from_wire(25); + let gain = dn.reproduction_gain_linear(67.0, 105.0); + let expected = 10.0f32.powf(-13.0 / 20.0); + assert!( + (gain - expected).abs() < 1e-4, + "got {gain}, expected {expected}" + ); + } + + /// `parse()` surfaces [`Bsi::dialnorm`] as the post-remap u8 (kept + /// for backward compatibility) AND exposes the typed + /// [`Bsi::dialogue_normalization`] accessor. The typed view loses + /// the reserved-wire-codepoint distinction since the post-remap + /// `dialnorm` field has already collapsed it; callers needing the + /// raw codepoint check the field directly. + #[test] + fn parse_surfaces_dialogue_normalization_accessor() { + // 2/0 stereo, dialnorm=20, all optional metadata off. + let bits: [(u8, u32); 14] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 2), // acmod=2 (2/0 stereo) + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 20), // dialnorm = -20 dB + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let mut bytes = pack_bits(&bits); + bytes.push(0); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.dialnorm, 20); + assert!(bsi.dialnorm_ch2.is_none()); + let dn = bsi.dialogue_normalization(); + assert_eq!(dn.codepoint(), 20); + assert_eq!(dn.db(), -20); + assert_eq!(dn.level_below_full_scale_db(), 20); + assert!(bsi.dialogue_normalization_ch2().is_none()); + } + + /// 1+1 dual-mono (`acmod == 0`) carries a second `dialnorm2` word + /// for Ch2 with identical §5.4.2.8 semantics per §5.4.2.16 + /// ("This 5-bit code has the same meaning as dialnorm"). Build a + /// 1+1 BSI with Ch1 dialnorm=27 (-27 dB) and Ch2 dialnorm2=11 + /// (-11 dB) and verify both surface independently — Ch2 via the + /// new `dialnorm_ch2` field + `dialogue_normalization_ch2()` + /// accessor. + #[test] + fn parse_surfaces_dialnorm_ch2_in_dual_mono() { + let bits: [(u8, u32); 18] = [ + (5, 8), + (3, 0), + (3, 0), // acmod=0 (1+1 dual mono) + (1, 0), // lfeon + (5, 27), // dialnorm = -27 dB + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (5, 11), // dialnorm2 = -11 dB + (1, 0), // compr2e + (1, 0), // langcod2e + (1, 0), // audprodi2e + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + (1, 0), // pad + ]; + let mut bytes = pack_bits(&bits); + bytes.push(0); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.acmod, 0); + assert_eq!(bsi.dialnorm, 27); + let dn_ch2 = bsi + .dialnorm_ch2 + .expect("acmod == 0 should surface dialnorm_ch2"); + assert_eq!(dn_ch2, 11); + let typed = bsi + .dialogue_normalization_ch2() + .expect("dialnorm_ch2 surfaced"); + assert_eq!(typed.codepoint(), 11); + assert_eq!(typed.db(), -11); + // Ch1 surface is independent. + assert_eq!(bsi.dialogue_normalization().codepoint(), 27); + } + + /// 1+1 dual-mono Ch2 with the reserved `dialnorm2 = 0` wire + /// codepoint remaps to `31` per §5.4.2.8 (reused by §5.4.2.16), + /// matching the Ch1 remap. The post-remap `31` is what the parser + /// stores; the wire-reserved-bit distinction is only available + /// via `DialNorm::from_wire(0)` on a freshly built value, not via + /// the BSI surface (since the BSI field is the remapped value + /// only — same shape as Ch1's existing `dialnorm: u8`). + #[test] + fn parse_remaps_dialnorm2_zero_codepoint_to_31() { + let bits: [(u8, u32); 18] = [ + (5, 8), + (3, 0), + (3, 0), // acmod=0 + (1, 0), + (5, 27), // dialnorm = -27 dB (legitimate) + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (5, 0), // dialnorm2 = reserved 0 → remaps to 31 + (1, 0), // compr2e + (1, 0), // langcod2e + (1, 0), // audprodi2e + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + (1, 0), + ]; + let mut bytes = pack_bits(&bits); + bytes.push(0); + let bsi = parse(&bytes).unwrap(); + let dn_ch2 = bsi.dialnorm_ch2.expect("acmod == 0"); + assert_eq!(dn_ch2, 31); + let typed = bsi + .dialogue_normalization_ch2() + .expect("dialnorm_ch2 surfaced"); + assert_eq!(typed.codepoint(), 31); + assert_eq!(typed.db(), -31); + } + + /// Non-1+1 streams (`acmod != 0`) never carry `dialnorm2` per + /// §5.4.2.16's "applies to the second audio channel when acmod + /// indicates two independent channels (dual mono 1+1 mode)". The + /// `dialnorm_ch2` field stays `None` for every other `acmod`. + #[test] + fn parse_leaves_dialnorm_ch2_none_outside_dual_mono() { + // 2/0 stereo (acmod=2) baseline. + let bits: [(u8, u32); 14] = [ + (5, 8), + (3, 0), + (3, 2), // acmod=2 + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let mut bytes = pack_bits(&bits); + bytes.push(0); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.acmod, 2); + assert!(bsi.dialnorm_ch2.is_none()); + assert!(bsi.dialogue_normalization_ch2().is_none()); + } + + // --------------------------------------------------------------- + // Annex D §2.3.1.7-10 — xbsi2 informational metadata. + // --------------------------------------------------------------- + + /// Table D2.7 — `dsurexmod` decodes verbatim across all 4 codepoints. + #[test] + fn dsurexmod_decodes_all_4_codepoints() { + use DolbySurroundExMode::*; + assert_eq!(DolbySurroundExMode::from_code(0b00), NotIndicated); + assert_eq!(DolbySurroundExMode::from_code(0b01), NotEncoded); + assert_eq!( + DolbySurroundExMode::from_code(0b10), + SurroundExOrProLogicIIx + ); + assert_eq!(DolbySurroundExMode::from_code(0b11), ProLogicIIz); + // raw() round-trip. + for code in 0u8..4 { + assert_eq!(DolbySurroundExMode::from_code(code).raw(), code); + } + } + + /// Table D2.8 — `dheadphonmod` decodes verbatim. The `'11'` + /// codepoint is `Reserved`; the spec instructs decoders to keep + /// reproducing audio when it appears. + #[test] + fn dheadphonmod_decodes_all_4_codepoints() { + use DolbyHeadphoneMode::*; + assert_eq!(DolbyHeadphoneMode::from_code(0b00), NotIndicated); + assert_eq!(DolbyHeadphoneMode::from_code(0b01), NotEncoded); + assert_eq!(DolbyHeadphoneMode::from_code(0b10), Encoded); + assert_eq!(DolbyHeadphoneMode::from_code(0b11), Reserved); + for code in 0u8..4 { + assert_eq!(DolbyHeadphoneMode::from_code(code).raw(), code); + } + } + + /// Table D2.9 — `adconvtyp` is a single bit (`Standard` vs `Hdcd`). + #[test] + fn adconvtyp_decodes_both_codepoints() { + assert_eq!(AdConverterType::from_code(0), AdConverterType::Standard); + assert_eq!(AdConverterType::from_code(1), AdConverterType::Hdcd); + // Defensive — `from_code` masks the low bit. + assert_eq!(AdConverterType::from_code(2), AdConverterType::Standard); + assert_eq!(AdConverterType::from_code(3), AdConverterType::Hdcd); + assert_eq!(AdConverterType::Standard.raw(), 0); + assert_eq!(AdConverterType::Hdcd.raw(), 1); + } + + /// Annex D §2.3.1.7 — `bsid == 6` with `xbsi2e == 1` surfaces the + /// three typed playback hints on the parsed [`Bsi`]. Build a 3/2 + /// frame (acmod=7) with `xbsi1e == 0` (mix-level extensions + /// absent), `xbsi2e == 1`, and Table D2.7 / D2.8 / D2.9 codepoints + /// `(0b10, 0b00, 0b1)` — Dolby Surround EX on, headphone hint not + /// indicated, HDCD source. The body `xbsi2(8)` + `encinfo(1)` + /// reserved fields are populated with non-zero bits to verify the + /// parser skips them but still surfaces the three typed fields. + #[test] + fn parse_surfaces_xbsi2_dsurexmod_dheadphonmod_adconvtyp() { + // bsid=6 (5), bsmod=0 (3), acmod=7 (3), cmixlev=0 (2), + // surmixlev=0 (2), lfeon=0 (1), dialnorm=27 (5), + // compre=0, langcode=0, audprodie=0, copyrightb=0, origbs=0, + // xbsi1e=0, + // xbsi2e=1, dsurexmod=0b10 (Surround EX / PLIIx), + // dheadphonmod=0b00 (NotIndicated), + // adconvtyp=0b1 (Hdcd), + // xbsi2=0b1010_1010 (reserved garbage — must be + // parsed-and-discarded), + // encinfo=0b1, + // addbsie=0. + let bits: [(u8, u32); 20] = [ + (5, 6), + (3, 0), + (3, 7), + (2, 0), + (2, 0), + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e=0 + (1, 1), // xbsi2e=1 + (2, 0b10), // dsurexmod = Surround EX / PLIIx + (2, 0b00), // dheadphonmod = NotIndicated + (1, 0b1), // adconvtyp = HDCD + (8, 0b1010_1010), // xbsi2 (reserved garbage) + (1, 0b1), // encinfo (encoder-private) + (1, 0), // addbsie + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + assert_eq!(b.acmod, 7); + assert!(b.annex_d_mix_levels.is_none()); + assert_eq!( + b.dsurexmod, + Some(DolbySurroundExMode::SurroundExOrProLogicIIx) + ); + assert_eq!(b.dheadphonmod, Some(DolbyHeadphoneMode::NotIndicated)); + assert_eq!(b.adconvtyp, Some(AdConverterType::Hdcd)); + } + + /// `bsid != 6` falls through the §5.3.2 base syntax — the + /// `xbsi2e` block doesn't exist, so the three Annex D fields stay + /// `None`. Use the round-202 `parses_minimal_2_0_stereo_bsi` + /// fixture (bsid=8, 2/0 stereo) and just assert the new fields. + #[test] + fn parse_leaves_xbsi2_fields_none_outside_bsid_6() { + // Identical layout to `parses_minimal_2_0_stereo_bsi`. + let bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), + (2, 0b00), + (1, 0), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 8); + assert!(b.dsurexmod.is_none()); + assert!(b.dheadphonmod.is_none()); + assert!(b.adconvtyp.is_none()); + assert!(b.extra_bsi.is_none()); + } + + // --------------------------------------------------------------- + // ExtraBsi2 — §2.3.1.11-12 reserved + encoder-private trailer. + // --------------------------------------------------------------- + + /// `from_raw` preserves the `xbsi2` byte and `encinfo` bit verbatim + /// across every combination — no validation per §2.3.1.11-12 (both + /// fields are reserved, so any 8+1-bit combination is wire-legal). + /// Also exercises the `Eq` + `Copy` semantics by passing the value + /// to a sibling helper after the implicit move. + #[test] + fn extra_bsi2_from_raw_round_trips_every_byte() { + // Every 8-bit `xbsi2` codepoint × both `encinfo` values. + for xbsi2 in 0u8..=255u8 { + for &enc in &[false, true] { + let x = ExtraBsi2::from_raw(xbsi2, enc); + assert_eq!(x.xbsi2(), xbsi2); + assert_eq!(x.encinfo(), enc); + assert_eq!(x.wire_bits(), 9); + // Copy: `x` survives the implicit move. + let y = x; + assert_eq!(x, y); + } + } + } + + /// `is_spec_reserved_value` flags only the §2.3.1.11 wire-conformance + /// `xbsi2 == 0x00` byte; `encinfo` is excluded from the check per + /// §2.3.1.12 (encoder-private, any value is wire-legal). + #[test] + fn extra_bsi2_predicate_only_accepts_zero_xbsi2() { + // `xbsi2 == 0x00` is the only spec-conformant codepoint. + assert!(ExtraBsi2::from_raw(0x00, false).is_spec_reserved_value()); + assert!(ExtraBsi2::from_raw(0x00, true).is_spec_reserved_value()); + // Any non-zero `xbsi2` byte fails the conformance check. + for xbsi2 in 1u8..=255u8 { + assert!(!ExtraBsi2::from_raw(xbsi2, false).is_spec_reserved_value()); + assert!(!ExtraBsi2::from_raw(xbsi2, true).is_spec_reserved_value()); + } + } + + /// Annex D `bsid == 6` with `xbsi2e == 1` surfaces the typed + /// [`ExtraBsi2`] alongside the pre-existing + /// `dsurexmod`/`dheadphonmod`/`adconvtyp` triplet. Confirms the + /// raw `xbsi2 == 0xAA` byte + `encinfo == 1` round-trip through + /// `parse()` and that `is_spec_reserved_value` reports `false` + /// for the non-conformant byte. Layout cloned from the existing + /// `parse_surfaces_xbsi2_dsurexmod_dheadphonmod_adconvtyp` so the + /// new field is the only behaviour change. + #[test] + fn parse_surfaces_extra_bsi2_nonzero_codepoint() { + let bits: [(u8, u32); 20] = [ + (5, 6), // bsid + (3, 0), // bsmod + (3, 7), // acmod (3/2) + (2, 0), // cmixlev + (2, 0), // surmixlev + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e + (1, 1), // xbsi2e = 1 + (2, 0b10), // dsurexmod + (2, 0b00), // dheadphonmod + (1, 0b1), // adconvtyp = HDCD + (8, 0b1010_1010), // xbsi2 (non-conformant codepoint) + (1, 0b1), // encinfo = 1 + (1, 0), // addbsie + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + let extra = b.extra_bsi.expect("xbsi2e == 1 surfaces an ExtraBsi2"); + assert_eq!(extra.xbsi2(), 0b1010_1010); + assert!(extra.encinfo()); + assert!(!extra.is_spec_reserved_value()); + } + + /// Spec-conformant Annex D xbsi2 — `xbsi2 == 0x00` per §2.3.1.11, + /// `encinfo == 0` (encoder did not stash a private bit). The + /// `is_spec_reserved_value` predicate reports `true`. The typed + /// surface is `Some`, distinguishing "conformant emitted block" + /// from "block absent" (which is `None`). + #[test] + fn parse_surfaces_extra_bsi2_conformant_zero_codepoint() { + let bits: [(u8, u32); 20] = [ + (5, 6), // bsid + (3, 0), // bsmod + (3, 2), // acmod (2/0 stereo) + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e + (1, 1), // xbsi2e = 1 + (2, 0b00), // dsurexmod = NotIndicated + (2, 0b10), // dheadphonmod = Encoded + (1, 0b0), // adconvtyp = Standard + (8, 0x00), // xbsi2 = spec-conformant zero + (1, 0b0), // encinfo = 0 + (1, 0), // addbsie + (1, 0), // padding (keeps the [(u8,u32); 20] cardinality) + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + let extra = b.extra_bsi.expect("xbsi2e == 1 surfaces an ExtraBsi2"); + assert_eq!(extra.xbsi2(), 0x00); + assert!(!extra.encinfo()); + assert!(extra.is_spec_reserved_value()); + // Cross-check the sibling typed fields surface correctly too — + // a 2/0 frame is the only mode where `dheadphonmod` is semantically + // defined per §2.3.1.9. + assert_eq!(b.dheadphonmod, Some(DolbyHeadphoneMode::Encoded)); + assert_eq!(b.adconvtyp, Some(AdConverterType::Standard)); + } + + /// `bsid == 6` with `xbsi2e == 0` short-circuits the xbsi2 block — + /// `extra_bsi` stays `None` even though the gating flag bit is on + /// the wire. Confirms the parser distinguishes "block absent" + /// (`None`) from "block present, all-zero" (`Some(0x00, false)`). + #[test] + fn parse_leaves_extra_bsi2_none_when_xbsi2e_zero() { + let bits: [(u8, u32); 14] = [ + (5, 6), // bsid + (3, 0), // bsmod + (3, 2), // acmod (2/0) + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e + (1, 0), // xbsi2e = 0 + (1, 0), // addbsie + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + assert!(b.extra_bsi.is_none()); + // Companion fields also stay None per the gate. + assert!(b.dsurexmod.is_none()); + assert!(b.dheadphonmod.is_none()); + assert!(b.adconvtyp.is_none()); + } + + /// §5.4.2.15 / Table 5.12 — every codepoint of `roomtyp` decodes + /// to its named variant and round-trips through `raw()`. + #[test] + fn room_type_table_5_12_round_trip() { + for (code, want) in [ + (0u8, RoomType::NotIndicated), + (1, RoomType::LargeXCurve), + (2, RoomType::SmallFlat), + (3, RoomType::Reserved), + ] { + let got = RoomType::from_code(code); + assert_eq!(got, want, "code={code:02b}"); + assert_eq!(got.raw(), code, "raw round-trip: code={code:02b}"); + } + // Upper 6 bits of input are ignored — only the low 2 bits matter. + assert_eq!(RoomType::from_code(0b1111_1110), RoomType::SmallFlat); + } + + /// §5.4.2.14 — `mixlevel` is the 5-bit code, peak SPL is + /// `80 + mixlevel` dB. Spot the endpoints (`0` → 80 dB SPL, + /// `31` → 111 dB SPL) and a typical mid-range value + /// (`mixlevel = 5` → 85 dB SPL, ITU-R BS.775 reference monitor). + #[test] + fn audio_production_info_peak_db_spl_endpoints() { + let lo = AudioProductionInfo { + mixlevel: 0, + roomtyp: RoomType::NotIndicated, + }; + assert_eq!(lo.peak_mix_level_db_spl(), 80); + let mid = AudioProductionInfo { + mixlevel: 5, + roomtyp: RoomType::LargeXCurve, + }; + assert_eq!(mid.peak_mix_level_db_spl(), 85); + let hi = AudioProductionInfo { + mixlevel: 31, + roomtyp: RoomType::SmallFlat, + }; + assert_eq!(hi.peak_mix_level_db_spl(), 111); + } + + /// `parse()` surfaces `audprodie==1` into a typed + /// [`AudioProductionInfo`] with the 5-bit `mixlevel` and Table 5.12 + /// `roomtyp` taken verbatim from the wire. Build a 1/0 mono BSI + /// (`acmod=1`) with `audprodie=1`, mixlevel=0b10101 (85 dB SPL), + /// roomtyp=0b01 (`LargeXCurve`), and verify both decode correctly. + /// `audio_production_ch2` stays `None` because the stream is not + /// 1+1 dual-mono. + #[test] + fn parse_surfaces_audio_production_when_audprodie_set() { + let bits: [(u8, u32); 16] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod = 1/0 mono → no cmix/surmix/dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 1), // audprodie = 1 + (5, 0b10101), // mixlevel = 21 → 101 dB SPL + (2, 0b01), // roomtyp = LargeXCurve + // No 1+1 mirror — acmod != 0. + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + (1, 0), // pad + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 1); + let ap = b + .audio_production + .expect("audprodie=1 should surface audio_production"); + assert_eq!(ap.mixlevel, 0b10101); + assert_eq!(ap.peak_mix_level_db_spl(), 80 + 21); + assert_eq!(ap.roomtyp, RoomType::LargeXCurve); + // Not 1+1 dual-mono → no Ch2 mirror. + assert!(b.audio_production_ch2.is_none()); + } + + /// `audprodie==0` leaves [`Bsi::audio_production`] as `None`. The + /// existing `parses_minimal_2_0_stereo_bsi` fixture exercises this + /// case (it clears `audprodie`), so just re-pack a minimal 2/0 + /// stream and assert. + #[test] + fn parse_leaves_audio_production_none_when_audprodie_clear() { + let bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), + (2, 0b00), + (1, 0), + (5, 27), + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie = 0 + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), + (1, 0), + (1, 0), + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert!(b.audio_production.is_none()); + assert!(b.audio_production_ch2.is_none()); + } + + /// 1+1 dual-mono (`acmod == 0`) emits an independent `audprodi2e` + /// chain for Ch2 per §5.4.2.21-23. Build a stream with Ch1 + /// audprodie=1 (mixlevel=8, roomtyp=SmallFlat) AND Ch2 + /// audprodi2e=1 (mixlevel=0, roomtyp=NotIndicated) and verify both + /// fields surface independently. + #[test] + fn parse_surfaces_audio_production_ch2_in_dual_mono() { + let bits: [(u8, u32); 20] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 0), // acmod = 0 (1+1 dual-mono) + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 1), // audprodie = 1 + (5, 0b01000), // mixlevel = 8 → 88 dB SPL + (2, 0b10), // roomtyp = SmallFlat + // 1+1 second block. + (5, 27), // dialnorm2 + (1, 0), // compr2e + (1, 0), // langcod2e + (1, 1), // audprodi2e = 1 + (5, 0b00000), // mixlevel2 = 0 → 80 dB SPL + (2, 0b00), // roomtyp2 = NotIndicated + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + ]; + let mut bytes = pack_bits(&bits); + bytes.push(0); // addbsie pad + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 0); + let ap1 = b + .audio_production + .expect("audprodie=1 should surface Ch1 production"); + assert_eq!(ap1.mixlevel, 8); + assert_eq!(ap1.peak_mix_level_db_spl(), 88); + assert_eq!(ap1.roomtyp, RoomType::SmallFlat); + let ap2 = b + .audio_production_ch2 + .expect("audprodi2e=1 should surface Ch2 production"); + assert_eq!(ap2.mixlevel, 0); + assert_eq!(ap2.peak_mix_level_db_spl(), 80); + assert_eq!(ap2.roomtyp, RoomType::NotIndicated); + } + + // --------------------------------------------------------------- + // Time code (`timecod1` / `timecod2`) — §5.4.2.26-28 / Table 5.13. + // --------------------------------------------------------------- + + /// [`TimeCode1`] splits its 14 wire bits as 5+6+3 (hours, minutes, + /// 8-second increments). Walk a few hand-packed codepoints and + /// verify each accessor lifts the right slice. + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn timecode1_field_decomposition_matches_spec() { + // (raw14, hours, minutes, eight_second_increments) + let cases: [(u16, u8, u8, u8); 6] = [ + // All zeroes → 00:00:00. + (0b00000_000000_000, 0, 0, 0), + // Maximum spec-valid: 23 h, 59 m, 56 s (7×8). + (0b10111_111011_111, 23, 59, 7), + // Minimal hour bump: 01:00:00. + (0b00001_000000_000, 1, 0, 0), + // Minute boundary: 00:59:00. + (0b00000_111011_000, 0, 59, 0), + // Eight-second boundary at 00:00:48 (8×6). + (0b00000_000000_110, 0, 0, 6), + // Out-of-range hour codepoint (24..=31) per §5.4.2.27 — the + // wire layout reserves these values; the parser still + // surfaces them so a careful consumer can decide. + (0b11111_111111_111, 31, 63, 7), + ]; + for (raw, h, m, s8) in cases { + let tc = TimeCode1::from_raw(raw); + assert_eq!(tc.raw(), raw & 0x3FFF, "raw mask, raw={raw:#018b}"); + assert_eq!(tc.hours(), h, "hours, raw={raw:#018b}"); + assert_eq!(tc.minutes(), m, "minutes, raw={raw:#018b}"); + assert_eq!( + tc.eight_second_increments(), + s8, + "8-second increments, raw={raw:#018b}" + ); + // seconds_in_day = h·3600 + m·60 + s8·8. + let want_secs = (h as u32) * 3600 + (m as u32) * 60 + (s8 as u32) * 8; + assert_eq!(tc.seconds_in_day(), want_secs); + } + } + + /// `TimeCode1::is_spec_valid()` flags out-of-range hours / minutes. + /// The eight-second-increment field cannot escape its 3-bit + /// 0..=7 range. + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn timecode1_spec_valid_checks_hours_and_minutes() { + // 23:59:56 is the maximum valid combination. + assert!(TimeCode1::from_raw(0b10111_111011_111).is_spec_valid()); + // hours = 24 (reserved). + assert!(!TimeCode1::from_raw(0b11000_111011_111).is_spec_valid()); + // minutes = 60 (reserved). + assert!(!TimeCode1::from_raw(0b10111_111100_111).is_spec_valid()); + // hours = 0, minutes = 0, s8 = 0 is also valid. + assert!(TimeCode1::from_raw(0).is_spec_valid()); + } + + /// [`TimeCode2`] splits its 14 wire bits as 3+5+6 (seconds, frames, + /// frame fractions). + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn timecode2_field_decomposition_matches_spec() { + // (raw14, seconds, frames, frame_fractions) + let cases: [(u16, u8, u8, u8); 5] = [ + // All zeroes. + (0b000_00000_000000, 0, 0, 0), + // Maximum spec-valid: s=7, f=29, ff=63. + (0b111_11101_111111, 7, 29, 63), + // Seconds boundary: s=7, f=0, ff=0. + (0b111_00000_000000, 7, 0, 0), + // Frames boundary at 30 fps (frames=29 is the max valid). + (0b000_11101_000000, 0, 29, 0), + // Out-of-range frames (30, 31) per §5.4.2.28 — codepoints + // beyond the 30 fps reference; pass-through for caller + // inspection. + (0b000_11111_111111, 0, 31, 63), + ]; + for (raw, s, f, ff) in cases { + let tc = TimeCode2::from_raw(raw); + assert_eq!(tc.raw(), raw & 0x3FFF, "raw mask, raw={raw:#018b}"); + assert_eq!(tc.seconds(), s, "seconds, raw={raw:#018b}"); + assert_eq!(tc.frames(), f, "frames, raw={raw:#018b}"); + assert_eq!(tc.frame_fractions(), ff, "frame fractions, raw={raw:#018b}"); + } + } + + /// `TimeCode2::is_spec_valid()` rejects out-of-range frame + /// codepoints (≥ 30 at the 30 fps reference assumed by §5.4.2.26). + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn timecode2_spec_valid_checks_frames() { + assert!(TimeCode2::from_raw(0b111_11101_111111).is_spec_valid()); // f=29 + assert!(!TimeCode2::from_raw(0b000_11110_000000).is_spec_valid()); // f=30 + assert!(!TimeCode2::from_raw(0b000_11111_000000).is_spec_valid()); // f=31 + assert!(TimeCode2::from_raw(0).is_spec_valid()); // all zero is valid + } + + /// Table 5.13 — the `(timecod2e, timecod1e)` pair maps to a + /// presence-pattern enum. Walk every codepoint. + #[test] + fn timecode_presence_table_5_13_round_trip() { + use TimeCodePresence::*; + let rows: [(bool, bool, TimeCodePresence); 4] = [ + (false, false, NotPresent), + (false, true, FirstHalfOnly), + (true, false, SecondHalfOnly), + (true, true, BothHalves), + ]; + for (tc2e, tc1e, want) in rows { + let got = TimeCodePresence::from_flags(tc2e, tc1e); + assert_eq!( + got, want, + "(timecod2e={tc2e}, timecod1e={tc1e}): want {want:?}, got {got:?}" + ); + } + } + + /// `parse()` surfaces `timecod1` / `timecod2` independently when + /// each `timecod*e` flag is set. Build a 1/0 mono BSI carrying + /// `(h=12, m=34, s8=5, s=3, f=15, ff=42)` and verify the parser + /// routes both halves into the typed surface plus + /// `timecode_presence == BothHalves`. + #[test] + fn parse_surfaces_both_timecode_halves() { + // tc1 raw = h(5)·512 + m(6)·8 + s8(3) packed MSB-first as + // (12 << 9) | (34 << 3) | 5 = 0x18 << 9 | 0x22 << 3 | 5 + // = 0b01100_100010_101 + // tc2 raw = s(3)·2048 + f(5)·64 + ff(6) packed MSB-first as + // (3 << 11) | (15 << 6) | 42 + // = 0b011_01111_101010 + let tc1_raw: u32 = (12u32 << 9) | (34u32 << 3) | 5u32; + let tc2_raw: u32 = (3u32 << 11) | (15u32 << 6) | 42u32; + let bits: [(u8, u32); 15] = [ + (5, 8), // bsid (base syntax) + (3, 0), // bsmod + (3, 1), // acmod = 1 (1/0 mono, no cmix / surmix / dsurmod) + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 1), // timecod1e + (14, tc1_raw), + (1, 1), // timecod2e + (14, tc2_raw), + (1, 0), // addbsie + ]; + let bytes = pack_bits(&bits); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.bsid, 8); + assert_eq!(bsi.acmod, 1); + assert_eq!(bsi.timecode_presence, TimeCodePresence::BothHalves); + let tc1 = bsi.timecod1.expect("timecod1e=1 should surface tc1"); + assert_eq!(tc1.hours(), 12); + assert_eq!(tc1.minutes(), 34); + assert_eq!(tc1.eight_second_increments(), 5); + assert_eq!(tc1.raw(), tc1_raw as u16); + assert_eq!(tc1.seconds_in_day(), 12 * 3600 + 34 * 60 + 5 * 8); + assert!(tc1.is_spec_valid()); + let tc2 = bsi.timecod2.expect("timecod2e=1 should surface tc2"); + assert_eq!(tc2.seconds(), 3); + assert_eq!(tc2.frames(), 15); + assert_eq!(tc2.frame_fractions(), 42); + assert_eq!(tc2.raw(), tc2_raw as u16); + assert!(tc2.is_spec_valid()); + } + + /// Only the first half present per Table 5.13 row + /// `(timecod2e=0, timecod1e=1)`. `parse()` should surface + /// `timecod1` and leave `timecod2 == None` with + /// `timecode_presence == FirstHalfOnly`. + #[test] + fn parse_surfaces_only_first_timecode_half_when_only_timecod1e() { + let tc1_raw: u32 = (1u32 << 9) | (2u32 << 3) | 3u32; // 01:02:24 + let bits: [(u8, u32); 14] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod = 1 + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 1), // timecod1e=1 + (14, tc1_raw), + (1, 0), // timecod2e=0 + (1, 0), // addbsie=0 + ]; + let bytes = pack_bits(&bits); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.timecode_presence, TimeCodePresence::FirstHalfOnly); + let tc1 = bsi.timecod1.expect("tc1 should surface"); + assert_eq!(tc1.hours(), 1); + assert_eq!(tc1.minutes(), 2); + assert_eq!(tc1.eight_second_increments(), 3); + assert!(bsi.timecod2.is_none(), "tc2 should not surface"); + } + + /// Only the second half present per Table 5.13 row + /// `(timecod2e=1, timecod1e=0)`. `parse()` should leave + /// `timecod1 == None` and surface `timecod2`. + #[test] + fn parse_surfaces_only_second_timecode_half_when_only_timecod2e() { + let tc2_raw: u32 = (4u32 << 11) | (10u32 << 6) | 20u32; + let bits: [(u8, u32); 14] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod = 1 + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e=0 + (1, 1), // timecod2e=1 + (14, tc2_raw), + (1, 0), // addbsie=0 + ]; + let bytes = pack_bits(&bits); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.timecode_presence, TimeCodePresence::SecondHalfOnly); + assert!(bsi.timecod1.is_none(), "tc1 should not surface"); + let tc2 = bsi.timecod2.expect("tc2 should surface"); + assert_eq!(tc2.seconds(), 4); + assert_eq!(tc2.frames(), 10); + assert_eq!(tc2.frame_fractions(), 20); + } + + /// `(timecod2e=0, timecod1e=0)` per Table 5.13 leaves both halves + /// at `None` and `timecode_presence == NotPresent`. Reuses the + /// minimal 2/0 stereo fixture which has both flags clear. + #[test] + fn parse_leaves_timecode_none_when_both_flags_zero() { + let bits: [(u8, u32); 14] = [ + (5, 8), + (3, 0), + (3, 2), + (2, 0), + (1, 0), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), // timecod1e=0 + (1, 0), // timecod2e=0 + (1, 0), // addbsie=0 + ]; + let bytes = pack_bits(&bits); + let bsi = parse(&bytes).unwrap(); + assert!(bsi.timecod1.is_none()); + assert!(bsi.timecod2.is_none()); + assert_eq!(bsi.timecode_presence, TimeCodePresence::NotPresent); + } + + /// `bsid == 6` activates the Annex D alternate bit stream syntax; + /// the `timecod*e` slots carry `xbsi*e` instead so the timecode is + /// definitionally absent regardless of how the slots were set. + /// Verify the parser leaves the timecode surface untouched on an + /// `xbsi1e == 1` stream (the Annex D mix-levels fixture). + #[test] + fn parse_leaves_timecode_none_on_annex_d_bsid_6() { + // Reuse the Annex D xbsi1 fixture from + // `parses_annex_d_bsid_6_xbsi1_mix_levels` — xbsi1e=1 sets the + // bits that under the base syntax would mean "timecode present". + let bits: &[(u8, u32)] = &[ + (5, 6), + (3, 0), + (3, 7), + (2, 0), + (2, 0), + (1, 1), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 1), // xbsi1e=1 (would be timecod1e=1 under base syntax) + (2, 0b01), // dmixmod + (3, 0b011), + (3, 0b100), + (3, 0b100), + (3, 0b101), + (1, 0), // xbsi2e=0 + (1, 0), // addbsie=0 + ]; + let bytes = pack_bits(bits); + let bsi = parse(&bytes).unwrap(); + assert_eq!(bsi.bsid, 6); + assert!( + bsi.annex_d_mix_levels.is_some(), + "Annex D mix-levels surfaced" + ); + assert!( + bsi.timecod1.is_none(), + "Annex D syntax must NOT surface timecod1" + ); + assert!( + bsi.timecod2.is_none(), + "Annex D syntax must NOT surface timecod2" + ); + assert_eq!(bsi.timecode_presence, TimeCodePresence::NotPresent); + } + + /// `bsid == 6` with `xbsi2e == 0` keeps the three Annex D fields at + /// `None` even though the alternate syntax is active — the encoder + /// chose to omit the playback metadata. The xbsi1 block is also + /// disabled here to keep the bit string short. + #[test] + fn parse_leaves_xbsi2_fields_none_when_xbsi2e_zero() { + // bsid=6, acmod=2 (2/0 stereo): no cmix, surmixlev=0xFF guard, + // dsurmod present. Skip xbsi1e/xbsi2e/addbsie. + let bits: [(u8, u32); 16] = [ + (5, 6), + (3, 0), + (3, 2), + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e=0 + (1, 0), // xbsi2e=0 + (1, 0), // addbsie=0 + (1, 0), // pad + (1, 0), // pad + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + assert!(b.dsurexmod.is_none()); + assert!(b.dheadphonmod.is_none()); + assert!(b.adconvtyp.is_none()); + } + + // --------------------------------------------------------------- + // DolbySurroundMode — §5.4.2.6 / Table 5.11 (2/0 stereo `acmod==2`). + // --------------------------------------------------------------- + + /// Table 5.11 — `dsurmod` decodes verbatim across all 4 codepoints + /// and the raw round-trip is byte-stable. + #[test] + fn dsurmod_decodes_all_4_codepoints() { + use DolbySurroundMode::*; + assert_eq!(DolbySurroundMode::from_code(0b00), NotIndicated); + assert_eq!(DolbySurroundMode::from_code(0b01), NotEncoded); + assert_eq!(DolbySurroundMode::from_code(0b10), Encoded); + assert_eq!(DolbySurroundMode::from_code(0b11), Reserved); + for code in 0u8..4 { + assert_eq!(DolbySurroundMode::from_code(code).raw(), code); + } + } + + /// §5.4.2.6 spec note: the reserved code "may be interpreted as + /// 'not indicated'" — `is_not_indicated()` collapses both + /// codepoints into one branch. + #[test] + fn dsurmod_is_not_indicated_collapses_reserved() { + use DolbySurroundMode::*; + assert!(NotIndicated.is_not_indicated()); + assert!(Reserved.is_not_indicated()); + assert!(!NotEncoded.is_not_indicated()); + assert!(!Encoded.is_not_indicated()); + // Encoded is the only codepoint that should arm a matrix decoder. + assert!(Encoded.is_dolby_surround_encoded()); + assert!(!NotIndicated.is_dolby_surround_encoded()); + assert!(!NotEncoded.is_dolby_surround_encoded()); + assert!(!Reserved.is_dolby_surround_encoded()); + } + + /// 2/0 stereo (`acmod == 2`) syncframe surfaces the typed + /// `dolby_surround_mode` view of the on-wire codepoint. Walk all + /// four Table 5.11 rows through `parse()` and check both the raw + /// `dsurmod` byte and the typed `Option` line + /// up. + #[test] + fn parse_surfaces_dolby_surround_mode_on_2_0() { + for code in 0u32..4 { + // bsid=8, bsmod=0, acmod=2, cmixlev absent, surmixlev absent, + // dsurmod=code, lfeon=0, dialnorm=27, then the optional + // chain clipped down to a tail of zeros (compre=0, langcode=0, + // audprodie=0, copyrightb=0, origbs=0, timecod1e=0, + // timecod2e=0, addbsie=0). + let bits: [(u8, u32); 14] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 2), // acmod + // cmixlev absent (acmod & 1 == 0 → guard false) + // surmixlev absent (acmod & 4 == 0 → guard false) + (2, code), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 8); + assert_eq!(b.acmod, 2); + assert_eq!(b.dsurmod, code as u8); + let expected = DolbySurroundMode::from_code(code as u8); + assert_eq!(b.dolby_surround_mode, Some(expected)); + // Accessor matches the field. + assert_eq!(b.dolby_surround_mode(), Some(expected)); + } + } + + /// `acmod != 2` syncframe — the §5.3.2 guard skips the 2-bit + /// `dsurmod` slot entirely, so the typed `dolby_surround_mode` + /// resolves to `None` and the raw byte stays at the `0xFF` + /// "absent" sentinel. + #[test] + fn parse_dolby_surround_mode_none_when_acmod_not_2_0() { + // bsid=8, bsmod=0, acmod=7 (3/2 — no dsurmod slot), cmixlev=0, + // surmixlev=0, lfeon=0, dialnorm=27, tail zeros. + let bits: [(u8, u32); 15] = [ + (5, 8), + (3, 0), + (3, 7), // acmod = 3/2 (5-channel) + (2, 0), // cmixlev (acmod & 1 != 0) + (2, 0), // surmixlev (acmod & 4 != 0) + // dsurmod absent (acmod != 2) + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 7); + assert_eq!(b.dsurmod, 0xFF); + assert!(b.dolby_surround_mode.is_none()); + assert!(b.dolby_surround_mode().is_none()); + } + + // --------------------------------------------------------------- + // CopyrightInfo — §5.4.2.24-25 distribution-control hint pair. + // --------------------------------------------------------------- + + /// Walk all four `(copyrightb, origbs)` codepoints and assert the + /// raw 1-bit values + the semantic accessors line up with the spec + /// text (§5.4.2.24 / §5.4.2.25). + #[test] + fn copyright_info_four_codepoints_round_trip() { + let cases: [(bool, bool, bool, bool); 4] = [ + (false, false, false, false), + (false, true, false, true), + (true, false, true, false), + (true, true, true, true), + ]; + for (c, o, exp_protected, exp_original) in cases { + let ci = CopyrightInfo::from_bits(c, o); + assert_eq!(ci.is_copyright_protected(), exp_protected); + assert_eq!(ci.is_original_bitstream(), exp_original); + assert_eq!(ci.copyrightb_bit(), u8::from(c)); + assert_eq!(ci.origbs_bit(), u8::from(o)); + } + } + + /// Equality + Copy semantics — two `CopyrightInfo` built from the + /// same bits compare equal, derive `Copy` so the typed surface can + /// be passed by value alongside the other small typed fields on + /// the BSI without ref-counting. + #[test] + fn copyright_info_eq_and_copy() { + let a = CopyrightInfo::from_bits(true, false); + let b = CopyrightInfo::from_bits(true, false); + let c = CopyrightInfo::from_bits(false, true); + assert_eq!(a, b); + assert_ne!(a, c); + // Copy: `a` survives the `let _ = a;` use after the implicit move. + let moved = a; + assert_eq!(moved, a); + } + + /// Parse a minimal 2/0 BSI with `copyrightb=0, origbs=1` (the + /// base-AC-3 encoder's default — "not protected, original + /// bitstream") and confirm the typed surface decodes the pair. + #[test] + fn parses_copyright_info_encoder_default() { + // 2/0 stereo, dialnorm=27. All metadata flags off. `copyrightb=0`, + // `origbs=1` matches the in-tree base-AC-3 encoder's emit. + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 2), // acmod + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 1), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert!(!b.copyright_info.is_copyright_protected()); + assert!(b.copyright_info.is_original_bitstream()); + assert_eq!(b.copyright_info.copyrightb_bit(), 0); + assert_eq!(b.copyright_info.origbs_bit(), 1); + } + + /// Parse a minimal 2/0 BSI with `copyrightb=1, origbs=0` (the + /// "protected copy" pattern — a downstream re-distribution should + /// honour the copyright tag and the "this is a copy" flag). + #[test] + fn parses_copyright_info_protected_copy() { + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 2), // acmod + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 1), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert!(b.copyright_info.is_copyright_protected()); + assert!(!b.copyright_info.is_original_bitstream()); + } + + /// Parse a 1+1 dual-mono BSI (acmod=0) and confirm `copyrightb` / + /// `origbs` decode correctly even when the 1+1 chain pushes the + /// pair further down the bit cursor (extra `dialnorm2` + Ch2 + /// `compr2e` + `langcod2e` + `audprodi2e` flags sit between the + /// Ch1 metadata block and the `copyrightb`/`origbs` slots). + #[test] + fn parses_copyright_info_dual_mono_acmod_0() { + // acmod=0 (1+1). Ch1 metadata flags off, Ch2 metadata flags + // off, copyrightb=1, origbs=1. + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 0), // acmod = 1+1 + // cmixlev / surmixlev / dsurmod all absent for acmod=0. + (1, 0), // lfeon + (5, 27), // dialnorm (Ch1) + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + // 1+1 Ch2 service-metadata block + (5, 27), // dialnorm2 + (1, 0), // compr2e + (1, 0), // langcod2e + (1, 0), // audprodi2e + (1, 1), // copyrightb + (1, 1), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 0); + assert!(b.copyright_info.is_copyright_protected()); + assert!(b.copyright_info.is_original_bitstream()); + } + + // ----------------------------------------------------------------- + // AdditionalBitStreamInfo (§5.4.2.29-31) + // ----------------------------------------------------------------- + + /// `from_addbsil_and_payload` rejects `addbsil > 63` (the wire field + /// is 6 bits) and rejects a payload-length mismatch — these would + /// not round-trip back through the bit-stream parser. + #[test] + fn additional_bsi_constructor_rejects_invalid_inputs() { + assert!(AdditionalBitStreamInfo::from_addbsil_and_payload(64, vec![0u8; 65]).is_none()); + assert!(AdditionalBitStreamInfo::from_addbsil_and_payload(255, vec![0u8; 1]).is_none()); + // Length mismatch — addbsil=0 means 1 byte, payload has 2. + assert!(AdditionalBitStreamInfo::from_addbsil_and_payload(0, vec![0u8, 0u8]).is_none()); + // Length mismatch — addbsil=3 means 4 bytes, payload has 3. + assert!( + AdditionalBitStreamInfo::from_addbsil_and_payload(3, vec![0u8, 0u8, 0u8]).is_none() + ); + } + + /// Minimum-length payload: `addbsil == 0` ⇒ payload is 1 byte; + /// surface exposes `len() == 1`, `is_empty() == false`, + /// `wire_bits() == 7 + 8 == 15`. + #[test] + fn additional_bsi_min_length_payload() { + let info = AdditionalBitStreamInfo::from_addbsil_and_payload(0, vec![0xA5]).unwrap(); + assert_eq!(info.addbsil(), 0); + assert_eq!(info.len(), 1); + assert!(!info.is_empty()); + assert_eq!(info.payload(), &[0xA5]); + assert_eq!(info.wire_bits(), 15); + } + + /// Maximum-length payload: `addbsil == 63` ⇒ payload is 64 bytes; + /// surface exposes `len() == 64`, `wire_bits() == 7 + 8 × 64 == + /// 519`. Payload is preserved verbatim. + #[test] + fn additional_bsi_max_length_payload() { + let payload: Vec = (0..64).collect(); + let info = AdditionalBitStreamInfo::from_addbsil_and_payload(63, payload.clone()).unwrap(); + assert_eq!(info.addbsil(), 63); + assert_eq!(info.len(), 64); + assert_eq!(info.payload(), payload.as_slice()); + assert_eq!(info.wire_bits(), 7 + 8 * 64); + } + + /// Round-trip through `parse()` on a synthetic 1/0 mono BSI where + /// the encoder set `addbsie == 1` with a 1-byte payload (the + /// minimum). Confirms the parser recovers the payload byte + /// verbatim and that `addbsil == 0 ⇒ len() == 1`. + #[test] + fn parses_addbsi_single_byte_payload() { + // 1/0 mono, no optional service metadata, addbsie=1, + // addbsil=0 (⇒ 1 payload byte), payload = 0b1011_0100. + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod (1/0 mono) + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 1), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 1), // addbsie + (6, 0), // addbsil (0 ⇒ 1 byte payload) + (8, 0xB4), // addbsi payload + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + let info = b.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.addbsil(), 0); + assert_eq!(info.len(), 1); + assert_eq!(info.payload(), &[0xB4]); + } + + /// Round-trip through `parse()` on a synthetic 1/0 mono BSI where + /// the encoder set `addbsie == 1` with the maximum-length 64-byte + /// payload. Confirms the parser walks all 64 bytes correctly and + /// `bits_consumed` advances by `7 + 8 × 64 == 519` over the BSI + /// tail block. + #[test] + fn parses_addbsi_max_length_payload() { + let total_prefix_bits: u32 = 5 + 3 + 3 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1; + let mut bits: Vec<(u8, u32)> = vec![ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 1), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 1), // addbsie + (6, 63), // addbsil = 63 ⇒ 64 payload bytes + ]; + // 64 distinct payload bytes — easy to detect any cursor slippage. + for k in 0..64u32 { + bits.push((8, k ^ 0x55)); + } + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + let info = b.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.addbsil(), 63); + assert_eq!(info.len(), 64); + let expected: Vec = (0u32..64).map(|k| (k ^ 0x55) as u8).collect(); + assert_eq!(info.payload(), expected.as_slice()); + let expected_bits = total_prefix_bits as u64 + 6 + 8 * 64; + assert_eq!(b.bits_consumed, expected_bits); + } + + /// `addbsie == 0` yields `addbsi == None` and the parser stops + /// after the flag bit — `bits_consumed` should match the + /// pre-addbsi byte count + 1 bit. + #[test] + fn parses_addbsi_absent_when_addbsie_zero() { + // 1/0 mono, no optional fields, addbsie = 0. + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 1), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie = 0 + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert!(b.addbsi.is_none()); + let expected_bits: u64 = 5 + 3 + 3 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1; + assert_eq!(b.bits_consumed, expected_bits); + } + + /// Annex D `bsid == 6` shares the addbsi position with the base + /// syntax — only the `timecod*e` slots flip to `xbsi*e` upstream. + /// Confirm the addbsi surface still decodes independently of the + /// `bsid == 6` switch. + #[test] + fn parses_addbsi_on_annex_d_bsid_6() { + // bsid=6, acmod=2 (2/0). xbsi1e=0, xbsi2e=0. addbsie=1 with a + // 2-byte payload (addbsil=1). + let bits: &[(u8, u32)] = &[ + (5, 6), // bsid + (3, 0), // bsmod + (3, 2), // acmod (2/0 stereo) + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 1), // origbs + (1, 0), // xbsi1e + (1, 0), // xbsi2e + (1, 1), // addbsie + (6, 1), // addbsil = 1 ⇒ 2 payload bytes + (8, 0xCA), + (8, 0xFE), + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + let info = b.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.addbsil(), 1); + assert_eq!(info.len(), 2); + assert_eq!(info.payload(), &[0xCA, 0xFE]); + } + + /// 1+1 dual-mono (`acmod == 0`) routes through the Ch2 service- + /// metadata block before reaching the addbsi position. Confirm the + /// addbsi surface still decodes correctly downstream of the longer + /// Ch2 chain. + #[test] + fn parses_addbsi_on_1plus1_dual_mono() { + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 0), // acmod=0 (1+1) + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + // Ch2 service-metadata block + (5, 27), // dialnorm2 + (1, 0), // compr2e + (1, 0), // langcod2e + (1, 0), // audprodi2e + (1, 0), // copyrightb + (1, 1), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 1), // addbsie + (6, 2), // addbsil = 2 ⇒ 3 payload bytes + (8, 0xDE), + (8, 0xAD), + (8, 0xBE), + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 0); + let info = b.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.addbsil(), 2); + assert_eq!(info.payload(), &[0xDE, 0xAD, 0xBE]); + } + + /// Annex D `bsid == 6` keeps the same `copyrightb` / `origbs` + /// position in the BSI — only the post-`origbs` slots flip from + /// `timecod*e` to `xbsi*e`. Confirm the typed surface decodes + /// independently of the `bsid == 6` switch. + #[test] + fn parses_copyright_info_annex_d_bsid_6() { + // bsid=6, acmod=2 (2/0). xbsi1e=0, xbsi2e=0, copyrightb=0, + // origbs=0 — distinct from the base-syntax default to confirm + // the parser isn't reading a stale value. + let bits: &[(u8, u32)] = &[ + (5, 6), // bsid + (3, 0), // bsmod + (3, 2), // acmod + (2, 0), // dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e + (1, 0), // xbsi2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + assert!(!b.copyright_info.is_copyright_protected()); + assert!(!b.copyright_info.is_original_bitstream()); + } + + /// Round 243 — Table D2.2 / §2.3.1.2 typed surface. The 2-bit + /// codepoint decode is a direct lookup: `00` → `NotIndicated`, + /// `01` → `LtRtPreferred`, `10` → `LoRoPreferred`, `11` → + /// `Reserved`. `raw()` round-trips back to the original + /// codepoint. + #[test] + fn stereo_downmix_preference_decodes_all_four_codepoints() { + let cases = [ + (0b00u8, StereoDownmixPreference::NotIndicated), + (0b01u8, StereoDownmixPreference::LtRtPreferred), + (0b10u8, StereoDownmixPreference::LoRoPreferred), + (0b11u8, StereoDownmixPreference::Reserved), + ]; + for (code, expected) in cases { + let decoded = StereoDownmixPreference::from_code(code); + assert_eq!(decoded, expected, "code = {code:#04b}"); + assert_eq!(decoded.raw(), code, "raw round-trip for {decoded:?}"); + } + } + + /// Spec §2.3.1.2 notes "the reserved code may be interpreted as + /// 'not indicated'" — confirm + /// [`StereoDownmixPreference::is_not_indicated`] collapses both + /// codepoints into one branch. + #[test] + fn stereo_downmix_preference_treats_reserved_as_not_indicated() { + assert!(StereoDownmixPreference::NotIndicated.is_not_indicated()); + assert!(StereoDownmixPreference::Reserved.is_not_indicated()); + assert!(!StereoDownmixPreference::LtRtPreferred.is_not_indicated()); + assert!(!StereoDownmixPreference::LoRoPreferred.is_not_indicated()); + } + + /// Confirm the LtRt / LoRo predicates short-circuit only on + /// the explicit-preference variants. + #[test] + fn stereo_downmix_preference_predicates_match_explicit_variants() { + assert!(StereoDownmixPreference::LtRtPreferred.prefers_lt_rt()); + assert!(!StereoDownmixPreference::LtRtPreferred.prefers_lo_ro()); + assert!(StereoDownmixPreference::LoRoPreferred.prefers_lo_ro()); + assert!(!StereoDownmixPreference::LoRoPreferred.prefers_lt_rt()); + assert!(!StereoDownmixPreference::NotIndicated.prefers_lt_rt()); + assert!(!StereoDownmixPreference::NotIndicated.prefers_lo_ro()); + assert!(!StereoDownmixPreference::Reserved.prefers_lt_rt()); + assert!(!StereoDownmixPreference::Reserved.prefers_lo_ro()); + } + + /// Base §5.3.2 timecode syntax (`bsid != 6`) reuses the bit slot + /// for `timecod*e/timecod*`, so the preferred-downmix-mode hint + /// stays absent. The typed surface must return `None` even when + /// the raw `dmixmod` field carries the `0xFF` "absent" sentinel. + #[test] + fn parse_leaves_dmixmod_preference_none_in_base_syntax() { + // bsid=8 (base syntax), acmod=7 (3/2 — the §2.3.1.2 note's + // meaningful range, were the hint actually carried). Confirm + // the parser reports `None` and the raw sentinel. + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid (base syntax, no Annex D xbsi1 slot) + (3, 0), // bsmod + (3, 7), // acmod = 3/2 + (2, 0b10), // cmixlev + (2, 0b01), // surmixlev + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 8); + assert_eq!(b.dmixmod, 0xFF); + assert!(b.dmixmod_preference.is_none()); + assert!(b.stereo_downmix_preference().is_none()); + } + + /// Annex D `bsid == 6` with `xbsi1e == 1` surfaces the typed + /// preference. Cover all four wire codepoints round-tripping + /// through `parse()`. + #[test] + fn parse_surfaces_dmixmod_preference_annex_d_all_codepoints() { + for (code, expected) in [ + (0b00u8, StereoDownmixPreference::NotIndicated), + (0b01u8, StereoDownmixPreference::LtRtPreferred), + (0b10u8, StereoDownmixPreference::LoRoPreferred), + (0b11u8, StereoDownmixPreference::Reserved), + ] { + let bits: &[(u8, u32)] = &[ + (5, 6), // bsid = 6 (Annex D alt syntax) + (3, 0), // bsmod + (3, 7), // acmod = 3/2 (multi-channel — Annex D xbsi1 slot present) + (2, 0b10), // cmixlev + (2, 0b01), // surmixlev + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 1), // xbsi1e + (2, code as u32), // dmixmod codepoint under test + (3, 0b010), // ltrtcmixlev + (3, 0b010), // ltrtsurmixlev + (3, 0b010), // lorocmixlev + (3, 0b010), // lorosurmixlev + (1, 0), // xbsi2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + assert_eq!(b.dmixmod, code, "raw dmixmod for {expected:?}"); + assert_eq!(b.dmixmod_preference, Some(expected)); + assert_eq!(b.stereo_downmix_preference(), Some(expected)); + } + } + + /// Annex D `bsid == 6` with `xbsi1e == 0` skips the xbsi1 + /// block — the typed preference is `None` even though the + /// alternate-syntax wire slot exists in the BSI layout. + #[test] + fn parse_leaves_dmixmod_preference_none_when_xbsi1e_clear() { + let bits: &[(u8, u32)] = &[ + (5, 6), // bsid = 6 + (3, 0), // bsmod + (3, 7), // acmod = 3/2 + (2, 0b10), // cmixlev + (2, 0b01), // surmixlev + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // xbsi1e (cleared — block absent) + (1, 0), // xbsi2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.bsid, 6); + assert_eq!(b.dmixmod, 0xFF); + assert!(b.dmixmod_preference.is_none()); + } + + // --------------------------------------------------------------- + // Language code (`langcod` / `langcod2`) — §5.4.2.11-12 / 19-20. + // --------------------------------------------------------------- + + /// [`LanguageCode::from_raw`] is a verbatim wrapper — every byte + /// `0x00..=0xFF` is representable, and `raw()` returns the same + /// value back. + #[test] + fn language_code_from_raw_round_trips_every_byte() { + for raw in 0u8..=255 { + let lc = LanguageCode::from_raw(raw); + assert_eq!(lc.raw(), raw, "raw={raw:#04x}"); + } + } + + /// Per §5.4.2.12 the spec-mandated wire value is `0xFF`. The + /// `is_spec_reserved_value` predicate must hold only for that + /// byte and reject everything else. + #[test] + fn language_code_is_spec_reserved_only_for_0xff() { + for raw in 0u8..=255 { + let lc = LanguageCode::from_raw(raw); + let want = raw == 0xFF; + assert_eq!( + lc.is_spec_reserved_value(), + want, + "raw={raw:#04x} should be spec_reserved={want}" + ); + } + } + + /// `parse()` surfaces `langcode == 1` into a typed + /// [`LanguageCode`] with the 8-bit `langcod` byte taken verbatim + /// from the wire. Build a 1/0 mono BSI (`acmod=1`) with + /// `langcode=1`, `langcod=0xFF` (spec-conforming), and verify the + /// typed surface holds and `is_spec_reserved_value` matches. + #[test] + fn parse_surfaces_language_code_when_langcode_flag_set() { + let bits: [(u8, u32); 16] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod = 1/0 mono → no cmix/surmix/dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // langcode = 1 + (8, 0xFF), // langcod = 0xFF (spec-conforming) + (1, 0), // audprodie = 0 + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + (1, 0), // pad + (1, 0), // pad + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 1); + let lc = b + .language_code + .expect("langcode=1 should surface language_code"); + assert_eq!(lc.raw(), 0xFF); + assert!(lc.is_spec_reserved_value()); + // Same value reachable via the typed accessor. + assert_eq!(b.language_code(), Some(lc)); + // Not 1+1 dual-mono → no Ch2 mirror. + assert!(b.language_code_ch2.is_none()); + assert!(b.language_code_ch2().is_none()); + } + + /// `parse()` surfaces a non-`0xFF` `langcod` legacy-encoded byte + /// verbatim and `is_spec_reserved_value` reports `false` so a + /// probe / archive tool can flag the stream. Build a 1/0 mono + /// BSI with `langcode=1`, `langcod=0x42` (a 1995-era table-lookup + /// codepoint). + #[test] + fn parse_surfaces_non_reserved_language_code_byte() { + let bits: [(u8, u32); 16] = [ + (5, 8), // bsid + (3, 0), // bsmod + (3, 1), // acmod = 1/0 mono + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // langcode = 1 + (8, 0x42), // langcod = legacy codepoint + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + (1, 0), // pad + (1, 0), // pad + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + let lc = b + .language_code + .expect("langcode=1 should surface language_code"); + assert_eq!(lc.raw(), 0x42); + assert!(!lc.is_spec_reserved_value()); + } + + /// `langcode == 0` leaves [`Bsi::language_code`] as `None` — no + /// `langcod` byte follows on the wire. Use a minimal 2/0 stereo + /// shape (the same fixture used by + /// `parses_minimal_2_0_stereo_bsi`). + #[test] + fn parse_leaves_language_code_none_when_langcode_flag_clear() { + let bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), + (2, 0b00), + (1, 0), + (5, 27), + (1, 0), // compre + (1, 0), // langcode = 0 → no langcod byte + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), + (1, 0), + (1, 0), + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert!(b.language_code.is_none()); + assert!(b.language_code_ch2.is_none()); + assert!(b.language_code().is_none()); + assert!(b.language_code_ch2().is_none()); + } + + /// 1+1 dual-mono (`acmod == 0`) emits an independent `langcod2e` + /// flag and (when set) the Ch2 `langcod2` byte. Build a 1+1 + /// stream with `langcode=1` / `langcod=0xFF` and + /// `langcod2e=1` / `langcod2=0xFF`, and verify both typed + /// surfaces hold. + #[test] + fn parse_surfaces_language_code_ch2_in_1_plus_1() { + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 0), // acmod = 0 (1+1 dual mono) — no cmix/surmix/dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm (Ch1) + (1, 0), // compre + (1, 1), // langcode = 1 + (8, 0xFF), // langcod = 0xFF + (1, 0), // audprodie = 0 + (5, 27), // dialnorm2 (Ch2) + (1, 0), // compr2e + (1, 1), // langcod2e = 1 + (8, 0xFF), // langcod2 = 0xFF + (1, 0), // audprodi2e + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 0); + let lc1 = b + .language_code + .expect("langcode=1 should surface language_code"); + assert_eq!(lc1.raw(), 0xFF); + assert!(lc1.is_spec_reserved_value()); + let lc2 = b + .language_code_ch2 + .expect("langcod2e=1 should surface language_code_ch2"); + assert_eq!(lc2.raw(), 0xFF); + assert!(lc2.is_spec_reserved_value()); + } + + /// In a 1+1 dual-mono stream with `langcode=0` / `langcod2e=0`, + /// both typed surfaces stay `None`. Confirms the per-channel + /// gates are independent of each other. + #[test] + fn parse_leaves_language_code_ch2_none_when_langcod2e_clear() { + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 0), // acmod = 0 (1+1) + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (5, 27), // dialnorm2 + (1, 0), // compr2e + (1, 0), // langcod2e + (1, 0), // audprodi2e + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 0); + assert!(b.language_code.is_none()); + assert!(b.language_code_ch2.is_none()); + } + + // --------------------------------------------------------------- + // CenterMixLevel / SurroundMixLevel — §5.4.2.4-5 typed surfaces. + // --------------------------------------------------------------- + + /// Every Table 5.9 codepoint round-trips through `from_code` / + /// `raw` and resolves to its spec-documented linear coefficient. + /// The reserved codepoint returns `None` from `coefficient()` and + /// the spec's intermediate-value substitution from + /// `coefficient_with_reserved_fallback`. + #[test] + fn center_mix_level_table_5_9_round_trip() { + let cases: [(u8, CenterMixLevel, Option, f32); 4] = [ + (0b00, CenterMixLevel::Minus3Db, Some(0.707), 0.707), + (0b01, CenterMixLevel::Minus4Point5Db, Some(0.595), 0.595), + (0b10, CenterMixLevel::Minus6Db, Some(0.500), 0.500), + (0b11, CenterMixLevel::Reserved, None, 0.595), + ]; + for (raw, want_variant, want_coef, want_fallback) in cases { + let v = CenterMixLevel::from_code(raw); + assert_eq!(v, want_variant, "raw {raw:02b}"); + assert_eq!(v.raw(), raw, "raw round-trip {raw:02b}"); + assert_eq!(v.coefficient(), want_coef); + assert!( + (v.coefficient_with_reserved_fallback() - want_fallback).abs() < 1e-6, + "fallback for {raw:02b}: {} vs {want_fallback}", + v.coefficient_with_reserved_fallback() + ); + assert_eq!(v.is_reserved(), raw == 0b11); + } + // `from_code` truncates anything outside the 2-bit codespace + // to its low 2 bits — `0xFF & 0x3 == 0b11` is reserved. + assert_eq!(CenterMixLevel::from_code(0xFF), CenterMixLevel::Reserved); + } + + /// Every Table 5.10 codepoint round-trips through `from_code` / + /// `raw` and resolves to its spec-documented linear coefficient. + /// The reserved codepoint returns `None` from `coefficient()` and + /// the spec's intermediate-value substitution from + /// `coefficient_with_reserved_fallback`. The mute codepoint + /// `'10'` collapses to coefficient `0.0` and is flagged by + /// `is_mute()`. + #[test] + fn surround_mix_level_table_5_10_round_trip() { + let cases: [(u8, SurroundMixLevel, Option, f32); 4] = [ + (0b00, SurroundMixLevel::Minus3Db, Some(0.707), 0.707), + (0b01, SurroundMixLevel::Minus6Db, Some(0.500), 0.500), + (0b10, SurroundMixLevel::Mute, Some(0.000), 0.000), + (0b11, SurroundMixLevel::Reserved, None, 0.500), + ]; + for (raw, want_variant, want_coef, want_fallback) in cases { + let v = SurroundMixLevel::from_code(raw); + assert_eq!(v, want_variant, "raw {raw:02b}"); + assert_eq!(v.raw(), raw, "raw round-trip {raw:02b}"); + assert_eq!(v.coefficient(), want_coef); + assert!( + (v.coefficient_with_reserved_fallback() - want_fallback).abs() < 1e-6, + "fallback for {raw:02b}: {} vs {want_fallback}", + v.coefficient_with_reserved_fallback() + ); + assert_eq!(v.is_reserved(), raw == 0b11); + assert_eq!(v.is_mute(), raw == 0b10); + } + // 2-bit truncation: low 2 bits of `0xFF` are `0b11` (reserved). + assert_eq!( + SurroundMixLevel::from_code(0xFF), + SurroundMixLevel::Reserved + ); + } + + /// A 3/2 (acmod=7) syncframe carries both `cmixlev` (3 front + /// channels gate satisfied) and `surmixlev` (surround gate + /// satisfied), so both typed surfaces become `Some` after + /// `parse()` and match the raw codepoint round-trip. + #[test] + fn parse_surfaces_center_and_surround_mix_on_3_2() { + // Exercise the non-default codepoints to prove the bit + // positions are correct: cmixlev = 0b10 (-6 dB), + // surmixlev = 0b01 (-6 dB). + let bits: &[(u8, u32)] = &[ + (5, 8), // bsid + (3, 0), // bsmod + (3, 7), // acmod = 3/2 + (2, 0b10), // cmixlev = -6 dB (0.500) + (2, 0b01), // surmixlev = -6 dB (0.500) + // dsurmod absent (acmod != 2) + (1, 1), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 7); + assert_eq!(b.cmixlev, 0b10); + assert_eq!(b.surmixlev, 0b01); + let center = b.center_mix.expect("3/2 carries cmixlev"); + assert_eq!(center, CenterMixLevel::Minus6Db); + assert_eq!(center.raw(), 0b10); + assert_eq!(center.coefficient(), Some(0.500)); + // Accessor view matches the field view. + assert_eq!(b.center_mix(), Some(CenterMixLevel::Minus6Db)); + let surround = b.surround_mix.expect("3/2 carries surmixlev"); + assert_eq!(surround, SurroundMixLevel::Minus6Db); + assert_eq!(surround.raw(), 0b01); + assert_eq!(surround.coefficient(), Some(0.500)); + assert_eq!(b.surround_mix(), Some(SurroundMixLevel::Minus6Db)); + } + + /// A 2/0 stereo (acmod=2) syncframe carries neither `cmixlev` nor + /// `surmixlev` — the §5.3.2 guards skip both 2-bit slots — so both + /// typed surfaces stay `None` and the raw fields keep the + /// "absent" sentinel `0xFF`. This is the same fixture as + /// `parses_minimal_2_0_stereo_bsi` extended with the typed + /// assertions. + #[test] + fn parse_center_and_surround_mix_none_when_acmod_2_0() { + let bits: [(u8, u32); 14] = [ + (5, 0b01000), + (3, 0b000), + (3, 0b010), // acmod = 2/0 + (2, 0b00), // dsurmod + (1, 0), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let bytes = pack_bits(&bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 2); + assert_eq!(b.cmixlev, 0xFF); + assert_eq!(b.surmixlev, 0xFF); + assert!(b.center_mix.is_none()); + assert!(b.surround_mix.is_none()); + assert!(b.center_mix().is_none()); + assert!(b.surround_mix().is_none()); + } + + /// A 1/0 mono (acmod=1) syncframe carries neither slot. The + /// `(acmod & 0x1) != 0 && acmod != 0x1` cmixlev guard rejects + /// `acmod==1` and the `(acmod & 0x4) != 0` surmixlev guard + /// rejects it too — so both typed surfaces stay `None`. + #[test] + fn parse_center_and_surround_mix_none_when_acmod_1_0_mono() { + let bits: &[(u8, u32)] = &[ + (5, 8), + (3, 0), + (3, 1), // acmod = 1/0 mono + // no cmixlev, no surmixlev, no dsurmod + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 1); + assert_eq!(b.cmixlev, 0xFF); + assert_eq!(b.surmixlev, 0xFF); + assert!(b.center_mix.is_none()); + assert!(b.surround_mix.is_none()); + } + + /// A 2/2 (acmod=6) syncframe is the asymmetric case — the + /// `cmixlev` guard fails (no centre channel, `acmod & 0x1 == 0`) + /// but the `surmixlev` guard passes (surround present, + /// `acmod & 0x4 != 0`). Confirms each typed surface is gated + /// independently and the surround codeword is decoded + /// correctly. + #[test] + fn parse_surfaces_only_surround_mix_on_2_2() { + let bits: &[(u8, u32)] = &[ + (5, 8), + (3, 0), + (3, 6), // acmod = 2/2 (L, R, Ls, Rs) + // cmixlev absent (acmod & 0x1 == 0) + (2, 0b10), // surmixlev = Mute + (1, 0), // lfeon + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // langcode + (1, 0), // audprodie + (1, 0), // copyrightb + (1, 0), // origbs + (1, 0), // timecod1e + (1, 0), // timecod2e + (1, 0), // addbsie + ]; + let bytes = pack_bits(bits); + let b = parse(&bytes).unwrap(); + assert_eq!(b.acmod, 6); + assert_eq!(b.cmixlev, 0xFF); + assert_eq!(b.surmixlev, 0b10); + assert!(b.center_mix.is_none()); + let surround = b.surround_mix.expect("2/2 carries surmixlev"); + assert_eq!(surround, SurroundMixLevel::Mute); + assert!(surround.is_mute()); + assert_eq!(surround.coefficient(), Some(0.0)); + } +} diff --git a/crates/vendor/oxideav-ac3/src/crc.rs b/crates/vendor/oxideav-ac3/src/crc.rs new file mode 100644 index 00000000..6e05f921 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/crc.rs @@ -0,0 +1,554 @@ +//! AC-3 / E-AC-3 frame error-detection — CRC-16 (poly 0x8005). +//! +//! ATSC A/52:2018 §7.10.1 specifies a 16-bit CRC computed by a linear +//! feedback shift register over the generator polynomial +//! `x^16 + x^15 + x^2 + 1`, i.e. binary `1_1000_0000_0000_0101` (the +//! leading `1` is the implicit `x^16`, so the feedback mask is +//! `0x8005`). Two CRC fields appear in every AC-3 syncframe: +//! +//! * **`crc1`** — second 16-bit word, covers the first 5/8 of the +//! syncframe (excluding the syncword). §5.4.1.2 + §7.10.1. +//! * **`crc2`** — last 16-bit word, covers the entire syncframe +//! excluding the syncword. §5.4.5.2 + §7.10.1. +//! +//! E-AC-3 (Annex E) syncframes carry only `crc2`; the spec elides +//! `crc1` because the variable-length frame body removes the +//! 5/8-checkpoint utility (§E.1.2 / Table E1.2). +//! +//! Per §7.10.1 the spec's reference check is **residue-based**: +//! shift the post-syncword data through the LFSR (with the stored +//! CRC bytes included), and the register must read zero at the +//! end. Validated empirically against the FFmpeg-produced +//! `tests/fixtures/sine440_stereo.ac3` corpus — every syncframe +//! satisfies `residue([2..frame_end]) == 0` AND +//! `residue([2..crc1_end]) == 0`. The verifier below implements +//! that residue check. +//! +//! Our own AC-3 and E-AC-3 encoders emit both CRC words in the +//! spec's reference form: `crc1` is solved via +//! `ac3_crc_solve_prefix` (gauss-elimination over GF(2)) so the +//! LFSR reaches zero at the 5/8 boundary, and `crc2` is computed +//! in **augmented** form (`ac3_crc_update(0, body || [0, 0])`) so +//! the LFSR reaches zero at frame end. The augmented form follows +//! the standard CRC codeword property `data·x^16 + r(x) ≡ 0 mod +//! g(x)` — see encoder `emit_*_packet` for the placement. +//! +//! Both checks are bit-exact CRC-16 over poly 0x8005, MSB-first. + +/// The CRC-16 LFSR feedback mask `1000_0000_0000_0101` (without the +/// implicit `x^16` term) — see §7.10.1. +pub(crate) const AC3_CRC_POLY: u16 = 0x8005; + +/// Plain MSB-first CRC-16 over a byte slice using `AC3_CRC_POLY`. +/// +/// The register is updated one bit at a time by shifting left and +/// XORing the feedback mask whenever the outgoing MSB is `1`. The +/// bit order of each byte is MSB-first to match the AC-3 bitstream +/// orientation (§5.3 transmission rule). +/// +/// `init` lets callers chain partial updates — most call-sites use +/// `init = 0` so the LFSR starts cleared per §7.10.1. +pub(crate) fn ac3_crc_update(init: u16, data: &[u8]) -> u16 { + let mut crc: u32 = init as u32; + for &b in data { + for i in (0..8).rev() { + let bit = ((b >> i) & 1) as u32; + let top = (crc >> 15) & 1; + crc = ((crc << 1) & 0xFFFF) | bit; + if top != 0 { + crc ^= AC3_CRC_POLY as u32; + } + } + } + crc as u16 +} + +/// Find a 16-bit value for the *first* 2 bytes of `region` such that +/// the running CRC of the entire region ends at zero. Used by the +/// encoder for `crc1`, where the CRC field sits at the *start* of +/// the covered area and must therefore be solved for rather than +/// derived from a trailing residue (§7.10.1 last paragraph: "crc1 +/// is generated by encoders such that the CRC calculation will +/// produce zero at the 5/8 point in the syncframe"). +/// +/// Linear-algebra approach: the CRC is linear over GF(2), so we +/// build a 16×16 matrix whose columns are the CRC contributions of +/// each basis bit set in the first two bytes (vs. an all-zero +/// prefix), compute the residue of the region with the prefix +/// zeroed, then Gaussian-eliminate for the prefix bits that cancel +/// the residue. +/// +/// The region must be at least 2 bytes (the 16-bit CRC field). +pub(crate) fn ac3_crc_solve_prefix(region: &[u8]) -> u16 { + assert!(region.len() >= 2); + // R = CRC(zeroed-prefix || rest-of-region). + let mut zeroed = region.to_vec(); + zeroed[0] = 0; + zeroed[1] = 0; + let r = ac3_crc_update(0, &zeroed); + + // Build 16 columns of E: column i is CRC(prefix-has-bit-i-set, rest=0). + // CRC over the zero-tail is 0, so each column reduces to the LFSR + // state after a single one-bit perturbation of the 2-byte prefix. + let mut cols = [0u16; 16]; + for i in 0..16 { + let prefix_val: u16 = 1 << (15 - i); // bit i MSB-first in first 2 bytes + let mut buf = vec![0u8; region.len()]; + buf[0] = (prefix_val >> 8) as u8; + buf[1] = (prefix_val & 0xFF) as u8; + cols[i] = ac3_crc_update(0, &buf); + } + // Solve cols * X = R over GF(2) for 16-bit X. + gauss_gf2_16(&cols, r) +} + +/// Solve `A · x = b` over GF(2) where `A` is a 16×16 matrix +/// represented as 16 column vectors (each a `u16` with bit `j` = +/// row `j`), and `b` and `x` are 16-bit vectors. The matrix is +/// expected to be invertible (it is, for AC-3 CRC-16 over any +/// region ≥ 2 bytes). +/// +/// Post-solve, the column index `i` in `x` maps to prefix bit +/// `15 - i` (MSB-first), because `ac3_crc_solve_prefix` builds +/// column `i` from `prefix_val = 1 << (15 - i)`. The returned +/// `u16` is the reassembled prefix word. +fn gauss_gf2_16(cols: &[u16; 16], b: u16) -> u16 { + // Build an augmented matrix as rows: each row is 17 bits (16 cols + 1 b). + let mut rows = [0u32; 16]; + for row in 0..16 { + let bit = 1u16 << row; + let mut r = 0u32; + for c in 0..16 { + if cols[c] & bit != 0 { + r |= 1 << c; + } + } + if b & bit != 0 { + r |= 1 << 16; + } + rows[row] = r; + } + // Forward elimination. + for col in 0..16 { + let mut pivot = None; + for r in col..16 { + if rows[r] & (1 << col) != 0 { + pivot = Some(r); + break; + } + } + let pivot = match pivot { + Some(p) => p, + None => continue, // singular column, leave as-is + }; + rows.swap(col, pivot); + for r in 0..16 { + if r != col && rows[r] & (1 << col) != 0 { + rows[r] ^= rows[col]; + } + } + } + // Read x from the augment column (LSB-first across columns). + let mut x = 0u16; + for r in 0..16 { + if rows[r] & (1 << 16) != 0 { + x |= 1 << r; + } + } + // Re-order from column-index space to prefix-bit space (MSB-first). + let mut prefix = 0u16; + for i in 0..16 { + if x & (1 << i) != 0 { + prefix |= 1 << (15 - i); + } + } + prefix +} + +/// Compute the 5/8-frame boundary used by `crc1` (§7.10.1). +/// +/// The spec writes the calculation in 16-bit-word units: +/// +/// ```text +/// 5/8_framesize = (framesize >> 1) + (framesize >> 3) +/// ``` +/// +/// where `framesize` is in words. Returns the byte offset such that +/// `syncframe[2..byte_offset]` is the region covered by `crc1`. +/// +/// Returns `None` if `frame_bytes` is too small to hold a 5-byte +/// syncinfo + the implied 5/8 region (i.e. `frame_bytes < 4`); valid +/// AC-3 frames per Table 5.18 are at least 128 bytes so this guard +/// only fires on truncated input. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn crc1_boundary_bytes(frame_bytes: usize) -> Option { + if frame_bytes < 4 { + return None; + } + let frame_words = frame_bytes / 2; + let five_eighths_words = (frame_words >> 1) + (frame_words >> 3); + Some(five_eighths_words * 2) +} + +/// Per-frame CRC validation outcome. +/// +/// `crc1_ok` and `crc2_ok` are reported independently so a caller +/// can implement either of the §6.1.2 / §7.10.1 strategies: +/// "accept on either CRC valid", "require both", or "drop frames +/// failing crc2". `None` means the field was not checked because +/// it doesn't apply to this syncframe (E-AC-3 has no `crc1`, so a +/// `verify_eac3_syncframe` always reports `crc1_ok = None`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CrcStatus { + /// `Some(true)` if the LFSR residue over the first 5/8 of the + /// syncframe (post-syncword) is zero, `Some(false)` if non-zero. + /// `None` when not checked (E-AC-3). + pub crc1_ok: Option, + /// `Some(true)` if the LFSR residue over the whole syncframe + /// (post-syncword) is zero. `Some(false)` if non-zero. + pub crc2_ok: Option, +} + +impl CrcStatus { + /// True when every checked field passed. For an AC-3 syncframe + /// this means `crc1_ok == Some(true) && crc2_ok == Some(true)`; + /// for E-AC-3, just `crc2_ok == Some(true)`. An unchecked field + /// (`None`) is treated as a pass, matching the §6.1.2 "accept on + /// either CRC" leniency. + pub fn all_ok(&self) -> bool { + let c1 = self.crc1_ok.unwrap_or(true); + let c2 = self.crc2_ok.unwrap_or(true); + c1 && c2 + } +} + +/// Verify both CRC words in an AC-3 syncframe per §7.10.1. +/// +/// `syncframe` must start with the 0x0B77 syncword and span exactly +/// `frame_bytes` bytes (the full §5.4.1.4 Table 5.18 syncframe). +/// +/// Both checks are **residue-form**: the LFSR is reset to zero, the +/// data bits are shifted through (with the stored CRC fields +/// included), and the register must read zero at the end. +/// +/// * `crc1` covers `syncframe[2..crc1_end]` — the first 5/8 of the +/// post-syncword bytes, including the crc1 field itself. +/// * `crc2` covers `syncframe[2..frame_bytes]` — the entire post- +/// syncword region, including both crc fields. +/// +/// Returns a [`CrcStatus`] populated with the two checks. A +/// truncated `syncframe` (shorter than `frame_bytes`, or shorter +/// than 4 bytes) reports both as failed. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn verify_ac3_syncframe(syncframe: &[u8], frame_bytes: usize) -> CrcStatus { + if syncframe.len() < frame_bytes || frame_bytes < 4 { + return CrcStatus { + crc1_ok: Some(false), + crc2_ok: Some(false), + }; + } + let crc1_end = match crc1_boundary_bytes(frame_bytes) { + Some(v) if v >= 4 && v <= frame_bytes => v, + _ => { + return CrcStatus { + crc1_ok: Some(false), + crc2_ok: Some(false), + } + } + }; + let crc1_residue = ac3_crc_update(0, &syncframe[2..crc1_end]); + let crc2_residue = ac3_crc_update(0, &syncframe[2..frame_bytes]); + CrcStatus { + crc1_ok: Some(crc1_residue == 0), + crc2_ok: Some(crc2_residue == 0), + } +} + +/// Verify the `crc2` word in an E-AC-3 syncframe per §E.1.2 / +/// §7.10.1. +/// +/// E-AC-3 has no `crc1`; the field is omitted from the syncframe +/// to make room for the variable-bitrate `frmsiz` word. `crc2` is +/// still the last 16-bit word and still computed with the same +/// poly 0x8005 LFSR over the post-syncword bytes (residue-form per +/// §7.10.1) — the verifier shifts every byte of the post-syncword +/// region (including the trailing crc2 field) through the LFSR and +/// expects the register to read zero. +/// +/// `syncframe` must start with the 0x0B77 syncword and span at +/// least `frame_bytes`. The reported `crc1_ok` is `None` because +/// E-AC-3 carries no `crc1` field. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn verify_eac3_syncframe(syncframe: &[u8], frame_bytes: usize) -> CrcStatus { + if syncframe.len() < frame_bytes || frame_bytes < 4 { + return CrcStatus { + crc1_ok: None, + crc2_ok: Some(false), + }; + } + let crc2_residue = ac3_crc_update(0, &syncframe[2..frame_bytes]); + CrcStatus { + crc1_ok: None, + crc2_ok: Some(crc2_residue == 0), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Spec sanity: a single zero byte shifted through the cleared + /// LFSR leaves the register at zero (no `1` bits → no XOR). + #[test] + fn zero_data_yields_zero_register() { + assert_eq!(ac3_crc_update(0, &[0u8; 8]), 0); + } + + /// Spec sanity: the LFSR is non-trivial — a single high bit in + /// the leading byte produces a non-zero register at end-of-byte. + #[test] + fn single_high_bit_propagates() { + assert_ne!(ac3_crc_update(0, &[0x80, 0x00]), 0); + } + + /// Algebraic property: CRC is linear over GF(2). For any two + /// equal-length byte slices `a`, `b`, `crc(a XOR b) == crc(a) + /// XOR crc(b)`. This proves the bit-shifter respects the + /// `gauss_gf2_16` solver's assumption. + #[test] + fn crc_is_gf2_linear() { + let a = [0x12u8, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]; + let b = [0xa5u8, 0x5a, 0x33, 0xcc, 0x0f, 0xf0, 0x99, 0x66]; + let mut x = [0u8; 8]; + for i in 0..8 { + x[i] = a[i] ^ b[i]; + } + let ca = ac3_crc_update(0, &a); + let cb = ac3_crc_update(0, &b); + let cx = ac3_crc_update(0, &x); + assert_eq!(ca ^ cb, cx); + } + + /// Solver round-trip: place the solved 16-bit value into the + /// first 2 bytes of a region and the running CRC over the full + /// region must be zero. Mirrors the encoder's `crc1` debug + /// assertion (encoder.rs). + #[test] + fn solver_drives_residue_to_zero() { + let mut region = vec![0u8; 80]; + // Fill the tail with arbitrary content so the residue with a + // zero prefix is non-zero. + for (i, byte) in region.iter_mut().enumerate().skip(2) { + *byte = ((i * 17 + 3) & 0xFF) as u8; + } + let x = ac3_crc_solve_prefix(®ion); + region[0] = (x >> 8) as u8; + region[1] = (x & 0xFF) as u8; + assert_eq!(ac3_crc_update(0, ®ion), 0); + } + + /// §7.10.1 example boundary calculation. For a 768-byte + /// (= 384-word) syncframe at 48 kHz / 192 kbps (frmsizecod=20), + /// 5/8_framesize = (384>>1) + (384>>3) = 192 + 48 = 240 words + /// = 480 bytes (Table 7.34). + #[test] + fn crc1_boundary_matches_table_7_34_48k_192kbps() { + assert_eq!(crc1_boundary_bytes(768), Some(480)); + } + + /// §7.10.1 example boundary calculation. 256-byte (= 128-word) + /// syncframe → (128>>1) + (128>>3) = 64 + 16 = 80 words = 160 B. + #[test] + fn crc1_boundary_minimal_frame() { + assert_eq!(crc1_boundary_bytes(256), Some(160)); + } + + /// Truncated-frame guard: shorter than the 4-byte minimum (the + /// syncword + the first byte of `crc1`) yields `None`. + #[test] + fn crc1_boundary_rejects_truncated() { + assert_eq!(crc1_boundary_bytes(0), None); + assert_eq!(crc1_boundary_bytes(3), None); + assert_eq!(crc1_boundary_bytes(4), Some(2)); // boundary land at field start + } + + /// CrcStatus::all_ok semantics — None fields are treated as + /// pass, matching the spec's "accept on either CRC valid" + /// leniency. + #[test] + fn crc_status_all_ok_treats_none_as_pass() { + let s = CrcStatus { + crc1_ok: None, + crc2_ok: Some(true), + }; + assert!(s.all_ok()); + let s = CrcStatus { + crc1_ok: Some(true), + crc2_ok: Some(true), + }; + assert!(s.all_ok()); + let s = CrcStatus { + crc1_ok: Some(false), + crc2_ok: Some(true), + }; + assert!(!s.all_ok()); + let s = CrcStatus { + crc1_ok: Some(true), + crc2_ok: Some(false), + }; + assert!(!s.all_ok()); + } + + /// Truncated AC-3 syncframe reports both checks as failed + /// rather than panicking on the slice index. + #[test] + fn verify_ac3_truncated_buffer_reports_failure() { + let buf = vec![0x0B, 0x77, 0x00, 0x00]; + let s = verify_ac3_syncframe(&buf, 768); + assert_eq!(s.crc1_ok, Some(false)); + assert_eq!(s.crc2_ok, Some(false)); + assert!(!s.all_ok()); + } + + /// Truncated E-AC-3 syncframe reports `crc2_ok = Some(false)` + /// and `crc1_ok = None` (no field exists). + #[test] + fn verify_eac3_truncated_buffer_reports_failure() { + let buf = vec![0x0B, 0x77]; + let s = verify_eac3_syncframe(&buf, 768); + assert_eq!(s.crc1_ok, None); + assert_eq!(s.crc2_ok, Some(false)); + } + + /// Synthetic CRC-clean frame: a 256-byte buffer where the + /// crc1 prefix has been solved so the LFSR is zero at the + /// 5/8 point, and the trailing 2 bytes are filled with the + /// *augmented* CRC (`ac3_crc_update(0, data || [0, 0])`) so + /// the LFSR residue is zero at the end of the whole frame + /// too. This mirrors what a §7.10.1-compliant encoder + /// writes for `crc2`. + #[test] + fn synthetic_frame_passes_both_checks() { + let frame_bytes = 256usize; + let mut frame = vec![0u8; frame_bytes]; + frame[0] = 0x0B; + frame[1] = 0x77; + // Pad the body with non-trivial content so neither residue + // is trivially zero from the data. + for i in 5..(frame_bytes - 2) { + frame[i] = ((i * 31 + 7) & 0xFF) as u8; + } + // Solve crc1 over bytes 2..crc1_end so the running CRC is + // zero at the 5/8 point. + let crc1_end = crc1_boundary_bytes(frame_bytes).unwrap(); + let x = ac3_crc_solve_prefix(&frame[2..crc1_end]); + frame[2] = (x >> 8) as u8; + frame[3] = (x & 0xFF) as u8; + debug_assert_eq!(ac3_crc_update(0, &frame[2..crc1_end]), 0); + // crc2 = augmented CRC of the post-syncword region minus + // the trailing 2 bytes. Appending two zero bytes to the + // payload flushes the register through 16 more shifts; + // the resulting state equals `payload·x^16 mod g(x)`. When + // that value is then written back into the trailing 2 + // bytes, shifting it through transitions the register to + // zero (`(payload·x^16 + crc2) mod g = 0`). + let mut padded = Vec::with_capacity(frame_bytes - 2 + 2); + padded.extend_from_slice(&frame[2..(frame_bytes - 2)]); + padded.extend_from_slice(&[0, 0]); + let crc2_val = ac3_crc_update(0, &padded); + frame[frame_bytes - 2] = (crc2_val >> 8) as u8; + frame[frame_bytes - 1] = (crc2_val & 0xFF) as u8; + + let status = verify_ac3_syncframe(&frame, frame_bytes); + assert_eq!(status.crc1_ok, Some(true), "crc1 residue should be zero"); + assert_eq!(status.crc2_ok, Some(true), "crc2 residue should be zero"); + assert!(status.all_ok()); + } + + /// Flipping any single bit in the post-syncword region of a + /// CRC-clean frame breaks at least one of the two checks. This + /// is the central error-detection property of §7.10.1 ("CRC + /// check is reliable to 0.0015 percent"). + #[test] + fn single_bit_flip_breaks_verification() { + let frame_bytes = 256usize; + let mut frame = vec![0u8; frame_bytes]; + frame[0] = 0x0B; + frame[1] = 0x77; + for i in 5..(frame_bytes - 2) { + frame[i] = ((i * 31 + 7) & 0xFF) as u8; + } + let crc1_end = crc1_boundary_bytes(frame_bytes).unwrap(); + let x = ac3_crc_solve_prefix(&frame[2..crc1_end]); + frame[2] = (x >> 8) as u8; + frame[3] = (x & 0xFF) as u8; + // Augmented-form crc2 (see `synthetic_frame_passes_both_checks`). + let mut padded = Vec::with_capacity(frame_bytes - 2 + 2); + padded.extend_from_slice(&frame[2..(frame_bytes - 2)]); + padded.extend_from_slice(&[0, 0]); + let crc2_val = ac3_crc_update(0, &padded); + frame[frame_bytes - 2] = (crc2_val >> 8) as u8; + frame[frame_bytes - 1] = (crc2_val & 0xFF) as u8; + // Baseline: clean frame validates. + assert!(verify_ac3_syncframe(&frame, frame_bytes).all_ok()); + // Flip a single bit deep in the body. Either crc1 (if the + // flip is in the 5/8 region) or crc2 (if past it) must fail + // — and a flip in the 5/8 region also fails crc2 because the + // running register can't recover at the 5/8 boundary. + let pos = 17; + frame[pos] ^= 0x01; + let status = verify_ac3_syncframe(&frame, frame_bytes); + assert!( + !status.all_ok(), + "single-bit flip should fail at least one CRC" + ); + } + + /// E-AC-3 path: a frame with `crc2` set to the augmented CRC + /// (`ac3_crc_update(0, payload || [0, 0])`) verifies cleanly + /// as a residue check. + #[test] + fn eac3_synthetic_frame_passes_crc2() { + let frame_bytes = 384usize; + let mut frame = vec![0u8; frame_bytes]; + frame[0] = 0x0B; + frame[1] = 0x77; + for i in 2..(frame_bytes - 2) { + frame[i] = ((i * 23 + 11) & 0xFF) as u8; + } + let mut padded = Vec::with_capacity(frame_bytes - 2 + 2); + padded.extend_from_slice(&frame[2..(frame_bytes - 2)]); + padded.extend_from_slice(&[0, 0]); + let crc2_val = ac3_crc_update(0, &padded); + frame[frame_bytes - 2] = (crc2_val >> 8) as u8; + frame[frame_bytes - 1] = (crc2_val & 0xFF) as u8; + let status = verify_eac3_syncframe(&frame, frame_bytes); + assert_eq!(status.crc1_ok, None); + assert_eq!(status.crc2_ok, Some(true)); + assert!(status.all_ok()); + } + + /// E-AC-3 flip: corrupting the body breaks the crc2 residue. + #[test] + fn eac3_bit_flip_breaks_crc2() { + let frame_bytes = 384usize; + let mut frame = vec![0u8; frame_bytes]; + frame[0] = 0x0B; + frame[1] = 0x77; + for i in 2..(frame_bytes - 2) { + frame[i] = ((i * 23 + 11) & 0xFF) as u8; + } + let mut padded = Vec::with_capacity(frame_bytes - 2 + 2); + padded.extend_from_slice(&frame[2..(frame_bytes - 2)]); + padded.extend_from_slice(&[0, 0]); + let crc2_val = ac3_crc_update(0, &padded); + frame[frame_bytes - 2] = (crc2_val >> 8) as u8; + frame[frame_bytes - 1] = (crc2_val & 0xFF) as u8; + assert!(verify_eac3_syncframe(&frame, frame_bytes).all_ok()); + frame[100] ^= 0x10; + assert!(!verify_eac3_syncframe(&frame, frame_bytes).all_ok()); + } +} diff --git a/crates/vendor/oxideav-ac3/src/decoder.rs b/crates/vendor/oxideav-ac3/src/decoder.rs new file mode 100644 index 00000000..0c26c5a5 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/decoder.rs @@ -0,0 +1,1010 @@ +//! AC-3 packet → AudioFrame decoder. +//! +//! The decoder runs the full §7 DSP pipeline: syncinfo + BSI parsing, +//! audio-block exponent decode, parametric bit allocation, mantissa +//! dequantization, channel decoupling, rematrixing (for 2/0 streams), +//! dynamic-range scaling, 512-point IMDCT with KBD window, and 50% +//! overlap-add across audio blocks. The per-frame output is 1536 S16 +//! samples per channel exactly as specified by §8.2.1.2. + +use oxideav_core::Decoder; +use oxideav_core::{ + AudioFrame, CodecId, CodecParameters, Error, Frame, Packet, Result, SampleFormat, TimeBase, +}; + +use crate::audblk::{self, Ac3State, BLOCKS_PER_FRAME, SAMPLES_PER_BLOCK}; +use crate::bsi::{self, Bsi}; +use crate::crc::{self, CrcStatus}; +use crate::downmix::{Downmix, DownmixMode}; +use crate::drc::DrcSettings; +use crate::eac3; +use crate::syncinfo::{self, SyncInfo}; +use crate::wave_order; + +/// Samples produced per AC-3 syncframe, per channel: 6 blocks × 256 +/// new samples each (each audio block is a 512-point TDAC transform +/// overlapping by 256 samples with its neighbour — §2.2). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub const SAMPLES_PER_FRAME: u32 = 1536; + +pub fn make_decoder(params: &CodecParameters) -> Result> { + Ok(Box::new(Ac3Decoder { + codec_id: params.codec_id.clone(), + time_base: TimeBase::new(1, 48_000), + pending: None, + eof: false, + state: Ac3State::new(), + eac3_state: eac3::Eac3DecoderState::default(), + requested_channels: params.channels, + prefer_ltrt: false, + drc: DrcSettings::default(), + })) +} + +/// Dedicated E-AC-3 decoder factory. Identical to [`make_decoder`] — +/// the same `Ac3Decoder` struct dispatches on the per-packet bsid — +/// but registered with the `eac3` codec id so the registry's +/// container-tag lookup hits it for `A_EAC3` / `0xA7` / etc. +pub fn make_eac3_decoder(params: &CodecParameters) -> Result> { + Ok(Box::new(Ac3Decoder { + codec_id: params.codec_id.clone(), + time_base: TimeBase::new(1, 48_000), + pending: None, + eof: false, + state: Ac3State::new(), + eac3_state: eac3::Eac3DecoderState::default(), + requested_channels: params.channels, + prefer_ltrt: false, + drc: DrcSettings::default(), + })) +} + +/// Variant of [`make_decoder`] that selects the §7.8.2 **LtRt** +/// (Dolby Surround matrix-encoded) downmix when a 2-channel target is +/// requested. Equivalent to `make_decoder` when the caller did not +/// request a stereo downmix (`params.channels != Some(2)` or the +/// source is already mono/stereo). The LtRt downmix preserves +/// surround information so a downstream matrix decoder (Pro Logic +/// et al.) can recover Ls/Rs from the stereo pair; LoRo's +/// straight-sum mix is unrecoverable in that sense. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn make_decoder_ltrt(params: &CodecParameters) -> Result> { + Ok(Box::new(Ac3Decoder { + codec_id: params.codec_id.clone(), + time_base: TimeBase::new(1, 48_000), + pending: None, + eof: false, + state: Ac3State::new(), + eac3_state: eac3::Eac3DecoderState::default(), + requested_channels: params.channels, + prefer_ltrt: true, + drc: DrcSettings::default(), + })) +} + +/// Build an AC-3 / E-AC-3 decoder with an explicit §6.1.9 / §7.7 dynamic- +/// range-control + §7.6 dialogue-normalisation configuration (see +/// [`crate::drc::DrcSettings`]). Equivalent to [`make_decoder`] followed +/// by [`Ac3Decoder::set_drc`], but returns the boxed trait object directly +/// so a registry consumer can request, e.g., heavy-compression "RF mode" +/// output without down-casting. +/// +/// The same struct dispatches AC-3 vs E-AC-3 on the per-packet `bsid`, so +/// the configured DRC regime applies to both syntaxes. +pub fn make_decoder_with_drc( + params: &CodecParameters, + drc: DrcSettings, +) -> Result> { + let mut dec = Ac3Decoder { + codec_id: params.codec_id.clone(), + time_base: TimeBase::new(1, 48_000), + pending: None, + eof: false, + state: Ac3State::new(), + eac3_state: eac3::Eac3DecoderState::default(), + requested_channels: params.channels, + prefer_ltrt: false, + drc: DrcSettings::default(), + }; + dec.set_drc(drc); + Ok(Box::new(dec)) +} + +struct Ac3Decoder { + codec_id: CodecId, + time_base: TimeBase, + pending: Option, + eof: bool, + state: Ac3State, + /// Per-decoder E-AC-3 state — empty in round 1 (no overlap-add + /// delay yet), present so round 2 can park dependent-substream + /// recombination scratch + per-channel IMDCT history without + /// changing this struct's layout. + eac3_state: eac3::Eac3DecoderState, + /// Downmix target channel count — `Some(1)` = mono, `Some(2)` = + /// stereo, `None` = passthrough of whatever the bitstream carries. + /// Drives the §7.8 matrix in [`Ac3Decoder::process_frame`]. + requested_channels: Option, + /// When `true` and a 2-channel downmix is requested, use the + /// §7.8.2 **LtRt** (Dolby Surround matrix-encoded) equations + /// instead of LoRo. Toggled by [`make_decoder_ltrt`]; the regular + /// [`make_decoder`] / [`make_eac3_decoder`] factories leave this + /// off (LoRo is §7.8.2's "preferred when mono is the ultimate + /// target" path and is the spec's default downmix matrix). + prefer_ltrt: bool, + /// §6.1.9 / §7.7 dynamic-range-control + §7.6 dialogue-normalisation + /// settings. [`Default`] is line-out (full `dynrng`, no heavy + /// compression, no dialnorm playback normalisation), so the decoder's + /// default output is the mandatory §7.7.1 decode. Steered via + /// [`Ac3Decoder::set_drc`]. + drc: DrcSettings, +} + +impl Decoder for Ac3Decoder { + fn codec_id(&self) -> &CodecId { + &self.codec_id + } + + fn send_packet(&mut self, packet: &Packet) -> Result<()> { + if self.pending.is_some() { + return Err(Error::other( + "AC-3 decoder: receive_frame must be called before sending another packet", + )); + } + self.pending = Some(packet.clone()); + Ok(()) + } + + fn receive_frame(&mut self) -> Result { + let pkt = match self.pending.take() { + Some(p) => p, + None => { + return if self.eof { + Err(Error::Eof) + } else { + Err(Error::NeedMore) + } + } + }; + self.process_frame(&pkt) + } + + fn flush(&mut self) -> Result<()> { + self.eof = true; + Ok(()) + } + + fn reset(&mut self) -> Result<()> { + self.pending = None; + self.eof = false; + self.state = Ac3State::new(); + self.eac3_state = eac3::Eac3DecoderState::default(); + // Preserve the configured DRC regime across a flush/reset — it is + // a decoder-lifetime listener setting, not per-frame state. + self.state.drc = self.drc; + self.eac3_state.set_drc(self.drc); + Ok(()) + } +} + +impl Ac3Decoder { + /// Configure the §6.1.9 / §7.7 dynamic-range-control + §7.6 + /// dialogue-normalisation behaviour applied to subsequent frames. + /// + /// The default is [`DrcSettings::line_out`] — the mandatory §7.7.1 + /// full-`dynrng` decode with no dialnorm playback normalisation. + /// Switching to [`DrcSettings::rf_mode`] substitutes the heavy- + /// compression `compr` word (§7.7.2), [`DrcSettings::partial`] applies + /// the §7.7.1.2 cut/boost factors, and + /// [`DrcSettings::with_dialnorm_target`] adds §7.6 playback + /// normalisation toward a chosen headroom target. + pub fn set_drc(&mut self, drc: DrcSettings) { + self.drc = drc; + self.state.drc = drc; + self.eac3_state.set_drc(drc); + } + + fn process_frame(&mut self, pkt: &Packet) -> Result { + let data = &pkt.data[..]; + if data.len() < 5 { + return Err(Error::invalid("ac3: packet too short for syncinfo")); + } + // Top-level dispatch: peek at the bsid byte to choose AC-3 + // vs E-AC-3. The 5-bit `bsid` field sits at byte 5 (top 5 + // bits) in BOTH syntaxes: + // + // AC-3: syncword(2B) + crc1(2B) + fscod+frmsizecod(1B) + // ⇒ BSI starts at byte 5; bsid is the first 5 + // bits = byte 5 top 5 bits. + // E-AC-3: syncword(2B) + strmtyp+substreamid+frmsiz(2B) + + // fscod+(numblkscod|fscod2)+acmod+lfeon(1B) ⇒ bsid + // starts at byte 5 bit 0 = byte 5 top 5 bits. + // + // So `data[5] >> 3` is bsid in either layout. Per §E.2.3.1.6, + // bsid 0..8 is base AC-3, 9/10 are reserved (we tolerate them + // via the same AC-3 path), and 11..16 routes to Annex E. + let try_ac3 = syncinfo::parse(data); + if let Ok(si) = try_ac3 { + // bsid lives in BSI byte 0 (= packet byte 5), top 5 bits. + // The first BSI byte sits at exactly the same place in + // both AC-3 (after 5 bytes of syncinfo) and E-AC-3 (after + // 16-bit syncword + 16-bit strmtyp/substreamid/frmsiz + + // 8-bit fscod/numblkscod/acmod/lfeon = 5 bytes). Whether + // the value at byte 5's top 5 bits parses as bsid in BOTH + // syntaxes is a documented spec property — see §E.2.3.1. + if data.len() > 5 { + let bsi_byte0 = data[5]; + let bsid = bsi_byte0 >> 3; + if bsid <= bsi::MAX_BSID_BASE { + return self.process_ac3_frame(pkt, si); + } + } + } + // E-AC-3 path. The AC-3 syncinfo path may have rejected the + // packet entirely (frmsizecod past Table 5.18) — that's still + // a valid E-AC-3 syncframe. Hand the whole packet to the + // Annex E decoder. + self.process_eac3_frame(pkt) + } + + fn process_eac3_frame(&mut self, pkt: &Packet) -> Result { + let data = &pkt.data[..]; + let decoded = eac3::decode_eac3_packet(&mut self.eac3_state, data)?; + let channels = decoded.channels; + + // Resolve the §7.8 downmix mode from the requested target. + // Annex E's `nfchans` (excludes LFE) drives the mode picker — + // an Eac3 5.1 stream has nfchans=5 and resolves to Stereo / + // StereoLtRt / Mono just like AC-3 does. + let dmx_mode = { + let base = DownmixMode::resolve(self.requested_channels, decoded.nfchans); + if self.prefer_ltrt && matches!(base, DownmixMode::Stereo) { + DownmixMode::StereoLtRt + } else { + base + } + }; + + // Active downmix? Walk the f32 PCM through the §7.8 matrix so + // negative LtRt surround weights don't truncate to 0 after a + // pre-quantised S16 input. Falls back to the s16 truncate-then- + // reorder path when no downmix is needed (passthrough) — that + // path also keeps the dep-substream-extended channels intact. + // §7.6 dialogue-normalisation playback scalar (opt-in; unity + // unless a dialnorm target was configured). Computed once per + // frame from the indep substream's dialnorm word. + let dn_gain = self.drc.dialnorm_gain(decoded.dialnorm); + + let (pcm, out_channels) = if matches!(dmx_mode, DownmixMode::Passthrough) { + let mut pcm = decoded.pcm_s16le; + if dn_gain != 1.0 { + // Scale the already-quantised S16 samples in place. + for chunk in pcm.chunks_exact_mut(2) { + let v = i16::from_le_bytes([chunk[0], chunk[1]]); + let scaled = (v as f32 * dn_gain).clamp(-32768.0, 32767.0) as i16; + let le = scaled.to_le_bytes(); + chunk[0] = le[0]; + chunk[1] = le[1]; + } + } + // Reorder bitstream-order multichannel layouts into WAV-mask + // order for the indep substream. For dep-extended programs + // (e.g. 7.1 emitted as indep 5.1 + dep [Lb,Rb]) the buffer's + // channel count exceeds the indep `output_channels(acmod, + // lfeon)` and the reorder no-ops via its channel-count + // guard — extended channels stay in bitstream order. + wave_order::reorder_s16le_in_place( + &mut pcm, + decoded.acmod, + decoded.lfeon, + channels as usize, + ); + (pcm, channels) + } else { + // Build a Downmix that honours Annex E mixmdata (Tables + // E1.13-16 / D2.3-6) when present. Without mixmdata the + // matrix uses the §7.8.2 fixed 0.707 defaults — identical + // to the previous "truncate-to-2-channels" behaviour for + // a 2/0 stereo source but spec-correct for 5.1 → LtRt / + // LoRo where the Annex D path already proved out. + let dmx = Downmix::from_eac3_fields( + decoded.acmod, + decoded.nfchans, + channels as u8, + decoded.lfeon, + decoded.annex_e_mix_levels, + dmx_mode, + ); + let out_ch = dmx.output_channels() as usize; + let src_f32 = self.eac3_state.indep_pcm_f32(); + let n_frames = decoded.samples as usize; + // Defensive — should never fire unless the eac3 state is + // out of sync with `decoded`. + if src_f32.len() != n_frames * channels as usize { + return Err(Error::invalid(format!( + "eac3 downmix: f32 scratch len {} != frames*ch {}*{}", + src_f32.len(), + n_frames, + channels, + ))); + } + let nfchans = decoded.nfchans as usize; + let nchans = channels as usize; + let mut out_f32 = vec![0.0f32; n_frames * out_ch]; + // §7.8 matrix is applied in fixed-size SAMPLES_PER_BLOCK + // chunks (the §2.2 256-sample block window the encoder also + // works in). Annex E doesn't change the block size; one + // syncframe is `num_blocks * 256` samples per channel. + let nblocks = n_frames / SAMPLES_PER_BLOCK; + for blk in 0..nblocks { + let mut per_ch: [[f32; SAMPLES_PER_BLOCK]; 5] = [[0.0; SAMPLES_PER_BLOCK]; 5]; + let base = blk * SAMPLES_PER_BLOCK * nchans; + for n in 0..SAMPLES_PER_BLOCK { + for ch in 0..nfchans.min(5) { + // §7.6 dialnorm scalar folded into the matrix input + // (unity unless a dialnorm target is configured). + per_ch[ch][n] = src_f32[base + n * nchans + ch] * dn_gain; + } + } + let out_base = blk * SAMPLES_PER_BLOCK * out_ch; + dmx.apply( + &per_ch, + SAMPLES_PER_BLOCK, + &mut out_f32[out_base..out_base + SAMPLES_PER_BLOCK * out_ch], + ); + } + // Pack f32 → S16LE. + let mut out_bytes = vec![0u8; out_f32.len() * 2]; + for (i, s) in out_f32.iter().enumerate() { + let clamped = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + let le = clamped.to_le_bytes(); + out_bytes[i * 2] = le[0]; + out_bytes[i * 2 + 1] = le[1]; + } + (out_bytes, out_ch as u16) + }; + + self.time_base = TimeBase::new(1, decoded.sample_rate as i64); + let _ = out_channels; // surfaced for future AudioFrame channel-count + Ok(Frame::Audio(AudioFrame { + samples: decoded.samples, + pts: pkt.pts, + data: vec![pcm], + })) + } + + fn process_ac3_frame(&mut self, pkt: &Packet, si: SyncInfo) -> Result { + let data = &pkt.data[..]; + if (si.frame_length as usize) > data.len() { + return Err(Error::invalid(format!( + "ac3: packet short: frame_length={} pkt_len={}", + si.frame_length, + data.len() + ))); + } + let bsi: Bsi = bsi::parse(&data[5..])?; + + let src_channels = bsi.nchans as u16; + let sample_rate = si.sample_rate; + self.time_base = TimeBase::new(1, sample_rate as i64); + + // 1) Decode the syncframe into a source-layout interleaved + // f32 buffer. This contains `nfchans + lfe` channels. + let src_samples = SAMPLES_PER_FRAME as usize * src_channels as usize; + let mut floats = vec![0.0f32; src_samples]; + audblk::decode_frame( + &mut self.state, + &si, + &bsi, + &data[..si.frame_length as usize], + &mut floats, + )?; + debug_assert_eq!( + floats.len(), + BLOCKS_PER_FRAME * SAMPLES_PER_BLOCK * src_channels as usize + ); + + // §7.6 dialogue-normalisation playback scalar (opt-in). Applied to + // the source-layout PCM before downmix so every output channel + // inherits the same normalisation. Unity when no dialnorm target + // is configured (the spec default — dialnorm is advisory). + let dn_gain = self.drc.dialnorm_gain(bsi.dialnorm); + if dn_gain != 1.0 { + for s in floats.iter_mut() { + *s *= dn_gain; + } + } + + // 2) Pick a §7.8 downmix mode from the requested output channel + // count (falls back to passthrough when unset or equal to + // source width). When `prefer_ltrt` is set, promote a + // LoRo (`Stereo`) selection to LtRt — Mono / Passthrough + // are unaffected (LtRt is a stereo-target option only, + // §7.8.2 explicitly notes "if the LtRt downmix is combined + // to mono, the surround information will be lost"). + let dmx_mode = { + let base = DownmixMode::resolve(self.requested_channels, bsi.nfchans); + if self.prefer_ltrt && matches!(base, DownmixMode::Stereo) { + DownmixMode::StereoLtRt + } else { + base + } + }; + let (out_channels, out_samples) = if matches!(dmx_mode, DownmixMode::Passthrough) { + (src_channels, floats.clone()) + } else { + let dmx = Downmix::from_bsi(&bsi, dmx_mode); + let out_ch = dmx.output_channels() as usize; + let mut out = vec![0.0f32; SAMPLES_PER_FRAME as usize * out_ch]; + // Walk each audio block; gather fbw channel rows into the + // downmixer's `[[f32; 256]; 5]` slot format, then apply. + // LFE lives at fbw index `nfchans` in the source interleaved + // buffer and is ignored by the downmix (§7.8 explicitly + // allows any coefficient for LFE; we choose zero). + let nfchans = bsi.nfchans as usize; + let nchans = src_channels as usize; + for blk in 0..BLOCKS_PER_FRAME { + let mut per_ch: [[f32; SAMPLES_PER_BLOCK]; 5] = [[0.0; SAMPLES_PER_BLOCK]; 5]; + let base = blk * SAMPLES_PER_BLOCK * nchans; + for n in 0..SAMPLES_PER_BLOCK { + for ch in 0..nfchans.min(5) { + per_ch[ch][n] = floats[base + n * nchans + ch]; + } + } + let out_base = blk * SAMPLES_PER_BLOCK * out_ch; + dmx.apply( + &per_ch, + SAMPLES_PER_BLOCK, + &mut out[out_base..out_base + SAMPLES_PER_BLOCK * out_ch], + ); + } + (out_ch as u16, out) + }; + + // 3) Pack f32 → S16 interleaved. + let bytes_per_sample = SampleFormat::S16.bytes_per_sample(); + let total_bytes = SAMPLES_PER_FRAME as usize * out_channels as usize * bytes_per_sample; + let mut out_bytes = vec![0u8; total_bytes]; + for (i, s) in out_samples.iter().enumerate() { + let clamped = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + let le = clamped.to_le_bytes(); + out_bytes[i * 2] = le[0]; + out_bytes[i * 2 + 1] = le[1]; + } + + // 4) Reorder bitstream-order channels into WAV-mask order so + // consumers that interpret the PCM as a WAVE file (or any + // `WAVE_FORMAT_EXTENSIBLE`-compliant sink — `pcm_s16le`, + // foobar2000, miniaudio, …) see (FL, FR, FC, LFE, BL, BR) + // instead of AC-3's bitstream (L, C, R, Ls, Rs, LFE). + // Mono / stereo / 2/1 / 2/2 layouts are no-ops; only + // acmod ∈ {3, 5, 7} (the front-center-bearing modes) get + // permuted. When downmix is active the output is already + // in standard order — `out_channels < src_channels` skips + // the reorder via [`wave_order::output_channels`] check. + if matches!(dmx_mode, DownmixMode::Passthrough) { + wave_order::reorder_s16le_in_place( + &mut out_bytes, + bsi.acmod, + bsi.lfeon, + out_channels as usize, + ); + } + + Ok(Frame::Audio(AudioFrame { + samples: SAMPLES_PER_FRAME, + pts: pkt.pts, + data: vec![out_bytes], + })) + } +} + +/// Verify the §7.10.1 CRC fields of a single AC-3 or E-AC-3 +/// syncframe. +/// +/// `syncframe` must start with the 0x0B77 syncword. The function +/// peeks `bsid` at byte 5 (top 5 bits) to choose between the AC-3 +/// double-CRC path (`bsid ≤ 10`) and the Annex E single-`crc2` +/// path (`bsid ≥ 11`). The frame length is parsed out of the +/// header on each path: AC-3 reads `(fscod, frmsizecod)` and looks +/// up Table 5.18; E-AC-3 reads the 11-bit `frmsiz` and computes +/// `(frmsiz + 1) * 2`. +/// +/// Per §6.1.2 the spec lets a decoder be lenient — accept on +/// either CRC valid — or strict (require both). [`CrcStatus`] +/// surfaces both checks so the caller can implement whichever +/// policy suits the carriage (file decode vs broadcast tuner). +/// +/// Returns `Err(Error::Invalid)` only on a malformed header +/// (bad syncword, reserved fscod / frmsizecod, or `syncframe` +/// shorter than the parsed `frame_length`); a real CRC failure +/// against a well-formed header lands as `crc{1,2}_ok: Some(false)`. +pub fn verify_packet_crc(syncframe: &[u8]) -> Result { + if syncframe.len() < 6 { + return Err(Error::invalid( + "ac3: syncframe shorter than syncinfo+bsid byte for CRC check", + )); + } + // Peek bsid at byte 5 (top 5 bits) — same trick as the per-packet + // dispatch in `process_frame`. + let bsid = syncframe[5] >> 3; + if bsid <= bsi::MAX_BSID_BASE { + // AC-3 path. Parse syncinfo to discover the frame length. + let si = syncinfo::parse(syncframe)?; + let frame_bytes = si.frame_length as usize; + if syncframe.len() < frame_bytes { + return Err(Error::invalid(format!( + "ac3: syncframe is {} bytes, frame_length says {}", + syncframe.len(), + frame_bytes + ))); + } + Ok(crc::verify_ac3_syncframe(syncframe, frame_bytes)) + } else { + // E-AC-3 path. Parse the 11-bit frmsiz out of bytes 2..4 + // (top 5 bits of byte 2 are strmtyp(2) + substreamid(3); the + // low 3 bits of byte 2 + all of byte 3 are frmsiz). Frame + // bytes = (frmsiz + 1) * 2 per §E.1.2. + let b2 = syncframe[2] as u16; + let b3 = syncframe[3] as u16; + let frmsiz = ((b2 & 0x07) << 8) | b3; + let frame_bytes = ((frmsiz as usize) + 1) * 2; + if syncframe.len() < frame_bytes { + return Err(Error::invalid(format!( + "eac3: syncframe is {} bytes, frmsiz implies {}", + syncframe.len(), + frame_bytes + ))); + } + Ok(crc::verify_eac3_syncframe(syncframe, frame_bytes)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::{CodecId, CodecParameters}; + + /// A decoder build must succeed for the canonical codec id. + #[test] + fn decoder_builds() { + let params = CodecParameters::audio(CodecId::new("ac3")); + let dec = make_decoder(¶ms).unwrap(); + assert_eq!(dec.codec_id().as_str(), "ac3"); + } + + /// The LtRt factory must accept the same parameters as the default + /// factory and produce a working decoder. + #[test] + fn ltrt_decoder_builds() { + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.channels = Some(2); + let dec = make_decoder_ltrt(¶ms).unwrap(); + assert_eq!(dec.codec_id().as_str(), "ac3"); + } + + /// E-AC-3 5.1 encoded packet → decode with `channels = Some(2)` + /// must run the §7.8 LoRo matrix end-to-end (not truncate the + /// channel set to the first two). The encoder produces a fresh 5.1 + /// indep substream; the decoder is configured for stereo output; + /// the resulting `AudioFrame` payload is exactly 2 ch × 1536 + /// samples × 2 bytes per frame. + /// + /// Round 129 wires `Downmix::from_eac3_fields` through + /// [`Ac3Decoder::process_eac3_frame`]; this test exercises that + /// new path end-to-end and locks in the output buffer shape + + /// the fact that both output channels still carry non-trivial + /// energy (the matrix coefficients pull C / Ls / Rs into both Lo + /// and Ro, so a constant-amplitude sine on every channel keeps a + /// recognisable envelope after the matrix). + #[test] + fn eac3_5_1_decodes_to_stereo_with_matrix_downmix() { + use oxideav_core::Packet; + use oxideav_core::TimeBase as TB; + // Encode a 5.1 sine fixture at 384 kbps so the indep substream + // has all six channels active (5 fbw + LFE). + let mut enc_params = CodecParameters::audio(CodecId::new(eac3::CODEC_ID_STR)); + enc_params.sample_rate = Some(48_000); + enc_params.channels = Some(6); + enc_params.sample_format = Some(SampleFormat::S16); + enc_params.bit_rate = Some(384_000); + let mut enc = match eac3::make_encoder(&enc_params) { + Ok(e) => e, + Err(e) => { + eprintln!("eac3 make_encoder failed: {e} — skipping"); + return; + } + }; + + // 1536 samples × 6 channels (interleaved S16). C carries 0.4, + // L/R carry 0.3 each, Ls/Rs -0.3 each, LFE zero. + let mut pcm = Vec::::with_capacity(1536 * 6 * 2); + for i in 0..1536 { + let t = i as f32 / 48_000.0; + let s = (2.0 * std::f32::consts::PI * 440.0 * t).sin(); + let push = |out: &mut Vec, v: f32| { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + out.extend_from_slice(&q.to_le_bytes()); + }; + push(&mut pcm, 0.3 * s); // L + push(&mut pcm, 0.4 * s); // C + push(&mut pcm, 0.3 * s); // R + push(&mut pcm, -0.3 * s); // Ls + push(&mut pcm, -0.3 * s); // Rs + push(&mut pcm, 0.0); // LFE + } + if enc + .send_frame(&Frame::Audio(AudioFrame { + samples: 1536, + pts: Some(0), + data: vec![pcm], + })) + .is_err() + { + eprintln!("eac3 encoder send_frame failed — skipping"); + return; + } + let _ = enc.flush(); + + let mut all_bytes = Vec::::new(); + loop { + match enc.receive_packet() { + Ok(p) => all_bytes.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => { + eprintln!("eac3 encoder receive_packet failed: {e} — skipping"); + return; + } + } + } + if all_bytes.is_empty() { + eprintln!("eac3 encoder produced no bytes for 5.1 input — skipping"); + return; + } + // First two bytes must be the syncword (cheap sanity check + // that we actually have an E-AC-3 elementary stream). + assert_eq!(&all_bytes[0..2], &[0x0B, 0x77]); + + // Decode with channels = Some(2) — request the LoRo downmix. + let mut dec_params = CodecParameters::audio(CodecId::new(eac3::CODEC_ID_STR)); + dec_params.sample_rate = Some(48_000); + dec_params.channels = Some(2); + dec_params.sample_format = Some(SampleFormat::S16); + let mut dec = make_eac3_decoder(&dec_params).expect("make_eac3_decoder"); + + // The decoder expects one full packet per `send_packet` call. + // The E-AC-3 encoder produces fixed 1536-byte frames at 384 + // kbps / 48 kHz / 1536 spf. Walk them one at a time. + let frame_bytes = 1536usize; + assert!(all_bytes.len() >= frame_bytes); + let mut got_any = false; + for off in (0..all_bytes.len()).step_by(frame_bytes) { + let end = (off + frame_bytes).min(all_bytes.len()); + let pkt = Packet::new(0, TB::new(1, 48_000), all_bytes[off..end].to_vec()); + if dec.send_packet(&pkt).is_err() { + continue; + } + loop { + match dec.receive_frame() { + Ok(Frame::Audio(af)) => { + got_any = true; + let expected_len = af.samples as usize * 2 * 2; + assert_eq!( + af.data[0].len(), + expected_len, + "stereo downmix payload size: want {} bytes, got {}", + expected_len, + af.data[0].len() + ); + } + Ok(_) => {} + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 decoder error: {e}"), + } + } + } + assert!( + got_any, + "decoder produced no audio frames from 5.1 → stereo path" + ); + } + + /// End-to-end CRC verification against the spec-compliant + /// FFmpeg-encoded `sine440_stereo.ac3` fixture. Walks every + /// syncframe and confirms `verify_packet_crc` reports both + /// CRC residues as zero (§7.10.1 baseline behaviour). Also + /// flips a single body bit and confirms the residue check + /// now rejects the frame. + #[test] + fn verify_packet_crc_matches_residue_on_ffmpeg_fixture() { + const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/sine440_stereo.ac3"); + // Fixture is 48 kHz / 192 kbps stereo per the existing + // ffmpeg_fixture.rs harness — Table 5.18 frmsizecod=20 → + // 768 bytes per syncframe. + let frame_bytes = 768usize; + assert!( + FIXTURE.len() >= frame_bytes, + "fixture too small for one frame" + ); + let mut nframes = 0usize; + for off in (0..FIXTURE.len()).step_by(frame_bytes) { + let end = (off + frame_bytes).min(FIXTURE.len()); + if end - off < frame_bytes { + break; + } + let status = + super::verify_packet_crc(&FIXTURE[off..end]).expect("verify_packet_crc parse"); + assert_eq!( + status.crc1_ok, + Some(true), + "frame {nframes} crc1 failed: {status:?}" + ); + assert_eq!( + status.crc2_ok, + Some(true), + "frame {nframes} crc2 failed: {status:?}" + ); + assert!(status.all_ok()); + nframes += 1; + } + assert!(nframes >= 4, "expected ≥4 frames, got {nframes}"); + + // Tamper: flip a body bit on the first frame and confirm + // at least one residue rejection. This is the §7.10.1 + // single-bit-error guarantee. + let mut tampered = FIXTURE[0..frame_bytes].to_vec(); + tampered[100] ^= 0x40; + let status = super::verify_packet_crc(&tampered).expect("verify_packet_crc parse"); + assert!( + !status.all_ok(), + "tampered frame should fail at least one CRC: {status:?}" + ); + } + + /// Our own AC-3 encoder produces both CRC words in + /// spec-compliant residue-zero form per §7.10.1: `crc1` + /// is generated by `ac3_crc_solve_prefix` so the LFSR + /// hits zero at the 5/8 boundary, and `crc2` is generated + /// in augmented form (`ac3_crc_update(0, body || [0, 0])`) + /// so the LFSR hits zero again at frame end. Walks every + /// emitted syncframe and asserts both checks. + #[test] + fn ac3_encoder_output_has_spec_correct_crc1_and_crc2() { + use crate::encoder; + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = encoder::make_encoder(¶ms).expect("make_encoder"); + let mut pcm = Vec::::with_capacity(1536 * 2 * 2); + for i in 0..1536 { + let t = i as f32 / 48_000.0; + let s = (2.0 * std::f32::consts::PI * 220.0 * t).sin() * 0.25; + let q = (s * 32767.0) as i16; + pcm.extend_from_slice(&q.to_le_bytes()); + pcm.extend_from_slice(&q.to_le_bytes()); + } + for _ in 0..3 { + enc.send_frame(&Frame::Audio(AudioFrame { + samples: 1536, + pts: Some(0), + data: vec![pcm.clone()], + })) + .expect("encoder send_frame"); + } + enc.flush().expect("encoder flush"); + let mut stream = Vec::::new(); + loop { + match enc.receive_packet() { + Ok(p) => stream.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("encoder receive_packet failed: {e}"), + } + } + assert!(!stream.is_empty(), "encoder produced no bytes"); + let mut nframes = 0; + for off in (0..stream.len()).step_by(768) { + let end = (off + 768).min(stream.len()); + if end - off < 768 { + break; + } + let status = + super::verify_packet_crc(&stream[off..end]).expect("verify_packet_crc parse"); + assert_eq!( + status.crc1_ok, + Some(true), + "encoder crc1 should be residue-zero (§7.10.1 spec compliant): {status:?}" + ); + assert_eq!( + status.crc2_ok, + Some(true), + "encoder crc2 should be residue-zero (§7.10.1 augmented form): {status:?}" + ); + assert!(status.all_ok()); + nframes += 1; + } + assert!(nframes >= 3, "expected ≥3 frames, got {nframes}"); + } + + /// E-AC-3 dispatch path: a fresh encoder packet routes + /// through `verify_eac3_syncframe`, which returns + /// `crc1_ok = None` (the field doesn't exist on Annex E + /// syncframes) and `crc2_ok = Some(true)` because the + /// E-AC-3 encoder now emits crc2 in augmented form. + #[test] + fn verify_packet_crc_dispatches_eac3_path_correctly() { + let mut enc_params = CodecParameters::audio(CodecId::new(eac3::CODEC_ID_STR)); + enc_params.sample_rate = Some(48_000); + enc_params.channels = Some(2); + enc_params.sample_format = Some(SampleFormat::S16); + enc_params.bit_rate = Some(192_000); + let mut enc = match eac3::make_encoder(&enc_params) { + Ok(e) => e, + Err(e) => { + eprintln!("eac3 make_encoder failed: {e} — skipping"); + return; + } + }; + let mut pcm = Vec::::with_capacity(1536 * 2 * 2); + for i in 0..1536 { + let t = i as f32 / 48_000.0; + let s = (2.0 * std::f32::consts::PI * 220.0 * t).sin() * 0.25; + let q = (s * 32767.0) as i16; + pcm.extend_from_slice(&q.to_le_bytes()); + pcm.extend_from_slice(&q.to_le_bytes()); + } + if enc + .send_frame(&Frame::Audio(AudioFrame { + samples: 1536, + pts: Some(0), + data: vec![pcm], + })) + .is_err() + { + eprintln!("eac3 send_frame failed — skipping"); + return; + } + let _ = enc.flush(); + let mut stream = Vec::::new(); + while let Ok(p) = enc.receive_packet() { + stream.extend_from_slice(&p.data); + } + if stream.len() < 5 { + eprintln!("eac3 encoder produced no bytes — skipping"); + return; + } + let status = super::verify_packet_crc(&stream).expect("verify_packet_crc parse"); + assert_eq!( + status.crc1_ok, None, + "E-AC-3 dispatch must report crc1_ok = None" + ); + assert_eq!( + status.crc2_ok, + Some(true), + "E-AC-3 encoder crc2 must be residue-zero (§E.1.2 / §7.10.1 augmented form): {status:?}" + ); + } + + // -- DRC control surface (§6.1.9 / §7.6 / §7.7) end-to-end tests -- + + /// Decode the in-tree `sine440_stereo.ac3` validator fixture and + /// return the RMS of the interleaved S16 output PCM under the supplied + /// DRC settings. + fn decode_fixture_rms(drc: DrcSettings) -> f64 { + use oxideav_core::Packet; + use oxideav_core::TimeBase as TB; + const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/sine440_stereo.ac3"); + let frame_bytes = 768usize; + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(48_000); + params.channels = None; // passthrough (no downmix) + params.sample_format = Some(SampleFormat::S16); + let mut dec = make_decoder_with_drc(¶ms, drc).expect("make_decoder_with_drc"); + let mut sum_sq = 0.0f64; + let mut n = 0u64; + for off in (0..FIXTURE.len()).step_by(frame_bytes) { + let end = (off + frame_bytes).min(FIXTURE.len()); + if end - off < frame_bytes { + break; + } + let pkt = Packet::new(0, TB::new(1, 48_000), FIXTURE[off..end].to_vec()); + if dec.send_packet(&pkt).is_err() { + continue; + } + while let Ok(Frame::Audio(af)) = dec.receive_frame() { + for chunk in af.data[0].chunks_exact(2) { + let v = i16::from_le_bytes([chunk[0], chunk[1]]) as f64; + sum_sq += v * v; + n += 1; + } + } + } + if n == 0 { + return 0.0; + } + (sum_sq / n as f64).sqrt() + } + + /// §7.6 dialogue normalisation: configuring a different dialnorm + /// target than the stream's authored dialnorm scales the output by + /// exactly `10^((target − dialnorm)/20)`. The validator fixture + /// carries the default dialnorm = 31; targeting 15 (a smaller + /// headroom = louder) boosts, and we confirm the ratio matches the + /// closed-form gain within quantisation noise. + #[test] + fn dialnorm_target_scales_output_by_closed_form_gain() { + let baseline = decode_fixture_rms(DrcSettings::line_out()); + if baseline < 1.0 { + eprintln!("fixture decoded to near-silence — skipping dialnorm test"); + return; + } + // The fixture's authored dialnorm is the default 31. Target 15 → + // gain 10^((15 − 31)/20) = 10^(-0.8) ≈ 0.1585 (attenuation). + let target = 15u8; + let dialnorm = 31u8; + let expected = 10f64.powf((target as f64 - dialnorm as f64) / 20.0); + let scaled = decode_fixture_rms(DrcSettings::line_out().with_dialnorm_target(target)); + let ratio = scaled / baseline; + assert!( + (ratio - expected).abs() / expected < 0.02, + "dialnorm-scaled RMS ratio {ratio:.4} != expected {expected:.4} (within 2%)" + ); + } + + /// §7.7.1.2 partial compression with cut=boost=0 ("no compression") + /// must not change the *default*-dialnorm output of a stream whose + /// dynrng words are all the 0 dB code: the fixture carries unity + /// dynrng, so removing compression is a no-op and the RMS is + /// unchanged. This locks in that the partial-compression path is + /// wired without disturbing a unity-gain stream. + #[test] + fn partial_compression_zero_is_noop_on_unity_dynrng_fixture() { + let baseline = decode_fixture_rms(DrcSettings::line_out()); + if baseline < 1.0 { + eprintln!("fixture decoded to near-silence — skipping"); + return; + } + let no_comp = decode_fixture_rms(DrcSettings::partial(0.0, 0.0)); + let ratio = no_comp / baseline; + assert!( + (ratio - 1.0).abs() < 1e-3, + "partial(0,0) changed unity-dynrng output: ratio {ratio:.5}" + ); + } + + /// RF mode on a stream with no `compr` word falls back to `dynrng` + /// (§7.7.2.1), so a unity-dynrng fixture decodes identically to + /// line-out. + #[test] + fn rf_mode_falls_back_to_dynrng_without_compr() { + let baseline = decode_fixture_rms(DrcSettings::line_out()); + if baseline < 1.0 { + eprintln!("fixture decoded to near-silence — skipping"); + return; + } + let rf = decode_fixture_rms(DrcSettings::rf_mode()); + let ratio = rf / baseline; + assert!( + (ratio - 1.0).abs() < 1e-3, + "RF mode without compr should equal line-out: ratio {ratio:.5}" + ); + } + + /// The configured DRC regime survives a `reset()` (it is a decoder- + /// lifetime listener setting, not per-frame state). + #[test] + fn drc_setting_survives_reset() { + let params = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = make_decoder_with_drc(¶ms, DrcSettings::rf_mode()).unwrap(); + dec.reset().unwrap(); + // Downcast is not available through the trait object; re-decode + // the fixture instead and confirm RF-mode fallback still holds + // after reset (a smoke check that reset didn't drop the setting). + // The concrete-type assertion is covered by the unit tests; here + // we just confirm reset() succeeds with a non-default DRC config. + assert_eq!(dec.codec_id().as_str(), "ac3"); + } +} diff --git a/crates/vendor/oxideav-ac3/src/downmix.rs b/crates/vendor/oxideav-ac3/src/downmix.rs new file mode 100644 index 00000000..b3743099 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/downmix.rs @@ -0,0 +1,1154 @@ +//! AC-3 channel-layout downmix (§7.8). +//! +//! Maps an arbitrary source `acmod` channel layout onto a 2- or 1-channel +//! output using the §7.8.1 / §7.8.2 matrix equations. The per-channel +//! weights come from §5.4.2.4 `cmixlev` / §5.4.2.5 `surmixlev` (Tables +//! 5.9 / 5.10), remapped to linear gains via the existing +//! [`crate::tables::CENTER_MIX_LEVEL`] / [`SURROUND_MIX_LEVEL`] LUTs. +//! +//! ## Scope +//! +//! - **Target layouts:** 2-channel `LoRo` (conventional stereo), +//! 2-channel `LtRt` (Dolby Surround matrix-encoded stereo per +//! §7.8.2's `Lt = L + 0.707·C − 0.707·Ls − 0.707·Rs` / `Rt = R + +//! 0.707·C + 0.707·Ls + 0.707·Rs`), and 1-channel mono. Per §7.8.2 +//! LoRo is the preferred downmix when the ultimate target is mono; +//! LtRt is selected only when the consumer wants a matrix-encoded +//! pair to feed a surround decoder downstream. +//! - **Source layouts:** every `acmod` ≥ 1 (1/0, 2/0, 3/0, 2/1, 3/1, +//! 2/2, 3/2). `acmod = 0` (dual-mono 1+1) is handled by routing Ch1 +//! into the Left output and Ch2 into the Right, which is the "Stereo" +//! dualmode path from §7.8.1. +//! - **LFE:** always dropped. §7.8 leaves the LFE downmix coefficient +//! implementation-defined; adding LFE to the stereo sum is risky +//! (speakers crossed-over to a sub bus will double-tap the bass), so +//! this decoder routes LFE to nothing — a spec-permitted default +//! matching the absence of LFE in mainstream stereo downmix output. +//! - **Overload scaling:** §7.8.2 mandates attenuating the matrix so +//! `sum-of-coefficients ≤ 1`. We compute the per-output sum exactly +//! and divide — for stereo 3/2 this lands at the spec's 0.4143 worst +//! case; for narrower layouts the scaling is looser, preserving +//! envelope. +//! +//! Construction is cheap (just a 5×2 matrix of `f32`) so callers can +//! cache a `Downmix` across syncframes or build one per block. + +use crate::bsi::{annex_d_center_mix_gain, annex_d_surround_mix_gain, AnnexDMixLevels, Bsi}; +use crate::eac3::Eac3Bsi; +use crate::tables::{CENTER_MIX_LEVEL, SURROUND_MIX_LEVEL}; + +/// Output layout requested from the decoder. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DownmixMode { + /// Leave the source channels untouched. + Passthrough, + /// Mix every source channel into a 2-channel LoRo pair (§7.8.2's + /// conventional stereo equations). + Stereo, + /// Mix every source channel into a 2-channel LtRt pair — the + /// §7.8.2 Dolby Surround matrix-encoded stereo form. Surrounds + /// fold in with opposite signs into Lt vs Rt at a fixed 0.707 + /// coefficient so a downstream matrix decoder (Pro Logic et al.) + /// can recover them. Spec equations: + /// `Lt = L + 0.707·C − 0.707·Ls − 0.707·Rs` + /// `Rt = R + 0.707·C + 0.707·Ls + 0.707·Rs` + StereoLtRt, + /// Mix every source channel into a single mono channel. + Mono, +} + +impl DownmixMode { + /// Resolve from a user-requested output channel count (`None` + /// meaning "pass through"). A requested count that matches the + /// source `nfchans` also becomes `Passthrough`, even when LFE is + /// on — AC-3 never downmixes LFE explicitly. `Some(2)` resolves + /// to [`Self::Stereo`] (LoRo); selecting LtRt requires explicit + /// API (decoder setter) since the wire `dsurmod` field advertises + /// whether the program *was* matrix-encoded but does not mandate + /// a particular downmix target. + pub fn resolve(requested: Option, source_nfchans: u8) -> Self { + match requested { + None => Self::Passthrough, + Some(1) => Self::Mono, + Some(2) if source_nfchans > 2 || source_nfchans == 0 => Self::Stereo, + Some(2) if source_nfchans == 2 => Self::Passthrough, + Some(2) if source_nfchans == 1 => Self::Stereo, + _ => Self::Passthrough, + } + } +} + +/// Pre-computed per-channel coefficients for one syncframe. The source +/// layout slot order matches `acmod`'s Table 5.8 ordering +/// `[L, C, R, Ls/S, Rs]`, with missing channels having a zero weight. +/// +/// Each `DownmixGains` row lists the weight applied to *that source slot* +/// when summed into an output channel. For stereo the two rows are +/// Left-output-coeffs and Right-output-coeffs. For mono there is one +/// row (Center-output-coeffs). +#[derive(Clone, Debug)] +pub struct Downmix { + mode: DownmixMode, + /// `out_coeffs[out_ch][src_slot]` — up to 5 slots per output. + out_coeffs: [[f32; 5]; 2], + /// Number of active output channels (1 or 2). + out_channels: u8, + /// Source acmod for spec diagnostics. + src_acmod: u8, + /// Source nfchans for arithmetic in `apply`. + src_nfchans: u8, + /// Whether LFE exists on the source (never mixed in). + src_lfe: bool, +} + +impl Downmix { + /// Build a downmix matrix from BSI state and a target mode. When + /// `mode` is [`DownmixMode::Passthrough`] the returned `Downmix` + /// still records the source layout but `out_channels` is set to + /// `nfchans + lfe`; [`Downmix::apply`] short-circuits. + pub fn from_bsi(bsi: &Bsi, mode: DownmixMode) -> Self { + // §5.4.2.4 / §5.4.2.5 — reserved code 0b11 maps to the + // "intermediate" coefficient per spec. Our `CENTER_MIX_LEVEL` + // table already repeats the middle value at index 3 so the + // reserved code resolves to 0.595 / 0.500. + let base_clev = if bsi.cmixlev == 0xFF { + 0.707 + } else { + CENTER_MIX_LEVEL[(bsi.cmixlev & 0x3) as usize] + }; + let base_slev = if bsi.surmixlev == 0xFF { + 0.707 + } else { + SURROUND_MIX_LEVEL[(bsi.surmixlev & 0x3) as usize] + }; + Self::build( + mode, + bsi.acmod, + bsi.nfchans, + bsi.nchans, + bsi.lfeon, + base_clev, + base_slev, + bsi.annex_d_mix_levels, + ) + } + + /// E-AC-3 field-by-field constructor — equivalent to + /// [`Self::from_eac3_bsi`] but lets callers that hold a + /// [`crate::eac3::DecodedFrame`] (not the parser-internal `Bsi`) + /// build a matrix without re-parsing the syncframe. `acmod` / + /// `nfchans` / `nchans` / `lfeon` / `mix` mirror the + /// `from_eac3_bsi` fields exactly. + pub fn from_eac3_fields( + acmod: u8, + nfchans: u8, + nchans: u8, + lfeon: bool, + mix: Option, + mode: DownmixMode, + ) -> Self { + Self::build(mode, acmod, nfchans, nchans, lfeon, 0.707, 0.707, mix) + } + + /// E-AC-3 (Annex E) counterpart to [`Self::from_bsi`]. Annex E + /// removes the body-spec 2-bit `cmixlev` / `surmixlev` fields and + /// instead carries refined 3-bit `ltrtcmixlev` / `lorocmixlev` / + /// `ltrtsurmixlev` / `lorosurmixlev` codewords inside the + /// `mixmdata` block (§E.2.3.1.3-6, Tables E1.13-16 = D2.3-D2.6). + /// + /// When the producer set `mixmdate == 1` the four 3-bit codes + /// override the §7.8 defaults for the LtRt and LoRo targets + /// respectively — exactly the same override that base AC-3's + /// `bsid == 6` xbsi1 block provides. Without `mixmdate` (or in + /// reduced-channel layouts where the per-channel guards skip the + /// field), the fixed §7.8.2 LtRt 0.707 and the 0.707 LoRo defaults + /// apply (mono / 2/0 stereo never have a downmix to refine). + /// + /// The Mono target keeps the §7.8.2 fixed 0.707 defaults (Annex E + /// mixmdata, like Annex D xbsi1, has no mono-specific mix levels). + pub fn from_eac3_bsi(bsi: &Eac3Bsi, mode: DownmixMode) -> Self { + // Annex E has no body-spec cmixlev/surmixlev; default to 0.707 + // (§7.8.2 "if not otherwise specified") for the mono path and + // any LoRo/LtRt downmix on a stream that elected to skip the + // mixmdata refinement. + Self::build( + mode, + bsi.acmod, + bsi.nfchans, + bsi.nchans, + bsi.lfeon, + 0.707, + 0.707, + bsi.annex_e_mix_levels, + ) + } + + /// Shared matrix-fill path for the AC-3 (`from_bsi`) and E-AC-3 + /// (`from_eac3_bsi`) constructors. Resolves the per-target + /// (`clev`, `slev`) pair from the Annex D / Annex E mix-level + /// codewords when present, falling back to the supplied + /// `base_clev` / `base_slev` (which are themselves either the + /// AC-3 body 2-bit codes or the §7.8.2 fixed 0.707 defaults for + /// Annex E). Then dispatches to `fill_stereo` / `fill_stereo_ltrt` + /// / `fill_mono` per `mode`. + #[allow(clippy::too_many_arguments)] + fn build( + mode: DownmixMode, + acmod: u8, + nfchans: u8, + nchans: u8, + lfeon: bool, + base_clev: f32, + base_slev: f32, + mix: Option, + ) -> Self { + let mut out = Self { + mode, + out_coeffs: [[0.0; 5]; 2], + out_channels: nchans, + src_acmod: acmod, + src_nfchans: nfchans, + src_lfe: lfeon, + }; + if matches!(mode, DownmixMode::Passthrough) { + return out; + } + + // Annex D §2.3.1.3-6 (and Annex E mixmdata) provide a refined + // 3-bit mix level per downmix target. When present they + // override the body / default for that target's downmix — + // missing center / surround codes (0xFF sentinel) fall back to + // the base value so a partial mixmdata block still works. + let (loro_clev, loro_slev) = match mix { + Some(m) => ( + if m.lorocmixlev == 0xFF { + base_clev + } else { + annex_d_center_mix_gain(m.lorocmixlev) + }, + if m.lorosurmixlev == 0xFF { + base_slev + } else { + annex_d_surround_mix_gain(m.lorosurmixlev) + }, + ), + None => (base_clev, base_slev), + }; + let (ltrt_clev, ltrt_slev) = match mix { + Some(m) => ( + if m.ltrtcmixlev == 0xFF { + // §7.8.2 fixed default when only the LoRo codes + // were carried (acmod-guard mismatch). + 0.707 + } else { + annex_d_center_mix_gain(m.ltrtcmixlev) + }, + if m.ltrtsurmixlev == 0xFF { + 0.707 + } else { + annex_d_surround_mix_gain(m.ltrtsurmixlev) + }, + ), + // Body §7.8.2: LtRt uses a fixed 0.707 for the C and + // surround coefficients regardless of `cmixlev`/`surmixlev`. + None => (0.707, 0.707), + }; + + match mode { + DownmixMode::Stereo => Self::fill_stereo(&mut out, acmod, loro_clev, loro_slev), + DownmixMode::StereoLtRt => { + Self::fill_stereo_ltrt(&mut out, acmod, ltrt_clev, ltrt_slev) + } + DownmixMode::Mono => Self::fill_mono(&mut out, acmod, base_clev, base_slev), + DownmixMode::Passthrough => unreachable!(), + } + out + } + + /// LoRo 2-channel downmix per §7.8.2. Source slots are indexed as + /// `L=0, C=1, R=2, Ls/S=3, Rs=4` per Table 5.8; missing channels + /// leave their coefficient at zero. Output slot 0 = Lo, slot 1 = Ro. + fn fill_stereo(out: &mut Self, acmod: u8, clev: f32, slev: f32) { + // Start with the §7.8.2 3/2 LoRo equations: + // Lo = 1·L + clev·C + slev·Ls + // Ro = 1·R + clev·C + slev·Rs + // with Table 5.8 dropping channels as the acmod narrows. + + // Source-channel presence per Table 5.8. + // acmod 0 (1+1 dual mono) is handled separately below — for all + // other acmods the bit pattern is: + // bit 0 (center): acmod == 1, 3, 5, 7 + // bit 2 (surround): acmod >= 4 + // two surround channels: acmod == 6, 7 + // two front (L/R): acmod != 1 (i.e. acmod != 1/0 mono) + match acmod { + 0 => { + // 1+1 dual mono — §7.8.1 'dualmode == Stereo' path: + // Ch1 → Lo, Ch2 → Ro. Slot layout [Ch1, _, Ch2, _, _]: + // Ch1 at slot 0, Ch2 at slot 2. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[1][2] = 1.0; + } + 1 => { + // 1/0 — pure center → both outputs get -3 dB of C. + // §7.8.1 `output_nfront == 2` path with `input_nfront==1`: + // mix center into left with –3 dB + // mix center into right with –3 dB + out.out_coeffs[0][0] = 0.707; + out.out_coeffs[1][0] = 0.707; + } + 2 => { + // 2/0 — pass through. L at slot 0, R at slot 2 per + // the [L, C, R, Ls/S, Rs] layout. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[1][2] = 1.0; + } + 3 => { + // 3/0 — L, C, R into Lo, Ro using clev. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][1] = clev; + out.out_coeffs[1][1] = clev; + out.out_coeffs[1][2] = 1.0; + } + 4 => { + // 2/1 — L, R, S. Single surround is folded into both + // outputs with slev and, per spec "0.7 * slev" factor + // for single-surround LoRo. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][3] = 0.7 * slev; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][3] = 0.7 * slev; + } + 5 => { + // 3/1 — L, C, R, S. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][1] = clev; + out.out_coeffs[0][3] = 0.7 * slev; + out.out_coeffs[1][1] = clev; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][3] = 0.7 * slev; + } + 6 => { + // 2/2 — L, R, Ls, Rs. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][3] = slev; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][4] = slev; + } + _ => { + // 3/2 (acmod=7) and any defensive fall-through — full + // L, C, R, Ls, Rs. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][1] = clev; + out.out_coeffs[0][3] = slev; + out.out_coeffs[1][1] = clev; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][4] = slev; + } + } + + out.out_channels = 2; + // Normalise per §7.8.2: divide each row so its sum is ≤ 1. + Self::normalise(&mut out.out_coeffs); + } + + /// LtRt 2-channel matrix-encoded stereo downmix per §7.8.2. The + /// 3/2 base equations are + /// `Lt = 1.0·L + clev·C − slev·Ls − slev·Rs` + /// `Rt = 1.0·R + clev·C + slev·Ls + slev·Rs` + /// and for 3/1 (single surround S folded in): + /// `Lt = 1.0·L + clev·C − slev·S` + /// `Rt = 1.0·R + clev·C + slev·S` + /// where `clev` / `slev` default to the §7.8.2 fixed 0.707 + /// coefficients but may be overridden via Annex D §2.3.1.3-4 + /// (`ltrtcmixlev` / `ltrtsurmixlev`, `bsid == 6` streams) — those + /// fields refine the C / surround gain per encoder authoring. + /// + /// Worst-case sum of |coeffs| is `1 + clev + 2·slev`. With the + /// default 0.707/0.707 it lands at 3.121 → §7.8.2 normalisation + /// scales every coefficient by 1/3.121 = 0.3204 (9.89 dB + /// attenuation; Table 7.32 headline). Stronger mix levels just + /// move the normalisation factor — the surround-channel sign + /// discipline is invariant, which is what makes the downstream + /// matrix decoder recoverable. + fn fill_stereo_ltrt(out: &mut Self, acmod: u8, clev: f32, slev: f32) { + // Slot layout [L, C, R, Ls/S, Rs] per Table 5.8. Surround + // channels enter with -slev on Lt and +slev on Rt. Center + // is symmetric (+clev on both). + let c = clev; + let k = slev; + match acmod { + 0 => { + // 1+1 dual mono — no matrix encoding makes sense + // (no surround information to preserve), so fall back + // to the §7.8.1 'Stereo' dualmode path: Ch1 → Lt, + // Ch2 → Rt. This is a sentinel choice; an LtRt request + // on a dual-mono source is degenerate but should still + // produce something playable. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[1][2] = 1.0; + } + 1 => { + // 1/0 — pure center → both outputs get the C gain + // (default -3 dB = 0.707). Symmetric with the LoRo case + // (no surround sign play). + out.out_coeffs[0][0] = c; + out.out_coeffs[1][0] = c; + } + 2 => { + // 2/0 — pass through. No surround to matrix-encode. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[1][2] = 1.0; + } + 3 => { + // 3/0 — L, C, R. No surround info; symmetric with LoRo. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][1] = c; + out.out_coeffs[1][1] = c; + out.out_coeffs[1][2] = 1.0; + } + 4 => { + // 2/1 — L, R, S. Single surround folds in with -k on + // Lt and +k on Rt (spec drops the C term). + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][3] = -k; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][3] = k; + } + 5 => { + // 3/1 — L, C, R, S. Single surround folds with opposite + // signs into Lt/Rt; C symmetric. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][1] = c; + out.out_coeffs[0][3] = -k; + out.out_coeffs[1][1] = c; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][3] = k; + } + 6 => { + // 2/2 — L, R, Ls, Rs. Both surrounds fold with + // opposite signs into Lt/Rt. C term dropped per §7.8.2 + // 'if center is missing'. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][3] = -k; + out.out_coeffs[0][4] = -k; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][3] = k; + out.out_coeffs[1][4] = k; + } + _ => { + // 3/2 (acmod=7) and defensive fall-through — the + // canonical §7.8.2 LtRt equations. + out.out_coeffs[0][0] = 1.0; + out.out_coeffs[0][1] = c; + out.out_coeffs[0][3] = -k; + out.out_coeffs[0][4] = -k; + out.out_coeffs[1][1] = c; + out.out_coeffs[1][2] = 1.0; + out.out_coeffs[1][3] = k; + out.out_coeffs[1][4] = k; + } + } + + out.out_channels = 2; + // Normalise per §7.8.2 — Self::normalise uses |coeff| so the + // negative surround weights count correctly toward the bound. + // For 3/2 with clev=slev=0.707 the row-sum-of-|coeffs| is + // 3.121, so the normalised L-coefficient is 1/3.121 = 0.3204 + // (Table 7.32's headline value); Annex D mix levels just move + // the normalisation factor without changing the sign discipline. + Self::normalise(&mut out.out_coeffs); + } + + /// Mono downmix: derive from the stereo LoRo pair and sum to mono + /// per §7.8.2 ("a simple summation of the 2 channels"). The spec + /// also gives the explicit 3/2 mono formula: + /// M = L + 2·clev·C + R + slev·Ls + slev·Rs + /// and the 3/1 form: + /// M = L + 2·clev·C + R + 1.4·slev·S + /// which we build from scratch rather than composing stereo, so we + /// can apply spec's "further scaling of 1/2" exactly once. + fn fill_mono(out: &mut Self, acmod: u8, clev: f32, slev: f32) { + // Slot layout [L, C, R, Ls/S, Rs]. + let (l, c, r, ls, rs) = match acmod { + 0 => { + // 1+1 dual mono — sum Ch1 + Ch2 with -6 dB each per + // §7.8.1 (dualmode=Stereo 1-front path). + (0.5, 0.0, 0.5, 0.0, 0.0) + } + 1 => (0.0, 1.0, 0.0, 0.0, 0.0), // 1/0 + 2 => (1.0, 0.0, 1.0, 0.0, 0.0), // 2/0 + 3 => (1.0, 2.0 * clev, 1.0, 0.0, 0.0), // 3/0 + 4 => (1.0, 0.0, 1.0, 1.4 * slev, 0.0), // 2/1 + 5 => (1.0, 2.0 * clev, 1.0, 1.4 * slev, 0.0), // 3/1 + 6 => (1.0, 0.0, 1.0, slev, slev), // 2/2 + _ => (1.0, 2.0 * clev, 1.0, slev, slev), // 3/2 (acmod=7) + }; + out.out_coeffs[0][0] = l; + out.out_coeffs[0][1] = c; + out.out_coeffs[0][2] = r; + out.out_coeffs[0][3] = ls; + out.out_coeffs[0][4] = rs; + out.out_channels = 1; + + // Normalise so the sum of coefficients is ≤ 1 (§7.8.2 overload + // guard — effectively the "further scaling of 1/2" for mono + // when the stereo sum already saturates). + let sum: f32 = out.out_coeffs[0].iter().sum(); + if sum > 1.0 { + let k = 1.0 / sum; + for v in out.out_coeffs[0].iter_mut() { + *v *= k; + } + } + } + + /// Normalise each output row so `sum(|coeff|) ≤ 1`. §7.8.2 calls + /// this "attenuating all downmix coefficients equally". For the 3/2 + /// LoRo case with default clev=slev=0.707 this matches the spec's + /// worst-case 0.4143 factor (1 / 2.414) to 3-sig-fig. + fn normalise(rows: &mut [[f32; 5]; 2]) { + for row in rows.iter_mut() { + let sum: f32 = row.iter().map(|c| c.abs()).sum(); + if sum > 1.0 { + let k = 1.0 / sum; + for v in row.iter_mut() { + *v *= k; + } + } + } + } + + /// How many channels the downmix produces, including LFE if any. + /// Passthrough returns the source count. + pub fn output_channels(&self) -> u8 { + match self.mode { + DownmixMode::Passthrough => self.src_nfchans + u8::from(self.src_lfe), + _ => self.out_channels, + } + } + + /// Whether this downmix will touch the samples at all. + pub fn is_passthrough(&self) -> bool { + matches!(self.mode, DownmixMode::Passthrough) + } + + /// Apply this downmix to one block of per-source-channel PCM. + /// + /// `src` is indexed as `src[ch][n]` with `ch` in the decoder's + /// internal fbw order (Table 5.8) — so for 3/2 the channels are + /// `[L, C, R, Ls, Rs]`. `lfe` is the optional 7th channel; + /// currently ignored. + /// + /// Writes `nsamples × output_channels()` samples into `dst` in + /// channel-interleaved layout. `dst.len()` must be at least + /// `nsamples × output_channels()` — extra trailing space is left + /// untouched. + pub fn apply(&self, src: &[[f32; 256]; 5], nsamples: usize, dst: &mut [f32]) { + debug_assert!(self.mode != DownmixMode::Passthrough); + // Map the decoder's fbw channel index to our [L, C, R, Ls/S, Rs] + // slot layout. For 1+1 dual-mono Ch1 and Ch2 live at fbw 0 / 1 + // — we alias them to slots L and R since the matrix was built + // with L/R gains for them in `fill_stereo`/`fill_mono`. For all + // other acmods we follow Table 5.8 directly. + let slot_of: [Option; 5] = match self.src_acmod { + 0 => [Some(0), None, Some(1), None, None], // Ch1, Ch2 → L, R + 1 => [Some(0), None, None, None, None], // C only at fbw 0 → slot 1? see below + 2 => [Some(0), None, Some(1), None, None], // L, R + 3 => [Some(0), Some(1), Some(2), None, None], // L, C, R + 4 => [Some(0), None, Some(1), Some(2), None], // L, R, S + 5 => [Some(0), Some(1), Some(2), Some(3), None], // L, C, R, S + 6 => [Some(0), None, Some(1), Some(2), Some(3)], // L, R, Ls, Rs + _ => [Some(0), Some(1), Some(2), Some(3), Some(4)], // 3/2 (acmod=7) + }; + // Special case acmod=1 (1/0): our single source channel is the + // center, so its matrix weights sit in slot 1. Rebuild the + // mapping with slot 1 pointing to fbw 0 and slot 0 nothing. + let slot_of = if self.src_acmod == 1 { + [None, Some(0), None, None, None] + } else { + slot_of + }; + + let nch = self.out_channels as usize; + for n in 0..nsamples { + for out_ch in 0..nch { + let coeffs = &self.out_coeffs[out_ch]; + let mut acc = 0.0f32; + for (slot, fbw) in slot_of.iter().enumerate() { + let Some(fbw_idx) = *fbw else { continue }; + let c = coeffs[slot]; + if c == 0.0 { + continue; + } + acc += c * src[fbw_idx][n]; + } + dst[n * nch + out_ch] = acc; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fake_bsi(acmod: u8, cmixlev: u8, surmixlev: u8, lfeon: bool) -> Bsi { + let nfchans = crate::tables::acmod_nfchans(acmod); + // Mirror the typed surfaces from the raw codepoints so the + // fixture exercises both views; `0xFF` collapses to `None`. + let center_mix = if cmixlev == 0xFF { + None + } else { + Some(crate::bsi::CenterMixLevel::from_code(cmixlev)) + }; + let surround_mix = if surmixlev == 0xFF { + None + } else { + Some(crate::bsi::SurroundMixLevel::from_code(surmixlev)) + }; + Bsi { + bsid: 8, + bsmod: 0, + acmod, + nfchans, + lfeon, + nchans: nfchans + u8::from(lfeon), + dialnorm: 27, + dialnorm_ch2: None, + cmixlev, + center_mix, + surmixlev, + surround_mix, + dsurmod: 0xFF, + dolby_surround_mode: None, + annex_d_mix_levels: None, + dmixmod: 0xFF, + dmixmod_preference: None, + compr: None, + compr_ch2: None, + language_code: None, + language_code_ch2: None, + dsurexmod: None, + dheadphonmod: None, + adconvtyp: None, + extra_bsi: None, + audio_production: None, + audio_production_ch2: None, + timecod1: None, + timecod2: None, + timecode_presence: crate::bsi::TimeCodePresence::NotPresent, + copyright_info: crate::bsi::CopyrightInfo::from_bits(false, true), + addbsi: None, + bits_consumed: 0, + } + } + + fn fake_bsi_annex_d( + acmod: u8, + lfeon: bool, + mix: crate::bsi::AnnexDMixLevels, + dmixmod: u8, + ) -> Bsi { + let nfchans = crate::tables::acmod_nfchans(acmod); + Bsi { + bsid: 6, + bsmod: 0, + acmod, + nfchans, + lfeon, + nchans: nfchans + u8::from(lfeon), + dialnorm: 27, + dialnorm_ch2: None, + cmixlev: 0xFF, + center_mix: None, + surmixlev: 0xFF, + surround_mix: None, + dsurmod: 0xFF, + dolby_surround_mode: None, + annex_d_mix_levels: Some(mix), + dmixmod, + // Mirror the typed view from the raw codepoint so the + // fixture exercises both surfaces. + dmixmod_preference: Some(crate::bsi::StereoDownmixPreference::from_code(dmixmod)), + compr: None, + compr_ch2: None, + language_code: None, + language_code_ch2: None, + dsurexmod: None, + dheadphonmod: None, + adconvtyp: None, + extra_bsi: None, + audio_production: None, + audio_production_ch2: None, + timecod1: None, + timecod2: None, + timecode_presence: crate::bsi::TimeCodePresence::NotPresent, + copyright_info: crate::bsi::CopyrightInfo::from_bits(false, true), + addbsi: None, + bits_consumed: 0, + } + } + + #[test] + fn stereo_downmix_3_2_sum_bounded() { + // acmod=7, default clev/slev. + let bsi = fake_bsi(7, 0, 0, true); + let d = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + // Both output rows must sum to ≤ 1. + let sum_l: f32 = d.out_coeffs[0].iter().map(|c| c.abs()).sum(); + let sum_r: f32 = d.out_coeffs[1].iter().map(|c| c.abs()).sum(); + assert!(sum_l <= 1.0 + 1e-6); + assert!(sum_r <= 1.0 + 1e-6); + // §7.8.2 says the 3/2 LoRo worst-case scale is 1/2.414 ≈ 0.4143 + // when clev=slev=0.707. The unnormalised L-row is + // [1, 0.707, 0, 0.707, 0] summing to 2.414, so after normalise + // the L-coeff (originally 1.0) should read ≈ 0.4143. + assert!((d.out_coeffs[0][0] - 0.4143).abs() < 1e-3); + } + + #[test] + fn stereo_downmix_2_0_is_identity() { + let bsi = fake_bsi(2, 0xFF, 0xFF, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + assert_eq!(d.output_channels(), 2); + // Lo-row picks up source slot 0 (L); Ro-row picks up slot 2 (R). + assert_eq!(d.out_coeffs[0][0], 1.0); + assert_eq!(d.out_coeffs[1][2], 1.0); + // Other slots zero on both rows. + for slot in 1..5 { + assert_eq!(d.out_coeffs[0][slot], 0.0); + } + for slot in [0, 1, 3, 4] { + assert_eq!(d.out_coeffs[1][slot], 0.0); + } + } + + #[test] + fn stereo_downmix_3_1() { + // 3/1 with cmixlev=0 (0.707), surmixlev=0 (0.707). + let bsi = fake_bsi(5, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + assert_eq!(d.output_channels(), 2); + // Each row should have L/R, C (clev) and S (0.7·slev) slots populated. + // Pre-normalise the row is [1, 0.707, 0, 0.4949, 0] summing to 2.2019. + let sum: f32 = d.out_coeffs[0].iter().map(|c| c.abs()).sum(); + assert!(sum <= 1.0 + 1e-6); + // Centre weight is non-zero on both rows; surround weight too. + assert!(d.out_coeffs[0][1] > 0.0); + assert!(d.out_coeffs[1][1] > 0.0); + assert!(d.out_coeffs[0][3] > 0.0); + assert!(d.out_coeffs[1][3] > 0.0); + } + + #[test] + fn mono_from_3_2_has_all_sources() { + let bsi = fake_bsi(7, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::Mono); + assert_eq!(d.output_channels(), 1); + // All five source slots must contribute. + for slot in 0..5 { + assert!( + d.out_coeffs[0][slot] > 0.0, + "mono slot {} should be >0", + slot + ); + } + // Sum bounded. + let sum: f32 = d.out_coeffs[0].iter().sum(); + assert!(sum <= 1.0 + 1e-6); + } + + #[test] + fn mono_from_mono_routes_center() { + let bsi = fake_bsi(1, 0xFF, 0xFF, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::Mono); + assert_eq!(d.output_channels(), 1); + // Only the center slot carries weight. + assert_eq!(d.out_coeffs[0][1], 1.0); + } + + #[test] + fn apply_stereo_passes_identity_for_2_0() { + let bsi = fake_bsi(2, 0xFF, 0xFF, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + let mut src: [[f32; 256]; 5] = [[0.0; 256]; 5]; + for n in 0..256 { + src[0][n] = 0.5; + src[1][n] = -0.25; + } + let mut out = vec![0.0f32; 256 * 2]; + d.apply(&src, 256, &mut out); + for n in 0..256 { + assert!((out[n * 2] - 0.5).abs() < 1e-6); + assert!((out[n * 2 + 1] - -0.25).abs() < 1e-6); + } + } + + #[test] + fn apply_stereo_on_3_2_full_scale_clamps() { + // Hand a full-scale signal on every source channel; worst-case + // normalisation must keep the output within ±1. + let bsi = fake_bsi(7, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + let mut src: [[f32; 256]; 5] = [[1.0; 256]; 5]; + // Make R (slot 2) negative so its path doesn't fully phase-cancel. + for n in 0..256 { + src[2][n] = 1.0; + } + let mut out = vec![0.0f32; 256 * 2]; + d.apply(&src, 256, &mut out); + for n in 0..256 { + assert!(out[n * 2].abs() <= 1.0 + 1e-6); + assert!(out[n * 2 + 1].abs() <= 1.0 + 1e-6); + } + } + + #[test] + fn ltrt_3_2_matches_table_7_32() { + // 3/2 LtRt: Lt = L + 0.707 C − 0.707 Ls − 0.707 Rs. + // Unscaled row-sum-of-|coeffs| = 1 + 3·0.707 = 3.121. + // After §7.8.2 normalisation each coeff is divided by 3.121, so + // the L term lands at 1/3.121 = 0.3204 (Table 7.32, headline). + let bsi = fake_bsi(7, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + assert_eq!(d.output_channels(), 2); + assert!((d.out_coeffs[0][0] - 0.3204).abs() < 1e-3); + // The 0.707 terms (C, Ls, Rs) scale to 0.707/3.121 = 0.2265 + // (Table 7.32's second row). + assert!((d.out_coeffs[0][1] - 0.2265).abs() < 1e-3); + assert!((d.out_coeffs[0][3] + 0.2265).abs() < 1e-3); // -K + assert!((d.out_coeffs[0][4] + 0.2265).abs() < 1e-3); // -K + // Rt mirrors with +K for both surrounds. + assert!((d.out_coeffs[1][2] - 0.3204).abs() < 1e-3); + assert!((d.out_coeffs[1][1] - 0.2265).abs() < 1e-3); + assert!((d.out_coeffs[1][3] - 0.2265).abs() < 1e-3); // +K + assert!((d.out_coeffs[1][4] - 0.2265).abs() < 1e-3); // +K + } + + #[test] + fn ltrt_surround_sign_discipline() { + // The whole point of LtRt vs LoRo is that the surround folds + // in with OPPOSITE signs into Lt vs Rt — that's what a Pro Logic + // matrix decoder pulls out. Verify the sign pattern across every + // surround-bearing acmod. + for &acmod in &[4u8, 5, 6, 7] { + let bsi = fake_bsi(acmod, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + // Surround slot 3 (S or Ls): negative on Lt, positive on Rt. + assert!( + d.out_coeffs[0][3] < 0.0, + "acmod={} Lt slot 3 should be negative, got {}", + acmod, + d.out_coeffs[0][3] + ); + assert!( + d.out_coeffs[1][3] > 0.0, + "acmod={} Rt slot 3 should be positive, got {}", + acmod, + d.out_coeffs[1][3] + ); + // The two surround terms must be equal-magnitude in opposite + // signs at the same slot — that's what makes the matrix + // decoder's subtraction recover the surround source. + assert!((d.out_coeffs[0][3] + d.out_coeffs[1][3]).abs() < 1e-6); + } + } + + #[test] + fn ltrt_2_2_drops_center() { + // §7.8.2: 'if the center channel is missing (2/2 or 2/1 mode) + // the C term is dropped.' acmod=6 (2/2) has no center. + let bsi = fake_bsi(6, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + assert_eq!(d.out_coeffs[0][1], 0.0, "Lt center weight must be zero"); + assert_eq!(d.out_coeffs[1][1], 0.0, "Rt center weight must be zero"); + // The two surrounds still ride in with opposite signs. + assert!(d.out_coeffs[0][3] < 0.0); + assert!(d.out_coeffs[0][4] < 0.0); + assert!(d.out_coeffs[1][3] > 0.0); + assert!(d.out_coeffs[1][4] > 0.0); + } + + #[test] + fn ltrt_3_1_uses_single_surround_form() { + // §7.8.2: 3/1 form is Lt = L + 0.707 C − 0.707 S; Rt mirror. + // acmod=5 (3/1). Single surround S sits at slot 3; slot 4 must + // stay zero. + let bsi = fake_bsi(5, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + assert_eq!(d.out_coeffs[0][4], 0.0); + assert_eq!(d.out_coeffs[1][4], 0.0); + assert!(d.out_coeffs[0][3] < 0.0); + assert!(d.out_coeffs[1][3] > 0.0); + // Center is present (acmod 5 has C). + assert!(d.out_coeffs[0][1] > 0.0); + assert!(d.out_coeffs[1][1] > 0.0); + } + + #[test] + fn ltrt_2_0_passes_no_surround_through() { + // 2/0 has no surround to matrix-encode; the LtRt path falls back + // to plain L→Lt, R→Rt. Sums equal 1 so no normalisation kicks in. + let bsi = fake_bsi(2, 0xFF, 0xFF, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + assert_eq!(d.out_coeffs[0][0], 1.0); + assert_eq!(d.out_coeffs[1][2], 1.0); + for slot in 1..5 { + assert_eq!(d.out_coeffs[0][slot], 0.0); + } + } + + #[test] + fn ltrt_apply_preserves_surround_phase_inversion() { + // Push a +1.0 signal on Ls only (fbw index 3 on acmod=7 source + // layout). Lt should come out negative; Rt positive; same + // magnitude. This is the matrix encoder's defining behaviour. + let bsi = fake_bsi(7, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + let mut src: [[f32; 256]; 5] = [[0.0; 256]; 5]; + for n in 0..256 { + src[3][n] = 1.0; // Ls + } + let mut out = vec![0.0f32; 256 * 2]; + d.apply(&src, 256, &mut out); + for n in 0..256 { + let lt = out[n * 2]; + let rt = out[n * 2 + 1]; + assert!(lt < 0.0, "Lt should be negative, got {}", lt); + assert!(rt > 0.0, "Rt should be positive, got {}", rt); + assert!( + (lt + rt).abs() < 1e-6, + "Lt + Rt should cancel, got {}", + lt + rt + ); + } + } + + #[test] + fn ltrt_3_2_full_scale_does_not_clip() { + // Worst case: every source channel at full scale. Even with sign + // flips the row-sum-of-|coeffs| ≤ 1 invariant means the result + // stays within ±1. + let bsi = fake_bsi(7, 0, 0, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + let mut src: [[f32; 256]; 5] = [[1.0; 256]; 5]; + // Make R negative so the Rt row's L=0 / R=1 / C+Ls+Rs at +K + // does not phase-cancel and we hit the true magnitude bound. + for n in 0..256 { + src[2][n] = 1.0; + } + let mut out = vec![0.0f32; 256 * 2]; + d.apply(&src, 256, &mut out); + for n in 0..256 { + assert!(out[n * 2].abs() <= 1.0 + 1e-6); + assert!(out[n * 2 + 1].abs() <= 1.0 + 1e-6); + } + } + + #[test] + fn ltrt_vs_loro_differ_on_surround() { + // LoRo sums surrounds with the SAME sign into Lt and Rt; LtRt + // inverts. Plant +1 on Ls and Rs simultaneously and verify the + // difference: LoRo doubles up, LtRt mostly cancels. + let bsi = fake_bsi(7, 0, 0, false); + let loro = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + let ltrt = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + let mut src: [[f32; 256]; 5] = [[0.0; 256]; 5]; + for n in 0..256 { + src[3][n] = 1.0; // Ls + src[4][n] = 1.0; // Rs + } + let mut loro_out = vec![0.0f32; 256 * 2]; + let mut ltrt_out = vec![0.0f32; 256 * 2]; + loro.apply(&src, 256, &mut loro_out); + ltrt.apply(&src, 256, &mut ltrt_out); + // LoRo Lt gets +slev·Ls and Lt's slot-4 is zero (LoRo only puts + // Rs into Ro, not Lo). LtRt Lt gets -K·Ls + -K·Rs, summing to + // a strongly negative number. Whatever the exact magnitudes, + // the SIGN of Lt differs between LoRo and LtRt for this input. + let loro_lt = loro_out[0]; + let ltrt_lt = ltrt_out[0]; + assert!(loro_lt > 0.0, "LoRo Lt should be positive, got {}", loro_lt); + assert!(ltrt_lt < 0.0, "LtRt Lt should be negative, got {}", ltrt_lt); + } + + #[test] + fn resolve_common_cases() { + assert_eq!(DownmixMode::resolve(None, 5), DownmixMode::Passthrough); + assert_eq!(DownmixMode::resolve(Some(2), 5), DownmixMode::Stereo); + assert_eq!(DownmixMode::resolve(Some(2), 2), DownmixMode::Passthrough); + assert_eq!(DownmixMode::resolve(Some(1), 2), DownmixMode::Mono); + assert_eq!(DownmixMode::resolve(Some(1), 5), DownmixMode::Mono); + assert_eq!(DownmixMode::resolve(Some(6), 5), DownmixMode::Passthrough); + } + + /// Annex D §2.3.1.3 — `ltrtcmixlev` overrides the §7.8.2 fixed + /// 0.707 center gain. Use 1.000 (code `010`) so the post- + /// normalisation Lt center weight exceeds the default-0.707 case + /// by a measurable margin. + #[test] + fn ltrt_3_2_honours_annex_d_ltrtcmixlev_override() { + use crate::bsi::AnnexDMixLevels; + let mix = AnnexDMixLevels { + ltrtcmixlev: 0b010, // 1.000 + ltrtsurmixlev: 0b100, // 0.707 (default) + lorocmixlev: 0b100, // 0.707 + lorosurmixlev: 0b100, // 0.707 + }; + let bsi = fake_bsi_annex_d(7, false, mix, 0xFF); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + // Pre-normalise: |row| = 1 + 1.0 + 2*0.707 = 3.414, so the + // normalised L weight is 1/3.414 ≈ 0.2929, C weight is the + // same. Both bigger than the 0.707-clev would produce on the + // C slot (0.2265) and smaller on the L slot (0.3204). + let l = d.out_coeffs[0][0]; + let c = d.out_coeffs[0][1]; + assert!( + (l - 0.2929).abs() < 1e-3, + "Lt L weight: want 0.2929, got {}", + l + ); + assert!( + (c - 0.2929).abs() < 1e-3, + "Lt C weight (ltrtcmixlev=010 → 1.0): want 0.2929, got {}", + c + ); + // Surround sign discipline preserved. + assert!(d.out_coeffs[0][3] < 0.0); + assert!(d.out_coeffs[1][3] > 0.0); + } + + /// Annex D §2.3.1.4 — reserved `ltrtsurmixlev` codes (000/001/010) + /// substitute 0.841 per spec note. Verify the coefficient ends up + /// at 0.841 / row-sum, not the default 0.707 / row-sum. + #[test] + fn ltrt_reserved_surround_code_substitutes_0_841() { + use crate::bsi::AnnexDMixLevels; + let mix = AnnexDMixLevels { + ltrtcmixlev: 0b100, // 0.707 + ltrtsurmixlev: 0b001, // reserved → 0.841 + lorocmixlev: 0b100, + lorosurmixlev: 0b100, + }; + let bsi = fake_bsi_annex_d(7, false, mix, 0xFF); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + // Pre-normalise row sum: 1 + 0.707 + 2*0.841 = 3.389. + // Surround weight after normalise = -0.841/3.389 ≈ -0.2482. + let s = d.out_coeffs[0][3]; + assert!( + (s.abs() - 0.2482).abs() < 1e-3, + "Lt surround weight: want 0.2482, got {}", + s + ); + assert!(s < 0.0, "Lt surround weight must still be negative"); + } + + /// Annex D §2.3.1.5 — `lorocmixlev` overrides the body `cmixlev` + /// for the LoRo downmix specifically. Verify a non-default + /// override propagates into the LoRo C weight. + #[test] + fn loro_honours_annex_d_lorocmixlev_override() { + use crate::bsi::AnnexDMixLevels; + let mix = AnnexDMixLevels { + ltrtcmixlev: 0b100, + ltrtsurmixlev: 0b100, + lorocmixlev: 0b010, // 1.000 — louder than the 0.707 default + lorosurmixlev: 0b100, // 0.707 + }; + let bsi = fake_bsi_annex_d(7, false, mix, 0xFF); + let d = Downmix::from_bsi(&bsi, DownmixMode::Stereo); + // Default LoRo (clev=0.707, slev=0.707) row = [1, 0.707, 0, + // 0.707, 0], sum=2.414 → C weight = 0.707/2.414 = 0.2928. + // With lorocmixlev=010 → clev=1.0, row = [1, 1.0, 0, 0.707, 0], + // sum=2.707 → C weight = 1.0/2.707 = 0.3694. + let c = d.out_coeffs[0][1]; + assert!( + (c - 0.3694).abs() < 1e-3, + "LoRo C weight (lorocmixlev=010 → 1.0): want 0.3694, got {}", + c + ); + } + + /// When `bsid != 6` (no Annex D extension) the §7.8.2 base form + /// applies — LtRt is the fixed-0.707 case, completely uninfluenced + /// by `cmixlev` / `surmixlev`. Regression guard for the round-126 + /// refactor that introduced parameterisation. + #[test] + fn ltrt_without_annex_d_uses_fixed_0_707() { + // Set body cmixlev to 0.500 (code 0b10) just to confirm it + // does NOT bleed into the LtRt path. Behaviour must match the + // pre-round-126 baseline. + let bsi = fake_bsi(7, 0b10, 0b10, false); + let d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + assert!((d.out_coeffs[0][0] - 0.3204).abs() < 1e-3); + assert!((d.out_coeffs[0][1] - 0.2265).abs() < 1e-3); + assert!((d.out_coeffs[0][3] + 0.2265).abs() < 1e-3); + } + + /// E-AC-3 (Annex E) field-based constructor with full mixmdata — + /// matrix matches the AC-3 / Annex D path for the same codeword + /// set. Regression guard against the shared-fill `build` helper + /// diverging between the two parsers. + #[test] + fn eac3_fields_match_annex_d_for_same_mix_codes() { + use crate::bsi::AnnexDMixLevels; + let mix = AnnexDMixLevels { + ltrtcmixlev: 0b010, // 1.000 + ltrtsurmixlev: 0b100, // 0.707 + lorocmixlev: 0b100, // 0.707 + lorosurmixlev: 0b101, // 0.595 + }; + let d_e = Downmix::from_eac3_fields(7, 5, 6, true, Some(mix), DownmixMode::StereoLtRt); + let bsi = fake_bsi_annex_d(7, true, mix, 0xFF); + let d_d = Downmix::from_bsi(&bsi, DownmixMode::StereoLtRt); + // Compare both rows coefficient-by-coefficient. + for row in 0..2 { + for col in 0..5 { + assert!( + (d_e.out_coeffs[row][col] - d_d.out_coeffs[row][col]).abs() < 1e-6, + "mismatch row {row} col {col}: e-ac3 {} vs annex-d {}", + d_e.out_coeffs[row][col], + d_d.out_coeffs[row][col], + ); + } + } + assert_eq!(d_e.output_channels(), 2); + } + + /// E-AC-3 without mixmdata — LtRt falls back to the §7.8.2 fixed + /// 0.707 defaults exactly like base AC-3 without xbsi1. Matrix is + /// byte-identical to the `ltrt_without_annex_d_uses_fixed_0_707` + /// baseline (the Eac3 path has no body cmixlev/surmixlev so this + /// is the only sensible default). + #[test] + fn eac3_fields_without_mixmdata_uses_fixed_0_707() { + let d = Downmix::from_eac3_fields(7, 5, 6, true, None, DownmixMode::StereoLtRt); + assert!((d.out_coeffs[0][0] - 0.3204).abs() < 1e-3); + assert!((d.out_coeffs[0][1] - 0.2265).abs() < 1e-3); + assert!((d.out_coeffs[0][3] + 0.2265).abs() < 1e-3); + } + + /// E-AC-3 LoRo with `lorocmixlev` override — verifies the Annex E + /// mix-level codeword takes effect on the LoRo path. Mirror of + /// `loro_honours_annex_d_lorocmixlev_override` but via the + /// `from_eac3_fields` constructor. + #[test] + fn eac3_loro_honours_lorocmixlev_override() { + use crate::bsi::AnnexDMixLevels; + let mix = AnnexDMixLevels { + ltrtcmixlev: 0b100, + ltrtsurmixlev: 0b100, + lorocmixlev: 0b010, // 1.000 + lorosurmixlev: 0b100, // 0.707 + }; + let d = Downmix::from_eac3_fields(7, 5, 5, false, Some(mix), DownmixMode::Stereo); + let c = d.out_coeffs[0][1]; + // sum = 1 + 1.0 + 0 + 0.707 + 0 = 2.707 → C weight = 1/2.707 = 0.3694. + assert!( + (c - 0.3694).abs() < 1e-3, + "Eac3 LoRo C weight: want 0.3694, got {}", + c + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/drc.rs b/crates/vendor/oxideav-ac3/src/drc.rs new file mode 100644 index 00000000..805ee5b3 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/drc.rs @@ -0,0 +1,418 @@ +//! AC-3 / E-AC-3 dynamic-range and dialogue-normalisation control +//! surface (§6.1.9 / §7.6 / §7.7). +//! +//! The base [`crate::audblk`] decode path already converts the per-block +//! `dynrng` gain word to a linear multiplier and scales the transform +//! coefficients by it (§7.7.1.2, the decoder's *default* behaviour). What +//! lives here is the **listener-controllable** layer the spec wraps around +//! that default: +//! +//! * **§7.7.1.2 "Partial Compression"** — a decoder may apply only a +//! *fraction* of each `dynrng` gain change, and a *different* fraction +//! for the positive (boost) and negative (cut) directions. The word is +//! reinterpreted as a signed 8-bit fraction `X0 . X1 X2 Y3 Y4 Y5 Y6 Y7`, +//! multiplied by the chosen direction-dependent factor, then re-formed +//! and used normally. Factor `1.0` reproduces the mandatory full +//! compression; factor `0.0` reproduces the original (un-compressed) +//! dynamic range. +//! * **§7.7.2 Heavy Compression (`compr`)** — when a product must +//! constrain the peak output level (the canonical "RF / set-top +//! modulator" case), it uses the BSI-resident `compr` word (±48 dB, +//! 0.5 dB resolution) *in place of* `dynrng`. `compr` is decoded here +//! to a linear multiplier per Table 7.30. +//! * **§7.6 Dialogue Normalisation** — the 5-bit `dialnorm` word advertises +//! the headroom (in dB) of normal spoken dialogue below digital 100%. +//! The spec is explicit that this value is *not* consumed inside the +//! core decoder; it is the reproduction system's responsibility to fold +//! `dialnorm` into the listener volume control. A decoder that *does* +//! own the playback gain can normalise to a chosen target level with +//! `gain = 10^((target_dB − dialnorm_dB)/20)` — implemented here as an +//! opt-in output scalar. +//! +//! None of these alter the default decode: [`DrcSettings::default()`] is +//! "line out" — full `dynrng`, no heavy compression, no dialnorm +//! normalisation — so the mandatory §7.7.1 behaviour is unchanged unless +//! the caller opts in. +//! +//! All gain arithmetic is `f32` to match the rest of the floating-point +//! DSP path; the dB constants are the spec's 6.02 dB / 0.25 dB / 0.5 dB +//! step sizes derived from the underlying `2^(±n)` shifts. + +/// Which dynamic-range control regime the decoder applies. +/// +/// The spec frames this as a product/listener choice (§7.7.1.1 / +/// §7.7.2.1): a "line out" product reproduces the full encoder-authored +/// compression, while a peak-constrained product ("RF mode") substitutes +/// the heavier `compr` word. The [`DrcMode::Custom`] arm exposes the +/// §7.7.1.2 partial-compression cut/boost factors directly. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum DrcMode { + /// §7.7.1 default — apply the full `dynrng` gain word (cut factor + /// `1.0`, boost factor `1.0`). This is the mandatory decoder + /// behaviour absent listener override. + #[default] + LineOut, + /// §7.7.2 heavy compression — substitute the BSI `compr` word for + /// `dynrng` so the peak output level is constrained (the "RF + /// modulator / set-top" case). When a syncframe carries no `compr` + /// word, §7.7.2.1 mandates falling back to `dynrng` for that frame. + RfMode, + /// §7.7.1.2 partial compression — apply `cut` of each gain *reduction* + /// and `boost` of each gain *increase*. Both factors are clamped to + /// `[0.0, 1.0]`. `Custom { cut: 1.0, boost: 1.0 }` is identical to + /// [`DrcMode::LineOut`]; `Custom { cut: 0.0, boost: 0.0 }` reproduces + /// the original dynamic range (no compression at all). + Custom { cut: f32, boost: f32 }, +} + +/// Listener-side dynamic-range + dialogue-normalisation control surface. +/// +/// Threaded onto the decoder so the §7.7 gain layer can be steered without +/// re-parsing the bitstream. [`Default`] leaves every knob at the +/// spec-mandated baseline (full `dynrng`, no heavy compression, no +/// dialnorm playback normalisation). +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct DrcSettings { + /// Which §7.7 regime to apply. + pub mode: DrcMode, + /// Optional §7.6 dialogue-normalisation **target** level, expressed as + /// the headroom in dB below digital 100% that the listener wants + /// dialogue reproduced at (same units as the `dialnorm` word: a value + /// in `1..=31`). `Some(target)` scales the output by + /// `10^((target − dialnorm)/20)`; `None` leaves the PCM at the encoder + /// level (the spec default — see §7.6, "not directly used by the + /// decoder"). + pub dialnorm_target: Option, +} + +impl DrcSettings { + /// Full-compression line-out default (§7.7.1, no dialnorm + /// normalisation). + pub const fn line_out() -> Self { + DrcSettings { + mode: DrcMode::LineOut, + dialnorm_target: None, + } + } + + /// Heavy-compression "RF mode" (§7.7.2): substitute `compr` for + /// `dynrng`, peak-constrained output. + pub const fn rf_mode() -> Self { + DrcSettings { + mode: DrcMode::RfMode, + dialnorm_target: None, + } + } + + /// §7.7.1.2 partial-compression preset with explicit cut/boost + /// fractions. The factors are clamped into `[0.0, 1.0]`. + pub fn partial(cut: f32, boost: f32) -> Self { + DrcSettings { + mode: DrcMode::Custom { + cut: cut.clamp(0.0, 1.0), + boost: boost.clamp(0.0, 1.0), + }, + dialnorm_target: None, + } + } + + /// Attach a §7.6 dialnorm playback-normalisation target (headroom in + /// dB below full scale, `1..=31`). + pub fn with_dialnorm_target(mut self, target: u8) -> Self { + self.dialnorm_target = Some(target); + self + } + + /// Resolve the per-block `dynrng` (or, in [`DrcMode::RfMode`], the + /// frame-level `compr`) gain word into a linear coefficient + /// multiplier. + /// + /// * `dynrng` — the raw 8-bit §5.4.3.4 dynamic-range word for this + /// block. + /// * `compr` — the BSI §5.4.2.10 heavy-compression word, `Some` when + /// the syncframe carried one. Only consulted in [`DrcMode::RfMode`]. + /// + /// Returns the linear gain to multiply the channel's transform + /// coefficients by (the same role the bare [`dynrng_to_linear`] result + /// played before this control surface existed). + pub fn resolve_block_gain(&self, dynrng: u8, compr: Option) -> f32 { + match self.mode { + DrcMode::LineOut => dynrng_to_linear(dynrng), + DrcMode::Custom { cut, boost } => { + dynrng_to_linear(scale_dynrng_partial(dynrng, cut, boost)) + } + DrcMode::RfMode => match compr { + // §7.7.2.1: heavy compression substitutes for dynrng. + Some(c) => compr_to_linear(c), + // §7.7.2.1: "If the decoder has been instructed to use + // compr, and compr is not present for a particular + // syncframe, then the dynrng control signal shall be used + // for that syncframe." + None => dynrng_to_linear(dynrng), + }, + } + } + + /// §7.6 dialogue-normalisation playback gain, given the syncframe's + /// `dialnorm` word. `1.0` (unity) when no target is configured. + /// + /// `dialnorm` and the configured target are both "dB of headroom below + /// digital 100%". Normalising dialogue to `target` from a stream + /// authored at `dialnorm` is an attenuation of `target − dialnorm` dB + /// (a *more* negative target = quieter output). Out-of-range words + /// (the reserved `0` codepoint) are treated as unity. + pub fn dialnorm_gain(&self, dialnorm: u8) -> f32 { + match self.dialnorm_target { + None => 1.0, + Some(target) => dialnorm_gain(dialnorm, target), + } + } +} + +/// Convert an 8-bit `dynrng` word to a linear gain multiplier (§7.7.1.2). +/// +/// Layout `X0 X1 X2 . Y3 Y4 Y5 Y6 Y7`: +/// * `X` is a 3-bit signed integer in `−4..=3`; the coarse gain is +/// `(X + 1) · 6.02 dB`, realised as `2^(X+1)` arithmetic shifts. +/// * `Y` is the fractional mantissa `0.1 Y3 Y4 Y5 Y6 Y7` (base 2), i.e. +/// `(32 + Y) / 64` in `[0.5, 0.984375]`. +/// +/// The combined linear gain is `2^(X+1) · (32 + Y) / 64`. The all-zero +/// word maps to unity. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn dynrng_to_linear(dynrng: u8) -> f32 { + let x = ((dynrng >> 5) & 0x7) as i32; + let x_signed = if x >= 4 { x - 8 } else { x }; + let y = (dynrng & 0x1F) as i32; + let y_val = (32 + y) as f32 / 64.0; + let shift = x_signed + 1; + let base = 2f32.powi(shift); + base * y_val +} + +/// §7.7.1.2 "Partial Compression": reinterpret the `dynrng` word as a +/// signed 8-bit fraction `X0 . X1 X2 Y3 Y4 Y5 Y6 Y7`, multiply by the +/// direction-appropriate factor, then re-form an 8-bit word in the +/// original `X0 X1 X2 . Y3 Y4 Y5 Y6 Y7` interpretation. +/// +/// Per the spec the byte is a two's-complement signed value in +/// `−128..=127` (units of `1/128` of full range). A *positive* word +/// indicates a gain increase (boost); a *negative* word a gain reduction +/// (cut). The selected factor scales the magnitude; the result is +/// rounded to the nearest integer code and clamped back into the 8-bit +/// signed range, then returned as a `u8` for re-linearisation by +/// [`dynrng_to_linear`]. +/// +/// `cut` and `boost` are assumed already clamped to `[0.0, 1.0]` (the +/// [`DrcSettings::partial`] constructor enforces that). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn scale_dynrng_partial(dynrng: u8, cut: f32, boost: f32) -> u8 { + // Reinterpret the raw byte as a signed 8-bit value: the §7.7.1.2 + // "signed fractional number" X0 . (X1 X2 Y3..Y7). The numeric ordering + // is identical to two's-complement on the byte. + let signed = dynrng as i8 as i32; + if signed == 0 { + // 0 dB word — no gain change in either direction; nothing to scale. + return 0; + } + let factor = if signed > 0 { boost } else { cut }; + // Scale the magnitude, round to nearest integer code. + let scaled = (signed as f32 * factor).round() as i32; + // Clamp into the signed 8-bit range, then reinterpret as the original + // unsigned X0 X1 X2 . Y3..Y7 byte. + let clamped = scaled.clamp(-128, 127); + (clamped as i8) as u8 +} + +/// Convert an 8-bit `compr` heavy-compression word to a linear gain +/// multiplier (§7.7.2.2, Table 7.30). +/// +/// Layout `X0 X1 X2 X3 . Y4 Y5 Y6 Y7`: +/// * `X` is a 4-bit signed integer in `−8..=7`; the coarse gain is +/// `(X + 1) · 6.02 dB`, realised as `2^(X+1)` arithmetic shifts (range +/// `+48.16 dB` down to `−42.14 dB`). +/// * `Y` is the fractional mantissa `0.1 Y4 Y5 Y6 Y7` (base 2), i.e. +/// `(16 + Y) / 32` in `[0.5, 0.96875]` (`−0.28 dB` to `−6.02 dB`). +/// +/// Combined linear gain is `2^(X+1) · (16 + Y) / 32`, spanning `+47.89 dB` +/// down to `−48.16 dB`. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn compr_to_linear(compr: u8) -> f32 { + let x = ((compr >> 4) & 0xF) as i32; + let x_signed = if x >= 8 { x - 16 } else { x }; + let y = (compr & 0xF) as i32; + let y_val = (16 + y) as f32 / 32.0; + let shift = x_signed + 1; + let base = 2f32.powi(shift); + base * y_val +} + +/// §7.6 dialogue-normalisation playback gain. +/// +/// `dialnorm` and `target` are both "dB of headroom below digital 100%" +/// (`1..=31`; the reserved `0` codepoint is treated as a no-op / unity). +/// Normalising a stream authored at `dialnorm` to a desired playback +/// `target` is an attenuation of `target − dialnorm` dB. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn dialnorm_gain(dialnorm: u8, target: u8) -> f32 { + if dialnorm == 0 { + // Reserved codepoint — §5.4.2.8 maps it to the −31 dB default; a + // decoder that can't trust the value should not re-scale. + return 1.0; + } + let delta_db = target as f32 - dialnorm as f32; + 10f32.powf(delta_db / 20.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn db(linear: f32) -> f32 { + 20.0 * linear.log10() + } + + #[test] + fn dynrng_zero_word_is_unity() { + assert!((dynrng_to_linear(0x00) - 1.0).abs() < 1e-6); + } + + #[test] + fn dynrng_table_729_endpoints() { + // X=3 (0b011), Y=0x1F → +24.08 dB coarse, then the Y mantissa. + // X=3, Y=31: 2^4 * (32+31)/64 = 16 * 63/64 = 15.75 → +23.95 dB. + let max = dynrng_to_linear(0b011_11111); + assert!((db(max) - 23.95).abs() < 0.05, "max dynrng {}", db(max)); + // X=-4 (0b100), Y=0 → 2^-3 * 32/64 = 0.125*0.5 = 0.0625 → -24.08 dB. + let min = dynrng_to_linear(0b100_00000); + assert!((db(min) - (-24.08)).abs() < 0.05, "min dynrng {}", db(min)); + } + + #[test] + fn compr_table_730_endpoints() { + // X=7 (0b0111), Y=15 → 2^8 * (16+15)/32 = 256 * 31/32 = 248 → +47.89 dB. + let max = compr_to_linear(0b0111_1111); + assert!((db(max) - 47.89).abs() < 0.05, "max compr {}", db(max)); + // X=-8 (0b1000), Y=0 → 2^-7 * 16/32 = (1/128)*0.5 → -48.16 dB. + let min = compr_to_linear(0b1000_0000); + assert!((db(min) - (-48.16)).abs() < 0.05, "min compr {}", db(min)); + // The 0 dB code per Table 7.30: X=-1 (0b1111), Y=15 → + // 2^0 * 31/32 ≈ 0.969 → -0.28 dB (the Y mantissa floor; the table + // lists the X-only "0 dB None" row, the Y term then trims it). + let near0 = compr_to_linear(0b1111_1111); + assert!( + (db(near0) - (-0.28)).abs() < 0.05, + "compr near-0 {}", + db(near0) + ); + } + + #[test] + fn partial_compression_full_factor_is_identity() { + // factor 1.0 must reproduce the raw word for every byte. + for raw in 0u16..=255 { + let raw = raw as u8; + assert_eq!(scale_dynrng_partial(raw, 1.0, 1.0), raw, "raw {raw:#04x}"); + } + } + + #[test] + fn partial_compression_zero_factor_removes_gain() { + // factor 0.0 must flatten every word to the 0 dB code. + for raw in 0u16..=255 { + let scaled = scale_dynrng_partial(raw as u8, 0.0, 0.0); + assert!( + (dynrng_to_linear(scaled) - 1.0).abs() < 1e-6, + "raw {raw:#04x} scaled {scaled:#04x} not unity" + ); + } + } + + #[test] + fn partial_compression_halves_a_cut() { + // A gain-reduction word (negative signed byte) scaled by cut=0.5 + // should land roughly halfway (in dB) toward unity. Pick -18.06 dB + // worth of cut: X=-4..., use 0b100_00000 = signed -128. + let raw = 0b100_00000u8; // signed -128 → strong cut + let scaled = scale_dynrng_partial(raw, 0.5, 1.0); + // -128 * 0.5 = -64 → 0b1100_0000 = 0xC0 = X=-2(0b110),Y=0. + assert_eq!(scaled, 0xC0); + let g = dynrng_to_linear(scaled); + // X=-2,Y=0 → 2^-1 * 0.5 = 0.25 → -12.04 dB. The original + // 0b100_00000 was -24.08 dB; half (in this signed-code sense) is + // -12.04 dB. ✓ + assert!((db(g) - (-12.04)).abs() < 0.05, "halved cut {}", db(g)); + } + + #[test] + fn partial_compression_independent_directions() { + // boost=0 must zero out a positive (increase) word while a cut=1 + // leaves negative words untouched. + let boost_word = 0b001_00000u8; // signed +32 → boost + assert_eq!(scale_dynrng_partial(boost_word, 1.0, 0.0), 0); + let cut_word = 0b110_00000u8; // signed -64 → cut + assert_eq!(scale_dynrng_partial(cut_word, 1.0, 1.0), cut_word); + } + + #[test] + fn dialnorm_gain_unity_when_target_equals_source() { + assert!((dialnorm_gain(27, 27) - 1.0).abs() < 1e-6); + assert!((dialnorm_gain(31, 31) - 1.0).abs() < 1e-6); + } + + #[test] + fn dialnorm_gain_quieter_target_attenuates() { + // Normalisation gain in dB is `target − dialnorm` (the same + // convention as the documented `2^((target − dialnorm)/6)` ≈ + // `10^((target − dialnorm)/20)` playback formula). Stream authored + // at dialnorm=27, target=31 → +4 dB; the inverse pair (27↔31) + // multiplies back to unity. + let g = dialnorm_gain(27, 31); + assert!((g - 10f32.powf(4.0 / 20.0)).abs() < 1e-5); + let g2 = dialnorm_gain(31, 27); + assert!((g2 - 10f32.powf(-4.0 / 20.0)).abs() < 1e-5); + // Inverse pair multiplies to unity. + assert!((g * g2 - 1.0).abs() < 1e-5); + } + + #[test] + fn dialnorm_reserved_codepoint_is_unity() { + assert!((dialnorm_gain(0, 31) - 1.0).abs() < 1e-6); + } + + #[test] + fn settings_line_out_resolves_full_dynrng() { + let s = DrcSettings::line_out(); + let raw = 0b110_00000u8; + assert!((s.resolve_block_gain(raw, Some(0x00)) - dynrng_to_linear(raw)).abs() < 1e-6); + assert!((s.dialnorm_gain(27) - 1.0).abs() < 1e-6); + } + + #[test] + fn settings_rf_mode_uses_compr_when_present() { + let s = DrcSettings::rf_mode(); + let dynrng = 0b110_00000u8; + let compr = 0b1000_0000u8; // -48.16 dB + let g = s.resolve_block_gain(dynrng, Some(compr)); + assert!((g - compr_to_linear(compr)).abs() < 1e-6); + // No compr in this frame → fall back to dynrng (§7.7.2.1). + let g2 = s.resolve_block_gain(dynrng, None); + assert!((g2 - dynrng_to_linear(dynrng)).abs() < 1e-6); + } + + #[test] + fn settings_custom_clamps_factors() { + let s = DrcSettings::partial(2.0, -1.0); + match s.mode { + DrcMode::Custom { cut, boost } => { + assert_eq!(cut, 1.0); + assert_eq!(boost, 0.0); + } + _ => panic!("expected Custom"), + } + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/aht.rs b/crates/vendor/oxideav-ac3/src/eac3/aht.rs new file mode 100644 index 00000000..30ad47c9 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/aht.rs @@ -0,0 +1,635 @@ +//! Adaptive Hybrid Transform (AHT) decode helpers — A/52:2018 Annex E §3.4. +//! +//! AHT layers a non-overlapped, non-windowed DCT-II of length 6 on top +//! of the standard 256-coefficient MDCT for blocks where the encoder +//! decides the signal is stationary enough that the coding gain of a +//! longer transform beats the time-domain smearing it would normally +//! incur. The AHT path lives entirely inside the §7.3 mantissa unpack +//! step: instead of reading 256 mantissas per audblk, the decoder reads +//! 6×N mantissas (one per (block, bin) pair) for the **first** AHT-active +//! audblk of each frame, dequantises them with a high-efficiency +//! quantiser table (`hebap`), and then inverse-DCT-II's per bin to +//! recover the per-block MDCT coefficients §3.4.5. +//! +//! ## Per-spec eligibility +//! +//! AHT is only available when: +//! +//! * `numblkscod == 0x3` (6 blocks per syncframe) — the AHT transform +//! length is hard-coded to 6. +//! * `ahte == 1` is set in `audfrm()`. +//! * For each AHT-active channel, `nchregs[ch] == 1` (i.e. exponents +//! are transmitted exactly once per syncframe — block 0 carries a +//! non-`REUSE` strategy and blocks 1..5 all reuse). Same rule for +//! `ncplregs`/`nlferegs` on coupling and LFE channels. +//! +//! ## Quantiser stack (§3.4.4) +//! +//! Once `hebap` is computed for a given mantissa bin (§3.4.3.1 — same +//! masking model as base AC-3 with a finer 64-entry pointer table), +//! each of the 6 cross-block coefficients in that bin is quantised by +//! one of three modes: +//! +//! * **`hebap == 0`** — bin contributes no bits (zero-mantissa). +//! * **`1 ≤ hebap ≤ 7`** — vector-quantised: a single 2..9-bit codeword +//! indexes into a 6-D codebook (Tables E4.1..E4.7 in +//! [`super::tables::aht_codebooks`]). The codebook entry IS the +//! 6-tuple of dequantised values. +//! * **`8 ≤ hebap ≤ 19`** — symmetric scalar quantiser (with optional +//! gain-adaptive step-size scaling per the per-frame `gaqmod`). Each +//! of the 6 mantissas reads its own short codeword; if the GAQ tag +//! (full-scale negative) is detected, an extra "large mantissa" +//! codeword follows. +//! +//! ## Cross-block IDCT (§3.4.5) +//! +//! After all 6 mantissas per AHT bin are dequantised, the spec's +//! inverse DCT-II reconstructs the per-block MDCT spectrum value: +//! +//! ```text +//! C(k, m) = 2 · Σ_{j=0..5} R(j) · X(k, j) · cos[ j·(2m+1)·π / 12 ] +//! R(j) = 1 for j != 0 +//! = 1/√2 for j == 0 +//! ``` +//! +//! Then the standard `coeff = mantissa · 2^(-exp)` reconstruction +//! converts each per-block C(k, m) into the regular fbw transform +//! coefficient slot, ready for IMDCT + overlap-add via the existing +//! [`crate::audblk::dsp_block`] path. +//! +//! ## Round-6 scope (this commit) +//! +//! * VQ codebooks E4.1..E4.7 transcribed (956 entries × 6 i16). +//! * `hebap` pointer table (Table E3.1) + quantiser-bit table (E3.2). +//! * GAQ gain-element decode (Table E3.4) + dead-zone large-mantissa +//! remap (Table E3.6). +//! * fbw-channel AHT mantissa unpack + cross-block IDCT. +//! * Coupling / LFE AHT (`cplahtinu`, `lfeahtinu`) — wired through +//! the per-channel iteration (LFE round 113, coupling round 117) so +//! 6-block-coupling and AHT-LFE syncframes decode; the helper routines +//! here (`hebap_from_address`, `fill_gaqbin`, `gaq_sections`, +//! `read_gaq_gains`, `vq_lookup`, `read_scalar_aht_mantissas`, +//! `idct_ii_6`) are channel-agnostic and serve all three paths +//! unchanged. No corpus fixture currently exercises coupling/LFE AHT, +//! so those paths are covered by unit tests in `super::dsp`. +//! +//! Symbol naming follows the spec: `hebap`, `chgaqmod`, `chgaqgain`, +//! `chgaqbin`, `pre_chmant` and the `[k][j]` (bin, AHT-block) ordering +//! are kept literal so the implementation can be cross-referenced +//! against §3.4 paragraph by paragraph. + +use std::f32::consts::PI; + +use oxideav_core::bits::BitReader; +use oxideav_core::Result; + +use super::tables::aht_codebooks::{ + VQ_HEBAP1, VQ_HEBAP2, VQ_HEBAP3, VQ_HEBAP4, VQ_HEBAP5, VQ_HEBAP6, VQ_HEBAP7, +}; + +/// AHT works on exactly six audio blocks per syncframe (§3.4.1). +pub const AHT_BLOCKS: usize = 6; + +/// Table E3.1 — high-efficiency bit-allocation pointer lookup. +/// Indexed by a 6-bit address derived from `(psd - mask) >> 5` clamped +/// to 0..63. Table values are 5-bit `hebap` codes per §3.4.3.1. +pub const HEBAPTAB: [u8; 64] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, + 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, + 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, +]; + +/// Table E3.2 — number of mantissa bits per `hebap` index for the +/// scalar/GAQ regime (`hebap >= 8`). Indices 0..7 are zero (VQ regime +/// has its own bit-count baked into the codebook size). +pub const HEBAP_MANT_BITS: [u8; 20] = [ + 0, 0, 0, 0, 0, 0, 0, 0, // 0..7 → VQ or zero + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, +]; + +/// Number of bits per VQ codeword for `hebap` 1..7 (Table E3.2 column +/// "Mantissa Bits" rounded up over 6 values: 2/6 → 2 bits, 3/6 → 3 bits, +/// etc.). Index 0 unused (signals zero-mantissa). +pub const VQ_BITS: [u8; 8] = [0, 2, 3, 4, 5, 7, 8, 9]; + +/// Decode a `hebap` value from `(psd, mask)` using the §3.4.3.1 +/// pseudo-code. Mirrors [`crate::audblk::run_bit_allocation`]'s tail +/// step but emits `hebap` (via [`HEBAPTAB`]) instead of the 5-bit +/// `bap`. +#[inline] +pub fn hebap_from_address(psd: i16, mask_after_floor: i32) -> u8 { + let addr = (((psd as i32) - mask_after_floor) >> 5).clamp(0, 63) as usize; + HEBAPTAB[addr] +} + +/// `endbap` for the GAQ-active range — mode 0/1 cap at 12 (i.e. apply +/// gains only for `8 <= hebap < 12`); mode 2/3 cap at 17. Per §3.4.2 +/// pseudo-code paragraph that builds `chgaqbin[ch][bin]`. +#[inline] +pub fn endbap_for_gaqmod(gaqmod: u8) -> u8 { + if gaqmod < 2 { + 12 + } else { + 17 + } +} + +/// Compute `chgaqbin[ch][bin]` per the §3.4.2 pseudo-code: +/// +/// * `+1` — bin is GAQ-coded, gain word follows for it. +/// * `-1` — bin is large-only (no GAQ gain word, falls through to the +/// fixed-quantiser regime). +/// * `0` — bin not in the GAQ regime. +/// +/// `gaqmod` is the per-channel `chgaqmod` (or `cplgaqmod` / +/// `lfegaqmod`); pass `0` to disable GAQ for the entire bin range. +pub fn fill_gaqbin(hebap: &[u8], gaqmod: u8, gaqbin: &mut [i8]) -> usize { + let endbap = endbap_for_gaqmod(gaqmod); + let mut active = 0usize; + for (i, &h) in hebap.iter().enumerate() { + if i >= gaqbin.len() { + break; + } + if h > 7 && h < endbap { + gaqbin[i] = 1; + active += 1; + } else if h >= endbap { + gaqbin[i] = -1; + } else { + gaqbin[i] = 0; + } + } + active +} + +/// Number of GAQ gain words transmitted for a channel given its +/// `gaqmod` and the active-bin count returned by [`fill_gaqbin`]. Per +/// the §3.4.2 final pseudo-code chunk (chgaqsections / cplgaqsections / +/// lfegaqsections). +pub fn gaq_sections(gaqmod: u8, active_gaqbins: usize) -> usize { + match gaqmod { + 0 => 0, + 1 | 2 => active_gaqbins, + 3 => active_gaqbins.div_ceil(3), + _ => 0, + } +} + +/// Unpack `nsections` GAQ gain words from the bit stream into a flat +/// `[u8; nbins]` array of mapped values 0..2 (per Table E3.4). +/// +/// * `gaqmod == 1` or `2` — 1 bit per active bin, mapped value = bit +/// (0 → Gk=1, 1 → Gk=2 or 4). +/// * `gaqmod == 3` — 5-bit composite triplet, decoded via +/// `M1 = grpgain/9`, `M2 = (grpgain%9)/3`, `M3 = grpgain%9%3`. +/// +/// Returns the per-bin mapped values aligned to the GAQ-active bins +/// (in order). The caller threads them onto `chgaqbin[bin] == 1` +/// positions as it walks the AHT mantissa loop. +pub fn read_gaq_gains( + br: &mut BitReader<'_>, + gaqmod: u8, + nsections: usize, + out: &mut [u8], +) -> Result { + if nsections == 0 || gaqmod == 0 { + return Ok(0); + } + let mut written = 0usize; + for _ in 0..nsections { + match gaqmod { + 1 | 2 => { + if written >= out.len() { + break; + } + let bit = br.read_u32(1)? as u8; + out[written] = bit; + written += 1; + } + 3 => { + let grp = br.read_u32(5)?; + // Triplet decode per §3.4.4.2 pseudo-code. + let m1 = (grp / 9) as u8; + let m2 = ((grp % 9) / 3) as u8; + let m3 = ((grp % 9) % 3) as u8; + for v in [m1, m2, m3] { + if written < out.len() { + out[written] = v.min(2); + written += 1; + } + } + } + _ => {} + } + } + Ok(written) +} + +/// Map GAQ gain code (0/1/2 from Table E3.4) to the linear gain factor +/// `Gk` (1 / 2 / 4). Used both in the bit-stream reader (to know how +/// many tag bits to peek for) and in the dequantiser (to invert the +/// encoder's amplification). +#[inline] +pub fn gaq_gain_value(code: u8, gaqmod: u8) -> u8 { + match gaqmod { + 1 => match code { + 0 => 1, + _ => 2, // mode 1: Gk ∈ {1, 2} + }, + 2 => match code { + 0 => 1, + _ => 4, // mode 2: Gk ∈ {1, 4} + }, + 3 => match code { + 0 => 1, + 1 => 2, + _ => 4, // mode 3: Gk ∈ {1, 2, 4} + }, + _ => 1, + } +} + +/// Look up the 6-element VQ codebook entry for a given `hebap` (1..=7) +/// and `index`. Returns the entry as f32 normalised into `(-1.0, 1.0)` +/// by dividing by `32768.0` (i.e. interpreting the 16-bit value as a +/// signed Q15 fractional). +pub fn vq_lookup(hebap: u8, index: usize) -> [f32; 6] { + let raw: &[i16; 6] = match hebap { + 1 => &VQ_HEBAP1[index & (VQ_HEBAP1.len() - 1)], + 2 => &VQ_HEBAP2[index & (VQ_HEBAP2.len() - 1)], + 3 => &VQ_HEBAP3[index & (VQ_HEBAP3.len() - 1)], + 4 => &VQ_HEBAP4[index & (VQ_HEBAP4.len() - 1)], + 5 => &VQ_HEBAP5[index & (VQ_HEBAP5.len() - 1)], + 6 => &VQ_HEBAP6[index & (VQ_HEBAP6.len() - 1)], + 7 => &VQ_HEBAP7[index & (VQ_HEBAP7.len() - 1)], + _ => &[0; 6], + }; + let mut out = [0.0f32; 6]; + for i in 0..6 { + out[i] = raw[i] as f32 / 32768.0; + } + out +} + +/// Table E3.6 — large-mantissa inverse-quantisation (remapping) +/// constants, transcribed literally from A/52:2018 Annex E. One row +/// per `hebap` in `8..=19`. Each `(a, b_pos, b_neg)` triple is a set +/// of 16-bit signed two's-complement Q15 fractions for the +/// `y = x + a·x + b` post-process (§3.4.4.2), with `b` selected by +/// the sign of the codeword `x` (`b_pos` for `x >= 0`, `b_neg` for +/// `x < 0`). +/// +/// Columns: `Gk = 1` (applies to ALL `gaqmod == 0` / gain-1 scalar +/// quantisers; `b == 0` in the spec table so only `a` is stored), +/// `Gk = 2` and `Gk = 4` (large-mantissa dead-zone remaps). The spec +/// marks `Gk = 2` / `Gk = 4` rows for `hebap >= 17` as N/A — those +/// hebaps sit outside every GAQ-active range (Table E3.3), so the +/// rows are unreachable; they are stored as zeros. +struct GaqRemap { + /// `Gk = 1` column: `a` (Q15); `b` is 0x0000 for every row. + a_g1: i16, + /// `Gk = 2` column: `(a, b for x>=0, b for x<0)` (Q15). + g2: (i16, i16, i16), + /// `Gk = 4` column: `(a, b for x>=0, b for x<0)` (Q15). + g4: (i16, i16, i16), +} + +/// Rows indexed by `hebap - 8` (hebap 8..=19). +#[rustfmt::skip] +const GAQ_REMAP: [GaqRemap; 12] = [ + /* 8 */ GaqRemap { a_g1: 0x1249, g2: (0xd555u16 as i16, 0x4000, 0xeaabu16 as i16), g4: (0xedb7u16 as i16, 0x2000, 0xfb6eu16 as i16) }, + /* 9 */ GaqRemap { a_g1: 0x0889, g2: (0xc925u16 as i16, 0x4000, 0xd249u16 as i16), g4: (0xe666u16 as i16, 0x2000, 0xeccdu16 as i16) }, + /* 10 */ GaqRemap { a_g1: 0x0421, g2: (0xc444u16 as i16, 0x4000, 0xc889u16 as i16), g4: (0xe319u16 as i16, 0x2000, 0xe632u16 as i16) }, + /* 11 */ GaqRemap { a_g1: 0x0208, g2: (0xc211u16 as i16, 0x4000, 0xc421u16 as i16), g4: (0xe186u16 as i16, 0x2000, 0xe30cu16 as i16) }, + /* 12 */ GaqRemap { a_g1: 0x0102, g2: (0xc104u16 as i16, 0x4000, 0xc208u16 as i16), g4: (0xe0c2u16 as i16, 0x2000, 0xe183u16 as i16) }, + /* 13 */ GaqRemap { a_g1: 0x0081, g2: (0xc081u16 as i16, 0x4000, 0xc102u16 as i16), g4: (0xe060u16 as i16, 0x2000, 0xe0c1u16 as i16) }, + /* 14 */ GaqRemap { a_g1: 0x0040, g2: (0xc040u16 as i16, 0x4000, 0xc081u16 as i16), g4: (0xe030u16 as i16, 0x2000, 0xe060u16 as i16) }, + /* 15 */ GaqRemap { a_g1: 0x0020, g2: (0xc020u16 as i16, 0x4000, 0xc040u16 as i16), g4: (0xe018u16 as i16, 0x2000, 0xe030u16 as i16) }, + /* 16 */ GaqRemap { a_g1: 0x0010, g2: (0xc010u16 as i16, 0x4000, 0xc020u16 as i16), g4: (0xe00cu16 as i16, 0x2000, 0xe018u16 as i16) }, + /* 17 */ GaqRemap { a_g1: 0x0008, g2: (0, 0, 0), g4: (0, 0, 0) }, + /* 18 */ GaqRemap { a_g1: 0x0002, g2: (0, 0, 0), g4: (0, 0, 0) }, + /* 19 */ GaqRemap { a_g1: 0x0000, g2: (0, 0, 0), g4: (0, 0, 0) }, +]; + +/// Q15 fraction → f32. +#[inline] +fn q15(v: i16) -> f32 { + v as f32 / 32768.0 +} + +/// §3.4.4.2 / Table E3.6 post-process `y = x + a·x + b` for a +/// large-mantissa (or `Gk = 1` symmetric) codeword. `x` is the +/// codeword interpreted as a signed two's-complement fraction. +/// +/// `pub(crate)` so the encoder side ([`super::ahtenc`]) can invert the +/// EXACT mapping (quantise by minimising `|gaq_remap(code) - target|`) +/// instead of duplicating the constants. +#[inline] +pub(crate) fn gaq_remap(hebap: u8, gk: u8, x: f32) -> f32 { + let row = &GAQ_REMAP[(hebap as usize - 8).min(11)]; + let (a, b) = match gk { + 1 => (row.a_g1, 0i16), + 2 => (row.g2.0, if x >= 0.0 { row.g2.1 } else { row.g2.2 }), + _ => (row.g4.0, if x >= 0.0 { row.g4.1 } else { row.g4.2 }), + }; + x + q15(a) * x + q15(b) +} + +/// Read 6 mantissas for a single AHT bin with `hebap` in the +/// scalar/GAQ regime (`hebap >= 8`), per §3.4.4.2 / Tables E3.5-E3.6. +/// +/// When `gaqbin == 1` the encoder may have applied a gain (signalled +/// via the per-section gain word `gain_code`): +/// +/// * **`Gk = 1`** (or no GAQ for this bin) — a single `m`-bit +/// two's-complement codeword per mantissa, post-processed with the +/// Table E3.6 `Gk = 1` column (`y = x·(1 + a)`, symmetric +/// `2^m - 1`-level quantiser of step `2 / (2^m - 1)`). +/// * **`Gk = 2`** (mode 1/3) — an `(m-1)`-bit small codeword of step +/// `1 / 2^(m-1)`; the full-scale-negative tag announces an +/// `(m-1)`-bit large codeword remapped via the `Gk = 2` column +/// (dead-zone quantiser, `2^(m-1)` points of step +/// `1 / (2^(m-1) - 1)`). The `(m-1)` LARGE width is easy to misread +/// as `m` off the printed Table E3.5 layout — the clarification +/// (with the `2^(m-1)` reconstruction-point cross-check) is +/// codified as `docs/audio/ac3/ac3-errata.md` entry **E2**; pinned +/// by `tests::scalar_gk2_small_and_large`. +/// * **`Gk = 4`** (mode 2/3) — an `(m-2)`-bit small codeword of step +/// `1 / 2^(m-1)`; the tag announces an `m`-bit large codeword +/// remapped via the `Gk = 4` column (step `3 / (2^(m+1) - 2)`). +/// +/// Returns the 6 dequantised mantissas in `(-1, 1)` normalised range. +pub fn read_scalar_aht_mantissas( + br: &mut BitReader<'_>, + hebap: u8, + gaqmod: u8, + gaqbin: i8, + gain_code: u8, + out: &mut [f32; 6], +) -> Result<()> { + if hebap < 8 { + for v in out.iter_mut() { + *v = 0.0; + } + return Ok(()); + } + let m = HEBAP_MANT_BITS[hebap as usize] as u32; + if m == 0 { + for v in out.iter_mut() { + *v = 0.0; + } + return Ok(()); + } + + // gain == 1 (or no GAQ for this bin) → fixed quantiser, m bits. + let gk = if gaqbin == 1 { + gaq_gain_value(gain_code, gaqmod) + } else { + 1 + }; + + let scale_full = 1.0f32 / ((1u32 << (m - 1)) as f32); // 1 / 2^(m-1) + + for sample in out.iter_mut() { + match gk { + 1 => { + // Symmetric quantiser: read m bits, sign-extend, + // interpret as a Q(m-1) fraction, stretch by (1 + a) + // to the Table E3.5 step of 2/(2^m - 1). + let raw = br.read_u32(m)? as i32; + let signed = (raw << (32 - m)) >> (32 - m); + *sample = gaq_remap(hebap, 1, signed as f32 * scale_full); + } + 2 => { + // Mode 1 / 3 with Gk=2: (m-1)-bit small value; the + // full-scale-negative tag announces an (m-1)-bit + // large value (Table E3.5: 2^(m-1) output points). + let small_bits = m - 1; + let raw_s = br.read_u32(small_bits)? as i32; + let small_signed = (raw_s << (32 - small_bits)) >> (32 - small_bits); + let small_min = -(1i32 << (small_bits - 1)); + if small_signed == small_min { + // Tag — (m-1)-bit large codeword, Q(m-2) fraction. + let raw_l = br.read_u32(small_bits)? as i32; + let large_signed = (raw_l << (32 - small_bits)) >> (32 - small_bits); + let x = large_signed as f32 / ((1u32 << (m - 2)) as f32); + *sample = gaq_remap(hebap, 2, x); + } else { + // Small mantissa: the encoder amplified by Gk=2 + // and coded with one fewer bit, so the composed + // step is 1/2^(m-1) (Table E3.5). + *sample = small_signed as f32 * scale_full; + } + } + 4 => { + // Mode 2 / 3 with Gk=4: (m-2)-bit small value; the + // tag announces an m-bit large value. + let small_bits = m - 2; + let raw_s = br.read_u32(small_bits)? as i32; + let small_signed = (raw_s << (32 - small_bits)) >> (32 - small_bits); + let small_min = -(1i32 << (small_bits - 1)); + if small_signed == small_min { + let raw_l = br.read_u32(m)? as i32; + let large_signed = (raw_l << (32 - m)) >> (32 - m); + let x = large_signed as f32 * scale_full; + *sample = gaq_remap(hebap, 4, x); + } else { + // Encoder amplified by Gk=4, coded with two fewer + // bits; composed step 1/2^(m-1) (Table E3.5). + *sample = small_signed as f32 * scale_full; + } + } + _ => { + // Unknown gain → zero. + *sample = 0.0; + } + } + } + Ok(()) +} + +/// Apply the §3.4.5 inverse DCT-II to the 6 AHT-domain coefficients +/// `x[0..6]` to reconstruct the per-block MDCT spectrum values +/// `c[0..6]`. +/// +/// `c(m) = √2 · Σ_{j=0..5} R(j) · x(j) · cos[ j·(2m+1)·π / 12 ]` +/// where `R(0) = 1/√2` and `R(j) = 1` for `j != 0`. +/// +/// **Deviation from the printed formula** — the A/52:2018 §3.4.5 text +/// shows a leading constant of `2`, but black-box cross-validation +/// against an independent production decoder shows the deployed +/// convention is `√2` (globally — pure-DC and modulated fixtures both +/// fit `ours(2·Σ) = external · √2` with ~89 dB residual). With `√2` +/// the DC basis weight is exactly 1 (`√2 · R(0) = 1`), i.e. a +/// constant cross-block signal reconstructs unchanged, which is the +/// natural quantiser-range convention. We follow the deployed +/// constant; the printed `2` is an erratum, codified with the fitted +/// evidence as `docs/audio/ac3/ac3-errata.md` entry **E1** (the +/// `R_0 = 1/√2` corollary keeping the DC gain at 1 is recorded +/// there). Pinned by `tests::idct_inverse_of_dct_constants`. +pub fn idct_ii_6(x: [f32; 6]) -> [f32; 6] { + let mut c = [0.0f32; 6]; + let r0 = std::f32::consts::FRAC_1_SQRT_2; + for m in 0..6 { + let mut acc = 0.0f32; + for j in 0..6 { + let r = if j == 0 { r0 } else { 1.0 }; + let theta = (j as f32) * ((2 * m + 1) as f32) * PI / 12.0; + acc += r * x[j] * theta.cos(); + } + c[m] = std::f32::consts::SQRT_2 * acc; + } + c +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hebaptab_has_expected_extremes() { + assert_eq!(HEBAPTAB[0], 0); + assert_eq!(HEBAPTAB[63], 19); + } + + #[test] + fn vq_lookup_in_range() { + // VQ_HEBAP1[0] = [7167, 4739, 1106, 4269, 10412, 4820] / 32768. + let v = vq_lookup(1, 0); + assert!((v[0] - 7167.0 / 32768.0).abs() < 1e-6); + assert!(v.iter().all(|s| s.is_finite())); + } + + #[test] + fn idct_inverse_of_dct_constants() { + // Constant input → DC output only at m=0..5 should equal + // √2 · R(0) · x(0) · cos(0) = 1 for x = (1, 0, 0, 0, 0, 0): + // the deployed convention's DC basis weight is exactly 1. + let c = idct_ii_6([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + for v in c.iter() { + assert!((v - 1.0).abs() < 1e-5, "got {v}, expected 1.0"); + } + } + + use oxideav_core::bits::BitWriter; + + fn bits_of(f: impl FnOnce(&mut BitWriter)) -> Vec { + let mut bw = BitWriter::with_capacity(16); + f(&mut bw); + bw.write_u32(0, 32); // tail padding so short reads never starve + bw.into_bytes() + } + + /// Table E3.5 `Gk = 1` column: hebap 8 (m = 3) is a 7-level + /// symmetric quantiser of step 2/7 — codeword 3 (`011`) + /// reconstructs to 6/7 via the Table E3.6 stretch `y = x·(1 + a)`. + #[test] + fn scalar_gk1_symmetric_step() { + let bytes = bits_of(|bw| bw.write_u32(0b011, 3)); + let mut br = BitReader::new(&bytes); + let mut out = [0.0f32; 6]; + // Consume ONE mantissa then stop (remaining reads eat padding). + read_scalar_aht_mantissas(&mut br, 8, 0, 0, 0, &mut out).unwrap(); + assert!( + (out[0] - 6.0 / 7.0).abs() < 1e-4, + "Gk=1 hebap=8 code 3 → 6/7, got {}", + out[0] + ); + // Codeword 0 → exactly 0 (mid-tread). + assert_eq!(out[1], 0.0); + } + + /// Table E3.5 `Gk = 2` column, hebap 8 (m = 3): a 2-bit small + /// codeword of step 1/4, with the full-scale-negative tag (`10`) + /// announcing a 2-bit large codeword remapped by Table E3.6 to the + /// dead-zone points {±1/2, ±5/6} (step 1/3). + #[test] + fn scalar_gk2_small_and_large() { + let bytes = bits_of(|bw| { + bw.write_u32(0b01, 2); // small = +1 → 1/4 + bw.write_u32(0b10, 2); // tag + bw.write_u32(0b01, 2); // large = +1 → x=1/2 → 5/6 + bw.write_u32(0b10, 2); // tag + bw.write_u32(0b00, 2); // large = 0 → x=0 → 1/2 + bw.write_u32(0b10, 2); // tag + bw.write_u32(0b10, 2); // large = -2 → x=-1 → -5/6 + }); + let mut br = BitReader::new(&bytes); + let mut out = [0.0f32; 6]; + // gaqmod=1, gaqbin=1, gain code 1 → Gk=2. + read_scalar_aht_mantissas(&mut br, 8, 1, 1, 1, &mut out).unwrap(); + assert!((out[0] - 0.25).abs() < 1e-6, "small: got {}", out[0]); + assert!( + (out[1] - 5.0 / 6.0).abs() < 1e-4, + "large +1: got {}", + out[1] + ); + assert!((out[2] - 0.5).abs() < 1e-4, "large 0: got {}", out[2]); + assert!( + (out[3] + 5.0 / 6.0).abs() < 1e-4, + "large -2: got {}", + out[3] + ); + } + + /// Table E3.5 `Gk = 4` column, hebap 9 (m = 4): 2-bit small of + /// step 1/8; tag announces an m-bit (4-bit) large codeword of + /// step 3/(2^5 - 2) = 0.1 anchored at ±1/4. + #[test] + fn scalar_gk4_large_step() { + let bytes = bits_of(|bw| { + bw.write_u32(0b10, 2); // tag (small_bits = m-2 = 2) + bw.write_u32(0b0000, 4); // large = 0 → 0.25 + bw.write_u32(0b10, 2); // tag + bw.write_u32(0b0010, 4); // large = +2 → 0.45 + bw.write_u32(0b01, 2); // small = +1 → 1/8 + }); + let mut br = BitReader::new(&bytes); + let mut out = [0.0f32; 6]; + // gaqmod=2, gaqbin=1, gain code 1 → Gk=4. + read_scalar_aht_mantissas(&mut br, 9, 2, 1, 1, &mut out).unwrap(); + assert!((out[0] - 0.25).abs() < 1e-4, "large 0: got {}", out[0]); + assert!((out[1] - 0.45).abs() < 1e-4, "large +2: got {}", out[1]); + assert!((out[2] - 0.125).abs() < 1e-6, "small: got {}", out[2]); + } + + /// The Gk = 2 escape consumes (m-1) + (m-1) bits — the large + /// codeword is `m-1` bits per Table E3.5 (2^(m-1) output points), + /// NOT m bits. + #[test] + fn scalar_gk2_escape_bit_length() { + let bytes = bits_of(|bw| { + for _ in 0..6 { + bw.write_u32(0b100, 3); // tag (m-1 = 3 bits for hebap 9) + bw.write_u32(0b001, 3); // large (m-1 = 3 bits) + } + }); + let mut br = BitReader::new(&bytes); + let mut out = [0.0f32; 6]; + read_scalar_aht_mantissas(&mut br, 9, 1, 1, 1, &mut out).unwrap(); + assert_eq!(br.bit_position(), 6 * 6, "6 escapes × (3 tag + 3 large)"); + } + + #[test] + fn gaq_remap_table_row_consistency() { + // Gk=1 `a` constants track 1/(2^m - 1) (Q15, spec-rounded). + for hebap in 8u8..=19 { + let m = HEBAP_MANT_BITS[hebap as usize] as i32; + let expect = 32768.0 / ((1i64 << m) - 1) as f64; + let got = GAQ_REMAP[hebap as usize - 8].a_g1 as f64; + assert!( + (got - expect).abs() <= 1.0, + "hebap {hebap}: a_g1 {got} vs 1/(2^m-1) {expect:.2}" + ); + } + } + + #[test] + fn gaq_sections_matches_spec() { + assert_eq!(gaq_sections(0, 100), 0); + assert_eq!(gaq_sections(1, 7), 7); + assert_eq!(gaq_sections(2, 3), 3); + assert_eq!(gaq_sections(3, 9), 3); + assert_eq!(gaq_sections(3, 10), 4); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/ahtenc.rs b/crates/vendor/oxideav-ac3/src/eac3/ahtenc.rs new file mode 100644 index 00000000..23464cd9 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/ahtenc.rs @@ -0,0 +1,678 @@ +//! Encoder-side Adaptive Hybrid Transform — A/52:2018 Annex E §3.4, +//! encode direction. +//! +//! Mirrors the decode helpers in [`super::aht`] tool for tool: +//! +//! * **Forward DCT-II** ([`dct_ii_6`]) — the exact inverse of the +//! §3.4.5 IDCT ([`super::aht::idct_ii_6`]): per spectral bin, the 6 +//! per-block mantissas `C(k, m)` become 6 AHT-domain coefficients +//! `X(k, j)` that the quantiser stack codes once per frame. +//! * **Vector quantisation** ([`vq_search`]) — §3.4.4.1: for +//! `1 <= hebap <= 7` the encoder "selects the best vector to +//! transmit ... by locating the vector which minimizes the Euclidean +//! distance between the actual mantissa vector and the table +//! vector". +//! * **Gain-adaptive quantisation** ([`plan_aht_channel`] / +//! [`write_aht_channel`]) — §3.4.4.2: per GAQ-eligible bin the +//! encoder picks the gain `Gk ∈ {1, 2, 4}` (as allowed by the +//! per-channel `gaqmod`, Table E3.3) that minimises the bit cost of +//! the 6 codewords; small mantissas ride the shortened `m-1`/`m-2` +//! bit codewords, large ones pay the full-scale-negative tag plus a +//! Table E3.5 large codeword. The per-channel `gaqmod` itself is +//! chosen by exact bit-count comparison over all four modes +//! (including the mode-3 composite 5-bit gain triplets). +//! +//! Every quantiser decision is made against the decoder's own +//! dequantisation map ([`super::aht::gaq_remap`], the literal Table +//! E3.6 constants), so the encode→decode round trip reconstructs the +//! nearest representable level by construction. + +use oxideav_core::bits::BitWriter; + +use super::aht::{endbap_for_gaqmod, gaq_remap, HEBAP_MANT_BITS, VQ_BITS}; +use super::tables::aht_codebooks::{ + VQ_HEBAP1, VQ_HEBAP2, VQ_HEBAP3, VQ_HEBAP4, VQ_HEBAP5, VQ_HEBAP6, VQ_HEBAP7, +}; +use crate::audblk::N_COEFFS; +use crate::encoder::{compute_bap_table, BitAllocParams, DbaPlan}; + +/// Forward DCT-II of length 6 — the inverse of the §3.4.5 transform +/// as deployed (see [`super::aht::idct_ii_6`]: leading constant `√2`, +/// black-box-validated; the printed `2` is an erratum, codified as +/// `docs/audio/ac3/ac3-errata.md` entry E1). +/// +/// The IDCT is +/// `C(m) = √2 · Σ_j R(j) · X(j) · cos[j·(2m+1)·π/12]` with +/// `R(0) = 1/√2`, `R(j≠0) = 1`. Orthogonality of the DCT basis over +/// 6 points (`Σ_m cos²[j·(2m+1)·π/12] = 3` for `j ≥ 1`, `= 6` for +/// `j = 0`) inverts it as: +/// +/// ```text +/// X(0) = (1 / 6) · Σ_m C(m) +/// X(j) = (√2 / 6) · Σ_m C(m) · cos[j·(2m+1)·π/12] (j ≥ 1) +/// ``` +pub fn dct_ii_6(c: &[f32; 6]) -> [f32; 6] { + use std::f32::consts::{PI, SQRT_2}; + let mut x = [0.0f32; 6]; + let sum: f32 = c.iter().sum(); + x[0] = sum / 6.0; + for (j, xj) in x.iter_mut().enumerate().skip(1) { + let mut acc = 0.0f32; + for (m, &cm) in c.iter().enumerate() { + let theta = (j as f32) * ((2 * m + 1) as f32) * PI / 12.0; + acc += cm * theta.cos(); + } + *xj = acc * SQRT_2 / 6.0; + } + x +} + +/// §3.4.4.1 vector-quantiser search: return the index of the codebook +/// entry (Tables E4.1..E4.7 for `hebap` 1..=7) with minimum Euclidean +/// distance to `x`. Exhaustive — the largest book (hebap 7) has 512 +/// entries. +pub fn vq_search(hebap: u8, x: &[f32; 6]) -> usize { + let book: &[[i16; 6]] = match hebap { + 1 => &VQ_HEBAP1[..], + 2 => &VQ_HEBAP2[..], + 3 => &VQ_HEBAP3[..], + 4 => &VQ_HEBAP4[..], + 5 => &VQ_HEBAP5[..], + 6 => &VQ_HEBAP6[..], + 7 => &VQ_HEBAP7[..], + _ => return 0, + }; + let mut best = 0usize; + let mut best_d = f32::INFINITY; + for (idx, entry) in book.iter().enumerate() { + let mut d = 0.0f32; + for (xi, &ei) in x.iter().zip(entry.iter()) { + let diff = xi - ei as f32 / 32768.0; + d += diff * diff; + } + if d < best_d { + best_d = d; + best = idx; + } + } + best +} + +/// One quantised AHT codeword: `(bit_count, raw_bits)` pairs as they +/// go on the wire. A small (or Gk=1) mantissa is one pair; an escape +/// is the tag pair followed by the large pair. +#[derive(Clone, Copy, Debug, Default)] +pub struct ScalarCode { + /// Tag + small codeword (always present). + pub first: (u32, u32), + /// Large codeword following a tag (escape only). + pub second: Option<(u32, u32)>, +} + +impl ScalarCode { + #[inline] + fn bits(&self) -> u32 { + self.first.0 + self.second.map_or(0, |(n, _)| n) + } +} + +/// Two's-complement truncation of `v` to `n` bits. +#[inline] +fn to_bits(v: i32, n: u32) -> u32 { + (v as u32) & ((1u32 << n) - 1) +} + +/// Quantise one mantissa `x` for the scalar/GAQ regime with gain `gk` +/// (§3.4.4.2 / Tables E3.5-E3.6), returning the codeword(s) and the +/// reconstruction the decoder will produce. +/// +/// * `gk == 1` — one `m`-bit codeword; level `k` chosen to minimise +/// `|gaq_remap(k/2^(m-1)) - x|` over `k ∈ [-(2^(m-1)-1), +/// 2^(m-1)-1]` (the full-scale negative is never emitted: Table +/// E3.5 gives the Gk=1 quantiser `2^m - 1` levels). +/// * `gk == 2` — small `s = round(x·2^(m-1))` in `(m-1)` bits when +/// representable (tag `-2^(m-2)` excluded); otherwise the tag plus +/// an `(m-1)`-bit large codeword against the Table E3.6 Gk=2 remap. +/// * `gk == 4` — small in `(m-2)` bits; escape pays the tag plus an +/// `m`-bit large codeword against the Gk=4 remap. +pub fn quantise_scalar(hebap: u8, gk: u8, x: f32) -> (ScalarCode, f32) { + let m = HEBAP_MANT_BITS[hebap as usize] as u32; + let full = (1i32 << (m - 1)) as f32; // 2^(m-1) + match gk { + 1 => { + // Ideal level from the exact remap: y = (k/2^(m-1))·(1+a) + // is affine in k, so start from the closed form and probe + // ±1 to absorb the Q15 rounding of `a`. + let kmax = (1i32 << (m - 1)) - 1; + let unit = gaq_remap(hebap, 1, 1.0 / full); + let est = if unit > 0.0 { + (x / unit).round() as i32 + } else { + 0 + }; + let (mut best_k, mut best_e) = (0i32, f32::INFINITY); + for k in [est - 1, est, est + 1] { + let k = k.clamp(-kmax, kmax); + let y = gaq_remap(hebap, 1, k as f32 / full); + let e = (y - x).abs(); + if e < best_e { + best_e = e; + best_k = k; + } + } + let y = gaq_remap(hebap, 1, best_k as f32 / full); + ( + ScalarCode { + first: (m, to_bits(best_k, m)), + second: None, + }, + y, + ) + } + 2 | 4 => { + let small_bits = if gk == 2 { m - 1 } else { m - 2 }; + let tag = -(1i32 << (small_bits - 1)); + let s = (x * full).round() as i32; + if s > tag && s < -tag { + // Small codeword — composed step 1/2^(m-1). + let y = s as f32 / full; + return ( + ScalarCode { + first: (small_bits, to_bits(s, small_bits)), + second: None, + }, + y, + ); + } + // Escape: tag + large codeword. + let (large_bits, large_full) = if gk == 2 { + (m - 1, (1i32 << (m - 2)) as f32) + } else { + (m, full) + }; + let lmax = (1i32 << (large_bits - 1)) - 1; + let lmin = -(1i32 << (large_bits - 1)); + // Invert y = x'·(1+a) + b(sign) around the requested sign + // (the `b` offset differs between the x >= 0 and x < 0 + // halves of Table E3.6), then probe ±1 against the exact + // remap to absorb the Q15 rounding. + let probe = |l: i32| gaq_remap(hebap, gk, l as f32 / large_full); + let slope = (probe(1.min(lmax)) - probe(0)).max(1e-9); + let est = if x >= 0.0 { + (((x - probe(0)) / slope).round() as i32).clamp(0, lmax) + } else { + (-1 + ((x - probe(-1)) / slope).round() as i32).clamp(lmin, -1) + }; + let (mut best_l, mut best_e) = (est, f32::INFINITY); + for l in [est - 1, est, est + 1] { + let l = l.clamp(lmin, lmax); + let e = (probe(l) - x).abs(); + if e < best_e { + best_e = e; + best_l = l; + } + } + let y = probe(best_l); + ( + ScalarCode { + first: (small_bits, to_bits(tag, small_bits)), + second: Some((large_bits, to_bits(best_l, large_bits))), + }, + y, + ) + } + _ => ( + ScalarCode { + first: (0, 0), + second: None, + }, + 0.0, + ), + } +} + +/// Quantise the 6 AHT-domain coefficients of one bin with gain `gk`, +/// returning the codewords, total bit count, and total squared error. +fn quantise_bin(hebap: u8, gk: u8, x: &[f32; 6]) -> ([ScalarCode; 6], u32, f32) { + let mut codes = [ScalarCode::default(); 6]; + let mut bits = 0u32; + let mut err = 0.0f32; + for (j, &xj) in x.iter().enumerate() { + let (code, y) = quantise_scalar(hebap, gk, xj); + bits += code.bits(); + err += (y - xj) * (y - xj); + codes[j] = code; + } + (codes, bits, err) +} + +/// Exact bit count of one mantissa under gain `gk` — the cheap +/// (no-probe) twin of [`quantise_scalar`]; the small-vs-escape +/// decision (`s = round(x·2^(m-1))` against the tag boundary) is the +/// same expression, so the planner's bit accounting always matches +/// the writer's emission. +#[inline] +fn scalar_bits(hebap: u8, gk: u8, x: f32) -> u32 { + let m = HEBAP_MANT_BITS[hebap as usize] as u32; + match gk { + 1 => m, + 2 | 4 => { + let small_bits = if gk == 2 { m - 1 } else { m - 2 }; + let tag = -(1i32 << (small_bits - 1)); + let s = (x * (1i32 << (m - 1)) as f32).round() as i32; + if s > tag && s < -tag { + small_bits + } else if gk == 2 { + small_bits + (m - 1) + } else { + small_bits + m + } + } + _ => 0, + } +} + +/// Exact bit count of one bin's 6 codewords under gain `gk`. +#[inline] +fn bin_bits(hebap: u8, gk: u8, x: &[f32; 6]) -> u32 { + x.iter().map(|&v| scalar_bits(hebap, gk, v)).sum() +} + +/// Per-channel GAQ plan: the chosen `gaqmod`, the per-active-bin +/// mapped gain values (Table E3.4: 0 → Gk=1, 1 → Gk=2, 2 → Gk=4, in +/// ascending bin order), and the exact mantissa-payload bit count +/// (everything after the 2-bit `chgaqmod`, gains included). +#[derive(Clone, Debug, Default)] +pub struct AhtChannelPlan { + pub gaqmod: u8, + /// Mapped gain value per GAQ-active bin (ascending bin order). + pub gains: Vec, + /// Total payload bits: gain words + all VQ/scalar codewords. + /// Excludes the 2-bit `chgaqmod` field itself. + pub payload_bits: u32, +} + +impl AhtChannelPlan { + /// Total bits this channel's AHT block occupies in the audblk + /// (including the 2-bit `chgaqmod`). + pub fn total_bits(&self) -> u32 { + 2 + self.payload_bits + } +} + +/// Map a Table E3.4 mapped value to the gain factor. +#[inline] +fn gain_of_mapped(mapped: u8) -> u8 { + match mapped { + 0 => 1, + 1 => 2, + _ => 4, + } +} + +/// Gains selectable per mode: `(allowed mapped values, gain-word bits +/// per active bin)`. Mode 3's 5-bit triplets are handled separately. +fn mode_allowed(gaqmod: u8) -> &'static [u8] { + match gaqmod { + 1 => &[0, 1], // Gk ∈ {1, 2} + 2 => &[0, 2], // Gk ∈ {1, 4} + 3 => &[0, 1, 2], // Gk ∈ {1, 2, 4} + _ => &[0], + } +} + +/// §3.4.4.2 encoder-side GAQ planning for one AHT channel. +/// +/// `hebap[bin]` and `x[bin]` (6 AHT-domain coefficients per bin) must +/// cover `start..end`; bins outside the range are ignored. Evaluates +/// all four `gaqmod` values with exact bit accounting (VQ bins cost +/// their Table E3.2 codeword width regardless of mode; scalar bins +/// pick the cheapest allowed gain, Gk=1 on ties since its large-value +/// levels are finer) and returns the cheapest mode (lowest `gaqmod` +/// on ties). +/// +/// Bit-count only — no codeword search — so it is cheap enough for +/// the SNR-offset tuner's inner loop. The writer's emission consumes +/// exactly `total_bits()` because the small-vs-escape decision here +/// ([`scalar_bits`]) is the same expression [`quantise_scalar`] uses. +pub fn plan_aht_channel(hebap: &[u8], start: usize, end: usize, x: &[[f32; 6]]) -> AhtChannelPlan { + let mut best: Option = None; + let max_mode: u8 = std::env::var("EAC3_AHT_MAX_GAQMOD") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(3); + for gaqmod in 0..=max_mode { + let endbap = endbap_for_gaqmod(gaqmod); + let mut gains: Vec = Vec::new(); + let mut bits = 0u32; + for bin in start..end { + let h = hebap[bin]; + if h == 0 { + continue; + } + if (1..=7).contains(&h) { + // VQ regime — cost is fixed by the codebook. + bits += VQ_BITS[h as usize] as u32; + continue; + } + if gaqmod > 0 && h < endbap { + // GAQ-active bin: pick the cheapest allowed gain + // (strict `<` keeps Gk=1 on ties). + let (mut b_gain, mut b_bits) = (0u8, u32::MAX); + for &mapped in mode_allowed(gaqmod) { + let gk = gain_of_mapped(mapped); + let gbits = bin_bits(h, gk, &x[bin]); + if gbits < b_bits { + b_gain = mapped; + b_bits = gbits; + } + } + gains.push(b_gain); + bits += b_bits; + } else { + // Fixed quantiser (h >= endbap, or gaqmod == 0). + bits += bin_bits(h, 1, &x[bin]); + } + } + // Gain-word side information (§3.4.2 gaqsections). + let gain_bits = match gaqmod { + 1 | 2 => gains.len() as u32, + 3 => (gains.len().div_ceil(3) as u32) * 5, + _ => 0, + }; + bits += gain_bits; + let plan = AhtChannelPlan { + gaqmod, + gains, + payload_bits: bits, + }; + let better = match &best { + None => true, + Some(b) => plan.payload_bits < b.payload_bits, + }; + if better { + best = Some(plan); + } + } + best.expect("at least gaqmod 0 evaluated") +} + +/// Emit one channel's front-loaded AHT mantissa block (§3.4.4 read +/// order): 2-bit `chgaqmod`, the gain words, then per ascending bin +/// the VQ index or 6 scalar/GAQ codewords. Must mirror +/// `decode_aht_channel_mantissas` in [`super::dsp`] exactly. +pub fn write_aht_channel( + bw: &mut BitWriter, + plan: &AhtChannelPlan, + hebap: &[u8], + start: usize, + end: usize, + x: &[[f32; 6]], +) { + bw.write_u32(plan.gaqmod as u32, 2); + // Gain words. + match plan.gaqmod { + 1 | 2 => { + for &g in &plan.gains { + bw.write_u32(u32::from(g != 0), 1); + } + } + 3 => { + for triple in plan.gains.chunks(3) { + let m1 = *triple.first().unwrap_or(&0) as u32; + let m2 = *triple.get(1).unwrap_or(&0) as u32; + let m3 = *triple.get(2).unwrap_or(&0) as u32; + bw.write_u32(m1 * 9 + m2 * 3 + m3, 5); + } + } + _ => {} + } + // Mantissas. + let endbap = endbap_for_gaqmod(plan.gaqmod); + let mut gain_iter = plan.gains.iter(); + for bin in start..end { + let h = hebap[bin]; + if h == 0 { + continue; + } + if (1..=7).contains(&h) { + let idx = vq_search(h, &x[bin]); + bw.write_u32(idx as u32, VQ_BITS[h as usize] as u32); + continue; + } + let gk = if plan.gaqmod > 0 && h < endbap { + gain_of_mapped(*gain_iter.next().unwrap_or(&0)) + } else { + 1 + }; + let (codes, _, _) = quantise_bin(h, gk, &x[bin]); + for code in codes.iter() { + bw.write_u32(code.first.1, code.first.0); + if let Some((n, v)) = code.second { + bw.write_u32(v, n); + } + } + } +} + +/// Derive the §3.4.3.1 high-efficiency bit-allocation pointers +/// (`hebap[]`) for one channel: the base-AC-3 psd / excitation / +/// masking pipeline with the final lookup routed through the Table +/// E3.1 `hebaptab[]` instead of `baptab[]`. +pub(crate) fn compute_hebap( + exp: &[u8; N_COEFFS], + end: usize, + fscod: u8, + ba: &BitAllocParams, + hebap_out: &mut [u8; N_COEFFS], + dba: Option<(&DbaPlan, usize)>, +) { + compute_bap_table(exp, end, fscod, ba, hebap_out, dba, &super::aht::HEBAPTAB) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::eac3::aht::{ + self, fill_gaqbin, gaq_sections, idct_ii_6, read_gaq_gains, read_scalar_aht_mantissas, + AHT_BLOCKS, + }; + use oxideav_core::bits::BitReader; + + #[test] + fn dct_idct_round_trip() { + let c = [0.3f32, -0.7, 0.11, 0.02, -0.4, 0.55]; + let x = dct_ii_6(&c); + let back = idct_ii_6(x); + for (a, b) in c.iter().zip(back.iter()) { + assert!((a - b).abs() < 1e-5, "{a} vs {b}"); + } + } + + #[test] + fn dct_of_constant_is_dc_only() { + // DC basis weight is 1 in the deployed convention: a constant + // cross-block mantissa maps to X(0) unchanged. + let x = dct_ii_6(&[0.5; 6]); + assert!((x[0] - 0.5).abs() < 1e-6); + for &v in &x[1..] { + assert!(v.abs() < 1e-6); + } + } + + #[test] + fn vq_search_recovers_exact_codebook_entry() { + for hebap in 1u8..=7 { + // Probe a few entries spread across the book. + for idx in [0usize, 1, 3] { + let target = aht::vq_lookup(hebap, idx); + let found = vq_search(hebap, &target); + let recon = aht::vq_lookup(hebap, found); + // The index may differ if two entries are identical; + // the reconstruction must be exact either way. + assert_eq!(recon, target, "hebap {hebap} idx {idx} → {found}"); + } + } + } + + /// Quantise → bitstream → decoder read → compare, across every + /// scalar hebap and every gain, on a sweep of values. + #[test] + fn scalar_quantise_round_trips_through_decoder() { + use oxideav_core::bits::BitWriter; + for hebap in 8u8..=19 { + let m = HEBAP_MANT_BITS[hebap as usize] as i32; + let step = 2.0f32 / ((1i64 << m) - 1) as f32; + for (gaqmod, gain_code, gk) in [ + (0u8, 0u8, 1u8), + (1, 0, 1), + (1, 1, 2), + (2, 1, 4), + (3, 1, 2), + (3, 2, 4), + ] { + // GAQ gains only exist inside the mode's active range. + if gk > 1 && hebap >= endbap_for_gaqmod(gaqmod) { + continue; + } + let vals: [f32; 6] = [-0.83, -0.31, -0.02, 0.0, 0.27, 0.78]; + let mut bw = BitWriter::with_capacity(64); + let mut expect = [0.0f32; 6]; + for (j, &v) in vals.iter().enumerate() { + let (code, y) = quantise_scalar(hebap, gk, v); + expect[j] = y; + assert!( + (y - v).abs() <= step * 1.01, + "hebap {hebap} gk {gk}: |{y} - {v}| > step {step}" + ); + bw.write_u32(code.first.1, code.first.0); + if let Some((n, val)) = code.second { + bw.write_u32(val, n); + } + } + bw.write_u32(0, 32); + let bytes = bw.into_bytes(); + let mut br = BitReader::new(&bytes); + let mut out = [0.0f32; 6]; + let gaqbin = if gk > 1 { 1 } else { 0 }; + read_scalar_aht_mantissas(&mut br, hebap, gaqmod, gaqbin, gain_code, &mut out) + .unwrap(); + for j in 0..6 { + assert!( + (out[j] - expect[j]).abs() < 1e-6, + "hebap {hebap} gaqmod {gaqmod} gk {gk} j {j}: decoder {} vs encoder {}", + out[j], + expect[j] + ); + } + } + } + } + + /// Full-channel plan + write → mirror of the decoder's AHT read + /// loop (fill_gaqbin / gaq_sections / read_gaq_gains / VQ / scalar) + /// → the reconstruction matches within the quantiser step, the bit + /// count matches the plan, and the whole thing survives all four + /// hebap regimes at once. + #[test] + fn channel_plan_write_decode_round_trip() { + use oxideav_core::bits::BitWriter; + // Craft an hebap profile that hits: zero (0), VQ (2, 6), + // GAQ-active scalar (8, 10), and fixed scalar (17, 19). + let hebap: Vec = vec![0, 2, 6, 8, 10, 17, 19, 9, 0, 12]; + let end = hebap.len(); + // Deterministic mantissa content: mixture of small and large + // values so GAQ escapes fire. + let mut x = vec![[0.0f32; 6]; end]; + let mut seed = 0x1357_9bdfu32; + for bin in 0..end { + for j in 0..6 { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + let v = (seed as f32 / u32::MAX as f32) * 1.6 - 0.8; + // Bias most values small so GAQ gains win. + x[bin][j] = if j % 3 == 0 { v } else { v * 0.2 }; + } + } + let plan = plan_aht_channel(&hebap, 0, end, &x); + let mut bw = BitWriter::with_capacity(256); + write_aht_channel(&mut bw, &plan, &hebap, 0, end, &x); + let written = bw.bit_position(); + assert_eq!( + written, + plan.total_bits() as u64, + "write must consume exactly the planned bit count" + ); + bw.write_u32(0, 32); + let bytes = bw.into_bytes(); + + // ---- decoder-mirror read ---- + let mut br = BitReader::new(&bytes); + let gaqmod = br.read_u32(2).unwrap() as u8; + assert_eq!(gaqmod, plan.gaqmod); + let mut gaqbin = vec![0i8; end]; + let active = fill_gaqbin(&hebap, gaqmod, &mut gaqbin); + assert_eq!(active, plan.gains.len()); + let nsections = gaq_sections(gaqmod, active); + let mut gain_words = vec![0u8; active]; + read_gaq_gains(&mut br, gaqmod, nsections, &mut gain_words).unwrap(); + let mut gain_iter = gain_words.into_iter(); + for bin in 0..end { + let h = hebap[bin]; + if h == 0 { + continue; + } + if (1..=7).contains(&h) { + let nb = VQ_BITS[h as usize] as u32; + let idx = br.read_u32(nb).unwrap() as usize; + let v = aht::vq_lookup(h, idx); + // VQ error bounded by codebook coverage — just check + // the decode is the entry the encoder picked. + assert_eq!(v, aht::vq_lookup(h, vq_search(h, &x[bin]))); + continue; + } + let gain_code = if gaqbin[bin] == 1 { + gain_iter.next().unwrap_or(0) + } else { + 0 + }; + let mut out = [0.0f32; 6]; + read_scalar_aht_mantissas(&mut br, h, gaqmod, gaqbin[bin], gain_code, &mut out) + .unwrap(); + let m = HEBAP_MANT_BITS[h as usize] as i32; + let step = 2.0f32 / ((1i64 << m) - 1) as f32; + for j in 0..6 { + assert!( + (out[j] - x[bin][j]).abs() <= step * 1.01, + "bin {bin} (hebap {h}) j {j}: {} vs {} (step {step})", + out[j], + x[bin][j] + ); + } + } + assert_eq!( + br.bit_position(), + written, + "decoder mirror must consume exactly the written bits" + ); + let _ = AHT_BLOCKS; + } + + /// GAQ must actually engage: an all-small-mantissa channel in the + /// GAQ-active hebap range must plan a non-zero gaqmod and beat the + /// gaqmod=0 fixed-quantiser bit count. + #[test] + fn gaq_engages_on_small_mantissas() { + let hebap = vec![9u8; 20]; + let x = vec![[0.05f32, -0.08, 0.11, -0.03, 0.06, -0.1]; 20]; + let plan = plan_aht_channel(&hebap, 0, 20, &x); + assert_ne!(plan.gaqmod, 0, "small mantissas must select a GAQ mode"); + // gaqmod 0 cost: 20 bins × 6 × 4 bits = 480. + assert!( + plan.payload_bits < 480, + "GAQ plan ({} bits) must beat the fixed 480-bit cost", + plan.payload_bits + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/audfrm.rs b/crates/vendor/oxideav-ac3/src/eac3/audfrm.rs new file mode 100644 index 00000000..7f5cb05b --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/audfrm.rs @@ -0,0 +1,813 @@ +//! E-AC-3 audio frame element — `audfrm()` per Table E1.3. +//! +//! `audfrm()` sits between `bsi()` and the first `audblk()`. It +//! carries frame-level strategy flags (`expstre`, `ahte`, +//! `snroffststr`, `transproce`, `blkswe`, `dithflage`, `bamode`, +//! `frmfgaincode`, `dbaflde`, `skipflde`, `spxattene`) plus, when the +//! corresponding flag is *cleared*, the per-channel frame-level +//! strategy values that would otherwise have been emitted per block. +//! +//! Round-1 scope (consumed-but-not-acted-upon for everything except +//! the strategy flags themselves): +//! +//! * Strategy flags: stored in [`AudFrm`]. +//! * Frame-level exponent strategies: parsed when `expstre == 0`. The +//! spec packs these as a per-channel run of fixed-width codes (2 +//! bits for fbw, 5 bits for converter exponents on `strmtyp == 0`, +//! 1 bit for LFE). +//! * AHT in-use flags: parsed when `ahte == 1`. +//! * Frame-level SNR offsets: parsed when `snroffststr == 0`. +//! * Transient pre-noise processing: parsed when `transproce == 1`. +//! * Spectral-extension attenuation parameters: parsed when +//! `spxattene == 1`. +//! * Block-start info: parsed when `numblkscod != 0` and the +//! `blkstrtinfoe` bit is set. +//! +//! All consumed values are surfaced on [`AudFrm`] so the audblk +//! parser can use them as defaults when the per-block flag says +//! "frame-level value reused". + +use oxideav_core::bits::BitReader; +use oxideav_core::Result; + +use super::bsi::{Bsi, StreamType}; + +/// Maximum coded channels in a single substream (5 fbw + LFE = 6, but +/// the parser indexes fbw and converter strategies independently). +const MAX_FBW: usize = 5; + +/// Maximum blocks per syncframe (Annex E). +pub const MAX_BLOCKS_PER_FRAME: usize = 6; + +/// Strategy codes: 0 = REUSE, 1 = D15, 2 = D25, 3 = D45 (matching the +/// AC-3 `chexpstr` 2-bit encoding so the audblk-side gates can be +/// reused unchanged for the `expstre == 0` path). +const R: u8 = 0; +const D15: u8 = 1; +const D25: u8 = 2; +const D45: u8 = 3; + +/// **Table E2.10** — Frame Exponent Strategy Combinations (ATSC +/// A/52:2018 Annex E §2.3.2.12 / §2.3.2.13). 32 rows × 6 blocks. Each +/// row is the 6-block strategy run encoded by one 5-bit +/// `frmcplexpstr` / `frmchexpstr[ch]` / `convexpstr[ch]` value. +/// Position 0 (block 0) is never `REUSE` — every row begins with a +/// concrete D15 / D25 / D45 strategy so the decoder always has fresh +/// exponents to reuse from. +pub(crate) const FRAME_EXP_STRAT_TABLE: [[u8; MAX_BLOCKS_PER_FRAME]; 32] = [ + [D15, R, R, R, R, R], // 0 + [D15, R, R, R, R, D45], // 1 + [D15, R, R, R, D25, R], // 2 + [D15, R, R, R, D45, D45], // 3 + [D25, R, R, D25, R, R], // 4 + [D25, R, R, D25, R, D45], // 5 + [D25, R, R, D45, D25, R], // 6 + [D25, R, R, D45, D45, D45], // 7 + [D25, R, D15, R, R, R], // 8 + [D25, R, D25, R, R, D45], // 9 + [D25, R, D25, R, D25, R], // 10 + [D25, R, D25, R, D45, D45], // 11 + [D25, R, D45, D25, R, R], // 12 + [D25, R, D45, D25, R, D45], // 13 + [D25, R, D45, D45, D25, R], // 14 + [D25, R, D45, D45, D45, D45], // 15 + [D45, D15, R, R, R, R], // 16 + [D45, D15, R, R, R, D45], // 17 + [D45, D25, R, R, D25, R], // 18 + [D45, D25, R, R, D45, D45], // 19 + [D45, D25, R, D25, R, R], // 20 + [D45, D25, R, D25, R, D45], // 21 + [D45, D25, R, D45, D25, R], // 22 + [D45, D25, R, D45, D45, D45], // 23 + [D45, D45, D15, R, R, R], // 24 + [D45, D45, D25, R, R, D45], // 25 + [D45, D45, D25, R, D25, R], // 26 + [D45, D45, D25, R, D45, D45], // 27 + [D45, D45, D45, D25, R, R], // 28 + [D45, D45, D45, D25, R, D45], // 29 + [D45, D45, D45, D45, D25, R], // 30 + [D45, D45, D45, D45, D45, D45], // 31 +]; + +/// Parsed `audfrm()` snapshot. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AudFrm { + /// `expstre` (1 bit, only present when `numblkscod == 0x3`). + /// `true` means each block carries its own exponent strategy + /// (mirroring AC-3 base behaviour); `false` means the frame + /// emitted per-channel `frmchexpstr` codes that drive a 6-block + /// strategy run via Table E2.10. + pub expstre: bool, + /// `ahte` (1 bit) — Adaptive Hybrid Transform in use this frame. + pub ahte: bool, + /// `snroffststr` (2 bits) — frame-level SNR offset packing mode. + /// 0 = single frame value; 1/2 = per-block sub-modes (see Table + /// E1.3 for the exact bit layout). + pub snroffststr: u8, + /// `transproce` (1 bit) — transient pre-noise processing in use. + pub transproce: bool, + /// `blkswe` (1 bit) — per-block block-switch flags emitted (when + /// false, `blksw[ch] = 0` for every block of every channel). + pub blkswe: bool, + /// `dithflage` (1 bit) — per-block per-channel dither flags + /// emitted (when false, `dithflag[ch] = 1` always). + pub dithflage: bool, + /// `bamode` (1 bit) — per-block bit-allocation parametric info + /// emitted (when false, the BA params take fixed defaults from + /// §E.2.2.4 / Table E1.4). + pub bamode: bool, + /// `frmfgaincode` (1 bit) — `fgaincode` is per-block (when true) + /// versus implicit (`fgaincode = 0`) when false. + pub frmfgaincode: bool, + /// `dbaflde` (1 bit) — delta bit-allocation info may appear in any + /// block when true; absent when false. + pub dbaflde: bool, + /// `skipflde` (1 bit) — skip-field-exists flag emitted per block + /// when true. + pub skipflde: bool, + /// `spxattene` (1 bit) — spectral-extension attenuation parameters + /// follow. + pub spxattene: bool, + /// Frame-level coupling exponent strategy (`frmcplexpstr`, 5 + /// bits, from Table E2.10). 0xFF when not applicable. Applies + /// only when `expstre == 0` AND coupling is in use across the + /// whole syncframe. + pub frmcplexpstr: u8, + /// Frame-level per-fbw-channel exponent strategy (`frmchexpstr`, + /// 5 bits each). 0xFF when not applicable. + pub frmchexpstr: [u8; MAX_FBW], + /// `lfeexpstr[blk]` — LFE exponent strategy per block (1 bit each). + /// Only valid when `lfeon == true`. Per Table E.1.3 / §E.1.2.3 the + /// LFE per-block strategy is emitted in audfrm UNCONDITIONALLY of + /// `expstre` (the `if(lfeon)` block sits OUTSIDE the + /// `if(expstre)` branch). + pub lfeexpstr: [u8; MAX_BLOCKS_PER_FRAME], + /// Per-block per-channel exponent strategy code (`chexpstr[blk][ch]`, + /// 2 bits each). Populated when `expstre == 1` (the per-block path, + /// which is what every reasonable encoder picks). Each value is + /// the AC-3 strategy code: 0 = REUSE, 1 = D15, 2 = D25, 3 = D45. + /// When `expstre == 0`, this field stays zeroed and the audblk + /// path must derive per-block strategies from `frmchexpstr` via + /// Table E2.10. + pub chexpstr_blk_ch: [[u8; MAX_FBW]; MAX_BLOCKS_PER_FRAME], + /// Per-block coupling-channel exponent strategy code + /// (`cplexpstr[blk]`, 2 bits each), populated when `expstre == 1` + /// AND `cplinu[blk] == 1` for that block. 0 = REUSE, 1 = D15, + /// 2 = D25, 3 = D45. + pub cplexpstr_blk: [u8; MAX_BLOCKS_PER_FRAME], + /// Frame-level coarse SNR offset (`frmcsnroffst`, 6 bits). Only + /// when `snroffststr == 0`. + pub frmcsnroffst: u8, + /// Frame-level fine SNR offset (`frmfsnroffst`, 4 bits). Only + /// when `snroffststr == 0`. + pub frmfsnroffst: u8, + /// Total bits the parser consumed (handy for callers that share + /// a bit cursor and want to seek to the start of `audblk[0]`). + pub bits_consumed: u64, + /// Per-block `cplinu[blk]` — surfaced from audfrm so the audblk + /// parser knows whether to read the coupling-coordinate block. + /// `false` for blocks with no coupling. Always `false` when + /// `acmod ≤ 1` (no coupling possible). + pub cplinu_blk: [bool; MAX_BLOCKS_PER_FRAME], + /// Per-block `cplstre[blk]` — coupling-strategy-exists flag per + /// block. Block 0 is always implicit `1` per §E.1.2.2 / Table E1.3 + /// (the spec only transmits `cplinu[0]`); subsequent blocks + /// transmit `cplstre[blk]` explicitly. Surfaced so the round-5 + /// audblk DSP path knows when to expect the coupling-strategy + /// fields (chincpl[], cplbegf, …) versus reusing the prior block's + /// strategy with fresh coordinates. + pub cplstre_blk: [bool; MAX_BLOCKS_PER_FRAME], + /// Number of blocks with `cplinu[blk] == 1`. Convenient summary + /// for the round-2 DSP path which rejects any non-zero value. + pub ncplblks: u32, + /// Bit position immediately before the AHT block in audfrm. Set + /// by `parse_with` regardless of whether `ahte` was true. The + /// dsp module uses this anchor to reseek into audfrm after a + /// pre-walk of audblks has produced `nchregs[ch]`/`ncplregs`/ + /// `nlferegs` — see [`super::dsp::decode_indep_audblks`]. + pub aht_anchor_bits: u64, + /// `true` when `parse_with` returned with `ahte == 1` BEFORE + /// consuming the AHT bits or the SNR/transient/SPX/blkstrtinfo + /// tail. The caller must invoke [`parse_phase_b`] with the + /// computed nchregs hints to finish parsing audfrm. + pub aht_phase_b_pending: bool, + /// Per-fbw-channel `chahtinu[ch]` flag from §3.4.2 / Table E1.3. + /// `true` means channel `ch` is AHT-coded for this syncframe. + /// Populated by [`parse_phase_b`]. + pub chahtinu: [bool; MAX_FBW], + /// `cplahtinu` flag (single coupling channel). Populated by + /// [`parse_phase_b`] when `ncplregs == 1 && ncplblks == 6`. + pub cplahtinu: bool, + /// `lfeahtinu` flag (single LFE channel). Populated by + /// [`parse_phase_b`] when `nlferegs == 1` and `lfeon == true`. + pub lfeahtinu: bool, + /// Per-fbw-channel `chintransproc[ch]` (§2.3.2.21 / Table E1.3) — + /// `true` when channel `ch` carries transient pre-noise time-scaling + /// synthesis data this frame. Only meaningful when `transproce`. + pub chintransproc: [bool; MAX_FBW], + /// Per-fbw-channel `transprocloc[ch]` (10 bits, §2.3.2.22). The + /// transient location relative to the first decoded PCM sample of + /// the frame, in units of 4 samples — multiply by 4 to get the + /// sample index (§E.3.7.2). Only valid when `chintransproc[ch]`. + pub transprocloc: [u16; MAX_FBW], + /// Per-fbw-channel `transproclen[ch]` (8 bits, §2.3.2.23) — the time + /// scaling length in samples. Only valid when `chintransproc[ch]`. + pub transproclen: [u16; MAX_FBW], + /// Per-fbw-channel `chinspxatten[ch]` (§2.3.2.24 / Table E1.3) — + /// `true` when channel `ch` carries spectral-extension attenuation + /// data this frame. Only meaningful when `spxattene`. + pub chinspxatten: [bool; MAX_FBW], + /// Per-fbw-channel `spxattencod[ch]` (5 bits, §2.3.2.25) — index + /// into Table E3.14 (`SPX_ATTEN_TABLE`) for the border-notch filter + /// taps. Only valid when `chinspxatten[ch]`. + pub spxattencod: [u8; MAX_FBW], +} + +impl AudFrm { + pub(crate) fn new() -> Self { + Self { + expstre: true, + ahte: false, + snroffststr: 0, + transproce: false, + blkswe: true, + dithflage: true, + bamode: true, + frmfgaincode: false, + dbaflde: true, + skipflde: true, + spxattene: false, + frmcplexpstr: 0xFF, + frmchexpstr: [0xFF; MAX_FBW], + lfeexpstr: [0; MAX_BLOCKS_PER_FRAME], + chexpstr_blk_ch: [[0; MAX_FBW]; MAX_BLOCKS_PER_FRAME], + cplexpstr_blk: [0; MAX_BLOCKS_PER_FRAME], + frmcsnroffst: 0, + frmfsnroffst: 0, + bits_consumed: 0, + cplinu_blk: [false; MAX_BLOCKS_PER_FRAME], + cplstre_blk: [false; MAX_BLOCKS_PER_FRAME], + ncplblks: 0, + aht_anchor_bits: 0, + aht_phase_b_pending: false, + chahtinu: [false; MAX_FBW], + cplahtinu: false, + lfeahtinu: false, + chintransproc: [false; MAX_FBW], + transprocloc: [0; MAX_FBW], + transproclen: [0; MAX_FBW], + chinspxatten: [false; MAX_FBW], + spxattencod: [0; MAX_FBW], + } + } +} + +/// Per-channel hint set produced by the dsp pre-walk before invoking +/// [`parse_phase_b`]. Each `nchregs[ch]` value is the number of +/// non-`REUSE` exponent strategies emitted across the 6 audblks for +/// that channel; `chahtinu[ch]` is present in the bitstream only when +/// `nchregs[ch] == 1` (and likewise for `ncplregs` / `nlferegs`). +#[derive(Clone, Copy, Debug, Default)] +pub struct AhtRegsHints { + pub nchregs: [u8; MAX_FBW], + pub ncplregs: u8, + pub nlferegs: u8, +} + +/// Parse `audfrm()` per Table E1.3. +/// +/// `bsi.num_blocks` must equal the number of audio blocks per +/// syncframe (1, 2, 3, or 6). The parser uses it to walk the +/// per-block lfeexpstr / blkstrtinfoe runs. +pub fn parse_with(br: &mut BitReader<'_>, bsi: &Bsi) -> Result { + let start_bits = br.bit_position(); + let mut a = AudFrm::new(); + + let num_blocks = bsi.num_blocks as usize; + let nfchans = bsi.nfchans as usize; + let lfeon = bsi.lfeon; + + // §E.2.2.3 / §E.2.3.2 — 6-block syncframes carry expstre+ahte; for + // smaller frames the spec hard-codes expstre=1 and ahte=0. + if num_blocks == MAX_BLOCKS_PER_FRAME { + a.expstre = br.read_u32(1)? != 0; + a.ahte = br.read_u32(1)? != 0; + } else { + a.expstre = true; + a.ahte = false; + } + a.snroffststr = br.read_u32(2)? as u8; + a.transproce = br.read_u32(1)? != 0; + a.blkswe = br.read_u32(1)? != 0; + a.dithflage = br.read_u32(1)? != 0; + a.bamode = br.read_u32(1)? != 0; + a.frmfgaincode = br.read_u32(1)? != 0; + a.dbaflde = br.read_u32(1)? != 0; + a.skipflde = br.read_u32(1)? != 0; + a.spxattene = br.read_u32(1)? != 0; + + // ---- coupling data ---- + // + // For acmod > 0x1, block 0 carries an explicit `cplinu[0]` flag + // and subsequent blocks emit (cplstre[blk], optional cplinu[blk]) + // pairs. The pure parser doesn't need to remember which blocks + // had coupling — it just consumes the bits. + // + // For acmod ≤ 0x1, every block has coupling implicitly off and + // there is nothing to consume. + let mut ncplblks = 0u32; + if bsi.acmod > 0x1 { + // cplstre[0] is fixed at 1 (not transmitted); cplinu[0] is 1 + // bit. + let cplinu0 = br.read_u32(1)?; + ncplblks += cplinu0; + a.cplinu_blk[0] = cplinu0 != 0; + a.cplstre_blk[0] = true; + let mut last_cplinu = cplinu0; + for blk in 1..num_blocks { + let cplstre = br.read_u32(1)? != 0; + a.cplstre_blk[blk] = cplstre; + if cplstre { + let v = br.read_u32(1)?; + last_cplinu = v; + } + ncplblks += last_cplinu; + a.cplinu_blk[blk] = last_cplinu != 0; + } + } + a.ncplblks = ncplblks; + + // ---- exponent strategy data ---- + // + // §E.1.2.3 / Table E.1.3 (ETSI TS 102 366 V1.4.1): + // + // if(expstre) { + // for(blk = 0..6) { + // if(cplinu[blk] == 1) cplexpstr[blk] 2 bits + // for(ch = 0..nfchans) chexpstr[blk][ch] 2 bits + // } + // } else { + // if((acmod>1) && (ncplblks>0)) frmcplexpstr 5 bits + // for(ch = 0..nfchans) frmchexpstr[ch] 5 bits + // } + // if(lfeon) { + // for(blk = 0..6) lfeexpstr[blk] 1 bit each + // } + // + // The `if(lfeon)` block sits OUTSIDE the `if(expstre)` branch — + // per-block lfeexpstr is emitted unconditionally of expstre. + // + // ETSI §E.1.3.2.1 / ATSC §E.2.3.2.1 text: "If the expstre bit is + // set to '1', the fields that carry the full exponent strategy + // syntax shall be present in **each audio block**." This wording + // refers to the per-block-indexed fields enumerated by the + // syntax table — they LIVE in audfrm, indexed by `[blk]`. Audblk + // (Table E.1.4) does NOT re-emit chexpstr/cplexpstr/lfeexpstr; it + // merely consumes them as state via gates like + // `if(chexpstr[blk][ch] != reuse) {chbwcod[ch]; ...}`. + // + // Earlier rounds inverted this — moving the bits into audblk — + // and the round-2 comment doubled down. The validator binary + // rejects every frame our encoder emitted under the inverted + // layout, surfacing as the cascade "new bit allocation info must + // be present in block 0" / "delta bit allocation strategy + // reserved" / "error in bit allocation". + if a.expstre { + for blk in 0..num_blocks { + if a.cplinu_blk[blk] { + a.cplexpstr_blk[blk] = br.read_u32(2)? as u8; + } + for ch in 0..nfchans { + a.chexpstr_blk_ch[blk][ch] = br.read_u32(2)? as u8; + } + } + } else { + // §E.2.3.2.12 / §E.2.3.2.13 — frame-based exponent strategy. A + // 5-bit `frmcplexpstr` (when coupling is in use anywhere in the + // frame) and one 5-bit `frmchexpstr[ch]` per fbw channel index + // into Table E2.10 to expand into 6 per-block strategies (each + // value is REUSE / D15 / D25 / D45). The 6-block expansion is + // also used to populate `cplexpstr_blk[]` on blocks where + // coupling is in use; entries for non-cplinu blocks are + // harmlessly left at the lookup value (the dsp module only + // consults `cplexpstr_blk[blk]` when `cplinu_blk[blk]` is true). + if bsi.acmod > 0x1 && ncplblks > 0 { + a.frmcplexpstr = br.read_u32(5)? as u8; + let row = FRAME_EXP_STRAT_TABLE[a.frmcplexpstr as usize]; + a.cplexpstr_blk[..num_blocks].copy_from_slice(&row[..num_blocks]); + } + for ch in 0..nfchans { + a.frmchexpstr[ch] = br.read_u32(5)? as u8; + let row = FRAME_EXP_STRAT_TABLE[a.frmchexpstr[ch] as usize]; + // chexpstr_blk_ch is `[blk][ch]` so we can't use a single + // copy_from_slice — walk the blocks for this channel. + for blk in 0..num_blocks { + a.chexpstr_blk_ch[blk][ch] = row[blk]; + } + } + } + if lfeon { + for blk in 0..num_blocks { + a.lfeexpstr[blk] = br.read_u32(1)? as u8; + } + } + + // ---- converter exponent strategy data ---- + // + // strmtyp == 0 (independent substream): when numblkscod != 0x3 a + // 1-bit `convexpstre` flag controls whether per-channel 5-bit + // `convexpstr` codes follow. With numblkscod == 0x3, `convexpstre` + // is implicit = 1 and the codes are always present. + if matches!(bsi.strmtyp, StreamType::Independent) { + let convexpstre_present = if num_blocks != MAX_BLOCKS_PER_FRAME { + br.read_u32(1)? != 0 + } else { + true + }; + if convexpstre_present { + for _ch in 0..nfchans { + let _convexpstr = br.read_u32(5)?; + } + } + } + + // ---- AHT data ---- + // + // Per §E.2.3.5 / Table E1.3, `ahte` is in scope only when + // `expstre == 1`. When `ahte == 1`, audfrm carries `ahtinu[ch]` + // (1 bit) for every fbw channel whose 6-block exponent + // strategies are all REUSE (`nchregs[ch] == 1`), and one + // `ahtinu_lfe`. The per-channel `nchregs[ch]` determination + // requires reading the audblk strategy bits **before** we get to + // the AHT block — but those bits live in audblk[0]..audblk[5], + // past the audfrm boundary. + // + // Round 6 (this commit) splits audfrm parsing into two phases: + // + // * `parse_with` (this entry point) walks fields 0..AHT and + // captures `aht_anchor_bits` = the bit position immediately + // before the AHT block. When `ahte == 0`, the parser proceeds + // straight to the SNR/transient/SPX/blkstrtinfo tail (the + // classic round-1 path). When `ahte == 1`, it RETURNS HERE + // without consuming the variable-length AHT bits OR the tail + // fields; the dsp module then pre-walks audblks for chexpstr, + // computes nchregs, and calls `parse_phase_b` with the hint + // to finish the parse. + // + // The `aht_anchor_bits` field is set in both branches so the + // dsp module can reseek the bit cursor to here before invoking + // `parse_phase_b`. + a.aht_anchor_bits = br.bit_position(); + if a.ahte { + // Phase A only — AHT bits + remaining tail are read by + // `parse_phase_b` once the dsp pre-walk has produced + // nchregs[ch] / ncplregs / nlferegs. + a.bits_consumed = br.bit_position() - start_bits; + a.aht_phase_b_pending = true; + return Ok(a); + } + + parse_tail(br, &mut a, bsi)?; + + a.bits_consumed = br.bit_position() - start_bits; + Ok(a) +} + +/// Parse the SNR-offset / transient / SPX-attenuation / blkstrtinfo +/// tail of `audfrm()`. Shared between `parse_with` (the AHT-off fast +/// path) and [`parse_phase_b`] (the AHT-on staged path). +fn parse_tail(br: &mut BitReader<'_>, a: &mut AudFrm, bsi: &Bsi) -> Result<()> { + let nfchans = bsi.nfchans as usize; + let num_blocks = bsi.num_blocks as usize; + + // ---- audio frame SNR offset data ---- + if a.snroffststr == 0 { + a.frmcsnroffst = br.read_u32(6)? as u8; + a.frmfsnroffst = br.read_u32(4)? as u8; + } + // snroffststr 1 / 2 → per-block values, parsed inside audblk. + + // ---- transient pre-noise processing (§2.3.2.20-23 / Table E1.3) ---- + // Capture the per-channel transient-location / time-scaling-length + // parameters so the §E.3.7.2 PCM-domain synthesis can run after + // overlap-add. Previously these were read for cursor alignment only + // and discarded (the dsp then errored the whole frame). + if a.transproce { + for ch in 0..nfchans.min(MAX_FBW) { + let chintransproc = br.read_u32(1)? != 0; + a.chintransproc[ch] = chintransproc; + if chintransproc { + a.transprocloc[ch] = br.read_u32(10)? as u16; + a.transproclen[ch] = br.read_u32(8)? as u16; + } + } + } + + // ---- spectral extension attenuation data (§2.3.2.24-25) ---- + // + // Captured per-fbw-channel so the SPX synthesis step + // (`audblk::apply_spectral_extension`) can apply the §3.6.4.2.3 + // 5-tap notch filter at the baseband / extension border + every + // §3.6.4.1 translation-copy wrap point. `chinspxatten[ch]` is a + // frame-scoped flag (the spec emits it in audfrm, not audblk) and + // applies identically to every block of the syncframe. + if a.spxattene { + for ch in 0..nfchans.min(MAX_FBW) { + let chinspxatten = br.read_u32(1)? != 0; + a.chinspxatten[ch] = chinspxatten; + if chinspxatten { + a.spxattencod[ch] = br.read_u32(5)? as u8; + } + } + } + + // ---- block start information ---- + // + // Only present for frames with > 1 block (numblkscod != 0); flagged + // by `blkstrtinfoe`. When set, `blkstrtinfo` follows with + // `nblkstrtbits` bits. nblkstrtbits is derived from frmsiz per + // §2.3.2.27. + if num_blocks != 1 { + let blkstrtinfoe = br.read_u32(1)? != 0; + if blkstrtinfoe { + // nblkstrtbits = (numblks - 1) * (4 + ceil(log2(frmsiz_bits))) + // For numblks=6 and frmsiz_bits ≤ 16, log2 ≤ 4 → 8 bits per + // entry → 5*8 = 40 bits. Spec-correct formula per §2.3.2.27. + let frame_bits = bsi.frame_bytes * 8; + let log2 = 32 - frame_bits.leading_zeros(); + let bits_per = 4 + log2; + let total = (num_blocks as u32 - 1) * bits_per; + br.skip(total)?; + } + } + + // ---- per-channel state initialisation flags ---- + // + // The spec requires the syntax-state init for every channel in the + // syncframe (firstspxcos[ch] = 1, firstcplcos[ch] = 1, firstcplleak + // = 1) — these are stateful initialisers, not bit-field reads. + Ok(()) +} + +/// Phase-B audfrm parse — consumes the AHT block (using the +/// pre-walked `nchregs` hints to know which channels emit `chahtinu` +/// bits) and the SNR/transient/SPX-attenuation/blkstrtinfo tail. +/// +/// Caller must: +/// +/// 1. Have called [`parse_with`] which returned `aht_phase_b_pending == true`. +/// 2. Reseek the bit reader to `audfrm.aht_anchor_bits` (the +/// bit position immediately before the AHT block). +/// 3. Pass `hints` produced by walking all 6 audblks for chexpstr. +/// +/// Updates `audfrm.chahtinu`/`cplahtinu`/`lfeahtinu` and clears +/// `aht_phase_b_pending`. The bit reader lands at the start of +/// `audblk[0]` on success. +pub fn parse_phase_b( + br: &mut BitReader<'_>, + audfrm: &mut AudFrm, + bsi: &Bsi, + hints: &AhtRegsHints, +) -> Result<()> { + if !audfrm.aht_phase_b_pending { + return Ok(()); + } + let nfchans = bsi.nfchans as usize; + let lfeon = bsi.lfeon; + + // §3.4.2 AHT bit stream syntax (Table E1.3 chunk gated by `ahte`): + // + // if (ncplblks == 6 && ncplregs == 1) cplahtinu (1 bit) + // for ch in 0..nfchans: + // if nchregs[ch] == 1 chahtinu[ch] (1 bit) + // if (lfeon && nlferegs == 1) lfeahtinu (1 bit) + if audfrm.ncplblks == 6 && hints.ncplregs == 1 { + audfrm.cplahtinu = br.read_u32(1)? != 0; + } + for ch in 0..nfchans { + if hints.nchregs[ch] == 1 { + audfrm.chahtinu[ch] = br.read_u32(1)? != 0; + } + } + if lfeon && hints.nlferegs == 1 { + audfrm.lfeahtinu = br.read_u32(1)? != 0; + } + + parse_tail(br, audfrm, bsi)?; + audfrm.aht_phase_b_pending = false; + Ok(()) +} + +/// Convenience parser that creates a fresh [`BitReader`] over `data`. +pub fn parse(data: &[u8], bsi: &Bsi) -> Result { + let mut br = BitReader::new(data); + parse_with(&mut br, bsi) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_bsi(acmod: u8, lfeon: bool, num_blocks: u8, strmtyp: StreamType) -> Bsi { + Bsi { + strmtyp, + substreamid: 0, + frmsiz: 383, + fscod: 0, + fscod2: 0xFF, + sample_rate: 48_000, + numblkscod: if num_blocks == 6 { 3 } else { num_blocks - 1 }, + num_blocks, + acmod, + nfchans: crate::tables::acmod_nfchans(acmod), + lfeon, + nchans: crate::tables::acmod_nfchans(acmod) + u8::from(lfeon), + bsid: 16, + dialnorm: 27, + dialnorm_ch2: None, + bsmod: None, + chanmap: None, + annex_e_mix_levels: None, + dmixmod: 0xFF, + dmixmod_preference: None, + lfemixlevcod: None, + pgmscl: None, + pgmscl2: None, + extpgmscl: None, + paninfo: None, + paninfo2: None, + premix_compression: None, + compr: None, + compr_ch2: None, + dsurexmod: None, + dheadphonmod: None, + dolby_surround_mode: None, + adconvtyp: None, + adconvtyp_ch2: None, + audio_production: None, + audio_production_ch2: None, + copyright_info: None, + addbsi: None, + frame_bytes: 768, + bits_consumed: 0, + } + } + + fn pack_msb(bits: &[(u32, u32)]) -> Vec { + let total: u32 = bits.iter().map(|(n, _)| *n).sum(); + let nbytes = total.div_ceil(8); + let mut out = vec![0u8; nbytes as usize]; + let mut bitpos = 0u32; + for &(n, v) in bits { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = (bitpos / 8) as usize; + let shift = 7 - (bitpos % 8); + out[byte] |= bit << shift; + bitpos += 1; + } + } + out + } + + /// 2/0 stereo, 6 blocks, all strategy flags at the encoder's + /// preferred values (expstre=1, blkswe=1, dithflage=1, bamode=1, + /// dbaflde=1, skipflde=1; ahte=0, transproce=0, spxattene=0, + /// snroffststr=0, frmfgaincode=0). acmod=2 has no LFE and (without + /// a coupling block) no per-block coupling bits. + #[test] + fn parses_minimal_indep_stereo_audfrm() { + let bsi = make_bsi(2, false, 6, StreamType::Independent); + // Strategy flags (numblkscod==3 → 6 blocks) + let mut bits: Vec<(u32, u32)> = vec![(1, 1)]; // expstre + bits.push((1, 0)); // ahte + bits.push((2, 0)); // snroffststr + bits.push((1, 0)); // transproce + bits.push((1, 1)); // blkswe + bits.push((1, 1)); // dithflage + bits.push((1, 1)); // bamode + bits.push((1, 0)); // frmfgaincode + bits.push((1, 1)); // dbaflde + bits.push((1, 1)); // skipflde + bits.push((1, 0)); // spxattene + // acmod>1 → block 0 cplinu, then 5*(cplstre[, cplinu]) pairs. + bits.push((1, 0)); // cplinu[0] = 0 + for _ in 1..6 { + bits.push((1, 0)); // cplstre[blk] = 0 + } + // expstre==1 + cplinu[blk]=0 for every block → per-block + // per-channel chexpstr (2 bits each) for 6 blocks × 2 channels + // = 24 bits live HERE in audfrm (Table E.1.3 / §E.1.2.3). + // No cplexpstr (no coupling). No lfeexpstr (lfeon=false). + for _ in 0..(6 * 2) { + bits.push((2, 0)); // chexpstr[blk][ch] = REUSE + } + // strmtyp == 0 + numblkscod == 0x3 → convexpstre implicit = 1, + // followed by per-channel convexpstr (5 bits each). + for _ in 0..2 { + bits.push((5, 0)); + } + // ahte=0 → no AHT block. + // snroffststr=0 → frmcsnroffst (6) + frmfsnroffst (4) + bits.push((6, 15)); + bits.push((4, 0)); + // transproce=0, spxattene=0 → nothing. + // num_blocks > 1 → blkstrtinfoe (1 bit, 0). + bits.push((1, 0)); + + let buf = pack_msb(&bits); + let af = parse(&buf, &bsi).unwrap(); + assert!(af.expstre); + assert!(af.blkswe); + assert!(af.dithflage); + assert!(af.bamode); + assert!(af.dbaflde); + assert!(af.skipflde); + assert!(!af.ahte); + assert!(!af.transproce); + assert!(!af.spxattene); + assert_eq!(af.snroffststr, 0); + assert_eq!(af.frmcsnroffst, 15); + assert_eq!(af.frmfsnroffst, 0); + } + + /// Table E2.10 row sanity — the table is the contract between the + /// audfrm parser (which expands `frmchexpstr` / `frmcplexpstr`) + /// and the audblk DSP (which consumes per-block strategy codes via + /// `chexpstr_blk_ch[blk][ch]` / `cplexpstr_blk[blk]`). + #[test] + fn frame_exp_strat_table_spot_check_e2_10() { + // Row 0: D15 R R R R R + assert_eq!(FRAME_EXP_STRAT_TABLE[0], [D15, R, R, R, R, R]); + // Row 16: D45 D15 R R R R (the prevailing corpus pattern — every + // validator-encoded fixture in our corpus picks row 16 for fbw + // channels). + assert_eq!(FRAME_EXP_STRAT_TABLE[16], [D45, D15, R, R, R, R]); + // Row 28: D45 D45 D45 D25 R R (used by the 64kbps low-rate + // stereo fixture's frmcplexpstr). + assert_eq!(FRAME_EXP_STRAT_TABLE[28], [D45, D45, D45, D25, R, R]); + // Row 31: D45 across every block — fully refreshed exponents + // each block, the most-bits / least-temporal-correlation choice. + assert_eq!(FRAME_EXP_STRAT_TABLE[31], [D45, D45, D45, D45, D45, D45]); + // Block 0 column shall never be REUSE per spec design (the decoder + // needs concrete exponents on every syncframe's first block). + for row in &FRAME_EXP_STRAT_TABLE { + assert_ne!(row[0], R, "Table E2.10: block 0 must not be REUSE"); + } + } + + /// `expstre == 0` path: the parser expands `frmchexpstr` / + /// `frmcplexpstr` codewords into the per-block-per-channel strategy + /// arrays via Table E2.10 — the audblk DSP needs them shaped the + /// same as the `expstre == 1` path. + #[test] + fn parses_minimal_indep_stereo_audfrm_with_frame_exp_strat() { + let bsi = make_bsi(2, false, 6, StreamType::Independent); + let mut bits: Vec<(u32, u32)> = vec![(1, 0)]; // expstre = 0 + bits.push((1, 0)); // ahte + bits.push((2, 0)); // snroffststr + bits.push((1, 0)); // transproce + bits.push((1, 1)); // blkswe + bits.push((1, 1)); // dithflage + bits.push((1, 1)); // bamode + bits.push((1, 0)); // frmfgaincode + bits.push((1, 1)); // dbaflde + bits.push((1, 1)); // skipflde + bits.push((1, 0)); // spxattene + // acmod>1 → cplinu[0]=1, then cplstre[1..5]=0 (sticky reuse keeps cplinu=1) + bits.push((1, 1)); + for _ in 1..6 { + bits.push((1, 0)); + } + // expstre==0 + acmod>1 + ncplblks>0 → frmcplexpstr (5 bits). + // Pick row 16 to match the prevailing corpus pattern. + bits.push((5, 16)); + // 2 fbw channels × frmchexpstr (5 bits each). + bits.push((5, 16)); + bits.push((5, 28)); + // strmtyp==0 + numblkscod==3 → convexpstre implicit, 2 × 5 bits. + bits.push((5, 0)); + bits.push((5, 0)); + // snroffststr=0 → frmcsnroffst + frmfsnroffst. + bits.push((6, 15)); + bits.push((4, 0)); + // num_blocks > 1 → blkstrtinfoe (1 bit, 0). + bits.push((1, 0)); + + let buf = pack_msb(&bits); + let af = parse(&buf, &bsi).unwrap(); + assert!(!af.expstre); + assert_eq!(af.ncplblks, 6); + assert_eq!(af.frmcplexpstr, 16); + assert_eq!(af.frmchexpstr[0], 16); + assert_eq!(af.frmchexpstr[1], 28); + // Expansion: row 16 = [D45, D15, R, R, R, R]; row 28 = [D45, D45, D45, D25, R, R]. + let exp16 = FRAME_EXP_STRAT_TABLE[16]; + let exp28 = FRAME_EXP_STRAT_TABLE[28]; + for blk in 0..6 { + assert_eq!(af.cplexpstr_blk[blk], exp16[blk], "cpl[{blk}]"); + assert_eq!(af.chexpstr_blk_ch[blk][0], exp16[blk], "ch0[{blk}]"); + assert_eq!(af.chexpstr_blk_ch[blk][1], exp28[blk], "ch1[{blk}]"); + } + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/bsi.rs b/crates/vendor/oxideav-ac3/src/eac3/bsi.rs new file mode 100644 index 00000000..db89b98a --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/bsi.rs @@ -0,0 +1,3343 @@ +//! E-AC-3 (Annex E) Bit Stream Information parser — `bsi()` per +//! §E.2.2.2 / Table E1.2. +//! +//! Unlike base AC-3, the E-AC-3 syncinfo is just a 16-bit syncword +//! (`0x0B77`) — no `crc1`, no `fscod`, no `frmsizecod`. The +//! sample-rate and frame-size codes have moved into the BSI itself, +//! reordered, and joined by a stream-type tag, substream id, and a +//! variable number-of-blocks code (1, 2, 3, or 6 audio blocks per +//! syncframe instead of AC-3's hard-coded 6). +//! +//! This module parses the **entire** Table E1.2 BSI bit-by-bit, +//! including the optional `mixmdate`, `infomdate`, and `addbsi` +//! chains. Fields that the round-1 decoder does not act on are still +//! consumed exactly so the bit cursor lands on byte/bit position +//! `start + bits_consumed = start of audfrm()`. +//! +//! ## Bit-stream order (Table E1.2 verbatim) +//! +//! ```text +//! strmtyp 2 +//! substreamid 3 +//! frmsiz 11 // size in 16-bit words minus one +//! fscod 2 +//! if (fscod == 0x3) { +//! fscod2 2 // numblkscod implicit = 0x3 (6 blocks) +//! } else { +//! numblkscod 2 +//! } +//! acmod 3 +//! lfeon 1 +//! bsid 5 // ≥ 11 for E-AC-3 (16 = canonical) +//! dialnorm 5 +//! compre 1 +//! if (compre) compr 8 +//! if (acmod == 0) { +//! dialnorm2 5 +//! compr2e 1 +//! if (compr2e) compr2 8 +//! } +//! if (strmtyp == 0x1) { // dependent substream +//! chanmape 1 +//! if (chanmape) chanmap 16 +//! } +//! mixmdate 1 +//! if (mixmdate) /* parses 0..200 bits per Table E1.2 */ +//! infomdate 1 +//! if (infomdate) /* parses 0..50 bits per Table E1.2 */ +//! addbsie 1 +//! if (addbsie) addbsil 6, addbsi (addbsil+1)*8 bits +//! ``` +//! +//! Field semantics are described per §E.2.3.1.x in the spec PDF. + +use oxideav_core::bits::BitReader; +use oxideav_core::{Error, Result}; + +use crate::bsi::{ + AdConverterType, AdditionalBitStreamInfo, AnnexDMixLevels, AudioProductionInfo, + CompressionGain, CopyrightInfo, DialNorm, DolbyHeadphoneMode, DolbySurroundExMode, + DolbySurroundMode, RoomType, StereoDownmixPreference, +}; +use crate::tables::acmod_nfchans; + +/// Largest `bsid` value still served by the base AC-3 parser. Streams +/// at `bsid` 11..=16 use the E-AC-3 (Annex E) syntax. +pub const BSID_BASE_AC3_MAX: u8 = 10; + +/// Canonical Annex E stream identification value (§E.2.3.1.6, "10000"). +/// Backwards-compatible variants 11..15 share the same syntax. +pub const EAC3_BSID: u8 = 16; + +/// Stream type — Table E2.1. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StreamType { + /// Type 0: independent substream (or sole independent stream). + Independent, + /// Type 1: dependent substream (refers back to the immediately + /// preceding independent substream). + Dependent, + /// Type 2: an AC-3 bit-stream wrapped inside an E-AC-3 sync layer + /// (§E.2.3.1.1 "may not have any dependent substreams associated"). + Ac3Convert, + /// Type 3: reserved. + Reserved, +} + +impl StreamType { + fn from_u8(v: u8) -> Self { + match v & 0x3 { + 0 => StreamType::Independent, + 1 => StreamType::Dependent, + 2 => StreamType::Ac3Convert, + _ => StreamType::Reserved, + } + } + + /// Raw 2-bit value the parser read. + pub fn raw(self) -> u8 { + match self { + StreamType::Independent => 0, + StreamType::Dependent => 1, + StreamType::Ac3Convert => 2, + StreamType::Reserved => 3, + } + } +} + +/// §E.2.3.1.12-17 program scale factor — the 6-bit gain word an +/// independent substream's mixing-metadata block can attach to its +/// own program (`pgmscl`, §E.2.3.1.13), to the second program of a +/// 1+1 dual-mono stream (`pgmscl2`, §E.2.3.1.15), or to an +/// *external* program carried in a different bit stream / +/// independent substream (`extpgmscl`, §E.2.3.1.17 — "this field +/// shall use the same scale as pgmscl"). +/// +/// Wire scale per §E.2.3.1.13: the value `0` shall be interpreted +/// as **mute**, and the values `1..=63` as a scale factor of +/// `-50 dB` to `+12 dB` in 1 dB steps — i.e. +/// `decibels() == code - 51`, with the `51` codepoint at 0 dB +/// (unity). When the corresponding exists-flag (`pgmscle` / +/// `pgmscl2e` / `extpgmscle`) is `0`, "the program scale factor +/// shall be 0 dB (no scaling)" per §E.2.3.1.12/.14/.16 — the BSI +/// fields represent that absent state as `None`. +/// +/// Per §E.3.10.1-2 the gain is applied "during the mixing process" +/// of a dual-decoder main + associated-service mixer (`pgmscl` +/// attenuates the program carried in the *same* substream; +/// `extpgmscl` attenuates the program carried in a *different* +/// bit stream or independent substream). The single-stream decode +/// path is unchanged — this is pure surfaced metadata for a +/// downstream §E.3.10 mixer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ProgramScaleFactor(u8); + +impl ProgramScaleFactor { + /// Wraps a 6-bit wire codepoint. The upper two bits of `code` + /// are ignored so a caller can pass a wider word verbatim. + pub fn from_code(code: u8) -> Self { + Self(code & 0x3F) + } + + /// The raw 6-bit codepoint (`0..=63`) for bit-stream round-trip. + pub fn raw(self) -> u8 { + self.0 + } + + /// `true` for the `0` codepoint — §E.2.3.1.13 "the value 0 shall + /// be interpreted as mute". + pub fn is_mute(self) -> bool { + self.0 == 0 + } + + /// The scale factor in dB: `Some(code - 51)` spanning `-50..=+12` + /// for the codepoints `1..=63`; `None` for the mute codepoint, + /// which has no finite dB value. + pub fn decibels(self) -> Option { + if self.0 == 0 { + None + } else { + Some(self.0 as i8 - 51) + } + } + + /// Linear amplitude scale a §E.3.10 mixer multiplies the program + /// by: `0.0` for the mute codepoint, otherwise `10^(dB / 20)`. + pub fn linear(self) -> f32 { + match self.decibels() { + None => 0.0, + Some(db) => 10f32.powf(f32::from(db) / 20.0), + } + } +} + +/// §E.2.3.1.53-58 pan information — the 8-bit mean-direction index +/// (`panmean`, §E.2.3.1.54) plus the 6-bit reserved trailer +/// (`paninfo`, §E.2.3.1.55) an independent mono / 1+1 dual-mono +/// substream's mixing-metadata block can attach so a §E.3.10.8 +/// dual-decoder mixer pans the mono associated-audio program across +/// the channels of the main audio service. +/// +/// Wire scale per §E.2.3.1.54: index `0` points the panned virtual +/// source toward the **center** speaker location (defined as 0 +/// degrees); each step is 1.5 degrees of clockwise rotation, so +/// indices `0..=239` span `0..=358.5` degrees while `240..=255` are +/// reserved. When the exists-flag (`paninfoe` / `paninfo2e`) is `0` +/// "the pan position word is defaulted to center" per +/// §E.2.3.1.53/.56 — the BSI fields represent that absent state as +/// `None` ([`PanInfo::CENTER`] is the equivalent explicit value). +/// +/// §E.3.10.8 derives per-output-channel scale factors for the +/// associated program from the index — [`Self::stereo_scale_factors`] +/// implements the stereo-output table and +/// [`Self::surround_scale_factors`] the 5.1-output pair of tables +/// (captioned Tables E3.15-E3.17; the §E.3.10.8 prose cites them +/// off-by-one as E3.16-E3.18 — the captions are followed here). The +/// single-stream decode path is unchanged — this is pure surfaced +/// metadata for a downstream §E.3.10 mixer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PanInfo { + panmean: u8, + reserved: u8, +} + +impl PanInfo { + /// The §E.2.3.1.53/.56 default when the exists-flag is `0`: pan + /// position "center" (index 0 → the center speaker location). + pub const CENTER: Self = Self { + panmean: 0, + reserved: 0, + }; + + /// Wraps the raw wire fields: the 8-bit `panmean` index and the + /// 6-bit reserved `paninfo` word (upper bits of `reserved` are + /// masked so a caller can pass a wider word verbatim). + pub fn from_fields(panmean: u8, reserved: u8) -> Self { + Self { + panmean, + reserved: reserved & 0x3F, + } + } + + /// The raw 8-bit `panmean` index (`0..=255`) for bit-stream + /// round-trip. + pub fn panmean(self) -> u8 { + self.panmean + } + + /// The raw 6-bit reserved `paninfo` field (§E.2.3.1.55 "reserved + /// for future mixing applications"), preserved verbatim for + /// bit-stream round-trip. + pub fn reserved(self) -> u8 { + self.reserved + } + + /// `true` for the reserved index codepoints `240..=255` + /// (§E.2.3.1.54 "values 240 to 255 are reserved"). + pub fn is_reserved_index(self) -> bool { + self.panmean >= 240 + } + + /// Mean angle of rotation relative to the center position, + /// clockwise, in degrees: `Some(panmean × 1.5)` spanning + /// `0.0..=358.5` for indices `0..=239`; `None` for the reserved + /// indices. + pub fn degrees(self) -> Option { + if self.is_reserved_index() { + None + } else { + Some(f32::from(self.panmean) * 1.5) + } + } + + /// Table E3.15 — associated-audio scale factors `(AL, AR)` for + /// stereo-output panning: the mono associated program is split + /// into two channels mixed with the main service's Left / Right + /// respectively. `None` for the reserved indices. Every + /// non-reserved index is power-preserving (`AL² + AR² == 1`). + pub fn stereo_scale_factors(self) -> Option<(f32, f32)> { + use std::f32::consts::FRAC_PI_2; + let p = f32::from(self.panmean); + match self.panmean { + 0..=19 => { + let a = FRAC_PI_2 * ((p + 20.0) / 40.0); + Some((a.cos(), a.sin())) + } + 20..=99 => Some((0.0, 1.0)), + 100..=139 => { + let a = FRAC_PI_2 * ((p - 100.0) / 40.0); + Some((a.sin(), a.cos())) + } + 140..=219 => Some((1.0, 0.0)), + 220..=239 => { + let a = FRAC_PI_2 * ((p - 220.0) / 40.0); + Some((a.cos(), a.sin())) + } + _ => None, + } + } + + /// Tables E3.16 + E3.17 — associated-audio scale factors + /// `[AL, AC, AR, ALS, ARS]` for 5.1-channel-output panning: the + /// mono associated program is split into five channels mixed + /// with the main service's Left / Center / Right / Left Surround + /// / Right Surround respectively (the LFE channel is not + /// included per §E.3.10.8). `None` for the reserved indices. + /// Every non-reserved index is power-preserving (the five + /// squares sum to 1 — each range is a single sin/cos pair across + /// two adjacent speakers). + pub fn surround_scale_factors(self) -> Option<[f32; 5]> { + use std::f32::consts::FRAC_PI_2; + let p = f32::from(self.panmean); + match self.panmean { + 0..=19 => { + let a = FRAC_PI_2 * (p / 20.0); + Some([0.0, a.cos(), a.sin(), 0.0, 0.0]) + } + 20..=72 => { + let a = FRAC_PI_2 * ((p - 20.0) / 53.0); + Some([0.0, 0.0, a.cos(), 0.0, a.sin()]) + } + 73..=166 => { + let a = FRAC_PI_2 * ((p - 73.0) / 94.0); + Some([0.0, 0.0, 0.0, a.sin(), a.cos()]) + } + 167..=219 => { + let a = FRAC_PI_2 * ((p - 167.0) / 53.0); + Some([a.sin(), 0.0, 0.0, a.cos(), 0.0]) + } + 220..=239 => { + let a = FRAC_PI_2 * ((p - 220.0) / 20.0); + Some([a.cos(), a.sin(), 0.0, 0.0, 0.0]) + } + _ => None, + } + } +} + +/// §E.2.3.1.19-21 premix-compression control — the three fields the +/// `mixdef == 0x1` ("mixing option 2") body of an independent +/// substream's mixing-metadata block carries to steer a §E.3.10 +/// dual-decoder mixer's *premix compression* process (the dynamic- +/// range / gain adjustment applied to the main audio service before +/// the associated program is mixed in): +/// +/// * `premixcmpsel` (§E.2.3.1.19, 1 bit) — *premix compression word +/// select*. `false` ⇒ the `dynrng` field is used in the premix +/// compression process; `true` ⇒ the `compr` field is used. +/// * `drcsrc` (§E.2.3.1.20, 1 bit) — *dynamic-range-control word +/// source*. `false` ⇒ the `dynrng` / `compr` fields of the +/// **external** program (the one in a separate bit stream / +/// independent substream) control the mix; `true` ⇒ the fields of +/// the **current** substream are used. Recommended `false`. +/// * `premixcmpscl` (§E.2.3.1.21, 3 bits) — *premix compression word +/// scale factor*. Table E2.7 maps the code to a compression +/// gain-reduction ratio applied before the main-service / external +/// mix. Recommended `0b000` (no compression). +/// +/// Per §E.2.3.1.21 all three fields "shall be present in the +/// bitstream. However they should be set to the recommended values, +/// as decoders are not required to use them." The single-stream +/// decode path is unchanged — this is pure surfaced metadata for a +/// downstream §E.3.10 mixer. The same three fields also appear inside +/// the `mixdef == 0x3` ("mixing option 4") `mixdata` body; this +/// surface covers the `mixdef == 0x1` carriage only (the variable +/// option-4 body stays an opaque skip). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PremixCompression { + premixcmpsel: bool, + drcsrc: bool, + premixcmpscl: u8, +} + +/// The compression word a §E.3.10 premix process draws on, selected +/// by `premixcmpsel` (§E.2.3.1.19). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PremixCompressionWord { + /// `premixcmpsel == 0` — the §5.4.3.x `dynrng` dynamic-range word. + DynRng, + /// `premixcmpsel == 1` — the §5.4.2.10 `compr` heavy-compression + /// word. + Compr, +} + +/// Which program's dynamic-range-control words drive the mix, selected +/// by `drcsrc` (§E.2.3.1.20). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DrcSource { + /// `drcsrc == 0` — the **external** program's `dynrng` / `compr` + /// fields (the recommended setting). + ExternalProgram, + /// `drcsrc == 1` — the **current** substream's `dynrng` / `compr` + /// fields. + CurrentSubstream, +} + +impl PremixCompression { + /// Wraps the raw `mixdef == 0x1` body: `premixcmpsel` (1 bit), + /// `drcsrc` (1 bit), and the 3-bit `premixcmpscl` code (upper bits + /// masked so a caller can pass a wider word verbatim). + pub fn from_fields(premixcmpsel: bool, drcsrc: bool, premixcmpscl: u8) -> Self { + Self { + premixcmpsel, + drcsrc, + premixcmpscl: premixcmpscl & 0x7, + } + } + + /// Raw `premixcmpsel` bit (§E.2.3.1.19) for bit-stream round-trip. + pub fn premixcmpsel(self) -> bool { + self.premixcmpsel + } + + /// Raw `drcsrc` bit (§E.2.3.1.20) for bit-stream round-trip. + pub fn drcsrc(self) -> bool { + self.drcsrc + } + + /// Raw 3-bit `premixcmpscl` code (§E.2.3.1.21) for bit-stream + /// round-trip. + pub fn premixcmpscl(self) -> u8 { + self.premixcmpscl + } + + /// Typed view of `premixcmpsel` — which compression word the + /// premix process uses (§E.2.3.1.19). + pub fn compression_word(self) -> PremixCompressionWord { + if self.premixcmpsel { + PremixCompressionWord::Compr + } else { + PremixCompressionWord::DynRng + } + } + + /// Typed view of `drcsrc` — which program's DRC words drive the + /// mix (§E.2.3.1.20). + pub fn drc_source(self) -> DrcSource { + if self.drcsrc { + DrcSource::CurrentSubstream + } else { + DrcSource::ExternalProgram + } + } + + /// `true` for the `0b110` `premixcmpscl` codepoint, which has no + /// row in Table E2.7 (the table lists `000,001,010,011,100,101,111` + /// only). Reserved / undefined; [`Self::scale_ratio`] returns + /// `None` for it. + pub fn is_premixcmpscl_reserved(self) -> bool { + self.premixcmpscl == 0b110 + } + + /// Table E2.7 compression gain-reduction ratio for `premixcmpscl`: + /// the seven listed codes map to `code / 6` (`0b000` ⇒ 0% / no + /// compression, ascending in `1/6` steps to `0b111` ⇒ 100% / + /// maximum compression). `None` for the unlisted `0b110` + /// codepoint. The spec captions the column "Scale Factor" with + /// percentage values (16.7%, 33.3%, …) that are exactly the + /// `n/6` ratios rounded to one decimal. + pub fn scale_ratio(self) -> Option { + let sixth = match self.premixcmpscl { + 0b000 => 0, + 0b001 => 1, + 0b010 => 2, + 0b011 => 3, + 0b100 => 4, + 0b101 => 5, + 0b111 => 6, + _ => return None, // 0b110: not in Table E2.7 + }; + Some(sixth as f32 / 6.0) + } + + /// `true` for the recommended default configuration + /// (§E.2.3.1.19-21): `premixcmpsel == 0`, `drcsrc == 0`, + /// `premixcmpscl == 0b000` (no compression). An encoder writing a + /// `mixdef == 0x1` block "should" set this; a decoder seeing a + /// non-default configuration is the signal that the encoder + /// actually intends premix compression to take effect. + pub fn is_recommended_default(self) -> bool { + !self.premixcmpsel && !self.drcsrc && self.premixcmpscl == 0 + } +} + +/// Parsed E-AC-3 BSI — the subset actually needed by the round-1 +/// decoder + dispatcher. Fields not surfaced are still parsed (the +/// bit cursor walk has to land at the start of `audfrm()`). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bsi { + pub strmtyp: StreamType, + pub substreamid: u8, + /// `frmsiz` raw value. Frame size in bytes = `(frmsiz + 1) * 2`. + pub frmsiz: u16, + /// `fscod` raw value (2 bits). 0x3 indicates a reduced-rate stream + /// (use `fscod2` for the actual rate). + pub fscod: u8, + /// `fscod2` (2 bits). Only valid when `fscod == 0x3`; 0xFF + /// otherwise. + pub fscod2: u8, + /// Sample rate in Hz, derived from `(fscod, fscod2)`. + pub sample_rate: u32, + /// `numblkscod` raw value (2 bits). 0x3 = 6 blocks. 0/1/2 = 1/2/3 + /// blocks. When `fscod == 0x3` it is implicitly 0x3. + pub numblkscod: u8, + /// Number of audio blocks per syncframe (= 256 PCM samples each). + /// Derived from `numblkscod`. Always 6 on reduced-rate streams. + pub num_blocks: u8, + /// AC-3 audio coding mode (Table 5.8, §5.4.2.3) — channel layout. + pub acmod: u8, + /// Number of full-bandwidth channels (`acmod_nfchans(acmod)`). + pub nfchans: u8, + /// Whether the LFE channel is coded in this substream. + pub lfeon: bool, + /// Total channel count = `nfchans + lfeon`. + pub nchans: u8, + /// `bsid` (5 bits). 16 = canonical Annex E; 11..15 = backward- + /// compatible Annex E variants. + pub bsid: u8, + /// Dialogue normalization, 1..=31 dB below reference. 0 in the + /// stream is reserved → mapped to 31 per §5.4.2.8 (Annex E reuses + /// the base spec semantics). For a typed surface exposing the + /// §7.6 reproduction-gain derivation, use + /// [`Self::dialogue_normalization`]. + pub dialnorm: u8, + /// §5.4.2.16 dialogue normalization for Ch2 in 1+1 dual-mono + /// Annex E streams (`acmod == 0`). `None` outside `acmod == 0`. + /// Same post-remap `1..=31` semantics as + /// [`Self::dialnorm`] (the `0` wire codepoint maps to `31`). + /// + /// For the typed surface, see + /// [`Self::dialogue_normalization_ch2`]. + pub dialnorm_ch2: Option, + /// `chanmap` field (16 bits, dependent substream only). `None` + /// when `strmtyp != Dependent` or `chanmape == 0`. + /// + /// Per Table E2.5, bit *i* (counted from the field's MSB → bit 0 + /// = MSB) flags channel location *i* — see the table for the + /// label assignment. + pub chanmap: Option, + /// Annex E mixmdata mix levels (Table E1.2 §E.1.2.2). `Some` when + /// `mixmdate == 1` AND the per-channel-presence guards in + /// [`parse_mixing_metadata`] fire (3 front channels for the LtRt/LoRo + /// **center** codes, a surround channel for the **surround** codes); + /// fields that the guard skips read back as `0xFF` so callers can + /// distinguish "spec-absent" from a legitimate 0b000 code. + /// + /// The 3-bit codewords map to linear gains via the same Tables + /// D2.3-D2.6 used by base AC-3's Annex D xbsi1 — Annex E §E.2.3.1.3-6 + /// states "the value of [field] is the same as defined for AC-3 + /// in Annex D, §2.3.1.3 [-6]". Reuse of [`AnnexDMixLevels`] keeps a + /// single source of truth. + pub annex_e_mix_levels: Option, + /// Annex E mixmdata preferred-stereo-downmix advisory (`dmixmod`, + /// 2 bits) — Table E1.2 §E.1.2.2 reuses Annex D §2.3.1.2 semantics + /// (`00` = not indicated, `01` = LtRt preferred, `10` = LoRo + /// preferred, `11` = reserved). `0xFF` when `mixmdate == 0` or the + /// `acmod > 2` guard fires. + pub dmixmod: u8, + /// Annex E mixmdata preferred stereo downmix mode (Table E1.2 + /// §E.1.2.2 reusing Annex D §2.3.1.2 / Table D2.2), surfaced as a + /// typed [`StereoDownmixPreference`]. `Some` only when + /// `mixmdate == 1` AND `acmod > 2`; `None` otherwise (the + /// per-Table-E1.2 guard skips the 2-bit slot for mono / 2/0 + /// streams, and a `mixmdate == 0` syncframe skips the entire + /// mixing-metadata block). Equivalent to the typed view of + /// [`Bsi::dmixmod`] where the `0xFF` "absent" sentinel becomes + /// `None`; the raw field stays authoritative for bit-stream + /// round-trip and the typed surface is a thin convenience over + /// it. Lets a §3.1.1 auto-mode two-channel-downmix router pick + /// LtRt vs LoRo without consulting a magic-number sentinel — + /// shared with the base-syntax [`crate::bsi::Bsi::dmixmod_preference`] + /// so a single chain consumer can handle both syntaxes. + pub dmixmod_preference: Option, + /// Annex E mixmdata LFE mix level (`lfemixlevcod`, 5 bits, §E.1.2.2). + /// `Some` when `lfeon == 1`, `mixmdate == 1`, and `lfemixlevcode == 1`; + /// `None` otherwise. The 5-bit code is **not** consulted by the + /// round-129 downmix (LFE stays muted per §7.8) but is surfaced so + /// downstream tooling and a future LFE-into-stereo bass-route can + /// honour it without re-parsing the BSI. + pub lfemixlevcod: Option, + /// §E.2.3.1.12-13 program scale factor (`pgmscl`) — the gain a + /// §E.3.10 dual-decoder mixer applies to the program carried in + /// *this* substream while mixing it with an associated service. + /// `Some` only when `mixmdate == 1`, the substream is independent + /// (Table E1.2 emits the `pgmscle` chain under + /// `strmtyp == 0x0` only), AND `pgmscle == 1`; `None` otherwise — + /// per §E.2.3.1.12 the absent state means "0 dB (no scaling)". + /// See [`ProgramScaleFactor`] for the mute / `-50..=+12 dB` wire + /// scale. The single-stream decode path does not consult it. + pub pgmscl: Option, + /// §E.2.3.1.14-15 program scale factor #2 (`pgmscl2`) — same + /// scale as [`Bsi::pgmscl`] but applying to the second audio + /// channel when `acmod` indicates two independent channels (1+1 + /// dual mono). `Some` only when `mixmdate == 1`, the substream is + /// independent, `acmod == 0`, AND `pgmscl2e == 1`. + pub pgmscl2: Option, + /// §E.2.3.1.16-17 external program scale factor (`extpgmscl`) — + /// the gain a §E.3.10 mixer applies to an *external* program (one + /// carried in a separate bit stream or independent substream from + /// the one carrying this instance). Same wire scale as + /// [`Bsi::pgmscl`] per §E.2.3.1.17. `Some` only when + /// `mixmdate == 1`, the substream is independent, AND + /// `extpgmscle == 1`. + pub extpgmscl: Option, + /// §E.2.3.1.53-55 pan information (`panmean` + the reserved + /// `paninfo` trailer) — the mean-direction index a §E.3.10.8 + /// mixer uses to pan this mono associated-audio program across + /// the channels of the main audio service. `Some` only when + /// `mixmdate == 1`, the substream is independent (Table E1.2 + /// emits the chain under `strmtyp == 0x0` only), + /// `acmod < 0x2` (mono or 1+1 dual-mono source), AND + /// `paninfoe == 1`; `None` otherwise — per §E.2.3.1.53 the + /// absent state defaults the pan position word to "center" + /// ([`PanInfo::CENTER`]). See [`PanInfo`] for the 1.5-degree + /// index scale and the Table E3.15-E3.17 output scale factors. + /// The single-stream decode path does not consult it. + pub paninfo: Option, + /// §E.2.3.1.56-58 pan information #2 (`panmean2` + `paninfo2`) + /// — same meaning as [`Bsi::paninfo`] but applying to the second + /// audio channel when `acmod` indicates two independent channels + /// (1+1 dual mono). `Some` only when `mixmdate == 1`, the + /// substream is independent, `acmod == 0`, AND `paninfo2e == 1`. + pub paninfo2: Option, + /// §E.2.3.1.19-21 premix-compression control (`premixcmpsel` / + /// `drcsrc` / `premixcmpscl`) — the three fields that steer a + /// §E.3.10 mixer's premix compression process. `Some` only when + /// `mixmdate == 1`, the substream is independent, AND the + /// mixing-metadata block selects `mixdef == 0x1` ("mixing option + /// 2"); `None` otherwise (a `mixdef ∈ {0, 2, 3}` block does not + /// carry the three fields as a standalone 5-bit group, and a + /// `mixmdate == 0` syncframe skips the whole block). See + /// [`PremixCompression`] for the field semantics + Table E2.7 + /// scale lookup. The single-stream decode path does not consult + /// it. + pub premix_compression: Option, + /// Heavy compression gain word (`compr`, §E.2.3.1.x / §5.4.2.10 + + /// §7.7.2.2 reused per Annex E). Identical semantics + wire format + /// to base AC-3 — see [`CompressionGain`] for the X/Y decode. For + /// 1+1 dual-mono (`acmod == 0`) this is the Ch1 word; Ch2 is + /// surfaced separately as [`Bsi::compr_ch2`]. `Some` when + /// `compre == 1`; `None` otherwise. + pub compr: Option, + /// Ch2 heavy compression gain word for 1+1 dual-mono only. `None` + /// outside `acmod == 0`, or inside `acmod == 0` when `compr2e == 0`. + pub compr_ch2: Option, + /// Bit-stream mode (`bsmod`, 3 bits, Table 5.7 semantics). In the + /// Annex E syntax this word lives inside the informational-metadata + /// block, so it is `Some` only when `infomdate == 1`; `None` means + /// "not transmitted" (treat as `0` = complete main). + pub bsmod: Option, + /// Dolby Surround EX mode (§E.2.3.1.x informational metadata, gated + /// by `infomdate==1` AND `acmod >= 6`). Carries the same semantics + /// as Annex D §2.3.1.8 / Table D2.7 — see + /// [`crate::bsi::DolbySurroundExMode`]. `None` when the informational + /// metadata block was absent or when `acmod < 6` (no stereo + /// surround pair to drive the EX matrix). + pub dsurexmod: Option, + /// Dolby Headphone mode (§E.2.3.1.x informational metadata, gated + /// by `infomdate==1` AND `acmod == 2`). Same semantics as Annex D + /// §2.3.1.9 / Table D2.8 — see + /// [`crate::bsi::DolbyHeadphoneMode`]. `None` when the + /// informational metadata block was absent or when `acmod != 2`. + pub dheadphonmod: Option, + /// Base-syntax Dolby Surround mode reused inside the Annex E + /// informational-metadata block. `Some` only when `infomdate == 1` + /// AND `acmod == 2` (2/0 stereo — the only channel layout that + /// carries the codeword on the wire per Table 5.11 / §5.4.2.6, + /// reused verbatim by Annex E §E.2.3.1.x); `None` otherwise. Same + /// semantics as [`crate::bsi::Bsi::dolby_surround_mode`] — see + /// [`crate::bsi::DolbySurroundMode`] for the typed surface. Single + /// source of truth across base + Annex E so a chain consumer can + /// route both syntaxes through one branch on + /// [`Bsi::dolby_surround_mode`]. + pub dolby_surround_mode: Option, + /// A/D converter type for the Ch1 audio production (§E.2.3.1.x + /// informational metadata, gated by `infomdate==1` AND + /// `audprodie==1`). Same semantics as Annex D §2.3.1.10 / Table + /// D2.9 — see [`crate::bsi::AdConverterType`]. `None` when the + /// audio-production block was absent. + pub adconvtyp: Option, + /// A/D converter type for the Ch2 audio production in 1+1 + /// dual-mono (`acmod == 0` AND `audprodi2e == 1`). `None` outside + /// 1+1 mode or when `audprodi2e == 0`. + pub adconvtyp_ch2: Option, + /// §E.2.3.1.x audio production information (`mixlevel` + `roomtyp`) + /// for the main channel, gated by `infomdate == 1` AND + /// `audprodie == 1`. Same semantics as base AC-3 §5.4.2.13-15 — see + /// [`AudioProductionInfo`]. `None` when the informational metadata + /// block was absent or when the encoder did not emit the production + /// chain. + pub audio_production: Option, + /// §E.2.3.1.x Ch2 audio production information for 1+1 dual-mono + /// streams (`acmod == 0` AND `audprodi2e == 1`). `None` outside + /// 1+1 mode or when the Ch2 production chain was absent. + pub audio_production_ch2: Option, + /// §E.2.3.1.62-65 distribution-control hint pair — same + /// `copyrightb` (§5.4.2.24) + `origbs` (§5.4.2.25) semantics as + /// base AC-3, sitting inside the informational-metadata block + /// gated by `infomdate == 1`. `None` when the encoder set + /// `infomdate == 0`. The decoder does not act on either bit; a + /// chain consumer can enforce a distribution / archival policy + /// without re-parsing the BSI. See [`crate::bsi::CopyrightInfo`] + /// for the typed surface. + pub copyright_info: Option, + /// §5.4.2.29-31 additional bit-stream information payload reused + /// verbatim by Annex E (Table E1.2 closes the BSI walk with + /// `addbsie + addbsil + addbsi` exactly as base AC-3 does at + /// §5.3.2). `Some` when `addbsie == 1`; `None` when the encoder + /// did not emit the trailer. Same semantics as + /// [`crate::bsi::Bsi::addbsi`] — see [`AdditionalBitStreamInfo`]. + pub addbsi: Option, + /// Frame size in bytes — `(frmsiz + 1) * 2`. Cached so the + /// dispatcher can range-check the packet without re-doing + /// arithmetic. + pub frame_bytes: u32, + /// Total number of bits the parser consumed out of the input + /// slice. Callers seek the audfrm parser to exactly this offset. + pub bits_consumed: u64, +} + +impl Bsi { + /// Typed view over [`Bsi::dialnorm`] per §5.4.2.8 (reused by + /// Annex E). Identical surface to + /// [`crate::bsi::Bsi::dialogue_normalization`] — see that + /// accessor for the §7.6 reproduction-gain derivation. + pub fn dialogue_normalization(&self) -> DialNorm { + DialNorm::from_wire(self.dialnorm) + } + + /// Typed view over [`Bsi::dialnorm_ch2`] per §5.4.2.16 — the + /// Ch2 mirror in Annex E 1+1 dual-mono streams. `None` outside + /// `acmod == 0`. + pub fn dialogue_normalization_ch2(&self) -> Option { + self.dialnorm_ch2.map(DialNorm::from_wire) + } + + /// Typed view over [`Bsi::dmixmod_preference`] — the Annex E + /// mixmdata preferred stereo downmix mode (Table E1.2 §E.1.2.2 + /// reusing Annex D §2.3.1.2 / Table D2.2). `Some` only when + /// `mixmdate == 1` AND `acmod > 2`; `None` otherwise. Identical + /// surface to [`crate::bsi::Bsi::stereo_downmix_preference`] so + /// a §3.1.1 auto-mode two-channel-downmix router can be shared + /// between the base AC-3 and Annex E paths. + pub fn stereo_downmix_preference(&self) -> Option { + self.dmixmod_preference + } + + /// Typed view over [`Bsi::dolby_surround_mode`] — the §E.2.3.1.x + /// Dolby Surround mode reused verbatim from base AC-3 Table 5.11. + /// `Some` only when `infomdate == 1` AND `acmod == 2` (2/0 stereo + /// — the only channel layout that carries the codeword on the + /// wire); `None` otherwise. Identical surface to + /// [`crate::bsi::Bsi::dolby_surround_mode`] so a chain consumer + /// can route both base and Annex E syntaxes through one branch. + pub fn dolby_surround_mode(&self) -> Option { + self.dolby_surround_mode + } +} + +/// Parse the E-AC-3 BSI starting at byte 0 of `data` (the byte *just +/// after* the 16-bit syncword — i.e. the third byte of the syncframe). +/// +/// Returns `Err(Error::Invalid)` for reserved / illegal field +/// combinations (`fscod2 == 0x3`, `bsid == 9 || bsid == 10` per +/// §E.2.3.1.6, or a malformed `addbsi` length). +pub fn parse(data: &[u8]) -> Result { + let mut br = BitReader::new(data); + parse_with(&mut br) +} + +/// Variant that reads from an externally-managed [`BitReader`] so a +/// caller already positioned past the syncword can share its cursor. +pub fn parse_with(br: &mut BitReader<'_>) -> Result { + let start_bits = br.bit_position(); + + // §E.2.3.1.1 + let strmtyp_raw = br.read_u32(2)? as u8; + let strmtyp = StreamType::from_u8(strmtyp_raw); + if matches!(strmtyp, StreamType::Reserved) { + return Err(Error::invalid("eac3 bsi: strmtyp '11' is reserved")); + } + + // §E.2.3.1.2 + let substreamid = br.read_u32(3)? as u8; + + // §E.2.3.1.3 — 11-bit value, frame_size_in_words = frmsiz + 1. + let frmsiz = br.read_u32(11)? as u16; + let frame_words = (frmsiz as u32) + 1; + let frame_bytes = frame_words * 2; + if !(64..=4096).contains(&frame_bytes) { + // Spec note in §E.2.3.1.3: "values at the lower end of this + // range do not occur as they do not represent enough words to + // convey a complete syncframe". We still accept anything that + // can plausibly fit a syncinfo + bsi + crc2; downstream sanity + // checks reject runts. + if frame_bytes < 8 { + return Err(Error::invalid(format!( + "eac3 bsi: frmsiz {frmsiz} → frame {frame_bytes} bytes is too small" + ))); + } + } + + // §E.2.3.1.4 + let fscod = br.read_u32(2)? as u8; + // §E.2.3.1.5 — fscod2 OR numblkscod + let (fscod2, numblkscod) = if fscod == 0x3 { + let f2 = br.read_u32(2)? as u8; + if f2 == 0x3 { + return Err(Error::invalid( + "eac3 bsi: fscod2 '11' is reserved (Table E2.3)", + )); + } + // numblkscod is implicitly 0x3 (six blocks per syncframe) when + // fscod indicates a reduced-rate stream. + (f2, 0x3u8) + } else { + (0xFFu8, br.read_u32(2)? as u8) + }; + let num_blocks = match numblkscod { + 0 => 1u8, + 1 => 2, + 2 => 3, + _ => 6, + }; + let sample_rate = match (fscod, fscod2) { + (0, _) => 48_000, + (1, _) => 44_100, + (2, _) => 32_000, + (3, 0) => 24_000, + (3, 1) => 22_050, + (3, 2) => 16_000, + _ => unreachable!("fscod/fscod2 combos covered above"), + }; + + // §E.2.3.1.x acmod / lfeon + let acmod = br.read_u32(3)? as u8; + let lfeon = br.read_u32(1)? != 0; + let nfchans = acmod_nfchans(acmod); + let nchans = nfchans + u8::from(lfeon); + + // §E.2.3.1.6 + let bsid = br.read_u32(5)? as u8; + if bsid == 9 || bsid == 10 || bsid > 16 { + return Err(Error::Unsupported(format!( + "eac3 bsi: bsid {bsid} is reserved/illegal per §E.2.3.1.6" + ))); + } + if bsid <= BSID_BASE_AC3_MAX { + // Caller should have routed this packet to the base AC-3 + // parser (which itself handles bsid ≤ 8 + the tolerated 9..=10 + // safety margin we permit elsewhere). Surfacing a clear error + // protects against double-dispatch bugs in the decoder loop. + return Err(Error::Unsupported(format!( + "eac3 bsi: bsid {bsid} routes through the base AC-3 parser, not Annex E" + ))); + } + + // §5.4.2.8 (reused) — dialnorm 0 maps to 31. + let dialnorm_raw = br.read_u32(5)? as u8; + let dialnorm = if dialnorm_raw == 0 { 31 } else { dialnorm_raw }; + + let compre = br.read_u32(1)? != 0; + let compr = if compre { + Some(CompressionGain::from_byte(br.read_u32(8)? as u8)) + } else { + None + }; + + // 1+1 dual-mono (acmod == 0): second copy of dialnorm + compr. + let (dialnorm_ch2, compr_ch2) = if acmod == 0 { + // §5.4.2.16 (reused by Annex E) — dialnorm2 has the same + // meaning as dialnorm; the `0` codepoint is reserved and + // remaps to `31` per §5.4.2.8. + let dialnorm2_raw = br.read_u32(5)? as u8; + let dialnorm2 = if dialnorm2_raw == 0 { + 31 + } else { + dialnorm2_raw + }; + let compr2e = br.read_u32(1)? != 0; + let c2 = if compr2e { + Some(CompressionGain::from_byte(br.read_u32(8)? as u8)) + } else { + None + }; + (Some(dialnorm2), c2) + } else { + (None, None) + }; + + // §E.2.3.1.7-8 — chanmape / chanmap, dependent substream only. + let chanmap = if matches!(strmtyp, StreamType::Dependent) { + let chanmape = br.read_u32(1)? != 0; + if chanmape { + Some(br.read_u32(16)? as u16) + } else { + None + } + } else { + None + }; + + // §E.2.3.1.9-61 — mixing meta-data block. + let mixmdate = br.read_u32(1)? != 0; + let MixingMetadata { + annex_e_mix_levels, + dmixmod, + dmixmod_preference, + lfemixlevcod, + pgmscl, + pgmscl2, + extpgmscl, + paninfo, + paninfo2, + premix_compression, + } = if mixmdate { + parse_mixing_metadata(br, acmod, lfeon, strmtyp, numblkscod)? + } else { + MixingMetadata::ABSENT + }; + + // §E.2.3.1.62 ff — informational meta-data. + let infomdate = br.read_u32(1)? != 0; + let ( + bsmod, + dsurexmod, + dheadphonmod, + dolby_surround_mode, + adconvtyp, + adconvtyp_ch2, + audio_production, + audio_production_ch2, + copyright_info, + ) = if infomdate { + let info = parse_informational_metadata(br, acmod, fscod, strmtyp, numblkscod)?; + ( + Some(info.bsmod), + info.dsurexmod, + info.dheadphonmod, + info.dolby_surround_mode, + info.adconvtyp, + info.adconvtyp_ch2, + info.audio_production, + info.audio_production_ch2, + Some(info.copyright_info), + ) + } else { + (None, None, None, None, None, None, None, None, None) + }; + + // addbsi — §5.4.2.29-31 trailer of 1..=64 encoder-defined bytes + // (Table E1.2 reuses base AC-3's syntax). Per §5.4.2.30 the + // decoder PCM path does not consult the payload, but it is + // surfaced verbatim for chain consumers (encoder-private + // metadata, OAMD packetisation) and the cursor is advanced + // exactly `7 + 8 × (addbsil + 1)` bits. + let addbsie = br.read_u32(1)? != 0; + let addbsi = if addbsie { + let addbsil = br.read_u32(6)? as u8; + let nbytes = addbsil as usize + 1; + let mut payload = Vec::with_capacity(nbytes); + for _ in 0..nbytes { + payload.push(br.read_u32(8)? as u8); + } + AdditionalBitStreamInfo::from_addbsil_and_payload(addbsil, payload) + } else { + None + }; + + let bits_consumed = br.bit_position() - start_bits; + + Ok(Bsi { + strmtyp, + substreamid, + frmsiz, + fscod, + fscod2, + sample_rate, + numblkscod, + num_blocks, + acmod, + nfchans, + lfeon, + nchans, + bsid, + dialnorm, + dialnorm_ch2, + chanmap, + annex_e_mix_levels, + dmixmod, + dmixmod_preference, + lfemixlevcod, + pgmscl, + pgmscl2, + extpgmscl, + paninfo, + paninfo2, + premix_compression, + compr, + compr_ch2, + bsmod, + dsurexmod, + dheadphonmod, + dolby_surround_mode, + adconvtyp, + adconvtyp_ch2, + audio_production, + audio_production_ch2, + copyright_info, + addbsi, + frame_bytes, + bits_consumed, + }) +} + +/// Fields the §E.2.3.1.9-61 mixing-metadata walk surfaces to the +/// public [`Bsi`]. Per the spec's guards (Table E1.2): +/// * `dmixmod` is only present when `acmod > 2` (more than 2 channels). +/// * `ltrtcmixlev` / `lorocmixlev` only when 3 front channels exist +/// (`acmod & 0x1 != 0 && acmod > 2`). +/// * `ltrtsurmixlev` / `lorosurmixlev` only when a surround channel +/// exists (`acmod & 0x4 != 0`). +/// * `lfemixlevcod` only when `lfeon && lfemixlevcode == 1`. +/// * `pgmscl` / `pgmscl2` / `extpgmscl` only on independent +/// substreams (`strmtyp == 0x0`) when the respective exists-flag +/// is set (`pgmscl2` additionally requires `acmod == 0`). +/// * `paninfo` / `paninfo2` only on independent substreams with +/// `acmod < 0x2` (mono or 1+1 dual-mono source) when the +/// respective exists-flag is set (`paninfo2` additionally +/// requires `acmod == 0`). +/// +/// Codewords whose guards fail read back as `0xFF` inside +/// [`AnnexDMixLevels`] so callers can distinguish "spec-absent" from a +/// legitimate `0b000` (1.414×) code. The `annex_e_mix_levels` `Option` +/// itself is `None` only when none of the four center/surround fields +/// were present (mono / 2/0 stereo with no surrounds) — those layouts +/// have no downmix to refine. +struct MixingMetadata { + annex_e_mix_levels: Option, + dmixmod: u8, + dmixmod_preference: Option, + lfemixlevcod: Option, + pgmscl: Option, + pgmscl2: Option, + extpgmscl: Option, + paninfo: Option, + paninfo2: Option, + premix_compression: Option, +} + +impl MixingMetadata { + /// The `mixmdate == 0` state — every surfaced field absent (the + /// program scale factors default to "0 dB, no scaling" per + /// §E.2.3.1.12/.14/.16, and the pan position words default to + /// "center" per §E.2.3.1.53/.56, all represented as `None`). + const ABSENT: Self = Self { + annex_e_mix_levels: None, + dmixmod: 0xFF, + dmixmod_preference: None, + lfemixlevcod: None, + pgmscl: None, + pgmscl2: None, + extpgmscl: None, + paninfo: None, + paninfo2: None, + premix_compression: None, + }; +} + +fn parse_mixing_metadata( + br: &mut BitReader<'_>, + acmod: u8, + lfeon: bool, + strmtyp: StreamType, + numblkscod: u8, +) -> Result { + // §E.2.3.1 mixing metadata — Table E1.2. + // dmixmod (2) when acmod > 0x2 (more than 2 channels). + let (dmixmod, dmixmod_preference) = if acmod > 0x2 { + let raw = br.read_u32(2)? as u8; + (raw, Some(StereoDownmixPreference::from_code(raw))) + } else { + (0xFFu8, None) + }; + // ltrtcmixlev (3) + lorocmixlev (3) when 3 front channels exist. + let (ltrtcmixlev, lorocmixlev) = if (acmod & 0x1) != 0 && acmod > 0x2 { + (br.read_u32(3)? as u8, br.read_u32(3)? as u8) + } else { + (0xFFu8, 0xFFu8) + }; + // ltrtsurmixlev (3) + lorosurmixlev (3) when a surround channel exists. + let (ltrtsurmixlev, lorosurmixlev) = if (acmod & 0x4) != 0 { + (br.read_u32(3)? as u8, br.read_u32(3)? as u8) + } else { + (0xFFu8, 0xFFu8) + }; + let annex_e_mix_levels = if ltrtcmixlev != 0xFF + || lorocmixlev != 0xFF + || ltrtsurmixlev != 0xFF + || lorosurmixlev != 0xFF + { + Some(AnnexDMixLevels { + ltrtcmixlev, + ltrtsurmixlev, + lorocmixlev, + lorosurmixlev, + }) + } else { + None + }; + // lfemixlevcode (1) + lfemixlevcod (5) when LFE on. + let lfemixlevcod = if lfeon { + let lfemixlevcode = br.read_u32(1)? != 0; + if lfemixlevcode { + Some(br.read_u32(5)? as u8) + } else { + None + } + } else { + None + }; + // strmtyp == 0x0 (independent) emits pgmscle/pgmscl + extpgmscle/ + // extpgmscl + mixdef + (mixdef-dependent body). + let mut pgmscl = None; + let mut pgmscl2 = None; + let mut extpgmscl = None; + let mut paninfo = None; + let mut paninfo2 = None; + let mut premix_compression = None; + if matches!(strmtyp, StreamType::Independent) { + // §E.2.3.1.12-13 — program scale factor for this substream's + // own program. Absent ⇒ 0 dB (no scaling). + let pgmscle = br.read_u32(1)? != 0; + if pgmscle { + pgmscl = Some(ProgramScaleFactor::from_code(br.read_u32(6)? as u8)); + } + if acmod == 0 { + // §E.2.3.1.14-15 — same meaning as pgmscl, applied to the + // second channel of a 1+1 dual-mono program. + let pgmscl2e = br.read_u32(1)? != 0; + if pgmscl2e { + pgmscl2 = Some(ProgramScaleFactor::from_code(br.read_u32(6)? as u8)); + } + } + // §E.2.3.1.16-17 — scale factor for an *external* program + // (carried in a different bit stream / independent substream), + // same wire scale as pgmscl. + let extpgmscle = br.read_u32(1)? != 0; + if extpgmscle { + extpgmscl = Some(ProgramScaleFactor::from_code(br.read_u32(6)? as u8)); + } + let mixdef = br.read_u32(2)?; + match mixdef { + 0 => { /* no additional bits */ } + 1 => { + // §E.2.3.1.19-21 — premixcmpsel(1) + drcsrc(1) + + // premixcmpscl(3) = 5 bits ("mixing option 2"). + let premixcmpsel = br.read_u32(1)? != 0; + let drcsrc = br.read_u32(1)? != 0; + let premixcmpscl = br.read_u32(3)? as u8; + premix_compression = Some(PremixCompression::from_fields( + premixcmpsel, + drcsrc, + premixcmpscl, + )); + } + 2 => { + // mixdata = 12 bits (Table E1.2 — "mixing option 3, 12 bits reserved"). + let _ = br.read_u32(12)?; + } + _ => { + // mixdef == 3: variable-length mixing parameter block. + // mixdeflen(5), mixdata2e(1), if mixdata2e {…}, mixdata3e(1), + // if mixdata3e {…}, mixdata field (8*(mixdeflen+2) - num_mixdata_bits), + // mixdatafill (0..7 bits to round to a byte). + let mixdeflen = br.read_u32(5)?; + let mut bits_used: u32 = 5; // mixdeflen itself + let mixdata2e = br.read_u32(1)? != 0; + bits_used += 1; + if mixdata2e { + bits_used += parse_mixdata2_block(br, acmod, lfeon)?; + } + let mixdata3e = br.read_u32(1)? != 0; + bits_used += 1; + if mixdata3e { + bits_used += parse_mixdata3_block(br)?; + } + let mixdata_bits_total = 8 * (mixdeflen + 2); + if bits_used >= mixdata_bits_total { + // Spec note: bits_used must be ≤ mixdata_bits_total. + // If we've already consumed more than the budget, + // the bit stream is malformed — bail. + return Err(Error::invalid(format!( + "eac3 bsi: mixdata overrun (used {bits_used} bits, budget {mixdata_bits_total})" + ))); + } + let pad = mixdata_bits_total - bits_used; + br.skip(pad)?; + // mixdatafill rounds the field to a whole byte. After + // the fixed 8*(mixdeflen+2) bits the field is byte-aligned + // by construction; nothing more to do. + } + } + // §E.2.3.1.53-58 — paninfoe / panmean / paninfo (+ the #2 + // copies in 1+1 dual mono) — only when acmod < 2 (a §E.3.10.8 + // mixer pans a *mono* associated program across the main + // service's channels). Note the Table E1.2 row prints the + // 6-bit reserved field as "paninfoe"; the §E.2.3.1.55 heading + // names it `paninfo`. + if acmod < 0x2 { + let paninfoe = br.read_u32(1)? != 0; + if paninfoe { + let panmean = br.read_u32(8)? as u8; + let reserved = br.read_u32(6)? as u8; + paninfo = Some(PanInfo::from_fields(panmean, reserved)); + } + if acmod == 0 { + let paninfo2e = br.read_u32(1)? != 0; + if paninfo2e { + let panmean2 = br.read_u32(8)? as u8; + let reserved2 = br.read_u32(6)? as u8; + paninfo2 = Some(PanInfo::from_fields(panmean2, reserved2)); + } + } + } + // frmmixcfginfoe — and per-block blkmixcfginfo. + let frmmixcfginfoe = br.read_u32(1)? != 0; + if frmmixcfginfoe { + if numblkscod == 0 { + let _blkmixcfginfo0 = br.read_u32(5)?; + } else { + let nblks = match numblkscod { + 1 => 2u32, + 2 => 3, + _ => 6, + }; + for _ in 0..nblks { + let blkmixcfginfoe = br.read_u32(1)? != 0; + if blkmixcfginfoe { + let _blkmixcfginfo = br.read_u32(5)?; + } + } + } + } + } + Ok(MixingMetadata { + annex_e_mix_levels, + dmixmod, + dmixmod_preference, + lfemixlevcod, + pgmscl, + pgmscl2, + extpgmscl, + paninfo, + paninfo2, + premix_compression, + }) +} + +/// Parses the body of `mixdata2e` (mixing option 4 with extra channel +/// scale factors). Returns the number of bits consumed. +fn parse_mixdata2_block(br: &mut BitReader<'_>, _acmod: u8, _lfeon: bool) -> Result { + let mut bits = 0u32; + // premixcmpsel(1) + drcsrc(1) + premixcmpscl(3) = 5 bits. + let _ = br.read_u32(5)?; + bits += 5; + // For each of L/C/R/Ls/Rs/LFE: presence(1) + (if set) scale(4) = 5 bits. + // Plus dmixscle(1) + (if set) dmixscl(4). + // Plus addche(1) + (if set) extpgmaux1scle(1)+(...4) + extpgmaux2scle(1)+(...4). + for _ in 0..6 { + let p = br.read_u32(1)? != 0; + bits += 1; + if p { + let _ = br.read_u32(4)?; + bits += 4; + } + } + let dmixscle = br.read_u32(1)? != 0; + bits += 1; + if dmixscle { + let _ = br.read_u32(4)?; + bits += 4; + } + let addche = br.read_u32(1)? != 0; + bits += 1; + if addche { + let p1 = br.read_u32(1)? != 0; + bits += 1; + if p1 { + let _ = br.read_u32(4)?; + bits += 4; + } + let p2 = br.read_u32(1)? != 0; + bits += 1; + if p2 { + let _ = br.read_u32(4)?; + bits += 4; + } + } + Ok(bits) +} + +/// Parses the body of `mixdata3e` (speech enhancement processing). +/// Returns the number of bits consumed. +fn parse_mixdata3_block(br: &mut BitReader<'_>) -> Result { + let mut bits = 0u32; + // spchdat(5) + addspchdate(1) + (if set) spchdat1(5) + spchan1att(2) + + // addspchdat1e(1) + (if set) spchdat2(5) + spchan2att(3). + let _ = br.read_u32(5)?; + bits += 5; + let addspchdate = br.read_u32(1)? != 0; + bits += 1; + if addspchdate { + let _ = br.read_u32(5)?; + let _ = br.read_u32(2)?; + bits += 7; + let addspchdat1e = br.read_u32(1)? != 0; + bits += 1; + if addspchdat1e { + let _ = br.read_u32(5)?; + let _ = br.read_u32(3)?; + bits += 8; + } + } + Ok(bits) +} + +/// Decoded informational metadata fields surfaced to the public BSI. +/// Layout mirrors §E.2.3.1.62 ff one-for-one — every field is `None` +/// when the spec's per-acmod / per-audprodie guard kept its codepoint +/// off the wire. +struct InformationalMetadata { + bsmod: u8, + dsurexmod: Option, + dheadphonmod: Option, + dolby_surround_mode: Option, + adconvtyp: Option, + adconvtyp_ch2: Option, + audio_production: Option, + audio_production_ch2: Option, + copyright_info: CopyrightInfo, +} + +/// Walk the §E.2.3.1.62 ff informational metadata block. The body is +/// the same structural shape as base AC-3's `bsmod`/`copyrightb`/ +/// `origbs`/`audprodie` chain plus a few Annex E additions +/// (`sourcefscod`, `convsync`, `blkid`/`frmsizecod` for AC-3-converted +/// streams). +/// +/// Surfaces `dsurexmod` (§E.2.3.1.x, acmod ∈ {6, 7} guard), +/// `dheadphonmod` (acmod == 2 guard), per-channel `adconvtyp` / +/// `adconvtyp_ch2` (inside the `audprodie` / `audprodi2e` chain), and +/// the §5.4.2.13-15 audio-production info (`mixlevel` + `roomtyp`, +/// reused verbatim per §E.2.3.1.x) for the main channel and the Ch2 +/// 1+1 dual-mono mirror. The remaining service-metadata fields +/// (`bsmod` is parsed in the body `Bsi`, source fscod, conv sync, +/// AC-3-convert blkid / frmsizecod) are still parsed bit-accurately +/// and discarded — they do not drive playback policy. +fn parse_informational_metadata( + br: &mut BitReader<'_>, + acmod: u8, + fscod: u8, + strmtyp: StreamType, + numblkscod: u8, +) -> Result { + let bsmod = br.read_u32(3)? as u8; + let copyrightb = br.read_u32(1)? != 0; + let origbs = br.read_u32(1)? != 0; + let copyright_info = CopyrightInfo::from_bits(copyrightb, origbs); + let (dolby_surround_mode, dheadphonmod) = if acmod == 0x2 { + let dsm_raw = br.read_u32(2)? as u8; + let dhpm_raw = br.read_u32(2)? as u8; + ( + Some(DolbySurroundMode::from_code(dsm_raw)), + Some(DolbyHeadphoneMode::from_code(dhpm_raw)), + ) + } else { + (None, None) + }; + let dsurexmod = if acmod >= 0x6 { + let dsex_raw = br.read_u32(2)? as u8; + Some(DolbySurroundExMode::from_code(dsex_raw)) + } else { + None + }; + let audprodie = br.read_u32(1)? != 0; + let (audio_production, adconvtyp) = if audprodie { + let mixlevel = br.read_u32(5)? as u8; + let roomtyp_raw = br.read_u32(2)? as u8; + let adcv_raw = br.read_u32(1)? as u8; + ( + Some(AudioProductionInfo { + mixlevel, + roomtyp: RoomType::from_code(roomtyp_raw), + }), + Some(AdConverterType::from_code(adcv_raw)), + ) + } else { + (None, None) + }; + let (audio_production_ch2, adconvtyp_ch2) = if acmod == 0 { + let audprodi2e = br.read_u32(1)? != 0; + if audprodi2e { + let mixlevel2 = br.read_u32(5)? as u8; + let roomtyp2_raw = br.read_u32(2)? as u8; + let adcv2_raw = br.read_u32(1)? as u8; + ( + Some(AudioProductionInfo { + mixlevel: mixlevel2, + roomtyp: RoomType::from_code(roomtyp2_raw), + }), + Some(AdConverterType::from_code(adcv2_raw)), + ) + } else { + (None, None) + } + } else { + (None, None) + }; + if fscod < 0x3 { + let _sourcefscod = br.read_u32(1)?; + } + // convsync is present only for indep substream (strmtyp == 0) when + // numblkscod != 0x3 (i.e. fewer than 6 blocks per syncframe). + if matches!(strmtyp, StreamType::Independent) && numblkscod != 0x3 { + let _convsync = br.read_u32(1)?; + } + // strmtyp == 0x2 → AC-3 wrapped in E-AC-3 syncframe; carries + // `blkid` (only if numblkscod != 0x3) + `frmsizecod` (6 bits). + if matches!(strmtyp, StreamType::Ac3Convert) { + if numblkscod != 0x3 { + let _blkid = br.read_u32(1)?; + } + let _frmsizecod = br.read_u32(6)?; + } + Ok(InformationalMetadata { + bsmod, + dsurexmod, + dheadphonmod, + dolby_surround_mode, + adconvtyp, + adconvtyp_ch2, + audio_production, + audio_production_ch2, + copyright_info, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper — pack a sequence of (n_bits, value) pairs MSB-first + /// into a fresh byte buffer, padded with zeros to the next byte + /// boundary. + fn pack_msb(bits: &[(u32, u32)]) -> (Vec, u64) { + let total: u32 = bits.iter().map(|(n, _)| *n).sum(); + let nbytes = total.div_ceil(8); + let mut out = vec![0u8; nbytes as usize]; + let mut bitpos = 0u32; + for &(n, v) in bits { + for i in (0..n).rev() { + let bit = ((v >> i) & 1) as u8; + let byte = (bitpos / 8) as usize; + let shift = 7 - (bitpos % 8); + out[byte] |= bit << shift; + bitpos += 1; + } + } + (out, total as u64) + } + + /// Independent substream, 2/0 stereo, 48 kHz, 6 blocks, 768 byte + /// frame. dialnorm=27, no compr, no chanmape (indep), no mixmdate, + /// no infomdate, no addbsi. Mirrors the validator-encoded fixture + /// `eac3-stereo-48000-192kbps`. + #[test] + fn parses_192kbps_indep_stereo() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = 0 + (3, 0), // substreamid = 0 + (11, 383), // frmsiz = 383 → 768 bytes + (2, 0), // fscod = 0 → 48 kHz + (2, 3), // numblkscod = 3 → 6 blocks + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid = 16 + (5, 27), // dialnorm = 27 → -27 dB + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.strmtyp, StreamType::Independent); + assert_eq!(bsi.substreamid, 0); + assert_eq!(bsi.frmsiz, 383); + assert_eq!(bsi.frame_bytes, 768); + assert_eq!(bsi.sample_rate, 48_000); + assert_eq!(bsi.num_blocks, 6); + assert_eq!(bsi.acmod, 2); + assert_eq!(bsi.nfchans, 2); + assert_eq!(bsi.nchans, 2); + assert!(!bsi.lfeon); + assert_eq!(bsi.bsid, 16); + assert_eq!(bsi.dialnorm, 27); + assert!(bsi.chanmap.is_none()); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// Reduced-rate (24 kHz) variant — fscod=3, fscod2=0, numblkscod + /// implicit at 6 blocks. + #[test] + fn parses_reduced_rate_24khz() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 100), // frmsiz + (2, 3), // fscod = 0x3 → reduced + (2, 0), // fscod2 = 0 → 24 kHz + (3, 2), // acmod = 2 + (1, 0), // lfeon + (5, 16), // bsid + (5, 31), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.fscod, 3); + assert_eq!(bsi.fscod2, 0); + assert_eq!(bsi.sample_rate, 24_000); + assert_eq!(bsi.num_blocks, 6); // implicit numblkscod=3 + assert_eq!(bsi.numblkscod, 3); + } + + /// 1-block-per-syncframe variant (`eac3-256-coeff-block` fixture + /// shape). + #[test] + fn parses_one_block_per_syncframe() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 50), // frmsiz + (2, 0), // fscod = 48 kHz + (2, 0), // numblkscod = 0 → 1 block + (3, 2), // acmod + (1, 0), // lfeon + (5, 16), // bsid + (5, 31), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.numblkscod, 0); + assert_eq!(bsi.num_blocks, 1); + } + + #[test] + fn rejects_reserved_bsid_9_10() { + for bad in [9u32, 10] { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 100), + (2, 0), + (2, 3), + (3, 2), + (1, 0), + (5, bad), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let (buf, _) = pack_msb(bits); + let r = parse(&buf); + assert!(r.is_err(), "expected reject for bsid={bad}, got {r:?}"); + } + } + + /// Dependent substream with chanmape=1 and chanmap = bit 6 + /// (Lrs/Rrs pair) — matches our 7.1 encoder output's dep payload. + #[test] + fn parses_dependent_substream_chanmap() { + let chanmap_val = 1u32 << (15 - 6); // bit 6 (Table E2.5) + let bits: &[(u32, u32)] = &[ + (2, 1), // strmtyp = dependent + (3, 0), // substreamid = 0 (first dep) + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 (2 channels: Lb, Rb) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // chanmape = 1 + (16, chanmap_val), + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.strmtyp, StreamType::Dependent); + assert_eq!(bsi.chanmap, Some(0x0200)); + } + + #[test] + fn rejects_strmtyp_reserved() { + let bits: &[(u32, u32)] = &[ + (2, 3), // strmtyp = '11' reserved + (3, 0), + (11, 100), + (2, 0), + (2, 3), + (3, 2), + (1, 0), + (5, 16), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let (buf, _) = pack_msb(bits); + assert!(parse(&buf).is_err()); + } + + #[test] + fn rejects_fscod2_reserved() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 100), + (2, 3), // fscod = 0x3 → reduced rate + (2, 3), // fscod2 = 0x3 reserved + (3, 2), + (1, 0), + (5, 16), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let (buf, _) = pack_msb(bits); + assert!(parse(&buf).is_err()); + } + + /// 5.1 indep with `mixmdate == 1` and all four mix-level fields + /// present (acmod=7 → 3 front + surround channels). Verifies the + /// captured codewords match the bit-stream and that `dmixmod` is + /// also surfaced. + #[test] + fn captures_mixmdata_5_1_full_mix_levels() { + // dmixmod=01 (LtRt preferred), ltrtcmixlev=010 (1.000), + // lorocmixlev=100 (0.707), ltrtsurmixlev=011 (0.841), + // lorosurmixlev=101 (0.595), no LFE refinement, indep flag + // off the rest of mixmdata. + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 → 6 blocks + (3, 7), // acmod = 7 (3/2) + (1, 1), // lfeon = 1 + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate + // mixmdata body: + (2, 1), // dmixmod = 01 (LtRt preferred) + (3, 2), // ltrtcmixlev = 010 (1.000) + (3, 4), // lorocmixlev = 100 (0.707) + (3, 3), // ltrtsurmixlev = 011 (0.841) + (3, 5), // lorosurmixlev = 101 (0.595) + (1, 1), // lfemixlevcode = 1 + (5, 15), // lfemixlevcod = 15 + // indep substream extras (strmtyp == 0): + (1, 0), // pgmscle = 0 + (1, 0), // extpgmscle = 0 + (2, 0), // mixdef = 0 (no extra) + (1, 0), // frmmixcfginfoe = 0 + (1, 0), // infomdate = 0 + (1, 0), // addbsie = 0 + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let mix = bsi + .annex_e_mix_levels + .expect("mix levels should be surfaced when mixmdate==1 and acmod=7"); + assert_eq!(mix.ltrtcmixlev, 0b010); + assert_eq!(mix.lorocmixlev, 0b100); + assert_eq!(mix.ltrtsurmixlev, 0b011); + assert_eq!(mix.lorosurmixlev, 0b101); + assert_eq!(bsi.dmixmod, 0b01); + assert_eq!(bsi.lfemixlevcod, Some(15)); + } + + /// 2/0 stereo indep with `mixmdate == 1` — none of the per-channel + /// guards fire (no third front channel, no surround), so the + /// `annex_e_mix_levels` accessor returns `None` even though the + /// `mixmdate` flag was set. `dmixmod` is also absent (guarded by + /// `acmod > 2`). + #[test] + fn mixmdate_on_stereo_yields_no_mix_levels() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body for 2/0 indep: no dmixmod, no ltrt/loro + // codes, no LFE code. Just the indep tail: + (1, 0), // pgmscle = 0 + (1, 0), // extpgmscle = 0 + (2, 0), // mixdef = 0 + (1, 0), // frmmixcfginfoe = 0 + (1, 0), // infomdate = 0 + (1, 0), // addbsie = 0 + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!( + bsi.annex_e_mix_levels.is_none(), + "2/0 stereo should not surface any mix-level codes" + ); + assert_eq!(bsi.dmixmod, 0xFF); + assert_eq!(bsi.lfemixlevcod, None); + } + + /// No-mixmdata baseline — the four fields default to `None` and + /// `dmixmod` / `lfemixlevcod` return the absent sentinels. + #[test] + fn no_mixmdate_yields_none() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 7), // acmod = 7 + (1, 1), // lfeon = 1 + (5, 16), + (5, 27), + (1, 0), // compre + (1, 0), // mixmdate = 0 + (1, 0), // infomdate = 0 + (1, 0), // addbsie = 0 + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.annex_e_mix_levels.is_none()); + assert_eq!(bsi.dmixmod, 0xFF); + assert_eq!(bsi.lfemixlevcod, None); + } + + /// 3/1 indep — surround codes present, center codes present, no + /// dual surround. Verifies the partial-mix-levels case where only + /// the four 3-bit codes are read (no LFE refinement). + #[test] + fn captures_mixmdata_3_1_no_lfe() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 200), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 5), // acmod = 5 (3/1) + (1, 0), // lfeon = 0 + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate + // mixmdata body: + (2, 2), // dmixmod = 10 (LoRo preferred) + (3, 1), // ltrtcmixlev = 001 (1.189) + (3, 2), // lorocmixlev = 010 (1.000) + (3, 6), // ltrtsurmixlev = 110 (0.500) + (3, 7), // lorosurmixlev = 111 (0.000 - silent surrounds) + // no LFE bits (lfeon=0). + // indep extras: + (1, 0), // pgmscle = 0 + (1, 0), // extpgmscle = 0 + (2, 0), // mixdef = 0 + (1, 0), // frmmixcfginfoe = 0 + (1, 0), // infomdate = 0 + (1, 0), // addbsie = 0 + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let mix = bsi.annex_e_mix_levels.unwrap(); + assert_eq!(mix.ltrtcmixlev, 0b001); + assert_eq!(mix.lorocmixlev, 0b010); + assert_eq!(mix.ltrtsurmixlev, 0b110); + assert_eq!(mix.lorosurmixlev, 0b111); + assert_eq!(bsi.dmixmod, 0b10); + assert_eq!(bsi.lfemixlevcod, None); + } + + /// E-AC-3 `compre=1` surfaces a `CompressionGain` byte verbatim + /// — the Annex E syntax reuses the base AC-3 §7.7.2.2 + Table 7.30 + /// semantics unchanged. + #[test] + fn parses_compr_when_compre_set() { + // 2/0 indep stereo with compre=1, compr=0b0100_0001 (X=4, Y=1). + // Linear = 2^5 * (16+1)/32 = 32 * 17/32 = 17.0; dB = 24.61 dB. + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 1), // compre = 1 + (8, 0b0100_0001), + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let cg = bsi.compr.expect("compre=1"); + assert_eq!(cg.raw(), 0b0100_0001); + assert_eq!(cg.x(), 4); + assert_eq!(cg.y(), 1); + assert!((cg.linear() - 17.0).abs() < 1e-5); + // Ch2 word stays None outside acmod==0. + assert!(bsi.compr_ch2.is_none()); + } + + /// `infomdate == 0` keeps the three Annex D playback hints at + /// `None` even though the BSI is otherwise fully formed. Reuses + /// the round-1 192 kbps stereo fixture shape — every existing + /// fixture builder sets `infomdate=0`. + #[test] + fn no_infomdate_yields_no_playback_hints() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate = 0 + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.dsurexmod.is_none()); + assert!(bsi.dheadphonmod.is_none()); + assert!(bsi.adconvtyp.is_none()); + assert!(bsi.adconvtyp_ch2.is_none()); + } + + /// 3/2 indep with `infomdate == 1` and `audprodie == 1` — the + /// `dsurexmod` slot (acmod ≥ 6 gate fires) and the `adconvtyp` + /// slot (inside the audprodie chain) both surface; `dheadphonmod` + /// stays `None` because the acmod == 2 gate doesn't fire. + /// `dsurexmod = 0b10` (Dolby Surround EX / PLIIx), `adconvtyp = 1` + /// (HDCD). + #[test] + fn infomdate_surfaces_dsurexmod_and_adconvtyp_on_3_2() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = indep + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod = 0 (48 kHz) + (2, 3), // numblkscod = 3 (6 blocks → convsync absent) + (3, 7), // acmod = 7 (3/2) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + // informational metadata body: + (3, 0), // bsmod + (1, 0), // copyrightb + (1, 0), // origbs + // acmod != 2 → no dsurmod/dheadphonmod + // acmod >= 6 → dsurexmod present + (2, 0b10), // dsurexmod = Surround EX / PLIIx + (1, 1), // audprodie = 1 + (5, 0b10101), // mixlevel + (2, 0b10), // roomtyp + (1, 1), // adconvtyp = 1 (HDCD) + // acmod != 0 → no audprodi2e block + (1, 0), // sourcefscod (fscod < 3) + // strmtyp == Indep AND numblkscod == 3 → no convsync + // strmtyp != Ac3Convert → no blkid/frmsizecod + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!( + bsi.dsurexmod, + Some(crate::bsi::DolbySurroundExMode::SurroundExOrProLogicIIx) + ); + // acmod != 2 → dheadphonmod gate didn't fire. + assert!(bsi.dheadphonmod.is_none()); + assert_eq!(bsi.adconvtyp, Some(crate::bsi::AdConverterType::Hdcd)); + assert!(bsi.adconvtyp_ch2.is_none()); + // §E.2.3.1.x reuses §5.4.2.13-15 audio production verbatim — + // `audprodie == 1` surfaces (mixlevel=21 → 101 dB SPL, + // roomtyp=SmallFlat) and the Ch2 mirror stays None outside + // 1+1 mode. + let ap = bsi + .audio_production + .expect("audprodie=1 should surface audio_production"); + assert_eq!(ap.mixlevel, 0b10101); + assert_eq!(ap.peak_mix_level_db_spl(), 101); + assert_eq!(ap.roomtyp, crate::bsi::RoomType::SmallFlat); + assert!(bsi.audio_production_ch2.is_none()); + } + + /// 2/0 indep with `infomdate == 1` — the `dheadphonmod` slot + /// (acmod == 2 gate fires) surfaces; `dsurexmod` and `adconvtyp` + /// stay `None` because their respective gates do not fire + /// (acmod < 6, audprodie == 0). + #[test] + fn infomdate_surfaces_dheadphonmod_on_2_0() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + // info body: + (3, 0), // bsmod + (1, 0), // copyrightb + (1, 0), // origbs + (2, 0b10), // dsurmod (table-D2-style, distinct from dsurexmod) + (2, 0b10), // dheadphonmod = Encoded + // acmod < 6 → no dsurexmod + (1, 0), // audprodie = 0 + // acmod != 0 → no audprodi2e + (1, 0), // sourcefscod + // strmtyp == Indep && numblkscod == 3 → no convsync + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.dsurexmod.is_none()); + assert_eq!( + bsi.dheadphonmod, + Some(crate::bsi::DolbyHeadphoneMode::Encoded) + ); + // The 2-bit `dsurmod` slot inside the acmod==2 branch surfaces + // as a typed `dolby_surround_mode` per Table 5.11 (`0b10` = + // Encoded). The fixture sets it alongside `dheadphonmod`. + assert_eq!( + bsi.dolby_surround_mode, + Some(crate::bsi::DolbySurroundMode::Encoded) + ); + assert!(bsi.adconvtyp.is_none()); + assert!(bsi.adconvtyp_ch2.is_none()); + } + + /// 2/0 indep with `infomdate == 1` — walk all four Table 5.11 + /// `dsurmod` codepoints through `parse()` and confirm the typed + /// `dolby_surround_mode` field matches each. `dheadphonmod` + /// surfaces alongside it (same acmod==2 gate fires both reads). + #[test] + fn infomdate_surfaces_dolby_surround_mode_all_codepoints_on_2_0() { + use crate::bsi::DolbySurroundMode::*; + let expected = [NotIndicated, NotEncoded, Encoded, Reserved]; + for code in 0u32..4 { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + // info body: + (3, 0), // bsmod + (1, 0), // copyrightb + (1, 0), // origbs + (2, code), // dsurmod walks 0..=3 + (2, 0), // dheadphonmod = NotIndicated + // acmod < 6 → no dsurexmod + (1, 0), // audprodie = 0 + // acmod != 0 → no audprodi2e + (1, 0), // sourcefscod + // strmtyp == Indep && numblkscod == 3 → no convsync + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.dolby_surround_mode, Some(expected[code as usize])); + assert_eq!(bsi.dolby_surround_mode(), Some(expected[code as usize])); + } + } + + /// `acmod != 2` (here 3/2 with acmod=7) inside the + /// informational-metadata block — the `dsurmod` slot is skipped + /// per Table E1.2, so the typed `dolby_surround_mode` resolves to + /// `None` even with `infomdate == 1`. + #[test] + fn infomdate_skips_dolby_surround_mode_when_acmod_not_2_0() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 7), // acmod = 7 (3/2) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + // info body: + (3, 0), // bsmod + (1, 0), // copyrightb + (1, 0), // origbs + // acmod != 2 → skip dsurmod + dheadphonmod + // acmod >= 6 → consume dsurexmod + (2, 0b00), // dsurexmod = NotIndicated + (1, 0), // audprodie = 0 + (1, 0), // sourcefscod + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.dolby_surround_mode.is_none()); + assert!(bsi.dolby_surround_mode().is_none()); + // dsurexmod gate fires though (acmod >= 6). + assert_eq!( + bsi.dsurexmod, + Some(crate::bsi::DolbySurroundExMode::NotIndicated) + ); + } + + /// `infomdate == 0` baseline — the entire informational metadata + /// block is skipped, so `dolby_surround_mode` is `None` even on a + /// 2/0 stream that would otherwise carry the codeword. + #[test] + fn infomdate_zero_leaves_dolby_surround_mode_none() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate = 0 + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.dolby_surround_mode.is_none()); + } + + /// 1+1 dual-mono indep with `infomdate == 1` and both + /// `audprodie == 1` (Ch1) AND `audprodi2e == 1` (Ch2). Both + /// `adconvtyp` (Ch1, HDCD) and `adconvtyp_ch2` (Ch2, Standard) + /// surface independently. `dsurexmod` / `dheadphonmod` stay `None` + /// because their acmod gates (≥6 and ==2 respectively) do not + /// fire for acmod=0. + #[test] + fn infomdate_surfaces_per_channel_adconvtyp_in_dual_mono() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 0), // acmod = 0 (1+1) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm (Ch1) + (1, 0), // compre (Ch1) = 0 + // 1+1 second-block dialnorm/compr2 + (5, 27), // dialnorm2 + (1, 0), // compr2e + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + // info body: + (3, 0), // bsmod + (1, 0), // copyrightb + (1, 0), // origbs + // acmod != 2 → no dsurmod/dheadphonmod + // acmod < 6 → no dsurexmod + (1, 1), // audprodie = 1 + (5, 0b10000), // mixlevel + (2, 0b00), // roomtyp + (1, 1), // adconvtyp = 1 (Hdcd) + // acmod == 0 → audprodi2e block + (1, 1), // audprodi2e = 1 + (5, 0b00001), // mixlevel2 + (2, 0b11), // roomtyp2 + (1, 0), // adconvtyp2 = 0 (Standard) + (1, 0), // sourcefscod + // strmtyp == Indep && numblkscod == 3 → no convsync + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.acmod, 0); + assert_eq!(bsi.adconvtyp, Some(crate::bsi::AdConverterType::Hdcd)); + assert_eq!( + bsi.adconvtyp_ch2, + Some(crate::bsi::AdConverterType::Standard) + ); + assert!(bsi.dsurexmod.is_none()); + assert!(bsi.dheadphonmod.is_none()); + // §5.4.2.13-15 audio-production block decodes independently + // for Ch1 (mixlevel=16 → 96 dB SPL, roomtyp=NotIndicated) and + // Ch2 (mixlevel=1 → 81 dB SPL, roomtyp=Reserved). The 1+1 + // mirror is the canonical test for the audprodi2e chain. + let ap1 = bsi + .audio_production + .expect("audprodie=1 should surface Ch1 audio_production"); + assert_eq!(ap1.mixlevel, 0b10000); + assert_eq!(ap1.peak_mix_level_db_spl(), 96); + assert_eq!(ap1.roomtyp, crate::bsi::RoomType::NotIndicated); + let ap2 = bsi + .audio_production_ch2 + .expect("audprodi2e=1 should surface Ch2 audio_production"); + assert_eq!(ap2.mixlevel, 0b00001); + assert_eq!(ap2.peak_mix_level_db_spl(), 81); + assert_eq!(ap2.roomtyp, crate::bsi::RoomType::Reserved); + } + + /// `infomdate == 0` short-circuits the whole §E.2.3.1.x + /// informational block: every typed surface stays `None` including + /// the freshly-lifted [`crate::bsi::AudioProductionInfo`] mirror. + /// Matches the round-208 `no_infomdate_yields_no_playback_hints` + /// shape but extended for the round-214 production fields. + #[test] + fn no_infomdate_yields_no_audio_production() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 7), // acmod = 3/2 + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate = 0 + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.audio_production.is_none()); + assert!(bsi.audio_production_ch2.is_none()); + } + + /// `infomdate == 0` → the `copyrightb` / `origbs` pair is + /// definitionally absent from the wire (they live inside the + /// §E.2.3.1.62 informational metadata block, gated on + /// `infomdate == 1`). Surface must stay `None`. + #[test] + fn no_infomdate_yields_no_copyright_info() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = indep + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod = 2/0 + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate = 0 + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.copyright_info.is_none()); + } + + /// `infomdate == 1` on a 3/2 indep frame surfaces the + /// `(copyrightb, origbs)` pair through `copyright_info`. Walk a + /// "protected, original" pattern (1, 1) to confirm both flags + /// land on the typed surface independently. + #[test] + fn infomdate_surfaces_copyright_info_protected_original_on_3_2() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = indep + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod = 48 kHz + (2, 3), // numblkscod = 6 blocks + (3, 7), // acmod = 7 (3/2) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + // info body — 3/2 with copyrightb=1, origbs=1: + (3, 0), // bsmod + (1, 1), // copyrightb + (1, 1), // origbs + // acmod >= 6 → dsurexmod present; acmod != 2 → no dheadphonmod + (2, 0), // dsurexmod + (1, 0), // audprodie = 0 + // acmod != 0 → no audprodi2e + (1, 0), // sourcefscod + // strmtyp == Indep AND numblkscod == 3 → no convsync + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let ci = bsi + .copyright_info + .expect("infomdate=1 should surface copyright_info"); + assert!(ci.is_copyright_protected()); + assert!(ci.is_original_bitstream()); + assert_eq!(ci.copyrightb_bit(), 1); + assert_eq!(ci.origbs_bit(), 1); + } + + /// `infomdate == 1` on a 2/0 indep frame with the "unprotected + /// copy" pattern `(copyrightb=0, origbs=0)`. Distinct from the + /// 3/2 case above (different acmod, different bit layout after + /// `origbs`) so a single shared bit-cursor bug would surface as a + /// disagreement between the two tests. + #[test] + fn infomdate_surfaces_copyright_info_unprotected_copy_on_2_0() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 6 blocks + (3, 2), // acmod = 2/0 + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 1), // infomdate = 1 + (3, 0), // bsmod + (1, 0), // copyrightb + (1, 0), // origbs + // acmod == 2 fires the dheadphonmod gate (consumes dsurmod+dhpm). + (2, 0), // dsurmod + (2, 0), // dheadphonmod + // acmod < 6 → no dsurexmod + (1, 0), // audprodie = 0 + // acmod != 0 → no audprodi2e + (1, 0), // sourcefscod + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let ci = bsi + .copyright_info + .expect("infomdate=1 should surface copyright_info"); + assert!(!ci.is_copyright_protected()); + assert!(!ci.is_original_bitstream()); + } + + // --------------------------------------------------------------- + // §5.4.2.8 / §5.4.2.16 (reused) — Annex E dialogue-normalization + // typed surface. + // --------------------------------------------------------------- + + /// On a stereo (acmod=2) indep substream the typed + /// `dialogue_normalization()` accessor returns a [`DialNorm`] over + /// the post-remap [`Bsi::dialnorm`] field, exposing `db()` and the + /// §7.6 reproduction-gain derivation. `acmod != 0` → no + /// `dialnorm_ch2`. + #[test] + fn parse_surfaces_dialogue_normalization_on_stereo_indep() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 (6 blocks) + (3, 2), // acmod = 2 (2/0 stereo) + (1, 0), // lfeon + (5, 16), // bsid + (5, 20), // dialnorm = -20 dB + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.dialnorm, 20); + assert!(bsi.dialnorm_ch2.is_none()); + let dn = bsi.dialogue_normalization(); + assert_eq!(dn.codepoint(), 20); + assert_eq!(dn.db(), -20); + assert_eq!(dn.level_below_full_scale_db(), 20); + assert!(bsi.dialogue_normalization_ch2().is_none()); + } + + /// 1+1 dual-mono (acmod == 0) Annex E indep substream surfaces a + /// separate `dialnorm_ch2` per §5.4.2.16 ("This 5-bit code has the + /// same meaning as dialnorm, except that it applies to the second + /// audio channel"). The typed accessor mirrors the AC-3 base + /// surface. + #[test] + fn parse_surfaces_dialnorm_ch2_in_dual_mono() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 0), // acmod = 0 (1+1 dual mono) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm (Ch1) = -27 dB + (1, 0), // compre (Ch1) = 0 + (5, 11), // dialnorm2 (Ch2) = -11 dB + (1, 0), // compr2e + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.acmod, 0); + assert_eq!(bsi.dialnorm, 27); + let dn_ch2_raw = bsi + .dialnorm_ch2 + .expect("acmod == 0 should surface dialnorm_ch2"); + assert_eq!(dn_ch2_raw, 11); + let typed = bsi + .dialogue_normalization_ch2() + .expect("dialnorm_ch2 surfaced"); + assert_eq!(typed.codepoint(), 11); + assert_eq!(typed.db(), -11); + // Ch1 surface is independent. + assert_eq!(bsi.dialogue_normalization().db(), -27); + } + + /// Annex E reuses §5.4.2.8 reserved-codepoint semantics for + /// `dialnorm2` per §5.4.2.16 ("This 5-bit code has the same meaning + /// as dialnorm"): wire `0` remaps to `31`. The parser stores the + /// post-remap value on `dialnorm_ch2`. + #[test] + fn parse_remaps_dialnorm2_zero_codepoint_to_31_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 0), // acmod = 0 + (1, 0), + (5, 16), + (5, 27), // dialnorm = -27 dB + (1, 0), // compre + (5, 0), // dialnorm2 = reserved 0 → remaps to 31 + (1, 0), // compr2e + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let dn_ch2_raw = bsi.dialnorm_ch2.expect("acmod == 0"); + assert_eq!(dn_ch2_raw, 31); + let typed = bsi + .dialogue_normalization_ch2() + .expect("dialnorm_ch2 surfaced"); + assert_eq!(typed.codepoint(), 31); + assert_eq!(typed.db(), -31); + } + + // ----------------------------------------------------------------- + // AdditionalBitStreamInfo (Annex E reuse of §5.4.2.29-31) + // ----------------------------------------------------------------- + + /// Encoder-default `addbsie == 0` leaves `addbsi == None` on the + /// Annex E surface — mirrors the base-AC-3 short-circuit. + #[test] + fn no_addbsie_yields_no_addbsi_eac3() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 2), + (1, 0), + (5, 16), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.addbsi.is_none()); + } + + /// Annex E independent substream with `addbsie == 1` and a 1-byte + /// payload (the minimum). Confirms the parser walks the addbsi + /// trailer correctly on E-AC-3 streams. + #[test] + fn parses_addbsi_single_byte_payload_eac3() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod = 2/0 + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 1), // addbsie + (6, 0), // addbsil = 0 → 1 byte + (8, 0x5A), // payload + ]; + let (buf, total) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let info = bsi.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.addbsil(), 0); + assert_eq!(info.len(), 1); + assert_eq!(info.payload(), &[0x5A]); + assert_eq!(bsi.bits_consumed, total); + } + + /// Annex E independent substream with the maximum-length 64-byte + /// addbsi payload — confirms the parser walks all 519 trailer + /// bits without slipping. + #[test] + fn parses_addbsi_max_length_payload_eac3() { + let mut bits: Vec<(u32, u32)> = vec![ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 2), + (1, 0), + (5, 16), + (5, 27), + (1, 0), + (1, 0), + (1, 0), + (1, 1), // addbsie + (6, 63), // addbsil = 63 → 64 bytes + ]; + for k in 0..64u32 { + bits.push((8, (k * 7) ^ 0xA5)); + } + let (buf, total) = pack_msb(&bits); + let bsi = parse(&buf).unwrap(); + let info = bsi.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.addbsil(), 63); + assert_eq!(info.len(), 64); + let expected: Vec = (0u32..64).map(|k| ((k * 7) ^ 0xA5) as u8).collect(); + assert_eq!(info.payload(), expected.as_slice()); + assert_eq!(bsi.bits_consumed, total); + // Annex E wire_bits sanity: the trailer block alone spans + // 1 (addbsie) + 6 (addbsil) + 64 × 8 (payload) = 519 bits. + assert_eq!(info.wire_bits(), 7 + 8 * 64); + } + + /// Dependent-substream Annex E (`strmtyp == Dependent` with + /// `chanmape == 0`) followed by a 4-byte addbsi payload — confirms + /// the addbsi cursor is unaffected by the upstream dependent- + /// substream branch. + #[test] + fn parses_addbsi_on_dependent_substream_eac3() { + let bits: &[(u32, u32)] = &[ + (2, 1), // strmtyp = Dependent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 7), // acmod = 3/2 5.0 (no LFE) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // chanmape (dependent-only flag) + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 1), // addbsie + (6, 3), // addbsil = 3 → 4 bytes + (8, 0xDE), + (8, 0xAD), + (8, 0xBE), + (8, 0xEF), + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.strmtyp, StreamType::Dependent); + let info = bsi.addbsi.expect("addbsie == 1 surfaces a payload"); + assert_eq!(info.payload(), &[0xDE, 0xAD, 0xBE, 0xEF]); + } + + /// Round 243 — Annex E mixmdata (§E.1.2.2 reusing Annex D + /// §2.3.1.2 / Table D2.2) surfaces the typed + /// [`StereoDownmixPreference`] when `mixmdate == 1` AND `acmod > 2`. + /// Cover all four wire codepoints round-tripping through `parse()`. + #[test] + fn parse_surfaces_dmixmod_preference_annex_e_all_codepoints() { + for (code, expected) in [ + (0b00u32, StereoDownmixPreference::NotIndicated), + (0b01u32, StereoDownmixPreference::LtRtPreferred), + (0b10u32, StereoDownmixPreference::LoRoPreferred), + (0b11u32, StereoDownmixPreference::Reserved), + ] { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 → 6 blocks + (3, 7), // acmod = 7 (3/2 — Annex E mixmdata dmixmod slot present) + (1, 1), // lfeon = 1 + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body: + (2, code), // dmixmod codepoint under test + (3, 2), // ltrtcmixlev + (3, 4), // lorocmixlev + (3, 3), // ltrtsurmixlev + (3, 5), // lorosurmixlev + (1, 0), // lfemixlevcode = 0 + // indep substream extras (strmtyp == 0): + (1, 0), // pgmscle + (1, 0), // extpgmscle + (2, 0), // mixdef = 0 + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.dmixmod, code as u8, "raw dmixmod for {expected:?}"); + assert_eq!(bsi.dmixmod_preference, Some(expected)); + assert_eq!(bsi.stereo_downmix_preference(), Some(expected)); + } + } + + /// Annex E 2/0 stereo with `mixmdate == 1` — the §E.1.2.2 guard + /// skips the 2-bit `dmixmod` slot when `acmod <= 2`, so the typed + /// preference is `None` even though the mixing-metadata block + /// was emitted. + #[test] + fn parse_leaves_dmixmod_preference_none_when_acmod_le_2_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 + (3, 2), // acmod = 2 (2/0 — no dmixmod slot per Table E1.2 guard) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body for 2/0 indep: no dmixmod, no ltrt/loro + // codes, no LFE code. Just the indep tail: + (1, 0), // pgmscle + (1, 0), // extpgmscle + (2, 0), // mixdef + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.acmod, 2); + assert_eq!(bsi.dmixmod, 0xFF); + assert!(bsi.dmixmod_preference.is_none()); + assert!(bsi.stereo_downmix_preference().is_none()); + } + + /// Annex E syncframe without a mixing-metadata block + /// (`mixmdate == 0`) — the typed preference is `None` regardless + /// of `acmod`. + #[test] + fn parse_leaves_dmixmod_preference_none_when_mixmdate_clear_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 7), // acmod = 7 (3/2 — slot would be present if mixmdate == 1) + (1, 1), // lfeon + (5, 16), + (5, 27), + (1, 0), // compre + (1, 0), // mixmdate = 0 — entire mixing-metadata block skipped + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.dmixmod_preference.is_none()); + assert_eq!(bsi.dmixmod, 0xFF); + } + + /// Non-1+1 Annex E streams (`acmod != 0`) never carry `dialnorm2` + /// — the `dialnorm_ch2` field stays `None`. Mirrors the AC-3 base + /// short-circuit. + #[test] + fn parse_leaves_dialnorm_ch2_none_outside_dual_mono_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 7), // acmod = 7 (3/2 5.0) + (1, 0), // lfeon + (5, 16), + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.acmod, 7); + assert!(bsi.dialnorm_ch2.is_none()); + assert!(bsi.dialogue_normalization_ch2().is_none()); + } + + /// Round 278 — §E.2.3.1.13 wire scale: the `0` codepoint is mute + /// (no finite dB value, linear gain 0.0). + #[test] + fn program_scale_factor_mute_codepoint() { + let psf = ProgramScaleFactor::from_code(0); + assert!(psf.is_mute()); + assert_eq!(psf.raw(), 0); + assert_eq!(psf.decibels(), None); + assert_eq!(psf.linear(), 0.0); + } + + /// §E.2.3.1.13 — codepoints `1..=63` map to `-50..=+12 dB` in + /// 1 dB steps (`code - 51`; `51` is unity). Check every codepoint + /// plus the spec's stated endpoints and the linear derivations. + #[test] + fn program_scale_factor_db_mapping_all_codepoints() { + for code in 1u8..=63 { + let psf = ProgramScaleFactor::from_code(code); + assert!(!psf.is_mute()); + assert_eq!(psf.raw(), code); + assert_eq!(psf.decibels(), Some(code as i8 - 51), "code {code}"); + } + // Spec endpoints: "the values 1–63 shall be interpreted as a + // scale factor of –50 dB to +12 dB in 1 dB steps". + assert_eq!(ProgramScaleFactor::from_code(1).decibels(), Some(-50)); + assert_eq!(ProgramScaleFactor::from_code(63).decibels(), Some(12)); + // Unity at the 0 dB codepoint. + let unity = ProgramScaleFactor::from_code(51); + assert_eq!(unity.decibels(), Some(0)); + assert!((unity.linear() - 1.0).abs() < 1e-6); + // Linear endpoints: 10^(-50/20) ≈ 0.0031623, 10^(12/20) ≈ 3.9811. + assert!((ProgramScaleFactor::from_code(1).linear() - 0.003_162_3).abs() < 1e-6); + assert!((ProgramScaleFactor::from_code(63).linear() - 3.981_07).abs() < 1e-4); + } + + /// `from_code` masks to the 6-bit wire width so a caller can pass + /// a wider word verbatim. + #[test] + fn program_scale_factor_from_code_masks_upper_bits() { + assert_eq!(ProgramScaleFactor::from_code(0xFF).raw(), 0x3F); + assert!(ProgramScaleFactor::from_code(0x40).is_mute()); + assert_eq!( + ProgramScaleFactor::from_code(0x73), + ProgramScaleFactor::from_code(0x33) + ); + } + + /// Round 278 — an independent 3/2+LFE syncframe with + /// `mixmdate == 1`, `pgmscle == 1` (-3 dB — the §E.3.10.1 worked + /// example) and `extpgmscle == 1` (-10 dB — the §E.3.10.2 worked + /// example) surfaces both typed scale factors; `pgmscl2` stays + /// `None` outside 1+1 dual mono. + #[test] + fn parse_surfaces_pgmscl_and_extpgmscl_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 → 6 blocks + (3, 7), // acmod = 7 (3/2) + (1, 1), // lfeon = 1 + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body (acmod = 7, lfeon = 1, independent): + (2, 0), // dmixmod + (3, 2), // ltrtcmixlev + (3, 4), // lorocmixlev + (3, 3), // ltrtsurmixlev + (3, 5), // lorosurmixlev + (1, 0), // lfemixlevcode = 0 + (1, 1), // pgmscle = 1 + (6, 48), // pgmscl = 48 → -3 dB (§E.3.10.1 example) + (1, 1), // extpgmscle = 1 + (6, 41), // extpgmscl = 41 → -10 dB (§E.3.10.2 example) + (2, 0), // mixdef = 0 + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let pgmscl = bsi.pgmscl.expect("pgmscle == 1 surfaces pgmscl"); + assert_eq!(pgmscl.raw(), 48); + assert_eq!(pgmscl.decibels(), Some(-3)); + assert!(bsi.pgmscl2.is_none(), "no pgmscl2 outside 1+1 dual mono"); + let ext = bsi.extpgmscl.expect("extpgmscle == 1 surfaces extpgmscl"); + assert_eq!(ext.raw(), 41); + assert_eq!(ext.decibels(), Some(-10)); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// 1+1 dual-mono (`acmod == 0`) independent substream — the + /// §E.2.3.1.14-15 `pgmscl2` slot is on the wire and surfaces + /// independently of `pgmscl`; the mute codepoint (`0`) survives + /// the round-trip. + #[test] + fn parse_surfaces_pgmscl2_on_dual_mono_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 0), // acmod = 0 (1+1 dual mono) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (5, 28), // dialnorm2 (acmod == 0) + (1, 0), // compr2e + (1, 1), // mixmdate = 1 + // mixmdata body (acmod = 0 → no dmixmod / mix levels; + // lfeon = 0 → no lfemixlevcode): + (1, 1), // pgmscle = 1 + (6, 51), // pgmscl = 51 → 0 dB unity + (1, 1), // pgmscl2e = 1 + (6, 0), // pgmscl2 = 0 → mute + (1, 0), // extpgmscle = 0 + (2, 0), // mixdef = 0 + (1, 0), // paninfoe (acmod < 2) + (1, 0), // paninfo2e (acmod == 0) + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let pgmscl = bsi.pgmscl.expect("pgmscle == 1"); + assert_eq!(pgmscl.decibels(), Some(0)); + let pgmscl2 = bsi.pgmscl2.expect("pgmscl2e == 1"); + assert!(pgmscl2.is_mute()); + assert!(bsi.extpgmscl.is_none(), "extpgmscle == 0 ⇒ 0 dB default"); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// `mixmdate == 1` with all three exists-flags clear — per + /// §E.2.3.1.12/.14/.16 the scale factors default to "0 dB (no + /// scaling)", represented as `None` on every field. + #[test] + fn parse_leaves_program_scale_factors_none_when_exists_flags_clear() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body for 2/0 indep: + (1, 0), // pgmscle = 0 + (1, 0), // extpgmscle = 0 + (2, 0), // mixdef + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.pgmscl.is_none()); + assert!(bsi.pgmscl2.is_none()); + assert!(bsi.extpgmscl.is_none()); + } + + /// `mixmdate == 0` skips the whole mixing-metadata block — all + /// three program scale factors stay `None` regardless of layout. + #[test] + fn parse_leaves_program_scale_factors_none_when_mixmdate_clear() { + let bits: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 7), // acmod = 7 + (1, 1), // lfeon + (5, 16), + (5, 27), + (1, 0), // compre + (1, 0), // mixmdate = 0 + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert!(bsi.pgmscl.is_none()); + assert!(bsi.pgmscl2.is_none()); + assert!(bsi.extpgmscl.is_none()); + } + + /// Dependent substreams never carry the program-scale-factor + /// chain — Table E1.2 emits `pgmscle` … `extpgmscl` under + /// `strmtyp == 0x0` only. A dependent substream with + /// `mixmdate == 1` still surfaces the mix-level codewords but + /// leaves all three scale factors `None`. + #[test] + fn parse_skips_program_scale_factors_on_dependent_substream() { + let bits: &[(u32, u32)] = &[ + (2, 1), // strmtyp = dependent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 7), // acmod = 7 (3/2) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // chanmape (dependent-only flag) + (1, 1), // mixmdate = 1 + // mixmdata body for dependent 3/2: dmixmod + four mix + // levels only — no indep pgmscl/extpgmscl/mixdef tail. + (2, 1), // dmixmod = LtRt preferred + (3, 2), // ltrtcmixlev + (3, 4), // lorocmixlev + (3, 3), // ltrtsurmixlev + (3, 5), // lorosurmixlev + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + assert_eq!(bsi.strmtyp, StreamType::Dependent); + assert!(bsi.annex_e_mix_levels.is_some()); + assert!(bsi.pgmscl.is_none()); + assert!(bsi.pgmscl2.is_none()); + assert!(bsi.extpgmscl.is_none()); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// Round 281 — §E.2.3.1.54 index scale: 1.5-degree clockwise + /// steps from the center speaker location, indices `0..=239` + /// spanning `0..=358.5` degrees, `240..=255` reserved. + #[test] + fn pan_info_degrees_mapping() { + assert_eq!(PanInfo::from_fields(0, 0).degrees(), Some(0.0)); + assert_eq!(PanInfo::from_fields(1, 0).degrees(), Some(1.5)); + assert_eq!(PanInfo::from_fields(239, 0).degrees(), Some(358.5)); + for idx in 240..=255u16 { + let pi = PanInfo::from_fields(idx as u8, 0); + assert!(pi.is_reserved_index(), "index {idx}"); + assert_eq!(pi.degrees(), None, "index {idx}"); + assert_eq!(pi.stereo_scale_factors(), None, "index {idx}"); + assert_eq!(pi.surround_scale_factors(), None, "index {idx}"); + } + assert!(!PanInfo::from_fields(239, 0).is_reserved_index()); + // §E.2.3.1.53 default: center, index 0. + assert_eq!(PanInfo::CENTER.panmean(), 0); + assert_eq!(PanInfo::CENTER.degrees(), Some(0.0)); + // The 6-bit reserved trailer is masked + preserved verbatim. + assert_eq!(PanInfo::from_fields(10, 0xFF).reserved(), 0x3F); + assert_eq!(PanInfo::from_fields(10, 0x2A).reserved(), 0x2A); + } + + /// Table E3.15 — stereo-output panning scale factors `(AL, AR)` + /// at the range boundaries: center (index 0) is the equal-power + /// `cos/sin(π/4)` split, the `20..=99` range is fully right, the + /// `140..=219` range fully left, and the trig ranges are + /// continuous with their flat neighbours. + #[test] + fn pan_info_stereo_scale_factors_table_e3_15() { + let eps = 1e-6f32; + // Index 0 (center): AL = cos(π/2 · 20/40) = cos(π/4) = AR. + let (al, ar) = PanInfo::from_fields(0, 0).stereo_scale_factors().unwrap(); + assert!((al - std::f32::consts::FRAC_1_SQRT_2).abs() < eps); + assert!((ar - std::f32::consts::FRAC_1_SQRT_2).abs() < eps); + // Flat ranges: 20..=99 fully right, 140..=219 fully left. + for idx in [20u8, 60, 99] { + assert_eq!( + PanInfo::from_fields(idx, 0).stereo_scale_factors(), + Some((0.0, 1.0)), + "index {idx}" + ); + } + for idx in [140u8, 180, 219] { + assert_eq!( + PanInfo::from_fields(idx, 0).stereo_scale_factors(), + Some((1.0, 0.0)), + "index {idx}" + ); + } + // Range starts are continuous with the preceding flat range: + // index 100 → (sin 0, cos 0) = (0, 1); index 220 → (cos 0, + // sin 0) = (1, 0). + let (al, ar) = PanInfo::from_fields(100, 0).stereo_scale_factors().unwrap(); + assert!((al - 0.0).abs() < eps && (ar - 1.0).abs() < eps); + let (al, ar) = PanInfo::from_fields(220, 0).stereo_scale_factors().unwrap(); + assert!((al - 1.0).abs() < eps && (ar - 0.0).abs() < eps); + } + + /// Table E3.15 is power-preserving over every non-reserved index: + /// `AL² + AR² == 1` (each range is a single sin/cos pair or a + /// degenerate 0/1 split). + #[test] + fn pan_info_stereo_scale_factors_power_preserving() { + for idx in 0..=239u8 { + let (al, ar) = PanInfo::from_fields(idx, 0).stereo_scale_factors().unwrap(); + let power = al * al + ar * ar; + assert!((power - 1.0).abs() < 1e-5, "index {idx}: power {power}"); + assert!((0.0..=1.0).contains(&al), "index {idx}: AL {al}"); + assert!((0.0..=1.0).contains(&ar), "index {idx}: AR {ar}"); + } + } + + /// Tables E3.16 + E3.17 — 5.1-output panning at the five range + /// starts, each landing the source fully on a single speaker: + /// index 0 → Center, 20 → Right, 73 → Right Surround, 167 → Left + /// Surround, 220 → Left (order `[AL, AC, AR, ALS, ARS]`). + #[test] + fn pan_info_surround_scale_factors_cardinal_points() { + let eps = 1e-6f32; + for (idx, expect) in [ + (0u8, [0.0f32, 1.0, 0.0, 0.0, 0.0]), + (20, [0.0, 0.0, 1.0, 0.0, 0.0]), + (73, [0.0, 0.0, 0.0, 0.0, 1.0]), + (167, [0.0, 0.0, 0.0, 1.0, 0.0]), + (220, [1.0, 0.0, 0.0, 0.0, 0.0]), + ] { + let got = PanInfo::from_fields(idx, 0) + .surround_scale_factors() + .unwrap(); + for (ch, (g, e)) in got.iter().zip(expect.iter()).enumerate() { + assert!((g - e).abs() < eps, "index {idx} ch {ch}: {g} vs {e}"); + } + } + // Mid-range check: index 10 splits Center/Right at + // cos/sin(π/4) per the Table E3.16 first row. + let got = PanInfo::from_fields(10, 0) + .surround_scale_factors() + .unwrap(); + assert!((got[1] - std::f32::consts::FRAC_1_SQRT_2).abs() < eps); + assert!((got[2] - std::f32::consts::FRAC_1_SQRT_2).abs() < eps); + assert_eq!((got[0], got[3], got[4]), (0.0, 0.0, 0.0)); + } + + /// Tables E3.16 + E3.17 are jointly power-preserving over every + /// non-reserved index — each range pans between exactly two + /// adjacent speakers with a sin/cos pair, so the five squared + /// factors sum to 1. + #[test] + fn pan_info_surround_scale_factors_power_preserving() { + for idx in 0..=239u8 { + let sf = PanInfo::from_fields(idx, 0) + .surround_scale_factors() + .unwrap(); + let power: f32 = sf.iter().map(|s| s * s).sum(); + assert!((power - 1.0).abs() < 1e-5, "index {idx}: power {power}"); + assert!( + sf.iter().all(|s| (0.0..=1.0).contains(s)), + "index {idx}: {sf:?}" + ); + } + } + + /// Round 281 — an independent mono (`acmod == 1`) syncframe with + /// `mixmdate == 1` and `paninfoe == 1` surfaces the typed + /// [`PanInfo`] (index 10 → 15°, reserved trailer preserved); + /// `paninfo2` stays `None` outside 1+1 dual mono. + #[test] + fn parse_surfaces_paninfo_on_mono_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 → 6 blocks + (3, 1), // acmod = 1 (1/0 mono — pan chain present) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body (acmod = 1 indep: no dmixmod, no mix + // levels, no LFE code): + (1, 0), // pgmscle + (1, 0), // extpgmscle + (2, 0), // mixdef = 0 + (1, 1), // paninfoe = 1 (acmod < 2) + (8, 10), // panmean = 10 → 15.0 degrees + (6, 0x2A), // paninfo (reserved trailer) + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let pan = bsi.paninfo.expect("paninfoe == 1 surfaces paninfo"); + assert_eq!(pan.panmean(), 10); + assert_eq!(pan.reserved(), 0x2A); + assert_eq!(pan.degrees(), Some(15.0)); + assert!(bsi.paninfo2.is_none(), "no paninfo2 outside 1+1 dual mono"); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// 1+1 dual-mono (`acmod == 0`) independent substream — the + /// §E.2.3.1.56-58 `paninfo2` slot is on the wire and surfaces + /// independently of `paninfo`; a reserved index (240) survives + /// the round-trip with `degrees() == None`. + #[test] + fn parse_surfaces_paninfo2_on_dual_mono_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 0), // acmod = 0 (1+1 dual mono) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (5, 28), // dialnorm2 (acmod == 0) + (1, 0), // compr2e + (1, 1), // mixmdate = 1 + // mixmdata body (acmod = 0 → no dmixmod / mix levels; + // lfeon = 0 → no lfemixlevcode): + (1, 0), // pgmscle + (1, 0), // pgmscl2e (acmod == 0) + (1, 0), // extpgmscle + (2, 0), // mixdef = 0 + (1, 1), // paninfoe = 1 + (8, 170), // panmean = 170 → fully-left flat range + (6, 0), // paninfo + (1, 1), // paninfo2e = 1 (acmod == 0) + (8, 240), // panmean2 = 240 → reserved index + (6, 0x3F), // paninfo2 + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let pan = bsi.paninfo.expect("paninfoe == 1"); + assert_eq!(pan.panmean(), 170); + assert_eq!(pan.stereo_scale_factors(), Some((1.0, 0.0))); + let pan2 = bsi.paninfo2.expect("paninfo2e == 1"); + assert_eq!(pan2.panmean(), 240); + assert!(pan2.is_reserved_index()); + assert_eq!(pan2.degrees(), None); + assert_eq!(pan2.reserved(), 0x3F); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// The Table E1.2 guards keep the pan chain off the wire: a 2/0 + /// stereo (`acmod == 2`) indep substream with `mixmdate == 1` + /// has no `paninfoe` slot at all, and a mono substream with + /// `paninfoe == 0` defaults to "center" — both read back as + /// `None` per §E.2.3.1.53. + #[test] + fn parse_leaves_paninfo_none_when_guards_fail() { + // acmod = 2 — pan chain skipped entirely. + let stereo: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod = 2 (2/0 — no pan chain per Table E1.2) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + (1, 0), // pgmscle + (1, 0), // extpgmscle + (2, 0), // mixdef + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, _) = pack_msb(stereo); + let bsi = parse(&buf).unwrap(); + assert!(bsi.paninfo.is_none()); + assert!(bsi.paninfo2.is_none()); + + // acmod = 1 with paninfoe = 0 — defaulted to center (None). + let mono: &[(u32, u32)] = &[ + (2, 0), + (3, 0), + (11, 383), + (2, 0), + (2, 3), + (3, 1), // acmod = 1 + (1, 0), + (5, 16), + (5, 27), + (1, 0), // compre + (1, 1), // mixmdate = 1 + (1, 0), // pgmscle + (1, 0), // extpgmscle + (2, 0), // mixdef + (1, 0), // paninfoe = 0 — pan word defaults to center + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(mono); + let bsi = parse(&buf).unwrap(); + assert!(bsi.paninfo.is_none()); + assert!(bsi.paninfo2.is_none()); + assert_eq!(bsi.bits_consumed, total_bits); + } + + // ---- §E.2.3.1.19-21 premix-compression (PremixCompression) ---- + + /// Table E2.7 — every listed `premixcmpscl` code maps to its + /// `n/6` compression gain-reduction ratio, the unlisted `0b110` + /// codepoint surfaces as reserved (`scale_ratio() == None`), and + /// the percentage captions in the spec round to the `n/6` values. + #[test] + fn premixcmpscl_scale_ratio_table_e2_7() { + // (code, expected sixths) + let listed: &[(u8, u8)] = &[ + (0b000, 0), + (0b001, 1), + (0b010, 2), + (0b011, 3), + (0b100, 4), + (0b101, 5), + (0b111, 6), + ]; + for &(code, sixths) in listed { + let pc = PremixCompression::from_fields(false, false, code); + assert!(!pc.is_premixcmpscl_reserved(), "code {code:#05b} is listed"); + let ratio = pc.scale_ratio().expect("listed code has a ratio"); + assert!( + (ratio - sixths as f32 / 6.0).abs() < 1e-6, + "code {code:#05b} → {ratio}, want {}/6", + sixths + ); + } + // 0b110: no Table E2.7 row. + let reserved = PremixCompression::from_fields(false, false, 0b110); + assert!(reserved.is_premixcmpscl_reserved()); + assert!(reserved.scale_ratio().is_none()); + // Spec percentage captions match the n/6 ratios. + let pct = |code: u8| PremixCompression::from_fields(false, false, code).scale_ratio(); + assert!((pct(0b001).unwrap() - 0.167).abs() < 0.001); // 16.7% + assert!((pct(0b010).unwrap() - 0.333).abs() < 0.001); // 33.3% + assert!((pct(0b011).unwrap() - 0.5).abs() < 1e-6); // 50% + assert!((pct(0b100).unwrap() - 0.667).abs() < 0.001); // 66.7% + assert!((pct(0b101).unwrap() - 0.833).abs() < 0.001); // 83.3% + } + + /// `premixcmpsel` / `drcsrc` typed views (§E.2.3.1.19-20) and the + /// recommended-default predicate (§E.2.3.1.21 note). + #[test] + fn premix_compression_typed_views_and_default() { + let default = PremixCompression::from_fields(false, false, 0b000); + assert_eq!(default.compression_word(), PremixCompressionWord::DynRng); + assert_eq!(default.drc_source(), DrcSource::ExternalProgram); + assert!(default.is_recommended_default()); + + let custom = PremixCompression::from_fields(true, true, 0b011); + assert_eq!(custom.compression_word(), PremixCompressionWord::Compr); + assert_eq!(custom.drc_source(), DrcSource::CurrentSubstream); + assert!(!custom.is_recommended_default()); + // Raw round-trip. + assert!(custom.premixcmpsel()); + assert!(custom.drcsrc()); + assert_eq!(custom.premixcmpscl(), 0b011); + // Each non-default field alone breaks the recommended default. + assert!(!PremixCompression::from_fields(true, false, 0).is_recommended_default()); + assert!(!PremixCompression::from_fields(false, true, 0).is_recommended_default()); + assert!(!PremixCompression::from_fields(false, false, 1).is_recommended_default()); + // Upper bits of premixcmpscl are masked to 3 bits. + assert_eq!( + PremixCompression::from_fields(false, false, 0b1111).premixcmpscl(), + 0b111 + ); + } + + /// `mixdef == 0x1` body on an independent 3/2 substream surfaces + /// the typed `premix_compression` field; the parse cursor lands + /// exactly at `audfrm()` (`bits_consumed == total`). + #[test] + fn parse_surfaces_premix_compression_mixdef1_annex_e() { + let bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod = 3 → 6 blocks + (3, 7), // acmod = 7 (3/2) + (1, 1), // lfeon = 1 + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + // mixmdata body (acmod = 7, lfeon = 1, independent): + (2, 0), // dmixmod + (3, 0), // ltrtcmixlev + (3, 0), // lorocmixlev + (3, 0), // ltrtsurmixlev + (3, 0), // lorosurmixlev + (1, 0), // lfemixlevcode = 0 + (1, 0), // pgmscle = 0 + (1, 0), // extpgmscle = 0 + (2, 1), // mixdef = 1 ("mixing option 2") + // §E.2.3.1.19-21 body: + (1, 1), // premixcmpsel = 1 → use compr + (1, 0), // drcsrc = 0 → external program (recommended) + (3, 4), // premixcmpscl = 0b100 → 66.7% + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(bits); + let bsi = parse(&buf).unwrap(); + let pc = bsi + .premix_compression + .expect("mixdef == 0x1 surfaces premix_compression"); + assert!(pc.premixcmpsel()); + assert!(!pc.drcsrc()); + assert_eq!(pc.premixcmpscl(), 0b100); + assert_eq!(pc.compression_word(), PremixCompressionWord::Compr); + assert_eq!(pc.drc_source(), DrcSource::ExternalProgram); + assert!((pc.scale_ratio().unwrap() - 4.0 / 6.0).abs() < 1e-6); + assert!(!pc.is_recommended_default()); + assert_eq!(bsi.bits_consumed, total_bits); + } + + /// A `mixdef ∈ {0, 2}` block leaves `premix_compression` `None` + /// (the three fields are not carried as a standalone group), and a + /// `mixmdate == 0` syncframe also reports `None`. + #[test] + fn parse_leaves_premix_compression_none_for_other_mixdef() { + // mixdef = 0 on a 2/0 independent substream. + let mixdef0: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 1), // mixmdate = 1 + (1, 0), // pgmscle + (1, 0), // extpgmscle + (2, 0), // mixdef = 0 → no premix body + (1, 0), // frmmixcfginfoe + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf, total_bits) = pack_msb(mixdef0); + let bsi = parse(&buf).unwrap(); + assert!(bsi.premix_compression.is_none()); + assert_eq!(bsi.bits_consumed, total_bits); + + // mixmdate = 0 → whole mixing-metadata block skipped. + let no_mix: &[(u32, u32)] = &[ + (2, 0), // strmtyp = independent + (3, 0), // substreamid + (11, 383), // frmsiz + (2, 0), // fscod + (2, 3), // numblkscod + (3, 2), // acmod = 2 + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate = 0 + (1, 0), // infomdate + (1, 0), // addbsie + ]; + let (buf2, total2) = pack_msb(no_mix); + let bsi2 = parse(&buf2).unwrap(); + assert!(bsi2.premix_compression.is_none()); + assert_eq!(bsi2.bits_consumed, total2); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/chanmap.rs b/crates/vendor/oxideav-ac3/src/eac3/chanmap.rs new file mode 100644 index 00000000..84f88089 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/chanmap.rs @@ -0,0 +1,678 @@ +//! Custom Channel Map (Table E2.5 — `chanmap` field) decoder. +//! +//! The 16-bit `chanmap` field is emitted on dependent substreams when +//! `chanmape == 1` (§E.2.3.1.7-8). It assigns each coded channel of +//! the dep substream to a fixed channel location drawn from a 16-slot +//! reference grid (Table E2.5). The MSB of the field is bit 0 +//! ("Left") and the LSB is bit 15 ("LFE"). +//! +//! Per spec (§E.2.3.1.8): +//! +//! > Bit 0, which indicates the presence of the left channel, is +//! > stored in the most significant bit of the chanmap field. For +//! > each channel present in the dependent substream, the +//! > corresponding location bit in the chanmap is set to '1'. The +//! > order of the coded channels in the dependent substream is the +//! > same as the order of the enabled location bits in the chanmap. +//! > […] When the enabled location bit in the chanmap field refers +//! > to a pair of channels, this defines the channel location of two +//! > adjacent channels in the dependent substream. +//! +//! The pair-bits (Table E2.5 entries that expand to **two** adjacent +//! channels) are bits 5, 6, 9, 10, 11, and 13. +//! +//! The spec constraint "the number of channel locations indicated by +//! the chanmap field must equal the total number of coded channels +//! present in the dependent substream, as indicated by the acmod and +//! lfeon bit stream parameters" is enforced by +//! [`expand_chanmap_locations`] returning [`ChanmapError::CountMismatch`] +//! when the expanded count does not match `dep_nchans`. +//! +//! This module is consumed by [`crate::eac3::decoder`] when splicing a +//! dependent substream's PCM into the independent substream's program. + +/// One physical channel-location slot per Table E2.5. +/// +/// The numeric value is the bit index in the 16-bit `chanmap` field +/// (NOT the bit weight); pair-bits expand to two distinct enum +/// variants in the order specified by the spec text ("first coded +/// channel is the Left Surround channel, the second coded channel +/// is the Right Surround channel"). For pair bit 6 ("Lrs/Rrs pair") +/// this yields [`ChannelLocation::LeftRearSurround`] then +/// [`ChannelLocation::RightRearSurround`]; for bit 9 ("Lsd/Rsd +/// pair") this yields [`ChannelLocation::LeftSurroundDirect`] then +/// [`ChannelLocation::RightSurroundDirect`]; etc. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ChannelLocation { + /// Bit 0 — Left. + Left, + /// Bit 1 — Center. + Center, + /// Bit 2 — Right. + Right, + /// Bit 3 — Left Surround. + LeftSurround, + /// Bit 4 — Right Surround. + RightSurround, + /// Bit 5 (pair) — left half of Lc/Rc. + LeftCenter, + /// Bit 5 (pair) — right half of Lc/Rc. + RightCenter, + /// Bit 6 (pair) — left half of Lrs/Rrs (left rear surround). + LeftRearSurround, + /// Bit 6 (pair) — right half of Lrs/Rrs (right rear surround). + RightRearSurround, + /// Bit 7 — Cs (center surround). + CenterSurround, + /// Bit 8 — Ts (top surround). + TopSurround, + /// Bit 9 (pair) — left half of Lsd/Rsd (left surround direct). + LeftSurroundDirect, + /// Bit 9 (pair) — right half of Lsd/Rsd (right surround direct). + RightSurroundDirect, + /// Bit 10 (pair) — left half of Lw/Rw. + LeftWide, + /// Bit 10 (pair) — right half of Lw/Rw. + RightWide, + /// Bit 11 (pair) — left half of Vhl/Vhr (vertical-height left). + VerticalHeightLeft, + /// Bit 11 (pair) — right half of Vhl/Vhr. + VerticalHeightRight, + /// Bit 12 — Vhc (vertical-height center). + VerticalHeightCenter, + /// Bit 13 (pair) — left half of Lts/Rts (top surround left). + TopSurroundLeft, + /// Bit 13 (pair) — right half of Lts/Rts. + TopSurroundRight, + /// Bit 14 — LFE2 (second low-frequency effect). + Lfe2, + /// Bit 15 — LFE. + Lfe, +} + +impl ChannelLocation { + /// Every [`ChannelLocation`] variant, in Table E2.5 bit order + /// (bit 0 → bit 15) with each pair-bit expanded to its two halves + /// in the spec's documented left-then-right order. Lets a consumer + /// iterate the full reference grid without re-deriving the variant + /// list (e.g. building a lookup from a physical speaker position + /// back to its [`ChannelLocation`]). + pub const ALL: [ChannelLocation; 22] = [ + ChannelLocation::Left, + ChannelLocation::Center, + ChannelLocation::Right, + ChannelLocation::LeftSurround, + ChannelLocation::RightSurround, + ChannelLocation::LeftCenter, + ChannelLocation::RightCenter, + ChannelLocation::LeftRearSurround, + ChannelLocation::RightRearSurround, + ChannelLocation::CenterSurround, + ChannelLocation::TopSurround, + ChannelLocation::LeftSurroundDirect, + ChannelLocation::RightSurroundDirect, + ChannelLocation::LeftWide, + ChannelLocation::RightWide, + ChannelLocation::VerticalHeightLeft, + ChannelLocation::VerticalHeightRight, + ChannelLocation::VerticalHeightCenter, + ChannelLocation::TopSurroundLeft, + ChannelLocation::TopSurroundRight, + ChannelLocation::Lfe2, + ChannelLocation::Lfe, + ]; + + /// The Table E2.5 location bit (`0..=15`) this variant was decoded + /// from. Each of the six pair-bits (5, 6, 9, 10, 11, 13) maps both + /// of its expanded halves to the same shared bit — e.g. both + /// [`Self::LeftRearSurround`] and [`Self::RightRearSurround`] return + /// `6` (the "Lrs/Rrs pair" row). This is the inverse of the + /// [`expand_chanmap_locations`] decode: a consumer that wants to + /// re-emit a `chanmap` field can OR together + /// `1 << (15 - loc.table_e2_5_bit())` over the location list. + pub fn table_e2_5_bit(self) -> u8 { + match self { + ChannelLocation::Left => 0, + ChannelLocation::Center => 1, + ChannelLocation::Right => 2, + ChannelLocation::LeftSurround => 3, + ChannelLocation::RightSurround => 4, + ChannelLocation::LeftCenter | ChannelLocation::RightCenter => 5, + ChannelLocation::LeftRearSurround | ChannelLocation::RightRearSurround => 6, + ChannelLocation::CenterSurround => 7, + ChannelLocation::TopSurround => 8, + ChannelLocation::LeftSurroundDirect | ChannelLocation::RightSurroundDirect => 9, + ChannelLocation::LeftWide | ChannelLocation::RightWide => 10, + ChannelLocation::VerticalHeightLeft | ChannelLocation::VerticalHeightRight => 11, + ChannelLocation::VerticalHeightCenter => 12, + ChannelLocation::TopSurroundLeft | ChannelLocation::TopSurroundRight => 13, + ChannelLocation::Lfe2 => 14, + ChannelLocation::Lfe => 15, + } + } + + /// The 16-bit `chanmap` field weight for this location's Table E2.5 + /// bit — `1 << (15 - table_e2_5_bit())`. Bit 0 (Left) lives in the + /// MSB per §E.2.3.1.8, so the weight of bit 0 is `0x8000` and the + /// weight of bit 15 (LFE) is `0x0001`. Both halves of a pair-bit + /// share the same weight (the single set bit that expanded to two + /// channels). + pub fn chanmap_weight(self) -> u16 { + 1u16 << (15 - self.table_e2_5_bit()) + } + + /// `true` when this location is one half of a Table E2.5 pair-bit + /// (bits 5, 6, 9, 10, 11, 13 — `Lc/Rc`, `Lrs/Rrs`, `Lsd/Rsd`, + /// `Lw/Rw`, `Vhl/Vhr`, `Lts/Rts`). A single set pair-bit decodes to + /// two adjacent coded channels per §E.2.3.1.8, so a consumer that + /// re-emits a `chanmap` must set the shared bit exactly once for the + /// two halves rather than once each. + pub fn is_pair_half(self) -> bool { + matches!( + self, + ChannelLocation::LeftCenter + | ChannelLocation::RightCenter + | ChannelLocation::LeftRearSurround + | ChannelLocation::RightRearSurround + | ChannelLocation::LeftSurroundDirect + | ChannelLocation::RightSurroundDirect + | ChannelLocation::LeftWide + | ChannelLocation::RightWide + | ChannelLocation::VerticalHeightLeft + | ChannelLocation::VerticalHeightRight + | ChannelLocation::TopSurroundLeft + | ChannelLocation::TopSurroundRight + ) + } + + /// The companion half of a Table E2.5 pair-bit location, or `None` + /// for a single-channel location. For [`Self::LeftRearSurround`] + /// this returns [`Self::RightRearSurround`] and vice-versa — letting + /// a consumer pair up the two adjacent coded channels a single set + /// pair-bit expanded to. + pub fn pair_companion(self) -> Option { + Some(match self { + ChannelLocation::LeftCenter => ChannelLocation::RightCenter, + ChannelLocation::RightCenter => ChannelLocation::LeftCenter, + ChannelLocation::LeftRearSurround => ChannelLocation::RightRearSurround, + ChannelLocation::RightRearSurround => ChannelLocation::LeftRearSurround, + ChannelLocation::LeftSurroundDirect => ChannelLocation::RightSurroundDirect, + ChannelLocation::RightSurroundDirect => ChannelLocation::LeftSurroundDirect, + ChannelLocation::LeftWide => ChannelLocation::RightWide, + ChannelLocation::RightWide => ChannelLocation::LeftWide, + ChannelLocation::VerticalHeightLeft => ChannelLocation::VerticalHeightRight, + ChannelLocation::VerticalHeightRight => ChannelLocation::VerticalHeightLeft, + ChannelLocation::TopSurroundLeft => ChannelLocation::TopSurroundRight, + ChannelLocation::TopSurroundRight => ChannelLocation::TopSurroundLeft, + _ => return None, + }) + } + + /// `true` for the two low-frequency-effects locations — `LFE` + /// (Table E2.5 bit 15) and `LFE2` (bit 14). Lets a §7.8 downmix + /// router or a WAVE-mask reorderer route the band-limited LFE feed + /// to the dedicated `LOW_FREQUENCY` speaker slot without re-walking + /// the location list. + pub fn is_lfe(self) -> bool { + matches!(self, ChannelLocation::Lfe | ChannelLocation::Lfe2) + } + + /// `true` for the height-plane locations — the `Vhl/Vhr` pair + /// (bit 11), `Vhc` (bit 12), and the `Lts/Rts` top-surround pair + /// (bit 13), plus the single `Ts` top-surround (bit 8). These are + /// the Table E2.5 rows that sit above the listener plane per + /// SMPTE 428-3, distinguishing them from the ear-level surround + /// rows for an immersive-capable renderer. + pub fn is_height(self) -> bool { + matches!( + self, + ChannelLocation::TopSurround + | ChannelLocation::VerticalHeightLeft + | ChannelLocation::VerticalHeightRight + | ChannelLocation::VerticalHeightCenter + | ChannelLocation::TopSurroundLeft + | ChannelLocation::TopSurroundRight + ) + } + + /// `true` for the ear-level surround locations — the base `Ls/Rs` + /// pair (bits 3, 4), the `Cs` center-surround (bit 7), the + /// `Lrs/Rrs` rear-surround pair (bit 6), and the `Lsd/Rsd` + /// surround-direct pair (bit 9). Excludes the height-plane surround + /// rows (see [`Self::is_height`]) and the front / wide rows. + pub fn is_surround(self) -> bool { + matches!( + self, + ChannelLocation::LeftSurround + | ChannelLocation::RightSurround + | ChannelLocation::CenterSurround + | ChannelLocation::LeftRearSurround + | ChannelLocation::RightRearSurround + | ChannelLocation::LeftSurroundDirect + | ChannelLocation::RightSurroundDirect + ) + } +} + +/// Errors raised by the chanmap decoder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ChanmapError { + /// The expanded chanmap location count does not match the dep + /// substream's coded channel count. Per §E.2.3.1.8 this is a + /// bit-stream violation. + CountMismatch { + expanded: u8, + dep_nchans: u8, + chanmap: u16, + }, +} + +impl core::fmt::Display for ChanmapError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ChanmapError::CountMismatch { + expanded, + dep_nchans, + chanmap, + } => write!( + f, + "eac3 chanmap: expanded {} locations but dep substream codes {} channels (chanmap=0x{:04X})", + expanded, dep_nchans, chanmap + ), + } + } +} + +impl core::error::Error for ChanmapError {} + +/// Expand the 16-bit `chanmap` field into the ordered list of channel +/// locations carried by the dep substream. +/// +/// `dep_nchans` is the dependent substream's total coded channel +/// count (`acmod_nfchans(acmod) + lfeon as u8`); the function checks +/// the spec invariant that the expanded count equals `dep_nchans`. +/// +/// Iteration order is bit 0 (Left) → bit 15 (LFE), i.e. MSB→LSB of +/// the `chanmap` field. Pair-bits produce two consecutive entries in +/// the order documented on each variant. +pub fn expand_chanmap_locations( + chanmap: u16, + dep_nchans: u8, +) -> Result, ChanmapError> { + let mut out: Vec = Vec::with_capacity(16); + // Iterate bit indices 0..16. Bit 0 sits in the MSB of the 16-bit + // field, so it has weight `1 << 15`. + for bit in 0u8..16 { + let mask = 1u16 << (15 - bit); + if chanmap & mask == 0 { + continue; + } + let push_results: &[ChannelLocation] = match bit { + 0 => &[ChannelLocation::Left], + 1 => &[ChannelLocation::Center], + 2 => &[ChannelLocation::Right], + 3 => &[ChannelLocation::LeftSurround], + 4 => &[ChannelLocation::RightSurround], + 5 => &[ChannelLocation::LeftCenter, ChannelLocation::RightCenter], + 6 => &[ + ChannelLocation::LeftRearSurround, + ChannelLocation::RightRearSurround, + ], + 7 => &[ChannelLocation::CenterSurround], + 8 => &[ChannelLocation::TopSurround], + 9 => &[ + ChannelLocation::LeftSurroundDirect, + ChannelLocation::RightSurroundDirect, + ], + 10 => &[ChannelLocation::LeftWide, ChannelLocation::RightWide], + 11 => &[ + ChannelLocation::VerticalHeightLeft, + ChannelLocation::VerticalHeightRight, + ], + 12 => &[ChannelLocation::VerticalHeightCenter], + 13 => &[ + ChannelLocation::TopSurroundLeft, + ChannelLocation::TopSurroundRight, + ], + 14 => &[ChannelLocation::Lfe2], + 15 => &[ChannelLocation::Lfe], + _ => unreachable!(), + }; + for &loc in push_results { + // At most 16 distinct entries (pair-bits use 2 slots each; + // total expanded count cannot exceed 16 since the spec + // limits dep-substream coded channels to A/52's maximum). + out.push(loc); + } + } + + let expanded = out.len() as u8; + if expanded != dep_nchans { + return Err(ChanmapError::CountMismatch { + expanded, + dep_nchans, + chanmap, + }); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Spec example #1 (§E.2.3.1.8): "if bits 0, 3, and 4 of the + /// chanmap field are set to '1', and the dependent stream is + /// coded with acmod = 3 and lfeon = 0, the first coded channel + /// in the dependent stream is the Left channel, the second + /// coded channel is the Left Surround channel, and the third + /// coded channel is the Right Surround channel." + #[test] + fn spec_example_bits_0_3_4() { + // Bit 0 = MSB = 0x8000; bit 3 = 0x1000; bit 4 = 0x0800. + let chanmap = 0x8000 | 0x1000 | 0x0800; + let dep_nchans = 3; // acmod=3 (3-ch) + lfeon=0 + let locs = expand_chanmap_locations(chanmap, dep_nchans).unwrap(); + assert_eq!(locs.len(), 3); + assert_eq!(locs[0], ChannelLocation::Left); + assert_eq!(locs[1], ChannelLocation::LeftSurround); + assert_eq!(locs[2], ChannelLocation::RightSurround); + } + + /// Spec example #2 (§E.2.3.1.8): "if bits 3, 4 and 6 of the + /// chanmap field are set to '1', and the dependent stream is + /// coded with acmod = 6 and lfeon = '0', the first coded channel + /// in the dependent stream is the Left Surround channel, the + /// second coded channel is the Right Surround channel, and the + /// third and fourth channels are the Left Rear Surround and + /// Right Rear Surround channels." + #[test] + fn spec_example_bits_3_4_6_pair() { + // Bit 3 = 0x1000; bit 4 = 0x0800; bit 6 (pair) = 0x0200. + let chanmap = 0x1000 | 0x0800 | 0x0200; + let dep_nchans = 4; // acmod=6 (4-ch) + lfeon=0 + let locs = expand_chanmap_locations(chanmap, dep_nchans).unwrap(); + assert_eq!(locs.len(), 4); + assert_eq!(locs[0], ChannelLocation::LeftSurround); + assert_eq!(locs[1], ChannelLocation::RightSurround); + assert_eq!(locs[2], ChannelLocation::LeftRearSurround); + assert_eq!(locs[3], ChannelLocation::RightRearSurround); + } + + /// The in-tree E-AC-3 encoder emits 7.1 as indep 5.1 (acmod=7, + /// lfeon=1) plus a dep substream carrying the Lb/Rb pair with + /// chanmap bit 6 ("Lrs/Rrs pair") set, dep acmod=2 (2 coded + /// channels). Decoder must round-trip the pair as the two rear- + /// surround channels. + #[test] + fn encoder_71_lb_rb_pair() { + // Bit 6 weight = 1 << (15 - 6) = 0x0200. + let chanmap = 0x0200; + let dep_nchans = 2; // acmod=2 (2-ch) + lfeon=0 + let locs = expand_chanmap_locations(chanmap, dep_nchans).unwrap(); + assert_eq!(locs.len(), 2); + assert_eq!(locs[0], ChannelLocation::LeftRearSurround); + assert_eq!(locs[1], ChannelLocation::RightRearSurround); + } + + /// Spec invariant: expanded count must equal dep_nchans. A pair + /// bit set with dep_nchans=1 is a bit-stream violation. + #[test] + fn count_mismatch_rejected() { + let chanmap = 0x0200; // bit 6 (pair) — expands to 2 + let dep_nchans = 1; // mismatched + let err = expand_chanmap_locations(chanmap, dep_nchans).unwrap_err(); + assert!(matches!(err, ChanmapError::CountMismatch { .. })); + } + + /// Bit 0 lives at MSB; bit 15 lives at LSB. Sanity-check the + /// extreme bits and confirm the iteration order picks low-index + /// (MSB) bits first. + #[test] + fn msb_lsb_extremes_and_order() { + // Bits 0 (Left, MSB) + 15 (LFE, LSB). + let chanmap = 0x8000 | 0x0001; + let locs = expand_chanmap_locations(chanmap, 2).unwrap(); + assert_eq!(locs.len(), 2); + assert_eq!(locs[0], ChannelLocation::Left); + assert_eq!(locs[1], ChannelLocation::Lfe); + } + + /// Every single-channel bit (i.e. non-pair bits) — exercises + /// all 10 non-pair Table E2.5 rows. + #[test] + fn all_single_bits_decode() { + let mut chanmap = 0u16; + let single_bits = [0, 1, 2, 3, 4, 7, 8, 12, 14, 15]; + for &b in &single_bits { + chanmap |= 1u16 << (15 - b); + } + let locs = expand_chanmap_locations(chanmap, single_bits.len() as u8).unwrap(); + assert_eq!(locs.len(), single_bits.len()); + let expected = [ + ChannelLocation::Left, + ChannelLocation::Center, + ChannelLocation::Right, + ChannelLocation::LeftSurround, + ChannelLocation::RightSurround, + ChannelLocation::CenterSurround, + ChannelLocation::TopSurround, + ChannelLocation::VerticalHeightCenter, + ChannelLocation::Lfe2, + ChannelLocation::Lfe, + ]; + for (i, &want) in expected.iter().enumerate() { + assert_eq!(locs[i], want, "single-bit slot {i}"); + } + } + + /// `ChannelLocation::ALL` lists exactly the 22 distinct variants in + /// Table E2.5 bit order (pair-bits expanded left-then-right), with no + /// duplicates and no omissions. + #[test] + fn all_lists_every_variant_in_table_order() { + // 16 location bits, 6 of which are pairs → 16 + 6 = 22 entries. + assert_eq!(ChannelLocation::ALL.len(), 22); + // The bit indices are non-decreasing across the list (pair halves + // share a bit; everything else strictly increases). + let mut prev = 0u8; + for (i, loc) in ChannelLocation::ALL.iter().enumerate() { + let bit = loc.table_e2_5_bit(); + if i > 0 { + assert!(bit >= prev, "ALL not in bit order at index {i}"); + } + prev = bit; + } + // No duplicate variants. + for (i, a) in ChannelLocation::ALL.iter().enumerate() { + for b in &ChannelLocation::ALL[i + 1..] { + assert_ne!(a, b, "duplicate variant in ALL"); + } + } + } + + /// `table_e2_5_bit` maps each variant to its Table E2.5 row; both + /// halves of a pair-bit share the row's single bit index. + #[test] + fn table_e2_5_bit_maps_each_row() { + assert_eq!(ChannelLocation::Left.table_e2_5_bit(), 0); + assert_eq!(ChannelLocation::Center.table_e2_5_bit(), 1); + assert_eq!(ChannelLocation::Right.table_e2_5_bit(), 2); + assert_eq!(ChannelLocation::LeftSurround.table_e2_5_bit(), 3); + assert_eq!(ChannelLocation::RightSurround.table_e2_5_bit(), 4); + // Pair bit 5 — both halves share bit 5. + assert_eq!(ChannelLocation::LeftCenter.table_e2_5_bit(), 5); + assert_eq!(ChannelLocation::RightCenter.table_e2_5_bit(), 5); + // Pair bit 6. + assert_eq!(ChannelLocation::LeftRearSurround.table_e2_5_bit(), 6); + assert_eq!(ChannelLocation::RightRearSurround.table_e2_5_bit(), 6); + assert_eq!(ChannelLocation::CenterSurround.table_e2_5_bit(), 7); + assert_eq!(ChannelLocation::TopSurround.table_e2_5_bit(), 8); + assert_eq!(ChannelLocation::LeftSurroundDirect.table_e2_5_bit(), 9); + assert_eq!(ChannelLocation::RightSurroundDirect.table_e2_5_bit(), 9); + assert_eq!(ChannelLocation::LeftWide.table_e2_5_bit(), 10); + assert_eq!(ChannelLocation::RightWide.table_e2_5_bit(), 10); + assert_eq!(ChannelLocation::VerticalHeightLeft.table_e2_5_bit(), 11); + assert_eq!(ChannelLocation::VerticalHeightRight.table_e2_5_bit(), 11); + assert_eq!(ChannelLocation::VerticalHeightCenter.table_e2_5_bit(), 12); + assert_eq!(ChannelLocation::TopSurroundLeft.table_e2_5_bit(), 13); + assert_eq!(ChannelLocation::TopSurroundRight.table_e2_5_bit(), 13); + assert_eq!(ChannelLocation::Lfe2.table_e2_5_bit(), 14); + assert_eq!(ChannelLocation::Lfe.table_e2_5_bit(), 15); + } + + /// `chanmap_weight` places bit 0 (Left) in the MSB and bit 15 (LFE) + /// in the LSB per §E.2.3.1.8; pair halves share the single weight. + #[test] + fn chanmap_weight_msb_first() { + assert_eq!(ChannelLocation::Left.chanmap_weight(), 0x8000); + assert_eq!(ChannelLocation::Lfe.chanmap_weight(), 0x0001); + // Bit 6 → weight 1 << (15 - 6) = 0x0200; both halves agree. + assert_eq!(ChannelLocation::LeftRearSurround.chanmap_weight(), 0x0200); + assert_eq!(ChannelLocation::RightRearSurround.chanmap_weight(), 0x0200); + } + + /// A decoded location list re-OR's back into the original `chanmap` + /// field via `chanmap_weight` — pair-bits, set once in the source, + /// must not be double-counted (both halves OR the same weight). + #[test] + fn chanmap_weight_round_trips_decoded_list() { + // Spec example #2: bits 3, 4, 6 set on a 4-channel dep substream. + let chanmap = 0x1000 | 0x0800 | 0x0200; + let locs = expand_chanmap_locations(chanmap, 4).unwrap(); + let reconstructed = locs + .iter() + .fold(0u16, |acc, loc| acc | loc.chanmap_weight()); + assert_eq!(reconstructed, chanmap); + + // A map with two pair-bits + a single bit (bits 0, 6, 9 → + // Left + Lrs/Rrs + Lsd/Rsd = 5 coded channels). + let chanmap = 0x8000 | 0x0200 | 0x0040; + let locs = expand_chanmap_locations(chanmap, 5).unwrap(); + let reconstructed = locs + .iter() + .fold(0u16, |acc, loc| acc | loc.chanmap_weight()); + assert_eq!(reconstructed, chanmap); + } + + /// `is_pair_half` is true exactly for the 12 expanded halves of the + /// 6 Table E2.5 pair-bits, and `pair_companion` returns the other + /// half (and `None` for single-channel locations). + #[test] + fn pair_half_and_companion() { + let pair_halves = [ + (ChannelLocation::LeftCenter, ChannelLocation::RightCenter), + ( + ChannelLocation::LeftRearSurround, + ChannelLocation::RightRearSurround, + ), + ( + ChannelLocation::LeftSurroundDirect, + ChannelLocation::RightSurroundDirect, + ), + (ChannelLocation::LeftWide, ChannelLocation::RightWide), + ( + ChannelLocation::VerticalHeightLeft, + ChannelLocation::VerticalHeightRight, + ), + ( + ChannelLocation::TopSurroundLeft, + ChannelLocation::TopSurroundRight, + ), + ]; + let mut pair_count = 0; + for (l, r) in pair_halves { + assert!(l.is_pair_half()); + assert!(r.is_pair_half()); + assert_eq!(l.pair_companion(), Some(r)); + assert_eq!(r.pair_companion(), Some(l)); + // Companions share the Table E2.5 bit. + assert_eq!(l.table_e2_5_bit(), r.table_e2_5_bit()); + pair_count += 2; + } + assert_eq!(pair_count, 12); + + // Single-channel locations are not pair halves and have no + // companion. + for loc in [ + ChannelLocation::Left, + ChannelLocation::Center, + ChannelLocation::CenterSurround, + ChannelLocation::VerticalHeightCenter, + ChannelLocation::Lfe, + ChannelLocation::Lfe2, + ] { + assert!(!loc.is_pair_half()); + assert_eq!(loc.pair_companion(), None); + } + } + + /// `is_lfe` flags only the two LFE rows (bits 14, 15). + #[test] + fn is_lfe_flags_lfe_rows() { + assert!(ChannelLocation::Lfe.is_lfe()); + assert!(ChannelLocation::Lfe2.is_lfe()); + for loc in ChannelLocation::ALL { + if !matches!(loc, ChannelLocation::Lfe | ChannelLocation::Lfe2) { + assert!(!loc.is_lfe(), "{loc:?} should not be LFE"); + } + } + } + + /// `is_height` flags exactly the SMPTE 428-3 above-plane rows: Ts + /// (bit 8), Vhl/Vhr (bit 11), Vhc (bit 12), Lts/Rts (bit 13). + #[test] + fn is_height_flags_above_plane_rows() { + let height = [ + ChannelLocation::TopSurround, + ChannelLocation::VerticalHeightLeft, + ChannelLocation::VerticalHeightRight, + ChannelLocation::VerticalHeightCenter, + ChannelLocation::TopSurroundLeft, + ChannelLocation::TopSurroundRight, + ]; + for loc in ChannelLocation::ALL { + let want = height.contains(&loc); + assert_eq!(loc.is_height(), want, "{loc:?} height classification"); + } + // Height and LFE are disjoint; height and ear-level surround are + // disjoint. + for loc in ChannelLocation::ALL { + if loc.is_height() { + assert!(!loc.is_lfe()); + assert!(!loc.is_surround()); + } + } + } + + /// `is_surround` flags the ear-level surround rows (Ls/Rs, Cs, + /// Lrs/Rrs, Lsd/Rsd) and excludes the height and front rows. + #[test] + fn is_surround_flags_ear_level_rows() { + let surround = [ + ChannelLocation::LeftSurround, + ChannelLocation::RightSurround, + ChannelLocation::CenterSurround, + ChannelLocation::LeftRearSurround, + ChannelLocation::RightRearSurround, + ChannelLocation::LeftSurroundDirect, + ChannelLocation::RightSurroundDirect, + ]; + for loc in ChannelLocation::ALL { + let want = surround.contains(&loc); + assert_eq!(loc.is_surround(), want, "{loc:?} surround classification"); + } + // Front rows are neither surround nor height nor LFE. + for loc in [ + ChannelLocation::Left, + ChannelLocation::Center, + ChannelLocation::Right, + ChannelLocation::LeftCenter, + ChannelLocation::RightCenter, + ] { + assert!(!loc.is_surround()); + assert!(!loc.is_height()); + assert!(!loc.is_lfe()); + } + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/decoder.rs b/crates/vendor/oxideav-ac3/src/eac3/decoder.rs new file mode 100644 index 00000000..badbca2a --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/decoder.rs @@ -0,0 +1,868 @@ +//! E-AC-3 syncframe decoder — rounds 1 + 2 + 3. +//! +//! Round-1 path: +//! +//! 1. Verify the 16-bit syncword (`0x0B77`). +//! 2. Parse the [`super::bsi`] BSI — channel layout, sample rate, +//! frame size, dialnorm, etc. +//! 3. Parse the [`super::audfrm`] audio-frame element — strategy +//! flags only (no DSP yet). +//! 4. Emit `bsi.num_blocks × 256 × nchans` interleaved S16 zeros. +//! +//! Round-2 path: +//! +//! 5. Hand the BitReader to [`super::dsp::decode_indep_audblks`], +//! which walks per-block side-info, runs §7 bit allocation + +//! mantissa unpack, and applies IMDCT + window + overlap-add via +//! the AC-3 helpers. On any "unsupported feature" error +//! (coupling, SPX, AHT, transient processing, …) the decoder +//! falls back to silent emit so the corpus driver keeps decoding. +//! +//! Round-3 path (this commit): +//! +//! 6. Walk dependent substreams in the same packet, decode each via +//! the round-2 DSP, and **splice** the resulting channels into +//! [`Eac3DecoderState::indep_pcm_f32`] per the §E.3.8.2 channel- +//! and-program-extension rule: each dep coded channel is routed by +//! its Table E2.5 location (or natural `acmod` order when +//! `chanmape == 0`); a location already present in the indep +//! program *replaces* that channel in place, a new location +//! *extends* the output. The replace-vs-extend partition is what +//! keeps a real greater-than-5.1 broadcast stream spatially +//! correct — a dep substream commonly re-codes Center / LFE (or, +//! with a custom chanmap, even L/R) that the indep 5.1 downmix +//! already carries, and a naive blind-append would duplicate and +//! decorrelate those channels. +//! None of the validator-encoded corpus fixtures exercise dep +//! substreams (the corpus omits them per +//! `eac3-5.1-side-768kbps/notes.md`); the in-tree 7.1 encoder +//! (indep 5.1 + dep [Lrs/Rrs] pair) and the +//! `splice_*_replace`/`_extend` unit tests cover the path. + +use oxideav_core::bits::BitReader; +use oxideav_core::{Error, Result}; + +use crate::audblk::{Ac3State, SAMPLES_PER_BLOCK}; + +use super::audfrm::{self, AudFrm}; +use super::bsi::{self, Bsi as Eac3Bsi, StreamType}; +use super::chanmap::{self, ChannelLocation}; +use super::dsp; + +/// E-AC-3 syncword — same value as base AC-3 (§E.2.2.1). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub const SYNCWORD: u16 = 0x0B77; + +/// Per-decoder state that persists across packets. +/// +/// Round 2 adds [`Ac3State`] — the per-channel exponent / bap / +/// overlap-add-delay state shared with the AC-3 decoder. We carry one +/// `Ac3State` per substream id so dependent + independent substreams +/// don't trample each other's delay lines (round 3 wires this). +#[derive(Default, Clone)] +pub struct Eac3DecoderState { + /// Last successfully-decoded indep substream parameters. Used by + /// dependent substreams to know how many channels to extend. + pub last_indep: Option, + /// Per-channel persistent DSP state for the **independent** + /// substream. Carries exponent reuse + 256-sample IMDCT delay line + /// across blocks and frames. + indep_state: Ac3State, + /// Per-channel persistent DSP state for the **dependent** substream. + /// Held separately so a dep-substream block's reuse-exponent path + /// doesn't read indep exponents. + dep_state: Ac3State, + /// Per-frame f32 PCM scratch — the indep substream's PCM in its + /// native layout immediately after [`decode_indep_substream`]; if + /// any dep substreams follow in the same packet, their channels + /// are spliced in by [`decode_dep_substream`], growing this slot + /// to `indep_nchans + dep_nchans`. + indep_pcm_f32: Vec, + /// Current channel count of [`Self::indep_pcm_f32`] — starts at + /// the indep BSI's `nchans` and grows as dep substreams splice + /// their channels in. + indep_nchans: u16, + /// Physical channel locations of the **independent** substream's + /// own channels, in `indep_pcm_f32` slot order (natural + /// `acmod`/`lfeon` order, Table 5.8). Populated by + /// [`decode_indep_substream`]. Used by [`splice_dep_into_indep`] + /// to implement the §E.3.8.2 dependent-substream combination rule: + /// a dep channel whose location already exists in this list + /// *replaces* the indep channel in place rather than being appended. + indep_locations: Vec, + /// Samples-per-frame (per channel) of [`Self::indep_pcm_f32`]. + indep_samples_per_frame: u32, + /// Per-frame error string (last seen). Diagnostic only. + pub last_error: Option, + /// Physical channel locations of dep-substream channels that were + /// spliced into [`Self::indep_pcm_f32`] during the most recent + /// packet, in the order they were appended. Always one entry per + /// dep coded channel, matching the §E.2.3.1.8 chanmap expansion + /// (Table E2.5). Empty when no dep substream contributed, or when + /// the dep substream's `chanmape == 0` (in which case the dep + /// channels are routed by the spec's "channel locations apply in + /// the natural order" default for the dep's `acmod` / `lfeon` — + /// see [`Self::default_dep_locations`]). + pub dep_locations: Vec, +} + +impl Eac3DecoderState { + /// Read-only view of the indep substream's per-frame interleaved + /// f32 PCM (range -1..1). After [`decode_eac3_packet`] this is the + /// indep substream's PCM in `(acmod, lfeon)` bitstream order; if + /// any dep substreams contributed in the same packet, their + /// channels have been combined in per §E.3.8.2 — replacing the + /// matching indep slot when the location is shared, or extending + /// the buffer when it is new (see + /// [`crate::eac3::decoder::splice_dep_into_indep`]). + /// + /// `process_eac3_frame` consumes this directly when running the + /// §7.8 downmix matrix so it can apply per-channel coefficients + /// before quantising to S16LE — matrix×S16 would lose 6+ dB of + /// headroom per cascade step on negatively-signed weights. + pub fn indep_pcm_f32(&self) -> &[f32] { + &self.indep_pcm_f32 + } + + /// Current channel count of [`Self::indep_pcm_f32`]. + pub fn indep_nchans(&self) -> u16 { + self.indep_nchans + } + + /// Propagate §7.7 DRC control settings into both the independent and + /// dependent substream DSP states so the next packet's `dynrng` / + /// `compr` decode (in [`crate::eac3::dsp`]) applies the requested + /// regime. Called by the decoder whenever the caller changes its DRC + /// configuration. + pub fn set_drc(&mut self, drc: crate::drc::DrcSettings) { + self.indep_state.drc = drc; + self.dep_state.drc = drc; + } + + /// Channel-location list assigned to the dep substream when + /// `chanmape == 0` (no custom channel map present), per + /// §E.2.3.1.7: "the channel map for a dependent substream shall + /// be defined by the audio coding mode" (the dep substream's + /// `acmod` + `lfeon`). + /// + /// Maps the dep substream's `acmod` to the natural Table 5.8 + /// channel order (L, C, R, Ls, Rs, …) plus LFE when `lfeon`. + /// `acmod == 0` (1+1 dual mono) is treated as two anonymous + /// channels assigned to `Left` / `Right` slots. + pub fn default_dep_locations(acmod: u8, lfeon: bool) -> Vec { + let mut out: Vec = Vec::with_capacity(6); + match acmod { + 0 => { + // 1+1 dual mono — two independent channels with no + // canonical assignment. Default to L / R. + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Right); + } + 1 => { + // 1/0 mono — Center. + out.push(ChannelLocation::Center); + } + 2 => { + // 2/0 stereo — L, R. + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Right); + } + 3 => { + // 3/0 — L, C, R. + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Center); + out.push(ChannelLocation::Right); + } + 4 => { + // 2/1 — L, R, S (treated as center-surround per + // Table E2.5 bit 7). + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Right); + out.push(ChannelLocation::CenterSurround); + } + 5 => { + // 3/1 — L, C, R, S. + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Center); + out.push(ChannelLocation::Right); + out.push(ChannelLocation::CenterSurround); + } + 6 => { + // 2/2 — L, R, Ls, Rs. + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Right); + out.push(ChannelLocation::LeftSurround); + out.push(ChannelLocation::RightSurround); + } + 7 => { + // 3/2 — L, C, R, Ls, Rs. + out.push(ChannelLocation::Left); + out.push(ChannelLocation::Center); + out.push(ChannelLocation::Right); + out.push(ChannelLocation::LeftSurround); + out.push(ChannelLocation::RightSurround); + } + _ => {} + } + if lfeon { + out.push(ChannelLocation::Lfe); + } + out + } +} + +/// Snapshot of the most recent independent substream's output shape +/// — channels, sample rate, samples per frame. +#[derive(Clone, Debug)] +pub struct IndepProgramShape { + pub nchans: u16, + pub sample_rate: u32, + pub samples_per_frame: u32, +} + +/// Result of decoding one E-AC-3 packet. +#[derive(Clone, Debug)] +pub struct DecodedFrame { + pub sample_rate: u32, + pub channels: u16, + /// PCM samples per channel (= `bsi.num_blocks * 256`). + pub samples: u32, + /// Interleaved S16LE bytes — `samples * channels * 2` total. + pub pcm_s16le: Vec, + /// Independent substream's `acmod` field (Table E1.2 §E.1.2.1). + /// Surfaced so callers that need to reorder bitstream-order + /// multichannel layouts into WAV-mask order can pick the right + /// permutation via [`crate::wave_order`]. For dep-substream-extended + /// programs (e.g. 7.1 emitted as indep 5.1 + dep [Lb,Rb]) this + /// reflects the **indep** acmod only — any dep channels at *new* + /// locations are appended at the end of the PCM buffer per the + /// §E.3.8.2 rule in `splice_dep_into_indep` and are not covered by + /// Table 5.8 (dep channels at a *shared* location replace the + /// matching indep slot in place and do not grow the layout). + pub acmod: u8, + /// Independent substream's `lfeon` flag (1 bit, §E.1.2.1). + pub lfeon: bool, + /// Independent substream's `nfchans` (= `acmod_nfchans(acmod)`). + /// Surfaced so callers building a [`crate::downmix::Downmix`] don't + /// have to re-derive it from `channels - lfeon as u16`. + pub nfchans: u8, + /// Independent substream's Annex E mixmdata refinement + /// (§E.2.3.1.3-6). `Some` only when `mixmdate == 1` AND at least + /// one of the four 3-bit codes' per-channel guards fired + /// (`acmod > 2` for center codes, `acmod & 0x4` for surround + /// codes). When `Some`, the [`crate::downmix::Downmix::from_eac3_bsi`] + /// constructor uses these as overrides for the §7.8.2 fixed-0.707 + /// LtRt defaults and the 0.707 LoRo defaults. + pub annex_e_mix_levels: Option, + /// Physical channel locations for the dep-substream channels that + /// *extended* the indep program (i.e. were appended because their + /// location was new), in append order (§E.3.8.2 / §E.2.3.1.7-8). + /// One entry per extending dep coded channel, derived from + /// `chanmap` (Table E2.5) when `chanmape == 1` or from the dep + /// substream's `acmod`/`lfeon` natural order when `chanmape == 0`. + /// Empty when no dep substream contributed, or when every dep + /// channel *replaced* a shared indep location (§E.3.8.2 replace). + /// + /// The indep program's channels occupy slots `0..indep_nchans` + /// (replacements overwrite the matching slot in place); the + /// extending dep channels occupy the appended slots in this order. + pub dep_locations: Vec, + /// Independent substream's 5-bit `dialnorm` word (§E.2.3.1.x, headroom + /// in dB below digital 100%, `1..=31`). Surfaced so a caller that owns + /// the playback gain can apply §7.6 dialogue normalisation toward a + /// chosen target level. Advisory — the core decode does not scale by + /// it (the spec leaves dialnorm to the reproduction system). + pub dialnorm: u8, +} + +/// Decode one or more concatenated E-AC-3 syncframes contained in a +/// single packet (a "transport syncframe" pair: indep + dep). +/// +/// The packet must begin with a 16-bit `0x0B77` syncword. Subsequent +/// syncframes are located via the BSI's `frame_bytes` field. Returns +/// the **independent** substream's program PCM. Dependent substreams +/// are parsed; round 3 will splice their channels into the indep PCM. +pub fn decode_eac3_packet(state: &mut Eac3DecoderState, data: &[u8]) -> Result { + if data.len() < 4 { + return Err(Error::invalid("eac3: packet too short for syncinfo")); + } + let mut indep_pcm: Option = None; + let mut off = 0usize; + state.last_error = None; + state.dep_locations.clear(); + while off + 4 <= data.len() { + // §E.2.2.1 — syncword. + let sync = u16::from_be_bytes([data[off], data[off + 1]]); + if sync != SYNCWORD { + return Err(Error::invalid(format!( + "eac3: bad syncword 0x{sync:04X} at offset {off} (expected 0x0B77)" + ))); + } + // BSI starts at byte off+2. + let bsi_data = &data[off + 2..]; + let mut br = BitReader::new(bsi_data); + let bsi = bsi::parse_with(&mut br)?; + let frame_bytes = bsi.frame_bytes as usize; + if off + frame_bytes > data.len() { + return Err(Error::invalid(format!( + "eac3: syncframe at offset {off} claims {frame_bytes} bytes but packet has only {}", + data.len() - off + ))); + } + + // Audfrm follows BSI in the same bit cursor. AHT (audfrm + // returning Unsupported) is recoverable: the silent emit path + // handles it by emitting zeros for this frame instead of + // bailing the whole packet. + let audfrm = match audfrm::parse_with(&mut br, &bsi) { + Ok(a) => a, + Err(e) => { + state.last_error = Some(format!("{e}")); + let pcm = build_silent_indep(&bsi)?; + if matches!( + bsi.strmtyp, + StreamType::Independent | StreamType::Ac3Convert + ) { + state.last_indep = Some(IndepProgramShape { + nchans: pcm.channels, + sample_rate: pcm.sample_rate, + samples_per_frame: pcm.samples, + }); + indep_pcm = Some(pcm); + } + off += frame_bytes; + continue; + } + }; + + match bsi.strmtyp { + StreamType::Independent | StreamType::Ac3Convert => { + let pcm = decode_indep_substream(state, &bsi, &audfrm, &mut br)?; + state.last_indep = Some(IndepProgramShape { + nchans: pcm.channels, + sample_rate: pcm.sample_rate, + samples_per_frame: pcm.samples, + }); + indep_pcm = Some(pcm); + } + StreamType::Dependent => { + // Round 3: decode + splice into indep_pcm_f32. On + // failure (Unsupported / parse error) we keep the + // indep PCM as-is so output is still meaningful. + let _ = decode_dep_substream(state, &bsi, &audfrm, &mut br); + } + StreamType::Reserved => { + return Err(Error::invalid("eac3: strmtyp '11' is reserved")); + } + } + + off += frame_bytes; + } + + let mut pcm = indep_pcm.ok_or_else(|| { + Error::invalid( + "eac3: packet contains no independent substream (only dependent or ac3-convert frames)", + ) + })?; + + // Rebuild the final S16 buffer from the (possibly extended) + // [`indep_pcm_f32`] scratch. When no dep substream was seen, this + // is the indep PCM unchanged; when one or more dep substreams + // contributed, the scratch has grown to `indep_nchans + Σ + // dep_nchans` channels. + if state.indep_nchans != pcm.channels { + pcm.channels = state.indep_nchans; + pcm.pcm_s16le = pack_f32_to_s16le(&state.indep_pcm_f32); + } + // Surface the accumulated dep-channel locations so callers know + // the physical assignment of the appended channels without + // re-parsing the chanmap. Empty when no dep substream + // contributed. + pcm.dep_locations.clone_from(&state.dep_locations); + Ok(pcm) +} + +/// Decode one independent substream's audblks. Tries the round-2 DSP +/// path first; on `Error::Unsupported` (or any other DSP error) falls +/// back to silent PCM of the right shape so the corpus driver sees +/// a frame-count match. +fn decode_indep_substream( + state: &mut Eac3DecoderState, + bsi: &Eac3Bsi, + audfrm: &AudFrm, + br: &mut BitReader<'_>, +) -> Result { + let samples = bsi.num_blocks as u32 * SAMPLES_PER_BLOCK as u32; + let nchans = bsi.nchans as usize; + let mut floats = vec![0.0f32; samples as usize * nchans]; + + let dsp_result = + dsp::decode_indep_audblks(bsi, audfrm, br, &mut state.indep_state, &mut floats); + if let Err(e) = &dsp_result { + // Silent fallback. Reset the per-channel exponent reuse state + // so the next frame's reuse-strategy blocks don't pick up + // garbage from a half-decoded prior frame. + state.last_error = Some(format!("{e}")); + state.indep_state = Ac3State::new(); + for v in floats.iter_mut() { + *v = 0.0; + } + } + + // Cache the indep f32 PCM in its native (acmod, lfeon) layout + // so any subsequent dep substream in the same packet can append + // its chanmap-routed channels (round 3). + state.indep_pcm_f32 = floats; + state.indep_nchans = nchans as u16; + state.indep_samples_per_frame = samples; + // Record the indep substream's own per-channel locations (natural + // acmod/lfeon order, Table 5.8) so a following dep substream can + // tell which of its channels *replace* an indep channel (shared + // location, §E.3.8.2) versus *extend* the program (new location). + state.indep_locations = Eac3DecoderState::default_dep_locations(bsi.acmod, bsi.lfeon); + + // Pack indep PCM only. If a dep substream follows, the packet- + // level driver will rebuild the final S16 buffer in + // `decode_eac3_packet` after all substreams are walked. + let pcm_s16le = pack_f32_to_s16le(&state.indep_pcm_f32); + + Ok(DecodedFrame { + sample_rate: bsi.sample_rate, + channels: bsi.nchans as u16, + samples, + pcm_s16le, + acmod: bsi.acmod, + lfeon: bsi.lfeon, + nfchans: bsi.nfchans, + annex_e_mix_levels: bsi.annex_e_mix_levels, + // Indep-substream-only emit: no dep channels yet. The + // packet-level driver overwrites this with the accumulated + // `state.dep_locations` if dep substreams follow. + dep_locations: Vec::new(), + dialnorm: bsi.dialnorm, + }) +} + +/// Decode one dependent substream and splice its `chanmap`-routed +/// channels into the indep substream's PCM scratch +/// [`Eac3DecoderState::indep_pcm_f32`]. +/// +/// Returns `Ok(extended_channel_count)` on success; `Err(...)` if +/// the dep substream uses a feature we can't decode (round-2-style +/// silent-fallback applied to the dep audio, leaving the indep PCM +/// untouched). +fn decode_dep_substream( + state: &mut Eac3DecoderState, + bsi: &Eac3Bsi, + audfrm: &AudFrm, + br: &mut BitReader<'_>, +) -> Result { + let samples = bsi.num_blocks as u32 * SAMPLES_PER_BLOCK as u32; + let dep_nchans = bsi.nchans as usize; + + // Without an indep substream in the same packet there is nothing + // to extend. Per §E.2.3.1.1 a dep substream must follow an + // indep substream; flag it but don't error. + if state.last_indep.is_none() { + return Err(Error::invalid( + "eac3 dep: dependent substream with no preceding independent substream", + )); + } + if state.indep_samples_per_frame != samples { + return Err(Error::invalid(format!( + "eac3 dep: dep substream sample-count {samples} differs from indep {}", + state.indep_samples_per_frame + ))); + } + + // Resolve the dep substream's per-channel location list per + // §E.2.3.1.7-8 BEFORE decoding the DSP. When `chanmape == 1` the + // chanmap field selects locations from Table E2.5 and the + // expanded count MUST equal `dep_nchans` (spec invariant). When + // `chanmape == 0` the locations default to the natural-acmod + // order (e.g. acmod=2 → [Left, Right]). + let dep_locations = match bsi.chanmap { + Some(map) => match chanmap::expand_chanmap_locations(map, dep_nchans as u8) { + Ok(locs) => locs, + Err(e) => { + state.last_error = Some(format!("eac3 dep chanmap: {e}")); + return Err(Error::invalid(format!("eac3 dep chanmap: {e}"))); + } + }, + None => Eac3DecoderState::default_dep_locations(bsi.acmod, bsi.lfeon), + }; + + // Decode the dep substream into its own f32 buffer. The dep + // substream's audblks have the same syntax (Table E1.4 doesn't + // branch on strmtyp). + let mut dep_floats = vec![0.0f32; samples as usize * dep_nchans]; + if let Err(e) = + dsp::decode_indep_audblks(bsi, audfrm, br, &mut state.dep_state, &mut dep_floats) + { + // Silent fallback for the dep substream — leave indep PCM + // untouched so the indep program is still audible. + state.last_error = Some(format!("{e}")); + state.dep_state = Ac3State::new(); + return Err(e); + } + + // Splice channels per §E.3.8.2: dep channels whose location already + // exists in the indep program *replace* the indep channel in place; + // dep channels with a new location *extend* the output. The + // `dep_locations` list (resolved above from chanmap / acmod) gives + // each dep coded channel its physical location. + let extended_locations = + splice_dep_into_indep(state, dep_nchans, bsi.chanmap, &dep_floats, &dep_locations); + + // Record only the *extending* dep-channel locations (the ones that + // grew the output past the indep program) so callers (e.g. a future + // WAV-mask reorderer or the §7.8 downmix when extended to 7.1) can + // find Lb/Rb without re-parsing the chanmap. Replaced channels are + // not appended, so they do not appear here. + state.dep_locations.extend(extended_locations); + + Ok(state.indep_nchans) +} + +/// Pack interleaved f32 PCM (range -1..1) into S16LE bytes. +fn pack_f32_to_s16le(floats: &[f32]) -> Vec { + let mut out = vec![0u8; floats.len() * 2]; + for (i, s) in floats.iter().enumerate() { + let clamped = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + let le = clamped.to_le_bytes(); + out[i * 2] = le[0]; + out[i * 2 + 1] = le[1]; + } + out +} + +/// Splice the dep substream's channels into the indep PCM scratch per +/// the §E.3.8.2 channel-and-program-extension combination rule. +/// +/// A/52:2018 §E.3.8.2 (Decoding a Single Program with Greater than 5.1 +/// Channels): +/// +/// > If channels are present in the dependent substream that +/// > correspond to channels in the associated independent substream, +/// > then the dependent substream data for those channels replaces the +/// > independent substream data for the corresponding channels. All +/// > channels present in the dependent substream that do not correspond +/// > to channels in the independent substream are used to enable output +/// > for speaker configurations with greater than 5.1 channels. +/// +/// The same paragraph also covers `chanmape == 0`, where the dep +/// substream's `acmod`/`lfeon` identify its channels and "the +/// corresponding audio channels in the independent substream are +/// overwritten with the dependent audio channel data". Both cases +/// therefore reduce to: for each dep coded channel, *replace in place* +/// when its [`ChannelLocation`] already exists in the indep program, +/// otherwise *append* it as a new output channel. +/// +/// Replacing rather than blind-appending is what keeps a real +/// greater-than-5.1 broadcast stream spatially correct — a dep +/// substream commonly re-codes the Center / LFE (or, with a custom +/// chanmap, even L/R; Table E2.5 shaded entries) that the indep +/// program already carries. A naive append duplicates and reorders +/// those channels, decorrelating the rendered layout. +/// +/// `dep_locations[ch]` gives the physical location of dep coded +/// channel `ch` (resolved from the chanmap, or the natural acmod +/// order when `chanmape == 0`). Returns the locations of only the +/// channels that *extended* the program (the ones that were appended), +/// in append order, so the caller can record them on +/// [`Eac3DecoderState::dep_locations`]. +fn splice_dep_into_indep( + state: &mut Eac3DecoderState, + dep_nchans: usize, + chanmap: Option, + dep_floats: &[f32], + dep_locations: &[ChannelLocation], +) -> Vec { + let samples = state.indep_samples_per_frame as usize; + let indep_nchans = state.indep_nchans as usize; + + // Partition the dep channels into "replace existing indep slot" + // versus "extend". For replacement, find the indep slot index that + // carries the same location. The indep_locations list is in + // indep_pcm_f32 slot order, so its index *is* the slot index. + // Defensive: if locations weren't recorded (shouldn't happen on a + // real decode), fall back to extend-only so we never lose audio. + let mut replace_slot: Vec> = Vec::with_capacity(dep_nchans); + let mut extend_locs: Vec = Vec::new(); + for loc in dep_locations.iter().copied().take(dep_nchans) { + match state.indep_locations.iter().position(|&l| l == loc) { + Some(slot) => replace_slot.push(Some(slot)), + None => { + replace_slot.push(None); + extend_locs.push(loc); + } + } + } + // Any dep channels beyond the resolved location list (malformed / + // missing locations) are extended with an unknown location to + // preserve their audio rather than drop it. + for _ in dep_locations.len().min(dep_nchans)..dep_nchans { + replace_slot.push(None); + extend_locs.push(ChannelLocation::Left); + } + + let num_extend = extend_locs.len(); + let new_nchans = indep_nchans + num_extend; + + let mut grown = vec![0.0f32; samples * new_nchans]; + for n in 0..samples { + // Start from the indep program (which the in-place replacements + // below overwrite slot-by-slot). + for ch in 0..indep_nchans { + grown[n * new_nchans + ch] = state.indep_pcm_f32[n * indep_nchans + ch]; + } + let mut extend_cursor = 0usize; + for (dch, slot) in replace_slot.iter().copied().enumerate() { + let sample = dep_floats[n * dep_nchans + dch]; + match slot { + // §E.3.8.2 replace: overwrite the matching indep slot. + Some(s) => grown[n * new_nchans + s] = sample, + // §E.3.8.2 extend: append in extend order. + None => { + grown[n * new_nchans + indep_nchans + extend_cursor] = sample; + extend_cursor += 1; + } + } + } + } + state.indep_pcm_f32 = grown; + state.indep_nchans = new_nchans as u16; + // The extended channels grow the location list; replaced channels + // already have their location in indep_locations and keep their slot. + state.indep_locations.extend(extend_locs.iter().copied()); + + // Diagnostic: log the chanmap for any future bug hunts. + if let Some(map) = chanmap { + if std::env::var("EAC3_TRACE_CHANMAP").is_ok() { + eprintln!( + "TRACE-CHANMAP dep substream: chanmap=0x{map:04X} dep_nchans={dep_nchans} \ + replaced={} extended={num_extend} → indep grown to {new_nchans} channels", + dep_nchans - num_extend, + ); + } + } + + extend_locs +} + +/// Build a silent S16 PCM buffer of the right shape for one +/// substream. Used as a fallback when a frame can't be DSP-decoded. +fn build_silent_indep(bsi: &Eac3Bsi) -> Result { + // Annex E does not change the §2.2 transform: each audio block + // produces 256 PCM samples per channel. Total per syncframe = + // num_blocks × 256. + let samples = bsi.num_blocks as u32 * 256; + let nchans = bsi.nchans as usize; + let total_bytes = samples as usize * nchans * 2; + let pcm_s16le = vec![0u8; total_bytes]; + Ok(DecodedFrame { + sample_rate: bsi.sample_rate, + channels: bsi.nchans as u16, + samples, + pcm_s16le, + acmod: bsi.acmod, + lfeon: bsi.lfeon, + nfchans: bsi.nfchans, + annex_e_mix_levels: bsi.annex_e_mix_levels, + // Silent-fallback path emits the indep program only — no + // dep channels were spliced. + dep_locations: Vec::new(), + dialnorm: bsi.dialnorm, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::BitWriter; + + /// Build a minimal valid E-AC-3 syncframe of `frame_bytes` bytes: + /// syncword + the BSI from `bsi_bits` + zero-padding through the + /// payload. Used by parser smoke tests; not bit-stream conformant + /// past the BSI/audfrm boundary. + fn build_syncframe(frame_bytes: usize, bsi_bits: &[(u32, u32)]) -> Vec { + let mut bw = BitWriter::with_capacity(frame_bytes); + bw.write_u32(SYNCWORD as u32, 16); + for &(n, v) in bsi_bits { + bw.write_u32(v, n); + } + let mut buf = bw.into_bytes(); + if buf.len() < frame_bytes { + buf.resize(frame_bytes, 0); + } else { + buf.truncate(frame_bytes); + } + buf + } + + /// Build a stereo 6-block 768-byte indep syncframe + a stripped + /// audfrm with zero strategy flags — enough that the round-1 + /// decoder produces a silent buffer of the right shape. + fn stereo_768_indep() -> Vec { + let bsi_bits: &[(u32, u32)] = &[ + (2, 0), // strmtyp = indep + (3, 0), // substreamid + (11, 383), // frmsiz → 768 bytes + (2, 0), // fscod = 48 kHz + (2, 3), // numblkscod = 3 → 6 blocks + (3, 2), // acmod = 2 (2/0) + (1, 0), // lfeon + (5, 16), // bsid + (5, 27), // dialnorm + (1, 0), // compre + (1, 0), // mixmdate + (1, 0), // infomdate + (1, 0), // addbsie + // ----- audfrm ----- + (1, 1), // expstre + (1, 0), // ahte + (2, 0), // snroffststr + (1, 0), // transproce + (1, 1), // blkswe + (1, 1), // dithflage + (1, 1), // bamode + (1, 0), // frmfgaincode + (1, 1), // dbaflde + (1, 1), // skipflde + (1, 0), // spxattene + // acmod>1 → cplinu[0] (1) + 5 × cplstre (1 each) + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + (1, 0), + // expstre==1 → per-block chexpstr lives in audblk(), NOT + // audfrm. (Round-2 fix: round 1 over-consumed these bits.) + // strmtyp=0 + numblkscod=3 → convexpstre implicit 1 + // → 2 × convexpstr (5 bits) + (5, 0), + (5, 0), + // snroffststr=0 → frmcsnroffst (6) + frmfsnroffst (4) + (6, 15), + (4, 0), + // num_blocks > 1 → blkstrtinfoe (1, 0) + (1, 0), + ]; + build_syncframe(768, bsi_bits) + } + + #[test] + fn decode_silent_indep_smoke() { + let pkt = stereo_768_indep(); + let mut st = Eac3DecoderState::default(); + let frm = decode_eac3_packet(&mut st, &pkt).unwrap(); + assert_eq!(frm.sample_rate, 48_000); + assert_eq!(frm.channels, 2); + assert_eq!(frm.samples, 6 * 256); + assert_eq!(frm.pcm_s16le.len(), (6 * 256) * 2 * 2); + assert!(frm.pcm_s16le.iter().all(|&b| b == 0)); + } + + #[test] + fn rejects_bad_syncword() { + let mut data = stereo_768_indep(); + data[0] = 0xFF; + let mut st = Eac3DecoderState::default(); + assert!(decode_eac3_packet(&mut st, &data).is_err()); + } + + /// Set up a decoder state with a 1-sample, 3-channel indep program + /// at the given locations and the given per-channel constant values. + fn indep_state_3ch(locs: [ChannelLocation; 3], vals: [f32; 3]) -> Eac3DecoderState { + Eac3DecoderState { + indep_samples_per_frame: 1, + indep_nchans: 3, + indep_pcm_f32: vals.to_vec(), + indep_locations: locs.to_vec(), + ..Default::default() + } + } + + /// §E.3.8.2: a dep channel whose location is NOT in the indep + /// program *extends* the output (grows the channel count). The 7.1 + /// case: indep 5.1 (here shrunk to L,C,R for the test) + a dep + /// [Lrs, Rrs] pair → 5 channels, the two new ones appended at the + /// end, and `dep_locations` reports exactly [Lrs, Rrs]. + #[test] + fn splice_extend_appends_new_locations() { + use ChannelLocation::*; + let mut st = indep_state_3ch([Left, Center, Right], [1.0, 2.0, 3.0]); + // Dep substream: 2 coded channels at the rear-surround pair. + // Table E2.5 bit 6 = Lrs/Rrs. + let chanmap = Some(1u16 << (15 - 6)); + let dep_floats = [4.0f32, 5.0f32]; // 1 sample × 2 dep channels + let locs = vec![LeftRearSurround, RightRearSurround]; + let extended = splice_dep_into_indep(&mut st, 2, chanmap, &dep_floats, &locs); + assert_eq!(st.indep_nchans, 5, "indep 3 + 2 new dep = 5 channels"); + // Indep channels untouched; dep pair appended in order. + assert_eq!(st.indep_pcm_f32, vec![1.0, 2.0, 3.0, 4.0, 5.0]); + assert_eq!(extended, vec![LeftRearSurround, RightRearSurround]); + assert_eq!( + st.indep_locations, + vec![Left, Center, Right, LeftRearSurround, RightRearSurround] + ); + } + + /// §E.3.8.2: a dep channel whose location ALREADY exists in the + /// indep program *replaces* the matching indep slot in place — it + /// does NOT grow the channel count, and the original indep sample + /// at that slot is overwritten with the dep data. This is the case + /// the spec spells out explicitly ("the center channel audio data + /// carried in the dependent stream will replace the center channel + /// audio data carried in the independent stream"). A naive append + /// would have produced [L,C,R, C'] (4 channels, duplicated + + /// decorrelated center) — the broadcast-decode defect this fixes. + #[test] + fn splice_replace_overwrites_shared_location_in_place() { + use ChannelLocation::*; + let mut st = indep_state_3ch([Left, Center, Right], [1.0, 2.0, 3.0]); + // Dep substream: a single channel re-coding the Center. + // Table E2.5 bit 1 = Center. + let chanmap = Some(1u16 << (15 - 1)); + let dep_floats = [9.0f32]; // 1 sample × 1 dep channel + let locs = vec![Center]; + let extended = splice_dep_into_indep(&mut st, 1, chanmap, &dep_floats, &locs); + assert_eq!( + st.indep_nchans, 3, + "replace must NOT grow the channel count" + ); + // Center slot (index 1) overwritten; L and R unchanged. + assert_eq!(st.indep_pcm_f32, vec![1.0, 9.0, 3.0]); + assert!( + extended.is_empty(), + "a replaced channel does not extend the program" + ); + assert_eq!(st.indep_locations, vec![Left, Center, Right]); + } + + /// §E.3.8.2 mixed case: a dep substream carrying both a shared + /// location (Center, replaced in place) and a new location (Cs, + /// appended). Verifies the partition is per-channel, not all-or- + /// nothing, and that the appended channel lands after the indep + /// program while the replaced one stays in its original slot. + #[test] + fn splice_mixed_replace_and_extend() { + use ChannelLocation::*; + let mut st = indep_state_3ch([Left, Center, Right], [1.0, 2.0, 3.0]); + // Table E2.5 bit 1 = Center, bit 7 = Cs. + let chanmap = Some((1u16 << (15 - 1)) | (1u16 << (15 - 7))); + let dep_floats = [7.0f32, 8.0f32]; // Center', Cs + let locs = vec![Center, CenterSurround]; + let extended = splice_dep_into_indep(&mut st, 2, chanmap, &dep_floats, &locs); + assert_eq!(st.indep_nchans, 4, "1 replace + 1 extend → 3 + 1 = 4"); + // Center (slot 1) replaced with 7.0; Cs appended at slot 3. + assert_eq!(st.indep_pcm_f32, vec![1.0, 7.0, 3.0, 8.0]); + assert_eq!(extended, vec![CenterSurround]); + assert_eq!( + st.indep_locations, + vec![Left, Center, Right, CenterSurround] + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/dsp.rs b/crates/vendor/oxideav-ac3/src/eac3/dsp.rs new file mode 100644 index 00000000..79c1bc8a --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/dsp.rs @@ -0,0 +1,3109 @@ +//! E-AC-3 audio-block DSP pipeline — rounds 2 / 4-stub / 5 / 6 / 7-SPX. +//! +//! Translates the parsed [`super::bsi::Bsi`] + [`super::audfrm::AudFrm`] +//! into the existing AC-3 [`crate::audblk::Ac3State`] shape so the §7 +//! DSP helpers (`decode_exponents`, `run_bit_allocation`, +//! `unpack_mantissas`, `dsp_block`) can be reused without modification. +//! +//! ## Round 7 (this commit) — Spectral Extension (SPX) decode +//! +//! The audblk parser now decodes the full §E.2.3.3 SPX strategy + +//! coordinate syntax (replacing the round-4 `spxinu == 1` mute): +//! `chinspx[ch]`, `spxstrtf`, `spxbegf`, `spxendf`, `spxbndstrce` + +//! `spxbndstrc[]` (with the Table E2.11 default banding), and the +//! per-channel coordinate block `spxcoe` / `spxblnd` / `mstrspxco` / +//! `spxcoexp` / `spxcomant`. SPX-channel `endmant` is set to the SPX +//! begin frequency (§E.3.3.3), `chbwcod` is skipped for SPX channels, +//! `cplendf` is derived from `spxbegf` when SPX is in use (§E.3.3.1), +//! and `nrematbd` folds in SPX (§E.3.3.2) — three derivations that +//! previously drifted the bit cursor on SPX frames. The §E.3.6 +//! high-frequency regeneration itself (coefficient translation, noise +//! blending, banded RMS scaling, coordinate scaling) runs in +//! [`crate::audblk::dsp_block`] via `apply_spectral_extension`. +//! +//! ## Adaptive Hybrid Transform (AHT) — multichannel fbw (round 110) +//! +//! Multichannel full-bandwidth AHT decode. The audblk loop keeps a +//! per-channel AHT-coefficient cache (`aht_coeffs[ch][blk][bin]`) +//! populated by [`unpack_mixed_mantissas`] on the FIRST AHT-active block +//! per channel; subsequent blocks load coefficients from the cache and +//! emit zero mantissa bits for that channel. The audfrm parser is split +//! into phase A ([`super::audfrm::parse_with`]) + phase B +//! ([`super::audfrm::parse_phase_b`]) so the dsp can hand it the §3.4.2 +//! helper variables `nchregs[ch]` / `ncplregs` / `nlferegs`. Round 6 +//! shipped mono-only by hardcoding `nchregs[0] = 1`; round 110 computes +//! all three regs directly from the already-parsed per-block exponent +//! strategies ([`compute_aht_regs`]) — no real audblk pre-walk needed — +//! so every fbw channel with `nchregs[ch] == 1` takes the AHT path. +//! The non-AHT (standard scalar) channels in a mixed frame now share +//! the bap-1/2/4 grouping buffers across channels in frequency-then- +//! channel order via the canonical [`crate::audblk::fetch_mantissa`], +//! matching base AC-3 §7.3.5 (round 6's per-channel grouping was correct +//! only for the mono case). LFE-AHT (`lfeahtinu`) synthesis decodes as of +//! round 113 (and the previously-skipped standard LFE mantissa read in +//! the AHT path is fixed). Coupling-AHT (`cplahtinu`) synthesis decodes +//! as of round 117: the coupling-channel AHT mantissa block (the +//! `cplgaqmod` word, gain words, 6×ncplmant VQ/GAQ mantissas, then IDCT +//! per §3.4) is read inline with the first coupled fbw channel — gated by +//! `got_cplchan` exactly as the base-AC-3 mantissa loop — over the +//! coupling range `[cpl_begf_mant, cpl_endf_mant)`, and its per-block +//! coefficients are loaded into the coupling pseudo-channel slot before +//! the §7.4 decouple step. No AHT flag is rejected by the dsp any more. +//! +//! Per-bin AHT decode flow: +//! +//! 1. Derive `hebap[bin]` from `psd[bin]` / `mask[masktab[bin]]` via +//! [`super::aht::hebap_from_address`] (Table E3.1). +//! 2. Read 2-bit `chgaqmod` + `chgaqsections` 1- or 5-bit gain words. +//! 3. Per bin: +//! * `hebap == 0` → zero coefficients across all 6 blocks. +//! * `1 ≤ hebap ≤ 7` → 2..9-bit VQ codeword indexes into Tables +//! E4.1..E4.7, returns 6 dequantised mantissas. +//! * `hebap ≥ 8` → 6 scalar / GAQ-tagged mantissa reads (with the +//! Gk gain factor applied per Table E3.5). +//! 4. Apply the §3.4.5 inverse DCT-II over the 6 mantissas to +//! recover per-block C(k, m). +//! 5. Multiply by `2^-exp` and stash in `aht_coeffs[ch][blk][bin]`. +//! +//! ## Round 5 — standard coupling +//! +//! The audblk syntax for **standard** (non-enhanced) coupling per +//! Table E1.4 is wired end-to-end: +//! +//! * `cplstre[blk]` + `cplinu[blk]` come from [`AudFrm::cplstre_blk`] +//! / [`AudFrm::cplinu_blk`] (audfrm-resident in Annex E, vs. +//! audblk-resident in base AC-3). +//! * When `cplstre[blk] && cplinu[blk]`: parse `ecplinu` (1 bit), +//! `chincpl[ch]` (per fbw channel, 1 bit each — implicit `1` for +//! 2/0), `phsflginu` (1 bit, only in 2/0), `cplbegf`/`cplendf` +//! (4 bits each), `cplbndstrce` (1 bit) + per-subband +//! `cplbndstrc[bnd]` (1 bit). +//! * When `cplinu[blk]`: parse `cplcoe[ch]` (1 bit, implicit `1` if +//! `firstcplcos[ch]`) + `mstrcplco`/`cplcoexp`/`cplcomant` (4+4 +//! bits per band) + `phsflg[bnd]` (1 bit, only in 2/0). +//! * Coupling-channel exponents (`cplabsexp` 4 bits + grouped exps), +//! `chbwcod[ch]` only for un-coupled channels, `cplleake` + +//! `cplfleak`/`cplsleak`, `cpldeltbae` (delta-BA for the cpl +//! channel) — all wired through to the existing [`Ac3State`] slots +//! so [`audblk::dsp_block`] runs the §7.4 decouple step unchanged. +//! +//! `ecplinu == 1` (enhanced coupling, §E.3.5.5) decodes through a +//! deferred two-pass path: the per-block loop parses the strategy + +//! coordinates and decodes the enhanced-coupling channel into the +//! `MAX_FBW` slot (pinning the coupling region at the ecpl bins), then +//! [`run_deferred_ecpl_dsp`] reconstructs the §E.3.5.5.1 carrier from +//! prev/curr/next blocks and emits each coupled channel's coefficients +//! via [`super::ecpl::synthesize_block`] before the §7.4 decouple is +//! skipped. None of the validator-encoded corpus fixtures exercise the +//! ecpl path (the corpus encoder emits only standard coupling), so +//! the synthesis is covered by [`super::ecpl`] unit tests; standard +//! coupling covers all four 5.1 / low-rate fixtures +//! (eac3-5.1-48000-384kbps, eac3-5.1-side-768kbps, +//! eac3-low-rate-stereo-64kbps, eac3-from-ac3-bitstream-recombination). +//! +//! Other newly-handled fields: +//! * `convsnroffste` (1 bit, always present for `strmtyp == 0`) + +//! optional 10-bit `convsnroffst` — was silently missing from +//! round 2. Most fixtures had it = 0 so the missing bit aliased +//! onto the next field cleanly, but coupled fixtures hit `cplleake` +//! right after which made the misalignment visible. +//! +//! ## Round 4 (prior commit) +//! +//! AHT and SPX are E-AC-3-specific psychoacoustic features that gate +//! decode for any fixture using them. The round-4 **stub** in this +//! commit: +//! +//! * Surfaces `ahte == 1` as a clear `Error::Unsupported` from the +//! audfrm parser instead of silently skipping bits (round 1 +//! incorrectly consumed AHT bits as if they were always-emitted). +//! * Tightens the `spxinu == 1` rejection in the audblk parser with +//! a spec citation (§E.2.2.5.4). +//! * Documents the path forward: a real round-4-bis lands the §E.2.2.4 +//! Karhunen-Loeve VQ codebooks (AHT) and the §E.2.2.5.4 SBR-style +//! parametric high-frequency reconstruction (SPX). Both are +//! substantial: AHT requires a 2-pass audblk decode (scan +//! chexpstr, then re-walk for AHT in-use bits) plus the VQ +//! codebook tables themselves; SPX needs the spxcoexp/spxcomant +//! coordinate decoding + the noise-blend / amplitude-fold +//! reconstruction pipeline. +//! +//! Fixtures unblocked by round 4-bis: `eac3-low-bitrate-32kbps` (AHT +//! at low bit budgets per its `notes.md`). No corpus fixture +//! exercises SPX (the validator-encoded corpus omits it). +//! +//! ## Scope (round 2) +//! +//! The round-2 DSP path covers the **simple-encoder happy path** that +//! the bulk of the corpus fixtures actually exercise: +//! +//! * `expstre == 1` — per-block per-channel chexpstr (no frame-level +//! strategy run; we don't currently translate the §E.1.3.4.4 +//! chexpstr-codeword runs from frmchexpstr into per-block strategies). +//! * `bamode == 1` — per-block bit-allocation parametric info. +//! * `dithflage == 1` — per-block per-channel dithflag (otherwise spec +//! says implicit 1). +//! * `blkswe == 1` — per-block blksw (otherwise implicit 0). +//! * `dbaflde == 1` — delta-bit-allocation may appear per block. +//! * `skipflde == 1` — skip-field may appear per block. +//! * `snroffststr == 0` — single frame-level (csnroffst, fsnroffst). +//! * Standard coupling (round 5) + enhanced coupling (§E.3.5.5, +//! deferred two-pass synthesis). +//! * No spectral extension (`spxinu == 0` always). +//! * No AHT (`ahte == 0` — gated upstream in audfrm parser). +//! * Transient pre-noise processing (`transproce == 1`) is decoded via +//! the §E.3.7.2 PCM-domain synthesis (round 103); no longer rejected. +//! +//! When any of these conditions is violated the parser returns +//! [`oxideav_core::Error::Unsupported`] and the caller (the decoder +//! in [`super::decoder`]) falls back to silent emit for that frame. +//! +//! ## Bit syntax — Table E.1.4 (audblk()) verbatim, simplified +//! +//! Per ETSI TS 102 366 V1.4.1 §E.1.2.4 / ATSC A/52:2018 Table E1.4 the +//! per-block-per-channel `chexpstr`, per-block `cplexpstr`, and +//! per-block `lfeexpstr` strategy codes are emitted in **audfrm** +//! (Table E.1.3, gated by `expstre`), NOT in audblk. Audblk merely +//! consumes them as state via the `chexpstr[blk][ch] != reuse` / +//! `cplexpstr[blk] != reuse` / `lfeexpstr[blk] != reuse` gates that +//! decide whether the bandwidth code + exponent payload follow. +//! +//! ```text +//! if (blkswe) for (ch=0..nfchans) blksw[ch] 1 +//! if (dithflage) for (ch=0..nfchans) dithflag[ch] 1 +//! dynrnge 1 +//! if (dynrnge) dynrng 8 +//! if (acmod==0) { +//! dynrng2e 1 +//! if (dynrng2e) dynrng2 8 +//! } +//! if (blk == 0) spxstre = 1 +//! else spxstre 1 +//! if (spxstre) spxinu (+ spx fields when set) 1+ +//! if (cplstre[blk]) ... coupling strategy fields ... (cplstre/cplinu in audfrm) +//! if (cplinu[blk]) ... coupling coordinates ... +//! if (acmod==2) { +//! if (blk == 0) rematstr = 1 (implicit) +//! else rematstr 1 +//! if (rematstr) rematflg[0..n] 1 each +//! } +//! /* §E.1.2.4 chbwcod — gated by audfrm-supplied chexpstr */ +//! for (ch) if (chexpstr[blk][ch] != reuse && !chincpl && !chinspx) +//! chbwcod[ch] 6 +//! /* exponents */ +//! if (cplinu[blk] && cplexpstr[blk] != reuse) — coupling exponents (cplabsexp + groups) +//! for (ch) if (chexpstr[blk][ch] != reuse) — exps[ch][0] + groups + gainrng[ch] (2) +//! if (lfeon && lfeexpstr[blk] != reuse) — LFE exponents (no gainrng) +//! /* bit-allocation parametric */ +//! if (bamode) { +//! baie 1 +//! if (baie) sdcycod(2) fdcycod(2) sgaincod(2) dbpbcod(2) floorcod(3) +//! } +//! if (snroffststr==0) — uses frame-level (csnroffst, fsnroffst). +//! else — per-block snroffste etc. (NOT IN ROUND 2) +//! if (frmfgaincode) — fgaincode (1 bit) per block, then 3 bits per channel if set. +//! if (strmtyp == 0) — convsnroffste (1 bit) [+10-bit convsnroffst when set] +//! if (cplinu[blk]) — cplleak block (cplleake + cplfleak/cplsleak). +//! /* dba */ +//! if (dbaflde) { +//! deltbaie 1 +//! ... (same as AC-3) ... +//! } +//! if (skipflde) { +//! skiple 1 +//! if (skiple) skipl(9) + skipfld(skipl*8) +//! } +//! /* mantissas — bap-driven, identical to AC-3 unpack_mantissas. */ +//! ``` + +use oxideav_core::bits::BitReader; +use oxideav_core::{Error, Result}; + +use crate::audblk::{self, Ac3State, BLOCKS_PER_FRAME, MAX_FBW, N_COEFFS, SAMPLES_PER_BLOCK}; +use crate::bsi::Bsi as Ac3Bsi; +use crate::syncinfo::SyncInfo; + +use super::aht::{self, AHT_BLOCKS}; +use super::audfrm::{self, AhtRegsHints, AudFrm}; +use super::bsi::{Bsi as Eac3Bsi, StreamType}; + +/// Default spectral-extension banding structure `defspxbndstrc[]` per +/// Table E2.11. Indexed by absolute SPX sub-band number; a `true` (the +/// '1' entries at sub-bands 8, 10, 12, 14, 16) means "merge into the +/// previous band". Used the first time SPX is active in a frame when +/// `spxbndstrce == 0`. +pub const DEFAULT_SPX_BNDSTRC: [bool; 18] = { + let mut t = [false; 18]; + t[8] = true; + t[10] = true; + t[12] = true; + t[14] = true; + t[16] = true; + t +}; + +/// §E.2.3.3.15 Table E2.12 — Default Coupling Banding Structure +/// `defcplbndstrc[]`. Indexed by the **absolute** coupling sub-band +/// number (0..17). A `true` entry means that sub-band is merged into +/// the previous band rather than starting a new band. Used when +/// `cplbndstrce == 0` in the first coupling block of a frame (Annex E +/// standard coupling); base AC-3 always transmits the structure +/// explicitly and never consults this table. +const DEFCPLBNDSTRC: [bool; 18] = { + let mut t = [false; 18]; + t[8] = true; + t[10] = true; + t[11] = true; + t[13] = true; + t[14] = true; + t[15] = true; + t[16] = true; + t[17] = true; + t +}; + +/// Decode one E-AC-3 independent substream's audblks into interleaved +/// f32 PCM. Returns `Ok(())` on a successful clean walk, or `Err(...)` +/// if any block hits a feature we don't support (caller substitutes +/// silence). +/// +/// `out` length must equal `bsi.num_blocks * 256 * bsi.nchans`. +pub fn decode_indep_audblks( + bsi: &Eac3Bsi, + audfrm: &AudFrm, + br: &mut BitReader<'_>, + state: &mut Ac3State, + out: &mut [f32], +) -> Result<()> { + // Phase-B audfrm finalisation when AHT is in use. The audfrm parser + // stopped at the AHT anchor so the dsp can compute the §3.4.2 helper + // variables `nchregs[ch]` / `ncplregs` / `nlferegs` — the number of + // times each channel transmits exponents in the 6-block frame. These + // are NOT in the bitstream; they are derived from the per-block + // exponent strategies that audfrm already parsed (`chexpstr_blk_ch`, + // `cplexpstr_blk` + `cplstre_blk`, `lfeexpstr`), so no real audblk + // pre-walk is needed — every input is available on `AudFrm`. + // + // `parse_phase_b` then reads `chahtinu[ch]` for every fbw channel + // with `nchregs[ch] == 1` (multichannel-capable as of round 110), + // plus `cplahtinu` / `lfeahtinu` when their regs gate fires. + // fbw AHT (round 110), LFE AHT (round 113), and coupling-AHT + // (round 117) all decode now, so no AHT flag is rejected here. + let mut audfrm_local; + let audfrm: &AudFrm = if audfrm.aht_phase_b_pending { + audfrm_local = audfrm.clone(); + let hints = compute_aht_regs(&audfrm_local, bsi); + audfrm::parse_phase_b(br, &mut audfrm_local, bsi, &hints)?; + &audfrm_local + } else { + audfrm + }; + + // Reject cases the parser does not handle. + reject_unsupported(bsi, audfrm)?; + + // Build a "shim" AC-3 BSI + SyncInfo so the reused helpers see the + // shape they expect. + let ac3_bsi = build_ac3_bsi_shim(bsi); + let si = build_syncinfo_shim(bsi); + + // Top-level frame init mirrors §7.2.2.6: clear delta-segment counts. + for n in state.deltnseg.iter_mut() { + *n = 0; + } + + let nfchans = bsi.nfchans as usize; + let nchans = bsi.nchans as usize; + let lfeon = bsi.lfeon; + let num_blocks = bsi.num_blocks as usize; + let strmtyp_indep = matches!(bsi.strmtyp, StreamType::Independent); + let _ = BLOCKS_PER_FRAME; // unused once cplinu_blk migrated to audfrm + + // §E.2.3.2 / §E.1.2.4 — per-frame syntax-state initialisation. The + // audfrm finishes by setting `firstcplcos[ch] = 1` for every fbw + // channel and `firstcplleak = 1`; both are stateful "have we seen + // a coupling-coordinate / leak-init block yet this frame" flags + // that gate whether the audblk reads the explicit `cplcoe[ch]` / + // `cplleake` bit or substitutes an implicit `1`. They reset every + // syncframe so we keep them as locals here rather than on `state`. + let mut firstcplcos: [bool; MAX_FBW] = [true; MAX_FBW]; + let mut firstcplleak = true; + // §E.2.3.3.15 — "have we parsed coupling-banding-structure in any + // earlier block of THIS frame yet". The default-band-structure + // substitution (Table E2.12) only applies when `cplbndstrce == 0` + // in the FIRST coupling block of the frame; a later `cplbndstrce == + // 0` reuses the previous block's structure instead. + let mut first_cpl_strategy_block = true; + // §E.2.3.3.9 firstspxcos[ch] — "have we seen explicit SPX + // coordinates for channel ch yet this frame". Resets every + // syncframe; the first block in which a channel is in SPX carries an + // implicit `spxcoe[ch] = 1`. + let mut firstspxcos: [bool; MAX_FBW] = [true; MAX_FBW]; + + // ---- §E.3.5.5 enhanced-coupling per-frame state ---- + // + // `ecpl_in_use` tracks whether the active coupling strategy is the + // enhanced (`ecplinu == 1`) variant; it persists across `cplstre == 0` + // reuse blocks exactly like the standard-coupling strategy fields. + // `ecpl_strategy` holds the band geometry; `ecpl_coords` the per-block + // amplitude/angle/chaos triples. `ecpl_prev_bndstrc` carries the + // previous block's banding so a `ecplbndstrce == 0` block reuses it + // (default on the first ecpl block of the frame, §E.2.3.3.18). + // + // When any block uses enhanced coupling the per-block DSP is deferred: + // each block's de-normalised ecpl-channel coefficients are snapshotted + // into `ecpl_blocks[blk]` and the per-channel synthesis (§E.3.5.5.1 + // carrier from prev/curr/next + §E.3.5.5.4 product) runs in a second + // pass after the block loop, because the §E.3.5.5.1 carrier needs the + // *next* block's coefficients which are not yet decoded mid-loop. + let mut ecpl_in_use = false; + let mut ecpl_strategy: Option = None; + let mut ecpl_coords: Option = None; + let mut ecpl_prev_bndstrc = super::ecpl::DEFAULT_ECPL_BNDSTRC; + // Per-block deferred-synthesis snapshots; `Some` only for ecpl blocks. + let mut ecpl_blocks: Vec> = vec![None; num_blocks]; + // Whether the frame used enhanced coupling in any block — once true, + // ALL remaining blocks defer their DSP so the overlap-add delay line + // renders in order across the second pass. + let mut frame_has_ecpl = false; + // Absolute block index where deferral began (= first ecpl block). The + // deferred pass maps snapshot index `i` to block `first_deferred_blk + i`. + let mut first_deferred_blk = 0usize; + // Per-deferred-block full channel-state snapshots + the decouple-skip + // flag (true for ecpl blocks). Only populated once `frame_has_ecpl`. + let mut deferred_channels: Vec<[crate::audblk::ChannelState; crate::audblk::MAX_CHANNELS]> = + Vec::new(); + let mut deferred_skip_decouple: Vec = Vec::new(); + + // §7.2.2.6 — clear leftover coupling state at the top of every + // frame so a previous frame's `cpl_in_use` doesn't leak forward + // when this frame's blk 0 has `cplinu == 0`. + state.cpl_in_use = false; + for ch in 0..MAX_FBW { + state.channels[ch].in_coupling = false; + } + + // §E.3.6 — clear SPX state at the top of every frame so a previous + // frame's spxinu / band structure doesn't leak forward. + state.spx_in_use = false; + for ch in 0..MAX_FBW { + state.channels[ch].in_spx = false; + } + + // §3.6.4.2.3 — propagate the frame-scoped `chinspxatten[ch]` + + // `spxattencod[ch]` (read by the audfrm parser, Table E1.3) onto + // the per-channel state. The SPX synthesis step + // (`audblk::apply_spectral_extension`) consults these to apply the + // 5-tap border notch filter at the baseband/extension boundary and + // every translation-copy wrap point. When `spxattene == 0` for the + // frame, both flags stay cleared (no notch applied). + for ch in 0..nfchans { + if audfrm.spxattene { + state.channels[ch].spx_atten_active = audfrm.chinspxatten[ch]; + state.channels[ch].spx_atten_code = audfrm.spxattencod[ch]; + } else { + state.channels[ch].spx_atten_active = false; + state.channels[ch].spx_atten_code = 0; + } + } + + // ---- §3.4 AHT pre-buffered coefficients ---- + // + // When `chahtinu[ch] == 1`, the audblk that decodes the FIRST + // non-`REUSE` exponent strategy for channel `ch` reads ALL 6×nmant + // AHT mantissas + GAQ side info up front. Subsequent audblks for + // that channel emit no mantissa bits — they pull their per-block + // coefficient values from this `aht_coeffs[ch][blk][bin]` cache, + // which holds the post-IDCT / post-`*2^-exp` floating coefficients. + // + // `aht_pending[ch] == true` for channels that have `chahtinu == 1` + // (or `lfeahtinu == 1` at the LFE slot) AND haven't emitted their + // AHT mantissa block yet this frame. `aht_filled[ch] == true` once + // the cache is populated. + // + // The arrays carry one slot per `state.channels` index — fbw 0..5, + // the coupling pseudo-channel at `MAX_FBW`, and the LFE channel at + // `MAX_FBW + 1` — so the LFE-AHT path (round 113) and the + // coupling-AHT path (round 117) share the same cache+flag machinery + // as the fbw channels. The `MAX_FBW` (coupling) slot is armed when + // `cplahtinu == 1`; the coupling mantissa block is read interleaved + // with the first coupled fbw channel inside `unpack_mixed_mantissas`. + // ~42 KB total (7 slots × 6 blks × 256 bins × 4 B). Heap-allocate so + // we don't blow the audio thread's modest stack budget. + const AHT_SLOTS: usize = MAX_FBW + 2; + let lfe_slot = MAX_FBW + 1; + let mut aht_coeffs: Vec<[[f32; N_COEFFS]; AHT_BLOCKS]> = + vec![[[0.0; N_COEFFS]; AHT_BLOCKS]; AHT_SLOTS]; + let mut aht_pending: [bool; AHT_SLOTS] = [false; AHT_SLOTS]; + let mut aht_filled: [bool; AHT_SLOTS] = [false; AHT_SLOTS]; + if audfrm.ahte { + aht_pending[..nfchans].copy_from_slice(&audfrm.chahtinu[..nfchans]); + aht_pending[MAX_FBW] = audfrm.cplahtinu; + if lfeon { + aht_pending[lfe_slot] = audfrm.lfeahtinu; + } + } + + for blk in 0..num_blocks { + state.blkidx = blk; + + // ---- §E.1.3.1 blksw[ch] ---- + if audfrm.blkswe { + for ch in 0..nfchans { + let v = br.read_u32(1)? != 0; + state.channels[ch].blksw = v; + } + } else { + for ch in 0..nfchans { + state.channels[ch].blksw = false; + } + } + + // ---- §E.1.3.2 dithflag[ch] ---- + if audfrm.dithflage { + for ch in 0..nfchans { + let v = br.read_u32(1)? != 0; + state.channels[ch].dithflag = v; + } + } else { + for ch in 0..nfchans { + state.channels[ch].dithflag = true; + } + } + + // ---- dynrng ---- + // The §6.1.9 / §7.7 DRC control surface maps the dynrng word + // (line-out / partial-compression) or the frame-level compr word + // (RF mode, §7.7.2.1) to the applied linear gain. Default is + // line-out (full dynrng), so existing E-AC-3 decodes are + // unaffected unless the caller opts in. + let compr_ch1 = bsi.compr.map(|c| c.raw()); + let compr_ch2 = bsi.compr_ch2.map(|c| c.raw()); + let dynrnge = br.read_u32(1)? != 0; + if dynrnge { + let dynrng = br.read_u32(8)? as u8; + let g = state.drc.resolve_block_gain(dynrng, compr_ch1); + for ch in 0..nfchans { + state.channels[ch].dynrng = g; + } + } else if blk == 0 { + let g = state.drc.resolve_block_gain(0x00, compr_ch1); + for ch in 0..nfchans { + state.channels[ch].dynrng = g; + } + } + if bsi.acmod == 0 { + let dynrng2e = br.read_u32(1)? != 0; + if dynrng2e { + let d2 = br.read_u32(8)? as u8; + state.channels[1].dynrng = state.drc.resolve_block_gain(d2, compr_ch2); + } else if blk == 0 { + state.channels[1].dynrng = state.drc.resolve_block_gain(0x00, compr_ch2); + } + } + + // ---- spectral extension strategy block (§E.1.3.5 / §E.3.6) ---- + // + // Per Table E1.4, blk 0 has implicit `spxstre = 1` with the + // 1-bit `spxinu[0]` emitted directly; subsequent blocks emit + // `spxstre[blk]` (1 bit) + (only if spxstre[blk]) `spxinu[blk]`. + // + // The §E.3.6 SPX decode is a parametric high-frequency + // reconstruction (the E-AC-3 analogue of SBR): the band + // [spx_begin .. spx_end) is copied from low-frequency bins, + // blended with banded noise, and scaled by per-band coordinates. + // The strategy fields here set up the sub-band → band geometry; + // the per-channel coordinate block (below) supplies spxco / + // spxblnd; the synthesis itself runs in `audblk::dsp_block`. + let spxstre = if blk == 0 { true } else { br.read_u32(1)? != 0 }; + if spxstre { + let spxinu = br.read_u32(1)? != 0; + state.spx_in_use = spxinu; + if spxinu { + // §E.2.3.3.3 chinspx[ch]. + if bsi.acmod == 0x1 { + state.channels[0].in_spx = true; + } else { + for ch in 0..nfchans { + state.channels[ch].in_spx = br.read_u32(1)? != 0; + } + } + // §E.2.3.3.4-6 spxstrtf (2) + spxbegf (3) + spxendf (3). + state.spx_strtf = br.read_u32(2)? as u8; + let spxbegf = br.read_u32(3)? as usize; + let spxendf = br.read_u32(3)? as usize; + state.spx_begin_subbnd = if spxbegf < 6 { + spxbegf + 2 + } else { + spxbegf * 2 - 3 + }; + state.spx_end_subbnd = if spxendf < 3 { + spxendf + 5 + } else { + spxendf * 2 + 3 + }; + if state.spx_end_subbnd <= state.spx_begin_subbnd || state.spx_end_subbnd > 17 { + return Err(Error::invalid( + "eac3 audblk: SPX sub-band range invalid (end <= begin or > 17)", + )); + } + // §E.2.3.3.7-8 spxbndstrce + spxbndstrc[bnd]. When the + // exist bit is 0 in the first SPX block use the default + // banding (Table E2.11); in later blocks reuse the prior + // structure (already on `state.spx_bndstrc`). + let spxbndstrce = br.read_u32(1)? != 0; + if spxbndstrce { + state.spx_bndstrc = [false; 18]; + for bnd in (state.spx_begin_subbnd + 1)..state.spx_end_subbnd { + state.spx_bndstrc[bnd] = br.read_u32(1)? != 0; + } + } else if firstspxcos.iter().take(nfchans).all(|&f| f) { + // First SPX block this frame, no explicit structure → + // default banding per Table E2.11 (merge bit set on + // odd sub-bands 8,10,12,14,16). + state.spx_bndstrc = DEFAULT_SPX_BNDSTRC; + } + // Derive nspxbnds + per-band size (§E.3.6.2). + let mut nspxbnds = 1usize; + let mut sztab = [0usize; 18]; + sztab[0] = 12; + for bnd in (state.spx_begin_subbnd + 1)..state.spx_end_subbnd { + if !state.spx_bndstrc[bnd] { + sztab[nspxbnds] = 12; + nspxbnds += 1; + } else { + sztab[nspxbnds - 1] += 12; + } + } + state.spx_nbnds = nspxbnds; + state.spx_bndsztab = sztab; + } else { + // §E.2.3.3.2 — SPX not in use this block; clear per-ch + // flags and arm firstspxcos for the next active block. + for ch in 0..nfchans { + state.channels[ch].in_spx = false; + firstspxcos[ch] = true; + } + } + } + + // ---- spectral extension coordinates (§E.1.3.5 / §E.3.6.3) ---- + if state.spx_in_use { + for ch in 0..nfchans { + if state.channels[ch].in_spx { + // §E.2.3.3.9 spxcoe[ch] — implicit 1 on the first SPX + // block for this channel; explicit thereafter. + let spxcoe = if firstspxcos[ch] { + firstspxcos[ch] = false; + true + } else { + br.read_u32(1)? != 0 + }; + if spxcoe { + // §E.2.3.3.10-11 spxblnd (5) + mstrspxco (2). + let spxblnd = br.read_u32(5)? as f32; + let mstrspxco = br.read_u32(2)? as i32; + let noffset = spxblnd / 32.0; + // §E.3.6.4.2.1 blend factors per band. + let spx_begin_tc = 25 + 12 * state.spx_begin_subbnd; + let spx_end_tc = 25 + 12 * state.spx_end_subbnd; + let mut spxmant = spx_begin_tc as f32; + for bnd in 0..state.spx_nbnds { + let bandsize = state.spx_bndsztab[bnd] as f32; + let mut nratio = + (spxmant + 0.5 * bandsize) / spx_end_tc as f32 - noffset; + nratio = nratio.clamp(0.0, 1.0); + state.channels[ch].spx_nblend[bnd] = nratio.sqrt(); + state.channels[ch].spx_sblend[bnd] = (1.0 - nratio).sqrt(); + spxmant += bandsize; + } + // §E.2.3.3.12-13 + §E.3.6.3 per-band coordinate. + for bnd in 0..state.spx_nbnds { + let spxcoexp = br.read_u32(4)? as i32; + let spxcomant = br.read_u32(2)? as f32; + let temp = if spxcoexp == 15 { + spxcomant / 4.0 + } else { + (spxcomant + 4.0) / 8.0 + }; + let shift = spxcoexp + 3 * mstrspxco; + state.channels[ch].spx_coord[bnd] = temp * 2f32.powi(-shift); + } + } + // spxcoe == 0 → reuse the prior block's coordinates + + // blend factors (already on `state.channels[ch]`). + } else { + firstspxcos[ch] = true; + } + } + } + + // ---- coupling strategy block (Table E1.4 + §E.1.3.3.5) ---- + // + // Per §E.1.2 / Table E1.3, `cplstre[blk]` + `cplinu[blk]` are + // emitted in **audfrm** (already parsed; surfaced as + // [`AudFrm::cplstre_blk`] / [`AudFrm::cplinu_blk`]). The audblk + // only carries the **strategy details** (chincpl, cplbegf, + // cplendf, cplbndstrc) when `cplstre[blk] && cplinu[blk]`, + // and the **coordinate block** (cplcoe, mstrcplco, cplcoexp, + // cplcomant, phsflg) whenever `cplinu[blk]`. + let cplinu = audfrm.cplinu_blk[blk]; + let cplstre = audfrm.cplstre_blk[blk]; + if cplstre { + if cplinu { + // §E.1.3.3.6 ecplinu — enhanced coupling flag. + let ecplinu = br.read_u32(1)? != 0; + // §E.1.3.3.7 chincpl[ch] — per the Annex E audblk syntax + // the per-channel in-coupling flags follow `ecplinu` + // immediately, BEFORE the standard/enhanced arm split: + // implicit 1 for both channels in 2/0, explicit 1-bit + // field per fbw channel otherwise. (An earlier revision + // read them after the enhanced-coupling begin/end/banding + // fields, which desynced the cursor on any multichannel + // — acmod > 2 — enhanced-coupling frame; harmless in 2/0 + // where no bits are transmitted. Pinned by the 5.1 + // enhanced-coupling encoder round-trip.) + if bsi.acmod == 0x2 { + state.channels[0].in_coupling = true; + state.channels[1].in_coupling = true; + } else { + for ch in 0..nfchans { + let v = br.read_u32(1)? != 0; + state.channels[ch].in_coupling = v; + } + } + if ecplinu { + // §E.2.3.3.16-19 enhanced-coupling strategy. The + // §E.3.5.5 carrier synthesis runs on the deferred path + // after the block loop; here we parse the strategy + // fields and pin the coupling region + // [cpl_begf_mant, cpl_endf_mant) at the ecpl region so + // the shared exponent / bit-allocation / mantissa + // machinery decodes the enhanced-coupling channel into + // the `MAX_FBW` slot exactly like standard coupling. + // Recover the raw 3-bit `spxbegf` from the derived + // `spx_begin_subbnd` (§E.2.3.3.5 inverse): + // spxbegf < 6 → spx_begin_subbnd = spxbegf + 2 + // spxbegf ≥ 6 → spx_begin_subbnd = spxbegf*2 - 3 + // so the inverse splits at spx_begin_subbnd == 7. + // `ecpl::end_subbnd` only consults `spxbegf` when SPX is + // co-active (it bounds the ecpl region just below SPX). + let spx_begf_for_ecpl = if state.spx_begin_subbnd <= 7 { + state.spx_begin_subbnd.saturating_sub(2) + } else { + (state.spx_begin_subbnd + 3) / 2 + }; + let strat = super::ecpl::parse_strategy( + br, + state.spx_in_use, + spx_begf_for_ecpl, + &ecpl_prev_bndstrc, + )?; + ecpl_prev_bndstrc = strat.bndstrc; + ecpl_in_use = true; + state.cpl_begf_mant = strat.begin_bin(); + state.cpl_endf_mant = strat.end_bin(); + state.cpl_in_use = true; + state.cpl_nsubbnd = strat.end_subbnd - strat.begin_subbnd; + state.cpl_nbnd = strat.necplbnd; + ecpl_strategy = Some(strat); + } else { + // §E.1.3.3.8 phsflginu — only in 2/0. + state.phsflginu = if bsi.acmod == 0x2 { + br.read_u32(1)? != 0 + } else { + false + }; + // §E.1.3.3.9 cplbegf (4 bits). + state.cpl_begf = br.read_u32(4)? as u8; + // §E.1.3.3.10 cplendf. Read 4 bits only when SPX is OFF; + // when SPX is in use the spec derives cplendf from the + // SPX begin so the coupled region ends one bin below the + // SPX region (spxbegf < 6 → cplendf = spxbegf − 2, else + // spxbegf·2 − 7 — both equal spx_begin_subbnd − 4). + state.cpl_endf = if state.spx_in_use { + (state.spx_begin_subbnd as i32 - 4).max(0) as u8 + } else { + br.read_u32(4)? as u8 + }; + // §5.4.3.12 spec envelope: the upper sub-band index is + // `cplendf + 2`, so `ncplsubnd = 3 + cplendf - cplbegf + // >= 1` is the actual validity test (equivalently + // `cplbegf <= cplendf + 2`). The earlier "cplbegf > + // cplendf" rejection mirrored the AC-3 round-7 bug — + // valid corpus bitstreams use narrow configs like + // `(cplbegf=11, cplendf=10)` for high-bandwidth + // multichannel frames. Use signed arithmetic so the + // `3 + cplendf - cplbegf` term can't underflow. + let ncplsubnd_signed = 3i32 + state.cpl_endf as i32 - state.cpl_begf as i32; + if ncplsubnd_signed < 1 { + return Err(Error::invalid( + "eac3 audblk: cplbegf > cplendf+2 — malformed coupling range", + )); + } + state.cpl_nsubbnd = ncplsubnd_signed as usize; + // §E.2.3.3.15 cplbndstrce — gates the explicit + // `cplbndstrc[]` array. When it is 1, the per-subband + // merge flags follow. When it is 0 in the FIRST + // coupling block of the frame, the **default coupling + // banding structure** `defcplbndstrc[]` (Table E2.12) + // applies — it is NOT all-zeros. When it is 0 in any + // later block, the previous block's structure is + // reused (handled by leaving `cpl_bndstrc` untouched). + // + // `defcplbndstrc[]` is indexed by the **absolute** + // coupling sub-band number; our `cpl_bndstrc[]` is + // indexed by the sub-band offset relative to + // `cplbegf`, so the lookup is + // `defcplbndstrc[cplbegf + offset]`. + let cplbndstrce = br.read_u32(1)? != 0; + if cplbndstrce { + state.cpl_bndstrc[0] = false; + for bnd in 1..state.cpl_nsubbnd.min(18) { + let v = br.read_u32(1)? != 0; + state.cpl_bndstrc[bnd] = v; + } + // Any remaining (in case nsubbnd capped at 18) stay + // at the default 0. + for bnd in state.cpl_nsubbnd.min(18)..18 { + state.cpl_bndstrc[bnd] = false; + } + } else if first_cpl_strategy_block { + // §E.2.3.3.15 first-block default structure. + state.cpl_bndstrc[0] = false; + for bnd in 1..state.cpl_nsubbnd.min(18) { + let abs_sbnd = state.cpl_begf as usize + bnd; + state.cpl_bndstrc[bnd] = + DEFCPLBNDSTRC.get(abs_sbnd).copied().unwrap_or(false); + } + for bnd in state.cpl_nsubbnd.min(18)..18 { + state.cpl_bndstrc[bnd] = false; + } + } + // else (cplbndstrce == 0 in a later block): keep the + // previous block's `cpl_bndstrc[]` untouched. + first_cpl_strategy_block = false; + // Mantissa-domain coupling range: bins [37+12·begf, + // 37+12·(endf+3)) per §7.4.2. + state.cpl_begf_mant = 37 + 12 * state.cpl_begf as usize; + state.cpl_endf_mant = 37 + 12 * (state.cpl_endf as usize + 3); + // Derive ncplbnd by merging sub-bands whose + // cplbndstrc=1 (same algorithm as base AC-3). + // `cpl_bndstrc` is sized 18 (§7.4.2 max sub-bands); clamp + // the merge walk so a malformed `cpl_nsubbnd > 18` can't + // index past it. + let mut n = state.cpl_nsubbnd; + for bnd in 1..state.cpl_nsubbnd.min(18) { + if state.cpl_bndstrc[bnd] { + n -= 1; + } + } + state.cpl_nbnd = n; + state.cpl_in_use = true; + ecpl_in_use = false; + } // end standard-coupling (ecplinu == 0) strategy parse + } else { + // !cplinu[blk] — clear all per-channel coupling flags + // and reset the per-frame state-init markers per + // Table E1.4 ("if !cplinu[blk] { firstcplcos[ch] = 1; + // firstcplleak = 1; phsflginu = 0; ecplinu = 0; }"). + for ch in 0..nfchans { + state.channels[ch].in_coupling = false; + firstcplcos[ch] = true; + } + firstcplleak = true; + state.phsflginu = false; + state.cpl_in_use = false; + ecpl_in_use = false; + ecpl_strategy = None; + } + } + // When !cplstre[blk], every persistent coupling-strategy field + // (cpl_begf/cpl_endf/cpl_nsubbnd/cpl_nbnd/cpl_begf_mant/ + // cpl_endf_mant/cpl_in_use/in_coupling) keeps its prior value + // from the last block where cplstre[blk] == 1 (that's the + // whole point of `cplstre` — strategy reuse). Coordinates + // (cplcoe, mstrcplco, cplcoexp, cplcomant) ARE re-emitted per + // block via the cplcoe[ch] gate below. + + // ---- coupling coordinates (Table E1.4) ---- + let mut any_cplcoe_this_block = false; + if cplinu && ecpl_in_use { + // §E.2.3.3.20-26 enhanced-coupling coordinate block. The + // per-channel amplitude / angle / chaos triples are read by + // [`super::ecpl::parse_coords`] (which advances the bit cursor + // exactly per the reference syntax); the result is stashed for + // the deferred §E.3.5.5 synthesis after the block loop. + let chincpl: Vec = (0..nfchans) + .map(|ch| state.channels[ch].in_coupling) + .collect(); + let mut coords = super::ecpl::parse_coords( + br, + nfchans, + &chincpl, + &mut firstcplcos[..nfchans], + state.cpl_nbnd, + )?; + // §2.3.3.21-22: `ecplparam1e[ch] == 0` / `ecplparam2e[ch] == 0` + // mean "the previously transmitted amplitudes / angle+chaos for + // this channel shall be reused" — thread the prior block's + // values into the fresh coordinate set (an earlier revision + // replaced the whole set each block, silencing every band of a + // reusing channel). + if let Some(prev) = &ecpl_coords { + super::ecpl::merge_reused_params(&mut coords, prev, state.cpl_nbnd); + } + ecpl_coords = Some(coords); + } else if cplinu { + // ecplinu == 0 path (standard coupling coordinates). + for ch in 0..nfchans { + if state.channels[ch].in_coupling { + // §E.1.3.3.13 cplcoe[ch] — implicit 1 on the very + // first block this channel enters coupling per + // frame (firstcplcos[ch]); explicit 1-bit field + // otherwise. + let cplcoe = if firstcplcos[ch] { + firstcplcos[ch] = false; + true + } else { + br.read_u32(1)? != 0 + }; + if cplcoe { + any_cplcoe_this_block = true; + // §E.1.3.3.14 mstrcplco[ch] — 2 bits. + let mstrcplco = br.read_u32(2)? as i32; + for bnd in 0..state.cpl_nbnd { + // §E.1.3.3.15-16 cplcoexp + cplcomant. + let cplcoexp = br.read_u32(4)? as i32; + let cplcomant = br.read_u32(4)? as i32; + let mant = if cplcoexp == 15 { + cplcomant as f32 / 16.0 + } else { + (cplcomant + 16) as f32 / 32.0 + }; + let shift = cplcoexp + 3 * mstrcplco; + state.cpl_coord[ch][bnd] = mant * 2f32.powi(-shift); + } + state.cpl_coord_valid[ch] = true; + } + } else { + // Channel is not part of the coupling group; reset + // the firstcplcos marker so a later block that + // brings this channel back into coupling treats + // its first cplcoe as implicit 1. + firstcplcos[ch] = true; + } + } + // §E.1.3.3.17 phsflg[bnd] — only in 2/0 + phsflginu + at + // least one channel emitted coordinates this block (the + // spec's `cplcoe[0] || cplcoe[1]` test, which is THIS + // block's cplcoe — not a sticky any-block flag). + if bsi.acmod == 0x2 && state.phsflginu && any_cplcoe_this_block { + for bnd in 0..state.cpl_nbnd { + state.cpl_phsflg[bnd] = br.read_u32(1)? != 0; + } + } + } + + // ---- §E.1.3.4 / §7.5 rematrixing — only for 2/0 (acmod==2) ---- + // Block 0 is special: encoder emits rematflg directly without + // a rematstr gate; subsequent blocks emit rematstr first and + // only if set do rematflg follow. AC-3 §5.4.3.19 has the same + // shape for base AC-3. + if bsi.acmod == 0x2 { + let rematstr = if blk == 0 { true } else { br.read_u32(1)? != 0 }; + if rematstr { + // §E.3.3.2 nrematbd — folds in spectral extension AND + // enhanced coupling. When SPX is in use without coupling + // the band count drops to 3 for spxbegf < 2 + // (spx_begin_subbnd < 4). When enhanced coupling is in use + // the count is sized from the raw `ecplbegf` (carried on + // the persistent `ecpl_strategy`, so a `cplstre == 0` reuse + // block keeps the prior strategy's begin code), NOT from + // `cpl_begf` (which the ecpl path never sets). Using the + // wrong arm drifts the bit cursor on ecpl / SPX 2/0 frames. + let ecplbegf = ecpl_strategy.as_ref().map_or(0, |s| s.ecplbegf); + let n_remat = crate::audblk::remat_band_count_spx( + cplinu, + state.cpl_begf, + ecpl_in_use, + ecplbegf, + state.spx_in_use, + state.spx_begin_subbnd, + ); + for rbnd in 0..n_remat { + let v = br.read_u32(1)? != 0; + state.rematflg[rbnd] = v; + } + } + } + + // ---- §E.1.2.4 exponent strategy lookup ---- + // + // §E.1.2.3 / Table E.1.3 emit `chexpstr[blk][ch]` (2 bits), + // `cplexpstr[blk]` (2 bits when cplinu[blk]), and per-block + // `lfeexpstr[blk]` (1 bit, when lfeon) IN audfrm — NOT in + // audblk. The audblk only carries the bandwidth code + + // exponent payload that those strategies gate. Round-29.5 + // moves the strategy reads back to where the spec puts them + // (audfrm); audblk just looks them up. + let cplexpstr = if cplinu { + audfrm.cplexpstr_blk[blk] + } else { + 0u8 + }; + let mut chexpstr = [0u8; MAX_FBW]; + chexpstr[..nfchans].copy_from_slice(&audfrm.chexpstr_blk_ch[blk][..nfchans]); + let lfeexpstr = if lfeon { audfrm.lfeexpstr[blk] } else { 0u8 }; + + // §E.1.3.4.5 chbwcod — only when chexpstr != REUSE AND the + // channel is neither in coupling NOR in spectral extension + // (per the audblk syntax: `if((!chincpl[ch]) && (!chinspx[ch])) + // {chbwcod[ch]}`). SPX channels derive their bandwidth from the + // SPX begin frequency instead. + let mut chbwcod = [0u8; MAX_FBW]; + for ch in 0..nfchans { + if chexpstr[ch] != 0 && !state.channels[ch].in_coupling && !state.channels[ch].in_spx { + chbwcod[ch] = br.read_u32(6)? as u8; + if chbwcod[ch] > 60 { + return Err(Error::invalid( + "eac3 audblk: chbwcod > 60 (E.1.3.4.6 invalid)", + )); + } + } + } + + // ---- coupling-channel exponents (§E.1.3.4.4) ---- + if cplinu && cplexpstr != 0 { + let cplabsexp = br.read_u32(4)? as i32; + let cpl_start = state.cpl_begf_mant; + let cpl_end = state.cpl_endf_mant; + let grpsize = match cplexpstr { + 1 => 1, + 2 => 2, + 3 => 4, + _ => 1, + }; + // Number of groups: (cpl_end - cpl_start) / (grpsize · 3). + // cpl_end - cpl_start = 12 · (cplendf + 3 - cplbegf) = + // 12 · ncplsubnd which is divisible by 3 for grpsize=1 + // and by 6/12 for grpsize=2/4 only when ncplsubnd is even + // (D2/D4). Spec-conformant encoders generally pick a + // strategy that makes this divisible; if not we'd round + // down and miss bins, but this is the spec's + // `(cpl_end - cpl_start) / (grpsize × 3)` formula verbatim. + let ncplgrps = (cpl_end - cpl_start) / (grpsize * 3); + let mut raw_exp = vec![0i32; ncplgrps * 3]; + audblk::decode_exponents( + br, + cplabsexp << 1, + ncplgrps, + cplexpstr as usize, + &mut raw_exp, + )?; + let cpl_ch = MAX_FBW; + for (i, e) in raw_exp.iter().enumerate() { + let idx = cpl_start + i * grpsize; + for j in 0..grpsize { + if idx + j < N_COEFFS { + state.channels[cpl_ch].exp[idx + j] = (*e).clamp(0, 24) as u8; + } + } + } + } + + // ---- fbw exponents ---- + for ch in 0..nfchans { + if chexpstr[ch] != 0 { + // Coupled channels stop at cpl_begf_mant; SPX channels + // stop at the SPX begin frequency (§E.3.3.3: + // endmant = spxbandtable[spx_begin_subbnd] = + // 25 + 12·spx_begin_subbnd); un-coupled / un-SPX channels + // go up to 37 + 3·(chbwcod+12). + let end = if state.channels[ch].in_coupling { + state.cpl_begf_mant + } else if state.channels[ch].in_spx { + 25 + 12 * state.spx_begin_subbnd + } else { + 37 + 3 * (chbwcod[ch] as usize + 12) + }; + state.channels[ch].end_mant = end; + let absexp = br.read_u32(4)? as i32; + let grpsize = match chexpstr[ch] { + 1 => 1, + 2 => 2, + 3 => 4, + _ => 1, + }; + // §7.1.3 nchgrps[ch]: + // D15 → (end-1)/3, D25 → (end-1+3)/6, D45 → (end-1+9)/12 + // (all truncated). The D25 form here previously used + // `div_ceil(6)` = `(end-1+5)/6`, which over-counts groups + // by one when `(end-1) mod 6 ∈ {2,3}` — that reads an + // extra 7-bit exponent word and drifts the bit cursor on + // D25 channels (the AC-3 path already uses the +3 form). + let nchgrps = match chexpstr[ch] { + 1 => (end - 1) / 3, + 2 => (end - 1 + 3) / 6, + 3 => (end - 1 + 9) / 12, + _ => 0, + }; + let mut raw_exp = vec![0i32; nchgrps * 3]; + audblk::decode_exponents(br, absexp, nchgrps, chexpstr[ch] as usize, &mut raw_exp)?; + state.channels[ch].exp[0] = absexp.clamp(0, 24) as u8; + for (i, e) in raw_exp.iter().enumerate() { + let base = i * grpsize + 1; + for j in 0..grpsize { + if base + j < end { + state.channels[ch].exp[base + j] = (*e).clamp(0, 24) as u8; + } + } + } + // §E.1.2.4 / Table E.1.4 — `gainrng[ch]` (2 bits) + // immediately after the per-channel exponent payload. + // We don't currently use it in the DSP path (the round-2 + // bit-allocation reuses base-AC-3 sgain logic that + // doesn't consult gainrng), but the bit MUST be + // consumed or every subsequent field slides. The + // earlier "Annex E dropped gainrng" comment was wrong; + // Table E.1.4 emits it for every fbw channel whose + // strategy this block is non-REUSE. + let _gainrng = br.read_u32(2)?; + } else if blk == 0 { + return Err(Error::invalid( + "eac3 audblk: chexpstr == 0 in block 0 (no prior exponents to reuse)", + )); + } else if state.channels[ch].in_coupling { + // Reuse path: end_mant follows the coupled channel's + // bandwidth, which is cpl_begf_mant. Re-set in case a + // prior block had this channel un-coupled with a + // different end_mant. + state.channels[ch].end_mant = state.cpl_begf_mant; + } else if state.channels[ch].in_spx { + // Reuse path for an SPX channel: coded mantissas stop at + // the SPX begin frequency (§E.3.3.3). + state.channels[ch].end_mant = 25 + 12 * state.spx_begin_subbnd; + } + } + if lfeon && lfeexpstr != 0 { + let lfe_ch = MAX_FBW + 1; + state.channels[lfe_ch].end_mant = 7; + let absexp = br.read_u32(4)? as i32; + let nlfegrps = 2usize; + let mut raw_exp = vec![0i32; nlfegrps * 3]; + audblk::decode_exponents(br, absexp, nlfegrps, 1, &mut raw_exp)?; + state.channels[lfe_ch].exp[0] = absexp.clamp(0, 24) as u8; + for (i, e) in raw_exp.iter().enumerate() { + if i + 1 < 7 { + state.channels[lfe_ch].exp[i + 1] = (*e).clamp(0, 24) as u8; + } + } + } + + // ---- §E.1.3.5 bit-allocation parametric info ---- + if audfrm.bamode { + let baie = br.read_u32(1)? != 0; + if baie { + state.sdcycod = br.read_u32(2)? as u8; + state.fdcycod = br.read_u32(2)? as u8; + state.sgaincod = br.read_u32(2)? as u8; + state.dbpbcod = br.read_u32(2)? as u8; + state.floorcod = br.read_u32(3)? as u8; + } else if blk == 0 { + // §E.2.2.4 — "if bamode == 0 the encoder uses default + // BA params". We use the spec's default codewords + // (Table E1.4 footnote / §E.2.2.4). + state.sdcycod = 0x2; + state.fdcycod = 0x1; + state.sgaincod = 0x1; + state.dbpbcod = 0x2; + state.floorcod = 0x7; + } + } else if blk == 0 { + // Same defaults when bamode == 0. + state.sdcycod = 0x2; + state.fdcycod = 0x1; + state.sgaincod = 0x1; + state.dbpbcod = 0x2; + state.floorcod = 0x7; + } + + // ---- SNR offset (§E.1.3.5.2 / Annex E audblk §2.3.3.27) ---- + // Two strategies (Table E1.4 / spec §2.3.3.27): + // * `snroffststr == 0` — a single (frmcsnroffst, frmfsnroffst) + // pair carried once in audfrm applies to every block of the + // frame (the frame-level path). + // * `snroffststr == 0x1` / `0x2` — per-block SNR offsets carried + // in the audblk itself. Block 0 always emits its offsets + // (implicit `snroffste = 1`); later blocks emit a 1-bit + // `snroffste` and reuse the prior block's values when it is 0. + // The fast-gain (`fgaincod`) defaults are independent of + // `snroffststr`, so they are initialised at block 0 either way. + if blk == 0 { + // §E.2.2.4: "If bamode == 0 the encoder uses fast-gain + // codeword 0x4 (mid)". Carry through for fgaincod when + // frmfgaincode == 0 (no per-block fgaincod). + for ch in 0..nfchans { + state.fgaincod[ch] = 0x4; + } + if lfeon { + state.lfefgaincod = 0x4; + } + state.cpl_fgaincod = 0x4; + } + if audfrm.snroffststr == 0 { + // Frame-level: block 0 latches the audfrm values; later + // blocks reuse them (they never change within the frame). + if blk == 0 { + state.snroffst_coarse = audfrm.frmcsnroffst; + for ch in 0..nfchans { + state.fsnroffst[ch] = audfrm.frmfsnroffst; + } + if lfeon { + state.lfefsnroffst = audfrm.frmfsnroffst; + } + state.cpl_fsnroffst = audfrm.frmfsnroffst; + } + } else { + // Per-block SNR offsets (§2.3.3.27). `snroffste` is implicit + // 1 in block 0 and explicit thereafter; when 0 the prior + // block's `(csnroffst, *fsnroffst)` values are reused (they + // already live on `state`). + let snroffste = if blk == 0 { true } else { br.read_u32(1)? != 0 }; + if snroffste { + state.snroffst_coarse = br.read_u32(6)? as u8; + if audfrm.snroffststr == 0x1 { + // One shared fine offset for coupling + every fbw + // channel + LFE. + let blkfsnroffst = br.read_u32(4)? as u8; + state.cpl_fsnroffst = blkfsnroffst; + for ch in 0..nfchans { + state.fsnroffst[ch] = blkfsnroffst; + } + if lfeon { + state.lfefsnroffst = blkfsnroffst; + } + } else { + // snroffststr == 0x2 — independent fine offset per + // coupling / channel / LFE slot. + if cplinu { + state.cpl_fsnroffst = br.read_u32(4)? as u8; + } + for ch in 0..nfchans { + state.fsnroffst[ch] = br.read_u32(4)? as u8; + } + if lfeon { + state.lfefsnroffst = br.read_u32(4)? as u8; + } + } + } + } + + // ---- §E.1.3.5.4 fgaincode (per-block fgain override) ---- + // Per Table E1.4, when `frmfgaincode == 1` a 1-bit `fgaincode` + // field follows; if set, the per-channel fgaincod (3 bits each) + // are emitted including the cpl-channel slot (only when + // cplinu[blk]). + if audfrm.frmfgaincode { + let fgaincode = br.read_u32(1)? != 0; + if fgaincode { + if cplinu { + state.cpl_fgaincod = br.read_u32(3)? as u8; + } + for ch in 0..nfchans { + state.fgaincod[ch] = br.read_u32(3)? as u8; + } + if lfeon { + state.lfefgaincod = br.read_u32(3)? as u8; + } + } + } + + // ---- §E.1.3.5.3 convsnroffste (always present for strmtyp == 0) ---- + // Optional 10-bit `convsnroffst` follows; we don't use it for + // playback (it adjusts the SNR offset for downstream AC-3 + // converter modes), but we must consume the bit(s). + if strmtyp_indep { + let convsnroffste = br.read_u32(1)? != 0; + if convsnroffste { + let _convsnroffst = br.read_u32(10)?; + } + } + + // ---- §E.1.3.5.4 cplleake (only when cplinu[blk]) ---- + // First-block (per frame) emits `cplleake = 1` implicitly; later + // blocks emit it explicitly. When set, `cplfleak` + `cplsleak` + // (3 bits each) follow. + if cplinu { + let cplleake = if firstcplleak { + firstcplleak = false; + true + } else { + br.read_u32(1)? != 0 + }; + if cplleake { + state.cpl_fleak = br.read_u32(3)? as u8; + state.cpl_sleak = br.read_u32(3)? as u8; + } + } + + // ---- §E.1.3.5.5 dba ---- + if audfrm.dbaflde { + let dbaie = br.read_u32(1)? != 0; + if dbaie { + let cpl_idx = MAX_FBW; + let mut cpldeltbae = 0u32; + if cplinu { + cpldeltbae = br.read_u32(2)?; + } + let mut deltbae = [0u32; MAX_FBW]; + for ch in 0..nfchans { + deltbae[ch] = br.read_u32(2)?; + } + if cplinu { + match cpldeltbae { + 1 => { + let nseg = (br.read_u32(3)? + 1) as usize; + state.deltnseg[cpl_idx] = nseg.min(8); + for seg in 0..state.deltnseg[cpl_idx] { + state.deltoffst[cpl_idx][seg] = br.read_u32(5)? as u8; + state.deltlen[cpl_idx][seg] = br.read_u32(4)? as u8; + state.deltba[cpl_idx][seg] = br.read_u32(3)? as u8; + } + } + 2 => { + state.deltnseg[cpl_idx] = 0; + } + _ => {} + } + } + for ch in 0..nfchans { + match deltbae[ch] { + 1 => { + let nseg = (br.read_u32(3)? + 1) as usize; + state.deltnseg[ch] = nseg.min(8); + for seg in 0..state.deltnseg[ch] { + state.deltoffst[ch][seg] = br.read_u32(5)? as u8; + state.deltlen[ch][seg] = br.read_u32(4)? as u8; + state.deltba[ch][seg] = br.read_u32(3)? as u8; + } + } + 2 => { + state.deltnseg[ch] = 0; + } + _ => {} + } + } + } else if blk == 0 { + for ch in 0..MAX_FBW + 1 { + state.deltnseg[ch] = 0; + } + } + } else if blk == 0 { + for ch in 0..MAX_FBW + 1 { + state.deltnseg[ch] = 0; + } + } + + // ---- §E.1.3.5.6 skip ---- + if audfrm.skipflde { + let skiple = br.read_u32(1)? != 0; + if skiple { + let skipl = br.read_u32(9)?; + br.skip(skipl * 8)?; + } + } + + // ---- bit allocation ---- + for ch in 0..nfchans { + let end = state.channels[ch].end_mant; + audblk::run_bit_allocation( + state, + ch, + 0, + end, + si.fscod, + state.fsnroffst[ch], + state.fgaincod[ch], + false, + ); + } + if cplinu { + let start = state.cpl_begf_mant; + let end = state.cpl_endf_mant; + audblk::run_bit_allocation( + state, + MAX_FBW, + start, + end, + si.fscod, + state.cpl_fsnroffst, + state.cpl_fgaincod, + true, + ); + } + if lfeon { + let lfe_ch = MAX_FBW + 1; + audblk::run_bit_allocation( + state, + lfe_ch, + 0, + 7, + si.fscod, + state.lfefsnroffst, + state.lfefgaincod, + false, + ); + } + + // ---- mantissas ---- + // + // Standard path: walk every (ch, bin) pair reading bap-coded + // mantissas (`audblk::unpack_mantissas`). + // + // AHT path: when chahtinu[ch] == 1, the FIRST audblk that + // would emit channel exponents (i.e. the block where chexpstr + // != REUSE — for AHT-eligible streams that's always block 0) + // reads instead chgaqmod + chgaqgain + 6×nmant AHT mantissas, + // applies the §3.4.5 IDCT-II to recover per-block transform + // coefficients, and caches them in `aht_coeffs[ch][blk][bin]`. + // Subsequent blocks for AHT channels skip the mantissa read + // and load coefficients from the cache. + if audfrm.ahte && (aht_pending.iter().any(|&p| p) || aht_filled.iter().any(|&p| p)) { + // AHT in use for at least one channel in this frame. + // unpack_mixed_mantissas walks per-channel: AHT-active + // channels skip bit reads on blocks 1..5 (their mantissas + // were front-loaded in block 0); standard channels are + // walked as usual via the per-channel scalar fallback. + unpack_mixed_mantissas( + state, + br, + &mut aht_coeffs, + &mut aht_pending, + &mut aht_filled, + nfchans, + lfeon, + cplinu, + )?; + } else { + audblk::unpack_mantissas(state, &ac3_bsi, br)?; + } + // For AHT-active channels, overwrite coeffs[bin] with the + // pre-cached value for THIS block index. The LFE channel + // (`lfe_slot`) is included so an AHT-coded LFE loads its 7 + // per-block coefficients from the cache too (round 113). + for ch in (0..nfchans).chain(lfeon.then_some(lfe_slot)) { + if aht_filled[ch] { + let end = state.channels[ch].end_mant; + for bin in 0..end { + state.channels[ch].coeffs[bin] = aht_coeffs[ch][blk][bin]; + } + // Clear bins past end_mant so stale data can't leak. + for bin in end..N_COEFFS { + state.channels[ch].coeffs[bin] = 0.0; + } + } + } + #[cfg(debug_assertions)] + if std::env::var("EAC3_DUMP_BLK").is_ok() { + let ch0end = state.channels[0].end_mant; + let ch1end = state.channels[1].end_mant; + let c0excpl = state.channels[0].in_coupling; + let c1excpl = state.channels[1].in_coupling; + let cplc00 = state.cpl_coord[0][0]; + let c00 = state.channels[0].coeffs[0]; + let c060 = state.channels[0].coeffs[60]; + let c0133 = state.channels[0].coeffs[133]; + let c10 = state.channels[1].coeffs[0]; + eprintln!( + "DBG blk={blk} cplinu={cplinu} cpl_in_use={} begf_m={} endf_m={} nbnd={} ch0[end={ch0end} excpl={c0excpl}] ch1[end={ch1end} excpl={c1excpl}] cplco0={cplc00:.4} c0[0]={c00:.4} c0[60]={c060:.4} c0[133]={c0133:.4} c1[0]={c10:.4}", + state.cpl_in_use, state.cpl_begf_mant, state.cpl_endf_mant, state.cpl_nbnd, + ); + } + + // Coupling pseudo-channel (round 117): when coupling-AHT is in + // use, load this block's cached coupling coefficients into the + // `MAX_FBW` slot BEFORE `dsp_block` runs §7.4 decouple — the + // decouple step reads `channels[MAX_FBW].coeffs[bin]` and scatters + // it into the fbw channels via the cplco coordinates. The valid + // span is the coupling range `[cpl_begf_mant, cpl_endf_mant)`, not + // the `end_mant` window the fbw/LFE channels use. + if cplinu && aht_filled[MAX_FBW] { + let start = state.cpl_begf_mant; + let end_c = state.cpl_endf_mant.min(N_COEFFS); + for bin in 0..start { + state.channels[MAX_FBW].coeffs[bin] = 0.0; + } + for bin in start..end_c { + state.channels[MAX_FBW].coeffs[bin] = aht_coeffs[MAX_FBW][blk][bin]; + } + for bin in end_c..N_COEFFS { + state.channels[MAX_FBW].coeffs[bin] = 0.0; + } + } + + // ---- DSP (decouple+rematrix+dynrng+IMDCT+overlap-add) ---- + // + // Enhanced coupling (§E.3.5.5) defers per-channel synthesis + DSP + // to a second pass: the §E.3.5.5.1 carrier for block `blk` needs + // the *next* block's enhanced-coupling coefficients, which are not + // decoded until the next loop iteration. When ecpl is active for + // this block we snapshot the de-normalised ecpl-channel + // coefficients (the carrier source) + strategy + coords + the + // coupled-channel set, plus the full per-channel state needed to + // run DSP later, and skip the immediate `dsp_block` + PCM emit. + if ecpl_in_use { + if !frame_has_ecpl { + // First ecpl block of the frame — remember where deferral + // begins so the second pass maps `deferred_channels[i]` back + // to the right absolute block (and PCM offset). Real + // encoders enable enhanced coupling from block 0; the rare + // mid-frame onset is handled by deferring only from here on + // (blocks before this already emitted via the immediate + // path, with their delay lines correctly advanced on + // `state`). + first_deferred_blk = blk; + } + frame_has_ecpl = true; + if let Some(strat) = &ecpl_strategy { + let mut chincpl = [false; super::ecpl::ECPL_MAX_FBW]; + for (ch, slot) in chincpl.iter_mut().enumerate().take(nfchans) { + *slot = state.channels[ch].in_coupling; + } + let mut mant = [0.0f32; 256]; + let start = state.cpl_begf_mant.min(256); + let end_c = state.cpl_endf_mant.min(256); + mant[start..end_c].copy_from_slice(&state.channels[MAX_FBW].coeffs[start..end_c]); + ecpl_blocks[blk] = Some(super::ecpl::EcplBlock { + mant, + strategy: strat.clone(), + coords: ecpl_coords.clone().unwrap_or_default(), + chincpl, + }); + } + } + if frame_has_ecpl { + // Snapshot the full per-channel state for the deferred DSP pass + // so blocks render in order with a correct overlap-add delay + // line (mixing ecpl + non-ecpl blocks within one frame stays + // correct). The `delay` field is intentionally re-threaded in + // pass 2, not from the snapshot. + deferred_channels.push(state.channels.clone()); + deferred_skip_decouple.push(ecpl_in_use); + } else { + audblk::dsp_block(state, &si, &ac3_bsi); + + // Write block PCM into `out`. + let base = blk * SAMPLES_PER_BLOCK * nchans; + for n in 0..SAMPLES_PER_BLOCK { + for ch in 0..nfchans { + let s = state.channels[ch].coeffs[n]; + out[base + n * nchans + ch] = s; + } + if lfeon { + let s = state.channels[MAX_FBW + 1].coeffs[n]; + out[base + n * nchans + nfchans] = s; + } + } + } + } + + // ---- Deferred enhanced-coupling DSP pass (§E.3.5.5) ---- + // + // Runs only when the frame used enhanced coupling. For each block in + // order: reconstruct the carrier from prev/curr/next ecpl coefficients + // (zero buffers at the frame edges per §E.3.5.5.1), synthesise each + // coupled channel's transform coefficients into the snapshot's + // `coeffs`, then run `dsp_block` (with decouple skipped on ecpl blocks + // because the coefficients are already per-channel) and emit PCM. The + // overlap-add delay line is threaded through `state.channels[ch].delay` + // across the pass exactly as the single-pass loop would have. + if frame_has_ecpl { + run_deferred_ecpl_dsp( + state, + &si, + &ac3_bsi, + first_deferred_blk, + &ecpl_blocks, + &deferred_channels, + &deferred_skip_decouple, + nfchans, + nchans, + lfeon, + out, + ); + } + + // ---- Transient pre-noise processing (§E.3.7.2) ---- + // After overlap-add, each fbw channel that carries TPNP data has its + // pre-transient region overwritten with a time-scaled copy of the + // cleaner audio that precedes it, removing the smeared pre-noise a + // low-rate transform coder leaves ahead of a sharp onset. LFE never + // carries TPNP. Operates in place on the interleaved `out` buffer. + if audfrm.transproce { + let total_samples = num_blocks * SAMPLES_PER_BLOCK; + for ch in 0..nfchans { + if !audfrm.chintransproc[ch] { + continue; + } + apply_transient_prenoise( + out, + nchans, + ch, + total_samples, + audfrm.transprocloc[ch], + audfrm.transproclen[ch], + ); + } + } + Ok(()) +} + +/// §E.3.5.5 — the deferred enhanced-coupling DSP second pass. +/// +/// Re-renders every audio block of a frame that used enhanced coupling, in +/// order, so the §E.3.5.5.1 carrier reconstruction can consult the *next* +/// block's enhanced-coupling coefficients (unavailable mid-decode). For +/// each block: +/// +/// 1. The block's snapshot channel-state is restored into `state.channels`, +/// preserving the evolving overlap-add `delay` lines (carried from the +/// previous block's DSP, not from the snapshot). +/// 2. For an enhanced-coupling block, [`super::ecpl::synthesize_block`] +/// reconstructs the carrier from the previous / current / next block's +/// de-normalised coefficients (zero buffers at the frame edges, per the +/// §E.3.5.5.1 "set to zero" rule) and writes each coupled channel's +/// transform coefficients; `skip_decouple` is set so `dsp_block` does +/// not run the standard §7.4 scalar decouple over them. +/// 3. `dsp_block` runs the remaining stages (rematrix / SPX / dynrng / +/// IMDCT / overlap-add) and the PCM is emitted to `out`. +/// +/// The "previous block" of frame block 0 is the *last* block of the prior +/// frame (block numbering is continuous across the stream). When that block +/// used enhanced coupling its de-normalised mantissa buffer is carried over +/// on [`super::ecpl::EcplState`] and used as block 0's `prev`; otherwise the +/// §E.3.5.5.1 "set to zero" rule applies and a zero buffer is used. This +/// frame's final enhanced-coupling block is in turn recorded for the next +/// frame. The "next block" of the frame's last block lives in a frame not +/// yet decoded, so it remains zero (true streaming lookahead is out of +/// scope). +#[allow(clippy::too_many_arguments)] +fn run_deferred_ecpl_dsp( + state: &mut Ac3State, + si: &SyncInfo, + ac3_bsi: &Ac3Bsi, + first_deferred_blk: usize, + ecpl_blocks: &[Option], + deferred_channels: &[[crate::audblk::ChannelState; crate::audblk::MAX_CHANNELS]], + deferred_skip_decouple: &[bool], + nfchans: usize, + nchans: usize, + lfeon: bool, + out: &mut [f32], +) { + let n_deferred = deferred_channels.len(); + // Zero neighbour for frame-edge blocks (§E.3.5.5.1 "set to zero"). + let zero_block = super::ecpl::EcplBlock { + mant: [0.0; 256], + strategy: super::ecpl::EcplStrategy { + ecplbegf: 0, + begin_subbnd: 0, + end_subbnd: 0, + bndstrc: [false; super::ecpl::N_ECPL_SUBBND], + necplbnd: 0, + }, + coords: super::ecpl::EcplCoords::default(), + chincpl: [false; super::ecpl::ECPL_MAX_FBW], + }; + + // Cross-frame "previous block" for frame block 0: the prior frame's last + // enhanced-coupling block, carried over on `EcplState`. Only its mantissa + // buffer is consulted by the carrier (strategy/coords/chincpl come from + // `curr`), so wrap the carried spectrum in an otherwise-zero block. When + // the prior frame had no trailing enhanced coupling this stays the zero + // block (the §E.3.5.5.1 boundary case). + let mut prev_frame_block = zero_block.clone(); + if let Some(carried) = state.ecpl_state.prev_frame_last_mant() { + prev_frame_block.mant = *carried; + } + + for i in 0..n_deferred { + // Snapshot index `i` maps to absolute block `abs_blk`. + let abs_blk = first_deferred_blk + i; + // Restore this block's parsed state. For the first deferred block + // the snapshot's own `delay` is the correct starting overlap-add + // tail (captured before this block's DSP ran). For later blocks the + // live delay produced by the previous block's `dsp_block` is the + // correct one — carry it over the snapshot's stale value. + let carry_delay: [[f32; SAMPLES_PER_BLOCK]; crate::audblk::MAX_CHANNELS] = + std::array::from_fn(|ch| state.channels[ch].delay); + state.channels = deferred_channels[i].clone(); + if i > 0 { + for ch in 0..crate::audblk::MAX_CHANNELS { + state.channels[ch].delay = carry_delay[ch]; + } + } + state.blkidx = abs_blk; + + let skip = deferred_skip_decouple[i]; + state.skip_decouple = skip; + if skip { + if let Some(curr) = &ecpl_blocks[abs_blk] { + let prev = if abs_blk > 0 { + ecpl_blocks[abs_blk - 1].as_ref().unwrap_or(&zero_block) + } else { + // Frame block 0 — its "previous block" is the prior + // frame's last enhanced-coupling block (§E.3.5.5.1). + &prev_frame_block + }; + let next = if abs_blk + 1 < ecpl_blocks.len() { + ecpl_blocks[abs_blk + 1].as_ref().unwrap_or(&zero_block) + } else { + &zero_block + }; + // Per-channel transform-coefficient buffers; pre-seed with + // the already-decoded independent (low-frequency) region so + // synthesis only overwrites the enhanced-coupling bins. + let mut chcoef: Vec<[f32; 256]> = (0..super::ecpl::ECPL_MAX_FBW) + .map(|ch| { + let mut b = [0.0f32; 256]; + if ch < nfchans { + b.copy_from_slice(&state.channels[ch].coeffs); + } + b + }) + .collect(); + super::ecpl::synthesize_block( + &mut state.ecpl_state, + prev, + curr, + next, + &mut chcoef, + 512, + ); + for ch in 0..nfchans { + state.channels[ch].coeffs.copy_from_slice(&chcoef[ch]); + } + } + } + + audblk::dsp_block(state, si, ac3_bsi); + state.skip_decouple = false; + + let base = abs_blk * SAMPLES_PER_BLOCK * nchans; + for n in 0..SAMPLES_PER_BLOCK { + for ch in 0..nfchans { + out[base + n * nchans + ch] = state.channels[ch].coeffs[n]; + } + if lfeon { + out[base + n * nchans + nfchans] = state.channels[MAX_FBW + 1].coeffs[n]; + } + } + } + + // Carry this frame's last block's enhanced-coupling spectrum to the next + // frame so its block 0 carrier can use it as the "previous block" + // (§E.3.5.5.1). When the final frame block did not use enhanced coupling + // the carry resets to `None` (the spec's "set to zero" boundary case). + let last_mant = ecpl_blocks.last().and_then(|b| b.as_ref()).map(|b| b.mant); + state.ecpl_state.set_prev_frame_last_mant(last_mant); +} + +/// Transient pre-noise time-scaling synthesis for one full-bandwidth +/// channel (§E.3.7.2). Operates in place on the interleaved f32 frame +/// buffer `out` (stride `nchans`, channel slot `ch`). +/// +/// The encoder transmits, relative to the first decoded PCM sample of +/// the frame, the transient location `transprocloc` (in 4-sample units; +/// multiply by 4) and the time-scaling length `transproclen` (samples). +/// The decoder reconstructs the pre-transient region from a synthesis +/// buffer copied from earlier (cleaner) audio and cross-fades it over +/// the noisy original per the spec pseudo-code: +/// +/// ```text +/// transloc = 4 * transprocloc +/// translen = transproclen +/// pnlen = transloc - aud_blk_samp_loc // pre-noise length +/// tot_corr_len = pnlen + translen + TC1 +/// synth_buf[s] = pcm_out[transloc - (2*TC1 + 2*pnlen) + s] // 0..2*TC1+pnlen +/// start_samp = transloc - tot_corr_len +/// [start .. start+TC1) : fade out original, fade in synth +/// [start+TC1 .. start+corr-TC2) : overwrite with synth +/// [start+corr-TC2 .. start+corr) : fade in original, fade out synth +/// ``` +/// +/// `TC1 = 256`, `TC2 = 128` are the spec's fixed time-scaling constants. +/// `aud_blk_samp_loc` is the first-sample index of the 256-sample audio +/// block that contains the transient — the decoder derives it directly +/// (the block boundary at or below `transloc`). +/// +/// Cross-fades use complementary Hann windows (§E.3.7.2 permits "nearly +/// any pair of constant-amplitude cross-fade windows"; Hann is the +/// spec's recommended choice). Reads that fall before the start of the +/// frame buffer (the spec allows a frame-N transient to reference +/// frame-(N-1) tail samples — §E.3.7.1) are clamped to index 0, the +/// conservative single-frame behaviour; a future round can thread the +/// previous frame's tail through `Eac3DecoderState` for the exact +/// cross-frame case. +fn apply_transient_prenoise( + out: &mut [f32], + nchans: usize, + ch: usize, + total_samples: usize, + transprocloc: u16, + transproclen: u16, +) { + const TC1: usize = 256; + const TC2: usize = 128; + + let transloc = 4 * transprocloc as usize; + let translen = transproclen as usize; + // A transient at/after the frame end (or a degenerate zero location) + // leaves nothing to correct. + if transloc == 0 || transloc >= total_samples { + return; + } + // First sample of the 256-sample audio block containing the transient. + let aud_blk_samp_loc = (transloc / SAMPLES_PER_BLOCK) * SAMPLES_PER_BLOCK; + let pnlen = transloc.saturating_sub(aud_blk_samp_loc); + if pnlen == 0 { + // Transient sits exactly on a block boundary → no pre-noise gap. + return; + } + let tot_corr_len = pnlen + translen + TC1; + let synth_len = 2 * TC1 + pnlen; + + // Build the synthesis buffer from earlier PCM. `src0` is the first + // source index; the spec uses `transloc - (2*TC1 + 2*pnlen)`. When + // that is negative the samples come from the previous frame — clamp + // to 0 (single-frame conservative path). + let want_src0 = transloc as isize - (2 * TC1 + 2 * pnlen) as isize; + let read = |samp_idx: isize| -> f32 { + let idx = samp_idx.max(0) as usize; + if idx < total_samples { + out[idx * nchans + ch] + } else { + 0.0 + } + }; + let mut synth_buf = vec![0.0f32; synth_len]; + for (s, slot) in synth_buf.iter_mut().enumerate() { + *slot = read(want_src0 + s as isize); + } + + // start_samp = transloc - tot_corr_len. Clamp the overwrite window to + // the valid buffer range so cross-frame underflow never panics. + let start_isize = transloc as isize - tot_corr_len as isize; + + // Complementary Hann cross-fade windows. + let hann_in = |i: usize, len: usize| -> f32 { + if len <= 1 { + return 1.0; + } + let x = std::f32::consts::PI * i as f32 / len as f32; + 0.5 - 0.5 * x.cos() + }; + + // Region 1: [start .. start+TC1) — fade out original, fade in synth. + for s in 0..TC1.min(tot_corr_len) { + let dst = start_isize + s as isize; + if dst < 0 || dst as usize >= total_samples { + continue; + } + let fi = hann_in(s, TC1); + let fo = 1.0 - fi; + let orig = out[dst as usize * nchans + ch]; + out[dst as usize * nchans + ch] = orig * fo + synth_buf[s] * fi; + } + // Region 2: [start+TC1 .. start+corr-TC2) — full synth overwrite. + let r2_end = tot_corr_len.saturating_sub(TC2); + for s in TC1..r2_end { + let dst = start_isize + s as isize; + if dst < 0 || dst as usize >= total_samples || s >= synth_len { + continue; + } + out[dst as usize * nchans + ch] = synth_buf[s]; + } + // Region 3: [start+corr-TC2 .. start+corr) — fade in original, fade + // out synth. + for (j, s) in (r2_end..tot_corr_len).enumerate() { + let dst = start_isize + s as isize; + if dst < 0 || dst as usize >= total_samples || s >= synth_len { + continue; + } + let fi = hann_in(j, TC2); + let fo = 1.0 - fi; + let orig = out[dst as usize * nchans + ch]; + out[dst as usize * nchans + ch] = orig * fi + synth_buf[s] * fo; + } +} + +/// AHT-aware mantissa unpacker. +/// +/// Mirrors [`audblk::unpack_mantissas`] but routes per-channel reads +/// through the AHT path when `aht_pending[ch] == true`. For AHT-active +/// channels we read 6×nmant mantissas + GAQ side info, dequantise via +/// VQ (Tables E4.1..E4.7) or scalar/GAQ (Table E3.5), apply the +/// §3.4.5 inverse DCT-II to recover per-block coefficients, multiply +/// by `2^-exp`, and cache the per-block coefficients in +/// `aht_coeffs[ch][blk][bin]` for the per-block dispatch loop above. +/// +/// Coupling AHT (`cplahtinu`, round 117) **is** handled: the coupling +/// pseudo-channel slot `MAX_FBW` is read interleaved INSIDE the fbw +/// channel loop, right after the first coupled channel's mantissas (the +/// `got_cplchan` gate, matching Table E1.4). When `cplahtinu == 1` the +/// front-loaded coupling-AHT block (`cplgaqmod` + gains + 6×ncplmant + +/// IDCT) fills the cache over `[cpl_begf_mant, cpl_endf_mant)`; when +/// `cplahtinu == 0` the standard coupling mantissas are read there +/// instead (never dithered, §7.3.4 para 1). The LFE channel **is** +/// handled (round 113): after the fbw loop, slot `MAX_FBW + 1` runs +/// either the standard 7-mantissa LFE read (`lfeahtinu == 0`) or the +/// front-loaded LFE-AHT block (`lfeahtinu == 1`, `aht_pending[lfe] == +/// true`), matching the §E.1.3.2 `if(lfeon)` tail of the audblk loop. +/// +/// Multichannel note (round 110): the non-AHT (standard scalar) channels +/// share the bap-1/2/4 triplet/pair grouping buffers across channels in +/// frequency-then-channel order, exactly as the base AC-3 +/// [`audblk::unpack_mantissas`] does — a started bap=1 group is consumed +/// by the next bap=1 mantissa even if it belongs to a later channel. +/// AHT channels read their mantissas in a separate front-loaded block, so +/// they never touch these shared buffers; the grouping threads only +/// across the standard channels present in this audblk's mantissa stream. +/// The standard LFE read shares the same grouping buffers (the base path +/// also threads LFE bap-1/2/4 mantissas through the fbw groups). +#[allow(clippy::too_many_arguments)] +fn unpack_mixed_mantissas( + state: &mut Ac3State, + br: &mut BitReader<'_>, + aht_coeffs: &mut [[[f32; N_COEFFS]; AHT_BLOCKS]], + aht_pending: &mut [bool], + aht_filled: &mut [bool], + nfchans: usize, + lfeon: bool, + cplinu: bool, +) -> Result<()> { + // Clear per-block transform-coefficient state for every channel — + // standard mantissas overwrite bins 0..end_mant, AHT mantissas + // populate via the cache below; bins outside those ranges must + // read as zero (matches the base AC-3 unpacker). + for ch in 0..crate::audblk::MAX_CHANNELS { + for v in state.channels[ch].coeffs.iter_mut() { + *v = 0.0; + } + } + + // Shared bap-1/2/4 grouping buffers, threaded across every standard + // (non-AHT) channel in this audblk — see the function docstring. + // Declared once outside the channel loop so a triplet/pair started by + // one channel is consumed by the next channel that needs it, matching + // base AC-3 [`audblk::unpack_mantissas`]. + let mut grp1: [f32; 3] = [0.0; 3]; + let mut grp1_n = 0usize; + let mut grp2: [f32; 3] = [0.0; 3]; + let mut grp2_n = 0usize; + let mut grp4: [f32; 2] = [0.0; 2]; + let mut grp4_n = 0usize; + + // Standard channels (and the AHT-skip blocks for AHT channels) + // pull from the bit stream; AHT channels on their FIRST appearance + // pull the mantissa block and IDCT it. We walk channels in order + // (matching the spec's `for ch in 0..nfchans` loop) so the bit + // cursor advances in the same order whether AHT is in use or not. + // + // The coupling-channel mantissas (standard or AHT) are read + // interleaved INSIDE this loop, right after the FIRST coupled + // channel's mantissas, gated by `got_cplchan` — exactly as the base + // AC-3 [`audblk::unpack_mantissas`] does (Table E1.4: + // `if(cplinu[blk] && chincpl[ch] && !got_cplchan)`). + let cpl = MAX_FBW; + let mut got_cplchan = false; + for ch in 0..nfchans { + let end = state.channels[ch].end_mant; + if aht_filled[ch] { + // AHT cache populated on a prior block — no bits to read + // here. The per-block dispatch loop in + // `decode_indep_audblks` will load coefficients from + // `aht_coeffs[ch][blk]` after this function returns. + } else if aht_pending[ch] { + // First AHT-active block for this channel — read GAQ side + // info + 6×nmant mantissas + IDCT into the coefficient + // cache. AHT reads a self-contained VQ/GAQ codeword stream + // and never touches the shared grouping buffers above. + let snroffset = + (((state.snroffst_coarse as i32 - 15) << 4) + state.fsnroffst[ch] as i32) << 2; + decode_aht_channel_mantissas(state, ch, 0, end, snroffset, br, &mut aht_coeffs[ch])?; + aht_filled[ch] = true; + aht_pending[ch] = false; + } else { + // Standard scalar mantissa path — uses the canonical base-AC-3 + // `fetch_mantissa` so bap-1/2/4 grouping shares the buffers above + // across all standard channels (§7.3.5) and bap=0 dither matches + // the base path's LFSR (§7.3.4). + let dith = state.channels[ch].dithflag; + for bin in 0..end { + let bap = state.channels[ch].bap[bin]; + let val = audblk::fetch_mantissa( + br, + bap, + &mut grp1, + &mut grp1_n, + &mut grp2, + &mut grp2_n, + &mut grp4, + &mut grp4_n, + false, + )?; + let final_val = if bap == 0 && dith { + audblk::dither_lfsr(&mut state.dither_lfsr_state) + } else { + val + }; + let e = state.channels[ch].exp[bin] as i32; + state.channels[ch].coeffs[bin] = final_val * 2f32.powi(-e); + } + } + + // ---- coupling-channel mantissas (Table E1.4) ---- + // Read once per block, immediately after the first coupled + // channel, before moving on to later channels. Both the standard + // (`cplahtinu == 0`) and AHT (`cplahtinu == 1`, round 117) + // branches land here so the bit cursor stays aligned. + if cplinu && state.channels[ch].in_coupling && !got_cplchan { + got_cplchan = true; + let start = state.cpl_begf_mant; + let end_c = state.cpl_endf_mant; + if aht_filled[cpl] { + // Coupling-AHT cache populated on a prior block — no bits. + } else if aht_pending[cpl] { + // First (and only) coupling-AHT block this frame: §3.4.3.1 + // hebap masking uses the coupling fine-SNR offset + // (`cpl_fsnroffst`). The bins span the coupling range + // [cpl_begf_mant, cpl_endf_mant); the cache is loaded into + // the cpl pseudo-channel slot for the per-block decouple + // step in `dsp_block`. + let snroffset = + (((state.snroffst_coarse as i32 - 15) << 4) + state.cpl_fsnroffst as i32) << 2; + decode_aht_channel_mantissas( + state, + cpl, + start, + end_c, + snroffset, + br, + &mut aht_coeffs[cpl], + )?; + aht_filled[cpl] = true; + aht_pending[cpl] = false; + } else { + // Standard coupling-channel read. Coupling mantissas are + // never dithered (§7.3.4 para 1: dither is applied after a + // channel is extracted from the coupling channel), so the + // bap=0 LFSR substitution is skipped here. + for bin in start..end_c { + let bap = state.channels[cpl].bap[bin]; + let val = audblk::fetch_mantissa( + br, + bap, + &mut grp1, + &mut grp1_n, + &mut grp2, + &mut grp2_n, + &mut grp4, + &mut grp4_n, + false, + )?; + let e = state.channels[cpl].exp[bin] as i32; + state.channels[cpl].coeffs[bin] = val * 2f32.powi(-e); + } + } + } + } + + // ---- LFE channel (§E.1.3.2 `if(lfeon)` mantissa tail) ---- + // + // LFE mantissas follow all fbw (+ coupling) mantissas in the audblk + // bit stream. The base AC-3 `unpack_mantissas` reads them here too; + // the round-110 mixed path skipped this entirely, so any AHT frame + // carrying an LFE channel desynced the bit cursor. Round 113 wires + // both LFE branches: + // * `lfeahtinu == 0` (or AHT not in use for LFE): standard 7-bin + // read sharing the fbw bap-1/2/4 grouping buffers (§7.3.5). + // * `lfeahtinu == 1` (`aht_pending[lfe_slot]`): the front-loaded + // LFE-AHT block — `lfegaqmod` + gains + 6×7 mantissas + IDCT, + // cached for the per-block dispatch loop. + if lfeon { + let lfe = MAX_FBW + 1; + let end = state.channels[lfe].end_mant; // 7 per §5.4.3.63 + if aht_filled[lfe] { + // LFE-AHT cache populated on a prior block — no bits to read. + } else if aht_pending[lfe] { + // First (and only) LFE-AHT block: §3.4.3.1 hebap masking uses + // the LFE fine-SNR offset (`lfefsnroffst`) in place of the + // per-channel `fsnroffst[ch]`. + let snroffset = + (((state.snroffst_coarse as i32 - 15) << 4) + state.lfefsnroffst as i32) << 2; + decode_aht_channel_mantissas(state, lfe, 0, end, snroffset, br, &mut aht_coeffs[lfe])?; + aht_filled[lfe] = true; + aht_pending[lfe] = false; + } else { + // Standard LFE mantissa read — shares the fbw grouping buffers + // exactly as the base AC-3 path does. LFE never participates in + // coupling, so there is no coupling-channel read interleaved. + let dith = state.channels[lfe].dithflag; + for bin in 0..end { + let bap = state.channels[lfe].bap[bin]; + let val = audblk::fetch_mantissa( + br, + bap, + &mut grp1, + &mut grp1_n, + &mut grp2, + &mut grp2_n, + &mut grp4, + &mut grp4_n, + false, + )?; + let final_val = if bap == 0 && dith { + audblk::dither_lfsr(&mut state.dither_lfsr_state) + } else { + val + }; + let e = state.channels[lfe].exp[bin] as i32; + state.channels[lfe].coeffs[bin] = final_val * 2f32.powi(-e); + } + } + } + Ok(()) +} + +/// Decode the AHT mantissa block for one channel (fbw or LFE) and fill +/// its 6×N coefficient cache. Per §3.4 / §3.4.4 / §3.4.5 / §3.4.4.2. +/// +/// 1. **hebap** — per-bin high-efficiency bap, derived in the §3.4.3.1 +/// pseudo-code from psd/mask. Reuses the masking curve already +/// computed by [`audblk::run_bit_allocation`] (so we walk +/// `state.channels[ch].psd`/`mask` rather than re-derive them). The +/// masking `snroffset` for this channel is passed in (`gaqmod`'s +/// siblings `chgaqmod` / `lfegaqmod` differ only by which fine-SNR +/// offset feeds the mask — `state.fsnroffst[ch]` for fbw, +/// `state.lfefsnroffst` for LFE), so this routine serves both. +/// 2. **gaqmod** (`chgaqmod` / `lfegaqmod`): 2 bits. +/// 3. **gaqbin[bin]**: derived from hebap (Table E3.3 logic). +/// 4. **gaqgain[n]** for `n in 0..gaqsections`: 1 or 5 bits each +/// depending on gaqmod (mode 3 packs 3 gains in 5 bits). +/// 5. **mantissas**: per bin, per AHT-block (j in 0..6): +/// * `hebap == 0` → mantissa = 0 (zero bin). +/// * `1 <= hebap <= 7` → 6-element VQ codeword shared across all 6 j's. +/// * `hebap >= 8` → scalar/GAQ per j (with optional gain word). +/// 6. **IDCT-II §3.4.5** → reconstruct per-block C(k, m) from X(k, j). +/// 7. **`coeff = mant · 2^-exp`** stored in `cache[blk][bin]`. +fn decode_aht_channel_mantissas( + state: &mut Ac3State, + ch: usize, + start: usize, + end: usize, + snroffset: i32, + br: &mut BitReader<'_>, + cache: &mut [[f32; N_COEFFS]; AHT_BLOCKS], +) -> Result<()> { + // ---- 1. derive hebap[] from psd / mask via §3.4.3.1 ---- + // + // The masking curve `state.channels[ch].mask` is in the spec's + // banded representation (50 entries indexed by masktab[bin]). We + // reproduce the per-band post-processing inline (`mask_after_floor`) + // so our hebap lookup matches the encoder's choice exactly. The + // mantissa range is `[start, end)`: fbw/LFE channels start at bin 0, + // the coupling pseudo-channel (round 117) starts at `cpl_begf_mant` + // and ends at `cpl_endf_mant`. `hebap` is indexed by absolute bin so + // it lines up with `psd`/`exp`/the per-block coefficient cache. + let mut hebap = vec![0u8; end.max(1)]; + if start < end { + use crate::tables::{BNDSZ, BNDTAB, FLOORTAB, MASKTAB}; + let floor = FLOORTAB[state.floorcod as usize]; + let mut i = start; + let mut j = MASKTAB[start] as usize; + while i < end { + let lastbin = (BNDTAB[j] as usize + BNDSZ[j] as usize).min(end); + let mut m = state.channels[ch].mask[j] as i32; + m -= snroffset; + m -= floor; + if m < 0 { + m = 0; + } + m &= 0x1fe0; + m += floor; + while i < lastbin { + hebap[i] = aht::hebap_from_address(state.channels[ch].psd[i], m); + i += 1; + } + j += 1; + } + } + + // ---- 2. gaqmod (chgaqmod / cplgaqmod / lfegaqmod, 2 bits) ---- + let gaqmod = br.read_u32(2)? as u8; + + // ---- 3. compute gaqbin[bin] (Table E3.3 logic) ---- + // `fill_gaqbin` walks `hebap[..]` from index 0; bins below `start` + // have `hebap == 0` (left zero above) so they classify as non-GAQ + // and never consume a gain word, matching the spec's + // `for(bin = cplstrtmant; bin < cplendmant; ...)` GAQ-active scan. + let mut gaqbin = vec![0i8; end.max(1)]; + let active = aht::fill_gaqbin(&hebap, gaqmod, &mut gaqbin); + + // ---- 4. read gaqgain[n] for nsections sections ---- + let nsections = aht::gaq_sections(gaqmod, active); + let mut gain_words = vec![0u8; active]; + aht::read_gaq_gains(br, gaqmod, nsections, &mut gain_words)?; + + // ---- 5/6/7. per-bin mantissa decode + IDCT + scale ---- + let mut gain_iter = gain_words.into_iter(); + let mut x = [0.0f32; 6]; + for bin in start..end { + let h = hebap[bin]; + if h == 0 { + // Zero-mantissa bin — coefficients are 0 across all blocks. + for blk in 0..AHT_BLOCKS { + cache[blk][bin] = 0.0; + } + continue; + } + if (1..=7).contains(&h) { + // VQ regime — single codeword shared across the 6 AHT blocks. + let nb = aht::VQ_BITS[h as usize] as u32; + let idx = br.read_u32(nb)? as usize; + x = aht::vq_lookup(h, idx); + } else { + // Scalar / GAQ regime. + let gain_code = if gaqbin[bin] == 1 { + gain_iter.next().unwrap_or(0) + } else { + 0 + }; + aht::read_scalar_aht_mantissas(br, h, gaqmod, gaqbin[bin], gain_code, &mut x)?; + } + // Inverse DCT-II to recover per-block C(k, m) (§3.4.5). + let c = aht::idct_ii_6(x); + // `coeff = mantissa · 2^(-exp)` for each block. + let exp = state.channels[ch].exp[bin] as i32; + let scale = 2f32.powi(-exp); + for blk in 0..AHT_BLOCKS { + cache[blk][bin] = c[blk] * scale; + } + } + + Ok(()) +} + +/// Compute the §3.4.2 AHT helper variables `nchregs[ch]` / `ncplregs` / +/// `nlferegs` from the per-block exponent strategies already parsed onto +/// `AudFrm`. Each variable counts the number of audio blocks in the +/// 6-block frame that transmit fresh exponents for that channel (i.e. a +/// strategy other than REUSE); coupling additionally counts blocks that +/// re-declare the coupling strategy (`cplstre[blk] == 1`). +/// +/// These are NOT in the bitstream — the spec derives them so the decoder +/// knows which `chahtinu` / `cplahtinu` / `lfeahtinu` presence bits the +/// `audfrm()` AHT block actually emitted (a flag is only present when its +/// regs count is exactly 1, meaning exponents are sent once per frame and +/// the channel is AHT-eligible). All inputs (`chexpstr_blk_ch`, +/// `cplexpstr_blk`, `cplstre_blk`, `lfeexpstr`) are filled by +/// `audfrm::parse_with` for both the `expstre == 1` and `expstre == 0` +/// (Table E2.10) paths, so no real audblk pre-walk is required. +fn compute_aht_regs(audfrm: &AudFrm, bsi: &Eac3Bsi) -> AhtRegsHints { + const REUSE: u8 = 0; + let nfchans = (bsi.nfchans as usize).min(MAX_FBW); + + // nchregs[ch] — §3.4.2: count blocks where chexpstr[blk][ch] != reuse. + let mut nchregs = [0u8; MAX_FBW]; + for (ch, regs) in nchregs.iter_mut().enumerate().take(nfchans) { + let mut n = 0u8; + for blk in 0..AHT_BLOCKS { + if audfrm.chexpstr_blk_ch[blk][ch] != REUSE { + n += 1; + } + } + *regs = n; + } + + // ncplregs — §3.4.2: only meaningful when coupling is in use for all + // 6 blocks (the AHT eligibility gate also checks `ncplblks == 6`). + // Count blocks where cplstre[blk] == 1 OR cplexpstr[blk] != reuse. + let mut ncplregs = 0u8; + for blk in 0..AHT_BLOCKS { + if audfrm.cplstre_blk[blk] || audfrm.cplexpstr_blk[blk] != REUSE { + ncplregs += 1; + } + } + + // nlferegs — §3.4.2: count blocks where lfeexpstr[blk] != reuse. + let mut nlferegs = 0u8; + if bsi.lfeon { + for blk in 0..AHT_BLOCKS { + if audfrm.lfeexpstr[blk] != REUSE { + nlferegs += 1; + } + } + } + + AhtRegsHints { + nchregs, + ncplregs, + nlferegs, + } +} + +/// Decide whether the round-2 DSP path can handle this frame. +fn reject_unsupported(bsi: &Eac3Bsi, audfrm: &AudFrm) -> Result<()> { + // expstre handling — both per-block (expstre==1) and frame-based + // (expstre==0) strategies are supported. Round 72 (this commit) + // landed the frame-based path: `audfrm::parse_with` expands the + // 5-bit `frmcplexpstr` + per-channel `frmchexpstr[ch]` codewords + // via Table E2.10 into `cplexpstr_blk[]` + `chexpstr_blk_ch[]` so + // the dsp body sees the same per-block-per-channel shape it + // already consumes for the expstre==1 case. Every validator-produced + // E-AC-3 fixture in the corpus picks expstre==0. + // snroffststr handling — all three strategies are supported. `0` + // uses the single frame-level (frmcsnroffst, frmfsnroffst) pair from + // audfrm; `0x1` / `0x2` read per-block SNR offsets out of each audblk + // (`snroffste` + `csnroffst` + the shared/per-channel fine offsets) + // per the Annex E audblk §2.3.3.27 syntax. See the SNR-offset block + // in `decode_indep_audblks`. + // Transient pre-noise processing (`transproce`) is no longer a + // whole-frame reject: the per-channel time-scaling synthesis runs in + // `apply_transient_prenoise` after overlap-add (§E.3.7.2). The + // baseband decode is unaffected by TPNP — it is a PCM-domain quality + // enhancement layered on top of already-valid samples. + // + // Spectral-extension attenuation (`spxattene`) is no longer a + // whole-frame reject either: the per-channel `chinspxatten[ch]` + + // `spxattencod[ch]` fields propagate onto state at the top of + // `decode_indep_audblks`, and `audblk::apply_spectral_extension` + // applies the §3.6.4.2.3 5-tap border notch filter when the flag + // is set for a channel. When `spxattene == 0` (every validator- + // encoded E-AC-3 fixture in the corpus carries this) the SPX + // synthesis path is byte-identical to the round-100 implementation. + // ahte is now handled by the round-6 phase-B path (mono-only). + // Defensive: reject any case where phase B was supposed to run + // but didn't get a chance (caller forgot to call parse_phase_b). + if audfrm.aht_phase_b_pending { + return Err(Error::invalid( + "eac3 dsp: audfrm phase-B AHT bits not consumed — caller must \ + invoke audfrm::parse_phase_b before decode_indep_audblks", + )); + } + if bsi.frmsiz == 0 { + return Err(Error::invalid("eac3 dsp: frmsiz=0 (would be 2-byte frame)")); + } + Ok(()) +} + +/// Build a synthetic [`Ac3Bsi`] from the parsed [`Eac3Bsi`] so the +/// AC-3 helpers (which read `bsi.nfchans`/`bsi.nchans`/`bsi.lfeon`) see +/// the shape they expect. +fn build_ac3_bsi_shim(bsi: &Eac3Bsi) -> Ac3Bsi { + Ac3Bsi { + bsid: bsi.bsid, + bsmod: 0, + acmod: bsi.acmod, + nfchans: bsi.nfchans, + lfeon: bsi.lfeon, + nchans: bsi.nchans, + dialnorm: bsi.dialnorm, + dialnorm_ch2: bsi.dialnorm_ch2, + // Annex E (E-AC-3) removes the base §5.4.2.4-5 2-bit + // `cmixlev` / `surmixlev` slots in favour of the refined + // 3-bit `ltrtcmixlev` / `lorocmixlev` / `ltrtsurmixlev` / + // `lorosurmixlev` codewords carried in the `mixmdata` block. + // The shim therefore hands the base helpers the "absent" + // sentinel `0xFF` plus the `None` typed surface unconditionally; + // any consumer that wants the refined coefficients should consult + // the Annex E `annex_d_mix_levels` instead. + cmixlev: 0xFF, + center_mix: None, + surmixlev: 0xFF, + surround_mix: None, + dsurmod: 0xFF, + // Forward the Annex E informational-metadata Dolby Surround mode + // (§E.2.3.1.x reusing §5.4.2.6 / Table 5.11) when present so the + // base AC-3 downmix helpers can consult the matrix-encode hint + // through the shim; `None` when the upstream BSI did not surface + // it (`infomdate == 0` or `acmod != 2`). + dolby_surround_mode: bsi.dolby_surround_mode, + annex_d_mix_levels: None, + dmixmod: 0xFF, + // Forward the Annex E mixmdata preferred stereo downmix mode + // (§E.1.2.2 reusing Annex D §2.3.1.2 / Table D2.2) when + // present so the base AC-3 downmix helpers can consult the + // hint through the shim; `None` short-circuits to the spec + // default branch. + dmixmod_preference: bsi.dmixmod_preference, + compr: bsi.compr, + compr_ch2: bsi.compr_ch2, + // Annex E does not carry a §5.4.2.11-12 `langcod` slot — the + // E-AC-3 BSI does not have a deprecated language-code field — + // so the shim hands the base helpers `None` unconditionally. + language_code: None, + language_code_ch2: None, + dsurexmod: None, + dheadphonmod: None, + adconvtyp: None, + // Annex E never carries the §2.3.1.11-12 reserved trailer — the + // E-AC-3 BSI does not have an `xbsi2e` block at all — so the + // shim hands the base helpers `None` unconditionally. + extra_bsi: None, + audio_production: None, + audio_production_ch2: None, + timecod1: None, + timecod2: None, + timecode_presence: crate::bsi::TimeCodePresence::NotPresent, + // Forward the Annex E informational-metadata `copyrightb` / + // `origbs` pair when present; default to the encoder-default + // unset pair (no policy hint) when the upstream BSI did not + // surface them (`infomdate == 0`). + copyright_info: bsi + .copyright_info + .unwrap_or(crate::bsi::CopyrightInfo::from_bits(false, false)), + // Forward the Annex E `addbsi` payload (or leave `None` when the + // upstream substream did not carry one) so downstream callers + // that route through the shim still observe the chain hint. + addbsi: bsi.addbsi.clone(), + bits_consumed: 0, + } +} + +/// Build a [`SyncInfo`] shim — only `fscod` is consumed downstream. +fn build_syncinfo_shim(bsi: &Eac3Bsi) -> SyncInfo { + let fscod_for_ba = match bsi.sample_rate { + 48_000 => 0, + 44_100 => 1, + 32_000 => 2, + // Reduced-rate (24/22.05/16 kHz) — round 2 maps these to the + // closest base-AC-3 fscod for the masking-curve table HTH lookup. + // §E.2.2.4 says "the masking model uses the (fscod, fscod2) + // pair to index a doubled-row HTH"; we approximate with the + // closest non-reduced row. PSNR will be a bit off on reduced + // streams; round-3 follow-up to fix. + 24_000 | 22_050 | 16_000 => 2, + _ => 0, + }; + SyncInfo { + crc1: 0, + fscod: fscod_for_ba, + frmsizecod: 0, + sample_rate: bsi.sample_rate, + frame_length: bsi.frame_bytes, + } +} + +#[cfg(test)] +mod tpnp_tests { + use super::*; + + // Mono helper: build an interleaved (stride 1) frame buffer. + fn frame(samples: usize) -> Vec { + vec![0.0; samples] + } + + /// A transient at or past the frame end is a no-op (nothing to + /// correct ahead of it within this frame). + #[test] + fn transient_at_or_after_frame_end_is_noop() { + let total = 6 * SAMPLES_PER_BLOCK; // 1536 + let mut buf = frame(total); + for (i, v) in buf.iter_mut().enumerate() { + *v = i as f32; + } + let before = buf.clone(); + // transprocloc * 4 == total → transient at the frame end. + apply_transient_prenoise(&mut buf, 1, 0, total, (total / 4) as u16, 50); + assert_eq!(buf, before, "transient at frame end must not modify PCM"); + // Past the end. + apply_transient_prenoise(&mut buf, 1, 0, total, (total / 4 + 100) as u16, 50); + assert_eq!(buf, before, "transient past frame end must not modify PCM"); + } + + /// A transient sitting exactly on a 256-sample block boundary has + /// zero pre-noise length → no correction window. + #[test] + fn transient_on_block_boundary_is_noop() { + let total = 6 * SAMPLES_PER_BLOCK; + let mut buf = frame(total); + for (i, v) in buf.iter_mut().enumerate() { + *v = (i as f32).sin(); + } + let before = buf.clone(); + // transloc = 4 * 256 = 1024 → exactly block 4's leading edge. + apply_transient_prenoise(&mut buf, 1, 0, total, (1024 / 4) as u16, 32); + assert_eq!(buf, before, "block-aligned transient → no pre-noise gap"); + } + + /// The corrected window must overwrite ONLY the pre-transient region + /// `[start .. transloc)` and leave samples at/after the transient (and + /// well before `start`) untouched. Uses a constant-1.0 baseband so the + /// synth buffer is also all-1.0, which keeps cross-faded values at 1.0 + /// (complementary windows sum to 1) — making the "unchanged" assertion + /// exact for every corrected sample too. + #[test] + fn correction_is_bounded_and_preserves_constant_signal() { + let total = 6 * SAMPLES_PER_BLOCK; // 1536 + let mut buf = vec![1.0f32; total]; + // transloc = 4 * 300 = 1200 (inside block 4: 1024..1280). + let transprocloc = 300u16; + let transloc = 4 * transprocloc as usize; // 1200 + let translen = 40usize; + apply_transient_prenoise(&mut buf, 1, 0, total, transprocloc, translen as u16); + // A constant signal is its own time-scaled copy: every sample must + // remain 1.0 within fp tolerance (the cross-fade windows are + // complementary and the synth buffer is all 1.0). + for (i, &v) in buf.iter().enumerate() { + assert!( + (v - 1.0).abs() < 1e-5, + "sample {i} drifted to {v} (constant signal must survive TPNP)" + ); + } + // Sanity: the transient sample itself and everything after it is + // strictly outside the overwrite window. + let pnlen = transloc - 1024; // 176 + let tot_corr_len = pnlen + translen + 256; // 472 + let start = transloc - tot_corr_len; // 728 + assert!(start < transloc, "correction window must precede transient"); + assert!( + tot_corr_len > 256 + 128, + "window must span all three §E.3.7.2 cross-fade/overwrite regions" + ); + } + + /// With distinct earlier audio, the middle (full-overwrite) region of + /// the corrected window must equal the copied synthesis samples — i.e. + /// the pre-noise is genuinely replaced, not merely attenuated. + #[test] + fn middle_region_overwrites_with_synthesis_samples() { + const TC1: usize = 256; + const TC2: usize = 128; + let total = 6 * SAMPLES_PER_BLOCK; + // Ramp so each sample is uniquely identifiable. + let mut buf: Vec = (0..total).map(|i| i as f32).collect(); + let transprocloc = 300u16; + let transloc = 4 * transprocloc as usize; // 1200 + let translen = 40usize; + let pnlen = transloc - 1024; // 176 + let tot_corr_len = pnlen + translen + TC1; // 472 + let start = transloc - tot_corr_len; // 728 + let want_src0 = transloc as isize - (2 * TC1 + 2 * pnlen) as isize; // 1200-864=336 + let orig = buf.clone(); + apply_transient_prenoise(&mut buf, 1, 0, total, transprocloc, translen as u16); + // Check a sample firmly inside region 2 [start+TC1 .. start+corr-TC2). + let s = TC1 + 10; // within [256 .. 472-128=344) + assert!(s < tot_corr_len - TC2); + let dst = start + s; + let expected = orig[(want_src0 + s as isize) as usize]; + assert!( + (buf[dst] - expected).abs() < 1e-4, + "region-2 sample {dst} should equal synth source {expected}, got {}", + buf[dst] + ); + // A sample at/after the transient is untouched. + assert_eq!(buf[transloc], orig[transloc], "transient sample untouched"); + assert_eq!( + buf[transloc + 5], + orig[transloc + 5], + "post-transient untouched" + ); + } +} + +#[cfg(test)] +mod aht_regs_tests { + use super::*; + + /// Build a minimal Annex-E BSI for the regs tests. `acmod` drives + /// `nfchans`; `lfeon` toggles the LFE; `num_blocks` is fixed at 6 + /// because AHT is only available in 6-block mode (§3.4.2). + fn bsi(acmod: u8, lfeon: bool) -> Eac3Bsi { + let nfchans = crate::tables::acmod_nfchans(acmod); + Eac3Bsi { + strmtyp: StreamType::Independent, + substreamid: 0, + frmsiz: 383, + fscod: 0, + fscod2: 0xFF, + sample_rate: 48_000, + numblkscod: 3, + num_blocks: 6, + acmod, + nfchans, + lfeon, + nchans: nfchans + u8::from(lfeon), + bsid: 16, + dialnorm: 27, + dialnorm_ch2: None, + bsmod: None, + chanmap: None, + annex_e_mix_levels: None, + dmixmod: 0xFF, + dmixmod_preference: None, + lfemixlevcod: None, + pgmscl: None, + pgmscl2: None, + extpgmscl: None, + paninfo: None, + paninfo2: None, + premix_compression: None, + compr: None, + compr_ch2: None, + dsurexmod: None, + dheadphonmod: None, + dolby_surround_mode: None, + adconvtyp: None, + adconvtyp_ch2: None, + audio_production: None, + audio_production_ch2: None, + copyright_info: None, + addbsi: None, + frame_bytes: 768, + bits_consumed: 0, + } + } + + /// REUSE = 0; D15 = 1 etc. nchregs[ch] counts the non-REUSE blocks. + #[test] + fn nchregs_counts_non_reuse_blocks_per_channel() { + let b = bsi(2, false); // 2/0 stereo, 2 fbw channels. + let mut af = AudFrm::new(); + // Channel 0: AHT-eligible — block 0 fresh (D15), blocks 1..5 REUSE. + af.chexpstr_blk_ch[0][0] = 1; + // Channel 1: NOT AHT-eligible — fresh on block 0 and block 3. + af.chexpstr_blk_ch[0][1] = 2; + af.chexpstr_blk_ch[3][1] = 1; + + let regs = compute_aht_regs(&af, &b); + assert_eq!(regs.nchregs[0], 1, "ch0 sends exponents once → eligible"); + assert_eq!( + regs.nchregs[1], 2, + "ch1 sends exponents twice → not eligible" + ); + // Channels beyond nfchans must stay zero. + assert_eq!(regs.nchregs[2], 0); + assert_eq!(regs.ncplregs, 0, "no coupling strategy set → 0"); + assert_eq!(regs.nlferegs, 0, "lfeon=false → 0"); + } + + /// ncplregs counts blocks with `cplstre[blk] == 1` OR a non-REUSE + /// coupling exponent strategy (§3.4.2 first pseudo-code block). + #[test] + fn ncplregs_counts_cplstre_or_non_reuse_cplexpstr() { + let b = bsi(7, false); // 3/2, coupling-capable. + let mut af = AudFrm::new(); + // Block 0: cplstre set (always true for block 0 when coupling + // is in use) → counts. Block 2: fresh cplexpstr only. Block 4: + // both. Others REUSE / no strategy. + af.cplstre_blk[0] = true; + af.cplexpstr_blk[2] = 2; // D25, non-REUSE → counts + af.cplstre_blk[4] = true; + af.cplexpstr_blk[4] = 1; // counted once (single block) + + let regs = compute_aht_regs(&af, &b); + assert_eq!( + regs.ncplregs, 3, + "blocks 0, 2, 4 transmit coupling exponents" + ); + } + + /// nlferegs counts non-REUSE LFE exponent strategy blocks, and is + /// only computed when `lfeon` is set. + #[test] + fn nlferegs_counts_non_reuse_lfe_blocks() { + let mut af = AudFrm::new(); + af.lfeexpstr[0] = 1; // D15 fresh + af.lfeexpstr[3] = 1; // D15 fresh + + // lfeon=false → always 0 regardless of lfeexpstr contents. + let no_lfe = compute_aht_regs(&af, &bsi(7, false)); + assert_eq!(no_lfe.nlferegs, 0, "lfeon=false suppresses nlferegs"); + + // lfeon=true → counts the two non-REUSE blocks. + let with_lfe = compute_aht_regs(&af, &bsi(7, true)); + assert_eq!(with_lfe.nlferegs, 2, "two fresh LFE strategy blocks"); + } + + /// A channel that transmits exponents only once across the frame is + /// AHT-eligible (`nchregs == 1`); a single fresh block 0 with all + /// reuse afterwards is the canonical eligible pattern. + #[test] + fn single_fresh_block_zero_is_aht_eligible() { + let b = bsi(1, false); // mono + let mut af = AudFrm::new(); + af.chexpstr_blk_ch[0][0] = 3; // D45 fresh, rest REUSE + let regs = compute_aht_regs(&af, &b); + assert_eq!(regs.nchregs[0], 1); + } +} + +/// Round-113 tests for the LFE branch of [`unpack_mixed_mantissas`]. +/// +/// Before round 113 the AHT-aware mantissa unpacker walked only the fbw +/// channels and never touched the LFE channel, so any AHT syncframe that +/// carried an LFE channel desynced the bit cursor (standard `lfeahtinu == +/// 0` LFE) or hit the blanket coupling/LFE reject (`lfeahtinu == 1`). +/// These tests exercise both LFE branches directly through +/// `unpack_mixed_mantissas` with a hand-built `Ac3State` so the new path +/// is covered without depending on a full syncframe fixture. +#[cfg(test)] +mod lfe_aht_tests { + use super::*; + + const LFE: usize = MAX_FBW + 1; + const AHT_SLOTS: usize = MAX_FBW + 2; + + /// Empty AHT cache + flag arrays (one slot per `state.channels` index). + fn empty_aht() -> ( + Vec<[[f32; N_COEFFS]; AHT_BLOCKS]>, + [bool; AHT_SLOTS], + [bool; AHT_SLOTS], + ) { + ( + vec![[[0.0; N_COEFFS]; AHT_BLOCKS]; AHT_SLOTS], + [false; AHT_SLOTS], + [false; AHT_SLOTS], + ) + } + + /// Standard LFE (`lfeahtinu == 0`) in an AHT frame: the 7 LFE + /// mantissas MUST be consumed from the bit stream. bap=5 reads exactly + /// 4 bits per bin (no grouping), so 7 bins → 28 bits, and each bin + /// reconstructs `MANT_LEVEL_15[code] · 2^-exp`. This is the regression + /// guard for the pre-round-113 cursor desync. + #[test] + fn standard_lfe_mantissas_are_consumed_in_aht_frame() { + let mut state = Ac3State::new(); + // No fbw channels in the mantissa stream; only LFE present. + state.channels[LFE].end_mant = 7; + state.channels[LFE].dithflag = false; + for bin in 0..7 { + state.channels[LFE].bap[bin] = 5; // 4-bit fixed quantiser + state.channels[LFE].exp[bin] = 3; + } + + // 7 × 4-bit LFE mantissa codewords, MSB-first. + let codes = [0u32, 1, 2, 7, 8, 14, 15]; + let mut w = oxideav_core::bits::BitWriter::new(); + for c in codes { + w.write_u32(c, 4); + } + let bytes = w.into_bytes(); + let mut br = BitReader::new(&bytes); + + let (mut cache, mut pending, mut filled) = empty_aht(); + // nfchans = 0 (no fbw), lfeon = true. + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 0, + true, + false, + ) + .expect("decode"); + + assert_eq!( + br.bit_position(), + 28, + "exactly 7 × 4-bit LFE mantissas must be consumed" + ); + // LFE coeffs are non-zero where the codeword is non-zero (code 0 + // maps to MANT_LEVEL_15[0] which is non-zero for the symmetric + // mid-tread quantiser, so just check finiteness + that the high + // codewords differ from the low ones). + let c = &state.channels[LFE].coeffs; + assert!(c[..7].iter().all(|v| v.is_finite())); + assert_ne!( + c[0], c[6], + "different codewords must reconstruct different coeffs" + ); + assert!(!filled[LFE], "standard LFE never fills the AHT cache"); + } + + /// LFE-AHT (`lfeahtinu == 1`) with every bin driven to `hebap == 0` + /// (zero-mantissa): the front-loaded block reads only the 2-bit + /// `lfegaqmod`, fills the 6-block cache with zeros, sets + /// `aht_filled[LFE]`, and the SECOND call (block 1) reads zero bits. + #[test] + fn lfe_aht_zero_mantissa_frontloads_and_caches() { + let mut state = Ac3State::new(); + state.channels[LFE].end_mant = 7; + // floorcod 0 → floor 0x2f0; mask 0 + positive snroffset clamps the + // band floor so `mask_after_floor == floor`. psd 0 → address + // `((0 - 0x2f0) >> 5)` is negative → clamps to 0 → hebap 0. + state.floorcod = 0; + state.snroffst_coarse = 15; // coarse term zero + state.lfefsnroffst = 0; + for bin in 0..7 { + state.channels[LFE].psd[bin] = 0; + state.channels[LFE].exp[bin] = 3; + } + for m in state.channels[LFE].mask.iter_mut() { + *m = 0; + } + + // Bit stream: lfegaqmod = 0 (2 bits) then nothing (all bins zero). + let mut w = oxideav_core::bits::BitWriter::new(); + w.write_u32(0, 2); + let bytes = w.into_bytes(); + let mut br = BitReader::new(&bytes); + + let (mut cache, mut pending, mut filled) = empty_aht(); + pending[LFE] = true; // lfeahtinu == 1 + + // Block 0 — front-load. + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 0, + true, + false, + ) + .expect("block 0 decode"); + assert_eq!(br.bit_position(), 2, "only lfegaqmod consumed"); + assert!(filled[LFE], "LFE-AHT cache filled after block 0"); + assert!(!pending[LFE], "pending cleared after front-load"); + for blk in 0..AHT_BLOCKS { + for bin in 0..7 { + assert_eq!(cache[LFE][blk][bin], 0.0, "zero-hebap → zero coeffs"); + } + } + + // Block 1 — cached, must read no further bits. + let pos_before = br.bit_position(); + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 0, + true, + false, + ) + .expect("block 1 decode"); + assert_eq!( + br.bit_position(), + pos_before, + "subsequent LFE-AHT blocks read no bits" + ); + } + + /// LFE-AHT with bins driven to a VQ regime (`hebap == 1`): the + /// front-loaded block reads `lfegaqmod` + one 2-bit VQ index per bin, + /// runs the §3.4.5 IDCT-II, and caches non-trivial per-block + /// coefficients (a single VQ codeword yields six distinct block + /// values via the inverse DCT-II, so the cache is not flat). + #[test] + fn lfe_aht_vq_regime_runs_idct_and_caches_nonflat() { + let mut state = Ac3State::new(); + state.channels[LFE].end_mant = 7; + // floorcod 7 → floor -2048; mask 0, snroffset 0. The §3.4.3.1 + // band-floor post-process `(0 - 0 - (-2048)) & 0x1fe0 + (-2048)` + // collapses to `mask_after_floor == 0`, so the hebap address is + // `(psd >> 5).clamp(0, 63)`. psd = 48 → `(48 >> 5) = 1` → + // HEBAPTAB[1] = 1 → VQ regime, VQ_BITS[1] = 2 bits. + state.floorcod = 7; + state.snroffst_coarse = 15; + state.lfefsnroffst = 0; + for bin in 0..7 { + state.channels[LFE].psd[bin] = 48; + state.channels[LFE].exp[bin] = 0; // 2^0 = 1, leave VQ value as-is + } + for m in state.channels[LFE].mask.iter_mut() { + *m = 0; + } + + // lfegaqmod = 0 (2 bits), then 7 × 2-bit VQ indices (all index 0). + let mut w = oxideav_core::bits::BitWriter::new(); + w.write_u32(0, 2); + for _ in 0..7 { + w.write_u32(0, 2); + } + let bytes = w.into_bytes(); + let mut br = BitReader::new(&bytes); + + let (mut cache, mut pending, mut filled) = empty_aht(); + pending[LFE] = true; + + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 0, + true, + false, + ) + .expect("decode"); + assert_eq!( + br.bit_position(), + 2 + 7 * 2, + "lfegaqmod + 7 × 2-bit VQ indices consumed" + ); + assert!(filled[LFE]); + // The inverse DCT-II of a fixed VQ 6-tuple produces six distinct + // block coefficients for at least one bin (the codeword is not a + // pure DC vector), so the per-block cache must vary across blocks. + let varies = (0..7).any(|bin| { + let first = cache[LFE][0][bin]; + (0..AHT_BLOCKS).any(|blk| (cache[LFE][blk][bin] - first).abs() > 1e-9) + }); + assert!( + varies, + "IDCT-II of a VQ codeword must yield block-varying coefficients" + ); + } +} + +/// Round-117 tests for the coupling branch of [`unpack_mixed_mantissas`]. +/// +/// Before round 117 the dsp rejected any AHT syncframe with `cplahtinu == +/// 1`. These tests drive the new coupling-AHT path (and the interleaved +/// standard coupling read inside an AHT frame) directly through +/// `unpack_mixed_mantissas` with a hand-built `Ac3State`, mirroring the +/// round-113 LFE tests. The coupling pseudo-channel lives at slot +/// `MAX_FBW`; its mantissas span `[cpl_begf_mant, cpl_endf_mant)`. +#[cfg(test)] +mod cpl_aht_tests { + use super::*; + + const CPL: usize = MAX_FBW; + const AHT_SLOTS: usize = MAX_FBW + 2; + + fn empty_aht() -> ( + Vec<[[f32; N_COEFFS]; AHT_BLOCKS]>, + [bool; AHT_SLOTS], + [bool; AHT_SLOTS], + ) { + ( + vec![[[0.0; N_COEFFS]; AHT_BLOCKS]; AHT_SLOTS], + [false; AHT_SLOTS], + [false; AHT_SLOTS], + ) + } + + /// One fbw channel in coupling + a standard (`cplahtinu == 0`) + /// coupling read, all inside an AHT frame (a different channel uses + /// AHT). The coupling mantissas MUST be consumed right after the fbw + /// channel's mantissas (the `got_cplchan` interleave). The fbw channel + /// here has `end_mant == cpl_begf_mant` (fully coupled), so it reads no + /// mantissas of its own; the only bits in the stream are the coupling + /// mantissas. This is the regression guard for the interleave order. + #[test] + fn standard_coupling_mantissas_consumed_in_aht_frame() { + let mut state = Ac3State::new(); + // 1 fbw channel, fully coupled from bin 0. + let cpl_begf_mant = 0usize; + let cpl_endf_mant = 6usize; // 6 coupling bins + state.cpl_begf_mant = cpl_begf_mant; + state.cpl_endf_mant = cpl_endf_mant; + state.channels[0].end_mant = cpl_begf_mant; // fully coupled + state.channels[0].in_coupling = true; + // Coupling channel quantiser: bap=5 → 4 bits/bin, no grouping. + for bin in cpl_begf_mant..cpl_endf_mant { + state.channels[CPL].bap[bin] = 5; + state.channels[CPL].exp[bin] = 2; + } + + // 6 × 4-bit coupling mantissa codewords, MSB-first. + let codes = [1u32, 3, 5, 9, 12, 15]; + let mut w = oxideav_core::bits::BitWriter::new(); + for c in codes { + w.write_u32(c, 4); + } + let bytes = w.into_bytes(); + let mut br = BitReader::new(&bytes); + + let (mut cache, mut pending, mut filled) = empty_aht(); + // nfchans = 1, lfeon = false, cplinu = true. No AHT-pending + // channel here, but the function is still exercised on the + // coupling interleave path (the dispatch arms it whenever ahte). + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 1, + false, + true, + ) + .expect("decode"); + + assert_eq!( + br.bit_position(), + 6u64 * 4, + "exactly 6 × 4-bit coupling mantissas must be consumed" + ); + assert!(!filled[CPL], "standard coupling never fills the AHT cache"); + let c = &state.channels[CPL].coeffs; + assert!(c[..cpl_endf_mant].iter().all(|v| v.is_finite())); + assert_ne!( + c[0], c[5], + "different coupling codewords reconstruct different coeffs" + ); + } + + /// Coupling-AHT (`cplahtinu == 1`) with every coupling bin driven to + /// `hebap == 0`: the front-loaded block reads only the 2-bit + /// `cplgaqmod`, fills the 6-block cache with zeros over the coupling + /// range, sets `aht_filled[CPL]`, and the SECOND call reads no bits. + #[test] + fn cpl_aht_zero_mantissa_frontloads_and_caches() { + let mut state = Ac3State::new(); + let start = 37usize; // cpl_begf_mant for cplbegf=0 + let end = 49usize; // 12 coupling bins + state.cpl_begf_mant = start; + state.cpl_endf_mant = end; + state.channels[0].end_mant = start; // fully coupled fbw 0 + state.channels[0].in_coupling = true; + + // floorcod 0 → floor 0x2f0; mask 0, snroffset 0 → mask_after_floor + // == floor, psd 0 → negative address → clamps to 0 → hebap 0. + state.floorcod = 0; + state.snroffst_coarse = 15; + state.cpl_fsnroffst = 0; + for bin in start..end { + state.channels[CPL].psd[bin] = 0; + state.channels[CPL].exp[bin] = 2; + } + for m in state.channels[CPL].mask.iter_mut() { + *m = 0; + } + + // cplgaqmod = 0 (2 bits), then nothing (all bins zero). + let mut w = oxideav_core::bits::BitWriter::new(); + w.write_u32(0, 2); + let bytes = w.into_bytes(); + let mut br = BitReader::new(&bytes); + + let (mut cache, mut pending, mut filled) = empty_aht(); + pending[CPL] = true; // cplahtinu == 1 + + // Block 0 — front-load. + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 1, + false, + true, + ) + .expect("block 0 decode"); + assert_eq!(br.bit_position(), 2, "only cplgaqmod consumed"); + assert!(filled[CPL], "coupling-AHT cache filled after block 0"); + assert!(!pending[CPL], "pending cleared after front-load"); + for blk in 0..AHT_BLOCKS { + for bin in start..end { + assert_eq!(cache[CPL][blk][bin], 0.0, "zero-hebap → zero coeffs"); + } + } + + // Block 1 — cached, must read no further bits. + let pos_before = br.bit_position(); + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 1, + false, + true, + ) + .expect("block 1 decode"); + assert_eq!( + br.bit_position(), + pos_before, + "subsequent coupling-AHT blocks read no bits" + ); + } + + /// Coupling-AHT with bins in the VQ regime (`hebap == 1`): the + /// front-loaded block reads `cplgaqmod` + one 2-bit VQ index per + /// coupling bin, runs the §3.4.5 IDCT-II, and caches block-varying + /// per-bin coefficients only across the coupling range — bins below + /// `cpl_begf_mant` stay zero (the encoder never codes them). + #[test] + fn cpl_aht_vq_regime_runs_idct_and_zero_below_begf() { + let mut state = Ac3State::new(); + let start = 37usize; + let end = 43usize; // 6 coupling bins + state.cpl_begf_mant = start; + state.cpl_endf_mant = end; + state.channels[0].end_mant = start; + state.channels[0].in_coupling = true; + + // floorcod 7 → floor -2048; mask 0, snroffset 0 → mask_after_floor + // == 0. psd 48 → (48 >> 5) = 1 → HEBAPTAB[1] = 1 → VQ, 2 bits. + state.floorcod = 7; + state.snroffst_coarse = 15; + state.cpl_fsnroffst = 0; + for bin in start..end { + state.channels[CPL].psd[bin] = 48; + state.channels[CPL].exp[bin] = 0; + } + for m in state.channels[CPL].mask.iter_mut() { + *m = 0; + } + + // cplgaqmod = 0 (2 bits), then 6 × 2-bit VQ indices (all index 0). + let mut w = oxideav_core::bits::BitWriter::new(); + w.write_u32(0, 2); + for _ in start..end { + w.write_u32(0, 2); + } + let bytes = w.into_bytes(); + let mut br = BitReader::new(&bytes); + + let (mut cache, mut pending, mut filled) = empty_aht(); + pending[CPL] = true; + + unpack_mixed_mantissas( + &mut state, + &mut br, + &mut cache, + &mut pending, + &mut filled, + 1, + false, + true, + ) + .expect("decode"); + assert_eq!( + br.bit_position(), + 2 + (end - start) as u64 * 2, + "cplgaqmod + one 2-bit VQ index per coupling bin" + ); + assert!(filled[CPL]); + // Bins below cpl_begf_mant are never coded → cache stays zero. + for blk in 0..AHT_BLOCKS { + for bin in 0..start { + assert_eq!(cache[CPL][blk][bin], 0.0, "no coupling coeffs below begf"); + } + } + // The IDCT-II of a VQ codeword yields six distinct block values for + // at least one coupling bin. + let varies = (start..end).any(|bin| { + let first = cache[CPL][0][bin]; + (0..AHT_BLOCKS).any(|blk| (cache[CPL][blk][bin] - first).abs() > 1e-9) + }); + assert!( + varies, + "IDCT-II of a coupling VQ codeword must yield block-varying coeffs" + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/ecpl.rs b/crates/vendor/oxideav-ac3/src/eac3/ecpl.rs new file mode 100644 index 00000000..689e8b5e --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/ecpl.rs @@ -0,0 +1,2537 @@ +//! Enhanced-coupling sub-band / band geometry — ATSC A/52:2018 Annex E +//! §E.2.3.3.16-19 + §E.3.5.2. +//! +//! Enhanced coupling (`ecplinu == 1`) reconstructs the high-frequency +//! transform coefficients of the fbw channels from a single shared +//! *enhanced coupling channel* plus per-band amplitude / angle / chaos +//! parameters (§E.3.5.5). Before any of that synthesis can run, the +//! decoder has to know the **band geometry**: which transform +//! coefficients (bins) belong to the enhanced-coupling region, how the +//! 22 fixed sub-bands of Table E3.7 are grouped into the variable +//! coupling *bands* that carry one coordinate each, and how many such +//! bands (`necplbnd`) exist for the current block. +//! +//! This module is the pure, spec-tabulated geometry layer. It carries: +//! +//! * [`begin_subbnd`] / [`end_subbnd`] — the Table E3.8 derivations of +//! `ecpl_begin_subbnd` (from `ecplbegf`) and `ecpl_end_subbnd` (from +//! `ecplendf`, or from the SPX begin when SPX is co-active). +//! * [`ECPL_SUBBND_TAB`] — Table E3.9 `ecplsubbndtab[]`, the starting +//! transform-coefficient number of each of the 22 sub-bands (plus the +//! one-past-the-end sentinel at index 22). +//! * [`DEFAULT_ECPL_BNDSTRC`] — Table E2.14 `defecplbndstrc[]`, the +//! default banding used the first time enhanced coupling is active in +//! a frame when `ecplbndstrce == 0`. +//! * [`necplbnd`] — §E.2.3.3.19 band-count derivation from the +//! per-sub-band `ecplbndstrc[]` merge bits. +//! * [`band_bin_counts`] — the §E.3.5.5.1 `nbins_per_bnd_array[]` +//! population: how many transform coefficients each enhanced coupling +//! band spans. +//! +//! The geometry layer is kept isolated and unit-tested so the synthesis +//! steps that build on it (parameter processing, carrier reconstruction, +//! per-channel coefficient generation) start from a verified foundation. +//! The full §E.3.5.5 per-block synthesis is orchestrated by +//! [`synthesize_block`]; the cross-block random sources live on +//! [`EcplState`]. +//! +//! As of round 300 the module also carries the **bitstream-syntax** layer +//! that sits on top of the geometry: [`parse_strategy`] reads the +//! §E.2.3.3.16-19 strategy fields and [`parse_coords`] reads the +//! §E.2.3.3.20-26 per-band amplitude / angle / chaos coordinates. These +//! advance the bit cursor exactly per the reference syntax so an +//! enhanced-coupling block can be walked without desync. +//! +//! Round 306 adds the **§E.3.5.5.2 / §E.3.5.5.3 parameter-processing** +//! layer: [`ampbnd`] / [`angle_value`] / [`chaos_value`] decode the +//! Table E3.10-E3.12 index triples, [`process_band_amplitudes`] applies +//! the §E.3.5.5.2 chaos amplitude modification, [`expand_bands_to_bins`] +//! fans the per-band values out to per-bin `ampbin[]`, and +//! [`interpolate_bin_angles`] implements the `ecplangleintrp == 1` +//! linear-interpolation path (§E.3.5.5.3). These turn the decoded index +//! triples into the per-bin amplitude / angle arrays the synthesis +//! consumes — pure tabulated arithmetic with no multi-block state. +//! +//! The next layer (added later) closes the §E.3.5.5.3 angle path with the +//! chaos × random de-correlation term and implements the §E.3.5.5.4 +//! channel transform-coefficient generation (the per-bin complex product +//! against the reconstructed coupling channel `Z[k]`): +//! [`apply_decorrelation`], [`generate_channel_coeffs`], the +//! [`RandNoTrans`] init-once non-transient random array, [`gen_rand_trans`] +//! per-block transient random, and the [`synthesis_window`] `y[bin]` +//! factor. These are pure tabulated arithmetic over the carrier `Z[k]`. +//! +//! The final layer ([`reconstruct_carrier`]) implements §E.3.5.5.1: the +//! prev/curr/next windowed IMDCT + overlap-add + forward-DFT that produces +//! the non-aliased complex coupling channel `Z[k]` from the de-normalised +//! enhanced-coupling mantissas. The function takes all three blocks' +//! coefficient buffers (the cross-block state is owned by the caller), so +//! this whole module stays a pure, unit-tested layer end to end. +//! +//! The decoder-level *integration* is provided by [`synthesize_block`] +//! plus the cross-block [`EcplState`] (the §E.3.5.5.3 random +//! de-correlation sources): given the previous / current / next blocks' +//! de-normalised enhanced-coupling coefficients ([`EcplBlock`]), it runs +//! the full §E.3.5.5 "for each block" procedure and writes each coupled +//! channel's transform coefficients. The E-AC-3 dsp layer +//! (`super::dsp`) owns the bitstream extraction + per-block buffering and +//! calls into it. +//! +//! **Spec note (erratum):** the default-banding table is captioned +//! "Table E2.14" in the document's table-of-contents (and list of +//! tables) but is cross-referenced as "Table E2.13" from the body of +//! §E.2.3.3.18 — the latter collides with the *standard* coupling +//! default at the genuine Table E2.13. The two tables hold different +//! values; the enhanced-coupling values used here are those listed in +//! full under the §E.2.3.3.18 heading. + +use crate::imdct::{dft_512_forward, imdct_512_fft}; +use crate::tables::WINDOW; +use oxideav_core::bits::BitReader; +use oxideav_core::{Error, Result}; + +/// Table E3.9 — `ecplsubbndtab[]`. The starting transform-coefficient +/// number of each of the 22 enhanced-coupling sub-bands, with a +/// one-past-the-end sentinel (`253`) at index 22 so the half-open span +/// of sub-band `s` is `ECPL_SUBBND_TAB[s] .. ECPL_SUBBND_TAB[s + 1]`. +/// +/// Sub-bands 0..=3 are 6 bins wide (13..18, 19..24, 25..30, 31..36); +/// sub-bands 4..=21 are 12 bins wide. The enhanced-coupling region thus +/// spans transform coefficients 13..=252. +pub const ECPL_SUBBND_TAB: [usize; 23] = [ + 13, 19, 25, 31, 37, 49, 61, 73, 85, 97, 109, 121, 133, 145, 157, 169, 181, 193, 205, 217, 229, + 241, 253, +]; + +/// Number of enhanced-coupling sub-bands defined by Table E3.7. +pub const N_ECPL_SUBBND: usize = 22; + +/// Table E2.14 — `defecplbndstrc[]`. The default enhanced-coupling +/// banding structure, indexed by absolute sub-band number. A `true` +/// ('1') entry means "merge sub-band `s` into the previous band". Per +/// §E.2.3.3.19 the merge bits for sub-bands `<= max(ecpl_begin_subbnd, +/// 8)` are always zero (and not transmitted); the table reflects that — +/// sub-bands 0..=8 are all `false`, with the first merge at sub-band 9. +pub const DEFAULT_ECPL_BNDSTRC: [bool; N_ECPL_SUBBND] = { + let mut t = [false; N_ECPL_SUBBND]; + // §E.2.3.3.18 default: sub-bands 0..8 → 0; then the per-row values. + t[9] = true; + // t[10] = false (12 → 0) + t[11] = true; + // t[12] = false + t[13] = true; + // t[14] = false + t[15] = true; + t[16] = true; + t[17] = true; + // t[18] = false + t[19] = true; + t[20] = true; + t[21] = true; + t +}; + +/// §E.2.3.3.16 — derive `ecpl_begin_subbnd` from the 4-bit `ecplbegf` +/// code (Table E3.8). +/// +/// ```text +/// if (ecplbegf < 3) ecpl_begin_subbnd = ecplbegf * 2 +/// else if (ecplbegf < 13) ecpl_begin_subbnd = ecplbegf + 2 +/// else ecpl_begin_subbnd = ecplbegf * 2 - 10 +/// ``` +#[inline] +pub fn begin_subbnd(ecplbegf: u8) -> usize { + let f = ecplbegf as usize; + if f < 3 { + f * 2 + } else if f < 13 { + f + 2 + } else { + f * 2 - 10 + } +} + +/// §E.2.3.3.17 — derive `ecpl_end_subbnd` (one greater than the highest +/// active enhanced-coupling sub-band), per Table E3.8. +/// +/// When spectral extension is **not** in use the end sub-band is taken +/// directly from the 4-bit `ecplendf` code (`ecplendf + 7`). When SPX +/// **is** co-active the enhanced-coupling region is instead bounded by +/// the SPX begin so the two regions abut: `spxbegf + 5` for +/// `spxbegf < 6`, else `spxbegf * 2`. In the SPX-active case `ecplendf` +/// is not transmitted. +#[inline] +pub fn end_subbnd(spxinu: bool, ecplendf: u8, spxbegf: usize) -> usize { + if !spxinu { + ecplendf as usize + 7 + } else if spxbegf < 6 { + spxbegf + 5 + } else { + spxbegf * 2 + } +} + +/// §E.2.3.3.19 — number of enhanced-coupling bands. +/// +/// ```text +/// necplbnd = ecpl_end_subbnd - ecpl_begin_subbnd; +/// necplbnd -= sum(ecplbndstrc[ecpl_begin_subbnd ..= ecpl_end_subbnd-1]) +/// ``` +/// +/// Each set merge bit collapses one sub-band into the previous band, so +/// the count is the number of sub-bands in the active span minus the +/// number of merges. `bndstrc` is indexed by absolute sub-band number. +#[inline] +pub fn necplbnd(begin: usize, end: usize, bndstrc: &[bool; N_ECPL_SUBBND]) -> usize { + let span = end.saturating_sub(begin); + // Clamp the slice bounds so an inverted or out-of-grid `begin..end` + // (only reachable on malformed input — `parse_strategy` rejects it + // upstream) cannot panic: `lo > hi` would be an invalid range. + let lo = begin.min(N_ECPL_SUBBND); + let hi = end.min(N_ECPL_SUBBND).max(lo); + let merges = bndstrc[lo..hi].iter().filter(|&&b| b).count(); + span - merges +} + +/// §E.3.5.5.1 — populate `nbins_per_bnd_array[]`: the number of +/// transform-coefficient bins spanned by each enhanced-coupling band. +/// +/// Walking the active sub-band span, a `0` merge bit opens a new band +/// and a `1` merge bit extends the current band; each sub-band +/// contributes `ecplsubbndtab[s+1] - ecplsubbndtab[s]` bins (6 for the +/// narrow low sub-bands, 12 for the rest). The returned vector has +/// length [`necplbnd`]. +pub fn band_bin_counts(begin: usize, end: usize, bndstrc: &[bool; N_ECPL_SUBBND]) -> Vec { + let mut counts: Vec = Vec::new(); + // `begin..end` is validated `< N_ECPL_SUBBND` and non-empty by + // `parse_strategy`; clamp here too so a stray inverted range from a + // future caller degrades to an empty walk instead of a panic. + let lo = begin.min(ECPL_SUBBND_TAB.len().saturating_sub(1)); + let hi = end.min(N_ECPL_SUBBND).max(lo); + for sbnd in lo..hi { + let bins = ECPL_SUBBND_TAB[sbnd + 1] - ECPL_SUBBND_TAB[sbnd]; + if !bndstrc[sbnd] { + // New band. + counts.push(bins); + } else if let Some(last) = counts.last_mut() { + // Merge into the current band. + *last += bins; + } else { + // Defensive: a leading merge bit with no open band. The spec + // guarantees `ecplbndstrc[begin] == 0`, so this can only be + // reached on malformed input; treat it as a new band. + counts.push(bins); + } + } + counts +} + +/// First transform-coefficient number of the enhanced-coupling region +/// for the given begin sub-band (Table E3.9 lookup). This is where the +/// shared enhanced-coupling channel starts; below it every channel is +/// independently coded. +#[inline] +pub fn begin_bin(begin: usize) -> usize { + ECPL_SUBBND_TAB[begin.min(N_ECPL_SUBBND)] +} + +/// One-past-the-last transform-coefficient number of the +/// enhanced-coupling region for the given end sub-band (Table E3.9 +/// lookup). +#[inline] +pub fn end_bin(end: usize) -> usize { + ECPL_SUBBND_TAB[end.min(N_ECPL_SUBBND)] +} + +/// Maximum number of enhanced-coupling bands. Equals [`N_ECPL_SUBBND`] +/// (no merge bits → every sub-band is its own band), so a fixed-size +/// per-band buffer never overflows. +pub const MAX_ECPL_BND: usize = N_ECPL_SUBBND; + +/// The decoded **enhanced-coupling strategy** for a block (§E.2.3.3.16-19). +/// +/// This is the resolved geometry the coordinate parse + the eventual +/// §E.3.5.5 synthesis consume: the active sub-band span, the per-sub-band +/// merge structure, and the derived band count. It is produced by +/// [`parse_strategy`] from the `cplstre[blk] && cplinu[blk] && ecplinu` +/// branch of Table E1.4, or carried forward unchanged on a block whose +/// `cplstre[blk]` is `0` (strategy reuse). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EcplStrategy { + /// Raw `ecplbegf` (§E.2.3.3.16) — the 4-bit begin-frequency code as + /// transmitted, before the [`begin_subbnd`] mapping. Retained because + /// the §E.3.3.2 `nrematbd` derivation thresholds the raw code directly + /// (0/1/2/<5), not the derived sub-band index. + pub ecplbegf: u8, + /// `ecpl_begin_subbnd` — index of the first active sub-band + /// (§E.2.3.3.16). + pub begin_subbnd: usize, + /// `ecpl_end_subbnd` — one greater than the highest active sub-band + /// (§E.2.3.3.17). + pub end_subbnd: usize, + /// Resolved `ecplbndstrc[]`, indexed by absolute sub-band number: a + /// `true` entry merges that sub-band into the previous band. + pub bndstrc: [bool; N_ECPL_SUBBND], + /// `necplbnd` — number of enhanced-coupling bands (§E.2.3.3.19). + pub necplbnd: usize, +} + +impl EcplStrategy { + /// First transform-coefficient (bin) of the enhanced-coupling region. + #[inline] + pub fn begin_bin(&self) -> usize { + begin_bin(self.begin_subbnd) + } + + /// One-past-the-last transform-coefficient (bin) of the region. + #[inline] + pub fn end_bin(&self) -> usize { + end_bin(self.end_subbnd) + } + + /// Per-band bin counts (`nbins_per_bnd_array[]`, §E.3.5.5.1). + #[inline] + pub fn band_bin_counts(&self) -> Vec { + band_bin_counts(self.begin_subbnd, self.end_subbnd, &self.bndstrc) + } +} + +/// Parse the enhanced-coupling **strategy** block (§E.2.3.3.16-19 / the +/// `ecplinu` arm of Table E1.4, reached only when +/// `cplstre[blk] && cplinu[blk] && ecplinu`). +/// +/// Field order (each `read_u32` advances the cursor exactly as the +/// reference syntax does): +/// +/// 1. `ecplbegf` (4 bits) → `ecpl_begin_subbnd` via [`begin_subbnd`]. +/// 2. `ecplendf` (4 bits) **only when SPX is off**; when SPX is active +/// `ecpl_end_subbnd` is derived from `spxbegf` and `ecplendf` is *not* +/// transmitted ([`end_subbnd`]). +/// 3. `ecplbndstrce` (1 bit). When set, the per-sub-band merge bits +/// `ecplbndstrc[sbnd]` follow for +/// `sbnd in [max(9, ecpl_begin_subbnd + 1), ecpl_end_subbnd)` — the +/// sub-bands up to and including `max(8, ecpl_begin_subbnd)` are known +/// to be `0` and are never sent (§E.2.3.3.19). +/// +/// `ecplbndstrce == 0` means *use the default / reuse the previous* +/// structure. The caller supplies `prev_bndstrc`: pass +/// [`DEFAULT_ECPL_BNDSTRC`] on the first block of the frame that enables +/// enhanced coupling, or the previously-decoded structure on a later +/// block (§E.2.3.3.18). When `ecplbndstrce == 1` the supplied default is +/// ignored and a fresh all-`false` base is populated from the wire bits. +pub fn parse_strategy( + br: &mut BitReader<'_>, + spxinu: bool, + spxbegf: usize, + prev_bndstrc: &[bool; N_ECPL_SUBBND], +) -> Result { + let ecplbegf = br.read_u32(4)? as u8; + let begin = begin_subbnd(ecplbegf); + let ecplendf = if spxinu { 0 } else { br.read_u32(4)? as u8 }; + let end = end_subbnd(spxinu, ecplendf, spxbegf); + + // §E.2.3.3.16-17: the enhanced-coupling region must be a non-empty + // span within the valid sub-band grid. `ecplbegf`/`ecplendf` are raw + // 4-bit codes, so a malformed frame can decode to `begin >= end` or + // `end > N_ECPL_SUBBND` — e.g. `ecplbegf == 15` (begin = 20) paired + // with a low `ecplendf`. Left unchecked, the inverted `begin..end` + // range panics the band-structure slicing in `necplbnd` / + // `band_bin_counts`. Reject here (mirroring the §E.2.3.3.4-6 SPX + // sub-band-range guard in `eac3::dsp`) so adversarial input yields a + // recoverable decode error instead of a process abort. + if begin >= end || end > N_ECPL_SUBBND { + return Err(Error::invalid( + "eac3 ecpl: enhanced-coupling sub-band range invalid \ + (begin >= end or end > N_ECPL_SUBBND)", + )); + } + + let ecplbndstrce = br.read_u32(1)? != 0; + let mut bndstrc = if ecplbndstrce { + // Fresh structure from the wire. Sub-bands up to and including + // max(8, begin) are implicitly 0 and not transmitted; the loop + // starts at max(9, begin + 1). The merge bit for the very first + // active sub-band is therefore never sent — it always starts a + // band (§E.2.3.3.19). + let mut t = [false; N_ECPL_SUBBND]; + let lo = (begin + 1).max(9); + for sbnd in lo..end.min(N_ECPL_SUBBND) { + t[sbnd] = br.read_u32(1)? != 0; + } + t + } else { + // Reuse the default (first block) or the previous block's + // structure (later block) — no bits consumed. + *prev_bndstrc + }; + // §E.2.3.3.19: "the elements of the array corresponding to the + // sub-bands up to and including ecpl_begin_subbnd or 8 (whichever + // is greater), are always zero". This must be enforced on the + // RESOLVED structure too: the Table E2.14 default carries a merge + // bit at sub-band 9, and a region beginning there (ecplbegf == 7) + // would otherwise count a phantom merge — `necplbnd` and the + // §E.3.5.5.1 band walk would disagree by one band, desyncing every + // coordinate field that follows. + for slot in bndstrc + .iter_mut() + .take((begin.max(8) + 1).min(N_ECPL_SUBBND)) + { + *slot = false; + } + + let necplbnd = necplbnd(begin, end, &bndstrc); + Ok(EcplStrategy { + ecplbegf, + begin_subbnd: begin, + end_subbnd: end, + bndstrc, + necplbnd, + }) +} + +/// Per-channel decoded enhanced-coupling **parameters** for a block +/// (§E.2.3.3.21-26). Only channels in coupling carry an entry; the angle +/// and chaos arrays of the *first* coupled channel are spec-fixed to `0` +/// and not transmitted, so they stay empty on that channel. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct EcplChannelParams { + /// `ecplparam1e[ch]` — amplitudes present this block. + pub param1e: bool, + /// `ecplparam2e[ch]` — angle + chaos present this block. + pub param2e: bool, + /// `ecplamp[ch][bnd]` — 5-bit amplitude index per band (present iff + /// `param1e`). + pub amp: Vec, + /// `ecplangle[ch][bnd]` — 6-bit angle index per band (present iff + /// `param2e` and not the first coupled channel). + pub angle: Vec, + /// `ecplchaos[ch][bnd]` — 3-bit chaos index per band (present iff + /// `param2e` and not the first coupled channel). + pub chaos: Vec, + /// `ecpltrans[ch]` — transient-present flag (not transmitted for the + /// first coupled channel, where it is `false`). + pub trans: bool, +} + +/// The decoded enhanced-coupling **coordinate** block for one audio block +/// (§E.2.3.3.20-26 / the `ecplinu` arm of the coupling-coordinate loop in +/// Table E1.4, reached when `cplinu[blk] && ecplinu`). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct EcplCoords { + /// `ecplangleintrp` — angle-interpolation flag (§E.2.3.3.20). + pub angleintrp: bool, + /// Per-front-channel parameters, indexed by channel number. Channels + /// not in coupling carry a default (all-`false`) entry. + pub channels: Vec, +} + +/// Parse the enhanced-coupling **coordinate** block (§E.2.3.3.20-26). +/// +/// `nfchans` is the number of full-bandwidth channels; `chincpl[ch]` +/// flags which of them are in coupling. `firstcplcos[ch]` is the +/// per-channel "first coupling block this frame" marker (the same one the +/// standard-coupling `cplcoe` gate uses): on the first block a channel +/// enters coupling, its `ecplparam1e`/`ecplparam2e` are *implicit* (not +/// read from the wire) and all parameters are forced present — the spec +/// guarantees every channel transmits its full parameter set the first +/// time enhanced coupling is enabled. The marker is cleared in place so +/// later blocks read the explicit exist bits. +/// +/// `necplbnd` is the band count from the active [`EcplStrategy`]. The +/// first coupled channel (`firstchincpl`) never carries `ecplparam2e`, +/// `ecplangle`, `ecplchaos`, or `ecpltrans` — its angle/chaos are +/// spec-fixed to `0` (§E.2.3.3.24-26). +pub fn parse_coords( + br: &mut BitReader<'_>, + nfchans: usize, + chincpl: &[bool], + firstcplcos: &mut [bool], + necplbnd: usize, +) -> Result { + let angleintrp = br.read_u32(1)? != 0; + let mut channels = vec![EcplChannelParams::default(); nfchans]; + + // firstchincpl = -1 → the first channel actually in coupling. + let mut firstchincpl: Option = None; + + for ch in 0..nfchans { + if !chincpl[ch] { + // §E.2.3.3 "!chincpl[ch]" arm: re-arm the first-coupling + // marker so a later block re-entering coupling treats its + // parameters as implicit-present again. + firstcplcos[ch] = true; + continue; + } + let is_first = firstchincpl.is_none(); + if is_first { + firstchincpl = Some(ch); + } + + let (param1e, param2e) = if firstcplcos[ch] { + // First block this channel is in coupling: parameters are + // implicit. param1e is always 1; param2e is 1 only for + // channels after the first coupled channel. + firstcplcos[ch] = false; + (true, !is_first) + } else { + let p1 = br.read_u32(1)? != 0; + // param2e is transmitted only for channels after the first + // coupled channel; the first coupled channel's angle/chaos + // are fixed to 0, so it has no param2e bit. + let p2 = if !is_first { + br.read_u32(1)? != 0 + } else { + false + }; + (p1, p2) + }; + + let mut params = EcplChannelParams { + param1e, + param2e, + ..Default::default() + }; + + if param1e { + params.amp.reserve_exact(necplbnd); + for _ in 0..necplbnd { + params.amp.push(br.read_u32(5)? as u8); + } + } + if param2e { + params.angle.reserve_exact(necplbnd); + params.chaos.reserve_exact(necplbnd); + for _ in 0..necplbnd { + params.angle.push(br.read_u32(6)? as u8); + params.chaos.push(br.read_u32(3)? as u8); + } + } + // ecpltrans[ch] is transmitted only for channels after the first + // coupled channel. + if !is_first { + params.trans = br.read_u32(1)? != 0; + } + + channels[ch] = params; + } + + Ok(EcplCoords { + angleintrp, + channels, + }) +} + +/// §2.3.3.21-22 reuse semantics: when `ecplparam1e[ch] == 0` the +/// previously transmitted amplitudes for that channel are reused, and +/// when `ecplparam2e[ch] == 0` the previously transmitted angle + chaos +/// values are reused. [`parse_coords`] leaves the corresponding vectors +/// empty on a reuse block; this merges the prior block's values into the +/// fresh set so the §E.3.5.5 synthesis sees the persistent coordinates. +/// +/// Values are only carried when the prior vector's length matches the +/// active band count (`necplbnd`) — a mid-frame strategy change that +/// alters the banding invalidates stale coordinates (the spec requires +/// retransmission in that case; degrading to silence is the defensive +/// fallback for a malformed stream). +pub fn merge_reused_params(curr: &mut EcplCoords, prev: &EcplCoords, necplbnd: usize) { + for (ch, params) in curr.channels.iter_mut().enumerate() { + let Some(prior) = prev.channels.get(ch) else { + continue; + }; + if !params.param1e && params.amp.is_empty() && prior.amp.len() == necplbnd { + params.amp = prior.amp.clone(); + } + if !params.param2e && params.angle.is_empty() && prior.angle.len() == necplbnd { + params.angle = prior.angle.clone(); + params.chaos = prior.chaos.clone(); + } + } +} + +// =========================================================================== +// §E.3.5.5.2 / §E.3.5.5.3 — enhanced-coupling parameter processing +// =========================================================================== +// +// This layer turns the decoded per-band index triples (`ecplamp` / +// `ecplangle` / `ecplchaos` carried in [`EcplChannelParams`]) into the +// per-*bin* amplitude and angle arrays the §E.3.5.5.4 complex-product +// synthesis consumes. It is pure tabulated arithmetic over the active +// band geometry — no multi-block state, no FFT. +// +// The §E.3.5.5.3 closing de-correlation step, the §E.3.5.5.4 complex +// synthesis, and the §E.3.5.5.1 carrier reconstruction +// ([`reconstruct_carrier`]) are all added below this block. The remaining +// work outside this pure layer is the decoder-level integration that +// supplies the prev/curr/next mantissa buffers and consumes the per-channel +// coefficients. + +/// Table E3.10 — `ecplampexptab[]`. The binary exponent (right-shift +/// count) applied to the mantissa for each 5-bit `ecplamp` index. Index +/// 31 (minus-infinity dB) has no exponent and is handled specially in +/// [`ampbnd`]; its slot here is `0` and never read on that path. +pub const ECPL_AMP_EXP_TAB: [u8; 32] = [ + 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 0, +]; + +/// Table E3.10 — `ecplampmanttab[]`. The 6-bit mantissa for each 5-bit +/// `ecplamp` index (the `/32` in [`ampbnd`] applies the implicit binary +/// point). Index 31 is `0x00` (minus-infinity dB → amplitude 0). +pub const ECPL_AMP_MANT_TAB: [u8; 32] = [ + 0x20, 0x1b, 0x17, 0x13, 0x10, 0x1b, 0x17, 0x13, 0x10, 0x1b, 0x17, 0x13, 0x10, 0x1b, 0x17, 0x13, + 0x10, 0x1b, 0x17, 0x13, 0x10, 0x1b, 0x17, 0x13, 0x10, 0x1b, 0x17, 0x13, 0x10, 0x1b, 0x17, 0x00, +]; + +/// Table E3.11 — `ecplangletab[]`. The band phase angle (in the spec's +/// normalised `-1.0 ..= 1.0` units, representing `-pi ..= pi`) for each +/// 6-bit `ecplangle` index. Codes 0..=31 are the non-negative angles +/// `0.0, 0.03125, … 0.96875`; codes 32..=63 are the negative angles +/// `-1.0, -0.96875, … -0.03125`. +pub const ECPL_ANGLE_TAB: [f32; 64] = { + let mut t = [0.0f32; 64]; + let mut i = 0; + while i < 32 { + t[i] = i as f32 / 32.0; + i += 1; + } + let mut i = 32; + while i < 64 { + t[i] = -1.0 + (i - 32) as f32 / 32.0; + i += 1; + } + t +}; + +/// Table E3.12 — `ecplchaostab[]`. The chaos scaling factor (in +/// `0.0 ..= -1.0`) for each 3-bit `ecplchaos` index. The values are the +/// eighths `-k/7` for `k = 0..=7`. +pub const ECPL_CHAOS_TAB: [f32; 8] = [ + 0.0, -0.142857, -0.285714, -0.428571, -0.571429, -0.714286, -0.857143, -1.0, +]; + +/// §E.3.5.5.2 — the band amplitude `ampbnd[ch][bnd]` for a single 5-bit +/// `ecplamp` index, *before* the chaos modification. +/// +/// ```text +/// if (ecplamp == 31) ampbnd = 0 +/// else ampbnd = (ecplampmanttab[ecplamp] / 32) >> ecplampexptab[ecplamp] +/// ``` +/// +/// The spec's `>> exp` is a fixed-point right shift, i.e. division by +/// `2^exp`; we evaluate it in floating point as `(mant / 32) / 2^exp`. +/// Index 0 (`mant = 0x20`, `exp = 0`) yields `1.0` (0 dB); index 30 +/// (`mant = 0x17`, `exp = 7`) yields `≈ 0.005615` (≈ -45.01 dB); index +/// 31 yields `0.0` (minus-infinity dB). +#[inline] +pub fn ampbnd(ecplamp: u8) -> f32 { + let idx = (ecplamp & 0x1f) as usize; + if idx == 31 { + return 0.0; + } + let mant = ECPL_AMP_MANT_TAB[idx] as f32 / 32.0; + let exp = ECPL_AMP_EXP_TAB[idx] as i32; + mant / 2f32.powi(exp) +} + +/// §E.3.5.5.2 — the chaos value `chaos[ch][bnd]` for a single 3-bit +/// `ecplchaos` index. The first coupled channel always reads `0.0` +/// regardless of the index (its chaos/angle are spec-fixed to zero). +#[inline] +pub fn chaos_value(ecplchaos: u8, is_first_coupled: bool) -> f32 { + if is_first_coupled { + 0.0 + } else { + ECPL_CHAOS_TAB[(ecplchaos & 0x07) as usize] + } +} + +/// §E.3.5.5.3 — the band angle `angle[ch][bnd]` for a single 6-bit +/// `ecplangle` index. The first coupled channel always reads `0.0`. +#[inline] +pub fn angle_value(ecplangle: u8, is_first_coupled: bool) -> f32 { + if is_first_coupled { + 0.0 + } else { + ECPL_ANGLE_TAB[(ecplangle & 0x3f) as usize] + } +} + +/// §E.3.5.5.2 — the fully-processed per-band amplitudes for one channel, +/// including the chaos modification. +/// +/// For each band, [`ampbnd`] converts the `ecplamp` index to a linear +/// gain, then the chaos modification +/// +/// ```text +/// if (ecpltrans == 0 && ch != firstchincpl) +/// ampbnd[bnd] *= 1 + 0.38 * chaos[bnd] +/// ``` +/// +/// scales it (note `chaos` is `<= 0`, so this *reduces* the amplitude of +/// non-transient, non-first coupled channels). `is_first_coupled` carries +/// the `ch == firstchincpl` test; `trans` carries `ecpltrans[ch]`. +/// +/// Returns a vector of length `necplbnd` (the length of `params.amp`). +pub fn process_band_amplitudes(params: &EcplChannelParams, is_first_coupled: bool) -> Vec { + let mut out = Vec::with_capacity(params.amp.len()); + for (bnd, &_idx) in params.amp.iter().enumerate() { + let mut a = ampbnd(amp_idx); + // The chaos modification applies only to non-first coupled + // channels with no transient. `chaos`/`angle` are only present + // for those channels (param2e), so a missing entry means 0. + if !params.trans && !is_first_coupled { + let chaos_idx = params.chaos.get(bnd).copied().unwrap_or(0); + let chaos = chaos_value(chaos_idx, is_first_coupled); + a *= 1.0 + 0.38 * chaos; + } + out.push(a); + } + out +} + +/// §E.3.5.5.2 — expand per-band values to per-sub-band then per-bin. +/// +/// `band_vals` holds one value per enhanced-coupling *band*; this fans +/// each band's value out across every transform-coefficient bin it spans, +/// using the merge structure `bndstrc[]` to walk the same band boundaries +/// the parse used. The returned vector is indexed by *bin offset from +/// `begin_bin`* (i.e. element `0` is transform coefficient +/// `ecplsubbndtab[begin_subbnd]`), with length +/// `end_bin(end) - begin_bin(begin)`. +/// +/// This is the §E.3.5.5.2 `ampbin[ch][bin]` reconstruction; the same +/// fan-out applies to the no-interpolation angle path (§E.3.5.5.3) and to +/// the per-bin chaos / random expansion. +pub fn expand_bands_to_bins( + begin: usize, + end: usize, + bndstrc: &[bool; N_ECPL_SUBBND], + band_vals: &[f32], +) -> Vec { + let total = end_bin(end).saturating_sub(begin_bin(begin)); + let mut out = Vec::with_capacity(total); + // `bnd` tracks the current band index into `band_vals`. The first + // active sub-band always opens band 0 (its merge bit is never sent). + let mut bnd: isize = -1; + for sbnd in begin..end.min(N_ECPL_SUBBND) { + if !bndstrc[sbnd] { + bnd += 1; + } + let val = if bnd >= 0 { + band_vals.get(bnd as usize).copied().unwrap_or(0.0) + } else { + // Defensive: a leading merge bit (spec guarantees the first + // active sub-band starts a band, so this is malformed input). + band_vals.first().copied().unwrap_or(0.0) + }; + let nbins = ECPL_SUBBND_TAB[sbnd + 1] - ECPL_SUBBND_TAB[sbnd]; + for _ in 0..nbins { + out.push(val); + } + } + out +} + +/// §E.3.5.5.3 — per-bin angle reconstruction with linear interpolation +/// between band centres (the `ecplangleintrp == 1` path). +/// +/// `band_angles` holds one angle per band (the [`angle_value`] outputs); +/// `nbins_per_bnd` holds the bin count of each band (from +/// [`band_bin_counts`]). The result is one angle per bin across the +/// active region, length `sum(nbins_per_bnd)`. Angles wrap into the +/// `-1.0 ..= 1.0` interval after each interpolation step, exactly as the +/// reference pseudo-code's `while` guards do. +/// +/// The single-band case (`nbands < 2`) has no inter-band slope, so the +/// one band's angle is simply fanned across all its bins. +pub fn interpolate_bin_angles(band_angles: &[f32], nbins_per_bnd: &[usize]) -> Vec { + let nbands = band_angles.len().min(nbins_per_bnd.len()); + let total: usize = nbins_per_bnd.iter().take(nbands).sum(); + let mut out = vec![0.0f32; total]; + if nbands == 0 { + return out; + } + if nbands == 1 { + for slot in out.iter_mut() { + *slot = band_angles[0]; + } + return out; + } + + let mut bin: usize = 0; + for bnd in 1..nbands { + let nbins_prev = nbins_per_bnd[bnd - 1]; + let nbins_curr = nbins_per_bnd[bnd]; + let angle_prev = band_angles[bnd - 1]; + let mut angle_curr = band_angles[bnd]; + // Unwrap the current band angle to within one step of the prev. + while (angle_curr - angle_prev) > 1.0 { + angle_curr -= 2.0; + } + while (angle_prev - angle_curr) > 1.0 { + angle_curr += 2.0; + } + let slope = (angle_curr - angle_prev) / ((nbins_curr + nbins_prev) as f32 / 2.0); + + // Lower half of the first band (walks downward from the centre). + if bnd == 1 && nbins_prev > 1 { + let (mut y, mut down_bin): (f32, usize); + if nbins_prev % 2 == 0 { + y = angle_prev - slope / 2.0; + down_bin = nbins_prev / 2 - 1; + } else { + y = angle_prev - slope; + down_bin = (nbins_prev - 3) / 2; + } + let count = down_bin + 1; + for _ in 0..count { + let ytmp = y; + while y > 1.0 { + y -= 2.0; + } + while y < -1.0 { + y += 2.0; + } + out[down_bin] = y; + down_bin = down_bin.saturating_sub(1); + y = ytmp - slope; + } + bin = count; + } + + let (mut y, count): (f32, usize) = if nbins_prev % 2 == 0 { + (angle_prev + slope / 2.0, nbins_curr / 2 + nbins_prev / 2) + } else { + (angle_prev, nbins_curr / 2 + nbins_prev.div_ceil(2)) + }; + for _ in 0..count { + let ytmp = y; + while y > 1.0 { + y -= 2.0; + } + while y < -1.0 { + y += 2.0; + } + if bin < total { + out[bin] = y; + bin += 1; + } + y = ytmp + slope; + } + + // Finish the last band when this is the final iteration. The + // reference carries `y`/`slope` out of the loop and runs one + // closing pass; we mirror that by detecting the last band here. + if bnd == nbands - 1 { + let last_count = if nbins_curr % 2 == 0 { + nbins_curr / 2 + } else { + nbins_curr / 2 + 1 + }; + for _ in 0..last_count { + let ytmp = y; + while y > 1.0 { + y -= 2.0; + } + while y < -1.0 { + y += 2.0; + } + if bin < total { + out[bin] = y; + bin += 1; + } + y = ytmp + slope; + } + } + } + + out +} + +// =========================================================================== +// §E.3.5.5.3 (closing) / §E.3.5.5.4 — de-correlation + complex synthesis +// =========================================================================== +// +// This layer closes the §E.3.5.5.3 angle path (the chaos × random +// de-correlation added to each bin angle) and implements the §E.3.5.5.4 +// channel transform-coefficient generation (the per-bin complex product +// against the reconstructed enhanced-coupling channel `Z[k]`). +// +// It consumes the per-bin amplitude / angle arrays produced by the r306 +// parameter-processing layer plus the per-bin `chaos` array (the same +// `expand_bands_to_bins` fan-out applied to the band chaos values) and a +// per-bin `rand` array (this module's [`RandNoTrans`] for non-transient +// channels, [`gen_rand_trans`] expanded for transient channels). The +// §E.3.5.5.1 carrier reconstruction that turns the de-normalised +// enhanced-coupling mantissas into the complex carrier `Z[k]` is +// implemented further below in [`reconstruct_carrier`]; the caller owns the +// cross-block mantissa buffers and feeds them in. + +/// §E.3.5.5.4 — the post-FFT MDCT synthesis window factor +/// `y[bin] = cos(2π · (N/4 + 0.5) / N · (bin + 0.5))` for the 512-point +/// transform (`N = 512`). The final real coefficient combines `y[bin]` +/// with the mirror `y[N/2 - 1 - bin]` per the spec's +/// `chmant = -2·(y[bin]·Zr + y[N/2-1-bin]·Zi)`. +#[inline] +pub fn synthesis_window(bin: usize, n: usize) -> f32 { + let nf = n as f32; + let arg = 2.0 * std::f32::consts::PI * (nf / 4.0 + 0.5) / nf * (bin as f32 + 0.5); + arg.cos() +} + +/// §E.3.5.5.3 (closing) — the de-correlation random sequence for a +/// **non-transient** channel: `rand_notrans[ch][bin]`. +/// +/// Per spec these values must be (a) uniformly distributed on `-1.0 ..= +/// 1.0`, (b) unique for each bin and channel, and (c) generated **once** +/// (e.g. at decoder init) and held constant for every block of every +/// frame. The generator itself is non-normative ("a scaled array of +/// random values"); a deterministic xorshift seeded per channel satisfies +/// all three properties while keeping decodes reproducible. +/// +/// The struct caches one full array of `N/2 = 256` values per channel so a +/// repeated block lookup is a cheap index, matching the spec's +/// "generated once" requirement. +#[derive(Clone, Debug)] +pub struct RandNoTrans { + /// One `[-1, 1]` value per transform-coefficient bin (`0 .. N/2`). + vals: Vec, +} + +impl RandNoTrans { + /// Build the per-bin random array for one channel. `n` is the + /// transform size (512); the array holds `n / 2` values. `ch` seeds + /// the generator so each channel gets a distinct, stable sequence. + pub fn new(ch: usize, n: usize) -> Self { + let half = n / 2; + // Seed per channel; a non-zero seed is required for xorshift. + let mut lfsr: u32 = 0x9E37_79B9 ^ (ch as u32).wrapping_mul(0x0100_0193).wrapping_add(1); + if lfsr == 0 { + lfsr = 1; + } + let mut vals = Vec::with_capacity(half); + for _ in 0..half { + vals.push(uniform_pm1(&mut lfsr)); + } + Self { vals } + } + + /// The cached `rand_notrans` value for `bin` (0-based from + /// transform-coefficient 0). Out-of-range bins return `0.0`. + #[inline] + pub fn get(&self, bin: usize) -> f32 { + self.vals.get(bin).copied().unwrap_or(0.0) + } +} + +/// §E.3.5.5.3 (closing) — the de-correlation random sequence for a +/// **transient** channel: `rand_trans[ch][bnd]`, one fresh value per +/// **band** generated for every block (the band values are afterward fanned +/// out to bins by [`expand_bands_to_bins`], exactly like the chaos array). +/// +/// Per spec these are uniform on `-1.0 ..= 1.0`, unique per band and +/// channel, and **new for each block** (in contrast to the non-transient +/// init-once array). The caller threads a mutable LFSR state so successive +/// blocks advance the sequence; `nbands` band values are produced. +pub fn gen_rand_trans(lfsr: &mut u32, nbands: usize) -> Vec { + let mut out = Vec::with_capacity(nbands); + for _ in 0..nbands { + out.push(uniform_pm1(lfsr)); + } + out +} + +/// One xorshift step mapped to a uniform value on `[-1, 1]`. Shared by the +/// non-transient and transient de-correlation generators. +#[inline] +fn uniform_pm1(lfsr: &mut u32) -> f32 { + let mut x = *lfsr; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + if x == 0 { + x = 1; + } + *lfsr = x; + (x as f32 / u32::MAX as f32) * 2.0 - 1.0 +} + +/// §E.3.5.5.3 (closing) — add the chaos-scaled random de-correlation term +/// to each per-bin angle and re-wrap into `-1.0 ..= 1.0`. +/// +/// ```text +/// angle[ch][bin] += chaos[ch][bin] * rand[ch][bin]; +/// if (angle < -1.0) angle += 2.0; +/// else if (angle >= 1.0) angle -= 2.0; +/// ``` +/// +/// `angles`, `chaos`, and `rand` are all per-bin arrays of equal length +/// (the `chaos` band values fanned out by [`expand_bands_to_bins`]; the +/// `rand` array is either the [`RandNoTrans`] cache for a non-transient +/// channel or the fanned-out [`gen_rand_trans`] band values for a transient +/// channel). The wrap is the spec's single-step fold — `chaos` is +/// `0.0 ..= -1.0` and `rand` is `-1.0 ..= 1.0`, so the product is within +/// `±1.0` and one fold suffices. +pub fn apply_decorrelation(angles: &mut [f32], chaos: &[f32], rand: &[f32]) { + for (bin, a) in angles.iter_mut().enumerate() { + let c = chaos.get(bin).copied().unwrap_or(0.0); + let r = rand.get(bin).copied().unwrap_or(0.0); + *a += c * r; + if *a < -1.0 { + *a += 2.0; + } else if *a >= 1.0 { + *a -= 2.0; + } + } +} + +/// §E.3.5.5.4 — generate the individual-channel transform coefficients +/// from the reconstructed enhanced-coupling channel. +/// +/// For each bin in the active region the spec forms the complex product of +/// the carrier `Z[bin] = Zr + j·Zi` with the per-channel coordinate +/// `ampbin · e^{jπ·angle}`: +/// +/// ```text +/// Zr_ch = Zr·amp·cos(π·angle) − Zi·amp·sin(π·angle) +/// Zi_ch = Zi·amp·cos(π·angle) + Zr·amp·sin(π·angle) +/// chmant[bin] = -2 · ( y[bin]·Zr_ch + y[N/2-1-bin]·Zi_ch ) +/// ``` +/// +/// `zr` / `zi` are the per-bin real / imaginary parts of the carrier `Z` +/// over `bin = 0 .. N/2` (the §E.3.5.5.1 FFT output, supplied by the +/// caller); `ampbin` / `bin_angle` are the per-bin amplitude / angle +/// arrays for this channel, indexed by *offset from `begin_bin`*. The +/// result `chmant` is written at the absolute transform-coefficient index +/// `begin_bin + offset`. `n` is the transform size (512). +/// +/// Bins outside `[begin_bin, begin_bin + ampbin.len())` are left +/// untouched, so the caller can pre-zero or pre-fill the low/independent +/// region. +#[allow(clippy::too_many_arguments)] +pub fn generate_channel_coeffs( + zr: &[f32], + zi: &[f32], + ampbin: &[f32], + bin_angle: &[f32], + begin_bin: usize, + n: usize, + out: &mut [f32], +) { + let half = n / 2; + let span = ampbin.len().min(bin_angle.len()); + for offset in 0..span { + let bin = begin_bin + offset; + if bin >= half { + break; + } + let amp = ampbin[offset]; + let angle = bin_angle[offset]; + let (s, c) = (std::f32::consts::PI * angle).sin_cos(); + let zr_bin = zr.get(bin).copied().unwrap_or(0.0); + let zi_bin = zi.get(bin).copied().unwrap_or(0.0); + let zr_ch = zr_bin * amp * c - zi_bin * amp * s; + let zi_ch = zi_bin * amp * c + zr_bin * amp * s; + let y_bin = synthesis_window(bin, n); + let y_mirror = synthesis_window(half - 1 - bin, n); + let chmant = -2.0 * (y_bin * zr_ch + y_mirror * zi_ch); + if let Some(slot) = out.get_mut(bin) { + *slot = chmant; + } + } +} + +// =========================================================================== +// §E.3.5.5.1 — enhanced-coupling channel processing (carrier `Z[k]`) +// =========================================================================== +// +// This closes the last deferred piece of §E.3.5.5: turning the +// de-normalised enhanced-coupling mantissas of the previous / current / +// next blocks into the non-aliased complex carrier `Z[k]` the +// §E.3.5.5.4 per-channel synthesis multiplies against. The procedure is +// stateful across blocks (it needs the prev + next block's coefficients), +// so the caller supplies all three buffers; the function itself is pure. +// +// The five spec steps: +// 1) zero-pad each block's ecpl mantissas into a length-N/2 MDCT buffer, +// 2) 512-sample IMDCT (§7.9.4.1 steps 1-5, windowed) of each buffer, +// 3) overlap-add prev's 2nd half + next's 1st half with curr, +// 4) apply the analysis window + xcos3/xsin3 oddly-stacked rotation, +// 5) forward DFT to obtain Z[k], k = 0..N-1. + +/// §E.3.5.5.1 step 4 — `xcos3[n] = cos(π·n/N)` for `N = 512`. +#[inline] +fn xcos3(n: usize) -> f32 { + (std::f32::consts::PI * n as f32 / 512.0).cos() +} + +/// §E.3.5.5.1 step 4 — `xsin3[n] = -sin(π·n/N)` for `N = 512`. +#[inline] +fn xsin3(n: usize) -> f32 { + -(std::f32::consts::PI * n as f32 / 512.0).sin() +} + +/// §E.3.5.5.1 step 5 output — the non-aliased complex enhanced-coupling +/// carrier `Z[k]`, `k = 0 .. N-1` (`N = 512`), as parallel real/imag +/// arrays. Element `k` is `Zr[k] + j·Zi[k]`. +#[derive(Clone, Debug)] +pub struct EcplCarrier { + /// Real part `Zr[k]`, `k = 0 .. 512`. + pub zr: [f32; 512], + /// Imaginary part `Zi[k]`, `k = 0 .. 512`. + pub zi: [f32; 512], +} + +/// §E.3.5.5.1 — reconstruct the non-aliased complex enhanced-coupling +/// channel `Z[k]` from the de-normalised enhanced-coupling mantissas of +/// the previous, current and next blocks. +/// +/// `prev` / `curr` / `next` are each the 256 (`= N/2`) MDCT transform +/// coefficients of the enhanced-coupling channel for that block, already +/// de-normalised and **zero outside** the active region +/// `[ecplstartmant, ecplendmant)` (step 1's `XPREV`/`XCURR`/`XNEXT` +/// definition). When enhanced coupling is not in use in the previous or +/// next block, the caller passes an all-zero buffer there (per the spec's +/// "set to zero" rule). +/// +/// The procedure (spec steps 2-5): +/// +/// 2. Each buffer is run through the windowed 512-sample IMDCT +/// (§7.9.4.1 steps 1-5): [`imdct_512_fft`] gives the bare time samples, +/// then the [`WINDOW`] (Table 7.33) multiply `x[n]·w[n]` / +/// `x[511-n]·w[n]` completes step 5. The result is the 512-sample +/// `xPREV` / `xCURR` / `xNEXT`. +/// 3. Overlap-add: `pcm[n] = xPREV[n+N/2] + xCURR[n]` and +/// `pcm[n+N/2] = xCURR[n+N/2] + xNEXT[n]` for `n = 0 .. N/2`. +/// 4. The complex analysis input is `pcm[n]·w·xcos3[n]` (real) and +/// `pcm[n]·w·xsin3[n]` (imag), with `w = w[n]` over the lower half and +/// `w = w[N/2-n-1]` over the upper half, mirroring the spec's two-arm +/// windowing. +/// 5. A normalised forward DFT ([`dft_512_forward`]) produces `Z[k]`. +pub fn reconstruct_carrier(prev: &[f32; 256], curr: &[f32; 256], next: &[f32; 256]) -> EcplCarrier { + // Step 2 — windowed IMDCT of each block. + let windowed = |x: &[f32; 256]| -> [f32; 512] { + let mut t = [0.0f32; 512]; + imdct_512_fft(x, &mut t); + for n in 0..256 { + t[n] *= WINDOW[n]; + t[511 - n] *= WINDOW[n]; + } + t + }; + let xprev = windowed(prev); + let xcurr = windowed(curr); + let xnext = windowed(next); + + // Step 3 — overlap-add into a single 512-sample pcm buffer. + // + // **Spec erratum (codified as `docs/audio/ac3/ac3-errata.md` entry + // E3):** as printed, §E.3.5.5.1 step 3 omits the §7.9.4.1 step-6 + // factor of 2 ("the factor of 2 scaling undoes headroom scaling + // performed in the encoder"): step 2 references only "steps 1 to 5" + // of §7.9.4.1, and step 3 — the direct analogue of step 6, an + // overlap-add over the same windowed samples — prints + // `pcm[n] = xPREV[n+N/2] + xCURR[n]` with no multiplier. Taken + // literally, the whole analysis → §E.3.5.5.4 synthesis chain + // returns exactly HALF the original MDCT coefficients (measured + // identity gain exactly 0.5 — every enhanced-coupling channel would + // decode 6 dB low), while the Table E3.10 amplitude ceiling of + // exactly 1.0 (`ecplamp = 0` is 0 dB per §E.3.5.4) pins the design + // intent at a unity identity. The corrected reading applies the + // same factor of 2 as §7.9.4.1 step 6. (The ETSI TS 102 366 V1.4.1 + // copy carries no counterpart clause — its §E.2.5.5 is + // amplitude-only and omits the channel-processing subclause + // entirely — so A/52:2018 §E.3.5.5.1 is the operative text.) + // Pinned by `tests::carrier_overlap_add_factor2_erratum_identity` + // (corrected fit gain 1.0 vs exactly 0.5 as printed) and + // `ecplenc::tests::analysis_synthesis_identity_amp1_angle0`. + let mut pcm = [0.0f32; 512]; + for n in 0..256 { + pcm[n] = 2.0 * (xprev[256 + n] + xcurr[n]); + pcm[256 + n] = 2.0 * (xcurr[256 + n] + xnext[n]); + } + + // Step 4 — oddly-stacked complex rotation. The window is `w[n]` over + // the lower half and `w[N/2-n-1]` over the upper half; `WINDOW[n]` + // holds `w[n]` for `n = 0 .. 256`. + let mut re = [0.0f32; 512]; + let mut im = [0.0f32; 512]; + for n in 0..256 { + let wl = WINDOW[n]; // w[n] + let wu = WINDOW[256 - n - 1]; // w[N/2-n-1] + re[n] = pcm[n] * wl * xcos3(n); + re[256 + n] = pcm[256 + n] * wu * xcos3(256 + n); + im[n] = pcm[n] * wl * xsin3(n); + im[256 + n] = pcm[256 + n] * wu * xsin3(256 + n); + } + + // Step 5 — forward DFT to the complex carrier `Z[k]`. + let (zr, zi) = dft_512_forward(&re, &im); + EcplCarrier { zr, zi } +} + +// =========================================================================== +// §E.3.5.5 — decoder-level enhanced-coupling synthesis orchestration +// =========================================================================== +// +// The pieces above are pure per-step primitives. This layer stitches them +// into the full §E.3.5.5 "for each block" procedure the decoder runs once +// the enhanced-coupling channel mantissas/exponents have been decoded and +// de-normalised: +// +// 1. Process the enhanced-coupling channel → carrier `Z[k]` +// ([`reconstruct_carrier`], from prev/curr/next mantissa buffers). +// 2. Prepare per-bin amplitudes for each coupled channel +// ([`process_band_amplitudes`] → [`expand_bands_to_bins`]). +// 3. Prepare per-bin angles for each coupled channel ([`angle_value`] → +// either [`expand_bands_to_bins`] or [`interpolate_bin_angles`], then +// [`apply_decorrelation`] with the chaos × random term). +// 4. Generate each coupled channel's transform coefficients from the +// carrier, amplitudes and angles ([`generate_channel_coeffs`]). +// +// The §E.3.5.5.1 carrier needs the *previous*, *current* and *next* +// block's enhanced-coupling mantissas; the decoder owns those buffers and +// passes all three in. The de-correlation random sources have cross-block +// lifetime (the non-transient array is generated once; the transient LFSR +// advances every block), so they live on the persistent [`EcplState`]. + +/// Maximum number of full-bandwidth channels that can be in coupling. +/// Matches the AC-3 `MAX_FBW` (5 fbw channels in 3/2 mode). +pub const ECPL_MAX_FBW: usize = 5; + +/// Cross-block enhanced-coupling synthesis state. Persisted on the decoder +/// for the lifetime of a stream so the §E.3.5.5.3 random de-correlation +/// sources keep their spec-required lifetimes: +/// +/// * `rand_notrans[ch]` — the non-transient random array, "generated once +/// (for example during decoder initialization) and … the same for every +/// block of every frame" (§E.3.5.5.3). Built lazily the first time a +/// channel needs it and then reused. +/// * `trans_lfsr` — the transient random generator state; the transient +/// values "must be new for each block", so this LFSR threads across +/// blocks/frames advancing the sequence. +/// * `prev_frame_last_mant` — the de-normalised enhanced-coupling mantissa +/// buffer of the *last* block of the immediately preceding frame, when +/// that block used enhanced coupling. The §E.3.5.5.1 carrier of block 0 +/// needs its "previous block" spectrum; block numbering is continuous +/// across the stream, so the previous frame's final enhanced-coupling +/// block is that neighbour. The spec's "set to zero" rule fires only +/// when enhanced coupling was *not* in use in that previous block, which +/// `None` represents (no carried spectrum → zero `prev`). +#[derive(Clone, Debug, Default)] +pub struct EcplState { + rand_notrans: [Option; ECPL_MAX_FBW], + trans_lfsr: u32, + prev_frame_last_mant: Option<[f32; 256]>, +} + +impl EcplState { + /// Fresh state (no random arrays generated yet, transient LFSR seeded). + pub fn new() -> Self { + Self { + rand_notrans: Default::default(), + // Non-zero seed required for the xorshift transient generator. + trans_lfsr: 0x2545_F491, + prev_frame_last_mant: None, + } + } + + /// The carried-over enhanced-coupling mantissa buffer of the previous + /// frame's last block, or `None` when that block did not use enhanced + /// coupling (the §E.3.5.5.1 "set to zero" boundary case for block 0's + /// `previous block`). + pub fn prev_frame_last_mant(&self) -> Option<&[f32; 256]> { + self.prev_frame_last_mant.as_ref() + } + + /// Record this frame's final enhanced-coupling mantissa buffer so the + /// next frame's block 0 carrier can consult it as its "previous block" + /// (§E.3.5.5.1). Pass `None` when the frame's last block did not use + /// enhanced coupling, which resets the carry to the zero boundary case. + pub fn set_prev_frame_last_mant(&mut self, mant: Option<[f32; 256]>) { + self.prev_frame_last_mant = mant; + } + + /// The cached non-transient random array for channel `ch`, building it + /// on first use (§E.3.5.5.3 "generated once"). `n` is the transform + /// size (512). + fn rand_notrans(&mut self, ch: usize, n: usize) -> &RandNoTrans { + if self.rand_notrans[ch].is_none() { + self.rand_notrans[ch] = Some(RandNoTrans::new(ch, n)); + } + self.rand_notrans[ch].as_ref().unwrap() + } +} + +/// One block's decoded enhanced-coupling inputs for [`synthesize_block`]. +/// +/// `mant` holds the de-normalised enhanced-coupling channel transform +/// coefficients for this block, indexed by absolute bin `0 .. N/2`, zero +/// outside the active region `[ecplstartmant, ecplendmant)` (the §E.3.5.5.1 +/// step-1 `XCURR` definition). `strategy` and `coords` are the decoded +/// geometry + per-channel parameters. `chincpl[ch]` flags the coupled +/// channels. +#[derive(Clone, Debug)] +pub struct EcplBlock { + /// De-normalised ecpl-channel MDCT coefficients, length `N/2 = 256`. + pub mant: [f32; 256], + /// Active strategy (band geometry) for this block. + pub strategy: EcplStrategy, + /// Per-channel decoded coordinates (angle-interp flag + per-band + /// amplitude/angle/chaos triples). + pub coords: EcplCoords, + /// Whether each fbw channel is in coupling this block. + pub chincpl: [bool; ECPL_MAX_FBW], +} + +/// §E.3.5.5 — synthesise the individual-channel transform coefficients for +/// one block of enhanced coupling, writing each coupled channel's result +/// into `out[ch]`. +/// +/// `prev` / `curr` / `next` are the three blocks' de-normalised +/// enhanced-coupling mantissa buffers; pass an all-zero `mant` (e.g. an +/// [`EcplBlock`] whose `mant` is `[0.0; 256]`) for a neighbour where +/// enhanced coupling is not in use, per the §E.3.5.5.1 "set to zero" rule. +/// The synthesis applies to `curr`'s strategy/coords; `prev`/`next` are +/// consulted only for their mantissa buffers (the carrier needs the +/// neighbouring spectra to suppress time-domain aliasing). +/// +/// `out[ch]` is the absolute-bin coefficient buffer for fbw channel `ch` +/// (length `N/2 = 256`); only bins in the active region +/// `[begin_bin, end_bin)` are written, so the caller pre-fills the +/// independently-coded low region. `n` is the transform size (512). +/// +/// The first coupled channel (`firstchincpl`) has angle/chaos fixed to `0` +/// (§E.3.5.5.2-3), so its synthesis is the pure carrier scaled by its +/// per-bin amplitude. +pub fn synthesize_block( + state: &mut EcplState, + prev: &EcplBlock, + curr: &EcplBlock, + next: &EcplBlock, + out: &mut [[f32; 256]], + n: usize, +) { + // Step 1-5 — reconstruct the non-aliased complex carrier `Z[k]`. + let carrier = reconstruct_carrier(&prev.mant, &curr.mant, &next.mant); + + let strat = &curr.strategy; + let begin = strat.begin_subbnd; + let end = strat.end_subbnd; + let begin_bin_abs = strat.begin_bin(); + let nbins_per_bnd = strat.band_bin_counts(); + + // The first coupled channel anchors the angle/chaos = 0 rule. + let firstchincpl = (0..ECPL_MAX_FBW).find(|&ch| curr.chincpl[ch]); + + for ch in 0..ECPL_MAX_FBW { + if !curr.chincpl[ch] { + continue; + } + let is_first = Some(ch) == firstchincpl; + let params = curr.coords.channels.get(ch); + + // Step 2 — per-bin amplitudes. Missing params (a channel whose + // coordinates were not re-sent this block) would normally reuse the + // prior block's values; the decoder threads the persisted params in + // via `coords`, so an empty `amp` means "all bands zero". + let band_amps = match params { + Some(p) if !p.amp.is_empty() => process_band_amplitudes(p, is_first), + _ => vec![0.0; nbins_per_bnd.len()], + }; + let ampbin = expand_bands_to_bins(begin, end, &strat.bndstrc, &band_amps); + + // Step 3 — per-bin angles. The first coupled channel and any channel + // without param2e have all-zero band angles. + let band_angles: Vec = if is_first { + vec![0.0; nbins_per_bnd.len()] + } else { + match params { + Some(p) if !p.angle.is_empty() => { + p.angle.iter().map(|&a| angle_value(a, false)).collect() + } + _ => vec![0.0; nbins_per_bnd.len()], + } + }; + let mut bin_angle = if curr.coords.angleintrp { + interpolate_bin_angles(&band_angles, &nbins_per_bnd) + } else { + expand_bands_to_bins(begin, end, &strat.bndstrc, &band_angles) + }; + + // Step 3 (closing) — chaos × random de-correlation. The first + // coupled channel has chaos 0 (no de-correlation); other channels + // fan their per-band chaos to bins, pick the transient/non-transient + // random source, and add the term. + if !is_first { + let trans = params.map(|p| p.trans).unwrap_or(false); + let band_chaos: Vec = match params { + Some(p) if !p.chaos.is_empty() => { + p.chaos.iter().map(|&c| chaos_value(c, false)).collect() + } + _ => vec![0.0; nbins_per_bnd.len()], + }; + let chaos_bin = expand_bands_to_bins(begin, end, &strat.bndstrc, &band_chaos); + let rand_bin: Vec = if trans { + // Transient: one fresh random value per band, fanned to bins. + let band_rand = gen_rand_trans(&mut state.trans_lfsr, nbins_per_bnd.len()); + expand_bands_to_bins(begin, end, &strat.bndstrc, &band_rand) + } else { + // Non-transient: the init-once per-bin array, sliced to the + // active region. + let rnt = state.rand_notrans(ch, n); + (0..bin_angle.len()) + .map(|off| rnt.get(begin_bin_abs + off)) + .collect() + }; + apply_decorrelation(&mut bin_angle, &chaos_bin, &rand_bin); + } + + // Step 4 — generate this channel's transform coefficients from the + // carrier and the per-bin amplitude/angle arrays. + generate_channel_coeffs( + &carrier.zr, + &carrier.zi, + &bin, + &bin_angle, + begin_bin_abs, + n, + &mut out[ch], + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::bits::{BitReader, BitWriter}; + + #[test] + fn ecplsubbndtab_matches_table_e3_9() { + // Spot-check the Table E3.9 values + the 6/12-bin widths. + assert_eq!(ECPL_SUBBND_TAB[0], 13); + assert_eq!(ECPL_SUBBND_TAB[4], 37); + assert_eq!(ECPL_SUBBND_TAB[21], 241); + assert_eq!(ECPL_SUBBND_TAB[22], 253); // sentinel + // Sub-bands 0..=3 are 6 bins wide. + for s in 0..4 { + assert_eq!(ECPL_SUBBND_TAB[s + 1] - ECPL_SUBBND_TAB[s], 6); + } + // Sub-bands 4..=21 are 12 bins wide. + for s in 4..N_ECPL_SUBBND { + assert_eq!(ECPL_SUBBND_TAB[s + 1] - ECPL_SUBBND_TAB[s], 12); + } + // Region spans tc 13..=252. + assert_eq!(begin_bin(0), 13); + assert_eq!(end_bin(22), 253); + } + + #[test] + fn begin_subbnd_table_e3_8() { + // ecplbegf < 3 → *2. + assert_eq!(begin_subbnd(0), 0); + assert_eq!(begin_subbnd(1), 2); + assert_eq!(begin_subbnd(2), 4); + // 3..=12 → +2. + assert_eq!(begin_subbnd(3), 5); + assert_eq!(begin_subbnd(7), 9); + assert_eq!(begin_subbnd(12), 14); + // >=13 → *2 - 10. + assert_eq!(begin_subbnd(13), 16); + assert_eq!(begin_subbnd(14), 18); + assert_eq!(begin_subbnd(15), 20); + } + + #[test] + fn end_subbnd_table_e3_8_no_spx() { + // SPX off: ecplendf + 7. + assert_eq!(end_subbnd(false, 0, 0), 7); + assert_eq!(end_subbnd(false, 8, 0), 15); + assert_eq!(end_subbnd(false, 15, 0), 22); + } + + #[test] + fn end_subbnd_table_e3_8_with_spx() { + // SPX on, spxbegf < 6 → spxbegf + 5. + assert_eq!(end_subbnd(true, 0, 0), 5); + assert_eq!(end_subbnd(true, 0, 5), 10); + // SPX on, spxbegf >= 6 → spxbegf * 2. + assert_eq!(end_subbnd(true, 0, 6), 12); + assert_eq!(end_subbnd(true, 0, 7), 14); + // ecplendf is ignored when SPX is active. + assert_eq!(end_subbnd(true, 15, 6), 12); + } + + #[test] + fn default_bndstrc_table_e2_14() { + // Sub-bands 0..=8 are all zero (never transmitted; always merge=0). + for s in 0..=8 { + assert!(!DEFAULT_ECPL_BNDSTRC[s], "sbnd {s} should be 0"); + } + // The Table E2.14 merge rows. + assert!(DEFAULT_ECPL_BNDSTRC[9]); + assert!(!DEFAULT_ECPL_BNDSTRC[10]); + assert!(DEFAULT_ECPL_BNDSTRC[11]); + assert!(!DEFAULT_ECPL_BNDSTRC[12]); + assert!(DEFAULT_ECPL_BNDSTRC[13]); + assert!(!DEFAULT_ECPL_BNDSTRC[14]); + assert!(DEFAULT_ECPL_BNDSTRC[15]); + assert!(DEFAULT_ECPL_BNDSTRC[16]); + assert!(DEFAULT_ECPL_BNDSTRC[17]); + assert!(!DEFAULT_ECPL_BNDSTRC[18]); + assert!(DEFAULT_ECPL_BNDSTRC[19]); + assert!(DEFAULT_ECPL_BNDSTRC[20]); + assert!(DEFAULT_ECPL_BNDSTRC[21]); + } + + #[test] + fn necplbnd_no_merges() { + // All-zero banding → every sub-band is its own band. + let bndstrc = [false; N_ECPL_SUBBND]; + // begin=9, end=22 → 13 sub-bands, no merges → 13 bands. + assert_eq!(necplbnd(9, 22, &bndstrc), 13); + // A small span: begin=5, end=8 → 3 sub-bands. + assert_eq!(necplbnd(5, 8, &bndstrc), 3); + } + + #[test] + fn necplbnd_with_default_banding() { + // Default banding over the full high span begin=9, end=22. + // 13 sub-bands; merges at 9,11,13,15,16,17,19,20,21 that fall in + // [9,22) → 9 merges → 13 - 9 = 4 bands. + let n = necplbnd(9, 22, &DEFAULT_ECPL_BNDSTRC); + assert_eq!(n, 4); + } + + #[test] + fn band_bin_counts_no_merges_low_subbands() { + // begin=0, end=4 → narrow 6-bin sub-bands, no merges → four + // 6-bin bands. + let bndstrc = [false; N_ECPL_SUBBND]; + let counts = band_bin_counts(0, 4, &bndstrc); + assert_eq!(counts, vec![6, 6, 6, 6]); + } + + #[test] + fn band_bin_counts_default_banding_high() { + // begin=9, end=22 under the default banding. Sub-bands here are + // all 12-bin wide. Bands open at sub-bands with merge==0 + // (9 opens? no — 9 has merge=1 but it is the first sub-band of + // the span, so per band_bin_counts the leading merge starts a new + // band defensively). To match the spec's guarantee that the + // first sub-band of the active span is never a merge, test a span + // whose first sub-band has merge=0. + // + // begin=10, end=22 → first sub-band 10 has merge=0 (new band). + // Merge bits in [10,22): 11,13,15,16,17,19,20,21 → 8 merges. + // 12 sub-bands → 4 bands. Each band's bin count = 12 * (sub-bands + // in band). + let counts = band_bin_counts(10, 22, &DEFAULT_ECPL_BNDSTRC); + // Bands: [10,11]=24, [12,13]=24, [14,15,16,17]=48, [18,19,20,21]=48. + assert_eq!(counts, vec![24, 24, 48, 48]); + // Total bins == span bins (12 each * 12 sub-bands = 144). + let total: usize = counts.iter().sum(); + assert_eq!(total, 144); + assert_eq!(total, end_bin(22) - begin_bin(10)); + } + + #[test] + fn band_bin_counts_length_equals_necplbnd() { + // The vector length must equal necplbnd for any banding. + let begin = 10; + let end = 22; + let n = necplbnd(begin, end, &DEFAULT_ECPL_BNDSTRC); + let counts = band_bin_counts(begin, end, &DEFAULT_ECPL_BNDSTRC); + assert_eq!(counts.len(), n); + } + + // ---- §E.2.3.3.16-19 strategy parse ---- + + #[test] + fn parse_strategy_no_spx_default_banding() { + // ecplbegf = 7 → begin = 9; SPX off, ecplendf = 15 → end = 22; + // ecplbndstrce = 0 → reuse the supplied default banding (no merge + // bits on the wire). + let mut w = BitWriter::new(); + w.write_u32(7, 4); // ecplbegf + w.write_u32(15, 4); // ecplendf + w.write_u32(0, 1); // ecplbndstrce = 0 + let bytes = w.finish(); + + let mut br = BitReader::new(&bytes); + let strat = parse_strategy(&mut br, false, 0, &DEFAULT_ECPL_BNDSTRC).unwrap(); + assert_eq!(strat.begin_subbnd, 9); + assert_eq!(strat.end_subbnd, 22); + // §E.2.3.3.19: the resolved structure masks the default table's + // merge bit AT the first active sub-band (entries up to and + // including max(begin, 8) are always zero) — sub-band 9 starts + // band 0 here, so the region has 5 bands, not 4. (The pre-r406 + // code counted the phantom merge in necplbnd while the + // §E.3.5.5.1 band walk opened a band anyway — a one-band + // coordinate-count desync on any default-banded region + // beginning at sub-band 9+.) + let mut expected = DEFAULT_ECPL_BNDSTRC; + expected[9] = false; + assert_eq!(strat.bndstrc, expected); + assert_eq!(strat.necplbnd, 5); + assert_eq!(strat.band_bin_counts().len(), 5); + assert_eq!(strat.begin_bin(), 97); + assert_eq!(strat.end_bin(), 253); + // Exactly 9 bits consumed (4 + 4 + 1). + assert_eq!(br.bit_position(), 9); + } + + #[test] + fn parse_strategy_spx_active_skips_ecplendf() { + // SPX on, spxbegf = 5 → end = spxbegf + 5 = 10; ecplendf is NOT + // transmitted. ecplbegf = 5 → begin = 7. ecplbndstrce = 0. + let mut w = BitWriter::new(); + w.write_u32(5, 4); // ecplbegf + w.write_u32(0, 1); // ecplbndstrce = 0 (no ecplendf field) + let bytes = w.finish(); + + let mut br = BitReader::new(&bytes); + let strat = parse_strategy(&mut br, true, 5, &DEFAULT_ECPL_BNDSTRC).unwrap(); + assert_eq!(strat.begin_subbnd, 7); + assert_eq!(strat.end_subbnd, 10); + // Only 5 bits consumed because ecplendf is omitted under SPX. + assert_eq!(br.bit_position(), 5); + } + + #[test] + fn parse_strategy_explicit_banding_bits() { + // ecplbegf = 7 → begin = 9; ecplendf = 15 → end = 22. + // ecplbndstrce = 1 → merge bits for sbnd in [max(9,10), 22) = + // [10, 22): that's 12 bits. Set every other bit so we can verify + // placement: 10=1, 11=0, 12=1, ... merge bits transmitted from + // sbnd 10 upward. The first active sub-band (9) is never a merge + // bit (not transmitted) and stays false. + let mut w = BitWriter::new(); + w.write_u32(7, 4); // ecplbegf + w.write_u32(15, 4); // ecplendf + w.write_u32(1, 1); // ecplbndstrce = 1 + let pattern = [ + true, false, true, false, true, false, true, false, true, false, true, false, + ]; + for &b in &pattern { + w.write_u32(b as u32, 1); + } + let bytes = w.finish(); + + let mut br = BitReader::new(&bytes); + let strat = parse_strategy(&mut br, false, 0, &DEFAULT_ECPL_BNDSTRC).unwrap(); + // Sub-band 9 is never transmitted → false. + assert!(!strat.bndstrc[9]); + // Sub-bands 10..22 match the pattern. + for (i, &b) in pattern.iter().enumerate() { + assert_eq!(strat.bndstrc[10 + i], b, "sbnd {}", 10 + i); + } + // 6 merges in the pattern → 13 sub-bands − 6 = 7 bands. + assert_eq!(strat.necplbnd, 13 - 6); + // 4 + 4 + 1 + 12 = 21 bits. + assert_eq!(br.bit_position(), 21); + } + + // ---- §E.2.3.3.20-26 coordinate parse ---- + + #[test] + fn parse_coords_first_block_implicit_present() { + // 2/0: both channels in coupling, both firstcplcos (first block). + // necplbnd = 2 for a compact test. ch0 = first coupled channel: + // param1e implicit 1 (amps follow), param2e = 0 (angle/chaos fixed + // to 0, not sent), no ecpltrans. ch1: param1e + param2e implicit + // 1, then ecpltrans (1 bit). + let necplbnd = 2usize; + let mut w = BitWriter::new(); + w.write_u32(1, 1); // ecplangleintrp = 1 + // ch0: implicit param1e=1 → 2 amps (5 bits each). + w.write_u32(3, 5); + w.write_u32(7, 5); + // ch1: implicit param1e=1 → 2 amps; implicit param2e=1 → 2×(angle + // 6 + chaos 3); ecpltrans = 1. + w.write_u32(11, 5); + w.write_u32(15, 5); + w.write_u32(20, 6); + w.write_u32(4, 3); + w.write_u32(33, 6); + w.write_u32(2, 3); + w.write_u32(1, 1); // ecpltrans[1] + let bytes = w.finish(); + + let mut br = BitReader::new(&bytes); + let chincpl = [true, true]; + let mut firstcplcos = [true, true]; + let c = parse_coords(&mut br, 2, &chincpl, &mut firstcplcos, necplbnd).unwrap(); + assert!(c.angleintrp); + // firstcplcos cleared for both. + assert_eq!(firstcplcos, [false, false]); + + // ch0 (first coupled): param1e, no param2e, no trans. + assert!(c.channels[0].param1e); + assert!(!c.channels[0].param2e); + assert_eq!(c.channels[0].amp, vec![3, 7]); + assert!(c.channels[0].angle.is_empty()); + assert!(c.channels[0].chaos.is_empty()); + assert!(!c.channels[0].trans); + + // ch1: param1e + param2e + trans. + assert!(c.channels[1].param1e); + assert!(c.channels[1].param2e); + assert_eq!(c.channels[1].amp, vec![11, 15]); + assert_eq!(c.channels[1].angle, vec![20, 33]); + assert_eq!(c.channels[1].chaos, vec![4, 2]); + assert!(c.channels[1].trans); + + // 1 + (2×5) + (2×5 + 2×(6+3) + 1) = 1 + 10 + 29 = 40 bits. + assert_eq!(br.bit_position(), 40); + } + + #[test] + fn parse_coords_later_block_explicit_exist_bits() { + // Later block (firstcplcos already cleared): exist bits are read + // from the wire. ch0 first coupled: param1e bit only (no param2e, + // no trans). ch1: param1e + param2e bits + trans. + let necplbnd = 1usize; + let mut w = BitWriter::new(); + w.write_u32(0, 1); // ecplangleintrp = 0 + // ch0: param1e = 1 → 1 amp. + w.write_u32(1, 1); // param1e + w.write_u32(9, 5); // amp + // ch1: param1e = 0 (reuse), param2e = 1 → angle+chaos; trans = 0. + w.write_u32(0, 1); // param1e + w.write_u32(1, 1); // param2e + w.write_u32(42, 6); // angle + w.write_u32(5, 3); // chaos + w.write_u32(0, 1); // ecpltrans[1] + let bytes = w.finish(); + + let mut br = BitReader::new(&bytes); + let chincpl = [true, true]; + let mut firstcplcos = [false, false]; + let c = parse_coords(&mut br, 2, &chincpl, &mut firstcplcos, necplbnd).unwrap(); + assert!(!c.angleintrp); + assert!(c.channels[0].param1e); + assert!(!c.channels[0].param2e); + assert_eq!(c.channels[0].amp, vec![9]); + assert!(!c.channels[1].param1e); + assert!(c.channels[1].param2e); + assert!(c.channels[1].amp.is_empty()); + assert_eq!(c.channels[1].angle, vec![42]); + assert_eq!(c.channels[1].chaos, vec![5]); + assert!(!c.channels[1].trans); + // 1 + (1 + 5) + (1 + 1 + 6 + 3 + 1) = 1 + 6 + 12 = 19 bits. + assert_eq!(br.bit_position(), 19); + } + + #[test] + fn parse_coords_channel_not_in_coupling_rearms_marker() { + // 3/0: ch1 not in coupling. firstcplcos[1] must be re-armed to + // true; its params stay default. ch0 + ch2 are coupled. + let necplbnd = 1usize; + let mut w = BitWriter::new(); + w.write_u32(0, 1); // ecplangleintrp + // ch0 (first coupled, first block): implicit param1e → 1 amp. + w.write_u32(8, 5); + // ch1 skipped (not in coupling). + // ch2 (second coupled, first block): implicit param1e + param2e + + // trans. + w.write_u32(12, 5); // amp + w.write_u32(30, 6); // angle + w.write_u32(6, 3); // chaos + w.write_u32(0, 1); // trans + let bytes = w.finish(); + + let mut br = BitReader::new(&bytes); + let chincpl = [true, false, true]; + let mut firstcplcos = [true, true, true]; + let c = parse_coords(&mut br, 3, &chincpl, &mut firstcplcos, necplbnd).unwrap(); + // ch1 re-armed, ch0 + ch2 cleared. + assert_eq!(firstcplcos, [false, true, false]); + assert_eq!(c.channels[0].amp, vec![8]); + assert_eq!(c.channels[1], EcplChannelParams::default()); + assert_eq!(c.channels[2].amp, vec![12]); + assert_eq!(c.channels[2].angle, vec![30]); + // ch2 is the *second* coupled channel → carries param2e + trans. + assert!(c.channels[2].param2e); + // 1 + 5 + (5 + 6 + 3 + 1) = 21 bits. + assert_eq!(br.bit_position(), 21); + } + + #[test] + fn merge_reused_params_threads_prior_block_values() { + // Prior block: full coordinate sets for a 2-band geometry. + let prev = EcplCoords { + angleintrp: false, + channels: vec![ + EcplChannelParams { + param1e: true, + param2e: false, + amp: vec![3, 4], + angle: vec![], + chaos: vec![], + trans: false, + }, + EcplChannelParams { + param1e: true, + param2e: true, + amp: vec![5, 6], + angle: vec![10, 20], + chaos: vec![1, 2], + trans: false, + }, + ], + }; + // Current block: ch0 reuses amps; ch1 reuses angle/chaos but + // retransmits amps. + let mut curr = EcplCoords { + angleintrp: false, + channels: vec![ + EcplChannelParams::default(), + EcplChannelParams { + param1e: true, + param2e: false, + amp: vec![7, 8], + angle: vec![], + chaos: vec![], + trans: true, + }, + ], + }; + merge_reused_params(&mut curr, &prev, 2); + assert_eq!(curr.channels[0].amp, vec![3, 4]); // §2.3.3.21 reuse + assert_eq!(curr.channels[1].amp, vec![7, 8]); // fresh, kept + assert_eq!(curr.channels[1].angle, vec![10, 20]); // §2.3.3.22 reuse + assert_eq!(curr.channels[1].chaos, vec![1, 2]); + assert!(curr.channels[1].trans); // trans is per-block, kept + } + + #[test] + fn merge_reused_params_ignores_stale_band_count() { + // Prior coords sized for 3 bands; the active strategy has 2 — + // stale values must NOT be carried. + let prev = EcplCoords { + angleintrp: false, + channels: vec![EcplChannelParams { + param1e: true, + param2e: false, + amp: vec![3, 4, 5], + angle: vec![], + chaos: vec![], + trans: false, + }], + }; + let mut curr = EcplCoords { + angleintrp: false, + channels: vec![EcplChannelParams::default()], + }; + merge_reused_params(&mut curr, &prev, 2); + assert!(curr.channels[0].amp.is_empty()); + } + + // ---- §E.3.5.5.2 / §E.3.5.5.3 parameter processing ---- + + fn db(linear: f32) -> f32 { + 20.0 * linear.log10() + } + + #[test] + fn ampbnd_endpoints_table_e3_10() { + // Index 0: mant 0x20 (=32) / 32 = 1.0, exp 0 → 0 dB. + assert!((ampbnd(0) - 1.0).abs() < 1e-6); + assert!(db(ampbnd(0)).abs() < 1e-4); + // Index 31: minus-infinity dB → amplitude 0. + assert_eq!(ampbnd(31), 0.0); + // Index 30: mant 0x17 (=23)/32 = 0.71875, exp 7 → /128 ≈ 0.005615 + // → ≈ -45.01 dB (the spec's documented lowest finite gain). + let a30 = ampbnd(30); + assert!((a30 - (23.0 / 32.0) / 128.0).abs() < 1e-7); + assert!((db(a30) - (-45.01)).abs() < 0.05); + // Monotone non-increasing across the finite range 0..=30. + for i in 1..=30u8 { + assert!( + ampbnd(i) <= ampbnd(i - 1) + 1e-7, + "amp[{i}] not <= amp[{}]", + i - 1 + ); + } + // Each exponent step (every 4 indices) approximately halves the + // mantissa-1.0 anchor: index 4 (mant 0x10/32 = 0.5, exp 0) and + // index 8 (mant 0x10/32, exp 1 → 0.25). + assert!((ampbnd(4) - 0.5).abs() < 1e-6); + assert!((ampbnd(8) - 0.25).abs() < 1e-6); + } + + #[test] + fn angle_and_chaos_tables() { + // Table E3.11: code 0 → 0.0; code 32 → -1.0; code 63 → -0.03125. + assert_eq!(ECPL_ANGLE_TAB[0], 0.0); + assert!((ECPL_ANGLE_TAB[31] - 0.96875).abs() < 1e-6); + assert!((ECPL_ANGLE_TAB[32] - (-1.0)).abs() < 1e-6); + assert!((ECPL_ANGLE_TAB[63] - (-0.03125)).abs() < 1e-6); + // Table E3.12: eighths -k/7. + assert_eq!(ECPL_CHAOS_TAB[0], 0.0); + assert!((ECPL_CHAOS_TAB[7] - (-1.0)).abs() < 1e-6); + assert!((ECPL_CHAOS_TAB[3] - (-3.0 / 7.0)).abs() < 1e-5); + // First-coupled channel forces angle/chaos to 0 regardless of code. + assert_eq!(angle_value(40, true), 0.0); + assert_eq!(chaos_value(5, true), 0.0); + assert!((angle_value(40, false) - (-0.75)).abs() < 1e-6); + } + + #[test] + fn chaos_modification_reduces_amplitude() { + // A non-first, non-transient channel with chaos applied: the + // §E.3.5.5.2 modification `*= 1 + 0.38*chaos` with chaos <= 0 + // reduces the gain. + let params = EcplChannelParams { + param1e: true, + param2e: true, + amp: vec![0, 4], // gains 1.0, 0.5 + chaos: vec![7, 0], // chaos -1.0, 0.0 + angle: vec![0, 0], + trans: false, + }; + let amps = process_band_amplitudes(¶ms, false); + // band 0: 1.0 * (1 + 0.38 * -1.0) = 0.62. + assert!((amps[0] - 0.62).abs() < 1e-6); + // band 1: 0.5 * (1 + 0.38 * 0.0) = 0.5. + assert!((amps[1] - 0.5).abs() < 1e-6); + + // Same params but transient present → no chaos modification. + let mut tp = params.clone(); + tp.trans = true; + let amps_t = process_band_amplitudes(&tp, false); + assert!((amps_t[0] - 1.0).abs() < 1e-6); + + // First coupled channel → no chaos modification even without + // transient (its chaos is fixed to 0). + let amps_first = process_band_amplitudes(¶ms, true); + assert!((amps_first[0] - 1.0).abs() < 1e-6); + } + + #[test] + fn expand_bands_to_bins_fans_out_per_subband() { + // begin sub-band 0, end sub-band 2: sub-bands 0 (6 bins) and 1 + // (6 bins). No merge → two bands of 6 bins each. begin_bin = 13, + // end_bin = ECPL_SUBBND_TAB[2] = 25 → 12 bins total. + let bndstrc = [false; N_ECPL_SUBBND]; + let out = expand_bands_to_bins(0, 2, &bndstrc, &[2.0, 5.0]); + assert_eq!(out.len(), 12); + assert!(out[..6].iter().all(|&v| (v - 2.0).abs() < 1e-9)); + assert!(out[6..].iter().all(|&v| (v - 5.0).abs() < 1e-9)); + + // Merge sub-band 1 into 0 → single band of 12 bins, one value. + let mut merged = [false; N_ECPL_SUBBND]; + merged[1] = true; + let out2 = expand_bands_to_bins(0, 2, &merged, &[3.0]); + assert_eq!(out2.len(), 12); + assert!(out2.iter().all(|&v| (v - 3.0).abs() < 1e-9)); + } + + #[test] + fn interpolate_single_band_is_constant() { + // One band → angle fanned across all bins, no slope. + let out = interpolate_bin_angles(&[0.25], &[6]); + assert_eq!(out.len(), 6); + assert!(out.iter().all(|&v| (v - 0.25).abs() < 1e-9)); + } + + #[test] + fn interpolate_two_bands_wraps_and_fills() { + // Two equal-size bands; verify output length == sum of bin counts + // and every angle stays within the wrapped -1.0 ..= 1.0 interval. + let band_angles = [0.0, 0.5]; + let nbins = [6usize, 6usize]; + let out = interpolate_bin_angles(&band_angles, &nbins); + assert_eq!(out.len(), 12); + for &v in &out { + assert!((-1.0..=1.0).contains(&v), "angle {v} out of range"); + } + // The interpolation walks from below the first band centre up + // through the second; values should be (weakly) increasing in the + // interior where no wrap occurs. + // First band centre region near 0.0, last band region near 0.5. + assert!(out[0] < out[out.len() - 1] + 1e-6); + } + + #[test] + fn interpolate_wrap_across_pi() { + // angle_prev = 0.9, angle_curr = -0.9: the raw difference is + // -1.8 but the spec unwraps (-0.9 + 2.0 = 1.1) so the slope is + // small + positive, crossing the +1/-1 boundary which the wrap + // guards fold back. Output must stay in range. + let out = interpolate_bin_angles(&[0.9, -0.9], &[6, 6]); + assert_eq!(out.len(), 12); + for &v in &out { + assert!((-1.0..=1.0).contains(&v), "wrapped angle {v} out of range"); + } + } + + // ---- §E.3.5.5.3 (closing) / §E.3.5.5.4 synthesis ---- + + #[test] + fn rand_notrans_properties() { + // Per spec: uniform on [-1, 1], unique per bin, generated once and + // stable across calls, and distinct between channels. + let n = 512; + let r0 = RandNoTrans::new(0, n); + let r0_again = RandNoTrans::new(0, n); + let r1 = RandNoTrans::new(1, n); + // 256 values for a 512-point transform. + for bin in 0..n / 2 { + let v = r0.get(bin); + assert!((-1.0..=1.0).contains(&v), "value {v} out of range"); + // Stable across construction (the "generated once" requirement). + assert_eq!(v, r0_again.get(bin)); + } + // Out-of-range → 0.0. + assert_eq!(r0.get(n / 2), 0.0); + // Channels differ (overwhelmingly likely with distinct seeds; check + // the first few bins differ between ch0 and ch1). + let differ = (0..8).any(|b| (r0.get(b) - r1.get(b)).abs() > 1e-9); + assert!(differ, "channel 0 and 1 random arrays are identical"); + // Roughly zero-mean over the full array. + let mean: f32 = (0..n / 2).map(|b| r0.get(b)).sum::() / (n / 2) as f32; + assert!(mean.abs() < 0.15, "mean {mean} not near zero"); + } + + #[test] + fn rand_trans_advances_per_block() { + // Transient random: new values each block (the LFSR advances), each + // in [-1, 1]. Two consecutive draws of the same band count differ. + let mut lfsr = 0x1234_5678u32; + let blk0 = gen_rand_trans(&mut lfsr, 4); + let blk1 = gen_rand_trans(&mut lfsr, 4); + assert_eq!(blk0.len(), 4); + assert_eq!(blk1.len(), 4); + for &v in blk0.iter().chain(blk1.iter()) { + assert!((-1.0..=1.0).contains(&v), "value {v} out of range"); + } + assert_ne!(blk0, blk1, "consecutive blocks produced identical noise"); + } + + #[test] + fn apply_decorrelation_wraps_single_step() { + // chaos in [0, -1], rand in [-1, 1] → product in [-1, 1]; a single + // fold keeps the result in [-1, 1). + let mut angles = vec![0.9, -0.9, 0.0]; + let chaos = vec![-1.0, -1.0, -0.5]; + let rand = vec![0.5, -0.5, 1.0]; + apply_decorrelation(&mut angles, &chaos, &rand); + // 0.9 + (-1.0 * 0.5) = 0.4. + assert!((angles[0] - 0.4).abs() < 1e-6); + // -0.9 + (-1.0 * -0.5) = -0.4. + assert!((angles[1] - (-0.4)).abs() < 1e-6); + // 0.0 + (-0.5 * 1.0) = -0.5. + assert!((angles[2] - (-0.5)).abs() < 1e-6); + for &a in &angles { + assert!((-1.0..1.0).contains(&a), "angle {a} not wrapped"); + } + + // Force a positive overflow: 0.95 + (-1.0 * -0.5) = 1.45 → -0.55. + let mut a = vec![0.95]; + apply_decorrelation(&mut a, &[-1.0], &[-0.5]); + assert!((a[0] - (-0.55)).abs() < 1e-6); + // Force a negative underflow: -0.95 + (-1.0 * 0.5) = -1.45 → 0.55. + let mut b = vec![-0.95]; + apply_decorrelation(&mut b, &[-1.0], &[0.5]); + assert!((b[0] - 0.55).abs() < 1e-6); + } + + #[test] + fn synthesis_window_mirror_symmetry() { + // y[bin] = cos(2π·(N/4+0.5)/N·(bin+0.5)). For N=512 the factor is + // bounded by 1, and the spec pairs y[bin] with y[N/2-1-bin]. + let n = 512; + for bin in [0usize, 1, 64, 128, 255] { + let y = synthesis_window(bin, n); + assert!((-1.0..=1.0).contains(&y), "y[{bin}]={y} out of range"); + } + // bin 0 of the 512-point window: cos(2π·128.5/512·0.5) = + // cos(π·128.5/512). + let expected0 = (std::f32::consts::PI * 128.5 / 512.0).cos(); + assert!((synthesis_window(0, n) - expected0).abs() < 1e-6); + } + + #[test] + fn generate_channel_coeffs_unity_passthrough() { + // amp = 1, angle = 0 → coordinate is real unity → the channel + // coefficient is the spec's pure-carrier MDCT synthesis: + // chmant = -2·(y[bin]·Zr + y[N/2-1-bin]·Zi). Verify one bin. + let n = 512; + let half = n / 2; + let begin_bin = 13; + let span = 6; + // Carrier: distinctive per-bin real/imag. + let mut zr = vec![0.0f32; half]; + let mut zi = vec![0.0f32; half]; + for bin in begin_bin..begin_bin + span { + zr[bin] = (bin as f32) * 0.01; + zi[bin] = (bin as f32) * -0.02; + } + let ampbin = vec![1.0f32; span]; + let bin_angle = vec![0.0f32; span]; + let mut out = vec![0.0f32; half]; + generate_channel_coeffs(&zr, &zi, &bin, &bin_angle, begin_bin, n, &mut out); + + for offset in 0..span { + let bin = begin_bin + offset; + let y_bin = synthesis_window(bin, n); + let y_mirror = synthesis_window(half - 1 - bin, n); + // angle 0 → cos=1, sin=0 → Zr_ch=Zr, Zi_ch=Zi. + let expected = -2.0 * (y_bin * zr[bin] + y_mirror * zi[bin]); + assert!( + (out[bin] - expected).abs() < 1e-5, + "bin {bin}: {} != {expected}", + out[bin] + ); + } + // Below the region untouched. + assert_eq!(out[begin_bin - 1], 0.0); + // Above the region untouched. + assert_eq!(out[begin_bin + span], 0.0); + } + + #[test] + fn generate_channel_coeffs_amplitude_scales() { + // Halving amp halves the magnitude of the complex coordinate, and + // since the synthesis is linear in amp, the output halves too. + let n = 512; + let half = n / 2; + let begin_bin = 13; + let span = 4; + let mut zr = vec![0.0f32; half]; + let mut zi = vec![0.0f32; half]; + for bin in begin_bin..begin_bin + span { + zr[bin] = 0.3; + zi[bin] = 0.1; + } + let angle = vec![0.25f32; span]; // π/4 + let mut full = vec![0.0f32; half]; + generate_channel_coeffs(&zr, &zi, &vec![1.0; span], &angle, begin_bin, n, &mut full); + let mut halved = vec![0.0f32; half]; + generate_channel_coeffs( + &zr, + &zi, + &vec![0.5; span], + &angle, + begin_bin, + n, + &mut halved, + ); + for bin in begin_bin..begin_bin + span { + assert!( + (halved[bin] * 2.0 - full[bin]).abs() < 1e-5, + "bin {bin}: amplitude scaling not linear" + ); + } + } + + #[test] + fn generate_channel_coeffs_angle_rotation() { + // A pure-real carrier with angle = 0.5 (π/2) rotates the coordinate + // to pure-imaginary: Zr_ch = -Zi·amp·sin, Zi_ch = Zr·amp·cos... at + // angle 0.5, cos(π·0.5)=0, sin(π·0.5)=1 → Zr_ch = -Zi, Zi_ch = Zr. + let n = 512; + let half = n / 2; + let begin_bin = 13; + let mut zr = vec![0.0f32; half]; + let mut zi = vec![0.0f32; half]; + zr[begin_bin] = 0.4; + zi[begin_bin] = 0.0; + let mut out = vec![0.0f32; half]; + generate_channel_coeffs(&zr, &zi, &[1.0], &[0.5], begin_bin, n, &mut out); + // Zr_ch = 0.4·0 - 0·1 = 0; Zi_ch = 0·0 + 0.4·1 = 0.4. + let y_mirror = synthesis_window(half - 1 - begin_bin, n); + let expected = -2.0 * (0.0 + y_mirror * 0.4); + assert!((out[begin_bin] - expected).abs() < 1e-5); + } + + // ---- §E.3.5.5.1 carrier reconstruction ---- + + #[test] + fn reconstruct_carrier_all_zero_is_zero() { + // No enhanced-coupling energy in any block → the carrier is zero + // everywhere (the spec's "set to zero" boundary case). + let z = [0.0f32; 256]; + let car = reconstruct_carrier(&z, &z, &z); + let max = car + .zr + .iter() + .chain(car.zi.iter()) + .fold(0.0f32, |a, &b| a.max(b.abs())); + assert!(max < 1e-6, "zero input must give zero carrier, got {max}"); + } + + #[test] + fn reconstruct_carrier_steady_state_energy_lives_in_active_region() { + // A steady-state single MDCT bin in the active enhanced-coupling + // region across all three blocks. With perfect overlap (prev = + // curr = next) the time-domain reconstruction is non-aliased, and + // the carrier should carry meaningful (non-zero) energy. We assert + // the carrier is non-trivial and finite — the exact spectrum is the + // full §E.3.5.5.1 chain, validated structurally here. + let mut x = [0.0f32; 256]; + // Bin 100 sits inside the ecpl region (13..=252). + x[100] = 1.0; + let car = reconstruct_carrier(&x, &x, &x); + let energy: f32 = car + .zr + .iter() + .zip(car.zi.iter()) + .map(|(&r, &i)| r * r + i * i) + .sum(); + assert!(energy > 1e-3, "steady tone produced no carrier energy"); + for (&r, &i) in car.zr.iter().zip(car.zi.iter()) { + assert!(r.is_finite() && i.is_finite(), "carrier has non-finite bin"); + } + } + + #[test] + fn reconstruct_carrier_linear_in_input() { + // The whole §E.3.5.5.1 chain (IMDCT, overlap-add, window, DFT) is + // linear, so scaling the input mantissas by k scales Z[k] by k. + let mut x = [0.0f32; 256]; + let mut s: u32 = 0x00C0_FFEE; + for v in x.iter_mut().take(253).skip(13) { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + *v = (s as i32 as f32) / (i32::MAX as f32); + } + let mut x2 = [0.0f32; 256]; + for (a, b) in x2.iter_mut().zip(x.iter()) { + *a = b * 2.0; + } + let c1 = reconstruct_carrier(&x, &x, &x); + let c2 = reconstruct_carrier(&x2, &x2, &x2); + for k in 0..512 { + assert!( + (c1.zr[k] * 2.0 - c2.zr[k]).abs() < 2e-4, + "Zr[{k}] non-linear" + ); + assert!( + (c1.zi[k] * 2.0 - c2.zi[k]).abs() < 2e-4, + "Zi[{k}] non-linear" + ); + } + } + + /// `docs/audio/ac3/ac3-errata.md` entry E3 discriminator — the + /// §E.3.5.5.1 step-3 overlap-add must carry the §7.9.4.1 step-6 + /// factor of 2. Drives a continuous multi-tone through the full + /// analysis (steps 2-5) → §E.3.5.5.4 synthesis chain at amplitude 1 + /// / angle 0 and least-squares-fits the identity gain over the + /// region interior: + /// + /// * the corrected chain ([`reconstruct_carrier`], with the factor + /// of 2) fits gain `1.0` — a unity coordinate reproduces the + /// coupling channel at unit scale, matching the Table E3.10 + /// `ecplamp = 0` = 0 dB ceiling; and + /// * a local re-implementation of the overlap-add *as printed* + /// (steps 2-5 identical, step 3 without the multiplier) fits gain + /// exactly `0.5` — the erratum's measured −6 dB defect. + #[test] + fn carrier_overlap_add_factor2_erratum_identity() { + use crate::mdct::mdct_512; + + // Three consecutive windowed-MDCT blocks (256-sample hop) of a + // continuous multi-tone with energy across the ecpl region + // (bins ~40-240 of a 512-point MDCT). + let sig = |n: usize| -> f32 { + let t = n as f32; + 0.4 * (2.0 * std::f32::consts::PI * 0.08 * t).sin() + + 0.3 * (2.0 * std::f32::consts::PI * 0.24 * t).sin() + + 0.2 * (2.0 * std::f32::consts::PI * 0.31 * t + 0.4).sin() + + 0.15 * (2.0 * std::f32::consts::PI * 0.45 * t + 1.1).sin() + }; + let mut blocks = Vec::new(); + for blk in 0..3usize { + let mut win = [0.0f32; 512]; + for n in 0..512 { + win[n] = sig(blk * 256 + n); + } + for n in 0..256 { + win[n] *= WINDOW[n]; + win[511 - n] *= WINDOW[n]; + } + let mut c = [0.0f32; 256]; + mdct_512(&win, &mut c); + // Region-restrict to the full ecpl span (step 1's XCURR + // definition: zero outside [ecplstartmant, ecplendmant)). + c[..ECPL_SUBBND_TAB[0]].fill(0.0); + blocks.push(c); + } + let (prev, curr, next) = (&blocks[0], &blocks[1], &blocks[2]); + + // Steps 2-5 with the overlap-add AS PRINTED (no factor of 2); + // everything else identical to `reconstruct_carrier`. + let windowed = |x: &[f32; 256]| -> [f32; 512] { + let mut t = [0.0f32; 512]; + imdct_512_fft(x, &mut t); + for n in 0..256 { + t[n] *= WINDOW[n]; + t[511 - n] *= WINDOW[n]; + } + t + }; + let (xprev, xcurr, xnext) = (windowed(prev), windowed(curr), windowed(next)); + let mut pcm = [0.0f32; 512]; + for n in 0..256 { + pcm[n] = xprev[256 + n] + xcurr[n]; // as printed: no `2 *` + pcm[256 + n] = xcurr[256 + n] + xnext[n]; + } + let mut re = [0.0f32; 512]; + let mut im = [0.0f32; 512]; + for n in 0..256 { + let wl = WINDOW[n]; + let wu = WINDOW[256 - n - 1]; + re[n] = pcm[n] * wl * xcos3(n); + re[256 + n] = pcm[256 + n] * wu * xcos3(256 + n); + im[n] = pcm[n] * wl * xsin3(n); + im[256 + n] = pcm[256 + n] * wu * xsin3(256 + n); + } + let (zr_p, zi_p) = dft_512_forward(&re, &im); + + // Corrected carrier (the shipping implementation). + let z = reconstruct_carrier(prev, curr, next); + + // §E.3.5.5.4 synthesis at amp = 1 / angle = 0 for both. + let begin = ECPL_SUBBND_TAB[0]; // 13 + let end = ECPL_SUBBND_TAB[N_ECPL_SUBBND]; // 253 + let amp = vec![1.0f32; end - begin]; + let ang = vec![0.0f32; end - begin]; + let mut out_corr = [0.0f32; 256]; + generate_channel_coeffs(&z.zr, &z.zi, &, &ang, begin, 512, &mut out_corr); + let mut out_printed = [0.0f32; 256]; + generate_channel_coeffs(&zr_p, &zi_p, &, &ang, begin, 512, &mut out_printed); + + // Least-squares identity-gain fit over the region interior + // (skip 12 bins at each edge: the brick-wall region restriction + // rings there). + let fit = |out: &[f32; 256]| -> f64 { + let mut num = 0.0f64; + let mut den = 0.0f64; + for bin in (begin + 12)..(end - 12) { + num += out[bin] as f64 * curr[bin] as f64; + den += curr[bin] as f64 * curr[bin] as f64; + } + assert!(den > 1e-3, "test signal has no in-region energy"); + num / den + }; + let g_corr = fit(&out_corr); + let g_printed = fit(&out_printed); + assert!( + (g_corr - 1.0).abs() < 5e-3, + "corrected overlap-add identity gain {g_corr:.4} != 1.0" + ); + assert!( + (g_printed - 0.5).abs() < 2.5e-3, + "as-printed overlap-add identity gain {g_printed:.4} != 0.5" + ); + // The ONLY processing difference is the step-3 multiplier, so + // the two fits differ by exactly that factor. + assert!( + (g_corr - 2.0 * g_printed).abs() < 1e-6, + "printed/corrected chains differ by more than the factor of 2" + ); + } + + // ---- §E.3.5.5 deferred-synthesis orchestration ---- + + /// Build a non-trivial single-block ecpl input over a small active + /// region (sub-bands 0..2 = bins 13..25) with `nfchans` coupled + /// channels, channel `0` first-coupled (amp index 0 → unit amplitude, + /// no angle/chaos), channel `1` carrying explicit angle/chaos. + fn sample_block(nfchans: usize) -> EcplBlock { + let begin = 0usize; + let end = 2usize; // sub-bands 0,1 → bins 13..25 + let necpl = necplbnd(begin, end, &[false; N_ECPL_SUBBND]); + let strategy = EcplStrategy { + ecplbegf: 0, + begin_subbnd: begin, + end_subbnd: end, + bndstrc: [false; N_ECPL_SUBBND], + necplbnd: necpl, + }; + let mut chincpl = [false; ECPL_MAX_FBW]; + let mut channels = vec![EcplChannelParams::default(); nfchans]; + for ch in 0..nfchans { + chincpl[ch] = true; + let p = &mut channels[ch]; + p.param1e = true; + p.amp = vec![0u8; necpl]; // index 0 → unit amplitude + if ch > 0 { + p.param2e = true; + p.angle = vec![4u8; necpl]; + p.chaos = vec![1u8; necpl]; + } + } + let mut mant = [0.0f32; 256]; + let mut s: u32 = 0x1357_9BDF; + for v in mant.iter_mut().take(end_bin(end)).skip(begin_bin(begin)) { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + *v = (s as i32 as f32) / (i32::MAX as f32); + } + EcplBlock { + mant, + strategy, + coords: EcplCoords { + angleintrp: false, + channels, + }, + chincpl, + } + } + + #[test] + fn synthesize_block_zero_carrier_is_zero() { + let mut st = EcplState::new(); + let blk = sample_block(2); + let zero = EcplBlock { + mant: [0.0; 256], + ..blk.clone() + }; + let mut out = vec![[0.0f32; 256]; ECPL_MAX_FBW]; + // prev = curr = next = zero mantissas → zero carrier → zero output. + synthesize_block(&mut st, &zero, &zero, &zero, &mut out, 512); + for ch in 0..2 { + for &v in out[ch].iter() { + assert!(v.abs() < 1e-6, "ch{ch} non-zero from zero carrier"); + } + } + } + + #[test] + fn synthesize_block_leaves_uncoupled_channel_region_untouched() { + let mut st = EcplState::new(); + let blk = sample_block(2); // chans 0,1 coupled; chan 2 not + let mut out = vec![[0.0f32; 256]; ECPL_MAX_FBW]; + // Pre-seed channel 2 (not coupled) with a marker in the active bins. + for bin in begin_bin(0)..end_bin(2) { + out[2][bin] = 42.0; + } + synthesize_block(&mut st, &blk, &blk, &blk, &mut out, 512); + for bin in begin_bin(0)..end_bin(2) { + assert_eq!(out[2][bin], 42.0, "uncoupled ch2 bin{bin} overwritten"); + } + // Coupled channels DID get written somewhere in the region. + let any0 = (begin_bin(0)..end_bin(2)).any(|b| out[0][b].abs() > 1e-9); + assert!(any0, "first coupled channel produced no coefficients"); + } + + #[test] + fn synthesize_block_is_linear_in_carrier() { + // The whole synthesis chain is linear in the ecpl-channel mantissas + // (carrier reconstruction + complex product), so scaling all three + // neighbour buffers by k scales every output coefficient by k. + let mut st1 = EcplState::new(); + let mut st2 = EcplState::new(); + let blk = sample_block(2); + let blk2 = EcplBlock { + mant: std::array::from_fn(|i| blk.mant[i] * 3.0), + ..blk.clone() + }; + let mut out1 = vec![[0.0f32; 256]; ECPL_MAX_FBW]; + let mut out2 = vec![[0.0f32; 256]; ECPL_MAX_FBW]; + synthesize_block(&mut st1, &blk, &blk, &blk, &mut out1, 512); + synthesize_block(&mut st2, &blk2, &blk2, &blk2, &mut out2, 512); + for ch in 0..2 { + for bin in begin_bin(0)..end_bin(2) { + assert!( + (out1[ch][bin] * 3.0 - out2[ch][bin]).abs() < 1e-3, + "ch{ch} bin{bin} not linear in carrier" + ); + } + } + } + + #[test] + fn ecpl_state_rand_notrans_generated_once() { + // §E.3.5.5.3: the non-transient random array is generated once and + // stays identical across blocks. Two synthesis calls on a + // non-transient channel must reuse the same cached array. + let mut st = EcplState::new(); + let _ = st.rand_notrans(1, 512); + let snapshot: Vec = (0..256).map(|b| st.rand_notrans(1, 512).get(b)).collect(); + // A second access yields the identical sequence. + for (b, &want) in snapshot.iter().enumerate() { + assert_eq!(st.rand_notrans(1, 512).get(b), want); + } + // Distinct channels get distinct sequences (spec: unique per ch). + let ch1_first = st.rand_notrans(1, 512).get(0); + let ch2_first = st.rand_notrans(2, 512).get(0); + assert!(ch1_first != ch2_first, "channels share a random sequence"); + } + + #[test] + fn ecpl_state_prev_frame_mant_roundtrips() { + // §E.3.5.5.1 cross-frame carry: a fresh state has no carried + // previous-frame spectrum (block 0's "previous block" defaults to + // the zero boundary case); set/get round-trips, and a `None` reset + // restores the boundary case. + let mut st = EcplState::new(); + assert!( + st.prev_frame_last_mant().is_none(), + "fresh state must carry no previous-frame spectrum" + ); + let carried: [f32; 256] = std::array::from_fn(|i| (i as f32) * 0.5); + st.set_prev_frame_last_mant(Some(carried)); + assert_eq!( + st.prev_frame_last_mant().copied(), + Some(carried), + "carried spectrum must round-trip" + ); + st.set_prev_frame_last_mant(None); + assert!( + st.prev_frame_last_mant().is_none(), + "None reset must restore the zero boundary case" + ); + } + + #[test] + fn synthesize_block_consults_prev_neighbour() { + // The §E.3.5.5.1 carrier of the current block depends on the + // *previous* block's spectrum (it suppresses time-domain aliasing). + // A non-zero `prev` (as the cross-frame carry supplies for block 0) + // must therefore change the synthesised output versus a zero `prev` + // — proving the previous-frame edge is actually threaded through. + let curr = sample_block(2); + let mut zero_prev = curr.clone(); + zero_prev.mant = [0.0; 256]; + let mut nonzero_prev = curr.clone(); + nonzero_prev.mant = std::array::from_fn(|i| sample_block(2).mant[i] * 0.75); + + let mut st_zero = EcplState::new(); + let mut st_carry = EcplState::new(); + let mut out_zero = vec![[0.0f32; 256]; ECPL_MAX_FBW]; + let mut out_carry = vec![[0.0f32; 256]; ECPL_MAX_FBW]; + // next = zero in both (the last-block boundary case) isolates the + // contribution of the previous-block spectrum. + let zero_next = zero_prev.clone(); + synthesize_block( + &mut st_zero, + &zero_prev, + &curr, + &zero_next, + &mut out_zero, + 512, + ); + synthesize_block( + &mut st_carry, + &nonzero_prev, + &curr, + &zero_next, + &mut out_carry, + 512, + ); + + let mut diff = 0.0f32; + for ch in 0..2 { + for bin in begin_bin(0)..end_bin(2) { + diff += (out_zero[ch][bin] - out_carry[ch][bin]).abs(); + } + } + assert!( + diff > 1e-3, + "non-zero previous-block spectrum did not affect synthesis (diff={diff})" + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/ecplenc.rs b/crates/vendor/oxideav-ac3/src/eac3/ecplenc.rs new file mode 100644 index 00000000..86dd9f6c --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/ecplenc.rs @@ -0,0 +1,773 @@ +//! Encoder-side enhanced coupling — ATSC A/52:2018 Annex E +//! §E.2.3.3.16-26 (syntax) / §E.3.5.5 (decode model), encode direction. +//! +//! Enhanced coupling replaces the waveform coding of every coupled +//! channel's high-frequency region with **one** shared carrier channel +//! plus per-band amplitude / angle / chaos coordinates (§E.3.5.5). The +//! encoder therefore has three jobs: +//! +//! 1. **Carrier construction** — produce the enhanced-coupling channel's +//! MDCT coefficients over the active region. The first coupled +//! channel's angle and chaos are spec-fixed to `0` (§E.3.5.5.2-3: +//! they are never transmitted for `firstchincpl`), which pins the +//! carrier's per-band *phase* to that channel: any carrier whose +//! phase deviates from the first coupled channel injects an +//! uncorrectable phase error into its reconstruction. The carrier is +//! therefore built from the first coupled channel's own MDCT +//! coefficients, scaled **per band** by [`carrier_band_gains`] so +//! that every other coupled channel's amplitude coordinate stays +//! within the Table E3.10 representable ceiling of `1.0` (the +//! loudest channel in each band maps to amp ≈ 1). Per-band scaling +//! of real MDCT coefficients is exactly linear through the +//! §E.3.5.5.1 analysis, so the carrier stays self-consistent. +//! 2. **Coordinate measurement** — for each coupled channel and band, +//! measure the amplitude ratio and phase difference against the +//! carrier **in the §E.3.5.5.1 complex analysis domain** (the same +//! `Z[k]` the decoder synthesises from), via +//! [`band_cross_stats`] over [`super::ecpl::reconstruct_carrier`] +//! outputs. Measuring against the decoder-faithful carrier (with its +//! zero next-block spectrum at the frame edge) folds every analysis +//! imperfection into the transmitted coordinates. +//! 3. **Coordinate quantisation** — [`quantise_amp`] (inverse of +//! Table E3.10, ~1.5 dB grid, code 31 = -∞ dB) and +//! [`quantise_angle`] (inverse of Table E3.11, π/32 grid with wrap). +//! +//! The bitstream emission (strategy + coordinate blocks, carrier +//! exponents / bit allocation / mantissas) lives in +//! [`super::encoder`]; this module is the pure DSP + quantiser layer, +//! unit-tested standalone. The chaos coordinate is transmitted as `0` +//! (fully correlated model) and `ecpltrans` as `0` — the §E.3.5.5.3 +//! random de-correlation then contributes nothing, keeping the decode +//! deterministic and the amplitude modification factor at unity. + +use super::ecpl::{ampbnd, band_bin_counts, begin_bin, end_bin, EcplCarrier, N_ECPL_SUBBND}; +use crate::audblk::N_COEFFS; + +/// Encoder-facing enhanced-coupling parameters (§E.2.3.3.16-17 codes). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EcplParams { + /// `ecplbegf` (4 bits) — begin-frequency code, Table E3.8. The + /// encoder restricts it to `2..=13` (begin sub-band ≥ 4, i.e. the + /// region starts at transform coefficient 37 or above) so the + /// carrier grid aligns with the shared coupling-channel exponent / + /// bit-allocation machinery. + pub ecplbegf: u8, + /// `ecplendf` (4 bits) — end-frequency code, Table E3.8: + /// `ecpl_end_subbnd = ecplendf + 7`. + pub ecplendf: u8, + /// Signal per-band **chaos** coordinates (§E.2.3.3.25 / + /// Table E3.12) derived from the measured band coherence, so the + /// decoder's §E.3.5.5.3 random de-correlation restores the + /// statistical character of channel content the shared carrier + /// cannot represent (inter-channel width). The transmitted + /// amplitude is pre-divided by the decoder's `1 + 0.38·chaosval` + /// modification so band energies stay matched. `false` transmits + /// chaos 0 everywhere (fully correlated model — the decode is then + /// a deterministic pure amplitude+angle reconstruction). + pub chaos: bool, +} + +impl Default for EcplParams { + /// Default geometry: begin sub-band 4 (tc 37 ≈ 3.5 kHz at 48 kHz) + /// through end sub-band 22 (tc 253) — the full coupling-eligible + /// region above the independently-coded low band. Coherence-driven + /// chaos coordinates are on. + fn default() -> Self { + Self { + ecplbegf: 2, + ecplendf: 15, + chaos: true, + } + } +} + +/// Resolved enhanced-coupling geometry the encoder codes against. +#[derive(Clone, Debug)] +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub struct EcplGeometry { + /// Raw begin-frequency code (re-emitted in the strategy block). + pub ecplbegf: u8, + /// Raw end-frequency code (re-emitted in the strategy block). Not + /// transmitted when the region is SPX-bounded (`spx_bounded`). + pub ecplendf: u8, + /// The region's end is derived from the SPX begin frequency + /// (§E.2.3.3.17 SPX-in-use arm) — `ecplendf` is NOT transmitted. + pub spx_bounded: bool, + /// `ecpl_begin_subbnd` (Table E3.8). + pub begin_subbnd: usize, + /// `ecpl_end_subbnd` (Table E3.8). + pub end_subbnd: usize, + /// Banding structure in force (`ecplbndstrce = 0` → the Table E2.14 + /// default), indexed by absolute sub-band. + pub bndstrc: [bool; N_ECPL_SUBBND], + /// Number of coordinate bands (§E.2.3.3.19). + pub necplbnd: usize, + /// First transform coefficient of the region (`ecplstartmant`). + pub start_bin: usize, + /// One-past-the-last transform coefficient (`ecplendmant`). + pub end_bin: usize, + /// Per-band bin counts (§E.3.5.5.1 `nbins_per_bnd_array[]`). + pub band_bins: Vec, +} + +impl EcplGeometry { + /// Derive the geometry from the raw codes + a banding structure + /// (pass [`super::ecpl::DEFAULT_ECPL_BNDSTRC`] for the + /// `ecplbndstrce = 0` default). Errors mirror the decoder-side + /// `parse_strategy` guards plus the encoder's own grid restriction. + pub fn derive( + params: &EcplParams, + bndstrc: &[bool; N_ECPL_SUBBND], + ) -> oxideav_core::Result { + if params.ecplbegf > 15 || params.ecplendf > 15 { + return Err(oxideav_core::Error::invalid( + "eac3 ecpl encoder: ecplbegf/ecplendf are 4-bit codes (0..=15)", + )); + } + let begin = super::ecpl::begin_subbnd(params.ecplbegf); + let end = super::ecpl::end_subbnd(false, params.ecplendf, 0); + if begin < 4 { + return Err(oxideav_core::Error::invalid( + "eac3 ecpl encoder: ecplbegf < 2 (begin sub-band < 4) is not \ + supported — the region must start at transform coefficient 37 \ + or above", + )); + } + if begin >= end || end > N_ECPL_SUBBND { + return Err(oxideav_core::Error::invalid( + "eac3 ecpl encoder: empty or out-of-grid sub-band range \ + (need begin < end <= 22)", + )); + } + // §E.2.3.3.19: bndstrc entries up to and including + // max(begin, 8) are always zero — mask the (default) table so + // a region beginning at sub-band 9+ cannot carry a phantom + // leading merge (necplbnd vs the band walk would disagree). + let mut bndstrc = *bndstrc; + for slot in bndstrc + .iter_mut() + .take((begin.max(8) + 1).min(N_ECPL_SUBBND)) + { + *slot = false; + } + let necplbnd = super::ecpl::necplbnd(begin, end, &bndstrc); + let band_bins = band_bin_counts(begin, end, &bndstrc); + Ok(Self { + ecplbegf: params.ecplbegf, + ecplendf: params.ecplendf, + begin_subbnd: begin, + end_subbnd: end, + bndstrc, + necplbnd, + start_bin: begin_bin(begin), + end_bin: end_bin(end), + band_bins, + spx_bounded: false, + }) + } + + /// Derive the geometry for the **SPX co-active** configuration + /// (§E.2.3.3.17 SPX-in-use arm / §3.6.1 "coupling for a mid-range + /// portion ... spectral extension for the higher-range portion"): + /// the enhanced-coupling region ends where the SPX region begins, + /// `ecplendf` is not transmitted, and `spxbegf` (the raw 3-bit SPX + /// begin code) drives the end sub-band — `spxbegf + 5` for + /// `spxbegf < 6`, else `spxbegf * 2` — so the two regions abut + /// exactly (`ecplsubbndtab[end] == spxbandtable[spx_begin]`). + pub fn derive_with_spx( + params: &EcplParams, + spxbegf: u8, + bndstrc: &[bool; N_ECPL_SUBBND], + ) -> oxideav_core::Result { + if params.ecplbegf > 15 { + return Err(oxideav_core::Error::invalid( + "eac3 ecpl encoder: ecplbegf is a 4-bit code (0..=15)", + )); + } + let begin = super::ecpl::begin_subbnd(params.ecplbegf); + let end = super::ecpl::end_subbnd(true, 0, spxbegf as usize); + if begin < 4 { + return Err(oxideav_core::Error::invalid( + "eac3 ecpl encoder: ecplbegf < 2 (begin sub-band < 4) is not \ + supported — the region must start at transform coefficient 37 \ + or above", + )); + } + if begin >= end || end > N_ECPL_SUBBND { + return Err(oxideav_core::Error::invalid( + "eac3 ecpl encoder: SPX begin frequency leaves an empty or \ + out-of-grid enhanced-coupling region (need begin < end <= 22)", + )); + } + // §E.2.3.3.19: bndstrc entries up to and including + // max(begin, 8) are always zero — mask the (default) table so + // a region beginning at sub-band 9+ cannot carry a phantom + // leading merge (necplbnd vs the band walk would disagree). + let mut bndstrc = *bndstrc; + for slot in bndstrc + .iter_mut() + .take((begin.max(8) + 1).min(N_ECPL_SUBBND)) + { + *slot = false; + } + let necplbnd = super::ecpl::necplbnd(begin, end, &bndstrc); + let band_bins = band_bin_counts(begin, end, &bndstrc); + Ok(Self { + ecplbegf: params.ecplbegf, + ecplendf: 0, + begin_subbnd: begin, + end_subbnd: end, + bndstrc, + necplbnd, + start_bin: begin_bin(begin), + end_bin: end_bin(end), + band_bins, + spx_bounded: true, + }) + } +} + +/// Quantise a linear amplitude to the nearest Table E3.10 `ecplamp` +/// code (log-domain nearest neighbour over the ~1.5 dB grid). +/// +/// Values at or above `1.0` saturate to code 0 (0 dB — the table's +/// ceiling); values more than half a grid step below the code-30 floor +/// (≈ -45 dB) map to code 31 (-∞ dB, amplitude 0), as do non-positive +/// inputs. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn quantise_amp(a: f32) -> u8 { + if a.is_nan() || a <= 0.0 { + return 31; + } + if a >= 1.0 { + return 0; + } + let target = a.ln(); + let mut best = 0u8; + let mut best_err = f32::INFINITY; + for code in 0..=30u8 { + let v = ampbnd(code); + let err = (v.ln() - target).abs(); + if err < best_err { + best_err = err; + best = code; + } + } + // Below the representable floor: if the input is further below + // code 30 than half the local grid step (~1.4 dB), -∞ is closer. + let floor = ampbnd(30); + if a < floor && (floor / a) > 1.09 { + // 1.09 ≈ 10^(0.75/20) — half of the ~1.5 dB step. + return 31; + } + best +} + +/// Quantise an angle in the spec's normalised units (`-1.0 ..= 1.0` +/// representing `-π ..= π`) to the nearest Table E3.11 `ecplangle` +/// code (π/32 grid). The value wraps modulo 2.0 first, so any real +/// phase difference maps onto the table's principal interval. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn quantise_angle(units: f32) -> u8 { + if !units.is_finite() { + return 0; + } + // Round to the nearest 1/32 step, then wrap into [-32, 31]. + let mut m = (units * 32.0).round() as i64; + m = m.rem_euclid(64); + if m >= 32 { + m -= 64; + } + if m >= 0 { + m as u8 // codes 0..=31: 0.0 .. 0.96875 + } else { + (m + 64) as u8 // codes 32..=63: -1.0 .. -0.03125 + } +} + +/// Per-band cross statistics between a coupled channel's complex +/// analysis spectrum `X[k]` and the carrier's `Z[k]` (both §E.3.5.5.1 +/// outputs), accumulated over one or more blocks. +#[derive(Clone, Copy, Debug, Default)] +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub struct BandStats { + /// `Σ |X[k]|²` — channel energy in the band. + pub ex: f64, + /// `Σ |Z[k]|²` — carrier energy in the band. + pub ez: f64, + /// `Σ Re(X · conj(Z))`. + pub cr: f64, + /// `Σ Im(X · conj(Z))`. + pub ci: f64, +} + +impl BandStats { + /// Energy-matching amplitude coordinate: `sqrt(ex / ez)` + /// (§E.3.5.5.2's amplitude semantics — the reconstruction + /// `amp · Z` carries the channel's band energy). Zero-carrier + /// bands yield 0 (nothing to scale). + pub fn amp(&self) -> f32 { + if self.ez <= 0.0 || self.ex <= 0.0 { + return 0.0; + } + (self.ex / self.ez).sqrt() as f32 + } + + /// Band phase difference in Table E3.11 units (`-1.0 ..= 1.0` = + /// `-π ..= π`): the argument of the band-summed cross spectrum. + /// A zero cross spectrum (uncorrelated or silent) yields 0. + pub fn angle_units(&self) -> f32 { + if self.cr == 0.0 && self.ci == 0.0 { + return 0.0; + } + (self.ci.atan2(self.cr) / std::f64::consts::PI) as f32 + } + + /// Band **coherence** `|Σ X·conj(Z)| / sqrt(ΣE_X · ΣE_Z)` in + /// `0.0 ..= 1.0`: how much of the channel's band content is a + /// (rotated, scaled) copy of the carrier. `1.0` = fully coherent + /// (a pure amplitude+angle relation reconstructs it exactly); + /// low values mean the channel carries content the carrier does + /// not — the §E.3.5.5.3 chaos de-correlation restores its + /// statistical character. Silent bands report full coherence + /// (nothing to de-correlate). + pub fn coherence(&self) -> f32 { + if self.ex <= 0.0 || self.ez <= 0.0 { + return 1.0; + } + let num = (self.cr * self.cr + self.ci * self.ci).sqrt(); + (num / (self.ex * self.ez).sqrt()).clamp(0.0, 1.0) as f32 + } +} + +/// Map a band coherence to a Table E3.12 `ecplchaos` code (0..=7, +/// value `-code/7`): the incoherent fraction `1 - γ` scaled onto the +/// 8-step grid. Fully coherent → code 0 (no de-correlation); fully +/// incoherent → code 7 (angle jitter up to ±π, uniform phase). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn chaos_code_for(coherence: f32) -> u8 { + let incoherent = (1.0 - coherence).clamp(0.0, 1.0); + (incoherent * 7.0).round() as u8 +} + +/// The §E.3.5.5.2 amplitude-modification factor the decoder applies to +/// a non-transient, non-first coupled channel: `1 + 0.38 · chaosval` +/// with `chaosval = -code/7` (Table E3.12) — always in `0.62 ..= 1.0`. +/// The encoder divides its measured amplitude by this factor before +/// quantisation so the decoded band energy stays matched. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn chaos_amp_factor(chaos_code: u8) -> f32 { + let chaosval = -(chaos_code.min(7) as f32) / 7.0; + 1.0 + 0.38 * chaosval +} + +/// Accumulate per-band cross statistics for one block: `X[k]` is the +/// coupled channel's analysis spectrum, `Z[k]` the carrier's, both from +/// [`super::ecpl::reconstruct_carrier`]. Only the first-half bins +/// (`k < 256`, the MDCT-aligned half the §E.3.5.5.4 synthesis reads) +/// inside the active region contribute. `stats` has one slot per band +/// (`geom.necplbnd`), accumulated in place so multi-block coordinate +/// spans sum their statistics before quantisation. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn band_cross_stats( + x: &EcplCarrier, + z: &EcplCarrier, + geom: &EcplGeometry, + stats: &mut [BandStats], +) { + let mut bin = geom.start_bin; + for (bnd, &nbins) in geom.band_bins.iter().enumerate() { + if bnd >= stats.len() { + break; + } + let s = &mut stats[bnd]; + for k in bin..(bin + nbins).min(256) { + let xr = x.zr[k] as f64; + let xi = x.zi[k] as f64; + let zr = z.zr[k] as f64; + let zi = z.zi[k] as f64; + s.ex += xr * xr + xi * xi; + s.ez += zr * zr + zi * zi; + // X · conj(Z) + s.cr += xr * zr + xi * zi; + s.ci += xi * zr - xr * zi; + } + bin += nbins; + } +} + +/// Per-band MDCT-domain energies of one channel over a span of blocks +/// (the coordinate-refresh span), used to size the carrier gains. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn band_mdct_energies(blocks: &[&[f32; N_COEFFS]], geom: &EcplGeometry) -> Vec { + let mut out = vec![0.0f64; geom.necplbnd]; + for mdct in blocks { + let mut bin = geom.start_bin; + for (bnd, &nbins) in geom.band_bins.iter().enumerate() { + for k in bin..(bin + nbins).min(N_COEFFS) { + out[bnd] += (mdct[k] as f64) * (mdct[k] as f64); + } + bin += nbins; + } + } + out +} + +/// Per-band carrier gains: the carrier is the first coupled channel +/// scaled up (never down) so the loudest coupled channel in each band +/// lands at an amplitude coordinate ≈ 1.0 — the Table E3.10 ceiling. +/// +/// `energies[ch][bnd]` are the [`band_mdct_energies`] of every coupled +/// channel over the span, with `energies[0]` the first coupled channel +/// (the carrier source). Gains are clamped to `[1, 32]` (+30 dB): a +/// band where the carrier source is silent but another channel is loud +/// cannot be represented losslessly by a phase-locked carrier anyway, +/// and an unbounded gain would blow up the carrier's exponent envelope. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn carrier_band_gains(energies: &[Vec], nbands: usize) -> Vec { + let mut gains = vec![1.0f32; nbands]; + let Some(base) = energies.first() else { + return gains; + }; + for bnd in 0..nbands { + let e0 = base.get(bnd).copied().unwrap_or(0.0); + let mut emax = 0.0f64; + for ch in energies { + emax = emax.max(ch.get(bnd).copied().unwrap_or(0.0)); + } + if e0 > 0.0 && emax > e0 { + gains[bnd] = ((emax / e0).sqrt() as f32).clamp(1.0, 32.0); + } + } + gains +} + +/// Build one block's carrier MDCT buffer: the first coupled channel's +/// coefficients restricted to the active region, scaled per band by +/// `gains`. Bins outside `[start_bin, end_bin)` are zero (the +/// §E.3.5.5.1 step-1 `XCURR` definition). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn build_carrier_block( + src_mdct: &[f32; N_COEFFS], + gains: &[f32], + geom: &EcplGeometry, +) -> [f32; 256] { + let mut out = [0.0f32; 256]; + let mut bin = geom.start_bin; + for (bnd, &nbins) in geom.band_bins.iter().enumerate() { + let g = gains.get(bnd).copied().unwrap_or(1.0); + for k in bin..(bin + nbins).min(256) { + out[k] = src_mdct[k] * g; + } + bin += nbins; + } + out +} + +/// Restrict one block's MDCT coefficients to the active region (zero +/// outside) — the per-channel analysis input mirroring the carrier's +/// step-1 buffer definition. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn region_restrict(mdct: &[f32; N_COEFFS], geom: &EcplGeometry) -> [f32; 256] { + let mut out = [0.0f32; 256]; + let end = geom.end_bin.min(256); + out[geom.start_bin..end].copy_from_slice(&mdct[geom.start_bin..end]); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::eac3::ecpl::{ + generate_channel_coeffs, reconstruct_carrier, DEFAULT_ECPL_BNDSTRC, ECPL_ANGLE_TAB, + }; + use crate::mdct::mdct_512; + use crate::tables::WINDOW; + + fn geom_default() -> EcplGeometry { + EcplGeometry::derive(&EcplParams::default(), &DEFAULT_ECPL_BNDSTRC).unwrap() + } + + #[test] + fn geometry_default_full_region() { + let g = geom_default(); + assert_eq!(g.begin_subbnd, 4); + assert_eq!(g.end_subbnd, 22); + assert_eq!(g.start_bin, 37); + assert_eq!(g.end_bin, 253); + // Default banding: 18 sub-bands in span, 9 merges (Table E2.14 + // rows 9, 11, 13, 15, 16, 17, 19, 20, 21) → 9 bands. + assert_eq!(g.necplbnd, 9); + assert_eq!(g.band_bins.iter().sum::(), 253 - 37); + } + + #[test] + fn geometry_rejects_low_begin_and_inverted_range() { + // ecplbegf 0/1 → begin sub-band 0/2 < 4 → rejected. + for begf in [0u8, 1] { + let p = EcplParams { + ecplbegf: begf, + ecplendf: 15, + ..EcplParams::default() + }; + assert!(EcplGeometry::derive(&p, &DEFAULT_ECPL_BNDSTRC).is_err()); + } + // begin >= end. + let p = EcplParams { + ecplbegf: 13, + ecplendf: 0, + ..EcplParams::default() + }; + // begin_subbnd(13) = 16, end = 7 → inverted. + assert!(EcplGeometry::derive(&p, &DEFAULT_ECPL_BNDSTRC).is_err()); + } + + #[test] + fn quantise_amp_roundtrips_every_code() { + for code in 0..=31u8 { + let v = ampbnd(code); + assert_eq!( + quantise_amp(v), + code, + "code {code} (amp {v}) did not round-trip" + ); + } + } + + #[test] + fn quantise_amp_edges() { + assert_eq!(quantise_amp(0.0), 31); + assert_eq!(quantise_amp(-1.0), 31); + assert_eq!(quantise_amp(f32::NAN), 31); + assert_eq!(quantise_amp(2.0), 0); + // Just below the code-30 floor stays 30; far below → 31. + assert_eq!(quantise_amp(ampbnd(30) * 0.95), 30); + assert_eq!(quantise_amp(ampbnd(30) * 0.25), 31); + // Midpoints land on a neighbour, never off-grid. + let mid = (ampbnd(3) * ampbnd(4)).sqrt(); + let q = quantise_amp(mid); + assert!(q == 3 || q == 4); + } + + #[test] + fn quantise_angle_roundtrips_every_code() { + for code in 0..64u8 { + let v = ECPL_ANGLE_TAB[code as usize]; + assert_eq!( + quantise_angle(v), + code, + "code {code} (angle {v}) did not round-trip" + ); + } + } + + #[test] + fn chaos_code_and_amp_factor() { + // Full coherence → no chaos; full incoherence → code 7. + assert_eq!(chaos_code_for(1.0), 0); + assert_eq!(chaos_code_for(0.0), 7); + // Grid midpoints round to the nearest step. + assert_eq!(chaos_code_for(0.5), 4); // (1-0.5)*7 = 3.5 → 4 + assert_eq!(chaos_code_for(1.5), 0); // clamped + // Amplitude-modification factor: 1 + 0.38·(-code/7). + assert!((chaos_amp_factor(0) - 1.0).abs() < 1e-6); + assert!((chaos_amp_factor(7) - 0.62).abs() < 1e-6); + assert!((chaos_amp_factor(3) - (1.0 - 0.38 * 3.0 / 7.0)).abs() < 1e-6); + // Round-trip with the decoder's chaos modification: pre-divided + // amp × decoder factor ≈ measured amp. + for code in 0..=7u8 { + let measured = 0.5f32; + let sent = measured / chaos_amp_factor(code); + let decoded = sent * chaos_amp_factor(code); + assert!((decoded - measured).abs() < 1e-6); + } + } + + #[test] + fn coherence_measures_correlation() { + // Identical spectra → coherence 1; orthogonal (Re vs Im) → the + // cross terms cancel per-bin only if constructed so; use the + // direct accumulator forms. + let full = BandStats { + ex: 4.0, + ez: 1.0, + cr: 2.0, + ci: 0.0, + }; + assert!((full.coherence() - 1.0).abs() < 1e-6); + let none = BandStats { + ex: 4.0, + ez: 1.0, + cr: 0.0, + ci: 0.0, + }; + assert!(none.coherence() < 1e-6); + let silent = BandStats::default(); + assert!((silent.coherence() - 1.0).abs() < 1e-6); + } + + #[test] + fn quantise_angle_wraps() { + // +1.0 (= +π) wraps to the -1.0 slot (code 32) — same angle. + assert_eq!(quantise_angle(1.0), 32); + // 2.0 ≡ 0.0. + assert_eq!(quantise_angle(2.0), 0); + // -1.03125 ≡ 0.96875 (code 31). + assert_eq!(quantise_angle(-1.0 - 1.0 / 32.0), 31); + assert_eq!(quantise_angle(f32::NAN), 0); + } + + /// Windowed-MDCT a 3-block excerpt of a time signal (256-sample + /// hop), mirroring the encoder DSP. + fn mdct_blocks(signal: &dyn Fn(usize) -> f32, nblocks: usize) -> Vec<[f32; N_COEFFS]> { + let mut out = Vec::with_capacity(nblocks); + for blk in 0..nblocks { + let mut buf = [0.0f32; 512]; + for (n, slot) in buf.iter_mut().enumerate() { + *slot = signal(blk * 256 + n); + } + let mut win = [0.0f32; 512]; + for n in 0..256 { + win[n] = buf[n] * WINDOW[n]; + win[511 - n] = buf[511 - n] * WINDOW[n]; + } + let mut c = [0.0f32; N_COEFFS]; + mdct_512(&win, &mut c); + out.push(c); + } + out + } + + /// The property the whole encoder model rests on: running a + /// region-restricted MDCT block through the §E.3.5.5.1 analysis + /// (with its true neighbours) and back through the §E.3.5.5.4 + /// synthesis with amplitude 1 / angle 0 reproduces the original + /// MDCT coefficients in the region interior. This is the spec's + /// "non-aliased carrier" design intent; the test pins our + /// primitives to it (and pins the identity's scale factor at 1). + #[test] + fn analysis_synthesis_identity_amp1_angle0() { + let geom = geom_default(); + // Multi-tone with energy well inside the region (bins ~60-200 of + // a 48 kHz 512-point MDCT: 5.6-18.8 kHz). + let sig = |n: usize| -> f32 { + let t = n as f32; + 0.4 * (2.0 * std::f32::consts::PI * 0.24 * t).sin() + + 0.3 * (2.0 * std::f32::consts::PI * 0.31 * t).sin() + + 0.2 * (2.0 * std::f32::consts::PI * 0.47 * t + 0.7).sin() + }; + let blocks = mdct_blocks(&sig, 3); + let prev = region_restrict(&blocks[0], &geom); + let curr = region_restrict(&blocks[1], &geom); + let next = region_restrict(&blocks[2], &geom); + + let z = reconstruct_carrier(&prev, &curr, &next); + let amp = vec![1.0f32; geom.end_bin - geom.start_bin]; + let ang = vec![0.0f32; geom.end_bin - geom.start_bin]; + let mut out = [0.0f32; 256]; + generate_channel_coeffs(&z.zr, &z.zi, &, &ang, geom.start_bin, 512, &mut out); + + // Compare in the region interior (skip 12 bins at each edge — + // the region restriction itself is a brick-wall filter whose + // ringing lands at the edges). + let lo = geom.start_bin + 12; + let hi = geom.end_bin - 12; + let mut err = 0.0f64; + let mut ref_e = 0.0f64; + for bin in lo..hi { + let d = (out[bin] - curr[bin]) as f64; + err += d * d; + ref_e += (curr[bin] as f64) * (curr[bin] as f64); + } + assert!(ref_e > 1e-6, "test signal has no in-region energy"); + let snr_db = 10.0 * (ref_e / err.max(1e-30)).log10(); + assert!( + snr_db > 40.0, + "analysis→synthesis identity too lossy: {snr_db:.1} dB" + ); + } + + #[test] + fn band_cross_stats_measures_scale_and_phase() { + let geom = geom_default(); + let sig = |n: usize| -> f32 { + let t = n as f32; + 0.5 * (2.0 * std::f32::consts::PI * 0.30 * t).sin() + + 0.3 * (2.0 * std::f32::consts::PI * 0.55 * t).sin() + }; + let blocks = mdct_blocks(&sig, 3); + let prev = region_restrict(&blocks[0], &geom); + let curr = region_restrict(&blocks[1], &geom); + let next = region_restrict(&blocks[2], &geom); + // Channel = 0.5 × carrier → amp 0.5, angle 0 in every band with + // energy. + let half = |b: &[f32; 256]| -> [f32; 256] { + let mut o = *b; + for v in o.iter_mut() { + *v *= 0.5; + } + o + }; + let z = reconstruct_carrier(&prev, &curr, &next); + let x = reconstruct_carrier(&half(&prev), &half(&curr), &half(&next)); + let mut stats = vec![BandStats::default(); geom.necplbnd]; + band_cross_stats(&x, &z, &geom, &mut stats); + for (bnd, s) in stats.iter().enumerate() { + if s.ez < 1e-4 { + continue; // silent band + } + let amp = s.amp(); + assert!((amp - 0.5).abs() < 0.01, "band {bnd}: amp {amp} != 0.5"); + let ang = s.angle_units(); + assert!(ang.abs() < 0.01, "band {bnd}: angle {ang} != 0"); + } + } + + #[test] + fn carrier_band_gains_cover_loudest_channel() { + // ch0 quiet in band 1, ch1 4× energy there → gain 2 in band 1. + let e0 = vec![1.0f64, 1.0, 1.0]; + let e1 = vec![0.25f64, 4.0, 1.0]; + let gains = carrier_band_gains(&[e0, e1], 3); + assert!((gains[0] - 1.0).abs() < 1e-6); // never scales down + assert!((gains[1] - 2.0).abs() < 1e-6); + assert!((gains[2] - 1.0).abs() < 1e-6); + // Silent carrier source → gain stays 1 (nothing to scale). + let gains = carrier_band_gains(&[vec![0.0], vec![9.0]], 1); + assert!((gains[0] - 1.0).abs() < 1e-6); + // Clamp at 32. + let gains = carrier_band_gains(&[vec![1e-9], vec![1.0]], 1); + assert!((gains[0] - 32.0).abs() < 1e-3); + } + + #[test] + fn build_carrier_scales_per_band_and_zeroes_outside() { + let geom = geom_default(); + let mut mdct = [0.0f32; N_COEFFS]; + for (k, v) in mdct.iter_mut().enumerate() { + *v = (k as f32) / 256.0; + } + let mut gains = vec![1.0f32; geom.necplbnd]; + gains[0] = 3.0; + let carrier = build_carrier_block(&mdct, &gains, &geom); + // Outside the region: zero. + assert_eq!(carrier[geom.start_bin - 1], 0.0); + assert_eq!(carrier[geom.end_bin.min(255)], 0.0); + // First band scaled by 3. + assert!((carrier[geom.start_bin] - mdct[geom.start_bin] * 3.0).abs() < 1e-6); + // A later band keeps gain 1. + let bin2 = geom.start_bin + geom.band_bins[0]; + assert!((carrier[bin2] - mdct[bin2]).abs() < 1e-6); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/encoder.rs b/crates/vendor/oxideav-ac3/src/eac3/encoder.rs new file mode 100644 index 00000000..3be8dad2 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/encoder.rs @@ -0,0 +1,5603 @@ +//! Enhanced AC-3 (E-AC-3 / Dolby Digital Plus) encoder per ATSC A/52 +//! Annex E. Scope: independent substream (`strmtyp=0`, `substreamid=0`) +//! for 1.0/2.0/5.1 layouts, plus a paired indep+dep substream emission +//! for 7.1 input (5.1 downmix in indep, Lb/Rb in dep with `chanmap` +//! bit 6 = Lrs/Rrs pair set). `bsid=16`, 6 audio blocks per syncframe +//! (`numblkscod=3`), no coupling, no Adaptive Hybrid Transform. +//! Spectral extension (§E.2.3.3 / §E.3.6) is available opt-in through +//! [`make_encoder_with_spx`] — every fbw channel of every substream is +//! then coded only up to the SPX begin frequency and the decoder +//! synthesizes the extension region from the [`super::spxenc`] +//! energy-matching coordinates. +//! +//! Differences from AC-3 (`super::encoder`) at this scope: +//! +//! * **Sync frame size** is signalled in **bytes** via the 11-bit +//! `frmsiz` field (§E.2.3.1.3) — `frmsiz = (frame_size_in_words - 1)`. +//! AC-3's `frmsizecod` 6-bit table (§5.4.1.4) is gone. The encoder is +//! free to pick any size between 64 and 4096 bytes (32–2048 words); +//! we pick a per-bitrate value that matches the AC-3 lookup so PSNR +//! comparisons against AC-3 are like-for-like. +//! * **No `crc1`**. The 4-byte syncinfo is just `syncword(16) + +//! strmtyp(2) + substreamid(3) + frmsiz(11)`. The errorcheck() field +//! carries only `encinfo(1) + crc2(16)` (§E.2.2.6). +//! * **`audfrm()`** sits between `bsi()` and the audio blocks and +//! carries frame-level strategy flags (`expstre`, `ahte`, +//! `snroffststr`, `transproce`, `blkswe`, `dithflage`, `bamode`, +//! `frmfgaincode`, `dbaflde`, `skipflde`, `spxattene`). This encoder +//! sets them so per-block strategy data still travels with each +//! block — i.e. `expstre=1`, `blkswe=1`, `dithflage=1`, `bamode=1`, +//! `frmfgaincode=1`, `dbaflde=1`, `skipflde=1`. AHT/SPX/transient +//! pre-noise-processing all set to 0 (out of round-1 scope). +//! * **`audblk()`** for E-AC-3 inserts a few new fields that +//! we leave at their disabled defaults (no SPX, no enhanced +//! coupling). +//! +//! The DSP pipeline (windowing, MDCT, exponent extraction, parametric +//! bit allocation, mantissa quantisation) is identical to AC-3 and we +//! reuse the helpers from `super::encoder` directly. + +use oxideav_core::bits::BitWriter; +use oxideav_core::Encoder; +use oxideav_core::{ + CodecId, CodecParameters, Error, Frame, Packet, Result, SampleFormat, TimeBase, +}; + +use crate::audblk::{remat_band_count_spx, BLOCKS_PER_FRAME, N_COEFFS, SAMPLES_PER_BLOCK}; +use crate::decoder::SAMPLES_PER_FRAME; +use crate::encoder::{ + ac3_crc_update, build_dba_plan, compute_bap, compute_bap_cpl, decode_input_samples, + extract_exponent, mantissa_bits_total, overhead_bits_for, pick_strategy_for_block, + preprocess_d15, quantise_exponents_to_grpsize, quantise_mantissa, + select_exp_strategies_per_end, tune_snroffst_with_plan_ends, write_exponents_cpl, + write_exponents_grouped, write_mantissa_stream, BitAllocParams, CouplingPlan, DbaPlan, + TransientDetector, LFE_END_MANT, +}; +use crate::mdct::{mdct_256_pair, mdct_512}; +use crate::tables::WINDOW; + +use super::ahtenc::{compute_hebap, dct_ii_6, plan_aht_channel, write_aht_channel}; +use super::dsp::DEFAULT_SPX_BNDSTRC; +use super::ecpl::{reconstruct_carrier, DEFAULT_ECPL_BNDSTRC}; +use super::ecplenc::{ + band_cross_stats, band_mdct_energies, build_carrier_block, carrier_band_gains, + chaos_amp_factor, chaos_code_for, quantise_amp, quantise_angle, region_restrict, BandStats, + EcplGeometry, EcplParams, +}; +use super::spxenc::{ + band_coord_targets_span_atten, choose_mstrspxco, quantise_coord, SpxGeometry, SpxParams, +}; + +/// Codec id string used by the E-AC-3 encoder (registered separately from +/// the AC-3 decoder/encoder in [`crate::register`]). +pub const CODEC_ID_STR: &str = "eac3"; + +// `EAC3_BSID` is the canonical bsid value for E-AC-3 streams (= 16). +// It lives in [`super::bsi`] now and is re-exported through +// `eac3::EAC3_BSID` in `mod.rs`. Re-import locally for the bit +// writer call-site below. +use super::bsi::EAC3_BSID; + +/// Build an E-AC-3 encoder. +/// +/// Required parameters: +/// * `sample_rate` — 48 000 / 44 100 / 32 000 Hz +/// * `channels` — 1 (mono), 2 (stereo), 6 (5.1 = L,C,R,Ls,Rs,LFE), +/// or 8 (7.1 = L,C,R,Ls,Rs,LFE,Lb,Rb — emits an indep+dep substream +/// pair per Annex E §E.3.8.2: indep carries 5.1 with Ls/Rs taken +/// directly from the source, dep carries Lb/Rb with `chanmape=1` +/// and `chanmap` bit 6 (Lrs/Rrs pair) set). +/// +/// Optional `bit_rate` (selects the syncframe size). Defaults: +/// * mono → 96 kbps +/// * stereo → 192 kbps +/// * 5.1 → 384 kbps +/// * 7.1 → 384 kbps indep + 192 kbps dep = 576 kbps total +/// +/// Spectral extension (§E.2.3.3 / §E.3.6) can be enabled through +/// `params.options` so the registry path reaches it too (the typed +/// alternative is [`make_encoder_with_spx`]): +/// +/// | key | value | meaning | +/// | --- | --- | --- | +/// | `spx` | `1`/`true` | enable SPX with [`SpxParams::default`] | +/// | `spx_begf` | `0..=7` | §E.2.3.3.5 begin frequency code | +/// | `spx_endf` | `0..=7` | §E.2.3.3.6 end frequency code | +/// | `spx_strtf` | `0..=3` | §E.2.3.3.4 copy start code | +/// | `spx_blnd` | `0..=31` | §E.2.3.3.10 noise blend offset | +/// | `spx_atten` | `0..=31` | §3.6.4.2.3 attenuation code (Table E3.14) | +/// | `spx_adaptive_copy_start` | `1`/`true` | per-frame `spxstrtf` re-selection | +/// | `spx_explicit_band_structure` | `1`/`true` | emit `spxbndstrce = 1` | +/// +/// The sub-keys imply `spx` unless it is explicitly `0`/`false`. +pub fn make_encoder(params: &CodecParameters) -> Result> { + let mut concrete = build_concrete_encoder(params)?; + concrete.meta = eac3_metadata_from_options(params)?; + if let Some(spx) = spx_params_from_options(params)? { + // Same construction-time validation as make_encoder_with_spx. + SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC)?; + validate_spx_channel_mask(&spx, params.channels.unwrap_or(0))?; + concrete.spx = Some(spx); + } + match params.options.get("aht") { + None | Some("0") | Some("false") => {} + Some("1") | Some("true") => concrete.aht = true, + Some(v) => { + return Err(Error::invalid(format!( + "eac3 encoder: option aht={v} (expected 1/true/0/false)" + ))) + } + } + if let Some(ecpl) = ecpl_params_from_options(params)? { + validate_ecpl(&ecpl, params.channels.unwrap_or(0))?; + concrete.ecpl = Some(ecpl); + } + if concrete.aht && concrete.spx.is_some() { + return Err(Error::Unsupported( + "eac3 encoder: aht and spx cannot be combined (single-subsystem scope)".into(), + )); + } + if concrete.ecpl.is_some() && concrete.aht { + return Err(Error::Unsupported( + "eac3 encoder: ecpl cannot be combined with aht (single-subsystem scope)".into(), + )); + } + if let (Some(ecpl), Some(spx)) = (&concrete.ecpl, &concrete.spx) { + validate_spx_ecpl(spx, ecpl)?; + } + Ok(Box::new(concrete)) +} + +/// SPX + enhanced coupling co-active (§3.6.1: "coupling for a mid-range +/// portion of the frequency spectrum and spectral extension for the +/// higher-range portion"): the enhanced-coupling region ends at the SPX +/// begin frequency (`ecplendf` is not transmitted). Every fbw channel +/// must participate in BOTH tools — a coupled channel outside SPX would +/// have no content above the coupling region at all (its coded +/// mantissas stop at the coupling begin), so a mixed `channel_mask` is +/// rejected in this configuration. +fn validate_spx_ecpl(spx: &SpxParams, ecpl: &EcplParams) -> Result<()> { + if spx.channel_mask.is_some() { + return Err(Error::Unsupported( + "eac3 encoder: spx channel_mask cannot be combined with ecpl — \ + every coupled channel must also be in SPX" + .into(), + )); + } + // The combined geometry must be valid (non-empty coupling region + // below the SPX begin). + EcplGeometry::derive_with_spx(ecpl, spx.spxbegf, &DEFAULT_ECPL_BNDSTRC)?; + Ok(()) +} + +/// Build an E-AC-3 encoder with **spectral extension AND enhanced +/// coupling co-active** (§3.6.1): channels are waveform-coded below the +/// enhanced-coupling begin frequency, carried by the shared coupling +/// channel + coordinates from there to the SPX begin frequency, and +/// SPX-synthesized above it. See [`make_encoder_with_spx`] and +/// [`make_encoder_with_ecpl`] for the individual tools. +pub fn make_encoder_with_spx_ecpl( + params: &CodecParameters, + spx: SpxParams, + ecpl: EcplParams, +) -> Result> { + SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC)?; + validate_spx_channel_mask(&spx, params.channels.unwrap_or(0))?; + validate_ecpl(&ecpl, params.channels.unwrap_or(0))?; + validate_spx_ecpl(&spx, &ecpl)?; + let mut concrete = build_concrete_encoder(params)?; + concrete.spx = Some(spx); + concrete.ecpl = Some(ecpl); + Ok(Box::new(concrete)) +} + +/// Build an E-AC-3 encoder with **enhanced coupling** enabled +/// (§E.2.3.3.16-26 syntax / §E.3.5.5 decode model). Every fbw channel +/// of the independent substream is coupled: below the enhanced-coupling +/// begin frequency each channel is waveform-coded as usual; above it a +/// single shared **carrier** channel is coded through the standard +/// exponent / bit-allocation / mantissa path and each coupled channel +/// is reconstructed from it via per-band amplitude + angle coordinates +/// (chaos = 0, no random de-correlation — deterministic decode). The +/// carrier is phase-locked to the first coupled channel (whose angle is +/// spec-fixed to 0) and scaled per band so the loudest channel maps to +/// the Table E3.10 amplitude ceiling. Coordinates refresh on the +/// exponent anchor blocks (0 and 3) and are thrifted (`ecplparam1e = 0`) +/// when the block-3 refresh quantises identically. Long transforms are +/// forced while enhanced coupling is on. +/// +/// Requires at least 2 fbw channels (stereo / 5.1 / 7.1; the 7.1 pair's +/// dependent substream stays plain). Also reachable through the +/// registry path via `CodecParameters::options` keys `ecpl` = +/// `1`/`true`, `ecpl_begf` (2..=13) and `ecpl_endf` (0..=15). +pub fn make_encoder_with_ecpl( + params: &CodecParameters, + ecpl: EcplParams, +) -> Result> { + // Same construction-time validation as the options path. + EcplGeometry::derive(&ecpl, &DEFAULT_ECPL_BNDSTRC)?; + validate_ecpl(&ecpl, params.channels.unwrap_or(0))?; + let mut concrete = build_concrete_encoder(params)?; + concrete.ecpl = Some(ecpl); + Ok(Box::new(concrete)) +} + +/// Parse the `ecpl*` codec options (see [`make_encoder`]) into an +/// [`EcplParams`], or `None` when enhanced coupling is not requested. +/// Sub-keys imply `ecpl` unless it is explicitly `0`/`false`; the +/// resulting geometry is validated via [`EcplGeometry::derive`]. +fn ecpl_params_from_options(params: &CodecParameters) -> Result> { + let opts = ¶ms.options; + let enabled = match opts.get("ecpl") { + None => None, + Some("1") | Some("true") => Some(true), + Some("0") | Some("false") => Some(false), + Some(v) => { + return Err(Error::invalid(format!( + "eac3 encoder: option ecpl={v} (expected 1/true/0/false)" + ))) + } + }; + let parse_u8 = |key: &str| -> Result> { + match opts.get(key) { + None => Ok(None), + Some(v) => match v.parse::() { + Ok(n) if n <= 15 => Ok(Some(n)), + _ => Err(Error::invalid(format!( + "eac3 encoder: option {key}={v} (expected 0..=15)" + ))), + }, + } + }; + let begf = parse_u8("ecpl_begf")?; + let endf = parse_u8("ecpl_endf")?; + let chaos = match opts.get("ecpl_chaos") { + None => None, + Some("1") | Some("true") => Some(true), + Some("0") | Some("false") => Some(false), + Some(v) => { + return Err(Error::invalid(format!( + "eac3 encoder: option ecpl_chaos={v} (expected 1/true/0/false)" + ))) + } + }; + let on = match enabled { + Some(v) => v, + None => begf.is_some() || endf.is_some() || chaos.is_some(), + }; + if !on { + return Ok(None); + } + let d = EcplParams::default(); + let p = EcplParams { + ecplbegf: begf.unwrap_or(d.ecplbegf), + ecplendf: endf.unwrap_or(d.ecplendf), + chaos: chaos.unwrap_or(d.chaos), + }; + EcplGeometry::derive(&p, &DEFAULT_ECPL_BNDSTRC)?; + Ok(Some(p)) +} + +/// Table E1.2 mixing-metadata block (`mixmdate = 1`) for encoder-side +/// emission on the independent substream. The acmod-gated arms +/// (`dmixmod` needs > 2 channels, the centre / surround mix levels +/// need those channels present, `lfemixlevcod` needs `lfeon`) are only +/// emitted when their guard holds; configured values are ignored +/// otherwise. The advisory `mixdef` body, pan information and +/// per-block mixing configuration are emitted as absent +/// (`mixdef = 0`, `paninfoe = 0`, `frmmixcfginfoe = 0`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Eac3MixMetadata { + /// `dmixmod` (2 bits, §E.2.3.1.11) — preferred stereo downmix + /// (0 = not indicated, 1 = LtRt, 2 = LoRo; 3 is reserved). + pub dmixmod: u8, + /// `ltrtcmixlev` (3 bits, Table D2.3 scale: 0 = +3 dB … 6 = −6 dB, + /// 7 = mute). + pub ltrtcmixlev: u8, + /// `lorocmixlev` (3 bits, Table D2.5 — same scale). + pub lorocmixlev: u8, + /// `ltrtsurmixlev` (3 bits, Table D2.4: 3 = −1.5 dB … 6 = −6 dB, + /// 7 = mute; 0..=2 are reserved). + pub ltrtsurmixlev: u8, + /// `lorosurmixlev` (3 bits, Table D2.6 — same scale). + pub lorosurmixlev: u8, + /// `lfemixlevcode`/`lfemixlevcod` (5 bits, §E.2.3.1.x): LFE mix + /// level is `10 − lfemixlevcod` dB. + pub lfemixlevcod: Option, + /// `pgmscle`/`pgmscl` (6 bits, §E.2.3.1.12-13): program scale + /// factor `code − 51` dB (`0` = mute). + pub pgmscl: Option, + /// `extpgmscle`/`extpgmscl` (6 bits, §E.2.3.1.16-17) — external + /// program scale, same wire scale as `pgmscl`. + pub extpgmscl: Option, +} + +impl Default for Eac3MixMetadata { + /// −3 dB centre / −3 dB surround downmix levels on both LtRt and + /// LoRo targets, downmix preference not indicated, no LFE mix + /// level, no program scale factors. + fn default() -> Self { + Self { + dmixmod: 0, + ltrtcmixlev: 4, + lorocmixlev: 4, + ltrtsurmixlev: 4, + lorosurmixlev: 4, + lfemixlevcod: None, + pgmscl: None, + extpgmscl: None, + } + } +} + +/// §E.2.3.1.62+ informational-metadata block (`infomdate = 1`) for +/// encoder-side emission on the independent substream. The 2/0-only +/// (`dsurmod` / `dheadphonmod`) and ≥ 6-channel (`dsurexmod`) arms are +/// emitted only when acmod carries them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Eac3InfoMetadata { + /// `bsmod` (3 bits, Table 5.7) — bit-stream mode. + pub bsmod: u8, + /// `copyrightb` (§5.4.2.24 semantics). + pub copyrightb: bool, + /// `origbs` (§5.4.2.25 semantics). + pub origbs: bool, + /// `dsurmod` (2 bits, Table 5.11; 2/0 only; 3 is reserved). + pub dsurmod: u8, + /// `dheadphonmod` (2 bits; 2/0 only; 0 = not indicated, 1 = not + /// encoded, 2 = encoded, 3 reserved). + pub dheadphonmod: u8, + /// `dsurexmod` (2 bits; acmod ≥ 6 only; 3 is reserved). + pub dsurexmod: u8, + /// `audprodie` body: `mixlevel` (5 bits), `roomtyp` (2 bits, + /// 0..=2), `adconvtyp` (1 bit, HDCD A/D converter). + pub audprod: Option, + /// `sourcefscod` (1 bit) — source was sampled at twice the rate. + pub sourcefscod: bool, +} + +/// `audprodie` body for [`Eac3InfoMetadata`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Eac3AudioProduction { + /// `mixlevel` (5 bits, 0..=31) — peak SPL is `80 + mixlevel`. + pub mixlevel: u8, + /// `roomtyp` (2 bits, 0..=2; 3 is reserved) — Table 5.12. + pub roomtyp: u8, + /// `adconvtyp` (1 bit) — HDCD-type A/D conversion. + pub adconvtyp: bool, +} + +impl Default for Eac3InfoMetadata { + fn default() -> Self { + Self { + bsmod: 0, + copyrightb: false, + origbs: true, + dsurmod: 0, + dheadphonmod: 0, + dsurexmod: 0, + audprod: None, + sourcefscod: false, + } + } +} + +/// Encoder-side E-AC-3 bitstream-metadata surface: the fixed-BSI +/// `dialnorm` + `compr` words, the per-block §5.4.3.3-4 `dynrng` word +/// (emitted in every block of every substream), and the optional +/// Table E1.2 mixing / informational metadata blocks (emitted on the +/// independent substream; a 7.1 pair's dependent substream keeps +/// `mixmdate = infomdate = 0`). +/// +/// Registry option keys: `dialnorm` (1..=31), `compr` / `dynrng` +/// (8-bit gain words, decimal or `0x`-hex); mixing block — `dmixmod`, +/// `ltrtcmixlev`, `lorocmixlev`, `ltrtsurmixlev`, `lorosurmixlev`, +/// `lfemixlevcod`, `pgmscl`, `extpgmscl`; informational block — +/// `bsmod`, `copyright`, `origbs`, `dsurmod`, `dheadphonmod`, +/// `dsurexmod`, `mixlevel` + `roomtyp` + `adconvtyp`, `sourcefscod`. +/// Setting any key of a block emits that block. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Eac3Metadata { + /// `dialnorm` (5 bits, §5.4.2.8 semantics) — valid 1..=31. + pub dialnorm: u8, + /// §5.4.2.9-10 heavy-compression word (Table 7.30), per substream. + pub compr: Option, + /// §5.4.3.3-4 dynamic-range word (§7.7.1.2), emitted in every + /// audio block of every substream. + pub dynrng: Option, + /// Table E1.2 mixing-metadata block (`mixmdate = 1`). + pub mixmd: Option, + /// §E.2.3.1.62+ informational-metadata block (`infomdate = 1`). + pub infomd: Option, +} + +impl Default for Eac3Metadata { + /// The encoder's historical fixed words: dialnorm −27 dB, nothing + /// else emitted. + fn default() -> Self { + Self { + dialnorm: 27, + compr: None, + dynrng: None, + mixmd: None, + infomd: None, + } + } +} + +impl Eac3Metadata { + /// Range-check every codepoint against its syntax slot. + fn validate(&self) -> Result<()> { + if self.dialnorm == 0 || self.dialnorm > 31 { + return Err(Error::invalid(format!( + "eac3 encoder: dialnorm {} out of range (1..=31; 0 is reserved)", + self.dialnorm + ))); + } + if let Some(m) = &self.mixmd { + if m.dmixmod > 2 { + return Err(Error::invalid(format!( + "eac3 encoder: dmixmod {} out of range (0..=2; 3 is reserved)", + m.dmixmod + ))); + } + for (name, v) in [ + ("ltrtcmixlev", m.ltrtcmixlev), + ("lorocmixlev", m.lorocmixlev), + ] { + if v > 7 { + return Err(Error::invalid(format!( + "eac3 encoder: {name} {v} out of range (0..=7)" + ))); + } + } + for (name, v) in [ + ("ltrtsurmixlev", m.ltrtsurmixlev), + ("lorosurmixlev", m.lorosurmixlev), + ] { + if !(3..=7).contains(&v) { + return Err(Error::invalid(format!( + "eac3 encoder: {name} {v} out of range (3..=7; 0..=2 are reserved)" + ))); + } + } + if let Some(l) = m.lfemixlevcod { + if l > 31 { + return Err(Error::invalid(format!( + "eac3 encoder: lfemixlevcod {l} out of range (0..=31)" + ))); + } + } + for (name, v) in [("pgmscl", m.pgmscl), ("extpgmscl", m.extpgmscl)] { + if let Some(v) = v { + if v > 63 { + return Err(Error::invalid(format!( + "eac3 encoder: {name} {v} out of range (0..=63)" + ))); + } + } + } + } + if let Some(i) = &self.infomd { + if i.bsmod > 7 { + return Err(Error::invalid(format!( + "eac3 encoder: bsmod {} out of range (0..=7)", + i.bsmod + ))); + } + for (name, v) in [ + ("dsurmod", i.dsurmod), + ("dheadphonmod", i.dheadphonmod), + ("dsurexmod", i.dsurexmod), + ] { + if v > 2 { + return Err(Error::invalid(format!( + "eac3 encoder: {name} {v} out of range (0..=2; 3 is reserved)" + ))); + } + } + if let Some(a) = i.audprod { + if a.mixlevel > 31 { + return Err(Error::invalid(format!( + "eac3 encoder: mixlevel {} out of range (0..=31)", + a.mixlevel + ))); + } + if a.roomtyp > 2 { + return Err(Error::invalid(format!( + "eac3 encoder: roomtyp {} out of range (0..=2; 3 is reserved)", + a.roomtyp + ))); + } + } + } + Ok(()) + } + + /// Bits this metadata adds to one substream's syncframe on top of + /// the fixed layout (which already spends the 1-bit `compre` / + /// `mixmdate` / `infomdate` / per-block `dynrnge` flags). + fn reserve_bits(&self, acmod: u8, lfeon: bool, indep: bool) -> u32 { + let mut bits = 0u32; + if self.compr.is_some() { + bits += 8; + } + if self.dynrng.is_some() { + bits += 8 * BLOCKS_PER_FRAME as u32; + } + if !indep { + return bits; + } + if let Some(m) = &self.mixmd { + if acmod > 0x2 { + bits += 2; // dmixmod + } + if (acmod & 0x1) != 0 && acmod > 0x2 { + bits += 6; // ltrtcmixlev + lorocmixlev + } + if (acmod & 0x4) != 0 { + bits += 6; // ltrtsurmixlev + lorosurmixlev + } + if lfeon { + bits += 1 + if m.lfemixlevcod.is_some() { 5 } else { 0 }; + } + bits += 1 + if m.pgmscl.is_some() { 6 } else { 0 }; + bits += 1 + if m.extpgmscl.is_some() { 6 } else { 0 }; + bits += 2; // mixdef = 0 + if acmod < 0x2 { + bits += 1; // paninfoe = 0 + } + bits += 1; // frmmixcfginfoe = 0 + } + if let Some(i) = &self.infomd { + bits += 3 + 1 + 1; // bsmod + copyrightb + origbs + if acmod == 0x2 { + bits += 4; // dsurmod + dheadphonmod + } + if acmod >= 0x6 { + bits += 2; // dsurexmod + } + bits += 1 + if i.audprod.is_some() { 8 } else { 0 }; + bits += 1; // sourcefscod (fscod < 0x3 always here) + } + bits + } +} + +/// Parse the metadata codec options (see [`Eac3Metadata`]) from +/// `CodecParameters::options`; absent keys keep the defaults. +fn eac3_metadata_from_options(params: &CodecParameters) -> Result { + let opts = ¶ms.options; + let get_u8 = |key: &str| -> Result> { + match opts.get(key) { + None => Ok(None), + Some(v) => { + let parsed = + if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) { + u8::from_str_radix(hex, 16).ok() + } else { + v.parse::().ok() + }; + parsed.map(Some).ok_or_else(|| { + Error::invalid(format!( + "eac3 encoder: option {key}={v} (expected 0..=255, decimal or 0x-hex)" + )) + }) + } + } + }; + let get_bool = |key: &str| -> Result> { + match opts.get(key) { + None => Ok(None), + Some("1") | Some("true") => Ok(Some(true)), + Some("0") | Some("false") => Ok(Some(false)), + Some(v) => Err(Error::invalid(format!( + "eac3 encoder: option {key}={v} (expected 1/true/0/false)" + ))), + } + }; + let mut meta = Eac3Metadata::default(); + if let Some(v) = get_u8("dialnorm")? { + meta.dialnorm = v; + } + meta.compr = get_u8("compr")?; + meta.dynrng = get_u8("dynrng")?; + // Mixing block — any key present emits the block. + let dmixmod = get_u8("dmixmod")?; + let ltrtc = get_u8("ltrtcmixlev")?; + let loroc = get_u8("lorocmixlev")?; + let ltrts = get_u8("ltrtsurmixlev")?; + let loros = get_u8("lorosurmixlev")?; + let lfemix = get_u8("lfemixlevcod")?; + let pgmscl = get_u8("pgmscl")?; + let extpgmscl = get_u8("extpgmscl")?; + if dmixmod.is_some() + || ltrtc.is_some() + || loroc.is_some() + || ltrts.is_some() + || loros.is_some() + || lfemix.is_some() + || pgmscl.is_some() + || extpgmscl.is_some() + { + let d = Eac3MixMetadata::default(); + meta.mixmd = Some(Eac3MixMetadata { + dmixmod: dmixmod.unwrap_or(d.dmixmod), + ltrtcmixlev: ltrtc.unwrap_or(d.ltrtcmixlev), + lorocmixlev: loroc.unwrap_or(d.lorocmixlev), + ltrtsurmixlev: ltrts.unwrap_or(d.ltrtsurmixlev), + lorosurmixlev: loros.unwrap_or(d.lorosurmixlev), + lfemixlevcod: lfemix, + pgmscl, + extpgmscl, + }); + } + // Informational block — any key present emits the block. + let bsmod = get_u8("bsmod")?; + let copyright = get_bool("copyright")?; + let origbs = get_bool("origbs")?; + let dsurmod = get_u8("dsurmod")?; + let dheadphonmod = get_u8("dheadphonmod")?; + let dsurexmod = get_u8("dsurexmod")?; + let mixlevel = get_u8("mixlevel")?; + let roomtyp = get_u8("roomtyp")?; + let adconvtyp = get_bool("adconvtyp")?; + let sourcefscod = get_bool("sourcefscod")?; + if bsmod.is_some() + || copyright.is_some() + || origbs.is_some() + || dsurmod.is_some() + || dheadphonmod.is_some() + || dsurexmod.is_some() + || mixlevel.is_some() + || roomtyp.is_some() + || adconvtyp.is_some() + || sourcefscod.is_some() + { + let d = Eac3InfoMetadata::default(); + let audprod = if mixlevel.is_some() || roomtyp.is_some() || adconvtyp.is_some() { + Some(Eac3AudioProduction { + mixlevel: mixlevel.unwrap_or(0), + roomtyp: roomtyp.unwrap_or(0), + adconvtyp: adconvtyp.unwrap_or(false), + }) + } else { + None + }; + meta.infomd = Some(Eac3InfoMetadata { + bsmod: bsmod.unwrap_or(d.bsmod), + copyrightb: copyright.unwrap_or(d.copyrightb), + origbs: origbs.unwrap_or(d.origbs), + dsurmod: dsurmod.unwrap_or(d.dsurmod), + dheadphonmod: dheadphonmod.unwrap_or(d.dheadphonmod), + dsurexmod: dsurexmod.unwrap_or(d.dsurexmod), + audprod, + sourcefscod: sourcefscod.unwrap_or(d.sourcefscod), + }); + } + meta.validate()?; + Ok(meta) +} + +/// [`make_encoder`] with a typed [`Eac3Metadata`] — the encoder-side +/// bitstream-metadata surface (fixed-BSI `dialnorm`/`compr`, per-block +/// `dynrng`, and the Table E1.2 mixing / informational blocks). +pub fn make_encoder_with_metadata( + params: &CodecParameters, + meta: Eac3Metadata, +) -> Result> { + meta.validate()?; + let mut concrete = build_concrete_encoder(params)?; + concrete.meta = meta; + Ok(Box::new(concrete)) +} + +/// Enhanced coupling needs at least two coupled fbw channels in the +/// independent substream — mono has nothing to couple. +fn validate_ecpl(_ecpl: &EcplParams, channels: u16) -> Result<()> { + match channels { + 2 | 6 | 8 => Ok(()), + n => Err(Error::Unsupported(format!( + "eac3 encoder: ecpl requires 2, 6, or 8 input channels (got {n}) — \ + the independent substream must carry at least two fbw channels" + ))), + } +} + +/// Build an E-AC-3 encoder with the Adaptive Hybrid Transform enabled +/// (§3.4 / Table E1.3 `ahte`). Every full-bandwidth channel of every +/// substream is coded through the 6-block DCT-II + VQ/GAQ quantiser +/// stack: exponents are transmitted once per frame (block-0 anchor, +/// blocks 1..5 REUSE, so `nchregs[ch] == 1`), the per-bin `hebap[]` +/// pointers come from the §3.4.3.1 high-efficiency allocation, and +/// the block-0 mantissa slot carries the front-loaded `chgaqmod` + +/// gain words + 6×nmant codeword stream instead of six per-block +/// mantissa payloads. Best suited to stationary content (the encoder +/// forces long transforms — no block switching — while AHT is on). +/// +/// Also reachable through the registry path via +/// `CodecParameters::options` key `aht` = `1`/`true`. +pub fn make_encoder_with_aht(params: &CodecParameters) -> Result> { + let mut concrete = build_concrete_encoder(params)?; + concrete.aht = true; + Ok(Box::new(concrete)) +} + +/// Parse the `spx*` codec options (see [`make_encoder`]) into an +/// [`SpxParams`], or `None` when SPX is not requested. +fn spx_params_from_options(params: &CodecParameters) -> Result> { + let opts = ¶ms.options; + let parse_bool = |key: &str| -> Result> { + match opts.get(key) { + None => Ok(None), + Some("1") | Some("true") => Ok(Some(true)), + Some("0") | Some("false") => Ok(Some(false)), + Some(v) => Err(Error::invalid(format!( + "eac3 encoder: option {key}={v} (expected 1/true/0/false)" + ))), + } + }; + let parse_u8 = |key: &str, max: u8| -> Result> { + match opts.get(key) { + None => Ok(None), + Some(v) => match v.parse::() { + Ok(n) if n <= max => Ok(Some(n)), + _ => Err(Error::invalid(format!( + "eac3 encoder: option {key}={v} (expected 0..={max})" + ))), + }, + } + }; + let enabled = parse_bool("spx")?; + let begf = parse_u8("spx_begf", 7)?; + let endf = parse_u8("spx_endf", 7)?; + let strtf = parse_u8("spx_strtf", 3)?; + let blnd = parse_u8("spx_blnd", 31)?; + let atten = parse_u8("spx_atten", 31)?; + let adaptive = parse_bool("spx_adaptive_copy_start")?; + let explicit = parse_bool("spx_explicit_band_structure")?; + // §E.2.3.3.3 mixed per-channel chinspx: a bitmask of fbw channels + // in SPX (bit ch → channel ch). 0 / absent → all channels. + let chmask = parse_u8("spx_chmask", 255)?; + let any_subkey = begf.is_some() + || endf.is_some() + || strtf.is_some() + || blnd.is_some() + || atten.is_some() + || adaptive.is_some() + || explicit.is_some() + || chmask.is_some(); + // `spx=0` wins over sub-keys; sub-keys alone imply enablement. + let on = match enabled { + Some(v) => v, + None => any_subkey, + }; + if !on { + return Ok(None); + } + let d = SpxParams::default(); + Ok(Some(SpxParams { + spxbegf: begf.unwrap_or(d.spxbegf), + spxendf: endf.unwrap_or(d.spxendf), + spxstrtf: strtf.unwrap_or(d.spxstrtf), + spxblnd: blnd.unwrap_or(d.spxblnd), + adaptive_copy_start: adaptive.unwrap_or(d.adaptive_copy_start), + atten_code: atten, + explicit_band_structure: explicit.unwrap_or(d.explicit_band_structure), + channel_mask: match chmask { + None | Some(0) => None, + m => m, + }, + })) +} + +/// Build the concrete [`Eac3Encoder`] (with `snroffststr == 0`). The +/// public [`make_encoder`] boxes this behind `dyn Encoder`; the test-only +/// [`make_encoder_with_snroffststr`] reuses it and overrides the strategy. +fn build_concrete_encoder(params: &CodecParameters) -> Result { + let sample_rate = params.sample_rate.ok_or_else(|| { + Error::invalid("eac3 encoder: sample_rate is required (48000/44100/32000)") + })?; + let channels = params + .channels + .ok_or_else(|| Error::invalid("eac3 encoder: channels is required (1, 2, 6, or 8)"))?; + let fscod: u8 = match sample_rate { + 48_000 => 0, + 44_100 => 1, + 32_000 => 2, + _ => { + return Err(Error::Unsupported(format!( + "eac3 encoder: unsupported sample rate {sample_rate} (48000/44100/32000 only)" + ))) + } + }; + let target_kbps: Option = params.bit_rate.map(|b| (b / 1000) as u32); + + // Build per-substream layout descriptors. For 1/2/6 channel input + // we emit one independent substream; for 8 we emit indep+dep. + let layout = match channels { + 1 => Layout::Indep(SubstreamLayout { + strmtyp: 0, + substreamid: 0, + acmod: 1, + lfeon: false, + nfchans: 1, + chanmap: None, + // Map each substream channel slot to the input PCM + // interleaved-channel index. Mono = passthrough. + src_indices: vec![0], + // Bytes per syncframe; chosen from `bit_rate` or the per- + // layout default below. + kbps: target_kbps.unwrap_or(96), + frame_bytes: 0, + }), + 2 => Layout::Indep(SubstreamLayout { + strmtyp: 0, + substreamid: 0, + acmod: 2, + lfeon: false, + nfchans: 2, + chanmap: None, + src_indices: vec![0, 1], + kbps: target_kbps.unwrap_or(192), + frame_bytes: 0, + }), + 6 => Layout::Indep(SubstreamLayout { + strmtyp: 0, + substreamid: 0, + acmod: 7, // 3/2 — L,C,R,Ls,Rs + lfeon: true, + nfchans: 5, + chanmap: None, + // Input layout L,C,R,Ls,Rs,LFE → substream order: + // [L, C, R, Ls, Rs] then LFE pseudo-channel last. + src_indices: vec![0, 1, 2, 3, 4, 5], + kbps: target_kbps.unwrap_or(384), + frame_bytes: 0, + }), + 8 => { + // 7.1 input: L,C,R,Ls,Rs,LFE,Lb,Rb. + // Indep substream = 5.1 downmix where Ls/Rs come straight + // from the 7.1 source's surround pair (the spec's + // §E.3.8.2 figure shows the 5.1 downmix as the + // independently-decodable program). + let total_kbps = target_kbps.unwrap_or(576); + // Split: most of the budget goes to the 5.1 indep stream, + // a quarter goes to the dep Lb/Rb pair (matches stereo). + let indep_kbps = match total_kbps { + k if k >= 384 + 192 => 384, + k => (k * 2) / 3, // generous fallback + }; + let dep_kbps = total_kbps.saturating_sub(indep_kbps).max(64); + Layout::Pair { + indep: SubstreamLayout { + strmtyp: 0, + substreamid: 0, + acmod: 7, + lfeon: true, + nfchans: 5, + chanmap: None, + src_indices: vec![0, 1, 2, 3, 4, 5], + kbps: indep_kbps, + frame_bytes: 0, + }, + dep: SubstreamLayout { + strmtyp: 1, + substreamid: 0, + acmod: 2, // 2/0 — two coded channels (Lb, Rb) + lfeon: false, + nfchans: 2, + // chanmap bit 6 = Lrs/Rrs pair (Table E2.5). + // Per §E.2.3.1.8: bit 0 → MSB of the 16-bit field. + // bit 6 → MSB-6 → mask = 1 << (15 - 6) = 0x0200. + chanmap: Some(1u16 << (15 - 6)), + // Source PCM offsets 6 (Lb) and 7 (Rb). + src_indices: vec![6, 7], + kbps: dep_kbps, + frame_bytes: 0, + }, + } + } + n => { + return Err(Error::Unsupported(format!( + "eac3 encoder: unsupported channel count {n} (must be 1, 2, 6, or 8)" + ))) + } + }; + + // Resolve frame_bytes for each substream + total per-syncframe size + // (used in `out_params.bit_rate` reporting). + let total_kbps_reported: u32; + let layout_resolved = match layout { + Layout::Indep(mut s) => { + let bytes = ac3_frame_bytes(fscod, s.kbps).ok_or_else(|| { + Error::Unsupported(format!( + "eac3 encoder: bit rate {} kbps has no frame-size mapping", + s.kbps + )) + })? as usize; + s.frame_bytes = bytes; + total_kbps_reported = s.kbps; + Layout::Indep(s) + } + Layout::Pair { mut indep, mut dep } => { + let i_bytes = ac3_frame_bytes(fscod, indep.kbps).ok_or_else(|| { + Error::Unsupported(format!( + "eac3 encoder: indep bit rate {} kbps has no frame-size mapping", + indep.kbps + )) + })? as usize; + let d_bytes = ac3_frame_bytes(fscod, dep.kbps).ok_or_else(|| { + Error::Unsupported(format!( + "eac3 encoder: dep bit rate {} kbps has no frame-size mapping", + dep.kbps + )) + })? as usize; + indep.frame_bytes = i_bytes; + dep.frame_bytes = d_bytes; + total_kbps_reported = indep.kbps + dep.kbps; + Layout::Pair { indep, dep } + } + }; + + let total_pcm_chans = channels as usize; + let out_params = { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(sample_rate); + p.channels = Some(channels); + p.sample_format = Some(SampleFormat::S16); + p.bit_rate = Some(total_kbps_reported as u64 * 1000); + p + }; + + let input_sample_format = params.sample_format.unwrap_or(SampleFormat::S16); + Ok(Eac3Encoder { + codec_id: CodecId::new(CODEC_ID_STR), + out_params, + sample_rate, + total_pcm_chans, + input_sample_format, + fscod, + layout: layout_resolved, + // One delay-line / transient slot per *input* channel. Every + // distinct PCM channel that ends up in any substream needs its + // own MDCT-priming history; channels never appear in two + // substreams under the 7.1 layout, so total_pcm_chans is the + // upper bound. + delay_line: vec![vec![0.0f32; SAMPLES_PER_BLOCK]; total_pcm_chans], + pending_samples: vec![Vec::::new(); total_pcm_chans], + transient_state: (0..total_pcm_chans) + .map(|_| TransientDetector::default()) + .collect(), + packet_queue: Vec::new(), + pts: 0, + snroffststr: 0, + spx: None, + aht: false, + ecpl: None, + meta: Eac3Metadata::default(), + ecpl_carry: vec![None, None], + }) +} + +/// Validate a mixed-membership `channel_mask` against the encoder's +/// channel count (§E.2.3.3.3): the mask must keep at least one coded +/// channel in SPX, and mono cannot exclude its only channel (the +/// spec makes `chinspx[0]` implicit for `acmod == 0x1`). +fn validate_spx_channel_mask(spx: &SpxParams, channels: u16) -> Result<()> { + if let Some(m) = spx.channel_mask { + if m == 0 { + return Err(Error::invalid( + "eac3 encoder: spx channel_mask must keep at least one channel in SPX", + )); + } + if channels == 1 && m & 1 == 0 { + return Err(Error::invalid( + "eac3 encoder: mono SPX cannot exclude channel 0 (chinspx[0] is implicit)", + )); + } + } + Ok(()) +} + +/// Build an E-AC-3 encoder with Spectral Extension (SPX, §E.2.3.3 / +/// §E.3.6) enabled. +/// +/// SPX is E-AC-3's parametric high-frequency reconstruction: the coded +/// bandwidth of every full-bandwidth channel stops at the SPX begin +/// frequency (`25 + 12·spx_begin_subbnd` transform coefficients; tc# +/// 109 ≈ 10.2 kHz at 48 kHz for the default [`SpxParams`]), and the +/// decoder regenerates the extension region from a translated copy of +/// the channel's own low-frequency spectrum, noise-blended and scaled +/// by per-band coordinates the encoder derives from the §3.6.4.3 +/// energy-matching rule. The bits saved on high-frequency exponents / +/// mantissas are re-spent on the coded low band by the SNR-offset +/// tuner, so SPX trades exact HF waveforms for a better LF floor — +/// the intended low-bit-rate operating mode. +/// +/// Accepts the same `params` as [`make_encoder`]. The geometry is +/// validated up front; an invalid combination (inverted sub-band +/// range, empty copy region, out-of-range field) fails here rather +/// than at the first frame. +pub fn make_encoder_with_spx(params: &CodecParameters, spx: SpxParams) -> Result> { + // Fail fast on invalid geometry (same validation the per-frame + // emitter performs). + SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC)?; + validate_spx_channel_mask(&spx, params.channels.unwrap_or(0))?; + let mut concrete = build_concrete_encoder(params)?; + concrete.spx = Some(spx); + Ok(Box::new(concrete)) +} + +/// Test-only constructor that selects a non-zero `snroffststr` (per-block +/// SNR-offset strategy). Builds the same encoder as [`make_encoder`] then +/// overrides the strategy. Used by the round-trip tests to drive the +/// decoder's §2.3.3.27 per-block SNR-offset parse. +#[cfg(test)] +pub(crate) fn make_encoder_with_snroffststr( + params: &CodecParameters, + snroffststr: u8, +) -> Result> { + let mut concrete = build_concrete_encoder(params)?; + concrete.snroffststr = snroffststr; + Ok(Box::new(concrete)) +} + +/// Pick a syncframe size in bytes for a given (fscod, target kbps). +/// Returns `None` for unsupported bit rates. +/// +/// Formula matches AC-3's Table 5.18 row size: +/// `bytes = round((kbps * 1000 * SAMPLES_PER_FRAME) / (sample_rate * 8))` +/// For 48 kHz this collapses to `kbps * 4` exactly. +fn ac3_frame_bytes(fscod: u8, kbps: u32) -> Option { + let sample_rate: u32 = match fscod { + 0 => 48_000, + 1 => 44_100, + 2 => 32_000, + _ => return None, + }; + // 1536 samples per syncframe; 8 bits per byte. + let numerator = kbps as u64 * 1000 * (SAMPLES_PER_FRAME as u64); + let denom = sample_rate as u64 * 8; + // AC-3 spec rounds toward the table value; we round down so the + // bit-rate is a hard upper bound. Even byte counts only: round to + // the nearest 2-byte boundary so the frame size is a whole word + // (E-AC-3 frmsiz is in 16-bit words, value = words - 1). + let raw = numerator / denom; + let rounded = raw & !1; + if (32..=4096).contains(&rounded) { + Some(rounded as u32) + } else { + None + } +} + +/// Configuration for one E-AC-3 substream within a syncframe pair. +#[derive(Clone, Debug)] +struct SubstreamLayout { + /// `strmtyp` field (§E.2.3.1.1). 0 = independent, 1 = dependent. + strmtyp: u8, + /// `substreamid` (§E.2.3.1.2). 0 for the primary indep / first dep. + substreamid: u8, + /// AC-3 audio coding mode (Table 5.8) of the coded substream. + acmod: u8, + /// Whether an LFE pseudo-channel is coded inside the substream. + lfeon: bool, + /// Number of full-bandwidth channels coded by this substream + /// (`acmod_nfchans(acmod)`). + nfchans: usize, + /// Custom chanmap (§E.2.3.1.7-8). When `Some`, `chanmape=1` is + /// emitted in the bsi and the 16-bit `chanmap` field follows. Only + /// valid when `strmtyp == 1`. + chanmap: Option, + /// Source-PCM channel index for each coded slot. Length = + /// `nfchans + lfeon as usize`. + src_indices: Vec, + /// Target bit rate in kbps (recorded for reporting / debug). + #[allow(dead_code)] + kbps: u32, + /// Computed syncframe size in bytes (filled in by `make_encoder`). + frame_bytes: usize, +} + +#[derive(Clone, Debug)] +enum Layout { + /// Single independent substream. + Indep(SubstreamLayout), + /// 7.1: independent (5.1 channel program) + dependent (Lb/Rb pair). + Pair { + indep: SubstreamLayout, + dep: SubstreamLayout, + }, +} + +struct Eac3Encoder { + codec_id: CodecId, + out_params: CodecParameters, + sample_rate: u32, + /// Total number of input PCM channels (1, 2, 6, or 8). + total_pcm_chans: usize, + input_sample_format: SampleFormat, + fscod: u8, + layout: Layout, + /// Per-input-channel left-half MDCT context (256 samples each). + delay_line: Vec>, + /// Per-input-channel pending PCM samples, drained 1536 at a time. + pending_samples: Vec>, + /// Per-input-channel transient-detector state. + transient_state: Vec, + packet_queue: Vec, + pts: i64, + /// SNR-offset strategy emitted in audfrm (§2.3.2.3 / Table E1.3). + /// `0` = single frame-level pair (the default the public + /// [`make_encoder`] always selects); `1` = one shared per-block fine + /// offset; `2` = independent per-channel per-block fine offsets. The + /// non-zero modes are exercised by the round-trip tests to validate + /// the decoder's per-block SNR-offset parse; the chosen offsets equal + /// the frame-level values so the decoded PCM matches the `0` baseline. + snroffststr: u8, + /// Spectral extension configuration (§E.2.3.3 / §E.3.6). `None` + /// (the [`make_encoder`] default) codes the full bandwidth; `Some` + /// (via [`make_encoder_with_spx`]) puts every fbw channel of every + /// substream in SPX. + spx: Option, + /// Adaptive Hybrid Transform (§3.4). When `true` (via + /// [`make_encoder_with_aht`] or the `aht` option) every fbw + /// channel of every substream is AHT-coded: single block-0 + /// exponent anchor, hebap-driven VQ/GAQ mantissas front-loaded in + /// block 0, long transforms only. Mutually exclusive with `spx`. + aht: bool, + /// Enhanced coupling (§E.2.3.3.16-26 / §E.3.5.5). When `Some` (via + /// [`make_encoder_with_ecpl`] or the `ecpl*` options) every fbw + /// channel of the independent substream is coupled: the region + /// above the enhanced-coupling begin frequency is carried by one + /// shared carrier channel plus per-band amplitude/angle + /// coordinates. Long transforms are forced while enhanced coupling + /// is on. Mutually exclusive with `spx` and `aht` + /// (single-subsystem scope). + ecpl: Option, + /// Encoder-side bitstream metadata (fixed-BSI dialnorm/compr, + /// per-block dynrng, Table E1.2 mixing / informational blocks). + /// Validated at construction. + meta: Eac3Metadata, + /// Per-substream cross-frame enhanced-coupling analysis carry: the + /// previous frame's last-block carrier + per-channel + /// region-restricted MDCT buffers, mirroring the decoder's + /// `EcplState::prev_frame_last_mant` threading (§E.3.5.5.1 "previous + /// block" of frame block 0). Index = substream position in the + /// layout (0 = indep, 1 = dep). + ecpl_carry: Vec>, +} + +/// Cross-frame enhanced-coupling carry (see [`Eac3Encoder::ecpl_carry`]). +#[derive(Clone)] +struct EcplCarry { + /// Previous frame's last-block carrier MDCT (region-restricted). + carrier: [f32; 256], + /// Previous frame's last-block per-fbw-channel region-restricted + /// MDCT buffers (the per-channel §E.3.5.5.1 analysis neighbours). + channels: Vec<[f32; 256]>, +} + +impl Encoder for Eac3Encoder { + fn codec_id(&self) -> &CodecId { + &self.codec_id + } + fn output_params(&self) -> &CodecParameters { + &self.out_params + } + fn send_frame(&mut self, frame: &Frame) -> Result<()> { + let audio = match frame { + Frame::Audio(a) => a, + _ => { + return Err(Error::invalid( + "eac3 encoder: send_frame requires an audio frame", + )) + } + }; + let per_chan = decode_input_samples(audio, self.total_pcm_chans, self.input_sample_format)?; + for ch in 0..self.total_pcm_chans { + self.pending_samples[ch].extend_from_slice(&per_chan[ch]); + } + while self.pending_samples[0].len() as u32 >= SAMPLES_PER_FRAME { + self.emit_syncframe()?; + } + Ok(()) + } + fn receive_packet(&mut self) -> Result { + if self.packet_queue.is_empty() { + return Err(Error::NeedMore); + } + Ok(self.packet_queue.remove(0)) + } + fn flush(&mut self) -> Result<()> { + if self.pending_samples[0].is_empty() { + return Ok(()); + } + let missing = SAMPLES_PER_FRAME as usize - self.pending_samples[0].len(); + if missing > 0 { + for ch in 0..self.total_pcm_chans { + self.pending_samples[ch].extend(std::iter::repeat(0.0).take(missing)); + } + } + self.emit_syncframe()?; + Ok(()) + } +} + +impl Eac3Encoder { + /// Drain one syncframe worth of PCM (1536 samples per input channel), + /// emit one packet whose payload is the indep substream — or the + /// indep+dep concatenation for the 7.1 pair layout. + fn emit_syncframe(&mut self) -> Result<()> { + let n_per = SAMPLES_PER_FRAME as usize; + + // Drain `n_per` samples per input channel into a per-input + // PCM matrix, *advance* the per-input delay-line + transient + // state once across all blocks/channels. We do the windowing / + // MDCT inside `emit_substream` per-substream so that blksw and + // exponents are computed against the substream's actual coded + // PCM (which for indep substream of 7.1 is just a select of + // the source channels). + let mut frame_pcm: Vec> = Vec::with_capacity(self.total_pcm_chans); + for ch in 0..self.total_pcm_chans { + let drain: Vec = self.pending_samples[ch].drain(0..n_per).collect(); + frame_pcm.push(drain); + } + + let mut payload = Vec::::with_capacity(self.layout_total_frame_bytes()); + // Snapshot the layout once so we don't borrow self while + // calling emit_substream. + let substreams: Vec = match &self.layout { + Layout::Indep(s) => vec![s.clone()], + Layout::Pair { indep, dep } => vec![indep.clone(), dep.clone()], + }; + for (sub_idx, sub) in substreams.iter().enumerate() { + let bytes = self.emit_substream(sub_idx, sub, &frame_pcm)?; + payload.extend_from_slice(&bytes); + } + + self.packet_queue.push( + Packet::new(0, TimeBase::new(1, self.sample_rate as i64), payload).with_pts(self.pts), + ); + self.pts += SAMPLES_PER_FRAME as i64; + Ok(()) + } + + /// Total syncframe-pair size in bytes. + fn layout_total_frame_bytes(&self) -> usize { + match &self.layout { + Layout::Indep(s) => s.frame_bytes, + Layout::Pair { indep, dep } => indep.frame_bytes + dep.frame_bytes, + } + } + + /// Emit one E-AC-3 syncframe (indep or dep substream) of size + /// `sub.frame_bytes` bytes for the channels named by `sub.src_indices`. + fn emit_substream( + &mut self, + sub_idx: usize, + sub: &SubstreamLayout, + frame_pcm: &[Vec], + ) -> Result> { + let nfchans = sub.nfchans; + let total_chans = nfchans + usize::from(sub.lfeon); + // Enhanced coupling applies to the independent substream only + // (the 7.1 pair's dependent Lb/Rb stream stays plain) and needs + // at least two fbw channels to couple. + let ecpl_on = self.ecpl.is_some() && sub.strmtyp == 0 && nfchans >= 2; + let ecpl_geom: Option = match (&self.ecpl, ecpl_on) { + (Some(p), true) => Some(match &self.spx { + // SPX co-active: the coupling region is bounded by the + // SPX begin frequency and `ecplendf` is not transmitted + // (§E.2.3.3.17). + Some(spx) => EcplGeometry::derive_with_spx(p, spx.spxbegf, &DEFAULT_ECPL_BNDSTRC)?, + None => EcplGeometry::derive(p, &DEFAULT_ECPL_BNDSTRC)?, + }), + _ => None, + }; + + // -------- DSP: window + MDCT per substream channel per block -------- + let mut coeffs: Vec> = + vec![vec![[0.0; N_COEFFS]; BLOCKS_PER_FRAME]; total_chans]; + // blksw exists only for fbw channels (LFE never short-blocks). + let mut blksw: Vec<[bool; BLOCKS_PER_FRAME]> = vec![[false; BLOCKS_PER_FRAME]; nfchans]; + for ch in 0..total_chans { + let src_idx = sub.src_indices[ch]; + let drain = &frame_pcm[src_idx]; + for blk in 0..BLOCKS_PER_FRAME { + let mut in_buf = [0.0f32; 512]; + in_buf[..256].copy_from_slice(&self.delay_line[src_idx]); + in_buf[256..].copy_from_slice( + &drain[blk * SAMPLES_PER_BLOCK..(blk + 1) * SAMPLES_PER_BLOCK], + ); + let is_lfe_chan = sub.lfeon && ch == nfchans; + // AHT frames force long transforms: the 6-block DCT-II + // (§3.4.1) targets stationary content and a short block + // would break the cross-block bin alignment. + let is_short = if is_lfe_chan + || self.aht + || ecpl_on + || std::env::var("EAC3_DISABLE_BLKSW").is_ok() + { + false + } else { + self.transient_state[src_idx].process(&in_buf[256..]) + }; + if !is_lfe_chan { + blksw[ch][blk] = is_short; + } + let mut win_buf = [0.0f32; 512]; + for n in 0..256 { + win_buf[n] = in_buf[n] * WINDOW[n]; + win_buf[511 - n] = in_buf[511 - n] * WINDOW[n]; + } + self.delay_line[src_idx].copy_from_slice( + &drain[blk * SAMPLES_PER_BLOCK..(blk + 1) * SAMPLES_PER_BLOCK], + ); + if is_short { + mdct_256_pair(&win_buf, &mut coeffs[ch][blk]); + } else { + mdct_512(&win_buf, &mut coeffs[ch][blk]); + } + } + } + + // -------- Exponents -------- + // Layout (matches the AC-3 helpers' expectation): + // 0..nfchans → fbw channels + // nfchans → coupling pseudo-channel (unused — cplinu=0) + // nfchans + 1 → LFE pseudo-channel (when lfeon) + let lfe_idx_in_exps = nfchans + 1; + let mut exps: Vec> = + vec![vec![[24u8; N_COEFFS]; BLOCKS_PER_FRAME]; nfchans + 2]; + let chbwcod: u8 = 60; + // §E.2.3.3 / §E.3.3.3 — with SPX in use, every fbw channel's + // coded bandwidth ends at the SPX begin frequency + // (`endmant = spxbandtable[spx_begin_subbnd]`) and no chbwcod + // is emitted; without SPX the bandwidth code drives it. + // The emitted `spxstrtf` — either the configured code or, with + // `adaptive_copy_start`, the per-frame least-saturated pick. + let mut spx_strtf_emit: u8 = self.spx.as_ref().map_or(0, |p| p.spxstrtf); + let spx_geom = match &self.spx { + Some(p) => { + let mut geom = SpxGeometry::derive(p, &DEFAULT_SPX_BNDSTRC)?; + if p.adaptive_copy_start { + // Score each valid candidate by the frame's total + // coordinate saturation (log-excess above the 0.875 + // representable ceiling, §E.2.3.3.11-13), summed + // over channels and bands. Strict `<` keeps the + // lowest candidate on ties — the largest copy + // region carries the most source correlation. + let mut best_score = f64::INFINITY; + for cand in 0..=3u8 { + let cand_params = SpxParams { + spxstrtf: cand, + ..*p + }; + let Ok(g) = SpxGeometry::derive(&cand_params, &DEFAULT_SPX_BNDSTRC) else { + continue; // empty copy region — invalid + }; + let mut score = 0.0f64; + for ch_coeffs in coeffs.iter().take(nfchans) { + let span: Vec<&[f32; N_COEFFS]> = ch_coeffs.iter().collect(); + let targets = band_coord_targets_span_atten(&span, &g, p.atten_code); + for &t in targets.iter().take(g.nbnds) { + if t as f64 > 0.875 { + score += (t as f64 / 0.875).log2(); + } + } + } + if score < best_score { + best_score = score; + spx_strtf_emit = cand; + geom = g; + } + } + } + Some(geom) + } + None => None, + }; + let end_full: usize = 37 + 3 * (chbwcod as usize + 12); + // §E.2.3.3.3 chinspx[ch] — mixed per-channel SPX membership. + // With no mask (or SPX off) the vector is uniform. + let in_spx: Vec = (0..nfchans) + .map(|ch| { + spx_geom.is_some() + && ( + // Mono substreams carry no chinspx bits — the + // decoder assumes chinspx[0] == 1 (§E.2.3.3.3), + // so the mask cannot exclude the only channel. + (sub.acmod == 0x1 && ch == 0) + || self + .spx + .as_ref() + .and_then(|p| p.channel_mask) + .map_or(true, |m| m & (1 << ch) != 0) + ) + }) + .collect(); + let end_mant: usize = match (&spx_geom, &ecpl_geom) { + (Some(g), _) => g.begin_tc, + (None, Some(g)) => g.start_bin, + (None, None) => end_full, + }; + // Per-channel coded bandwidth: SPX channels stop at the SPX + // begin frequency (§E.3.3.3), enhanced-coupling channels stop + // at the region start (§E.3.3.3: endmant[ch] = + // ecplsubbndtab[ecpl_begin_subbnd]), the rest run to the + // chbwcod-derived end. + let end_mant_ch: Vec = (0..nfchans) + .map(|ch| { + if let Some(g) = &ecpl_geom { + g.start_bin + } else if in_spx[ch] { + spx_geom.as_ref().map_or(end_full, |g| g.begin_tc) + } else { + end_full + } + }) + .collect(); + let ch_end_mant = end_mant; + for ch in 0..nfchans { + for blk in 0..BLOCKS_PER_FRAME { + for k in 0..end_mant_ch[ch] { + exps[ch][blk][k] = extract_exponent(coeffs[ch][blk][k]); + } + } + } + if self.aht { + // §3.4.2: AHT channels transmit exponents ONCE per frame + // (nchregs[ch] == 1). The single set must bound all six + // blocks' coefficients, so extract from the per-bin frame + // maximum — every block's mantissa then stays sub-unity. + for ch in 0..nfchans { + for k in 0..ch_end_mant { + let mut mx = 0.0f32; + for blk in 0..BLOCKS_PER_FRAME { + mx = mx.max(coeffs[ch][blk][k].abs()); + } + exps[ch][0][k] = extract_exponent(mx); + } + } + } + if sub.lfeon { + // §7.1.3: LFE is spectrally constrained to 0-120 Hz per the + // AC-3 / E-AC-3 specification. At 48 kHz with a 512-point + // MDCT, bin k ≈ (2k+1)×48000/1024 Hz; bin 0 ≈ 47 Hz, + // bin 1 ≈ 141 Hz. We zero coefficients at bin ≥ 2 to enforce + // the 0–120 Hz constraint before exponent extraction, keeping + // only the sub-120 Hz content in the coded LFE signal. The + // LFE_END_MANT bitstream limit remains 7 (decoder expects it), + // but bins 2..7 are set to silence so they don't consume bits. + let lfe_cutoff = match self.sample_rate { + 48_000 => 2usize, // bin 0 ≈ 47 Hz, bin 1 ≈ 141 Hz → keep 0..2 + 44_100 => 2usize, + 32_000 => 2usize, + _ => 2usize, + }; + for blk in 0..BLOCKS_PER_FRAME { + for k in lfe_cutoff..LFE_END_MANT { + coeffs[nfchans][blk][k] = 0.0; + } + for k in 0..LFE_END_MANT { + exps[lfe_idx_in_exps][blk][k] = extract_exponent(coeffs[nfchans][blk][k]); + } + } + } + // Exponent-strategy selection: adaptive D15/D25/D45 per-channel + // per-anchor-block (§7.1.3 / §5.4.3.22). The frame-wide anchor + // pattern [1,0,0,1,0,0] is preserved; for each anchor block + // (block 0 and block 3) we pick the smoothest legal strategy + // (D15/D25/D45) per channel. Blocks 1/2/4/5 always REUSE. + // EAC3_DISABLE_EXPSTR_SEL=1 pins every anchor to D15 (same as + // old static behaviour) for A/B testing. + let exp_strategies: [u8; BLOCKS_PER_FRAME] = [1, 0, 0, 1, 0, 0]; + // §3.4.2: LFE AHT eligibility needs nlferegs == 1 — a single + // D15 anchor at block 0. Non-AHT frames keep the two-anchor + // pattern. + let lfe_exp_strategies: [u8; BLOCKS_PER_FRAME] = if self.aht { + [1, 0, 0, 0, 0, 0] + } else { + exp_strategies + }; + let chexpstr_plan: Vec<[u8; BLOCKS_PER_FRAME]> = if self.aht { + // §3.4.2 AHT eligibility: nchregs[ch] == 1 — a single + // block-0 anchor with blocks 1..5 all REUSE. Pick the + // smoothest legal strategy for the frame-wide exponent set + // and propagate it to every block (the decoder reuses the + // block-0 exponents for the whole frame, and the AHT + // mantissa cache is scaled against them once). + let mut out = vec![[0u8; BLOCKS_PER_FRAME]; nfchans]; + for ch in 0..nfchans { + preprocess_d15(&mut exps[ch][0][..ch_end_mant]); + let strat = pick_strategy_for_block(&exps[ch][0], ch_end_mant); + out[ch][0] = strat; + if strat >= 2 { + let grpsize = if strat == 2 { 2 } else { 4 }; + quantise_exponents_to_grpsize(&mut exps[ch][0][..ch_end_mant], grpsize); + } + for blk in 1..BLOCKS_PER_FRAME { + let src: [u8; N_COEFFS] = exps[ch][0]; + exps[ch][blk][..ch_end_mant].copy_from_slice(&src[..ch_end_mant]); + } + } + out + } else { + // D15-preprocess every anchor block first so the strategy + // picker sees legalised exponents. + for ch in 0..nfchans { + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + preprocess_d15(&mut exps[ch][blk][..end_mant_ch[ch]]); + } + } + } + let plan: Vec<[u8; BLOCKS_PER_FRAME]> = + if std::env::var("EAC3_DISABLE_EXPSTR_SEL").is_ok() { + let mut out = vec![[0u8; BLOCKS_PER_FRAME]; nfchans]; + for ch in 0..nfchans { + out[ch] = exp_strategies; + } + out + } else { + select_exp_strategies_per_end(&exps, nfchans, &end_mant_ch) + }; + // Apply grpsize quantisation for D25/D45 anchor blocks. + for ch in 0..nfchans { + let ch_end = end_mant_ch[ch]; + for blk in 0..BLOCKS_PER_FRAME { + let strat = plan[ch][blk]; + if strat >= 2 { + let grpsize = if strat == 2 { 2 } else { 4 }; + quantise_exponents_to_grpsize(&mut exps[ch][blk][..ch_end], grpsize); + } + } + // REUSE blocks get the most-recent anchor's exponents. + let mut last = 0usize; + for blk in 0..BLOCKS_PER_FRAME { + if plan[ch][blk] != 0 { + last = blk; + } else { + let src: [u8; N_COEFFS] = exps[ch][last]; + exps[ch][blk][..ch_end].copy_from_slice(&src[..ch_end]); + } + } + } + plan + }; + if sub.lfeon { + // AHT LFE: one exponent set per frame from the per-bin + // 6-block maximum (mirrors the fbw AHT handling above). + if self.aht { + for k in 0..LFE_END_MANT { + let mut mx = 0.0f32; + for blk in 0..BLOCKS_PER_FRAME { + mx = mx.max(coeffs[nfchans][blk][k].abs()); + } + exps[lfe_idx_in_exps][0][k] = extract_exponent(mx); + } + } + // LFE strategy: D15 on anchor blocks, REUSE elsewhere. The + // 1-bit lfeexpstr field only supports D15 or REUSE (§5.4.3.23 + // / §E.1.2.3). + for blk in 0..BLOCKS_PER_FRAME { + if lfe_exp_strategies[blk] == 1 { + preprocess_d15(&mut exps[lfe_idx_in_exps][blk][..LFE_END_MANT]); + } + } + let mut last = 0usize; + for blk in 0..BLOCKS_PER_FRAME { + if lfe_exp_strategies[blk] == 1 { + last = blk; + } else { + let src: [u8; N_COEFFS] = exps[lfe_idx_in_exps][last]; + exps[lfe_idx_in_exps][blk][..LFE_END_MANT] + .copy_from_slice(&src[..LFE_END_MANT]); + } + } + } + + // -------- Enhanced coupling: carrier + coordinates (§E.3.5.5) ------ + // + // Built before the SNR tuner so the carrier's exponents ride the + // shared coupling-channel accounting. The coordinate measurement + // (pass 2) analyses the decoder-faithful carrier — including the + // zero next-block spectrum at the frame edge and the previous + // frame's carried last block — so every analysis imperfection + // folds into the transmitted amplitudes/angles. + let ecpl_carrier_plan: Option = ecpl_geom + .as_ref() + .map(|g| build_ecpl_carrier(g, &coeffs, nfchans)); + if let (Some(g), Some(plan)) = (&ecpl_geom, &ecpl_carrier_plan) { + // Carrier exponents into the cpl pseudo-channel slot: D15 on + // the anchor blocks (0 and 3, matching `exp_strategies` and + // the audfrm `cplexpstr[blk]` codes), REUSE elsewhere — + // mirroring the fbw handling so the decoder's exponent state + // matches the encoder's `exps[]` bin-for-bin. + let cpl_idx = nfchans; + for blk in 0..BLOCKS_PER_FRAME { + for k in g.start_bin..g.end_bin { + exps[cpl_idx][blk][k] = extract_exponent(plan.carrier[blk][k]); + } + if exp_strategies[blk] == 1 { + preprocess_d15(&mut exps[cpl_idx][blk][g.start_bin..g.end_bin]); + } + } + let mut last = 0usize; + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + last = blk; + } else { + let src: [u8; N_COEFFS] = exps[cpl_idx][last]; + exps[cpl_idx][blk][g.start_bin..g.end_bin] + .copy_from_slice(&src[g.start_bin..g.end_bin]); + } + } + } + + // -------- SPX coordinate planning (§E.2.3.3.9-13 / §E.3.6.3) -------- + // + // Coordinates are refreshed twice per frame — block 0 (where + // `spxcoe` is implicit for a channel's first SPX block, + // §E.2.3.3.9) and block 3, matching the exponent anchor + // pattern; blocks 1/2/4/5 emit `spxcoe = 0` and reuse. Each + // refresh's coordinate set energy-matches its whole 3-block + // span (§3.6.4.3), computed from the encoder's own unquantised + // MDCT spectra via the shared decoder translation plan. + const SPX_REFRESH_BLOCKS: [usize; 2] = [0, 3]; + const SPX_SPAN: usize = 3; + // Per channel, per refresh span: (mstrspxco, per-band (exp, mant)). + type SpxCoordSet = (u8, [(u8, u8); 18]); + let spx_coords: Option> = spx_geom.as_ref().map(|g| { + (0..nfchans) + .map(|ch| { + SPX_REFRESH_BLOCKS.map(|start| { + let span: Vec<&[f32; N_COEFFS]> = + (start..start + SPX_SPAN).map(|b| &coeffs[ch][b]).collect(); + let targets = band_coord_targets_span_atten( + &span, + g, + self.spx.as_ref().and_then(|p| p.atten_code), + ); + let mstr = choose_mstrspxco(&targets[..g.nbnds]); + let mut codes = [(0u8, 0u8); 18]; + for bnd in 0..g.nbnds { + codes[bnd] = quantise_coord(targets[bnd], mstr); + } + (mstr, codes) + }) + }) + .collect() + }); + + // -------- AHT-domain coefficients (§3.4.5, forward direction) ---- + // + // Per bin, the 6 per-block mantissas (coeff · 2^exp, using the + // frame's single exponent set) map through the forward DCT-II + // into the AHT coefficients X(k, j) that the VQ/GAQ quantiser + // stack codes once per frame. + let aht_x: Option>> = if self.aht { + Some( + (0..nfchans) + .map(|ch| { + (0..ch_end_mant) + .map(|bin| { + let e = exps[ch][0][bin] as i32; + let scale = 2f32.powi(e); + let mut c = [0.0f32; 6]; + for (blk, cm) in c.iter_mut().enumerate() { + *cm = coeffs[ch][blk][bin] * scale; + } + dct_ii_6(&c) + }) + .collect() + }) + .collect(), + ) + } else { + None + }; + let aht_lfe_x: Option> = if self.aht && sub.lfeon { + Some( + (0..LFE_END_MANT) + .map(|bin| { + let e = exps[lfe_idx_in_exps][0][bin] as i32; + let scale = 2f32.powi(e); + let mut c = [0.0f32; 6]; + for (blk, cm) in c.iter_mut().enumerate() { + *cm = coeffs[nfchans][blk][bin] * scale; + } + dct_ii_6(&c) + }) + .collect(), + ) + } else { + None + }; + + // -------- Bit allocation -------- + let ba = BitAllocParams { + sdcycod: 2, + fdcycod: 1, + sgaincod: 1, + dbpbcod: 2, + floorcod: 4, + csnroffst: 15, + fsnroffst: 0, + fsnroffst_ch: [0u8; crate::audblk::MAX_FBW], + cplfsnroffst: 0, + lfefsnroffst: 0, + fgaincod: 4, + cplfgaincod: 4, + lfefgaincod: 4, + }; + // Coupling plan: the enhanced-coupling carrier region for the + // shared tuner / bap machinery, or the no-coupling default. + let cpl = match &ecpl_geom { + Some(g) => CouplingPlan::for_ecpl(g.begin_subbnd, g.end_subbnd, nfchans, g.necplbnd), + None => CouplingPlan::default(), + }; + let dba_plan = if std::env::var("EAC3_DISABLE_DBA").is_ok() { + crate::encoder::DbaPlan::default() + } else { + build_dba_plan( + &exps, + nfchans, + end_mant_ch.iter().copied().min().unwrap_or(ch_end_mant), + &cpl, + ) + }; + // When `snroffststr != 0`, the audblk carries per-block SNR + // offsets instead of the single frame-level pair in audfrm. Those + // extra header bits must be reserved out of the mantissa budget, + // or the bit-allocation tuner would fill the frame and the + // per-block offsets would push the syncframe past `frame_bytes`. + // Net extra bits vs the `snroffststr == 0` baseline: + // * audfrm drops `frmcsnroffst`(6) + `frmfsnroffst`(4) = 10. + // * each block adds an explicit `snroffste`(1) except block 0, + // plus `csnroffst`(6) plus the fine offsets. + // * snroffststr==1: one shared `blkfsnroffst`(4) per block. + // * snroffststr==2: `fsnroffst[ch]`(4·nfchans) + LFE(4) per + // block (no coupling slot — cplinu==0 throughout). + // When `snroffststr != 0` the audblk carries per-block SNR + // offsets instead of the single frame-level pair in audfrm. Those + // extra header bits must be reserved out of the mantissa budget, + // or the bit-allocation tuner would fill the frame and the + // per-block offsets would push the syncframe past `frame_bytes`. + // The `snroffststr == 0` default path reserves nothing, so its + // output is byte-identical to before this change. Net extra bits + // vs the baseline: + // * audfrm drops `frmcsnroffst`(6) + `frmfsnroffst`(4) = 10. + // * each block adds an explicit `snroffste`(1) except block 0, + // plus `csnroffst`(6) plus the fine offsets. + // * snroffststr==1: one shared `blkfsnroffst`(4) per block. + // * snroffststr==2: `fsnroffst[ch]`(4·nfchans) + LFE(4) per + // block (no coupling slot — cplinu==0 throughout). + // Both non-zero strategies reserve the SAME (worst-case, i.e. + // `snroffststr == 2`) overhead so that `1` and `2` tune against an + // identical mantissa budget — making their decoded PCM bit-exact + // to each other, which the round-trip test asserts. The `1` mode + // leaves a few reserved bits unused as extra padding; that does + // not change the bit allocation. + let snr_reserve_bits: u32 = if self.snroffststr == 0 { + 0 + } else { + let per_block_fine = 4 * nfchans as u32 + if sub.lfeon { 4 } else { 0 }; + let explicit_snroffste = (BLOCKS_PER_FRAME as u32) - 1; // blk 0 implicit + let per_block = BLOCKS_PER_FRAME as u32 * (6 + per_block_fine); + (per_block + explicit_snroffste).saturating_sub(10) + }; + // SPX header bits over the SPX-off baseline (which spends one + // bit per block on the disabled `spxinu`/`spxstre` slot). The + // reserve always assumes the worst case — an explicit + // `spxbndstrce == 1` band structure — so the default-banding + // and explicit-banding emissions tune against an identical + // mantissa budget and decode bit-identically (pinned by test). + let spx_reserve_bits: u32 = match &spx_geom { + None => 0, + Some(g) => { + // Block-0 strategy: chinspx (explicit unless mono) + + // spxstrtf(2) + spxbegf(3) + spxendf(3) + spxbndstrce(1) + // + explicit structure flags. spxinu itself replaces the + // baseline's 1-bit slot (no delta). + let chinspx = if sub.acmod == 0x1 { 0 } else { nfchans as u32 }; + let strat = chinspx + 2 + 3 + 3 + 1 + (g.end_subbnd - g.begin_subbnd - 1) as u32; + // Coordinates: spxblnd(5) + mstrspxco(2) + 6 bits/band. + let payload = 5 + 2 + 6 * g.nbnds as u32; + // Attenuation (audfrm): chinspxatten(1) + spxattencod(5) + // per fbw channel when signalled. + let atten = if self.spx.as_ref().is_some_and(|p| p.atten_code.is_some()) { + 6 * nfchans as u32 + } else { + 0 + }; + // blk 0: payload (spxcoe implicit); blk 3: 1 + payload; + // blks 1/2/4/5: 1-bit spxcoe each. Only channels in + // SPX carry coordinates (mixed chinspx). + let n_spx = in_spx.iter().filter(|&&v| v).count() as u32; + strat + atten + n_spx * (payload + (1 + payload) + 4) + } + }; + // AHT header bits over the AHT-off baseline: one chahtinu bit + // per fbw channel in audfrm (the 2-bit chgaqmod fields are + // accounted inside the AHT mantissa cost). + let aht_reserve_bits: u32 = if self.aht { + nfchans as u32 + u32::from(sub.lfeon) + 8 + } else { + 0 + }; + // Enhanced-coupling header bits over what the shared (base-AC-3 + // shaped) overhead model already counts once `cpl.in_use` is set. + // The model prices AC-3 standard-coupling syntax: strategy fields + // on BOTH anchor blocks, per-block `cplcoe` bits and one + // block-0 `mstrcplco + 8·nbnd` coordinate payload per coupled + // channel. True Annex E enhanced coupling emits its strategy on + // block 0 only but carries a much larger coordinate block — + // `ecplangleintrp` every block plus per-channel exist bits, + // 5-bit amplitudes, 6+3-bit angle/chaos pairs and a transient + // flag. Reserve the (worst-case: block-3 full refresh) true cost + // minus the modelled cost, with a small slop; over-reservation + // only pads the frame, under-reservation would overflow the + // hard bit-budget check below. + let ecpl_reserve_bits: u32 = match &ecpl_geom { + None => 0, + Some(g) => { + let nb = g.necplbnd as u32; + let nch = nfchans as u32; + let chincpl_bits = if sub.acmod == 0x2 { 0 } else { nch }; + let strategy = 1 + chincpl_bits + 4 + if g.spx_bounded { 0 } else { 4 } + 1; + // Coordinates: block 0 (implicit exist bits) + block 3 + // (explicit, worst-case refresh) + 4 reuse blocks, plus + // one ecplangleintrp bit per block. + let blk0 = 5 * nb + (nch - 1) * (5 * nb + 9 * nb + 1); + let blk3 = (1 + 5 * nb) + (nch - 1) * (2 + 5 * nb + 9 * nb + 1); + let reuse = 4 * (1 + 3 * (nch - 1)); + let true_coords = 6 + blk0 + blk3 + reuse; + // Modelled by `overhead_bits_for_ends` with cpl.in_use: + let modelled_strategy = 2 + * (nch + + 8 + + u32::from(sub.acmod == 0x2) + + (g.end_subbnd - g.begin_subbnd - 1) as u32); + let modelled_coords = 6 * nch + nch * (2 + 8 * nb); + (strategy + true_coords + 16).saturating_sub(modelled_strategy + modelled_coords) + } + }; + // Optional metadata words (compr / mixing / informational + // blocks / per-block dynrng) on top of the fixed layout. + let meta_reserve_bits: u32 = self + .meta + .reserve_bits(sub.acmod, sub.lfeon, sub.strmtyp == 0); + let tuner_frame_bytes = sub.frame_bytes.saturating_sub( + (snr_reserve_bits + + spx_reserve_bits + + aht_reserve_bits + + ecpl_reserve_bits + + meta_reserve_bits) + .div_ceil(8) as usize, + ); + // Pass chexpstr_plan so the overhead calculator accounts for + // D25/D45 exponent savings when sizing the mantissa budget. + let tuned_ba = if let Some(x) = &aht_x { + tune_snroffst_aht( + &ba, + &exps, + x, + aht_lfe_x.as_deref(), + ch_end_mant, + nfchans, + self.fscod, + tuner_frame_bytes, + &lfe_exp_strategies, + &chexpstr_plan, + &dba_plan, + sub.acmod, + sub.lfeon, + ) + } else { + tune_snroffst_with_plan_ends( + &ba, + &exps, + ch_end_mant, + Some(&end_mant_ch), + nfchans, + self.fscod, + tuner_frame_bytes, + &exp_strategies, + Some(&chexpstr_plan), + &cpl, + &dba_plan, + sub.acmod, + sub.lfeon, + ) + }; + let mut baps: Vec> = + vec![vec![[0u8; N_COEFFS]; BLOCKS_PER_FRAME]; nfchans + 2]; + let frame_ba = tuned_ba; + if !self.aht { + // AHT channels use hebap[] (computed at emission) instead + // of bap[]; only non-AHT frames need the fbw bap arrays. + for ch in 0..nfchans { + for blk in 0..BLOCKS_PER_FRAME { + compute_bap( + &exps[ch][blk], + end_mant_ch[ch], + self.fscod, + &frame_ba, + &mut baps[ch][blk], + Some((&dba_plan, ch)), + ); + } + } + } + if sub.lfeon && !self.aht { + // LFE bit allocation. The shared `compute_bap` is generic + // — pass the LFE exponents and the LFE-specific budget + // through a dedicated `BitAllocParams` snapshot whose + // fsnroffst_ch is irrelevant (LFE uses the lfefsnroffst). + // AHT frames use hebap[] at emission instead. + let lfe_ba = BitAllocParams { + fsnroffst: tuned_ba.lfefsnroffst, + fgaincod: tuned_ba.lfefgaincod, + ..frame_ba + }; + for blk in 0..BLOCKS_PER_FRAME { + compute_bap( + &exps[lfe_idx_in_exps][blk], + LFE_END_MANT, + self.fscod, + &lfe_ba, + &mut baps[lfe_idx_in_exps][blk], + None, + ); + } + } + if let Some(g) = &ecpl_geom { + // Enhanced-coupling carrier bap — the §7.2.2.4 coupling + // excitation path with the tuner's coupling fine offset + // (tied to the fbw fine offset) and the default fast gain, + // exactly what the decoder derives from `frmfsnroffst` + + // `fgaincode == 0` + `cplfleak = cplsleak = 0`. + let cpl_ba = BitAllocParams { + fsnroffst: frame_ba.cplfsnroffst, + fgaincod: frame_ba.cplfgaincod, + ..frame_ba + }; + let cpl_idx = nfchans; + for blk in 0..BLOCKS_PER_FRAME { + compute_bap_cpl( + &exps[cpl_idx][blk], + g.start_bin, + g.end_bin, + self.fscod, + &cpl_ba, + &mut baps[cpl_idx][blk], + Some((&dba_plan, crate::audblk::MAX_FBW)), + ); + } + } + // Enhanced-coupling coordinates (pass 2) — measured against the + // carrier the decoder will actually reconstruct: this frame's + // carrier run through the exponent + bap + mantissa quantiser + // (bap = 0 bins are true zeros — the coupling channel is never + // dithered), with the previous frame's carried QUANTISED last + // block at the frame head. Band-level carrier coding loss then + // folds into the transmitted amplitudes. + let ecpl_plan: Option = match (&ecpl_geom, &ecpl_carrier_plan) { + (Some(g), Some(cp)) => Some(plan_ecpl_coords( + g, + cp, + &exps[nfchans], + &baps[nfchans], + nfchans, + self.ecpl.as_ref().is_some_and(|p| p.chaos), + self.ecpl_carry.get(sub_idx).and_then(|c| c.as_ref()), + )), + _ => None, + }; + + // -------- Pack syncframe -------- + let mut bw = BitWriter::with_capacity(sub.frame_bytes); + + // syncinfo (§E.2.2.1) — just the 16-bit syncword. + bw.write_u32(0x0B77, 16); + + // bsi (§E.2.2.2) + bw.write_u32(sub.strmtyp as u32, 2); + bw.write_u32(sub.substreamid as u32, 3); + let frmsiz = (sub.frame_bytes / 2 - 1) as u32; + bw.write_u32(frmsiz, 11); + bw.write_u32(self.fscod as u32, 2); + // fscod != 0x3, so emit numblkscod (2 bits). 0x3 = 6 blocks. + bw.write_u32(0x3, 2); + bw.write_u32(sub.acmod as u32, 3); + bw.write_u32(u32::from(sub.lfeon), 1); + bw.write_u32(EAC3_BSID as u32, 5); + bw.write_u32(self.meta.dialnorm as u32, 5); + match self.meta.compr { + // §5.4.2.9-10 semantics — Table 7.30 heavy compression. + Some(c) => { + bw.write_u32(1, 1); // compre = 1 + bw.write_u32(c as u32, 8); + } + None => bw.write_u32(0, 1), // compre = 0 + } + // acmod != 0 → no dialnorm2/compr2e (we never produce 1+1). + // §E.2.2.2 / E.2.3.1.7-8: dependent substreams emit chanmape + // (and chanmap when chanmape=1) immediately after compre. + if sub.strmtyp == 1 { + if let Some(map) = sub.chanmap { + bw.write_u32(1, 1); // chanmape = 1 + bw.write_u32(map as u32, 16); // chanmap + } else { + bw.write_u32(0, 1); // chanmape = 0 + } + } + // §E.2.3.1.9-61 mixing metadata (independent substream only; + // a paired dependent substream keeps mixmdate = 0). + match (&self.meta.mixmd, sub.strmtyp) { + (Some(m), 0) => { + bw.write_u32(1, 1); // mixmdate = 1 + if sub.acmod > 0x2 { + bw.write_u32(m.dmixmod as u32, 2); + } + if (sub.acmod & 0x1) != 0 && sub.acmod > 0x2 { + bw.write_u32(m.ltrtcmixlev as u32, 3); + bw.write_u32(m.lorocmixlev as u32, 3); + } + if (sub.acmod & 0x4) != 0 { + bw.write_u32(m.ltrtsurmixlev as u32, 3); + bw.write_u32(m.lorosurmixlev as u32, 3); + } + if sub.lfeon { + match m.lfemixlevcod { + Some(l) => { + bw.write_u32(1, 1); // lfemixlevcode = 1 + bw.write_u32(l as u32, 5); + } + None => bw.write_u32(0, 1), + } + } + // strmtyp == 0 arm: pgmscle / extpgmscle / mixdef / + // (acmod < 2: paninfoe) / frmmixcfginfoe. + match m.pgmscl { + Some(p) => { + bw.write_u32(1, 1); + bw.write_u32(p as u32, 6); + } + None => bw.write_u32(0, 1), + } + match m.extpgmscl { + Some(p) => { + bw.write_u32(1, 1); + bw.write_u32(p as u32, 6); + } + None => bw.write_u32(0, 1), + } + bw.write_u32(0, 2); // mixdef = 0 (no further mixdata) + if sub.acmod < 0x2 { + bw.write_u32(0, 1); // paninfoe = 0 + } + bw.write_u32(0, 1); // frmmixcfginfoe = 0 + } + _ => bw.write_u32(0, 1), // mixmdate = 0 + } + // §E.2.3.1.62+ informational metadata (independent substream + // only). + match (&self.meta.infomd, sub.strmtyp) { + (Some(i), 0) => { + bw.write_u32(1, 1); // infomdate = 1 + bw.write_u32(i.bsmod as u32, 3); + bw.write_u32(u32::from(i.copyrightb), 1); + bw.write_u32(u32::from(i.origbs), 1); + if sub.acmod == 0x2 { + bw.write_u32(i.dsurmod as u32, 2); + bw.write_u32(i.dheadphonmod as u32, 2); + } + if sub.acmod >= 0x6 { + bw.write_u32(i.dsurexmod as u32, 2); + } + match i.audprod { + Some(a) => { + bw.write_u32(1, 1); // audprodie = 1 + bw.write_u32(a.mixlevel as u32, 5); + bw.write_u32(a.roomtyp as u32, 2); + bw.write_u32(u32::from(a.adconvtyp), 1); + } + None => bw.write_u32(0, 1), + } + // fscod < 0x3 always holds here (48/44.1/32 kHz). + bw.write_u32(u32::from(i.sourcefscod), 1); + // numblkscod == 0x3 → no convsync field. + } + _ => bw.write_u32(0, 1), // infomdate = 0 + } + // numblkscod == 0x3 → no convsync / blkid / frmsizecod fields. + bw.write_u32(0, 1); // addbsie = 0 + + // audfrm (§E.2.2.3 / §E.2.3.2) + bw.write_u32(1, 1); // expstre = 1 + bw.write_u32(u32::from(self.aht), 1); // ahte (§2.3.2.2) + bw.write_u32(self.snroffststr as u32, 2); // snroffststr + bw.write_u32(0, 1); // transproce = 0 + bw.write_u32(1, 1); // blkswe = 1 + bw.write_u32(1, 1); // dithflage = 1 + bw.write_u32(1, 1); // bamode = 1 + bw.write_u32(1, 1); // frmfgaincode = 1 + bw.write_u32(1, 1); // dbaflde = 1 + bw.write_u32(1, 1); // skipflde = 1 + let spx_atten: Option = self.spx.as_ref().and_then(|p| p.atten_code); + bw.write_u32(u32::from(spx_atten.is_some()), 1); // spxattene (§2.3.2.23) + if sub.acmod > 1 { + // cplinu[0] — 1 when enhanced coupling is on (the coupling + // strategy then rides block 0's implicit cplstre = 1 and is + // reused by every later block via cplstre[blk] = 0). + bw.write_u32(u32::from(ecpl_on), 1); + for _blk in 1..BLOCKS_PER_FRAME { + bw.write_u32(0, 1); // cplstre[blk] = 0 (strategy reuse) + } + } + // §E.1.2.3 / Table E1.3 — exponent strategy data. + // + // When `expstre == 1`, the per-block per-channel `chexpstr` + // codes (2 bits each) AND any per-block `cplexpstr` (when + // coupling is in use) live HERE in audfrm — NOT in audblk. + // The previous "round 2 fix" inverted this: it moved the bits + // into audblk because the spec text for §E.2.3.2.1 (`expstre`) + // says "the fields for the full exponent strategy shall be + // present in each audio block" — but Table E1.3 makes clear + // those fields are still emitted in audfrm, just indexed by + // block. The validator binary's parser consumes them in audfrm + // and rejects any frame that doesn't supply them, surfacing + // as the "new bit allocation info must be present in block 0" + // / "delta bit allocation strategy reserved" / "error in bit + // allocation" cascade once the parser misaligns. + // + // Coupling: cplinu==0 for every block of every substream we + // emit, so the `if (cplinu[blk] == 1) {cplexpstr[blk]}` branch + // never fires. We emit per-channel chexpstr from the adaptive + // D15/D25/D45 plan selected above (chexpstr_plan[ch][blk]). + for blk in 0..BLOCKS_PER_FRAME { + // §E.1.2.3: `cplexpstr[blk]` (2 bits) precedes the + // per-channel codes on every block where cplinu[blk] == 1. + // The carrier follows the frame-wide anchor cadence + // (D15 on blocks 0/3, REUSE elsewhere). + if ecpl_on { + bw.write_u32(exp_strategies[blk] as u32, 2); + } + for ch in 0..nfchans { + bw.write_u32(chexpstr_plan[ch][blk] as u32, 2); + } + } + // §E.1.2.3 — `lfeexpstr[blk]` (1 bit) per block when lfeon, + // OUTSIDE the `if (expstre)` gate (always present when LFE is + // on, regardless of expstre). LFE always D15 or REUSE. + if sub.lfeon { + for blk in 0..BLOCKS_PER_FRAME { + bw.write_u32(lfe_exp_strategies[blk] as u32, 1); + } + } + // §E.1.2.3 — converter exponent strategy data. strmtyp == 0x0 + // (independent substream) and numblkscod == 0x3 ⇒ convexpstre + // implicit = 1, followed by per-channel convexpstr (5 bits each). + if sub.strmtyp == 0 { + for _ in 0..nfchans { + bw.write_u32(0, 5); // convexpstr = 0 (REUSE codeword) + } + } + // §3.4.2 / Table E1.3 AHT in-use flags. Presence is gated by + // the decoder-derived regs counts: no cplahtinu (coupling is + // never in use, so ncplblks == 0), one chahtinu[ch] bit per + // fbw channel (every channel's plan is a single block-0 + // anchor → nchregs[ch] == 1), and one lfeahtinu bit (the AHT + // LFE plan is a single D15 anchor → nlferegs == 1). + if self.aht { + for _ in 0..nfchans { + bw.write_u32(1, 1); // chahtinu[ch] = 1 + } + if sub.lfeon { + bw.write_u32(1, 1); // lfeahtinu = 1 (nlferegs == 1) + } + } + // snroffststr == 0 ⇒ frame-level (frmcsnroffst, frmfsnroffst) in + // audfrm; snroffststr ∈ {1, 2} ⇒ no frame-level fields here (the + // offsets are carried per-block in each audblk instead). + if self.snroffststr == 0 { + bw.write_u32(tuned_ba.csnroffst as u32, 6); // frmcsnroffst + bw.write_u32(tuned_ba.fsnroffst as u32, 4); // frmfsnroffst + } + // §2.3.2.24-25 — spectral extension attenuation data: one + // chinspxatten[ch] bit per fbw channel, + spxattencod[ch] + // (5 bits) when set. Emitted between the SNR-offset tail and + // blkstrtinfoe per Table E1.3. Channels in SPX carry the + // (shared) code; non-SPX channels signal chinspxatten = 0. + if let Some(code) = spx_atten { + for ch in 0..nfchans { + if in_spx[ch] { + bw.write_u32(1, 1); // chinspxatten[ch] = 1 + bw.write_u32(code as u32, 5); // spxattencod[ch] + } else { + bw.write_u32(0, 1); // chinspxatten[ch] = 0 + } + } + } + bw.write_u32(0, 1); // blkstrtinfoe = 0 + + // -------- audio blocks -------- + for blk in 0..BLOCKS_PER_FRAME { + for ch in 0..nfchans { + bw.write_u32(blksw[ch][blk] as u32, 1); + } + for _ in 0..nfchans { + // AHT channels reconstruct every bin from the front- + // loaded coefficient cache — hebap==0 bins are true + // zeros, so signal dithflag=0 to keep spec-strict + // decoders from substituting dither there. + bw.write_u32(u32::from(!self.aht), 1); // dithflag + } + // §5.4.3.3-4 dynrnge + dynrng: when metadata configures a + // dynamic-range word it is transmitted in EVERY block. + match self.meta.dynrng { + Some(w) => { + bw.write_u32(1, 1); + bw.write_u32(w as u32, 8); + } + None => bw.write_u32(0, 1), // dynrnge = 0 + } + + // SPX strategy (§E.2.3.3.1-8). Block 0 has implicit + // `spxstre = 1` and emits `spxinu` directly; later blocks + // emit `spxstre = 0` (strategy reuse) — the geometry is + // frame-constant. + match (&spx_geom, blk) { + (Some(g), 0) => { + let p = self.spx.as_ref().expect("spx params when geom is set"); + bw.write_u32(1, 1); // spxinu = 1 + // §E.2.3.3.3 chinspx[ch] — implicit for mono + // (acmod == 0x1); explicit per fbw channel + // otherwise (mixed membership allowed). + if sub.acmod != 0x1 { + for &member in in_spx.iter().take(nfchans) { + bw.write_u32(u32::from(member), 1); + } + } + bw.write_u32(spx_strtf_emit as u32, 2); // §E.2.3.3.4 + bw.write_u32(p.spxbegf as u32, 3); // §E.2.3.3.5 + bw.write_u32(p.spxendf as u32, 3); // §E.2.3.3.6 + if p.explicit_band_structure { + // §E.2.3.3.7-8 — explicit structure carrying the + // same content as the Table E2.11 default. + bw.write_u32(1, 1); + for bnd in (g.begin_subbnd + 1)..g.end_subbnd { + bw.write_u32(u32::from(g.bndstrc[bnd]), 1); + } + } else { + bw.write_u32(0, 1); // spxbndstrce = 0 → default banding + } + } + (Some(_), _) => bw.write_u32(0, 1), // spxstre = 0 (reuse) + (None, 0) => bw.write_u32(0, 1), // spxinu = 0 + (None, _) => bw.write_u32(0, 1), // spxstre = 0 + } + // SPX coordinates (§E.2.3.3.9-13). `spxcoe` is implicit-1 + // on a channel's first SPX block (block 0 here); explicit + // thereafter. Refresh on the anchor blocks, reuse between — + // and when a channel's block-3 refresh quantises to exactly + // the block-0 codes (stationary spectrum), thrift the + // payload with `spxcoe = 0` instead: the decoder's reuse + // path reconstructs identical coordinates, so the decode is + // unchanged and the ~7 + 6·nbnds bits return to padding. + if let (Some(g), Some(coords)) = (&spx_geom, &spx_coords) { + let refresh = SPX_REFRESH_BLOCKS.contains(&blk); + let span_idx = usize::from(blk >= SPX_REFRESH_BLOCKS[1]); + let spxblnd = self.spx.as_ref().expect("spx params").spxblnd; + for ch in 0..nfchans { + if !in_spx[ch] { + // §E.2.3.3.9: spxcoe exists only for channels + // with chinspx[ch] == 1. + continue; + } + let reuse_prior = !refresh || (span_idx == 1 && coords[ch][1] == coords[ch][0]); + if blk != 0 { + bw.write_u32(u32::from(!reuse_prior), 1); // spxcoe[ch] + } + if blk == 0 || !reuse_prior { + let (mstr, codes) = &coords[ch][span_idx]; + bw.write_u32(spxblnd as u32, 5); // §E.2.3.3.10 + bw.write_u32(*mstr as u32, 2); // §E.2.3.3.11 + for bnd in 0..g.nbnds { + bw.write_u32(codes[bnd].0 as u32, 4); // spxcoexp + bw.write_u32(codes[bnd].1 as u32, 2); // spxcomant + } + } + } + } + + // Coupling strategy (§E.2.3.3.14-19). Only with enhanced + // coupling: block 0 (implicit cplstre = 1, cplinu = 1 from + // audfrm) carries `ecplinu` + `chincpl[ch]` (implicit for + // 2/0) + the begin/end frequency codes + `ecplbndstrce = 0` + // (Table E2.14 default banding). Later blocks have + // cplstre = 0 in audfrm, so no strategy bits at all. + // Without coupling: cplinu[0] = 0 in audfrm — no audblk + // bits either way. + if let (Some(g), 0) = (&ecpl_geom, blk) { + bw.write_u32(1, 1); // ecplinu = 1 + if sub.acmod != 0x2 { + for _ch in 0..nfchans { + bw.write_u32(1, 1); // chincpl[ch] = 1 + } + } + bw.write_u32(g.ecplbegf as u32, 4); // §E.2.3.3.16 + if !g.spx_bounded { + // §E.2.3.3.17 — transmitted only when SPX is off; + // with SPX co-active the end sub-band is derived + // from spxbegf and the field is absent. + bw.write_u32(g.ecplendf as u32, 4); + } + bw.write_u32(0, 1); // ecplbndstrce = 0 → default banding + } + + // Enhanced-coupling coordinates (§E.2.3.3.20-26) — present + // on every block where cplinu[blk] == 1. Coordinates + // refresh on the exponent anchor blocks: block 0 has + // implicit exist bits (`firstcplcos`), block 3 re-emits + // them explicitly unless the span-1 codes quantised + // identically to span 0 (thrift: `ecplparam1e = 0` — the + // decoder's §2.3.3.21-22 reuse path reconstructs the same + // coordinates), and blocks 1/2/4/5 always reuse. Chaos is + // 0 (no random de-correlation) and `ecpltrans` is 0. + if let Some(plan) = &ecpl_plan { + bw.write_u32(0, 1); // ecplangleintrp = 0 + for ch in 0..nfchans { + let is_first = ch == 0; + let span = usize::from(blk >= 3); + let (p1, p2) = match blk { + 0 => (true, !is_first), + 3 => (!plan.reuse1_amp[ch], !is_first && !plan.reuse1_ang[ch]), + _ => (false, false), + }; + if blk != 0 { + bw.write_u32(u32::from(p1), 1); // ecplparam1e[ch] + if !is_first { + bw.write_u32(u32::from(p2), 1); // ecplparam2e[ch] + } + } + if p1 { + for &code in &plan.amp[ch][span] { + bw.write_u32(code as u32, 5); // ecplamp + } + } + if p2 { + for (bnd, &code) in plan.angle[ch][span].iter().enumerate() { + bw.write_u32(code as u32, 6); // ecplangle + let cha = plan.chaos[ch][span].get(bnd).copied().unwrap_or(0); + bw.write_u32(cha as u32, 3); // ecplchaos + } + } + if !is_first { + bw.write_u32(0, 1); // ecpltrans[ch] = 0 + } + } + } + + // Rematrixing — only acmod==2. The flag-field size folds in + // SPX per §E.3.3.2 (spxbegf < 2 → 3 bands, else 4; 4 when + // SPX is off since coupling is never in use here). Round-1 + // keeps rematrixing disabled (all flags 0). + if sub.acmod == 2 { + let nrematbd = remat_band_count_spx( + ecpl_on, + 0, + ecpl_on, + ecpl_geom.as_ref().map_or(0, |g| g.ecplbegf), + spx_geom.is_some(), + spx_geom.as_ref().map_or(0, |g| g.begin_subbnd), + ); + if blk == 0 { + for _bnd in 0..nrematbd { + bw.write_u32(0, 1); + } + } else { + bw.write_u32(0, 1); // rematstr = 0 + } + } + + let lfe_exp_strategy = lfe_exp_strategies[blk]; + // §E.1.2.4 / Table E1.4 — `chexpstr[blk][ch]` and + // `lfeexpstr[blk]` were already emitted in audfrm above. + // audblk only carries the **bandwidth code + exponent + // payload** that follows from those strategies, gated by + // the `chexpstr[blk][ch] != reuse` test. + // + // §E.1.2.4 chbwcod[ch] — 6 bits per fbw channel whose + // strategy this block is non-REUSE AND that channel is + // neither in coupling nor in spectral extension + // (`if((!chincpl[ch]) && (!chinspx[ch])) {chbwcod[ch]}`). + // SPX channels derive their bandwidth from the SPX begin + // frequency instead (§E.3.3.3); with mixed chinspx the + // full-bandwidth channels still carry their chbwcod. + for ch in 0..nfchans { + if !ecpl_on && !in_spx[ch] && chexpstr_plan[ch][blk] != 0 { + bw.write_u32(chbwcod as u32, 6); + } + } + // §E.1.3.4.4 coupling-channel exponents — cplabsexp (4 bits) + // + D15 groups over [ecplstartmant, ecplendmant), emitted on + // the blocks whose audfrm `cplexpstr[blk]` is non-REUSE + // (anchors 0 and 3). No gainrng for the coupling channel. + if let Some(g) = &ecpl_geom { + if exp_strategies[blk] == 1 { + write_exponents_cpl(&mut bw, &exps[nfchans][blk], g.start_bin, g.end_bin); + } + } + // §E.1.2.4 fbw exponents. After each channel's grouped + // exponents, the spec emits `gainrng[ch]` (2 bits) — same as + // base AC-3 §5.4.2.20. Emit exponents using the per-channel + // strategy (D15/D25/D45 from chexpstr_plan). + for ch in 0..nfchans { + let strat = chexpstr_plan[ch][blk]; + if strat != 0 { + let grpsize = match strat { + 1 => 1usize, + 2 => 2usize, + _ => 4usize, + }; + write_exponents_grouped(&mut bw, &exps[ch][blk], end_mant_ch[ch], grpsize); + bw.write_u32(0, 2); // gainrng[ch] = 0 + } + } + // LFE exponents — D15 only, no chbwcod (LFE has fixed bw), + // and no gainrng (only fbw channels carry gainrng). + if sub.lfeon && lfe_exp_strategy == 1 { + write_exponents_grouped(&mut bw, &exps[lfe_idx_in_exps][blk], LFE_END_MANT, 1); + } + + let baie = blk == 0; + bw.write_u32(baie as u32, 1); + if baie { + bw.write_u32(tuned_ba.sdcycod as u32, 2); + bw.write_u32(tuned_ba.fdcycod as u32, 2); + bw.write_u32(tuned_ba.sgaincod as u32, 2); + bw.write_u32(tuned_ba.dbpbcod as u32, 2); + bw.write_u32(tuned_ba.floorcod as u32, 3); + } + + // §2.3.3.27 — per-block SNR offsets when snroffststr ∈ {1,2}. + // (The snroffststr == 0 path carries them once in audfrm.) + // Block 0 emits implicitly (snroffste == 1); later blocks emit + // an explicit `snroffste = 1` so every block carries the same + // value (the decode then matches the frame-level reference). + // cplinu == 0 for every block, so the coupling slot is skipped. + if self.snroffststr != 0 { + if blk != 0 { + bw.write_u32(1, 1); // snroffste = 1 + } + bw.write_u32(tuned_ba.csnroffst as u32, 6); // csnroffst + if self.snroffststr == 1 { + bw.write_u32(tuned_ba.fsnroffst as u32, 4); // blkfsnroffst + } else { + // snroffststr == 2 — per-channel fine offsets (no cpl + // slot: cplinu == 0). LFE slot when present. + for _ in 0..nfchans { + bw.write_u32(tuned_ba.fsnroffst as u32, 4); // fsnroffst[ch] + } + if sub.lfeon { + bw.write_u32(tuned_ba.lfefsnroffst as u32, 4); // lfefsnroffst + } + } + } + + // §E.1.3.5.4 frmfgaincode==1 ⇒ per-block `fgaincode` flag. + bw.write_u32(0, 1); // fgaincode = 0 (no per-channel override) + + // §E.1.2.4 convsnroffste — UNCONDITIONAL when strmtyp == 0 + // (independent substream). Per Table E1.4 line 7965, this + // sits between the fgaincode block and the cplleak block, + // gated only by `if (strmtyp == 0x0)` — NOT inside the + // `if (snroffste)` branch as the previous comment claimed. + // We never emit `convsnroffst`, so write the gating bit at + // 0. + if sub.strmtyp == 0 { + bw.write_u32(0, 1); // convsnroffste = 0 + } + // §E.1.3.5.4 cplleak — only when cplinu[blk]. The first + // coupling block of the frame has `cplleake = 1` implicit + // (no gate bit): emit cplfleak = cplsleak = 0 directly, + // matching the 768 + 0 leak init `compute_bap_cpl` assumes. + // Later blocks emit an explicit `cplleake = 0` (reuse). + if ecpl_on { + if blk == 0 { + bw.write_u32(0, 3); // cplfleak = 0 + bw.write_u32(0, 3); // cplsleak = 0 + } else { + bw.write_u32(0, 1); // cplleake = 0 + } + } + + let any_fbw_dba = (0..nfchans).any(|c| dba_plan.nseg[c] > 0); + if blk == 0 && any_fbw_dba { + bw.write_u32(1, 1); // deltbaie = 1 + if ecpl_on { + // cpldeltbae precedes the per-channel codes when + // cplinu[blk] == 1; 2 = "perform no delta bit + // allocation" for the coupling channel. + bw.write_u32(2, 2); + } + for ch in 0..nfchans { + let code = if dba_plan.nseg[ch] > 0 { 1 } else { 2 }; + bw.write_u32(code as u32, 2); + } + for ch in 0..nfchans { + if dba_plan.nseg[ch] > 0 { + let nseg = dba_plan.nseg[ch] as u32; + bw.write_u32(nseg - 1, 3); + for seg in 0..nseg as usize { + bw.write_u32(dba_plan.offst[ch][seg] as u32, 5); + bw.write_u32(dba_plan.len[ch][seg] as u32, 4); + bw.write_u32(dba_plan.ba[ch][seg] as u32, 3); + } + } + } + } else { + bw.write_u32(0, 1); // deltbaie = 0 + } + + bw.write_u32(0, 1); // skiple = 0 + + // -------- Mantissas -------- + // Order per AC-3 §5.4.3.49: fbw channels in ascending index, + // then (when present) the LFE pseudo-channel. + // + // AHT frames front-load every fbw channel's mantissas in + // block 0 as a self-contained chgaqmod + gain-word + + // VQ/GAQ codeword stream (§3.4.4); blocks 1..5 carry no + // fbw mantissa bits at all (the decoder replays its + // 6-block coefficient cache). The LFE stays on the + // standard per-block path either way, and its bap-1/2/4 + // grouping is self-contained because AHT channels + // contribute nothing to the shared grouping buffers. + let mut codes: Vec<(u8, u32)> = Vec::with_capacity(nfchans * ch_end_mant + 4); + if let Some(x) = &aht_x { + if blk == 0 { + for ch in 0..nfchans { + // §3.4.3.1 hebap[] with the same psd/mask/ + // snroffset/dba state the decoder derives for + // this channel's block-0 bit allocation. + let mut hebap = [0u8; N_COEFFS]; + compute_hebap( + &exps[ch][0], + ch_end_mant, + self.fscod, + &tuned_ba, + &mut hebap, + Some((&dba_plan, ch)), + ); + let plan = plan_aht_channel(&hebap[..ch_end_mant], 0, ch_end_mant, &x[ch]); + write_aht_channel( + &mut bw, + &plan, + &hebap[..ch_end_mant], + 0, + ch_end_mant, + &x[ch], + ); + } + if let Some(lx) = &aht_lfe_x { + // LFE-AHT front-loaded block (§3.4.2 lfeahtinu): + // hebap masking uses the LFE fine-SNR offset and + // fast gain in place of the per-channel values. + let lfe_ba = BitAllocParams { + fsnroffst: tuned_ba.lfefsnroffst, + fgaincod: tuned_ba.lfefgaincod, + ..tuned_ba + }; + let mut hebap = [0u8; N_COEFFS]; + compute_hebap( + &exps[lfe_idx_in_exps][0], + LFE_END_MANT, + self.fscod, + &lfe_ba, + &mut hebap, + None, + ); + let plan = plan_aht_channel(&hebap[..LFE_END_MANT], 0, LFE_END_MANT, lx); + write_aht_channel( + &mut bw, + &plan, + &hebap[..LFE_END_MANT], + 0, + LFE_END_MANT, + lx, + ); + } + } + } else { + for ch in 0..nfchans { + for bin in 0..end_mant_ch[ch] { + let bap = baps[ch][blk][bin]; + if bap == 0 { + continue; + } + let e = exps[ch][blk][bin] as i32; + let mant = quantise_mantissa(coeffs[ch][blk][bin], e, bap); + codes.push((bap, mant)); + } + // §5.4.3.49 order: the coupling-channel mantissas + // follow the FIRST coupled channel's own mantissas + // (ch 0 — every fbw channel is coupled here), then + // the remaining channels and the LFE. The grouped + // bap-1/2/4 buffers are shared across the whole + // walk, which the single `codes` stream preserves. + if let (Some(g), Some(plan), 0) = (&ecpl_geom, &ecpl_carrier_plan, ch) { + for bin in g.start_bin..g.end_bin { + let bap = baps[nfchans][blk][bin]; + if bap == 0 { + continue; + } + let e = exps[nfchans][blk][bin] as i32; + let mant = quantise_mantissa(plan.carrier[blk][bin], e, bap); + codes.push((bap, mant)); + } + } + } + } + if sub.lfeon && !self.aht { + for bin in 0..LFE_END_MANT { + let bap = baps[lfe_idx_in_exps][blk][bin]; + if bap == 0 { + continue; + } + let e = exps[lfe_idx_in_exps][blk][bin] as i32; + let mant = quantise_mantissa(coeffs[nfchans][blk][bin], e, bap); + codes.push((bap, mant)); + } + } + write_mantissa_stream(&mut bw, &codes); + } + + // Thread the cross-frame enhanced-coupling analysis carry: the + // next frame's block-0 carrier (and per-channel analysis) uses + // this frame's last block as its "previous block" (§E.3.5.5.1), + // exactly as the decoder's `EcplState::prev_frame_last_mant` + // does on its side. + if sub_idx < self.ecpl_carry.len() { + self.ecpl_carry[sub_idx] = match (&ecpl_carrier_plan, &ecpl_plan) { + (Some(cp), Some(plan)) => Some(EcplCarry { + carrier: plan.carrier_hat_last, + channels: (0..nfchans) + .map(|ch| cp.restricted[ch][BLOCKS_PER_FRAME - 1]) + .collect(), + }), + _ => None, + }; + } + + // auxdata + errorcheck. + let target_bits = (sub.frame_bytes * 8) as u64; + let used_bits = bw.bit_position(); + let errorcheck_bits = 17u64; + if used_bits + errorcheck_bits > target_bits { + return Err(Error::other(format!( + "eac3 encoder: bit budget overflow ({} bits used, frame {} bits, strmtyp={}, acmod={}, lfeon={})", + used_bits, target_bits, sub.strmtyp, sub.acmod, sub.lfeon + ))); + } + let pad_bits = target_bits - used_bits - errorcheck_bits; + let mut left = pad_bits; + while left >= 32 { + bw.write_u32(0, 32); + left -= 32; + } + if left > 0 { + bw.write_u32(0, left as u32); + } + bw.write_u32(0, 1); // encinfo = 0 + bw.write_u32(0, 16); // crc2 placeholder + let mut frame = bw.into_bytes(); + debug_assert_eq!(frame.len(), sub.frame_bytes); + + // crc2 in augmented form per §7.10.1 (Annex E §E.1.2 inherits + // the same residue check). Shift the body + 16 trailing zero + // bits through the LFSR; the resulting register value goes in + // the crc2 field so a spec-strict decoder's residue check + // `ac3_crc_update(0, post_syncword) == 0` succeeds. E-AC-3 + // syncframes have no crc1 (§E.1.2 elides it), so the running + // CRC starts at byte 2 (post-syncword). + let body_residue = ac3_crc_update(0, &frame[2..(sub.frame_bytes - 2)]); + let crc2_val = ac3_crc_update(body_residue, &[0u8, 0u8]); + let n = sub.frame_bytes; + frame[n - 2] = (crc2_val >> 8) as u8; + frame[n - 1] = (crc2_val & 0xFF) as u8; + debug_assert_eq!( + ac3_crc_update(0, &frame[2..sub.frame_bytes]), + 0, + "E-AC-3 crc2 emit produced a non-zero post-syncword residue" + ); + Ok(frame) + } +} + +/// Phase A of the enhanced-coupling encode (§E.3.5.5, encode +/// direction): the carrier MDCT buffers + the per-channel +/// region-restricted analysis inputs. Built before the SNR tuner so +/// the carrier's exponents ride the shared coupling-channel +/// accounting; the coordinates ([`plan_ecpl_coords`]) wait until the +/// bit allocation is final. +struct EcplCarrierPlan { + /// Region-restricted carrier MDCT per block (what the coupling + /// pseudo-channel codes). + carrier: Vec<[f32; 256]>, + /// Region-restricted per-channel MDCT buffers + /// (`restricted[ch][blk]`) — the §E.3.5.5.1 analysis inputs for the + /// coordinate targets. + restricted: Vec>, +} + +/// Phase B: the quantised per-span coordinate codes. +struct EcplCoordPlan { + /// Quantised Table E3.10 amplitude codes: `amp[ch][span][bnd]` + /// (span 0 = blocks 0..3, span 1 = blocks 3..6). + amp: Vec<[Vec; 2]>, + /// Quantised Table E3.11 angle codes: `angle[ch][span][bnd]`. + /// Empty for the first coupled channel (spec-fixed to 0, never + /// transmitted). + angle: Vec<[Vec; 2]>, + /// Table E3.12 chaos codes: `chaos[ch][span][bnd]` (all zero when + /// the coherence-driven chaos signalling is off). Empty for the + /// first coupled channel. + chaos: Vec<[Vec; 2]>, + /// Per channel: span 1's amplitude codes quantised identically to + /// span 0's — block 3 emits `ecplparam1e = 0` (reuse thrift). + reuse1_amp: Vec, + /// Per channel: span 1's angle codes match span 0's — block 3 + /// emits `ecplparam2e = 0`. + reuse1_ang: Vec, + /// Last block's QUANTISED carrier (the decoder-faithful buffer the + /// next frame's block-0 analysis uses as its "previous block"). + carrier_hat_last: [f32; 256], +} + +/// Carrier-headroom margin: the carrier is scaled ~3 dB above the +/// loudest coupled channel per band so the Table E3.10 amplitude +/// ceiling of 1.0 can absorb band-level carrier *coding* loss (bap = 0 +/// bins reconstruct as true zeros; the amplitude coordinate re-scales +/// what survives back to the channel's band energy, but only downward +/// from 1.0). +const ECPL_CARRIER_MARGIN: f32 = std::f32::consts::SQRT_2; + +/// Build the enhanced-coupling carrier for one frame: the first +/// coupled channel's MDCT restricted to the active region, scaled per +/// band (per coordinate span) so the loudest coupled channel's +/// amplitude coordinate lands ~3 dB under the Table E3.10 ceiling +/// ([`carrier_band_gains`] × [`ECPL_CARRIER_MARGIN`]). Using channel +/// 0's own coefficients keeps the carrier's phase locked to the first +/// coupled channel, whose angle is spec-fixed to 0 and never +/// transmitted. +fn build_ecpl_carrier( + geom: &EcplGeometry, + coeffs: &[Vec<[f32; N_COEFFS]>], + nfchans: usize, +) -> EcplCarrierPlan { + const SPAN: usize = 3; + let mut gains: [Vec; 2] = [Vec::new(), Vec::new()]; + for (span, g) in gains.iter_mut().enumerate() { + let energies: Vec> = (0..nfchans) + .map(|ch| { + let blocks: Vec<&[f32; N_COEFFS]> = (span * SPAN..(span + 1) * SPAN) + .map(|blk| &coeffs[ch][blk]) + .collect(); + band_mdct_energies(&blocks, geom) + }) + .collect(); + *g = carrier_band_gains(&energies, geom.necplbnd); + for v in g.iter_mut() { + *v = (*v * ECPL_CARRIER_MARGIN).min(32.0); + } + } + let carrier: Vec<[f32; 256]> = (0..BLOCKS_PER_FRAME) + .map(|blk| build_carrier_block(&coeffs[0][blk], &gains[blk / SPAN], geom)) + .collect(); + let restricted: Vec> = (0..nfchans) + .map(|ch| { + (0..BLOCKS_PER_FRAME) + .map(|blk| region_restrict(&coeffs[ch][blk], geom)) + .collect() + }) + .collect(); + EcplCarrierPlan { + carrier, + restricted, + } +} + +/// Measure + quantise the enhanced-coupling coordinates against the +/// decoder-faithful carrier. +/// +/// Per block, `Z = reconstruct_carrier(prev, curr, next)` over the +/// QUANTISED carrier (each bin run through the final exponent + bap +/// mantissa quantiser via `quantise_reconstruct`; bap = 0 bins are true +/// zeros — the coupling channel is never dithered), with the previous +/// frame's carried quantised last block at the frame head and a zero +/// spectrum after the frame tail (the decoder's streaming edge). The +/// same analysis applied to each coupled channel's region-restricted +/// (unquantised — they are the *targets*) MDCT gives `X`; band +/// statistics accumulate over each 3-block coordinate span and +/// quantise through [`quantise_amp`] / [`quantise_angle`]. Measuring +/// against the quantised carrier folds band-level carrier coding loss +/// into the transmitted amplitudes. +fn plan_ecpl_coords( + geom: &EcplGeometry, + cp: &EcplCarrierPlan, + cpl_exps: &[[u8; N_COEFFS]], + cpl_baps: &[[u8; N_COEFFS]], + nfchans: usize, + chaos_enabled: bool, + carry: Option<&EcplCarry>, +) -> EcplCoordPlan { + const SPAN: usize = 3; + let zeros = [0.0f32; 256]; + + // Decoder-faithful (quantised) carrier per block. + let carrier_hat: Vec<[f32; 256]> = (0..BLOCKS_PER_FRAME) + .map(|blk| { + let mut out = [0.0f32; 256]; + for bin in geom.start_bin..geom.end_bin.min(256) { + out[bin] = crate::encoder::quantise_reconstruct( + cp.carrier[blk][bin], + cpl_exps[blk][bin] as i32, + cpl_baps[blk][bin], + ); + } + out + }) + .collect(); + + let carry_carrier = carry.map_or(&zeros, |c| &c.carrier); + let z: Vec<_> = (0..BLOCKS_PER_FRAME) + .map(|blk| { + let prev = if blk == 0 { + carry_carrier + } else { + &carrier_hat[blk - 1] + }; + let next = if blk + 1 < BLOCKS_PER_FRAME { + &carrier_hat[blk + 1] + } else { + &zeros + }; + reconstruct_carrier(prev, &carrier_hat[blk], next) + }) + .collect(); + + let mut amp: Vec<[Vec; 2]> = Vec::with_capacity(nfchans); + let mut angle: Vec<[Vec; 2]> = Vec::with_capacity(nfchans); + let mut chaos: Vec<[Vec; 2]> = Vec::with_capacity(nfchans); + let mut reuse1_amp = vec![false; nfchans]; + let mut reuse1_ang = vec![false; nfchans]; + for ch in 0..nfchans { + let carry_ch = carry.and_then(|c| c.channels.get(ch)).unwrap_or(&zeros); + let mut amp_spans: [Vec; 2] = [Vec::new(), Vec::new()]; + let mut ang_spans: [Vec; 2] = [Vec::new(), Vec::new()]; + let mut cha_spans: [Vec; 2] = [Vec::new(), Vec::new()]; + for span in 0..2 { + let mut stats = vec![BandStats::default(); geom.necplbnd]; + for blk in span * SPAN..(span + 1) * SPAN { + let prev = if blk == 0 { + carry_ch + } else { + &cp.restricted[ch][blk - 1] + }; + let next = if blk + 1 < BLOCKS_PER_FRAME { + &cp.restricted[ch][blk + 1] + } else { + &zeros + }; + let x = reconstruct_carrier(prev, &cp.restricted[ch][blk], next); + band_cross_stats(&x, &z[blk], geom, &mut stats); + } + // Per band: chaos from the measured coherence (non-first + // channels only — the first coupled channel's chaos is + // spec-fixed to 0), then the amplitude pre-divided by the + // decoder's §E.3.5.5.2 modification factor. If the + // pre-compensated amplitude would exceed the Table E3.10 + // ceiling of 1.0, back the chaos off until it fits — + // preserving band ENERGY takes precedence over restoring + // de-correlation. + let mut amps = Vec::with_capacity(geom.necplbnd); + let mut chas = Vec::with_capacity(geom.necplbnd); + for st in &stats { + let mut code = if chaos_enabled && ch != 0 { + chaos_code_for(st.coherence()) + } else { + 0 + }; + let measured = st.amp(); + while code > 0 && measured / chaos_amp_factor(code) > 1.0 { + code -= 1; + } + amps.push(quantise_amp(measured / chaos_amp_factor(code))); + chas.push(code); + } + amp_spans[span] = amps; + if ch != 0 { + ang_spans[span] = stats + .iter() + .map(|st| quantise_angle(st.angle_units())) + .collect(); + cha_spans[span] = chas; + } + } + reuse1_amp[ch] = amp_spans[1] == amp_spans[0]; + reuse1_ang[ch] = ch == 0 || (ang_spans[1] == ang_spans[0] && cha_spans[1] == cha_spans[0]); + amp.push(amp_spans); + angle.push(ang_spans); + chaos.push(cha_spans); + } + + EcplCoordPlan { + amp, + angle, + chaos, + reuse1_amp, + reuse1_ang, + carrier_hat_last: carrier_hat[BLOCKS_PER_FRAME - 1], + } +} + +/// SNR-offset tuner for AHT frames. The fbw mantissa cost is the +/// exact §3.4.4 payload (hebap-driven VQ/GAQ codewords + gain words + +/// chgaqmod, front-loaded once per frame) instead of six per-block bap +/// payloads; the LFE keeps the standard bap cost. `hebap[]` — and +/// therefore the cost — is monotone non-decreasing in the combined +/// SNR offset, so the largest offset that fits the budget is found by +/// binary search over `csnroffst·16 + fsnroffst`. +#[allow(clippy::too_many_arguments)] +fn tune_snroffst_aht( + ba: &BitAllocParams, + exps: &[Vec<[u8; N_COEFFS]>], + aht_x: &[Vec<[f32; 6]>], + aht_lfe_x: Option<&[[f32; 6]]>, + end: usize, + nchan: usize, + fscod: u8, + frame_bytes: usize, + exp_strategies: &[u8; BLOCKS_PER_FRAME], + chexpstr_plan: &[[u8; BLOCKS_PER_FRAME]], + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> BitAllocParams { + let cpl = CouplingPlan::default(); + let overhead = overhead_bits_for( + exp_strategies, + Some(chexpstr_plan), + end, + nchan, + &cpl, + dba, + acmod, + lfeon, + ) + 32; + let total_bits = (frame_bytes * 8) as u32; + let mut best = *ba; + best.csnroffst = 0; + best.fsnroffst = 0; + best.lfefsnroffst = 0; + if overhead >= total_bits { + return best; + } + let budget = total_bits - overhead; + + let used_at = |combined: i32| -> u32 { + let mut cand = *ba; + cand.csnroffst = (combined / 16) as u8; + cand.fsnroffst = (combined % 16) as u8; + cand.lfefsnroffst = cand.fsnroffst; + let mut used = 0u32; + for ch in 0..nchan { + let mut hebap = [0u8; N_COEFFS]; + compute_hebap(&exps[ch][0], end, fscod, &cand, &mut hebap, Some((dba, ch))); + used += plan_aht_channel(&hebap[..end], 0, end, &aht_x[ch]).total_bits(); + } + if lfeon { + let lfe_idx = nchan + 1; + let mut lfe_ba = cand; + lfe_ba.fsnroffst = cand.lfefsnroffst; + lfe_ba.fgaincod = cand.lfefgaincod; + if let Some(lx) = aht_lfe_x { + // LFE-AHT: exact front-loaded payload (§3.4.4). + let mut hebap = [0u8; N_COEFFS]; + compute_hebap( + &exps[lfe_idx][0], + LFE_END_MANT, + fscod, + &lfe_ba, + &mut hebap, + None, + ); + used += plan_aht_channel(&hebap[..LFE_END_MANT], 0, LFE_END_MANT, lx).total_bits(); + } else { + // Layout for mantissa_bits_total with nchan == 0: slot 0 + // is the (unused) coupling pseudo-channel, slot 1 the LFE. + let mut lfe_baps: Vec> = + vec![vec![[0u8; N_COEFFS]; BLOCKS_PER_FRAME]; 2]; + for blk in 0..BLOCKS_PER_FRAME { + compute_bap( + &exps[lfe_idx][blk], + LFE_END_MANT, + fscod, + &lfe_ba, + &mut lfe_baps[1][blk], + None, + ); + } + used += mantissa_bits_total(&lfe_baps, 0, 0, &cpl, true); + } + } + used + }; + + if used_at(0) > budget { + // Even the most negative SNR offset overflows — emit it anyway + // and let the frame packer surface the budget error. + return best; + } + let (mut lo, mut hi) = (0i32, 63 * 16 + 15); + while lo < hi { + let mid = (lo + hi + 1) / 2; + if used_at(mid) <= budget { + lo = mid; + } else { + hi = mid - 1; + } + } + best.csnroffst = (lo / 16) as u8; + best.fsnroffst = (lo % 16) as u8; + best.lfefsnroffst = best.fsnroffst; + for ch in 0..crate::audblk::MAX_FBW { + best.fsnroffst_ch[ch] = best.fsnroffst; + } + best +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_bytes_lookup_48k() { + // 192 kbps @ 48 kHz ⇒ 768 bytes (matches AC-3's frmsizecod=20). + assert_eq!(ac3_frame_bytes(0, 192), Some(768)); + // 96 kbps @ 48 kHz ⇒ 384 bytes. + assert_eq!(ac3_frame_bytes(0, 96), Some(384)); + // 32 kbps lower bound — even, in range. + assert_eq!(ac3_frame_bytes(0, 32), Some(128)); + // 640 kbps @ 48 kHz ⇒ 2560 bytes (largest A/52 row). + assert_eq!(ac3_frame_bytes(0, 640), Some(2560)); + } + + #[test] + fn make_encoder_stereo_48k() { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(48_000); + p.channels = Some(2); + p.bit_rate = Some(192_000); + let _enc = make_encoder(&p).expect("eac3 stereo 192k"); + } + + // ---- per-block SNR-offset (snroffststr ∈ {1, 2}) round-trip ---- + // + // The decoder's §2.3.3.27 per-block SNR-offset parse is validated by + // a self-encode → self-decode round-trip: the same PCM is encoded + // three times — once with the default frame-level `snroffststr == 0` + // and once each with `snroffststr == 1` and `== 2`. The `1` / `2` + // encodes carry the SAME numeric (csnroffst, fsnroffst) the bit + // allocator chose, just spread per-block across the audblks instead of + // once in audfrm; their mantissa budget is fractionally smaller (the + // per-block headers are reserved), so the decoded PCM is near-identical + // to the `0` baseline rather than bit-exact. The discriminating signal + // is that a ONE-BIT cursor misalignment in the new parse would corrupt + // every downstream mantissa and collapse the PSNR — so we gate on a + // high PSNR-vs-baseline floor (≥ 70 dB), which only a correctly + // bit-aligned parse can reach. + + pub(crate) fn psnr_vs(a: &[i16], b: &[i16]) -> f64 { + assert_eq!(a.len(), b.len(), "length mismatch in psnr_vs"); + let mut se = 0.0f64; + for (x, y) in a.iter().zip(b.iter()) { + let d = *x as f64 - *y as f64; + se += d * d; + } + let mse = se / a.len().max(1) as f64; + if mse == 0.0 { + return f64::INFINITY; + } + let peak = 32768.0f64; + 10.0 * (peak * peak / mse).log10() + } + + pub(crate) fn build_sine_pcm(channels: usize, frames: usize) -> Vec { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + for i in 0..n { + let t = i as f32 / 48_000.0; + // Two tones so several bands carry energy (so the SNR offset + // actually changes how many mantissa bits each band gets). + let s = 0.4 * (2.0 * std::f32::consts::PI * 440.0 * t).sin() + + 0.25 * (2.0 * std::f32::consts::PI * 3500.0 * t).sin(); + for ch in 0..channels { + pcm[i * channels + ch] = s * (1.0 - 0.1 * ch as f32); + } + } + pcm + } + + pub(crate) fn encode_with( + pcm: &[f32], + channels: usize, + bit_rate: u64, + snroffststr: u8, + ) -> Vec { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(channels as u16); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(bit_rate); + let mut enc: Box = if snroffststr == 0 { + make_encoder(¶ms).expect("eac3 make_encoder") + } else { + make_encoder_with_snroffststr(¶ms, snroffststr).expect("eac3 make_encoder snr") + }; + let n_samp = pcm.len() / channels; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut out = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => out.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 encode error: {e:?}"), + } + } + out + } + + pub(crate) fn decode_all(bytes: &[u8], frame_bytes: usize) -> Vec { + use crate::eac3::decoder::{decode_eac3_packet, Eac3DecoderState}; + let mut st = Eac3DecoderState::default(); + let mut out = Vec::new(); + let mut off = 0usize; + while off + frame_bytes <= bytes.len() { + let frame = decode_eac3_packet(&mut st, &bytes[off..off + frame_bytes]) + .expect("decode eac3 packet"); + for chunk in frame.pcm_s16le.chunks_exact(2) { + out.push(i16::from_le_bytes([chunk[0], chunk[1]])); + } + off += frame_bytes; + } + out + } + + fn assert_snroffststr_roundtrip(channels: usize, bit_rate: u64, frame_bytes: usize) { + let pcm = build_sine_pcm(channels, 4); + let base = decode_all(&encode_with(&pcm, channels, bit_rate, 0), frame_bytes); + let dec1 = decode_all(&encode_with(&pcm, channels, bit_rate, 1), frame_bytes); + let dec2 = decode_all(&encode_with(&pcm, channels, bit_rate, 2), frame_bytes); + assert!(!base.is_empty(), "baseline decode produced no PCM"); + assert_eq!( + dec1.len(), + base.len(), + "snroffststr=1 sample-count mismatch" + ); + assert_eq!( + dec2.len(), + base.len(), + "snroffststr=2 sample-count mismatch" + ); + + // (a) The two per-block strategies share an identical mantissa + // budget and SNR offsets, so they MUST decode bit-for-bit equal. + // This is the strict cursor-alignment invariant: if either parse + // mis-consumes a single bit, the mantissas diverge and the two + // decodes differ. + assert_eq!( + dec1, dec2, + "snroffststr=1 and =2 must decode bit-identically (same budget + offsets); \ + a mismatch means one of the per-block SNR-offset parses is misaligned" + ); + + // (b) Sanity: each per-block strategy decodes to near-identical + // audio as the frame-level baseline (the only delta is the few + // reserved header bits). A misaligned parse would collapse this + // PSNR toward 0 dB; the small budget delta keeps it ≥ 55 dB. + for (strat, dec) in [(1u8, &dec1), (2u8, &dec2)] { + let psnr = psnr_vs(dec, &base); + assert!( + psnr >= 55.0, + "snroffststr={strat}: PSNR vs frame-level baseline {psnr:.2} dB < 55 dB — \ + the per-block SNR-offset parse is bit-misaligned" + ); + } + } + + #[test] + fn snroffststr_per_block_roundtrip_mono() { + // 128 kbps @ 48 kHz ⇒ 512-byte frames, single indep substream. + // The slightly higher rate (vs the 96k default) leaves padding + // headroom for the extra per-block SNR-offset header bits the + // §2.3.3.27 strategies carry over the frame-level baseline. + assert_snroffststr_roundtrip(1, 128_000, 512); + } + + #[test] + fn snroffststr_per_block_roundtrip_stereo() { + // 256 kbps @ 48 kHz ⇒ 1024-byte frames. + assert_snroffststr_roundtrip(2, 256_000, 1024); + } + + #[test] + fn snroffststr_per_block_roundtrip_51() { + // 5.1 @ 448 kbps ⇒ 1792-byte frames, lfeon=1 so the §2.3.3.27 + // `snroffststr == 2` path also exercises the per-block + // `lfefsnroffst` slot. + assert_snroffststr_roundtrip(6, 448_000, 1792); + } + + // ---- spectral extension (§E.2.3.3 / §E.3.6) round-trips ---- + // + // The SPX encode is validated with the in-tree decoder on two + // axes: + // + // 1. **Cursor alignment** — the SPX strategy + coordinate fields + // thread through the audblk between the dynrng and coupling + // slots and re-gate chbwcod / nrematbd; one mis-sized field + // corrupts every downstream exponent + mantissa and collapses + // the decode to noise. The banded-energy gates below can only + // pass on a bit-aligned parse. + // 2. **§3.6.4.3 energy matching** — the whole point of the tool: + // per SPX band, the synthesized HF energy must match the + // original signal's banded HF energy (the encoder's coordinate + // computation + quantiser + the decoder's translation / blend / + // scale chain, end to end). + // + // The analysis measures banded energy in the encoder's own MDCT + // domain (same window + transform), averaged over the steady-state + // interior. Pure-tone content is stationary, so the metric is + // insensitive to coder delay and needs no lag alignment. + + fn encode_spx(pcm: &[f32], channels: usize, bit_rate: u64, spx: SpxParams) -> Vec { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(channels as u16); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(bit_rate); + let mut enc = make_encoder_with_spx(¶ms, spx).expect("eac3 make_encoder_with_spx"); + let n_samp = pcm.len() / channels; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut out = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => out.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 spx encode error: {e:?}"), + } + } + out + } + + /// Multitone with content below AND above the default SPX begin + /// frequency (tc# 109 ≈ 10.2 kHz at 48 kHz): LF tones at 700 Hz / + /// 3.1 kHz / 6 kHz feed the coded band + translation copy region, + /// HF tones at 12.2 / 15.4 / 19 kHz land in SPX bands 0 / 2 / 3. + /// + /// A deterministic broadband noise bed (~-38 dBFS) rides under the + /// tones. SPX synthesizes each HF band by *scaling a translated + /// copy* of the low band — a band whose translation source is + /// spectrally EMPTY can only be filled up to the coordinate + /// ceiling (`0.875 · 32 ×` the source RMS), so an all-tones signal + /// with silent gaps between tones is unencodable by construction + /// (the coordinate saturates and the band comes out low). Real + /// content carries a broadband floor; the bed models that and + /// keeps every band's coordinate inside the representable range. + fn build_spx_multitone(channels: usize, frames: usize) -> Vec { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + let tones: [(f32, f32); 6] = [ + (700.0, 0.22), + (3_100.0, 0.18), + (6_000.0, 0.14), + (12_200.0, 0.055), + (15_400.0, 0.035), + (19_000.0, 0.030), + ]; + let mut lfsr: u32 = 0x2545_F491; + for i in 0..n { + let t = i as f32 / 48_000.0; + let mut s = 0.0f32; + for (f, a) in tones { + s += a * (2.0 * std::f32::consts::PI * f * t).sin(); + } + // Deterministic xorshift noise bed, uniform in ±0.012. + lfsr ^= lfsr << 13; + lfsr ^= lfsr >> 17; + lfsr ^= lfsr << 5; + s += ((lfsr as f32 / u32::MAX as f32) * 2.0 - 1.0) * 0.012; + for ch in 0..channels { + pcm[i * channels + ch] = s * (1.0 - 0.08 * ch as f32); + } + } + pcm + } + + /// Mean per-bin MDCT energy of one channel of interleaved PCM, + /// using the encoder's own window + long transform, skipping the + /// first / last few blocks (codec priming + flush edges). + pub(crate) fn mdct_energy_profile(pcm: &[f32], channels: usize, ch: usize) -> [f64; N_COEFFS] { + use crate::mdct::mdct_512; + use crate::tables::WINDOW; + let n = pcm.len() / channels; + let mut acc = [0.0f64; N_COEFFS]; + let mut blocks = 0usize; + let mut start = 4 * SAMPLES_PER_BLOCK; // skip priming edge + while start + 512 + 4 * SAMPLES_PER_BLOCK <= n { + let mut win = [0.0f32; 512]; + for k in 0..256 { + win[k] = pcm[(start + k) * channels + ch] * WINDOW[k]; + win[511 - k] = pcm[(start + 511 - k) * channels + ch] * WINDOW[k]; + } + let mut coeffs = [0.0f32; N_COEFFS]; + mdct_512(&win, &mut coeffs); + for (a, &c) in acc.iter_mut().zip(coeffs.iter()) { + *a += (c as f64) * (c as f64); + } + blocks += 1; + start += SAMPLES_PER_BLOCK; + } + assert!(blocks > 8, "analysis needs a steady-state interior"); + for a in acc.iter_mut() { + *a /= blocks as f64; + } + acc + } + + fn band_db_deltas( + orig: &[f64; N_COEFFS], + dec: &[f64; N_COEFFS], + geom: &crate::eac3::spxenc::SpxGeometry, + ) -> Vec { + let mut out = Vec::new(); + let mut lo = geom.begin_tc; + for bnd in 0..geom.nbnds { + let hi = lo + geom.bndsztab[bnd]; + let eo: f64 = orig[lo..hi].iter().sum(); + let ed: f64 = dec[lo..hi].iter().sum(); + out.push(10.0 * (ed.max(1e-30) / eo.max(1e-30)).log10()); + lo = hi; + } + out + } + + pub(crate) fn to_f32_interleaved(pcm: &[i16]) -> Vec { + pcm.iter().map(|&v| v as f32 / 32767.0).collect() + } + + fn assert_spx_roundtrip(channels: usize, bit_rate: u64, frame_bytes: usize, tol_db: f64) { + let spx = SpxParams::default(); + let geom = SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC).unwrap(); + let pcm = build_spx_multitone(channels, 8); + let stream = encode_spx(&pcm, channels, bit_rate, spx); + assert_eq!( + stream.len() % frame_bytes, + 0, + "stream not a whole number of {frame_bytes}-byte frames" + ); + let dec = decode_all(&stream, frame_bytes); + assert_eq!( + dec.len() / channels, + (stream.len() / frame_bytes) * SAMPLES_PER_FRAME as usize, + "sample-count mismatch" + ); + let dec_f = to_f32_interleaved(&dec); + for ch in 0..channels.min(2) { + let orig_prof = mdct_energy_profile(&pcm, channels, ch); + let dec_prof = mdct_energy_profile(&dec_f, channels, ch); + // (a) SPX-band energy match (§3.6.4.3). + let deltas = band_db_deltas(&orig_prof, &dec_prof, &geom); + for (bnd, d) in deltas.iter().enumerate() { + assert!( + d.abs() <= tol_db, + "ch{ch} SPX band {bnd}: energy delta {d:+.2} dB exceeds ±{tol_db} dB \ + (all deltas: {deltas:?})" + ); + } + // (b) Coded-band fidelity: total LF energy within ±1.5 dB. + let eo: f64 = orig_prof[..geom.begin_tc].iter().sum(); + let ed: f64 = dec_prof[..geom.begin_tc].iter().sum(); + let lf_delta = 10.0 * (ed / eo).log10(); + assert!( + lf_delta.abs() <= 1.5, + "ch{ch}: coded-band energy delta {lf_delta:+.2} dB" + ); + } + } + + #[test] + fn spx_roundtrip_stereo_band_energy() { + // 192 kbps @ 48 kHz ⇒ 768-byte frames. + assert_spx_roundtrip(2, 192_000, 768, 3.0); + } + + #[test] + fn spx_roundtrip_mono_implicit_chinspx() { + // Mono exercises the §E.2.3.3.3 implicit `chinspx[0] = 1` arm + // (no per-channel bits). 96 kbps ⇒ 384-byte frames. + assert_spx_roundtrip(1, 96_000, 384, 3.0); + } + + #[test] + fn spx_roundtrip_51_with_lfe() { + // 5.1 exercises SPX alongside the LFE pseudo-channel (LFE is + // never in SPX; its exponents/mantissas follow the fbw SPX + // fields, so a mis-sized SPX field would corrupt it too). + // 384 kbps ⇒ 1536-byte frames. + assert_spx_roundtrip(6, 384_000, 1536, 3.5); + } + + #[test] + fn spx_explicit_band_structure_decodes_bit_identically() { + // `spxbndstrce = 1` with the Table E2.11 content vs + // `spxbndstrce = 0` (decoder default): identical geometry and + // — because the encoder reserves the worst-case (explicit) + // header bits in both modes — identical mantissa budgets, so + // the two decodes must match bit-for-bit. A one-bit misparse + // of the explicit structure field would break this instantly. + let pcm = build_spx_multitone(2, 4); + let dec_default = decode_all(&encode_spx(&pcm, 2, 192_000, SpxParams::default()), 768); + let dec_explicit = decode_all( + &encode_spx( + &pcm, + 2, + 192_000, + SpxParams { + explicit_band_structure: true, + ..SpxParams::default() + }, + ), + 768, + ); + assert!(!dec_default.is_empty()); + assert_eq!( + dec_default, dec_explicit, + "default-banding and explicit-banding SPX encodes must decode bit-identically" + ); + } + + #[test] + fn spx_narrow_region_nondefault_codes() { + // Non-default geometry: begin sub-band 4 (spxbegf=2 → tc 73), + // end sub-band 9 (spxendf=3 → tc 133), copy start sub-band 1 + // (tc 37 — non-zero spxstrtf arm), low-noise blend. Exercises + // remat_band_count_spx's spxbegf ≥ 2 arm with a narrower coded + // band and a copy region smaller than the SPX span (wraps). + let spx = SpxParams { + spxbegf: 2, + spxendf: 3, + spxstrtf: 1, + spxblnd: 31, + ..SpxParams::default() + }; + let geom = SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC).unwrap(); + assert_eq!((geom.begin_tc, geom.end_tc), (73, 133)); + let pcm = build_spx_multitone(2, 6); + let stream = encode_spx(&pcm, 2, 192_000, spx); + let dec = decode_all(&stream, 768); + let dec_f = to_f32_interleaved(&dec); + let orig_prof = mdct_energy_profile(&pcm, 2, 0); + let dec_prof = mdct_energy_profile(&dec_f, 2, 0); + let deltas = band_db_deltas(&orig_prof, &dec_prof, &geom); + for (bnd, d) in deltas.iter().enumerate() { + assert!( + d.abs() <= 3.5, + "SPX band {bnd}: energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + } + + /// Fixture for the adaptive copy-start test: a spectral HOLE in + /// tc# 25..49 (no content between ~2.3 and ~4.6 kHz), LF tones only + /// above it, HF tones in SPX bands 0/2/3, and a bed too weak to + /// carry a band on its own. With `spxstrtf = 0` the copy region + /// starts at tc# 25, so SPX band 0 (tc 109..133) translates the + /// near-silent hole and its coordinate pins at the 0.875 ceiling; + /// `spxstrtf = 2` starts the copy at tc# 49 where the tones live. + fn build_gap_multitone(channels: usize, frames: usize) -> Vec { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + let tones: [(f32, f32); 6] = [ + (700.0, 0.20), // below the copy region entirely + (5_500.0, 0.16), // tc ≈ 58 — inside 49..109 + (8_600.0, 0.12), // tc ≈ 91 — inside 49..109 + (12_200.0, 0.045), // SPX band 0 + (15_400.0, 0.030), // SPX band 2 + (19_000.0, 0.025), // SPX band 3 + ]; + let mut lfsr: u32 = 0x0BAD_5EED; + for i in 0..n { + let t = i as f32 / 48_000.0; + let mut s = 0.0f32; + for (f, a) in tones { + s += a * (2.0 * std::f32::consts::PI * f * t).sin(); + } + lfsr ^= lfsr << 13; + lfsr ^= lfsr >> 17; + lfsr ^= lfsr << 5; + s += ((lfsr as f32 / u32::MAX as f32) * 2.0 - 1.0) * 0.001; + for ch in 0..channels { + pcm[i * channels + ch] = s * (1.0 - 0.08 * ch as f32); + } + } + pcm + } + + #[test] + fn spx_adaptive_copy_start_avoids_saturation() { + let geom = SpxGeometry::derive(&SpxParams::default(), &DEFAULT_SPX_BNDSTRC).unwrap(); + let pcm = build_gap_multitone(2, 8); + // Tone-bearing SPX bands (12.2 / 15.4 / 19 kHz → bands 0/2/3). + let tone_bands = [0usize, 2, 3]; + + // Fixed spxstrtf = 0: band 0's translation source is the + // spectral hole → the coordinate saturates and the band decodes + // far below the original energy. + let fixed = SpxParams::default(); // spxstrtf = 0 + let dec_fixed = to_f32_interleaved(&decode_all(&encode_spx(&pcm, 2, 192_000, fixed), 768)); + let orig_prof = mdct_energy_profile(&pcm, 2, 0); + let fixed_prof = mdct_energy_profile(&dec_fixed, 2, 0); + let fixed_deltas = band_db_deltas(&orig_prof, &fixed_prof, &geom); + assert!( + fixed_deltas[0] < -6.0, + "premise: fixed copy-start must saturate band 0 (delta {:+.2} dB)", + fixed_deltas[0] + ); + + // Adaptive: the per-frame scorer must move the copy start past + // the hole and recover every tone band. + let adaptive = SpxParams { + adaptive_copy_start: true, + ..SpxParams::default() + }; + let dec_ad = to_f32_interleaved(&decode_all(&encode_spx(&pcm, 2, 192_000, adaptive), 768)); + let ad_prof = mdct_energy_profile(&dec_ad, 2, 0); + let ad_deltas = band_db_deltas(&orig_prof, &ad_prof, &geom); + for &bnd in &tone_bands { + assert!( + ad_deltas[bnd].abs() <= 3.5, + "adaptive: SPX band {bnd} delta {:+.2} dB (all: {ad_deltas:?}; fixed: {fixed_deltas:?})", + ad_deltas[bnd] + ); + } + // Coded band unaffected by the choice. + let eo: f64 = orig_prof[..geom.begin_tc].iter().sum(); + let ed: f64 = ad_prof[..geom.begin_tc].iter().sum(); + assert!((10.0 * (ed / eo).log10()).abs() <= 1.5); + } + + #[test] + fn spx_atten_roundtrip_stereo_band_energy() { + // §3.6.4.2.3 attenuation: spxattene=1 in audfrm with + // chinspxatten/spxattencod per channel; the decoder notches the + // translated coefficients at the border + wrap sites and the + // encoder folds the notch into its coordinate computation, so + // the banded-energy contract must STILL hold. This gates (a) + // the audfrm attenuation-field alignment (a mis-sized field + // shifts blkstrtinfoe and every audblk after it) and (b) the + // energy compensation. + let spx = SpxParams { + atten_code: Some(14), // taps [0.5, 0.25, 0.125] — a strong notch + ..SpxParams::default() + }; + let geom = SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC).unwrap(); + let pcm = build_spx_multitone(2, 8); + let stream = encode_spx(&pcm, 2, 192_000, spx); + let dec = decode_all(&stream, 768); + let dec_f = to_f32_interleaved(&dec); + for ch in 0..2 { + let orig_prof = mdct_energy_profile(&pcm, 2, ch); + let dec_prof = mdct_energy_profile(&dec_f, 2, ch); + let deltas = band_db_deltas(&orig_prof, &dec_prof, &geom); + for (bnd, d) in deltas.iter().enumerate() { + assert!( + d.abs() <= 3.0, + "ch{ch} SPX band {bnd} (atten): energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + } + // The attenuated stream must actually differ from the plain + // one (the notch + the audfrm fields are real bit-level + // effects, not a silent no-op). + let plain = encode_spx(&pcm, 2, 192_000, SpxParams::default()); + assert_ne!(stream, plain, "attenuation must change the bitstream"); + } + + #[test] + fn spx_coordinate_refresh_tracks_mid_frame_level_step() { + // The frame carries TWO coordinate refreshes (blocks 0 and 3, + // each energy-matching its own 3-block span) with a thrifted + // `spxcoe = 0` when block 3 quantises identically. This test + // proves the per-span refresh actually fires when the spectrum + // moves: a 14 kHz tone (SPX band 1) is gated ON only in the + // second half of every frame. A correct encoder+decoder tracks + // the step — the decoded second-half band energy must sit well + // above the first half's. A broken span refresh (or a thrift + // that wrongly reuses across the step) would flatten the two + // halves. + let channels = 2usize; + let frames = 8usize; + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + let mut lfsr: u32 = 0x1234_5677; + for i in 0..n { + let t = i as f32 / 48_000.0; + let in_second_half = (i % SAMPLES_PER_FRAME as usize) >= 768; + let mut s = 0.20 * (2.0 * std::f32::consts::PI * 700.0 * t).sin() + + 0.16 * (2.0 * std::f32::consts::PI * 3_100.0 * t).sin() + + 0.12 * (2.0 * std::f32::consts::PI * 6_000.0 * t).sin(); + if in_second_half { + s += 0.10 * (2.0 * std::f32::consts::PI * 14_000.0 * t).sin(); + } + lfsr ^= lfsr << 13; + lfsr ^= lfsr >> 17; + lfsr ^= lfsr << 5; + s += ((lfsr as f32 / u32::MAX as f32) * 2.0 - 1.0) * 0.008; + for ch in 0..channels { + pcm[i * channels + ch] = s * (1.0 - 0.08 * ch as f32); + } + } + let stream = encode_spx(&pcm, channels, 192_000, SpxParams::default()); + let dec_f = to_f32_interleaved(&decode_all(&stream, 768)); + + // 14 kHz ≈ tc 149 → SPX band 1 of the default geometry + // (tc 133..157). Measure that band's energy in analysis windows + // confined to each half of each frame (self round-trip output + // is sample-aligned with the input). + use crate::mdct::mdct_512; + use crate::tables::WINDOW; + let band = 133usize..157; + let half_energy = |pcm: &[f32], second: bool| -> f64 { + let mut acc = 0.0f64; + let mut cnt = 0usize; + for f in 1..frames - 1 { + let base = f * SAMPLES_PER_FRAME as usize + if second { 768 } else { 0 }; + for w0 in [0usize, 256] { + let start = base + w0; + let mut win = [0.0f32; 512]; + for k in 0..256 { + win[k] = pcm[(start + k) * 2] * WINDOW[k]; + win[511 - k] = pcm[(start + 511 - k) * 2] * WINDOW[k]; + } + let mut coeffs = [0.0f32; N_COEFFS]; + mdct_512(&win, &mut coeffs); + for tc in band.clone() { + acc += (coeffs[tc] as f64).powi(2); + } + cnt += 1; + } + } + acc / cnt as f64 + }; + let orig_ratio = 10.0 * (half_energy(&pcm, true) / half_energy(&pcm, false)).log10(); + let dec_ratio = 10.0 * (half_energy(&dec_f, true) / half_energy(&dec_f, false)).log10(); + assert!( + orig_ratio >= 12.0, + "premise: source step should be >= 12 dB (got {orig_ratio:+.1})" + ); + assert!( + dec_ratio >= 6.0, + "decoded SPX band 1 must track the mid-frame level step (orig {orig_ratio:+.1} dB, decoded {dec_ratio:+.1} dB)" + ); + } + + /// Mixed per-channel chinspx (§E.2.3.3.3): stereo with ch0 in SPX + /// and ch1 waveform-coded to its full chbwcod bandwidth. Gates: + /// + /// * ch0 keeps the §3.6.4.3 SPX band-energy contract; + /// * ch1's high-frequency region (above the SPX begin frequency, + /// where ch0 is synthesized) is waveform-coded and must hold its + /// total energy; + /// * both coded bands stay faithful. + /// + /// This exercises the per-channel end_mant plumbing end to end: + /// exponent sets of different lengths in one frame, chbwcod + /// emitted only for the non-SPX channel, spxcoe/coordinates only + /// for the SPX channel, and the SNR tuner budgeting each channel + /// at its own bandwidth. A mis-sized field desyncs everything. + #[test] + fn spx_mixed_membership_stereo() { + let spx = SpxParams { + channel_mask: Some(0b01), // ch0 in SPX, ch1 full-bandwidth + ..SpxParams::default() + }; + let geom = SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC).unwrap(); + let pcm = build_spx_multitone(2, 8); + // 256 kbps ⇒ 1024-byte frames — headroom for the full-bw ch1. + let stream = encode_spx(&pcm, 2, 256_000, spx); + assert_eq!(stream.len() % 1024, 0); + // Mixed membership must actually change the bitstream vs the + // uniform all-SPX encode at the same rate. + assert_ne!( + stream, + encode_spx(&pcm, 2, 256_000, SpxParams::default()), + "channel_mask must change the emission" + ); + let dec_f = to_f32_interleaved(&decode_all(&stream, 1024)); + + // ch0 — SPX band-energy contract (§3.6.4.3). + let orig0 = mdct_energy_profile(&pcm, 2, 0); + let dec0 = mdct_energy_profile(&dec_f, 2, 0); + let deltas = band_db_deltas(&orig0, &dec0, &geom); + for (bnd, d) in deltas.iter().enumerate() { + assert!( + d.abs() <= 3.5, + "ch0 (SPX) band {bnd}: energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + let eo: f64 = orig0[..geom.begin_tc].iter().sum(); + let ed: f64 = dec0[..geom.begin_tc].iter().sum(); + assert!( + (10.0 * (ed / eo).log10()).abs() <= 1.5, + "ch0 coded-band energy" + ); + + // ch1 — full-bandwidth waveform coding: the HF region that ch0 + // synthesizes must be carried by real mantissas here. + let orig1 = mdct_energy_profile(&pcm, 2, 1); + let dec1 = mdct_energy_profile(&dec_f, 2, 1); + let hf_o: f64 = orig1[geom.begin_tc..geom.end_tc].iter().sum(); + let hf_d: f64 = dec1[geom.begin_tc..geom.end_tc].iter().sum(); + let hf_delta = 10.0 * (hf_d.max(1e-30) / hf_o.max(1e-30)).log10(); + assert!( + hf_delta.abs() <= 3.0, + "ch1 (full-bw) HF energy delta {hf_delta:+.2} dB" + ); + let lo_o: f64 = orig1[..geom.begin_tc].iter().sum(); + let lo_d: f64 = dec1[..geom.begin_tc].iter().sum(); + assert!( + (10.0 * (lo_d / lo_o).log10()).abs() <= 1.5, + "ch1 coded-band energy" + ); + } + + /// The `spx_chmask` option key must build the same encoder as the + /// typed channel_mask, and the §E.2.3.3.3 constraints are enforced + /// at construction. + #[test] + fn spx_chmask_option_and_validation() { + let pcm = build_spx_multitone(2, 3); + let typed = encode_spx( + &pcm, + 2, + 192_000, + SpxParams { + channel_mask: Some(0b10), + ..SpxParams::default() + }, + ); + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + params.options = oxideav_core::CodecOptions::new().set("spx_chmask", "2"); + let mut enc = make_encoder(¶ms).expect("options-driven mixed SPX encoder"); + let n_samp = pcm.len() / 2; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in &pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut from_options = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => from_options.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("options mixed-spx encode error: {e:?}"), + } + } + assert_eq!( + typed, from_options, + "spx_chmask option and typed channel_mask must emit identical bytes" + ); + + // Validation: an all-zero typed mask is rejected. + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(48_000); + p.channels = Some(2); + assert!(make_encoder_with_spx( + &p, + SpxParams { + channel_mask: Some(0), + ..SpxParams::default() + } + ) + .is_err()); + // Mono cannot exclude its only channel (chinspx[0] implicit). + p.channels = Some(1); + assert!(make_encoder_with_spx( + &p, + SpxParams { + channel_mask: Some(0b10), + ..SpxParams::default() + } + ) + .is_err()); + } + + #[test] + fn spx_options_path_matches_typed_constructor_bit_for_bit() { + // The registry-facing options surface (`spx*` keys on + // CodecParameters::options) must build the SAME encoder as the + // typed make_encoder_with_spx — proven by byte-identical + // streams for a non-default configuration. + let pcm = build_spx_multitone(2, 3); + let spx = SpxParams { + spxbegf: 4, + spxendf: 7, + spxstrtf: 1, + spxblnd: 20, + adaptive_copy_start: false, + atten_code: Some(9), + explicit_band_structure: true, + channel_mask: None, + }; + let typed = encode_spx(&pcm, 2, 192_000, spx); + + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + params.options = oxideav_core::CodecOptions::new() + .set("spx_begf", "4") + .set("spx_endf", "7") + .set("spx_strtf", "1") + .set("spx_blnd", "20") + .set("spx_atten", "9") + .set("spx_explicit_band_structure", "true"); + let mut enc = make_encoder(¶ms).expect("options-driven SPX encoder"); + let n_samp = pcm.len() / 2; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in &pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut from_options = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => from_options.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("options encode error: {e:?}"), + } + } + assert_eq!( + typed, from_options, + "options-driven and typed SPX constructors must emit identical bytes" + ); + } + + #[test] + fn spx_options_validation_and_gating() { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + // Bad value → rejected at construction. + params.options = oxideav_core::CodecOptions::new().set("spx_begf", "9"); + assert!(make_encoder(¶ms).is_err()); + params.options = oxideav_core::CodecOptions::new().set("spx", "maybe"); + assert!(make_encoder(¶ms).is_err()); + // spx=0 disables even when sub-keys are present. + params.options = oxideav_core::CodecOptions::new() + .set("spx", "0") + .set("spx_begf", "4"); + assert!(make_encoder(¶ms).is_ok()); + // Geometry validation still applies through the options path + // (inverted range). + params.options = oxideav_core::CodecOptions::new() + .set("spx_begf", "7") + .set("spx_endf", "0"); + assert!(make_encoder(¶ms).is_err()); + // Plain enable works. + params.options = oxideav_core::CodecOptions::new().set("spx", "1"); + assert!(make_encoder(¶ms).is_ok()); + } + + #[test] + fn make_encoder_with_spx_rejects_invalid_geometry() { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(48_000); + p.channels = Some(2); + // Inverted sub-band range (begf=7 → sub-band 11, endf=0 → 5). + let bad = SpxParams { + spxbegf: 7, + spxendf: 0, + ..SpxParams::default() + }; + assert!(make_encoder_with_spx(&p, bad).is_err()); + // Empty copy region (strtf sub-band 3 ≥ begin sub-band 2). + let bad = SpxParams { + spxbegf: 0, + spxendf: 7, + spxstrtf: 3, + ..SpxParams::default() + }; + assert!(make_encoder_with_spx(&p, bad).is_err()); + } + + #[test] + fn make_encoder_rejects_unsupported_channels() { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(48_000); + // 4 ch is between stereo (2) and 5.1 (6) — not yet covered by + // the encoder's allow-list (1, 2, 6, 8). Likewise 7 isn't in + // the list (it would map to 3/2 + 1 ambiguous extra channel + // and we don't speculate). + p.channels = Some(4); + match make_encoder(&p) { + Ok(_) => panic!("must reject 4ch (not in 1/2/6/8 allow-list)"), + Err(e) => assert!(matches!(e, Error::Unsupported(_))), + } + } + + #[test] + fn make_encoder_rejects_bad_sample_rate() { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(16_000); + p.channels = Some(2); + match make_encoder(&p) { + Ok(_) => panic!("must reject 16 kHz"), + Err(e) => assert!(matches!(e, Error::Unsupported(_))), + } + } + + #[test] + fn make_encoder_71_builds_pair_layout() { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(48_000); + p.channels = Some(8); + let _enc = make_encoder(&p).expect("eac3 7.1"); + // Reach into the layout via a probe encode would require an + // accessor; since the layout struct is private, we cover the + // chanmap math directly: bit 6 (Lrs/Rrs pair, Table E2.5) is + // stored in MSB-6 = 1 << 9 = 0x0200. + assert_eq!(1u16 << (15 - 6), 0x0200); + } + + #[test] + fn make_encoder_51_5fbw_plus_lfe() { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(48_000); + p.channels = Some(6); + p.bit_rate = Some(384_000); + let _enc = make_encoder(&p).expect("eac3 5.1"); + } +} + +#[cfg(test)] +mod aht_tests { + use super::tests::{build_sine_pcm, decode_all, encode_with, psnr_vs}; + use super::*; + + pub(crate) fn encode_aht(pcm: &[f32], channels: usize, bit_rate: u64) -> Vec { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(channels as u16); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(bit_rate); + let mut enc = make_encoder_with_aht(¶ms).expect("eac3 make_encoder_with_aht"); + let n_samp = pcm.len() / channels; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut out = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => out.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 aht encode error: {e:?}"), + } + } + out + } + + /// AHT round-trip: encode stationary two-tone PCM with AHT, decode + /// with the in-tree decoder, and require BOTH a healthy absolute + /// PSNR and a clear coding-gain margin over the non-AHT baseline + /// at the same bit rate. The 6-block DCT-II concentrates a + /// stationary signal into few AHT-domain coefficients, so the + /// same bit budget buys a much finer spectrum — the measured gain + /// on this fixture is ~+20 dB; the ±6 dB gate leaves headroom. + /// + /// A single mis-sized field anywhere in the new audfrm chahtinu / + /// chgaqmod / gain-word / VQ / GAQ chain would corrupt every + /// downstream bit and collapse the PSNR to single digits, so this + /// is also the cursor-alignment proof. + fn assert_aht_roundtrip(channels: usize, bit_rate: u64, frame_bytes: usize) { + let pcm = build_sine_pcm(channels, 4); + let base_stream = encode_with(&pcm, channels, bit_rate, 0); + let base = decode_all(&base_stream, frame_bytes); + let aht_stream = encode_aht(&pcm, channels, bit_rate); + assert_eq!( + aht_stream.len() % frame_bytes, + 0, + "AHT stream not a whole number of {frame_bytes}-byte frames" + ); + assert_eq!(aht_stream.len(), base_stream.len(), "same rate → same size"); + assert_ne!(aht_stream, base_stream, "AHT must change the bitstream"); + let dec = decode_all(&aht_stream, frame_bytes); + assert_eq!(dec.len(), base.len(), "sample-count mismatch"); + + // Align out the 256-sample MDCT priming delay, and measure the + // fbw channels only: the LFE keeps the identical standard path + // in both encoders, and its 0-120 Hz band-limit makes it drop + // the fixture's tonal content by design — including it would + // just mask the fbw comparison behind a shared floor. + let delay = 256 * channels; + let fbw = if channels == 6 { 5 } else { channels }; + let input_i16: Vec = pcm + .iter() + .map(|&v| (v * 32767.0).clamp(-32768.0, 32767.0) as i16) + .collect(); + let n = base.len() - delay; + let strip_lfe = |data: &[i16]| -> Vec { + data.iter() + .enumerate() + .filter(|(i, _)| i % channels < fbw) + .map(|(_, &v)| v) + .collect() + }; + let p_base = psnr_vs(&strip_lfe(&base[delay..]), &strip_lfe(&input_i16[..n])); + let p_aht = psnr_vs(&strip_lfe(&dec[delay..]), &strip_lfe(&input_i16[..n])); + assert!( + p_aht >= 35.0, + "AHT decode PSNR {p_aht:.2} dB < 35 dB (baseline {p_base:.2} dB)" + ); + assert!( + p_aht >= p_base + 6.0, + "AHT ({p_aht:.2} dB) must out-code the non-AHT baseline ({p_base:.2} dB) by >= 6 dB on stationary content" + ); + } + + #[test] + fn aht_roundtrip_mono() { + assert_aht_roundtrip(1, 96_000, 384); + } + + #[test] + fn aht_roundtrip_stereo() { + assert_aht_roundtrip(2, 192_000, 768); + } + + #[test] + fn aht_roundtrip_51_with_lfe() { + // 5.1 exercises AHT alongside the (non-AHT) LFE pseudo-channel: + // the LFE's standard per-block mantissas follow the fbw AHT + // blocks in block 0 and stand alone in blocks 1..5. + assert_aht_roundtrip(6, 384_000, 1536); + } + + /// LFE-AHT (§3.4.2 lfeahtinu): a 5.1 fixture whose LFE carries a + /// 60 Hz tone (inside the 0-120 Hz coded band). The decoded LFE + /// channel must round-trip through the front-loaded LFE-AHT block + /// at a healthy PSNR and must not regress against the standard + /// per-block LFE path at the same rate. A mis-sized lfeahtinu / + /// lfegaqmod / LFE codeword field would corrupt the whole frame + /// tail and collapse both. + #[test] + fn aht_lfe_channel_roundtrip() { + let channels = 6usize; + let n = 4 * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + for i in 0..n { + let t = i as f32 / 48_000.0; + let s = 0.4 * (2.0 * std::f32::consts::PI * 440.0 * t).sin() + + 0.25 * (2.0 * std::f32::consts::PI * 3500.0 * t).sin(); + let lfe = 0.4 * (2.0 * std::f32::consts::PI * 60.0 * t).sin(); + for ch in 0..5 { + pcm[i * channels + ch] = s * (1.0 - 0.1 * ch as f32); + } + pcm[i * channels + 5] = lfe; + } + let base = decode_all(&encode_with(&pcm, channels, 384_000, 0), 1536); + let dec = decode_all(&encode_aht(&pcm, channels, 384_000), 1536); + assert_eq!(dec.len(), base.len()); + let delay = 256 * channels; + let input_i16: Vec = pcm + .iter() + .map(|&v| (v * 32767.0).clamp(-32768.0, 32767.0) as i16) + .collect(); + let n_cmp = base.len() - delay; + let lfe_only = |data: &[i16]| -> Vec { + data.iter() + .enumerate() + .filter(|(i, _)| i % channels == 5) + .map(|(_, &v)| v) + .collect() + }; + let p_base = psnr_vs(&lfe_only(&base[delay..]), &lfe_only(&input_i16[..n_cmp])); + let p_aht = psnr_vs(&lfe_only(&dec[delay..]), &lfe_only(&input_i16[..n_cmp])); + assert!( + p_aht >= 30.0, + "LFE-AHT PSNR {p_aht:.2} dB < 30 dB (standard-path LFE {p_base:.2} dB)" + ); + assert!( + p_aht >= p_base - 3.0, + "LFE-AHT ({p_aht:.2} dB) must not regress the standard LFE path ({p_base:.2} dB)" + ); + } + + /// The per-frame SNR-offset search must convert additional rate + /// into quality: across a 96 → 192 → 384 kbps ladder the AHT + /// decode PSNR must be non-decreasing and gain substantially end + /// to end (the measured stereo curve runs ~42 dB at 96 kbps to + /// ~73 dB at 384 kbps; the ±0.5 dB slack and the ≥ 15 dB + /// end-to-end floor leave wide margins). A tuner regression that + /// stopped spending the larger budget — or a cost model that + /// overflowed a frame — would flatten or break the curve. + #[test] + fn aht_quality_scales_with_rate() { + let pcm = build_sine_pcm(2, 4); + let input: Vec = pcm + .iter() + .map(|&v| (v * 32767.0).clamp(-32768.0, 32767.0) as i16) + .collect(); + let delay = 256 * 2; + let mut curve = Vec::new(); + for (rate, frame_bytes) in [(96_000u64, 384usize), (192_000, 768), (384_000, 1536)] { + let dec = decode_all(&encode_aht(&pcm, 2, rate), frame_bytes); + let n = dec.len() - delay; + curve.push(psnr_vs(&dec[delay..], &input[..n])); + } + for w in curve.windows(2) { + assert!( + w[1] >= w[0] - 0.5, + "AHT PSNR must be non-decreasing in rate: {curve:?}" + ); + } + assert!( + curve[curve.len() - 1] - curve[0] >= 15.0, + "AHT PSNR must gain >= 15 dB from 96k to 384k: {curve:?}" + ); + } + + #[test] + fn aht_options_path_matches_typed_constructor_bit_for_bit() { + let pcm = build_sine_pcm(2, 3); + let typed = encode_aht(&pcm, 2, 192_000); + + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + params.options = oxideav_core::CodecOptions::new().set("aht", "1"); + let mut enc = make_encoder(¶ms).expect("options-driven AHT encoder"); + let n_samp = pcm.len() / 2; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in &pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut from_options = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => from_options.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("options aht encode error: {e:?}"), + } + } + assert_eq!( + typed, from_options, + "options-driven and typed AHT constructors must emit identical bytes" + ); + } + + #[test] + fn aht_and_spx_cannot_combine() { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.options = oxideav_core::CodecOptions::new() + .set("aht", "1") + .set("spx", "1"); + assert!(make_encoder(¶ms).is_err()); + // Bad value rejected too. + params.options = oxideav_core::CodecOptions::new().set("aht", "maybe"); + assert!(make_encoder(¶ms).is_err()); + } +} + +#[cfg(test)] +mod ecpl_tests { + use super::tests::{decode_all, mdct_energy_profile, to_f32_interleaved}; + use super::*; + + // ---- enhanced coupling (§E.2.3.3.16-26 / §E.3.5.5) round-trip ---- + // + // Enhanced coupling is parametric above the begin frequency: the + // decoder rebuilds each coupled channel from one shared carrier via + // per-band amplitude + angle coordinates, so the quality contract + // is (a) per-ecpl-band decoded ENERGY within a small tolerance of + // the original (the §E.3.5.5.2 amplitude semantics), (b) unchanged + // fidelity of the independently-coded low band, and (c) waveform- + // level accuracy for phase-locked content (the carrier is phase- + // locked to the first coupled channel; other channels' phases ride + // the Table E3.11 angle coordinates — a broken angle path turns a + // quadrature-shifted tone into 100 % error energy, which the PSNR + // floor catches). + + fn encode_ecpl(pcm: &[f32], channels: usize, bit_rate: u64, ecpl: EcplParams) -> Vec { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(channels as u16); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(bit_rate); + let mut enc = make_encoder_with_ecpl(¶ms, ecpl).expect("eac3 make_encoder_with_ecpl"); + let n_samp = pcm.len() / channels; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut out = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => out.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 ecpl encode error: {e:?}"), + } + } + out + } + + /// Per-enhanced-coupling-band decoded-vs-original energy deltas in + /// dB, walking the geometry's band structure. + fn ecpl_band_db_deltas( + orig: &[f64; N_COEFFS], + dec: &[f64; N_COEFFS], + geom: &EcplGeometry, + ) -> Vec { + let mut out = Vec::new(); + let mut lo = geom.start_bin; + for &nb in &geom.band_bins { + let hi = (lo + nb).min(N_COEFFS); + let eo: f64 = orig[lo..hi].iter().sum(); + let ed: f64 = dec[lo..hi].iter().sum(); + out.push(10.0 * (ed.max(1e-30) / eo.max(1e-30)).log10()); + lo = hi; + } + out + } + + /// Correlated multitone spanning the coupled region (bins 37..253 ≈ + /// 3.5-23.7 kHz at 48 kHz) plus a shared LF tone below it. Each + /// channel carries the SAME tone complex, level-panned, with an + /// optional per-channel phase offset on the in-region tones (to + /// exercise the §E.2.3.3.24 angle coordinates). + fn build_ecpl_multitone(channels: usize, frames: usize, phase_step: f32) -> Vec { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + // In-region tones (all above tc 37 ≈ 3.5 kHz). + let hf_tones: [(f32, f32); 4] = [ + (4_300.0, 0.16), + (6_700.0, 0.13), + (9_800.0, 0.10), + (14_500.0, 0.07), + ]; + for i in 0..n { + let t = i as f32 / 48_000.0; + let lf = 0.20 * (2.0 * std::f32::consts::PI * 700.0 * t).sin(); + for ch in 0..channels { + let phase = phase_step * ch as f32; + let mut s = lf; + for (f, a) in hf_tones { + s += a * (2.0 * std::f32::consts::PI * f * t + phase).sin(); + } + pcm[i * channels + ch] = s * (1.0 - 0.12 * ch as f32); + } + } + pcm + } + + /// Interior PSNR between the original PCM and the decoded stream + /// for one channel, searching over the codec's fixed small latency + /// (the MDCT overlap delay) and skipping the priming / flush edges. + fn interior_psnr_ch(orig: &[f32], dec: &[i16], channels: usize, ch: usize) -> f64 { + let n = (orig.len() / channels).min(dec.len() / channels); + let skip = 4 * SAMPLES_PER_BLOCK; // priming edge + let tail = 2 * SAMPLES_PER_BLOCK; // flush edge + let mut best = f64::MIN; + for lag in 0..=512usize { + let mut se = 0.0f64; + let mut count = 0usize; + let mut i = skip; + while i + lag < n - tail { + let o = (orig[i * channels + ch] * 32767.0) as f64; + let d = dec[(i + lag) * channels + ch] as f64; + se += (o - d) * (o - d); + count += 1; + i += 1; + } + if count == 0 { + continue; + } + let mse = se / count as f64; + let psnr = 10.0 * (32768.0f64 * 32768.0 / mse.max(1e-12)).log10(); + if psnr > best { + best = psnr; + } + } + best + } + + #[allow(clippy::too_many_arguments)] + fn assert_ecpl_roundtrip( + channels: usize, + bit_rate: u64, + frame_bytes: usize, + phase_step: f32, + band_tol_db: f64, + lf_tol_db: f64, + psnr_floor_db: f64, + ) { + let ecpl = EcplParams::default(); + let geom = EcplGeometry::derive(&ecpl, &DEFAULT_ECPL_BNDSTRC).unwrap(); + let pcm = build_ecpl_multitone(channels, 8, phase_step); + let stream = encode_ecpl(&pcm, channels, bit_rate, ecpl); + assert_eq!( + stream.len() % frame_bytes, + 0, + "stream not a whole number of {frame_bytes}-byte frames" + ); + let dec = decode_all(&stream, frame_bytes); + assert_eq!( + dec.len() / channels, + (stream.len() / frame_bytes) * SAMPLES_PER_FRAME as usize, + "sample-count mismatch" + ); + let dec_f = to_f32_interleaved(&dec); + let nfchans = if channels == 6 { 5 } else { channels }; + for ch in 0..nfchans { + let orig_prof = mdct_energy_profile(&pcm, channels, ch); + let dec_prof = mdct_energy_profile(&dec_f, channels, ch); + // (a) per-band energy match across the coupled region. + // Bands more than 40 dB below the loudest coupled band + // carry no signal (fixture noise floor) — their ratio is + // quantiser-noise-vs-noise and meaningless, so skip them. + let deltas = ecpl_band_db_deltas(&orig_prof, &dec_prof, &geom); + let band_energies: Vec = { + let mut out = Vec::new(); + let mut lo = geom.start_bin; + for &nb in &geom.band_bins { + let hi = (lo + nb).min(N_COEFFS); + out.push(orig_prof[lo..hi].iter().sum()); + lo = hi; + } + out + }; + let peak = band_energies.iter().cloned().fold(0.0f64, f64::max); + for (bnd, d) in deltas.iter().enumerate() { + if band_energies[bnd] < peak * 1e-3 { + continue; // > 30 dB below the loudest band: the + // pure-tone fixtures leave only MDCT + // leakage there — reconstruction noise + // vs leakage is not a meaningful ratio. + } + assert!( + d.abs() <= band_tol_db, + "ch{ch} ecpl band {bnd}: energy delta {d:+.2} dB exceeds ±{band_tol_db} dB \ + (all deltas: {deltas:?})" + ); + } + // (b) coded low-band fidelity. + let eo: f64 = orig_prof[..geom.start_bin].iter().sum(); + let ed: f64 = dec_prof[..geom.start_bin].iter().sum(); + let lf_delta: f64 = 10.0 * (ed / eo).log10(); + assert!( + lf_delta.abs() <= lf_tol_db, + "ch{ch}: coded-band energy delta {lf_delta:+.2} dB exceeds ±{lf_tol_db} dB" + ); + // (c) waveform-level accuracy (amplitude AND phase). + let psnr = interior_psnr_ch(&pcm, &dec, channels, ch); + assert!( + psnr >= psnr_floor_db, + "ch{ch}: interior PSNR {psnr:.1} dB below the {psnr_floor_db} dB floor" + ); + } + } + + #[test] + fn ecpl_roundtrip_stereo_band_energy() { + // 2/0 exercises the implicit-chincpl arm. 192 kbps ⇒ 768-byte + // frames. In-phase panned content: the carrier is (a scaled) + // channel 0, so both channels reconstruct with angle ≈ 0. + assert_ecpl_roundtrip(2, 192_000, 768, 0.0, 3.0, 1.5, 20.0); + } + + #[test] + fn ecpl_roundtrip_stereo_quadrature_angles() { + // Channel 1's in-region tones are 90° out of phase with the + // carrier source: without the §E.2.3.3.24 angle coordinates the + // reconstruction would be in quadrature (≈ 3 dB PSNR against + // the original); with them the waveform floor holds. + assert_ecpl_roundtrip(2, 192_000, 768, std::f32::consts::FRAC_PI_2, 3.0, 1.5, 18.0); + } + + /// `docs/audio/ac3/ac3-errata.md` entry E3, encode→decode + /// direction: the full bitstream round-trip through the corrected + /// §E.3.5.5.1 overlap-add reproduces the enhanced-coupling region + /// at unity level. The erratum's as-printed reading (step 3 without + /// the §7.9.4.1 step-6 factor of 2) reconstructs the carrier at + /// half scale; the loudest coupled channel would then need the + /// unrepresentable amplitude coordinate 2.0 (Table E3.10 ceiling is + /// exactly 1.0, §E.3.5.4 code 0 = 0 dB), so the region decodes at a + /// systematic −6 dB. Gate the aggregate in-region energy delta well + /// inside that discriminator, per channel. + #[test] + fn ecpl_roundtrip_unity_level_pins_factor2_erratum() { + let ecpl = EcplParams::default(); + let geom = EcplGeometry::derive(&ecpl, &DEFAULT_ECPL_BNDSTRC).unwrap(); + let pcm = build_ecpl_multitone(2, 8, 0.0); + let stream = encode_ecpl(&pcm, 2, 192_000, ecpl); + let dec = decode_all(&stream, 768); + let dec_f = to_f32_interleaved(&dec); + let (lo, hi) = (geom.start_bin, geom.end_bin.min(N_COEFFS)); + for ch in 0..2 { + let orig = mdct_energy_profile(&pcm, 2, ch); + let decp = mdct_energy_profile(&dec_f, 2, ch); + let eo: f64 = orig[lo..hi].iter().sum(); + let ed: f64 = decp[lo..hi].iter().sum(); + let delta = 10.0 * (ed / eo.max(1e-30)).log10(); + assert!( + delta.abs() <= 1.5, + "ch{ch}: aggregate ecpl-region energy delta {delta:+.2} dB is not unity \ + (the as-printed §E.3.5.5.1 overlap-add sits at −6.02 dB)" + ); + } + } + + #[test] + fn ecpl_roundtrip_51_explicit_chincpl() { + // acmod = 7 (5 fbw channels) transmits explicit chincpl[ch] + // bits — this pins the §E.1.3.3.7 field ORDER (chincpl before + // the enhanced-coupling strategy fields; the pre-r406 decoder + // read them swapped, which desyncs every multichannel ecpl + // frame) and the LFE integrity behind the coupling fields. + // 384 kbps ⇒ 1536-byte frames. + assert_ecpl_roundtrip(6, 384_000, 1536, 0.35, 3.5, 2.0, 15.0); + } + + #[test] + fn ecpl_registry_options_build_identical_encoder() { + let pcm = build_ecpl_multitone(2, 3, 0.0); + let typed = encode_ecpl(&pcm, 2, 192_000, EcplParams::default()); + + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + params.options = oxideav_core::CodecOptions::new().set("ecpl", "1"); + let mut enc = make_encoder(¶ms).expect("options ecpl encoder"); + let n_samp = pcm.len() / 2; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in &pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut from_options = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => from_options.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("options encode error: {e:?}"), + } + } + assert_eq!( + typed, from_options, + "options-driven and typed ecpl constructors must emit identical bytes" + ); + } + + #[test] + fn ecpl_options_validation_and_gating() { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + // Bad values → rejected at construction. + params.options = oxideav_core::CodecOptions::new().set("ecpl_begf", "16"); + assert!(make_encoder(¶ms).is_err()); + params.options = oxideav_core::CodecOptions::new().set("ecpl", "maybe"); + assert!(make_encoder(¶ms).is_err()); + // Below the supported begin grid (begf 0/1 → begin sub-band < 4). + params.options = oxideav_core::CodecOptions::new().set("ecpl_begf", "1"); + assert!(make_encoder(¶ms).is_err()); + // Inverted range. + params.options = oxideav_core::CodecOptions::new() + .set("ecpl_begf", "13") + .set("ecpl_endf", "0"); + assert!(make_encoder(¶ms).is_err()); + // ecpl=0 disables even with sub-keys present. + params.options = oxideav_core::CodecOptions::new() + .set("ecpl", "0") + .set("ecpl_begf", "3"); + assert!(make_encoder(¶ms).is_ok()); + // Plain enable works; mutual exclusions fire. + params.options = oxideav_core::CodecOptions::new().set("ecpl", "1"); + assert!(make_encoder(¶ms).is_ok()); + params.options = oxideav_core::CodecOptions::new() + .set("ecpl", "1") + .set("aht", "1"); + assert!(make_encoder(¶ms).is_err()); + // spx + ecpl is the §3.6.1 co-active configuration — allowed + // (see `spx_ecpl_coactive_stereo_band_energy`). + params.options = oxideav_core::CodecOptions::new() + .set("ecpl", "1") + .set("spx", "1"); + assert!(make_encoder(¶ms).is_ok()); + // Mono cannot couple. + params.channels = Some(1); + params.options = oxideav_core::CodecOptions::new().set("ecpl", "1"); + assert!(make_encoder(¶ms).is_err()); + } + #[test] + fn ecpl_71_pair_couples_indep_only() { + // 7.1 emits an indep 5.1 substream (enhanced-coupled) + a + // plain dependent Lb/Rb substream in every packet — the two + // syntaxes must coexist: the dep substream carries no coupling + // bits (cplinu[0] = 0) while the indep one carries the full + // strategy/coordinate/carrier stack. A field-width error in + // either would desync the pair walk. + let pcm = build_ecpl_multitone(8, 6, 0.3); + let stream = encode_ecpl(&pcm, 8, 576_000, EcplParams::default()); + // 384k indep (1536 B) + 192k dep (768 B) per packet. + let pair_bytes = 1536 + 768; + assert_eq!(stream.len() % pair_bytes, 0, "not a whole pair stream"); + + use crate::eac3::decoder::{decode_eac3_packet, Eac3DecoderState}; + let mut st = Eac3DecoderState::default(); + let mut out: Vec = Vec::new(); + let mut channels = 0usize; + let mut off = 0usize; + while off + pair_bytes <= stream.len() { + let frame = decode_eac3_packet(&mut st, &stream[off..off + pair_bytes]) + .expect("decode ecpl 7.1 indep+dep packet"); + assert_eq!(frame.channels, 8, "expected 8 output channels"); + channels = frame.channels as usize; + for c in frame.pcm_s16le.chunks_exact(2) { + out.push(i16::from_le_bytes([c[0], c[1]])); + } + off += pair_bytes; + } + assert!(!out.is_empty(), "no PCM decoded"); + // Every output channel is live (the fixture feeds all 8 source + // channels; LFE gets the sub-120 Hz residue of the 700 Hz tone + // suppressed, so exempt slot 5's level check). + let n = out.len() / channels; + for ch in 0..channels { + if ch == 5 { + continue; // LFE — fixture has no sub-120 Hz content + } + let mut e = 0.0f64; + for i in n / 4..(3 * n / 4) { + let v = out[i * channels + ch] as f64; + e += v * v; + } + let rms = (e / (n / 2) as f64).sqrt(); + assert!( + rms > 100.0, + "output channel {ch} is near-silent (rms {rms:.1})" + ); + } + } + // ---- SPX + enhanced coupling co-active (§3.6.1) ---- + + fn encode_spx_ecpl(pcm: &[f32], channels: usize, bit_rate: u64) -> Vec { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(channels as u16); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(bit_rate); + let mut enc = + make_encoder_with_spx_ecpl(¶ms, SpxParams::default(), EcplParams::default()) + .expect("eac3 make_encoder_with_spx_ecpl"); + let n_samp = pcm.len() / channels; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut out = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => out.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 spx+ecpl encode error: {e:?}"), + } + } + out + } + + /// Three-region fixture: an LF tone below the coupling begin + /// (bins < 37), phase-offset tones inside the coupling region + /// (37..109 — 4.3 / 6.7 / 9.2 kHz), HF tones + a noise bed inside + /// the SPX region (109..229 — 12.2 / 15.4 kHz). + fn build_spx_ecpl_multitone(channels: usize, frames: usize) -> Vec { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * channels]; + let cpl_tones: [(f32, f32); 3] = [(4_300.0, 0.16), (6_700.0, 0.13), (9_200.0, 0.10)]; + let spx_tones: [(f32, f32); 2] = [(12_200.0, 0.05), (15_400.0, 0.035)]; + let mut lfsr: u32 = 0x2545_F491; + for i in 0..n { + let t = i as f32 / 48_000.0; + let lf = 0.20 * (2.0 * std::f32::consts::PI * 700.0 * t).sin(); + lfsr ^= lfsr << 13; + lfsr ^= lfsr >> 17; + lfsr ^= lfsr << 5; + let noise = ((lfsr as f32 / u32::MAX as f32) * 2.0 - 1.0) * 0.012; + for ch in 0..channels { + let phase = 0.6 * ch as f32; + let mut s = lf + noise; + for (f, a) in cpl_tones { + s += a * (2.0 * std::f32::consts::PI * f * t + phase).sin(); + } + for (f, a) in spx_tones { + s += a * (2.0 * std::f32::consts::PI * f * t).sin(); + } + pcm[i * channels + ch] = s * (1.0 - 0.1 * ch as f32); + } + } + pcm + } + + #[test] + fn spx_ecpl_coactive_stereo_band_energy() { + let spx = SpxParams::default(); + let spx_geom = SpxGeometry::derive(&spx, &DEFAULT_SPX_BNDSTRC).unwrap(); + let ecpl_geom = EcplGeometry::derive_with_spx( + &EcplParams::default(), + spx.spxbegf, + &DEFAULT_ECPL_BNDSTRC, + ) + .unwrap(); + // The two regions abut exactly: ecplsubbndtab[end] == SPX begin tc. + assert_eq!(ecpl_geom.end_bin, spx_geom.begin_tc); + assert!(ecpl_geom.spx_bounded); + + let pcm = build_spx_ecpl_multitone(2, 8); + let stream = encode_spx_ecpl(&pcm, 2, 192_000); + assert_eq!(stream.len() % 768, 0); + let dec = decode_all(&stream, 768); + assert_eq!( + dec.len() / 2, + (stream.len() / 768) * SAMPLES_PER_FRAME as usize + ); + let dec_f = to_f32_interleaved(&dec); + for ch in 0..2 { + let orig_prof = mdct_energy_profile(&pcm, 2, ch); + let dec_prof = mdct_energy_profile(&dec_f, 2, ch); + // (a) coupling-region band energies (37..109). + let deltas = ecpl_band_db_deltas(&orig_prof, &dec_prof, &ecpl_geom); + let mut lo = ecpl_geom.start_bin; + let mut energies = Vec::new(); + for &nb in &ecpl_geom.band_bins { + let hi = (lo + nb).min(N_COEFFS); + energies.push(orig_prof[lo..hi].iter().sum::()); + lo = hi; + } + let peak = energies.iter().cloned().fold(0.0f64, f64::max); + for (bnd, d) in deltas.iter().enumerate() { + if energies[bnd] < peak * 1e-4 { + continue; + } + assert!( + d.abs() <= 3.0, + "ch{ch} coupling band {bnd}: energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + // (b) SPX-region band energies (109..229, §3.6.4.3). + let mut lo = spx_geom.begin_tc; + for bnd in 0..spx_geom.nbnds { + let hi = lo + spx_geom.bndsztab[bnd]; + let eo: f64 = orig_prof[lo..hi].iter().sum(); + let ed: f64 = dec_prof[lo..hi].iter().sum(); + let d = 10.0 * (ed.max(1e-30) / eo.max(1e-30)).log10(); + assert!( + d.abs() <= 3.5, + "ch{ch} SPX band {bnd}: energy delta {d:+.2} dB" + ); + lo = hi; + } + // (c) coded low band (< 37) fidelity. + let eo: f64 = orig_prof[..ecpl_geom.start_bin].iter().sum(); + let ed: f64 = dec_prof[..ecpl_geom.start_bin].iter().sum(); + let lf_delta: f64 = 10.0 * (ed / eo).log10(); + assert!( + lf_delta.abs() <= 1.5, + "ch{ch}: coded-band energy delta {lf_delta:+.2} dB" + ); + } + } + + #[test] + fn spx_ecpl_constructor_rules() { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.bit_rate = Some(192_000); + // chmask + ecpl rejected (a coupled channel outside SPX would + // have no content above the coupling region). + let masked = SpxParams { + channel_mask: Some(1), + ..SpxParams::default() + }; + assert!(make_encoder_with_spx_ecpl(¶ms, masked, EcplParams::default()).is_err()); + // aht + ecpl still rejected via options. + params.options = oxideav_core::CodecOptions::new() + .set("ecpl", "1") + .set("aht", "1"); + assert!(make_encoder(¶ms).is_err()); + // spx + ecpl now allowed via options. + params.options = oxideav_core::CodecOptions::new() + .set("ecpl", "1") + .set("spx", "1"); + assert!(make_encoder(¶ms).is_ok()); + // An SPX begin low enough to empty the coupling region is + // rejected: spxbegf = 0 → ecpl end sub-band 5 > begin 4 is + // still valid, so use a begin code above it (ecplbegf 4 → + // begin sub-band 6 >= end 5). + let spx_low = SpxParams { + spxbegf: 0, + ..SpxParams::default() + }; + params.options = oxideav_core::CodecOptions::new(); + let ecpl_high = EcplParams { + ecplbegf: 4, + ecplendf: 15, + ..EcplParams::default() + }; + assert!(make_encoder_with_spx_ecpl(¶ms, spx_low, ecpl_high).is_err()); + } + + #[test] + fn spx_ecpl_registry_options_build_identical_encoder() { + let pcm = build_spx_ecpl_multitone(2, 3); + let typed = encode_spx_ecpl(&pcm, 2, 192_000); + + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + params.options = oxideav_core::CodecOptions::new() + .set("spx", "1") + .set("ecpl", "1"); + let mut enc = make_encoder(¶ms).expect("options spx+ecpl encoder"); + let n_samp = pcm.len() / 2; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in &pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut from_options = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => from_options.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("options encode error: {e:?}"), + } + } + assert_eq!( + typed, from_options, + "options-driven and typed spx+ecpl constructors must emit identical bytes" + ); + } + /// MDCT-domain inter-channel coherence of interleaved stereo PCM + /// over the enhanced-coupling region: `|Σ L·R| / sqrt(ΣL²·ΣR²)` + /// accumulated per block across the steady interior. ≈1 for + /// phase-locked copies (what a chaos-less parametric reconstruction + /// produces), ≈0 for independent content. + fn region_coherence(pcm: &[f32], geom: &EcplGeometry) -> f64 { + use crate::mdct::mdct_512; + use crate::tables::WINDOW; + let channels = 2usize; + let n = pcm.len() / channels; + let mut cross = 0.0f64; + let mut el = 0.0f64; + let mut er = 0.0f64; + let mut start = 4 * SAMPLES_PER_BLOCK; + while start + 512 + 4 * SAMPLES_PER_BLOCK <= n { + let mut coeffs = [[0.0f32; N_COEFFS]; 2]; + for (ch, out) in coeffs.iter_mut().enumerate() { + let mut win = [0.0f32; 512]; + for k in 0..256 { + win[k] = pcm[(start + k) * channels + ch] * WINDOW[k]; + win[511 - k] = pcm[(start + 511 - k) * channels + ch] * WINDOW[k]; + } + mdct_512(&win, out); + } + for bin in geom.start_bin..geom.end_bin.min(N_COEFFS) { + let l = coeffs[0][bin] as f64; + let r = coeffs[1][bin] as f64; + cross += l * r; + el += l * l; + er += r * r; + } + start += SAMPLES_PER_BLOCK; + } + cross.abs() / (el * er).sqrt().max(1e-30) + } + + /// Stereo fixture with PARTIAL in-region coherence: the right + /// channel carries the left channel's tones in phase (the coherent + /// part — its band angle vs the carrier is ≈ 0) plus equal-level + /// independent tones 200 Hz away, landing in the SAME coupling + /// bands (the incoherent part). Per-band coherence ≈ 0.7, so the + /// coherence-driven chaos codes fire while the measured band angle + /// stays pinned near zero — isolating the chaos path from the + /// angle path. + fn build_width_fixture(frames: usize) -> Vec { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * 2]; + let tones: [(f32, f32); 3] = [(4_300.0, 0.16), (6_700.0, 0.13), (9_800.0, 0.10)]; + for i in 0..n { + let t = i as f32 / 48_000.0; + let lf = 0.20 * (2.0 * std::f32::consts::PI * 700.0 * t).sin(); + let mut l = lf; + let mut r = lf; + for (f, a) in tones { + let common = a * (2.0 * std::f32::consts::PI * f * t).sin(); + l += common; + r += 0.7 * common + + 0.7 * a * (2.0 * std::f32::consts::PI * (f + 200.0) * t + 1.1).sin(); + } + pcm[i * 2] = l; + pcm[i * 2 + 1] = r; + } + pcm + } + + #[test] + fn ecpl_chaos_restores_stereo_width() { + // A chaos-less parametric reconstruction turns the right + // channel into a phase-rotated copy of the carrier (≈ left): + // decoded inter-channel coherence over the coupled region + // collapses toward 1 even though the source channels are + // independent there. With coherence-driven chaos coordinates + // the decoder's §E.3.5.5.3 random de-correlation restores the + // width: decoded coherence drops far below the chaos-less + // decode. Band energies must stay matched either way (the + // §E.3.5.5.2 amplitude modification is pre-compensated). + let geom = EcplGeometry::derive(&EcplParams::default(), &DEFAULT_ECPL_BNDSTRC).unwrap(); + let pcm = build_width_fixture(8); + let orig_coh = region_coherence(&pcm, &geom); + assert!( + orig_coh < 0.75, + "fixture is not partially incoherent in-region (coherence {orig_coh:.3})" + ); + + let with_chaos = EcplParams::default(); + let without_chaos = EcplParams { + chaos: false, + ..EcplParams::default() + }; + let dec_on = + to_f32_interleaved(&decode_all(&encode_ecpl(&pcm, 2, 192_000, with_chaos), 768)); + let dec_off = to_f32_interleaved(&decode_all( + &encode_ecpl(&pcm, 2, 192_000, without_chaos), + 768, + )); + let coh_on = region_coherence(&dec_on, &geom); + let coh_off = region_coherence(&dec_off, &geom); + eprintln!("ECPL-WIDTH orig={orig_coh:.3} off={coh_off:.3} on={coh_on:.3}"); + // Measured (deterministic decode): orig ≈ 0.71, chaos-less + // 0.914 (the coherent part is reconstructed phase-locked; the + // incoherent part is REPLACED by more phase-locked carrier + // content), chaos-on 0.796 (the per-bin ±2/7·π jitter of the + // code-2 bands knocks the excess coherence back down — + // sinc-of-jitter ≈ 0.87 multiplier, matching theory). Gate the + // regression envelope rather than exact values. + assert!( + coh_off > 0.87, + "chaos-less decode should be near-coherent (got {coh_off:.3})" + ); + assert!( + coh_on < 0.83, + "chaos decode should restore width (got {coh_on:.3} vs chaos-less {coh_off:.3})" + ); + assert!( + coh_off - coh_on > 0.06, + "chaos should reduce coherence by a clear margin \ + (off {coh_off:.3} vs on {coh_on:.3})" + ); + // Band energies stay matched with chaos on (amplitude + // pre-compensation): reuse the per-band check on both channels. + for ch in 0..2 { + let orig_prof = mdct_energy_profile(&pcm, 2, ch); + let dec_prof = mdct_energy_profile(&dec_on, 2, ch); + let deltas = ecpl_band_db_deltas(&orig_prof, &dec_prof, &geom); + let mut lo = geom.start_bin; + let mut energies = Vec::new(); + for &nb in &geom.band_bins { + let hi = (lo + nb).min(N_COEFFS); + energies.push(orig_prof[lo..hi].iter().sum::()); + lo = hi; + } + let peak = energies.iter().cloned().fold(0.0f64, f64::max); + for (bnd, d) in deltas.iter().enumerate() { + // This fixture is pure tones (no noise bed): bands + // without a tone hold only MDCT leakage, 25-35 dB + // down — their "energy delta" is reconstruction noise + // vs leakage and meaningless. Gate the tone bands. + if energies[bnd] < peak * 1e-3 { + continue; + } + assert!( + d.abs() <= 3.5, + "ch{ch} band {bnd}: chaos-on energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + } + } + #[test] + fn ecpl_roundtrip_narrow_geometry() { + // Non-default begin/end codes: ecplbegf = 7 exercises the + // Table E3.8 middle arm (begin = ecplbegf + 2 = 9 → tc 97); + // ecplendf = 8 → end sub-band 15 → tc 169. The region spans + // 9.1-15.8 kHz and the default Table E2.14 banding merges its + // 6 sub-bands into 3 bands (merges at 9, 11, 13). + let params = EcplParams { + ecplbegf: 7, + ecplendf: 8, + ..EcplParams::default() + }; + let geom = EcplGeometry::derive(¶ms, &DEFAULT_ECPL_BNDSTRC).unwrap(); + assert_eq!((geom.start_bin, geom.end_bin), (97, 169)); + // Span 9..15 under the default banding: sub-band 9 starts band + // 0 (its Table E2.14 merge bit is masked per §E.2.3.3.19), + // merges at 11 and 13 → 4 bands. + assert_eq!(geom.necplbnd, 4); + assert_eq!(geom.band_bins.len(), 4); + + // Tones inside the narrow region (10.1 / 12.4 / 14.6 kHz → + // bins 108 / 132 / 156) + LF anchor below it. + let n = 8 * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * 2]; + for i in 0..n { + let t = i as f32 / 48_000.0; + let lf = 0.20 * (2.0 * std::f32::consts::PI * 700.0 * t).sin(); + for ch in 0..2 { + let phase = 0.5 * ch as f32; + let s = lf + + 0.15 * (2.0 * std::f32::consts::PI * 10_100.0 * t + phase).sin() + + 0.12 * (2.0 * std::f32::consts::PI * 12_400.0 * t + phase).sin() + + 0.08 * (2.0 * std::f32::consts::PI * 14_600.0 * t + phase).sin(); + pcm[i * 2 + ch] = s * (1.0 - 0.1 * ch as f32); + } + } + let stream = encode_ecpl(&pcm, 2, 192_000, params); + assert_eq!(stream.len() % 768, 0); + let dec_f = to_f32_interleaved(&decode_all(&stream, 768)); + for ch in 0..2 { + let orig_prof = mdct_energy_profile(&pcm, 2, ch); + let dec_prof = mdct_energy_profile(&dec_f, 2, ch); + let deltas = ecpl_band_db_deltas(&orig_prof, &dec_prof, &geom); + for (bnd, d) in deltas.iter().enumerate() { + assert!( + d.abs() <= 3.0, + "ch{ch} narrow band {bnd}: energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + // Content ABOVE the narrow region is waveform-coded + // normally (chbwcod suppressed but end_mant for coupled + // channels is the region start... the region end < full + // bandwidth means bins above 169 are NOT coded — the + // decoder zeroes them). The fixture keeps everything + // below tc 169, so total energy still matches. + let eo: f64 = orig_prof[..geom.start_bin].iter().sum(); + let ed: f64 = dec_prof[..geom.start_bin].iter().sum(); + let lf_delta: f64 = 10.0 * (ed / eo).log10(); + assert!( + lf_delta.abs() <= 1.5, + "ch{ch}: below-region energy delta {lf_delta:+.2} dB" + ); + } + } + + #[test] + fn ecpl_roundtrip_44100() { + // fscod = 1 interplay: the coupling grid is a transform-bin + // grid, so nothing rate-specific should change — this guards + // the frame-size lookup + bit-allocation table plumbing at + // 44.1 kHz. 192 kbps @ 44.1 kHz frames. + let params = { + let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + p.sample_rate = Some(44_100); + p.channels = Some(2); + p.sample_format = Some(SampleFormat::S16); + p.bit_rate = Some(192_000); + p + }; + let mut enc = + make_encoder_with_ecpl(¶ms, EcplParams::default()).expect("44.1k ecpl encoder"); + let geom = EcplGeometry::derive(&EcplParams::default(), &DEFAULT_ECPL_BNDSTRC).unwrap(); + let frames = 8usize; + let n = frames * SAMPLES_PER_FRAME as usize; + let mut pcm = vec![0.0f32; n * 2]; + for i in 0..n { + let t = i as f32 / 44_100.0; + let lf = 0.20 * (2.0 * std::f32::consts::PI * 650.0 * t).sin(); + for ch in 0..2 { + let phase = 0.5 * ch as f32; + let s = lf + + 0.16 * (2.0 * std::f32::consts::PI * 4_000.0 * t + phase).sin() + + 0.12 * (2.0 * std::f32::consts::PI * 8_200.0 * t + phase).sin() + + 0.08 * (2.0 * std::f32::consts::PI * 13_000.0 * t + phase).sin(); + pcm[i * 2 + ch] = s * (1.0 - 0.1 * ch as f32); + } + } + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in &pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut stream = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => stream.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("44.1k ecpl encode error: {e:?}"), + } + } + assert_eq!(stream.len() % frames, 0, "uneven frame sizes"); + let frame_bytes = stream.len() / frames; + let dec = decode_all(&stream, frame_bytes); + assert_eq!(dec.len() / 2, frames * SAMPLES_PER_FRAME as usize); + let dec_f = to_f32_interleaved(&dec); + for ch in 0..2 { + let orig_prof = mdct_energy_profile(&pcm, 2, ch); + let dec_prof = mdct_energy_profile(&dec_f, 2, ch); + let deltas = ecpl_band_db_deltas(&orig_prof, &dec_prof, &geom); + let mut lo = geom.start_bin; + let mut energies = Vec::new(); + for &nb in &geom.band_bins { + let hi = (lo + nb).min(N_COEFFS); + energies.push(orig_prof[lo..hi].iter().sum::()); + lo = hi; + } + let peak = energies.iter().cloned().fold(0.0f64, f64::max); + for (bnd, d) in deltas.iter().enumerate() { + if energies[bnd] < peak * 1e-3 { + continue; + } + assert!( + d.abs() <= 3.0, + "ch{ch} 44.1k band {bnd}: energy delta {d:+.2} dB (all: {deltas:?})" + ); + } + } + } +} + +#[cfg(test)] +mod meta_tests { + use super::tests::{build_sine_pcm, decode_all}; + use super::*; + + fn encode_meta( + pcm: &[f32], + channels: usize, + bit_rate: u64, + meta: Option, + options: &[(&str, &str)], + ) -> Vec { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(channels as u16); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(bit_rate); + let mut opts = oxideav_core::CodecOptions::new(); + for (k, v) in options { + opts = opts.set(*k, *v); + } + params.options = opts; + let mut enc: Box = match meta { + Some(m) => make_encoder_with_metadata(¶ms, m).expect("typed metadata encoder"), + None => make_encoder(¶ms).expect("options encoder"), + }; + let n_samp = pcm.len() / channels; + let mut s16 = Vec::with_capacity(pcm.len() * 2); + for &v in pcm { + let q = (v * 32767.0).clamp(-32768.0, 32767.0) as i16; + s16.extend_from_slice(&q.to_le_bytes()); + } + enc.send_frame(&Frame::Audio(oxideav_core::AudioFrame { + samples: n_samp as u32, + pts: Some(0), + data: vec![s16], + })) + .unwrap(); + enc.flush().unwrap(); + let mut out = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => out.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("eac3 encode error: {e:?}"), + } + } + assert!(!out.is_empty(), "no packets produced"); + out + } + + fn full_meta() -> Eac3Metadata { + Eac3Metadata { + dialnorm: 22, + compr: Some(0xB3), + dynrng: None, + mixmd: Some(Eac3MixMetadata { + dmixmod: 2, // LoRo preferred + ltrtcmixlev: 5, + lorocmixlev: 4, + ltrtsurmixlev: 6, + lorosurmixlev: 5, + lfemixlevcod: Some(15), + pgmscl: Some(51), // 0 dB + extpgmscl: Some(45), + }), + infomd: Some(Eac3InfoMetadata { + bsmod: 2, + copyrightb: true, + origbs: false, + dsurmod: 0, + dheadphonmod: 0, + dsurexmod: 2, + audprod: Some(Eac3AudioProduction { + mixlevel: 18, + roomtyp: 1, + adconvtyp: true, + }), + sourcefscod: false, + }), + } + } + + /// Every Table E1.2 mixing + informational word configured through + /// [`Eac3Metadata`] must read back through the typed Annex E BSI + /// surface on every syncframe — 5.1 exercises the dmixmod, + /// centre/surround mix-level, LFE-mix-level and dsurexmod arms. + #[test] + fn metadata_bsi_words_roundtrip_51() { + let pcm = build_sine_pcm(6, 3); + let stream = encode_meta(&pcm, 6, 384_000, Some(full_meta()), &[]); + let fb = 1536usize; // 384 kbps @ 48 kHz + assert_eq!(stream.len() % fb, 0); + for off in (0..stream.len()).step_by(fb) { + let bsi = crate::eac3::bsi::parse(&stream[off + 2..]).expect("eac3 bsi"); + assert_eq!(bsi.dialnorm, 22); + assert_eq!(bsi.compr.expect("compr present").raw(), 0xB3); + assert_eq!(bsi.dmixmod, 2); + let ml = bsi.annex_e_mix_levels.expect("mix levels present"); + assert_eq!(ml.ltrtcmixlev, 5); + assert_eq!(ml.lorocmixlev, 4); + assert_eq!(ml.ltrtsurmixlev, 6); + assert_eq!(ml.lorosurmixlev, 5); + assert_eq!(bsi.lfemixlevcod, Some(15)); + let pg = bsi.pgmscl.expect("pgmscl present"); + assert_eq!(pg.raw(), 51); + assert_eq!(pg.decibels(), Some(0)); + assert_eq!(bsi.extpgmscl.expect("extpgmscl present").raw(), 45); + assert_eq!(bsi.bsmod, Some(2)); + assert!(bsi.dsurexmod.is_some(), "dsurexmod absent (acmod 7)"); + let ap = bsi.audio_production.expect("audprod present"); + assert_eq!(ap.mixlevel, 18); + assert_eq!(ap.roomtyp.raw(), 1); + assert!(bsi.adconvtyp.is_some()); + let ci = bsi.copyright_info.expect("copyright info present"); + assert!(ci.is_copyright_protected()); + assert!(!ci.is_original_bitstream()); + } + // The metadata-bearing stream still decodes. + let dec = decode_all(&stream, fb); + assert!(!dec.is_empty()); + } + + /// 2/0 exercises the dsurmod + dheadphonmod informational arm and + /// the front/surround-less mixing block (pgmscl only). + #[test] + fn metadata_stereo_dsurmod_dheadphon_roundtrip() { + let meta = Eac3Metadata { + mixmd: Some(Eac3MixMetadata { + pgmscl: Some(40), // −11 dB + ..Eac3MixMetadata::default() + }), + infomd: Some(Eac3InfoMetadata { + dsurmod: 2, + dheadphonmod: 2, + ..Eac3InfoMetadata::default() + }), + ..Eac3Metadata::default() + }; + let pcm = build_sine_pcm(2, 2); + let stream = encode_meta(&pcm, 2, 192_000, Some(meta), &[]); + let fb = 768usize; + assert_eq!(stream.len() % fb, 0); + for off in (0..stream.len()).step_by(fb) { + let bsi = crate::eac3::bsi::parse(&stream[off + 2..]).expect("eac3 bsi"); + // 2/0: no dmixmod / mix-level arms. + assert_eq!(bsi.dmixmod, 0xFF); + assert!(bsi.annex_e_mix_levels.is_none()); + assert_eq!(bsi.pgmscl.expect("pgmscl").decibels(), Some(-11)); + assert!(bsi.dolby_surround_mode.is_some(), "dsurmod absent"); + assert!(bsi.dheadphonmod.is_some(), "dheadphonmod absent"); + assert_eq!(bsi.bsmod, Some(0)); + } + } + + /// A 7.1 indep+dep pair carries the metadata blocks on the + /// INDEPENDENT substream only; the dependent substream keeps + /// `mixmdate = infomdate = 0` but shares dialnorm/compr. + #[test] + fn metadata_71_pair_blocks_on_indep_only() { + let meta = Eac3Metadata { + compr: Some(0x77), + ..full_meta() + }; + let pcm = build_sine_pcm(8, 3); + let stream = encode_meta(&pcm, 8, 576_000, Some(meta), &[]); + let pair = 1536 + 768; + assert_eq!(stream.len() % pair, 0, "not a whole pair stream"); + for off in (0..stream.len()).step_by(pair) { + let indep = crate::eac3::bsi::parse(&stream[off + 2..]).expect("indep bsi"); + assert_eq!(indep.frame_bytes, 1536); + assert_eq!(indep.dialnorm, 22); + assert_eq!(indep.compr.expect("indep compr").raw(), 0x77); + assert_eq!(indep.dmixmod, 2); + assert_eq!(indep.bsmod, Some(2)); + let dep = crate::eac3::bsi::parse(&stream[off + 1536 + 2..]).expect("dep bsi"); + assert_eq!(dep.frame_bytes, 768); + assert_eq!(dep.dialnorm, 22); + assert_eq!(dep.compr.expect("dep compr").raw(), 0x77); + // Blocks absent on the dependent substream. + assert_eq!(dep.dmixmod, 0xFF); + assert!(dep.annex_e_mix_levels.is_none()); + assert!(dep.pgmscl.is_none()); + assert_eq!(dep.bsmod, None); + assert!(dep.copyright_info.is_none()); + } + } + + /// The per-block §5.4.3.4 dynrng word is applied by the E-AC-3 + /// decode path's mandatory §7.7.1 line-out gain: −12.04 dB word ⇒ + /// 0.25× output. + #[test] + fn metadata_dynrng_scales_eac3_decode() { + let word = 0xC0u8; + let expected = crate::drc::dynrng_to_linear(word) as f64; // 0.25 + let pcm = build_sine_pcm(2, 4); + let fb = 768usize; + let plain = decode_all(&encode_meta(&pcm, 2, 192_000, None, &[]), fb); + let cut = decode_all( + &encode_meta( + &pcm, + 2, + 192_000, + Some(Eac3Metadata { + dynrng: Some(word), + ..Eac3Metadata::default() + }), + &[], + ), + fb, + ); + assert_eq!(plain.len(), cut.len()); + let rms = |v: &[i16]| -> f64 { + let n = v.len().max(1); + (v.iter().map(|&s| (s as f64) * (s as f64)).sum::() / n as f64).sqrt() + }; + let skip = 2 * SAMPLES_PER_BLOCK * 2; + let ratio = rms(&cut[skip..]) / rms(&plain[skip..]).max(1e-9); + let delta_db = 20.0 * (ratio / expected).log10().abs(); + assert!( + delta_db < 0.5, + "eac3 decoded dynrng gain {ratio:.4} vs authored {expected:.4} \ + (off by {delta_db:.2} dB)" + ); + } + + /// Registry `options` and the typed constructor must build the + /// same encoder — pinned byte-identical. + #[test] + fn metadata_options_build_identical_encoder() { + let pcm = build_sine_pcm(6, 2); + let typed = encode_meta(&pcm, 6, 384_000, Some(full_meta()), &[]); + let from_options = encode_meta( + &pcm, + 6, + 384_000, + None, + &[ + ("dialnorm", "22"), + ("compr", "0xB3"), + ("dmixmod", "2"), + ("ltrtcmixlev", "5"), + ("lorocmixlev", "4"), + ("ltrtsurmixlev", "6"), + ("lorosurmixlev", "5"), + ("lfemixlevcod", "15"), + ("pgmscl", "51"), + ("extpgmscl", "45"), + ("bsmod", "2"), + ("copyright", "1"), + ("origbs", "false"), + ("dsurexmod", "2"), + ("mixlevel", "18"), + ("roomtyp", "1"), + ("adconvtyp", "true"), + ], + ); + assert_eq!( + typed, from_options, + "options-driven and typed metadata constructors must emit identical bytes" + ); + } + + /// Out-of-range codepoints are rejected at construction. + #[test] + fn metadata_validation_rejects_out_of_range() { + let mut params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); + params.sample_rate = Some(48_000); + params.channels = Some(2); + let bad_metas = [ + Eac3Metadata { + dialnorm: 0, + ..Eac3Metadata::default() + }, + Eac3Metadata { + mixmd: Some(Eac3MixMetadata { + dmixmod: 3, + ..Eac3MixMetadata::default() + }), + ..Eac3Metadata::default() + }, + Eac3Metadata { + mixmd: Some(Eac3MixMetadata { + ltrtsurmixlev: 2, // 0..=2 reserved + ..Eac3MixMetadata::default() + }), + ..Eac3Metadata::default() + }, + Eac3Metadata { + mixmd: Some(Eac3MixMetadata { + pgmscl: Some(64), + ..Eac3MixMetadata::default() + }), + ..Eac3Metadata::default() + }, + Eac3Metadata { + infomd: Some(Eac3InfoMetadata { + dsurmod: 3, + ..Eac3InfoMetadata::default() + }), + ..Eac3Metadata::default() + }, + Eac3Metadata { + infomd: Some(Eac3InfoMetadata { + audprod: Some(Eac3AudioProduction { + mixlevel: 0, + roomtyp: 3, + adconvtyp: false, + }), + ..Eac3InfoMetadata::default() + }), + ..Eac3Metadata::default() + }, + ]; + for bad in bad_metas { + assert!( + make_encoder_with_metadata(¶ms, bad).is_err(), + "expected rejection: {bad:?}" + ); + } + // Options-path rejection. + params.options = oxideav_core::CodecOptions::new().set("dmixmod", "3"); + assert!(make_encoder(¶ms).is_err()); + } + + /// Decode an E-AC-3 elementary stream through the external decoder + /// binary, returning the s16le samples. `None` when the binary is + /// unavailable. + fn ffmpeg_decode(stream: &[u8], tag: &str, dec_args: &[&str]) -> Option> { + use std::process::Command; + let in_path = std::env::temp_dir().join(format!("oxideav_eac3_meta_{tag}.ec3")); + let out_path = std::env::temp_dir().join(format!("oxideav_eac3_meta_{tag}.pcm")); + std::fs::write(&in_path, stream).expect("write ec3"); + let _ = std::fs::remove_file(&out_path); + let mut cmd = Command::new("ffmpeg"); + cmd.args(["-y", "-hide_banner", "-loglevel", "error", "-f", "eac3"]); + cmd.args(dec_args); + cmd.arg("-i").arg(&in_path); + cmd.args(["-f", "s16le", "-acodec", "pcm_s16le"]); + cmd.arg(&out_path); + let status = cmd.status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = status else { + eprintln!("ffmpeg unavailable — skipping eac3 metadata black-box gate"); + return None; + }; + assert!(status.success(), "ffmpeg failed to decode ({tag})"); + let bytes = std::fs::read(&out_path).expect("ffmpeg output"); + let _ = std::fs::remove_file(&out_path); + Some( + bytes + .chunks_exact(2) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(), + ) + } + + fn interior_rms(samples: &[i16], channels: usize) -> f64 { + let skip = (SAMPLES_PER_FRAME as usize * channels).min(samples.len() / 2); + let tail = &samples[skip..]; + (tail.iter().map(|&s| (s as f64) * (s as f64)).sum::() / tail.len().max(1) as f64) + .sqrt() + } + + /// Black-box cross-validation of the metadata emission syntax and + /// the eac3-path dynrng semantics: + /// + /// 1. The external decoder binary accepts a 5.1 stream carrying the + /// full mixing + informational blocks and decodes it at the same + /// level as the block-less encode of the same PCM — a single + /// misaligned bit anywhere in the Table E1.2 walk would corrupt + /// every downstream field and collapse the decode. + /// 2. The per-block dynrng word decodes at exactly its Table 7.29 + /// gain (DRC on vs off). + #[test] + fn metadata_black_box_syntax_and_dynrng() { + let pcm = build_sine_pcm(6, 4); + let plain = encode_meta(&pcm, 6, 384_000, Some(Eac3Metadata::default()), &[]); + let meta = encode_meta(&pcm, 6, 384_000, Some(full_meta()), &[]); + let Some(dec_plain) = ffmpeg_decode(&plain, "plain", &["-drc_scale", "0"]) else { + return; + }; + let dec_meta = ffmpeg_decode(&meta, "blocks", &["-drc_scale", "0"]).unwrap(); + assert_eq!( + dec_plain.len(), + dec_meta.len(), + "metadata blocks changed the decoded sample count" + ); + let rms_plain = interior_rms(&dec_plain, 6); + let rms_meta = interior_rms(&dec_meta, 6); + assert!(rms_plain > 100.0, "plain decode suspiciously quiet"); + let level_db = 20.0 * (rms_meta / rms_plain).log10().abs(); + assert!( + level_db < 0.5, + "metadata-bearing stream decodes {level_db:.2} dB off the plain encode" + ); + + // dynrng word gain, eac3 path. + let word = 0xC0u8; + let dyn_stream = encode_meta( + &pcm, + 6, + 384_000, + Some(Eac3Metadata { + dynrng: Some(word), + ..Eac3Metadata::default() + }), + &[], + ); + let off = ffmpeg_decode(&dyn_stream, "dyn0", &["-drc_scale", "0"]).unwrap(); + let on = ffmpeg_decode(&dyn_stream, "dyn1", &["-drc_scale", "1"]).unwrap(); + let gain = interior_rms(&on, 6) / interior_rms(&off, 6).max(1e-9); + let expected = crate::drc::dynrng_to_linear(word) as f64; + let err_db = 20.0 * (gain / expected).log10().abs(); + assert!( + err_db < 0.5, + "black-box eac3 dynrng gain {gain:.4} vs authored {expected:.4} \ + (off by {err_db:.2} dB)" + ); + eprintln!( + "black-box eac3 metadata: block-bearing level delta {level_db:.3} dB, \ + dynrng gain {gain:.4} (authored {expected:.4}, delta {err_db:.3} dB)" + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/mod.rs b/crates/vendor/oxideav-ac3/src/eac3/mod.rs new file mode 100644 index 00000000..4641d723 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/mod.rs @@ -0,0 +1,167 @@ +//! Enhanced AC-3 (E-AC-3 / Dolby Digital Plus) — ATSC A/52 Annex E. +//! +//! E-AC-3 is **not** backwards-compatible with AC-3 at the bit-stream +//! level: the syncinfo loses crc1, the bsi grows new fields +//! (`strmtyp`, `substreamid`, `frmsiz`, `numblkscod`), the audio frame +//! gains a new `audfrm()` element with frame-level strategy flags, and +//! every audio block carries SPX, AHT, and enhanced-coupling fields +//! that don't exist in the base spec. The bsid value (16, or 11..15 +//! for backward-compatible variants) selects the syntax — base-AC-3 +//! decoders MUST mute on bsid > 10 per A/52 §E.2.3.1.6. +//! +//! ## Module layout +//! +//! * **[`bsi`]** — Table E1.2 parser: stream type, substream id, +//! frame size, sample-rate code (incl. fscod2 reduced rates), +//! number of blocks, channel layout, dialnorm, compression, +//! `chanmape`/`chanmap` for dependent substreams, plus the full +//! `mixmdate`/`infomdate`/`addbsi` opt-in chain. +//! * **[`audfrm`]** — Table E1.3 parser: the 11 strategy flags, +//! frame-level exponent strategies (`frmcplexpstr`, +//! `frmchexpstr`, `lfeexpstr` runs), AHT in-use flags, frame-level +//! SNR offsets, transient pre-noise + spectral-extension attenuation +//! parameters, per-block start info. Two-phase: [`audfrm::parse_with`] +//! stops at the AHT anchor when `ahte == 1`; once the dsp pre-walk has +//! produced `nchregs[ch]` / `ncplregs` / `nlferegs` from the per-block +//! exponent strategies, [`audfrm::parse_phase_b`] consumes the +//! variable-width `chahtinu` / `cplahtinu` / `lfeahtinu` bits. +//! * **[`aht`]** — Adaptive Hybrid Transform decode (§3.4). VQ +//! codebooks E4.1..E4.7 (956 × 6 i16) + `hebap` pointer table +//! (E3.1) + quantiser-bit table (E3.2) + the literal Table E3.6 +//! Q15 remap constants. [`aht::vq_lookup`] / +//! [`aht::read_scalar_aht_mantissas`] plus the §3.4.5 inverse +//! DCT-II ([`aht::idct_ii_6`] — leading constant √2 per black-box +//! validation; the printed `2` is an erratum). +//! * **[`ahtenc`]** — encoder-side AHT (§3.4, encode direction): +//! forward DCT-II, §3.4.4.1 minimum-distance VQ search, the +//! Tables E3.5/E3.6 scalar+GAQ quantiser (exact inverse of the +//! decoder's remap), per-channel `gaqmod` planning by exact bit +//! accounting, the §3.4.4 emission order, and the §3.4.3.1 +//! `hebap[]` derivation through the shared bit-allocation core. +//! * **[`ecpl`]** — enhanced-coupling sub-band / band geometry +//! (§E.2.3.3.16-19 + §E.3.5.2): the Table E3.8 begin/end sub-band +//! derivations, Table E3.9 `ecplsubbndtab[]`, Table E2.14 default +//! banding, the §E.2.3.3.19 `necplbnd` band count, and the +//! §E.3.5.5.1 per-band bin counts; the §E.2.3.3.16-26 bitstream-syntax +//! parse; the §E.3.5.5.2 / §E.3.5.5.3 parameter-processing layer +//! (Table E3.10-E3.12 amplitude / angle / chaos decode, the chaos +//! amplitude modification, per-band→per-bin expansion, the +//! angle-interpolation path); and the full §E.3.5.5.1 carrier +//! reconstruction + §E.3.5.5.4 complex synthesis +//! ([`ecpl::synthesize_block`] / [`ecpl::EcplState`]). +//! * **[`ecplenc`]** — encoder-side enhanced coupling (§E.2.3.3.16-26 / +//! §E.3.5.5, encode direction): Table E3.10 / E3.11 inverse +//! quantisers, §E.3.5.5.1 band cross-spectrum statistics, and the +//! first-coupled-channel-phase-locked carrier construction with +//! per-band gains. +//! * **[`dsp`]** — per-frame DSP: §7.4 decouple, AHT mantissa cache, +//! §3.6 spectral extension (translate → noise-blend → coordinate +//! scale + §3.6.4.2.3 SPXATTEN border notch), §3.7.2 transient +//! pre-noise processing (PCM-domain time-scaling synthesis). +//! * **[`decoder`]** — top-level per-substream decode. Routes packets +//! with `bsid ∈ {11..=16}` through BSI → audfrm phase-A → dsp +//! pre-walk → audfrm phase-B → audblk DSP → IMDCT → overlap-add → +//! §7.8 downmix. +//! * **[`encoder`]** — Annex E encoder. Indep substream for +//! 1.0 / 2.0 / 5.1 layouts (acmod ∈ {1, 2, 7} with `lfeon=1` for +//! 5.1); 7.1 input emits an indep+dep substream pair (indep +//! carries the 5.1 program, dep 0 carries Lb/Rb back surrounds +//! with chanmap bit 6 set per §E.2.3.1.7-8 / §E.3.8.2). Spectral +//! extension is available opt-in via [`encoder::make_encoder_with_spx`] +//! (incl. mixed per-channel `chinspx` via +//! `SpxParams::channel_mask`), the Adaptive Hybrid Transform +//! via [`encoder::make_encoder_with_aht`] (fbw + LFE channels, +//! §3.4 — mutually exclusive with the others), enhanced coupling +//! via [`encoder::make_encoder_with_ecpl`], and the §3.6.1 +//! mid-range-coupling + high-range-SPX combination via +//! [`encoder::make_encoder_with_spx_ecpl`]. +//! * **[`spxenc`]** — encoder-side spectral extension (§E.2.3.3 / +//! §E.3.6): geometry derivation + validation, the §3.6.4.3 +//! energy-matching coordinate targets (through the decoder-shared +//! translation plan, with the §3.6.4.2.3 attenuation notch folded +//! in when signalled), and the §E.2.3.3.11-13 exponent / mantissa / +//! master-coordinate quantiser. +//! +//! ## Known decoder gaps +//! +//! * **Enhanced coupling** (`ecplinu == 1`, §E.1.3.3.7-26 / +//! §E.2.3.3.16-26 / §E.3.5.5) decodes end-to-end. The audblk parser +//! reads the strategy + per-channel amplitude/angle/chaos coordinates +//! and decodes the enhanced-coupling channel through the shared +//! exponent / bit-allocation / mantissa path; a deferred second pass +//! (see [`dsp`]) reconstructs the §E.3.5.5.1 complex carrier `Z[k]` +//! from the previous / current / next blocks, processes the per-bin +//! amplitudes + de-correlated angles, and emits each coupled channel's +//! transform coefficients via the §E.3.5.5.4 complex product. The +//! per-step primitives + the [`ecpl::synthesize_block`] orchestration +//! are spec-derived and unit-tested in [`ecpl`]. Block 0's "previous +//! block" carrier source is now threaded from the prior frame's last +//! enhanced-coupling block (carried on [`ecpl::EcplState`], §E.3.5.5.1); +//! the prior-frame edge no longer collapses to a zero carrier. The +//! frame's last block's "next block" still uses a zero carrier (it lives +//! in a not-yet-decoded frame — streaming lookahead is out of scope). +//! Standard coupling is fully in. +//! * **Cross-frame transient pre-noise reference** (§E.3.7.1) is +//! clamped to the current frame; intra-frame transients (§E.3.7.2) +//! are fully synthesised. +//! * **Standard-coupling default banding** (§E.2.3.3.15 Table E2.12): +//! when `cplbndstrce == 0` in the first coupling block of a frame the +//! decoder now applies the `defcplbndstrc[]` default structure +//! (indexed by absolute sub-band number) instead of leaving every +//! sub-band un-merged. This was the root cause of the three +//! previously floor-bound stereo fixtures; they now decode at +//! ~91 dB PSNR (gated `MinPsnr` floors in `tests/docs_corpus.rs`). +//! * Corpus status: every multichannel / stereo E-AC-3 fixture in the +//! `tests/docs_corpus.rs` set now decodes at ~88-92 dB PSNR and is +//! CI-gated at a `MinPsnr(80.0)` floor — including +//! `eac3-5.1-side-768kbps` (~91.7 dB, promoted from the earlier +//! side-channel-glitch floor in round 365). The only remaining +//! `ReportOnly` E-AC-3 fixtures are the deliberately torture-grade +//! low-rate cases (`eac3-low-bitrate-32kbps` ~66 dB, +//! `eac3-low-rate-stereo-64kbps` ~72 dB), whose error is confined to +//! the signal attack/release blocks (the steady-state interior +//! decodes at ~85 dB+) and reflects the lossy floor at those budgets +//! rather than a decode defect (see crate `README.md`). + +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod aht; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ahtenc; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod audfrm; +pub mod bsi; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod chanmap; +pub mod decoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod dsp; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod ecpl; +pub mod ecplenc; +pub mod encoder; +pub mod spxenc; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod tables; + +// Re-exports — keep the public surface identical to the old single- +// file `eac3.rs` so external callers (the encoder integration test in +// `tests/eac3_ffmpeg.rs` and the workspace registration in +// `crate::lib::register`) don't need to change. +pub use bsi::{ + Bsi as Eac3Bsi, DrcSource, PanInfo, PremixCompression, PremixCompressionWord, + ProgramScaleFactor, BSID_BASE_AC3_MAX, EAC3_BSID, +}; +pub use decoder::{decode_eac3_packet, Eac3DecoderState}; +pub use ecplenc::EcplParams; +pub use encoder::{ + make_encoder, make_encoder_with_aht, make_encoder_with_ecpl, make_encoder_with_spx, + make_encoder_with_spx_ecpl, CODEC_ID_STR, +}; +pub use spxenc::SpxParams; diff --git a/crates/vendor/oxideav-ac3/src/eac3/spxenc.rs b/crates/vendor/oxideav-ac3/src/eac3/spxenc.rs new file mode 100644 index 00000000..64ccfff5 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/spxenc.rs @@ -0,0 +1,786 @@ +//! Encoder-side Spectral Extension (SPX) — ATSC A/52:2018 Annex E +//! §E.2.3.3.1-13 (bitstream syntax) + §E.3.6 (decode model the encoder +//! must invert). +//! +//! SPX is E-AC-3's parametric high-frequency reconstruction: the +//! encoder stops coding transform coefficients at the SPX begin +//! frequency and instead sends, per channel per band, a coordinate +//! that scales a *translated copy* of the channel's own low-frequency +//! spectrum (optionally blended with noise) so that — per §3.6.4.3 — +//! +//! > "the banded energy of the synthesized high frequency transform +//! > coefficients should match the banded energy of the high +//! > frequency transform coefficients of the original signal." +//! +//! That sentence is the whole encoder contract. This module provides +//! the pieces the bitstream emitter (`eac3::encoder`) needs: +//! +//! * [`SpxParams`] — the user-facing configuration (begin / end / copy +//! start frequency codes, noise-blend offset, explicit-vs-default +//! band structure). +//! * [`SpxGeometry`] — the derived sub-band / band / transform- +//! coefficient geometry (§E.2.3.3.5-8 + Table E3.13), validated the +//! same way the decoder validates it. +//! * [`band_coord_targets`] — the §3.6.4.3 energy-matching coordinate +//! for each band: `spxco = rms(original HF band) / +//! (rms(translated band) · 32)`, using the *same* translation plan +//! the decoder executes ([`crate::audblk::spx_translation_plan`]). +//! * [`quantise_coord`] / [`choose_mstrspxco`] — the §E.2.3.3.11-13 +//! exponent / mantissa / master-coordinate quantiser, with +//! [`decode_coord`] as the exact decoder-side inverse for tests. + +use crate::audblk::{spx_translation_plan, N_COEFFS, SPX_ATTEN_TABLE}; +use oxideav_core::{Error, Result}; + +/// Encoder-facing SPX configuration. +/// +/// Field ranges mirror the §E.2.3.3.4-6 / §E.2.3.3.10 bitstream +/// fields; [`SpxGeometry::derive`] validates the combination. +#[derive(Clone, Copy, Debug)] +pub struct SpxParams { + /// `spxbegf` (§E.2.3.3.5, 3 bits, 0..=7) — SPX begin frequency + /// code. `spx_begin_subbnd = spxbegf + 2` for `spxbegf < 6`, else + /// `spxbegf·2 − 3`; coded bandwidth ends (and the synthesized + /// region starts) at tc# `25 + 12·spx_begin_subbnd`. + pub spxbegf: u8, + /// `spxendf` (§E.2.3.3.6, 3 bits, 0..=7) — SPX end frequency code. + /// `spx_end_subbnd = spxendf + 5` for `spxendf < 3`, else + /// `spxendf·2 + 3` (7 → 17 → tc# 229, the Table E3.13 top). + pub spxendf: u8, + /// `spxstrtf` (§E.2.3.3.4, 2 bits, 0..=3) — translation copy start + /// frequency code; the copy region begins at tc# `25 + 12·spxstrtf`. + pub spxstrtf: u8, + /// `spxblnd` (§E.2.3.3.10, 5 bits, 0..=31) — noise blend offset. + /// The decoder computes `nratio = (band centre)/(spx end tc) − + /// spxblnd/32` (clamped to [0, 1]); larger values mean less noise. + /// The default 24 keeps the lower extension bands translation- + /// dominated while admitting some §E.3.6.4.2.4 noise fill in the + /// top bands. + pub spxblnd: u8, + /// When true the encoder re-evaluates `spxstrtf` per frame: it + /// scores every valid copy-start candidate (0..=3, copy region + /// non-empty) by the total coordinate saturation the frame's + /// spectra would incur (a §3.6.4.3 target above the representable + /// `0.875` ceiling means the translated source region is too weak + /// to reach the original band energy) and picks the least-saturated + /// one, preferring lower codes (larger copy regions) on ties. The + /// configured [`SpxParams::spxstrtf`] is ignored when set. This + /// matters for spectra with a hole just above the first copy + /// sub-band: a fixed copy start would translate near-silence into + /// a loud extension band and pin its coordinate at the ceiling. + pub adaptive_copy_start: bool, + /// Optional §3.6.4.2.3 spectral-extension attenuation: `Some(code)` + /// signals `spxattene = 1` in audfrm with `chinspxatten[ch] = 1` + + /// `spxattencod[ch] = code` (0..=31, a Table E3.14 row) for every + /// fbw channel. The decoder then applies the 5-tap symmetric notch + /// filter at the baseband/extension border and at every + /// translation-copy wrap point; the encoder folds the notch into + /// its §3.6.4.3 energy computation so the coordinates compensate + /// (band energy still matches). Larger codes attenuate harder. + pub atten_code: Option, + /// When true the encoder emits `spxbndstrce = 1` with an explicit + /// band structure (identical content to the Table E2.11 default); + /// when false it emits `spxbndstrce = 0` and relies on the + /// decoder's default banding. Both decode identically — the flag + /// exists so tests can pin the two syntax paths against each other. + pub explicit_band_structure: bool, + /// Mixed per-channel SPX (§E.2.3.3.3 `chinspx[ch]`): bit `ch` set + /// means fbw channel `ch` is in spectral extension. `None` (the + /// default) puts every fbw channel in SPX. Channels NOT in SPX are + /// waveform-coded to their full chbwcod-derived bandwidth (they + /// emit `chinspx[ch] = 0`, keep their `chbwcod`, and carry no SPX + /// coordinates); the SNR tuner budgets each channel at its own + /// coded bandwidth. Constraints: at least one bit for a coded + /// channel must be set, and mono (`acmod == 0x1`) cannot exclude + /// its only channel (the spec makes `chinspx[0]` implicit there). + pub channel_mask: Option, +} + +impl Default for SpxParams { + fn default() -> Self { + SpxParams { + // begin sub-band 7 → coded bandwidth ends at tc# 109 + // (≈ 10.2 kHz at 48 kHz); extension runs to tc# 229 + // (≈ 21.5 kHz) — a conventional low-rate operating point. + spxbegf: 5, + spxendf: 7, + spxstrtf: 0, + spxblnd: 24, + adaptive_copy_start: false, + atten_code: None, + explicit_band_structure: false, + channel_mask: None, + } + } +} + +/// Derived SPX geometry (§E.2.3.3.5-8 + Table E3.13), the encoder-side +/// mirror of the decoder's strategy-parse state. +#[derive(Clone, Debug)] +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub struct SpxGeometry { + /// First SPX sub-band (`spx_begin_subbnd`). + pub begin_subbnd: usize, + /// One-past-last SPX sub-band (`spx_end_subbnd`). + pub end_subbnd: usize, + /// First synthesized tc# (= coded-bandwidth end for SPX channels, + /// §E.3.3.3 `endmant = spxbandtable[spx_begin_subbnd]`). + pub begin_tc: usize, + /// One-past-last synthesized tc#. + pub end_tc: usize, + /// Translation copy region start tc# (`spxbandtable[spxstrtf]`). + pub copy_start_tc: usize, + /// Band structure over absolute sub-bands (`spxbndstrc[]`, + /// §E.2.3.3.8) — `true` merges the sub-band into the previous band. + pub bndstrc: [bool; 18], + /// Number of coordinate bands (`nspxbnds`). + pub nbnds: usize, + /// Per-band bin counts (`spxbndsztab[]`). + pub bndsztab: [usize; 18], +} + +/// Table E3.13 — lowest tc# of SPX sub-band `subbnd` (`25 + 12·s`). +#[inline] +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn spx_bandtable(subbnd: usize) -> usize { + 25 + 12 * subbnd +} + +impl SpxGeometry { + /// Derive and validate the geometry for a parameter set, using the + /// given band structure (pass + /// [`crate::eac3::dsp::DEFAULT_SPX_BNDSTRC`] for the Table E2.11 + /// default the decoder assumes when `spxbndstrce == 0`). + pub fn derive(params: &SpxParams, bndstrc: &[bool; 18]) -> Result { + if params.spxbegf > 7 || params.spxendf > 7 || params.spxstrtf > 3 || params.spxblnd > 31 { + return Err(Error::invalid( + "spxenc: field out of range (spxbegf/spxendf 0..=7, spxstrtf 0..=3, spxblnd 0..=31)", + )); + } + if params.atten_code.is_some_and(|c| c > 31) { + return Err(Error::invalid( + "spxenc: spxattencod out of range (0..=31, Table E3.14)", + )); + } + // §E.2.3.3.5-6 sub-band derivations (same arms as the decoder). + let begin_subbnd = if params.spxbegf < 6 { + params.spxbegf as usize + 2 + } else { + params.spxbegf as usize * 2 - 3 + }; + let end_subbnd = if params.spxendf < 3 { + params.spxendf as usize + 5 + } else { + params.spxendf as usize * 2 + 3 + }; + if end_subbnd <= begin_subbnd || end_subbnd > 17 { + return Err(Error::invalid( + "spxenc: SPX sub-band range invalid (end <= begin or > 17)", + )); + } + let begin_tc = spx_bandtable(begin_subbnd); + let end_tc = spx_bandtable(end_subbnd); + let copy_start_tc = spx_bandtable(params.spxstrtf as usize); + // The decoder rejects an empty copy region (`copyend <= + // copystart`); require it up front so the translation plan is + // well-defined. + if copy_start_tc >= begin_tc { + return Err(Error::invalid( + "spxenc: copy region empty (spxstrtf sub-band >= spx begin sub-band)", + )); + } + // §E.3.6.2 band sizing — identical loop to the decoder's. + let mut nbnds = 1usize; + let mut bndsztab = [0usize; 18]; + bndsztab[0] = 12; + for bnd in (begin_subbnd + 1)..end_subbnd { + if !bndstrc[bnd] { + bndsztab[nbnds] = 12; + nbnds += 1; + } else { + bndsztab[nbnds - 1] += 12; + } + } + Ok(SpxGeometry { + begin_subbnd, + end_subbnd, + begin_tc, + end_tc, + copy_start_tc, + bndstrc: *bndstrc, + nbnds, + bndsztab, + }) + } +} + +/// §3.6.4.3 energy-matching coordinate targets for one channel-block. +/// +/// `coeffs` is the channel's full-bandwidth MDCT spectrum (the encoder +/// keeps the true HF bins even though it won't code them). For each +/// SPX band this computes +/// +/// ```text +/// spxco[bnd] = rms(original coeffs over band) / +/// (rms(translated coeffs over band) · 32) +/// ``` +/// +/// using the exact decoder translation plan, so that after the decoder +/// runs translation → noise blend (which preserves band energy: +/// `sblend² + nblend² = 1` and the noise is scaled to the translated +/// band RMS) → `·spxco·32`, the synthesized band energy equals the +/// original band energy. +/// +/// Bands whose translated energy is (near) zero get coordinate 0 — +/// there is nothing to scale; the decoder will synthesize silence +/// (plus noise-blend fill scaled by the same zero RMS). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn band_coord_targets(coeffs: &[f32; N_COEFFS], geom: &SpxGeometry) -> [f32; 18] { + band_coord_targets_span(&[coeffs], geom) +} + +/// Multi-block variant of [`band_coord_targets`]: accumulates the +/// original / translated band energies across all supplied blocks +/// before forming the ratio. The encoder signals one coordinate set +/// per refresh span (`spxcoe[ch] == 1` block; the following +/// `spxcoe == 0` blocks reuse it, §E.2.3.3.9), so the coordinate must +/// energy-match the *whole span*, not just the refresh block. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn band_coord_targets_span(blocks: &[&[f32; N_COEFFS]], geom: &SpxGeometry) -> [f32; 18] { + band_coord_targets_span_atten(blocks, geom, None) +} + +/// [`band_coord_targets_span`] with the §3.6.4.2.3 attenuation folded +/// in. The decoder computes the banded RMS (which scales both the +/// noise fill and — through the coordinate — the final output) *after* +/// applying the border/wrap notch filters to the translated +/// coefficients, so when attenuation is signalled the encoder must +/// derive its coordinates from the *notched* translated energy or every +/// filtered band would come out low by the notch loss. Only the +/// extension-side taps affect the translated energy; the two +/// coded-region bins below the border are attenuated in the decoder's +/// output but are not part of any SPX band. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn band_coord_targets_span_atten( + blocks: &[&[f32; N_COEFFS]], + geom: &SpxGeometry, + atten_code: Option, +) -> [f32; 18] { + let (copy_map, wrapflag) = spx_translation_plan( + geom.copy_start_tc, + geom.begin_tc, + geom.nbnds, + &geom.bndsztab, + ); + let total: usize = geom.bndsztab[..geom.nbnds].iter().sum(); + // Extension-relative notch gain profile (unity when no attenuation): + // the 5-tap kernel [T0, T1, T2, T1, T0] sits centred one bin below + // each border/wrap bin (filter start = site - 2), so extension bins + // site+0/+1/+2 receive T2/T1/T0. Sites: the baseband/extension + // border (offset 0, unconditional) and every wrapped band start. + let mut gain = vec![1.0f64; total]; + if let Some(code) = atten_code { + let row = SPX_ATTEN_TABLE[(code & 0x1F) as usize]; + let taps = [ + row[0] as f64, + row[1] as f64, + row[2] as f64, + row[1] as f64, + row[0] as f64, + ]; + let mut apply = |site: usize| { + for (i, tap) in taps.iter().enumerate() { + // Filter start is 2 bins below the site; extension-side + // indices are site - 2 + i (negative → coded region). + let idx = site as isize - 2 + i as isize; + if idx >= 0 && (idx as usize) < total { + gain[idx as usize] *= *tap; + } + } + }; + apply(0); // baseband / extension border (§3.6.4.2.3) + let mut band_start = 0usize; + for bnd in 0..geom.nbnds { + if bnd > 0 && wrapflag[bnd] { + apply(band_start); + } + band_start += geom.bndsztab[bnd]; + } + } + let mut out = [0.0f32; 18]; + let mut offset = 0usize; + for bnd in 0..geom.nbnds { + let bandsize = geom.bndsztab[bnd]; + let mut e_orig = 0.0f64; + let mut e_trans = 0.0f64; + for coeffs in blocks { + for i in 0..bandsize { + let tc = geom.begin_tc + offset + i; + if tc < N_COEFFS { + let v = coeffs[tc] as f64; + e_orig += v * v; + } + let src = copy_map[offset + i]; + if src < N_COEFFS { + let v = coeffs[src] as f64 * gain[offset + i]; + e_trans += v * v; + } + } + } + out[bnd] = if e_trans > f64::MIN_POSITIVE && e_orig > f64::MIN_POSITIVE { + ((e_orig / e_trans).sqrt() / 32.0) as f32 + } else { + 0.0 + }; + offset += bandsize; + } + out +} + +/// Decoder-side coordinate reconstruction (§E.2.3.3.11-13 pseudo-code) +/// — the exact inverse of [`quantise_coord`], used by the emitter (to +/// know the value the decoder will apply) and by the quantiser tests. +/// +/// ```text +/// temp = (spxcoexp == 15) ? spxcomant / 4 : (spxcomant + 4) / 8 +/// spxco = temp >> (spxcoexp + 3·mstrspxco) +/// ``` +#[inline] +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn decode_coord(exp: u8, mant: u8, mstr: u8) -> f32 { + let temp = if exp == 15 { + mant as f32 / 4.0 + } else { + (mant as f32 + 4.0) / 8.0 + }; + temp * 2f32.powi(-(exp as i32 + 3 * mstr as i32)) +} + +/// Quantise one §3.6.4.3 coordinate target into the (`spxcoexp`, +/// `spxcomant`) pair for a channel whose master coordinate is `mstr`. +/// +/// The normal-form representation covers `temp ∈ {4..7}/8` (the +/// implicit-msb form: values in [0.5, 0.875] stepped by 1/8) shifted by +/// `2^-(exp + 3·mstr)` for `exp ∈ 0..=14`; `exp == 15` is the denormal +/// escape (`temp = mant/4`, admitting exact zero). Values too large to +/// represent saturate at (0, 3) — `0.875·2^(−3·mstr)` — and values too +/// small collapse into the denormal form (rounding to zero when even +/// that underflows). +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn quantise_coord(target: f32, mstr: u8) -> (u8, u8) { + let base_shift = 3 * mstr as i32; + if !target.is_finite() || target <= 0.0 { + return (15, 0); + } + // Normalise: target = m · 2^-s with m ∈ [0.5, 1). + let mut m = target; + let mut s = 0i32; + while m >= 1.0 { + m *= 0.5; + s -= 1; + } + while m < 0.5 && s < 64 { + m *= 2.0; + s += 1; + } + let exp_needed = s - base_shift; + if exp_needed < 0 { + // Larger than the representable maximum for this mstr — + // saturate at temp = 7/8, exp = 0. + return (0, 3); + } + if exp_needed >= 15 { + // Denormal escape: spxco = mant/4 · 2^-(15 + 3·mstr). + let scaled = target * 4.0 * 2f32.powi(15 + base_shift); + let mant = scaled.round().clamp(0.0, 3.0) as u8; + return (15, mant); + } + // Normal form: temp = (mant + 4)/8 ∈ {0.5, 0.625, 0.75, 0.875}. + let rounded = (m * 8.0).round() as i32; // 4..=8 + if rounded >= 8 { + // m ≈ 1.0 rounds past the top of the mantissa range; the value + // 1.0·2^-exp equals 0.5·2^-(exp-1), representable one exponent + // up (or saturating at exp 0). + if exp_needed == 0 { + return (0, 3); + } + return ((exp_needed - 1) as u8, 0); + } + ((exp_needed) as u8, (rounded - 4).clamp(0, 3) as u8) +} + +/// Choose the per-channel `mstrspxco` (§E.2.3.3.11) for a set of band +/// coordinate targets. +/// +/// `mstrspxco` adds `3·mstr` to every band exponent, extending reach +/// toward *smaller* coordinates (up to 54 dB) at the cost of lowering +/// the representable maximum (`0.875·2^-3·mstr`). Pick the largest +/// `mstr ∈ 0..=3` that (a) the largest target still fits under without +/// saturating and (b) actually helps the smallest non-zero target +/// escape the low-precision `exp == 15` denormal form. +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub fn choose_mstrspxco(targets: &[f32]) -> u8 { + let mut max_t = 0.0f32; + let mut min_shift: Option = None; // shift s of the smallest non-zero target + let mut max_shift: i32 = 0; // shift s of the largest target + for &t in targets { + if t > 0.0 && t.is_finite() { + let s = -t.log2().ceil() as i32; // t ∈ (2^-(s+1), 2^-s] + if t > max_t { + max_t = t; + max_shift = s.max(0); + } + min_shift = Some(min_shift.map_or(s, |m: i32| m.max(s))); + } + } + let Some(min_shift) = min_shift else { + return 0; // all-zero coordinates — mstr is irrelevant + }; + // (b) how much extra shift would the smallest target like, to land + // its exponent at <= 14 (normal form)? ceil((min_shift - 14)/3). + let wanted = ((min_shift - 14) + 2) / 3; + // (a) the largest target must keep exp_needed >= 0: 3·mstr <= max_shift. + let cap = max_shift / 3; + wanted.clamp(0, 3).min(cap.max(0)) as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::eac3::dsp::DEFAULT_SPX_BNDSTRC; + + // ---- geometry (§E.2.3.3.5-8 / Table E3.13) ---- + + #[test] + fn geometry_default_params() { + let p = SpxParams::default(); + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).expect("default geometry"); + // spxbegf=5 < 6 → begin_subbnd 7 → tc 109. + assert_eq!(g.begin_subbnd, 7); + assert_eq!(g.begin_tc, 109); + // spxendf=7 ≥ 3 → end_subbnd 17 → tc 229 (Table E3.13 top). + assert_eq!(g.end_subbnd, 17); + assert_eq!(g.end_tc, 229); + assert_eq!(g.copy_start_tc, 25); + // Default banding merges sub-bands 8/10/12/14/16 into their + // predecessors: sub-bands 7..17 = 10 sub-bands → 5 bands of 24. + assert_eq!(g.nbnds, 5); + assert_eq!(&g.bndsztab[..5], &[24, 24, 24, 24, 24]); + assert_eq!(g.bndsztab[..g.nbnds].iter().sum::(), 229 - 109); + } + + #[test] + fn geometry_subband_derivation_arms() { + // spxbegf ≥ 6 arm: 6 → 9, 7 → 11. + for (begf, want) in [(6u8, 9usize), (7, 11)] { + let p = SpxParams { + spxbegf: begf, + spxendf: 7, + ..SpxParams::default() + }; + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).unwrap(); + assert_eq!(g.begin_subbnd, want, "spxbegf={begf}"); + } + // spxendf < 3 arm: 0 → 5, 2 → 7; ≥ 3 arm: 3 → 9. + for (endf, want) in [(0u8, 5usize), (2, 7), (3, 9)] { + let p = SpxParams { + spxbegf: 0, + spxendf: endf, + ..SpxParams::default() + }; + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).unwrap(); + assert_eq!(g.end_subbnd, want, "spxendf={endf}"); + } + } + + #[test] + fn geometry_rejects_invalid_combinations() { + // Inverted range: begf=7 (sub-band 11) with endf=0 (sub-band 5). + let p = SpxParams { + spxbegf: 7, + spxendf: 0, + ..SpxParams::default() + }; + assert!(SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).is_err()); + // Empty copy region: strtf=3 (tc 61) with begf=0 (begin tc 49). + let p = SpxParams { + spxbegf: 0, + spxendf: 7, + spxstrtf: 3, + ..SpxParams::default() + }; + assert!(SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).is_err()); + // Out-of-range raw fields. + let p = SpxParams { + spxbegf: 8, + ..SpxParams::default() + }; + assert!(SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).is_err()); + let p = SpxParams { + spxblnd: 32, + ..SpxParams::default() + }; + assert!(SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).is_err()); + } + + #[test] + fn geometry_explicit_unmerged_structure() { + // All-false structure: every sub-band its own 12-bin band. + let p = SpxParams { + spxbegf: 5, + spxendf: 7, + ..SpxParams::default() + }; + let g = SpxGeometry::derive(&p, &[false; 18]).unwrap(); + assert_eq!(g.nbnds, 10); + assert!(g.bndsztab[..10].iter().all(|&s| s == 12)); + } + + // ---- coordinate quantiser (§E.2.3.3.11-13) ---- + + #[test] + fn quantise_coord_round_trips_through_decoder_formula() { + // Sweep coordinate magnitudes across the full normal range for + // each mstr; the decoded value must sit within the quantiser's + // worst-case relative error (mantissa step 1/8 over m ∈ + // [0.5, 1) → ≤ 1/16 / 0.5 = 12.5%). + for mstr in 0..=3u8 { + let mut c = 0.875f32 * 2f32.powi(-(3 * mstr as i32)); + while c > 2f32.powi(-(14 + 3 * mstr as i32)) { + let (exp, mant) = quantise_coord(c, mstr); + let dec = decode_coord(exp, mant, mstr); + let rel = (dec - c).abs() / c; + assert!( + rel <= 0.126, + "mstr={mstr} c={c:e}: decoded {dec:e} rel err {rel:.3}" + ); + c *= 0.83; // irregular step to hit varied mantissas + } + } + } + + #[test] + fn quantise_coord_edge_cases() { + // Zero / negative / non-finite → exact zero via the exp-15 form. + for bad in [0.0f32, -1.0, f32::NAN, f32::INFINITY] { + let (exp, mant) = quantise_coord(bad, 0); + assert_eq!((exp, mant), (15, 0)); + assert_eq!(decode_coord(exp, mant, 0), 0.0); + } + // Oversized target saturates at the representable maximum. + let (exp, mant) = quantise_coord(3.0, 0); + assert_eq!((exp, mant), (0, 3)); + assert!((decode_coord(0, 3, 0) - 0.875).abs() < 1e-6); + // Tiny target lands in the denormal exp==15 form, not zero. + // (Smallest non-zero denormal at mstr=0 is 1/4·2^-15 ≈ 7.6e-6; + // pick a value above half that step so it rounds to mant 1.) + let tiny = 6.0e-6f32; // ~2^-17.3 + let (exp, mant) = quantise_coord(tiny, 0); + assert_eq!(exp, 15); + assert!(mant > 0, "denormal form must retain a non-zero mantissa"); + let dec = decode_coord(exp, mant, 0); + assert!((dec - tiny).abs() / tiny < 0.5, "coarse but non-zero"); + // A value that underflows even the denormal form rounds to 0. + let (exp, mant) = quantise_coord(1e-9, 0); + assert_eq!((exp, mant), (15, 0)); + // m ≈ 1.0 rounding carry: 0.99·2^-4 rounds to mant 8 → one + // exponent up with mant 0 (temp 0.5): 0.5·2^-3 = 0.0625. + let c = 0.99f32 / 16.0; + let (exp, mant) = quantise_coord(c, 0); + assert_eq!((exp, mant), (3, 0)); + let dec = decode_coord(exp, mant, 0); + assert!((dec - c).abs() / c < 0.02); + } + + #[test] + fn choose_mstrspxco_balances_range() { + // Ordinary coordinates (~1/32 scale) need no master shift. + assert_eq!(choose_mstrspxco(&[0.03, 0.01, 0.005]), 0); + // All-zero → 0. + assert_eq!(choose_mstrspxco(&[0.0, 0.0]), 0); + // Very small coordinates want shift — but only as much as the + // largest coordinate tolerates. All-tiny set: full shift. + let m = choose_mstrspxco(&[2e-6, 1e-6]); + assert!(m >= 1, "tiny coordinates should engage mstrspxco, got {m}"); + // A large coordinate caps the shift at 0 regardless of small + // companions (saturation would cost more than denormal loss). + assert_eq!(choose_mstrspxco(&[0.6, 1e-6]), 0); + } + + #[test] + fn choose_mstrspxco_never_saturates_largest_target() { + // Whatever mstr is chosen, quantising the largest target must + // not hit the (0, 3) saturation clamp unless the target really + // exceeds 0.875. + for targets in [ + &[0.4f32, 1e-5, 3e-6][..], + &[0.05, 4e-6][..], + &[0.8, 0.2][..], + &[9e-4, 2e-7][..], + ] { + let mstr = choose_mstrspxco(targets); + let max = targets.iter().cloned().fold(0.0f32, f32::max); + let (exp, mant) = quantise_coord(max, mstr); + let dec = decode_coord(exp, mant, mstr); + let rel = (dec - max).abs() / max; + assert!( + rel <= 0.126, + "mstr={mstr} saturated the largest target {max:e} → {dec:e}" + ); + } + } + + // ---- §3.6.4.2.3 attenuation folding ---- + + #[test] + fn atten_fold_raises_border_band_coordinate() { + // Uniform copy region + uniform HF: without attenuation every + // band's target is (B/A)/32. With attenuation, band 0's first + // three translated bins are notched by [T2, T1, T0], so its + // translated energy drops by exactly + // (T2² + T1² + T0² + (n-3)) / n + // and the target must rise by the inverse square root. Bands + // that neither border nor wrap are unchanged. + let p = SpxParams::default(); // copy 25..109 (84 bins), spx 109..229 + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).unwrap(); + let mut coeffs = [0.0f32; N_COEFFS]; + for tc in g.copy_start_tc..g.begin_tc { + coeffs[tc] = 0.02; + } + for tc in g.begin_tc..g.end_tc { + coeffs[tc] = 0.005; + } + let code = 14u8; // Table E3.14 row [0.5, 0.25, 0.125] + let plain = band_coord_targets_span(&[&coeffs], &g); + let atten = band_coord_targets_span_atten(&[&coeffs], &g, Some(code)); + let row = SPX_ATTEN_TABLE[code as usize]; + let n = g.bndsztab[0] as f64; + let notched = + ((row[2] as f64).powi(2) + (row[1] as f64).powi(2) + (row[0] as f64).powi(2) + n - 3.0) + / n; + let want = plain[0] as f64 / notched.sqrt(); + assert!( + (atten[0] as f64 - want).abs() / want < 1e-4, + "band 0: atten target {:.6e}, want {want:.6e}", + atten[0] + ); + // Band 1 starts at translated offset 24 with copy region 84 + // bins — no wrap yet — so it is unchanged. + assert!( + (atten[1] - plain[1]).abs() / plain[1] < 1e-6, + "band 1 must be unaffected (no border, no wrap)" + ); + // Band 3 (offset 72; 72 + 24 > 84 → pre-band wrap) is notched + // at its start → its target must exceed the plain one. + assert!( + atten[3] > plain[3] * 1.001, + "wrapped band 3 must compensate the wrap-site notch" + ); + } + + #[test] + fn geometry_rejects_bad_atten_code() { + let p = SpxParams { + atten_code: Some(32), + ..SpxParams::default() + }; + assert!(SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).is_err()); + let p = SpxParams { + atten_code: Some(31), + ..SpxParams::default() + }; + assert!(SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).is_ok()); + } + + // ---- §3.6.4.3 energy-matching coordinate targets ---- + + #[test] + fn band_coord_targets_closed_form() { + // Construct a spectrum where the copy region is a constant + // amplitude A and the HF region a constant amplitude B: every + // band's translated RMS is A, original RMS is B, so the target + // is (B/A)/32 exactly. + let p = SpxParams::default(); // begin tc 109, end tc 229, copy from 25 + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).unwrap(); + let mut coeffs = [0.0f32; N_COEFFS]; + let a = 0.02f32; + let b = 0.005f32; + for tc in g.copy_start_tc..g.begin_tc { + coeffs[tc] = a; + } + for tc in g.begin_tc..g.end_tc { + coeffs[tc] = b; + } + let targets = band_coord_targets(&coeffs, &g); + for bnd in 0..g.nbnds { + let want = (b / a) / 32.0; + let got = targets[bnd]; + assert!( + (got - want).abs() / want < 1e-4, + "band {bnd}: target {got:e}, want {want:e}" + ); + } + // Bands beyond nbnds stay zero. + assert_eq!(targets[g.nbnds], 0.0); + } + + #[test] + fn band_coord_targets_zero_translated_energy() { + // Silent copy region → coordinate 0 (nothing to scale). + let p = SpxParams::default(); + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).unwrap(); + let mut coeffs = [0.0f32; N_COEFFS]; + for tc in g.begin_tc..g.end_tc { + coeffs[tc] = 0.01; + } + let targets = band_coord_targets(&coeffs, &g); + for bnd in 0..g.nbnds { + assert_eq!(targets[bnd], 0.0, "band {bnd}"); + } + } + + #[test] + fn band_coord_targets_use_decoder_translation_plan() { + // A copy region smaller than the SPX region forces wraps; put + // all the LF energy in the FIRST copy sub-band so bands whose + // translated content wraps back to it get non-zero targets while + // bands sourced from the silent tail would get zero if the wrap + // walk diverged from the decoder's. + // + // begf=0 → begin sub-band 2 (tc 49); copy region [25, 49) = 24 + // bins; strtf=0. SPX region 49..229 with default banding. + let p = SpxParams { + spxbegf: 0, + spxendf: 7, + spxstrtf: 0, + ..SpxParams::default() + }; + let g = SpxGeometry::derive(&p, &DEFAULT_SPX_BNDSTRC).unwrap(); + let mut coeffs = [0.0f32; N_COEFFS]; + for tc in 25..49 { + coeffs[tc] = 0.03; // uniform copy region + } + for tc in g.begin_tc..g.end_tc { + coeffs[tc] = 0.006; // uniform HF + } + let targets = band_coord_targets(&coeffs, &g); + // Uniform energy everywhere → every band's translated RMS is + // 0.03 regardless of wrap positions → target (0.006/0.03)/32. + let want = (0.006f32 / 0.03) / 32.0; + for bnd in 0..g.nbnds { + assert!( + (targets[bnd] - want).abs() / want < 1e-4, + "band {bnd}: {:.6e} vs {want:.6e}", + targets[bnd] + ); + } + } +} diff --git a/crates/vendor/oxideav-ac3/src/eac3/tables/aht_codebooks.rs b/crates/vendor/oxideav-ac3/src/eac3/tables/aht_codebooks.rs new file mode 100644 index 00000000..c4793573 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/tables/aht_codebooks.rs @@ -0,0 +1,995 @@ +//! AHT VQ codebooks — A/52:2018 Annex E §4 Tables E4.1 .. E4.7. +//! +//! Each table maps a `(2..9)`-bit index to a 6-tuple of 16-bit two's- +//! complement quantizer outputs that replace the corresponding 6 mantissa +//! values across blocks 0..5 for one spectral bin (per §3.4.4.1). +//! +//! These constants are spec values (not implementation source) and are +//! transcribed verbatim from Tables E4.1..E4.7 of ATSC A/52:2018 (= ETSI TS +//! 102 366 v1.4.1 Annex E §4). + +#![allow(clippy::unreadable_literal)] + +/// Table E4.1 — VQ codebook for hebap = 1. +pub const VQ_HEBAP1: [[i16; 6]; 4] = [ + /* 0 */ [7167, 4739, 1106, 4269, 10412, 4820], + /* 1 */ [-5702, -3187, -14483, -1392, -2027, 849], + /* 2 */ [633, 6199, 7009, -12779, -2306, -2636], + /* 3 */ [-1468, -7031, 7592, 10617, -5946, -3062], +]; + +/// Table E4.2 — VQ codebook for hebap = 2. +pub const VQ_HEBAP2: [[i16; 6]; 8] = [ + /* 0 */ [-12073, 608, -7019, 590, 4000, 869], + /* 1 */ [6692, 15689, -6178, -9239, -74, 133], + /* 2 */ [1855, -989, 20596, -2920, -4475, 225], + /* 3 */ [-1194, -3901, -821, -6566, -875, -20298], + /* 4 */ [-2762, -3181, -4094, -5623, -16945, 9765], + /* 5 */ [1547, 6839, 1980, 20233, -1071, -4986], + /* 6 */ [6221, -17915, -5516, 6266, 358, 1162], + /* 7 */ [3753, -1066, 4283, -3227, 15928, 10186], +]; + +/// Table E4.3 — VQ codebook for hebap = 3. +pub const VQ_HEBAP3: [[i16; 6]; 16] = [ + /* 0 */ [-10028, 20779, 10982, -4560, 798, -68], + /* 1 */ [11050, 20490, -6617, -5342, -1797, -1631], + /* 2 */ [3977, -542, 7118, -1166, 18844, 14678], + /* 3 */ [-4320, -96, -7295, -492, -22050, -4277], + /* 4 */ [2692, 5856, 5530, 21862, -7212, -5325], + /* 5 */ [-135, -23391, 962, 8115, -644, 382], + /* 6 */ [-1563, 3400, -3299, 4693, -6892, 22398], + /* 7 */ [3535, 3030, 7296, 6214, 20476, -12099], + /* 8 */ [57, -6823, 1848, -22349, -5919, 6823], + /* 9 */ [-821, -3655, -387, -6253, -1735, -22373], + /* 10 */ [-6046, 1586, -18890, -14392, 9214, 705], + /* 11 */ [-5716, 264, -17964, 14618, 7921, -337], + /* 12 */ [-110, 108, 8, 74, -89, -50], + /* 13 */ [6612, -1517, 21687, -1658, -7949, -246], + /* 14 */ [21667, -6335, -8290, -101, -1349, -22], + /* 15 */ [-22003, -6476, 7974, 648, 2054, -331], +]; + +/// Table E4.4 — VQ codebook for hebap = 4. +pub const VQ_HEBAP4: [[i16; 6]; 32] = [ + /* 0 */ [22787, 5568, -5658, -156, -506, -33], + /* 1 */ [6636, -4593, 14173, -17297, -16523, 864], + /* 2 */ [3658, 22540, 104, -1763, -84, 6], + /* 3 */ [21580, -17815, -7282, -1575, -2078, -320], + /* 4 */ [-2233, 10017, -2728, 14938, -13640, -17659], + /* 5 */ [-1564, -17738, -19161, 13735, 2757, 2951], + /* 6 */ [4520, 5510, 7393, 10799, 19231, -13770], + /* 7 */ [399, 2976, -1099, 5013, -1159, 22095], + /* 8 */ [3624, -2359, 4680, -2238, 22702, 3765], + /* 9 */ [-4201, -8285, -6810, -12390, -18414, 15382], + /* 10 */ [-5198, -6869, -10047, -8364, -16022, -20562], + /* 11 */ [-142, -22671, -368, 4391, -464, -13], + /* 12 */ [814, -1118, -1089, -22019, 74, 1553], + /* 13 */ [-1618, 19222, -17642, -13490, 842, -2309], + /* 14 */ [4689, 16490, 20813, -15387, -4164, -3968], + /* 15 */ [-3308, 11214, -13542, 13599, -19473, 13770], + /* 16 */ [1817, 854, 21225, -966, -1643, -268], + /* 17 */ [-2587, -107, -20154, 376, 1174, -304], + /* 18 */ [-2919, 453, -5390, 750, -22034, -978], + /* 19 */ [-19012, 16839, 10000, -3580, 2211, 1459], + /* 20 */ [1363, -2658, -33, -4067, 1165, -21985], + /* 21 */ [-8592, -2760, -17520, -15985, 14897, 1323], + /* 22 */ [652, -9331, 3253, -14622, 12181, 19692], + /* 23 */ [-6361, 5773, -15395, 17291, 16590, -2922], + /* 24 */ [-661, -601, 1609, 22610, 992, -1045], + /* 25 */ [4961, 9107, 11225, 7829, 16320, 18627], + /* 26 */ [-21872, -1433, 138, 1470, -1891, -196], + /* 27 */ [-19499, -18203, 11056, -516, 2543, -2249], + /* 28 */ [-1196, -17574, 20150, 11462, -401, 2619], + /* 29 */ [4638, -8154, 11891, -15759, 17615, -14955], + /* 30 */ [-83, 278, 323, 55, -154, 232], + /* 31 */ [7788, 1462, 18395, 15296, -15763, -1131], +]; + +/// Table E4.5 — VQ codebook for hebap = 5. +pub const VQ_HEBAP5: [[i16; 6]; 128] = [ + /* 0 */ [-3394, -19730, 2963, 9590, 4660, 19673], + /* 1 */ [-15665, -6405, 17671, 3860, -8232, -19429], + /* 2 */ [4467, 412, -17873, -8037, 691, -17307], + /* 3 */ [3580, 2363, 6886, 3763, 6379, -20522], + /* 4 */ [-17230, -14133, -1396, -23939, 8373, -12537], + /* 5 */ [-8073, -21469, -15638, 3214, 8105, -5965], + /* 6 */ [4343, 5169, 2683, -16822, -5146, -16558], + /* 7 */ [6348, -10668, 12995, -25500, -22090, 4091], + /* 8 */ [-2880, -8366, -5968, -17158, -2638, 23132], + /* 9 */ [-5095, -14281, -22371, 21741, 3689, 2961], + /* 10 */ [-2443, -17739, 25155, 2707, 1594, 7], + /* 11 */ [-18379, 9010, 4270, 731, -426, -640], + /* 12 */ [-23695, 24732, 5642, 612, -308, -964], + /* 13 */ [-767, 1268, 225, 1635, 173, 916], + /* 14 */ [5455, 6493, 4902, 10560, 23041, -17140], + /* 15 */ [17219, -21054, -18716, 4936, -3420, 3357], + /* 16 */ [-1390, 15488, -21946, -14611, 1339, 542], + /* 17 */ [-6866, -2254, -12070, -3075, -19981, -20622], + /* 18 */ [-1803, 11775, 1343, 8917, 693, 24497], + /* 19 */ [-21610, 9462, 4681, 9254, -7815, 15904], + /* 20 */ [-5559, -3018, -9169, -1347, -22547, 12868], + /* 21 */ [-366, 5076, -1727, 20427, -283, -2923], + /* 22 */ [-1886, -6313, -939, -2081, -1399, 3513], + /* 23 */ [-3161, -537, -5075, 11268, 19396, 989], + /* 24 */ [2345, 4153, 5769, -4273, 233, -399], + /* 25 */ [-21894, -1138, -16474, 5902, 5488, -3211], + /* 26 */ [10007, -12530, 18829, 20932, -1158, 1790], + /* 27 */ [-1165, 5014, -1199, 6415, -8418, -21038], + /* 28 */ [1892, -3534, 3815, -5846, 16427, 20288], + /* 29 */ [-2664, -11627, -4147, -18311, -22710, 14848], + /* 30 */ [17256, 10419, 7764, 12040, 18956, 2525], + /* 31 */ [-21419, -18685, -10897, 4368, -7051, 4539], + /* 32 */ [-1574, 2050, 5760, 24756, 15983, 17678], + /* 33 */ [-538, -22867, 11067, 10301, 385, 528], + /* 34 */ [-8465, -3025, -16357, -23237, 16491, 3654], + /* 35 */ [5840, 575, 11890, 1947, 25157, 6653], + /* 36 */ [6625, -3516, -1964, 3850, -390, -116], + /* 37 */ [18005, 20900, 14323, -7621, -10922, 11802], + /* 38 */ [-4857, -2932, -13334, -7815, 21622, 2267], + /* 39 */ [-579, -9431, -748, -21321, 12367, 8265], + /* 40 */ [-8317, 1375, -17847, 2921, 9062, 22046], + /* 41 */ [18398, 8635, -1503, -2418, -18295, -14734], + /* 42 */ [-2987, 15129, -3331, 22300, 13878, -13639], + /* 43 */ [5874, -19026, 15587, 11350, -20738, 1971], + /* 44 */ [1581, -6955, -21440, 2455, 65, 414], + /* 45 */ [515, -4468, -665, -4672, 125, -19222], + /* 46 */ [21495, -20301, -1872, -1926, -211, -1022], + /* 47 */ [5189, -12250, -1775, -23550, -4546, 5813], + /* 48 */ [321, -6331, 14646, 6975, -1773, 867], + /* 49 */ [-13814, 3180, 7927, 444, 19552, 3146], + /* 50 */ [-6660, 12252, -1972, 17408, -24280, -12956], + /* 51 */ [-745, 14356, -1107, 23742, -9631, -18344], + /* 52 */ [18284, -7909, -7531, 19118, 7721, -12659], + /* 53 */ [1926, 15101, -12848, 2153, 21631, 1864], + /* 54 */ [-2130, 23416, 17056, -15597, -1544, 87], + /* 55 */ [8314, -11824, 14581, -20591, 7891, -2099], + /* 56 */ [19600, 22814, -17304, -2040, 285, -3863], + /* 57 */ [-8214, -18322, 10724, -13744, -13469, -1666], + /* 58 */ [14351, 4880, -20034, 964, -4221, -180], + /* 59 */ [-24598, -16635, 19724, 5925, 4777, 4414], + /* 60 */ [-2495, 23493, -16141, 2918, -1038, -2010], + /* 61 */ [18974, -2540, 13343, 1405, -6194, -1136], + /* 62 */ [2489, 13670, 22638, -7311, -129, -2792], + /* 63 */ [-13962, 16775, 23012, 728, 3397, 162], + /* 64 */ [3038, 993, 8774, -21969, -6609, 910], + /* 65 */ [-12444, -22386, -2626, -5295, 19520, 9872], + /* 66 */ [-1911, -18274, -18506, -14962, 4760, 7119], + /* 67 */ [8298, -2978, 25886, 7660, -7897, 1020], + /* 68 */ [6132, 15127, 18757, -24370, -6529, -6627], + /* 69 */ [7924, 12125, -9459, -23962, 5502, 937], + /* 70 */ [-17056, -5373, 2522, 327, 1129, -390], + /* 71 */ [15774, 19955, -10380, 11172, -3107, 14853], + /* 72 */ [-11904, -8091, -17928, -22287, -17237, -6803], + /* 73 */ [-12862, -2172, -6509, 5927, 12458, -22355], + /* 74 */ [-497, 322, 1038, -6643, -5404, 20311], + /* 75 */ [1083, -22984, -8494, 12130, -762, 2623], + /* 76 */ [5067, 19712, -1901, -30, -325, 85], + /* 77 */ [987, -5830, 4212, -9030, 9121, -25038], + /* 78 */ [-7868, 7284, -12292, 12914, -21592, 20941], + /* 79 */ [-1630, -7694, -2187, -8525, -5604, -25196], + /* 80 */ [-6668, 388, -22535, 1526, 9082, 193], + /* 81 */ [-7867, -22308, 5163, 362, 944, -259], + /* 82 */ [3824, -11850, 7591, -23176, 25342, 23771], + /* 83 */ [-10504, 4123, -21111, 21173, 22439, -838], + /* 84 */ [-4723, 21795, 6184, -122, 1642, -717], + /* 85 */ [24504, 19887, -2043, 986, 7, -55], + /* 86 */ [-27313, -135, 2437, 259, 89, 307], + /* 87 */ [24446, -3873, -5391, -820, -2387, 361], + /* 88 */ [5529, 5784, 18682, 242, -21896, -4003], + /* 89 */ [22304, 4483, 722, -12242, 7570, 15448], + /* 90 */ [8673, 3009, 20437, 21108, -21100, -3080], + /* 91 */ [-1132, 2705, -1825, 5420, -785, 18532], + /* 92 */ [16932, -13517, -16509, -14858, -20327, -14221], + /* 93 */ [2219, 1380, 21474, -1128, 327, 83], + /* 94 */ [-2177, 21517, -3856, -14180, -204, -2191], + /* 95 */ [953, -9426, 15874, -10710, -3231, 21030], + /* 96 */ [-421, -1377, 640, -8239, -20976, 2174], + /* 97 */ [4309, 18514, -9100, -18319, -15518, 3704], + /* 98 */ [-5943, 449, -8387, 1075, -22210, -4992], + /* 99 */ [2953, 12788, 18285, 1430, 14937, 21731], + /* 100 */ [-2913, 401, -4739, -20105, 1699, -1147], + /* 101 */ [3449, 5241, 8853, 22134, -7547, 1451], + /* 102 */ [-2154, 8584, 18120, -15614, 19319, -5991], + /* 103 */ [3501, 2841, 5897, 6397, 8630, 23018], + /* 104 */ [2467, 2956, 379, 5703, -22047, -2189], + /* 105 */ [-16963, -594, 18822, -5295, 1640, 774], + /* 106 */ [2896, -1424, 3586, -2292, 19910, -1822], + /* 107 */ [-18575, 21219, -14001, -12573, 16466, 635], + /* 108 */ [-1998, -19314, -16527, 12208, -16576, -7854], + /* 109 */ [-9674, 1012, -21645, 2883, -12712, 2321], + /* 110 */ [-1005, 471, -3629, 8045, -11087, 25533], + /* 111 */ [4141, -21472, -2673, 756, -663, -523], + /* 112 */ [6490, 8531, 19289, 18949, 6092, -9347], + /* 113 */ [16965, 24599, 14024, 10072, -536, -10438], + /* 114 */ [-8147, 2145, -23028, -17073, 5451, -4401], + /* 115 */ [-14873, 20520, -18303, -9717, -11885, -17831], + /* 116 */ [-2290, -14120, 2070, 22467, 1671, 725], + /* 117 */ [-8538, 14629, 3521, -20577, 6673, 8200], + /* 118 */ [20248, 4410, -1366, -585, 1229, -2449], + /* 119 */ [7467, -7148, 13667, -8246, 22392, -17320], + /* 120 */ [-1932, 3875, -9064, -3812, 958, 265], + /* 121 */ [-4399, 2959, -15911, 19598, 4954, -1105], + /* 122 */ [18009, -9923, -18137, -3862, 11178, 5821], + /* 123 */ [-14596, -1227, 9660, 21619, 11228, -11721], + /* 124 */ [-721, -1700, 109, -2142, 61, -6772], + /* 125 */ [-24619, -22520, 5608, -1957, -1761, -1012], + /* 126 */ [-23728, -4451, -2688, -14679, -4266, 9919], + /* 127 */ [8495, -894, 20438, -13820, -17267, 139], +]; + +/// Table E4.6 — VQ codebook for hebap = 6. +pub const VQ_HEBAP6: [[i16; 6]; 256] = [ + /* 0 */ [10154, 7365, 16861, 18681, -22893, -3636], + /* 1 */ [-2619, -3788, -5529, -5192, -9009, -20298], + /* 2 */ [-5583, -22800, 21297, 7012, 745, 720], + /* 3 */ [428, -1459, 109, -3082, 361, -8403], + /* 4 */ [8161, 22401, 241, 1755, -874, -2824], + /* 5 */ [1140, 12643, 2306, 22263, -25146, -17557], + /* 6 */ [-2609, 3379, 10337, -19730, -15468, -23944], + /* 7 */ [-4040, -12796, -25772, 13096, 3905, 1315], + /* 8 */ [4624, -23799, 13608, 25317, -1175, 2173], + /* 9 */ [-97, 13747, -5122, 23255, 4214, -22145], + /* 10 */ [6878, -322, 18264, -854, -11916, -733], + /* 11 */ [17280, -12669, -9693, 23563, -16240, -1309], + /* 12 */ [5802, -4968, 19526, -21194, -24622, -183], + /* 13 */ [5851, -16137, 15229, -9496, -1538, 377], + /* 14 */ [14096, 25057, 13419, 8290, 23320, 16818], + /* 15 */ [-7261, 118, -15867, 19097, 9781, -277], + /* 16 */ [-4288, 21589, -13288, -16259, 16633, -4862], + /* 17 */ [4909, -19217, 23411, 14705, -722, 125], + /* 18 */ [19462, -4732, -1928, -11527, 20770, 5425], + /* 19 */ [-27562, -2881, -4331, 384, -2103, 1367], + /* 20 */ [-266, -9175, 5441, 26333, -1924, 4221], + /* 21 */ [-2970, -20170, -21816, 5450, -7426, 5344], + /* 22 */ [-221, -6696, 603, -9140, 1308, -27506], + /* 23 */ [9621, -8380, -1967, 9403, -1651, 22817], + /* 24 */ [7566, -5250, -4165, 1385, -990, 560], + /* 25 */ [-1262, 24738, -19057, 10741, 7585, -7098], + /* 26 */ [451, 20130, -9949, -6015, -2188, -1458], + /* 27 */ [22249, 9380, 9096, 10959, -2365, -3724], + /* 28 */ [18668, -650, -1234, 11092, 7678, 5969], + /* 29 */ [19207, -1485, -1076, -731, -684, 43], + /* 30 */ [-4973, 13430, 20139, 60, 476, -935], + /* 31 */ [-20029, 8710, 2499, 1016, -1158, 335], + /* 32 */ [-26413, 18598, -2201, -669, 3409, 793], + /* 33 */ [-4726, 8875, -24607, -9646, 3643, -283], + /* 34 */ [13303, -21404, -3691, -1184, -1970, 1612], + /* 35 */ [173, 60, 919, 1229, 6942, -665], + /* 36 */ [16377, 16991, 5341, -14015, -2304, -20390], + /* 37 */ [25334, -10609, 11947, -7653, -6363, 14058], + /* 38 */ [23929, -13259, -7226, -937, 234, -187], + /* 39 */ [6311, -1877, 12506, -1879, 18751, -23341], + /* 40 */ [621, 6445, 3354, -24274, 8406, 5315], + /* 41 */ [-3297, -5034, -4704, -5080, -25730, 5347], + /* 42 */ [-1275, -13295, -965, -23318, 1214, 26259], + /* 43 */ [-6252, 10035, -20105, 15301, -16073, 5136], + /* 44 */ [9562, -3911, -19510, 4745, 22270, -4171], + /* 45 */ [7978, -19600, 14024, -5745, -20855, 8939], + /* 46 */ [7, -4039, 991, -6065, 52, -19423], + /* 47 */ [3485, 2969, 7732, 7786, 25312, 6206], + /* 48 */ [-959, -12812, -1840, -22743, 7324, 10830], + /* 49 */ [-4686, 1678, -10172, -5205, 4294, -1271], + /* 50 */ [3889, 1302, 7450, 638, 20374, -3133], + /* 51 */ [-12496, -9123, 18463, -12343, -7238, 18552], + /* 52 */ [-6185, 8649, -6903, -895, 17109, 16604], + /* 53 */ [-9896, 28579, 2845, 1640, 2925, -298], + /* 54 */ [14968, -25988, 14878, -24012, 1815, -6474], + /* 55 */ [26107, 5166, 21225, 15873, 21617, 14825], + /* 56 */ [-21684, 16438, 20504, -14346, -7114, -4162], + /* 57 */ [28647, 90, -1572, 789, -902, -75], + /* 58 */ [-1479, 2471, -4061, 3612, -2240, 10914], + /* 59 */ [8616, 17491, 17255, -17456, 17022, -16357], + /* 60 */ [-20722, -18597, 25274, 17720, -3573, 1695], + /* 61 */ [-997, 6129, -6303, 11250, -11359, -19739], + /* 62 */ [-74, -4001, -1584, 13384, 162, -144], + /* 63 */ [-529, 21068, 7923, -11396, 422, -26], + /* 64 */ [7102, -13531, -20055, 2629, -178, -429], + /* 65 */ [9201, 1368, -22238, 2623, -20499, 24889], + /* 66 */ [-432, 6675, -266, 8723, 80, 28024], + /* 67 */ [19493, -3108, -9261, 1910, -21777, 5345], + /* 68 */ [14079, -11489, 12604, 6079, 19877, 1315], + /* 69 */ [10947, 9837, -18612, 15742, 4792, 605], + /* 70 */ [-1777, 3758, -4087, 21696, 6024, -576], + /* 71 */ [3567, -3578, 16379, 2680, -1752, 716], + /* 72 */ [-5049, -1399, -4550, -652, -17721, -3366], + /* 73 */ [-3635, -4372, -6522, -22152, 7382, 1458], + /* 74 */ [12242, 19190, 5646, -7815, -20289, 21344], + /* 75 */ [-7508, 19952, 23542, -9753, 5669, -1990], + /* 76 */ [-2275, 15438, 10907, -17879, 6497, 13582], + /* 77 */ [-15894, -15646, -4716, 6019, 24250, -6179], + /* 78 */ [-2049, -6856, -1208, 918, 17735, -69], + /* 79 */ [-3721, 9099, -16065, -23621, 5981, -2344], + /* 80 */ [7862, -8918, 24033, 25508, -11033, -741], + /* 81 */ [-12588, 19468, 14649, 15451, -21226, 1171], + /* 82 */ [2102, 1147, 2789, 4096, 2179, 8750], + /* 83 */ [-18214, -17758, -10366, -5203, -1066, -3541], + /* 84 */ [-2819, -19958, -11921, 6032, 8315, 10374], + /* 85 */ [-9078, -2100, 19431, -17, 732, -689], + /* 86 */ [-14512, -19224, -7095, 18727, 1870, 22906], + /* 87 */ [3912, 659, 25597, -4006, 9619, 877], + /* 88 */ [2616, 22695, -5770, 17920, 3812, 20220], + /* 89 */ [2561, 26847, -5245, -10908, 2256, -517], + /* 90 */ [-4974, 198, -21983, -3608, 22174, -18924], + /* 91 */ [21308, -1211, 19144, 16691, -1588, 11390], + /* 92 */ [-1790, 3959, -3488, 7003, -7107, 20877], + /* 93 */ [-6108, -17955, -18722, 24763, 16508, 3211], + /* 94 */ [20462, -24987, -20361, 4484, -5111, -478], + /* 95 */ [-6378, -1998, -10229, -561, -22039, -22339], + /* 96 */ [3047, -18850, 7586, 14743, -19862, 6351], + /* 97 */ [-5047, 1405, -9672, 1055, -21881, 11170], + /* 98 */ [3481, -9699, 6526, -16655, 22813, 21907], + /* 99 */ [-18570, 17501, 14664, 1291, 5026, 19676], + /* 100 */ [16134, -19810, -16956, -17939, -16933, 5800], + /* 101 */ [-8224, 4908, 8935, 2272, -1140, -23217], + /* 102 */ [1572, 2753, -1598, 2143, -3346, -21926], + /* 103 */ [-9832, -1060, -27818, 1214, 7289, 150], + /* 104 */ [98, 1538, 535, 17429, -23198, -901], + /* 105 */ [21340, -20146, 3297, -1744, -8207, -21462], + /* 106 */ [-4166, -4633, -17902, 5478, 1285, 136], + /* 107 */ [18713, 21003, 24818, 11421, 1282, -4618], + /* 108 */ [-3535, 7636, -265, 2141, -829, -2035], + /* 109 */ [-3184, 19713, 2775, -2, 1090, 104], + /* 110 */ [-6771, -20185, 2938, -2125, -36, 1268], + /* 111 */ [9560, 9430, 9586, 22100, 13827, 6296], + /* 112 */ [-535, -20018, 4276, -1868, -448, -17183], + /* 113 */ [-24352, 14244, -13647, -21040, 2271, 11555], + /* 114 */ [-2646, 15437, -4589, 18638, -4299, -622], + /* 115 */ [-20064, 4169, 18115, -1404, 13722, -1825], + /* 116 */ [-16359, 9080, 744, 22021, 125, 10794], + /* 117 */ [9644, -14607, -18479, -14714, 11174, -20754], + /* 118 */ [-326, -23762, 6144, 7909, 602, 1540], + /* 119 */ [-6650, 6634, -12683, 21396, 20785, -6839], + /* 120 */ [4252, -21043, 5628, 18687, 23860, 8328], + /* 121 */ [17986, 5704, -5245, -18093, -555, 3219], + /* 122 */ [6091, 14232, -5117, -17456, -19452, -11649], + /* 123 */ [-21586, 11302, 15434, 25590, 6777, -26683], + /* 124 */ [21355, -8244, 5877, -3540, 6079, -2567], + /* 125 */ [2603, -2455, 5421, -12286, -19100, 5574], + /* 126 */ [-1721, -26393, -23664, 22904, -349, 3787], + /* 127 */ [2189, -1203, 5340, 3249, -22617, 104], + /* 128 */ [-1664, -11020, -2857, -20723, -24049, 19900], + /* 129 */ [22873, -7345, -18481, -14616, -8400, -12965], + /* 130 */ [3777, 3958, 8239, 20494, -6991, -1201], + /* 131 */ [-160, -1613, -793, -8681, 573, 776], + /* 132 */ [4297, -3786, 20373, 6082, -5321, -18400], + /* 133 */ [18745, 2463, 12546, -7749, -7734, -2183], + /* 134 */ [11074, -4720, 22119, 1825, -24351, 4080], + /* 135 */ [1503, -19178, -1569, 13, -313, 375], + /* 136 */ [318, -575, 2544, 178, 102, 40], + /* 137 */ [-15996, -26897, 5008, 3320, 686, 1159], + /* 138 */ [25755, 26886, 574, -5930, -3916, 1407], + /* 139 */ [-9148, -7665, -2875, -8384, -18663, 26400], + /* 140 */ [-7445, -18040, -18396, 8802, -2252, -21886], + /* 141 */ [7851, 11773, 27485, -12847, -1410, 19590], + /* 142 */ [2240, 5947, 11247, 15980, -6499, 24280], + /* 143 */ [21673, -18515, 9771, 6550, -2730, 334], + /* 144 */ [-4149, 1576, -11010, 89, -24429, -5710], + /* 145 */ [7720, 1478, 21412, -25025, -8385, 9], + /* 146 */ [-2448, 10218, -12756, -16079, 1161, -21284], + /* 147 */ [-8757, -14429, -22918, -14812, 2629, 13844], + /* 148 */ [-7252, 2843, -9639, 2882, -14625, 24497], + /* 149 */ [-674, -6530, 414, -23333, -21343, 454], + /* 150 */ [2104, -6312, 10887, 18087, -1199, 175], + /* 151 */ [-493, -562, -2739, 118, -1074, 93], + /* 152 */ [-10011, -4075, -28071, 22180, 15077, -636], + /* 153 */ [-4637, -16408, -9003, -20418, -11608, -20932], + /* 154 */ [4815, 15892, 24238, -13634, -3074, -1059], + /* 155 */ [-6724, 4610, -18772, -15283, -16685, 23988], + /* 156 */ [15349, -674, -3682, 21679, 4475, -12088], + /* 157 */ [4756, 2593, 5354, 6001, 15063, 26490], + /* 158 */ [-23815, -17251, 6944, 378, 694, 670], + /* 159 */ [23392, -8839, -14713, 7544, -876, 11088], + /* 160 */ [3640, 3336, 22593, -3495, -2328, -113], + /* 161 */ [284, 6914, 3097, 10171, 6638, -18621], + /* 162 */ [2472, 5976, 11054, -11936, -603, -663], + /* 163 */ [16175, 16441, 13164, -4043, 4667, 7431], + /* 164 */ [19338, 15534, -6533, 1681, -4857, 17048], + /* 165 */ [17027, 532, -19064, -1441, -5130, 1085], + /* 166 */ [-12617, -17609, 2062, -25332, 19009, -16121], + /* 167 */ [10056, -21000, -13634, -2949, 15367, 19934], + /* 168 */ [-648, -1605, 10046, -1592, 13296, 19808], + /* 169 */ [-1054, 10744, 538, 24938, 9630, -9052], + /* 170 */ [-10099, 3042, -25076, -24052, 13971, 100], + /* 171 */ [6547, 6907, 7031, 10348, 23775, -17886], + /* 172 */ [-22793, -1984, -1393, -3330, 9267, 14317], + /* 173 */ [-14346, -3967, 3042, 16254, -17303, 9646], + /* 174 */ [-21393, 23628, 16773, 716, 2663, 114], + /* 175 */ [-19016, -3038, 1574, -245, 1463, -793], + /* 176 */ [22410, 23441, -14637, -530, 17310, 13617], + /* 177 */ [-11582, 7935, -13954, 23465, -24628, 26550], + /* 178 */ [-1045, 3679, -2218, 10572, 20999, -3702], + /* 179 */ [-15513, 197, 16718, -24603, 4945, 5], + /* 180 */ [10781, 4335, 26790, -9059, -16152, -2840], + /* 181 */ [16075, -24100, -3933, -6833, 12645, -7029], + /* 182 */ [2096, -25572, -8370, 6814, 11, 1178], + /* 183 */ [-11848, -583, -8889, -20543, -10471, -380], + /* 184 */ [-2487, 24777, -21639, -19341, 1660, -732], + /* 185 */ [2313, 13679, 4085, 24549, 24691, -21179], + /* 186 */ [-2366, -504, -4130, -10570, 23668, 1961], + /* 187 */ [20379, 17809, -9506, 3733, -18954, -6292], + /* 188 */ [-3856, 16802, -929, -20310, -17739, 6797], + /* 189 */ [12431, 6078, -11272, -14450, 6913, 23476], + /* 190 */ [7636, -1655, 23017, 10719, -8292, 838], + /* 191 */ [-8559, -1235, -18096, 3897, 16093, 1490], + /* 192 */ [-3586, 8276, 15165, -3791, -21149, 1741], + /* 193 */ [-4497, 21739, 2366, -278, -4792, 15549], + /* 194 */ [-23122, -13708, 7668, 16232, 24120, 15025], + /* 195 */ [-20043, 12821, -20160, 16691, -11655, -16081], + /* 196 */ [-12601, 20239, 3496, -2549, -6745, -11850], + /* 197 */ [4441, 7812, 20783, 17080, 11523, -9643], + /* 198 */ [24766, 8494, -23298, -3262, 11101, -7120], + /* 199 */ [-10107, -7623, -22152, -18303, 26645, 9550], + /* 200 */ [-25549, 477, 7874, -1538, 1123, -168], + /* 201 */ [470, 9834, -347, 23945, -10381, -9467], + /* 202 */ [-4096, -9702, -6856, -21544, 20845, 7174], + /* 203 */ [5370, 9748, -23765, -1190, 512, -1538], + /* 204 */ [-1006, -10046, -12649, 19234, -1790, -890], + /* 205 */ [15108, 23620, -15646, -2522, -1203, -1325], + /* 206 */ [-7406, -2605, 1095, -247, -473, 177], + /* 207 */ [8089, 4, 12424, -22284, 10405, -7728], + /* 208 */ [22196, 10775, -5043, 690, 534, -212], + /* 209 */ [-3153, -1418, -16835, 18426, 15821, 22956], + /* 210 */ [5681, -2229, 3196, -3414, -21817, -14807], + /* 211 */ [19, 787, 1032, 170, -8295, -645], + /* 212 */ [-882, -2319, -27105, 432, -4392, 1499], + /* 213 */ [-1354, -11819, -76, -20380, -10293, 11328], + /* 214 */ [211, -4753, -4675, -6933, -13538, 14479], + /* 215 */ [6043, 5260, -459, -462, 143, -65], + /* 216 */ [-2572, 7256, -3317, 9212, -23184, -9990], + /* 217 */ [-24882, -9532, 18874, 6101, 2429, -14482], + /* 218 */ [8314, 2277, 14192, 3512, 25881, 22000], + /* 219 */ [208, 20218, -281, -24778, -63, -1183], + /* 220 */ [1095, -6034, 2706, -21935, -2655, 563], + /* 221 */ [23, -5930, 243, -8989, 5345, 20558], + /* 222 */ [-15466, 12699, 4160, 11087, 20621, -10416], + /* 223 */ [20995, -85, -8468, 194, 1003, -9515], + /* 224 */ [-19637, -3335, -14081, 3574, -23381, -667], + /* 225 */ [-2076, 3489, -3192, -19367, 539, -1530], + /* 226 */ [7352, -15213, 22596, 19369, 1043, 16627], + /* 227 */ [-1872, -413, 1235, -5276, -3550, 21903], + /* 228 */ [7931, -2008, 16968, -6799, 29393, -2475], + /* 229 */ [-13589, 8389, -23636, -22091, -14178, -14297], + /* 230 */ [-11575, -20090, 16056, -1848, 15721, 4500], + /* 231 */ [3849, -16581, 20161, -21155, 7778, 11864], + /* 232 */ [-6547, -1273, -18837, -11218, 11636, 1044], + /* 233 */ [2528, -6691, -17917, -11362, -4894, -1008], + /* 234 */ [1241, 4260, 2319, 6111, 3485, 20209], + /* 235 */ [3014, -3048, 5316, -4539, 20831, 8702], + /* 236 */ [-1790, -14683, 278, 13956, -10065, -10547], + /* 237 */ [-22732, -7957, -1154, 13821, -1484, -1247], + /* 238 */ [-7317, -615, 13094, 18927, 9897, 1452], + /* 239 */ [2552, -2338, 3424, -4630, 11124, -19584], + /* 240 */ [-11125, -20553, -10855, -10783, -20767, 6833], + /* 241 */ [984, -15095, 5775, 25125, 5377, -19799], + /* 242 */ [517, 13272, -7458, -1711, 20612, -6013], + /* 243 */ [-21417, 13251, -20795, 13449, 17281, 13104], + /* 244 */ [-15811, -16248, 23093, -4037, -8195, 871], + /* 245 */ [582, 12571, -21129, -14766, -9187, 5685], + /* 246 */ [4318, -1776, 11425, -17763, -9921, 577], + /* 247 */ [6013, 16830, 17655, -25766, -4400, -3550], + /* 248 */ [-13744, -16541, 3636, -3330, -21091, -15886], + /* 249 */ [6565, -11147, 8649, -13114, 23345, -13565], + /* 250 */ [-2542, -9046, -7558, 29240, 3701, -383], + /* 251 */ [-10612, 24995, 1893, -8210, 20920, -16210], + /* 252 */ [5276, 16726, 10659, 19940, -4799, -19324], + /* 253 */ [-532, -9300, 27856, 4965, -241, 536], + /* 254 */ [-765, -20706, -3412, 18870, 2765, 1420], + /* 255 */ [-3059, 2708, -19022, -331, 3537, 116], +]; + +/// Table E4.7 — VQ codebook for hebap = 7. +pub const VQ_HEBAP7: [[i16; 6]; 512] = [ + /* 0 */ [-21173, 21893, 10390, 13646, 10718, -9177], + /* 1 */ [-22519, -8193, 18328, -6629, 25518, -10848], + /* 2 */ [6800, -13758, -13278, 22418, 14667, -20938], + /* 3 */ [2347, 10516, 1125, -3455, 5569, 27136], + /* 4 */ [-6617, 11851, -24524, 22937, 20362, -6019], + /* 5 */ [-21768, 10681, -19615, -15021, -8478, -2081], + /* 6 */ [-2745, 8684, -4895, 27739, 7554, -11961], + /* 7 */ [-1020, 2460, -954, 4754, -627, -16368], + /* 8 */ [-19702, 23097, 75, -13684, -2644, 2108], + /* 9 */ [4049, -2872, 5851, -4459, 22150, 12560], + /* 10 */ [-21304, -17129, -730, 7419, -11658, -10523], + /* 11 */ [11332, 1792, 26666, 23518, -19561, -491], + /* 12 */ [-17827, -16777, -13606, -14389, -22029, -2464], + /* 13 */ [1091, -5967, -7975, -16977, -20432, -21931], + /* 14 */ [18388, -1103, 1933, 13342, -17463, 18114], + /* 15 */ [22646, 17345, -9966, 17919, 18274, 698], + /* 16 */ [1484, 20297, -5754, -26515, 4941, -22263], + /* 17 */ [-2603, 4587, -5842, 18464, 8767, -2568], + /* 18 */ [-2797, -1602, 21713, 3099, -25683, 3224], + /* 19 */ [-19027, 4693, -5007, 6060, 1972, -15095], + /* 20 */ [-2189, 9516, -530, 20669, -4662, -8301], + /* 21 */ [-22325, -8887, 2529, -11352, 5476, 998], + /* 22 */ [22100, -5052, 1651, -2657, 4615, 2319], + /* 23 */ [20855, -3078, -3330, 4105, 13470, 3069], + /* 24 */ [85, 17289, 10264, -14752, 214, 90], + /* 25 */ [-26365, -18849, -19352, 19244, -10218, 9909], + /* 26 */ [-9739, 20497, -6579, -6983, 2891, -738], + /* 27 */ [20575, -15860, -22913, 6870, 76, 327], + /* 28 */ [8744, -12877, -22945, -2372, -19424, -9771], + /* 29 */ [-12886, 16183, 21084, 3821, 749, -13792], + /* 30 */ [-15995, 18399, 2391, -17661, 19484, -6018], + /* 31 */ [1423, 11734, 4051, 19290, 6857, -19681], + /* 32 */ [-5200, 9766, 18246, 2463, 18764, -4852], + /* 33 */ [-597, 19498, 1323, -9096, -308, -1104], + /* 34 */ [-3099, -25731, -15665, 25332, 4634, 2635], + /* 35 */ [19623, -2384, -7913, 11796, -9333, -14084], + /* 36 */ [2642, 26453, -21091, -10354, -1693, -1711], + /* 37 */ [22031, 21625, 11580, -22915, -4141, 129], + /* 38 */ [-6122, 3542, 915, -261, -17, -383], + /* 39 */ [1696, 6704, -1425, 20838, 857, -4416], + /* 40 */ [1423, -15280, -8550, -9667, 5210, 5687], + /* 41 */ [-4520, -613, -11683, 5618, 4230, 619], + /* 42 */ [937, -4963, -14102, -17104, -6906, -5952], + /* 43 */ [-15068, -481, -7237, -14894, 18876, 21673], + /* 44 */ [-25658, 2910, 1143, -327, -458, -995], + /* 45 */ [-9656, -819, -24900, 2804, 20225, 1083], + /* 46 */ [-1111, -3682, -1788, -19492, 966, 821], + /* 47 */ [7293, -21759, 10790, -7059, -23293, -1723], + /* 48 */ [-282, -11093, 170, -20950, -28926, 12615], + /* 49 */ [17938, 3713, -1563, 885, 5, 564], + /* 50 */ [6116, 22696, 2242, -6951, 9975, -6132], + /* 51 */ [4338, 26808, -3705, 1976, -1079, -2570], + /* 52 */ [-661, -7901, -2668, -15194, 17722, 4375], + /* 53 */ [-4174, -11053, 717, -22506, 1562, 12252], + /* 54 */ [-6405, 18334, 6103, 6983, 5956, 18195], + /* 55 */ [9851, 5370, 23604, -6861, -6569, -62], + /* 56 */ [21964, 13359, -683, 3785, 2168, 209], + /* 57 */ [-3569, -1127, -19724, -1544, 1308, -803], + /* 58 */ [-3083, 16049, -13791, -3077, 4294, 23713], + /* 59 */ [-9999, 9943, -15872, 12934, -23631, 21699], + /* 60 */ [9722, 22837, 12192, 15091, 5533, 4837], + /* 61 */ [2243, 2099, 1243, 4089, 4748, 12956], + /* 62 */ [4007, -2468, 3353, -3092, 8843, 17024], + /* 63 */ [4330, 6127, 5549, 9249, 11226, 28592], + /* 64 */ [-9586, -8825, 236, 1009, 455, -964], + /* 65 */ [6829, 19290, -1018, 200, 1821, 578], + /* 66 */ [5196, 957, 10372, 3330, -12800, -127], + /* 67 */ [-3022, -8193, -14557, 22061, 5920, 1053], + /* 68 */ [10982, 25942, -24546, -23278, -11905, -6789], + /* 69 */ [22667, -11010, 5736, 2567, 23705, -10253], + /* 70 */ [-3343, -4233, -5458, 20667, -10843, -3605], + /* 71 */ [-4131, -3612, 4575, -829, -350, -847], + /* 72 */ [-3303, 3451, -7398, -11604, 3023, 455], + /* 73 */ [3200, -9547, 3202, -22893, 11184, -26466], + /* 74 */ [-14093, -4117, 15382, 14295, -10915, -20377], + /* 75 */ [3807, -11016, 22052, 14370, -15328, -7733], + /* 76 */ [-6291, -17719, -1560, 12048, -19805, -443], + /* 77 */ [-6147, -4234, -160, 8363, 22638, 11911], + /* 78 */ [19197, 1175, 7422, -9875, -4136, 4704], + /* 79 */ [-72, -7652, -112, -11955, -3230, 27175], + /* 80 */ [3274, 5963, 7501, -17019, 866, -25452], + /* 81 */ [737, 1861, 1833, 2022, 2384, 4755], + /* 82 */ [-5217, 7512, 3323, 2715, 3065, -1606], + /* 83 */ [4247, 565, 5629, 2497, 18019, -4920], + /* 84 */ [-2833, -17920, -8062, 15738, -1018, 2136], + /* 85 */ [3050, -19483, 16930, 29835, -10222, 15153], + /* 86 */ [-11346, 118, -25796, -13761, 15320, -468], + /* 87 */ [-4824, 4960, -4263, 1575, -10593, 19561], + /* 88 */ [-8203, -1409, -763, -1139, -607, 1408], + /* 89 */ [-2203, -11415, 2021, -6388, -2600, 711], + /* 90 */ [-413, -2511, -216, -3519, -28267, 1719], + /* 91 */ [-14446, 17050, 13917, 13499, -25762, -16121], + /* 92 */ [19228, 7341, -12301, 682, -3791, -199], + /* 93 */ [-4193, 20746, -15651, 11349, 5860, -824], + /* 94 */ [-21490, -3546, -3, -1705, -3959, 9213], + /* 95 */ [15445, -1876, 2012, -19627, 16228, -4845], + /* 96 */ [-2867, -3733, -7354, -175, -20119, 11174], + /* 97 */ [-3571, -24587, 19700, 6654, 979, -654], + /* 98 */ [21820, -7430, -6639, -10767, -8362, 15543], + /* 99 */ [14827, 17977, -7204, -3409, 1906, -17288], + /* 100 */ [3525, -3947, -1415, -2798, 17648, 2082], + /* 101 */ [-6580, -15255, -17913, 1337, 15338, 21158], + /* 102 */ [6210, 9698, 15155, -24666, -22507, -3999], + /* 103 */ [-1740, -593, 1095, -7779, 25058, 5601], + /* 104 */ [21415, -432, -1658, -6898, -1438, -14454], + /* 105 */ [-6943, 700, -12139, -745, -24187, 22466], + /* 106 */ [6287, 3283, 11006, 3844, 19184, 14781], + /* 107 */ [-22502, 15274, 5443, -2808, -970, -3343], + /* 108 */ [3257, -3708, 4744, -8301, 22814, -10208], + /* 109 */ [24346, -20970, 19846, 987, -11958, -6277], + /* 110 */ [3906, -19701, 13060, -1609, 18641, 7466], + /* 111 */ [-26409, -22549, 16305, 2014, 10975, 18032], + /* 112 */ [-7039, 4655, -14818, 18739, 15789, 1296], + /* 113 */ [9310, -1681, 14667, -3326, 26535, -11853], + /* 114 */ [5728, 5917, 13400, 10020, -2236, -24704], + /* 115 */ [1741, -6727, 12695, -22009, 4080, 5450], + /* 116 */ [-2621, 9393, 21143, -25938, -3162, -2529], + /* 117 */ [20672, 18894, -13939, 6990, -8260, 15811], + /* 118 */ [-23818, 11183, -13639, 11868, 16045, 2630], + /* 119 */ [18361, -10220, 829, 856, -1010, 157], + /* 120 */ [14400, -4678, 5153, -13290, -27434, -11028], + /* 121 */ [21613, 11256, 17453, 7604, 13130, -484], + /* 122 */ [7, 1236, 573, 4214, 5576, -3081], + /* 123 */ [916, -9092, 1285, -8958, 1185, -28699], + /* 124 */ [21587, 23695, 19116, -2885, -14282, -8438], + /* 125 */ [23414, -6161, 12978, 3061, -9351, 2236], + /* 126 */ [-3070, -7344, -20140, 5788, 582, -551], + /* 127 */ [-3993, 315, -7773, 8224, -28082, -12465], + /* 128 */ [13766, -15357, 19205, -20624, 13043, -19247], + /* 129 */ [3777, -177, 8029, -1001, 17812, 5162], + /* 130 */ [-7308, -4327, -18096, -620, -1350, 14932], + /* 131 */ [14756, -1221, -12819, -14922, -547, 27125], + /* 132 */ [2234, 1708, 2764, 5416, 7986, -25163], + /* 133 */ [2873, 3636, 3992, 5344, 10142, 21259], + /* 134 */ [1158, 5379, 508, -10514, 290, -1615], + /* 135 */ [1114, 24789, 16575, -25168, -298, -2832], + /* 136 */ [-1107, -6144, -1918, -7791, -2971, -23276], + /* 137 */ [4016, 10793, 17317, -4342, -20982, -3383], + /* 138 */ [-4494, -207, -9951, -3575, 7947, 1154], + /* 139 */ [-7576, 8117, -14047, 16982, -26457, -27540], + /* 140 */ [-15164, 16096, -16844, -8886, -23720, 15906], + /* 141 */ [24922, 5680, -1874, 420, 132, 117], + /* 142 */ [-506, -19310, -198, 412, -311, 752], + /* 143 */ [-1906, 3981, -7688, 16566, -19291, -14722], + /* 144 */ [-399, -729, -3807, -4196, -12395, 7639], + /* 145 */ [3368, 2330, 9092, 23686, -10290, -1705], + /* 146 */ [-3148, 2596, -7986, 14602, -4807, 16627], + /* 147 */ [8057, 1481, 49, 17205, 24869, 7474], + /* 148 */ [-19304, -513, 11905, 2346, 5588, 3365], + /* 149 */ [-5063, -21812, 11370, 10896, 4881, 261], + /* 150 */ [4794, 20577, 5109, -6025, -8049, -1521], + /* 151 */ [8125, -14756, 20639, -14918, 23941, -3650], + /* 152 */ [12451, 1381, 3613, 8687, -24002, 4848], + /* 153 */ [6726, 10643, 10086, 25217, -25159, -1065], + /* 154 */ [6561, 13977, 2911, 21737, 16465, -26050], + /* 155 */ [-1776, 2575, -19606, -16800, 3032, 6679], + /* 156 */ [15012, -17910, -8438, -21554, -27111, 11808], + /* 157 */ [3448, -924, -15913, -1135, 5126, -20613], + /* 158 */ [7720, 2226, 17463, 5434, 28942, 17552], + /* 159 */ [1246, 15614, -11743, 24618, -17539, 3272], + /* 160 */ [3215, 17950, 2783, -722, -22672, 5979], + /* 161 */ [-5678, -3184, -26087, 26034, 6583, 3302], + /* 162 */ [20310, -3555, -2715, -444, -1487, 1526], + /* 163 */ [-20640, -21970, -12207, -25793, 8863, -1036], + /* 164 */ [17888, 570, -16102, 8329, -2553, 15275], + /* 165 */ [-2677, 9950, -1879, 16477, -12762, -29007], + /* 166 */ [-120, -2221, 219, 97, 365, 35], + /* 167 */ [1270, -718, 1480, -2689, 1930, -7527], + /* 168 */ [1896, 8750, 1906, 18235, -12692, -6174], + /* 169 */ [-3733, 13713, -9882, -15960, -1376, -7146], + /* 170 */ [-10600, 8496, 15967, -8792, 7532, 20439], + /* 171 */ [3041, -13457, 1032, -26952, 5787, 24984], + /* 172 */ [-4590, -8220, -9322, -6112, -17243, 25745], + /* 173 */ [-17808, 6970, 3752, 626, -114, 2178], + /* 174 */ [4449, -4862, 7054, -5404, 4738, -2827], + /* 175 */ [4922, -651, 18939, -9866, 848, 1886], + /* 176 */ [-336, -5410, 7234, 20444, -9583, -600], + /* 177 */ [781, -19474, -12648, 6634, 1414, 450], + /* 178 */ [-3399, -16770, 11107, 13200, -5498, 21663], + /* 179 */ [-3265, 4859, -5961, 7530, -10837, 28086], + /* 180 */ [10350, -12901, 25699, 25640, -639, 351], + /* 181 */ [1163, 18763, -5466, -15087, -145, -1377], + /* 182 */ [-14477, 27229, -31383, -32653, 21439, -2894], + /* 183 */ [15420, 18823, 22128, 19398, 22583, 13587], + /* 184 */ [-10674, 10710, 5089, -4756, 909, -20760], + /* 185 */ [-12948, -20660, 7410, 2722, 3427, 11585], + /* 186 */ [-1105, 18374, 19731, -9650, 22442, 19634], + /* 187 */ [-296, -6798, -14677, 21603, 19796, 21399], + /* 188 */ [-19350, -7501, 25446, 13144, 8588, -25298], + /* 189 */ [3092, -10618, 20896, 9249, -3326, 1796], + /* 190 */ [-811, 1449, 3106, 4748, 12073, -14262], + /* 191 */ [-20720, 14275, -4332, -25838, -5781, -21149], + /* 192 */ [-5132, 10554, -14020, -22150, 2840, -554], + /* 193 */ [25533, 17648, 14886, -21074, 2459, 25142], + /* 194 */ [-9370, -1788, -12862, -5870, -25811, -11023], + /* 195 */ [6698, 819, 10313, 166, 27581, 523], + /* 196 */ [101, -19388, 3413, 9638, 64, 806], + /* 197 */ [-2742, -17931, -2576, 22818, 8553, 1126], + /* 198 */ [2972, 15203, 1792, 25434, -5728, -17265], + /* 199 */ [-1419, 1604, 4398, 11452, 1731, 23787], + /* 200 */ [-5136, 4625, -10653, 27981, 9897, -2510], + /* 201 */ [-10528, -28033, 2999, -1530, -832, -830], + /* 202 */ [-11133, -12511, 22206, -7243, -23578, -21698], + /* 203 */ [16935, -21892, 1861, -9606, 9432, 19026], + /* 204 */ [10277, 9516, 26815, 2010, -4943, -9080], + /* 205 */ [5547, -2210, 14270, -15300, -19316, 1822], + /* 206 */ [-4850, -783, -8959, -3076, -20056, -3197], + /* 207 */ [8232, -2794, -17752, 13308, 3229, -991], + /* 208 */ [-12237, -6581, 10315, -9552, 2260, -20648], + /* 209 */ [-7000, 5529, -7553, -7490, -10342, -10266], + /* 210 */ [3641, 19479, -5972, -19097, -18570, 12805], + /* 211 */ [1283, -4164, 4198, -28473, -2498, 1866], + /* 212 */ [16047, 26826, -13053, -6316, 985, -1597], + /* 213 */ [-403, 13680, 6457, 25070, 27124, -20710], + /* 214 */ [-18070, -1790, -24986, 5953, -954, 26600], + /* 215 */ [-24224, -15383, 24788, 1953, -1136, 187], + /* 216 */ [-2289, 12505, -20738, -904, 18324, 21258], + /* 217 */ [2658, -6140, 16179, 22276, -556, 2154], + /* 218 */ [-6087, 13950, -25682, -27713, 4049, -4795], + /* 219 */ [-21452, 26473, 19435, -9124, 895, 303], + /* 220 */ [-22200, -26177, -6026, 24729, -22926, -9030], + /* 221 */ [-14276, -15982, 23732, -22851, 9268, -3841], + /* 222 */ [29482, 21923, -6213, 1679, -2059, -1120], + /* 223 */ [-435, 9802, -3891, 12359, -4288, -18971], + /* 224 */ [19768, -86, 2467, 1990, -1021, -5354], + /* 225 */ [20986, -8783, -5329, -23562, -4730, 2673], + /* 226 */ [-5095, 5605, -4629, 19150, 26037, -12259], + /* 227 */ [972, 6858, 4551, 27949, -4025, -2272], + /* 228 */ [6075, -3260, -4989, -373, -1571, -3730], + /* 229 */ [-7256, -12992, -8820, -5109, 23054, 5054], + /* 230 */ [920, 2615, 7912, -7353, -4905, 20186], + /* 231 */ [-250, 5454, 3140, 6928, -18723, -2051], + /* 232 */ [-10299, -4372, 19608, 4879, -661, -1885], + /* 233 */ [14816, -8603, -19815, 6135, -21210, 14108], + /* 234 */ [-11945, -2223, 5018, 11892, 22741, 406], + /* 235 */ [-13184, -2613, -13256, -22433, -12482, -8380], + /* 236 */ [17066, 25267, -2273, 5056, -342, 145], + /* 237 */ [8401, -17683, 19112, 10615, -19453, 17083], + /* 238 */ [20821, -5700, 12298, -25598, 10391, 7692], + /* 239 */ [4550, 15779, 17338, -19379, -4768, 1206], + /* 240 */ [-7723, 10836, -27164, -11439, 6835, -1776], + /* 241 */ [2542, 3199, 4442, 17513, -3711, -914], + /* 242 */ [20960, -16774, -5814, 11087, -70, 22961], + /* 243 */ [3305, 2919, 6256, -4800, -20966, -3230], + /* 244 */ [5924, -16547, 2183, 2733, 3446, -23306], + /* 245 */ [-6061, -194, -13852, -10971, 19488, 1029], + /* 246 */ [4467, -5964, -19004, 1519, -359, 855], + /* 247 */ [-1581, -7607, 22070, -11580, -10032, 17102], + /* 248 */ [-12412, 2553, 4324, 22500, 5751, 12170], + /* 249 */ [-25127, 17996, -6384, 1180, 1182, 9622], + /* 250 */ [23462, -8471, -4392, -2669, 7638, -16835], + /* 251 */ [-5511, -2887, -10757, -20883, 7246, 1053], + /* 252 */ [2703, -20602, -7554, 7516, -7740, 5868], + /* 253 */ [20670, 21901, 457, 14969, -17657, -11921], + /* 254 */ [3603, -1595, -2177, -157, -43, 605], + /* 255 */ [2513, 8954, 10527, 22559, -16100, -16041], + /* 256 */ [6002, 4951, 6795, -4862, -22400, 18849], + /* 257 */ [7590, -1693, -24688, -3404, 14169, 1214], + /* 258 */ [-4398, -6663, -6870, -10083, -24596, 9253], + /* 259 */ [10468, 17751, -7748, 147, -6314, 4419], + /* 260 */ [16187, -16557, -4119, 4302, 7625, 5409], + /* 261 */ [3303, 2735, 7458, -19902, -2254, -3702], + /* 262 */ [-2077, 21609, 14870, 12545, -6081, -1764], + /* 263 */ [4678, 11740, 2859, 6953, 1919, -3871], + /* 264 */ [3522, -21853, -2469, -10453, 18893, -10742], + /* 265 */ [3759, -10191, -4866, -2659, -17831, -1242], + /* 266 */ [14991, 9351, 11870, -1573, -4848, 22549], + /* 267 */ [9509, -27152, 10734, 20851, -26185, -17878], + /* 268 */ [-7170, -1392, -19495, 12746, 8198, -1988], + /* 269 */ [1883, 28158, -846, -7235, 249, 233], + /* 270 */ [-7200, 669, -371, -2948, 23234, -5635], + /* 271 */ [3141, 288, 3223, -1258, -98, -27607], + /* 272 */ [17373, -23235, 5110, -11199, -2574, -11487], + /* 273 */ [-4928, 1518, -5456, 670, -18278, 1951], + /* 274 */ [10334, -19865, -4649, 361, -160, -923], + /* 275 */ [18732, 14264, -3155, -7485, -3328, 5959], + /* 276 */ [-3614, 21077, 7276, 3536, 8121, -1528], + /* 277 */ [-8422, 500, -19182, 18929, 26392, -1039], + /* 278 */ [15639, 25668, 8375, 1903, 1945, -11979], + /* 279 */ [-2716, 3389, 26850, -4587, 1803, 22], + /* 280 */ [1177, -655, 1233, -2128, 7844, 1767], + /* 281 */ [-761, 8209, -19290, -4593, 1923, -343], + /* 282 */ [-689, -3530, -3267, -3804, -2753, 18566], + /* 283 */ [-2110, 1962, -1353, 16643, 2765, -23102], + /* 284 */ [-433, 4905, 302, 13016, 15933, -5905], + /* 285 */ [3203, 4126, 11181, -5496, -2529, -1160], + /* 286 */ [-1091, -6469, -1415, 5682, -268, 583], + /* 287 */ [-9405, -19572, 6216, 1658, 993, -75], + /* 288 */ [-1695, -4504, -2289, -4088, -6556, -16577], + /* 289 */ [4760, -892, -10902, 6516, 24199, -6011], + /* 290 */ [-253, 1000, 63, -81, -115, -382], + /* 291 */ [-1333, 24224, -698, -4667, -2801, -19144], + /* 292 */ [-876, -28866, -21873, 12677, -6344, 3235], + /* 293 */ [16847, 21145, -26172, -3183, -396, 230], + /* 294 */ [18296, -7790, -12857, -679, -1473, 5], + /* 295 */ [-10488, 11429, 25805, -1122, 1401, -438], + /* 296 */ [3782, -7429, 26720, 17567, 19257, 12542], + /* 297 */ [6332, -746, 12789, 9316, -22542, -5354], + /* 298 */ [3418, -22728, 26978, 18303, 1076, 956], + /* 299 */ [-27315, -2988, 920, 235, 2233, 81], + /* 300 */ [6199, 5296, 16093, 14768, -8429, -1112], + /* 301 */ [-6432, 19244, 9921, -3253, 1278, -954], + /* 302 */ [24213, 2049, -22931, 2585, -2410, -4216], + /* 303 */ [9286, 14282, -19735, -3985, -2344, 1028], + /* 304 */ [-20128, 17993, -9458, 23012, -16983, 8625], + /* 305 */ [-6896, -20730, 3762, 17415, 22341, 19024], + /* 306 */ [842, 24181, 25062, -5839, -78, 937], + /* 307 */ [-621, 19722, -24204, -1962, -14854, -56], + /* 308 */ [22766, -5119, 17365, 23868, -19480, -6558], + /* 309 */ [-2158, 17490, -21435, 3340, -12819, -20295], + /* 310 */ [-9621, 17325, 715, 2265, -4123, -492], + /* 311 */ [9156, 12947, 27303, -21175, -6072, -9457], + /* 312 */ [-13164, -23269, -14006, -4184, 6978, 2], + /* 313 */ [938, -13381, 3520, -24297, 22902, 19589], + /* 314 */ [-4911, -19774, 19764, -9310, -12650, 3819], + /* 315 */ [-5462, -4249, -6987, -6260, -13943, -25150], + /* 316 */ [9341, 10369, -13862, -6704, 22556, -519], + /* 317 */ [6651, 18768, -4855, 12570, 14730, -10209], + /* 318 */ [-823, 18119, 398, -1582, -116, -363], + /* 319 */ [-6935, -12694, -28392, 8552, 6961, -239], + /* 320 */ [-2602, -4704, -1021, 2015, 5129, 23670], + /* 321 */ [-12559, -8190, -25028, 18544, 14179, 1663], + /* 322 */ [3813, 21036, -9620, -5051, -1800, -1087], + /* 323 */ [-22057, 16675, 14960, 9459, 2786, 16991], + /* 324 */ [-26040, -19318, -6414, 1104, 5798, -18039], + /* 325 */ [-1737, 24825, 10417, -11087, 896, -5273], + /* 326 */ [-1855, 11661, -2803, 24809, -21435, -19792], + /* 327 */ [-23473, -16729, -5782, 5643, 2636, 4940], + /* 328 */ [-1724, 4388, -26673, -13695, 10570, -25895], + /* 329 */ [15358, -19496, 26242, -18493, 1736, 8054], + /* 330 */ [5684, 20890, 4091, -19100, -14588, -10468], + /* 331 */ [17260, -16291, 14859, -17711, -19174, 12435], + /* 332 */ [-27185, -12573, 6743, -562, 976, -257], + /* 333 */ [12395, -8618, -22248, -19843, 11013, 7762], + /* 334 */ [3799, 11853, -27622, -8473, 1089, -1495], + /* 335 */ [4141, -2182, -26720, -735, -774, 1469], + /* 336 */ [3125, 13762, 4606, 29257, 18771, -9958], + /* 337 */ [-17465, -9445, -17562, -2530, -6435, -3726], + /* 338 */ [-1742, 4351, -6841, -19773, 9627, -10654], + /* 339 */ [7251, 3525, 10835, 5601, 25198, -23348], + /* 340 */ [-10300, -17830, 631, 11640, 2044, -20878], + /* 341 */ [-873, -8502, -1063, -15674, -10693, 14934], + /* 342 */ [-15957, 28137, 5268, 477, -1053, 1158], + /* 343 */ [-1495, -8814, -5764, -24965, 25988, 7907], + /* 344 */ [-1038, -114, -2308, -1319, -6480, 1472], + /* 345 */ [4895, -17897, -25850, 5301, -188, 1581], + /* 346 */ [3200, 17225, 4346, 22101, -18543, 22028], + /* 347 */ [-10250, 545, -10932, 2276, -28070, 8118], + /* 348 */ [15343, 2329, 9316, 20537, 14908, 21021], + /* 349 */ [6329, 6130, -24508, 837, -8637, -5844], + /* 350 */ [7386, -501, 10503, 20131, 11435, -4755], + /* 351 */ [-2745, 24174, -9274, 15273, -8389, -5835], + /* 352 */ [2992, -2864, 6048, -7473, 11687, -19996], + /* 353 */ [-883, -11954, -9976, -21829, -4436, -27178], + /* 354 */ [3458, 19626, 1280, 2597, 19849, 5255], + /* 355 */ [-5315, 19133, -14518, -8946, 13749, -1352], + /* 356 */ [18642, 17655, 11001, 6817, -18418, 6336], + /* 357 */ [-1697, 2244, -4640, 3948, -12890, -5273], + /* 358 */ [20428, 10542, 4170, -1012, 19439, 21691], + /* 359 */ [-2943, -19735, -4208, 1320, 909, -8897], + /* 360 */ [9351, -8066, -2618, -12933, 26582, 3507], + /* 361 */ [9705, -22628, 8311, 8167, -13293, 5608], + /* 362 */ [3222, 3749, -1508, 165, -52, -196], + /* 363 */ [102, -22744, -8832, 903, -11421, -14662], + /* 364 */ [-120, 5998, 19765, 13401, 3628, 5197], + /* 365 */ [8528, 5827, -1066, 774, -39, -166], + /* 366 */ [9411, -9476, 9581, -13004, 24456, 24900], + /* 367 */ [17878, 2235, -21639, 20478, 4716, -7190], + /* 368 */ [-2482, 9511, 1611, -21943, 14230, -1289], + /* 369 */ [9288, -2291, 23215, -3452, -10842, 11], + /* 370 */ [9496, 3041, 5130, -3890, -21219, -22589], + /* 371 */ [14262, -9838, 20195, 14019, 91, -17200], + /* 372 */ [-18591, 980, 17, 821, 120, -574], + /* 373 */ [12285, -19269, 13742, 16373, -161, 6025], + /* 374 */ [-3364, 1530, -4005, 2454, -10872, -23839], + /* 375 */ [105, 5085, -260, 5790, -588, 19170], + /* 376 */ [4121, 4169, 13439, 14644, 20899, 7434], + /* 377 */ [-175, 13101, -3704, 23233, 3907, 10106], + /* 378 */ [-6101, 23467, 5204, -1341, 1599, 13174], + /* 379 */ [-3217, -3494, 15117, -8387, -11762, -4750], + /* 380 */ [1146, 4675, -19378, 14917, -5091, 249], + /* 381 */ [-21506, 10136, -16473, -13305, 18382, -8601], + /* 382 */ [628, 2447, 3344, 3130, -5115, 119], + /* 383 */ [17900, -22422, -17633, 21967, -16293, -7676], + /* 384 */ [16863, 24214, 5612, -3858, -809, 3822], + /* 385 */ [-2291, 10091, -2360, -25109, -1226, 312], + /* 386 */ [2957, 11256, 26745, -13266, -3455, -1128], + /* 387 */ [-19762, -2708, 4604, 6355, 1638, 25501], + /* 388 */ [-19593, -7753, 3159, -85, -489, -1855], + /* 389 */ [814, 12510, 19077, -4681, -2610, -1474], + /* 390 */ [-23408, -19027, 8137, 19878, 7912, -282], + /* 391 */ [839, -19652, 11927, 27278, -3211, 2266], + /* 392 */ [4020, -1110, 8226, -1274, 20922, 25060], + /* 393 */ [26576, 325, -8693, -232, -2218, -699], + /* 394 */ [-11293, -4200, 1805, -6673, -22940, -1339], + /* 395 */ [-2005, -15886, -1047, -27687, -13235, 14370], + /* 396 */ [-22073, 1949, 13175, -15656, -1846, 8055], + /* 397 */ [3039, 12025, 7132, -24632, 413, -2347], + /* 398 */ [-24048, -206, 12459, -6654, -417, -10091], + /* 399 */ [18179, -23688, -20515, -16396, 7230, 763], + /* 400 */ [5659, -5085, 13878, -23729, -11077, -19587], + /* 401 */ [11340, 501, 25040, 7616, -19658, 1605], + /* 402 */ [-26650, 8878, 10544, 417, 1299, 261], + /* 403 */ [14460, 11369, -3263, 9990, 8194, 18111], + /* 404 */ [1355, -20838, -9196, -16060, -8559, -730], + /* 405 */ [-1918, -20937, -18293, -2461, -2651, 4316], + /* 406 */ [-2810, 24521, -10996, -25721, 308, -1234], + /* 407 */ [-9075, -17280, -1833, -29342, -24213, -16631], + /* 408 */ [-2843, 10165, -5339, -2888, 21858, -21340], + /* 409 */ [-15832, 14849, -23780, 5184, 10113, -20639], + /* 410 */ [-19535, -11361, 8413, 1486, -23658, -5759], + /* 411 */ [-7512, 1027, -20794, 13732, 19892, -21934], + /* 412 */ [-12132, -7022, -19175, -8840, 22125, -16490], + /* 413 */ [1937, 5210, -6318, -23788, 13141, 11082], + /* 414 */ [-205, 6036, -380, 8658, -233, 28020], + /* 415 */ [-5523, 7477, 7635, 23595, 9763, -2590], + /* 416 */ [21658, -28313, -3086, -300, -1032, 1744], + /* 417 */ [-22352, 16646, 208, 6665, -17400, -3028], + /* 418 */ [18482, 9336, -2737, -19372, 407, -4389], + /* 419 */ [-4913, -17370, 18819, -17654, 13416, 15232], + /* 420 */ [7749, 6368, 23135, -18174, 7584, -4248], + /* 421 */ [-1489, -6523, 586, -10157, 14964, 25568], + /* 422 */ [3844, -6156, 4897, -13045, -22526, 5647], + /* 423 */ [-8491, -2105, -24774, 905, -9326, 1456], + /* 424 */ [-3040, -1476, 1166, -4428, 11236, 9204], + /* 425 */ [3397, -1451, 13598, -15841, 24540, 5819], + /* 426 */ [8483, -2993, 21547, -16916, 7741, 24018], + /* 427 */ [-14932, -23758, -5332, -6664, -4497, 13267], + /* 428 */ [19379, 12916, -2142, -737, 21100, -22101], + /* 429 */ [3393, -4629, 5735, -18913, -6969, 2687], + /* 430 */ [1148, -16147, -21433, -28095, -630, -14449], + /* 431 */ [7300, 672, 18530, -17452, -10149, 351], + /* 432 */ [11356, -10974, 17212, 4624, 145, 17791], + /* 433 */ [-711, -3479, -2238, 15887, 2027, 0], + /* 434 */ [-28048, 1794, -593, -2758, -21852, 11535], + /* 435 */ [-19683, 4937, 22004, 21523, -3148, 1790], + /* 436 */ [813, 8231, 2633, 11981, -3043, 22201], + /* 437 */ [8952, -24760, -690, 14873, -2366, -5372], + /* 438 */ [8406, -5439, -274, -642, -145, 778], + /* 439 */ [-6605, 7258, 20780, -23507, -18625, 22782], + /* 440 */ [-22896, -25488, 10020, -1614, 1508, -1393], + /* 441 */ [7607, 407, -24678, -16385, -1804, -4699], + /* 442 */ [-10592, -19139, 10462, -3747, 8721, -6919], + /* 443 */ [13010, 5292, -6230, -4884, -20904, -1797], + /* 444 */ [16891, -13770, -465, 19343, -10741, -12959], + /* 445 */ [25193, -14799, -5681, -521, -321, -1211], + /* 446 */ [6917, -3093, 20183, -26903, -12026, 1295], + /* 447 */ [305, 1992, 19457, -985, 25, -521], + /* 448 */ [6707, -3698, 8365, -8687, 21921, -27166], + /* 449 */ [4668, 5997, 7117, 11696, 24401, -10794], + /* 450 */ [744, -9416, 19893, 1963, 7922, -9824], + /* 451 */ [3430, 21282, -1736, 10844, 8821, 27015], + /* 452 */ [-8813, 1521, -24038, 1651, 7838, -1208], + /* 453 */ [3911, -11221, 3273, -12541, 7168, 18402], + /* 454 */ [21642, 9117, -11536, -5256, 7077, 2382], + /* 455 */ [100, 3817, -6713, 1244, 1518, -321], + /* 456 */ [7946, -18670, 10667, -4866, 727, 776], + /* 457 */ [-15883, -8150, -2087, 22739, 1567, -3482], + /* 458 */ [4380, -2735, 8469, -7025, -11424, 1317], + /* 459 */ [26970, 4393, 7665, 17561, -714, 650], + /* 460 */ [-16191, -835, 8365, 1795, -14314, 16297], + /* 461 */ [4504, -10048, 7662, -26690, -17428, 2580], + /* 462 */ [48, -3984, 564, -5871, 2658, -18658], + /* 463 */ [12579, -26016, -15642, 2672, -1347, -887], + /* 464 */ [-4950, 4208, -6811, 2569, -20621, -8658], + /* 465 */ [-1836, -14818, -5571, -23322, -14800, 25867], + /* 466 */ [5434, -28139, -2357, -2883, -570, 2431], + /* 467 */ [13096, -2771, 24994, -12496, -24723, -1025], + /* 468 */ [-5676, -4339, 1908, 18628, -21323, 17366], + /* 469 */ [27660, -27897, -15409, 1436, -7112, -2241], + /* 470 */ [8019, 3847, 24568, -469, 9674, 10683], + /* 471 */ [-903, -10149, 1801, -21260, 4795, -8751], + /* 472 */ [1122, -9582, 2625, 22791, 956, 882], + /* 473 */ [7876, 19075, -9900, -24266, 7496, 9277], + /* 474 */ [980, -26764, -5386, 5396, 1086, 1648], + /* 475 */ [28838, -1270, -447, 5, -429, -20], + /* 476 */ [-15283, 6132, 22812, 1252, -9963, 511], + /* 477 */ [851, 7925, -457, -12210, 4261, 7579], + /* 478 */ [-4530, 8452, -1246, 14501, -24951, -5760], + /* 479 */ [-17814, -10727, 9887, -23929, -13432, 1878], + /* 480 */ [-15049, 10165, 16491, -14603, -11712, -21156], + /* 481 */ [-3317, 840, -5683, 22413, 1994, 586], + /* 482 */ [23158, -5788, -15043, -10372, -9271, -13523], + /* 483 */ [-773, -9509, -3993, -24264, 8463, 5804], + /* 484 */ [-8545, -703, -12440, -3985, -25122, -28147], + /* 485 */ [-16659, 16001, 2746, 1611, 5097, -1043], + /* 486 */ [41, -7181, 19903, 31555, -32237, 13927], + /* 487 */ [-5658, 845, -12774, 5705, 16695, -86], + /* 488 */ [5282, 14875, 27026, 21124, 15776, -10477], + /* 489 */ [14712, 19648, -11487, -13361, -20196, -15229], + /* 490 */ [8597, -9138, -626, 10891, -6015, 6346], + /* 491 */ [-1488, -1272, -1479, -1303, -3704, -5485], + /* 492 */ [-3370, 17871, -6604, 24930, 25886, -3127], + /* 493 */ [8416, 27783, -1385, 5350, -4260, 19993], + /* 494 */ [5688, 362, 17246, 3809, -3246, 1088], + /* 495 */ [-105, -29607, 2747, 15223, -167, 3722], + /* 496 */ [3502, -3195, 8602, 7772, -1566, -915], + /* 497 */ [-491, 3257, -2423, 5522, 20606, -100], + /* 498 */ [-13948, -11368, -15375, -21866, -8520, 12221], + /* 499 */ [-616, 2424, -2023, 4398, -3805, 8108], + /* 500 */ [-7204, 21043, 21211, -9395, -19391, 896], + /* 501 */ [-5737, -15160, -21298, 17066, -1006, -366], + /* 502 */ [6261, 3240, -11937, -16213, -15820, 6581], + /* 503 */ [-3155, 24796, 2733, -1257, -875, -1597], + /* 504 */ [-20469, 11094, 24071, -8987, 14136, 2220], + /* 505 */ [-14106, 11959, -22495, 4135, -1055, -5420], + /* 506 */ [801, -2655, 60, -5324, -790, 5937], + /* 507 */ [-7372, -1764, -22433, -26060, 21707, 4178], + /* 508 */ [-5715, -6648, -14908, 1325, -24044, 1493], + /* 509 */ [-6024, -12488, 23930, 2950, 1601, 1173], + /* 510 */ [19067, 17630, 17929, -10654, 10928, -4958], + /* 511 */ [3231, -3284, 27336, 4174, -1683, 497], +]; diff --git a/crates/vendor/oxideav-ac3/src/eac3/tables/mod.rs b/crates/vendor/oxideav-ac3/src/eac3/tables/mod.rs new file mode 100644 index 00000000..ab063d52 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/eac3/tables/mod.rs @@ -0,0 +1,8 @@ +//! Spec-tabulated constants used by the E-AC-3 (Annex E) decode path. +//! +//! The values in [`aht_codebooks`] are transcribed verbatim from +//! ATSC A/52:2018 Annex E §4 (= ETSI TS 102 366 v1.4.1 Annex E §4) and +//! are spec values, not implementation source. See the per-table doc +//! comments for the originating Table E4.x reference. + +pub mod aht_codebooks; diff --git a/crates/vendor/oxideav-ac3/src/encoder.rs b/crates/vendor/oxideav-ac3/src/encoder.rs new file mode 100644 index 00000000..0e6da490 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/encoder.rs @@ -0,0 +1,8087 @@ +//! AC-3 (Dolby Digital) encoder — builds syncframes from PCM. +//! +//! This is a *basic* encoder in the spirit of §8 (A/52:2018): fixed +//! bit-allocation parameters, no coupling, no rematrixing, long +//! (512-point) transforms only. Target configuration for this initial +//! revision is 48 kHz stereo at 192 kbps — the fixture format already +//! exercised by the decoder round-trip test. +//! +//! Pipeline per block (§8.1 Fig. 8.1): +//! +//! 1. window + forward MDCT (see [`super::mdct`]) +//! 2. exponent extraction (leading-zero count on each coefficient) +//! 3. exponent preprocessing + strategy selection (D15/D25/D45) +//! 4. delta encoding of exponents +//! 5. parametric bit allocation (shared routine with the decoder) +//! 6. mantissa quantisation + grouped packing +//! 7. pack into syncframe, emit crc1 / crc2 +//! +//! ## Status +//! +//! Only the scaffolding + block-zero encode path is wired up so far; +//! incremental commits flesh out the remaining stages. The current +//! implementation produces a syntactically-valid syncframe for the +//! 48 kHz stereo 192 kbps mode. + +use oxideav_core::bits::BitWriter; +use oxideav_core::Encoder; +use oxideav_core::{ + AudioFrame, ChannelLayout, CodecId, CodecParameters, Error, Frame, Packet, Result, + SampleFormat, TimeBase, +}; + +use crate::audblk::{remat_band_count, BLOCKS_PER_FRAME, MAX_FBW, N_COEFFS, SAMPLES_PER_BLOCK}; + +/// LFE channel mantissa-bin upper bound. Per §7.1.3 / §5.4.3.23 the LFE +/// is fixed at 7 mantissa bins (one D15 absexp + 2 groups of 3 deltas → +/// `nlfegrps = 2`). The decoder hard-codes `state.channels[lfe].end_mant +/// = 7` and we mirror that bound on every LFE exp/mantissa loop. +pub(crate) const LFE_END_MANT: usize = 7; +use crate::decoder::SAMPLES_PER_FRAME; +use crate::mdct::{mdct_256_pair, mdct_512}; +use crate::tables::{ + frame_length_bytes, nominal_bitrate_kbps, BAPTAB, BNDSZ, BNDTAB, DBPBTAB, FASTDEC, FASTGAIN, + FLOORTAB, HTH, LATAB, MANT_LEVEL_11, MANT_LEVEL_15, MANT_LEVEL_3, MANT_LEVEL_5, MANT_LEVEL_7, + MASKTAB, QUANTIZATION_BITS, SLOWDEC, SLOWGAIN, WINDOW, +}; + +/// Build an encoder instance. Required parameters (via +/// [`CodecParameters::audio`]): +/// +/// * `sample_rate` — 48 000, 44 100, or 32 000 Hz +/// * `channels` — 1..=6. Mapping to AC-3 acmod (Table 5.8) is driven +/// by `channels` alone unless `channel_layout` resolves the +/// ambiguity at a given channel count: +/// - `1` → acmod=1 (1/0 mono) +/// - `2` → acmod=2 (2/0 L,R) +/// - `3` → acmod=3 (3/0 L,C,R) — DEFAULT for 3 channels +/// - `3` + `channel_layout=Stereo21` → acmod=2 + lfeon=1 +/// (2/0 + LFE, "2.1": L,R,LFE) +/// - `4` → acmod=6 (2/2 L,R,Ls,Rs) +/// - `5` → acmod=7 (3/2 L,C,R,Ls,Rs) +/// - `6` → acmod=7 + lfeon=1 (3/2 + LFE — the canonical "5.1" layout +/// L,C,R,Ls,Rs,LFE) +/// +/// The bit rate defaults per channel-count to a sensible level +/// (mono=96 kbps, stereo=192 kbps, 3ch=256 kbps, 4ch=320 kbps, 5ch=384 +/// kbps, 5.1=448 kbps); callers may override via `bit_rate` on +/// [`CodecParameters`] if it maps to a valid row of Table 5.18. +pub fn make_encoder(params: &CodecParameters) -> Result> { + let meta = metadata_from_options(params)?; + make_encoder_with_metadata(params, meta) +} + +/// [`make_encoder`] with a typed [`MetadataParams`] — the encoder-side +/// bitstream-metadata surface (§5.4.2 BSI advisory words, the +/// §5.4.2.9-10 heavy-compression word, and the §5.4.3.3-4 per-block +/// dynamic-range word). The registry path reaches the same surface via +/// `CodecParameters::options` keys (see [`MetadataParams`]). +pub fn make_encoder_with_metadata( + params: &CodecParameters, + meta: MetadataParams, +) -> Result> { + meta.validate()?; + let sample_rate = params.sample_rate.ok_or_else(|| { + Error::invalid("ac3 encoder: sample_rate is required (48000/44100/32000)") + })?; + let channels = params + .channels + .ok_or_else(|| Error::invalid("ac3 encoder: channels is required"))?; + // §5.4.2.3 acmod mapping (Table 5.8) is mostly determined by the + // channel count alone, but at 3ch the count is ambiguous between + // 3/0 (L,C,R, acmod=3) and 2/0+LFE (L,R,LFE, acmod=2+lfeon=1). When + // `channel_layout` is supplied we honour it; otherwise we default to + // 3/0 for backward compatibility with callers that only set + // `channels`. + let (acmod, lfeon, nfchans) = match (channels, params.channel_layout) { + (1, _) => (1u8, false, 1usize), // 1/0 mono + (2, _) => (2u8, false, 2usize), // 2/0 L,R + (3, Some(ChannelLayout::Stereo21)) => { + // 2.1 = L,R,LFE — acmod=2 (2/0) + lfeon=1. Spec §5.4.2.3 + // lists the LFE channel as orthogonal to acmod, so any + // acmod can carry an LFE; the canonical 2.1 layout pairs + // it with acmod=2. Input PCM order: (L, R, LFE). + (2u8, true, 2usize) + } + (3, _) => (3u8, false, 3usize), // 3/0 L,C,R (default for 3ch) + (4, _) => (6u8, false, 4usize), // 2/2 L,R,Ls,Rs + (5, _) => (7u8, false, 5usize), // 3/2 L,C,R,Ls,Rs + (6, _) => (7u8, true, 5usize), // 3/2 + LFE (5.1: L,C,R,Ls,Rs,LFE) + _ => { + return Err(Error::Unsupported(format!( + "ac3 encoder: unsupported channel count {channels} (must be 1..=6)" + ))) + } + }; + let fscod: u8 = match sample_rate { + 48_000 => 0, + 44_100 => 1, + 32_000 => 2, + _ => { + return Err(Error::Unsupported(format!( + "ac3 encoder: unsupported sample rate {sample_rate} (48000/44100/32000 only)" + ))) + } + }; + + // Bit-rate → frmsizecod lookup. Default per channel count chosen so + // the per-channel bit budget stays roughly constant (~80 kbps/ch + // for fbw, plus a small premium for LFE side-info), matching the + // long-standing AC-3 production defaults documented in Annex A. + let default_kbps: u32 = match channels { + 1 => 96, + 2 => 192, + 3 => 256, + 4 => 320, + 5 => 384, + 6 => 448, + _ => 192, + }; + let target_kbps: u32 = params + .bit_rate + .map(|b| (b / 1000) as u32) + .unwrap_or(default_kbps); + let frmsizecod = pick_frmsizecod(target_kbps).ok_or_else(|| { + Error::Unsupported(format!( + "ac3 encoder: bit rate {target_kbps} kbps has no frmsizecod mapping" + )) + })?; + let frame_bytes = frame_length_bytes(fscod, frmsizecod) + .ok_or_else(|| Error::invalid("ac3 encoder: internal frame-length lookup failed"))?; + + let out_params = { + let mut p = CodecParameters::audio(CodecId::new(crate::CODEC_ID_STR)); + p.sample_rate = Some(sample_rate); + p.channels = Some(channels); + p.sample_format = Some(SampleFormat::S16); + p.bit_rate = Some(nominal_bitrate_kbps(frmsizecod).unwrap_or(target_kbps) as u64 * 1000); + p + }; + + let input_sample_format = params.sample_format.unwrap_or(SampleFormat::S16); + let total_chans = nfchans + usize::from(lfeon); + Ok(Box::new(Ac3Encoder { + codec_id: CodecId::new(crate::CODEC_ID_STR), + out_params, + sample_rate, + channels: nfchans, + lfeon, + acmod, + meta, + input_sample_format, + fscod, + frmsizecod, + frame_bytes: frame_bytes as usize, + // 256 samples of left-context per channel feed the first MDCT. + // Includes the LFE channel (last interleaved slot when lfeon=1). + delay_line: vec![vec![0.0f32; SAMPLES_PER_BLOCK]; total_chans], + pending_samples: vec![Vec::::new(); total_chans], + // Per-fbw-channel transient-detector state. LFE doesn't use + // blksw (§5.4.3.1) so we skip it; index 0..nfchans. + transient_state: (0..nfchans).map(|_| TransientDetector::default()).collect(), + packet_queue: Vec::new(), + pts: 0, + })) +} + +/// Match a target kbps to a row of Table 5.18. Returns the lower +/// (even-indexed) frmsizecod for each bitrate — both members of a pair +/// encode the same rate, and the lower row matches the usual encoder +/// default at 48 kHz. +fn pick_frmsizecod(kbps: u32) -> Option { + const TABLE: &[(u32, u8)] = &[ + (32, 0), + (40, 2), + (48, 4), + (56, 6), + (64, 8), + (80, 10), + (96, 12), + (112, 14), + (128, 16), + (160, 18), + (192, 20), + (224, 22), + (256, 24), + (320, 26), + (384, 28), + (448, 30), + (512, 32), + (576, 34), + (640, 36), + ]; + for &(rate, code) in TABLE { + if rate == kbps { + return Some(code); + } + } + None +} + +/// §5.4.2.13-15 audio-production information for encoder-side emission +/// (`audprodie = 1`): the 5-bit `mixlevel` (peak mixing-session SPL is +/// `80 + mixlevel` dB SPL) and the 2-bit `roomtyp` code (Table 5.12: +/// 0 = not indicated, 1 = large room / X-curve, 2 = small room / flat). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AudioProductionParams { + /// `mixlevel` (5 bits, 0..=31) — §5.4.2.14. + pub mixlevel: u8, + /// `roomtyp` (2 bits, 0..=2; 3 is reserved) — §5.4.2.15. + pub roomtyp: u8, +} + +/// Encoder-side bitstream-metadata surface: the §5.4.2 BSI advisory +/// words plus the §5.4.3.3-4 per-block dynamic-range word. +/// +/// None of these change the coded audio — they steer downstream +/// consumer behaviour (downmix levels, DRC, dialogue normalisation) +/// and are all decoded back by [`crate::bsi::parse`] / +/// [`crate::decoder`]'s §7.7 gain layer. Fields whose syntax slot is +/// acmod-gated (`cmixlev` / `surmixlev` / `dsurmod`) are only emitted +/// for the acmods that carry them (§5.4.2.4-6); the configured value +/// is ignored otherwise. +/// +/// Registry path: `CodecParameters::options` keys `dialnorm` (1..=31), +/// `compr` / `dynrng` (8-bit gain words, decimal or `0x`-hex), +/// `bsmod` (0..=7), `cmixlev` / `surmixlev` (0..=2), `dsurmod` +/// (0..=2), `langcod` (8-bit, spec says `0xFF` when present), +/// `mixlevel` (0..=31) + `roomtyp` (0..=2) (setting either emits +/// `audprodie = 1`), `copyright` and `origbs` (`1`/`true`/`0`/`false`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct MetadataParams { + /// `dialnorm` (5 bits, §5.4.2.8) — dB of dialogue headroom below + /// digital 100%, valid `1..=31` (`0` is reserved on the wire). + pub dialnorm: u8, + /// §5.4.2.9-10 heavy-compression word: `Some` emits `compre = 1` + + /// the 8-bit Table 7.30 `compr` gain word every syncframe. + pub compr: Option, + /// §5.4.3.3-4 dynamic-range word: `Some` emits `dynrnge = 1` + the + /// 8-bit §7.7.1.2 gain word in EVERY audio block (unambiguous + /// under the block-to-block reuse rule; 48 bits/frame). + pub dynrng: Option, + /// `bsmod` (3 bits, §5.4.2.2 / Table 5.7) — bit-stream mode + /// (0 = complete main). + pub bsmod: u8, + /// `cmixlev` (2 bits, Table 5.9: 0 = −3.0 dB, 1 = −4.5 dB, + /// 2 = −6.0 dB) — emitted only when acmod carries a centre channel + /// (acmod ∈ {3, 5, 7}). + pub cmixlev: u8, + /// `surmixlev` (2 bits, Table 5.10: 0 = −3 dB, 1 = −6 dB, + /// 2 = 0 (mute)) — emitted only when acmod carries surrounds + /// (acmod ∈ {4, 5, 6, 7}). + pub surmixlev: u8, + /// `dsurmod` (2 bits, Table 5.11: 0 = not indicated, 1 = NOT Dolby + /// Surround encoded, 2 = Dolby Surround encoded) — emitted only in + /// 2/0 (acmod == 2). + pub dsurmod: u8, + /// §5.4.2.11-12 deprecated language-code slot: `Some` emits + /// `langcode = 1` + the byte (the current spec says the slot + /// "shall be set to 0xFF if present"). + pub langcod: Option, + /// §5.4.2.13-15 audio-production information (`audprodie`). + pub audprod: Option, + /// `copyrightb` (§5.4.2.24). + pub copyrightb: bool, + /// `origbs` (§5.4.2.25). + pub origbs: bool, +} + +impl Default for MetadataParams { + /// The encoder's historical fixed words: dialnorm −27 dB, no + /// compr/dynrng/langcod/audprod, complete-main bsmod, −4.5 dB + /// centre + −6 dB surround downmix codes, Dolby-Surround "not + /// indicated", not copyright-flagged, original bitstream. + fn default() -> Self { + Self { + dialnorm: 27, + compr: None, + dynrng: None, + bsmod: 0, + cmixlev: 1, + surmixlev: 1, + dsurmod: 0, + langcod: None, + audprod: None, + copyrightb: false, + origbs: true, + } + } +} + +impl MetadataParams { + /// Range-check every codepoint against its syntax slot. + pub(crate) fn validate(&self) -> Result<()> { + if self.dialnorm == 0 || self.dialnorm > 31 { + return Err(Error::invalid(format!( + "ac3 encoder: dialnorm {} out of range (1..=31; 0 is reserved)", + self.dialnorm + ))); + } + if self.bsmod > 7 { + return Err(Error::invalid(format!( + "ac3 encoder: bsmod {} out of range (0..=7)", + self.bsmod + ))); + } + for (name, v) in [ + ("cmixlev", self.cmixlev), + ("surmixlev", self.surmixlev), + ("dsurmod", self.dsurmod), + ] { + if v > 2 { + return Err(Error::invalid(format!( + "ac3 encoder: {name} {v} out of range (0..=2; 3 is reserved)" + ))); + } + } + if let Some(a) = self.audprod { + if a.mixlevel > 31 { + return Err(Error::invalid(format!( + "ac3 encoder: mixlevel {} out of range (0..=31)", + a.mixlevel + ))); + } + if a.roomtyp > 2 { + return Err(Error::invalid(format!( + "ac3 encoder: roomtyp {} out of range (0..=2; 3 is reserved)", + a.roomtyp + ))); + } + } + Ok(()) + } + + /// Bits the optional metadata adds on top of the fixed BSI + + /// per-block layout `overhead_bits_for_ends` models: `compr` (8) + + /// `langcod` (8) + `audprodie` body (5 + 2) in BSI, plus the + /// 8-bit `dynrng` word in each of the 6 audio blocks. + pub(crate) fn frame_extra_bits(&self) -> u32 { + let mut bits = 0u32; + if self.compr.is_some() { + bits += 8; + } + if self.langcod.is_some() { + bits += 8; + } + if self.audprod.is_some() { + bits += 7; + } + if self.dynrng.is_some() { + bits += 8 * BLOCKS_PER_FRAME as u32; + } + bits + } + + /// The frame-byte budget handed to the SNR-offset tuners: the real + /// frame size less the (byte-rounded-up) optional-metadata bits, + /// so the mantissa budget the tuner maximises still fits after the + /// extra words are written. + pub(crate) fn tuner_frame_bytes(&self, frame_bytes: usize) -> usize { + frame_bytes - self.frame_extra_bits().div_ceil(8) as usize + } +} + +/// Parse a `1`/`true`/`0`/`false` option value. +fn parse_bool_opt(key: &str, v: &str) -> Result { + match v { + "1" | "true" => Ok(true), + "0" | "false" => Ok(false), + _ => Err(Error::invalid(format!( + "ac3 encoder: option {key}={v} (expected 1/true/0/false)" + ))), + } +} + +/// Parse an 8-bit option value, decimal or `0x`-prefixed hex (gain +/// words like `dynrng`/`compr` are bit patterns, so hex is natural). +fn parse_u8_opt(key: &str, v: &str) -> Result { + let parsed = if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) { + u8::from_str_radix(hex, 16).ok() + } else { + v.parse::().ok() + }; + parsed.ok_or_else(|| { + Error::invalid(format!( + "ac3 encoder: option {key}={v} (expected 0..=255, decimal or 0x-hex)" + )) + }) +} + +/// Assemble [`MetadataParams`] from `CodecParameters::options` (keys +/// documented on [`MetadataParams`]); absent keys keep the defaults. +fn metadata_from_options(params: &CodecParameters) -> Result { + let opts = ¶ms.options; + let mut meta = MetadataParams::default(); + if let Some(v) = opts.get("dialnorm") { + meta.dialnorm = parse_u8_opt("dialnorm", v)?; + } + if let Some(v) = opts.get("compr") { + meta.compr = Some(parse_u8_opt("compr", v)?); + } + if let Some(v) = opts.get("dynrng") { + meta.dynrng = Some(parse_u8_opt("dynrng", v)?); + } + if let Some(v) = opts.get("bsmod") { + meta.bsmod = parse_u8_opt("bsmod", v)?; + } + if let Some(v) = opts.get("cmixlev") { + meta.cmixlev = parse_u8_opt("cmixlev", v)?; + } + if let Some(v) = opts.get("surmixlev") { + meta.surmixlev = parse_u8_opt("surmixlev", v)?; + } + if let Some(v) = opts.get("dsurmod") { + meta.dsurmod = parse_u8_opt("dsurmod", v)?; + } + if let Some(v) = opts.get("langcod") { + meta.langcod = Some(parse_u8_opt("langcod", v)?); + } + let mixlevel = opts.get("mixlevel").map(|v| parse_u8_opt("mixlevel", v)); + let roomtyp = opts.get("roomtyp").map(|v| parse_u8_opt("roomtyp", v)); + if mixlevel.is_some() || roomtyp.is_some() { + meta.audprod = Some(AudioProductionParams { + mixlevel: mixlevel.transpose()?.unwrap_or(0), + roomtyp: roomtyp.transpose()?.unwrap_or(0), + }); + } + if let Some(v) = opts.get("copyright") { + meta.copyrightb = parse_bool_opt("copyright", v)?; + } + if let Some(v) = opts.get("origbs") { + meta.origbs = parse_bool_opt("origbs", v)?; + } + Ok(meta) +} + +struct Ac3Encoder { + codec_id: CodecId, + out_params: CodecParameters, + sample_rate: u32, + /// Number of full-bandwidth channels (1..=5). LFE is *not* counted + /// here; see [`Ac3Encoder::lfeon`]. Mirrors `nfchans` from BSI. + channels: usize, + /// Whether the LFE channel is present. When `true`, the input PCM + /// stride is `channels + 1` and the LFE samples come last in + /// interleaved order (canonical 5.1 layout: L,C,R,Ls,Rs,LFE). + lfeon: bool, + /// AC-3 audio coding mode (Table 5.8). One of {1,2,3,6,7} for the + /// channel counts we accept. + acmod: u8, + /// Encoder-side bitstream metadata (§5.4.2 BSI words + per-block + /// `dynrng`). Validated at construction. + meta: MetadataParams, + /// Input PCM sample format. Defaults to `S16` when params don't + /// declare one. Accepts `S16` and `F32`. + input_sample_format: SampleFormat, + fscod: u8, + frmsizecod: u8, + frame_bytes: usize, + /// Last block's right-half (256 samples) per channel, forming the + /// left context for the next MDCT window. Includes the LFE channel + /// at index `channels` when `lfeon`. + delay_line: Vec>, + /// Samples that have been sent via `send_frame` but not yet + /// consumed into a syncframe. Each inner `Vec` is per-channel + /// (fbw 0..channels, then LFE when present). + pending_samples: Vec>, + /// Per-fbw-channel state for the §8.2.2 spec-compliant transient + /// detector. Holds the cascaded biquad HPF state and the previous + /// 256-sample block's last-segment peak per hierarchy level so the + /// "P[j][0] = previous-tree last-segment peak" test of §8.2.2 + /// step 4 has the right history. + transient_state: Vec, + packet_queue: Vec, + /// Running sample PTS. Each produced syncframe carries SAMPLES_PER_FRAME. + pts: i64, +} + +impl Encoder for Ac3Encoder { + fn codec_id(&self) -> &CodecId { + &self.codec_id + } + + fn output_params(&self) -> &CodecParameters { + &self.out_params + } + + fn send_frame(&mut self, frame: &Frame) -> Result<()> { + let audio = match frame { + Frame::Audio(a) => a, + _ => { + return Err(Error::invalid( + "ac3 encoder: send_frame requires an audio frame", + )) + } + }; + // Per-frame channel-count and sample-rate are no longer carried on + // AudioFrame; the encoder validates layout/rate at construction + // time via CodecParameters and trusts the caller to feed matching + // PCM here. Channel count and stride are taken from `self.channels` + // (fbw) plus an optional LFE slot at the end. + let total_chans = self.channels + usize::from(self.lfeon); + let per_chan = decode_input_samples(audio, total_chans, self.input_sample_format)?; + for ch in 0..total_chans { + self.pending_samples[ch].extend_from_slice(&per_chan[ch]); + } + // Flush whole syncframes while we have enough. + while self.pending_samples[0].len() as u32 >= SAMPLES_PER_FRAME { + self.emit_syncframe()?; + } + Ok(()) + } + + fn receive_packet(&mut self) -> Result { + if self.packet_queue.is_empty() { + return Err(Error::NeedMore); + } + Ok(self.packet_queue.remove(0)) + } + + fn flush(&mut self) -> Result<()> { + // Pad any partial frame with zeros so the last PCM samples reach + // the decoder. + if self.pending_samples[0].is_empty() { + return Ok(()); + } + let missing = SAMPLES_PER_FRAME as usize - self.pending_samples[0].len(); + if missing > 0 { + let total_chans = self.channels + usize::from(self.lfeon); + for ch in 0..total_chans { + self.pending_samples[ch].extend(std::iter::repeat(0.0).take(missing)); + } + } + self.emit_syncframe()?; + Ok(()) + } +} + +/// Convert a decoded [`AudioFrame`] into normalized f32 samples per +/// channel. Supports the two formats most commonly supplied by +/// upstream demuxers / resamplers: interleaved S16 and interleaved F32. +pub(crate) fn decode_input_samples( + a: &AudioFrame, + nch: usize, + fmt: SampleFormat, +) -> Result>> { + let nsamp = a.samples as usize; + let mut out = vec![Vec::with_capacity(nsamp); nch]; + match fmt { + SampleFormat::S16 => { + let plane = a + .data + .first() + .ok_or_else(|| Error::invalid("ac3 encoder: S16 frame missing data plane"))?; + if plane.len() < nsamp * nch * 2 { + return Err(Error::invalid("ac3 encoder: S16 plane too short")); + } + for n in 0..nsamp { + for ch in 0..nch { + let off = (n * nch + ch) * 2; + let v = i16::from_le_bytes([plane[off], plane[off + 1]]); + out[ch].push(v as f32 / 32768.0); + } + } + } + SampleFormat::F32 => { + let plane = a + .data + .first() + .ok_or_else(|| Error::invalid("ac3 encoder: F32 frame missing data plane"))?; + if plane.len() < nsamp * nch * 4 { + return Err(Error::invalid("ac3 encoder: F32 plane too short")); + } + for n in 0..nsamp { + for ch in 0..nch { + let off = (n * nch + ch) * 4; + let v = f32::from_le_bytes([ + plane[off], + plane[off + 1], + plane[off + 2], + plane[off + 3], + ]); + out[ch].push(v); + } + } + } + other => { + return Err(Error::Unsupported(format!( + "ac3 encoder: sample format {other:?} not yet supported" + ))) + } + } + Ok(out) +} + +impl Ac3Encoder { + fn emit_syncframe(&mut self) -> Result<()> { + let n_per = SAMPLES_PER_FRAME as usize; + let total_chans = self.channels + usize::from(self.lfeon); + let lfe_idx = self.channels; // index into pending_samples / delay_line for LFE + // Run the 6-block MDCT pipeline per channel and stash coefficient + // blocks per channel: blocks × N_COEFFS. + // We allocate `channels + 1` slots so LFE coefficients can live + // at index `self.channels` when present, mirroring the source- + // interleaved layout (LFE last). Index isn't a coupling + // pseudo-channel here — that lives at index `self.channels` in a + // *separate* `+1`-sized exps array allocated below. + let mut coeffs: Vec> = + vec![vec![[0.0; N_COEFFS]; BLOCKS_PER_FRAME]; total_chans]; + // §5.4.3.1 blksw[ch][blk] — per-block per-channel block-switch + // flag. Decided per block from the time-domain transient + // detector (see `detect_transient`). When `true`, the encoder + // runs the 256-sample MDCT pair (§7.6 / §8.2.3.2 short + // transform) instead of the long 512-sample MDCT, and the + // decoder swaps to the matching IMDCT path on the same flag. + // LFE has no blksw bit per spec — the LFE channel always uses + // the long-block MDCT (§5.4.3.1 lists blksw[ch] only for fbw). + let mut blksw: Vec<[bool; BLOCKS_PER_FRAME]> = + vec![[false; BLOCKS_PER_FRAME]; self.channels]; + for ch in 0..total_chans { + let drain: Vec = self.pending_samples[ch].drain(0..n_per).collect(); + for blk in 0..BLOCKS_PER_FRAME { + // Build 512-sample input: left context + next 256. + let mut in_buf = [0.0f32; 512]; + in_buf[..256].copy_from_slice(&self.delay_line[ch]); + in_buf[256..].copy_from_slice( + &drain[blk * SAMPLES_PER_BLOCK..(blk + 1) * SAMPLES_PER_BLOCK], + ); + // Per-block transient decision. Implements §8.2.2 of + // ATSC A/52: a 4th-order Butterworth HPF at 8 kHz + // followed by a hierarchical peak-ratio test on three + // levels (256 / 128×2 / 64×4). The "second half" of + // the 512-sample MDCT window — i.e. the freshly drained + // 256 samples in `in_buf[256..]` — is what we test; + // a transient there is what the short-block pair + // localises so it doesn't smear across the prior 256 + // samples of left-context. + // + // Spec uses very strict ratios (T[1]=0.1, T[2]=0.075, + // T[3]=0.05) → ~10×–20× peak rises required; pure tones + // (even at low frequency) sit nowhere near these + // thresholds because the 8 kHz HPF removes the carrier + // entirely. + // + // The `AC3_DISABLE_BLKSW=1` environment variable + // forces long blocks regardless of detector output — + // useful when bisecting whether a quality regression + // is short-block-related. + // LFE never short-blocks (no blksw bit per §5.4.3.1). + let is_lfe_chan = self.lfeon && ch == lfe_idx; + let is_short = if is_lfe_chan || std::env::var("AC3_DISABLE_BLKSW").is_ok() { + false + } else { + self.transient_state[ch].process(&in_buf[256..]) + }; + if !is_lfe_chan { + blksw[ch][blk] = is_short; + } + // Windowing (symmetric 512-sample AC-3 window). The + // window is the same regardless of long/short — the + // decoder applies the same 256-coeff KBD window after + // its IMDCT in both cases (`audblk.rs` around the + // `time[n] *= WINDOW[n]` line). The spec's §7.9.5 + // distinguishes long-only / long-to-short / etc. + // window shapes, but the decoder's choice makes the + // 4-way distinction collapse to the long window for + // every block, which we honour here. + let mut win_buf = [0.0f32; 512]; + for n in 0..256 { + win_buf[n] = in_buf[n] * WINDOW[n]; + win_buf[511 - n] = in_buf[511 - n] * WINDOW[n]; + } + // Update delay line to right-half of the next block. + self.delay_line[ch].copy_from_slice( + &drain[blk * SAMPLES_PER_BLOCK..(blk + 1) * SAMPLES_PER_BLOCK], + ); + // Forward MDCT — long (one 512-pt) or short pair + // (two interleaved 256-pt halves per §7.9.4.2). + if is_short { + mdct_256_pair(&win_buf, &mut coeffs[ch][blk]); + } else { + mdct_512(&win_buf, &mut coeffs[ch][blk]); + } + } + } + + // Per-block exponents: channels × blocks × N_COEFFS (u8 in 0..=24). + // Layout (mirrors audblk's `state.channels[..]`): + // 0..nfchans → fbw channels + // nfchans → coupling pseudo-channel + // nfchans + 1 → LFE pseudo-channel (when lfeon) + // We always allocate `nfchans + 2` slots so the index arithmetic + // stays uniform regardless of `cpl.in_use` / `self.lfeon`. + let cpl_idx_in_exps = self.channels; + let lfe_idx_in_exps = self.channels + 1; + let mut exps: Vec> = + vec![vec![[24u8; N_COEFFS]; BLOCKS_PER_FRAME]; self.channels + 2]; + // Limit active bins: the decoder starts from end_mant = 37 + 3*(chbwcod+12). + // Use chbwcod=60 → end_mant=253 (full bandwidth minus the top 3 bins). + let chbwcod: u8 = 60; + let end_mant: usize = 37 + 3 * (chbwcod as usize + 12); + + // ------------------------------------------------------------- + // §7.4 Channel coupling (encoder-side) + // ------------------------------------------------------------- + // + // Decide once per frame whether to enable coupling. The default + // policy is: 2/0 stereo + AC3_DISABLE_CPL not set. The enable + // logic could test inter-channel correlation in the high band + // and skip coupling when channels are uncorrelated (mid-side + // would actually hurt), but in practice the decoder-side bit + // savings are large enough that always-on coupling is the + // standard choice for production AC-3 encoders at our 192 + // kbps target. + // + // Coupling region: + // cplbegf=8 → first cpl coefficient at bin 133 (~6.0 kHz @ 48 kHz) + // cplendf=15 → last subband index 17 (bins 241..252, ~22 kHz) + // → cpl_endf_mant = 37 + 12*18 = 253 (matches end_mant) + // + // Above bin 133, individual L/R coefficients are replaced by + // shared coupling-channel coefficients = 0.5*(L+R). The decoder + // re-derives L_recv = cplmant * cplco_L * 8, R_recv = cplmant * + // cplco_R * 8 — preserving the per-band envelope of each + // channel without spending bits on per-channel mantissas. + let cpl_disabled = std::env::var("AC3_DISABLE_CPL").is_ok(); + let mut cpl = CouplingPlan::default(); + // ATSC A/52 §7.4: coupling is allowed for any acmod with ≥ 2 fbw + // channels. The coupling group can include up to 5 fbw channels + // (5.1 minus LFE — LFE is a separate pseudo-channel, never + // coupled per §7.4.1). We enable coupling for every multichan + // mode (2/0, 3/0, 2/2, 3/2) since the bit-savings of one shared + // HF spectrum + per-channel coordinates dominate the per-channel + // mantissa cost above ~6 kHz. + // + // Centre-channel exclusion (chincpl[C]=false) is a quality knob + // some production encoders use to preserve dialogue intelligibility + // by keeping the centre's high band uncoupled. We include the + // centre too: at 384-448 kbps it doesn't audibly degrade dialogue + // and dropping it from the coupling group would cost ~15 kbps in + // per-centre HF mantissas. + if self.channels >= 2 && !cpl_disabled { + cpl.in_use = true; + cpl.begf = 8; + cpl.endf = 15; + for ch in 0..self.channels { + cpl.chincpl[ch] = true; + } + // No phase flags by default (mid-side over-suppression on + // anti-correlated transients can sound like ping-ponging + // smear; the decoder side handles phsflg=0 trivially). + // §5.4.3.10 forbids phsflginu outside acmod==2 (2/0 stereo) + // anyway, so multichan paths leave it false. + cpl.phsflginu = false; + // Merge each pair of subbands into one coupling band: + // cplbndstrc[0]=false (always), [1]=true, [2]=false, + // [3]=true, ... → bands of size 2. With 10 subbands + // (cplbegf=8, cplendf=15) this gives 5 coupling bands. + // + // Coarser bands ⇒ fewer cplco emissions per block ⇒ more + // bit savings, at a small per-band envelope-resolution + // cost. 5 bands × 5 ms blocks → ~140 Hz envelope tracking + // resolution which is fine well above the masker. + cpl.nsubbnd = 3 + cpl.endf as usize - cpl.begf as usize; + cpl.bndstrc[0] = false; + for sbnd in 1..cpl.nsubbnd { + cpl.bndstrc[sbnd] = sbnd % 2 == 1; + } + let mut nbnd = cpl.nsubbnd; + for sbnd in 1..cpl.nsubbnd { + if cpl.bndstrc[sbnd] { + nbnd -= 1; + } + } + cpl.nbnd = nbnd; + // Coupling coordinates are signalled on block 0 only; + // every later block reuses (cplcoe[blk][ch]=false). Only + // coupled channels emit coords. + for ch in 0..self.channels { + if cpl.chincpl[ch] { + cpl.cplcoe[0][ch] = true; + } + } + } + + // Storage for the coupling-channel coefficients. Index by + // [blk][bin]; only bins in [cpl_begf_mant, cpl_endf_mant) are + // meaningful when coupling is in use. + let mut cpl_coeffs: Vec<[f32; N_COEFFS]> = vec![[0.0f32; N_COEFFS]; BLOCKS_PER_FRAME]; + + if cpl.in_use { + // §7.4.1: "channel coupling is performed on encode by + // averaging the transform coefficients across channels + // that are included in the coupling channel." + // + // cplmant[k] = (1/N) * Σ_{ch ∈ chincpl} coeffs[ch][k] + // + // For 2/0 with both channels coupled, cplmant = 0.5*(L+R). + // For 3/2 with all 5 fbw coupled, cplmant = 0.2*(L+C+R+Ls+Rs). + // The per-band envelope ratio is captured by the coupling + // coordinates so the decoder reconstructs each channel's + // approximate magnitude. + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + let n_coupled = (0..self.channels).filter(|&ch| cpl.chincpl[ch]).count(); + let inv_n = if n_coupled > 0 { + 1.0f32 / n_coupled as f32 + } else { + 0.0 + }; + for blk in 0..BLOCKS_PER_FRAME { + for bin in begf_mant..endf_mant { + let mut sum = 0.0f32; + for ch in 0..self.channels { + if cpl.chincpl[ch] { + sum += coeffs[ch][blk][bin]; + } + } + cpl_coeffs[blk][bin] = sum * inv_n; + } + } + + // Compute and quantise coupling coordinates from the + // *block-0* envelope. This matches our cplcoe[blk][ch] + // policy: signal coords on block 0, reuse for blocks 1..5. + // + // Per-band per-channel coordinate: + // cplco[ch][bnd] = sqrt(Σ ch[k]² / Σ cpl[k]²) / 8 + // + // The /8 cancels the decoder's `coord = state.cpl_coord * + // 8.0` lift, and the sqrt(energy ratio) preserves the + // per-channel magnitude in the average sense across the + // band. + // + // We sum energies over *all 6 blocks* before computing the + // coordinate so the block-0 coords represent the frame + // envelope rather than a single-block snapshot — the + // coords are reused for the whole frame, and a single- + // block measurement would over- or under-shoot on bursts. + let sbnd2bnd = cpl.sbnd_to_bnd(); + let mut e_ch_band = [[0.0f64; 18]; MAX_FBW]; + let mut e_cpl_band = [0.0f64; 18]; + for blk in 0..BLOCKS_PER_FRAME { + for sbnd_off in 0..cpl.nsubbnd { + let bnd = sbnd2bnd[sbnd_off]; + let base = begf_mant + sbnd_off * 12; + let limit = (base + 12).min(endf_mant); + for bin in base..limit { + let c = cpl_coeffs[blk][bin] as f64; + e_cpl_band[bnd] += c * c; + for ch in 0..self.channels { + if !cpl.chincpl[ch] { + continue; + } + let v = coeffs[ch][blk][bin] as f64; + e_ch_band[ch][bnd] += v * v; + } + } + } + } + // Per-channel: max raw cplco across bands → mstrcplco. Skip + // channels that are not in the coupling group — their cplco + // would be 0 anyway and the spec's cplcoe[ch] gate already + // suppresses transmission, but leaving the arrays at the + // Default {0,0,0} avoids any chance of stale values from a + // previous syncframe leaking in. + let mut raw_cplco = [[0.0f32; 18]; MAX_FBW]; + for ch in 0..self.channels { + if !cpl.chincpl[ch] { + continue; + } + let mut max_co: f32 = 0.0; + for bnd in 0..cpl.nbnd { + let denom = e_cpl_band[bnd]; + let co = if denom > 1e-20 { + ((e_ch_band[ch][bnd] / denom).sqrt() as f32) / 8.0 + } else { + 0.0 + }; + raw_cplco[ch][bnd] = co; + if co > max_co { + max_co = co; + } + } + cpl.mstrcplco[ch] = pick_mstrcplco(max_co); + for bnd in 0..cpl.nbnd { + let (e, m) = quantise_cplco(raw_cplco[ch][bnd], cpl.mstrcplco[ch]); + cpl.cplcoexp[ch][bnd] = e; + cpl.cplcomant[ch][bnd] = m; + } + } + + // Replace the per-channel high-band MDCT coefficients + // with the *encoder's view* of what the decoder will + // reconstruct from the cpl channel + the quantised + // coords. This is critical for two downstream stages: + // + // 1. Rematrixing — must run on the *post-coupling* + // coefficients so the encoder and decoder agree on + // what's in each channel's exponent/mantissa + // buffers in the cpl region (rematrix is bypassed + // above bin = cpl_begf_mant by the band-table cap, + // so this only matters for stages downstream of + // rematrix). + // 2. The per-channel `end_mant` used for fbw exponent / + // mantissa emission is clamped to `cpl_begf_mant` + // below — so the post-coupling bins above that + // index are intentionally not transmitted as + // per-channel data; they exist only so subsequent + // sanity checks see realistic magnitudes. + // + // Reconstructed coefficient: `chmant * cplco * 8 = cpl * + // cplco_recon * 8`. Note phsflginu=0 so no sign flip. + let mut cplco_recon = [[0.0f32; 18]; MAX_FBW]; + for ch in 0..self.channels { + if !cpl.chincpl[ch] { + continue; + } + for bnd in 0..cpl.nbnd { + cplco_recon[ch][bnd] = reconstruct_cplco( + cpl.cplcoexp[ch][bnd], + cpl.cplcomant[ch][bnd], + cpl.mstrcplco[ch], + ); + } + } + for blk in 0..BLOCKS_PER_FRAME { + for sbnd_off in 0..cpl.nsubbnd { + let bnd = sbnd2bnd[sbnd_off]; + let base = begf_mant + sbnd_off * 12; + let limit = (base + 12).min(endf_mant); + for bin in base..limit { + let cpl_v = cpl_coeffs[blk][bin]; + for ch in 0..self.channels { + if !cpl.chincpl[ch] { + continue; + } + coeffs[ch][blk][bin] = cpl_v * cplco_recon[ch][bnd] * 8.0; + } + } + } + } + + if std::env::var("AC3_TRACE_CPL_ENC").is_ok() { + eprintln!( + "CPL-ENC begf={} endf={} nsubbnd={} nbnd={} chincpl={:?} mstr={:?}", + cpl.begf, + cpl.endf, + cpl.nsubbnd, + cpl.nbnd, + &cpl.chincpl[..self.channels], + &cpl.mstrcplco[..self.channels], + ); + for ch in 0..self.channels { + if !cpl.chincpl[ch] { + continue; + } + eprintln!(" ch{} cplco[bnd]: {:?}", ch, &raw_cplco[ch][..cpl.nbnd]); + eprintln!( + " ch{} cplcoexp: {:?}", + ch, + &cpl.cplcoexp[ch][..cpl.nbnd] + ); + eprintln!( + " ch{} cplcomant: {:?}", + ch, + &cpl.cplcomant[ch][..cpl.nbnd] + ); + } + } + } + + // Per-channel mantissa-bin upper bound. When coupling is in + // use the channel only carries data up to cpl_begf_mant; + // above that, the decoder fills from the coupling channel + // (post-coord application) so transmitting per-channel data + // would be wasted bits. With cplinu=0 the channel goes the + // full chbwcod range. + let ch_end_mant: usize = if cpl.in_use { + cpl.begf_mant() + } else { + end_mant + }; + + // Rematrixing decision (§7.5.3) — only meaningful for 2/0 stereo. + // For each block and each rematrix band compare Σ|L|² + Σ|R|² + // against Σ|L+R|² + Σ|L-R|² and pick the smaller-energy pair. + // When (L+R, L-R) wins we replace the L and R coefficients in + // that band with their sum/difference forms scaled by 0.5 — + // the spec's "transmitted left = 0.5*(L+R)" formula. The + // decoder reverses with `L = L'+R'`, `R = L'-R'`. + // + // Rationale for picking 0.5 vs leaving the unscaled sum: + // * `L_recv = 0.5*(L+R)`, `R_recv = 0.5*(L-R)` + // * `L_dec = L_recv + R_recv = 0.5*(L+R) + 0.5*(L-R) = L` ✓ + // + // The 0.5 keeps the rematrixed coefficient magnitudes in the + // same range as the original L/R, so quantiser exponents do + // not jump by a stage. + // + // Per Tables 7.25-7.28, the upper edge of the LAST rematrix + // band tracks the coupling lower edge: with cplinu=0 it ends + // at bin 252; with cplinu=1 it ends at A = 36 + 12*cplbegf. + // For cplbegf=8 that's bin 132, so rematrix band 3 = (61, 133). + let last_remat_hi = if cpl.in_use { + 36 + 12 * cpl.begf as usize + 1 + } else { + 253 + }; + let remat_bands: [(usize, usize); 4] = + [(13, 25), (25, 37), (37, 61), (61, last_remat_hi.max(61))]; + let nrematbd = if self.channels == 2 { + remat_band_count(cpl.in_use, cpl.begf) + } else { + 0 + }; + let mut rematflg: Vec<[bool; 4]> = vec![[false; 4]; BLOCKS_PER_FRAME]; + if nrematbd > 0 { + for blk in 0..BLOCKS_PER_FRAME { + for (bnd_idx, &(lo, hi_full)) in remat_bands.iter().take(nrematbd).enumerate() { + let hi = hi_full.min(ch_end_mant); + if lo >= hi { + continue; + } + let mut e_l = 0.0f64; + let mut e_r = 0.0f64; + let mut e_s = 0.0f64; + let mut e_d = 0.0f64; + for bin in lo..hi { + let l = coeffs[0][blk][bin] as f64; + let r = coeffs[1][blk][bin] as f64; + e_l += l * l; + e_r += r * r; + let s = l + r; + let d = l - r; + e_s += s * s; + e_d += d * d; + } + // §7.5.3 picks the minimum-energy combination among the + // 4 candidates {L, R, L+R, L-R}. Rematrix if the minimum + // belongs to the {L+R, L-R} pair: that is, the smaller + // of the sum/difference energies undercuts the smaller + // of the L/R energies. The scaling-by-0.5 we apply on + // the transmitted side only changes the magnitude — the + // *relative* ranking is preserved, so we compare the + // unscaled energies here. + if e_s.min(e_d) < e_l.min(e_r) { + rematflg[blk][bnd_idx] = true; + for bin in lo..hi { + let l = coeffs[0][blk][bin]; + let r = coeffs[1][blk][bin]; + coeffs[0][blk][bin] = 0.5 * (l + r); + coeffs[1][blk][bin] = 0.5 * (l - r); + } + } + if std::env::var("AC3_TRACE_REMAT_ENC").is_ok() { + eprintln!( + "REMAT-ENC blk={} bnd={} lo={} hi={} e_l={:.3e} e_r={:.3e} e_s={:.3e} e_d={:.3e} flg={}", + blk, bnd_idx, lo, hi, e_l, e_r, e_s, e_d, rematflg[blk][bnd_idx] + ); + } + } + } + } + // Step 1: raw exponent extraction per block, per channel. + for ch in 0..self.channels { + for blk in 0..BLOCKS_PER_FRAME { + for k in 0..ch_end_mant { + exps[ch][blk][k] = extract_exponent(coeffs[ch][blk][k]); + } + // For each exponent that was just extracted, take the *minimum* + // across the coefficient's bin neighbourhood of radius 0 + // (i.e. no change) — this is a stub for the spec's §7.1.5 + // exponent-sharing that grpsize>1 strategies imply. Currently + // only D15 (grpsize=1) is used so no sharing happens here. + } + } + // Coupling-channel exponents: extract from cpl_coeffs over + // [cpl_begf_mant, cpl_endf_mant). The cpl pseudo-channel's + // exponent buffer lives at index `self.channels` (one past + // the last fbw channel). The decoder's first cpl exponent + // (`cplabsexp << 1`) is just a starting reference; the + // actual bin-aligned exponents start at `cpl_start`. + if cpl.in_use { + let cpl_idx = cpl_idx_in_exps; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + for blk in 0..BLOCKS_PER_FRAME { + for k in begf_mant..endf_mant { + exps[cpl_idx][blk][k] = extract_exponent(cpl_coeffs[blk][k]); + } + } + } + // LFE exponents (§5.4.3.23 / §7.1.3 — bins 0..7 only). The + // decoder treats the LFE pseudo-channel like an fbw channel + // limited to end_mant=7 with `nlfegrps=2` (i.e. 6 D15 deltas + // covering bins 1..7). Encoder mirrors that: extract on each + // block, with the same D15-on-blocks-0/3 strategy. + // + // §7.1.3 / A/52 §5.5.5 — LFE is spectrally constrained to + // 0–120 Hz. At 48 kHz with a 512-point MDCT, bin k has centre + // frequency (2k+1)×48000/1024 Hz: bin 0 ≈ 47 Hz, bin 1 ≈ 141 Hz. + // We zero coefficients from bin 2 onward so only sub-120 Hz + // content is coded in the LFE channel. LFE_END_MANT stays at 7 + // (decoder expects it), but bins 2..7 carry exp=24 → bap=0 → + // no mantissa bits allocated. + if self.lfeon { + let lfe_cutoff = match self.sample_rate { + 48_000 => 2usize, + 44_100 => 2usize, + 32_000 => 2usize, // 32k: bin 1 ≈ 125 Hz — still close enough + _ => 2usize, + }; + for blk in 0..BLOCKS_PER_FRAME { + for k in lfe_cutoff..LFE_END_MANT { + coeffs[lfe_idx][blk][k] = 0.0; + } + for k in 0..LFE_END_MANT { + exps[lfe_idx_in_exps][blk][k] = extract_exponent(coeffs[lfe_idx][blk][k]); + } + } + } + + // Exponent strategy per block per channel. + // + // A basic encoder can legally transmit D15 on block 0 and REUSE + // on blocks 1..5 — which is what this encoder shipped with. But + // that badly hurts quality on any non-stationary input: blocks + // 1..5 are quantised using block-0's spectral envelope, so their + // mantissas saturate (|coeff| * 2^e clamps to ±1) whenever the + // actual bin energy disagrees. Here we refresh exponents twice + // per frame — D15 on blocks 0 and 3, REUSE for 1/2/4/5 — which + // fits inside the 192 kbps budget for 2/0 stereo and recovers a + // large SNR margin on non-steady-state signals. + let exp_strategies: [u8; BLOCKS_PER_FRAME] = [1, 0, 0, 1, 0, 0]; + // Pre-process the D15 exponents: clamp absexp to 4-bit range and + // clamp each forward delta to ±2. The output is a legal D15 + // sequence the decoder will replay verbatim. + for ch in 0..self.channels { + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + preprocess_d15(&mut exps[ch][blk][..ch_end_mant]); + } + } + } + // Per-channel exponent-strategy selection (§7.1.3 / §5.4.3.22). + // After D15 preprocessing each anchor block (block 0 / 3) is + // smooth enough to consider D25 (grpsize=2) or D45 (grpsize=4) + // when adjacent bins share similar exponents. We pick per + // channel per block so a HF-rich channel can still emit D15 + // while a smooth bass channel saves bits via D45. The frame + // anchor pattern (new on 0/3, REUSE on 1/2/4/5) is preserved. + // `AC3_DISABLE_EXPSTR_SEL=1` pins every "new" anchor to D15 + // for A/B testing. + let chexpstr_plan: Vec<[u8; BLOCKS_PER_FRAME]> = + if std::env::var("AC3_DISABLE_EXPSTR_SEL").is_ok() { + let mut out = vec![[0u8; BLOCKS_PER_FRAME]; self.channels]; + for ch in 0..self.channels { + out[ch] = exp_strategies; + } + out + } else { + select_exp_strategies(&exps, self.channels, ch_end_mant) + }; + // Apply grpsize quantisation for any channel that picked D25/D45 + // on an anchor block. The decoder will reconstruct the same + // exponents (one per grpsize span replicated across the span) + // so feeding the bit allocator and mantissa quantiser the same + // values keeps everything in lockstep. + for ch in 0..self.channels { + for blk in 0..BLOCKS_PER_FRAME { + let strat = chexpstr_plan[ch][blk]; + if strat >= 2 { + let grpsize = if strat == 2 { 2 } else { 4 }; + quantise_exponents_to_grpsize(&mut exps[ch][blk][..ch_end_mant], grpsize); + } + } + // For REUSE blocks (chexpstr==0), copy the most recent + // transmitted exponent set forward so compute_bap + + // mantissa quantisation use the exponents the decoder will + // see on this block. + let mut last = 0usize; + for blk in 0..BLOCKS_PER_FRAME { + if chexpstr_plan[ch][blk] != 0 { + last = blk; + } else { + let src: [u8; N_COEFFS] = exps[ch][last]; + exps[ch][blk][..ch_end_mant].copy_from_slice(&src[..ch_end_mant]); + } + } + } + // Coupling-channel exponent strategy + D15 preprocessing. + // Same per-block strategy as the fbw channels (D15 on blocks + // 0 and 3, REUSE elsewhere) so the cpl side info adds no + // new strategy decisions to track. + if cpl.in_use { + let cpl_idx = cpl_idx_in_exps; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + preprocess_d15(&mut exps[cpl_idx][blk][begf_mant..endf_mant]); + } + } + let mut last = 0usize; + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + last = blk; + } else { + let src: [u8; N_COEFFS] = exps[cpl_idx][last]; + exps[cpl_idx][blk][begf_mant..endf_mant] + .copy_from_slice(&src[begf_mant..endf_mant]); + } + } + } + // LFE exponent preprocessing + REUSE block fill. Same per-block + // strategy choice as fbw, but lfeexpstr is a *1-bit* flag in the + // bitstream (§5.4.3.23) rather than 2 bits — value 0 means + // REUSE, 1 means new D15. We map exp_strategies==1 → lfeexpstr=1 + // and exp_strategies==0 → lfeexpstr=0, matching the fbw cadence. + if self.lfeon { + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + preprocess_d15(&mut exps[lfe_idx_in_exps][blk][..LFE_END_MANT]); + } + } + let mut last = 0usize; + for blk in 0..BLOCKS_PER_FRAME { + if exp_strategies[blk] == 1 { + last = blk; + } else { + let src: [u8; N_COEFFS] = exps[lfe_idx_in_exps][last]; + exps[lfe_idx_in_exps][blk][..LFE_END_MANT] + .copy_from_slice(&src[..LFE_END_MANT]); + } + } + } + + // Bit-allocation state we'll feed into the shared allocator. + // Fixed parameters per §8.2.12 "Core Bit Allocation" (basic encoder). + let ba = BitAllocParams { + sdcycod: 2, + fdcycod: 1, + sgaincod: 1, + dbpbcod: 2, + floorcod: 4, + // These SNR offsets are "loose" — a production encoder would + // iterate them so the total mantissa bit count fills the + // frame budget exactly. For now we pick a conservative + // baseline that under-shoots the budget (padded with skip + // bytes) rather than overshoots. + csnroffst: 15, + fsnroffst: 0, + fsnroffst_ch: [0u8; MAX_FBW], + cplfsnroffst: 0, + lfefsnroffst: 0, + fgaincod: 4, + cplfgaincod: 4, + lfefgaincod: 4, + }; + + // §7.2.2.6 / §5.4.3.47-57 — build the per-frame DBA plan. + // Done BEFORE snroffst tuning so the bit budget the tuner sees + // already accounts for the dba syntax cost AND the bap[] arrays + // tune_snroffst computes use the dba-modified mask. The + // AC3_DISABLE_DBA env var pins the plan to all-zero (no + // segments) — useful for A/B-ing the dba contribution. + let dba_plan = if std::env::var("AC3_DISABLE_DBA").is_ok() { + DbaPlan::default() + } else { + build_dba_plan(&exps, self.channels, ch_end_mant, &cpl) + }; + + // Iteratively tune csnroffst+fsnroffst so the encoded mantissa + // bits + side-info fit the frame payload. This is the minimal + // loop §8.2.12 describes. Pass the per-channel chexpstr plan so + // the budget calculation accounts for D25/D45 savings. + // Optional metadata words (compr/langcod/audprod/dynrng) are + // not modelled inside `overhead_bits_for_ends`; shrink the + // byte budget the tuners see instead so the mantissa payload + // still fits after those words are written. + let tuner_frame_bytes = self.meta.tuner_frame_bytes(self.frame_bytes); + let tuned_ba = tune_snroffst_with_plan( + &ba, + &exps, + ch_end_mant, + self.channels, + self.fscod, + tuner_frame_bytes, + &exp_strategies, + Some(&chexpstr_plan), + &cpl, + &dba_plan, + self.acmod, + self.lfeon, + ); + // Round-24 / task #170: per-block snroffst redistribution. + // After the global tuner picks a frame-wide (csnr, fsnr_ch) + // baseline, this pass moves bits between blocks based on + // per-block masking demand. When a transient sits in one block + // and the rest of the frame is silent, the demand-heavy block + // gets a fsnr bump (more mantissa bits → less PSNR drop on the + // transient) while quiet blocks donate the savings. The + // bitstream syntax §5.4.3.37-43 already supports per-block + // snroffste so the decoder applies the new values immediately. + // The AC3_DISABLE_PERBLOCK_SNR env var pins the plan to the + // flat global one — useful for A/B-ing the contribution. + let snr_plan = if std::env::var("AC3_DISABLE_PERBLOCK_SNR").is_ok() { + PerBlockSnr::from_global(&tuned_ba) + } else { + tune_per_block_snroffst_with_plan( + &tuned_ba, + &exps, + ch_end_mant, + self.channels, + self.fscod, + tuner_frame_bytes, + &exp_strategies, + Some(&chexpstr_plan), + &cpl, + &dba_plan, + self.acmod, + self.lfeon, + ) + }; + // Compute bap arrays per channel per block using the tuned + // params. Layout matches `exps`: + // 0..nfchans → fbw, nfchans → cpl, nfchans+1 → LFE. + // Per-channel fsnroffst is read from `snr_plan.fsnroffst_ch[blk]` + // so each (channel, block) uses its own bit-allocation refinement. + let mut baps: Vec> = + vec![vec![[0u8; N_COEFFS]; BLOCKS_PER_FRAME]; self.channels + 2]; + for ch in 0..self.channels { + for blk in 0..BLOCKS_PER_FRAME { + let ch_ba = snr_plan.ba_for_fbw(&tuned_ba, blk, ch); + compute_bap( + &exps[ch][blk], + ch_end_mant, + self.fscod, + &ch_ba, + &mut baps[ch][blk], + Some((&dba_plan, ch)), + ); + } + } + // Coupling-channel bap. The decoder runs `run_bit_allocation` + // on the cpl pseudo-channel over [cpl_begf_mant, cpl_endf_mant) + // with `is_coupling=true` (which uses cpl_fsnroffst / + // cpl_fgaincod, currently inherited from the fbw cstd values + // via the BitAllocParams struct). For the encoder we run the + // same compute_bap routine; the start is implicit at bin 0 + // for the masking model so we use `compute_bap_range` (a thin + // wrapper around compute_bap) that masks with the cpl-specific + // snroffset. + if cpl.in_use { + let cpl_idx = cpl_idx_in_exps; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + // Per-block (csnr, cplfsnr) substitution via snr_plan. + for blk in 0..BLOCKS_PER_FRAME { + let cpl_ba = snr_plan.ba_for_cpl(&tuned_ba, blk); + compute_bap_cpl( + &exps[cpl_idx][blk], + begf_mant, + endf_mant, + self.fscod, + &cpl_ba, + &mut baps[cpl_idx][blk], + Some((&dba_plan, MAX_FBW)), + ); + } + } + // LFE bap. The decoder treats LFE as a fbw channel with start=0, + // end=7 and `is_coupling=false`, using the LFE-specific + // (lfefsnroffst, lfefgaincod) pair. We run `compute_bap` with a + // per-block `lfe_ba` variant from `snr_plan`. + if self.lfeon { + for blk in 0..BLOCKS_PER_FRAME { + let lfe_ba = snr_plan.ba_for_lfe(&tuned_ba, blk); + compute_bap( + &exps[lfe_idx_in_exps][blk], + LFE_END_MANT, + self.fscod, + &lfe_ba, + &mut baps[lfe_idx_in_exps][blk], + None, // §5.4.3.47 forbids LFE dba — pass None. + ); + } + } + + // --------- Pack the syncframe --------- + let mut bw = BitWriter::with_capacity(self.frame_bytes); + + // syncinfo: syncword + placeholder crc1 + fscod/frmsizecod. + bw.write_u32(0x0B77, 16); + bw.write_u32(0, 16); // crc1 placeholder, filled post-hoc + bw.write_u32(self.fscod as u32, 2); + bw.write_u32(self.frmsizecod as u32, 6); + + // BSI — bsid=8 plus the §5.4.2 metadata words from `self.meta`. + // §5.4.2.3 acmod (3 bits) per Table 5.8 — supplied at + // make-encoder time. The optional `cmixlev` / `surmixlev` / + // `dsurmod` fields are only present for the acmods that + // actually carry a centre channel / surround channel / + // 2-front-only respectively (§5.4.2.4-6). + bw.write_u32(8, 5); // bsid + bw.write_u32(self.meta.bsmod as u32, 3); + bw.write_u32(self.acmod as u32, 3); + // §5.4.2.4 cmixlev — present when the 3 LSBs of acmod include a + // centre channel: `(acmod & 0x1) != 0 && acmod != 0x1` (i.e. + // acmod ∈ {3, 5, 7}). Table 5.9 codes (default 1 = -4.5 dB). + if (self.acmod & 0x1) != 0 && self.acmod != 0x1 { + bw.write_u32(self.meta.cmixlev as u32, 2); + } + // §5.4.2.5 surmixlev — present when a surround channel exists + // (acmod & 0x4 set, i.e. acmod ∈ {4, 5, 6, 7}). Table 5.10 + // codes (default 1 = -6 dB). + if (self.acmod & 0x4) != 0 { + bw.write_u32(self.meta.surmixlev as u32, 2); + } + // §5.4.2.6 dsurmod — Dolby Surround flag, only in 2/0. + if self.acmod == 0x2 { + bw.write_u32(self.meta.dsurmod as u32, 2); + } + bw.write_u32(self.lfeon as u32, 1); + bw.write_u32(self.meta.dialnorm as u32, 5); + match self.meta.compr { + // §5.4.2.9-10 — heavy-compression gain word. + Some(c) => { + bw.write_u32(1, 1); // compre + bw.write_u32(c as u32, 8); + } + None => bw.write_u32(0, 1), + } + match self.meta.langcod { + // §5.4.2.11-12 — deprecated language-code slot. + Some(l) => { + bw.write_u32(1, 1); // langcode + bw.write_u32(l as u32, 8); + } + None => bw.write_u32(0, 1), + } + match self.meta.audprod { + // §5.4.2.13-15 — mixlevel(5) + roomtyp(2). + Some(a) => { + bw.write_u32(1, 1); // audprodie + bw.write_u32(a.mixlevel as u32, 5); + bw.write_u32(a.roomtyp as u32, 2); + } + None => bw.write_u32(0, 1), + } + bw.write_u32(u32::from(self.meta.copyrightb), 1); + bw.write_u32(u32::from(self.meta.origbs), 1); + bw.write_u32(0, 1); // timecod1e + bw.write_u32(0, 1); // timecod2e + bw.write_u32(0, 1); // addbsie + + // ---- Audio blocks ---- + for blk in 0..BLOCKS_PER_FRAME { + // §5.4.3.1 blksw[ch] — per-channel block-switch flag. + // Value 1 ⇒ this channel uses the 256-sample short-block + // pair for this audio block; the decoder takes the + // matching `imdct_256_pair_fft` branch. + for ch in 0..self.channels { + bw.write_u32(blksw[ch][blk] as u32, 1); + } + // dithflag per channel: 1 (enable dither on zero-bap bins). + // Spec-recommended default; decoder drives an LFSR-backed + // pseudo-random mantissa replacement on bap=0 bins which + // removes coloration of the IMDCT's stop band on masked + // bins. After the backward-pass legaliser lowers some + // silent-bin exponents, dither there multiplies `0.707` by + // `2^-exp` which can be perceptible; however disabling + // dither globally is worse than enabling it (we measured + // ~2 dB PSNR regression on speech fixtures with dith off). + for _ in 0..self.channels { + bw.write_u32(1, 1); + } + // §5.4.3.3-4 dynrnge + dynrng. When metadata configures a + // dynamic-range word it is transmitted in EVERY block + // (unambiguous under the reuse rule); otherwise dynrnge=0 + // (block 0 then sets gain=1). + match self.meta.dynrng { + Some(w) => { + bw.write_u32(1, 1); + bw.write_u32(w as u32, 8); + } + None => bw.write_u32(0, 1), + } + // §5.4.3.7-13 cplstre + cplinu + (when cplinu) the + // chincpl[ch] / phsflginu / cplbegf / cplendf / cplbndstrc + // sequence. The encoder commits to a single cpl + // configuration for the whole frame, so all of this side + // info is emitted on block 0 and reused thereafter. + if blk == 0 { + bw.write_u32(1, 1); // cplstre = 1 + bw.write_u32(cpl.in_use as u32, 1); // cplinu + if cpl.in_use { + for ch in 0..self.channels { + bw.write_u32(cpl.chincpl[ch] as u32, 1); + } + // §5.4.3.10 phsflginu — present ONLY when acmod == 2 + // (2/0 stereo). For multichannel modes (acmod ∈ + // {3,4,5,6,7}) the field is absent from the + // bitstream entirely; the decoder treats phsflginu + // as implicitly 0. Writing it unconditionally + // shifts every subsequent block-0 field by 1 bit, + // which the validator binary detects as a malformed + // cplcoe stream. + if self.acmod == 0x2 { + bw.write_u32(cpl.phsflginu as u32, 1); + } + bw.write_u32(cpl.begf as u32, 4); + bw.write_u32(cpl.endf as u32, 4); + // §5.4.3.13 cplbndstrc[sbnd] for sbnd >= 1. + for sbnd in 1..cpl.nsubbnd { + bw.write_u32(cpl.bndstrc[sbnd] as u32, 1); + } + } + } else { + bw.write_u32(0, 1); // cplstre = 0 (reuse) + } + // §5.4.3.14-18 cplcoe[ch] / mstrcplco / cplcoexp / cplcomant + // / phsflg. Coupling coordinates are signalled on block 0 + // only (cplcoe[blk][ch] = (blk == 0)); subsequent blocks + // reuse, which is the bit-saving win of the coupling + // mechanism: one envelope per ~32 ms frame. + if cpl.in_use { + let mut any_coe = false; + for ch in 0..self.channels { + if !cpl.chincpl[ch] { + continue; + } + let coe = cpl.cplcoe[blk][ch]; + bw.write_u32(coe as u32, 1); + if coe { + any_coe = true; + bw.write_u32(cpl.mstrcplco[ch] as u32, 2); + for bnd in 0..cpl.nbnd { + bw.write_u32(cpl.cplcoexp[ch][bnd] as u32, 4); + bw.write_u32(cpl.cplcomant[ch][bnd] as u32, 4); + } + } + } + // §5.4.3.18 phsflg[bnd] — only when 2/0 + phsflginu + + // any cplcoe set this block. We disabled phsflginu so + // the field is suppressed; left as a guard for when + // it is enabled in a future iteration. + if cpl.phsflginu && any_coe { + for bnd in 0..cpl.nbnd { + bw.write_u32(cpl.phsflg[bnd] as u32, 1); + } + } + } + // §5.4.3.19 rematstr (acmod == 2): we refresh rematflg + // every block so the encoder can adapt the L/R vs L+R/L-R + // decision per block. Cost is 1 + nrematbd bits per block. + // When coupling is active the rematrix-band count shrinks + // to track the lower edge of the cpl region (Table 5.15). + if nrematbd > 0 { + bw.write_u32(1, 1); // rematstr — flags follow + for bnd in 0..nrematbd { + bw.write_u32(rematflg[blk][bnd] as u32, 1); + } + } + + // chexpstr: per-channel-per-block strategy chosen above. + // The frame-wide `exp_strategies[blk]` still drives cpl / + // lfe / chbwcod / dba decisions because those side-info + // fields are tied to the anchor cadence. + let exp_strategy: u8 = exp_strategies[blk]; + // §5.4.3.21 cplexpstr — only when cplinu. Use the same + // strategy as the anchor cadence (cpl D25/D45 selection + // would need its own smoothness probe; round defers it). + if cpl.in_use { + bw.write_u32(exp_strategy as u32, 2); + } + // §5.4.3.22 chexpstr[ch] — 2 bits per fbw channel from the + // per-channel plan. + for ch in 0..self.channels { + bw.write_u32(chexpstr_plan[ch][blk] as u32, 2); + } + // §5.4.3.23 lfeexpstr — 1 bit when lfeon. LFE always uses + // D15 in this encoder (the bin-7 LFE band is tiny). + if self.lfeon { + bw.write_u32(if exp_strategy == 1 { 1 } else { 0 }, 1); + } + // chbwcod (only when exp strategy != reuse, AND channel not + // coupled). When coupling is active, channels do not + // transmit their own bandwidth code. + for ch in 0..self.channels { + if chexpstr_plan[ch][blk] != 0 && !(cpl.in_use && cpl.chincpl[ch]) { + bw.write_u32(chbwcod as u32, 6); + } + } + + // Exponents: only transmitted when chexpstr != reuse. + // + // Order per spec §5.4.3.25-29: cplexps (when cplinu), + // exps[0], exps[1], ..., lfeexps. cpl uses cplabsexp + D15 + // grouping over [cpl_begf_mant, cpl_endf_mant) per + // §7.1.3, with the absolute exponent value implied to be + // the 4-bit cplabsexp left-shifted by 1. + if cpl.in_use && exp_strategy == 1 { + let cpl_idx = cpl_idx_in_exps; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + write_exponents_cpl(&mut bw, &exps[cpl_idx][blk], begf_mant, endf_mant); + } + for ch in 0..self.channels { + let strat = chexpstr_plan[ch][blk]; + if strat == 0 { + continue; + } + let grpsize: usize = if strat == 1 { + 1 + } else if strat == 2 { + 2 + } else { + 4 + }; + write_exponents_grouped(&mut bw, &exps[ch][blk], ch_end_mant, grpsize); + bw.write_u32(0, 2); // gainrng = 0 + } + // §5.4.3.29 LFE exponents — D15 over bins 0..7, with + // `nlfegrps=2` (2 groups of 3 deltas after the 4-bit + // absexp). + if self.lfeon && exp_strategy == 1 { + write_exponents_d15(&mut bw, &exps[lfe_idx_in_exps][blk], LFE_END_MANT); + } + + // Bit-allocation side-info: block 0 transmits the parametric + // set + snroffst; later blocks reuse. + let baie = blk == 0; + bw.write_u32(baie as u32, 1); + if baie { + bw.write_u32(tuned_ba.sdcycod as u32, 2); + bw.write_u32(tuned_ba.fdcycod as u32, 2); + bw.write_u32(tuned_ba.sgaincod as u32, 2); + bw.write_u32(tuned_ba.dbpbcod as u32, 2); + bw.write_u32(tuned_ba.floorcod as u32, 3); + } + // §5.4.3.37 snroffste — block 0 is mandatory; on later + // blocks we set snroffste=1 only when the per-block plan + // (#170) carries a value differing from the previous + // emitted set. Otherwise snroffste=0 means "decoder reuses + // the prior block's csnr/fsnr*", which keeps the cost at + // 1 bit when no redistribution was beneficial. + let snroffste = snr_plan.snroffste(blk); + bw.write_u32(snroffste as u32, 1); + if snroffste { + bw.write_u32(snr_plan.csnroffst[blk] as u32, 6); + if cpl.in_use { + // §5.4.3.38 cplfsnroffst (4 bits), §5.4.3.39 cplfgaincod (3 bits). + bw.write_u32(snr_plan.cplfsnroffst[blk] as u32, 4); + bw.write_u32(tuned_ba.cplfgaincod as u32, 3); + } + // §5.4.3.40-41 fsnroffst[ch] (4 bits) + fgaincod[ch] + // (3 bits) per fbw channel. Per-block per-channel + // fsnroffst values come from the round-24 redistribution + // pass; demand-heavy blocks have higher fsnr than the + // frame-wide global baseline picked by `tune_snroffst`. + for ch in 0..self.channels { + bw.write_u32(snr_plan.fsnroffst_ch[blk][ch] as u32, 4); + bw.write_u32(tuned_ba.fgaincod as u32, 3); + } + // §5.4.3.42 lfefsnroffst (4 bits) + §5.4.3.43 lfefgaincod (3 bits). + if self.lfeon { + bw.write_u32(snr_plan.lfefsnroffst[blk] as u32, 4); + bw.write_u32(tuned_ba.lfefgaincod as u32, 3); + } + } + // §5.4.3.44-46 cplleake / cplfleak / cplsleak. The spec + // requires cplleake=1 on the first block where coupling + // is in use (so the decoder gets fresh leak-init values + // for the §7.2.2.4 cpl excitation path); subsequent + // blocks may reuse with cplleake=0. We always send + // cplfleak=cplsleak=0, matching the encoder's + // expectation in the cpl bap routine + // (`compute_bap_cpl` initialises leak from 768 + 0). + if cpl.in_use { + if blk == 0 { + bw.write_u32(1, 1); // cplleake = 1 + bw.write_u32(0, 3); // cplfleak = 0 + bw.write_u32(0, 3); // cplsleak = 0 + } else { + bw.write_u32(0, 1); // cplleake = 0 (reuse) + } + } + // §5.4.3.47-57 deltbaie + delta bit allocation. v1 policy: + // + // Block 0: deltbaie=1, then per-channel deltbae[ch]∈{1,2} + // and (when cpl.in_use) cpldeltbae∈{1,2}. Channels with + // `dba_plan.nseg[ch] > 0` emit '01' (new info follows) + // plus their segment list (cpldeltnseg/cpldeltoffst/... + // for cpl, deltnseg/deltoffst/... for fbw); channels + // with no segments emit '10' (perform no delta alloc). + // + // Blocks 1..5: deltbaie=0. Per §5.4.3.47, "the previously + // transmitted delta bit allocation information still + // applies" — i.e. the decoder keeps applying block 0's + // segments for the rest of the syncframe. The encoder + // side mirrors this by computing bap[] with the same + // dba_plan applied on every block. + // + // Per Table 5.16 ('00' = reuse) is illegal in block 0, so + // channels without segments use '10' there instead. + let any_fbw_dba = (0..self.channels).any(|c| dba_plan.nseg[c] > 0); + let any_cpl_dba = cpl.in_use && dba_plan.nseg[MAX_FBW] > 0; + let any_dba = any_fbw_dba || any_cpl_dba; + if blk == 0 && (any_dba || cpl.in_use) { + bw.write_u32(1, 1); // deltbaie = 1 + if cpl.in_use { + let code = if dba_plan.nseg[MAX_FBW] > 0 { 1 } else { 2 }; + bw.write_u32(code as u32, 2); // cpldeltbae + } + for ch in 0..self.channels { + let code = if dba_plan.nseg[ch] > 0 { 1 } else { 2 }; + bw.write_u32(code as u32, 2); // deltbae[ch] + } + if cpl.in_use && dba_plan.nseg[MAX_FBW] > 0 { + let nseg = dba_plan.nseg[MAX_FBW] as u32; + bw.write_u32(nseg - 1, 3); // cpldeltnseg + for seg in 0..nseg as usize { + // §5.4.3.51 deltoffst is a 5-bit field — clipping + // here is a panic-on-bug guard so future plan + // builders that exceed 31 fail loudly instead of + // silently truncating + mis-targeting the mask + // delta on the decoder side. The wire write below + // would mask to 5 bits regardless. + debug_assert!( + dba_plan.offst[MAX_FBW][seg] <= 31, + "cpldeltoffst[{}]={} exceeds 5-bit field range", + seg, + dba_plan.offst[MAX_FBW][seg] + ); + bw.write_u32(dba_plan.offst[MAX_FBW][seg] as u32, 5); + bw.write_u32(dba_plan.len[MAX_FBW][seg] as u32, 4); + bw.write_u32(dba_plan.ba[MAX_FBW][seg] as u32, 3); + } + } + for ch in 0..self.channels { + if dba_plan.nseg[ch] > 0 { + let nseg = dba_plan.nseg[ch] as u32; + bw.write_u32(nseg - 1, 3); // deltnseg[ch] + for seg in 0..nseg as usize { + debug_assert!( + dba_plan.offst[ch][seg] <= 31, + "deltoffst[ch={}][seg={}]={} exceeds 5-bit field range", + ch, + seg, + dba_plan.offst[ch][seg] + ); + bw.write_u32(dba_plan.offst[ch][seg] as u32, 5); + bw.write_u32(dba_plan.len[ch][seg] as u32, 4); + bw.write_u32(dba_plan.ba[ch][seg] as u32, 3); + } + } + } + } else { + bw.write_u32(0, 1); // deltbaie = 0 (reuse on blocks 1..5) + } + + // skiple / skipl: potentially used at frame-end to pad out to + // frame_bytes; for now, none per block. + bw.write_u32(0, 1); + + // Mantissas per channel. + // + // Pre-compute all mantissa codes for this block first, then + // walk them in decoder-read order and emit each grouped + // quantizer's 5/7-bit packed word at the position of the + // *first* code in the triple/pair. The decoder pre-fetches + // groups (reads 5/7 bits whenever its buffer empties) while + // the natural encoder loop would only emit when the buffer + // fills — an asymmetric mismatch that desyncs the bitstream + // across the channel boundary. By pre-quantising and then + // emitting proactively we match the decoder's expected read + // schedule exactly. + // + // Decoder mantissa-read order (§7.3.2): for each channel + // walk bins 0..ch_end_mant emitting bap values; if the + // channel is coupled and this is the first coupled channel + // we encounter, also walk the coupling channel's + // [cpl_begf_mant, cpl_endf_mant) bap sequence appended to + // that channel's mantissa stream. The encoder must emit + // codes in exactly the same order. + let mut codes: Vec<(u8, u32)> = Vec::with_capacity((self.channels + 2) * ch_end_mant); + let mut got_cplchan = false; + for ch in 0..self.channels { + for bin in 0..ch_end_mant { + let bap = baps[ch][blk][bin]; + if bap == 0 { + continue; + } + let e = exps[ch][blk][bin] as i32; + let mant = quantise_mantissa(coeffs[ch][blk][bin], e, bap); + codes.push((bap, mant)); + } + if cpl.in_use && cpl.chincpl[ch] && !got_cplchan { + got_cplchan = true; + let cpl_idx = cpl_idx_in_exps; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + for bin in begf_mant..endf_mant { + let bap = baps[cpl_idx][blk][bin]; + if bap == 0 { + continue; + } + let e = exps[cpl_idx][blk][bin] as i32; + let mant = quantise_mantissa(cpl_coeffs[blk][bin], e, bap); + codes.push((bap, mant)); + } + } + } + // §7.3.2 — LFE mantissas come last, over bins 0..7. We use + // the LFE channel's own coefficients (live in + // `coeffs[lfe_idx][blk]`). + if self.lfeon { + for bin in 0..LFE_END_MANT { + let bap = baps[lfe_idx_in_exps][blk][bin]; + if bap == 0 { + continue; + } + let e = exps[lfe_idx_in_exps][blk][bin] as i32; + let mant = quantise_mantissa(coeffs[lfe_idx][blk][bin], e, bap); + codes.push((bap, mant)); + } + } + write_mantissa_stream(&mut bw, &codes); + } + + // auxdata: auxdatae=0 plus any necessary skip-padding so the + // remainder of the frame before crc2 (16 bits) is filled with + // zeros. The auxdata() field is defined as (nauxbits) then a + // 1-bit auxdatae flag; we just set the whole field to a zero + // tail up through the last byte before crc2. + // + // Compute how many bits remain before the last 16 bits (crc2). + let target_bits = (self.frame_bytes * 8) as u64; + let used_bits = bw.bit_position(); + let crc2_bits = 16u64; + if used_bits + crc2_bits > target_bits { + return Err(Error::other(format!( + "ac3 encoder: mantissa budget overflow ({} bits used, frame {} bits)", + used_bits, target_bits + ))); + } + let pad_bits = target_bits - used_bits - crc2_bits; + // Write pad_bits of zeros — the final bit before crc2 is + // auxdatae=0 by virtue of being in the padding zone. + let mut left = pad_bits; + while left >= 32 { + bw.write_u32(0, 32); + left -= 32; + } + if left > 0 { + bw.write_u32(0, left as u32); + } + + // Placeholder crc2 — filled after the body is emitted. + bw.write_u32(0, 16); + let mut frame = bw.into_bytes(); + debug_assert_eq!(frame.len(), self.frame_bytes); + + // Compute crc1 over the first 5/8 of the syncframe — the sync + // word (bytes 0..1) is excluded, but the 2-byte crc1 field + // itself (bytes 2..3) is *included*. We need to place a value X + // into bytes 2..3 such that the LFSR residue at the end of the + // 5/8 region is zero. Since the CRC is XOR-linear, we compute + // the residue R_0 with X = 0, and independently compute how a + // 16-bit value placed at bytes 2..3 propagates forward — then + // solve for the X whose propagation cancels R_0. + let frame_words = self.frame_bytes / 2; + let five_eighths_words = (frame_words >> 1) + (frame_words >> 3); + let five_eighths_bytes = five_eighths_words * 2; + let crc1_val = ac3_crc_solve_prefix(&frame[2..five_eighths_bytes]); + frame[2] = (crc1_val >> 8) as u8; + frame[3] = (crc1_val & 0xFF) as u8; + debug_assert_eq!( + ac3_crc_update(0, &frame[2..five_eighths_bytes]), + 0, + "crc1 solver produced a non-zero residue" + ); + + // crc2 covers the entire post-syncword region of the + // syncframe per §7.10.1: "if the calculation is continued + // until all data in the syncframe has been shifted through, + // and the value is again equal to zero, then crc2 is + // considered valid." This is the **augmented-form** + // emit: the LFSR is shifted past the body bytes AND past + // 16 trailing zero bits (the crc2 field placeholder), and + // the resulting register value is written into the crc2 + // field. By the standard CRC-augmented-codeword property + // (`data·x^16 + r(x) ≡ 0 mod g(x)`), the residue check + // `ac3_crc_update(0, &frame[2..frame_bytes]) == 0` then + // succeeds on the decoder side. + // + // We can start the running CRC at byte five_eighths_bytes + // rather than at byte 2 because the crc1 solver above + // already drove the register to zero at the 5/8 boundary + // (asserted by the debug_assert), so the residue from + // [2..five_eighths_bytes] is identically zero and chaining + // forward from five_eighths_bytes is equivalent to chaining + // from byte 2. + let body_residue = ac3_crc_update(0, &frame[five_eighths_bytes..(self.frame_bytes - 2)]); + let crc2_val = ac3_crc_update(body_residue, &[0u8, 0u8]); + let n = self.frame_bytes; + frame[n - 2] = (crc2_val >> 8) as u8; + frame[n - 1] = (crc2_val & 0xFF) as u8; + debug_assert_eq!( + ac3_crc_update(0, &frame[2..self.frame_bytes]), + 0, + "crc2 emit produced a non-zero post-syncword residue" + ); + + self.packet_queue.push( + Packet::new(0, TimeBase::new(1, self.sample_rate as i64), frame).with_pts(self.pts), + ); + self.pts += SAMPLES_PER_FRAME as i64; + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// Transient detection (§7.6 block-switch decision) +// --------------------------------------------------------------------------- + +/// Decide whether a 256-sample input block contains a transient that +/// warrants a short-block MDCT (§7.6.1). The heuristic compares the +/// *high-pass* energy of consecutive 64-sample sub-frames within the +/// block; if any pair's ratio exceeds a fixed threshold the block is +/// flagged short. +/// +/// Why high-pass: a long MDCT smears transients across the 256-sample +/// post-IMDCT support, raising mid/high-frequency noise *across the +/// whole block*. The short-block pair localises the transient to one +/// of the two 128-sample sub-windows, halving the temporal smear. +/// Detecting on the high-pass band keeps the heuristic insensitive +/// to slowly-varying low-frequency content (e.g. a 50 Hz hum) while +/// reacting strongly to drum hits / clicks. +/// +/// The IIR filter is the simplest stable HP: `y[n] = x[n] - x[n-1]`, +/// a one-tap differentiator that doubles the noise floor on white +/// input but adds zero state — important because the transient +/// detector is invoked once per block per channel and we don't carry +/// per-channel HP filter state across blocks (each block decides +/// independently — adjacent transients on different channels are +/// allowed to flip blksw differently per spec §5.4.3.1). +/// +/// Spec-faithful (ATSC A/52 §8.2.2) per-channel transient detector. +/// +/// Holds the cascaded biquad HPF state across 256-sample blocks plus +/// the prior block's last-segment peak per hierarchy level so the +/// "P[j][0] = previous tree's last segment peak" rule of step 4 has +/// the right history. +/// +/// The HPF is a 4th-order Butterworth high-pass at 8 kHz cutoff @ +/// 48 kHz sample rate, implemented as two cascaded direct-form-I +/// biquads with Butterworth Q values (~0.541 and ~1.307) producing a +/// 24 dB/oct rolloff below 8 kHz. Coefficients are pre-computed for +/// (fc=8000, fs=48000) — the §8.2.2 spec doesn't bind the filter +/// coefficients but does bind the topology and cutoff. +#[derive(Clone, Default)] +pub(crate) struct TransientDetector { + /// Direct-form-I biquad memory: [x[n-1], x[n-2], y[n-1], y[n-2]] + /// for stage 0 (low Q) and stage 1 (high Q). + biquad_state: [[f32; 4]; 2], + /// Last-segment peak per hierarchy level from the previous block, + /// representing P[1][0] / P[2][0] / P[3][0] in the §8.2.2 + /// formulation (the "k=0" entry is the previous tree's last + /// segment). + prev_peak_l1: f32, + prev_peak_l2: f32, + prev_peak_l3: f32, + /// `false` until the first block has been processed. The very + /// first call sees zeroed biquad state, which means the HPF has a + /// startup transient over the first ~10 samples regardless of + /// input — that transient looks like a sharp onset to the + /// segment-1-vs-prior-block comparison and would over-trigger. + /// We therefore skip the k=1 (cross-block) parts of the §8.2.2 + /// step-4 test on the first call only. + primed: bool, +} + +/// Cascaded-biquad direct-form-I 4th-order Butterworth HPF at 8 kHz @ +/// 48 kHz. Coefficients computed via bilinear-transform RBJ HPF +/// formulae with the two stage-Q values for a 4th-order Butterworth +/// (Q₁ ≈ 0.5412, Q₂ ≈ 1.3066). +/// +/// Each biquad: `y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] +/// - a1*y[n-1] - a2*y[n-2]`. +/// Indexing: `[b0, b1, b2, a1, a2]` (a0 normalised to 1). +const HPF_8K_BIQUADS: [[f32; 5]; 2] = [ + // Stage 0 — low Q (≈ 0.5412), more damped pole pair. + [ + 0.416_658_9, + -0.833_317_8, + 0.416_658_9, + -0.555_545_6, + 0.111_09, + ], + // Stage 1 — high Q (≈ 1.3066), peaking pole pair. + [ + 0.563_325_9, + -1.126_651_7, + 0.563_325_9, + -0.751_113, + 0.502_226_3, + ], +]; + +impl TransientDetector { + /// Run the 256-sample second-half through the HPF and the + /// hierarchical peak-ratio test. Returns `true` when a transient + /// is detected per §8.2.2 step 4. + pub(crate) fn process(&mut self, block: &[f32]) -> bool { + if block.len() < 256 { + // Pre-priming or partial input — never short-block. + return false; + } + // 1) High-pass filter — cascaded biquad direct-form-I. + let mut hp = [0.0f32; 256]; + for i in 0..256 { + let mut x = block[i]; + for stage in 0..2 { + let s = &mut self.biquad_state[stage]; + let c = &HPF_8K_BIQUADS[stage]; + let y = c[0] * x + c[1] * s[0] + c[2] * s[1] - c[3] * s[2] - c[4] * s[3]; + // Shift state. + s[1] = s[0]; // x[n-2] = x[n-1] + s[0] = x; // x[n-1] = x[n] + s[3] = s[2]; // y[n-2] = y[n-1] + s[2] = y; // y[n-1] = y[n] + x = y; + } + hp[i] = x; + } + // 4a) Silence threshold — if the overall block peak is below + // 100/32768 ≈ 0.003, force long block regardless of relative + // peaks. + let p11 = hp.iter().fold(0.0f32, |a, &v| a.max(v.abs())); + const SILENCE_THRESHOLD: f32 = 100.0 / 32768.0; + if p11 < SILENCE_THRESHOLD { + // Persist last-segment peaks for the next block (use 0; + // silence has no carry-over significance). + self.prev_peak_l1 = p11; + self.prev_peak_l2 = peak(&hp[128..256]); + self.prev_peak_l3 = peak(&hp[192..256]); + return false; + } + // 2/3) Hierarchical peak detection. + // Level 1: P[1][1] = peak over [0,256) — already = p11. + // Level 2: P[2][1] = peak over [0,128); P[2][2] = peak over [128,256). + // Level 3: P[3][1..4] = peak over each 64-sample segment. + let p21 = peak(&hp[0..128]); + let p22 = peak(&hp[128..256]); + let p31 = peak(&hp[0..64]); + let p32 = peak(&hp[64..128]); + let p33 = peak(&hp[128..192]); + let p34 = peak(&hp[192..256]); + // Threshold ratios per §8.2.2 step 4: |P[j][k]| × T[j] > |P[j][k-1]| + // means the new peak is more than 1/T[j] times the previous — + // i.e. a sharp rise. For k=1 the previous-segment reference is + // the prior block's last-segment peak (P[j][0]). + const T1: f32 = 0.1; + const T2: f32 = 0.075; + const T3: f32 = 0.05; + // We also guard against division-by-zero when the previous + // segment was effectively silent. The spec test is multiplicative + // (no division), so silence → previous peak ≈ 0 → any positive + // current peak satisfies the inequality. The silence-threshold + // check above (step 4a) already short-circuits the + // "everything-quiet" block; here we just need a tiny epsilon to + // keep the comparison numerically sane for adjacent-segment + // pairs where one segment landed near a HPF zero-crossing. + // Setting this too high (e.g. 100/32768) suppresses real burst + // detection where a near-silent pre-burst segment is followed by + // a moderate-amplitude attack — the pure-tone case is already + // ruled out by the 8 kHz HPF's removal of the carrier. + const PREV_FLOOR: f32 = 1e-8; + // Cross-block (k=1) comparisons. Skip on the very first call + // because the un-primed biquad startup transient would always + // fire them. + let (trig_l1_x, trig_l2_x, trig_l3_x) = if self.primed { + ( + p11 * T1 > self.prev_peak_l1.max(PREV_FLOOR), + p21 * T2 > self.prev_peak_l2.max(PREV_FLOOR), + p31 * T3 > self.prev_peak_l3.max(PREV_FLOOR), + ) + } else { + (false, false, false) + }; + let trig_l2 = trig_l2_x || (p22 * T2 > p21.max(PREV_FLOOR)); + let trig_l3 = trig_l3_x + || (p32 * T3 > p31.max(PREV_FLOOR)) + || (p33 * T3 > p32.max(PREV_FLOOR)) + || (p34 * T3 > p33.max(PREV_FLOOR)); + let triggered = trig_l1_x || trig_l2 || trig_l3; + // Persist this block's last-segment peaks for the next call. + self.prev_peak_l1 = p11; + self.prev_peak_l2 = p22; + self.prev_peak_l3 = p34; + self.primed = true; + triggered + } +} + +#[inline] +fn peak(seg: &[f32]) -> f32 { + seg.iter().fold(0.0f32, |a, &v| a.max(v.abs())) +} + +/// Stateless wrapper around [`TransientDetector::process`] for the +/// existing `transient_detector_sanity` smoke test, which doesn't +/// thread per-call state (single-shot inputs). +#[cfg(test)] +fn detect_transient(block: &[f32]) -> bool { + TransientDetector::default().process(block) +} + +// --------------------------------------------------------------------------- +// Exponent extraction + D15 encoding +// --------------------------------------------------------------------------- + +/// Compute the AC-3 exponent for a single coefficient: the number of +/// left shifts that would bring `|x|` to the interval `[0.5, 1)`, clamped +/// to `0..=24` (§8.2.7 extract_exponents). +pub(crate) fn extract_exponent(x: f32) -> u8 { + let ax = x.abs(); + if ax < f32::MIN_POSITIVE { + return 24; + } + // x = m * 2^-e with |m| in [0.5, 1) ⇒ e = -floor(log2(ax)) - 1, + // which for ax ∈ [2^-25, 1) lies in 0..=24. + let e = (-ax.log2().floor() as i32) - 1; + e.clamp(0, 24) as u8 +} + +/// Pre-process the D15 exponent run so that successive differences +/// stay in `[-2, +2]` **and** the absolute-value constraints of the +/// bitstream layout hold (absolute exponent fits in 4 bits → `0..=15`; +/// subsequent exponents remain ≥0 after the decoder replays the +/// differences). Two-pass implementation: +/// +/// 1. **Backward pass** — propagate low (loud-bin) exponents *toward* +/// the start of the array. For each bin, `exp[i]` must ≤ `exp[i+1] + 2` +/// so the encoder can reach the loud bin's exponent within the D15 +/// per-step slope. Without this, a narrow-band spike (e.g. a sine +/// tone) surrounded by silent (exp=24) bins would force the encoder +/// to stay at 24 until two bins before the spike, then step down in +/// ±2 increments — which can't reach exp=1 in time. The backward +/// pass pre-drops the silent bins' exponents so the decoder can +/// reconstruct the spike's exponent accurately. +/// 2. **Forward pass** — legalise absexp to 4 bits, then clamp each +/// forward delta to `±2` and each running value to `[0, 24]`. After +/// the backward pass, the forward pass is typically a no-op; it's +/// kept to guarantee legality for pathological inputs. +/// +/// `exp` is mutated in place. +pub(crate) fn preprocess_d15(exp: &mut [u8]) { + if exp.is_empty() { + return; + } + // Backward pass: ensure exp[i] ≤ exp[i+1] + 2 for every adjacent + // pair. Propagates low values leftward at the maximum legal slope. + for i in (0..exp.len() - 1).rev() { + let next_plus_two = (exp[i + 1] as i32 + 2).min(24) as u8; + if exp[i] > next_plus_two { + exp[i] = next_plus_two; + } + } + // absexp (exps[0]) is transmitted in 4 bits → 0..=15. + if exp[0] > 15 { + exp[0] = 15; + } + // Forward pass: clamp each delta to ±2 and the running value to [0, 24]. + for i in 1..exp.len() { + let prev = exp[i - 1] as i32; + let cur = exp[i] as i32; + let d = cur - prev; + let clamped = d.clamp(-2, 2); + exp[i] = (prev + clamped).clamp(0, 24) as u8; + } +} + +/// Write D15 exponents for the coupling pseudo-channel per §5.4.3.25 + +/// §7.1.3. Differs from the fbw `write_exponents_d15` in two ways: +/// +/// 1. **`cplabsexp` is a *reference* not a real exponent.** The 4-bit +/// `cplabsexp` field is left-shifted by 1 to seed the differential +/// decoder; the first transmitted exponent is `exp[cpl_strtmant]` +/// (no `exp[0]` involved). We pick `cplabsexp` ≈ `exp[cpl_strtmant] +/// / 2` so the first delta lies inside ±2. +/// 2. **Groups span `[cpl_strtmant, cpl_endmant)`.** With D15 grpsize=1 +/// that's `ncplgrps = (cpl_endmant - cpl_strtmant) / 3` 7-bit words. +pub(crate) fn write_exponents_cpl( + bw: &mut BitWriter, + exp: &[u8; N_COEFFS], + start: usize, + end: usize, +) { + if end <= start { + return; + } + // Pick cplabsexp such that (cplabsexp << 1) is the closest even + // value to exp[start], clamped to the 4-bit range. Enforce that + // the first delta lies in ±2 by clamping the start exponent first. + // + // Note: the decoder uses `prev = cplabsexp << 1` as the seed, with + // values up to 30 — but the deltas reconstructed must keep the + // running exp in [0, 24]. Clamping cplabsexp to 12 (= 24/2) avoids + // the case where prev = 30 + small negative delta lands above 24, + // which the spec's §7.1 exponent envelope (and the validator + // binary's dexp validity check) rejects as out-of-range. + let first_exp = exp[start] as i32; + let cplabsexp = ((first_exp + 1) >> 1).clamp(0, 12) as u8; + bw.write_u32(cplabsexp as u32, 4); + let ncplgrps = (end - start) / 3; + let mut prev = (cplabsexp as i32) << 1; + for grp in 0..ncplgrps { + let base = start + grp * 3; + let e0 = exp[base] as i32; + let e1 = exp[base + 1] as i32; + let e2 = exp[base + 2] as i32; + let d0 = (e0 - prev).clamp(-2, 2) + 2; + let d1 = (e1 - e0).clamp(-2, 2) + 2; + let d2 = (e2 - e1).clamp(-2, 2) + 2; + let packed: u32 = (25 * d0 + 5 * d1 + d2) as u32; + debug_assert!( + packed <= 124, + "cpl D15 group out of range: d0={d0} d1={d1} d2={d2} packed={packed}", + ); + bw.write_u32(packed, 7); + prev = e2; + } +} + +/// Write D15 exponents per §7.1.3 / §5.4.3.16+. D15 is `grpsize = 1`: +/// every raw exponent carries one delta. The first absolute exponent is +/// 4 bits (exps[ch][0]); subsequent exponents are packed in groups of +/// three (ngrps = (end-1)/3) as a single 7-bit word encoding three +/// `(dexp+2)` values via m = 25*(dexp0+2) + 5*(dexp1+2) + (dexp2+2). +pub(crate) fn write_exponents_d15(bw: &mut BitWriter, exp: &[u8; N_COEFFS], end: usize) { + write_exponents_grouped(bw, exp, end, 1); +} + +/// Generic per-§7.1.3 grouped exponent emitter. `grpsize` ∈ {1, 2, 4} +/// selects the strategy: D15 (=1), D25 (=2), D45 (=4). The bit-stream +/// layout is the same in every case — 4-bit absexp followed by N +/// 7-bit packed groups — only `N = ngrps_for_strategy(end, grpsize)` +/// changes. Per-strategy `nchgrps` matches the decoder side +/// (`audblk::decode_exponents`): +/// +/// * D15 → ngrps = (end - 1) / 3 — 1 delta per bin +/// * D25 → ngrps = (end - 1 + 3) / 6 — 1 delta per 2 bins +/// * D45 → ngrps = (end - 1 + 9) / 12 — 1 delta per 4 bins +/// +/// For grpsize > 1 the caller is expected to have *already* averaged +/// the per-bin raw exponents down to one representative per grpsize +/// span (see `quantise_exponents_to_grpsize`). Without that pre-pass +/// the deltas would clip wildly when bin energies vary inside a group. +pub(crate) fn write_exponents_grouped( + bw: &mut BitWriter, + exp: &[u8; N_COEFFS], + end: usize, + grpsize: usize, +) { + if end == 0 { + return; + } + let absexp = exp[0]; + bw.write_u32(absexp as u32, 4); + let ngrps = ngrps_for_strategy(end, grpsize); + // The caller has already invoked `quantise_exponents_to_grpsize` + // (or `preprocess_d15` for grpsize=1) so adjacent representatives + // already differ by at most 2. We just need to walk the array, + // sampling one representative per grpsize span, and pack them + // into 7-bit deltas. + let mut prev = absexp as i32; + for grp in 0..ngrps { + // The three representatives this group encodes live at + // bin positions (1 + (grp*3 + 0..3)*grpsize). When `end` is + // smaller than that range the spec implicitly pads with the + // last representative (delta = 0); the decoder's grpsize + // expansion will write past `end` only if our `end`/`ngrps` + // pair disagrees with the decoder's, so we emit zero-deltas + // for any pad slot. + let p0 = 1 + (grp * 3) * grpsize; + let p1 = p0 + grpsize; + let p2 = p1 + grpsize; + let e0 = if p0 < end { exp[p0] as i32 } else { prev }; + let e1 = if p1 < end { exp[p1] as i32 } else { e0 }; + let e2 = if p2 < end { exp[p2] as i32 } else { e1 }; + let d0 = (e0 - prev).clamp(-2, 2) + 2; + let d1 = (e1 - e0).clamp(-2, 2) + 2; + let d2 = (e2 - e1).clamp(-2, 2) + 2; + let packed: u32 = (25 * d0 + 5 * d1 + d2) as u32; + bw.write_u32(packed, 7); + prev = e2; + } +} + +/// Match-decoder formula for the number of 7-bit exponent groups +/// transmitted under each strategy (§7.1.3, mirrors +/// `audblk::decode_exponents`'s grpsize→nchgrps switch). +pub(crate) fn ngrps_for_strategy(end: usize, grpsize: usize) -> usize { + if end == 0 { + return 0; + } + match grpsize { + 1 => (end - 1) / 3, + 2 => (end - 1 + 3) / 6, + 4 => (end - 1 + 9) / 12, + _ => 0, + } +} + +/// Quantise per-bin raw exponents down to one representative per +/// grpsize span, clamp deltas between successive representatives to +/// ±2 (the AC-3 differential encoding limit), then expand back to +/// per-bin (replicating the representative) so the bit allocator + +/// mantissa quantiser see the exponents the decoder will actually +/// reconstruct. +/// +/// Operates in place. For grpsize=1 this is a no-op (the caller has +/// already invoked `preprocess_d15` for the D15 case). For grpsize>1 +/// the representative is the minimum of the span (covers the loudest +/// bin in the group; matches `write_exponents_grouped`). +pub(crate) fn quantise_exponents_to_grpsize(exp: &mut [u8], grpsize: usize) { + if grpsize <= 1 || exp.len() < 2 { + return; + } + let n = exp.len(); + // Bin 0 is the absexp seed; D15 preprocessing already legalised + // it to ≤15 (4-bit absexp range). + if exp[0] > 15 { + exp[0] = 15; + } + // Pass 1 (in place): replace each grpsize-span starting at bin 1 + // with the minimum of the span — that's the largest-magnitude + // bin's exponent and so will not clip on quantisation. + let mut i = 1usize; + while i < n { + let span_end = (i + grpsize).min(n); + let mut m = exp[i]; + for k in (i + 1)..span_end { + if exp[k] < m { + m = exp[k]; + } + } + for k in i..span_end { + exp[k] = m; + } + i = span_end; + } + // Pass 2: walk the representative sequence (one rep per grpsize + // span starting at bin 1) and clamp the delta from one rep to the + // next to ±2. The decoder packs each representative as a single + // ±2 delta against the prior representative, so any jump beyond + // ±2 would be silently clamped on decode and the encoder's bit + // allocator would diverge from the decoder's reconstruction. + // + // Walk both forward (push reps down toward subsequent loud bins) + // and back-prop (let a quiet rep pull its predecessors down so a + // sudden silence after a loud span doesn't leave the encoder + // stuck at exp=0 across a 4-bin span). Two passes mirror the + // `preprocess_d15` shape. + // Backward pass: rep[i] ≤ rep[i+grpsize] + 2 (where indices refer + // to bin positions, but reps are constant within a grpsize span + // so we can compare endpoint values). + let mut i = if n > grpsize { n - grpsize } else { 1 }; + while i > grpsize { + let next_first = i; // first bin of next span + let cur_first = i - grpsize; // first bin of current span + let next_plus_two = (exp[next_first] as i32 + 2).min(24) as u8; + if exp[cur_first] > next_plus_two { + // re-stamp the entire current span. + let span_end = (cur_first + grpsize).min(n); + for k in cur_first..span_end { + exp[k] = next_plus_two; + } + } + if i <= grpsize { + break; + } + i -= grpsize; + } + // Back-prop the absexp slot too: exp[0] ≤ exp[1] + 2. + let next_plus_two = (exp[1] as i32 + 2).min(15) as u8; + if exp[0] > next_plus_two { + exp[0] = next_plus_two; + } + // Forward pass: clamp each forward delta to ±2 and the running + // value to [0, 24]. The first rep's delta is against absexp; each + // subsequent rep's delta is against the previous rep. + let mut prev = exp[0]; + let mut i = 1usize; + while i < n { + let span_end = (i + grpsize).min(n); + let target = exp[i]; + let d = (target as i32 - prev as i32).clamp(-2, 2); + let new_val = ((prev as i32 + d).clamp(0, 24)) as u8; + for k in i..span_end { + exp[k] = new_val; + } + prev = new_val; + i = span_end; + } +} + +/// Pick the smoothest strategy (D15 / D25 / D45) for one channel's +/// block of raw exponents. "Smoothest" = the strategy that, after the +/// grpsize-merge, loses the *least* energy resolution. We use the +/// per-bin clipping cost the merge would cause: a bin clipped from +/// `e` to `min(e, e_neighbour)` loses `(e - shared_e)` units of +/// dynamic range. Pick the largest grpsize whose total clipping cost +/// across the band stays below thresholds derived from the bit +/// budget of the alternative. +/// +/// Returns one of `1` (D15), `2` (D25), `3` (D45). Never returns 0 +/// (REUSE) — that's a higher-level decision per-block. +/// +/// The thresholds reflect the bit savings: D45 vs D15 saves +/// ~(d15_bits - d45_bits) bits, so the merge can absorb up to +/// ~(savings / 4) units of clipping (each clipped bin upcasts a +/// mantissa bap by ~1 ⇒ ~4 bit cost). Empirically tuned for the +/// `chbwcod=60` (end=252) case where d15=4+7×83=585, d45=4+7×21=151. +pub(crate) fn pick_strategy_for_block(exp: &[u8], end: usize) -> u8 { + if end <= 1 { + return 1; + } + // D45 cost: how much mantissa-side dynamic range we'd burn if we + // collapsed every 4-bin span to its minimum exponent. + let mut cost_d45 = 0u32; + let mut cost_d25 = 0u32; + let mut i = 1usize; + while i < end { + let s4_end = (i + 4).min(end); + let s2_end = (i + 2).min(end); + let mut m4 = exp[i]; + for k in (i + 1)..s4_end { + if exp[k] < m4 { + m4 = exp[k]; + } + } + for k in i..s4_end { + cost_d45 += (exp[k] - m4) as u32; + } + // D25 cost computed on the leading half of the same span. + let mut m2 = exp[i]; + for k in (i + 1)..s2_end { + if exp[k] < m2 { + m2 = exp[k]; + } + } + for k in i..s2_end { + cost_d25 += (exp[k] - m2) as u32; + } + // Continue with the next D25 pair so cost_d25 covers all bins. + if s2_end < s4_end { + let mut m2b = exp[s2_end]; + for k in (s2_end + 1)..s4_end { + if exp[k] < m2b { + m2b = exp[k]; + } + } + for k in s2_end..s4_end { + cost_d25 += (exp[k] - m2b) as u32; + } + } + i = s4_end; + } + // Thresholds: per-bin avg clipping budget. D45 saves ~430 bits vs + // D15 on a full-bandwidth channel; spending up to 1 bit/bin on + // dynamic-range loss is well worth it. D25 saves ~290 bits vs D15. + let bins = (end - 1) as u32; + if bins == 0 { + return 1; + } + let d45_avg_x100 = (cost_d45 * 100) / bins; + let d25_avg_x100 = (cost_d25 * 100) / bins; + // < 0.5 exp-units/bin avg ⇒ D45 is cheap enough. + // Selection thresholds (tunable via env for empirical sweeps). + // Lower = more aggressive (more D25/D45). Higher = more conservative + // (more D15). Defaults derived from preserved-PSNR experiments on + // the round 28 / task #324 fixtures. + let d45_thr = std::env::var("AC3_EXPSTR_D45_THR") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(50); + let d25_thr = std::env::var("AC3_EXPSTR_D25_THR") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(75); + // D45 is enabled by default since round 29 — the prior frame-1 + // mantissa-stream desync was caused by `build_dba_plan` letting the + // chosen DBA band exceed 31 (the 5-bit `deltoffst` field range per + // §5.4.3.51). Wire-side truncation re-targeted the +6 dB mask delta + // at a low band that the encoder had not tagged, drifting `bap[]` + // by 1 at that bin and shifting the rest of the mantissa stream. + // The fix clamped `hi_band ≤ 32` in `build_dba_plan`; D45 now + // round-trips bit-exact through the decoder. Set + // `AC3_DISABLE_D45=1` to fall back to D25-only for A/B sweeps. + let d45_enabled = std::env::var("AC3_DISABLE_D45").is_err(); + let pick = if d45_enabled && d45_avg_x100 <= d45_thr { + 3 + } else if d25_avg_x100 <= d25_thr { + 2 + } else { + 1 + }; + if let Ok(force) = std::env::var("AC3_FORCE_EXPSTR") { + if let Ok(v) = force.parse::() { + if (1..=3).contains(&v) { + return v; + } + } + } + pick +} + +/// Pick a per-channel-per-block exponent strategy plan. The plan +/// honours the encoder's anchor-block convention (D15/D25/D45 on +/// blocks 0 and 3, REUSE elsewhere — cadence chosen by the existing +/// snr-offset and dba state machinery) but lets each anchor block +/// pick the cheapest strategy that still represents its spectrum. +/// +/// `exps[ch][blk]` = pre-D15-preprocessed raw exponents. +/// `nchan` = number of fbw channels (0..nchan-1 indexed). +/// `end` = ch_end_mant. +/// +/// Returned shape `[ch][blk]` matches `chexpstr[ch]` semantics: +/// 0 = REUSE, 1 = D15, 2 = D25, 3 = D45. +pub(crate) fn select_exp_strategies( + exps: &[Vec<[u8; N_COEFFS]>], + nchan: usize, + end: usize, +) -> Vec<[u8; BLOCKS_PER_FRAME]> { + select_exp_strategies_per_end(exps, nchan, &vec![end; nchan]) +} + +/// [`select_exp_strategies`] with a per-channel coded bandwidth — +/// needed when a subset of channels is in spectral extension (their +/// exponent sets stop at the SPX begin frequency while full-bandwidth +/// siblings run to the chbwcod-derived end). +pub(crate) fn select_exp_strategies_per_end( + exps: &[Vec<[u8; N_COEFFS]>], + nchan: usize, + ends: &[usize], +) -> Vec<[u8; BLOCKS_PER_FRAME]> { + let mut out = vec![[0u8; BLOCKS_PER_FRAME]; nchan]; + for (ch, plan) in out.iter_mut().enumerate().take(nchan) { + // Anchor pattern: blocks 0 and 3 are "new". Pick the + // cheapest legal strategy for each anchor based on its + // smoothness; the in-between blocks REUSE. + let s0 = pick_strategy_for_block(&exps[ch][0], ends[ch]); + let s3 = pick_strategy_for_block(&exps[ch][3], ends[ch]); + plan[0] = s0; + plan[1] = 0; + plan[2] = 0; + plan[3] = s3; + plan[4] = 0; + plan[5] = 0; + } + out +} + +// --------------------------------------------------------------------------- +// Bit allocation (encoder-side) — runs the same §7.2.2 routine the +// decoder uses, but retains the bap array for mantissa quantisation. +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +#[allow(dead_code)] +pub(crate) struct BitAllocParams { + pub(crate) sdcycod: u8, + pub(crate) fdcycod: u8, + pub(crate) sgaincod: u8, + pub(crate) dbpbcod: u8, + pub(crate) floorcod: u8, + pub(crate) csnroffst: u8, + /// Base / "global" fsnroffst for fbw channels. After + /// `tune_snroffst` runs, the per-channel array `fsnroffst_ch` + /// carries the per-channel tuned values (each ≥ this base) and the + /// bitstream emitter writes the per-channel values. The base value + /// is also the one fed into `compute_bap_cpl` when its caller + /// substitutes `fsnroffst = cplfsnroffst`. + pub(crate) fsnroffst: u8, + /// Per-fbw-channel fine SNR offset (§5.4.3.40). Bitstream-level + /// width is 4 bits / channel. Defaults to `[fsnroffst; MAX_FBW]` + /// when the per-channel tuner hasn't run yet. + pub(crate) fsnroffst_ch: [u8; MAX_FBW], + pub(crate) cplfsnroffst: u8, + pub(crate) lfefsnroffst: u8, + pub(crate) fgaincod: u8, + pub(crate) cplfgaincod: u8, + pub(crate) lfefgaincod: u8, +} + +/// Run the parametric bit allocator for one channel (start=0..end) +/// and fill `bap_out` with the resulting pointers. +/// +/// `dba_segments`: optional borrow of `(plan, idx)` selecting which +/// channel's dba segments to apply to the masking curve before bap[] +/// is computed. Pass `None` when the encoder will signal deltbae==2 for +/// this channel (no delta this block) — the decoder behaves the same +/// way under that signal. +pub(crate) fn compute_bap( + exp: &[u8; N_COEFFS], + end: usize, + fscod: u8, + ba: &BitAllocParams, + bap_out: &mut [u8; N_COEFFS], + dba: Option<(&DbaPlan, usize)>, +) { + compute_bap_table(exp, end, fscod, ba, bap_out, dba, &BAPTAB) +} + +/// [`compute_bap`] with a caller-selected 64-entry pointer table for +/// the final address lookup. Base AC-3 passes `BAPTAB` (§7.2.2.4); +/// the E-AC-3 AHT path passes the Annex E Table E3.1 `hebaptab[]` to +/// derive the high-efficiency `hebap[]` array from the SAME psd / +/// excitation / masking pipeline (§3.4.3.1: "the bit allocation +/// routine for that channel is modified to incorporate the new high +/// efficiency bit allocation pointers" — only the last table lookup +/// differs). +pub(crate) fn compute_bap_table( + exp: &[u8; N_COEFFS], + end: usize, + fscod: u8, + ba: &BitAllocParams, + bap_out: &mut [u8; N_COEFFS], + dba: Option<(&DbaPlan, usize)>, + ptr_tab: &[u8; 64], +) { + if end == 0 { + return; + } + // PSD + let mut psd = [0i32; N_COEFFS]; + for bin in 0..end { + psd[bin] = 3072 - ((exp[bin] as i32) << 7); + } + // Band PSD + let mut bndpsd = [0i32; 50]; + let bndstrt = MASKTAB[0] as usize; + let bndend = MASKTAB[end - 1] as usize + 1; + { + let mut j = 0usize; + let mut k = bndstrt; + loop { + let lastbin = (BNDTAB[k] as usize + BNDSZ[k] as usize).min(end); + bndpsd[k] = psd[j]; + j += 1; + while j < lastbin { + bndpsd[k] = logadd(bndpsd[k], psd[j]); + j += 1; + } + k += 1; + if end <= lastbin { + break; + } + } + } + // Excitation — fbw, non-coupled path. + let sdecay = SLOWDEC[ba.sdcycod as usize]; + let fdecay = FASTDEC[ba.fdcycod as usize]; + let sgain = SLOWGAIN[ba.sgaincod as usize]; + let dbknee = DBPBTAB[ba.dbpbcod as usize]; + let floor = FLOORTAB[ba.floorcod as usize]; + let fgain = FASTGAIN[ba.fgaincod as usize]; + let snroffset = (((ba.csnroffst as i32 - 15) << 4) + ba.fsnroffst as i32) << 2; + + let mut excite = [0i32; 50]; + let mut lowcomp = 0i32; + // §7.2.2.4 fbw path (start == 0). The `lfe_last` flag mirrors the + // decoder's same-named branch: when this channel is the LFE + // (end == 7), we skip the calc_lowcomp call at bin=6 so encoder + // and decoder derive the same excitation/mask/bap[] arrays. + let lfe_last = end == 7; + if bndend > 0 { + lowcomp = calc_lowcomp(lowcomp, bndpsd[0], bndpsd[1], 0); + excite[0] = bndpsd[0] - fgain - lowcomp; + } + if bndend > 1 { + lowcomp = calc_lowcomp(lowcomp, bndpsd[1], bndpsd[2], 1); + excite[1] = bndpsd[1] - fgain - lowcomp; + } + let mut begin = 7.min(bndend); + let mut fastleak = 0i32; + let mut slowleak = 0i32; + for bin in 2..7.min(bndend) { + if !(lfe_last && bin == 6) { + lowcomp = calc_lowcomp(lowcomp, bndpsd[bin], bndpsd[bin + 1], bin); + } + fastleak = bndpsd[bin] - fgain; + slowleak = bndpsd[bin] - sgain; + excite[bin] = fastleak - lowcomp; + if !(lfe_last && bin == 6) && bndpsd[bin] <= bndpsd[bin + 1] { + begin = bin + 1; + break; + } + } + for bin in begin..22.min(bndend) { + if !(lfe_last && bin == 6) { + lowcomp = calc_lowcomp(lowcomp, bndpsd[bin], bndpsd[bin + 1], bin); + } + fastleak -= fdecay; + fastleak = fastleak.max(bndpsd[bin] - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bndpsd[bin] - sgain); + excite[bin] = (fastleak - lowcomp).max(slowleak); + } + if bndend > 22 { + for bin in 22..bndend { + fastleak -= fdecay; + fastleak = fastleak.max(bndpsd[bin] - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bndpsd[bin] - sgain); + excite[bin] = fastleak.max(slowleak); + } + } + // Mask. + let hth_row = &HTH[fscod as usize]; + let mut mask = [0i32; 50]; + for bin in bndstrt..bndend { + let mut exc = excite[bin]; + if bndpsd[bin] < dbknee { + exc += (dbknee - bndpsd[bin]) >> 2; + } + mask[bin] = exc.max(hth_row[bin] as i32); + } + // §7.2.2.6 delta bit allocation — apply BEFORE bap[] computation, + // exactly mirroring the decoder. The mask offsets in dba can be + // either negative (more bits assigned in that band) or positive + // (fewer bits — what our encoder picks by default to free budget + // for snroffst). + if let Some((plan, idx)) = dba { + apply_dba_segments(plan, idx, &mut mask); + } + // bap. + let mut i = 0usize; + let mut j = MASKTAB[0] as usize; + loop { + let lastbin = (BNDTAB[j] as usize + BNDSZ[j] as usize).min(end); + let mut m = mask[j]; + m -= snroffset; + m -= floor; + if m < 0 { + m = 0; + } + m &= 0x1fe0; + m += floor; + while i < lastbin { + let addr = ((psd[i] - m) >> 5).clamp(0, 63) as usize; + bap_out[i] = ptr_tab[addr]; + i += 1; + } + if i >= end { + break; + } + j += 1; + } +} + +/// Bit allocation for the coupling pseudo-channel, mirroring the +/// decoder's `run_bit_allocation(..., is_coupling=true)` path. +/// +/// Differences from the fbw `compute_bap`: +/// * no lowcomp / start-of-spectrum special cases — the cpl region +/// starts mid-spectrum so the leak filters init from `768` and run +/// the simple `fastleak.max(slowleak)` excitation across every +/// band. +/// * `start = cpl_begf_mant`, `end = cpl_endf_mant` (in coefficient +/// bins). Bands are derived via MASKTAB[bin] just like fbw. +/// +/// `ba.fsnroffst` and `ba.fgaincod` should be the cpl-specific +/// values (cplfsnroffst, cplfgaincod) — the caller is responsible for +/// substituting them into the BitAllocParams before calling. +pub(crate) fn compute_bap_cpl( + exp: &[u8; N_COEFFS], + start: usize, + end: usize, + fscod: u8, + ba: &BitAllocParams, + bap_out: &mut [u8; N_COEFFS], + dba: Option<(&DbaPlan, usize)>, +) { + if end <= start { + return; + } + let mut psd = [0i32; N_COEFFS]; + for bin in start..end { + psd[bin] = 3072 - ((exp[bin] as i32) << 7); + } + let bndstrt = MASKTAB[start] as usize; + let bndend = MASKTAB[end - 1] as usize + 1; + let mut bndpsd = [0i32; 50]; + { + let mut j = start; + let mut k = bndstrt; + loop { + let lastbin = (BNDTAB[k] as usize + BNDSZ[k] as usize).min(end); + bndpsd[k] = psd[j]; + j += 1; + while j < lastbin { + bndpsd[k] = logadd(bndpsd[k], psd[j]); + j += 1; + } + k += 1; + if end <= lastbin { + break; + } + } + } + let sdecay = SLOWDEC[ba.sdcycod as usize]; + let fdecay = FASTDEC[ba.fdcycod as usize]; + let sgain = SLOWGAIN[ba.sgaincod as usize]; + let dbknee = DBPBTAB[ba.dbpbcod as usize]; + let floor = FLOORTAB[ba.floorcod as usize]; + let fgain = FASTGAIN[ba.fgaincod as usize]; + let snroffset = (((ba.csnroffst as i32 - 15) << 4) + ba.fsnroffst as i32) << 2; + // §7.2.2.4 cpl path. cpl_fleak / cpl_sleak default to 0 (no + // cplleake transmitted), giving fastleak_init = slowleak_init = 0 + // and the +768 offset applied in the decoder. + let mut fastleak = 768i32; + let mut slowleak = 768i32; + let mut excite = [0i32; 50]; + for bin in bndstrt..bndend { + fastleak -= fdecay; + fastleak = fastleak.max(bndpsd[bin] - fgain); + slowleak -= sdecay; + slowleak = slowleak.max(bndpsd[bin] - sgain); + excite[bin] = fastleak.max(slowleak); + } + let hth_row = &HTH[fscod as usize]; + let mut mask = [0i32; 50]; + for bin in bndstrt..bndend { + let mut exc = excite[bin]; + if bndpsd[bin] < dbknee { + exc += (dbknee - bndpsd[bin]) >> 2; + } + mask[bin] = exc.max(hth_row[bin] as i32); + } + // §7.2.2.6 dba — coupling-channel variant. The decoder applies the + // cpl-channel deltba segments to the same `mask[]` array before + // bap[] is computed, even though the cpl excitation path is the + // simpler `fastleak.max(slowleak)` branch. + if let Some((plan, idx)) = dba { + apply_dba_segments(plan, idx, &mut mask); + } + let mut i = start; + let mut j = MASKTAB[start] as usize; + loop { + let lastbin = (BNDTAB[j] as usize + BNDSZ[j] as usize).min(end); + let mut m = mask[j]; + m -= snroffset; + m -= floor; + if m < 0 { + m = 0; + } + m &= 0x1fe0; + m += floor; + while i < lastbin { + let addr = ((psd[i] - m) >> 5).clamp(0, 63) as usize; + bap_out[i] = BAPTAB[addr]; + i += 1; + } + if i >= end { + break; + } + j += 1; + } +} + +fn logadd(a: i32, b: i32) -> i32 { + let c = a - b; + let addr = ((c.abs() >> 1) as usize).min(255); + if c >= 0 { + a + LATAB[addr] as i32 + } else { + b + LATAB[addr] as i32 + } +} + +fn calc_lowcomp(a: i32, b0: i32, b1: i32, bin: usize) -> i32 { + let mut a = a; + if bin < 7 { + if b0 + 256 == b1 { + a = 384; + } else if b0 > b1 { + a = (a - 64).max(0); + } + } else if bin < 20 { + if b0 + 256 == b1 { + a = 320; + } else if b0 > b1 { + a = (a - 64).max(0); + } + } else { + a = (a - 128).max(0); + } + a +} + +/// Count mantissa bits used by a bap histogram over all channels and +/// blocks. Grouped bap values (1, 2, 4) charge per-group cost; other +/// values charge nbits per mantissa. Returns the total in bits. +/// +/// `nchan` excludes the cpl pseudo-channel and the LFE pseudo-channel; +/// `end` is the per-fbw-channel upper bound (= cpl_begf_mant when +/// coupling is in use). The cpl pseudo-channel's bap (when active) is +/// appended into the same group stream right after the first coupled +/// channel — same order as the decoder's read schedule +/// (`unpack_mantissas`). When `lfeon`, the LFE channel's bap (lives at +/// `baps[nchan + 1]`) is walked last over bins 0..LFE_END_MANT. +pub(crate) fn mantissa_bits_total( + baps: &[Vec<[u8; N_COEFFS]>], + end: usize, + nchan: usize, + cpl: &CouplingPlan, + lfeon: bool, +) -> u32 { + mantissa_bits_total_ends(baps, end, None, nchan, cpl, lfeon) +} + +/// [`mantissa_bits_total`] with an optional per-channel coded +/// bandwidth (`end_ch[ch]` overrides `end` for fbw channel `ch`). +pub(crate) fn mantissa_bits_total_ends( + baps: &[Vec<[u8; N_COEFFS]>], + end: usize, + end_ch: Option<&[usize]>, + nchan: usize, + cpl: &CouplingPlan, + lfeon: bool, +) -> u32 { + let blocks = baps[0].len(); + let mut total = 0u32; + let cpl_idx = nchan; + let lfe_idx = nchan + 1; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + for blk in 0..blocks { + let mut g1_left = 0u32; + let mut g2_left = 0u32; + let mut g4_left = 0u32; + let mut got_cplchan = false; + for ch in 0..nchan { + let ch_end = end_ch.map_or(end, |e| e[ch]); + for bin in 0..ch_end { + let bap = baps[ch][blk][bin]; + match bap { + 0 => {} + 1 => { + if g1_left == 0 { + total += 5; + g1_left = 3; + } + g1_left -= 1; + } + 2 => { + if g2_left == 0 { + total += 7; + g2_left = 3; + } + g2_left -= 1; + } + 4 => { + if g4_left == 0 { + total += 7; + g4_left = 2; + } + g4_left -= 1; + } + b => total += QUANTIZATION_BITS[b as usize] as u32, + } + } + if cpl.in_use && cpl.chincpl[ch] && !got_cplchan { + got_cplchan = true; + for bin in begf_mant..endf_mant { + let bap = baps[cpl_idx][blk][bin]; + match bap { + 0 => {} + 1 => { + if g1_left == 0 { + total += 5; + g1_left = 3; + } + g1_left -= 1; + } + 2 => { + if g2_left == 0 { + total += 7; + g2_left = 3; + } + g2_left -= 1; + } + 4 => { + if g4_left == 0 { + total += 7; + g4_left = 2; + } + g4_left -= 1; + } + b => total += QUANTIZATION_BITS[b as usize] as u32, + } + } + } + } + if lfeon { + for bin in 0..LFE_END_MANT { + let bap = baps[lfe_idx][blk][bin]; + match bap { + 0 => {} + 1 => { + if g1_left == 0 { + total += 5; + g1_left = 3; + } + g1_left -= 1; + } + 2 => { + if g2_left == 0 { + total += 7; + g2_left = 3; + } + g2_left -= 1; + } + 4 => { + if g4_left == 0 { + total += 7; + g4_left = 2; + } + g4_left -= 1; + } + b => total += QUANTIZATION_BITS[b as usize] as u32, + } + } + } + } + total +} + +/// Compute the exact overhead bits (everything except mantissas) for a +/// given per-block exponent strategy. We walk the bitstream layout the +/// emitter uses and sum each field's width. +/// +/// `chexpstr_per_ch` carries the per-channel-per-block exponent +/// strategy (chexpstr semantics: 0=REUSE / 1=D15 / 2=D25 / 3=D45). +/// When `None`, falls back to the legacy frame-wide `exp_strategies` +/// (every fbw channel uses the same strategy). +#[allow(clippy::too_many_arguments)] +pub(crate) fn overhead_bits_for( + exp_strategies: &[u8; BLOCKS_PER_FRAME], + chexpstr_per_ch: Option<&[[u8; BLOCKS_PER_FRAME]]>, + end: usize, + nchan: usize, + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> u32 { + overhead_bits_for_ends( + exp_strategies, + chexpstr_per_ch, + end, + None, + nchan, + cpl, + dba, + acmod, + lfeon, + ) +} + +/// [`overhead_bits_for`] with an optional per-channel coded bandwidth +/// (`end_ch[ch]` overrides `end` when sizing channel `ch`'s exponent +/// payload). +#[allow(clippy::too_many_arguments)] +pub(crate) fn overhead_bits_for_ends( + exp_strategies: &[u8; BLOCKS_PER_FRAME], + chexpstr_per_ch: Option<&[[u8; BLOCKS_PER_FRAME]]>, + end: usize, + end_ch: Option<&[usize]>, + nchan: usize, + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> u32 { + // syncinfo: 16 (sync) + 16 (crc1) + 2 (fscod) + 6 (frmsizecod) = 40 + // BSI fixed: bsid(5) + bsmod(3) + acmod(3) + lfeon(1) + dialnorm(5) + // + 8 single-bit flags (compre/langcode/audprodie/copyrightb/ + // origbs/timecod1e/timecod2e/addbsie) = 25 + // BSI variable per acmod: + // + cmixlev(2) when acmod has centre + isn't 1/0 (acmod ∈ {3,5,7}) + // + surmixlev(2) when acmod has surround (acmod ∈ {4,5,6,7}) + // + dsurmod(2) when acmod == 2/0 (acmod == 2) + let mut bsi_bits = 25u32; + if (acmod & 0x1) != 0 && acmod != 0x1 { + bsi_bits += 2; + } + if (acmod & 0x4) != 0 { + bsi_bits += 2; + } + if acmod == 0x2 { + bsi_bits += 2; + } + let mut bits: u32 = 40 + bsi_bits; + // Per-strategy bits-per-channel helper: 4-bit absexp + 7-bit groups. + // grpsize ∈ {1, 2, 4} matches strategy code {1, 2, 3}. + let exp_bits_for_strategy = |strategy: u8, ch_end: usize| -> u32 { + let grpsize = match strategy { + 1 => 1, + 2 => 2, + 3 => 4, + _ => return 0, + }; + 4 + 7 * ngrps_for_strategy(ch_end, grpsize) as u32 + }; + let end_for = |ch: usize| -> usize { end_ch.map_or(end, |e| e[ch]) }; + // Default fbw bits when the per-channel plan isn't supplied (every + // channel uses the frame-wide strategy code from `exp_strategies`). + let d15_bits_per_ch = exp_bits_for_strategy(1, end); + // D15 ngrps for the cpl pseudo-channel (over [cpl_begf_mant, + // cpl_endf_mant)). Per §7.1.3 cpl uses + // ncplgrps = (cplendmant - cplstrtmant) / 3 for D15. + let cpl_d15_bits = if cpl.in_use { + let n = (cpl.endf_mant() - cpl.begf_mant()) / 3; + 4 + 7 * n as u32 + } else { + 0 + }; + // §5.4.3.19-20 rematstr + per-band rematflg. Only present in + // 2/0 stereo (acmod == 2) per the audblk syntax — multichannel + // modes carry no rematrix syntax at all. + let nrematbd_bits = if acmod == 2 { + // 1 bit rematstr + nrematbd flags. With cplinu the band count + // tracks Table 5.15. + 1 + remat_band_count(cpl.in_use, cpl.begf) as u32 + } else { + 0 + }; + let coupled_chs = if cpl.in_use { + cpl.chincpl[..nchan].iter().filter(|&&v| v).count() as u32 + } else { + 0 + }; + for (blk_i, &s) in exp_strategies.iter().enumerate() { + // blksw per ch (1 bit × nchan), dithflag × nchan, dynrnge(1) + bits += nchan as u32 * 2 + 1; + // cplstre + cplinu + (block 0 only) the cpl strategy fields. + if s == 1 { + // block 0 emits cplstre=1 + cplinu (+ cpl strategy fields + // when cpl.in_use). + bits += 1 + 1; + if cpl.in_use { + // chincpl[ch] × nchan + phsflginu(1, only acmod==2) + + // cplbegf(4) + cplendf(4) + (nsubbnd-1) cplbndstrc bits. + bits += nchan as u32 + 4 + 4; + if acmod == 2 { + bits += 1; + } + bits += cpl.nsubbnd.saturating_sub(1) as u32; + } + } else { + // non-block-0: cplstre = 0 (reuse), no further cpl strategy fields. + bits += 1; + } + // §5.4.3.14-18 cplcoe[ch] (1 bit per coupled ch) + the + // mstrcplco/cplcoexp/cplcomant payload when cplcoe=1, plus the + // optional phsflg burst. Coordinates are signalled on block 0 + // only (cplcoe[blk][ch]=(blk==0)). + if cpl.in_use { + // cplcoe per coupled ch = coupled_chs bits. + bits += coupled_chs; + // payload only when cplcoe=1 → only on block 0 in our + // policy. The strategy code is 1 on block 0 in our default + // [1,0,0,1,0,0]; map block-0 to s==1 by checking position. + } + // rematstr (1 bit) + nrematbd flags. + bits += nrematbd_bits; + // §5.4.3.21 cplexpstr — 2 bits per block when cplinu. + if cpl.in_use { + bits += 2; + } + // chexpstr × nchan (2 bits each) + bits += 2 * nchan as u32; + // §5.4.3.23 lfeexpstr — 1 bit per block when lfeon. + if lfeon { + bits += 1; + } + // chbwcod × nchan (6 bits) only when exp strategy != reuse and + // channel not coupled. + // Effective per-channel "is this channel new this block?": + // either we're using the per-channel plan (chexpstr_per_ch[ch][blk]) + // or every channel mirrors the frame-wide strategy `s`. + let ch_strategy = |ch: usize| -> u8 { + match chexpstr_per_ch { + Some(plan) => plan[ch][blk_i], + None => s, + } + }; + let n_new_ch = (0..nchan).filter(|&c| ch_strategy(c) != 0).count() as u32; + if n_new_ch > 0 { + let n_new_indep = if cpl.in_use { + let coupled_new = (0..nchan) + .filter(|&c| cpl.chincpl[c] && ch_strategy(c) != 0) + .count() as u32; + n_new_ch - coupled_new + } else { + n_new_ch + }; + bits += 6 * n_new_indep; + } + // exponents when new: cpl D15 (when cplexpstr=new) + + // per-channel exponents + 2 bits gainrng × (channels with new + // strategy) + LFE D15. + if s == 1 && cpl.in_use { + bits += cpl_d15_bits; + } + // Per-channel: pay (4 + 7 * ngrps + 2) bits when this channel's + // strategy this block is non-REUSE; otherwise pay nothing. + for ch in 0..nchan { + let strat = ch_strategy(ch); + if strat != 0 { + bits += exp_bits_for_strategy(strat, end_for(ch)) + 2 /* gainrng */; + } + } + if s == 1 && lfeon { + // §5.4.3.29 LFE D15 exponents over bins 0..7 = 4 bits absexp + // + 2 groups × 7 bits = 18 bits. No gainrng for LFE. + bits += 4 + 2 * 7; + } + // Suppress unused-warning when `chexpstr_per_ch` is None and + // every channel mirrors the frame-wide `s` — `d15_bits_per_ch` + // is still used below for legacy single-strategy callers. + let _ = d15_bits_per_ch; + // baie(1) + bit-alloc side info + if s == 1 { + // baie=1: sdcycod(2)+fdcycod(2)+sgaincod(2)+dbpbcod(2)+floorcod(3)=11 + bits += 1 + 11; + // snroffste=1: csnr(6) + (cpl: cplfsnr(4)+cplfgain(3)) + + // fsnr(4)+fgain(3) per ch + (lfe: lfefsnr(4)+lfefgain(3)). + bits += 1 + 6 + nchan as u32 * (4 + 3); + if cpl.in_use { + bits += 4 + 3; + } + if lfeon { + bits += 4 + 3; + } + } else { + bits += 1; // baie=0 + bits += 1; // snroffste=0 + } + // §5.4.3.44 cplleake (1 bit per block when cplinu). + if cpl.in_use { + bits += 1; + } + // §5.4.3.47-57 deltbaie + dba payload. We emit dba info on + // block 0 only: deltbaie=1, then per-channel deltbae[ch]==1 + // for channels with segments + per-segment payload, plus + // cpldeltbae=2 (no delta this block) when cpl is active and + // we have no cpl-channel dba in v1. Blocks 1..5 emit + // deltbaie=0 (reuse), and the decoder keeps applying the + // block-0 segment list for the remainder of the syncframe. + let any_dba = (0..nchan).any(|c| dba.nseg[c] > 0) || (cpl.in_use && dba.nseg[MAX_FBW] > 0); + if blk_i == 0 && (any_dba || cpl.in_use) { + bits += 1; // deltbaie=1 + if cpl.in_use { + bits += 2; // cpldeltbae (we send 2='no delta' when cpl seg list is empty) + } + bits += 2 * nchan as u32; // deltbae[ch] + if cpl.in_use && dba.nseg[MAX_FBW] > 0 { + bits += 3 + dba.nseg[MAX_FBW] as u32 * 12; + } + for c in 0..nchan { + if dba.nseg[c] > 0 { + bits += 3 + dba.nseg[c] as u32 * 12; + } + } + } else { + bits += 1; // deltbaie=0 (reuse on non-block-0; or no dba at all) + } + bits += 1; // skiple=0 + } + // §5.4.3.45-46 cplfleak/cplsleak — 3+3 bits, sent once on + // block 0 since the spec requires cplleake=1 there. Subsequent + // blocks emit cplleake=0 and reuse. + if cpl.in_use { + bits += 6; + } + // Block-0 cplcoe payload accounting (one-shot, outside the per- + // block strategy loop). When cplcoe[blk=0][ch]=1 the bitstream + // emits mstrcplco(2) + (cplcoexp(4)+cplcomant(4)) × nbnd per + // coupled channel. + if cpl.in_use { + bits += coupled_chs * (2 + 8 * cpl.nbnd as u32); + } + // auxdatae flag inherent in final pad byte; crc2 (16 bits). + bits += 16; + bits +} + +/// Adjust `csnroffst`/`fsnroffst` so the total mantissa bit count fits +/// the frame payload, minus the exact overhead cost. The sub-optimal +/// sweep is O(16*16) which is trivial given 6 blocks × 253 bins. +#[allow(clippy::too_many_arguments, dead_code)] +pub(crate) fn tune_snroffst( + ba: &BitAllocParams, + exps: &[Vec<[u8; N_COEFFS]>], + end: usize, + nchan: usize, + fscod: u8, + frame_bytes: usize, + exp_strategies: &[u8; BLOCKS_PER_FRAME], + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> BitAllocParams { + tune_snroffst_with_plan( + ba, + exps, + end, + nchan, + fscod, + frame_bytes, + exp_strategies, + None, + cpl, + dba, + acmod, + lfeon, + ) +} + +/// Same as [`tune_snroffst`] but accepts a per-channel-per-block +/// chexpstr plan (`chexpstr_per_ch[ch][blk]` = 0/1/2/3). When the plan +/// uses D25 or D45 strategies, the exponent-payload bits are smaller, +/// freeing more of the frame budget for mantissas. +#[allow(clippy::too_many_arguments)] +pub(crate) fn tune_snroffst_with_plan( + ba: &BitAllocParams, + exps: &[Vec<[u8; N_COEFFS]>], + end: usize, + nchan: usize, + fscod: u8, + frame_bytes: usize, + exp_strategies: &[u8; BLOCKS_PER_FRAME], + chexpstr_per_ch: Option<&[[u8; BLOCKS_PER_FRAME]]>, + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> BitAllocParams { + tune_snroffst_with_plan_ends( + ba, + exps, + end, + None, + nchan, + fscod, + frame_bytes, + exp_strategies, + chexpstr_per_ch, + cpl, + dba, + acmod, + lfeon, + ) +} + +/// [`tune_snroffst_with_plan`] with an optional per-channel coded +/// bandwidth: `end_ch[ch]` overrides `end` for fbw channel `ch` in +/// the bap computation, mantissa accounting, and exponent-overhead +/// sizing. Needed for mixed per-channel `chinspx` frames, where SPX +/// channels stop at the SPX begin frequency while full-bandwidth +/// channels run to the chbwcod-derived end. +#[allow(clippy::too_many_arguments)] +pub(crate) fn tune_snroffst_with_plan_ends( + ba: &BitAllocParams, + exps: &[Vec<[u8; N_COEFFS]>], + end: usize, + end_ch: Option<&[usize]>, + nchan: usize, + fscod: u8, + frame_bytes: usize, + exp_strategies: &[u8; BLOCKS_PER_FRAME], + chexpstr_per_ch: Option<&[[u8; BLOCKS_PER_FRAME]]>, + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> BitAllocParams { + let overhead = overhead_bits_for_ends( + exp_strategies, + chexpstr_per_ch, + end, + end_ch, + nchan, + cpl, + dba, + acmod, + lfeon, + ) + 32 /* safety */; + let total_bits = (frame_bytes * 8) as u32; + if overhead >= total_bits { + return *ba; + } + let budget = total_bits - overhead; + + // Maximise SNR offset subject to mantissa bit count fitting `budget`. + // Search over all (csnroffst, fsnroffst) pairs — with a small early + // termination once the combined offset starts producing non-monotone + // growth. The 256-pair sweep is fast in release and lets us find a + // finer optimum than the old greedy walk. + // + // When coupling is in use we also tune `cplfsnroffst` together + // with the per-channel `fsnroffst`. To keep the search small we + // tie cplfsnr ≡ fsnr (the cpl pseudo-channel and the fbw channels + // share the same sub-band SNR offset). This is sub-optimal but + // adequate for an initial coupling-encode implementation. + let mut best = *ba; + let mut best_offset: i32 = -1; + for csnr in 0..=63u8 { + for fsnr in 0..=15u8 { + let mut cand = *ba; + cand.csnroffst = csnr; + cand.fsnroffst = fsnr; + cand.cplfsnroffst = fsnr; + cand.lfefsnroffst = fsnr; + // baps slot layout: 0..nchan = fbw, nchan = cpl, nchan+1 = lfe. + let mut baps: Vec> = + vec![vec![[0u8; N_COEFFS]; exps[0].len()]; nchan + 2]; + for ch in 0..nchan { + let ch_end = end_ch.map_or(end, |e| e[ch]); + for blk in 0..exps[ch].len() { + compute_bap( + &exps[ch][blk], + ch_end, + fscod, + &cand, + &mut baps[ch][blk], + Some((dba, ch)), + ); + } + } + if cpl.in_use { + let cpl_idx = nchan; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + let mut cpl_ba = cand; + cpl_ba.fsnroffst = cand.cplfsnroffst; + cpl_ba.fgaincod = cand.cplfgaincod; + for blk in 0..exps[cpl_idx].len() { + compute_bap_cpl( + &exps[cpl_idx][blk], + begf_mant, + endf_mant, + fscod, + &cpl_ba, + &mut baps[cpl_idx][blk], + Some((dba, MAX_FBW)), + ); + } + } + if lfeon { + let lfe_idx = nchan + 1; + let mut lfe_ba = cand; + lfe_ba.fsnroffst = cand.lfefsnroffst; + lfe_ba.fgaincod = cand.lfefgaincod; + for blk in 0..exps[lfe_idx].len() { + compute_bap( + &exps[lfe_idx][blk], + LFE_END_MANT, + fscod, + &lfe_ba, + &mut baps[lfe_idx][blk], + None, + ); + } + } + let used = mantissa_bits_total_ends(&baps, end, end_ch, nchan, cpl, lfeon); + if used <= budget { + let combined = (csnr as i32) * 16 + fsnr as i32; + if combined > best_offset { + best_offset = combined; + best = cand; + } + } + } + } + // Seed per-channel array with the chosen global fsnr so the + // bitstream emitter has a populated `fsnroffst_ch` even when the + // per-channel refinement loop below is a no-op. + for ch in 0..MAX_FBW { + best.fsnroffst_ch[ch] = best.fsnroffst; + } + if nchan == 0 { + return best; + } + + // ----------------------------------------------------------------- + // Per-channel fsnroffst refinement (round-23/103, §5.4.3.40; + // r95 fairness fix). + // + // After the global (csnr, fsnr) is chosen above, the budget is + // typically not exhausted — many channels only need fsnr=k while a + // few would benefit from fsnr=k+δ. The bitstream syntax allows a + // distinct fsnroffst per fbw channel (4 bits each); spending the + // residual budget per-channel turns leftover bits into per-channel + // PSNR. + // + // **Bump ordering** — round-23 visited channels in `0..nchan` + // index order. When a low-index channel's signal had little HF + // energy (e.g. a 220 Hz tone on ch=0 in a 5-channel sine-sweep), + // each of its bumps cost very few mantissa bits, so under + // round-robin it consumed almost all of the residual budget + // (capping at fsnroffst=15) before higher-index channels could + // bump even once. R91's per-channel PSNR gates exposed this: + // the 3/2 self-decode trace recorded L=32.7 dB while C=10.5 dB + // and R=11.0 dB barely cleared the 10 dB floor. + // + // R95 swaps the inner loop to **least-served first**: per round, + // we walk the channels in ascending current `fsnroffst_ch[ch]` + // (ch index as tiebreaker), so the channel furthest behind gets + // first crack at the slack each pass. The non-normative policy + // matches ATSC A/52:2018 Annex C's guidance that the encoder + // SHOULD balance the per-channel SNR — §5.4.3.40 itself only + // defines the bitstream field; the choice of `fsnroffst[ch]` + // value is left to the encoder. + // + // We don't tune cplfsnroffst / lfefsnroffst per-channel here — + // they're singletons in the bitstream syntax and the cpl/lfe + // pseudo-channels don't need it for the current test inputs. + let recompute_used = |ba_in: &BitAllocParams| -> u32 { + let mut bps: Vec> = + vec![vec![[0u8; N_COEFFS]; exps[0].len()]; nchan + 2]; + for ch in 0..nchan { + let mut ch_ba = *ba_in; + ch_ba.fsnroffst = ba_in.fsnroffst_ch[ch]; + let ch_end = end_ch.map_or(end, |e| e[ch]); + for blk in 0..exps[ch].len() { + compute_bap( + &exps[ch][blk], + ch_end, + fscod, + &ch_ba, + &mut bps[ch][blk], + Some((dba, ch)), + ); + } + } + if cpl.in_use { + let cpl_idx = nchan; + let begf_mant = cpl.begf_mant(); + let endf_mant = cpl.endf_mant(); + let mut cpl_ba = *ba_in; + cpl_ba.fsnroffst = ba_in.cplfsnroffst; + cpl_ba.fgaincod = ba_in.cplfgaincod; + for blk in 0..exps[cpl_idx].len() { + compute_bap_cpl( + &exps[cpl_idx][blk], + begf_mant, + endf_mant, + fscod, + &cpl_ba, + &mut bps[cpl_idx][blk], + Some((dba, MAX_FBW)), + ); + } + } + if lfeon { + let lfe_idx = nchan + 1; + let mut lfe_ba = *ba_in; + lfe_ba.fsnroffst = ba_in.lfefsnroffst; + lfe_ba.fgaincod = ba_in.lfefgaincod; + for blk in 0..exps[lfe_idx].len() { + compute_bap( + &exps[lfe_idx][blk], + LFE_END_MANT, + fscod, + &lfe_ba, + &mut bps[lfe_idx][blk], + None, + ); + } + } + mantissa_bits_total_ends(&bps, end, end_ch, nchan, cpl, lfeon) + }; + + // Per-channel greedy bumps, **least-served first with fairness + // cap** (r95). + // + // Two-stage greedy: + // + // 1. **Equalisation pass** — repeatedly bump every channel + // currently sitting at the round-wide minimum + // `fsnroffst_ch`. Walked in ascending-fsnr / ascending-ch + // order. This guarantees every fbw channel gets at least + // `min(fsnroffst_ch[..nchan])` bumps before any one runs + // ahead. + // + // 2. **Free-bump pass** — once the equalisation pass stalls + // (= no minimum-channel bump fits, e.g. because a + // high-frequency channel can't afford another bump on the + // remaining budget), fall back to a per-channel greedy + // walk that lets cheap-bump channels consume the residual + // slack. This recovers the old behaviour for slack that + // *no* channel can spread fairly. + // + // The equalisation pass closes the long-standing r91 gap where a + // low-frequency-tone channel (220 Hz on slot 0 in the per-channel + // PSNR tests) consumed ~15 bumps before higher-frequency + // siblings managed one each, leaving the 660 Hz centre slot + // pinned at fsnroffst_ch = 0 while slot 0 sat at 15. + // + // ATSC A/52:2018 §5.4.3.40 only defines the bitstream field; + // the encoder's choice of value is non-normative. The Annex C + // reference encoder suggests balancing the per-channel SNR; the + // fairness cap is one realisation of that guidance. + let mut order: [usize; MAX_FBW] = [0; MAX_FBW]; + + // Stage 1: equalisation. Bump the minimum-fsnr channels until + // none of them fit further bumps. + for _round in 0..(MAX_FBW * 16) { + let mut min_fsnr: u8 = u8::MAX; + for ch in 0..nchan { + if best.fsnroffst_ch[ch] < 15 && best.fsnroffst_ch[ch] < min_fsnr { + min_fsnr = best.fsnroffst_ch[ch]; + } + } + if min_fsnr == u8::MAX { + break; + } + let mut n_eligible = 0usize; + for ch in 0..nchan { + if best.fsnroffst_ch[ch] == min_fsnr { + order[n_eligible] = ch; + n_eligible += 1; + } + } + let mut bumped = false; + for &ch in &order[..n_eligible] { + let mut trial = best; + trial.fsnroffst_ch[ch] += 1; + let trial_used = recompute_used(&trial); + if trial_used <= budget { + best = trial; + bumped = true; + } + } + if !bumped { + break; + } + } + + // Stage 2: free residual-bump pass with **spread cap**. Sort + // eligible channels by ascending current `fsnroffst_ch`. A + // channel is only eligible if its fsnr_ch stays within + // `FAIR_SPREAD` of the round-wide minimum after the bump — + // this prevents a single cheap-mantissa channel (low-frequency + // tone) from running away to fsnr_ch=15 while siblings sit at 0. + // + // FAIR_SPREAD=2 gives every fbw channel within 2 fine SNR + // steps of any other. fsnr is 4 bits / channel, so 2 steps + // corresponds to roughly 1.5 dB of per-channel SNR variance + // — well below the 10 dB floor while leaving slack for cheap + // channels to absorb otherwise-wasted budget. + const FAIR_SPREAD: u8 = 2; + for _round in 0..(MAX_FBW * 16) { + let cur_min = (0..nchan) + .map(|ch| best.fsnroffst_ch[ch]) + .min() + .unwrap_or(0); + let mut n_eligible = 0usize; + for ch in 0..nchan { + if best.fsnroffst_ch[ch] < 15 + && best.fsnroffst_ch[ch].saturating_sub(cur_min) < FAIR_SPREAD + { + order[n_eligible] = ch; + n_eligible += 1; + } + } + if n_eligible == 0 { + break; + } + order[..n_eligible].sort_by_key(|&ch| best.fsnroffst_ch[ch]); + + let mut bumped = false; + for &ch in &order[..n_eligible] { + let mut trial = best; + trial.fsnroffst_ch[ch] += 1; + let trial_used = recompute_used(&trial); + if trial_used <= budget { + best = trial; + bumped = true; + } + } + if !bumped { + break; + } + } + if std::env::var("AC3_DEBUG_PERCH_SNR").is_ok() { + eprintln!( + "tune_snroffst: csnr={} fsnr={} fsnr_ch={:?} cpl_fsnr={} lfe_fsnr={}", + best.csnroffst, best.fsnroffst, best.fsnroffst_ch, best.cplfsnroffst, best.lfefsnroffst, + ); + } + best +} + +// --------------------------------------------------------------------------- +// Per-block snroffst tuning (round-24 / task #170) +// +// ATSC A/52 §5.4.3.37-43 lets each audio block re-transmit a fresh +// (csnroffst, fsnroffst[ch], cplfsnroffst, lfefsnroffst) tuple via the +// `snroffste=1` flag. The decoder applies these immediately, so they +// take effect for the rest of that block onward (and remain in effect +// until the next snroffste=1). +// +// Without per-block tuning every block within a 32 ms syncframe shares +// one global SNR offset budget. When the frame contains a transient +// (high masking demand on one block, near-silence elsewhere), a flat +// allocation under-spends on the transient block while wasting bits on +// the silent neighbours. The redistribution pass below moves bits from +// quiet blocks onto demand-heavy blocks within the unchanged frame +// budget. +// --------------------------------------------------------------------------- + +/// Per-block override of the SNR-offset fields. Layout mirrors the +/// per-block bitstream syntax: each block carries its own csnroffst + +/// per-channel fsnroffst + cpl/lfe fsnroffst values. The encoder +/// emits `snroffste=1` for any block whose values differ from the +/// previous emitted set, otherwise `snroffste=0` (reuse). +#[derive(Clone, Copy)] +pub(crate) struct PerBlockSnr { + pub(crate) csnroffst: [u8; BLOCKS_PER_FRAME], + pub(crate) fsnroffst_ch: [[u8; MAX_FBW]; BLOCKS_PER_FRAME], + pub(crate) cplfsnroffst: [u8; BLOCKS_PER_FRAME], + pub(crate) lfefsnroffst: [u8; BLOCKS_PER_FRAME], +} + +impl PerBlockSnr { + /// Construct a per-block plan that reuses the global tuned values + /// for every block. Equivalent to the pre-#170 behaviour. + pub(crate) fn from_global(ba: &BitAllocParams) -> Self { + Self { + csnroffst: [ba.csnroffst; BLOCKS_PER_FRAME], + fsnroffst_ch: [ba.fsnroffst_ch; BLOCKS_PER_FRAME], + cplfsnroffst: [ba.cplfsnroffst; BLOCKS_PER_FRAME], + lfefsnroffst: [ba.lfefsnroffst; BLOCKS_PER_FRAME], + } + } + + /// Returns true when block `blk`'s snroffst tuple differs from the + /// previous block's. Block 0 always returns true (must transmit). + pub(crate) fn snroffste(&self, blk: usize) -> bool { + if blk == 0 { + return true; + } + let prev = blk - 1; + self.csnroffst[blk] != self.csnroffst[prev] + || self.fsnroffst_ch[blk] != self.fsnroffst_ch[prev] + || self.cplfsnroffst[blk] != self.cplfsnroffst[prev] + || self.lfefsnroffst[blk] != self.lfefsnroffst[prev] + } + + /// Build a `BitAllocParams` snapshot for the fbw channel `ch` at + /// block `blk`, substituting the per-block (csnr, fsnr) values into + /// `ba`. Used by `compute_bap` callers to render block-specific + /// mantissa allocations. + pub(crate) fn ba_for_fbw( + &self, + base: &BitAllocParams, + blk: usize, + ch: usize, + ) -> BitAllocParams { + let mut out = *base; + out.csnroffst = self.csnroffst[blk]; + out.fsnroffst = self.fsnroffst_ch[blk][ch]; + out + } + + pub(crate) fn ba_for_cpl(&self, base: &BitAllocParams, blk: usize) -> BitAllocParams { + let mut out = *base; + out.csnroffst = self.csnroffst[blk]; + out.fsnroffst = self.cplfsnroffst[blk]; + out.fgaincod = base.cplfgaincod; + out + } + + pub(crate) fn ba_for_lfe(&self, base: &BitAllocParams, blk: usize) -> BitAllocParams { + let mut out = *base; + out.csnroffst = self.csnroffst[blk]; + out.fsnroffst = self.lfefsnroffst[blk]; + out.fgaincod = base.lfefgaincod; + out + } +} + +/// Per-block masking demand, measured as the average (PSD - mask floor) +/// gap in dB-equivalent units across the channel set. Larger gap means +/// more coefficients sit above the mask and would benefit from extra +/// mantissa bits — i.e. higher SNR offset is more PSNR per bit there. +/// +/// We use the **mean** PSD over [0, end) as a proxy for masking demand: +/// silent blocks have PSD ≈ -3072 (24 << 7) for every bin, while +/// transient blocks have a wide dynamic range and high mean PSD. The +/// proxy correlates well with actual mantissa hunger because +/// `compute_bap` derives bap from `mask - PSD`, so high-PSD bins +/// dominate the bap budget. +fn per_block_demand( + exps: &[Vec<[u8; N_COEFFS]>], + end: usize, + nchan: usize, +) -> [i64; BLOCKS_PER_FRAME] { + let mut demand = [0i64; BLOCKS_PER_FRAME]; + if nchan == 0 || end == 0 { + return demand; + } + for blk in 0..BLOCKS_PER_FRAME { + let mut sum: i64 = 0; + let mut cnt: i64 = 0; + for ch in 0..nchan { + for bin in 0..end { + // PSD = 3072 - (exp << 7) per §7.2.2.1. Lower exponent + // (larger coefficient magnitude) → higher PSD. + let psd = 3072i64 - ((exps[ch][blk][bin] as i64) << 7); + sum += psd; + cnt += 1; + } + } + demand[blk] = if cnt > 0 { sum / cnt } else { 0 }; + } + demand +} + +/// Adjust the per-block snroffst plan so high-demand blocks get a +/// fsnroffst bump and low-demand blocks get a fsnroffst drop, keeping +/// the total mantissa bits + per-block snroffste overhead within the +/// frame budget. +/// +/// Algorithm: +/// 1. Sort blocks by demand (high → low). +/// 2. Group blocks into "left half" (blocks 0/1/2) and "right half" +/// (blocks 3/4/5). The split aligns with the encoder's D15-on- +/// blocks-0-and-3 exponent strategy: blocks 0/1/2 share an +/// exponent set (D15 on 0, REUSE on 1/2) and blocks 3/4/5 share +/// another (D15 on 3, REUSE on 4/5). Treating each half as one +/// bit-allocation unit means a single snroffste=1 emission on +/// block 3 carries the per-half tuning at a fixed 27-30 bit +/// overhead (vs the cascading overhead of arbitrary per-block +/// changes). +/// 3. Drop the donor half's all-channel fsnroffst by k steps (k = +/// 1, 2, 3) and bump the recipient half's by k steps, accepting +/// the largest k that fits the budget after accounting for the +/// block-3 snroffste payload. Try `(donor=left, recipient=right)` +/// and the swap; pick whichever yields a profitable transfer +/// based on the demand sign. +/// 4. Then run the original 1-step pair-walk for fine refinement. +/// +/// Returns the populated `PerBlockSnr`. When no profitable transfer +/// exists (uniform demand, tight budget), this returns the global plan +/// untouched so the encoder remains fully spec-compliant — `snroffste` +/// stays at the original "block-0 only" pattern. +#[allow(clippy::too_many_arguments, dead_code)] +pub(crate) fn tune_per_block_snroffst( + ba: &BitAllocParams, + exps: &[Vec<[u8; N_COEFFS]>], + end: usize, + nchan: usize, + fscod: u8, + frame_bytes: usize, + exp_strategies: &[u8; BLOCKS_PER_FRAME], + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> PerBlockSnr { + tune_per_block_snroffst_with_plan( + ba, + exps, + end, + nchan, + fscod, + frame_bytes, + exp_strategies, + None, + cpl, + dba, + acmod, + lfeon, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn tune_per_block_snroffst_with_plan( + ba: &BitAllocParams, + exps: &[Vec<[u8; N_COEFFS]>], + end: usize, + nchan: usize, + fscod: u8, + frame_bytes: usize, + exp_strategies: &[u8; BLOCKS_PER_FRAME], + chexpstr_per_ch: Option<&[[u8; BLOCKS_PER_FRAME]]>, + cpl: &CouplingPlan, + dba: &DbaPlan, + acmod: u8, + lfeon: bool, +) -> PerBlockSnr { + let mut plan = PerBlockSnr::from_global(ba); + if nchan == 0 { + return plan; + } + + // Order blocks by descending demand. We only redistribute when + // there is a meaningful spread — uniform demand means the global + // tuner already converged to the right answer. + let demand = per_block_demand(exps, end, nchan); + let mut order: [usize; BLOCKS_PER_FRAME] = [0, 1, 2, 3, 4, 5]; + order.sort_by(|&a, &b| demand[b].cmp(&demand[a])); + let spread = demand[order[0]] - demand[order[BLOCKS_PER_FRAME - 1]]; + if std::env::var("AC3_DEBUG_PERBLOCK_SNR").is_ok() { + eprintln!( + "perblock_snr: demand={:?} order={:?} spread={} ba.csnr={} ba.fsnr_ch={:?}", + demand, order, spread, ba.csnroffst, ba.fsnroffst_ch + ); + } + // Threshold ≈ 256 PSD-units (≈ 2 exponents worth) — below this the + // blocks look perceptually uniform and per-block emission would + // just waste the snroffste overhead bits. + if spread < 256 { + return plan; + } + + // Helper: compute current total mantissa bits across all blocks + // under the current `plan`, plus per-block snroffste overhead + // delta vs the pre-#170 baseline (block-0-only emission). + let recompute = |plan: &PerBlockSnr| -> u32 { + let mut bps: Vec> = + vec![vec![[0u8; N_COEFFS]; exps[0].len()]; nchan + 2]; + for blk in 0..BLOCKS_PER_FRAME { + for ch in 0..nchan { + let ch_ba = plan.ba_for_fbw(ba, blk, ch); + compute_bap( + &exps[ch][blk], + end, + fscod, + &ch_ba, + &mut bps[ch][blk], + Some((dba, ch)), + ); + } + if cpl.in_use { + let cpl_idx = nchan; + let cpl_ba = plan.ba_for_cpl(ba, blk); + compute_bap_cpl( + &exps[cpl_idx][blk], + cpl.begf_mant(), + cpl.endf_mant(), + fscod, + &cpl_ba, + &mut bps[cpl_idx][blk], + Some((dba, MAX_FBW)), + ); + } + if lfeon { + let lfe_idx = nchan + 1; + let lfe_ba = plan.ba_for_lfe(ba, blk); + compute_bap( + &exps[lfe_idx][blk], + LFE_END_MANT, + fscod, + &lfe_ba, + &mut bps[lfe_idx][blk], + None, + ); + } + } + mantissa_bits_total(&bps, end, nchan, cpl, lfeon) + }; + + // Per-block snroffste overhead bits when set to 1 (block 0 was + // already counted by the existing overhead model). For each *extra* + // block where snroffste flips to 1 we add the snroffst payload: + // csnr(6) + nchan*(fsnr(4)+fgain(3)) [+ cpl 4+3] [+ lfe 4+3] + // The leading 1-bit `snroffste` flag itself is already part of the + // baseline overhead. + let snroffste_payload: u32 = 6 + + nchan as u32 * (4 + 3) + + if cpl.in_use { 4 + 3 } else { 0 } + + if lfeon { 4 + 3 } else { 0 }; + + let baseline_overhead = + overhead_bits_for(exp_strategies, chexpstr_per_ch, end, nchan, cpl, dba, acmod, lfeon) + + 32 /* safety */; + let total_bits = (frame_bytes * 8) as u32; + if baseline_overhead >= total_bits { + return plan; + } + let baseline_budget = total_bits - baseline_overhead; + let baseline_used = recompute(&plan); + if baseline_used > baseline_budget { + // Global tuner should have prevented this; bail rather than + // make things worse. + return plan; + } + + // Cost of the per-block snroffste payloads currently used by the + // plan (counting blocks 1..5 that flip on). Block 0 is always a + // payload block in the baseline overhead. + let extra_snr_overhead = |plan: &PerBlockSnr| -> u32 { + let mut n_extra = 0u32; + for blk in 1..BLOCKS_PER_FRAME { + if plan.snroffste(blk) { + n_extra += 1; + } + } + n_extra * snroffste_payload + }; + + // Compute per-half mean demand. The encoder runs D15 on blocks 0 + // and 3 with REUSE elsewhere, so blocks 0/1/2 share an exponent + // set and blocks 3/4/5 share another. Treating each half as the + // bit-allocation unit costs only 1 snroffste=1 payload (on block + // 3), keeping the per-block overhead bounded. + let left_demand: i64 = (0..3).map(|b| demand[b]).sum::() / 3; + let right_demand: i64 = (3..6).map(|b| demand[b]).sum::() / 3; + let half_spread = (left_demand - right_demand).abs(); + if std::env::var("AC3_DEBUG_PERBLOCK_SNR").is_ok() { + eprintln!( + "perblock_snr: left_demand={} right_demand={} half_spread={} baseline_used={}/{}", + left_demand, right_demand, half_spread, baseline_used, baseline_budget + ); + } + if half_spread >= 128 { + // Direction: positive half_spread means left is higher demand, + // so right is the donor and left is the recipient (and vice + // versa). + let (donor_blocks, recip_blocks): (&[usize], &[usize]) = if left_demand > right_demand { + (&[3, 4, 5], &[0, 1, 2]) + } else { + (&[0, 1, 2], &[3, 4, 5]) + }; + + // The global tuner consumes most of the frame budget, so a + // naive equal-and-opposite transfer rarely fits the snroffste + // overhead (~27 bits for stereo). The redistribution is two + // independent moves: + // + // * **bank step `down`** — drop donor blocks' fsnr by `down` + // unconditionally. Donates donor PSNR to free mantissa + // bits. + // * **bump step `up`** — raise recipient blocks' fsnr by + // `up` unconditionally. Spends those bits where the + // masking demand is highest. + // + // We iterate (down, up) and accept the trial maximising + // `up - down` (a heuristic for "PSNR gained on the demand + // side, less PSNR lost on the donor side") subject to + // `trial_used + extra_snr_overhead ≤ baseline_budget`. The + // search is O(16²) which is trivial. + let mut best_score = i32::MIN; + let mut best_trial = plan; + for down in 0..=8i32 { + // Donor-side eligibility. + let donor_ok = donor_blocks + .iter() + .all(|&db| (0..nchan).all(|c| plan.fsnroffst_ch[db][c] as i32 >= down)); + if !donor_ok { + continue; + } + for up in 0..=8i32 { + if down == 0 && up == 0 { + continue; + } + let recip_ok = recip_blocks + .iter() + .all(|&rb| (0..nchan).all(|c| (plan.fsnroffst_ch[rb][c] as i32) + up <= 15)); + if !recip_ok { + continue; + } + let mut trial = plan; + for &db in donor_blocks { + for c in 0..nchan { + trial.fsnroffst_ch[db][c] = + (trial.fsnroffst_ch[db][c] as i32 - down).max(0) as u8; + } + if cpl.in_use { + trial.cplfsnroffst[db] = + (trial.cplfsnroffst[db] as i32 - down).max(0) as u8; + } + if lfeon { + trial.lfefsnroffst[db] = + (trial.lfefsnroffst[db] as i32 - down).max(0) as u8; + } + } + for &rb in recip_blocks { + for c in 0..nchan { + trial.fsnroffst_ch[rb][c] = + ((trial.fsnroffst_ch[rb][c] as i32) + up).min(15) as u8; + } + if cpl.in_use { + trial.cplfsnroffst[rb] = + ((trial.cplfsnroffst[rb] as i32) + up).min(15) as u8; + } + if lfeon { + trial.lfefsnroffst[rb] = + ((trial.lfefsnroffst[rb] as i32) + up).min(15) as u8; + } + } + let trial_used = recompute(&trial); + let trial_overhead = extra_snr_overhead(&trial); + let fits = trial_used + trial_overhead <= baseline_budget; + if std::env::var("AC3_DEBUG_PERBLOCK_SNR").is_ok() && fits { + eprintln!( + " half-transfer down={} up={} used={} overhead={} budget={} accepted", + down, up, trial_used, trial_overhead, baseline_budget + ); + } + if fits { + let score = up - down; + if score > best_score { + best_score = score; + best_trial = trial; + } + } + } + } + if best_score > i32::MIN { + plan = best_trial; + } + } + + // Optional fine refinement: greedy pair walk over individual blocks + // for cases where the half-frame transfer was rejected but a tiny + // single-channel transfer still fits. Kept narrow because each + // accepted swap can flip 2-4 snroffste bits. + let max_rounds = 4; + for _round in 0..max_rounds { + let mut improved = false; + for i in 0..(BLOCKS_PER_FRAME / 2) { + let recipient = order[i]; + let donor = order[BLOCKS_PER_FRAME - 1 - i]; + if recipient == donor || demand[recipient] - demand[donor] < 256 { + continue; + } + let donor_ch = (0..nchan) + .filter(|&c| plan.fsnroffst_ch[donor][c] > 0) + .max_by_key(|&c| plan.fsnroffst_ch[donor][c]); + let recip_ch = (0..nchan) + .filter(|&c| plan.fsnroffst_ch[recipient][c] < 15) + .min_by_key(|&c| plan.fsnroffst_ch[recipient][c]); + if let (Some(dc), Some(rc)) = (donor_ch, recip_ch) { + let mut trial = plan; + trial.fsnroffst_ch[donor][dc] -= 1; + trial.fsnroffst_ch[recipient][rc] += 1; + let trial_used = recompute(&trial); + let trial_overhead = extra_snr_overhead(&trial); + if trial_used + trial_overhead <= baseline_budget { + plan = trial; + improved = true; + } + } + } + if !improved { + break; + } + } + + // Final guard: the per-block plan must still fit. If somehow the + // greedy walk overshot (shouldn't happen, but be defensive), fall + // back to the flat plan. + let final_used = recompute(&plan); + let final_overhead = extra_snr_overhead(&plan); + if final_used + final_overhead > baseline_budget { + return PerBlockSnr::from_global(ba); + } + if std::env::var("AC3_DEBUG_PERBLOCK_SNR").is_ok() { + eprintln!( + "perblock_snr: final csnr={:?} fsnr_ch={:?} extra_overhead={}", + plan.csnroffst, plan.fsnroffst_ch, final_overhead + ); + } + plan +} + +// --------------------------------------------------------------------------- +// Mantissa quantisation + packing +// --------------------------------------------------------------------------- + +#[derive(Default)] +#[allow(dead_code)] +struct MantGroupCtx { + /// Pending 3-level mantissa codes (0..3) to pack when the group fills. + g3_codes: [u32; 3], + g3_n: usize, + /// Pending 5-level mantissa codes (0..5). + g5_codes: [u32; 3], + g5_n: usize, + /// Pending 11-level mantissa codes (0..11). + g11_codes: [u32; 2], + g11_n: usize, +} + +/// Quantise a floating-point transform coefficient into its AC-3 +/// mantissa code for bap ∈ 1..=15. Returns `u32` (upper-bit-unused for +/// narrow bap). +pub(crate) fn quantise_mantissa(coeff: f32, exp: i32, bap: u8) -> u32 { + // Normalised mantissa in (-1, 1): coeff * 2^exp. + let m = (coeff * 2f32.powi(exp)).clamp(-1.0, 1.0); + match bap { + 1 => { + // 3-level: nearest of {-2/3, 0, 2/3} → codes 0..2. + let nearest = nearest_symmetric(m, &MANT_LEVEL_3); + nearest as u32 + } + 2 => nearest_symmetric(m, &MANT_LEVEL_5) as u32, + 3 => nearest_symmetric(m, &MANT_LEVEL_7) as u32, + 4 => nearest_symmetric(m, &MANT_LEVEL_11) as u32, + 5 => nearest_symmetric(m, &MANT_LEVEL_15) as u32, + b => { + // 6..=15: asymmetric 2's-complement fractional, `nbits` bits. + let nbits = QUANTIZATION_BITS[b as usize] as u32; + let shift = nbits - 1; + let v = (m * (1u32 << shift) as f32).round() as i32; + let max = (1i32 << shift) - 1; + let min = -(1i32 << shift); + let clamped = v.clamp(min, max); + let mask = if nbits == 32 { + u32::MAX + } else { + (1u32 << nbits) - 1 + }; + (clamped as u32) & mask + } + } +} + +/// The decoder-side reconstruction of one mantissa after [`quantise_mantissa`]: +/// the de-normalised coefficient value (`level * 2^-exp`) the decoder's +/// §7.3.3 dequantisation produces for the code the encoder would emit. +/// Used by the E-AC-3 enhanced-coupling encoder to measure coordinates +/// against the carrier the decoder will actually reconstruct (bap = 0 +/// bins are true zeros — the coupling channel is never dithered). +pub(crate) fn quantise_reconstruct(coeff: f32, exp: i32, bap: u8) -> f32 { + let m = (coeff * 2f32.powi(exp)).clamp(-1.0, 1.0); + let level = match bap { + 0 => 0.0, + 1 => MANT_LEVEL_3[nearest_symmetric(m, &MANT_LEVEL_3)], + 2 => MANT_LEVEL_5[nearest_symmetric(m, &MANT_LEVEL_5)], + 3 => MANT_LEVEL_7[nearest_symmetric(m, &MANT_LEVEL_7)], + 4 => MANT_LEVEL_11[nearest_symmetric(m, &MANT_LEVEL_11)], + 5 => MANT_LEVEL_15[nearest_symmetric(m, &MANT_LEVEL_15)], + b => { + let nbits = QUANTIZATION_BITS[b as usize] as u32; + let shift = nbits - 1; + let v = (m * (1u32 << shift) as f32).round() as i32; + let max = (1i32 << shift) - 1; + let min = -(1i32 << shift); + v.clamp(min, max) as f32 / (1u32 << shift) as f32 + } + }; + level * 2f32.powi(-exp) +} + +fn nearest_symmetric(m: f32, table: &[f32]) -> usize { + let mut best_i = 0usize; + let mut best_d = f32::INFINITY; + for (i, &v) in table.iter().enumerate() { + let d = (m - v).abs(); + if d < best_d { + best_d = d; + best_i = i; + } + } + best_i +} + +/// Emit a block's mantissa codes in the decoder's expected read order. +/// +/// The AC-3 decoder pre-fetches grouped mantissas (bap=1/2/4) — it +/// reads the 5-bit triple (bap=1), 7-bit triple (bap=2), or 7-bit pair +/// (bap=4) *as soon as its internal buffer empties and the next code +/// of that bap is requested*. A naive encoder that only emits when a +/// group fills would lag the decoder by one group at the very first +/// non-full boundary, causing a persistent bitstream desync that +/// corrupts every mantissa read after that point. +/// +/// To match the decoder's schedule exactly, we pre-quantise all of the +/// block's mantissas (already done in `codes`) and then walk the list +/// in order. Whenever we encounter a bap=1/2/4 code and no codes of +/// that bap are buffered, we scan forward to the next two (or one, for +/// bap=4) codes of the same bap, pack them into the group word, and +/// emit it at the current bit position. The "consumed" flags track +/// which codes have already been rolled into a group so the outer loop +/// skips them when reached. +/// +/// Bap values 3, 5, and 6..=15 are emitted inline (no grouping). +pub(crate) fn write_mantissa_stream(bw: &mut BitWriter, codes: &[(u8, u32)]) { + let n = codes.len(); + let mut consumed = vec![false; n]; + for i in 0..n { + if consumed[i] { + continue; + } + let (bap, mant) = codes[i]; + match bap { + 1 => { + // 5-bit group of 3 codes (3-level quantiser). + let m1 = mant; + let (m2, m3) = grab_next_two(codes, &mut consumed, i, 1); + let packed = m1 * 9 + m2 * 3 + m3; + bw.write_u32(packed, 5); + } + 2 => { + // 7-bit group of 3 codes (5-level). + let m1 = mant; + let (m2, m3) = grab_next_two(codes, &mut consumed, i, 2); + let packed = m1 * 25 + m2 * 5 + m3; + bw.write_u32(packed, 7); + } + 4 => { + // 7-bit group of 2 codes (11-level). + let m1 = mant; + let m2 = grab_next_one(codes, &mut consumed, i, 4); + let packed = m1 * 11 + m2; + bw.write_u32(packed, 7); + } + 3 => bw.write_u32(mant, 3), + 5 => bw.write_u32(mant, 4), + b if (6..=15).contains(&b) => { + let nbits = QUANTIZATION_BITS[b as usize] as u32; + bw.write_u32(mant, nbits); + } + _ => {} + } + } +} + +/// Scan forward in `codes` for the next two entries matching `target_bap` +/// that have not yet been consumed. Marks those entries consumed and +/// returns their mantissa codes. Pads with zero if fewer than two are +/// available (typical for end-of-block partial groups). +fn grab_next_two( + codes: &[(u8, u32)], + consumed: &mut [bool], + start: usize, + target_bap: u8, +) -> (u32, u32) { + let mut out = [0u32; 2]; + let mut n = 0usize; + for j in (start + 1)..codes.len() { + if consumed[j] { + continue; + } + if codes[j].0 == target_bap { + out[n] = codes[j].1; + consumed[j] = true; + n += 1; + if n == 2 { + return (out[0], out[1]); + } + } + } + (out[0], out[1]) +} + +/// Scan forward in `codes` for the next entry matching `target_bap` +/// that has not yet been consumed. Marks it consumed and returns its +/// mantissa code. Pads with zero if none remains. +fn grab_next_one(codes: &[(u8, u32)], consumed: &mut [bool], start: usize, target_bap: u8) -> u32 { + for j in (start + 1)..codes.len() { + if consumed[j] { + continue; + } + if codes[j].0 == target_bap { + consumed[j] = true; + return codes[j].1; + } + } + 0 +} + +/// Flush any pending mantissa-group accumulators: grouped-bap codes +/// (bap=1/2/4) live in triples/pairs that are packed only when the +/// group fills. If a block ends with 1 or 2 codes pending, the decoder +/// (whose group state also resets per block) would otherwise read five +/// bits of zero-padding as phantom mantissas. Here we emit a zero- +/// padded group so the bit position stays aligned with the decoder. +#[allow(dead_code)] +fn flush_mant_groups(bw: &mut BitWriter, ctx: &mut MantGroupCtx) { + if ctx.g3_n > 0 { + let m1 = ctx.g3_codes[0]; + let m2 = if ctx.g3_n >= 2 { ctx.g3_codes[1] } else { 0 }; + let m3 = if ctx.g3_n >= 3 { ctx.g3_codes[2] } else { 0 }; + let packed = m1 * 9 + m2 * 3 + m3; + bw.write_u32(packed, 5); + ctx.g3_n = 0; + } + if ctx.g5_n > 0 { + let m1 = ctx.g5_codes[0]; + let m2 = if ctx.g5_n >= 2 { ctx.g5_codes[1] } else { 0 }; + let m3 = if ctx.g5_n >= 3 { ctx.g5_codes[2] } else { 0 }; + let packed = m1 * 25 + m2 * 5 + m3; + bw.write_u32(packed, 7); + ctx.g5_n = 0; + } + if ctx.g11_n > 0 { + let m1 = ctx.g11_codes[0]; + let m2 = if ctx.g11_n >= 2 { ctx.g11_codes[1] } else { 0 }; + let packed = m1 * 11 + m2; + bw.write_u32(packed, 7); + ctx.g11_n = 0; + } +} + +#[allow(dead_code)] +fn write_mantissa(bw: &mut BitWriter, bap: u8, code: u32, ctx: &mut MantGroupCtx) { + match bap { + 1 => { + ctx.g3_codes[ctx.g3_n] = code; + ctx.g3_n += 1; + if ctx.g3_n == 3 { + let m1 = ctx.g3_codes[0]; + let m2 = ctx.g3_codes[1]; + let m3 = ctx.g3_codes[2]; + let packed = m1 * 9 + m2 * 3 + m3; + bw.write_u32(packed, 5); + ctx.g3_n = 0; + } + } + 2 => { + ctx.g5_codes[ctx.g5_n] = code; + ctx.g5_n += 1; + if ctx.g5_n == 3 { + let m1 = ctx.g5_codes[0]; + let m2 = ctx.g5_codes[1]; + let m3 = ctx.g5_codes[2]; + let packed = m1 * 25 + m2 * 5 + m3; + bw.write_u32(packed, 7); + ctx.g5_n = 0; + } + } + 3 => bw.write_u32(code, 3), + 4 => { + ctx.g11_codes[ctx.g11_n] = code; + ctx.g11_n += 1; + if ctx.g11_n == 2 { + let m1 = ctx.g11_codes[0]; + let m2 = ctx.g11_codes[1]; + let packed = m1 * 11 + m2; + bw.write_u32(packed, 7); + ctx.g11_n = 0; + } + } + 5 => bw.write_u32(code, 4), + b if (6..=15).contains(&b) => { + let nbits = QUANTIZATION_BITS[b as usize] as u32; + bw.write_u32(code, nbits); + } + _ => {} + } +} + +// CRC-16 primitives live in `crate::crc` so the decoder can use the +// same residue check (§7.10.1). Re-exported below for the existing +// in-crate `use crate::encoder::ac3_crc_update;` sites (eac3/encoder.rs). +pub(crate) use crate::crc::{ac3_crc_solve_prefix, ac3_crc_update}; + +// --------------------------------------------------------------------------- +// Coupling (§7.4) — encoder-side helpers +// --------------------------------------------------------------------------- + +/// Coupling configuration for a syncframe. Filled once per frame in +/// [`Ac3Encoder::emit_syncframe`] (when the per-frame correlation +/// heuristic decides coupling is worth enabling), then consumed during +/// the bitstream pack. +/// +/// All field names match the §5.4.3 syntax elements. Per-block fields +/// always have `BLOCKS_PER_FRAME` entries; per-channel fields have +/// `nfchans` (=2 for the encoder's currently-supported 2/0 acmod). +#[allow(dead_code)] +pub(crate) struct CouplingPlan { + /// `cplinu` — whether coupling is in use for this frame. When false + /// every other field is meaningless and the encoder emits the + /// "coupling off" syntax (cplstre=1, cplinu=0 on block 0; reuse + /// thereafter). + in_use: bool, + /// `cplbegf` (4 bits) — first coupled subband (Table 7.24). + /// Coefficients below `37 + 12*cplbegf` are coded per-channel. + begf: u8, + /// `cplendf` (4 bits) — last subband index = cplendf + 2. + /// Mantissa-domain end (exclusive) = `37 + 12*(cplendf + 3)`. + endf: u8, + /// `chincpl[ch]` — whether each fbw channel participates in the + /// coupling group. For 2/0 stereo we enable both. + chincpl: [bool; MAX_FBW], + /// `phsflginu` — phase flags in use (only meaningful for 2/0). + phsflginu: bool, + /// `cplbndstrc[sbnd]` for `sbnd ∈ 1..ncplsubnd`. False ⇒ subband + /// starts a new band; true ⇒ merge into previous. Index 0 is + /// always implicitly false. + bndstrc: [bool; 18], + /// Number of coupling subbands (`3 + endf - begf`). + nsubbnd: usize, + /// Number of coupling bands after merging via `bndstrc`. + nbnd: usize, + /// Quantised coupling coordinate exponent (4 bits) per channel + /// per band (`§5.4.3.16` cplcoexp). + cplcoexp: [[u8; 18]; MAX_FBW], + /// Quantised coupling coordinate mantissa (4 bits) per channel + /// per band (`§5.4.3.17` cplcomant). + cplcomant: [[u8; 18]; MAX_FBW], + /// Per-channel master coupling coordinate (`§5.4.3.15` mstrcplco, + /// 2 bits). Adds `3*mstrcplco` to every band's exponent. + mstrcplco: [u8; MAX_FBW], + /// `cplcoe[blk][ch]` — whether new coupling coordinates are + /// signalled for that block. We send them on block 0 (and reuse + /// thereafter), giving the per-frame envelope a stable reference. + cplcoe: [[bool; MAX_FBW]; BLOCKS_PER_FRAME], + /// `phsflg[bnd]` per coupling band (only when `phsflginu`). + phsflg: [bool; 18], +} + +impl Default for CouplingPlan { + fn default() -> Self { + Self { + in_use: false, + begf: 0, + endf: 0, + chincpl: [false; MAX_FBW], + phsflginu: false, + bndstrc: [false; 18], + nsubbnd: 0, + nbnd: 0, + cplcoexp: [[0u8; 18]; MAX_FBW], + cplcomant: [[0u8; 18]; MAX_FBW], + mstrcplco: [0u8; MAX_FBW], + cplcoe: [[false; MAX_FBW]; BLOCKS_PER_FRAME], + phsflg: [false; 18], + } + } +} + +impl CouplingPlan { + /// Build a coupling plan describing the **enhanced-coupling** + /// carrier region for the shared tuner / bit-accounting machinery + /// (`tune_snroffst_with_plan_ends`, `mantissa_bits_total_ends`, + /// `compute_bap_cpl`). Enhanced coupling with `ecpl_begin_subbnd >= + /// 4` lives on the same 12-bin grid as standard coupling + /// (`ecplsubbndtab[s] = 37 + 12*(s - 4)` for `s >= 4`), so the + /// standard-coupling mantissa-domain mapping applies with + /// `begf = begin_subbnd - 4` / `endf = end_subbnd - 7`. Only the + /// region bounds, the channel membership and the band count are + /// meaningful to those helpers; the coordinate-plan fields stay at + /// their defaults (enhanced-coupling coordinates are planned and + /// emitted by `eac3::encoder` itself). + pub(crate) fn for_ecpl( + begin_subbnd: usize, + end_subbnd: usize, + nfchans: usize, + necplbnd: usize, + ) -> Self { + debug_assert!(begin_subbnd >= 4 && end_subbnd > begin_subbnd && end_subbnd <= 22); + let mut chincpl = [false; MAX_FBW]; + for slot in chincpl.iter_mut().take(nfchans.min(MAX_FBW)) { + *slot = true; + } + Self { + in_use: true, + begf: (begin_subbnd - 4) as u8, + endf: (end_subbnd - 7) as u8, + chincpl, + nsubbnd: end_subbnd - begin_subbnd, + nbnd: necplbnd, + ..Self::default() + } + } + + /// Mantissa-domain inclusive lower bound of coupling region. + fn begf_mant(&self) -> usize { + 37 + 12 * self.begf as usize + } + /// Mantissa-domain exclusive upper bound (matches the decoder's + /// `cpl_endf_mant = 37 + 12*(cplendf + 3)`). + fn endf_mant(&self) -> usize { + 37 + 12 * (self.endf as usize + 3) + } + /// Build the `subband -> band` lookup table. Same logic as the + /// decoder so encoder + decoder agree on per-band coordinate + /// application. + fn sbnd_to_bnd(&self) -> [usize; 18] { + let mut out = [0usize; 18]; + let mut bnd = 0usize; + for sbnd in 0..self.nsubbnd { + if sbnd > 0 && !self.bndstrc[sbnd] { + bnd += 1; + } + out[sbnd] = bnd; + } + out + } +} + +// --------------------------------------------------------------------------- +// Delta Bit Allocation (§7.2.2.6, §5.4.3.47-57) — encoder-side helpers +// --------------------------------------------------------------------------- + +/// Per-frame delta bit allocation plan. Holds the segments transmitted on +/// block 0 of each syncframe (deltbae[ch]==1 / cpldeltbae==1 → "new info +/// follows") and reused for blocks 1..5 via deltbae==0. The decoder +/// applies these segments before the §7.2.2.7 bap[] computation; the +/// encoder must apply *exactly* the same offsets so encoder and decoder +/// derive the same bap[] arrays. +/// +/// Index `MAX_FBW` is the coupling pseudo-channel; indices 0..nfchans +/// are the fbw channels. +/// +/// `nseg == 0` means no segments are emitted for that channel (the +/// encoder will signal deltbae==2 = "perform no delta alloc"). +/// `nseg > 0` means deltbae==1 on block 0 (transmit the segments). +/// +/// Encoder policy (round-18 v1): +/// * One segment per fbw channel, spanning a single 1/6th-octave +/// band picked by `pick_dba_band`. The delta is `+6 dB` (deltba=4) +/// which raises the masking floor in that band — the §7.2.2.6 +/// mechanism uses this to free a few mantissa bits in psycho- +/// acoustically-unimportant bands during bursts. We pick a band +/// where the band PSD is well +/// below the channel average, which approximates the "this band +/// is masked harder than the parametric model thinks" decision. +/// * No coupling-channel dba in v1 (cpldeltbae==2 emitted on block 0 +/// when coupling is active). +#[derive(Clone, Copy)] +pub(crate) struct DbaPlan { + /// Per-channel segment count (0..=8). + pub(crate) nseg: [u8; MAX_FBW + 1], + /// Per-channel segment offsets (5 bits each). For seg=0 this is + /// the absolute starting band; for seg>0 it's the gap from the + /// previous segment's end. + pub(crate) offst: [[u8; 8]; MAX_FBW + 1], + /// Per-channel segment lengths in bands (4 bits each, 1..=15). + pub(crate) len: [[u8; 8]; MAX_FBW + 1], + /// Per-channel segment dba codes (3 bits each, Table 5.17). + pub(crate) ba: [[u8; 8]; MAX_FBW + 1], +} + +impl Default for DbaPlan { + fn default() -> Self { + Self { + nseg: [0; MAX_FBW + 1], + offst: [[0; 8]; MAX_FBW + 1], + len: [[0; 8]; MAX_FBW + 1], + ba: [[0; 8]; MAX_FBW + 1], + } + } +} + +/// Convert a Table 5.17 dba code (0..7) into the §7.2.2.6 mask delta +/// (in the same fixed-point units as `mask[]`: each LSB = 1/128 dB, +/// step is ±6 dB = 768 = 6<<7). Mirrors the decoder's branch in +/// `audblk.rs::run_bit_allocation`. +fn dba_delta_for_code(code: u8) -> i32 { + let raw = code as i32; + if raw >= 4 { + (raw - 3) << 7 + } else { + (raw - 4) << 7 + } +} + +/// Apply a channel's dba segments to a `mask[]` array in place. Must +/// be called with the *same* segment list the encoder will transmit so +/// that the decoder reproduces the same mask exactly. Mirrors +/// `audblk.rs::run_bit_allocation` dba branch. +fn apply_dba_segments(plan: &DbaPlan, idx: usize, mask: &mut [i32; 50]) { + if plan.nseg[idx] == 0 { + return; + } + let mut band: usize = 0; + for seg in 0..plan.nseg[idx] as usize { + band += plan.offst[idx][seg] as usize; + let delta = dba_delta_for_code(plan.ba[idx][seg]); + let len = plan.len[idx][seg] as usize; + for _ in 0..len { + if band < 50 { + mask[band] += delta; + } + band += 1; + } + } +} + +/// Classify a band as tonal (1) or noise-like (0) using a simple +/// Spectral Flatness Measure (SFM) on the per-bin exponent values. +/// +/// Tonal bands have one or a few dominant bins (low exponent = high +/// energy) surrounded by silent bins (high exponent). Noise-like bands +/// have more uniform energy distribution. +/// +/// `bins` is the slice of exponent values covering the band. Returns +/// `true` when the band is tonal (peak exponent much smaller than mean). +fn band_is_tonal(bins: &[u8]) -> bool { + if bins.is_empty() { + return false; + } + // Measure: min exponent (loudest bin) vs mean exponent. + // A tonal band has min << mean (one peak dominates). + // A noise band has min ≈ mean (energy spread uniformly). + let min_exp = bins.iter().cloned().min().unwrap_or(24); + let mean_exp_x8: u32 = + bins.iter().map(|&e| e as u32).sum::() * 8 / bins.len().max(1) as u32; + let min_exp_x8: u32 = min_exp as u32 * 8; + // Tonal if mean is > 3 exponent units (= 3/8 * 8 = 3 in x8 scale) + // above the peak — i.e. the loudest bin is at least 3 exponent + // steps (≈ 9 dB) quieter than the average implies. In practice this + // fires when there's a pure tone (single dominant bin) in the band. + mean_exp_x8 > min_exp_x8 + 24 // 24 = 3 exponent units × 8 +} + +/// Build a per-frame DBA plan. For each fbw channel we pick a single +/// mid/high frequency band in [lo_band, hi_band) that is: +/// a) noise-like (not tonal, per SFM) — raising the mask on a tonal +/// band would cost more in perceivable quality than on a noise band; +/// b) relatively quiet (low summed PSD over the frame). +/// +/// This avoids raising the masking threshold on bands where a pure tone +/// is present, instead preferring psychoacoustically expendable bands. +/// +/// Always conservative: nseg=1 per channel, `deltoffst` ≤ 31 (5-bit +/// limit per §5.4.3.51). Guarantees the dba syntax cost per channel is +/// ≤ 17 bits — under 1% of a 192 kbps frame. +/// +/// Cpl-channel dba is left empty (cpldeltbae=2 on block 0). +pub(crate) fn build_dba_plan( + exps: &[Vec<[u8; N_COEFFS]>], + nchan: usize, + end: usize, + cpl: &CouplingPlan, +) -> DbaPlan { + let mut plan = DbaPlan::default(); + // Search range: bands 25..32 (mid frequencies, well below the + // coupling cut-off and above the bass region). The §7.2.2 BNDTAB + // covers bands 0..49; bins 25..32 cover ≈ 1.4–2.7 kHz at 48 kHz. + // + // Upper bound is 32 (= 2^5) because §5.4.3.51 specifies `deltoffst` + // as a 5-bit field (range 0..31) and we currently emit nseg=1, so + // the segment's `deltoffst` is the absolute band number — anything + // ≥ 32 truncates on the wire to `band & 31`, which mis-targets the + // delta on the decoder side and causes a per-frame mask divergence + // at a low band the encoder never tagged. To stay above 31 the + // emitter would need nseg ≥ 2 with an intermediate skip segment, + // which costs more bits than the dba saves; clamp instead. + let lo_band = 25usize; + let hi_band = 32usize.min(MASKTAB[end.saturating_sub(1).max(1)] as usize); + if hi_band <= lo_band + 1 { + return plan; // not enough headroom — leave everything zero + } + for ch in 0..nchan { + // Sum the PSD over all 6 blocks to get a stable per-band + // estimate. PSD = 3072 - 128*exp; bigger PSD = louder bin. + // Also accumulate a per-band tonal vote (how many blocks see a + // tonal distribution in this band). + let mut band_score = [0i64; 50]; + let mut band_tonal_votes = [0u32; 50]; + let nblks = exps[ch].len(); + for blk in 0..nblks { + for bin in 0..end { + let psd = 3072 - ((exps[ch][blk][bin] as i32) << 7); + let band = MASKTAB[bin] as usize; + if band < 50 { + band_score[band] += psd as i64; + } + } + // Per-band tonal vote: check the exponents in each target band. + for b in lo_band..hi_band { + // Collect exponents of bins in this band. + let band_bins: Vec = (0..end) + .filter(|&k| MASKTAB[k] as usize == b) + .map(|k| exps[ch][blk][k]) + .collect(); + if band_is_tonal(&band_bins) { + band_tonal_votes[b] += 1; + } + } + } + // Build a composite score: prefer bands that are + // 1. not predominantly tonal (tonal_votes < nblks/2); + // 2. quiet (low band_score). + // Bands that are mostly tonal get a large penalty so the picker + // skips them and looks for a noisier alternative. + let tonal_penalty: i64 = 1_000_000; // large enough to always prefer non-tonal + let mut best_band = lo_band; + let mut best_score = i64::MAX; + for b in lo_band..hi_band { + let is_mostly_tonal = band_tonal_votes[b] as usize > nblks / 2; + let adj_score = band_score[b] + if is_mostly_tonal { tonal_penalty } else { 0 }; + if adj_score < best_score { + best_score = adj_score; + best_band = b; + } + } + plan.nseg[ch] = 1; + plan.offst[ch][0] = best_band as u8; // first segment: absolute starting band + plan.len[ch][0] = 1; + plan.ba[ch][0] = 4; // +6 dB → raise mask, save ~1 bit per mantissa + } + // Coupling channel: keep nseg=0 in v1. The cpl excitation path + // already runs on a smaller band range and the encoder bit savings + // from coupling are large enough that nudging the cpl mask is a + // second-order optimisation. + let _ = cpl; // unused + plan +} + +/// Quantise a single coupling coordinate `cplco` ∈ (0, 1] into the +/// (cplcoexp, cplcomant) pair encoded by §7.4.3. +/// +/// The decoder reconstructs: +/// * `cplco_temp = (cplcomant + 16) / 32` (when cplcoexp < 15) +/// * `cplco_temp = cplcomant / 16` (when cplcoexp == 15) +/// * `cplco = cplco_temp * 2^-(cplcoexp + 3*mstrcplco)` +/// +/// We therefore choose `shift = cplcoexp + 3*mstrcplco` such that +/// `cplco * 2^shift` lies in `[0.5, 1.0)` (the cplcoexp<15 mantissa +/// range), then quantise the mantissa to one of 16 levels. +/// +/// `mstrcplco` is supplied by the caller (computed once per channel +/// from the band-maximum coordinate) so that all band coordinates for +/// that channel share the same coarse range. +/// +/// Returns `(cplcoexp, cplcomant)`. For `cplco ≤ 0` (silent band), +/// returns the "all silent" code `(15, 0)` which decodes to 0. +fn quantise_cplco(cplco: f32, mstrcplco: u8) -> (u8, u8) { + if cplco <= 0.0 || !cplco.is_finite() { + return (15, 0); + } + // shift = -floor(log2(cplco)) - 1 ⇒ mant = cplco * 2^shift ∈ [0.5, 1.0). + let lg = cplco.log2(); + let mut shift_total = (-(lg.floor()) as i32) - 1; + if shift_total < 0 { + shift_total = 0; + } + // Total shift = cplcoexp + 3 * mstrcplco. Recover the per-band + // exponent from the master coordinate. + let mut cplcoexp = shift_total - 3 * mstrcplco as i32; + if cplcoexp < 0 { + // cplco brighter than the master can express — clamp the + // exponent to 0 (mant will saturate to ~1.0). This happens + // when one band is far louder than the channel's max-band + // average; rare in practice with our master picked from the + // band maximum. + cplcoexp = 0; + } + if cplcoexp >= 15 { + // Use the cplcoexp==15 branch: mant = cplcomant / 16, range + // (0, 1]. Pick the mantissa as cplco * 16 * 2^(3*mstr). + let scale = (1u32 << (3 * mstrcplco as u32)) as f32 * 16.0; + let v = (cplco * scale).round() as i32; + let cplcomant = v.clamp(0, 15) as u8; + return (15, cplcomant); + } + // cplcoexp < 15: mant ∈ [0.5, 1.0). cplcomant in 0..15 represents + // (cplcomant + 16) / 32 — the leading "1" is implicit, only the + // next 4 bits are sent. + let mant = cplco * (1u32 << shift_total as u32) as f32; // ∈ [0.5, 1.0) + let v = (mant * 32.0).round() as i32 - 16; + let cplcomant = v.clamp(0, 15) as u8; + (cplcoexp as u8, cplcomant) +} + +/// Pick `mstrcplco` (2 bits) from the channel's band-max coordinate. +/// The per-band exponent has 4 bits of headroom; the master +/// coordinate adds another 9 bits (3 * mstrcplco, mstrcplco ∈ 0..3). +/// We pick the smallest mstrcplco such that the loudest band still +/// has a usable cplcoexp ∈ 0..14 (reserving 15 for the cplcoexp==15 +/// branch). +/// +/// Loud bands ⇒ small total shift ⇒ mstrcplco = 0. +/// Quiet bands ⇒ large total shift ⇒ mstrcplco grows. +fn pick_mstrcplco(max_cplco: f32) -> u8 { + if max_cplco >= 1.0 { + return 0; + } + if max_cplco <= 0.0 || !max_cplco.is_finite() { + return 3; + } + let lg = max_cplco.log2(); + let need_shift = (-(lg.floor()) as i32) - 1; // total shift for the loudest band + // We want need_shift - 3*mstr ∈ 0..15; minimise mstr. + let need_shift = need_shift.max(0); + let mstr = (need_shift - 14).max(0); // ensure cplcoexp ≤ 14 + let mstr = mstr.div_euclid(3) + i32::from(mstr.rem_euclid(3) > 0); + mstr.clamp(0, 3) as u8 +} + +/// Reconstruct the linear coupling coordinate from its quantised +/// (cplcoexp, cplcomant, mstrcplco) representation. Mirrors the +/// decoder's `state.cpl_coord = mant * 2^(-shift)` formula. +fn reconstruct_cplco(cplcoexp: u8, cplcomant: u8, mstrcplco: u8) -> f32 { + let mant = if cplcoexp == 15 { + cplcomant as f32 / 16.0 + } else { + (cplcomant as f32 + 16.0) / 32.0 + }; + let shift = cplcoexp as i32 + 3 * mstrcplco as i32; + mant * 2f32.powi(-shift) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::{CodecId, CodecParameters, SampleFormat}; + + /// Walk the decoder's grouped-exponent reconstruction (matches + /// `audblk::decode_exponents` + grpsize expansion) on the encoder's + /// emitted bits, to verify round-trip equality of the per-bin + /// exponent array. Returns the reconstructed exponent array. + fn decode_exponents_test( + bits: &[u8], + absexp_bits: u32, + ngrps: usize, + grpsize: usize, + end: usize, + ) -> Vec { + use oxideav_core::bits::BitReader; + let mut br = BitReader::new(bits); + let absexp = br.read_u32(4).unwrap() as i32; + debug_assert_eq!(absexp as u32, absexp_bits); + let mut prev = absexp; + let mut out = vec![0u8; end]; + out[0] = absexp.clamp(0, 24) as u8; + for grp in 0..ngrps { + let gexp = br.read_u32(7).unwrap() as i32; + let m1 = gexp / 25; + let m2 = (gexp % 25) / 5; + let m3 = (gexp % 25) % 5; + let dexp0 = m1 - 2; + let dexp1 = m2 - 2; + let dexp2 = m3 - 2; + let e0 = (prev + dexp0).clamp(0, 24); + let e1 = (e0 + dexp1).clamp(0, 24); + let e2 = (e1 + dexp2).clamp(0, 24); + for (k, &e) in [e0, e1, e2].iter().enumerate() { + let i = grp * 3 + k; + let base = i * grpsize + 1; + for j in 0..grpsize { + if base + j < end { + out[base + j] = e as u8; + } + } + } + prev = e2; + } + out + } + + fn encode_exponents_test(exp: &[u8], end: usize, grpsize: usize) -> Vec { + use oxideav_core::bits::BitWriter; + let mut bw = BitWriter::with_capacity(64); + let mut padded = [0u8; N_COEFFS]; + padded[..exp.len().min(N_COEFFS)].copy_from_slice(&exp[..exp.len().min(N_COEFFS)]); + write_exponents_grouped(&mut bw, &padded, end, grpsize); + bw.into_bytes() + } + + /// Encoder/decoder round-trip parity for the new + /// `quantise_exponents_to_grpsize` + `write_exponents_grouped` + /// pipeline. Verifies that for every input shape the bit allocator + /// (encoder side, post-quantise) and the decoder (post-grpsize + /// expansion) see the same per-bin exponents — without this, bap[] + /// disagreement causes mantissa-stream byte drift. + #[test] + fn quantise_grpsize_roundtrip_parity_d25_d45() { + for (label, mut exp_init) in [ + ( + "ch0_dba_test_blk0_realistic", + vec![ + 9u8, 1, 1, 5, 8, 16, 18, 19, 19, 20, 20, 20, 20, 21, 21, 21, 22, 22, 22, 22, + 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + ], + ), + ( + "ramp_with_silence_tail", + vec![ + 3u8, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + ], + ), + ] { + // Pad to 133 bins (typical coupled fbw end_mant). + while exp_init.len() < 133 { + exp_init.push(23); + } + for &(grpsize, strat_label) in &[(2usize, "D25"), (4usize, "D45")] { + let mut exp = exp_init.clone(); + let end = 133usize; + quantise_exponents_to_grpsize(&mut exp[..end], grpsize); + let bits = encode_exponents_test(&exp, end, grpsize); + let ngrps = ngrps_for_strategy(end, grpsize); + let decoded = decode_exponents_test(&bits, exp[0] as u32, ngrps, grpsize, end); + for k in 0..end { + assert_eq!( + exp[k], decoded[k], + "{}: {} mismatch at bin {}: encoder sees {}, decoder reconstructs {}", + label, strat_label, k, exp[k], decoded[k], + ); + } + } + } + } + + /// Encode a 440 Hz sine, then decode it back through our own + /// decoder. We expect the round-trip to produce a non-zero RMS on + /// both channels (evidence the bit-stream is legal and the + /// spectral envelope survives the codec). + #[test] + fn sine_roundtrip_self_decode() { + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(48_000); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + + // Build 1 second of 440 Hz stereo sine at 48 kHz. + let sr = 48_000u32; + let dur = 1.0f32; + let nsamp = (sr as f32 * dur) as usize; + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let s = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.4; + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + + // Drain packets. + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 30, "got only {} packets", pkts.len()); + // Each packet is a 768-byte syncframe. + for p in &pkts { + assert_eq!(p.data.len(), 768); + assert_eq!(p.data[0], 0x0B); + assert_eq!(p.data[1], 0x77); + } + + // Decode back. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + + let mut decoded_samples_left: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + match dec.receive_frame() { + Ok(Frame::Audio(a)) => { + let plane = &a.data[0]; + for s in plane.chunks_exact(4) { + let l = i16::from_le_bytes([s[0], s[1]]); + decoded_samples_left.push(l); + } + } + Ok(_) => panic!("unexpected frame"), + Err(e) => panic!("decode error: {e:?}"), + } + } + assert!(!decoded_samples_left.is_empty()); + let skip = 512usize.min(decoded_samples_left.len()); + let sq: f64 = decoded_samples_left[skip..] + .iter() + .map(|&s| (s as f64) * (s as f64)) + .sum(); + let rms = (sq / (decoded_samples_left.len() - skip) as f64).sqrt(); + eprintln!( + "self-decode RMS: {:.1} ({} decoded samples)", + rms, + decoded_samples_left.len() + ); + // Loose sanity — simply *non-silent* output proves the encoder + // produced a syntactically-valid syncframe that the decoder + // consumed end-to-end. + assert!(rms > 50.0, "self-decoded RMS too low: {rms}"); + } + + /// Sanity: long vs short MDCT on a transient input produce + /// substantially different spectra. Otherwise the round-trip + /// can't see any improvement from short-block emission. We feed + /// a 512-sample windowed input with a single Gaussian impulse at + /// the centre through both paths, then compare a few mid-band + /// coefficient magnitudes. + #[test] + fn long_vs_short_mdct_differ_on_impulse() { + let mut buf = [0.0f32; 512]; + // Sharp click at sample 384 (right half of the 512-sample window). + for n in 0..512 { + let dn = (n as f32 - 384.0) / 4.0; + buf[n] = (-(dn * dn)).exp(); + } + // Apply the standard window so both paths get the same input. + let mut win_buf = [0.0f32; 512]; + for n in 0..256 { + win_buf[n] = buf[n] * crate::tables::WINDOW[n]; + win_buf[511 - n] = buf[511 - n] * crate::tables::WINDOW[n]; + } + let mut x_long = [0.0f32; 256]; + let mut x_short = [0.0f32; 256]; + crate::mdct::mdct_512(&win_buf, &mut x_long); + crate::mdct::mdct_256_pair(&win_buf, &mut x_short); + + let mut max_long_mag: f32 = 0.0; + let mut max_short_mag: f32 = 0.0; + for k in 0..256 { + max_long_mag = max_long_mag.max(x_long[k].abs()); + max_short_mag = max_short_mag.max(x_short[k].abs()); + } + eprintln!("long peak={max_long_mag:.4} short peak={max_short_mag:.4}"); + // The two transforms must produce different coefficients. If + // they're identical, my short MDCT collapsed to the long path. + let mut max_diff = 0.0f32; + for k in 0..256 { + max_diff = max_diff.max((x_long[k] - x_short[k]).abs()); + } + eprintln!("max long-vs-short coeff diff: {max_diff:.4}"); + assert!( + max_diff > 0.001, + "long and short MDCT produce identical output — encoder bug" + ); + } + + /// `detect_transient` smoke test on synthesised inputs. A pure + /// 440 Hz sine should NOT trigger; a Gaussian-amplitude burst at + /// the centre of the block SHOULD trigger. + #[test] + fn transient_detector_sanity() { + let mut sine = [0.0f32; 256]; + for n in 0..256 { + let t = n as f32 / 48_000.0; + sine[n] = 0.4 * (2.0 * std::f32::consts::PI * 440.0 * t).sin(); + } + assert!( + !detect_transient(&sine), + "pure sine flagged as transient — false positive" + ); + // Gaussian burst at sample 192 (well within block 256). + let mut burst = sine; + for n in 0..256 { + let dn = (n as f32 - 192.0) / 8.0; + let env = (-(dn * dn)).exp(); + burst[n] += + 0.6 * env * (2.0 * std::f32::consts::PI * 1200.0 / 48_000.0 * n as f32).sin(); + } + assert!( + detect_transient(&burst), + "Gaussian burst missed by transient detector" + ); + } + + /// End-to-end transient encode + decode: build a stereo signal with + /// three sharp clicks (matching the broadband content of real AC-3 + /// transients), encode, then decode through our own decoder. Verify + /// (a) at least a handful of audio blocks emit `blksw=1`, and + /// (b) the round-trip RMS is non-trivial. The encoder→decoder PSNR + /// floor for transient content with short blocks active is + /// meaningfully above the long-only baseline (~21 dB documented in + /// the task brief). + /// + /// Click structure: a half-Hann-windowed broadband impulse (4 kHz + + /// 8 kHz components, σ ≈ 12 samples) at each click time. The 8 kHz + /// content is essential — the §8.2.2 transient detector applies a + /// 4th-order Butterworth HPF at 8 kHz cutoff, so a click whose + /// spectrum dies below 4 kHz produces no detectable post-HPF peak. + #[test] + fn transient_roundtrip_self_decode() { + let sr = 48_000u32; + let dur = 1.0f32; + let nsamp = (sr as f32 * dur) as usize; + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let base = 0.10 * (2.0 * std::f32::consts::PI * 440.0 * t).sin(); + let mut burst = 0.0f32; + // Sharp clicks at 0.20 / 0.50 / 0.80 s. Each click is a + // narrow Gaussian-windowed sine pair (4 kHz + 8 kHz) at + // σ=12 samples (~0.25 ms wide). The HF content survives + // the 8 kHz HPF and produces a clean P[3] peak for the + // §8.2.2 detector to fire on. + for &t_burst in &[0.20f32, 0.50, 0.80] { + let dt = (t - t_burst) * sr as f32 / 12.0; + let env = (-(dt * dt)).exp(); + burst += 0.7 + * env + * ((2.0 * std::f32::consts::PI * 4000.0 * t).sin() * 0.5 + + (2.0 * std::f32::consts::PI * 8000.0 * t).sin() * 0.5); + } + let s = (base + burst).clamp(-1.0, 1.0); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 30, "expected ≥30 packets, got {}", pkts.len()); + + // Count blksw=1 blocks across the produced bitstream. + let mut total_blocks = 0usize; + let mut short_blocks = 0usize; + for (frame_idx, p) in pkts.iter().enumerate() { + let si = crate::syncinfo::parse(&p.data).expect("syncinfo"); + let b = crate::bsi::parse(&p.data[5..]).expect("bsi"); + let side = crate::audblk::parse_frame_side_info(&si, &b, &p.data).expect("side-info"); + for (blk_idx, s) in side.iter().enumerate() { + total_blocks += 1; + if s.blksw.iter().take(b.nfchans as usize).any(|&x| x) { + short_blocks += 1; + if std::env::var("AC3_DUMP_BLKSW").is_ok() { + eprintln!( + " blksw=1 at frame {frame_idx} block {blk_idx} (sample ~{})", + (frame_idx * BLOCKS_PER_FRAME + blk_idx) * SAMPLES_PER_BLOCK + ); + } + } + } + } + eprintln!("encoder emitted {short_blocks}/{total_blocks} short-block audblks"); + // Skip the short-block-count gate when the env-var disables + // the detector — we still want PSNR numbers in that mode for + // the round-15 A/B comparison. + if std::env::var("AC3_DISABLE_BLKSW").is_err() { + assert!( + short_blocks >= 3, + "transient detector failed to fire on burst fixture: {short_blocks}/{total_blocks}" + ); + } + + // Decode and compare RMS / peak — proof the bitstream is valid + // and the transient regions reconstruct. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(4) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + assert!( + decoded.len() > nsamp / 2, + "decoded too few samples: {}", + decoded.len() + ); + let skip = 512usize.min(decoded.len()); + let sq: f64 = decoded[skip..].iter().map(|&s| (s as f64).powi(2)).sum(); + let rms = (sq / (decoded.len() - skip) as f64).sqrt(); + eprintln!("transient self-decode RMS: {rms:.1}"); + assert!(rms > 200.0, "transient self-decode RMS too low: {rms}"); + + // Compute PSNR vs the original PCM. The decoder primes its + // overlap-add window on the first frame (samples 0..256 are + // silent by construction), and the encoder's first MDCT also + // sees a zero left context — so we cross-correlate ±512 + // samples to align before measuring PSNR. + let orig_l: Vec = (0..nsamp).map(|i| pcm[i * 2]).collect(); + let n = decoded.len().min(orig_l.len()); + let skip = 768usize.min(n); + let usable = n.saturating_sub(skip); + let mut best_lag = 0i32; + let mut best_sse = f64::INFINITY; + for lag in -512i32..=512 { + let mut sse = 0.0f64; + let mut count = 0usize; + for i in 0..usable { + let a = (skip + i) as i32; + let b = a + lag; + if b < 0 || (b as usize) >= orig_l.len() { + continue; + } + let d = decoded[a as usize] as f64 - orig_l[b as usize] as f64; + sse += d * d; + count += 1; + } + if count > 0 { + let mse = sse / count as f64; + if mse < best_sse { + best_sse = mse; + best_lag = lag; + } + } + } + let psnr = if best_sse > 0.0 { + 10.0 * (32767.0f64.powi(2) / best_sse).log10() + } else { + f64::INFINITY + }; + eprintln!("transient self-decode PSNR: {psnr:.2} dB (best_lag={best_lag})"); + + // Localised PSNR over a 1024-sample window centred on each + // burst — this is the metric where short-block emission + // matters. The whole-fixture PSNR is dominated by the steady + // 440 Hz background which both encoder paths handle equally + // well; the short-block win shows up only inside the bursts. + let burst_centres = [ + (0.20 * sr as f32) as usize, + (0.50 * sr as f32) as usize, + (0.80 * sr as f32) as usize, + ]; + let mut burst_sse = 0.0f64; + let mut burst_count = 0usize; + for ¢re in &burst_centres { + // Tight ±256-sample window — centred on the burst peak, + // covers ~5 ms which roughly matches the audible region + // where pre/post-echo from a long-block MDCT lives. + let lo = centre.saturating_sub(256); + let hi = (centre + 256).min(n); + for i in lo..hi { + let a = i as i32; + let b = a + best_lag; + if b < 0 || (b as usize) >= orig_l.len() { + continue; + } + let d = decoded[a as usize] as f64 - orig_l[b as usize] as f64; + burst_sse += d * d; + burst_count += 1; + } + } + if burst_count > 0 { + let burst_mse = burst_sse / burst_count as f64; + let burst_psnr = if burst_mse > 0.0 { + 10.0 * (32767.0f64.powi(2) / burst_mse).log10() + } else { + f64::INFINITY + }; + eprintln!("burst-only PSNR: {burst_psnr:.2} dB ({burst_count} samples)"); + } + // Sanity floor — must be well above the 21 dB long-only + // baseline. A real bug (eg. blksw bit not reaching the + // decoder) would crash this back to single-digit dB. + if std::env::var("AC3_DISABLE_BLKSW").is_err() { + assert!( + psnr > 18.0, + "transient PSNR {psnr:.2} dB below 18 dB short-block floor" + ); + } + } + + /// ffmpeg-decode-our-output gate. Encode the transient fixture + /// with short blocks active, write the syncframes to a temp file, + /// pipe through `ffmpeg` to produce a PCM decode, and verify + /// non-zero output. This proves the bitstream is genuinely + /// spec-compliant — a decoder we did NOT write parses our + /// `blksw`-bearing audblks without bailing. Skips gracefully if + /// ffmpeg is absent. + #[test] + fn ffmpeg_decodes_our_blksw_output() { + use std::process::Command; + let sr = 48_000u32; + let nsamp = sr as usize / 2; // 0.5 s + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let base = 0.10 * (2.0 * std::f32::consts::PI * 440.0 * t).sin(); + // Single Gaussian burst at 0.25 s. + let dt = (t - 0.25) * sr as f32 / 32.0; + let env = (-(dt * dt)).exp(); + let burst = 0.7 * env * (2.0 * std::f32::consts::PI * 1500.0 * t).sin(); + let s = (base + burst).clamp(-1.0, 1.0); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut ac3_bytes: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => ac3_bytes.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + let in_path = std::env::temp_dir().join("oxideav_ac3_blksw_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_blksw_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args(["-f", "s16le", "-acodec", "pcm_s16le", "-ac", "2"]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping cross-decode gate"); + return; + }; + if !status.success() { + panic!("ffmpeg failed to decode our blksw-bearing AC-3 output"); + } + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + assert!( + decoded_bytes.len() > 1000, + "ffmpeg produced suspiciously short decode: {} bytes", + decoded_bytes.len() + ); + let dsq: f64 = decoded_bytes + .chunks_exact(4) + .map(|c| { + let l = i16::from_le_bytes([c[0], c[1]]) as f64; + l * l + }) + .sum(); + let drms = (dsq / (decoded_bytes.len() / 4) as f64).sqrt(); + eprintln!( + "ffmpeg decode of our blksw output: {} bytes, RMS {:.1}", + decoded_bytes.len(), + drms + ); + assert!(drms > 200.0, "ffmpeg-decoded RMS too low: {drms}"); + } + + /// Per-channel-per-block exponent strategy selection (§7.1.3 / + /// §5.4.3.22) — verify the encoder picks D25 (`chexpstr=2`) on a + /// smooth-envelope source where it's spec-legal, and that the + /// validator binary cross-decodes the resulting bit-stream cleanly. + /// + /// Setup: a stereo bass tone (220 Hz) plus a few mid-band + /// harmonics. The energy is concentrated below ~2 kHz where each + /// 1/6-octave band's exponent envelope is smooth; D25's + /// pair-shared exponent representation costs ~½ the bits of D15 + /// and the bit allocator can spend the savings on mantissa + /// resolution. + /// + /// Gates: (a) `parse_frame_side_info` reads `chexpstr[ch] == 2` + /// (D25) on at least one anchor block (block 0 or 3) of each + /// frame, (b) the validator binary decodes the elementary stream + /// without error, (c) the decoded RMS is non-trivial (not silence). + #[test] + fn d25_exp_strategy_selection_and_ffmpeg_crosscheck() { + use std::process::Command; + let sr = 48_000u32; + let dur = 1.0f32; + let nsamp = (sr as f32 * dur) as usize; + // Bass + mid harmonic mix — smooth spectral envelope below + // 2 kHz, near-silent above. The encoder's strategy selector + // should pick D25 on the anchor blocks. + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let lo = 0.40 * (2.0 * std::f32::consts::PI * 220.0 * t).sin(); + let mid1 = 0.20 * (2.0 * std::f32::consts::PI * 440.0 * t).sin(); + let mid2 = 0.10 * (2.0 * std::f32::consts::PI * 880.0 * t).sin(); + let s = (lo + mid1 + mid2).clamp(-1.0, 1.0); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + let mut ac3_bytes: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => { + ac3_bytes.extend_from_slice(&p.data); + pkts.push(p); + } + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 30, "got only {} packets", pkts.len()); + + // Gate (a): at least one anchor block of each frame uses D25. + let mut frames_with_d25 = 0usize; + for p in &pkts { + let si = crate::syncinfo::parse(&p.data).expect("syncinfo"); + let b = crate::bsi::parse(&p.data[5..]).expect("bsi"); + let side = crate::audblk::parse_frame_side_info(&si, &b, &p.data).expect("side-info"); + let mut frame_has_d25 = false; + for s in &side { + for v in s.chexpstr.iter().take(2) { + if *v == 2 { + frame_has_d25 = true; + } + } + } + if frame_has_d25 { + frames_with_d25 += 1; + } + } + eprintln!( + "D25 selection: {}/{} frames carry chexpstr=2 on at least one fbw channel/block", + frames_with_d25, + pkts.len() + ); + assert!( + frames_with_d25 * 2 >= pkts.len(), + "D25 strategy never picked ({} of {} frames) — selector thresholds may be off", + frames_with_d25, + pkts.len() + ); + + // Gate (b)/(c): ffmpeg cross-decode. + let in_path = std::env::temp_dir().join("oxideav_ac3_d25_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_d25_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args(["-f", "s16le", "-acodec", "pcm_s16le", "-ac", "2"]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping cross-decode gate"); + return; + }; + assert!( + status.success(), + "ffmpeg failed to decode our D25-strategy AC-3 output" + ); + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + assert!( + decoded_bytes.len() > 4_000, + "ffmpeg produced suspiciously short decode: {} bytes", + decoded_bytes.len() + ); + let dsq: f64 = decoded_bytes + .chunks_exact(4) + .map(|c| { + let l = i16::from_le_bytes([c[0], c[1]]) as f64; + l * l + }) + .sum(); + let drms = (dsq / (decoded_bytes.len() / 4) as f64).sqrt(); + eprintln!( + "ffmpeg decode of our D25 output: {} bytes, RMS {:.1}", + decoded_bytes.len(), + drms + ); + assert!(drms > 1000.0, "ffmpeg-decoded RMS too low: {drms}"); + } + + /// Round-29 regression: D45 grpsize=4 exponent strategy round-trips + /// bit-exact through both the in-tree decoder AND the validator + /// binary. + /// + /// The dba-offset-truncation bug fixed in `build_dba_plan` + /// (best_band capped at 31 to fit the 5-bit `deltoffst` field per + /// §5.4.3.51) made the first-frame mantissa stream desync by one + /// bit; with the cap this test passes against the validator binary + /// and against our decoder's PSNR floor. + /// + /// Picks a smooth low-band signal so the strategy selector emits + /// chexpstr=3 on at least one anchor block per frame. + #[test] + fn d45_exp_strategy_selection_and_ffmpeg_crosscheck() { + use std::process::Command; + let sr = 48_000u32; + let dur = 1.0f32; + let nsamp = (sr as f32 * dur) as usize; + // Pure 110 Hz tone: HF bins are zero so the decimated exponent + // ladder is monotonically increasing and very smooth — the + // smoothness test in `pick_strategy_for_block` should pick D45 + // on at least one anchor block per frame. + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let s = 0.50 * (2.0 * std::f32::consts::PI * 110.0 * t).sin(); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + let mut ac3_bytes: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => { + ac3_bytes.extend_from_slice(&p.data); + pkts.push(p); + } + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 30, "got only {} packets", pkts.len()); + + // Gate (a): at least half the frames should carry chexpstr=3 + // (D45) on at least one fbw channel anchor block. + let mut frames_with_d45 = 0usize; + for p in &pkts { + let si = crate::syncinfo::parse(&p.data).expect("syncinfo"); + let b = crate::bsi::parse(&p.data[5..]).expect("bsi"); + let side = crate::audblk::parse_frame_side_info(&si, &b, &p.data).expect("side-info"); + let mut frame_has_d45 = false; + for s in &side { + for v in s.chexpstr.iter().take(2) { + if *v == 3 { + frame_has_d45 = true; + } + } + } + if frame_has_d45 { + frames_with_d45 += 1; + } + } + eprintln!( + "D45 selection: {}/{} frames carry chexpstr=3 on at least one fbw channel/block", + frames_with_d45, + pkts.len() + ); + assert!( + frames_with_d45 * 2 >= pkts.len(), + "D45 strategy never picked ({} of {} frames) — selector thresholds may be off", + frames_with_d45, + pkts.len() + ); + + // Gate (b): self-decode round-trip — PSNR > 20 dB after lag align. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(4) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + assert!(decoded.len() > nsamp / 2, "decoded too few samples"); + let orig_l: Vec = (0..nsamp).map(|i| pcm[i * 2]).collect(); + let n = decoded.len().min(orig_l.len()); + let skip = 768usize.min(n); + let usable = n.saturating_sub(skip); + let mut best_sse = f64::INFINITY; + for lag in -512i32..=512 { + let mut sse = 0.0f64; + let mut count = 0usize; + for i in 0..usable { + let a = (skip + i) as i32; + let b = a + lag; + if b < 0 || (b as usize) >= orig_l.len() { + continue; + } + let d = decoded[a as usize] as f64 - orig_l[b as usize] as f64; + sse += d * d; + count += 1; + } + if count > 0 { + let mse = sse / count as f64; + if mse < best_sse { + best_sse = mse; + } + } + } + let psnr = if best_sse > 0.0 { + 10.0 * (32767.0f64.powi(2) / best_sse).log10() + } else { + f64::INFINITY + }; + eprintln!("D45 self-decode PSNR: {psnr:.2} dB"); + assert!( + psnr > 20.0, + "D45 self-decode PSNR collapsed: {psnr:.2} dB (regression of the 5-bit dba_offst truncation bug?)" + ); + + // Gate (c): ffmpeg cross-decode. + let in_path = std::env::temp_dir().join("oxideav_ac3_d45_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_d45_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args(["-f", "s16le", "-acodec", "pcm_s16le", "-ac", "2"]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping cross-decode gate"); + return; + }; + assert!( + status.success(), + "ffmpeg failed to decode our D45-strategy AC-3 output" + ); + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + assert!( + decoded_bytes.len() > 4_000, + "ffmpeg produced suspiciously short decode: {} bytes", + decoded_bytes.len() + ); + let dsq: f64 = decoded_bytes + .chunks_exact(4) + .map(|c| { + let l = i16::from_le_bytes([c[0], c[1]]) as f64; + l * l + }) + .sum(); + let drms = (dsq / (decoded_bytes.len() / 4) as f64).sqrt(); + eprintln!( + "ffmpeg decode of our D45 output: {} bytes, RMS {:.1}", + decoded_bytes.len(), + drms + ); + assert!(drms > 1000.0, "ffmpeg-decoded RMS too low: {drms}"); + } + + /// Encode a stereo signal with strong high-frequency content, then + /// verify (a) the decoder side parses cplinu=1 in every audblk + /// (proof the coupling syntax is on the wire), (b) the round-trip + /// PSNR remains above the per-channel-only baseline, and (c) + /// ffmpeg cross-decodes the coupled stream successfully. + /// + /// Coupling encode is the round-16 deliverable; this test gates + /// that the encoder doesn't regress while we add §7.4 syntax. + #[test] + fn coupling_self_decode_and_ffmpeg_crosscheck() { + use std::process::Command; + let sr = 48_000u32; + let dur = 1.0f32; + let nsamp = (sr as f32 * dur) as usize; + // Stereo signal with rich HF content above the cpl_begf + // boundary (133 bins ≈ 6.0 kHz @ 48 kHz). Two correlated + // sine tones at 880 Hz and 8 kHz on both channels. + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let lo = 0.30 * (2.0 * std::f32::consts::PI * 880.0 * t).sin(); + let hi = 0.20 * (2.0 * std::f32::consts::PI * 8000.0 * t).sin(); + let s = (lo + hi).clamp(-1.0, 1.0); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 30, "got only {} packets", pkts.len()); + + // Verify cplinu=1 in side info on every audblk that signals + // a strategy (block 0 of each frame transmits cplstre=1). + let mut cpl_blocks_seen = 0usize; + for p in &pkts { + let si = crate::syncinfo::parse(&p.data).expect("syncinfo"); + let b = crate::bsi::parse(&p.data[5..]).expect("bsi"); + let side = crate::audblk::parse_frame_side_info(&si, &b, &p.data).expect("side-info"); + for (blk, s) in side.iter().enumerate() { + if blk == 0 { + assert!( + s.cplstre, + "cplstre missing on block 0 of a syncframe — coupling syntax not emitted" + ); + assert!( + s.cplinu, + "cplinu=0 on a frame the encoder is supposed to couple" + ); + if s.cplinu { + cpl_blocks_seen += 1; + } + } + } + } + eprintln!("cpl-encoded blocks: {cpl_blocks_seen} (frames carrying cplinu=1)"); + assert!( + cpl_blocks_seen >= pkts.len(), + "cplinu was set on fewer frames than expected: {} of {}", + cpl_blocks_seen, + pkts.len() + ); + + // Self-decode round-trip. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(4) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + assert!(decoded.len() > nsamp / 2, "decoded too few samples"); + // PSNR with the same lag-search as the transient round-trip. + let orig_l: Vec = (0..nsamp).map(|i| pcm[i * 2]).collect(); + let n = decoded.len().min(orig_l.len()); + let skip = 768usize.min(n); + let usable = n.saturating_sub(skip); + let mut best_lag = 0i32; + let mut best_sse = f64::INFINITY; + for lag in -512i32..=512 { + let mut sse = 0.0f64; + let mut count = 0usize; + for i in 0..usable { + let a = (skip + i) as i32; + let b = a + lag; + if b < 0 || (b as usize) >= orig_l.len() { + continue; + } + let d = decoded[a as usize] as f64 - orig_l[b as usize] as f64; + sse += d * d; + count += 1; + } + if count > 0 { + let mse = sse / count as f64; + if mse < best_sse { + best_sse = mse; + best_lag = lag; + } + } + } + let psnr = if best_sse > 0.0 { + 10.0 * (32767.0f64.powi(2) / best_sse).log10() + } else { + f64::INFINITY + }; + eprintln!( + "coupled self-decode PSNR: {psnr:.2} dB (best_lag={best_lag}, decoded {} samples)", + decoded.len() + ); + // PSNR floor: coupling on a low-tone+HF-tone pair should + // still recover both tones. Our measured baseline (with cpl + // wired correctly) is ~28-32 dB depending on the exact + // burst content; a gross failure (eg. the cpl side info is + // misaligned) drops this to single digits. + assert!(psnr > 18.0, "coupled self-decode PSNR too low: {psnr:.2}"); + + // ffmpeg cross-decode of the coupled stream. + let mut ac3_bytes: Vec = Vec::new(); + for p in &pkts { + ac3_bytes.extend_from_slice(&p.data); + } + let in_path = std::env::temp_dir().join("oxideav_ac3_cpl_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_cpl_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args(["-f", "s16le", "-acodec", "pcm_s16le", "-ac", "2"]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping cross-decode gate"); + return; + }; + assert!( + status.success(), + "ffmpeg failed to decode our coupling-bearing AC-3 output" + ); + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + assert!( + decoded_bytes.len() > 1000, + "ffmpeg coupled-stream decode too short: {} bytes", + decoded_bytes.len() + ); + let dsq: f64 = decoded_bytes + .chunks_exact(4) + .map(|c| { + let l = i16::from_le_bytes([c[0], c[1]]) as f64; + l * l + }) + .sum(); + let drms = (dsq / (decoded_bytes.len() / 4) as f64).sqrt(); + eprintln!( + "ffmpeg decode of our coupling output: {} bytes, RMS {:.1}", + decoded_bytes.len(), + drms + ); + assert!(drms > 200.0, "ffmpeg-coupled RMS too low: {drms}"); + } + + /// Round-18 §7.2.2.6 / §5.4.3.47-57 delta bit allocation gate. + /// + /// Encode a 1-second stereo sine, parse the resulting syncframes + /// and verify: + /// (a) deltbaie == true on block 0 of every frame (proof the dba + /// syntax is now on the wire, not the round-15..17 default + /// deltbaie=0 marker), + /// (b) deltbaie == false on blocks 1..5 of every frame (reuse — + /// block-0's segment list applies for the whole syncframe), + /// (c) self-decode round-trip RMS stays > 50 (the + /// sine_roundtrip_self_decode invariant — bap[] must still + /// be coherent between encoder and decoder under dba), and + /// (d) ffmpeg cross-decodes the dba-bearing stream (gold + /// standard for spec compliance). + #[test] + fn dba_self_decode_and_ffmpeg_crosscheck() { + use std::process::Command; + let sr = 48_000u32; + let dur = 1.0f32; + let nsamp = (sr as f32 * dur) as usize; + let mut pcm = vec![0i16; nsamp * 2]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + let s = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.4; + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 30, "got only {} packets", pkts.len()); + + // (a) + (b): inspect deltbaie across every audblk. + let mut block0_dba = 0usize; + let mut blockn_no_dba = 0usize; + for p in &pkts { + let si = crate::syncinfo::parse(&p.data).expect("syncinfo"); + let b = crate::bsi::parse(&p.data[5..]).expect("bsi"); + let side = crate::audblk::parse_frame_side_info(&si, &b, &p.data).expect("side-info"); + for (blk, s) in side.iter().enumerate() { + if blk == 0 { + assert!( + s.deltbaie, + "deltbaie missing on block 0 of a syncframe — round-18 dba not on the wire" + ); + block0_dba += 1; + } else if !s.deltbaie { + blockn_no_dba += 1; + } + } + } + assert_eq!(block0_dba, pkts.len(), "block-0 dba count mismatch"); + assert_eq!( + blockn_no_dba, + pkts.len() * 5, + "blocks 1..5 should all reuse (deltbaie=0)" + ); + + // (c): self-decode round-trip RMS preserved. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(4) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + let skip = 512usize.min(decoded.len()); + let usable = decoded.len().saturating_sub(skip); + assert!(usable > 0, "no decoded samples to score"); + let sq: f64 = decoded[skip..] + .iter() + .map(|&s| (s as f64) * (s as f64)) + .sum(); + let rms = (sq / usable as f64).sqrt(); + eprintln!( + "dba self-decode RMS: {:.1} ({} decoded samples)", + rms, + decoded.len() + ); + assert!(rms > 50.0, "dba self-decoded RMS too low: {rms}"); + + // (d): ffmpeg cross-decode (skips when ffmpeg unavailable). + let mut ac3_bytes: Vec = Vec::new(); + for p in &pkts { + ac3_bytes.extend_from_slice(&p.data); + } + let in_path = std::env::temp_dir().join("oxideav_ac3_dba_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_dba_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args(["-f", "s16le", "-acodec", "pcm_s16le", "-ac", "2"]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping dba cross-decode gate"); + return; + }; + assert!( + status.success(), + "ffmpeg failed to decode our dba-bearing AC-3 output" + ); + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + assert!( + decoded_bytes.len() > 1000, + "ffmpeg dba-stream decode too short: {} bytes", + decoded_bytes.len() + ); + let dsq: f64 = decoded_bytes + .chunks_exact(4) + .map(|c| { + let l = i16::from_le_bytes([c[0], c[1]]) as f64; + l * l + }) + .sum(); + let drms = (dsq / (decoded_bytes.len() / 4) as f64).sqrt(); + eprintln!( + "ffmpeg decode of our dba output: {} bytes, RMS {:.1}", + decoded_bytes.len(), + drms + ); + assert!(drms > 200.0, "ffmpeg-decoded dba RMS too low: {drms}"); + } + + // ----------------------------------------------------------------- + // Round-19 — multichannel encode (mono, 3/0, 3/2, 5.1). + // ----------------------------------------------------------------- + + /// Helper: build N seconds of an N-channel S16 PCM buffer where + /// channel `c` carries a sine at `base_hz * (c + 1)` for fbw + /// channels and a fixed sub-bass tone (~80 Hz) when `c` is the + /// LFE slot in 5.1 layout. The LFE channel in AC-3 is band- + /// limited to ~656 Hz (end_mant=7) so high-frequency tones get + /// filtered out; an 80 Hz fundamental survives cleanly. + fn build_multichan_pcm(channels: u16, sr: u32, dur_s: f32, base_hz: f32) -> (usize, Vec) { + let nsamp = (sr as f32 * dur_s) as usize; + let mut pcm = vec![0i16; nsamp * channels as usize]; + // For 6-channel input the last slot is LFE. + let lfe_idx: Option = if channels == 6 { Some(5) } else { None }; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + for c in 0..channels as usize { + let freq = if Some(c) == lfe_idx { + 80.0 // sub-bass — well inside the LFE bandwidth + } else { + // Per-channel tone — distinct frequencies so a per- + // channel decode test can spot mis-routing. + base_hz * (c as f32 + 1.0) + }; + let s = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.30; + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * channels as usize + c] = q; + } + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + (nsamp, bytes) + } + + /// Common roundtrip+verify routine. Encodes the supplied PCM, + /// parses the BSI of every produced syncframe to assert it carries + /// the expected acmod / lfeon, then self-decodes and asserts the + /// per-channel RMS is non-zero (i.e. each fbw channel's tone made + /// it through the codec). + fn encode_decode_multichan(channels: u16, expected_acmod: u8, expected_lfeon: bool) { + let sr = 48_000u32; + let dur = 0.5f32; + let (nsamp, bytes) = build_multichan_pcm(channels, sr, dur, 220.0); + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 10, "got only {} packets", pkts.len()); + + // Verify BSI on every frame matches the expected layout. + for p in &pkts { + assert_eq!(p.data[0], 0x0B); + assert_eq!(p.data[1], 0x77); + let bsi = crate::bsi::parse(&p.data[5..]).expect("bsi"); + assert_eq!(bsi.acmod, expected_acmod); + assert_eq!(bsi.lfeon, expected_lfeon); + } + + // Self-decode and check per-channel RMS. The decoder always + // emits in source layout (passthrough — we don't request a + // downmix), so each channel's tone shows up in its slot. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(2) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + let nch = channels as usize; + let frames = decoded.len() / nch; + // Skip the first 768 samples to let overlap-add prime. + let skip = 768usize.min(frames); + let usable = frames.saturating_sub(skip); + assert!(usable > sr as usize / 4, "decoded too few samples"); + // Per-channel RMS — every channel must carry signal. (LFE is + // the exception: its band-limited 220 Hz fundamental might or + // might not survive depending on its low-pass shape, but on + // the test signal — fundamental at 220 Hz × 6 = 1320 Hz for + // ch5 in 6ch — the LFE channel actually receives the *first* + // tone (220 Hz) which sits inside the LFE band-limit.) + let mut per_ch_rms = vec![0.0f64; nch]; + for ch in 0..nch { + let sq: f64 = (skip..frames) + .map(|n| decoded[n * nch + ch] as f64) + .map(|s| s * s) + .sum(); + per_ch_rms[ch] = (sq / usable as f64).sqrt(); + } + eprintln!( + "{}-channel self-decode per-ch RMS: {:?}", + channels, per_ch_rms + ); + for (ch, rms) in per_ch_rms.iter().enumerate() { + assert!(*rms > 50.0, "channel {ch} RMS too low: {rms}"); + } + } + + /// Round-19: 1/0 mono encode + self-decode roundtrip. + #[test] + fn mono_self_decode_roundtrip() { + encode_decode_multichan(1, 1, false); + } + + /// Round-19: 3/0 (L,C,R) encode + self-decode roundtrip. + #[test] + fn three_zero_self_decode_roundtrip() { + encode_decode_multichan(3, 3, false); + } + + /// Round-19: 3/2 (L,C,R,Ls,Rs) encode + self-decode roundtrip. + #[test] + fn three_two_self_decode_roundtrip() { + encode_decode_multichan(5, 7, false); + } + + /// Round-19: 5.1 (L,C,R,Ls,Rs + LFE) encode + self-decode + /// roundtrip. This is the canonical Dolby Digital home-theatre + /// layout — the new headline capability of the encoder. + #[test] + fn five_one_self_decode_roundtrip() { + encode_decode_multichan(6, 7, true); + } + + /// Round-91: 2/2 (L,R,Ls,Rs) — ATSC A/52 Table 5.8 acmod=6 with + /// 4 fbw channels and no LFE. Mirrors the §5.3.3 channel-map for + /// the "quad surround" mode, exercising the encoder's `channels=4` + /// arm in `make_encoder` (the only multichannel layout previously + /// without a self-decode roundtrip test). + /// + /// Each fbw channel carries a distinct sine (220/440/660/880 Hz); + /// the per-channel RMS check inside `encode_decode_multichan` + /// confirms every slot routes through the encoder + decoder + /// without crosstalk-collapsing into a single mid channel. + #[test] + fn two_two_self_decode_roundtrip() { + encode_decode_multichan(4, 6, false); + } + + /// Round-91: tightened PSNR-per-channel gate on the 5.0 (3/2) + /// 5-fbw-channel encode path (acmod=7, lfeon=0). The pre-existing + /// `three_two_self_decode_roundtrip` only checked + /// `per_ch_rms > 50` — a very loose bound that says nothing about + /// quantisation fidelity once each tone makes it through the + /// codec. This new test computes per-channel lag-aligned PSNR + /// against the source PCM and asserts every fbw slot exceeds a + /// minimum-acceptable PSNR for the 384 kbps default bit budget. + /// + /// Floor is 10 dB — this matches the in-tree + /// `tests/eac3_ffmpeg.rs::psnr_min` convention where 18 dB is the + /// quoted AC-3 baseline on pure-sine input through the validator + /// binary's decoder. Self-decode tends to score a few dB lower + /// than the validator's smoothing-aware path, so 10 dB is the + /// headline floor. + #[test] + fn three_two_psnr_per_channel() { + encode_decode_multichan_psnr(5, 7, false, &[10.0f64; 5]); + } + + /// Round-91: tightened PSNR-per-channel gate on the 5.1 (3/2 + + /// LFE) acmod=7 + lfeon=1 path. fbw channels carry the same + /// 220/440/660/880/1100 Hz sweep; LFE carries an 80 Hz tone + /// (within `LFE_END_MANT=7` band-limit). PSNR threshold on LFE + /// matches fbw — the encoder runs the same bap pipeline on LFE + /// bins 0..7 so quantisation noise on the LFE tone tracks the + /// fbw shape. + #[test] + fn five_one_psnr_per_channel() { + encode_decode_multichan_psnr(6, 7, true, &[10.0f64; 6]); + } + + /// Round-91: PSNR-per-channel gate on the 2/2 (L,R,Ls,Rs) 4-fbw + /// acmod=6 encode path. Mirrors the 5.0 floor (10 dB) since the + /// per-channel bit budget at the 320 kbps default is similar. + #[test] + fn two_two_psnr_per_channel() { + encode_decode_multichan_psnr(4, 6, false, &[10.0f64; 4]); + } + + /// Round-95: `tune_snroffst_with_plan` per-channel fsnroffst + /// fairness regression. Synthetic exponents simulate a + /// 5-channel scenario where channel 0 has near-zero PSD (so its + /// per-bump mantissa cost is tiny) and channels 1..4 carry + /// dense HF energy (large per-bump cost). The r91 round-robin + /// would let ch=0 reach `fsnroffst_ch=15` while ch=1..4 stayed + /// at the global baseline; r95's equalise + spread-cap two-stage + /// greedy keeps the per-channel spread ≤ 2 + cheap-bump residual. + /// + /// The test asserts the **maximum** fsnroffst_ch minus the + /// **minimum** fsnroffst_ch is bounded — without the fix the + /// spread reached 15 on this input shape. + #[test] + fn tune_snroffst_per_channel_spread_bounded() { + use crate::audblk::{BLOCKS_PER_FRAME, N_COEFFS}; + let nchan = 5usize; + let end = 253usize; + // Build per-channel exponent grids. ch=0: all-quiet (exp=24 + // means PSD ~ 0). ch=1..4: HF-rich (low exps in upper bins + // = high PSD across the band, so each fsnr bump moves many + // bap-bins from bap=0 to bap=1+, costing real mantissa + // bits). + let mut exps: Vec> = vec![ + vec![[24u8; N_COEFFS]; BLOCKS_PER_FRAME]; + nchan + 2 /* +cpl +lfe slots */ + ]; + for ch in 1..nchan { + for blk in 0..BLOCKS_PER_FRAME { + for bin in 0..end { + // Low exps in upper half = strong HF energy. + exps[ch][blk][bin] = if bin < end / 2 { 8 } else { 4 }; + } + } + } + // Couple-pseudo and LFE slots are also populated to keep + // recompute_used valid (the cpl/lfe paths are gated off by + // CouplingPlan::in_use=false and lfeon=false below). + let ba = BitAllocParams { + sdcycod: 2, + fdcycod: 1, + sgaincod: 1, + dbpbcod: 2, + floorcod: 4, + csnroffst: 32, + fsnroffst: 0, + fsnroffst_ch: [0u8; MAX_FBW], + cplfsnroffst: 0, + lfefsnroffst: 0, + fgaincod: 4, + cplfgaincod: 4, + lfefgaincod: 4, + }; + let cpl = CouplingPlan::default(); + let dba = DbaPlan::default(); + let exp_strategies = [1u8; BLOCKS_PER_FRAME]; + // Pick a frame_bytes that yields a tight budget. 384 kbps + // → ~1536 B / frame at 48 kHz. + let frame_bytes = 1536usize; + let fscod = 0u8; + let tuned = tune_snroffst_with_plan( + &ba, + &exps, + end, + nchan, + fscod, + frame_bytes, + &exp_strategies, + None, + &cpl, + &dba, + 7, // acmod + false, // lfeon + ); + let min_v = (0..nchan).map(|c| tuned.fsnroffst_ch[c]).min().unwrap(); + let max_v = (0..nchan).map(|c| tuned.fsnroffst_ch[c]).max().unwrap(); + let spread = max_v - min_v; + // Without r95 the spread reaches 14-15 on this input + // (cheap-bump ch=0 runs away to 15 while expensive-bump + // ch=1..4 stay at the global baseline). r95 bounds the + // spread to FAIR_SPREAD + at-most-one-residual = 3. + assert!( + spread <= 3, + "fsnroffst_ch spread {} > 3 (values {:?}); the r95 fairness cap regressed", + spread, + &tuned.fsnroffst_ch[..nchan] + ); + } + + /// Round-91 helper — encode + self-decode + measure per-channel + /// PSNR against the source PCM with cross-correlation lag + /// alignment (mirrors the lag search in + /// `tests/eac3_ffmpeg.rs::psnr_min`). + /// + /// `min_psnr_db[ch]` is the per-channel floor (each channel + /// asserted independently so a single weak slot surfaces + /// directly in the failure message). The lag search covers the + /// usual ±768-sample MDCT overlap-add window. + fn encode_decode_multichan_psnr( + channels: u16, + expected_acmod: u8, + expected_lfeon: bool, + min_psnr_db: &[f64], + ) { + let sr = 48_000u32; + let dur = 0.5f32; + let (nsamp, bytes) = build_multichan_pcm(channels, sr, dur, 220.0); + // Reconstruct the source PCM as f32 / i16 for the PSNR baseline. + let mut src_i16: Vec = Vec::with_capacity(nsamp * channels as usize); + for c in bytes.chunks_exact(2) { + src_i16.push(i16::from_le_bytes([c[0], c[1]])); + } + + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + for p in &pkts { + let bsi = crate::bsi::parse(&p.data[5..]).expect("bsi"); + assert_eq!(bsi.acmod, expected_acmod); + assert_eq!(bsi.lfeon, expected_lfeon); + } + + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(2) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + let nch = channels as usize; + let frames = decoded.len() / nch; + let skip = 2048usize.min(frames.saturating_sub(1)); + let lag_corr_n = 1024usize; + assert!( + skip + lag_corr_n < frames && skip + lag_corr_n < nsamp, + "not enough decoded/source samples (skip={skip}, frames={frames}, nsamp={nsamp})" + ); + // Per-channel lag search. Each test channel carries a + // distinct sine frequency (220 Hz × (c + 1) for fbw, 80 Hz for + // LFE) so a single global lag picked off channel 0's + // cross-correlation aliases the other channels to the nearest + // phase-multiple of their own fundamental. Searching + // independently per channel keeps every slot's PSNR honest. + let max_lag: usize = 2048; + let mut best_lag = vec![0usize; nch]; + let mut per_ch_psnr = vec![0.0f64; nch]; + for ch in 0..nch { + let mut best_err = f64::INFINITY; + let mut lag_for_ch: usize = 0; + for lag in 0..=max_lag { + if skip + lag + lag_corr_n > frames { + continue; + } + let mut err = 0.0f64; + for i in 0..lag_corr_n { + let s = src_i16[(skip + i) * nch + ch] as f64; + let d = decoded[(skip + lag + i) * nch + ch] as f64; + err += (s - d) * (s - d); + } + if err < best_err { + best_err = err; + lag_for_ch = lag; + } + } + best_lag[ch] = lag_for_ch; + let usable = nsamp + .saturating_sub(skip) + .min(frames.saturating_sub(skip + lag_for_ch)); + let mut sq_err = 0.0f64; + for i in 0..usable { + let s = src_i16[(skip + i) * nch + ch] as f64; + let d = decoded[(skip + lag_for_ch + i) * nch + ch] as f64; + sq_err += (s - d) * (s - d); + } + let mse = sq_err / usable as f64; + let peak: f64 = 32767.0; + per_ch_psnr[ch] = if mse > 0.0 { + 10.0 * (peak * peak / mse).log10() + } else { + f64::INFINITY + }; + } + eprintln!( + "ch={} per-ch lag={:?} PSNR (dB)={:?}", + channels, best_lag, per_ch_psnr + ); + for (ch, psnr) in per_ch_psnr.iter().enumerate() { + let floor = min_psnr_db[ch]; + assert!( + *psnr >= floor, + "channel {ch} PSNR {:.2} dB below floor {:.2} dB (lag={})", + psnr, + floor, + best_lag[ch] + ); + } + } + + /// Round-78: 2.1 (L, R, LFE) encode + self-decode roundtrip. + /// Exercises the new `channel_layout=Stereo21` path that maps a + /// 3-channel input to acmod=2 + lfeon=1 instead of the default + /// 3-channel acmod=3 (3/0 L,C,R). The bitstream-side LFE slot + /// reuses the same per-channel exponent / mantissa pipeline as 5.1; + /// nothing else changes — both the BSI emit and the audblk loop + /// already gate LFE on `self.lfeon` alone, not on `self.acmod`. + #[test] + fn two_one_lfe_self_decode_roundtrip() { + let sr = 48_000u32; + let channels: u16 = 3; + let dur = 0.5f32; + // Custom PCM builder so slot 2 carries an 80 Hz tone (inside + // the LFE band-limit) instead of build_multichan_pcm's default + // 3 × 220 Hz = 660 Hz which sits above LFE's ~656 Hz upper + // edge (LFE_END_MANT=7). + let nsamp = (sr as f32 * dur) as usize; + let mut pcm = vec![0i16; nsamp * channels as usize]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + // Slot 0 = L (220 Hz), 1 = R (440 Hz), 2 = LFE (80 Hz). + for (c, freq) in [220.0f32, 440.0, 80.0].iter().enumerate() { + let s = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.30; + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * channels as usize + c] = q; + } + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + params.channel_layout = Some(ChannelLayout::Stereo21); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(pkts.len() >= 10, "got only {} packets", pkts.len()); + + // Every syncframe must carry acmod=2 (2/0) + lfeon=1 — the + // distinguishing wire-level marker of the new 2.1 path. + for p in &pkts { + assert_eq!(p.data[0], 0x0B); + assert_eq!(p.data[1], 0x77); + let bsi = crate::bsi::parse(&p.data[5..]).expect("bsi"); + assert_eq!(bsi.acmod, 2, "expected acmod=2 (2/0 stereo)"); + assert!(bsi.lfeon, "expected lfeon=1 (2.1 carries LFE)"); + assert_eq!(bsi.nchans, 3, "expected nchans=3 (2 fbw + LFE)"); + // §5.4.2.6 dsurmod is present only when acmod == 2 — make + // sure the BSI parser saw a syntactically valid 2.1 header + // (a wrongly-emitted phsflginu or missing dsurmod would + // mis-align the bit cursor and bsi parse would either fail + // or land on garbage values). + assert_ne!(bsi.dsurmod, 0xFF, "dsurmod absent for acmod=2"); + } + + // Self-decode and check per-channel signal survives. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(2) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + let nch = channels as usize; + let frames = decoded.len() / nch; + let skip = 768usize.min(frames); + let usable = frames.saturating_sub(skip); + assert!(usable > sr as usize / 4, "decoded too few samples"); + + // Per-channel RMS — every channel must carry signal. Decoder + // emits in WAV-mask order; for acmod=2+lfeon=1 the mask order + // is (FL, FR, LFE) which already matches the bitstream order, + // so wave_order::reorder_s16le_in_place is a no-op here. + let mut per_ch_rms = vec![0.0f64; nch]; + for ch in 0..nch { + let sq: f64 = (skip..frames) + .map(|n| decoded[n * nch + ch] as f64) + .map(|s| s * s) + .sum(); + per_ch_rms[ch] = (sq / usable as f64).sqrt(); + } + eprintln!("2.1 self-decode per-ch RMS: {:?}", per_ch_rms); + for (ch, rms) in per_ch_rms.iter().enumerate() { + assert!(*rms > 50.0, "channel {ch} RMS too low: {rms}"); + } + } + + /// Round-78: ffmpeg cross-decode of a 2.1 (acmod=2 + lfeon=1) + /// stream. Encodes an (L, R, LFE) signal and pipes the syncframes + /// through ffmpeg's AC-3 decoder. ffmpeg refuses to decode a + /// malformed BSI (e.g. emitting `phsflginu` outside acmod==2 would + /// shift every subsequent block-0 field by 1 bit and trigger + /// "frame sync error" / "cplcoe" mismatch). A clean exit + non-zero + /// per-channel RMS proves the wire-level conformance of the new + /// 2.1 emit path. + /// + /// Skips when ffmpeg is missing. + #[test] + fn two_one_lfe_ffmpeg_crossdecode() { + use std::process::Command; + let sr = 48_000u32; + let channels: u16 = 3; + let dur = 0.5f32; + let nsamp = (sr as f32 * dur) as usize; + let mut pcm = vec![0i16; nsamp * channels as usize]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + for (c, freq) in [220.0f32, 440.0, 80.0].iter().enumerate() { + let s = (2.0 * std::f32::consts::PI * freq * t).sin() * 0.30; + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * channels as usize + c] = q; + } + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + params.channel_layout = Some(ChannelLayout::Stereo21); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + enc.send_frame(&Frame::Audio(AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + })) + .unwrap(); + let _ = enc.flush(); + let mut ac3_bytes: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => ac3_bytes.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + let in_path = std::env::temp_dir().join("oxideav_ac3_21_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_21_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args([ + "-f", + "s16le", + "-acodec", + "pcm_s16le", + "-ac", + "3", + "-ar", + "48000", + ]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping 2.1 cross-decode gate"); + return; + }; + if !status.success() { + panic!("ffmpeg failed to decode our 2.1 AC-3 output"); + } + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + let nch = channels as usize; + let total = decoded_bytes.len() / 2; + let frames = total / nch; + assert!(frames > sr as usize / 4, "ffmpeg decoded too few samples"); + let skip = 768usize.min(frames); + let usable = frames.saturating_sub(skip); + let samples: Vec = decoded_bytes + .chunks_exact(2) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(); + let mut per_ch_rms = vec![0.0f64; nch]; + for ch in 0..nch { + let sq: f64 = (skip..frames) + .map(|n| samples[n * nch + ch] as f64) + .map(|s| s * s) + .sum(); + per_ch_rms[ch] = (sq / usable as f64).sqrt(); + } + eprintln!("ffmpeg 2.1 decode per-ch RMS: {:?}", per_ch_rms); + for (ch, rms) in per_ch_rms.iter().enumerate() { + assert!( + *rms > 50.0, + "ffmpeg-decoded ch{ch} RMS too low: {rms} — channel was lost", + ); + } + } + + /// Round-19: ffmpeg cross-decode of a 5.1 stream. Encodes a 5.1 + /// signal with a unique tone per channel, writes the syncframes to + /// disk, and pipes the file through ffmpeg's AC-3 decoder. The + /// produced PCM is compared per-channel against the original — a + /// mis-aligned channel (e.g. encoder swapping the surround pair) + /// would show up as one channel's tone being dropped. + /// + /// Skips when ffmpeg is missing. + #[test] + fn five_one_ffmpeg_crossdecode() { + use std::process::Command; + let sr = 48_000u32; + let channels = 6u16; + let (nsamp, bytes) = build_multichan_pcm(channels, sr, 0.5, 220.0); + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + enc.send_frame(&Frame::Audio(AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + })) + .unwrap(); + let _ = enc.flush(); + let mut ac3_bytes: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => ac3_bytes.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + let in_path = std::env::temp_dir().join("oxideav_ac3_51_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_51_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args([ + "-f", + "s16le", + "-acodec", + "pcm_s16le", + "-ac", + "6", + "-ar", + "48000", + ]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping 5.1 cross-decode gate"); + return; + }; + if !status.success() { + panic!("ffmpeg failed to decode our 5.1 AC-3 output"); + } + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + // Per-channel RMS diagnostic. The validator binary may apply + // a different channel reorder (its native AC-3 order is + // L,R,C,LFE,Ls,Rs when decoding to PCM), so we just + // sanity-check that every channel in the PCM stream carries + // signal energy — proof + // that the encoder didn't drop any of the 5.1 inputs. + let nch = channels as usize; + let total = decoded_bytes.len() / 2; + let frames = total / nch; + assert!(frames > sr as usize / 4, "ffmpeg decoded too few samples"); + let skip = 768usize.min(frames); + let usable = frames.saturating_sub(skip); + let samples: Vec = decoded_bytes + .chunks_exact(2) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(); + let mut per_ch_rms = vec![0.0f64; nch]; + for ch in 0..nch { + let sq: f64 = (skip..frames) + .map(|n| samples[n * nch + ch] as f64) + .map(|s| s * s) + .sum(); + per_ch_rms[ch] = (sq / usable as f64).sqrt(); + } + eprintln!("ffmpeg 5.1 decode per-ch RMS: {:?}", per_ch_rms); + for (ch, rms) in per_ch_rms.iter().enumerate() { + assert!( + *rms > 50.0, + "ffmpeg-decoded ch{ch} RMS too low: {rms} — channel was lost", + ); + } + + // ---------- Per-channel PSNR vs reference ---------- + // + // Our source layout: L, C, R, Ls, Rs, LFE (per A/52 §5.4.2.3 + // acmod=7 + LFE). + // ffmpeg's PCM out: L, R, C, LFE, Ls, Rs (Microsoft / WAVEEX + // channel order, which + // ffmpeg uses by default + // for raw s16le output). + // Pair source ch ↔ ffmpeg-PCM ch via the table below, then + // compute per-channel PSNR with a small lag search to absorb + // the encoder's overlap-add prime (~256 samples of latency + // on the first frame). + let src_to_ffmpeg = [0usize, 2, 1, 4, 5, 3]; + let (_nsamp_src, src_bytes) = build_multichan_pcm(channels, sr, 0.5, 220.0); + let src_samples: Vec = src_bytes + .chunks_exact(2) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(); + for src_ch in 0..nch { + let ff_ch = src_to_ffmpeg[src_ch]; + let mut best_sse = f64::INFINITY; + let mut best_lag = 0i32; + for lag in -512i32..=512 { + let mut sse = 0.0f64; + let mut count = 0usize; + for i in 0..usable { + let src_n = (skip + i) as i32 + lag; + if src_n < 0 || (src_n as usize) >= src_samples.len() / nch { + continue; + } + let dec = samples[(skip + i) * nch + ff_ch] as f64; + let orig = src_samples[src_n as usize * nch + src_ch] as f64; + let d = dec - orig; + sse += d * d; + count += 1; + } + if count > 0 { + let mse = sse / count as f64; + if mse < best_sse { + best_sse = mse; + best_lag = lag; + } + } + } + let psnr = if best_sse > 0.0 { + 10.0 * (32767.0f64.powi(2) / best_sse).log10() + } else { + f64::INFINITY + }; + eprintln!( + "ffmpeg 5.1 src_ch={src_ch} (ffmpeg ch={ff_ch}) PSNR={psnr:.2} dB lag={best_lag}", + ); + // PSNR floor — chosen well below typical 5.1 / 448 kbps + // performance (~25 dB for tonal stationary signals) so + // the gate doesn't trip on small lag-search residue. + // The measured per-channel numbers are printed in the + // log above for capacity diagnosis. A real failure + // (e.g. one channel encoded as silence or routed to the + // wrong slot) drops PSNR below 5 dB. + assert!( + psnr > 10.0, + "ffmpeg src_ch{src_ch} PSNR {psnr:.2} dB below 10 dB floor", + ); + } + } + + /// Build a 5.1 PCM source whose every fbw channel carries HF tones + /// well INSIDE the coupling region (cplbegf=8 → bin 133 ≈ 6.2 kHz + /// at 48 kHz). Each channel gets a distinct tone in 7-15 kHz so the + /// coupling channel must actually carry per-channel-distinguishable + /// energy and the per-channel cplco coordinates become load-bearing. + /// LFE is left at 80 Hz (bandlimited). + fn build_multichan_hf_pcm(channels: u16, sr: u32, dur_s: f32) -> (usize, Vec) { + let nsamp = (sr as f32 * dur_s) as usize; + let mut pcm = vec![0i16; nsamp * channels as usize]; + let lfe_idx: Option = if channels == 6 { Some(5) } else { None }; + // The channel-coupling tool (§7.4) shares a single high-band + // coefficient set across the coupled fbw channels, recovering + // each channel only through a per-band amplitude coordinate. It + // therefore wins precisely when the high band is *correlated* + // across channels (a common cue panned by level), and it cannot + // represent channels whose HF energy sits at *distinct* + // frequencies — that is the pathological case for coupling, not + // its strength. + // + // So the fixture is built HF-correlated: every fbw channel + // carries the SAME high-band tone complex (8/11/14 kHz, all + // above the cplbegf=8 ⇒ ~6.2 kHz boundary), scaled by a + // per-channel level so the coupling coordinates have a non- + // trivial amplitude envelope to encode. Channel identity lives + // entirely in a distinct *low*-frequency tone per channel (below + // the coupling boundary), which both the coupling-on and + // coupling-off paths reproduce identically. With coupling active + // the five correlated HF mantissa sets collapse into one shared + // cpl pseudo-channel, freeing budget that `tune_snroffst` spends + // on lifting the mantissa SNR. + let hf_tones = [8000.0f32, 11_000.0, 14_000.0]; + let lf_tones = [220.0f32, 330.0, 440.0, 550.0, 660.0]; + // Per-channel HF level (correlated content, level-panned). + let hf_levels = [0.30f32, 0.26, 0.22, 0.18, 0.14]; + for n in 0..nsamp { + let t = n as f32 / sr as f32; + for c in 0..channels as usize { + if Some(c) == lfe_idx { + let s = (2.0 * std::f32::consts::PI * 80.0 * t).sin() * 0.30; + pcm[n * channels as usize + c] = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + continue; + } + let ci = c.min(lf_tones.len() - 1); + // Shared correlated HF complex (same phase across + // channels), level-panned per channel. + let mut hf = 0.0f32; + for &f in &hf_tones { + hf += (2.0 * std::f32::consts::PI * f * t).sin(); + } + hf *= hf_levels[ci] / hf_tones.len() as f32; + // Distinct per-channel LF identity tone below the cpl + // boundary. + let lf = (2.0 * std::f32::consts::PI * lf_tones[ci] * t).sin() * 0.18; + let s = (hf + lf).clamp(-1.0, 1.0); + pcm[n * channels as usize + c] = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + } + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + (nsamp, bytes) + } + + /// Round-25 (task #155): demonstrate the bit-savings of multichan + /// coupling on HF-rich content. Encode a 5.1 fixture twice — once + /// with all 5 fbw channels coupled (the default), and once with + /// `AC3_DISABLE_CPL` set so coupling is suppressed — at the same + /// nominal bitrate, and verifies the coupling tool engages + /// end-to-end without regressing the decode. + /// + /// **What this gates.** §7.4 channel coupling shares a single + /// high-band coefficient set across the coupled fbw channels, + /// recovering each channel through a per-band amplitude coordinate. + /// The fixture is therefore built HF-*correlated* (the same 8/11/14 + /// kHz complex in every fbw channel, level-panned; see + /// [`build_multichan_hf_pcm`]) so the coupling channel has genuinely + /// shared content to carry, with channel identity living in a + /// distinct *low*-frequency tone below the cplbegf=8 (~6.2 kHz) + /// boundary. + /// + /// The earlier form of this test asserted a ≥ 1 dB self-decode PSNR + /// *win* for coupling-on over coupling-off. That premise does not + /// hold for this in-tree encoder: at a constrained 5.1 budget both + /// paths land on the same ~10-12 dB floor (the per-channel PSNR gate + /// `five_one_psnr_per_channel` likewise sits at a 10 dB floor), so + /// the ±0.1 dB difference between the two paths is below the + /// estimation noise and the win is not robust. The test was + /// suspended on that fragile assertion. It is re-armed here on the + /// deterministic invariants that actually characterise the tool: + /// + /// 1. coupling-on must *engage* — at least one block emits + /// `cplinu = 1` in the bitstream side-info; + /// 2. coupling-off (`AC3_DISABLE_CPL`) must emit `cplinu = 0` in + /// every block; + /// 3. both paths produce a *valid, non-degraded* self-decode at or + /// above the same per-channel floor the rest of the 5.1 suite + /// uses — i.e. coupling does not *regress* the decode; + /// 4. at matched `frmsizecod` the two bitstreams are the same size. + /// + /// Still `#[ignore]`d because it mutates the process-wide + /// `AC3_DISABLE_CPL` env var and would race a concurrently-running + /// encode; run alone with `cargo test -- --ignored + /// five_one_coupling_beats_no_coupling_at_low_bitrate`. + #[test] + #[ignore = "mutates AC3_DISABLE_CPL — must run alone (cargo test -- --ignored)"] + fn five_one_coupling_beats_no_coupling_at_low_bitrate() { + // 320 kbps for 5.1 — below the 448 kbps default, the budget at + // which coupling is most relevant. + let kbps = 320u64; + let sr = 48_000u32; + let channels = 6u16; + let dur = 0.25f32; + let (nsamp, bytes) = build_multichan_hf_pcm(channels, sr, dur); + + // Returns (avg-fbw PSNR, bitstream byte count, count of blocks + // whose side-info carried `cplinu == 1`, total blocks). + let encode_and_psnr = |disable_cpl: bool| -> (f64, usize, usize, usize) { + // Switch the env knob inside this closure; restore on exit. + // The encoder reads `AC3_DISABLE_CPL` per frame so setting + // it before the encode loop is sufficient. + if disable_cpl { + std::env::set_var("AC3_DISABLE_CPL", "1"); + } else { + std::env::remove_var("AC3_DISABLE_CPL"); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(kbps * 1000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + enc.send_frame(&Frame::Audio(AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + })) + .unwrap(); + let _ = enc.flush(); + let mut bitstream: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => bitstream.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + // Count blocks whose side-info carried `cplinu == 1`. The + // §5.4.3.8 flag is sticky across `cplstre == 0` blocks, so we + // thread the last-seen value through the frame walk exactly + // as the parser does. + let mut cpl_blocks = 0usize; + let mut total_blocks = 0usize; + { + let mut off = 0usize; + while off < bitstream.len() { + let si = crate::syncinfo::parse(&bitstream[off..]) + .expect("syncinfo parse on our own output"); + let flen = si.frame_length as usize; + let b = crate::bsi::parse(&bitstream[off + 5..]).expect("bsi parse"); + let frame = &bitstream[off..off + flen]; + let side = crate::audblk::parse_frame_side_info(&si, &b, frame) + .expect("side-info parse"); + let mut cur_cplinu = false; + for s in side.iter() { + if s.cplstre { + cur_cplinu = s.cplinu; + } + if cur_cplinu { + cpl_blocks += 1; + } + total_blocks += 1; + } + off += flen; + } + } + // Self-decode (round-trip): the most stable comparison since + // both runs share the same DSP path. PSNR vs the original PCM + // on every fbw channel. + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + let mut offset = 0usize; + while offset < bitstream.len() { + let si = crate::syncinfo::parse(&bitstream[offset..]) + .expect("syncinfo parse on our own output"); + let flen = si.frame_length as usize; + let pkt = oxideav_core::Packet::new( + 0, + oxideav_core::TimeBase::new(1, sr as i64), + bitstream[offset..offset + flen].to_vec(), + ); + dec.send_packet(&pkt).unwrap(); + if let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(2) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + offset += flen; + } + let nch = channels as usize; + let frames_decoded = decoded.len() / nch; + // Skip overlap-add prime + LFE channel (PSNR is misleading + // there). Compute average per-channel PSNR over the 5 fbw + // channels. + let skip = 768usize.min(frames_decoded); + let usable = frames_decoded.saturating_sub(skip); + let src_samples: Vec = bytes + .chunks_exact(2) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(); + let mut total_psnr = 0.0f64; + let fbw_count = 5usize; // ch 0..4 are fbw; ch 5 is LFE + for ch in 0..fbw_count { + let mut sse = 0.0f64; + let mut count = 0usize; + for i in 0..usable { + let src_n = skip + i; + if src_n >= src_samples.len() / nch { + continue; + } + let d = decoded[(skip + i) * nch + ch] as f64 + - src_samples[src_n * nch + ch] as f64; + sse += d * d; + count += 1; + } + let mse = if count > 0 { sse / count as f64 } else { 0.0 }; + let psnr = if mse > 0.0 { + 10.0 * (32767.0f64.powi(2) / mse).log10() + } else { + 100.0 + }; + total_psnr += psnr; + } + ( + total_psnr / fbw_count as f64, + bitstream.len(), + cpl_blocks, + total_blocks, + ) + }; + + let (psnr_with, bytes_with, cpl_blocks_with, total_blocks) = encode_and_psnr(false); + let (psnr_without, bytes_without, cpl_blocks_without, _) = encode_and_psnr(true); + // Restore env state. + std::env::remove_var("AC3_DISABLE_CPL"); + + eprintln!( + "5.1 @ {} kbps coupling-on : avg-fbw PSNR {:.2} dB ({} bytes, {}/{} cpl blocks)", + kbps, psnr_with, bytes_with, cpl_blocks_with, total_blocks + ); + eprintln!( + "5.1 @ {} kbps coupling-off : avg-fbw PSNR {:.2} dB ({} bytes, {}/{} cpl blocks)", + kbps, psnr_without, bytes_without, cpl_blocks_without, total_blocks + ); + + // (4) Same frmsizecod ⇒ identical bitstream sizes (matched-rate + // comparison). + assert_eq!( + bytes_with, bytes_without, + "bitrate {} kbps should yield identical bitstream sizes", + kbps + ); + // (1) Coupling-on must actually engage on this HF-correlated + // content — the §7.4 tool is only meaningful if at least one + // block emits `cplinu = 1`. + assert!( + cpl_blocks_with > 0, + "coupling-on encoded {}/{} blocks with cplinu=1 — the §7.4 \ + coupling decision never fired on HF-correlated 5.1 content", + cpl_blocks_with, + total_blocks + ); + // (2) Coupling-off must suppress the tool in every block. + assert_eq!( + cpl_blocks_without, 0, + "AC3_DISABLE_CPL still emitted {}/{} coupled blocks", + cpl_blocks_without, total_blocks + ); + // (3) Coupling must not *regress* the decode: the coupling-on + // path stays at or above the same per-channel floor the rest + // of the 5.1 suite gates on (`five_one_psnr_per_channel` + // uses 10 dB), and within estimation noise of the + // coupling-off path. The earlier ">= 1 dB win" claim was not + // robust at this constrained budget (both paths share the + // same floor); the invariant we can assert deterministically + // is no-regression. + assert!( + psnr_with >= 10.0, + "coupling-on self-decode PSNR {:.2} dB fell below the 10 dB \ + 5.1 floor — coupling reconstruction regressed", + psnr_with + ); + assert!( + psnr_with >= psnr_without - 0.5, + "coupling-on regressed the decode vs coupling-off: \ + with={:.2} dB, without={:.2} dB", + psnr_with, + psnr_without + ); + } + + // ----------------------------------------------------------------- + // Per-block snroffst tuning (round-24 / task #170) tests. + // ----------------------------------------------------------------- + + /// Build a stereo fixture where audio block 3 of a syncframe carries + /// a broadband transient and the surrounding blocks are near + /// silence. With a 256-sample block size and 6 blocks per frame the + /// transient lives in samples [256*3, 256*4) = [768, 1024). + /// Multiple syncframes are emitted so the encoder/decoder can warm + /// up before the metric window. + fn build_perblock_transient_pcm(sr: u32) -> (usize, Vec) { + // 4 syncframes of audio = 4*1536 = 6144 samples. We measure + // from the 2nd frame onwards so the decoder/encoder priming + // windows have settled. + let frames = 4usize; + let nsamp = frames * 1536; + let mut pcm = vec![0i16; nsamp * 2]; + for f in 0..frames { + let frame_off = f * 1536; + // Steady low-amplitude 880 Hz tone everywhere → blocks 0/1/2 + // have non-zero bap bins (so their fsnroffst donations + // actually save mantissa bits). Then add a HF chord burst on + // block 3 of each frame (samples 768..1024) to create the + // demand spike that should attract the redistributed bits. + for k in 0..1536 { + let n = frame_off + k; + let t = n as f32 / sr as f32; + let bg = 0.10 * (2.0 * std::f32::consts::PI * 880.0 * t).sin(); + let burst = if (768..1024).contains(&k) { + 0.50 * ((2.0 * std::f32::consts::PI * 4000.0 * t).sin() * 0.25 + + (2.0 * std::f32::consts::PI * 6000.0 * t).sin() * 0.25 + + (2.0 * std::f32::consts::PI * 8000.0 * t).sin() * 0.25 + + (2.0 * std::f32::consts::PI * 10_000.0 * t).sin() * 0.25) + } else { + 0.0 + }; + let s = (bg + burst).clamp(-1.0, 1.0); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + pcm[n * 2] = q; + pcm[n * 2 + 1] = q; + } + } + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for s in &pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + (nsamp, bytes) + } + + /// Self-decode roundtrip with per-block snroffst tuning enabled + /// (default). This is the primary spec-conformance test for the + /// round-24 work — proves a frame whose `snroffste=1` fires on + /// blocks other than block 0 still self-decodes through our own + /// AC-3 decoder. Catches bitstream sync regressions in the + /// `snroffste/csnroffst/fsnroffst` write path. + #[test] + fn perblock_snroffst_self_decode() { + let sr = 48_000u32; + let (nsamp, bytes) = build_perblock_transient_pcm(sr); + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(!pkts.is_empty(), "expected at least one packet"); + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded = 0usize; + for p in &pkts { + dec.send_packet(p).unwrap(); + while let Ok(Frame::Audio(a)) = dec.receive_frame() { + decoded += a.data[0].len() / 4; + } + } + assert!( + decoded > 0, + "decoder produced no audio from per-block-snr-tuned bitstream" + ); + } + + /// A/B PSNR on the transient-in-block-3 fixture. Encodes the same + /// PCM twice — once with per-block snroffst tuning active (#170, + /// the default) and once with `AC3_DISABLE_PERBLOCK_SNR=1` set so + /// every block reuses the global flat allocation. Both encodes use + /// the same frmsizecod (so identical bitstream byte budget). The + /// per-block-tuned run must produce equal-or-better localised + /// PSNR over block 3's sample range. + /// + /// Marked `#[ignore]` so it doesn't race with other tests over the + /// `AC3_DISABLE_PERBLOCK_SNR` env var (set/remove here would flip + /// the encoder's decision in any concurrently-running encode). + /// Run with `cargo test -- --ignored perblock_snroffst_helps_transient`. + #[test] + #[ignore = "mutates AC3_DISABLE_PERBLOCK_SNR — must run alone (cargo test -- --ignored)"] + fn perblock_snroffst_helps_transient() { + let sr = 48_000u32; + let (nsamp, bytes) = build_perblock_transient_pcm(sr); + // 96 kbps stereo — tight enough that the global tuner + // typically lands on csnr ≈ 4-6 with substantial mantissa + // hunger on the transient block, so per-block redistribution + // can move bits where they matter. At 192 kbps the budget is + // loose enough that both paths reach ceiling PSNR and the A/B + // signal vanishes. + let kbps = 96u64; + + let encode_and_psnr = |disable: bool| -> (f64, usize) { + if disable { + std::env::set_var("AC3_DISABLE_PERBLOCK_SNR", "1"); + } else { + std::env::remove_var("AC3_DISABLE_PERBLOCK_SNR"); + } + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(kbps * 1000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes.clone()], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + let mut total_bytes = 0usize; + loop { + match enc.receive_packet() { + Ok(p) => { + total_bytes += p.data.len(); + pkts.push(p); + } + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut decoded: Vec = Vec::new(); + for p in &pkts { + dec.send_packet(p).unwrap(); + while let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(4) { + decoded.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + let orig: Vec = bytes + .chunks_exact(4) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(); + let n = decoded.len().min(orig.len()); + // Lag search across the first transient (frame 1 burst at + // ~ sample 1536+896 = 2432) — the IMDCT priming offset is + // 256 samples but a frame-aligned reference decoder may + // also carry a frame of look-ahead, so we sweep ±1024. + let mut best_lag = 0i32; + let mut best_sse = f64::INFINITY; + for lag in -1024i32..=1024 { + let mut sse = 0.0f64; + let mut count = 0usize; + for i in 1536..n { + let b = i as i32 + lag; + if b < 0 || (b as usize) >= orig.len() { + continue; + } + let d = decoded[i] as f64 - orig[b as usize] as f64; + sse += d * d; + count += 1; + } + if count > 0 { + let mse = sse / count as f64; + if mse < best_sse { + best_sse = mse; + best_lag = lag; + } + } + } + // Localised PSNR over block-3 windows of frames 1..N (skip + // frame 0 priming). Block 3 of frame f sits at + // [f*1536+768, f*1536+1024). + let mut burst_sse = 0.0f64; + let mut burst_count = 0usize; + let frames = nsamp / 1536; + for f in 1..frames { + for i in (f * 1536 + 768)..(f * 1536 + 1024).min(n) { + let b = i as i32 + best_lag; + if b < 0 || (b as usize) >= orig.len() { + continue; + } + let d = decoded[i] as f64 - orig[b as usize] as f64; + burst_sse += d * d; + burst_count += 1; + } + } + let psnr = if burst_count > 0 && burst_sse > 0.0 { + let mse = burst_sse / burst_count as f64; + 10.0 * (32767.0f64.powi(2) / mse).log10() + } else { + f64::INFINITY + }; + (psnr, total_bytes) + }; + + let (psnr_with, bytes_with) = encode_and_psnr(false); + let (psnr_without, bytes_without) = encode_and_psnr(true); + std::env::remove_var("AC3_DISABLE_PERBLOCK_SNR"); + + eprintln!( + "block-3 transient @ {} kbps: per-block-tuned = {:.2} dB ({} bytes)", + kbps, psnr_with, bytes_with + ); + eprintln!( + "block-3 transient @ {} kbps: flat allocation = {:.2} dB ({} bytes)", + kbps, psnr_without, bytes_without + ); + // Same frmsizecod ⇒ same byte budget per frame. + assert_eq!( + bytes_with, bytes_without, + "per-block tuning should not change frame byte count" + ); + // Per-block tuning must be at least non-regressive on the + // demand-heavy block. We accept equality (no-op) too — for + // some seeds the demand spread is below the redistribution + // threshold and the plan stays flat by design. + assert!( + psnr_with >= psnr_without - 0.1, + "per-block snroffst tuning regressed transient PSNR: with={:.2} dB, without={:.2} dB", + psnr_with, + psnr_without + ); + } + + /// ffmpeg cross-decode of the per-block-snroffst output. Encode the + /// transient-in-block-3 fixture (which exercises the snroffste=1 + /// path on non-block-0 audio blocks) and verify ffmpeg parses it + /// cleanly. This is the spec-conformance gate for #170: a + /// production decoder we did NOT write must accept our per-block + /// snroffst stream. Skips when ffmpeg is missing. + #[test] + fn perblock_snroffst_ffmpeg_crossdecode() { + use std::process::Command; + let sr = 48_000u32; + let (nsamp, bytes) = build_perblock_transient_pcm(sr); + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(sr); + params.channels = Some(2); + params.sample_format = Some(SampleFormat::S16); + params.bit_rate = Some(192_000); + let mut enc = make_encoder(¶ms).expect("make_encoder"); + let audio = AudioFrame { + samples: nsamp as u32, + pts: Some(0), + data: vec![bytes], + }; + enc.send_frame(&Frame::Audio(audio)).unwrap(); + let _ = enc.flush(); + let mut ac3_bytes: Vec = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => ac3_bytes.extend_from_slice(&p.data), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + let in_path = std::env::temp_dir().join("oxideav_ac3_perblock_snr_enc.ac3"); + let out_path = std::env::temp_dir().join("oxideav_ac3_perblock_snr_dec.pcm"); + std::fs::write(&in_path, &ac3_bytes).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let out = Command::new("ffmpeg") + .args([ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-f", + "ac3", + "-i", + ]) + .arg(&in_path) + .args(["-f", "s16le", "-acodec", "pcm_s16le", "-ac", "2"]) + .arg(&out_path) + .status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = out else { + eprintln!("ffmpeg unavailable — skipping per-block snr cross-decode gate"); + return; + }; + if !status.success() { + panic!("ffmpeg failed to decode our per-block-snroffst AC-3 output"); + } + let Ok(decoded_bytes) = std::fs::read(&out_path) else { + panic!("ffmpeg produced no decode output"); + }; + let _ = std::fs::remove_file(&out_path); + assert!( + decoded_bytes.len() > 1000, + "ffmpeg per-block decode suspiciously short: {} bytes", + decoded_bytes.len() + ); + eprintln!( + "ffmpeg cross-decoded our per-block-snroffst stream: {} bytes", + decoded_bytes.len() + ); + } + + // ---- §5.4.2 / §5.4.3.3-4 encoder-side metadata surface ---- + + /// Interleaved stereo/multichannel test tone, `frames` syncframes + /// long, as S16LE bytes. + fn meta_tone_bytes(channels: usize, frames: usize) -> (Vec, usize) { + let n = frames * SAMPLES_PER_FRAME as usize; + let mut bytes = Vec::with_capacity(n * channels * 2); + for i in 0..n { + let t = i as f32 / 48_000.0; + for ch in 0..channels { + let s = 0.30 + * (2.0 * std::f32::consts::PI * (440.0 + 60.0 * ch as f32) * t).sin() + * (1.0 - 0.05 * ch as f32); + let q = (s * 32767.0).clamp(-32768.0, 32767.0) as i16; + bytes.extend_from_slice(&q.to_le_bytes()); + } + } + (bytes, n) + } + + /// Encode `frames` syncframes of tone with either a typed + /// [`MetadataParams`] or `CodecParameters::options` pairs. + fn meta_encode( + channels: u16, + meta: Option, + options: &[(&str, &str)], + frames: usize, + ) -> Vec { + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(48_000); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + let mut opts = oxideav_core::CodecOptions::new(); + for (k, v) in options { + opts = opts.set(*k, *v); + } + params.options = opts; + let mut enc = match meta { + Some(m) => make_encoder_with_metadata(¶ms, m).expect("typed metadata encoder"), + None => make_encoder(¶ms).expect("options encoder"), + }; + let (bytes, n) = meta_tone_bytes(channels as usize, frames); + enc.send_frame(&Frame::Audio(AudioFrame { + samples: n as u32, + pts: Some(0), + data: vec![bytes], + })) + .unwrap(); + let _ = enc.flush(); + let mut pkts = Vec::new(); + loop { + match enc.receive_packet() { + Ok(p) => pkts.push(p), + Err(Error::NeedMore) | Err(Error::Eof) => break, + Err(e) => panic!("receive_packet: {e:?}"), + } + } + assert!(!pkts.is_empty(), "no packets produced"); + pkts + } + + /// Decode packets with our own decoder, returning interleaved i16. + fn meta_decode(pkts: &[Packet]) -> Vec { + let dparams = CodecParameters::audio(CodecId::new("ac3")); + let mut dec = crate::decoder::make_decoder(&dparams).expect("make_decoder"); + let mut out = Vec::new(); + for p in pkts { + dec.send_packet(p).unwrap(); + while let Ok(Frame::Audio(a)) = dec.receive_frame() { + for s in a.data[0].chunks_exact(2) { + out.push(i16::from_le_bytes([s[0], s[1]])); + } + } + } + out + } + + /// Every §5.4.2 word configured through [`MetadataParams`] must + /// read back through the typed BSI surface on EVERY syncframe — + /// 5.1 exercises the cmixlev + surmixlev slots. + #[test] + fn metadata_bsi_words_roundtrip_51() { + let meta = MetadataParams { + dialnorm: 24, + compr: Some(0xC5), + dynrng: None, + bsmod: 2, // visually impaired service + cmixlev: 2, + surmixlev: 0, + dsurmod: 0, + langcod: Some(0xFF), + audprod: Some(AudioProductionParams { + mixlevel: 21, // 101 dB SPL peak + roomtyp: 2, // small room, flat + }), + copyrightb: true, + origbs: false, + }; + let pkts = meta_encode(6, Some(meta), &[], 4); + for p in &pkts { + let bsi = crate::bsi::parse(&p.data[5..]).expect("bsi"); + assert_eq!(bsi.dialnorm, 24); + assert_eq!(bsi.bsmod, 2); + assert_eq!(bsi.cmixlev, 2); + assert_eq!(bsi.surmixlev, 0); + assert_eq!(bsi.compr.expect("compr present").raw(), 0xC5); + assert_eq!(bsi.language_code().expect("langcod present").raw(), 0xFF); + let ap = bsi.audio_production.expect("audprod present"); + assert_eq!(ap.mixlevel, 21); + assert_eq!(ap.roomtyp.raw(), 2); + let ci = bsi.copyright_info; + assert!(ci.is_copyright_protected()); + assert!(!ci.is_original_bitstream()); + } + // The metadata-bearing stream still decodes. + let dec = meta_decode(&pkts); + assert!(!dec.is_empty()); + } + + /// 2/0 exercises the dsurmod slot (absent in every other acmod). + #[test] + fn metadata_bsi_dsurmod_stereo_roundtrip() { + let meta = MetadataParams { + dsurmod: 2, // Dolby Surround encoded + ..MetadataParams::default() + }; + let pkts = meta_encode(2, Some(meta), &[], 2); + for p in &pkts { + let bsi = crate::bsi::parse(&p.data[5..]).expect("bsi"); + assert_eq!(bsi.dsurmod, 2); + // Defaults still hold. + assert_eq!(bsi.dialnorm, 27); + assert!(bsi.compr.is_none()); + assert!(bsi.language_code().is_none()); + } + } + + /// The §5.4.3.4 dynrng word must be APPLIED by the decoder's + /// mandatory §7.7.1 line-out path: an identical tone encoded with a + /// −12.04 dB dynrng word decodes at one quarter the amplitude of + /// the word-less stream. + #[test] + fn metadata_dynrng_word_scales_decoded_output() { + let word = 0xC0u8; // X=-2, Y=0 → 2^-1 · 0.5 = 0.25 (−12.04 dB) + let expected = crate::drc::dynrng_to_linear(word) as f64; + assert!((expected - 0.25).abs() < 1e-6); + let plain = meta_decode(&meta_encode(2, Some(MetadataParams::default()), &[], 4)); + let cut = meta_decode(&meta_encode( + 2, + Some(MetadataParams { + dynrng: Some(word), + ..MetadataParams::default() + }), + &[], + 4, + )); + assert_eq!(plain.len(), cut.len()); + let rms = |v: &[i16]| -> f64 { + let n = v.len().max(1); + (v.iter().map(|&s| (s as f64) * (s as f64)).sum::() / n as f64).sqrt() + }; + // Skip the priming edge (first two blocks of the first frame). + let skip = 2 * SAMPLES_PER_BLOCK * 2; + let ratio = rms(&cut[skip..]) / rms(&plain[skip..]).max(1e-9); + let delta_db = 20.0 * (ratio / expected).log10().abs(); + assert!( + delta_db < 0.5, + "decoded dynrng gain {ratio:.4} vs expected {expected:.4} (off by {delta_db:.2} dB)" + ); + } + + /// Registry `options` and the typed constructor must build the + /// same encoder — pinned byte-identical. + #[test] + fn metadata_options_build_identical_encoder() { + let meta = MetadataParams { + dialnorm: 20, + compr: Some(0x9A), + dynrng: Some(0xC0), + bsmod: 1, + dsurmod: 1, + langcod: Some(0xFF), + audprod: Some(AudioProductionParams { + mixlevel: 15, + roomtyp: 1, + }), + copyrightb: true, + origbs: false, + ..MetadataParams::default() + }; + let typed: Vec = meta_encode(2, Some(meta), &[], 2) + .iter() + .flat_map(|p| p.data.clone()) + .collect(); + let from_options: Vec = meta_encode( + 2, + None, + &[ + ("dialnorm", "20"), + ("compr", "0x9A"), + ("dynrng", "0xC0"), + ("bsmod", "1"), + ("dsurmod", "1"), + ("langcod", "255"), + ("mixlevel", "15"), + ("roomtyp", "1"), + ("copyright", "true"), + ("origbs", "0"), + ], + 2, + ) + .iter() + .flat_map(|p| p.data.clone()) + .collect(); + assert_eq!( + typed, from_options, + "options-driven and typed metadata constructors must emit identical bytes" + ); + } + + /// Out-of-range codepoints are rejected at construction, both + /// through the typed path and the options path. + #[test] + fn metadata_validation_rejects_out_of_range() { + let mut params = CodecParameters::audio(CodecId::new("ac3")); + params.sample_rate = Some(48_000); + params.channels = Some(2); + for bad in [ + MetadataParams { + dialnorm: 0, + ..MetadataParams::default() + }, + MetadataParams { + dialnorm: 32, + ..MetadataParams::default() + }, + MetadataParams { + cmixlev: 3, + ..MetadataParams::default() + }, + MetadataParams { + surmixlev: 3, + ..MetadataParams::default() + }, + MetadataParams { + dsurmod: 3, + ..MetadataParams::default() + }, + MetadataParams { + bsmod: 8, + ..MetadataParams::default() + }, + MetadataParams { + audprod: Some(AudioProductionParams { + mixlevel: 32, + roomtyp: 0, + }), + ..MetadataParams::default() + }, + MetadataParams { + audprod: Some(AudioProductionParams { + mixlevel: 0, + roomtyp: 3, + }), + ..MetadataParams::default() + }, + ] { + assert!( + make_encoder_with_metadata(¶ms, bad).is_err(), + "expected rejection: {bad:?}" + ); + } + // Options-path parse failures. + for (k, v) in [ + ("dynrng", "0x1G"), + ("compr", "256"), + ("copyright", "maybe"), + ("dialnorm", "0"), + ] { + params.options = oxideav_core::CodecOptions::new().set(k, v); + assert!( + make_encoder(¶ms).is_err(), + "expected rejection: {k}={v}" + ); + } + } + + /// Decode an AC-3 elementary stream through the external decoder + /// binary with extra decoder args, returning the interior RMS of + /// the s16le output (priming edge skipped). `None` when the binary + /// is unavailable. + fn ffmpeg_decode_rms(stream: &[u8], tag: &str, dec_args: &[&str]) -> Option { + use std::process::Command; + let in_path = std::env::temp_dir().join(format!("oxideav_ac3_meta_{tag}.ac3")); + let out_path = std::env::temp_dir().join(format!("oxideav_ac3_meta_{tag}.pcm")); + std::fs::write(&in_path, stream).expect("write ac3"); + let _ = std::fs::remove_file(&out_path); + let mut cmd = Command::new("ffmpeg"); + cmd.args(["-y", "-hide_banner", "-loglevel", "error", "-f", "ac3"]); + cmd.args(dec_args); + cmd.arg("-i").arg(&in_path); + cmd.args(["-f", "s16le", "-acodec", "pcm_s16le"]); + cmd.arg(&out_path); + let status = cmd.status(); + let _ = std::fs::remove_file(&in_path); + let Ok(status) = status else { + eprintln!("ffmpeg unavailable — skipping metadata black-box gate"); + return None; + }; + assert!(status.success(), "ffmpeg failed to decode ({tag})"); + let bytes = std::fs::read(&out_path).expect("ffmpeg output"); + let _ = std::fs::remove_file(&out_path); + let samples: Vec = bytes + .chunks_exact(2) + .map(|c| i16::from_le_bytes([c[0], c[1]])) + .collect(); + // Skip the first frame's worth of samples (priming edge). + let skip = (SAMPLES_PER_FRAME as usize * 2).min(samples.len() / 2); + let tail = &samples[skip..]; + assert!(!tail.is_empty(), "ffmpeg decode too short ({tag})"); + Some( + (tail.iter().map(|&s| (s as f64) * (s as f64)).sum::() / tail.len() as f64).sqrt(), + ) + } + + /// Black-box semantics of the emitted §5.4.3.4 `dynrng` and + /// §5.4.2.10 `compr` words: the external decoder's DRC controls + /// must reproduce exactly the Table 7.29/7.30 gains our encoder + /// authored. `drc_scale 0` vs `drc_scale 1` isolates `dynrng`; + /// `heavy_compr` swaps in the `compr` word (§7.7.2). + #[test] + fn metadata_dynrng_compr_black_box_gain_semantics() { + let dynrng_word = 0xC0u8; // −12.04 dB + let compr_word = 0xDFu8; // X=-3, Y=15 → 2^-2·(31/32) ≈ −12.32 dB + let meta = MetadataParams { + dynrng: Some(dynrng_word), + compr: Some(compr_word), + ..MetadataParams::default() + }; + let stream: Vec = meta_encode(2, Some(meta), &[], 6) + .iter() + .flat_map(|p| p.data.clone()) + .collect(); + let Some(rms_off) = ffmpeg_decode_rms(&stream, "drc0", &["-drc_scale", "0"]) else { + return; + }; + let rms_dyn = ffmpeg_decode_rms(&stream, "drc1", &["-drc_scale", "1"]).unwrap(); + let rms_compr = + ffmpeg_decode_rms(&stream, "heavy", &["-drc_scale", "1", "-heavy_compr", "1"]).unwrap(); + assert!(rms_off > 100.0, "baseline decode suspiciously quiet"); + let dyn_gain = rms_dyn / rms_off; + let expected_dyn = crate::drc::dynrng_to_linear(dynrng_word) as f64; + let dyn_err_db = 20.0 * (dyn_gain / expected_dyn).log10().abs(); + assert!( + dyn_err_db < 0.5, + "black-box dynrng gain {dyn_gain:.4} vs authored {expected_dyn:.4} \ + (off by {dyn_err_db:.2} dB)" + ); + let compr_gain = rms_compr / rms_off; + let expected_compr = crate::drc::compr_to_linear(compr_word) as f64; + let compr_err_db = 20.0 * (compr_gain / expected_compr).log10().abs(); + assert!( + compr_err_db < 0.5, + "black-box compr gain {compr_gain:.4} vs authored {expected_compr:.4} \ + (off by {compr_err_db:.2} dB)" + ); + eprintln!( + "black-box DRC words: dynrng {dyn_gain:.4} (authored {expected_dyn:.4}, \ + Δ{dyn_err_db:.3} dB), compr {compr_gain:.4} (authored {expected_compr:.4}, \ + Δ{compr_err_db:.3} dB)" + ); + } + + /// Black-box semantics of the emitted `dialnorm` word: two streams + /// identical except for dialnorm (21 vs 31), decoded with the + /// external decoder's target-level normalisation, must differ by + /// exactly the 10 dB dialnorm delta. + #[test] + fn metadata_dialnorm_black_box_target_level() { + let enc = |dialnorm: u8| -> Vec { + meta_encode( + 2, + Some(MetadataParams { + dialnorm, + ..MetadataParams::default() + }), + &[], + 6, + ) + .iter() + .flat_map(|p| p.data.clone()) + .collect() + }; + let s21 = enc(21); + let s31 = enc(31); + let args = ["-drc_scale", "0", "-target_level", "-31"]; + let Some(rms21) = ffmpeg_decode_rms(&s21, "dn21", &args) else { + return; + }; + let rms31 = ffmpeg_decode_rms(&s31, "dn31", &args).unwrap(); + assert!(rms31 > 100.0, "dialnorm-31 decode suspiciously quiet"); + // dialnorm 31 at target −31 → 0 dB; dialnorm 21 → −10 dB. + let ratio_db = 20.0 * (rms21 / rms31).log10(); + assert!( + (ratio_db + 10.0).abs() < 0.5, + "black-box dialnorm delta {ratio_db:+.2} dB (expected −10.00 dB)" + ); + eprintln!("black-box dialnorm: authored 10 dB delta measured {ratio_db:+.3} dB"); + } +} diff --git a/crates/vendor/oxideav-ac3/src/imdct.rs b/crates/vendor/oxideav-ac3/src/imdct.rs new file mode 100644 index 00000000..e9f00425 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/imdct.rs @@ -0,0 +1,609 @@ +//! FFT-backed IMDCT for AC-3 (§7.9.4 of A/52:2018). +//! +//! AC-3 IMDCT is implemented with the textbook MDCT-to-FFT decomposition: +//! +//! 1. Pre-twiddle: combine two real bins into one complex product +//! `Z[k] = (X[N/2-2k-1] + j*X[2k]) * (xcos + j*xsin)` with the bin-dependent +//! unit-magnitude twiddle from §7.9.4.1-step-2. +//! 2. Complex IFFT of length N/4 (long block) or N/8 (short block). +//! 3. Post-twiddle: multiply each IFFT output by the same twiddle +//! `y[n] = z[n] * (xcos + j*xsin)` (§7.9.4.1-step-4). +//! 4. De-interleave the real/imag parts of y[] into the N-sample time-domain +//! buffer using the spec's step-5 permutation — but WITHOUT the window +//! multiplication, because the caller applies the window separately (to +//! keep the IMDCT gate unit-testable against the direct-form reference). +//! +//! AC-3's overlap-add scales the summed block by 2, so we pick the IMDCT +//! sign/scale to match that contract (see `SCALE_LONG` / `SCALE_SHORT` +//! constants). The resulting FFT-backed IMDCT agrees with the direct-form +//! reference in §7.9.4 to within 1e-4 per sample on arbitrary inputs. +//! +//! The FFT is a pure-Rust iterative radix-2 decimation-in-time Cooley-Tukey +//! transform specialised to the exact two lengths AC-3 uses (128 and 64). +//! No external crates, no unsafe code — the working set is ~2 kB and the +//! transform runs O(N log N). + +use std::f32::consts::PI; + +/// Complex number `a + j*b` as a (f32, f32) tuple. We avoid pulling in +/// `num-complex` to keep the dependency graph flat. +type C = (f32, f32); + +/// Long-block IMDCT scale. The direct-form reference in `audblk::imdct_512` +/// uses `scale = -1.0` to match the encoder's `-2/N` forward MDCT; the +/// FFT path lands on the same polarity because the pre- and post-twiddles +/// each contribute a `-` sign, and the IFFT bin 0 carries the DC sum +/// without normalisation (we DO NOT divide by N — AC-3's overlap-add has +/// its own factor-of-2 scale, matched here by not normalising at all). +const SCALE_LONG: f32 = 1.0; + +/// Short-block IMDCT scale. Same argument as the long case; the §7.9.4.2 +/// decomposition is structurally identical to the long one, just run at +/// half the length on each half of the interleaved input. +const SCALE_SHORT: f32 = 1.0; + +/// Cached pre/post twiddle constants for the N=512 long block. +/// `xcos1[k] = -cos(2π*(8k+1)/(8N))`, `xsin1[k] = -sin(2π*(8k+1)/(8N))` +/// with N=512 and k in 0..128 (§7.9.4.1 step 2). +struct LongTwiddle { + xcos: [f32; 128], + xsin: [f32; 128], +} + +impl LongTwiddle { + const fn placeholder() -> Self { + Self { + xcos: [0.0; 128], + xsin: [0.0; 128], + } + } + fn build() -> Self { + let mut t = Self::placeholder(); + let n = 512.0f32; + for k in 0..128 { + let arg = 2.0 * PI * (8.0 * k as f32 + 1.0) / (8.0 * n); + t.xcos[k] = -arg.cos(); + t.xsin[k] = -arg.sin(); + } + t + } +} + +/// Cached pre/post twiddle constants for the N=512 short block pair. +/// `xcos2[k] = -cos(2π*(8k+1)/(4N))`, `xsin2[k] = -sin(2π*(8k+1)/(4N))` +/// with N=512 and k in 0..64 (§7.9.4.2 step 2). Note the `/4N` vs `/8N`: +/// short-block twiddles are sampled at *twice* the rate of long-block ones. +struct ShortTwiddle { + xcos: [f32; 64], + xsin: [f32; 64], +} + +impl ShortTwiddle { + const fn placeholder() -> Self { + Self { + xcos: [0.0; 64], + xsin: [0.0; 64], + } + } + fn build() -> Self { + let mut t = Self::placeholder(); + let n = 512.0f32; + for k in 0..64 { + let arg = 2.0 * PI * (8.0 * k as f32 + 1.0) / (4.0 * n); + t.xcos[k] = -arg.cos(); + t.xsin[k] = -arg.sin(); + } + t + } +} + +/// Bit-reverse `x` within `log2n` bits. Used to unscramble the DIT FFT +/// input buffer in place. +fn bit_reverse(mut x: usize, log2n: u32) -> usize { + let mut r = 0usize; + for _ in 0..log2n { + r = (r << 1) | (x & 1); + x >>= 1; + } + r +} + +/// In-place iterative radix-2 decimation-in-time IFFT of `buf`. +/// +/// `buf.len()` must be a power of two. The inverse convention is the +/// unnormalised one — we sum `sum_k X[k] * exp(+j*2πkn/N)`, which matches +/// the spec's `(cos(8πkn/N) + j*sin(8πkn/N))` kernel when you account for +/// the IMDCT's N=4·FFT-length scaling (and, for short blocks, N=8·len). +/// +/// DIT radix-2 is the simplest correct choice for AC-3's 128/64-point +/// transforms — both are <= 128 butterflies per stage × 7 stages, well +/// inside the per-frame budget. +fn ifft_r2_dit(buf: &mut [C]) { + let n = buf.len(); + assert!(n.is_power_of_two()); + let log2n = n.trailing_zeros(); + + // Bit-reversed input shuffle. + for i in 0..n { + let j = bit_reverse(i, log2n); + if j > i { + buf.swap(i, j); + } + } + + // Butterflies. `half` iterates 1, 2, 4, ..., n/2. + let mut half = 1usize; + while half < n { + let step = half * 2; + // Twiddle step per butterfly group. For IFFT, `exp(+j*2π/step)`. + let theta = PI / half as f32; // 2π / step = 2π / (2*half) = π / half + let wpr = theta.cos(); + let wpi = theta.sin(); + let mut k = 0usize; + while k < n { + let mut wr = 1.0f32; + let mut wi = 0.0f32; + for j in 0..half { + let a = buf[k + j]; + let b = buf[k + j + half]; + // t = w * b + let tr = wr * b.0 - wi * b.1; + let ti = wr * b.1 + wi * b.0; + buf[k + j + half] = (a.0 - tr, a.1 - ti); + buf[k + j] = (a.0 + tr, a.1 + ti); + // Advance the twiddle: w *= exp(+j*theta) + let nwr = wr * wpr - wi * wpi; + let nwi = wr * wpi + wi * wpr; + wr = nwr; + wi = nwi; + } + k += step; + } + half = step; + } +} + +/// FFT-backed 512-point IMDCT (§7.9.4.1 long-block path). +/// +/// Input: 256 MDCT coefficients `X[k]`. Output: 512 bare IMDCT samples +/// `x[n]` — without the windowing step 5 multiplication, because the +/// decoder applies `WINDOW[]` itself in the overlap-add glue (see +/// `audblk.rs` around line 1474). The polarity and scale match the +/// direct-form reference in `audblk::imdct_512` within f32 precision. +pub fn imdct_512_fft(x: &[f32; 256], out: &mut [f32; 512]) { + const NOVER4: usize = 128; + let tw = LongTwiddle::build(); + + // Step 2 — pre-IFFT complex multiply: + // Z[k] = (X[N/2-2k-1] + j*X[2k]) * (xcos[k] + j*xsin[k]) + let mut z = [(0.0f32, 0.0f32); NOVER4]; + for k in 0..NOVER4 { + let a = x[256 - 2 * k - 1]; // real part of (X[N/2-2k-1] + j*X[2k]) + let b = x[2 * k]; // imag part + let cr = tw.xcos[k]; + let ci = tw.xsin[k]; + z[k] = (a * cr - b * ci, b * cr + a * ci); + } + + // Step 3 — N/4-point complex IFFT (unnormalised, +j convention). + ifft_r2_dit(&mut z); + + // Step 4 — post-IFFT complex multiply: + // y[n] = z[n] * (xcos[n] + j*xsin[n]) + let mut yr = [0.0f32; NOVER4]; + let mut yi = [0.0f32; NOVER4]; + for n in 0..NOVER4 { + let cr = tw.xcos[n]; + let ci = tw.xsin[n]; + yr[n] = z[n].0 * cr - z[n].1 * ci; + yi[n] = z[n].1 * cr + z[n].0 * ci; + } + + // Step 5 — de-interleave (WITHOUT window multiplication; the caller + // applies WINDOW[] downstream). N=512 so N/8=64, N/4=128, N/2=256, + // 3N/4=384. + // + // x[2n] = -yi[N/8+n] + // x[2n+1] = yr[N/8-n-1] + // x[N/4+2n] = -yr[n] + // x[N/4+2n+1] = yi[N/4-n-1] + // x[N/2+2n] = -yr[N/8+n] + // x[N/2+2n+1] = yi[N/8-n-1] + // x[3N/4+2n] = yi[n] + // x[3N/4+2n+1] = -yr[N/4-n-1] + const NOVER8: usize = 64; + for n in 0..NOVER8 { + out[2 * n] = -yi[NOVER8 + n] * SCALE_LONG; + out[2 * n + 1] = yr[NOVER8 - n - 1] * SCALE_LONG; + out[128 + 2 * n] = -yr[n] * SCALE_LONG; + out[128 + 2 * n + 1] = yi[NOVER4 - n - 1] * SCALE_LONG; + out[256 + 2 * n] = -yr[NOVER8 + n] * SCALE_LONG; + out[256 + 2 * n + 1] = yi[NOVER8 - n - 1] * SCALE_LONG; + out[384 + 2 * n] = yi[n] * SCALE_LONG; + out[384 + 2 * n + 1] = -yr[NOVER4 - n - 1] * SCALE_LONG; + } +} + +/// FFT-backed short-block IMDCT pair (§7.9.4.2). +/// +/// The 256 input coefficients are interleaved as `X1[k] = x[2k]` and +/// `X2[k] = x[2k+1]` per step 1. Each half is then transformed by an +/// N/8 = 64-point complex IFFT with short-block twiddles `xcos2/xsin2`. +/// The two halves are de-interleaved into a single 512-sample output +/// using the step-5 permutation (without windowing; same rationale as +/// the long-block routine). +pub fn imdct_256_pair_fft(x: &[f32; 256], out: &mut [f32; 512]) { + const NOVER8: usize = 64; + const NOVER4: usize = 128; + let tw = ShortTwiddle::build(); + + // Step 1 — split into two halves. + let mut x1 = [0.0f32; NOVER4]; + let mut x2 = [0.0f32; NOVER4]; + for k in 0..NOVER4 { + x1[k] = x[2 * k]; + x2[k] = x[2 * k + 1]; + } + + // Step 2 — per-half pre-IFFT complex multiply. + // Z_i[k] = (Xi[N/4-2k-1] + j*Xi[2k]) * (xcos2[k] + j*xsin2[k]) + let mut z1 = [(0.0f32, 0.0f32); NOVER8]; + let mut z2 = [(0.0f32, 0.0f32); NOVER8]; + for k in 0..NOVER8 { + let cr = tw.xcos[k]; + let ci = tw.xsin[k]; + let a1 = x1[NOVER4 - 2 * k - 1]; + let b1 = x1[2 * k]; + z1[k] = (a1 * cr - b1 * ci, b1 * cr + a1 * ci); + let a2 = x2[NOVER4 - 2 * k - 1]; + let b2 = x2[2 * k]; + z2[k] = (a2 * cr - b2 * ci, b2 * cr + a2 * ci); + } + + // Step 3 — N/8-point complex IFFTs. + ifft_r2_dit(&mut z1); + ifft_r2_dit(&mut z2); + + // Step 4 — per-half post-IFFT complex multiply. + let mut yr1 = [0.0f32; NOVER8]; + let mut yi1 = [0.0f32; NOVER8]; + let mut yr2 = [0.0f32; NOVER8]; + let mut yi2 = [0.0f32; NOVER8]; + for n in 0..NOVER8 { + let cr = tw.xcos[n]; + let ci = tw.xsin[n]; + yr1[n] = z1[n].0 * cr - z1[n].1 * ci; + yi1[n] = z1[n].1 * cr + z1[n].0 * ci; + yr2[n] = z2[n].0 * cr - z2[n].1 * ci; + yi2[n] = z2[n].1 * cr + z2[n].0 * ci; + } + + // Step 5 — de-interleave the two halves into the 512-sample output, + // without the window multiplication. N=512 so N/8=64, N/4=128, + // N/2=256, 3N/4=384. The 256-pair steps iterate n in 0..N/8. + // + // ATSC A/52:2018 §7.9.4.2 step 5 (transcribed verbatim, window factor + // dropped because the caller applies the window separately): + // + // x[2n] = -yi1[n] + // x[2n+1] = yr1[N/8-n-1] + // x[N/4+2n] = -yr1[n] + // x[N/4+2n+1] = yi1[N/8-n-1] + // x[N/2+2n] = -yr2[n] + // x[N/2+2n+1] = yi2[N/8-n-1] + // x[3N/4+2n] = yi2[n] + // x[3N/4+2n+1]= -yr2[N/8-n-1] + // + // The first short transform (X1) uses the α=−1 phase (§8.2.3.2) and + // the second (X2) the α=+1 phase; the asymmetric de-interleave above + // is what realises that phase difference. Numerically, the verbatim + // pattern reproduces the α-parameterised direct-form IMDCT of each + // 256-point sub-block to within f32 rounding, with the same overall + // sign convention as the long-block §7.9.4.1 path — so SCALE_SHORT + // (= SCALE_LONG = 1.0) and the shared post-window / `2·(x+delay)` + // overlap-add reconstruct full-scale PCM. + for n in 0..NOVER8 { + out[2 * n] = -yi1[n] * SCALE_SHORT; + out[2 * n + 1] = yr1[NOVER8 - n - 1] * SCALE_SHORT; + out[128 + 2 * n] = -yr1[n] * SCALE_SHORT; + out[128 + 2 * n + 1] = yi1[NOVER8 - n - 1] * SCALE_SHORT; + out[256 + 2 * n] = -yr2[n] * SCALE_SHORT; + out[256 + 2 * n + 1] = yi2[NOVER8 - n - 1] * SCALE_SHORT; + out[384 + 2 * n] = yi2[n] * SCALE_SHORT; + out[384 + 2 * n + 1] = -yr2[NOVER8 - n - 1] * SCALE_SHORT; + } +} + +/// In-place iterative radix-2 decimation-in-time **forward** FFT of `buf` +/// with the `exp(-j·2πkn/N)` kernel (the analysis convention). +/// +/// `buf.len()` must be a power of two. This is the conjugate-twiddle twin +/// of [`ifft_r2_dit`]: identical butterfly structure, but the per-stage +/// twiddle advances by `exp(-j·θ)` instead of `exp(+j·θ)`, so the output +/// is `X[k] = Σ_n x[n]·(cos(2πkn/N) − j·sin(2πkn/N))` (unnormalised). The +/// caller applies any `1/N` scaling. +fn fft_r2_dit(buf: &mut [C]) { + let n = buf.len(); + assert!(n.is_power_of_two()); + let log2n = n.trailing_zeros(); + + for i in 0..n { + let j = bit_reverse(i, log2n); + if j > i { + buf.swap(i, j); + } + } + + let mut half = 1usize; + while half < n { + let step = half * 2; + // Forward FFT twiddle: exp(-j·2π/step) = exp(-j·π/half). + let theta = -PI / half as f32; + let wpr = theta.cos(); + let wpi = theta.sin(); + let mut k = 0usize; + while k < n { + let mut wr = 1.0f32; + let mut wi = 0.0f32; + for j in 0..half { + let a = buf[k + j]; + let b = buf[k + j + half]; + let tr = wr * b.0 - wi * b.1; + let ti = wr * b.1 + wi * b.0; + buf[k + j + half] = (a.0 - tr, a.1 - ti); + buf[k + j] = (a.0 + tr, a.1 + ti); + let nwr = wr * wpr - wi * wpi; + let nwi = wr * wpi + wi * wpr; + wr = nwr; + wi = nwi; + } + k += step; + } + half = step; + } +} + +/// §E.3.5.5.1 step 5 — the normalised forward DFT +/// `Z[k] = (1/N)·Σ_n (re[n] + j·im[n])·(cos(2πkn/N) − j·sin(2πkn/N))` +/// for `N = 512`, returned as parallel real/imag arrays of length `N`. +/// +/// `re` / `im` are the length-512 complex input samples +/// (`pcm_real[n]` / `pcm_imag[n]` from the enhanced-coupling step 4); the +/// output `Z[k]` (`k = 0 .. N−1`) is the complex carrier the §E.3.5.5.4 +/// per-channel synthesis multiplies against. This is the only DFT in the +/// crate that is normalised by `1/N` (the IMDCT path deliberately omits +/// normalisation to fold AC-3's overlap-add factor of 2), matching the +/// spec's explicit `(1/N)·Σ` here. +pub fn dft_512_forward(re: &[f32; 512], im: &[f32; 512]) -> ([f32; 512], [f32; 512]) { + let mut buf: Vec = (0..512).map(|n| (re[n], im[n])).collect(); + fft_r2_dit(&mut buf); + let mut zr = [0.0f32; 512]; + let mut zi = [0.0f32; 512]; + let inv_n = 1.0 / 512.0; + for k in 0..512 { + zr[k] = buf[k].0 * inv_n; + zi[k] = buf[k].1 * inv_n; + } + (zr, zi) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Direct-form 512-point IMDCT reference (§7.9.4.1 as a plain cosine + /// sum). Used only to gate the FFT path. Not exposed publicly. + fn ref_imdct_512(x: &[f32; 256], out: &mut [f32; 512]) { + let n: usize = 512; + let scale = -1.0f32; + for nn in 0..n { + let mut s = 0.0f32; + for k in 0..256 { + let phase = + PI / (2.0 * n as f32) * ((2 * nn + 1 + n / 2) as f32) * ((2 * k + 1) as f32); + s += x[k] * phase.cos(); + } + out[nn] = scale * s; + } + } + + fn cmp_max_abs(a: &[f32], b: &[f32]) -> f32 { + a.iter() + .zip(b.iter()) + .map(|(&x, &y)| (x - y).abs()) + .fold(0.0f32, f32::max) + } + + /// Sanity-check the radix-2 IFFT kernel: a length-8 impulse response + /// must produce uniform samples equal to the impulse magnitude. + #[test] + fn ifft_impulse_is_constant() { + let mut buf = [(0.0f32, 0.0f32); 8]; + buf[0] = (3.0, 0.0); + ifft_r2_dit(&mut buf); + for (i, &c) in buf.iter().enumerate() { + assert!((c.0 - 3.0).abs() < 1e-5, "idx {i}: re={}", c.0); + assert!(c.1.abs() < 1e-5, "idx {i}: im={}", c.1); + } + } + + /// Forward + inverse DFT of a known vector: the IFFT of `X[k] = δ[k-m]` + /// is `x[n] = exp(+j*2πmn/N)`. We exercise N=16, m=3 and check both + /// components. + #[test] + fn ifft_single_bin_is_cis_tone() { + let n = 16usize; + let m = 3usize; + let mut buf = vec![(0.0f32, 0.0f32); n]; + buf[m] = (1.0, 0.0); + ifft_r2_dit(&mut buf); + for i in 0..n { + let arg = 2.0 * PI * m as f32 * i as f32 / n as f32; + let (er, ei) = (arg.cos(), arg.sin()); + assert!((buf[i].0 - er).abs() < 1e-5, "re @ {i}"); + assert!((buf[i].1 - ei).abs() < 1e-5, "im @ {i}"); + } + } + + /// §E.3.5.5.1 step-5 forward DFT against a direct O(N²) reference on a + /// deterministic complex input. The reference is the literal spec + /// kernel `(1/N)·Σ_n (re+j·im)·(cos − j·sin)`. + #[test] + fn dft_512_forward_matches_direct_reference() { + let mut re = [0.0f32; 512]; + let mut im = [0.0f32; 512]; + let mut s: u32 = 0x0BAD_F00D; + for n in 0..512 { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + re[n] = (s as i32 as f32) / (i32::MAX as f32); + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + im[n] = (s as i32 as f32) / (i32::MAX as f32); + } + let (zr, zi) = dft_512_forward(&re, &im); + let inv_n = 1.0f32 / 512.0; + for &k in &[0usize, 1, 5, 128, 256, 511] { + let mut rr = 0.0f32; + let mut ri = 0.0f32; + for n in 0..512 { + let arg = 2.0 * PI * k as f32 * n as f32 / 512.0; + let (c, si) = (arg.cos(), arg.sin()); + // (re + j·im)·(c − j·si) + rr += re[n] * c + im[n] * si; + ri += im[n] * c - re[n] * si; + } + rr *= inv_n; + ri *= inv_n; + assert!((zr[k] - rr).abs() < 2e-4, "Zr[{k}]: fft={} ref={rr}", zr[k]); + assert!((zi[k] - ri).abs() < 2e-4, "Zi[{k}]: fft={} ref={ri}", zi[k]); + } + } + + #[test] + fn imdct_512_fft_matches_reference_impulse() { + for &k in &[0usize, 1, 7, 64, 128, 255] { + let mut x = [0.0f32; 256]; + x[k] = 1.0; + let mut r = [0.0f32; 512]; + let mut f = [0.0f32; 512]; + ref_imdct_512(&x, &mut r); + imdct_512_fft(&x, &mut f); + let err = cmp_max_abs(&r, &f); + assert!(err < 1e-3, "k={k} err={err}"); + } + } + + #[test] + fn imdct_512_fft_matches_reference_random() { + // LCG-based deterministic "random" input — no rand dependency. + let mut x = [0.0f32; 256]; + let mut s: u32 = 0x1234_5678; + for v in x.iter_mut() { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + *v = (s as i32 as f32) / (i32::MAX as f32); + } + let mut r = [0.0f32; 512]; + let mut f = [0.0f32; 512]; + ref_imdct_512(&x, &mut r); + imdct_512_fft(&x, &mut f); + let err = cmp_max_abs(&r, &f); + // ±2e-3 on a 256-term sum of unit-magnitude oscillators is + // acceptable f32 round-off; the reference itself is not more + // precise than that. + assert!(err < 2e-3, "err={err}"); + } + + /// The spec's §7.9.4.2 short-block fast decomposition does NOT produce + /// the same output as a naive per-half 256-point IMDCT with N=256 and + /// n/2=128 phase offset — it folds the two halves into a single + /// N=512 time-domain buffer with the spec's specific interleaving. We + /// verify a weaker property here: that on an *all-ones* input both + /// paths produce a low-DC (nearly symmetric) waveform with matching + /// RMS. This is enough to catch an order-of-magnitude bug in the + /// scale without pinning us to the direct form, which we don't fully + /// trust for the short block anyway (the validator-fixture RMS test + /// is the real gate once we wire the FFT paths into `audblk.rs`). + #[test] + fn imdct_256_pair_fft_has_reasonable_envelope() { + let mut x = [0.0f32; 256]; + for (i, v) in x.iter_mut().enumerate() { + // Small smooth signal — pure sine at the transform's bin-1. + *v = ((i as f32) * 0.01).sin(); + } + let mut f = [0.0f32; 512]; + imdct_256_pair_fft(&x, &mut f); + let peak = f.iter().fold(0.0f32, |a, &b| a.max(b.abs())); + let sse: f32 = f.iter().map(|&v| v * v).sum(); + let rms = (sse / 512.0).sqrt(); + // Envelope should be bounded; a runaway scale would blow this out. + assert!(peak < 200.0, "peak={peak} too large — scale runaway?"); + assert!(rms > 0.001, "rms={rms} — output essentially zero?"); + } + + /// The FFT-backed short-block pair must reproduce the spec's + /// α-parameterised direct-form IMDCT (§7.9.4.2 / §8.2.3.2): the + /// first 256 output samples equal the α=−1 256-point IMDCT of the + /// even coefficients X1[k]=X[2k], the last 256 the α=+1 IMDCT of + /// the odd coefficients X2[k]=X[2k+1]. The two short transforms + /// therefore have *different* internal symmetry (α=−1 is the + /// antisymmetric MDCT-IV form, α=+1 the mirror form), which is + /// exactly why §7.9.4.2 step 5 de-interleaves the X2 half with a + /// different (−yr,yi,yi,−yr) pattern than the X1 half. This test + /// pins the FFT path to the direct-form reference and fails if the + /// X2 de-interleave is ever reverted to the X1 pattern. + #[test] + fn imdct_256_pair_fft_matches_alpha_direct_form() { + // Direct-form α-parameterised 256-point IMDCT (N_s = 256). + // x[n] = Σ_k X[k] cos( (2π/4N)(2n+1)(2k+1) + (π/4)(2k+1)(1+α) ) + // with the overall −1 sign convention shared with the FFT path. + fn short_imdct(half: &[f32; 128], alpha: f32) -> [f32; 256] { + const NS: usize = 256; + let mut out = [0.0f32; NS]; + for (n, o) in out.iter_mut().enumerate() { + let mut s = 0.0f32; + for (k, &xk) in half.iter().enumerate() { + let two_k1 = (2 * k + 1) as f32; + let phase = (2.0 * PI / (4.0 * NS as f32)) * (2 * n + 1) as f32 * two_k1 + + (PI / 4.0) * two_k1 * (1.0 + alpha); + s += xk * phase.cos(); + } + *o = -s; + } + out + } + // LCG-based deterministic random input. + let mut x = [0.0f32; 256]; + let mut s: u32 = 0x1234_5678; + for v in x.iter_mut() { + s = s.wrapping_mul(1664525).wrapping_add(1013904223); + *v = (s as i32 as f32) / (i32::MAX as f32); + } + let mut x1 = [0.0f32; 128]; + let mut x2 = [0.0f32; 128]; + for k in 0..128 { + x1[k] = x[2 * k]; + x2[k] = x[2 * k + 1]; + } + let ref1 = short_imdct(&x1, -1.0); + let ref2 = short_imdct(&x2, 1.0); + let mut f = [0.0f32; 512]; + imdct_256_pair_fft(&x, &mut f); + let max_lo = (0..256usize) + .map(|n| (f[n] - ref1[n]).abs()) + .fold(0.0f32, f32::max); + let max_hi = (0..256usize) + .map(|n| (f[256 + n] - ref2[n]).abs()) + .fold(0.0f32, f32::max); + let scale = ref1 + .iter() + .chain(ref2.iter()) + .fold(0.0f32, |a, &b| a.max(b.abs())); + assert!( + max_lo < 1e-3 * scale.max(1.0), + "short1 vs α=-1 direct form diverges: max |Δ| = {max_lo}" + ); + assert!( + max_hi < 1e-3 * scale.max(1.0), + "short2 vs α=+1 direct form diverges: max |Δ| = {max_hi}" + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/lib.rs b/crates/vendor/oxideav-ac3/src/lib.rs new file mode 100644 index 00000000..b13bed95 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/lib.rs @@ -0,0 +1,240 @@ +//! Pure-Rust **AC-3 (Dolby Digital)** + **E-AC-3 (Enhanced AC-3, +//! a.k.a. Dolby Digital Plus)** audio decoder + encoder. +//! +//! Implements ATSC A/52:2018 (= ETSI TS 102 366) elementary streams: +//! base AC-3 (`bsid ≤ 10`) and Annex E (`bsid ∈ {11..=16}`). +//! +//! # Architecture +//! +//! The pipeline follows the spec's natural ordering. Each stage is a +//! module that owns one slice of §5..§7 (base AC-3) or §E (E-AC-3): +//! +//! 1. [`syncinfo`] — 16-bit sync word 0x0B77, crc1, fscod, frmsizecod, +//! frame-length table lookup (§5.3.1 / §5.4.1 / Table 5.18). +//! 2. [`bsi`] — Bit Stream Information: bsid, bsmod, acmod → +//! channel-layout + lfeon + dialnorm + optional timecodes / +//! Annex D §2.3 alternate-syntax mix-level params (§5.4.2). +//! 3. [`audblk`] — per-block exponent decode (§7.1), parametric bit +//! allocation (§7.2 with §7.2.2.6 delta-bit-allocation), mantissa +//! decode (§7.3), channel coupling (§7.4), rematrixing (§7.5), +//! dynamic-range compression (§7.7). +//! 4. [`imdct`] + [`mdct`] — §7.9.4 FFT-backed 512-point IMDCT and +//! 256-point short-block pair plus the forward transforms used by +//! the encoder; the direct-form references in `audblk` are kept +//! only as test oracles. +//! 5. [`downmix`] — §7.8 LoRo + §7.8.2 LtRt (Dolby Surround +//! matrix-encoded) downmix matrices for every source acmod, with +//! Annex D §2.3 / E-AC-3 mixmdata mix-level extension routing. +//! 6. [`wave_order`] — WAVE_FORMAT_EXTENSIBLE dwChannelMask channel +//! reorder for front-centre-bearing layouts (`acmod ∈ {3, 5, 7}`). +//! 7. [`encoder`] — full base AC-3 encoder (acmod 1/0..3/2 + LFE, +//! per-channel D15/D25/D45 exponent strategies, DBA, 5-fbw +//! coupling, per-channel `fsnroffst[ch]` tuning, per-block +//! snroffst redistribution, §7.10.1 dual-CRC emission). +//! 8. [`eac3`] — Annex E decoder + encoder. Decoder covers BSI, +//! audfrm (Tables E1.2 / E1.3), audblk DSP, §3.4 Adaptive Hybrid +//! Transform on fbw / LFE / coupling channels, §3.6 spectral +//! extension with §3.6.4.2.3 SPXATTEN border notch, and §3.7.2 +//! transient pre-noise processing. Encoder covers +//! indep+dep-substream pairs for 1.0 / 2.0 / 5.1 / 7.1 layouts; +//! SPX and AHT are out of scope on the encoder side. +//! 9. [`crc`] — §7.10.1 CRC-16 over poly 0x8005, shared between +//! encoder (forward generation, augmented form for crc2) and the +//! opt-in [`decoder::verify_packet_crc`] residue check. +//! +//! See `README.md` for the round-by-round status checklist and the +//! per-feature dB measurements. + +#![allow(clippy::needless_range_loop)] + +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod audblk; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod bsi; +pub mod crc; +pub mod decoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod downmix; +pub mod drc; +pub mod eac3; +pub mod encoder; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod imdct; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod mdct; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod syncinfo; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod tables; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub mod wave_order; + +use oxideav_core::{CodecCapabilities, CodecId, CodecParameters, CodecTag, Result}; +use oxideav_core::{CodecInfo, CodecRegistry, Decoder, Encoder}; + +pub const CODEC_ID_STR: &str = "ac3"; +pub const CODEC_ID_STR_EAC3: &str = "eac3"; + +/// Re-export the §6.1.9 / §7.6 / §7.7 dynamic-range-control + dialogue- +/// normalisation control surface at the crate root so callers can build a +/// DRC-configured decoder via [`decoder::make_decoder_with_drc`] without +/// reaching into the [`drc`] submodule. +pub use crate::drc::{DrcMode, DrcSettings}; + +/// Register the AC-3 + E-AC-3 decoder + encoder with the supplied codec +/// registry. +pub fn register_codecs(reg: &mut CodecRegistry) { + let cid = CodecId::new(CODEC_ID_STR); + let dec_caps = CodecCapabilities::audio("ac3_sw_dec") + .with_lossy(true) + .with_intra_only(true) + .with_max_channels(6) + .with_max_sample_rate(48_000); + let enc_caps = CodecCapabilities::audio("ac3_sw_enc") + .with_lossy(true) + .with_intra_only(true) + .with_max_channels(6) + .with_max_sample_rate(48_000); + // Container tag claims. AC-3 is identified by: + // - WAVEFORMATEX::wFormatTag = 0x2000 (AVI / WAV) + // - MP4 ObjectTypeIndication 0xA5 (for MP4/ISOBMFF carriage) + // - Matroska CodecID "A_AC3" + reg.register( + CodecInfo::new(cid.clone()) + .capabilities(dec_caps.clone()) + .decoder(make_decoder) + .tag(CodecTag::wave_format(0x2000)), + ); + reg.register( + CodecInfo::new(cid.clone()) + .capabilities(dec_caps.clone()) + .decoder(make_decoder) + .tag(CodecTag::mp4_object_type(0xA5)), + ); + reg.register( + CodecInfo::new(cid.clone()) + .capabilities(dec_caps) + .decoder(make_decoder) + .tag(CodecTag::matroska("A_AC3")), + ); + // Encoder registration — keyed on codec id only (no container tag + // so it gets picked up regardless of output muxer). + reg.register( + CodecInfo::new(cid) + .capabilities(enc_caps) + .encoder(make_encoder), + ); + + // E-AC-3 (Annex E). Accepts mono, stereo, 5.1, and 7.1 input on + // the encoder side. 7.1 (8 ch) emits an independent substream + // pair: indep substream carries a 5.1 program (acmod=7, lfeon=1) + // and dep substream 0 carries Lb/Rb back surrounds with chanmap + // bit 6 set (Lrs/Rrs pair) per ATSC A/52 Annex E §E.2.3.1.7-8 / + // §E.3.8.2. Encoder-side SPX and AHT are out of scope (decoder + // implements both). + // + // Decoder side: full Annex E DSP — §3.4 Adaptive Hybrid + // Transform on fbw / LFE / coupling channels, §3.6 spectral + // extension with §3.6.4.2.3 SPXATTEN border notch, §3.7.2 + // transient pre-noise processing, and §7.8 LoRo / LtRt downmix + // including mixmdata mix-level routing. + let eac3_cid = CodecId::new(CODEC_ID_STR_EAC3); + let eac3_dec_caps = CodecCapabilities::audio("eac3_sw_dec") + .with_lossy(true) + .with_intra_only(true) + .with_max_channels(8) + .with_max_sample_rate(48_000); + let eac3_enc_caps = CodecCapabilities::audio("eac3_sw_enc") + .with_lossy(true) + .with_intra_only(true) + .with_max_channels(8) + .with_max_sample_rate(48_000); + // Container tag claims for E-AC-3: + // - WAVEFORMATEX::wFormatTag = 0xA7 (DD+ in WAV/AVI) + // - MP4 ObjectTypeIndication 0xA6 (ISOBMFF carriage) + // - Matroska CodecID "A_EAC3" + reg.register( + CodecInfo::new(eac3_cid.clone()) + .capabilities(eac3_dec_caps.clone()) + .decoder(make_eac3_decoder) + .tag(CodecTag::wave_format(0xA7)), + ); + reg.register( + CodecInfo::new(eac3_cid.clone()) + .capabilities(eac3_dec_caps.clone()) + .decoder(make_eac3_decoder) + .tag(CodecTag::mp4_object_type(0xA6)), + ); + reg.register( + CodecInfo::new(eac3_cid.clone()) + .capabilities(eac3_dec_caps) + .decoder(make_eac3_decoder) + .tag(CodecTag::matroska("A_EAC3")), + ); + reg.register( + CodecInfo::new(eac3_cid) + .capabilities(eac3_enc_caps) + .encoder(make_eac3_encoder), + ); +} + +/// Unified registration entry point — installs AC-3 + E-AC-3 into the +/// codec sub-registry of the supplied [`oxideav_core::RuntimeContext`]. +pub fn register(ctx: &mut oxideav_core::RuntimeContext) { + register_codecs(&mut ctx.codecs); +} + +oxideav_core::register!("ac3", register); + +fn make_decoder(params: &CodecParameters) -> Result> { + decoder::make_decoder(params) +} + +fn make_encoder(params: &CodecParameters) -> Result> { + encoder::make_encoder(params) +} + +fn make_eac3_encoder(params: &CodecParameters) -> Result> { + eac3::make_encoder(params) +} + +fn make_eac3_decoder(params: &CodecParameters) -> Result> { + decoder::make_eac3_decoder(params) +} + +#[cfg(test)] +mod register_tests { + use super::*; + + #[test] + fn register_via_runtime_context_installs_codec_factory() { + let mut ctx = oxideav_core::RuntimeContext::new(); + register(&mut ctx); + let ac3 = CodecId::new(CODEC_ID_STR); + let eac3 = CodecId::new(CODEC_ID_STR_EAC3); + assert!( + ctx.codecs.has_decoder(&ac3), + "AC-3 decoder factory not installed via RuntimeContext" + ); + assert!( + ctx.codecs.has_encoder(&ac3), + "AC-3 encoder factory not installed via RuntimeContext" + ); + assert!( + ctx.codecs.has_decoder(&eac3), + "E-AC-3 decoder factory not installed via RuntimeContext" + ); + assert!( + ctx.codecs.has_encoder(&eac3), + "E-AC-3 encoder factory not installed via RuntimeContext" + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/mdct.rs b/crates/vendor/oxideav-ac3/src/mdct.rs new file mode 100644 index 00000000..724c42b1 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/mdct.rs @@ -0,0 +1,304 @@ +//! Forward MDCT (Modified Discrete Cosine Transform) for AC-3 encoding. +//! +//! Per §8.2.3.2 (A/52:2018), the AC-3 forward transform is +//! +//! ```text +//! X_D[k] = (-2/N) * sum_{n=0..N-1} x[n] * +//! cos( (2π/(4N)) * (2n+1) * (2k+1) +//! + (π/4) * (2k+1) * (1+α) ) +//! ``` +//! +//! with **N = 512** for the long block (α = 0) and **two N = 256 +//! transforms** for a short-block pair, the first using α = −1 and the +//! second α = +1. Each half therefore carries a different `(π/4)·(2k+1) +//! ·(1+α)` phase offset — the analysis counterpart of the asymmetric +//! §7.9.4.2 step-5 de-interleave in `crate::imdct::imdct_256_pair_fft`. +//! The 128 coefficients from each short transform are interleaved into +//! a single 256-coeff buffer per §7.9.4.2 step 1: `X[2k] = X1[k]`, +//! `X[2k+1] = X2[k]`. +//! +//! The 256-coefficient output of this transform, when fed back through +//! our [`super::audblk::imdct_512`] reference, recovers the windowed +//! input (modulo the standard TDAC 50% overlap-add). Concretely, for a +//! block of 512 windowed input samples, this forward MDCT plus the +//! decoder's IMDCT+window+overlap-add chain reproduces the middle 256 +//! samples exactly (to floating-point precision). The factor-of-`-2/N` +//! here pairs with the decoder's factor-of-`2` overlap-add so that the +//! combined round-trip gain is `1.0`. + +use std::f32::consts::PI; + +/// 512-point forward MDCT (§8.2.3.2, α=0 long transform). +/// +/// `input` : 512 windowed time-domain samples. +/// `output` : 256 MDCT coefficients — indices 0..N/2. +/// +/// The reference implementation is `O(N^2)` — 256 × 512 ≈ 128 k +/// multiply-adds per block. For a 48 kHz stereo frame we run it 12 +/// times per syncframe, well inside budget for a pure-Rust encoder +/// whose job is correctness first. +pub fn mdct_512(input: &[f32; 512], output: &mut [f32; 256]) { + let n: usize = 512; + // §8.2.3.2 mandates a `-2/N` normalisation. Combined with the + // decoder's `2/N` IMDCT scale and the ×2 overlap-add, the full + // analysis-synthesis round-trip lands on unity gain (to within + // window-table rounding). + let scale: f32 = -2.0 / n as f32; + let two_pi_over_4n = 2.0 * PI / (4.0 * n as f32); + let pi_over_4 = PI / 4.0; + for k in 0..256 { + let mut s = 0.0f32; + let two_k_plus_1 = (2 * k + 1) as f32; + let phase_b = pi_over_4 * two_k_plus_1; // α = 0 → (1+α) = 1 + for nn in 0..n { + let phase = two_pi_over_4n * (2 * nn + 1) as f32 * two_k_plus_1 + phase_b; + s += input[nn] * phase.cos(); + } + output[k] = scale * s; + } +} + +/// 256-point forward MDCT used for one half of a short-block pair +/// (§8.2.3.2). The two halves do **not** share a kernel: the spec's +/// forward transform carries a phase-offset parameter +/// +/// X[k] = (-2/N) · Σ x[n] · cos( (2π/4N)·(2n+1)·(2k+1) +/// + (π/4)·(2k+1)·(1+α) ) +/// +/// with α = −1 for the first short transform and α = +1 for the +/// second (§8.2.3.2). The corresponding decoder IMDCT +/// (`imdct_256_pair_fft`) realises that same α distinction through the +/// asymmetric §7.9.4.2 step-5 de-interleave, so the forward half must +/// pass the matching α to round-trip. The `-2/N` scale pairs with the +/// decoder's IMDCT scale + overlap-add gain to land on unity gain +/// under the spec's KBD window. +/// +/// `input` : 256 windowed time-domain samples (one half of the +/// short-block pair). +/// `alpha` : −1 for the first short transform, +1 for the second. +/// `output` : 128 MDCT coefficients. +fn mdct_256_half(input: &[f32; 256], alpha: f32, output: &mut [f32; 128]) { + let n: usize = 256; + let scale: f32 = -2.0 / n as f32; + // (2π/4N)·(2n+1)·(2k+1) = (π/2N)·(2n+1)·(2k+1). + let pi_over_2n = PI / (2.0 * n as f32); + let quarter_pi = PI / 4.0; + for k in 0..128 { + let mut s = 0.0f32; + let two_k_plus_1 = (2 * k + 1) as f32; + let phase_offset = quarter_pi * two_k_plus_1 * (1.0 + alpha); + for nn in 0..n { + let phase = pi_over_2n * (2 * nn + 1) as f32 * two_k_plus_1 + phase_offset; + s += input[nn] * phase.cos(); + } + output[k] = scale * s; + } +} + +/// Forward short-block MDCT pair (§8.2.3.2 + §7.9.4.2). +/// +/// The 512-sample windowed input is split into two 256-sample halves; +/// each half is run through [`mdct_256_half`] to produce 128 +/// coefficients, then the two coefficient sets are **interleaved** per +/// §7.9.4.2 step 1: `X[2k] = X1[k]`, `X[2k+1] = X2[k]`. This is the +/// exact layout `imdct_256_pair_fft` reads on the decoder side. +/// +/// Note that AC-3's per-channel windowing differs slightly between +/// long-only / short-only / long-to-short / short-to-long block-type +/// transitions (§7.9.5). For now the encoder applies the symmetric +/// 512-point KBD window in **all** cases — long-only and short-only — +/// which is identical to the long-only window the decoder applies +/// after IMDCT regardless of `blksw[ch]`. The transition cases (where +/// one neighbour is long and the other short) introduce a small TDAC +/// mismatch in the overlap region; the encoder's transient-detection +/// heuristic deliberately picks short blocks in *runs* of 1+ blocks +/// to keep transitions outside the burst peak's overlap window, which +/// keeps the residual below the per-block quantisation noise floor. +/// +/// `input` : 512 windowed time-domain samples (covers two short halves). +/// `output` : 256 interleaved MDCT coefficients. +pub fn mdct_256_pair(input: &[f32; 512], output: &mut [f32; 256]) { + let mut h1 = [0.0f32; 256]; + let mut h2 = [0.0f32; 256]; + h1.copy_from_slice(&input[..256]); + h2.copy_from_slice(&input[256..]); + let mut x1 = [0.0f32; 128]; + let mut x2 = [0.0f32; 128]; + // First short transform uses α=−1, the second α=+1 (§8.2.3.2). + mdct_256_half(&h1, -1.0, &mut x1); + mdct_256_half(&h2, 1.0, &mut x2); + for k in 0..128 { + output[2 * k] = x1[k]; + output[2 * k + 1] = x2[k]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::audblk; + use crate::tables::WINDOW; + + /// Forward MDCT followed by the decoder's IMDCT should approximately + /// invert under the AC-3 windowing + TDAC overlap-add rule. We feed + /// the same windowed block in twice (blocks N and N+1), run the + /// full analysis-synthesis chain, and expect the second block's + /// output to match the (windowed^2) input in the middle region. + #[test] + fn mdct_imdct_roundtrip_identity_window_tdac() { + // Construct a 768-sample ramp and encode two adjacent 512-sample + // overlapping blocks out of it. Decoder's overlap-add needs two + // blocks to produce valid output for the first block. + let sig_len = 512 + 256; + let mut sig = vec![0.0f32; sig_len]; + for (i, s) in sig.iter_mut().enumerate() { + // 100 Hz-ish sine to keep the magnitudes sensible under a 48 kHz rate. + let t = i as f32 / 48_000.0; + *s = (2.0 * PI * 440.0 * t).sin() * 0.3; + } + + // Build the symmetric 512-sample window from WINDOW[0..256] + mirror. + let mut full_win = [0.0f32; 512]; + for n in 0..256 { + full_win[n] = WINDOW[n]; + full_win[511 - n] = WINDOW[n]; + } + + // Window block 0 (samples 0..512). + let mut blk0 = [0.0f32; 512]; + for n in 0..512 { + blk0[n] = sig[n] * full_win[n]; + } + let mut x0 = [0.0f32; 256]; + mdct_512(&blk0, &mut x0); + + // Window block 1 (samples 256..768). + let mut blk1 = [0.0f32; 512]; + for n in 0..512 { + blk1[n] = sig[256 + n] * full_win[n]; + } + let mut x1 = [0.0f32; 256]; + mdct_512(&blk1, &mut x1); + + // IMDCT + window + overlap-add path, exactly as the decoder runs. + let mut delay = [0.0f32; 256]; + + // Block 0: IMDCT, window, OLA (primes delay; pcm0 is discarded). + let mut time0 = [0.0f32; 512]; + audblk::imdct_512(&x0, &mut time0); + for n in 0..256 { + time0[n] *= WINDOW[n]; + time0[511 - n] *= WINDOW[n]; + } + let mut _pcm0 = [0.0f32; 256]; + for n in 0..256 { + _pcm0[n] = 2.0 * (time0[n] + delay[n]); + delay[n] = time0[256 + n]; + } + + // Block 1: IMDCT, window, OLA → pcm1 should match input[256..512]. + let mut time1 = [0.0f32; 512]; + audblk::imdct_512(&x1, &mut time1); + for n in 0..256 { + time1[n] *= WINDOW[n]; + time1[511 - n] *= WINDOW[n]; + } + let mut pcm1 = [0.0f32; 256]; + for n in 0..256 { + pcm1[n] = 2.0 * (time1[n] + delay[n]); + } + + // Compare pcm1 to input[256..512]. The overlap-add equation + // pcm[n] = window[n]^2 * x[n] + window[n+256]^2 * x[n] = x[n] + // (because AC-3's window satisfies w[n]^2 + w[n+256]^2 = 1 — + // the Princen-Bradley condition for MDCT TDAC). + let mut worst: f32 = 0.0; + let mut sse: f32 = 0.0; + for n in 0..256 { + let err = (pcm1[n] - sig[256 + n]).abs(); + worst = worst.max(err); + sse += err * err; + } + let rms = (sse / 256.0).sqrt(); + eprintln!("mdct-imdct roundtrip: worst={worst:.5}, rms={rms:.5}"); + // The window is only approximate in the tables (5-decimal rounding); + // a few 1e-3 worst-case error is acceptable here. + assert!(worst < 0.01, "worst {worst} too large"); + assert!(rms < 5e-3, "rms {rms} too large"); + } + + /// The 128 inverse-basis vectors for a short-block half (X1) span a + /// 128-dimensional subspace of R^256. The encoder's forward MDCT is + /// the orthogonal projector onto that subspace; the per-half + /// MDCT-then-IMDCT round-trip recovers exactly the projection of + /// the input. We assert the basis is orthogonal with uniform norm + /// `N/2 = 128` here so any future change to the IMDCT polarity / + /// scale is caught at this gate (and the encoder's scale stays + /// derivable as `1/‖basis‖² = 2/N`). + #[test] + fn imdct_short_basis_is_uniform_orthogonal() { + let mut basis = vec![[0.0f32; 256]; 128]; + for k in 0..128 { + let mut x = [0.0f32; 256]; + x[2 * k] = 1.0; + let mut t = [0.0f32; 512]; + crate::imdct::imdct_256_pair_fft(&x, &mut t); + basis[k].copy_from_slice(&t[..256]); + } + let mut max_off = 0.0f32; + let mut min_norm = f32::INFINITY; + let mut max_norm = 0.0f32; + for k in 0..128 { + let n: f32 = basis[k].iter().map(|&v| v * v).sum(); + min_norm = min_norm.min(n); + max_norm = max_norm.max(n); + for j in (k + 1)..128 { + let dot: f32 = basis[k] + .iter() + .zip(basis[j].iter()) + .map(|(&a, &b)| a * b) + .sum(); + max_off = max_off.max(dot.abs()); + } + } + // Norm = N/2 = 128 (basis vectors are unit-amplitude cosines). + assert!((min_norm - 128.0).abs() < 0.01, "min_norm={min_norm}"); + assert!((max_norm - 128.0).abs() < 0.01, "max_norm={max_norm}"); + assert!(max_off < 0.01, "off-diagonal {max_off}"); + } + + /// End-to-end forward + inverse round-trip on a TDAC-compatible + /// input. Because the per-half MDCT only spans a 128-dim subspace + /// of R^256, we feed an input that is *already in the subspace* — + /// constructed by inverting an arbitrary 128-coeff bin pattern. + /// The forward must then exactly recover those coefficients, and + /// re-inverting must reproduce the original signal to f32 + /// precision. + #[test] + fn mdct_256_pair_recovers_subspace_signal() { + // Pick an arbitrary 128-coefficient pattern for short1 + + // short2 (X2 chosen to be a different low-order pattern so + // the full 256 input has harmonic content in both halves). + let mut x_target = [0.0f32; 256]; + for k in 0..16 { + x_target[2 * k] = 0.7 * (k as f32).sin(); + x_target[2 * k + 1] = 0.5 * (k as f32 * 1.3).cos(); + } + // Inverse → 512-sample signal (which lives in the subspace + // by construction). + let mut sig = [0.0f32; 512]; + crate::imdct::imdct_256_pair_fft(&x_target, &mut sig); + // Forward → should recover x_target exactly. + let mut x_back = [0.0f32; 256]; + mdct_256_pair(&sig, &mut x_back); + let mut max_err: f32 = 0.0; + for k in 0..256 { + max_err = max_err.max((x_back[k] - x_target[k]).abs()); + } + eprintln!("subspace round-trip: max coeff err = {max_err:.6e}"); + assert!( + max_err < 1e-3, + "forward/inverse mismatch on basis-subspace input: {max_err}" + ); + } +} diff --git a/crates/vendor/oxideav-ac3/src/syncinfo.rs b/crates/vendor/oxideav-ac3/src/syncinfo.rs new file mode 100644 index 00000000..f67a56a3 --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/syncinfo.rs @@ -0,0 +1,670 @@ +//! AC-3 synchronization frame header — `syncinfo` (§5.3.1 / §5.4.1). +//! +//! The syncinfo is the first field of every AC-3 syncframe. It is +//! exactly **5 bytes** of fixed-width fields (no variable bits): +//! +//! ```text +//! syncword 16 bits (always 0x0B77, transmission is MSB-first) +//! crc1 16 bits (CRC over the first 5/8 of the syncframe) +//! fscod 2 bits (sample rate: Table 5.6) +//! frmsizecod 6 bits (frame length: Table 5.18) +//! ``` +//! +//! `parse` does zero CRC validation — the spec allows either crc1 or +//! crc2 (or neither) to be checked; we defer that to an optional +//! verification pass once the whole frame is buffered. + +use oxideav_core::{Error, Result}; + +use crate::tables::{frame_length_bytes, nominal_bitrate_kbps, sample_rate_hz, FRAME_SIZE_TABLE}; + +/// The 16-bit AC-3 syncword (§5.4.1.1). +pub const SYNCWORD: u16 = 0x0B77; + +/// A fully-parsed syncinfo header. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SyncInfo { + pub crc1: u16, + pub fscod: u8, + pub frmsizecod: u8, + /// Sample rate in Hz as derived from `fscod` (Table 5.6). + pub sample_rate: u32, + /// Full frame length in **bytes** (including syncinfo itself, BSI, + /// all 6 audio blocks, auxdata, and crc2). Derived from + /// (fscod, frmsizecod) via Table 5.18. + pub frame_length: u32, +} + +impl SyncInfo { + /// Typed view over [`Self::fscod`] per §5.4.1.3 / Table 5.6. The + /// returned [`SampleRateCode`] decodes the 2-bit `fscod` field + /// into one of the three valid sampling-rate codepoints + /// ([`SampleRateCode::FortyEightKHz`] / + /// [`SampleRateCode::FortyFourPointOneKHz`] / + /// [`SampleRateCode::ThirtyTwoKHz`]) or + /// [`SampleRateCode::Reserved`] for the spec-reserved `'11'` + /// codepoint (per §5.4.1.3 the decoder must mute on receipt). + /// + /// [`parse`] already rejects the reserved codepoint at frame + /// boundary — a [`SyncInfo`] handed back from [`parse`] therefore + /// always reports a non-reserved [`SampleRateCode`]. The accessor + /// remains a thin wrapper over [`Self::fscod`] for chain consumers + /// that construct a [`SyncInfo`] by hand (e.g. resynthesising one + /// from container-stored metadata) and want the typed surface + /// without re-walking Table 5.6. + pub fn sample_rate_code(&self) -> SampleRateCode { + SampleRateCode::from_code(self.fscod) + } + + /// Typed view over [`Self::frmsizecod`] per §5.4.1.4 / Table 5.18. + /// The returned [`FrameSizeCode`] decodes the 6-bit `frmsizecod` + /// field into one of the 38 valid frame-size codepoints + /// (`0..=37`) or [`FrameSizeCode::Reserved`] for the `38..=63` + /// codepoints that have no Table 5.18 row. + /// + /// [`parse`] already rejects an out-of-range `frmsizecod` at frame + /// boundary — a [`SyncInfo`] handed back from [`parse`] therefore + /// always reports a non-reserved [`FrameSizeCode`]. The accessor + /// remains a thin wrapper over [`Self::frmsizecod`] for chain + /// consumers that construct a [`SyncInfo`] by hand (e.g. + /// resynthesising one from container-stored metadata) and want the + /// typed surface — the nominal bit-rate and per-rate word count — + /// without re-walking Table 5.18. + pub fn frame_size_code(&self) -> FrameSizeCode { + FrameSizeCode::from_code(self.frmsizecod) + } +} + +/// §5.4.1.3 sample-rate code (Table 5.6). A 2-bit codeword carried in +/// every AC-3 syncframe that selects one of the three supported +/// sampling rates — 48 kHz, 44.1 kHz, or 32 kHz — or the reserved +/// `'11'` codepoint that mandates a decoder mute. +/// +/// Surfaced via [`SyncInfo::sample_rate_code`]. Callers that just +/// want the rate in Hz can keep reading the pre-resolved +/// [`SyncInfo::sample_rate`] field; the typed enum is for chain +/// consumers that branch on the codepoint itself (e.g. a §7.15 +/// hearing-threshold table lookup that indexes into the per-`fscod` +/// row, or a §7.2.2.5 masking-curve evaluator that needs to flag a +/// reserved `fscod` separately from an out-of-range `dbpbcod`). +/// +/// Per §5.4.1.3: "If the reserved code is indicated, the decoder +/// should not attempt to decode audio and should mute." [`parse`] +/// enforces that rule at frame boundary by returning +/// [`oxideav_core::Error::Invalid`] for the `'11'` codepoint, so a +/// [`SyncInfo`] obtained from [`parse`] never reports +/// [`Self::Reserved`]; the variant is preserved so a chain consumer +/// constructing a [`SyncInfo`] from container-stored metadata (where +/// the upstream demuxer may not have validated `fscod`) can detect +/// the reserved code without re-walking Table 5.6. +/// +/// Annex E (E-AC-3) overloads the `'11'` codepoint as a reduced-rate +/// indicator that triggers a follow-on `fscod2` codeword (§E.2.3.1.4 +/// / §E.2.3.1.5), so this enum's [`Self::Reserved`] variant +/// corresponds to the **base AC-3** decoder-mute semantics only. The +/// Annex E `bsi` carries its own `fscod` + `fscod2` raw pair and the +/// reduced-rate paths are surfaced via the E-AC-3 BSI; this base-AC-3 +/// enum is not mirrored on the Annex E `Bsi`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SampleRateCode { + /// `'00'` — 48 kHz. + FortyEightKHz, + /// `'01'` — 44.1 kHz. + FortyFourPointOneKHz, + /// `'10'` — 32 kHz. + ThirtyTwoKHz, + /// `'11'` — reserved. Per §5.4.1.3 the decoder "should not attempt + /// to decode audio and should mute" on receipt; [`parse`] rejects + /// this codepoint at frame boundary so a [`SyncInfo`] obtained + /// from [`parse`] never carries it. + Reserved, +} + +impl SampleRateCode { + /// Decode the 2-bit wire value verbatim per Table 5.6. Only the + /// low 2 bits of `code` are consulted; the upper bits are + /// ignored so a caller that passes a full `bsid`-style byte does + /// not need to mask first. + pub fn from_code(code: u8) -> Self { + match code & 0x3 { + 0 => SampleRateCode::FortyEightKHz, + 1 => SampleRateCode::FortyFourPointOneKHz, + 2 => SampleRateCode::ThirtyTwoKHz, + _ => SampleRateCode::Reserved, + } + } + + /// Raw 2-bit code as it appeared on the wire — the round-trip + /// inverse of [`Self::from_code`]. + pub fn raw(self) -> u8 { + match self { + SampleRateCode::FortyEightKHz => 0, + SampleRateCode::FortyFourPointOneKHz => 1, + SampleRateCode::ThirtyTwoKHz => 2, + SampleRateCode::Reserved => 3, + } + } + + /// Sample rate in **Hz** per Table 5.6, or `None` for the + /// reserved codepoint (the spec mandates a decoder mute, with no + /// associated playback rate). + pub fn hertz(self) -> Option { + match self { + SampleRateCode::FortyEightKHz => Some(48_000), + SampleRateCode::FortyFourPointOneKHz => Some(44_100), + SampleRateCode::ThirtyTwoKHz => Some(32_000), + SampleRateCode::Reserved => None, + } + } + + /// Sample rate in **kHz** per Table 5.6, or `None` for the + /// reserved codepoint. Equivalent to [`Self::hertz`] divided by + /// 1 000; kept as a separate accessor since the spec text / + /// Table 5.6 / Annex D handbook tables phrase the rate in kHz + /// throughout. + pub fn kilohertz(self) -> Option { + match self { + SampleRateCode::FortyEightKHz => Some(48), + SampleRateCode::FortyFourPointOneKHz => Some(44), + SampleRateCode::ThirtyTwoKHz => Some(32), + SampleRateCode::Reserved => None, + } + } + + /// `true` only for [`Self::Reserved`] — lets a probe / re-emit + /// tool flag streams that carry the `'11'` reserved codepoint + /// (per §5.4.1.3 such streams must trigger a decoder mute) + /// without re-walking Table 5.6. + pub fn is_reserved(self) -> bool { + matches!(self, SampleRateCode::Reserved) + } + + /// Row index into the §7.15 hearing-threshold table + /// (`tables::HTH[fscod]`) and into [`crate::tables::HTH`] in + /// general. Returns `None` for [`Self::Reserved`] since the spec + /// only defines the three valid rows (fscod 0..=2). The index is + /// equal to [`Self::raw`] for the three valid codepoints but + /// returning it from a dedicated accessor keeps Table 5.6 → + /// Table 7.15 routing one call away from the typed surface. + pub fn hth_row_index(self) -> Option { + match self { + SampleRateCode::FortyEightKHz => Some(0), + SampleRateCode::FortyFourPointOneKHz => Some(1), + SampleRateCode::ThirtyTwoKHz => Some(2), + SampleRateCode::Reserved => None, + } + } +} + +/// §5.4.1.4 frame-size code (Table 5.18). A 6-bit codeword carried in +/// every AC-3 syncframe that — together with the [`SampleRateCode`] — +/// selects the number of 16-bit words in the syncframe before the next +/// syncword. Per §5.4.1.4: "The frame size code is used along with the +/// sample rate code to determine the number of (2-byte) words before +/// the next syncword." +/// +/// Table 5.18 defines 38 valid codepoints (`frmsizecod = 0..=37`); the +/// remaining `38..=63` codepoints have no table row and are surfaced as +/// [`FrameSizeCode::Reserved`]. Each nominal bit-rate occupies two +/// neighbouring codepoints (the even/odd pair) because a 44.1 kHz +/// encoding must alternate frame sizes to hit the declared bit-rate on +/// average — both sizes appear in Table 5.18. +/// +/// Surfaced via [`SyncInfo::frame_size_code`]. Callers that just want +/// the byte length the demuxer must consume can keep reading the +/// pre-resolved [`SyncInfo::frame_length`] field; the typed enum is for +/// chain consumers that branch on the nominal bit-rate +/// ([`Self::nominal_bitrate_kbps`]) or need the raw per-rate word count +/// ([`Self::words`]) without re-walking Table 5.18. +/// +/// [`parse`] rejects an out-of-range `frmsizecod` at frame boundary by +/// returning [`oxideav_core::Error::Invalid`], so a [`SyncInfo`] +/// obtained from [`parse`] never reports [`Self::Reserved`]; the +/// variant is preserved so a chain consumer constructing a [`SyncInfo`] +/// from container-stored metadata (where the upstream demuxer may not +/// have validated `frmsizecod`) can detect the reserved range without +/// re-walking Table 5.18. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameSizeCode { + /// A valid Table 5.18 codepoint (`frmsizecod = 0..=37`). + Valid(u8), + /// A `frmsizecod` in the `38..=63` range — no Table 5.18 row. Per + /// §5.4.1.4 only `0..=37` are defined; [`parse`] rejects this range + /// at frame boundary so a [`SyncInfo`] from [`parse`] never carries + /// it. + Reserved, +} + +impl FrameSizeCode { + /// Decode the 6-bit wire value verbatim per Table 5.18. The low 6 + /// bits of `code` are consulted; the upper 2 bits are ignored so a + /// caller that passes byte 4 of the syncinfo (which packs `fscod` + /// in the top two bits) does not need to mask first. A masked value + /// of `0..=37` maps to [`Self::Valid`]; `38..=63` maps to + /// [`Self::Reserved`]. + pub fn from_code(code: u8) -> Self { + let frmsizecod = code & 0x3F; + if (frmsizecod as usize) < FRAME_SIZE_TABLE.len() { + FrameSizeCode::Valid(frmsizecod) + } else { + FrameSizeCode::Reserved + } + } + + /// Raw 6-bit code as it appeared on the wire — the round-trip + /// inverse of [`Self::from_code`] for the valid range. Returns the + /// stored codepoint for [`Self::Valid`]; for [`Self::Reserved`] + /// there is no single wire value (the variant collapses the whole + /// `38..=63` range) so this returns `None`. + pub fn raw(self) -> Option { + match self { + FrameSizeCode::Valid(c) => Some(c), + FrameSizeCode::Reserved => None, + } + } + + /// `true` only for [`Self::Reserved`] — lets a probe / re-emit tool + /// flag streams that carry a `frmsizecod` in the undefined + /// `38..=63` range without re-walking Table 5.18. + pub fn is_reserved(self) -> bool { + matches!(self, FrameSizeCode::Reserved) + } + + /// Nominal bit rate in **kbps** per Table 5.18, or `None` for the + /// reserved range. The two neighbouring codepoints that share a + /// bit-rate (e.g. `frmsizecod = 0` and `1` both = 32 kbps) return + /// the same value. + pub fn nominal_bitrate_kbps(self) -> Option { + match self { + FrameSizeCode::Valid(c) => nominal_bitrate_kbps(c), + FrameSizeCode::Reserved => None, + } + } + + /// The Table 5.18 syncframe length in **16-bit words** for the given + /// [`SampleRateCode`], or `None` if either this code or the rate + /// code is reserved. The byte length consumed by the demuxer is + /// twice this value (the table is denominated in 2-byte words per + /// §5.4.1.4). + pub fn words(self, rate: SampleRateCode) -> Option { + let c = match self { + FrameSizeCode::Valid(c) => c as usize, + FrameSizeCode::Reserved => return None, + }; + let (_, w32, w44, w48) = FRAME_SIZE_TABLE[c]; + match rate { + SampleRateCode::FortyEightKHz => Some(w48), + SampleRateCode::FortyFourPointOneKHz => Some(w44), + SampleRateCode::ThirtyTwoKHz => Some(w32), + SampleRateCode::Reserved => None, + } + } + + /// The Table 5.18 syncframe length in **bytes** for the given + /// [`SampleRateCode`] (`2 ×` [`Self::words`]), or `None` if either + /// code is reserved. This matches the pre-resolved + /// [`SyncInfo::frame_length`] field for any frame [`parse`] accepts. + pub fn frame_length_bytes(self, rate: SampleRateCode) -> Option { + self.words(rate).map(|w| w * 2) + } +} + +/// Parse the 5-byte syncinfo out of the front of `data`. +/// +/// Returns `Error::Invalid` if the syncword is missing or if the +/// fscod/frmsizecod pair lands on a reserved entry (in which case the +/// spec mandates the decoder mute — see §5.4.1.3 and §5.4.1.4). +pub fn parse(data: &[u8]) -> Result { + if data.len() < 5 { + return Err(Error::invalid(format!( + "ac3: syncinfo needs 5 bytes, got {}", + data.len() + ))); + } + let syncword = u16::from_be_bytes([data[0], data[1]]); + if syncword != SYNCWORD { + return Err(Error::invalid(format!( + "ac3: bad syncword 0x{syncword:04X} (expected 0x0B77)" + ))); + } + let crc1 = u16::from_be_bytes([data[2], data[3]]); + // Byte 4: fscod (2 bits, MSB) + frmsizecod (6 bits, LSB). + let b4 = data[4]; + let fscod = (b4 >> 6) & 0x03; + let frmsizecod = b4 & 0x3F; + let sample_rate = sample_rate_hz(fscod) + .ok_or_else(|| Error::invalid("ac3: reserved fscod '11' — decoder must mute"))?; + let frame_length = frame_length_bytes(fscod, frmsizecod) + .ok_or_else(|| Error::invalid(format!("ac3: invalid frmsizecod {frmsizecod}")))?; + Ok(SyncInfo { + crc1, + fscod, + frmsizecod, + sample_rate, + frame_length, + }) +} + +/// Find the byte offset of the next AC-3 syncword starting from `offset`. +/// Returns `None` if no syncword is found. +/// +/// Used by demuxers that don't already know where frame boundaries are +/// (e.g. raw / corrupted elementary streams) — but well-formed +/// containers hand us frames pre-cut and we can call `parse` directly. +pub fn find_syncword(data: &[u8], start: usize) -> Option { + let mut i = start; + while i + 1 < data.len() { + if data[i] == 0x0B && data[i + 1] == 0x77 { + return Some(i); + } + i += 1; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Happy path: a hand-crafted 5-byte header for a 48 kHz, 192 kbps + /// stream (frmsizecod = 20 → 384 words = 768 bytes). + #[test] + fn parses_valid_header() { + let mut buf = [0u8; 5]; + buf[0] = 0x0B; + buf[1] = 0x77; + buf[2] = 0xAB; + buf[3] = 0xCD; + // fscod=0 (48 kHz), frmsizecod=20 + buf[4] = 20; + let si = parse(&buf).unwrap(); + assert_eq!(si.sample_rate, 48_000); + assert_eq!(si.frame_length, 768); + assert_eq!(si.crc1, 0xABCD); + assert_eq!(si.frmsizecod, 20); + } + + #[test] + fn rejects_bad_syncword() { + let buf = [0xFF, 0x00, 0, 0, 0]; + assert!(parse(&buf).is_err()); + } + + #[test] + fn rejects_reserved_fscod() { + let buf = [0x0B, 0x77, 0, 0, 0xC0]; // fscod = 0b11 + assert!(parse(&buf).is_err()); + } + + #[test] + fn rejects_reserved_frmsizecod() { + // frmsizecod = 38 is past the end of Table 5.18. + let buf = [0x0B, 0x77, 0, 0, 38]; + assert!(parse(&buf).is_err()); + } + + #[test] + fn finds_syncword() { + let mut buf = vec![0u8; 32]; + buf[17] = 0x0B; + buf[18] = 0x77; + assert_eq!(find_syncword(&buf, 0), Some(17)); + assert_eq!(find_syncword(&buf, 18), None); + } + + /// Round-trip every Table 5.6 row through `SampleRateCode` — + /// the four codepoints `'00'`/`'01'`/`'10'`/`'11'` decode to + /// the four enum variants and `raw()` returns the original + /// codepoint verbatim. + #[test] + fn sample_rate_code_round_trip_all_codepoints() { + let rows = [ + (0u8, SampleRateCode::FortyEightKHz), + (1, SampleRateCode::FortyFourPointOneKHz), + (2, SampleRateCode::ThirtyTwoKHz), + (3, SampleRateCode::Reserved), + ]; + for (code, expected) in rows { + let got = SampleRateCode::from_code(code); + assert_eq!(got, expected, "code 0x{code:02X}"); + assert_eq!(got.raw(), code, "raw() round-trip for 0x{code:02X}"); + } + } + + /// `SampleRateCode::from_code` ignores the upper bits of the + /// argument so a caller can pass a full `bsid`-style byte without + /// masking first. + #[test] + fn sample_rate_code_ignores_upper_bits() { + assert_eq!( + SampleRateCode::from_code(0xFC), + SampleRateCode::FortyEightKHz + ); + assert_eq!( + SampleRateCode::from_code(0b1111_1101), + SampleRateCode::FortyFourPointOneKHz, + ); + assert_eq!( + SampleRateCode::from_code(0x06), + SampleRateCode::ThirtyTwoKHz + ); + assert_eq!(SampleRateCode::from_code(0xFF), SampleRateCode::Reserved); + } + + /// `hertz()` and `kilohertz()` return the Table 5.6 rates for the + /// three valid codepoints and `None` for the reserved codepoint. + #[test] + fn sample_rate_code_hertz_and_kilohertz() { + assert_eq!(SampleRateCode::FortyEightKHz.hertz(), Some(48_000)); + assert_eq!(SampleRateCode::FortyFourPointOneKHz.hertz(), Some(44_100)); + assert_eq!(SampleRateCode::ThirtyTwoKHz.hertz(), Some(32_000)); + assert_eq!(SampleRateCode::Reserved.hertz(), None); + + assert_eq!(SampleRateCode::FortyEightKHz.kilohertz(), Some(48)); + assert_eq!(SampleRateCode::FortyFourPointOneKHz.kilohertz(), Some(44)); + assert_eq!(SampleRateCode::ThirtyTwoKHz.kilohertz(), Some(32)); + assert_eq!(SampleRateCode::Reserved.kilohertz(), None); + } + + /// `is_reserved()` flags only the `'11'` codepoint. + #[test] + fn sample_rate_code_is_reserved_only_for_eleven() { + assert!(!SampleRateCode::FortyEightKHz.is_reserved()); + assert!(!SampleRateCode::FortyFourPointOneKHz.is_reserved()); + assert!(!SampleRateCode::ThirtyTwoKHz.is_reserved()); + assert!(SampleRateCode::Reserved.is_reserved()); + } + + /// `hth_row_index()` matches the Table 7.15 row order so callers + /// can route a typed sample-rate code straight into a §7.2.2.5 + /// hearing-threshold lookup. + #[test] + fn sample_rate_code_hth_row_index_matches_table_7_15() { + assert_eq!(SampleRateCode::FortyEightKHz.hth_row_index(), Some(0)); + assert_eq!( + SampleRateCode::FortyFourPointOneKHz.hth_row_index(), + Some(1) + ); + assert_eq!(SampleRateCode::ThirtyTwoKHz.hth_row_index(), Some(2)); + assert_eq!(SampleRateCode::Reserved.hth_row_index(), None); + } + + /// `SyncInfo::sample_rate_code()` mirrors the resolved + /// `sample_rate_hz()` lookup on every valid codepoint — for any + /// frame `parse` accepts, the typed surface and the raw + /// `sample_rate` field always agree. + #[test] + fn syncinfo_sample_rate_code_matches_resolved_rate() { + // fscod=0 / frmsizecod=20 = 48 kHz, 384 words. + let buf = [0x0B, 0x77, 0xAB, 0xCD, 20]; + let si = parse(&buf).unwrap(); + let src = si.sample_rate_code(); + assert_eq!(src, SampleRateCode::FortyEightKHz); + assert_eq!(src.hertz(), Some(si.sample_rate)); + assert!(!src.is_reserved()); + + // fscod=1 / frmsizecod=0 = 44.1 kHz, 69 words. + let buf = [0x0B, 0x77, 0, 0, 0x40]; + let si = parse(&buf).unwrap(); + let src = si.sample_rate_code(); + assert_eq!(src, SampleRateCode::FortyFourPointOneKHz); + assert_eq!(src.hertz(), Some(si.sample_rate)); + + // fscod=2 / frmsizecod=0 = 32 kHz, 96 words. + let buf = [0x0B, 0x77, 0, 0, 0x80]; + let si = parse(&buf).unwrap(); + let src = si.sample_rate_code(); + assert_eq!(src, SampleRateCode::ThirtyTwoKHz); + assert_eq!(src.hertz(), Some(si.sample_rate)); + } + + /// A caller constructing a `SyncInfo` by hand (e.g. from + /// container-stored metadata) with the reserved `fscod = 3` + /// code still gets the `SampleRateCode::Reserved` typed surface + /// — `parse` itself never lets the reserved code through, but + /// the typed accessor on a hand-built `SyncInfo` does flag it. + #[test] + fn syncinfo_sample_rate_code_surfaces_reserved_for_hand_built() { + let si = SyncInfo { + crc1: 0, + fscod: 3, + frmsizecod: 0, + sample_rate: 0, + frame_length: 0, + }; + let src = si.sample_rate_code(); + assert_eq!(src, SampleRateCode::Reserved); + assert!(src.is_reserved()); + assert_eq!(src.hertz(), None); + assert_eq!(src.hth_row_index(), None); + } + + /// Every valid Table 5.18 codepoint (`0..=37`) round-trips through + /// `FrameSizeCode::Valid` and `raw()` returns the original code; the + /// `38..=63` reserved range collapses to `FrameSizeCode::Reserved` + /// with `raw() == None`. + #[test] + fn frame_size_code_round_trip_valid_and_reserved() { + for code in 0u8..=37 { + let fsc = FrameSizeCode::from_code(code); + assert_eq!(fsc, FrameSizeCode::Valid(code), "code {code}"); + assert_eq!(fsc.raw(), Some(code), "raw() round-trip for {code}"); + assert!(!fsc.is_reserved(), "code {code} should not be reserved"); + } + for code in 38u8..=63 { + let fsc = FrameSizeCode::from_code(code); + assert_eq!(fsc, FrameSizeCode::Reserved, "code {code}"); + assert_eq!(fsc.raw(), None, "reserved raw() for {code}"); + assert!(fsc.is_reserved(), "code {code} should be reserved"); + } + } + + /// `from_code` masks off the upper 2 bits so a caller can pass byte + /// 4 of the syncinfo (which packs `fscod` in bits 7..6) without + /// masking first: `frmsizecod = 20` with `fscod = '00'` in the top + /// bits, vs the same `20` with `fscod = '11'` (byte = 0xD4), both + /// decode to `Valid(20)`. + #[test] + fn frame_size_code_ignores_upper_two_bits() { + assert_eq!(FrameSizeCode::from_code(20), FrameSizeCode::Valid(20)); + // 0b11_010100 = fscod '11' packed over frmsizecod = 20. + assert_eq!(FrameSizeCode::from_code(0xD4), FrameSizeCode::Valid(20)); + // 0b11_111111 = fscod '11' over frmsizecod = 63 (reserved). + assert_eq!(FrameSizeCode::from_code(0xFF), FrameSizeCode::Reserved); + } + + /// `nominal_bitrate_kbps` returns the Table 5.18 nominal rate for a + /// valid code (and the two-codepoints-per-rate pairing), `None` for + /// the reserved range. + #[test] + fn frame_size_code_nominal_bitrate() { + // frmsizecod 0 and 1 both = 32 kbps; 20 and 21 both = 192 kbps; + // 36 and 37 both = 640 kbps (the Table 5.18 endpoints). + assert_eq!(FrameSizeCode::Valid(0).nominal_bitrate_kbps(), Some(32)); + assert_eq!(FrameSizeCode::Valid(1).nominal_bitrate_kbps(), Some(32)); + assert_eq!(FrameSizeCode::Valid(20).nominal_bitrate_kbps(), Some(192)); + assert_eq!(FrameSizeCode::Valid(21).nominal_bitrate_kbps(), Some(192)); + assert_eq!(FrameSizeCode::Valid(37).nominal_bitrate_kbps(), Some(640)); + assert_eq!(FrameSizeCode::Reserved.nominal_bitrate_kbps(), None); + } + + /// `words` / `frame_length_bytes` return the per-rate Table 5.18 + /// values for a valid code and `None` when either this code or the + /// rate code is reserved. + #[test] + fn frame_size_code_words_and_bytes_per_rate() { + // frmsizecod = 20 (192 kbps): 576 words @ 32 kHz, 417 @ 44.1, + // 384 @ 48 — verbatim from Table 5.18. + let fsc = FrameSizeCode::Valid(20); + assert_eq!(fsc.words(SampleRateCode::ThirtyTwoKHz), Some(576)); + assert_eq!(fsc.words(SampleRateCode::FortyFourPointOneKHz), Some(417)); + assert_eq!(fsc.words(SampleRateCode::FortyEightKHz), Some(384)); + assert_eq!( + fsc.frame_length_bytes(SampleRateCode::FortyEightKHz), + Some(768) + ); + // Reserved rate → None even on a valid frame-size code. + assert_eq!(fsc.words(SampleRateCode::Reserved), None); + assert_eq!(fsc.frame_length_bytes(SampleRateCode::Reserved), None); + // Reserved frame-size code → None on every rate. + assert_eq!( + FrameSizeCode::Reserved.words(SampleRateCode::FortyEightKHz), + None + ); + assert_eq!( + FrameSizeCode::Reserved.frame_length_bytes(SampleRateCode::FortyEightKHz), + None + ); + } + + /// `SyncInfo::frame_size_code()` agrees with the pre-resolved + /// `frame_length` field on every frame `parse` accepts — the typed + /// surface's `frame_length_bytes(rate)` matches `si.frame_length`. + #[test] + fn syncinfo_frame_size_code_matches_resolved_length() { + // fscod=0 (48 kHz) / frmsizecod=20 → 768 bytes. + let buf = [0x0B, 0x77, 0xAB, 0xCD, 20]; + let si = parse(&buf).unwrap(); + let fsc = si.frame_size_code(); + assert_eq!(fsc, FrameSizeCode::Valid(20)); + assert_eq!(fsc.raw(), Some(si.frmsizecod)); + assert!(!fsc.is_reserved()); + assert_eq!( + fsc.frame_length_bytes(si.sample_rate_code()), + Some(si.frame_length) + ); + assert_eq!(fsc.nominal_bitrate_kbps(), Some(192)); + + // fscod=2 (32 kHz) / frmsizecod=0 → 96 words = 192 bytes. + let buf = [0x0B, 0x77, 0, 0, 0x80]; + let si = parse(&buf).unwrap(); + let fsc = si.frame_size_code(); + assert_eq!(fsc, FrameSizeCode::Valid(0)); + assert_eq!( + fsc.frame_length_bytes(si.sample_rate_code()), + Some(si.frame_length) + ); + } + + /// A caller constructing a `SyncInfo` by hand with an out-of-range + /// `frmsizecod = 40` still gets the `FrameSizeCode::Reserved` typed + /// surface — `parse` itself never lets a reserved frmsizecod + /// through, but the typed accessor on a hand-built `SyncInfo` flags + /// it. + #[test] + fn syncinfo_frame_size_code_surfaces_reserved_for_hand_built() { + let si = SyncInfo { + crc1: 0, + fscod: 0, + frmsizecod: 40, + sample_rate: 48_000, + frame_length: 0, + }; + let fsc = si.frame_size_code(); + assert_eq!(fsc, FrameSizeCode::Reserved); + assert!(fsc.is_reserved()); + assert_eq!(fsc.raw(), None); + assert_eq!(fsc.nominal_bitrate_kbps(), None); + assert_eq!(fsc.frame_length_bytes(SampleRateCode::FortyEightKHz), None); + } +} diff --git a/crates/vendor/oxideav-ac3/src/tables.rs b/crates/vendor/oxideav-ac3/src/tables.rs new file mode 100644 index 00000000..339936ad --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/tables.rs @@ -0,0 +1,487 @@ +//! Static lookup tables taken directly from ATSC A/52:2018. +//! +//! Every table here is line-for-line from a numbered table in the spec — +//! call sites always cite the section/table so it stays greppable. + +/// Table 5.6 — sampling frequency code (fscod) → sample rate in Hz. +/// +/// fscod `'11'` is **reserved** and the decoder must mute on receipt +/// (spec §5.4.1.3); this table returns `None` for that case. +pub fn sample_rate_hz(fscod: u8) -> Option { + match fscod { + 0 => Some(48_000), + 1 => Some(44_100), + 2 => Some(32_000), + _ => None, + } +} + +/// Table 5.18 — Frame Size Code Table (1 word = 16 bits). +/// +/// Each entry is `(nominal_kbps, words_32k, words_44k, words_48k)`. +/// frmsizecod ranges 0..=37 (6 bits, but values 38..=63 are reserved / +/// invalid); each nominal bitrate has two neighbouring frmsizecod values +/// because 44.1 kHz encoding must alternate frame sizes to hit the +/// declared bit-rate on average (Table 5.18 shows both sizes). +pub const FRAME_SIZE_TABLE: &[(u32, u32, u32, u32)] = &[ + (32, 96, 69, 64), + (32, 96, 70, 64), + (40, 120, 87, 80), + (40, 120, 88, 80), + (48, 144, 104, 96), + (48, 144, 105, 96), + (56, 168, 121, 112), + (56, 168, 122, 112), + (64, 192, 139, 128), + (64, 192, 140, 128), + (80, 240, 174, 160), + (80, 240, 175, 160), + (96, 288, 208, 192), + (96, 288, 209, 192), + (112, 336, 243, 224), + (112, 336, 244, 224), + (128, 384, 278, 256), + (128, 384, 279, 256), + (160, 480, 348, 320), + (160, 480, 349, 320), + (192, 576, 417, 384), + (192, 576, 418, 384), + (224, 672, 487, 448), + (224, 672, 488, 448), + (256, 768, 557, 512), + (256, 768, 558, 512), + (320, 960, 696, 640), + (320, 960, 697, 640), + (384, 1152, 835, 768), + (384, 1152, 836, 768), + (448, 1344, 975, 896), + (448, 1344, 976, 896), + (512, 1536, 1114, 1024), + (512, 1536, 1115, 1024), + (576, 1728, 1253, 1152), + (576, 1728, 1254, 1152), + (640, 1920, 1393, 1280), + (640, 1920, 1394, 1280), +]; + +/// Compute the total frame length in **bytes** from (fscod, frmsizecod). +/// +/// Returns `None` for reserved fscod (=3) or out-of-range frmsizecod +/// (>= 38). The table is indexed in "16-bit words per syncframe"; we +/// multiply by 2 so the result is the byte length the demuxer / parser +/// expects to consume before the next syncword. +pub fn frame_length_bytes(fscod: u8, frmsizecod: u8) -> Option { + let frmsizecod = frmsizecod as usize; + if frmsizecod >= FRAME_SIZE_TABLE.len() { + return None; + } + let (_, w32, w44, w48) = FRAME_SIZE_TABLE[frmsizecod]; + let words = match fscod { + 0 => w48, + 1 => w44, + 2 => w32, + _ => return None, + }; + Some(words * 2) +} + +/// Return the nominal bit rate in kbps for a given frmsizecod. The two +/// frmsizecod entries per bitrate are collapsed here since the bitrate +/// is identical. Returns `None` when out of range. +pub fn nominal_bitrate_kbps(frmsizecod: u8) -> Option { + let i = frmsizecod as usize; + if i >= FRAME_SIZE_TABLE.len() { + return None; + } + Some(FRAME_SIZE_TABLE[i].0) +} + +/// Table 5.8 — Audio Coding Mode (acmod) → (nfchans, channel ordering). +/// +/// `nfchans` here is the number of *full-bandwidth* channels; the total +/// channel count (`nchans`) is `nfchans + 1` when the LFE channel is on. +pub fn acmod_nfchans(acmod: u8) -> u8 { + match acmod { + 0 => 2, // 1+1 (Ch1, Ch2 — dual mono) + 1 => 1, // 1/0 (C) + 2 => 2, // 2/0 (L, R) + 3 => 3, // 3/0 (L, C, R) + 4 => 3, // 2/1 (L, R, S) + 5 => 4, // 3/1 (L, C, R, S) + 6 => 4, // 2/2 (L, R, SL, SR) + 7 => 5, // 3/2 (L, C, R, SL, SR) + _ => 0, + } +} + +// =========================================================================== +// Bit allocation tables (§7.2.3) — all values reproduced exactly from the +// spec. Addresses marked in comments. +// =========================================================================== + +/// Table 7.6 — Slow Decay Table. +pub const SLOWDEC: [i32; 4] = [0x0f, 0x11, 0x13, 0x15]; + +/// Table 7.7 — Fast Decay Table. +pub const FASTDEC: [i32; 4] = [0x3f, 0x53, 0x67, 0x7b]; + +/// Table 7.8 — Slow Gain Table. +pub const SLOWGAIN: [i32; 4] = [0x540, 0x4d8, 0x478, 0x410]; + +/// Table 7.9 — dB/Bit Table. +pub const DBPBTAB: [i32; 4] = [0x000, 0x700, 0x900, 0xb00]; + +/// Table 7.10 — Floor Table. Entries are 16-bit signed integers in the +/// spec; address 7 (0xf800) represents –2048. +pub const FLOORTAB: [i32; 8] = [0x2f0, 0x2b0, 0x270, 0x230, 0x1f0, 0x170, 0x0f0, -2048]; + +/// Table 7.11 — Fast Gain Table. +pub const FASTGAIN: [i32; 8] = [0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, 0x400]; + +/// Table 7.12 — Banding Structure Tables. First column is `bndtab[band]` +/// (first mantissa bin in each band), second is `bndsz[band]` (width in +/// mantissa bins). 50 entries. +pub const BNDTAB: [u32; 50] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 31, 34, 37, 40, 43, 46, 49, 55, 61, 67, 73, 79, 85, 97, 109, 121, 133, 157, 181, + 205, 229, +]; + +pub const BNDSZ: [u32; 50] = [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, + 3, 3, 3, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 24, 24, 24, 24, 24, +]; + +/// Table 7.13 — Bin Number to Band Number Table. masktab[bin] = band. +/// Indexed as `(10 * A) + B` with A=0..25, B=0..9. We unroll the full +/// 256-entry mapping for easy bin→band lookup. +pub const MASKTAB: [u8; 256] = { + let mut t = [0u8; 256]; + // Row 0 — 10 entries + let rows: &[(usize, [u8; 10])] = &[ + (0, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + (1, [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]), + (2, [20, 21, 22, 23, 24, 25, 26, 27, 28, 28]), + (3, [28, 29, 29, 29, 30, 30, 30, 31, 31, 31]), + (4, [32, 32, 32, 33, 33, 33, 34, 34, 34, 35]), + (5, [35, 35, 35, 35, 35, 36, 36, 36, 36, 36]), + (6, [36, 37, 37, 37, 37, 37, 37, 38, 38, 38]), + (7, [38, 38, 38, 39, 39, 39, 39, 39, 39, 40]), + (8, [40, 40, 40, 40, 40, 41, 41, 41, 41, 41]), + (9, [41, 41, 41, 41, 41, 41, 41, 42, 42, 42]), + (10, [42, 42, 42, 42, 42, 42, 42, 42, 42, 43]), + (11, [43, 43, 43, 43, 43, 43, 43, 43, 43, 43]), + (12, [43, 44, 44, 44, 44, 44, 44, 44, 44, 44]), + (13, [44, 44, 44, 45, 45, 45, 45, 45, 45, 45]), + (14, [45, 45, 45, 45, 45, 45, 45, 45, 45, 45]), + (15, [45, 45, 45, 45, 45, 45, 45, 46, 46, 46]), + (16, [46, 46, 46, 46, 46, 46, 46, 46, 46, 46]), + (17, [46, 46, 46, 46, 46, 46, 46, 46, 46, 46]), + (18, [46, 47, 47, 47, 47, 47, 47, 47, 47, 47]), + (19, [47, 47, 47, 47, 47, 47, 47, 47, 47, 47]), + (20, [47, 47, 47, 47, 47, 48, 48, 48, 48, 48]), + (21, [48, 48, 48, 48, 48, 48, 48, 48, 48, 48]), + (22, [48, 48, 48, 48, 48, 48, 48, 48, 48, 49]), + (23, [49, 49, 49, 49, 49, 49, 49, 49, 49, 49]), + (24, [49, 49, 49, 49, 49, 49, 49, 49, 49, 49]), + (25, [49, 49, 49, 0, 0, 0, 0, 0, 0, 0]), + ]; + let mut r = 0; + while r < rows.len() { + let (a, row) = rows[r]; + let mut b = 0; + while b < 10 { + let bin = 10 * a + b; + if bin < 256 { + t[bin] = row[b]; + } + b += 1; + } + r += 1; + } + t +}; + +/// Table 7.14 — Log-Addition Table, `latab[val]` with val = 10*A + B. +/// +/// 256 spec entries (A=0..25, B=0..9; A=25 only B=0..5), transcribed +/// row-for-row from ATSC A/52:2018 Table 7.14 — one A-row per source line. +/// The array is length 260 so that indices 256..259 (never reached: +/// `logadd` clamps `address = min(|c|>>1, 255)`) are padded with 0x0000. +pub const LATAB: [u16; 260] = [ + // A=0 + 0x0040, 0x003f, 0x003e, 0x003d, 0x003c, 0x003b, 0x003a, 0x0039, 0x0038, 0x0037, + // A=1 + 0x0036, 0x0035, 0x0034, 0x0034, 0x0033, 0x0032, 0x0031, 0x0030, 0x002f, 0x002f, + // A=2 + 0x002e, 0x002d, 0x002c, 0x002c, 0x002b, 0x002a, 0x0029, 0x0029, 0x0028, 0x0027, + // A=3 + 0x0026, 0x0026, 0x0025, 0x0024, 0x0024, 0x0023, 0x0023, 0x0022, 0x0021, 0x0021, + // A=4 + 0x0020, 0x0020, 0x001f, 0x001e, 0x001e, 0x001d, 0x001d, 0x001c, 0x001c, 0x001b, + // A=5 + 0x001b, 0x001a, 0x001a, 0x0019, 0x0019, 0x0018, 0x0018, 0x0017, 0x0017, 0x0016, + // A=6 + 0x0016, 0x0015, 0x0015, 0x0015, 0x0014, 0x0014, 0x0013, 0x0013, 0x0013, 0x0012, + // A=7 + 0x0012, 0x0012, 0x0011, 0x0011, 0x0011, 0x0010, 0x0010, 0x0010, 0x000f, 0x000f, + // A=8 + 0x000f, 0x000e, 0x000e, 0x000e, 0x000d, 0x000d, 0x000d, 0x000d, 0x000c, 0x000c, + // A=9 + 0x000c, 0x000c, 0x000b, 0x000b, 0x000b, 0x000b, 0x000a, 0x000a, 0x000a, 0x000a, + // A=10 + 0x000a, 0x0009, 0x0009, 0x0009, 0x0009, 0x0009, 0x0008, 0x0008, 0x0008, 0x0008, + // A=11 + 0x0008, 0x0008, 0x0007, 0x0007, 0x0007, 0x0007, 0x0007, 0x0007, 0x0006, 0x0006, + // A=12 + 0x0006, 0x0006, 0x0006, 0x0006, 0x0006, 0x0006, 0x0005, 0x0005, 0x0005, 0x0005, + // A=13 + 0x0005, 0x0005, 0x0005, 0x0005, 0x0004, 0x0004, 0x0004, 0x0004, 0x0004, 0x0004, + // A=14 + 0x0004, 0x0004, 0x0004, 0x0004, 0x0004, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + // A=15 + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0002, + // A=16 + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + // A=17 + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0001, 0x0001, + // A=18 + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + // A=19 + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + // A=20 + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + // A=21 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=22 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=23 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=24 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=25 (B=0..5) + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // pad to length 260 (indices 256..259, never indexed) + 0x0000, 0x0000, 0x0000, 0x0000, +]; + +/// Table 7.15 — Hearing Threshold Table, `hth[fscod][band]`, 50 bands × 3 fscods. +/// Row i column c gives hth[c=fscod][i=band]. +pub const HTH: [[u16; 50]; 3] = [ + // fscod = 0 (48 kHz) + [ + 0x04d0, 0x04d0, 0x0440, 0x0400, 0x03e0, 0x03c0, 0x03b0, 0x03b0, 0x03a0, 0x03a0, 0x03a0, + 0x03a0, 0x03a0, 0x0390, 0x0390, 0x0390, 0x0380, 0x0380, 0x0370, 0x0370, 0x0360, 0x0360, + 0x0350, 0x0350, 0x0340, 0x0340, 0x0330, 0x0320, 0x0310, 0x0300, 0x02f0, 0x02f0, 0x02f0, + 0x02f0, 0x0300, 0x0310, 0x0340, 0x0390, 0x03e0, 0x0420, 0x0460, 0x0490, 0x04a0, 0x0460, + 0x0440, 0x0440, 0x0520, 0x0800, 0x0840, 0x0840, + ], + // fscod = 1 (44.1 kHz) + [ + 0x04f0, 0x04f0, 0x0460, 0x0410, 0x03e0, 0x03d0, 0x03c0, 0x03b0, 0x03b0, 0x03a0, 0x03a0, + 0x03a0, 0x03a0, 0x03a0, 0x0390, 0x0390, 0x0390, 0x0380, 0x0380, 0x0380, 0x0370, 0x0370, + 0x0360, 0x0360, 0x0350, 0x0340, 0x0340, 0x0340, 0x0310, 0x0310, 0x0300, 0x02f0, 0x02f0, + 0x02f0, 0x0300, 0x0300, 0x0320, 0x0350, 0x0390, 0x03e0, 0x0420, 0x0450, 0x04a0, 0x0490, + 0x0460, 0x0440, 0x0480, 0x0630, 0x0840, 0x0840, + ], + // fscod = 2 (32 kHz) + [ + 0x0580, 0x0580, 0x04b0, 0x0450, 0x0420, 0x03f0, 0x03e0, 0x03d0, 0x03c0, 0x03b0, 0x03b0, + 0x03b0, 0x03a0, 0x03a0, 0x03a0, 0x03a0, 0x03a0, 0x03a0, 0x03a0, 0x03a0, 0x0390, 0x0390, + 0x0390, 0x0390, 0x0380, 0x0380, 0x0380, 0x0370, 0x0360, 0x0350, 0x0340, 0x0330, 0x0320, + 0x0310, 0x0300, 0x02f0, 0x02f0, 0x02f0, 0x0300, 0x0310, 0x0330, 0x0350, 0x03c0, 0x0410, + 0x0470, 0x04a0, 0x0460, 0x0440, 0x0450, 0x04e0, + ], +]; + +/// Table 7.16 — Bit Allocation Pointer Table (`baptab`), 64 entries. +/// Spec intervals: 0→0, 1..=5→1, 6..=7→2, 8..=10→3, 11..=12→4, 13..=14→5, +/// 15..=18→6, 19..=22→7, 23..=26→8, 27..=30→9, 31..=34→10, 35..=38→11, +/// 39..=42→12, 43..=46→13, 47..=54→14, 55..=63→15. +pub const BAPTAB: [u8; 64] = [ + 0, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, + 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, + 15, 15, 15, 15, 15, 15, 15, 15, 15, +]; + +/// Table 7.17/7.18 — Quantizer levels and mantissa bits per bap. +pub const QUANTIZATION_BITS: [u8; 16] = [0, 5, 7, 3, 7, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16]; + +/// Table 7.19 — bap=1 (3-level) mantissa map: -2/3, 0, 2/3. +pub const MANT_LEVEL_3: [f32; 3] = [-2.0 / 3.0, 0.0, 2.0 / 3.0]; + +/// Table 7.20 — bap=2 (5-level) mantissa map. +pub const MANT_LEVEL_5: [f32; 5] = [-4.0 / 5.0, -2.0 / 5.0, 0.0, 2.0 / 5.0, 4.0 / 5.0]; + +/// Table 7.21 — bap=3 (7-level) mantissa map. +pub const MANT_LEVEL_7: [f32; 7] = [ + -6.0 / 7.0, + -4.0 / 7.0, + -2.0 / 7.0, + 0.0, + 2.0 / 7.0, + 4.0 / 7.0, + 6.0 / 7.0, +]; + +/// Table 7.22 — bap=4 (11-level) mantissa map. +pub const MANT_LEVEL_11: [f32; 11] = [ + -10.0 / 11.0, + -8.0 / 11.0, + -6.0 / 11.0, + -4.0 / 11.0, + -2.0 / 11.0, + 0.0, + 2.0 / 11.0, + 4.0 / 11.0, + 6.0 / 11.0, + 8.0 / 11.0, + 10.0 / 11.0, +]; + +/// Table 7.23 — bap=5 (15-level) mantissa map. +pub const MANT_LEVEL_15: [f32; 15] = [ + -14.0 / 15.0, + -12.0 / 15.0, + -10.0 / 15.0, + -8.0 / 15.0, + -6.0 / 15.0, + -4.0 / 15.0, + -2.0 / 15.0, + 0.0, + 2.0 / 15.0, + 4.0 / 15.0, + 6.0 / 15.0, + 8.0 / 15.0, + 10.0 / 15.0, + 12.0 / 15.0, + 14.0 / 15.0, +]; + +/// Table 7.33 — Transform Window Sequence w[n], 256 entries, symmetric. +/// The table lists only the first 256 samples; the full 512-sample +/// window is symmetric with w[511-n] = w[n] for an MDCT window. AC-3's +/// KBD window meets the TDAC constraint that w[n]^2 + w[n+256]^2 = 1. +/// Values given to 5 decimals — accurate enough for 16-bit PCM output. +// reason: one KBD sample (0.78530) coincidentally matches FRAC_PI_4 to five +// decimals but is a tabulated window coefficient, not π/4. +#[allow(clippy::approx_constant)] +pub const WINDOW: [f32; 256] = [ + 0.00014, 0.00024, 0.00037, 0.00051, 0.00067, 0.00086, 0.00107, 0.00130, 0.00157, 0.00187, + 0.00220, 0.00256, 0.00297, 0.00341, 0.00390, 0.00443, 0.00501, 0.00564, 0.00632, 0.00706, + 0.00785, 0.00871, 0.00962, 0.01061, 0.01166, 0.01279, 0.01399, 0.01526, 0.01662, 0.01806, + 0.01959, 0.02121, 0.02292, 0.02472, 0.02662, 0.02863, 0.03073, 0.03294, 0.03527, 0.03770, + 0.04025, 0.04292, 0.04571, 0.04862, 0.05165, 0.05481, 0.05810, 0.06153, 0.06508, 0.06878, + 0.07261, 0.07658, 0.08069, 0.08495, 0.08935, 0.09389, 0.09859, 0.10343, 0.10842, 0.11356, + 0.11885, 0.12429, 0.12988, 0.13563, 0.14152, 0.14757, 0.15376, 0.16011, 0.16661, 0.17325, + 0.18005, 0.18699, 0.19407, 0.20130, 0.20867, 0.21618, 0.22382, 0.23161, 0.23952, 0.24757, + 0.25574, 0.26404, 0.27246, 0.28100, 0.28965, 0.29841, 0.30729, 0.31626, 0.32533, 0.33450, + 0.34376, 0.35311, 0.36253, 0.37204, 0.38161, 0.39126, 0.40096, 0.41072, 0.42054, 0.43040, + 0.44030, 0.45023, 0.46020, 0.47019, 0.48020, 0.49022, 0.50025, 0.51028, 0.52031, 0.53033, + 0.54033, 0.55031, 0.56026, 0.57019, 0.58007, 0.58991, 0.59970, 0.60944, 0.61912, 0.62873, + 0.63827, 0.64774, 0.65713, 0.66643, 0.67564, 0.68476, 0.69377, 0.70269, 0.71150, 0.72019, + 0.72877, 0.73723, 0.74557, 0.75378, 0.76186, 0.76981, 0.77762, 0.78530, 0.79283, 0.80022, + 0.80747, 0.81457, 0.82151, 0.82831, 0.83496, 0.84145, 0.84779, 0.85398, 0.86001, 0.86588, + 0.87160, 0.87716, 0.88257, 0.88782, 0.89291, 0.89785, 0.90264, 0.90728, 0.91176, 0.91610, + 0.92028, 0.92432, 0.92822, 0.93197, 0.93558, 0.93906, 0.94240, 0.94560, 0.94867, 0.95162, + 0.95444, 0.95713, 0.95971, 0.96217, 0.96451, 0.96674, 0.96887, 0.97089, 0.97281, 0.97463, + 0.97635, 0.97799, 0.97953, 0.98099, 0.98236, 0.98366, 0.98488, 0.98602, 0.98710, 0.98811, + 0.98905, 0.98994, 0.99076, 0.99153, 0.99225, 0.99291, 0.99353, 0.99411, 0.99464, 0.99513, + 0.99558, 0.99600, 0.99639, 0.99674, 0.99706, 0.99736, 0.99763, 0.99788, 0.99811, 0.99831, + 0.99850, 0.99867, 0.99882, 0.99895, 0.99908, 0.99919, 0.99929, 0.99938, 0.99946, 0.99953, + 0.99959, 0.99965, 0.99969, 0.99974, 0.99978, 0.99981, 0.99984, 0.99986, 0.99988, 0.99990, + 0.99992, 0.99993, 0.99994, 0.99995, 0.99996, 0.99997, 0.99998, 0.99998, 0.99998, 0.99999, + 0.99999, 0.99999, 0.99999, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, + 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, +]; + +/// Table 5.9 — Center Mix Level coefficients: cmixlev → clev. +pub const CENTER_MIX_LEVEL: [f32; 4] = [0.707, 0.595, 0.500, 0.595]; + +/// Table 5.10 — Surround Mix Level coefficients: surmixlev → slev. +pub const SURROUND_MIX_LEVEL: [f32; 4] = [0.707, 0.500, 0.0, 0.500]; + +#[cfg(test)] +mod tests { + use super::LATAB; + + /// Independently-transcribed oracle of ATSC A/52:2018 Table 7.14 + /// (`latab[val]`, val = 10*A + B, A=0..25, B=0..9; A=25 only B=0..5). + /// 256 spec entries. Guards `LATAB[0..256]` against run-length + /// regressions like issue #10 (understated mid-table runs at A=12/13/14/17 + /// caused railed full-scale bursts on multi-tone content). + #[rustfmt::skip] + const LATAB_SPEC: [u16; 256] = [ + // A=0 + 0x0040, 0x003f, 0x003e, 0x003d, 0x003c, 0x003b, 0x003a, 0x0039, 0x0038, 0x0037, + // A=1 + 0x0036, 0x0035, 0x0034, 0x0034, 0x0033, 0x0032, 0x0031, 0x0030, 0x002f, 0x002f, + // A=2 + 0x002e, 0x002d, 0x002c, 0x002c, 0x002b, 0x002a, 0x0029, 0x0029, 0x0028, 0x0027, + // A=3 + 0x0026, 0x0026, 0x0025, 0x0024, 0x0024, 0x0023, 0x0023, 0x0022, 0x0021, 0x0021, + // A=4 + 0x0020, 0x0020, 0x001f, 0x001e, 0x001e, 0x001d, 0x001d, 0x001c, 0x001c, 0x001b, + // A=5 + 0x001b, 0x001a, 0x001a, 0x0019, 0x0019, 0x0018, 0x0018, 0x0017, 0x0017, 0x0016, + // A=6 + 0x0016, 0x0015, 0x0015, 0x0015, 0x0014, 0x0014, 0x0013, 0x0013, 0x0013, 0x0012, + // A=7 + 0x0012, 0x0012, 0x0011, 0x0011, 0x0011, 0x0010, 0x0010, 0x0010, 0x000f, 0x000f, + // A=8 + 0x000f, 0x000e, 0x000e, 0x000e, 0x000d, 0x000d, 0x000d, 0x000d, 0x000c, 0x000c, + // A=9 + 0x000c, 0x000c, 0x000b, 0x000b, 0x000b, 0x000b, 0x000a, 0x000a, 0x000a, 0x000a, + // A=10 + 0x000a, 0x0009, 0x0009, 0x0009, 0x0009, 0x0009, 0x0008, 0x0008, 0x0008, 0x0008, + // A=11 + 0x0008, 0x0008, 0x0007, 0x0007, 0x0007, 0x0007, 0x0007, 0x0007, 0x0006, 0x0006, + // A=12 + 0x0006, 0x0006, 0x0006, 0x0006, 0x0006, 0x0006, 0x0005, 0x0005, 0x0005, 0x0005, + // A=13 + 0x0005, 0x0005, 0x0005, 0x0005, 0x0004, 0x0004, 0x0004, 0x0004, 0x0004, 0x0004, + // A=14 + 0x0004, 0x0004, 0x0004, 0x0004, 0x0004, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + // A=15 + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0002, + // A=16 + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + // A=17 + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0001, 0x0001, + // A=18 + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + // A=19 + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + // A=20 + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + // A=21 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=22 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=23 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=24 + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + // A=25 (B=0..5) + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + ]; + + #[test] + fn latab_matches_spec_table_7_14() { + for (val, &expected) in LATAB_SPEC.iter().enumerate() { + assert_eq!( + LATAB[val], + expected, + "LATAB[{val}] (A={}, B={}) = 0x{:04x}, spec Table 7.14 = 0x{:04x}", + val / 10, + val % 10, + LATAB[val], + expected, + ); + } + // Tail padding (indices 256..259) is never indexed by `logadd`. + for &v in &LATAB[256..260] { + assert_eq!(v, 0x0000); + } + } +} diff --git a/crates/vendor/oxideav-ac3/src/wave_order.rs b/crates/vendor/oxideav-ac3/src/wave_order.rs new file mode 100644 index 00000000..745d316c --- /dev/null +++ b/crates/vendor/oxideav-ac3/src/wave_order.rs @@ -0,0 +1,408 @@ +//! Bitstream-order → WAVE-order channel reorder. +//! +//! AC-3 / E-AC-3 transmit multichannel audio in `acmod` order (Table 5.8 / +//! Table E1.5): +//! +//! | acmod | nfchans | Bitstream slot order | +//! |-------|---------|---------------------------------| +//! | 0 | 2 | Ch1, Ch2 (dual mono 1+1) | +//! | 1 | 1 | C | +//! | 2 | 2 | L, R | +//! | 3 | 3 | L, C, R | +//! | 4 | 3 | L, R, S | +//! | 5 | 4 | L, C, R, S | +//! | 6 | 4 | L, R, Ls, Rs | +//! | 7 | 5 | L, C, R, Ls, Rs | +//! +//! When `lfeon == 1`, the LFE sample sits at slot index `nfchans` (after +//! every fbw channel — the spec treats LFE as a separate audio block, +//! but our decoder writes it interleaved as the last channel of each +//! interleaved frame). +//! +//! Consumers that interpret the decoded PCM as a WAV file (or any +//! WAVE_FORMAT_EXTENSIBLE-compliant sink — e.g. a validator binary's +//! `pcm_s16le` mux, foobar2000, miniaudio, …) expect samples laid +//! out in the `dwChannelMask` / SMPTE order: +//! +//! | bit | speaker | +//! |-----|--------------------| +//! | 0 | FRONT_LEFT (FL) | +//! | 1 | FRONT_RIGHT (FR) | +//! | 2 | FRONT_CENTER (FC) | +//! | 3 | LOW_FREQUENCY (LFE)| +//! | 4 | BACK_LEFT (BL/Ls) | +//! | 5 | BACK_RIGHT (BR/Rs) | +//! +//! AC-3's bitstream order matches WAVE order for mono (acmod=1) and +//! stereo (acmod=2) but DIFFERS for every multichannel mode — most +//! notably `acmod=7` 3/2: bitstream `(L, C, R, Ls, Rs)` versus WAV +//! `(L, R, C, Ls, Rs)`. With LFE on, this becomes bitstream +//! `(L, C, R, Ls, Rs, LFE)` versus WAV `(L, R, C, LFE, Ls, Rs)`. +//! +//! Round 6 of `oxideav-ac3` adds this conversion so the decoder's S16 +//! output is byte-for-byte identical (modulo IMDCT rounding) to the +//! reference S16LE PCM produced by black-box validator-binary decode of +//! the same fixtures; validated in the multichannel reorder integration +//! tests in `tests/`. Mono and stereo paths are a no-op. +//! +//! The same WAVE order applies to E-AC-3 (Annex E): A/52 Annex E +//! reuses the same `acmod` / `lfeon` channel-layout fields with the +//! same per-mode bitstream order as the AC-3 base layer (Annex E does +//! not redefine them), so the AC-3 reorder LUTs are correct for +//! E-AC-3 substreams without modification. + +/// Bitstream-slot index for each WAV-order output position. +/// +/// `wave_to_bitstream_map(acmod, lfeon)[wave_idx]` returns the +/// **source** slot index in the bitstream-order interleaved buffer +/// that should be copied into the **destination** slot at WAV index +/// `wave_idx`. +/// +/// The map covers every (acmod, lfeon) pair defined by Table 5.8. +/// `acmod == 0` (1+1 dual mono) is treated as plain stereo: the two +/// independent channels stay in slot order (Ch1 → WAV index 0, Ch2 → +/// WAV index 1). +/// +/// Returns a fixed-size array slot-padded with `0xFF` past the active +/// channel count. The caller uses [`output_channels`] to know how many +/// of the leading entries to consult. +pub fn wave_to_bitstream_map(acmod: u8, lfeon: bool) -> [u8; 8] { + let mut map = [0xFFu8; 8]; + match acmod { + 0 => { + // 1+1 dual mono — Ch1 → 0, Ch2 → 1 (no reorder). + map[0] = 0; + map[1] = 1; + if lfeon { + map[2] = 2; + } + } + 1 => { + // 1/0 mono — single channel, lfe (if any) at slot 1. + map[0] = 0; + if lfeon { + map[1] = 1; + } + } + 2 => { + // 2/0 stereo — bitstream (L, R) matches WAV (L, R). + map[0] = 0; + map[1] = 1; + if lfeon { + map[2] = 2; + } + } + 3 => { + // 3/0 — bitstream (L, C, R) → WAV (L, R, C). + map[0] = 0; // L + map[1] = 2; // R + map[2] = 1; // C + if lfeon { + map[3] = 3; + } + } + 4 => { + // 2/1 — bitstream (L, R, S). WAV channel-mask convention + // for "stereo + back center" is L, R, BC at bits {0, 1, + // 8}. The validator binary's AC-3 decoder maps S to the + // BACK_CENTER slot, but the mask emitted in expected.wav + // for AC-3 2/1 uses BL (bit 4) — verified empirically by + // the ac3-2-1-48000-256kbps fixture: ours-ch0=L, + // ours-ch1=R, ours-ch2=S already line up with ref + // ch0..ch2 (the 31.79 % match-pct on round 5 was the L+R + // hit, ch2 diverging because of IMDCT rounding on the S + // channel). + // We therefore leave 2/1 unpermuted: bitstream (L, R, S) + // → WAV (L, R, S). + map[0] = 0; // L + map[1] = 1; // R + map[2] = 2; // S + if lfeon { + map[3] = 3; + } + } + 5 => { + // 3/1 — bitstream (L, C, R, S) → WAV (L, R, C, S). + map[0] = 0; // L + map[1] = 2; // R + map[2] = 1; // C + map[3] = 3; // S (back center) + if lfeon { + map[4] = 4; + } + } + 6 => { + // 2/2 — bitstream (L, R, Ls, Rs) → WAV (L, R, Ls, Rs). + map[0] = 0; // L + map[1] = 1; // R + map[2] = 2; // Ls + map[3] = 3; // Rs + if lfeon { + map[4] = 4; + } + } + 7 => { + // 3/2 — bitstream (L, C, R, Ls, Rs) → WAV (L, R, C, Ls, Rs). + // With LFE on: bitstream (L, C, R, Ls, Rs, LFE) → WAV + // (L, R, C, LFE, Ls, Rs). + map[0] = 0; // L + map[1] = 2; // R + map[2] = 1; // C + if lfeon { + map[3] = 5; // LFE (last bitstream slot) + map[4] = 3; // Ls + map[5] = 4; // Rs + } else { + map[3] = 3; // Ls + map[4] = 4; // Rs + } + } + _ => { + // Out-of-range acmod: fall back to identity to preserve + // whatever the caller already had. + for (i, slot) in map.iter_mut().enumerate() { + *slot = i as u8; + } + } + } + map +} + +/// Number of active channels (`nfchans + lfeon`) for an `(acmod, lfeon)` +/// pair. Values past this index in [`wave_to_bitstream_map`]'s output +/// are sentinel `0xFF` placeholders. +pub fn output_channels(acmod: u8, lfeon: bool) -> usize { + let nfchans = match acmod { + 0 => 2, + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 3, + 5 => 4, + 6 => 4, + 7 => 5, + _ => 0, + }; + nfchans + usize::from(lfeon) +} + +/// Reorder one frame of interleaved S16LE bytes from bitstream order +/// to WAV-mask order. +/// +/// `bytes` is the input/output buffer; on entry it contains +/// `samples_per_frame × channels × 2` bytes in bitstream order. +/// On return the same buffer holds the WAV-order interleaved samples. +/// +/// Mono / stereo / 2-2 / 2-1 paths are a no-op (the WAV order matches +/// the bitstream order for those modes); see [`wave_to_bitstream_map`] +/// for the per-acmod permutation. +/// +/// `channels` MUST equal `output_channels(acmod, lfeon)` — passing a +/// mismatched value is treated as a no-op (defensive — refuses to +/// reorder if the layout doesn't match the buffer). This catches the +/// case where the caller has already downmixed (out_channels < source). +pub fn reorder_s16le_in_place(bytes: &mut [u8], acmod: u8, lfeon: bool, channels: usize) { + if channels != output_channels(acmod, lfeon) { + return; + } + if !needs_reorder(acmod, lfeon) { + return; + } + reorder_interleaved::<2>(bytes, acmod, lfeon, channels); +} + +/// Reorder one frame of interleaved f32 samples from bitstream order +/// to WAV-mask order. Used by the E-AC-3 decoder which carries f32 +/// PCM internally before packing to S16LE. +pub fn reorder_f32_in_place(samples: &mut [f32], acmod: u8, lfeon: bool, channels: usize) { + if channels != output_channels(acmod, lfeon) { + return; + } + if !needs_reorder(acmod, lfeon) { + return; + } + let map = wave_to_bitstream_map(acmod, lfeon); + let n_frames = samples.len() / channels; + let mut tmp = vec![0.0f32; channels]; + for f in 0..n_frames { + let base = f * channels; + // Snapshot the source frame so we can permute in-place. + tmp[..channels].copy_from_slice(&samples[base..base + channels]); + for wave_idx in 0..channels { + let src_idx = map[wave_idx] as usize; + samples[base + wave_idx] = tmp[src_idx]; + } + } +} + +/// True when the (acmod, lfeon) pair requires a non-identity permutation. +/// Returning `false` early lets the AC-3 / E-AC-3 hot path skip the +/// per-frame copy on stereo and mono streams (which is the common case). +/// +/// The reorder-required acmod values are exactly those that include a +/// front-center channel **and** at least one front-left/right channel: +/// 3 (3/0 = L,C,R), 5 (3/1 = L,C,R,S), and 7 (3/2 = L,C,R,Ls,Rs). +/// Mono (acmod=1) maps to a single FRONT_CENTER slot; stereo (acmod=2) +/// to (FL, FR); 2/1 (acmod=4) to (FL, FR, S); 2/2 (acmod=6) to +/// (FL, FR, BL, BR). All four already match the WAV-mask order. The +/// 1+1 dual-mono case (acmod=0) is treated as plain stereo for routing +/// purposes — no reorder. LFE on/off does not change the answer +/// because the 3-channel-front modes also need their LFE moved (slot +/// 5 in bitstream → bit 3 in WAV mask), which is handled by the same +/// permutation. +fn needs_reorder(acmod: u8, _lfeon: bool) -> bool { + matches!(acmod, 3 | 5 | 7) +} + +/// Generic in-place reorder for `BPS`-byte samples (typically 2 for +/// S16LE, 4 for S32LE or f32). The buffer is split into per-frame +/// chunks of `channels * BPS` bytes; within each chunk we permute the +/// `channels` slots according to [`wave_to_bitstream_map`]. +fn reorder_interleaved( + bytes: &mut [u8], + acmod: u8, + lfeon: bool, + channels: usize, +) { + let map = wave_to_bitstream_map(acmod, lfeon); + let frame_bytes = channels * BPS; + if frame_bytes == 0 { + return; + } + let mut tmp = [0u8; 8 * 4]; // up to 8 channels × 4 bytes per sample + for chunk in bytes.chunks_exact_mut(frame_bytes) { + // Snapshot the source frame. + tmp[..frame_bytes].copy_from_slice(chunk); + for wave_idx in 0..channels { + let src_idx = map[wave_idx] as usize; + let src_off = src_idx * BPS; + let dst_off = wave_idx * BPS; + chunk[dst_off..dst_off + BPS].copy_from_slice(&tmp[src_off..src_off + BPS]); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn map_30_permutes_lcr_to_lrc() { + let m = wave_to_bitstream_map(3, false); + // bitstream (L, C, R) at slots (0, 1, 2); wave (L, R, C). + assert_eq!(m[0], 0); // L → L + assert_eq!(m[1], 2); // R → R (was bitstream slot 2) + assert_eq!(m[2], 1); // C → C (was bitstream slot 1) + } + + #[test] + fn map_32_lfe_permutes_to_wav_51() { + let m = wave_to_bitstream_map(7, true); + // bitstream (L, C, R, Ls, Rs, LFE) at slots 0..5 + // wave (L, R, C, LFE, Ls, Rs) + assert_eq!(m[0], 0); // L + assert_eq!(m[1], 2); // R + assert_eq!(m[2], 1); // C + assert_eq!(m[3], 5); // LFE + assert_eq!(m[4], 3); // Ls + assert_eq!(m[5], 4); // Rs + } + + #[test] + fn map_22_is_identity() { + let m = wave_to_bitstream_map(6, false); + assert_eq!(m[0], 0); // L + assert_eq!(m[1], 1); // R + assert_eq!(m[2], 2); // Ls + assert_eq!(m[3], 3); // Rs + } + + #[test] + fn map_stereo_is_identity() { + let m = wave_to_bitstream_map(2, false); + assert_eq!(m[0], 0); + assert_eq!(m[1], 1); + } + + #[test] + fn output_channels_handles_lfe() { + assert_eq!(output_channels(2, false), 2); + assert_eq!(output_channels(2, true), 3); + assert_eq!(output_channels(7, true), 6); + } + + #[test] + fn needs_reorder_skips_stereo_and_mono() { + assert!(!needs_reorder(1, false)); + assert!(!needs_reorder(1, true)); + assert!(!needs_reorder(2, false)); + assert!(!needs_reorder(2, true)); + assert!(!needs_reorder(6, false)); + } + + #[test] + fn reorder_s16_30_swaps_c_and_r() { + // 3 channels × 1 frame, bitstream (L=100, C=200, R=300). + let l: i16 = 100; + let c: i16 = 200; + let r: i16 = 300; + let mut buf: Vec = Vec::new(); + buf.extend_from_slice(&l.to_le_bytes()); + buf.extend_from_slice(&c.to_le_bytes()); + buf.extend_from_slice(&r.to_le_bytes()); + reorder_s16le_in_place(&mut buf, 3, false, 3); + let v0 = i16::from_le_bytes([buf[0], buf[1]]); + let v1 = i16::from_le_bytes([buf[2], buf[3]]); + let v2 = i16::from_le_bytes([buf[4], buf[5]]); + assert_eq!(v0, 100); // L + assert_eq!(v1, 300); // R + assert_eq!(v2, 200); // C + } + + #[test] + fn reorder_s16_51_full_permutation() { + // 6 channels × 1 frame, bitstream (L,C,R,Ls,Rs,LFE) = + // (10, 20, 30, 40, 50, 60). + let mut buf: Vec = Vec::new(); + for v in [10i16, 20, 30, 40, 50, 60] { + buf.extend_from_slice(&v.to_le_bytes()); + } + reorder_s16le_in_place(&mut buf, 7, true, 6); + let mut got = [0i16; 6]; + for (i, ch) in buf.chunks_exact(2).enumerate() { + got[i] = i16::from_le_bytes([ch[0], ch[1]]); + } + // wave order (L, R, C, LFE, Ls, Rs) = (10, 30, 20, 60, 40, 50) + assert_eq!(got, [10, 30, 20, 60, 40, 50]); + } + + #[test] + fn reorder_s16_stereo_noop() { + let mut buf: Vec = Vec::new(); + for v in [11i16, 22, 33, 44] { + buf.extend_from_slice(&v.to_le_bytes()); + } + let snapshot = buf.clone(); + reorder_s16le_in_place(&mut buf, 2, false, 2); + assert_eq!(buf, snapshot); + } + + #[test] + fn reorder_s16_size_mismatch_noop() { + // channels=4 but acmod=7 lfeon=true expects 6 → no reorder. + let mut buf: Vec = Vec::new(); + for v in [11i16, 22, 33, 44] { + buf.extend_from_slice(&v.to_le_bytes()); + } + let snapshot = buf.clone(); + reorder_s16le_in_place(&mut buf, 7, true, 4); + assert_eq!(buf, snapshot); + } + + #[test] + fn reorder_f32_51_full_permutation() { + let mut buf = vec![10.0f32, 20.0, 30.0, 40.0, 50.0, 60.0]; + reorder_f32_in_place(&mut buf, 7, true, 6); + assert_eq!(buf, vec![10.0, 30.0, 20.0, 60.0, 40.0, 50.0]); + } +} diff --git a/crates/vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3 b/crates/vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3 new file mode 100644 index 0000000000000000000000000000000000000000..da51b29a4351efe71e027e070edf9b45560731ea GIT binary patch literal 12288 zcmb7~WmKEpx9x+w1$TG%0xeM7g1fr}g1Z&B;skehcc(yG+}&METU=_F_MH3S^u6QU z_n!}uk+EmS^Xy;dSZl9|=JhibPh4^r@y5b=U|^7li0I9I-H+?z>HHU8oVWWT9&ZGv zBgwCi+o$tI{U0yW?Uc87(pT_Vk3OrwQ;7?);&E)b%8d*z+if)O#n6Hu&km@{I|FXfB0uHicsr zg)uQrydDj-{O4I-S98XCFLSk6^{FG#Wn5JSHS=CLFzD2GhZ)seXN?(?}q6h zg}2Qh@0dzdN!{9!IZ3Q?eISBr(i@K2oyEUBTl3V)@+Du`uL|=iDzQrGl#gU4-$qd( zNk-%Q^0P3H`&ai!)ZKXbV^f+=_k3d}YDxNTyKIoOuu|U^m z9v~>TYKXvFvgCFqOJ>sG7_^0@p0>Ha7TIwD5U86&LVxarbefR@?2v({abLsc3{zO( zq5Nz}w6VDaDM}tz#OC!t=gs9MQcW9{xFR_p%fr7HF#up5lRkahm6ZJE&^!7|ne1C- zViZm5+WEL~lykXbmR|dvebm^Zt|h%1?ck^5Tx4oPXS{(=wSO-P8UU~b7Pmz_9rC{` zMeOsSTV&`ZNYQ=lX+mS`MKw~OcGAI&z?R7^OR~zGsa^ZbhE*)DNr^q~@YkYF0Dwsh z&8r*kfASxoSDu%0JXLYzfB!ZR)08eSWIw$TpTW(AhlHn@a>Ny%zT^j+pn{3|Qt2&= z{^qO6tmfm-xdnA;B;{?DV(yurXku$VUhn^Zcllxb+b>c7APyB(*F{48dF)eZu1}Ul zGx2z>qM?>c`15ha_I5UwABrSyTCG#EDdG;LN>E8N@YR;W?Hn&>7%KTKte1Ot$xya? zri-Ydxn^Qmd{f{6r z@tR1>aUAYlR;E0>_;o)RbQEy6)l#$)T6j|Qge5P(z@N}+6MymO9SWH>U(n#BnoJ;& zghoq@uAFQ9Q#A-+=J*PeF-?yE#$J5d+J4uY=Eq4P&0ASuRZ==Gb)@{fg0u?#tRD^Bn^n@ z#q0qlVww_gntntI-M#i_2P1obLpdC6a=gv-bQNgCIj{ML>hs;R!8xOFSDPTvK=+gl zY$l91c>J)fo-h+U+W4aUu-t>{W{4iOOrRZS)6OjUi1f5D@iZ*aJ+Ez-gd=l#JOUHY z$o$%HkHj!-ky3Wyqr&d+K^d4L66c?)K>+*3tEq8JT*s!+(J$GP%jHZ6Q3Z-J;d^RK z9obX1M8ijI!|6c6X7JKlX2BgRJ>KMAb_GTy!pP5n<)pOQapTM4_pSMBB3Q9^QTV`m z+vwy7?X8cb=YOmIb~#N3G%qKyzxl5>x;~iC7xI6ik&B}2Qx1H^k3-I2@4~vqlYczo zNlst$gvU23IOJb?%ec6myDJ3pI6={~#*Ol#!zm*?22X*zSp420)P2D*i7oW+g)^7%9iEjZdnPGZF8kQejZc5G*Hf-cr8k*Y?>-)ouzb6US`9* z>^SsDX*-E;v})6y&iP|8 zn_nSkE~%e&W8uTQ?h^Uy4QjD%ltme!8`C&PBig7=3UY=$@Kr>Os8CUt#zzS+NEg967n*GODNtlOOd2Yjm06lq?M6ePlV>A zpf-{bM-&0aptDmM8P@M3PB`f8BJIa}GY|!k#96;$QkNxX#<4!??A&m~%&x3`;APR! z8bLiR0D$Bd`89ZaY9OADDiJ@Aj-dfx1HxDewZLdFv0JlrxTxw>GEh1!H_J>;-tFz? z2JLPplX2UUV)Bq*4Wb2L%qU1zMcGsSuvg%t(a&veEt$nd?d*09?Hzg$KRh;-1!Bvg zP6KguQ30G(XkIVx{^q~pDdkW(U#L`}`-&r_q{5NLCK<2akDB$2Pla;C?_6{$lihuk zwZB+Dt$cBl#Sf&!q?O-Ucb9TMZ6>CfuCW(~`|te!5C8P5PGAMshAv-xy@$R#&58pQ z_gM|>Mv1cbK$X_Jlq2qRu8*$5ph76U)DlF?`kZc88!tWYXDY`@X;rL>eDBRw^t$oc zvhm_zaGAQu*=*wA44?Tb$GD%mMr!!{Ox5)J`=(M^D#hmw1foQ#ljB@XX*g=Tp!Fv& zs|bXis2`5E+L0*_Hq6N^g+dA~E5oYMmx#QzvZQLeI%Aed4D$(W`y?3YUAEu)E#V<# zD~A;kq!1sGZ`dMK+9k92vYjLmq`79hHb)tKPwVnA3j$!Z?m2&}K}y15J*iMv zmXWK(D2r{5r=M*(9Ql^<$Sau0HQ#6~f@sE96}w2%>_HTkw5K@V54(-%%V-}dL-;Nh z;dKEuNR!QbFZTyGlrJNbkR74JGN2wea?|u>RJ4h-M6xT7w44JG0A@OujP$EOQXbcs zY&4(I2L@wN$ zkCC6ekbZU3zqx_y4*|C<3Zk^Ln`kAq$<5dlOXy&xwXwM-s}@Hnj@2F+3c+6C*@wj` z?{s8(!fq% zN*Gn|E)D%4lz2uN)sr&oQoJZY(?{O z(fOPIa$^U`ch-RtUvyb*HtQi<6fVyU6*>-DzwR5s?eqT z%zxVvRPdBpv@BhV=0Ea(pgKV%VeqXydmS){-Q|;N9bE8+=5!k`NFeW&L^|gwIshFq zW;r!}+U|U2D7!RYTsCnhgDmL!l57&;tF0{~pTVOf4T8L!o!oW}eYrhkS@R)&GL;S* zUM0N~psr{iR)A*chsPR}0YE^!ub(aR7MWZ_D)V+1ylOOltML}|E#-@isCDx~z*L-J zGkNyX%V-5&rnE{eFE(N?I!j`=@6M22oZ468954$oqfsygSrdHD+4`2nwUzK7j|uk! zr`NsbH+FfL*Pg+rdE^-1Yjg^O0Cb4K<^n%6lC6czW(;G9iiL?PO3k@iS>I;*GWvR4KHWpw}2g#{d_dqr(qSc1AB9 zxp|^fm;&9n{xZ7EcwKiwnh2(;2qFN$rZ_MBsL(b-bDIosi?!QTz)8BSw{JhV=DH`- z6T=r^LTIPYE1Lsm?a-p&b3-qd0O}k1iX@baI8QH0%+#lkQ>P&fEbwWwv!n#M42Dj=IqGKcww8C zbadz=z9knao=aLWTNw)uPsv+=cIo3RV_RCEbgtd%_>6q>VFbGfTZmu;6TUe@n7jAw z^!QUW?`$M)8GDg*)ZS^x$GmDkh#pdj2_yo*HaZ1Vy>#|v@4=Eo+;fVh3671C$(3hK z$8jZNKSJtB(Y2E1Z*k2XWEIy(Z;sFsFV8L?x8!t@q2A?_Rgrz;XOZE8CHwV*^y^maAT0e{Cg$DVX|9S(hR^|MXz%&{~WXg9@ zj(axAlnk1+ynpyls4k62eJU9>lc?3ZYv-%1a+AM4{%oe+cRFXKV!T|I2Icv;{Qs%{ zeIxOiHfPzyz$wz#eAIu39i)$U7ykB0jRURn1pD3W-^FIA83Y!7|w4;BwhhDIY2+@4Z zm%~%<^15}l$eMno1*#(FAXX1Aap}>tiY-NN>`qLjAU(Akn#4d}xgZtU)dF%B6)4AU zZ=Sqh?2Kei24z+APfV(Etou?J4gOP<0RZUj{q{B^kXR|sY@7N3qgqonn;pJvt#TdG zymyj1VdtW!%A>Zz4}M{$R!66;p#aiKv*03@oN$#}PLrFFd;OkD!cR)uL**{Uu)0hFNnY>O~R(M zZWT>(Xaz1hyZ$3-Hnz!@)lwe{HnsmDI`_N&_ew%O&Zzxo+yS;k{t(H_&AW@7?6%Nb%BsiT@SEl_R)c;7B zWI+}MZleiv4(}ptpw|~ISKG^?rgsbWuwc|$uDG!Ssdy+sI{rs)m8w3&RLvc_#FlyT z&Gu6F%P;Trmtt?0w|KlTB)frKqDhA@{CRqpgPCd@09nIS3=n060yiSe99Z z--6AaMTI?_o1@bA{lvK2P3oqImOuB@|3ksqE`< zg}jqv4gzoj8EM+VbWtCiHjv4W9{Dz5GmaR7@QfIuM+Qj`d{nENnQ?9BZIhNLglr)5 z+8{#;lBy$;FDnl6KUbf;{V^56X2dD%Yx|Afsw(Mg)hR6ePIya;dr}A57g9+?liz~j zdbSy|3)F%D`XvVV5KOI9)A{P56O2r)Rndag0)>zg`#P;tV;L^X)l;9&5;eKa4!SW} z2Wha8%n?2}@q|>#_PjaX!#we9w)6Z?zXu*G{&nQ9*c=z!%I%&j@2fT6n%5SNHPw)K zcn82Vvvf1g1_At1oxgq6@)`Xi6-NCd^u6C(7%fJ_=rs9wcA2Hshw_LUiGwn06%K-a z)J$$nC_$}Y%|6#L0Mf!1cdergM6DNk zb&?n+*5F<5V)}gdSRCX;OO_;an&f*ONw$dVeM1^#`iObfn&TNp>!6>N9G6lvAbwn= z>1C2KPs8?ni^Oq#gZ`r4ioNgLee;8cQtw6RL?j+nw>mLDV@<}IyR<-DCl*1`NDEmb zZfWv@wL=C&{9ja6pZT|ZK=bNc{fmFSdZWqK=laM0jGSKCt}K(7QyM{zCf8)^MC9Af9iit;rnddrGf_ z%xvNHgJo`(h=Zj@v)Zyz-P0C6yVJotki)8+0!_;*sZD%z9(3MZE)^GRalHd0ul^_f z<(gWi)wK_<9S-tzAPewgMCK7%QGskqm2)IDQ+da?9{-8erER(l0Kk8v0vXnL4!M)6 z3P$~Nh?ZHEy`e0UcD9_1-i3R(%aae&_(yQusB%nO4Bl2#v%CncOYXFf(Q^+f{e2GJ z39-O>h=T2M-WhuLMH-z%&yFEiL}p>==uli7c$X+WilB;`?hdTRkPiaT5~uJ`R{G2- z_QhiBdRXQR0xt)#K{_Lawbrk!u%lMW1XtcM5ycx5Ckw|I;i|M0Yk#DZ#y;lvV1Aq5 z!^roO_-AS(JPGsq4MaqR6FJnQn*Dmv+K6Z(Gx3MwRI<^HX~6sZu?Gvh4H;g(HpuG=%KrD}PuueYtLZ1x~r zxB5lKeE$(}oqP#+=T>ocM z^kwAaEM=wb=fJ~om6N{avMQ`P*NtmyS(6*CmZilTi&i0lJw)W?aX(cMo~JfI!I|BC ziuoprIt;#QCmL44?80s=ATT>r>Fm_nwAAxV9L;J(AGy65J~(PwXeZmDLJ5Ddh9bwV zjOJx2{}=zy{y*tHm3I~(JotS6)7#)EVS|Iq`WnOFPyOSuD_mBF@5Ps|UaOFH%UNAn zs^7w-T4^1TmQ~)m(HD`)?)o3`ucEWsij=-_FiJ&J9!B+x9_dJ5)OG>LEkeGq(&(xy z<)7`lc8nZKh=C)~A*WM#p%vy>SQ3UDOXRK781nk8yHPZ%ciC1r+yIp=(t=9H&M-ON zURx`#z8Q?g**TglF0F$6`lkZc!~g&bjTBBKf<@W*hBIl!O7>`@~)tk>xqTLEPa2G@M(->EJ~2LN*Bzr#Otc z*>1tElsrT+pEVBsE|}fC0g(MdloW|604^umQ(!6>R)jDZfP7$@a1QA4%zp90kB8~A zL@b+|eQhf{0ys@)eW;ky!k#T}t!6g^CB|Z_)87)5rA^4ul>u*8wg-C<^zB@OOF1W(e~wOLGzO{)ek zx=ZEWYf`&nh`*soA}kp@YC;5I6uO-Z{V zq&9e;@@RGEQ)0Y@g+;~NWr(^I<0ZNkZR*hwIskyHc0O>AJ^K!>-bI`-5*s8=gmEns zLB}*ab##`r_dZ;a!JNIhxT>UM3S8B!rIC!TRo}!JO3*1k`IGzWiNG8GB!=VCJ%^`? zc+nQ(@8@~MqiFBpoKgFjOjhaeg61*u?CWd%_I0PB-zUTSH7@vqnLIO#Q?FRbu1 z6c?pIAMxD(0?GOnjSTdE*FV1JV*ZtEf}D`KS|g6L>FUY=kP_oezIIz#md$jsWJK!i zzvmwg5j{w|w5i=htvyLyKvwN=4MlAt5T$cMc+u>_YO+vuo(X%AHJQe&(_V_yXhlmJ z=qlXMG-Vpt5{@LUh2olWendrh1))pz&Rauzqnv=1fE%Mq)BzE24>wQlHBm4;lCKqxJz@I?3XEkO*56Uf1SN zzWe&R>GD-3T4l>U%-PUhNofDEVfKoC`2f$hf%*vdR}qpf-pg3;S&R9a`H@fcy%SV@ z;=I^oh3-vJF+>0)i;AHiA;`i33WP^*(xtW)D$o%T`xIq9)6A6VG6Cbu-}0tzu9Zrh z=51<9k;)XkO4ZHmM66*Ap|qYW=TS@Ld?-ukJS)kNV5 zO|dviZ@2tBY+dFi8{zARld|o$U?(O!i>YP~%m-AhmLzss(Y}XoR@ePZ(^xs$d${Vd zys^5v%!i1vRa^6_66y4L)WCV3))VllY61}#m7?RVfMj&zB$(vNQ{9!9-`ejrM=aVb zI}L-4V`@_2oYX=b*@qhFdhCoY3@}=WM%+y|t(8Iu+w5KP*)ch@8(!lQVVVcJpAN@B zf1Up2eN#0TkrI>d9@{D=gDHTdiV7DzL_uMDzo+jX&6s&E#$`di@J&gO9tNIYwJtWT zk~~??$zeX%CX(dS42Lq^o{uNG1Yrl^!ez4_1?8dqg}Ij8X^)j4hekt~K@&?Alsz&$rDc+8@yQ0gpsreoRpd3K+@;Uk^{}*0U&-@>UKG*;1WqXn($@na8M*mg+ zBovmGqNcjH=2y9w-c?N(Rw?Rt>3lJntxJ6>Z`l}($OQd+{(qnU+lnHKCM_{qyC==+ z49Cf)gPs~IV@8rD+N~;UMwxqKdd1IXNyBqRv3BxrB{7!$bSY z-TX&sJ+Q1e#^Z9*cUupWjY=xCR0jrv>)GORr7UGCzJP=B_8g{OpGeMqvg)<;zZG*x|Gvbl_!VyEr9Jwa%Gt^ z_@#L#3tzH7O&mC|>Eg>9F&dd$!u;M72dUq<4T;Y>;Nb+zX~MvG@W2#6MB?x|RLQE- z`#5|1yq4b3>HGUhAX=g#s3fG>f*5`BrSuDk<7bWd7>A5DYGOs@$!q>BvZSpPB!l1` z%)LO+(=c<3oj42E8iCRgiZR!<(sCU;KpcNnw&^&Dvdu_t=3Ko?U$uij+*oE!Y2zxgaW73!x@5lS|= z=RejyRY^&u#F=UitDz9nqkQzhYYGL{T1{1Eb)+|#zx5+Ex2$S5D*46xc`h^m^x6EM zACwwsUfXql@vrY8f8ljvBIJJ`dT6WhDk^Y4gNK|&E%=7zxBq+LGS}&`Nnkk&JzKrQ zbY*YZ>G|;&lg_%s8^IY9(ZDqPf6qSxDqhf=Lgzq$ypJv*A}kR=Is#yY(V~Bg3Bien zmNs#YrSejEWX$WC(9-g8jJH`&x}hH&rL)Q;UcCXtth!g@2c`4M_u}r%bDNzr_jm9s zZ{fG$op8#7eo5?n2P9uLz+j+I0R$5P+OW@i7*7Kvc)7r)SRhwoV6nA zr-LdaEe+hUwh<4cF*%KB`4W7}yAVl^uGZog&!w`uBX#eKouaIxKV=- z(z2Th7r1B~9(nRhsZ6Z#Cea~PQRbMoje$_7r%{uaHN*NNU}FYkbZ<}k48}&g3A=Y+ z1eZ(a#78wAjn}Bly6&rae=ZHbb!tWQodr&#gc^FWzy=doQno%U0|5UpaEixdx`E5l&)5g_ z*~t7dITKPP%=aD>LryW1I!;j;*n-&_n(WAS(eVO8D;!s}M29w<5VX=W3-5Pd9CIbr ztma&fVQ9{eu;Z#($AruASqBBP^D=mjN%dU^gHfQ0KizdF?{)@kJDQjE%0Kv5c+Nlk zp8x*tf7KfT_jH<`{U5vOPyOR@E*epK+cL>MSF4{2J~inr&Fa$C-0Y~!Jik}T>nrhs8ouA&Cyv!hbn7wvG$tOEzLQi_o;7UT~a+LJ%TS;|oe^ zfXuk#JS0&*Zwhqu?Y=1O#U0X6vv8vrG4F^{Fw=HMk8&@&n7z~G&_E#SK*Q7g7<&Rw zL6FCfi}>7CnMR>o$;!q`H|r)1tdDkvTQbsi3iNqGn0XyW?Q=GLBowaW@Rd%e40!5n z7*O=wO!dc=VUuvT{o3WVz+#8W>xMvX_bV9V?xTp)#dra&D)mKrc$#gsTxJPthy$sRAXZB?@yfY(w%1Jn}?1a)@-Op&a z4dOH?VZc`D!nXpRhkB?%vEGd8&#@$fK#AvA(pxQYV-YIJjHwHQ21u8El}~UMLMuH^ zgq@5=slIj+8nBgkqC!D@A0NhT$zdECvDjTDnV79J$g0J`B-U z6-j=h?^ zHLA4RtCxBYLHV?!V5K5$hqu74h6Oe-aEMJphy=F`agy5wa~o+LE`-RyUrEW7GVd>8 z+1UD5w8XA3Ok?n7GqV2Ff2FeZL_)c4^;zY~`S891))YV>QZa7}snakdQMEr-S6qk- zL}{ZR>(7ON9hmc7f3SjMd37A~6kWUoCM~4W)|&AfgC{b3>rM<;FgyfRi<`FuL0c-E zB^ne?z2pnkq#g~Q${kbZK6;QqFj~j#LlrRm&*XiR>W2UT!}x;KDvs7N>fPt?l)QA& z3$=I=wW`oh89O&?#ur`j-kztRGO-0GEEN>9>MD&3BgZqjk z;t?J=lryz6wphQ zgSyQ{!t7iZs%RE?7E!An7ygu~mDP5c5q+fNs<{~paX+{a^4CondqB(1viTI6#E!3; zFYizaGwKr7jT5PnrpbrB0F63;%72!&M2x32L`i5b#tk+&7yxj!Q@G*Iv%Lkh%m}WQ ze(j<8Vfab#c*zPmD_p6Tu1UY#Qhh0_6S+~#bU`M0r~QSEr6aNMqwy-v&=CIPc7t1^ zK`T#0Uiq|oBYCKFlsEKtK2bjTh+u!p+}A3}lpqW78($w`m`TB(`e#WN&5OhCZ~on% zk2+QU_&+HerFO&|jZPCKiyOJ{AOH8dMlZ$64#e;@tGwVQ95uc|QPid^C$3R`^AEM8rqFxgYhsIrjb0!KE|E*154?uN_ zCNw%X42nbsMQNz%<4H8YIC0RfwW?O+Z>q~Ajfn2;`Ho%9g@v#_%`qYK%H2t}D&K$( zrz4XfDVwP#%sP2xuJ0eBWdOhcOq+e<>rC{rK8V@zO{mF$#x5+diyazowCqUajir&3 zMm5$=0e;InvQ*YUH+(K#OTS|EMGV;vU!<(DZ;R^vvZhg+D>{c$P3b}niD#(={VVd7 ze~3Ph8H`*KJ+?-@E*g#FewE|9Sqxnh>U-q1K}elqKkz{VBQCUhbE7)ja)GG^&9vBw z%mPtiQWJbs2GmNBVweK^ol+O~hjke`71A&9d$BQ4s+F^}Gt$k!MXyW&pU%6z@I$H~_T`iAFDq%K=?Z)#X|9YByda0iw1Tb5b@Bsso)_^n`LJzO?U%V3 zsTvs4I$~B1*Kc5Rer$<~oK7s|e&9>xF6jM-XwV<}0RZYGG%qZs|H*$~)^j*_MY~X< z+Gzb?tblMjZ&QRAV}fORVXoBfR&dlzjOytPkEnm${}$o>{atK-2R4-)ppnWCKZGQ~ z$gweR{^mu$|5Nvg);0hp3L%3rQIGzWZxIap9Xy6A1x)%kvCW?e--1`@!~2(OPj29` zuqog3d1A_hUC^ex5o!k5ciZ|!d(T%wxLfLefk4%7m&*~OOrCp^Q zm^Vk+8xJ^Wuf7(WyiMGq@^CJ&Ku$6h+6}lVJfUfC_x}*hx#A%eU40J;$y_LJRKUP` zP7SIExbmBcG9k$)1_gy+V`EEx|F01Fo2mN$&XXrRF8+6_{QqXy*CF=$ORcr`;?JBs S?>yxB?eEWjKK*_gQvN?3&0wDZ literal 0 HcmV?d00001 diff --git a/crates/vendor/oxideav-core/Cargo.toml b/crates/vendor/oxideav-core/Cargo.toml new file mode 100644 index 00000000..40f54251 --- /dev/null +++ b/crates/vendor/oxideav-core/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "oxideav-core" +publish = false +version = "0.1.34" +edition = "2021" +rust-version = "1.80" +license = "MIT" +authors = ["Mark Karpeles"] +description = "Core types and registries for oxideav — timestamps, packets, frames, codec/container/source/filter registries (pure Rust, no C deps)" +repository = "https://github.com/OxideAV/oxideav-core" +homepage = "https://github.com/OxideAV/oxideav-core" +readme = "README.md" +keywords = ["multimedia", "audio", "video", "codec", "pure-rust"] +categories = ["multimedia", "encoding"] + +[features] +default = [] +# Retained as a no-op alias for back-compat — `serde_json` is now a +# hard dep of `oxideav-core` (the FilterFactory signature uses +# `serde_json::Value`). Consumers that previously gated on this can +# drop the feature flag entirely. +json-options = [] + +[dependencies] +thiserror = "2" +serde_json = "1" +# Used for the `Zeroable` marker trait that bounds `Arena::alloc` +# — guarantees that an all-zero bit pattern is a valid value for T, +# which is required because pool buffers are zero-filled at allocation +# time and `alloc` returns a `&mut [T]` pointing at those bytes +# (reading any other type, e.g. `NonZeroU8`, would be UB). bytemuck is +# a zero-overhead, no-deps marker-trait crate. +bytemuck = "1" + + +# Vendored verbatim — see scripts/vendor-oxideav.sh. Upstream does not build +# under this repository's `-D warnings`, and making it would mean carrying a +# patch set across every refresh. +[lints.rust] +warnings = "allow" + +[lints.clippy] +all = "allow" diff --git a/crates/vendor/oxideav-core/LICENSE b/crates/vendor/oxideav-core/LICENSE new file mode 100644 index 00000000..ffe2468a --- /dev/null +++ b/crates/vendor/oxideav-core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Karpelès Lab Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/vendor/oxideav-core/README.md b/crates/vendor/oxideav-core/README.md new file mode 100644 index 00000000..f5242ea2 --- /dev/null +++ b/crates/vendor/oxideav-core/README.md @@ -0,0 +1,132 @@ +# oxideav-core + +[![CI](https://github.com/OxideAV/oxideav-core/actions/workflows/ci.yml/badge.svg)](https://github.com/OxideAV/oxideav-core/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/oxideav-core.svg)](https://crates.io/crates/oxideav-core) [![docs.rs](https://docs.rs/oxideav-core/badge.svg)](https://docs.rs/oxideav-core) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +Core types for the [oxideav](https://github.com/OxideAV/oxideav-workspace) +pure-Rust media framework: + +* **`Packet`** — one compressed chunk belonging to one stream, with + timestamps. Chainable `with_*` builders cover every + [`PacketFlags`](crate::packet::PacketFlags) field + (`with_keyframe` / `with_header` / `with_corrupt` / `with_discard` / + `with_unit_boundary`, plus a bulk `with_flags`) and the + stream-index / time-base / pts / dts / duration setters used by + demuxers and remuxers. An `end_pts()` accessor returns the + overflow-checked `pts + duration` for muxers that need a per- + packet end timestamp. +* **`Frame`** — one uncompressed audio / video / subtitle chunk. + `VideoFrame` can carry typed in-band side-channels alongside its + pixel planes: a palette for palette-indexed (`Pal8`) content + (`palette()` / `set_palette` / `take_palette`) and a per-plane + significant-bits record for mixed depths no single `PixelFormat` + names — e.g. 12-bit luma with 10-bit chroma from a custom signal + range (`significant_bits()` / `set_significant_bits` / + `take_significant_bits`, LSB-anchored values). The two records + compose on one frame; `image_planes()` iterates pixel data + side-channel-agnostically. +* **`StreamInfo`** / **`CodecParameters`** — what a demuxer advertises and + what a decoder / encoder consumes. +* **`TimeBase`** / **`Timestamp`** / **`Rational`** — rational time per + stream; timestamps are integers in that base. Named constants + (`MILLIS` / `MICROS` / `NANOS` / `MPEG_TS` / `AUDIO_48K` / `AUDIO_44K1` + / `AUDIO_8K` / `SECONDS`) replace the workspace's `TimeBase::new(1, …)` + magic-numbers; `TimeBase::from_rate(u32)` constructs the inverse-of-rate + form, and `ticks_of(seconds: f64)` is the overflow-clamped inverse of + the existing `seconds_of(ticks)`. `Timestamp::from_seconds` / + `checked_add_ticks` / `checked_sub_ticks` / `checked_diff` / + `checked_rescale` cover per-stream timestamp arithmetic (including + cross-base differences for remux pipelines). + + The whole numeric core is **total — no panic, no silent wrap, even on + `i64::MIN` terms or zero denominators**. `rescale` computes in 128-bit + sign+magnitude space, rounds half-away-from-zero, and *saturates* at + the `i64` boundaries; `rescale_checked` returns `None` instead + wherever `rescale` would saturate or default; `rescale_rnd` takes an + explicit `Rounding` mode (`NearestAway` / `Floor` for DTS-safe stamps + / `Ceil` / `TowardZero`). `Rational` supports `+ - * /` and unary `-` + (exact via `i128` intermediates, reduced, closest-representable + approximation when even the reduced result exceeds `i64`), + `checked_add/sub/mul/div` that report `None` exactly where the + operators approximate, plus `cmp_value` / `equals_value` for value + comparison (`30000/1001` vs `30/1`) that doesn't disturb the + structural `Eq`/`Hash` callers rely on to preserve the on-wire + fraction. Property-tested against independent `i128` oracles (~200k + edge-biased cases in `tests/props.rs`). +* **`PixelFormat`** / **`SampleFormat`** — enum of supported raw formats + (70 pixel variants including 8/10/12/16-bit YUV at + 4:2:0/4:2:2/4:4:4/4:1:1/4:4:0, YUV+alpha at 4:2:0/4:2:2/4:4:4 in both + 8-bit and deep 10/12/16-bit flavours, planar GBR(A) across the full + 8/10/12/14/16-bit depth ladder with an alpha companion at every + depth, scene-referred 32-bit float gray/RGB(A)/planar-GBR(A) for + linear-light HDR, packed RGB/RGBA, gray+alpha at 8 and 16 bits, CMYK + in both ink conventions, NV12/NV21, all common sample layouts), plus + plane-geometry helpers on every variant: `chroma_subsampling()` + (log2 shifts per sampling class), `plane_dimensions()` + (ceil-division subsampled grids), and tightly-packed sizing via + `plane_row_bytes()` / `plane_size_bytes()` / `frame_size_bytes()` + with checked arithmetic. +* **`AttachedPicture`** / **`PictureType`** — ID3v2 `APIC` taxonomy + shared by ID3v2 / FLAC / MP4 / Vorbis cover-art carriage. `PictureType` + round-trips byte-for-byte through `from_u8` ↔ `to_u8` over the spec- + assigned `0x00..=0x14` range; unassigned bytes collapse to `Unknown`, + flagged via `is_known()` so strict writers can refuse to emit the + `0xFF` sentinel. `AttachedPicture::new(mime, kind)` plus chainable + `with_description` / `with_data` / `with_picture_type` builders cover + the producer side (parsers writing into a partially-decoded picture + as bytes arrive), and `is_external_link()` distinguishes ID3v2's + `"-->"` URL-sentinel mime from inline image bytes without having to + hardcode the string at every call site. +* **`CodecTag`** / **`CodecResolver`** — neutral abstraction for mapping + container-level tags (AVI FourCC, WAVEFORMATEX `wFormatTag`, MP4 OTI, + Matroska CodecID strings) to oxideav `CodecId`s. Lets codec crates own + their own tag claims without pulling a codec registry into every + container. Tag-less identification gets its own container-agnostic + path: codecs declare the payload magic prefixes they answer to + (`CodecInfo::payload_magic(b"\x01vorbis")`, `b"OpusHead"`, `b"fLaC"`, + …) and callers resolve a stream's leading payload bytes with + `CodecResolver::resolve_payload_magic(first_bytes)` — longest + matching magic wins, then registration order. Serves Ogg's BOS + packets and raw elementary-stream sniffing alike. +* **`bits`** — shared MSB-first / LSB-first `BitReader` / `BitWriter` + plus unary helpers. Used by the FLAC, AAC, H.264, HEVC, Vorbis and a + dozen other codecs in the workspace. The LSB pair (the Vorbis §2.1.4 + layout) exposes the full MSB surface — `peek_u32` (Huffman lookup + windows), `skip` / `consume`, `align_to_byte`, `read_bytes`, + positional bookkeeping, `write_bytes` and the alias set. Criterion + baselines live in `benches/primitives.rs` (~1.3 GiB/s read, + ~430 MiB/s write on a mixed-width field schedule). +* **`SourceRegistry`** — URI scheme dispatch for sources. Drivers + register as one of three shapes — `BytesSource` (file / http), + `PacketSource` (transport-layer protocols that pre-demux), or + `FrameSource` (synthetic generators that emit decoded frames) — + and `open(uri)` returns a `SourceOutput` enum the pipeline executor + branches on. +* **`Error`** — one unified error enum used across the ecosystem, with + a documented caller-action taxonomy (verdict vs starvation vs + backpressure), constructors for every string variant, and + `is_eof` / `is_need_more` / `is_starved` / `is_resource_exhausted` + predicates (the enum can't be `PartialEq` — `Io` wraps + `std::io::Error`). + +Every public item is documented (`#![warn(missing_docs)]` is enforced +at the crate root, promoted to deny by CI's clippy gate) and +`cargo doc` is warning-clean under docs.rs-strict settings. + +Zero C dependencies. Zero FFI. Zero `*-sys` crates. + +## Usage + +```toml +[dependencies] +oxideav-core = "0.1" +``` + +Everything downstream in oxideav (codec traits, container traits, codec +implementations, the CLI) depends on this crate transitively, so the +surface is kept deliberately small. The 0.1 series is the first stable +semver line — additive changes are `0.1.x` patch bumps; breaking +reshapes go to `0.2.0`. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/crates/vendor/oxideav-core/VENDOR.toml b/crates/vendor/oxideav-core/VENDOR.toml new file mode 100644 index 00000000..121dcbc7 --- /dev/null +++ b/crates/vendor/oxideav-core/VENDOR.toml @@ -0,0 +1,9 @@ +# Written by scripts/vendor-oxideav.sh. Do not edit, and do not +# hand-edit the vendored sources beside it — change them upstream +# and re-run the script. +source = "https://github.com/OxideAV/oxideav-core" +commit = "defa866dffdd224424d75ac7a38be868723395a5" +describe = "v0.1.34-1-gdefa866" +version = "0.1.34" +vendored_at = "2026-08-24T08:52:58Z" +patches = [] diff --git a/crates/vendor/oxideav-core/src/arena/mod.rs b/crates/vendor/oxideav-core/src/arena/mod.rs new file mode 100644 index 00000000..7e263993 --- /dev/null +++ b/crates/vendor/oxideav-core/src/arena/mod.rs @@ -0,0 +1,938 @@ +//! Refcounted arena pool for decoder frame allocations. +//! +//! This module is the runtime half of the DoS-protection framework +//! described in [`crate::limits`]. It provides three types: +//! +//! - [`ArenaPool`] — a pool of reusable raw byte buffers (allocated +//! via [`std::alloc::alloc`] with a fixed `MAX_ALIGN` alignment so +//! each buffer's base pointer is suitable for any `T` whose +//! alignment is `<= MAX_ALIGN`) that a decoder leases from. Pool +//! size and per-buffer capacity are fixed at construction; together +//! they bound peak RSS by construction +//! (`max_arenas × cap_per_arena`). +//! +//! - [`Arena`] — a single buffer leased from the pool. Allocations are +//! bump-pointer (no per-alloc bookkeeping, no fragmentation). When +//! the `Arena` is dropped, its buffer is returned to the pool, *not* +//! freed — this is what makes the pool memory-reusing rather than +//! memory-leaking. If the pool has been dropped before the arena +//! (last-arena-outlives-pool), the arena's buffer is freed normally. +//! +//! - [`Frame`] / [`FrameInner`] — a refcounted (`Rc`) +//! handle that holds an `Arena` plus per-plane offset/length pairs +//! and a small [`FrameHeader`]. As long as any clone of a `Frame` +//! exists, its arena (and therefore its buffer) stays out of the +//! pool. The last `Drop` returns the buffer. +//! +//! ## Design choices for round 1 +//! +//! - **Hand-rolled bump allocator** over a raw `NonNull` from +//! [`std::alloc::alloc`]. We deliberately do not depend on the +//! `bumpalo` crate yet — the logic is twenty lines and avoids +//! pulling in a dependency before profiling justifies it. The +//! signature is intentionally compatible with what a +//! `bumpalo`-backed implementation would look like, so swapping +//! later is a contained refactor. +//! +//! - **`Rc` for `Frame`, not `Arc`.** This module targets the +//! single-threaded decode path (one decoder, one consumer thread). +//! The bump-pointer cursor is `Cell` for the same reason +//! (no atomics on the hot path). For the cross-thread decode path +//! — where a decoder produces frames on one thread and a consumer +//! reads them on another — see the sibling [`sync`] module, which +//! mirrors this API 1:1 with `Arc` / atomic cursor so +//! `Frame: Send + Sync`. +//! +//! - **`Arena::alloc` returns `&mut [T]` borrowed from the arena.** +//! The borrow is bounded by the lifetime of the `&Arena` reference, +//! not the lifetime of the arena itself; the arena holds the +//! buffer's base address as a raw [`NonNull`] so multiple +//! calls to `alloc` against the same `&Arena` can each carve out +//! non-overlapping sub-slices without ever materialising a +//! whole-buffer mutable borrow (which would invalidate previously +//! returned slices under stacked borrows). This matches +//! `bumpalo::Bump::alloc_slice_*` semantics. +//! +//! ## Soundness notes +//! +//! Three issues called out by an external Miri audit (PR #12, May +//! 2026) shaped the current implementation; they are noted here so +//! future refactors don't reintroduce them: +//! +//! 1. **Base-pointer alignment.** A `Box<[u8]>` is byte-aligned only, +//! so even an empty `&mut [u32]` carved out of one would have an +//! unaligned pointer (UB). Each pool buffer is now allocated +//! directly via [`std::alloc::alloc`] with `MAX_ALIGN` (= 64 B, +//! enough for AVX-512), so the base pointer is suitable for any +//! type the arena will hand out. `alloc::` rejects types whose +//! alignment exceeds `MAX_ALIGN` at compile time via a +//! `const`-evaluated assertion. +//! +//! 2. **Invalid bit patterns.** Pool buffers are zero-filled, but +//! zero is not a valid bit pattern for every `Copy` type +//! (`NonZeroU8`, references, function pointers, niche-optimised +//! enums, …). `alloc` is therefore bounded on +//! `bytemuck::Zeroable` rather than just `Copy`, so the safe API +//! cannot hand out `&mut [NonZeroU8]` over zero bytes. +//! +//! 3. **Stacked-borrows retag.** Each `alloc` previously took +//! `[u8]::as_mut_ptr` of the whole backing slice, which retagged +//! the whole buffer and popped the borrow stacks of every +//! previously returned `&mut [T]`. The fix is the raw `NonNull` +//! base pointer above: each `alloc` does +//! `base.as_ptr().add(offset).cast::()` and never re-borrows +//! the whole buffer. + +pub mod sync; + +use std::alloc::{alloc_zeroed, dealloc, Layout}; +use std::cell::Cell; +use std::mem::{align_of, size_of}; +use std::ptr::{self, NonNull}; +use std::rc::Rc; +use std::sync::{Arc, Mutex, Weak}; + +use crate::error::{Error, Result}; +use crate::format::PixelFormat; + +/// Alignment used for every pool buffer's base pointer. 64 bytes +/// covers the alignment requirements of every primitive type and of +/// AVX-512 SIMD loads (`__m512` is 64-byte aligned). [`Arena::alloc`] +/// statically rejects any type with a stricter alignment requirement. +pub(crate) const MAX_ALIGN: usize = 64; + +/// Strict-provenance-compatible `MAX_ALIGN`-aligned dangling sentinel +/// used for the `cap == 0` empty-buffer case in [`Buffer::new_zeroed`]. +/// +/// Casting a bare integer to `*mut u8` is rejected by Miri's +/// `-Zmiri-strict-provenance` check (the resulting pointer has no +/// provenance and cannot legally be reborrowed). Taking the address of +/// a real static gives us a properly-provenanced pointer with the same +/// runtime properties (non-null, `MAX_ALIGN`-aligned, never +/// dereferenced for `cap == 0`). +/// +/// `#[repr(align(N))]` requires a literal, so the `const_assert` below +/// (a const-eval'd `assert!`) catches the day someone bumps +/// `MAX_ALIGN` without updating the literal here. +#[repr(align(64))] +struct AlignedSentinel([u8; 0]); + +const _: () = assert!( + align_of::() == MAX_ALIGN, + "AlignedSentinel alignment must match MAX_ALIGN; \ + update the #[repr(align(N))] literal on AlignedSentinel" +); + +static EMPTY_SENTINEL: AlignedSentinel = AlignedSentinel([]); + +/// Layout used to allocate (and deallocate) pool buffers. `cap` is the +/// per-arena byte capacity; alignment is fixed at `MAX_ALIGN`. +/// +/// Returns `None` for `cap == 0` — `Layout::from_size_align` rejects +/// zero-sized layouts and we can't pass a zero-sized layout to +/// `std::alloc::alloc`. Callers must special-case the empty arena. +pub(crate) fn buffer_layout(cap: usize) -> Option { + if cap == 0 { + None + } else { + Layout::from_size_align(cap, MAX_ALIGN).ok() + } +} + +/// Backing storage for one pool buffer — a raw aligned byte buffer +/// produced by [`std::alloc::alloc_zeroed`] (or a sentinel for the +/// `cap == 0` case, which doesn't allocate). Owns the allocation; +/// frees it in `Drop`. Used by both [`crate::arena::ArenaPool`] and +/// [`crate::arena::sync::ArenaPool`]. +pub(crate) struct Buffer { + /// Base pointer. For `cap > 0` this points at a live allocation + /// of `cap` bytes aligned to `MAX_ALIGN`. For `cap == 0` this is + /// a `MAX_ALIGN`-aligned dangling pointer (no backing storage). + pub(crate) ptr: NonNull, + /// Capacity of the allocation in bytes (also the layout `size`). + pub(crate) cap: usize, +} + +// SAFETY: `Buffer` owns its allocation outright (no aliasing) and +// `NonNull` is `!Send + !Sync` only out of caution; sending the +// owning handle to another thread is sound. +unsafe impl Send for Buffer {} +unsafe impl Sync for Buffer {} + +impl Buffer { + /// Allocate a buffer of `cap` bytes aligned to `MAX_ALIGN`, + /// zero-filled. For `cap == 0` returns a dangling-but-aligned + /// sentinel (matching `NonNull::dangling()` semantics for an + /// arbitrary-alignment pointer) without touching the global + /// allocator. + pub(crate) fn new_zeroed(cap: usize) -> Self { + match buffer_layout(cap) { + None => { + // Produce a `MAX_ALIGN`-aligned dangling pointer that + // is never dereferenced (cap == 0 means no allocation + // accesses go through it). We use the address of a + // real `MAX_ALIGN`-aligned static rather than an + // integer-to-pointer cast: the latter is rejected by + // Miri's `-Zmiri-strict-provenance` check (the + // resulting pointer has no provenance and cannot + // legally be reborrowed). `NonNull::from(&...)` + // preserves provenance and is strict-provenance + // friendly; the `cast::()` is also + // provenance-preserving. + Buffer { + ptr: NonNull::from(&EMPTY_SENTINEL).cast::(), + cap: 0, + } + } + Some(layout) => { + // SAFETY: layout has non-zero size (we just checked). + let raw = unsafe { alloc_zeroed(layout) }; + let ptr = + NonNull::new(raw).unwrap_or_else(|| std::alloc::handle_alloc_error(layout)); + Buffer { ptr, cap } + } + } + } + + /// Zero the entire buffer. Called when a buffer is returned to the + /// pool so a subsequent lease starts from a clean (and therefore + /// `Zeroable`-valid) state. + pub(crate) fn zero(&mut self) { + if self.cap > 0 { + // SAFETY: ptr points to `cap` bytes of writable storage we + // own exclusively (`&mut self`). + unsafe { ptr::write_bytes(self.ptr.as_ptr(), 0, self.cap) }; + } + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + if let Some(layout) = buffer_layout(self.cap) { + // SAFETY: ptr was returned by `alloc_zeroed(layout)` and + // we have not freed it yet. + unsafe { dealloc(self.ptr.as_ptr(), layout) }; + } + } +} + +/// Pool of reusable byte buffers for arena-backed frame allocations. +/// +/// Construct one per decoder via [`ArenaPool::new`]. Lease an +/// [`Arena`] per frame via [`ArenaPool::lease`]; drop the arena (or +/// drop the last clone of a [`Frame`] holding it) to return its +/// buffer to the pool. +/// +/// **Backpressure:** when all `max_arenas` slots are checked out the +/// next [`ArenaPool::lease`] returns +/// [`Error::ResourceExhausted`]. A decoder that hits this should +/// surface the error to its caller rather than busy-loop — the +/// upstream pipeline is supposed to drop frames it no longer needs, +/// which returns a buffer to the pool. +/// +/// `ArenaPool` is `Send + Sync` (the inner `Mutex>` makes it +/// safe to share across threads even though [`Arena`] / [`Frame`] +/// themselves are `!Send` due to their `Rc`/`Cell` contents). This +/// asymmetry is intentional: a parallel-decoder thread can share a +/// single pool while each thread owns its own arenas — see also the +/// sibling [`sync::ArenaPool`] whose leases are themselves `Send + Sync`. +pub struct ArenaPool { + inner: Mutex, + cap_per_arena: usize, + max_arenas: usize, + max_alloc_count_per_arena: u32, +} + +struct PoolInner { + /// Buffers currently sitting idle in the pool (ready to lease). + idle: Vec, + /// Total buffers ever allocated by this pool (idle + in-flight). + /// Caps lazy growth at `max_arenas`. + total_allocated: usize, +} + +impl ArenaPool { + /// Construct a new pool with `max_arenas` buffer slots, each of + /// `cap_per_arena` bytes. Buffers are allocated lazily on first + /// lease — a freshly constructed pool holds no memory. + /// + /// Per-arena allocation count is capped at `max_alloc_count` (use + /// [`ArenaPool::new`] which defaults to a generous 1M, or + /// [`ArenaPool::with_alloc_count_cap`] to tighten further). + pub fn new(max_arenas: usize, cap_per_arena: usize) -> Arc { + Self::with_alloc_count_cap(max_arenas, cap_per_arena, 1_000_000) + } + + /// Like [`ArenaPool::new`] but lets the caller set the per-arena + /// allocation-count cap. Useful when the caller is plumbing + /// [`crate::DecoderLimits`] through. + pub fn with_alloc_count_cap( + max_arenas: usize, + cap_per_arena: usize, + max_alloc_count_per_arena: u32, + ) -> Arc { + Arc::new(Self { + inner: Mutex::new(PoolInner { + idle: Vec::with_capacity(max_arenas), + total_allocated: 0, + }), + cap_per_arena, + max_arenas, + max_alloc_count_per_arena, + }) + } + + /// Capacity of each arena buffer this pool hands out, in bytes. + pub fn cap_per_arena(&self) -> usize { + self.cap_per_arena + } + + /// Maximum number of arenas that may be checked out at once. + pub fn max_arenas(&self) -> usize { + self.max_arenas + } + + /// Lease one arena from the pool. Returns + /// [`Error::ResourceExhausted`] if every arena slot is already + /// checked out by an [`Arena`] (or a [`Frame`] holding one). + pub fn lease(self: &Arc) -> Result { + let buffer = { + let mut inner = self.inner.lock().expect("ArenaPool mutex poisoned"); + if let Some(buf) = inner.idle.pop() { + buf + } else if inner.total_allocated < self.max_arenas { + inner.total_allocated += 1; + Buffer::new_zeroed(self.cap_per_arena) + } else { + return Err(Error::resource_exhausted(format!( + "ArenaPool exhausted: all {} arenas checked out", + self.max_arenas + ))); + } + }; + + let base = buffer.ptr; + Ok(Arena { + buffer: Cell::new(Some(buffer)), + base, + cursor: Cell::new(0), + alloc_count: Cell::new(0), + cap: self.cap_per_arena, + alloc_count_cap: self.max_alloc_count_per_arena, + pool: Arc::downgrade(self), + }) + } + + /// Return a buffer to the idle list. Called from `Arena::Drop`; + /// not part of the public API. The buffer is zeroed before being + /// returned so the next lease starts from a clean state — this is + /// what makes `Zeroable` a sufficient bound on `Arena::alloc` + /// across pool reuse cycles. + fn release(&self, mut buffer: Buffer) { + buffer.zero(); + if let Ok(mut inner) = self.inner.lock() { + inner.idle.push(buffer); + } + // If the lock is poisoned, drop the buffer normally — the + // pool is in an unusable state already. + } +} + +/// One leased buffer from an [`ArenaPool`]. +/// +/// Allocations are bump-pointer: each call to [`Arena::alloc`] carves +/// out a fresh aligned slice from the head of the buffer. There is no +/// per-allocation header and no individual free — the entire arena +/// is reset (returned to the pool) only when the `Arena` is dropped. +/// +/// `Arena` is `!Send + !Sync` because its bump cursor is a `Cell` and +/// its buffer cell is `Cell>` (not synchronised). This +/// is fine for the round-1 single-threaded decoder path. The sibling +/// [`sync::Arena`] uses `AtomicUsize` for the cursor and a `Mutex` +/// around the buffer slot to regain `Send + Sync`. +pub struct Arena { + /// Backing buffer leased from the pool. `Cell>` so + /// `Drop` can `take()` the buffer and hand it back to the pool + /// without needing `&mut self`. Outside of `Drop` this is always + /// `Some`. + /// + /// We never re-borrow this buffer mutably while handing out + /// slices from it — the typed pointers returned by `alloc` are + /// derived from the cached raw `base` pointer below, never from + /// `(*buffer).as_mut_ptr()`. This avoids the stacked-borrows + /// "whole-buffer retag invalidates previously returned slices" + /// problem. + buffer: Cell>, + /// Cached base pointer of `buffer` (a `MAX_ALIGN`-aligned + /// allocation owned by `buffer`). Stable for the lifetime of the + /// arena: `Buffer` does not move its allocation, and we only take + /// `buffer` out of the cell during `Drop` after no allocator + /// activity remains. All `alloc` calls derive their typed + /// pointers from `base.as_ptr().add(offset)`. + base: NonNull, + /// Bump cursor: the next free byte offset within the buffer. + cursor: Cell, + /// Number of allocations performed so far. + alloc_count: Cell, + /// Cached cap (== `pool.cap_per_arena` at lease time). + cap: usize, + /// Cached cap (== `pool.max_alloc_count_per_arena` at lease time). + alloc_count_cap: u32, + /// Weak handle back to the pool so `Drop` can return the buffer. + pool: Weak, +} + +impl Arena { + /// Capacity of this arena in bytes. + pub fn capacity(&self) -> usize { + self.cap + } + + /// Bytes consumed by allocations so far. + pub fn used(&self) -> usize { + self.cursor.get() + } + + /// Number of allocations performed so far. + pub fn alloc_count(&self) -> u32 { + self.alloc_count.get() + } + + /// `true` once the per-arena allocation-count cap has been + /// reached. Decoders that produce many small allocations should + /// poll this and bail with [`Error::ResourceExhausted`] when it + /// flips, instead of waiting for the next [`Arena::alloc`] call + /// to fail. + pub fn alloc_count_exceeded(&self) -> bool { + self.alloc_count.get() >= self.alloc_count_cap + } + + /// Allocate `count` `T`s out of this arena. Returns a borrowed + /// `&mut [T]` (lifetime bounded by the borrow of `self`). + /// + /// The returned slice points at zero-filled bytes (the pool + /// zero-fills on initial allocation and again whenever a buffer + /// is returned). The `Zeroable` bound on `T` guarantees that an + /// all-zero bit pattern is a valid value for `T`, so reading the + /// slice without first writing it is sound. **The intended + /// pattern is still "decoder fills the slice, then reads back + /// what it wrote" — but unwritten bytes will read back as + /// `T::zeroed()` rather than as UB.** + /// + /// Returns [`Error::ResourceExhausted`] if either the per-arena + /// byte cap or the per-arena allocation-count cap would be + /// exceeded. + /// + /// # Type bounds + /// + /// - `T: bytemuck::Zeroable` — pool buffers are zero-filled, so + /// handing back `&mut [T]` over those bytes is only sound when + /// the all-zero bit pattern is valid for `T`. This rules out + /// `NonZeroU8`/`NonZeroU16`/…/references/function pointers/ + /// niche-optimised enums (anything where the optimizer relies + /// on a forbidden-bit-pattern invariant). + /// - `align_of::() <= MAX_ALIGN` — checked at compile time via + /// a `const` assertion. The pool buffer's base pointer is + /// aligned to `MAX_ALIGN` (= 64 bytes); per-`T` alignment is + /// then a relative-offset adjustment of the bump cursor. + /// - The arena does not run destructors on allocated values, so + /// `T` should not have meaningful `Drop` glue. `Zeroable` is + /// automatically implemented only for types where this is the + /// case (primitives, `[T; N]` of zeroable, `#[derive(Zeroable)]` + /// on POD structs). + /// + /// **Aliasing model:** the bump cursor is monotonically + /// non-decreasing, so successive `alloc` calls return slices + /// covering disjoint regions of the underlying buffer. The + /// returned typed pointer is derived from the arena's cached raw + /// base pointer (`base.as_ptr().add(offset)`), never from a + /// re-borrow of the whole buffer — that's what keeps previously + /// returned `&mut [T]` slices valid under stacked borrows. This + /// is the standard arena-allocator pattern (cf. + /// `bumpalo::Bump::alloc_slice_*`) and is the reason this method + /// takes `&self` rather than `&mut self`. + #[allow(clippy::mut_from_ref)] // see "Aliasing model" doc above. + pub fn alloc(&self, count: usize) -> Result<&mut [T]> + where + T: bytemuck::Zeroable, + { + // Compile-time check: T's alignment must not exceed the + // pool buffer's base alignment. Doing this as a const-eval'd + // assert means a violating monomorphisation fails the build. + const fn assert_align() { + assert!( + align_of::() <= MAX_ALIGN, + "Arena::alloc: align_of::() exceeds MAX_ALIGN; \ + increase MAX_ALIGN in arena/mod.rs" + ); + } + const { assert_align::() }; + + // Allocation-count cap. + let next_count = + self.alloc_count.get().checked_add(1).ok_or_else(|| { + Error::resource_exhausted("Arena alloc_count overflow".to_string()) + })?; + if next_count > self.alloc_count_cap { + return Err(Error::resource_exhausted(format!( + "Arena alloc-count cap of {} exceeded", + self.alloc_count_cap + ))); + } + + let elem_size = size_of::(); + let elem_align = align_of::(); + // Bytes requested. + let bytes = elem_size + .checked_mul(count) + .ok_or_else(|| Error::resource_exhausted("Arena alloc size overflow".to_string()))?; + + // Align cursor up to T's alignment. + let cursor = self.cursor.get(); + let aligned = align_up(cursor, elem_align).ok_or_else(|| { + Error::resource_exhausted("Arena cursor alignment overflow".to_string()) + })?; + let new_cursor = aligned.checked_add(bytes).ok_or_else(|| { + Error::resource_exhausted("Arena cursor advance overflow".to_string()) + })?; + + if new_cursor > self.cap { + return Err(Error::resource_exhausted(format!( + "Arena cap of {} bytes exceeded (would consume {} bytes)", + self.cap, new_cursor + ))); + } + + // SAFETY: + // + // - `self.base` points to a `MAX_ALIGN`-aligned allocation of + // `self.cap` bytes owned by the `Buffer` inside `self.buffer`, + // which lives at least as long as `&self`. + // - `aligned + count*size_of::() <= self.cap` (just checked + // above), so the byte range we slice is in-bounds. + // - `aligned` is a multiple of `align_of::()` (computed via + // `align_up`), and `MAX_ALIGN >= align_of::()` (compile- + // time assert above), so `base + aligned` is `T`-aligned. + // This holds even for `count == 0` (the slice still has an + // aligned dangling pointer, which is what an empty `&mut [T]` + // requires). + // - The cursor is monotonically non-decreasing, so the byte + // range `aligned..new_cursor` does not overlap any byte + // range previously returned by `alloc`. We never re-borrow + // the whole buffer — the typed pointer is derived from the + // raw base pointer — so the new `&mut [T]` does not invalidate + // any previously returned slice under stacked borrows. + // - `T: Zeroable` and the buffer bytes are zero, so the + // `&mut [T]` references valid `T` values (the safe API + // contract). + let slice: &mut [T] = unsafe { + let elem_ptr = self.base.as_ptr().add(aligned).cast::(); + std::slice::from_raw_parts_mut(elem_ptr, count) + }; + + self.cursor.set(new_cursor); + self.alloc_count.set(next_count); + Ok(slice) + } + + /// Reset the arena to empty without releasing its buffer to the + /// pool. Useful for a decoder that wants to reuse the same arena + /// across several intermediate stages of the same frame. Callers + /// must ensure no slice previously returned from [`Arena::alloc`] + /// is still in use — Rust's borrow checker enforces this, since + /// `reset` takes `&mut self`. + pub fn reset(&mut self) { + self.cursor.set(0); + self.alloc_count.set(0); + } +} + +impl Drop for Arena { + fn drop(&mut self) { + // Take the buffer out of the cell. We're in Drop with `&mut + // self`, so no `alloc`-returned slices can still be borrowing + // from `base`. + if let Some(buffer) = self.buffer.take() { + if let Some(pool) = self.pool.upgrade() { + pool.release(buffer); + } else { + // Pool was dropped before us — buffer drops here and + // its allocation is freed via `Buffer::Drop`. + drop(buffer); + } + } + } +} + +/// Round `n` up to the next multiple of `align`. `align` must be a +/// power of two. Returns `None` on overflow. +fn align_up(n: usize, align: usize) -> Option { + debug_assert!(align.is_power_of_two(), "alignment must be a power of two"); + let mask = align - 1; + n.checked_add(mask).map(|m| m & !mask) +} + +/// Per-frame metadata carried alongside an [`Arena`] inside a +/// [`Frame`]. Kept minimal in round 1; round 2 will extend with +/// stride/colorspace/HDR fields as decoders need them. +/// +/// `Copy` so it travels through the hot path with no allocation. +#[non_exhaustive] +#[derive(Copy, Clone, Debug)] +pub struct FrameHeader { + /// Visible picture width in pixels. + pub width: u32, + /// Visible picture height in pixels. + pub height: u32, + /// Pixel format of the plane data in the arena. + pub pixel_format: PixelFormat, + /// Presentation timestamp in stream time-base units. `None` when + /// the codec did not surface one (e.g. a still image). + pub presentation_timestamp: Option, +} + +impl FrameHeader { + /// Construct a header with all four mandatory fields set. Use + /// functional-update syntax (`FrameHeader { ..header }`) to add + /// future fields safely. + pub fn new( + width: u32, + height: u32, + pixel_format: PixelFormat, + presentation_timestamp: Option, + ) -> Self { + Self { + width, + height, + pixel_format, + presentation_timestamp, + } + } +} + +/// Maximum number of planes a [`FrameInner`] can describe in round 1. +/// Covers every real-world video pixel format (1 plane for packed +/// RGB/YUV 4:2:2, 3 planes for I420/YV12/I444, 4 planes for YUVA / RGBA +/// planar). Audio is handled by a separate sibling type in a future +/// round; this module is video-only for now. +pub const MAX_PLANES: usize = 4; + +/// The owned body of a refcounted [`Frame`]. +/// +/// Holds an [`Arena`] (the bytes), a fixed-size table of +/// `(offset_in_arena, length_in_bytes)` pairs (one per plane), and a +/// [`FrameHeader`]. The `plane_count` field tracks how many entries of +/// `plane_offsets` are actually populated. Up to [`MAX_PLANES`] planes +/// are supported. +/// +/// **Lifetime:** an `Arena` returns its buffer to the pool when +/// dropped. A `Rc` keeps the arena alive via its single +/// owned field, so as long as any clone of a [`Frame`] exists the +/// underlying buffer stays out of the pool. +pub struct FrameInner { + arena: Arena, + plane_offsets: [(usize, usize); MAX_PLANES], + plane_count: u8, + header: FrameHeader, +} + +/// Refcounted handle to a decoded video frame. Construct via +/// [`Frame::new`]; clone freely (each clone bumps the refcount by 1). +/// The arena and its buffer are released back to the pool when the +/// last clone is dropped. +/// +/// `Frame` is `Rc` (single-threaded decoder path). For the +/// cross-thread decode path — where the consumer runs on a different +/// thread from the decoder — use the sibling [`sync::Frame`] which is +/// `Arc` and is `Send + Sync`. +pub type Frame = Rc; + +impl FrameInner { + /// Construct a `Frame` (refcounted `Rc`) from an arena, + /// a slice of `(offset, length)` plane descriptors, and a header. + /// Returns [`Error::InvalidData`] if more than [`MAX_PLANES`] + /// planes are supplied or if any plane range falls outside the + /// arena's used region. + pub fn new(arena: Arena, planes: &[(usize, usize)], header: FrameHeader) -> Result { + if planes.len() > MAX_PLANES { + return Err(Error::invalid(format!( + "FrameInner supports at most {} planes (got {})", + MAX_PLANES, + planes.len() + ))); + } + let used = arena.used(); + for (i, (off, len)) in planes.iter().enumerate() { + let end = off + .checked_add(*len) + .ok_or_else(|| Error::invalid(format!("plane {i}: offset+len overflow")))?; + if end > used { + return Err(Error::invalid(format!( + "plane {i}: range {off}..{end} exceeds arena used={used}" + ))); + } + } + let mut plane_offsets = [(0usize, 0usize); MAX_PLANES]; + for (i, p) in planes.iter().enumerate() { + plane_offsets[i] = *p; + } + Ok(Rc::new(FrameInner { + arena, + plane_offsets, + plane_count: planes.len() as u8, + header, + })) + } + + /// Number of planes this frame holds. + pub fn plane_count(&self) -> usize { + self.plane_count as usize + } + + /// Read-only access to plane `i`. Returns `None` if `i` is out of + /// range. + pub fn plane(&self, i: usize) -> Option<&[u8]> { + if i >= self.plane_count as usize { + return None; + } + let (off, len) = self.plane_offsets[i]; + // SAFETY: + // - plane ranges were validated against `arena.used()` at + // construction (`off + len <= arena.cursor`), and the + // cursor is monotonically non-decreasing, so the byte + // range is still in-bounds. + // - The bytes were written by `alloc` and never moved (the + // buffer's allocation is stable for the arena's lifetime). + // - We derive the slice from the raw base pointer, never via + // a re-borrow of the whole buffer, so this `&[u8]` does not + // invalidate any other slice the caller is holding. + // - The borrow lifetime is bounded by `&self`. + let buf: &[u8] = unsafe { + let elem_ptr = self.arena.base.as_ptr().add(off); + std::slice::from_raw_parts(elem_ptr, len) + }; + Some(buf) + } + + /// Frame header (width / height / pixel format / pts). + pub fn header(&self) -> &FrameHeader { + &self.header + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn small_pool(slots: usize, cap: usize) -> Arc { + ArenaPool::new(slots, cap) + } + + #[test] + fn pool_lease_returns_err_when_exhausted() { + let pool = small_pool(2, 1024); + let a = pool.lease().expect("first lease"); + let b = pool.lease().expect("second lease"); + let third = pool.lease(); + assert!(matches!(third, Err(Error::ResourceExhausted(_)))); + // Keep a and b alive past the assertion so they aren't dropped + // before the failing lease. + drop((a, b)); + } + + #[test] + fn arena_alloc_caps_at_size_limit() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + // 64 bytes capacity. Allocate 32 u8s — fits. + let _: &mut [u8] = arena.alloc::(32).unwrap(); + // Allocate another 32 u8s — exactly fills. + let _: &mut [u8] = arena.alloc::(32).unwrap(); + // Any further allocation fails. + let third = arena.alloc::(1); + assert!(matches!(third, Err(Error::ResourceExhausted(_)))); + } + + #[test] + fn arena_alloc_count_cap_fires() { + let pool = ArenaPool::with_alloc_count_cap(1, 1024, 3); + let arena = pool.lease().unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + assert!(arena.alloc_count_exceeded()); + let fourth = arena.alloc::(1); + assert!(matches!(fourth, Err(Error::ResourceExhausted(_)))); + } + + #[test] + fn arena_returns_to_pool_on_drop() { + let pool = small_pool(1, 256); + { + let arena = pool.lease().expect("first lease"); + // Sanity: arena is leased; further leases would fail. + assert!(matches!(pool.lease(), Err(Error::ResourceExhausted(_)))); + drop(arena); + } + // Arena dropped — pool slot must be free again. + let _again = pool.lease().expect("re-lease after drop"); + } + + #[test] + fn arena_alignment_is_respected() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + // Allocate a single u8 to misalign the cursor. + let _: &mut [u8] = arena.alloc::(1).unwrap(); + // Now allocate u32s; expect cursor to be aligned to 4. + let s: &mut [u32] = arena.alloc::(4).unwrap(); + let addr = s.as_ptr() as usize; + assert_eq!(addr % align_of::(), 0); + assert_eq!(s.len(), 4); + } + + #[cfg(miri)] + #[test] + fn arena_alloc_can_return_misaligned_typed_slice() { + let pool = small_pool(1, 0); + let arena = pool.lease().unwrap(); + + // Memory-safety issue: the arena's backing allocation is a + // `Box<[u8]>`, so its base pointer is only guaranteed to be + // byte-aligned. `alloc::` aligns only the byte offset, not + // the absolute address, and then constructs `&mut [T]`. The + // empty-buffer case makes this deterministic: even an empty + // `&mut [u32]` must have an aligned pointer, but `Box<[u8]>` + // uses an alignment-1 dangling pointer when its length is 0. + let _s: &mut [u32] = arena.alloc::(0).unwrap(); + } + + // Pre-fix this test was: + // + // let values = arena.alloc::(1).unwrap(); + // let _ = values[0].get(); + // + // and failed under Miri because pool buffers are zero-filled and + // zero is not a valid `NonZeroU8`. Post-fix, the `Zeroable` bound + // on `Arena::alloc` makes that call a hard *compile* error — the + // strongest possible enforcement. The test below is a regression + // assertion that the bound stays as-or-stricter than `Zeroable`: + // if a future refactor weakened it back to `Copy`, the + // commented-out call site would start compiling again and Miri + // would once again accept the invalid bit pattern. + #[cfg(miri)] + #[test] + fn arena_alloc_allows_invalid_bit_patterns_for_copy_types() { + // `requires_zeroable::()` would not compile — + // `NonZeroU8: !Zeroable`. Sanity-check the helper itself with + // a known zeroable type so the test is an actual exercise. + fn requires_zeroable() {} + requires_zeroable::(); + // Uncommenting the next line must fail to compile: + // requires_zeroable::(); + } + + #[cfg(miri)] + #[test] + fn arena_alloc_second_slice_invalidates_first_mut_reference() { + let pool = small_pool(1, 2); + let arena = pool.lease().unwrap(); + + // Memory-safety issue: each `alloc` calls `[u8]::as_mut_ptr` on + // the whole backing slice before carving out the requested + // subslice. That materializes a new mutable borrow of the whole + // buffer and invalidates previously returned `&mut` slices, even + // when the byte ranges are disjoint. + let first = arena.alloc::(1).unwrap(); + let second = arena.alloc::(1).unwrap(); + first[0] = 1; + second[0] = 2; + } + + fn build_simple_frame(pool: &Arc) -> Frame { + let arena = pool.lease().unwrap(); + // Allocate 16 bytes for plane 0. + let plane0: &mut [u8] = arena.alloc::(16).unwrap(); + for (i, b) in plane0.iter_mut().enumerate() { + *b = i as u8; + } + // The slice borrowed from arena ends here. + let header = FrameHeader::new(4, 4, PixelFormat::Gray8, Some(42)); + FrameInner::new(arena, &[(0, 16)], header).unwrap() + } + + #[test] + fn frame_refcount_keeps_arena_alive() { + let pool = small_pool(1, 256); + let frame = build_simple_frame(&pool); + let clone = Rc::clone(&frame); + drop(frame); + // Clone is still valid; arena still leased. + let plane = clone.plane(0).expect("plane 0"); + assert_eq!(plane.len(), 16); + for (i, b) in plane.iter().enumerate() { + assert_eq!(*b, i as u8); + } + assert_eq!(clone.header().width, 4); + assert_eq!(clone.header().height, 4); + assert_eq!(clone.header().presentation_timestamp, Some(42)); + // Pool still exhausted because clone holds the arena. + assert!(matches!(pool.lease(), Err(Error::ResourceExhausted(_)))); + } + + #[test] + fn last_drop_returns_arena_to_pool() { + let pool = small_pool(1, 256); + let frame = build_simple_frame(&pool); + let clone = Rc::clone(&frame); + drop(frame); + drop(clone); + // All clones gone — buffer must be back in the pool. + let _again = pool.lease().expect("lease after last drop"); + } + + #[test] + fn frame_rejects_too_many_planes() { + let pool = small_pool(1, 256); + let arena = pool.lease().unwrap(); + let header = FrameHeader::new(1, 1, PixelFormat::Gray8, None); + let too_many = vec![(0usize, 0usize); MAX_PLANES + 1]; + let r = FrameInner::new(arena, &too_many, header); + assert!(matches!(r, Err(Error::InvalidData(_)))); + } + + #[test] + fn frame_rejects_plane_outside_arena() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + // arena.used() == 0; any non-empty plane is out of range. + let header = FrameHeader::new(1, 1, PixelFormat::Gray8, None); + let r = FrameInner::new(arena, &[(0, 16)], header); + assert!(matches!(r, Err(Error::InvalidData(_)))); + } + + #[test] + fn pool_outlives_buffer_drop_when_pool_dropped_first() { + // Exotic: arena outlives its pool. Buffer just frees normally. + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + drop(pool); + // Drop arena — must not panic. The Weak handle won't upgrade. + drop(arena); + } + + #[test] + fn arena_reset_clears_allocations() { + let pool = small_pool(1, 32); + let mut arena = pool.lease().unwrap(); + let _: &mut [u8] = arena.alloc::(32).unwrap(); + // Cap reached. + assert!(matches!( + arena.alloc::(1), + Err(Error::ResourceExhausted(_)) + )); + arena.reset(); + // After reset we can allocate again. + let _: &mut [u8] = arena.alloc::(32).unwrap(); + } +} diff --git a/crates/vendor/oxideav-core/src/arena/sync.rs b/crates/vendor/oxideav-core/src/arena/sync.rs new file mode 100644 index 00000000..9f938db3 --- /dev/null +++ b/crates/vendor/oxideav-core/src/arena/sync.rs @@ -0,0 +1,873 @@ +//! `Send + Sync` mirror of the parent [`crate::arena`] module. +//! +//! This module exposes the same four-type API ([`ArenaPool`], +//! [`Arena`], [`Frame`], [`FrameInner`]) as its sibling, with one +//! difference that ripples through the whole shape: +//! +//! - [`Arena`] uses `AtomicUsize` / `AtomicU32` for its bump cursor +//! and allocation counter (instead of `Cell` / `Cell`), +//! and is therefore `Send + Sync`. +//! - [`Frame`] is `Arc` (instead of `Rc`), +//! so a decoded frame can be moved or shared across threads. +//! - [`FrameInner`] holds a sync [`Arena`], so it is itself `Send + +//! Sync` and `Arc: Send + Sync` falls out for free. +//! +//! ## When to use which +//! +//! Use [`crate::arena`] (the `Rc` variant) when the decoder produces +//! frames on the same thread that consumes them. The bump cursor is +//! a plain `Cell` and there are no atomic operations on the +//! hot allocation path. +//! +//! Use this module (the `Arc` variant) when the decoder hands frames +//! to a different thread — the typical case for a pipeline that +//! decodes on one worker and renders / encodes / transmits on +//! another. The cost is a relaxed atomic load + CAS per allocation +//! and an atomic refcount per frame clone; both are negligible +//! compared to the actual decode work. +//! +//! ## Concurrent allocation contract +//! +//! [`Arena::alloc`] uses a CAS loop on the cursor, so two threads +//! that both call [`Arena::alloc`] on the same `&Arena` will receive +//! disjoint slices (the loser of the CAS retries against the new +//! cursor). The returned `&mut [T]` points into a region that no +//! other in-flight `alloc()` call can also receive, and the slice's +//! lifetime is bounded by the borrow of `&self`. +//! +//! In practice the typical pattern is **one decoder thread allocates, +//! then freezes into a [`Frame`] which is shared read-only across +//! threads** — concurrent allocation is supported but rarely useful. +//! The bytes returned by [`Arena::alloc`] are not zero-initialised; +//! callers must fully overwrite them before reading. +//! +//! Everything else (per-arena byte cap, per-arena allocation-count +//! cap, weak handle back to the pool for `Drop`-time release, the +//! `FrameHeader` shape, plane validation in [`FrameInner::new`]) +//! matches the parent module exactly. + +use std::mem::{align_of, size_of}; +use std::ptr::NonNull; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, Weak}; + +use crate::error::{Error, Result}; + +// Re-export the shared `FrameHeader` and `MAX_PLANES` constant so +// users of either arena module see the same metadata shape — there is +// no thread-safety angle to either of them, and duplicating them +// would only add drift. +pub use super::{FrameHeader, MAX_PLANES}; +// `Buffer` and `MAX_ALIGN` are shared with the parent module — the +// pool-backing storage and the soundness-critical alignment constant +// are identical for the `Rc` and `Arc` variants. See `arena/mod.rs` +// for the full soundness rationale. +use super::{Buffer, MAX_ALIGN}; + +/// `Send + Sync` pool of reusable byte buffers for arena-backed frame +/// allocations. Mirrors [`crate::arena::ArenaPool`] in shape and +/// behaviour; the only difference is that the [`Arena`] (and the +/// [`Frame`] holding it) handed out are themselves `Send + Sync`. +/// +/// Construct via [`ArenaPool::new`]. Lease an [`Arena`] per frame via +/// [`ArenaPool::lease`]; drop the arena (or drop the last clone of a +/// [`Frame`] holding it) to return its buffer to the pool. +pub struct ArenaPool { + inner: Mutex, + cap_per_arena: usize, + max_arenas: usize, + max_alloc_count_per_arena: u32, +} + +struct PoolInner { + /// Buffers currently sitting idle in the pool (ready to lease). + idle: Vec, + /// Total buffers ever allocated by this pool (idle + in-flight). + /// Caps lazy growth at `max_arenas`. + total_allocated: usize, +} + +impl ArenaPool { + /// Construct a new pool with `max_arenas` buffer slots, each of + /// `cap_per_arena` bytes. Buffers are allocated lazily on first + /// lease — a freshly constructed pool holds no memory. + /// + /// Per-arena allocation count is capped at a generous 1 M + /// (override via [`ArenaPool::with_alloc_count_cap`]). + pub fn new(max_arenas: usize, cap_per_arena: usize) -> Arc { + Self::with_alloc_count_cap(max_arenas, cap_per_arena, 1_000_000) + } + + /// Like [`ArenaPool::new`] but lets the caller set the per-arena + /// allocation-count cap. Useful when the caller is plumbing + /// [`crate::DecoderLimits`] through. + pub fn with_alloc_count_cap( + max_arenas: usize, + cap_per_arena: usize, + max_alloc_count_per_arena: u32, + ) -> Arc { + Arc::new(Self { + inner: Mutex::new(PoolInner { + idle: Vec::with_capacity(max_arenas), + total_allocated: 0, + }), + cap_per_arena, + max_arenas, + max_alloc_count_per_arena, + }) + } + + /// Capacity of each arena buffer this pool hands out, in bytes. + pub fn cap_per_arena(&self) -> usize { + self.cap_per_arena + } + + /// Maximum number of arenas that may be checked out at once. + pub fn max_arenas(&self) -> usize { + self.max_arenas + } + + /// Lease one arena from the pool. Returns + /// [`Error::ResourceExhausted`] if every arena slot is already + /// checked out by an [`Arena`] (or a [`Frame`] holding one). + pub fn lease(self: &Arc) -> Result { + let buffer = { + let mut inner = self.inner.lock().expect("ArenaPool mutex poisoned"); + if let Some(buf) = inner.idle.pop() { + buf + } else if inner.total_allocated < self.max_arenas { + inner.total_allocated += 1; + Buffer::new_zeroed(self.cap_per_arena) + } else { + return Err(Error::resource_exhausted(format!( + "ArenaPool exhausted: all {} arenas checked out", + self.max_arenas + ))); + } + }; + + let base = buffer.ptr; + Ok(Arena { + buffer: Mutex::new(Some(buffer)), + base, + cursor: AtomicUsize::new(0), + alloc_count: AtomicU32::new(0), + cap: self.cap_per_arena, + alloc_count_cap: self.max_alloc_count_per_arena, + pool: Arc::downgrade(self), + }) + } + + /// Return a buffer to the idle list. Called from `Arena::Drop`; + /// not part of the public API. The buffer is zeroed before being + /// returned so the next lease starts from a clean state — this is + /// what makes `Zeroable` a sufficient bound on `Arena::alloc` + /// across pool reuse cycles. + fn release(&self, mut buffer: Buffer) { + buffer.zero(); + if let Ok(mut inner) = self.inner.lock() { + inner.idle.push(buffer); + } + // If the lock is poisoned, drop the buffer normally — the + // pool is in an unusable state already. + } +} + +/// One leased buffer from a [`ArenaPool`]. `Send + Sync`. +/// +/// Allocations are bump-pointer on an atomic cursor: each call to +/// [`Arena::alloc`] CAS-advances the cursor and returns a fresh +/// aligned slice carved out of the buffer at the old position. There +/// is no per-allocation header and no individual free — the entire +/// arena is reset (returned to the pool) only when the `Arena` is +/// dropped. +/// +/// Concurrent calls to [`Arena::alloc`] on the same `&Arena` are +/// supported and produce disjoint slices (the CAS loser retries +/// against the new cursor). See the module docs for the full +/// concurrency contract. +pub struct Arena { + /// Backing buffer leased from the pool. Stored in a `Mutex` so + /// `Drop` can `take()` the buffer without needing direct + /// `UnsafeCell` access (which would re-borrow the whole storage + /// and invalidate previously-returned slices under stacked + /// borrows). Outside of `Drop` this is always `Some` — the + /// mutex itself is essentially uncontended (nothing else touches + /// it on the hot path). + /// + /// We never re-borrow this buffer mutably while handing out + /// slices from it — the typed pointers returned by `alloc` are + /// derived from the cached raw `base` pointer below, never from + /// a fresh borrow of the whole storage. This is what avoids the + /// stacked-borrows whole-buffer-retag race that Miri reported + /// when two threads called `alloc` concurrently while a third + /// held a previously-returned `&mut [T]`. + buffer: Mutex>, + /// Cached base pointer of `buffer` (a `MAX_ALIGN`-aligned + /// allocation owned by `buffer`). Stable for the lifetime of the + /// arena: `Buffer` does not move its allocation, and we only take + /// `buffer` out of the mutex during `Drop` after no allocator + /// activity remains. All `alloc` calls derive their typed + /// pointers from `base.as_ptr().add(offset)`. + base: NonNull, + /// Atomic bump cursor: the next free byte offset within the + /// buffer. + cursor: AtomicUsize, + /// Atomic allocation counter. + alloc_count: AtomicU32, + /// Cached cap (== `pool.cap_per_arena` at lease time). + cap: usize, + /// Cached cap (== `pool.max_alloc_count_per_arena` at lease time). + alloc_count_cap: u32, + /// Weak handle back to the pool so `Drop` can return the buffer. + pool: Weak, +} + +// SAFETY: `Arena` owns its buffer's allocation outright (no shared +// ownership), all cursor/count mutations go through atomics, and the +// raw `base` pointer is only used to derive disjoint typed slices +// whose ranges the CAS loop guarantees not to overlap. The `Drop` +// path takes the buffer out of the mutex under `&mut self` — no +// other thread can be in `alloc` at that point. +unsafe impl Send for Arena {} +// SAFETY: `&Arena::alloc` mutates only via the atomic cursor and the +// allocation counter (themselves `Sync`) and writes into a region of +// the buffer that no other in-flight call has been handed (CAS +// guarantees disjoint regions). The raw `base` pointer is never used +// to materialise a whole-buffer mutable borrow, so a new `alloc` +// call cannot invalidate any other thread's previously returned +// `&mut [T]` slice under stacked borrows. +unsafe impl Sync for Arena {} + +impl Arena { + /// Capacity of this arena in bytes. + pub fn capacity(&self) -> usize { + self.cap + } + + /// Bytes consumed by allocations so far. + pub fn used(&self) -> usize { + self.cursor.load(Ordering::Acquire) + } + + /// Number of allocations performed so far. + pub fn alloc_count(&self) -> u32 { + self.alloc_count.load(Ordering::Acquire) + } + + /// `true` once the per-arena allocation-count cap has been + /// reached. Decoders that produce many small allocations should + /// poll this and bail with [`Error::ResourceExhausted`] when it + /// flips, instead of waiting for the next [`Arena::alloc`] call + /// to fail. + pub fn alloc_count_exceeded(&self) -> bool { + self.alloc_count.load(Ordering::Acquire) >= self.alloc_count_cap + } + + /// Allocate `count` `T`s out of this arena. Returns a borrowed + /// `&mut [T]` (lifetime bounded by the borrow of `self`). + /// + /// The returned slice points at zero-filled bytes (the pool + /// zero-fills on initial allocation and again whenever a buffer + /// is returned). The `Zeroable` bound on `T` guarantees that an + /// all-zero bit pattern is a valid value for `T`, so reading the + /// slice without first writing it is sound. The intended pattern + /// is still "decoder fills the slice, then reads back what it + /// wrote" — but unwritten bytes will read back as `T::zeroed()` + /// rather than as UB. + /// + /// Returns [`Error::ResourceExhausted`] if either the per-arena + /// byte cap or the per-arena allocation-count cap would be + /// exceeded. + /// + /// # Type bounds + /// + /// - `T: bytemuck::Zeroable` — pool buffers are zero-filled, so + /// handing back `&mut [T]` over those bytes is only sound when + /// the all-zero bit pattern is valid for `T`. This rules out + /// `NonZeroU8`/`NonZeroU16`/…/references/function pointers/ + /// niche-optimised enums. + /// - `align_of::() <= MAX_ALIGN` — checked at compile time via + /// a `const` assertion. The pool buffer's base pointer is + /// aligned to `MAX_ALIGN` (= 64 bytes); per-`T` alignment is + /// then a relative-offset adjustment of the bump cursor. + /// - The arena does not run destructors on allocated values, so + /// `T` should not have meaningful `Drop` glue. + /// + /// **Concurrency:** the bump cursor is advanced via a CAS loop, + /// so concurrent `alloc` calls on the same `&Arena` produce + /// disjoint slices. The CAS loser retries against the new + /// cursor; in the uncontended case the cost is a single relaxed + /// load plus one successful CAS. Crucially, no `alloc` call + /// re-borrows the whole buffer (the typed pointer is derived + /// from the cached raw base pointer), so concurrent allocators + /// cannot invalidate each other's previously-returned slices + /// under stacked borrows. + #[allow(clippy::mut_from_ref)] // see "Concurrency" doc above. + pub fn alloc(&self, count: usize) -> Result<&mut [T]> + where + T: bytemuck::Zeroable, + { + // Compile-time check: T's alignment must not exceed the + // pool buffer's base alignment. Doing this as a const-eval'd + // assert means a violating monomorphisation fails the build. + const fn assert_align() { + assert!( + align_of::() <= MAX_ALIGN, + "Arena::alloc: align_of::() exceeds MAX_ALIGN; \ + increase MAX_ALIGN in arena/mod.rs" + ); + } + const { assert_align::() }; + + // Allocation-count cap. Increment first; if we overshoot, + // roll back so subsequent calls still see the correct value. + let prev_count = self.alloc_count.fetch_add(1, Ordering::AcqRel); + if prev_count >= self.alloc_count_cap { + // Roll back so `alloc_count_exceeded()` keeps returning + // a stable cap value rather than drifting upward. + self.alloc_count.fetch_sub(1, Ordering::AcqRel); + return Err(Error::resource_exhausted(format!( + "Arena alloc-count cap of {} exceeded", + self.alloc_count_cap + ))); + } + + let elem_size = size_of::(); + let elem_align = align_of::(); + // Bytes requested. + let bytes = elem_size.checked_mul(count).ok_or_else(|| { + // Roll back the alloc-count bump on size-overflow too. + self.alloc_count.fetch_sub(1, Ordering::AcqRel); + Error::resource_exhausted("Arena alloc size overflow".to_string()) + })?; + + // CAS loop on the cursor. We compute aligned + new_cursor + // from the latest observed cursor value, then attempt to + // claim that range; if another thread won the race, retry + // against the updated cursor. + let mut current = self.cursor.load(Ordering::Acquire); + let aligned; + let new_cursor; + loop { + let candidate_aligned = match align_up(current, elem_align) { + Some(a) => a, + None => { + self.alloc_count.fetch_sub(1, Ordering::AcqRel); + return Err(Error::resource_exhausted( + "Arena cursor alignment overflow".to_string(), + )); + } + }; + let candidate_new = match candidate_aligned.checked_add(bytes) { + Some(n) => n, + None => { + self.alloc_count.fetch_sub(1, Ordering::AcqRel); + return Err(Error::resource_exhausted( + "Arena cursor advance overflow".to_string(), + )); + } + }; + + if candidate_new > self.cap { + self.alloc_count.fetch_sub(1, Ordering::AcqRel); + return Err(Error::resource_exhausted(format!( + "Arena cap of {} bytes exceeded (would consume {} bytes)", + self.cap, candidate_new + ))); + } + + match self.cursor.compare_exchange_weak( + current, + candidate_new, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + aligned = candidate_aligned; + new_cursor = candidate_new; + let _ = new_cursor; // silence "unused" if optimised + break; + } + Err(observed) => { + current = observed; + // Retry with the freshly observed cursor. + } + } + } + + // SAFETY: + // + // - `self.base` points to a `MAX_ALIGN`-aligned allocation of + // `self.cap` bytes owned by the `Buffer` inside + // `self.buffer`, which lives at least as long as `&self`. + // - We just CAS-claimed the byte range `aligned..new_cursor`, + // so no other in-flight `alloc` call can claim any byte + // inside it (the cursor is monotonically non-decreasing + // under successful CAS, so a subsequent winner observes a + // `current` >= our `new_cursor`). + // - `aligned + count*size_of::() <= self.cap` (just checked + // above), so the byte range is in-bounds of the allocation. + // - `aligned` is a multiple of `align_of::()` and `MAX_ALIGN + // >= align_of::()` (compile-time assert above), so `base + // + aligned` is `T`-aligned (true even for `count == 0`). + // - We derive the typed pointer from `self.base.as_ptr()`, not + // from a fresh borrow of the whole buffer, so this slice + // does not invalidate any other thread's previously + // returned `&mut [T]` under stacked borrows. + // - `T: Zeroable` and the buffer bytes are zero, so the + // `&mut [T]` references valid `T` values. + let slice: &mut [T] = unsafe { + let elem_ptr = self.base.as_ptr().add(aligned).cast::(); + std::slice::from_raw_parts_mut(elem_ptr, count) + }; + + Ok(slice) + } + + /// Reset the arena to empty without releasing its buffer to the + /// pool. Useful for a decoder that wants to reuse the same arena + /// across several intermediate stages of the same frame. Callers + /// must ensure no slice previously returned from [`Arena::alloc`] + /// is still in use — Rust's borrow checker enforces this, since + /// `reset` takes `&mut self`. + pub fn reset(&mut self) { + // `&mut self` proves exclusive access; non-atomic stores + // would suffice, but the atomic API is uniform. + self.cursor.store(0, Ordering::Release); + self.alloc_count.store(0, Ordering::Release); + } +} + +impl Drop for Arena { + fn drop(&mut self) { + // We're in Drop with `&mut self`, so no `alloc`-returned + // slices can still be borrowing from `base` and no other + // thread can be in `alloc`. Take the buffer out of the mutex + // and either return it to the pool or let it free here. + let taken = self.buffer.get_mut().ok().and_then(|slot| slot.take()); + if let Some(buffer) = taken { + if let Some(pool) = self.pool.upgrade() { + pool.release(buffer); + } else { + // Pool was dropped before us — buffer drops here and + // its allocation is freed via `Buffer::Drop`. + drop(buffer); + } + } + } +} + +/// Round `n` up to the next multiple of `align`. `align` must be a +/// power of two. Returns `None` on overflow. +fn align_up(n: usize, align: usize) -> Option { + debug_assert!(align.is_power_of_two(), "alignment must be a power of two"); + let mask = align - 1; + n.checked_add(mask).map(|m| m & !mask) +} + +/// The owned body of a refcounted [`Frame`]. `Send + Sync`. +/// +/// Holds a [`sync::Arena`](Arena) (the bytes), a fixed-size table of +/// `(offset_in_arena, length_in_bytes)` pairs (one per plane), and a +/// [`FrameHeader`]. The `plane_count` field tracks how many entries +/// of `plane_offsets` are actually populated. Up to [`MAX_PLANES`] +/// planes are supported. +/// +/// **Lifetime:** an [`Arena`] returns its buffer to the pool when +/// dropped. An `Arc` keeps the arena alive via its single +/// owned field, so as long as any clone of a [`Frame`] exists the +/// underlying buffer stays out of the pool. +pub struct FrameInner { + arena: Arena, + plane_offsets: [(usize, usize); MAX_PLANES], + plane_count: u8, + header: FrameHeader, +} + +/// Refcounted handle to a decoded video frame. `Send + Sync`. +/// +/// Construct via [`FrameInner::new`]; clone freely (each clone bumps +/// the atomic refcount by 1). The arena and its buffer are released +/// back to the pool when the last clone is dropped. +/// +/// Use this type when the decoder hands frames to a different thread +/// from the one that produced them. For same-thread decode/consume, +/// the cheaper [`crate::arena::Frame`] (`Rc`-backed) is preferable. +pub type Frame = Arc; + +impl FrameInner { + /// Construct a `Frame` (`Arc`) from an arena, a slice + /// of `(offset, length)` plane descriptors, and a header. Returns + /// [`Error::InvalidData`] if more than [`MAX_PLANES`] planes are + /// supplied or if any plane range falls outside the arena's used + /// region. + pub fn new(arena: Arena, planes: &[(usize, usize)], header: FrameHeader) -> Result { + if planes.len() > MAX_PLANES { + return Err(Error::invalid(format!( + "FrameInner supports at most {} planes (got {})", + MAX_PLANES, + planes.len() + ))); + } + let used = arena.used(); + for (i, (off, len)) in planes.iter().enumerate() { + let end = off + .checked_add(*len) + .ok_or_else(|| Error::invalid(format!("plane {i}: offset+len overflow")))?; + if end > used { + return Err(Error::invalid(format!( + "plane {i}: range {off}..{end} exceeds arena used={used}" + ))); + } + } + let mut plane_offsets = [(0usize, 0usize); MAX_PLANES]; + for (i, p) in planes.iter().enumerate() { + plane_offsets[i] = *p; + } + Ok(Arc::new(FrameInner { + arena, + plane_offsets, + plane_count: planes.len() as u8, + header, + })) + } + + /// Number of planes this frame holds. + pub fn plane_count(&self) -> usize { + self.plane_count as usize + } + + /// Read-only access to plane `i`. Returns `None` if `i` is out of + /// range. + pub fn plane(&self, i: usize) -> Option<&[u8]> { + if i >= self.plane_count as usize { + return None; + } + let (off, len) = self.plane_offsets[i]; + // SAFETY: + // - plane ranges were validated against `arena.used()` at + // construction (`off + len <= arena.cursor`), and the + // cursor only advances, so the byte range is in-bounds. + // - The bytes were written by `alloc` and never moved (the + // buffer's allocation is stable for the arena's lifetime). + // - We derive the slice from the raw base pointer, never via + // a re-borrow of the whole buffer, so this `&[u8]` does not + // invalidate any other slice the caller is holding. + // - The borrow lifetime is bounded by `&self`. + let buf: &[u8] = unsafe { + let elem_ptr = self.arena.base.as_ptr().add(off); + std::slice::from_raw_parts(elem_ptr, len) + }; + Some(buf) + } + + /// Frame header (width / height / pixel format / pts). + pub fn header(&self) -> &FrameHeader { + &self.header + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::format::PixelFormat; + + fn assert_send_sync() {} + + #[test] + fn types_are_send_sync() { + // The whole point of this module: prove the public types + // satisfy the cross-thread contract that `crate::arena` does + // not. + assert_send_sync::(); + assert_send_sync::>(); + assert_send_sync::(); + assert_send_sync::(); + assert_send_sync::(); + } + + fn small_pool(slots: usize, cap: usize) -> Arc { + ArenaPool::new(slots, cap) + } + + #[test] + fn pool_lease_returns_err_when_exhausted() { + let pool = small_pool(2, 1024); + let a = pool.lease().expect("first lease"); + let b = pool.lease().expect("second lease"); + let third = pool.lease(); + assert!(matches!(third, Err(Error::ResourceExhausted(_)))); + drop((a, b)); + } + + #[test] + fn arena_alloc_caps_at_size_limit() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + let _: &mut [u8] = arena.alloc::(32).unwrap(); + let _: &mut [u8] = arena.alloc::(32).unwrap(); + let third = arena.alloc::(1); + assert!(matches!(third, Err(Error::ResourceExhausted(_)))); + } + + #[test] + fn arena_alloc_count_cap_fires() { + let pool = ArenaPool::with_alloc_count_cap(1, 1024, 3); + let arena = pool.lease().unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + assert!(arena.alloc_count_exceeded()); + let fourth = arena.alloc::(1); + assert!(matches!(fourth, Err(Error::ResourceExhausted(_)))); + // Counter must remain at the cap even after a refused alloc + // — no drift from the rollback path. + assert_eq!(arena.alloc_count(), 3); + } + + #[test] + fn arena_returns_to_pool_on_drop() { + let pool = small_pool(1, 256); + { + let arena = pool.lease().expect("first lease"); + assert!(matches!(pool.lease(), Err(Error::ResourceExhausted(_)))); + drop(arena); + } + let _again = pool.lease().expect("re-lease after drop"); + } + + #[test] + fn arena_alignment_is_respected() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + let _: &mut [u8] = arena.alloc::(1).unwrap(); + let s: &mut [u32] = arena.alloc::(4).unwrap(); + let addr = s.as_ptr() as usize; + assert_eq!(addr % align_of::(), 0); + assert_eq!(s.len(), 4); + } + + #[cfg(miri)] + #[test] + fn arena_alloc_can_return_misaligned_typed_slice() { + let pool = small_pool(1, 0); + let arena = pool.lease().unwrap(); + + // Memory-safety issue: the arena's backing allocation is a + // `Box<[u8]>`, so its base pointer is only guaranteed to be + // byte-aligned. `alloc::` aligns only the byte offset, not + // the absolute address, and then constructs `&mut [T]`. The + // empty-buffer case makes this deterministic: even an empty + // `&mut [u32]` must have an aligned pointer, but `Box<[u8]>` + // uses an alignment-1 dangling pointer when its length is 0. + let _s: &mut [u32] = arena.alloc::(0).unwrap(); + } + + // Pre-fix this test was: + // + // let values = arena.alloc::(1).unwrap(); + // let _ = values[0].get(); + // + // and failed under Miri because pool buffers are zero-filled and + // zero is not a valid `NonZeroU8`. Post-fix, the `Zeroable` bound + // on `Arena::alloc` makes that call a hard *compile* error — the + // strongest possible enforcement. The test below is a regression + // assertion that the bound stays as-or-stricter than `Zeroable`: + // if a future refactor weakened it back to `Copy`, the + // commented-out call site would start compiling again and Miri + // would once again accept the invalid bit pattern. + #[cfg(miri)] + #[test] + fn arena_alloc_allows_invalid_bit_patterns_for_copy_types() { + // `requires_zeroable::()` would not compile — + // `NonZeroU8: !Zeroable`. Sanity-check the helper itself with + // a known zeroable type so the test is an actual exercise. + fn requires_zeroable() {} + requires_zeroable::(); + // Uncommenting the next line must fail to compile: + // requires_zeroable::(); + } + + #[cfg(miri)] + #[test] + fn arena_alloc_second_slice_invalidates_first_mut_reference() { + let pool = small_pool(1, 2); + let arena = pool.lease().unwrap(); + + // Memory-safety issue: each `alloc` calls `[u8]::as_mut_ptr` on + // the whole backing slice before carving out the requested + // subslice. That materializes a new mutable borrow of the whole + // buffer and invalidates previously returned `&mut` slices, even + // when the byte ranges are disjoint. + let first = arena.alloc::(1).unwrap(); + let second = arena.alloc::(1).unwrap(); + first[0] = 1; + second[0] = 2; + } + + fn build_simple_frame(pool: &Arc) -> Frame { + let arena = pool.lease().unwrap(); + let plane0: &mut [u8] = arena.alloc::(16).unwrap(); + for (i, b) in plane0.iter_mut().enumerate() { + *b = i as u8; + } + let header = FrameHeader::new(4, 4, PixelFormat::Gray8, Some(42)); + FrameInner::new(arena, &[(0, 16)], header).unwrap() + } + + #[test] + fn frame_refcount_keeps_arena_alive() { + let pool = small_pool(1, 256); + let frame = build_simple_frame(&pool); + let clone = Arc::clone(&frame); + drop(frame); + let plane = clone.plane(0).expect("plane 0"); + assert_eq!(plane.len(), 16); + for (i, b) in plane.iter().enumerate() { + assert_eq!(*b, i as u8); + } + assert_eq!(clone.header().width, 4); + assert_eq!(clone.header().height, 4); + assert_eq!(clone.header().presentation_timestamp, Some(42)); + assert!(matches!(pool.lease(), Err(Error::ResourceExhausted(_)))); + } + + #[test] + fn last_drop_returns_arena_to_pool() { + let pool = small_pool(1, 256); + let frame = build_simple_frame(&pool); + let clone = Arc::clone(&frame); + drop(frame); + drop(clone); + let _again = pool.lease().expect("lease after last drop"); + } + + #[test] + fn frame_rejects_too_many_planes() { + let pool = small_pool(1, 256); + let arena = pool.lease().unwrap(); + let header = FrameHeader::new(1, 1, PixelFormat::Gray8, None); + let too_many = vec![(0usize, 0usize); MAX_PLANES + 1]; + let r = FrameInner::new(arena, &too_many, header); + assert!(matches!(r, Err(Error::InvalidData(_)))); + } + + #[test] + fn frame_rejects_plane_outside_arena() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + let header = FrameHeader::new(1, 1, PixelFormat::Gray8, None); + let r = FrameInner::new(arena, &[(0, 16)], header); + assert!(matches!(r, Err(Error::InvalidData(_)))); + } + + #[test] + fn pool_outlives_buffer_drop_when_pool_dropped_first() { + let pool = small_pool(1, 64); + let arena = pool.lease().unwrap(); + drop(pool); + drop(arena); + } + + #[test] + fn arena_reset_clears_allocations() { + let pool = small_pool(1, 32); + let mut arena = pool.lease().unwrap(); + let _: &mut [u8] = arena.alloc::(32).unwrap(); + assert!(matches!( + arena.alloc::(1), + Err(Error::ResourceExhausted(_)) + )); + arena.reset(); + let _: &mut [u8] = arena.alloc::(32).unwrap(); + } + + #[test] + fn frame_can_be_sent_across_thread_boundary() { + // Build a frame on this thread, ship it to a worker thread, + // read its bytes there. This is the use case the module + // exists to enable; if it ever stops compiling, the + // `Send + Sync` impls above are wrong. + let pool = small_pool(1, 256); + let frame = build_simple_frame(&pool); + let frame_for_worker = Arc::clone(&frame); + let handle = std::thread::spawn(move || { + let plane = frame_for_worker.plane(0).expect("plane 0 on worker"); + let mut sum: u32 = 0; + for b in plane { + sum += *b as u32; + } + sum + }); + let sum = handle.join().expect("worker joined"); + // Plane was filled with 0..16, sum = 120. + assert_eq!(sum, (0..16u32).sum::()); + // Original frame still readable here too. + assert_eq!(frame.plane(0).unwrap().len(), 16); + } + + #[test] + fn concurrent_alloc_produces_disjoint_slices() { + // Two threads alloc 64 bytes each from a 256-byte arena. + // Their slices must not overlap. + let pool = small_pool(1, 256); + let arena = Arc::new(pool.lease().unwrap()); + let a = Arc::clone(&arena); + let b = Arc::clone(&arena); + let h1 = std::thread::spawn(move || { + let s: &mut [u8] = a.alloc::(64).unwrap(); + // Fill so we can detect overlap from the other thread. + for x in s.iter_mut() { + *x = 0xAA; + } + (s.as_ptr() as usize, s.len()) + }); + let h2 = std::thread::spawn(move || { + let s: &mut [u8] = b.alloc::(64).unwrap(); + for x in s.iter_mut() { + *x = 0xBB; + } + (s.as_ptr() as usize, s.len()) + }); + let (p1, l1) = h1.join().unwrap(); + let (p2, l2) = h2.join().unwrap(); + // Disjoint ranges: [p1, p1+l1) and [p2, p2+l2) do not overlap. + let no_overlap = p1 + l1 <= p2 || p2 + l2 <= p1; + assert!(no_overlap, "concurrent alloc returned overlapping slices"); + } + + #[cfg(miri)] + #[test] + fn concurrent_alloc_retags_whole_buffer_while_other_thread_writes() { + // Memory-safety issue: `Arena` is `Sync`, so safe code can call + // `alloc` while another thread writes through a previously + // returned slice. The CAS cursor makes the byte ranges disjoint, + // but `alloc` still materializes a mutable borrow of the whole + // buffer via `[u8]::as_mut_ptr`; Miri reports that retag as a + // data race with the other thread's write. + let pool = small_pool(1, 256); + let arena = Arc::new(pool.lease().unwrap()); + let barrier = Arc::new(std::sync::Barrier::new(2)); + let a = Arc::clone(&arena); + let b = Arc::clone(&arena); + let barrier_a = Arc::clone(&barrier); + let barrier_b = Arc::clone(&barrier); + let h1 = std::thread::spawn(move || { + let s: &mut [u8] = a.alloc::(64).unwrap(); + barrier_a.wait(); + for x in s.iter_mut() { + *x = 0xAA; + } + }); + let h2 = std::thread::spawn(move || { + barrier_b.wait(); + let s: &mut [u8] = b.alloc::(64).unwrap(); + for x in s.iter_mut() { + *x = 0xBB; + } + }); + h1.join().unwrap(); + h2.join().unwrap(); + } +} diff --git a/crates/vendor/oxideav-core/src/bits.rs b/crates/vendor/oxideav-core/src/bits.rs new file mode 100644 index 00000000..10836c97 --- /dev/null +++ b/crates/vendor/oxideav-core/src/bits.rs @@ -0,0 +1,1021 @@ +//! Shared bit-level I/O — MSB-first and LSB-first readers and writers. +//! +//! Every `oxideav-*` codec crate used to ship its own bitwriter / +//! bitreader implementation. Those implementations converged on the +//! same two layouts (MSB-first for ~everything, LSB-first for Vorbis) +//! with only cosmetic method-name differences, so the core copy lives +//! here and each codec crate pulls it in via `oxideav_core::bits`. +//! +//! # Bit orders +//! +//! * [`BitReader`] / [`BitWriter`] — **MSB-first**. Within each byte +//! the high bit is read/written first. This matches AAC, MP1/2/3, +//! FLAC, Speex, H.263/4, MPEG-1/2/4 video, and just about every +//! modern codec bitstream. +//! * [`BitReaderLsb`] / [`BitWriterLsb`] — **LSB-first**. Within each +//! byte the low bit is read/written first. Vorbis I §2.1.4 packs +//! its bitstream this way. +//! +//! Both variants carry a 64-bit accumulator so callers can request up +//! to 32 bits per call without straddling refill logic. 64-bit values +//! are handled in two halves by `read_u64` / `write_u64`. +//! +//! # Method naming +//! +//! Every writer exposes both of these names for the same operation — +//! historical drift across the codec crates made both common, and +//! keeping both as aliases lets migration stay mechanical: +//! +//! * `write_u32(value, n)` ≡ `write_bits(value, n)` — append low `n` bits +//! * `finish(self)` ≡ `into_bytes(self)` — pad + consume +//! +//! Similarly `skip(n)` and `consume(n)` are aliases on the reader. + +use crate::{Error, Result}; + +// ==================== MSB-first ==================== + +/// MSB-first bit reader over a borrowed byte slice. +/// +/// Copy+Clone: the reader owns no heap state, only a borrowed slice +/// and a handful of counters, so `let saved = br;` followed by a +/// later `br = saved;` is a valid checkpoint/restore — some codec +/// parsers (e.g. MPEG-4 Part 2's VOP resync) use exactly that pattern. +#[derive(Clone, Copy)] +pub struct BitReader<'a> { + data: &'a [u8], + /// Index of the next byte to load into the accumulator. + byte_pos: usize, + /// Bits buffered from `data`, left-aligned (high bit = next to consume). + acc: u64, + /// Valid bits currently in `acc`, in the range `0..=64`. + bits_in_acc: u32, +} + +impl<'a> BitReader<'a> { + /// Start reading at the beginning of `data`. + pub fn new(data: &'a [u8]) -> Self { + Self { + data, + byte_pos: 0, + acc: 0, + bits_in_acc: 0, + } + } + + /// Start reading at a specific byte offset (useful for parsers that + /// need to re-anchor into the middle of a buffer without copying). + pub fn with_position(data: &'a [u8], byte_pos: usize) -> Self { + let byte_pos = byte_pos.min(data.len()); + Self { + data, + byte_pos, + acc: 0, + bits_in_acc: 0, + } + } + + /// Bits already consumed from the logical stream. + pub fn bit_position(&self) -> u64 { + self.byte_pos as u64 * 8 - self.bits_in_acc as u64 + } + + /// Byte offset of the reader (floor of `bit_position / 8`). + pub fn byte_position(&self) -> usize { + (self.bit_position() / 8) as usize + } + + /// Total remaining bits (buffered + unread from the slice). + pub fn bits_remaining(&self) -> u64 { + self.bits_in_acc as u64 + ((self.data.len() - self.byte_pos) as u64) * 8 + } + + /// True if the reader is positioned on a byte boundary. + pub fn is_byte_aligned(&self) -> bool { + self.bits_in_acc % 8 == 0 + } + + /// Skip remaining bits in the current byte, leaving the reader byte-aligned. + pub fn align_to_byte(&mut self) { + let drop = self.bits_in_acc % 8; + self.acc <<= drop; + self.bits_in_acc -= drop; + } + + fn refill(&mut self) { + while self.bits_in_acc <= 56 && self.byte_pos < self.data.len() { + self.acc |= (self.data[self.byte_pos] as u64) << (56 - self.bits_in_acc); + self.bits_in_acc += 8; + self.byte_pos += 1; + } + } + + /// Read `n` bits (0..=32) as an unsigned integer. + pub fn read_u32(&mut self, n: u32) -> Result { + debug_assert!(n <= 32, "BitReader::read_u32 supports up to 32 bits"); + if n == 0 { + return Ok(0); + } + if self.bits_in_acc < n { + self.refill(); + if self.bits_in_acc < n { + return Err(Error::invalid("bitreader: out of bits")); + } + } + let v = (self.acc >> (64 - n)) as u32; + self.acc <<= n; + self.bits_in_acc -= n; + Ok(v) + } + + /// Read `n` bits (0..=64) as an unsigned integer. + pub fn read_u64(&mut self, n: u32) -> Result { + debug_assert!(n <= 64); + if n <= 32 { + return self.read_u32(n).map(|v| v as u64); + } + let hi = self.read_u32(n - 32)? as u64; + let lo = self.read_u32(32)? as u64; + Ok((hi << 32) | lo) + } + + /// Read `n` bits as a signed integer, sign-extended from the high bit. + pub fn read_i32(&mut self, n: u32) -> Result { + if n == 0 { + return Ok(0); + } + let raw = self.read_u32(n)? as i32; + let shift = 32 - n; + Ok((raw << shift) >> shift) + } + + /// Read a single bit as a bool. + pub fn read_bit(&mut self) -> Result { + Ok(self.read_u32(1)? != 0) + } + + /// Read a single bit as `0` or `1` (some codec specs phrase flags this way). + pub fn read_u1(&mut self) -> Result { + self.read_u32(1) + } + + /// Peek `n` bits (0..=32) without consuming them. + pub fn peek_u32(&mut self, n: u32) -> Result { + debug_assert!(n <= 32); + if n == 0 { + return Ok(0); + } + if self.bits_in_acc < n { + self.refill(); + if self.bits_in_acc < n { + return Err(Error::invalid("bitreader: out of bits for peek")); + } + } + Ok((self.acc >> (64 - n)) as u32) + } + + /// Discard `n` bits. + pub fn skip(&mut self, n: u32) -> Result<()> { + let mut left = n; + while left > 32 { + self.read_u32(32)?; + left -= 32; + } + self.read_u32(left)?; + Ok(()) + } + + /// Alias for [`Self::skip`] — some spec wordings prefer "consume". + pub fn consume(&mut self, n: u32) -> Result<()> { + self.skip(n) + } + + /// Read a unary-coded value: the count of leading zero bits, terminated + /// by a single `1`. Used by FLAC Rice residuals and any other codec + /// that needs variable-length counts. Uses `leading_zeros()` on the + /// 64-bit accumulator for the fast path. + pub fn read_unary(&mut self) -> Result { + let mut count = 0u32; + loop { + if self.bits_in_acc == 0 { + self.refill(); + if self.bits_in_acc == 0 { + return Err(Error::invalid("bitreader: out of bits in unary code")); + } + } + let lz_total = self.acc.leading_zeros(); + let lz_avail = lz_total.min(self.bits_in_acc); + count = count + .checked_add(lz_avail) + .ok_or_else(|| Error::invalid("bitreader: unary count overflow"))?; + // Shifting a u64 by 64 is UB in Rust — guard that case. It only + // arises for very long zero runs. + if lz_avail >= 64 { + self.acc = 0; + } else { + self.acc <<= lz_avail; + } + self.bits_in_acc -= lz_avail; + if lz_avail < lz_total || self.bits_in_acc == 0 { + continue; + } + // Consume the terminating 1 bit. + self.acc <<= 1; + self.bits_in_acc -= 1; + return Ok(count); + } + } + + /// Read `n` bytes. Requires the reader to be byte-aligned. + pub fn read_bytes(&mut self, n: usize) -> Result> { + if !self.is_byte_aligned() { + return Err(Error::invalid( + "bitreader: read_bytes requires byte alignment", + )); + } + self.align_to_byte(); + let start = self.byte_pos - (self.bits_in_acc as usize / 8); + // `bits_in_acc` is a multiple of 8 here (because we're aligned); each + // full byte in the accumulator is one unconsumed input byte whose + // `byte_pos` has already been advanced — so the actual logical cursor + // is `byte_pos - bits_in_acc / 8`. + if start + n > self.data.len() { + return Err(Error::invalid("bitreader: read_bytes past end")); + } + let out = self.data[start..start + n].to_vec(); + // Advance: empty the accumulator and re-anchor `byte_pos` past the + // copied region. + self.acc = 0; + self.bits_in_acc = 0; + self.byte_pos = start + n; + Ok(out) + } +} + +/// MSB-first bit writer over an internal byte buffer. +pub struct BitWriter { + data: Vec, + /// Bits buffered at the *high* end of `acc` (next-to-emit at top). + acc: u64, + /// Valid bits currently in `acc` (0..=64). + bits_in_acc: u32, +} + +impl BitWriter { + /// An empty writer. + pub fn new() -> Self { + Self { + data: Vec::new(), + acc: 0, + bits_in_acc: 0, + } + } + + /// An empty writer whose output buffer is pre-allocated for `cap` bytes. + pub fn with_capacity(cap: usize) -> Self { + Self { + data: Vec::with_capacity(cap), + acc: 0, + bits_in_acc: 0, + } + } + + /// Total bits written so far (including any in the unflushed accumulator). + pub fn bit_position(&self) -> u64 { + self.data.len() as u64 * 8 + self.bits_in_acc as u64 + } + + /// Bytes of output produced so far (excluding any unflushed partial byte). + pub fn byte_len(&self) -> usize { + self.data.len() + } + + /// True if the writer is currently on a byte boundary. + pub fn is_byte_aligned(&self) -> bool { + self.bits_in_acc % 8 == 0 + } + + /// Append `n` bits (0..=32) from the low `n` bits of `value`, MSB first. + pub fn write_u32(&mut self, value: u32, n: u32) { + debug_assert!(n <= 32, "BitWriter::write_u32 supports up to 32 bits"); + if n == 0 { + return; + } + let mask: u32 = if n == 32 { u32::MAX } else { (1u32 << n) - 1 }; + let v = (value & mask) as u64; + let shift = 64 - self.bits_in_acc - n; + self.acc |= v << shift; + self.bits_in_acc += n; + while self.bits_in_acc >= 8 { + let byte = (self.acc >> 56) as u8; + self.data.push(byte); + self.acc <<= 8; + self.bits_in_acc -= 8; + } + } + + /// Alias of [`Self::write_u32`] — some codec crates historically + /// spell this `write_bits`. + pub fn write_bits(&mut self, value: u32, n: u32) { + self.write_u32(value, n) + } + + /// Append up to 64 bits. + pub fn write_u64(&mut self, value: u64, n: u32) { + debug_assert!(n <= 64); + if n <= 32 { + self.write_u32(value as u32, n); + } else { + self.write_u32((value >> 32) as u32, n - 32); + self.write_u32(value as u32, 32); + } + } + + /// Append `n` bits interpreted as a signed integer. Only the low + /// `n` bits of the 2's-complement representation are written. + pub fn write_i32(&mut self, value: i32, n: u32) { + self.write_u32(value as u32, n); + } + + /// Append a single bit. + pub fn write_bit(&mut self, bit: bool) { + self.write_u32(bit as u32, 1); + } + + /// Emit a unary-coded value: `n` zero bits followed by a single `1`. + /// Inverse of [`BitReader::read_unary`]. + pub fn write_unary(&mut self, n: u32) { + let mut remaining = n; + while remaining >= 32 { + self.write_u32(0, 32); + remaining -= 32; + } + if remaining > 0 { + self.write_u32(0, remaining); + } + self.write_bit(true); + } + + /// Append one whole byte (8 bits). + pub fn write_byte(&mut self, b: u8) { + self.write_u32(b as u32, 8); + } + + /// Append a slice of bytes. Fast path when byte-aligned. + pub fn write_bytes(&mut self, bytes: &[u8]) { + if self.is_byte_aligned() { + // Flush the accumulator (which is already at 0 bits_in_acc). + self.data.extend_from_slice(bytes); + } else { + for &b in bytes { + self.write_u32(b as u32, 8); + } + } + } + + /// Pad to the next byte boundary with zero bits. + pub fn align_to_byte(&mut self) { + let pad = (8 - self.bits_in_acc % 8) % 8; + if pad > 0 { + self.write_u32(0, pad); + } + } + + /// Alias of [`Self::align_to_byte`] — MPEG-4 video spells it + /// `align_to_byte_zero` since the spec also defines alternate + /// align-with-ones tails in other contexts. + pub fn align_to_byte_zero(&mut self) { + self.align_to_byte() + } + + /// Borrow the bytes accumulated so far (excluding any unflushed partial byte). + pub fn bytes(&self) -> &[u8] { + &self.data + } + + /// Alias of [`Self::bytes`] — some codec crates spell this `buffer`. + pub fn buffer(&self) -> &[u8] { + &self.data + } + + /// Pad with zero bits to the next byte boundary, then return the bytes. + pub fn finish(mut self) -> Vec { + if self.bits_in_acc > 0 { + let byte = (self.acc >> 56) as u8; + self.data.push(byte); + self.acc = 0; + self.bits_in_acc = 0; + } + self.data + } + + /// Alias of [`Self::finish`] — some codec crates spell this `into_bytes`. + pub fn into_bytes(self) -> Vec { + self.finish() + } +} + +impl Default for BitWriter { + fn default() -> Self { + Self::new() + } +} + +// ==================== LSB-first (Vorbis) ==================== + +/// LSB-first bit reader. See [module docs](self) for the LSB convention. +/// +/// Copy+Clone for the same reason as [`BitReader`] — no heap state. +#[derive(Clone, Copy)] +pub struct BitReaderLsb<'a> { + data: &'a [u8], + byte_pos: usize, + /// Buffered bits, low-aligned (next bit to emit is bit 0 of `acc`). + acc: u64, + bits_in_acc: u32, +} + +impl<'a> BitReaderLsb<'a> { + /// Start reading at the beginning of `data`. + pub fn new(data: &'a [u8]) -> Self { + Self { + data, + byte_pos: 0, + acc: 0, + bits_in_acc: 0, + } + } + + /// Start reading at a specific byte offset (useful for parsers that + /// need to re-anchor into the middle of a buffer without copying). + /// Mirrors [`BitReader::with_position`]. + pub fn with_position(data: &'a [u8], byte_pos: usize) -> Self { + let byte_pos = byte_pos.min(data.len()); + Self { + data, + byte_pos, + acc: 0, + bits_in_acc: 0, + } + } + + /// Bits already consumed from the logical stream. + pub fn bit_position(&self) -> u64 { + self.byte_pos as u64 * 8 - self.bits_in_acc as u64 + } + + /// Byte offset of the reader (floor of `bit_position / 8`). + pub fn byte_position(&self) -> usize { + (self.bit_position() / 8) as usize + } + + /// Total remaining bits (buffered + unread from the slice). + pub fn bits_remaining(&self) -> u64 { + self.bits_in_acc as u64 + ((self.data.len() - self.byte_pos) as u64) * 8 + } + + /// True if the reader is positioned on a byte boundary. + pub fn is_byte_aligned(&self) -> bool { + self.bits_in_acc % 8 == 0 + } + + /// Skip remaining bits in the current byte, leaving the reader + /// byte-aligned. In the LSB layout the *low* bits of the partial + /// byte are the already-consumed ones, so this drops the buffered + /// low bits. + pub fn align_to_byte(&mut self) { + let drop = self.bits_in_acc % 8; + self.acc >>= drop; + self.bits_in_acc -= drop; + } + + fn refill(&mut self) { + while self.bits_in_acc <= 56 && self.byte_pos < self.data.len() { + self.acc |= (self.data[self.byte_pos] as u64) << self.bits_in_acc; + self.bits_in_acc += 8; + self.byte_pos += 1; + } + } + + /// Read `n` bits (0..=32) as an unsigned integer. + pub fn read_u32(&mut self, n: u32) -> Result { + debug_assert!(n <= 32, "BitReaderLsb::read_u32 supports up to 32 bits"); + if n == 0 { + return Ok(0); + } + if self.bits_in_acc < n { + self.refill(); + if self.bits_in_acc < n { + return Err(Error::Eof); + } + } + let mask = if n == 32 { u32::MAX } else { (1u32 << n) - 1 }; + let v = (self.acc as u32) & mask; + self.acc >>= n; + self.bits_in_acc -= n; + Ok(v) + } + + /// Read `n` bits (0..=64) as an unsigned integer (low half first). + pub fn read_u64(&mut self, n: u32) -> Result { + debug_assert!(n <= 64); + if n == 0 { + return Ok(0); + } + if n <= 32 { + return Ok(self.read_u32(n)? as u64); + } + let lo = self.read_u32(32)? as u64; + let hi = self.read_u32(n - 32)? as u64; + Ok(lo | (hi << 32)) + } + + /// Read `n` bits as a signed integer, sign-extended from the high bit. + pub fn read_i32(&mut self, n: u32) -> Result { + if n == 0 { + return Ok(0); + } + let raw = self.read_u32(n)? as i32; + let shift = 32 - n; + Ok((raw << shift) >> shift) + } + + /// Read a single bit as a bool. + pub fn read_bit(&mut self) -> Result { + Ok(self.read_u32(1)? != 0) + } + + /// Read a single bit as `0` or `1` (some codec specs phrase flags + /// this way). Mirrors [`BitReader::read_u1`]. + pub fn read_u1(&mut self) -> Result { + self.read_u32(1) + } + + /// Peek `n` bits (0..=32) without consuming them. Mirrors + /// [`BitReader::peek_u32`] — the workhorse of table-driven Huffman + /// decoders (peek a fixed window, look up, then consume the code + /// length). + pub fn peek_u32(&mut self, n: u32) -> Result { + debug_assert!(n <= 32); + if n == 0 { + return Ok(0); + } + if self.bits_in_acc < n { + self.refill(); + if self.bits_in_acc < n { + return Err(Error::Eof); + } + } + let mask = if n == 32 { u32::MAX } else { (1u32 << n) - 1 }; + Ok((self.acc as u32) & mask) + } + + /// Discard `n` bits. + pub fn skip(&mut self, n: u32) -> Result<()> { + let mut left = n; + while left > 32 { + self.read_u32(32)?; + left -= 32; + } + self.read_u32(left)?; + Ok(()) + } + + /// Alias for [`Self::skip`] — some spec wordings prefer "consume". + pub fn consume(&mut self, n: u32) -> Result<()> { + self.skip(n) + } + + /// Read `n` bytes. Requires the reader to be byte-aligned. Mirrors + /// [`BitReader::read_bytes`]. + pub fn read_bytes(&mut self, n: usize) -> Result> { + if !self.is_byte_aligned() { + return Err(Error::invalid( + "bitreader: read_bytes requires byte alignment", + )); + } + // Aligned, so `bits_in_acc` is a multiple of 8; each full byte + // in the accumulator is one unconsumed input byte whose + // `byte_pos` has already been advanced. + let start = self.byte_pos - (self.bits_in_acc as usize / 8); + if start + n > self.data.len() { + return Err(Error::Eof); + } + let out = self.data[start..start + n].to_vec(); + self.acc = 0; + self.bits_in_acc = 0; + self.byte_pos = start + n; + Ok(out) + } +} + +/// LSB-first bit writer — inverse of [`BitReaderLsb`]. +pub struct BitWriterLsb { + data: Vec, + /// Bits held over from the last partial byte, low-aligned. + acc: u64, + bits_in_acc: u32, +} + +impl BitWriterLsb { + /// An empty writer. + pub fn new() -> Self { + Self { + data: Vec::new(), + acc: 0, + bits_in_acc: 0, + } + } + + /// An empty writer whose output buffer is pre-allocated for `cap` bytes. + pub fn with_capacity(cap: usize) -> Self { + Self { + data: Vec::with_capacity(cap), + acc: 0, + bits_in_acc: 0, + } + } + + /// Total bits written so far (including any in the unflushed accumulator). + pub fn bit_position(&self) -> u64 { + self.data.len() as u64 * 8 + self.bits_in_acc as u64 + } + + /// Bytes of output produced so far (excluding any unflushed partial + /// byte). + pub fn byte_len(&self) -> usize { + self.data.len() + } + + /// True if the writer is currently on a byte boundary. + pub fn is_byte_aligned(&self) -> bool { + self.bits_in_acc % 8 == 0 + } + + /// Append `n` bits (0..=32) from the low `n` bits of `value`, LSB first. + pub fn write_u32(&mut self, value: u32, n: u32) { + debug_assert!(n <= 32, "BitWriterLsb::write_u32 supports up to 32 bits"); + if n == 0 { + return; + } + let mask: u32 = if n == 32 { u32::MAX } else { (1u32 << n) - 1 }; + let v = value & mask; + self.acc |= (v as u64) << self.bits_in_acc; + self.bits_in_acc += n; + while self.bits_in_acc >= 8 { + self.data.push((self.acc & 0xFF) as u8); + self.acc >>= 8; + self.bits_in_acc -= 8; + } + } + + /// Append up to 64 bits (low half first). + pub fn write_u64(&mut self, value: u64, n: u32) { + debug_assert!(n <= 64); + if n <= 32 { + self.write_u32(value as u32, n); + } else { + self.write_u32(value as u32, 32); + self.write_u32((value >> 32) as u32, n - 32); + } + } + + /// Alias of [`Self::write_u32`] — mirrors [`BitWriter::write_bits`]. + pub fn write_bits(&mut self, value: u32, n: u32) { + self.write_u32(value, n) + } + + /// Append `n` bits interpreted as a signed integer. Only the low + /// `n` bits of the 2's-complement representation are written. + pub fn write_i32(&mut self, value: i32, n: u32) { + self.write_u32(value as u32, n); + } + + /// Append a single bit. + pub fn write_bit(&mut self, bit: bool) { + self.write_u32(bit as u32, 1); + } + + /// Append one whole byte (8 bits). + pub fn write_byte(&mut self, b: u8) { + self.write_u32(b as u32, 8); + } + + /// Append a slice of bytes. Fast path when byte-aligned. + pub fn write_bytes(&mut self, bytes: &[u8]) { + if self.is_byte_aligned() { + self.data.extend_from_slice(bytes); + } else { + for &b in bytes { + self.write_u32(b as u32, 8); + } + } + } + + /// Pad to the next byte boundary with zero bits. + pub fn align_to_byte(&mut self) { + let pad = (8 - self.bits_in_acc % 8) % 8; + self.write_u32(0, pad); + } + + /// Borrow the bytes accumulated so far (excluding any unflushed + /// partial byte). + pub fn bytes(&self) -> &[u8] { + &self.data + } + + /// Alias of [`Self::bytes`] — mirrors [`BitWriter::buffer`]. + pub fn buffer(&self) -> &[u8] { + &self.data + } + + /// Pad with zero bits to the next byte boundary, then return the bytes. + pub fn finish(mut self) -> Vec { + if self.bits_in_acc > 0 { + self.data.push((self.acc & 0xFF) as u8); + self.acc = 0; + self.bits_in_acc = 0; + } + self.data + } + + /// Alias of [`Self::finish`] — mirrors [`BitWriter::into_bytes`]. + pub fn into_bytes(self) -> Vec { + self.finish() + } +} + +impl Default for BitWriterLsb { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---- MSB ---- + + #[test] + fn msb_roundtrip_byte() { + let mut w = BitWriter::new(); + for &b in &[1u32, 0, 1, 0, 0, 1, 0, 1] { + w.write_u32(b, 1); + } + assert_eq!(w.finish(), vec![0xA5]); + } + + #[test] + fn msb_roundtrip_varied_widths() { + let mut bw = BitWriter::new(); + let writes: Vec<(u32, u32)> = vec![ + (0b1, 1), + (0b10101, 5), + (0b111100001111, 12), + (0xDEADBEEF, 32), + (0b001, 3), + (0xC, 4), + (0xABCD, 16), + (0x12345, 20), + (0, 8), + (0xFFFFFFFF, 32), + ]; + for &(v, n) in &writes { + bw.write_u32(v, n); + } + let bytes = bw.finish(); + let mut br = BitReader::new(&bytes); + for &(v, n) in &writes { + let got = br.read_u32(n).unwrap(); + let mask = if n == 32 { u32::MAX } else { (1 << n) - 1 }; + assert_eq!(got, v & mask, "mismatch for ({v:#x}, {n})"); + } + } + + #[test] + fn msb_signed_extension() { + let mut br = BitReader::new(&[0xFF]); + assert_eq!(br.read_i32(4).unwrap(), -1); + assert_eq!(br.read_i32(4).unwrap(), -1); + } + + #[test] + fn msb_peek_skip() { + let mut br = BitReader::new(&[0xFF, 0x00]); + assert_eq!(br.peek_u32(12).unwrap(), 0xFF0); + br.skip(4).unwrap(); + assert_eq!(br.read_u32(8).unwrap(), 0xF0); + } + + #[test] + fn msb_alignment() { + let mut br = BitReader::new(&[0xFF, 0x55]); + br.read_u32(3).unwrap(); + assert!(!br.is_byte_aligned()); + br.align_to_byte(); + assert!(br.is_byte_aligned()); + assert_eq!(br.read_u32(8).unwrap(), 0x55); + } + + #[test] + fn msb_write_bytes_fast_path() { + let mut w = BitWriter::new(); + w.write_bytes(&[0x11, 0x22, 0x33]); + assert_eq!(w.finish(), vec![0x11, 0x22, 0x33]); + } + + #[test] + fn msb_write_bytes_unaligned() { + let mut w = BitWriter::new(); + w.write_u32(0b101, 3); + w.write_bytes(&[0xFF, 0x00]); + // 3 bits + 2*8 = 19 bits → 3 bytes after zero-pad. + let out = w.finish(); + assert_eq!(out.len(), 3); + } + + #[test] + fn msb_write_bits_alias() { + let mut w = BitWriter::new(); + w.write_bits(0xA, 4); + w.write_u32(0x5, 4); + assert_eq!(w.finish(), vec![0xA5]); + } + + #[test] + fn msb_into_bytes_alias() { + let mut w = BitWriter::new(); + w.write_u32(0xA5, 8); + assert_eq!(w.into_bytes(), vec![0xA5]); + } + + #[test] + fn msb_read_u64_high_bits() { + // Write 0x1234567890ABCDEF as 64 bits MSB-first, read back. + let mut w = BitWriter::new(); + w.write_u64(0x1234567890ABCDEF, 64); + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + assert_eq!(r.read_u64(64).unwrap(), 0x1234567890ABCDEF); + } + + #[test] + fn msb_unary_roundtrip() { + let mut w = BitWriter::new(); + let counts: Vec = vec![0, 1, 7, 31, 32, 33, 64, 65, 100]; + for &c in &counts { + w.write_unary(c); + } + let bytes = w.finish(); + let mut r = BitReader::new(&bytes); + for &c in &counts { + assert_eq!(r.read_unary().unwrap(), c, "unary roundtrip for {c}"); + } + } + + #[test] + fn msb_read_bytes_aligned() { + let mut br = BitReader::new(&[0xAA, 0xBB, 0xCC, 0xDD]); + let _ = br.read_u32(8).unwrap(); + let got = br.read_bytes(2).unwrap(); + assert_eq!(got, vec![0xBB, 0xCC]); + assert_eq!(br.read_u32(8).unwrap(), 0xDD); + } + + // ---- LSB ---- + + #[test] + fn lsb_roundtrip_byte() { + let mut w = BitWriterLsb::new(); + for &b in &[1u32, 0, 1, 0, 0, 1, 0, 1] { + w.write_u32(b, 1); + } + assert_eq!(w.finish(), vec![0xA5]); + } + + #[test] + fn lsb_multi_byte() { + let mut w = BitWriterLsb::new(); + w.write_u32(0x3412, 16); + let bytes = w.finish(); + assert_eq!(bytes, vec![0x12, 0x34]); + let mut r = BitReaderLsb::new(&bytes); + assert_eq!(r.read_u32(16).unwrap(), 0x3412); + } + + #[test] + fn lsb_roundtrip_varied_widths() { + let mut bw = BitWriterLsb::new(); + let writes: Vec<(u32, u32)> = vec![(5, 3), (0xABCD, 16), (0x1234567, 27), (1, 1)]; + for &(v, n) in &writes { + bw.write_u32(v, n); + } + let bytes = bw.finish(); + let mut r = BitReaderLsb::new(&bytes); + for &(v, n) in &writes { + assert_eq!(r.read_u32(n).unwrap(), v); + } + } + + #[test] + fn lsb_peek_skip_consume() { + let mut r = BitReaderLsb::new(&[0xA5, 0x5A]); + // Low nibble of 0xA5 first in LSB order. + assert_eq!(r.peek_u32(4).unwrap(), 0x5); + // Peek does not consume. + assert_eq!(r.peek_u32(8).unwrap(), 0xA5); + r.skip(4).unwrap(); + assert_eq!(r.read_u32(4).unwrap(), 0xA); + r.consume(4).unwrap(); + assert_eq!(r.read_u32(4).unwrap(), 0x5); + // Past-end peek reports Eof. + assert!(r.peek_u32(8).is_err()); + } + + #[test] + fn lsb_alignment_and_positions() { + let mut r = BitReaderLsb::new(&[0xFF, 0x55, 0x33]); + assert_eq!(r.bits_remaining(), 24); + r.read_u32(3).unwrap(); + assert!(!r.is_byte_aligned()); + assert_eq!(r.bit_position(), 3); + assert_eq!(r.byte_position(), 0); + r.align_to_byte(); + assert!(r.is_byte_aligned()); + assert_eq!(r.bit_position(), 8); + assert_eq!(r.byte_position(), 1); + assert_eq!(r.bits_remaining(), 16); + assert_eq!(r.read_u32(8).unwrap(), 0x55); + // with_position anchors mid-buffer (and clamps past-end). + let mut r2 = BitReaderLsb::with_position(&[0xFF, 0x55, 0x33], 2); + assert_eq!(r2.read_u32(8).unwrap(), 0x33); + let r3 = BitReaderLsb::with_position(&[0xFF], 9); + assert_eq!(r3.bits_remaining(), 0); + } + + #[test] + fn lsb_read_bytes_aligned() { + let mut r = BitReaderLsb::new(&[0xAA, 0xBB, 0xCC, 0xDD]); + let _ = r.read_u32(8).unwrap(); + let got = r.read_bytes(2).unwrap(); + assert_eq!(got, vec![0xBB, 0xCC]); + assert_eq!(r.read_u32(8).unwrap(), 0xDD); + // Unaligned read_bytes is a usage error. + let mut r = BitReaderLsb::new(&[0xAA, 0xBB]); + r.read_u32(3).unwrap(); + assert!(r.read_bytes(1).is_err()); + // Past-end read_bytes reports Eof. + let mut r = BitReaderLsb::new(&[0xAA]); + assert!(r.read_bytes(2).is_err()); + } + + #[test] + fn lsb_read_u1() { + // 0xA5 LSB-first = 1,0,1,0,0,1,0,1. + let mut r = BitReaderLsb::new(&[0xA5]); + let bits: Vec = (0..8).map(|_| r.read_u1().unwrap()).collect(); + assert_eq!(bits, vec![1, 0, 1, 0, 0, 1, 0, 1]); + } + + #[test] + fn lsb_writer_bytes_and_aliases() { + let mut w = BitWriterLsb::new(); + assert!(w.is_byte_aligned()); + w.write_bits(0x5, 4); + assert!(!w.is_byte_aligned()); + assert_eq!(w.byte_len(), 0); + w.write_u32(0xA, 4); + assert_eq!(w.byte_len(), 1); + assert_eq!(w.bytes(), &[0xA5]); + assert_eq!(w.buffer(), &[0xA5]); + w.write_byte(0x7E); + w.write_bytes(&[0x11, 0x22]); + assert_eq!(w.into_bytes(), vec![0xA5, 0x7E, 0x11, 0x22]); + } + + #[test] + fn lsb_writer_unaligned_write_bytes() { + let mut w = BitWriterLsb::new(); + w.write_u32(0b101, 3); + w.write_bytes(&[0xFF, 0x00]); + let out = w.finish(); + assert_eq!(out.len(), 3); + // Read back: 3 bits then the two bytes. + let mut r = BitReaderLsb::new(&out); + assert_eq!(r.read_u32(3).unwrap(), 0b101); + assert_eq!(r.read_u32(8).unwrap(), 0xFF); + assert_eq!(r.read_u32(8).unwrap(), 0x00); + } + + #[test] + fn lsb_writer_signed() { + let mut w = BitWriterLsb::new(); + w.write_i32(-1, 4); + w.write_i32(3, 4); + let bytes = w.finish(); + let mut r = BitReaderLsb::new(&bytes); + assert_eq!(r.read_i32(4).unwrap(), -1); + assert_eq!(r.read_i32(4).unwrap(), 3); + } +} diff --git a/crates/vendor/oxideav-core/src/capabilities.rs b/crates/vendor/oxideav-core/src/capabilities.rs new file mode 100644 index 00000000..8a9537df --- /dev/null +++ b/crates/vendor/oxideav-core/src/capabilities.rs @@ -0,0 +1,252 @@ +//! Codec capability description. +//! +//! Each codec implementation registered with the codec registry attaches one +//! of these structs to declare what it can do, what its constraints are, and +//! how the registry should rank it against alternative implementations of +//! the same codec id. +//! +//! The flag layout is a 6-column capability string (one letter per +//! capability, `.` when absent): +//! +//! ```text +//! D..... = Decoding supported +//! .E.... = Encoding supported +//! ..V... = Video codec ..A... = Audio ..S... = Subtitle +//! ..D... = Data ..T... = Attachment +//! ...I.. = Intra-frame-only codec +//! ....L. = Lossy compression +//! .....S = Lossless compression +//! ``` + +use std::fmt; + +use crate::format::{MediaType, PixelFormat}; + +/// Default priority for software implementations. Lower numbers are preferred +/// at resolution time, so register hardware impls with a smaller value (e.g. +/// `10`) and software fallbacks with the default `100`. +pub const DEFAULT_PRIORITY: i32 = 100; + +/// What an implementation can do plus how it ranks vs alternatives. +#[derive(Clone, Debug)] +pub struct CodecCapabilities { + /// Decoding supported by this implementation. + pub decode: bool, + /// Encoding supported by this implementation. + pub encode: bool, + /// Media type this implementation handles (audio, video, ...). + pub media_type: MediaType, + /// Every coded unit is independently decodable (no inter-frame + /// prediction). + pub intra_only: bool, + /// Supports lossy compression. + pub lossy: bool, + /// Supports lossless compression. `lossy` and `lossless` may both + /// be set for codecs that offer both modes. + pub lossless: bool, + /// Hardware-accelerated implementation (VAAPI/NVENC/QSV/VideoToolbox/...). + pub hardware_accelerated: bool, + /// Short identifier for this implementation, e.g. "flac_sw", "h264_qsv". + pub implementation: String, + /// Restrictions — `None` means "no constraint". + pub max_width: Option, + /// Maximum supported frame height in pixels; `None` = unconstrained. + pub max_height: Option, + /// Maximum supported bit rate in bits per second; `None` = + /// unconstrained. + pub max_bitrate: Option, + /// Maximum supported audio sample rate in Hz; `None` = unconstrained. + pub max_sample_rate: Option, + /// Maximum supported audio channel count; `None` = unconstrained. + pub max_channels: Option, + /// Lower numbers are preferred. HW impls should be ~10, SW impls ~100. + pub priority: i32, + /// Pixel formats this implementation accepts (video only). An empty + /// `Vec` means "any format" — resolution won't filter on it. When + /// populated, the registry can skip impls whose accepted set does not + /// include the format requested by the caller. + pub accepted_pixel_formats: Vec, +} + +impl CodecCapabilities { + /// Construct a software audio decoder/encoder capability set with sensible + /// defaults — adjust fields after creation. + pub fn audio(implementation: impl Into) -> Self { + Self { + decode: false, + encode: false, + media_type: MediaType::Audio, + intra_only: true, // audio packets are independently decodable in most codecs + lossy: false, + lossless: false, + hardware_accelerated: false, + implementation: implementation.into(), + max_width: None, + max_height: None, + max_bitrate: None, + max_sample_rate: None, + max_channels: None, + priority: DEFAULT_PRIORITY, + accepted_pixel_formats: Vec::new(), + } + } + + /// Construct a software video decoder/encoder capability set with + /// sensible defaults — adjust fields after creation. + pub fn video(implementation: impl Into) -> Self { + Self { + decode: false, + encode: false, + media_type: MediaType::Video, + intra_only: false, + lossy: false, + lossless: false, + hardware_accelerated: false, + implementation: implementation.into(), + max_width: None, + max_height: None, + max_bitrate: None, + max_sample_rate: None, + max_channels: None, + priority: DEFAULT_PRIORITY, + accepted_pixel_formats: Vec::new(), + } + } + + /// 6-character capability flag string (see the module docs for the + /// column layout). Useful for `oxideav list`-style + /// output. + pub fn flag_string(&self) -> String { + let mut s = String::with_capacity(6); + s.push(if self.decode { 'D' } else { '.' }); + s.push(if self.encode { 'E' } else { '.' }); + s.push(match self.media_type { + MediaType::Video => 'V', + MediaType::Audio => 'A', + MediaType::Subtitle => 'S', + MediaType::Data => 'D', + MediaType::Unknown => '.', + }); + s.push(if self.intra_only { 'I' } else { '.' }); + s.push(if self.lossy { 'L' } else { '.' }); + s.push(if self.lossless { 'S' } else { '.' }); + s + } + + // Builder-style helpers so registrations stay compact. + + /// Mark this implementation as supporting decode. + pub fn with_decode(mut self) -> Self { + self.decode = true; + self + } + /// Mark this implementation as supporting encode. + pub fn with_encode(mut self) -> Self { + self.encode = true; + self + } + /// Set the intra-frame-only flag. + pub fn with_intra_only(mut self, v: bool) -> Self { + self.intra_only = v; + self + } + /// Set the lossy-compression flag. + pub fn with_lossy(mut self, v: bool) -> Self { + self.lossy = v; + self + } + /// Set the lossless-compression flag. + pub fn with_lossless(mut self, v: bool) -> Self { + self.lossless = v; + self + } + /// Set the hardware-accelerated flag. + pub fn with_hardware(mut self, v: bool) -> Self { + self.hardware_accelerated = v; + self + } + /// Set the registry ranking priority (lower is preferred; HW ~10, + /// SW ~100). + pub fn with_priority(mut self, p: i32) -> Self { + self.priority = p; + self + } + /// Constrain the maximum frame size to `w` × `h` pixels. + pub fn with_max_size(mut self, w: u32, h: u32) -> Self { + self.max_width = Some(w); + self.max_height = Some(h); + self + } + /// Constrain the maximum bit rate (bits per second). + pub fn with_max_bitrate(mut self, br: u64) -> Self { + self.max_bitrate = Some(br); + self + } + /// Constrain the maximum audio sample rate (Hz). + pub fn with_max_sample_rate(mut self, sr: u32) -> Self { + self.max_sample_rate = Some(sr); + self + } + /// Constrain the maximum audio channel count. + pub fn with_max_channels(mut self, ch: u16) -> Self { + self.max_channels = Some(ch); + self + } + + /// Add one accepted pixel format. Appends — call multiple times to + /// list several. + pub fn with_pixel_format(mut self, fmt: PixelFormat) -> Self { + self.accepted_pixel_formats.push(fmt); + self + } + + /// Replace the accepted pixel-format set wholesale. + pub fn with_pixel_formats(mut self, fmts: Vec) -> Self { + self.accepted_pixel_formats = fmts; + self + } +} + +impl fmt::Display for CodecCapabilities { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.flag_string(), self.implementation) + } +} + +impl CodecCapabilities { + /// Whether this implementation's max-* restrictions are compatible + /// with the requested codec parameters. `for_encode` is reserved + /// for restrictions that apply asymmetrically. Used by the + /// registry's `make_decoder` / `make_encoder` walker and by + /// out-of-tree selection layers (e.g. `oxideav-pipeline`'s + /// `CodecPreferences` filter). + pub fn fits_params(&self, p: &crate::CodecParameters, for_encode: bool) -> bool { + let _ = for_encode; + if let (Some(max), Some(w)) = (self.max_width, p.width) { + if w > max { + return false; + } + } + if let (Some(max), Some(h)) = (self.max_height, p.height) { + if h > max { + return false; + } + } + if let (Some(max), Some(br)) = (self.max_bitrate, p.bit_rate) { + if br > max { + return false; + } + } + if let (Some(max), Some(sr)) = (self.max_sample_rate, p.sample_rate) { + if sr > max { + return false; + } + } + if let (Some(max), Some(ch)) = (self.max_channels, p.channels) { + if ch > max { + return false; + } + } + true + } +} diff --git a/crates/vendor/oxideav-core/src/engine.rs b/crates/vendor/oxideav-core/src/engine.rs new file mode 100644 index 00000000..cee278f0 --- /dev/null +++ b/crates/vendor/oxideav-core/src/engine.rs @@ -0,0 +1,126 @@ +//! Per-codec hardware engine probing. +//! +//! Each HW-accel sibling crate (oxideav-nvidia, oxideav-vaapi, +//! oxideav-vdpau, oxideav-vulkan-video, oxideav-videotoolbox) attaches +//! an [`EngineProbeFn`] to every [`crate::CodecInfo`] it registers via +//! [`crate::CodecInfo::with_engine_probe`]. The CLI's `info` command +//! (and any other consumer) calls the probe on demand to enumerate +//! the physical / logical engines that backend can dispatch to — +//! GPU name, driver version, per-codec capability matrix, etc. +//! +//! Probes are called on demand, not at registration time, so the cost +//! of opening device handles + querying capabilities is only paid +//! when someone asks. Probes should be idempotent and side-effect +//! free; consumers may call them more than once per process. +//! +//! There is no distributed slice and no collection macro: engine info +//! travels with each [`crate::CodecInfo`], matching the explicit-calls +//! pattern already used by `oxideav-meta`'s `register_all`. Consumers +//! that want to enumerate engines walk the codec registry, group +//! entries by [`crate::CodecInfo::engine_id`], and call each backend's +//! [`EngineProbeFn`] at most once per group. + +/// A single hardware engine the backend can dispatch to. For NVIDIA / +/// Vulkan / VA-API DRM, this is one entry per physical GPU. For +/// VDPAU on a single-X11-display system, this is one entry per X +/// screen. For VideoToolbox on Apple Silicon, this is one entry per +/// SoC. +#[derive(Clone, Debug)] +pub struct HwDeviceInfo { + /// Human-readable device name. e.g. "NVIDIA GeForce RTX 5080", + /// "Intel(R) UHD Graphics 770", "Apple M3 Max Media Engine". + pub name: String, + /// Driver / runtime version, if reportable. e.g. "580.95.05", + /// "Mesa 24.2", "VideoToolbox (system)". + pub driver_version: Option, + /// API version the backend speaks. e.g. "CUDA 12.6", + /// "VDPAU API 1", "Vulkan 1.4", "VA-API 1.22". + pub api_version: Option, + /// On-card memory in bytes if known. Discrete GPUs report a real + /// figure; integrated and shared-memory engines usually report + /// `None`. + pub total_memory_bytes: Option, + /// Backend-specific extras keyed by string. e.g. for NVIDIA: + /// `("compute_capability", "12.0")`. CLI prints these as + /// `key = value` in a sub-block. Order is preserved. + pub extra: Vec<(String, String)>, + /// Per-codec capabilities for codecs this engine can decode and/or + /// encode. + pub codecs: Vec, +} + +/// Capabilities of a single codec on a single device. +#[derive(Clone, Debug)] +pub struct HwCodecCaps { + /// Codec id matching `oxideav_core::CodecId`. e.g. "h264", "hevc", + /// "av1", "vp9". Should be the same string the SW codec uses. + pub codec: String, + /// Whether this device can decode this codec. + pub decode: bool, + /// Whether this device can encode this codec. + pub encode: bool, + /// Max coded width supported, if reportable. + pub max_width: Option, + /// Max coded height supported, if reportable. + pub max_height: Option, + /// Max bit-depth supported, if reportable. Typically 8, 10, or 12. + pub max_bit_depth: Option, + /// Profile names (backend-specific). e.g. for H.264 on NVDEC: + /// `["Baseline", "Main", "High"]`. + pub profiles: Vec, + /// Backend-specific extras (e.g. `("max_dpb_slots", "17")`). + pub extra: Vec<(String, String)>, +} + +/// Function signature for a backend's engine probe. Returns one entry +/// per device the backend currently sees. Call cheaply; consumers may +/// call multiple times per process. +pub type EngineProbeFn = fn() -> Vec; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hw_device_info_clone_and_extras_round_trip() { + let info = HwDeviceInfo { + name: "Test GPU".into(), + driver_version: Some("1.0".into()), + api_version: Some("API 1".into()), + total_memory_bytes: Some(16 * 1024 * 1024 * 1024), + extra: vec![("compute_capability".into(), "12.0".into())], + codecs: vec![HwCodecCaps { + codec: "h264".into(), + decode: true, + encode: true, + max_width: Some(8192), + max_height: Some(8192), + max_bit_depth: Some(8), + profiles: vec!["Baseline".into(), "Main".into(), "High".into()], + extra: vec![], + }], + }; + let clone = info.clone(); + assert_eq!(clone.name, "Test GPU"); + assert_eq!(clone.driver_version.as_deref(), Some("1.0")); + assert_eq!(clone.api_version.as_deref(), Some("API 1")); + assert_eq!(clone.total_memory_bytes, Some(16 * 1024 * 1024 * 1024)); + assert_eq!(clone.extra.len(), 1); + assert_eq!(clone.extra[0].0, "compute_capability"); + assert_eq!(clone.extra[0].1, "12.0"); + assert_eq!(clone.codecs.len(), 1); + assert_eq!(clone.codecs[0].codec, "h264"); + assert!(clone.codecs[0].decode); + assert!(clone.codecs[0].encode); + assert_eq!(clone.codecs[0].profiles.len(), 3); + } + + #[test] + fn engine_probe_fn_is_callable() { + fn empty_probe() -> Vec { + vec![] + } + let probe: EngineProbeFn = empty_probe; + assert!(probe().is_empty()); + } +} diff --git a/crates/vendor/oxideav-core/src/error.rs b/crates/vendor/oxideav-core/src/error.rs new file mode 100644 index 00000000..4e20300f --- /dev/null +++ b/crates/vendor/oxideav-core/src/error.rs @@ -0,0 +1,205 @@ +//! Shared error type for oxideav. +//! +//! # Taxonomy +//! +//! Pick the variant by what the *caller* should do about it: +//! +//! * [`Error::InvalidData`] — the input violates its format's rules; +//! retrying or feeding more bytes won't help. Skip the packet / abort +//! the stream. +//! * [`Error::Unsupported`] — the input is (as far as we can tell) +//! valid, but exercises a feature this implementation doesn't cover. +//! A different implementation might succeed. +//! * [`Error::Eof`] — the logical end of the stream was reached. Not a +//! failure when it happens between packets; drain and stop. +//! * [`Error::NeedMore`] — a push-style parser stopped mid-unit; feed +//! more bytes and call again. Unlike `Eof`, progress resumes. +//! * [`Error::FormatNotFound`] / [`Error::CodecNotFound`] — registry +//! probe/lookup misses. +//! * [`Error::ResourceExhausted`] — a configured cap or pool limit +//! fired; hard-reject the input or back off, never retry blindly. +//! * [`Error::Io`] / [`Error::Other`] — transport problems and +//! everything else. + +use thiserror::Error; + +/// Convenience alias: `std::result::Result` pinned to [`enum@Error`]. +pub type Result = std::result::Result; + +/// The error type shared by every oxideav crate. See the +/// [module docs](self) for which variant to pick. +#[derive(Debug, Error)] +pub enum Error { + /// An underlying transport / filesystem operation failed. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + /// Valid input exercising a feature this implementation lacks. + #[error("unsupported: {0}")] + Unsupported(String), + + /// The input violates its format's rules; not retryable. + #[error("invalid data: {0}")] + InvalidData(String), + + /// Logical end of stream (clean between packets; short otherwise). + #[error("end of stream")] + Eof, + + /// Push-parser starvation: feed more bytes and call again. + #[error("need more data")] + NeedMore, + + /// No registered container format matched the probe subject. + #[error("format not found: {0}")] + FormatNotFound(String), + + /// No registered codec matched the requested name or tag. + #[error("codec not found: {0}")] + CodecNotFound(String), + + /// A decoder (or arena pool) refused to allocate or proceed because + /// doing so would exceed a configured [`DecoderLimits`](crate::DecoderLimits) + /// cap, or because a pool has no free slot. This is the canonical + /// "DoS protection fired" error — callers should treat it as a hard + /// rejection of the input or a transient backpressure signal, never + /// retry blindly. + #[error("resource exhausted: {0}")] + ResourceExhausted(String), + + /// Anything that doesn't fit the other variants; the message + /// carries the whole story. + #[error("{0}")] + Other(String), +} + +impl Error { + /// Construct an [`Error::Unsupported`] with the given message. + pub fn unsupported(msg: impl Into) -> Self { + Self::Unsupported(msg.into()) + } + + /// Construct an [`Error::InvalidData`] with the given message. + pub fn invalid(msg: impl Into) -> Self { + Self::InvalidData(msg.into()) + } + + /// Construct an [`Error::Other`] with the given message. + pub fn other(msg: impl Into) -> Self { + Self::Other(msg.into()) + } + + /// Construct a [`Error::ResourceExhausted`] with the given message. + /// Use this from any decoder that has just hit a `DecoderLimits` cap + /// or an arena-pool exhaustion. + pub fn resource_exhausted(msg: impl Into) -> Self { + Self::ResourceExhausted(msg.into()) + } + + /// Construct a [`Error::FormatNotFound`] with the given probe + /// subject (file name, extension, or magic description). + pub fn format_not_found(msg: impl Into) -> Self { + Self::FormatNotFound(msg.into()) + } + + /// Construct a [`Error::CodecNotFound`] with the codec name or tag + /// that missed the registry. + pub fn codec_not_found(msg: impl Into) -> Self { + Self::CodecNotFound(msg.into()) + } + + /// `true` for [`Error::Eof`]. `Error` cannot implement `PartialEq` + /// (the `Io` variant wraps `std::io::Error`), so drain loops that + /// need "stop cleanly on end-of-stream" branch on this instead of + /// a `matches!` at every call site. + pub fn is_eof(&self) -> bool { + matches!(self, Self::Eof) + } + + /// `true` for [`Error::NeedMore`] — the push-parser "feed me more + /// bytes and retry" signal. + pub fn is_need_more(&self) -> bool { + matches!(self, Self::NeedMore) + } + + /// `true` for [`Error::ResourceExhausted`] — the "DoS cap fired" + /// signal that must not be blindly retried. + pub fn is_resource_exhausted(&self) -> bool { + matches!(self, Self::ResourceExhausted(_)) + } + + /// `true` when the error only says the stream stopped short — + /// [`Error::Eof`] or [`Error::NeedMore`] — rather than reporting + /// malformed or unsupported content. Useful for probe loops that + /// try successive parsers on a growing prefix: starvation means + /// "inconclusive, buffer more", anything else means "this parser + /// has a verdict". + pub fn is_starved(&self) -> bool { + matches!(self, Self::Eof | Self::NeedMore) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constructors_produce_matching_variants() { + assert!(matches!( + Error::format_not_found("mkv"), + Error::FormatNotFound(s) if s == "mkv" + )); + assert!(matches!( + Error::codec_not_found("vp8"), + Error::CodecNotFound(s) if s == "vp8" + )); + assert!(matches!( + Error::resource_exhausted("pool"), + Error::ResourceExhausted(s) if s == "pool" + )); + } + + #[test] + fn predicates_partition_correctly() { + assert!(Error::Eof.is_eof()); + assert!(!Error::Eof.is_need_more()); + assert!(Error::NeedMore.is_need_more()); + assert!(!Error::NeedMore.is_eof()); + assert!(Error::Eof.is_starved()); + assert!(Error::NeedMore.is_starved()); + assert!(Error::resource_exhausted("x").is_resource_exhausted()); + for e in [ + Error::invalid("bad"), + Error::unsupported("feature"), + Error::other("misc"), + Error::format_not_found("f"), + Error::codec_not_found("c"), + ] { + assert!(!e.is_eof()); + assert!(!e.is_need_more()); + assert!(!e.is_starved()); + assert!(!e.is_resource_exhausted()); + } + } + + #[test] + fn display_messages_are_stable() { + assert_eq!(Error::Eof.to_string(), "end of stream"); + assert_eq!(Error::NeedMore.to_string(), "need more data"); + assert_eq!(Error::invalid("x").to_string(), "invalid data: x"); + assert_eq!(Error::unsupported("y").to_string(), "unsupported: y"); + assert_eq!( + Error::format_not_found("z").to_string(), + "format not found: z" + ); + assert_eq!( + Error::codec_not_found("w").to_string(), + "codec not found: w" + ); + assert_eq!( + Error::resource_exhausted("v").to_string(), + "resource exhausted: v" + ); + assert_eq!(Error::other("u").to_string(), "u"); + } +} diff --git a/crates/vendor/oxideav-core/src/execution.rs b/crates/vendor/oxideav-core/src/execution.rs new file mode 100644 index 00000000..2d37d4c2 --- /dev/null +++ b/crates/vendor/oxideav-core/src/execution.rs @@ -0,0 +1,121 @@ +//! Runtime hints passed from the executor to codecs and filters. +//! +//! An [`ExecutionContext`] carries advisory information — today only a +//! thread budget — that codecs can use to tune their internal +//! parallelism. Codecs that don't care can ignore it; the default trait +//! method on [`Decoder`](../../oxideav_codec/trait.Decoder.html) / +//! [`Encoder`](../../oxideav_codec/trait.Encoder.html) is a no-op. +//! +//! # Threading contract +//! +//! The context is the **single threading authority** for a codec: +//! +//! * A codec runs **serial until told otherwise** — before +//! `set_execution_context` is called (or when it never is), internal +//! fan-out is one worker. +//! * Every internal fan-out is bounded through +//! [`ExecutionContext::effective_workers`], never by querying the host +//! directly. Host-derived budgets are the *caller's* decision, made by +//! constructing the context with [`ExecutionContext::auto`]. +//! * Threading stays optional: a codec with no internal parallelism +//! simply keeps the default no-op trait method, and callers must +//! always work with a codec that runs serial regardless of the budget +//! they granted. + +/// Advisory runtime information handed to a codec after construction. +/// +/// The struct is deliberately tiny for now. New fields can be added +/// without breaking API consumers that already construct the value via +/// [`ExecutionContext::serial`] or [`ExecutionContext::with_threads`]. +#[derive(Clone, Debug)] +pub struct ExecutionContext { + /// Advisory cap on how many threads a codec may use for its own + /// internal parallelism (slice-parallel decode, GOP-parallel decode, + /// etc.). Always `≥ 1`. `1` means "caller requests serial execution + /// from this codec" — obey it unless you have a very good reason. + pub threads: usize, +} + +impl ExecutionContext { + /// Ask the codec to run strictly single-threaded. + pub const fn serial() -> Self { + Self { threads: 1 } + } + + /// Budget the codec to at most `threads` internal workers. Values + /// below 1 are clamped up to 1. + pub fn with_threads(threads: usize) -> Self { + Self { + threads: threads.max(1), + } + } + + /// Derive the budget from the host: + /// [`std::thread::available_parallelism`], falling back to `1` when + /// the host refuses to answer. + /// + /// This is the **caller-side** convenience for "use the machine". + /// Codecs never call it — they receive whatever budget the caller + /// chose and bound their fan-out with [`Self::effective_workers`]. + pub fn auto() -> Self { + let threads = std::thread::available_parallelism() + .map(std::num::NonZeroUsize::get) + .unwrap_or(1); + Self { threads } + } + + /// Bound a codec-internal fan-out: the number of workers to spawn + /// for `work_units` independent units of work under this budget. + /// + /// Returns `min(self.threads, work_units)`, and never less than 1 + /// (`work_units == 0` still yields 1 so degenerate inputs stay on + /// the plain serial path). This is the one clamp codecs use for + /// every slice-/tile-/field-/GOP-parallel dispatch; querying host + /// parallelism directly from codec code is out of contract. + pub fn effective_workers(&self, work_units: usize) -> usize { + self.threads.min(work_units).max(1) + } +} + +impl Default for ExecutionContext { + fn default() -> Self { + Self::serial() + } +} + +#[cfg(test)] +mod tests { + use super::ExecutionContext; + + #[test] + fn serial_is_one_thread_and_default() { + assert_eq!(ExecutionContext::serial().threads, 1); + assert_eq!(ExecutionContext::default().threads, 1); + } + + #[test] + fn with_threads_clamps_up_to_one() { + assert_eq!(ExecutionContext::with_threads(0).threads, 1); + assert_eq!(ExecutionContext::with_threads(1).threads, 1); + assert_eq!(ExecutionContext::with_threads(8).threads, 8); + } + + #[test] + fn auto_is_at_least_one() { + assert!(ExecutionContext::auto().threads >= 1); + } + + #[test] + fn effective_workers_clamps_both_sides() { + let ctx = ExecutionContext::with_threads(4); + assert_eq!(ctx.effective_workers(0), 1); + assert_eq!(ctx.effective_workers(1), 1); + assert_eq!(ctx.effective_workers(3), 3); + assert_eq!(ctx.effective_workers(4), 4); + assert_eq!(ctx.effective_workers(64), 4); + + let serial = ExecutionContext::serial(); + assert_eq!(serial.effective_workers(64), 1); + assert_eq!(serial.effective_workers(0), 1); + } +} diff --git a/crates/vendor/oxideav-core/src/filter.rs b/crates/vendor/oxideav-core/src/filter.rs new file mode 100644 index 00000000..9b671924 --- /dev/null +++ b/crates/vendor/oxideav-core/src/filter.rs @@ -0,0 +1,285 @@ +//! Multi-stream filter model. +//! +//! A [`StreamFilter`] is a node in the pipeline that consumes N input streams +//! and produces M output streams, where input and output media kinds may +//! differ. This generalises the single-kind `AudioFilter` / +//! `ImageFilter` traits and is the substrate used by +//! `oxideav-pipeline`'s filter registry. +//! +//! # Ports +//! +//! Every filter declares its [`PortSpec`]s at construction time. The pipeline +//! introspects ports to: +//! +//! - wire each input port to the correct upstream stream, +//! - synthesise [`StreamInfo`](crate::StreamInfo) entries for each output +//! port so sinks see the forthcoming streams in their `start()` call +//! (before the first frame arrives), +//! - size the per-port back-pressure channels. +//! +//! Output [`PortParams`] carry the concrete stream parameters (sample rate, +//! resolution, etc.) — not placeholders. A spectrogram filter that renders +//! 800×256 RGB at 30 fps declares those numbers in its `Video` port params +//! the moment it's built. +//! +//! # Back-pressure and frame emission +//! +//! `push` and `flush` take a [`FilterContext`] whose [`emit`](FilterContext::emit) +//! method is a per-port bounded-channel send. Filters call `emit` as many +//! times as they like per `push`, interleaving ports freely (`emit(0, a); +//! emit(0, a); emit(1, v); emit(0, a)` is fine). A slow consumer on one +//! port blocks only that port's `emit`, not the whole filter, so a 30 Hz +//! video output cannot stall a 48 kHz audio passthrough. +//! +//! # PTS responsibility +//! +//! Filters own their output frames' timestamps. For rate-changing or +//! kind-changing filters (e.g. an audio → video visualiser), the filter +//! is the only thing that knows how to map source pts onto output pts. +//! The pipeline does not rewrite pts on emitted frames. Downstream stages +//! expect constant-frame-rate outputs to have integer-multiple pts +//! spacing; vary from that at your peril. + +use crate::{Error, Frame, MediaType, PixelFormat, Result, SampleFormat, TimeBase}; + +/// Description of one port (input or output) exposed by a filter. +#[derive(Clone, Debug)] +pub struct PortSpec { + /// Port name, unique per filter-port-direction (e.g. `"audio"` / + /// `"video"` / `"left"` / `"right"`). Used by the schema to address + /// a specific output when the default "route by kind" isn't enough. + pub name: String, + /// Media type this port carries. + pub kind: MediaType, + /// Concrete stream parameters. For output ports these are + /// authoritative; for input ports they describe what the filter + /// *expects* and are used as a shape hint at wiring time. + pub params: PortParams, +} + +impl PortSpec { + /// Convenience: audio port with the given params. + pub fn audio( + name: impl Into, + sample_rate: u32, + channels: u16, + format: SampleFormat, + ) -> Self { + Self { + name: name.into(), + kind: MediaType::Audio, + params: PortParams::Audio { + sample_rate, + channels, + format, + }, + } + } + + /// Convenience: video port with the given params. + pub fn video( + name: impl Into, + width: u32, + height: u32, + format: PixelFormat, + time_base: TimeBase, + ) -> Self { + Self { + name: name.into(), + kind: MediaType::Video, + params: PortParams::Video { + width, + height, + format, + time_base, + }, + } + } +} + +/// Concrete stream parameters for a port. +/// +/// Subtitle/Metadata are placeholders — they exist so the pipeline +/// can route those kinds once filters that emit them land, but no +/// current filter consumes them. +#[derive(Clone, Debug)] +pub enum PortParams { + /// An audio port. + Audio { + /// Samples per second per channel. + sample_rate: u32, + /// Channel count. + channels: u16, + /// Sample format flowing through the port. + format: SampleFormat, + }, + /// A video port. + Video { + /// Picture width in pixels. + width: u32, + /// Picture height in pixels. + height: u32, + /// Pixel format flowing through the port. + format: PixelFormat, + /// Time base the port's frame timestamps are expressed in. + time_base: TimeBase, + }, + /// A subtitle-cue port (placeholder — no current filter emits it). + Subtitle, + /// A metadata/data port (placeholder — no current filter emits it). + Metadata, +} + +impl PortParams { + /// Media type implied by the variant. + pub fn kind(&self) -> MediaType { + match self { + PortParams::Audio { .. } => MediaType::Audio, + PortParams::Video { .. } => MediaType::Video, + PortParams::Subtitle => MediaType::Subtitle, + PortParams::Metadata => MediaType::Data, + } + } +} + +/// Runtime plumbing handed to [`StreamFilter::push`] / [`StreamFilter::flush`]. +/// +/// The executor implements this to route emitted frames to the correct +/// downstream stage. `emit` is a bounded-channel send and may block if a +/// consumer is slow, which provides natural per-port back-pressure. +pub trait FilterContext { + /// Emit a frame on the named output port. Blocks if the downstream + /// channel is full. + fn emit(&mut self, output_port: usize, frame: Frame) -> Result<()>; +} + +/// Multi-stream filter. +/// +/// See the [module docs](self) for the overall model. +pub trait StreamFilter: Send { + /// Input ports, ordered by port id. The returned slice is lifetime- + /// tied to `self`; implementors typically hold the spec as an + /// owned field and return a borrow. + fn input_ports(&self) -> &[PortSpec]; + + /// Output ports, ordered by port id. + fn output_ports(&self) -> &[PortSpec]; + + /// Process one input frame on `port`. The filter may call + /// [`FilterContext::emit`] zero or more times on any of its + /// output ports before returning. + fn push(&mut self, ctx: &mut dyn FilterContext, port: usize, frame: &Frame) -> Result<()>; + + /// Drain any internally buffered state at end-of-stream. Filters that + /// hold rolling windows (spectrogram) or temporal buffers (resample) + /// emit their remaining output here. + fn flush(&mut self, _ctx: &mut dyn FilterContext) -> Result<()> { + Ok(()) + } + + /// Reset internal state on a flow barrier (seek). Drops any buffered + /// frames silently — unlike `flush`, no frames are emitted, because + /// the upstream pipeline is going to start delivering frames from a + /// new wall-clock position. Filters with rolling windows + /// (spectrogram) or temporal smoothing should restart from empty so + /// the user sees a clean cut over the seek. + /// + /// Default no-op so stateless / freshly-restartable filters need no + /// boilerplate. + fn reset(&mut self) -> Result<()> { + Ok(()) + } +} + +/// Helper used by pipeline registries when a named filter isn't known. +pub fn unknown_filter_error(name: &str) -> Error { + Error::unsupported(format!("unknown filter '{name}'")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AudioFrame, VideoFrame, VideoPlane}; + + /// A trivial filter that doubles audio samples on port 0 (pass-through + /// with gain) and emits nothing on port 1 (video). Exists only to + /// prove the trait shape compiles and `FilterContext::emit` wires. + struct Fake { + inp: Vec, + outp: Vec, + } + + impl Fake { + fn new() -> Self { + Self { + inp: vec![PortSpec::audio("in", 48_000, 2, SampleFormat::S16)], + outp: vec![ + PortSpec::audio("audio", 48_000, 2, SampleFormat::S16), + PortSpec::video("video", 16, 8, PixelFormat::Rgb24, TimeBase::new(1, 30)), + ], + } + } + } + + impl StreamFilter for Fake { + fn input_ports(&self) -> &[PortSpec] { + &self.inp + } + fn output_ports(&self) -> &[PortSpec] { + &self.outp + } + fn push(&mut self, ctx: &mut dyn FilterContext, port: usize, frame: &Frame) -> Result<()> { + assert_eq!(port, 0); + if let Frame::Audio(a) = frame { + ctx.emit(0, Frame::Audio(a.clone()))?; + } + Ok(()) + } + } + + struct CollectCtx { + out: Vec<(usize, Frame)>, + } + impl FilterContext for CollectCtx { + fn emit(&mut self, port: usize, frame: Frame) -> Result<()> { + self.out.push((port, frame)); + Ok(()) + } + } + + #[test] + fn trait_compiles_and_ports_round_trip() { + let mut f = Fake::new(); + assert_eq!(f.input_ports().len(), 1); + assert_eq!(f.output_ports().len(), 2); + assert_eq!(f.output_ports()[0].kind, MediaType::Audio); + assert_eq!(f.output_ports()[1].kind, MediaType::Video); + + let audio = AudioFrame { + samples: 0, + pts: None, + data: vec![vec![]; 2], + }; + let mut ctx = CollectCtx { out: Vec::new() }; + f.push(&mut ctx, 0, &Frame::Audio(audio)).unwrap(); + assert_eq!(ctx.out.len(), 1); + matches!(&ctx.out[0].1, Frame::Video(_)); + } + + #[test] + fn video_frame_shape_is_unchanged() { + // Guard: this trait module does not change the VideoFrame / + // AudioFrame types. Anything consuming `Frame` via the trait + // treats them as opaque carriers. Stream-level properties + // (format/dimensions/time_base) live on the stream's + // `CodecParameters`, not the frame. + let vf = VideoFrame { + pts: None, + planes: vec![VideoPlane { + stride: 12, + data: vec![0u8; 24], + }], + }; + assert_eq!(vf.planes.len(), 1); + } +} diff --git a/crates/vendor/oxideav-core/src/format.rs b/crates/vendor/oxideav-core/src/format.rs new file mode 100644 index 00000000..18f22f8e --- /dev/null +++ b/crates/vendor/oxideav-core/src/format.rs @@ -0,0 +1,2394 @@ +//! Media-type and sample/pixel format enumerations. +//! +//! Audio channel ordering follows SMPTE 2036-2 / ITU-R BS.775 conventions +//! for surround layouts; per-channel positions are named with the +//! WAVEFORMATEXTENSIBLE "front-left, front-right, …" vocabulary. + +/// Broad category of a stream's payload. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MediaType { + /// Audio samples. + Audio, + /// Video pictures. + Video, + /// Timed-text / bitmap subtitle cues. + Subtitle, + /// Opaque non-media payload (timecodes, klv, chapters, …). + Data, + /// Category not (yet) determined. + Unknown, +} + +/// A single speaker position within a multi-channel audio layout. +/// +/// Names follow the WAVEFORMATEXTENSIBLE / SMPTE convention. +/// `Side*` and `Back*` are kept distinct (mirroring 7.1's +/// L/R + Ls/Rs + Lb/Rb separation) so codecs that surface the +/// distinction don't collapse it. `Lr`/`Rr` (rear / back-rear) are aliases +/// for `BackLeft`/`BackRight` in this taxonomy — the rear pair sits behind +/// the listener on the room's centreline-extension, the side pair is at +/// roughly ±90° from front. The enum is `#[non_exhaustive]` so additional +/// positions (height channels for Atmos / Auro-3D, etc.) can be added +/// without breaking downstream match arms. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ChannelPosition { + /// Front-left (L). 30° left of centre in BS.775 listening geometry. + FrontLeft, + /// Front-right (R). 30° right of centre. + FrontRight, + /// Front-centre (C). Direct centre, 0°. + FrontCenter, + /// Low-frequency effects (LFE). Sub-bass, no positional meaning. + LowFrequency, + /// Back-left (Lb / Lr). Behind the listener, ±150° in 7.1. + BackLeft, + /// Back-right (Rb / Rr). Behind the listener, mirror of `BackLeft`. + BackRight, + /// Front left-of-centre (Lc). Used in cinema 7.1 SDDS layouts. + FrontLeftOfCenter, + /// Front right-of-centre (Rc). Mirror of `FrontLeftOfCenter`. + FrontRightOfCenter, + /// Back-centre (Cs). Single rear channel for 6.1 / BS.775 4.0. + BackCenter, + /// Side-left (Ls). ±90° on the listener's left in 5.1 / 7.1. + SideLeft, + /// Side-right (Rs). Mirror of `SideLeft`. + SideRight, + /// Top front-left. Atmos / Auro-3D height layer (placeholder). + TopFrontLeft, + /// Top front-right. Atmos / Auro-3D height layer (placeholder). + TopFrontRight, + /// Top back-left. Atmos / Auro-3D ceiling layer (placeholder). + TopBackLeft, + /// Top back-right. Atmos / Auro-3D ceiling layer (placeholder). + TopBackRight, +} + +/// Audio channel layout — names a fixed ordered tuple of speaker +/// positions, OR carries a discrete fallback count when the layout is +/// unknown / non-standard. +/// +/// Channel orderings are taken from ITU-R BS.775 (5.1 / 7.1 surround +/// reference) and SMPTE ST 2036-2 (audio channel ordering for UHDTV). +/// For 5.1 the canonical order this crate adopts is +/// `L, R, C, LFE, Ls, Rs` (the WAVEFORMATEXTENSIBLE / Vorbis / Opus +/// convention). 7.1 extends that with `Lb, Rb` (back-rear pair). +/// +/// The `Stereo` variant covers both regular two-channel stereo and the +/// AC-3 / AC-4 matrix-encoded downmix carriers `Lo/Ro` ("two of", +/// downmix-compatible) and `Lt/Rt` ("matrix-encoded for Pro Logic +/// extraction"); the dedicated [`LoRo`](ChannelLayout::LoRo) / +/// [`LtRt`](ChannelLayout::LtRt) variants surface the distinction +/// explicitly when a downstream filter or muxer needs it. +/// +/// `DiscreteN(n)` is the catch-all for "we know there are `n` channels +/// but no recognised layout" — used when a codec produces an unusual +/// channel count (>8) or when the container failed to surface a layout +/// flag. It is the only variant whose `position()` returns `None`. +/// +/// Marked `#[non_exhaustive]` so additional standard layouts (Atmos +/// 7.1.4, Auro-3D 9.1, …) can be added without breaking match-exhaustive +/// downstream consumers. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ChannelLayout { + /// Mono (1ch): C. + Mono, + /// Stereo (2ch): L, R. + Stereo, + /// 2.1 (3ch): L, R, LFE. + Stereo21, + /// 3.0 surround (3ch): L, R, C. + Surround30, + /// Quadraphonic (4ch): L, R, Ls, Rs — no centre, side surrounds. + Quad, + /// 4.0 surround per BS.775 (4ch): L, R, C, Cs — centre + back surround. + Surround40, + /// 4.1 surround (5ch): L, R, C, Cs, LFE. + Surround41, + /// 5.0 surround (5ch): L, R, C, Ls, Rs. + Surround50, + /// 5.1 surround (6ch): L, R, C, LFE, Ls, Rs. + Surround51, + /// 6.0 surround (6ch): L, R, C, Cs, Ls, Rs. + Surround60, + /// 6.1 surround (7ch): L, R, C, LFE, Cs, Ls, Rs. + Surround61, + /// 7.0 surround (7ch): L, R, C, Ls, Rs, Lb, Rb. + Surround70, + /// 7.1 surround (8ch): L, R, C, LFE, Ls, Rs, Lb, Rb. + Surround71, + /// AC-3 / AC-4 Lo/Ro stereo downmix (2ch). Two-channel mix preserving + /// downmix-compatibility coefficients; not matrix-encoded. + LoRo, + /// AC-3 / AC-4 Lt/Rt stereo downmix (2ch). Two-channel matrix-encoded + /// downmix carrying surround information for Dolby Pro Logic decoding. + LtRt, + /// Discrete fallback: `n` channels with no recognised layout. Used for + /// unusual / >8ch / unknown layouts surfaced by exotic codecs or + /// containers that drop layout flags. + DiscreteN(u16), +} + +impl ChannelLayout { + /// Number of channels in this layout. + pub fn channel_count(&self) -> u16 { + match self { + Self::Mono => 1, + Self::Stereo | Self::LoRo | Self::LtRt => 2, + Self::Stereo21 | Self::Surround30 => 3, + Self::Quad | Self::Surround40 => 4, + Self::Surround41 | Self::Surround50 => 5, + Self::Surround51 | Self::Surround60 => 6, + Self::Surround61 | Self::Surround70 => 7, + Self::Surround71 => 8, + Self::DiscreteN(n) => *n, + } + } + + /// Speaker positions in canonical order. Returns an empty slice for + /// `DiscreteN` since the layout is unknown — call [`positions_owned`] + /// to get a `Vec` if you need to enumerate slots regardless of + /// known/unknown status. + /// + /// [`positions_owned`]: Self::positions_owned + pub fn positions(&self) -> &'static [ChannelPosition] { + use ChannelPosition::*; + match self { + Self::Mono => &[FrontCenter], + Self::Stereo | Self::LoRo | Self::LtRt => &[FrontLeft, FrontRight], + Self::Stereo21 => &[FrontLeft, FrontRight, LowFrequency], + Self::Surround30 => &[FrontLeft, FrontRight, FrontCenter], + Self::Quad => &[FrontLeft, FrontRight, SideLeft, SideRight], + Self::Surround40 => &[FrontLeft, FrontRight, FrontCenter, BackCenter], + Self::Surround41 => &[FrontLeft, FrontRight, FrontCenter, BackCenter, LowFrequency], + Self::Surround50 => &[FrontLeft, FrontRight, FrontCenter, SideLeft, SideRight], + Self::Surround51 => &[ + FrontLeft, + FrontRight, + FrontCenter, + LowFrequency, + SideLeft, + SideRight, + ], + Self::Surround60 => &[ + FrontLeft, + FrontRight, + FrontCenter, + BackCenter, + SideLeft, + SideRight, + ], + Self::Surround61 => &[ + FrontLeft, + FrontRight, + FrontCenter, + LowFrequency, + BackCenter, + SideLeft, + SideRight, + ], + Self::Surround70 => &[ + FrontLeft, + FrontRight, + FrontCenter, + SideLeft, + SideRight, + BackLeft, + BackRight, + ], + Self::Surround71 => &[ + FrontLeft, + FrontRight, + FrontCenter, + LowFrequency, + SideLeft, + SideRight, + BackLeft, + BackRight, + ], + Self::DiscreteN(_) => &[], + } + } + + /// Owned position list. For known layouts this clones [`positions`]; + /// for `DiscreteN(n)` it returns an empty `Vec` (positions remain + /// unknown). Provided so callers that just want "give me positions + /// for any layout" don't have to special-case the discrete arm. + /// + /// [`positions`]: Self::positions + pub fn positions_owned(&self) -> Vec { + self.positions().to_vec() + } + + /// Speaker position at slot `idx` in canonical order, or `None` for + /// out-of-range slots and for `DiscreteN` (where the layout is + /// unknown). + pub fn position(&self, idx: usize) -> Option { + self.positions().get(idx).copied() + } + + /// True when this layout carries a low-frequency-effects (LFE) channel. + pub fn has_lfe(&self) -> bool { + self.positions() + .iter() + .any(|p| matches!(p, ChannelPosition::LowFrequency)) + } + + /// True when this layout carries surround information (more than two + /// channels OR an LFE). `Stereo` / `Mono` return false; `LoRo` / + /// `LtRt` are 2-channel downmixes and also return false even though + /// they encode surround content (that's the whole point of a + /// downmix). + pub fn is_surround(&self) -> bool { + self.channel_count() > 2 || self.has_lfe() + } + + /// Back-compat bridge: infer a layout from a bare channel count. + /// + /// This mapping is what lets codecs that haven't been updated to set + /// a layout explicitly continue to work: they keep producing a count + /// and we infer the most-common layout for that count. The choices + /// follow industry defaults — 5.1 wins for 6ch (more common than + /// 6.0), 7.1 wins for 8ch, and so on. + /// + /// | count | layout | + /// |-------|--------------| + /// | 1 | `Mono` | + /// | 2 | `Stereo` | + /// | 3 | `Surround30` | + /// | 4 | `Quad` | + /// | 5 | `Surround50` | + /// | 6 | `Surround51` | + /// | 7 | `Surround61` | + /// | 8 | `Surround71` | + /// | other | `DiscreteN` | + pub fn from_count(n: u16) -> ChannelLayout { + match n { + 1 => Self::Mono, + 2 => Self::Stereo, + 3 => Self::Surround30, + 4 => Self::Quad, + 5 => Self::Surround50, + 6 => Self::Surround51, + 7 => Self::Surround61, + 8 => Self::Surround71, + other => Self::DiscreteN(other), + } + } +} + +impl std::fmt::Display for ChannelLayout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Mono => "mono", + Self::Stereo => "stereo", + Self::Stereo21 => "2.1", + Self::Surround30 => "3.0", + Self::Quad => "quad", + Self::Surround40 => "4.0", + Self::Surround41 => "4.1", + Self::Surround50 => "5.0", + Self::Surround51 => "5.1", + Self::Surround60 => "6.0", + Self::Surround61 => "6.1", + Self::Surround70 => "7.0", + Self::Surround71 => "7.1", + Self::LoRo => "loro", + Self::LtRt => "ltrt", + Self::DiscreteN(n) => return write!(f, "discrete{n}"), + }; + f.write_str(s) + } +} + +/// Error returned by the [`ChannelLayout`] `FromStr` impl when the input +/// doesn't match any recognised layout name. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseChannelLayoutError(pub String); + +impl std::fmt::Display for ParseChannelLayoutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "unrecognised channel layout: {:?}", self.0) + } +} + +impl std::error::Error for ParseChannelLayoutError {} + +impl std::str::FromStr for ChannelLayout { + type Err = ParseChannelLayoutError; + + fn from_str(s: &str) -> Result { + let lower = s.trim().to_ascii_lowercase(); + let layout = match lower.as_str() { + "mono" | "1.0" => Self::Mono, + "stereo" | "2.0" => Self::Stereo, + "2.1" => Self::Stereo21, + "3.0" | "surround3" | "surround30" => Self::Surround30, + "quad" => Self::Quad, + "4.0" | "surround4" | "surround40" => Self::Surround40, + "4.1" | "surround41" => Self::Surround41, + "5.0" | "surround5" | "surround50" => Self::Surround50, + "5.1" | "surround51" => Self::Surround51, + "6.0" | "surround6" | "surround60" => Self::Surround60, + "6.1" | "surround61" => Self::Surround61, + "7.0" | "surround7" | "surround70" => Self::Surround70, + "7.1" | "surround71" => Self::Surround71, + "loro" | "lo/ro" => Self::LoRo, + "ltrt" | "lt/rt" => Self::LtRt, + other => { + if let Some(rest) = other.strip_prefix("discrete") { + if let Ok(n) = rest.parse::() { + return Ok(Self::DiscreteN(n)); + } + } + return Err(ParseChannelLayoutError(s.to_owned())); + } + }; + Ok(layout) + } +} + +/// Audio sample format. +/// +/// Variants carry **stable explicit discriminants** — the integer value +/// of `SampleFormat::S16 as u8` is part of the public ABI. Add new +/// variants only at the end with a fresh number; never reorder, renumber, +/// or remove. `#[non_exhaustive]` lets the enum grow without breaking +/// downstream `match` statements; pinned discriminants additionally let +/// the format round-trip through any byte-stable serialization +/// (config files, capability blobs, IPC) without losing meaning across +/// crate versions. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +#[repr(u8)] +pub enum SampleFormat { + /// Unsigned 8-bit, interleaved. + U8 = 0, + /// Signed 8-bit, interleaved. Native format of Amiga 8SVX and MOD samples. + S8 = 1, + /// Signed 16-bit little-endian, interleaved. + S16 = 2, + /// Signed 24-bit packed (3 bytes/sample) little-endian, interleaved. + S24 = 3, + /// Signed 32-bit little-endian, interleaved. + S32 = 4, + /// 32-bit IEEE float, interleaved. + F32 = 5, + /// 64-bit IEEE float, interleaved. + F64 = 6, + /// Unsigned 8-bit, planar (one plane per channel). + U8P = 7, + /// Signed 16-bit little-endian, planar (one plane per channel). + S16P = 8, + /// Signed 32-bit little-endian, planar (one plane per channel). + S32P = 9, + /// 32-bit IEEE float, planar (one plane per channel). + F32P = 10, + /// 64-bit IEEE float, planar (one plane per channel). + F64P = 11, +} + +impl SampleFormat { + /// `true` for the planar (one-plane-per-channel) variants. + pub fn is_planar(&self) -> bool { + matches!( + self, + Self::U8P | Self::S16P | Self::S32P | Self::F32P | Self::F64P + ) + } + + /// Bytes per sample *per channel*. + pub fn bytes_per_sample(&self) -> usize { + match self { + Self::U8 | Self::U8P | Self::S8 => 1, + Self::S16 | Self::S16P => 2, + Self::S24 => 3, + Self::S32 | Self::S32P | Self::F32 | Self::F32P => 4, + Self::F64 | Self::F64P => 8, + } + } + + /// `true` for the IEEE-float variants (32- or 64-bit, either layout). + pub fn is_float(&self) -> bool { + matches!(self, Self::F32 | Self::F64 | Self::F32P | Self::F64P) + } + + /// Number of `Vec` planes an [`AudioFrame`](crate::AudioFrame) + /// of this format carries for `channels` channels: planar formats + /// use one plane per channel, interleaved formats use one plane + /// total. + pub fn plane_count(&self, channels: u16) -> usize { + if self.is_planar() { + channels as usize + } else { + 1 + } + } +} + +/// Video pixel format. +/// +/// Variants carry **stable explicit discriminants** — the integer value +/// of `PixelFormat::Yuv420P as u16` is part of the public ABI. Add new +/// variants only at the end with a fresh number; never reorder, renumber, +/// or remove. `#[non_exhaustive]` lets the enum grow without breaking +/// downstream `match` statements; pinned discriminants additionally let +/// the format round-trip through any byte-stable serialization +/// (config files, capability blobs, IPC, on-disk caches) without losing +/// meaning across crate versions, and prevent inserts in the middle of +/// the enum from shifting every later variant's number (which +/// cargo-semver-checks rightly flags as a breaking change). +/// +/// The first six variants (`Yuv420P` through `Gray8`) are the original +/// formats produced by the early codec crates. Everything beyond that +/// is additional surface handled by `oxideav-pixfmt` and the still-image +/// codecs (PNG, GIF, still-JPEG). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +#[repr(u16)] +pub enum PixelFormat { + /// 8-bit YUV 4:2:0, planar (Y, U, V). + Yuv420P = 0, + /// 8-bit YUV 4:2:2, planar. + Yuv422P = 1, + /// 8-bit YUV 4:4:4, planar. + Yuv444P = 2, + /// Packed 8-bit RGB, 3 bytes/pixel. + Rgb24 = 3, + /// Packed 8-bit RGBA, 4 bytes/pixel. + Rgba = 4, + /// Packed 8-bit grayscale. + Gray8 = 5, + + // --- Palette --- + /// 8-bit palette indices — companion palette carried out of band. + Pal8 = 6, + + // --- Packed RGB/BGR swizzles --- + /// Packed 8-bit BGR, 3 bytes/pixel. + Bgr24 = 7, + /// Packed 8-bit BGRA, 4 bytes/pixel. + Bgra = 8, + /// Packed 8-bit ARGB, 4 bytes/pixel (alpha first). + Argb = 9, + /// Packed 8-bit ABGR, 4 bytes/pixel. + Abgr = 10, + + // --- Deeper packed RGB --- + /// Packed 16-bit-per-channel RGB, little-endian, 6 bytes/pixel. + Rgb48Le = 11, + /// Packed 16-bit-per-channel RGBA, little-endian, 8 bytes/pixel. + Rgba64Le = 12, + + // --- Grayscale deeper / partial bit depths --- + /// 16-bit little-endian grayscale. + Gray16Le = 13, + /// 10-bit grayscale in a 16-bit little-endian word. + Gray10Le = 14, + /// 12-bit grayscale in a 16-bit little-endian word. + Gray12Le = 15, + + // --- Higher-precision YUV --- + /// 10-bit YUV 4:2:0 planar, little-endian 16-bit storage. + Yuv420P10Le = 16, + /// 10-bit YUV 4:2:2 planar, little-endian 16-bit storage. + Yuv422P10Le = 17, + /// 10-bit YUV 4:4:4 planar, little-endian 16-bit storage. + Yuv444P10Le = 18, + /// 12-bit YUV 4:2:0 planar, little-endian 16-bit storage. + Yuv420P12Le = 19, + /// 12-bit YUV 4:2:2 planar, little-endian 16-bit storage. + Yuv422P12Le = 20, + /// 12-bit YUV 4:4:4 planar, little-endian 16-bit storage. + Yuv444P12Le = 21, + + // --- Full-range ("J") YUV --- + /// JPEG/full-range YUV 4:2:0 planar. + YuvJ420P = 22, + /// JPEG/full-range YUV 4:2:2 planar. + YuvJ422P = 23, + /// JPEG/full-range YUV 4:4:4 planar. + YuvJ444P = 24, + + // --- Semi-planar YUV --- + /// YUV 4:2:0, planar Y + interleaved UV (NV12). + Nv12 = 25, + /// YUV 4:2:0, planar Y + interleaved VU (NV21). + Nv21 = 26, + + // --- Gray + alpha / YUV + alpha --- + /// Packed grayscale + alpha, 2 bytes/pixel (Y, A). + Ya8 = 27, + /// Yuv420P with an additional full-resolution alpha plane. + Yuva420P = 28, + + // --- Mono (1 bit per pixel) --- + /// 1 bit per pixel, packed MSB-first, 0 = black. + MonoBlack = 29, + /// 1 bit per pixel, packed MSB-first, 0 = white. + MonoWhite = 30, + + // --- Interleaved YUV 4:2:2 --- + /// Packed 4:2:2, byte order Y0 U0 Y1 V0. + Yuyv422 = 31, + /// Packed 4:2:2, byte order U0 Y0 V0 Y1. + Uyvy422 = 32, + + // --- Print / prepress --- + /// Packed 8-bit CMYK, 4 bytes/pixel in byte order C, M, Y, K. + /// "Regular" convention: C=0 means no cyan ink (white), C=255 means + /// full cyan. Used by JPEG 4-component scans from non-Adobe encoders + /// and by many print-side image toolchains. Adobe Photoshop's + /// inverted CMYK (where 0 = full ink) is the separate + /// [`CmykInverted`](Self::CmykInverted) variant. + Cmyk = 33, + + // --- Wide-horizontal subsampled YUV --- + /// 8-bit YUV 4:1:1, planar (Y, U, V). Luma at full resolution; chroma + /// horizontally subsampled by 4 (each chroma sample covers a 4×1 + /// luma block), no vertical subsampling. Native sampling of + /// NTSC DV-25 and a legal JPEG sampling layout (luma H=4, V=1; + /// chroma H=V=1) emitted by some real-world JPEG corpora. + Yuv411P = 34, + + // --- Planar GBR / GBRA (RGB stored as planes in G,B,R order) --- + // + // High-bit-depth GBR(A) layouts used by MagicYUV, JPEG 2000, OpenEXR, + // TIFF and similar workflows that need lossless RGB at 10/12/14 bits + // per channel. Planes are ordered G, B, R (and A for the `Gbrap*` + // variants) — and + // each sample is stored as a 16-bit little-endian word with the + // top bits zero. The native 8-bit ([`Gbrp8`](Self::Gbrp8)) and + // full-width 16-bit ([`Gbrp16Le`](Self::Gbrp16Le) / + // [`Gbrap16Le`](Self::Gbrap16Le)) companions arrived later and + // therefore live at fresh appended discriminants (52-54), per the + // append-only rule. + /// 10-bit planar GBR, little-endian 16-bit storage. 3 planes ordered + /// G, B, R; each sample uses the low 10 bits of a 16-bit word. + Gbrp10Le = 35, + /// 10-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes + /// ordered G, B, R, A; each sample uses the low 10 bits of a 16-bit + /// word. + Gbrap10Le = 36, + /// 12-bit planar GBR, little-endian 16-bit storage. 3 planes ordered + /// G, B, R; each sample uses the low 12 bits of a 16-bit word. + Gbrp12Le = 37, + /// 12-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes + /// ordered G, B, R, A; each sample uses the low 12 bits of a 16-bit + /// word. + Gbrap12Le = 38, + /// 14-bit planar GBR, little-endian 16-bit storage. 3 planes ordered + /// G, B, R; each sample uses the low 14 bits of a 16-bit word. + Gbrp14Le = 39, + /// 14-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes + /// ordered G, B, R, A; each sample uses the low 14 bits of a 16-bit + /// word. + Gbrap14Le = 40, + + // --- 16-bit YUV planar --- + // + // Full-width companions to the 10/12-bit planar YUV variants above: + // same three-plane layout and little-endian 16-bit words, but ALL 16 + // bits of every word are significant (there are no zero top bits and + // no separate "valid bits" count — full-scale is 65535). Needed by + // wavelet codecs whose signal-range presets go to 16 bits per + // component (SMPTE VC-2 / Dirac video-format presets 7 and 8). + /// 16-bit YUV 4:2:0 planar, little-endian 16-bit storage. All 16 + /// bits of each sample word are significant. + Yuv420P16Le = 41, + /// 16-bit YUV 4:2:2 planar, little-endian 16-bit storage. All 16 + /// bits of each sample word are significant. + Yuv422P16Le = 42, + /// 16-bit YUV 4:4:4 planar, little-endian 16-bit storage. All 16 + /// bits of each sample word are significant. + Yuv444P16Le = 43, + + // --- 8-bit YUV + alpha at the remaining chroma samplings --- + // + // Companions to `Yuva420P`: the alpha plane is always full + // resolution (one 8-bit sample per pixel, never chroma-subsampled), + // appended after the V plane as plane index 3. Intermediate/mezzanine + // codecs carry alpha at 4:2:2 and 4:4:4 samplings. + /// Yuv422P with an additional full-resolution alpha plane. + Yuva422P = 44, + /// Yuv444P with an additional full-resolution alpha plane. + Yuva444P = 45, + + // --- Deep YUV + alpha (10/12/16-bit words with full-resolution A) --- + // + // Alpha-carrying companions to the 10/12/16-bit planar YUV variants + // above, completing the Yuva family for mezzanine codecs that carry + // deep colour together with an alpha channel. Same conventions as + // the 8-bit `Yuva*` trio: 4 planes ordered Y, U, V, A with the + // alpha plane always at full resolution (one sample per pixel, + // never chroma-subsampled) as plane index 3. Every sample — alpha + // included — is stored as a little-endian 16-bit word; for the + // 10/12-bit variants each sample uses the low bits of the word with + // the top bits zero, and for the 16-bit variants all 16 bits of + // every word are significant (full-scale is 65535), matching + // `Yuv420P16Le`/`Yuv422P16Le`/`Yuv444P16Le`. + /// 10-bit YUV 4:2:2 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses + /// the low 10 bits of a 16-bit word. + Yuva422P10Le = 46, + /// 12-bit YUV 4:2:2 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses + /// the low 12 bits of a 16-bit word. + Yuva422P12Le = 47, + /// 10-bit YUV 4:4:4 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses + /// the low 10 bits of a 16-bit word. + Yuva444P10Le = 48, + /// 12-bit YUV 4:4:4 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses + /// the low 12 bits of a 16-bit word. + Yuva444P12Le = 49, + /// 16-bit YUV 4:2:2 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of + /// each sample word are significant. + Yuva422P16Le = 50, + /// 16-bit YUV 4:4:4 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of + /// each sample word are significant. + Yuva444P16Le = 51, + + // --- Native 8-bit and full-width 16-bit planar GBR(A) --- + // + // Companions to the 10/12/14-bit `Gbrp*`/`Gbrap*` family above, + // closing the planar-RGB depth ladder at both ends for lossless + // RGB codecs whose native coding space is per-plane G, B, R. + // Plane order is identical to the rest of the family: G, B, R + // (and A as plane index 3 for `Gbrap16Le`, always at full + // resolution — RGB has no chroma subsampling). `Gbrp8` stores one + // byte per sample with all 8 bits significant; the 16-bit variants + // store little-endian 16-bit words with ALL 16 bits significant + // (full-scale is 65535, matching the `Yuv*P16Le` convention — no + // zero top bits, no separate valid-bits count). Odd in-between + // depths on these storage formats (e.g. 9- or 15-bit RGB) are + // expressed via the per-plane significant-bits side-channel on + // `VideoFrame`, not by new enum variants. + /// 8-bit planar GBR. 3 planes ordered G, B, R; one byte per + /// sample, all 8 bits significant. + Gbrp8 = 52, + /// 16-bit planar GBR, little-endian 16-bit storage. 3 planes + /// ordered G, B, R; all 16 bits of each sample word are + /// significant. + Gbrp16Le = 53, + /// 16-bit planar GBR + alpha, little-endian 16-bit storage. 4 + /// planes ordered G, B, R, A; all 16 bits of each sample word are + /// significant. + Gbrap16Le = 54, + + // --- Deep YUV + alpha at 4:2:0 --- + // + // Completes the deep Yuva family begun by the 4:2:2/4:4:4 variants + // above (46-51) at the remaining chroma sampling. Same conventions: + // 4 planes ordered Y, U, V, A with the alpha plane always at full + // resolution (one sample per pixel, never chroma-subsampled) as + // plane index 3. Every sample — alpha included — is stored as a + // little-endian 16-bit word; the 10/12-bit variants keep values in + // the low bits of the word with the top bits zero, and the 16-bit + // variant has all 16 bits of every word significant (full-scale is + // 65535), matching `Yuv420P16Le`. + /// 10-bit YUV 4:2:0 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses + /// the low 10 bits of a 16-bit word. + Yuva420P10Le = 55, + /// 12-bit YUV 4:2:0 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; each sample uses + /// the low 12 bits of a 16-bit word. + Yuva420P12Le = 56, + /// 16-bit YUV 4:2:0 planar + full-resolution alpha, little-endian + /// 16-bit storage. 4 planes ordered Y, U, V, A; all 16 bits of + /// each sample word are significant. + Yuva420P16Le = 57, + + // --- 8-bit planar GBR + alpha --- + // + // Alpha-carrying companion to `Gbrp8`, filling the last hole in + // the planar GBR(A) family: with this variant every depth on the + // ladder (8/10/12/14/16) exists in both alpha-less and + // alpha-carrying form. Same conventions as the rest of the family: + // planes ordered G, B, R, A with the alpha plane at full + // resolution (RGB has no chroma subsampling) as plane index 3, + // one byte per sample, all 8 bits significant. Lossless RGB codecs + // whose native coding space is per-plane G, B, R carry 8-bit RGBA + // in exactly this shape. + /// 8-bit planar GBR + alpha. 4 planes ordered G, B, R, A; one + /// byte per sample, all 8 bits significant. + Gbrap8 = 58, + + // --- Deep gray + alpha --- + // + // 16-bit companion to `Ya8`, ending the gray+alpha ladder at the + // same depth the plain gray ladder already reaches (`Gray16Le`). + // Still-image wire formats carry 16-bit greyscale-with-alpha + // natively (PNG colour type 4 at bit depth 16); without this + // variant that content must detour through `Rgba64Le`, tripling + // the gray payload and losing the "single luminance component" + // semantics. Same packed shape as `Ya8` — interleaved Y then A — + // with each sample widened to a little-endian 16-bit word, all 16 + // bits significant (full-scale is 65535, the `Gray16Le` + // convention). In-between gray+alpha depths stay the job of the + // per-plane significant-bits side-channel. + /// Packed 16-bit grayscale + alpha, little-endian, 4 bytes/pixel + /// (Y, A). All 16 bits of each sample word are significant. + Ya16Le = 59, + + // --- Print / prepress, inverted-ink convention --- + // + // The companion `Cmyk` (33) reserved this name when it was added: + // Adobe-authored 4-component scans store ink coverage inverted on + // the wire (0 = full ink, 255 = no ink), and decoders that want to + // hand the wire values through losslessly need a format that says + // so rather than silently re-using the regular-convention `Cmyk`. + /// Packed 8-bit inverted CMYK, 4 bytes/pixel in byte order C, M, + /// Y, K. Inverted-ink convention: C=0 means full cyan ink, C=255 + /// means no cyan (white) — the complement of [`Cmyk`](Self::Cmyk). + CmykInverted = 60, + + // --- 4:4:0 planar YUV (full-width, half-height chroma) --- + // + // Vertical-only chroma subsampling: each chroma plane keeps the + // full luma width but carries half the rows — subsampling shifts + // ssx = 0, ssy = 1, the transpose of 4:2:2's half-width, + // full-height geometry. A legal JPEG sampling combination (luma + // H=1, V=2) seen in real-world corpora, and a coded + // chroma-sampling mode of video bitstreams whose sampling flags + // allow horizontal and vertical decimation to be chosen + // independently. The depth ladder mirrors the other planar YUV + // samplings: 8-bit bytes, then 10/12-bit values in the low bits + // of little-endian 16-bit words, then full-width 16-bit words + // with every bit significant (full-scale is 65535). + /// 8-bit YUV 4:4:0, planar (Y, U, V). Chroma at full width, half + /// height (ssx = 0, ssy = 1). + Yuv440P = 61, + /// 10-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each + /// sample uses the low 10 bits of a 16-bit word. + Yuv440P10Le = 62, + /// 12-bit YUV 4:4:0 planar, little-endian 16-bit storage. Each + /// sample uses the low 12 bits of a 16-bit word. + Yuv440P12Le = 63, + /// 16-bit YUV 4:4:0 planar, little-endian 16-bit storage. All 16 + /// bits of each sample word are significant. + Yuv440P16Le = 64, + + // --- Scene-referred 32-bit float (linear-light HDR) --- + // + // IEEE 754 binary32 components stored as little-endian 32-bit + // words, one word per sample. Unlike every integer format above + // there is no integer full-scale: samples are scene-referred + // linear light where 1.0 is the nominal diffuse-white anchor and + // values outside [0, 1] are legal (speculars above white, + // negative out-of-gamut excursions). Needed by HDR image wire + // formats whose native component type is floating point. The + // packed trio mirrors `Gray8`/`Rgb24`/`Rgba` component orders at + // float width; the planar pair extends the planar GBR(A) family + // beyond the integer depth ladder, with the usual G, B, R (+ A) + // plane order and the alpha plane at full resolution as plane + // index 3. + /// Packed 32-bit float grayscale, little-endian, 4 bytes/pixel. + /// Scene-referred linear light. + GrayF32Le = 65, + /// Packed 32-bit float RGB, little-endian, 12 bytes/pixel in + /// component order R, G, B. Scene-referred linear light. + RgbF32Le = 66, + /// Packed 32-bit float RGBA, little-endian, 16 bytes/pixel in + /// component order R, G, B, A. Scene-referred linear light; + /// alpha is straight (non-premultiplied), nominal range [0, 1]. + RgbaF32Le = 67, + /// 32-bit float planar GBR, little-endian. 3 planes ordered G, B, + /// R; one 4-byte word per sample. Scene-referred linear light. + GbrpF32Le = 68, + /// 32-bit float planar GBR + alpha, little-endian. 4 planes + /// ordered G, B, R, A; one 4-byte word per sample; the alpha + /// plane is at full resolution as plane index 3, straight + /// (non-premultiplied), nominal range [0, 1]. + GbrapF32Le = 69, +} + +impl PixelFormat { + /// True if this format stores its components in separate planes. + pub fn is_planar(&self) -> bool { + matches!( + self, + Self::Yuv420P + | Self::Yuv422P + | Self::Yuv444P + | Self::Yuv411P + | Self::Yuv420P10Le + | Self::Yuv422P10Le + | Self::Yuv444P10Le + | Self::Yuv420P12Le + | Self::Yuv422P12Le + | Self::Yuv444P12Le + | Self::Yuv420P16Le + | Self::Yuv422P16Le + | Self::Yuv444P16Le + | Self::Yuv440P + | Self::Yuv440P10Le + | Self::Yuv440P12Le + | Self::Yuv440P16Le + | Self::YuvJ420P + | Self::YuvJ422P + | Self::YuvJ444P + | Self::Nv12 + | Self::Nv21 + | Self::Yuva420P + | Self::Yuva422P + | Self::Yuva444P + | Self::Yuva422P10Le + | Self::Yuva422P12Le + | Self::Yuva444P10Le + | Self::Yuva444P12Le + | Self::Yuva422P16Le + | Self::Yuva444P16Le + | Self::Yuva420P10Le + | Self::Yuva420P12Le + | Self::Yuva420P16Le + | Self::Gbrp8 + | Self::Gbrap8 + | Self::Gbrp10Le + | Self::Gbrap10Le + | Self::Gbrp12Le + | Self::Gbrap12Le + | Self::Gbrp14Le + | Self::Gbrap14Le + | Self::Gbrp16Le + | Self::Gbrap16Le + | Self::GbrpF32Le + | Self::GbrapF32Le + ) + } + + /// True if the format is a palette index format (`Pal8`). + pub fn is_palette(&self) -> bool { + matches!(self, Self::Pal8) + } + + /// True if this format carries an alpha channel. + pub fn has_alpha(&self) -> bool { + matches!( + self, + Self::Rgba + | Self::Bgra + | Self::Argb + | Self::Abgr + | Self::Rgba64Le + | Self::Ya8 + | Self::Ya16Le + | Self::Yuva420P + | Self::Yuva422P + | Self::Yuva444P + | Self::Yuva422P10Le + | Self::Yuva422P12Le + | Self::Yuva444P10Le + | Self::Yuva444P12Le + | Self::Yuva422P16Le + | Self::Yuva444P16Le + | Self::Yuva420P10Le + | Self::Yuva420P12Le + | Self::Yuva420P16Le + | Self::Gbrap8 + | Self::Gbrap10Le + | Self::Gbrap12Le + | Self::Gbrap14Le + | Self::Gbrap16Le + | Self::RgbaF32Le + | Self::GbrapF32Le + ) + } + + /// True for the 32-bit IEEE-float variants, packed or planar. + /// Float formats are scene-referred: samples carry linear light + /// with no integer full-scale — 1.0 is the nominal diffuse-white + /// anchor and values outside [0, 1] are legal. + pub fn is_float(&self) -> bool { + matches!( + self, + Self::GrayF32Le | Self::RgbF32Le | Self::RgbaF32Le | Self::GbrpF32Le | Self::GbrapF32Le + ) + } + + /// Number of planes in the stored layout. Packed and palette formats + /// return 1; NV12/NV21 return 2; planar YUV without alpha and the + /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4. + pub fn plane_count(&self) -> usize { + match self { + Self::Nv12 | Self::Nv21 => 2, + Self::Yuv420P + | Self::Yuv422P + | Self::Yuv444P + | Self::Yuv411P + | Self::Yuv420P10Le + | Self::Yuv422P10Le + | Self::Yuv444P10Le + | Self::Yuv420P12Le + | Self::Yuv422P12Le + | Self::Yuv444P12Le + | Self::Yuv420P16Le + | Self::Yuv422P16Le + | Self::Yuv444P16Le + | Self::Yuv440P + | Self::Yuv440P10Le + | Self::Yuv440P12Le + | Self::Yuv440P16Le + | Self::YuvJ420P + | Self::YuvJ422P + | Self::YuvJ444P + | Self::Gbrp8 + | Self::Gbrp10Le + | Self::Gbrp12Le + | Self::Gbrp14Le + | Self::Gbrp16Le + | Self::GbrpF32Le => 3, + Self::Yuva420P + | Self::Yuva422P + | Self::Yuva444P + | Self::Yuva422P10Le + | Self::Yuva422P12Le + | Self::Yuva444P10Le + | Self::Yuva444P12Le + | Self::Yuva422P16Le + | Self::Yuva444P16Le + | Self::Yuva420P10Le + | Self::Yuva420P12Le + | Self::Yuva420P16Le + | Self::Gbrap8 + | Self::Gbrap10Le + | Self::Gbrap12Le + | Self::Gbrap14Le + | Self::Gbrap16Le + | Self::GbrapF32Le => 4, + _ => 1, + } + } + + /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact + /// for chroma-subsampled YUV — intended for worst-case preallocation + /// rather than wire-accurate accounting. + pub fn bits_per_pixel_approx(&self) -> u32 { + match self { + Self::MonoBlack | Self::MonoWhite => 1, + Self::Gray8 | Self::Pal8 => 8, + Self::Ya8 => 16, + // 16-bit gray + alpha: two LE 16-bit words per pixel, all + // bits significant — packed bits equal storage bits. + Self::Ya16Le => 32, + Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16, + Self::Rgb24 | Self::Bgr24 => 24, + Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32, + Self::Rgb48Le => 48, + Self::Rgba64Le => 64, + Self::Yuyv422 | Self::Uyvy422 => 16, + Self::Cmyk | Self::CmykInverted => 32, + // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24 + // 10/12-bit variants double the byte count but we report the + // packed-bits-per-pixel estimate for a uniform heuristic. + Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12, + // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at + // full res + 2 chroma planes each subsampled by 4). + Self::Yuv411P => 12, + Self::Yuv422P | Self::YuvJ422P => 16, + // 4:4:0 packs the same 2 samples/pixel as 4:2:2 (Y at full + // res + 2 chroma planes at half height, full width). + Self::Yuv440P => 16, + Self::Yuv444P | Self::YuvJ444P => 24, + Self::Yuv420P10Le | Self::Yuv420P12Le | Self::Yuv420P16Le => 24, + Self::Yuv422P10Le | Self::Yuv422P12Le | Self::Yuv422P16Le => 32, + // Deep 4:4:0 matches deep 4:2:2 — 2 sample words per pixel. + Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => 32, + Self::Yuv444P10Le | Self::Yuv444P12Le | Self::Yuv444P16Le => 48, + Self::Yuva420P => 20, + // 4:2:2 + full-res alpha: 8 (Y) + 4 (U) + 4 (V) + 8 (A). + Self::Yuva422P => 24, + // 4:4:4 + full-res alpha: four full-resolution 8-bit planes. + Self::Yuva444P => 32, + // Deep 4:2:2 + full-res alpha in 16-bit words: the estimator + // reports the 16-bit-word cost like the alpha-less deep YUV + // arms above — 3 sample words per pixel (Y + U/2 + V/2 + A). + Self::Yuva422P10Le | Self::Yuva422P12Le | Self::Yuva422P16Le => 48, + // Deep 4:4:4 + full-res alpha: 4 sample words per pixel. + Self::Yuva444P10Le | Self::Yuva444P12Le | Self::Yuva444P16Le => 64, + // Deep 4:2:0 + full-res alpha in 16-bit words: 16-bit-word + // storage cost of the alpha-less 4:2:0 arms above (24) plus + // one full-resolution 16-bit alpha word per pixel. + Self::Yuva420P10Le | Self::Yuva420P12Le | Self::Yuva420P16Le => 40, + // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we + // report the packed bits-per-pixel density (samples × bits) + // rather than the 16-bit storage cost, matching how the + // 10/12-bit YUV variants are reported above. + Self::Gbrp10Le => 30, + Self::Gbrap10Le => 40, + Self::Gbrp12Le => 36, + Self::Gbrap12Le => 48, + Self::Gbrp14Le => 42, + Self::Gbrap14Le => 56, + // Native 8-bit GBR: three bytes per pixel, like Rgb24 but + // planar. 16-bit GBR(A): packed bits == storage bits (every + // bit of each 16-bit word is significant), so the density + // and storage numbers coincide. + Self::Gbrp8 => 24, + Self::Gbrp16Le => 48, + Self::Gbrap16Le => 64, + // 8-bit GBR + alpha: four bytes per pixel, like Rgba but + // planar. + Self::Gbrap8 => 32, + // 32-bit float family: every sample is a full binary32 + // word, so packed bits equal storage bits (32 per sample; + // no chroma subsampling anywhere in the family). + Self::GrayF32Le => 32, + Self::RgbF32Le | Self::GbrpF32Le => 96, + Self::RgbaF32Le | Self::GbrapF32Le => 128, + } + } + + /// Log2 chroma-subsampling shifts `(ssx, ssy)` relative to the + /// luma grid, for formats that carry chroma on a subsampled (or + /// potentially subsampled) grid. The chroma sample grid is the + /// luma grid right-shifted by `ssx` horizontally and `ssy` + /// vertically, with ceiling division for odd luma sizes (see + /// [`plane_dimensions`](Self::plane_dimensions)). + /// + /// | sampling | `(ssx, ssy)` | chroma geometry | + /// |----------|--------------|-----------------| + /// | 4:2:0 | `(1, 1)` | half width, half height | + /// | 4:2:2 | `(1, 0)` | half width, full height | + /// | 4:4:4 | `(0, 0)` | full resolution | + /// | 4:1:1 | `(2, 0)` | quarter width, full height | + /// | 4:4:0 | `(0, 1)` | full width, half height | + /// + /// Returns `None` for formats without a distinct chroma grid + /// (grayscale, RGB/GBR in any layout, palette, mono, CMYK). + /// Packed 4:2:2 (`Yuyv422`/`Uyvy422`) and semi-planar 4:2:0 + /// (`Nv12`/`Nv21`) report their sampling even though the chroma + /// samples don't live in standalone planes. + /// + /// ``` + /// use oxideav_core::PixelFormat; + /// // 4:4:0: full-width, half-height chroma. + /// assert_eq!(PixelFormat::Yuv440P.chroma_subsampling(), Some((0, 1))); + /// // 4:2:0: subsampled on both axes. + /// assert_eq!(PixelFormat::Yuv420P.chroma_subsampling(), Some((1, 1))); + /// // RGB has no chroma grid. + /// assert_eq!(PixelFormat::Rgba.chroma_subsampling(), None); + /// ``` + pub fn chroma_subsampling(&self) -> Option<(u32, u32)> { + match self { + // 4:2:0 — half width, half height. + Self::Yuv420P + | Self::YuvJ420P + | Self::Yuv420P10Le + | Self::Yuv420P12Le + | Self::Yuv420P16Le + | Self::Nv12 + | Self::Nv21 + | Self::Yuva420P + | Self::Yuva420P10Le + | Self::Yuva420P12Le + | Self::Yuva420P16Le => Some((1, 1)), + // 4:2:2 — half width, full height (packed 4:2:2 included). + Self::Yuv422P + | Self::YuvJ422P + | Self::Yuv422P10Le + | Self::Yuv422P12Le + | Self::Yuv422P16Le + | Self::Yuva422P + | Self::Yuva422P10Le + | Self::Yuva422P12Le + | Self::Yuva422P16Le + | Self::Yuyv422 + | Self::Uyvy422 => Some((1, 0)), + // 4:4:4 — chroma at full resolution. + Self::Yuv444P + | Self::YuvJ444P + | Self::Yuv444P10Le + | Self::Yuv444P12Le + | Self::Yuv444P16Le + | Self::Yuva444P + | Self::Yuva444P10Le + | Self::Yuva444P12Le + | Self::Yuva444P16Le => Some((0, 0)), + // 4:1:1 — quarter width, full height. + Self::Yuv411P => Some((2, 0)), + // 4:4:0 — full width, half height. + Self::Yuv440P | Self::Yuv440P10Le | Self::Yuv440P12Le | Self::Yuv440P16Le => { + Some((0, 1)) + } + // Everything else has no distinct chroma grid. + _ => None, + } + } + + /// Sample-grid dimensions of plane `plane` for a `width` × + /// `height` picture, with ceiling division on subsampled axes so + /// odd luma sizes still cover every pixel. + /// + /// Conventions: + /// - Plane 0 (luma / the packed plane) is always `(width, height)`. + /// - Chroma planes (indices 1 and 2 of planar YUV, index 1 of the + /// semi-planar formats) are the luma grid right-shifted by the + /// [`chroma_subsampling`](Self::chroma_subsampling) factors. + /// Semi-planar chroma dimensions are in chroma *positions* — + /// each position stores two interleaved samples, which + /// [`plane_row_bytes`](Self::plane_row_bytes) accounts for. + /// - Alpha planes (index 3) and all planar-RGB planes are at full + /// resolution. + /// - Packed, palette, and bit-packed mono formats report pixel + /// dimensions for their single plane; per-row byte cost comes + /// from [`plane_row_bytes`](Self::plane_row_bytes). + /// + /// Returns `None` when `plane >= plane_count()`. + /// + /// ``` + /// use oxideav_core::PixelFormat; + /// // 4:4:0 chroma: full width, half height (odd height rounds up). + /// assert_eq!( + /// PixelFormat::Yuv440P.plane_dimensions(1, 640, 481), + /// Some((640, 241)) + /// ); + /// // Alpha plane of a deep YUVA format stays at full resolution. + /// assert_eq!( + /// PixelFormat::Yuva420P10Le.plane_dimensions(3, 7, 5), + /// Some((7, 5)) + /// ); + /// assert_eq!(PixelFormat::Rgb24.plane_dimensions(1, 8, 8), None); + /// ``` + pub fn plane_dimensions(&self, plane: usize, width: u32, height: u32) -> Option<(u32, u32)> { + if plane >= self.plane_count() { + return None; + } + match (self.chroma_subsampling(), plane) { + (Some((ssx, ssy)), 1 | 2) => { + Some((width.div_ceil(1 << ssx), height.div_ceil(1 << ssy))) + } + _ => Some((width, height)), + } + } + + /// Tightly-packed byte count of one row of plane `plane` for a + /// picture `width` pixels wide — no stride padding or alignment. + /// Real codecs frequently over-allocate rows for alignment; this + /// is the minimum a row occupies. + /// + /// Returns `None` when `plane >= plane_count()` or the byte count + /// overflows `usize`. + pub fn plane_row_bytes(&self, plane: usize, width: u32) -> Option { + let (pw, _) = self.plane_dimensions(plane, width, 1)?; + let pw = pw as usize; + let bytes_per_position: usize = match self { + // Bit-packed mono: 8 pixels per byte, ragged tail byte. + Self::MonoBlack | Self::MonoWhite => return Some(pw.div_ceil(8)), + // Packed 4:2:2 macropixels: 4 bytes per 2 pixels; an odd + // trailing pixel still occupies a full macropixel. + Self::Yuyv422 | Self::Uyvy422 => return pw.div_ceil(2).checked_mul(4), + // One byte per sample position. + Self::Gray8 + | Self::Pal8 + | Self::Yuv420P + | Self::Yuv422P + | Self::Yuv444P + | Self::Yuv411P + | Self::Yuv440P + | Self::YuvJ420P + | Self::YuvJ422P + | Self::YuvJ444P + | Self::Yuva420P + | Self::Yuva422P + | Self::Yuva444P + | Self::Gbrp8 + | Self::Gbrap8 => 1, + // Semi-planar: one byte per luma sample on plane 0, an + // interleaved two-sample pair per chroma position on + // plane 1. + Self::Nv12 | Self::Nv21 => { + if plane == 0 { + 1 + } else { + 2 + } + } + // Little-endian 16-bit words (10/12/14/16-bit storage). + Self::Gray10Le + | Self::Gray12Le + | Self::Gray16Le + | Self::Yuv420P10Le + | Self::Yuv422P10Le + | Self::Yuv444P10Le + | Self::Yuv420P12Le + | Self::Yuv422P12Le + | Self::Yuv444P12Le + | Self::Yuv420P16Le + | Self::Yuv422P16Le + | Self::Yuv444P16Le + | Self::Yuv440P10Le + | Self::Yuv440P12Le + | Self::Yuv440P16Le + | Self::Yuva422P10Le + | Self::Yuva422P12Le + | Self::Yuva444P10Le + | Self::Yuva444P12Le + | Self::Yuva422P16Le + | Self::Yuva444P16Le + | Self::Yuva420P10Le + | Self::Yuva420P12Le + | Self::Yuva420P16Le + | Self::Gbrp10Le + | Self::Gbrap10Le + | Self::Gbrp12Le + | Self::Gbrap12Le + | Self::Gbrp14Le + | Self::Gbrap14Le + | Self::Gbrp16Le + | Self::Gbrap16Le => 2, + // Packed multi-component: whole-pixel byte cost. + Self::Ya8 => 2, + Self::Rgb24 | Self::Bgr24 => 3, + Self::Rgba + | Self::Bgra + | Self::Argb + | Self::Abgr + | Self::Cmyk + | Self::CmykInverted + | Self::Ya16Le => 4, + Self::Rgb48Le => 6, + Self::Rgba64Le => 8, + // 32-bit float: one binary32 word per sample (packed + // grayscale and the planar GBR(A) planes), or the + // whole-pixel cost for packed multi-component float. + Self::GrayF32Le | Self::GbrpF32Le | Self::GbrapF32Le => 4, + Self::RgbF32Le => 12, + Self::RgbaF32Le => 16, + }; + pw.checked_mul(bytes_per_position) + } + + /// Tightly-packed byte size of plane `plane` for a `width` × + /// `height` picture: + /// [`plane_row_bytes`](Self::plane_row_bytes) × the plane's row + /// count from [`plane_dimensions`](Self::plane_dimensions). + /// + /// Returns `None` when `plane >= plane_count()` or the size + /// overflows `usize`. + pub fn plane_size_bytes(&self, plane: usize, width: u32, height: u32) -> Option { + let (_, ph) = self.plane_dimensions(plane, width, height)?; + self.plane_row_bytes(plane, width)?.checked_mul(ph as usize) + } + + /// Tightly-packed byte size of a whole `width` × `height` frame in + /// this format — the sum of + /// [`plane_size_bytes`](Self::plane_size_bytes) over every plane, + /// with no stride padding or inter-plane alignment. Out-of-band + /// side data (the `Pal8` palette table, significant-bits records) + /// is not included. + /// + /// Returns `None` on `usize` overflow. + /// + /// ``` + /// use oxideav_core::PixelFormat; + /// // 4:2:0 at 4×4: 16 luma + 4 + 4 chroma bytes. + /// assert_eq!(PixelFormat::Yuv420P.frame_size_bytes(4, 4), Some(24)); + /// // 4:4:0 at 6×5: 30 luma + 2 × (6 × 3) chroma bytes. + /// assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66)); + /// // Packed float RGBA: 16 bytes per pixel. + /// assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144)); + /// ``` + pub fn frame_size_bytes(&self, width: u32, height: u32) -> Option { + let mut total = 0usize; + for plane in 0..self.plane_count() { + total = total.checked_add(self.plane_size_bytes(plane, width, height)?)?; + } + Some(total) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the + /// stability commitment — the integer value of each variant is part of + /// the public ABI. Any reorder, renumber, or removal will fail this test + /// and the change MUST be a major version bump (or a fresh variant + /// appended at a new number, leaving the existing ones untouched). + #[test] + fn pixel_format_discriminants_pinned() { + assert_eq!(PixelFormat::Yuv420P as u16, 0); + assert_eq!(PixelFormat::Yuv422P as u16, 1); + assert_eq!(PixelFormat::Yuv444P as u16, 2); + assert_eq!(PixelFormat::Rgb24 as u16, 3); + assert_eq!(PixelFormat::Rgba as u16, 4); + assert_eq!(PixelFormat::Gray8 as u16, 5); + assert_eq!(PixelFormat::Pal8 as u16, 6); + assert_eq!(PixelFormat::Bgr24 as u16, 7); + assert_eq!(PixelFormat::Bgra as u16, 8); + assert_eq!(PixelFormat::Argb as u16, 9); + assert_eq!(PixelFormat::Abgr as u16, 10); + assert_eq!(PixelFormat::Rgb48Le as u16, 11); + assert_eq!(PixelFormat::Rgba64Le as u16, 12); + assert_eq!(PixelFormat::Gray16Le as u16, 13); + assert_eq!(PixelFormat::Gray10Le as u16, 14); + assert_eq!(PixelFormat::Gray12Le as u16, 15); + assert_eq!(PixelFormat::Yuv420P10Le as u16, 16); + assert_eq!(PixelFormat::Yuv422P10Le as u16, 17); + assert_eq!(PixelFormat::Yuv444P10Le as u16, 18); + assert_eq!(PixelFormat::Yuv420P12Le as u16, 19); + assert_eq!(PixelFormat::Yuv422P12Le as u16, 20); + assert_eq!(PixelFormat::Yuv444P12Le as u16, 21); + assert_eq!(PixelFormat::YuvJ420P as u16, 22); + assert_eq!(PixelFormat::YuvJ422P as u16, 23); + assert_eq!(PixelFormat::YuvJ444P as u16, 24); + assert_eq!(PixelFormat::Nv12 as u16, 25); + assert_eq!(PixelFormat::Nv21 as u16, 26); + assert_eq!(PixelFormat::Ya8 as u16, 27); + assert_eq!(PixelFormat::Yuva420P as u16, 28); + assert_eq!(PixelFormat::MonoBlack as u16, 29); + assert_eq!(PixelFormat::MonoWhite as u16, 30); + assert_eq!(PixelFormat::Yuyv422 as u16, 31); + assert_eq!(PixelFormat::Uyvy422 as u16, 32); + assert_eq!(PixelFormat::Cmyk as u16, 33); + assert_eq!(PixelFormat::Yuv411P as u16, 34); + assert_eq!(PixelFormat::Gbrp10Le as u16, 35); + assert_eq!(PixelFormat::Gbrap10Le as u16, 36); + assert_eq!(PixelFormat::Gbrp12Le as u16, 37); + assert_eq!(PixelFormat::Gbrap12Le as u16, 38); + assert_eq!(PixelFormat::Gbrp14Le as u16, 39); + assert_eq!(PixelFormat::Gbrap14Le as u16, 40); + assert_eq!(PixelFormat::Yuv420P16Le as u16, 41); + assert_eq!(PixelFormat::Yuv422P16Le as u16, 42); + assert_eq!(PixelFormat::Yuv444P16Le as u16, 43); + assert_eq!(PixelFormat::Yuva422P as u16, 44); + assert_eq!(PixelFormat::Yuva444P as u16, 45); + assert_eq!(PixelFormat::Yuva422P10Le as u16, 46); + assert_eq!(PixelFormat::Yuva422P12Le as u16, 47); + assert_eq!(PixelFormat::Yuva444P10Le as u16, 48); + assert_eq!(PixelFormat::Yuva444P12Le as u16, 49); + assert_eq!(PixelFormat::Yuva422P16Le as u16, 50); + assert_eq!(PixelFormat::Yuva444P16Le as u16, 51); + assert_eq!(PixelFormat::Gbrp8 as u16, 52); + assert_eq!(PixelFormat::Gbrp16Le as u16, 53); + assert_eq!(PixelFormat::Gbrap16Le as u16, 54); + assert_eq!(PixelFormat::Yuva420P10Le as u16, 55); + assert_eq!(PixelFormat::Yuva420P12Le as u16, 56); + assert_eq!(PixelFormat::Yuva420P16Le as u16, 57); + assert_eq!(PixelFormat::Gbrap8 as u16, 58); + assert_eq!(PixelFormat::Ya16Le as u16, 59); + assert_eq!(PixelFormat::CmykInverted as u16, 60); + assert_eq!(PixelFormat::Yuv440P as u16, 61); + assert_eq!(PixelFormat::Yuv440P10Le as u16, 62); + assert_eq!(PixelFormat::Yuv440P12Le as u16, 63); + assert_eq!(PixelFormat::Yuv440P16Le as u16, 64); + assert_eq!(PixelFormat::GrayF32Le as u16, 65); + assert_eq!(PixelFormat::RgbF32Le as u16, 66); + assert_eq!(PixelFormat::RgbaF32Le as u16, 67); + assert_eq!(PixelFormat::GbrpF32Le as u16, 68); + assert_eq!(PixelFormat::GbrapF32Le as u16, 69); + } + + #[test] + fn sample_format_discriminants_pinned() { + assert_eq!(SampleFormat::U8 as u8, 0); + assert_eq!(SampleFormat::S8 as u8, 1); + assert_eq!(SampleFormat::S16 as u8, 2); + assert_eq!(SampleFormat::S24 as u8, 3); + assert_eq!(SampleFormat::S32 as u8, 4); + assert_eq!(SampleFormat::F32 as u8, 5); + assert_eq!(SampleFormat::F64 as u8, 6); + assert_eq!(SampleFormat::U8P as u8, 7); + assert_eq!(SampleFormat::S16P as u8, 8); + assert_eq!(SampleFormat::S32P as u8, 9); + assert_eq!(SampleFormat::F32P as u8, 10); + assert_eq!(SampleFormat::F64P as u8, 11); + } + + #[test] + fn high_bit_yuv_planar_metadata() { + // 10-bit reference variants are planar with three planes. + assert!(PixelFormat::Yuv420P10Le.is_planar()); + assert!(PixelFormat::Yuv422P10Le.is_planar()); + assert!(PixelFormat::Yuv444P10Le.is_planar()); + + // 12-bit variants must follow the same shape. + assert!(PixelFormat::Yuv420P12Le.is_planar()); + assert!(PixelFormat::Yuv422P12Le.is_planar()); + assert!(PixelFormat::Yuv444P12Le.is_planar()); + + assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3); + assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3); + assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3); + + // 16-bit variants must follow the same shape. + for fmt in [ + PixelFormat::Yuv420P16Le, + PixelFormat::Yuv422P16Le, + PixelFormat::Yuv444P16Le, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes"); + } + + // None of the high-bit YUV variants carry alpha or palette. + assert!(!PixelFormat::Yuv422P12Le.has_alpha()); + assert!(!PixelFormat::Yuv444P12Le.has_alpha()); + assert!(!PixelFormat::Yuv422P12Le.is_palette()); + assert!(!PixelFormat::Yuv444P12Le.is_palette()); + assert!(!PixelFormat::Yuv420P16Le.has_alpha()); + assert!(!PixelFormat::Yuv422P16Le.has_alpha()); + assert!(!PixelFormat::Yuv444P16Le.has_alpha()); + assert!(!PixelFormat::Yuv420P16Le.is_palette()); + assert!(!PixelFormat::Yuv422P16Le.is_palette()); + assert!(!PixelFormat::Yuv444P16Le.is_palette()); + } + + #[test] + fn channel_layout_round_trip_count_for_known_layouts() { + // For every `n` that `from_count` maps to a named layout, the + // resulting layout's `channel_count()` must equal `n` again. + for n in 1..=8u16 { + let layout = ChannelLayout::from_count(n); + assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}"); + // None of these defaults should fall through to DiscreteN. + assert!( + !matches!(layout, ChannelLayout::DiscreteN(_)), + "from_count({n}) unexpectedly produced DiscreteN" + ); + } + } + + #[test] + fn channel_layout_from_count_default_table() { + // The exact mapping documented on `from_count` — pin it so + // future refactors don't silently change the inferred layout. + assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono); + assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo); + assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30); + assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad); + assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50); + assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51); + assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61); + assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71); + } + + #[test] + fn channel_layout_unknown_count_falls_through_to_discrete() { + assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0)); + assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13)); + assert_eq!( + ChannelLayout::from_count(64).channel_count(), + 64, + "DiscreteN must report the count it was constructed with" + ); + } + + #[test] + fn channel_layout_position_lookup() { + assert_eq!( + ChannelLayout::Stereo.position(0), + Some(ChannelPosition::FrontLeft) + ); + assert_eq!( + ChannelLayout::Stereo.position(1), + Some(ChannelPosition::FrontRight) + ); + assert_eq!(ChannelLayout::Stereo.position(2), None); + + // 5.1 canonical: L, R, C, LFE, Ls, Rs. + let s51 = ChannelLayout::Surround51; + assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft)); + assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight)); + assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter)); + assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency)); + assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft)); + assert_eq!(s51.position(5), Some(ChannelPosition::SideRight)); + assert_eq!(s51.position(6), None); + + // DiscreteN never reveals a position. + assert_eq!(ChannelLayout::DiscreteN(13).position(0), None); + } + + #[test] + fn channel_layout_lfe_and_surround_predicates() { + assert!(ChannelLayout::Surround51.has_lfe()); + assert!(ChannelLayout::Surround71.has_lfe()); + assert!(ChannelLayout::Stereo21.has_lfe()); + assert!(!ChannelLayout::Quad.has_lfe()); + assert!(!ChannelLayout::Surround50.has_lfe()); + assert!(!ChannelLayout::Stereo.has_lfe()); + + assert!(!ChannelLayout::Mono.is_surround()); + assert!(!ChannelLayout::Stereo.is_surround()); + // Downmix carriers are still 2ch / no-LFE → not "surround" by + // the layout-shape definition; the surround info lives in the + // sample matrix itself. + assert!(!ChannelLayout::LoRo.is_surround()); + assert!(!ChannelLayout::LtRt.is_surround()); + assert!(ChannelLayout::Stereo21.is_surround()); + assert!(ChannelLayout::Surround51.is_surround()); + assert!(ChannelLayout::Surround71.is_surround()); + } + + #[test] + fn channel_layout_display_and_fromstr_round_trip() { + use std::str::FromStr; + let cases = [ + ChannelLayout::Mono, + ChannelLayout::Stereo, + ChannelLayout::Stereo21, + ChannelLayout::Surround30, + ChannelLayout::Quad, + ChannelLayout::Surround40, + ChannelLayout::Surround41, + ChannelLayout::Surround50, + ChannelLayout::Surround51, + ChannelLayout::Surround60, + ChannelLayout::Surround61, + ChannelLayout::Surround70, + ChannelLayout::Surround71, + ChannelLayout::LoRo, + ChannelLayout::LtRt, + ChannelLayout::DiscreteN(13), + ]; + for layout in cases { + let s = layout.to_string(); + let parsed = ChannelLayout::from_str(&s).expect("display output must parse back"); + assert_eq!(parsed, layout, "round-trip failed via {s:?}"); + } + } + + #[test] + fn channel_layout_fromstr_accepts_aliases_and_case() { + use std::str::FromStr; + assert_eq!( + ChannelLayout::from_str("STEREO").unwrap(), + ChannelLayout::Stereo + ); + assert_eq!( + ChannelLayout::from_str("2.0").unwrap(), + ChannelLayout::Stereo + ); + assert_eq!( + ChannelLayout::from_str("5.1").unwrap(), + ChannelLayout::Surround51 + ); + assert_eq!( + ChannelLayout::from_str("Lo/Ro").unwrap(), + ChannelLayout::LoRo + ); + assert_eq!( + ChannelLayout::from_str("lt/rt").unwrap(), + ChannelLayout::LtRt + ); + assert!(ChannelLayout::from_str("absurd_layout").is_err()); + } + + #[test] + fn channel_layout_positions_owned_matches_static_slice() { + for layout in [ + ChannelLayout::Mono, + ChannelLayout::Surround51, + ChannelLayout::Surround71, + ] { + assert_eq!(layout.positions_owned(), layout.positions()); + } + // DiscreteN returns an empty owned vec — positions are unknown. + assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty()); + } + + #[test] + fn sample_format_plane_count_interleaved_is_one() { + // Interleaved formats always pack into a single plane, regardless + // of channel count. + for ch in [1u16, 2, 6, 8, 64, 0] { + assert_eq!(SampleFormat::S16.plane_count(ch), 1); + assert_eq!(SampleFormat::F32.plane_count(ch), 1); + assert_eq!(SampleFormat::U8.plane_count(ch), 1); + assert_eq!(SampleFormat::S24.plane_count(ch), 1); + } + } + + #[test] + fn sample_format_plane_count_planar_matches_channels() { + // Planar formats use one plane per channel. + assert_eq!(SampleFormat::S16P.plane_count(1), 1); + assert_eq!(SampleFormat::S16P.plane_count(2), 2); + assert_eq!(SampleFormat::F32P.plane_count(6), 6); + assert_eq!(SampleFormat::F64P.plane_count(8), 8); + + // Edge case: zero channels in a planar format yields zero planes. + assert_eq!(SampleFormat::S32P.plane_count(0), 0); + } + + #[test] + fn high_bit_yuv_bits_per_pixel_approx() { + // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the + // packed-bits estimator (the approximation reports samples-per-pixel + // density, not the 16-bit storage width). + assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32); + assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32); + assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48); + assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48); + assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24); + + // 16-bit: packed bits == storage bits (every bit of the 16-bit + // word is significant), so the estimator lands on the same + // numbers as the 10/12-bit siblings. + assert_eq!(PixelFormat::Yuv420P16Le.bits_per_pixel_approx(), 24); + assert_eq!(PixelFormat::Yuv422P16Le.bits_per_pixel_approx(), 32); + assert_eq!(PixelFormat::Yuv444P16Le.bits_per_pixel_approx(), 48); + } + + #[test] + fn yuva_planar_metadata() { + // All three alpha-carrying planar YUV samplings share one shape: + // planar, 4 planes (Y, U, V, full-resolution A), alpha set, not + // a palette format. + for fmt in [ + PixelFormat::Yuva420P, + PixelFormat::Yuva422P, + PixelFormat::Yuva444P, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes"); + assert!(fmt.has_alpha(), "{fmt:?} must carry alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + } + + // Packed-bits estimator: the alpha plane adds a full 8 bits per + // pixel on top of the alpha-less sampling's density. + assert_eq!( + PixelFormat::Yuva420P.bits_per_pixel_approx(), + PixelFormat::Yuv420P.bits_per_pixel_approx() + 8 + ); + assert_eq!( + PixelFormat::Yuva422P.bits_per_pixel_approx(), + PixelFormat::Yuv422P.bits_per_pixel_approx() + 8 + ); + assert_eq!( + PixelFormat::Yuva444P.bits_per_pixel_approx(), + PixelFormat::Yuv444P.bits_per_pixel_approx() + 8 + ); + assert_eq!(PixelFormat::Yuva422P.bits_per_pixel_approx(), 24); + assert_eq!(PixelFormat::Yuva444P.bits_per_pixel_approx(), 32); + } + + #[test] + fn deep_yuva_planar_metadata() { + // All six deep alpha-carrying variants share one shape: planar, + // 4 planes (Y, U, V, full-resolution A), alpha set, no palette. + for fmt in [ + PixelFormat::Yuva422P10Le, + PixelFormat::Yuva422P12Le, + PixelFormat::Yuva444P10Le, + PixelFormat::Yuva444P12Le, + PixelFormat::Yuva422P16Le, + PixelFormat::Yuva444P16Le, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes"); + assert!(fmt.has_alpha(), "{fmt:?} must carry alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + } + } + + #[test] + fn deep_yuva_bits_per_pixel_approx() { + // Estimator reports 16-bit-word storage cost, matching the + // alpha-less deep YUV trio: the full-resolution alpha word adds + // 16 on top of the alpha-less sampling's number. + for fmt in [ + PixelFormat::Yuva422P10Le, + PixelFormat::Yuva422P12Le, + PixelFormat::Yuva422P16Le, + ] { + assert_eq!(fmt.bits_per_pixel_approx(), 48, "{fmt:?}"); + } + for fmt in [ + PixelFormat::Yuva444P10Le, + PixelFormat::Yuva444P12Le, + PixelFormat::Yuva444P16Le, + ] { + assert_eq!(fmt.bits_per_pixel_approx(), 64, "{fmt:?}"); + } + assert_eq!( + PixelFormat::Yuva422P16Le.bits_per_pixel_approx(), + PixelFormat::Yuv422P16Le.bits_per_pixel_approx() + 16 + ); + assert_eq!( + PixelFormat::Yuva444P16Le.bits_per_pixel_approx(), + PixelFormat::Yuv444P16Le.bits_per_pixel_approx() + 16 + ); + assert_eq!( + PixelFormat::Yuva422P10Le.bits_per_pixel_approx(), + PixelFormat::Yuv422P10Le.bits_per_pixel_approx() + 16 + ); + assert_eq!( + PixelFormat::Yuva444P12Le.bits_per_pixel_approx(), + PixelFormat::Yuv444P12Le.bits_per_pixel_approx() + 16 + ); + } + + #[test] + fn high_bit_gbr_planar_metadata() { + // All six new variants are planar with the right plane count. + for fmt in [ + PixelFormat::Gbrp10Le, + PixelFormat::Gbrp12Le, + PixelFormat::Gbrp14Le, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes"); + assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + } + for fmt in [ + PixelFormat::Gbrap10Le, + PixelFormat::Gbrap12Le, + PixelFormat::Gbrap14Le, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes"); + assert!(fmt.has_alpha(), "{fmt:?} must carry alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + } + } + + #[test] + fn high_bit_gbr_bits_per_pixel_approx() { + // Packed bits-per-pixel = samples × bits (consistent with how + // the 10/12-bit YUV variants are reported above). + assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30); + assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40); + assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36); + assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48); + assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42); + assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56); + } + + #[test] + fn high_bit_gbr_constructible_and_distinct() { + // Round-trip the discriminant through `as u16` and back via the + // pinning test's reverse mapping — every variant must be unique. + let all = [ + PixelFormat::Gbrp10Le, + PixelFormat::Gbrap10Le, + PixelFormat::Gbrp12Le, + PixelFormat::Gbrap12Le, + PixelFormat::Gbrp14Le, + PixelFormat::Gbrap14Le, + PixelFormat::Gbrp8, + PixelFormat::Gbrap8, + PixelFormat::Gbrp16Le, + PixelFormat::Gbrap16Le, + ]; + let mut seen = std::collections::HashSet::new(); + for fmt in all { + assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}"); + } + } + + #[test] + fn gbr_depth_ladder_ends_metadata() { + // Gbrp8 and the 16-bit pair share the family shape: planar, + // G/B/R plane order (3 planes), alpha only on Gbrap16Le, never + // palette. + for fmt in [PixelFormat::Gbrp8, PixelFormat::Gbrp16Le] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes"); + assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + } + assert!(PixelFormat::Gbrap16Le.is_planar()); + assert_eq!(PixelFormat::Gbrap16Le.plane_count(), 4); + assert!(PixelFormat::Gbrap16Le.has_alpha()); + assert!(!PixelFormat::Gbrap16Le.is_palette()); + } + + #[test] + fn gbr_depth_ladder_ends_bits_per_pixel_approx() { + // Gbrp8 matches the packed 8-bit RGB density (planar layout + // doesn't change bits-per-pixel), and the 16-bit pair matches + // the packed 16-bit RGB(A) densities — for 16-bit words packed + // bits equal storage bits. + assert_eq!( + PixelFormat::Gbrp8.bits_per_pixel_approx(), + PixelFormat::Rgb24.bits_per_pixel_approx() + ); + assert_eq!( + PixelFormat::Gbrp16Le.bits_per_pixel_approx(), + PixelFormat::Rgb48Le.bits_per_pixel_approx() + ); + assert_eq!( + PixelFormat::Gbrap16Le.bits_per_pixel_approx(), + PixelFormat::Rgba64Le.bits_per_pixel_approx() + ); + assert_eq!(PixelFormat::Gbrp8.bits_per_pixel_approx(), 24); + assert_eq!(PixelFormat::Gbrp16Le.bits_per_pixel_approx(), 48); + assert_eq!(PixelFormat::Gbrap16Le.bits_per_pixel_approx(), 64); + } + + #[test] + fn gbrap8_metadata() { + // Gbrap8 completes the GBR(A) family: every depth on the + // 8/10/12/14/16 ladder now has both an alpha-less and an + // alpha-carrying variant. Shape matches the rest of the + // alpha-carrying family: planar, 4 planes (G, B, R, + // full-resolution A), alpha set, never palette. + let fmt = PixelFormat::Gbrap8; + assert!(fmt.is_planar()); + assert_eq!(fmt.plane_count(), 4); + assert!(fmt.has_alpha()); + assert!(!fmt.is_palette()); + } + + #[test] + fn gbrap8_bits_per_pixel_approx() { + // Four bytes per pixel: the packed Rgba density (planar layout + // doesn't change bits-per-pixel), i.e. the alpha plane adds a + // full 8 bits on top of Gbrp8. + assert_eq!(PixelFormat::Gbrap8.bits_per_pixel_approx(), 32); + assert_eq!( + PixelFormat::Gbrap8.bits_per_pixel_approx(), + PixelFormat::Rgba.bits_per_pixel_approx() + ); + assert_eq!( + PixelFormat::Gbrap8.bits_per_pixel_approx(), + PixelFormat::Gbrp8.bits_per_pixel_approx() + 8 + ); + } + + #[test] + fn gbr_family_alpha_ladder_complete() { + // Every GBR depth has an alpha companion with exactly one more + // plane and the same planarity — the asymmetry Gbrap8 closed. + let pairs = [ + (PixelFormat::Gbrp8, PixelFormat::Gbrap8), + (PixelFormat::Gbrp10Le, PixelFormat::Gbrap10Le), + (PixelFormat::Gbrp12Le, PixelFormat::Gbrap12Le), + (PixelFormat::Gbrp14Le, PixelFormat::Gbrap14Le), + (PixelFormat::Gbrp16Le, PixelFormat::Gbrap16Le), + ]; + for (gbr, gbra) in pairs { + assert!(gbr.is_planar() && gbra.is_planar()); + assert_eq!(gbr.plane_count(), 3, "{gbr:?}"); + assert_eq!(gbra.plane_count(), 4, "{gbra:?}"); + assert!(!gbr.has_alpha(), "{gbr:?}"); + assert!(gbra.has_alpha(), "{gbra:?}"); + } + } + + #[test] + fn ya16le_metadata() { + // Same packed shape as Ya8 (interleaved Y, A in one plane), + // widened to 16-bit LE words: not planar, single plane, alpha + // set, never palette. Density is exactly double Ya8's and + // matches half of Rgba64Le (two components instead of four). + let fmt = PixelFormat::Ya16Le; + assert!(!fmt.is_planar()); + assert_eq!(fmt.plane_count(), 1); + assert!(fmt.has_alpha()); + assert!(!fmt.is_palette()); + assert_eq!(fmt.bits_per_pixel_approx(), 32); + assert_eq!( + fmt.bits_per_pixel_approx(), + PixelFormat::Ya8.bits_per_pixel_approx() * 2 + ); + assert_eq!( + fmt.bits_per_pixel_approx(), + PixelFormat::Rgba64Le.bits_per_pixel_approx() / 2 + ); + // The alpha word adds a full 16 bits on top of Gray16Le. + assert_eq!( + fmt.bits_per_pixel_approx(), + PixelFormat::Gray16Le.bits_per_pixel_approx() + 16 + ); + } + + #[test] + fn cmyk_inverted_metadata() { + // The inverted-ink convention changes sample semantics, not + // layout: CmykInverted must be metadata-identical to Cmyk on + // every shape predicate. + let (reg, inv) = (PixelFormat::Cmyk, PixelFormat::CmykInverted); + for fmt in [reg, inv] { + assert!(!fmt.is_planar(), "{fmt:?}"); + assert_eq!(fmt.plane_count(), 1, "{fmt:?}"); + assert!(!fmt.has_alpha(), "{fmt:?}"); + assert!(!fmt.is_palette(), "{fmt:?}"); + } + assert_eq!(reg.bits_per_pixel_approx(), inv.bits_per_pixel_approx()); + assert_eq!(inv.bits_per_pixel_approx(), 32); + // They remain distinct formats on the wire-stable axis. + assert_ne!(reg as u16, inv as u16); + } + + #[test] + fn deep_yuva420_planar_metadata() { + // The 4:2:0 completions share the deep-Yuva shape: planar, 4 + // planes (Y, U, V, full-resolution A), alpha set, no palette. + for fmt in [ + PixelFormat::Yuva420P10Le, + PixelFormat::Yuva420P12Le, + PixelFormat::Yuva420P16Le, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes"); + assert!(fmt.has_alpha(), "{fmt:?} must carry alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + } + } + + #[test] + fn deep_yuva420_bits_per_pixel_approx() { + // Same estimator convention as the 4:2:2/4:4:4 deep Yuva arms: + // 16-bit-word storage cost, with the full-resolution alpha word + // adding 16 on top of the alpha-less sampling's number. + for fmt in [ + PixelFormat::Yuva420P10Le, + PixelFormat::Yuva420P12Le, + PixelFormat::Yuva420P16Le, + ] { + assert_eq!(fmt.bits_per_pixel_approx(), 40, "{fmt:?}"); + } + assert_eq!( + PixelFormat::Yuva420P10Le.bits_per_pixel_approx(), + PixelFormat::Yuv420P10Le.bits_per_pixel_approx() + 16 + ); + assert_eq!( + PixelFormat::Yuva420P12Le.bits_per_pixel_approx(), + PixelFormat::Yuv420P12Le.bits_per_pixel_approx() + 16 + ); + assert_eq!( + PixelFormat::Yuva420P16Le.bits_per_pixel_approx(), + PixelFormat::Yuv420P16Le.bits_per_pixel_approx() + 16 + ); + } + + /// Every `PixelFormat` variant, in discriminant order. Extend this + /// list whenever a variant is appended — the consistency tests + /// below sweep it. + const ALL_PIXEL_FORMATS: [PixelFormat; 70] = [ + PixelFormat::Yuv420P, + PixelFormat::Yuv422P, + PixelFormat::Yuv444P, + PixelFormat::Rgb24, + PixelFormat::Rgba, + PixelFormat::Gray8, + PixelFormat::Pal8, + PixelFormat::Bgr24, + PixelFormat::Bgra, + PixelFormat::Argb, + PixelFormat::Abgr, + PixelFormat::Rgb48Le, + PixelFormat::Rgba64Le, + PixelFormat::Gray16Le, + PixelFormat::Gray10Le, + PixelFormat::Gray12Le, + PixelFormat::Yuv420P10Le, + PixelFormat::Yuv422P10Le, + PixelFormat::Yuv444P10Le, + PixelFormat::Yuv420P12Le, + PixelFormat::Yuv422P12Le, + PixelFormat::Yuv444P12Le, + PixelFormat::YuvJ420P, + PixelFormat::YuvJ422P, + PixelFormat::YuvJ444P, + PixelFormat::Nv12, + PixelFormat::Nv21, + PixelFormat::Ya8, + PixelFormat::Yuva420P, + PixelFormat::MonoBlack, + PixelFormat::MonoWhite, + PixelFormat::Yuyv422, + PixelFormat::Uyvy422, + PixelFormat::Cmyk, + PixelFormat::Yuv411P, + PixelFormat::Gbrp10Le, + PixelFormat::Gbrap10Le, + PixelFormat::Gbrp12Le, + PixelFormat::Gbrap12Le, + PixelFormat::Gbrp14Le, + PixelFormat::Gbrap14Le, + PixelFormat::Yuv420P16Le, + PixelFormat::Yuv422P16Le, + PixelFormat::Yuv444P16Le, + PixelFormat::Yuva422P, + PixelFormat::Yuva444P, + PixelFormat::Yuva422P10Le, + PixelFormat::Yuva422P12Le, + PixelFormat::Yuva444P10Le, + PixelFormat::Yuva444P12Le, + PixelFormat::Yuva422P16Le, + PixelFormat::Yuva444P16Le, + PixelFormat::Gbrp8, + PixelFormat::Gbrp16Le, + PixelFormat::Gbrap16Le, + PixelFormat::Yuva420P10Le, + PixelFormat::Yuva420P12Le, + PixelFormat::Yuva420P16Le, + PixelFormat::Gbrap8, + PixelFormat::Ya16Le, + PixelFormat::CmykInverted, + PixelFormat::Yuv440P, + PixelFormat::Yuv440P10Le, + PixelFormat::Yuv440P12Le, + PixelFormat::Yuv440P16Le, + PixelFormat::GrayF32Le, + PixelFormat::RgbF32Le, + PixelFormat::RgbaF32Le, + PixelFormat::GbrpF32Le, + PixelFormat::GbrapF32Le, + ]; + + #[test] + fn all_pixel_formats_list_is_complete_and_distinct() { + // The list is discriminant-ordered and dense: 0..70 with no + // gaps and no duplicates. A newly appended variant that isn't + // added to the list will break the length or density check. + let mut seen = std::collections::HashSet::new(); + for fmt in ALL_PIXEL_FORMATS { + assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}"); + } + for d in 0..ALL_PIXEL_FORMATS.len() as u16 { + assert!(seen.contains(&d), "discriminant {d} missing from list"); + } + } + + #[test] + fn yuv440_family_metadata() { + // The whole 4:4:0 ladder shares one shape: planar, 3 planes, + // no alpha, no palette, full-width half-height chroma. + for fmt in [ + PixelFormat::Yuv440P, + PixelFormat::Yuv440P10Le, + PixelFormat::Yuv440P12Le, + PixelFormat::Yuv440P16Le, + ] { + assert!(fmt.is_planar(), "{fmt:?} must be planar"); + assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes"); + assert!(!fmt.has_alpha(), "{fmt:?} must not carry alpha"); + assert!(!fmt.is_palette(), "{fmt:?} must not be palette"); + assert!(!fmt.is_float(), "{fmt:?} must not be float"); + assert_eq!( + fmt.chroma_subsampling(), + Some((0, 1)), + "{fmt:?} must be full-width, half-height chroma" + ); + } + } + + #[test] + fn yuv440_bits_per_pixel_approx() { + // 4:4:0 packs the same samples-per-pixel as 4:2:2 at every + // depth (2 samples/pixel), so the estimator numbers coincide. + assert_eq!( + PixelFormat::Yuv440P.bits_per_pixel_approx(), + PixelFormat::Yuv422P.bits_per_pixel_approx() + ); + assert_eq!(PixelFormat::Yuv440P.bits_per_pixel_approx(), 16); + for (f440, f422) in [ + (PixelFormat::Yuv440P10Le, PixelFormat::Yuv422P10Le), + (PixelFormat::Yuv440P12Le, PixelFormat::Yuv422P12Le), + (PixelFormat::Yuv440P16Le, PixelFormat::Yuv422P16Le), + ] { + assert_eq!( + f440.bits_per_pixel_approx(), + f422.bits_per_pixel_approx(), + "{f440:?}" + ); + assert_eq!(f440.bits_per_pixel_approx(), 32, "{f440:?}"); + } + } + + #[test] + fn yuv440_plane_geometry() { + // Even sizes: chroma keeps the width, halves the height. + assert_eq!( + PixelFormat::Yuv440P.plane_dimensions(0, 640, 480), + Some((640, 480)) + ); + assert_eq!( + PixelFormat::Yuv440P.plane_dimensions(1, 640, 480), + Some((640, 240)) + ); + assert_eq!( + PixelFormat::Yuv440P.plane_dimensions(2, 640, 480), + Some((640, 240)) + ); + // Odd height rounds up; odd width is untouched (ssx = 0). + for fmt in [ + PixelFormat::Yuv440P, + PixelFormat::Yuv440P10Le, + PixelFormat::Yuv440P12Le, + PixelFormat::Yuv440P16Le, + ] { + assert_eq!(fmt.plane_dimensions(0, 7, 5), Some((7, 5)), "{fmt:?}"); + assert_eq!(fmt.plane_dimensions(1, 7, 5), Some((7, 3)), "{fmt:?}"); + assert_eq!(fmt.plane_dimensions(2, 7, 5), Some((7, 3)), "{fmt:?}"); + assert_eq!(fmt.plane_dimensions(3, 7, 5), None, "{fmt:?}"); + } + // Degenerate 1-row picture: the chroma plane still has a row. + assert_eq!(PixelFormat::Yuv440P.plane_dimensions(1, 3, 1), Some((3, 1))); + } + + #[test] + fn yuv440_sizing_round_trips() { + // 6×5 8-bit: luma 6×5 = 30, each chroma 6×ceil(5/2) = 18. + assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(0, 6, 5), Some(30)); + assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(1, 6, 5), Some(18)); + assert_eq!(PixelFormat::Yuv440P.plane_size_bytes(2, 6, 5), Some(18)); + assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(6, 5), Some(66)); + // 7×5: 35 + 21 + 21. + assert_eq!(PixelFormat::Yuv440P.frame_size_bytes(7, 5), Some(77)); + // Deep variants store 16-bit words: exactly double at every + // depth (row bytes = width × 2 regardless of valid bits). + for fmt in [ + PixelFormat::Yuv440P10Le, + PixelFormat::Yuv440P12Le, + PixelFormat::Yuv440P16Le, + ] { + assert_eq!(fmt.plane_row_bytes(0, 7), Some(14), "{fmt:?}"); + assert_eq!(fmt.plane_row_bytes(1, 7), Some(14), "{fmt:?}"); + assert_eq!(fmt.frame_size_bytes(7, 5), Some(154), "{fmt:?}"); + } + } + + #[test] + fn float_family_metadata() { + // Packed trio: single plane, not planar. + for fmt in [ + PixelFormat::GrayF32Le, + PixelFormat::RgbF32Le, + PixelFormat::RgbaF32Le, + ] { + assert!(!fmt.is_planar(), "{fmt:?}"); + assert_eq!(fmt.plane_count(), 1, "{fmt:?}"); + } + // Planar pair: GBR(A) shape. + assert!(PixelFormat::GbrpF32Le.is_planar()); + assert_eq!(PixelFormat::GbrpF32Le.plane_count(), 3); + assert!(PixelFormat::GbrapF32Le.is_planar()); + assert_eq!(PixelFormat::GbrapF32Le.plane_count(), 4); + // Alpha only on the RGBA/GBRA members. + assert!(!PixelFormat::GrayF32Le.has_alpha()); + assert!(!PixelFormat::RgbF32Le.has_alpha()); + assert!(PixelFormat::RgbaF32Le.has_alpha()); + assert!(!PixelFormat::GbrpF32Le.has_alpha()); + assert!(PixelFormat::GbrapF32Le.has_alpha()); + // The whole family is float, non-palette, and has no chroma + // grid. + for fmt in [ + PixelFormat::GrayF32Le, + PixelFormat::RgbF32Le, + PixelFormat::RgbaF32Le, + PixelFormat::GbrpF32Le, + PixelFormat::GbrapF32Le, + ] { + assert!(fmt.is_float(), "{fmt:?} must be float"); + assert!(!fmt.is_palette(), "{fmt:?}"); + assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}"); + } + } + + #[test] + fn is_float_false_for_integer_formats() { + for fmt in ALL_PIXEL_FORMATS { + let expect = matches!( + fmt, + PixelFormat::GrayF32Le + | PixelFormat::RgbF32Le + | PixelFormat::RgbaF32Le + | PixelFormat::GbrpF32Le + | PixelFormat::GbrapF32Le + ); + assert_eq!(fmt.is_float(), expect, "{fmt:?}"); + } + } + + #[test] + fn float_family_bits_per_pixel_and_sizing() { + // Packed bits equal storage bits: every sample is a full + // binary32 word. + assert_eq!(PixelFormat::GrayF32Le.bits_per_pixel_approx(), 32); + assert_eq!(PixelFormat::RgbF32Le.bits_per_pixel_approx(), 96); + assert_eq!(PixelFormat::RgbaF32Le.bits_per_pixel_approx(), 128); + assert_eq!(PixelFormat::GbrpF32Le.bits_per_pixel_approx(), 96); + assert_eq!(PixelFormat::GbrapF32Le.bits_per_pixel_approx(), 128); + // Packed row/frame sizes. + assert_eq!(PixelFormat::GrayF32Le.plane_row_bytes(0, 3), Some(12)); + assert_eq!(PixelFormat::GrayF32Le.frame_size_bytes(5, 3), Some(60)); + assert_eq!(PixelFormat::RgbF32Le.plane_row_bytes(0, 7), Some(84)); + assert_eq!(PixelFormat::RgbaF32Le.frame_size_bytes(3, 3), Some(144)); + // Planar float: 4 bytes per sample on every plane; the packed + // and planar layouts of the same component set cost the same. + assert_eq!(PixelFormat::GbrpF32Le.plane_row_bytes(1, 7), Some(28)); + assert_eq!( + PixelFormat::GbrpF32Le.frame_size_bytes(7, 5), + PixelFormat::RgbF32Le.frame_size_bytes(7, 5) + ); + assert_eq!( + PixelFormat::GbrapF32Le.frame_size_bytes(7, 5), + PixelFormat::RgbaF32Le.frame_size_bytes(7, 5) + ); + // All planes of planar float GBR(A) are full resolution. + for plane in 0..4 { + assert_eq!( + PixelFormat::GbrapF32Le.plane_dimensions(plane, 7, 5), + Some((7, 5)) + ); + } + } + + #[test] + fn chroma_subsampling_table() { + use PixelFormat::*; + // One representative per sampling class plus the full new + // family; the wildcard class returns None. + assert_eq!(Yuv420P.chroma_subsampling(), Some((1, 1))); + assert_eq!(Nv12.chroma_subsampling(), Some((1, 1))); + assert_eq!(Yuva420P16Le.chroma_subsampling(), Some((1, 1))); + assert_eq!(Yuv422P.chroma_subsampling(), Some((1, 0))); + assert_eq!(Yuyv422.chroma_subsampling(), Some((1, 0))); + assert_eq!(Uyvy422.chroma_subsampling(), Some((1, 0))); + assert_eq!(Yuv444P.chroma_subsampling(), Some((0, 0))); + assert_eq!(Yuva444P12Le.chroma_subsampling(), Some((0, 0))); + assert_eq!(Yuv411P.chroma_subsampling(), Some((2, 0))); + assert_eq!(Yuv440P.chroma_subsampling(), Some((0, 1))); + assert_eq!(Yuv440P16Le.chroma_subsampling(), Some((0, 1))); + for fmt in [ + Gray8, + Gray16Le, + Ya8, + Ya16Le, + Pal8, + MonoBlack, + MonoWhite, + Rgb24, + Rgba, + Rgb48Le, + Rgba64Le, + Cmyk, + CmykInverted, + Gbrp8, + Gbrap16Le, + GrayF32Le, + RgbaF32Le, + GbrapF32Le, + ] { + assert_eq!(fmt.chroma_subsampling(), None, "{fmt:?}"); + } + } + + #[test] + fn plane_dimensions_odd_sizes_across_samplings() { + use PixelFormat::*; + // 4:2:0 — both axes ceil-halved. + assert_eq!(Yuv420P.plane_dimensions(1, 7, 5), Some((4, 3))); + // 4:2:2 — width ceil-halved, height untouched. + assert_eq!(Yuv422P.plane_dimensions(2, 7, 5), Some((4, 5))); + // 4:1:1 — width ceil-quartered. + assert_eq!(Yuv411P.plane_dimensions(1, 7, 5), Some((2, 5))); + assert_eq!(Yuv411P.plane_dimensions(1, 9, 5), Some((3, 5))); + // 4:4:4 — untouched. + assert_eq!(Yuv444P.plane_dimensions(1, 7, 5), Some((7, 5))); + // Semi-planar chroma positions. + assert_eq!(Nv12.plane_dimensions(1, 7, 5), Some((4, 3))); + assert_eq!(Nv21.plane_dimensions(1, 7, 5), Some((4, 3))); + // Alpha planes are never subsampled. + assert_eq!(Yuva420P.plane_dimensions(3, 7, 5), Some((7, 5))); + assert_eq!(Yuva422P16Le.plane_dimensions(3, 7, 5), Some((7, 5))); + // Planar RGB planes are never subsampled. + for plane in 0..3 { + assert_eq!(Gbrp12Le.plane_dimensions(plane, 7, 5), Some((7, 5))); + } + // Out-of-range planes. + assert_eq!(Rgb24.plane_dimensions(1, 8, 8), None); + assert_eq!(Yuv420P.plane_dimensions(3, 8, 8), None); + assert_eq!(Yuva420P.plane_dimensions(4, 8, 8), None); + // Zero-sized pictures collapse every plane to zero. + assert_eq!(Yuv440P.plane_dimensions(1, 0, 0), Some((0, 0))); + } + + #[test] + fn plane_row_bytes_conventions() { + use PixelFormat::*; + // Bit-packed mono: ceil(width / 8) with a ragged tail byte. + assert_eq!(MonoBlack.plane_row_bytes(0, 13), Some(2)); + assert_eq!(MonoWhite.plane_row_bytes(0, 16), Some(2)); + assert_eq!(MonoBlack.plane_row_bytes(0, 17), Some(3)); + // Packed 4:2:2: 4-byte macropixels, odd width rounds up. + assert_eq!(Yuyv422.plane_row_bytes(0, 6), Some(12)); + assert_eq!(Uyvy422.plane_row_bytes(0, 7), Some(16)); + // Semi-planar chroma: 2 bytes per position. + assert_eq!(Nv12.plane_row_bytes(0, 7), Some(7)); + assert_eq!(Nv12.plane_row_bytes(1, 7), Some(8)); + // Deep planar planes: 2 bytes per sample regardless of the + // number of valid bits in the word. + assert_eq!(Yuv420P10Le.plane_row_bytes(1, 7), Some(8)); + assert_eq!(Gbrap14Le.plane_row_bytes(3, 5), Some(10)); + // Packed pixel costs. + assert_eq!(Rgb24.plane_row_bytes(0, 5), Some(15)); + assert_eq!(Rgb48Le.plane_row_bytes(0, 2), Some(12)); + assert_eq!(Rgba64Le.plane_row_bytes(0, 2), Some(16)); + assert_eq!(Ya16Le.plane_row_bytes(0, 3), Some(12)); + assert_eq!(Cmyk.plane_row_bytes(0, 3), Some(12)); + // Out-of-range plane. + assert_eq!(Gray8.plane_row_bytes(1, 8), None); + } + + #[test] + fn frame_size_examples() { + use PixelFormat::*; + assert_eq!(Yuv420P.frame_size_bytes(4, 4), Some(24)); + assert_eq!(Nv12.frame_size_bytes(7, 5), Some(59)); // 35 + 4×3×2 + assert_eq!(Yuyv422.frame_size_bytes(7, 2), Some(32)); + assert_eq!(MonoBlack.frame_size_bytes(13, 3), Some(6)); + assert_eq!(Pal8.frame_size_bytes(5, 4), Some(20)); + assert_eq!(Ya16Le.frame_size_bytes(3, 3), Some(36)); + assert_eq!(Yuva444P16Le.frame_size_bytes(3, 3), Some(72)); + } + + #[test] + fn frame_size_is_sum_of_planes_for_every_format() { + for fmt in ALL_PIXEL_FORMATS { + for (w, h) in [(0, 0), (1, 1), (2, 2), (7, 5), (16, 16), (13, 1), (1, 13)] { + let total = fmt + .frame_size_bytes(w, h) + .unwrap_or_else(|| panic!("{fmt:?} {w}x{h} must size")); + let sum: usize = (0..fmt.plane_count()) + .map(|p| fmt.plane_size_bytes(p, w, h).unwrap()) + .sum(); + assert_eq!(total, sum, "{fmt:?} {w}x{h}"); + // Plane 0 is always the full pixel grid. + assert_eq!(fmt.plane_dimensions(0, w, h), Some((w, h)), "{fmt:?}"); + // The plane table ends exactly at plane_count. + assert_eq!(fmt.plane_dimensions(fmt.plane_count(), w, h), None); + // Tightly-packed storage can never be smaller than the + // packed-bits density estimate. + let storage_bits = total as u128 * 8; + let density_bits = w as u128 * h as u128 * fmt.bits_per_pixel_approx() as u128; + assert!( + storage_bits >= density_bits, + "{fmt:?} {w}x{h}: storage {storage_bits} < density {density_bits}" + ); + } + } + } + + #[test] + fn sizing_overflow_returns_none() { + assert_eq!( + PixelFormat::Rgba64Le.frame_size_bytes(u32::MAX, u32::MAX), + None + ); + assert_eq!( + PixelFormat::RgbaF32Le.frame_size_bytes(u32::MAX, u32::MAX), + None + ); + assert_eq!( + PixelFormat::Yuv440P16Le.plane_size_bytes(0, u32::MAX, u32::MAX), + None + ); + } +} diff --git a/crates/vendor/oxideav-core/src/frame.rs b/crates/vendor/oxideav-core/src/frame.rs new file mode 100644 index 00000000..a62c7c3a --- /dev/null +++ b/crates/vendor/oxideav-core/src/frame.rs @@ -0,0 +1,649 @@ +//! Uncompressed audio and video frames. + +use crate::subtitle::SubtitleCue; +use crate::vector::VectorFrame; + +/// A decoded chunk of uncompressed data: either audio samples, a video +/// picture, or (for subtitle streams) a single styled cue. +/// +/// Marked `#[non_exhaustive]` — consumers that match on variants must +/// include a wildcard arm. This lets the crate add new frame kinds (data +/// tracks, hap rops, …) without breaking downstream code. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Frame { + /// Uncompressed audio samples. + Audio(AudioFrame), + /// One uncompressed video picture. + Video(VideoFrame), + /// A single subtitle cue. Timing is carried inside the cue itself + /// (`start_us`/`end_us`) so it's independent of container time bases, + /// but the enclosing pipeline/muxer can still rescale via `pts` at + /// the packet layer. + Subtitle(SubtitleCue), + /// A resolution-independent vector-graphics frame. Produced by + /// vector-format decoders (`oxideav-svg`, the vector path of + /// `oxideav-pdf`) and consumed by vector renderers / writers. + /// See [`crate::vector`] for the full primitive set. + Vector(VectorFrame), +} + +impl Frame { + /// Presentation timestamp of the frame in its stream's time base + /// (a subtitle cue reports its `start_us`); `None` if unknown. + pub fn pts(&self) -> Option { + match self { + Self::Audio(a) => a.pts, + Self::Video(v) => v.pts, + Self::Subtitle(s) => Some(s.start_us), + Self::Vector(v) => v.pts, + } + } +} + +/// Uncompressed audio frame. +/// +/// Stream-level properties (sample format, channel count, sample rate, +/// time base) are NOT carried per-frame — read them from the stream's +/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight +/// because real-time playback moves thousands per second per stream. +/// +/// Sample layout is determined by the stream's `SampleFormat`: +/// - Interleaved formats: `data` has one plane; samples are stored as +/// `ch0 ch1 ... chN ch0 ch1 ... chN ...`. +/// - Planar formats: `data` has one plane per channel. +/// +/// Use [`SampleFormat::plane_count`](crate::SampleFormat::plane_count) +/// with the stream's channel count to compute the expected `data.len()`. +#[derive(Clone, Debug)] +pub struct AudioFrame { + /// Number of samples *per channel* in this frame. Variable per-frame + /// for VBR codecs and on partial flushes. + pub samples: u32, + /// Presentation timestamp in the stream's time base; `None` if unknown. + pub pts: Option, + /// Raw sample bytes. Length matches `format.plane_count(channels)` + /// from the stream's `CodecParameters`. + pub data: Vec>, +} + +/// Uncompressed video frame. +/// +/// Stream-level properties (pixel format, width, height, time base) are +/// NOT carried per-frame — read them from the stream's +/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight +/// because real-time playback moves thousands per second per stream. +/// +/// # Side-channels +/// +/// `VideoFrame` (like [`VideoPlane`]) is a fully-public struct built by +/// struct literal throughout the codec crates, so per-frame metadata +/// cannot be added as new fields without breaking every constructor. +/// Instead, optional metadata rides in-band as *side-channel* entries at +/// the tail of `planes`: [`VideoPlane`] values whose shape is impossible +/// for an image plane, which makes them unambiguous. Two side-channel +/// record kinds exist, distinguished by their `stride` tag: +/// +/// - **Palette** — `stride == 0`, non-empty `data`. Impossible for an +/// image plane because an image plane's `data` is `stride × rows` +/// long, so a zero stride forces empty data. Carries the color table +/// for palette-indexed content +/// ([`PixelFormat::Pal8`](crate::PixelFormat::Pal8)); see +/// [`palette`](Self::palette) / [`set_palette`](Self::set_palette). +/// - **Per-plane significant bits** — `stride == usize::MAX`, non-empty +/// `data`. Impossible for an image plane because `stride × rows` +/// bytes with any non-zero row count would exceed what a `Vec` can +/// hold. Carries mixed per-plane bit depths (e.g. 12-bit luma with +/// 10-bit chroma from a wavelet codec's custom signal range); see +/// [`significant_bits`](Self::significant_bits) / +/// [`set_significant_bits`](Self::set_significant_bits). +/// +/// The two records compose: a frame can carry both at once, in either +/// order, within the trailing run of side-channel-shaped entries. The +/// typed accessors find each record by its `stride` tag regardless of +/// order, and [`image_planes`](Self::image_planes) / +/// [`image_plane_count`](Self::image_plane_count) exclude the whole +/// trailing run. Frames without any attached side-channel are +/// byte-for-byte identical to what they always were. +#[derive(Clone, Debug)] +pub struct VideoFrame { + /// Presentation timestamp in the stream's time base; `None` if unknown. + pub pts: Option, + /// One entry per plane (e.g., 3 for Yuv420P). Each entry is `(stride, bytes)`. + /// + /// May additionally end with side-channel entries (palette, + /// per-plane significant bits — see the type-level docs). Code that + /// wants only pixel planes should iterate + /// [`image_planes`](Self::image_planes) instead of this field. + pub planes: Vec, +} + +/// `stride` tag of the per-plane significant-bits side-channel record. +/// (The palette record's tag is `0`; see the [`VideoFrame`] docs.) +const SIGNIFICANT_BITS_STRIDE: usize = usize::MAX; + +impl VideoFrame { + /// `true` when `plane` has a side-channel record shape: one of the + /// two impossible-for-an-image-plane sentinels described in the + /// type-level docs. + fn is_side_channel_entry(plane: &VideoPlane) -> bool { + (plane.stride == 0 || plane.stride == SIGNIFICANT_BITS_STRIDE) && !plane.data.is_empty() + } + + /// Index of the first entry of the trailing side-channel run — equal + /// to the number of image planes. Scans backwards from the tail + /// while entries have a side-channel shape. + fn side_channel_run_start(&self) -> usize { + let mut start = self.planes.len(); + while start > 0 && Self::is_side_channel_entry(&self.planes[start - 1]) { + start -= 1; + } + start + } + + /// Index in `planes` of the side-channel record tagged with + /// `stride_tag`, searching the trailing side-channel run only (the + /// last match wins if a malformed frame carries duplicates). + fn side_channel_index(&self, stride_tag: usize) -> Option { + let start = self.side_channel_run_start(); + self.planes[start..] + .iter() + .rposition(|p| p.stride == stride_tag) + .map(|i| start + i) + } + + /// Remove every record tagged `stride_tag` from the trailing + /// side-channel run, returning the data of the record the readers + /// would have reported (the last match — consistent with + /// [`side_channel_index`](Self::side_channel_index)). + fn remove_side_channel(&mut self, stride_tag: usize) -> Option> { + let reported = self + .side_channel_index(stride_tag) + .map(|i| self.planes.remove(i).data); + while let Some(i) = self.side_channel_index(stride_tag) { + self.planes.remove(i); + } + reported + } + + /// The frame's attached palette, if any. + /// + /// Returns the raw bytes of the palette side-channel (see the + /// type-level docs): packed 3-byte RGB entries, entry `i` at bytes + /// `3*i .. 3*i + 3` in R, G, B order. A full + /// [`Pal8`](crate::PixelFormat::Pal8) table is 256 entries + /// (768 bytes), but producers may attach fewer when the source + /// image declares a shorter table; indices at or beyond + /// `len / 3` are undefined by this frame and up to the consumer's + /// missing-entry policy (typically black). + pub fn palette(&self) -> Option<&[u8]> { + self.side_channel_index(0) + .map(|i| self.planes[i].data.as_slice()) + } + + /// The RGB triplet for palette entry `index`, or `None` when no + /// palette is attached or the attached table is too short to cover + /// `index`. Sugar over [`palette`](Self::palette) for per-pixel + /// lookups. + pub fn palette_rgb(&self, index: u8) -> Option<[u8; 3]> { + let pal = self.palette()?; + let at = usize::from(index) * 3; + let entry = pal.get(at..at + 3)?; + Some([entry[0], entry[1], entry[2]]) + } + + /// Attach (or replace) the frame's palette side-channel. + /// + /// `rgb` is packed 3-byte RGB entries — see + /// [`palette`](Self::palette) for the exact layout; pass a length + /// that is a multiple of 3 (up to 768 bytes for a full 256-entry + /// [`Pal8`](crate::PixelFormat::Pal8) table). The bytes are stored + /// verbatim. An empty `rgb` removes any attached palette instead + /// (the sentinel requires non-empty data), leaving the frame + /// exactly as it was before any palette was attached. + pub fn set_palette(&mut self, rgb: Vec) { + self.remove_side_channel(0); + if !rgb.is_empty() { + self.planes.push(VideoPlane { + stride: 0, + data: rgb, + }); + } + } + + /// Builder-style counterpart to [`set_palette`](Self::set_palette) + /// for construction chains: + /// `VideoFrame { pts, planes }.with_palette(rgb)`. + pub fn with_palette(mut self, rgb: Vec) -> Self { + self.set_palette(rgb); + self + } + + /// Detach and return the frame's palette side-channel, if any. + /// Afterwards the frame carries no palette (any other side-channel + /// record is left in place). + pub fn take_palette(&mut self) -> Option> { + self.remove_side_channel(0) + } + + /// The frame's attached per-plane significant-bits record, if any. + /// + /// Returns the raw bytes of the significant-bits side-channel (see + /// the type-level docs): byte `k` is the number of significant bits + /// in the samples of image plane `k`, in plane order. This lets a + /// producer express **mixed** per-plane depths that no single + /// [`PixelFormat`](crate::PixelFormat) variant can name — e.g. a + /// wavelet codec's custom signal range with 12-bit luma and 10-bit + /// chroma, stored on a `Yuv444P12Le` surface with an attached + /// record of `[12, 10, 10]`. + /// + /// # Semantics + /// + /// - Values are **LSB-anchored**: a plane with `b` significant bits + /// keeps its sample values in the low `b` bits of each storage + /// word, with the upper bits zero — the same convention as this + /// crate's partial-depth formats (`Gray10Le`, `Yuv420P10Le`, + /// `Gbrp12Le`, …, each documented as "uses the low N bits of a + /// 16-bit word"). Full-scale for `b` significant bits is + /// `(1 << b) - 1`. + /// - Each value must satisfy `1 ≤ b ≤ 8 × storage-word-bytes` of + /// the frame's pixel format (so at most 8 for byte-sized planes, + /// 16 for LE-16-bit-word planes). The record refines the storage + /// format's *significant* depth; it never changes the storage + /// word size or plane geometry. + /// - A record shorter than the image-plane count (or a missing + /// record) leaves the uncovered planes at the pixel format's own + /// documented depth. Bytes are stored verbatim; out-of-range + /// values are a producer bug and consumers may clamp or reject + /// them. + pub fn significant_bits(&self) -> Option<&[u8]> { + self.side_channel_index(SIGNIFICANT_BITS_STRIDE) + .map(|i| self.planes[i].data.as_slice()) + } + + /// The significant-bit count for image plane `plane`, or `None` + /// when no record is attached or the attached record is too short + /// to cover `plane` (fall back to the pixel format's own depth). + /// Sugar over [`significant_bits`](Self::significant_bits) for + /// per-plane lookups. + pub fn plane_significant_bits(&self, plane: usize) -> Option { + self.significant_bits()?.get(plane).copied() + } + + /// Attach (or replace) the frame's per-plane significant-bits + /// side-channel. + /// + /// `bits` holds one byte per image plane, in plane order — see + /// [`significant_bits`](Self::significant_bits) for the exact + /// semantics (LSB-anchored values, `1 ≤ b ≤ storage word bits`). + /// The bytes are stored verbatim. An empty `bits` removes any + /// attached record instead (the sentinel requires non-empty data), + /// leaving the frame exactly as it was before any record was + /// attached. Any attached palette is unaffected. + pub fn set_significant_bits(&mut self, bits: Vec) { + self.remove_side_channel(SIGNIFICANT_BITS_STRIDE); + if !bits.is_empty() { + self.planes.push(VideoPlane { + stride: SIGNIFICANT_BITS_STRIDE, + data: bits, + }); + } + } + + /// Builder-style counterpart to + /// [`set_significant_bits`](Self::set_significant_bits) for + /// construction chains: + /// `VideoFrame { pts, planes }.with_significant_bits(bits)`. + pub fn with_significant_bits(mut self, bits: Vec) -> Self { + self.set_significant_bits(bits); + self + } + + /// Detach and return the frame's per-plane significant-bits + /// side-channel, if any. Afterwards the frame carries no + /// significant-bits record (any attached palette is left in place). + pub fn take_significant_bits(&mut self) -> Option> { + self.remove_side_channel(SIGNIFICANT_BITS_STRIDE) + } + + /// The frame's image planes — `planes` with the trailing + /// side-channel entries (palette, significant bits) excluded. + /// Prefer this over indexing `planes` directly in code that + /// handles side-channel-capable frames. + pub fn image_planes(&self) -> &[VideoPlane] { + &self.planes[..self.image_plane_count()] + } + + /// Number of image planes (excludes every side-channel entry). + /// Matches the stream pixel format's + /// [`plane_count`](crate::PixelFormat::plane_count) for well-formed + /// frames. + pub fn image_plane_count(&self) -> usize { + self.side_channel_run_start() + } +} + +/// One plane of a [`VideoFrame`]: row-major sample bytes plus the +/// stride between rows. +/// +/// An entry with non-empty `data` and a `stride` of `0` or `usize::MAX` +/// is not an image plane: it is a side-channel record (palette and +/// per-plane significant bits respectively) described on [`VideoFrame`] +/// — only meaningful within the trailing run of `VideoFrame::planes`. +#[derive(Clone, Debug)] +pub struct VideoPlane { + /// Bytes per row in `data`. + pub stride: usize, + /// Raw plane bytes, `stride × rows` long (rows may carry padding + /// beyond the visible width). + pub data: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn gray_frame() -> VideoFrame { + // 4×2 Gray8 image plane. + VideoFrame { + pts: Some(7), + planes: vec![VideoPlane { + stride: 4, + data: vec![0u8; 8], + }], + } + } + + /// A full 256-entry table where entry i is (i, !i, i^0x55). + fn full_palette() -> Vec { + (0u16..256) + .flat_map(|i| { + let i = i as u8; + [i, !i, i ^ 0x55] + }) + .collect() + } + + #[test] + fn frame_without_palette_reports_none_and_full_image_planes() { + let f = gray_frame(); + assert_eq!(f.palette(), None); + assert_eq!(f.palette_rgb(0), None); + assert_eq!(f.image_plane_count(), 1); + assert_eq!(f.image_planes().len(), 1); + assert_eq!(f.image_planes()[0].stride, 4); + } + + #[test] + fn set_palette_round_trips_and_keeps_image_planes_intact() { + let mut f = gray_frame(); + let pal = full_palette(); + f.set_palette(pal.clone()); + + assert_eq!(f.palette(), Some(pal.as_slice())); + // Image-plane view is unchanged by the side-channel. + assert_eq!(f.image_plane_count(), 1); + assert_eq!(f.image_planes()[0].data.len(), 8); + // The raw field sees the sentinel entry at the tail. + assert_eq!(f.planes.len(), 2); + assert_eq!(f.planes[1].stride, 0); + + // Entry lookup: entry i is (i, !i, i ^ 0x55) by construction. + assert_eq!(f.palette_rgb(0), Some([0x00, 0xFF, 0x55])); + assert_eq!(f.palette_rgb(0xAB), Some([0xAB, 0x54, 0xFE])); + assert_eq!(f.palette_rgb(255), Some([0xFF, 0x00, 0xAA])); + } + + #[test] + fn set_palette_replaces_existing_table() { + let mut f = gray_frame(); + f.set_palette(vec![1, 2, 3]); + f.set_palette(vec![9, 8, 7, 6, 5, 4]); + // Replacement, not stacking: one image plane + one sentinel. + assert_eq!(f.planes.len(), 2); + assert_eq!(f.palette(), Some(&[9, 8, 7, 6, 5, 4][..])); + assert_eq!(f.palette_rgb(1), Some([6, 5, 4])); + } + + #[test] + fn short_palette_covers_only_its_entries() { + let f = gray_frame().with_palette(vec![10, 20, 30, 40, 50, 60]); + assert_eq!(f.palette_rgb(0), Some([10, 20, 30])); + assert_eq!(f.palette_rgb(1), Some([40, 50, 60])); + // Beyond the table: undefined by the frame → None. + assert_eq!(f.palette_rgb(2), None); + assert_eq!(f.palette_rgb(255), None); + } + + #[test] + fn empty_palette_clears_and_take_palette_detaches() { + let mut f = gray_frame(); + f.set_palette(vec![1, 2, 3]); + assert!(f.palette().is_some()); + + // Empty input removes the side-channel entirely. + f.set_palette(Vec::new()); + assert_eq!(f.palette(), None); + assert_eq!(f.planes.len(), 1); + + // take_palette detaches and returns the bytes. + f.set_palette(vec![4, 5, 6]); + assert_eq!(f.take_palette(), Some(vec![4, 5, 6])); + assert_eq!(f.palette(), None); + assert_eq!(f.take_palette(), None); + assert_eq!(f.planes.len(), 1); + } + + #[test] + fn zero_stride_empty_plane_is_not_mistaken_for_a_palette() { + // stride == 0 with EMPTY data is the degenerate (but + // contract-consistent) empty image plane, not the sentinel. + let f = VideoFrame { + pts: None, + planes: vec![ + VideoPlane { + stride: 4, + data: vec![0u8; 8], + }, + VideoPlane { + stride: 0, + data: Vec::new(), + }, + ], + }; + assert_eq!(f.palette(), None); + assert_eq!(f.image_plane_count(), 2); + } + + #[test] + fn palette_on_frame_without_image_planes() { + // A palette can be attached before pixel planes exist (encoder + // scaffolding); the image-plane view is then empty. + let f = VideoFrame { + pts: None, + planes: Vec::new(), + } + .with_palette(vec![1, 2, 3]); + assert_eq!(f.palette(), Some(&[1, 2, 3][..])); + assert_eq!(f.image_plane_count(), 0); + assert!(f.image_planes().is_empty()); + } + + #[test] + fn frame_without_significant_bits_reports_none() { + let f = gray_frame(); + assert_eq!(f.significant_bits(), None); + assert_eq!(f.plane_significant_bits(0), None); + assert_eq!(f.image_plane_count(), 1); + } + + #[test] + fn set_significant_bits_round_trips_and_keeps_image_planes_intact() { + // A 12-bit-luma / 10-bit-chroma mixed-depth frame (the VC-2 + // custom-signal-range shape that motivated the record). + let mut f = VideoFrame { + pts: Some(3), + planes: vec![ + VideoPlane { + stride: 8, + data: vec![0u8; 16], + }, + VideoPlane { + stride: 8, + data: vec![0u8; 16], + }, + VideoPlane { + stride: 8, + data: vec![0u8; 16], + }, + ], + }; + f.set_significant_bits(vec![12, 10, 10]); + + assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..])); + assert_eq!(f.plane_significant_bits(0), Some(12)); + assert_eq!(f.plane_significant_bits(1), Some(10)); + assert_eq!(f.plane_significant_bits(2), Some(10)); + // Beyond the record: fall back to the format default → None. + assert_eq!(f.plane_significant_bits(3), None); + + // Image-plane view is unchanged by the side-channel. + assert_eq!(f.image_plane_count(), 3); + assert_eq!(f.image_planes().len(), 3); + // The raw field sees the sentinel entry at the tail. + assert_eq!(f.planes.len(), 4); + assert_eq!(f.planes[3].stride, usize::MAX); + } + + #[test] + fn set_significant_bits_replaces_and_empty_clears_and_take_detaches() { + let mut f = gray_frame(); + f.set_significant_bits(vec![7]); + f.set_significant_bits(vec![6]); + // Replacement, not stacking. + assert_eq!(f.planes.len(), 2); + assert_eq!(f.significant_bits(), Some(&[6][..])); + + // Empty input removes the side-channel entirely. + f.set_significant_bits(Vec::new()); + assert_eq!(f.significant_bits(), None); + assert_eq!(f.planes.len(), 1); + + // take_significant_bits detaches and returns the bytes. + f.set_significant_bits(vec![5]); + assert_eq!(f.take_significant_bits(), Some(vec![5])); + assert_eq!(f.significant_bits(), None); + assert_eq!(f.take_significant_bits(), None); + assert_eq!(f.planes.len(), 1); + } + + #[test] + fn palette_and_significant_bits_compose_in_either_order() { + // Palette first, then depths. + let mut f = gray_frame() + .with_palette(vec![1, 2, 3]) + .with_significant_bits(vec![8]); + assert_eq!(f.palette(), Some(&[1, 2, 3][..])); + assert_eq!(f.significant_bits(), Some(&[8][..])); + assert_eq!(f.image_plane_count(), 1); + assert_eq!(f.planes.len(), 3); + + // Replacing one record must not disturb the other, regardless + // of which currently sits at the tail. + f.set_palette(vec![9, 8, 7]); + assert_eq!(f.palette(), Some(&[9, 8, 7][..])); + assert_eq!(f.significant_bits(), Some(&[8][..])); + f.set_significant_bits(vec![7]); + assert_eq!(f.palette(), Some(&[9, 8, 7][..])); + assert_eq!(f.significant_bits(), Some(&[7][..])); + assert_eq!(f.image_plane_count(), 1); + + // Depths first, then palette. + let g = gray_frame() + .with_significant_bits(vec![4]) + .with_palette(full_palette()); + assert_eq!(g.significant_bits(), Some(&[4][..])); + assert_eq!(g.palette_rgb(0), Some([0x00, 0xFF, 0x55])); + assert_eq!(g.image_plane_count(), 1); + + // Detaching one leaves the other attached. + let mut h = g; + assert_eq!(h.take_significant_bits(), Some(vec![4])); + assert_eq!(h.significant_bits(), None); + assert_eq!(h.palette().map(<[u8]>::len), Some(768)); + assert_eq!(h.take_palette().map(|p| p.len()), Some(768)); + assert_eq!(h.planes.len(), 1); + assert_eq!(h.image_plane_count(), 1); + } + + #[test] + fn max_stride_empty_plane_is_not_mistaken_for_significant_bits() { + // stride == usize::MAX with EMPTY data is not the sentinel + // (mirroring the palette rule: sentinels require non-empty + // data). Degenerate, but must not be misread as a record. + let f = VideoFrame { + pts: None, + planes: vec![ + VideoPlane { + stride: 4, + data: vec![0u8; 8], + }, + VideoPlane { + stride: usize::MAX, + data: Vec::new(), + }, + ], + }; + assert_eq!(f.significant_bits(), None); + assert_eq!(f.image_plane_count(), 2); + } + + #[test] + fn significant_bits_on_frame_without_image_planes() { + // Like the palette, the record can be attached before pixel + // planes exist (encoder scaffolding). + let f = VideoFrame { + pts: None, + planes: Vec::new(), + } + .with_significant_bits(vec![12, 10, 10]); + assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..])); + assert_eq!(f.image_plane_count(), 0); + assert!(f.image_planes().is_empty()); + } + + #[test] + fn side_channels_survive_clone_and_frame_wrapping() { + let f = gray_frame() + .with_palette(vec![1, 2, 3]) + .with_significant_bits(vec![6]); + let cloned = f.clone(); + assert_eq!(cloned.palette(), f.palette()); + assert_eq!(cloned.significant_bits(), f.significant_bits()); + + let wrapped = Frame::Video(cloned); + assert_eq!(wrapped.pts(), Some(7)); + if let Frame::Video(v) = wrapped { + assert_eq!(v.palette(), Some(&[1, 2, 3][..])); + assert_eq!(v.significant_bits(), Some(&[6][..])); + } else { + unreachable!("wrapped as Video above"); + } + } + + #[test] + fn palette_survives_clone_and_frame_wrapping() { + let f = gray_frame().with_palette(full_palette()); + let cloned = f.clone(); + assert_eq!(cloned.palette(), f.palette()); + + // Through the Frame enum, pts and palette both survive. + let wrapped = Frame::Video(cloned); + assert_eq!(wrapped.pts(), Some(7)); + if let Frame::Video(v) = wrapped { + assert_eq!(v.palette().map(<[u8]>::len), Some(768)); + } else { + unreachable!("wrapped as Video above"); + } + } +} diff --git a/crates/vendor/oxideav-core/src/lib.rs b/crates/vendor/oxideav-core/src/lib.rs new file mode 100644 index 00000000..478f1424 --- /dev/null +++ b/crates/vendor/oxideav-core/src/lib.rs @@ -0,0 +1,67 @@ +//! Core types and registries for the oxideav framework. +//! +//! This crate is the dependency-light foundation: primitive types +//! (timestamps, packets, frames, media formats) plus the registries +//! every sibling crate registers itself into. The aggregate +//! [`RuntimeContext`] bundles all four registries (codec / container / +//! source / filter) into a single value that consumers pass around. + +#![warn(missing_docs)] + +pub mod arena; +pub mod bits; +pub mod capabilities; +pub mod engine; +pub mod error; +pub mod execution; +pub mod filter; +pub mod format; +pub mod frame; +pub mod limits; +pub mod metadata; +pub mod options; +pub mod packet; +pub mod picture; +pub mod rational; +pub mod registry; +pub mod stream; +pub mod subtitle; +pub mod time; +pub mod vector; + +pub use capabilities::{CodecCapabilities, DEFAULT_PRIORITY}; +pub use engine::{EngineProbeFn, HwCodecCaps, HwDeviceInfo}; +pub use error::{Error, Result}; +pub use execution::ExecutionContext; +pub use filter::{FilterContext, PortParams, PortSpec, StreamFilter}; +pub use format::{ + ChannelLayout, ChannelPosition, MediaType, ParseChannelLayoutError, PixelFormat, SampleFormat, +}; +pub use frame::{AudioFrame, Frame, VideoFrame, VideoPlane}; +pub use limits::DecoderLimits; +pub use metadata::{Attachment, Chapter}; +pub use options::{ + parse_options, CodecOptions, CodecOptionsStruct, OptionField, OptionKind, OptionValue, +}; +pub use packet::Packet; +pub use picture::{AttachedPicture, PictureType}; +pub use rational::Rational; +pub use registry::{ + BytesSource, CodecImplementation, CodecInfo, CodecRegistry, ContainerProbeFn, + ContainerRegistry, Decoder, DecoderFactory, Demuxer, Encoder, EncoderFactory, FilterFactory, + FilterRegistry, FrameSource, MultiTitleSource, Muxer, OpenBytesFn, OpenDemuxerFn, OpenFramesFn, + OpenMultiTitleFn, OpenMuxerFn, OpenPacketsFn, PacketSource, ProbeData, ProbeScore, ReadSeek, + RuntimeContext, SourceOutput, SourceRegistry, WriteSeek, MAX_PROBE_SCORE, + PROBE_SCORE_EXTENSION, +}; +pub use stream::{ + CodecId, CodecParameters, CodecResolver, CodecTag, Confidence, NullCodecResolver, ProbeContext, + ProbeFn, StreamInfo, +}; +pub use subtitle::{CuePosition, Segment, SubtitleCue, SubtitleStyle, TextAlign}; +pub use time::{rescale, rescale_checked, rescale_rnd, Rounding, TimeBase, Timestamp}; +pub use vector::{ + DashPattern, FillRule, GradientStop, Group, ImageRef, LineCap, LineJoin, LinearGradient, + MaskKind, Node, Paint, Path, PathCommand, PathNode, Point, RadialGradient, Rect, Rgba, + SpreadMethod, Stroke, Transform2D, VectorFrame, ViewBox, +}; diff --git a/crates/vendor/oxideav-core/src/limits.rs b/crates/vendor/oxideav-core/src/limits.rs new file mode 100644 index 00000000..6988ccf8 --- /dev/null +++ b/crates/vendor/oxideav-core/src/limits.rs @@ -0,0 +1,183 @@ +//! Decoder DoS-protection limits. +//! +//! [`DecoderLimits`] is a small `Copy + Default` configuration struct +//! threaded through [`CodecParameters`](crate::CodecParameters) so every +//! decoder constructed from a stream sees the same caps. Each cap is a +//! conservative default chosen to be generous enough that no real-world +//! file trips it but tight enough that a malicious input (huge declared +//! dimensions in a tiny container, decompression bombs, etc.) returns +//! [`Error::ResourceExhausted`](crate::Error::ResourceExhausted) instead +//! of OOM-ing the process. +//! +//! Two layers consume these caps: +//! +//! 1. **Header-parse layer.** Every decoder, immediately after parsing +//! a stream/sequence header that declares dimensions, channel/group +//! counts, or sample-rate × duration products, must check those +//! declared values against [`DecoderLimits::max_pixels_per_frame`] / +//! [`DecoderLimits::max_decoded_audio_seconds_per_packet`] *before* +//! any allocation. A 1 GiB declared frame in a 4 KiB file should +//! error here without ever calling `Vec::with_capacity`. +//! +//! 2. **Arena layer.** [`ArenaPool`](crate::arena::ArenaPool) honours +//! [`DecoderLimits::max_arenas_in_flight`] (pool size) and +//! [`DecoderLimits::max_alloc_bytes_per_frame`] (arena capacity). +//! [`DecoderLimits::max_alloc_count_per_frame`] catches small-alloc +//! DoS where each individual allocation is tiny but the count grows +//! unbounded (e.g. one alloc per macroblock × millions of macroblocks). +//! +//! The struct is `Copy` so threading it through call chains never +//! involves clones or refcounts. It is also `#[non_exhaustive]` so +//! additional caps can be added without a semver break — construct +//! defaults with [`DecoderLimits::default`] and use the builder methods +//! to tighten individual fields. + +/// Caps that bound a single decoder's peak resource use. +/// +/// Defaults are intentionally **generous** (32 k × 32 k pixels, 1 GiB +/// per arena, 60 s of decoded audio per packet, …) so existing +/// real-world media decodes unchanged. Callers wanting tighter bounds +/// (e.g. a server processing untrusted uploads) should construct +/// `DecoderLimits` explicitly with the builder methods. +/// +/// `Copy` and `Default` so the struct travels through hot paths +/// without indirection. `#[non_exhaustive]` so future caps can be +/// added without breaking semver — use [`DecoderLimits::default`] and +/// the `with_*` builder methods rather than struct-literal syntax. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DecoderLimits { + /// Hard cap on `width × height` for a single decoded video frame. + /// Header-parse code computes this product (using `u64` to avoid + /// `u32::MAX × u32::MAX` overflow) and compares against this cap + /// before allocating any plane. Default: `32_768 × 32_768` = + /// `1_073_741_824` pixels (4 GiB at 32-bpp / 1 GiB at 8-bpp). + pub max_pixels_per_frame: u64, + + /// Hard cap on the total bytes any single decoded frame may + /// consume across all of its plane allocations. Also defines the + /// per-arena capacity — see + /// [`crate::arena::ArenaPool::new`]. Default: `1 GiB`. Tighter + /// than `max_pixels_per_frame × bytes_per_pixel` for catching + /// pathological pixel formats (e.g. a 16-bit-per-channel RGBA + /// surface at near-cap dimensions). + pub max_alloc_bytes_per_frame: u64, + + /// Hard cap on the *count* of allocations performed inside a + /// single arena, regardless of total bytes. Catches small-alloc + /// DoS (e.g. one alloc per macroblock × millions of macroblocks + /// where the bytes-per-frame check would be too loose to fire). + /// Default: `1_000_000` allocations. + pub max_alloc_count_per_frame: u32, + + /// Hard cap on how many arenas a single decoder may have in + /// flight at once — i.e. the size of the per-decoder + /// [`ArenaPool`](crate::arena::ArenaPool). When all arenas are + /// checked out the next `lease()` returns + /// [`Error::ResourceExhausted`](crate::Error::ResourceExhausted), + /// providing automatic backpressure: a slow downstream consumer + /// stalls the decoder rather than letting it grow memory + /// unboundedly. Default: `8` arenas. + pub max_arenas_in_flight: u8, + + /// Audio-only cap on the wall-clock duration (in seconds) of + /// decoded samples a single packet may produce. Header-parse + /// code computes `(samples_per_frame × frames_per_packet) / + /// sample_rate` and rejects packets whose declared output + /// exceeds this. Default: `60` seconds — far more than any + /// real-world AAC/Opus/etc. packet would ever produce, but + /// finite enough to refuse a malformed packet that claims + /// hours of output. + pub max_decoded_audio_seconds_per_packet: u32, +} + +impl Default for DecoderLimits { + fn default() -> Self { + Self { + max_pixels_per_frame: 32_768u64 * 32_768u64, + max_alloc_bytes_per_frame: 1u64 << 30, // 1 GiB + max_alloc_count_per_frame: 1_000_000, + max_arenas_in_flight: 8, + max_decoded_audio_seconds_per_packet: 60, + } + } +} + +impl DecoderLimits { + /// Tighten the per-frame pixel cap. See + /// [`DecoderLimits::max_pixels_per_frame`]. + pub fn with_max_pixels_per_frame(mut self, n: u64) -> Self { + self.max_pixels_per_frame = n; + self + } + + /// Tighten the per-frame allocation byte cap (also defines arena + /// capacity). See [`DecoderLimits::max_alloc_bytes_per_frame`]. + pub fn with_max_alloc_bytes_per_frame(mut self, n: u64) -> Self { + self.max_alloc_bytes_per_frame = n; + self + } + + /// Tighten the per-frame allocation count cap. See + /// [`DecoderLimits::max_alloc_count_per_frame`]. + pub fn with_max_alloc_count_per_frame(mut self, n: u32) -> Self { + self.max_alloc_count_per_frame = n; + self + } + + /// Tighten the per-decoder pool size. See + /// [`DecoderLimits::max_arenas_in_flight`]. + pub fn with_max_arenas_in_flight(mut self, n: u8) -> Self { + self.max_arenas_in_flight = n; + self + } + + /// Tighten the per-packet decoded-audio duration cap. See + /// [`DecoderLimits::max_decoded_audio_seconds_per_packet`]. + pub fn with_max_decoded_audio_seconds_per_packet(mut self, n: u32) -> Self { + self.max_decoded_audio_seconds_per_packet = n; + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_are_conservative_but_finite() { + let l = DecoderLimits::default(); + // 32k x 32k pixels. + assert_eq!(l.max_pixels_per_frame, 1_073_741_824); + // 1 GiB per arena. + assert_eq!(l.max_alloc_bytes_per_frame, 1u64 << 30); + // 1M allocations per frame. + assert_eq!(l.max_alloc_count_per_frame, 1_000_000); + // 8 arenas in flight. + assert_eq!(l.max_arenas_in_flight, 8); + // 60 s of decoded audio per packet. + assert_eq!(l.max_decoded_audio_seconds_per_packet, 60); + } + + #[test] + fn builder_methods_compose() { + let l = DecoderLimits::default() + .with_max_pixels_per_frame(1024 * 1024) + .with_max_alloc_bytes_per_frame(8 * 1024 * 1024) + .with_max_alloc_count_per_frame(1024) + .with_max_arenas_in_flight(2) + .with_max_decoded_audio_seconds_per_packet(1); + assert_eq!(l.max_pixels_per_frame, 1024 * 1024); + assert_eq!(l.max_alloc_bytes_per_frame, 8 * 1024 * 1024); + assert_eq!(l.max_alloc_count_per_frame, 1024); + assert_eq!(l.max_arenas_in_flight, 2); + assert_eq!(l.max_decoded_audio_seconds_per_packet, 1); + } + + #[test] + fn copy_semantics() { + let a = DecoderLimits::default(); + let b = a; // would not compile if not Copy + assert_eq!(a, b); + } +} diff --git a/crates/vendor/oxideav-core/src/metadata.rs b/crates/vendor/oxideav-core/src/metadata.rs new file mode 100644 index 00000000..22ac9e4e --- /dev/null +++ b/crates/vendor/oxideav-core/src/metadata.rs @@ -0,0 +1,148 @@ +//! Structured container metadata. +//! +//! Today most demuxers expose chapters and attachments as flat +//! [`Demuxer::metadata`](crate::Demuxer::metadata) entries — strings +//! like `chapter:0:start_ms` / `attachment:2:filename`. Those keep +//! working, but consumers that want to iterate chapters or pull an +//! attachment's payload should use the structured +//! [`Demuxer::chapters`](crate::Demuxer::chapters) and +//! [`Demuxer::attachments`](crate::Demuxer::attachments) accessors +//! instead. Both default to an empty slice on the trait, so demuxers +//! that don't carry such data — and demuxers that haven't been ported +//! to the structured API yet — keep compiling unchanged. +//! +//! The two structs are deliberately container-agnostic. They cover the +//! intersection of MKV (`Chapters` / `Attachments` master elements), +//! MP4 (chapter track + `iTunSMPB`-style chapter atoms; `meta`/`covr` +//! adjacent for attachments), Ogg (`CHAPTERnn=…` Vorbis comments), +//! and DVD/Blu-ray IFO chapter tables. + +use crate::time::Timestamp; + +/// One chapter / cue point inside a container. +/// +/// Containers that only carry a start time (Vorbis-comment chapters, +/// DVD IFO PGCs without explicit end times) set `end == start`. The +/// `id` field is whatever the container uses internally — MKV's +/// `ChapterUID`, MP4 chapter track sample index, or a synthesised +/// counter for formats without a stable ID. +#[derive(Clone, Debug, PartialEq)] +pub struct Chapter { + /// Container-native chapter identifier. Stable across demuxer + /// re-opens of the same file but **not** comparable across + /// different containers. + pub id: u64, + /// Chapter start time. The [`Timestamp`]'s time base is whatever + /// the demuxer reports; consumers should + /// [`rescale`](Timestamp::rescale) to a common base before + /// comparing chapters from different sources. + pub start: Timestamp, + /// Chapter end time. Equal to `start` when the container does not + /// store an explicit end (the next chapter's start is the + /// implicit end in that case). + pub end: Timestamp, + /// Display title in the chapter's primary language, if present. + pub title: Option, + /// BCP-47 / ISO 639 language tag for the title (`"en"`, `"jpn"`, + /// …) when the container labels it. `None` means "unspecified" — + /// not "neutral". + pub language: Option, +} + +/// One file-shaped payload attached to a container. +/// +/// Distinct from [`AttachedPicture`](crate::AttachedPicture): an +/// `Attachment` is an arbitrary byte blob with a filename — fonts +/// (MKV `application/x-truetype-font`), thumbnail strips, subtitle +/// fragments, README text — whereas `AttachedPicture` is the +/// ID3v2/FLAC/MP4 cover-art pathway that carries a typed +/// [`PictureType`](crate::PictureType). MKV `Attachments` map +/// cleanly onto this struct; MP4 `meta` boxes and Ogg +/// `METADATA_BLOCK_PICTURE` are emitted as +/// [`attached_pictures`](crate::Demuxer::attached_pictures) instead. +#[derive(Clone, Debug, PartialEq)] +pub struct Attachment { + /// Original filename as stored in the container (no path + /// stripping, no normalisation). Containers that don't track a + /// name still populate this — synthesise something stable like + /// `attachment_.bin` so callers always have a routing handle. + pub name: String, + /// IANA media type (`"image/png"`, `"application/x-truetype-font"`, + /// …) when the container declares one. `None` means "unspecified" + /// — callers are free to sniff the bytes. + pub mime: Option, + /// Free-form description supplied by the tagger. + pub description: Option, + /// Raw attachment bytes exactly as stored in the container. + pub data: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::time::TimeBase; + + #[test] + fn chapter_clone_eq_round_trip() { + let base = TimeBase::new(1, 1000); + let c = Chapter { + id: 42, + start: Timestamp::new(0, base), + end: Timestamp::new(5_000, base), + title: Some("Intro".into()), + language: Some("en".into()), + }; + let c2 = c.clone(); + assert_eq!(c, c2); + assert_eq!(c.id, 42); + assert_eq!(c.start.value, 0); + assert_eq!(c.end.value, 5_000); + assert_eq!(c.title.as_deref(), Some("Intro")); + assert_eq!(c.language.as_deref(), Some("en")); + } + + #[test] + fn chapter_optional_fields_default_to_none() { + let base = TimeBase::new(1, 1); + let c = Chapter { + id: 1, + start: Timestamp::new(0, base), + end: Timestamp::new(0, base), + title: None, + language: None, + }; + assert!(c.title.is_none()); + assert!(c.language.is_none()); + // Containers without an explicit end time set end == start. + assert_eq!(c.start, c.end); + } + + #[test] + fn attachment_clone_eq_round_trip() { + let a = Attachment { + name: "cover.png".into(), + mime: Some("image/png".into()), + description: Some("Album front".into()), + data: vec![0x89, b'P', b'N', b'G'], + }; + let a2 = a.clone(); + assert_eq!(a, a2); + assert_eq!(a.name, "cover.png"); + assert_eq!(a.mime.as_deref(), Some("image/png")); + assert_eq!(a.description.as_deref(), Some("Album front")); + assert_eq!(a.data, vec![0x89, b'P', b'N', b'G']); + } + + #[test] + fn attachment_optional_fields_default_to_none() { + let a = Attachment { + name: "blob.bin".into(), + mime: None, + description: None, + data: Vec::new(), + }; + assert!(a.mime.is_none()); + assert!(a.description.is_none()); + assert!(a.data.is_empty()); + } +} diff --git a/crates/vendor/oxideav-core/src/options.rs b/crates/vendor/oxideav-core/src/options.rs new file mode 100644 index 00000000..60d5f04e --- /dev/null +++ b/crates/vendor/oxideav-core/src/options.rs @@ -0,0 +1,480 @@ +//! Generic, schema-validated option bag for codec (and container) init. +//! +//! The over-the-wire form is an untyped string→string bag +//! ([`CodecOptions`]). Each codec defines a typed struct implementing +//! [`CodecOptionsStruct`], which declares a static [`OptionField`] +//! schema and an [`apply`](CodecOptionsStruct::apply) method that +//! writes one coerced value into the struct. [`parse_options`] drives +//! the whole thing: it walks the bag, looks up every key in the +//! schema, coerces the string to the declared [`OptionKind`], and +//! hands the resulting [`OptionValue`] to `apply`. +//! +//! Strict at init: unknown keys and malformed values return +//! [`Error::InvalidData`]. Consumers that want "ignore unknown keys" +//! should pre-filter the bag before calling [`parse_options`]. +//! +//! All parsing happens once, at encoder/decoder construction — the +//! hot path never touches this module. +//! +//! Consumers have two entry points: +//! - **Dynamic / JSON** — build a [`CodecOptions`] via `.set(k, v)` or +//! [`CodecOptions::from_json`] (feature `json-options`) and attach +//! it to `CodecParameters::options`. +//! - **Typed** — skip the bag entirely: build the codec's options +//! struct directly and pass it to a codec-specific typed entry point +//! (e.g. `encode_single_with_options`). The bag only exists for +//! consumers who can't know the typed struct at compile time. + +use crate::error::{Error, Result}; + +/// Untyped string → string bag. The over-the-wire shape of options +/// as they travel from the caller (CLI / pipeline JSON / FFI) to a +/// codec factory. +/// +/// Insertion order is preserved and [`iter`](Self::iter) walks keys in +/// the order they were set. Duplicate keys overwrite (last writer +/// wins). +#[derive(Debug, Clone, Default)] +pub struct CodecOptions { + entries: Vec<(String, String)>, +} + +impl CodecOptions { + /// Create an empty option bag (same as `CodecOptions::default()`). + pub fn new() -> Self { + Self::default() + } + + /// Builder-style setter, useful for one-liners. + /// `CodecOptions::new().set("interlace", "true")`. + pub fn set(mut self, k: impl Into, v: impl Into) -> Self { + self.insert(k, v); + self + } + + /// Mutating insert. Overwrites any existing entry with the same + /// key. + pub fn insert(&mut self, k: impl Into, v: impl Into) { + let k = k.into(); + let v = v.into(); + if let Some(existing) = self.entries.iter_mut().find(|(kk, _)| kk == &k) { + existing.1 = v; + } else { + self.entries.push((k, v)); + } + } + + /// Look up the value for key `k`, or `None` if the key was never + /// set. + pub fn get(&self, k: &str) -> Option<&str> { + self.entries + .iter() + .find(|(kk, _)| kk == k) + .map(|(_, v)| v.as_str()) + } + + /// `true` when the bag contains no entries. + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Number of entries in the bag. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Iterate over `(key, value)` pairs in insertion order. + pub fn iter(&self) -> impl Iterator { + self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str())) + } + + /// Build a bag from a JSON object. Scalar values (bool / number / + /// string) are stringified into the bag; arrays and nested objects + /// are rejected — keys with structured values don't map into the + /// flat string bag. + pub fn from_json(s: &str) -> Result { + let v: serde_json::Value = + serde_json::from_str(s).map_err(|e| Error::invalid(format!("options json: {e}")))?; + Self::from_json_value(&v) + } + + /// As [`from_json`](Self::from_json) but takes a pre-parsed value + /// (the shape pipelines already use — `TrackSpec.codec_params`). + pub fn from_json_value(v: &serde_json::Value) -> Result { + use serde_json::Value; + let obj = match v { + Value::Null => return Ok(Self::default()), + Value::Object(m) => m, + other => { + return Err(Error::invalid(format!( + "options json: expected object, got {}", + json_type_name(other) + ))) + } + }; + let mut out = Self::default(); + for (k, val) in obj { + let s = match val { + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::String(s) => s.clone(), + Value::Null => continue, // null = "leave default" + other => { + return Err(Error::invalid(format!( + "option '{k}': structured values ({}) are not supported", + json_type_name(other) + ))) + } + }; + out.insert(k.clone(), s); + } + Ok(out) + } +} + +fn json_type_name(v: &serde_json::Value) -> &'static str { + use serde_json::Value; + match v { + Value::Null => "null", + Value::Bool(_) => "bool", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Array(_) => "array", + Value::Object(_) => "object", + } +} + +/// Declared type of a single option. Used at parse time to coerce a +/// raw string (or JSON scalar) into a typed [`OptionValue`] and to +/// reject malformed values up front. +#[derive(Clone, Copy, Debug)] +pub enum OptionKind { + /// Boolean; accepts `true`/`1`/`yes`/`on` and `false`/`0`/`no`/`off`. + Bool, + /// Unsigned 32-bit integer. + U32, + /// Signed 32-bit integer. + I32, + /// 32-bit floating point. + F32, + /// Free-form string; any value is accepted verbatim. + String, + /// Enumeration: the only accepted values are the strings in this + /// slice. Matching is case-sensitive. + Enum(&'static [&'static str]), +} + +/// Coerced value handed to a codec's `apply` method. Codec code +/// chooses the appropriate `as_*` accessor based on the field name. +#[derive(Clone, Debug)] +pub enum OptionValue { + /// A coerced boolean value. + Bool(bool), + /// A coerced unsigned 32-bit integer. + U32(u32), + /// A coerced signed 32-bit integer. + I32(i32), + /// A coerced 32-bit float. + F32(f32), + /// A string value (also used for [`OptionKind::Enum`] matches). + String(String), +} + +impl OptionValue { + /// Extract the boolean, or [`Error::InvalidData`] when the value is + /// a different kind. + pub fn as_bool(&self) -> Result { + match self { + OptionValue::Bool(b) => Ok(*b), + other => Err(Error::invalid(format!("expected bool, got {other:?}"))), + } + } + /// Extract the `u32`, or [`Error::InvalidData`] when the value is a + /// different kind. + pub fn as_u32(&self) -> Result { + match self { + OptionValue::U32(n) => Ok(*n), + other => Err(Error::invalid(format!("expected u32, got {other:?}"))), + } + } + /// Extract the `i32`, or [`Error::InvalidData`] when the value is a + /// different kind. + pub fn as_i32(&self) -> Result { + match self { + OptionValue::I32(n) => Ok(*n), + other => Err(Error::invalid(format!("expected i32, got {other:?}"))), + } + } + /// Extract the `f32`, or [`Error::InvalidData`] when the value is a + /// different kind. + pub fn as_f32(&self) -> Result { + match self { + OptionValue::F32(n) => Ok(*n), + other => Err(Error::invalid(format!("expected f32, got {other:?}"))), + } + } + /// Extract the string (also the shape of `Enum` matches), or + /// [`Error::InvalidData`] when the value is a different kind. + pub fn as_str(&self) -> Result<&str> { + match self { + OptionValue::String(s) => Ok(s.as_str()), + other => Err(Error::invalid(format!("expected string, got {other:?}"))), + } + } +} + +/// Schema entry describing one recognised option. Codec crates declare +/// a `&'static [OptionField]` listing every key their options struct +/// consumes. +#[derive(Debug)] +pub struct OptionField { + /// Option key as it appears in the [`CodecOptions`] bag. + pub name: &'static str, + /// Declared type used to coerce and validate the raw string value. + pub kind: OptionKind, + /// Value used when the key is absent from the bag (documentation / + /// introspection — the actual default lives in the struct's + /// `Default` impl). + pub default: OptionValue, + /// One-line human-readable description for `--help`-style listings. + pub help: &'static str, +} + +/// Trait implemented by each codec's typed options struct. +/// +/// Typical hand-written implementation: +/// +/// ```ignore +/// impl CodecOptionsStruct for PngEncoderOptions { +/// const SCHEMA: &'static [OptionField] = &[ +/// OptionField { +/// name: "interlace", +/// kind: OptionKind::Bool, +/// default: OptionValue::Bool(false), +/// help: "Adam7 interlaced encode", +/// }, +/// ]; +/// fn apply(&mut self, key: &str, v: &OptionValue) -> Result<()> { +/// match key { +/// "interlace" => self.interlace = v.as_bool()?, +/// _ => unreachable!("guarded by SCHEMA"), +/// } +/// Ok(()) +/// } +/// } +/// ``` +pub trait CodecOptionsStruct: Default + 'static { + /// Static schema listing every option key this struct consumes, + /// with its declared kind, default, and help text. + const SCHEMA: &'static [OptionField]; + /// Write one coerced value into the struct. `key` is guaranteed to + /// be present in [`SCHEMA`](Self::SCHEMA) and `value` to match the + /// declared [`OptionKind`] when called via [`parse_options`]. + fn apply(&mut self, key: &str, value: &OptionValue) -> Result<()>; +} + +/// Parse a [`CodecOptions`] bag into a typed options struct. +/// +/// Strict: unknown keys return [`Error::InvalidData`]; malformed values +/// do the same. The returned struct is seeded from +/// `T::default()` — any key not set in the bag keeps the struct's +/// default value. +pub fn parse_options(opts: &CodecOptions) -> Result { + let mut out = T::default(); + for (k, v_str) in opts.iter() { + let field = T::SCHEMA + .iter() + .find(|f| f.name == k) + .ok_or_else(|| Error::invalid(format!("unknown option '{k}'")))?; + let v = coerce(k, field.kind, v_str)?; + out.apply(k, &v)?; + } + Ok(out) +} + +/// Shorthand: parse straight from a JSON-object source. +pub fn parse_options_json(s: &str) -> Result { + parse_options::(&CodecOptions::from_json(s)?) +} + +fn coerce(name: &str, kind: OptionKind, raw: &str) -> Result { + match kind { + OptionKind::Bool => match raw { + "true" | "1" | "yes" | "on" => Ok(OptionValue::Bool(true)), + "false" | "0" | "no" | "off" => Ok(OptionValue::Bool(false)), + other => Err(Error::invalid(format!( + "option '{name}' expects bool, got {other:?}" + ))), + }, + OptionKind::U32 => raw + .parse::() + .map(OptionValue::U32) + .map_err(|_| Error::invalid(format!("option '{name}' expects u32, got {raw:?}"))), + OptionKind::I32 => raw + .parse::() + .map(OptionValue::I32) + .map_err(|_| Error::invalid(format!("option '{name}' expects i32, got {raw:?}"))), + OptionKind::F32 => raw + .parse::() + .map(OptionValue::F32) + .map_err(|_| Error::invalid(format!("option '{name}' expects f32, got {raw:?}"))), + OptionKind::String => Ok(OptionValue::String(raw.to_owned())), + OptionKind::Enum(allowed) => { + if allowed.contains(&raw) { + Ok(OptionValue::String(raw.to_owned())) + } else { + Err(Error::invalid(format!( + "option '{name}' must be one of {:?}, got {raw:?}", + allowed + ))) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Default, Debug, PartialEq)] + struct Demo { + interlace: bool, + level: u32, + mode: String, + } + + impl CodecOptionsStruct for Demo { + const SCHEMA: &'static [OptionField] = &[ + OptionField { + name: "interlace", + kind: OptionKind::Bool, + default: OptionValue::Bool(false), + help: "", + }, + OptionField { + name: "level", + kind: OptionKind::U32, + default: OptionValue::U32(6), + help: "", + }, + OptionField { + name: "mode", + kind: OptionKind::Enum(&["fast", "slow"]), + default: OptionValue::String(String::new()), + help: "", + }, + ]; + fn apply(&mut self, key: &str, v: &OptionValue) -> Result<()> { + match key { + "interlace" => self.interlace = v.as_bool()?, + "level" => self.level = v.as_u32()?, + "mode" => self.mode = v.as_str()?.to_owned(), + _ => unreachable!("guarded by SCHEMA"), + } + Ok(()) + } + } + + #[test] + fn bag_preserves_order_and_overwrites() { + let opts = CodecOptions::new() + .set("a", "1") + .set("b", "2") + .set("a", "3"); + assert_eq!(opts.get("a"), Some("3")); + let collected: Vec<_> = opts.iter().collect(); + assert_eq!(collected, vec![("a", "3"), ("b", "2")]); + } + + #[test] + fn parse_empty_returns_default() { + let opts = CodecOptions::new(); + let d = parse_options::(&opts).unwrap(); + assert_eq!(d, Demo::default()); + } + + #[test] + fn parse_typed_values() { + let opts = CodecOptions::new() + .set("interlace", "true") + .set("level", "9") + .set("mode", "fast"); + let d = parse_options::(&opts).unwrap(); + assert!(d.interlace); + assert_eq!(d.level, 9); + assert_eq!(d.mode, "fast"); + } + + #[test] + fn parse_rejects_unknown_key() { + let opts = CodecOptions::new().set("nope", "1"); + let err = parse_options::(&opts).unwrap_err(); + assert!(matches!(err, Error::InvalidData(ref s) if s.contains("unknown option 'nope'"))); + } + + #[test] + fn parse_rejects_bad_bool() { + let opts = CodecOptions::new().set("interlace", "maybe"); + let err = parse_options::(&opts).unwrap_err(); + assert!(matches!(err, Error::InvalidData(ref s) if s.contains("expects bool"))); + } + + #[test] + fn parse_rejects_bad_u32() { + let opts = CodecOptions::new().set("level", "-1"); + assert!(parse_options::(&opts).is_err()); + } + + #[test] + fn parse_rejects_enum_miss() { + let opts = CodecOptions::new().set("mode", "medium"); + let err = parse_options::(&opts).unwrap_err(); + assert!(matches!(err, Error::InvalidData(ref s) if s.contains("must be one of"))); + } + + #[test] + fn bool_accepts_common_synonyms() { + for (raw, want) in [ + ("true", true), + ("1", true), + ("yes", true), + ("on", true), + ("false", false), + ("0", false), + ("no", false), + ("off", false), + ] { + let opts = CodecOptions::new().set("interlace", raw); + let d = parse_options::(&opts).unwrap(); + assert_eq!(d.interlace, want, "raw = {raw}"); + } + } + + #[test] + fn from_json_object() { + let bag = + CodecOptions::from_json(r#"{"interlace": true, "level": 9, "mode": "fast"}"#).unwrap(); + let d = parse_options::(&bag).unwrap(); + assert!(d.interlace); + assert_eq!(d.level, 9); + assert_eq!(d.mode, "fast"); + } + + #[test] + fn from_json_null_is_empty() { + let bag = CodecOptions::from_json("null").unwrap(); + assert!(bag.is_empty()); + } + + #[test] + fn from_json_rejects_nested() { + let err = CodecOptions::from_json(r#"{"k": [1, 2]}"#).unwrap_err(); + assert!(matches!(err, Error::InvalidData(ref s) if s.contains("structured"))); + } + + #[test] + fn parse_options_json_shortcut() { + let d = parse_options_json::(r#"{"level": 3}"#).unwrap(); + assert_eq!(d.level, 3); + } +} diff --git a/crates/vendor/oxideav-core/src/packet.rs b/crates/vendor/oxideav-core/src/packet.rs new file mode 100644 index 00000000..50612366 --- /dev/null +++ b/crates/vendor/oxideav-core/src/packet.rs @@ -0,0 +1,275 @@ +//! Compressed-data packet passed between demuxer → decoder and encoder → muxer. + +use crate::time::TimeBase; + +/// Metadata flags on a packet. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct PacketFlags { + /// Packet is (or starts) a keyframe / random-access point. + pub keyframe: bool, + /// Packet holds codec-level headers rather than media data. + pub header: bool, + /// Packet's data may be corrupt but decode should still be attempted. + pub corrupt: bool, + /// Packet should be discarded (e.g., decoder delay padding). + pub discard: bool, + /// Packet is the last in its source container's natural framing unit + /// (Ogg page, MP4 chunk, MKV cluster, …). Container muxers may use this + /// signal to recreate similar boundaries in their output. Decoders + /// should ignore it. + pub unit_boundary: bool, +} + +/// A chunk of compressed (encoded) data belonging to one stream. +#[derive(Clone, Debug)] +pub struct Packet { + /// Stream index this packet belongs to. + pub stream_index: u32, + /// Time base in which `pts` and `dts` are expressed. + pub time_base: TimeBase, + /// Presentation timestamp (display order). `None` if unknown. + pub pts: Option, + /// Decode timestamp (decode order). Often equal to `pts` for intra-only codecs. + pub dts: Option, + /// Packet duration in `time_base` units, or `None` if unknown. + pub duration: Option, + /// Flags describing this packet. + pub flags: PacketFlags, + /// Compressed payload. + pub data: Vec, +} + +impl Packet { + /// Construct a packet with the given payload and no timing + /// information (all timestamps `None`, default flags). + pub fn new(stream_index: u32, time_base: TimeBase, data: Vec) -> Self { + Self { + stream_index, + time_base, + pts: None, + dts: None, + duration: None, + flags: PacketFlags::default(), + data, + } + } + + /// Builder: set the presentation timestamp (in `time_base` units). + pub fn with_pts(mut self, pts: i64) -> Self { + self.pts = Some(pts); + self + } + + /// Builder: set the decode timestamp (in `time_base` units). + pub fn with_dts(mut self, dts: i64) -> Self { + self.dts = Some(dts); + self + } + + /// Builder: set the packet duration (in `time_base` units). + pub fn with_duration(mut self, d: i64) -> Self { + self.duration = Some(d); + self + } + + /// Builder: mark (or unmark) the packet as a keyframe / + /// random-access point. + pub fn with_keyframe(mut self, kf: bool) -> Self { + self.flags.keyframe = kf; + self + } + + /// Mark this packet as carrying codec-level headers rather than + /// media data (extradata, parameter sets, codec-private blobs). + pub fn with_header(mut self, header: bool) -> Self { + self.flags.header = header; + self + } + + /// Mark this packet's payload as possibly corrupt. Decoders should + /// still attempt to decode it but may produce best-effort output. + pub fn with_corrupt(mut self, corrupt: bool) -> Self { + self.flags.corrupt = corrupt; + self + } + + /// Mark this packet for downstream discard (e.g. decoder delay + /// padding, encoder priming samples, ASS dialogue tags shipped only + /// for muxer round-trip). + pub fn with_discard(mut self, discard: bool) -> Self { + self.flags.discard = discard; + self + } + + /// Mark this packet as the last entry inside its source container's + /// natural framing unit (Ogg page, MP4 chunk, MKV cluster). Decoders + /// ignore the flag; muxers may use it to recreate similar + /// boundaries in their output. + pub fn with_unit_boundary(mut self, boundary: bool) -> Self { + self.flags.unit_boundary = boundary; + self + } + + /// Replace this packet's full flag set in one call. Useful for + /// demuxers that compute flags up front and want a single setter + /// rather than four chained builder calls. + pub fn with_flags(mut self, flags: PacketFlags) -> Self { + self.flags = flags; + self + } + + /// Override the packet's stream index. Builder-style chainable + /// counterpart to the public field, for cases where the demuxer + /// builds packets with a placeholder stream index and remaps them + /// to the final index downstream. + pub fn with_stream_index(mut self, stream_index: u32) -> Self { + self.stream_index = stream_index; + self + } + + /// Override the packet's time base. Builder-style chainable + /// counterpart to the public field, for cases where the time base + /// isn't known at construction time (e.g. a remuxer rescaling all + /// packets onto a unified output base). + pub fn with_time_base(mut self, time_base: TimeBase) -> Self { + self.time_base = time_base; + self + } + + /// Compute the packet's end PTS (`pts + duration`) when both are + /// known. Returns `None` if either is missing, or if the sum would + /// overflow `i64`. Useful for muxers that need to derive a per- + /// packet end timestamp without recomputing it at every call site. + pub fn end_pts(&self) -> Option { + self.pts + .zip(self.duration) + .and_then(|(p, d)| p.checked_add(d)) + } + + /// Convenience accessor: `true` when [`PacketFlags::keyframe`] is + /// set. Mirrors the builder pair `with_keyframe(true)`. + pub fn is_keyframe(&self) -> bool { + self.flags.keyframe + } + + /// Convenience accessor: `true` when [`PacketFlags::header`] is set + /// (the packet carries codec-level headers rather than media data). + pub fn is_header(&self) -> bool { + self.flags.header + } + + /// Convenience accessor: `true` when [`PacketFlags::discard`] is + /// set (downstream consumers should drop the packet). + pub fn is_discard(&self) -> bool { + self.flags.discard + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tb() -> TimeBase { + TimeBase::new(1, 1000) + } + + #[test] + fn new_packet_has_default_flags_and_no_timing() { + let p = Packet::new(3, tb(), vec![1, 2, 3]); + assert_eq!(p.stream_index, 3); + assert_eq!(p.time_base, tb()); + assert!(p.pts.is_none()); + assert!(p.dts.is_none()); + assert!(p.duration.is_none()); + assert_eq!(p.flags, PacketFlags::default()); + assert_eq!(p.data, vec![1, 2, 3]); + // All accessor convenience helpers default to false. + assert!(!p.is_keyframe()); + assert!(!p.is_header()); + assert!(!p.is_discard()); + } + + #[test] + fn builder_chain_sets_every_flag_field() { + let p = Packet::new(0, tb(), vec![]) + .with_keyframe(true) + .with_header(true) + .with_corrupt(true) + .with_discard(true) + .with_unit_boundary(true); + assert!(p.flags.keyframe); + assert!(p.flags.header); + assert!(p.flags.corrupt); + assert!(p.flags.discard); + assert!(p.flags.unit_boundary); + assert!(p.is_keyframe()); + assert!(p.is_header()); + assert!(p.is_discard()); + } + + #[test] + fn with_flags_replaces_full_flag_set() { + let flags = PacketFlags { + keyframe: true, + header: false, + corrupt: true, + discard: false, + unit_boundary: true, + }; + let p = Packet::new(0, tb(), vec![]).with_flags(flags); + assert_eq!(p.flags, flags); + // A second with_flags wipes the prior set rather than OR-ing. + let cleared = p.with_flags(PacketFlags::default()); + assert_eq!(cleared.flags, PacketFlags::default()); + } + + #[test] + fn with_stream_index_and_time_base_override() { + let original = TimeBase::new(1, 1); + let replacement = TimeBase::new(1, 90_000); + let p = Packet::new(0, original, vec![]) + .with_stream_index(7) + .with_time_base(replacement); + assert_eq!(p.stream_index, 7); + assert_eq!(p.time_base, replacement); + } + + #[test] + fn end_pts_requires_both_pts_and_duration() { + // Neither set. + assert_eq!(Packet::new(0, tb(), vec![]).end_pts(), None); + // pts only. + assert_eq!(Packet::new(0, tb(), vec![]).with_pts(100).end_pts(), None); + // duration only. + assert_eq!( + Packet::new(0, tb(), vec![]).with_duration(50).end_pts(), + None + ); + // Both: returns pts + duration. + assert_eq!( + Packet::new(0, tb(), vec![]) + .with_pts(100) + .with_duration(50) + .end_pts(), + Some(150) + ); + } + + #[test] + fn end_pts_saturates_on_overflow() { + // pts + duration would overflow i64::MAX; checked_add returns + // None so end_pts surfaces that instead of wrapping. + let p = Packet::new(0, tb(), vec![]) + .with_pts(i64::MAX - 1) + .with_duration(10); + assert_eq!(p.end_pts(), None); + } + + #[test] + fn end_pts_handles_negative_pts() { + // Negative pts is legal (B-frames pre-roll); ensure the sum + // still works through zero. + let p = Packet::new(0, tb(), vec![]).with_pts(-25).with_duration(40); + assert_eq!(p.end_pts(), Some(15)); + } +} diff --git a/crates/vendor/oxideav-core/src/picture.rs b/crates/vendor/oxideav-core/src/picture.rs new file mode 100644 index 00000000..6672b214 --- /dev/null +++ b/crates/vendor/oxideav-core/src/picture.rs @@ -0,0 +1,282 @@ +//! Attached picture metadata (cover art, artist photos, etc.). +//! +//! Used by containers that can carry embedded image data — notably MP3 +//! (via `APIC` / `PIC` frames in an ID3v2 tag), FLAC (via +//! `METADATA_BLOCK_PICTURE`), MP4 (`covr` atoms), and Ogg/Vorbis (base64- +//! encoded `METADATA_BLOCK_PICTURE` inside a Vorbis comment). +//! +//! The picture type values follow the ID3v2 `APIC` spec (which FLAC and +//! Ogg reuse verbatim), so a `PictureType` round-trips cleanly between +//! all four containers. + +/// A single attached picture as it appears in a file's metadata. +/// +/// `data` is the raw encoded image bytes exactly as stored in the file — +/// the container does not decode the image. Callers that want pixels +/// should feed `data` through the appropriate image decoder (JPEG, PNG, +/// ...) using `mime_type` as a routing hint. +#[derive(Clone, Debug)] +pub struct AttachedPicture { + /// MIME type of the image payload (`"image/jpeg"`, `"image/png"`, + /// ...). The special value `"-->"` means `data` is a URL string + /// pointing to an external image rather than inline bytes (ID3v2). + pub mime_type: String, + /// Semantic role of the picture (cover art, artist photo, ...). + pub picture_type: PictureType, + /// Human-readable description supplied by the tagger. Often empty. + pub description: String, + /// Raw image bytes (or URL bytes when `mime_type == "-->"`). + pub data: Vec, +} + +impl AttachedPicture { + /// Construct a new `AttachedPicture` with the given MIME type and + /// picture role. `description` defaults to empty and `data` to an + /// empty `Vec`; chain [`with_description`](Self::with_description) + /// and [`with_data`](Self::with_data) to fill them in. + /// + /// Convenience over the public-field struct literal for producers + /// (ID3v2 / FLAC / MP4 / Vorbis) that build the picture + /// incrementally as bytes scroll past the parser. + pub fn new(mime_type: impl Into, picture_type: PictureType) -> Self { + Self { + mime_type: mime_type.into(), + picture_type, + description: String::new(), + data: Vec::new(), + } + } + + /// Chainable setter for the human-readable description field. + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = description.into(); + self + } + + /// Chainable setter for the raw image bytes (or URL bytes when + /// `mime_type == "-->"`). + pub fn with_data(mut self, data: Vec) -> Self { + self.data = data; + self + } + + /// Replace the picture-type field. Builder counterpart to the + /// public `picture_type` field for callers that initialize with a + /// placeholder role and refine it once the parse position settles. + pub fn with_picture_type(mut self, picture_type: PictureType) -> Self { + self.picture_type = picture_type; + self + } + + /// `true` when `mime_type` carries the ID3v2 sentinel `"-->"`, + /// indicating that `data` is a URL string pointing to an external + /// image rather than inline bytes. Pure sugar — equivalent to + /// `self.mime_type == "-->"` — but expressing the intent at call + /// sites that branch on link-vs-inline semantics. + pub fn is_external_link(&self) -> bool { + self.mime_type == "-->" + } +} + +/// ID3v2 `APIC` picture-type taxonomy (also reused by FLAC and Vorbis). +/// +/// The numeric values match the ID3v2 specification and are stable: +/// callers are free to cast a `PictureType` to `u8`. New values added to +/// future revisions of the spec will land as `Unknown` rather than +/// breaking existing code — the enum is `#[non_exhaustive]`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +#[repr(u8)] +pub enum PictureType { + /// Picture with no more specific role. + Other = 0x00, + /// 32×32 pixel file icon (PNG only, per the ID3v2 spec). + FileIcon32x32 = 0x01, + /// Other file icon (no size restriction). + FileIcon = 0x02, + /// Front cover of the release. + FrontCover = 0x03, + /// Back cover of the release. + BackCover = 0x04, + /// Leaflet page from the release packaging. + LeafletPage = 0x05, + /// Picture of the physical media itself (e.g. disc label). + Media = 0x06, + /// Lead artist / lead performer / soloist. + LeadArtist = 0x07, + /// Artist or performer. + Artist = 0x08, + /// Conductor. + Conductor = 0x09, + /// Band or orchestra. + BandOrchestra = 0x0A, + /// Composer. + Composer = 0x0B, + /// Lyricist or text writer. + Lyricist = 0x0C, + /// Recording location. + RecordingLocation = 0x0D, + /// Photo taken during the recording. + DuringRecording = 0x0E, + /// Photo taken during a performance. + DuringPerformance = 0x0F, + /// Movie or video screen capture. + MovieScreenCapture = 0x10, + /// "A bright coloured fish" — verbatim spec-assigned role. + ABrightColouredFish = 0x11, + /// Illustration. + Illustration = 0x12, + /// Band or artist logotype. + BandLogo = 0x13, + /// Publisher or studio logotype. + PublisherLogo = 0x14, + /// Catch-all for unrecognised or out-of-range codes. + Unknown = 0xFF, +} + +impl PictureType { + /// Convert a raw ID3v2/FLAC picture-type byte into a `PictureType`. + /// Unknown or reserved codes (> 0x14) collapse to `Unknown`. + pub fn from_u8(b: u8) -> Self { + match b { + 0x00 => Self::Other, + 0x01 => Self::FileIcon32x32, + 0x02 => Self::FileIcon, + 0x03 => Self::FrontCover, + 0x04 => Self::BackCover, + 0x05 => Self::LeafletPage, + 0x06 => Self::Media, + 0x07 => Self::LeadArtist, + 0x08 => Self::Artist, + 0x09 => Self::Conductor, + 0x0A => Self::BandOrchestra, + 0x0B => Self::Composer, + 0x0C => Self::Lyricist, + 0x0D => Self::RecordingLocation, + 0x0E => Self::DuringRecording, + 0x0F => Self::DuringPerformance, + 0x10 => Self::MovieScreenCapture, + 0x11 => Self::ABrightColouredFish, + 0x12 => Self::Illustration, + 0x13 => Self::BandLogo, + 0x14 => Self::PublisherLogo, + _ => Self::Unknown, + } + } + + /// Convert this `PictureType` back into its raw ID3v2/FLAC byte — + /// the structural inverse of [`from_u8`](Self::from_u8). Because the + /// enum is `#[repr(u8)]` and every variant has a stable + /// discriminant matching the spec, this is equivalent to a `self as + /// u8` cast; the named method documents the round-trip contract and + /// gives consumer crates (ID3 writers, FLAC tag emitters) a + /// reviewable call site that doesn't paper over the + /// [`Unknown`](Self::Unknown) caveat. + /// + /// Round-tripping `Unknown` re-emits the sentinel `0xFF` value, + /// which is itself outside the assigned ID3v2 picture-type range + /// (max assigned is `0x14`). Callers writing strict output should + /// gate on [`is_known`](Self::is_known) and decide on a fallback + /// (skip the frame, substitute `Other`) before serialising — the + /// raw `0xFF` round-trip stays bit-stable but reserved-byte-aware + /// muxers will refuse it. + pub fn to_u8(self) -> u8 { + self as u8 + } + + /// `true` when this picture type came from a spec-assigned code + /// (`Other` through `PublisherLogo`); `false` for + /// [`Unknown`](Self::Unknown) — the sentinel produced when + /// [`from_u8`](Self::from_u8) sees a reserved or future-spec byte. + /// + /// Consumer-side gate before emitting a strict ID3v2 / FLAC byte: + /// an `Unknown` round-trips as `0xFF`, which is itself outside the + /// assigned range and will be rejected by strict parsers. + pub fn is_known(self) -> bool { + !matches!(self, Self::Unknown) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn picture_type_known_values() { + assert_eq!(PictureType::from_u8(0x03), PictureType::FrontCover); + assert_eq!(PictureType::from_u8(0x07), PictureType::LeadArtist); + assert_eq!(PictureType::from_u8(0x14), PictureType::PublisherLogo); + } + + #[test] + fn picture_type_unknown_collapses() { + assert_eq!(PictureType::from_u8(0x15), PictureType::Unknown); + assert_eq!(PictureType::from_u8(0xAB), PictureType::Unknown); + } + + #[test] + fn to_u8_inverts_from_u8_on_every_assigned_code() { + // Spec-assigned range 0x00..=0x14 round-trips byte-for-byte + // through from_u8 → to_u8. + for b in 0x00u8..=0x14 { + assert_eq!(PictureType::from_u8(b).to_u8(), b); + } + } + + #[test] + fn to_u8_unknown_emits_sentinel_byte() { + // Unknown variant deliberately carries discriminant 0xFF so + // to_u8 stays a pure `as u8` cast. Reserved-byte audit: 0xFF + // is outside the assigned 0x00..=0x14 range. + assert_eq!(PictureType::Unknown.to_u8(), 0xFF); + // A byte that collapses to Unknown does NOT round-trip + // structurally — the original code is lost to the catch-all, + // which is what is_known() exists to flag. + let collapsed = PictureType::from_u8(0xAB); + assert_eq!(collapsed, PictureType::Unknown); + assert_eq!(collapsed.to_u8(), 0xFF); + assert_ne!(collapsed.to_u8(), 0xAB); + } + + #[test] + fn is_known_separates_assigned_from_sentinel() { + assert!(PictureType::Other.is_known()); + assert!(PictureType::FrontCover.is_known()); + assert!(PictureType::PublisherLogo.is_known()); + assert!(!PictureType::Unknown.is_known()); + } + + #[test] + fn attached_picture_new_defaults_then_builders_fill() { + let p = AttachedPicture::new("image/png", PictureType::FrontCover); + assert_eq!(p.mime_type, "image/png"); + assert_eq!(p.picture_type, PictureType::FrontCover); + assert!(p.description.is_empty()); + assert!(p.data.is_empty()); + assert!(!p.is_external_link()); + + let filled = p + .with_description("Album art") + .with_data(vec![0x89, b'P', b'N', b'G']); + assert_eq!(filled.description, "Album art"); + assert_eq!(filled.data, vec![0x89, b'P', b'N', b'G']); + } + + #[test] + fn attached_picture_with_picture_type_replaces() { + let p = AttachedPicture::new("image/jpeg", PictureType::Other) + .with_picture_type(PictureType::BackCover); + assert_eq!(p.picture_type, PictureType::BackCover); + } + + #[test] + fn attached_picture_external_link_detected_by_sentinel_mime() { + // ID3v2 "-->" sentinel means data is a URL, not inline bytes. + let p = AttachedPicture::new("-->", PictureType::FrontCover) + .with_data(b"https://example.invalid/cover.jpg".to_vec()); + assert!(p.is_external_link()); + + let inline = AttachedPicture::new("image/jpeg", PictureType::FrontCover); + assert!(!inline.is_external_link()); + } +} diff --git a/crates/vendor/oxideav-core/src/rational.rs b/crates/vendor/oxideav-core/src/rational.rs new file mode 100644 index 00000000..4d12f756 --- /dev/null +++ b/crates/vendor/oxideav-core/src/rational.rs @@ -0,0 +1,621 @@ +//! Rational number used for time bases and frame rates. + +use std::cmp::Ordering; +use std::fmt; +use std::ops::{Add, Div, Mul, Neg, Sub}; + +/// An exact fraction `num/den`. +/// +/// # Equality vs. value comparison +/// +/// The derived [`PartialEq`] / [`Eq`] / [`Hash`] are **structural**: +/// `1/2` and `2/4` compare *unequal* because their fields differ. This +/// is deliberate — it keeps `Hash` cheap and lets callers preserve the +/// exact on-wire fraction (e.g. a `30000/1001` frame rate must not be +/// silently folded into `30/1`). When you want to compare by *value* +/// — "do these two fractions denote the same number?" — use +/// [`Rational::equals_value`] or [`Rational::cmp_value`], which reduce +/// the comparison to an overflow-safe `i128` cross-product. Because the +/// derived `Eq` is structural and value-`cmp` is not, `Rational` +/// deliberately does **not** implement [`Ord`] / [`PartialOrd`] (a +/// value-based ordering would violate the `Ord`/`Eq` consistency +/// contract against the structural `Eq`). +/// +/// # Overflow policy +/// +/// All operations are total — nothing here panics, not even on +/// `i64::MIN` terms or zero denominators: +/// +/// * The arithmetic operators (`+ - * /`) and [`reduced`](Self::reduced) +/// compute exactly in 128 bits, reduce to lowest terms, and — when the +/// reduced result still doesn't fit `i64` — return the **closest +/// representable approximation** (a saturated numerator for +/// out-of-range magnitudes, a rescaled `i64::MAX` denominator for +/// out-of-range precision) instead of silently wrapping. +/// * The `checked_add` / `checked_sub` / `checked_mul` / `checked_div` +/// variants return `None` in exactly the cases where the operators +/// would approximate, for callers that need to detect inexactness. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Rational { + /// Numerator. Carries the sign after normalization; stored verbatim + /// otherwise. + pub num: i64, + /// Denominator. Kept as constructed (may be negative or zero — see + /// the type-level docs for how operations treat those). + pub den: i64, +} + +impl Rational { + /// Construct the fraction `num/den`, stored verbatim (no reduction + /// or sign normalization). + pub const fn new(num: i64, den: i64) -> Self { + Self { num, den } + } + + /// The canonical zero: `0/1`. + pub const fn zero() -> Self { + Self { num: 0, den: 1 } + } + + /// `true` when the numerator is zero (the denoted value is zero for + /// any non-zero denominator). + pub fn is_zero(&self) -> bool { + self.num == 0 + } + + /// The denoted value as an `f64` (lossy for terms above 2^53; + /// `±∞`/NaN for zero denominators). + pub fn as_f64(&self) -> f64 { + self.num as f64 / self.den as f64 + } + + /// Reduce the fraction to lowest terms. Sign is normalized onto the + /// numerator. + /// + /// Computed in 128 bits so `i64::MIN` terms reduce correctly (e.g. + /// `i64::MIN / i64::MIN` → `1/1`, `i64::MIN / -2` → `2^62 / 1`) + /// instead of overflowing on negation. In the rare case where the + /// reduced value still doesn't fit `i64` (e.g. `i64::MIN / -3`), + /// the result is the closest representable approximation — see the + /// type-level overflow policy. + pub fn reduced(self) -> Self { + reduce_i128(self.num as i128, self.den as i128) + } + + /// Invert the fraction (num/den → den/num). + pub fn invert(self) -> Self { + Self { + num: self.den, + den: self.num, + } + } + + /// Compare two fractions by the *number they denote*, not by their + /// stored fields. Uses a 128-bit cross-product so `30000/1001` + /// and `30/1` order correctly without reducing or losing precision, + /// and handles negative denominators by folding the sign onto the + /// numerator first. + /// + /// A zero denominator is treated as a signed "infinity": `+n/0` + /// sorts above every finite fraction, `-n/0` below every finite + /// fraction, and `0/0` ties with a finite zero. All `+∞` compare + /// equal to each other (likewise all `-∞`). This is a defensive + /// total order, not a claim that such a fraction is meaningful. + pub fn cmp_value(&self, other: &Self) -> Ordering { + // Normalize sign onto the numerator so the cross-product + // comparison is monotonic regardless of denominator sign. The + // normalization happens in i128 so an `i64::MIN` term survives + // negation. + let (an, ad) = sign_normalized(self.num, self.den); + let (bn, bd) = sign_normalized(other.num, other.den); + if ad != 0 && bd != 0 { + // Both finite: exact i128 cross-product (|terms| ≤ 2^63, so + // each product is ≤ 2^126 and fits). + return (an * bd).cmp(&(bn * ad)); + } + // At least one side is zero-denominator. Map each value to an + // "extended sign rank" on the line −∞ … 0 … +∞: + // +∞ (n>0, d=0) → +2, −∞ (n<0, d=0) → −2, 0/0 → 0, + // finite > 0 → +1, finite < 0 → −1, finite == 0 → 0. + // Comparing ranks yields a defensive total order: all +∞ tie, + // all −∞ tie, ±∞ bracket every finite value, and 0/0 ties with + // a finite zero. (Two finite operands never reach this branch; + // they are compared exactly above.) + fn rank(n: i128, d: i128) -> i128 { + if d == 0 { + n.signum() * 2 + } else { + n.signum() + } + } + rank(an, ad).cmp(&rank(bn, bd)) + } + + /// Whether two fractions denote the same number (value equality), + /// e.g. `Rational::new(2, 4).equals_value(&Rational::new(1, 2))`. + /// Contrast with `==`, which is structural (field-by-field). + pub fn equals_value(&self, other: &Self) -> bool { + self.cmp_value(other) == Ordering::Equal + } + + /// The sign of the fraction: `-1`, `0`, or `1`. A zero denominator + /// reports the sign of the numerator. + pub fn signum(&self) -> i64 { + let (n, _) = sign_normalized(self.num, self.den); + n.signum() as i64 + } + + /// The absolute value of the fraction (sign stripped from both + /// terms; a negative denominator is normalized onto the numerator + /// first). + /// + /// An `i64::MIN` term (whose absolute value doesn't fit `i64`) is + /// handled by reducing in 128 bits — `abs(i64::MIN / 2)` is exactly + /// `2^62 / 1` — falling back to the closest representable + /// approximation when reduction can't bring the term into range. + /// All other inputs keep their fields verbatim (no reduction). + pub fn abs(self) -> Self { + let (n, d) = sign_normalized(self.num, self.den); + let n = n.abs(); + if n <= i64::MAX as i128 && d <= i64::MAX as i128 { + return Self { + num: n as i64, + den: d as i64, + }; + } + reduce_i128(n, d) + } + + /// Exact addition: `Some(reduced sum)` when the reduced result fits + /// `i64`, `None` otherwise (the `+` operator approximates instead). + pub fn checked_add(self, rhs: Self) -> Option { + let a = self.num as i128 * rhs.den as i128; + let b = rhs.num as i128 * self.den as i128; + let den = self.den as i128 * rhs.den as i128; + reduce_exact_i128(a.checked_add(b)?, den) + } + + /// Exact subtraction: `Some(reduced difference)` when the reduced + /// result fits `i64`, `None` otherwise. + pub fn checked_sub(self, rhs: Self) -> Option { + let a = self.num as i128 * rhs.den as i128; + let b = rhs.num as i128 * self.den as i128; + let den = self.den as i128 * rhs.den as i128; + reduce_exact_i128(a.checked_sub(b)?, den) + } + + /// Exact multiplication: `Some(reduced product)` when the reduced + /// result fits `i64`, `None` otherwise. + pub fn checked_mul(self, rhs: Self) -> Option { + let num = self.num as i128 * rhs.num as i128; + let den = self.den as i128 * rhs.den as i128; + reduce_exact_i128(num, den) + } + + /// Exact division: `Some(reduced quotient)` when the reduced result + /// fits `i64`, `None` otherwise. Division by a zero-valued fraction + /// yields a zero-denominator result (the same defensive "infinity" + /// the operators produce), not `None`. + pub fn checked_div(self, rhs: Self) -> Option { + let num = self.num as i128 * rhs.den as i128; + let den = self.den as i128 * rhs.num as i128; + reduce_exact_i128(num, den) + } +} + +/// Move any sign on the denominator onto the numerator, leaving the +/// denominator non-negative. A zero denominator is left as-is. Widens +/// to `i128` so negating an `i64::MIN` term cannot overflow. +#[inline] +const fn sign_normalized(num: i64, den: i64) -> (i128, i128) { + let (n, d) = (num as i128, den as i128); + if d < 0 { + (-n, -d) + } else { + (n, d) + } +} + +/// Reduce a 128-bit `num/den` pair exactly to lowest terms (sign on the +/// numerator) and narrow to `i64`. Returns `None` when either reduced +/// term is out of `i64` range. +fn reduce_exact_i128(mut num: i128, mut den: i128) -> Option { + if den < 0 { + num = -num; + den = -den; + } + let g = gcd_i128(num.unsigned_abs(), den.unsigned_abs()) as i128; + if g > 1 { + num /= g; + den /= g; + } + Some(Rational { + num: i64::try_from(num).ok()?, + den: i64::try_from(den).ok()?, + }) +} + +/// Reduce a 128-bit `num/den` pair to lowest terms (sign on the +/// numerator) and narrow back to `i64`. Used by the arithmetic +/// operators so intermediate products that overflow `i64` but reduce +/// back into range still yield the right answer. When even the reduced +/// form doesn't fit, returns the closest representable approximation +/// (never wraps): +/// +/// * numerator out of range, denominator in range → numerator saturates +/// to `i64::MAX` / `i64::MIN` (magnitude overflow); +/// * denominator out of range, numerator in range → the fraction is +/// rescaled onto an `i64::MAX` denominator with a rounded numerator +/// (precision overflow — the value is tiny); +/// * both out of range → both terms are right-shifted (with rounding) +/// until the denominator fits, then the numerator saturates if it +/// still doesn't. +fn reduce_i128(mut num: i128, mut den: i128) -> Rational { + if den < 0 { + num = -num; + den = -den; + } + let g = gcd_i128(num.unsigned_abs(), den.unsigned_abs()) as i128; + if g > 1 { + num /= g; + den /= g; + } + if let (Ok(n), Ok(d)) = (i64::try_from(num), i64::try_from(den)) { + return Rational { num: n, den: d }; + } + approx_narrow(num, den) +} + +/// Best-effort narrowing of an already-reduced, sign-normalized +/// (`den >= 0`) `i128` fraction that doesn't fit `i64`. See +/// [`reduce_i128`] for the three cases. +fn approx_narrow(num: i128, den: i128) -> Rational { + fn sat(v: i128) -> i64 { + if v > i64::MAX as i128 { + i64::MAX + } else if v < i64::MIN as i128 { + i64::MIN + } else { + v as i64 + } + } + if den <= i64::MAX as i128 { + // Magnitude overflow: only the numerator is out of range. + return Rational { + num: sat(num), + den: den as i64, + }; + } + if num.unsigned_abs() <= i64::MAX as u128 { + // Precision overflow: |value| < 1 with a too-fine denominator. + // Rescale onto an i64::MAX denominator, rounding half away from + // zero. (|num| ≤ 2^63 and den/2 < 2^126, so no i128 overflow.) + let n = (num * i64::MAX as i128 + num.signum() * (den / 2)) / den; + return Rational { + num: n as i64, + den: i64::MAX, + }; + } + // Both out of range: shift the denominator into range, apply the + // same shift to the numerator (rounding half up on the magnitude), + // and saturate whatever still doesn't fit. + let dbits = 128 - den.leading_zeros(); + let k = dbits - 63; + let half = 1u128 << (k - 1); + let n_abs = (num.unsigned_abs() + half) >> k; + let d = ((den as u128 + half) >> k).max(1); + let n = if num < 0 { + -(n_abs as i128) + } else { + n_abs as i128 + }; + Rational { + num: sat(n), + den: sat(d as i128), + } +} + +impl Add for Rational { + type Output = Rational; + /// Add two fractions, returning the result in lowest terms (or the + /// closest representable approximation on overflow — see the + /// type-level overflow policy). + fn add(self, rhs: Self) -> Self { + // The saturating add only differs from `+` when both cross + // products are near ±2^126 (all four fields near ±2^63); the + // result then approximates instead of panicking in debug. + let a = self.num as i128 * rhs.den as i128; + let b = rhs.num as i128 * self.den as i128; + let den = self.den as i128 * rhs.den as i128; + reduce_i128(a.saturating_add(b), den) + } +} + +impl Sub for Rational { + type Output = Rational; + /// Subtract `rhs` from `self`, returning the result in lowest terms + /// (or the closest representable approximation on overflow). + fn sub(self, rhs: Self) -> Self { + let a = self.num as i128 * rhs.den as i128; + let b = rhs.num as i128 * self.den as i128; + let den = self.den as i128 * rhs.den as i128; + reduce_i128(a.saturating_sub(b), den) + } +} + +impl Mul for Rational { + type Output = Rational; + /// Multiply two fractions, returning the result in lowest terms. + fn mul(self, rhs: Self) -> Self { + let num = self.num as i128 * rhs.num as i128; + let den = self.den as i128 * rhs.den as i128; + reduce_i128(num, den) + } +} + +impl Div for Rational { + type Output = Rational; + /// Divide `self` by `rhs` (multiply by the reciprocal), returning + /// the result in lowest terms. + fn div(self, rhs: Self) -> Self { + let num = self.num as i128 * rhs.den as i128; + let den = self.den as i128 * rhs.num as i128; + reduce_i128(num, den) + } +} + +impl Neg for Rational { + type Output = Rational; + /// Negate the fraction (sign applied to the numerator). When the + /// numerator is `i64::MIN` — whose negation doesn't fit `i64` — the + /// sign is applied to the denominator instead, which denotes the + /// same negated value; `-(i64::MIN / i64::MIN)` (value exactly `1`) + /// returns `-1/1`, and `-(i64::MIN / 0)` (the defensive `-∞`) + /// returns the saturated `+∞` `i64::MAX / 0`. + fn neg(self) -> Self { + if self.num != i64::MIN { + Self { + num: -self.num, + den: self.den, + } + } else if self.den == 0 { + Self { + num: i64::MAX, + den: 0, + } + } else if self.den != i64::MIN { + Self { + num: self.num, + den: -self.den, + } + } else { + Self { num: -1, den: 1 } + } + } +} + +impl fmt::Display for Rational { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.num, self.den) + } +} + +fn gcd_i128(mut a: u128, mut b: u128) -> u128 { + while b != 0 { + let t = b; + b = a % b; + a = t; + } + a.max(1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reduce() { + assert_eq!(Rational::new(10, 20).reduced(), Rational::new(1, 2)); + assert_eq!(Rational::new(-6, 9).reduced(), Rational::new(-2, 3)); + assert_eq!(Rational::new(6, -9).reduced(), Rational::new(-2, 3)); + } + + #[test] + fn invert() { + assert_eq!(Rational::new(1, 2).invert(), Rational::new(2, 1)); + } + + #[test] + fn cmp_value_orders_by_number_not_fields() { + // Structurally unequal, value-equal. + assert!(Rational::new(1, 2).equals_value(&Rational::new(2, 4))); + assert_ne!(Rational::new(1, 2), Rational::new(2, 4)); + + assert_eq!( + Rational::new(1, 3).cmp_value(&Rational::new(1, 2)), + std::cmp::Ordering::Less + ); + assert_eq!( + Rational::new(30_000, 1001).cmp_value(&Rational::new(30, 1)), + std::cmp::Ordering::Less + ); + // Negative denominator folds onto numerator for comparison. + assert!(Rational::new(1, -2).equals_value(&Rational::new(-1, 2))); + assert_eq!( + Rational::new(-1, 2).cmp_value(&Rational::new(1, 2)), + std::cmp::Ordering::Less + ); + } + + #[test] + fn cmp_value_zero_denominator_is_total() { + let pos_inf = Rational::new(1, 0); + let neg_inf = Rational::new(-1, 0); + let finite = Rational::new(1_000_000, 1); + assert_eq!(pos_inf.cmp_value(&finite), std::cmp::Ordering::Greater); + assert_eq!(finite.cmp_value(&pos_inf), std::cmp::Ordering::Less); + assert_eq!(neg_inf.cmp_value(&finite), std::cmp::Ordering::Less); + assert_eq!(pos_inf.cmp_value(&neg_inf), std::cmp::Ordering::Greater); + assert!(pos_inf.equals_value(&Rational::new(7, 0))); + } + + #[test] + fn signum_and_abs() { + assert_eq!(Rational::new(3, 4).signum(), 1); + assert_eq!(Rational::new(-3, 4).signum(), -1); + assert_eq!(Rational::new(3, -4).signum(), -1); + assert_eq!(Rational::new(0, 4).signum(), 0); + assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4)); + assert_eq!(Rational::new(3, -4).abs(), Rational::new(3, 4)); + } + + #[test] + fn arithmetic_reduces() { + assert_eq!( + Rational::new(1, 2) + Rational::new(1, 3), + Rational::new(5, 6) + ); + assert_eq!( + Rational::new(1, 2) - Rational::new(1, 3), + Rational::new(1, 6) + ); + // 2/4 * 3/9 = 6/36 = 1/6 + assert_eq!( + Rational::new(2, 4) * Rational::new(3, 9), + Rational::new(1, 6) + ); + // (1/2) / (3/4) = 4/6 = 2/3 + assert_eq!( + Rational::new(1, 2) / Rational::new(3, 4), + Rational::new(2, 3) + ); + assert_eq!(-Rational::new(1, 2), Rational::new(-1, 2)); + } + + #[test] + fn reduced_handles_i64_min_terms() { + // Negative denominator with an i64::MIN numerator used to + // overflow on negation; the i128 path reduces it correctly. + assert_eq!( + Rational::new(i64::MIN, i64::MIN).reduced(), + Rational::new(1, 1) + ); + assert_eq!( + Rational::new(i64::MIN, -2).reduced(), + Rational::new(1 << 62, 1) + ); + assert_eq!( + Rational::new(i64::MIN, 2).reduced(), + Rational::new(-(1 << 62), 1) + ); + assert_eq!( + Rational::new(-2, i64::MIN).reduced(), + Rational::new(1, 1 << 62) + ); + // Coprime i64::MIN / -3: the exact reduction 2^63/3 doesn't fit, + // so the numerator saturates (closest representable value). + assert_eq!( + Rational::new(i64::MIN, -3).reduced(), + Rational::new(i64::MAX, 3) + ); + } + + #[test] + fn neg_handles_i64_min_numerator() { + // Sign moves to the denominator when the numerator can't flip. + let r = -Rational::new(i64::MIN, 5); + assert_eq!(r, Rational::new(i64::MIN, -5)); + // Double negation restores the original value. + assert!((-r).equals_value(&Rational::new(i64::MIN, 5))); + // i64::MIN / i64::MIN denotes exactly 1 → its negation is -1. + assert_eq!(-Rational::new(i64::MIN, i64::MIN), Rational::new(-1, 1)); + // Defensive -∞ negates to a saturated +∞. + let inf = -Rational::new(i64::MIN, 0); + assert_eq!(inf, Rational::new(i64::MAX, 0)); + } + + #[test] + fn abs_signum_cmp_handle_i64_min_terms() { + // abs of i64::MIN/2 reduces exactly to 2^62/1. + assert_eq!(Rational::new(i64::MIN, 2).abs(), Rational::new(1 << 62, 1)); + // Coprime denominator: saturated approximation. + assert_eq!(Rational::new(i64::MIN, 3).abs(), Rational::new(i64::MAX, 3)); + // i64::MIN *denominator* used to overflow in sign normalization. + assert_eq!(Rational::new(3, i64::MIN).signum(), -1); + assert_eq!( + Rational::new(3, i64::MIN).cmp_value(&Rational::zero()), + std::cmp::Ordering::Less + ); + assert!(Rational::new(i64::MIN, i64::MIN).equals_value(&Rational::new(1, 1))); + } + + #[test] + fn checked_ops_exact_or_none() { + // In-range results match the operators. + assert_eq!( + Rational::new(1, 2).checked_add(Rational::new(1, 3)), + Some(Rational::new(5, 6)) + ); + assert_eq!( + Rational::new(1, 2).checked_sub(Rational::new(1, 3)), + Some(Rational::new(1, 6)) + ); + assert_eq!( + Rational::new(2, 4).checked_mul(Rational::new(3, 9)), + Some(Rational::new(1, 6)) + ); + assert_eq!( + Rational::new(1, 2).checked_div(Rational::new(3, 4)), + Some(Rational::new(2, 3)) + ); + // Results whose reduced form exceeds i64 report None. + let max = Rational::new(i64::MAX, 1); + assert_eq!(max.checked_add(Rational::new(1, 1)), None); + assert_eq!( + Rational::new(1 << 32, 1).checked_mul(Rational::new(1 << 32, 1)), + None + ); + assert_eq!( + Rational::new(1, 1 << 32).checked_mul(Rational::new(1, 1 << 32)), + None + ); + // Division by zero-valued rhs is the defensive infinity, not None. + assert_eq!( + Rational::new(1, 2).checked_div(Rational::zero()), + Some(Rational::new(1, 0)) + ); + } + + #[test] + fn operators_approximate_instead_of_wrapping() { + // Magnitude overflow: numerator saturates. + let r = Rational::new(i64::MAX, 1) + Rational::new(1, 1); + assert_eq!(r, Rational::new(i64::MAX, 1)); + let r = Rational::new(i64::MIN, 1) - Rational::new(1, 1); + assert_eq!(r, Rational::new(i64::MIN, 1)); + // Precision overflow: denominator rescales onto i64::MAX with a + // rounded numerator (value stays tiny, sign preserved). + let tiny = Rational::new(1, i64::MAX) * Rational::new(1, 2); + assert_eq!(tiny.den, i64::MAX); + assert!(tiny.num == 0 || tiny.num == 1); + let tiny_neg = Rational::new(-3, i64::MAX) * Rational::new(1, 2); + assert_eq!(tiny_neg.den, i64::MAX); + assert!(tiny_neg.num <= 0 && tiny_neg.num >= -2); + // Large but reducible products still come out exact. + let r = Rational::new(i64::MAX, 3) * Rational::new(3, i64::MAX); + assert_eq!(r, Rational::new(1, 1)); + } + + #[test] + fn arithmetic_uses_128bit_intermediates() { + // Products that overflow i64 but reduce back into range must + // still yield the correct reduced fraction. + let big = Rational::new(i64::MAX / 2, 3); + let r = big * Rational::new(3, 1); + assert_eq!(r, Rational::new(i64::MAX / 2, 1)); + // num/den both large, equal → reduces to 1/1. + let a = Rational::new(1_000_000_000, 1); + let sum = a + a; // 2_000_000_000 / 1 + assert_eq!(sum, Rational::new(2_000_000_000, 1)); + } +} diff --git a/crates/vendor/oxideav-core/src/registry/codec.rs b/crates/vendor/oxideav-core/src/registry/codec.rs new file mode 100644 index 00000000..3b150e18 --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/codec.rs @@ -0,0 +1,1342 @@ +//! In-process codec registry. +//! +//! Every codec crate declares itself with one [`CodecInfo`] value — +//! capabilities, factory functions, the container tags it claims, and +//! (optionally) a probe function used to disambiguate genuine tag +//! collisions. The registry stores those registrations and exposes +//! three orthogonal lookups: +//! +//! - **id-keyed** — `make_decoder(params)` / `make_encoder(params)` walk +//! the implementations registered under `params.codec_id`, filter by +//! capability restrictions, and try them in priority order with init- +//! time fallback. +//! - **tag-keyed** — `resolve_tag(&ProbeContext)` walks every +//! registration whose `tags` contains `ctx.tag`, calls each probe +//! (treating `None` as "returns 1.0"), and returns the id with the +//! highest resulting confidence. First-registered wins on ties. +//! - **payload-magic-keyed** — `resolve_payload_magic(first_bytes)` +//! prefix-matches the claimed payload magic prefixes against a +//! stream's leading bytes; longest matching magic wins, then +//! registration order. For containers that identify a codec by the +//! payload itself rather than a tag (e.g. an Ogg logical stream's +//! first packet, or raw elementary streams). +//! - **diagnostic** — `all_implementations`, `all_tag_registrations`, +//! `all_payload_magic_registrations`. +//! +//! The tag path explicitly DOES NOT short-circuit on "first claim with +//! no probe" — every claimant is asked, so a lower-priority probed +//! claim can out-rank a higher-priority unprobed one when the content +//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.). + +use std::collections::HashMap; + +use crate::arena; +use crate::{ + CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecResolver, CodecTag, + Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn, + Result, +}; + +// ───────────────────────── codec traits ───────────────────────── + +/// A packet-to-frame decoder. +pub trait Decoder: Send { + /// Identifier of the codec this decoder handles. + fn codec_id(&self) -> &CodecId; + + /// Feed one compressed packet. May or may not produce a frame immediately — + /// call `receive_frame` in a loop afterwards. + fn send_packet(&mut self, packet: &Packet) -> Result<()>; + + /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the + /// decoder needs another packet. + fn receive_frame(&mut self) -> Result; + + /// Pull the next decoded frame as an arena-backed [`arena::sync::Frame`]. + /// + /// Decoders that build their output through an + /// [`arena::sync::ArenaPool`] override this to return the pooled + /// [`arena::sync::Frame`] **directly**, with no per-plane memcpy + /// out — the caller gets true zero-copy plane access via + /// [`arena::sync::FrameInner::plane`]. + /// + /// The default implementation delegates to [`Self::receive_frame`] + /// and copies the video planes into a freshly-leased one-shot + /// `arena::sync::ArenaPool`. This makes the method an additive + /// change for every existing [`Decoder`] impl: callers using the + /// new API still work, but pay one memcpy per plane. + /// + /// **Audio / subtitle frames:** the [`arena::sync::Frame`] body is + /// video-only (planes + [`arena::sync::FrameHeader`] with + /// width/height/pixel format). The default implementation returns + /// [`Error::Unsupported`] for non-video frames; an audio decoder + /// that wants to expose `receive_arena_frame()` must override it + /// with its own arena-backed audio-frame type once the framework + /// gains one. Until then, audio decoders should keep using + /// [`Self::receive_frame`]. + fn receive_arena_frame(&mut self) -> Result { + let frame = self.receive_frame()?; + match frame { + Frame::Video(v) => video_frame_to_arena_sync_frame(&v), + Frame::Audio(_) => Err(Error::unsupported( + "receive_arena_frame: audio frames not yet supported by default impl", + )), + Frame::Subtitle(_) => Err(Error::unsupported( + "receive_arena_frame: subtitle frames have no arena-backed representation", + )), + Frame::Vector(_) => Err(Error::unsupported( + "receive_arena_frame: vector frames have no arena-backed representation", + )), + } + } + + /// Signal end-of-stream. After this, `receive_frame` will drain buffered + /// frames and eventually return `Error::Eof`. + fn flush(&mut self) -> Result<()>; + + /// Discard all carry-over state so the decoder can resume from a new + /// bitstream position without producing stale output. Called by the + /// player after a container seek. + /// + /// Unlike [`flush`](Self::flush) (which signals end-of-stream and + /// drains buffered frames), `reset` is expected to: + /// * drop every buffered input packet and pending output frame; + /// * zero any per-stream filter / predictor / overlap memory so the + /// next `send_packet` decodes as if it were the first; + /// * leave the codec id and stream parameters untouched. + /// + /// The default is a conservative "drain-then-forget": call + /// [`flush`](Self::flush) and ignore any remaining frames. Stateful + /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap, + /// reference pictures, …) should override this to wipe their + /// internal state explicitly — otherwise the first ~N output + /// samples after a seek will be glitchy until the state re-adapts. + fn reset(&mut self) -> Result<()> { + self.flush()?; + // Drain any remaining output frames so the next send_packet + // starts clean. NeedMore / Eof both mean "no more frames"; any + // other error is surfaced so the caller can see why. + loop { + match self.receive_frame() { + Ok(_) => {} + Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()), + Err(e) => return Err(e), + } + } + } + + /// Advisory: announce the runtime environment (today: a thread budget + /// for codec-internal parallelism). Called at most once, before the + /// first `send_packet`. Default no-op; codecs that want to run + /// slice-/GOP-/tile-parallel override this to capture the budget. + /// Ignoring the hint is always safe — callers must still work with + /// a decoder that runs serial. + fn set_execution_context(&mut self, _ctx: &ExecutionContext) {} +} + +/// A frame-to-packet encoder. +pub trait Encoder: Send { + /// Identifier of the codec this encoder produces. + fn codec_id(&self) -> &CodecId; + + /// Parameters describing this encoder's output stream (to feed into a muxer). + fn output_params(&self) -> &CodecParameters; + + /// Feed one uncompressed frame. May or may not produce a packet + /// immediately — call `receive_packet` in a loop afterwards. + fn send_frame(&mut self, frame: &Frame) -> Result<()>; + + /// Pull the next encoded packet, if any. Returns `Error::NeedMore` + /// when the encoder needs another frame (or a `flush`). + fn receive_packet(&mut self) -> Result; + + /// Signal end of input: drain internal lookahead so the remaining + /// packets become available via `receive_packet`. + fn flush(&mut self) -> Result<()>; + + /// Advisory: announce the runtime environment. Same semantics as + /// [`Decoder::set_execution_context`]. + fn set_execution_context(&mut self, _ctx: &ExecutionContext) {} +} + +/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a +/// heap-backed [`crate::VideoFrame`] into a freshly-leased +/// [`arena::sync::Frame`]. +/// +/// Allocates a single-slot, single-arena `arena::sync::ArenaPool` +/// sized to fit the planes verbatim. The pool is dropped at the end of +/// this call; the returned `Frame` keeps its leased buffer alive via +/// `Arc` (the `Arena`'s `Weak` handle to the dropped pool +/// just stops upgrading — the buffer drops normally when the last +/// `Frame` clone goes away). +/// +/// Width / height / pixel-format on the returned `FrameHeader` are +/// derived from the plane shape: `width = plane[0].stride`, +/// `height = plane[0].data.len() / stride`. Pixel format is left as +/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first +/// per-plane sensible default — this is a best-effort label for the +/// generic conversion path; decoders that override +/// `receive_arena_frame` themselves should set the correct pixel +/// format. +fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result { + if v.planes.is_empty() { + return Err(Error::invalid( + "receive_arena_frame: video frame has no planes", + )); + } + let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum(); + if total_bytes == 0 { + return Err(Error::invalid( + "receive_arena_frame: video frame planes are empty", + )); + } + // One-shot pool sized exactly to the frame. The pool drops at end + // of scope; the leased Arena lives on inside the returned Frame + // (its Weak handle just won't upgrade in Drop, so the + // Box<[u8]> falls through to a normal heap free). + let pool = arena::sync::ArenaPool::with_alloc_count_cap( + 1, + total_bytes, + // One alloc per plane, plus a generous safety margin. + (v.planes.len() as u32).saturating_add(4), + ); + let arena = pool.lease()?; + let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len()); + let mut cursor = 0usize; + for plane in &v.planes { + let dst = arena.alloc::(plane.data.len())?; + dst.copy_from_slice(&plane.data); + plane_offsets.push((cursor, plane.data.len())); + cursor += plane.data.len(); + } + // Best-effort header: width = stride of plane 0, height inferred + // from plane 0's data length. Pixel format defaults to Yuv420P for + // the common 3-plane case, Gray8 for single-plane, otherwise + // Yuv444P. Decoders that care about exact pixel-format / width / + // height should override `receive_arena_frame` themselves so they + // can emit a correct `FrameHeader` straight from their arena + // build path. + let stride0 = v.planes[0].stride.max(1); + let width = stride0 as u32; + let height = (v.planes[0].data.len() / stride0) as u32; + // Count only image planes for the format guess — side-channel + // entries (palette, significant bits; copied verbatim above like + // any other plane) must not bump e.g. a single-plane palette frame + // out of the Gray8 label. + let pixel_format = match v.image_plane_count() { + 1 => PixelFormat::Gray8, + 3 => PixelFormat::Yuv420P, + _ => PixelFormat::Yuv444P, + }; + let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts); + arena::sync::FrameInner::new(arena, &plane_offsets, header) +} + +/// Factory that builds a decoder for a given codec parameter set. +pub type DecoderFactory = fn(params: &CodecParameters) -> Result>; + +/// Factory that builds an encoder for a given codec parameter set. +pub type EncoderFactory = fn(params: &CodecParameters) -> Result>; + +// ───────────────────────── CodecInfo ───────────────────────── + +/// A single registration: capabilities, decoder/encoder factories, +/// optional probe, and the container tags this codec claims. +/// +/// Codec crates build one of these per codec id inside their +/// `register(reg)` function and hand it to +/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so +/// additional fields can be added without breaking existing codec +/// crates — construction is only possible through +/// [`CodecInfo::new`] plus the builder methods below. +#[non_exhaustive] +pub struct CodecInfo { + /// Canonical codec identifier this entry registers. + pub id: CodecId, + /// Capability description (media kind, feature flags, priority). + pub capabilities: CodecCapabilities, + /// Factory producing a fresh decoder instance, if decode is supported. + pub decoder_factory: Option, + /// Factory producing a fresh encoder instance, if encode is supported. + pub encoder_factory: Option, + /// Probe function that returns a confidence in `0.0..=1.0` for a + /// given [`ProbeContext`]. `None` means "confidence 1.0 for every + /// claimed tag" — the correct default for codecs whose tag claims + /// are unambiguous. + pub probe: Option, + /// Tags this codec is willing to be looked up under. One codec may + /// claim many tags (an AAC decoder covers several WaveFormat ids, + /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once). + pub tags: Vec, + /// Payload magic prefixes this codec answers to (`\x01vorbis`, + /// `OpusHead`, …). Some carriage formats have no codec tag — the + /// codec is announced by a magic byte prefix on the payload itself + /// (an Ogg logical stream's first packet is the canonical case; + /// raw elementary streams are another). Such claims are + /// prefix-matched by + /// [`CodecRegistry::resolve_payload_magic_ref`] instead of living in + /// the exact-match [`CodecTag`] index. Attached with + /// [`Self::payload_magic`] / [`Self::payload_magics`]. Empty prefixes are + /// ignored at registration time (a zero-length prefix would match + /// every stream while carrying no evidence). + pub payload_magics: Vec>, + /// Schema of the encoder's recognised option keys + /// (`CodecParameters::options`). Attached with + /// [`Self::encoder_options`]. Used for validation / `oxideav list` + /// / pipeline JSON checks. + pub encoder_options_schema: Option<&'static [OptionField]>, + /// Schema of the decoder's recognised option keys. + pub decoder_options_schema: Option<&'static [OptionField]>, + /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`, + /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every + /// `CodecInfo` they register; SW codecs leave this `None`. + /// Consumers (e.g. the CLI's `info` command) use it to group + /// codec entries by backend and to dedupe probe calls — multiple + /// `CodecInfo` entries with the same `engine_id` typically share + /// an `engine_probe` function, and consumers should call the probe + /// at most once per `engine_id` per pass. Attached via + /// [`Self::with_engine_id`]. + pub engine_id: Option<&'static str>, + /// Optional engine probe function. When `Some`, calling it returns + /// one [`crate::engine::HwDeviceInfo`] entry per device the backend + /// sees. Phase-2 HW siblings populate this on every `CodecInfo` + /// they register; Phase-3 consumers (CLI) call it on demand. + /// Attached via [`Self::with_engine_probe`]. + pub engine_probe: Option, +} + +impl CodecInfo { + /// Start a new registration for `id` with empty capabilities, no + /// factories, no probe, and no tags. Chain the builder methods + /// below to fill it in, then hand the result to + /// [`CodecRegistry::register`]. + pub fn new(id: CodecId) -> Self { + Self { + capabilities: CodecCapabilities::audio(id.as_str()), + id, + decoder_factory: None, + encoder_factory: None, + probe: None, + tags: Vec::new(), + payload_magics: Vec::new(), + encoder_options_schema: None, + decoder_options_schema: None, + engine_id: None, + engine_probe: None, + } + } + + /// Replace the capability description. The default built by + /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every + /// real registration should call this. + pub fn capabilities(mut self, caps: CodecCapabilities) -> Self { + self.capabilities = caps; + self + } + + /// Builder: attach the decoder factory. + pub fn decoder(mut self, factory: DecoderFactory) -> Self { + self.decoder_factory = Some(factory); + self + } + + /// Builder: attach the encoder factory. + pub fn encoder(mut self, factory: EncoderFactory) -> Self { + self.encoder_factory = Some(factory); + self + } + + /// Builder: attach a confidence probe (see [`CodecInfo::probe`]). + pub fn probe(mut self, probe: ProbeFn) -> Self { + self.probe = Some(probe); + self + } + + /// Claim a single container tag for this codec. Equivalent to + /// `.tags([tag])` but avoids the array ceremony for single-tag + /// claims. + pub fn tag(mut self, tag: CodecTag) -> Self { + self.tags.push(tag); + self + } + + /// Claim a set of container tags for this codec. Takes any + /// iterable (arrays, `Vec`, `Option`, …) so the common case of a + /// codec with 3-6 tags reads as one clean block. + pub fn tags(mut self, tags: impl IntoIterator) -> Self { + self.tags.extend(tags); + self + } + + /// Claim one payload magic prefix for this codec (see + /// [`Self::payload_magics`]). Chain repeatedly for codecs that answer + /// to more than one magic: + /// + /// ``` + /// # use oxideav_core::registry::CodecInfo; + /// # use oxideav_core::CodecId; + /// let info = CodecInfo::new(CodecId::new("vorbis")).payload_magic(b"\x01vorbis"); + /// # let _ = info; + /// ``` + pub fn payload_magic(mut self, magic: impl Into>) -> Self { + self.payload_magics.push(magic.into()); + self + } + + /// Claim a set of payload magic prefixes for this codec — the + /// iterable companion to [`Self::payload_magic`], mirroring the + /// [`Self::tag`] / [`Self::tags`] pair. + pub fn payload_magics(mut self, magics: I) -> Self + where + I: IntoIterator, + I::Item: Into>, + { + self.payload_magics + .extend(magics.into_iter().map(Into::into)); + self + } + + /// Declare the options struct this codec's encoder factory expects. + /// Attaches `T::SCHEMA` so the registry can enumerate recognised + /// option keys (for `oxideav list`, pipeline JSON validation, etc.). + /// The factory itself still has to call + /// [`crate::parse_options::()`] against + /// `CodecParameters::options` at init time. + pub fn encoder_options(mut self) -> Self { + self.encoder_options_schema = Some(T::SCHEMA); + self + } + + /// Declare the options struct this codec's decoder factory expects. + /// See [`Self::encoder_options`] for the encoder counterpart. + pub fn decoder_options(mut self) -> Self { + self.decoder_options_schema = Some(T::SCHEMA); + self + } + + /// Tag this codec as belonging to a HW backend identified by + /// `engine_id`. Should match the `engine_id` of every other + /// `CodecInfo` registered by the same backend, and the corresponding + /// `engine_id` field used by the CLI for grouping. SW codecs leave + /// this unset. + pub fn with_engine_id(mut self, engine_id: &'static str) -> Self { + self.engine_id = Some(engine_id); + self + } + + /// Attach a probe function. Consumers call it to enumerate the + /// engines (devices) this backend can dispatch to. Probes are + /// expected to be idempotent and side-effect free; consumers may + /// call them more than once per process and should dedupe by + /// [`Self::engine_id`]. + pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self { + self.engine_probe = Some(probe); + self + } +} + +/// Internal per-impl record held inside the registry's id map. Kept +/// distinct from [`CodecInfo`] so the id map stays cheap to walk +/// during `make_decoder` / `make_encoder` lookups. +#[derive(Clone)] +pub struct CodecImplementation { + /// Capability description copied from the originating [`CodecInfo`]. + pub caps: CodecCapabilities, + /// Decoder factory, if this implementation can decode. + pub make_decoder: Option, + /// Encoder factory, if this implementation can encode. + pub make_encoder: Option, + /// Encoder options schema declared via + /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts + /// no tuning knobs (any non-empty `CodecParameters::options` will + /// still be rejected by the factory if the encoder calls + /// `parse_options` — this is purely informational for discovery). + pub encoder_options_schema: Option<&'static [OptionField]>, + /// Decoder options schema declared via + /// [`CodecInfo::decoder_options`]; same semantics as the encoder + /// schema above. + pub decoder_options_schema: Option<&'static [OptionField]>, + /// HW backend identifier copied verbatim from the originating + /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW + /// backends; `None` on SW codecs. Consumers (CLI `info` command, + /// pipeline dispatcher, bench loop) read this to group entries by + /// backend without grepping `caps.implementation`. + pub engine_id: Option<&'static str>, + /// Engine probe function copied verbatim from the originating + /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a + /// probe wired; `None` on SW codecs. Consumers call it on demand + /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]). + pub engine_probe: Option, +} + +/// Registry mapping codec ids and container tags to their registered +/// implementations; the lookup point behind `make_decoder` / +/// `make_encoder` / `resolve_tag`. +#[derive(Default)] +pub struct CodecRegistry { + /// id → list of implementations. Each registered codec appends one + /// entry here. `make_decoder` / `make_encoder` walk this list in + /// preference order. + impls: HashMap>, + /// Append-only list of every registration — the `tag_index` stores + /// offsets into this vector. + registrations: Vec, + /// Tag → indices into `registrations`. Indices are stored in + /// registration order so tie-breaking in `resolve_tag` is + /// deterministic (first-registered wins). + tag_index: HashMap>, + /// Payload magic-prefix claims: `(magic, registration index)` in + /// registration order. Kept as a flat list rather than a map + /// because resolution is prefix matching (see + /// [`Self::resolve_payload_magic_ref`]), not exact-key lookup. + magic_index: Vec<(Vec, usize)>, +} + +/// Internal registry record. Mirrors the subset of [`CodecInfo`] +/// needed at resolve time. +struct RegistrationRecord { + id: CodecId, + probe: Option, +} + +impl CodecRegistry { + /// An empty registry (same as `Default`). + pub fn new() -> Self { + Self::default() + } + + /// Register one codec. Expands into: + /// * an entry in the id → implementations map (for + /// `make_decoder` / `make_encoder`); + /// * an entry in the tag index for every claimed tag (for + /// `resolve_tag`). + /// + /// Calling `register` multiple times with the same id is allowed + /// and how multi-implementation codecs (software-plus-hardware + /// FLAC, for example) are expressed. + pub fn register(&mut self, info: CodecInfo) { + let CodecInfo { + id, + capabilities, + decoder_factory, + encoder_factory, + probe, + tags, + payload_magics, + encoder_options_schema, + decoder_options_schema, + // engine_id / engine_probe are metadata attached to a + // CodecInfo for backends that want consumers (CLI `info`, + // pipeline bench) to enumerate the underlying devices on + // demand. They're surfaced verbatim on the resulting + // CodecImplementation so consumers can read them without + // grepping `caps.implementation`. Tag-only CodecInfo entries + // (no factories) drop the values on the floor — there's no + // CodecImplementation built in that branch. + engine_id, + engine_probe, + } = info; + + let caps = { + let mut c = capabilities; + if decoder_factory.is_some() { + c = c.with_decode(); + } + if encoder_factory.is_some() { + c = c.with_encode(); + } + c + }; + + // Only record an implementation entry when at least one factory + // is present. A "tag-only" CodecInfo — used to attach extra tag + // claims to a codec that was already registered with factories — + // shouldn't pollute the impl list. + if decoder_factory.is_some() || encoder_factory.is_some() { + self.impls + .entry(id.clone()) + .or_default() + .push(CodecImplementation { + caps, + make_decoder: decoder_factory, + make_encoder: encoder_factory, + encoder_options_schema, + decoder_options_schema, + engine_id, + engine_probe, + }); + } + + let record_idx = self.registrations.len(); + self.registrations.push(RegistrationRecord { + id: id.clone(), + probe, + }); + for tag in tags { + self.tag_index.entry(tag).or_default().push(record_idx); + } + for magic in payload_magics { + // A zero-length prefix would match every stream while + // carrying no evidence — drop it here so resolution never + // has to special-case it. + if !magic.is_empty() { + self.magic_index.push((magic, record_idx)); + } + } + } + + /// Whether at least one registered implementation of `id` can decode. + pub fn has_decoder(&self, id: &CodecId) -> bool { + self.impls + .get(id) + .map(|v| v.iter().any(|i| i.make_decoder.is_some())) + .unwrap_or(false) + } + + /// Whether at least one registered implementation of `id` can encode. + pub fn has_encoder(&self, id: &CodecId) -> bool { + self.impls + .get(id) + .map(|v| v.iter().any(|i| i.make_encoder.is_some())) + .unwrap_or(false) + } + + /// First registered decoder factory for `params.codec_id`, invoked + /// with `params`. No priority walk, no preference filter, no + /// init-time fallback to a lower-priority impl. Errors if no + /// decoder is registered for the codec. + /// + /// Intended for single-impl scenarios — typically a codec crate's + /// own self-tests, where exactly one impl has been registered into + /// a freshly-constructed registry. Production callers selecting + /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox) + /// should use `oxideav_pipeline::make_decoder_with` instead, which + /// applies `CodecPreferences` and walks priorities. + pub fn first_decoder(&self, params: &CodecParameters) -> Result> { + let imp = self + .implementations(¶ms.codec_id) + .iter() + .find(|i| i.make_decoder.is_some()) + .ok_or_else(|| { + Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id)) + })?; + (imp.make_decoder.expect("checked above"))(params) + } + + /// First registered encoder factory — see [`first_decoder`]. + /// + /// [`first_decoder`]: Self::first_decoder + pub fn first_encoder(&self, params: &CodecParameters) -> Result> { + let imp = self + .implementations(¶ms.codec_id) + .iter() + .find(|i| i.make_encoder.is_some()) + .ok_or_else(|| { + Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id)) + })?; + (imp.make_encoder.expect("checked above"))(params) + } + + /// Look up a decoder by exact implementation name + /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl + /// isn't registered or if it has no decoder factory. + pub fn decoder_by_impl( + &self, + impl_name: &str, + params: &CodecParameters, + ) -> Result> { + let imp = self + .implementations(¶ms.codec_id) + .iter() + .find(|i| i.caps.implementation == impl_name) + .ok_or_else(|| { + Error::CodecNotFound(format!( + "no implementation `{impl_name}` for codec {}", + params.codec_id + )) + })?; + let factory = imp + .make_decoder + .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?; + factory(params) + } + + /// Look up an encoder by exact implementation name — see + /// [`decoder_by_impl`]. + /// + /// [`decoder_by_impl`]: Self::decoder_by_impl + pub fn encoder_by_impl( + &self, + impl_name: &str, + params: &CodecParameters, + ) -> Result> { + let imp = self + .implementations(¶ms.codec_id) + .iter() + .find(|i| i.caps.implementation == impl_name) + .ok_or_else(|| { + Error::CodecNotFound(format!( + "no implementation `{impl_name}` for codec {}", + params.codec_id + )) + })?; + let factory = imp + .make_encoder + .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?; + factory(params) + } + + /// Iterate codec ids that have at least one decoder implementation. + pub fn decoder_ids(&self) -> impl Iterator { + self.impls + .iter() + .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some())) + .map(|(id, _)| id) + } + + /// Iterate codec ids that have at least one encoder implementation. + pub fn encoder_ids(&self) -> impl Iterator { + self.impls + .iter() + .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some())) + .map(|(id, _)| id) + } + + /// All registered implementations of a given codec id. + pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] { + self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[]) + } + + /// Lookup the encoder options schema for a registered codec. Walks + /// implementations in registration order and returns the first + /// schema found. `None` means either the codec isn't registered or + /// no implementation declared an encoder schema. + pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> { + self.impls + .get(id)? + .iter() + .find_map(|i| i.encoder_options_schema) + } + + /// Lookup the decoder options schema — see + /// [`encoder_options_schema`](Self::encoder_options_schema). + pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> { + self.impls + .get(id)? + .iter() + .find_map(|i| i.decoder_options_schema) + } + + /// Iterator over every (codec_id, impl) pair — useful for `oxideav list` + /// to show capability flags per implementation. + pub fn all_implementations(&self) -> impl Iterator { + self.impls + .iter() + .flat_map(|(id, v)| v.iter().map(move |i| (id, i))) + } + + /// Iterator over every `(tag, codec_id)` pair currently registered — + /// used by `oxideav tags` debug output and by tests that want to + /// walk the tag surface. + pub fn all_tag_registrations(&self) -> impl Iterator { + self.tag_index.iter().flat_map(move |(tag, idxs)| { + idxs.iter().map(move |&i| (tag, &self.registrations[i].id)) + }) + } + + /// Inherent form of tag resolution that returns a reference. + /// The owned-value form used by container code lives behind the + /// [`CodecResolver`] trait impl below. + /// + /// Walks every registration that claimed `ctx.tag`, calls its + /// probe with `ctx`, and returns the id of the registration that + /// scored highest. Probes that return `0.0` are discarded; ties + /// on confidence are broken by registration order (first wins). + /// Registrations with no probe are treated as returning `1.0`. + pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> { + let idxs = self.tag_index.get(ctx.tag)?; + let mut best: Option<(f32, usize)> = None; + for &i in idxs { + let rec = &self.registrations[i]; + let conf = match rec.probe { + Some(f) => f(ctx), + None => 1.0, + }; + if conf <= 0.0 { + continue; + } + best = match best { + None => Some((conf, i)), + Some((bc, _)) if conf > bc => Some((conf, i)), + other => other, + }; + } + best.map(|(_, i)| &self.registrations[i].id) + } + + /// Inherent form of payload-magic resolution that returns a + /// reference. The owned-value form used by container code lives + /// behind the [`CodecResolver`] trait impl below. + /// + /// Walks every registered payload magic prefix (declared via + /// [`CodecInfo::payload_magic`] / [`CodecInfo::payload_magics`]) and + /// returns the codec whose magic is a prefix of `first_bytes` — + /// however much of the stream's leading payload the caller has + /// (an Ogg demuxer passes the first packet of a logical stream; a + /// raw-stream prober passes the file head). The **longest** + /// matching magic wins (most specific claim); remaining ties are + /// broken by registration order (first wins). Unlike the tag path + /// there is no probe step: a payload magic is itself the bitstream + /// evidence a probe would look for, and specificity is expressed + /// by prefix length instead of a confidence value. + pub fn resolve_payload_magic_ref(&self, first_bytes: &[u8]) -> Option<&CodecId> { + let mut best: Option<(usize, usize)> = None; // (magic_len, reg idx) + for (magic, idx) in &self.magic_index { + if !first_bytes.starts_with(magic) { + continue; + } + // Strict `>` keeps the earlier registration on equal + // lengths — `magic_index` is in registration order. + best = match best { + None => Some((magic.len(), *idx)), + Some((len, _)) if magic.len() > len => Some((magic.len(), *idx)), + other => other, + }; + } + best.map(|(_, i)| &self.registrations[i].id) + } + + /// Iterator over every `(payload magic, codec_id)` pair currently + /// registered, in registration order — the payload-magic companion + /// to [`all_tag_registrations`](Self::all_tag_registrations). + pub fn all_payload_magic_registrations(&self) -> impl Iterator { + self.magic_index + .iter() + .map(move |(magic, i)| (magic.as_slice(), &self.registrations[*i].id)) + } +} + +/// Implement the shared [`CodecResolver`] interface so container +/// demuxers can accept `&dyn CodecResolver` without depending on +/// this crate directly — the trait lives in oxideav-core. +impl CodecResolver for CodecRegistry { + fn resolve_tag(&self, ctx: &ProbeContext) -> Option { + self.resolve_tag_ref(ctx).cloned() + } + + fn resolve_payload_magic(&self, first_packet: &[u8]) -> Option { + self.resolve_payload_magic_ref(first_packet).cloned() + } +} + +#[cfg(test)] +mod tag_tests { + use super::*; + use crate::CodecCapabilities; + + /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no + /// 0x000001 start code in the first few bytes). + fn probe_msmpeg4(ctx: &ProbeContext) -> f32 { + match ctx.packet { + Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0, + Some(_) => 0.0, + None => 0.5, // no data yet — weak evidence + } + } + + /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2 + /// (starts with a 0x000001 start code in the first few bytes). + fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 { + match ctx.packet { + Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0, + Some(_) => 0.0, + None => 0.5, + } + } + + fn info(id: &str) -> CodecInfo { + CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id)) + } + + #[test] + fn resolve_single_claim_no_probe() { + let mut reg = CodecRegistry::new(); + reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC"))); + let t = CodecTag::fourcc(b"FLAC"); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&t)) + .map(|c| c.as_str()), + Some("flac"), + ); + } + + #[test] + fn resolve_missing_tag_returns_none() { + let reg = CodecRegistry::new(); + let t = CodecTag::fourcc(b"????"); + assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none()); + } + + #[test] + fn unprobed_claims_tie_first_registered_wins() { + // Two unprobed claims on the same tag: deterministic order. + let mut reg = CodecRegistry::new(); + reg.register(info("first").tag(CodecTag::fourcc(b"TEST"))); + reg.register(info("second").tag(CodecTag::fourcc(b"TEST"))); + let t = CodecTag::fourcc(b"TEST"); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&t)) + .map(|c| c.as_str()), + Some("first"), + ); + } + + #[test] + fn probe_picks_matching_bitstream() { + // The core bug fix: every probe is asked and the highest + // confidence wins regardless of registration order. + let mut reg = CodecRegistry::new(); + reg.register( + info("msmpeg4v3") + .probe(probe_msmpeg4) + .tag(CodecTag::fourcc(b"DIV3")), + ); + reg.register( + info("mpeg4video") + .probe(probe_mpeg4_part2) + .tag(CodecTag::fourcc(b"DIV3")), + ); + + let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00]; + let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2]; + let tag = CodecTag::fourcc(b"DIV3"); + + let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2); + assert_eq!( + reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()), + Some("mpeg4video"), + ); + let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4); + assert_eq!( + reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()), + Some("msmpeg4v3"), + ); + } + + #[test] + fn unprobed_claim_wins_against_low_confidence_probe() { + // One codec claims a tag without a probe (→ confidence 1.0) + // and another claims it with a probe returning 0.3. The + // unprobed one wins — a codec that knows it owns the tag + // outright should not lose to a speculative probe. + let mut reg = CodecRegistry::new(); + reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_"))); + reg.register( + info("speculative") + .probe(|_| 0.3) + .tag(CodecTag::fourcc(b"OWN_")), + ); + let t = CodecTag::fourcc(b"OWN_"); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&t)) + .map(|c| c.as_str()), + Some("owner"), + ); + } + + #[test] + fn probe_returning_zero_is_skipped() { + let mut reg = CodecRegistry::new(); + reg.register( + info("refuses") + .probe(|_| 0.0) + .tag(CodecTag::fourcc(b"MAYB")), + ); + reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB"))); + let t = CodecTag::fourcc(b"MAYB"); + let ctx = ProbeContext::new(&t).packet(b"hello"); + assert_eq!( + reg.resolve_tag_ref(&ctx).map(|c| c.as_str()), + Some("fallback"), + ); + } + + #[test] + fn fourcc_case_insensitive_lookup() { + let mut reg = CodecRegistry::new(); + reg.register(info("vid").tag(CodecTag::fourcc(b"div3"))); + // Registered as "DIV3" (uppercase via ctor); lookup using + // lowercase / mixed case also hits. + let upper = CodecTag::fourcc(b"DIV3"); + let lower = CodecTag::fourcc(b"div3"); + let mixed = CodecTag::fourcc(b"DiV3"); + assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some()); + assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some()); + assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some()); + } + + #[test] + fn wave_format_and_matroska_tags_work() { + let mut reg = CodecRegistry::new(); + reg.register(info("mp3").tag(CodecTag::wave_format(0x0055))); + reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC"))); + let wf = CodecTag::wave_format(0x0055); + let mk = CodecTag::matroska("V_MPEG4/ISO/AVC"); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&wf)) + .map(|c| c.as_str()), + Some("mp3"), + ); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&mk)) + .map(|c| c.as_str()), + Some("h264"), + ); + } + + #[test] + fn mp4_object_type_tag_works() { + let mut reg = CodecRegistry::new(); + reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40))); + let t = CodecTag::mp4_object_type(0x40); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&t)) + .map(|c| c.as_str()), + Some("aac"), + ); + } + + #[test] + fn multi_tag_claim_all_resolve() { + let mut reg = CodecRegistry::new(); + reg.register(info("aac").tags([ + CodecTag::fourcc(b"MP4A"), + CodecTag::wave_format(0x00FF), + CodecTag::mp4_object_type(0x40), + CodecTag::matroska("A_AAC"), + ])); + for t in [ + CodecTag::fourcc(b"MP4A"), + CodecTag::wave_format(0x00FF), + CodecTag::mp4_object_type(0x40), + CodecTag::matroska("A_AAC"), + ] { + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&t)) + .map(|c| c.as_str()), + Some("aac"), + "tag {t:?} did not resolve", + ); + } + } +} + +#[cfg(test)] +mod payload_magic_tests { + use super::*; + use crate::CodecCapabilities; + + fn info(id: &str) -> CodecInfo { + CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id)) + } + + /// Registry with the classic Ogg family registered, each under its + /// real BOS magic. + fn ogg_family_registry() -> CodecRegistry { + let mut reg = CodecRegistry::new(); + reg.register(info("vorbis").payload_magic(b"\x01vorbis")); + reg.register(info("opus").payload_magic(b"OpusHead")); + reg.register(info("theora").payload_magic(b"\x80theora")); + reg.register(info("flac").payload_magic(b"\x7fFLAC")); + reg + } + + #[test] + fn resolve_payload_magic_matches_first_packet_prefix() { + let reg = ogg_family_registry(); + // A Vorbis identification header: magic + version + channels + + // rate + ... — the resolver only needs the prefix to match. + let vorbis_id_header = b"\x01vorbis\x00\x00\x00\x00\x02\x44\xac\x00\x00"; + assert_eq!( + reg.resolve_payload_magic_ref(vorbis_id_header) + .map(|c| c.as_str()), + Some("vorbis"), + ); + assert_eq!( + reg.resolve_payload_magic_ref(b"OpusHead\x01\x02\x38\x01") + .map(|c| c.as_str()), + Some("opus"), + ); + assert_eq!( + reg.resolve_payload_magic_ref(b"\x80theora\x03\x02\x01") + .map(|c| c.as_str()), + Some("theora"), + ); + assert_eq!( + reg.resolve_payload_magic_ref(b"\x7fFLAC\x01\x00") + .map(|c| c.as_str()), + Some("flac"), + ); + } + + #[test] + fn resolve_payload_magic_exact_length_packet_matches() { + // A packet that is exactly the magic (nothing after it) still + // resolves — starts_with is inclusive of equality. + let reg = ogg_family_registry(); + assert_eq!( + reg.resolve_payload_magic_ref(b"OpusHead") + .map(|c| c.as_str()), + Some("opus"), + ); + } + + #[test] + fn resolve_payload_magic_unknown_or_short_packet_is_none() { + let reg = ogg_family_registry(); + // Unknown magic. + assert!(reg.resolve_payload_magic_ref(b"Speex 1.2.0").is_none()); + // Packet shorter than every registered magic. + assert!(reg.resolve_payload_magic_ref(b"Opus").is_none()); + // Empty packet. + assert!(reg.resolve_payload_magic_ref(b"").is_none()); + } + + #[test] + fn resolve_payload_magic_longest_prefix_wins_regardless_of_order() { + // A shorter magic that is itself a prefix of a longer one must + // lose to the more specific claim, whichever registered first. + let mut reg = CodecRegistry::new(); + reg.register(info("generic").payload_magic(b"Opus")); + reg.register(info("opus").payload_magic(b"OpusHead")); + assert_eq!( + reg.resolve_payload_magic_ref(b"OpusHead\x01") + .map(|c| c.as_str()), + Some("opus"), + ); + // ...but the shorter claim still wins packets only it matches. + assert_eq!( + reg.resolve_payload_magic_ref(b"OpusTags") + .map(|c| c.as_str()), + Some("generic"), + ); + + // Same result with the registration order flipped. + let mut reg = CodecRegistry::new(); + reg.register(info("opus").payload_magic(b"OpusHead")); + reg.register(info("generic").payload_magic(b"Opus")); + assert_eq!( + reg.resolve_payload_magic_ref(b"OpusHead\x01") + .map(|c| c.as_str()), + Some("opus"), + ); + } + + #[test] + fn resolve_payload_magic_equal_length_tie_first_registered_wins() { + let mut reg = CodecRegistry::new(); + reg.register(info("first").payload_magic(b"SameMagic")); + reg.register(info("second").payload_magic(b"SameMagic")); + assert_eq!( + reg.resolve_payload_magic_ref(b"SameMagic\x00") + .map(|c| c.as_str()), + Some("first"), + ); + } + + #[test] + fn empty_payload_magic_is_ignored_at_registration() { + let mut reg = CodecRegistry::new(); + reg.register(info("greedy").payload_magic(b"")); + assert!(reg.resolve_payload_magic_ref(b"anything at all").is_none()); + assert_eq!(reg.all_payload_magic_registrations().count(), 0); + } + + #[test] + fn payload_magics_plural_builder_and_diagnostics() { + // One codec answering to several magics via the iterable + // builder; the diagnostic iterator surfaces each claim in + // registration order. + let mut reg = CodecRegistry::new(); + reg.register(info("speex").payload_magics([b"Speex ".to_vec(), b"speex-alt".to_vec()])); + assert_eq!( + reg.resolve_payload_magic_ref(b"Speex 1.2") + .map(|c| c.as_str()), + Some("speex"), + ); + assert_eq!( + reg.resolve_payload_magic_ref(b"speex-alt\x00") + .map(|c| c.as_str()), + Some("speex"), + ); + let all: Vec<(&[u8], &str)> = reg + .all_payload_magic_registrations() + .map(|(m, id)| (m, id.as_str())) + .collect(); + assert_eq!( + all, + vec![ + (b"Speex ".as_slice(), "speex"), + (b"speex-alt".as_slice(), "speex"), + ], + ); + } + + #[test] + fn magic_claims_compose_with_tag_claims_on_one_registration() { + // A codec that lives in both Ogg and Matroska declares both + // claim kinds on one CodecInfo; each resolution path finds it. + let mut reg = CodecRegistry::new(); + reg.register( + info("vorbis") + .tag(CodecTag::matroska("A_VORBIS")) + .payload_magic(b"\x01vorbis"), + ); + let mk = CodecTag::matroska("A_VORBIS"); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&mk)) + .map(|c| c.as_str()), + Some("vorbis"), + ); + assert_eq!( + reg.resolve_payload_magic_ref(b"\x01vorbis\x00") + .map(|c| c.as_str()), + Some("vorbis"), + ); + } + + #[test] + fn resolver_trait_payload_magic_surface() { + // The owned-value trait form mirrors the inherent form, and + // the default implementation (NullCodecResolver) resolves + // nothing. + let reg = ogg_family_registry(); + let resolver: &dyn CodecResolver = ® + assert_eq!( + resolver + .resolve_payload_magic(b"OpusHead\x01") + .map(|c| c.0.clone()), + Some("opus".to_owned()), + ); + assert!(resolver.resolve_payload_magic(b"unknown").is_none()); + + let null = crate::NullCodecResolver; + assert!(null.resolve_payload_magic(b"OpusHead\x01").is_none()); + } + + /// The surface is container-agnostic: a raw elementary stream + /// identified by a file-head magic resolves through the same path + /// as the Ogg family — nothing about the mechanism is Ogg-shaped. + #[test] + fn payload_magic_serves_non_ogg_carriage() { + let mut reg = CodecRegistry::new(); + reg.register(info("flac").payload_magic(b"fLaC")); + reg.register(info("shorten").payload_magic(b"ajkg")); + + assert_eq!( + reg.resolve_payload_magic_ref(b"fLaC\x00\x00\x00\x22"), + Some(&CodecId::new("flac")) + ); + assert_eq!( + reg.resolve_payload_magic_ref(b"ajkg\x02"), + Some(&CodecId::new("shorten")) + ); + assert_eq!(reg.resolve_payload_magic_ref(b"RIFF"), None); + } +} + +#[cfg(test)] +mod engine_tests { + use super::*; + use crate::engine::HwDeviceInfo; + + #[test] + fn codec_info_engine_id_and_probe_default_to_none() { + let ci = CodecInfo::new(CodecId::new("h264")); + assert!(ci.engine_id.is_none()); + assert!(ci.engine_probe.is_none()); + } + + #[test] + fn codec_info_engine_builder_methods_set_fields() { + fn dummy_probe() -> Vec { + vec![] + } + let ci = CodecInfo::new(CodecId::new("h264")) + .with_engine_id("nvidia") + .with_engine_probe(dummy_probe); + assert_eq!(ci.engine_id, Some("nvidia")); + assert!(ci.engine_probe.is_some()); + let probe = ci.engine_probe.unwrap(); + let result = probe(); + assert!(result.is_empty()); + } + + #[test] + fn registering_codec_with_engine_metadata_does_not_panic() { + // The new fields are passthrough metadata — register() should + // accept them without affecting existing id/tag bookkeeping. + fn dummy_probe() -> Vec { + vec![] + } + let mut reg = CodecRegistry::new(); + reg.register( + CodecInfo::new(CodecId::new("h264")) + .capabilities(CodecCapabilities::audio("h264_nvdec")) + .tag(CodecTag::fourcc(b"H264")) + .with_engine_id("nvidia") + .with_engine_probe(dummy_probe), + ); + let t = CodecTag::fourcc(b"H264"); + assert_eq!( + reg.resolve_tag_ref(&ProbeContext::new(&t)) + .map(|c| c.as_str()), + Some("h264"), + ); + } + + /// No-op decoder factory so the registration produces a real + /// CodecImplementation (the registry skips tag-only entries — + /// without a factory there'd be nothing in `implementations()` + /// to assert against). + fn dummy_decoder_factory( + _params: &crate::CodecParameters, + ) -> crate::Result> { + Err(crate::Error::unsupported("dummy decoder")) + } + + #[test] + fn engine_metadata_propagates_through_register() { + fn dummy_probe() -> Vec { + vec![] + } + let mut reg = CodecRegistry::default(); + reg.register( + CodecInfo::new(CodecId::new("h264")) + .capabilities(CodecCapabilities::video("h264_test")) + .decoder(dummy_decoder_factory) + .with_engine_id("test-backend") + .with_engine_probe(dummy_probe), + ); + let impls = reg.implementations(&CodecId::new("h264")); + assert_eq!(impls.len(), 1); + assert_eq!(impls[0].engine_id, Some("test-backend")); + assert!(impls[0].engine_probe.is_some()); + } + + #[test] + fn engine_metadata_absent_for_sw_codecs() { + // SW codecs don't call the engine builders — both fields + // should land as None on the resulting CodecImplementation. + let mut reg = CodecRegistry::default(); + reg.register( + CodecInfo::new(CodecId::new("flac")) + .capabilities(CodecCapabilities::audio("flac_sw")) + .decoder(dummy_decoder_factory), + ); + let impls = reg.implementations(&CodecId::new("flac")); + assert_eq!(impls.len(), 1); + assert!(impls[0].engine_id.is_none()); + assert!(impls[0].engine_probe.is_none()); + } +} diff --git a/crates/vendor/oxideav-core/src/registry/container.rs b/crates/vendor/oxideav-core/src/registry/container.rs new file mode 100644 index 00000000..fc8a9e2f --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/container.rs @@ -0,0 +1,361 @@ +//! Container traits (demuxer + muxer) and a registry. +//! +//! This module defines the abstract [`Demuxer`] / [`Muxer`] traits that +//! every container implementation (oxideav-mp4, oxideav-mkv, +//! oxideav-flac, oxideav-ogg, …) fulfils, plus a +//! [`ContainerRegistry`] that consumers of the framework use to pick a +//! demuxer by probe bytes or filename hint. + +use std::collections::HashMap; +use std::io::{Read, Seek, SeekFrom, Write}; + +use crate::{CodecResolver, Error, Packet, Result, StreamInfo}; + +// ───────────────────────── traits ───────────────────────── + +/// Reads a container and emits packets per stream. +pub trait Demuxer: Send { + /// Name of the container format (e.g., `"wav"`). + fn format_name(&self) -> &str; + + /// Streams in this container. Stable across the lifetime of the demuxer. + fn streams(&self) -> &[StreamInfo]; + + /// Read the next packet from any stream. Returns `Error::Eof` at end. + fn next_packet(&mut self) -> Result; + + /// Hint that only the listed stream indices will be consumed by the + /// pipeline. Demuxers that can efficiently skip inactive streams at + /// the container level (e.g., MKV cluster-aware, MP4 trak-aware) + /// should override this. The default is a no-op — the pipeline + /// drops unwanted packets on the floor. + fn set_active_streams(&mut self, _indices: &[u32]) {} + + /// Seek to the nearest keyframe at or before `pts` (in the given + /// stream's time base). Returns the actual timestamp seeked to, or + /// `Error::Unsupported` if this demuxer can't seek. + fn seek_to(&mut self, _stream_index: u32, _pts: i64) -> Result { + Err(Error::unsupported("this demuxer does not support seeking")) + } + + /// Container-level metadata as ordered (key, value) pairs. + /// Keys follow a loose convention borrowed from Vorbis comments: + /// `title`, `artist`, `album`, `comment`, `date`, `sample_name:`, + /// `channels`, `n_patterns`, etc. Demuxers that carry no metadata + /// return an empty slice (the default). + fn metadata(&self) -> &[(String, String)] { + &[] + } + /// Container-level duration, if known. Default is `None` — callers + /// may fall back to the longest per-stream duration. Expressed as + /// microseconds for portability; convert to seconds at the edge. + fn duration_micros(&self) -> Option { + None + } + + /// Attached pictures (cover art, artist photos, ...) embedded in + /// the container. Returns an empty slice (the default) when the + /// container carries none or doesn't support them. Containers that + /// do — ID3v2 on MP3, `METADATA_BLOCK_PICTURE` on FLAC, `covr` + /// atoms on MP4, etc. — override this to expose the images. + fn attached_pictures(&self) -> &[crate::AttachedPicture] { + &[] + } + + /// Structured chapter / cue list. Default returns an empty slice + /// for back-compat; demuxers that carry chapters (MKV `Chapters`, + /// MP4 chapter track, Ogg `CHAPTERnn=` Vorbis comments, …) should + /// override and return [`Chapter`](crate::Chapter) records in + /// presentation order. Coexists with the legacy `chapter:N:*` + /// flat-metadata keys; new consumers should prefer this. + fn chapters(&self) -> &[crate::Chapter] { + &[] + } + + /// Structured attachment list. Default returns an empty slice for + /// back-compat; demuxers that carry attachments (MKV `Attachments`, + /// …) should override and return [`Attachment`](crate::Attachment) + /// records in container order. Coexists with the legacy + /// `attachment:N:*` flat-metadata keys; new consumers should prefer + /// this. + fn attachments(&self) -> &[crate::Attachment] { + &[] + } +} + +/// Writes packets into a container. +pub trait Muxer: Send { + /// Registered name of the container format being written. + fn format_name(&self) -> &str; + + /// Write the container header. Must be called after stream configuration + /// and before the first `write_packet`. + fn write_header(&mut self) -> Result<()>; + + /// Write one compressed packet into the container. + fn write_packet(&mut self, packet: &Packet) -> Result<()>; + + /// Finalize the file (write index, patch in total sizes, etc.). + fn write_trailer(&mut self) -> Result<()>; +} + +/// Factory that tries to open a stream as a particular container format. +/// +/// Implementations should read the minimum needed to confirm the format and +/// return `Error::InvalidData` if the stream is not in this format. +/// +/// The `codecs` parameter carries a resolver that converts container- +/// level codec tags (FourCCs, WAVEFORMATEX wFormatTag, Matroska +/// CodecIDs, …) into [`CodecId`](crate::CodecId) values. +pub type OpenDemuxerFn = + fn(input: Box, codecs: &dyn CodecResolver) -> Result>; + +/// Factory that creates a muxer for a set of streams. +pub type OpenMuxerFn = + fn(output: Box, streams: &[StreamInfo]) -> Result>; + +/// Information passed to a content-based [`ContainerProbeFn`]. +/// +/// `buf` holds the first few KB of the input — enough to recognise the +/// magic bytes of any container we know about. `ext` carries the file +/// extension as a hint (lowercase, no leading dot); some containers +/// (raw MP3 with no ID3v2, headerless tracker formats) need it to break +/// ties with otherwise weak signatures. +pub struct ProbeData<'a> { + /// First few KB of the input, for magic-byte matching. + pub buf: &'a [u8], + /// File-extension hint (lowercase, no leading dot), when known. + pub ext: Option<&'a str>, +} + +/// Confidence score returned by a [`ContainerProbeFn`]. `0` means no match. +/// Higher means more certain. Conventional values: +/// +/// * `100` – unambiguous magic bytes at a known offset +/// * `75` – signature match corroborated by file extension +/// * `50` – signature match without extension corroboration +/// * `25` – extension match only (no content signature available) +pub type ProbeScore = u8; + +/// Maximum probe score (alias for `100`). +pub const MAX_PROBE_SCORE: ProbeScore = 100; +/// Default score returned when only the file extension matches. +pub const PROBE_SCORE_EXTENSION: ProbeScore = 25; + +/// Content-based format detection function. +/// +/// Returns a [`ProbeScore`] in `0..=100`. Implementations should be +/// pure (no I/O, no allocation beyond the stack) and fast — they may +/// be invoked once per registered demuxer on every input file. +pub type ContainerProbeFn = fn(probe: &ProbeData) -> ProbeScore; + +/// Convenience trait bundle for seekable readers. +pub trait ReadSeek: Read + Seek + Send {} +impl ReadSeek for T {} + +/// Convenience trait bundle for seekable writers. +pub trait WriteSeek: Write + Seek + Send {} +impl WriteSeek for T {} + +// ───────────────────────── ContainerRegistry ───────────────────────── + +/// Registry of container formats: demuxer/muxer factories keyed by +/// format name, plus the extension map and content probes that back +/// input auto-detection. +#[derive(Default)] +pub struct ContainerRegistry { + demuxers: HashMap, + muxers: HashMap, + /// Lowercase file extension → container name (e.g. "wav" → "wav"). + extensions: HashMap, + /// Container name → content-probe function. Optional — containers + /// without a probe still work but require an extension hint or an + /// explicit format name. + probes: HashMap, +} + +impl ContainerRegistry { + /// An empty registry (same as `Default`). + pub fn new() -> Self { + Self::default() + } + + /// Register a demuxer factory under a container format name. + pub fn register_demuxer(&mut self, name: &str, open: OpenDemuxerFn) { + self.demuxers.insert(name.to_owned(), open); + } + + /// Register a muxer factory under a container format name. + pub fn register_muxer(&mut self, name: &str, open: OpenMuxerFn) { + self.muxers.insert(name.to_owned(), open); + } + + /// Map a file extension (case-insensitive) to a registered + /// container name, for extension-hint lookups. + pub fn register_extension(&mut self, ext: &str, container_name: &str) { + self.extensions + .insert(ext.to_lowercase(), container_name.to_owned()); + } + + /// Attach a content-based probe to a registered demuxer. Called by + /// the registry's [`probe_input`](Self::probe_input) to detect the + /// container format from the first few KB of an input stream. + pub fn register_probe(&mut self, container_name: &str, probe: ContainerProbeFn) { + self.probes.insert(container_name.to_owned(), probe); + } + + /// Iterate the registered demuxer format names (arbitrary order). + pub fn demuxer_names(&self) -> impl Iterator { + self.demuxers.keys().map(|s| s.as_str()) + } + + /// Iterate the registered muxer format names (arbitrary order). + pub fn muxer_names(&self) -> impl Iterator { + self.muxers.keys().map(|s| s.as_str()) + } + + /// Open a demuxer explicitly by format name. The `codecs` resolver + /// is passed through to the demuxer so it can translate the + /// container's in-stream codec tags (FourCCs / wFormatTag / + /// Matroska CodecIDs) into [`CodecId`](crate::CodecId) + /// values. Demuxers that don't need tag resolution can ignore it. + pub fn open_demuxer( + &self, + name: &str, + input: Box, + codecs: &dyn CodecResolver, + ) -> Result> { + let open = self + .demuxers + .get(name) + .ok_or_else(|| Error::FormatNotFound(name.to_owned()))?; + open(input, codecs) + } + + /// Open a muxer by format name. + pub fn open_muxer( + &self, + name: &str, + output: Box, + streams: &[StreamInfo], + ) -> Result> { + let open = self + .muxers + .get(name) + .ok_or_else(|| Error::FormatNotFound(name.to_owned()))?; + open(output, streams) + } + + /// Look up a container name from a file extension (no leading dot). + pub fn container_for_extension(&self, ext: &str) -> Option<&str> { + self.extensions.get(&ext.to_lowercase()).map(|s| s.as_str()) + } + + /// Detect the container format by reading the first ~256 KB of the + /// input, scoring each registered probe, and returning the highest- + /// scoring container's name. The extension is passed to probes as a + /// hint — they may use it to break ties when their signature is weak. + /// + /// Falls back to the extension table if no probe scores above zero. + /// The input cursor is restored to its starting position on success + /// and on the I/O failure paths that allow it. + pub fn probe_input(&self, input: &mut dyn ReadSeek, ext_hint: Option<&str>) -> Result { + const PROBE_BUF_SIZE: usize = 256 * 1024; + + let saved_pos = input.stream_position()?; + input.seek(SeekFrom::Start(0))?; + let mut buf = vec![0u8; PROBE_BUF_SIZE]; + let mut got = 0; + while got < buf.len() { + match input.read(&mut buf[got..]) { + Ok(0) => break, + Ok(n) => got += n, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => { + let _ = input.seek(SeekFrom::Start(saved_pos)); + return Err(e.into()); + } + } + } + buf.truncate(got); + input.seek(SeekFrom::Start(saved_pos))?; + + let ext_lower = ext_hint.map(|s| s.to_ascii_lowercase()); + let probe_data = ProbeData { + buf: &buf, + ext: ext_lower.as_deref(), + }; + + let mut best: Option<(&str, ProbeScore)> = None; + for (name, probe) in &self.probes { + let score = probe(&probe_data); + if score == 0 { + continue; + } + match best { + Some((_, prev)) if score <= prev => {} + _ => best = Some((name.as_str(), score)), + } + } + if let Some((name, _)) = best { + return Ok(name.to_owned()); + } + + // Fall back to extension lookup with the conventional weak score. + if let Some(ext) = ext_hint { + if let Some(name) = self.container_for_extension(ext) { + let _ = PROBE_SCORE_EXTENSION; // export retained for symmetry + return Ok(name.to_owned()); + } + } + + Err(Error::FormatNotFound( + "no registered demuxer recognises this input".into(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct DummyDemuxer; + + impl Demuxer for DummyDemuxer { + fn format_name(&self) -> &str { + "dummy" + } + fn streams(&self) -> &[StreamInfo] { + &[] + } + fn next_packet(&mut self) -> Result { + Err(Error::Eof) + } + } + + #[test] + fn default_seek_to_is_unsupported() { + let mut d = DummyDemuxer; + match d.seek_to(0, 0) { + Err(Error::Unsupported(_)) => {} + other => panic!( + "expected default seek_to to return Unsupported, got {:?}", + other + ), + } + } + + #[test] + fn default_chapters_and_attachments_are_empty() { + // A demuxer that overrides nothing must compile and return + // empty slices for both structured accessors. This is the + // back-compat contract that lets every existing demuxer pick + // up the new API without source changes. + let d = DummyDemuxer; + assert!(d.chapters().is_empty()); + assert!(d.attachments().is_empty()); + assert!(d.attached_pictures().is_empty()); + assert!(d.metadata().is_empty()); + assert_eq!(d.duration_micros(), None); + } +} diff --git a/crates/vendor/oxideav-core/src/registry/context.rs b/crates/vendor/oxideav-core/src/registry/context.rs new file mode 100644 index 00000000..d24ed710 --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/context.rs @@ -0,0 +1,39 @@ +//! Unified runtime registration context. +//! +//! [`RuntimeContext`] bundles every registry the framework needs into a +//! single value that consumers pass around (codec/container/source/ +//! filter). Sibling crates expose a uniform `register(&mut +//! RuntimeContext)` entry point that installs themselves into the +//! relevant sub-registry. + +use super::codec::CodecRegistry; +use super::container::ContainerRegistry; +use super::filter::FilterRegistry; +use super::source::SourceRegistry; + +/// Aggregate of every registry the framework consumes. +/// +/// Every sibling crate that contributes implementations exposes +/// `pub fn register(ctx: &mut RuntimeContext)` to install itself. +/// Construct with [`RuntimeContext::new`] for an empty context, then +/// call each sibling's `register` to fill it in. +#[derive(Default)] +pub struct RuntimeContext { + /// Codec registry (decoder/encoder factories, tags, probes). + pub codecs: CodecRegistry, + /// Container registry (demuxer/muxer factories, extensions, probes). + pub containers: ContainerRegistry, + /// Source registry (URL-scheme / device input drivers). + pub sources: SourceRegistry, + /// Stream-filter registry (frame-to-frame transforms). + pub filters: FilterRegistry, +} + +impl RuntimeContext { + /// Empty context — no codecs, no containers, no source schemes, no + /// filters. Sibling crates fill in the four sub-registries via + /// their `register(&mut RuntimeContext)` entry points. + pub fn new() -> Self { + Self::default() + } +} diff --git a/crates/vendor/oxideav-core/src/registry/filter.rs b/crates/vendor/oxideav-core/src/registry/filter.rs new file mode 100644 index 00000000..73eab7da --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/filter.rs @@ -0,0 +1,77 @@ +//! Named-filter registry. +//! +//! This is the registry side of the filter pipeline: given a filter +//! name + JSON params + upstream [`PortSpec`]s, construct a +//! [`StreamFilter`] ready to wire into the pipeline. Concrete filter +//! factories (Volume, Blur, Resize, …) live in their own crates +//! (`oxideav-audio-filter`, `oxideav-image-filter`) and register +//! themselves into a [`FilterRegistry`] via their +//! `register(&mut RuntimeContext)` entry point. + +use std::collections::HashMap; + +use serde_json::Value; + +use crate::{filter::unknown_filter_error, PortSpec, Result, StreamFilter}; + +/// Factory for a named filter. The registry invokes this with the +/// caller-supplied JSON `params` and the input port specs resolved +/// from upstream. Factories are free to inspect the upstream port +/// params — e.g. a resampler reads `inputs[0]` to learn the source +/// sample rate. +pub type FilterFactory = + Box Result> + Send + Sync>; + +/// Named-filter registry. Construct with [`FilterRegistry::new`] (empty); +/// concrete filter crates populate it through their `register` entry +/// points. +#[derive(Default)] +pub struct FilterRegistry { + factories: HashMap, +} + +impl FilterRegistry { + /// Empty registry — no filters resolvable until [`register`](Self::register) + /// is called. + pub fn new() -> Self { + Self::default() + } + + /// Register a factory under `name`. Overwrites any existing entry + /// with the same name — last write wins. + pub fn register(&mut self, name: &str, factory: FilterFactory) { + self.factories.insert(name.to_string(), factory); + } + + /// True when a filter is registered under `name`. + pub fn contains(&self, name: &str) -> bool { + self.factories.contains_key(name) + } + + /// Instantiate the named filter. Returns an "unknown filter" error + /// for unregistered names. + pub fn make( + &self, + name: &str, + params: &Value, + inputs: &[PortSpec], + ) -> Result> { + let bare = strip_filter_prefix(name); + let factory = self + .factories + .get(bare) + .or_else(|| self.factories.get(name)) + .ok_or_else(|| unknown_filter_error(name))?; + factory(params, inputs) + } +} + +/// Strip `video.`, `v:`, `audio.`, `a:` prefixes — the schema allows +/// them for disambiguation; the registry doesn't care. +fn strip_filter_prefix(name: &str) -> &str { + name.strip_prefix("video.") + .or_else(|| name.strip_prefix("v:")) + .or_else(|| name.strip_prefix("audio.")) + .or_else(|| name.strip_prefix("a:")) + .unwrap_or(name) +} diff --git a/crates/vendor/oxideav-core/src/registry/mod.rs b/crates/vendor/oxideav-core/src/registry/mod.rs new file mode 100644 index 00000000..8b1c5fe4 --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/mod.rs @@ -0,0 +1,26 @@ +//! Framework registries. +//! +//! Codec / container / source / filter implementations register +//! themselves into one of the per-kind registries here. Most consumers +//! interact with the bundle via [`RuntimeContext`]. + +pub mod codec; +pub mod container; +pub mod context; +pub mod filter; +pub mod slice; +pub mod source; + +pub use codec::{ + CodecImplementation, CodecInfo, CodecRegistry, Decoder, DecoderFactory, Encoder, EncoderFactory, +}; +pub use container::{ + ContainerProbeFn, ContainerRegistry, Demuxer, Muxer, OpenDemuxerFn, OpenMuxerFn, ProbeData, + ProbeScore, ReadSeek, WriteSeek, MAX_PROBE_SCORE, PROBE_SCORE_EXTENSION, +}; +pub use context::RuntimeContext; +pub use filter::{FilterFactory, FilterRegistry}; +pub use source::{ + BytesSource, FrameSource, MultiTitleSource, OpenBytesFn, OpenFramesFn, OpenMultiTitleFn, + OpenPacketsFn, PacketSource, SourceOutput, SourceRegistry, +}; diff --git a/crates/vendor/oxideav-core/src/registry/slice.rs b/crates/vendor/oxideav-core/src/registry/slice.rs new file mode 100644 index 00000000..853d93a2 --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/slice.rs @@ -0,0 +1,61 @@ +//! Sibling registration entry-point macro. +//! +//! Each sibling crate that ships a codec / container / filter / source +//! invokes [`crate::register!`] at module scope to declare its +//! `register(ctx)` function as the canonical entry point. The +//! [`oxideav-meta`](https://crates.io/crates/oxideav-meta) crate's +//! build script enumerates the enabled sibling deps in its Cargo.toml +//! and emits a `register_all(ctx)` body that calls the macro-generated +//! entry point of each. +//! +//! The macro is the dispatch contract between sibling and meta. Today +//! it expands to a thin `pub fn __oxideav_entry(ctx)` wrapper around +//! the user-supplied register fn. The macro body is the only place +//! that needs to change if the dispatch transport switches in the +//! future (e.g. add a metadata arg, defer init, async-init, audit +//! hook) — sibling call sites stay untouched. +//! +//! # Standalone opt-out +//! +//! Each sibling's `register!()` call lives behind that crate's +//! default-on `registry` cargo feature. Consumers that want the +//! standalone (no-`oxideav-core`-dep) build path turn the feature off +//! and the macro call disappears. + +/// Declare the canonical sibling entry point. +/// +/// Place at module scope inside the sibling crate, gated behind the +/// crate's `registry` cargo feature so the standalone build path +/// stays decoupled from `oxideav-core`: +/// +/// ```ignore +/// pub fn register(ctx: &mut oxideav_core::RuntimeContext) { +/// /* install factories */ +/// } +/// +/// #[cfg(feature = "registry")] +/// oxideav_core::register!("aac", register); +/// ``` +/// +/// The macro expands to a `pub fn __oxideav_entry(ctx)` wrapper that +/// invokes the supplied function. `oxideav-meta`'s `register_all` +/// calls `crate::__oxideav_entry(ctx)` for each enabled sibling dep. +/// +/// The display-name argument (first literal) is reserved for future +/// dispatch-transport changes; it is currently ignored by the +/// expansion but kept in the macro signature for forward +/// compatibility. +#[macro_export] +macro_rules! register { + ($name:literal, $func:path) => { + /// Canonical entry point invoked by `oxideav_meta::register_all`. + /// + /// Generated by the [`oxideav_core::register!`] macro; do not + /// invoke directly. + #[doc(hidden)] + pub fn __oxideav_entry(ctx: &mut $crate::RuntimeContext) { + let _ = $name; + $func(ctx); + } + }; +} diff --git a/crates/vendor/oxideav-core/src/registry/source.rs b/crates/vendor/oxideav-core/src/registry/source.rs new file mode 100644 index 00000000..35bf2e62 --- /dev/null +++ b/crates/vendor/oxideav-core/src/registry/source.rs @@ -0,0 +1,577 @@ +//! Generic source registry. +//! +//! `SourceRegistry` maps URI schemes (`file`, `http`, `rtmp`, `generate`, +//! …) to opener functions and dispatches `open(uri)` to the right driver. +//! A driver opens a URI as one of four shapes: +//! +//! * [`BytesSource`] — a `Read + Seek` byte stream that downstream code +//! then passes to a container demuxer (the historical shape, used by +//! `file://` and `http(s)://`). +//! * [`PacketSource`] — a producer of already-demuxed [`Packet`]s. Used +//! by transport-layer protocols that do their own demux (RTMP, future +//! SRT / WebRTC). Skips the container layer entirely. +//! * [`FrameSource`] — a producer of already-decoded [`Frame`]s. Used by +//! synthetic generators that emit frames natively, skipping both the +//! container and decoder stages. +//! * [`MultiTitleSource`] — a source that emits N discrete byte streams +//! (titles), one per logical "segment" the source carries +//! (BD-ROM chapters or unique titles, DVD VTS entries, multi-title +//! MKV editions, …). The CLI's `oxideav remux` substitutes a +//! per-title token into a `%s.`-style output-path template, so +//! each title lands in its own output file. +//! +//! The driver picks the variant when it registers; [`SourceRegistry::open`] +//! returns the corresponding [`SourceOutput`] enum so the pipeline +//! executor can branch on the source shape. + +use std::collections::HashMap; +use std::io::{Read, Seek}; + +use crate::{CodecParameters, Error, Frame, Packet, Result, StreamInfo}; + +// ───────────────────────── traits ───────────────────────── + +/// A seekable byte stream (`Read + Seek + Send`). Replaces the historical +/// `Box` opener-return type with a name that mirrors the +/// other source-shape traits in this module. Blanket-implemented for +/// every type that satisfies the bounds, so existing readers (files, +/// `Cursor>`, HTTP-over-Range adapters) work unchanged. +pub trait BytesSource: Read + Seek + Send {} +impl BytesSource for T {} + +/// A producer of already-demuxed [`Packet`]s. +/// +/// Used by transport-layer protocols that perform demux themselves +/// (RTMP, RTSP, …). The pipeline executor consumes packets directly, +/// skipping the container-demux stage that bytes-shape sources go +/// through. +pub trait PacketSource: Send { + /// Streams advertised by this source. Stable across the lifetime of + /// the source. + fn streams(&self) -> &[StreamInfo]; + + /// Read the next packet from any stream. Returns [`Error::Eof`] at + /// end of stream. + fn next_packet(&mut self) -> Result; + + /// Source-level metadata as ordered (key, value) pairs. Default is + /// empty. + fn metadata(&self) -> &[(String, String)] { + &[] + } + + /// Source-level duration in microseconds, if known. Default is + /// `None`. Live sources (RTMP push, etc.) typically return `None`. + fn duration_micros(&self) -> Option { + None + } +} + +/// A source that emits N discrete byte streams ("titles") rather +/// than a single contiguous one. +/// +/// The motivating shape is BD-ROM: a disc contains many *titles* +/// (whole movies, behind-the-scenes featurettes, trailers) and each +/// title can be sliced further into *chapters*. The Blu-ray source +/// driver expresses both shapes through this trait — a URI like +/// `bluray:///path?title=1&chapters=2-5` opens a [`MultiTitleSource`] +/// whose four titles are chapters 2, 3, 4, 5 of disc-title 1; a URI +/// without `?chapters=` opens a [`MultiTitleSource`] with a single +/// title (the autoplay title). DVD-Video, multi-edition MKV, and +/// any other format with explicit segment structure plug in the +/// same way. +/// +/// Downstream callers fan out: each title is opened as its own +/// [`BytesSource`], demuxed independently, and written to its own +/// output path. The CLI's `oxideav remux` substitutes +/// [`Self::title_label`] into a `%s` token in the output-path +/// template so each title lands in a separate file. Other front-ends +/// (`oxideplay bluray://`, a future GUI title-picker, …) can iterate +/// titles the same way. +/// +/// Sources that don't have multi-title structure should keep +/// returning a [`BytesSource`] — there's no benefit to wrapping a +/// single-title file in this trait. +pub trait MultiTitleSource: Send { + /// Number of titles this source emits. Stable for the lifetime + /// of the source — title discovery happens at `open` time, not + /// while streaming. + fn title_count(&self) -> usize; + + /// Open the title at `index` (0-based) as a single-stream + /// [`BytesSource`] the existing container registry can demux. + /// `index` must satisfy `index < self.title_count()`. Calling + /// `open_title` more than once on the same index is allowed — + /// the returned source is a fresh handle each time. + fn open_title(&mut self, index: usize) -> Result>; + + /// Stable per-title identifier substituted into a `%s` token of + /// a templated output path. Examples: `"3"` for chapter 3, + /// `"t01"` for title 1, `"introduction"` for a named edition. + /// Returned values must be filename-safe: ASCII letters / digits + /// / `-` / `_`, no path separators, no whitespace, no leading + /// dot. Calling code is free to additionally sanitise; an empty + /// string is rejected. + fn title_label(&self, index: usize) -> String; + + /// Human-readable display name for the title (e.g. + /// `"Kite Uncut — Director's Cut"`) — used by interactive + /// front-ends to render menus. `None` when the source carries + /// no name. The default returns `None`. + fn title_display_name(&self, index: usize) -> Option { + let _ = index; + None + } + + /// Container-format hint for the title's byte stream + /// (`"mpegts"`, `"matroska"`, `"mp4"`, …). When `Some`, callers + /// can skip the format-detector pass and hand the bytes straight + /// to that demuxer. `None` means "sniff it" — preserves the + /// existing detection path. The default returns `None`. + fn title_container_hint(&self, index: usize) -> Option<&'static str> { + let _ = index; + None + } + + /// Source-level metadata as ordered (key, value) pairs (disc + /// label, BDMT ``, region code, …). Default is empty. + fn metadata(&self) -> &[(String, String)] { + &[] + } +} + +/// A producer of already-decoded [`Frame`]s. +/// +/// Used by synthetic generators (testsrc, sine sweep, gradient image, +/// …) that emit decoded frames natively. The pipeline executor consumes +/// frames directly, skipping both the container-demux and decode stages. +pub trait FrameSource: Send { + /// Codec parameters describing the frames this source emits. Stable + /// across the lifetime of the source. Even though the frames are + /// already decoded, downstream filters and encoders need the + /// parameter shape (sample rate / pixel format / channel layout / + /// frame rate / …) to configure themselves. + fn params(&self) -> &CodecParameters; + + /// Produce the next frame. Returns [`Error::Eof`] at end of stream. + fn next_frame(&mut self) -> Result; + + /// Source-level metadata as ordered (key, value) pairs. Default is + /// empty. + fn metadata(&self) -> &[(String, String)] { + &[] + } + + /// Source-level duration in microseconds, if known. Default is + /// `None`. + fn duration_micros(&self) -> Option { + None + } +} + +/// What a [`SourceRegistry::open`] call returns. The variant is decided +/// at driver-registration time, so callers can match on the shape and +/// branch the pipeline accordingly. +/// +/// **Marked `#[non_exhaustive]`** so a new source kind (e.g. a future +/// `LiveStream` variant) can be added without semver-breaking +/// downstream consumers. Match arms must include a wildcard. +#[non_exhaustive] +pub enum SourceOutput { + /// A raw byte stream — feed it to container probing / a demuxer. + Bytes(Box), + /// Already-demuxed compressed packets — feed them to decoders. + Packets(Box), + /// Already-decoded frames (e.g. a capture device) — feed them to + /// filters / encoders directly. + Frames(Box), + /// A multi-title source (BD-ROM, DVD-Video, multi-edition MKV). + /// Callers fan out: each title is opened independently via + /// [`MultiTitleSource::open_title`], demuxed, and routed to its + /// own output sink. + MultiTitle(Box), +} + +// ───────────────────────── opener function aliases ───────────────────────── + +/// Opener for a [`BytesSource`] driver. +pub type OpenBytesFn = fn(uri: &str) -> Result>; + +/// Opener for a [`PacketSource`] driver. +pub type OpenPacketsFn = fn(uri: &str) -> Result>; + +/// Opener for a [`FrameSource`] driver. +pub type OpenFramesFn = fn(uri: &str) -> Result>; + +/// Opener for a [`MultiTitleSource`] driver. +pub type OpenMultiTitleFn = fn(uri: &str) -> Result>; + +/// Internal per-scheme entry: which opener kind is registered for this +/// scheme. Stored in a single map so [`SourceRegistry::open`] can +/// dispatch with a single lookup, then match the variant to wrap in the +/// returned [`SourceOutput`]. +enum OpenerEntry { + Bytes(OpenBytesFn), + Packets(OpenPacketsFn), + Frames(OpenFramesFn), + MultiTitle(OpenMultiTitleFn), +} + +// ───────────────────────── SourceRegistry ───────────────────────── + +/// Registry mapping URI schemes to opener functions. Each scheme picks +/// one of three opener kinds (bytes / packets / frames) at registration +/// time; callers see the choice via the [`SourceOutput`] variant +/// returned from [`open`](Self::open). +#[derive(Default)] +pub struct SourceRegistry { + schemes: HashMap, +} + +impl SourceRegistry { + /// Empty registry. Callers must register at least one driver before + /// calling [`open`](Self::open). The conventional minimum is the + /// `file` driver (provided by the `oxideav-source` crate). + pub fn new() -> Self { + Self::default() + } + + /// Register a [`BytesSource`] opener for a scheme. Schemes are + /// normalised to ASCII lowercase. Replaces any prior registration + /// (including registrations of other opener kinds). + pub fn register_bytes(&mut self, scheme: &str, opener: OpenBytesFn) { + self.schemes + .insert(scheme.to_ascii_lowercase(), OpenerEntry::Bytes(opener)); + } + + /// Register a [`PacketSource`] opener for a scheme. Schemes are + /// normalised to ASCII lowercase. Replaces any prior registration + /// (including registrations of other opener kinds). + pub fn register_packets(&mut self, scheme: &str, opener: OpenPacketsFn) { + self.schemes + .insert(scheme.to_ascii_lowercase(), OpenerEntry::Packets(opener)); + } + + /// Register a [`FrameSource`] opener for a scheme. Schemes are + /// normalised to ASCII lowercase. Replaces any prior registration + /// (including registrations of other opener kinds). + pub fn register_frames(&mut self, scheme: &str, opener: OpenFramesFn) { + self.schemes + .insert(scheme.to_ascii_lowercase(), OpenerEntry::Frames(opener)); + } + + /// Register a [`MultiTitleSource`] opener for a scheme. Schemes + /// are normalised to ASCII lowercase. Replaces any prior + /// registration (including registrations of other opener kinds). + pub fn register_multi_title(&mut self, scheme: &str, opener: OpenMultiTitleFn) { + self.schemes + .insert(scheme.to_ascii_lowercase(), OpenerEntry::MultiTitle(opener)); + } + + /// Open a URI. The URI's scheme determines which opener runs; bare + /// paths (no scheme) and unrecognised schemes both fall back to the + /// `file` driver if it is registered. + /// + /// Returns a [`SourceOutput`] whose variant matches the registered + /// opener kind: bytes-shape drivers return `SourceOutput::Bytes`, + /// packet-shape drivers return `SourceOutput::Packets`, and so on. + pub fn open(&self, uri_str: &str) -> Result { + let (scheme, _) = split_scheme(uri_str); + let scheme = scheme.to_ascii_lowercase(); + if let Some(entry) = self.schemes.get(&scheme) { + return dispatch(entry, uri_str); + } + // Fall back to file driver for unknown schemes. + if let Some(entry) = self.schemes.get("file") { + return dispatch(entry, uri_str); + } + Err(Error::Unsupported(format!( + "no source driver for scheme '{scheme}' (URI: {uri_str})" + ))) + } + + /// Iterate the registered schemes (for diagnostics). + pub fn schemes(&self) -> impl Iterator { + self.schemes.keys().map(|s| s.as_str()) + } +} + +fn dispatch(entry: &OpenerEntry, uri_str: &str) -> Result { + match entry { + OpenerEntry::Bytes(open) => open(uri_str).map(SourceOutput::Bytes), + OpenerEntry::Packets(open) => open(uri_str).map(SourceOutput::Packets), + OpenerEntry::Frames(open) => open(uri_str).map(SourceOutput::Frames), + OpenerEntry::MultiTitle(open) => open(uri_str).map(SourceOutput::MultiTitle), + } +} + +/// Split a URI into `(scheme, rest)`. Bare paths (no scheme) report scheme +/// `"file"` and `rest = uri`. Path-like inputs that happen to start with +/// `c:` on Windows are treated as bare paths. +pub(crate) fn split_scheme(uri: &str) -> (&str, &str) { + if let Some(idx) = uri.find(':') { + let (scheme, rest) = uri.split_at(idx); + let rest = &rest[1..]; // skip ':' + + // Reject single-letter scheme that looks like a Windows drive letter. + if scheme.len() == 1 && scheme.chars().next().unwrap().is_ascii_alphabetic() { + return ("file", uri); + } + + // Scheme must be ASCII alphanumeric / `+` / `-` / `.`, starting with a letter. + let valid = !scheme.is_empty() + && scheme.chars().next().unwrap().is_ascii_alphabetic() + && scheme + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.')); + + if !valid { + return ("file", uri); + } + + // Strip leading `//` from rest if present. + let rest = rest.strip_prefix("//").unwrap_or(rest); + return (scheme, rest); + } + ("file", uri) +} + +// ───────────────────────── tests ───────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::frame::{AudioFrame, Frame}; + use crate::packet::Packet; + use crate::stream::{CodecId, CodecParameters, StreamInfo}; + use crate::time::TimeBase; + use std::io::{Cursor, Read}; + + // ---- mock BytesSource ---- + fn open_bytes_mock(_uri: &str) -> Result> { + Ok(Box::new(Cursor::new(b"hello world".to_vec()))) + } + + #[test] + fn register_bytes_and_open_returns_bytes_variant() { + let mut reg = SourceRegistry::new(); + reg.register_bytes("mockb", open_bytes_mock); + let out = reg.open("mockb://anything").expect("open"); + match out { + SourceOutput::Bytes(mut r) => { + let mut buf = String::new(); + r.read_to_string(&mut buf).unwrap(); + assert_eq!(buf, "hello world"); + } + _ => panic!("expected SourceOutput::Bytes"), + } + } + + // ---- mock PacketSource ---- + struct MockPacketSource { + streams: Vec, + emitted: bool, + } + + impl MockPacketSource { + fn new() -> Self { + let params = CodecParameters::audio(CodecId::new("pcm_s16le")); + let s = StreamInfo { + index: 0, + time_base: TimeBase::new(1, 1000), + duration: None, + start_time: None, + params, + }; + Self { + streams: vec![s], + emitted: false, + } + } + } + + impl PacketSource for MockPacketSource { + fn streams(&self) -> &[StreamInfo] { + &self.streams + } + fn next_packet(&mut self) -> Result { + if self.emitted { + return Err(Error::Eof); + } + self.emitted = true; + Ok(Packet::new(0, TimeBase::new(1, 1000), vec![1, 2, 3, 4])) + } + } + + fn open_packets_mock(_uri: &str) -> Result> { + Ok(Box::new(MockPacketSource::new())) + } + + #[test] + fn register_packets_and_open_returns_packets_variant() { + let mut reg = SourceRegistry::new(); + reg.register_packets("mockp", open_packets_mock); + let out = reg.open("mockp://anything").expect("open"); + match out { + SourceOutput::Packets(mut p) => { + assert_eq!(p.streams().len(), 1); + let pkt = p.next_packet().expect("first packet"); + assert_eq!(pkt.data, vec![1, 2, 3, 4]); + assert!(matches!(p.next_packet(), Err(Error::Eof))); + } + _ => panic!("expected SourceOutput::Packets"), + } + } + + // ---- mock FrameSource ---- + struct MockFrameSource { + params: CodecParameters, + emitted: bool, + } + + impl MockFrameSource { + fn new() -> Self { + Self { + params: CodecParameters::audio(CodecId::new("pcm_s16le")), + emitted: false, + } + } + } + + impl FrameSource for MockFrameSource { + fn params(&self) -> &CodecParameters { + &self.params + } + fn next_frame(&mut self) -> Result { + if self.emitted { + return Err(Error::Eof); + } + self.emitted = true; + Ok(Frame::Audio(AudioFrame { + samples: 1, + pts: Some(0), + data: vec![vec![0u8, 0u8]], + })) + } + } + + fn open_frames_mock(_uri: &str) -> Result> { + Ok(Box::new(MockFrameSource::new())) + } + + #[test] + fn register_frames_and_open_returns_frames_variant() { + let mut reg = SourceRegistry::new(); + reg.register_frames("mockf", open_frames_mock); + let out = reg.open("mockf://anything").expect("open"); + match out { + SourceOutput::Frames(mut f) => { + assert_eq!(f.params().codec_id.as_str(), "pcm_s16le"); + let frame = f.next_frame().expect("first frame"); + match frame { + Frame::Audio(a) => assert_eq!(a.samples, 1), + _ => panic!("expected audio frame"), + } + assert!(matches!(f.next_frame(), Err(Error::Eof))); + } + _ => panic!("expected SourceOutput::Frames"), + } + } + + #[test] + fn unknown_scheme_falls_back_to_file_when_registered() { + let mut reg = SourceRegistry::new(); + reg.register_bytes("file", open_bytes_mock); + // No `foo` driver — falls through to the `file` driver. + let out = reg.open("foo://x").expect("fallback open"); + assert!(matches!(out, SourceOutput::Bytes(_))); + } + + #[test] + fn unknown_scheme_with_no_file_driver_errors() { + let reg = SourceRegistry::new(); + let r = reg.open("nope://x"); + assert!(matches!(r, Err(Error::Unsupported(_)))); + } + + // ---- mock MultiTitleSource ---- + struct MockMultiTitleSource { + labels: Vec, + } + + impl MultiTitleSource for MockMultiTitleSource { + fn title_count(&self) -> usize { + self.labels.len() + } + fn open_title(&mut self, index: usize) -> Result> { + if index >= self.labels.len() { + return Err(Error::Unsupported(format!( + "no title {index} (have {})", + self.labels.len() + ))); + } + // Each title is just its label repeated 4×. + let payload = self.labels[index].as_bytes().repeat(4); + Ok(Box::new(Cursor::new(payload))) + } + fn title_label(&self, index: usize) -> String { + self.labels[index].clone() + } + fn title_display_name(&self, index: usize) -> Option { + Some(format!("Title {}", self.labels[index])) + } + fn title_container_hint(&self, _index: usize) -> Option<&'static str> { + Some("mpegts") + } + } + + fn open_multi_title_mock(_uri: &str) -> Result> { + Ok(Box::new(MockMultiTitleSource { + labels: vec!["1".to_string(), "2".to_string(), "3".to_string()], + })) + } + + #[test] + fn register_multi_title_and_open_returns_multi_title_variant() { + let mut reg = SourceRegistry::new(); + reg.register_multi_title("mockmt", open_multi_title_mock); + let out = reg.open("mockmt://anything").expect("open"); + match out { + SourceOutput::MultiTitle(mut mt) => { + assert_eq!(mt.title_count(), 3); + assert_eq!(mt.title_label(0), "1"); + assert_eq!(mt.title_display_name(2).as_deref(), Some("Title 3")); + assert_eq!(mt.title_container_hint(0), Some("mpegts")); + let mut buf = String::new(); + mt.open_title(1) + .expect("title 1") + .read_to_string(&mut buf) + .unwrap(); + assert_eq!(buf, "2222"); + } + _ => panic!("expected SourceOutput::MultiTitle"), + } + } + + #[test] + fn register_overrides_prior_kind() { + // Registering `mock` first as bytes then as frames should leave + // only the frames opener active (last write wins). + let mut reg = SourceRegistry::new(); + reg.register_bytes("mock", open_bytes_mock); + reg.register_frames("mock", open_frames_mock); + let out = reg.open("mock://x").expect("open"); + assert!(matches!(out, SourceOutput::Frames(_))); + } + + #[test] + fn schemes_iterator_lists_registered() { + let mut reg = SourceRegistry::new(); + reg.register_bytes("mockb", open_bytes_mock); + reg.register_packets("mockp", open_packets_mock); + reg.register_frames("mockf", open_frames_mock); + let mut names: Vec<&str> = reg.schemes().collect(); + names.sort(); + assert_eq!(names, vec!["mockb", "mockf", "mockp"]); + } +} diff --git a/crates/vendor/oxideav-core/src/stream.rs b/crates/vendor/oxideav-core/src/stream.rs new file mode 100644 index 00000000..8a513393 --- /dev/null +++ b/crates/vendor/oxideav-core/src/stream.rs @@ -0,0 +1,922 @@ +//! Stream metadata shared between containers and codecs. + +use crate::format::{ChannelLayout, MediaType, PixelFormat, SampleFormat}; +use crate::limits::DecoderLimits; +use crate::options::CodecOptions; +use crate::rational::Rational; +use crate::time::TimeBase; + +/// A stable identifier for a codec. Codec crates register a `CodecId` so the +/// codec registry can look them up by name. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct CodecId(pub String); + +impl CodecId { + /// Build a `CodecId` from any string-like codec name (e.g. `"h264"`). + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + /// The codec name as a borrowed string slice. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From<&str> for CodecId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl std::fmt::Display for CodecId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A codec identifier scoped to a container format — the thing a +/// demuxer reads out of the file to name a codec. Resolved to a +/// [`CodecId`] by the codec registry. +/// +/// Centralising these in the registry (instead of each container +/// hand-rolling its own FourCC → CodecId table) lets: +/// +/// * a codec crate declare its own tag claims in `register()`, keeping +/// ownership co-located with the decoder; +/// * multiple codecs claim the same tag with priority ordering; +/// * optional per-claim probes disambiguate the tag-collision cases +/// that happen everywhere in the wild (DIV3 that's actually MPEG-4 +/// Part 2, XVID that's actually MS-MPEG4v3, audio wFormatTag=0x0055 +/// that could be MP3 or — very rarely — something else, etc.). +/// +/// **Payload magics are intentionally absent** from this enum: some +/// carriage formats have no codec tag at all — the codec is announced +/// by a magic byte prefix on the payload itself (an Ogg logical +/// stream's first packet is the canonical case), which is +/// prefix-matched rather than looked up as an exact key. Such claims +/// are declared via +/// [`CodecInfo::payload_magic`](crate::registry::CodecInfo::payload_magic) +/// and resolved via [`CodecResolver::resolve_payload_magic`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum CodecTag { + /// Four-character code used by AVI's `bmih.biCompression`, MP4 / + /// QuickTime sample-entry type, Matroska V_/A_ tags built around + /// FourCC, and many others. Always stored with alphabetic bytes + /// upper-cased so lookups are case-insensitive; non-alphabetic + /// bytes are preserved as-is. + Fourcc([u8; 4]), + + /// AVI / WAV `WAVEFORMATEX::wFormatTag` (e.g. 0x0001 = PCM, + /// 0x0055 = MP3, 0x00FF = "raw" AAC, 0x1610 = AAC ADTS). + WaveFormat(u16), + + /// MP4 ObjectTypeIndication (ISO/IEC 14496-1 Table 5 / the values + /// in an MP4 `esds` `DecoderConfigDescriptor`). e.g. 0x40 = MPEG-4 + /// AAC, 0x20 = MPEG-4 Visual, 0x69 = MP3. + Mp4ObjectType(u8), + + /// Matroska `CodecID` element (full string, e.g. + /// `"V_MPEG4/ISO/AVC"`, `"A_AAC"`, `"A_VORBIS"`). + Matroska(String), +} + +impl CodecTag { + /// Build a FourCC tag, upper-casing alphabetic bytes. + pub fn fourcc(raw: &[u8; 4]) -> Self { + let mut out = [0u8; 4]; + for i in 0..4 { + out[i] = raw[i].to_ascii_uppercase(); + } + Self::Fourcc(out) + } + + /// Build a [`CodecTag::WaveFormat`] tag from a `wFormatTag` value. + pub fn wave_format(tag: u16) -> Self { + Self::WaveFormat(tag) + } + + /// Build a [`CodecTag::Mp4ObjectType`] tag from an MP4 + /// ObjectTypeIndication byte. + pub fn mp4_object_type(oti: u8) -> Self { + Self::Mp4ObjectType(oti) + } + + /// Build a [`CodecTag::Matroska`] tag from a full Matroska + /// `CodecID` string (e.g. `"A_VORBIS"`). + pub fn matroska(id: impl Into) -> Self { + Self::Matroska(id.into()) + } +} + +impl std::fmt::Display for CodecTag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Fourcc(fcc) => { + // Print as bytes when ASCII-printable, else as hex. + if fcc.iter().all(|b| b.is_ascii_graphic() || *b == b' ') { + write!(f, "fourcc({})", std::str::from_utf8(fcc).unwrap_or("????")) + } else { + write!( + f, + "fourcc(0x{:02X}{:02X}{:02X}{:02X})", + fcc[0], fcc[1], fcc[2], fcc[3] + ) + } + } + Self::WaveFormat(t) => write!(f, "wFormatTag(0x{t:04X})"), + Self::Mp4ObjectType(o) => write!(f, "mp4_oti(0x{o:02X})"), + Self::Matroska(s) => write!(f, "matroska({s})"), + } + } +} + +/// Context passed to a codec's probe function during tag resolution. +/// +/// Built by the demuxer from whatever it has already parsed (stream +/// format block, a peek at the first packet, numeric hints like +/// `bits_per_sample`). Probes read fields directly; the struct is +/// `#[non_exhaustive]` so additional hints can be added later without +/// breaking codec crates that match on it. +/// +/// The canonical construction pattern, for a demuxer: +/// +/// ``` +/// # use oxideav_core::{CodecTag, ProbeContext}; +/// let tag = CodecTag::wave_format(0x0001); +/// let ctx = ProbeContext::new(&tag) +/// .bits(24) +/// .channels(2) +/// .sample_rate(48_000); +/// # let _ = ctx; +/// ``` +/// +/// Codec authors read fields like `ctx.bits_per_sample` / `ctx.tag` +/// directly — `#[non_exhaustive]` forbids struct-literal construction +/// from outside this crate but does not restrict field access. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub struct ProbeContext<'a> { + /// The tag being resolved — always set. + pub tag: &'a CodecTag, + /// Raw container-level stream-format blob if available + /// (e.g. WAVEFORMATEX, BITMAPINFOHEADER, MP4 sample-entry bytes, + /// Matroska `CodecPrivate`). Format is container-specific. + pub header: Option<&'a [u8]>, + /// First packet bytes if the demuxer has already read one. + /// Most demuxers resolve tags at stream-discovery time before any + /// packet exists; this is `None` in that case. + pub packet: Option<&'a [u8]>, + /// Audio: bits per sample (from WAVEFORMATEX, MP4 sample entry, + /// Matroska `BitDepth`, etc.). + pub bits_per_sample: Option, + /// Audio: channel count from the container's stream header. + pub channels: Option, + /// Audio: sample rate in Hz from the container's stream header. + pub sample_rate: Option, + /// Video: coded frame width in pixels from the container's stream + /// header. + pub width: Option, + /// Video: coded frame height in pixels from the container's stream + /// header. + pub height: Option, +} + +impl<'a> ProbeContext<'a> { + /// Start building a context for `tag` with every hint field empty. + pub fn new(tag: &'a CodecTag) -> Self { + Self { + tag, + header: None, + packet: None, + bits_per_sample: None, + channels: None, + sample_rate: None, + width: None, + height: None, + } + } + + /// Builder method: attach the raw container-level stream-format + /// blob (WAVEFORMATEX, BITMAPINFOHEADER, MP4 sample-entry bytes, + /// Matroska `CodecPrivate`, ...). + pub fn header(mut self, h: &'a [u8]) -> Self { + self.header = Some(h); + self + } + + /// Builder method: attach the first packet's bytes, when the + /// demuxer has already read one. + pub fn packet(mut self, p: &'a [u8]) -> Self { + self.packet = Some(p); + self + } + + /// Builder method: set the audio bits-per-sample hint. + pub fn bits(mut self, n: u16) -> Self { + self.bits_per_sample = Some(n); + self + } + + /// Builder method: set the audio channel-count hint. + pub fn channels(mut self, n: u16) -> Self { + self.channels = Some(n); + self + } + + /// Builder method: set the audio sample-rate hint (Hz). + pub fn sample_rate(mut self, n: u32) -> Self { + self.sample_rate = Some(n); + self + } + + /// Builder method: set the video frame-width hint (pixels). + pub fn width(mut self, n: u32) -> Self { + self.width = Some(n); + self + } + + /// Builder method: set the video frame-height hint (pixels). + pub fn height(mut self, n: u32) -> Self { + self.height = Some(n); + self + } +} + +/// Confidence value returned by a probe. `1.0` means "certainly me", +/// `0.0` means "not me", values in between mean "partial evidence — if +/// no higher-confidence claim exists, this should win". The registry +/// picks the claim with the highest returned confidence and skips any +/// that return `0.0`. +pub type Confidence = f32; + +/// A probe function a codec attaches to its registration to +/// disambiguate tag collisions. Called once per candidate +/// registration during `resolve_tag`. +pub type ProbeFn = fn(&ProbeContext) -> Confidence; + +/// Resolve a [`CodecTag`] (FourCC / WAVEFORMATEX / Matroska id / …) to a +/// [`CodecId`]. The [`oxideav-codec`](https://crates.io/crates/oxideav-codec) +/// registry implements this, but defining the trait here lets +/// containers consume tag resolution via `&dyn CodecResolver` without +/// pulling in the codec crate as a direct dependency. +/// +/// **Inverse direction** (codec_id → wire tag) is intentionally NOT a +/// method on this trait. Wire tags are per-stream state: different +/// `mpeg4video` streams correctly identify as `DIVX` / `XVID` / +/// `MP4V` / `FMP4`, different `h264` streams as `H264` vs `AVC1`, +/// and so on. The stream's [`CodecParameters::tag`] field is the +/// canonical home for that data — set by the demuxer when reading +/// existing media and by the encoder via its `output_params()` at +/// configure-time. A registry-level "give me the canonical tag for +/// this codec_id" lookup walks registration order and returns +/// whichever tag was declared first, which is arbitrary and breaks +/// round-trip preservation. +pub trait CodecResolver: Sync { + /// Resolve the tag in `ctx.tag` to a codec id. Implementations walk + /// every registration whose tag set contains the tag, call each + /// probe (treating `None` as "always 1.0"), and return the id with + /// the highest resulting confidence. Ties are broken by + /// registration order. + fn resolve_tag(&self, ctx: &ProbeContext) -> Option; + + /// Resolve a codec from a stream's leading payload bytes, for + /// carriage formats that announce the codec in the payload itself + /// rather than through a container tag. + /// + /// The canonical case is Ogg, which has no numeric codec tag at + /// all: a logical stream announces its codec purely through a + /// magic byte prefix at the start of the first packet + /// (`\x01vorbis`, `OpusHead`, `\x80theora`, `\x7fFLAC`, + /// `Speex `, …); raw elementary streams identified by a file + /// head are the same shape. That identification model is + /// prefix-shaped rather than exact-key-shaped, so it gets its own + /// resolution entry point instead of a [`CodecTag`] form: codec + /// crates declare the magic prefixes they answer to at + /// registration time, and the caller hands the stream's leading + /// payload bytes (an Ogg demuxer: the first packet of a logical + /// stream; a raw-stream prober: the file head — or however much of + /// it is available) to this method. Implementations return the + /// codec whose declared magic is a prefix of `first_bytes`, + /// preferring the **longest** + /// matching magic (most specific claim) and breaking remaining + /// ties by registration order. + /// + /// The default implementation resolves nothing, so existing + /// resolver implementations (and [`NullCodecResolver`]) are + /// unaffected. + fn resolve_payload_magic(&self, first_bytes: &[u8]) -> Option { + let _ = first_bytes; + None + } +} + +/// Null resolver that resolves nothing — useful as a default when a +/// caller doesn't have a real registry handy (e.g. unit tests, or +/// legacy callers of the tag-free `open()` APIs). +#[derive(Default, Clone, Copy)] +pub struct NullCodecResolver; + +impl CodecResolver for NullCodecResolver { + fn resolve_tag(&self, _ctx: &ProbeContext) -> Option { + None + } +} + +/// Codec-level parameters shared between demuxer/muxer and en/decoder. +/// +/// **Marked `#[non_exhaustive]`** — construction via struct-literal +/// syntax is not supported. Use the [`audio`](Self::audio) / +/// [`video`](Self::video) constructors (or functional-update +/// `CodecParameters { ..base }` syntax) so new fields can be added +/// without another semver break. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct CodecParameters { + /// Registry identifier of the codec this stream is encoded with. + pub codec_id: CodecId, + /// Whether this stream is audio, video, subtitle, or data. Set by + /// the constructor ([`audio`](Self::audio), [`video`](Self::video), + /// [`subtitle`](Self::subtitle), [`data`](Self::data)). + pub media_type: MediaType, + + // Audio-specific + /// Audio: sample rate in Hz. `None` for non-audio streams. + pub sample_rate: Option, + /// Audio: number of channels. See [`Self::resolved_channels`] for + /// the layout-aware accessor. + pub channels: Option, + /// Audio: sample format of the decoded output (or encoder input). + pub sample_format: Option, + /// Speaker layout for the audio stream. **This is the canonical + /// answer to "what layout does this stream have?"** — layout is a + /// stream-level property and is intentionally *not* duplicated on + /// individual [`AudioFrame`](crate::AudioFrame)s. + /// + /// Optional and additive alongside [`channels`](Self::channels): a + /// codec/container that only knows the count can leave this `None` + /// and consumers will fall back to [`ChannelLayout::from_count`] + /// via [`Self::resolved_layout`]. When both are set, they must + /// agree on channel count. + pub channel_layout: Option, + + // Video-specific + /// Video: coded frame width in pixels. `None` for non-video streams. + pub width: Option, + /// Video: coded frame height in pixels. `None` for non-video streams. + pub height: Option, + /// Video: pixel format of the decoded output (or encoder input). + pub pixel_format: Option, + /// Video: nominal frame rate in frames per second, as a rational + /// (e.g. 30000/1001). `None` when unknown or variable. + pub frame_rate: Option, + + /// Per-codec setup bytes (e.g., SPS/PPS, OpusHead). Format defined by codec. + pub extradata: Vec, + + /// Nominal stream bit rate in bits per second, when the container + /// or encoder declares one. + pub bit_rate: Option, + + /// Codec-specific tuning knobs (e.g. `{"interlace": "true"}` for PNG's + /// Adam7 encode, `{"crf": "23"}` for h264). Empty by default. The shape + /// is declared by each codec's options struct — see + /// [`crate::options`]. Parsed once at encoder/decoder construction; + /// the hot path never touches this. + pub options: CodecOptions, + + /// DoS-protection caps threaded into every decoder constructed from + /// these parameters. See [`DecoderLimits`] for the semantics of each + /// field. Defaults are conservative-but-finite (32 k × 32 k pixels, + /// 1 GiB per arena, etc.) — every existing real-world stream + /// decodes unchanged. Tighten via [`Self::with_limits`] when the + /// caller wants to harden the pipeline against untrusted input. + pub limits: DecoderLimits, + + /// Optional 0-based device selector for hardware-accelerated codecs. + /// `None` (the default) means "use the backend's default device"; + /// `Some(n)` requests device `n` from the backend's + /// [`crate::engine::HwDeviceInfo`] enumeration order. + /// + /// Software codecs ignore this field. Hardware codecs read it as + /// `params.device_index.unwrap_or(0)` to pick which physical engine + /// to bind to. Indexing matches the order of devices reported by the + /// codec entry's `engine_probe` function. + pub device_index: Option, + + /// On-wire tag for this stream — the FourCC / WAVEFORMATEX + /// `wFormatTag` / MP4 ObjectTypeIndication / Matroska `CodecID` + /// string carried by the container. Set by the **producer**: + /// + /// * **Demuxers** populate this from the stream's container + /// header at read-time so muxers re-emitting the same stream + /// round-trip the original tag byte-for-byte (`mpeg4video` + /// demuxed as `DIVX` re-muxes as `DIVX`, not as the codec + /// crate's first-declared `XVID`). + /// * **Encoders** populate this in [`crate::Encoder::output_params`] + /// to tell muxers which wire tag to write — needed for + /// multi-FourCC codecs whose configuration (pixel format / bit + /// depth / alpha / chroma sampling) selects one of several + /// valid FourCCs (e.g. MagicYUV's 17 native v7 codes). + /// + /// `None` is the default — sensible for in-memory streams that + /// haven't been bound to a container yet. Muxers that need a + /// wire tag and find `None` here will fall back to whatever + /// container-specific synthesis they support (e.g. AVI's PCM + /// `wFormatTag` synthesis from `sample_format`, or the + /// `extradata[0..4]` printable-FourCC hint for legacy callers) + /// and otherwise return `Error::Unsupported`. + pub tag: Option, + + /// BCP-47 / ISO 639 language tag (`"en"`, `"jpn"`, …) when the + /// container labels the stream's language. `None` means + /// "unspecified" — not "neutral". + /// + /// Demuxers populate this from the container's per-track language + /// element (MKV `Language` / `LanguageBCP47`, MP4 `mdhd` ISO 639-2 + /// code, Ogg `LANGUAGE=` comment, …). Muxers re-emit it on the + /// matching container element so a round-trip preserves the + /// caller-visible tag byte-for-byte. No validation is performed + /// here — the value is whatever string the producer supplied. + pub language: Option, +} + +impl CodecParameters { + /// Construct audio codec parameters with every optional field + /// unset. Chain builder methods ([`channels`](Self::channels), + /// [`channel_layout`](Self::channel_layout), ...) or assign fields + /// directly to fill in the format. + pub fn audio(codec_id: CodecId) -> Self { + Self { + codec_id, + media_type: MediaType::Audio, + sample_rate: None, + channels: None, + sample_format: None, + channel_layout: None, + width: None, + height: None, + pixel_format: None, + frame_rate: None, + extradata: Vec::new(), + bit_rate: None, + options: CodecOptions::default(), + limits: DecoderLimits::default(), + device_index: None, + tag: None, + language: None, + } + } + + /// True when `self` and `other` have the same codec_id and core + /// format parameters (sample_rate/channels/sample_format for audio, + /// width/height/pixel_format for video). Extradata and bitrate + /// differences are tolerated — many containers rewrite extradata + /// losslessly during a copy operation. `channel_layout` is compared + /// only via the channel count (through [`Self::resolved_layout`]) so + /// a stream that surfaces an explicit layout still matches a + /// count-only stream of the same width. + pub fn matches_core(&self, other: &CodecParameters) -> bool { + self.codec_id == other.codec_id + && self.sample_rate == other.sample_rate + && self.channels == other.channels + && self.sample_format == other.sample_format + && self.width == other.width + && self.height == other.height + && self.pixel_format == other.pixel_format + } + + /// Construct video codec parameters with every optional field + /// unset. Assign `width` / `height` / `pixel_format` (or use the + /// builder methods) to fill in the format. + pub fn video(codec_id: CodecId) -> Self { + Self { + codec_id, + media_type: MediaType::Video, + sample_rate: None, + channels: None, + sample_format: None, + channel_layout: None, + width: None, + height: None, + pixel_format: None, + frame_rate: None, + extradata: Vec::new(), + bit_rate: None, + options: CodecOptions::default(), + limits: DecoderLimits::default(), + device_index: None, + tag: None, + language: None, + } + } + + /// Construct subtitle codec parameters. No format-specific fields + /// are populated — subtitle codecs typically only carry an opaque + /// `extradata` blob (the format's header / style block) and the + /// codec id. + pub fn subtitle(codec_id: CodecId) -> Self { + Self { + codec_id, + media_type: MediaType::Subtitle, + sample_rate: None, + channels: None, + sample_format: None, + channel_layout: None, + width: None, + height: None, + pixel_format: None, + frame_rate: None, + extradata: Vec::new(), + bit_rate: None, + options: CodecOptions::default(), + limits: DecoderLimits::default(), + device_index: None, + tag: None, + language: None, + } + } + + /// Construct generic data-stream codec parameters (timed metadata, + /// chapters, etc.). Like [`Self::subtitle`], no format-specific + /// fields are populated. + pub fn data(codec_id: CodecId) -> Self { + Self { + codec_id, + media_type: MediaType::Data, + sample_rate: None, + channels: None, + sample_format: None, + channel_layout: None, + width: None, + height: None, + pixel_format: None, + frame_rate: None, + extradata: Vec::new(), + bit_rate: None, + options: CodecOptions::default(), + limits: DecoderLimits::default(), + device_index: None, + tag: None, + language: None, + } + } + + /// Builder method: set the channel count. + /// + /// Pairs with [`Self::channel_layout`] for the layout. The two are + /// kept as independent fields so a codec that only knows one or the + /// other can populate just the field it has; [`Self::resolved_layout`] + /// derives a layout from whatever is set. + pub fn channels(mut self, n: u16) -> Self { + self.channels = Some(n); + self + } + + /// Builder method: set the channel layout. Mirrors + /// [`Self::channels`]; setting one does not auto-fill the other — + /// use [`Self::resolved_layout`] / [`Self::resolved_channels`] at + /// read time to bridge the two. + pub fn channel_layout(mut self, layout: ChannelLayout) -> Self { + self.channel_layout = Some(layout); + self + } + + /// Best-effort layout: prefers an explicit [`Self::channel_layout`] + /// when set, otherwise infers one from [`Self::channels`] via + /// [`ChannelLayout::from_count`]. Returns `None` only when neither + /// field is populated (e.g. video / data streams, or audio params + /// surfaced before the codec has been opened). + /// + /// This is the canonical call-site for resolving a stream's + /// channel layout — frames do *not* carry layout, so audio + /// consumers (downmix, device routing, channel-aware filters) + /// should read it from the stream's `CodecParameters` once and + /// pass it down with the frame. + pub fn resolved_layout(&self) -> Option { + self.channel_layout + .or_else(|| self.channels.map(ChannelLayout::from_count)) + } + + /// Best-effort channel count: prefers an explicit + /// [`Self::channels`] when set, otherwise reads the count off + /// [`Self::channel_layout`]. Returns `None` only when neither + /// field is populated. + pub fn resolved_channels(&self) -> Option { + self.channels + .or_else(|| self.channel_layout.map(|l| l.channel_count())) + } + + /// Read-only access to the DoS-protection caps for any decoder + /// constructed from these parameters. See [`DecoderLimits`]. + pub fn limits(&self) -> &DecoderLimits { + &self.limits + } + + /// Builder method: replace the [`DecoderLimits`] for these + /// parameters. Use to tighten caps before passing parameters into + /// `make_decoder` (e.g. when processing untrusted uploads on a + /// shared server). + /// + /// ``` + /// # use oxideav_core::{CodecId, CodecParameters, DecoderLimits}; + /// let limits = DecoderLimits::default() + /// .with_max_pixels_per_frame(4096 * 4096) + /// .with_max_arenas_in_flight(2); + /// let p = CodecParameters::video(CodecId::new("h263")).with_limits(limits); + /// assert_eq!(p.limits().max_pixels_per_frame, 4096 * 4096); + /// ``` + pub fn with_limits(mut self, limits: DecoderLimits) -> Self { + self.limits = limits; + self + } + + /// Bind subsequent decoder/encoder construction to a specific device. + /// `index` matches the position in the `engine_probe` device list. + /// + /// Software codecs ignore this field. Hardware codecs read it as + /// `params.device_index.unwrap_or(0)` to pick which physical engine + /// to bind to. + pub fn with_device_index(mut self, index: u32) -> Self { + self.device_index = Some(index); + self + } + + /// Builder method: set the on-wire [`tag`](Self::tag). + /// + /// Demuxers call this from their stream-format parser so muxers + /// re-emitting the stream preserve the original FourCC / wFormatTag + /// byte-for-byte. Encoders call this in `output_params()` to + /// announce which wire tag they're producing. + /// + /// ``` + /// # use oxideav_core::{CodecId, CodecParameters, CodecTag}; + /// let p = CodecParameters::video(CodecId::new("magicyuv")) + /// .with_tag(CodecTag::fourcc(b"M8RG")); + /// assert_eq!(p.tag, Some(CodecTag::fourcc(b"M8RG"))); + /// ``` + pub fn with_tag(mut self, tag: CodecTag) -> Self { + self.tag = Some(tag); + self + } + + /// Builder method: set the per-stream [`language`](Self::language) + /// tag. Accepts any string — BCP-47 short codes (`"en"`), ISO + /// 639-2/T three-letter codes (`"jpn"`), or container-native + /// values are all passed through verbatim. No validation is + /// performed; the muxer writes whatever the caller hands in. + /// + /// ``` + /// # use oxideav_core::{CodecId, CodecParameters}; + /// let p = CodecParameters::audio(CodecId::new("aac")).with_language("jpn"); + /// assert_eq!(p.language.as_deref(), Some("jpn")); + /// ``` + pub fn with_language(mut self, language: impl Into) -> Self { + self.language = Some(language.into()); + self + } +} + +/// Description of a single stream inside a container. +#[derive(Clone, Debug)] +pub struct StreamInfo { + /// 0-based index of the stream within its container. + pub index: u32, + /// Time base in which this stream's packet timestamps (and + /// `duration` / `start_time` below) are expressed. + pub time_base: TimeBase, + /// Stream duration in `time_base` units, when the container + /// declares one. + pub duration: Option, + /// Presentation timestamp of the first packet, in `time_base` + /// units, when known. + pub start_time: Option, + /// Codec-level parameters (codec id, format, extradata, ...). + pub params: CodecParameters, +} + +#[cfg(test)] +mod codec_tag_tests { + use super::*; + + #[test] + fn fourcc_uppercases_on_construction() { + let t = CodecTag::fourcc(b"div3"); + assert_eq!(t, CodecTag::Fourcc(*b"DIV3")); + // Non-alphabetic bytes preserved unchanged. + let t2 = CodecTag::fourcc(b"MP42"); + assert_eq!(t2, CodecTag::Fourcc(*b"MP42")); + let t3 = CodecTag::fourcc(&[0xFF, b'a', 0x00, b'1']); + assert_eq!(t3, CodecTag::Fourcc([0xFF, b'A', 0x00, b'1'])); + } + + #[test] + fn fourcc_equality_case_insensitive_via_ctor() { + assert_eq!(CodecTag::fourcc(b"xvid"), CodecTag::fourcc(b"XVID")); + assert_eq!(CodecTag::fourcc(b"DiV3"), CodecTag::fourcc(b"div3")); + } + + #[test] + fn display_printable_fourcc() { + assert_eq!(CodecTag::fourcc(b"XVID").to_string(), "fourcc(XVID)"); + } + + #[test] + fn display_non_printable_fourcc_as_hex() { + let t = CodecTag::Fourcc([0x00, 0x00, 0x00, 0x01]); + assert_eq!(t.to_string(), "fourcc(0x00000001)"); + } + + #[test] + fn display_wave_format() { + assert_eq!( + CodecTag::wave_format(0x0055).to_string(), + "wFormatTag(0x0055)" + ); + } + + #[test] + fn display_mp4_oti() { + assert_eq!(CodecTag::mp4_object_type(0x40).to_string(), "mp4_oti(0x40)"); + } + + #[test] + fn display_matroska() { + assert_eq!( + CodecTag::matroska("V_MPEG4/ISO/AVC").to_string(), + "matroska(V_MPEG4/ISO/AVC)", + ); + } + + #[test] + fn null_resolver_resolves_nothing() { + let r = NullCodecResolver; + let xvid = CodecTag::fourcc(b"XVID"); + assert!(r.resolve_tag(&ProbeContext::new(&xvid)).is_none()); + let wf = CodecTag::wave_format(0x0055); + assert!(r.resolve_tag(&ProbeContext::new(&wf)).is_none()); + } + + #[test] + fn probe_context_builder_fills_hints() { + let tag = CodecTag::wave_format(0x0001); + let ctx = ProbeContext::new(&tag) + .bits(24) + .channels(2) + .sample_rate(48_000) + .header(&[1, 2, 3]) + .packet(&[4, 5]); + assert_eq!(ctx.bits_per_sample, Some(24)); + assert_eq!(ctx.channels, Some(2)); + assert_eq!(ctx.sample_rate, Some(48_000)); + assert_eq!(ctx.header.unwrap(), &[1, 2, 3]); + assert_eq!(ctx.packet.unwrap(), &[4, 5]); + } +} + +#[cfg(test)] +mod channel_layout_plumbing_tests { + use super::*; + + #[test] + fn audio_params_default_to_no_layout() { + let p = CodecParameters::audio(CodecId::new("pcm_s16le")); + assert!(p.channel_layout.is_none()); + assert!(p.channels.is_none()); + assert!(p.resolved_layout().is_none()); + assert!(p.resolved_channels().is_none()); + } + + #[test] + fn channels_only_infers_layout_via_from_count() { + let p = CodecParameters::audio(CodecId::new("pcm_s16le")).channels(6); + assert_eq!(p.channels, Some(6)); + assert!(p.channel_layout.is_none()); + assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround51)); + assert_eq!(p.resolved_channels(), Some(6)); + } + + #[test] + fn explicit_layout_wins_over_count() { + let p = CodecParameters::audio(CodecId::new("ac3")) + .channels(6) + .channel_layout(ChannelLayout::Surround60); + // 6ch by-count would default to Surround51, but the explicit + // layout overrides. + assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround60)); + assert_eq!(p.resolved_channels(), Some(6)); + } + + #[test] + fn layout_only_yields_count_via_resolved_channels() { + let p = + CodecParameters::audio(CodecId::new("ac3")).channel_layout(ChannelLayout::Surround71); + assert!(p.channels.is_none()); + assert_eq!(p.resolved_channels(), Some(8)); + assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround71)); + } +} + +#[cfg(test)] +mod codec_parameters_device_index_tests { + use super::*; + + #[test] + fn codec_parameters_device_index_defaults_to_none() { + assert!(CodecParameters::audio(CodecId::new("pcm_s16le")) + .device_index + .is_none()); + assert!(CodecParameters::video(CodecId::new("h264")) + .device_index + .is_none()); + assert!(CodecParameters::subtitle(CodecId::new("srt")) + .device_index + .is_none()); + assert!(CodecParameters::data(CodecId::new("bin")) + .device_index + .is_none()); + } + + #[test] + fn codec_parameters_with_device_index_sets_field() { + let p = CodecParameters::video(CodecId::new("h264")).with_device_index(2); + assert_eq!(p.device_index, Some(2)); + } +} + +#[cfg(test)] +mod codec_parameters_tag_tests { + use super::*; + + #[test] + fn tag_defaults_to_none_on_every_constructor() { + assert!(CodecParameters::audio(CodecId::new("aac")).tag.is_none()); + assert!(CodecParameters::video(CodecId::new("h264")).tag.is_none()); + assert!(CodecParameters::subtitle(CodecId::new("srt")).tag.is_none()); + assert!(CodecParameters::data(CodecId::new("bin")).tag.is_none()); + } + + #[test] + fn with_tag_builder_sets_field() { + let p = + CodecParameters::video(CodecId::new("magicyuv")).with_tag(CodecTag::fourcc(b"M8RG")); + assert_eq!(p.tag, Some(CodecTag::fourcc(b"M8RG"))); + } + + #[test] + fn with_tag_round_trip_preserves_demuxed_fourcc() { + // The canonical use-case: a demuxer sees DIVX in the bitstream + // and tags the params accordingly. The mpeg4video codec also + // claims XVID / MP4V / FMP4, but the muxer must re-emit DIVX. + let demuxed = + CodecParameters::video(CodecId::new("mpeg4video")).with_tag(CodecTag::fourcc(b"DIVX")); + // Muxer reads `params.tag` directly — no registry round-trip. + assert_eq!(demuxed.tag, Some(CodecTag::fourcc(b"DIVX"))); + } + + #[test] + fn wave_format_tag_preserved() { + let p = CodecParameters::audio(CodecId::new("mp3")).with_tag(CodecTag::wave_format(0x0055)); + assert_eq!(p.tag, Some(CodecTag::WaveFormat(0x0055))); + } +} + +#[cfg(test)] +mod codec_parameters_language_tests { + use super::*; + + #[test] + fn language_defaults_to_none_on_every_constructor() { + assert!(CodecParameters::audio(CodecId::new("aac")) + .language + .is_none()); + assert!(CodecParameters::video(CodecId::new("h264")) + .language + .is_none()); + assert!(CodecParameters::subtitle(CodecId::new("srt")) + .language + .is_none()); + assert!(CodecParameters::data(CodecId::new("bin")) + .language + .is_none()); + } + + #[test] + fn with_language_round_trips_value() { + let p = CodecParameters::audio(CodecId::new("aac")).with_language("jpn"); + assert_eq!(p.language.as_deref(), Some("jpn")); + } + + #[test] + fn with_language_accepts_bcp47_short_code() { + let p = CodecParameters::audio(CodecId::new("aac")).with_language("en"); + assert_eq!(p.language.as_deref(), Some("en")); + } + + #[test] + fn with_language_accepts_owned_string() { + let tag = String::from("fre"); + let p = CodecParameters::audio(CodecId::new("aac")).with_language(tag); + assert_eq!(p.language.as_deref(), Some("fre")); + } +} diff --git a/crates/vendor/oxideav-core/src/subtitle.rs b/crates/vendor/oxideav-core/src/subtitle.rs new file mode 100644 index 00000000..2f16be12 --- /dev/null +++ b/crates/vendor/oxideav-core/src/subtitle.rs @@ -0,0 +1,178 @@ +//! Unified subtitle cue representation. +//! +//! Produced by subtitle-format decoders (SRT, WebVTT, ASS/SSA) and consumed +//! by the corresponding encoders. Timing is expressed in microseconds from +//! the start of the stream so the IR is format-independent. + +/// A single displayable subtitle event. +#[derive(Clone, Debug, Default)] +pub struct SubtitleCue { + /// Cue start, microseconds from stream start. + pub start_us: i64, + /// Cue end, microseconds from stream start. + pub end_us: i64, + /// Optional style name this cue inherits from. References an entry in + /// the track-level style table (ASS `Style:` rows or WebVTT `::cue(.X)` rules). + pub style_ref: Option, + /// Optional overriding position for this cue. `None` → use the style default. + pub positioning: Option, + /// Cue body as a sequence of styled segments. + pub segments: Vec, +} + +/// Positioning information for a cue. +/// +/// Interpretation differs by source format: +/// * WebVTT — `x`/`y` are percentages of the viewport, `align` from cue settings. +/// * ASS `\pos(x, y)` — absolute pixel coordinates in the `PlayResX`×`PlayResY` canvas. +#[derive(Clone, Debug, Default)] +pub struct CuePosition { + /// Horizontal position (WebVTT `position:N%` percentage, or ASS + /// `\pos` pixel X). `None` → format default. + pub x: Option, + /// Vertical position (WebVTT `line:N%` percentage, or ASS `\pos` + /// pixel Y). `None` → format default. + pub y: Option, + /// Horizontal text alignment for this cue. + pub align: TextAlign, + /// WebVTT `size:N%` cue setting. Irrelevant for ASS. + pub size: Option, +} + +/// Horizontal alignment for a cue / a style row. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TextAlign { + /// Aligned to the text-direction start edge (left in left-to-right + /// scripts). WebVTT default. + #[default] + Start, + /// Centered. + Center, + /// Aligned to the text-direction end edge (right in left-to-right + /// scripts). + End, + /// Aligned to the left edge regardless of text direction. + Left, + /// Aligned to the right edge regardless of text direction. + Right, +} + +/// One inline element of a cue body. +#[derive(Clone, Debug)] +pub enum Segment { + /// Plain text run. + Text(String), + /// Explicit line break (SRT/WebVTT newline, ASS `\N`). + LineBreak, + /// Bold-styled children (``, ASS `\b1`). + Bold(Vec), + /// Italic-styled children (``, ASS `\i1`). + Italic(Vec), + /// Underlined children (``, ASS `\u1`). + Underline(Vec), + /// Strikethrough children (``, ASS `\s1`). + Strike(Vec), + /// Children rendered in a specific text color (SRT ``, + /// ASS `\c`). + Color { + /// Text color as an `(r, g, b)` triple, each channel `0..=255`. + rgb: (u8, u8, u8), + /// Segments the color applies to. + children: Vec, + }, + /// Children rendered with a font override (SRT ``, ASS + /// `\fn` / `\fs`). + Font { + /// Font family name. `None` → inherit. + family: Option, + /// Font size in the source format's units (ASS `\fs` points / + /// `` value). `None` → inherit. + size: Option, + /// Segments the font override applies to. + children: Vec, + }, + /// WebVTT `...`. + Voice { + /// Speaker name (the `` annotation). + name: String, + /// Segments spoken by this voice. + children: Vec, + }, + /// WebVTT `...`. + Class { + /// CSS class name (without the leading dot). + name: String, + /// Segments the class applies to. + children: Vec, + }, + /// ASS `{\k}` — the following text is highlighted for `cs` centiseconds. + /// The children slice is the text under this karaoke beat (until the next + /// `\k` override). + Karaoke { + /// Beat duration in centiseconds (the `\k` argument). + cs: u32, + /// Segments highlighted during this beat. + children: Vec, + }, + /// WebVTT inline timestamp `<00:00:01.500>`. + Timestamp { + /// Timestamp value, microseconds from stream start. + offset_us: i64, + }, + /// Fallback for override tags we don't model explicitly. Carries the + /// textual source verbatim so a re-emit to the same format stays faithful. + Raw(String), +} + +/// A named style definition — reusable across many cues. +#[derive(Clone, Debug, Default)] +pub struct SubtitleStyle { + /// Style name that cues reference via [`SubtitleCue::style_ref`] + /// (ASS `Style:` name or WebVTT cue class). + pub name: String, + /// Font family name. `None` → renderer default. + pub font_family: Option, + /// Font size in the source format's units (ASS `Fontsize` points). + /// `None` → renderer default. + pub font_size: Option, + /// Main text fill color as `(r, g, b, a)`, each channel `0..=255`. + /// `None` → renderer default. + pub primary_color: Option<(u8, u8, u8, u8)>, + /// Text outline (border) color as `(r, g, b, a)`. `None` → default. + pub outline_color: Option<(u8, u8, u8, u8)>, + /// Background / shadow color as `(r, g, b, a)` (ASS `BackColour`). + /// `None` → default. + pub back_color: Option<(u8, u8, u8, u8)>, + /// Bold text. + pub bold: bool, + /// Italic text. + pub italic: bool, + /// Underlined text. + pub underline: bool, + /// Strikethrough text. + pub strike: bool, + /// Horizontal text alignment. + pub align: TextAlign, + /// Left margin in pixels (ASS `MarginL`). `None` → default. + pub margin_l: Option, + /// Right margin in pixels (ASS `MarginR`). `None` → default. + pub margin_r: Option, + /// Vertical margin in pixels (ASS `MarginV`). `None` → default. + pub margin_v: Option, + /// Outline (border) thickness in pixels (ASS `Outline`). + /// `None` → default. + pub outline: Option, + /// Drop-shadow offset in pixels (ASS `Shadow`). `None` → default. + pub shadow: Option, +} + +impl SubtitleStyle { + /// Build a style with the given name and every other field at its + /// default (`None` / `false` / [`TextAlign::Start`]). + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + ..Default::default() + } + } +} diff --git a/crates/vendor/oxideav-core/src/time.rs b/crates/vendor/oxideav-core/src/time.rs new file mode 100644 index 00000000..48a372d9 --- /dev/null +++ b/crates/vendor/oxideav-core/src/time.rs @@ -0,0 +1,675 @@ +//! Time base and timestamp types. + +use crate::rational::Rational; + +/// A time base expressed as a rational number of seconds per tick. +/// +/// A `TimeBase` of 1/48000 means each timestamp unit is 1/48000 second. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TimeBase(pub Rational); + +impl TimeBase { + /// Construct a time base of `num/den` seconds per tick. + pub const fn new(num: i64, den: i64) -> Self { + Self(Rational::new(num, den)) + } + + /// Construct a `TimeBase` representing `1/rate` seconds per tick — + /// the canonical "sample-rate-style" base used by audio codecs + /// (`1/48000` for 48 kHz PCM, `1/44100` for CD audio, `1/8000` for + /// G.711) and by the common video bases (`1/90000` for MPEG-TS, + /// `1/1000000` for microsecond PTS). + /// + /// Equivalent to `TimeBase::new(1, rate as i64)`, but reads more + /// clearly at call sites and documents the inverse-of-rate + /// convention so a reader doesn't have to mentally swap arguments. + pub const fn from_rate(rate: u32) -> Self { + Self(Rational::new(1, rate as i64)) + } + + /// `num` of the underlying [`Rational`]. Sugar over `tb.0.num` for + /// callers that don't want to reach through the tuple-struct field. + pub const fn num(&self) -> i64 { + self.0.num + } + + /// `den` of the underlying [`Rational`]. Sugar over `tb.0.den`. + pub const fn den(&self) -> i64 { + self.0.den + } + + /// The underlying seconds-per-tick fraction. + pub fn as_rational(&self) -> Rational { + self.0 + } + + /// `true` when this time base is usable for rescaling — both terms + /// non-zero. A zero denominator denotes "no defined time base" (the + /// `1/0` placeholder some demuxers stamp on data-only streams); + /// callers that want to skip rescaling on those streams can branch + /// on `is_valid()` instead of re-doing the same `den != 0 && num != 0` + /// check at every call site. + pub const fn is_valid(&self) -> bool { + self.0.num != 0 && self.0.den != 0 + } + + /// Convert a tick count in this time base to seconds. + pub fn seconds_of(&self, ticks: i64) -> f64 { + ticks as f64 * self.0.as_f64() + } + + /// Convert a fractional-seconds count to the nearest tick count in + /// this time base. The inverse of [`seconds_of`](Self::seconds_of): + /// `seconds_of` goes + /// `ticks → seconds`; `ticks_of` goes `seconds → ticks`. Useful + /// for muxers and encoders that have a target wall-clock duration + /// and need to land it on the stream's time base without hand-rolling + /// the divide-and-round at every call site. + /// + /// Rounds half-away-from-zero (matches [`rescale`]). On an invalid + /// time base (`is_valid() == false`) or when the result would exceed + /// `i64` range, returns `0` — pick a defaulted timestamp rather than + /// panicking, since callers are typically muxing best-effort output. + pub fn ticks_of(&self, seconds: f64) -> i64 { + // ticks = seconds / (num/den) = seconds * den / num + if !self.is_valid() || !seconds.is_finite() { + return 0; + } + let scaled = seconds * (self.0.den as f64) / (self.0.num as f64); + if !scaled.is_finite() { + return 0; + } + // Half-away-from-zero rounding, matching `rescale`. + let rounded = if scaled >= 0.0 { + (scaled + 0.5).floor() + } else { + (scaled - 0.5).ceil() + }; + // Clamp to i64 range. + if rounded >= i64::MAX as f64 { + i64::MAX + } else if rounded <= i64::MIN as f64 { + i64::MIN + } else { + rounded as i64 + } + } + + /// Rescale a timestamp from this time base to another. + /// + /// Saturates at the `i64` range boundaries and returns `0` on an + /// undefined conversion (zero term in the factor) — see [`rescale`]. + pub fn rescale(&self, ts: i64, target: TimeBase) -> i64 { + rescale(ts, self.0, target.0) + } + + /// Rescale a timestamp from this time base to another with an + /// explicit [`Rounding`] mode. See [`rescale_rnd`]. + pub fn rescale_rnd(&self, ts: i64, target: TimeBase, rounding: Rounding) -> i64 { + rescale_rnd(ts, self.0, target.0, rounding) + } + + /// Rescale a timestamp from this time base to another, reporting + /// `None` instead of saturating or defaulting — see + /// [`rescale_checked`]. + pub fn rescale_checked(&self, ts: i64, target: TimeBase) -> Option { + rescale_checked(ts, self.0, target.0) + } +} + +/// Common time-base constants. +/// +/// These are the rates that show up over and over across the workspace: +/// MPEG-TS / RTP video at 90 kHz, microsecond PTS (most demuxers' +/// "expose-everything" base), MKV at 1 ms, and the audio sample rates +/// the codec crates spend most of their lives at. Naming them once +/// removes the magic-numbers-at-call-sites that grep-fishing has to +/// distinguish from random integer literals. +impl TimeBase { + /// 1/1 — one tick per second. The "no rescaling" identity base, + /// useful for placeholders on streams without a defined cadence + /// (e.g. one-shot SVG / image frames). + pub const SECONDS: TimeBase = TimeBase::new(1, 1); + + /// 1/1000 — millisecond ticks (Matroska / WebM `Timecode` default). + pub const MILLIS: TimeBase = TimeBase::new(1, 1_000); + + /// 1/1_000_000 — microsecond ticks (the base most demuxers expose + /// to consumers when they want the finest sane resolution without + /// going to nanoseconds). + pub const MICROS: TimeBase = TimeBase::new(1, 1_000_000); + + /// 1/1_000_000_000 — nanosecond ticks. + pub const NANOS: TimeBase = TimeBase::new(1, 1_000_000_000); + + /// 1/90000 — 90 kHz, the MPEG-TS / RTP video PTS clock. + pub const MPEG_TS: TimeBase = TimeBase::new(1, 90_000); + + /// 1/48000 — 48 kHz audio sample-clock (Opus, AC-3, most modern + /// AAC, DTS). + pub const AUDIO_48K: TimeBase = TimeBase::new(1, 48_000); + + /// 1/44100 — 44.1 kHz audio sample-clock (CD audio, MP3 at 44.1, + /// many FLAC streams). + pub const AUDIO_44K1: TimeBase = TimeBase::new(1, 44_100); + + /// 1/8000 — 8 kHz audio sample-clock (G.711, G.722, G.729, AMR-NB). + pub const AUDIO_8K: TimeBase = TimeBase::new(1, 8_000); +} + +/// A timestamp in a particular time base. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Timestamp { + /// Tick count in `base` units. + pub value: i64, + /// The time base the tick count is expressed in. + pub base: TimeBase, +} + +impl Timestamp { + /// Construct a timestamp of `value` ticks in `base`. + pub const fn new(value: i64, base: TimeBase) -> Self { + Self { value, base } + } + + /// Construct a timestamp at `seconds` in the given `base`, rounded + /// to the nearest tick. Sugar over `Timestamp::new(base.ticks_of(s), base)`. + pub fn from_seconds(seconds: f64, base: TimeBase) -> Self { + Self::new(base.ticks_of(seconds), base) + } + + /// The timestamp as fractional seconds (`value × base`). + pub fn seconds(&self) -> f64 { + self.base.seconds_of(self.value) + } + + /// Rescale onto `target` with half-away-from-zero rounding; the + /// saturating/defaulting semantics of [`rescale`]. + pub fn rescale(&self, target: TimeBase) -> Self { + Self { + value: self.base.rescale(self.value, target), + base: target, + } + } + + /// Rescale onto `target` with an explicit [`Rounding`] mode. Muxers + /// that must never stamp a DTS later than the true instant use + /// [`Rounding::Floor`]; [`Rounding::NearestAway`] reproduces + /// [`rescale`](Self::rescale). + pub fn rescale_rnd(&self, target: TimeBase, rounding: Rounding) -> Self { + Self { + value: self.base.rescale_rnd(self.value, target, rounding), + base: target, + } + } + + /// Rescale onto `target`, returning `None` when the conversion is + /// undefined (zero term in the factor) or the result doesn't fit + /// `i64` — instead of the defaulting/saturating [`rescale`](Self::rescale). + pub fn checked_rescale(&self, target: TimeBase) -> Option { + self.base + .rescale_checked(self.value, target) + .map(|value| Self { + value, + base: target, + }) + } + + /// Advance the timestamp by `ticks` units in its own base. Returns + /// `None` on `i64` overflow rather than wrapping silently — muxers + /// that compute a packet-end timestamp at the edge of the + /// representable range get a clean signal instead of a wrap. + pub fn checked_add_ticks(&self, ticks: i64) -> Option { + self.value.checked_add(ticks).map(|v| Self { + value: v, + base: self.base, + }) + } + + /// Move the timestamp backwards by `ticks` units in its own base. + /// Returns `None` on `i64` overflow. + pub fn checked_sub_ticks(&self, ticks: i64) -> Option { + self.value.checked_sub(ticks).map(|v| Self { + value: v, + base: self.base, + }) + } + + /// Tick-difference `self - other` after rescaling `other` onto + /// `self`'s base. Returns `None` when the subtraction would overflow + /// `i64` (rare in practice but easy to surface cleanly). + /// + /// Use this to compute the duration between two `Timestamp`s that + /// may have been produced by different sources (e.g. a packet from a + /// container demuxer minus a packet from a different demuxer in a + /// remux pipeline). + pub fn checked_diff(&self, other: Timestamp) -> Option { + let other_in_self_base = other.rescale(self.base).value; + self.value.checked_sub(other_in_self_base) + } +} + +/// How a rescale operation rounds a result that falls between two +/// integer ticks of the target base. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Rounding { + /// Round to the nearest tick; a tie (exactly halfway) rounds away + /// from zero (`+1.5 → +2`, `-1.5 → -2`). The default, and the mode + /// the plain [`rescale`] uses. + #[default] + NearestAway, + /// Round toward negative infinity. The right choice for DTS-like + /// stamps that must never land *after* the true instant. + Floor, + /// Round toward positive infinity. The mirror of [`Rounding::Floor`] + /// for end-of-range stamps that must never land *before* the true + /// instant. + Ceil, + /// Round toward zero (truncate the fractional part). + TowardZero, +} + +/// Divide `|prod|` by `den` with the given rounding mode, in unsigned +/// magnitude space so no intermediate can overflow (`p ≤ 2^127`, +/// `d ≤ 2^126`, and every adjustment stays below `2^128`). `neg` is the +/// sign of the true quotient; sign-dependent modes (floor / ceil) use +/// it to pick the right direction. +fn div_round_abs(p: u128, d: u128, neg: bool, rounding: Rounding) -> u128 { + let q = p / d; + let r = p % d; + // Whether the magnitude rounds up to q+1. Remainder-based so no + // intermediate exceeds u128 (`r < d ≤ 2^126`, so `r * 2 < 2^127`). + let bump = match rounding { + // Ties (r*2 == d) round the magnitude up = away from zero. + Rounding::NearestAway => r * 2 >= d, + // Floor rounds a negative quotient's magnitude up, a positive + // one down; Ceil is the mirror. + Rounding::Floor => neg && r != 0, + Rounding::Ceil => !neg && r != 0, + Rounding::TowardZero => false, + }; + if bump { + // q + 1 can only wrap when q == u128::MAX (p = 2^128-1, d = 1); + // the saturated value narrows to the same saturated i64 anyway. + q.saturating_add(1) + } else { + q + } +} + +/// Narrow a sign+magnitude quotient to `i64`, saturating out-of-range +/// magnitudes. +fn sat_narrow(neg: bool, q_abs: u128) -> i64 { + if neg { + if q_abs >= 1u128 << 63 { + i64::MIN + } else { + -(q_abs as i64) + } + } else if q_abs > i64::MAX as u128 { + i64::MAX + } else { + q_abs as i64 + } +} + +/// Narrow a sign+magnitude quotient to `i64`, reporting `None` for +/// out-of-range magnitudes. +fn checked_narrow(neg: bool, q_abs: u128) -> Option { + if neg { + if q_abs > 1u128 << 63 { + None + } else { + Some((q_abs as i128).wrapping_neg() as i64) + } + } else if q_abs > i64::MAX as u128 { + None + } else { + Some(q_abs as i64) + } +} + +/// Split the rescale factor into `(numerator, positive denominator)`, +/// folding the denominator's sign onto the numerator. `None` when the +/// denominator is zero (undefined conversion). +fn rescale_factor(from: Rational, to: Rational) -> Option<(i128, u128)> { + // value * (from.num/from.den) / (to.num/to.den) + // = value * from.num * to.den / (from.den * to.num) + let mut num = from.num as i128 * to.den as i128; + let den = from.den as i128 * to.num as i128; + if den == 0 { + return None; + } + if den < 0 { + // |num| ≤ 2^126, so the negation cannot overflow. + num = -num; + } + Some((num, den.unsigned_abs())) +} + +/// Rescale a value from one rational time base to another using 128-bit +/// intermediate arithmetic. Rounding is half-away-from-zero: a tie +/// rounds toward the larger magnitude (e.g. `+1.5 → +2`, `-1.5 → -2`). +/// +/// Total — never panics or wraps: an undefined conversion factor +/// (`from.den * to.num == 0`) returns `0`, and a result outside `i64` +/// range **saturates** to `i64::MAX` / `i64::MIN`. Use +/// [`rescale_checked`] to detect those cases instead, or +/// [`rescale_rnd`] for a different rounding mode. +pub fn rescale(value: i64, from: Rational, to: Rational) -> i64 { + rescale_rnd(value, from, to, Rounding::NearestAway) +} + +/// [`rescale`] with an explicit [`Rounding`] mode. Same totality +/// guarantees: `0` on an undefined factor, saturation at the `i64` +/// boundaries. +pub fn rescale_rnd(value: i64, from: Rational, to: Rational, rounding: Rounding) -> i64 { + let Some((num, den)) = rescale_factor(from, to) else { + return 0; + }; + let neg = (value < 0) != (num < 0); + // |value| ≤ 2^63 and |num| ≤ 2^126, so the magnitude product can + // reach ~2^189 with pathological bases; the true result is then far + // outside i64 either way, so saturate by sign. + let Some(p) = (value.unsigned_abs() as u128).checked_mul(num.unsigned_abs()) else { + return if neg { i64::MIN } else { i64::MAX }; + }; + sat_narrow(neg && p != 0, div_round_abs(p, den, neg, rounding)) +} + +/// [`rescale`] that reports failure instead of papering over it: +/// returns `None` when the conversion factor is undefined +/// (`from.den * to.num == 0`) or the rounded result doesn't fit `i64`. +/// Rounding is half-away-from-zero, matching [`rescale`]. +pub fn rescale_checked(value: i64, from: Rational, to: Rational) -> Option { + let (num, den) = rescale_factor(from, to)?; + let neg = (value < 0) != (num < 0); + let p = (value.unsigned_abs() as u128).checked_mul(num.unsigned_abs())?; + checked_narrow( + neg && p != 0, + div_round_abs(p, den, neg, Rounding::NearestAway), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rescale_samples_to_pts() { + // 48000 samples at 1/48000 base → 1 second at 1/1000 base = 1000 ticks + assert_eq!( + rescale(48000, Rational::new(1, 48000), Rational::new(1, 1000)), + 1000 + ); + } + + #[test] + fn timestamp_seconds() { + let ts = Timestamp::new(48000, TimeBase::new(1, 48000)); + assert!((ts.seconds() - 1.0).abs() < 1e-9); + } + + #[test] + fn rescale_rounds_half_away_from_zero() { + // 1 tick at 1/2 s/tick → 1/1 base = 0.5 → ties up to 1. + assert_eq!(rescale(1, Rational::new(1, 2), Rational::new(1, 1)), 1); + // -1 tick at 1/2 s/tick → -0.5 → ties to -1 (away from zero). + assert_eq!(rescale(-1, Rational::new(1, 2), Rational::new(1, 1)), -1); + // 3 ticks at 1/2 → 1.5 → 2. + assert_eq!(rescale(3, Rational::new(1, 2), Rational::new(1, 1)), 2); + assert_eq!(rescale(-3, Rational::new(1, 2), Rational::new(1, 1)), -2); + } + + #[test] + fn rescale_saturates_instead_of_wrapping() { + // i64::MAX seconds → milliseconds overflows ×1000; the result + // used to wrap through `as i64`, now it saturates. + assert_eq!( + rescale(i64::MAX, Rational::new(1, 1), Rational::new(1, 1000)), + i64::MAX + ); + assert_eq!( + rescale(i64::MIN, Rational::new(1, 1), Rational::new(1, 1000)), + i64::MIN + ); + // Sign flip through a negative factor saturates the other way. + assert_eq!( + rescale(i64::MAX, Rational::new(-1, 1), Rational::new(1, 1000)), + i64::MIN + ); + // Pathological factor whose 128-bit product overflows: still + // saturates by sign instead of panicking. + assert_eq!( + rescale( + i64::MAX, + Rational::new(i64::MAX, 1), + Rational::new(1, i64::MAX) + ), + i64::MAX + ); + // Undefined factor (zero denominator term) stays 0. + assert_eq!(rescale(5, Rational::new(1, 0), Rational::new(1, 1)), 0); + assert_eq!(rescale(5, Rational::new(1, 1), Rational::new(0, 1)), 0); + } + + #[test] + fn rescale_negative_denominator_ties_away_from_zero() { + // 3 ticks × (1/1) ÷ (-2/1) = -1.5 → -2 (half away from zero). + // The old sign handling rounded these ties toward zero. + assert_eq!(rescale(3, Rational::new(1, 1), Rational::new(-2, 1)), -2); + assert_eq!(rescale(-3, Rational::new(1, 1), Rational::new(-2, 1)), 2); + // Non-tie sanity through a negative source den. + assert_eq!(rescale(4, Rational::new(1, -2), Rational::new(1, 1)), -2); + } + + #[test] + fn rescale_checked_reports_failure() { + // In-range conversions match the plain rescale. + assert_eq!( + rescale_checked(48000, Rational::new(1, 48000), Rational::new(1, 1000)), + Some(1000) + ); + // Overflow → None (plain rescale saturates). + assert_eq!( + rescale_checked(i64::MAX, Rational::new(1, 1), Rational::new(1, 1000)), + None + ); + // Undefined factor → None (plain rescale returns 0). + assert_eq!( + rescale_checked(5, Rational::new(1, 0), Rational::new(1, 1)), + None + ); + assert_eq!( + rescale_checked(5, Rational::new(1, 1), Rational::new(0, 1)), + None + ); + // Exactly i64::MIN is representable, one tick below is not. + assert_eq!( + rescale_checked(i64::MIN, Rational::new(1, 1), Rational::new(1, 1)), + Some(i64::MIN) + ); + assert_eq!( + rescale_checked(i64::MIN, Rational::new(2, 1), Rational::new(1, 1)), + None + ); + } + + #[test] + fn rescale_rnd_modes() { + let from = Rational::new(1, 2); + let to = Rational::new(1, 1); + // 5 × (1/2) = 2.5 + assert_eq!(rescale_rnd(5, from, to, Rounding::NearestAway), 3); + assert_eq!(rescale_rnd(5, from, to, Rounding::Floor), 2); + assert_eq!(rescale_rnd(5, from, to, Rounding::Ceil), 3); + assert_eq!(rescale_rnd(5, from, to, Rounding::TowardZero), 2); + // -5 × (1/2) = -2.5 + assert_eq!(rescale_rnd(-5, from, to, Rounding::NearestAway), -3); + assert_eq!(rescale_rnd(-5, from, to, Rounding::Floor), -3); + assert_eq!(rescale_rnd(-5, from, to, Rounding::Ceil), -2); + assert_eq!(rescale_rnd(-5, from, to, Rounding::TowardZero), -2); + // Exact results are mode-independent. + for mode in [ + Rounding::NearestAway, + Rounding::Floor, + Rounding::Ceil, + Rounding::TowardZero, + ] { + assert_eq!(rescale_rnd(4, from, to, mode), 2); + } + // Default mode is NearestAway (matches plain rescale). + assert_eq!( + rescale_rnd(5, from, to, Rounding::default()), + rescale(5, from, to) + ); + } + + #[test] + fn timestamp_rescale_rnd_and_checked() { + // 1 tick at 1/3 s → milliseconds = 333.33… + let ts = Timestamp::new(1, TimeBase::new(1, 3)); + assert_eq!(ts.rescale_rnd(TimeBase::MILLIS, Rounding::Floor).value, 333); + assert_eq!(ts.rescale_rnd(TimeBase::MILLIS, Rounding::Ceil).value, 334); + assert_eq!( + ts.rescale_rnd(TimeBase::MILLIS, Rounding::Ceil).base, + TimeBase::MILLIS + ); + // checked_rescale mirrors rescale in range… + let ok = Timestamp::new(48_000, TimeBase::AUDIO_48K) + .checked_rescale(TimeBase::MILLIS) + .unwrap(); + assert_eq!(ok.value, 1000); + assert_eq!(ok.base, TimeBase::MILLIS); + // …and reports None past it. + let edge = Timestamp::new(i64::MAX, TimeBase::SECONDS); + assert!(edge.checked_rescale(TimeBase::MILLIS).is_none()); + assert_eq!(edge.rescale(TimeBase::MILLIS).value, i64::MAX); + } + + #[test] + fn from_rate_matches_long_form() { + assert_eq!(TimeBase::from_rate(48_000), TimeBase::new(1, 48_000)); + assert_eq!(TimeBase::from_rate(90_000), TimeBase::new(1, 90_000)); + assert_eq!(TimeBase::from_rate(1), TimeBase::new(1, 1)); + } + + #[test] + fn num_den_accessors() { + let tb = TimeBase::new(1, 90_000); + assert_eq!(tb.num(), 1); + assert_eq!(tb.den(), 90_000); + // Const-context callable. + const NUM: i64 = TimeBase::AUDIO_48K.num(); + const DEN: i64 = TimeBase::AUDIO_48K.den(); + assert_eq!(NUM, 1); + assert_eq!(DEN, 48_000); + } + + #[test] + fn is_valid_rejects_zero_terms() { + assert!(TimeBase::new(1, 1000).is_valid()); + // Den == 0: undefined rate. + assert!(!TimeBase::new(1, 0).is_valid()); + // Num == 0: degenerate ratio (everything is zero seconds). + assert!(!TimeBase::new(0, 1).is_valid()); + } + + #[test] + fn ticks_of_is_inverse_of_seconds_of() { + // 1 second on a 1/48000 base = 48000 ticks. + assert_eq!(TimeBase::AUDIO_48K.ticks_of(1.0), 48_000); + // 1 second on a 1/90000 base = 90000 ticks. + assert_eq!(TimeBase::MPEG_TS.ticks_of(1.0), 90_000); + // 0.5 second on 1/1000 base = 500 ticks. + assert_eq!(TimeBase::MILLIS.ticks_of(0.5), 500); + // Round-trip on integer multiples. + let tb = TimeBase::AUDIO_44K1; + assert_eq!(tb.ticks_of(tb.seconds_of(44_100)), 44_100); + } + + #[test] + fn ticks_of_rounds_half_away_from_zero() { + // 0.5 tick on 1/1 base → 1 (positive ties up). + assert_eq!(TimeBase::SECONDS.ticks_of(0.5), 1); + // -0.5 tick on 1/1 base → -1 (negative ties down). + assert_eq!(TimeBase::SECONDS.ticks_of(-0.5), -1); + // 1.5 ticks → 2. + assert_eq!(TimeBase::SECONDS.ticks_of(1.5), 2); + // -1.5 ticks → -2. + assert_eq!(TimeBase::SECONDS.ticks_of(-1.5), -2); + } + + #[test] + fn ticks_of_invalid_inputs() { + // Invalid time base → 0. + assert_eq!(TimeBase::new(1, 0).ticks_of(1.0), 0); + assert_eq!(TimeBase::new(0, 1).ticks_of(1.0), 0); + // Non-finite seconds → 0. + assert_eq!(TimeBase::MILLIS.ticks_of(f64::NAN), 0); + assert_eq!(TimeBase::MILLIS.ticks_of(f64::INFINITY), 0); + assert_eq!(TimeBase::MILLIS.ticks_of(f64::NEG_INFINITY), 0); + } + + #[test] + fn common_constants_match_long_form() { + assert_eq!(TimeBase::SECONDS, TimeBase::new(1, 1)); + assert_eq!(TimeBase::MILLIS, TimeBase::new(1, 1_000)); + assert_eq!(TimeBase::MICROS, TimeBase::new(1, 1_000_000)); + assert_eq!(TimeBase::NANOS, TimeBase::new(1, 1_000_000_000)); + assert_eq!(TimeBase::MPEG_TS, TimeBase::new(1, 90_000)); + assert_eq!(TimeBase::AUDIO_48K, TimeBase::new(1, 48_000)); + assert_eq!(TimeBase::AUDIO_44K1, TimeBase::new(1, 44_100)); + assert_eq!(TimeBase::AUDIO_8K, TimeBase::new(1, 8_000)); + } + + #[test] + fn timestamp_from_seconds() { + let ts = Timestamp::from_seconds(1.0, TimeBase::AUDIO_48K); + assert_eq!(ts.value, 48_000); + assert_eq!(ts.base, TimeBase::AUDIO_48K); + // Round-trip. + assert!((ts.seconds() - 1.0).abs() < 1e-9); + } + + #[test] + fn checked_add_sub_ticks_round_trip() { + let ts = Timestamp::new(100, TimeBase::MILLIS); + assert_eq!(ts.checked_add_ticks(50).unwrap().value, 150); + assert_eq!(ts.checked_sub_ticks(50).unwrap().value, 50); + // Base unchanged through the arithmetic. + assert_eq!(ts.checked_add_ticks(50).unwrap().base, TimeBase::MILLIS); + } + + #[test] + fn checked_add_ticks_detects_overflow() { + let ts = Timestamp::new(i64::MAX - 5, TimeBase::SECONDS); + assert!(ts.checked_add_ticks(10).is_none()); + // Boundary case: i64::MAX exactly is fine. + let near_max = Timestamp::new(i64::MAX - 1, TimeBase::SECONDS); + assert_eq!(near_max.checked_add_ticks(1).unwrap().value, i64::MAX); + } + + #[test] + fn checked_sub_ticks_detects_overflow() { + let ts = Timestamp::new(i64::MIN + 5, TimeBase::SECONDS); + assert!(ts.checked_sub_ticks(10).is_none()); + } + + #[test] + fn checked_diff_rescales_other_onto_self_base() { + // 1 second at 1/48000 minus 500ms at 1/1000 = 500ms = 24000 ticks at 48k. + let a = Timestamp::new(48_000, TimeBase::AUDIO_48K); // 1.0s + let b = Timestamp::new(500, TimeBase::MILLIS); // 0.5s + assert_eq!(a.checked_diff(b), Some(24_000)); + } + + #[test] + fn checked_diff_same_base() { + let a = Timestamp::new(1000, TimeBase::MILLIS); + let b = Timestamp::new(250, TimeBase::MILLIS); + assert_eq!(a.checked_diff(b), Some(750)); + assert_eq!(b.checked_diff(a), Some(-750)); + } +} diff --git a/crates/vendor/oxideav-core/src/vector.rs b/crates/vendor/oxideav-core/src/vector.rs new file mode 100644 index 00000000..4388240d --- /dev/null +++ b/crates/vendor/oxideav-core/src/vector.rs @@ -0,0 +1,1441 @@ +//! Vector graphics frame and primitive types. +//! +//! This module models a resolution-independent, scene-graph-style vector +//! frame so the same [`VectorFrame`] can round-trip through both SVG 1.1 +//! and PDF 1.4 without lossy conversion. The primitive set is the +//! intersection of what those two formats represent natively: +//! +//! * paths built from move / line / quadratic / cubic / elliptic-arc / close +//! commands, +//! * solid + linear-gradient + radial-gradient paints, +//! * stroke style (width, cap, join, miter limit, dash), +//! * even-odd / non-zero fill rules, +//! * 2D affine transforms, +//! * group nodes (transform, opacity, optional clip), +//! * embedded raster passthrough via [`ImageRef`] (carries a child +//! [`VideoFrame`](crate::VideoFrame) — the rasterizer paints the image +//! into vector space). +//! +//! Text nodes are intentionally **deferred to round 2** — text needs +//! font handling and tight scribe coupling that will land alongside the +//! `oxideav-svg` parser (#349). Round 1 is shape-only. +//! +//! No rasterizer / SVG parser / PDF writer lives in `oxideav-core`; those +//! are downstream tasks (#349 / #350 / #351). This module ships only the +//! data types every consumer of the vector pipeline needs to agree on. + +use crate::time::TimeBase; + +/// A decoded vector-graphics frame. +/// +/// The `width` / `height` define the natural rendering canvas size in +/// user units. `view_box` lets a producer separate the user-coordinate +/// system from the canvas (an SVG `viewBox` attribute, or the PDF +/// `MediaBox` vs. `CropBox`); when `None`, callers should treat it as +/// `(0, 0, width, height)`. +#[derive(Clone, Debug)] +pub struct VectorFrame { + /// Viewport width in user units. + pub width: f32, + /// Viewport height in user units. + pub height: f32, + /// Optional view box. `None` defaults to `(0, 0, width, height)`. + pub view_box: Option, + /// Root group of the scene. + pub root: Group, + /// Presentation timestamp in `time_base` units, or `None` if unknown. + pub pts: Option, + /// Time base for `pts`. Consumers that don't care about timing + /// (e.g. a one-shot SVG render) can use `TimeBase::new(1, 1)`. + pub time_base: TimeBase, +} + +impl VectorFrame { + /// Build a `VectorFrame` of the given canvas size with an empty root + /// group, no view box, no timestamp, and a `1/1` time base. + pub fn new(width: f32, height: f32) -> Self { + Self { + width, + height, + view_box: None, + root: Group::default(), + pts: None, + time_base: TimeBase::new(1, 1), + } + } + + /// Replace the view box. + pub fn with_view_box(mut self, view_box: ViewBox) -> Self { + self.view_box = Some(view_box); + self + } + + /// Replace the root group. + pub fn with_root(mut self, root: Group) -> Self { + self.root = root; + self + } + + /// Set the presentation timestamp (in `time_base` units). + pub fn with_pts(mut self, pts: i64) -> Self { + self.pts = Some(pts); + self + } + + /// Replace the time base. + pub fn with_time_base(mut self, time_base: TimeBase) -> Self { + self.time_base = time_base; + self + } +} + +impl Default for VectorFrame { + /// An empty 0×0 frame with an empty root group, no view box, no + /// timestamp, and a `1/1` time base. Useful as a starting point for + /// builder-style construction or as a placeholder in + /// `std::mem::take`-style swaps. + fn default() -> Self { + Self::new(0.0, 0.0) + } +} + +/// User-coordinate system rectangle. Mirrors the SVG `viewBox` attribute +/// and the PDF `MediaBox` / `CropBox` rectangles. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ViewBox { + /// Left edge of the user-coordinate rectangle. + pub min_x: f32, + /// Top edge of the user-coordinate rectangle. + pub min_y: f32, + /// Width of the user-coordinate rectangle, in user units. + pub width: f32, + /// Height of the user-coordinate rectangle, in user units. + pub height: f32, +} + +impl ViewBox { + /// Build a `ViewBox` from its origin and size. + pub const fn new(min_x: f32, min_y: f32, width: f32, height: f32) -> Self { + Self { + min_x, + min_y, + width, + height, + } + } +} + +/// One node in the scene tree. +/// +/// Marked `#[non_exhaustive]` so future variants (text, filters) can +/// be added without breaking downstream `match` arms. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Node { + /// A drawn path with optional fill and stroke. + Path(PathNode), + /// A nested group applying transform / opacity / clip to its children. + Group(Group), + /// An embedded raster image painted into vector space. + Image(ImageRef), + /// A soft-mask composite. The `mask` subtree is rasterised and + /// converted to a per-pixel alpha multiplier (luminance or alpha, + /// per [`MaskKind`]), then applied to the rasterised `content` + /// subtree. Mirrors SVG `` and PDF `SMask` (subtype `Luminosity` + /// vs. `Alpha`). + SoftMask { + /// Subtree rasterised to produce the per-pixel opacity + /// modulator. + mask: Box, + /// How to convert the rasterised mask to a coverage value. + mask_kind: MaskKind, + /// Subtree whose pixels are modulated by the mask. + content: Box, + }, +} + +/// How to interpret a soft mask's rasterised pixels as a coverage +/// modulator. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum MaskKind { + /// Convert the mask's RGB to luminance (ITU-R BT.709 coefficients + /// — Y = 0.2126·R + 0.7152·G + 0.0722·B) and use Y as the + /// per-pixel alpha multiplier. Matches SVG `` default + /// (`mask-type="luminance"`) and PDF `SMask` `/Luminosity`. + #[default] + Luminance, + /// Use the mask's own alpha channel as the multiplier. Matches + /// SVG `` and PDF `SMask` `/Alpha`. + Alpha, +} + +/// A grouping node — applies a transform / opacity / optional clip path +/// to all descendants. Mirrors SVG `` and PDF `q ... Q` graphic-state +/// blocks. +#[derive(Clone, Debug)] +pub struct Group { + /// Coordinate transform applied to children. Identity by default. + pub transform: Transform2D, + /// Group opacity in `0.0..=1.0`. `1.0` is fully opaque. + pub opacity: f32, + /// Optional clip path. Children are clipped to this path's interior + /// (using the path's own fill rule). `None` means "no clip". + pub clip: Option, + /// Child nodes, painted in order (later children over earlier ones). + pub children: Vec, + /// Opaque cache key. When `Some(k)`, a downstream rasterizer is free + /// to memoise the rendered bitmap of this group's content (after + /// `transform` is applied) under key `k`, so re-rendering the same + /// group at the same effective resolution returns the cached bitmap. + /// + /// Producers that emit cacheable content (e.g. scribe shaping a + /// glyph at `(face_id, glyph_id, size_q8, subpixel_x)`) compute a + /// deterministic hash of their identity tuple and put it here. The + /// rasterizer treats it as a black box — `oxideav-core` never + /// inspects the value, so each producer's namespace stays private. + /// + /// `None` (the default) means "do not cache; render fresh every + /// time". Most synthesised vector content (a one-off rectangle, a + /// gradient panel) leaves this `None`. + pub cache_key: Option, +} + +impl Default for Group { + fn default() -> Self { + Self { + transform: Transform2D::identity(), + opacity: 1.0, + clip: None, + children: Vec::new(), + cache_key: None, + } + } +} + +impl Group { + /// An empty group: identity transform, opacity `1.0`, no clip, no + /// children, no cache key. Same as [`Group::default`]. + pub fn new() -> Self { + Self::default() + } + + /// Replace the transform. + pub fn with_transform(mut self, transform: Transform2D) -> Self { + self.transform = transform; + self + } + + /// Set the group opacity in `0.0..=1.0`. + pub fn with_opacity(mut self, opacity: f32) -> Self { + self.opacity = opacity; + self + } + + /// Set the clip path. + pub fn with_clip(mut self, clip: Path) -> Self { + self.clip = Some(clip); + self + } + + /// Append a child node. + pub fn with_child(mut self, child: Node) -> Self { + self.children.push(child); + self + } + + /// Replace the children list wholesale. + pub fn with_children(mut self, children: Vec) -> Self { + self.children = children; + self + } + + /// Set the rasterizer cache key. See [`Group::cache_key`]. + pub fn with_cache_key(mut self, key: u64) -> Self { + self.cache_key = Some(key); + self + } +} + +/// A drawn path with optional fill and stroke. +/// +/// SVG `` and PDF path-painting operators (`f`, `S`, `B`, `f*`, +/// `B*`) both express "one path, optional fill, optional stroke", so a +/// single struct covers both formats. At least one of `fill` / `stroke` +/// would normally be `Some` to produce visible output. +#[derive(Clone, Debug)] +pub struct PathNode { + /// Path geometry, in the local user space of the enclosing group. + pub path: Path, + /// Paint for the path interior. `None` means "not filled". + pub fill: Option, + /// Stroke style for the path outline. `None` means "not stroked". + pub stroke: Option, + /// Fill rule used for `fill` (and for hit-testing the interior). + pub fill_rule: FillRule, +} + +impl PathNode { + /// Build a `PathNode` with `path`, no fill, no stroke, and + /// `FillRule::NonZero`. + pub fn new(path: Path) -> Self { + Self { + path, + fill: None, + stroke: None, + fill_rule: FillRule::NonZero, + } + } + + /// Set the fill paint. + pub fn with_fill(mut self, fill: Paint) -> Self { + self.fill = Some(fill); + self + } + + /// Set the stroke style. + pub fn with_stroke(mut self, stroke: Stroke) -> Self { + self.stroke = Some(stroke); + self + } + + /// Set the fill rule. + pub fn with_fill_rule(mut self, fill_rule: FillRule) -> Self { + self.fill_rule = fill_rule; + self + } +} + +/// A geometric path expressed as a sequence of drawing commands. +/// +/// All coordinates are in the local user space of the enclosing group. +#[derive(Clone, Debug, Default)] +pub struct Path { + /// Drawing commands, executed in order. + pub commands: Vec, +} + +impl Path { + /// An empty path with no commands. + pub fn new() -> Self { + Self::default() + } + + /// Append a [`PathCommand::MoveTo`] — start a new subpath at `p`. + pub fn move_to(&mut self, p: Point) -> &mut Self { + self.commands.push(PathCommand::MoveTo(p)); + self + } + + /// Append a [`PathCommand::LineTo`] — straight line to `p`. + pub fn line_to(&mut self, p: Point) -> &mut Self { + self.commands.push(PathCommand::LineTo(p)); + self + } + + /// Append a [`PathCommand::QuadCurveTo`] — quadratic Bezier to `end` + /// with control point `control`. + pub fn quad_to(&mut self, control: Point, end: Point) -> &mut Self { + self.commands + .push(PathCommand::QuadCurveTo { control, end }); + self + } + + /// Append a [`PathCommand::CubicCurveTo`] — cubic Bezier to `end` + /// with control points `c1` and `c2`. + pub fn cubic_to(&mut self, c1: Point, c2: Point, end: Point) -> &mut Self { + self.commands + .push(PathCommand::CubicCurveTo { c1, c2, end }); + self + } + + /// Append a [`PathCommand::Close`] — close the current subpath. + pub fn close(&mut self) -> &mut Self { + self.commands.push(PathCommand::Close); + self + } +} + +/// A single path-construction command. +/// +/// Marked `#[non_exhaustive]` so smooth-curve / Bezier-shorthand +/// variants can be added later without breaking match arms. +/// +/// Note on `ArcTo`: SVG and PDF both accept elliptic-arc segments in +/// their path syntax (SVG `A` command, PDF via cubic approximation in +/// the writer). We keep the variant in the round-1 IR — converting an +/// arc to its spec-correct cubic-Bezier flattening is a pure function +/// of the arc parameters that downstream rasterizers / writers can do +/// independently. +#[derive(Clone, Copy, Debug, PartialEq)] +#[non_exhaustive] +pub enum PathCommand { + /// Start a new subpath at the given point (SVG `M`). + MoveTo(Point), + /// Straight line from the current point to the given point (SVG `L`). + LineTo(Point), + /// Quadratic Bezier segment from the current point (SVG `Q`). + QuadCurveTo { + /// The single quadratic control point. + control: Point, + /// Segment end point. + end: Point, + }, + /// Cubic Bezier segment from the current point (SVG `C`). + CubicCurveTo { + /// First control point (attached to the segment start). + c1: Point, + /// Second control point (attached to the segment end). + c2: Point, + /// Segment end point. + end: Point, + }, + /// SVG `A`-style elliptic arc segment. `x_axis_rot` is in radians + /// (consistent with `Transform2D::rotate`); `large_arc` / `sweep` + /// match the SVG flag semantics. + ArcTo { + /// Ellipse radius along its X axis, in user units. + rx: f32, + /// Ellipse radius along its Y axis, in user units. + ry: f32, + /// Rotation of the ellipse's X axis relative to the user-space + /// X axis, in radians. + x_axis_rot: f32, + /// When `true`, pick the arc sweep of 180° or more (SVG + /// `large-arc-flag`). + large_arc: bool, + /// When `true`, draw the arc in the positive-angle direction + /// (SVG `sweep-flag`). + sweep: bool, + /// Arc end point. + end: Point, + }, + /// Close the current subpath with a straight line back to its + /// starting point (SVG `Z`). + Close, +} + +/// 2D point in user-space coordinates. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Point { + /// Horizontal coordinate in user units. + pub x: f32, + /// Vertical coordinate in user units (Y grows downward, per the + /// SVG / PDF device-space convention used throughout this module). + pub y: f32, +} + +impl Point { + /// Build a point from its coordinates. + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } +} + +impl From<[f32; 2]> for Point { + fn from([x, y]: [f32; 2]) -> Self { + Self { x, y } + } +} + +impl From<(f32, f32)> for Point { + fn from((x, y): (f32, f32)) -> Self { + Self { x, y } + } +} + +/// A paint server — what fills the inside of a path or strokes its +/// outline. The variant set is the SVG/PDF intersection. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Paint { + /// A single flat RGBA color. + Solid(Rgba), + /// Color stops swept along a straight line. + LinearGradient(LinearGradient), + /// Color stops swept outward from a focal point to a circle. + RadialGradient(RadialGradient), +} + +/// 32-bit straight (non-premultiplied) RGBA color. +/// +/// Matches SVG's `rgb()` + `opacity` model and PDF's `RGB` + `CA`/`ca` +/// graphic-state model. Premultiplication is a rasterizer concern; this +/// IR carries straight alpha to avoid lossy round-trips. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Rgba { + /// Red channel, `0..=255`. + pub r: u8, + /// Green channel, `0..=255`. + pub g: u8, + /// Blue channel, `0..=255`. + pub b: u8, + /// Straight (non-premultiplied) alpha, `0` transparent to `255` opaque. + pub a: u8, +} + +impl Rgba { + /// Build a color from its four channels (straight alpha). + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + /// Fully-opaque color with the given RGB triple. + pub const fn opaque(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b, a: 255 } + } +} + +impl From<(u8, u8, u8, u8)> for Rgba { + fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self { + Self { r, g, b, a } + } +} + +impl From<(u8, u8, u8)> for Rgba { + /// Fully-opaque color with the given RGB triple. + fn from((r, g, b): (u8, u8, u8)) -> Self { + Self { r, g, b, a: 255 } + } +} + +impl From<[u8; 4]> for Rgba { + fn from([r, g, b, a]: [u8; 4]) -> Self { + Self { r, g, b, a } + } +} + +impl From for Paint { + /// Wrap an [`Rgba`] in a `Paint::Solid`. + fn from(color: Rgba) -> Self { + Paint::Solid(color) + } +} + +/// A linear gradient: color stops sweep along the line `start` → `end`. +#[derive(Clone, Debug)] +pub struct LinearGradient { + /// Gradient axis start point (offset `0.0`), in user space. + pub start: Point, + /// Gradient axis end point (offset `1.0`), in user space. + pub end: Point, + /// Color stops, ordered by ascending `offset`. + pub stops: Vec, + /// What to paint past the axis endpoints. + pub spread: SpreadMethod, +} + +impl LinearGradient { + /// Build a `LinearGradient` from `start` → `end` with no stops and + /// `SpreadMethod::Pad`. + pub fn new(start: Point, end: Point) -> Self { + Self { + start, + end, + stops: Vec::new(), + spread: SpreadMethod::Pad, + } + } + + /// Replace the gradient stops. + pub fn with_stops(mut self, stops: Vec) -> Self { + self.stops = stops; + self + } + + /// Append a single stop. + pub fn with_stop(mut self, stop: GradientStop) -> Self { + self.stops.push(stop); + self + } + + /// Set the spread method. + pub fn with_spread(mut self, spread: SpreadMethod) -> Self { + self.spread = spread; + self + } +} + +/// A radial gradient: color stops sweep from `focal` outward to a +/// circle of radius `radius` centered on `center`. When `focal` is +/// `None`, it defaults to `center` (the common case). +#[derive(Clone, Debug)] +pub struct RadialGradient { + /// Center of the outer circle (offset `1.0`), in user space. + pub center: Point, + /// Radius of the outer circle, in user units. + pub radius: f32, + /// Focal point the stops sweep outward from (offset `0.0`). + /// `None` defaults to `center`. + pub focal: Option, + /// Color stops, ordered by ascending `offset`. + pub stops: Vec, + /// What to paint outside the outer circle. + pub spread: SpreadMethod, +} + +impl RadialGradient { + /// Build a `RadialGradient` centered at `center` with `radius`, no + /// focal point, no stops, and `SpreadMethod::Pad`. + pub fn new(center: Point, radius: f32) -> Self { + Self { + center, + radius, + focal: None, + stops: Vec::new(), + spread: SpreadMethod::Pad, + } + } + + /// Set the focal point (defaults to `center` when `None`). + pub fn with_focal(mut self, focal: Point) -> Self { + self.focal = Some(focal); + self + } + + /// Replace the gradient stops. + pub fn with_stops(mut self, stops: Vec) -> Self { + self.stops = stops; + self + } + + /// Append a single stop. + pub fn with_stop(mut self, stop: GradientStop) -> Self { + self.stops.push(stop); + self + } + + /// Set the spread method. + pub fn with_spread(mut self, spread: SpreadMethod) -> Self { + self.spread = spread; + self + } +} + +/// One color stop along a gradient. `offset` is in `0.0..=1.0`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GradientStop { + /// Position of the stop along the gradient axis. `0.0` is the + /// start, `1.0` is the end. + pub offset: f32, + /// Color at this stop. + pub color: Rgba, +} + +impl GradientStop { + /// Build a stop at `offset` (`0.0..=1.0`) with the given color. + pub const fn new(offset: f32, color: Rgba) -> Self { + Self { offset, color } + } +} + +/// What happens past the gradient endpoints. Mirrors SVG +/// `spreadMethod="pad|reflect|repeat"` and PDF gradient `Extend` arrays. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SpreadMethod { + /// Final stop colors extend forever. SVG default. + #[default] + Pad, + /// Gradient mirrors at each boundary. + Reflect, + /// Gradient repeats periodically. + Repeat, +} + +/// Stroke style for a path's outline. +#[derive(Clone, Debug)] +pub struct Stroke { + /// Stroke width in user units, centered on the path. + pub width: f32, + /// Paint applied to the stroked outline. + pub paint: Paint, + /// How open-subpath endpoints are drawn. + pub cap: LineCap, + /// How segment corners are drawn. + pub join: LineJoin, + /// Miter limit ratio. SVG / PDF default is `4.0`. + pub miter_limit: f32, + /// Optional dash pattern. `None` means a solid (undashed) stroke. + pub dash: Option, +} + +impl Stroke { + /// Build a default solid-paint stroke with width `width`. + pub fn solid(width: f32, color: Rgba) -> Self { + Self { + width, + paint: Paint::Solid(color), + cap: LineCap::Butt, + join: LineJoin::Miter, + miter_limit: 4.0, + dash: None, + } + } + + /// Build a stroke with the given `width` and `paint`, and SVG/PDF + /// default cap (`Butt`), join (`Miter`), miter limit (`4.0`), and + /// no dash pattern. + pub fn new(width: f32, paint: Paint) -> Self { + Self { + width, + paint, + cap: LineCap::Butt, + join: LineJoin::Miter, + miter_limit: 4.0, + dash: None, + } + } + + /// Replace the stroke paint. + pub fn with_paint(mut self, paint: Paint) -> Self { + self.paint = paint; + self + } + + /// Set the line cap style. + pub fn with_cap(mut self, cap: LineCap) -> Self { + self.cap = cap; + self + } + + /// Set the line join style. + pub fn with_join(mut self, join: LineJoin) -> Self { + self.join = join; + self + } + + /// Set the miter limit ratio (SVG/PDF default is `4.0`). + pub fn with_miter_limit(mut self, miter_limit: f32) -> Self { + self.miter_limit = miter_limit; + self + } + + /// Set the dash pattern. + pub fn with_dash(mut self, dash: DashPattern) -> Self { + self.dash = Some(dash); + self + } +} + +/// How an open path's endpoints are drawn. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LineCap { + /// Squared-off end flush with the endpoint. SVG / PDF default. + #[default] + Butt, + /// Semicircular end of radius `width / 2` centered on the endpoint. + Round, + /// Squared-off end extending `width / 2` past the endpoint. + Square, +} + +/// How two stroke segments meet at a corner. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LineJoin { + /// Sharp corner extended to a point, subject to the miter limit. + /// SVG / PDF default. + #[default] + Miter, + /// Corner rounded with a circular arc of radius `width / 2`. + Round, + /// Corner cut off with a straight edge across the outer angle. + Bevel, +} + +/// Dash pattern for a stroke. `array` is an alternating +/// dash-on / dash-off length list (in user units); `offset` is the +/// phase offset from the path start. +#[derive(Clone, Debug, Default)] +pub struct DashPattern { + /// Alternating dash-on / dash-off lengths, in user units. An empty + /// array means a solid stroke. + pub array: Vec, + /// Phase offset from the path start, in user units. + pub offset: f32, +} + +impl DashPattern { + /// Build a dash pattern with the given lengths and a `0.0` phase + /// offset. + pub fn new(array: Vec) -> Self { + Self { array, offset: 0.0 } + } + + /// Set the phase offset from the path start. + pub fn with_offset(mut self, offset: f32) -> Self { + self.offset = offset; + self + } +} + +/// Fill rule for self-intersecting and compound paths. Matches SVG's +/// `fill-rule` attribute and PDF's `f` (non-zero) vs. `f*` (even-odd) +/// painting operators. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum FillRule { + /// A point is inside when the winding number of the path around it + /// is non-zero. SVG `fill-rule="nonzero"` (the default) / PDF `f`. + #[default] + NonZero, + /// A point is inside when a ray from it crosses the path an odd + /// number of times. SVG `fill-rule="evenodd"` / PDF `f*`. + EvenOdd, +} + +/// A 2D affine transform stored as the column-major matrix +/// +/// ```text +/// | a c e | | x | +/// | b d f | * | y | +/// | 0 0 1 | | 1 | +/// ``` +/// +/// — i.e. `(x', y') = (a*x + c*y + e, b*x + d*y + f)`. The layout +/// matches SVG's `matrix(a, b, c, d, e, f)` and PDF's `cm` operator +/// argument order, so emitters can serialize fields directly. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Transform2D { + /// X-scale term: contribution of input `x` to output `x`. + pub a: f32, + /// Y-skew term: contribution of input `x` to output `y`. + pub b: f32, + /// X-skew term: contribution of input `y` to output `x`. + pub c: f32, + /// Y-scale term: contribution of input `y` to output `y`. + pub d: f32, + /// X translation, in user units. + pub e: f32, + /// Y translation, in user units. + pub f: f32, +} + +impl Transform2D { + /// The identity transform. `compose(identity, x) == x`. + pub const fn identity() -> Self { + Self { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + e: 0.0, + f: 0.0, + } + } + + /// Build a translation by `(tx, ty)`. + pub const fn translate(tx: f32, ty: f32) -> Self { + Self { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + e: tx, + f: ty, + } + } + + /// Build a non-uniform scale by `(sx, sy)` about the origin. + pub const fn scale(sx: f32, sy: f32) -> Self { + Self { + a: sx, + b: 0.0, + c: 0.0, + d: sy, + e: 0.0, + f: 0.0, + } + } + + /// Build a rotation by `angle_radians` about the origin + /// (counter-clockwise in a Y-up system, clockwise visually under + /// the SVG / PDF Y-down convention — this matches both formats). + pub fn rotate(angle_radians: f32) -> Self { + let (s, c) = angle_radians.sin_cos(); + Self { + a: c, + b: s, + c: -s, + d: c, + e: 0.0, + f: 0.0, + } + } + + /// Build a horizontal skew (shear along X) by `angle_radians`. + pub fn skew_x(angle_radians: f32) -> Self { + Self { + a: 1.0, + b: 0.0, + c: angle_radians.tan(), + d: 1.0, + e: 0.0, + f: 0.0, + } + } + + /// Build a vertical skew (shear along Y) by `angle_radians`. + pub fn skew_y(angle_radians: f32) -> Self { + Self { + a: 1.0, + b: angle_radians.tan(), + c: 0.0, + d: 1.0, + e: 0.0, + f: 0.0, + } + } + + /// Compose `self ∘ other` — the resulting transform applies + /// `other` first, then `self`, to a point. Equivalent to + /// `self.matrix() * other.matrix()` in column-vector form. + pub fn compose(&self, other: &Self) -> Self { + Self { + a: self.a * other.a + self.c * other.b, + b: self.b * other.a + self.d * other.b, + c: self.a * other.c + self.c * other.d, + d: self.b * other.c + self.d * other.d, + e: self.a * other.e + self.c * other.f + self.e, + f: self.b * other.e + self.d * other.f + self.f, + } + } + + /// Apply this transform to a point. + pub fn apply(&self, p: Point) -> Point { + Point { + x: self.a * p.x + self.c * p.y + self.e, + y: self.b * p.x + self.d * p.y + self.f, + } + } + + /// `true` when this transform is bit-identical to the identity. + /// Useful for emitters that want to skip a no-op `matrix(...)` / + /// `cm` write. + pub fn is_identity(&self) -> bool { + *self == Self::identity() + } +} + +impl Default for Transform2D { + fn default() -> Self { + Self::identity() + } +} + +/// An embedded raster image painted into vector space. +/// +/// `bounds` is the axis-aligned rectangle (in the local user space, +/// before `transform`) that the image is painted into; SVG `` +/// `x/y/width/height` and PDF `Do` with a matrix-pre-positioned +/// `Image` XObject both reduce to this shape. +#[derive(Clone, Debug)] +pub struct ImageRef { + /// Embedded raster payload. Boxed so a `Node::Image` variant + /// doesn't bloat every other [`Node`] case. + pub frame: Box, + /// Destination rectangle the image is scaled into, in the local + /// user space (before `transform`). + pub bounds: Rect, + /// Additional transform applied to the placed image, on top of the + /// enclosing group's transform. + pub transform: Transform2D, +} + +/// Axis-aligned rectangle in user-space coordinates. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Rect { + /// Left edge. + pub x: f32, + /// Top edge. + pub y: f32, + /// Rectangle width, in user units. + pub width: f32, + /// Rectangle height, in user units. + pub height: f32, +} + +impl Rect { + /// Build a rectangle from its top-left corner and size. + pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self { + Self { + x, + y, + width, + height, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::time::TimeBase; + + fn approx_point(a: Point, b: Point) -> bool { + (a.x - b.x).abs() < 1e-5 && (a.y - b.y).abs() < 1e-5 + } + + #[test] + fn path_builder_produces_command_sequence() { + let mut p = Path::new(); + p.move_to(Point::new(0.0, 0.0)) + .line_to(Point::new(10.0, 0.0)) + .quad_to(Point::new(15.0, 5.0), Point::new(10.0, 10.0)) + .cubic_to( + Point::new(5.0, 15.0), + Point::new(0.0, 10.0), + Point::new(0.0, 0.0), + ) + .close(); + assert_eq!(p.commands.len(), 5); + assert_eq!(p.commands[0], PathCommand::MoveTo(Point::new(0.0, 0.0))); + assert_eq!(p.commands[4], PathCommand::Close); + } + + #[test] + fn transform_identity_round_trips() { + let id = Transform2D::identity(); + assert!(id.is_identity()); + let p = Point::new(3.5, -2.25); + assert_eq!(id.apply(p), p); + } + + #[test] + fn transform_translate_round_trip() { + let t = Transform2D::translate(10.0, -5.0); + assert_eq!(t.apply(Point::new(0.0, 0.0)), Point::new(10.0, -5.0)); + assert_eq!(t.apply(Point::new(1.0, 1.0)), Point::new(11.0, -4.0)); + } + + #[test] + fn transform_scale_round_trip() { + let s = Transform2D::scale(2.0, 3.0); + assert_eq!(s.apply(Point::new(1.0, 1.0)), Point::new(2.0, 3.0)); + assert_eq!(s.apply(Point::new(0.0, 0.0)), Point::new(0.0, 0.0)); + } + + #[test] + fn transform_rotate_quarter_turn() { + let r = Transform2D::rotate(std::f32::consts::FRAC_PI_2); + // Under SVG/PDF Y-down with matrix(c,s,-s,c,0,0): + // (1, 0) rotates to (cos, sin) = (0, 1). + assert!(approx_point( + r.apply(Point::new(1.0, 0.0)), + Point::new(0.0, 1.0) + )); + // (0, 1) rotates to (-sin, cos) = (-1, 0). + assert!(approx_point( + r.apply(Point::new(0.0, 1.0)), + Point::new(-1.0, 0.0) + )); + } + + #[test] + fn transform_compose_identity_is_left_and_right_unit() { + let t = Transform2D::translate(7.0, 11.0); + let id = Transform2D::identity(); + assert_eq!(id.compose(&t), t); + assert_eq!(t.compose(&id), t); + } + + #[test] + fn transform_compose_translate_then_scale() { + // Apply translate(2,3) first, then scale(10,10): + // p -> p + (2,3) -> 10*(p+(2,3)) = 10p + (20,30). + let scale = Transform2D::scale(10.0, 10.0); + let translate = Transform2D::translate(2.0, 3.0); + let composed = scale.compose(&translate); + let result = composed.apply(Point::new(1.0, 1.0)); + assert!(approx_point(result, Point::new(30.0, 40.0))); + } + + #[test] + fn transform_compose_matches_sequential_apply() { + // Composition equivalence: composed.apply(p) == a.apply(b.apply(p)). + let a = Transform2D::rotate(0.5); + let b = Transform2D::translate(3.0, -1.0); + let composed = a.compose(&b); + let p = Point::new(2.0, 5.0); + let direct = composed.apply(p); + let stepwise = a.apply(b.apply(p)); + assert!(approx_point(direct, stepwise)); + } + + #[test] + fn group_default_is_identity_opacity_one_no_clip() { + let g = Group::default(); + assert!(g.transform.is_identity()); + assert_eq!(g.opacity, 1.0); + assert!(g.clip.is_none()); + assert!(g.children.is_empty()); + } + + #[test] + fn group_nesting_with_transforms() { + // Outer group translates by (10, 10); inner group scales by 2. + // A point (1, 1) drawn at the inner level should land at + // (12, 12) after the outer transform is also applied — but the + // tree itself only stores the local transforms. This test + // pins down that the nested data is preserved verbatim, since + // composing transforms is a rasterizer responsibility. + let inner = Group { + transform: Transform2D::scale(2.0, 2.0), + children: vec![Node::Path(PathNode { + path: { + let mut p = Path::new(); + p.move_to(Point::new(1.0, 1.0)); + p + }, + fill: Some(Paint::Solid(Rgba::opaque(255, 0, 0))), + stroke: None, + fill_rule: FillRule::NonZero, + })], + ..Group::default() + }; + let outer = Group { + transform: Transform2D::translate(10.0, 10.0), + children: vec![Node::Group(inner)], + ..Group::default() + }; + match &outer.children[0] { + Node::Group(g) => { + assert_eq!(g.transform, Transform2D::scale(2.0, 2.0)); + assert_eq!(g.children.len(), 1); + } + _ => panic!("expected a Group child"), + } + assert_eq!(outer.transform, Transform2D::translate(10.0, 10.0)); + } + + #[test] + fn vector_frame_construction() { + let frame = VectorFrame { + width: 100.0, + height: 50.0, + view_box: Some(ViewBox { + min_x: 0.0, + min_y: 0.0, + width: 100.0, + height: 50.0, + }), + root: Group::default(), + pts: Some(0), + time_base: TimeBase::new(1, 1000), + }; + assert_eq!(frame.width, 100.0); + assert_eq!(frame.height, 50.0); + assert!(frame.view_box.is_some()); + assert_eq!(frame.pts, Some(0)); + } + + #[test] + fn rgba_constructors() { + let c = Rgba::opaque(10, 20, 30); + assert_eq!(c.a, 255); + let c2 = Rgba::new(10, 20, 30, 128); + assert_eq!(c2.a, 128); + } + + #[test] + fn gradient_stop_round_trips() { + let s = GradientStop::new(0.5, Rgba::opaque(255, 0, 0)); + assert_eq!(s.offset, 0.5); + let s2 = GradientStop::new(0.5, Rgba::opaque(255, 0, 0)); + assert_eq!(s, s2); + } + + #[test] + fn stroke_solid_defaults() { + let s = Stroke::solid(2.0, Rgba::opaque(0, 0, 0)); + assert_eq!(s.width, 2.0); + assert_eq!(s.cap, LineCap::Butt); + assert_eq!(s.join, LineJoin::Miter); + assert_eq!(s.miter_limit, 4.0); + assert!(s.dash.is_none()); + } + + #[test] + fn soft_mask_construction_and_inspection() { + // Wrap a path in a SoftMask node with a luminance mask. Round- + // trips both children verbatim through clone + match. + fn rect_path() -> PathNode { + let mut p = Path::new(); + p.move_to(Point::new(0.0, 0.0)) + .line_to(Point::new(10.0, 0.0)) + .line_to(Point::new(10.0, 10.0)) + .line_to(Point::new(0.0, 10.0)) + .close(); + PathNode { + path: p, + fill: Some(Paint::Solid(Rgba::opaque(255, 255, 255))), + stroke: None, + fill_rule: FillRule::NonZero, + } + } + let n = Node::SoftMask { + mask: Box::new(Node::Path(rect_path())), + mask_kind: MaskKind::Luminance, + content: Box::new(Node::Path(rect_path())), + }; + match &n { + Node::SoftMask { + mask_kind, content, .. + } => { + assert_eq!(*mask_kind, MaskKind::Luminance); + match content.as_ref() { + Node::Path(_) => {} + _ => panic!("expected Path content"), + } + } + _ => panic!("expected SoftMask"), + } + } + + #[test] + fn mask_kind_default_is_luminance() { + assert_eq!(MaskKind::default(), MaskKind::Luminance); + } + + #[test] + fn vector_frame_default_is_empty_zero_size() { + let f = VectorFrame::default(); + assert_eq!(f.width, 0.0); + assert_eq!(f.height, 0.0); + assert!(f.view_box.is_none()); + assert!(f.root.children.is_empty()); + assert!(f.pts.is_none()); + assert_eq!(f.time_base, TimeBase::new(1, 1)); + } + + #[test] + fn vector_frame_new_sets_canvas_size() { + let f = VectorFrame::new(640.0, 480.0); + assert_eq!(f.width, 640.0); + assert_eq!(f.height, 480.0); + assert!(f.view_box.is_none()); + assert!(f.root.children.is_empty()); + assert!(f.pts.is_none()); + } + + #[test] + fn vector_frame_builder_chain() { + let vb = ViewBox::new(0.0, 0.0, 100.0, 100.0); + let f = VectorFrame::new(100.0, 100.0) + .with_view_box(vb) + .with_pts(42) + .with_time_base(TimeBase::new(1, 90_000)); + assert_eq!(f.view_box, Some(vb)); + assert_eq!(f.pts, Some(42)); + assert_eq!(f.time_base, TimeBase::new(1, 90_000)); + } + + #[test] + fn vector_frame_with_root_replaces_root() { + let root = Group::new().with_opacity(0.5); + let f = VectorFrame::new(10.0, 10.0).with_root(root); + assert_eq!(f.root.opacity, 0.5); + } + + #[test] + fn view_box_new_round_trips_fields() { + let vb = ViewBox::new(1.0, 2.0, 3.0, 4.0); + assert_eq!(vb.min_x, 1.0); + assert_eq!(vb.min_y, 2.0); + assert_eq!(vb.width, 3.0); + assert_eq!(vb.height, 4.0); + } + + #[test] + fn rect_new_round_trips_fields() { + let r = Rect::new(1.0, 2.0, 3.0, 4.0); + assert_eq!(r.x, 1.0); + assert_eq!(r.y, 2.0); + assert_eq!(r.width, 3.0); + assert_eq!(r.height, 4.0); + } + + #[test] + fn group_new_matches_default() { + let a = Group::new(); + let b = Group::default(); + assert!(a.transform.is_identity()); + assert_eq!(a.opacity, b.opacity); + assert!(a.clip.is_none()); + assert_eq!(a.children.len(), b.children.len()); + assert_eq!(a.cache_key, b.cache_key); + } + + #[test] + fn group_builder_chain() { + let mut clip = Path::new(); + clip.move_to(Point::new(0.0, 0.0)) + .line_to(Point::new(1.0, 1.0)) + .close(); + let g = Group::new() + .with_transform(Transform2D::translate(5.0, 7.0)) + .with_opacity(0.25) + .with_clip(clip) + .with_cache_key(0xdead_beef); + assert_eq!(g.transform, Transform2D::translate(5.0, 7.0)); + assert_eq!(g.opacity, 0.25); + assert!(g.clip.is_some()); + assert_eq!(g.cache_key, Some(0xdead_beef)); + } + + #[test] + fn group_with_child_appends() { + let g = Group::new() + .with_child(Node::Group(Group::new())) + .with_child(Node::Group(Group::new().with_opacity(0.5))); + assert_eq!(g.children.len(), 2); + match &g.children[1] { + Node::Group(inner) => assert_eq!(inner.opacity, 0.5), + _ => panic!("expected Group child"), + } + } + + #[test] + fn group_with_children_replaces_list() { + let g = Group::new() + .with_child(Node::Group(Group::new())) + .with_children(vec![Node::Group(Group::new().with_opacity(0.1))]); + assert_eq!(g.children.len(), 1); + match &g.children[0] { + Node::Group(inner) => assert_eq!(inner.opacity, 0.1), + _ => panic!("expected Group child"), + } + } + + #[test] + fn path_node_new_then_builder() { + let mut p = Path::new(); + p.move_to(Point::new(0.0, 0.0)) + .line_to(Point::new(10.0, 0.0)); + let n = PathNode::new(p) + .with_fill(Paint::Solid(Rgba::opaque(255, 0, 0))) + .with_stroke(Stroke::solid(1.0, Rgba::opaque(0, 0, 0))) + .with_fill_rule(FillRule::EvenOdd); + assert!(n.fill.is_some()); + assert!(n.stroke.is_some()); + assert_eq!(n.fill_rule, FillRule::EvenOdd); + } + + #[test] + fn path_node_new_defaults() { + let n = PathNode::new(Path::new()); + assert!(n.fill.is_none()); + assert!(n.stroke.is_none()); + assert_eq!(n.fill_rule, FillRule::NonZero); + } + + #[test] + fn point_from_array_and_tuple() { + let p1: Point = [1.0_f32, 2.0_f32].into(); + let p2: Point = (3.0_f32, 4.0_f32).into(); + assert_eq!(p1, Point::new(1.0, 2.0)); + assert_eq!(p2, Point::new(3.0, 4.0)); + } + + #[test] + fn rgba_from_tuples_and_array() { + let a: Rgba = (10u8, 20u8, 30u8, 40u8).into(); + let b: Rgba = (50u8, 60u8, 70u8).into(); + let c: Rgba = [1u8, 2u8, 3u8, 4u8].into(); + assert_eq!(a, Rgba::new(10, 20, 30, 40)); + assert_eq!(b, Rgba::opaque(50, 60, 70)); + assert_eq!(c, Rgba::new(1, 2, 3, 4)); + } + + #[test] + fn paint_from_rgba_wraps_solid() { + let p: Paint = Rgba::opaque(1, 2, 3).into(); + match p { + Paint::Solid(c) => assert_eq!(c, Rgba::opaque(1, 2, 3)), + _ => panic!("expected Paint::Solid"), + } + } + + #[test] + fn linear_gradient_new_then_builder() { + let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0)) + .with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0))) + .with_stop(GradientStop::new(1.0, Rgba::opaque(255, 255, 255))) + .with_spread(SpreadMethod::Reflect); + assert_eq!(g.start, Point::new(0.0, 0.0)); + assert_eq!(g.end, Point::new(1.0, 0.0)); + assert_eq!(g.stops.len(), 2); + assert_eq!(g.spread, SpreadMethod::Reflect); + } + + #[test] + fn linear_gradient_with_stops_replaces() { + let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0)) + .with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0))) + .with_stops(vec![GradientStop::new(0.0, Rgba::opaque(1, 1, 1))]); + assert_eq!(g.stops.len(), 1); + assert_eq!(g.stops[0].offset, 0.0); + } + + #[test] + fn radial_gradient_new_then_builder() { + let g = RadialGradient::new(Point::new(5.0, 5.0), 10.0) + .with_focal(Point::new(4.0, 4.0)) + .with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0))) + .with_spread(SpreadMethod::Repeat); + assert_eq!(g.center, Point::new(5.0, 5.0)); + assert_eq!(g.radius, 10.0); + assert_eq!(g.focal, Some(Point::new(4.0, 4.0))); + assert_eq!(g.stops.len(), 1); + assert_eq!(g.spread, SpreadMethod::Repeat); + } + + #[test] + fn radial_gradient_with_stops_replaces() { + let g = RadialGradient::new(Point::new(0.0, 0.0), 1.0) + .with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0))) + .with_stops(vec![GradientStop::new(1.0, Rgba::opaque(1, 1, 1))]); + assert_eq!(g.stops.len(), 1); + assert_eq!(g.stops[0].offset, 1.0); + } + + #[test] + fn stroke_new_defaults() { + let s = Stroke::new(3.0, Paint::Solid(Rgba::opaque(0, 0, 0))); + assert_eq!(s.width, 3.0); + assert_eq!(s.cap, LineCap::Butt); + assert_eq!(s.join, LineJoin::Miter); + assert_eq!(s.miter_limit, 4.0); + assert!(s.dash.is_none()); + } + + #[test] + fn stroke_builder_chain() { + let s = Stroke::solid(1.0, Rgba::opaque(0, 0, 0)) + .with_cap(LineCap::Round) + .with_join(LineJoin::Bevel) + .with_miter_limit(10.0) + .with_dash(DashPattern::new(vec![2.0, 1.0]).with_offset(0.5)) + .with_paint(Paint::Solid(Rgba::opaque(128, 128, 128))); + assert_eq!(s.cap, LineCap::Round); + assert_eq!(s.join, LineJoin::Bevel); + assert_eq!(s.miter_limit, 10.0); + let d = s.dash.expect("dash set"); + assert_eq!(d.array, vec![2.0, 1.0]); + assert_eq!(d.offset, 0.5); + match s.paint { + Paint::Solid(c) => assert_eq!(c, Rgba::opaque(128, 128, 128)), + _ => panic!("expected Paint::Solid"), + } + } + + #[test] + fn dash_pattern_new_zero_offset() { + let d = DashPattern::new(vec![1.0, 2.0, 3.0]); + assert_eq!(d.array, vec![1.0, 2.0, 3.0]); + assert_eq!(d.offset, 0.0); + } + + #[test] + fn dash_pattern_with_offset_sets_phase() { + let d = DashPattern::new(vec![1.0]).with_offset(0.25); + assert_eq!(d.offset, 0.25); + } +} diff --git a/crates/vendor/oxideav-dts/Cargo.toml b/crates/vendor/oxideav-dts/Cargo.toml new file mode 100644 index 00000000..4894f742 --- /dev/null +++ b/crates/vendor/oxideav-dts/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "oxideav-dts" +publish = false +version = "0.0.1" +edition = "2021" +rust-version = "1.80" +license = "MIT" +repository = "https://github.com/OxideAV/oxideav-dts" +authors = ["Mark Karpeles"] +description = "Pure-Rust DTS audio decoder (Core profile, with EXSS/XCH/XXCH/X96/XLL extensions planned) for the oxideav framework" + +readme = "README.md" +homepage = "https://github.com/OxideAV/oxideav-dts" +keywords = ["multimedia", "audio", "dts", "dca", "codec"] +categories = ["multimedia::encoding", "multimedia::audio"] + +[dependencies] +oxideav-core = { path = "../oxideav-core", optional = true } + +[features] +default = ["registry"] +# Gate the oxideav-core dep + Decoder trait impl + register!() macro +# behind this feature so the crate also builds with +# `--no-default-features --lib` for standalone consumers (CLI tools, +# stream-only parsers) that just want the structural frame-header +# parser without the codec-registry surface. +registry = ["dep:oxideav-core"] + +# Vendored verbatim — see scripts/vendor-oxideav.sh. Upstream does not build +# under this repository's `-D warnings`, and making it would mean carrying a +# patch set across every refresh. +[lints.rust] +warnings = "allow" + +[lints.clippy] +all = "allow" diff --git a/crates/vendor/oxideav-dts/LICENSE b/crates/vendor/oxideav-dts/LICENSE new file mode 100644 index 00000000..ffe2468a --- /dev/null +++ b/crates/vendor/oxideav-dts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Karpelès Lab Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/vendor/oxideav-dts/README.md b/crates/vendor/oxideav-dts/README.md new file mode 100644 index 00000000..680c77af --- /dev/null +++ b/crates/vendor/oxideav-dts/README.md @@ -0,0 +1,426 @@ +# oxideav-dts + +[![CI](https://github.com/OxideAV/oxideav-dts/actions/workflows/ci.yml/badge.svg)](https://github.com/OxideAV/oxideav-dts/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/oxideav-dts.svg)](https://crates.io/crates/oxideav-dts) [![docs.rs](https://docs.rs/oxideav-dts/badge.svg)](https://docs.rs/oxideav-dts) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +A pure-Rust DTS (DTS Coherent Acoustics) decoder for the +[oxideav](https://github.com/OxideAV/oxideav) framework, built clean-room +from a locally-staged copy of ETSI TS 102 114 V1.3.1. + +## Status + +This crate is a **Core-profile decoder with the complete decode chain +landed** (extensions — EXSS / XCH / XXCH / X96 / XLL — are out of +scope). The frame container, structural parsing, and the full DSP +reconstruction chain are in place: the registry `Decoder` **decodes +raw 16-bit *and* 14-bit container DTS Core frames to PCM end to end** +(§5.3 → §5.4 → §5.5 → §C.2.5), emitting a planar S32 `AudioFrame` — +including, since round 439, §D.10 VQ/ADPCM frames through the built-in +Annex D code books. A +14-bit-packed frame (either container byte order) is unpacked to the +raw-16-bit-word domain in `send_packet` and decodes through the identical +chain, producing **bit-exact** PCM to the equivalent raw-16-bit frame +(asserted byte-for-byte in the registry test suite). The decoder also +**carries the §C.2.5 +per-channel QMF filter tail across frames** (`CoreStreamDecoder`) so a +multi-frame elementary stream reconstructs without a per-frame +filter-warmup transient. This full-chain output is **validated against +black-box `ffmpeg` reference decodes** of three bundled +fixtures: the 5-frame stereo stream (Pearson correlation 1.0, 100 % +sign agreement on both channels), a 10-frame +**5.1 stream** (`AMODE 9` = C L R SL SR + `LFE Mode2`), where **all +five primary channels and the LFE channel** are shape-identical to +the reference (correlation 1.000000 per plane; +`tests/black_box_ffmpeg_lfe.rs`), and — new in round 429 — the +5-frame spec-built **joint-intensity stream** (`JOINX = 1`), +shape-identical on both channels +(`tests/black_box_joint_intensity.rs`), confirming the +reconstruction chain — including the §5.5 LFE phase and §C.2.6 64× +interpolation — is correct up to the implementation-defined output +`rScale` gain (the spec leaves §C.2.5 `rScale` non-normative). The +§5.4.1 Table 5-28 side-info tail is handled for **dynamic range** +(`DYNF`: the 8-bit `RANGE` code is read as signed Q2 — +`dB = (int8)code × 0.25`, `dts_dynrng_to_db` — and the linear gain +applied to the reconstructed PCM after QMF synthesis) and the **side-info CRC** +(`CPF`: the 16-bit `SICRC` is consumed for framing, not verified). +**LFE-bearing frames** (`LFF != 0`) now decode correctly: the §5.5 LFE +phase (`2·LFF·nSSC` 8-bit samples + `LFEscaleIndex`) is consumed before +the audio-data phase so the audio-data cursor stays aligned, and the LFE +samples are dequantised (§D.1.2 `RMS_7BIT` scale + `0.035` step) and +upsampled through the §C.2.6 `InterpolationFIR()` polyphase convolution +(`LfeChannel`); the registry `Decoder` emits the decoded LFE channel as +a trailing equal-length plane of the planar S32 `AudioFrame` (the +interpolation lands exactly the primary `nSSC·256` per-frame length). +**Joint-intensity frames** (`JOINX > 0`) decode and are **validated**: +the §5.4.1 Table 5-28 `JOIN_SHUFF` / `JOIN_SCALES` side-info tail is +walked (the per-channel 3-bit `QSCALES` selector then one biased +quantization index per imported sub-band, resolved through the §D.3 +joint-scale table `JScaleTbl`), the §C.2.3 sub-band copy imports the +source channel's sub-band samples — scaled by the matching +`JOIN_SCALES` factor — before QMF synthesis, and (round 429) the +§C.2.5 driving call widens each jointly-coded channel's active-subband +count to the **source** channel's `nSUBS` per the spec's driving-call +note ("For joint intensity coded subbands, it must be set to that of +the source channel"), so the imported sub-bands actually reach the +output. Because no reachable black-box encoder emits `JOINX != 0` +(verified by parsing its output across its whole accepted parameter +matrix), the validation streams are **spec-built** field-by-field +(deterministic builder, `tests/common/mod.rs`): every frame is +confirmed by parsing to carry `JOINX == [0, 1]`, decode is bit-exact +against an analytic reconstruction, and the committed 5-frame joint +fixture is accepted cleanly by the black-box `ffmpeg` reference +decoder with our PCM **shape-identical** to its decode on both +channels (correlation 1.000000; `tests/black_box_joint_intensity.rs` +— the jointly-coded channel's upper sixteen sub-bands exist only +through the §C.2.3 import). A boundary battery +(`tests/joint_edge_cases.rs`) covers forward-pointing `JOINX`, +Huffman / Linear7 `JOIN_SHUFF` books, `JOINX`+`DYNF`+`CPF` tail +ordering, `JOINX`+`FRONT_SUM` over the effective range, +multi-subframe joint frames, zero-slack framing, and the three typed +error paths. **Sum/difference frames** are also handled: the §C.2.4 +front L/R matrix (`L' = L+R`, `R' = L−R`) is applied on the reconstructed +sub-band samples when the `FRONT_SUM` (`SUMF`) flag is set — or +unconditionally for `AMODE == 3` — and the surround L/R matrix when +`SURROUND_SUM` (`SUMS`) is set, using the Table 5-4 channel ordering to +locate each pair (`AmodeArrangement::front_lr_channels` / +`surround_lr_channels`), between §C.2.3 and §C.2.5. The **§5.7 +optional-information chunks** are decoded too: `parse_aux_data` / +`FrameView::aux_data` walk the §5.7.1 Auxiliary Data chunk (decode +time stamp + the dynamic **embedded downmix coefficients**, resolved +through the §D.11 `DmixTable` and applicable to planar PCM via +`DynamicDownmix::apply_planar`), and `parse_rev2_aux` / +`FrameView::rev2_aux` walk the §5.7.2 Rev2 chunk (embedded-ES downmix +scale, per-subsubframe broadcast DRC values, `DIALNORM_rev2aux`). +**§D.10 VQ / ADPCM frames** +(high-frequency VQ sub-bands and ADPCM prediction) **decode out of the +box** (round 439): the two Annex D code books the spec deliberately +omits ("Due to its extensive size, this table is not included here", +§D.10.1/§D.10.2) are staged as clean-room data tables under +`docs/audio/dts/tables/` and **built into the crate** +(`VqCodebooks::builtin()`, the default of every decoder). Our decode +of the §D.10-bearing fixture is shape-identical to the black-box +reference on **every frame class** — HF-VQ, ADPCM, and the combined +`HFLAG = 1` frame — at Pearson 1.000000 and 95-98 dB SNR after the +one implementation-defined output-scale constant +(`tests/black_box_d10.rs`). The last Core-profile decode blocker is +gone. + +### What works today + +- **Frame-header parsing** (`parse_frame_header` / + `parse_frame_header_14bit`, typed `DtsFrameHeader`) — the §5.3 Core + sync header for all four bitstream forms (16-bit big/little-endian and + the two 14-bit container forms, via the `unpack14` helpers), including + the trailing single-bit / small-field flags, the optional 16-bit + `HEADER_CRC` field, and the post-CRC trailing window (multirate-inter, + version, copy-history, PCMR, front/surround sum, and the §5.3.1 + Table 5-20 `DIALNORM` dialog-normalization gain). +- **Frame framing** — `iter_frames` / `iter_frames_14bit` / + `FrameIterator` / `FrameView` plus `find_next_sync` walk and resync a + multi-frame elementary stream (raw and 14-bit container streams are + routed by encoding). +- **Side-information decode** — the §5.4.1 Primary Audio Coding Side + Information walker (`decode_primary_side_info_at`) decodes the + SSC/PSC prefix, PMODE/PVQ/ABITS/TMODE/SCALES planes, and the TMODE + codebooks end-to-end through SCALES. +- **DSP primitives** — clean-room transcriptions of the building blocks + the §5.5 audio-data reconstruction needs: the §C.2.1 block-code + decoder (both the modulus and table-look-up variants), the §C.2.2 + inverse-ADPCM predictor, the §C.2.3 / §C.2.4 sum-difference and + joint-subband steps, the §C.2.5 32-band synthesis QMF + (`QmfSynthesis`), the §D.2 quantization step-size tables and §5.5 + inverse-quantization scale composition, the §D.8 512-tap 32-band + interpolation FIR coefficient sets plus the two §D.8 512-tap **LFE** + interpolation FIR sets (`RA_COEFF_LFE64` / `RA_COEFF_LFE128`) with the + typed §C.2.6 `LfeInterpolationSelection` (`nDecimationSelect`) driver + selector **and the §C.2.6 `InterpolationFIR()` polyphase convolution + driver body** (`LfeInterpolator`, `src/lfe_synth.rs`: each decimated + LFE sample expands to 64/128 interpolated PCM samples, carrying the + `taps_per_phase − 1` inter-sub-frame history) and the **§5.5 LFE phase + dequant** (`LfeChannel`: 8-bit `LFE[n]` → `rLFE[n] = LFE[n]·nScale· + 0.035` with the §D.1.2 `RMS_7BIT` scale, then `InterpolationFIR(LFF)`), + the §5.5 `nQType` dispatch, the + §D.6 block code books, the + §D.5.1/§D.5.3/§D.5.4/§D.5.5/§D.5.7/§D.5.8/§D.5.9 audio-data + quantization-index Huffman code books (the seven lowest `ABITS` + families — 3/5/7/9/13/17/25-level; the 17-level group is the seven + §D.5.8 books `A17`…`G17` and the 25-level group the seven §D.5.9 + books `A25`…`G25` whose deepest codeword reaches 14 bits — feeding + the `nQType == 1` path, decoding to signed `AUDIO[m]` levels via + `AudioHuffCodebook` / `decode_audio_huff_at` with a per-book + `max_code_len` walk bound), and the §5.5 `DSYNC` subsubframe check + word. +- **Header → §C.2.5 QMF-driver bridge** — `DtsFrameHeader` now resolves + the two header-sourced parameters of the §C.2.5 `QMFInterpolation()` + driver directly: `filter_bank_selection()` maps the `MULTIRATE_INTER` + bit (the spec's `FILTS` "Multirate Interpolator Switch" of §5.3.1 + Table 5-15) to the §D.8 coefficient set (`false`/`FILTS==0` → + non-perfect `raCoeffLossy`, `true`/`FILTS==1` → perfect + `raCoeffLossLess`), and `output_r_scale()` derives the post-filterbank + output gain `rScale = 2^(PCMR_bits−1)` from the §5.3.1 Table 5-17 + source-PCM resolution (`Some(32768/524288/8388608)` for 16/20/24-bit, + `None` for the two reserved PCMR codes). A parsed header now feeds + `QmfSynthesis::synthesize` end-to-end with no out-of-band parameters. +- **Per-frame multi-channel synthesis** — `MultiChannelQmf` owns one + persistent `QmfSynthesis` per channel (the §C.2.5 `aPrmCh[ch]` filter + objects) and runs the per-channel driving call + `aPrmCh[ch].QMFInterpolation(FILTS, nSUBS[ch])` for every channel of a + frame in one step, with the frame-wide `FILTS` and output `rScale` + shared across channels. It reconstructs a whole frame's PCM either + **planar** (per-channel `Vec`) or **interleaved** (sample-major), + takes per-channel `nSUBS`, persists every channel's inter-frame filter + tail across calls, and offers a `synthesize_planar_from_header` + convenience that sources `FILTS`/`rScale` straight from a parsed + `DtsFrameHeader` (returning `Ok(None)` for the reserved PCMR codes). + +- **End-to-end frame decode** — `decode_core_frame(bytes, &header)` + chains the §5.3.2 Audio Coding Header (Table 5-21), the per-subframe + §5.4.1 side-info walk (Table 5-28) **including the `RANGE`/`SICRC` + tail**, and the §5.5 + §C.2.5 reconstruction into one raw-bytes-to-PCM + call. It decodes normal **and termination** frames — including + `JOINX > 0` (joint-intensity, see below), `DYNF != 0` frames (the + signed-Q2 dynamic-range gain is applied to each subframe's PCM after + synthesis), `CPF == 1` frames (the `SICRC` word is consumed), and + §5.4.1 `PSC > 0` partial subsubframes. `SubframePcmDecoder` (with + `decode_subframe` / `decode_frame`) is the lower-level composition of + the §5.5 `decode_audio_data_subframe_at` walk and the §C.2.5 + `MultiChannelQmf` synthesis, owning a persistent per-channel filter so + the inter-subframe filter tail carries across subframes. +- **Streaming decode** — `CoreStreamDecoder` wraps a stream-lifetime + `SubframePcmDecoder` so the §C.2.5 per-channel filter tail (`raX[]` / + `raZ[]`) carries across **frame** boundaries of a contiguous + elementary stream — the spec's QMF filter is a continuous per-channel + object, not reset between frames. `decode_core_frame` (a fresh + per-call decoder) keeps single-frame semantics; `CoreStreamDecoder` is + the multi-frame path. The registry `Decoder::receive_frame` holds a + persistent `CoreStreamDecoder` so multi-packet streams carry the + filter tail across packets, and emits a planar S32 `AudioFrame`; + joint-intensity frames decode (see above) and §D.10 VQ/ADPCM frames + decode through the built-in code books (round 439; round 446 sweeps + **every index of both books** through this path bit-exactly) — no + Core frame class maps to `Unsupported` for missing book data. Carrying the + inter-frame tail is what makes the decode match the `ffmpeg` reference + (correlation 1.0 vs 0.73 with a per-frame reset — see + `tests/black_box_ffmpeg_pcm.rs`). +- **§5.4.1 side-info tail** — `decode_primary_side_info_tail_at` / + `SideInfoTail` walk the full Table 5-28 tail after the SCALES block: + the per-channel `JOIN_SHUFF[ch]` (3-bit `QSCALES` selector) and the + `JOIN_SCALES[ch][n]` loop (one biased quantization index per imported + sub-band `n ∈ [nSUBS[ch], nSUBS[nSourceCh])`, resolved through the + §D.3 joint-scale table), the 8-bit `RANGE` dynamic-range code + (`DYNF`, resolved as **8-bit signed Q2** via `dts_dynrng_to_db` / + `dts_dynrng_to_linear` — `dB = (int8)code × 0.25`, per the staged + `docs/audio/dts/dts-drc-dynrng.md`; the §D.4 table stays available + as reference data keyed by its offset-binary printed Index), and + the 16-bit `SICRC` (`CPF`). The resolved `JOIN_SCALES` + factors are carried in `SideInfoTail::join_scales`. +- **§D.3 joint-intensity scale table** — `join_scale` / + `JOIN_SCALE_FACTOR` transcribe the §D.3 `JScaleTbl` (129 entries, + index 64 → unity), the look-up the biased `JOIN_SCALES` index feeds. +- **§C.2.3 joint-intensity sub-band copy** — `decode_core_frame` / + `SubframePcmDecoder::decode_subframe_with_joint` import a jointly-coded + channel's high sub-bands from its source channel + (`nSourceCh = JOINX[ch] − 1`), each scaled by the matching + `JOIN_SCALES` factor, on the decoded sub-band matrices **before** QMF + synthesis — and both the §C.2.4 sum/difference matrix and the §C.2.5 + synthesis then run over the **effective** active-subband counts + (widened to the source channel's `nSUBS` for jointly-coded channels, + per the §C.2.5 driving-call note). `JOINX > 0` frames decode end to + end, bit-exact against an analytic reconstruction and shape-identical + to a black-box reference decode of the bundled spec-built joint + fixture (`tests/fixtures/dts_joint_5_frames.bin`, re-derived + byte-for-byte from its deterministic builder in CI). + +- **§5.3.1 termination frames (`FTYPE = 0`) + §5.4.1 partial + subsubframe (`PSC`)** — a termination-frame subframe whose + `SSC`/`PSC` prefix signals `PSC ∈ 1..=7` decodes its **last** + subsubframe as partial (`PSC` subband samples per active subband + instead of 8), yielding the valid-prefix PCM + (`((nSSC−1)·8 + PSC) · 32` samples per channel; frame total always + `(NBLKS+1) · 32`) with the §5.5 bit budget exact through the + truncation — per-sample carriers extract `PSC` codewords, the + §D.6 block-code carrier extracts `ceil(PSC/4)` four-sample words + keeping the first `PSC`, and the DSYNC trailer follows the partial + subsubframe. `PSC > 0` on a *normal* frame declines with the typed + `PartialSubsubframeInNormalFrame` ("It exists only in a + termination frame", PDF p.30). The `SHORT` deficit surfaces as + `DtsFrameHeader::termination_pad_samples` (the `1..=31`-sample + output pad; the decode chain returns decoded samples only). LFE + planes are truncated to the valid prefix (the §5.5 LFE count + `2·LFF·nSSC` has no `PSC` term). Validated by a full `SSC × PSC` + grid, JOINX/DYNF/CPF/ASPF/LFE/multi-subframe interaction and + corruption batteries over a spec-built termination fixture + (`tests/fixtures/dts_term_5_frames.bin`, re-derived in CI); the + black-box reference decoder was observed to *skip* `FTYPE = 0` + frames at the parser level, so the reference comparison pins the + normal-frame prefix shape-exactly and the termination tail is + validated in-crate (`tests/black_box_termination.rs`). +- **§5.6 Unpack Optional Information (Table 5-30)** — + `decode_optional_info_at` walks the flag-gated region after the + last audio-data array (`TIMES` time code stamp when `TIMEF`, + `AUXCT`/`AUXD` auxiliary bytes when `AUXF`, `OCRC` when + `CPF && DYNF` — surfaced raw per the spec's "shall not be + applied"), and the `*_with_info` decode entry points + (`decode_core_frame_with_info`, + `SubframePcmDecoder::decode_core_frame_with_info_into`, + `CoreStreamDecoder::decode_frame_with_info`) run it from the real + end-of-audio bit cursor so callers get PCM + optional info in one + pass (validated bit-identical to the plain decode on the bundled + fixture). +- **§D.11 downmix scale-factor tables** — `DMIX_TABLE` (241 × u16, + the Q15 `DmixTable` column, `-60 dB` … unity) and `INV_DMIX_TABLE` + (201 × u32, the Q16 `InvDmixTbl` column for `DmixTblIndex >= 40`), + with `dmix_scale` / `inv_dmix_scale` look-ups and + `decode_dmix_code` (the §5.7.1 Table 5-31 9-bit coefficient-code + resolution: phase MSB, one-biased low byte, `0` → exact `0.0`). + Every entry of both columns is unit-verified against the spec's own + closed-form dB-ramp derivation (including the deliberate index-216 + half-power point `1/sqrt(2)`). +- **§5.7.1 Auxiliary Data chunk** — `find_aux_data` (the spec's + suggested backward search for the DWORD-aligned `nSYNCAUX` + `0x9A1105A0`), `parse_aux_data` / `parse_aux_data_at` / + `FrameView::aux_data`: the 36-bit decode time stamp (nibble + realignment + both `0b1011` marker validations) and the dynamic + downmix coefficient table (`DownmixType`, Table 5-32; + `DeriveNumDwnMixCodeCoeffs()` from `anNumCh[AMODE]` + LFE; + `DynamicDownmix::coefficient_matrix` through §D.11; + `DynamicDownmix::apply_planar` folds planar PCM through the table + with the §C.2.5 `int()` truncation convention). The `nAUXCRC16` is + **verified** with the Annex B CRC-16 over its documented coverage + span (`AuxData::crc_valid`). +- **§5.7.2 Rev2 Auxiliary Data Chunk** — `find_rev2_aux` / + `parse_rev2_aux` / `FrameView::rev2_aux`: `nRev2AUXDataByteSize` + (validated `3..=128`), the embedded-ES downmix scale index + (validated `40..=240`, resolved via §D.11 + `Rev2AuxChunk::es_downmix_scale`), the size-gated broadcast + metadata — per-subsubframe 8-bit DRC values for + `DRCversion_Rev2AUX == 1` (one per `32·(NBLKS+1)/256` subsubframe, + Table 5-34; unsupported versions are skipped per the spec's ignore + rule) and the 5-bit `DIALNORM_rev2aux` (`DNG = −value` dB, + Table 5-36) — and the `nRev2AUXCRC16` read at its size-located + offset (also skipping the reserved field of "unspecified + duration") and **verified** with the Annex B CRC-16 over the + `nRev2AUXDataByteSize − 2` covered bytes + (`Rev2AuxChunk::crc_valid`). `Rev2Drc::gains_db` / + `Rev2Drc::multipliers` resolve the DRC codes through the §5.7.2 + `dts_dynrng_to_db()` signed-Q2 function (the legacy-core + coefficient space the spec says these values replace; the raw + codes stay exposed). On the decode path, a CRC-verified version-1 + Rev2AUX DRC payload **overrides** the legacy `DYNF` gain per + §5.7.2.2: `decode_core_frame` / `CoreStreamDecoder` suppress the + per-subframe `RANGE` multiply and scale each Table 5-34 256-sample + subsubframe window by its own Rev2 gain instead + (`tests/rev2_drc_override.rs`). +- **§D.10 VQ decode with the built-in code books (rounds 434 + 439)** + — the two §5.5 sub-paths that long sat behind the spec-omitted + §D.10 code books decode by default. The books themselves are staged + clean-room data (`docs/audio/dts/tables/dts-d10-1-adpcm-coeff-vq.csv`, + 4096 × 4 signed Q13; `dts-d10-2-hfreq-vq.csv`, 1024 × 32 int8; + chain of custody in `docs/audio/dts/provenance/11-extractor-d10-vq.md`, + two independent sources agreeing on every value), transcribed + SHA-256-pinned into `d10_tables` and exposed as + `HfVqCodebook::builtin()` / `AdpcmVqCodebook::builtin()` / + `VqCodebooks::builtin()` — the default of every decoder; + `VqCodebooks::none()` (via `set_vq_codebooks`) restores the typed + bookless blocker. The staged recovery record also settled two + §D.10.2 facts the spec left open: the element divisor is + **`2^4 = 16`** (the printed "24" is a typo — a `2^4` with a lost + superscript; the literal reading costs a constant 2/3 gain on every + VQ-coded HF subband) and element `2k` is entry `k`'s **low** byte. + On an HF-VQ frame the phase-1 10-bit `nVQIndex` region (ahead of + the LFE phase) is walked and each HF subband's rows are + `SCALES[ch][n][0] · HFREQ[m]` (the Table 5-29 `Scale`/`rScale` + naming conflation is spec-verbatim; the p.33 HFREQ prose resolves + it and gives the termination-frame valid-prefix pick rule); on a + `PMODE != 0` frame the §C.2.2 inverse-ADPCM predictor runs from the + captured 12-bit `PVQ` index, the per-subband reconstruction history + (`AdpcmHistory`) carried across subsubframes/subframes and gated at + frame boundaries by the §5.3.1 `HFLAG` Predictor History Flag + Switch. Validated bit-exactly against analytic reconstructions + (`tests/d10_vq_decode.rs`: HFLAG carry/reset grid, PSC × HF-VQ, + PSC × ADPCM, an HF+ADPCM+LFE+JOINX+DYNF kitchen-sink frame) and + black-box (`tests/black_box_d10.rs` + + `tests/fixtures/dts_d10_5_frames.bin`): with the built-in books our + decode is **shape-identical to the reference on all five frames** + (Pearson 1.000000 per frame per channel; 95-98 dB SNR after the √2 + output-scale constant), and the registry surface decodes the same + stream by default, bit-identical to the direct path. Round 446 + closes the index space: the **full-book sweeps** drive all 1024 + §D.10.2 and all 4096 §D.10.1 vectors through the real bitstream + decode path, each frame bit-exact against an analytic + reconstruction recomputed from the built-in books + (`tests/d10_vq_decode.rs`), and the committed 12-frame + **book-coverage stream** (`tests/black_box_d10_coverage.rs`) + black-box-confirms 480 swept vectors — the §D.10.2 + duplicate-codeword cluster, both book heads and tails, four frames + predicting all 32 subbands of both channels, and two `HFLAG = 1` + history-chained frames — shape-identical to the reference decode + (Pearson 1.000000; 90.5-95.9 dB SNR after the same √2 constant). +- **Annex B CRC-16** — `dts_crc16` / `dts_crc16_update` / + `DTS_CRC16_TABLE`: the single normative DTS CRC (CRC-CCITT, + polynomial `0x1021`, init `0xFFFF`, MSB-first, no reflection, no + final XOR — the CRC-16/CCITT-FALSE parameter set), per the staged + `docs/audio/dts/dts-crc16.md`. Drives the aux / Rev2-aux + verification above; the core `HCRC` / `AHCRC` / `SICRC` / `OCRC` + stay unverified **by spec mandate** ("The CRC value test shall not + be applied" — they are informational placeholders). + +### Not yet implemented + +- Extensions (EXSS / XCH / XXCH / X96 / XLL) are out of scope for the + current Core-profile effort. +- `DtsFrameHeader::verify_header_crc` returns `None` **by design**, + not because of a docs gap: the Annex B CRC algorithm is documented + and implemented (`dts_crc16`), but §5.3.1 states "The CRC value + test shall not be applied" for the core `HCRC` (likewise `AHCRC` / + `SICRC` / `OCRC`), and the spec does not normatively pin the + `HCRC` coverage span. The raw 16-bit field stays surfaced for + pass-through callers; the genuinely testable check words + (`nAUXCRC16`, `nRev2AUXCRC16`) *are* verified. + +## Usage + +```rust +use oxideav_dts::{parse_frame_header, iter_frames}; + +let bytes: &[u8] = b""; // a DTS Core (raw 16-bit) elementary stream + +// Parse a single Core frame header. +if let Ok(_hdr) = parse_frame_header(bytes) { + // inspect channel layout, sample-rate code, frame size, ... +} + +// Walk a multi-frame stream. +for frame in iter_frames(bytes) { + let _payload = frame.payload(); +} + +// Decode one whole Core frame to planar PCM (common Core case). +use oxideav_dts::decode_core_frame; +if let Ok(hdr) = parse_frame_header(bytes) { + match decode_core_frame(bytes, &hdr) { + Ok(pcm) => { /* pcm[ch] is a Vec of reconstructed samples */ } + Err(_unsupported_tail_or_vq) => { /* not the common Core case */ } + } +} +``` + +The DSP primitives are public crate-root re-exports +(`decode_block_code`, `QmfSynthesis`, `fir_step`, `dequant_subsubframe`, +…) for callers experimenting with the reconstruction chain directly. + +## Cargo features + +| Feature | Default | Effect | +|------------|---------|--------| +| `registry` | yes | Pulls in `oxideav-core` and registers the codec via `register`, exposing the `Decoder` trait surface and `probe_dts`. Disable (`default-features = false`, build `--no-default-features --lib`) for a standalone build that exposes only the header parser, framing, and DSP primitives without the framework dependency. | + +## Clean-room provenance + +Implemented entirely from a locally-staged copy of ETSI TS 102 114 +V1.3.1 under `docs/audio/dts/`. No external decoder or library source +was consulted; binaries are used only as black-box fixture generators +and validators, never as a source of constants or layout. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/crates/vendor/oxideav-dts/VENDOR.toml b/crates/vendor/oxideav-dts/VENDOR.toml new file mode 100644 index 00000000..85a5152e --- /dev/null +++ b/crates/vendor/oxideav-dts/VENDOR.toml @@ -0,0 +1,9 @@ +# Written by scripts/vendor-oxideav.sh. Do not edit, and do not +# hand-edit the vendored sources beside it — change them upstream +# and re-run the script. +source = "https://github.com/OxideAV/oxideav-dts" +commit = "528203ed608223c5137843009054e05920af5c50" +describe = "528203e" +version = "0.0.1" +vendored_at = "2026-08-24T08:53:00Z" +patches = [] diff --git a/crates/vendor/oxideav-dts/src/audio_array.rs b/crates/vendor/oxideav-dts/src/audio_array.rs new file mode 100644 index 00000000..d8dd574c --- /dev/null +++ b/crates/vendor/oxideav-dts/src/audio_array.rs @@ -0,0 +1,1664 @@ +//! DTS Coherent Acoustics — §5.5 Primary Audio Data Arrays (`Audio +//! Data`) decode walk (ETSI TS 102 114 V1.3.1, Table 5-29, staged PDF +//! p.31-33). +//! +//! Round 340 (2026-06-19) composes the already-landed per-subband +//! primitives into the §5.5 `Audio Data` block: the per-subsubframe +//! nested loop that extracts the eight `AUDIO[m]` quantization indices +//! for every `(ch, n)` subband (dispatching on the round-258 +//! [`AudioQuantType`] resolved from the `(ABITS, SEL)` pair), applies +//! the round-293 §5.5 `rScale · AUDIO[m]` transient-aware +//! dequantization, runs the round-228 §C.2.2 inverse-ADPCM predictor +//! where `PMODE != 0`, and consumes the §5.5 `DSYNC` trailers — all the +//! way to the per-channel subband-sample matrix +//! `aPrmCh[ch].aSubband[n].aSample[m]` the §C.2.5 QMF synthesis +//! consumes. +//! +//! The Table 5-29 `Audio Data` pseudocode (staged PDF p.31-32), +//! transcribed verbatim: +//! +//! ```text +//! for (nSubSubFrame=0; nSubSubFrame AUDIO[m..m+4]; +//! } +//! // dequant: rScale = rStepSize·SCALES[ch][n][transient]; +//! // rScale *= arADJ[ch][SEL[ch][nABITS-1]]; +//! nSample = 8*nSubSubFrame; +//! aSample[nSample+m] = rScale * AUDIO[m]; // m<8 +//! if (PMODE[ch][n] != 0) InverseADPCM(); +//! } +//! if ((nSubSubFrame==nSSC-1) || (ASPF==1)) { +//! DSYNC = ExtractBits(16); +//! if (DSYNC != 0xffff) "DSYNC error"; +//! } +//! } +//! ``` +//! +//! # Scope +//! +//! Two §5.5 sub-paths consume the Annex D §D.10 VQ code books, which +//! the ETSI spec deliberately omits ("Due to its extensive size, this +//! table is not included here", §D.10.1 / §D.10.2, PDF p.255) and +//! which are now staged as clean-room data and **built into the +//! crate** ([`crate::VqCodebooks::builtin`], round 439 — see +//! `docs/audio/dts/dts-d10-vq-tables-GAP.md`, CLOSED): +//! +//! * The **high-frequency VQ subbands** loop (`n ∈ [nVQSUB, nSUBS)`, +//! `nVQIndex = ExtractBits(10); HFreqVQ.LookUp(...)`) uses the §D.10.2 +//! "High Frequency Subbands" 32-sample VQ code book (1024 vectors; +//! entries decode as two 8-bit signed integers, low byte first, +//! **each ÷ 2⁴** — [`crate::unpack_hfreq_vq_entry`]). Its 10-bit +//! indices are captured structurally +//! ([`crate::scan_hf_vq_indices_at`]) and the book supplied as an +//! [`HfVqFill`] to [`decode_audio_data_subframe_vq_at`] +//! reconstructs the subband (`SCALES[ch][n][0] · HFREQ[m]` over the +//! subframe's rows). +//! * The **inverse-ADPCM coefficient lookup** (`PMODE != 0`, the §5.4.1 +//! `ADPCMCoeffVQ.LookUp(nVQIndex, PVQ[ch][n])`) uses the §D.10.1 +//! ADPCM-coefficient VQ code book (4096 × 4 stored integers, actual +//! coefficient = entry ÷ 2¹³ — [`crate::adpcm_vq_coeff`]); with the +//! book supplied as an [`AdpcmContext`] the §C.2.2 +//! predictor runs per subsubframe from the captured 12-bit +//! `pvq_index`, primed by the persistent [`AdpcmHistory`]. +//! +//! Without the matching book (a caller-stripped decoder, +//! [`crate::VqCodebooks::none`]) each sub-path surfaces the typed +//! [`AudioArrayError::VqCodebookUnavailable`] refusal. A frame +//! whose primary channels are all linearly / Huffman / block coded with +//! `PMODE == 0` and `nVQSUB == nSUBS` (the common Core case) decodes to +//! PCM end-to-end with no books at all. + +use crate::audio_data::{audio_quant_type, AudioQuantType}; +use crate::audio_huff::{decode_audio_huff_at, AudioHuffCodebook}; +use crate::bitreader::BitReader; +use crate::block_code::decode_block_code; +use crate::cos_mod::NUM_SUBBAND; +use crate::d10_vq::{AdpcmVqCodebook, HfVqCodebook}; +use crate::dsync::DSYNC_WORD; +use crate::inverse_adpcm::{inverse_adpcm_decode_f64, NUM_ADPCM_COEFF}; +use crate::side_info::ScaleFactorAdjustment; +use crate::step_size::{transient_scale_index, StepSizeTable, SAMPLES_PER_SUBSUBFRAME}; +use crate::subframe::ChannelSideInfo; +use crate::{Error, Result}; + +/// Errors specific to the §5.5 audio-data array walk that are not +/// already covered by the crate-level [`Error`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum AudioArrayError { + /// A subband required an Annex D §D.10 VQ code book that the + /// caller stripped from the decoder + /// ([`crate::VqCodebooks::none`]; the built-in books are the + /// default since round 439, so this fires only on an explicit + /// opt-out). Either the §D.10.2 high-frequency VQ book (a + /// `nVQSUB < nSUBS` subband) or the §D.10.1 ADPCM-coefficient VQ + /// book (a `PMODE != 0` subband). Carries the channel/subband + /// that hit the blocker and which book is missing. + VqCodebookUnavailable { + /// 0-based channel index. + ch: usize, + /// 0-based subband index. + n: usize, + /// `true` = high-frequency VQ (§D.10.2); `false` = ADPCM + /// coefficient VQ (§D.10.1). + high_frequency_vq: bool, + }, + /// The §5.5 LFE phase (§2.2) dequant failed — a reserved §D.1.2 + /// `RMS_7BIT` scale index or an absent LFE channel + /// ([`crate::LfeChannelError`]). + LfePhase(crate::LfeChannelError), + /// The caller-supplied §5.5 phase-1 HF-VQ index capture does not + /// match the per-channel `[nVQSUB, nSUBS)` shape the walk needs + /// (wrong channel count or wrong per-channel index count). + HfVqIndexShape { + /// 0-based channel index whose captured indices mismatched + /// (equal to the channel count when the outer capture is the + /// wrong length). + ch: usize, + }, + /// A `PMODE != 0` subband carried no captured 12-bit `PVQ` index + /// (a structurally impossible [`ChannelSideInfo`] — the §5.4.1 + /// walk always captures the index when the PMODE bit is set — + /// so this only surfaces on hand-built side info). + MissingPvqIndex { + /// 0-based channel index. + ch: usize, + /// 0-based subband index. + n: usize, + }, +} + +impl core::fmt::Display for AudioArrayError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + AudioArrayError::VqCodebookUnavailable { + ch, + n, + high_frequency_vq, + } => { + let book = if *high_frequency_vq { + "§D.10.2 high-frequency VQ" + } else { + "§D.10.1 ADPCM-coefficient VQ" + }; + write!( + f, + "oxideav-dts: channel {ch} subband {n} needs the {book} code \ + book, which this decoder was configured without \ + (VqCodebooks::none(); the built-in books are the default)" + ) + } + AudioArrayError::LfePhase(e) => write!(f, "oxideav-dts: §5.5 LFE phase: {e}"), + AudioArrayError::HfVqIndexShape { ch } => write!( + f, + "oxideav-dts: §5.5 phase-1 HF-VQ index capture shape mismatch \ + at channel {ch}" + ), + AudioArrayError::MissingPvqIndex { ch, n } => write!( + f, + "oxideav-dts: channel {ch} subband {n} has PMODE set but no \ + captured §5.4.1 PVQ index" + ), + } + } +} + +impl std::error::Error for AudioArrayError {} + +/// The §D.6 `V…` block-code-book word width (in bits) for the `ABITS` +/// family `1..=7`, read off the §D.6 table titles (staged PDF +/// p.231-236): `V3` 7-bit, `V5` 10-bit, `V7` 12-bit, `V9` 13-bit, +/// `V13` 15-bit, `V17` 17-bit, `V25` 19-bit. Each block-code word +/// expands to four samples. +fn block_code_word_bits(abits: u8) -> Option { + Some(match abits { + 1 => 7, + 2 => 10, + 3 => 12, + 4 => 13, + 5 => 15, + 6 => 17, + 7 => 19, + _ => return None, + }) +} + +/// The §5.5 "No Further Encoding" (NFE) binary-code word width (in +/// bits) for an `ABITS` index, sign-extended on read. Table 5-26's +/// even "or 2ⁿ" level forms (PDF p.27) give `2^(ABITS-3)` levels for +/// `ABITS ∈ 8..=26` (e.g. ABITS 8 → 32 = 2⁵, ABITS 26 → 2²³), so the +/// binary code carries `ABITS - 3` bits. For `ABITS > 26` (the +/// no-SEL-transmitted region) the same `ABITS - 3` width holds up to +/// the 32-bit reader bound. +fn nfe_word_bits(abits: u8) -> Option { + if abits < 8 { + return None; + } + let bits = u32::from(abits) - 3; + if (1..=32).contains(&bits) { + Some(bits) + } else { + None + } +} + +/// Sign-extend a `width`-bit two's-complement field read as an +/// unsigned integer (`pCQGroup->ppQ[nSEL]->SignExtension(nCode)`). +fn sign_extend(value: u32, width: u32) -> i32 { + debug_assert!((1..=32).contains(&width)); + let shift = 32 - width; + ((value << shift) as i32) >> shift +} + +/// Extract one subband's `count` `AUDIO[m]` quantization indices for +/// one subsubframe from `br`, dispatching on the `(abits, sel)` pair +/// per the §5.5 Table 5-29 `switch (nQType)`. +/// +/// `count` is 8 for a normal subsubframe and `PSC ∈ 1..=7` for the +/// trailing **partial** subsubframe of a termination frame (§5.4.1 +/// PSC, PDF p.30: "PSC indicates the number of subband samples held +/// in a partial subsubframe for each of the active subbands"). +/// +/// * [`AudioQuantType::NoBits`] — `count` zeros, no bits read. +/// * [`AudioQuantType::Huffman`] — `count` §D.5 Huffman-coded indices +/// (the code is per-sample, so a partial subsubframe extracts +/// exactly `count` codewords). +/// * [`AudioQuantType::NoEncoding`] — `count` sign-extended +/// binary-code fields of [`nfe_word_bits`] width (likewise +/// per-sample). +/// * [`AudioQuantType::BlockCode`] — [`block_code_word_bits`]-wide +/// block-code words, each expanding to **four** samples; a partial +/// subsubframe extracts `ceil(count / 4)` words and keeps the first +/// `count` decoded samples. The four-sample word is indivisible, so +/// the encoder pads the trailing word the same way the spec +/// documents for the other fixed-span carrier (§5.5 HFREQ, PDF +/// p.33: samples beyond the subframe "are padded with either zeros +/// or 'don't care' and then vector-quantized" and the decoder "will +/// only pick" the live ones). +fn extract_subband_audio( + br: &mut BitReader<'_>, + abits: u8, + sel: u8, + count: usize, +) -> Result<[i32; SAMPLES_PER_SUBSUBFRAME]> { + debug_assert!((1..=SAMPLES_PER_SUBSUBFRAME).contains(&count)); + let mut audio = [0_i32; SAMPLES_PER_SUBSUBFRAME]; + match audio_quant_type(abits, sel) { + AudioQuantType::NoBits => {} + AudioQuantType::Huffman => { + // SEL selects the §D.5 book within the ABITS group. + let codebook = AudioHuffCodebook::from_abits_sel(abits, sel) + .ok_or(Error::HuffmanDecodeFailed { table: "AUDIO" })?; + for slot in audio.iter_mut().take(count) { + let level = decode_audio_huff_in(br, codebook)?; + *slot = i32::from(level); + } + } + AudioQuantType::NoEncoding => { + let width = nfe_word_bits(abits).ok_or(Error::InvalidStepSize { abits })?; + for slot in audio.iter_mut().take(count) { + let raw = br.read_bits(width)?; + *slot = sign_extend(raw, width); + } + } + AudioQuantType::BlockCode => { + let width = block_code_word_bits(abits).ok_or(Error::InvalidStepSize { abits })?; + let n_levels = u32::from(crate::audio_data::QUANT_LEVELS[abits as usize]); + let mut m = 0usize; + while m < count { + let code = br.read_bits(width)?; + if count - m >= 4 { + decode_block_code(code, n_levels, &mut audio[m..m + 4])?; + } else { + // Trailing partial word: decode all four samples, + // keep only the live `count - m` (the rest are the + // encoder's pad). + let mut word = [0_i32; 4]; + decode_block_code(code, n_levels, &mut word)?; + audio[m..count].copy_from_slice(&word[..count - m]); + } + m += 4; + } + } + } + Ok(audio) +} + +/// Decode one §D.5 Huffman `AUDIO[m]` index through a `BitReader` +/// already positioned mid-stream (the [`decode_audio_huff_at`] +/// byte-offset entry point re-seeks from a byte boundary, which the +/// per-subsubframe walk cannot do because it shares one running +/// reader). This re-walks the book bit-at-a-time from `br`. +fn decode_audio_huff_in(br: &mut BitReader<'_>, codebook: AudioHuffCodebook) -> Result { + // Bridge through the byte-offset API by re-reading from the + // current absolute bit position over the same backing buffer. + // `decode_audio_huff_at` borrows the buffer immutably and reports + // bits_consumed; we then advance `br` by that many bits. + let pos = br.absolute_bit_position(); + let bytes = br.backing_bytes(); + let (level, consumed) = decode_audio_huff_at(bytes, pos, codebook)?; + br.skip_bits(consumed as u32)?; + Ok(level) +} + +/// Per-channel decoded subband-sample matrix for one subframe: row `s` +/// (`s ∈ 0..n_ssc*8`) is the §C.2.5 per-sample subband vector +/// `[aSubband[0].aSample[s], …, aSubband[31].aSample[s]]` for one +/// channel. The QMF synthesis consumes this directly. +pub type SubbandSampleMatrix = Vec<[f64; NUM_SUBBAND]>; + +/// Decode the §5.5 LFE phase (the `if (LFF > 0) { … }` block of the +/// `docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §2.2 +/// walker) for one subframe, returning the interpolated LFE PCM and the +/// number of bits consumed. +/// +/// The LFE phase sits between the high-frequency-VQ phase (§2.1, empty +/// for the accepted Core case where `nVQSUB == nSUBS`) and the +/// per-subsubframe audio-data phase (§2.3). It reads `2·LFF·nSSC` 8-bit +/// two's-complement decimated LFE samples followed by an 8-bit +/// `LFEscaleIndex`, dequantises (`rLFE[n] = LFE[n]·nScale·0.035` with the +/// §D.1.2 `RMS_7BIT` scale), then upsamples via the §C.2.6 +/// `InterpolationFIR(LFF)` polyphase convolution ([`crate::LfeChannel`]). +/// +/// * `bytes` / `bit_offset` — positioned at the first LFE-phase bit. +/// * `lff` — the frame header's non-zero `LFF` (1 → 128×, 2 → 64×). +/// * `n_ssc` — the subframe's subsubframe count (`SSC + 1`). +/// * `lfe` — the persistent per-channel [`crate::LfeChannel`] whose +/// §C.2.6 history carries across subframes. +/// +/// Returns `(lfe_pcm, bits_consumed)`. The PCM length is +/// `2·LFF·nSSC·(64 | 128)`. +/// +/// # Errors +/// +/// * [`Error::UnexpectedEof`] on a truncated LFE region; +/// * [`AudioArrayError::LfePhase`] wrapping a [`crate::LfeChannelError`] +/// (a reserved §D.1.2 scale index, or `lff == 0`). +pub fn decode_lfe_phase_at( + bytes: &[u8], + bit_offset: usize, + lff: u8, + n_ssc: usize, + lfe: &mut crate::LfeChannel, +) -> core::result::Result<(Vec, usize), AudioArrayDecodeError> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + + // 2·LFF·nSSC 8-bit two's-complement decimated LFE samples. + let n_lfe = 2 * (lff as usize) * n_ssc; + let mut samples: Vec = Vec::with_capacity(n_lfe); + for _ in 0..n_lfe { + // ExtractBits(8) read as a signed char. + samples.push(br.read_bits(8)? as u8 as i8); + } + + // 8-bit LFEscaleIndex. + let scale_index = br.read_bits(8)? as u8; + + let bits_consumed = br.absolute_bit_position() - bit_offset; + + let pcm = lfe + .decode_subframe(&samples, scale_index, lff) + .map_err(AudioArrayError::LfePhase)?; + + Ok((pcm, bits_consumed)) +} + +/// Persistent per-channel, per-subband §C.2.2 reconstruction history +/// — the four most recently reconstructed subband samples that prime +/// the inverse-ADPCM predictor of the next decode block ("history +/// from last subframe or subsubframe", §C.2.2; "the decoder will use +/// reconstruction history of the previous frame if HFLAG = 1", +/// §5.3.1). +/// +/// The walk updates it from every decoded subframe's final rows +/// (whether or not any subband was predicted, since any subband may +/// turn `PMODE` on in a later subframe); the frame-level driver +/// clears it at a frame boundary whose header says `HFLAG = 0` +/// (entry-point frames are coded without the previous frame's +/// predictor history). +#[derive(Debug, Clone, PartialEq)] +pub struct AdpcmHistory { + /// `per_channel[ch][n]` = the §C.2.2 `raSample[-4..0)` slots of + /// channel `ch`, subband `n`, **oldest first** (slot 0 = + /// `raSample[-4]`, slot 3 = `raSample[-1]`). + per_channel: Vec<[[f64; NUM_ADPCM_COEFF]; NUM_SUBBAND]>, +} + +impl AdpcmHistory { + /// Cleared history for `channels` primary channels (the state of + /// a stream entry point: "the history will be ignored" when + /// `HFLAG = 0`, i.e. treated as zero). + #[must_use] + pub fn new(channels: usize) -> Self { + Self { + per_channel: vec![[[0.0; NUM_ADPCM_COEFF]; NUM_SUBBAND]; channels], + } + } + + /// The configured channel count. + #[must_use] + pub fn channel_count(&self) -> usize { + self.per_channel.len() + } + + /// Zero every subband's history (the §5.3.1 `HFLAG = 0` frame + /// gate: "Otherwise, the history will be ignored"). + pub fn clear(&mut self) { + for ch in &mut self.per_channel { + *ch = [[0.0; NUM_ADPCM_COEFF]; NUM_SUBBAND]; + } + } + + /// The four-sample history of one `(ch, n)` subband, oldest + /// first. + #[must_use] + pub fn subband(&self, ch: usize, n: usize) -> &[f64; NUM_ADPCM_COEFF] { + &self.per_channel[ch][n] + } + + /// Slide every subband's history forward over a decoded + /// subframe's reconstructed sample matrices (`matrices[ch]` with + /// `rows` rows): the last four rows become the new history, with + /// the short-subframe (`rows < 4`) shift semantics of + /// [`crate::update_history_f64`]. + pub fn absorb_matrices(&mut self, matrices: &[SubbandSampleMatrix]) { + for (ch_hist, matrix) in self.per_channel.iter_mut().zip(matrices) { + let rows = matrix.len(); + let take = rows.min(NUM_ADPCM_COEFF); + for (n, hist) in ch_hist.iter_mut().enumerate() { + if take < NUM_ADPCM_COEFF { + hist.copy_within(take.., 0); + } + for (k, row) in matrix[rows - take..].iter().enumerate() { + hist[NUM_ADPCM_COEFF - take + k] = row[n]; + } + } + } + } +} + +/// The §5.5 phase-1 high-frequency-VQ inputs for +/// [`decode_audio_data_subframe_vq_at`]: a recovered §D.10.2 book +/// plus the 10-bit indices captured (in walk order) by +/// [`crate::scan_hf_vq_indices_at`] from the region that precedes the +/// LFE phase. +#[derive(Debug, Clone, Copy)] +pub struct HfVqFill<'a> { + /// The recovered §D.10.2 `HFreqVQ` book. + pub book: &'a HfVqCodebook, + /// `indices[ch]` = the captured `nVQIndex` values for channel + /// `ch`'s subbands `nVQSUB[ch]..nSUBS[ch]`, in subband order. + pub indices: &'a [Vec], +} + +/// The §D.10.1 / §C.2.2 inverse-ADPCM inputs for +/// [`decode_audio_data_subframe_vq_at`]: a recovered coefficient book +/// plus the persistent per-subband reconstruction history the +/// predictor primes from (and which the walk advances). +#[derive(Debug)] +pub struct AdpcmContext<'a> { + /// The recovered §D.10.1 `ADPCMCoeffVQ` book. + pub book: &'a AdpcmVqCodebook, + /// The persistent reconstruction history (advanced by the walk + /// over **all** subbands, predicted or not). + pub history: &'a mut AdpcmHistory, +} + +/// Decode the §5.5 `Audio Data` block for one subframe, given the +/// already-decoded §5.4.1 side information and §5.3.2 header context. +/// +/// Walks the Table 5-29 `nSubSubFrame × ch × n` loop, extracting and +/// dequantizing every primary subband, running inverse-ADPCM where +/// `PMODE != 0`, and consuming the `DSYNC` trailers. Returns one +/// [`SubbandSampleMatrix`] per channel (length `n_ssc * 8` rows). +/// +/// * `bytes` / `bit_offset` — the bit stream positioned at the first +/// §5.5 `Audio Data` bit (after the §5.4.1 side-info block). +/// * `side` — the per-channel [`ChannelSideInfo`] (round-281). +/// * `sel` — `|ch, abits| -> u8`, the §5.3.2 `SEL[ch][nABITS-1]` +/// selector ([`crate::AudioCodingHeader::sel`]). +/// * `adj` — `|ch, abits| -> ScaleFactorAdjustment`, the §5.5 +/// `arADJ[ch][SEL[ch][nABITS-1]]` multiplier +/// ([`crate::AudioCodingHeader::adj`]). +/// * `n_vqsub` / `n_subs` — per-channel loop bounds. +/// * `n_ssc` — the subframe's subsubframe count (`SSC + 1`). +/// * `table` — the §5.5 `RATE`-selected step-size table. +/// * `aspf` — the §5.3.1 Audio Sync-Word Insertion Flag (a `DSYNC` +/// trailer follows every subsubframe when set, else only the last). +/// +/// Returns `(Vec, bits_consumed)`. +/// +/// # Errors +/// +/// * [`Error::InvalidStepSize`] for an out-of-range `ABITS`; +/// * [`Error::HuffmanDecodeFailed`] on a corrupt audio Huffman prefix +/// or an `(ABITS, SEL)` pair with no §D.5 book; +/// * [`Error::DsyncMismatch`] when a `DSYNC` trailer is not `0xffff`; +/// * [`Error::UnexpectedEof`] on a truncated array. +/// +/// VQ / ADPCM-coefficient blockers surface +/// [`AudioArrayError::VqCodebookUnavailable`] wrapped through the +/// [`AudioArrayDecodeError`] return type. +#[allow(clippy::too_many_arguments)] +pub fn decode_audio_data_subframe_at( + bytes: &[u8], + bit_offset: usize, + side: &[ChannelSideInfo], + sel: impl Fn(usize, u8) -> u8, + adj: impl Fn(usize, u8) -> ScaleFactorAdjustment, + n_vqsub: &[usize], + n_subs: &[usize], + n_ssc: usize, + table: StepSizeTable, + aspf: bool, +) -> core::result::Result<(Vec, usize), AudioArrayDecodeError> { + decode_audio_data_subframe_partial_at( + bytes, bit_offset, side, sel, adj, n_vqsub, n_subs, n_ssc, 0, table, aspf, + ) +} + +/// [`decode_audio_data_subframe_at`] with the §5.4.1 `PSC` (Partial +/// Subsubframe Sample Count) semantics of a **termination frame** +/// applied: when `psc ∈ 1..=7`, the **last** of the subframe's `n_ssc` +/// subsubframes is *partial* — it holds `psc` subband samples per +/// active subband instead of 8 (PDF p.30: "PSC indicates the number +/// of subband samples held in a partial subsubframe for each of the +/// active subbands. A partial subsubframe is one which has less than +/// 8 subband samples. It exists only in a termination frame and is +/// always at the end of last normal subsubframe. A DSYNC word will +/// always occur after a partial subsubframe."). +/// +/// That the partial subsubframe is the last one **counted by** `nSSC` +/// (rather than an extra, uncounted tail after them) follows from the +/// staged spec's own ranges: a termination frame's `NBLKS` "can take +/// any value in its valid range" `[5, 127]` (PDF p.18), so the +/// minimum legal termination frame carries 6 subband-sample blocks — +/// which is expressible as `nSSC = 1` with a 6-sample partial +/// subsubframe but not as one full subsubframe *plus* a tail (8 + PSC +/// ≥ 8 > 6); and §5.2's frame layout caps a subframe at "up to 4 +/// subsubframes" (PDF p.16), which an uncounted fifth tail after +/// `nSSC = 4` would violate. +/// +/// The partial subsubframe changes only the last iteration of the +/// Table 5-29 sample loop: +/// +/// * per-sample carriers (§D.5 Huffman, NFE binary) extract exactly +/// `psc` codewords per active subband; +/// * the four-sample §D.6 block-code carrier extracts +/// `ceil(psc / 4)` words and keeps the first `psc` samples (see +/// [`extract_subband_audio`]); +/// * `ABITS = 0` subbands extract nothing, as always; +/// * the `DSYNC` trailer placement is unchanged — after the last +/// (here: partial) subsubframe always, and after every subsubframe +/// when `ASPF` is set, which realises the p.30 "A DSYNC word will +/// always occur after a partial subsubframe" clause. +/// +/// The returned matrices have `(n_ssc - 1) * 8 + psc` rows per +/// channel when `psc > 0` (the frame-level row budget is `NBLKS + 1` +/// across all subframes), and `bits_consumed` accounts exactly for +/// the truncated extraction. +/// +/// `psc = 0` reproduces [`decode_audio_data_subframe_at`] verbatim. +/// `psc` is trusted to be `< 8` (it is a 3-bit wire field); the +/// termination-frame gating ("exists only in a termination frame") +/// is the frame-level caller's to enforce, since this walk does not +/// see the §5.3.1 `FTYPE`. +#[allow(clippy::too_many_arguments)] +pub fn decode_audio_data_subframe_partial_at( + bytes: &[u8], + bit_offset: usize, + side: &[ChannelSideInfo], + sel: impl Fn(usize, u8) -> u8, + adj: impl Fn(usize, u8) -> ScaleFactorAdjustment, + n_vqsub: &[usize], + n_subs: &[usize], + n_ssc: usize, + psc: u8, + table: StepSizeTable, + aspf: bool, +) -> core::result::Result<(Vec, usize), AudioArrayDecodeError> { + decode_audio_data_subframe_vq_at( + bytes, bit_offset, side, sel, adj, n_vqsub, n_subs, n_ssc, psc, table, aspf, None, None, + ) +} + +/// [`decode_audio_data_subframe_partial_at`] with the two §D.10 +/// VQ-book sub-paths **enabled** by caller-supplied recovered books: +/// +/// * `hf` — the §5.5 phase-1 high-frequency-VQ reconstruction. The +/// 10-bit indices (captured by [`crate::scan_hf_vq_indices_at`] +/// from the region *before* the LFE phase) select 32-element +/// §D.10.2 vectors, and each HF subband's samples are +/// `SCALES[ch][n][0] · HFREQ[ch][n][m]` for the subframe's `m` +/// rows. The Table 5-29 listing assigns +/// `Scale = (real)SCALES[ch][n][0]` and then multiplies by a +/// variable it spells `rScale` — a spec-verbatim naming conflation +/// (re-verified against the staged PDF by the round-9 extraction +/// pass) that the §5.5 HFREQ prose on p.33 resolves: the decoder +/// picks `nSSC × 8` of the 32 samples "and scale[s] them with the +/// scale factor SCALES". On a termination-frame subframe the valid +/// prefix (`(nSSC−1)·8 + PSC` rows) is picked instead — the p.33 +/// pad rule ("padded with either zeros or 'don't care' … the +/// decoder will only pick" the live ones) makes the vector tail +/// don't-care. +/// * `adpcm` — the §5.5 `if (PMODE[ch][n] != 0) InverseADPCM()` step: +/// the four §C.2.2 predictor coefficients are looked up from the +/// §D.10.1 book by the subband's captured 12-bit `PVQ` index, and +/// the dequantized residuals of every subsubframe are reconstructed +/// in walk order, primed by the persistent [`AdpcmHistory`] (which +/// the walk advances over the subframe's final rows — for **all** +/// subbands, so a subband that turns `PMODE` on later still finds +/// its reconstruction history; the §5.3.1 `HFLAG` frame gate is the +/// frame-level caller's). +/// +/// With `None` for a needed book the corresponding blocker surfaces +/// as before ([`AudioArrayError::VqCodebookUnavailable`]); with both +/// `None` this is exactly [`decode_audio_data_subframe_partial_at`]. +/// +/// # Errors +/// +/// As [`decode_audio_data_subframe_partial_at`], plus +/// [`AudioArrayError::HfVqIndexShape`] when `hf` is supplied with a +/// capture that does not match the per-channel `[nVQSUB, nSUBS)` +/// shape, and [`AudioArrayError::MissingPvqIndex`] for a hand-built +/// `PMODE != 0` subband lacking its captured index. +#[allow(clippy::too_many_arguments)] +pub fn decode_audio_data_subframe_vq_at( + bytes: &[u8], + bit_offset: usize, + side: &[ChannelSideInfo], + sel: impl Fn(usize, u8) -> u8, + adj: impl Fn(usize, u8) -> ScaleFactorAdjustment, + n_vqsub: &[usize], + n_subs: &[usize], + n_ssc: usize, + psc: u8, + table: StepSizeTable, + aspf: bool, + hf: Option>, + mut adpcm: Option>, +) -> core::result::Result<(Vec, usize), AudioArrayDecodeError> { + let n_pchs = side.len(); + let psc = usize::from(psc) % SAMPLES_PER_SUBSUBFRAME; + + // Reject the VQ / ADPCM blockers up front so a partially-decoded + // matrix is never returned. Each blocker is lifted exactly when + // the matching recovered book is supplied. + for (ch, ch_side) in side.iter().enumerate() { + if n_vqsub[ch] < n_subs[ch] && hf.is_none() { + return Err(AudioArrayError::VqCodebookUnavailable { + ch, + n: n_vqsub[ch], + high_frequency_vq: true, + } + .into()); + } + if let Some(n) = ch_side.pmode[..n_vqsub[ch]].iter().position(|&p| p != 0) { + match &adpcm { + None => { + return Err(AudioArrayError::VqCodebookUnavailable { + ch, + n, + high_frequency_vq: false, + } + .into()); + } + Some(_) if ch_side.pvq_index[n].is_none() => { + return Err(AudioArrayError::MissingPvqIndex { ch, n }.into()); + } + Some(_) => {} + } + } + } + if let Some(fill) = &hf { + if fill.indices.len() != n_pchs { + return Err(AudioArrayError::HfVqIndexShape { ch: n_pchs }.into()); + } + for (ch, ch_indices) in fill.indices.iter().enumerate() { + if ch_indices.len() != n_subs[ch] - n_vqsub[ch] { + return Err(AudioArrayError::HfVqIndexShape { ch }.into()); + } + } + } + + // Row budget: the last subsubframe is partial (psc rows) on a + // termination-frame subframe, full (8 rows) otherwise. + let rows = if psc > 0 { + (n_ssc - 1) * SAMPLES_PER_SUBSUBFRAME + psc + } else { + n_ssc * SAMPLES_PER_SUBSUBFRAME + }; + let mut matrices: Vec = vec![vec![[0.0_f64; NUM_SUBBAND]; rows]; n_pchs]; + + // §5.5 phase 1 — high-frequency VQ subbands: fill the HF columns + // from the recovered §D.10.2 book before the audio-data walk (the + // indices were extracted from the bit stream ahead of the LFE + // phase; the fill itself consumes no bits here). + if let Some(fill) = &hf { + for (ch, ch_indices) in fill.indices.iter().enumerate() { + for (k, &index) in ch_indices.iter().enumerate() { + let n = n_vqsub[ch] + k; + // The p.33 HFREQ rule: pick the subframe's rows out of + // the 32-sample vector, scaled by SCALES[ch][n][0]. + let scale = f64::from(side[ch].scales[n][0]); + let vector = fill.book.vector(index); + for (row, &element) in matrices[ch].iter_mut().zip(vector.iter().take(rows)) { + row[n] = scale * element; + } + } + } + } + + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + + for subsubframe in 0..n_ssc { + let base = subsubframe * SAMPLES_PER_SUBSUBFRAME; + // §5.4.1 PSC: the last subsubframe of a termination-frame + // subframe holds `psc < 8` samples per active subband. + let count = if psc > 0 && subsubframe == n_ssc - 1 { + psc + } else { + SAMPLES_PER_SUBSUBFRAME + }; + for (ch, ch_side) in side.iter().enumerate() { + let matrix = &mut matrices[ch]; + // `n` is the subband index, used to address ch_side.abits / + // tmode / scales and matrix[row][n]; an enumerate() over any + // single one would not capture the cross-array indexing. + #[allow(clippy::needless_range_loop)] + for n in 0..n_vqsub[ch] { + let abits = ch_side.abits[n]; + let sel_val = sel(ch, abits); + let audio = extract_subband_audio(&mut br, abits, sel_val, count)?; + + // §5.5 transient-aware rScale composition. + let scale_idx = transient_scale_index(ch_side.tmode[n], n_ssc, subsubframe); + let scale = ch_side.scales[n][scale_idx]; + let step = table.step_size(abits)?; + let r_scale = step * f64::from(scale) * adj(ch, abits).multiplier_f64(); + + for (m, &index) in audio.iter().enumerate().take(count) { + matrix[base + m][n] = r_scale * f64::from(index); + } + + // §5.5: "if (PMODE[ch][n] != 0) + // aPrmCh[ch].aSubband[n].InverseADPCM();" — the four + // §C.2.2 coefficients come from the recovered §D.10.1 + // book via the subband's captured PVQ index; the + // history is the four samples preceding this + // subsubframe (earlier rows of this subframe, else + // the persistent inter-subframe history). + if ch_side.pmode[n] != 0 { + if let Some(ctx) = adpcm.as_mut() { + // Checked non-None in the pre-walk validation. + let pvq = ch_side.pvq_index[n].unwrap_or_default(); + let coeffs = ctx.book.coefficients(pvq); + let mut hist = [0.0_f64; NUM_ADPCM_COEFF]; + for (j, slot) in hist.iter_mut().enumerate() { + // Logical row `base - 4 + j`. + *slot = if base + j >= NUM_ADPCM_COEFF { + matrix[base + j - NUM_ADPCM_COEFF][n] + } else { + ctx.history.subband(ch, n)[j] + }; + } + let mut block = [0.0_f64; SAMPLES_PER_SUBSUBFRAME]; + for (m, slot) in block.iter_mut().enumerate().take(count) { + *slot = matrix[base + m][n]; + } + inverse_adpcm_decode_f64(&hist, coeffs, &mut block[..count])?; + for (m, &value) in block.iter().enumerate().take(count) { + matrix[base + m][n] = value; + } + } + } + } + } + // DSYNC trailer: present after the last subsubframe always, and + // after every subsubframe when ASPF == 1. + if subsubframe == n_ssc - 1 || aspf { + let dsync = br.read_bits(16)? as u16; + if dsync != DSYNC_WORD { + return Err(Error::DsyncMismatch { + found: dsync, + n_subsubframe: subsubframe as u8, + } + .into()); + } + } + } + + // Advance the persistent §C.2.2 reconstruction history over this + // subframe's final rows (all subbands — see [`AdpcmHistory`]). + if let Some(ctx) = adpcm.as_mut() { + ctx.history.absorb_matrices(&matrices); + } + + let bits_consumed = br.absolute_bit_position() - bit_offset; + Ok((matrices, bits_consumed)) +} + +/// Composite error for the §5.5 audio-data walk: either a crate-level +/// bit-stream [`Error`] or an [`AudioArrayError`] VQ/ADPCM blocker. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum AudioArrayDecodeError { + /// A bit-stream-level decode error (EOF, bad Huffman prefix, + /// invalid step size, DSYNC mismatch, …). + Bitstream(Error), + /// A subband needed an Annex D VQ code book not yet in `docs/`. + Blocked(AudioArrayError), +} + +impl From for AudioArrayDecodeError { + fn from(e: Error) -> Self { + AudioArrayDecodeError::Bitstream(e) + } +} + +impl From for AudioArrayDecodeError { + fn from(e: AudioArrayError) -> Self { + AudioArrayDecodeError::Blocked(e) + } +} + +impl core::fmt::Display for AudioArrayDecodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + AudioArrayDecodeError::Bitstream(e) => write!(f, "{e}"), + AudioArrayDecodeError::Blocked(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for AudioArrayDecodeError {} + +#[cfg(test)] +mod tests { + use super::*; + + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + #[test] + fn sign_extend_round_trips() { + assert_eq!(sign_extend(0b011, 3), 3); + assert_eq!(sign_extend(0b111, 3), -1); + assert_eq!(sign_extend(0b100, 3), -4); + assert_eq!(sign_extend(0, 5), 0); + } + + #[test] + fn nfe_and_block_widths() { + assert_eq!(nfe_word_bits(8), Some(5)); // 32 levels + assert_eq!(nfe_word_bits(11), Some(8)); // 256 levels + assert_eq!(nfe_word_bits(26), Some(23)); + assert_eq!(nfe_word_bits(7), None); + assert_eq!(block_code_word_bits(1), Some(7)); // V3 + assert_eq!(block_code_word_bits(7), Some(19)); // V25 + assert_eq!(block_code_word_bits(8), None); + } + + /// A single-channel, single-subsubframe, no-bits subband stream + /// decodes to an all-zero matrix and a single DSYNC trailer. + #[test] + fn no_bits_subband_zeroes_matrix() { + // nSSC = 1, one channel, nVQSUB = nSUBS = 1, ABITS = 0. + let side = vec![ChannelSideInfo::cleared()]; + let stream = pack_fields(&[(0xffff, 16)]); // just the DSYNC + let (mats, bits) = decode_audio_data_subframe_at( + &stream, + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + assert_eq!(mats.len(), 1); + assert_eq!(mats[0].len(), 8); + assert!(mats[0].iter().all(|row| row.iter().all(|&v| v == 0.0))); + assert_eq!(bits, 16); + } + + /// A NoEncoding (NFE) subband with ABITS 8 reads eight 5-bit + /// sign-extended fields and scales them by the dequant rScale. + #[test] + fn nfe_subband_dequantizes() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; // NFE width 5; lossy step for 8 = 796918/2^22 + ch.scales[0][0] = 4; + let side = vec![ch]; + + // Eight 5-bit values: 1,-1,2,-2,3,-3,4,-4 (two's complement). + let vals = [1i32, -1, 2, -2, 3, -3, 4, -4]; + let mut fields: Vec<(u32, u8)> = vals.iter().map(|&v| ((v as u32) & 0x1f, 5u8)).collect(); + fields.push((0xffff, 16)); // DSYNC + let stream = pack_fields(&fields); + + // SEL must select the terminal NFE entry for ABITS 8 (group of + // 8 -> top SEL 7). + let (mats, _) = decode_audio_data_subframe_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + let step = StepSizeTable::Lossy.step_size(8).unwrap(); + let r = step * 4.0; + for (m, &v) in vals.iter().enumerate() { + assert!((mats[0][m][0] - r * f64::from(v)).abs() < 1e-9); + } + } + + /// A bad DSYNC surfaces a typed mismatch. + #[test] + fn bad_dsync_rejected() { + let side = vec![ChannelSideInfo::cleared()]; + let stream = pack_fields(&[(0x1234, 16)]); + let err = decode_audio_data_subframe_at( + &stream, + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + StepSizeTable::Lossy, + false, + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Bitstream(Error::DsyncMismatch { found: 0x1234, .. }) + )); + } + + /// A subband with high-frequency VQ (nVQSUB < nSUBS) is blocked. + #[test] + fn high_frequency_vq_blocked() { + let side = vec![ChannelSideInfo::cleared()]; + let err = decode_audio_data_subframe_at( + &[0u8; 8], + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], // nVQSUB + &[3], // nSUBS > nVQSUB -> VQ subbands + 1, + StepSizeTable::Lossy, + false, + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Blocked(AudioArrayError::VqCodebookUnavailable { + high_frequency_vq: true, + .. + }) + )); + } + + /// A PMODE-active subband is blocked on the §D.10.1 coefficient VQ. + #[test] + fn adpcm_subband_blocked() { + let mut ch = ChannelSideInfo::cleared(); + ch.pmode[0] = 1; + let side = vec![ch]; + let err = decode_audio_data_subframe_at( + &[0u8; 8], + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + StepSizeTable::Lossy, + false, + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Blocked(AudioArrayError::VqCodebookUnavailable { + high_frequency_vq: false, + .. + }) + )); + } + + /// ASPF == 1 inserts a DSYNC after every subsubframe; two + /// subsubframes therefore carry two trailers. + #[test] + fn aspf_inserts_dsync_each_subsubframe() { + let side = vec![ChannelSideInfo::cleared()]; + // nSSC = 2, ABITS 0 -> no audio bits, two DSYNC trailers. + let stream = pack_fields(&[(0xffff, 16), (0xffff, 16)]); + let (_, bits) = decode_audio_data_subframe_at( + &stream, + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 2, + StepSizeTable::Lossy, + true, + ) + .unwrap(); + assert_eq!(bits, 32); + } + + // ----------------------------------------------------------- + // §5.4.1 PSC — termination-frame partial subsubframe. + // ----------------------------------------------------------- + + /// `psc = 0` through the partial entry point is bit-for-bit the + /// normal walk: same matrices, same bit count. + #[test] + fn psc_zero_is_identity_with_normal_walk() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; + ch.scales[0][0] = 4; + let side = vec![ch]; + let vals = [1i32, -1, 2, -2, 3, -3, 4, -4]; + let mut fields: Vec<(u32, u8)> = vals.iter().map(|&v| ((v as u32) & 0x1f, 5u8)).collect(); + fields.push((0xffff, 16)); + let stream = pack_fields(&fields); + + let normal = decode_audio_data_subframe_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + let partial = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + 0, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + assert_eq!(normal, partial); + } + + /// NFE (per-sample binary) partial subsubframe: `nSSC = 2`, + /// `PSC = 3` extracts 8 + 3 five-bit fields, returns 11 rows, and + /// the bit budget is exactly `11·5 + 16` (one DSYNC). + #[test] + fn psc_nfe_truncates_rows_and_bits_exactly() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; + ch.scales[0][0] = 4; + let side = vec![ch]; + + let vals = [1i32, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6]; + let mut fields: Vec<(u32, u8)> = vals.iter().map(|&v| ((v as u32) & 0x1f, 5u8)).collect(); + fields.push((0xffff, 16)); // DSYNC after the partial subsubframe + let stream = pack_fields(&fields); + + let (mats, bits) = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 2, + 3, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + assert_eq!(mats[0].len(), 11, "(nSSC-1)*8 + PSC rows"); + assert_eq!(bits, 11 * 5 + 16, "bit budget exact through truncation"); + let step = StepSizeTable::Lossy.step_size(8).unwrap(); + let r = step * 4.0; + for (m, &v) in vals.iter().enumerate() { + assert!((mats[0][m][0] - r * f64::from(v)).abs() < 1e-9); + } + } + + /// §D.6 block-code partial subsubframe, `PSC ≤ 4`: one four-sample + /// word is extracted (`ceil(3/4) = 1`), the first three decoded + /// samples are kept, the fourth (encoder pad) is discarded. + #[test] + fn psc_block_code_single_word_keeps_live_samples() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 1; // V3: 3 levels, 7-bit word, 4 samples/word + ch.scales[0][0] = 1; + let side = vec![ch]; + + // Base-3 digits LSD-first (element i = code%3 - 1): live + // samples (+1, -1, 0), pad digit 2 (= +1, must be ignored). + // code = 2 + 3·0 + 9·1 + 27·2 = 65. + let stream = pack_fields(&[(65, 7), (0xffff, 16)]); + + let (mats, bits) = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 1, // terminal SEL for the ABITS=1 group -> block code + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + 3, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + assert_eq!(mats[0].len(), 3); + assert_eq!(bits, 7 + 16, "one 7-bit V3 word + DSYNC"); + let step = StepSizeTable::Lossy.step_size(1).unwrap(); + let got: Vec = (0..3).map(|m| mats[0][m][0]).collect(); + let want = [step, -step, 0.0]; + for (g, w) in got.iter().zip(want) { + assert!((g - w).abs() < 1e-9, "got {got:?}"); + } + } + + /// §D.6 block-code partial subsubframe, `PSC = 5`: two words + /// (`ceil(5/4) = 2`), the second word contributes one live sample. + #[test] + fn psc_block_code_two_words_for_five_samples() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 1; + ch.scales[0][0] = 1; + let side = vec![ch]; + + // Word 1: (+1, +1, -1, -1) -> digits (2,2,0,0) -> 2 + 6 = 8. + // Word 2: live (-1), pads 0 -> digits (0,1,1,1) -> 3+9+27 = 39. + let stream = pack_fields(&[(8, 7), (39, 7), (0xffff, 16)]); + + let (mats, bits) = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 1, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + 5, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + assert_eq!(mats[0].len(), 5); + assert_eq!(bits, 2 * 7 + 16, "two 7-bit V3 words + DSYNC"); + let step = StepSizeTable::Lossy.step_size(1).unwrap(); + let want = [step, step, -step, -step, -step]; + for (m, w) in want.iter().enumerate() { + assert!((mats[0][m][0] - w).abs() < 1e-9); + } + } + + /// Find the `(code, len)` pair a §D.5 book decodes to `level`, by + /// scanning prefixes through the decoder itself (test-side encode + /// for books whose encode direction is not otherwise needed). + fn huff_codeword(book: AudioHuffCodebook, level: i16) -> (u32, u8) { + for len in 1..=16u8 { + for code in 0..(1u32 << len) { + // Lay the candidate at the front of a padded buffer. + let padded = (code << (32 - len)) | ((1 << (32 - len)) - 1) >> 1; + let bytes = padded.to_be_bytes(); + if let Ok((got, consumed)) = decode_audio_huff_at(&bytes, 0, book) { + if consumed == usize::from(len) && got == level { + return (code, len); + } + } + } + } + panic!("no codeword for level {level}"); + } + + /// §D.5 Huffman partial subsubframe: the per-sample carrier + /// extracts exactly `PSC` codewords — verified with the 3-level + /// `ABITS = 1` book, `nSSC = 1`, `PSC = 3`, bit budget exact. + #[test] + fn psc_huffman_extracts_exactly_psc_codewords() { + let book = AudioHuffCodebook::from_abits_sel(1, 0).expect("ABITS=1 SEL=0 book exists"); + let levels = [1i16, -1, 0]; + let mut fields: Vec<(u32, u8)> = Vec::new(); + let mut audio_bits = 0usize; + for &level in &levels { + let (code, len) = huff_codeword(book, level); + fields.push((code, len)); + audio_bits += usize::from(len); + } + fields.push((0xffff, 16)); // DSYNC after the partial subsubframe + let stream = pack_fields(&fields); + + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 1; + ch.scales[0][0] = 1; + let side = vec![ch]; + + let (mats, bits) = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 0, // SEL = 0 -> Huffman book A3 + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + 3, + StepSizeTable::Lossy, + false, + ) + .unwrap(); + assert_eq!(mats[0].len(), 3); + assert_eq!(bits, audio_bits + 16, "exactly PSC codewords + DSYNC"); + let step = StepSizeTable::Lossy.step_size(1).unwrap(); + for (m, &level) in levels.iter().enumerate() { + assert!((mats[0][m][0] - step * f64::from(level)).abs() < 1e-9); + } + } + + /// ASPF on a partial subframe: a DSYNC follows the full + /// subsubframe *and* the partial one (the p.30 "A DSYNC word will + /// always occur after a partial subsubframe" clause composes with + /// the per-subsubframe ASPF rule). + #[test] + fn psc_with_aspf_places_dsync_after_both_subsubframes() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; + ch.scales[0][0] = 4; + let side = vec![ch]; + + let mut fields: Vec<(u32, u8)> = (0..8).map(|_| (0u32, 5u8)).collect(); + fields.push((0xffff, 16)); // ASPF DSYNC after subsubframe 0 + fields.extend((0..2).map(|_| (0u32, 5u8))); // partial: PSC = 2 + fields.push((0xffff, 16)); // DSYNC after the partial subsubframe + let stream = pack_fields(&fields); + + let (mats, bits) = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 2, + 2, + StepSizeTable::Lossy, + true, + ) + .unwrap(); + assert_eq!(mats[0].len(), 10); + assert_eq!(bits, 10 * 5 + 2 * 16); + } + + /// A truncated partial subsubframe (stream ends inside the PSC + /// samples) surfaces a typed EOF, not a panic or a padded matrix. + #[test] + fn psc_truncated_stream_is_typed_eof() { + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; + ch.scales[0][0] = 4; + let side = vec![ch]; + + // 8 full samples then only 1 of the 3 partial samples. + let fields: Vec<(u32, u8)> = (0..9).map(|_| (0u32, 5u8)).collect(); + let stream = pack_fields(&fields); + + let err = decode_audio_data_subframe_partial_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 2, + 3, + StepSizeTable::Lossy, + false, + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Bitstream(Error::UnexpectedEof) + )); + } + + // ----------------------------------------------------------- + // §5.5 LFE phase walker (§2.2). + // ----------------------------------------------------------- + + /// The LFE phase consumes `2·LFF·nSSC` 8-bit samples + an 8-bit + /// scale index, and reports exactly that many bits. + #[test] + fn lfe_phase_consumes_samples_plus_scale_index() { + let lff = 1u8; // 128× + let n_ssc = 2usize; + let n_lfe = 2 * (lff as usize) * n_ssc; // 4 samples + // 4 sample bytes (all 0) + 1 scale-index byte (10). + let mut fields: Vec<(u32, u8)> = vec![(0, 8); n_lfe]; + fields.push((10, 8)); + let stream = pack_fields(&fields); + let mut lfe = crate::LfeChannel::new(); + let (pcm, bits) = decode_lfe_phase_at(&stream, 0, lff, n_ssc, &mut lfe).unwrap(); + assert_eq!(bits, (n_lfe + 1) * 8); + // Each decimated sample expands to 128 PCM samples. + assert_eq!(pcm.len(), n_lfe * 128); + // All-zero LFE samples decode to silence. + assert!(pcm.iter().all(|&s| s == 0)); + } + + /// 8-bit two's-complement LFE samples are read as signed: a 0xFF byte + /// is -1, which (with a non-zero scale) produces non-zero PCM of the + /// correct sign at phase 0. + #[test] + fn lfe_phase_reads_signed_samples() { + let lff = 2u8; // 64× + let n_ssc = 1usize; + let n_lfe = 2 * (lff as usize) * n_ssc; // 4 samples + let scale_index = 60u8; + // First sample = 0xFF (= -1), rest 0. + let mut fields: Vec<(u32, u8)> = vec![(0xFF, 8)]; + fields.extend(vec![(0, 8); n_lfe - 1]); + fields.push((u32::from(scale_index), 8)); + let stream = pack_fields(&fields); + + let mut lfe = crate::LfeChannel::new(); + let (pcm, _) = decode_lfe_phase_at(&stream, 0, lff, n_ssc, &mut lfe).unwrap(); + + // Reference: phase-0 first output = (int)((-1)·nScale·0.035·c0). + let n_scale = crate::side_info::RMS_7BIT[scale_index as usize] as f64; + let r_scale = n_scale * crate::LFE_SCALE_STEP; + let sel = crate::LfeInterpolationSelection::Decimation64; + let c0 = sel.coefficients()[0]; + let expected0 = (-(r_scale * c0)) as i32; + assert_eq!(pcm[0], expected0); + } + + // ----------------------------------------------------------- + // §D.10 recovered-book walk (round 434). + // ----------------------------------------------------------- + + fn tiny_hf_book() -> crate::HfVqCodebook { + // Vector v, element m: (v + m) / 24 — small, distinct, exact. + let vectors: Vec<[f64; 32]> = (0i32..1024) + .map(|v| core::array::from_fn(|m| f64::from(v + m as i32) / 24.0)) + .collect(); + crate::HfVqCodebook::from_elements(&vectors).unwrap() + } + + fn tiny_adpcm_book() -> crate::AdpcmVqCodebook { + // Vector i: coefficients (i mod 5 − 2) / 16 in every tap. + let vectors: Vec<[f64; 4]> = (0i32..4096) + .map(|i| [(f64::from(i % 5) - 2.0) / 16.0; 4]) + .collect(); + crate::AdpcmVqCodebook::from_coefficients(&vectors).unwrap() + } + + /// A supplied HF fill whose capture shape disagrees with the + /// per-channel `[nVQSUB, nSUBS)` bounds surfaces the typed shape + /// error (wrong outer length and wrong per-channel count). + #[test] + fn hf_fill_shape_mismatch_is_typed() { + let book = tiny_hf_book(); + let side = vec![ChannelSideInfo::cleared()]; + let stream = pack_fields(&[(0xffff, 16)]); + + // Outer capture length 2 for a 1-channel walk. + let indices = vec![vec![0u16], vec![]]; + let err = decode_audio_data_subframe_vq_at( + &stream, + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[2], + 1, + 0, + StepSizeTable::Lossy, + false, + Some(HfVqFill { + book: &book, + indices: &indices, + }), + None, + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Blocked(AudioArrayError::HfVqIndexShape { ch: 1 }) + )); + + // Right outer length, wrong per-channel count (2 for 1 HF + // subband). + let indices = vec![vec![0u16, 1]]; + let err = decode_audio_data_subframe_vq_at( + &stream, + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[2], + 1, + 0, + StepSizeTable::Lossy, + false, + Some(HfVqFill { + book: &book, + indices: &indices, + }), + None, + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Blocked(AudioArrayError::HfVqIndexShape { ch: 0 }) + )); + } + + /// A hand-built `PMODE != 0` subband with no captured PVQ index is + /// rejected with the typed error even when the book is present. + #[test] + fn missing_pvq_index_is_typed() { + let book = tiny_adpcm_book(); + let mut ch = ChannelSideInfo::cleared(); + ch.pmode[0] = 1; // pvq_index stays None — impossible via decode + let side = vec![ch]; + let mut history = AdpcmHistory::new(1); + let err = decode_audio_data_subframe_vq_at( + &[0u8; 8], + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + 0, + StepSizeTable::Lossy, + false, + None, + Some(AdpcmContext { + book: &book, + history: &mut history, + }), + ) + .unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Blocked(AudioArrayError::MissingPvqIndex { ch: 0, n: 0 }) + )); + } + + /// The HF fill populates exactly the `[nVQSUB, nSUBS)` columns + /// with `SCALES[ch][n][0] · vector[m]`, consumes no §5.5 bits, + /// and lifts the bookless blocker. + #[test] + fn hf_fill_populates_hf_columns() { + let book = tiny_hf_book(); + let mut ch = ChannelSideInfo::cleared(); + ch.scales[1][0] = 3; // HF subband n=1: SCALES[ch][1][0] = 3 + ch.scales[2][0] = 5; // HF subband n=2 + let side = vec![ch]; + let stream = pack_fields(&[(0xffff, 16)]); // just the DSYNC + let indices = vec![vec![7u16, 100]]; + let (mats, bits) = decode_audio_data_subframe_vq_at( + &stream, + 0, + &side, + |_, _| 0, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[3], + 1, + 0, + StepSizeTable::Lossy, + false, + Some(HfVqFill { + book: &book, + indices: &indices, + }), + None, + ) + .unwrap(); + assert_eq!(bits, 16, "the fill itself reads no bits"); + for (m, row) in mats[0].iter().enumerate() { + assert_eq!(row[0], 0.0, "coded subband (ABITS=0) stays 0"); + assert_eq!(row[1], 3.0 * (f64::from(7 + m as i32) / 24.0)); + assert_eq!(row[2], 5.0 * (f64::from(100 + m as i32) / 24.0)); + } + } + + /// [`AdpcmHistory::absorb_matrices`] slides the last four rows in + /// (oldest first), with the short-subframe shift semantics for + /// fewer than four rows. + #[test] + fn adpcm_history_absorb_semantics() { + let mut hist = AdpcmHistory::new(1); + // 5 rows: subband 0 carries 1..=5. + let mut m: SubbandSampleMatrix = vec![[0.0; NUM_SUBBAND]; 5]; + for (k, row) in m.iter_mut().enumerate() { + row[0] = (k + 1) as f64; + } + hist.absorb_matrices(std::slice::from_ref(&m)); + assert_eq!(hist.subband(0, 0), &[2.0, 3.0, 4.0, 5.0]); + + // A 2-row (short) subframe shifts and appends. + let mut m2: SubbandSampleMatrix = vec![[0.0; NUM_SUBBAND]; 2]; + m2[0][0] = 10.0; + m2[1][0] = 11.0; + hist.absorb_matrices(std::slice::from_ref(&m2)); + assert_eq!(hist.subband(0, 0), &[4.0, 5.0, 10.0, 11.0]); + } + + /// The ADPCM context reconstructs a predicted subband: residuals + /// plus the 4-tap dot product over the priming history, history + /// advanced to the block's final rows. + #[test] + fn adpcm_context_predicts_and_advances_history() { + let book = tiny_adpcm_book(); + // PVQ index 1 -> coefficients [-1/16; 4]. + let mut ch = ChannelSideInfo::cleared(); + ch.pmode[0] = 1; + ch.pvq_index[0] = Some(1); + ch.abits[0] = 8; + ch.scales[0][0] = 1; + let side = vec![ch]; + + // One subsubframe of NFE residuals: 16, 0, 0, 0, 0, 0, 0, 0 + // — but NFE range for ABITS=8 is 5 bits, so use 8. + let vals = [8i32, 0, 0, 0, 0, 0, 0, 0]; + let mut fields: Vec<(u32, u8)> = vals.iter().map(|&v| ((v as u32) & 0x1f, 5u8)).collect(); + fields.push((0xffff, 16)); + let stream = pack_fields(&fields); + + let mut history = AdpcmHistory::new(1); + let (mats, _) = decode_audio_data_subframe_vq_at( + &stream, + 0, + &side, + |_, _| 7, // terminal NFE SEL for ABITS=8 + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + 0, + StepSizeTable::Lossy, + false, + None, + Some(AdpcmContext { + book: &book, + history: &mut history, + }), + ) + .unwrap(); + + // Analytic: r[0] = 8·step; r[m] = Σ c·r[m-1..m-4], c = -1/16. + let step = StepSizeTable::Lossy.step_size(8).unwrap(); + let c = -1.0 / 16.0; + let mut expect = [0.0f64; 8]; + let residual = [8.0 * step, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + for m in 0..8 { + let mut acc = residual[m]; + for t in 0..4usize { + if m > t { + acc += c * expect[m - t - 1]; + } + } + expect[m] = acc; + } + for m in 0..8 { + assert!((mats[0][m][0] - expect[m]).abs() < 1e-12, "row {m}"); + } + // History advanced to rows 4..8. + let h = history.subband(0, 0); + for k in 0..4 { + assert!((h[k] - expect[4 + k]).abs() < 1e-12); + } + } + + /// A reserved §D.1.2 scale index surfaces the typed LFE-phase blocker. + #[test] + fn lfe_phase_rejects_reserved_scale_index() { + let lff = 1u8; + let n_ssc = 1usize; + let n_lfe = 2 * (lff as usize) * n_ssc; + let mut fields: Vec<(u32, u8)> = vec![(0, 8); n_lfe]; + fields.push((126, 8)); // reserved + let stream = pack_fields(&fields); + let mut lfe = crate::LfeChannel::new(); + let err = decode_lfe_phase_at(&stream, 0, lff, n_ssc, &mut lfe).unwrap_err(); + assert!(matches!( + err, + AudioArrayDecodeError::Blocked(AudioArrayError::LfePhase( + crate::LfeChannelError::ReservedScaleIndex { index: 126 } + )) + )); + } +} diff --git a/crates/vendor/oxideav-dts/src/audio_data.rs b/crates/vendor/oxideav-dts/src/audio_data.rs new file mode 100644 index 00000000..e4323171 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/audio_data.rs @@ -0,0 +1,400 @@ +//! DTS Coherent Acoustics — §5.5 Table 5-29 `Audio Data` quantization- +//! type dispatch and the Table 5-26 `(ABITS, SEL)` codebook-group +//! geometry it dispatches on. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), staged PDF at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. +//! +//! Two clauses combine here: +//! +//! * **Table 5-26 "Selection of Quantization Levels and Codebooks"** +//! (staged PDF p.27) tabulates, per `ABITS` bit-allocation index, +//! the number of mid-tread quantizer levels and the list of +//! quantization-index code books selectable by the `SEL` field. The +//! per-`ABITS` code-book group has a fixed size (`nNumQ` in the §5.5 +//! pseudocode); the *last* entry of each group is special — it is a +//! block-code book (the `V…` column) for `ABITS ≤ 7`, or a +//! "no further encoding" (`NFE`) entry for `ABITS ≥ 8`. +//! +//! * **§5.5 Table 5-29 `Audio Data`** (staged PDF p.31-32) resolves +//! the per-subband quantization *type* `nQType` from the +//! `(ABITS, SEL)` pair before it extracts the eight `AUDIO[m]` +//! indices. Transcribed verbatim from the staged pseudocode: +//! +//! ```text +//! nABITS = ABITS[ch][n]; +//! pCQGroup = &pCQGroupAUDIO[nABITS-1]; +//! nNumQ = pCQGroupAUDIO[nABITS-1].nNumQ-1; // top SEL index of group +//! nSEL = SEL[ch][nABITS-1]; +//! nQType = 1; // Assume Huffman type by default +//! if ( nSEL==nNumQ ) { // Not Huffman type (last group entry) +//! if ( nABITS<=7 ) nQType = 3; // Block code +//! else nQType = 2; // No further encoding +//! } +//! if ( nABITS==0 ) nQType = 0; // No bits allocated +//! ``` +//! +//! This module exposes the Table 5-26 geometry verbatim (levels + +//! group size per `ABITS`) and the `nQType` resolver +//! ([`AudioQuantType`] / [`audio_quant_type`]) that the §5.5 +//! per-subsubframe `Audio Data` walker dispatches on. It is the +//! decision core that routes each subband into one of the four +//! already-landed extraction paths: +//! +//! * [`AudioQuantType::NoBits`] → eight zero `AUDIO[m]` values; +//! * [`AudioQuantType::Huffman`] → eight Huffman-coded indices (the +//! §D.6 audio code books, a separate transcription); +//! * [`AudioQuantType::NoEncoding`] → eight plain sign-extended +//! quantization indices read directly from the bit stream; +//! * [`AudioQuantType::BlockCode`] → two block-code words, each +//! expanded to four samples by the round-232 +//! [`crate::decode_block_code`]. +//! +//! # Scope and follow-ups +//! +//! The actual §D.6 audio quantization code books (`A3`, `B12`, …) and +//! the `SEL[ch][ABITS]` field decode (Table 5-21 header) are *not* +//! part of this module — they are larger separate transcriptions. The +//! `nQType` dispatch is fully fixed by the spec text and Table 5-26 +//! alone, so it lands ahead of the code-book tables it will route +//! into. + +/// The number of distinct `ABITS` bit-allocation indices Table 5-26 +/// tabulates a quantizer for: `ABITS = 0..=11` (PDF p.27). Index `0` +/// is "no bits allocated" (no quantizer); `1..=11` carry a mid-tread +/// quantizer. `ABITS > 11` is not a Table 5-26 row (no SEL is +/// transmitted and no further encoding is applied — see +/// [`audio_quant_type`]). +pub const ABITS_TABLE_LEN: usize = 12; + +/// The largest `ABITS` index that selects a code-book group with a +/// transmitted `SEL` field (Table 5-26, PDF p.27): "No SEL is +/// transmitted for `ABITS[ch] > 11`, because no further encoding is +/// used for those quantizers." `ABITS` in `1..=ABITS_MAX_SEL` carries +/// a `SEL` field; `ABITS == 0` is "Not transmitted". +pub const ABITS_MAX_SEL: u8 = 11; + +/// The largest `ABITS` whose group's terminal code book is a block +/// code (the `V…` column of Table 5-26): for `ABITS ≤ 7` the last +/// group entry is a block-code book, for `ABITS ≥ 8` it is a "no +/// further encoding" (`NFE`) entry. The §5.5 `nQType` resolver tests +/// `if (nABITS <= 7)`. +pub const ABITS_MAX_BLOCK_CODE: u8 = 7; + +/// Per-`ABITS` "Number of Index Quantization Levels" column of +/// Table 5-26 (PDF p.27), indexed by `ABITS = 0..=11`. The mid-tread +/// linear quantizer for `ABITS` has this many output levels (the two +/// values written "33 or 32" / "65 or 64" / "129 or 128" in the PDF — +/// the symmetric-with-zero vs. symmetric-without-zero variants — are +/// tabulated here at their odd "with zero" form; the alternate even +/// form is noted in the spec for the NFE code-book variant). `ABITS 0` +/// has zero levels ("no bits allocated"). +pub const QUANT_LEVELS: [u16; ABITS_TABLE_LEN] = [ + 0, // ABITS 0 — no bits allocated + 3, // ABITS 1 + 5, // ABITS 2 + 7, // ABITS 3 + 9, // ABITS 4 + 13, // ABITS 5 + 17, // ABITS 6 + 25, // ABITS 7 + 33, // ABITS 8 (33 or 32) + 65, // ABITS 9 (65 or 64) + 129, // ABITS 10 (129 or 128) + 256, // ABITS 11 +]; + +/// Per-`ABITS` code-book group size `nNumQ` of Table 5-26 (PDF p.27), +/// indexed by `ABITS = 0..=11`: the number of code books listed in the +/// `SEL` columns of that row. The §5.5 pseudocode reads this struct +/// field and subtracts one (`nNumQ - 1`) to obtain the *top* valid +/// `SEL` index of the group; `nSEL == nNumQ - 1` selects the group's +/// terminal (block-code or NFE) entry. `ABITS 0` lists no code books +/// ("Not transmitted"). +/// +/// Group sizes read straight off Table 5-26's `SEL` columns: +/// +/// | ABITS | code books listed | nNumQ | +/// |-------|------------------------------------------|-------| +/// | 1 | `A3 V3` | 2 | +/// | 2 | `A5 B5 C5 V5` | 4 | +/// | 3 | `A7 B7 C7 V7` | 4 | +/// | 4 | `A9 B9 C9 V9` | 4 | +/// | 5 | `A13 B13 C13 V13` | 4 | +/// | 6 | `A17 B17 C17 D17 E17 F17 G17 V17` | 8 | +/// | 7 | `A25 B25 C25 D25 E25 F25 G25 V25` | 8 | +/// | 8 | `A33 B33 C33 D33 E33 F33 G33 NFE` | 8 | +/// | 9 | `A65 B65 C65 D65 E65 F65 G65 NFE` | 8 | +/// | 10 | `A129 B129 C129 D129 E129 F129 G129 NFE` | 8 | +/// | 11 | `NFE` | 1 | +pub const CODEBOOK_GROUP_SIZE: [u8; ABITS_TABLE_LEN] = [ + 0, // ABITS 0 — no code books transmitted + 2, // ABITS 1 + 4, // ABITS 2 + 4, // ABITS 3 + 4, // ABITS 4 + 4, // ABITS 5 + 8, // ABITS 6 + 8, // ABITS 7 + 8, // ABITS 8 + 8, // ABITS 9 + 8, // ABITS 10 + 1, // ABITS 11 +]; + +/// The §5.5 quantization *type* `nQType` of the Table 5-29 `Audio +/// Data` block: how the eight `AUDIO[m]` quantization indices of one +/// `(ch, n, subsubframe)` subband are represented in the bit stream +/// (PDF p.31-32). Each variant routes into a distinct extraction path. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AudioQuantType { + /// `nQType == 0` — "No bits allocated" (`ABITS == 0`): the eight + /// `AUDIO[m]` values are all zero; no bits are read. + NoBits, + /// `nQType == 1` — "Huffman code" (the default for every + /// `(ABITS, SEL)` pair whose `SEL` is *not* the group's terminal + /// entry): eight Huffman-coded indices follow, decoded through the + /// `SEL`-selected §D.6 audio code book. + Huffman, + /// `nQType == 2` — "No further encoding" (`ABITS >= 8` with `SEL` + /// at the group's terminal `NFE` entry): eight plain quantization + /// indices follow, each read as a fixed-width field and + /// sign-extended. + NoEncoding, + /// `nQType == 3` — "Block code" (`ABITS <= 7` with `SEL` at the + /// group's terminal `V…` entry): two block-code words follow, each + /// expanded to four samples by the §C.2.1 block-code book + /// ([`crate::decode_block_code`]). + BlockCode, +} + +/// The top valid `SEL` index for an `ABITS` code-book group: the §5.5 +/// pseudocode's `nNumQ - 1` (PDF p.31). Returns `None` for +/// `ABITS == 0` (no code books transmitted) and for `ABITS` outside +/// the Table 5-26 range (`ABITS > 11`), which carry no `SEL` field. +/// +/// `nSEL == terminal_sel_index(ABITS)` is the §5.5 "Not Huffman type" +/// condition that selects the group's block-code (`V…`) or +/// no-further-encoding (`NFE`) entry. +#[must_use] +pub fn terminal_sel_index(abits: u8) -> Option { + let idx = abits as usize; + if idx == 0 || idx >= ABITS_TABLE_LEN { + return None; + } + // CODEBOOK_GROUP_SIZE[abits] is >= 1 for every 1..=11 row, so the + // subtraction never underflows. + Some(CODEBOOK_GROUP_SIZE[idx] - 1) +} + +/// Resolve the §5.5 Table 5-29 quantization type `nQType` for one +/// subband from its `(ABITS, SEL)` pair, exactly per the staged +/// pseudocode (PDF p.31-32): +/// +/// ```text +/// nQType = 1; // Assume Huffman type by default +/// if ( nSEL == nNumQ-1 ) { // Not Huffman type (last group entry) +/// if ( nABITS <= 7 ) nQType = 3; // Block code +/// else nQType = 2; // No further encoding +/// } +/// if ( nABITS == 0 ) nQType = 0; // No bits allocated +/// ``` +/// +/// * `abits` is `ABITS[ch][n]`; `0` is "no bits allocated". +/// * `sel` is `SEL[ch][ABITS-1]`, the code-book selector for this +/// quantizer (only meaningful for `ABITS >= 1`). +/// +/// `ABITS == 0` resolves to [`AudioQuantType::NoBits`] regardless of +/// `sel` (the `nABITS == 0` test runs last and overrides). For +/// `ABITS > 11` (no Table 5-26 row, no `SEL` transmitted) the spec's +/// "no further encoding is used for those quantizers" sentence +/// (PDF p.27) makes the type [`AudioQuantType::NoEncoding`]: the eight +/// indices are read as plain sign-extended fields. +#[must_use] +pub fn audio_quant_type(abits: u8, sel: u8) -> AudioQuantType { + if abits == 0 { + return AudioQuantType::NoBits; + } + if abits > ABITS_MAX_SEL { + // No SEL transmitted, no further encoding (PDF p.27). + return AudioQuantType::NoEncoding; + } + // Table 5-26 row: the terminal SEL entry is block (ABITS<=7) or + // NFE (ABITS>=8); every earlier SEL is a Huffman code book. + match terminal_sel_index(abits) { + Some(top) if sel == top => { + if abits <= ABITS_MAX_BLOCK_CODE { + AudioQuantType::BlockCode + } else { + AudioQuantType::NoEncoding + } + } + _ => AudioQuantType::Huffman, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quant_levels_match_table_5_26() { + // PDF p.27 "Number of Index Quantization Levels" column. + assert_eq!(QUANT_LEVELS[0], 0); + assert_eq!(QUANT_LEVELS[1], 3); + assert_eq!(QUANT_LEVELS[2], 5); + assert_eq!(QUANT_LEVELS[3], 7); + assert_eq!(QUANT_LEVELS[4], 9); + assert_eq!(QUANT_LEVELS[5], 13); + assert_eq!(QUANT_LEVELS[6], 17); + assert_eq!(QUANT_LEVELS[7], 25); + assert_eq!(QUANT_LEVELS[8], 33); + assert_eq!(QUANT_LEVELS[9], 65); + assert_eq!(QUANT_LEVELS[10], 129); + assert_eq!(QUANT_LEVELS[11], 256); + } + + #[test] + fn group_sizes_match_table_5_26() { + // SEL-column counts off Table 5-26 (PDF p.27). + assert_eq!(CODEBOOK_GROUP_SIZE[0], 0); + assert_eq!(CODEBOOK_GROUP_SIZE[1], 2); // A3 V3 + assert_eq!(CODEBOOK_GROUP_SIZE[2], 4); // A5 B5 C5 V5 + assert_eq!(CODEBOOK_GROUP_SIZE[3], 4); + assert_eq!(CODEBOOK_GROUP_SIZE[4], 4); + assert_eq!(CODEBOOK_GROUP_SIZE[5], 4); + assert_eq!(CODEBOOK_GROUP_SIZE[6], 8); // A17..G17 V17 + assert_eq!(CODEBOOK_GROUP_SIZE[7], 8); + assert_eq!(CODEBOOK_GROUP_SIZE[8], 8); // A33..G33 NFE + assert_eq!(CODEBOOK_GROUP_SIZE[9], 8); + assert_eq!(CODEBOOK_GROUP_SIZE[10], 8); + assert_eq!(CODEBOOK_GROUP_SIZE[11], 1); // NFE + } + + #[test] + fn tables_have_twelve_rows() { + assert_eq!(QUANT_LEVELS.len(), ABITS_TABLE_LEN); + assert_eq!(CODEBOOK_GROUP_SIZE.len(), ABITS_TABLE_LEN); + } + + #[test] + fn terminal_sel_index_is_group_size_minus_one() { + assert_eq!(terminal_sel_index(0), None); // not transmitted + assert_eq!(terminal_sel_index(1), Some(1)); // group of 2 -> top 1 + assert_eq!(terminal_sel_index(2), Some(3)); // group of 4 -> top 3 + assert_eq!(terminal_sel_index(6), Some(7)); // group of 8 -> top 7 + assert_eq!(terminal_sel_index(11), Some(0)); // group of 1 -> top 0 + assert_eq!(terminal_sel_index(12), None); // no Table 5-26 row + assert_eq!(terminal_sel_index(40), None); + } + + #[test] + fn abits_zero_is_no_bits_regardless_of_sel() { + for sel in 0u8..=7 { + assert_eq!(audio_quant_type(0, sel), AudioQuantType::NoBits); + } + } + + #[test] + fn non_terminal_sel_is_huffman() { + // ABITS 6 group of 8 (top SEL 7): SEL 0..6 are Huffman code + // books A17..G17. + for sel in 0u8..=6 { + assert_eq!(audio_quant_type(6, sel), AudioQuantType::Huffman); + } + // ABITS 2 group of 4 (top SEL 3): SEL 0..2 are A5/B5/C5. + for sel in 0u8..=2 { + assert_eq!(audio_quant_type(2, sel), AudioQuantType::Huffman); + } + } + + #[test] + fn terminal_sel_block_code_for_low_abits() { + // ABITS 1..=7 terminal entry is the V… block-code book. + for abits in 1u8..=ABITS_MAX_BLOCK_CODE { + let top = terminal_sel_index(abits).unwrap(); + assert_eq!( + audio_quant_type(abits, top), + AudioQuantType::BlockCode, + "ABITS {abits} terminal SEL {top}" + ); + } + } + + #[test] + fn terminal_sel_no_encoding_for_high_abits() { + // ABITS 8..=11 terminal entry is the NFE (no-further-encoding) + // slot. + for abits in 8u8..=ABITS_MAX_SEL { + let top = terminal_sel_index(abits).unwrap(); + assert_eq!( + audio_quant_type(abits, top), + AudioQuantType::NoEncoding, + "ABITS {abits} terminal SEL {top}" + ); + } + } + + #[test] + fn abits_eleven_only_entry_is_nfe() { + // ABITS 11 group has a single NFE entry at SEL 0. + assert_eq!(audio_quant_type(11, 0), AudioQuantType::NoEncoding); + } + + #[test] + fn abits_above_table_is_no_encoding() { + // PDF p.27: "No SEL is transmitted for ABITS[ch]>11, because + // no further encoding is used for those quantizers." + for abits in 12u8..=31 { + for sel in 0u8..=7 { + assert_eq!(audio_quant_type(abits, sel), AudioQuantType::NoEncoding); + } + } + } + + #[test] + fn full_abits_sel_dispatch_matrix() { + // Exhaustively cross-check audio_quant_type against the §5.5 + // pseudocode for every Table 5-26 (ABITS, SEL) pair. + for abits in 0u8..=ABITS_MAX_SEL { + let group = CODEBOOK_GROUP_SIZE[abits as usize]; + // ABITS 0 has no SEL field; the loop below is empty for it + // and the explicit NoBits check covers it. + if abits == 0 { + assert_eq!(audio_quant_type(0, 0), AudioQuantType::NoBits); + continue; + } + let top = group - 1; + for sel in 0..group { + let want = if sel == top { + if abits <= ABITS_MAX_BLOCK_CODE { + AudioQuantType::BlockCode + } else { + AudioQuantType::NoEncoding + } + } else { + AudioQuantType::Huffman + }; + assert_eq!( + audio_quant_type(abits, sel), + want, + "ABITS {abits} SEL {sel}" + ); + } + } + } + + #[test] + fn block_code_two_words_of_four_equals_eight_samples() { + // The §5.5 block-code path expands two code words to four + // samples each, matching SAMPLES_PER_SUBSUBFRAME = 8. + assert_eq!(crate::SAMPLES_PER_SUBSUBFRAME, 8); + } + + #[test] + fn constants_match_spec_bounds() { + assert_eq!(ABITS_MAX_SEL, 11); + assert_eq!(ABITS_MAX_BLOCK_CODE, 7); + assert_eq!(ABITS_TABLE_LEN, 12); + } +} diff --git a/crates/vendor/oxideav-dts/src/audio_header.rs b/crates/vendor/oxideav-dts/src/audio_header.rs new file mode 100644 index 00000000..7dd5527f --- /dev/null +++ b/crates/vendor/oxideav-dts/src/audio_header.rs @@ -0,0 +1,653 @@ +//! DTS Coherent Acoustics — §5.3.2 Primary Audio Coding Header +//! (ETSI TS 102 114 V1.3.1, Table 5-21, staged PDF p.24-28). +//! +//! Round 340 (2026-06-19) lands the Table 5-21 header decoder that the +//! round-281 [`crate::decode_primary_side_info_at`] side-info walker +//! and the §5.5 audio-data array walk both need: it produces the +//! per-channel loop bounds and codebook selectors (`nSUBFS`, `nPCHS`, +//! `nSUBS[ch]`, `nVQSUB[ch]`, `JOINX[ch]`, `THUFF`/`SHUFF`/`BHUFF`), +//! the transposed `SEL[ch][n]` quantization-index codebook plane, and +//! the `arADJ[ch][n]` scale-factor adjustment plane that the §5.5 +//! `rScale *= arADJ[ch][SEL[ch][nABITS-1]]` step multiplies in. +//! +//! Field order, exactly as Table 5-21 fixes it (PDF p.24-25): +//! +//! ```text +//! SUBFS = ExtractBits(4); nSUBFS = SUBFS + 1; // 4 bits +//! PCHS = ExtractBits(3); nPCHS = PCHS + 1; // 3 bits +//! for (ch) SUBS[ch] = ExtractBits(5); nSUBS[ch] = SUBS[ch] + 2; +//! for (ch) VQSUB[ch] = ExtractBits(5); nVQSUB[ch] = VQSUB[ch] + 1; +//! for (ch) JOINX[ch] = ExtractBits(3); +//! for (ch) THUFF[ch] = ExtractBits(2); +//! for (ch) SHUFF[ch] = ExtractBits(3); +//! for (ch) BHUFF[ch] = ExtractBits(3); +//! // SEL plane, ABITS-major then channel-minor: +//! for (ch) SEL[ch][0] = ExtractBits(1); // ABITS 1 +//! for (n=1..5) for (ch) SEL[ch][n] = ExtractBits(2); // ABITS 2..5 +//! for (n=5..10) for (ch) SEL[ch][n] = ExtractBits(3); // ABITS 6..10 +//! for (n=10..26) for (ch) SEL[ch][n] = 0; // not transmitted +//! // ADJ plane (only where SEL indicates a Huffman code book): +//! for (ch) if (SEL[ch][0] == 0) arADJ[ch][0] = AdjTable[ExtractBits(2)]; +//! for (n=1..5) for (ch) if (SEL[ch][n] < 3) arADJ[ch][n] = AdjTable[ExtractBits(2)]; +//! for (n=5..10) for (ch) if (SEL[ch][n] < 7) arADJ[ch][n] = AdjTable[ExtractBits(2)]; +//! if (CPF == 1) AHCRC = ExtractBits(16); // header CRC, skipped +//! ``` +//! +//! The §5.3.2 `SEL[ch][n]` plane is indexed by the *`ABITS` index* +//! `n` (`0..26`), **not** by the subband index. The §5.5 audio-array +//! walk reads `SEL[ch][nABITS-1]` for each subband's `ABITS[ch][n]` +//! value — see [`AudioCodingHeader::sel`] / [`AudioCodingHeader::adj`]. +//! +//! # Scope +//! +//! The `AHCRC` Header CRC tail (transmitted only when `CPF == 1`) is +//! skipped: the algorithm is the Annex B CRC-CCITT +//! ([`crate::dts_crc16`], `docs/audio/dts/dts-crc16.md`), but the +//! spec text states "the CRC value test shall not be applied" for the +//! core check words (`HCRC` / `AHCRC` / `SICRC` / `OCRC`) — they are +//! informational placeholders. The 16-bit field is consumed (so the cursor lands +//! at the first §5.4 subframe bit) but not verified. `CPF` is the +//! §5.3.1 frame-header "Predictor History Flag Switch" companion — it +//! is passed in by the caller (the round-202 header carries it as +//! [`crate::DtsFrameHeader::predictor_history`]). + +use crate::bitreader::BitReader; +use crate::side_info::{AbitsCodebook, ScaleFactorAdjustment, ScalesCodebook, TmodeCodebook}; +use crate::subframe::{ChannelSideInfoParams, MAX_PRIMARY_CHANNELS}; +use crate::{Error, Result}; + +/// Number of `ABITS` indices the §5.3.2 `SEL[ch][n]` / `arADJ[ch][n]` +/// planes tabulate (Table 5-21: `n = 0..26`, i.e. `ABITS = 1..26`). +/// `SEL`/`ADJ` slots for `n >= SEL_PLANE_LEN` are never transmitted +/// (the spec sets them to zero). +pub const SEL_PLANE_LEN: usize = 26; + +/// Decoded §5.3.2 Primary Audio Coding Header (Table 5-21). +/// +/// Carries the per-channel loop bounds + codebook selectors the §5.4.1 +/// side-info walk and §5.5 audio-data array consume, plus the +/// transposed `SEL`/`arADJ` planes indexed by `ABITS` index. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct AudioCodingHeader { + /// `nSUBFS = SUBFS + 1` — number of audio subframes in the core + /// frame (Table 5-21 / PDF p.25). Range `1..=16`. + pub n_subframes: usize, + /// `nPCHS = PCHS + 1` — number of primary audio channels + /// (`1..=5`, PDF p.25). + pub n_pchs: usize, + /// `JOINX[ch]` — joint-intensity source-channel index (Table 5-22): + /// `0` = disabled, `>0` = source channel `JOINX[ch]`. + pub joinx: Vec, + /// Per-channel side-info loop bounds + Huffman codebook selectors, + /// ready to hand to [`crate::decode_primary_side_info_at`]. + pub channel_params: Vec, + /// `SEL[ch][n]` — the quantization-index codebook selector for + /// `ABITS` index `n` (`0..26` ↔ `ABITS 1..26`), per channel. Slots + /// `n >= SEL_PLANE_LEN` are zero (not transmitted). + sel: Vec<[u8; SEL_PLANE_LEN]>, + /// `arADJ[ch][n]` — the §5.5 scale-factor adjustment multiplier for + /// `ABITS` index `n`, per channel. Defaults to + /// [`ScaleFactorAdjustment::Adj0`] (unity) where no `ADJ` field was + /// transmitted (the spec's `arADJ == 1` default). + adj: Vec<[ScaleFactorAdjustment; SEL_PLANE_LEN]>, +} + +impl AudioCodingHeader { + /// The `SEL[ch][nABITS-1]` quantization-index codebook selector for + /// a subband whose bit-allocation index is `abits` (`>= 1`). The + /// §5.5 walk reads `SEL[ch][nABITS-1]`; this accessor takes the + /// raw `abits` and applies the `-1` ABITS→plane-index shift. + /// + /// Returns `0` for `abits == 0` (no bits allocated; no SEL) and for + /// `abits > SEL_PLANE_LEN` (PDF p.27: "No SEL is transmitted for + /// `ABITS > 11`", and `ABITS > 11` carries no further encoding). + #[must_use] + pub fn sel(&self, ch: usize, abits: u8) -> u8 { + if abits == 0 || ch >= self.sel.len() { + return 0; + } + let idx = (abits - 1) as usize; + if idx >= SEL_PLANE_LEN { + return 0; + } + self.sel[ch][idx] + } + + /// The `arADJ[ch][SEL[ch][nABITS-1]]` scale-factor adjustment for a + /// subband whose bit-allocation index is `abits` (`>= 1`), keyed — + /// like [`Self::sel`] — by the `nABITS-1` ABITS→plane index. + /// + /// Returns [`ScaleFactorAdjustment::Adj0`] (unity) for `abits == 0` + /// or out-of-range channels/indices — the §5.5 default of + /// `arADJ == 1` when no `ADJ` field was read. + #[must_use] + pub fn adj(&self, ch: usize, abits: u8) -> ScaleFactorAdjustment { + if abits == 0 || ch >= self.adj.len() { + return ScaleFactorAdjustment::Adj0; + } + let idx = (abits - 1) as usize; + if idx >= SEL_PLANE_LEN { + return ScaleFactorAdjustment::Adj0; + } + self.adj[ch][idx] + } + + /// The per-channel `nSUBS[ch]` loop bound (number of active subbands) + /// collected from [`Self::channel_params`], in channel order. This + /// is the slice the §5.5 [`crate::decode_audio_data_subframe_at`] + /// walk and the §C.2.5 [`crate::MultiChannelQmf`] driver both take. + #[must_use] + pub fn n_subs(&self) -> Vec { + self.channel_params.iter().map(|p| p.n_subs).collect() + } + + /// The per-channel `nVQSUB[ch]` loop bound (the highest + /// non-high-frequency-VQ subband index) collected from + /// [`Self::channel_params`], in channel order. The §5.5 walk uses it + /// as its inner-loop bound and to detect the §D.10.2 high-frequency + /// VQ region (`nVQSUB < nSUBS`). + #[must_use] + pub fn n_vqsub(&self) -> Vec { + self.channel_params.iter().map(|p| p.n_vqsub).collect() + } + + /// Test-only constructor for a single-channel [`AudioCodingHeader`] + /// with a uniform `sel` across the whole SEL plane, used by the + /// `subframe_pcm` bridge tests that need a header without parsing a + /// full §5.3.2 bit stream. `n_subs` / `n_vqsub` set the channel's + /// loop bounds; `joinx` defaults to 0 (no joint coding). + #[cfg(test)] + pub(crate) fn single_channel_for_test(n_subs: usize, n_vqsub: usize, sel: u8) -> Self { + use crate::subframe::ChannelSideInfoParams; + AudioCodingHeader { + n_subframes: 1, + n_pchs: 1, + joinx: vec![0], + channel_params: vec![ChannelSideInfoParams { + n_subs, + n_vqsub, + abits_codebook: AbitsCodebook::from_bhuff(0).unwrap(), + tmode_codebook: TmodeCodebook::from_thuff(0), + scales_codebook: ScalesCodebook::from_shuff(0).unwrap(), + }], + sel: vec![[sel; SEL_PLANE_LEN]], + adj: vec![[ScaleFactorAdjustment::Adj0; SEL_PLANE_LEN]], + } + } + + /// Test-only setter for `joinx[ch]`, used by the `subframe_pcm` + /// bridge test that checks the joint-intensity decline path. + #[cfg(test)] + pub(crate) fn set_joinx_for_test(&mut self, ch: usize, joinx: u8) { + self.joinx[ch] = joinx; + } + + /// Test-only constructor for a two-channel header, used by the + /// `subframe_pcm` bridge tests that exercise the §C.2.3 + /// joint-intensity sub-band copy. Both channels default to + /// `JOINX == 0`; per-channel loop bounds come from `n_subs` / + /// `n_vqsub` pairs. + #[cfg(test)] + pub(crate) fn two_channel_for_test(ch0: (usize, usize), ch1: (usize, usize), sel: u8) -> Self { + use crate::subframe::ChannelSideInfoParams; + let mk = |n_subs: usize, n_vqsub: usize| ChannelSideInfoParams { + n_subs, + n_vqsub, + abits_codebook: AbitsCodebook::from_bhuff(0).unwrap(), + tmode_codebook: TmodeCodebook::from_thuff(0), + scales_codebook: ScalesCodebook::from_shuff(0).unwrap(), + }; + AudioCodingHeader { + n_subframes: 1, + n_pchs: 2, + joinx: vec![0, 0], + channel_params: vec![mk(ch0.0, ch0.1), mk(ch1.0, ch1.1)], + sel: vec![[sel; SEL_PLANE_LEN]; 2], + adj: vec![[ScaleFactorAdjustment::Adj0; SEL_PLANE_LEN]; 2], + } + } +} + +/// The §5.3.2 `SEL` plane bit width for `ABITS` index `n` (`0..26`), +/// per Table 5-21: `n == 0` (ABITS 1) is 1 bit, `n ∈ 1..5` (ABITS 2-5) +/// is 2 bits, `n ∈ 5..10` (ABITS 6-10) is 3 bits, `n >= 10` is not +/// transmitted (0 bits). +fn sel_bit_width(n: usize) -> u32 { + match n { + 0 => 1, + 1..=4 => 2, + 5..=9 => 3, + _ => 0, + } +} + +/// The §5.3.2 `ADJ`-transmitted predicate for `ABITS` index `n`: the +/// `ADJ` field follows `SEL[ch][n]` only when `SEL` indicates a Huffman +/// code book — i.e. `SEL` is *below* the group's terminal (block / +/// NFE) entry. Table 5-21 spells the per-group bound out as: +/// `n == 0` → `SEL == 0`; `n ∈ 1..5` → `SEL < 3`; `n ∈ 5..10` → +/// `SEL < 7`. +fn adj_transmitted(n: usize, sel: u8) -> bool { + match n { + 0 => sel == 0, + 1..=4 => sel < 3, + 5..=9 => sel < 7, + _ => false, + } +} + +/// Decode the §5.3.2 Primary Audio Coding Header (Table 5-21) from +/// `bytes` starting at `bit_offset` (MSB-first from `bytes[0]`). +/// +/// `cpf` is the §5.3.1 frame-header `CPF` (CRC Present Flag, Table 5-1) +/// bit — when set, a 16-bit `AHCRC` Header CRC trailer is consumed (but +/// not verified; see the module docs). Pass +/// [`crate::DtsFrameHeader::crc_present`], **not** `predictor_history`: +/// `CPF` is the same flag that gates the §5.3.1 `HCRC`, the §5.4.1 +/// `SICRC`, and this `AHCRC`. +/// +/// Returns `(AudioCodingHeader, bits_consumed)`; the cursor +/// `bit_offset + bits_consumed` is the first bit of the §5.4 subframe +/// region (the round-281 side-info walk's entry point). +/// +/// # Errors +/// +/// * [`Error::InvalidSideInfo`] with field `"nPCHS"` when +/// `nPCHS > 5`, `"nSUBS"` when a channel's `nSUBS` exceeds +/// [`NUM_SUBBAND`](crate::NUM_SUBBAND), or `"VQSUB"` when +/// `nVQSUB > nSUBS`; +/// * [`Error::InvalidSideInfo`] with field `"BHUFF"` / `"SHUFF"` when a +/// reserved selector value `7` is read; +/// * [`Error::UnexpectedEof`] when the buffer ends mid-walk. +pub fn decode_audio_coding_header_at( + bytes: &[u8], + bit_offset: usize, + cpf: bool, +) -> Result<(AudioCodingHeader, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + + // SUBFS / PCHS. + let n_subframes = br.read_bits(4)? as usize + 1; + let n_pchs = br.read_bits(3)? as usize + 1; + if n_pchs > MAX_PRIMARY_CHANNELS { + return Err(Error::InvalidSideInfo { + field: "nPCHS", + value: n_pchs as u32, + }); + } + + // SUBS[ch] -> nSUBS[ch]. + let mut n_subs = vec![0usize; n_pchs]; + for slot in n_subs.iter_mut() { + let v = br.read_bits(5)? as usize + 2; + if v > crate::cos_mod::NUM_SUBBAND { + return Err(Error::InvalidSideInfo { + field: "nSUBS", + value: v as u32, + }); + } + *slot = v; + } + + // VQSUB[ch] -> nVQSUB[ch]. + let mut n_vqsub = vec![0usize; n_pchs]; + for (ch, slot) in n_vqsub.iter_mut().enumerate() { + let v = br.read_bits(5)? as usize + 1; + if v > n_subs[ch] { + return Err(Error::InvalidSideInfo { + field: "VQSUB", + value: v as u32, + }); + } + *slot = v; + } + + // JOINX[ch]. + let mut joinx = vec![0u8; n_pchs]; + for slot in joinx.iter_mut() { + *slot = br.read_bits(3)? as u8; + } + + // THUFF[ch] / SHUFF[ch] / BHUFF[ch]. + let mut thuff = vec![0u8; n_pchs]; + for slot in thuff.iter_mut() { + *slot = br.read_bits(2)? as u8; + } + let mut shuff = vec![0u8; n_pchs]; + for slot in shuff.iter_mut() { + *slot = br.read_bits(3)? as u8; + } + let mut bhuff = vec![0u8; n_pchs]; + for slot in bhuff.iter_mut() { + *slot = br.read_bits(3)? as u8; + } + + // SEL[ch][n] plane, ABITS-major then channel-minor (Table 5-21). + let mut sel = vec![[0u8; SEL_PLANE_LEN]; n_pchs]; + for n in 0..SEL_PLANE_LEN { + let width = sel_bit_width(n); + if width == 0 { + // ABITS >= 11: not transmitted, already zero. + continue; + } + for sel_ch in sel.iter_mut() { + sel_ch[n] = br.read_bits(width)? as u8; + } + } + + // arADJ[ch][n] plane: an ADJ field follows each SEL that indicates + // a Huffman code book, in the same ABITS-major / channel-minor + // order. Default is unity (Adj0) everywhere else. + let mut adj = vec![[ScaleFactorAdjustment::Adj0; SEL_PLANE_LEN]; n_pchs]; + for n in 0..SEL_PLANE_LEN { + if sel_bit_width(n) == 0 { + continue; + } + for (ch, adj_ch) in adj.iter_mut().enumerate() { + if adj_transmitted(n, sel[ch][n]) { + let raw = br.read_bits(2)? as u8; + adj_ch[n] = ScaleFactorAdjustment::from_index(raw); + } + } + } + + // AHCRC Header CRC trailer — consumed but not verified. + if cpf { + br.read_bits(16)?; + } + + // Resolve the per-channel codebook selectors into the round-195 + // typed selectors; reserved 7 values surface InvalidSideInfo here. + let mut channel_params = Vec::with_capacity(n_pchs); + for ch in 0..n_pchs { + let abits_codebook = AbitsCodebook::from_bhuff(bhuff[ch])?; + let tmode_codebook = TmodeCodebook::from_thuff(thuff[ch]); + let scales_codebook = ScalesCodebook::from_shuff(shuff[ch])?; + channel_params.push(ChannelSideInfoParams { + n_subs: n_subs[ch], + n_vqsub: n_vqsub[ch], + abits_codebook, + tmode_codebook, + scales_codebook, + }); + } + + let bits_consumed = br.absolute_bit_position() - bit_offset; + Ok(( + AudioCodingHeader { + n_subframes, + n_pchs, + joinx, + channel_params, + sel, + adj, + }, + bits_consumed, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Pack a series of (value, bit_width) fields MSB-first. + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + /// Build the fixed-width prefix of a one-channel header up to the + /// SEL plane: SUBFS, PCHS, SUBS, VQSUB, JOINX, THUFF, SHUFF, BHUFF. + fn one_channel_prefix( + subfs: u32, + subs: u32, + vqsub: u32, + joinx: u32, + thuff: u32, + shuff: u32, + bhuff: u32, + ) -> Vec<(u32, u8)> { + vec![ + (subfs, 4), + (0, 3), // PCHS = 0 -> nPCHS = 1 + (subs, 5), + (vqsub, 5), + (joinx, 3), + (thuff, 2), + (shuff, 3), + (bhuff, 3), + ] + } + + #[test] + fn single_channel_linear_header_decodes_bounds() { + // SUBFS=0 -> 1 subframe; nSUBS = 2+2 = 4; nVQSUB = 1+1 = 2; + // JOINX=0; THUFF=3 (D4); SHUFF=5 (6-bit linear); BHUFF=6 + // (Linear5Bit). SEL: ABITS 1 (n=0) 1 bit = 0; ABITS 2-5 four + // 2-bit = 0; ABITS 6-10 five 3-bit = 0. With every SEL == 0 + // each transmits an ADJ (Huffman-coded path), so 10 ADJ + // fields of 2 bits follow. + let mut fields = one_channel_prefix(0, 2, 1, 0, 3, 5, 6); + // SEL plane: n=0 1 bit, n=1..4 2 bits, n=5..9 3 bits. + fields.push((0, 1)); + for _ in 1..5 { + fields.push((0, 2)); + } + for _ in 5..10 { + fields.push((0, 3)); + } + // ADJ plane: SEL==0 everywhere -> every group transmits ADJ. + for _ in 0..10 { + fields.push((0, 2)); + } + let bytes = pack_fields(&fields); + let (hdr, _) = decode_audio_coding_header_at(&bytes, 0, false).unwrap(); + assert_eq!(hdr.n_subframes, 1); + assert_eq!(hdr.n_pchs, 1); + assert_eq!(hdr.channel_params.len(), 1); + assert_eq!(hdr.channel_params[0].n_subs, 4); + assert_eq!(hdr.channel_params[0].n_vqsub, 2); + assert_eq!(hdr.joinx, vec![0]); + assert_eq!( + hdr.channel_params[0].abits_codebook, + AbitsCodebook::Linear5Bit + ); + assert_eq!(hdr.channel_params[0].tmode_codebook, TmodeCodebook::D4); + assert_eq!( + hdr.channel_params[0].scales_codebook, + ScalesCodebook::Linear6Bit + ); + } + + #[test] + fn sel_plane_routes_by_abits_index() { + // SEL values that distinguish the three width groups: set + // SEL[0][0]=1 (ABITS 1), SEL[0][1]=2 (ABITS 2), SEL[0][5]=4 + // (ABITS 6). Choose values so no ADJ is transmitted where we + // want clarity: ABITS-1 SEL=1 (not 0) -> no ADJ; ABITS-2 SEL=2 + // (not <3? 2<3 true) -> ADJ present; ABITS-6 SEL=4 (<7) -> ADJ + // present. To keep the stream simple, set every other SEL to + // its terminal so no ADJ follows. + let mut fields = one_channel_prefix(0, 2, 2, 0, 0, 5, 6); + // SEL plane. + fields.push((1, 1)); // n=0 ABITS1: SEL=1 (terminal, no ADJ) + fields.push((2, 2)); // n=1 ABITS2: SEL=2 (<3 -> ADJ) + fields.push((3, 2)); // n=2 ABITS3: SEL=3 (terminal, no ADJ) + fields.push((3, 2)); // n=3 ABITS4: SEL=3 terminal + fields.push((3, 2)); // n=4 ABITS5: SEL=3 terminal + fields.push((4, 3)); // n=5 ABITS6: SEL=4 (<7 -> ADJ) + fields.push((7, 3)); // n=6 ABITS7: SEL=7 terminal + fields.push((7, 3)); // n=7 ABITS8: SEL=7 terminal + fields.push((7, 3)); // n=8 ABITS9: SEL=7 terminal + fields.push((7, 3)); // n=9 ABITS10: SEL=7 terminal + // ADJ plane: only ABITS2 (SEL 2<3) and ABITS6 (SEL 4<7) emit. + fields.push((1, 2)); // ABITS2 ADJ index 1 (1.1250) + fields.push((2, 2)); // ABITS6 ADJ index 2 (1.2500) + let bytes = pack_fields(&fields); + let (hdr, _) = decode_audio_coding_header_at(&bytes, 0, false).unwrap(); + // sel() applies the ABITS-1 shift: sel(ch, abits). + assert_eq!(hdr.sel(0, 1), 1); + assert_eq!(hdr.sel(0, 2), 2); + assert_eq!(hdr.sel(0, 6), 4); + // abits 0 and out-of-range yield SEL 0. + assert_eq!(hdr.sel(0, 0), 0); + assert_eq!(hdr.sel(0, 30), 0); + // ADJ resolved only where transmitted. + assert_eq!(hdr.adj(0, 2), ScaleFactorAdjustment::from_index(1)); + assert_eq!(hdr.adj(0, 6), ScaleFactorAdjustment::from_index(2)); + // Non-transmitted ADJ defaults to unity. + assert_eq!(hdr.adj(0, 1), ScaleFactorAdjustment::Adj0); + assert_eq!(hdr.adj(0, 7), ScaleFactorAdjustment::Adj0); + } + + #[test] + fn two_channel_planes_interleave_by_channel() { + // nPCHS = 2: every per-channel loop reads ch0 then ch1. + let fields = vec![ + (0, 4), // SUBFS = 0 + (1, 3), // PCHS = 1 -> nPCHS = 2 + (0, 5), // SUBS[0] = 0 -> nSUBS 2 + (3, 5), // SUBS[1] = 3 -> nSUBS 5 + (0, 5), // VQSUB[0] = 0 -> nVQSUB 1 + (1, 5), // VQSUB[1] = 1 -> nVQSUB 2 + (0, 3), // JOINX[0] + (1, 3), // JOINX[1] = 1 (source ch 1) + (0, 2), // THUFF[0] = A4 + (1, 2), // THUFF[1] = B4 + (5, 3), // SHUFF[0] = 6-bit linear + (6, 3), // SHUFF[1] = 7-bit linear + (6, 3), // BHUFF[0] = Linear5Bit + (5, 3), // BHUFF[1] = Linear4Bit + // SEL plane n=0 (1 bit) ch0,ch1 then n=1..4 (2 bit) ... + (1, 1), + (1, 1), // SEL[*][0]=1 terminal, no ADJ + ]; + // Fill remaining SEL groups with terminal values (no ADJ) to + // keep the stream self-contained: ABITS2-5 SEL=3, ABITS6-10 + // SEL=7, both channels. + let mut fields = fields; + for _ in 1..5 { + fields.push((3, 2)); + fields.push((3, 2)); + } + for _ in 5..10 { + fields.push((7, 3)); + fields.push((7, 3)); + } + let bytes = pack_fields(&fields); + let (hdr, _) = decode_audio_coding_header_at(&bytes, 0, false).unwrap(); + assert_eq!(hdr.n_pchs, 2); + assert_eq!(hdr.channel_params[0].n_subs, 2); + assert_eq!(hdr.channel_params[1].n_subs, 5); + assert_eq!(hdr.channel_params[0].n_vqsub, 1); + assert_eq!(hdr.channel_params[1].n_vqsub, 2); + assert_eq!(hdr.joinx, vec![0, 1]); + assert_eq!(hdr.channel_params[0].tmode_codebook, TmodeCodebook::A4); + assert_eq!(hdr.channel_params[1].tmode_codebook, TmodeCodebook::B4); + assert_eq!( + hdr.channel_params[0].abits_codebook, + AbitsCodebook::Linear5Bit + ); + assert_eq!( + hdr.channel_params[1].abits_codebook, + AbitsCodebook::Linear4Bit + ); + } + + #[test] + fn reserved_bhuff_seven_rejected() { + let mut fields = one_channel_prefix(0, 0, 0, 0, 0, 0, 7); + // pad SEL plane (all terminal, no ADJ) so the walk doesn't EOF + // before the BHUFF resolve — but the resolve runs after the + // full bit walk, so just supply enough bytes. + fields.push((0, 1)); + for _ in 1..5 { + fields.push((3, 2)); + } + for _ in 5..10 { + fields.push((7, 3)); + } + // ABITS1 SEL=0 -> one ADJ. + fields.push((0, 2)); + let bytes = pack_fields(&fields); + assert_eq!( + decode_audio_coding_header_at(&bytes, 0, false).unwrap_err(), + Error::InvalidSideInfo { + field: "BHUFF", + value: 7 + } + ); + } + + #[test] + fn too_many_channels_rejected() { + // PCHS = 5 -> nPCHS = 6 > 5. + let fields = vec![(0, 4), (5, 3)]; + let bytes = pack_fields(&fields); + assert_eq!( + decode_audio_coding_header_at(&bytes, 0, false).unwrap_err(), + Error::InvalidSideInfo { + field: "nPCHS", + value: 6 + } + ); + } + + #[test] + fn cpf_consumes_ahcrc_trailer() { + // Build a minimal 1-channel header with all-terminal SEL (no + // ADJ) and CPF set, then confirm 16 extra bits are consumed. + let mut fields = one_channel_prefix(0, 0, 0, 0, 0, 5, 6); + fields.push((1, 1)); // ABITS1 SEL=1 terminal + for _ in 1..5 { + fields.push((3, 2)); + } + for _ in 5..10 { + fields.push((7, 3)); + } + let without_crc = fields.clone(); + let mut with_crc = fields; + with_crc.push((0xABCD, 16)); // AHCRC + + let bytes_no = pack_fields(&without_crc); + let (_, bits_no) = decode_audio_coding_header_at(&bytes_no, 0, false).unwrap(); + let bytes_crc = pack_fields(&with_crc); + let (_, bits_crc) = decode_audio_coding_header_at(&bytes_crc, 0, true).unwrap(); + assert_eq!(bits_crc, bits_no + 16); + } + + #[test] + fn arbitrary_bit_offset_matches_aligned() { + let mut fields = one_channel_prefix(0, 0, 0, 0, 0, 5, 6); + fields.push((1, 1)); + for _ in 1..5 { + fields.push((3, 2)); + } + for _ in 5..10 { + fields.push((7, 3)); + } + let aligned = pack_fields(&fields); + let mut shifted_fields = vec![(0b101, 3)]; + shifted_fields.extend_from_slice(&fields); + let shifted = pack_fields(&shifted_fields); + let (a, bits_a) = decode_audio_coding_header_at(&aligned, 0, false).unwrap(); + let (b, bits_b) = decode_audio_coding_header_at(&shifted, 3, false).unwrap(); + assert_eq!(a, b); + assert_eq!(bits_a, bits_b); + } +} diff --git a/crates/vendor/oxideav-dts/src/audio_huff.rs b/crates/vendor/oxideav-dts/src/audio_huff.rs new file mode 100644 index 00000000..dc6a7092 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/audio_huff.rs @@ -0,0 +1,3197 @@ +//! DTS Coherent Acoustics — Annex D §D.5 audio-data quantization-index +//! Huffman code books (the low-to-mid `ABITS` families), feeding the §5.5 +//! Table 5-29 `nQType == 1` ("Huffman code") `AUDIO[m]` extraction +//! path. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), staged PDF at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. +//! +//! # What this module covers +//! +//! The §5.5 `Audio Data` walker dispatches each subband on its +//! `nQType` (see [`crate::audio_quant_type`]). For `nQType == 1` the +//! eight `AUDIO[m]` quantization indices are Huffman-coded, decoded +//! through the §D.5 code book that the `(ABITS, SEL)` pair selects +//! (Table 5-26, staged PDF p.27). This module transcribes the +//! **signed-level** audio-data code books for the lowest `ABITS` +//! families — the ones whose mid-tread quantizer has a small enough +//! level count to print a full code book: +//! +//! | ABITS | levels | clause | SEL → book | +//! |-------|--------|---------|----------------------------------| +//! | 1 | 3 | §D.5.1 | SEL 0 → `A3` | +//! | 2 | 5 | §D.5.3 | SEL 0/1/2 → `A5/B5/C5` | +//! | 3 | 7 | §D.5.4 | SEL 0/1/2 → `A7/B7/C7` | +//! | 4 | 9 | §D.5.5 | SEL 0/1/2 → `A9/B9/C9` | +//! | 5 | 13 | §D.5.7 | SEL 0/1/2 → `A13/B13/C13` | +//! | 6 | 17 | §D.5.8 | SEL 0..6 → `A17/B17/…/G17` | +//! | 7 | 25 | §D.5.9 | SEL 0..6 → `A25/B25/…/G25` | +//! | 8 | 33 | §D.5.10 | SEL 0..6 → `A33/B33/…/G33` | +//! | 9 | 65 | §D.5.11 | SEL 0..6 → `A65/B65/…/G65` | +//! | 10 | 129 | §D.5.12 | SEL 0..6 → `A129/B129/…/G129` | +//! +//! (The terminal `SEL` entry of each group is the `V…` block code — +//! `nQType == 3`, handled by [`crate::decode_block_code`] — not a +//! Huffman book, so it is *not* in this module. For the 17-level group +//! that terminal entry is `V17` at SEL 7.) +//! +//! Unlike the §5.4.1 side-information code books (BHUFF/THUFF/SHUFF), +//! whose symbols are *unsigned* indices, the audio-data books decode +//! to **signed** quantization levels: the printed "Quantization level" +//! column runs `0, 1, -1, 2, -2, …`, mirroring the mid-tread +//! quantizer's symmetric output. The decoded level is the signed +//! integer `AUDIO[m]` that §5.5 then scales by `rScale` +//! ([`crate::dequant_subsubframe`]). +//! +//! # Scope and follow-ups +//! +//! For `ABITS 8..=10` the Table 5-26 group is `A… B… C… D… E… F… G… +//! NFE` (eight entries): SEL 0..6 select the seven Huffman books and +//! the terminal SEL 7 the "no further encoding" fixed-width field +//! (`nQType == 2`), *not* a `V…` block code. All three higher families +//! (§D.5.10/§D.5.11/§D.5.12) are now transcribed here, so the §5.5 +//! per-subsubframe `Audio Data` walker can dispatch any +//! `nQType == 1` subband for every `ABITS 1..=10`. +//! +//! The Table 5-26 (staged PDF p.27) `(ABITS, SEL)` mapping is the +//! authoritative selector: for ABITS 5 (13 levels) the group is +//! `A13 B13 C13 V13`, so SEL 0/1/2 select the Huffman books and SEL 3 +//! the terminal `V13` 4-element block code ([`crate::decode_block_code`], +//! `nQType == 3`). + +use crate::bitreader::BitReader; +use crate::{Error, Result}; + +/// One entry of a §D.5 audio-data Huffman code book: +/// `(quantization_level, code_length, code)`. The codeword is the low +/// `code_length` bits of `code`, read MSB-first from the bit stream +/// (the [`BitReader::read_bits`] convention used throughout this +/// crate). `quantization_level` is the **signed** mid-tread output +/// level the §5.5 `AUDIO[m]` index carries. +type AudioHuffEntry = (i16, u8, u16); + +/// Maximum code length across every audio-data Huffman book transcribed +/// here, the bound the bit-at-a-time decoder walks to before declaring a +/// stream-format failure. The longest printed codes are 16 bits, in the +/// 65-level §D.5.11 and 129-level §D.5.12 families (e.g. §D.5.11 `A65`'s +/// ±31/±32 codes 40 543/40 542/40 541/40 540 and §D.5.12 `D129`'s ±64); +/// the 33-level §D.5.10 family tops out at 14 (`D33` ±15/±16), the +/// 25-level §D.5.9 `A25` likewise at 14, and the smaller families far +/// below. The decoder still stops at each book's own +/// [`AudioHuffCodebook::max_code_len`] once a prefix-matched code is +/// found, so this only caps the worst case. +const MAX_AUDIO_HUFF_CODE_LEN: u32 = 16; + +// --------------------------------------------------------------- +// §D.5.1 — 3 Levels (ABITS 1, SEL 0). Staged PDF p.198. +// --------------------------------------------------------------- + +/// Annex D §D.5.1 Table A3. +const TABLE_A3: &[AudioHuffEntry] = &[ + (0, 1, 0), // 0 + (1, 2, 2), // +1 + (-1, 2, 3), // -1 +]; + +// --------------------------------------------------------------- +// §D.5.3 — 5 Levels (ABITS 2, SEL 0/1/2). Staged PDF p.199. +// --------------------------------------------------------------- + +/// Annex D §D.5.3 Table A5. +const TABLE_A5: &[AudioHuffEntry] = &[(0, 1, 0), (1, 2, 2), (-1, 3, 6), (2, 4, 14), (-2, 4, 15)]; + +/// Annex D §D.5.3 Table B5. +const TABLE_B5: &[AudioHuffEntry] = &[(0, 2, 2), (1, 2, 0), (-1, 2, 1), (2, 3, 6), (-2, 3, 7)]; + +/// Annex D §D.5.3 Table C5. +const TABLE_C5: &[AudioHuffEntry] = &[(0, 1, 0), (1, 3, 4), (-1, 3, 5), (2, 3, 6), (-2, 3, 7)]; + +// --------------------------------------------------------------- +// §D.5.4 — 7 Levels (ABITS 3, SEL 0/1/2). Staged PDF p.199-200. +// --------------------------------------------------------------- + +/// Annex D §D.5.4 Table A7. +const TABLE_A7: &[AudioHuffEntry] = &[ + (0, 1, 0), + (1, 3, 6), + (-1, 3, 5), + (2, 3, 4), + (-2, 4, 14), + (3, 5, 31), + (-3, 5, 30), +]; + +/// Annex D §D.5.4 Table B7. +const TABLE_B7: &[AudioHuffEntry] = &[ + (0, 2, 3), + (1, 2, 1), + (-1, 2, 0), + (2, 3, 4), + (-2, 4, 11), + (3, 5, 21), + (-3, 5, 20), +]; + +/// Annex D §D.5.4 Table C7. +const TABLE_C7: &[AudioHuffEntry] = &[ + (0, 2, 3), + (1, 2, 2), + (-1, 2, 1), + (2, 4, 3), + (-2, 4, 2), + (3, 4, 1), + (-3, 4, 0), +]; + +// --------------------------------------------------------------- +// §D.5.5 — 9 Levels (ABITS 4, SEL 0/1/2). Staged PDF p.200-201. +// --------------------------------------------------------------- + +/// Annex D §D.5.5 Table A9. +const TABLE_A9: &[AudioHuffEntry] = &[ + (0, 1, 0), + (1, 3, 7), + (-1, 3, 5), + (2, 4, 13), + (-2, 4, 9), + (3, 4, 8), + (-3, 5, 25), + (4, 6, 49), + (-4, 6, 48), +]; + +/// Annex D §D.5.5 Table B9. +const TABLE_B9: &[AudioHuffEntry] = &[ + (0, 2, 2), + (1, 2, 0), + (-1, 3, 7), + (2, 3, 3), + (-2, 3, 2), + (3, 5, 27), + (-3, 5, 26), + (4, 5, 25), + (-4, 5, 24), +]; + +/// Annex D §D.5.5 Table C9. +const TABLE_C9: &[AudioHuffEntry] = &[ + (0, 2, 2), + (1, 2, 0), + (-1, 3, 7), + (2, 3, 6), + (-2, 3, 2), + (3, 4, 6), + (-3, 5, 15), + (4, 6, 29), + (-4, 6, 28), +]; + +// --------------------------------------------------------------- +// §D.5.7 — 13 Levels (ABITS 5, SEL 0/1/2). Staged PDF p.202-203. +// --------------------------------------------------------------- + +/// Annex D §D.5.7 Table A13. +const TABLE_A13: &[AudioHuffEntry] = &[ + (0, 1, 0), + (1, 3, 4), + (-1, 4, 15), + (2, 4, 13), + (-2, 4, 12), + (3, 4, 10), + (-3, 5, 29), + (4, 5, 22), + (-4, 6, 57), + (5, 6, 47), + (-5, 6, 46), + (6, 7, 113), + (-6, 7, 112), +]; + +/// Annex D §D.5.7 Table B13. +const TABLE_B13: &[AudioHuffEntry] = &[ + (0, 2, 0), + (1, 3, 6), + (-1, 3, 5), + (2, 3, 2), + (-2, 4, 15), + (3, 4, 9), + (-3, 4, 7), + (4, 4, 6), + (-4, 5, 29), + (5, 5, 17), + (-5, 5, 16), + (6, 6, 57), + (-6, 6, 56), +]; + +/// Annex D §D.5.7 Table C13. +const TABLE_C13: &[AudioHuffEntry] = &[ + (0, 3, 5), + (1, 3, 4), + (-1, 3, 3), + (2, 3, 2), + (-2, 3, 0), + (3, 4, 15), + (-3, 4, 14), + (4, 4, 12), + (-4, 4, 3), + (5, 5, 27), + (-5, 5, 26), + (6, 5, 5), + (-6, 5, 4), +]; + +// --------------------------------------------------------------- +// §D.5.8 — 17 Levels (ABITS 6, SEL 0..6). Staged PDF p.203-205. +// +// Table 5-26 group for ABITS 6 is `A17 B17 C17 D17 E17 F17 G17 V17`, +// so SEL 0..6 select the seven Huffman books and SEL 7 the terminal +// `V17` 4-element block code (`nQType == 3`). Codes reach 12 bits in +// `A17` (the ±8 codes 341/340) — the deepest of any §D.5 family. +// --------------------------------------------------------------- + +/// Annex D §D.5.8 Table A17. +const TABLE_A17: &[AudioHuffEntry] = &[ + (0, 2, 1), + (1, 3, 7), + (-1, 3, 6), + (2, 3, 4), + (-2, 3, 1), + (3, 4, 11), + (-3, 4, 10), + (4, 4, 0), + (-4, 5, 3), + (5, 6, 4), + (-5, 7, 11), + (6, 8, 20), + (-6, 9, 43), + (7, 10, 84), + (-7, 11, 171), + (8, 12, 341), + (-8, 12, 340), +]; + +/// Annex D §D.5.8 Table B17. +const TABLE_B17: &[AudioHuffEntry] = &[ + (0, 2, 0), + (1, 3, 6), + (-1, 3, 5), + (2, 3, 2), + (-2, 4, 15), + (3, 4, 9), + (-3, 4, 8), + (4, 5, 29), + (-4, 5, 28), + (5, 5, 14), + (-5, 5, 13), + (6, 6, 30), + (-6, 6, 25), + (7, 6, 24), + (-7, 7, 63), + (8, 8, 125), + (-8, 8, 124), +]; + +/// Annex D §D.5.8 Table C17. +const TABLE_C17: &[AudioHuffEntry] = &[ + (0, 3, 6), + (1, 3, 4), + (-1, 3, 3), + (2, 3, 0), + (-2, 4, 15), + (3, 4, 11), + (-3, 4, 10), + (4, 4, 4), + (-4, 4, 3), + (5, 5, 29), + (-5, 5, 28), + (6, 5, 10), + (-6, 5, 5), + (7, 5, 4), + (-7, 6, 23), + (8, 7, 45), + (-8, 7, 44), +]; + +/// Annex D §D.5.8 Table D17. +const TABLE_D17: &[AudioHuffEntry] = &[ + (0, 1, 0), + (1, 3, 7), + (-1, 3, 6), + (2, 4, 11), + (-2, 4, 10), + (3, 5, 19), + (-3, 5, 18), + (4, 6, 35), + (-4, 6, 34), + (5, 7, 67), + (-5, 7, 66), + (6, 8, 131), + (-6, 8, 130), + (7, 9, 259), + (-7, 9, 258), + (8, 9, 257), + (-8, 9, 256), +]; + +/// Annex D §D.5.8 Table E17. +const TABLE_E17: &[AudioHuffEntry] = &[ + (0, 1, 0), + (1, 3, 5), + (-1, 3, 4), + (2, 4, 12), + (-2, 5, 31), + (3, 5, 28), + (-3, 5, 27), + (4, 6, 60), + (-4, 6, 59), + (5, 6, 53), + (-5, 6, 52), + (6, 7, 122), + (-6, 7, 117), + (7, 8, 247), + (-7, 8, 246), + (8, 8, 233), + (-8, 8, 232), +]; + +/// Annex D §D.5.8 Table F17. +const TABLE_F17: &[AudioHuffEntry] = &[ + (0, 3, 6), + (1, 3, 5), + (-1, 3, 4), + (2, 3, 2), + (-2, 3, 1), + (3, 4, 15), + (-3, 4, 14), + (4, 4, 6), + (-4, 4, 1), + (5, 5, 14), + (-5, 5, 1), + (6, 6, 31), + (-6, 6, 30), + (7, 6, 0), + (-7, 7, 3), + (8, 8, 5), + (-8, 8, 4), +]; + +/// Annex D §D.5.8 Table G17. +const TABLE_G17: &[AudioHuffEntry] = &[ + (0, 2, 2), + (1, 3, 7), + (-1, 3, 6), + (2, 3, 1), + (-2, 3, 0), + (3, 4, 5), + (-3, 4, 4), + (4, 5, 14), + (-4, 5, 13), + (5, 6, 30), + (-5, 6, 25), + (6, 7, 62), + (-6, 7, 49), + (7, 8, 127), + (-7, 8, 126), + (8, 8, 97), + (-8, 8, 96), +]; + +// --------------------------------------------------------------- +// §D.5.9 — 25 Levels (ABITS 7, SEL 0..6 → A25..G25). Staged PDF +// p.205-208. The mid-tread quantizer carries 25 output levels +// (0, ±1..±12); each book is a complete prefix code. Table A25's ±12 +// codes reach 14 bits (10 325/10 324), the deepest of any audio-data +// Huffman book transcribed in this crate. +// --------------------------------------------------------------- + +/// Annex D §D.5.9 Table A25. +const TABLE_A25: &[AudioHuffEntry] = &[ + (0, 3, 6), + (1, 3, 4), + (-1, 3, 3), + (2, 3, 1), + (-2, 3, 0), + (3, 4, 15), + (-3, 4, 14), + (4, 4, 5), + (-4, 4, 4), + (5, 5, 22), + (-5, 5, 21), + (6, 6, 47), + (-6, 6, 46), + (7, 7, 83), + (-7, 7, 82), + (8, 8, 163), + (-8, 8, 162), + (9, 8, 160), + (-9, 9, 323), + (10, 10, 644), + (-10, 11, 1291), + (11, 12, 2580), + (-11, 13, 5163), + (12, 14, 10325), + (-12, 14, 10324), +]; + +/// Annex D §D.5.9 Table B25. +const TABLE_B25: &[AudioHuffEntry] = &[ + (0, 3, 5), + (1, 3, 2), + (-1, 3, 1), + (2, 4, 15), + (-2, 4, 14), + (3, 4, 9), + (-3, 4, 8), + (4, 4, 6), + (-4, 4, 1), + (5, 5, 26), + (-5, 5, 25), + (6, 5, 15), + (-6, 5, 14), + (7, 6, 55), + (-7, 6, 54), + (8, 6, 49), + (-8, 6, 48), + (9, 6, 1), + (-9, 6, 0), + (10, 7, 6), + (-10, 7, 5), + (11, 7, 4), + (-11, 8, 15), + (12, 9, 29), + (-12, 9, 28), +]; + +/// Annex D §D.5.9 Table C25. +const TABLE_C25: &[AudioHuffEntry] = &[ + (0, 3, 1), + (1, 4, 15), + (-1, 4, 14), + (2, 4, 12), + (-2, 4, 11), + (3, 4, 9), + (-3, 4, 8), + (4, 4, 6), + (-4, 4, 5), + (5, 4, 1), + (-5, 4, 0), + (6, 5, 26), + (-6, 5, 21), + (7, 5, 15), + (-7, 5, 14), + (8, 5, 8), + (-8, 6, 55), + (9, 6, 41), + (-9, 6, 40), + (10, 6, 18), + (-10, 7, 109), + (11, 7, 108), + (-11, 7, 39), + (12, 8, 77), + (-12, 8, 76), +]; + +/// Annex D §D.5.9 Table D25. +const TABLE_D25: &[AudioHuffEntry] = &[ + (0, 2, 2), + (1, 3, 7), + (-1, 3, 6), + (2, 3, 1), + (-2, 3, 0), + (3, 4, 5), + (-3, 4, 4), + (4, 5, 13), + (-4, 5, 12), + (5, 6, 29), + (-5, 6, 28), + (6, 7, 62), + (-6, 7, 61), + (7, 8, 126), + (-7, 8, 121), + (8, 9, 255), + (-8, 9, 254), + (9, 10, 483), + (-9, 10, 482), + (10, 11, 963), + (-10, 11, 962), + (11, 12, 1923), + (-11, 12, 1922), + (12, 12, 1921), + (-12, 12, 1920), +]; + +/// Annex D §D.5.9 Table E25. +const TABLE_E25: &[AudioHuffEntry] = &[ + (0, 2, 3), + (1, 3, 3), + (-1, 3, 2), + (2, 4, 11), + (-2, 4, 10), + (3, 4, 1), + (-3, 4, 0), + (4, 5, 17), + (-4, 5, 16), + (5, 5, 5), + (-5, 5, 4), + (6, 6, 38), + (-6, 6, 37), + (7, 6, 14), + (-7, 6, 13), + (8, 7, 79), + (-8, 7, 78), + (9, 7, 72), + (-9, 7, 31), + (10, 7, 25), + (-10, 7, 24), + (11, 8, 147), + (-11, 8, 146), + (12, 8, 61), + (-12, 8, 60), +]; + +/// Annex D §D.5.9 Table F25. +const TABLE_F25: &[AudioHuffEntry] = &[ + (0, 3, 1), + (1, 3, 0), + (-1, 4, 15), + (2, 4, 14), + (-2, 4, 13), + (3, 4, 11), + (-3, 4, 10), + (4, 4, 8), + (-4, 4, 7), + (5, 4, 5), + (-5, 4, 4), + (6, 5, 24), + (-6, 5, 19), + (7, 5, 13), + (-7, 5, 12), + (8, 6, 37), + (-8, 6, 36), + (9, 7, 102), + (-9, 7, 101), + (10, 8, 207), + (-10, 8, 206), + (11, 8, 200), + (-11, 9, 403), + (12, 10, 805), + (-12, 10, 804), +]; + +/// Annex D §D.5.9 Table G25. +const TABLE_G25: &[AudioHuffEntry] = &[ + (0, 2, 1), + (1, 3, 6), + (-1, 3, 5), + (2, 3, 0), + (-2, 4, 15), + (3, 4, 8), + (-3, 4, 3), + (4, 5, 28), + (-4, 5, 19), + (5, 5, 4), + (-5, 6, 59), + (6, 6, 36), + (-6, 6, 11), + (7, 7, 116), + (-7, 7, 75), + (8, 7, 21), + (-8, 7, 20), + (9, 8, 149), + (-9, 8, 148), + (10, 9, 470), + (-10, 9, 469), + (11, 10, 943), + (-11, 10, 942), + (12, 10, 937), + (-12, 10, 936), +]; + +// --------------------------------------------------------------- +// §D.5.10 — 33 Levels (ABITS 8, SEL 0..6 → A33..G33). Staged PDF +// p.208-211. The mid-tread quantizer carries 33 output levels +// (0, ±1..±16); each book is a complete prefix code (Kraft sum 1). +// The Table 5-26 group for ABITS 8 is `A33 B33 C33 D33 E33 F33 G33 +// NFE`, so SEL 0..6 select these Huffman books and SEL 7 the terminal +// "no further encoding" fixed-width field (nQType == 2), not a block +// code. The deepest codeword is 14 bits (Table D33 ±15/±16). +// --------------------------------------------------------------- +/// Annex D §D.5.10 Table A33. +const TABLE_A33: &[AudioHuffEntry] = &[ + (0, 3, 2), + (1, 3, 1), + (-1, 3, 0), + (2, 4, 14), + (-2, 4, 13), + (3, 4, 12), + (-3, 4, 11), + (4, 4, 9), + (-4, 4, 8), + (5, 4, 6), + (-5, 5, 31), + (6, 5, 20), + (-6, 5, 15), + (7, 6, 61), + (-7, 6, 60), + (8, 6, 29), + (-8, 6, 28), + (9, 7, 85), + (-9, 7, 84), + (10, 8, 174), + (-10, 8, 173), + (11, 9, 351), + (-11, 9, 350), + (12, 10, 691), + (-12, 10, 690), + (13, 11, 1379), + (-13, 11, 1378), + (14, 12, 2755), + (-14, 12, 2754), + (15, 13, 5507), + (-15, 13, 5506), + (16, 13, 5505), + (-16, 13, 5504), +]; + +/// Annex D §D.5.10 Table B33. +const TABLE_B33: &[AudioHuffEntry] = &[ + (0, 3, 1), + (1, 4, 15), + (-1, 4, 14), + (2, 4, 11), + (-2, 4, 10), + (3, 4, 8), + (-3, 4, 7), + (4, 4, 4), + (-4, 4, 1), + (5, 5, 27), + (-5, 5, 26), + (6, 5, 19), + (-6, 5, 18), + (7, 5, 12), + (-7, 5, 11), + (8, 5, 1), + (-8, 5, 0), + (9, 6, 50), + (-9, 6, 49), + (10, 6, 26), + (-10, 6, 21), + (11, 7, 103), + (-11, 7, 102), + (12, 7, 96), + (-12, 7, 55), + (13, 7, 41), + (-13, 7, 40), + (14, 8, 194), + (-14, 8, 109), + (15, 8, 108), + (-15, 9, 391), + (16, 10, 781), + (-16, 10, 780), +]; + +/// Annex D §D.5.10 Table C33. +const TABLE_C33: &[AudioHuffEntry] = &[ + (0, 4, 13), + (1, 4, 11), + (-1, 4, 10), + (2, 4, 8), + (-2, 4, 7), + (3, 4, 4), + (-3, 4, 3), + (4, 4, 2), + (-4, 4, 1), + (5, 5, 30), + (-5, 5, 29), + (6, 5, 25), + (-6, 5, 24), + (7, 5, 19), + (-7, 5, 18), + (8, 5, 11), + (-8, 5, 10), + (9, 5, 0), + (-9, 6, 63), + (10, 6, 62), + (-10, 6, 57), + (11, 6, 27), + (-11, 6, 26), + (12, 6, 24), + (-12, 6, 3), + (13, 7, 113), + (-13, 7, 112), + (14, 7, 50), + (-14, 7, 5), + (15, 7, 4), + (-15, 8, 103), + (16, 9, 205), + (-16, 9, 204), +]; + +/// Annex D §D.5.10 Table D33. +const TABLE_D33: &[AudioHuffEntry] = &[ + (0, 2, 1), + (1, 3, 6), + (-1, 3, 5), + (2, 3, 0), + (-2, 4, 15), + (3, 4, 8), + (-3, 4, 3), + (4, 5, 28), + (-4, 5, 19), + (5, 5, 4), + (-5, 6, 59), + (6, 6, 36), + (-6, 6, 11), + (7, 7, 116), + (-7, 7, 75), + (8, 7, 21), + (-8, 7, 20), + (9, 8, 149), + (-9, 8, 148), + (10, 9, 469), + (-10, 9, 468), + (11, 10, 941), + (-11, 10, 940), + (12, 11, 1885), + (-12, 11, 1884), + (13, 12, 3773), + (-13, 12, 3772), + (14, 13, 7551), + (-14, 13, 7550), + (15, 14, 15099), + (-15, 14, 15098), + (16, 14, 15097), + (-16, 14, 15096), +]; + +/// Annex D §D.5.10 Table E33. +const TABLE_E33: &[AudioHuffEntry] = &[ + (0, 2, 2), + (1, 3, 2), + (-1, 3, 1), + (2, 4, 12), + (-2, 4, 7), + (3, 4, 0), + (-3, 5, 31), + (4, 5, 27), + (-4, 5, 26), + (5, 5, 3), + (-5, 5, 2), + (6, 6, 59), + (-6, 6, 58), + (7, 6, 27), + (-7, 6, 26), + (8, 7, 123), + (-8, 7, 122), + (9, 7, 120), + (-9, 7, 115), + (10, 7, 112), + (-10, 7, 51), + (11, 7, 49), + (-11, 7, 48), + (12, 8, 242), + (-12, 8, 229), + (13, 8, 227), + (-13, 8, 226), + (14, 8, 101), + (-14, 8, 100), + (15, 9, 487), + (-15, 9, 486), + (16, 9, 457), + (-16, 9, 456), +]; + +/// Annex D §D.5.10 Table F33. +const TABLE_F33: &[AudioHuffEntry] = &[ + (0, 4, 13), + (1, 4, 12), + (-1, 4, 11), + (2, 4, 9), + (-2, 4, 8), + (3, 4, 7), + (-3, 4, 6), + (4, 4, 4), + (-4, 4, 3), + (5, 4, 1), + (-5, 4, 0), + (6, 5, 30), + (-6, 5, 29), + (7, 5, 21), + (-7, 5, 20), + (8, 5, 10), + (-8, 5, 5), + (9, 6, 63), + (-9, 6, 62), + (10, 6, 56), + (-10, 6, 23), + (11, 6, 9), + (-11, 6, 8), + (12, 7, 45), + (-12, 7, 44), + (13, 8, 230), + (-13, 8, 229), + (14, 9, 463), + (-14, 9, 462), + (15, 9, 456), + (-15, 10, 915), + (16, 11, 1829), + (-16, 11, 1828), +]; + +/// Annex D §D.5.10 Table G33. +const TABLE_G33: &[AudioHuffEntry] = &[ + (0, 3, 6), + (1, 3, 3), + (-1, 3, 2), + (2, 4, 15), + (-2, 4, 14), + (3, 4, 9), + (-3, 4, 8), + (4, 4, 1), + (-4, 4, 0), + (5, 5, 22), + (-5, 5, 21), + (6, 5, 6), + (-6, 5, 5), + (7, 6, 46), + (-7, 6, 41), + (8, 6, 14), + (-8, 6, 9), + (9, 7, 94), + (-9, 7, 81), + (10, 7, 30), + (-10, 7, 17), + (11, 8, 191), + (-11, 8, 190), + (12, 8, 63), + (-12, 8, 62), + (13, 8, 32), + (-13, 9, 323), + (14, 9, 321), + (-14, 9, 320), + (15, 9, 67), + (-15, 9, 66), + (16, 10, 645), + (-16, 10, 644), +]; + +// --------------------------------------------------------------- +// §D.5.11 — 65 Levels (ABITS 9, SEL 0..6 → A65..G65). Staged PDF +// p.212-218. The mid-tread quantizer carries 65 output levels +// (0, ±1..±32); each book is a complete prefix code (Kraft sum 1). +// Table 5-26 group for ABITS 9 is `A65 B65 C65 D65 E65 F65 G65 NFE` +// (SEL 0..6 Huffman, SEL 7 NFE). The deepest codeword is 16 bits. +// --------------------------------------------------------------- +/// Annex D §D.5.11 Table A65. +const TABLE_A65: &[AudioHuffEntry] = &[ + (0, 4, 6), + (1, 4, 5), + (-1, 4, 4), + (2, 4, 2), + (-2, 4, 1), + (3, 4, 0), + (-3, 5, 31), + (4, 5, 29), + (-4, 5, 28), + (5, 5, 27), + (-5, 5, 26), + (6, 5, 24), + (-6, 5, 23), + (7, 5, 21), + (-7, 5, 20), + (8, 5, 18), + (-8, 5, 17), + (9, 5, 14), + (-9, 5, 7), + (10, 5, 6), + (-10, 6, 61), + (11, 6, 50), + (-11, 6, 45), + (12, 6, 38), + (-12, 6, 33), + (13, 6, 31), + (-13, 6, 30), + (14, 7, 120), + (-14, 7, 103), + (15, 7, 89), + (-15, 7, 88), + (16, 7, 65), + (-16, 7, 64), + (17, 8, 205), + (-17, 8, 204), + (18, 8, 157), + (-18, 8, 156), + (19, 9, 486), + (-19, 9, 485), + (20, 9, 318), + (-20, 9, 317), + (21, 10, 975), + (-21, 10, 974), + (22, 10, 639), + (-22, 10, 638), + (23, 11, 1939), + (-23, 11, 1938), + (24, 11, 1936), + (-24, 11, 1267), + (25, 11, 1264), + (-25, 12, 3875), + (26, 12, 2532), + (-26, 12, 2531), + (27, 13, 7749), + (-27, 13, 7748), + (28, 13, 5061), + (-28, 13, 5060), + (29, 14, 10133), + (-29, 14, 10132), + (30, 15, 20269), + (-30, 15, 20268), + (31, 16, 40543), + (-31, 16, 40542), + (32, 16, 40541), + (-32, 16, 40540), +]; + +/// Annex D §D.5.11 Table B65. +const TABLE_B65: &[AudioHuffEntry] = &[ + (0, 4, 4), + (1, 4, 2), + (-1, 4, 1), + (2, 5, 30), + (-2, 5, 29), + (3, 5, 26), + (-3, 5, 25), + (4, 5, 23), + (-4, 5, 22), + (5, 5, 19), + (-5, 5, 18), + (6, 5, 16), + (-6, 5, 15), + (7, 5, 12), + (-7, 5, 11), + (8, 5, 7), + (-8, 5, 6), + (9, 6, 63), + (-9, 6, 62), + (10, 6, 56), + (-10, 6, 55), + (11, 6, 49), + (-11, 6, 48), + (12, 6, 41), + (-12, 6, 40), + (13, 6, 34), + (-13, 6, 29), + (14, 6, 26), + (-14, 6, 21), + (15, 6, 20), + (-15, 6, 3), + (16, 6, 0), + (-16, 7, 115), + (17, 7, 109), + (-17, 7, 108), + (18, 7, 86), + (-18, 7, 85), + (19, 7, 70), + (-19, 7, 57), + (20, 7, 56), + (-20, 7, 55), + (21, 7, 4), + (-21, 7, 3), + (22, 8, 229), + (-22, 8, 228), + (23, 8, 175), + (-23, 8, 174), + (24, 8, 143), + (-24, 8, 142), + (25, 8, 108), + (-25, 8, 11), + (26, 8, 10), + (-26, 8, 5), + (27, 9, 339), + (-27, 9, 338), + (28, 9, 336), + (-28, 9, 219), + (29, 9, 9), + (-29, 9, 8), + (30, 10, 674), + (-30, 10, 437), + (31, 10, 436), + (-31, 11, 1351), + (32, 12, 2701), + (-32, 12, 2700), +]; + +/// Annex D §D.5.11 Table C65. +const TABLE_C65: &[AudioHuffEntry] = &[ + (0, 5, 28), + (1, 5, 25), + (-1, 5, 24), + (2, 5, 23), + (-2, 5, 22), + (3, 5, 19), + (-3, 5, 18), + (4, 5, 16), + (-4, 5, 15), + (5, 5, 13), + (-5, 5, 12), + (6, 5, 10), + (-6, 5, 9), + (7, 5, 7), + (-7, 5, 6), + (8, 5, 4), + (-8, 5, 3), + (9, 5, 1), + (-9, 5, 0), + (10, 6, 62), + (-10, 6, 61), + (11, 6, 59), + (-11, 6, 58), + (12, 6, 54), + (-12, 6, 53), + (13, 6, 43), + (-13, 6, 42), + (14, 6, 40), + (-14, 6, 35), + (15, 6, 29), + (-15, 6, 28), + (16, 6, 17), + (-16, 6, 16), + (17, 6, 11), + (-17, 6, 10), + (18, 6, 4), + (-18, 7, 127), + (19, 7, 121), + (-19, 7, 120), + (20, 7, 110), + (-20, 7, 105), + (21, 7, 83), + (-21, 7, 82), + (22, 7, 68), + (-22, 7, 47), + (23, 7, 46), + (-23, 7, 45), + (24, 7, 11), + (-24, 7, 10), + (25, 8, 252), + (-25, 8, 223), + (26, 8, 209), + (-26, 8, 208), + (27, 8, 138), + (-27, 8, 89), + (28, 8, 88), + (-28, 9, 507), + (29, 9, 445), + (-29, 9, 444), + (30, 9, 278), + (-30, 10, 1013), + (31, 10, 1012), + (-31, 10, 559), + (32, 11, 1117), + (-32, 11, 1116), +]; + +/// Annex D §D.5.11 Table D65. +const TABLE_D65: &[AudioHuffEntry] = &[ + (0, 3, 4), + (1, 3, 1), + (-1, 3, 0), + (2, 4, 13), + (-2, 4, 12), + (3, 4, 7), + (-3, 4, 6), + (4, 5, 31), + (-4, 5, 30), + (5, 5, 23), + (-5, 5, 22), + (6, 5, 11), + (-6, 5, 10), + (7, 6, 59), + (-7, 6, 58), + (8, 6, 43), + (-8, 6, 42), + (9, 6, 19), + (-9, 6, 18), + (10, 7, 115), + (-10, 7, 114), + (11, 7, 83), + (-11, 7, 82), + (12, 7, 35), + (-12, 7, 34), + (13, 8, 227), + (-13, 8, 226), + (14, 8, 163), + (-14, 8, 162), + (15, 8, 160), + (-15, 8, 67), + (16, 8, 64), + (-16, 9, 451), + (17, 9, 448), + (-17, 9, 323), + (18, 9, 132), + (-18, 9, 131), + (19, 10, 900), + (-19, 10, 899), + (20, 10, 644), + (-20, 10, 267), + (21, 10, 261), + (-21, 10, 260), + (22, 11, 1797), + (-22, 11, 1796), + (23, 11, 533), + (-23, 11, 532), + (24, 12, 3605), + (-24, 12, 3604), + (25, 12, 2582), + (-25, 12, 2581), + (26, 13, 7215), + (-26, 13, 7214), + (27, 13, 5167), + (-27, 13, 5166), + (28, 13, 5160), + (-28, 14, 14427), + (29, 14, 10323), + (-29, 14, 10322), + (30, 15, 28853), + (-30, 15, 28852), + (31, 15, 28851), + (-31, 15, 28850), + (32, 15, 28849), + (-32, 15, 28848), +]; + +/// Annex D §D.5.11 Table E65. +const TABLE_E65: &[AudioHuffEntry] = &[ + (0, 3, 4), + (1, 3, 0), + (-1, 4, 15), + (2, 4, 7), + (-2, 4, 6), + (3, 5, 29), + (-3, 5, 28), + (4, 5, 23), + (-4, 5, 22), + (5, 5, 10), + (-5, 5, 9), + (6, 5, 6), + (-6, 5, 5), + (7, 6, 54), + (-7, 6, 53), + (8, 6, 48), + (-8, 6, 43), + (9, 6, 40), + (-9, 6, 23), + (10, 6, 16), + (-10, 6, 15), + (11, 6, 9), + (-11, 6, 8), + (12, 7, 105), + (-12, 7, 104), + (13, 7, 100), + (-13, 7, 99), + (14, 7, 84), + (-14, 7, 83), + (15, 7, 45), + (-15, 7, 44), + (16, 7, 29), + (-16, 7, 28), + (17, 8, 221), + (-17, 8, 220), + (18, 8, 206), + (-18, 8, 205), + (19, 8, 202), + (-19, 8, 197), + (20, 8, 171), + (-20, 8, 170), + (21, 8, 164), + (-21, 8, 71), + (22, 8, 69), + (-22, 8, 68), + (23, 9, 446), + (-23, 9, 445), + (24, 9, 415), + (-24, 9, 414), + (25, 9, 408), + (-25, 9, 407), + (26, 9, 393), + (-26, 9, 392), + (27, 9, 331), + (-27, 9, 330), + (28, 9, 141), + (-28, 9, 140), + (29, 10, 895), + (-29, 10, 894), + (30, 10, 889), + (-30, 10, 888), + (31, 10, 819), + (-31, 10, 818), + (32, 10, 813), + (-32, 10, 812), +]; + +/// Annex D §D.5.11 Table F65. +const TABLE_F65: &[AudioHuffEntry] = &[ + (0, 3, 6), + (1, 3, 3), + (-1, 3, 2), + (2, 4, 15), + (-2, 4, 14), + (3, 4, 9), + (-3, 4, 8), + (4, 4, 1), + (-4, 4, 0), + (5, 5, 21), + (-5, 5, 20), + (6, 5, 5), + (-6, 5, 4), + (7, 6, 45), + (-7, 6, 44), + (8, 6, 13), + (-8, 6, 12), + (9, 7, 93), + (-9, 7, 92), + (10, 7, 29), + (-10, 7, 28), + (11, 8, 189), + (-11, 8, 188), + (12, 8, 61), + (-12, 8, 60), + (13, 9, 381), + (-13, 9, 380), + (14, 9, 125), + (-14, 9, 124), + (15, 10, 765), + (-15, 10, 764), + (16, 10, 252), + (-16, 11, 1535), + (17, 11, 1532), + (-17, 11, 511), + (18, 11, 506), + (-18, 12, 3069), + (19, 12, 3067), + (-19, 12, 3066), + (20, 12, 1015), + (-20, 12, 1014), + (21, 13, 6136), + (-21, 13, 2043), + (22, 13, 2035), + (-22, 13, 2034), + (23, 14, 12275), + (-23, 14, 12274), + (24, 14, 4085), + (-24, 14, 4084), + (25, 14, 4083), + (-25, 14, 4082), + (26, 14, 4081), + (-26, 14, 4080), + (27, 14, 4079), + (-27, 14, 4078), + (28, 14, 4077), + (-28, 14, 4076), + (29, 14, 4075), + (-29, 14, 4074), + (30, 14, 4073), + (-30, 14, 4072), + (31, 14, 4067), + (-31, 14, 4066), + (32, 14, 4065), + (-32, 14, 4064), +]; + +/// Annex D §D.5.11 Table G65. +const TABLE_G65: &[AudioHuffEntry] = &[ + (0, 4, 14), + (1, 4, 11), + (-1, 4, 10), + (2, 4, 8), + (-2, 4, 6), + (3, 4, 4), + (-3, 4, 3), + (4, 4, 0), + (-4, 5, 31), + (5, 5, 26), + (-5, 5, 25), + (6, 5, 18), + (-6, 5, 15), + (7, 5, 10), + (-7, 5, 5), + (8, 5, 2), + (-8, 6, 61), + (9, 6, 54), + (-9, 6, 49), + (10, 6, 38), + (-10, 6, 29), + (11, 6, 22), + (-11, 6, 9), + (12, 6, 6), + (-12, 7, 121), + (13, 7, 110), + (-13, 7, 97), + (14, 7, 78), + (-14, 7, 57), + (15, 7, 46), + (-15, 7, 17), + (16, 7, 14), + (-16, 8, 241), + (17, 8, 223), + (-17, 8, 222), + (18, 8, 159), + (-18, 8, 158), + (19, 8, 95), + (-19, 8, 94), + (20, 8, 31), + (-20, 8, 30), + (21, 9, 480), + (-21, 9, 387), + (22, 9, 384), + (-22, 9, 227), + (23, 9, 225), + (-23, 9, 224), + (24, 9, 65), + (-24, 9, 64), + (25, 10, 962), + (-25, 10, 773), + (26, 10, 771), + (-26, 10, 770), + (27, 10, 452), + (-27, 10, 135), + (28, 10, 133), + (-28, 10, 132), + (29, 11, 1927), + (-29, 11, 1926), + (30, 11, 1545), + (-30, 11, 1544), + (31, 11, 907), + (-31, 11, 906), + (32, 11, 269), + (-32, 11, 268), +]; + +// --------------------------------------------------------------- +// §D.5.12 — 129 Levels (ABITS 10, SEL 0..6 → A129..G129). Staged PDF +// p.224-230. The mid-tread quantizer carries 129 output levels +// (0, ±1..±64); each book is a complete prefix code (Kraft sum 1). +// Table 5-26 group for ABITS 10 is `A129 B129 C129 D129 E129 F129 +// G129 NFE` (SEL 0..6 Huffman, SEL 7 NFE). The deepest codeword is +// 16 bits (e.g. Table D129 ±64 at 16 bits). +// --------------------------------------------------------------- +/// Annex D §D.5.12 Table A129. +const TABLE_A129: &[AudioHuffEntry] = &[ + (0, 4, 8), + (1, 4, 10), + (-1, 4, 9), + (2, 4, 0), + (-2, 5, 31), + (3, 5, 24), + (-3, 5, 23), + (4, 5, 12), + (-4, 5, 11), + (5, 5, 5), + (-5, 5, 4), + (6, 6, 60), + (-6, 6, 58), + (7, 6, 54), + (-7, 6, 53), + (8, 6, 45), + (-8, 6, 44), + (9, 6, 28), + (-9, 6, 27), + (10, 6, 19), + (-10, 6, 18), + (11, 6, 14), + (-11, 6, 13), + (12, 6, 6), + (-12, 6, 5), + (13, 7, 122), + (-13, 7, 119), + (14, 7, 113), + (-14, 7, 112), + (15, 7, 104), + (-15, 7, 103), + (16, 7, 100), + (-16, 7, 63), + (17, 7, 60), + (-17, 7, 59), + (18, 7, 52), + (-18, 7, 43), + (19, 7, 40), + (-19, 7, 35), + (20, 7, 32), + (-20, 7, 31), + (21, 7, 15), + (-21, 7, 14), + (22, 8, 247), + (-22, 8, 246), + (23, 8, 231), + (-23, 8, 230), + (24, 8, 223), + (-24, 8, 222), + (25, 8, 211), + (-25, 8, 210), + (26, 8, 203), + (-26, 8, 202), + (27, 8, 123), + (-27, 8, 122), + (28, 8, 116), + (-28, 8, 107), + (29, 8, 84), + (-29, 8, 83), + (30, 8, 68), + (-30, 8, 67), + (31, 8, 60), + (-31, 8, 51), + (32, 8, 49), + (-32, 8, 48), + (33, 8, 17), + (-33, 8, 16), + (34, 9, 474), + (-34, 9, 473), + (35, 9, 458), + (-35, 9, 457), + (36, 9, 442), + (-36, 9, 441), + (37, 9, 411), + (-37, 9, 410), + (38, 9, 251), + (-38, 9, 250), + (39, 9, 248), + (-39, 9, 235), + (40, 9, 213), + (-40, 9, 212), + (41, 9, 170), + (-41, 9, 165), + (42, 9, 139), + (-42, 9, 138), + (43, 9, 132), + (-43, 9, 123), + (44, 9, 101), + (-44, 9, 100), + (45, 9, 37), + (-45, 9, 36), + (46, 10, 950), + (-46, 10, 945), + (47, 10, 919), + (-47, 10, 918), + (48, 10, 912), + (-48, 10, 887), + (49, 10, 881), + (-49, 10, 880), + (50, 10, 818), + (-50, 10, 817), + (51, 10, 499), + (-51, 10, 498), + (52, 10, 469), + (-52, 10, 468), + (53, 10, 343), + (-53, 10, 342), + (54, 10, 329), + (-54, 10, 328), + (55, 10, 267), + (-55, 10, 266), + (56, 10, 245), + (-56, 10, 244), + (57, 10, 79), + (-57, 10, 78), + (58, 10, 77), + (-58, 10, 76), + (59, 11, 1903), + (-59, 11, 1902), + (60, 11, 1889), + (-60, 11, 1888), + (61, 11, 1827), + (-61, 11, 1826), + (62, 11, 1773), + (-62, 11, 1772), + (63, 11, 1639), + (-63, 11, 1638), + (64, 11, 1633), + (-64, 11, 1632), +]; + +/// Annex D §D.5.12 Table B129. +const TABLE_B129: &[AudioHuffEntry] = &[ + (0, 5, 10), + (1, 5, 7), + (-1, 5, 6), + (2, 5, 4), + (-2, 5, 3), + (3, 5, 0), + (-3, 6, 63), + (4, 6, 60), + (-4, 6, 59), + (5, 6, 57), + (-5, 6, 56), + (6, 6, 53), + (-6, 6, 52), + (7, 6, 50), + (-7, 6, 49), + (8, 6, 46), + (-8, 6, 45), + (9, 6, 43), + (-9, 6, 42), + (10, 6, 39), + (-10, 6, 38), + (11, 6, 35), + (-11, 6, 34), + (12, 6, 32), + (-12, 6, 31), + (13, 6, 28), + (-13, 6, 27), + (14, 6, 25), + (-14, 6, 24), + (15, 6, 22), + (-15, 6, 19), + (16, 6, 16), + (-16, 6, 11), + (17, 6, 5), + (-17, 6, 4), + (18, 7, 125), + (-18, 7, 124), + (19, 7, 122), + (-19, 7, 117), + (20, 7, 110), + (-20, 7, 109), + (21, 7, 103), + (-21, 7, 102), + (22, 7, 96), + (-22, 7, 95), + (23, 7, 89), + (-23, 7, 88), + (24, 7, 81), + (-24, 7, 80), + (25, 7, 74), + (-25, 7, 73), + (26, 7, 66), + (-26, 7, 61), + (27, 7, 59), + (-27, 7, 58), + (28, 7, 52), + (-28, 7, 47), + (29, 7, 37), + (-29, 7, 36), + (30, 7, 21), + (-30, 7, 20), + (31, 7, 6), + (-31, 7, 5), + (32, 8, 247), + (-32, 8, 246), + (33, 8, 223), + (-33, 8, 222), + (34, 8, 217), + (-34, 8, 216), + (35, 8, 189), + (-35, 8, 188), + (36, 8, 166), + (-36, 8, 165), + (37, 8, 151), + (-37, 8, 150), + (38, 8, 144), + (-38, 8, 135), + (39, 8, 121), + (-39, 8, 120), + (40, 8, 106), + (-40, 8, 93), + (41, 8, 71), + (-41, 8, 70), + (42, 8, 68), + (-42, 8, 15), + (43, 8, 9), + (-43, 8, 8), + (44, 9, 466), + (-44, 9, 465), + (45, 9, 391), + (-45, 9, 390), + (46, 9, 388), + (-46, 9, 335), + (47, 9, 329), + (-47, 9, 328), + (48, 9, 269), + (-48, 9, 268), + (49, 9, 215), + (-49, 9, 214), + (50, 9, 184), + (-50, 9, 139), + (51, 9, 29), + (-51, 9, 28), + (52, 10, 934), + (-52, 10, 929), + (53, 10, 779), + (-53, 10, 778), + (54, 10, 668), + (-54, 10, 583), + (55, 10, 582), + (-55, 10, 581), + (56, 10, 371), + (-56, 10, 370), + (57, 10, 276), + (-57, 11, 1871), + (58, 11, 1857), + (-58, 11, 1856), + (59, 11, 1338), + (-59, 11, 1161), + (60, 11, 1160), + (-60, 11, 555), + (61, 12, 3741), + (-61, 12, 3740), + (62, 12, 2678), + (-62, 12, 1109), + (63, 12, 1108), + (-63, 13, 5359), + (64, 14, 10717), + (-64, 14, 10716), +]; + +/// Annex D §D.5.12 Table C129. +const TABLE_C129: &[AudioHuffEntry] = &[ + (0, 6, 58), + (1, 6, 55), + (-1, 6, 54), + (2, 6, 52), + (-2, 6, 51), + (3, 6, 49), + (-3, 6, 48), + (4, 6, 46), + (-4, 6, 45), + (5, 6, 43), + (-5, 6, 42), + (6, 6, 40), + (-6, 6, 39), + (7, 6, 37), + (-7, 6, 36), + (8, 6, 34), + (-8, 6, 33), + (9, 6, 30), + (-9, 6, 29), + (10, 6, 27), + (-10, 6, 26), + (11, 6, 24), + (-11, 6, 23), + (12, 6, 21), + (-12, 6, 20), + (13, 6, 18), + (-13, 6, 17), + (14, 6, 14), + (-14, 6, 13), + (15, 6, 12), + (-15, 6, 11), + (16, 6, 8), + (-16, 6, 7), + (17, 6, 6), + (-17, 6, 5), + (18, 6, 3), + (-18, 6, 2), + (19, 7, 127), + (-19, 7, 126), + (20, 7, 124), + (-20, 7, 123), + (21, 7, 121), + (-21, 7, 120), + (22, 7, 118), + (-22, 7, 115), + (23, 7, 113), + (-23, 7, 112), + (24, 7, 106), + (-24, 7, 101), + (25, 7, 95), + (-25, 7, 94), + (26, 7, 88), + (-26, 7, 83), + (27, 7, 77), + (-27, 7, 76), + (28, 7, 70), + (-28, 7, 65), + (29, 7, 64), + (-29, 7, 63), + (30, 7, 56), + (-30, 7, 51), + (31, 7, 45), + (-31, 7, 44), + (32, 7, 39), + (-32, 7, 38), + (33, 7, 31), + (-33, 7, 30), + (34, 7, 20), + (-34, 7, 19), + (35, 7, 18), + (-35, 7, 9), + (36, 7, 3), + (-36, 7, 2), + (37, 7, 0), + (-37, 8, 251), + (38, 8, 245), + (-38, 8, 244), + (39, 8, 238), + (-39, 8, 229), + (40, 8, 215), + (-40, 8, 214), + (41, 8, 200), + (-41, 8, 179), + (42, 8, 165), + (-42, 8, 164), + (43, 8, 143), + (-43, 8, 142), + (44, 8, 124), + (-44, 8, 115), + (45, 8, 101), + (-45, 8, 100), + (46, 8, 66), + (-46, 8, 65), + (47, 8, 43), + (-47, 8, 42), + (48, 8, 17), + (-48, 8, 16), + (49, 8, 2), + (-49, 9, 501), + (50, 9, 479), + (-50, 9, 478), + (51, 9, 456), + (-51, 9, 403), + (52, 9, 357), + (-52, 9, 356), + (53, 9, 251), + (-53, 9, 250), + (54, 9, 228), + (-54, 9, 135), + (55, 9, 129), + (-55, 9, 128), + (56, 9, 6), + (-56, 10, 1001), + (57, 10, 1000), + (-57, 10, 915), + (58, 10, 805), + (-58, 10, 804), + (59, 10, 458), + (-59, 10, 269), + (60, 10, 268), + (-60, 10, 15), + (61, 11, 1829), + (-61, 11, 1828), + (62, 11, 918), + (-62, 11, 29), + (63, 11, 28), + (-63, 12, 1839), + (64, 13, 3677), + (-64, 13, 3676), +]; + +/// Annex D §D.5.12 Table D129. +const TABLE_D129: &[AudioHuffEntry] = &[ + (0, 4, 9), + (1, 4, 6), + (-1, 4, 5), + (2, 4, 2), + (-2, 4, 1), + (3, 5, 30), + (-3, 5, 29), + (4, 5, 26), + (-4, 5, 25), + (5, 5, 22), + (-5, 5, 21), + (6, 5, 16), + (-6, 5, 15), + (7, 5, 8), + (-7, 5, 7), + (8, 5, 0), + (-8, 6, 63), + (9, 6, 56), + (-9, 6, 55), + (10, 6, 48), + (-10, 6, 47), + (11, 6, 40), + (-11, 6, 35), + (12, 6, 28), + (-12, 6, 19), + (13, 6, 12), + (-13, 6, 3), + (14, 7, 124), + (-14, 7, 115), + (15, 7, 108), + (-15, 7, 99), + (16, 7, 92), + (-16, 7, 83), + (17, 7, 68), + (-17, 7, 59), + (18, 7, 36), + (-18, 7, 27), + (19, 7, 4), + (-19, 8, 251), + (20, 8, 228), + (-20, 8, 219), + (21, 8, 196), + (-21, 8, 187), + (22, 8, 164), + (-22, 8, 139), + (23, 8, 116), + (-23, 8, 75), + (24, 8, 52), + (-24, 8, 11), + (25, 9, 501), + (-25, 9, 500), + (26, 9, 437), + (-26, 9, 436), + (27, 9, 373), + (-27, 9, 372), + (28, 9, 277), + (-28, 9, 276), + (29, 9, 149), + (-29, 9, 148), + (30, 9, 21), + (-30, 9, 20), + (31, 10, 917), + (-31, 10, 916), + (32, 10, 789), + (-32, 10, 788), + (33, 10, 661), + (-33, 10, 660), + (34, 10, 469), + (-34, 10, 468), + (35, 10, 214), + (-35, 10, 213), + (36, 11, 1838), + (-36, 11, 1837), + (37, 11, 1582), + (-37, 11, 1581), + (38, 11, 1326), + (-38, 11, 1325), + (39, 11, 942), + (-39, 11, 941), + (40, 11, 431), + (-40, 11, 430), + (41, 12, 3679), + (-41, 12, 3678), + (42, 12, 3167), + (-42, 12, 3166), + (43, 12, 3160), + (-43, 12, 2655), + (44, 12, 2648), + (-44, 12, 1887), + (45, 12, 1880), + (-45, 12, 851), + (46, 12, 849), + (-46, 12, 848), + (47, 13, 7346), + (-47, 13, 7345), + (48, 13, 6322), + (-48, 13, 5309), + (49, 13, 3773), + (-49, 13, 3772), + (50, 13, 3762), + (-50, 13, 1701), + (51, 14, 14695), + (-51, 14, 14694), + (52, 14, 14688), + (-52, 14, 12647), + (53, 14, 10617), + (-53, 14, 10616), + (54, 14, 10596), + (-54, 14, 7527), + (55, 14, 3401), + (-55, 14, 3400), + (56, 15, 29378), + (-56, 15, 25293), + (57, 15, 21195), + (-57, 15, 21194), + (58, 15, 15053), + (-58, 15, 15052), + (59, 16, 58759), + (-59, 16, 58758), + (60, 16, 50585), + (-60, 16, 50584), + (61, 16, 42399), + (-61, 16, 42398), + (62, 16, 42397), + (-62, 16, 42396), + (63, 16, 42395), + (-63, 16, 42394), + (64, 16, 42393), + (-64, 16, 42392), +]; + +/// Annex D §D.5.12 Table E129. +const TABLE_E129: &[AudioHuffEntry] = &[ + (0, 5, 12), + (1, 5, 11), + (-1, 5, 10), + (2, 5, 9), + (-2, 5, 8), + (3, 5, 7), + (-3, 5, 6), + (4, 5, 4), + (-4, 5, 3), + (5, 5, 2), + (-5, 5, 1), + (6, 5, 0), + (-6, 6, 63), + (7, 6, 61), + (-7, 6, 60), + (8, 6, 59), + (-8, 6, 58), + (9, 6, 56), + (-9, 6, 55), + (10, 6, 53), + (-10, 6, 52), + (11, 6, 51), + (-11, 6, 50), + (12, 6, 47), + (-12, 6, 46), + (13, 6, 45), + (-13, 6, 44), + (14, 6, 42), + (-14, 6, 41), + (15, 6, 38), + (-15, 6, 37), + (16, 6, 36), + (-16, 6, 35), + (17, 6, 32), + (-17, 6, 31), + (18, 6, 29), + (-18, 6, 28), + (19, 6, 26), + (-19, 6, 11), + (20, 7, 125), + (-20, 7, 124), + (21, 7, 109), + (-21, 7, 108), + (22, 7, 98), + (-22, 7, 97), + (23, 7, 87), + (-23, 7, 86), + (24, 7, 79), + (-24, 7, 78), + (25, 7, 68), + (-25, 7, 67), + (26, 7, 60), + (-26, 7, 55), + (27, 7, 21), + (-27, 7, 20), + (28, 8, 230), + (-28, 8, 229), + (29, 8, 198), + (-29, 8, 193), + (30, 8, 163), + (-30, 8, 162), + (31, 8, 139), + (-31, 8, 138), + (32, 8, 123), + (-32, 8, 122), + (33, 8, 108), + (-33, 9, 463), + (34, 9, 457), + (-34, 9, 456), + (35, 9, 385), + (-35, 9, 384), + (36, 9, 321), + (-36, 9, 320), + (37, 9, 266), + (-37, 9, 265), + (38, 9, 218), + (-38, 10, 925), + (39, 10, 798), + (-39, 10, 797), + (40, 10, 646), + (-40, 10, 645), + (41, 10, 535), + (-41, 10, 534), + (42, 10, 528), + (-42, 10, 439), + (43, 11, 1848), + (-43, 11, 1599), + (44, 11, 1592), + (-44, 11, 1295), + (45, 11, 1288), + (-45, 11, 1059), + (46, 11, 877), + (-46, 11, 876), + (47, 12, 3197), + (-47, 12, 3196), + (48, 12, 2589), + (-48, 12, 2588), + (49, 12, 2117), + (-49, 12, 2116), + (50, 13, 7398), + (-50, 13, 7397), + (51, 13, 6374), + (-51, 13, 6373), + (52, 13, 5158), + (-52, 13, 5157), + (53, 14, 14799), + (-53, 14, 14798), + (54, 14, 12751), + (-54, 14, 12750), + (55, 14, 10318), + (-55, 14, 10313), + (56, 15, 29587), + (-56, 15, 29586), + (57, 15, 29584), + (-57, 15, 25491), + (58, 15, 20625), + (-58, 15, 20624), + (59, 16, 59171), + (-59, 16, 59170), + (60, 16, 50980), + (-60, 16, 41277), + (61, 16, 50981), + (-61, 16, 41278), + (62, 16, 50978), + (-62, 16, 41279), + (63, 16, 50979), + (-63, 16, 50976), + (64, 16, 50977), + (-64, 16, 41276), +]; + +/// Annex D §D.5.12 Table F129. +const TABLE_F129: &[AudioHuffEntry] = &[ + (0, 6, 56), + (1, 6, 55), + (-1, 6, 54), + (2, 6, 52), + (-2, 6, 51), + (3, 6, 50), + (-3, 6, 49), + (4, 6, 48), + (-4, 6, 47), + (5, 6, 46), + (-5, 6, 45), + (6, 6, 44), + (-6, 6, 43), + (7, 6, 41), + (-7, 6, 40), + (8, 6, 39), + (-8, 6, 38), + (9, 6, 36), + (-9, 6, 35), + (10, 6, 34), + (-10, 6, 33), + (11, 6, 31), + (-11, 6, 30), + (12, 6, 29), + (-12, 6, 28), + (13, 6, 26), + (-13, 6, 25), + (14, 6, 23), + (-14, 6, 22), + (15, 6, 21), + (-15, 6, 20), + (16, 6, 18), + (-16, 6, 17), + (17, 6, 15), + (-17, 6, 14), + (18, 6, 12), + (-18, 6, 11), + (19, 6, 9), + (-19, 6, 8), + (20, 6, 7), + (-20, 6, 6), + (21, 6, 3), + (-21, 6, 2), + (22, 6, 1), + (-22, 6, 0), + (23, 7, 125), + (-23, 7, 124), + (24, 7, 123), + (-24, 7, 122), + (25, 7, 120), + (-25, 7, 119), + (26, 7, 116), + (-26, 7, 115), + (27, 7, 114), + (-27, 7, 107), + (28, 7, 84), + (-28, 7, 75), + (29, 7, 65), + (-29, 7, 64), + (30, 7, 54), + (-30, 7, 49), + (31, 7, 39), + (-31, 7, 38), + (32, 7, 27), + (-32, 7, 26), + (33, 7, 20), + (-33, 7, 11), + (34, 7, 10), + (-34, 7, 9), + (35, 8, 254), + (-35, 8, 253), + (36, 8, 243), + (-36, 8, 242), + (37, 8, 235), + (-37, 8, 234), + (38, 8, 213), + (-38, 8, 212), + (39, 8, 149), + (-39, 8, 148), + (40, 8, 110), + (-40, 8, 97), + (41, 8, 66), + (-41, 8, 65), + (42, 8, 43), + (-42, 8, 42), + (43, 8, 16), + (-43, 9, 511), + (44, 9, 505), + (-44, 9, 504), + (45, 9, 474), + (-45, 9, 473), + (46, 9, 343), + (-46, 9, 342), + (47, 9, 340), + (-47, 9, 223), + (48, 9, 192), + (-48, 9, 135), + (49, 9, 129), + (-49, 9, 128), + (50, 9, 34), + (-50, 10, 1021), + (51, 10, 951), + (-51, 10, 950), + (52, 10, 944), + (-52, 10, 683), + (53, 10, 445), + (-53, 10, 444), + (54, 10, 269), + (-54, 10, 268), + (55, 10, 71), + (-55, 10, 70), + (56, 11, 2040), + (-56, 11, 1891), + (57, 11, 1364), + (-57, 11, 775), + (58, 11, 774), + (-58, 11, 773), + (59, 12, 4083), + (-59, 12, 4082), + (60, 12, 3780), + (-60, 12, 2731), + (61, 12, 1545), + (-61, 12, 1544), + (62, 13, 7562), + (-62, 13, 5461), + (63, 13, 5460), + (-63, 14, 15127), + (64, 15, 30253), + (-64, 15, 30252), +]; + +/// Annex D §D.5.12 Table G129. +const TABLE_G129: &[AudioHuffEntry] = &[ + (0, 4, 0), + (1, 5, 29), + (-1, 5, 28), + (2, 5, 25), + (-2, 5, 24), + (3, 5, 21), + (-3, 5, 20), + (4, 5, 17), + (-4, 5, 16), + (5, 5, 13), + (-5, 5, 12), + (6, 5, 9), + (-6, 5, 8), + (7, 5, 5), + (-7, 5, 4), + (8, 6, 63), + (-8, 6, 62), + (9, 6, 55), + (-9, 6, 54), + (10, 6, 47), + (-10, 6, 46), + (11, 6, 39), + (-11, 6, 38), + (12, 6, 31), + (-12, 6, 30), + (13, 6, 23), + (-13, 6, 22), + (14, 6, 15), + (-14, 6, 14), + (15, 6, 7), + (-15, 6, 6), + (16, 7, 123), + (-16, 7, 122), + (17, 7, 107), + (-17, 7, 106), + (18, 7, 91), + (-18, 7, 90), + (19, 7, 75), + (-19, 7, 74), + (20, 7, 59), + (-20, 7, 58), + (21, 7, 43), + (-21, 7, 42), + (22, 7, 27), + (-22, 7, 26), + (23, 7, 11), + (-23, 7, 10), + (24, 7, 8), + (-24, 8, 243), + (25, 8, 240), + (-25, 8, 211), + (26, 8, 208), + (-26, 8, 179), + (27, 8, 176), + (-27, 8, 147), + (28, 8, 144), + (-28, 8, 115), + (29, 8, 112), + (-29, 8, 83), + (30, 8, 80), + (-30, 8, 51), + (31, 8, 48), + (-31, 8, 19), + (32, 9, 484), + (-32, 9, 483), + (33, 9, 421), + (-33, 9, 420), + (34, 9, 357), + (-34, 9, 356), + (35, 9, 293), + (-35, 9, 292), + (36, 9, 229), + (-36, 9, 228), + (37, 9, 226), + (-37, 9, 165), + (38, 9, 162), + (-38, 9, 101), + (39, 9, 98), + (-39, 9, 37), + (40, 10, 970), + (-40, 10, 965), + (41, 10, 839), + (-41, 10, 838), + (42, 10, 711), + (-42, 10, 710), + (43, 10, 708), + (-43, 10, 583), + (44, 10, 580), + (-44, 10, 455), + (45, 10, 329), + (-45, 10, 328), + (46, 10, 201), + (-46, 10, 200), + (47, 10, 198), + (-47, 10, 73), + (48, 11, 1942), + (-48, 11, 1929), + (49, 11, 1675), + (-49, 11, 1674), + (50, 11, 1672), + (-50, 11, 1419), + (51, 11, 1165), + (-51, 11, 1164), + (52, 11, 1162), + (-52, 11, 909), + (53, 11, 655), + (-53, 11, 654), + (54, 11, 652), + (-54, 11, 399), + (55, 11, 145), + (-55, 11, 144), + (56, 12, 3886), + (-56, 12, 3857), + (57, 12, 3347), + (-57, 12, 3346), + (58, 12, 2837), + (-58, 12, 2836), + (59, 12, 2327), + (-59, 12, 2326), + (60, 12, 1817), + (-60, 12, 1816), + (61, 12, 1307), + (-61, 12, 1306), + (62, 12, 797), + (-62, 12, 796), + (63, 13, 7775), + (-63, 13, 7774), + (64, 13, 7713), + (-64, 13, 7712), +]; + +/// One of the §D.5.1/§D.5.3/§D.5.4/§D.5.5/§D.5.7/§D.5.8/§D.5.9 +/// audio-data quantization-index Huffman code books, selected by the +/// §5.5 `(ABITS, SEL)` pair. +/// +/// Each variant names its §D.5 table and the `ABITS` family +/// (= mid-tread level count) it belongs to. Resolve from a +/// `(ABITS, SEL)` pair with [`AudioHuffCodebook::from_abits_sel`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AudioHuffCodebook { + /// §D.5.1 Table A3 — ABITS 1, SEL 0 (3 levels). + A3, + /// §D.5.3 Table A5 — ABITS 2, SEL 0 (5 levels). + A5, + /// §D.5.3 Table B5 — ABITS 2, SEL 1 (5 levels). + B5, + /// §D.5.3 Table C5 — ABITS 2, SEL 2 (5 levels). + C5, + /// §D.5.4 Table A7 — ABITS 3, SEL 0 (7 levels). + A7, + /// §D.5.4 Table B7 — ABITS 3, SEL 1 (7 levels). + B7, + /// §D.5.4 Table C7 — ABITS 3, SEL 2 (7 levels). + C7, + /// §D.5.5 Table A9 — ABITS 4, SEL 0 (9 levels). + A9, + /// §D.5.5 Table B9 — ABITS 4, SEL 1 (9 levels). + B9, + /// §D.5.5 Table C9 — ABITS 4, SEL 2 (9 levels). + C9, + /// §D.5.7 Table A13 — ABITS 5, SEL 0 (13 levels). + A13, + /// §D.5.7 Table B13 — ABITS 5, SEL 1 (13 levels). + B13, + /// §D.5.7 Table C13 — ABITS 5, SEL 2 (13 levels). + C13, + /// §D.5.8 Table A17 — ABITS 6, SEL 0 (17 levels). + A17, + /// §D.5.8 Table B17 — ABITS 6, SEL 1 (17 levels). + B17, + /// §D.5.8 Table C17 — ABITS 6, SEL 2 (17 levels). + C17, + /// §D.5.8 Table D17 — ABITS 6, SEL 3 (17 levels). + D17, + /// §D.5.8 Table E17 — ABITS 6, SEL 4 (17 levels). + E17, + /// §D.5.8 Table F17 — ABITS 6, SEL 5 (17 levels). + F17, + /// §D.5.8 Table G17 — ABITS 6, SEL 6 (17 levels). + G17, + /// §D.5.9 Table A25 — ABITS 7, SEL 0 (25 levels). + A25, + /// §D.5.9 Table B25 — ABITS 7, SEL 1 (25 levels). + B25, + /// §D.5.9 Table C25 — ABITS 7, SEL 2 (25 levels). + C25, + /// §D.5.9 Table D25 — ABITS 7, SEL 3 (25 levels). + D25, + /// §D.5.9 Table E25 — ABITS 7, SEL 4 (25 levels). + E25, + /// §D.5.9 Table F25 — ABITS 7, SEL 5 (25 levels). + F25, + /// §D.5.9 Table G25 — ABITS 7, SEL 6 (25 levels). + G25, + /// §D.5.10 Table A33 — ABITS 8, SEL 0 (33 levels). + A33, + /// §D.5.10 Table B33 — ABITS 8, SEL 1 (33 levels). + B33, + /// §D.5.10 Table C33 — ABITS 8, SEL 2 (33 levels). + C33, + /// §D.5.10 Table D33 — ABITS 8, SEL 3 (33 levels). + D33, + /// §D.5.10 Table E33 — ABITS 8, SEL 4 (33 levels). + E33, + /// §D.5.10 Table F33 — ABITS 8, SEL 5 (33 levels). + F33, + /// §D.5.10 Table G33 — ABITS 8, SEL 6 (33 levels). + G33, + /// §D.5.11 Table A65 — ABITS 9, SEL 0 (65 levels). + A65, + /// §D.5.11 Table B65 — ABITS 9, SEL 1 (65 levels). + B65, + /// §D.5.11 Table C65 — ABITS 9, SEL 2 (65 levels). + C65, + /// §D.5.11 Table D65 — ABITS 9, SEL 3 (65 levels). + D65, + /// §D.5.11 Table E65 — ABITS 9, SEL 4 (65 levels). + E65, + /// §D.5.11 Table F65 — ABITS 9, SEL 5 (65 levels). + F65, + /// §D.5.11 Table G65 — ABITS 9, SEL 6 (65 levels). + G65, + /// §D.5.12 Table A129 — ABITS 10, SEL 0 (129 levels). + A129, + /// §D.5.12 Table B129 — ABITS 10, SEL 1 (129 levels). + B129, + /// §D.5.12 Table C129 — ABITS 10, SEL 2 (129 levels). + C129, + /// §D.5.12 Table D129 — ABITS 10, SEL 3 (129 levels). + D129, + /// §D.5.12 Table E129 — ABITS 10, SEL 4 (129 levels). + E129, + /// §D.5.12 Table F129 — ABITS 10, SEL 5 (129 levels). + F129, + /// §D.5.12 Table G129 — ABITS 10, SEL 6 (129 levels). + G129, +} + +impl AudioHuffCodebook { + /// Resolve the audio-data Huffman code book for one subband from + /// its `(ABITS, SEL)` pair, per the Table 5-26 `SEL`-column order + /// (staged PDF p.27): + /// + /// * ABITS 1 group `A3 V3` → SEL 0 = `A3`; + /// * ABITS 2 group `A5 B5 C5 V5` → SEL 0/1/2 = `A5/B5/C5`; + /// * ABITS 3 group `A7 B7 C7 V7` → SEL 0/1/2 = `A7/B7/C7`; + /// * ABITS 4 group `A9 B9 C9 V9` → SEL 0/1/2 = `A9/B9/C9`; + /// * ABITS 5 group `A13 B13 C13 V13` → SEL 0/1/2 = `A13/B13/C13`; + /// * ABITS 6 group `A17 B17 C17 D17 E17 F17 G17 V17` → SEL 0..6 = + /// `A17/B17/C17/D17/E17/F17/G17`; + /// * ABITS 7 group `A25 B25 C25 D25 E25 F25 G25 V25` → SEL 0..6 = + /// `A25/B25/C25/D25/E25/F25/G25`. + /// + /// Returns `None` when the `(ABITS, SEL)` pair does not select a + /// Huffman book in this module: an `ABITS` outside `1..=7`, or a + /// `SEL` at (or past) the group's terminal `V…` block-code entry + /// (the [`crate::AudioQuantType::BlockCode`] path, not Huffman). + /// Use [`crate::audio_quant_type`] first to confirm the subband is + /// `nQType == 1` before calling this. + #[must_use] + pub fn from_abits_sel(abits: u8, sel: u8) -> Option { + match (abits, sel) { + (1, 0) => Some(Self::A3), + (2, 0) => Some(Self::A5), + (2, 1) => Some(Self::B5), + (2, 2) => Some(Self::C5), + (3, 0) => Some(Self::A7), + (3, 1) => Some(Self::B7), + (3, 2) => Some(Self::C7), + (4, 0) => Some(Self::A9), + (4, 1) => Some(Self::B9), + (4, 2) => Some(Self::C9), + (5, 0) => Some(Self::A13), + (5, 1) => Some(Self::B13), + (5, 2) => Some(Self::C13), + (6, 0) => Some(Self::A17), + (6, 1) => Some(Self::B17), + (6, 2) => Some(Self::C17), + (6, 3) => Some(Self::D17), + (6, 4) => Some(Self::E17), + (6, 5) => Some(Self::F17), + (6, 6) => Some(Self::G17), + (7, 0) => Some(Self::A25), + (7, 1) => Some(Self::B25), + (7, 2) => Some(Self::C25), + (7, 3) => Some(Self::D25), + (7, 4) => Some(Self::E25), + (7, 5) => Some(Self::F25), + (7, 6) => Some(Self::G25), + (8, 0) => Some(Self::A33), + (8, 1) => Some(Self::B33), + (8, 2) => Some(Self::C33), + (8, 3) => Some(Self::D33), + (8, 4) => Some(Self::E33), + (8, 5) => Some(Self::F33), + (8, 6) => Some(Self::G33), + (9, 0) => Some(Self::A65), + (9, 1) => Some(Self::B65), + (9, 2) => Some(Self::C65), + (9, 3) => Some(Self::D65), + (9, 4) => Some(Self::E65), + (9, 5) => Some(Self::F65), + (9, 6) => Some(Self::G65), + (10, 0) => Some(Self::A129), + (10, 1) => Some(Self::B129), + (10, 2) => Some(Self::C129), + (10, 3) => Some(Self::D129), + (10, 4) => Some(Self::E129), + (10, 5) => Some(Self::F129), + (10, 6) => Some(Self::G129), + _ => None, + } + } + + /// The `ABITS` family (= mid-tread quantizer level count) this book + /// belongs to: 1 (3 levels), 2 (5 levels), 3 (7 levels), 4 + /// (9 levels), 5 (13 levels), 6 (17 levels), or 7 (25 levels). + #[must_use] + pub fn abits(self) -> u8 { + match self { + Self::A3 => 1, + Self::A5 | Self::B5 | Self::C5 => 2, + Self::A7 | Self::B7 | Self::C7 => 3, + Self::A9 | Self::B9 | Self::C9 => 4, + Self::A13 | Self::B13 | Self::C13 => 5, + Self::A17 | Self::B17 | Self::C17 | Self::D17 | Self::E17 | Self::F17 | Self::G17 => 6, + Self::A25 | Self::B25 | Self::C25 | Self::D25 | Self::E25 | Self::F25 | Self::G25 => 7, + Self::A33 | Self::B33 | Self::C33 | Self::D33 | Self::E33 | Self::F33 | Self::G33 => 8, + Self::A65 | Self::B65 | Self::C65 | Self::D65 | Self::E65 | Self::F65 | Self::G65 => 9, + Self::A129 + | Self::B129 + | Self::C129 + | Self::D129 + | Self::E129 + | Self::F129 + | Self::G129 => 10, + } + } + + /// The number of quantizer levels of this book's `ABITS` family + /// (Table 5-26 "Number of Index Quantization Levels"): 3, 5, 7, 9, + /// 13, 17, or 25. Equals the number of entries in the underlying + /// §D.5 table. + #[must_use] + pub fn levels(self) -> u16 { + match self.abits() { + 1 => 3, + 2 => 5, + 3 => 7, + 4 => 9, + 5 => 13, + 6 => 17, + 7 => 25, + 8 => 33, + 9 => 65, + _ => 129, + } + } + + /// The static §D.5 code-book table backing this variant and a + /// stable name for [`Error::HuffmanDecodeFailed`]. + fn table(self) -> (&'static [AudioHuffEntry], &'static str) { + match self { + Self::A3 => (TABLE_A3, "A3"), + Self::A5 => (TABLE_A5, "A5"), + Self::B5 => (TABLE_B5, "B5"), + Self::C5 => (TABLE_C5, "C5"), + Self::A7 => (TABLE_A7, "A7"), + Self::B7 => (TABLE_B7, "B7"), + Self::C7 => (TABLE_C7, "C7"), + Self::A9 => (TABLE_A9, "A9"), + Self::B9 => (TABLE_B9, "B9"), + Self::C9 => (TABLE_C9, "C9"), + Self::A13 => (TABLE_A13, "A13"), + Self::B13 => (TABLE_B13, "B13"), + Self::C13 => (TABLE_C13, "C13"), + Self::A17 => (TABLE_A17, "A17"), + Self::B17 => (TABLE_B17, "B17"), + Self::C17 => (TABLE_C17, "C17"), + Self::D17 => (TABLE_D17, "D17"), + Self::E17 => (TABLE_E17, "E17"), + Self::F17 => (TABLE_F17, "F17"), + Self::G17 => (TABLE_G17, "G17"), + Self::A25 => (TABLE_A25, "A25"), + Self::B25 => (TABLE_B25, "B25"), + Self::C25 => (TABLE_C25, "C25"), + Self::D25 => (TABLE_D25, "D25"), + Self::E25 => (TABLE_E25, "E25"), + Self::F25 => (TABLE_F25, "F25"), + Self::G25 => (TABLE_G25, "G25"), + Self::A33 => (TABLE_A33, "A33"), + Self::B33 => (TABLE_B33, "B33"), + Self::C33 => (TABLE_C33, "C33"), + Self::D33 => (TABLE_D33, "D33"), + Self::E33 => (TABLE_E33, "E33"), + Self::F33 => (TABLE_F33, "F33"), + Self::G33 => (TABLE_G33, "G33"), + Self::A65 => (TABLE_A65, "A65"), + Self::B65 => (TABLE_B65, "B65"), + Self::C65 => (TABLE_C65, "C65"), + Self::D65 => (TABLE_D65, "D65"), + Self::E65 => (TABLE_E65, "E65"), + Self::F65 => (TABLE_F65, "F65"), + Self::G65 => (TABLE_G65, "G65"), + Self::A129 => (TABLE_A129, "A129"), + Self::B129 => (TABLE_B129, "B129"), + Self::C129 => (TABLE_C129, "C129"), + Self::D129 => (TABLE_D129, "D129"), + Self::E129 => (TABLE_E129, "E129"), + Self::F129 => (TABLE_F129, "F129"), + Self::G129 => (TABLE_G129, "G129"), + } + } + + /// The longest codeword length in this book, the bit bound the + /// decoder needs to reach to resolve its deepest leaf. Computed from + /// the backing §D.5 table so it cannot drift from the data. The + /// 25-level §D.5.9 `A25` book is the deepest at 14 bits (the ±12 + /// codes 10 325/10 324); the 17-level §D.5.8 `A17` book reaches 12 + /// bits and the smaller families top out at 7 (§D.5.7) or below. + #[must_use] + pub fn max_code_len(self) -> u8 { + let (table, _) = self.table(); + table.iter().map(|&(_, len, _)| len).max().unwrap_or(0) + } +} + +/// Walk a §D.5 audio-data Huffman code book one bit at a time, +/// MSB-first, returning the matching signed quantization level when a +/// code of the prefix-matched length is found. Returns +/// [`Error::HuffmanDecodeFailed`] when no entry matches within +/// [`MAX_AUDIO_HUFF_CODE_LEN`] bits. +fn decode_audio_huff(br: &mut BitReader<'_>, codebook: AudioHuffCodebook) -> Result { + let (table, name) = codebook.table(); + // Each book's own deepest leaf bounds the walk; never read past the + // global worst case even if a table is malformed. + let bound = codebook.max_code_len().min(MAX_AUDIO_HUFF_CODE_LEN as u8); + let mut value: u32 = 0; + let mut bits_read: u8 = 0; + while bits_read < bound { + let bit = br.read_bits(1)?; + value = (value << 1) | bit; + bits_read += 1; + for &(level, code_len, code) in table { + if code_len == bits_read && value == code as u32 { + return Ok(level); + } + } + } + Err(Error::HuffmanDecodeFailed { table: name }) +} + +/// Decode a single §5.5 `nQType == 1` `AUDIO[m]` quantization index +/// from `bytes` starting at `bit_offset` (MSB-first from `bytes[0]`), +/// through the §D.5 code book selected by `codebook`. +/// +/// Returns `(quantization_level, bits_consumed)` where +/// `quantization_level` is the **signed** mid-tread output level §5.5 +/// scales by `rScale`, and `bits_consumed` is the codeword length. +/// +/// # Errors +/// +/// * [`Error::UnexpectedEof`] when the buffer ends mid-codeword; +/// * [`Error::HuffmanDecodeFailed`] when no §D.5 entry matches. +pub fn decode_audio_huff_at( + bytes: &[u8], + bit_offset: usize, + codebook: AudioHuffCodebook, +) -> Result<(i16, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let level = decode_audio_huff(&mut br, codebook)?; + let bits_consumed = br.absolute_bit_position() - bit_offset; + Ok((level, bits_consumed)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Pack a series of (value, bit_width) fields MSB-first. + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + const ALL_BOOKS: &[AudioHuffCodebook] = &[ + AudioHuffCodebook::A3, + AudioHuffCodebook::A5, + AudioHuffCodebook::B5, + AudioHuffCodebook::C5, + AudioHuffCodebook::A7, + AudioHuffCodebook::B7, + AudioHuffCodebook::C7, + AudioHuffCodebook::A9, + AudioHuffCodebook::B9, + AudioHuffCodebook::C9, + AudioHuffCodebook::A13, + AudioHuffCodebook::B13, + AudioHuffCodebook::C13, + AudioHuffCodebook::A17, + AudioHuffCodebook::B17, + AudioHuffCodebook::C17, + AudioHuffCodebook::D17, + AudioHuffCodebook::E17, + AudioHuffCodebook::F17, + AudioHuffCodebook::G17, + AudioHuffCodebook::A25, + AudioHuffCodebook::B25, + AudioHuffCodebook::C25, + AudioHuffCodebook::D25, + AudioHuffCodebook::E25, + AudioHuffCodebook::F25, + AudioHuffCodebook::G25, + AudioHuffCodebook::A33, + AudioHuffCodebook::B33, + AudioHuffCodebook::C33, + AudioHuffCodebook::D33, + AudioHuffCodebook::E33, + AudioHuffCodebook::F33, + AudioHuffCodebook::G33, + AudioHuffCodebook::A65, + AudioHuffCodebook::B65, + AudioHuffCodebook::C65, + AudioHuffCodebook::D65, + AudioHuffCodebook::E65, + AudioHuffCodebook::F65, + AudioHuffCodebook::G65, + AudioHuffCodebook::A129, + AudioHuffCodebook::B129, + AudioHuffCodebook::C129, + AudioHuffCodebook::D129, + AudioHuffCodebook::E129, + AudioHuffCodebook::F129, + AudioHuffCodebook::G129, + ]; + + #[test] + fn from_abits_sel_resolves_table_5_26_groups() { + assert_eq!( + AudioHuffCodebook::from_abits_sel(1, 0), + Some(AudioHuffCodebook::A3) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(2, 0), + Some(AudioHuffCodebook::A5) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(2, 1), + Some(AudioHuffCodebook::B5) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(2, 2), + Some(AudioHuffCodebook::C5) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(3, 0), + Some(AudioHuffCodebook::A7) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(3, 1), + Some(AudioHuffCodebook::B7) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(3, 2), + Some(AudioHuffCodebook::C7) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(4, 0), + Some(AudioHuffCodebook::A9) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(4, 1), + Some(AudioHuffCodebook::B9) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(4, 2), + Some(AudioHuffCodebook::C9) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(5, 0), + Some(AudioHuffCodebook::A13) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(5, 1), + Some(AudioHuffCodebook::B13) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(5, 2), + Some(AudioHuffCodebook::C13) + ); + // ABITS 6 group `A17 B17 C17 D17 E17 F17 G17 V17`: SEL 0..6 are + // the seven Huffman books. + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 0), + Some(AudioHuffCodebook::A17) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 1), + Some(AudioHuffCodebook::B17) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 2), + Some(AudioHuffCodebook::C17) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 3), + Some(AudioHuffCodebook::D17) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 4), + Some(AudioHuffCodebook::E17) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 5), + Some(AudioHuffCodebook::F17) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(6, 6), + Some(AudioHuffCodebook::G17) + ); + // ABITS 7 group `A25 B25 C25 D25 E25 F25 G25 V25`: SEL 0..6 are + // the seven Huffman books. + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 0), + Some(AudioHuffCodebook::A25) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 1), + Some(AudioHuffCodebook::B25) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 2), + Some(AudioHuffCodebook::C25) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 3), + Some(AudioHuffCodebook::D25) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 4), + Some(AudioHuffCodebook::E25) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 5), + Some(AudioHuffCodebook::F25) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(7, 6), + Some(AudioHuffCodebook::G25) + ); + // ABITS 8 group `A33..G33 NFE`: SEL 0..6 are the Huffman books. + assert_eq!( + AudioHuffCodebook::from_abits_sel(8, 0), + Some(AudioHuffCodebook::A33) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(8, 6), + Some(AudioHuffCodebook::G33) + ); + // ABITS 9 group `A65..G65 NFE`. + assert_eq!( + AudioHuffCodebook::from_abits_sel(9, 0), + Some(AudioHuffCodebook::A65) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(9, 4), + Some(AudioHuffCodebook::E65) + ); + // ABITS 10 group `A129..G129 NFE`. + assert_eq!( + AudioHuffCodebook::from_abits_sel(10, 0), + Some(AudioHuffCodebook::A129) + ); + assert_eq!( + AudioHuffCodebook::from_abits_sel(10, 6), + Some(AudioHuffCodebook::G129) + ); + } + + #[test] + fn higher_families_abits_and_levels() { + assert_eq!(AudioHuffCodebook::A33.abits(), 8); + assert_eq!(AudioHuffCodebook::A33.levels(), 33); + assert_eq!(AudioHuffCodebook::G33.levels(), 33); + assert_eq!(AudioHuffCodebook::A65.abits(), 9); + assert_eq!(AudioHuffCodebook::A65.levels(), 65); + assert_eq!(AudioHuffCodebook::G65.levels(), 65); + assert_eq!(AudioHuffCodebook::A129.abits(), 10); + assert_eq!(AudioHuffCodebook::A129.levels(), 129); + assert_eq!(AudioHuffCodebook::G129.levels(), 129); + } + + /// The §D.5.11 / §D.5.12 families reach the crate-wide deepest + /// audio-data codeword of 16 bits; the bit-at-a-time walk must be + /// able to resolve them. + #[test] + fn higher_families_reach_sixteen_bit_codes() { + let deepest = ALL_BOOKS.iter().map(|b| b.max_code_len()).max().unwrap(); + assert_eq!(deepest, 16); + assert!(u32::from(deepest) <= MAX_AUDIO_HUFF_CODE_LEN); + } + + #[test] + fn from_abits_sel_none_for_terminal_or_out_of_family() { + // Terminal SEL of each group is the V… block code, not Huffman. + assert_eq!(AudioHuffCodebook::from_abits_sel(1, 1), None); // V3 + assert_eq!(AudioHuffCodebook::from_abits_sel(2, 3), None); // V5 + assert_eq!(AudioHuffCodebook::from_abits_sel(3, 3), None); // V7 + assert_eq!(AudioHuffCodebook::from_abits_sel(4, 3), None); // V9 + assert_eq!(AudioHuffCodebook::from_abits_sel(5, 3), None); // V13 + assert_eq!(AudioHuffCodebook::from_abits_sel(6, 7), None); // V17 + assert_eq!(AudioHuffCodebook::from_abits_sel(7, 7), None); // V25 + // No bits allocated / outside the transcribed families. + assert_eq!(AudioHuffCodebook::from_abits_sel(0, 0), None); + // ABITS 8..=10 now resolve their seven Huffman books; the + // terminal SEL 7 (NFE) and ABITS 11 carry no Huffman book. + assert_eq!(AudioHuffCodebook::from_abits_sel(8, 7), None); // NFE + assert_eq!(AudioHuffCodebook::from_abits_sel(9, 7), None); // NFE + assert_eq!(AudioHuffCodebook::from_abits_sel(10, 7), None); // NFE + assert_eq!(AudioHuffCodebook::from_abits_sel(11, 0), None); + } + + #[test] + fn abits_and_levels_match_family() { + assert_eq!(AudioHuffCodebook::A3.abits(), 1); + assert_eq!(AudioHuffCodebook::A3.levels(), 3); + assert_eq!(AudioHuffCodebook::C5.abits(), 2); + assert_eq!(AudioHuffCodebook::C5.levels(), 5); + assert_eq!(AudioHuffCodebook::B7.abits(), 3); + assert_eq!(AudioHuffCodebook::B7.levels(), 7); + assert_eq!(AudioHuffCodebook::C9.abits(), 4); + assert_eq!(AudioHuffCodebook::C9.levels(), 9); + assert_eq!(AudioHuffCodebook::A13.abits(), 5); + assert_eq!(AudioHuffCodebook::A13.levels(), 13); + assert_eq!(AudioHuffCodebook::C13.abits(), 5); + assert_eq!(AudioHuffCodebook::C13.levels(), 13); + assert_eq!(AudioHuffCodebook::A17.abits(), 6); + assert_eq!(AudioHuffCodebook::A17.levels(), 17); + assert_eq!(AudioHuffCodebook::G17.abits(), 6); + assert_eq!(AudioHuffCodebook::G17.levels(), 17); + assert_eq!(AudioHuffCodebook::A25.abits(), 7); + assert_eq!(AudioHuffCodebook::A25.levels(), 25); + assert_eq!(AudioHuffCodebook::G25.abits(), 7); + assert_eq!(AudioHuffCodebook::G25.levels(), 25); + } + + #[test] + fn level_count_equals_table_length() { + for &book in ALL_BOOKS { + let (table, _) = book.table(); + assert_eq!( + table.len() as u16, + book.levels(), + "{book:?} table length must equal its level count" + ); + } + } + + #[test] + fn every_book_is_a_complete_prefix_code() { + // A valid Huffman book: no code is a prefix of another, and the + // Kraft sum over all leaves equals 1 (a full mid-tread set). + for &book in ALL_BOOKS { + let (table, name) = book.table(); + // Prefix-freeness: for any two entries, the shorter is not a + // prefix of the longer. + for (i, &(_, len_i, code_i)) in table.iter().enumerate() { + for (j, &(_, len_j, code_j)) in table.iter().enumerate() { + if i == j { + continue; + } + if len_i <= len_j { + let shift = len_j - len_i; + assert_ne!( + (code_j >> shift), + code_i, + "{name}: code {code_i:b}/{len_i} is a prefix of {code_j:b}/{len_j}" + ); + } + } + } + // Kraft equality (complete code). + let kraft: f64 = table + .iter() + .map(|&(_, len, _)| 2f64.powi(-(len as i32))) + .sum(); + assert!( + (kraft - 1.0).abs() < 1e-9, + "{name}: Kraft sum {kraft} != 1 (incomplete code)" + ); + } + } + + #[test] + fn every_book_round_trips_every_symbol() { + // Encode each printed codeword, decode it, and confirm the + // signed level + consumed-bit count come back exactly. + for &book in ALL_BOOKS { + let (table, _) = book.table(); + for &(level, code_len, code) in table { + let stream = pack_fields(&[(code as u32, code_len)]); + let (got, bits) = decode_audio_huff_at(&stream, 0, book).unwrap(); + assert_eq!(got, level, "{book:?}: level for code {code:b}/{code_len}"); + assert_eq!(bits, code_len as usize, "{book:?}: consumed bits"); + } + } + } + + #[test] + fn books_decode_signed_levels_symmetrically() { + // Each family carries a symmetric ± level set around 0; verify + // the printed level columns include both signs up to the family + // amplitude. + let max_amp = |book: AudioHuffCodebook| (book.levels() as i16 - 1) / 2; + for &book in ALL_BOOKS { + let (table, _) = book.table(); + let amp = max_amp(book); + for lvl in -amp..=amp { + assert!( + table.iter().any(|&(l, _, _)| l == lvl), + "{book:?}: missing level {lvl}" + ); + } + } + } + + #[test] + fn a3_specific_codes() { + // §D.5.1 Table A3, transcribed verbatim (PDF p.198). + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(0, 1)]), 0, AudioHuffCodebook::A3).unwrap(), + (0, 1) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(2, 2)]), 0, AudioHuffCodebook::A3).unwrap(), + (1, 2) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(3, 2)]), 0, AudioHuffCodebook::A3).unwrap(), + (-1, 2) + ); + } + + #[test] + fn a9_longest_codes_are_six_bits() { + // §D.5.5 Table A9 (PDF p.200): ±4 are the 6-bit codes 49/48. + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(49, 6)]), 0, AudioHuffCodebook::A9).unwrap(), + (4, 6) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(48, 6)]), 0, AudioHuffCodebook::A9).unwrap(), + (-4, 6) + ); + } + + #[test] + fn a13_longest_codes_are_seven_bits() { + // §D.5.7 Table A13 (PDF p.202): ±6 are the 7-bit codes 113/112, + // the deepest codewords in any §D.5 family transcribed here. + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(113, 7)]), 0, AudioHuffCodebook::A13).unwrap(), + (6, 7) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(112, 7)]), 0, AudioHuffCodebook::A13).unwrap(), + (-6, 7) + ); + // §D.5.7 Table C13 has no code longer than 5 bits (±6 = 5/4). + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(5, 5)]), 0, AudioHuffCodebook::C13).unwrap(), + (6, 5) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(4, 5)]), 0, AudioHuffCodebook::C13).unwrap(), + (-6, 5) + ); + } + + #[test] + fn a17_longest_codes_are_twelve_bits() { + // §D.5.8 Table A17 (PDF p.203): ±8 are the 12-bit codes 341/340, + // the deepest codewords in any §D.5 family transcribed here. The + // decoder must walk all 12 bits to resolve them. + assert_eq!(AudioHuffCodebook::A17.max_code_len(), 12); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(341, 12)]), 0, AudioHuffCodebook::A17).unwrap(), + (8, 12) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(340, 12)]), 0, AudioHuffCodebook::A17).unwrap(), + (-8, 12) + ); + // D17's single 1-bit code (level 0) is the shallowest 17-level + // entry; max_code_len tracks each book independently. + assert_eq!(AudioHuffCodebook::D17.max_code_len(), 9); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(0, 1)]), 0, AudioHuffCodebook::D17).unwrap(), + (0, 1) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(256, 9)]), 0, AudioHuffCodebook::D17).unwrap(), + (-8, 9) + ); + // F17 carries the deepest 1-padded code (level +7 = 6-bit 0): + // confirm a mid-table 8-bit leaf resolves. + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(5, 8)]), 0, AudioHuffCodebook::F17).unwrap(), + (8, 8) + ); + } + + #[test] + fn a25_longest_codes_are_fourteen_bits() { + // §D.5.9 Table A25 (PDF p.205): ±12 are the 14-bit codes + // 10 325/10 324 — the deepest codewords in any §D.5 audio-data + // family transcribed in this crate. The decoder must walk all 14 + // bits to resolve them. + assert_eq!(AudioHuffCodebook::A25.max_code_len(), 14); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(10325, 14)]), 0, AudioHuffCodebook::A25).unwrap(), + (12, 14) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(10324, 14)]), 0, AudioHuffCodebook::A25).unwrap(), + (-12, 14) + ); + // A25's escalating tail (9-bit 323 → 14-bit 10 324) means the + // intermediate ±10/±11 codes resolve at 10/11/12/13 bits. + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(644, 10)]), 0, AudioHuffCodebook::A25).unwrap(), + (10, 10) + ); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(5163, 13)]), 0, AudioHuffCodebook::A25).unwrap(), + (-11, 13) + ); + // D25 caps at 12 bits despite 25 levels: ±12 = 1921/1920. + assert_eq!(AudioHuffCodebook::D25.max_code_len(), 12); + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(1920, 12)]), 0, AudioHuffCodebook::D25).unwrap(), + (-12, 12) + ); + // G25's 2-bit level-0 code (1) is the shallowest 25-level entry. + assert_eq!( + decode_audio_huff_at(&pack_fields(&[(1, 2)]), 0, AudioHuffCodebook::G25).unwrap(), + (0, 2) + ); + } + + #[test] + fn decode_at_unaligned_offset_matches_aligned() { + // Prepend 3 filler bits; the decode must match the aligned read. + let aligned = pack_fields(&[(31, 5)]); // A7 level +3 + let shifted = pack_fields(&[(0b101, 3), (31, 5)]); + let a = decode_audio_huff_at(&aligned, 0, AudioHuffCodebook::A7).unwrap(); + let b = decode_audio_huff_at(&shifted, 3, AudioHuffCodebook::A7).unwrap(); + assert_eq!(a, b); + assert_eq!(a, (3, 5)); + } + + #[test] + fn truncated_stream_surfaces_eof() { + // An empty buffer cannot supply even the first codeword bit. + assert_eq!( + decode_audio_huff_at(&[], 0, AudioHuffCodebook::A9).unwrap_err(), + Error::UnexpectedEof + ); + // A single byte whose first bit forces the long branch but + // whose tail runs out: `110001` (A9 level +4) needs 6 bits; + // start 4 bits into the byte so only 4 remain. The first three + // available bits `100` are not a complete A9 code shorter than + // 3 bits with that prefix, so the read walks past EOF. Use a + // byte laid out as `xxxx 1000` and a level whose code begins + // `100…`: A9's 4-bit codes are 13/9/8 — `1000` = 8 (level -2) + // is exactly 4 bits and resolves, so instead truncate harder: + // start 6 bits in, leaving 2 bits, and require the 6-bit code. + let one_byte = pack_fields(&[(0b00000011, 8)]); + assert_eq!(one_byte.len(), 1); + // Remaining bits from offset 6: `11`. A9 has no 1- or 2-bit code + // matching `1`/`11`, so the third-bit read hits EOF. + assert_eq!( + decode_audio_huff_at(&one_byte, 6, AudioHuffCodebook::A9).unwrap_err(), + Error::UnexpectedEof + ); + } + + #[test] + fn complete_codes_always_resolve_within_max_len() { + // Every §D.5 book transcribed here is a complete prefix code + // (Kraft sum = 1, checked above), so any bit pattern long + // enough resolves to *some* symbol within the book's own + // `max_code_len` bits — the `HuffmanDecodeFailed` arm is only + // reachable on a truncated read (covered by + // `truncated_stream_surfaces_eof`). Confirm every + // MAX_AUDIO_HUFF_CODE_LEN-bit (= deepest, 14-bit `A25`) prefix + // decodes for the deepest family. + for raw in 0u32..(1 << MAX_AUDIO_HUFF_CODE_LEN) { + let stream = pack_fields(&[(raw, MAX_AUDIO_HUFF_CODE_LEN as u8)]); + let res = decode_audio_huff_at(&stream, 0, AudioHuffCodebook::A25); + assert!( + res.is_ok(), + "A25: {MAX_AUDIO_HUFF_CODE_LEN}-bit prefix {raw:014b} failed to resolve: {res:?}" + ); + } + } +} diff --git a/crates/vendor/oxideav-dts/src/aux_data.rs b/crates/vendor/oxideav-dts/src/aux_data.rs new file mode 100644 index 00000000..b511e74f --- /dev/null +++ b/crates/vendor/oxideav-dts/src/aux_data.rs @@ -0,0 +1,770 @@ +//! §5.7.1 Auxiliary Data chunk (Table 5-31): the optional +//! end-of-frame metadata block carrying a decode time stamp and the +//! dynamic (embedded) downmix coefficients. +//! +//! Transcribed from ETSI TS 102 114 V1.3.1 (2011-08) §5.6 +//! (Table 5-30, the optional-information region after the audio-data +//! arrays) and §5.7.1 (Table 5-31 + field descriptions, PDF p.34-37), +//! staged at `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. +//! +//! ## Navigation +//! +//! Per §5.7.1: "Navigation to the start location of the auxiliary +//! data is achieved by either reading the AUXCT variable and +//! traversing to the next DWORD or by searching for the DWORD aligned +//! AUX sync word 0x9A1105A0 from the end of the audio frame. Since +//! the data in the auxiliary may be required prior to unpacking the +//! subframe data, the latter approach of searching for the AUX sync +//! word is the suggested method." [`find_aux_data`] implements the +//! suggested backward search over DWORD-aligned offsets ("aligned on +//! the 32-bit boundary from the beginning of the core stream", i.e. +//! `offset % 4 == 0` counting from the frame's first sync byte). +//! +//! ## Chunk layout (Table 5-31) +//! +//! ```text +//! nSYNCAUX = ExtractBits(32); // 0x9A1105A0 +//! bAUXTimeStampFlag = ExtractBits(1); +//! if ( bAUXTimeStampFlag ) { +//! Advance2Next4BitPos(); +//! nMSByte = ExtractBits(8); +//! nMarker = ExtractBits(4); // == 0b1011 +//! nLSByte28 = ExtractBits(28); +//! nMarker = ExtractBits(4); // == 0b1011 +//! nAUXTimeStamp = (nMSByte << 28) | nLSByte28; +//! } +//! bAUXDynamCoeffFlag = ExtractBits(1); +//! if ( bAUXDynamCoeffFlag ) { +//! nPrmChDownMixType = ExtractBits(3); // Table 5-32 +//! nNumDwnMixCodeCoeffs = DeriveNumDwnMixCodeCoeffs(); +//! for (n = 0; n < nNumDwnMixCodeCoeffs; n++) +//! panDwnMixCodeCoeffs[n] = ExtractBits(9); +//! } +//! ByteAlign = ExtractBits(0 ... 7); +//! nAUXCRC16 = ExtractBits(16); +//! ``` +//! +//! `DeriveNumDwnMixCodeCoeffs()` multiplies the input channel count +//! `nPriCh` (`anNumCh[AMODE]`, plus one when `LFF > 0`) by the number +//! of resultant downmix channels the Table 5-32 type designates +//! (`m_nNumChPrevHierChSet`): `1/0 → 1`, `Lo/Ro`+`Lt/Rt → 2`, +//! `3/0`+`2/1 → 3`, `2/2`+`3/1 → 4`, `Unused → 0`. The coefficients +//! are packed as an N×M table walked output-channel-major +//! (`for nIndPrmCh ... for nIndXCh`), each a 9-bit code resolved +//! through [`crate::decode_dmix_code`] into the §D.11 `DmixTable`. +//! +//! The `nAUXCRC16` value "is calculated for the auxiliary data from +//! positions bAUXTimeStampFlag to the byte prior to the start of the +//! CRC inclusive" using the Annex B algorithm (CRC-CCITT, polynomial +//! `0x1021`, init `0xFFFF` — see [`crate::dts_crc16`] and +//! `docs/audio/dts/dts-crc16.md`). `bAUXTimeStampFlag` begins on a +//! byte boundary (right after the 32-bit sync word) and the +//! `ByteAlign` pad closes the region on one, so the coverage span is +//! the whole-byte window from `offset + 4` to the byte before the +//! CRC field. The parser recomputes it and reports the outcome in +//! [`AuxData::crc_valid`]; unlike the core `HCRC`-family fields, the +//! aux CRC is genuinely meant to be tested (it also disambiguates +//! false `nSYNCAUX` alias matches). + +use crate::bitreader::BitReader; +use crate::crc16::dts_crc16; +use crate::header::DtsFrameHeader; +use crate::{decode_dmix_code, Error, Result}; + +/// The §5.7.1 auxiliary-data sync word (`nSYNCAUX`), DWORD-aligned +/// from the beginning of the core frame. +pub const AUX_SYNC_WORD: u32 = 0x9A11_05A0; + +/// The 4-bit marker value (`0b1011`) bracketing the two halves of the +/// §5.7.1 36-bit decode time stamp. +pub const AUX_TIME_STAMP_MARKER: u8 = 0b1011; + +/// §5.7.1 Table 5-32 "Downmix Channel Groups": the layout the primary +/// channel group folds down to (`nPrmChDownMixType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DownmixType { + /// `000` — mono (`1/0`). + OneZero, + /// `001` — stereo `Lo/Ro`. + LoRo, + /// `010` — stereo `Lt/Rt` (matrix surround encoded). + LtRt, + /// `011` — three front channels (`3/0`). + ThreeZero, + /// `100` — two front + one surround (`2/1`). + TwoOne, + /// `101` — two front + two surround (`2/2`). + TwoTwo, + /// `110` — three front + one surround (`3/1`). + ThreeOne, + /// `111` — marked "Unused" in Table 5-32; the Table 5-31 + /// pseudocode's `default:` arm derives zero coefficients for it. + Unused, +} + +impl DownmixType { + /// Resolve the 3-bit `nPrmChDownMixType` field. + #[must_use] + pub fn from_code(code: u8) -> Option { + Some(match code { + 0b000 => DownmixType::OneZero, + 0b001 => DownmixType::LoRo, + 0b010 => DownmixType::LtRt, + 0b011 => DownmixType::ThreeZero, + 0b100 => DownmixType::TwoOne, + 0b101 => DownmixType::TwoTwo, + 0b110 => DownmixType::ThreeOne, + 0b111 => DownmixType::Unused, + _ => return None, + }) + } + + /// The on-wire 3-bit code. + #[must_use] + pub fn code(self) -> u8 { + match self { + DownmixType::OneZero => 0b000, + DownmixType::LoRo => 0b001, + DownmixType::LtRt => 0b010, + DownmixType::ThreeZero => 0b011, + DownmixType::TwoOne => 0b100, + DownmixType::TwoTwo => 0b101, + DownmixType::ThreeOne => 0b110, + DownmixType::Unused => 0b111, + } + } + + /// Number of resultant downmix channels (the Table 5-31 + /// pseudocode's `m_nNumChPrevHierChSet`): `1`, `2`, `3`, or `4` — + /// `0` for the `Unused` code (the pseudocode's `default:` arm). + #[must_use] + pub fn output_channel_count(self) -> usize { + match self { + DownmixType::OneZero => 1, + DownmixType::LoRo | DownmixType::LtRt => 2, + DownmixType::ThreeZero | DownmixType::TwoOne => 3, + DownmixType::TwoTwo | DownmixType::ThreeOne => 4, + DownmixType::Unused => 0, + } + } +} + +/// The §5.7.1 dynamic (embedded) downmix coefficient set: the N×M +/// code table folding the frame's primary channels (plus LFE when +/// present) down to the [`DownmixType`] channel group. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DynamicDownmix { + /// The Table 5-32 downmix channel group (`nPrmChDownMixType`). + pub downmix_type: DownmixType, + /// Input channel count `nPriCh`: `anNumCh[AMODE]` plus one when + /// the frame carries an LFE channel (`LFF > 0`). + pub input_channel_count: usize, + /// The raw 9-bit `panDwnMixCodeCoeffs[]` code words, + /// output-channel-major (`codes[out_ch * input_channel_count + + /// in_ch]`), exactly `output_channel_count() * + /// input_channel_count` long. + pub codes: Vec, +} + +impl DynamicDownmix { + /// Number of resultant downmix channels (`M`). + #[must_use] + pub fn output_channel_count(&self) -> usize { + self.downmix_type.output_channel_count() + } + + /// The raw 9-bit code word folding input channel `in_ch` into + /// output channel `out_ch`, or `None` when either index is out of + /// range. + #[must_use] + pub fn code(&self, out_ch: usize, in_ch: usize) -> Option { + if out_ch >= self.output_channel_count() || in_ch >= self.input_channel_count { + return None; + } + self.codes + .get(out_ch * self.input_channel_count + in_ch) + .copied() + } + + /// The real-valued coefficient folding input channel `in_ch` into + /// output channel `out_ch`, resolved through + /// [`crate::decode_dmix_code`] (§D.11 `DmixTable`, phase MSB, + /// one-biased index, `0` → exact `0.0`). + /// + /// # Errors + /// + /// [`Error::DownmixCodeOutOfRange`] when the indices are out of + /// range (surfaced with the sentinel code `u16::MAX`) or the + /// stored code word does not resolve through the §D.11 table. + pub fn coefficient(&self, out_ch: usize, in_ch: usize) -> Result { + let code = self + .code(out_ch, in_ch) + .ok_or(Error::DownmixCodeOutOfRange { code: u16::MAX })?; + decode_dmix_code(code) + } + + /// The full M×N real-valued coefficient matrix, + /// `matrix[out_ch][in_ch]`. + /// + /// # Errors + /// + /// [`Error::DownmixCodeOutOfRange`] when any stored code word does + /// not resolve through the §D.11 table. + pub fn coefficient_matrix(&self) -> Result>> { + (0..self.output_channel_count()) + .map(|out_ch| { + (0..self.input_channel_count) + .map(|in_ch| self.coefficient(out_ch, in_ch)) + .collect() + }) + .collect() + } + + /// Fold planar PCM through the coefficient table: + /// `output[m][t] = Σ_n coefficient(m, n) · input[n][t]`. + /// + /// `input` must carry exactly [`Self::input_channel_count`] + /// equal-length planes (the §5.7.1 layout: the Table 5-4 primary + /// channels in bitstream order, then the LFE plane when the frame + /// carries one — matching the plane order the registry decoder + /// emits). Each accumulated sample is truncated toward zero, the + /// same `int()` cast convention as the §C.2.5 PCM output step, + /// and saturated to the `i32` range. + /// + /// # Errors + /// + /// - [`Error::DownmixInputShapeMismatch`] when the plane count is + /// not `input_channel_count` or the planes have unequal + /// lengths. + /// - [`Error::DownmixCodeOutOfRange`] when a stored code word + /// does not resolve through the §D.11 table. + pub fn apply_planar(&self, input: &[Vec]) -> Result>> { + if input.len() != self.input_channel_count { + return Err(Error::DownmixInputShapeMismatch { + expected: self.input_channel_count, + found: input.len(), + }); + } + let samples = input.first().map_or(0, Vec::len); + if let Some(plane) = input.iter().find(|p| p.len() != samples) { + return Err(Error::DownmixInputShapeMismatch { + expected: samples, + found: plane.len(), + }); + } + let matrix = self.coefficient_matrix()?; + let mut output = vec![vec![0i32; samples]; self.output_channel_count()]; + for (out_plane, row) in output.iter_mut().zip(&matrix) { + for (t, out_sample) in out_plane.iter_mut().enumerate() { + let mut acc = 0.0f64; + for (coeff, in_plane) in row.iter().zip(input) { + acc += coeff * f64::from(in_plane[t]); + } + // Truncate toward zero (the §C.2.5 int() convention), + // saturating at the i32 bounds. + *out_sample = acc.trunc().clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32; + } + } + Ok(output) + } +} + +/// A parsed §5.7.1 Auxiliary Data chunk (Table 5-31). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuxData { + /// Byte offset of the DWORD-aligned `nSYNCAUX` sync word within + /// the frame. + pub offset: usize, + /// The 36-bit `nAUXTimeStamp` decode time stamp (present when + /// `bAUXTimeStampFlag` is set): a running sample counter used to + /// synchronize the core audio with another audio stream. The two + /// 4-bit `0b1011` markers bracketing its halves are validated and + /// stripped. + pub time_stamp: Option, + /// The dynamic downmix coefficient set (present when + /// `bAUXDynamCoeffFlag` is set). + pub dynamic_downmix: Option, + /// The raw 16-bit `nAUXCRC16` word (Annex B CRC-CCITT, see + /// [`crate::dts_crc16`]). + pub crc16: u16, + /// Whether [`Self::crc16`] matches the Annex B CRC-16 recomputed + /// over the chunk's protected region — "the auxiliary data from + /// positions bAUXTimeStampFlag to the byte prior to the start of + /// the CRC inclusive" (§5.6). A `false` here means the chunk is + /// corrupt or the DWORD-aligned sync match was a false alias; + /// the parse result is surfaced either way so callers can decide. + pub crc_valid: bool, +} + +/// Search a core frame for the DWORD-aligned §5.7.1 auxiliary-data +/// sync word `0x9A1105A0`, returning its byte offset. +/// +/// Implements the spec's suggested navigation: "searching for the +/// DWORD aligned AUX sync word 0x9A1105A0 from the end of the audio +/// frame" — the scan walks backward over 32-bit-aligned offsets +/// (alignment counted from the frame's first sync byte) and returns +/// the last aligned occurrence in the frame. +#[must_use] +pub fn find_aux_data(frame: &[u8]) -> Option { + let sync = AUX_SYNC_WORD.to_be_bytes(); + if frame.len() < 4 { + return None; + } + // Highest DWORD-aligned offset with room for the 4 sync bytes. + let mut offset = (frame.len() - 4) & !3; + loop { + if frame[offset..offset + 4] == sync { + return Some(offset); + } + if offset == 0 { + return None; + } + offset -= 4; + } +} + +/// Parse the §5.7.1 Auxiliary Data chunk beginning at `byte_offset` +/// within `frame` (an offset produced by [`find_aux_data`]). +/// +/// `header` supplies the input-channel derivation for +/// `DeriveNumDwnMixCodeCoeffs()`: `nPriCh = anNumCh[AMODE]` plus one +/// when `LFF > 0`. +/// +/// # Errors +/// +/// - [`Error::AuxSyncMismatch`] — the bytes at `byte_offset` are not +/// the `0x9A1105A0` sync word. +/// - [`Error::AuxTimeStampMarkerMismatch`] — a 4-bit time-stamp +/// marker was not `0b1011`. +/// - [`Error::AuxChannelCountUnresolved`] — the frame's `AMODE` is a +/// user-defined code whose channel count Table 5-4 does not define, +/// so the coefficient count cannot be derived. +/// - [`Error::UnexpectedEof`] — the chunk walked past the end of the +/// frame. +pub fn parse_aux_data_at( + frame: &[u8], + byte_offset: usize, + header: &DtsFrameHeader, +) -> Result { + let mut br = BitReader::from_byte_offset(frame, byte_offset); + let sync = br.read_bits(32)?; + if sync != AUX_SYNC_WORD { + return Err(Error::AuxSyncMismatch { found: sync }); + } + + // bAUXTimeStampFlag + optional 36-bit time stamp. + let time_stamp = if br.read_bit()? { + // Advance2Next4BitPos: align the cursor to the next 4-bit + // position (counted from the beginning of the core frame). + let misalign = (br.absolute_bit_position() % 4) as u32; + if misalign != 0 { + br.skip_bits(4 - misalign)?; + } + let ms_byte = br.read_bits(8)? as u64; + let marker = br.read_bits(4)? as u8; + if marker != AUX_TIME_STAMP_MARKER { + return Err(Error::AuxTimeStampMarkerMismatch { found: marker }); + } + let ls_28 = br.read_bits(28)? as u64; + let marker = br.read_bits(4)? as u8; + if marker != AUX_TIME_STAMP_MARKER { + return Err(Error::AuxTimeStampMarkerMismatch { found: marker }); + } + Some((ms_byte << 28) | ls_28) + } else { + None + }; + + // bAUXDynamCoeffFlag + optional coefficient table. + let dynamic_downmix = if br.read_bit()? { + let type_code = br.read_bits(3)? as u8; + let downmix_type = + DownmixType::from_code(type_code).expect("3-bit field covers all Table 5-32 codes"); + // DeriveNumDwnMixCodeCoeffs(): nPriCh = anNumCh[AMODE] (+1 + // when LFF > 0). + let base_channels = header + .channel_count() + .ok_or(Error::AuxChannelCountUnresolved { + amode: header.amode, + })?; + let mut input_channel_count = usize::from(base_channels); + if header.lfe.is_present() { + input_channel_count += 1; + } + let n_codes = downmix_type.output_channel_count() * input_channel_count; + let mut codes = Vec::with_capacity(n_codes); + for _ in 0..n_codes { + codes.push(br.read_bits(9)? as u16); + } + Some(DynamicDownmix { + downmix_type, + input_channel_count, + codes, + }) + } else { + None + }; + + // ByteAlign (0..7 zero bits) then the 16-bit nAUXCRC16. + let misalign = (br.absolute_bit_position() % 8) as u32; + if misalign != 0 { + br.skip_bits(8 - misalign)?; + } + // The CRC field starts here (byte-aligned); the protected region + // is "from positions bAUXTimeStampFlag to the byte prior to the + // start of the CRC inclusive" — bAUXTimeStampFlag begins right + // after the 4 sync bytes, so the covered span is the whole-byte + // window [offset + 4, crc_start). + let crc_start = br.absolute_bit_position() / 8; + let crc16 = br.read_bits(16)? as u16; + let crc_valid = dts_crc16(&frame[byte_offset + 4..crc_start]) == crc16; + + Ok(AuxData { + offset: byte_offset, + time_stamp, + dynamic_downmix, + crc16, + crc_valid, + }) +} + +/// Locate ([`find_aux_data`]) and parse ([`parse_aux_data_at`]) the +/// §5.7.1 Auxiliary Data chunk of a core frame, returning `Ok(None)` +/// when the frame carries no DWORD-aligned aux sync word. +/// +/// # Errors +/// +/// Propagates the [`parse_aux_data_at`] errors when a sync word is +/// found but the chunk does not parse. +pub fn parse_aux_data(frame: &[u8], header: &DtsFrameHeader) -> Result> { + match find_aux_data(frame) { + Some(offset) => parse_aux_data_at(frame, offset, header).map(Some), + None => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{synth_header, BitWriter}; + + /// AMODE 2 = stereo, 2 channels, no LFE. + fn stereo_header() -> DtsFrameHeader { + synth_header(2, 0) + } + + /// Build a frame: 32 bytes of non-sync filler, then the aux chunk + /// DWORD-aligned at offset 32. + fn frame_with_chunk(chunk: &[u8]) -> Vec { + let mut frame = vec![0x55u8; 32]; + frame.extend_from_slice(chunk); + frame + } + + fn empty_chunk() -> Vec { + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(0, 1); // no time stamp + w.push_bits(0, 1); // no dynamic coeffs + w.align(8); // ByteAlign + w.push_bits(0xBEEF, 16); // nAUXCRC16 + w.into_bytes() + } + + /// Overwrite the trailing 16-bit `nAUXCRC16` of a test chunk with + /// the correct Annex B value over the covered span (byte 4 through + /// the byte before the CRC — the test builders always emit the CRC + /// as the final two bytes). + fn patch_crc(chunk: &mut [u8]) { + let crc_start = chunk.len() - 2; + let crc = dts_crc16(&chunk[4..crc_start]); + chunk[crc_start..].copy_from_slice(&crc.to_be_bytes()); + } + + #[test] + fn parses_empty_chunk() { + let frame = frame_with_chunk(&empty_chunk()); + let header = stereo_header(); + let aux = parse_aux_data(&frame, &header).unwrap().unwrap(); + assert_eq!(aux.offset, 32); + assert_eq!(aux.time_stamp, None); + assert_eq!(aux.dynamic_downmix, None); + assert_eq!(aux.crc16, 0xBEEF); + // 0xBEEF is not the Annex B CRC of the covered span. + assert!(!aux.crc_valid); + } + + #[test] + fn crc_valid_when_check_word_matches() { + // The empty chunk's protected region is the single flag byte + // between the sync word and the CRC. + let mut chunk = empty_chunk(); + patch_crc(&mut chunk); + let frame = frame_with_chunk(&chunk); + let aux = parse_aux_data(&frame, &stereo_header()).unwrap().unwrap(); + assert_eq!(aux.crc16, dts_crc16(&chunk[4..chunk.len() - 2])); + assert!(aux.crc_valid); + } + + #[test] + fn crc_valid_over_downmix_payload() { + // A non-trivial protected region: flags + Table 5-32 type + + // four 9-bit codes + ByteAlign, verified end to end. + let codes: [u16; 4] = [0x100 | 241, 0x000, 216 + 1, 0x100 | 1]; + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(0, 1); // no time stamp + w.push_bits(1, 1); // dynamic coeffs present + w.push_bits(u64::from(DownmixType::LoRo.code()), 3); + for &c in &codes { + w.push_bits(u64::from(c), 9); + } + w.align(8); + w.push_bits(0, 16); // placeholder CRC + let mut chunk = w.into_bytes(); + patch_crc(&mut chunk); + let frame = frame_with_chunk(&chunk); + let aux = parse_aux_data(&frame, &stereo_header()).unwrap().unwrap(); + assert!(aux.crc_valid); + + // Any corruption inside the covered span must flip the verdict. + let mut corrupt = frame.clone(); + corrupt[32 + 5] ^= 0x40; // inside the coefficient region + let aux = parse_aux_data_at(&corrupt, 32, &stereo_header()).unwrap(); + assert!(!aux.crc_valid); + } + + #[test] + fn parses_time_stamp_with_markers_and_nibble_alignment() { + // Chunk at offset 32: sync ends at bit 32*8+32 (multiple of + // 4), the flag bit leaves the cursor 1 bit past a nibble, so + // Advance2Next4BitPos skips 3 bits. + let stamp: u64 = 0x8_1234_5678; // 36 bits + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(1, 1); // time stamp present + w.push_bits(0, 3); // Advance2Next4BitPos padding + w.push_bits((stamp >> 28) & 0xFF, 8); // nMSByte + w.push_bits(u64::from(AUX_TIME_STAMP_MARKER), 4); + w.push_bits(stamp & 0x0FFF_FFFF, 28); // nLSByte28 + w.push_bits(u64::from(AUX_TIME_STAMP_MARKER), 4); + w.push_bits(0, 1); // no dynamic coeffs + w.align(8); + w.push_bits(0x1234, 16); + let frame = frame_with_chunk(&w.into_bytes()); + let aux = parse_aux_data(&frame, &stereo_header()).unwrap().unwrap(); + assert_eq!(aux.time_stamp, Some(stamp)); + assert_eq!(aux.crc16, 0x1234); + } + + #[test] + fn rejects_bad_time_stamp_marker() { + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(1, 1); + w.push_bits(0, 3); + w.push_bits(0xAB, 8); + w.push_bits(0b0110, 4); // wrong marker + let frame = frame_with_chunk(&w.into_bytes()); + assert_eq!( + parse_aux_data(&frame, &stereo_header()), + Err(Error::AuxTimeStampMarkerMismatch { found: 0b0110 }) + ); + } + + #[test] + fn parses_loro_downmix_for_stereo_input() { + // Stereo (2 input channels, no LFE) folded Lo/Ro (2 output + // channels) -> 4 nine-bit codes, output-channel-major. + let codes: [u16; 4] = [ + 0x100 | 241, // +unity (Lo <- L) + 0x000, // 0.0 (Lo <- R) + 216 + 1, // -1/sqrt2 (Ro <- L), one-biased index 216 + 0x100 | 1, // +DmixTable[0] (Ro <- R) + ]; + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(0, 1); // no time stamp + w.push_bits(1, 1); // dynamic coeffs present + w.push_bits(u64::from(DownmixType::LoRo.code()), 3); + for &c in &codes { + w.push_bits(u64::from(c), 9); + } + w.align(8); + w.push_bits(0xCAFE, 16); + let frame = frame_with_chunk(&w.into_bytes()); + let aux = parse_aux_data(&frame, &stereo_header()).unwrap().unwrap(); + let dmix = aux.dynamic_downmix.expect("downmix present"); + assert_eq!(dmix.downmix_type, DownmixType::LoRo); + assert_eq!(dmix.input_channel_count, 2); + assert_eq!(dmix.output_channel_count(), 2); + assert_eq!(dmix.codes, codes); + let m = dmix.coefficient_matrix().unwrap(); + assert_eq!(m[0][0], 1.0); + assert_eq!(m[0][1], 0.0); + assert!((m[1][0] - (-(23170.0 / 32768.0))).abs() < 1e-12); + assert!((m[1][1] - 33.0 / 32768.0).abs() < 1e-12); + assert_eq!(aux.crc16, 0xCAFE); + } + + #[test] + fn lfe_bearing_frame_adds_one_input_channel() { + // DeriveNumDwnMixCodeCoeffs(): "if (LFF > 0) nPriCh++;" — + // stereo + LFE folded 1/0 needs 3 codes, not 2. The 2-bit + // `LFF` field sits one bit above `predictor_history` in the + // 13-bit flag window. + let header = synth_header(2, 0b10 << 1); + assert!(header.lfe.is_present()); + let codes: [u16; 3] = [0x100 | 241, 0x100 | 241, 0x000]; + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(0, 1); // no time stamp + w.push_bits(1, 1); // dynamic coeffs present + w.push_bits(u64::from(DownmixType::OneZero.code()), 3); + for &c in &codes { + w.push_bits(u64::from(c), 9); + } + w.align(8); + w.push_bits(0x5A5A, 16); + let frame = frame_with_chunk(&w.into_bytes()); + let aux = parse_aux_data(&frame, &header).unwrap().unwrap(); + let dmix = aux.dynamic_downmix.expect("downmix present"); + assert_eq!(dmix.input_channel_count, 3); + assert_eq!(dmix.output_channel_count(), 1); + assert_eq!(dmix.codes, codes); + assert_eq!(aux.crc16, 0x5A5A); + } + + #[test] + fn unused_downmix_type_carries_no_codes() { + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(0, 1); + w.push_bits(1, 1); + w.push_bits(u64::from(DownmixType::Unused.code()), 3); + w.align(8); + w.push_bits(0x0001, 16); + let frame = frame_with_chunk(&w.into_bytes()); + let aux = parse_aux_data(&frame, &stereo_header()).unwrap().unwrap(); + let dmix = aux.dynamic_downmix.expect("flag was set"); + assert_eq!(dmix.downmix_type, DownmixType::Unused); + assert_eq!(dmix.output_channel_count(), 0); + assert!(dmix.codes.is_empty()); + assert_eq!(dmix.coefficient_matrix().unwrap(), Vec::>::new()); + } + + #[test] + fn find_ignores_unaligned_sync() { + // Sync word at byte offset 33 (not DWORD-aligned) must not + // match; the same word at offset 36 must. + let mut frame = vec![0x55u8; 33]; + frame.extend_from_slice(&AUX_SYNC_WORD.to_be_bytes()); + assert_eq!(find_aux_data(&frame), None); + + let mut frame = vec![0x55u8; 36]; + frame.extend_from_slice(&AUX_SYNC_WORD.to_be_bytes()); + assert_eq!(find_aux_data(&frame), Some(36)); + } + + #[test] + fn find_searches_backward_from_frame_end() { + // Two aligned sync words: the backward search returns the + // later one, per the spec's "from the end of the audio frame". + let mut frame = Vec::new(); + frame.extend_from_slice(&AUX_SYNC_WORD.to_be_bytes()); + frame.extend_from_slice(&[0u8; 12]); + frame.extend_from_slice(&AUX_SYNC_WORD.to_be_bytes()); + frame.extend_from_slice(&[0u8; 4]); + assert_eq!(find_aux_data(&frame), Some(16)); + } + + #[test] + fn parse_at_rejects_wrong_sync() { + let frame = vec![0u8; 8]; + assert_eq!( + parse_aux_data_at(&frame, 0, &stereo_header()), + Err(Error::AuxSyncMismatch { found: 0 }) + ); + } + + #[test] + fn truncated_chunk_reports_eof() { + // Sync + flags but no room for the CRC. + let mut w = BitWriter::new(); + w.push_bits(u64::from(AUX_SYNC_WORD), 32); + w.push_bits(0, 1); + w.push_bits(0, 1); + let bytes = w.into_bytes(); + let frame = frame_with_chunk(&bytes[..bytes.len()]); + assert_eq!( + parse_aux_data(&frame, &stereo_header()), + Err(Error::UnexpectedEof) + ); + } + + #[test] + fn dynamic_downmix_code_accessor_bounds() { + let dmix = DynamicDownmix { + downmix_type: DownmixType::OneZero, + input_channel_count: 2, + codes: vec![0x100 | 241, 0x100 | 241], + }; + assert_eq!(dmix.code(0, 0), Some(0x100 | 241)); + assert_eq!(dmix.code(0, 2), None); + assert_eq!(dmix.code(1, 0), None); + assert!(dmix.coefficient(1, 0).is_err()); + } + + #[test] + fn apply_planar_folds_and_truncates() { + // Stereo -> mono with coefficients (+1.0, -1/sqrt(2)). + let dmix = DynamicDownmix { + downmix_type: DownmixType::OneZero, + input_channel_count: 2, + codes: vec![0x100 | 241, 216 + 1], + }; + let input = vec![vec![1000, -1000, 0], vec![1000, 1000, 32768]]; + let out = dmix.apply_planar(&input).unwrap(); + assert_eq!(out.len(), 1); + let c: f64 = 23170.0 / 32768.0; + // 1000 - c*1000 = 292.9 -> 292 (truncate toward zero). + assert_eq!(out[0][0], (1000.0 - c * 1000.0).trunc() as i32); + // -1000 - c*1000 = -1707.09 -> -1707 (not -1708). + assert_eq!(out[0][1], (-1000.0 - c * 1000.0).trunc() as i32); + assert_eq!(out[0][2], (-c * 32768.0).trunc() as i32); + } + + #[test] + fn apply_planar_rejects_shape_mismatch() { + let dmix = DynamicDownmix { + downmix_type: DownmixType::OneZero, + input_channel_count: 2, + codes: vec![0x100 | 241, 0x100 | 241], + }; + // Wrong plane count. + assert_eq!( + dmix.apply_planar(&[vec![0i32; 4]]), + Err(Error::DownmixInputShapeMismatch { + expected: 2, + found: 1 + }) + ); + // Unequal plane lengths. + assert_eq!( + dmix.apply_planar(&[vec![0i32; 4], vec![0i32; 3]]), + Err(Error::DownmixInputShapeMismatch { + expected: 4, + found: 3 + }) + ); + } + + #[test] + fn downmix_type_round_trips_all_codes() { + for code in 0..8u8 { + let t = DownmixType::from_code(code).unwrap(); + assert_eq!(t.code(), code); + } + assert_eq!(DownmixType::from_code(8), None); + } +} diff --git a/crates/vendor/oxideav-dts/src/bitreader.rs b/crates/vendor/oxideav-dts/src/bitreader.rs new file mode 100644 index 00000000..e1cf99f2 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/bitreader.rs @@ -0,0 +1,192 @@ +//! Minimal MSB-first bit reader for parsing the DTS frame sync +//! header. +//! +//! Round 1 only needs to walk roughly 100 bits of header from a +//! byte buffer, so this reader is deliberately small: no buffering, +//! no slicing, no skip-to-alignment helpers. The DTS bitstream is +//! defined MSB-first within each byte (per the wiki snapshot at +//! `docs/audio/dts/wiki/DTS.wiki`, which mirrors the ETSI spec's +//! convention). +//! +//! All reads return [`Result`]; the only failure mode is running +//! past the end of the buffer. + +use crate::{Error, Result}; + +/// MSB-first bit reader over a borrowed byte slice. +/// +/// Tracks the current bit position (`pos` is the index of the next +/// bit to read, counted from the MSB of `bytes[0]`). +#[derive(Debug)] +pub(crate) struct BitReader<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl<'a> BitReader<'a> { + /// Construct a fresh reader positioned at bit 0. Only used by + /// the in-module unit tests; the production header parser + /// always starts at a byte-offset after the syncword via + /// [`Self::from_byte_offset`]. + #[cfg(test)] + pub(crate) fn new(bytes: &'a [u8]) -> Self { + BitReader { bytes, pos: 0 } + } + + /// Construct a reader that begins at an arbitrary byte offset. + /// Used after the syncword is identified so the header read can + /// resume from the byte immediately following the sync. + pub(crate) fn from_byte_offset(bytes: &'a [u8], byte_offset: usize) -> Self { + BitReader { + bytes, + pos: byte_offset * 8, + } + } + + /// Read `n` bits (1..=32) as an unsigned big-endian integer. + /// + /// Returns [`Error::UnexpectedEof`] if the read would walk past + /// the end of the underlying buffer. + pub(crate) fn read_bits(&mut self, n: u32) -> Result { + debug_assert!((1..=32).contains(&n), "BitReader::read_bits expects 1..=32"); + let end = self.pos + n as usize; + if end > self.bytes.len() * 8 { + return Err(Error::UnexpectedEof); + } + let mut value: u32 = 0; + let mut remaining = n; + while remaining > 0 { + let byte_idx = self.pos / 8; + let bit_in_byte = self.pos % 8; + // Bits available in this byte, MSB-first. + let avail = (8 - bit_in_byte) as u32; + let take = remaining.min(avail); + // Shift the byte so the MSB of the bit window is at + // bit position 7, then mask + shift down to bit 0 of + // the take-bit field. + let byte = self.bytes[byte_idx] as u32; + let shifted = byte >> (avail - take); + let mask = (1u32 << take) - 1; + let chunk = shifted & mask; + value = (value << take) | chunk; + self.pos += take as usize; + remaining -= take; + } + Ok(value) + } + + /// Read a single bit and return it as a `bool`. + pub(crate) fn read_bit(&mut self) -> Result { + Ok(self.read_bits(1)? == 1) + } + + /// Current absolute bit position. + #[cfg(test)] + pub(crate) fn position_bits(&self) -> usize { + self.pos + } + + /// Current absolute bit position, counted from the MSB of + /// `bytes[0]`. Round-195 side-info decoders use this to report + /// `bits_consumed` back to the caller (so the caller can advance + /// its own bit cursor through the side-information block). + pub(crate) fn absolute_bit_position(&self) -> usize { + self.pos + } + + /// Borrow the backing byte buffer. The round-340 §5.5 audio-array + /// walk uses this to bridge a running reader into the byte-offset + /// [`crate::audio_huff::decode_audio_huff_at`] entry point (which + /// re-seeks over the same buffer) and then re-advance this reader. + pub(crate) fn backing_bytes(&self) -> &'a [u8] { + self.bytes + } + + /// Advance the reader by `n` bits without materialising a value, + /// returning [`Error::UnexpectedEof`] if the skip would walk past + /// the end of the buffer. Used by the round-340 §5.5 audio-array + /// walk after a Huffman index was decoded through the byte-offset + /// entry point. + pub(crate) fn skip_bits(&mut self, n: u32) -> Result<()> { + let end = self.pos + n as usize; + if end > self.bytes.len() * 8 { + return Err(Error::UnexpectedEof); + } + self.pos = end; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_single_bits() { + // 0b1011_0010 = 0xB2 + let mut br = BitReader::new(&[0xB2]); + assert!(br.read_bit().unwrap()); + assert!(!br.read_bit().unwrap()); + assert!(br.read_bit().unwrap()); + assert!(br.read_bit().unwrap()); + assert!(!br.read_bit().unwrap()); + assert!(!br.read_bit().unwrap()); + assert!(br.read_bit().unwrap()); + assert!(!br.read_bit().unwrap()); + assert_eq!(br.position_bits(), 8); + } + + #[test] + fn read_multi_byte_field() { + // 0x7F 0xFE 0x80 0x01 — the DTS BE syncword. + let bytes = [0x7F, 0xFE, 0x80, 0x01]; + let mut br = BitReader::new(&bytes); + assert_eq!(br.read_bits(32).unwrap(), 0x7FFE_8001); + assert_eq!(br.position_bits(), 32); + } + + #[test] + fn read_crosses_byte_boundary() { + // Bytes 0xFF 0xF0 → top 12 bits = 0xFFF. + let bytes = [0xFF, 0xF0]; + let mut br = BitReader::new(&bytes); + assert_eq!(br.read_bits(12).unwrap(), 0xFFF); + // remaining 4 bits = 0. + assert_eq!(br.read_bits(4).unwrap(), 0); + } + + #[test] + fn read_from_byte_offset_skips_sync() { + // Skip the 4 sync bytes and read a 1-bit flag from byte 4. + let bytes = [0x7F, 0xFE, 0x80, 0x01, 0b1000_0000]; + let mut br = BitReader::from_byte_offset(&bytes, 4); + assert!(br.read_bit().unwrap()); + assert_eq!(br.position_bits(), 33); + } + + #[test] + fn read_past_end_returns_eof() { + let mut br = BitReader::new(&[0xAA]); + assert!(br.read_bits(8).is_ok()); + assert_eq!(br.read_bits(1).unwrap_err(), Error::UnexpectedEof); + } + + #[test] + fn read_field_at_arbitrary_bit_alignment() { + // Construct a stream whose first 7 bits are 0b1010101 and + // whose next 14 bits are 0b00110011001100 = 0x0CCC. + // First byte: 0b1010101_0 | next 6 bits of 0b001100 -> 0b1010_1010 + // 0b1010101 (7 bits) + first bit of next field (0) = 0b1010_1010 = 0xAA + // next 8 bits (bits 8..16) = top 8 bits of remaining 13 field bits + // remaining 13 bits = 0b0110011001100 + // wait: original 14 bits = 0b00_1100_1100_1100 = 0x0CCC + // first bit already consumed = 0, so 13 bits left = 0b0110011001100 + // That's bits 8..16 = 0b01100110 and bits 16..21 = 0b01100 + // So encoded bytes = 0b1010_1010, 0b0110_0110, 0b0110_0xxx (pad). + let bytes = [0xAA, 0x66, 0b0110_0000]; + let mut br = BitReader::new(&bytes); + assert_eq!(br.read_bits(7).unwrap(), 0b1010101); + assert_eq!(br.read_bits(14).unwrap(), 0x0CCC); + assert_eq!(br.position_bits(), 21); + } +} diff --git a/crates/vendor/oxideav-dts/src/block_code.rs b/crates/vendor/oxideav-dts/src/block_code.rs new file mode 100644 index 00000000..b0ba0cf7 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/block_code.rs @@ -0,0 +1,544 @@ +//! DTS Coherent Acoustics — §C.2.1 Block Code. +//! +//! Round 232 (2026-06-04) lands the §C.2.1 block-code decoder, the +//! prerequisite step that turns a single multi-symbol code word into +//! the array of quantisation indices the rest of the §C.2 chain +//! (§C.2.2 inverse ADPCM, §C.2.3 joint subband, §C.2.4 sum/difference, +//! and downstream stages) consumes. Each "block" packs `n_elements` +//! quantisation indices from a `n_levels`-level alphabet into one +//! integer code word using mixed-radix arithmetic. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), Annex C (informative) +//! §C.2.1 "Block Code" (staged PDF p.182–183) at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. The +//! spec gives two decoder variants: +//! +//! 1. A **table look-up** decoder that walks a rearranged §D.6 +//! code-book row by row, subtracting the largest entry that fits +//! and recording the row index. +//! 2. A **modulus / integer-division** decoder that does the same +//! extraction with one `nCode % nNumLevel` + one +//! `nCode /= nNumLevel` per quantisation index, no table needed. +//! +//! Both produce the same quantisation-index array, ordered +//! "first-element first" (i.e. element 0 is the **first** +//! quantisation index extracted, element `n_elements - 1` is the +//! last). The spec's reproduced normative pseudocode for the +//! modulus/division variant reads (reproduced as documented): +//! +//! ```text +//! int DecodeBlockCode(int nCode, int *pnValue) { +//! // nCode: Input code to be decoded. +//! // nNumElement: Number of elements (samples) encoded in a block. +//! // nNumLevel: Number of quantization levels. +//! // *pnValue: Array of decoded sample values. +//! nOffset = (nNumLevel-1)>>1; +//! for (int n=0; n< nNumElement; n++) { +//! pnValue[n] = (nCode % nNumLevel) - nOffset; +//! nCode /= nNumLevel; +//! } +//! if ( nCode == 0 ) return 1; +//! else { printf("ERROR: block code lock-up fail.\n"); return NULL; } +//! } +//! ``` +//! +//! Worked example reproduced from the spec text: the three-level +//! four-element block-code `nCode = 64` decodes element-by-element +//! as +//! +//! | step | reads | quotient | remainder | index | +//! |------|----------------|----------|-----------|-------------------| +//! | 1 | `64 = 3·21+1` | `21` | `1` | `1 - 1 = 0` | +//! | 2 | `21 = 3·7+0` | `7` | `0` | `0 - 1 = -1` | +//! | 3 | `7 = 3·2+1` | `2` | `1` | `1 - 1 = 0` | +//! | 4 | `2 = 3·0+2` | `0` | `2` | `2 - 1 = +1` | +//! +//! producing the quantisation-index array `[0, -1, 0, +1]`. (The +//! spec text walks the same example for both decoder variants and +//! records the identical output.) +//! +//! # Mid-range offset +//! +//! `nOffset = (nNumLevel - 1) >> 1` centres the index alphabet on +//! zero: a 3-level alphabet uses indices `{-1, 0, +1}` (`offset = +//! 1`), a 5-level alphabet `{-2, -1, 0, +1, +2}` (`offset = 2`), a +//! 7-level alphabet `{-3, -2, -1, 0, +1, +2, +3}` (`offset = 3`), +//! etc. The spec's `(nNumLevel - 1) >> 1` form is integer +//! arithmetic (right shift by one); for any odd `n_levels` it equals +//! `(n_levels - 1) / 2`. The §C.2.1 spec text only worked-examples +//! odd-level alphabets (3, 5, 7, 9, 13, 17, 25 per the §D.6 +//! sub-clauses); even-level alphabets are not enumerated as +//! block-code variants. +//! +//! # End-of-code consistency check +//! +//! After consuming all `n_elements` quotient steps, the §C.2.1 +//! pseudocode's success criterion is `nCode == 0`. A non-zero +//! residual means either the input code word was out-of-range for +//! the declared `(n_elements, n_levels)` block, or one of the +//! parameters was wrong. The Rust API surfaces this as a recoverable +//! [`Error::BlockCodeResidual`] rather than the spec's +//! `printf + return NULL`. +//! +//! # Scope +//! +//! This round lands the modulus / integer-division decoder +//! ([`decode_block_code`]) plus the dispatch predicate / accessor +//! surface. The table-look-up decoder variant requires the §D.6 +//! "rearranged" code-book rows enumerated as Table C-1 (3-level +//! 4-element), with parallel rearranged tables for 5/7/9/13/17/25 +//! levels not transcribed into this crate yet — left to a follow-up +//! round once the §D.6 + Table C-1 tables are extracted. +//! +//! Both decoders produce the same quantisation-index array per the +//! spec's worked example, so the table variant is purely an +//! optimisation; the modulus/division variant is sufficient to +//! decode the entire §C.2.1 surface end-to-end. +//! +//! # Arithmetic +//! +//! The §C.2.1 pseudocode uses plain C `int` arithmetic for the +//! quantisation-index offset (`(nCode % nNumLevel) - nOffset`); the +//! quantisation indices are small signed values +//! (`-(n_levels-1)/2..=(n_levels-1)/2`) so overflow is not a +//! concern. The Rust impl works in `i32` for the indices and `u32` +//! for the code-word arithmetic (the modulus / integer-division +//! steps); the spec does not bound the code-word width, but the +//! §D.6.1 worked example fits within seven bits (`64`) and the +//! larger-level §D.6.x tables fit within the spec's documented bit +//! widths for those clauses (cited in §5.4 / §6 unpack routines). + +use crate::{Error, Result}; + +/// Decode one §C.2.1 block-code word into its quantisation-index +/// array, in place. +/// +/// On entry: +/// +/// - `code` is the unsigned block-code word read from the bit +/// stream. +/// - `n_levels` is the quantisation-level count of the alphabet +/// (`nNumLevel` in the spec). Must be `>= 2`. +/// - `output` is the destination quantisation-index array. The +/// spec's `nNumElement` is taken from `output.len()`. On return, +/// `output[0..output.len()]` carries the decoded quantisation +/// indices, ordered first-element-first. +/// +/// On return: +/// +/// - `Ok(())` if every quantisation index extracted cleanly and +/// the residual code word reached zero after the last element +/// (the §C.2.1 success criterion `nCode == 0`). +/// +/// # Errors +/// +/// - [`Error::BlockCodeLevelsOutOfRange`] if `n_levels < 2`. A +/// one-level alphabet has only the index `0` and cannot encode +/// information; the spec's `(nNumLevel-1)>>1` offset and +/// `nCode % nNumLevel` recurrence are not defined for `n_levels +/// == 0` or `n_levels == 1` (the latter would cause `nCode /= +/// nNumLevel` to never advance). +/// - [`Error::BlockCodeResidual`] if, after walking all +/// `output.len()` elements, the residual code word is non-zero. +/// The §C.2.1 spec text treats this as a fatal "ERROR: block +/// code look-up fail" condition; the Rust API surfaces it as a +/// recoverable error so callers can distinguish a corrupted +/// bit-stream segment from a structural decoder bug. +/// +/// An empty `output` slice is **not** an error: the §C.2.1 +/// pseudocode's success condition is `nCode == 0` regardless of +/// element count, so the zero-element decode succeeds when (and +/// only when) `code == 0`. +/// +/// # Example +/// +/// Spec worked example (§C.2.1 PDF p.182): +/// +/// ```rust +/// use oxideav_dts::decode_block_code; +/// +/// let mut out = [0_i32; 4]; +/// decode_block_code(64, 3, &mut out).unwrap(); +/// // 3-level 4-element block code: `64` decodes to (0, -1, 0, +1). +/// assert_eq!(out, [0, -1, 0, 1]); +/// ``` +pub fn decode_block_code(code: u32, n_levels: u32, output: &mut [i32]) -> Result<()> { + if n_levels < 2 { + return Err(Error::BlockCodeLevelsOutOfRange { n_levels }); + } + // The spec writes `nOffset = (nNumLevel - 1) >> 1`. For + // `n_levels >= 2` this fits in i32 because `n_levels` would + // have to exceed 2^31 + 1 to overflow, and the §C.2.1 block-code + // alphabets enumerated by §D.6 cap at 25 levels. + let offset = ((n_levels - 1) >> 1) as i32; + let mut residual = code; + for slot in output.iter_mut() { + // `nCode % nNumLevel` — the index of this element within + // the alphabet, biased by `nOffset` so the alphabet is + // centred on zero. + let remainder = (residual % n_levels) as i32; + *slot = remainder - offset; + // `nCode /= nNumLevel` — shift to the next element's + // mixed-radix digit. + residual /= n_levels; + } + // §C.2.1 success criterion: after consuming all elements, the + // residual code word must equal zero. A non-zero residual means + // the input code word was out-of-range for the declared + // (n_elements, n_levels) block. + if residual != 0 { + return Err(Error::BlockCodeResidual { + residual, + n_elements: output.len(), + n_levels, + }); + } + Ok(()) +} + +/// Compute the §C.2.1 mid-range offset `nOffset = (nNumLevel - 1) >> +/// 1` for the given quantisation-level count. +/// +/// This is the bias applied to each `(nCode % nNumLevel)` remainder +/// so the decoded quantisation-index alphabet is centred on zero +/// (`{-(n_levels-1)/2, ..., 0, ..., (n_levels-1)/2}`). Exposed as a +/// public helper for callers that need to size index buffers or +/// validate alphabet ranges by the same invariant the spec writes +/// against. +/// +/// Returns the offset for any `n_levels >= 1`. For `n_levels == 0` +/// the offset is mathematically undefined and the function returns +/// `0` (the integer-arithmetic interpretation of `(0 - 1) >> 1` +/// after an unsigned `wrapping_sub` would be `u32::MAX >> 1`, which +/// is not what the spec means); guard `n_levels == 0` at the call +/// site if a numeric answer is required. +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::block_code_offset; +/// +/// // 3-level: indices in {-1, 0, +1} → offset 1 +/// assert_eq!(block_code_offset(3), 1); +/// // 5-level: indices in {-2, -1, 0, +1, +2} → offset 2 +/// assert_eq!(block_code_offset(5), 2); +/// // 7-level: indices in {-3, -2, -1, 0, +1, +2, +3} → offset 3 +/// assert_eq!(block_code_offset(7), 3); +/// // 25-level (largest in §D.6): offset 12 +/// assert_eq!(block_code_offset(25), 12); +/// ``` +pub fn block_code_offset(n_levels: u32) -> i32 { + if n_levels == 0 { + 0 + } else { + ((n_levels - 1) >> 1) as i32 + } +} + +/// Compute the maximum code-word value the §C.2.1 block decoder +/// will accept without a [`Error::BlockCodeResidual`] error, for the +/// given `(n_elements, n_levels)` block dimensions. +/// +/// The mixed-radix encoding represents each element as a digit in +/// base `n_levels`, so an `n_elements`-element block uses values in +/// `0..n_levels.pow(n_elements as u32)`. The largest valid code word +/// is therefore `n_levels.pow(n_elements as u32) - 1`. Returns +/// `None` when the exponentiation overflows `u32` (which the §D.6 +/// tables stay within: even 25-level × 1-element fits in five bits, +/// and the largest tabulated block-size product fits well inside +/// 32 bits). +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::block_code_max_code; +/// +/// // §D.6.1 3-level 4-element block code: 3^4 - 1 = 80 +/// assert_eq!(block_code_max_code(4, 3), Some(80)); +/// // 5-level 3-element: 5^3 - 1 = 124 +/// assert_eq!(block_code_max_code(3, 5), Some(124)); +/// ``` +pub fn block_code_max_code(n_elements: usize, n_levels: u32) -> Option { + if n_elements == 0 { + return Some(0); + } + let exp = u32::try_from(n_elements).ok()?; + let total = n_levels.checked_pow(exp)?; + Some(total.saturating_sub(1)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// §C.2.1 worked example (PDF p.182): the three-level + /// four-element block-code `nCode = 64` decodes to the + /// quantisation-index array `(0, -1, 0, +1)` ordered + /// first-element-first. + #[test] + fn spec_worked_example_three_level_four_element_code_sixty_four() { + let mut out = [0_i32; 4]; + decode_block_code(64, 3, &mut out).unwrap(); + assert_eq!(out, [0, -1, 0, 1]); + } + + /// All-zero code word decodes to the all-`-offset` (bottom of + /// alphabet) quantisation-index array: every digit is the + /// remainder `0 % n_levels = 0`, biased by `-offset`. For a + /// 3-level alphabet (offset 1) this is `-1`; for a 5-level + /// alphabet (offset 2) it is `-2`; etc. + #[test] + fn zero_code_decodes_to_bottom_of_alphabet() { + for (n_elements, n_levels) in [(1, 3), (4, 3), (1, 5), (3, 5), (4, 7), (3, 25)] { + let mut out = vec![i32::MAX; n_elements]; + decode_block_code(0, n_levels, &mut out).unwrap(); + let expected = -block_code_offset(n_levels); + assert!( + out.iter().all(|&v| v == expected), + "n_elements={n_elements} n_levels={n_levels} expected all={expected} got {out:?}" + ); + } + } + + /// Maximum valid code word for `(n_elements, n_levels)` decodes + /// to the all-`+offset` quantisation-index array (every digit + /// is the largest in the alphabet). + #[test] + fn max_code_decodes_to_top_of_alphabet() { + // 3-level 4-element: max code = 3^4 - 1 = 80 + let mut out = [0_i32; 4]; + decode_block_code(80, 3, &mut out).unwrap(); + assert_eq!(out, [1, 1, 1, 1]); + // 5-level 3-element: max code = 5^3 - 1 = 124 + let mut out = [0_i32; 3]; + decode_block_code(124, 5, &mut out).unwrap(); + assert_eq!(out, [2, 2, 2]); + } + + /// One past the maximum valid code word produces a non-zero + /// residual and surfaces [`Error::BlockCodeResidual`]: §C.2.1 + /// success requires `nCode == 0` after the last extraction. + #[test] + fn over_max_code_produces_residual_error() { + // 3-level 4-element: 81 = 3^4. After 4 elements the + // residual is 1 (digit-array carry). + let mut out = [0_i32; 4]; + let err = decode_block_code(81, 3, &mut out).unwrap_err(); + assert!(matches!( + err, + Error::BlockCodeResidual { + residual: 1, + n_elements: 4, + n_levels: 3, + } + )); + } + + /// `n_levels < 2` is rejected: a one-level alphabet has only + /// the index `0` and the spec's mixed-radix recurrence is + /// undefined (division by zero / one degenerates to an + /// infinite loop on any non-zero `code`). + #[test] + fn levels_less_than_two_rejected() { + let mut out = [0_i32; 4]; + assert!(matches!( + decode_block_code(0, 0, &mut out).unwrap_err(), + Error::BlockCodeLevelsOutOfRange { n_levels: 0 } + )); + assert!(matches!( + decode_block_code(0, 1, &mut out).unwrap_err(), + Error::BlockCodeLevelsOutOfRange { n_levels: 1 } + )); + } + + /// Empty `output` slice with `code == 0` succeeds (the §C.2.1 + /// success criterion `nCode == 0` is met trivially without + /// extracting any digits). + #[test] + fn empty_output_succeeds_when_code_is_zero() { + let mut out: [i32; 0] = []; + decode_block_code(0, 3, &mut out).unwrap(); + } + + /// Empty `output` slice with `code != 0` surfaces the residual + /// error: no extraction step ran, so the residual is the full + /// input code word. + #[test] + fn empty_output_with_non_zero_code_residual_error() { + let mut out: [i32; 0] = []; + let err = decode_block_code(42, 3, &mut out).unwrap_err(); + assert!(matches!( + err, + Error::BlockCodeResidual { + residual: 42, + n_elements: 0, + n_levels: 3, + } + )); + } + + /// Round-trip: every valid `code ∈ [0, n_levels.pow(n_elements))` + /// for a small `(n_elements, n_levels)` block decodes to a + /// distinct quantisation-index array, and re-encoding the array + /// via the mixed-radix base recovers the original `code`. This + /// exercises the entire valid-code-word domain. + #[test] + fn round_trip_three_level_four_element_block() { + for code in 0..81_u32 { + let mut out = [0_i32; 4]; + decode_block_code(code, 3, &mut out).unwrap(); + let offset = block_code_offset(3); + // Re-encode least-significant-first (matches the + // decoder's first-element-first extraction). + let mut recovered = 0_u32; + for &idx in out.iter().rev() { + recovered = recovered * 3 + (idx + offset) as u32; + } + assert_eq!(recovered, code, "round-trip failed for code={code}"); + } + } + + /// Round-trip for the 5-level 3-element block-code domain. + #[test] + fn round_trip_five_level_three_element_block() { + for code in 0..125_u32 { + let mut out = [0_i32; 3]; + decode_block_code(code, 5, &mut out).unwrap(); + let offset = block_code_offset(5); + let mut recovered = 0_u32; + for &idx in out.iter().rev() { + recovered = recovered * 5 + (idx + offset) as u32; + } + assert_eq!(recovered, code, "round-trip failed for code={code}"); + } + } + + /// Every decoded index falls within the §C.2.1 alphabet bounds + /// `[-(n_levels - 1) / 2, (n_levels - 1) / 2]`. Exhaustive over + /// the 3-level 4-element domain. + #[test] + fn decoded_indices_within_alphabet_bounds() { + let n_levels = 3_u32; + let offset = block_code_offset(n_levels); + for code in 0..81_u32 { + let mut out = [0_i32; 4]; + decode_block_code(code, n_levels, &mut out).unwrap(); + for v in out { + assert!( + (-offset..=offset).contains(&v), + "code={code} produced out-of-alphabet index {v}" + ); + } + } + } + + /// `block_code_offset` matches the spec's `(n_levels - 1) >> 1` + /// integer-shift formula across the §D.6 enumerated alphabets. + #[test] + fn offset_helper_matches_spec_formula() { + for n_levels in [3_u32, 5, 7, 9, 13, 17, 25] { + assert_eq!(block_code_offset(n_levels), ((n_levels - 1) >> 1) as i32); + } + } + + /// `block_code_offset(0)` is `0` (guarded fallback), and + /// `block_code_offset(1)` is `0` (the single-element alphabet + /// is just the zero index). + #[test] + fn offset_helper_degenerate_alphabets() { + assert_eq!(block_code_offset(0), 0); + assert_eq!(block_code_offset(1), 0); + assert_eq!(block_code_offset(2), 0); + } + + /// `block_code_max_code` matches `n_levels.pow(n_elements) - 1` + /// for the §D.6 enumerated dimensions and reports `None` on + /// overflow. + #[test] + fn max_code_helper_matches_formula() { + // §D.6.1 3-level 4-element: 80 + assert_eq!(block_code_max_code(4, 3), Some(80)); + // 5-level 3-element: 124 + assert_eq!(block_code_max_code(3, 5), Some(124)); + // 7-level 2-element: 48 + assert_eq!(block_code_max_code(2, 7), Some(48)); + // 25-level 1-element: 24 + assert_eq!(block_code_max_code(1, 25), Some(24)); + // Zero-element block: max code is 0 (only the empty + // decode succeeds). + assert_eq!(block_code_max_code(0, 3), Some(0)); + } + + /// Worked-example trace — the §C.2.1 PDF p.182 step-by-step + /// table for `nCode = 64` (3-level 4-element) records + /// quotient/remainder pairs `(21, 1)`, `(7, 0)`, `(2, 1)`, + /// `(0, 2)` producing indices `(0, -1, 0, +1)`. Verify each + /// intermediate quotient by stepping the recurrence manually. + #[test] + fn spec_worked_example_intermediate_quotients() { + let n_levels = 3_u32; + let offset = block_code_offset(n_levels); + let mut residual = 64_u32; + let expected = [(1_u32, 21_u32), (0, 7), (1, 2), (2, 0)]; + for (i, &(rem, next_q)) in expected.iter().enumerate() { + assert_eq!( + residual % n_levels, + rem, + "step {i} expected remainder {rem}" + ); + let idx = (residual % n_levels) as i32 - offset; + // Expected indices from the spec's worked example. + assert_eq!(idx, [0, -1, 0, 1][i], "step {i} index disagrees"); + residual /= n_levels; + assert_eq!(residual, next_q, "step {i} expected next quotient {next_q}"); + } + assert_eq!(residual, 0); + } + + /// Three-level 1-element block decode (smallest non-trivial + /// alphabet × single element). Codes `0/1/2` decode to indices + /// `-1/0/+1`. + #[test] + fn three_level_one_element_block() { + for (code, expected) in [(0_u32, -1_i32), (1, 0), (2, 1)] { + let mut out = [0_i32; 1]; + decode_block_code(code, 3, &mut out).unwrap(); + assert_eq!(out, [expected], "code={code} expected index {expected}"); + } + // Code = 3 produces residual 1 after one element. + let mut out = [0_i32; 1]; + let err = decode_block_code(3, 3, &mut out).unwrap_err(); + assert!(matches!( + err, + Error::BlockCodeResidual { + residual: 1, + n_elements: 1, + n_levels: 3, + } + )); + } + + /// `n_levels == 2` is the smallest accepted alphabet. Index + /// alphabet is `{0, 1}` (offset = 0), and the recurrence + /// reads the code's bits LSB-first. + #[test] + fn two_level_block_decodes_binary() { + // 4-bit code 0b1011 = 11 decodes to bits LSB-first. + let mut out = [0_i32; 4]; + decode_block_code(0b1011, 2, &mut out).unwrap(); + assert_eq!(out, [1, 1, 0, 1]); + assert_eq!(block_code_offset(2), 0); + } + + /// 25-level 1-element decode — the largest §D.6 alphabet. + /// Codes `0..25` decode to indices `-12..=12`. + #[test] + fn twenty_five_level_single_element_alphabet() { + let offset = block_code_offset(25); + for code in 0..25_u32 { + let mut out = [0_i32; 1]; + decode_block_code(code, 25, &mut out).unwrap(); + assert_eq!(out[0], code as i32 - offset); + } + } +} diff --git a/crates/vendor/oxideav-dts/src/cos_mod.rs b/crates/vendor/oxideav-dts/src/cos_mod.rs new file mode 100644 index 00000000..057c48c8 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/cos_mod.rs @@ -0,0 +1,790 @@ +//! Cosine-modulation coefficient matrix for the DTS Core 32-band +//! synthesis QMF filterbank. +//! +//! Transcribed verbatim from `docs/audio/dts/dts-core-extracts.md` +//! §2.3 ("Cosine-modulation coefficient definition (§C.2.5, PDF +//! p.184)"), which in turn quotes Annex C §C.2.5 `PreCalCosMod()` of +//! ETSI TS 102 114 V1.3.1 (the staged PDF at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`). +//! +//! The 544-entry array `raCosMod[]` is pre-computed once by the +//! decoder and re-used by every `QMFInterpolation` invocation +//! (§C.2.5, PDF p.185). The §2.4 extract documents the four roles +//! the four blocks play inside that loop: +//! +//! ```text +//! Block 1 (indices 0..=255): cos((2i+1)(2k+1) π / 64) — 16×16 +//! Block 2 (indices 256..=511): cos(i(2k+1) π / 32) — 16×16 +//! Block 3 (indices 512..=527): + 0.25 / (2·cos((2k+1) π / 128)) — 16 +//! Block 4 (indices 528..=543): − 0.25 / (2·sin((2k+1) π / 128)) — 16 +//! ``` +//! +//! The j-counter in the spec's `PreCalCosMod()` pseudocode walks +//! 0..=543 with no gaps; this module materialises the same packing. +//! +//! Scope: this round only lands the matrix builder. The downstream +//! `QMFInterpolation` synthesis loop (and the §D.8 512-tap +//! `raCoeffLossy` / `raCoeffLossLess` FIR coefficient tables it +//! consumes) is a follow-up — the §D.8 tables are referenced in the +//! staged PDF p.238 but not yet transcribed under `docs/audio/dts/`. + +/// Total number of entries in the [`raCosMod`] array per +/// `PreCalCosMod()` (§C.2.5 / `dts-core-extracts.md` §2.3). +/// +/// Decomposes as 256 + 256 + 16 + 16 (the four blocks of the +/// spec's pseudocode). +pub const COS_MOD_LEN: usize = 544; + +/// Number of subbands the 32-band synthesis QMF reconstructs per +/// `QMFInterpolation()` invocation (§C.2.5 / `dts-core-extracts.md` +/// §2.4: `NumSubband = 32`). Also the length of the `raXin[]` +/// per-sample input vector and of the `raX[0..32]` output window +/// produced by the cosine-modulation stage. +pub const NUM_SUBBAND: usize = 32; + +/// Start index of Block 1 inside [`raCosMod`] +/// (`cos((2i+1)(2k+1) π / 64)`). +pub const COS_MOD_BLOCK1_START: usize = 0; + +/// Start index of Block 2 inside [`raCosMod`] +/// (`cos(i(2k+1) π / 32)`). +pub const COS_MOD_BLOCK2_START: usize = 256; + +/// Start index of Block 3 inside [`raCosMod`] +/// (`+0.25 / (2·cos((2k+1) π / 128))`). +pub const COS_MOD_BLOCK3_START: usize = 512; + +/// Start index of Block 4 inside [`raCosMod`] +/// (`−0.25 / (2·sin((2k+1) π / 128))`). +pub const COS_MOD_BLOCK4_START: usize = 528; + +/// Pre-compute the 544-entry cosine-modulation matrix. +/// +/// This is a direct Rust transliteration of the §C.2.5 +/// `PreCalCosMod()` pseudocode transcribed in +/// `docs/audio/dts/dts-core-extracts.md` §2.3: +/// +/// ```text +/// PreCalCosMod() { +/// for (j=0,k=0; k<16; k++) +/// for (i=0; i<16; i++) +/// raCosMod[j++] = cos((2*i+1)*(2*k+1)*Pi/64); +/// for (k=0; k<16; k++) +/// for (i=0; i<16; i++) +/// raCosMod[j++] = cos((i)*(2*k+1)*Pi/32); +/// for (k=0; k<16; k++) +/// raCosMod[j++] = 0.25 / (2*cos((2*k+1)*Pi/128)); +/// for (k=0; k<16; k++) +/// raCosMod[j++] = -0.25 / (2*sin((2*k+1)*Pi/128)); +/// } +/// ``` +/// +/// The returned array is intended to be allocated once per decoder +/// instance (§C.2.5: "computed once") and shared across every +/// `QMFInterpolation` call for the lifetime of that instance. +/// +/// The output is deterministic: every byte-identical run produces +/// the same array. Callers that need bit-exact reproducibility +/// across runs can rely on this directly without seeding a PRNG. +pub fn precal_cos_mod() -> [f64; COS_MOD_LEN] { + let mut ra = [0.0_f64; COS_MOD_LEN]; + let mut j = 0usize; + + // Block 1: indices 0..256 + // raCosMod[j++] = cos((2*i+1)*(2*k+1) * π / 64) + for k in 0..16 { + for i in 0..16 { + let num = ((2 * i + 1) * (2 * k + 1)) as f64; + ra[j] = (num * core::f64::consts::PI / 64.0).cos(); + j += 1; + } + } + debug_assert_eq!(j, COS_MOD_BLOCK2_START); + + // Block 2: indices 256..512 + // raCosMod[j++] = cos(i * (2*k+1) * π / 32) + for k in 0..16 { + for i in 0..16 { + let num = (i * (2 * k + 1)) as f64; + ra[j] = (num * core::f64::consts::PI / 32.0).cos(); + j += 1; + } + } + debug_assert_eq!(j, COS_MOD_BLOCK3_START); + + // Block 3: indices 512..528 + // raCosMod[j++] = 0.25 / (2 * cos((2*k+1) * π / 128)) + for k in 0..16 { + let arg = ((2 * k + 1) as f64) * core::f64::consts::PI / 128.0; + ra[j] = 0.25 / (2.0 * arg.cos()); + j += 1; + } + debug_assert_eq!(j, COS_MOD_BLOCK4_START); + + // Block 4: indices 528..544 + // raCosMod[j++] = -0.25 / (2 * sin((2*k+1) * π / 128)) + for k in 0..16 { + let arg = ((2 * k + 1) as f64) * core::f64::consts::PI / 128.0; + ra[j] = -0.25 / (2.0 * arg.sin()); + j += 1; + } + debug_assert_eq!(j, COS_MOD_LEN); + + ra +} + +// --------------------------------------------------------------- +// Cosine-modulation stage of `QMFInterpolation()` (§C.2.5, +// `dts-core-extracts.md` §2.4, PDF p.185) — the per-sample loop +// body's first half, up to (and including) the placement of +// `raX[0..32]` from `SUM[k]` / `DIFF[k]` via the Block-3 / Block-4 +// scaling coefficients. +// --------------------------------------------------------------- +// +// `QMFInterpolation()` is the 32-band synthesis-QMF reconstruction +// algorithm. Per sample-index `nSubIndex`, the algorithm reads one +// sample from each of the 32 subband sample arrays (the active +// subbands populate `raXin[0..nSUBS]`; the inactive ones are +// zero-filled), then performs three substeps: +// +// 1. Build the 16-entry `A[k]` / `B[k]` accumulators using the +// Block 1 (k outer, i inner, 16x16) and Block 2 (same shape) +// cosine-modulation coefficients from `raCosMod`, with a +// slightly asymmetric `B[k]` accumulation that pairs +// `raXin[2i]` with `raXin[2i-1]` for `i > 0` (and with itself +// for `i = 0`). +// 2. Combine into 16-entry `SUM[k] = A[k] + B[k]` and +// `DIFF[k] = A[k] - B[k]` vectors. +// 3. Place `raX[k] = raCosMod[Block3 + k] * SUM[k]` and +// `raX[32 - k - 1] = raCosMod[Block4 + k] * DIFF[k]` for +// `k = 0..16`, populating the leading 32 entries of the +// synthesis-filter shift register `raX[]`. +// +// After step 3, `QMFInterpolation()` continues with a 512-tap FIR +// convolution against `prCoeff` (§D.8 `raCoeffLossy` or +// `raCoeffLossLess`, selected by `FILTS`), the integer PCM output +// step, and the per-sample shift of `raX[]` / `raZ[]` history. +// Those substeps depend on the §D.8 FIR coefficient tables, which +// are not yet transcribed under `docs/audio/dts/` (round-208 docs +// gap #9). The cosine-modulation stage implemented here is +// FIR-independent: it consumes only `raXin[]` and `raCosMod[]` and +// produces only `raX[0..32]`, so it can be landed and exercised +// from the staged extracts alone. +// +// The j-counter in the spec's pseudocode walks across all three +// substeps without resetting: substep 1 reads `raCosMod[0..256]` +// (Block 1), substep 2's `B[k]` accumulation reads +// `raCosMod[256..512]` (Block 2), and substep 3 reads +// `raCosMod[512..528]` (Block 3) and `raCosMod[528..544]` +// (Block 4). This module's [`cos_mod_stage`] reproduces that +// walk with an explicit running index that ends at 544, matching +// the spec's `j` value at the boundary between substep 3 and the +// FIR step. + +/// Run the cosine-modulation stage of the §C.2.5 +/// `QMFInterpolation()` synthesis-QMF algorithm for one +/// sample-index inside its outer `for (nSubIndex=nStart; +/// nSubIndex= nSUBS` zero-filled per the spec's +/// `for (i=nSUBS; i [f64; NUM_SUBBAND] { + // Substep 1: A[k] = sum_{i=0..16} (raXin[2i] + raXin[2i+1]) + // * raCosMod[Block1 + 16k + i]. + // The spec's pseudocode uses a single running j; we materialise + // the same packing as Block1 row-major + Block2 row-major + + // Block3 + Block4 by relying on the constants from + // `precal_cos_mod()`. + let mut a = [0.0_f64; 16]; + let mut j = COS_MOD_BLOCK1_START; + for a_k in &mut a { + let mut acc = 0.0_f64; + for i in 0..16 { + acc += (ra_xin[2 * i] + ra_xin[2 * i + 1]) * ra_cos_mod[j]; + j += 1; + } + *a_k = acc; + } + debug_assert_eq!(j, COS_MOD_BLOCK2_START); + + // Substep 1 (continued): B[k] = sum_{i=0..16} f(i) where + // f(0) = raXin[0] * raCosMod[Block2 + 16k + 0] + // f(i) = (raXin[2i] + raXin[2i-1]) * raCosMod[Block2 + 16k + i] for i > 0 + let mut b = [0.0_f64; 16]; + for b_k in &mut b { + let mut acc = 0.0_f64; + for i in 0..16 { + let pair = if i > 0 { + ra_xin[2 * i] + ra_xin[2 * i - 1] + } else { + ra_xin[0] + }; + acc += pair * ra_cos_mod[j]; + j += 1; + } + *b_k = acc; + } + debug_assert_eq!(j, COS_MOD_BLOCK3_START); + + // Substep 2: SUM[k] = A[k] + B[k]; DIFF[k] = A[k] - B[k]. + // Held inline below to fuse with substep 3. + + // Substep 3: raX[k] = raCosMod[Block3 + k] * SUM[k] + // raX[32 - k - 1] = raCosMod[Block4 + k] * DIFF[k] + let mut ra_x = [0.0_f64; NUM_SUBBAND]; + // SUM step reads Block 3 (indices 512..528). + for k in 0..16 { + let sum_k = a[k] + b[k]; + ra_x[k] = ra_cos_mod[j] * sum_k; + j += 1; + } + debug_assert_eq!(j, COS_MOD_BLOCK4_START); + // DIFF step reads Block 4 (indices 528..544). + for k in 0..16 { + let diff_k = a[k] - b[k]; + // raX[32 - k - 1] writes 31, 30, ..., 16 as k = 0..16. + ra_x[NUM_SUBBAND - k - 1] = ra_cos_mod[j] * diff_k; + j += 1; + } + debug_assert_eq!(j, COS_MOD_LEN); + + ra_x +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tolerance for cross-checks between the closed-form values and + /// the matrix entry. The expected values come from the same + /// `cos` / `sin` calls in the spec pseudocode, so the residual is + /// effectively zero on IEEE-754; we leave a small slack for the + /// theoretical-vs-runtime-rounding mismatch. + const EPS: f64 = 1e-12; + + #[test] + fn length_matches_spec() { + let ra = precal_cos_mod(); + assert_eq!(ra.len(), 544); + assert_eq!(COS_MOD_LEN, 544); + } + + #[test] + fn block_boundaries_are_documented_constants() { + // Documented in module docs + start-constant docstrings. + assert_eq!(COS_MOD_BLOCK1_START, 0); + assert_eq!(COS_MOD_BLOCK2_START, 256); + assert_eq!(COS_MOD_BLOCK3_START, 512); + assert_eq!(COS_MOD_BLOCK4_START, 528); + // Decomposition adds up. + assert_eq!( + COS_MOD_BLOCK2_START - COS_MOD_BLOCK1_START + COS_MOD_BLOCK3_START + - COS_MOD_BLOCK2_START + + COS_MOD_BLOCK4_START + - COS_MOD_BLOCK3_START + + COS_MOD_LEN + - COS_MOD_BLOCK4_START, + COS_MOD_LEN + ); + } + + #[test] + fn block1_first_entry_is_cos_pi_over_64() { + // k=0, i=0 → (2·0+1)(2·0+1) π/64 = π/64 + let ra = precal_cos_mod(); + let expected = (core::f64::consts::PI / 64.0).cos(); + assert!((ra[0] - expected).abs() < EPS); + } + + #[test] + fn block1_walks_all_256_indices() { + // Spec pseudocode walks k outer, i inner, both 0..16. The + // packed index for (k, i) is 16*k + i. + let ra = precal_cos_mod(); + for k in 0..16 { + for i in 0..16 { + let idx = COS_MOD_BLOCK1_START + 16 * k + i; + let num = ((2 * i + 1) * (2 * k + 1)) as f64; + let expected = (num * core::f64::consts::PI / 64.0).cos(); + assert!( + (ra[idx] - expected).abs() < EPS, + "Block 1 ({k}, {i}) mismatch: got {} expected {}", + ra[idx], + expected + ); + } + } + } + + #[test] + fn block2_first_entry_is_one() { + // k=0, i=0 → cos(0·1·π/32) = cos(0) = 1.0 + let ra = precal_cos_mod(); + assert!((ra[COS_MOD_BLOCK2_START] - 1.0).abs() < EPS); + } + + #[test] + fn block2_walks_all_256_indices() { + let ra = precal_cos_mod(); + for k in 0..16 { + for i in 0..16 { + let idx = COS_MOD_BLOCK2_START + 16 * k + i; + let num = (i * (2 * k + 1)) as f64; + let expected = (num * core::f64::consts::PI / 32.0).cos(); + assert!( + (ra[idx] - expected).abs() < EPS, + "Block 2 ({k}, {i}) mismatch: got {} expected {}", + ra[idx], + expected + ); + } + } + } + + #[test] + fn block3_first_entry_matches_closed_form() { + // k=0 → 0.25 / (2 · cos(π/128)) + let ra = precal_cos_mod(); + let arg = core::f64::consts::PI / 128.0; + let expected = 0.25 / (2.0 * arg.cos()); + assert!((ra[COS_MOD_BLOCK3_START] - expected).abs() < EPS); + } + + #[test] + fn block3_walks_all_16_indices() { + let ra = precal_cos_mod(); + for k in 0..16 { + let idx = COS_MOD_BLOCK3_START + k; + let arg = ((2 * k + 1) as f64) * core::f64::consts::PI / 128.0; + let expected = 0.25 / (2.0 * arg.cos()); + assert!( + (ra[idx] - expected).abs() < EPS, + "Block 3 ({k}) mismatch: got {} expected {}", + ra[idx], + expected + ); + } + } + + #[test] + fn block3_entries_are_strictly_positive() { + // (2k+1)π/128 is in (0, π/2) for k in 0..16, so cos > 0 + // and the +0.25 / (2·cos) factor is strictly positive. + let ra = precal_cos_mod(); + for k in 0..16 { + let v = ra[COS_MOD_BLOCK3_START + k]; + assert!(v > 0.0, "Block 3 ({k}) = {v} should be > 0"); + } + } + + #[test] + fn block4_first_entry_matches_closed_form() { + // k=0 → -0.25 / (2 · sin(π/128)) + let ra = precal_cos_mod(); + let arg = core::f64::consts::PI / 128.0; + let expected = -0.25 / (2.0 * arg.sin()); + assert!((ra[COS_MOD_BLOCK4_START] - expected).abs() < EPS); + } + + #[test] + fn block4_walks_all_16_indices() { + let ra = precal_cos_mod(); + for k in 0..16 { + let idx = COS_MOD_BLOCK4_START + k; + let arg = ((2 * k + 1) as f64) * core::f64::consts::PI / 128.0; + let expected = -0.25 / (2.0 * arg.sin()); + assert!( + (ra[idx] - expected).abs() < EPS, + "Block 4 ({k}) mismatch: got {} expected {}", + ra[idx], + expected + ); + } + } + + #[test] + fn block4_entries_are_strictly_negative() { + // (2k+1)π/128 is in (0, π/2) for k in 0..16, so sin > 0 + // and the -0.25 / (2·sin) factor is strictly negative. + let ra = precal_cos_mod(); + for k in 0..16 { + let v = ra[COS_MOD_BLOCK4_START + k]; + assert!(v < 0.0, "Block 4 ({k}) = {v} should be < 0"); + } + } + + #[test] + fn block1_last_entry_of_row_zero() { + // k=0, i=15 → (2·15+1)(2·0+1) π / 64 = 31 π / 64 + let ra = precal_cos_mod(); + let expected = (31.0 * core::f64::consts::PI / 64.0).cos(); + assert!((ra[15] - expected).abs() < EPS); + } + + #[test] + fn block1_row_count_and_row_length() { + // Verify the packing density: 16 rows of 16 columns = + // 256 entries. Done by cross-checking the index arithmetic + // matches the explicit row/col enumeration. + let ra = precal_cos_mod(); + let mut seen = 0usize; + for k in 0..16 { + for i in 0..16 { + let idx = COS_MOD_BLOCK1_START + 16 * k + i; + let num = ((2 * i + 1) * (2 * k + 1)) as f64; + let expected = (num * core::f64::consts::PI / 64.0).cos(); + assert!((ra[idx] - expected).abs() < EPS); + seen += 1; + } + } + assert_eq!(seen, 256); + } + + #[test] + fn block2_row_first_entry_is_one_for_all_k() { + // i=0 always gives cos(0) = 1 regardless of k. + let ra = precal_cos_mod(); + for k in 0..16 { + let idx = COS_MOD_BLOCK2_START + 16 * k; + assert!( + (ra[idx] - 1.0).abs() < EPS, + "Block 2 row {k} entry 0 should be 1.0" + ); + } + } + + #[test] + fn block3_value_grows_with_k() { + // (2k+1)π/128 grows monotonically with k, cos(·) shrinks, + // so +0.25 / (2·cos) grows monotonically with k. + let ra = precal_cos_mod(); + for k in 1..16 { + let prev = ra[COS_MOD_BLOCK3_START + k - 1]; + let cur = ra[COS_MOD_BLOCK3_START + k]; + assert!( + cur > prev, + "Block 3 should grow: prev={prev} cur={cur} at k={k}" + ); + } + } + + #[test] + fn block4_magnitude_shrinks_with_k() { + // (2k+1)π/128 grows, sin(·) grows, so |-0.25 / (2·sin)| + // shrinks. The entries are negative; their absolute values + // shrink. + let ra = precal_cos_mod(); + for k in 1..16 { + let prev = ra[COS_MOD_BLOCK4_START + k - 1].abs(); + let cur = ra[COS_MOD_BLOCK4_START + k].abs(); + assert!( + cur < prev, + "|Block 4| should shrink: prev={prev} cur={cur} at k={k}" + ); + } + } + + #[test] + fn deterministic_across_calls() { + // Two independent invocations must produce bit-identical + // arrays (§C.2.5 documents the matrix as computed once and + // reused; that only makes sense if the result is + // deterministic). + let a = precal_cos_mod(); + let b = precal_cos_mod(); + for i in 0..COS_MOD_LEN { + // Bit-exact, not approximate, because the same `cos` / + // `sin` arguments are passed in the same order. + assert_eq!(a[i].to_bits(), b[i].to_bits(), "mismatch at index {i}"); + } + } + + #[test] + fn all_entries_are_finite() { + // No NaN, no ±∞. The Block 3 / 4 denominators are cos / sin + // at angles strictly inside (0, π/2), so neither vanishes + // and the division is finite. + let ra = precal_cos_mod(); + for (i, v) in ra.iter().enumerate() { + assert!(v.is_finite(), "ra[{i}] = {v} is not finite"); + } + } + + #[test] + fn block1_and_block2_entries_are_bounded_by_one() { + // cos returns a value in [-1, +1]. Block 1 / 2 are pure + // cosine evaluations. + let ra = precal_cos_mod(); + for (i, v) in ra[COS_MOD_BLOCK1_START..COS_MOD_BLOCK3_START] + .iter() + .enumerate() + { + assert!(v.abs() <= 1.0 + EPS, "ra[{i}] = {v} outside [-1, 1]"); + } + } + + // ----------------------------------------------------------- + // cos_mod_stage() — cosine-modulation stage of + // QMFInterpolation() (§C.2.5, PDF p.185). + // ----------------------------------------------------------- + + /// Reference implementation: a direct verbatim translation of + /// the §2.4 pseudocode's first half, used as the oracle the + /// optimised [`cos_mod_stage`] is cross-checked against. This + /// mirrors the spec line-for-line with no fusing of the SUM / + /// DIFF combine step into substep 3, so a bug in either the + /// reference or the live function shows up as a per-index + /// divergence. + /// + /// Indexes a / b / sum / diff / ra_x with the loop variable so + /// the reference body remains a 1:1 textual match of the + /// pseudocode; iterator-flavoured rewrites would obscure the + /// correspondence with the spec. + #[allow(clippy::needless_range_loop)] + fn cos_mod_stage_reference( + ra_xin: &[f64; NUM_SUBBAND], + ra_cos_mod: &[f64; COS_MOD_LEN], + ) -> [f64; NUM_SUBBAND] { + let mut a = [0.0_f64; 16]; + let mut b = [0.0_f64; 16]; + let mut sum = [0.0_f64; 16]; + let mut diff = [0.0_f64; 16]; + let mut ra_x = [0.0_f64; NUM_SUBBAND]; + + let mut j = 0usize; + for k in 0..16 { + for i in 0..16 { + a[k] += (ra_xin[2 * i] + ra_xin[2 * i + 1]) * ra_cos_mod[j]; + j += 1; + } + } + for k in 0..16 { + for i in 0..16 { + if i > 0 { + b[k] += (ra_xin[2 * i] + ra_xin[2 * i - 1]) * ra_cos_mod[j]; + } else { + b[k] += ra_xin[2 * i] * ra_cos_mod[j]; + } + j += 1; + } + sum[k] = a[k] + b[k]; + diff[k] = a[k] - b[k]; + } + for k in 0..16 { + ra_x[k] = ra_cos_mod[j] * sum[k]; + j += 1; + } + for k in 0..16 { + ra_x[NUM_SUBBAND - k - 1] = ra_cos_mod[j] * diff[k]; + j += 1; + } + assert_eq!(j, COS_MOD_LEN); + ra_x + } + + #[test] + fn cos_mod_stage_zero_input_yields_zero_output() { + // raXin = 0 → A[k] = B[k] = 0 → SUM[k] = DIFF[k] = 0 → + // raX[0..32] = 0 regardless of the cosine-modulation + // matrix. This pins the linearity-at-zero corner. + let ra_xin = [0.0_f64; NUM_SUBBAND]; + let ra_cos_mod = precal_cos_mod(); + let out = cos_mod_stage(&ra_xin, &ra_cos_mod); + for (i, v) in out.iter().enumerate() { + assert_eq!(*v, 0.0, "raX[{i}] = {v} should be 0"); + } + } + + #[test] + fn cos_mod_stage_matches_reference_on_zero_input() { + // Bit-exact agreement at the trivial input ensures the + // fused live implementation and the line-for-line + // reference walk the j-counter the same way. + let ra_xin = [0.0_f64; NUM_SUBBAND]; + let ra_cos_mod = precal_cos_mod(); + let live = cos_mod_stage(&ra_xin, &ra_cos_mod); + let reference = cos_mod_stage_reference(&ra_xin, &ra_cos_mod); + for i in 0..NUM_SUBBAND { + assert_eq!( + live[i].to_bits(), + reference[i].to_bits(), + "raX[{i}] mismatch: live={} ref={}", + live[i], + reference[i] + ); + } + } + + #[test] + fn cos_mod_stage_matches_reference_on_unit_basis_inputs() { + // For every j ∈ 0..32, set raXin[j] = 1 (all others zero) + // and verify the live function matches the spec-line-for- + // line reference. This exercises every Block-1 / Block-2 + // cosine entry exactly once across the 32 sweeps. + let ra_cos_mod = precal_cos_mod(); + for j in 0..NUM_SUBBAND { + let mut ra_xin = [0.0_f64; NUM_SUBBAND]; + ra_xin[j] = 1.0; + let live = cos_mod_stage(&ra_xin, &ra_cos_mod); + let reference = cos_mod_stage_reference(&ra_xin, &ra_cos_mod); + for i in 0..NUM_SUBBAND { + assert_eq!( + live[i].to_bits(), + reference[i].to_bits(), + "raX[{i}] mismatch (impulse at {j}): live={} ref={}", + live[i], + reference[i] + ); + } + } + } + + #[test] + fn cos_mod_stage_matches_reference_on_ramp_input() { + // raXin[i] = i + 0.5 (non-trivial, no zeros) — exercises + // every Block-1 / Block-2 cosine entry with a non-zero + // pair-sum and gives the SUM[k] / DIFF[k] step distinct + // values to scale. + let mut ra_xin = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in ra_xin.iter_mut().enumerate() { + *slot = (i as f64) + 0.5; + } + let ra_cos_mod = precal_cos_mod(); + let live = cos_mod_stage(&ra_xin, &ra_cos_mod); + let reference = cos_mod_stage_reference(&ra_xin, &ra_cos_mod); + for i in 0..NUM_SUBBAND { + assert_eq!( + live[i].to_bits(), + reference[i].to_bits(), + "raX[{i}] mismatch on ramp input: live={} ref={}", + live[i], + reference[i] + ); + } + } + + #[test] + fn cos_mod_stage_matches_reference_on_alternating_signs() { + // raXin[i] = (-1)^i — the pair-sums (raXin[2i] + + // raXin[2i+1]) are 0, and the asymmetric B-pair sums + // raXin[2i] + raXin[2i-1] alternate; covers a different + // regime than the ramp. + let mut ra_xin = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in ra_xin.iter_mut().enumerate() { + *slot = if i.is_multiple_of(2) { 1.0 } else { -1.0 }; + } + let ra_cos_mod = precal_cos_mod(); + let live = cos_mod_stage(&ra_xin, &ra_cos_mod); + let reference = cos_mod_stage_reference(&ra_xin, &ra_cos_mod); + for i in 0..NUM_SUBBAND { + assert_eq!( + live[i].to_bits(), + reference[i].to_bits(), + "raX[{i}] mismatch on alternating signs: live={} ref={}", + live[i], + reference[i] + ); + } + } + + #[test] + fn cos_mod_stage_output_is_finite() { + // Every raCosMod entry is finite (covered by + // `all_entries_are_finite`), every raXin entry is finite, + // and substep 3's scaling factors do not blow up, so the + // output must be finite for any finite input. + let mut ra_xin = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in ra_xin.iter_mut().enumerate() { + *slot = (i as f64).sin(); + } + let ra_cos_mod = precal_cos_mod(); + let out = cos_mod_stage(&ra_xin, &ra_cos_mod); + for (i, v) in out.iter().enumerate() { + assert!(v.is_finite(), "raX[{i}] = {v} not finite"); + } + } + + #[test] + fn cos_mod_stage_is_linear() { + // The stage is bilinear in (raXin, raCosMod) and pure + // linear in raXin (with raCosMod fixed). Check + // cos_mod_stage(2*x) = 2 * cos_mod_stage(x) for a ramp + // input — this is a structural property derived from the + // spec pseudocode (the only multiplications by raCosMod are + // against linear combinations of raXin). + let mut ra_xin = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in ra_xin.iter_mut().enumerate() { + *slot = (i as f64) - 16.0; + } + let mut ra_xin_2x = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in ra_xin_2x.iter_mut().enumerate() { + *slot = 2.0 * ra_xin[i]; + } + let ra_cos_mod = precal_cos_mod(); + let out_1x = cos_mod_stage(&ra_xin, &ra_cos_mod); + let out_2x = cos_mod_stage(&ra_xin_2x, &ra_cos_mod); + const LINEARITY_EPS: f64 = 1e-9; + for i in 0..NUM_SUBBAND { + let expected = 2.0 * out_1x[i]; + assert!( + (out_2x[i] - expected).abs() < LINEARITY_EPS, + "raX[{i}] not linear: 2*x→{} but expected {}", + out_2x[i], + expected + ); + } + } + + #[test] + fn cos_mod_stage_is_deterministic() { + // Two identical inputs must produce bit-identical outputs. + let mut ra_xin = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in ra_xin.iter_mut().enumerate() { + *slot = ((i + 1) as f64).cos(); + } + let ra_cos_mod = precal_cos_mod(); + let a = cos_mod_stage(&ra_xin, &ra_cos_mod); + let b = cos_mod_stage(&ra_xin, &ra_cos_mod); + for i in 0..NUM_SUBBAND { + assert_eq!(a[i].to_bits(), b[i].to_bits(), "raX[{i}] differs"); + } + } + + #[test] + fn num_subband_is_thirty_two() { + // Spec invariant: §C.2.5 fixes NumSubband = 32 for the + // 32-band synthesis QMF. + assert_eq!(NUM_SUBBAND, 32); + } +} diff --git a/crates/vendor/oxideav-dts/src/crc16.rs b/crates/vendor/oxideav-dts/src/crc16.rs new file mode 100644 index 00000000..fafcd496 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/crc16.rs @@ -0,0 +1,222 @@ +//! Annex B (normative) "CRC Algorithm" — the single CRC-16 every DTS +//! Coherent Acoustics check word uses. +//! +//! Transcribed from ETSI TS 102 114 V1.3.1 Annex B (printed p.181), +//! staged as `docs/audio/dts/dts-crc16.md`. Annex B names the +//! algorithm "CRC-CCITT" and fixes every parameter: +//! +//! | Parameter | Value | +//! |-----------|-------| +//! | Width | 16 bits | +//! | Generator polynomial | `G(x) = x¹⁶ + x¹² + x⁵ + 1` (`0x1021`) | +//! | Initial value | `0xFFFF` ("initialized to the value of 0xFFFF before checksum computation commences") | +//! | Bit order | MSB-first (un-reflected) | +//! | Final XOR | none | +//! +//! This parameter set is commonly catalogued as **CRC-16/CCITT-FALSE**. +//! The bitstream zero-pads every protected region to a byte boundary +//! before its CRC field (the §5.x `ByteAlign…` fields) precisely so a +//! byte-wise table implementation can run — this module provides that +//! table form ([`DTS_CRC16_TABLE`], derived purely from the +//! polynomial) plus the incremental update entry point for callers +//! that checksum a region in pieces. +//! +//! ## Where the check words live +//! +//! Every DTS CRC field uses this one algorithm; each field's coverage +//! span runs from the first byte of the protected region up to the +//! byte immediately preceding the CRC field, inclusive +//! (`docs/audio/dts/dts-crc16.md` "Where CRC-16 is applied"): +//! +//! * Core substream (§5): `HCRC` / `AHCRC` / `SICRC` / `OCRC` when +//! `CPF == 1` — **extracted but not tested** (the spec states "The +//! CRC value test shall not be applied" for these core fields; they +//! are informational placeholders). +//! * `nAUXCRC16` (§5.6/§5.7.1) — genuinely verified over the aux data +//! from `bAUXTimeStampFlag` to the byte before the CRC +//! ([`crate::AuxData::crc_valid`]). +//! * `nRev2AUXCRC16` (§5.7.2) — genuinely verified over +//! `nRev2AUXDataByteSize − 2` bytes starting at the size field +//! ([`crate::Rev2AuxChunk::crc_valid`]). +//! * Extension substreams (§6, e.g. `nCRC16HeaderX96`) — reference +//! the same Annex-B algorithm; used during sync detection to reject +//! false sync words. +//! +//! This module is feature-independent (no `oxideav-core` dep), so it +//! is available under both the default and `--no-default-features` +//! build modes. + +/// The Annex B generator polynomial in normal (MSB-first) form: +/// `G(x) = x¹⁶ + x¹² + x⁵ + 1`. +pub const DTS_CRC16_POLY: u16 = 0x1021; + +/// The Annex B initial register value: "The CRC16 is initialized to +/// the value of 0xFFFF before checksum computation commences." +pub const DTS_CRC16_INIT: u16 = 0xFFFF; + +/// Build one row of the byte-wise CRC table: the register evolution +/// of a single byte `i` shifted through the MSB-first polynomial +/// division. Pure data derived from [`DTS_CRC16_POLY`]. +const fn table_entry(i: u16) -> u16 { + let mut crc = i << 8; + let mut b = 0; + while b < 8 { + crc = if crc & 0x8000 != 0 { + (crc << 1) ^ DTS_CRC16_POLY + } else { + crc << 1 + }; + b += 1; + } + crc +} + +const fn build_table() -> [u16; 256] { + let mut table = [0u16; 256]; + let mut i = 0; + while i < 256 { + table[i] = table_entry(i as u16); + i += 1; + } + table +} + +/// The 256-entry byte-wise lookup table (MSB-first, seeded from +/// [`DTS_CRC16_POLY`]) — the "fast table-based CRC16 calculation" form +/// the spec's byte-alignment fields exist to enable. Generated at +/// compile time from the polynomial alone. +pub static DTS_CRC16_TABLE: [u16; 256] = build_table(); + +/// Fold `bytes` into a running Annex B CRC-16 register value. +/// +/// Start `crc` at [`DTS_CRC16_INIT`] (or use [`dts_crc16`] for the +/// one-shot form); feed successive slices of the protected region in +/// order. No reflection and no final XOR are applied — the returned +/// register value **is** the check word the bitstream carries. +#[must_use] +pub fn dts_crc16_update(crc: u16, bytes: &[u8]) -> u16 { + let mut crc = crc; + for &byte in bytes { + let idx = ((crc >> 8) ^ u16::from(byte)) & 0xFF; + crc = (crc << 8) ^ DTS_CRC16_TABLE[idx as usize]; + } + crc +} + +/// Compute the Annex B CRC-16 (CRC-CCITT: polynomial `0x1021`, initial +/// value `0xFFFF`, MSB-first, no reflection, no final XOR) over a +/// byte-aligned protected region. +/// +/// The result equals the 16-bit check word the DTS bitstream stores +/// immediately after the region; a receiver verifies by recomputing +/// over the same span and comparing (`docs/audio/dts/dts-crc16.md`). +#[must_use] +pub fn dts_crc16(bytes: &[u8]) -> u16 { + dts_crc16_update(DTS_CRC16_INIT, bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Bit-at-a-time reference implementation straight from the Annex B + /// parameters (the doc's "reference implementation sketch"), + /// independent of the table generation above. + fn bitwise_reference(bytes: &[u8]) -> u16 { + let mut crc = DTS_CRC16_INIT; + for &byte in bytes { + crc ^= u16::from(byte) << 8; + for _ in 0..8 { + crc = if crc & 0x8000 != 0 { + (crc << 1) ^ DTS_CRC16_POLY + } else { + crc << 1 + }; + } + } + crc + } + + /// An empty region leaves the register at the initial value: no + /// bytes, no division steps. + #[test] + fn empty_input_is_init_value() { + assert_eq!(dts_crc16(&[]), DTS_CRC16_INIT); + } + + /// The catalogued CRC-16/CCITT-FALSE check value: the ASCII bytes + /// `"123456789"` produce `0x29B1` under poly `0x1021` / init + /// `0xFFFF` / no reflection / no final XOR — the standard + /// check-value row for the parameter set Annex B specifies. + #[test] + fn catalog_check_value() { + assert_eq!(dts_crc16(b"123456789"), 0x29B1); + assert_eq!(bitwise_reference(b"123456789"), 0x29B1); + } + + /// Table row 0 is zero (no set bits, no reduction) and row 1 is the + /// polynomial itself shifted into place. + #[test] + fn table_anchor_rows() { + assert_eq!(DTS_CRC16_TABLE[0], 0); + assert_eq!(DTS_CRC16_TABLE[1], DTS_CRC16_POLY); + } + + /// The byte-wise table form agrees with the bit-at-a-time Annex B + /// reference over a deterministic pseudo-random buffer. + #[test] + fn table_form_matches_bitwise_reference() { + // xorshift-style deterministic byte stream, no external data. + let mut state = 0x1234_5678_u32; + let bytes: Vec = (0..4096) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect(); + for len in [0usize, 1, 2, 3, 15, 16, 17, 255, 256, 4096] { + assert_eq!( + dts_crc16(&bytes[..len]), + bitwise_reference(&bytes[..len]), + "length {len}" + ); + } + } + + /// Incremental folding over arbitrary splits equals the one-shot + /// computation (the ByteAlign'd regions are checksummed bytewise, + /// so any byte-boundary split must be transparent). + #[test] + fn incremental_update_matches_one_shot() { + let bytes: Vec = (0u16..777) + .map(|i| (i.wrapping_mul(31) >> 3) as u8) + .collect(); + let whole = dts_crc16(&bytes); + for split in [0usize, 1, 76, 400, 776, 777] { + let partial = dts_crc16_update(DTS_CRC16_INIT, &bytes[..split]); + assert_eq!(dts_crc16_update(partial, &bytes[split..]), whole); + } + } + + /// A single-bit corruption anywhere in the region changes the check + /// word (CRC-16 detects all single-bit errors by construction — + /// G(x) has more than one term). + #[test] + fn detects_single_bit_flips() { + let bytes: Vec = (0..64u8).collect(); + let reference = dts_crc16(&bytes); + for byte in 0..bytes.len() { + for bit in 0..8 { + let mut corrupted = bytes.clone(); + corrupted[byte] ^= 1 << bit; + assert_ne!( + dts_crc16(&corrupted), + reference, + "flip at byte {byte} bit {bit} went undetected" + ); + } + } + } +} diff --git a/crates/vendor/oxideav-dts/src/d10_tables.rs b/crates/vendor/oxideav-dts/src/d10_tables.rs new file mode 100644 index 00000000..37118b22 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/d10_tables.rs @@ -0,0 +1,5157 @@ +//! The two ETSI TS 102 114 Annex D §D.10 VQ code books, as **data**. +//! +//! Machine-generated transcription of the staged clean-room tables +//! (do not hand-edit; regenerate from the CSVs): +//! +//! * [`ADPCM_VQ_TABLE`] — `docs/audio/dts/tables/dts-d10-1-adpcm-coeff-vq.csv` +//! (SHA-256 `65ee69cd518229b5e0936844db794e49dec2e993af39440fef36658e0dfa30d9`), +//! the §D.10.1 `ADPCMCoeffVQ` book: 4096 vectors × 4 signed Q13 +//! integers. Actual prediction coefficient = entry ÷ 2¹³ +//! ([`crate::adpcm_vq_coeff`]); the spec's printed anchor is entry +//! `9928` → `1.2119140625`, which is row 0 element 0 here. +//! * [`HFREQ_VQ_TABLE`] — `docs/audio/dts/tables/dts-d10-2-hfreq-vq.csv` +//! (SHA-256 `3d5d409a975f57720fa67760c2ec96cc1d00d12b9aa1c7de3e5b48a982acede6`), +//! the §D.10.2 `HFreqVQ` book: 1024 vectors × 32 signed 8-bit +//! elements, already unpacked from the 16-bit two-element wire +//! entries into vector-element order (element `2k` is entry `k`'s +//! low byte). Actual subband sample = element ÷ 2⁴ +//! ([`crate::HFREQ_VQ_ELEMENT_DIVISOR`]). +//! +//! The spec defines both books normatively but omits their values +//! ("Due to its extensive size, this table is not included here", +//! p.255); the staged tables recover them per +//! `docs/audio/dts/provenance/11-extractor-d10-vq.md`, with two +//! independent sources agreeing on every value in both books. The +//! `.meta.md` sidecars alongside the CSVs carry the shape, scaling, +//! ordering, and cross-check record. + +/// §D.10.1 `ADPCMCoeffVQ`: 4096 × 4 signed Q13 stored integers. +#[rustfmt::skip] +pub static ADPCM_VQ_TABLE: [[i16; 4]; 4096] = [ + [9928, -2618, -1093, -1263], + [11077, -2876, -1747, -308], + [10503, -1082, -1426, -1167], + [9337, -2403, -1495, 274], + [10698, -2529, -532, -1122], + [10368, -3974, -1264, -750], + [10070, -3667, 346, 863], + [10278, -3093, 311, -576], + [9894, -1330, -1428, -860], + [10544, -1923, -1058, -971], + [10996, -1632, -841, -1404], + [11832, -3465, 1658, -1990], + [10852, -688, -2658, -499], + [10546, -1749, -147, -1733], + [10801, -1004, -708, -1453], + [10588, -441, -2113, -952], + [10141, -3331, -582, -1432], + [9608, -2590, 383, 258], + [11422, -3265, 229, -1544], + [10460, -1338, -713, -1568], + [10306, -1721, -1660, -603], + [9580, -1812, -1235, -1061], + [11471, -2285, -1617, -607], + [10081, -2225, -1408, -868], + [10715, -2624, -1367, -704], + [10616, -1871, -2770, -35], + [9352, -2340, -1024, -1566], + [11065, -1458, -1926, -735], + [11334, -2056, -1041, -1144], + [9825, -2048, -794, -1536], + [11850, -2695, -1123, -867], + [10654, -2226, -1891, -373], + [10024, -1557, -808, -1069], + [11142, -1266, -3238, 128], + [11729, -3282, -514, -1011], + [11402, -2094, -2335, -189], + [10195, -3658, 181, -1875], + [11431, -2626, -404, -1377], + [11001, -3868, -619, -1077], + [10894, -2559, 274, -1758], + [9633, -1482, -2253, -773], + [11245, -3321, 830, -1972], + [9768, -2701, -199, -1859], + [10500, -2042, 525, -2043], + [11669, -4069, 293, -1468], + [9192, -1991, -583, -61], + [10057, -3220, -2015, -473], + [9497, -2315, -2490, -467], + [10455, -3069, -1194, -1007], + [9994, -1936, -60, -1225], + [9295, -2156, -1761, -1134], + [10085, -3748, -1026, 197], + [9334, -2360, 804, -351], + [11561, -2553, 1352, -2313], + [12837, -3998, 1195, -1958], + [10114, -1100, -2414, -394], + [9341, -2530, 315, 755], + [10131, -3164, 1411, -674], + [9535, -905, -1551, 579], + [11717, -1519, -3051, 91], + [9824, -2911, -2775, 192], + [9662, -2934, -561, 1450], + [11085, -3392, -1298, -659], + [8955, -2102, -1899, 703], + [8607, -1742, -4348, 814], + [7640, -2063, -3617, 52], + [7074, -826, -4325, 4375], + [7714, 584, -4238, 1927], + [6355, -952, -4912, 3127], + [7069, -660, -6413, 4087], + [8313, -132, -2964, -876], + [6952, -1422, -3962, -24], + [9299, -734, -3088, -263], + [9484, -574, -4513, 466], + [7246, -91, -3735, -704], + [8325, -1417, -3090, -530], + [6469, -1226, -4757, 829], + [6652, -368, -5682, 1393], + [7971, -1278, -2284, 1205], + [7229, -699, -3556, 1840], + [7994, 1284, -2729, 732], + [9005, -698, -4522, 2189], + [6963, 197, -2727, 380], + [8527, 135, -3991, -213], + [8840, 934, -3014, -567], + [10125, 418, -3284, -371], + [6367, 361, -2318, 2554], + [7892, 172, -5247, 4673], + [6674, 387, -5424, 4398], + [6240, 684, -4047, 1219], + [11170, -794, -5081, 1195], + [11765, -648, -6265, 2052], + [10845, -775, -3837, 366], + [12496, -689, -8260, 3562], + [7893, -1166, -4972, 988], + [8592, 1052, -5986, 3087], + [7277, 1874, -5685, 3579], + [6900, 2016, -4809, 3491], + [8530, -2405, -3250, 1986], + [9426, 494, -7067, 5038], + [10285, 564, -8210, 5370], + [8749, -2207, -3980, 2852], + [9653, -2686, -4300, 1400], + [9770, -2286, -5663, 4233], + [8490, -4, -7048, 4496], + [7697, -1209, -5328, 3183], + [6451, 801, -4324, -554], + [7387, 1806, -5265, 545], + [7450, -2302, -4445, 1418], + [8817, -1370, -5827, 2168], + [10324, -2406, -5629, 2579], + [8863, -2578, -3537, 467], + [6901, -1624, -3169, 3392], + [7846, 156, -6948, 3381], + [7928, -1115, -5972, 4816], + [6089, -599, -4368, -320], + [7833, 1246, -3960, -621], + [8931, 2521, -6768, 2052], + [8900, 1944, -4126, 40], + [7661, -34, -2855, 2480], + [5873, 474, -3262, 3712], + [7535, -234, -4699, 216], + [5856, 143, -5142, 73], + [8944, -106, -5874, 3663], + [7134, 426, -5879, 2895], + [10199, 1011, -4762, 369], + [8454, 264, -5971, 1291], + [7822, -2449, -4333, 4540], + [6200, -2758, -2632, 1497], + [6070, -4315, -2699, 414], + [7047, -3739, -3210, 1060], + [5675, -3801, -2717, -407], + [4789, -4063, -2628, -744], + [4023, -3366, -3133, -726], + [4296, -2407, -3381, -513], + [4388, -2931, -2820, 1512], + [4559, -4233, -1941, 1976], + [6702, -3208, -1755, 1680], + [4416, -3521, -1052, 2984], + [7154, -4266, -1203, 3732], + [3625, -4242, -3244, 1395], + [6518, -2856, -1304, 2887], + [6170, -1949, -3014, 3973], + [5189, -2451, -4020, 3477], + [6218, -2988, -1921, 3844], + [4827, -3688, -1928, 3343], + [6668, -3991, -2805, 3095], + [5297, -3115, -3684, 2390], + [5354, -4614, -2662, 1504], + [4196, -3091, -4147, 1135], + [3540, -2893, -4007, 100], + [5569, -1602, -4007, 1909], + [4341, -2091, -4272, 252], + [5559, -2878, -3832, 498], + [4548, -4479, -2898, -27], + [5176, -2494, -4635, 1476], + [3294, -3485, -3738, 716], + [4920, -1229, -4195, -365], + [3257, -3518, -3349, 2862], + [5286, -1948, -3485, -778], + [6502, -3051, -152, 2854], + [5864, -4192, -1076, 3451], + [4656, -3122, -3448, 179], + [5907, -754, -1596, 3116], + [7229, -3680, -1590, 2892], + [5107, -3888, -3364, 806], + [6764, -2635, -3450, 134], + [5258, -2827, -2844, -1052], + [5798, -1725, -4305, 205], + [5404, -1213, -3362, 449], + [6224, -2738, -3046, -581], + [4223, -2438, -2725, 3745], + [4751, -3411, -2123, 116], + [3868, -3000, -3954, 2297], + [6819, -2899, -4277, 2825], + [4207, -4754, -2808, 865], + [4804, -1494, -1997, 4688], + [5282, -2213, -548, 3559], + [5580, -1912, -566, 4370], + [6168, -2857, -672, 4053], + [6583, -4515, -2850, 1670], + [6511, -3093, -3988, 1421], + [4646, -1790, -1443, 3650], + [5915, -924, -2020, 896], + [7814, -4181, -3152, 2007], + [6190, -2238, -4817, 2279], + [4737, -4034, -3288, 1835], + [8161, -3633, -3423, 3137], + [7415, -2351, -2088, 4290], + [4106, -2517, -62, 2905], + [4909, -3145, -614, 4112], + [4938, -3281, -397, 1100], + [-173, 919, 1589, -5363], + [-13, 796, -295, -6655], + [-1860, -829, 1141, -4555], + [2298, -838, -664, -5005], + [-884, -1097, 2074, -4613], + [-101, 281, 2846, -4535], + [1166, 453, 2429, -5910], + [879, -664, 2370, -5452], + [1415, -370, -1699, -4727], + [-1413, 1277, -669, -6649], + [2133, 304, -968, -4624], + [380, 586, -2087, -4892], + [1336, 275, -82, -5789], + [-2459, 1057, -34, -5416], + [2278, -1758, 866, -5653], + [1945, -2295, -149, -5302], + [1287, -3525, 996, -5255], + [2297, 803, 1177, -6067], + [187, -180, -619, -6202], + [-793, -2537, 1554, -5057], + [-2703, -204, -629, -5853], + [-1007, -146, 313, -5582], + [830, 357, 869, -6363], + [-228, -575, -3177, -4433], + [-1001, -1553, -142, -5708], + [-1644, 1683, 1721, -4533], + [893, 1924, -15, -5791], + [2195, 2061, -262, -5471], + [3031, 270, 311, -5096], + [1912, 1638, -1523, -4677], + [-3142, -55, 253, -4914], + [356, -1680, 343, -6123], + [-2241, -1734, -976, -5939], + [-2196, -2893, 547, -4938], + [-1245, 126, -1916, -5419], + [-249, -3755, -1422, -5594], + [575, -2683, -1926, -4566], + [-762, 1885, 192, -5880], + [-811, -2562, -1068, -6013], + [-2264, -3086, -976, -4775], + [70, -1215, 2880, -4410], + [714, -3760, 2916, -4691], + [-244, -3404, 1740, -4493], + [684, -5137, -328, -5608], + [-529, -3825, -1786, -4535], + [-713, -4743, -1118, -5546], + [2718, -3788, 1798, -5708], + [-1639, -3679, -1564, -6095], + [1693, -2642, -1389, -4539], + [505, -1573, -1651, -4878], + [-835, -2256, -1941, -5352], + [1464, -411, 1993, -6441], + [493, -3184, -145, -6148], + [-1413, 499, -1617, -6479], + [-294, 1722, -1419, -5725], + [-2937, -1528, -175, -4624], + [-594, -5911, -56, -6146], + [-300, -4275, 1156, -5947], + [552, -2643, 2669, -3959], + [905, -4158, 1789, -5809], + [1336, -2009, 2108, -5903], + [1555, -3600, 1110, -6759], + [-1294, -3464, 77, -6084], + [-1139, -4006, -1270, -4181], + [-5094, -3296, 1092, -2847], + [-5503, -2883, 1984, -2067], + [-4671, -4218, -1417, -4132], + [-3763, -3818, 1262, -3082], + [-5132, -3430, 2928, -728], + [-5957, -2877, 1251, -2446], + [-4425, -2319, -212, -4276], + [-6201, -1993, 1774, -2182], + [-5500, -3836, 2201, -1396], + [-6934, -2334, 2366, -1293], + [-6124, -4140, 1337, -1977], + [-6553, -4186, 1756, -1325], + [-5126, -1258, 744, -3656], + [-5167, -1390, 1581, -2895], + [-4525, -3398, 2429, -1865], + [-4076, -3183, 2027, -2510], + [-6191, -3274, 1838, -1814], + [-4454, -2753, 2723, -1185], + [-6655, -4797, 251, -2595], + [-6332, -2232, 1832, 217], + [-5869, -1698, 134, 340], + [-6614, -1045, 2126, -1932], + [-4859, -2107, 2010, -2435], + [-6274, -1622, 2808, -1374], + [-3119, -3209, 521, -3988], + [-5676, -2082, -420, -2711], + [-7073, -3623, 696, -2343], + [-5986, -4224, 572, -2454], + [-4340, -4521, 882, -2771], + [-6178, -1933, 535, -1444], + [-4923, -4163, 1744, -2066], + [-6410, -1519, 1058, -2683], + [-5077, -1185, 856, -2216], + [-7091, -2444, 687, -2597], + [-5284, -2165, 3239, -993], + [-4763, -1497, 197, -3179], + [-4128, -4958, -396, -3578], + [-5054, -3878, -647, -2672], + [-7005, -3348, 1679, -1579], + [-5767, -1017, 2582, -1915], + [-7069, -2787, 1331, -2070], + [-5532, -2296, 706, -2950], + [-5059, -3543, -821, -3637], + [-6639, -1835, 1016, -696], + [-5611, -5220, -694, -3371], + [-5994, -2803, 2933, -729], + [-5948, -619, 1596, -2676], + [-5486, -4419, 153, -3265], + [-4329, -3440, 1646, -1439], + [-4083, -3978, 177, -3569], + [-4289, -2599, 1224, -3075], + [-5707, -3253, 1912, -759], + [-6606, -3437, 2562, -571], + [-5254, -2444, 769, -352], + [-6545, -3154, 582, -1103], + [-5328, -2241, 2566, -1775], + [-7216, -1936, 1538, -1983], + [-3730, -2451, 426, -3869], + [-5110, -1385, 2031, -1169], + [-6470, -2715, 269, -3123], + [-5806, -2480, -97, -3832], + [-3683, -4916, -490, -4330], + [-6341, -2083, -669, -115], + [-4913, -4079, -837, -4673], + [-3274, -2497, 2334, -2652], + [-1286, -1731, 2550, -3756], + [-3375, -877, 926, -3977], + [-2525, -2079, 2879, -2625], + [-5308, -504, 3111, -1607], + [-4904, 460, 4093, -1232], + [-1993, 1616, 4656, -1913], + [-3481, -1176, 3119, -2236], + [-4132, -1502, 2339, -2545], + [-2542, 1151, 3569, -2550], + [-4381, 430, 3147, -2082], + [-3888, 867, 3899, -1657], + [-2861, 1290, 4202, -1979], + [-3893, -253, 2363, -2764], + [-1705, 688, 3827, -2923], + [-2223, 2312, 3700, -3148], + [-1986, -720, 5021, -795], + [-3177, 242, 1952, -3352], + [-1854, 1509, 2528, -3815], + [-3173, 97, 5019, -706], + [-2689, -145, 1375, -3915], + [-4838, -385, 2488, -2427], + [-4557, -355, 1603, -3060], + [-3522, 1832, 3292, -2674], + [-3769, 780, 2378, -2704], + [-4323, -1932, 3414, -1169], + [-2740, 1158, 2729, -3273], + [-3647, 210, 1464, -2892], + [-2342, -2097, 1513, -3727], + [-4422, -1242, 3130, -1833], + [-1308, -1039, 4290, -1875], + [-1754, -2535, 3298, -2314], + [-4102, -186, 4037, -1094], + [-1008, 1570, 3290, 171], + [-3322, -2621, 2791, -1536], + [-2539, -2597, 3442, -1672], + [-3411, -2015, 3670, -1174], + [-2097, 730, 5581, -1399], + [-1510, -74, 4820, -2004], + [-4086, -868, 4425, -771], + [-956, -986, 3640, -2925], + [-2087, -1250, 3464, -2458], + [-3308, -2411, 1334, -3667], + [-2264, -389, 4004, -1854], + [-680, 239, 4058, -3388], + [-1357, 30, 2993, -3658], + [-3601, -552, 1177, -1136], + [-2641, 442, 4374, -1625], + [-2525, 770, 1640, -3895], + [-3172, -891, 3893, -1608], + [-2996, 13, 3277, -2414], + [-899, 1055, 4470, -2501], + [-422, -584, 3475, -3787], + [-1978, -593, 2566, -3415], + [-3150, -1280, 2362, -3047], + [-3592, 224, 1026, -3932], + [-4840, -1189, 3633, -879], + [-3952, -2255, 2916, -1826], + [-1695, 28, 1810, -349], + [-745, -2484, 3308, -3293], + [-1016, 1563, 5365, -1823], + [-2172, -1787, 4266, -1287], + [-1241, -1951, 3982, -2413], + [-2009, -2639, 2330, -3480], + [5105, -1618, -2588, -2015], + [6497, -1523, -3218, -910], + [6526, -2305, -2029, -1790], + [5289, -99, -3436, -400], + [5781, -1623, -1577, -2617], + [5259, -670, -3125, -1700], + [6343, -1256, -331, -3222], + [7967, -678, -2195, -1462], + [6119, -695, -2988, -1538], + [6108, 494, -3359, -1548], + [5067, 969, -2328, -2707], + [7595, -435, -1497, -2056], + [6929, -719, -2420, -1665], + [5190, 584, -2982, -2103], + [6106, -444, -1411, -2739], + [5584, 289, -1804, -2803], + [5276, 227, -1180, -3361], + [7544, -1525, -1834, -1725], + [5986, -1470, -2606, -1701], + [5096, -765, -1712, -3006], + [5423, -149, -3933, -1157], + [7651, 26, -2445, -1507], + [4745, -464, -1735, -2362], + [5352, -1011, -1094, -1999], + [6300, -672, -542, -1950], + [6675, -1020, -1318, -1059], + [7218, -2036, -603, -2462], + [7755, -1514, -2430, -1229], + [5041, 449, -1056, -2405], + [6710, -2277, -1344, -2284], + [6824, -1347, -2254, 251], + [6068, -1857, -983, -1316], + [5603, -2177, -2730, -1477], + [5838, -1059, -3604, -970], + [5076, -789, -335, -2413], + [6191, -1634, -2000, -2129], + [5092, -1292, -2543, -1034], + [5305, 435, -1710, -1850], + [6140, 561, -2176, -2380], + [6752, 348, -2496, -1890], + [6405, 273, -1098, -2778], + [6942, -1340, -496, -1381], + [5238, -687, -2454, -2349], + [6959, -882, -1833, -2061], + [6292, -253, -2125, -2199], + [5838, -574, -759, -3215], + [6954, -1484, -640, -2771], + [7498, -1706, -1210, -2154], + [6772, -1003, -1235, -2532], + [6014, 228, -2154, -1108], + [6943, -2178, -2644, -1122], + [7262, -763, -3056, -1090], + [6273, -1478, -1072, 177], + [4734, 425, -2912, 357], + [7129, 168, -1537, -2327], + [7204, -434, -746, -2660], + [6879, 57, -3087, -1310], + [4623, -610, -718, -3459], + [6565, -543, -1998, -339], + [4752, -277, -2066, -1405], + [7435, -1416, -1904, -505], + [4076, 150, -1222, -3556], + [7082, -28, -1456, -1174], + [5941, -446, -1326, -1158], + [3870, -1648, -2474, -2589], + [858, 37, -3387, -3721], + [3557, -1503, -1664, -3383], + [3336, -1972, -3079, -2216], + [3186, 60, -4185, -863], + [3456, -773, -3066, -2457], + [4131, -913, -2060, -2601], + [4431, -691, -4114, -972], + [3461, -334, -3680, -1751], + [2006, -459, -2214, -3827], + [1322, 32, -2816, -3203], + [4425, -1897, -2791, -1946], + [4504, 23, -3421, -1909], + [3090, -885, -2366, -3264], + [3209, -2363, -3730, -834], + [3312, -1471, -3641, -1579], + [4184, -1669, -3323, -1248], + [2190, -931, -3302, -2944], + [2947, -229, -4791, -1195], + [2020, -1626, -2700, -3125], + [2214, -326, -4352, -1683], + [3286, -2619, -2412, -2458], + [1000, -2571, -4129, -2158], + [2496, -2627, -3611, -1433], + [2043, -2191, -2167, -3827], + [2571, -2544, -1915, -3222], + [2022, -1501, -3856, -2165], + [2685, -1180, -1461, -4038], + [1610, -2313, -4391, -1173], + [2340, -2490, -4215, -516], + [1742, -2615, -3632, -2146], + [523, -1293, -4246, -2442], + [3725, -2723, -3014, -1576], + [3554, -1381, -4200, -824], + [1291, -1594, -4777, -1430], + [1452, 515, -2960, -3830], + [4264, -894, -3305, -1826], + [2606, -1452, -4522, -966], + [1196, -830, -4807, -1816], + [1054, -775, -2616, -4071], + [4206, 415, -4344, -1132], + [3044, 491, -4126, -1934], + [988, -901, -3353, -3443], + [1729, -3063, -2267, -3370], + [3915, 912, -2989, -2387], + [3781, 300, -2457, -3050], + [2712, 924, -1350, -1206], + [4230, 405, -2343, 665], + [1878, -873, -225, -29], + [3510, 56, -1334, -3420], + [2850, 1447, -2651, -3150], + [1510, -706, -4125, -2483], + [3115, 793, -1692, -3894], + [2667, 213, -2973, -2786], + [1184, -2384, -3051, -3173], + [2139, 796, -2079, -3697], + [1464, -1483, -3726, -2754], + [2407, -1148, -3915, -1569], + [2612, -1779, -3217, -2271], + [2406, -2870, -2937, -2496], + [2140, 126, -3646, -2758], + [2952, -1036, 268, -1423], + [93, -1931, -3841, -3535], + [389, -2953, -3383, -3343], + [8652, -5511, -1662, 565], + [7427, -2791, -2535, -842], + [8541, -4253, -1407, -988], + [8018, -3203, -2998, 105], + [7231, -3926, -958, 1308], + [7331, -3690, -363, 2586], + [6803, -3646, -2226, -903], + [8163, -2811, -477, -2235], + [9356, -3818, -1685, -684], + [8466, -2854, -302, -698], + [8458, -3224, 517, 279], + [8074, -2619, -1326, 2596], + [8779, -2761, -2527, -441], + [6533, -2887, -899, -696], + [7394, -2305, -1642, -120], + [8281, -3780, -22, 1305], + [9158, -4413, -779, 901], + [9031, -5240, -1109, 1678], + [8717, -3650, 410, -1075], + [7317, -3197, -818, -2264], + [7934, -2385, -1214, -1886], + [8256, -4441, -291, -587], + [7358, -3395, 1090, -270], + [9446, -4910, -1343, -473], + [8187, -4726, -808, 1166], + [7504, -3845, -47, 267], + [8029, -2146, -1283, -383], + [7461, -2705, -853, 783], + [9367, -3636, -645, -354], + [8955, -3473, -308, -1947], + [8676, -2683, -2099, 1485], + [7481, -3003, -871, -444], + [8015, -2839, -1673, 1175], + [6947, -4643, -1527, -1047], + [7622, -2575, -137, -960], + [9388, -4279, -707, -1322], + [8382, -5259, -1283, -565], + [6856, -4138, -1030, 630], + [8659, -2571, -1124, -1666], + [8763, -3807, -537, 2543], + [8049, -3578, -2186, -604], + [8272, -2351, -1985, -1214], + [6855, -3796, -1527, -1631], + [7178, -2896, -1600, -1756], + [7040, -2888, -89, -1586], + [6261, -3403, -264, 998], + [7756, -4699, -1543, -834], + [7682, -4622, -758, -1721], + [8839, -4232, -2932, 1959], + [9363, -4679, -1956, 39], + [7883, -3616, -1414, -1432], + [8828, -3188, -1356, -1312], + [7746, -3987, -121, -2424], + [9262, -3256, -693, 818], + [7670, -3420, -148, 3504], + [7344, -3183, 608, 1595], + [8976, -4139, -1848, 1304], + [6708, -4131, 33, -852], + [7840, -4429, -2275, 79], + [8980, -3858, -2838, 453], + [7815, -4604, -2563, 944], + [8372, -4422, -1783, 3071], + [8623, -5128, -1754, 2888], + [7462, -3281, 889, 920], + [8416, -59, -1320, -1825], + [7928, -1488, -414, -2499], + [8110, -977, -1047, -2042], + [8278, -687, -1597, -1550], + [7988, -174, -977, -2106], + [8609, -1547, -1628, -1527], + [9000, -1798, -946, -1761], + [8954, -872, -1404, -1594], + [8939, 466, -748, -1212], + [9549, -329, -177, -1360], + [9411, -18, -1126, -1568], + [8859, -782, -488, -1338], + [8955, -218, -43, -1209], + [9131, -69, -453, -1001], + [9069, -1519, -1091, -1199], + [9247, -1309, -566, -1146], + [8528, -1617, -287, -1313], + [7763, -745, -149, -2040], + [8294, -343, 257, -2633], + [10149, -893, -552, -1649], + [9398, -915, 218, -2042], + [9703, -1194, -675, -1592], + [9586, -700, -427, -1710], + [8930, 497, -1445, -1218], + [9285, -1323, -163, -1552], + [8431, -1289, -985, -1404], + [8965, -655, 653, -1483], + [9542, -1001, -951, -1128], + [9205, -647, -37, -882], + [8603, -56, 514, -1793], + [9300, -12, -1324, -567], + [8773, 238, -184, -1456], + [9941, -1306, -69, -1792], + [9360, 279, -376, -1919], + [9180, -285, 95, -2170], + [9922, -501, -970, -1570], + [8341, -1493, -856, -2092], + [8780, -981, -850, -1014], + [9721, -548, -1504, -1094], + [9973, -1493, 482, -2105], + [8707, -333, -1027, -1087], + [9098, -469, -315, -1723], + [8879, -1050, -661, -2020], + [8857, 602, -866, -1918], + [8945, -1025, -2154, -1071], + [8484, -1930, -468, -2179], + [9177, -1903, -224, -2112], + [8652, -137, -2097, -1214], + [9063, -973, -1405, -772], + [9328, -456, 662, -2469], + [10101, -697, 127, -2113], + [9685, 811, -2359, -1024], + [8586, -94, -460, -1982], + [7924, -141, -509, -2513], + [7773, -669, -107, -2835], + [8636, -1064, -46, -2409], + [9748, 596, -1815, -1349], + [8924, 304, 547, -2614], + [9442, 746, -1153, -1679], + [9454, -278, -529, -1976], + [8488, 561, -32, -2160], + [10083, -63, -1544, -1364], + [9390, -1278, 568, -1131], + [9740, -49, -2253, -910], + [3636, -2391, -1115, -3614], + [6014, -3204, -1902, -1808], + [5787, -3497, -1116, -2590], + [4365, -3046, -1632, -2668], + [4733, -2192, -2029, -2468], + [5412, -2753, -1633, -2464], + [4455, -3375, -767, -3399], + [4456, -1644, -983, -2841], + [4039, -2523, 38, -3967], + [3406, -2662, 72, -4757], + [4279, -2005, 1055, -4399], + [4321, -1377, -860, -3786], + [3743, -5739, -651, -3047], + [3528, -5510, 361, -4060], + [6496, -4886, -136, -2689], + [4513, -5254, 551, -4010], + [6557, -3413, -92, -3063], + [4186, -2059, 187, 47], + [6210, -4117, -1256, -1985], + [6038, -4343, 351, -2124], + [4305, -4780, -2077, -1897], + [4480, -3815, -2228, -1533], + [5582, -3689, 1221, -3429], + [5532, -4874, 1195, -2765], + [6518, -2853, -905, -2568], + [5467, -2192, 470, -4115], + [4139, -1577, 240, -3493], + [5281, -1926, -729, -3340], + [5214, -2870, 1359, -4289], + [3046, -3510, -1536, -3214], + [5433, -2881, -1230, -1184], + [4861, -3932, -1071, -2791], + [5693, -4234, -1906, -1502], + [4004, -3935, -1804, -2383], + [3728, -3792, 681, -4773], + [3621, -3030, -1951, -2598], + [5133, -3903, 44, -3700], + [3561, -3451, 1183, -5301], + [5026, -2762, -2341, -1780], + [5841, -2492, -467, -3210], + [5591, -1791, 497, -2472], + [5054, -3898, -1822, -2097], + [5813, -2792, 83, -1469], + [4432, -4497, 1670, -5193], + [5338, -4653, -1109, -2200], + [3239, -4401, -648, -3655], + [2147, -3598, -1200, -4242], + [4417, -2271, -1552, -3210], + [6494, -4360, 852, -3565], + [2393, -6358, -856, -4524], + [4959, -4196, -847, -1403], + [4924, -5438, -226, -3026], + [4254, -5303, -1306, -2424], + [4121, -3126, -2334, -1981], + [3437, -4443, -1464, -2953], + [3203, -3459, -529, -4339], + [5896, -5945, 543, -3246], + [1987, -4733, -220, -4863], + [4358, -4431, -514, -3081], + [4583, -2416, -492, -2287], + [2943, -5035, 419, -4927], + [5358, -5129, 987, -4309], + [4460, -3392, 1752, -5634], + [3415, -4633, 1507, -5945], + [811, -4692, -445, 2333], + [1009, -5613, -1857, 1360], + [1338, -2712, -2720, 3036], + [1002, -3754, -2582, 2344], + [750, -4608, -2334, 714], + [2043, -3207, -2822, 2173], + [-140, -4654, -2953, 357], + [-54, -4026, -2376, 2695], + [1858, -5022, -717, 2287], + [2064, -3894, -722, 3255], + [2727, -4558, -332, 2603], + [1810, -5378, 283, 1826], + [3935, -4326, 762, 3383], + [-767, -4697, -2510, 1922], + [2146, -4312, -3090, 1641], + [54, -5881, -2114, 921], + [1992, -5766, -640, 1574], + [1200, -5371, -1114, 1828], + [2973, -5337, 34, 2266], + [1531, -5018, -2817, 1192], + [3078, -4570, 117, 1990], + [924, -4286, -1388, 2713], + [142, -5058, -2848, 1487], + [-106, -6180, -881, 842], + [673, -5433, -229, 1596], + [783, -5710, -2784, 562], + [1935, -5729, -2009, 856], + [-410, -3375, -3326, 2734], + [234, -3000, -2628, 3260], + [733, -3405, -3806, 1589], + [771, -4285, -3544, 1314], + [1192, -3563, -3960, 2178], + [206, -5555, -1250, 1546], + [-130, -3815, -1210, 3041], + [646, -3940, -393, 2992], + [-184, -4931, -1767, 1925], + [2746, -5120, -2275, 1464], + [2440, -3731, -3352, 2729], + [-490, -4942, -3779, 997], + [68, -2636, -4167, 3778], + [48, -3986, -4118, 2106], + [-978, -5486, -1336, 1390], + [1126, -5297, -855, 640], + [-472, -3975, -3622, 1557], + [2456, -5344, -1523, 1648], + [-774, -5652, -2417, 1147], + [995, -6122, -812, 1132], + [3282, -4571, -1763, 2175], + [3655, -3862, -676, 3568], + [3038, -3647, -1672, 3381], + [2595, -2964, -2772, 3263], + [4176, -3353, -1148, 4354], + [1603, -3442, -1500, 3444], + [828, -6226, -1783, 678], + [1421, -3333, -3080, 3403], + [1121, -4727, -1924, 1984], + [-186, -5083, -682, 1796], + [819, -2778, -3488, 530], + [421, -2873, -3832, 2596], + [2164, -4263, -1605, 2282], + [585, -4437, -682, -491], + [-644, -4452, -1157, 2325], + [1991, -4299, 210, 2834], + [2135, -3632, -2113, 665], + [-7482, -2724, -2662, -1380], + [-6983, -2166, -3756, -3509], + [-7085, -1439, -2397, -3112], + [-7760, -3049, -3319, -2822], + [-8413, -2760, -4406, -3298], + [-5995, -3943, -1260, -3750], + [-7879, -1554, -3464, -2606], + [-6314, -2034, -3878, -1681], + [-8849, -2084, -1399, -1231], + [-7153, -2602, -1384, -817], + [-8041, -2571, -407, -2785], + [-7246, -2233, -1578, 260], + [-7336, -3883, -4061, -1342], + [-7619, -3908, -2342, 382], + [-8684, -3724, -1662, -727], + [-7850, -2922, -1770, -3449], + [-6766, -2034, -1293, -1988], + [-6895, -2116, -968, -3744], + [-7136, -5147, -2618, -2809], + [-8224, -3724, -2519, -1589], + [-6711, -2750, -3021, -219], + [-8059, -1638, -1102, -3175], + [-8710, -4839, -3963, -3143], + [-9363, -4965, -3257, -1002], + [-6099, -1751, -3157, -395], + [-6453, -3216, -4597, -483], + [-7879, -5477, -839, -2638], + [-7202, -4038, -526, -2856], + [-8022, -1228, -1910, -1646], + [-9117, -1393, -1582, -2535], + [-9095, -2693, -636, -2605], + [-9076, -2580, -3481, -2519], + [-8327, -4859, -2422, 83], + [-8368, -2129, -2324, -2173], + [-8554, -4563, -3842, -2007], + [-10462, -4261, -1934, -2084], + [-9717, -3187, -2294, -1896], + [-9625, -3889, -3020, -3224], + [-9857, -4955, -4239, -2184], + [-9752, -2351, -2277, -3129], + [-7219, -1302, -2639, -1603], + [-7477, -4360, -3718, -559], + [-5680, -2033, -2326, -3078], + [-10190, -5548, -4643, -3601], + [-9431, -4121, -879, -2479], + [-8365, -5450, -2020, -1439], + [-6289, -5178, -1605, -3845], + [-8319, -3866, -687, -2792], + [-8131, -1031, -3608, -3947], + [-10510, -2560, -1199, -2082], + [-11015, -3640, -2748, -3041], + [-8762, -5022, -5231, -1162], + [-10153, -2715, -4648, -4859], + [-7930, -5205, -1900, -3600], + [-9561, -3548, -4812, -3722], + [-7663, -4709, -1180, -1475], + [-9073, -5707, -1815, -2980], + [-8602, -2363, -2675, -3770], + [-9967, -5614, -3575, -3838], + [-8324, -1005, -2131, -3254], + [-10331, -5737, -2550, -2940], + [-8234, -3354, -3361, -4479], + [-8140, -1951, -4526, -4545], + [-6679, -2662, -2284, -4182], + [-1122, -1514, -6427, -212], + [54, -1660, -5424, -1404], + [254, -2778, -5222, 846], + [-267, -1661, -6577, 814], + [-305, -2021, -5759, 1484], + [-1791, -2446, -6867, -86], + [-2929, -3158, -6603, -1799], + [-1391, -3189, -5557, -1053], + [-1602, -884, -6767, -1213], + [-361, -318, -6219, -44], + [-4078, -2635, -5523, -433], + [-956, 478, -4382, 1470], + [-3300, -2462, -6021, -2721], + [708, -2434, -5085, -540], + [-2435, -3607, -5647, -2110], + [-491, -1134, -4681, -2886], + [87, -3435, -4641, -1194], + [-586, -2927, -4784, 366], + [-1394, -2326, -6021, 350], + [97, -2519, -4678, -2120], + [-1547, -1907, -5069, -2993], + [268, -3724, -4719, 127], + [-827, -1190, -5912, 1144], + [-3959, -2322, -6898, -1974], + [-2728, -2228, -6426, -562], + [-456, -666, -5785, -1609], + [531, -1096, -5731, -656], + [-3569, -688, -3915, 110], + [-4752, -1725, -4393, -377], + [-3210, -3315, -6960, -840], + [-688, -3416, -4971, 1221], + [-1833, 77, -6491, -2434], + [-239, -255, -6850, -886], + [-2112, -1490, -6291, -2689], + [-1544, -4579, -5198, -1261], + [-2771, -4014, -5520, 683], + [-1635, -2829, -5512, 1214], + [-958, -2582, -4823, 2360], + [-2077, -4566, -4642, 365], + [-3112, -4214, -5960, -823], + [-2467, -2510, -4858, 1467], + [-1561, -3399, -5822, 211], + [-775, -1081, -4424, 2636], + [-1263, 25, -6378, -1392], + [-3476, -366, -5417, -1393], + [-3176, -1476, -4149, 1466], + [-2479, 518, -4448, -257], + [-2992, 158, -4660, -1279], + [-1320, -3872, -4479, 1147], + [-1475, -312, -5318, 539], + [-3527, -1679, -5860, -1681], + [-3397, -3438, -5593, 1866], + [-4089, -2439, -4763, 1275], + [-748, -4513, -4687, -48], + [-2166, -4531, -4691, -2856], + [-2385, -853, -6035, -627], + [-1194, -4091, -4472, -1963], + [-682, -3234, -4084, -3033], + [-3255, -5015, -5328, -12], + [-2313, -3436, -4601, -155], + [-2792, -1038, -6947, -2019], + [-1244, -1526, -5771, -1882], + [-4679, -3731, -5506, 283], + [-3062, -66, -3558, -758], + [-4895, -1187, 4751, 3728], + [-7600, -2752, 3320, 4613], + [-5703, -2975, 3944, 2659], + [-4972, -1257, -246, 2952], + [-4221, -2487, 1702, 4295], + [-2900, -1529, 2458, 4935], + [-5061, 407, 2416, 4050], + [-6931, -3478, 2761, 2213], + [-6037, -3921, 3192, 1866], + [-6113, -811, 2407, 3782], + [-5878, -1716, 1207, 3478], + [-5953, -2853, 2207, 2712], + [-6807, -3223, 2749, 3595], + [-3272, -3157, 1389, 3788], + [-5368, -1904, 1980, 5077], + [-7235, -1398, 3075, 4548], + [-4765, -3487, 2755, 2796], + [-7658, -4435, 2694, 2582], + [-6997, -4282, 456, 3832], + [-5563, -3115, -63, 3713], + [-4244, -4220, 1450, 2767], + [-3801, -2194, 190, 4303], + [-5458, -4119, 1958, 2274], + [-7300, -3469, 3514, 3193], + [-4594, -2067, 775, 4752], + [-3389, -1654, 1464, 5412], + [-4845, -3483, 964, 3437], + [-6007, -2818, 1666, 4659], + [-8709, -5007, 1757, 3287], + [-5833, -4389, 1025, 3171], + [-5788, -1780, 3944, 3661], + [-4430, -920, 1938, 4753], + [-7066, -1857, 4591, 4538], + [-3549, -513, 1427, 5317], + [-7517, -1220, 2883, 3049], + [-7605, -2687, 1874, 2735], + [-8718, -4035, 2676, 3730], + [-7990, -3907, 1185, 2607], + [-6058, -1744, 3349, 5157], + [-5954, 565, 3161, 3250], + [-6478, -612, 1930, 2271], + [-6535, -1445, -2, 1618], + [-8963, -4151, 1192, 4044], + [-7227, -3570, 1600, 4234], + [-4674, 79, 595, 3015], + [-3974, 430, 2727, 5137], + [-5299, 9, 3714, 4779], + [-6779, -2699, -8, 2436], + [-7016, -1145, 1293, 2310], + [-6955, -3312, 1534, 1801], + [-4025, 740, 1850, 4054], + [-9589, -3460, 4154, 5270], + [-4404, -1181, 4298, 5173], + [-7356, -4583, -18, 2644], + [-6516, -1235, 4439, 6234], + [-3453, -301, 4344, 4464], + [-4643, 1530, 3315, 4340], + [-4575, -2557, 3754, 3682], + [-3643, -3501, 2051, 2997], + [-5412, -2475, 2301, 1579], + [-5846, 259, 1360, 2348], + [-5258, -1358, 1050, 838], + [-5542, -219, 6377, 5750], + [-5713, -2952, 922, 899], + [-2049, -1135, 5206, 1033], + [-1693, -1886, 4835, -106], + [-2344, -3504, 4232, -13], + [-2475, -2334, 5043, 1126], + [-787, -2549, 3880, 2138], + [-3159, -2341, 4830, 2887], + [-1780, -1009, 6240, 2061], + [-4327, -3363, 2818, 886], + [-3376, -2743, 4104, 207], + [-3250, -4640, 2718, 1498], + [-382, -1075, 4382, 3460], + [-2416, -4168, 3530, 816], + [-1756, -2708, 4861, 622], + [-1879, -2097, 5156, 2889], + [-2496, -2418, 3722, 2671], + [-2717, -3252, 3341, 1944], + [-4063, -4091, 3306, 267], + [-3549, -3808, 3747, 842], + [-2635, 546, 5794, 1894], + [-1857, -1121, 4383, 3964], + [-2226, -2166, 3489, 3678], + [-3492, -660, 5323, 1063], + [-3033, -3130, 4382, 1828], + [-2703, -625, 6369, 2851], + [-1656, -2842, 4584, -528], + [-4781, -2622, 4390, 2097], + [-413, -2045, 5081, 3035], + [-3810, -2662, 4532, 1095], + [-3144, -1858, 5215, 1880], + [-3562, -1795, 4928, 670], + [-4800, -1509, 5189, 1859], + [-1085, -3832, 4169, 900], + [-1969, -3270, 2857, 2878], + [-4267, -4140, 3176, 1805], + [-5145, -3727, 3524, 1168], + [-1346, -1876, 5501, 1748], + [-4998, -2945, 3699, 338], + [-3458, -3096, 3406, -635], + [-1751, -3209, 3508, 395], + [-2507, 170, 5987, 705], + [-3756, -1072, 5647, 3536], + [-2870, -1439, 5026, 3212], + [-3913, -3225, 3669, 2144], + [-3739, 226, 5747, 764], + [-2052, -820, 5266, 3093], + [-3214, -3820, 2409, 2391], + [-4398, -2588, 3501, -218], + [-4484, -1763, 4180, -198], + [-3368, -1525, 4362, -134], + [-2407, 224, 4905, 3533], + [-1369, -2937, 4728, 1788], + [-4848, -1707, 4159, 851], + [-3454, -1749, 4281, 3230], + [-1990, -3853, 3487, 1735], + [-3117, 92, 6155, 4075], + [-2676, -2472, 4078, -589], + [-1547, -2012, 2626, 1835], + [-4275, -588, 4824, 725], + [-601, -2249, 3736, 3548], + [-4060, -61, 5333, 3097], + [-4303, 7, 6551, 3054], + [-5003, -1029, 5786, 3319], + [-2810, -728, 5392, 199], + [-1232, -200, 5228, 3121], + [2621, 165, -6255, 298], + [3669, 537, -6844, 1564], + [1598, -1190, -6235, 2523], + [2164, -32, -6894, 1383], + [853, -1597, -6069, 1449], + [1377, -1661, -5266, 108], + [2660, 48, -5172, -517], + [1903, -391, -5677, 1010], + [3792, 206, -5274, -11], + [1239, 2776, -2929, 2721], + [4071, 149, -7259, 3125], + [1436, -480, -6156, -196], + [1373, -1960, -5005, 3122], + [3413, -1271, -5176, 3283], + [3060, -68, -6495, 2238], + [2700, -2075, -4681, 91], + [2928, -1728, -5168, 1858], + [4424, 828, -4471, 88], + [2672, -2604, -4038, 2753], + [5223, -123, -6749, 2295], + [4237, -420, -5538, 1353], + [4744, -1281, -4097, 4708], + [1103, -2764, -4751, 2024], + [3747, -1913, -3911, 3960], + [2470, -1416, -5542, 615], + [4847, -1354, -5334, 1733], + [5336, 88, -7593, 4007], + [2388, -2880, -4807, 1037], + [4495, 1391, -5685, -139], + [5253, 1637, -6450, 1533], + [1199, 795, -5515, 1261], + [1397, -1259, -4252, 3838], + [746, 70, -6640, 604], + [1584, 166, -4972, 3072], + [380, -999, -5397, 2267], + [2974, 1707, -3242, 5360], + [5202, -403, -5453, 2832], + [3718, -1731, -4760, 714], + [4150, -975, -4792, 61], + [2925, -818, -4841, 15], + [5301, 577, -4006, 3259], + [5265, 1986, -5679, 3028], + [3752, 1928, -4509, 3729], + [3278, 1925, -6370, 1247], + [5107, 1721, -4853, 3127], + [3279, 2982, -2515, 4005], + [4622, 668, -6204, 759], + [6034, 317, -5763, 4818], + [-558, 57, -3785, 2817], + [4476, 1616, -3965, 4536], + [5953, 2056, -8215, 2715], + [4387, 2613, -7463, 868], + [5834, 1088, -4736, 4924], + [6473, -856, -6991, 4172], + [4959, -293, -5162, 76], + [2731, -843, -6119, 3847], + [3245, 1202, -6833, 616], + [2553, 1383, -3829, 3859], + [4332, 2099, -3480, 3622], + [2110, 2683, -2728, 3990], + [876, 1167, -3290, 3466], + [3991, 1709, -2410, 4077], + [5105, 939, -2584, 3256], + [4719, 688, -1566, 3040], + [-3632, 4335, 1266, -3303], + [-4956, 3207, 1312, -2806], + [-4669, 2627, 2663, -2435], + [-4282, 3708, 2303, -3038], + [-4536, 2297, -175, -3350], + [-5234, 2503, -139, -880], + [-3978, 1512, 1092, -3619], + [-4519, 4649, 1363, -2455], + [-5118, 3132, 1961, -1577], + [-5196, 3379, -182, -1378], + [-6420, 4486, 2397, -1993], + [-5030, 5046, 1292, -1118], + [-4559, 2573, -927, -1406], + [-3501, 3730, 691, -4930], + [-4364, 2758, 1007, -3909], + [-4026, 2839, -1559, -2340], + [-5037, 4053, 836, -1571], + [-4727, 5136, 1110, -3588], + [-5245, 2799, -999, -2164], + [-4954, 1501, 422, -3963], + [-5994, 2726, 1462, -2833], + [-5621, 5159, 2038, -2512], + [-4991, 2291, 1917, -3151], + [-5469, 4382, -148, -2978], + [-5858, 1983, 807, -2720], + [-4709, 3556, 952, -467], + [-2489, 2362, 1714, -4230], + [-4717, 5004, -1180, -3672], + [-5914, 3653, 1359, -1317], + [-5506, 2995, 780, -1059], + [-5287, 3945, 2480, -2293], + [-3849, 4358, 322, -1770], + [-3911, 3570, 252, -3185], + [-3660, 5128, 158, -3719], + [-4599, 3277, -503, -2727], + [-3673, 3760, -1252, -3339], + [-5161, 2337, 388, -1943], + [-3529, 2216, 2156, -3080], + [-4309, 4331, 1808, -1460], + [-4782, 3820, 480, -2504], + [-4166, 3544, -378, -1567], + [-5572, 2466, -418, -2909], + [-6096, 2930, 119, -1878], + [-5963, 3554, 1011, -2233], + [-6433, 4335, 935, -2930], + [-5004, 3314, -1352, -3430], + [-6042, 3463, -1008, -3940], + [-4671, 2214, -640, -5040], + [-2795, 3759, 1412, -3803], + [-3647, 4436, 729, -515], + [-3594, 1033, 56, -4148], + [-2908, 3027, 2889, -3485], + [-3338, 2234, 313, -4285], + [-3825, 4497, -561, -2634], + [-6167, 3012, -48, -3149], + [-4828, 3515, -969, -4475], + [-5789, 2757, -539, -4173], + [-2452, 3067, 564, -4249], + [-4921, 1358, 1331, -2889], + [-3127, 4239, -1045, -1523], + [-4780, 2326, -1118, -3446], + [-3908, 5546, 152, -2622], + [-6972, 2976, 337, -2809], + [-4839, 4613, -35, -4077], + [-1408, 4822, -1149, -4997], + [-981, 4979, -912, -6304], + [-2098, 5689, -888, -2878], + [-3343, 4814, -657, -4434], + [-2461, 3601, -967, -4869], + [-2652, 3944, 87, -5520], + [-1104, 6076, 174, -6407], + [355, 5370, -1721, -5869], + [1242, 4497, -1107, -5091], + [-89, 4002, -1491, -5182], + [1059, 5693, -1591, -4905], + [1323, 4682, -2078, -4768], + [818, 3996, -549, -5468], + [-287, 4529, 929, -5543], + [-919, 5519, -2791, -2844], + [-1407, 5679, -3289, -3974], + [-189, 6530, -3547, -4002], + [-900, 7039, -3371, -4855], + [-2983, 7211, -363, -4835], + [-814, 6503, -104, -5106], + [-2386, 6896, 809, -4919], + [845, 4492, 352, -6621], + [-1998, 7237, -1646, -4231], + [-3380, 6251, 471, -4577], + [-1908, 7059, 84, -5726], + [-340, 6346, -803, -6265], + [-2279, 5834, -47, -4633], + [-1532, 5286, -1748, -1901], + [-2757, 6188, -453, -3415], + [-1255, 6405, -2043, -6357], + [918, 5581, -121, -5667], + [1840, 5336, -821, -5034], + [-2475, 4992, -1825, -3104], + [-2413, 5606, -1789, -4298], + [132, 5128, -2389, -4442], + [223, 6400, -2653, -4742], + [-673, 5012, 680, -4582], + [-1657, 6624, -349, -3596], + [-755, 6289, -1860, -3978], + [-572, 6894, -1946, -5207], + [-1141, 4756, -2665, -5586], + [-1073, 4269, -431, -4030], + [186, 5761, 916, -5868], + [-1907, 4836, 1017, -5106], + [-963, 3363, -1248, -6348], + [-3262, 4774, -1818, -5858], + [847, 3812, -2538, -4302], + [-1223, 5903, 1360, -5479], + [-1094, 6923, -1244, -2381], + [267, 6276, -709, -2846], + [-157, 5840, 1124, -4266], + [889, 3206, -910, -5305], + [-1736, 3344, 582, -4838], + [-2357, 5676, -2695, -6277], + [-1916, 6901, -986, -5397], + [-3062, 6028, -695, -5687], + [1836, 3566, -1357, -5226], + [-2176, 4938, 646, -3872], + [-2199, 3055, -208, -6124], + [-236, 3032, -821, -5325], + [-3989, 7277, -565, -3899], + [-595, 4362, 74, -5975], + [684, 5874, -841, -4424], + [-2731, 6305, -2389, -5465], + [-5775, 1325, -56, -2528], + [-7029, -534, -1890, -3278], + [-5798, -15, -2734, -2210], + [-5504, -1198, -353, -3659], + [-5079, 960, -894, -4336], + [-6073, -36, -133, -3014], + [-5782, -259, -1025, -3986], + [-6843, 1262, -807, -1639], + [-5263, -918, -3290, -579], + [-4840, 461, -2158, -533], + [-6014, -50, -620, 504], + [-5843, 241, -1359, -282], + [-5898, 577, 769, -3271], + [-6833, -946, -466, -3347], + [-6026, 1459, -512, -729], + [-7361, 747, -388, -1110], + [-6391, 2142, -1160, -2513], + [-6995, 304, 498, -2673], + [-6757, 679, -386, -433], + [-5222, 1688, -1093, -1032], + [-5019, 575, 184, -3627], + [-4237, 628, -3507, -1243], + [-7479, -456, -1722, -1486], + [-6464, 713, -1273, -1153], + [-6255, 1682, -606, -3607], + [-7033, 1497, -71, -1955], + [-6694, 1556, -1721, -3214], + [-6114, -356, 813, -2575], + [-5308, 632, -1851, -1636], + [-5742, -911, -1733, 383], + [-6083, -387, -2313, -879], + [-6535, -530, -1505, -2083], + [-4896, 1223, -2750, -1816], + [-6392, -463, -3247, -2093], + [-5373, 1264, -2706, -3042], + [-3894, -1390, -1020, -891], + [-6179, 1168, -1966, -1922], + [-5162, 1668, -1617, -1916], + [-6453, 920, -1169, -2432], + [-6130, 2005, -536, -1519], + [-6552, -98, -518, -1938], + [-7528, 355, -1101, -1772], + [-5745, 610, -247, -1360], + [-7003, 177, -2064, -1958], + [-6956, -570, -2220, -4225], + [-7830, 791, -1394, -2774], + [-7634, 480, -3171, -4224], + [-7913, 1154, -350, -2381], + [-5063, 1704, -1804, -2977], + [-4887, -524, -2703, 188], + [-5551, 406, -1620, -3063], + [-7109, 1342, 381, -3021], + [-6846, 631, -458, -3398], + [-4606, -605, 11, -3930], + [-8134, -225, -1738, -2648], + [-7043, 402, -2734, -3059], + [-7417, 1825, -2545, -4389], + [-6971, -236, -1031, -665], + [-5752, 2111, -1632, -3808], + [-7660, -78, -624, -3135], + [-6358, 619, -1951, -3911], + [-8134, 408, -1935, -3695], + [-6335, 1911, -2368, -4505], + [-7116, 2163, -344, -2753], + [2357, 4488, 2220, -5682], + [1385, 3206, 2300, -5305], + [1419, 2557, 5203, -3516], + [262, 4315, 3920, -1847], + [3316, 3187, 1612, -5609], + [1729, 2350, 1673, -6068], + [1603, 6126, 1467, -2839], + [-1339, 3316, 3691, -3530], + [-563, 4618, 3180, -4548], + [463, 4624, 3111, -5614], + [1246, 5455, 3356, -5720], + [480, 2149, 5422, -2893], + [1768, 4827, 913, -5579], + [-149, 5381, 4366, -3297], + [985, 3672, 2644, -92], + [-258, 2911, 5817, -2213], + [3428, 3289, 3351, -3541], + [-666, 3295, 4727, -2869], + [35, 6641, 4160, -4052], + [623, 6787, 3156, -4560], + [2654, 4360, 4676, -4632], + [1386, 5246, 4834, -4497], + [3488, 4574, 3856, -5946], + [383, 4481, 4168, -4110], + [1753, 3652, 4288, -3326], + [1344, 4905, 2508, -4660], + [1580, 4106, 3104, -2224], + [2027, 5038, 1683, -1554], + [446, 3699, 5872, -3013], + [4637, 4087, 3578, -5018], + [2629, 3560, 5331, -4900], + [1527, 6674, 2523, -4131], + [-1437, 2804, 2528, -4464], + [-229, 3355, 2016, -5537], + [3666, 3418, 4374, -4581], + [1192, 3799, 923, -6596], + [2040, 2956, 448, -5322], + [2468, 5768, 4029, -5869], + [3438, 6516, 3529, -6667], + [2737, 5495, 680, -5535], + [3896, 5727, 1801, -4958], + [4988, 4957, 3592, -6518], + [-542, 4416, 5794, -2787], + [4136, 4354, 2064, -4696], + [3067, 5936, 1207, -3396], + [2789, 4966, 2405, -3854], + [1731, 3270, 3251, -1063], + [1767, 5537, 2084, -2349], + [465, 3116, 4532, -837], + [1499, 2627, 4610, -2212], + [122, 3095, 3642, -3552], + [2542, 2866, 2705, -6402], + [3134, 4323, 698, -4785], + [731, 1859, 3112, -5242], + [2553, 2980, 3241, -4846], + [1329, 5310, 1607, -6624], + [2468, 1858, 3476, -1034], + [-172, 4996, 2000, -5562], + [2621, 4220, 1574, -3386], + [-333, 1832, 3362, -4117], + [2169, 6762, 3065, -6225], + [2844, 5528, 3223, -4765], + [526, 5175, 1644, -4267], + [2922, 4426, 2414, -2610], + [452, 1399, -4516, -2636], + [2872, 1720, -4667, -1435], + [1279, 702, -5424, -1984], + [2187, 870, -5021, -1341], + [583, -144, -4628, -2464], + [3, 2237, -5284, -2827], + [-19, 1005, -5460, -1819], + [2897, 2084, -5885, -515], + [-400, 3370, -5527, -2947], + [1505, 2593, -5518, -1802], + [1341, 4534, -5094, -1899], + [3241, 3670, -5493, -1252], + [-1287, 921, -5994, -1675], + [627, 408, -6652, -364], + [-260, 1127, -4849, -3247], + [371, 3400, -5976, -2285], + [1533, 1566, -6373, -610], + [2462, 4274, -6184, -1254], + [1782, 3363, -6222, -1381], + [572, 4650, -5673, -2754], + [2674, 3414, -4460, -2154], + [3614, 3820, -6883, -398], + [1136, -1, -5511, -1112], + [-1773, 1137, -5647, -2377], + [-753, 2104, -6085, -2565], + [-204, 3025, -4731, -1418], + [-1486, 1438, -4380, -216], + [302, 858, -5786, -264], + [3486, 1495, -5234, -783], + [888, 2327, -3423, -3720], + [-259, 772, -6596, -1311], + [-1197, 2073, -5174, -1826], + [1500, 3470, -4462, -2645], + [3072, 1960, -3277, -2264], + [1841, 952, -4324, -2340], + [1994, 2200, -3940, -2923], + [-1782, 1699, -4667, -1075], + [-1464, 2906, -3468, -375], + [366, 2380, -3747, 1467], + [-545, 1645, -4619, 376], + [1724, 2350, -2374, -3512], + [3184, 2628, -2996, -3275], + [734, 2010, -6239, -1479], + [524, 3756, -4496, -3263], + [1492, 3570, -3494, -3600], + [-932, 618, -5389, -2894], + [-133, 2161, -4083, -3267], + [786, 774, -3279, -3731], + [1078, 803, -3843, -3007], + [-332, 3405, -3347, 40], + [-17, 6, -4005, -3690], + [-189, 4372, -4488, -2561], + [-450, 3846, -3790, -1370], + [362, 2212, -5272, -15], + [-1529, 791, -6802, -2296], + [2145, 4241, -4474, 376], + [1813, 2426, -2932, -2726], + [-542, 4557, -3140, -1080], + [1192, 3784, -4371, -20], + [2784, 5188, -6399, -1394], + [431, 4561, -3673, -1398], + [1382, 3096, -4083, 1253], + [1209, 4224, -2930, 1500], + [2798, 2684, -6676, -606], + [-2396, 1510, -5381, -2713], + [-2625, 2542, -4032, -2880], + [-1231, 3967, -4098, -2886], + [-1393, 2374, -3862, -4525], + [-2495, 1665, -1637, -5445], + [-3854, 1759, -1750, -4944], + [-2373, 1668, -2856, -6251], + [-2668, 1981, -886, -4557], + [-2927, 4427, -3451, -6172], + [-1925, 2596, -4696, -2527], + [-3202, 2847, -3928, -5896], + [-3332, 1665, -5025, -3412], + [-3212, 3115, -4155, -4062], + [-1013, 3205, -5133, -3751], + [-2022, 4595, -3947, -5611], + [-3556, 1755, -3715, -2300], + [-1784, 4114, -2723, -1773], + [-3586, 4081, -2733, -4942], + [-1608, 3685, -4154, -4573], + [-3368, 4042, -4452, -6227], + [-1407, 3881, -5729, -3719], + [-2751, 3281, -5077, -4999], + [-3791, 2410, -4906, -5288], + [-730, 2303, -4217, -3755], + [-1812, 2311, -5492, -3709], + [-610, 4336, -3915, -3783], + [-2841, 4337, -4278, -4430], + [-1662, 4666, -4661, -3964], + [-589, 5209, -4923, -3682], + [-4155, 2234, -4076, -4218], + [-3951, 2770, -2665, -2805], + [-2302, 3228, -3717, -1908], + [-3129, 4373, -2264, -2851], + [-447, 1363, -3578, -4323], + [-2648, 4237, -3159, -3071], + [-4072, 3241, -3541, -4605], + [-4507, 3458, -2339, -3838], + [-1646, 997, -4926, -3970], + [-3025, 1614, -3940, -1242], + [-1337, 1756, -3163, -5529], + [-3203, 1865, -3282, -4354], + [-1646, 2118, -2203, -6018], + [174, 1871, -2707, -4639], + [-2607, 1485, -4778, -4750], + [-2199, 3991, -3134, -4879], + [-2962, 3323, -2816, -2419], + [-5286, 2495, -4548, -5395], + [-2810, 3710, -2274, -4211], + [-330, 3006, -2993, -4678], + [-1187, 2411, -2743, -5196], + [-664, 4033, -3101, -5641], + [-1458, 3602, -2816, -5371], + [-4116, 4923, -3321, -5630], + [-4165, 2528, -2592, -4798], + [-2759, 3080, -2333, -5719], + [-5157, 3011, -5526, -6348], + [-3095, 2126, -5881, -4234], + [-4377, 3849, -3600, -6099], + [-1994, 4947, -5235, -4753], + [-1067, 600, -3258, -5133], + [-4992, 3302, -2208, -5051], + [-3377, 2981, -1655, -4815], + [-3325, 2446, -1787, -6116], + [-2341, 2737, -3240, -6347], + [-2258, -3732, 3710, -1235], + [-1558, -3849, 2694, -3012], + [-599, -4837, 3050, -2951], + [-2246, -5433, 2798, -1910], + [-2255, -4989, 3260, 270], + [-3026, -5353, 2693, -1036], + [-1151, -6097, 1097, -3782], + [-3391, -6012, 2130, -1303], + [-2850, -4422, 3375, -480], + [-1138, -3779, 1491, -4162], + [-551, -3892, 3787, -2082], + [-3221, -3676, 3144, -1202], + [-3023, -5196, 2650, 605], + [-1756, -5729, 2646, 321], + [-2693, -4409, 494, -4797], + [-1913, -4573, 3372, -1730], + [-1277, -3604, 4061, -993], + [-420, -4993, 1351, -4796], + [-3052, -5333, 1435, -1242], + [-602, -5034, 3869, -1141], + [-2436, -4680, 1665, -3019], + [-2657, -3658, 1459, -3391], + [-1220, -6246, 2749, -525], + [-3838, -4844, 2265, -1735], + [-1247, -5679, 3356, -1417], + [-917, -5448, 3342, 105], + [-1756, -6839, 2276, -2350], + [-412, -5206, 1764, -3539], + [-1439, -6915, 1442, -3750], + [-1381, -4439, 3863, -282], + [-3482, -4953, 2726, -336], + [-1376, -5931, 1714, -1987], + [-1716, -4405, 2608, 105], + [-1590, -5191, 2652, -2704], + [-2149, -6442, 2453, -1263], + [-3426, -3832, 2334, -1829], + [-2747, -5948, 2362, -173], + [-2435, -3267, 2966, -1710], + [-3979, -4282, 2705, -775], + [-356, -4238, 2544, -4343], + [-1363, -6471, 2817, -1836], + [-2878, -5117, 218, -3149], + [-3539, -5196, 1710, -2356], + [-2888, -4537, 2746, -1701], + [-1870, -4439, 1496, -4121], + [-1486, -3388, 3349, -2145], + [-3333, -4138, 1467, -2876], + [-345, -5340, 1012, -1190], + [-1672, -4992, 2289, -1029], + [-2146, -5528, 3038, -635], + [-316, -3656, 3426, -3152], + [-2695, -5812, 2336, -2050], + [-2067, -6052, 737, -3258], + [-2664, -4205, -350, -1266], + [-617, -5406, 80, -4853], + [-2418, -3825, 1853, -1326], + [-1961, -4339, 583, -4315], + [-1495, -5141, -133, -5205], + [-3208, -6440, 1691, -2069], + [-2632, -3633, 2325, -2761], + [-2624, -5670, 1252, -3676], + [-3687, -5608, 687, -2833], + [-3320, -5707, 16, -3877], + [-2738, -6112, 84, -5135], + [2277, -5661, 3076, 843], + [1555, -5769, 2821, -5236], + [536, -6381, 603, -4910], + [734, -4609, 3314, -4092], + [1836, -4547, 3267, -4322], + [-13, -5976, 3752, -1607], + [1423, -6318, 2336, 398], + [365, -7779, 1498, -534], + [2104, -8366, 2946, -1345], + [143, -5545, 1898, -3756], + [655, -6852, 1430, 148], + [4, -6653, 2397, -59], + [2346, -5996, 4562, -934], + [1229, -7104, 2963, -598], + [-528, -7048, 2887, -1790], + [1451, -6857, 3900, -1637], + [554, -6018, 3336, 9], + [3278, -5758, 4034, 129], + [3541, -7145, 4905, -1575], + [2339, -6907, 3464, -301], + [2775, -7301, 1667, -3894], + [539, -7887, 991, -4156], + [2115, -7421, 3131, -3075], + [2803, -8546, 2564, -5836], + [2869, -5833, 1620, -4561], + [2591, -7281, 3215, -4719], + [-1228, -8477, 706, -4782], + [1967, -5243, 4813, -1940], + [701, -7010, 2273, -3893], + [915, -8470, 1918, -5620], + [-94, -6715, 156, -3873], + [1074, -5607, 4389, -1017], + [2739, -6551, 1227, -3521], + [725, -7835, 2701, -1291], + [-493, -7475, 2263, -1075], + [-412, -6508, 2984, -744], + [665, -5451, 3725, -2692], + [1499, -8129, 3564, -2072], + [2870, -6333, 4487, -2108], + [706, -5007, 3911, -152], + [-482, -8660, 1483, -2900], + [2481, -6596, 2518, -1715], + [1403, -6414, 1398, -5387], + [652, -6267, 583, -5942], + [694, -7540, 646, -6272], + [2275, -7614, 256, -5015], + [1416, -9727, 1900, -3153], + [2760, -6433, 3875, -3771], + [2325, -11196, 2182, -5155], + [1223, -11061, 1377, -5097], + [108, -10603, 307, -4952], + [-118, -8268, 1650, -1572], + [1839, -7943, 1755, -612], + [2501, -9056, 981, -2969], + [2902, -8476, 1491, -5780], + [1995, -11175, 1585, -3643], + [696, -8212, 828, -2474], + [1526, -8649, 1380, -1210], + [461, -7253, 3222, -2229], + [2966, -8641, 4121, -3271], + [833, -6039, 2361, -1086], + [3565, -7312, 1980, -5427], + [2850, -8671, 3760, -1846], + [2643, -7281, 2163, -173], + [3463, -3706, -3132, -923], + [1315, -3825, -3443, 2], + [2594, -4083, -3815, 670], + [1826, -4291, -2741, -155], + [868, -3749, -4175, -298], + [2008, -4237, -3897, -517], + [1242, -3493, -4335, -1335], + [-88, -4142, -3390, -1529], + [2176, -3488, -3822, -975], + [1706, -5188, -3415, -637], + [2717, -6159, -2333, -882], + [1276, -3978, -4361, 537], + [2471, -5556, -2866, -208], + [799, -4673, -4086, 56], + [1901, -4786, -3533, 270], + [3036, -3902, -3606, -333], + [2249, -3317, -4319, -144], + [2594, -4207, -2105, -2930], + [4008, -4774, -2626, -902], + [1038, -3659, -3496, -2454], + [2725, -3597, -3298, -1535], + [1662, -5803, -2813, 175], + [705, -3757, -3441, -1484], + [1860, -5987, -2821, -886], + [3786, -4918, -2199, -1929], + [3683, -4235, -2547, -1287], + [2531, -4896, -2956, -1593], + [1005, -5585, -3324, -180], + [1625, -5229, -1756, -3642], + [1494, -5041, -2989, -2685], + [2718, -4655, -3224, -867], + [2374, -6640, -1745, -2975], + [2133, -6436, -2477, -1499], + [1833, -4418, -3523, -1512], + [1128, -4910, -2658, -1106], + [689, -4777, -2831, -2085], + [3593, -5280, -2627, -315], + [3264, -3771, -2673, -1861], + [3202, -5602, -2409, 402], + [552, -4618, -2221, -3002], + [3095, -5356, -2666, -1083], + [3401, -4609, -3146, 45], + [3051, -4662, -2192, -2232], + [2798, -5552, -2462, -1941], + [2354, -5815, -2223, -2619], + [192, -3708, -2807, -2658], + [1886, -4226, -1862, -3529], + [2526, -3976, -2819, -2332], + [1577, -3870, -2711, -2806], + [1288, -5588, -3382, -1403], + [2711, -5399, -1564, -3253], + [1459, -5492, -2222, -322], + [2823, -5091, -2886, 776], + [3559, -5821, -2109, -1360], + [1587, -6331, -2760, -1909], + [2139, -5213, -2874, -2120], + [1318, -4337, -3695, -2098], + [821, -4471, -1849, -565], + [3329, -4782, -1725, -89], + [582, -4914, -4105, -1119], + [417, -4144, -4072, -2529], + [-199, -3803, -2765, -4042], + [2731, -4283, -2143, 1], + [2911, -6187, -1951, -2116], + [1573, -6094, -493, -2838], + [2081, -6927, -864, -3211], + [1058, -7826, 79, -364], + [3147, -5570, -684, -978], + [3572, -5856, 1060, 1824], + [1143, -6702, -1478, 338], + [2341, -7220, -88, 260], + [3639, -6861, 668, 815], + [2227, -6268, -1706, 446], + [3390, -6082, -353, 1302], + [1123, -7556, -1237, -430], + [1729, -7742, 729, -218], + [1457, -6774, 587, 579], + [505, -6919, -569, 371], + [1106, -7245, 78, 158], + [2755, -6745, -1122, 338], + [3069, -6040, -1415, 986], + [2174, -7064, -1430, -283], + [1390, -8626, -446, -3031], + [3534, -6890, -431, 547], + [2267, -9618, 475, -2994], + [3672, -7673, 75, -115], + [2131, -7560, -1206, -750], + [2972, -7477, -685, -262], + [1604, -6637, -672, 699], + [1666, -7577, -577, -240], + [1591, -6554, -2158, -94], + [2348, -6286, -353, 1123], + [2017, -8810, -412, -1805], + [2892, -6713, -1765, -554], + [2500, -6828, -1995, -1197], + [3877, -6639, -224, -1655], + [2392, -7872, -91, -333], + [3562, -7370, -532, -2836], + [2552, -7614, 164, -1805], + [990, -6104, 218, 438], + [910, -7861, 312, -1195], + [1472, -6327, 372, -640], + [1576, -7143, -1983, -843], + [422, -7625, -457, -278], + [1797, -8532, 405, -1011], + [1088, -7396, -238, -2277], + [3209, -6753, -1431, -2072], + [2617, -6839, 100, -2573], + [2575, -8573, -387, -3188], + [3618, -6971, -1190, -321], + [2205, -7361, -1695, -2008], + [2985, -6297, 1464, 1179], + [2804, -7310, 1053, 338], + [1362, -6074, -1163, -840], + [3336, -6325, -1794, 21], + [2836, -8109, 818, -329], + [2791, -5879, 560, 1546], + [2392, -6064, 135, 100], + [1838, -6194, 596, 1085], + [1926, -7515, -414, -4901], + [3225, -7298, -1202, -1189], + [3960, -7558, -659, -719], + [3442, -6647, -1692, -1095], + [3381, -6441, 262, -886], + [1431, -8150, -1186, -1406], + [340, -8498, -150, -899], + [3004, -8149, -260, -953], + [2749, -6611, 563, 873], + [-6647, -1325, -4517, -4691], + [-6005, -1657, -4089, -3797], + [-3157, 588, -5213, -3068], + [-3311, -1425, -6329, -3726], + [-5866, -819, -3857, -2744], + [-5001, -1799, -1075, -4621], + [-5330, -2650, -2672, -4664], + [-4930, -539, -2363, -4010], + [-2984, 10, -3863, -5749], + [-1055, -2106, -3713, -4267], + [-5476, -502, -4279, -6504], + [-5231, -1543, -5018, -6425], + [-5134, -363, -3165, -5109], + [-3953, -771, -4107, -6393], + [-2159, -563, -3652, -5342], + [-3888, -2321, -919, -5057], + [-1236, -597, -4235, -4193], + [-4053, 675, -3083, -6174], + [-2793, -1089, -5396, -3460], + [-3000, -44, -2209, -6575], + [-3336, -1531, -4313, -5160], + [-2127, 128, -4851, -3692], + [-3321, 136, -2067, -5660], + [-5215, 1404, -4374, -4356], + [-2747, 400, -6340, -3691], + [-3926, -599, -5361, -5006], + [-2875, -2592, -5143, -4092], + [-4991, -1958, -5322, -4891], + [-4965, -1318, -6652, -5333], + [-4920, -1691, -3388, -5561], + [-3644, -3354, -2688, -5982], + [-5076, -919, -4563, -2984], + [-6114, 250, -3884, -3915], + [-4014, 744, -3973, -1924], + [-5543, -1041, -5557, -3847], + [-4711, -1352, -5649, -2603], + [-3362, 775, -5305, -4879], + [-5001, 107, -3554, -2888], + [-6258, -1651, -6356, -6566], + [-4529, 407, -5003, -3865], + [-5154, 550, -5278, -5465], + [-4195, -467, -1894, -3129], + [-5022, 1127, -3349, -3314], + [-6075, 1250, -4313, -5641], + [-2677, -2283, -2312, -5903], + [-4113, 193, -1195, -4833], + [-3940, -1048, -1389, -5079], + [-3703, 917, -4043, -4451], + [-3366, -4231, -1534, -5488], + [-3326, -3583, -2091, -4903], + [-5144, 1254, -2532, -4949], + [-5982, -870, -2545, -4555], + [-3925, -157, -5367, -2281], + [-6419, -746, -5668, -4371], + [-5787, 518, -7096, -5805], + [-4258, 954, -6453, -4321], + [-4771, -695, -4158, -1639], + [-7078, -760, -5195, -5877], + [-7348, 83, -4101, -4586], + [-2430, 184, -2874, -1679], + [-2284, -3943, -2924, -5034], + [-1804, -1785, -3002, -4710], + [-4399, -2772, -1815, -4637], + [-6340, -2626, -2824, -5191], + [-4998, -5168, -3480, 1905], + [-3958, -5492, -1599, 1579], + [-2471, -3755, -276, 3182], + [-3033, -5779, -1063, 1554], + [-2936, -4829, -1290, 2386], + [-1835, -5073, -3051, 1299], + [-1724, -3771, -3935, 2324], + [-5070, -2550, -3692, 768], + [-4326, -5333, -297, 1878], + [-3472, -5619, -3094, 992], + [-3027, -4384, -3038, 2265], + [-3201, -5332, 67, 2200], + [-1681, -4373, -1947, 2461], + [-3221, -3329, -4238, 2564], + [-1262, -2968, -2915, 3227], + [-3419, -1878, -3373, 2110], + [-2244, -5583, -2012, 1288], + [-1971, -5266, -990, 1812], + [-2975, -2778, -452, 4063], + [-2198, -1165, -3298, 2965], + [-4782, -4894, -4767, 664], + [-6002, -3950, -2806, 2025], + [-3142, -3162, -2859, 3295], + [-3262, -3340, -4123, 1596], + [-4014, -3918, -1955, 3361], + [-1700, -3463, -1346, 3449], + [-4245, -4445, -4743, 1644], + [-4180, -3969, -401, 3281], + [-2782, -5240, -4117, 1156], + [-5744, -4040, -1439, 3470], + [-5063, -4663, -323, 3172], + [-4531, -3319, -844, 3988], + [-6226, -5125, -2064, 2976], + [-3115, -3267, -1531, 3898], + [-4628, -4421, -2864, 2808], + [-4559, -2989, -3442, 2024], + [-1775, -4487, -656, 2477], + [-2664, -1865, -1884, 4081], + [-1828, -2575, -3894, 3378], + [-6441, -3677, -2025, 1677], + [-4141, -2156, -1191, 3474], + [-4802, -1623, -1727, 2160], + [-5474, -2745, -1475, 2498], + [-3664, -1056, -1975, 2491], + [-4672, -3062, -2235, 2933], + [-4205, -5960, -2849, 1517], + [-4995, -5708, -1739, 1805], + [-4892, -6080, -4793, 872], + [-4270, -4172, -4263, 2185], + [-4687, -1470, -2905, 1023], + [-6446, -5017, -3919, 1000], + [-6046, -5538, -3943, 2006], + [-6028, -3750, -3953, 771], + [-5959, -4582, -5024, 824], + [-5818, -2576, -2249, 1326], + [-5659, -5345, -1119, 2500], + [-3346, -4155, 606, 2749], + [-5680, -4827, -2501, 1838], + [-6193, -2543, -1295, 840], + [-6871, -4925, -3512, 1801], + [-5605, -1788, -1895, 779], + [-3922, -5712, -4644, 510], + [-4745, -3869, -4533, 99], + [-2984, -4907, -399, 1497], + [1847, -478, 3061, -5812], + [4450, -1116, 3609, -6570], + [3139, 99, 3007, -5532], + [2590, -3782, 3138, -4770], + [1881, 1204, 5778, -3404], + [3631, 2060, 5566, -5038], + [3461, 1961, 5167, -3800], + [2947, 273, 4536, -4389], + [4453, -1730, 5788, -4370], + [4032, 1805, 2666, -4534], + [3487, -944, 2313, -6028], + [1313, 34, 4210, -4067], + [5632, -1502, 5825, -5855], + [7736, -547, 4879, -5476], + [4906, -1512, 4760, -5760], + [3843, 447, 1091, -4958], + [2982, -1135, 5442, -4386], + [3579, 271, 3031, -6770], + [3932, -211, 4688, -5507], + [4411, 1720, 2387, -5584], + [5379, -479, 4575, -6280], + [3613, -362, 2012, -4885], + [3744, -2013, 4493, -5073], + [5693, 109, 4379, -3362], + [5475, -621, 5317, -3985], + [6411, -673, 5708, -4752], + [4933, -796, 7262, -4290], + [2804, 444, 6276, -3655], + [4120, -517, 6078, -4531], + [5119, 841, 3486, -3910], + [4738, 1539, 3525, -2970], + [5086, 370, 5895, -5640], + [4235, 2716, 4589, -5044], + [3691, 682, 6199, -4700], + [6111, -570, 6271, -6528], + [2611, 1277, 3756, -4802], + [4395, 970, 3807, -5879], + [5225, 2299, 3242, -4333], + [5144, 1778, 4946, -5545], + [2989, -3016, 3247, -5495], + [2983, 920, 2071, -6059], + [5270, -903, 4434, -2350], + [6415, -585, 3970, -3554], + [3866, -197, 5216, -2884], + [3767, -1298, 6702, -3315], + [6299, 2620, 5284, -6824], + [6654, 646, 3653, -4927], + [4770, 3047, 5160, -6287], + [5364, 434, 2919, -5207], + [2998, 1344, 4801, -2456], + [3896, 1013, 3773, -1864], + [2115, 655, 2999, -6344], + [5170, -981, 2849, -4464], + [2735, -2159, 2717, -5776], + [2430, -1952, 4392, -4559], + [6143, -1180, 3659, -4746], + [4978, -1483, 1726, -4875], + [3486, -2383, 3306, -4301], + [1434, -1372, 4171, -4770], + [3354, -2627, 1525, -5093], + [6790, 2386, 3995, -5909], + [1475, -2674, 3451, -4204], + [1999, -3494, 3693, -5556], + [4764, -2848, 2856, -5589], + [-3677, 5131, 2827, -2934], + [-2844, 7078, 2852, -3580], + [-3902, 6434, 4118, -1911], + [-1769, 7530, 3492, -3541], + [-1937, 5679, -447, -1127], + [-2456, 4680, 4196, -2407], + [-2778, 8241, 1698, -4288], + [-2876, 6104, 5182, -2387], + [-2802, 7341, 4463, -2938], + [-1025, 6267, 4752, -3201], + [-2349, 5413, 2041, -3794], + [-2252, 8225, 2856, -4269], + [-1465, 4967, 4976, -2500], + [-636, 7565, 3517, -4233], + [-1905, 5618, 3904, -2942], + [-302, 6816, 3343, -3316], + [-2210, 4156, 2817, -3511], + [-717, 6568, 1863, -2951], + [-3873, 5682, 2164, -575], + [-2878, 5835, 440, -2597], + [-3228, 7701, 2610, -2514], + [-3608, 8888, 3377, -2468], + [-2582, 9717, 2519, -3126], + [-5238, 6202, 2866, -2831], + [-3428, 7370, 3056, -335], + [-1681, 8836, 1210, -2010], + [-3276, 6724, 1156, -3930], + [-894, 8149, 827, -1258], + [-2965, 8631, 2549, -1320], + [-3961, 6902, 3581, 55], + [-1894, 7745, 1750, -841], + [-821, 6844, 850, -676], + [-608, 6948, -4, -1376], + [615, 6524, 1089, -1147], + [-2972, 5668, 1091, -489], + [-157, 4649, 2904, -413], + [673, 5121, 1498, -66], + [-390, 5902, 1611, -245], + [-2349, 5478, 4772, -1320], + [88, 6798, 1972, -1859], + [-1213, 5120, 2991, 200], + [-2347, 6040, 2839, 376], + [-578, 5976, 3364, -1796], + [-1391, 5872, 3002, -965], + [-564, 4496, 3946, -1186], + [-2299, 6386, 3135, -2176], + [-2131, 5641, 2011, 1223], + [-772, 5807, 1124, 895], + [-2837, 6758, 2297, -740], + [-3091, 6298, 1415, -2126], + [-4197, 6036, 1843, -3022], + [-41, 6459, 92, 344], + [-2241, 6860, 2095, -4396], + [-1931, 7088, 2117, -2135], + [-2375, 4422, 1688, -3169], + [-1742, 6674, 1538, -119], + [-4818, 7749, 4192, -1577], + [-2004, 5672, 193, -430], + [-3825, 6042, 2128, -1898], + [-1108, 8033, 2119, -3013], + [-2370, 5453, 1721, 266], + [-1570, 7134, 614, -2638], + [-1519, 8752, 3503, -4330], + [-2050, 3845, 2907, -1126], + [5085, 4412, -335, -1923], + [3618, 1423, -613, -4012], + [4481, 3729, 589, -4631], + [4270, 3216, -1763, -3168], + [4241, 1796, -1701, -2796], + [4787, 2338, -487, -3639], + [2915, 3429, -621, -4753], + [5175, 1660, -1265, -3223], + [4280, 4057, -684, -4079], + [4980, 4419, -1455, -2719], + [5436, 2464, 387, -4197], + [4507, 4018, 1121, -3314], + [6020, 2401, -413, -3201], + [4200, 3789, -333, -2813], + [5229, 2493, -1194, -1878], + [5851, 2695, -492, -2292], + [5743, 3288, -697, -1221], + [5692, 2612, 979, -2227], + [5085, 2067, 1046, -1214], + [3163, 2240, -2098, -3435], + [5228, 1898, 145, -2397], + [5860, 3976, -418, -2872], + [6008, 3399, 1027, -3506], + [4126, 2035, 1865, -893], + [5375, 3596, 511, -2362], + [1937, 1493, -852, -122], + [3473, 4849, 547, -2603], + [4631, 2977, 1141, -1768], + [6149, 3050, -71, -1886], + [4069, 4353, -289, -1429], + [2884, 1225, -1388, 365], + [5485, 2518, -235, -571], + [1216, 4375, 1443, 398], + [4988, 3106, 107, -1435], + [4511, 2801, 307, -444], + [3235, 4386, 327, -676], + [2055, 3708, 1657, -305], + [5839, 2374, 290, -1385], + [5110, 3305, 1936, -4206], + [6416, 2920, 338, -2736], + [3350, 2824, -1269, -3881], + [4840, 1815, 464, 186], + [2399, 3332, 238, 1238], + [3516, 1363, 1582, 688], + [3582, 1874, 154, -4770], + [3261, 2878, 886, 283], + [3877, 2658, -327, 884], + [4151, 3436, 2173, -2923], + [3592, 3674, 1281, -1295], + [4561, 3730, -1114, -1747], + [4595, 3625, -558, -575], + [2577, 2348, 2267, 120], + [5242, 3299, 32, -3412], + [4264, 3637, 709, -2320], + [6556, 3570, -838, -2472], + [5745, 4014, -940, -1973], + [5629, 4475, 477, -3328], + [5269, 3199, 1682, -3085], + [4432, 2416, 1145, -3299], + [4465, 2505, 2162, -2186], + [4643, 4941, -88, -2885], + [4568, 5231, 552, -3915], + [5667, 3075, -1406, -2963], + [5418, 5259, -771, -2818], + [-256, -7875, 511, -471], + [-1813, -7971, -424, -396], + [-306, -7006, 862, 282], + [-2306, -6422, -1440, 508], + [-245, -6787, 375, -100], + [-1309, -6065, -20, 779], + [-1656, -6047, -641, 1307], + [-1496, -6522, 964, 726], + [-2291, -6588, -202, 795], + [-762, -7522, 1454, -558], + [-2270, -7004, -834, -580], + [-1139, -7078, 259, 362], + [-2535, -7568, -1040, 49], + [-3786, -7280, 934, -476], + [-3336, -6368, 606, 1056], + [-3602, -6924, 52, 714], + [-2278, -6550, 1674, 204], + [-2855, -5765, 930, 1530], + [-2889, -7325, -215, 305], + [-2749, -6080, -237, 1452], + [-985, -6667, 1577, 400], + [-2036, -6083, 380, 1267], + [-2077, -7460, 380, -30], + [-1775, -7175, 1540, -386], + [-3065, -6927, 989, 168], + [-2836, -7602, 117, -3392], + [-1058, -6396, 593, -3078], + [-844, -6062, 999, -236], + [-3261, -6951, 1491, -720], + [-2186, -8484, 75, -1287], + [-2882, -7756, 456, -510], + [-1800, -6879, 960, -1183], + [-2554, -7241, 1614, -1474], + [-2608, -5305, 392, 851], + [-2973, -6562, -859, 858], + [-2640, -5989, 1031, -416], + [-977, -8366, 705, -1434], + [-1213, -7409, -77, -1390], + [-1335, -6657, 2125, -123], + [-2544, -6862, 1852, -737], + [-3235, -6422, 1752, -103], + [-1300, -7557, 939, -348], + [-3476, -7579, 202, -109], + [-2482, -6572, 753, 619], + [-2554, -8136, -648, -429], + [-1012, -7870, -3, -421], + [-3604, -6247, 32, -3102], + [-1486, -7271, 2013, -1021], + [-578, -6799, -523, 405], + [-2841, -5948, 1644, 911], + [-2411, -7473, 1084, -484], + [-2238, -6033, 294, -1059], + [-3459, -6470, -201, -790], + [-2027, -6009, 1833, 805], + [-1433, -8047, 1531, -1754], + [-3258, -7884, 763, -1422], + [-1544, -6928, -729, 478], + [-2314, -8415, 74, -3757], + [-3201, -5684, 95, -2214], + [-2423, -8694, 725, -3631], + [-3545, -7071, 1162, -1798], + [-294, -9662, 403, -2274], + [-2290, -5460, 1196, 402], + [-1603, -6713, 903, -2363], + [4121, 2491, -3142, -2482], + [4500, 3305, -3671, -1567], + [5973, 3172, -1348, -534], + [4830, 3379, -1549, 643], + [5214, 3938, -2641, -2302], + [4639, 4826, -5532, -847], + [5639, 2731, -2170, -963], + [6084, 3487, -3525, -1346], + [5971, 3154, -2190, -2316], + [5618, 4865, -6927, 116], + [5345, 3568, -7391, 709], + [5429, 5078, -3811, -1524], + [6960, 2037, -3515, -1096], + [7092, 2531, -4557, -588], + [6061, 4247, -5651, -478], + [4595, 3684, -4907, -827], + [7497, 3213, -3048, -424], + [5996, 2137, -3098, -1745], + [6198, 5199, -2223, -2274], + [6888, 2851, -2768, -1675], + [6114, 4210, -2316, -954], + [7127, 4242, -3041, -1408], + [6126, 3668, -1517, -1427], + [6245, 6129, -4225, -1186], + [6816, 3213, -2101, -964], + [5345, 5276, -2643, -847], + [6592, 4665, -4338, 484], + [6746, 3751, -3443, 124], + [5453, 1980, -2738, 2606], + [4662, 2179, -4226, -1059], + [5571, 3208, -3554, 174], + [5256, 4447, -1815, -1481], + [5400, 2570, -1210, 235], + [7056, 2549, -2674, 318], + [4574, 4340, -2892, -130], + [6203, 4587, -3273, -305], + [5103, 1925, -2715, -2137], + [3905, 4296, -1700, 247], + [4421, 4605, -3299, 811], + [5671, 1273, -3870, -924], + [5486, 1805, -4901, 133], + [6437, 2578, -1828, -106], + [5530, 5253, -5058, 1223], + [4816, 2025, -1215, 1443], + [3457, 3525, -2456, 3217], + [3316, 2595, -1108, 2459], + [3068, 3810, -2207, 1926], + [6351, 5436, -6470, 600], + [6324, 4240, -5365, 2416], + [4851, 4774, -4075, 1878], + [4900, 3679, -5198, 1078], + [8347, 3633, -4565, -171], + [5244, 5718, -3853, 173], + [3960, 3492, -2939, 2105], + [6070, 3473, -2351, 161], + [8228, 3034, -3360, -901], + [7006, 3985, -1940, -1926], + [7123, 4681, -4301, -878], + [5122, 4097, -1851, -449], + [6200, 2060, -2251, 1049], + [7106, 3844, -7209, 2625], + [7108, 3370, -6734, 533], + [6859, 2849, -3992, 1360], + [5458, 2278, -3253, 1131], + [-1072, -2109, 4783, -1073], + [-319, -2604, 4257, -2418], + [2466, 1300, 3476, -314], + [2847, -1502, 5296, -141], + [1667, -1273, 5559, -2725], + [2877, -3402, 6434, 204], + [53, -2637, 5275, -1181], + [1091, -2215, 5803, -1549], + [2397, -922, 4327, 1182], + [219, -3747, 4647, -1564], + [-29, -2705, 4812, 1277], + [1499, -2608, 5648, 1407], + [2139, -2399, 4202, 2791], + [-426, -2064, 5528, 151], + [2560, -2803, 6179, -2806], + [4537, -2479, 3797, 1095], + [888, -3357, 5341, -415], + [4460, -1814, 5388, -1227], + [3920, -3268, 6364, -703], + [3343, -4698, 4410, 784], + [309, -1897, 6306, 1223], + [958, -3318, 4254, -3167], + [-99, 1596, 6018, -1983], + [-429, -853, 6407, 878], + [1170, -1322, 6290, -417], + [2288, -505, 6303, -1999], + [3312, -1674, 6749, -2494], + [-415, -3401, 4721, -371], + [-189, -1210, 4844, -2002], + [888, -4142, 4377, 130], + [2469, -4381, 5398, -2492], + [2879, -2912, 5094, -2598], + [-717, -617, 5650, -685], + [1470, -3863, 5352, -1684], + [3935, -96, 3823, -730], + [3769, -430, 3168, 694], + [2556, 385, 3539, 512], + [77, -1415, 5111, 2655], + [2724, -2158, 6715, -822], + [1832, 1001, 5385, -1900], + [900, 2198, 4464, -559], + [441, 69, 5921, -1743], + [-1161, 738, 6732, -308], + [257, 2035, 4091, 736], + [1607, 1288, 4355, -23], + [-13, 1316, 4180, 1672], + [1511, 1336, 3057, 1435], + [2189, -3813, 4530, 939], + [3632, -706, 2646, 1375], + [4266, -3761, 4241, 1077], + [3101, -427, 5273, -1202], + [2293, 276, 4810, -313], + [3430, -1851, 3101, 2045], + [3453, -2979, 5142, 942], + [1683, -3281, 4802, 2002], + [3954, -4715, 5611, 578], + [1272, -155, 5085, 454], + [128, -194, 5095, 1409], + [820, 880, 5797, -2658], + [-1095, 656, 5774, 1095], + [813, -1669, 4320, -3251], + [-119, 518, 6372, -651], + [2922, -4299, 6115, -877], + [4205, -4273, 4004, 2642], + [-1211, -3892, 224, 3127], + [-34, -4371, 1321, 2318], + [77, -6326, 1201, 828], + [3995, -3775, 1958, 3233], + [178, -3301, 1985, 3318], + [2330, -3801, 1033, 3195], + [1413, -5536, 826, 1709], + [2468, -3499, 3653, 3631], + [741, -4617, 1723, 2008], + [1246, -3043, 2978, 3949], + [-343, -4308, 2258, 2189], + [-682, -4640, 454, 2272], + [1236, -4829, 2491, 1642], + [-512, -3766, 1182, 3052], + [119, -3939, 3712, 971], + [-1145, -4624, 1360, 2281], + [101, -4746, 2866, 1255], + [-1500, -5455, 539, 1637], + [-969, -5909, 1414, 1128], + [-1261, -4939, -231, 2022], + [-226, -5345, 1207, 705], + [2712, -5109, 3205, 1866], + [-476, -5913, 273, 1208], + [-2039, -4464, 624, 2545], + [-2351, -3930, 2019, 2673], + [-2675, -4849, 1522, 1990], + [-1524, -3461, 1446, 3204], + [477, -5314, 1710, 1577], + [656, -3729, 2346, 2511], + [550, -5917, 1975, 1040], + [1728, -4704, 3067, 1058], + [-9, -5247, 506, 1760], + [-574, -5135, 1675, 1672], + [2129, -3781, 3444, 2313], + [1144, -4439, 2214, 2529], + [1292, -4160, 3185, 1833], + [2445, -3262, 2534, 3227], + [2266, -4401, 2023, 2400], + [-587, -3602, 3408, 2067], + [-885, -4951, 3228, 1174], + [-728, -2711, 2807, 3552], + [1019, -3043, 3195, 2954], + [1888, -4615, 1140, 2454], + [660, -5616, 754, 800], + [-1975, -5371, 1649, 1585], + [-1544, -5436, 2422, 1081], + [-422, -5882, 2390, 750], + [1336, -5557, 2441, 1230], + [136, -4001, 267, 2854], + [-522, -3289, 2226, 2728], + [-971, -4580, 2471, 708], + [704, -5306, 3300, 1001], + [325, -3464, 3555, 2398], + [794, -3686, 848, 3169], + [660, -3017, 4584, 3242], + [-1486, -3978, 2170, 1644], + [-1615, -4650, 2688, 1844], + [750, -4578, 538, 2239], + [1668, -5849, 1455, 1031], + [3486, -4681, 2030, 2183], + [2642, -5429, 1696, 1761], + [4491, -4502, 3538, 2767], + [3545, -4528, 3514, 2982], + [3269, -3676, 2758, 3966], + [5572, 1146, 209, -3379], + [7459, 1053, 593, -1896], + [4480, 200, -310, -4259], + [5577, -939, 242, -3992], + [8142, 442, 1257, -3083], + [5442, 1261, 1424, -3236], + [6260, -183, 3125, -2532], + [7179, 889, 1618, -2548], + [6416, 932, 2379, -2487], + [7094, 2560, 961, -3392], + [7322, 463, 2732, -3735], + [6632, 1577, 1912, -3272], + [6312, 1349, 3028, -3460], + [6105, 386, 1213, -977], + [5478, 1158, 1114, -486], + [6493, 410, 1686, -2180], + [6378, 1881, 1333, -2240], + [5711, 812, 1958, -1300], + [6844, 877, 730, -1189], + [6824, -245, 2249, -2000], + [7515, 1521, 1251, -3058], + [6697, 1051, 1300, -1749], + [6476, 1425, 811, -2773], + [7350, 465, -76, -2849], + [6975, 2095, 567, -2492], + [4691, 1736, 2660, -2289], + [7837, 1456, 340, -2767], + [7930, 507, 838, -2074], + [6106, 1502, 766, -1110], + [4891, -659, 835, -3954], + [7250, 141, 1369, -1523], + [7651, 67, 1651, -2298], + [7364, -305, 601, -3132], + [7179, 193, 2491, -2871], + [6504, -272, 2167, -1322], + [4456, 983, 2300, -421], + [4817, 457, 1695, 371], + [6914, 555, 850, -3159], + [5904, 1030, 202, -1959], + [6258, 880, 2233, -4503], + [6029, 10, 2130, -3600], + [6449, 985, 1129, -3963], + [6616, -18, -111, -3285], + [4496, 775, 817, -4276], + [6134, 2338, 1470, -2973], + [6911, 152, 430, -1946], + [4053, 991, 3218, -1193], + [5435, 1285, 3124, -2412], + [5507, 1836, 1935, -1988], + [5240, 689, 2189, -2670], + [6638, 1719, 606, -1799], + [5556, -180, 129, -2595], + [5644, 1918, 1281, -4316], + [6410, 1088, -282, -3117], + [6503, 1841, 312, -3514], + [6947, 20, 1358, -3886], + [5464, 2109, 2398, -3194], + [5616, -407, 2140, -498], + [6121, 2707, 2379, -4096], + [7303, 1846, 2266, -4095], + [5444, 470, 2718, -1553], + [5817, -645, 3285, -1349], + [5625, 1427, 1103, -1991], + [6041, -806, 1196, -2943], + [3050, -5722, 4070, -5460], + [3420, -4386, 4078, -5155], + [6020, -3982, 7268, -2689], + [7502, -4317, 7894, -3973], + [4156, -3558, 5247, -4316], + [4725, -4401, 7290, -1540], + [6688, -5122, 8216, -3210], + [9176, -6576, 9276, -4963], + [8706, -5708, 7987, -4621], + [7060, -3535, 6532, -3308], + [5600, -2719, 5363, -1568], + [4661, -2803, 6263, -4716], + [3673, -3636, 6147, -3433], + [5305, -2585, 6073, -2638], + [7614, -1962, 6079, -5266], + [6760, -3366, 7382, -4322], + [6385, -3883, 4797, -1353], + [8182, -5120, 4298, -4641], + [9130, -6198, 4975, -3063], + [7421, -5436, 5576, -3713], + [3483, -4898, 5443, -2745], + [4907, -5643, 6390, -4105], + [8119, -7008, 7992, -6764], + [6528, -6122, 6967, -5590], + [5890, -4190, 6624, -5688], + [6815, -7934, 7275, -5456], + [5434, -4306, 5169, -5378], + [4364, -6436, 5376, -2604], + [8152, -3404, 5913, -5048], + [7983, -4863, 4262, -2461], + [8023, -6188, 6238, -5062], + [6753, -3692, 3935, -3723], + [6826, -4760, 3284, -4051], + [7224, -7423, 4492, -3875], + [6904, -2590, 6587, -6248], + [6106, -1944, 7345, -5506], + [4956, -2990, 7808, -3146], + [6908, -6885, 5949, -1288], + [7162, -6058, 3419, -3401], + [7015, -7080, 6907, -3018], + [6971, -6832, 5646, -3273], + [8014, -5546, 5471, -1544], + [6792, -2220, 5105, -2879], + [8494, -3974, 4408, -3999], + [9591, -4866, 6027, -4558], + [5264, -5161, 6101, -738], + [5803, -6141, 5197, -5231], + [4657, -6822, 3232, -5189], + [4791, -5135, 3809, -4665], + [6108, -5103, 2379, -3873], + [4680, -3909, 3234, -5093], + [5802, -3853, 3795, -4984], + [4360, -7483, 4802, -3877], + [5429, -7517, 5911, -3717], + [6866, -2280, 4880, -4634], + [10131, -4628, 4414, -4092], + [10811, -5189, 7746, -5337], + [5663, -8941, 5287, -5680], + [8023, -5991, 7403, -2796], + [9669, -6919, 6525, -4932], + [7275, -3796, 4962, -2547], + [8848, -4806, 5677, -3080], + [8128, -4308, 7749, -6569], + [4032, -5196, 2282, -6239], + [6593, 700, -229, 304], + [8260, 539, -66, -1259], + [6605, 176, -814, -109], + [8057, 0, -1, -136], + [7382, -38, -484, -1129], + [8373, -929, 682, -454], + [7674, 690, -1278, 546], + [7326, -517, 406, -1283], + [7612, -1715, -1167, 1175], + [8590, 441, -782, -710], + [8572, -1202, -291, 260], + [7308, -147, -1785, 414], + [6787, -353, -672, 934], + [5177, -133, 179, 82], + [4161, -34, 447, 1497], + [5997, -902, 1533, -121], + [5727, -871, -1370, 945], + [8386, -252, 293, -823], + [6573, -1354, 682, 616], + [7650, -2096, 725, 457], + [8122, 78, 636, -1400], + [8421, 428, -1620, 131], + [7341, -1292, -717, 186], + [7998, -49, -720, 266], + [5987, -351, 669, 844], + [7314, -1620, 250, -603], + [7219, -1562, -572, 1994], + [8682, -358, -290, -388], + [5810, 155, -178, 1199], + [7246, -12, 1042, -786], + [7357, -923, 1468, -475], + [7801, 621, -212, -724], + [5346, -514, 1210, 1356], + [8459, 36, -127, -779], + [6878, -2429, 854, 1750], + [7280, -1401, -1353, 2845], + [7579, -2148, -1463, 2087], + [6637, 946, -872, 750], + [4807, -1100, 1289, 2602], + [4495, 219, 1551, 1128], + [7639, 506, 446, -1107], + [6359, 188, 1009, -115], + [6641, -1820, 1655, 723], + [5394, -2382, 1604, 2542], + [6021, -2644, 2396, 1407], + [4698, 882, 245, 1525], + [8103, 573, -798, -349], + [8045, -519, 997, -1092], + [7571, -122, 227, -338], + [5347, -1200, 630, 1718], + [7070, 790, 218, -544], + [7440, 728, -527, -20], + [6402, -355, 197, -736], + [4031, 771, 866, 1895], + [6009, 896, 445, -31], + [5160, 1098, -856, 1784], + [7980, -886, -1293, 1396], + [6318, -1361, 2423, 252], + [7547, -699, 133, 506], + [8562, -2344, 940, 264], + [5890, 1187, -1425, 2194], + [6558, -645, -1311, 2621], + [4634, -1671, 2075, 1623], + [5614, 105, -816, 2376], + [6646, 1558, -1365, 630], + [6998, 1150, -2117, -990], + [6555, 2311, -1093, -1783], + [6682, 1430, -2391, -1940], + [7861, 1555, -2977, -1188], + [6745, 1723, -459, -2085], + [7504, 1229, -1666, -2060], + [7937, 671, -2128, -1529], + [7139, 991, -735, -2632], + [6867, 1592, -1303, -2324], + [6401, 2230, -1732, -2508], + [7201, 2184, -2169, -1988], + [6636, 2190, -995, -2840], + [7620, 2306, -2089, -651], + [7584, 1875, -1438, -631], + [9214, 1561, -2464, -1139], + [6154, 1318, -1237, -2917], + [7917, 2847, -1797, -1599], + [8309, 2029, -2555, -465], + [8204, 1282, -584, -2405], + [8440, 1035, -1147, -1137], + [7107, 1858, -60, -1568], + [6781, 2912, -873, -1463], + [7603, 1316, -319, -1249], + [7833, 1335, -78, -1849], + [7930, 1141, -1016, -695], + [7883, 1610, -1017, -1314], + [8069, 1409, -1811, -196], + [8319, 1031, -582, -1590], + [5948, 1537, -2153, -2373], + [8684, 1171, -1871, -850], + [8357, 2484, -2411, -1292], + [6516, 2092, -193, -1167], + [6112, 1697, 22, -525], + [7161, 703, -602, -1879], + [6047, 2351, -807, -219], + [8072, 1854, -1817, -1553], + [6956, 1304, 76, -1011], + [6607, 1481, -544, -162], + [6958, 2541, -265, -1938], + [6416, 2514, -777, -850], + [7272, 2110, -899, -1171], + [7741, 2153, -283, -2614], + [6482, 2041, -1758, -1221], + [6762, 940, -1862, -2281], + [5610, 1194, -1691, -1561], + [7833, 2164, -823, -1952], + [5460, 1438, -848, 1189], + [6011, 1377, -771, -1557], + [7679, 544, -1134, -2214], + [7209, 1292, -2714, -1564], + [5567, 1200, -404, -169], + [5853, 1461, -1465, -518], + [6782, 689, -844, -860], + [7330, 1337, -1152, -71], + [7189, 1506, -653, -685], + [6860, 2116, -1403, -240], + [8804, 1516, -1391, -1760], + [7210, 2689, -1498, -989], + [7030, 3022, -1441, -2083], + [5649, 1836, -407, 525], + [7451, 3099, -717, -2464], + [7384, 1656, -2007, 398], + [6504, 707, -1919, -134], + [-1851, 3639, -2279, -695], + [-4037, 1644, -77, 1329], + [-4025, 1960, -1565, -567], + [-3430, 2495, -795, 368], + [-4771, 2480, 993, 756], + [-3431, 2058, -2539, -971], + [-3802, 3418, 380, 217], + [-3074, 3350, -1652, -1056], + [-3705, 326, -1650, 1535], + [-3122, 1281, -1192, 1607], + [-4601, 1367, -968, 53], + [-3808, 958, 44, 2560], + [-2079, 2530, -1485, 1166], + [-3707, 343, -2889, 180], + [-5249, 1431, -31, 688], + [-4990, 125, -704, 1270], + [-2771, 1334, -2446, 746], + [-2292, 994, -1527, 2630], + [-1261, 3070, -2519, 268], + [-2544, 3890, -1057, -552], + [-4421, 255, -1980, 530], + [-2951, 454, -13, 3643], + [-2262, 1815, -370, 2880], + [-2383, 3657, -649, 576], + [-3541, -161, -1389, 2550], + [-4241, 1575, 1325, 2561], + [-2767, 4037, 1221, 1578], + [-3748, 2697, 1148, 1801], + [-4686, 2385, -220, 0], + [-1531, 1645, -2751, 1327], + [-45, 4032, -799, 2298], + [-2915, 2280, 709, 2495], + [-1199, 3278, -406, 2346], + [-2471, 116, -2706, 2060], + [-2440, 2173, -2894, -344], + [-3375, 2287, 1781, 3226], + [-2153, 3568, 1827, 2918], + [-862, 2267, -1626, 2527], + [-2698, 1135, 301, 4239], + [-2364, 2123, 1010, 3710], + [-2447, 3281, -81, 1408], + [-2660, 4735, 472, 258], + [-1053, 3097, 2682, 2398], + [-3366, -1037, -1152, -868], + [-643, 4242, 2212, 1259], + [971, 3991, 934, 643], + [-1617, 2002, 2139, 2195], + [-4897, 972, 784, 1719], + [-1275, 2992, 1039, 3821], + [-392, 4973, -209, 1821], + [-1028, 4718, -1479, -137], + [50, 3914, 553, 2210], + [678, 4364, 359, 1303], + [-582, 4911, 514, 1671], + [1276, 3914, -1252, 2934], + [-1496, 3984, 857, 2330], + [772, 4744, -655, 2332], + [-799, 5283, -439, 624], + [1341, 2937, 650, 2027], + [-1739, 4892, 1275, 1702], + [-892, 2596, -151, 3951], + [-3532, 1090, 1292, 32], + [321, 3146, 2647, 1475], + [264, 4199, -1591, 1317], + [-452, -2357, 2266, 4192], + [3022, -1033, -2389, 5678], + [-1162, -1342, 3543, 4990], + [-474, -1477, -1223, 5016], + [-699, -2857, 900, 3835], + [-461, -2255, -117, 4626], + [1204, -2062, -1211, 4403], + [2192, -3035, -337, 3966], + [108, -831, 279, 5643], + [1457, -620, -2908, 5276], + [-2527, -78, 1085, 5460], + [-1978, -1918, -949, 4733], + [32, 367, -1904, 5166], + [1890, -1665, 440, 4752], + [-518, -348, 2816, 4891], + [3695, -2490, -1374, 4603], + [246, -1965, 3549, 3969], + [1100, -3111, 656, 3737], + [-1379, 870, -414, 4575], + [628, -357, -1227, 6179], + [-1129, -1318, -2457, 4576], + [-425, -98, -73, 6336], + [367, -887, 2990, 4207], + [2091, -1251, 2444, 3557], + [-1759, -1610, 2046, 5273], + [3210, 1414, -20, 2616], + [3303, -2636, 1005, 4237], + [-327, -3107, -640, 3687], + [-197, 764, 572, 5486], + [646, -767, 1388, 5464], + [104, 2742, -228, 3907], + [-236, 1829, -579, 4585], + [-2150, -474, -1525, 4006], + [-23, -2632, -2400, 3892], + [-12, -1739, -2910, 4867], + [-2310, -368, -102, 4583], + [-1991, -2061, 533, 4531], + [3884, -1446, -153, 4393], + [1568, 14, -289, 5268], + [-1376, -253, -2797, 3417], + [3193, -2577, 2475, 3566], + [3418, 617, 1350, 1857], + [3792, -24, -272, 3370], + [153, 1159, 2906, 2877], + [511, 2162, 1548, 2741], + [262, 819, -2791, 3734], + [4232, -2015, 1486, 3477], + [2943, -1110, -1014, 5480], + [2842, 369, 703, 3476], + [3011, 1634, -933, 3553], + [4412, -1548, -942, 5021], + [-1405, 593, 2372, 5267], + [2093, 2129, 896, 2365], + [4845, -1980, 0, 3823], + [-2140, 81, 3278, 5637], + [1484, 2665, -324, 3653], + [10, 192, 1620, 5291], + [2152, 738, -2269, 5000], + [2102, 2748, -1652, 4707], + [2855, -2131, -387, 5188], + [1173, 676, 1338, 3277], + [2340, -2329, -2064, 4095], + [861, -2024, 1296, 5055], + [2189, 3225, -695, 2626], + [6196, -7079, 1943, -822], + [4547, -4813, 3261, 1856], + [4243, -6904, 3443, 448], + [4581, -7503, 946, 506], + [6626, -7754, 3427, 470], + [3407, -9088, 3269, -1496], + [4079, -6464, 2304, 777], + [5621, -9336, 2684, -768], + [5351, -6464, 5238, -214], + [5961, -8007, 1724, -3091], + [4213, -8067, 603, -246], + [7208, -7403, 3168, -1738], + [6098, -7700, 329, -1379], + [6525, -6735, 4248, -1072], + [6073, -6241, 2167, -2378], + [4609, -9218, 3051, -1033], + [6813, -7283, 1581, -1897], + [6126, -6275, 2789, 681], + [4423, -6538, 1621, -1692], + [6272, -8298, 3167, -1855], + [6172, -8558, 4498, -1169], + [4844, -8588, 1647, -366], + [6209, -8807, 1581, -369], + [5389, -8059, 550, -192], + [6654, -9775, 2504, -1063], + [7103, -7998, 806, 530], + [5662, -6736, 1565, -3620], + [4165, -9564, 4191, -2131], + [4526, -7181, 576, -2875], + [4633, -8623, 2807, -4742], + [3709, -7794, 1815, 34], + [3634, -8622, 2313, -826], + [6991, -8447, 2063, -3198], + [7757, -9486, 2255, -558], + [4149, -7778, 4728, -1696], + [5767, -7427, 1113, 707], + [4592, -6261, 2329, 1864], + [3159, -10498, 1677, -4273], + [3534, -9010, 2437, -3565], + [4479, -10821, 2715, -4942], + [3207, -9805, 3054, -3886], + [4627, -8189, 3018, -2354], + [5527, -10566, 3244, -2749], + [4346, -10127, 3335, -3084], + [6132, -10085, 3316, -1308], + [5629, -9704, 2178, -3058], + [3603, -8538, 1246, -624], + [3737, -8488, 395, -3167], + [5465, -11414, 2810, -4640], + [5306, -7745, 2721, -3988], + [7000, -9111, 1695, -1409], + [6663, -7741, 2466, -4079], + [4083, -7175, 1836, -4831], + [3613, -9926, 1342, -3455], + [6588, -8033, 457, -258], + [4720, -8102, 17, -1209], + [7414, -8709, 1294, -344], + [5437, -10030, 4043, -1704], + [4862, -9281, 1558, -1431], + [6800, -6403, 5113, 862], + [4623, -8242, 2667, -228], + [5919, -5083, 3348, 2135], + [5985, -8889, 2733, -5105], + [5029, -5767, 4407, 719], + [354, -6158, -838, -3001], + [351, -5943, -2104, -1534], + [-633, -7190, -25, -4798], + [-1595, -7235, -3812, -1400], + [103, -6197, -2933, -78], + [-1722, -5020, -3441, -4333], + [-1963, -5644, -4365, -270], + [-846, -5743, -3477, 196], + [-191, -5348, -4054, -469], + [-2515, -7754, -3495, -818], + [-2090, -6710, -2701, 117], + [-546, -7036, -1398, 163], + [-278, -7091, -2662, -536], + [-622, -7962, -2731, -1464], + [-1555, -8118, -3612, -2057], + [-1094, -6280, -2314, 505], + [-2556, -8538, -4024, -2247], + [109, -7134, -3107, -1823], + [-900, -6954, -3340, -717], + [-605, -7113, -3656, -2154], + [837, -6263, -3211, -2177], + [-417, -5810, -3871, -1469], + [-1318, -5649, -4207, -3198], + [413, -6765, -2082, -33], + [-3101, -6450, -4362, -766], + [755, -6489, -2967, -846], + [1117, -7106, -2452, -1352], + [-1202, -8387, -3072, -2897], + [-365, -4894, -3561, -2937], + [-2372, -8776, -265, -4441], + [-1224, -8678, -896, -5074], + [-755, -10096, -600, -6623], + [300, -8206, -225, -4568], + [-1176, -6824, -2633, -3527], + [-2006, -5443, -1526, -5849], + [-1115, -5540, -2363, -4785], + [1059, -6812, -2543, -2654], + [-1976, -6861, -3062, -5508], + [-379, -5328, -2321, -3624], + [-2108, -5860, -4518, -1915], + [-379, -7885, -1329, -594], + [774, -5389, -581, -5213], + [-2601, -5083, -1849, -4921], + [-176, -5580, 74, -5075], + [-204, -6780, -190, -6232], + [418, -7594, -1987, -820], + [-1873, -8529, -2926, -1609], + [1340, -6362, -919, -4975], + [577, -7990, -2044, -1873], + [-2572, -7413, -1745, -2224], + [-2037, -7030, -1461, -7138], + [-2559, -8756, -2039, -5836], + [-2079, -6764, -1209, -5669], + [-1613, -7801, -2006, -685], + [-1865, -6583, -722, -3529], + [-589, -6358, -1377, -1003], + [-540, -7514, -1331, -3542], + [419, -6192, -1677, -4927], + [-2786, -8763, -2966, -5065], + [-2172, -8411, -1726, -4675], + [-3382, -9833, -3497, -5722], + [-2433, -10169, -2077, -5775], + [-424, -9451, -1096, -3658], + [-537, -8522, -910, -1897], + [-5550, 2807, 1683, -693], + [-6395, 635, 3573, -1246], + [-7544, 2280, 2140, 44], + [-8751, 1136, 2951, -794], + [-5605, 2709, 2052, 916], + [-7650, 654, 869, 135], + [-6939, 967, 1409, 870], + [-7834, 2123, 3310, 974], + [-6935, 2818, 1274, -1678], + [-5605, 2233, 1013, 471], + [-7095, 1849, 1648, 198], + [-6636, 1634, 712, -37], + [-7279, 978, 296, -315], + [-7664, 3504, 3292, -216], + [-7836, 1209, 1221, -257], + [-7913, 2201, 1765, -1529], + [-7077, 3783, 2632, -1407], + [-5565, 1645, 1410, -622], + [-6494, 2879, 1181, -759], + [-7073, 3137, 3010, 550], + [-7249, 1839, 847, -805], + [-6630, 2197, 282, -1096], + [-8836, 1573, 1988, -1090], + [-7809, 1274, 836, -1198], + [-7895, 2970, 3511, -1097], + [-6960, 1664, 1356, -2442], + [-6582, 2866, 2273, 307], + [-7221, 821, 2851, -1435], + [-6015, 1703, 2001, -2367], + [-8082, 1034, 2103, 239], + [-5952, 1912, 301, -465], + [-6099, 841, 379, 567], + [-6343, 50, 494, 658], + [-6586, 983, 591, -893], + [-5500, 869, 2187, -2479], + [-6482, 60, 1545, -979], + [-6705, 515, 1974, -53], + [-6460, 1755, 1325, -1275], + [-6093, 2617, 2465, -623], + [-7330, 2161, 594, -2115], + [-7324, 762, 1593, -2004], + [-6385, 679, 1510, -2514], + [-6159, 241, 2976, -1631], + [-8583, 3030, 4045, -162], + [-6299, 66, 2209, -2103], + [-5428, 1279, 3267, -1846], + [-6438, 1335, 2728, -1631], + [-8012, 1070, 2428, -1151], + [-6201, 2781, 2349, -1918], + [-5918, 1139, 3121, -148], + [-6314, 2481, 3137, -1808], + [-7180, 1722, 2435, -1602], + [-6750, 1829, 3763, -1145], + [-6713, 1777, 2221, 1212], + [-7479, 1835, 3627, -479], + [-7299, 10, 2406, -1593], + [-8249, 3129, 996, -2870], + [-8374, 1534, 1333, -1882], + [-7507, 3353, 1598, -2299], + [-7379, 2701, 2326, -1167], + [-8440, 2276, 2796, -542], + [-10348, 1527, 2649, -1165], + [-8184, 3614, 2574, -1738], + [-5539, 1574, 1733, 1138], + [9404, -7652, 67, 79], + [8654, -3972, 1358, -60], + [8617, -4794, 117, 2318], + [7886, -4505, 1784, 1200], + [8636, -6125, 3879, -1003], + [9654, -6836, 1816, 205], + [9374, -6553, 913, 1875], + [8020, -6150, 1134, 2390], + [7786, -4970, 2078, -1857], + [8691, -6119, 711, 708], + [9039, -5568, 2944, -1902], + [9955, -5048, 1433, -601], + [8089, -6927, 3093, -2846], + [8487, -7024, 2415, 19], + [9388, -5287, 3577, -2655], + [8591, -7371, 2300, -996], + [9104, -4763, 1453, -2558], + [7615, -5457, 596, 164], + [9860, -7047, 3433, -614], + [8756, -4404, 2235, -964], + [9462, -4660, 299, -1822], + [10119, -5550, 2689, -1273], + [10915, -7471, 2705, -1007], + [11433, -7090, 1410, -1198], + [9882, -7431, 2965, -1895], + [7628, -5219, 769, -2661], + [8169, -5318, 2262, 70], + [8846, -6320, 1939, -754], + [7147, -5593, 1248, -971], + [10652, -5485, 935, 137], + [7778, -6533, 2564, -1932], + [8878, -5173, 1214, -361], + [9828, -4943, 282, 510], + [10042, -6134, 3895, -1914], + [7965, -6630, 3566, -433], + [8573, -4502, 3574, -1209], + [8398, -4801, 1031, -1347], + [10136, -7772, 2612, 1547], + [9890, -7280, 1768, -1083], + [8407, -6585, -706, -58], + [7976, -7582, 229, -131], + [10481, -8866, 1166, -147], + [10914, -4342, 3189, -2412], + [10440, -5198, -104, -1109], + [11227, -6530, 2381, -2449], + [8487, -8064, 1086, 230], + [9975, -6123, -857, -134], + [8339, -6498, 1232, -2337], + [11042, -4506, 1119, -2098], + [12563, -5592, 1837, -2062], + [11801, -5590, 632, -1296], + [10152, -5617, 1511, -1917], + [7800, -6473, 51, -1337], + [7941, -5560, 2438, -3270], + [6554, -3834, 2100, 1476], + [9065, -5520, -226, -1120], + [10794, -7120, -243, 122], + [10429, -6968, 272, -806], + [8942, -8914, 1442, -392], + [9969, -5051, 2033, -2953], + [7275, -4152, 3058, -64], + [11127, -5488, 4589, -3227], + [9626, -6666, 2739, -2958], + [6943, -5362, 4470, 1008], + [-7456, -967, 2936, -1002], + [-8622, -333, 6962, 2606], + [-7486, -3392, 3668, 1287], + [-8053, -827, 5148, 1097], + [-6610, 454, 4952, 96], + [-7701, -1982, 3161, -468], + [-7307, -1132, 4071, -36], + [-8125, -271, 5199, 3862], + [-9182, -1950, 2813, 1878], + [-9855, -952, 4794, 3010], + [-7241, 1431, 4202, 2468], + [-9646, 157, 4766, 1046], + [-9371, 1230, 6009, 2958], + [-11514, -64, 8630, 5248], + [-6766, 565, 2766, 2140], + [-8426, -9, 2852, 1271], + [-11291, -1113, 5087, 2937], + [-8297, 2092, 4495, 1264], + [-9983, 735, 3809, -51], + [-9048, -1000, 3191, -308], + [-7331, -1987, 2655, 1391], + [-7144, -21, 4333, 2161], + [-6032, -1540, 3543, 896], + [-7987, -1036, 1985, 1529], + [-9264, 2004, 5194, 290], + [-11308, -840, 5754, 1654], + [-9130, -2398, 4292, 2973], + [-6248, 838, 3563, 1223], + [-6819, -2760, 3511, 119], + [-7213, -2006, 4364, 762], + [-5431, -1047, 4533, 166], + [-7098, -641, 2021, 639], + [-8628, -2249, 3588, 399], + [-6352, -1498, 3560, -648], + [-7033, -2190, 4870, 2562], + [-7405, -46, 3772, -581], + [-6104, 796, 5143, 1965], + [-5787, 943, 5784, 3030], + [-8367, 1465, 7192, 4097], + [-8259, 789, 5694, 1963], + [-10614, -1899, 5748, 2645], + [-8258, -805, 3698, 2275], + [-6877, -972, 6431, 3160], + [-6483, 363, 7018, 3129], + [-6283, -1358, 5191, 1524], + [-8853, -3157, 4119, 1741], + [-6086, -267, 3883, -835], + [-7254, 1032, 6613, 4017], + [-11470, -3350, 4649, 3426], + [-6743, 481, 6148, 1239], + [-5394, -166, 5309, 3165], + [-7958, 1068, 4268, -240], + [-10520, 2256, 7916, 2828], + [-5132, -4, 5739, 1176], + [-8643, 120, 3255, -629], + [-9631, 1974, 8870, 4362], + [-10663, -1221, 3733, 589], + [-8224, -1843, 5806, 2655], + [-8282, 1255, 8647, 3478], + [-12311, -1505, 9043, 6256], + [-11312, -856, 7136, 4681], + [-11944, -722, 7941, 3309], + [-7868, -463, 6846, 4196], + [-8679, -241, 7410, 5347], + [6759, -4680, -508, 1220], + [5176, -6111, 944, 121], + [6843, -5667, -1368, -533], + [5616, -5884, -1471, -695], + [6030, -5089, -1808, -940], + [7444, -5463, -52, 1881], + [4207, -6079, -506, 1571], + [6785, -4410, -649, 3084], + [4838, -5214, 2026, 2998], + [4201, -5790, 645, 1811], + [6930, -5129, -1940, 1698], + [6332, -4627, 692, 3027], + [6285, -4314, -106, 3644], + [6255, -5450, -1975, 742], + [4199, -4676, -459, 1796], + [5592, -5500, 1345, 1300], + [4358, -5556, -2236, 114], + [4620, -5875, -1563, 888], + [4892, -7550, -327, -419], + [4734, -7085, 7, 613], + [3883, -5562, -1969, 1080], + [5610, -4990, -204, 834], + [4117, -6482, -1271, 341], + [6585, -5107, 892, 1169], + [6632, -3683, 302, 3002], + [6326, -5351, -983, -1250], + [4382, -7192, -730, -158], + [5227, -6540, -451, 1123], + [5468, -6472, -870, -1471], + [5191, -6402, -1365, -127], + [7407, -6317, -973, -336], + [4611, -6530, -820, -1980], + [4963, -5159, -2050, -966], + [4414, -5691, -211, -998], + [5954, -5873, 750, -1749], + [4394, -4796, -1268, 254], + [7161, -6214, -1010, 689], + [4965, -3598, 2372, 1711], + [6248, -6180, 981, 864], + [6473, -5336, 525, -600], + [4591, -6864, -1131, -900], + [6314, -6440, -1021, -375], + [5838, -6209, -1199, 944], + [5308, -5283, -2100, 1267], + [4342, -5860, -1637, -1356], + [5680, -4388, -1227, -104], + [4900, -4098, 1449, 4046], + [4677, -4284, -106, 3190], + [7574, -6173, -848, 1859], + [6493, -7207, -131, 726], + [5513, -5261, -2117, 4], + [6191, -7352, -193, -505], + [5885, -4333, 324, -134], + [6162, -6081, -312, -2044], + [4216, -6200, -1810, -572], + [5652, -7035, -696, -197], + [7131, -7189, -366, -60], + [5032, -4803, -1514, 2832], + [7386, -4610, -606, 3489], + [4211, -5031, 1221, 3047], + [4050, -4653, 1584, 1469], + [6852, -5302, -1861, 206], + [7736, -4816, -1794, 3359], + [6290, -3439, 1522, 2454], + [1768, 5990, -5560, -2594], + [3903, 5326, -1530, -1501], + [2472, 3738, -2117, -4240], + [3260, 5448, -904, -4733], + [1435, 7297, -3676, -4102], + [4096, 5951, -656, -3312], + [2178, 6009, -3146, -3724], + [3787, 5493, -5473, -1633], + [2998, 7286, -3334, -3571], + [2894, 6576, -4708, -2804], + [830, 6163, -4286, -3348], + [4755, 5569, -1730, -2739], + [4604, 6065, -3562, -2605], + [2749, 5141, -3986, -2775], + [3942, 4875, -2143, -3340], + [2819, 8517, -2004, -2724], + [2146, 6298, -689, -3093], + [5196, 6504, -3393, -1475], + [1851, 8386, -1748, -1420], + [3474, 8572, -3534, -2688], + [4503, 7560, -3561, -2245], + [4433, 6219, -2393, -1575], + [3506, 7248, -2275, -1977], + [3490, 7409, -3147, -604], + [4214, 6447, -3520, 516], + [619, 7034, -829, -1705], + [1732, 7395, -356, -2208], + [1226, 5204, -3294, -3732], + [2027, 5619, -1813, -4146], + [3078, 5877, 47, -2651], + [1654, 5458, 424, -682], + [3163, 5464, -2026, -270], + [2884, 5375, -685, -530], + [2950, 7286, -35, -2967], + [1986, 5066, -597, 482], + [3459, 4308, -3845, -2333], + [3155, 7037, -1346, -4345], + [2193, 6696, -717, -1319], + [3677, 5089, -3892, -487], + [2186, 5136, -4186, -1492], + [773, 5796, -917, 817], + [2489, 6546, -3570, -2117], + [1223, 6469, -1362, -33], + [271, 6061, -1466, -1725], + [2540, 5171, -1847, 1032], + [2548, 5251, -2697, 1677], + [771, 7600, -768, -632], + [4710, 6647, -4736, -1275], + [1369, 5917, -2971, -1056], + [163, 5239, -3499, -2275], + [2104, 4285, -3211, -3286], + [1107, 7411, -1972, -1671], + [2196, 7262, -2310, -1926], + [-244, 6439, -1745, -839], + [3293, 3832, -2890, -3000], + [419, 6443, -379, -407], + [3077, 4930, -1156, -2869], + [2131, 5874, -2330, 224], + [690, 6538, -2212, -2841], + [1602, 4421, -2515, 1542], + [3318, 9373, -3032, -3477], + [5646, 7462, -5153, -1463], + [4139, 7137, -1539, -3321], + [3481, 9077, -1645, -3653], + [-7747, 375, -106, -543], + [-8587, -1379, -586, -461], + [-10146, -892, 2094, 694], + [-8103, 382, 504, -325], + [-8548, -92, 94, -656], + [-7460, 38, 152, 388], + [-8266, -271, -459, -883], + [-7935, -664, -1026, -802], + [-8341, -109, 853, 161], + [-8802, -1355, 1099, 630], + [-8957, -6, 1108, -669], + [-7260, -1520, -43, -407], + [-7555, -174, 668, -2562], + [-9014, -126, 227, -1191], + [-8184, 769, 290, -1375], + [-9476, 55, 962, -1528], + [-8679, 541, 755, -1030], + [-9842, -1626, 838, -1588], + [-8513, -702, 788, -1998], + [-10101, -1558, -366, -1841], + [-8135, 78, 1479, -1813], + [-9128, -454, 313, -1786], + [-7554, -1084, 831, -2442], + [-7576, -701, 2068, -1665], + [-7791, -1481, 1587, -1808], + [-6701, -596, -97, 802], + [-7418, -15, 684, -963], + [-7127, -477, -139, -426], + [-8097, -110, -36, -264], + [-7620, -1922, -590, -101], + [-7647, -1201, 279, 660], + [-7856, -1974, 758, -2271], + [-8496, -167, 2232, -1143], + [-8506, -1359, 624, -740], + [-7274, -1052, 1062, -139], + [-7800, -217, 91, -1794], + [-7030, -1694, -955, 615], + [-9020, -1864, 101, -2182], + [-9400, -740, 598, -667], + [-8448, -1184, 2024, -1272], + [-8812, -570, -897, -2384], + [-10559, -1286, 538, -1536], + [-8728, -888, -1089, -1397], + [-7080, -1185, 636, -1252], + [-9880, 233, 2344, -782], + [-7952, -1326, -378, -1947], + [-7207, -378, 1408, -2237], + [-8467, -1545, 902, -1987], + [-9163, -1474, 924, -1739], + [-8159, -992, -77, -2744], + [-8343, 148, -423, -1573], + [-9105, -649, -254, -1214], + [-8939, 456, 281, -1905], + [-8837, 179, -394, -2634], + [-9145, 757, 1547, -1319], + [-9775, -723, 441, -1680], + [-8910, -686, 1529, -1525], + [-9492, -1134, 2064, -938], + [-6111, -943, 677, -31], + [-7411, -613, -814, 46], + [-9479, -922, -430, -2061], + [-11298, -1268, 1318, -1117], + [-8190, 832, 671, -2214], + [-10453, -550, 1672, -886], + [1044, 9353, -1651, -5423], + [1034, 8149, -455, -6166], + [761, 8293, -3214, -4838], + [938, 8077, 164, -5130], + [1295, 8673, 2582, -5490], + [-314, 7973, -2395, -5231], + [-507, 9012, -2497, -5775], + [2396, 8314, -1022, -4673], + [-1516, 8501, 1950, -4969], + [-308, 7401, 1549, -4866], + [-112, 8340, 3003, -4920], + [-50, 9315, 1371, -5666], + [-659, 9449, 2496, -5547], + [2573, 9148, -2270, -4783], + [830, 7104, -438, -3907], + [522, 10672, -677, -6483], + [-1190, 10108, -510, -6518], + [-427, 8271, -579, -6315], + [1602, 8113, -1927, -4418], + [-2266, 8180, 448, -5190], + [-1633, 8816, -226, -5771], + [759, 9481, -105, -5813], + [2254, 6679, -466, -5662], + [-88, 6946, 895, -5958], + [-1705, 10009, 1394, -5574], + [748, 7943, 540, -6692], + [1411, 7009, 232, -6145], + [697, 7290, -1221, -5342], + [-1764, 10580, 1944, -3981], + [-1334, 9124, 1195, -3903], + [-905, 10067, 635, -5039], + [664, 10680, 49, -4625], + [1374, 9536, -777, -3591], + [252, 9698, -597, -2931], + [824, 9164, -1014, -2144], + [2438, 10569, -2289, -4424], + [2101, 7102, 507, -3614], + [294, 8051, -432, -1518], + [-665, 10337, 547, -2852], + [1168, 11989, -492, -5427], + [1344, 6416, 302, -5061], + [-1727, 12264, 1507, -4543], + [674, 10889, -902, -3605], + [-582, 9504, 300, -3618], + [641, 7654, 689, -2109], + [2065, 9243, 508, -4367], + [1055, 8373, 688, -3144], + [-641, 8185, 986, -3307], + [1120, 7426, 1785, -3757], + [1660, 8070, -593, -3104], + [2002, 9467, -1722, -3475], + [2361, 8368, 100, -3709], + [-772, 7845, -613, -4988], + [1485, 7430, 1896, -6127], + [-432, 7823, -947, -2882], + [313, 11122, -760, -4871], + [412, 8412, -283, -4231], + [1585, 10402, -1884, -3267], + [321, 6952, 773, -3016], + [-105, 9014, 121, -2249], + [1585, 10313, -977, -4812], + [1619, 11869, 1306, -6876], + [-1168, 8886, -81, -2500], + [-395, 10886, 733, -6490], + [-4949, 4274, 3992, -1054], + [-4241, 5299, 4262, -1584], + [-2710, 3862, 4552, -1673], + [-4608, 2472, 3672, -1715], + [-2843, 2816, 4003, -2326], + [-5229, 2964, 5636, 90], + [-4924, 3442, 5015, -1096], + [-1281, 3313, 5537, -2066], + [-3808, 1939, 4351, -919], + [-1915, 2585, 4939, -1614], + [-3470, 1843, 5562, -682], + [-3800, 870, 5827, 144], + [-4985, 1452, 4728, -709], + [-3745, 2750, 7220, 259], + [-1875, 1900, 6514, -826], + [-4329, 1574, 7192, 1304], + [-5408, 1444, 6208, 631], + [-3327, 5312, 5707, -1541], + [-6966, 3334, 4034, 1028], + [-7484, 4245, 4218, -212], + [-6567, 5839, 4539, -512], + [-5715, 5935, 3747, -1186], + [-6410, 4881, 3356, -1610], + [-5146, 2590, 2850, 2172], + [-5196, 4095, 2569, -373], + [-5043, 6025, 4318, 692], + [-5525, 4884, 3513, 370], + [-6804, 7533, 5812, -488], + [-5657, 2480, 4061, 1234], + [-3155, 1472, 6071, 1188], + [-3427, 5217, 3442, 858], + [-4698, 3013, 5517, 2586], + [-4449, 2226, 5418, 3580], + [-6395, 3547, 5487, 2028], + [-3500, 5019, 4787, 1], + [-4038, 2578, 3073, 3151], + [-2750, 1955, 4469, 3856], + [-5696, 1659, 6118, 2469], + [-4350, 1241, 6840, 3126], + [-5565, 5058, 5196, 1314], + [-1642, 4190, 3948, 607], + [-1233, 4108, 4850, -640], + [-997, 3428, 3239, 1378], + [-6488, 2741, 6926, 2792], + [-4188, 3763, 4235, 2018], + [-3210, 3224, 5646, 1427], + [-5526, 6909, 5070, -627], + [-2815, 3994, 3425, 1903], + [-2163, 2734, 5423, 145], + [-4149, 4247, 2355, 734], + [-410, 2521, 4138, -16], + [-2411, 2385, 4927, 2105], + [-6077, 3591, 3114, 594], + [-4186, 4834, 5926, -1004], + [-7315, 3369, 5966, 448], + [-7042, 5721, 5771, 238], + [-4466, 3907, 3535, -1751], + [-2116, 3970, 6163, -1392], + [-7239, 2143, 8407, 3630], + [-5431, 4486, 6486, -42], + [-1874, 1617, 6333, 519], + [-6478, 2629, 4634, -505], + [-7784, 2342, 7216, 1365], + [-1154, 1432, 4831, 1544], + [-4964, -5801, 1797, 506], + [-4436, -6905, 1059, -1237], + [-5400, -6886, 884, -290], + [-6259, -7103, 523, -227], + [-4819, -6450, 1412, -450], + [-4056, -6213, 1725, -943], + [-5642, -6091, 1357, 605], + [-4196, -5678, 2187, -173], + [-4726, -5126, 2470, 321], + [-6642, -5091, 1507, -1005], + [-5304, -5250, 1944, 1579], + [-7179, -5520, 1468, -425], + [-6033, -4895, 1876, -955], + [-6595, -5143, 2207, 1291], + [-4224, -4943, 1846, 1792], + [-7128, -6950, 539, 724], + [-4369, -4901, 2590, 1103], + [-7413, -5696, 1712, 1440], + [-5885, -6821, 418, 871], + [-6828, -5599, 710, -1563], + [-6123, -5817, 1358, 1631], + [-5291, -5622, 578, 2138], + [-7171, -6004, 347, 2208], + [-6083, -5251, 2132, 425], + [-4329, -5721, 407, -2993], + [-5326, -5056, 1119, -1837], + [-5485, -5856, 185, -2389], + [-6529, -5178, 403, -697], + [-6719, -4412, 2726, 871], + [-5126, -5629, 1835, -771], + [-5622, -4361, 2973, 858], + [-5282, -5895, 45, -335], + [-4357, -5656, 1696, -1558], + [-7139, -6659, 627, -409], + [-4415, -6328, 35, 1306], + [-7639, -6110, 1134, 197], + [-3626, -5592, 2019, 901], + [-3547, -5064, 1176, 1738], + [-5075, -3899, 2087, 266], + [-4086, -6311, 1479, 360], + [-6210, -5220, -199, -1477], + [-3910, -5063, 1356, -15], + [-7616, -4977, 461, 2401], + [-6118, -6131, 1258, -563], + [-6127, -4968, 1286, -27], + [-4121, -5852, 1113, 1476], + [-5157, -4881, 1162, -662], + [-4637, -5031, 1179, 709], + [-5509, -5452, -397, 1224], + [-4597, -6861, 646, 467], + [-6247, -4043, 468, 278], + [-5336, -6465, 874, -1472], + [-6998, -6346, 78, -1798], + [-4915, -4530, 2756, -203], + [-6048, -4373, 1468, 1052], + [-4273, -7100, 942, -323], + [-6552, -4287, 2351, 69], + [-6954, -4613, 722, 1521], + [-4201, -5361, 763, -1562], + [-6881, -5596, -748, 669], + [-6695, -3547, -34, 1299], + [-3981, -5728, 84, 111], + [-4663, -4809, 2173, -1031], + [-6599, -6077, 1303, 256], + [-7596, -4265, -5791, -4140], + [-6610, -2758, -5288, -3936], + [-5880, -3865, -6563, -3088], + [-7228, -5510, -7677, -3912], + [-8854, -6553, -8318, -5361], + [-9362, -5249, -6413, -4319], + [-4418, -3110, -6368, -4358], + [-5544, -4203, -6863, -5013], + [-3056, -4316, -5567, -3181], + [-3078, -5999, -5051, -2657], + [-5884, -6292, -5756, -4013], + [-4825, -4549, -5535, -4053], + [-4443, -6126, -5316, -1368], + [-3972, -6341, -6098, -2686], + [-5751, -2781, -5398, -6230], + [-4466, -6135, -5570, -3679], + [-4291, -5992, -3564, -5189], + [-7189, -4429, -7279, -6082], + [-5076, -4433, -2748, -5366], + [-6225, -2825, -6833, -5663], + [-2989, -4792, -3960, -4492], + [-7836, -7773, -7722, -5741], + [-6559, -5703, -5844, -5589], + [-7612, -5438, -4136, -3774], + [-4218, -4176, -6591, -2333], + [-4837, -5063, -6581, 322], + [-6590, -5990, -2980, -3847], + [-5558, -2971, -5489, -1932], + [-7001, -5323, -4975, -1697], + [-4694, -2688, -6904, -3044], + [-8511, -5379, -5767, -2549], + [-7548, -5412, -6522, -2572], + [-6597, -4973, -6423, -1274], + [-6415, -4022, -5168, -1072], + [-5528, -5530, -7218, -2345], + [-4845, -4805, -5943, -1227], + [-6049, -7150, -6744, -2161], + [-9061, -7299, -8542, -4375], + [-5010, -5546, -5416, -82], + [-4135, -4205, -5109, -3373], + [-3311, -5869, -4007, -5061], + [-5993, -6472, -3962, -4718], + [-2966, -5832, -2821, -6305], + [-4851, -5152, -2067, -3930], + [-3620, -4441, -3362, -5836], + [-4469, -5221, -4534, -5592], + [-4022, -6335, -4321, -6107], + [-4899, -4503, -3084, -3725], + [-4490, -8276, -4620, -6236], + [-6591, -4342, -7365, -4063], + [-6498, -5057, -5553, 485], + [-6060, -2714, -7093, -4144], + [-6199, -7774, -7094, -4057], + [-7536, -6424, -6415, -4265], + [-7439, -2454, -6348, -4827], + [-5333, -7565, -4417, -4639], + [-4353, -7103, -4197, -2689], + [-5229, -6549, -5129, -6804], + [-6129, -7701, -5236, -4836], + [-6797, -3983, -3884, -4406], + [-6624, -4467, -4745, -5052], + [-3324, -7596, -2720, -6553], + [-5473, -6284, -1704, -4511], + [-4131, -7263, -3180, -5196], + [-7116, -5565, -3469, 685], + [-6002, -6021, -3858, 576], + [-3144, -8203, -1291, -434], + [-6096, -7027, -4004, 1353], + [-3943, -7709, -2344, -36], + [-4510, -6767, -2642, 631], + [-3657, -11541, -2570, -3984], + [-5959, -8854, -1333, -867], + [-6699, -8866, -1606, -344], + [-3836, -7961, -2334, -2028], + [-3430, -8045, -3037, -672], + [-3868, -9184, -3635, -1819], + [-4258, -9060, -2621, -1008], + [-3595, -8693, -2022, -752], + [-4573, -8048, -3166, -2622], + [-4852, -7903, -1405, 256], + [-4591, -7057, -1560, 965], + [-6963, -7655, -980, 808], + [-5179, -6641, -3356, 1196], + [-7102, -6941, -2798, 2123], + [-6867, -5834, -3320, -770], + [-5977, -7369, -2500, -778], + [-6160, -6400, -934, -2543], + [-6741, -7608, -355, -1289], + [-6856, -6466, -1433, -1643], + [-4786, -6292, -4970, 376], + [-5407, -8866, -2255, -400], + [-3814, -6506, -1387, -3620], + [-4998, -6137, -1200, -4092], + [-5123, -9557, -2849, -1306], + [-4259, -6444, -4395, -338], + [-5221, -6810, -883, 1225], + [-6137, -6215, -2165, 554], + [-3895, -6557, -3176, -1829], + [-3886, -8188, -87, -954], + [-7243, -6707, -2216, -316], + [-5592, -7606, 85, -432], + [-3957, -7945, -504, -144], + [-4617, -7624, 218, -312], + [-4797, -8737, -844, -1051], + [-4478, -8516, -1401, -454], + [-4557, -7058, -302, -2332], + [-6623, -7736, -271, -50], + [-3157, -7532, -1111, -2207], + [-3590, -7300, -1271, 517], + [-4442, -7306, -507, 590], + [-6458, -7524, -2807, 666], + [-4991, -8466, -3363, -785], + [-7474, -7541, -1056, -1839], + [-7501, -8316, -938, -180], + [-5329, -7739, -579, -2341], + [-4549, -7063, -176, -3539], + [-5191, -8612, -1504, -4250], + [-3083, -7058, -2251, 32], + [-4003, -7043, -1093, -791], + [-5523, -8093, -678, -114], + [-3022, -10265, -2070, -3109], + [-3905, -6274, -182, -3652], + [-3269, -9217, -551, -2650], + [-3138, -9314, -1726, -1704], + [-4420, -10339, -1744, -3459], + [-4163, -8609, -2298, -4113], + [-5566, -6505, -1241, -463], + [-3130, -9746, -2352, -4884], + [-7825, -3439, 1451, -1468], + [-8451, -3318, 2360, -435], + [-8462, -4130, 1438, -1024], + [-9425, -4564, 1328, -689], + [-11014, -3202, 2278, 2080], + [-8269, -2761, -146, -440], + [-7497, -2618, -166, 413], + [-8250, -3060, 522, -2133], + [-8365, -5366, 1347, -451], + [-8589, -3979, 2943, 714], + [-8111, -2572, 1272, -1748], + [-7830, -5193, 605, -1484], + [-8119, -4736, 2141, 256], + [-7724, -4769, 1463, -812], + [-7363, -3911, 2540, 4], + [-7974, -3397, 2363, 1366], + [-7359, -4204, 1752, -958], + [-7622, -3505, 660, 916], + [-9934, -3665, 3165, 828], + [-8721, -4162, 62, 1718], + [-9433, -4768, 2722, 1234], + [-7960, -4496, 138, 1528], + [-8198, -3454, -443, 631], + [-7756, -2246, 655, 1137], + [-8841, -3145, 1113, 829], + [-7817, -3298, 1251, 230], + [-9413, -2733, 323, -1862], + [-9408, -4168, 1270, 1549], + [-9037, -3892, -942, 283], + [-8255, -3849, 1301, 1762], + [-9057, -3987, -41, -682], + [-9441, -4187, 2019, -111], + [-9740, -3178, 1602, -871], + [-8344, -2474, 1461, 1506], + [-9752, -2925, 1996, 1243], + [-9199, -3796, 180, 537], + [-9060, -2405, 1140, -1562], + [-9348, -2376, 309, -162], + [-10786, -3182, -5, -1500], + [-8142, -4540, -434, -826], + [-7528, -2341, 1104, -73], + [-9360, -2658, 3062, 56], + [-8267, -2335, 2000, -1193], + [-12169, -3154, 1287, -640], + [-11398, -2120, 946, -1163], + [-8940, -4559, 328, -1696], + [-11025, -4213, 2813, 840], + [-9224, -3581, 2224, 2039], + [-8943, -3337, 1248, -1298], + [-7900, -4042, 485, -2080], + [-9221, -1947, 2191, -880], + [-10762, -1800, 2516, -324], + [-10095, -2238, 981, -1335], + [-11908, -2808, 3255, 645], + [-10640, -4105, 1283, -595], + [-7663, -2863, 2467, -797], + [-10712, -3854, 3710, 1538], + [-10823, -2893, 1408, -801], + [-9874, -3832, 256, -1638], + [-10394, -3391, 2315, -94], + [-11525, -4079, 4153, 2122], + [-9546, -2088, 1541, 481], + [-8731, -2433, 1042, 2160], + [-7852, -3977, -1370, 1677], + [7072, -3420, 1398, -1741], + [6180, -1976, 1280, -3557], + [7692, -1793, 2844, -1700], + [8363, -1773, 3104, -2679], + [9213, -3266, 3756, -3542], + [9650, -2644, 1426, -1318], + [7712, -2796, 3686, -1975], + [7316, -3517, 2821, -622], + [7434, -2594, 2305, -2264], + [7237, -1797, 255, -3114], + [8663, -1983, 1338, -3056], + [6616, -952, 4059, -2652], + [8823, -1327, 1362, -1356], + [9938, -1722, 1287, -2362], + [7207, -1057, 1913, -1315], + [7508, -1585, 870, -1982], + [8217, -3680, 1417, -3170], + [8329, -2541, 1684, -585], + [8062, -2335, 252, -2800], + [8204, -4108, 3097, -2569], + [7701, -3367, 576, -3008], + [7350, -786, 2414, -2129], + [6948, -2568, 1607, -225], + [7684, -2387, 1308, -3449], + [8306, -3458, 2394, -1454], + [8438, -2781, 1043, -1362], + [9175, -2076, 2144, -1987], + [8347, -2709, 3489, -4301], + [5696, -2377, 2870, 851], + [8825, -1243, 2219, -2603], + [8801, -1614, 584, -2513], + [8413, -384, 1421, -2244], + [9228, -3050, 3279, -2164], + [6342, -2698, 3547, -107], + [10053, -2476, 2837, -3168], + [7439, -604, 3177, -3991], + [7749, -1064, 4329, -4855], + [8655, -2177, 2252, -3519], + [8490, -228, 1958, -3233], + [10513, -2968, 1911, -2340], + [8146, -862, 1884, -1723], + [7788, -666, 3004, -2891], + [7785, -1620, 4133, -3417], + [10262, -3731, 3455, -2971], + [8570, -905, 4519, -4649], + [9129, -2562, 463, -2465], + [9451, -3587, 1904, -3056], + [6549, -2236, 3010, -4523], + [7175, -2684, 2967, -3458], + [9872, -3278, 1054, -2472], + [9153, -931, 1217, -2565], + [8789, -3469, 753, -2568], + [6683, -3791, 1797, -3968], + [6801, -1977, 2311, -452], + [6336, -1572, 2612, -3264], + [7996, -1008, 730, -2964], + [7521, -1059, 1573, -3694], + [8148, -3973, 2600, -3572], + [7765, -1532, 2528, -3856], + [7404, -3918, 4472, -143], + [8894, -1398, 3299, -3685], + [5768, -2041, 1487, -637], + [5131, -2865, 2463, -811], + [6439, -1568, 3500, -1550], + [-8878, -6798, -5319, -1452], + [-6332, -9713, -3112, -990], + [-8444, -6316, -3694, -687], + [-6123, -10840, -3637, -4358], + [-4784, -9580, -4577, -2581], + [-6108, -10515, -4859, -2524], + [-7605, -7518, -2327, -2797], + [-9662, -8775, -2467, -2010], + [-6494, -7523, -4715, -118], + [-8290, -8982, -1672, -317], + [-8798, -11051, -3888, -1426], + [-6273, -6623, -6791, -142], + [-8313, -7668, -2141, -1275], + [-6453, -8412, -3589, -4102], + [-6747, -7750, -5690, -2498], + [-7814, -6693, -3174, -2446], + [-10383, -10130, -3931, -2364], + [-10606, -8467, -5539, -2772], + [-9475, -6671, -3305, -2271], + [-8982, -9457, -5635, -4005], + [-10111, -7965, -6515, -4180], + [-7301, -6479, -5364, 720], + [-9543, -8999, -7921, -912], + [-9534, -8562, -3469, -384], + [-7601, -10344, -3205, -1127], + [-8088, -8620, -4954, -2888], + [-8202, -8406, -7038, -3775], + [-7312, -8324, -3334, -1775], + [-8566, -9262, -8071, -4174], + [-7068, -11300, -5573, -2907], + [-8295, -8952, -4366, -1544], + [-11104, -10210, -2285, -384], + [-5213, -7520, -5008, -1339], + [-5889, -7940, -5987, -1385], + [-10816, -8201, -4153, -1485], + [-10277, -8919, -6315, -1652], + [-5888, -10320, -3821, -1733], + [-10497, -7181, -6083, -3032], + [-7721, -9724, -6591, -5336], + [-5688, -7894, -3486, -2552], + [-10014, -10500, -3247, -820], + [-6301, -8765, -4506, -2923], + [-8261, -7847, -6213, -1552], + [-10212, -7481, -8113, -3954], + [-6938, -10874, -6074, -4703], + [-7183, -10968, -4446, -1773], + [-7120, -9193, -1966, -2509], + [-6234, -9263, -2313, -4284], + [-8503, -9857, -2429, -608], + [-9372, -7844, -8391, -2120], + [-7951, -7157, -6535, -11], + [-7256, -9473, -2172, -660], + [-10063, -9612, -2515, -15], + [-6684, -9134, -6109, -4206], + [-8204, -11932, -5220, -2306], + [-9710, -6706, -4115, -3275], + [-6855, -7078, -2409, -4447], + [-7344, -7673, -4479, -4116], + [-8851, -6842, -4927, -2948], + [-8927, -10452, -5633, -2194], + [-8627, -9002, -7176, -1575], + [-8209, -9722, -7021, -3324], + [-3770, -10249, -3623, -4816], + [-8183, -7465, -4090, 646], + [-8163, -7149, 200, 498], + [-8289, -6266, 686, -206], + [-10030, -6241, -1032, -1864], + [-8793, -8327, -773, -169], + [-9149, -6215, 969, -15], + [-8303, -5859, -7, 2006], + [-9682, -7283, 255, 1322], + [-9293, -7227, 71, -231], + [-8525, -6215, 287, -837], + [-10477, -5379, 1159, 1449], + [-10726, -7856, -130, 102], + [-8694, -7461, -1210, 690], + [-9367, -5324, 1103, 3170], + [-10686, -8055, -831, 1633], + [-9201, -6873, -2704, 2258], + [-8421, -5358, -1405, 226], + [-9066, -5830, -307, -1571], + [-11150, -7381, -2746, -900], + [-9978, -5925, -2006, -437], + [-9464, -4741, -273, 1061], + [-10543, -6684, -1113, 1660], + [-10073, -5576, 1083, -269], + [-8826, -5763, 1600, 1486], + [-10445, -9071, -1253, -64], + [-12085, -5799, 2, 769], + [-12939, -6663, 1650, 1437], + [-10932, -6434, -1252, -649], + [-11650, -7826, -2053, 710], + [-12122, -6733, -1889, -731], + [-9093, -6095, -2463, -842], + [-10977, -4364, 469, 420], + [-11488, -6908, -521, 893], + [-9669, -5478, -842, 337], + [-10606, -5203, -632, -1361], + [-10198, -6284, 1662, 1277], + [-10135, -5292, 2435, 3493], + [-11027, -6561, 655, 56], + [-10977, -5030, 1127, -358], + [-12766, -3986, 1348, -335], + [-14244, -7731, 264, 317], + [-15124, -10309, -508, 1447], + [-12821, -8638, -608, 137], + [-13076, -8693, -2852, -431], + [-11156, -5546, -2252, -1600], + [-8692, -7366, -819, -1223], + [-12507, -9816, -1714, -121], + [-10712, -6666, 544, 3349], + [-12462, -5890, -2491, -2318], + [-12468, -7226, 437, 232], + [-11300, -5226, 2068, 687], + [-11994, -8320, -626, 2728], + [-12222, -5476, 1142, 18], + [-10277, -8122, -2418, 2003], + [-13418, -6115, -3563, -2802], + [-14759, -9834, -1243, 21], + [-13699, -5665, 1525, 507], + [-16269, -9476, -701, 163], + [-12677, -5437, -247, -1019], + [-11827, -4295, -181, -1243], + [-12847, -4496, 2984, 1123], + [-13860, -7915, -1166, -547], + [-12276, -8145, -2290, -1527], + [-11417, -4830, 2983, 1854], + [-11793, -6002, 1163, 1940], + [11443, -4920, -3235, 3151], + [11300, -6616, -1506, 1175], + [9198, -4628, -2060, 2390], + [10532, -4027, -643, 912], + [9902, -3573, -1606, 1327], + [9653, -3536, -2240, 1869], + [9948, -5171, -423, 2662], + [12316, -4004, -1989, 281], + [12125, -4800, -1265, -163], + [10650, -2617, -2337, 1462], + [9909, -4968, -2376, 916], + [12944, -4647, -1958, 460], + [12988, -5283, -1141, 41], + [12321, -2915, -3621, 1025], + [11449, -2894, -2728, 351], + [12087, -3041, -2002, -32], + [11558, -4031, -1343, -399], + [12983, -3740, -3516, 1245], + [12099, -2515, -2752, 225], + [12515, -3465, -2701, 550], + [14683, -5022, -5272, 2996], + [12260, -3383, -1215, -528], + [13810, -5422, -2443, 1166], + [13421, -5378, -1886, 721], + [12961, -4259, -2594, 796], + [12266, -2104, -4768, 1591], + [13523, -4710, -3045, 1342], + [12437, -2099, -5610, 2117], + [11850, -2183, -3497, 661], + [12275, -3936, -597, -697], + [12459, -5253, -517, -544], + [12835, -4094, -1322, -168], + [14360, -5677, -3305, 1859], + [13905, -4552, -4309, 2117], + [11559, -3412, -1847, -81], + [13379, -3167, -5764, 2746], + [11910, -1634, -4342, 1052], + [12662, -4742, 71, -974], + [13057, -3254, -4424, 1705], + [15046, -5706, -4851, 3019], + [14162, -4142, -5514, 2843], + [12764, -1845, -6684, 2888], + [13714, -2374, -7838, 3857], + [13295, -1663, -8293, 4073], + [10032, -4152, -3403, 1421], + [10942, -5386, -2222, 950], + [10532, -6385, -1750, 1925], + [10273, -5972, -1534, 643], + [10605, -4782, -1695, 27], + [10988, -5153, -1123, -341], + [11629, -5884, -1060, 48], + [10441, -4045, -2431, 311], + [10788, -3595, -4171, 1807], + [12110, -5686, -2127, 976], + [11746, -4773, -2639, 891], + [11541, -5299, -3031, 1732], + [11416, -2559, -5359, 2198], + [11583, -5376, -704, 677], + [10416, -3214, -3516, 872], + [9651, -5435, -1618, 3255], + [9973, -5133, -996, 3923], + [11707, -4643, -430, -796], + [10994, -2709, -3587, 2302], + [10716, -5118, -645, 270], + [14100, -10314, 1095, 1531], + [12944, -8049, 1105, -741], + [13276, -7035, -511, 274], + [14008, -7254, -283, 139], + [11594, -6536, -91, 1671], + [11732, -8645, 746, 15], + [14613, -7085, -1578, 1183], + [13083, -6224, -750, -4], + [13988, -6256, -1592, 820], + [14678, -8683, 441, 126], + [15571, -8872, -521, 1139], + [15642, -9533, 341, 697], + [15960, -9586, -168, 1121], + [15464, -10239, 1433, -1], + [14934, -7887, -1046, 1080], + [15252, -7630, -1899, 1628], + [15485, -8384, -1234, 1484], + [15962, -8638, -1815, 1931], + [16501, -10664, 398, 1167], + [16146, -10145, 411, 918], + [14573, -7475, -697, 601], + [14302, -7996, 28, 257], + [14769, -6792, -2286, 1574], + [14144, -6137, -2169, 1257], + [14770, -6271, -3111, 1933], + [14110, -8312, 1083, -531], + [15235, -6991, -2993, 2174], + [13222, -5805, 547, -891], + [14796, -8762, 1254, -246], + [16040, -9181, -1005, 1551], + [16487, -10086, -373, 1420], + [15077, -9479, 966, 51], + [13026, -6468, 932, -1080], + [12703, -6152, -33, -573], + [15641, -6810, -4128, 2874], + [13282, -7673, 1583, -1283], + [12373, -7150, 1512, -917], + [12992, -7751, -678, 783], + [10907, -6858, -313, 2597], + [13026, -8963, 125, 2152], + [12770, -9946, 1957, -505], + [12482, -6849, -1268, 833], + [13790, -6181, -138, -279], + [12709, -8382, 2044, 227], + [12244, -6630, 203, -457], + [14209, -6816, -1032, 632], + [15134, -8267, -288, 640], + [13619, -6157, -1090, 356], + [14044, -7413, 725, -484], + [12958, -7753, 2585, -1980], + [13188, -8396, 2306, -1558], + [14379, -9980, 2132, -688], + [14275, -9857, 1162, 179], + [13690, -8648, 1621, -889], + [11770, -6829, -746, 278], + [12732, -8202, 286, 90], + [13630, -10146, 1867, -207], + [12072, -8740, 1299, -645], + [12852, -9492, 1226, 62], + [11792, -7382, -54, -116], + [13779, -9014, 487, 351], + [11951, -7729, 121, 834], + [11970, -9781, 2276, -4], + [12680, -7984, 2787, -787], + [13300, -14488, 6408, -1927], + [13635, -15355, 9153, -3073], + [12804, -13566, 5517, -1625], + [16624, -10854, 1690, 28], + [20387, -18532, 6162, -261], + [16515, -12642, 3392, -519], + [15800, -11095, 2151, -202], + [16824, -11790, 1651, 599], + [17604, -13213, 2563, 538], + [17892, -14177, 3562, 147], + [16987, -11399, 869, 1052], + [17003, -12456, 2442, 265], + [21657, -21806, 9198, -1250], + [16825, -13341, 3980, -686], + [17525, -12714, 1887, 805], + [16419, -11034, 1216, 617], + [20931, -19939, 7469, -684], + [18452, -15390, 4573, -191], + [14778, -10077, 2841, -1209], + [17402, -13319, 3042, 160], + [19365, -17922, 7087, -1061], + [16298, -11941, 2810, -351], + [19087, -16176, 4775, -84], + [17666, -12289, 938, 1224], + [18581, -15894, 5132, -430], + [19823, -16717, 4142, 545], + [19960, -19423, 8400, -1492], + [18973, -16817, 5906, -594], + [19079, -15431, 3528, 503], + [16667, -12485, 4467, -1302], + [19791, -17797, 6196, -529], + [20005, -17606, 5354, -20], + [20123, -18599, 6886, -728], + [19068, -14805, 2394, 1105], + [14443, -13723, 5631, -2029], + [14730, -14231, 5631, -1450], + [16089, -15959, 7271, -2029], + [13473, -11200, 3236, -924], + [14413, -10902, 2347, -267], + [17666, -18662, 11381, -3496], + [14749, -11042, 3305, -275], + [15304, -10486, 1869, -240], + [14809, -12126, 3369, -616], + [16896, -16561, 7307, -1845], + [15782, -14336, 5380, -1264], + [16395, -15520, 6415, -1588], + [13681, -11114, 2584, -320], + [14244, -12326, 4480, -1632], + [15247, -13119, 4265, -898], + [13987, -12091, 3469, -597], + [13941, -12770, 4240, -839], + [13771, -13627, 5252, -1384], + [15010, -16074, 7592, -2249], + [15852, -17226, 8619, -2655], + [18921, -16916, 6875, -1501], + [14909, -11678, 2768, -295], + [18988, -18353, 8424, -2070], + [15457, -15080, 6218, -1513], + [14916, -15512, 6949, -1883], + [18108, -14702, 4681, -701], + [17600, -15733, 5616, -775], + [14070, -13683, 6472, -2626], + [13832, -11914, 5201, -2232], + [18846, -19009, 9192, -1961], + [-11981, -10994, -6324, -2264], + [-10976, -9047, -6546, -3828], + [-11288, -10532, -7014, -4191], + [-10139, -10189, -7799, -2688], + [-10555, -9988, -9181, -2040], + [-11596, -11339, -10022, -2707], + [-13400, -13395, -11306, -4206], + [-9774, -12281, -7466, -4133], + [-10842, -13125, -8777, -4956], + [-11964, -15082, -9779, -5095], + [-9382, -10188, -9053, -4927], + [-11562, -11296, -3651, -985], + [-9287, -10083, -7918, -4069], + [-12821, -16556, -11410, -6195], + [-12628, -8959, -4521, -1113], + [-13845, -11581, -3649, -681], + [-12685, -10269, -5483, -1275], + [-14988, -12874, -5107, -1189], + [-13761, -11367, -6202, -1804], + [-13225, -11249, -7820, -3354], + [-14809, -11992, -3202, -312], + [-15620, -15519, -10210, -3433], + [-12954, -10200, -3139, -611], + [-11536, -9981, -5284, -923], + [-13034, -12417, -4612, -1098], + [-16911, -15505, -6123, -1352], + [-17396, -17685, -8330, -2171], + [-14120, -10764, -2265, -99], + [-12598, -7367, -5406, -3530], + [-14143, -12793, -10909, -5226], + [-14692, -16871, -11626, -5554], + [-12581, -11197, -9194, -3837], + [-16752, -16726, -9746, -2808], + [-10600, -10358, -6560, -1227], + [-14573, -13312, -8957, -3393], + [-10172, -8463, -8579, -3387], + [-11418, -12421, -5522, -1842], + [-11855, -14204, -6669, -2625], + [-13308, -8191, -3941, -2194], + [-10007, -12266, -5022, -1811], + [-13532, -15771, -9497, -3175], + [-11760, -11148, -10339, -5529], + [-12149, -12763, -11198, -3697], + [-12029, -12119, -8555, -1792], + [-16995, -19957, -11447, -3471], + [-13144, -14504, -9988, -3191], + [-9938, -11064, -6139, -3162], + [-8873, -11550, -8294, -6550], + [-9303, -13010, -6150, -2711], + [-15463, -10469, -1766, -170], + [-15985, -11693, -3007, -650], + [-17142, -10671, -1434, 47], + [-16063, -13858, -4817, -1058], + [-19446, -19599, -9594, -2464], + [-20076, -18744, -8313, -1889], + [-15047, -16085, -7590, -2250], + [-13481, -16195, -8552, -2998], + [-13829, -14869, -6704, -1932], + [-16357, -18484, -9802, -2959], + [-10551, -8393, -9303, -5070], + [-11345, -9156, -5641, -3107], + [-13217, -13449, -9270, -4541], + [-11988, -13732, -9995, -6374], + [-11007, -9519, -5168, -4107], + [9930, -7858, 8061, -4375], + [8274, -7867, 5992, -2096], + [9692, -9675, 7621, -3670], + [9589, -8110, 6509, -3010], + [12617, -11976, 10122, -5360], + [11867, -8895, 7948, -5323], + [10388, -10482, 9234, -4324], + [8188, -8220, 7810, -2737], + [10407, -8787, 4806, -1930], + [10348, -8845, 9233, -6614], + [9422, -7091, 4820, -2878], + [9758, -9796, 5584, -2256], + [10188, -7994, 5347, -3343], + [11133, -7455, 4015, -2306], + [10676, -10744, 6093, -2629], + [11522, -12184, 7848, -3375], + [8805, -9883, 5317, -3071], + [9498, -9654, 6555, -3592], + [10488, -8008, 4066, -1252], + [11261, -8930, 6068, -2738], + [12180, -10397, 5027, -1531], + [9138, -8531, 3601, -1959], + [8107, -8380, 4970, -2061], + [9737, -13248, 6438, -2617], + [11178, -10423, 2622, -522], + [9572, -12372, 5199, -2019], + [12057, -12144, 4147, -1099], + [9047, -9925, 2516, -665], + [10790, -8030, 5882, -4386], + [7199, -8426, 6337, -2841], + [7778, -8285, 3529, -3442], + [7559, -10569, 3484, -1332], + [9404, -8115, 7484, -5541], + [7792, -11976, 5546, -2573], + [9313, -10264, 7661, -5195], + [6701, -10725, 4370, -1784], + [4918, -11361, 4507, -4527], + [5147, -12305, 3978, -5556], + [6525, -9899, 4481, -3129], + [7538, -12855, 6060, -4826], + [8659, -12111, 7159, -4430], + [8440, -11304, 4547, -1747], + [9216, -10918, 3507, -1195], + [6165, -9254, 4771, -4677], + [9163, -11019, 5637, -4935], + [13441, -11509, 6676, -2434], + [7912, -9398, 6663, -4048], + [11723, -13745, 8131, -4148], + [6065, -10257, 5005, -6327], + [11618, -12417, 5336, -1894], + [8891, -13924, 8407, -6131], + [9622, -12563, 7908, -5109], + [11479, -10315, 8349, -3991], + [11676, -14103, 6611, -2330], + [11951, -8953, 3829, -1550], + [10486, -8044, 10493, -5920], + [11801, -10769, 9763, -5305], + [6109, -8676, 5827, -1346], + [7030, -9611, 5624, -5761], + [12808, -12886, 8683, -4148], + [13213, -10464, 6381, -3189], + [11796, -13681, 10703, -6075], + [9639, -7949, 9625, -3944], + [8538, -6997, 5309, 453], +]; + +/// §D.10.2 `HFreqVQ`: 1024 × 32 signed 8-bit elements in +/// vector-element order (÷ 2⁴ scaling not yet applied). +#[rustfmt::skip] +pub static HFREQ_VQ_TABLE: [[i8; 32]; 1024] = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-4, -2, 2, 1, -16, -10, 1, 3, 1, 0, 6, 1, -3, 7, 1, -22, 2, -4, -3, 11, 14, 6, -1, 1, -13, 29, -28, 10, 10, -8, 0, -9], + [-8, 8, -7, 10, -3, -12, -5, -8, 1, -2, 9, -2, -5, -18, 1, 9, -8, -8, 3, 41, 7, -9, -9, 22, -42, -29, 14, -18, -14, -32, 1, -15], + [-16, 8, 15, 16, -16, 5, 2, 7, -6, -16, -7, 1, 1, -3, -2, 0, 8, 20, -26, -11, 2, -17, 0, -3, -34, -37, 10, 44, -2, 22, 2, -4], + [7, 14, 5, 6, 15, -1, 3, -3, -9, -23, -5, -14, 8, -1, -14, -6, -5, -8, 54, 31, -6, 18, 2, -19, -2, -11, -30, -6, -19, 2, -2, -14], + [1, 2, -2, -1, -3, -3, 1, -5, 1, -3, -4, -8, 5, -4, 0, 1, 3, 7, -5, -4, -3, -12, 3, -2, -3, 12, -53, -51, 6, -1, 6, 8], + [0, -1, 5, 1, -6, -8, 7, 5, -18, -4, -1, 1, 0, -3, -3, -14, -1, -6, 0, -14, -1, -1, 5, -3, -11, 1, -20, 10, 2, 19, -2, -2], + [2, 4, 3, 0, 5, 0, 3, 1, -2, 0, -6, -3, -4, -5, -3, -3, -7, 0, -34, 4, -43, 17, 0, -53, -13, -7, 24, 14, 5, -18, 9, -20], + [1, 0, -3, 2, 3, -5, -2, 7, -21, 5, -25, 23, 11, -28, 2, 1, -11, 9, 13, -6, -12, 5, 7, 2, 4, -11, -6, -1, 8, 0, 1, -2], + [2, -4, -6, -4, 0, -5, -29, 13, -6, -22, -3, -43, 12, -41, 5, 24, 18, -9, -36, -6, 4, -7, -4, 13, 4, -15, -1, -5, 1, 2, -5, 4], + [0, -1, 13, -6, -5, 1, 0, -3, 1, -5, 19, -22, 31, -27, 4, -15, -6, 15, 9, -13, 1, -9, 10, -17, 4, -1, -1, 4, 2, 0, -3, -5], + [-7, 3, -8, 13, 19, -12, 8, -19, -3, -2, -24, 31, 14, 0, 7, -13, -18, 0, 3, 6, 13, -2, 1, -12, -21, 9, -2, 30, 21, -14, 2, -14], + [-3, -7, 8, -1, -2, -9, 6, 1, -7, 7, 13, 3, -1, -10, 30, 4, -10, 12, 5, 6, -13, -7, -4, -2, -2, 7, -3, -6, 3, 4, 1, 2], + [-8, 9, 2, -3, -5, 2, 0, 9, 3, 7, -4, -16, -13, 3, 23, -27, 18, 46, -38, 6, 4, 43, -1, 0, 8, -7, -4, -1, 11, -7, 6, -3], + [1, 1, 18, -8, -6, 0, 3, 4, 22, -3, -4, -2, -4, -11, 40, -7, -3, -13, -14, -7, -10, 14, 7, 5, -14, 11, -5, 7, 21, -2, 9, -3], + [0, 0, -2, 4, -2, 0, 2, 0, -1, 2, -1, 0, 0, 2, 2, 2, -1, 1, -3, -1, -15, -2, -63, -27, -21, -47, -14, 1, -14, 10, 0, 2], + [1, 0, -4, 0, -3, -9, 4, 2, 6, -6, 0, -5, 11, -7, -15, 6, -7, -6, 3, 7, -15, -5, 23, -13, -6, 12, -8, 9, 2, -3, 3, 4], + [6, 0, 3, 0, -2, -4, 2, 1, 1, -1, 1, -2, -1, -4, -22, -15, -46, -66, 10, 20, 2, -17, 12, -6, 1, -2, -2, 0, 1, -5, 1, 2], + [-1, 0, 0, 1, 0, -4, 0, 1, -10, -3, -8, 5, 7, -11, 2, -11, 29, -25, 11, 10, 0, -1, 5, -7, -2, -5, -2, 4, 4, -3, 5, -2], + [1, -1, -1, -3, -2, 1, -8, -3, 2, -2, 4, -5, -1, -7, -2, 1, -14, -7, 3, -30, -15, -14, 3, -4, -1, 3, -13, -1, -3, 1, 2, 3], + [-1, -2, -3, 2, 2, -3, 3, 1, -3, 2, 0, -4, 6, 5, -5, 10, -57, 3, 22, -50, 1, -2, -5, -6, -1, 5, 1, 2, 2, 1, -2, 2], + [2, 0, -1, -7, 2, 1, 3, 2, 0, 4, 3, -2, 3, -3, 4, -4, 24, -35, -3, 38, -6, -5, 15, 20, 3, 16, -7, -5, 0, -4, -5, 0], + [0, 1, 0, 0, 0, -1, -1, 1, 1, -1, 1, -2, 0, 0, 0, 0, 0, -1, -2, -1, -5, -2, -43, -3, 46, -52, -10, 7, -8, 11, -2, -1], + [0, 0, -1, 0, -1, 2, -41, 33, -44, -48, -15, -26, -9, 6, 3, 3, -3, 2, 2, 2, 2, -1, -1, -2, 1, 3, 0, 0, 5, 2, 3, 1], + [-4, 1, 6, 1, -6, -1, -2, 1, -14, -4, 0, -5, -2, 2, -2, 0, -6, 1, 0, 8, -21, 32, -3, -36, -6, -2, -1, -7, 3, 0, 1, -6], + [-3, -2, 3, 0, 2, 2, 8, -4, -4, 6, 2, 1, 3, -6, 4, 3, 13, 0, -12, -1, 25, -20, -2, -23, -15, 7, -3, -11, -3, 6, -1, 0], + [0, 0, -3, -1, 0, 0, -2, -1, -2, -2, 1, -1, 0, 0, 10, 3, -2, 3, 3, -7, -6, -5, 0, -4, -60, -16, -6, 38, 5, 6, -5, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 1, -1, -1, 0, 1, 0, 0, 1, 0, 0, -1, 0, -8, 2, -9, 10, 40, 31, -56, -21, 4, 20, -4, 7], + [-2, -2, 0, 4, -3, -1, 7, 3, 1, 3, -8, 0, 3, 1, 2, 5, 1, -2, 14, 5, 4, 5, 5, 5, -5, 9, -66, 0, -20, -2, -8, 4], + [-2, -1, 4, -1, -8, -2, -4, -1, -3, -3, 2, -7, -3, 5, 7, -2, 45, 31, -17, -16, -2, -2, -1, -22, 1, -1, -3, 3, 5, -3, 5, -1], + [-4, 0, 7, 5, 8, 7, 2, 9, -9, -9, -7, -11, -3, -8, 17, -4, 34, 32, 18, 22, 1, 2, 1, -7, -5, 6, -1, 6, 4, 10, -2, -7], + [6, 0, 14, 9, 6, -1, -2, -3, 4, -6, -8, 4, 7, -1, 28, 38, 15, -1, 16, -11, 5, 8, 4, -10, 3, -10, -17, 5, 3, 3, 3, 1], + [1, 1, 2, -1, 2, 1, 0, 0, -1, 0, 0, -2, 1, -3, 0, 1, 2, -2, -4, -2, 0, -1, 1, -3, 1, 1, 1, -1, 8, 8, 66, 33], + [-5, 2, -3, -7, 2, -8, -4, 10, 17, -18, -7, 4, -4, -7, -6, -6, -5, 5, -12, 2, 0, 6, 8, -2, 1, 4, -11, 2, 1, 8, 31, 19], + [6, 9, 16, -6, -6, -1, -2, -3, -11, -2, 7, 7, 17, 3, 4, 10, 2, 5, -13, 8, 7, 1, 4, 5, 7, 6, 7, -8, 9, -8, 33, 6], + [3, -1, 1, 0, -7, -5, 0, 14, -7, 1, -7, 1, 2, -4, 7, 10, -16, 12, 1, -6, 3, 8, -1, 10, -13, -6, -12, -23, 12, -3, 30, 14], + [-2, -15, 0, 8, 3, -19, 5, -3, 2, 3, 13, 7, 14, -3, -10, 0, 8, 5, -6, -16, -8, -8, 14, 2, -1, 1, -9, -11, 11, -5, 27, 9], + [-8, 6, -4, 4, -4, -1, 5, 4, 1, -7, -5, -4, -15, 1, 9, 0, 8, 4, 1, -17, 11, -2, -19, -1, -6, -8, 3, -12, 3, -17, 33, -10], + [-3, -1, 2, 7, 7, -2, 9, 8, -18, -1, -13, -10, -3, -3, 11, 8, -2, -12, -8, 1, 4, 9, 14, 10, -3, 0, 2, 1, -2, 3, 31, 10], + [-3, -10, 8, -1, -5, -11, 7, -5, 3, 6, 1, 4, -16, 10, 5, -4, -2, -10, -1, 13, 6, -5, -7, 12, 7, -3, -17, 1, 12, -4, 29, 8], + [1, 2, 5, 2, -6, -7, 0, -1, 6, -1, 10, 6, -4, 5, 2, 2, -2, -8, -6, -11, 14, -13, 27, 3, -2, -12, 5, -16, 2, -26, 20, 15], + [-1, -3, -5, -3, -3, 6, -1, 3, -5, 1, 7, 2, 1, 0, -1, -1, 0, -1, 9, 7, -6, -3, 4, -5, -4, 8, -8, -25, -8, -4, 34, 23], + [-1, -2, 1, 1, -1, -2, -1, 1, -1, 0, 0, 0, 0, -2, -1, 1, 0, 2, 1, -1, 4, 0, 0, 1, -1, 0, 5, 3, 12, -9, 68, -16], + [10, 0, -8, 14, -6, 1, -12, 0, 0, -3, -5, -11, -6, 12, 9, -10, -3, 5, 0, 7, 11, 2, 4, -3, -8, -3, 7, 4, 3, -3, 34, 4], + [-12, 13, -5, 7, -11, -2, -1, 1, -4, -14, -21, 3, -3, -3, -4, -7, -9, -4, 3, -17, -2, -13, 10, -2, 12, -4, 0, -9, 1, -5, 31, 10], + [-10, 6, 5, 6, 4, -7, 10, 0, -28, -3, 0, -11, -1, -5, 16, -10, -16, 7, 20, 2, -4, 2, -5, 0, 15, 6, 5, -10, 7, -9, 20, 4], + [1, -7, -2, -7, 4, -3, -2, -7, -1, -14, 6, -16, 4, -5, -4, -6, -5, 0, -2, 2, -6, 9, -5, 4, -18, 8, -10, 8, 15, 0, 32, 1], + [-5, 7, -3, 7, 15, -4, 0, -16, 9, 5, -5, 5, 4, -3, -12, -9, -18, 10, 2, 2, -3, 7, 3, -1, 6, -9, -10, 3, 15, -4, 35, -7], + [-1, -10, 2, 2, -4, -2, 10, 2, -1, 2, -2, 1, -1, -14, -11, 3, -8, 5, -8, -2, 6, -1, -7, 1, 7, 5, 7, 8, 30, -4, 30, 14], + [2, -2, 1, 2, 3, -8, 3, 0, -2, 0, -9, 2, 1, 4, -6, -1, -2, 5, 0, 1, -2, 12, 6, -3, 9, -3, 4, -12, 21, -39, 24, -2], + [3, 5, 1, -2, -2, -2, -3, 6, -8, -2, -11, -8, -1, 4, 2, 2, -4, -10, 12, -5, -11, 1, -15, -34, -11, -7, -11, -1, 7, -14, 38, -1], + [-4, 4, 8, 9, 8, 1, -5, -9, 4, -2, 15, -4, 11, -15, 20, -1, -1, -3, 4, -9, -2, -2, -2, 8, 6, 12, -5, 0, 11, -12, 27, -4], + [0, 8, -4, 3, -11, 6, -11, 2, 3, 0, 5, -8, -7, -6, -9, -21, 4, -11, -1, -16, -7, 16, -3, 7, -7, 4, -5, 0, 11, -7, 31, 3], + [1, 3, 4, 11, -11, -2, -3, -6, 6, 5, 0, 3, -9, -6, 4, -4, 0, 4, -8, 13, -6, -13, -1, -5, -1, 4, 0, 0, 9, -22, 24, 18], + [-7, 3, 10, -13, -6, 6, -6, 6, 22, 1, 0, -14, 2, 3, 7, -1, 8, 20, -1, 5, -4, 13, 9, -9, -9, 6, 0, -4, 0, -8, 31, -4], + [-3, -4, 0, 1, 7, 3, -7, 0, 5, -2, 1, 3, 3, 1, -5, -2, 5, 2, -11, 4, 0, -1, 12, 0, -3, -13, 15, 8, -6, -27, 34, 0], + [-3, -3, 10, -4, 2, -1, -3, 0, -1, -1, -4, 2, 6, -2, 12, 1, 3, -6, -7, -6, -5, 4, -19, -6, -8, -34, -4, -8, 10, -7, 23, 10], + [-7, 0, -1, -6, 8, 4, -4, 2, -5, -8, -7, -9, -8, 5, 9, 7, -6, 1, -12, -12, -1, -16, 5, 0, 16, 3, -7, -8, 27, -4, 23, 15], + [-8, 4, 8, 5, 6, 11, -3, 5, 3, -1, -11, 6, -5, 0, 2, -6, -3, -6, 4, -1, 5, -5, -12, -6, 7, -5, 9, 3, 6, -7, 29, 1], + [1, 3, -2, -2, -6, -2, 1, 6, -6, -3, 1, 2, 3, 4, 1, 5, -1, 0, 4, 2, 11, 6, 2, -3, 13, -9, -19, 18, -15, -10, 36, 21], + [-3, -3, 2, -1, -7, 6, -4, 1, -3, -1, -2, 2, 3, -7, -3, 0, -2, 0, -2, 6, -19, 3, -8, 2, -6, 7, -1, 0, 29, -6, 28, -10], + [-5, 1, -3, -7, -12, -4, 1, 1, -1, 13, -10, -1, -9, -5, -13, 6, 13, 3, -4, 2, 3, 11, 2, 6, -25, -16, -6, 0, 14, -1, 27, 16], + [-6, -1, -7, -5, -2, -5, -5, -1, 9, 1, 0, 3, -8, -12, -6, 5, -6, 5, 3, -9, 1, 4, -7, -10, -9, -7, -17, -5, -15, -23, 25, 3], + [-8, -2, 9, -3, -4, 3, -1, 8, -7, -7, -5, -4, -2, 9, 4, -1, -7, -4, -5, -16, 3, -6, 18, -13, -9, 16, -15, 8, 15, -10, 24, 5], + [1, -38, 2, 34, 9, 10, 11, 2, 2, -6, 3, 2, -2, 5, 4, -7, -1, 1, 4, 0, 3, 1, -8, -1, -6, 5, 4, 2, -4, 5, 2, -1], + [1, -22, 15, 18, -2, 10, -16, -9, -8, -11, 8, 4, 0, 7, -14, -5, -1, -7, 12, 17, 9, 5, -7, -4, -12, -6, 7, 0, 7, 2, -2, 1], + [-11, -29, 7, 10, 19, -1, -8, -9, 7, 1, 9, 6, 8, -7, -14, 8, -3, -11, -13, 0, -7, -23, -2, -8, 12, 9, 2, 14, 19, 1, -1, 5], + [-24, -27, -11, 36, 2, 6, -3, 4, -6, 8, 0, 12, -1, -4, -6, 3, 4, -1, 2, -3, -2, 3, 2, -1, -2, -4, 0, -1, -2, 7, 2, 3], + [-9, -24, 11, 13, -10, -12, 12, -2, 7, 4, 8, 13, -3, -3, 2, 9, -3, -4, 4, 13, 5, 13, -6, -3, 1, 15, 7, -3, 0, 19, -2, -9], + [-8, -15, 7, 14, -4, -5, 2, -18, -19, -2, 2, 17, 16, 6, -10, 10, -9, 14, -1, -5, -1, -6, -7, 2, 9, 11, 13, 6, -5, -12, 3, 2], + [-10, -37, 13, 1, 3, -14, 0, -20, 4, -3, 8, 2, -2, -3, -9, -5, -3, -17, -1, 13, -11, 2, -6, 4, 4, 0, 3, 1, -9, -4, -5, -4], + [-2, -22, -5, 46, -8, 5, 9, -11, 8, 7, 7, -1, -1, -2, -7, 2, -3, 3, -1, -2, 7, 0, 2, -1, 1, -2, -2, -3, 6, 0, -4, -6], + [-16, -27, 15, 16, -4, 14, -7, -26, 2, -2, 6, 5, -3, 11, 0, 2, 3, 9, -7, -1, 2, -4, -4, -1, 6, 10, 1, 1, -3, -2, 3, 0], + [-3, -22, 10, 26, 1, 2, -3, 3, 17, -3, -7, 9, 1, -21, -4, 5, 3, 0, -7, -6, 3, 3, -8, -7, -9, 3, 7, 1, -8, 12, 6, -7], + [-9, -25, 3, 18, 9, -6, -11, 0, -5, -12, 9, -8, -7, -6, -6, 22, 2, -6, -3, 15, 3, 2, -2, 9, 14, -10, -7, 15, 13, 6, -2, 11], + [5, -20, -5, 28, 11, 10, -4, -4, 0, -7, 3, 5, 2, -5, -8, 2, 6, 10, 9, -9, -18, 3, 14, 1, 3, -3, -1, -6, 7, 7, 2, -1], + [-8, -30, 7, 12, 10, 8, 7, -13, -16, 0, 1, -1, -6, -11, -15, 4, 1, -2, 10, -15, 1, 11, -2, 8, 9, -7, -7, 9, -5, 2, 7, -18], + [-10, -32, 10, 11, 3, -1, 3, -5, 5, 2, 14, -6, 3, 1, 5, -15, -11, 6, 20, 4, 0, -12, -7, 3, 1, -1, 10, 6, -1, -9, -4, -1], + [1, -25, -14, 12, -11, 9, 9, -16, -24, -17, 22, -9, 11, -30, -3, -4, 6, -7, 9, 2, -1, -5, -6, 2, -1, -1, 10, 1, -3, 3, 4, 8], + [-14, -26, -6, 9, 8, 17, -11, -24, -7, -4, -8, -2, 10, 2, 2, -1, 2, 13, 12, -7, 4, -6, -10, 6, 6, -13, -11, -7, -16, 0, -2, 5], + [-4, -30, -13, 12, 16, -6, 12, -16, -13, 5, 15, -2, -2, -10, -7, 7, 11, -1, -4, -2, -4, 7, 4, -8, 1, 3, 0, 11, 3, -2, -5, 4], + [-4, -21, 20, 22, 2, 20, -8, 1, -12, -5, -9, 4, -10, -17, -3, -8, -3, 3, -12, 1, -3, 0, 7, 4, 7, 7, -3, 7, 5, 3, 1, -5], + [-12, -20, 2, 29, 11, -6, 9, -7, -6, -4, 0, 6, 17, -13, -2, -10, -17, -1, -18, 2, 0, 14, -6, 1, 0, 3, 2, -10, 1, -5, -2, 5], + [16, -37, -1, 26, -2, -14, 1, -5, -14, 2, 2, 3, 6, 1, 1, 4, 0, -1, 0, -2, -2, 4, 9, -6, 0, -2, 10, -7, -2, 4, 1, 0], + [-9, -24, -12, 5, 5, 3, -17, -14, 4, 3, 2, -4, 10, -22, -8, -3, 6, 1, 12, -8, 4, 1, 9, -1, 18, -3, 6, 5, 3, -5, 9, -5], + [-14, -33, -2, 20, -13, -10, 2, -7, -1, 11, -9, -8, 18, -3, 1, 8, 0, -2, 10, 7, -2, -13, 9, -3, -4, 5, -2, -2, -1, -5, 1, -7], + [-10, -23, 8, 14, 1, 7, 1, -3, -7, 4, 1, 1, 8, -7, 15, -14, 13, 14, 2, 5, -13, -5, -8, -1, 6, 3, 6, 9, 6, 15, 14, 5], + [-13, -25, -10, 13, -17, -24, -7, -13, -6, -10, -8, 2, 0, -13, -10, -4, -8, 4, -9, 9, -4, 4, -3, -3, 3, 3, -5, -9, 1, -2, 11, 2], + [-12, -23, 1, 18, -11, -2, 5, 9, -5, 5, 14, -9, -3, -2, -6, 2, -2, 11, -13, 1, -3, 11, -9, -4, -2, -6, 8, 10, 1, 4, 2, 1], + [-5, -18, 16, 22, 2, 0, 8, -6, -9, -7, 10, -16, 23, 10, -11, -1, 7, 2, 7, 2, 1, -5, 6, 1, 0, -4, 9, 2, -3, 1, 0, -4], + [-3, -26, 14, 11, 2, -9, 17, -2, -1, -5, -16, -9, -5, 10, -13, 1, 6, 12, 10, 11, 0, 0, -3, -14, 6, -2, 0, 4, -5, -1, -7, -1], + [-10, -33, 1, 8, 11, -5, 1, -6, 7, 4, 5, 6, 1, -2, -10, -5, -6, 12, -11, 5, -10, 4, 12, -1, -1, -3, 4, -1, 9, 0, 16, -17], + [-14, -37, 7, 7, -2, 5, -8, -11, 2, -13, 4, -19, 1, 8, 8, 4, -9, 2, -4, 3, 12, 2, 4, -4, -8, 8, 1, 4, 8, -1, 6, -2], + [-6, -30, 18, 17, 1, -22, -3, 4, -7, -10, 7, 0, -8, 8, -1, 4, 2, 8, 6, -2, 2, 7, 4, 4, 3, -6, 2, 1, -3, 1, -1, -5], + [-17, -18, -3, 22, -8, 1, 9, -2, -17, 20, -5, -5, -12, -5, 4, -5, -9, 8, -2, 16, -3, 0, 19, -8, 8, 1, 2, -4, 0, 11, 0, -3], + [-9, -23, 3, 10, 4, 4, -3, -2, -2, -2, 1, -22, 11, 0, -2, 5, -2, 14, -9, -11, -4, 7, 5, 32, 1, -3, -7, 0, 21, -9, 7, -6], + [0, 0, 0, 2, -1, 1, 0, 1, 3, 0, 0, 1, 0, 1, 0, 1, -3, 0, -1, -2, 0, -1, -1, -3, -1, 1, -4, 1, -1, -5, -69, -19], + [-3, -5, -8, -12, 4, -3, -19, -11, -5, 0, -14, 7, 18, -6, 7, 22, 8, 14, 15, 10, 3, -1, -3, 5, -1, 7, -7, 1, -6, 3, -26, -11], + [-1, -6, 4, -4, -5, -16, 0, -6, -3, 11, 1, 0, 9, 5, 16, 3, -4, -33, -4, 4, -7, 0, 1, 6, -11, -2, -13, -2, -18, 20, -25, -16], + [4, 0, -1, 0, -5, 1, 0, 2, 0, 11, -10, 4, -10, 7, 16, 2, 16, 15, 2, -1, 2, 9, 2, 8, -3, -5, -2, 0, -3, 0, -33, -2], + [-3, -15, 10, 10, -9, -1, 7, 3, 5, -5, -8, -8, -3, 15, -9, 4, 12, 13, -13, -14, 10, -6, 9, 22, -27, 23, -1, 5, -24, 2, -30, 5], + [0, -2, 7, -5, -5, 3, 5, 3, -3, -5, 2, 1, -4, 3, -3, -1, 1, -2, 10, 22, -3, -4, -2, -2, -7, 3, 8, 1, 14, 4, -37, 9], + [-3, -4, -1, 1, -4, 0, 6, 2, 6, -7, -10, -10, -1, -4, 11, -3, 7, -6, 4, -12, -1, 5, 1, -7, 10, -6, 17, -4, 8, 3, -40, 13], + [2, 12, 4, -7, 14, -3, 16, -2, 18, 2, 13, 5, 5, 1, 11, -1, 0, 9, 2, -6, -1, 2, -6, 2, -5, 3, 5, 1, -1, 1, -32, -7], + [-16, 11, 7, -4, 2, -5, -9, 9, 11, 11, 15, -13, -11, 11, 9, 4, 3, -8, -10, 12, 12, 0, 0, -16, -9, 13, 2, 9, 4, -13, -33, 3], + [6, 4, 5, 4, 3, -1, 5, 6, 4, 2, -11, -1, -15, -11, -1, 1, 11, -3, -2, 24, -4, -6, -25, -10, -15, -8, 0, 0, -5, 4, -30, 2], + [10, -3, -6, 1, -9, -5, 6, 9, -10, -3, 8, -1, 4, -1, 11, -11, 3, 9, 11, -3, 6, -17, 5, -8, -33, 9, -13, 19, -2, 9, -25, 2], + [0, 0, -1, -3, 0, -2, 1, 0, 0, 2, 1, 0, -2, 0, -1, 2, 0, -1, 4, -1, 2, -3, 4, -2, 3, 3, 1, 0, -15, 12, -63, 27], + [-2, 14, 9, -1, 3, 0, 1, 1, -19, 15, 3, 4, 0, -10, 1, -5, 3, 0, -5, -10, 2, -16, -4, 8, -12, -6, 7, -5, -10, -1, -33, -4], + [0, 3, 1, 3, 1, 2, 4, 4, 9, -6, -8, -5, 1, -12, 3, 8, -10, 6, -1, 1, 13, -5, -5, 2, -4, 13, -18, -10, -7, -9, -33, 10], + [-6, -3, -12, 5, -1, 11, -6, 0, -2, 1, 2, -7, 3, 1, 3, -2, 1, 8, -10, 7, -1, -3, 3, 0, 13, 1, 6, 7, -16, -7, -39, 8], + [-6, -1, 11, 6, -3, 8, 3, -5, 3, 0, -5, -2, -6, -3, -4, 2, -3, 13, -11, 1, 7, 5, 19, -5, -3, -15, -1, 7, -1, 6, -33, 8], + [-7, 3, -4, -3, -4, 1, 6, -5, -5, 6, -8, -1, -7, 4, -1, -6, -2, 1, 7, 0, 1, 1, -5, 2, -2, 0, -13, -2, -31, -14, -39, -12], + [-10, 9, 0, -3, 1, -1, -1, 0, 1, -5, -1, -4, -2, 5, 2, -7, 18, -8, -2, -19, -7, -7, -12, -14, -11, -1, -9, -13, -7, -12, -31, -9], + [-3, -16, 10, 9, 1, -10, -12, 2, -2, 2, 7, -3, -3, 1, -4, -5, -9, 5, 7, 3, -1, 4, -11, -8, 4, 13, -10, 13, 10, -4, -36, 1], + [-7, -12, 4, -20, -7, -7, 2, 11, -1, -2, 3, -12, 1, 0, -6, -7, 6, 4, 13, 3, -3, 4, 3, -6, -12, 5, -5, -22, -13, -8, -37, -6], + [-7, 5, 3, 5, 7, 9, -14, -3, 10, 17, -1, 1, -12, 5, -6, 0, -4, -9, 0, -11, -14, 3, 13, 6, -25, -8, -12, 4, -10, 18, -30, -1], + [-10, 6, -10, 6, 6, 1, -10, 0, -7, 5, -2, 17, -18, -4, 0, -3, -16, -6, -3, -8, 5, 1, -4, 6, -7, 16, 6, 10, -1, 0, -32, -11], + [-1, 9, 9, -5, 4, 9, 6, 9, -4, -2, 7, 11, 4, 2, -5, -4, -6, 0, 2, -3, -1, 5, 10, 0, 12, -10, -18, -3, -1, 14, -33, 2], + [4, -8, -18, -4, -5, -11, 4, -10, -4, 9, 13, -12, 1, -6, 1, 2, 4, -9, 8, 3, -6, 21, 13, -1, -2, 1, -2, 6, -7, 0, -30, 1], + [6, -1, 2, -3, -1, -4, 6, -4, 0, 4, 2, 2, -9, 2, 6, 3, -2, 4, -1, 9, -6, 0, 7, -8, 5, 19, -2, 9, -5, 2, -33, -8], + [2, 1, 12, -5, -8, 8, 3, -2, -4, 1, -2, 5, -4, -9, -8, -8, 7, -11, -4, 6, -10, 7, -1, -1, -2, -1, 16, 32, -7, 20, -33, -6], + [-18, 2, 6, 13, 9, 9, -1, 3, -17, 24, -2, -6, 28, 8, -2, 6, 3, -10, -34, -16, -13, -4, -15, -11, -12, -3, -10, 4, -8, 4, -31, -4], + [-11, 0, 18, 2, -16, -9, -13, -2, -2, -12, -3, -22, 30, 0, 8, 3, 9, -4, -16, 1, 0, -11, 15, -2, -4, 6, -5, 6, 1, 2, -25, -12], + [14, -1, 5, 7, 3, -15, -8, 1, 5, -2, 12, 13, 11, -25, 3, 1, 0, -2, -4, -16, -23, 0, -5, -17, 7, 5, -9, 6, -5, 2, -32, -7], + [3, -1, 6, 14, 2, -12, -9, -9, 4, 7, 4, 6, 5, -8, 4, 2, 4, 5, -2, 8, 8, -6, 0, 10, -20, -1, 3, -1, 8, 23, -33, -5], + [-3, 11, -6, 3, -4, 5, 7, 3, 4, 5, -2, 3, -1, 30, 6, 1, 8, -6, 0, 0, -9, 6, -9, 4, 2, 9, -6, 1, -12, 0, -34, 18], + [-17, 13, 0, 1, 9, -4, -11, 0, 7, 0, -10, -4, -1, 6, -6, 4, 1, 6, -9, 3, -5, -6, -11, 2, -4, 14, 23, -3, 2, 5, -30, 12], + [-14, 5, -27, 2, 0, 7, 1, 4, 30, 8, 7, 5, 1, -1, 0, 5, 8, -10, 48, -11, 12, 33, 6, 8, -15, 20, -2, -5, 32, 5, -19, 10], + [-16, -4, -12, -7, -2, 0, 8, -6, -20, -18, 16, -3, 0, 31, -2, 11, 2, -9, 49, -19, -12, -23, 10, 26, 16, -2, 4, -21, -14, 13, -11, -9], + [-5, -9, -1, 3, -5, -21, 2, 10, 0, 0, 10, -21, -7, 7, -26, -9, 22, 32, 58, 11, -3, 11, -5, -8, -13, 6, -5, -9, 1, 10, 14, -8], + [7, 7, 10, 3, -2, -1, -11, -11, -6, -43, -3, 14, -19, -18, 19, 18, -32, 10, 45, -6, 6, 21, -20, -12, 2, 4, 6, 6, -4, 3, 3, 1], + [21, 22, -3, -2, -11, -6, -1, -2, 8, 8, 32, -21, 7, 28, -4, -6, -3, -2, 50, 2, 2, 27, -5, -8, 12, 7, -5, -1, -4, -17, 27, 6], + [13, 7, 2, -6, -12, 2, -10, -5, -17, 11, 4, 17, -12, -2, 5, -17, 37, -16, 48, -14, -18, 29, 8, 24, 11, -5, -9, 11, -1, 1, -13, -3], + [1, 1, -1, 2, 0, 0, 0, -1, 1, -1, 7, 2, -3, 3, 0, 6, 2, 10, 54, -25, 7, 54, -5, -6, -1, -15, 9, 13, -24, -15, -12, 3], + [21, 5, 8, 3, -3, -4, -2, -4, 3, -11, -5, -8, 9, 16, 8, -9, -10, -3, 46, -46, 2, 1, -10, 10, 17, 11, -20, -36, 10, 14, 0, -5], + [7, -13, -6, -9, -24, 45, 2, 8, 8, 0, 17, 20, 12, -24, 1, -7, -15, -3, 46, -13, -2, 20, 1, -13, -11, -13, 2, 15, 1, 10, -1, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -2, -1, -16, -9, 31, -69, -34, 26, 7, 17, -1, -6, -1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -5, -20, 18, -82, 22, 3, -7, 9, 4, 6, 2, -4, -1, 0, -2, 2], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, -1, 15, -5, 62, -36, 4, 52, -7, 5, 0, 6, 1, 2, 1, 1, -1, 0], + [3, -19, 19, -20, 13, -4, -11, 8, 8, -16, 10, 1, -14, 30, 1, -33, 10, -11, 45, -30, 3, -4, -3, -13, 7, 12, 3, -22, 3, -2, -4, -2], + [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 11, 8, 70, 48, -10, 21, 4, 9, -9, -9, -4, -6, 0, -1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, -1, 80, 2, -15, -36, -10, -5, -2, 8, -2, 2, 0, 0, 0, 0], + [10, 8, -8, -8, -24, 12, -1, 0, 20, 9, -1, -2, 2, -2, 12, -10, -2, -13, 35, -43, 44, 15, -10, -25, 4, 10, -3, -5, -5, 7, -1, 3], + [1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -2, -1, -18, 9, 49, -72, 7, -8, 7, -5, 2, 3, 2, -2, 1, -2, -3, 1], + [-1, 4, -3, 10, 19, 4, 3, 20, 6, -24, 6, 9, 8, 15, 18, 18, -36, 19, 57, -11, 4, -3, 8, 7, 2, -3, -2, -9, -15, -2, 12, -4], + [20, 3, 11, -9, -4, 22, 42, -25, 1, 5, -10, -19, 0, 9, -16, 5, 2, 10, 44, -29, 17, -3, -9, -2, -1, 8, 14, -7, -1, 16, -5, 1], + [-7, 16, -11, 12, 6, 33, -15, 14, -23, 2, -26, 8, 2, 10, 0, -5, 8, -8, 38, -38, -4, 5, 5, 5, 1, 22, -15, 7, 6, 0, 4, 28], + [-1, -12, 2, 10, -2, 0, 7, 17, 12, 22, -4, 10, 25, 29, 5, 18, 4, 1, 27, -39, 31, 17, 2, 2, 22, -23, 13, 16, 1, -7, -4, -5], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -2, 0, -14, 0, -7, -11, 49, -22, -4, 19, 17, -39, 4, -29, 10, 2, 36, -4, 23, -1], + [-2, -2, -2, -2, 1, 15, -5, -7, -16, -8, -19, 16, -3, -20, 36, -9, -3, 20, 39, -20, 0, 2, 27, -16, 10, 10, -14, -22, -16, -3, 13, -8], + [5, -9, 6, -25, 7, 37, 13, -10, -5, 3, -5, 7, 18, -22, -7, 9, -5, -4, 50, -11, -4, -5, -5, 8, -4, -2, -4, -27, 14, 20, 7, -9], + [0, -14, -10, -27, -14, -17, -6, 26, 10, 2, 14, -12, -5, 0, 8, 9, 0, -28, 55, -7, -12, -7, 4, -10, 10, 7, -12, 11, 3, 5, 9, -8], + [2, 23, 4, -2, -1, -20, -2, 14, 10, -9, -9, -24, 10, 0, 11, -12, 12, 11, 49, -25, -2, 29, 7, -13, 21, -10, 11, -17, 3, 1, -8, 5], + [3, 0, -14, -6, 18, -2, 17, -9, -19, 9, -5, 9, 14, 6, 19, -3, 27, 1, 41, -21, 20, -15, 33, 0, 26, 14, 7, 10, 3, 20, -3, -12], + [-1, 16, 15, -8, 3, -8, -8, 21, -5, -16, -29, 4, 1, -6, -4, -28, 2, 31, 37, -26, -2, 13, 24, 8, -9, -6, -29, 10, 7, 2, 7, 8], + [-10, -10, 11, 13, -32, 2, 16, 9, 14, 23, -15, -13, 24, 13, 4, -27, 14, 12, 31, -18, 17, 23, -2, -7, -14, 9, -17, -6, -10, 20, 9, 6], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 5, 1, 89, 8, 10, -6, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1, 4, -7, 64, -50, 7, 37, 2, 5, 0, 0, 0, 0, 0, 0, 0, 0], + [-2, 5, 3, -4, -4, -3, 2, -3, 3, -3, 5, 4, 1, -6, -1, 1, 6, -2, 50, -35, -7, 43, 7, -7, -5, -26, 24, 21, 3, -15, 5, 6], + [-8, 21, -19, 33, -8, 22, -11, 17, 3, 0, 0, -2, 1, -3, 6, -1, 10, -8, 4, -11, -4, -5, 0, 8, -4, 3, 1, -4, 4, 2, 8, 4], + [-7, 5, -20, 9, -22, 3, -14, 1, 6, 13, 23, -2, -4, -7, 2, 0, 11, 4, 6, 3, -7, -11, -7, 4, 5, 5, -12, 8, 2, 4, 7, -3], + [-7, 6, -4, 20, -20, 16, -2, 7, 6, 16, 11, 12, -7, -7, 5, 3, -9, -4, 1, 2, 5, 2, 1, -9, -2, -17, -4, 6, -10, 7, -7, -6], + [-9, 18, -17, 12, -24, 1, -1, 4, 14, 9, 4, 3, 2, 8, -12, -14, 4, -8, -4, 7, 7, 6, -1, 13, -9, -4, -1, 1, 0, -4, 15, 8], + [-25, 2, -11, 6, -5, 24, -28, -5, 8, 12, -2, 6, 8, -3, 8, -9, -1, -5, -1, -5, 6, -1, -1, -1, -4, 8, -12, -2, -13, 7, 2, 1], + [-14, 14, -18, 20, -10, 12, -2, 9, 1, 0, 12, -2, 15, -10, 26, -17, 16, -11, 10, -10, 9, -2, 4, -8, 2, -3, 4, 4, 2, -3, -5, 1], + [-18, 12, -18, 21, -6, 12, -6, 13, -25, 18, 1, 11, -9, -5, 0, 10, -5, 3, -3, 8, -9, 7, 4, 2, -9, 0, 5, 0, 2, -3, 9, -8], + [-4, 16, 1, 18, -30, 9, 1, 6, -8, 13, 13, -12, -6, -1, 13, 7, 6, 2, -15, -3, 5, 5, 1, -6, 1, -5, 0, 2, -16, 0, 3, -4], + [-21, 1, -2, 6, -43, 18, -1, 5, -1, 4, 6, -2, -1, -3, -1, -3, 0, 1, 2, -9, 0, -1, 0, -2, 0, -1, -1, -2, 6, 0, 1, -2], + [-23, 10, 4, 7, -32, -11, -18, 2, -2, -7, -6, -3, -3, -12, 19, 3, -5, -6, 16, -6, 16, 2, 16, 16, 8, -2, 13, 8, -15, -11, 2, 10], + [-8, 2, -13, 2, -29, 24, -20, 19, 1, 10, -4, 10, 1, 2, -9, 11, -1, -2, 9, -5, 19, -7, 16, -9, -2, -18, 11, 1, 1, 0, 7, -3], + [-6, 3, 4, 13, -26, 10, -10, 28, -7, 28, 1, 7, 0, -14, 5, 7, 4, -4, 3, -2, 3, 3, -11, 7, 6, 4, 0, -1, 2, -1, -3, 2], + [-6, 16, -31, 13, -10, 17, -6, 4, -14, 4, 4, -1, -10, 12, -5, 1, -14, 15, 0, -8, 1, -5, 3, 3, 9, -5, 7, -20, 7, 4, 11, -5], + [-19, 3, -17, 14, -12, 16, -22, 18, 14, 8, -2, 4, 10, 12, -14, 4, -3, 2, 3, 7, -7, 7, -6, 2, -2, -4, -5, 0, -5, -2, 2, 1], + [-9, -7, -11, 24, -36, -9, -11, 5, 7, -12, -13, 18, -2, 20, 1, -4, -1, -10, 15, -6, 14, 1, 0, 2, 1, 2, -9, -16, -11, 7, 13, 0], + [-24, 24, -18, 18, -22, 14, -11, 13, -12, 11, -10, 11, -7, 11, -5, -4, -1, 1, 5, 2, 3, -1, 1, -5, 7, -4, 5, -6, 8, -7, 8, -6], + [-6, 18, -22, 22, 5, 11, -1, 6, 19, 22, 8, 4, -8, 20, -2, 15, -6, -18, 0, -33, -9, -12, -1, 6, 5, 2, 5, 5, -5, -17, -3, -3], + [1, 11, -16, 9, -18, 11, -4, 18, 20, 26, -10, 8, 1, -11, 8, -4, 0, 7, 3, 5, 2, 2, 10, -2, -4, 4, -4, -2, 1, -4, -5, -1], + [-10, 6, -1, 18, -17, 27, -3, 10, -2, 12, -7, -9, 1, 1, -1, 7, -12, -1, -7, -6, -1, 8, 3, -15, 8, 9, 3, -7, 4, -1, 1, -1], + [-14, 6, -16, 22, 2, 5, 0, 5, -18, 11, 6, -3, 22, -20, -9, -3, 6, -6, -7, -15, 1, 15, -8, 11, 8, -3, -8, 1, -8, 2, 6, -2], + [-21, 5, -19, 19, -7, 4, -7, 0, -8, 6, 12, 5, -3, -22, -13, -6, -1, -3, -2, -14, 6, -3, 1, -8, -7, -5, -6, 11, -3, -10, -5, 2], + [-1, 9, -12, 15, -6, 6, -19, 14, -9, 11, 3, 12, -17, -3, 8, -4, -3, -4, 1, -5, 4, 5, -7, -15, -7, 15, -6, -5, 1, -5, -3, 1], + [-12, 20, -15, 20, -14, 3, -14, 9, -6, 33, -13, 6, -2, 8, -6, 7, -5, -6, -3, -3, 0, 8, -3, -3, 1, -2, 2, 2, 6, -5, -5, -2], + [-7, 12, -18, 12, -18, 10, -4, 8, 2, 4, 8, 9, 0, 3, -8, 3, 6, -12, -4, 1, 25, -5, -9, 6, -7, 0, -9, -7, 3, -5, -4, -4], + [-18, 12, -10, 11, -22, 0, -15, 5, -2, 2, -3, 6, -4, -4, -3, -15, -2, -3, 21, 6, -12, -11, 19, 3, 3, -14, 7, 0, -11, -22, -10, 0], + [-15, 2, -30, 15, -17, 13, -16, 8, -7, 10, -8, 2, 11, 3, 10, -7, 7, -22, 12, -10, 3, -12, 6, -10, 12, -10, 7, -8, 5, 2, 9, 1], + [-9, 11, -14, 6, -10, 21, 5, 12, -5, 5, 7, 21, 6, 2, -2, -1, -1, 4, 2, -20, -18, -1, -14, 3, -1, 4, -7, 10, 1, 11, 4, -4], + [-22, 8, -30, 13, -21, -4, 4, -1, 12, 9, -2, -3, 2, -6, 4, -13, -2, 8, 8, 1, -7, 3, -4, -5, -1, -7, -2, 8, 8, 7, 8, 0], + [-6, -4, -35, 16, -13, 15, -11, 14, -7, 9, -1, 11, 7, 0, 13, 10, -1, 8, 1, 1, -2, 8, -1, 2, 2, 3, -10, -1, 7, -13, -3, -7], + [-15, 7, -16, 14, -18, 17, -6, 14, 3, 4, 7, -3, 10, -22, 5, -15, 4, -4, -11, 15, -15, 11, -11, 20, 1, 0, 2, 1, 11, -3, 11, -7], + [-12, 3, 5, 16, -37, -1, 15, 15, -15, 10, 3, -10, 1, 15, 7, -15, -13, 8, 9, -3, 2, 12, -8, 2, -5, 0, -3, 4, 5, -9, -4, 5], + [-16, 26, -4, 14, -22, 26, 6, -3, -8, 4, 21, 6, 16, -4, -11, 7, -10, 3, 3, 7, -4, 2, -9, 8, -2, 2, 5, -2, -4, -2, 7, -1], + [-7, -10, 4, 3, 2, -4, -12, -10, -4, -5, 16, 19, -16, 1, 2, -9, -10, 0, 9, 7, -8, 3, 12, 8, -6, -11, -13, -1, -3, -20, 6, -5], + [-14, -17, 3, -5, 14, -12, -12, 8, -6, -25, 21, 21, 10, -8, -12, 4, 10, -4, 3, -9, 11, 9, 0, 4, 2, -15, 1, -14, 4, 1, 0, -4], + [-4, -9, -3, -1, 6, 3, -6, 6, -10, -4, 14, 8, 2, -3, -12, -19, 0, 11, -20, 1, 6, -2, -27, -6, 10, -17, -14, -17, -9, 8, -8, 3], + [-12, -13, 16, -4, -2, 12, -7, -11, 2, -13, 3, 7, -16, -18, -1, -12, -2, 1, -12, -9, -2, -6, 2, 9, -22, -3, -4, -14, -7, 7, -1, 2], + [-7, -8, -8, 15, 15, 18, 15, 16, -4, -37, 11, 15, -12, -1, -3, 3, 6, 6, 0, -5, -3, -5, 9, 1, 1, -11, -1, -8, -6, 2, 3, 0], + [-6, 7, -5, -12, 13, 10, -18, -4, -3, -21, 6, 16, -15, -7, -12, -9, 1, -12, -1, 10, -2, -1, -3, 4, -4, 1, -16, -1, 12, -9, 5, 9], + [-14, -5, 9, 3, 4, 26, -28, 3, -6, -24, 4, 5, 3, 13, 5, -1, 3, -1, 3, 1, 1, -5, 3, 0, -7, -8, -7, -3, 3, -5, 4, 0], + [-4, 2, -10, -6, 25, 26, -6, 10, -6, -8, 15, 11, -6, -3, 2, -7, 5, 14, 9, -1, 0, -12, 4, -4, -10, 1, -3, 3, -2, -2, -6, -1], + [-10, 8, -15, -10, 19, 17, -8, 0, -3, -7, 7, 5, -13, -1, 7, -7, 1, 13, -12, -13, 17, -12, 1, 26, -18, -3, -5, -6, 4, 5, 8, 1], + [2, -5, 3, 0, 0, 0, 2, -3, -2, -5, 7, 13, -4, 9, 0, -5, 4, -1, -11, -8, -4, 0, -13, 2, -47, -23, -8, -11, -4, 4, -2, -3], + [-18, -4, 4, 5, -1, 17, -12, -8, 1, -12, 7, 20, -12, 3, -2, -11, 16, 12, -6, 1, -13, -16, -6, -3, -3, -5, 4, -12, -5, -9, 10, 1], + [-11, 0, 4, 7, 7, 8, 3, -1, 3, -19, 32, 8, -19, -8, 2, 4, -12, 15, -16, 3, 1, 9, -2, 1, -2, 8, 5, 6, -4, -1, 11, -8], + [3, -1, 4, -2, 14, 32, -9, -23, -10, -12, 22, 15, -1, -2, 10, 0, 4, 6, -8, 4, -15, -2, -1, -4, 0, -8, 4, 1, -8, 3, 4, 1], + [-17, -12, 6, -8, 16, 13, -20, -8, -1, -16, 10, 21, -19, 11, -9, -5, 7, 18, -6, 7, -7, -18, 13, 2, -2, 8, -12, -9, 2, 4, -5, 16], + [4, 0, 17, -11, 12, 7, -12, 5, -1, -25, 30, -8, -7, -6, -4, -7, 9, 8, 7, 3, 3, -16, 8, 0, -2, -2, -18, -3, -4, -5, 1, 4], + [-3, -6, 6, -16, 17, 6, -3, 2, -9, -17, 12, 11, 11, 2, -20, 8, 1, 1, 0, 2, -2, -6, -21, -13, -9, -15, -1, -8, -6, -8, 0, -2], + [-11, -7, 6, -9, 3, 6, 8, 16, 4, -5, 23, 26, -10, -3, 4, 0, 2, 2, -4, 4, -2, -12, 12, 10, -11, 0, -10, -16, 3, 0, 0, -10], + [-5, -16, 10, -6, 27, 13, -3, 4, -2, -13, 15, 5, 2, 5, 3, -4, 13, 12, -11, -7, 0, 1, 11, 12, 2, 13, -15, -8, 9, -2, 3, 8], + [-5, -8, 4, 3, 9, 3, -11, 10, 14, -25, 14, 8, -2, 5, -12, -21, 2, 10, -7, 2, -3, 2, 0, 2, -1, -3, -5, -6, -1, -16, 2, 8], + [-1, 5, 1, -11, 5, 9, -7, 8, -13, -12, 4, 12, -4, 1, -1, -1, 27, 29, 10, 15, 2, -6, -3, 4, -21, 10, -9, -11, -6, -1, -9, -3], + [-6, -3, -1, -6, 11, -5, 0, -2, -5, -31, 11, 3, -1, 5, -3, 4, 5, 7, -10, 5, -10, -13, 4, 12, -15, -2, 2, -7, 1, -9, -3, -10], + [-3, -7, 17, -8, -5, 36, 8, -7, -8, -20, 12, 8, 1, -1, 3, 0, 1, 4, -10, 3, 1, 4, -2, -3, -2, -3, -10, 4, -1, -7, 3, 2], + [-13, -3, -5, 9, 22, 6, -23, 3, -10, -7, 17, 17, 18, -14, -8, -8, 2, 4, -8, 2, -3, -8, 6, 4, -1, 7, 0, 0, -3, 0, -12, -3], + [-3, -10, -15, -3, 9, 3, -23, -9, -13, -18, 12, 13, -2, 0, 1, 8, -1, 2, -7, -12, -5, 14, 2, 1, -22, 6, -10, -8, -9, 28, -7, -14], + [-3, 1, 2, -1, 13, 7, -2, -7, 1, -3, 6, 9, -3, -2, 4, -2, 2, 1, -10, -2, -2, -22, -2, -7, -10, -5, -11, -27, -12, -16, 4, -7], + [2, -6, -3, 1, 8, 0, -2, 12, -3, -4, 58, 15, -10, -4, -2, 2, -2, 0, -2, -6, 2, 4, -1, 1, -4, 1, -1, -5, -4, -3, 3, 1], + [10, -1, 0, 5, 21, 7, -14, 6, -3, -16, 15, 17, -16, 13, 3, -6, -4, 6, -12, -5, 1, -4, -7, -8, 2, 3, -6, 6, -1, -8, 5, 4], + [-6, -2, -8, -11, 15, 10, 0, 8, -6, -15, 33, 8, -2, 18, -15, -11, 5, -1, 0, 15, -15, -4, -4, -1, 10, 7, -13, 4, -4, 0, 8, 3], + [-7, -2, 0, -2, 0, -2, -4, -5, -14, -16, 12, 38, 7, 12, 6, -4, 0, -1, 0, 3, -2, -6, 0, 2, -9, 1, 0, -1, 0, -2, 4, 1], + [-8, -4, 18, 1, 14, 5, -12, -3, 20, -17, 5, 19, -11, -8, 11, -3, 3, 9, -7, -8, 9, -17, 2, 15, -10, -11, 5, -5, 7, 15, -6, -2], + [-7, 2, 38, 5, 19, 16, -5, 4, -13, -20, 0, 4, -4, 6, 4, 2, -7, 6, -8, -2, -5, -7, 6, 3, -4, -3, -2, -3, 7, -6, -4, 0], + [-11, -12, 8, -15, -3, 14, -7, -22, -11, 2, 22, 14, -19, 2, -19, -6, 1, 3, -18, 14, 2, -6, -2, -8, -3, -6, 5, -7, -8, -4, 1, 1], + [8, 7, 25, -21, 12, -6, -5, -4, -10, 6, 0, 10, 1, -12, 18, -5, -15, 4, 1, 14, -1, 5, 8, -7, 1, -7, -3, 9, 10, 1, -1, 0], + [9, 10, 32, -15, 8, 2, 11, -7, -18, -8, 2, -6, -9, -16, -3, 3, -1, 3, 1, -5, 4, -2, 1, -8, 0, -6, -3, -11, 1, 5, 0, 0], + [14, 0, 23, -25, 22, 3, 7, 10, 0, -2, 7, 8, 0, 10, 0, 0, 3, 2, 3, -10, 0, 10, 0, -7, 0, 10, -1, -5, -7, 1, -1, 2], + [12, 0, 25, -18, -5, -4, 13, -10, 3, -6, 7, 21, 0, -16, 3, -10, -6, 5, -7, -3, 2, 5, 3, -6, 4, 9, -8, 12, -2, 3, 2, 4], + [31, 15, 27, -20, 10, -7, 15, -10, 9, -8, 4, -5, 3, -3, 5, 6, 11, -2, -12, -2, 6, -2, 1, 2, -1, -1, 1, 1, 3, 1, 1, 2], + [12, -4, 13, -23, 12, -6, 2, 4, -3, 13, 6, -7, 5, -19, -7, 18, 1, -7, 7, 1, 16, -7, 3, 0, 3, 0, -12, 8, -11, 9, 4, 7], + [29, 1, 3, -22, -5, 6, 0, 12, -14, 11, 1, 6, -3, 4, 6, -2, 4, -13, 12, 1, 1, 3, -11, 9, -10, -1, -7, 16, -11, -1, 3, 9], + [4, 4, 36, -23, -5, -8, -15, 1, -6, 3, 13, -1, -5, -7, 4, 9, 2, -11, -3, 5, 1, 3, -6, -1, -4, -4, -2, 2, 3, -1, -5, -2], + [19, 10, 6, -17, 2, -4, -2, -4, -3, 13, 2, 2, -13, -7, -3, -11, 9, -6, 1, -9, -5, 4, -5, -9, -18, -7, -11, 9, 4, -11, 8, 4], + [16, -3, 9, -16, 18, -2, -12, -16, -11, 11, -18, 16, -13, 6, 2, 8, 3, 8, -4, -16, 10, -11, -1, -3, -8, 5, -9, -4, 9, -4, 0, -3], + [14, 15, 3, -23, -5, 7, -8, -6, 2, 17, 2, 12, -8, -12, 13, -1, -9, 3, 1, 1, 19, 15, 4, -1, 1, 2, -3, 2, -3, 1, 5, 3], + [32, 5, -10, -47, -5, -1, 4, 11, -7, 0, 2, -2, 1, -7, 6, -4, 6, 2, -4, -2, 2, -2, 0, -4, 1, -6, -5, 2, -2, -1, -3, -4], + [20, 8, 10, -21, -7, -9, -16, 12, 1, 4, 6, -5, 9, -11, -7, 4, -11, 28, -3, 2, 4, -6, 10, -8, -5, -5, -9, 9, -2, -1, 6, -5], + [38, 3, 23, -25, -6, -18, 3, -10, -8, 6, -10, 1, -10, 2, 2, 0, -7, 2, -4, 5, -1, 8, -3, 0, 3, 3, -1, 1, 0, -4, -4, 0], + [20, 5, 16, -22, 24, -18, 2, -12, -14, -7, -3, 10, 2, 7, -10, 2, -8, 1, 8, -1, 4, 1, 4, -2, 5, -9, -18, -8, -13, 5, -11, 10], + [14, 8, -12, -16, 9, -11, -3, -6, -25, -7, 6, 5, -7, -16, 10, 2, -7, -1, -9, -3, 16, 4, 3, 3, -3, -3, -15, 13, -3, 4, 13, -7], + [16, -9, 19, -23, 7, -19, -3, -5, -15, 11, -21, 21, -16, 18, -1, 6, 10, -10, 18, -14, 16, -15, 6, -5, -9, 5, -17, 13, -10, 13, 0, 10], + [8, -4, 4, -24, 8, -21, -18, 9, -11, 4, -6, 17, 5, -9, -2, -2, 2, 15, -2, -3, -2, 1, 7, -13, 15, -10, -8, -11, 3, 3, -1, -1], + [14, 17, 6, -32, 5, -17, -2, 0, 15, -1, -5, 16, 1, -5, -2, 9, -3, 8, 4, -2, -2, -4, -3, 1, 0, 7, -3, 4, -5, 0, -7, 2], + [24, 6, 22, -12, 8, 3, -14, 4, -7, 8, 6, 5, 6, 1, 6, -12, 15, 10, 4, 11, 9, 6, -7, -4, 10, -9, 2, -1, -5, 11, 15, 3], + [17, 12, 3, -23, 5, -1, -2, 1, -9, -1, -3, 1, 8, 1, -5, 17, 11, 0, -2, -11, 7, 4, 0, -27, -7, 1, 2, -8, 9, 7, 5, 3], + [12, 10, 12, -10, -4, 5, -1, 2, -24, 5, -8, 2, 6, -17, 19, 5, 12, -2, 16, -7, -6, -14, 4, 1, -3, 13, -16, 5, -1, 4, 1, 1], + [31, 9, 11, -17, 10, -3, -7, 7, 1, 2, 2, 4, -3, -1, 11, 4, -5, -8, 1, 4, 15, -6, -28, 1, 8, 3, -6, 5, 17, -2, 2, -4], + [11, 19, 16, -26, 0, -7, -7, 2, -13, -15, -12, 9, -3, 27, 8, 4, -6, 1, 4, -6, 11, -1, -6, -7, -3, 0, -6, 4, -6, -7, -3, -1], + [10, 18, 16, -32, 19, -9, -4, -3, -7, 8, 8, -3, -11, -2, -6, -16, 13, 13, -6, -1, 10, -2, -2, -9, 0, -3, 9, 4, 11, -2, -6, 6], + [9, 4, 19, -33, 4, 7, -12, 36, -3, -1, 8, -2, 2, -8, -9, -4, -8, 0, 1, -1, 0, -4, -4, 3, 0, 3, 6, 0, -6, 2, 0, -2], + [25, 7, 15, -12, 2, -24, -1, 24, -4, 4, 9, 0, -2, -9, 4, 6, 3, 13, -3, 1, 5, -1, -3, -5, -1, 7, -2, 3, 4, 4, 1, 0], + [19, 6, 8, -20, 9, -9, 5, -4, -13, 7, 11, -3, 5, -13, -9, 6, -11, -1, 0, 4, 11, 26, 3, 6, -7, 12, 6, -3, 1, -9, 7, 1], + [15, 6, 19, -23, -3, -9, 3, 16, -6, -4, 6, -5, -10, 1, 16, -14, 2, 0, 2, -13, -3, 8, -6, 3, 1, 1, 2, -5, 12, -4, -8, -3], + [14, 4, 16, -20, 1, 12, 0, 6, -3, 9, 4, 16, 10, -16, 5, 7, 5, -4, -4, -18, -3, -11, -4, 4, -7, 3, 13, 7, 3, 3, 2, -7], + [22, 3, -1, -30, 18, -3, -9, 9, -2, 11, -16, -2, -14, 12, 0, 4, -5, 4, -1, 3, -20, 12, 4, -10, -2, -2, -12, -12, 10, 6, 11, -3], + [15, 7, 2, -21, 5, 4, 9, -9, -33, 7, 7, 3, -6, -14, -8, 10, 12, 0, 2, -1, 5, 4, -2, 0, -7, 0, 2, 4, 0, 1, -3, 8], + [-7, 0, 12, 3, 0, -6, 8, -4, 0, 2, 14, -15, 2, -7, -31, -3, 14, 0, 14, -15, -1, -4, -15, 10, 1, -3, 1, 2, 5, 2, -8, 1], + [-2, 5, 1, 0, -3, 3, 3, -6, -1, 2, -4, 1, -19, 0, -11, 18, 11, 10, 21, 5, 6, 2, 10, 3, -6, 0, -2, 13, 5, -1, -2, 9], + [-9, 1, -5, 0, 0, -15, 8, 4, 8, 3, 8, 12, -13, -2, -39, -2, 4, -4, 5, -3, -4, 3, -3, 3, 10, 5, 3, 2, -3, 5, -2, 8], + [-9, 6, 6, -8, 12, -12, 23, -18, 4, -15, -5, 2, -20, 13, -7, 7, 7, -12, 14, -12, 6, 1, 1, -3, -8, 9, 0, 1, -7, 3, 7, -6], + [-18, 13, 4, 3, -10, -30, -10, -6, -14, 1, -7, -4, -35, 5, -25, 11, 9, 8, 19, -4, -7, -3, -18, -8, 1, 5, 10, -4, -14, -9, 3, -4], + [-6, -1, 4, -9, -9, 4, 20, 0, 0, 3, 11, 7, -16, -17, -20, 11, -6, -14, 1, 4, 19, 2, -8, 6, -15, 3, 6, -5, -14, 3, 7, 2], + [1, 6, -2, -8, -5, -3, 3, -8, 21, 1, 3, 16, -14, -2, -9, -4, 13, -2, 18, 14, 14, 19, -13, 5, -10, 2, -3, 3, 5, 5, 1, -1], + [-1, -5, -6, -2, -11, -7, 5, -4, 5, -1, 0, 3, -3, 2, -19, 18, 16, 4, 14, -22, -2, -11, -22, 1, -1, 11, 1, 2, 11, -10, 7, -12], + [1, 4, 5, -1, -9, -5, 1, 12, 5, 6, 12, 9, -24, 23, 1, 20, 14, -11, 13, 5, -2, -2, 5, 6, 2, 1, -9, 6, 10, 5, -4, 11], + [-1, -1, 1, 7, -3, -4, 8, -16, 15, -1, -7, 9, -22, -11, -11, 10, 16, 9, -2, 4, 13, 10, 6, 16, 4, 7, 1, -8, -7, -14, -7, 4], + [1, 3, -6, 0, 15, -9, -4, 0, 4, 6, 12, 9, -6, -5, -22, 17, 7, -11, 15, -5, 1, 3, -19, 0, -15, -3, 16, 5, 5, -7, -11, 12], + [-2, -1, 13, 2, 4, -24, 37, -5, -2, -6, 12, 7, -2, -23, -4, 9, 2, -3, 3, 2, 3, 3, -14, 11, 0, -4, -2, -2, 3, 10, -10, 4], + [2, 9, 8, -6, -28, 14, 28, -11, 18, -11, 0, 2, -2, 4, -12, 3, 6, 0, 7, -7, -6, 2, 5, -1, -1, -1, 5, 2, 3, 0, -3, 9], + [-7, 14, 5, -10, -3, 7, 4, -5, 7, -8, -7, 4, -12, 14, -16, 25, 3, 0, 1, -5, 12, -10, 0, -10, 0, 12, 12, 17, 12, 10, -1, 0], + [-4, -2, 5, -2, -17, -3, 5, -5, 7, -17, 1, 5, -4, 4, -20, 0, 11, -15, 13, -8, 10, 1, 1, 5, -12, 9, -8, 0, 6, -1, -11, 4], + [-3, 12, 13, -15, -7, -7, 0, 5, 33, 3, 3, -6, -13, -7, -15, 10, 3, 3, 3, -5, 2, 7, -1, 0, -12, 2, 11, -6, -9, 0, 5, 11], + [-8, 5, 10, -7, -14, -4, 13, 0, 18, -3, -6, 7, 1, -6, 0, 21, 8, -7, 10, -8, -3, 17, -9, 0, -5, 1, 4, 8, -3, 11, -5, 0], + [-8, 8, -3, -8, 8, -11, 16, -16, 17, 0, 8, 16, -17, 10, -16, 10, -8, 6, 11, 0, 10, 7, 4, 5, 7, -5, -5, -6, -7, -5, -1, 16], + [-6, 0, 6, 1, -8, -8, 8, -7, -5, -10, -11, 8, -19, 6, -7, 13, 5, -3, 4, -8, 7, -1, -18, 9, 0, -5, 6, 26, 3, 8, 2, 4], + [-2, -2, 23, -2, -20, 2, 7, -7, -6, -15, 3, 9, -19, -2, -10, 7, -2, 7, 9, 11, 0, 4, -4, 6, 9, -2, 4, -3, 4, 3, 2, 8], + [-6, 12, 10, -10, -7, 4, 17, 11, -6, 1, 12, 11, -18, 8, -12, 4, 1, 13, 6, -13, 23, 9, -5, 8, -2, -5, 1, 3, 0, -2, -4, 4], + [7, 1, 7, -17, -8, 8, -1, -7, 5, -6, 4, -3, -16, 9, -24, 18, -3, 10, 13, -11, -6, -11, -4, 10, 0, 11, 8, 2, 6, -5, -11, 4], + [-4, 1, -5, -10, 0, -3, 9, -2, 4, -1, 1, 5, -41, -10, -7, 4, -3, 3, 1, 0, -12, 4, -3, 0, 2, -1, -2, -5, 3, 2, -7, 5], + [-2, 1, 4, 4, -3, -6, 1, 0, 12, -5, 11, 0, -17, -3, -1, 11, 4, 1, 27, -12, 0, -14, 2, -15, -3, -9, 0, -7, -3, 15, -8, 6], + [-6, 4, 9, 2, 4, 3, 7, -10, 28, 1, -2, 48, 7, 0, -10, 10, 1, -9, 2, -1, 0, 3, -5, 5, -4, -2, 7, 7, 1, 3, 2, 5], + [-3, 3, -1, 3, -9, 0, -1, 3, 2, -6, 39, -14, -12, 5, -19, 21, 7, -6, 4, -1, -4, 0, -4, 1, 0, -9, 1, 10, 0, -2, 0, 7], + [4, 2, -29, 12, 5, -3, 16, -6, 15, -13, -4, -1, -13, 22, -16, 17, 16, 4, 9, -4, 4, -6, -4, 11, -8, 7, 8, 4, 3, -3, -7, -13], + [0, 3, 3, -6, -4, 0, 9, 0, 5, 0, 10, 10, 4, -13, -12, 16, 23, -4, -12, -6, -4, 20, 2, 0, -4, 23, 1, 8, 11, -4, -5, 15], + [-6, 4, -15, -9, -1, -19, 12, -30, -17, -4, 1, -13, -13, 4, -3, 26, 5, -25, 11, -14, -6, -13, 0, -7, 9, 2, 8, -1, -8, 1, -8, 13], + [1, 6, 1, -4, -4, 1, 2, 0, -3, 2, 10, 6, -6, -2, -11, 4, 32, 15, 15, -47, -8, 3, -12, 4, -5, 4, -1, 0, -5, 5, 1, -7], + [2, -1, 0, 0, -1, -6, 0, -6, 4, -4, 5, 9, -5, 1, -3, 51, 4, -5, 4, -14, -1, -4, -3, 1, -4, -1, 0, 2, -8, 0, 1, 2], + [0, 4, -2, -7, -2, -9, 6, -8, 11, -3, -6, 3, -11, -8, -12, 8, 11, 5, 19, 3, -24, 19, -14, 11, -5, -18, -8, -12, -5, -4, -1, 4], + [16, 9, 10, 14, -18, -2, -18, -27, 10, -5, 12, 14, 4, 0, -2, -6, -12, -7, -1, 3, 4, 7, 11, 10, 5, -5, -7, -16, -3, -6, 6, 9], + [7, 15, -9, 10, -19, 4, -5, -37, -2, -4, 8, 2, 4, -1, 1, 9, -5, -5, -12, 1, -1, -8, 3, -3, 4, 6, 9, 3, 3, -1, 2, 4], + [13, 17, 3, 9, -7, -7, -15, -17, -8, -13, -4, -8, 19, 2, 16, 25, 7, 15, 2, 16, -5, -6, -10, -9, -7, -6, -2, -7, 7, 2, 4, 5], + [24, 7, 9, 8, -13, -2, 0, -4, 1, -13, 3, 6, 7, 10, -4, 15, 5, 7, -4, 5, -5, 3, 13, -7, 5, 15, -11, -2, 7, 5, 8, 6], + [17, 6, -15, 23, -2, -1, -6, -2, 0, -4, 11, -3, 12, 15, 6, -8, -15, 10, -9, 7, -1, -11, 2, -8, -4, 3, 4, -10, 4, 4, 11, 1], + [21, 12, -3, 6, -8, 8, -11, -8, -5, -5, 3, 7, -1, -5, 12, 15, -10, -11, 3, 15, 8, 4, 2, -15, 0, 14, 1, -8, -1, 3, 10, -7], + [16, 12, 5, 13, -6, 15, -23, 0, -17, -9, 0, 4, -9, 13, 6, 18, 0, 0, -4, -1, 0, 14, 5, -1, 8, -4, -8, -6, 5, -2, -2, 0], + [14, 16, -1, 12, -15, -9, -6, -20, 4, 6, 8, 9, 3, 1, -9, -4, -1, -11, 9, 11, -12, 1, -14, -7, 2, -8, 11, 9, -4, 10, 4, -16], + [13, 10, 3, 7, 0, -8, -33, -6, 4, -4, 19, -2, 14, 6, 5, 7, 6, -3, -1, -10, -10, -9, 4, -3, 5, 9, 2, 2, 10, 9, -2, -3], + [11, 10, 25, 18, -1, -6, -21, -21, -11, -16, 6, 5, 14, 4, 8, 7, 0, -10, -7, -9, -5, -4, 3, -1, 1, 6, -1, 6, -2, 2, -3, -9], + [15, 9, 5, 22, -17, 15, -9, 7, 7, -9, 13, 9, 10, -1, 8, -3, -2, 6, 1, 17, 8, -14, 7, -3, 12, 9, 1, 0, 1, -5, 17, -18], + [25, 19, -17, 12, -4, -10, 1, -13, -19, -7, -3, 9, 6, -2, 3, 1, 4, -2, -11, -14, -1, -7, -5, -9, 7, -1, -3, 4, -5, 1, 0, -1], + [20, 8, -3, -10, -24, 3, -6, -2, 0, -12, 14, 6, 7, 11, 4, 7, -12, -5, -8, -10, 5, -1, -4, 4, 16, 7, -14, 6, -1, -2, -7, -11], + [16, 18, 17, 1, -15, -6, -5, -3, -1, -19, 8, -2, 2, 8, 12, -19, -12, 8, 0, -3, -1, -1, 4, -14, 9, -1, -12, -1, -7, 10, -3, 5], + [18, 12, -7, 7, 0, -3, -13, 0, -1, -4, 9, -2, 6, -1, 0, 1, 15, -21, 1, -8, 25, -19, 13, -9, 2, 12, 5, -7, -3, -1, -3, 1], + [13, 16, -4, 9, -2, 2, -1, -19, -7, -4, 18, -6, 14, 18, -5, 4, -6, -3, -19, -14, -1, -12, 10, 6, 7, 17, -12, -13, -10, -4, 5, 4], + [27, 17, 4, 14, -9, -2, -4, -8, 0, -6, 14, -11, -7, 2, -3, -3, -2, -3, -13, 12, 16, 1, -5, -9, -10, -11, -2, 3, -7, 5, 11, -7], + [7, 17, -16, -2, -14, -28, -7, -8, 15, -10, 7, 15, 8, 17, 13, -1, 4, -7, -12, -11, 0, 0, 2, 3, -3, 7, -6, 6, 1, -16, 1, -2], + [23, 11, -9, 15, -23, -4, -6, -4, 2, -9, -7, 9, -8, 3, -13, -4, 8, 18, -6, -2, 1, -5, 6, -14, -5, -2, -6, -5, -3, -2, 4, -5], + [12, 13, 18, 18, -35, 2, 7, -17, 3, -11, 6, 9, -3, -2, 10, -4, 3, 3, -2, -7, 0, 2, -4, 0, -4, 0, -6, 5, 10, 4, -3, -1], + [19, 11, 1, 20, -14, 4, -9, -13, -2, 11, 0, 17, -1, -1, -1, -1, -5, -8, 0, 5, -1, -8, 5, -1, 3, 2, -12, 21, -2, -24, 5, 7], + [15, 15, -15, 17, -14, -22, 3, -4, -11, -3, -7, 1, 18, 10, 1, 10, -6, -3, 8, 2, -7, 0, -2, 1, 1, 2, -9, -2, 1, 2, -3, 4], + [45, 13, 8, 17, -5, 2, -16, 2, 8, -2, 8, -15, 4, 5, -1, 7, -6, -2, -6, 2, -3, 0, 0, -9, -1, 7, 2, 3, -3, -3, -1, 5], + [1, 18, -8, 18, -12, -10, 3, 4, -22, -12, 20, 8, -3, 9, 2, 10, -10, -3, 9, 3, 6, -3, 10, -1, -3, 2, -2, 4, 2, 3, -3, -18], + [9, 10, -5, 9, -35, -21, -18, -16, -1, -12, -6, -7, -15, -19, 12, 4, 4, 9, -7, 2, 14, 1, 4, 0, -1, 6, -7, 2, 1, 1, -4, 4], + [31, 8, -17, 35, -8, 1, -5, -6, -7, -6, 10, -2, -3, 6, 9, 3, -6, -2, 3, 3, 5, -3, 0, 6, 0, 1, -5, -3, -2, -4, -1, 0], + [18, 4, -8, 7, -8, -15, -1, -16, 12, 18, 3, 19, 2, 4, 8, 8, 0, -5, -8, -12, 10, -5, 0, 1, 0, 4, -3, 16, 11, 11, -2, -6], + [27, 15, -17, -10, -23, -22, -1, -14, -4, -7, 20, -2, -7, 6, 15, -5, 32, 4, 9, -11, -3, -8, 11, -4, -1, -4, -8, -6, -4, -5, -2, -7], + [22, 4, -7, 2, -15, -11, -17, -10, 2, 0, 15, 11, 7, 12, -8, 6, -10, -18, -6, -12, 7, 3, 22, 3, -7, 14, -5, -2, -13, -7, -1, -7], + [18, 13, 9, 24, -4, -19, -9, -11, 13, 8, 2, 4, -1, 8, 14, 10, -12, 0, 0, 5, 10, 5, 4, -1, 5, 1, -1, 11, 2, -4, 0, -9], + [15, 19, -5, 1, -4, -10, -8, -27, 6, 8, 5, 10, 4, 11, 5, -5, -11, 0, -11, -14, -4, -9, -8, -8, 6, -9, 4, -5, -1, 1, 5, -4], + [18, 1, -13, 14, -14, 9, -15, -7, 12, 1, 13, -4, -20, 12, 10, 12, -12, 7, 1, -13, 10, -6, 5, -3, 4, 8, 10, -13, -3, -6, 9, -3], + [19, -14, 5, -8, -6, 2, -5, 5, -3, -1, -28, 11, 18, -6, -4, -2, 11, 14, -43, -42, 9, 2, 20, -23, 6, 32, 0, 5, 0, 6, 9, 5], + [8, 11, -14, -1, 7, 12, -7, 2, -16, 2, 10, -3, -1, -7, -7, -1, 1, -10, -60, -23, -18, 42, -13, 9, 18, -11, 0, 1, 0, 2, -5, 1], + [-5, -1, 2, 0, 3, -3, 3, -2, -6, 0, -3, -3, 7, 2, 0, -2, -2, 3, -34, -15, 37, 47, 10, 20, 9, 1, 3, -21, -25, -33, -14, 8], + [5, 6, 2, -2, -2, -2, 6, 5, -5, 7, -3, 1, -5, -13, 9, 3, -17, -19, -2, -79, -12, -7, -8, -6, -2, -2, -1, -1, -7, -13, 6, -1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 3, 4, -87, 6, -11, 16, -9, -1, 8, 0, 5, 0, 1, 2, 1], + [-5, 6, 2, -24, 5, -9, -7, 0, 7, 3, -3, 16, -14, -16, 0, 18, 15, -9, -14, -28, -17, 53, 14, -6, -28, -1, -3, -10, -7, -14, 19, -15], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, -13, 0, -53, 3, -22, 63, 19, 16, 1, -11, 0, -3, 0, -3, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -6, -43, -43, -2, 65, -13, -4, 9, 1, 1, 2, 1, 0, 0, 1], + [0, 1, 0, 0, -1, 0, 1, 1, 0, 0, 1, 2, -1, -1, -3, -1, -23, 1, -61, -55, 3, -28, -6, -4, -4, 8, 2, 1, 1, -1, 0, 0], + [0, 1, -1, 1, -1, 0, -1, 0, 1, -1, 0, 1, -1, 0, -9, -4, -48, -19, -52, -46, 11, -12, 5, -14, 0, -10, 0, 0, -1, -2, -1, 0], + [0, -3, -1, -4, 2, -1, -7, 3, 1, 3, -1, 1, -3, 0, -7, 0, 3, -7, -61, -51, -4, -21, -16, -21, -11, 14, -7, 8, 3, -5, 1, 2], + [0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, -1, 9, -3, 56, -11, -6, -67, -1, 13, 0, 7, 1, -9, -1, -1, 0, 0, 1, 0], + [14, 9, -2, 14, -10, -10, 9, -5, 1, -8, -23, 30, 8, -7, 23, 8, 2, 10, -1, -27, -17, 57, 22, 4, -5, 2, -12, -6, 2, -7, -4, -9], + [1, 5, 12, -2, -2, -3, 2, -3, 6, 0, 4, -2, -8, -6, 0, 16, -15, 29, -55, -29, -24, 29, 3, 10, 6, 13, 10, -5, 21, 11, -14, 5], + [4, 2, 26, -6, 10, 11, -23, -10, -27, -20, 3, -24, -11, -10, -13, 25, -10, 5, -9, -36, -7, 43, 3, -13, 6, 13, -2, 0, 1, 3, -3, -4], + [-1, 0, -1, 0, 0, 0, 0, -1, 1, 0, -1, 0, 0, 0, -1, 1, -12, 12, -26, -64, -15, 29, 37, -7, -3, -12, -5, 14, 8, -8, -10, -2], + [19, -4, -11, -16, 8, 14, 5, 19, 3, 22, -11, -21, -1, -6, -11, 11, 10, -24, -23, -40, -8, 20, 17, 5, 13, -6, 3, 14, -20, -8, 3, 28], + [2, -12, 10, -14, -18, 26, -22, 4, -2, 5, -21, 8, 3, 1, 19, 0, -12, 24, -14, -40, 15, 29, -15, 6, 15, 1, -19, 2, 4, 7, -12, -3], + [0, 17, 13, 7, -5, -11, 2, -19, 3, 38, -21, -3, -6, -4, 7, 1, 1, -5, -40, -10, -2, 35, 8, 8, -10, -8, -9, 33, 4, 4, 0, -2], + [-2, -12, 7, 29, -24, 2, 16, -1, -7, 16, 10, -2, -2, -2, 13, -2, -37, 15, -22, -40, -11, 33, 10, -1, 8, 10, 6, 8, 9, 0, -12, 2], + [15, -8, -9, -2, 7, -17, 7, 19, 14, 4, 12, 27, 11, 10, 4, 11, -15, 14, -13, -48, 5, 18, 0, -9, -36, -11, 2, 4, 5, 5, -15, -12], + [-12, 0, 3, 4, 7, -5, 5, -14, -24, -18, -6, -15, -8, -20, 1, -7, -33, -28, -40, -38, -18, -10, -5, 17, -12, 4, 3, -5, 5, -13, 4, -7], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -3, -9, -49, -60, -5, 45, -1, 6, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -3, -9, -49, -60, -5, 45, -1, 6, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 3, -2, 9, -29, -11, 55, 8, 32, -36, -13, -7, 37, 4, 11, 0, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -1, -39, -4, -30, 63, 28, -17, -6, 10, 7, -14, -9, 11, 9, 7], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 13, -2, -50, -32, 22, 51, 4, 7, 6, 11, -20, -13, 9, -5, 21, -4], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -3, -9, -49, -60, -5, 45, -1, 6, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -3, -9, -49, -60, -5, 45, -1, 6, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 3, -2, 9, -29, -11, 55, 8, 32, -36, -13, -7, 37, 4, 11, 0, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -1, -39, -4, -30, 63, 28, -17, -6, 10, 7, -14, -9, 11, 9, 7], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 13, -2, -50, -32, 22, 51, 4, 7, 6, 11, -20, -13, 9, -5, 21, -4], + [-8, 2, 1, 22, -31, -6, -25, -3, -3, 1, -15, -11, -2, -3, 4, -13, -9, 15, -18, 37, -7, -37, 12, -13, -11, -25, -10, -11, -22, 7, 16, 7], + [14, 10, 4, -10, -1, -5, -7, -3, 16, 13, -5, -15, 5, 11, -1, 8, -27, 7, -12, 49, 17, -22, 9, -2, -9, -1, 2, -15, -1, 41, -18, -17], + [-4, -9, -15, -3, 3, 4, 4, 2, 7, -3, -7, -8, -5, 17, -19, -7, 36, -9, -38, 17, 1, -48, 11, -18, -13, -2, -8, 4, -10, -5, 21, 11], + [15, -13, 4, 2, 1, -5, -2, 1, -10, 7, -1, 3, -6, 0, 11, -11, 8, 20, -17, 51, -17, -41, 2, 15, 4, 8, -2, 16, -32, -1, 17, 6], + [-8, 8, -18, -5, 4, 6, -3, 8, 0, -4, 2, 0, -1, -4, 5, 8, 30, 30, -8, 70, 2, 8, 2, 0, 7, 1, 13, -1, -6, -7, -11, 2], + [-8, -7, 9, -10, -13, 6, -11, -14, 13, 25, -26, 5, 2, -5, -5, 5, -8, 4, 0, 33, 12, -38, -4, 6, 13, 6, 25, 34, -1, 25, -19, -5], + [18, 3, -17, 4, -8, 7, 20, 1, -1, 5, -5, -2, -8, 8, -35, 15, 24, 43, -5, 51, 5, -12, -3, 1, -2, 3, -3, -3, -9, 8, -9, 2], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 10, 24, 76, -2, -22, 11, -1, 4, 33, 4, 1, -1, 1, 2, 0], + [0, -1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 2, 0, 24, 13, 32, 70, 26, 5, -21, -9, -6, -15, 2, -2, 2, 4, 1, 1], + [5, -4, -11, 4, -4, 22, 10, -2, 13, -11, -4, -21, -17, 0, -7, 4, 10, -34, 11, 52, 2, -46, -5, 0, 0, -1, 2, 4, -9, 1, 1, -7], + [0, 1, 1, 0, -1, 0, 1, 0, 1, 1, 0, 1, 0, 0, -3, 1, -8, 9, -1, 64, -13, -61, -3, 3, -5, 10, 1, 3, -1, -1, -1, -1], + [0, 1, 0, -1, 0, -1, 0, 0, 1, 0, 0, 0, 1, 1, 2, 1, 10, -2, -31, 79, -10, 27, 0, -1, 3, 8, 1, 1, 0, -1, 0, -1], + [3, 12, 10, 26, -19, 10, -9, 6, -4, -15, 10, 3, -16, 6, 11, -19, 3, 10, 18, 44, 5, -30, 5, -9, 21, 4, 20, 10, 14, -25, 8, -17], + [0, 0, 0, 1, -1, 0, -1, 0, 1, 0, 1, 1, 0, 0, -6, -2, 8, -8, 13, 69, 26, -19, -25, -17, 16, 6, -12, 22, 2, -6, 9, 5], + [0, -1, 0, 1, 0, -1, -1, 0, 0, 1, -2, 1, 0, 0, -4, -1, -34, -15, -33, 56, 9, -42, 9, 10, 6, 9, -8, -11, 0, -6, 15, 5], + [10, 2, -14, -3, -15, -35, -1, 7, -18, 14, 8, -1, -15, -26, 6, -15, -18, 22, 9, 33, 0, -32, -9, 3, -11, 7, 4, -1, 5, 30, 9, 1], + [4, 15, 0, 6, -5, -11, 9, 6, 6, 6, 14, 2, -1, 10, -24, -25, -2, -4, -1, 37, 2, -29, 14, -9, 22, 17, -2, 33, 10, -25, 11, -11], + [0, 5, 2, 18, -12, 21, 22, 33, -7, 21, -9, -7, 7, -15, -7, 16, 7, 0, -14, 44, 10, -25, 5, -4, 15, -8, 10, -4, 5, 9, -1, 16], + [3, 13, 12, 12, 8, 25, -23, 8, -22, -3, -18, -8, 15, 12, 9, 19, 0, 0, -9, 49, -27, -15, -9, -15, 12, -8, -16, -7, 13, 5, 13, 2], + [12, -6, 7, -2, 20, -9, -14, 12, 13, -5, -17, 22, -8, -4, 2, 7, -13, -2, -15, 43, -5, -30, 27, 4, 10, -27, 5, 27, -10, -10, -18, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 10, -18, 70, -2, -52, -1, -7, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 10, -18, 70, -2, -52, -1, -7, 0, 0, 0, 0, 0, 0, 0, 0], + [15, -13, -20, 16, 2, 13, 5, -11, -8, -5, -3, 2, 24, -23, 30, -7, 11, 30, -15, 43, 5, -15, 15, -3, -14, 1, -23, 8, 3, 9, 4, -11], + [0, -1, 0, 1, 0, -1, -1, 0, 0, 1, -2, 1, 0, 0, -4, -1, -34, -15, -33, 56, 9, -42, 9, 10, 6, 9, -8, -11, 0, -6, 15, 5], + [10, 2, -14, -3, -15, -35, -1, 7, -18, 14, 8, -1, -15, -26, 6, -15, -18, 22, 9, 33, 0, -32, -9, 3, -11, 7, 4, -1, 5, 30, 9, 1], + [4, 15, 0, 6, -5, -11, 9, 6, 6, 6, 14, 2, -1, 10, -24, -25, -2, -4, -1, 37, 2, -29, 14, -9, 22, 17, -2, 33, 10, -25, 11, -11], + [0, 5, 2, 18, -12, 21, 22, 33, -7, 21, -9, -7, 7, -15, -7, 16, 7, 0, -14, 44, 10, -25, 5, -4, 15, -8, 10, -4, 5, 9, -1, 16], + [3, 13, 12, 12, 8, 25, -23, 8, -22, -3, -18, -8, 15, 12, 9, 19, 0, 0, -9, 49, -27, -15, -9, -15, 12, -8, -16, -7, 13, 5, 13, 2], + [12, -6, 7, -2, 20, -9, -14, 12, 13, -5, -17, 22, -8, -4, 2, 7, -13, -2, -15, 43, -5, -30, 27, 4, 10, -27, 5, 27, -10, -10, -18, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 10, -18, 70, -2, -52, -1, -7, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 10, -18, 70, -2, -52, -1, -7, 0, 0, 0, 0, 0, 0, 0, 0], + [15, -13, -20, 16, 2, 13, 5, -11, -8, -5, -3, 2, 24, -23, 30, -7, 11, 30, -15, 43, 5, -15, 15, -3, -14, 1, -23, 8, 3, 9, 4, -11], + [16, -18, 7, -4, 31, -15, -9, -13, 20, -12, -6, 0, 12, -6, -2, 4, 3, -3, -1, 0, 1, 3, 3, -2, 1, 6, 4, 0, -3, 2, -5, 1], + [38, -5, -13, -4, 8, -15, 11, 1, 2, -4, -1, 9, 13, 4, -12, -7, 0, -2, 7, 2, -6, -2, -3, -2, 3, -4, 6, 15, 1, 1, -11, -2], + [47, -22, 9, -26, 3, -5, 2, -7, 4, -2, 2, -2, 3, 0, 3, -4, 3, -3, 2, -3, 7, -3, -1, 1, 1, -5, 5, 0, 2, -5, -3, -2], + [14, -16, 2, -6, 7, -2, -7, -4, -4, -7, 14, -3, 7, -19, -14, -17, -29, 6, 26, 16, -5, 13, -4, -1, 21, 14, 1, 3, -6, 0, -7, -1], + [29, -11, 5, -3, 4, 11, 4, -10, 1, -22, -3, -10, 5, 4, 2, 8, -2, -7, -12, -12, -8, -3, -18, -2, -9, -5, -1, -3, 2, -14, -14, 7], + [28, -12, 5, 3, 9, -7, 0, -2, 2, 1, 4, 0, -7, -3, -2, 4, 4, 14, 8, -1, -4, 14, -7, 17, -2, -2, -9, 2, 19, -7, 9, -8], + [31, -18, -22, 8, 15, -5, -10, -15, 1, 10, 6, 7, 6, -8, 2, -1, 12, -3, 3, -1, 1, 5, -6, -4, 0, 1, 7, -10, -2, 4, -3, -4], + [53, -30, -4, 12, 2, 3, -3, -3, 0, 1, 6, 5, -5, -4, -7, 1, 0, 2, 1, 3, 1, 5, 0, 2, 2, -1, 0, 4, 2, 0, -2, 0], + [27, -18, -3, -2, 4, -8, 3, -2, -11, 2, 10, -8, -8, -4, 0, -2, 8, 0, 9, 0, -16, 11, 1, -6, 13, -3, -10, -13, -15, 25, 1, 0], + [35, -5, -1, -8, 23, 11, -14, -3, 2, -2, 8, -6, 17, -2, 7, 0, -2, 10, -17, 13, -2, -2, 11, 11, -14, 2, -2, -3, -8, -1, -12, -5], + [29, -9, 7, 3, 2, -10, 0, 3, 9, 0, -3, 5, 1, -10, 10, -5, 3, 6, -20, -9, -6, -4, 1, 0, 12, 17, -8, 9, 3, -1, -9, 0], + [15, -16, 18, -19, 16, -15, 17, -18, 13, -16, 17, -14, 15, -9, 13, -17, 9, -7, 4, -5, 3, -4, -3, 0, -6, 7, -9, 7, -2, 7, -9, 9], + [21, -10, 7, -2, 12, -7, 13, -17, 11, -2, 20, 3, 5, -11, -6, -6, -15, 0, -9, 5, -11, 7, -1, 7, 8, -10, -9, 3, -5, 9, -8, -2], + [23, -22, 15, -5, 16, -4, -3, -12, 9, 3, -1, -2, -8, 2, -2, -16, 3, 4, -2, -6, -7, 12, -8, 2, -14, 2, -7, 11, -2, 6, -4, -1], + [34, -17, -4, 8, 4, -6, 1, 8, 4, 16, 3, 6, 12, -1, -1, -15, 6, 4, -7, -6, 6, 0, 2, 1, -2, 2, 3, 3, -3, -2, 8, -6], + [18, -18, 2, -2, 10, 1, 18, -23, -3, -10, 0, 4, 20, -19, -3, -4, 2, 8, 6, 1, -3, 1, 1, 3, 5, -1, -11, 3, -7, 5, -1, 1], + [15, -14, 2, 3, 10, -8, 12, -13, 13, -15, 6, -8, -4, -10, 14, -9, 24, 2, -7, -18, 13, -11, 8, 14, -6, -2, 3, -1, -4, 7, -7, -4], + [20, -12, 13, 5, -1, -10, 15, -6, 8, -1, -3, -10, 17, 0, -6, -19, 2, -1, 8, -3, -16, 0, -3, 2, -2, 0, 8, -9, 0, 1, -10, -9], + [32, 0, -9, -5, -1, 5, 13, -11, 8, 3, 11, -11, 0, -8, -2, -14, 7, 10, 6, -5, 1, 10, 2, 12, -10, 4, 4, 6, 4, 0, -7, -10], + [16, -14, 10, -7, 11, -11, 11, -11, 18, -13, 8, -15, 16, -11, 13, -9, 8, -7, 12, -11, 7, -6, 3, -5, 9, -5, 4, -1, 7, -4, 8, -3], + [24, -27, -1, 5, 8, -5, 12, 7, 4, -3, 3, -1, -9, -11, -13, -5, 10, 0, -13, 7, 1, -5, 4, -9, 7, -3, 13, 2, -5, -3, -17, -2], + [23, -19, 15, 1, -10, -18, -12, -6, 8, -3, 12, 0, -12, -10, -4, -4, 8, -10, 4, 2, -2, -8, 13, -3, -2, -6, 2, -3, 5, -2, 2, 11], + [25, -12, 4, 2, 24, -3, 3, -6, 14, 11, 0, -21, -3, -3, 1, -8, 7, 0, 0, 3, 3, -6, -7, 6, 2, 1, -4, 5, -1, 10, -2, 9], + [24, -8, -6, 7, 16, -12, 13, -1, 11, -21, 2, -6, 3, -12, 0, 9, 4, 11, -7, 1, 4, 1, -8, 3, 3, -6, 3, 3, 0, -8, 8, 4], + [25, -21, 13, 14, 13, -18, 4, -3, 0, -5, -4, 5, -3, 0, 4, 12, 7, 3, 5, -5, 2, -2, 3, -10, 2, -9, -15, 6, 1, 7, -5, 1], + [23, -16, -2, 10, 4, -1, 3, 1, 32, 3, -5, -2, 9, 10, -1, -4, -6, 2, 9, -1, 14, 12, -6, -1, -17, -2, -4, -9, -7, -6, -8, 3], + [50, -8, 5, 2, -11, 10, 0, 0, 6, -3, 7, 0, -3, -2, -3, 0, 6, -4, 2, -5, -9, 0, 3, 10, 1, -7, -2, -3, -6, -9, 1, -2], + [28, -17, 0, -2, 2, -9, 1, 5, -4, -1, 0, 0, 19, -27, 5, -12, 7, -14, -3, -6, 10, -2, -4, -2, 4, -5, -2, -7, 1, 7, -9, 4], + [22, -19, -6, -6, 3, -22, 3, 5, 20, -8, -14, -5, 1, 1, 20, 2, 16, 6, 3, 14, 4, 3, 5, 1, 5, -7, -10, -6, 3, -6, 1, -14], + [29, -14, -8, 13, 8, -10, -6, 4, 4, -6, 5, -7, 1, 12, 14, 11, -7, 1, 2, -9, -11, -9, 0, 4, -1, 7, 10, 4, 4, 20, -1, -11], + [18, -9, 4, 1, 7, -29, 12, 1, -1, -9, -2, -1, -2, 2, 9, -8, -13, 5, 4, -13, -4, 2, -5, -7, -6, 14, -10, -34, -3, 1, -3, -13], + [38, -9, 24, 8, 11, 4, -6, -11, -2, -12, 1, 1, -11, -8, -5, -2, -15, -8, 8, 0, 1, -7, 5, 4, -1, 8, -2, 11, -3, -1, -5, -5], + [-20, 11, -4, 24, -11, 1, 15, 4, 0, -28, -10, -1, 10, 10, -6, 5, -6, 2, 7, -2, 1, -2, -6, -3, -7, 1, 2, 12, -1, 7, 0, -2], + [-9, 10, -23, 27, -4, -17, 20, -6, 14, -17, 5, -1, 5, -9, -7, 5, -6, 4, -2, 9, 0, 8, 0, 1, -3, -3, -5, -8, 5, -2, -2, 12], + [-10, 19, 4, 9, 1, -16, 17, -2, 9, -29, -16, -11, -4, 7, -5, 4, -1, -3, 3, 2, 3, -4, 5, -12, -2, 6, 5, -4, 4, 1, 4, 10], + [-20, 10, -24, 14, -5, 11, 9, 0, 16, -20, 10, -5, -6, -6, -1, 2, -4, 5, -16, 8, -2, 5, 5, -11, 9, -11, 4, -11, -1, -1, 4, 3], + [-9, 11, 3, 19, 24, 4, 5, -14, 30, -17, -4, -2, -17, 7, 2, 3, 1, 3, -7, -4, 2, -3, 1, 4, -1, -1, 3, -12, -2, 3, -3, 10], + [-19, 18, 11, 19, 19, 19, 10, 4, 13, 6, 5, 4, 8, 3, -2, 12, -6, -2, 7, -6, 15, 12, 16, 16, 18, -3, -4, -20, 0, 10, -9, -3], + [-21, 9, 20, 12, 0, -3, 5, -9, 15, -13, 5, -5, -6, 24, 2, 9, -5, 2, -7, 2, 5, 7, -5, 2, 15, 3, 1, -1, -4, -2, 7, 0], + [-18, 16, 13, 15, 2, -10, 14, -11, 4, -11, 5, 12, 12, 20, 8, 30, 2, 11, -9, 7, 0, -3, -16, -5, -6, 5, -4, -21, 0, 5, 6, 1], + [-26, 8, -13, 9, 6, -10, 2, -11, 7, -4, 6, -19, -11, -6, -12, 16, 0, 5, -7, 8, 5, 6, 17, -9, 10, -10, 5, -3, -11, 2, 4, 10], + [-11, 17, -3, 22, -5, 18, 3, 1, 4, -5, 14, -27, 5, -7, -4, -5, -10, 11, 1, 15, 1, 1, -6, -5, 10, -22, -7, -7, -15, 13, -4, 5], + [-17, 14, -7, 13, 3, 0, 13, -6, 9, -14, -22, -1, 1, 19, 14, -3, 4, -13, -13, 2, -4, 8, -2, -2, 13, -12, 13, -12, -7, -5, -3, 6], + [-17, 17, -1, 33, 6, 3, 9, -16, 3, -14, -8, 6, -17, 8, 3, 13, 8, -6, 3, 1, -2, 0, -2, 8, 4, 9, 13, -10, 4, -17, 0, -6], + [-20, 7, 7, 21, 1, -3, 7, -3, -2, -12, 9, -7, 2, -3, 14, 1, -1, -7, 12, -10, 5, -20, 11, -2, 0, -24, -17, 6, 6, -4, 3, -1], + [-8, 10, 6, 7, -1, -6, 28, -6, 10, -33, 1, -20, 0, -12, 10, 1, -6, 8, -3, -1, -10, 8, 5, 0, 10, -2, 8, 16, -5, -3, -7, 4], + [-17, 13, 3, 15, 1, -5, 27, -5, 6, -6, 12, 2, -4, 8, -1, -3, -2, 12, -15, 3, 4, 1, 2, -9, 0, -16, -21, 2, -4, 16, -7, 4], + [-15, 20, 8, 17, 5, -14, 15, -11, 21, -11, 13, -13, 2, -15, -13, 1, -5, 5, 2, 10, -9, 4, -1, 3, 2, -4, 13, -5, 1, -4, 5, -3], + [-21, 8, 2, 16, -1, 2, 15, -16, 13, -12, -12, -7, -8, 2, -7, 11, -8, 5, 2, -7, 16, -4, 1, -7, 3, -15, 6, -5, -8, 2, -8, 5], + [-15, 17, -6, 3, -3, 3, 9, -7, 14, -23, 11, 1, -1, 4, 7, 6, -1, -14, 7, 6, -8, 5, 1, -15, 10, -9, 2, -3, -1, 4, -10, -4], + [-10, 18, 3, 11, 1, 4, 14, -14, 7, -4, 15, -10, 10, -11, 10, -4, 5, -14, 10, 4, 15, -12, 15, -13, 20, -15, 14, -15, 8, -11, 4, -6], + [-7, 23, 2, 20, 7, 8, 19, -5, 9, -16, -8, -17, -5, 1, 5, -6, -8, 1, -6, -4, 10, 6, 6, 2, -11, -4, 0, 2, 4, 7, 9, -4], + [-15, 20, -5, 22, 11, -8, 9, -5, 10, -13, -8, 8, 2, -2, -3, 7, 6, 10, 1, 2, -5, -9, 1, 10, 16, -22, -7, 0, 7, 7, 6, 1], + [-26, 19, -5, 3, 5, 25, 18, -5, 9, -14, -8, -6, -2, -6, 2, 3, -8, -2, -7, 7, -3, 7, 3, 4, -8, 0, 1, -8, -4, -2, -2, 1], + [-20, 14, -10, 6, -3, 7, 8, -32, -2, -7, -2, -10, 16, -12, -9, 15, -2, -5, -6, 2, -7, 5, 9, 1, 6, -7, -1, 0, -2, -4, -7, 3], + [-14, 16, 4, 11, -8, 1, 23, -4, 17, -13, -10, 1, 12, 9, 12, -4, 7, -1, -1, 5, -8, -6, 3, 3, -6, -3, -18, 0, 18, 20, 4, -2], + [-33, 19, -10, 30, 15, 2, -3, -1, -4, -14, 7, -7, -1, 7, -8, 9, -1, -3, -5, 2, 2, 4, 0, 5, 0, 0, 2, 3, 3, -3, -3, 4], + [-6, 20, 0, 5, 17, -10, 18, -17, 9, -16, 4, -13, -6, 2, -14, 14, -28, 9, -12, 25, -4, 7, 7, -8, 6, -6, -2, -10, 2, -11, -1, 2], + [-12, 14, 12, 52, -3, 5, -5, 4, 8, -13, 2, -5, -4, 2, -2, -1, -2, 3, 3, 5, 2, 3, 0, 1, -5, 2, -4, -3, 1, -5, -2, 0], + [-13, 6, 9, 24, 0, 8, 14, -15, 18, -9, -11, -8, 3, 15, -2, -4, -9, 4, -3, 12, 14, -13, 11, -4, 2, -4, 0, -6, -6, -6, -14, -1], + [-10, 28, 3, 12, 9, 3, 11, -28, 6, -11, -7, 4, 0, 7, 8, -9, 0, -6, 0, -16, 4, 7, 4, 4, 7, 3, 4, -7, 0, -3, -10, 6], + [-11, 14, -2, 19, -1, -1, 7, 9, -2, -27, 10, -14, 15, -4, 12, -4, 2, -2, -6, 12, -6, 0, -5, -4, -5, 1, 3, -11, 5, -9, 3, -8], + [-18, 7, 13, 16, -4, 3, 9, -10, 10, -10, -3, -22, -4, -12, 3, -16, 0, -3, -16, 8, -11, 1, 10, -7, 15, 3, 0, -1, -13, 8, 1, 6], + [-20, 10, -10, 10, 8, -1, 6, 0, 16, -12, 9, -10, -1, -5, -4, -13, 13, 16, -8, 12, -2, 14, 18, 13, 0, -16, 2, -5, -5, -5, -4, 3], + [-14, 5, -7, -17, 5, -13, 23, 20, -4, -1, 1, -6, 13, 5, -1, 4, -14, -2, -7, 8, 3, 2, 2, -7, 2, -1, 4, 7, 3, -9, -1, -5], + [-19, 3, -24, -28, -9, -7, 19, 3, 2, 19, 7, 5, -13, 8, -15, -17, 3, -11, 4, 13, 3, 2, -1, -3, -4, -4, 2, 0, -5, -6, 6, 2], + [-17, 18, -30, -20, -2, -3, 1, 15, -1, -11, 6, -4, 11, 11, -4, -5, -10, 0, 0, 1, 3, -7, 8, 2, 5, 1, 5, -5, 1, 6, 4, 1], + [-6, 1, -30, -25, -1, -8, -2, -9, -17, 16, 3, -1, -2, -9, -6, -7, -3, 12, 6, -4, -10, 0, 10, -8, -6, -5, -3, -11, -4, 0, -1, -3], + [-1, -1, -34, -28, 1, -10, 2, 9, 4, 16, 2, 6, 14, 17, 0, 7, -4, 4, 4, 4, 0, 1, -1, -5, 8, 1, -4, 1, -9, -2, 5, 6], + [-11, 14, 1, -31, -7, -24, 9, 7, 6, 5, -13, 1, -1, 3, 4, -1, -2, -8, -6, 3, 5, -4, -6, 7, -2, 5, 3, 3, 0, 0, -5, 2], + [-25, 8, -11, -18, 1, -4, 8, -3, -4, 15, 6, -5, 8, 2, 3, 4, -4, 5, 6, 8, -7, 6, 1, -11, -15, -13, 9, -4, -14, 10, 12, 7], + [-20, 11, -15, -25, 3, 4, 18, 13, -4, -5, -9, -1, -5, -2, -2, -7, 16, 5, -4, -5, -7, -2, -3, -9, 11, -2, 0, -7, -17, -6, -11, 6], + [-11, 18, -5, -20, -15, -3, 9, 11, -20, 12, 5, 5, 11, -3, 7, 1, 10, -6, -3, -3, 3, 3, 14, -7, 10, -17, 9, -11, -2, -6, 7, -12], + [-20, 8, -14, -17, -9, -13, -3, 0, -27, -14, -3, -14, 4, 3, 6, -6, 7, 4, 23, 9, 11, 9, 3, -4, 9, 2, 4, -1, -6, 1, -8, -11], + [-9, 14, 2, -37, -7, 13, 6, -11, -6, 9, 18, -11, -6, 2, 12, 4, -1, 3, 1, -2, -2, 1, -9, -4, -2, -3, 3, 5, -6, 0, -2, -8], + [-29, 8, -1, -13, -2, 8, 23, 2, -10, 7, 13, -6, -5, 11, 13, 0, -10, -13, 11, -12, -10, 6, 4, 6, 4, 3, 6, -5, -9, -2, -1, 3], + [-18, 6, -10, -55, -4, -11, -2, 0, 1, -3, -9, -6, 3, -2, -1, 6, 3, -1, 3, 1, -4, -7, -2, 6, 3, -2, -1, -3, -2, 0, 4, 1], + [-14, 5, 3, -21, -8, -16, -4, -2, -11, 27, 15, -20, 3, 0, 1, 1, 2, -5, -5, 4, 1, -9, 5, -3, 3, 0, -4, -2, -11, -4, -3, 7], + [-17, -1, -9, -17, -8, -18, 12, -13, -9, 13, -3, 3, 3, -3, 1, -2, 0, 16, -9, 6, 12, 9, 5, 11, 2, -15, 1, -4, -16, 7, -4, -12], + [-18, 8, -6, -11, -8, -7, 13, 7, 1, 6, 8, -1, 21, -4, 14, 15, 18, -4, -3, 15, 0, 9, 4, 7, 3, -1, 9, -2, 0, 7, -8, 2], + [-10, 7, -18, -29, 3, 12, 12, 9, 11, 4, -1, -15, 1, -1, 8, -2, -2, 10, -15, -1, 0, 6, 12, -6, -1, 10, -6, -3, -11, -4, 9, -6], + [-14, 14, -9, -21, -12, -2, -1, -7, -5, -10, 5, -8, 0, 6, 9, -11, 11, -3, -5, 3, 8, 15, -2, -4, -22, 4, -6, 12, 2, 13, 6, -7], + [-12, 11, -5, -29, -25, 4, 12, -13, -11, -7, 4, 2, 2, -5, 5, 8, 7, -5, -5, 6, 3, -10, 1, -6, 6, -6, -5, -1, -2, -4, 7, 6], + [-15, 11, -5, -16, 0, -13, 26, -23, -6, -3, 5, -2, -2, 21, -6, -3, -5, -1, 6, -1, 0, -13, 2, -3, -9, -1, -4, -3, 5, -4, 12, -16], + [-9, 9, -1, -17, -3, -6, 12, 6, -18, -2, 11, -14, -6, 3, 14, -12, -11, -5, 14, 2, 5, -8, -4, -11, 2, -5, 16, 6, -7, -4, 8, 13], + [-13, 5, 3, -28, -14, 0, 6, 23, 5, 4, -1, -17, 1, -3, 0, 0, 5, 4, 0, -18, 14, 10, 4, 2, 5, -2, 4, -3, 2, 0, 2, 0], + [-15, 4, -13, -16, -3, -12, -2, 2, 7, 10, 9, 3, 11, 4, 23, 14, 9, 16, 4, 1, -12, -3, 4, -7, -15, -7, -10, -14, -6, -8, -1, -6], + [-7, 10, -5, -10, -3, -13, 16, -1, -12, 7, -3, -12, 2, 13, 13, 2, 17, 15, -13, 1, -5, -2, 3, -1, 1, -3, 6, -3, -12, -16, 7, -7], + [-11, -5, -12, -30, -6, -22, 1, 4, -6, -3, 12, 6, 7, 0, 16, 6, -2, 0, -22, -2, -9, 2, -13, 8, 6, -8, 4, -7, -1, -6, 4, 6], + [-14, 5, 1, -27, -4, 2, 1, 14, -11, -7, -8, -4, 1, 8, 0, -6, -13, 11, -12, -7, -5, 1, 10, 7, 3, -2, 0, 6, -8, 2, 10, -1], + [-10, 10, -25, -13, -20, -4, 19, 3, 13, 5, 5, 7, -8, 2, 4, 2, 3, -1, -1, -9, 14, 10, 9, 14, 3, 3, -6, 0, -5, 4, 1, -1], + [-9, 15, -18, -17, 4, -11, 6, 7, -12, 8, -1, -11, 2, 3, 7, 16, -3, -9, 7, -12, 23, 0, 6, 7, -14, -9, 8, 1, -2, 6, -2, -1], + [-6, 9, -16, -26, -14, -11, 9, -6, 5, -2, 13, 17, 21, 7, 18, -19, 6, -23, -2, -15, -2, 2, -10, -8, 2, 1, -2, 4, -3, -4, -5, -4], + [0, 6, -5, -28, -17, -32, 2, -10, 11, 3, -5, 9, 10, 3, 11, 11, -3, 12, -2, 2, 4, -6, 9, -4, -4, -4, -4, -9, 2, 0, 2, 4], + [0, -8, -18, -34, -9, -7, -4, -11, 10, 15, 11, -1, -8, 15, 6, -13, 9, 2, -4, -12, 0, -1, 19, 12, 6, 5, 0, -3, -10, -12, 3, -5], + [-10, 6, -9, -17, -12, -11, 9, -6, 11, 11, 18, -7, 0, 16, 4, 2, -6, 3, -12, -1, 0, 1, -5, -22, -2, -12, 0, 6, 17, 5, 5, 6], + [12, -5, 7, 1, -5, -2, -1, 2, 2, -4, -3, -3, -3, -2, -29, 11, 5, -13, -73, 24, 12, 4, -14, -10, 5, 1, 0, -11, -7, -7, 7, 3], + [10, -3, -1, -3, 4, -11, -5, -2, -8, 7, 9, 2, -8, -6, 6, 7, 21, 17, -54, 47, -14, -10, 14, 19, 13, 21, -4, 3, 1, 2, -4, 2], + [-12, 4, -16, -12, 5, -9, -4, 19, -7, -22, -22, -17, 3, 0, -6, 8, 23, -4, -55, -28, 2, -26, 2, 1, 4, 0, -13, 6, 0, 10, -7, -11], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -1, 35, -1, -67, -35, -24, -24, -6, 2, 2, -2, 1, 3, 2, 0, -1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 5, 0, 41, -4, -73, -15, 18, 4, 17, 8, -1, -16, -1, -2, 1, 0, 0, 0], + [-4, -4, 4, 6, -1, 2, -16, -10, -15, -10, 21, -2, -6, -2, 14, -7, 10, -5, -55, 34, -12, 11, -13, -2, 2, 28, -26, 0, 7, 4, 21, -7], + [2, 1, 15, -22, 10, -3, 14, -6, -2, 15, -2, -7, 20, 6, -15, -7, 23, 10, -60, 8, -4, 29, -22, 2, -13, 9, -10, 12, -1, -3, 4, 7], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -2, 11, -5, -21, -11, -60, -27, -17, -39, 6, 36, 0, -8, 2, 2, 0, 0, -2, 3], + [2, -5, 9, -17, -1, 2, -3, -6, 8, 12, 7, -6, -33, -11, -14, -40, 10, 36, -46, 0, -19, 5, 0, -10, 3, 12, -6, -8, 6, -12, -7, 1], + [1, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 1, 0, -2, 0, 4, -2, -87, -3, -2, 2, -2, 20, 2, 6, -1, 6, 0, 0, 2, -1], + [1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 1, 7, -76, 41, -7, -24, 0, -6, 3, 6, 0, -2, -1, 1, 0, 0], + [0, -3, 4, 2, 3, 2, 2, 0, 3, -1, 4, 0, -1, 4, -2, -4, -32, -11, -64, -29, -9, -43, 2, -11, -1, -7, 0, -4, -2, -2, -2, 2], + [10, -20, 3, -3, 13, 13, 0, -4, 2, 7, -8, 7, -2, 2, -20, -20, -19, 3, -47, -18, -16, -6, -15, -42, -17, 14, -6, 8, 12, -10, 11, -12], + [-3, -2, -2, -1, -1, 4, -3, -1, -6, -2, 3, 2, -3, 6, -1, -9, 10, 13, -68, -9, 26, 3, 5, 3, -21, 10, -15, 21, -22, 19, 11, -14], + [1, 5, 18, -19, -29, -13, -2, 18, -10, 20, 2, 10, -10, 11, 1, 8, -16, -17, -41, 10, -14, -25, 0, -14, -19, 17, 7, -12, 14, -11, 14, 5], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -1, -43, 5, 6, -12, -48, 19, 8, -38, -8, -3, 22, -21, -10, 15, 20, -9, -5, 8], + [0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 6, -3, 22, -14, -71, -24, -2, -33, 23, 7, -8, 7, -3, 2, -4, 1, -8, -2], + [1, 0, -1, 2, 0, -2, 0, 0, -1, 0, 4, 0, 26, -1, 10, -11, -17, -32, -58, 14, -14, -11, -2, 15, 2, -8, 12, 10, -9, 13, -33, -14], + [15, -17, -19, 7, -8, -15, -32, -22, 7, 12, 18, 0, 0, -15, -4, 16, 37, -2, -46, 11, 2, -8, -10, -8, 14, 9, -4, 5, 7, -17, 4, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -5, 3, -85, 23, -9, -17, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -5, 3, -85, 23, -9, -17, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 1, 1, 7, -76, 41, -7, -24, 0, -6, 3, 6, 0, -2, -1, 1, 0, 0], + [0, -3, 4, 2, 3, 2, 2, 0, 3, -1, 4, 0, -1, 4, -2, -4, -32, -11, -64, -29, -9, -43, 2, -11, -1, -7, 0, -4, -2, -2, -2, 2], + [10, -20, 3, -3, 13, 13, 0, -4, 2, 7, -8, 7, -2, 2, -20, -20, -19, 3, -47, -18, -16, -6, -15, -42, -17, 14, -6, 8, 12, -10, 11, -12], + [-3, -2, -2, -1, -1, 4, -3, -1, -6, -2, 3, 2, -3, 6, -1, -9, 10, 13, -68, -9, 26, 3, 5, 3, -21, 10, -15, 21, -22, 19, 11, -14], + [1, 5, 18, -19, -29, -13, -2, 18, -10, 20, 2, 10, -10, 11, 1, 8, -16, -17, -41, 10, -14, -25, 0, -14, -19, 17, 7, -12, 14, -11, 14, 5], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, -1, -43, 5, 6, -12, -48, 19, 8, -38, -8, -3, 22, -21, -10, 15, 20, -9, -5, 8], + [0, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 0, 0, 6, -3, 22, -14, -71, -24, -2, -33, 23, 7, -8, 7, -3, 2, -4, 1, -8, -2], + [1, 0, -1, 2, 0, -2, 0, 0, -1, 0, 4, 0, 26, -1, 10, -11, -17, -32, -58, 14, -14, -11, -2, 15, 2, -8, 12, 10, -9, 13, -33, -14], + [15, -17, -19, 7, -8, -15, -32, -22, 7, 12, 18, 0, 0, -15, -4, 16, 37, -2, -46, 11, 2, -8, -10, -8, 14, 9, -4, 5, 7, -17, 4, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -5, 3, -85, 23, -9, -17, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -5, 3, -85, 23, -9, -17, -2, -2, 0, 0, 0, 0, 0, 0, 0, 0], + [16, 65, -2, -2, 4, 3, 0, -7, 3, 1, 3, 1, 0, 5, 1, -5, 0, 2, -1, 3, 0, 0, -1, -2, 6, 0, -2, 0, 0, -1, 1, 1], + [5, 37, -4, 8, -4, -1, 9, 17, 6, -7, 5, -1, 11, 6, -4, 7, -2, 4, 1, -3, 11, 3, 3, -9, 6, 0, -2, -4, -5, 4, -12, -11], + [15, 24, -14, 2, 6, 17, 26, 5, 8, 11, -9, -7, -6, -8, 3, -5, 9, 10, -3, 10, 0, 1, 4, -9, 4, 9, 3, 0, 4, 0, -5, 3], + [9, 36, -9, -8, 7, 7, 4, 3, -1, -16, -2, 7, -5, -6, 6, 12, -11, -12, 9, -1, -3, -9, 12, 6, -6, 2, 2, 5, 0, 5, 6, -6], + [25, 39, -5, 24, 3, 10, 3, -6, 13, -8, 3, -7, 2, -10, -5, 2, -2, 3, 5, -2, 1, 5, -2, 3, -4, 1, -5, -4, 0, 1, -2, 0], + [16, 27, -1, 0, -14, 6, 4, -5, 7, -2, -6, 0, -3, -5, 2, -1, -1, -19, 5, -8, 0, 11, 12, 5, 0, 3, 10, 6, -14, 14, -13, -15], + [12, 23, -14, 2, 1, 4, -3, 16, 7, -8, 2, -8, 8, 6, -8, -7, -3, 0, 2, 8, -13, 7, 13, -6, -4, 6, -13, -16, 14, 11, -7, 5], + [16, 28, -7, -1, 6, -3, 9, 0, -7, 3, 0, 3, -12, 20, 8, 9, 8, 23, 8, -13, -2, 4, 9, 3, -5, 13, 5, -2, 12, 14, 5, -1], + [19, 37, 19, 5, 7, 5, 10, 5, 19, 10, 14, 0, 2, 5, 1, -4, -4, 2, 2, -5, -2, -1, 2, -6, -4, -4, -5, -3, 2, -2, -2, -2], + [24, 21, 1, -11, -10, 17, -14, 14, 6, -1, -6, -1, 0, -13, -1, -12, -2, -5, 6, -4, -12, 14, 5, -2, -8, -8, 15, -7, -30, -12, 4, 0], + [11, 26, -3, 3, 5, -1, -2, 3, -2, 10, 15, -4, 10, -28, 10, -17, -8, 1, 2, -7, -1, -6, -15, -1, 4, 5, -7, 9, 0, -5, -4, 4], + [18, 32, 1, 2, -7, 4, 15, 2, -9, -2, 12, -11, 7, 11, 13, 2, 0, 5, 9, -10, 16, 3, -3, 5, -9, -23, 2, -2, -1, 5, 2, 11], + [35, 24, -20, 2, 4, -1, 5, 14, -10, -9, 8, -7, 0, 5, -7, -7, 11, 1, 5, 3, 2, 0, -2, 3, 0, 1, 4, 0, -2, -8, 0, -4], + [9, 35, -1, 2, -1, -19, -3, 12, -1, 8, 8, -13, -1, -2, 2, 5, -8, -1, 13, -2, 11, 1, 0, -10, 0, -3, -7, 2, 1, -12, 3, 12], + [20, 27, -12, -12, 7, 4, -1, -13, -1, -9, 2, 13, -11, 5, 7, -9, 9, 1, 1, 8, -9, 0, -6, 7, 4, 2, -2, 7, 3, -2, 1, -9], + [8, 37, -20, -5, 0, -21, 10, -8, 3, 19, -9, 7, -3, -8, 10, -2, 0, 5, 6, -4, -2, -1, 0, -7, 6, 1, 0, 4, -5, 6, -8, 2], + [8, 27, 1, -3, -5, 1, 6, 0, 15, 2, 17, -1, 3, -17, 10, 5, 5, -6, -6, 6, -10, 18, -5, 0, 0, 13, 7, 10, -5, -6, -2, -4], + [14, 29, -20, -4, -3, 1, -5, -1, 2, 12, -10, -3, 4, -18, 4, 14, -4, -1, -9, 15, -2, 2, -5, -3, 2, 9, -2, -14, -3, 4, -4, -7], + [23, 23, -23, -11, 27, 4, 4, -1, 7, 0, -5, 9, 2, -11, 3, 7, -2, -5, 2, -7, -7, 13, -3, -6, 2, 3, 3, -4, -1, -8, 5, -2], + [16, 26, -6, 8, -9, -1, -2, -1, -8, 4, -2, 0, -12, 9, -1, 0, -17, -9, 30, -5, -15, -16, -13, 0, 10, -11, -7, -3, -1, 0, -11, -2], + [12, 32, -4, -5, 10, 19, -10, 4, -12, 5, -6, 9, -12, -6, -6, -8, 4, 1, 3, 0, 8, 0, -3, -4, -7, -4, 10, 8, 6, 5, -1, 4], + [46, 42, -3, -14, -2, -6, 6, -2, -5, -1, -3, -3, 1, -1, 3, 1, 1, 4, -1, 2, 3, 1, -2, 6, 0, -1, -2, 4, -2, -1, 2, 2], + [9, 33, -13, 4, -11, 3, -8, 22, 12, -2, 4, 0, -16, 5, 4, -1, 7, -6, -9, 1, 7, 5, 0, -5, 5, -1, 10, 3, -2, -1, 3, -2], + [9, 30, 6, -3, 6, 1, -7, 5, 11, 14, 7, 1, 0, 2, 2, -1, 8, 7, -6, -13, -10, -2, 1, -6, 10, 7, 6, 5, -2, -5, -1, -16], + [9, 28, -11, -10, 9, -10, 15, 8, 4, 9, -4, -7, 0, -5, 9, 8, -7, 2, -15, -23, 4, -4, 4, 16, -8, -3, 0, -8, 14, 5, -3, 15], + [17, 26, -5, -5, -1, -8, 20, 18, -7, -2, 4, -7, -8, -5, -4, 16, 0, 0, -7, -2, -13, -5, -2, 3, 12, 1, 3, -5, 2, 2, 0, -1], + [11, 37, 7, -23, 6, -1, 15, 13, 4, -9, 7, 5, 3, -3, -5, -8, -2, 3, -5, -1, -8, 7, 2, 13, 1, 3, 0, -3, -1, 2, 0, -2], + [21, 33, 7, 20, 21, -10, 6, -5, -5, -6, -9, 2, 10, 0, 8, -4, 10, 2, -2, -2, 0, -10, -6, -2, 0, -5, 3, -11, 3, -9, -3, 1], + [6, 30, -15, -8, 16, 1, 4, 6, 4, 5, 8, -3, 8, -9, -1, -6, 8, 2, -2, 4, -2, 5, 11, -21, 3, -10, 16, -11, 24, 10, 14, -6], + [15, 36, -3, -9, -20, 12, 0, -7, -18, -4, -8, -9, 9, -7, -3, -1, 2, 7, -5, -8, 6, 2, 2, -1, 7, 1, 1, -3, 3, -4, -8, 1], + [16, 34, 21, 3, -9, 10, 7, 9, -7, 1, -4, -9, -4, -5, -5, 3, 3, -19, 1, 5, 4, -2, -6, -5, -10, -11, -8, -2, 2, -5, -8, -7], + [28, 29, -3, 18, -2, 0, -6, 12, -2, 10, -11, -4, -13, -12, -6, -4, 0, 4, -1, -8, 6, 4, 12, 11, 10, 10, -3, -6, 1, 2, 1, 7], + [3, 8, 22, -8, 3, 36, -8, -1, 9, 6, -13, -14, 8, -1, 1, 2, -2, -8, 0, 3, 1, 2, -1, 5, -1, -8, 0, -2, 2, 2, -1, 1], + [0, 6, 0, 0, 4, 13, -7, -16, -6, 15, -14, -21, -9, -10, -10, -6, -21, 5, 4, 2, 12, 4, 12, 11, -4, -6, -6, -10, -7, -18, 1, 4], + [-1, 3, 10, 1, -1, 15, 4, -7, -16, 3, 0, -22, 10, 2, -3, -2, 13, 5, -8, 16, -5, 4, 0, -11, -10, -22, 0, -4, -17, 5, 2, 1], + [12, 8, -4, -9, 14, 40, -21, 0, 1, -15, -10, -12, 12, 6, -10, 2, 8, 6, -12, -10, -11, 1, 0, -11, 2, 1, 13, 0, 6, 3, 8, 4], + [-10, 3, 5, -4, -3, 3, 0, -9, 2, 8, -22, -23, 17, 8, -17, -3, 14, -8, -4, 1, -8, 3, 0, 5, -1, -3, -2, -4, 1, -10, 0, -2], + [0, -1, 5, -7, 4, 12, -2, 0, -7, 2, -16, -15, 12, 21, -7, -4, 7, -7, -11, -15, -7, -9, -5, -8, 0, -6, 8, -3, -8, 22, -7, -9], + [7, 19, 4, -9, 24, 22, 2, -6, 8, 13, -14, -20, -4, 11, 8, -4, -1, 2, 0, -7, 5, -17, -3, 3, -6, 5, 3, 4, -5, -7, -3, 14], + [-2, 6, 2, 8, -2, 5, -4, -2, -10, 3, -45, -30, -3, -3, -12, -4, -3, -3, -1, 9, -6, -6, 5, -4, 0, 5, -1, -2, -1, 0, -6, -1], + [-3, 14, -16, -10, 10, 0, -2, -40, -9, 12, 2, -19, 15, -4, 4, 3, 3, -4, 7, 1, -4, -5, 0, 4, -1, 0, -9, -2, -4, -1, -2, 0], + [7, 16, 2, -7, 8, 2, 0, 1, 5, 21, -10, -26, 7, 2, -9, -7, -3, -16, 8, 5, 5, -6, 10, 4, -14, -6, 5, 3, -2, -2, -4, 1], + [-9, 14, -1, 3, 3, 11, 1, -5, -3, 13, -16, -18, 20, 6, -5, 0, -3, 2, 8, 4, -19, -9, 12, 0, -8, 2, 2, 1, 6, 13, -7, -11], + [2, 5, 16, -4, 19, 15, 4, 0, -11, 7, -10, -10, -16, 18, -11, -12, -9, -4, 7, -4, -4, -17, 1, 1, -8, -3, -3, 5, -2, -6, -11, -5], + [2, 12, 0, -9, -10, 14, 6, 2, -3, 2, -12, -28, 12, 1, -1, 2, 0, -3, -4, 7, 16, 5, -7, 8, -4, -3, -1, 3, -12, 4, -17, -5], + [-4, 7, 11, 6, 1, 14, -4, -6, 5, 5, -6, -24, 23, -9, -15, 13, -7, -9, -15, 10, -1, 8, -5, 1, 12, 6, 2, 0, 4, -2, 9, -10], + [1, 5, 11, 3, 6, 12, -3, 8, -21, 5, -7, -20, 12, -2, -9, -3, 17, -7, -8, -9, -14, 3, -13, 18, -8, 9, 2, -8, 4, -8, -5, -2], + [-3, -3, -1, 5, -2, 15, 3, 2, 1, -8, 1, -39, -6, 13, -13, 0, -2, -5, -6, -3, 0, -5, -2, 15, -9, 5, -3, -6, -2, 7, 0, -13], + [2, 8, 5, -12, -13, 22, 8, -16, 11, 5, -2, -32, -2, -4, 11, 5, 5, -6, 1, 3, 1, 5, 3, 6, -5, 4, 4, -8, 8, 4, 1, 3], + [13, 9, 5, -4, 9, 18, -11, 2, -1, 15, -10, -19, -2, 14, 0, -10, 1, 1, -18, 3, 2, -6, -8, 20, 7, -8, 16, 9, 9, -13, -3, -2], + [-13, 11, 11, -9, -10, 13, -3, -18, 2, 10, 5, -21, 6, 15, -11, -21, 3, 14, 0, -12, 9, -1, -2, -4, 3, -3, -9, -8, -5, -2, -8, 2], + [3, 3, 11, 4, 0, 13, 1, -8, 10, 13, -6, -26, 2, 12, -3, -5, 12, -2, 1, 8, -7, -17, -19, 5, 10, 7, -3, 2, -3, 0, 5, 0], + [5, 0, 3, -3, -9, 5, -15, -5, -5, 17, -5, -31, 0, 13, 13, 5, -1, -6, -14, 7, -8, 9, -14, -2, -16, -4, -4, -6, 6, -6, -10, 6], + [13, 3, 1, 7, -3, 4, -1, -2, -1, 4, -8, -32, -1, -4, 0, 3, -10, 7, 10, -10, 4, -1, 6, 2, -16, -9, 4, 3, 13, -23, -3, -4], + [4, 11, -4, -9, 4, 11, -12, -12, -12, 6, 1, -28, -3, 14, 18, -2, -12, 7, 15, -3, -5, -7, -3, 2, -6, 4, 4, -2, -5, -3, 2, -13], + [8, 7, -7, 0, 13, 7, -8, -7, 8, 36, -10, -22, 3, 23, -3, -10, -3, 11, 1, -7, 3, 3, -1, -7, -4, 2, 3, 2, 5, 3, -4, -1], + [-1, 1, 13, 1, -6, -1, -6, -9, -18, 17, -5, -37, -1, -1, -6, -4, 1, -6, -15, 2, 17, -9, 0, -3, 0, 4, 0, -5, 0, 4, 1, -5], + [0, 14, 5, 0, -7, 2, -6, 17, -6, -9, 7, -16, -5, 23, -14, -13, 8, -15, 11, 10, -11, -13, -33, -5, -2, 1, 6, 8, 0, -13, -9, 5], + [11, 7, -2, -8, 9, 11, 25, -14, 7, 3, -1, -33, 14, 8, -6, -19, 3, 3, 2, -1, -3, -1, -2, -10, -3, 1, 2, 1, 4, 2, -3, 4], + [-2, 8, 4, -2, 9, 13, -4, -2, -15, -3, 19, -37, 9, 25, -9, 2, -5, -2, -2, -4, 4, 2, 2, 0, 3, 3, 3, 5, -2, -3, -4, -3], + [10, 13, -1, -15, 4, 6, -18, -4, 25, 1, -23, -17, 15, 13, -8, -8, 7, 4, -5, 3, 6, 9, -7, 6, 0, -5, 8, 0, -6, -1, -2, -2], + [1, 3, 9, -5, 27, 15, -9, -31, -1, 23, -2, -9, 1, 8, -1, -7, -2, -8, -4, -4, -2, -1, 3, 5, 0, 0, -1, 1, -7, 7, -3, -3], + [-8, 7, 3, -6, 8, 3, -11, -2, 36, 14, 1, -30, 6, 10, -12, -6, -6, -2, -4, -3, -5, 0, 9, 4, -5, -5, -8, 12, 4, -3, 1, -8], + [-2, 9, 33, 0, 12, -3, -7, -4, -4, -1, 6, -25, 11, -6, -9, -11, -2, -4, -2, 6, -1, -3, -6, 15, -6, 3, 10, -4, 1, 0, 5, 8], + [-22, -21, -9, -19, -5, -7, -12, -15, -8, 9, -19, 14, -7, -4, 5, -8, -2, 7, 1, -3, 4, -4, 6, 11, 2, 6, -3, -5, 2, -2, 0, -3], + [-32, -13, 3, -24, 3, -8, 4, 1, -10, 14, -15, 0, 4, 6, -1, 6, 7, -1, 6, 4, -3, -17, 1, 4, -6, -1, 1, 0, 3, 3, -7, -4], + [-32, -11, 7, -8, -12, 13, -5, -22, -4, 12, -16, 2, 0, 4, 0, 1, 0, 6, -5, -8, 2, 6, 5, 0, -3, -6, 5, 6, 5, 5, 13, -4], + [-44, -33, 6, -4, 2, 0, -9, 10, 3, 4, 7, 0, -1, 7, 5, 1, 1, -3, 1, 6, -1, 0, 2, 3, -4, 0, 0, 1, 0, -1, -2, -1], + [-30, -18, -24, -8, 5, 0, -2, 14, 7, 0, 1, 12, 6, 4, -9, 7, 5, 7, -11, -5, 1, -8, -1, 2, 2, -9, 7, -1, 7, 5, 6, 6], + [-22, -20, -13, -9, 20, -3, 10, -8, 6, -4, 2, -7, 10, 8, 0, -1, 2, -3, 6, -19, 2, 4, 3, 3, -7, 2, -1, -6, 1, 1, 6, -2], + [-27, -8, -1, 3, -1, -11, 24, 4, -1, 1, -8, 8, 5, -11, 15, -3, -15, -1, -1, -13, -1, 1, -5, 5, 2, 3, -9, 0, 4, 3, -7, 6], + [-33, -16, -1, -8, 10, -23, 6, 13, -1, -3, -9, 0, 5, -7, -5, -12, -2, 3, 3, 6, -2, -3, 2, -3, 9, -6, -3, -2, 0, 5, -3, -4], + [-22, -17, 11, -3, 3, 1, -1, -5, 17, 2, -15, -2, 10, -9, 6, 14, -16, -12, 20, -1, -7, 6, -3, -12, 1, 10, -10, -1, 7, -3, -1, 10], + [-28, -13, 1, -3, -1, -1, 0, 3, 3, 5, 1, 10, -10, -3, 7, 2, 4, 19, -1, -1, 10, 5, -8, 1, 11, -15, -4, -3, -5, 4, -13, 3], + [-22, -13, 42, -20, 5, -13, 7, -11, 1, 1, -1, 1, 6, 3, 6, -11, 3, 3, -2, 0, -4, 4, -3, -1, -5, 2, 0, 0, -9, -1, 4, 4], + [-26, -15, -2, -6, -4, -2, 16, 8, 21, 8, 1, -3, -10, 7, -8, -12, -5, 12, -9, 3, -2, -3, 18, 1, -12, -15, -4, 5, -3, 0, 12, 7], + [-26, -16, 5, 6, 14, -3, 15, 6, 1, -7, -13, 16, -15, 5, 11, -2, 9, -7, -4, -2, 0, 0, -2, 7, -8, -6, -5, 2, 7, -3, 2, 12], + [-31, -17, -8, -30, 4, 14, 6, -6, 6, -11, 0, 3, -4, 0, 0, -4, 0, -4, 1, 4, 3, 4, 0, -5, 3, 2, 2, 0, 2, 1, 3, 5], + [-61, -10, 4, 10, 4, 7, 0, -3, 0, 1, 0, -3, 0, 1, 0, -2, -1, 1, 2, -2, 4, -3, 1, 1, -1, 1, -2, -4, -4, 4, 0, 0], + [-28, -13, -8, -4, 3, -3, 2, 1, 11, 14, 3, 9, 1, 13, 3, 5, -3, -2, -2, -12, -14, -9, -11, -15, -12, -5, -4, -12, 3, -3, 0, -5], + [-41, 0, 12, -24, 13, 4, 5, 16, -5, -4, 0, 0, 13, -4, 1, -9, 9, -6, -1, 6, -2, 5, 2, 9, 6, -9, -8, 8, -2, -3, -6, -4], + [-26, -19, -2, -15, 4, -14, 6, 0, 26, 20, 8, 9, 9, 3, -4, -5, -8, 1, 0, -1, 5, 9, 3, 4, 4, 7, 1, 3, -2, -2, -10, 0], + [-29, -18, 9, -4, 1, -5, -14, -12, 5, -10, -5, 4, -5, 0, -1, -1, 4, -5, 7, -16, -11, 2, 7, -15, 2, -4, 6, -4, -6, 7, -3, 7], + [-27, -16, 9, -14, 3, -8, 9, 0, 7, -4, -3, -7, 0, -10, -1, 2, 1, -2, 15, -10, 14, 7, 6, 17, 3, -4, 3, -10, 8, -8, 3, 11], + [-21, -20, -8, -8, 4, 5, -3, -2, 0, -5, 14, -10, 11, -4, 13, 0, 5, -11, 19, -18, 18, 3, -5, -3, -4, -8, 11, -10, 10, 3, 4, -9], + [-35, -15, 13, -12, 4, 0, -2, -4, -12, -3, -8, -24, -7, 1, 7, 8, -3, 0, -2, -1, 3, -2, -2, -6, 8, 1, 0, 1, -6, -1, 2, -6], + [-19, -14, 13, -10, 9, -1, 1, 3, -12, 5, -16, 7, 13, 9, 4, -4, 6, -5, 4, 9, -3, 17, -4, 12, -11, -6, -5, -6, 13, 2, 7, -9], + [-34, -8, -4, 1, 2, -1, 3, 6, -20, -11, 8, -1, 4, 2, -9, 4, -4, -5, 16, 10, -4, 14, -13, 1, -6, 0, 2, -10, 0, -3, -3, 7], + [-36, -10, -8, -3, 2, -2, 14, -4, -1, -7, -4, 10, -1, -3, 15, -11, 0, 2, 3, -1, 4, 0, 8, -1, 0, 18, -11, -5, 15, -5, 13, -12], + [-22, -13, 14, -20, 15, 25, 16, 10, 8, -2, -10, -5, -1, -8, 11, 8, -1, -2, -4, 1, 2, -1, -7, 0, 0, 0, -3, 0, 2, -1, 0, 2], + [-31, -22, 7, 6, -2, 5, -20, 14, -6, 7, 0, 14, 3, -7, 3, -6, -2, 1, -3, -5, 1, -10, 1, -24, 6, -2, 3, -7, 1, -7, 8, 7], + [-25, -20, -3, -9, 10, 6, 12, 7, 5, 4, -3, 6, -1, -5, -6, -8, 3, 5, 6, 5, -10, 10, -4, -15, -15, -2, -9, 2, 18, 1, 8, 12], + [-24, -19, -2, -4, -7, 11, 6, 9, 16, 2, -7, 18, 6, -7, 6, 6, -2, -9, 3, 12, -2, 3, -1, 6, 7, 8, 0, 8, -11, 8, 4, 2], + [-26, -20, -12, -12, -2, -3, 1, -5, -1, -2, 0, 3, 7, 9, -2, 2, 9, 22, 13, 4, -4, -1, -2, -14, 5, 15, -8, -5, -7, -11, -14, -6], + [-21, -18, -1, -4, 0, 3, 7, -2, 10, 8, -8, -1, 15, 1, -9, 3, 1, 3, -5, -2, 2, 4, 0, -1, 10, 2, -19, -8, 8, 30, -7, 8], + [-25, -6, 26, 4, -8, 4, -2, 21, 5, -4, -16, 5, 13, 4, -10, -1, -6, -2, 2, -10, -13, 1, 3, -3, -6, -8, 2, 11, 1, -7, 0, 5], + [0, -1, -2, 19, -12, -48, -6, 11, 8, -2, -4, -2, -7, 5, -3, 2, -2, -1, -1, -7, 0, -3, -3, -4, -4, 4, 1, 3, -3, -1, -2, -5], + [-11, -8, -28, 18, 16, -24, -8, 19, 4, 8, -12, 9, -4, -2, 4, -7, 6, 2, 3, 3, -4, 0, 1, -6, -4, -2, 2, 6, 0, -3, 1, -16], + [-9, -5, -26, 7, -3, -37, -16, -2, 2, -7, 4, -13, 0, -4, -6, -5, -6, -4, 0, 3, 4, -3, -4, -4, 4, -3, 9, -4, -2, 2, 7, -4], + [2, 9, -18, 7, 29, -24, -1, 7, 14, 10, 3, -3, -2, -5, 6, -10, -6, -3, -8, 0, 5, 1, 4, 3, -12, 2, 6, 1, 3, 4, 1, -3], + [-20, 2, 8, 20, -9, -24, -4, 18, 3, 11, -1, -11, 6, 9, -1, -3, 1, -1, -15, 3, 15, 9, 3, 2, -13, 2, -8, 8, 1, -1, 1, -8], + [-12, 5, -11, 6, 19, -26, -17, -6, 4, 14, 6, -8, 9, 5, -6, -5, 2, -1, 20, 1, -11, -10, -18, 20, -7, 0, -3, 4, 2, 0, 10, 4], + [-15, 1, -2, 13, -8, -21, -22, 4, 4, 3, 3, -7, -31, 4, -10, -14, 0, 8, 4, 5, 8, 11, 2, -8, 6, 7, 0, -2, 6, 8, 8, 7], + [-13, -10, -9, 12, 19, -16, -3, -2, 9, 2, 11, -29, -1, 9, 4, -3, 1, -10, -10, 16, 1, 7, -7, -6, -4, -1, -5, 3, 6, 0, 3, 1], + [-17, -1, -5, 19, 12, -9, -21, -5, 2, 12, -7, -7, -3, 8, 7, -2, 6, -9, -9, 1, -4, 1, 1, 3, -14, 2, -8, 0, 10, 1, -12, -6], + [-13, -5, 8, 15, 0, -20, -2, 20, 8, -8, 8, -19, 12, 10, 2, -11, 0, 12, 1, -11, 0, -11, -15, 5, -11, 2, 4, -4, -11, 5, -4, -5], + [3, -11, -7, 8, 0, -17, -26, 15, 19, -7, 10, -9, -5, -5, 14, -25, 0, -8, 2, -9, -3, 9, 1, -6, 4, -4, 3, -9, -1, 6, 2, 2], + [-12, 5, 5, 9, 14, -18, -19, 4, 2, 16, 14, -21, -15, -9, -1, 16, 12, -11, -10, -5, -7, 4, 15, -8, -5, -1, 1, 14, 13, -7, -1, -4], + [-10, -5, -1, 8, 7, -23, -10, 14, 6, 11, 10, -16, -3, 16, 6, 0, 0, 9, 6, -2, -7, 1, 22, 5, 3, -8, 0, 3, -2, -10, 3, 0], + [-2, -14, 2, 16, 15, -17, -17, 6, 19, 4, -10, -15, -1, 15, 11, -14, -8, 5, 8, 8, -2, -8, -11, 10, 10, -8, -14, 2, 13, 4, -2, -12], + [-10, 3, 6, 4, 19, -23, -19, 1, 4, -9, -30, 3, -6, 18, 0, 2, 0, -11, 0, 3, 7, -2, 8, 5, 2, -3, 6, -9, 1, -4, 7, -6], + [9, 5, -2, 21, 20, -33, -13, 7, -10, 8, 8, -15, -6, -4, 1, 5, 3, 7, -2, -9, -1, 4, -6, 1, 0, 9, -1, -5, 2, 1, -3, 3], + [-9, -3, 3, 15, -3, -30, -7, -7, -25, 6, 2, -6, 1, 19, 1, -12, 1, -8, -13, 9, 13, 1, 8, 2, 5, 15, -2, 3, -9, 0, -4, 4], + [-6, -12, -17, 25, 22, -13, -10, 9, 2, 11, -7, -16, 4, 6, 1, 0, 0, 18, -4, -5, 4, -2, -1, -5, 0, -4, 6, 1, 6, -1, 7, 0], + [-1, 0, -10, 8, 8, -27, 0, -2, 29, 16, -2, -4, 9, -1, 2, 0, 6, 10, 6, 4, 2, -7, 9, -18, 3, 3, 3, -10, 17, 10, 9, -6], + [-3, -12, -6, 11, 20, -32, 5, 21, 3, -4, -9, 2, -10, 1, 7, -4, 5, 0, 0, -1, -8, -9, -7, 4, -10, 5, 0, 2, -5, 4, 9, 1], + [-5, -1, -5, 1, 2, -19, -13, 1, 6, 12, 2, -16, -17, 11, 10, 13, 16, -12, -11, 3, -6, 0, 6, 4, -3, 1, 8, 2, 5, -11, 3, -14], + [-19, 5, 10, 11, 2, -23, -9, 16, -2, 7, 0, -11, -7, 10, 6, -7, 26, -15, -4, 8, 6, -4, 7, -9, -15, 1, 8, -4, 4, 2, -12, 16], + [-11, 1, 11, -4, 1, -31, -13, -1, 8, 5, 4, -2, 0, 13, 7, -17, 7, -10, -6, 1, 4, -1, 2, -9, -4, 9, 3, 3, -4, -5, 3, 4], + [-3, 1, 10, -1, 0, -15, -22, 4, 40, -11, -4, -3, -14, 9, 11, -1, 9, -1, -6, 6, 3, -6, 0, 0, -12, 7, -2, 0, 9, 3, 1, 3], + [-1, -1, -1, 14, 8, -24, -14, -8, 5, 8, 5, -12, -17, 8, 2, 7, 10, -8, 0, 4, -6, -6, -10, 8, 4, -12, 3, -9, -12, 5, 4, -3], + [-5, 1, -11, 8, 9, -24, 0, 2, 2, 14, -12, -13, 1, 6, 7, 0, 7, -6, 9, 26, 11, -14, 8, 10, 1, 9, 0, 11, -2, 6, 2, -10], + [-13, 1, 4, 34, 19, -17, -15, 0, 3, -2, -7, -1, 0, -3, -3, -1, 1, -1, -10, 8, 5, 0, -8, 4, -17, 9, -2, 0, 0, 6, 2, -3], + [-6, -4, 1, 2, 2, -14, -29, 0, 9, 34, -3, -5, -14, 6, -10, -9, -5, -1, 0, 3, 3, 0, 1, -1, -2, -1, -1, -3, -3, -4, 3, -3], + [-4, 6, 3, 14, 14, -8, -29, 31, 11, 14, -4, -5, -6, 10, 6, -9, -1, -11, -7, 1, 7, 4, 1, -6, 4, 0, 10, -7, -5, -1, 2, 4], + [-4, -4, -2, 14, 6, -32, -6, -14, 14, -5, -11, 10, -18, -4, 6, -8, 9, 5, -4, 1, -4, 5, -2, -9, 3, 5, 2, -10, -6, -17, 3, 17], + [-16, 9, 21, 19, 4, -20, -17, 14, 9, 15, -6, -17, -1, 1, 6, -3, 1, 1, 8, -3, -6, 6, 9, 4, 9, -9, -5, 1, -1, 0, -1, 2], + [-7, -5, 3, 19, 1, -20, -9, 14, 21, -7, -18, -9, 26, -7, -17, -7, 12, 6, 0, -9, -6, 14, 9, -9, -8, 4, 15, -7, -9, -1, 9, 1], + [-20, 30, -6, 11, 24, -4, 0, -6, -2, 8, -4, 12, -8, -17, 0, 5, -4, 1, -1, 3, -3, 5, 3, 3, 7, -2, -3, -2, 4, 0, 0, -1], + [-35, 17, 6, 1, -9, -1, -16, 3, -20, -13, 8, 7, -4, -7, -4, -20, 7, 12, -5, 5, -5, -11, 12, -1, 15, -9, -6, 16, -4, -9, -13, 4], + [-21, 36, -19, 9, 0, -7, -8, 9, -4, -3, 3, 0, 7, -8, -2, -2, -11, 13, -1, 5, -3, 7, 2, 3, -1, -2, -5, 1, -1, -2, -5, -3], + [-12, 33, -4, 1, -12, -9, 0, -13, -1, 2, -8, 4, -10, 6, -16, -7, -1, -4, -10, 15, -1, 0, -5, -8, 5, 5, -3, 0, 2, -7, 1, -7], + [-14, 32, 5, -7, -15, 3, -5, 8, 14, 5, 9, 13, 3, 18, -3, 7, 4, -10, -10, 10, -1, 2, 0, -2, -11, 5, -3, -4, 2, 2, 7, 4], + [-14, 34, 1, 20, -1, -12, 0, -3, -7, -4, 7, 18, 9, -3, 14, -7, -9, -20, -7, -4, -13, 12, 1, 12, 5, -6, 2, -4, 0, -15, 1, 3], + [-21, 23, 7, -8, 3, -13, -3, 0, -6, -2, -7, 6, -12, 9, -6, -2, -2, -4, -1, 6, 9, 5, -9, 15, 0, 8, -8, 7, 6, -15, 3, -5], + [-27, 32, -1, -4, -2, 4, -10, 12, -3, 8, 13, 7, 0, -15, 4, -2, 3, 5, 7, -4, 9, -12, -1, -2, -1, -4, 0, -4, 2, -5, 6, -6], + [-17, 29, 15, 0, -1, -4, -10, 13, 12, -1, -8, -10, -10, 4, 7, -2, 6, -5, -13, 19, 6, 1, -7, 2, -9, -2, 12, -4, -8, -3, 2, 4], + [-38, 27, 16, -15, -6, 3, -7, -4, 0, -1, 6, -2, -3, -6, 6, -6, -3, 0, 2, 0, -4, 6, 1, -1, 0, 4, -1, 3, 4, 1, -2, 5], + [-33, 40, -4, 2, 1, 0, 0, -10, -14, 0, -7, 4, -1, 3, -2, 5, 7, 6, -1, 4, 1, 3, 1, -7, 1, -4, 5, 7, 0, 4, 3, -4], + [-20, 25, 12, -4, 16, -4, 2, 2, -14, -2, -3, 29, -1, 1, 3, 1, 9, -5, 2, -8, -3, 1, -7, -2, -7, 1, 0, 4, 16, -2, -1, -1], + [-10, 30, 17, 3, -5, -2, 0, -5, -22, 4, 5, 5, -3, -18, -6, 10, -5, -7, 2, 8, 7, -7, -11, -2, 0, -3, 3, 2, 11, -4, 4, -4], + [-11, 30, 11, 4, -3, -8, 1, -2, 4, 18, 3, 1, -1, 0, -8, -4, -3, 10, 13, 14, 5, -5, 1, 1, -10, 2, 15, 4, 9, -1, -5, -3], + [-17, 32, 18, -18, -3, -5, 6, 10, 1, -15, -5, 9, 8, -12, -10, -6, 11, 9, -5, -8, -7, 10, 5, -10, -14, -4, -3, 1, 9, -11, 2, 1], + [-13, 28, -11, -1, 2, -16, -2, 7, -24, 0, 3, 6, 3, -1, -8, -7, -12, 2, 2, -20, 10, 4, 0, -13, -2, -2, 1, 8, -14, 0, 4, 1], + [-14, 23, 12, 8, 8, -26, 2, -4, -14, 13, -14, 15, 3, -9, -1, -13, -10, -2, -10, 6, -16, 12, 8, 0, 9, -10, -7, -4, -4, 7, -8, 8], + [-20, 45, 10, -14, 4, 16, 8, -9, 1, -8, 10, 5, -7, -2, 2, -5, -1, 0, -5, 4, -6, -2, 4, 1, 3, 4, -4, 2, -2, -2, 5, 1], + [-20, 26, -4, 1, 7, 4, -8, 1, -5, -13, 2, 13, -7, -3, 6, -6, 22, 0, 5, 11, -4, -11, 8, -9, 2, -2, -4, -2, 2, -13, -4, -8], + [-28, 18, 17, 3, -8, -23, -16, -6, 5, -10, 14, 10, 5, -1, -8, 4, -2, 13, -3, -2, 3, 4, 3, -2, -3, -4, 0, 1, 3, 4, 0, 4], + [-12, 32, -6, -16, 18, 12, -16, 0, 7, 13, -4, 5, -8, -1, -3, 4, 6, -2, -1, -13, 4, -1, 3, 12, -3, -10, 1, 6, 8, -11, -2, 4], + [-18, 26, 2, 5, 0, -9, -17, 14, 5, 1, 7, -3, -8, -3, 11, 7, -5, -12, -8, 7, 0, -7, 2, -12, -9, 13, -11, 9, 6, -11, -5, 11], + [-24, 22, -15, -9, 8, 1, -7, -12, -9, 3, 11, 15, 14, -11, 12, -15, -5, 7, -2, 0, -8, 3, 3, -1, 2, 11, -11, 14, -6, 13, 1, -6], + [-20, 28, 18, -4, -6, -5, 12, 14, 2, 10, -13, -6, -8, -6, -13, -1, -26, 22, -3, -14, 6, 0, 10, -15, -13, -9, 6, -7, 1, -5, -4, -1], + [-19, 26, -8, -3, -14, -6, -9, -4, -8, 15, -8, 3, -12, -4, -2, -7, -5, 3, 13, -3, -4, -25, 4, -1, 5, -12, -1, -13, 5, 2, 0, 6], + [-18, 43, 14, -8, 1, -23, -2, -2, 1, 3, -7, 0, 0, 8, -1, -3, -5, 1, 5, 2, 0, -2, -2, -2, 1, -1, -1, -7, 0, 3, -3, 9], + [-11, 30, 10, -14, 3, 1, 10, -11, 1, -7, -4, 14, 2, 1, -9, 1, -11, -2, -7, 5, -11, 1, 3, 14, 1, -16, -8, 3, -5, 7, -4, 4], + [-18, 24, 6, 3, 8, 7, -22, -7, -7, 3, -8, 4, 23, 9, 3, -1, 3, 6, 7, -1, -7, 6, 4, 1, -3, 1, -6, -1, 2, -7, 3, 3], + [-15, 38, -7, -1, -11, 2, -17, -24, 24, 8, 7, -4, -5, 2, 2, -7, 1, 4, 0, -9, 5, 0, -1, 1, -1, -5, -6, 3, 0, 7, 8, -3], + [-14, 22, 1, -5, 9, -12, -9, -5, -6, 5, 7, 8, -1, -4, -9, -3, -33, -16, -9, -1, 12, -11, 17, -7, -3, -1, -7, 3, 2, -3, 16, -4], + [-14, 20, 6, 4, -10, -4, -4, -4, 1, -7, 2, 6, 8, -12, 4, 1, -1, 12, 10, 3, -14, -10, -3, 18, -2, 33, -5, -17, 17, -5, 9, 7], + [-12, 23, 13, 0, -11, -8, -11, 12, -5, -9, -16, 11, 6, 4, 12, -5, 5, -13, 7, -12, -3, 1, 2, 12, 1, -4, -1, 5, 4, 11, -12, -3], + [15, 2, 14, 7, 1, 2, 1, 12, 10, 23, 4, 6, -20, -10, 4, 26, -6, 13, 4, 3, 2, -11, 5, -7, -10, 4, 9, 1, 10, -4, 11, 4], + [17, 15, 31, 17, 18, 16, 11, 24, 2, 4, 2, 3, -8, -3, 7, -3, -5, -7, -2, -6, -4, -5, -4, -1, -4, -2, -5, -6, 2, -1, 4, -2], + [16, 8, 15, 14, 3, 7, 21, 9, 8, 15, 21, 6, 8, 12, 5, -5, 7, -3, 10, 2, -3, 8, 6, 0, 5, 5, 6, -3, 2, 4, 0, -5], + [5, -4, 6, 12, 6, 13, 24, 17, -5, 17, -1, -6, -7, -10, -8, -18, 3, -2, 2, 7, -15, -11, 12, -3, -2, -2, -4, -7, 2, 0, 5, 5], + [10, -6, 8, 11, 12, 20, 22, -11, -3, 15, -3, 15, -2, -2, 0, 2, 5, -8, 4, -5, -9, -4, -1, 2, -1, -3, 1, 3, 13, -1, 9, 7], + [-5, 8, 5, 11, 14, -5, 14, -9, 2, 35, 8, 15, 1, -2, 2, -2, 4, -9, -3, -14, -12, -2, -2, -4, -2, -8, -3, 1, -6, 3, 10, 0], + [16, 0, -6, 15, -3, 4, 4, 3, 3, 20, 5, -4, 10, 9, -9, -3, -10, -2, -7, 11, -11, -10, 17, -1, 3, -15, 2, 9, -15, -10, 16, 10], + [14, 4, -7, 19, 3, 0, 19, 8, 16, 34, -9, 6, -13, -1, 6, 5, -1, -2, 4, 3, 2, 1, 1, -1, 0, -7, 2, -1, 1, 0, 6, -1], + [1, 6, 9, 13, 9, 10, 15, 16, 10, 18, 13, 17, 3, -1, -7, 2, -15, -11, -10, -4, -13, -6, -17, -13, -6, -14, 1, -10, 6, 4, -1, -1], + [13, 1, 7, 10, 14, 13, -7, 5, 5, 28, 14, 14, -2, 2, 3, -3, -13, -4, 10, -9, 19, -4, -3, 4, -5, -5, 0, 5, -5, 0, 3, -4], + [1, 0, 6, 22, 9, 18, 18, -3, 5, 10, 12, -2, 1, -3, -8, -12, 9, -10, -7, 1, -1, 19, 0, 2, -8, -11, -10, 9, 6, 11, 0, 3], + [10, 11, 19, 44, 0, 14, 1, -7, 6, 22, 2, -1, 9, 2, 0, -4, 4, 0, -6, -6, 3, 0, 0, -2, 2, -5, 1, -2, 0, 1, 1, 1], + [5, 7, 0, 32, 30, 26, 5, 4, -7, -3, 15, -6, 3, -10, 7, 6, -8, -7, 2, -13, -5, -1, -3, 7, 3, -2, -8, 0, 6, 4, 5, 0], + [9, 8, -2, 4, 2, 11, 4, 29, -5, 14, 8, -5, -14, 8, 0, 9, 8, -10, 5, -15, -6, -9, 9, -1, 18, -16, 9, -21, -3, -13, -2, 8], + [25, 7, -9, 23, 20, 18, 6, 16, -9, 8, 8, -5, 11, 13, -8, 7, 4, 10, -2, -1, -7, -9, -7, -9, -4, 1, 1, -5, -10, 8, 4, -5], + [9, 2, 16, 14, -5, 14, 1, 0, -21, 17, -1, 9, 12, -3, -3, 4, -4, 14, 10, 3, 0, -10, 7, 4, 4, -11, 2, 4, -1, -3, 9, -1], + [17, 8, 11, 26, 15, -3, 14, -1, 12, 9, 10, -8, 8, -18, -11, -3, -14, -7, 7, -3, -3, -4, 1, -7, -3, 2, -3, 16, 10, 0, 9, 6], + [9, 8, 3, 8, 18, 14, 11, 1, 10, 6, 1, -4, -16, -2, 14, -2, 1, 8, 12, 14, 3, -3, 8, 8, 12, -15, 3, -3, 3, -2, 14, 10], + [22, -3, -11, 13, -7, 11, 4, 11, 3, 14, 0, -6, -2, -9, 4, 2, -2, 0, -5, -27, -10, 3, -1, 5, 8, -24, -3, -11, -3, 2, 11, -1], + [19, 2, 8, 36, 5, -6, 3, 15, -3, -4, -5, 14, -10, 1, -12, -10, -3, -4, 3, -2, 1, -8, 4, 3, 5, -3, 0, 4, 8, -2, 8, 4], + [8, 14, 15, 9, -4, 10, 5, 11, 9, 10, 8, 9, -15, 15, 6, -8, -10, -13, 5, -8, -20, -13, -6, -11, -1, -3, -6, -4, -1, 0, 13, 15], + [-2, -1, 9, 12, 2, 2, 13, 3, -23, 33, 15, 2, -4, -1, 3, 8, 8, 6, 6, -7, 8, 6, 9, -1, 3, -8, 0, -4, 1, -8, 11, -1], + [6, 5, -6, 16, 2, -3, 31, 21, -9, 12, 0, -1, -4, 1, -12, 3, -13, -18, 2, -11, -9, 2, -8, -6, 11, -3, -1, 0, -1, 0, 13, 5], + [5, -1, 2, 0, 25, 5, 10, 16, -5, 21, 14, 12, 13, 2, -5, 5, 5, -3, -2, -14, 0, -12, 7, 11, -1, -7, 19, -1, -1, -1, 8, -1], + [10, 7, 3, 11, 0, 8, 22, 3, 3, 19, -4, 12, 15, 9, 5, 15, 2, 1, 2, -10, -10, 0, 2, -1, 0, 1, -12, -1, 21, 16, 9, -7], + [11, -4, -5, 24, -7, 11, 20, 11, -15, 18, 5, -13, -15, 0, -5, 9, 1, 0, -1, -9, 4, -8, 6, -8, 1, -2, -7, 20, 9, 3, 9, 3], + [20, 0, -12, -6, 9, 31, 9, 12, 8, 27, 15, 7, -16, 5, -3, -7, -1, -9, -2, -7, -3, 4, -8, -3, 3, -6, -2, -2, -3, -6, -1, 2], + [6, -6, 48, 8, -3, 19, 12, 11, -7, 2, 3, 0, -1, 1, 8, -4, 4, -6, 0, -4, -4, -3, 3, 6, 3, -13, -8, 5, -3, -7, 8, 5], + [7, -2, 6, 11, 12, 2, 14, 4, -5, 12, 2, 9, 4, 2, 0, -1, 2, 0, -15, -9, -16, -2, 8, -17, -5, -22, -19, -5, -1, -10, 1, -2], + [11, -9, 3, 12, 6, 6, 1, 17, -6, 19, 14, 7, -7, -1, -1, -9, 9, -11, -17, 0, -6, 16, 0, 1, 9, -24, 3, 3, -9, -3, 3, -2], + [9, 0, 1, 8, 1, 7, 2, -5, -3, 8, -1, 7, 2, 6, -3, -6, 5, -2, 6, -2, -4, -3, 0, -3, 13, -50, 1, -2, 2, 4, 4, 3], + [7, 0, 26, 21, -4, 2, 17, 8, 7, 11, -7, 1, -1, -15, -1, -15, -11, -4, -17, -4, 1, -7, 3, 6, 3, -9, 2, 3, 6, 10, 6, 12], + [1, -2, 2, -1, -10, -4, 6, -3, -5, -2, -8, 2, 2, 2, 8, 0, 1, 1, 6, 0, 11, 13, 3, 4, 0, -12, 11, -5, 19, 20, 2, 5], + [5, 3, -13, -2, 1, -12, 11, -7, -12, 7, 10, 0, 7, 0, -2, 4, -6, -9, -11, -12, -23, 12, 10, -3, 0, 6, 19, -1, 24, 18, 9, 12], + [6, -3, 2, 5, 2, 2, -2, -5, -8, -11, -4, 3, -8, -4, 5, -3, -16, -4, 3, -12, -4, 3, 32, 7, 2, 8, 32, -18, -1, 12, 1, 7], + [0, -8, -1, 0, -8, 7, -8, -1, -1, 4, -12, -1, 3, 0, 1, -18, 8, 8, -14, -10, -11, 19, 9, 5, -7, 6, 8, -4, 26, 12, -1, 6], + [3, 5, -14, 7, 14, 8, 20, -13, -16, -10, -2, 17, -7, 4, -8, -9, 14, -5, 3, -4, -12, 7, 14, -10, -19, -20, 35, 8, 13, 14, -2, 9], + [-2, -4, -1, 1, -3, 0, -1, 1, 2, 2, 6, 0, 0, 4, 5, -2, 3, 3, 3, -2, -7, -3, -3, -1, 6, -2, 29, 22, 13, 34, 0, 14], + [-3, -9, 3, 1, 5, -4, 2, 0, 7, -9, 0, 2, -5, -3, 0, 6, -1, -1, -1, 2, 2, 4, 8, 7, 20, -6, 7, 16, 33, 20, 6, -1], + [-11, 1, -3, -3, -11, 3, -9, -25, -1, -16, 4, -8, 15, 1, -2, 7, 8, 23, 2, 18, -13, 16, 3, -7, 6, 3, 16, -8, 12, 16, 3, 4], + [0, 5, 5, -5, 1, -1, 2, -3, -2, 1, -13, 2, 2, 10, 6, 7, 18, 18, 7, 9, 8, 9, 21, 14, 7, 12, 15, 14, 15, 12, 11, 5], + [1, -5, 11, -2, 17, 8, 3, 0, -1, 6, 11, -7, 6, 6, 7, 5, -15, 14, 1, 11, 4, 10, 12, 1, 2, 4, 30, 1, 11, 1, 6, 13], + [2, 4, 3, -7, 5, 8, -11, 7, -5, 9, -10, 6, 8, -10, -3, 10, 1, -29, -4, -26, 5, -8, 13, 4, 3, 6, 35, 1, 3, 6, 3, 0], + [-2, 1, 0, 0, -1, -3, -7, -3, -9, -3, -1, -6, 3, 4, 4, 0, 5, -1, -2, -2, -1, -4, -10, 8, 0, -6, 10, -4, 46, 12, 2, 28], + [4, -1, 4, 1, 0, 4, -2, -2, -2, -1, 2, -4, 1, 5, 0, -3, 1, 1, -2, 0, 1, -2, -1, -1, 3, -6, 35, -11, 13, 53, -3, -1], + [-5, -2, 0, -13, -16, 5, -12, -11, 1, -30, 3, -18, -24, -8, -5, -19, 1, -3, -8, 7, -7, -8, 15, -19, 4, 10, 30, 24, 6, 1, -9, 10], + [-4, 8, -7, -4, -6, 12, -1, -9, -4, 2, -9, 3, 2, -2, 4, 2, 22, 9, 4, -5, 0, 5, -2, -9, -3, 1, 18, -12, 18, 16, 4, 16], + [-5, -8, -3, -5, -3, 6, -7, -3, -2, -5, -3, 1, 2, 2, 4, -6, 10, 3, 12, -3, 20, 0, 27, -4, 16, 5, 18, -3, 23, 4, 12, 11], + [0, 1, 0, 1, -2, 1, 2, 1, -1, 0, -2, 2, -2, -4, 1, -2, -2, -1, -5, -2, 0, 0, -2, 2, 9, 7, 63, 5, 12, -1, 1, 0], + [4, -3, -7, -5, -11, -5, -12, -10, -10, -12, -15, -12, -14, -14, 1, 1, 10, -10, 16, 6, 2, 9, 11, 9, 9, 8, 12, -1, 13, 12, 6, 3], + [7, -3, -2, 4, 6, -8, 2, -3, -12, -5, -9, -8, -10, 15, -2, -4, 8, 9, 7, -13, -18, 34, -5, 7, 12, 22, 16, -11, 13, 25, -15, -11], + [-3, -2, 0, -4, 1, 0, -3, -13, -7, 13, 12, -7, -10, 13, 19, 6, 16, 15, -12, -15, -3, 34, 1, 5, 1, -9, 11, 21, 8, 17, -5, -6], + [3, -5, 0, -4, 0, 4, -11, 4, -7, -3, -1, -8, 3, -2, 2, 1, 11, 5, 6, 14, -3, 2, -4, -7, 0, 31, 15, -2, 24, 11, 5, 4], + [-1, -4, -9, 5, -8, -18, -4, -9, -20, -18, 7, -14, -16, 3, 8, -3, 29, 11, -13, -13, 7, 1, 17, 6, 6, 21, 11, 1, 14, -8, 2, 5], + [-3, 8, -10, -6, 12, 2, 1, 3, 3, 3, 3, -6, -8, -14, 15, -5, 16, 4, 16, 0, 7, -1, 0, 16, 2, 1, 22, 4, 19, 13, -11, 1], + [2, -3, 10, 20, -4, -1, -8, 5, -8, -9, -6, -2, -4, -7, 8, -10, 0, 8, -6, 1, -8, 14, 13, 5, 17, -6, 26, -1, 7, -1, 0, 12], + [-4, -7, -31, -2, -7, -1, 5, -5, -5, -12, 4, -7, -6, 3, 15, -2, 5, -2, 7, -1, 10, 7, 8, -1, 14, 20, 14, 9, 16, 16, 8, 24], + [-7, 0, -3, -6, 1, 3, -13, -6, -4, -4, -5, -9, -1, -10, -4, -8, 2, 0, -1, 1, 24, 24, 21, 31, 5, 2, 11, 12, 7, 4, 3, 6], + [-3, -5, 6, -4, -3, -1, 2, -1, -2, 1, 0, -8, -1, 2, 0, -4, 6, 22, -1, -5, 8, 12, -1, -2, 28, 27, 20, -27, 14, 1, 2, -3], + [1, -5, -2, -2, 6, -2, 9, 1, -2, -5, 3, 4, 11, 5, 2, 8, -3, -1, 1, -2, -3, -5, 5, 8, 49, 12, 8, -3, 9, 20, 12, 17], + [-6, 0, 1, 7, 0, 9, -2, -4, 8, 0, -2, -10, 0, 7, 21, -1, 0, 1, 17, -7, -5, 2, 4, 16, -2, 17, 14, -20, 15, 14, 4, 15], + [0, 3, -4, 9, -4, 0, 6, 4, -6, -6, -5, -7, 2, -9, -10, -2, -5, 0, -3, -21, 9, 14, -11, 13, 29, 2, 25, 4, 22, -1, 2, -3], + [2, 12, -11, 2, 16, 9, -4, 7, 1, -10, -15, 11, -4, 3, -2, 4, 4, -5, -10, 1, 4, 19, -15, 6, -4, -2, 30, -7, 11, 21, -12, 5], + [-2, -3, -2, 4, -1, -5, -3, -7, -5, 1, 0, -6, 1, -6, 7, 0, 8, -7, -3, -2, 2, 14, 2, -3, -26, -1, 26, 22, 32, 1, -2, 6], + [1, -38, -1, -20, -2, -3, -6, -4, 2, 2, 7, 0, 3, 5, 3, 10, 6, 1, -3, -5, 7, 5, -5, -4, 8, 3, 1, -14, -1, -9, -5, -4], + [-5, -26, -7, -19, -10, -5, -11, 5, -11, -25, -8, -14, -9, -16, -8, -6, -17, -14, -1, -1, 6, 2, 2, 2, 3, 0, 2, 8, -8, 3, 0, -3], + [17, -49, -3, -23, -1, 11, 7, 3, 4, -4, 0, 0, -1, 4, 2, 4, -2, -4, 2, -2, -1, -2, 2, 0, 0, -1, 0, 0, 1, 2, 0, 0], + [4, -34, -6, -9, 1, 21, -7, 3, -2, -1, -3, 18, 2, -16, 7, -3, 8, 7, -5, 7, 2, 4, 8, -6, -7, -2, -5, -1, 4, 1, 2, -4], + [5, -29, 13, -2, -14, 3, 1, 18, -15, 4, -8, 8, -10, 8, 2, 1, -8, 15, 3, -10, -4, -4, -2, 0, -3, -4, 2, -3, -4, -3, 12, -6], + [13, -20, 3, -18, -17, 4, -14, 13, 28, 11, -8, -6, 16, 6, 0, 10, 3, 4, -9, 13, 5, -7, 12, -5, 0, -7, 5, 1, 3, 3, 2, 1], + [3, -27, -5, -11, -21, -11, -12, 0, -5, 7, -22, 1, 3, 5, 0, -5, 8, 7, 1, -5, -7, 2, -5, 4, 1, 3, -8, -2, 0, 4, -2, 6], + [31, -45, 0, -1, -12, 1, 2, -6, 4, 3, -1, 3, 3, 0, 5, 3, -5, 12, 4, 6, 2, 1, -2, 1, 3, 2, 5, 2, 2, 2, 3, -1], + [9, -45, 6, 5, -1, -17, -2, 18, -3, 2, 0, 1, 0, -1, 10, 8, -7, -2, -5, -8, 6, -1, 0, 4, 6, -3, 12, -1, -2, 0, 5, -7], + [3, -26, -2, -12, -12, 2, -10, 16, -3, 12, 4, 5, 11, 8, -16, -17, -2, -3, -3, 2, 5, -9, 13, 1, 10, 11, 3, 5, -2, 2, 2, -7], + [8, -26, 32, -7, -5, 22, 2, 14, -10, -8, -7, 3, 3, 7, 0, -5, 0, -1, -3, 0, 8, 4, -5, -7, 6, -1, 4, 8, 1, 1, 7, -6], + [4, -31, 2, -14, 2, 0, 1, 8, -6, -1, 17, -3, 13, -6, 5, -10, -2, -10, -2, -10, -3, 7, 1, 5, -8, 8, -14, -3, -15, 7, -10, -6], + [16, -27, 13, -4, -23, 7, -9, 6, -7, 5, 4, 2, -1, -3, 23, -18, 7, 0, -3, 4, -3, 9, -6, -2, -1, 8, -6, 2, 6, -3, 2, -2], + [-1, -35, -2, -8, 11, -1, -7, -3, -2, 11, 7, 6, -6, -10, 9, 6, -3, -5, -6, -3, 9, 16, -16, -9, -20, 12, 3, 5, -3, 1, -9, 4], + [2, -24, 1, -12, -16, 5, -4, 3, -4, -1, -11, -11, -8, -14, 14, 10, -8, 20, 8, -3, -11, 1, 1, -4, -4, -7, -3, 15, 2, -6, -2, 7], + [9, -21, 2, -19, -7, -5, -8, 25, 3, 17, 5, -3, 9, -12, 8, 2, -4, 3, 3, 1, 11, -9, -4, -3, 4, 3, -22, 6, 4, 6, 11, -5], + [16, -23, 13, -17, -21, -12, 5, 9, -20, 7, 6, -6, 0, 2, -9, 6, -6, -13, -7, -1, 5, -3, 5, -7, -10, 1, 0, 8, -9, 11, 0, -8], + [10, -26, -9, -7, -19, -4, 6, 16, -7, 5, -4, 4, 8, 0, 4, -1, 6, -7, 1, -8, -11, 10, -14, 0, -16, 6, -3, 5, -1, 14, 12, 1], + [8, -27, 12, -14, -1, -1, -19, 10, -11, 21, -14, 9, -8, -3, 8, -1, 12, -13, 3, -4, -2, 0, -9, 0, -7, 2, -3, 12, 1, -3, 3, 1], + [18, -20, -14, -14, -16, -3, -24, 6, -17, 2, -3, -11, 2, -3, 12, 10, 10, 1, 10, 7, 8, 5, 5, 4, -1, 7, 2, 2, 0, 4, 7, 0], + [0, -30, 9, -16, -18, 15, 12, -3, 4, -4, -5, -11, -4, -12, -10, 0, 2, -2, -4, -1, 2, 0, -1, -6, 2, -3, 4, -5, 7, 3, 5, 7], + [25, -24, -1, -6, -9, 6, -13, -2, 3, 15, -3, 11, 4, -8, -11, 2, 0, -9, -2, 7, 4, 8, 5, -8, 5, 6, -1, -11, -15, -5, 0, 11], + [0, -34, -7, -11, -7, 9, -3, 19, 4, -8, 3, -11, 11, -3, -9, 12, 9, 9, 2, 1, -7, 1, -3, 0, -6, -2, -1, 3, 0, -7, -2, -5], + [6, -34, -4, -5, -3, -9, 2, 9, -1, 9, -5, -3, -26, -12, 8, -6, -7, 11, -8, 4, 4, 1, -1, 0, 8, 9, -4, 7, -1, 1, -3, -1], + [3, -30, 5, 6, -10, 3, -7, 6, 3, 3, -26, -19, -3, 1, 7, 5, -4, -5, 6, 10, 13, -10, 4, -7, -4, 5, -3, 9, -6, 3, 9, 5], + [4, -24, 9, -19, 2, -4, -5, 8, -3, 2, 0, -15, -1, 9, -4, 22, 6, 9, 3, 7, 11, -9, 0, -3, 4, 5, -5, 10, -8, 5, -7, -3], + [8, -27, 7, -3, -1, 2, -9, 13, 7, 12, -4, -6, -6, 5, 0, 7, 5, 1, 15, -3, -4, 0, -5, -2, 7, -5, -7, 1, -2, 13, -8, 13], + [17, -22, -15, -11, -8, 16, -14, 18, 2, -1, 14, -7, 14, -6, -6, -7, -8, 17, 6, 4, 4, -7, -5, -9, -14, -6, -1, 9, -3, 1, 6, -5], + [25, -30, 2, -12, -13, 18, -18, 16, 8, -3, 10, -8, -3, -1, -6, 3, -5, -7, 4, 6, 7, 1, 1, -11, -5, 6, 2, -4, 9, -1, -5, -2], + [7, -23, 7, -15, -1, -3, -1, 0, -10, 12, 2, 5, -4, 0, 4, 6, -1, 5, -9, -1, -1, -7, 1, 17, 9, -17, -16, 8, 4, -14, 11, 14], + [0, -31, 7, -13, 3, -11, -7, 6, 1, -11, 8, -7, 15, -3, 16, -11, -1, -15, 16, -3, 5, 0, -2, -2, -6, 11, 5, 6, 5, -5, 6, 3], + [13, -24, -2, -20, -10, 7, -3, -1, 15, 2, 6, -5, -7, -10, -20, 1, -4, 14, 8, -2, 3, -13, -3, 1, -4, 1, -3, 2, 8, -7, 16, -4], + [1, -2, -2, -3, -4, -7, 0, 3, 6, 7, 3, 2, 1, -2, -1, 0, -6, 4, 2, -4, -3, -4, 5, 9, 5, 0, -3, -3, -4, -7, -31, -50], + [-1, -3, 7, 2, -1, 2, 4, 6, 0, 10, -2, 0, -20, -6, -3, 9, -20, -22, -1, -1, 15, 9, -12, 10, -13, -20, 12, 3, 5, 6, -7, -26], + [0, 4, -2, -14, -12, 6, -13, 11, -10, 3, 22, 6, 16, -2, -5, 1, -3, -11, 0, -7, 5, -5, 0, 1, -1, -6, 8, 8, 10, 9, -5, -27], + [-5, 10, -2, 7, 9, -9, 5, -9, 5, 4, -15, 14, 1, 3, -10, 5, 0, -2, 7, 3, -13, 6, 9, -6, 5, -14, -17, -1, 11, 14, -2, -26], + [0, 6, -3, 0, -8, 6, 0, 1, 4, -8, 2, -5, 4, 7, 15, 11, 9, 19, -2, 14, -8, 7, -1, 3, -3, -3, -10, -2, 12, -2, -12, -29], + [-12, -5, 0, -3, -2, 6, 3, -3, 2, -2, 1, 11, 2, -7, 5, 1, 2, -2, -14, 0, -1, -5, 3, 8, -28, -26, 6, -6, 3, 8, -10, -27], + [-1, -3, 6, 2, 4, 15, 1, 0, 2, -2, -2, 13, 3, 6, 0, 6, -1, -4, -1, -5, 8, -1, 5, -5, -15, 11, -8, -5, 14, -6, -14, -29], + [-5, -6, 0, 1, 0, 6, -3, 2, -5, -1, 5, -3, 2, -10, 3, 4, 3, 0, 13, -3, -1, 4, -4, -6, 2, 9, 8, 2, -3, 28, -11, -31], + [1, -4, -10, -9, -4, -3, -15, -6, 1, 5, -3, -6, 5, -6, -22, 27, -13, 5, 3, -7, -4, 20, -7, -12, -1, -24, -4, -13, -8, -11, -15, -21], + [-6, -4, 19, -6, 2, 11, -6, 1, -3, -10, 9, -9, 12, -10, 2, 1, -9, 1, 15, 7, -5, 5, -29, -35, 4, -30, 9, 9, 19, 17, 2, -17], + [-3, 3, -3, 1, 2, 5, -1, 5, -2, -3, 1, -3, -8, 3, -4, -2, -4, -1, 12, 0, 2, -8, -6, -4, 16, -1, -14, -2, 25, -6, -15, -36], + [0, -1, 3, -4, -4, -1, 7, -4, 8, 0, 10, 9, -4, 1, 10, -1, -3, -13, -5, -4, -1, -4, 8, 11, 14, -7, -5, 16, 12, 13, -1, -28], + [1, -2, 2, -3, -8, 10, 4, 9, 12, 3, 5, 0, 8, -3, -6, 2, 16, -11, 11, 0, 1, 6, 1, 18, -10, -16, -1, -4, 5, -14, -15, -20], + [1, -12, 5, 4, -7, 8, -1, -17, -2, -9, -14, -11, 6, -9, 5, -4, 3, -2, 7, 18, -5, 5, 6, -1, -11, -2, -10, -3, 8, -3, -2, -32], + [-12, 5, 20, -5, -6, -11, -6, -6, -13, 4, -6, 19, -8, 2, 3, -9, -4, -4, -1, 9, -1, 21, -1, 7, 15, -10, -1, -3, 9, -3, 2, -24], + [0, -3, 2, -6, 4, -1, -9, -2, -1, -3, 6, -1, -5, -6, -5, -8, 0, -2, -6, 9, -4, 3, 2, -13, 1, -7, 23, -13, 4, -3, -15, -33], + [-7, 2, -15, 11, -10, 14, 0, -11, 3, -1, 12, -4, -4, 9, 11, -13, -13, -3, -14, 1, 3, 6, -5, 8, 0, 5, 5, -10, 4, 5, -6, -30], + [-6, 4, 0, -5, 4, 1, -1, -1, 3, 6, 5, -2, -5, 0, -2, 5, -4, -2, -4, -2, 4, 7, -7, -1, 1, -4, -3, -19, 37, 12, 10, -40], + [-7, 2, -7, -12, 17, 11, -7, 2, 2, 3, 1, -1, 3, 4, -2, -5, 9, -9, 6, 4, 9, 12, 11, -5, 2, -1, 0, 9, 5, -7, -2, -24], + [-7, 6, 1, 3, 1, 0, 6, 0, 4, -12, -2, -2, 1, -9, 10, -2, 11, -1, 21, -12, 15, -5, 10, -5, 5, -5, 14, -6, 5, -7, -3, -29], + [-2, 0, -5, -2, -3, 1, -3, 0, 4, 2, 3, 0, 2, -2, 7, -2, 3, -5, 2, -1, 6, -4, 0, -3, 8, -11, 19, -8, 22, -34, 13, -35], + [-1, -3, -1, 9, 11, -3, -3, -1, 7, 18, 11, -5, 2, -12, -11, 18, 9, -5, 1, -6, -9, 12, 1, -3, -3, -9, -14, 9, 9, 8, -6, -26], + [0, 5, -5, -1, -1, -2, 4, 6, 8, 2, -1, -2, 5, 1, -5, -4, 1, 1, 18, 1, 7, -10, 3, -2, 12, -1, -15, 9, 12, -14, 13, -38], + [3, 0, -8, -1, 0, 8, -9, -3, -8, 16, 3, 16, -5, -9, 0, -1, -7, -1, -4, 13, 7, 0, 1, 2, -1, -16, 0, -2, 1, 8, -8, -28], + [7, 9, -5, -3, -2, 2, 0, 3, 11, -6, -4, -2, -2, -5, 28, -18, -6, 2, 15, -10, -15, -10, -2, 0, -2, -2, 4, -3, 7, 11, 5, -30], + [9, 0, -7, -1, -4, -7, 2, 2, 9, -2, 2, 3, -8, -6, -6, 3, -10, 4, 10, 5, 21, -4, 14, -18, 1, 3, -10, -2, 6, 14, -8, -26], + [-14, -1, 2, 3, -3, 7, 1, -22, -1, -1, 0, 1, 12, -14, 3, -5, 0, 10, -3, 1, -5, 12, -3, 10, -8, -22, -11, -13, -7, -10, -13, -25], + [-2, -5, -4, -4, -9, -18, 9, -3, -5, 17, 13, 5, 6, 11, 3, 8, 20, 4, 2, 9, 8, 5, 6, 1, 7, -7, -6, -2, -7, 0, -17, -23], + [-5, -5, 2, 0, 6, 2, -2, 2, -3, 4, 4, 0, -5, -2, -4, 6, 8, 10, -1, 1, -5, 5, -14, -2, -11, 8, 6, 25, 7, -1, 0, -43], + [-4, 0, 4, -2, 7, 0, 3, 17, 5, 2, -5, 1, 21, 3, -2, -10, -16, -9, 7, -12, 9, -8, 2, 5, -5, -10, -2, -11, -5, -1, -9, -30], + [-2, 3, 1, -4, -1, 0, 8, 1, 12, 4, -1, -1, 3, -17, 13, 9, 0, 7, -6, -5, 9, 1, 5, 4, -10, -18, 0, 14, 11, -4, -16, -28], + [-1, 0, 2, -1, 4, 1, -1, 1, -1, -2, -1, -2, 3, 0, 0, -1, -1, 1, 2, -2, 3, 3, -2, 4, -2, -1, -6, 1, -1, -1, 6, -70], + [7, 3, -11, -1, 12, -4, -14, 4, 4, -4, 4, -2, 2, -12, -4, 15, -17, -4, -3, 6, 8, -5, 22, -22, 5, -11, 15, -4, 4, -1, -21, -1], + [10, -2, -13, 11, 4, 14, 4, 9, 8, 8, 19, 15, 14, 15, 5, 10, 8, 15, -5, 4, 14, -8, 1, 1, 2, 1, -1, -3, 21, 8, -29, 13], + [-6, 0, -6, 6, -1, 2, 8, -4, -5, 4, -4, -5, 0, -2, -4, 0, 9, -2, 1, -2, 26, -19, 21, -10, 4, 1, -8, 5, 22, -10, -13, 15], + [11, -5, 1, 0, 6, 3, 7, -2, -2, -3, -5, -1, -2, -6, 1, 1, -8, -5, -13, 13, -2, -3, -1, -9, -28, 4, 2, -11, 18, -20, -24, 9], + [7, 4, -3, 6, 6, -6, -7, -5, -7, -4, -4, 0, -7, -5, -6, -5, 2, -13, -12, 2, 0, 5, 18, 15, -13, -7, 13, -20, 16, -10, -19, 6], + [5, -8, -1, 5, 10, 2, -1, -10, -11, 23, 8, -5, -8, 4, -5, -4, -5, -5, -11, -8, 5, 1, 7, -9, -9, -6, 12, 14, 17, -12, -22, 3], + [-5, -8, -3, 3, 12, -1, 0, -4, -5, 1, 1, 6, 1, 5, -5, 7, -2, 7, 1, 6, 6, 2, 0, -5, 17, -4, -5, -24, 13, -20, -27, 14], + [-1, 2, -3, 1, -3, 1, -3, 0, -2, 3, -2, 1, 2, -1, -2, -1, -2, -5, 5, -2, 0, -7, 1, -6, 8, 8, 11, -5, 24, -43, -13, 2], + [-2, 4, 7, -3, -4, 4, 13, -4, 0, 0, -2, 9, 0, -3, -6, 1, -7, 1, -1, 10, 0, 5, -1, -24, 25, -15, 7, 2, 22, -10, -21, 0], + [-5, 2, 6, -2, 13, 3, 5, -12, -11, 16, 6, 10, -5, 0, -3, 6, 5, -5, -5, 10, 12, 10, 11, -7, 8, -14, 2, -15, 13, -14, -8, -3], + [5, 6, -7, -5, 5, 2, 9, 5, 0, -1, -4, 2, 8, 0, 3, 5, -12, 3, -3, -6, 2, -1, -5, 14, 11, -20, -21, -25, 24, -1, -10, 6], + [-5, 5, -2, 9, 4, -4, -1, -6, 11, -6, 5, 0, 2, -3, 6, -1, -17, -18, -4, -13, 9, -1, 9, -7, -4, -8, 2, -3, 12, -31, -18, 5], + [-7, -11, 6, -8, 4, -3, -12, 0, -1, -6, -3, 0, 5, 9, 7, 2, 1, -8, -6, 8, 2, -5, 7, -1, 16, -10, 16, -12, 18, -1, -25, -12], + [3, -12, 1, 2, -2, -18, -8, -15, -10, -9, 2, -7, 11, -11, 2, -1, -1, -1, -9, -6, 3, -14, -2, -1, 2, -13, -7, -9, 19, -5, -17, 2], + [7, 1, -8, 7, 17, -13, -10, 5, 7, 1, -6, 4, 9, -4, 0, 3, 8, 1, -14, -9, 4, 7, -9, 0, 6, -5, -12, -2, 25, -2, -19, 1], + [7, -3, 6, -3, 1, 6, -7, 0, 10, 0, 4, -5, -17, -4, 4, -1, 0, -3, -7, 19, 24, -1, 21, 8, 10, 9, 8, -1, 23, -2, -18, -2], + [3, -3, 0, 5, 8, -2, -9, 2, 9, 6, 19, 8, 2, 6, -9, -2, -4, -3, -8, 7, -7, -8, 5, 4, 26, -6, 7, 18, 24, 0, -13, 4], + [0, -13, -11, -1, 3, -9, 5, 4, -7, 3, 0, 2, -1, 4, -5, 2, 9, -2, -11, 15, 1, -21, 1, -1, 0, 4, -14, -4, 24, -16, -13, 1], + [1, -9, -8, 0, 0, -4, 11, -1, 14, 16, 0, 17, -2, -9, -12, 0, -1, -14, -9, -14, 0, -2, 19, 4, 6, 4, 4, -11, 8, -17, -19, -5], + [-3, 1, 2, 12, -4, -18, -1, -4, -7, 14, -3, 2, 0, -7, -8, 12, -5, -9, 14, 12, -9, -2, 4, -6, 4, 18, -1, -25, 22, 2, -23, -5], + [-2, 0, 0, 0, 1, 3, 5, -1, 5, -2, -2, 2, -3, 0, 1, 2, 0, -1, 2, -1, -9, -6, -7, -4, -2, 4, -7, -5, 64, -3, -25, 4], + [12, -2, -3, 0, 8, -9, 13, -7, 6, -3, -12, 12, 15, -9, -4, 2, 9, -4, -12, 3, 14, 1, 7, -15, 15, 0, -6, -12, 0, -3, -20, 6], + [2, -1, -4, 5, 9, 6, -7, 2, -2, -7, -2, 0, -1, -18, -4, -6, -15, -5, 11, 5, -10, -1, 2, 7, 12, -19, -7, 8, 21, -4, -15, 4], + [4, 2, 5, 5, -5, 1, 3, 2, -8, 13, 0, -5, -2, -14, -11, 6, 2, 17, 8, -13, 26, -2, 5, -15, -4, -14, 12, -9, 13, -21, -23, -4], + [2, -3, -2, -3, 3, -2, 6, 9, -9, 13, 4, 2, 12, -3, -3, 1, -17, -22, -3, 4, 3, -2, 1, -9, 1, -6, 11, -13, 14, 0, -15, 6], + [-16, -4, 17, -2, -20, -11, 11, 10, 5, -8, 16, 2, -17, -14, 11, 11, -6, -11, -7, 12, 12, -10, -6, 5, 8, -4, -2, -5, 28, 3, -13, 4], + [0, -3, 3, -7, 6, 8, -12, 20, -19, 18, -11, 10, -5, 0, -9, 11, 3, 0, -2, 9, -7, -5, 18, 3, -2, -16, 1, 6, 12, -7, -16, 1], + [4, 1, 5, -5, 15, 2, -8, 3, 5, -11, 15, -3, 8, -8, -1, 7, 4, 7, -2, 6, -9, 5, 12, 2, 33, -2, -6, -18, 4, 0, -18, 11], + [3, -1, 1, -1, 0, 1, 4, -1, -5, 0, 1, 0, 4, 2, -1, 4, -3, 2, 0, -2, 4, 6, -1, 6, 42, 19, -4, -37, 19, 1, -15, -4], + [2, 0, -5, 0, 10, 0, 0, -5, 3, 0, 0, -3, -3, 0, 2, -4, -10, 2, -6, 4, 4, 1, 27, -7, 17, -34, 5, -9, 15, -16, -7, -5], + [-2, 7, 7, -2, 9, -2, -15, 11, 11, 7, 5, 1, 15, 1, -9, 31, 2, -15, 2, 4, 3, 4, -1, -8, 2, -7, 6, -17, 11, -14, -11, 2], + [1, 1, -11, 9, 9, -6, -14, -11, -10, 8, -3, 11, 16, -9, -8, -13, -8, 9, 0, 6, 6, -2, 13, -8, -2, 3, 13, -3, 10, -6, -17, 4], + [14, 5, 4, -6, -12, 10, -7, 8, 21, -8, -30, 15, -2, 1, 11, -9, -5, 1, 0, -1, -1, -6, -2, 3, -5, 7, 9, 5, -5, 2, 0, 1], + [-1, 2, 20, -17, -15, 3, 3, 7, 11, -17, -13, -6, -3, 18, 17, -15, -4, -4, -5, 22, 14, -14, -2, -10, -7, 11, 8, -7, -3, 0, -7, 11], + [7, -11, -7, -8, -14, 22, 5, 2, 6, 13, -12, -2, 10, 3, 0, -21, -4, 20, 3, 10, 21, -10, -12, 8, 11, 2, -5, 2, 1, 3, -1, 15], + [-1, -2, -1, -2, -13, 8, -4, 0, 7, -2, -17, 8, 18, 5, 3, 8, -8, -2, 3, -4, 14, -18, -13, 14, 15, -13, -1, -2, 4, 11, 1, 12], + [13, -6, -4, -16, -17, 16, 21, -2, 5, -11, -9, 19, 21, -17, -3, -17, 3, 12, 8, -12, -6, 1, -7, 9, 9, -7, -5, -1, -3, 5, -6, -4], + [11, 5, 12, -20, -6, 10, 4, 12, 8, -5, -10, 15, 13, 14, 10, -15, -13, 1, 6, 14, 15, -17, -13, 4, -5, 10, 7, -6, -8, -3, -4, 12], + [25, -1, 7, -5, -7, 11, 1, 17, 13, -15, -14, -4, 5, 3, 8, -3, -2, 2, 0, 6, 16, -12, -6, -4, 4, -3, 7, -10, -3, -7, -13, 7], + [-8, 10, -3, -13, 5, 2, 4, 9, 9, -17, -13, 2, 11, 1, 6, -4, 8, -10, 4, 1, 19, -15, -4, 12, 31, 7, -5, -17, -4, 9, -2, 7], + [14, -6, -6, -6, -14, 13, 17, -5, 4, -14, -9, 7, 7, -9, 3, -16, -15, 11, 11, 6, 4, -11, -19, 3, 5, 8, 13, -14, -14, 3, -4, 12], + [-2, -4, 10, -4, -7, -1, 27, 5, 2, -16, -18, 4, 12, -2, -3, -2, -1, 1, -8, -12, 3, -4, 8, 15, 2, 4, 9, -13, -14, 9, -7, 5], + [4, 2, -10, -5, -7, 2, 1, 4, -1, -6, -15, 6, 1, 10, 5, -10, -9, -1, 13, -3, 5, -21, -11, 8, 8, 5, 27, -21, -18, -5, -1, 15], + [11, 1, -16, -8, -11, 0, 5, -8, -12, -13, -17, 22, 4, -6, -1, -18, -10, 0, 19, 2, -2, -8, -7, -3, 2, -2, -9, -17, -5, 4, 4, 10], + [8, -6, -19, -5, -4, 12, 14, 15, 10, -9, -1, -9, 19, 12, 0, -1, 2, 4, 7, 9, 16, -16, -14, 9, -4, 3, 1, 0, -2, 10, -1, -1], + [12, -8, 12, -9, 0, 25, 7, 9, 2, -31, -9, -4, 15, 4, -5, 1, -10, 11, 8, 10, 0, -6, 5, 11, -1, -6, 4, -10, -9, 6, 4, 5], + [14, 6, -17, -2, 17, 12, -9, 2, 0, -25, -14, 5, 20, 14, 8, -20, 5, 2, -2, -3, 9, -13, -3, -1, -6, 3, 7, -6, 0, 2, 3, 1], + [8, 4, -15, -3, 10, 18, -4, 13, 8, -22, -10, 9, 19, -15, 7, -5, -13, 12, -4, 9, 2, -9, -6, 0, 2, 1, -9, -6, 6, 1, -1, 11], + [4, 1, 4, -5, -10, 18, 7, 2, -4, -9, -11, 0, 32, -7, 4, -16, -1, 0, 6, 3, 6, -3, -14, 16, 9, -2, 7, -1, 0, -5, 5, -3], + [-3, 2, 3, -8, -6, 4, 6, 2, 4, -12, -15, 2, 8, 8, 9, -3, -18, 6, 34, 11, 12, -15, -1, 2, 9, 2, -4, -4, 2, 4, 2, -3], + [18, -6, -12, -8, -1, 15, 20, -4, -1, -11, -5, 6, 6, -11, -15, -7, 3, 7, 10, 2, 8, -10, -5, 8, 15, -5, 5, -17, -13, 13, 11, 7], + [8, -4, -6, -1, -14, -3, 6, -2, 1, -5, -1, 10, 10, -15, 5, 0, -10, -4, -3, 7, -4, -19, -15, 27, 11, 18, 3, -19, -2, 6, 0, 12], + [12, 0, -5, 0, 4, -5, 1, 5, 10, -7, -11, 21, 29, 1, -2, 1, -4, -11, -1, 13, 11, -20, -1, 4, 4, 4, -5, 6, -13, -2, 11, 9], + [2, -7, -7, -3, -10, -1, 20, 12, 1, -19, -19, -1, 5, 4, -7, -25, 14, 1, -3, 2, 12, -4, -3, -3, -2, 6, 1, 0, 3, 2, 5, -1], + [12, -8, 3, -12, -10, 10, 13, 0, 23, -14, -18, 10, 0, 15, 3, -12, -3, -5, 5, -4, 2, -14, -10, 8, 2, 9, -1, -11, -3, 5, 13, 2], + [9, -6, 7, -7, -30, 17, 6, 13, 1, -14, 0, -1, 6, -9, 8, 3, -4, 0, -1, -7, -5, -13, -19, -3, -4, 4, -6, -2, -13, 1, -2, 3], + [10, 1, 3, -18, -26, 17, 4, -16, 4, -3, -13, -4, -6, -11, -4, -21, 7, 8, 2, 5, 13, -6, 1, 5, 8, 7, 9, -6, -6, 1, -1, 2], + [-3, -1, 0, -2, -2, 0, -1, 3, 4, -14, -8, -9, 13, 2, 50, -23, -8, 8, 7, 11, 16, 3, -7, 0, -2, 6, 5, -1, 1, -2, 4, 3], + [1, 3, 1, 1, -6, 3, 6, 6, 2, -2, -3, 10, 2, -8, -5, -5, 5, 4, 4, -2, 10, -8, -40, -1, 21, 8, 3, -4, -1, 13, 4, 7], + [2, 0, -4, -8, 5, 2, 7, -5, 5, -8, -4, -1, 12, 2, 12, -13, -9, 0, 1, -12, 9, -43, 1, -5, 12, 1, 3, 6, 1, -1, 3, -2], + [6, -2, -1, 1, 0, 4, 8, 14, 4, -7, -23, -5, 23, -17, -6, -15, -8, 7, 10, -1, 7, -16, 4, -6, 2, 3, -3, -3, -1, 8, -1, 4], + [10, 4, -4, 1, 7, -3, 2, 11, 4, -6, -3, 8, 5, 4, 1, -45, -6, -4, 4, 2, 1, -14, -10, 1, 1, 6, 2, -8, -1, -3, 3, 3], + [1, -1, 2, -3, -8, 9, 3, 3, -2, -5, -8, 8, 7, -7, -4, -6, 5, -9, 11, -2, 46, -5, -1, 9, -2, 0, 3, -5, -3, -5, 7, 0], + [-4, 1, -2, -1, -11, 11, 8, -3, -2, -10, 0, 4, 9, 9, -17, -17, -34, -4, -5, -7, -3, -12, -3, 11, 18, 3, -2, -5, -18, -5, -3, 6], + [7, -5, -3, 1, -4, -3, -5, -1, 2, 5, -2, 3, -10, 12, -18, -5, -10, 12, -9, 4, -6, 2, 0, 16, -17, 15, 14, -12, -10, -2, -9, -1], + [4, -5, -3, -5, -3, -1, 7, 18, -7, 12, 3, 5, -8, -4, -20, 1, -25, 1, -8, 13, -10, 8, -19, -1, -8, 10, 6, -9, -1, 0, 12, 4], + [-4, 5, 0, -1, 2, 5, -8, -2, -6, 4, -8, 9, 3, 2, -7, 4, -25, 13, -23, 10, 14, 15, -11, 3, -18, 4, 16, -4, 1, -10, -10, 3], + [5, -3, -1, -3, 4, 1, -3, -4, -5, 1, -12, 14, -7, 11, -15, 6, -6, 24, -4, 13, -1, 15, -13, 8, 3, 7, -5, 2, 2, 0, 3, -7], + [-3, 1, 0, 8, 6, -1, 6, 5, -5, -2, -12, 4, 0, -2, -3, 5, -6, 0, -8, 9, -10, 4, -28, 12, -20, 11, -13, 7, -18, 1, -11, 1], + [1, -4, -15, 5, 0, -13, -5, 13, -11, 4, -4, -5, 5, -14, -16, 0, -14, 5, -20, 12, 10, -7, -5, 6, 6, 22, 6, -4, -2, 3, 8, 11], + [13, -11, -2, 16, 16, -7, 0, 20, -7, -1, 0, 5, -9, 12, -2, -5, -22, 5, -10, 12, -6, 11, 9, 21, -8, 15, 4, 0, -8, -4, -4, 10], + [18, -4, -13, 0, 1, -15, -1, -3, 2, 10, -1, 6, 1, -4, -20, -5, -8, 6, -8, 17, -5, 5, -10, 8, -22, 6, -5, -2, 8, -17, 8, 2], + [1, -2, -9, 6, -31, -8, -8, 8, 0, 5, -9, -4, 2, 3, -12, 11, -18, 10, -5, 3, -11, 13, -6, 11, -3, 12, -7, 3, -9, -1, 2, 11], + [-9, -6, 21, -8, -15, 4, -11, 12, -11, 17, -1, 2, -6, 0, -15, 13, -12, 19, 0, 2, -6, -3, -9, 10, 3, 17, -2, 5, -10, -3, 0, 1], + [4, -6, 5, -10, 1, -5, 1, 0, 0, 0, 2, 7, -2, 2, -2, 0, -4, 3, -4, 1, -12, 6, -49, 16, -10, 13, 0, -2, 8, 6, 1, 8], + [5, -8, -7, 9, 13, -5, 7, 0, 10, 11, -4, -3, -1, 13, -14, 6, -15, -6, -14, 16, 15, 1, -18, -4, -20, 20, -7, -1, -9, -2, -10, 10], + [-12, 4, 0, 10, 0, 3, 8, 4, -27, -1, -2, 19, -4, 2, -13, 3, 1, 9, -12, 1, -22, 19, -5, 4, -9, 12, 2, -9, -8, 11, -3, 7], + [4, -5, 11, -6, 17, -17, 5, -4, -2, -6, 1, -5, 2, 4, -14, 6, -20, 19, -20, 12, -21, 5, -14, 13, -2, 11, 4, -3, 0, -10, -4, -2], + [-2, -1, -3, 8, -9, -7, -22, -3, -24, 13, -2, 10, -15, 5, -9, 4, -7, 0, -5, 15, -8, 11, -13, 6, -4, 19, -8, 12, -4, 6, 9, 7], + [2, -3, 2, -1, 0, 3, 1, 2, 1, -4, -2, -3, 1, 5, -12, 6, -16, 14, -23, 10, -14, 17, -15, 16, -2, 9, -25, 9, -10, 16, 4, 9], + [-3, 7, -8, -3, 2, 2, -4, -8, -9, 10, 3, -11, 25, -10, -28, 27, -9, 7, -13, 9, -2, 4, -12, -8, -14, 6, 7, -10, 3, 3, -3, 5], + [-8, -3, 1, -10, 8, -3, -9, -4, 13, 7, 2, 4, -10, 4, 3, 7, -18, 2, -22, 15, 4, 20, -7, 5, -6, 13, -1, 4, -7, -6, 6, 13], + [-2, 3, 0, 2, -4, -2, 0, 0, 1, 2, -2, -5, 0, 1, -4, 0, -2, -3, 1, 2, -1, 2, -8, -1, -24, 68, -3, 8, 3, 3, -1, -1], + [-15, -2, -9, -7, -1, 8, -14, 8, 3, 6, 0, -1, -8, 8, -23, 2, -14, 17, -15, 8, -4, 7, -18, 0, -8, -3, -1, -4, -10, 4, -1, 4], + [8, 0, 2, -7, 0, 5, 1, 3, -11, 4, -8, 14, 3, 20, 1, 26, -11, 13, -13, 20, -2, 0, -8, 2, -6, 6, -1, 9, 3, -6, -3, 10], + [5, 0, -1, -7, 10, 1, -3, 5, 4, 7, -5, -1, -3, -1, 12, -3, -15, 7, -9, 22, -19, 8, -9, 4, -23, 13, -14, 6, -6, -14, -4, 7], + [14, -5, -8, -10, 25, 3, -23, -7, -28, 0, -1, -9, 4, 1, -13, 20, -8, 10, -16, 8, 12, -13, -21, 5, -13, 11, -2, 1, 12, -7, 2, -10], + [-5, -4, 9, 5, -6, 35, -7, 8, 15, 2, -1, -9, -6, 2, -18, 7, -15, 6, -3, 2, 8, 12, -30, 7, -4, 20, 2, 6, 13, -6, -4, 0], + [1, 8, -9, 9, -5, 12, -9, 16, -9, 16, -17, 14, -13, 15, -18, 14, -15, 17, -12, 14, -13, 7, -16, 13, -9, 5, -11, 10, -9, 6, -12, 13], + [-10, -4, 5, 3, 1, 6, 8, -14, -5, 15, 7, 4, 8, 7, -22, 8, -7, -8, -15, 26, 1, 13, -3, 17, -5, 9, -2, 4, -6, 3, -8, 9], + [8, -3, 2, 3, 3, 1, -2, -1, -11, 8, -4, 0, -6, -5, -1, 13, -37, 9, 1, -6, -10, -2, -10, 11, 8, 13, -3, -2, -6, 8, -4, 13], + [3, 2, -3, -4, -4, 7, -8, 9, -8, 9, -20, 12, -19, 15, -18, 17, -15, 7, -1, 20, -11, 6, -6, 3, 1, 9, 2, -14, -2, -2, 2, 1], + [-7, 1, -1, -3, -6, 4, 4, -3, 3, -1, 5, -4, 3, 2, -1, 9, -59, 5, -4, 30, 3, 3, -2, -3, -1, 2, 2, 1, -1, -1, -2, 1], + [0, -3, 2, 0, -1, -8, 0, 2, -3, 4, -4, 1, 10, 6, -6, 8, -7, 4, 10, 11, -41, 27, -20, 3, -3, 8, 1, 11, -5, -8, 0, 4], + [5, 1, 4, -2, 1, 2, -1, 6, -7, 2, 11, 4, 0, 0, -8, 7, -10, 0, 0, 8, 2, 10, -1, 1, -2, 44, -2, -21, -12, -3, -1, 2], + [-4, 4, -2, -2, 6, -8, 2, 1, -10, 14, 8, 6, 5, 1, -2, 4, -13, 4, 2, 5, 10, -2, -21, 32, -3, 18, 9, -6, -9, -9, 10, 2], + [9, -16, -6, -2, 1, 4, 22, 2, -2, 1, -3, -2, -9, 3, 16, 19, -24, -6, -6, -5, -8, -7, 8, -7, -1, -12, 5, -3, 0, 4, 2, -3], + [10, 3, -16, -4, -1, 13, 4, 4, 1, -3, 1, -6, -14, 18, 3, 8, -8, -28, -16, 4, 4, 2, 12, 7, 9, -4, -4, 5, -1, -1, 2, 2], + [-5, -13, -22, -3, -8, 21, -2, -9, 21, -4, -9, 5, -8, 15, 5, 1, -5, -9, -7, -2, -5, -5, -1, -5, -5, -5, 3, 10, -4, 0, -7, -2], + [5, -10, -18, 2, 20, 4, 13, -10, 8, -15, -11, -3, -1, 16, 10, 9, -8, 6, 7, -5, 6, 11, 5, 17, -4, 7, -11, 5, -3, -6, 2, 1], + [3, -5, -19, 1, 1, -3, -2, -25, -11, -17, 0, -13, -4, 10, 10, 2, -5, 4, 0, 3, -3, -5, -10, -2, 13, -22, 0, 3, -11, -5, 7, -1], + [12, -14, -29, 6, -1, 10, 7, -17, -12, 14, 3, 9, -9, 9, 7, 6, -3, -13, 0, 5, 3, -1, -6, -1, 0, 2, 4, -12, -5, -1, 2, 11], + [12, -15, -7, -2, -12, 17, 20, -16, -2, -12, -6, 15, -6, 12, 11, 9, 7, -6, 7, -4, -19, 6, 2, 2, 3, -11, -10, -4, -5, -3, 3, 2], + [11, -22, -6, 0, 8, 18, 3, -11, -4, -7, -15, -17, -12, 6, 16, 4, -9, 4, -5, 3, 6, -16, 10, -7, -7, -3, 5, 0, 1, -15, -4, 5], + [12, -22, -16, 5, -6, 8, 12, -4, -9, -17, -11, 3, 5, 8, -17, 0, 11, -4, -13, -6, 2, -1, -1, 3, 3, -11, -12, -1, 1, 1, 12, -2], + [8, -10, -33, -5, -3, -6, 1, -7, -8, -4, -6, -1, 5, -4, -6, -12, -16, -8, 11, 8, -14, 7, 12, 11, 4, -14, -3, 6, -7, -5, -3, 3], + [0, -8, -7, 2, -4, 24, 2, -9, -11, -3, -7, 11, -12, 17, 1, -1, 3, -5, -7, 12, 4, 11, 0, 3, 2, -18, -3, 4, 7, -6, 3, 15], + [10, -15, -16, -2, -4, -9, 7, -15, -6, 2, -16, 13, -8, 7, 19, -21, -4, -12, -9, -3, -3, 6, 11, -3, -1, -19, 3, -7, -9, -4, 3, -6], + [-5, -10, -21, 0, -3, -7, 18, -21, 15, -5, -12, -4, -13, 2, 6, -9, -9, -11, -4, 13, -3, 6, 4, -1, 7, -9, -4, 9, 5, 2, 6, 3], + [15, -1, -27, -2, 10, 3, 7, -8, 9, -2, 7, 1, -2, -5, 18, 9, -11, -17, -2, 7, -9, 11, 10, 0, -8, 6, -16, -3, 2, -7, 3, 11], + [4, -9, -39, 19, 6, -13, 13, -5, -5, -15, -2, 9, 0, 4, 14, 6, -10, -4, -5, 2, -4, -2, 5, -11, 3, 3, -2, -2, -7, 9, 7, -10], + [5, -11, -8, 10, -2, 12, 16, 0, 12, -2, -6, 8, 14, 8, 7, 1, 18, -30, 4, 10, -4, -6, 2, -11, 9, -10, -8, 5, 0, 0, -7, 6], + [-1, -16, -10, 11, 0, 13, 12, -4, -4, -5, -21, 12, 4, 13, 14, -7, 6, -16, -13, 8, 2, 9, 15, -12, 1, -9, -22, 10, -9, 9, 9, -7], + [4, -12, -27, 1, -2, 11, 15, 3, 14, -14, -9, 0, -9, 16, 22, 10, 16, -10, 5, -5, -9, 1, 1, 6, 6, -4, 2, -17, -5, -6, -15, -1], + [7, -12, -17, 1, -9, 5, 20, -7, 3, 23, -8, -8, -8, -1, 13, 17, -7, -13, 4, -4, 7, 14, 8, 11, -3, -3, 4, 0, 4, 6, -1, -9], + [7, -15, -15, -4, 10, 12, 3, -13, 6, 14, 9, -8, -15, 14, 23, -5, -10, -5, 1, 15, -10, -7, 1, 9, 4, -13, -10, 10, 7, -3, 2, 3], + [4, -10, -14, 0, 3, 4, 0, -9, -3, -4, -11, 2, -17, 8, 2, 15, 6, -12, -12, 15, -5, 17, 18, 3, -3, -3, -4, -6, -8, 13, 4, 10], + [-2, -18, -26, 10, -4, 10, 13, 4, -4, -16, -7, -17, -3, 5, -4, 2, -15, -10, -1, -8, -7, -3, 2, 2, 8, -10, -7, 2, 2, -4, 4, -1], + [4, -19, -5, -1, -1, -6, 2, -8, 10, -16, -28, -6, 8, -1, 11, 28, 2, -10, -4, 6, -6, 6, 11, 15, -4, -2, 7, 3, 7, -7, 4, 1], + [-3, -6, -10, -5, 13, 18, 10, -15, -5, -3, -13, 5, 1, 2, 18, -5, -10, -10, -7, 4, 2, 1, 5, 4, 2, 5, 4, 8, -9, -17, 7, 7], + [20, -12, -2, -4, 5, 14, 7, -11, -1, -16, -6, -4, -11, 17, 14, 0, -8, -10, -8, 10, 3, 5, 10, -16, 3, -8, -14, 10, 3, 9, 0, 3], + [12, -10, -36, 0, 7, 15, 2, -16, 2, -1, 0, -1, 5, 4, 5, -3, 1, -10, 5, -1, -15, -3, -12, 12, 2, 5, -1, 5, 6, -3, -2, 2], + [17, -15, -31, 23, -4, 15, -2, -3, 6, -7, -5, 1, -12, 4, 6, 8, -10, 8, 3, 5, -4, 1, 5, 3, -1, -4, -3, 1, 10, -4, -2, -2], + [6, -18, -5, 12, 10, 12, 14, -11, 15, 2, -9, -6, -5, -2, -9, 4, -5, -28, -4, 14, 0, -16, 9, 14, -1, 3, -4, -4, 2, 1, 0, 4], + [-5, -14, -31, 8, 16, 7, 13, -13, 5, 6, -16, 10, -5, 2, -2, 2, 14, -5, 8, -5, 7, -16, 6, -13, -5, 0, -5, 8, -3, -1, 4, 3], + [1, -2, -1, 0, 6, 5, 2, -4, -3, -1, 0, 1, 4, 2, 43, 28, -12, -35, -2, -2, -7, -1, 0, 2, -1, -2, -2, 1, -4, 0, -2, 3], + [2, -9, -22, 12, 3, 3, -7, -4, -19, -22, -14, -4, -1, 21, 9, -3, -15, -16, -13, 1, -11, 4, -9, 1, -7, -1, -1, 0, -2, 9, -13, -3], + [-1, -3, -23, 0, 2, 12, 3, -9, -4, 7, 3, 9, -10, 1, 27, 28, 0, 9, -15, -2, -2, 1, 6, 8, -8, 7, -3, 20, 0, 0, -1, -6], + [-1, 11, 8, -2, 1, 5, -6, -1, 4, 2, -4, 0, -1, -5, 4, -6, -10, -12, 19, 1, -7, 9, -8, -9, -16, -11, -2, 12, 14, 4, 4, 34], + [17, 7, -6, 1, 4, -10, -5, 4, -11, 3, -18, 4, 14, -13, -3, 1, 0, 0, -11, 0, 7, -17, -4, 4, -11, -6, -8, 18, 0, 0, 0, 26], + [-6, -7, -1, -1, 11, -8, 1, 3, 2, 11, -6, -6, 10, -3, 1, -3, 7, 4, -12, -8, 0, -9, 8, -22, -5, 0, -6, 22, -2, 11, -13, 24], + [-3, 4, 0, 3, 9, 10, -1, 3, -9, -12, 1, -5, 18, 0, -3, 8, 25, 15, -8, 2, 2, -2, 4, 8, 9, -1, -5, 10, -3, 1, -1, 23], + [-5, 2, -9, -1, -3, 0, 3, -1, -10, -4, 0, -13, 16, 9, -1, -14, 2, 6, -2, -6, -5, -2, -7, 7, 5, 3, 11, -2, -14, 0, -9, 30], + [4, 6, 6, 5, -3, -1, 4, 5, 10, 0, 5, -4, 7, -11, 14, 14, 7, 34, -9, 0, -10, 22, -7, -1, 7, -9, 2, -8, 0, -7, -5, 29], + [-4, 3, -1, -4, -3, 5, 1, -4, 0, 2, 4, 2, 1, -1, -10, 1, 6, -6, -4, 1, 4, -3, -3, -5, 0, 3, 7, -12, 0, -2, -10, 55], + [5, 9, -1, 0, 4, 9, -21, -9, 4, 2, 6, -7, 11, -7, 1, -5, 0, -4, 2, -3, -13, -8, 0, -9, -4, 2, 16, -2, -15, -7, -11, 31], + [8, 2, -1, 0, 3, -5, -5, 5, 1, -1, -9, 1, 0, -6, -2, -1, 5, 2, 0, 0, 12, 20, -19, 1, 8, -12, -11, 0, 6, -5, 2, 31], + [-1, -1, -2, 1, -1, 3, -9, -5, 8, -2, 5, -1, 0, -2, 4, -2, -3, -12, 0, -2, 3, 0, 9, 4, -1, 21, -8, 3, -4, 9, -6, 30], + [-4, 0, -7, 17, 10, -12, -2, -10, -12, -3, 10, 0, 11, -4, -13, -3, 5, 6, 10, 7, -8, 0, -7, -13, 1, 0, -2, 7, -12, 4, -3, 24], + [-13, 9, 4, -2, 2, -4, -14, -1, -3, -5, -10, 4, 13, -2, 5, 13, 8, 3, -2, 1, 5, -6, 7, -18, -10, 1, -1, 5, 4, 1, 0, 25], + [-5, -1, 18, 12, 8, 8, -16, -1, 1, 1, 1, -4, -5, 3, 3, 4, 4, -11, -12, -16, -6, 2, 12, -13, 0, 9, 7, 9, -9, 0, -10, 24], + [-4, 1, -3, 0, 2, -4, 4, 1, 5, 0, -3, 2, -3, -2, 2, -1, 1, 4, -1, -2, -2, 1, -1, -1, -4, -1, -4, -2, -6, 6, 12, 69], + [8, 5, 11, 0, -15, -4, 13, 6, 0, -4, 9, 1, -5, -3, 15, 0, 1, 6, -5, 0, 1, 6, 5, 8, 0, 7, 1, -1, -4, -11, -9, 41], + [-4, -9, 32, -6, 0, 7, -4, 6, -6, 1, -6, -2, 4, -8, -5, -3, -16, -1, -2, -6, 1, 15, 0, 21, 3, -3, -4, 3, -12, 16, 2, 27], + [-6, -5, 1, -9, -5, 3, 7, -3, 5, 5, 14, 13, 20, -7, -1, 12, -1, 10, -11, -11, -7, -4, -14, 7, -14, 13, 22, 18, -1, 0, 14, 28], + [-8, 3, -2, 0, 5, 6, -1, -4, 1, 3, -7, 3, 1, -15, 4, -9, 22, -10, -9, -4, 1, 8, -4, 9, -15, 2, -6, -4, -16, 12, -10, 23], + [0, 0, 2, 0, -1, 3, -3, -1, 3, -5, 7, 1, 5, -5, -8, 1, 13, -15, -5, -7, 12, -6, -2, 3, 10, -5, -8, 17, -5, -11, -14, 23], + [-7, -4, 6, -4, 5, -6, -5, 2, -4, 11, 9, -4, 2, -2, -4, 6, 15, 3, -3, 18, -15, -2, -6, 3, 3, -20, 17, 11, -4, 2, 3, 29], + [6, 1, -6, 2, 3, 0, 0, -3, 3, 3, -1, 3, -4, -6, -6, -7, -3, -2, -7, -2, -4, 5, 3, -5, -20, -13, -4, 10, -14, -29, 14, 37], + [3, 4, 3, -6, -4, 5, 0, 3, 2, 3, 0, -2, 4, 0, -3, -5, -4, 4, -4, 4, 4, 3, 1, -4, -4, -9, -14, 20, -30, 3, -18, 33], + [0, 2, 5, -2, -4, -2, -1, 2, -6, -3, -2, -2, 2, -5, -1, 4, 3, 2, -3, 0, -1, -1, -10, -7, 2, -4, -18, 2, -37, -1, 12, 40], + [-7, 2, -1, 0, -2, 4, -8, 1, -4, 12, 7, 4, 15, -7, 1, -9, 18, 0, 12, -17, -3, -1, 0, 0, 0, 2, -6, 0, -4, -3, -1, 26], + [-6, 4, 8, -5, -6, -2, 2, -1, 1, -1, -15, 8, 7, -1, -17, -4, 1, 5, 6, -11, -6, 14, 17, -5, -15, 11, 8, 0, -3, -15, -6, 28], + [-1, 0, 0, 0, 1, 0, -1, 0, 1, 3, 2, -2, 3, -1, -1, 2, 2, -1, -1, -7, 1, 2, -9, 0, -1, -4, -18, 7, -10, 49, -13, 32], + [-1, -3, 4, 1, 2, -5, 1, -7, -1, 5, -9, 4, 4, 25, 1, -1, 2, -5, 2, -7, 17, -2, 10, -5, 0, 2, -15, 3, -9, 7, -9, 30], + [-5, -1, 0, 2, 1, -1, 2, 5, -33, 3, -5, 14, 11, 7, 5, -3, 2, -8, -4, -2, -7, -6, 4, -8, -1, -8, 2, -2, -8, -1, -4, 27], + [-1, 0, -1, -2, 1, -1, -2, -1, 2, 0, 1, 2, 2, 4, 1, 3, 4, 2, 1, -7, -4, 1, -3, -4, -35, -25, 17, 10, -3, -26, -7, 32], + [-5, 1, 6, -2, 6, 6, -9, 3, -1, -4, 5, -4, -2, -2, -9, 2, -5, 2, 2, 4, 3, 5, -5, -16, -31, -12, -11, 2, -19, 20, -2, 21], + [-5, 2, 7, -7, -7, 5, -7, 2, 0, 0, -4, 3, -1, 0, -1, -2, 0, -3, 5, -11, -8, -3, -7, -7, 28, -11, -7, 0, -16, -11, -4, 29], + [2, 1, -3, -2, -1, 3, 4, 0, 1, 0, -1, -5, 4, -5, -12, 2, -2, -5, -22, -2, -1, 11, 8, -7, -12, 0, -34, 6, -5, 11, -8, 19], + [-1, -3, 5, 11, 18, -2, -2, -5, -2, 4, -1, 8, 5, -6, 1, -1, 2, 8, 4, -5, -8, -2, 5, -18, 7, 12, 7, 19, -18, 2, -6, -13], + [9, 0, 0, 5, 4, 3, -6, 4, 1, -4, 5, -1, -4, 8, 8, 6, -8, -6, 0, 6, -3, 3, 5, -3, 17, 31, 16, 10, -13, 0, -9, -19], + [12, -10, 2, -2, -2, -1, -3, 6, -12, -5, -2, 14, -16, 4, 12, 12, 17, 4, 7, -16, 7, -6, 11, 7, 7, 2, -25, 23, -24, 5, -7, -9], + [10, 4, 13, 10, 10, 3, -6, 3, 3, 2, -1, -6, 8, 4, 10, 0, 1, 2, -4, 2, -3, -8, 0, -1, 9, 9, -10, -3, -29, 1, -1, -27], + [2, 2, 0, 7, 9, -2, -10, -1, -1, 1, -9, -5, 8, 4, 1, 2, -10, 1, 13, 12, -3, 15, -9, 2, -7, 1, -10, 23, -20, -18, -9, -15], + [-3, -5, -1, 8, 0, -5, -1, 4, 7, -1, -7, 2, -8, -5, 11, 7, -6, 3, -3, -9, 7, 9, -22, 1, 6, -4, 14, 27, -25, -14, 3, -5], + [1, 3, 8, 4, 7, 6, 12, -17, -15, 1, -8, -10, 7, -14, -8, 6, -2, -2, -11, -11, -7, 13, -2, -2, 4, 5, -5, 13, -23, -6, -17, -8], + [-5, 4, -14, -5, -4, -5, 6, 5, -8, -5, -2, -11, -7, -12, 3, -11, 2, -6, 4, -10, -5, -7, 14, 5, 23, 11, 7, 12, -16, -6, -4, -16], + [5, 6, 2, 5, -2, -5, -5, -6, -5, -19, -13, -1, -3, -13, 5, 0, 6, -2, -2, -6, -7, -7, -1, -9, 4, 14, 17, -12, -27, 3, 0, -1], + [7, -1, 9, -10, 8, 2, -7, -2, 5, 2, -3, -7, 3, 0, 6, 4, 12, 5, 11, 14, -13, -1, 8, 1, 13, 9, 12, 12, -18, -14, -11, -16], + [-7, -5, -6, -5, 0, -1, -3, 2, 2, 1, 4, 9, 2, 3, 5, -2, 2, 1, 8, 0, 3, 0, -2, 2, 1, 7, 29, 0, -36, -5, -9, -21], + [14, -6, -9, 0, -1, -8, -8, -11, 2, 2, -9, -12, 12, -4, 5, 3, -5, -9, 11, -1, -3, 12, -21, -3, 12, 5, 3, 11, -18, -15, 1, -2], + [-1, 3, -9, -3, 7, -7, -18, 2, 4, 12, -10, 2, 8, -3, -14, 13, 17, -5, 5, -9, 13, -3, -7, -18, 17, -2, 5, 7, -20, -3, -6, -11], + [-3, 3, 3, -1, 1, -6, -5, 1, 5, -3, -14, -6, -5, -8, 14, -6, 7, -1, 5, 1, 15, -1, -7, -4, 6, -11, 9, -2, -37, 16, -7, -3], + [-1, 0, 6, 1, -3, -9, 0, 11, -8, 2, -2, 0, 5, 2, 12, -10, 10, 13, 2, 7, -6, 2, -10, -10, 21, -5, 5, 5, -12, -23, 3, -14], + [6, 0, -2, 1, 0, 1, 0, -4, 1, 1, 8, -2, 2, -5, -2, 1, 8, -4, -1, -1, 4, -1, 2, 6, 32, 1, -5, -20, -40, -4, -18, -14], + [2, 2, -7, -2, 4, 4, -1, 2, 0, -2, -4, -7, 3, 5, 0, -5, 1, 2, -6, 4, -1, -2, -1, -15, 8, 3, 9, 46, -7, -18, 6, -11], + [5, 5, 16, 21, 3, -11, -4, 11, -12, 2, 4, -12, -1, 11, 8, 1, -4, 11, -11, -21, 1, 1, -11, 3, 13, 1, 5, 12, -25, 1, -3, -2], + [1, 6, -7, 4, 2, 3, 1, -5, 8, 9, -15, 3, -3, -14, 17, 4, -8, 14, -2, -8, -4, 5, 8, -7, 8, 9, 7, 6, -29, -17, 8, 4], + [-7, -7, 4, 0, 13, 1, 0, 4, 4, -16, -10, -7, 5, 9, -15, -10, -10, 8, -4, -1, -11, -1, -10, -15, 3, 3, 14, 10, -19, 2, -18, -12], + [-4, 0, 2, 0, 5, -2, -9, 0, 4, -4, 2, -1, -2, 2, -4, 9, 2, -6, -4, -2, -1, -3, -3, -1, 2, 5, -1, 11, -24, -44, -9, -15], + [-1, -10, 6, 21, 11, 15, -7, 10, -14, -9, -8, -8, 4, 6, 19, 1, -6, 1, -5, -17, -8, -10, 9, 5, 11, 18, -1, 10, -16, -7, -9, -8], + [3, -5, 0, 0, -2, -2, -6, 4, -4, 1, -1, 0, 7, -3, 4, -4, -7, 7, 17, -20, 6, 4, 1, -6, -12, 31, 13, 19, -14, -10, -7, -2], + [-2, 6, -10, 3, 9, 6, -14, 15, 2, -5, 2, -11, 9, -8, 4, 6, 20, -15, -3, -3, -1, 32, -21, 6, 1, 9, 11, 17, -19, 6, -1, -3], + [8, 10, -2, 0, -8, -16, 7, 7, 6, 10, 4, -14, 7, -6, 21, -7, 10, 5, 5, 0, -7, 2, -6, 0, -7, 11, -9, 15, -20, -7, -11, 2], + [0, -7, 5, 2, 0, -3, -6, -4, -2, -1, -4, -5, -13, -1, 27, -9, -6, -11, -7, 1, 11, -4, -4, -14, -2, 11, 6, 10, -19, -6, -15, 2], + [0, 7, -1, 2, -7, -15, -2, -3, 13, -5, -5, 12, 3, 0, 5, -5, -22, 2, 7, 22, 13, 0, -1, 2, 3, 2, -7, 7, -27, -4, -4, -12], + [11, 1, -16, 6, -15, 1, 3, 2, 0, 2, -3, 2, 5, -2, -5, 9, 5, -3, 3, -2, -11, 3, 9, 6, 9, 3, -1, 12, -41, 8, -6, 9], + [3, -7, 3, 2, 5, 5, 0, -1, 1, 3, -5, -2, -13, 7, -1, -2, -2, -6, 4, -6, 0, 2, -2, 2, 4, 1, -4, 1, -47, -21, 7, -6], + [3, 16, -7, 13, -4, -2, 10, -3, -1, 18, -13, 7, -13, -4, 8, 4, 8, 9, -5, 13, 8, -5, 3, -6, 7, 18, -8, 10, -25, -3, -12, -12], + [1, -1, -1, 0, 2, 5, -5, -3, 0, -5, -1, 0, -4, -8, -2, 3, 2, -2, -17, -6, -4, 1, 33, -6, -20, -6, 8, 31, -26, -8, -1, -4], + [3, -3, -3, 5, -3, -2, 1, 7, 0, 3, 6, 3, 6, -2, 9, 15, -10, -3, -15, -5, -3, -4, -6, -30, 17, -8, -2, 2, -20, 0, -8, -2], + [-2, -1, -1, -1, 3, -5, -2, -3, 4, -2, 0, 5, 8, -3, 1, -4, 1, 1, -3, 4, 4, -14, 3, 11, -5, 3, -3, 7, -3, 13, 23, -16], + [2, -6, 1, -3, 5, 0, -6, -11, -7, -4, -1, 2, -7, -1, -1, 7, 1, -2, 6, 12, -6, 8, -13, 17, 25, -23, -19, -7, -12, 9, 16, -17], + [9, 4, 4, 4, -3, -1, 6, -2, -3, 0, 13, -4, -7, 14, 1, -7, 0, -5, 3, -19, -3, 5, 3, 9, -1, 9, -13, 13, -17, 4, 21, -26], + [0, -5, 0, 0, -4, -5, 2, -6, -4, 5, -7, 10, 0, 2, 0, -2, -2, 0, 4, -6, 7, -2, 6, 5, -5, 2, -12, 1, -29, 29, 27, 12], + [9, -10, -22, 6, -1, -1, 9, -14, -12, -2, 1, -1, 10, -11, -16, 0, 3, 11, 13, -14, -9, -2, -1, 6, 4, -14, 0, -10, -2, 16, 17, -11], + [2, 0, -1, -2, 4, 3, -6, -2, 1, -1, 1, 3, -4, 1, 3, -4, -1, -1, 4, -1, 1, 0, 1, 6, -5, -7, 2, 1, -47, -3, 50, -17], + [8, -4, -11, -7, 11, 11, 14, -7, 12, -7, 6, 2, 13, -6, -3, -2, -14, 6, 6, 6, 0, 2, -1, 5, -20, 2, -1, 4, -5, 6, 21, -11], + [-2, -9, 3, 0, -6, 7, 8, -8, 1, -3, 4, 1, 5, -2, -3, -7, 4, 7, -12, -9, -2, 10, -6, 13, 6, 5, 20, 2, -15, 9, 28, -7], + [0, -5, -6, -6, -6, 1, -6, 6, -2, 4, 8, -3, 12, -1, -4, -2, 6, 16, -14, 9, -14, -2, -8, -27, -3, 18, -1, -7, -3, 8, 23, -23], + [1, 4, -9, -1, -5, 10, -2, 1, -11, 1, -9, 4, 7, 14, -9, -2, -3, 2, -5, -1, -6, -10, -7, 11, 20, 2, 3, -19, 3, 15, 30, -9], + [7, 2, -14, -4, 0, -2, 5, 2, 5, -2, 8, -3, -7, 6, 6, -11, -14, 1, 10, -1, -7, -8, 1, 10, 3, -6, -15, -12, -17, 4, 30, -6], + [4, 2, 1, -2, 3, 0, 1, 0, 2, 0, 1, 6, -7, 0, 3, 4, 4, -4, -2, -5, -2, 2, -1, -2, 0, -2, -11, -7, -3, 42, 24, -14], + [4, 1, 3, 2, 0, -2, -3, -2, 2, -1, 4, 11, -2, 2, 3, -4, -5, 9, 2, -4, -9, 5, 8, -1, -7, 1, 24, -13, -28, 20, 15, -22], + [-3, 7, 6, 3, -2, -5, -10, -2, -2, -1, -6, -6, -2, -14, -16, -6, -5, 0, 18, 0, 9, 1, 7, -13, -5, -6, -9, 11, -15, 9, 22, -11], + [9, -2, 6, 5, 2, 9, -10, 1, 1, 5, -4, 12, 2, 2, -10, -7, -4, -6, 7, 9, 6, 15, 6, 6, -10, 10, 5, -13, -5, 6, 24, -12], + [1, 3, -3, -3, 8, 1, -6, 2, -5, -3, 7, 2, 14, 6, 9, -6, -5, -4, 27, 7, -3, 8, -6, 3, -8, 8, 22, -5, -6, -2, 22, -17], + [-2, -2, 3, 10, 9, 9, 12, -15, -1, -11, -13, 3, -2, 1, -3, -11, 7, 9, 16, -3, -10, -5, -5, 1, 8, -3, 9, 9, -5, 3, 31, -12], + [7, -5, 10, -4, -8, 2, 16, -2, 10, 10, -3, -2, 3, -8, -3, 3, -13, -6, 15, 20, -9, -3, -12, 1, -2, -16, 8, 8, -1, 16, 22, -5], + [5, -3, -15, -2, 12, -8, 8, -5, 2, -8, 20, -18, 14, -4, 3, 3, 7, -13, -16, 1, -10, 7, 16, 7, 4, -14, -4, -5, -9, 8, 23, -6], + [5, -4, -5, -4, 1, 8, 4, -7, -5, 8, 10, 6, -6, -10, -2, 6, 9, -17, -14, 11, 12, -3, -13, -7, 2, 18, 3, -25, -16, 18, 22, -5], + [5, 6, -7, -20, -4, 2, 8, 4, -24, -4, 1, 4, -5, -2, 1, -10, -2, 9, 3, -4, -3, -4, -4, -4, 10, 10, 3, 0, -6, 25, 21, -11], + [0, 7, -1, 14, -6, -4, -10, 5, 4, 4, 4, -5, 3, 4, -1, -7, 8, -19, 0, 6, 2, 3, -18, -3, -6, 2, 8, 14, -26, 22, 27, -13], + [-2, -6, 7, -5, 12, -7, 8, -1, 3, -2, 4, 1, 8, -2, 0, 14, 6, -5, 6, -4, -7, 7, -21, 8, 1, 8, -9, -4, -3, 11, 25, -13], + [4, 4, -1, -6, 4, 9, -8, 1, -3, -10, -2, 0, 15, -9, -16, 11, 1, 1, 6, 3, -9, -5, 16, 26, 1, -14, 1, -3, -14, 7, 15, -9], + [-12, -2, -9, -13, 2, 6, 14, 0, 1, 0, -1, -13, 0, 10, -1, 6, 9, -7, 8, 8, 19, 6, -1, 9, 10, -4, 1, -7, -22, -2, 29, -7], + [2, 4, 13, -12, -8, -4, -5, 13, 12, -5, -3, -3, -4, 1, -1, 10, 15, -6, -1, -11, -30, 4, 15, -1, 9, -7, 0, -2, -7, 10, 25, -16], + [7, -15, -7, -7, -1, -5, -5, -11, -20, 10, 3, -10, -3, 5, 20, -4, 0, -2, -2, 17, 2, 0, -3, 3, 6, 5, -1, -12, -3, 15, 22, -16], + [4, -1, 3, 4, -5, 0, -1, -5, -24, -29, 4, -9, 1, -3, 0, 0, 0, -4, 7, -4, -4, -4, 3, 1, -6, 5, -3, -5, -10, 3, 25, -10], + [-2, -1, -1, 4, 4, -1, 2, 0, -4, -4, 2, -1, -3, -1, -2, -2, 1, -3, -5, -1, 2, -3, -4, -4, -3, 5, -9, 1, -11, 7, 46, -46], + [0, -9, 3, 4, 4, 3, -5, -6, 5, -4, 4, -2, 1, 7, -4, -10, 13, 1, 3, -6, 4, -4, 7, 2, -19, -25, -3, -16, -12, 16, 20, -1], + [18, 6, 4, -12, 0, -14, 9, -6, -1, -4, -5, 2, 1, 12, 4, 2, 7, 0, 2, 5, -11, -5, -2, 2, -4, 10, 0, -9, -7, 9, 25, -8], + [5, 0, -6, 5, 6, 3, 3, -10, -5, 1, -1, 4, 3, -11, -8, 5, 4, -5, 5, -5, -7, -5, 11, 5, 20, -8, -16, 21, -4, 27, 23, -5], +]; diff --git a/crates/vendor/oxideav-dts/src/d10_vq.rs b/crates/vendor/oxideav-dts/src/d10_vq.rs new file mode 100644 index 00000000..69e7dbf3 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/d10_vq.rs @@ -0,0 +1,824 @@ +//! §D.10 Vector-Quantization code books — the wire facts (dimensions, +//! index widths, entry packing, element scaling), the structural +//! scanner for the §5.5 phase-1 high-frequency VQ indices, and — since +//! round 439 — the **books themselves**, built in +//! ([`VqCodebooks::builtin`]). +//! +//! ETSI TS 102 114 defines both books normatively but omits their +//! numeric contents, once for each of §D.10.1 and §D.10.2: "Due to +//! its extensive size, this table is not included here" (PDF p.255). +//! For a long time that was this crate's recorded docs gap +//! (`docs/audio/dts/dts-d10-vq-tables-GAP.md`, now **CLOSED**): the +//! GAP doc's container-level forensics proved the values were never in +//! the PDF in any form. The books are now staged as clean-room *data* +//! under `docs/audio/dts/tables/` — `dts-d10-1-adpcm-coeff-vq.csv` +//! (4096 × 4) and `dts-d10-2-hfreq-vq.csv` (1024 × 32), each with a +//! `.meta.md` sidecar; chain of custody in +//! `docs/audio/dts/provenance/11-extractor-d10-vq.md`, with two +//! independent sources agreeing on every value in both books. This +//! crate transcribes them in [`crate::d10_tables`] and exposes them as +//! ready-to-use books via [`HfVqCodebook::builtin`] / +//! [`AdpcmVqCodebook::builtin`] / [`VqCodebooks::builtin`] — the +//! default state of every decoder +//! ([`crate::SubframePcmDecoder::new`]), so `nVQSUB < nSUBS` (HF-VQ) +//! and `PMODE != 0` (ADPCM) subbands decode out of the box. The typed +//! [`crate::AudioArrayError::VqCodebookUnavailable`] refusal now fires +//! only when a caller explicitly strips the books +//! ([`VqCodebooks::none`]). +//! +//! What this module defines around the data: +//! +//! * the wire facts (index widths, book sizes, vector lengths) as +//! constants, from the §5.4/§5.5 walkers and the §D.10 definitions; +//! * the §D.10 entry-decoding primitives — +//! [`unpack_hfreq_vq_entry`] (16-bit entry → two 8-bit signed +//! elements, low byte first, **each ÷ 2⁴**) and [`adpcm_vq_coeff`] +//! (stored integer ÷ 2¹³, spec anchor: entry `9928` → +//! `1.2119140625`); +//! * [`scan_hf_vq_indices_at`], the purely structural §5.5 phase-1 +//! walk (`nVQIndex = ExtractBits(10)` per HF subband) that captures +//! the indices the lookup consumes; +//! * the caller-supplied-book containers ([`HfVqCodebook`] / +//! [`AdpcmVqCodebook`], rounds 408/434) that the built-in books now +//! flow through unchanged. +//! +//! ## The §D.10.2 divisor is `2^4 = 16` — a spec typo, corrected +//! +//! The spec's p.255 text renders the §D.10.2 element divisor as `24`, +//! and rounds 408/9 pinned the *rendering* carefully (the `24` sits on +//! the text baseline, unlike §D.10.1's `2^13` whose exponent is a +//! raised superscript) and carried the literal reading. The staged +//! recovery record (`tables/dts-d10-2-hfreq-vq.meta.md`) contradicts +//! the literal reading three independent ways and settles the divisor +//! as **`2^4 = 16`** — a `2^4` whose superscript was lost in +//! typesetting, giving §D.10.2 exactly the same form as §D.10.1's +//! `2^13`. Using the literal 24 costs a constant `16/24 = 2/3` gain +//! error on every VQ-coded HF subband. The same record settles the +//! intra-entry element order the spec never pinned: element `2k` is +//! entry `k`'s **low** byte. Both corrections are confirmed end to end +//! by the black-box reference decode of the §D.10-bearing fixture +//! (`tests/black_box_d10.rs`). + +use crate::bitreader::BitReader; +use crate::Result; + +// ------------------------------------------------------------------ +// §D.10.2 — High-Frequency Subband VQ (`HFreqVQ`) +// ------------------------------------------------------------------ + +/// §D.10.2 code-book size: `2^10 = 1024` vectors. +pub const HFREQ_VQ_BOOK_SIZE: usize = 1024; + +/// Width of the §5.5 phase-1 `nVQIndex` bitstream field +/// (`nVQIndex = ExtractBits(10)`, Table 5-29). +pub const HFREQ_VQ_INDEX_BITS: u32 = 10; + +/// Elements per §D.10.2 vector: 32 subband samples (one subband +/// analysis window). +pub const HFREQ_VQ_VECTOR_LEN: usize = 32; + +/// 16-bit table entries per §D.10.2 vector: each entry packs **two** +/// vector elements, so 16 entries make one 32-element vector. +pub const HFREQ_VQ_ENTRIES_PER_VECTOR: usize = HFREQ_VQ_VECTOR_LEN / 2; + +/// The §D.10.2 element divisor: each 8-bit signed integer unpacked +/// from a 16-bit entry is divided by `2^4 = 16` to give a vector +/// element. The spec's p.255 text renders this as `24` — a `2^4` with +/// a lost superscript; see the module docs and +/// `docs/audio/dts/tables/dts-d10-2-hfreq-vq.meta.md` for the +/// three-way evidence that corrected the earlier literal-24 reading. +pub const HFREQ_VQ_ELEMENT_DIVISOR: f64 = 16.0; + +/// Decode one 16-bit §D.10.2 `HFreqVQ` table entry into its two +/// vector elements: split into two 8-bit signed integers, each +/// divided by [`HFREQ_VQ_ELEMENT_DIVISOR`] (= `2^4`). +/// +/// Returned as `[low-byte element, high-byte element]`: element `2k` +/// of a vector is entry `k`'s **low** byte, element `2k+1` its high +/// byte. The spec defines the packing ("each table entry is 16 bits = +/// two packed vector elements") but publishes no anchor pinning which +/// byte is the earlier vector element; the staged recovery record +/// settles it (see `docs/audio/dts/tables/dts-d10-2-hfreq-vq.meta.md`, +/// "Intra-entry byte order" — two independent sources agree). +#[must_use] +pub fn unpack_hfreq_vq_entry(entry: u16) -> [f64; 2] { + let lo = entry as u8 as i8; + let hi = (entry >> 8) as u8 as i8; + [ + f64::from(lo) / HFREQ_VQ_ELEMENT_DIVISOR, + f64::from(hi) / HFREQ_VQ_ELEMENT_DIVISOR, + ] +} + +// ------------------------------------------------------------------ +// §D.10.1 — ADPCM Coefficient VQ (`ADPCMCoeffVQ`) +// ------------------------------------------------------------------ + +/// §D.10.1 code-book size: `2^12 = 4096` vectors. +pub const ADPCM_VQ_BOOK_SIZE: usize = 4096; + +/// Width of the §5.4 `PVQ` index bitstream field +/// (`nVQIndex = ExtractBits(12)`). +pub const ADPCM_VQ_INDEX_BITS: u32 = 12; + +/// Elements per §D.10.1 vector: the 4 ADPCM subband-prediction +/// coefficients (`PVQ[ch][n]`, consumed by the §C.2.2 predictor). +pub const ADPCM_VQ_VECTOR_LEN: usize = 4; + +/// The §D.10.1 stored-entry scaling divisor: the actual coefficient +/// is the stored signed integer divided by `2^13 = 8192`. +pub const ADPCM_VQ_COEFF_DIVISOR: f64 = 8192.0; + +/// Scale a §D.10.1 `ADPCMCoeffVQ` stored integer entry to the actual +/// prediction coefficient: `entry / 2^13`. +/// +/// The spec's single published anchor: entry `9928` → +/// `9928 / 2^13 = 1.2119140625` (§D.10.1, PDF p.255). +#[must_use] +pub fn adpcm_vq_coeff(entry: i32) -> f64 { + f64::from(entry) / ADPCM_VQ_COEFF_DIVISOR +} + +// ------------------------------------------------------------------ +// Drop-in containers for recovered §D.10 books +// ------------------------------------------------------------------ + +/// A caller-supplied §D.10 code book had the wrong shape (vector +/// count or vector length). The §D.10 dimensions are wire facts — +/// 1024 × 32 for `HFreqVQ`, 4096 × 4 for `ADPCMCoeffVQ` — so a +/// mis-shaped book is rejected up front rather than silently +/// truncated or padded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VqCodebookShapeError { + /// The §D.10 book size the constructor requires. + pub expected_vectors: usize, + /// The vector count the caller supplied. + pub got_vectors: usize, +} + +impl core::fmt::Display for VqCodebookShapeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "oxideav-dts: §D.10 VQ code book has {} vectors, expected {}", + self.got_vectors, self.expected_vectors + ) + } +} + +impl std::error::Error for VqCodebookShapeError {} + +/// A decoded §D.10.2 `HFreqVQ` high-frequency-subband code book: +/// [`HFREQ_VQ_BOOK_SIZE`] vectors of [`HFREQ_VQ_VECTOR_LEN`] scaled +/// elements, ready for the §5.5 phase-1 +/// `HFreqVQ.LookUp(nVQIndex, HFREQ[ch][n])`. +/// +/// [`HfVqCodebook::builtin`] returns the real book, transcribed from +/// the staged clean-room table +/// (`docs/audio/dts/tables/dts-d10-2-hfreq-vq.csv`); the caller- +/// supplied constructors remain for tests and experimentation. +/// Everything *around* the numbers — the 10-bit index, the 1024 × 32 +/// dimensions, the two-int8-per-entry packing, the ÷ 2⁴ element +/// scaling — is spec-pinned and enforced here. +#[derive(Clone)] +pub struct HfVqCodebook { + vectors: Vec<[f64; HFREQ_VQ_VECTOR_LEN]>, +} + +impl core::fmt::Debug for HfVqCodebook { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("HfVqCodebook") + .field("vectors", &self.vectors.len()) + .field("vector_len", &HFREQ_VQ_VECTOR_LEN) + .finish() + } +} + +impl HfVqCodebook { + /// The real §D.10.2 `HFreqVQ` book, built once (and shared) from + /// the in-crate transcription of the staged clean-room table + /// `docs/audio/dts/tables/dts-d10-2-hfreq-vq.csv` (see + /// [`crate::d10_tables`] for the provenance chain): 1024 × 32 + /// int8 elements, each ÷ 2⁴ ([`HFREQ_VQ_ELEMENT_DIVISOR`]). + #[must_use] + pub fn builtin() -> std::sync::Arc { + static BOOK: std::sync::OnceLock> = std::sync::OnceLock::new(); + BOOK.get_or_init(|| { + let vectors = crate::d10_tables::HFREQ_VQ_TABLE + .iter() + .map(|row| row.map(|e| f64::from(e) / HFREQ_VQ_ELEMENT_DIVISOR)) + .collect(); + std::sync::Arc::new(Self { vectors }) + }) + .clone() + } + + /// Build the book from its raw 16-bit table entries — + /// [`HFREQ_VQ_ENTRIES_PER_VECTOR`] (= 16) entries per vector, each + /// unpacked to two elements via [`unpack_hfreq_vq_entry`] + /// (low-byte element first, then high-byte element — the recovered + /// intra-entry order; a caller holding elements in vector order + /// can use [`HfVqCodebook::from_elements`] instead). + /// + /// # Errors + /// + /// [`VqCodebookShapeError`] unless exactly + /// [`HFREQ_VQ_BOOK_SIZE`] vectors are supplied. + pub fn from_packed_entries( + entries: &[[u16; HFREQ_VQ_ENTRIES_PER_VECTOR]], + ) -> core::result::Result { + if entries.len() != HFREQ_VQ_BOOK_SIZE { + return Err(VqCodebookShapeError { + expected_vectors: HFREQ_VQ_BOOK_SIZE, + got_vectors: entries.len(), + }); + } + let vectors = entries + .iter() + .map(|packed| { + let mut v = [0.0_f64; HFREQ_VQ_VECTOR_LEN]; + for (pair, out) in packed.iter().zip(v.chunks_exact_mut(2)) { + out.copy_from_slice(&unpack_hfreq_vq_entry(*pair)); + } + v + }) + .collect(); + Ok(Self { vectors }) + } + + /// Build the book from already-decoded vector elements (the ÷ 2⁴ + /// scaling already applied, in vector-element order). + /// + /// # Errors + /// + /// [`VqCodebookShapeError`] unless exactly + /// [`HFREQ_VQ_BOOK_SIZE`] vectors are supplied. + pub fn from_elements( + vectors: &[[f64; HFREQ_VQ_VECTOR_LEN]], + ) -> core::result::Result { + if vectors.len() != HFREQ_VQ_BOOK_SIZE { + return Err(VqCodebookShapeError { + expected_vectors: HFREQ_VQ_BOOK_SIZE, + got_vectors: vectors.len(), + }); + } + Ok(Self { + vectors: vectors.to_vec(), + }) + } + + /// Look up one 32-element vector by its 10-bit `nVQIndex`. Every + /// wire-representable index (`0..1024`) is in range, so the §5.5 + /// phase-1 lookup cannot fail on a well-shaped book. + #[must_use] + pub fn vector(&self, index: u16) -> &[f64; HFREQ_VQ_VECTOR_LEN] { + &self.vectors[usize::from(index) % HFREQ_VQ_BOOK_SIZE] + } +} + +/// A decoded §D.10.1 `ADPCMCoeffVQ` prediction-coefficient code book: +/// [`ADPCM_VQ_BOOK_SIZE`] vectors of [`ADPCM_VQ_VECTOR_LEN`] (= 4) +/// scaled coefficients, ready for the §5.4.1 +/// `ADPCMCoeffVQ.LookUp(nVQIndex, PVQ[ch][n])` that feeds the §C.2.2 +/// inverse-ADPCM predictor. +/// +/// Like [`HfVqCodebook`], the real book is built in +/// ([`AdpcmVqCodebook::builtin`], from +/// `docs/audio/dts/tables/dts-d10-1-adpcm-coeff-vq.csv`); the +/// caller-supplied constructors remain for tests. +#[derive(Clone)] +pub struct AdpcmVqCodebook { + coeffs: Vec<[f64; ADPCM_VQ_VECTOR_LEN]>, +} + +impl core::fmt::Debug for AdpcmVqCodebook { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("AdpcmVqCodebook") + .field("vectors", &self.coeffs.len()) + .field("vector_len", &ADPCM_VQ_VECTOR_LEN) + .finish() + } +} + +impl AdpcmVqCodebook { + /// The real §D.10.1 `ADPCMCoeffVQ` book, built once (and shared) + /// from the in-crate transcription of the staged clean-room table + /// `docs/audio/dts/tables/dts-d10-1-adpcm-coeff-vq.csv` (see + /// [`crate::d10_tables`]): 4096 × 4 signed Q13 integers, each + /// ÷ 2¹³ ([`adpcm_vq_coeff`]; spec anchor `9928` → + /// `1.2119140625` at index 0 element 0). + #[must_use] + pub fn builtin() -> std::sync::Arc { + static BOOK: std::sync::OnceLock> = + std::sync::OnceLock::new(); + BOOK.get_or_init(|| { + let coeffs = crate::d10_tables::ADPCM_VQ_TABLE + .iter() + .map(|row| row.map(|e| adpcm_vq_coeff(i32::from(e)))) + .collect(); + std::sync::Arc::new(Self { coeffs }) + }) + .clone() + } + + /// Build the book from its raw stored-integer entries, applying + /// the §D.10.1 ÷ 2¹³ scaling ([`adpcm_vq_coeff`]) to each element. + /// + /// # Errors + /// + /// [`VqCodebookShapeError`] unless exactly + /// [`ADPCM_VQ_BOOK_SIZE`] vectors are supplied. + pub fn from_entries( + entries: &[[i32; ADPCM_VQ_VECTOR_LEN]], + ) -> core::result::Result { + if entries.len() != ADPCM_VQ_BOOK_SIZE { + return Err(VqCodebookShapeError { + expected_vectors: ADPCM_VQ_BOOK_SIZE, + got_vectors: entries.len(), + }); + } + let coeffs = entries + .iter() + .map(|stored| stored.map(adpcm_vq_coeff)) + .collect(); + Ok(Self { coeffs }) + } + + /// Build the book from already-scaled coefficients. + /// + /// # Errors + /// + /// [`VqCodebookShapeError`] unless exactly + /// [`ADPCM_VQ_BOOK_SIZE`] vectors are supplied. + pub fn from_coefficients( + vectors: &[[f64; ADPCM_VQ_VECTOR_LEN]], + ) -> core::result::Result { + if vectors.len() != ADPCM_VQ_BOOK_SIZE { + return Err(VqCodebookShapeError { + expected_vectors: ADPCM_VQ_BOOK_SIZE, + got_vectors: vectors.len(), + }); + } + Ok(Self { + coeffs: vectors.to_vec(), + }) + } + + /// Look up the four §C.2.2 predictor coefficients by the 12-bit + /// `PVQ` index. Every wire-representable index (`0..4096`) is in + /// range, so the lookup cannot fail on a well-shaped book. + #[must_use] + pub fn coefficients(&self, index: u16) -> &[f64; ADPCM_VQ_VECTOR_LEN] { + &self.coeffs[usize::from(index) % ADPCM_VQ_BOOK_SIZE] + } +} + +/// The (optional) pair of §D.10 code books a decoder carries. +/// +/// [`VqCodebooks::builtin`] — **both real books** — is what every +/// decoder now starts with ([`crate::SubframePcmDecoder::new`]). +/// [`VqCodebooks::none`] (also `Default`, kept for source +/// compatibility with the drop-in era) strips them, restoring the +/// typed [`crate::AudioArrayError::VqCodebookUnavailable`] refusal on +/// the affected sub-paths. The books are held behind +/// [`std::sync::Arc`] so a stream decoder clone (the all-or-nothing +/// decode pattern) does not copy the table data — and the built-in +/// books are additionally process-wide singletons. +#[derive(Debug, Clone, Default)] +pub struct VqCodebooks { + /// The §D.10.2 high-frequency-subband book (`HFreqVQ`). + pub hfreq: Option>, + /// The §D.10.1 ADPCM prediction-coefficient book + /// (`ADPCMCoeffVQ`). + pub adpcm: Option>, +} + +impl VqCodebooks { + /// Both real §D.10 books ([`HfVqCodebook::builtin`] + + /// [`AdpcmVqCodebook::builtin`]) — the default state of every + /// decoder. + #[must_use] + pub fn builtin() -> Self { + Self { + hfreq: Some(HfVqCodebook::builtin()), + adpcm: Some(AdpcmVqCodebook::builtin()), + } + } + + /// No books: `nVQSUB < nSUBS` / `PMODE != 0` subbands surface the + /// typed [`crate::AudioArrayError::VqCodebookUnavailable`] + /// refusal (the pre-round-439 shipped state). + #[must_use] + pub fn none() -> Self { + Self::default() + } + + /// `true` when neither book is present. + #[must_use] + pub fn is_empty(&self) -> bool { + self.hfreq.is_none() && self.adpcm.is_none() + } + + /// Attach a §D.10.2 `HFreqVQ` book. + #[must_use] + pub fn with_hfreq(mut self, book: HfVqCodebook) -> Self { + self.hfreq = Some(std::sync::Arc::new(book)); + self + } + + /// Attach a §D.10.1 `ADPCMCoeffVQ` book. + #[must_use] + pub fn with_adpcm(mut self, book: AdpcmVqCodebook) -> Self { + self.adpcm = Some(std::sync::Arc::new(book)); + self + } +} + +// ------------------------------------------------------------------ +// §5.5 phase 1 — structural HF-VQ index scan +// ------------------------------------------------------------------ + +/// Walk the §5.5 Table 5-29 phase-1 high-frequency VQ region +/// structurally, capturing the 10-bit `nVQIndex` of every HF-VQ +/// subband without attempting the (gap-blocked) `HFreqVQ.LookUp`. +/// +/// Per the corrected walker trace +/// (`docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §2.1): +/// +/// ```text +/// for (ch = 0; ch < nPCHS; ch++) +/// for (n = nVQSUB[ch]; n < nSUBS[ch]; n++) +/// nVQIndex = ExtractBits(10); // then HFreqVQ.LookUp(...) +/// ``` +/// +/// * `bytes` / `bit_offset` — positioned at the first §5.5 bit (the +/// phase-1 region precedes the LFE phase and the audio-data +/// arrays). +/// * `n_vqsub` / `n_subs` — the per-channel loop bounds +/// ([`crate::AudioCodingHeader::n_vqsub`] / `n_subs`); slices of +/// equal length, one entry per primary channel. +/// +/// Returns `(indices, bits_consumed)` where `indices[ch]` holds the +/// captured 10-bit indices for channel `ch`'s subbands +/// `nVQSUB[ch]..nSUBS[ch]` in walk order (empty when the channel has +/// no HF-VQ subbands — the common Core case where +/// `nVQSUB == nSUBS`). `bits_consumed` is exactly +/// `10 · Σ (nSUBS[ch] − nVQSUB[ch])`, letting a caller advance its +/// cursor to the §5.5 LFE phase. +/// +/// The full decode ([`crate::SubframePcmDecoder`]) looks the captured +/// indices up in the built-in §D.10.2 book; this structural scan +/// remains useful for stream inspection. +/// +/// # Errors +/// +/// [`crate::Error::UnexpectedEof`] on a truncated region. +pub fn scan_hf_vq_indices_at( + bytes: &[u8], + bit_offset: usize, + n_vqsub: &[usize], + n_subs: &[usize], +) -> Result<(Vec>, usize)> { + debug_assert_eq!(n_vqsub.len(), n_subs.len()); + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + + let mut indices = Vec::with_capacity(n_vqsub.len()); + for (&vqsub, &subs) in n_vqsub.iter().zip(n_subs) { + let mut ch_indices = Vec::new(); + for _ in vqsub..subs { + ch_indices.push(br.read_bits(HFREQ_VQ_INDEX_BITS)? as u16); + } + indices.push(ch_indices); + } + + let bits_consumed = br.absolute_bit_position() - bit_offset; + Ok((indices, bits_consumed)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Error; + + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + /// The §D.10.1 anchor printed in the spec: stored entry 9928 → + /// coefficient 1.2119140625 (= 9928 / 2^13). + #[test] + fn adpcm_anchor_entry_9928() { + assert_eq!(adpcm_vq_coeff(9928), 1.2119140625); + assert_eq!(adpcm_vq_coeff(0), 0.0); + assert_eq!(adpcm_vq_coeff(-8192), -1.0); + } + + /// §D.10.2 entry unpacking: two 8-bit signed halves, low byte + /// first, each divided by 2⁴ = 16. + #[test] + fn hfreq_entry_unpacks_two_signed_bytes_low_first_over_16() { + // lo = 0x10 = +16 -> 1.0; hi = 0xF0 = -16 -> -1.0. + assert_eq!(unpack_hfreq_vq_entry(0xF010), [1.0, -1.0]); + // Zero entry -> two zero elements. + assert_eq!(unpack_hfreq_vq_entry(0), [0.0, 0.0]); + // lo = 0x80 = -128 -> -8.0; hi = 0x7F = +127 -> 127/16. + let [a, b] = unpack_hfreq_vq_entry(0x7F80); + assert_eq!(a, -8.0); + assert!((b - 127.0 / 16.0).abs() < 1e-15); + } + + /// The book/vector dimensional facts hold together: 16 two-element + /// entries per 32-element vector; 10 bits address 1024 vectors; + /// 12 bits address 4096. + #[test] + fn dimensional_facts_consistent() { + assert_eq!(HFREQ_VQ_ENTRIES_PER_VECTOR * 2, HFREQ_VQ_VECTOR_LEN); + assert_eq!(1usize << HFREQ_VQ_INDEX_BITS, HFREQ_VQ_BOOK_SIZE); + assert_eq!(1usize << ADPCM_VQ_INDEX_BITS, ADPCM_VQ_BOOK_SIZE); + } + + /// The structural scan reads exactly 10 bits per HF-VQ subband in + /// (ch, n) walk order and reports the consumed bit count. + #[test] + fn scan_captures_indices_in_walk_order() { + // ch0: nVQSUB=2, nSUBS=4 -> 2 indices; ch1: 3..3 -> none; + // ch2: 0..2 -> 2 indices. + let vals = [0x3FFu32, 0x001, 0x155, 0x2AA]; + let fields: Vec<(u32, u8)> = vals.iter().map(|&v| (v, 10u8)).collect(); + let stream = pack_fields(&fields); + let (idx, bits) = scan_hf_vq_indices_at(&stream, 0, &[2, 3, 0], &[4, 3, 2]).unwrap(); + assert_eq!(bits, 40); + assert_eq!(idx, vec![vec![0x3FF, 0x001], vec![], vec![0x155, 0x2AA]]); + } + + /// A non-byte-aligned start cursor is honoured (the §5.5 region + /// rarely begins on a byte boundary). + #[test] + fn scan_honours_bit_offset() { + let fields = [(0b101u32, 3u8), (0x2AB, 10)]; + let stream = pack_fields(&fields); + let (idx, bits) = scan_hf_vq_indices_at(&stream, 3, &[1], &[2]).unwrap(); + assert_eq!(bits, 10); + assert_eq!(idx, vec![vec![0x2AB]]); + } + + /// The common Core case (`nVQSUB == nSUBS` everywhere) consumes + /// zero bits. + #[test] + fn scan_empty_when_no_hf_vq_subbands() { + let (idx, bits) = scan_hf_vq_indices_at(&[0u8; 4], 0, &[2, 4], &[2, 4]).unwrap(); + assert_eq!(bits, 0); + assert_eq!(idx, vec![Vec::::new(), Vec::new()]); + } + + /// [`HfVqCodebook::from_packed_entries`] applies the §D.10.2 + /// two-int8 ÷ 2⁴ unpacking to every entry, low byte first, and + /// the 10-bit lookup returns the decoded vector. + #[test] + fn hf_book_from_packed_entries_decodes_all_elements() { + let mut entries = vec![[0u16; HFREQ_VQ_ENTRIES_PER_VECTOR]; HFREQ_VQ_BOOK_SIZE]; + // Vector 5: entry k packs (lo = k+1, hi = -(k+1)). + for (k, e) in entries[5].iter_mut().enumerate() { + let lo = (k as i8 + 1) as u8; + let hi = (-(k as i8 + 1)) as u8; + *e = (u16::from(hi) << 8) | u16::from(lo); + } + let book = HfVqCodebook::from_packed_entries(&entries).unwrap(); + let v = book.vector(5); + for k in 0..HFREQ_VQ_ENTRIES_PER_VECTOR { + let want = f64::from(k as i8 + 1) / HFREQ_VQ_ELEMENT_DIVISOR; + assert_eq!(v[2 * k], want); + assert_eq!(v[2 * k + 1], -want); + } + // Every other vector decodes to zeros. + assert!(book.vector(0).iter().all(|&x| x == 0.0)); + assert!(book.vector(1023).iter().all(|&x| x == 0.0)); + } + + /// The book constructors reject wrong vector counts with the + /// typed shape error (the §D.10 dimensions are wire facts). + #[test] + fn book_constructors_reject_wrong_shapes() { + let short_hf = vec![[0u16; HFREQ_VQ_ENTRIES_PER_VECTOR]; 1023]; + assert_eq!( + HfVqCodebook::from_packed_entries(&short_hf).unwrap_err(), + VqCodebookShapeError { + expected_vectors: HFREQ_VQ_BOOK_SIZE, + got_vectors: 1023 + } + ); + let long_adpcm = vec![[0i32; ADPCM_VQ_VECTOR_LEN]; ADPCM_VQ_BOOK_SIZE + 1]; + assert_eq!( + AdpcmVqCodebook::from_entries(&long_adpcm).unwrap_err(), + VqCodebookShapeError { + expected_vectors: ADPCM_VQ_BOOK_SIZE, + got_vectors: ADPCM_VQ_BOOK_SIZE + 1 + } + ); + assert!( + HfVqCodebook::from_elements(&vec![[0.0; HFREQ_VQ_VECTOR_LEN]; HFREQ_VQ_BOOK_SIZE]) + .is_ok() + ); + assert!(AdpcmVqCodebook::from_coefficients(&vec![ + [0.0; ADPCM_VQ_VECTOR_LEN]; + ADPCM_VQ_BOOK_SIZE + ]) + .is_ok()); + } + + /// [`AdpcmVqCodebook::from_entries`] applies the §D.10.1 ÷ 2¹³ + /// scaling — pinned by the spec's own printed anchor. + #[test] + fn adpcm_book_applies_divisor_with_spec_anchor() { + let mut entries = vec![[0i32; ADPCM_VQ_VECTOR_LEN]; ADPCM_VQ_BOOK_SIZE]; + entries[4095] = [9928, -8192, 0, 4096]; + let book = AdpcmVqCodebook::from_entries(&entries).unwrap(); + assert_eq!(book.coefficients(4095), &[1.2119140625, -1.0, 0.0, 0.5]); + assert_eq!(book.coefficients(0), &[0.0; 4]); + } + + /// `VqCodebooks` defaults to the shipped no-books state and the + /// builder methods attach each book independently. + #[test] + fn vq_codebooks_default_is_empty() { + let none = VqCodebooks::none(); + assert!(none.is_empty()); + assert!(none.hfreq.is_none() && none.adpcm.is_none()); + + let hf = HfVqCodebook::from_elements(&vec![[0.0; HFREQ_VQ_VECTOR_LEN]; HFREQ_VQ_BOOK_SIZE]) + .unwrap(); + let with_hf = VqCodebooks::none().with_hfreq(hf); + assert!(!with_hf.is_empty()); + assert!(with_hf.hfreq.is_some() && with_hf.adpcm.is_none()); + } + + /// The built-in §D.10.1 book reproduces the spec's only printed + /// anchor (index 0 element 0: entry `9928` → `1.2119140625`) and + /// the staged table's pinned sample rows (`.meta.md` "Sample + /// values"). + #[test] + fn builtin_adpcm_book_matches_staged_table_anchors() { + let book = AdpcmVqCodebook::builtin(); + assert_eq!( + book.coefficients(0), + &[9928, -2618, -1093, -1263].map(|e| f64::from(e) / 8192.0) + ); + assert_eq!(book.coefficients(0)[0], 1.2119140625); + assert_eq!( + book.coefficients(1), + &[11077, -2876, -1747, -308].map(|e| f64::from(e) / 8192.0) + ); + assert_eq!( + book.coefficients(4095), + &[8538, -6997, 5309, 453].map(|e| f64::from(e) / 8192.0) + ); + } + + /// The transcribed §D.10.1 table reproduces the staged `.meta.md` + /// verification facts: stored range `-21806 … 21657`, and all + /// 4096 vectors distinct. + #[test] + fn builtin_adpcm_table_range_and_distinctness() { + let table = &crate::d10_tables::ADPCM_VQ_TABLE; + let min = table.iter().flatten().min().unwrap(); + let max = table.iter().flatten().max().unwrap(); + assert_eq!((*min, *max), (-21806, 21657)); + let distinct: std::collections::HashSet<[i16; 4]> = table.iter().copied().collect(); + assert_eq!(distinct.len(), 4096, "all §D.10.1 vectors are distinct"); + } + + /// Whether every root of the degree-4 predictor polynomial + /// `A(z) = 1 − Σ cₖ z^(−k−1)` lies strictly inside radius `m`, + /// via the Schur–Cohn (step-down) recursion on `A(m·z)` — all + /// reflection coefficients strictly inside the unit disc. + fn predictor_roots_inside(coeffs: &[f64; 4], m: f64) -> bool { + // A(m·z) in powers of z^{-1}: aᵢ = −c_{i−1} · m^{−i}, a₀ = 1. + let mut a = [1.0, 0.0, 0.0, 0.0, 0.0]; + for (i, &c) in coeffs.iter().enumerate() { + a[i + 1] = -c / m.powi(i as i32 + 1); + } + let mut n = 4; + while n > 0 { + let k = a[n]; + if k.abs() >= 1.0 { + return false; + } + let denom = 1.0 - k * k; + let prev = a; + for (i, slot) in a.iter_mut().enumerate().take(n).skip(1) { + *slot = (prev[i] - k * prev[n - i]) / denom; + } + n -= 1; + } + true + } + + /// The staged `.meta.md` stability fact, re-proved on the + /// transcription: every one of the 4096 §D.10.1 vectors is a + /// strictly minimum-phase fourth-order predictor, with the + /// largest root modulus bracketed around the recorded `0.98702` + /// (all inside radius 0.988; not all inside 0.986). A mis-framed + /// transcription (wrong stride/order/signedness) does not produce + /// 4096 consecutive stable predictors with that exact margin. + #[test] + fn builtin_adpcm_predictors_all_minimum_phase_with_recorded_margin() { + let book = AdpcmVqCodebook::builtin(); + let mut inside_0986 = 0usize; + for idx in 0..ADPCM_VQ_BOOK_SIZE { + let coeffs = book.coefficients(idx as u16); + assert!( + predictor_roots_inside(coeffs, 1.0), + "vector {idx} is not minimum-phase" + ); + assert!( + predictor_roots_inside(coeffs, 0.988), + "vector {idx} has a root beyond the recorded 0.98702 margin" + ); + if predictor_roots_inside(coeffs, 0.986) { + inside_0986 += 1; + } + } + assert!( + inside_0986 < ADPCM_VQ_BOOK_SIZE, + "some vector must reach past 0.986 (recorded max modulus 0.98702)" + ); + } + + /// The built-in §D.10.2 book reproduces the staged table's pinned + /// sample rows, with the ÷ 2⁴ scaling applied. + #[test] + fn builtin_hf_book_matches_staged_table_anchors() { + let book = HfVqCodebook::builtin(); + assert!( + book.vector(0).iter().all(|&e| e == 0.0), + "index 0 is the zero vector" + ); + let v1 = book.vector(1); + let want1 = [-4, -2, 2, 1, -16, -10, 1, 3].map(|e| f64::from(e) / 16.0); + assert_eq!(&v1[..8], &want1); + let v1023 = book.vector(1023); + let want1023 = [5, 0, -6, 5, 6, 3, 3, -10].map(|e| f64::from(e) / 16.0); + assert_eq!(&v1023[..8], &want1023); + } + + /// The transcribed §D.10.2 table reproduces the staged `.meta.md` + /// verification facts: element range `-87 … 89`, exactly one zero + /// vector, and 996 distinct patterns (28 genuine duplicate code + /// words, clustered in the recovered book). + #[test] + fn builtin_hf_table_range_zero_vector_and_duplicates() { + let table = &crate::d10_tables::HFREQ_VQ_TABLE; + let min = table.iter().flatten().min().unwrap(); + let max = table.iter().flatten().max().unwrap(); + assert_eq!((*min, *max), (-87, 89)); + let zero_vectors = table + .iter() + .filter(|row| row.iter().all(|&e| e == 0)) + .count(); + assert_eq!(zero_vectors, 1, "index 0 is the only zero vector"); + let distinct: std::collections::HashSet<[i8; 32]> = table.iter().copied().collect(); + assert_eq!(distinct.len(), 996, "996 distinct patterns / 28 duplicates"); + } + + /// The built-in books are process-wide singletons (one build, one + /// allocation, shared by every decoder). + #[test] + fn builtin_books_are_shared_singletons() { + assert!(std::sync::Arc::ptr_eq( + &HfVqCodebook::builtin(), + &HfVqCodebook::builtin() + )); + assert!(std::sync::Arc::ptr_eq( + &AdpcmVqCodebook::builtin(), + &AdpcmVqCodebook::builtin() + )); + let books = VqCodebooks::builtin(); + assert!(!books.is_empty()); + assert!(books.hfreq.is_some() && books.adpcm.is_some()); + } + + /// A truncated region reports EOF rather than fabricating indices. + #[test] + fn scan_reports_eof_on_truncation() { + let stream = [0u8; 1]; // 8 bits; a single index needs 10. + assert_eq!( + scan_hf_vq_indices_at(&stream, 0, &[0], &[1]).unwrap_err(), + Error::UnexpectedEof + ); + } +} diff --git a/crates/vendor/oxideav-dts/src/d6_block_book.rs b/crates/vendor/oxideav-dts/src/d6_block_book.rs new file mode 100644 index 00000000..11e56756 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/d6_block_book.rs @@ -0,0 +1,560 @@ +//! DTS Coherent Acoustics — Annex D §D.6 Block Code Books and the +//! §C.2.1 table-look-up block-code decoder variant. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), Annex D §D.6 "Block Code +//! Books" (staged PDF p.231-236) and Annex C (informative) §C.2.1 +//! "Block Code" (staged PDF p.182-183) at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. +//! +//! # What this module adds +//! +//! Round 232 landed the §C.2.1 **modulus / integer-division** block-code +//! decoder ([`crate::decode_block_code`]) and left the §C.2.1 **table +//! look-up** decoder variant as an explicit follow-up, blocked on the +//! §D.6 code-book rows enumerated as the §C.2.1 Table C-1. This module +//! transcribes those §D.6 tables and implements the table-look-up +//! decoder against them. +//! +//! The spec presents both decoder variants and states (PDF p.182) that +//! they produce the identical quantisation-index array for the same code +//! word — so the table-look-up decoder here is cross-validated against +//! the round-232 modulus decoder over the full §D.6 code domain in the +//! tests, with no implementation read from anywhere else. +//! +//! # §D.6 code-book structure +//! +//! Each §D.6.x sub-clause tabulates a `4`-element block code over an +//! `nNumLevel`-level alphabet. The `e`-th element (`e` in `1..=4`, +//! one-based as the spec writes "1st/2nd/3rd/4th element") lists one +//! code value per quantisation-level index `L` in `0..nNumLevel`: +//! +//! ```text +//! code(element e, level index L) = L * nNumLevel^(e-1) +//! ``` +//! +//! (For the 3-level book, element 1 lists `0, 1, 2`; element 2 lists +//! `0, 3, 6`; element 3 lists `0, 9, 18`; element 4 lists `0, 27, 54` — +//! verbatim §D.6.1.) The quantisation-level index `L` maps to the +//! signed quantisation index by the §C.2.1 mid-range offset: +//! `index = L - (nNumLevel - 1) / 2` (so `L = 0` is the most negative +//! index and `L = nNumLevel - 1` the most positive). +//! +//! # §C.2.1 table-look-up walk (Table C-1) +//! +//! To decode, §C.2.1 rearranges each §D.6 book into a per-element table +//! and walks it **from the last element down to the first**, at each +//! element subtracting the largest table entry that does not exceed the +//! remaining code, and recording that entry's level index. Reproduced +//! as documented from the §C.2.1 worked example (PDF p.182), decoding +//! the 3-level 4-element code `64`: +//! +//! ```text +//! 4th Element: 64 - 54 = 10 > 0; level index 2 -> quantisation index +1 +//! 3rd Element: 10 - 9 = 1 > 0; level index 1 -> quantisation index 0 +//! 2nd Element: 1 - 0 = 1 > 0; level index 0 -> quantisation index -1 +//! 1st Element: 1 - 1 = 0 ; level index 1 -> quantisation index 0 +//! ``` +//! +//! producing `[0, -1, 0, +1]` first-element-first — identical to the +//! modulus decoder's worked example. The success criterion is the same +//! `nCode == 0` residual check after the last (i.e. the first-element) +//! step. + +use crate::{Error, Result}; + +/// The fixed number of elements (subband samples) in every §D.6 block +/// code book: the §D.6.x tables all tabulate a `4`-element block (PDF +/// p.231-236, "4-element ... Block Code Book"). +pub const D6_BLOCK_ELEMENTS: usize = 4; + +/// A transcribed §D.6 block code book: the per-element code values, one +/// inner row per element (element 1 first), each row holding one code +/// value per quantisation-level index `L` in `0..nNumLevel`. +/// +/// `levels` records the alphabet size `nNumLevel`; only the first +/// `levels` entries of each `rows[e]` are meaningful (the remainder are +/// zero padding so every book shares one storage shape). The widest +/// §D.6 book is 25 levels (§D.6.7), so each row is sized for 25. +#[derive(Debug, Clone, Copy)] +pub struct D6BlockBook { + /// The alphabet size `nNumLevel` for this book. + levels: u32, + /// `rows[e][L]` is the §D.6 code value for the `(e+1)`-th element at + /// quantisation-level index `L`. Entries at `L >= levels` are unused + /// zero padding. + rows: [[u32; 25]; D6_BLOCK_ELEMENTS], +} + +impl D6BlockBook { + /// The alphabet size `nNumLevel` this book decodes. + #[must_use] + pub const fn levels(&self) -> u32 { + self.levels + } + + /// The §D.6 code value for the one-based `element` (`1..=4`) at + /// quantisation-level index `level_index` (`0..levels`). Returns + /// `None` when `element` is outside `1..=4` or `level_index` is + /// outside `0..levels`. + #[must_use] + pub fn code_value(&self, element: usize, level_index: u32) -> Option { + if !(1..=D6_BLOCK_ELEMENTS).contains(&element) || level_index >= self.levels { + return None; + } + Some(self.rows[element - 1][level_index as usize]) + } +} + +// --------------------------------------------------------------------- +// §D.6 table construction. +// +// Every §D.6 book is `code(element e, level L) = L * nNumLevel^(e-1)` +// (verified against the printed §D.6.1..§D.6.7 tables row by row in the +// tests). Building the rows from that closed form transcribes the +// table's own arithmetic rather than re-typing several hundred decimal +// cells; the test module then asserts each printed anchor cell against +// the constructed row so the closed form is pinned to the staged PDF. +// +// PDF print errata noted in the tests: §D.6.3 (7-level) 3rd-element +// level-0 cell prints "47" where the table's own `L * 49` arithmetic +// and the §C.2.1 modulus decoder both give 147 — the constructed table +// carries the arithmetically-consistent 147 and the test records the +// print error. +// --------------------------------------------------------------------- + +const fn build_book(levels: u32) -> D6BlockBook { + let mut rows = [[0u32; 25]; D6_BLOCK_ELEMENTS]; + let mut e = 0; + while e < D6_BLOCK_ELEMENTS { + // factor = nNumLevel^e (element index e is zero-based here, so + // the spec's `e-1` exponent for the one-based "(e+1)-th element" + // is exactly this zero-based `e`). + let mut factor: u32 = 1; + let mut k = 0; + while k < e { + factor *= levels; + k += 1; + } + let mut l = 0; + while l < levels { + rows[e][l as usize] = l * factor; + l += 1; + } + e += 1; + } + D6BlockBook { levels, rows } +} + +/// §D.6.1 — 3-level 4-element 7-bit block code book (Table V.3). +pub const D6_BOOK_3: D6BlockBook = build_book(3); +/// §D.6.2 — 5-level 4-element 10-bit block code book (Table V.5). +pub const D6_BOOK_5: D6BlockBook = build_book(5); +/// §D.6.3 — 7-level 4-element 12-bit block code book (Table V.7). +pub const D6_BOOK_7: D6BlockBook = build_book(7); +/// §D.6.4 — 9-level 4-element 13-bit block code book (Table V.9). +pub const D6_BOOK_9: D6BlockBook = build_book(9); +/// §D.6.5 — 13-level 4-element 15-bit block code book (Table V.13). +pub const D6_BOOK_13: D6BlockBook = build_book(13); +/// §D.6.6 — 17-level 4-element 17-bit block code book (Table V.17). +pub const D6_BOOK_17: D6BlockBook = build_book(17); +/// §D.6.7 — 25-level 4-element 19-bit block code book (Table V.25). +pub const D6_BOOK_25: D6BlockBook = build_book(25); + +/// Resolve the §D.6 block code book for a quantisation-level count, or +/// `None` if the level count is not one of the §D.6 sub-clauses +/// (`3, 5, 7, 9, 13, 17, 25`). +#[must_use] +pub fn d6_book_for_levels(n_levels: u32) -> Option<&'static D6BlockBook> { + match n_levels { + 3 => Some(&D6_BOOK_3), + 5 => Some(&D6_BOOK_5), + 7 => Some(&D6_BOOK_7), + 9 => Some(&D6_BOOK_9), + 13 => Some(&D6_BOOK_13), + 17 => Some(&D6_BOOK_17), + 25 => Some(&D6_BOOK_25), + _ => None, + } +} + +/// Decode one §C.2.1 block-code word using the §D.6 table-look-up +/// variant, in place. +/// +/// This is the §C.2.1 table-look-up decoder (PDF p.182-183). It walks +/// the `book` from the last element down to the first, subtracting the +/// largest code value that does not exceed the remaining code and +/// recording that entry's quantisation index, exactly per the §C.2.1 +/// `DecodeBlockCode` table-look-up pseudocode. The result is identical +/// to [`crate::decode_block_code`] (the modulus variant) for every code +/// word the spec defines. +/// +/// On entry: +/// +/// - `code` is the unsigned block-code word read from the bit stream. +/// - `book` is the §D.6 code book (e.g. [`D6_BOOK_3`]); its +/// [`D6BlockBook::levels`] fixes the alphabet size `nNumLevel`. +/// - `output` is the destination quantisation-index array, ordered +/// first-element-first. Its length is the spec's `nNumElement`; it +/// must not exceed [`D6_BLOCK_ELEMENTS`] (the §D.6 books are 4-element). +/// +/// # Errors +/// +/// - [`Error::BlockCodeLevelsOutOfRange`] if the book's level count is +/// `< 2` (no §D.6 book is that small; guarded for completeness). +/// - [`Error::BlockCodeResidual`] if `output.len()` exceeds +/// [`D6_BLOCK_ELEMENTS`], or if, after walking every element, the +/// residual code word is non-zero — the §C.2.1 "ERROR: block code +/// look-up fail" condition surfaced as a recoverable error. +pub fn decode_block_code_table(code: u32, book: &D6BlockBook, output: &mut [i32]) -> Result<()> { + let n_levels = book.levels; + if n_levels < 2 { + return Err(Error::BlockCodeLevelsOutOfRange { n_levels }); + } + if output.len() > D6_BLOCK_ELEMENTS { + return Err(Error::BlockCodeResidual { + residual: code, + n_elements: output.len(), + n_levels, + }); + } + let offset = ((n_levels - 1) >> 1) as i32; + let n_elements = output.len(); + let mut residual = code; + // §C.2.1: walk from the last element back to the first. `pnEntry` + // points to the last entry in the element's code book and counts + // down; the first entry that fits (largest first) wins. + for e in (1..=n_elements).rev() { + let mut matched = false; + // m walks the level index from the top of the alphabet + // (largest code value) down to 0; the largest entry that does + // not exceed the residual is selected. + for level_index in (0..n_levels).rev() { + // Unwrap is safe: e in 1..=n_elements <= D6_BLOCK_ELEMENTS, + // level_index < n_levels. + let entry = book.code_value(e, level_index).unwrap(); + if residual >= entry { + residual -= entry; + // quantisation index = level_index - offset. + output[e - 1] = level_index as i32 - offset; + matched = true; + break; + } + } + if !matched { + // No entry fit — only possible for a malformed code; the + // L=0 entry is always 0 so this branch is unreachable for + // well-formed books, but guard it to surface corruption. + return Err(Error::BlockCodeResidual { + residual, + n_elements, + n_levels, + }); + } + } + // §C.2.1 success criterion: residual must be zero after the walk. + if residual != 0 { + return Err(Error::BlockCodeResidual { + residual, + n_elements, + n_levels, + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decode_block_code; + + // ----------------------------------------------------------------- + // §D.6 table transcription: assert printed anchor cells against the + // constructed books, row by row, to pin the closed form to the + // staged PDF (p.231-236). + // ----------------------------------------------------------------- + + #[test] + fn d6_1_three_level_matches_printed_table_v3() { + // §D.6.1 Table V.3 (PDF p.231), one row per element. + assert_eq!(D6_BOOK_3.levels(), 3); + // 1st element: 0, 1, 2 + assert_eq!( + [ + D6_BOOK_3.code_value(1, 0).unwrap(), + D6_BOOK_3.code_value(1, 1).unwrap(), + D6_BOOK_3.code_value(1, 2).unwrap() + ], + [0, 1, 2] + ); + // 2nd element: 0, 3, 6 + assert_eq!( + [ + D6_BOOK_3.code_value(2, 0).unwrap(), + D6_BOOK_3.code_value(2, 1).unwrap(), + D6_BOOK_3.code_value(2, 2).unwrap() + ], + [0, 3, 6] + ); + // 3rd element: 0, 9, 18 + assert_eq!( + [ + D6_BOOK_3.code_value(3, 0).unwrap(), + D6_BOOK_3.code_value(3, 1).unwrap(), + D6_BOOK_3.code_value(3, 2).unwrap() + ], + [0, 9, 18] + ); + // 4th element: 0, 27, 54 + assert_eq!( + [ + D6_BOOK_3.code_value(4, 0).unwrap(), + D6_BOOK_3.code_value(4, 1).unwrap(), + D6_BOOK_3.code_value(4, 2).unwrap() + ], + [0, 27, 54] + ); + } + + #[test] + fn d6_2_five_level_matches_printed_table_v5() { + // §D.6.2 Table V.5 (PDF p.232). + assert_eq!(D6_BOOK_5.levels(), 5); + // 2nd element: 0, 5, 10, 15, 20 + for (l, want) in [0u32, 5, 10, 15, 20].iter().enumerate() { + assert_eq!(D6_BOOK_5.code_value(2, l as u32).unwrap(), *want); + } + // 4th element: 0, 125, 250, 375, 500 + for (l, want) in [0u32, 125, 250, 375, 500].iter().enumerate() { + assert_eq!(D6_BOOK_5.code_value(4, l as u32).unwrap(), *want); + } + } + + #[test] + fn d6_3_seven_level_matches_printed_table_v7_with_print_erratum() { + // §D.6.3 Table V.7 (PDF p.233). + assert_eq!(D6_BOOK_7.levels(), 7); + // 1st element: 0..6 + for l in 0u32..7 { + assert_eq!(D6_BOOK_7.code_value(1, l).unwrap(), l); + } + // 3rd element factor 49: 0,49,98,147,196,245,294. + // PDF print erratum: the level-0... wait — the printed cell that + // reads "47" is at quantisation index 0 = level index 3, whose + // arithmetically-correct value is 3*49 = 147. The table's own + // `L*49` progression and the §C.2.1 modulus decoder both give + // 147; the constructed book carries 147. + for (l, want) in [0u32, 49, 98, 147, 196, 245, 294].iter().enumerate() { + assert_eq!(D6_BOOK_7.code_value(3, l as u32).unwrap(), *want); + } + // 4th element factor 343. + assert_eq!(D6_BOOK_7.code_value(4, 6).unwrap(), 2058); + } + + #[test] + fn d6_4_nine_level_matches_printed_table_v9() { + // §D.6.4 Table V.9 (PDF p.234). + assert_eq!(D6_BOOK_9.levels(), 9); + // 3rd element factor 81: top entry 8*81 = 648. + assert_eq!(D6_BOOK_9.code_value(3, 8).unwrap(), 648); + // 4th element factor 729: top entry 8*729 = 5832. + assert_eq!(D6_BOOK_9.code_value(4, 8).unwrap(), 5832); + } + + #[test] + fn d6_5_thirteen_level_matches_printed_table_v13() { + // §D.6.5 Table V.13 (PDF p.235). + assert_eq!(D6_BOOK_13.levels(), 13); + // 3rd element factor 169: level 12 (quant +6) = 12*169 = 2028. + assert_eq!(D6_BOOK_13.code_value(3, 12).unwrap(), 2028); + // 4th element factor 2197: level 12 = 12*2197 = 26364. + assert_eq!(D6_BOOK_13.code_value(4, 12).unwrap(), 26364); + } + + #[test] + fn d6_6_seventeen_level_matches_printed_table_v17() { + // §D.6.6 Table V.17 (PDF p.236). + assert_eq!(D6_BOOK_17.levels(), 17); + // 3rd element factor 289: top 16*289 = 4624. + assert_eq!(D6_BOOK_17.code_value(3, 16).unwrap(), 4624); + // 4th element factor 4913: top 16*4913 = 78608. + assert_eq!(D6_BOOK_17.code_value(4, 16).unwrap(), 78608); + } + + #[test] + fn d6_7_twenty_five_level_matches_printed_table_v25() { + // §D.6.7 Table V.25 (PDF p.236). + assert_eq!(D6_BOOK_25.levels(), 25); + // 3rd element factor 625: top 24*625 = 15000. + assert_eq!(D6_BOOK_25.code_value(3, 24).unwrap(), 15000); + // 4th element factor 15625: top 24*15625 = 375000. + assert_eq!(D6_BOOK_25.code_value(4, 24).unwrap(), 375000); + } + + #[test] + fn code_value_out_of_range_is_none() { + assert_eq!(D6_BOOK_3.code_value(0, 0), None); // element 0 invalid + assert_eq!(D6_BOOK_3.code_value(5, 0), None); // element 5 > 4 + assert_eq!(D6_BOOK_3.code_value(1, 3), None); // level 3 >= 3 + } + + #[test] + fn d6_book_for_levels_resolves_every_sub_clause() { + assert_eq!(d6_book_for_levels(3).unwrap().levels(), 3); + assert_eq!(d6_book_for_levels(5).unwrap().levels(), 5); + assert_eq!(d6_book_for_levels(7).unwrap().levels(), 7); + assert_eq!(d6_book_for_levels(9).unwrap().levels(), 9); + assert_eq!(d6_book_for_levels(13).unwrap().levels(), 13); + assert_eq!(d6_book_for_levels(17).unwrap().levels(), 17); + assert_eq!(d6_book_for_levels(25).unwrap().levels(), 25); + // Non-§D.6 level counts. + assert!(d6_book_for_levels(2).is_none()); + assert!(d6_book_for_levels(4).is_none()); + assert!(d6_book_for_levels(33).is_none()); + assert!(d6_book_for_levels(0).is_none()); + } + + // ----------------------------------------------------------------- + // §C.2.1 table-look-up decoder. + // ----------------------------------------------------------------- + + #[test] + fn spec_worked_example_table_lookup_code_sixty_four() { + // §C.2.1 worked example (PDF p.182): code 64, 3-level 4-element + // → [0, -1, 0, +1]. + let mut out = [0_i32; 4]; + decode_block_code_table(64, &D6_BOOK_3, &mut out).unwrap(); + assert_eq!(out, [0, -1, 0, 1]); + } + + #[test] + fn table_lookup_all_zero_code_is_all_bottom_of_alphabet() { + // code 0 → every element at level index 0 = -offset. + let mut out = [0_i32; 4]; + decode_block_code_table(0, &D6_BOOK_5, &mut out).unwrap(); + assert_eq!(out, [-2, -2, -2, -2]); + } + + #[test] + fn table_lookup_max_code_is_all_top_of_alphabet() { + // The largest valid code is sum over elements of + // (n_levels-1) * n_levels^(e-1) = n_levels^4 - 1. + let n: u32 = 3; + let max = n.pow(4) - 1; // 80 + let mut out = [0_i32; 4]; + decode_block_code_table(max, &D6_BOOK_3, &mut out).unwrap(); + assert_eq!(out, [1, 1, 1, 1]); + } + + #[test] + fn table_lookup_residual_error_one_past_max() { + let n: u32 = 3; + let max = n.pow(4) - 1; + let mut out = [0_i32; 4]; + let err = decode_block_code_table(max + 1, &D6_BOOK_3, &mut out).unwrap_err(); + assert!(matches!(err, Error::BlockCodeResidual { .. })); + } + + #[test] + fn table_lookup_too_many_elements_rejected() { + let mut out = [0_i32; 5]; // > D6_BLOCK_ELEMENTS + let err = decode_block_code_table(0, &D6_BOOK_3, &mut out).unwrap_err(); + assert!(matches!(err, Error::BlockCodeResidual { .. })); + } + + #[test] + fn table_lookup_empty_output_succeeds_only_for_zero_code() { + let mut empty: [i32; 0] = []; + decode_block_code_table(0, &D6_BOOK_3, &mut empty).unwrap(); + let err = decode_block_code_table(5, &D6_BOOK_3, &mut empty).unwrap_err(); + assert!(matches!(err, Error::BlockCodeResidual { .. })); + } + + #[test] + fn table_lookup_matches_modulus_decoder_three_level_full_domain() { + // The spec states both decoders produce the identical output. + // Cross-validate over the entire 3-level 4-element domain. + let n: u32 = 3; + for code in 0..n.pow(4) { + let mut a = [0_i32; 4]; + let mut b = [0_i32; 4]; + decode_block_code_table(code, &D6_BOOK_3, &mut a).unwrap(); + decode_block_code(code, n, &mut b).unwrap(); + assert_eq!(a, b, "mismatch at code {code}"); + } + } + + #[test] + fn table_lookup_matches_modulus_decoder_five_level_full_domain() { + let n: u32 = 5; + for code in 0..n.pow(4) { + let mut a = [0_i32; 4]; + let mut b = [0_i32; 4]; + decode_block_code_table(code, &D6_BOOK_5, &mut a).unwrap(); + decode_block_code(code, n, &mut b).unwrap(); + assert_eq!(a, b, "mismatch at code {code}"); + } + } + + #[test] + fn table_lookup_matches_modulus_decoder_seven_level_full_domain() { + // Exercises the §D.6.3 3rd-element 147 erratum cell across the + // full 7^4 = 2401 code domain. + let n: u32 = 7; + for code in 0..n.pow(4) { + let mut a = [0_i32; 4]; + let mut b = [0_i32; 4]; + decode_block_code_table(code, &D6_BOOK_7, &mut a).unwrap(); + decode_block_code(code, n, &mut b).unwrap(); + assert_eq!(a, b, "mismatch at code {code}"); + } + } + + #[test] + fn table_lookup_matches_modulus_decoder_wide_books_sampled() { + // 9/13/17/25-level domains are large; sample a stride across + // each full range and cross-check both decoders agree. + for (book, n) in [ + (&D6_BOOK_9, 9u32), + (&D6_BOOK_13, 13), + (&D6_BOOK_17, 17), + (&D6_BOOK_25, 25), + ] { + let max = n.pow(4); + let stride = (max / 997).max(1); + let mut code = 0u32; + while code < max { + let mut a = [0_i32; 4]; + let mut b = [0_i32; 4]; + decode_block_code_table(code, book, &mut a).unwrap(); + decode_block_code(code, n, &mut b).unwrap(); + assert_eq!(a, b, "mismatch at {n}-level code {code}"); + code += stride; + } + } + } + + #[test] + fn table_lookup_indices_stay_within_alphabet() { + // Every decoded index must land in [-offset, +offset]. + let n: u32 = 9; + let offset = ((n - 1) >> 1) as i32; + let max = n.pow(4); + let mut code = 0u32; + while code < max { + let mut out = [0_i32; 4]; + decode_block_code_table(code, &D6_BOOK_9, &mut out).unwrap(); + for v in out { + assert!( + (-offset..=offset).contains(&v), + "index {v} out of range at code {code}" + ); + } + code += 311; + } + } + + #[test] + fn block_elements_constant_is_four() { + assert_eq!(D6_BLOCK_ELEMENTS, 4); + } +} diff --git a/crates/vendor/oxideav-dts/src/dmix_coeff.rs b/crates/vendor/oxideav-dts/src/dmix_coeff.rs new file mode 100644 index 00000000..828fafaf --- /dev/null +++ b/crates/vendor/oxideav-dts/src/dmix_coeff.rs @@ -0,0 +1,344 @@ +//! §D.11 "Look-up Table for Downmix Scale Factors" (`DmixTable` / +//! `InvDmixTbl`) plus the §5.7.1 Table 5-31 downmix-coefficient code +//! resolver. +//! +//! Transcribed verbatim from ETSI TS 102 114 V1.3.1 (2011-08) Annex D +//! §D.11 (staged at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`, PDF +//! p.256-259). The printed table has six columns; this module keeps +//! the two normative integer columns: +//! +//! - `DmixTable` — unsigned 16-bit values, the `AbsValues` column +//! "after multiplication by 2^15 and rounding to the nearest +//! integer value" (§5.7.2.2), indexed by `DmixTblIndex` `0..=240`. +//! - `InvDmixTbl` — unsigned 24-bit values, the `InvAbsValues` +//! column "after multiplication by 2^16 and rounding to the +//! nearest integer value" (§8.5.x wording mirrored at PDF p.256), +//! indexed by `InvDmixTblIndex` `0..=200`. Inverse entries exist +//! only for `DmixTblIndex >= 40` (`-40 dB` and louder): +//! `InvDmixTblIndex = DmixTblIndex - 40`. +//! +//! The informative `LogAbsValues (dB)` column is a piecewise-uniform +//! ramp: `0.5 dB` steps from `-60 dB` (index 0) to `-30 dB` +//! (index 60), `0.25 dB` steps to `-15 dB` (index 120), and +//! `0.125 dB` steps to `0 dB` (index 240) — except index 216, which +//! the spec prints as the exact half-power point `0.707107` +//! (`1/sqrt(2)`, i.e. `-3.0103 dB`) rather than `10^(-3/20)`. +//! +//! Two §5.7 consumers feed this table: +//! +//! - §5.7.1 Table 5-31 dynamic downmix coefficients: each 9-bit +//! `panDwnMixCodeCoeffs[n]` code word carries a phase bit in the +//! MSB (`1` → in phase `+1`, `0` → out of phase `-1`) and an 8-bit +//! biased table index in the low bits (`0` → the coefficient is +//! exactly `0.0` — "-Infinity is not part of the table" — else +//! `DmixTable[index - 1]`). [`decode_dmix_code`] implements that +//! resolution. +//! - §5.7.2 Table 5-33 `nEmbESDownMixScaleIndex`: a plain 8-bit +//! `DmixTable[]` index whose encode-side range is limited to +//! `40..=240` (`[-40 dB, 0 dB]`). +//! +//! This module is feature-independent (no `oxideav-core` dep), so it +//! is available under both the default and `--no-default-features` +//! build modes. + +use crate::{Error, Result}; + +/// Number of entries in the §D.11 `DmixTable` (`DmixTblIndex` +/// `0..=240`). +pub const DMIX_TABLE_LEN: usize = 241; + +/// The §D.11 `DmixTable` unity-gain index (`32768` = `1.0` in Q15, +/// `0 dB`). +pub const DMIX_TABLE_UNITY_INDEX: usize = 240; + +/// Number of entries in the §D.11 `InvDmixTbl` (`InvDmixTblIndex` +/// `0..=200`). +pub const INV_DMIX_TABLE_LEN: usize = 201; + +/// Offset between the two §D.11 index columns: inverse entries exist +/// only for `DmixTblIndex >= 40`, and +/// `InvDmixTblIndex = DmixTblIndex - 40`. +pub const INV_DMIX_INDEX_OFFSET: usize = 40; + +/// §D.11 `DmixTable` column: unsigned 16-bit Q15 downmix scale +/// factors (`AbsValues * 2^15`, rounded to nearest), indexed by +/// `DmixTblIndex` `0..=240`. Entry 0 is `-60 dB` (`33`), entry 240 +/// is unity (`32768`). +/// +/// Transcribed from ETSI TS 102 114 V1.3.1 §D.11 (PDF p.256-259). +pub static DMIX_TABLE: [u16; DMIX_TABLE_LEN] = [ + 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, // 0..=9 + 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, // 10..=19 + 104, 110, 116, 123, 130, 138, 146, 155, 164, 174, // 20..=29 + 184, 195, 207, 219, 232, 246, 260, 276, 292, 309, // 30..=39 + 328, 347, 368, 389, 413, 437, 463, 490, 519, 550, // 40..=49 + 583, 617, 654, 693, 734, 777, 823, 872, 924, 978, // 50..=59 + 1036, 1066, 1098, 1130, 1163, 1197, 1232, 1268, 1305, 1343, // 60..=69 + 1382, 1422, 1464, 1506, 1550, 1596, 1642, 1690, 1740, 1790, // 70..=79 + 1843, 1896, 1952, 2009, 2068, 2128, 2190, 2254, 2320, 2388, // 80..=89 + 2457, 2529, 2603, 2679, 2757, 2838, 2920, 3006, 3093, 3184, // 90..=99 + 3277, 3372, 3471, 3572, 3677, 3784, 3894, 4008, 4125, 4246, // 100..=109 + 4370, 4497, 4629, 4764, 4903, 5046, 5193, 5345, 5501, 5662, // 110..=119 + 5827, 5912, 5997, 6084, 6172, 6262, 6353, 6445, 6538, 6633, // 120..=129 + 6729, 6827, 6925, 7026, 7128, 7231, 7336, 7442, 7550, 7659, // 130..=139 + 7771, 7883, 7997, 8113, 8231, 8350, 8471, 8594, 8719, 8845, // 140..=149 + 8973, 9103, 9235, 9369, 9505, 9643, 9783, 9924, 10068, 10214, // 150..=159 + 10362, 10512, 10665, 10819, 10976, 11135, 11297, 11460, 11627, 11795, // 160..=169 + 11966, 12139, 12315, 12494, 12675, 12859, 13045, 13234, 13426, 13621, // 170..=179 + 13818, 14018, 14222, 14428, 14637, 14849, 15064, 15283, 15504, 15729, // 180..=189 + 15957, 16188, 16423, 16661, 16902, 17147, 17396, 17648, 17904, 18164, // 190..=199 + 18427, 18694, 18965, 19240, 19519, 19802, 20089, 20380, 20675, 20975, // 200..=209 + 21279, 21587, 21900, 22218, 22540, 22867, 23170, 23534, 23875, 24221, // 210..=219 + 24573, 24929, 25290, 25657, 26029, 26406, 26789, 27177, 27571, 27970, // 220..=229 + 28376, 28787, 29205, 29628, 30057, 30493, 30935, 31383, 31838, 32300, // 230..=239 + 32768, // 240 +]; + +/// §D.11 `InvDmixTbl` column: unsigned 24-bit Q16 inverse downmix +/// scale factors (`InvAbsValues * 2^16`, rounded to nearest), indexed +/// by `InvDmixTblIndex` `0..=200` (i.e. `DmixTblIndex - 40`). Entry 0 +/// inverts `-40 dB` (`6553600` = `100.0` in Q16), entry 200 inverts +/// unity (`65536`). +/// +/// Transcribed from ETSI TS 102 114 V1.3.1 §D.11 (PDF p.256-259). +pub static INV_DMIX_TABLE: [u32; INV_DMIX_TABLE_LEN] = [ + 6553600, 6186997, 5840902, 5514167, 5205710, 4914507, 4639593, 4380059, // 0..=7 + 4135042, 3903731, 3685360, 3479204, 3284581, 3100844, 2927386, 2763630, // 8..=15 + 2609035, 2463088, 2325305, 2195230, 2072430, 2013631, 1956500, 1900990, // 16..=23 + 1847055, 1794651, 1743733, 1694260, 1646190, 1599484, 1554103, 1510010, // 24..=31 + 1467168, 1425542, 1385096, 1345798, 1307615, 1270515, 1234468, 1199444, // 32..=39 + 1165413, 1132348, 1100221, 1069005, 1038676, 1009206, 980573, 952752, // 40..=47 + 925721, 899456, 873937, 849141, 825049, 801641, 778897, 756798, // 48..=55 + 735326, 714463, 694193, 674497, 655360, 636766, 618700, 601146, // 56..=63 + 584090, 567518, 551417, 535772, 520571, 505801, 491451, 477507, // 64..=71 + 463959, 450796, 438006, 425579, 413504, 401772, 390373, 379297, // 72..=79 + 368536, 363270, 358080, 352964, 347920, 342949, 338049, 333219, // 80..=87 + 328458, 323765, 319139, 314579, 310084, 305654, 301287, 296982, // 88..=95 + 292739, 288556, 284433, 280369, 276363, 272414, 268522, 264685, // 96..=103 + 260904, 257176, 253501, 249879, 246309, 242790, 239321, 235901, // 104..=111 + 232531, 229208, 225933, 222705, 219523, 216386, 213295, 210247, // 112..=119 + 207243, 204282, 201363, 198486, 195650, 192855, 190099, 187383, // 120..=127 + 184706, 182066, 179465, 176901, 174373, 171882, 169426, 167005, // 128..=135 + 164619, 162267, 159948, 157663, 155410, 153190, 151001, 148844, // 136..=143 + 146717, 144621, 142554, 140517, 138510, 136531, 134580, 132657, // 144..=151 + 130762, 128893, 127052, 125236, 123447, 121683, 119944, 118231, // 152..=159 + 116541, 114876, 113235, 111617, 110022, 108450, 106901, 105373, // 160..=167 + 103868, 102383, 100921, 99479, 98057, 96656, 95275, 93914, // 168..=175 + 92682, 91249, 89946, 88660, 87394, 86145, 84914, 83701, // 176..=183 + 82505, 81326, 80164, 79019, 77890, 76777, 75680, 74598, // 184..=191 + 73533, 72482, 71446, 70425, 69419, 68427, 67450, 66486, // 192..=199 + 65536, // 200 +]; + +/// Look up the §D.11 `DmixTable` scale factor for a `DmixTblIndex`, +/// returned as the real-valued gain (`DmixTable[index] / 2^15`). +/// +/// Returns `None` when `index > 240` (outside the printed table). +#[must_use] +pub fn dmix_scale(index: usize) -> Option { + DMIX_TABLE.get(index).map(|&q15| f64::from(q15) / 32768.0) +} + +/// Look up the §D.11 `InvDmixTbl` inverse scale factor for a +/// `DmixTblIndex` (**not** an `InvDmixTblIndex` — the +/// [`INV_DMIX_INDEX_OFFSET`] rebasing is applied internally), +/// returned as the real-valued inverse gain +/// (`InvDmixTbl[index - 40] / 2^16`). +/// +/// Returns `None` when `index < 40` (the spec prints `N/A` for the +/// quietest 40 rows) or `index > 240`. +#[must_use] +pub fn inv_dmix_scale(index: usize) -> Option { + let inv_index = index.checked_sub(INV_DMIX_INDEX_OFFSET)?; + INV_DMIX_TABLE + .get(inv_index) + .map(|&q16| f64::from(q16) / 65536.0) +} + +/// Resolve one §5.7.1 Table 5-31 9-bit dynamic-downmix coefficient +/// code word (`panDwnMixCodeCoeffs[n]`) to its real-valued +/// coefficient. +/// +/// Per the Table 5-31 pseudocode: +/// +/// ```text +/// nSign = ( nTmp & nMask1 ) ? 1 : -1; // MSB: 1 -> in phase (+1) +/// nTmp = (nTmp & nMask2); // low 8 bits: biased index +/// if (nTmp > 0) { +/// nTmp--; // -Infinity is not part of the table +/// if (nTmp > nTblSize) +/// return false; +/// m_panCoreDwnMixCoeffs[n] = (nSign * DmixCoeffTable[nTmp]); +/// } else +/// m_panCoreDwnMixCoeffs[n] = 0.0; +/// ``` +/// +/// A zero low-byte therefore encodes an exact `0.0` (the muted +/// channel pairing), and any other value is a one-biased +/// [`DMIX_TABLE`] index carrying the phase in bit 8. +/// +/// # Errors +/// +/// Returns [`Error::DownmixCodeOutOfRange`] when the code is wider +/// than 9 bits or its unbiased index walks past the end of the +/// 241-entry table (the pseudocode's `return false` arm). +pub fn decode_dmix_code(code: u16) -> Result { + if code >= 1 << 9 { + return Err(Error::DownmixCodeOutOfRange { code }); + } + let sign = if code & 0x100 != 0 { 1.0 } else { -1.0 }; + let biased = (code & 0xFF) as usize; + if biased == 0 { + return Ok(0.0); + } + let index = biased - 1; + if index >= DMIX_TABLE_LEN { + return Err(Error::DownmixCodeOutOfRange { code }); + } + Ok(sign * f64::from(DMIX_TABLE[index]) / 32768.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The informative §D.11 `LogAbsValues (dB)` ramp: `0.5 dB` steps + /// to index 60, `0.25 dB` steps to index 120, `0.125 dB` steps to + /// index 240. + fn spec_db(index: usize) -> f64 { + if index <= 60 { + -60.0 + 0.5 * index as f64 + } else if index <= 120 { + -30.0 + 0.25 * (index - 60) as f64 + } else { + -15.0 + 0.125 * (index - 120) as f64 + } + } + + #[test] + fn table_lengths_and_anchors() { + assert_eq!(DMIX_TABLE.len(), DMIX_TABLE_LEN); + assert_eq!(INV_DMIX_TABLE.len(), INV_DMIX_TABLE_LEN); + // Verbatim §D.11 anchor rows from the staged PDF. + assert_eq!(DMIX_TABLE[0], 33); // -60.0 dB + assert_eq!(DMIX_TABLE[40], 328); // -40.0 dB + assert_eq!(DMIX_TABLE[100], 3277); // -20.0 dB + assert_eq!(DMIX_TABLE[216], 23170); // 1/sqrt(2) + assert_eq!(DMIX_TABLE[DMIX_TABLE_UNITY_INDEX], 32768); // 0 dB + assert_eq!(INV_DMIX_TABLE[0], 6553600); // inverts -40.0 dB + assert_eq!(INV_DMIX_TABLE[60], 655360); // inverts -20.0 dB + assert_eq!(INV_DMIX_TABLE[200], 65536); // inverts 0 dB + } + + #[test] + fn dmix_table_matches_db_ramp_closed_form() { + // Every DmixTable entry is round(10^(dB/20) * 2^15) on the + // piecewise dB ramp — except index 216, which the spec prints + // as the exact half-power point 1/sqrt(2) (-3.0103 dB) rather + // than 10^(-3/20). + for (i, &q15) in DMIX_TABLE.iter().enumerate() { + let abs = if i == 216 { + std::f64::consts::FRAC_1_SQRT_2 + } else { + 10f64.powf(spec_db(i) / 20.0) + }; + let predicted = (abs * 32768.0).round() as u16; + assert_eq!(q15, predicted, "DmixTable[{i}]"); + } + } + + #[test] + fn inv_dmix_table_matches_db_ramp_closed_form() { + // Every InvDmixTbl entry is round(2^16 / 10^(dB/20)) on the + // same ramp rebased by INV_DMIX_INDEX_OFFSET, with the same + // index-216 half-power exception (inv index 176). + for (j, &q16) in INV_DMIX_TABLE.iter().enumerate() { + let i = j + INV_DMIX_INDEX_OFFSET; + let abs = if i == 216 { + std::f64::consts::FRAC_1_SQRT_2 + } else { + 10f64.powf(spec_db(i) / 20.0) + }; + let predicted = (65536.0 / abs).round() as u32; + assert_eq!(q16, predicted, "InvDmixTbl[{j}]"); + } + } + + #[test] + fn tables_are_strictly_monotone() { + for i in 1..DMIX_TABLE_LEN { + assert!(DMIX_TABLE[i] > DMIX_TABLE[i - 1], "DmixTable[{i}]"); + } + for j in 1..INV_DMIX_TABLE_LEN { + assert!(INV_DMIX_TABLE[j] < INV_DMIX_TABLE[j - 1], "InvDmixTbl[{j}]"); + } + } + + #[test] + fn forward_and_inverse_columns_agree() { + // DmixTable (Q15) x InvDmixTbl (Q16) ~= 2^31 wherever both + // columns are printed. Both are independently rounded from + // the real-valued column, so the worst relative slack is half + // a quantization step of each integer column. + for j in 0..INV_DMIX_TABLE_LEN { + let dmix = u64::from(DMIX_TABLE[j + INV_DMIX_INDEX_OFFSET]); + let inv = u64::from(INV_DMIX_TABLE[j]); + let product = dmix * inv; + let rel = (product as f64 - 2f64.powi(31)).abs() / 2f64.powi(31); + let bound = 0.5 / dmix as f64 + 0.5 / inv as f64 + 1e-9; + assert!( + rel < bound, + "row {j}: product {product} rel err {rel} bound {bound}" + ); + } + } + + #[test] + fn dmix_scale_bounds() { + assert_eq!(dmix_scale(DMIX_TABLE_UNITY_INDEX), Some(1.0)); + assert_eq!(dmix_scale(241), None); + let quietest = dmix_scale(0).unwrap(); + assert!((quietest - 33.0 / 32768.0).abs() < 1e-12); + } + + #[test] + fn inv_dmix_scale_bounds() { + assert_eq!(inv_dmix_scale(240), Some(1.0)); + assert_eq!(inv_dmix_scale(40), Some(100.0)); // inverts -40 dB + assert_eq!(inv_dmix_scale(39), None); // spec prints N/A + assert_eq!(inv_dmix_scale(241), None); + } + + #[test] + fn decode_dmix_code_phase_and_bias() { + // Low byte 0 -> exact 0.0 regardless of the phase bit. + assert_eq!(decode_dmix_code(0x000).unwrap(), 0.0); + assert_eq!(decode_dmix_code(0x100).unwrap(), 0.0); + // Biased index 241 -> DmixTable[240] = unity; MSB set -> +1. + assert_eq!(decode_dmix_code(0x100 | 241).unwrap(), 1.0); + // MSB clear -> out of phase (-1). + assert_eq!(decode_dmix_code(241).unwrap(), -1.0); + // Biased index 1 -> DmixTable[0] = 33 (Q15). + let quietest = decode_dmix_code(0x100 | 1).unwrap(); + assert!((quietest - 33.0 / 32768.0).abs() < 1e-12); + } + + #[test] + fn decode_dmix_code_rejects_out_of_domain() { + // Biased index 242 -> unbiased 241, past the table end. + assert_eq!( + decode_dmix_code(242), + Err(Error::DownmixCodeOutOfRange { code: 242 }) + ); + assert_eq!( + decode_dmix_code(0x100 | 255), + Err(Error::DownmixCodeOutOfRange { code: 0x1FF }) + ); + // Wider than the 9-bit field. + assert_eq!( + decode_dmix_code(0x200), + Err(Error::DownmixCodeOutOfRange { code: 0x200 }) + ); + } +} diff --git a/crates/vendor/oxideav-dts/src/drc_range.rs b/crates/vendor/oxideav-dts/src/drc_range.rs new file mode 100644 index 00000000..406c0344 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/drc_range.rs @@ -0,0 +1,272 @@ +//! DTS Dynamic Range Control: the signed-Q2 `dts_dynrng_to_db()` +//! code→dB mapping (§5.4.1 / §5.7.2) plus the §D.4 "Dynamic Range +//! Control" presentation table. +//! +//! ## The wire format: an 8-bit signed Q2 code (round 408) +//! +//! Per §5.4.1 (`RANGE` field description, recovered as a closed-form +//! function in the freshly staged `docs/audio/dts/dts-drc-dynrng.md`): +//! +//! > "Each coefficient is **8-bit signed fractional Q2 binary** and +//! > represents a logarithmic gain value as shown in clause D.4 +//! > giving a range of **±31,75 dB in steps of 0,25 dB**. Dynamic +//! > range compression is affected by multiplying the decoded audio +//! > samples by the linear coefficient." +//! +//! So the byte the bitstream carries (`ExtractBits(8)` in the §5.4.1 +//! `DYNF != 0` tail, and each `subsubFrameDRC_Rev2AUX[]` byte of the +//! §5.7.2 Rev2AUX chunk) is a **two's-complement signed Q2** value: +//! +//! ```text +//! dB = (int8_t)code × 0.25 // dts_dynrng_to_db() +//! linear = 10^(dB / 20) // multiplies each sample +//! ``` +//! +//! Code `0` is therefore **unity** (0 dB), and the applied gain is +//! `10^(dB/20)`, post-QMF ([`dts_dynrng_to_db`] / +//! [`dts_dynrng_to_linear`]). +//! +//! ## The §D.4 table is an offset-binary *presentation* — do not +//! index it with the raw code +//! +//! Annex D §D.4 (PDF p.195-197) prints the same mapping as a 256-row +//! `Index | Q18 binary | Multiplier | Log Multiplier (dB)` table, +//! where `dB(Index) = (Index − 127) × 0.25` — i.e. the printed +//! `Index` column is **offset-binary** (`Index = signed_code + 127`; +//! Index `127` = code `0` = 0 dB). [`DRC_RANGE_MULTIPLIER`] preserves +//! the printed `Multiplier` column keyed by that printed Index. +//! Indexing it directly with a raw wire code (`table[code]`) is off +//! by 127 steps — code `0` would wrongly yield −31.75 dB — which is +//! exactly the correctness trap `docs/audio/dts/dts-drc-dynrng.md` +//! documents ("Why the §D.4 table was reverted"). Decoders must use +//! the signed-Q2 function; the table stays available as the §D.4 +//! reference data and is cross-checked against the function in the +//! tests below. +//! +//! §5.4.1 Table 5-28 application pseudocode (the `RANGE` multiply +//! runs **after** the §C.2.5 QMF synthesis): +//! +//! ```text +//! if ( DYNF != 0 ) { +//! nIndex = ExtractBits(8); +//! RANGEtbl.LookUp(nIndex, RANGE); +//! for (ch=0; ch f64 { + // Sign-extend: two's complement, then Q2 -> 0.25 dB / LSB. + f64::from(code as i8) * 0.25 +} + +/// The linear gain a §5.4.1 `RANGE` / §5.7.2 Rev2AUX DRC code applies +/// to every reconstructed PCM sample: `10^(dts_dynrng_to_db(code)/20)` +/// (§5.4.1: "Dynamic range compression is affected by multiplying the +/// decoded audio samples by the linear coefficient"). +/// +/// Code `0` returns exactly `1.0`. +#[must_use] +pub fn dts_dynrng_to_linear(code: u8) -> f64 { + if code == 0 { + return 1.0; + } + 10f64.powf(dts_dynrng_to_db(code) / 20.0) +} + +/// Number of rows in the printed §D.4 table (`Index` column `0..=255`). +pub const DRC_RANGE_LEN: usize = 256; + +/// The §D.4 unity-gain **printed Index** (`RANGE == 1.0000`, +/// `0.0000` dB). Note this is the offset-binary presentation index, +/// not a wire code — the wire code for unity is `0` +/// ([`dts_dynrng_to_linear`]). +pub const DRC_RANGE_UNITY_INDEX: usize = 127; + +/// §D.4 Dynamic Range Control multiplier table, keyed by the table's +/// **printed offset-binary `Index` column** (`Index = signed_code + +/// 127`; row 127 = 0 dB). Row `i` is `10^((i − 127)·0.25 / 20)` to the +/// spec's 4-decimal rounding. +/// +/// **Do not index this with the raw 8-bit wire code** — the §5.4.1 / +/// §5.7.2 DRC byte is two's-complement signed Q2 and must go through +/// [`dts_dynrng_to_db`] / [`dts_dynrng_to_linear`] (see the module +/// docs and `docs/audio/dts/dts-drc-dynrng.md`). +/// +/// Transcribed from ETSI TS 102 114 V1.3.1 §D.4, "Multiplier" column, +/// indices `0..=255`. +pub static DRC_RANGE_MULTIPLIER: [f64; DRC_RANGE_LEN] = [ + 0.0259, 0.0266, 0.0274, 0.0282, 0.029, 0.0299, 0.0307, 0.0316, 0.0325, 0.0335, 0.0345, 0.0355, + 0.0365, 0.0376, 0.0387, 0.0398, 0.041, 0.0422, 0.0434, 0.0447, 0.046, 0.0473, 0.0487, 0.0501, + 0.0516, 0.0531, 0.0546, 0.0562, 0.0579, 0.0596, 0.0613, 0.0631, 0.0649, 0.0668, 0.0688, 0.0708, + 0.0729, 0.075, 0.0772, 0.0794, 0.0818, 0.0841, 0.0866, 0.0891, 0.0917, 0.0944, 0.0972, 0.1, + 0.1029, 0.1059, 0.109, 0.1122, 0.1155, 0.1189, 0.1223, 0.1259, 0.1296, 0.1334, 0.1372, 0.1413, + 0.1454, 0.1496, 0.154, 0.1585, 0.1631, 0.1679, 0.1728, 0.1778, 0.183, 0.1884, 0.1939, 0.1995, + 0.2054, 0.2113, 0.2175, 0.2239, 0.2304, 0.2371, 0.2441, 0.2512, 0.2585, 0.2661, 0.2738, 0.2818, + 0.2901, 0.2985, 0.3073, 0.3162, 0.3255, 0.335, 0.3447, 0.3548, 0.3652, 0.3758, 0.3868, 0.3981, + 0.4097, 0.4217, 0.434, 0.4467, 0.4597, 0.4732, 0.487, 0.5012, 0.5158, 0.5309, 0.5464, 0.5623, + 0.5788, 0.5957, 0.6131, 0.631, 0.6494, 0.6683, 0.6879, 0.7079, 0.7286, 0.7499, 0.7718, 0.7943, + 0.8175, 0.8414, 0.866, 0.8913, 0.9173, 0.9441, 0.9716, 1.0, 1.0292, 1.0593, 1.0902, 1.122, + 1.1548, 1.1885, 1.2232, 1.2589, 1.2957, 1.3335, 1.3725, 1.4125, 1.4538, 1.4962, 1.5399, 1.5849, + 1.6312, 1.6788, 1.7278, 1.7783, 1.8302, 1.8836, 1.9387, 1.9953, 2.0535, 2.1135, 2.1752, 2.2387, + 2.3041, 2.3714, 2.4406, 2.5119, 2.5852, 2.6607, 2.7384, 2.8184, 2.9007, 2.9854, 3.0726, 3.1623, + 3.2546, 3.3497, 3.4475, 3.5481, 3.6517, 3.7584, 3.8681, 3.9811, 4.0973, 4.217, 4.3401, 4.4668, + 4.5973, 4.7315, 4.8697, 5.0119, 5.1582, 5.3088, 5.4639, 5.6234, 5.7876, 5.9566, 6.1306, 6.3096, + 6.4938, 6.6834, 6.8786, 7.0795, 7.2862, 7.4989, 7.7179, 7.9433, 8.1752, 8.414, 8.6596, 8.9125, + 9.1728, 9.4406, 9.7163, 10.0, 10.292, 10.5925, 10.9018, 11.2202, 11.5478, 11.885, 12.2321, + 12.5893, 12.9569, 13.3352, 13.7246, 14.1254, 14.5378, 14.9624, 15.3993, 15.8489, 16.3117, + 16.788, 17.2783, 17.7828, 18.3021, 18.8365, 19.3865, 19.9526, 20.5353, 21.1349, 21.752, + 22.3872, 23.0409, 23.7137, 24.4062, 25.1189, 25.8523, 26.6073, 27.3842, 28.1838, 29.0068, + 29.8538, 30.7256, 31.6228, 32.5462, 33.4965, 34.4747, 35.4813, 36.5174, 37.5837, 38.6812, + 39.8107, +]; + +/// Look up a row of the printed §D.4 table by its **offset-binary +/// `Index` column** (`Index = signed_code + 127`). +/// +/// This is a reference-data accessor for the table as printed, *not* +/// the wire-code resolution: feeding the raw §5.4.1 / §5.7.2 DRC byte +/// here is off by 127 steps (code `0` would wrongly yield −31.75 dB). +/// Decode wire codes with [`dts_dynrng_to_linear`] / +/// [`dts_dynrng_to_db`] instead (`docs/audio/dts/dts-drc-dynrng.md`, +/// "Why the §D.4 table was reverted"). +#[must_use] +pub fn drc_range(index: u8) -> f64 { + DRC_RANGE_MULTIPLIER[index as usize] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn table_has_full_8bit_range() { + assert_eq!(DRC_RANGE_MULTIPLIER.len(), 256); + assert_eq!(DRC_RANGE_LEN, 256); + } + + #[test] + fn unity_at_index_127() { + // §D.4: index 127 -> Multiplier 1.0000 (0.0000 dB). + assert_eq!(drc_range(127), 1.0); + assert_eq!(DRC_RANGE_UNITY_INDEX, 127); + } + + #[test] + fn anchor_rows_match_spec() { + // Verbatim §D.4 anchor values from the staged PDF. + assert_eq!(drc_range(0), 0.0259); // -31.75 dB + assert_eq!(drc_range(47), 0.1); // -20.00 dB + assert_eq!(drc_range(80), 0.2585); // -11.75 dB + assert_eq!(drc_range(127), 1.0); // 0.00 dB + assert_eq!(drc_range(128), 1.0292); // 0.25 dB + assert_eq!(drc_range(207), 10.0); // 20.00 dB + assert_eq!(drc_range(255), 39.8107); // 32.00 dB + } + + #[test] + fn table_is_strictly_monotone_increasing() { + // The §D.4 multiplier rises monotonically with the index (the + // dB column is an exact 0.25 dB ramp), so every successor is + // strictly larger. + for i in 1..DRC_RANGE_LEN { + assert!( + DRC_RANGE_MULTIPLIER[i] > DRC_RANGE_MULTIPLIER[i - 1], + "entry {i} not greater than predecessor" + ); + } + } + + // ----------------------------------------------------------- + // dts_dynrng_to_db / dts_dynrng_to_linear (signed Q2, round 408) + // ----------------------------------------------------------- + + /// The signed-Q2 anchors from `docs/audio/dts/dts-drc-dynrng.md`: + /// code 0 = 0 dB (unity), 0.25 dB per LSB in both directions, and + /// the two's-complement extremes. + #[test] + fn dynrng_signed_q2_anchors() { + assert_eq!(dts_dynrng_to_db(0), 0.0); + assert_eq!(dts_dynrng_to_linear(0), 1.0); + assert_eq!(dts_dynrng_to_db(1), 0.25); + assert_eq!(dts_dynrng_to_db(0xFF), -0.25); // signed -1 + assert_eq!(dts_dynrng_to_db(0x7F), 31.75); // +127 + assert_eq!(dts_dynrng_to_db(0x80), -32.0); // -128 (outside nominal) + assert_eq!(dts_dynrng_to_db(0x81), -31.75); // -127 + // +20 dB = 80 quarter-dB steps -> linear 10.0. + assert_eq!(dts_dynrng_to_db(80), 20.0); + assert!((dts_dynrng_to_linear(80) - 10.0).abs() < 1e-12); + // -20 dB -> linear 0.1. + let minus_20 = 0u8.wrapping_sub(80); + assert_eq!(dts_dynrng_to_db(minus_20), -20.0); + assert!((dts_dynrng_to_linear(minus_20) - 0.1).abs() < 1e-12); + } + + /// The closed-form function and the printed §D.4 table describe + /// the same mapping, related by `Index = signed_code + 127`: for + /// every signed code in the table's domain (−127..=+127, i.e. + /// printed Index 0..=254) the function's linear gain matches the + /// table row to the spec's 4-decimal rounding. + #[test] + fn dynrng_function_matches_d4_table_via_offset_binary_index() { + for signed in -127i32..=127 { + let code = signed as i8 as u8; + let index = (signed + 127) as usize; + let from_fn = dts_dynrng_to_linear(code); + let from_table = DRC_RANGE_MULTIPLIER[index]; + // The printed Multiplier column is rounded to 4 decimals, + // so the absolute disagreement is bounded by half an ULP + // of that rounding. + assert!( + (from_fn - from_table).abs() < 6e-5, + "code {signed}: fn {from_fn} vs table[{index}] {from_table}" + ); + } + // The extremes that do NOT correspond: table row 255 is + // +32 dB (no reachable signed code maps there via the offset), + // and code -128 (-32 dB) has no table row. + assert_eq!(drc_range(255), 39.8107); + assert!((dts_dynrng_to_linear(0x80) - 10f64.powf(-32.0 / 20.0)).abs() < 1e-12); + } + + /// Demonstrate the documented off-by-127 trap: raw-code indexing + /// of the §D.4 table disagrees with the signed-Q2 function at + /// code 0 (the most common wire value). + #[test] + fn raw_code_table_indexing_is_the_documented_trap() { + assert_eq!(drc_range(0), 0.0259); // table row 0 = -31.75 dB + assert_eq!(dts_dynrng_to_linear(0), 1.0); // wire code 0 = 0 dB + } + + #[test] + fn multiplier_tracks_log_db_column() { + // Cross-check the transcribed Multiplier column against the + // informative Log-Multiplier(dB) column: dB[i] = -31.75 + 0.25*i, + // and Multiplier ≈ 10^(dB/20) to within the spec's 4-decimal + // rounding. + for (i, &actual) in DRC_RANGE_MULTIPLIER.iter().enumerate() { + let db = -31.75 + 0.25 * i as f64; + let predicted = 10f64.powf(db / 20.0); + let rel = (predicted - actual).abs() / actual; + assert!( + rel < 0.01, + "index {i}: rel err {rel} (pred {predicted}, got {actual})" + ); + } + } +} diff --git a/crates/vendor/oxideav-dts/src/dsync.rs b/crates/vendor/oxideav-dts/src/dsync.rs new file mode 100644 index 00000000..9015f6ce --- /dev/null +++ b/crates/vendor/oxideav-dts/src/dsync.rs @@ -0,0 +1,289 @@ +//! DTS Coherent Acoustics — §5.5 Table 5-29 `DSYNC` subsubframe +//! synchronization check word. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), staged PDF at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf` +//! (Table 5-29 pseudocode on PDF p.32, prose on PDF p.33). +//! +//! The final step of the §5.5 per-subsubframe `Audio Data` walk is a +//! conditional 16-bit synchronization word read. Transcribed verbatim +//! from the staged Table 5-29 pseudocode (PDF p.32): +//! +//! ```text +//! // Check for DSYNC +//! if ( (nSubSubFrame==(nSSC-1)) || (ASPF==1) ) { +//! DSYNC = ExtractBits(16); +//! if ( DSYNC != 0xffff ) +//! printf("DSYNC error at end of subsubframe #%d", nSubSubFrame); +//! } +//! ``` +//! +//! and the §5.5 prose (PDF p.33, "AUDIO (Audio data)"): +//! +//! > "At end of each subsubframe there may be a synchronization check +//! > word `DSYNC = 0xffff` depending on the flag `ASPF` in the frame +//! > header, but there must be at least a DSYNC at the end of each +//! > subframe." +//! +//! Two facts fix the gating completely: +//! +//! * **End of subframe** (`nSubSubFrame == nSSC - 1`, the last +//! subsubframe of the audio subframe): a DSYNC word is *always* +//! present here regardless of `ASPF`. This is the "at least a DSYNC +//! at the end of each subframe" guarantee. +//! * **`ASPF == 1`** (the §5.3.2 "Audio Sync-Word Insertion Flag" of +//! the frame header, [`crate::DtsFrameHeader::aspf`]): a DSYNC word +//! is present after *every* subsubframe, not only the last. +//! +//! When neither condition holds, no DSYNC word is read and the bit +//! cursor stays on the next subsubframe's first audio field. +//! +//! This module exposes the gating predicate +//! ([`dsync_present`]), the expected word +//! ([`DSYNC_WORD`]), and the bit-stream reader +//! ([`decode_dsync_at`]) that reads the 16-bit field and verifies it +//! against `0xffff`. It composes the round-281 `aspf` header field +//! with the round-249 [`crate::SubsubframeCount::n_ssc`] count to +//! drive the §5.5 walker's trailer step. + +use crate::bitreader::BitReader; +use crate::{Error, Result}; + +/// The §5.5 `DSYNC` synchronization check word value, `0xffff` +/// (PDF p.32: `if ( DSYNC != 0xffff )`). A correctly framed +/// subsubframe trailer carries exactly this 16-bit pattern. +pub const DSYNC_WORD: u16 = 0xffff; + +/// Wire width of the `DSYNC` field, in bits: `ExtractBits(16)` +/// (PDF p.32). +pub const DSYNC_WIRE_BITS: u32 = 16; + +/// The §5.5 gating predicate for the per-subsubframe `DSYNC` trailer +/// (PDF p.32): is a 16-bit `DSYNC` word present after subsubframe +/// `n_subsubframe`? +/// +/// Transcribes the spec's `if ( (nSubSubFrame==(nSSC-1)) || +/// (ASPF==1) )` condition exactly: +/// +/// * `n_subsubframe` — the zero-based index of the subsubframe whose +/// audio data was just unpacked (`nSubSubFrame`, the §5.5 loop +/// variable `for (nSubSubFrame=0; nSubSubFrame= 1` on the wire, so this only guards +/// against a malformed caller, never a real bit stream). +#[must_use] +pub fn dsync_present(n_subsubframe: u8, n_ssc: u8, aspf: bool) -> bool { + aspf || (n_ssc != 0 && n_subsubframe + 1 == n_ssc) +} + +/// Read and verify the §5.5 `DSYNC` synchronization check word at +/// `bit_offset` in `bytes` (PDF p.32), returning the number of bits +/// consumed (always [`DSYNC_WIRE_BITS`] = 16) on success. +/// +/// The bit offset is measured from the MSB of `bytes[0]`, matching the +/// MSB-first convention used elsewhere in this crate. The 16-bit field +/// is read big-endian (`ExtractBits(16)`) and compared against +/// [`DSYNC_WORD`] (`0xffff`). +/// +/// Errors: +/// +/// * [`Error::UnexpectedEof`] when fewer than 16 bits remain after +/// `bit_offset`. +/// * [`Error::DsyncMismatch`] when the 16 bits read are not `0xffff`. +/// The spec text only `printf`s a diagnostic at this point and keeps +/// decoding; this API surfaces the mismatch as a recoverable typed +/// error carrying the bad word and the subsubframe index so the +/// caller can choose whether to treat it as fatal (it is the only +/// in-band integrity check the core profile provides for the audio +/// data array). +/// +/// `n_subsubframe` is the zero-based subsubframe index this trailer +/// follows; it is threaded only into the [`Error::DsyncMismatch`] +/// diagnostic to mirror the spec's `"DSYNC error at end of subsubframe +/// #%d"` message. The caller is responsible for having already checked +/// [`dsync_present`] — this reader unconditionally consumes 16 bits. +pub fn decode_dsync_at(bytes: &[u8], bit_offset: usize, n_subsubframe: u8) -> Result { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let word = br.read_bits(DSYNC_WIRE_BITS)? as u16; + if word != DSYNC_WORD { + return Err(Error::DsyncMismatch { + found: word, + n_subsubframe, + }); + } + Ok(DSYNC_WIRE_BITS as usize) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dsync_word_constants() { + assert_eq!(DSYNC_WORD, 0xffff); + assert_eq!(DSYNC_WIRE_BITS, 16); + } + + #[test] + fn present_only_at_last_subsubframe_when_aspf_clear() { + // ASPF == 0: a DSYNC follows only the last subsubframe of the + // subframe (n_subsubframe + 1 == n_ssc). + // nSSC = 4 → subsubframes 0,1,2,3; only 3 carries a DSYNC. + assert!(!dsync_present(0, 4, false)); + assert!(!dsync_present(1, 4, false)); + assert!(!dsync_present(2, 4, false)); + assert!(dsync_present(3, 4, false)); + } + + #[test] + fn present_after_every_subsubframe_when_aspf_set() { + // ASPF == 1: a DSYNC follows every subsubframe. + for n_ssc in 1u8..=4 { + for n in 0..n_ssc { + assert!( + dsync_present(n, n_ssc, true), + "ASPF set: DSYNC must follow subsubframe {n} of {n_ssc}" + ); + } + } + } + + #[test] + fn single_subsubframe_subframe_always_has_dsync() { + // nSSC = 1 → the lone subsubframe (index 0) is also the last, + // so it always carries a DSYNC even with ASPF clear. + assert!(dsync_present(0, 1, false)); + assert!(dsync_present(0, 1, true)); + } + + #[test] + fn full_gating_matrix_matches_spec_condition() { + // Exhaustively cross-check dsync_present against the spec's + // `(nSubSubFrame == nSSC-1) || (ASPF==1)` for every + // (n_subsubframe, n_ssc, aspf) the wire can produce + // (nSSC in 1..=4, n_subsubframe in 0..nSSC). + for n_ssc in 1u8..=4 { + for n in 0..n_ssc { + for &aspf in &[false, true] { + let want = aspf || (n + 1 == n_ssc); + assert_eq!( + dsync_present(n, n_ssc, aspf), + want, + "n={n} n_ssc={n_ssc} aspf={aspf}" + ); + } + } + } + } + + #[test] + fn degenerate_zero_nssc_does_not_underflow() { + // A malformed caller passing n_ssc == 0 (never produced by the + // wire, since SSC + 1 >= 1) must not panic on the + // `n_subsubframe + 1 == n_ssc` check. With ASPF clear it + // reports no DSYNC; with ASPF set it still reports present. + assert!(!dsync_present(0, 0, false)); + assert!(dsync_present(0, 0, true)); + } + + #[test] + fn decode_valid_word_byte_aligned() { + // 0xffff at bit 0. + let bytes = [0xff, 0xff]; + assert_eq!(decode_dsync_at(&bytes, 0, 3).unwrap(), 16); + } + + #[test] + fn decode_valid_word_non_aligned() { + // 5 leading filler bits, then 0xffff, then trailing pad. + // bits: 00000 1111111111111111 000 + // byte0 = 0b00000_111 = 0x07 + // byte1 = 0b11111111 = 0xff + // byte2 = 0b11111_000 = 0xf8 + let bytes = [0x07, 0xff, 0xf8]; + assert_eq!(decode_dsync_at(&bytes, 5, 0).unwrap(), 16); + } + + #[test] + fn decode_valid_word_crossing_byte_boundary() { + // bit_offset 4: nibble of filler then 0xffff straddling 3 bytes. + // byte0 low nibble = 1111, byte1 = 0xff, byte2 high nibble = 1111 + let bytes = [0x0f, 0xff, 0xf0]; + assert_eq!(decode_dsync_at(&bytes, 4, 1).unwrap(), 16); + } + + #[test] + fn decode_mismatch_surfaces_bad_word_and_index() { + // 0xfffe is not the sync word. + let bytes = [0xff, 0xfe]; + let err = decode_dsync_at(&bytes, 0, 2).unwrap_err(); + assert_eq!( + err, + Error::DsyncMismatch { + found: 0xfffe, + n_subsubframe: 2, + } + ); + } + + #[test] + fn decode_zero_word_is_mismatch() { + let bytes = [0x00, 0x00]; + assert_eq!( + decode_dsync_at(&bytes, 0, 0).unwrap_err(), + Error::DsyncMismatch { + found: 0x0000, + n_subsubframe: 0, + } + ); + } + + #[test] + fn decode_eof_when_fewer_than_16_bits_remain() { + // Only 8 bits available. + let bytes = [0xff]; + assert_eq!( + decode_dsync_at(&bytes, 0, 0).unwrap_err(), + Error::UnexpectedEof + ); + } + + #[test] + fn decode_eof_when_offset_leaves_too_few_bits() { + // 16 bits total, offset 1 leaves only 15. + let bytes = [0xff, 0xff]; + assert_eq!( + decode_dsync_at(&bytes, 1, 0).unwrap_err(), + Error::UnexpectedEof + ); + } + + #[test] + fn decode_consumes_exactly_sixteen_bits() { + // A valid word followed by a distinct trailing byte; the + // reader reports 16 bits consumed (the trailing byte is not + // touched). Confirmed indirectly by reading a second field + // immediately after via a fresh offset. + let bytes = [0xff, 0xff, 0xab]; + let consumed = decode_dsync_at(&bytes, 0, 0).unwrap(); + assert_eq!(consumed, 16); + // The next field begins at bit 16; reading it must see 0xab. + let mut br = BitReader::from_byte_offset(&bytes, 16 / 8); + assert_eq!(br.read_bits(8).unwrap(), 0xab); + } +} diff --git a/crates/vendor/oxideav-dts/src/filter_bank.rs b/crates/vendor/oxideav-dts/src/filter_bank.rs new file mode 100644 index 00000000..468ffabc --- /dev/null +++ b/crates/vendor/oxideav-dts/src/filter_bank.rs @@ -0,0 +1,341 @@ +//! Typed selector for the §C.2.5 `QMFInterpolation()` 512-tap FIR +//! coefficient set. +//! +//! `QMFInterpolation()` (ETSI TS 102 114 V1.3.1 Annex C §C.2.5, PDF +//! p.185, per `docs/audio/dts/dts-core-extracts.md` §2.4) opens with +//! a one-bit `FILTS` parameter that selects between two named §D.8 +//! coefficient sets, transcribed verbatim from the staged §2.4 +//! pseudocode (lines 174-178): +//! +//! ```text +//! QMFInterpolation(FILTS, int nSUBS) { +//! // Select filter +//! if (FILTS==0) prCoeff = raCoeffLossy; // Non-perfect +//! else prCoeff = raCoeffLossLess; // Perfect +//! … +//! } +//! ``` +//! +//! The two coefficient sets (`raCoeffLossy`, the *non-perfect +//! reconstruction* 512-tap interpolation FIR, and `raCoeffLossLess`, +//! the *perfect reconstruction* 512-tap interpolation FIR) are +//! defined in §D.8 "32-Band Interpolation and LFE Interpolation FIR" +//! (staged PDF p.238-246) and transcribed at the crate +//! root as [`crate::RA_COEFF_LOSSY`] / [`crate::RA_COEFF_LOSSLESS`]. +//! +//! This module exposes the §C.2.5 selection step as a typed +//! [`FilterBankSelection`] enum plus a [`FilterBankSelection::from_filts`] +//! resolver that mirrors the spec's `if (FILTS==0) … else …` branch, +//! and [`FilterBankSelection::coefficients`] resolves the selection +//! to the matching §D.8 512-tap table for the §C.2.5 FIR step +//! ([`crate::fir_step`]). +//! +//! ## Relationship to the frame-header `multirate_inter` bit +//! +//! The DTS Core frame header carries a one-bit `MULTIRATE_INTER` +//! field, surfaced as [`crate::DtsFrameHeader::multirate_inter`]. +//! Per ETSI TS 102 114 §5.3 (cited in `wiki/DTS.wiki` line 87) the +//! `MULTIRATE_INTER` bit selects between the same two filter modes +//! the §C.2.5 `FILTS` parameter selects, but the precise polarity +//! mapping (`multirate_inter == 0` → `FILTS == 0` or the inverse) +//! is **not** documented in the staged extracts under +//! `docs/audio/dts/` — neither the `dts-core-extracts.md` §1 header +//! tables (which cover RATE / DYNF / TIMEF only) nor the §2.x +//! filterbank extracts (which cover the §C.2.5 / Annex D side) make +//! the polarity claim. Until that mapping is staged, this module +//! does **not** expose a `DtsFrameHeader::filter_bank_selection()` +//! accessor; callers that need the FIR coefficient set from a parsed +//! header must read [`DtsFrameHeader::multirate_inter`] directly, +//! resolve the polarity from their own out-of-band source, and pass +//! the resulting `FILTS` value (`0` for lossy, non-zero for +//! lossless) to [`FilterBankSelection::from_filts`]. + +/// The two named 512-tap interpolation-FIR coefficient sets +/// referenced by `QMFInterpolation()` per ETSI TS 102 114 V1.3.1 +/// Annex C §C.2.5 (staged in `docs/audio/dts/dts-core-extracts.md` +/// §2.4 lines 175-178). +/// +/// Each variant names exactly one of the two §D.8 "32-Band +/// Interpolation and LFE Interpolation FIR" coefficient tables +/// (PDF p.238-246), transcribed as [`crate::RA_COEFF_LOSSY`] / +/// [`crate::RA_COEFF_LOSSLESS`] and reachable through +/// [`Self::coefficients`]. The variant names +/// mirror the spec pseudocode's identifiers (`raCoeffLossy` for +/// the non-perfect set, `raCoeffLossLess` for the perfect set) +/// rendered in idiomatic Rust. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum FilterBankSelection { + /// The §C.2.5 `raCoeffLossy` 512-tap **non-perfect** + /// reconstruction interpolation FIR (§D.8). Selected by + /// `FILTS == 0` per the §C.2.5 pseudocode's + /// `if (FILTS==0) prCoeff = raCoeffLossy;` branch. + NonPerfectReconstruction, + /// The §C.2.5 `raCoeffLossLess` 512-tap **perfect** + /// reconstruction interpolation FIR (§D.8). Selected by any + /// non-zero `FILTS` value per the §C.2.5 pseudocode's + /// `else prCoeff = raCoeffLossLess;` branch. + PerfectReconstruction, +} + +impl FilterBankSelection { + /// Resolve a §C.2.5 `FILTS` flag value to the named §D.8 + /// coefficient set it picks, per the pseudocode's + /// `if (FILTS==0) prCoeff = raCoeffLossy; else prCoeff = raCoeffLossLess;` + /// branch (`dts-core-extracts.md` §2.4 lines 175-178). + /// + /// Per the spec the `FILTS` parameter is one bit (the §C.2.5 + /// pseudocode treats every non-zero value the same — only the + /// `== 0` branch is distinguished). This resolver therefore + /// accepts an arbitrary `u8` and groups all non-zero inputs + /// into the `PerfectReconstruction` variant, matching the + /// spec's `if (FILTS==0) ... else ...` semantics exactly. + #[must_use] + pub fn from_filts(filts: u8) -> Self { + if filts == 0 { + FilterBankSelection::NonPerfectReconstruction + } else { + FilterBankSelection::PerfectReconstruction + } + } + + /// Inverse of [`Self::from_filts`]: the **canonical** `FILTS` + /// flag value the §C.2.5 pseudocode reads to select this + /// coefficient set. + /// + /// Returns `0` for [`FilterBankSelection::NonPerfectReconstruction`] + /// (the `FILTS == 0` branch) and `1` for + /// [`FilterBankSelection::PerfectReconstruction`] (the canonical + /// "any non-zero value" representative; the spec collapses the + /// entire non-zero range to the same `else` branch, so `1` is + /// the smallest equally-valid choice). + #[must_use] + pub fn filts(self) -> u8 { + match self { + FilterBankSelection::NonPerfectReconstruction => 0, + FilterBankSelection::PerfectReconstruction => 1, + } + } + + /// The §C.2.5 coefficient-table identifier this selection + /// names, as written in the staged §2.4 pseudocode + /// (`raCoeffLossy` or `raCoeffLossLess`). + /// + /// Returned as a `&'static str` so callers can format spec- + /// referencing diagnostics without reaching into the enum + /// variants; the strings match the pseudocode's identifiers + /// verbatim. + #[must_use] + pub fn spec_table_name(self) -> &'static str { + match self { + FilterBankSelection::NonPerfectReconstruction => "raCoeffLossy", + FilterBankSelection::PerfectReconstruction => "raCoeffLossLess", + } + } + + /// The §D.8 512-tap coefficient table this selection picks — + /// the spec's `prCoeff` after the §C.2.5 + /// `if (FILTS==0) prCoeff = raCoeffLossy; else prCoeff = raCoeffLossLess;` + /// assignment, ready for the FIR step ([`crate::fir_step`]). + /// + /// Returns [`crate::RA_COEFF_LOSSY`] (the §D.8 "Non-Perfect + /// Reconstruction" column) for + /// [`FilterBankSelection::NonPerfectReconstruction`] and + /// [`crate::RA_COEFF_LOSSLESS`] (the "Perfect Reconstruction" + /// column) for [`FilterBankSelection::PerfectReconstruction`], + /// both transcribed verbatim from the staged PDF p.238-246. + #[must_use] + pub fn coefficients(self) -> &'static [f64; crate::fir_coeff::FIR_COEFF_LEN] { + match self { + FilterBankSelection::NonPerfectReconstruction => &crate::fir_coeff::RA_COEFF_LOSSY, + FilterBankSelection::PerfectReconstruction => &crate::fir_coeff::RA_COEFF_LOSSLESS, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------- + // from_filts — selection per §C.2.5 pseudocode. + // ----------------------------------------------------------- + + #[test] + fn from_filts_zero_picks_non_perfect_reconstruction() { + // Spec line 176: `if (FILTS==0) prCoeff = raCoeffLossy;` + assert_eq!( + FilterBankSelection::from_filts(0), + FilterBankSelection::NonPerfectReconstruction + ); + } + + #[test] + fn from_filts_one_picks_perfect_reconstruction() { + // Spec line 177: `else prCoeff = raCoeffLossLess;` + assert_eq!( + FilterBankSelection::from_filts(1), + FilterBankSelection::PerfectReconstruction + ); + } + + #[test] + fn from_filts_treats_every_non_zero_value_identically() { + // §C.2.5 uses `if (FILTS==0) … else …` with no further + // discrimination — every non-zero `FILTS` value picks the + // lossless set. Verify across the full u8 range. + for filts in 1u16..=255 { + assert_eq!( + FilterBankSelection::from_filts(filts as u8), + FilterBankSelection::PerfectReconstruction, + "FILTS={filts} should pick PerfectReconstruction per the §C.2.5 else branch" + ); + } + } + + // ----------------------------------------------------------- + // filts — canonical inverse. + // ----------------------------------------------------------- + + #[test] + fn filts_round_trips_non_perfect_reconstruction() { + let sel = FilterBankSelection::NonPerfectReconstruction; + assert_eq!(sel.filts(), 0); + assert_eq!(FilterBankSelection::from_filts(sel.filts()), sel); + } + + #[test] + fn filts_round_trips_perfect_reconstruction() { + let sel = FilterBankSelection::PerfectReconstruction; + assert_eq!(sel.filts(), 1); + assert_eq!(FilterBankSelection::from_filts(sel.filts()), sel); + } + + #[test] + fn from_filts_after_filts_is_identity_for_canonical_values() { + // The canonical `filts()` values 0 and 1 are the spec's two + // distinguishable inputs; round-trip must be the identity. + for sel in [ + FilterBankSelection::NonPerfectReconstruction, + FilterBankSelection::PerfectReconstruction, + ] { + assert_eq!(FilterBankSelection::from_filts(sel.filts()), sel); + } + } + + // ----------------------------------------------------------- + // spec_table_name — pseudocode identifier passthrough. + // ----------------------------------------------------------- + + #[test] + fn spec_table_name_for_non_perfect_is_ra_coeff_lossy() { + // Spec line 176: `prCoeff = raCoeffLossy;` + assert_eq!( + FilterBankSelection::NonPerfectReconstruction.spec_table_name(), + "raCoeffLossy" + ); + } + + #[test] + fn spec_table_name_for_perfect_is_ra_coeff_loss_less() { + // Spec line 177: `prCoeff = raCoeffLossLess;` + assert_eq!( + FilterBankSelection::PerfectReconstruction.spec_table_name(), + "raCoeffLossLess" + ); + } + + #[test] + fn spec_table_names_are_distinct() { + // Sanity: the two §C.2.5 identifiers are not aliases for + // the same string — they refer to two different §D.8 + // coefficient sets. + assert_ne!( + FilterBankSelection::NonPerfectReconstruction.spec_table_name(), + FilterBankSelection::PerfectReconstruction.spec_table_name() + ); + } + + // ----------------------------------------------------------- + // coefficients — §D.8 table resolution. + // ----------------------------------------------------------- + + #[test] + fn coefficients_for_non_perfect_is_the_lossy_table() { + // Spec line 176: `if (FILTS==0) prCoeff = raCoeffLossy;` — + // the non-perfect variant resolves to the §D.8 "Non-Perfect + // Reconstruction" column. + assert!(core::ptr::eq( + FilterBankSelection::NonPerfectReconstruction.coefficients(), + &crate::fir_coeff::RA_COEFF_LOSSY, + )); + } + + #[test] + fn coefficients_for_perfect_is_the_lossless_table() { + // Spec line 177: `else prCoeff = raCoeffLossLess;` — the + // perfect variant resolves to the §D.8 "Perfect + // Reconstruction" column. + assert!(core::ptr::eq( + FilterBankSelection::PerfectReconstruction.coefficients(), + &crate::fir_coeff::RA_COEFF_LOSSLESS, + )); + } + + #[test] + fn coefficients_composed_with_from_filts_reproduces_the_spec_branch() { + // from_filts + coefficients together are the §C.2.5 + // two-line `prCoeff` assignment. + assert!(core::ptr::eq( + FilterBankSelection::from_filts(0).coefficients(), + &crate::fir_coeff::RA_COEFF_LOSSY, + )); + assert!(core::ptr::eq( + FilterBankSelection::from_filts(1).coefficients(), + &crate::fir_coeff::RA_COEFF_LOSSLESS, + )); + } + + // ----------------------------------------------------------- + // Trait derives. + // ----------------------------------------------------------- + + #[test] + fn variants_are_copyable_and_comparable() { + // The enum is Copy + PartialEq + Eq + Hash by derive; make + // sure those land and behave as expected (a sibling crate + // using this enum in a HashMap key needs Hash + Eq). + let a = FilterBankSelection::NonPerfectReconstruction; + let b = a; + assert_eq!(a, b); + assert_ne!(a, FilterBankSelection::PerfectReconstruction); + + // Hash collision check: hash both variants into the same + // hasher and confirm the resulting digests differ — not a + // proof of correctness but a sanity check that the derive + // hashes the discriminant. + use core::hash::{Hash, Hasher}; + let mut h1 = std::collections::hash_map::DefaultHasher::new(); + FilterBankSelection::NonPerfectReconstruction.hash(&mut h1); + let mut h2 = std::collections::hash_map::DefaultHasher::new(); + FilterBankSelection::PerfectReconstruction.hash(&mut h2); + assert_ne!(h1.finish(), h2.finish()); + } + + #[test] + fn variants_have_stable_debug_output() { + // Debug-format the variants so downstream test failures + // print recognisable enum-variant names rather than opaque + // discriminant integers. + let s = format!("{:?}", FilterBankSelection::NonPerfectReconstruction); + assert!( + s.contains("NonPerfectReconstruction"), + "Debug should name the variant, got {s:?}" + ); + let s = format!("{:?}", FilterBankSelection::PerfectReconstruction); + assert!( + s.contains("PerfectReconstruction"), + "Debug should name the variant, got {s:?}" + ); + } +} diff --git a/crates/vendor/oxideav-dts/src/fir_coeff.rs b/crates/vendor/oxideav-dts/src/fir_coeff.rs new file mode 100644 index 00000000..3636a354 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/fir_coeff.rs @@ -0,0 +1,453 @@ +//! §D.8 32-band interpolation FIR coefficient tables for the DTS +//! Core 32-band synthesis QMF. +//! +//! Transcribed verbatim from ETSI TS 102 114 V1.3.1 (2011-08) +//! Annex D §D.8 "32-Band Interpolation and LFE Interpolation FIR" +//! (staged at `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`, +//! PDF p.238-246), columns "Perfect Reconstruction" and "Non-Perfect +//! Reconstruction" (indices 0..=511; the spec table prints decimal +//! commas, rendered here as decimal points). These are the two +//! 512-tap `prCoeff` sets the §C.2.5 `QMFInterpolation()` FIR step +//! convolves against the `raX[]` shift register +//! (`docs/audio/dts/dts-core-extracts.md` §2.4): +//! +//! - [`RA_COEFF_LOSSLESS`] is the spec pseudocode's `raCoeffLossLess` +//! identifier — the **perfect reconstruction** set, selected by the +//! `else` branch of `if (FILTS==0) ...` (i.e. `FILTS != 0`). +//! - [`RA_COEFF_LOSSY`] is the spec pseudocode's `raCoeffLossy` +//! identifier — the **non-perfect reconstruction** set, selected by +//! the `FILTS == 0` branch. +//! +//! [`crate::FilterBankSelection::coefficients`] resolves the typed +//! round-263 selector to the matching table. +//! +//! The same §D.8 table also carries two LFE interpolation FIR columns +//! ("64 x Interpolation" / "128 x Interpolation"); those drive the +//! LFE decimation/interpolation path, not the 32-band synthesis QMF, +//! and live in a separate module ([`crate::lfe_fir_coeff`], selected +//! by [`crate::LfeInterpolationSelection`]). +//! +//! Both tables are exactly antisymmetric in their printed digits +//! (`coeff[i] == -coeff[511 - i]` for every `i`); the test suite +//! verifies that whole-table property plus verbatim anchor rows at +//! every page boundary of the staged PDF (p.238/239/240/241/242/ +//! 243/244/245/246). + +/// Number of taps in each §D.8 32-band interpolation FIR set, fixed +/// by the §C.2.5 FIR step's `for (j=0; j<512; j+=64)` bound and equal +/// to [`crate::X_HISTORY_LEN`] (the `raX[]` register the taps are +/// convolved against). +pub const FIR_COEFF_LEN: usize = 512; + +/// §D.8 "Perfect Reconstruction" 512-tap 32-band interpolation FIR — +/// the §C.2.5 pseudocode's `raCoeffLossLess` set, selected by +/// `FILTS != 0` (ETSI TS 102 114 V1.3.1 §D.8, staged PDF p.238-246). +#[rustfmt::skip] +pub static RA_COEFF_LOSSLESS: [f64; FIR_COEFF_LEN] = [ + 1.140033200e-10, 7.138742100e-11, -8.358679600e-09, -2.529296600e-08, // 0..=3 + -9.130198800e-08, -2.771560000e-07, -5.746147600e-07, -3.712986200e-07, // 4..=7 + -4.468735700e-07, -5.697322600e-07, -6.300390500e-07, -6.677818900e-07, // 8..=11 + -6.770656500e-07, -6.601852300e-07, -6.193701600e-07, -5.586146700e-07, // 12..=15 + 7.034745600e-07, 8.348606100e-07, 9.544782800e-07, 1.052683900e-06, // 16..=19 + 1.119829700e-06, 1.144180200e-06, 1.124542400e-06, 9.822894700e-07, // 20..=23 + 8.920065800e-07, 1.560941800e-06, 8.454480100e-07, 3.167104300e-07, // 24..=27 + 1.028149000e-07, 4.147967800e-08, -6.821591800e-10, -1.611726200e-09, // 28..=31 + -2.668096400e-09, -3.377455500e-09, 6.820855300e-09, 3.715261200e-09, // 32..=35 + 1.643020800e-08, 1.007547900e-07, 2.448299500e-07, 1.306777300e-06, // 36..=39 + 1.904890000e-06, 2.555774300e-06, 3.253336000e-06, 3.953604500e-06, // 40..=43 + 4.617880200e-06, 5.210775600e-06, 5.696789700e-06, 6.046428700e-06, // 44..=47 + 7.614387900e-06, 7.678809700e-06, 7.533601500e-06, 7.179758900e-06, // 48..=51 + 6.629955000e-06, 5.908209500e-06, 5.044609200e-06, 4.187209700e-06, // 52..=55 + 3.139397100e-06, 6.650809100e-07, 3.073465500e-07, 5.699348500e-08, // 56..=59 + 1.510238900e-08, 3.384827600e-08, -3.227406600e-08, -3.772031200e-08, // 60..=63 + 8.454083600e-08, 6.479789100e-08, 1.236415900e-06, 2.480143600e-06, // 64..=67 + 3.694976800e-06, 3.742137100e-06, 3.262621300e-06, 7.476824700e-06, // 68..=71 + 9.321632700e-06, 1.121856000e-05, 1.317522400e-05, 1.505747500e-05, // 72..=75 + 1.676702500e-05, 1.819741000e-05, 1.925789500e-05, 1.987389300e-05, // 76..=79 + -3.076839000e-05, -3.254459900e-05, -3.367812600e-05, -3.411568400e-05, // 80..=83 + -3.382472000e-05, -3.280414400e-05, -3.109003600e-05, -2.861654300e-05, // 84..=87 + -2.571454500e-05, -1.870056200e-05, -1.771374800e-05, -1.568432200e-05, // 88..=91 + -1.128458200e-05, -6.805568100e-06, -5.671807300e-07, -9.974569000e-07, // 92..=95 + -1.466421500e-06, -1.846174800e-06, 7.763173700e-08, 1.809597500e-06, // 96..=99 + 4.157326000e-06, 7.240269200e-06, 1.073666400e-05, 2.089583800e-05, // 100..=103 + 2.647159500e-05, 3.196094400e-05, 3.698112500e-05, 4.149260300e-05, // 104..=107 + 4.534151200e-05, 4.846834800e-05, 5.081695700e-05, 5.236303900e-05, // 108..=111 + 3.803557300e-06, 7.916183300e-06, 1.191309700e-05, 1.561346600e-05, // 112..=115 + 1.881671400e-05, 2.131957100e-05, 2.295038200e-05, 2.354812700e-05, // 116..=119 + 2.291622100e-05, 2.497457200e-05, 1.979628700e-05, 1.390508100e-05, // 120..=123 + 7.179248900e-06, -1.614022200e-07, -1.518084500e-05, -1.610369300e-05, // 124..=127 + 1.994364800e-05, 1.774116500e-05, 4.511232400e-05, 5.311715600e-05, // 128..=131 + 6.144976200e-05, 7.052899300e-05, 7.984114900e-05, 8.597821200e-05, // 132..=135 + 9.341758200e-05, 1.002681400e-04, 1.064814700e-04, 1.119841200e-04, // 136..=139 + 1.165901700e-04, 1.202018700e-04, 1.226936800e-04, 1.237377900e-04, // 140..=143 + 1.200453700e-04, 1.185602000e-04, 1.152534400e-04, 1.097435100e-04, // 144..=147 + 1.018237000e-04, 9.130172200e-05, 7.793692700e-05, 6.157321800e-05, // 148..=151 + 4.214289700e-05, 2.010055900e-05, -6.512868000e-06, -3.623958500e-05, // 152..=155 + -6.898332300e-05, -1.052143400e-04, -1.311540500e-04, -1.772621900e-04, // 156..=159 + -2.231129500e-04, -2.678985000e-04, -3.353960600e-04, -3.909221300e-04, // 160..=163 + -4.488403900e-04, -5.091327500e-04, -5.717321000e-04, -6.360244700e-04, // 164..=167 + -7.021067600e-04, -7.695597500e-04, -8.380918900e-04, -9.072555100e-04, // 168..=171 + -9.767158300e-04, -1.045985500e-03, -1.114606900e-03, -1.182107000e-03, // 172..=175 + -1.251459700e-03, -1.314813200e-03, -1.375058300e-03, -1.431717500e-03, // 176..=179 + -1.484159500e-03, -1.531686400e-03, -1.573715600e-03, -1.609496400e-03, // 180..=183 + -1.638393400e-03, -1.659751400e-03, -1.672691700e-03, -1.676540900e-03, // 184..=187 + -1.670887400e-03, -1.654649800e-03, -1.632849400e-03, -1.592423900e-03, // 188..=191 + 1.541196600e-03, 1.478566700e-03, 1.394017000e-03, 1.301623400e-03, // 192..=195 + 1.194737700e-03, 1.072608600e-03, 9.349224800e-04, 7.810380900e-04, // 196..=199 + 6.109076600e-04, 4.241331700e-04, 2.204804700e-04, -2.272228400e-07, // 200..=203 + -2.380696500e-04, -4.930996000e-04, -7.653038000e-04, -1.054538000e-03, // 204..=207 + -1.360519200e-03, -1.683383000e-03, -2.022614600e-03, -2.377899500e-03, // 208..=211 + -2.748797700e-03, -3.134797500e-03, -3.535329200e-03, -3.949734800e-03, // 212..=215 + -4.377291000e-03, -4.817122000e-03, -5.268542300e-03, -5.730478300e-03, // 216..=219 + -6.202005100e-03, -6.681936000e-03, -7.167914500e-03, -7.662045500e-03, // 220..=223 + -8.160839200e-03, -8.663190500e-03, -9.169050700e-03, -9.675131500e-03, // 224..=227 + -1.018101800e-02, -1.068536400e-02, -1.118674000e-02, -1.168377500e-02, // 228..=231 + -1.217496400e-02, -1.265891600e-02, -1.313420500e-02, -1.359941000e-02, // 232..=235 + -1.405313100e-02, -1.449398400e-02, -1.492061500e-02, -1.533170500e-02, // 236..=239 + -1.572581500e-02, -1.610200000e-02, -1.645893800e-02, -1.679548100e-02, // 240..=243 + -1.711054800e-02, -1.740312600e-02, -1.767225900e-02, -1.791707200e-02, // 244..=247 + -1.813675700e-02, -1.833061700e-02, -1.849797400e-02, -1.863830100e-02, // 248..=251 + -1.875108700e-02, -1.883604000e-02, -1.889315300e-02, -1.892151600e-02, // 252..=255 + 1.892151600e-02, 1.889315300e-02, 1.883604000e-02, 1.875108700e-02, // 256..=259 + 1.863830100e-02, 1.849797400e-02, 1.833061700e-02, 1.813675700e-02, // 260..=263 + 1.791707200e-02, 1.767225900e-02, 1.740312600e-02, 1.711054800e-02, // 264..=267 + 1.679548100e-02, 1.645893800e-02, 1.610200000e-02, 1.572581500e-02, // 268..=271 + 1.533170500e-02, 1.492061500e-02, 1.449398400e-02, 1.405313100e-02, // 272..=275 + 1.359941000e-02, 1.313420500e-02, 1.265891600e-02, 1.217496400e-02, // 276..=279 + 1.168377500e-02, 1.118674000e-02, 1.068536400e-02, 1.018101800e-02, // 280..=283 + 9.675131500e-03, 9.169050700e-03, 8.663190500e-03, 8.160839200e-03, // 284..=287 + 7.662045500e-03, 7.167914500e-03, 6.681936000e-03, 6.202005100e-03, // 288..=291 + 5.730478300e-03, 5.268542300e-03, 4.817122000e-03, 4.377291000e-03, // 292..=295 + 3.949734800e-03, 3.535329200e-03, 3.134797500e-03, 2.748797700e-03, // 296..=299 + 2.377899500e-03, 2.022614600e-03, 1.683383000e-03, 1.360519200e-03, // 300..=303 + 1.054538000e-03, 7.653038000e-04, 4.930996000e-04, 2.380696500e-04, // 304..=307 + 2.272228400e-07, -2.204804700e-04, -4.241331700e-04, -6.109076600e-04, // 308..=311 + -7.810380900e-04, -9.349224800e-04, -1.072608600e-03, -1.194737700e-03, // 312..=315 + -1.301623400e-03, -1.394017000e-03, -1.478566700e-03, -1.541196600e-03, // 316..=319 + 1.592423900e-03, 1.632849400e-03, 1.654649800e-03, 1.670887400e-03, // 320..=323 + 1.676540900e-03, 1.672691700e-03, 1.659751400e-03, 1.638393400e-03, // 324..=327 + 1.609496400e-03, 1.573715600e-03, 1.531686400e-03, 1.484159500e-03, // 328..=331 + 1.431717500e-03, 1.375058300e-03, 1.314813200e-03, 1.251459700e-03, // 332..=335 + 1.182107000e-03, 1.114606900e-03, 1.045985500e-03, 9.767158300e-04, // 336..=339 + 9.072555100e-04, 8.380918900e-04, 7.695597500e-04, 7.021067600e-04, // 340..=343 + 6.360244700e-04, 5.717321000e-04, 5.091327500e-04, 4.488403900e-04, // 344..=347 + 3.909221300e-04, 3.353960600e-04, 2.678985000e-04, 2.231129500e-04, // 348..=351 + 1.772621900e-04, 1.311540500e-04, 1.052143400e-04, 6.898332300e-05, // 352..=355 + 3.623958500e-05, 6.512868000e-06, -2.010055900e-05, -4.214289700e-05, // 356..=359 + -6.157321800e-05, -7.793692700e-05, -9.130172200e-05, -1.018237000e-04, // 360..=363 + -1.097435100e-04, -1.152534400e-04, -1.185602000e-04, -1.200453700e-04, // 364..=367 + -1.237377900e-04, -1.226936800e-04, -1.202018700e-04, -1.165901700e-04, // 368..=371 + -1.119841200e-04, -1.064814700e-04, -1.002681400e-04, -9.341758200e-05, // 372..=375 + -8.597821200e-05, -7.984114900e-05, -7.052899300e-05, -6.144976200e-05, // 376..=379 + -5.311715600e-05, -4.511232400e-05, -1.774116500e-05, -1.994364800e-05, // 380..=383 + 1.610369300e-05, 1.518084500e-05, 1.614022200e-07, -7.179248900e-06, // 384..=387 + -1.390508100e-05, -1.979628700e-05, -2.497457200e-05, -2.291622100e-05, // 388..=391 + -2.354812700e-05, -2.295038200e-05, -2.131957100e-05, -1.881671400e-05, // 392..=395 + -1.561346600e-05, -1.191309700e-05, -7.916183300e-06, -3.803557300e-06, // 396..=399 + -5.236303900e-05, -5.081695700e-05, -4.846834800e-05, -4.534151200e-05, // 400..=403 + -4.149260300e-05, -3.698112500e-05, -3.196094400e-05, -2.647159500e-05, // 404..=407 + -2.089583800e-05, -1.073666400e-05, -7.240269200e-06, -4.157326000e-06, // 408..=411 + -1.809597500e-06, -7.763173700e-08, 1.846174800e-06, 1.466421500e-06, // 412..=415 + 9.974569000e-07, 5.671807300e-07, 6.805568100e-06, 1.128458200e-05, // 416..=419 + 1.568432200e-05, 1.771374800e-05, 1.870056200e-05, 2.571454500e-05, // 420..=423 + 2.861654300e-05, 3.109003600e-05, 3.280414400e-05, 3.382472000e-05, // 424..=427 + 3.411568400e-05, 3.367812600e-05, 3.254459900e-05, 3.076839000e-05, // 428..=431 + -1.987389300e-05, -1.925789500e-05, -1.819741000e-05, -1.676702500e-05, // 432..=435 + -1.505747500e-05, -1.317522400e-05, -1.121856000e-05, -9.321632700e-06, // 436..=439 + -7.476824700e-06, -3.262621300e-06, -3.742137100e-06, -3.694976800e-06, // 440..=443 + -2.480143600e-06, -1.236415900e-06, -6.479789100e-08, -8.454083600e-08, // 444..=447 + 3.772031200e-08, 3.227406600e-08, -3.384827600e-08, -1.510238900e-08, // 448..=451 + -5.699348500e-08, -3.073465500e-07, -6.650809100e-07, -3.139397100e-06, // 452..=455 + -4.187209700e-06, -5.044609200e-06, -5.908209500e-06, -6.629955000e-06, // 456..=459 + -7.179758900e-06, -7.533601500e-06, -7.678809700e-06, -7.614387900e-06, // 460..=463 + -6.046428700e-06, -5.696789700e-06, -5.210775600e-06, -4.617880200e-06, // 464..=467 + -3.953604500e-06, -3.253336000e-06, -2.555774300e-06, -1.904890000e-06, // 468..=471 + -1.306777300e-06, -2.448299500e-07, -1.007547900e-07, -1.643020800e-08, // 472..=475 + -3.715261200e-09, -6.820855300e-09, 3.377455500e-09, 2.668096400e-09, // 476..=479 + 1.611726200e-09, 6.821591800e-10, -4.147967800e-08, -1.028149000e-07, // 480..=483 + -3.167104300e-07, -8.454480100e-07, -1.560941800e-06, -8.920065800e-07, // 484..=487 + -9.822894700e-07, -1.124542400e-06, -1.144180200e-06, -1.119829700e-06, // 488..=491 + -1.052683900e-06, -9.544782800e-07, -8.348606100e-07, -7.034745600e-07, // 492..=495 + 5.586146700e-07, 6.193701600e-07, 6.601852300e-07, 6.770656500e-07, // 496..=499 + 6.677818900e-07, 6.300390500e-07, 5.697322600e-07, 4.468735700e-07, // 500..=503 + 3.712986200e-07, 5.746147600e-07, 2.771560000e-07, 9.130198800e-08, // 504..=507 + 2.529296600e-08, 8.358679600e-09, -7.138742100e-11, -1.140033200e-10, // 508..=511 +]; + +/// §D.8 "Non-Perfect Reconstruction" 512-tap 32-band interpolation +/// FIR — the §C.2.5 pseudocode's `raCoeffLossy` set, selected by +/// `FILTS == 0` (ETSI TS 102 114 V1.3.1 §D.8, staged PDF p.238-246). +#[rustfmt::skip] +pub static RA_COEFF_LOSSY: [f64; FIR_COEFF_LEN] = [ + -1.390191784e-07, -1.693738625e-07, -2.030677564e-07, -2.404238444e-07, // 0..=3 + -2.818143514e-07, -3.276689142e-07, -3.784752209e-07, -4.347855338e-07, // 4..=7 + -4.972276315e-07, -5.665120852e-07, -6.434325428e-07, -7.288739425e-07, // 8..=11 + -8.238164355e-07, -9.293416952e-07, -1.046637067e-06, -1.176999604e-06, // 12..=15 + -1.321840614e-06, -1.482681114e-06, -1.661159786e-06, -1.859034001e-06, // 16..=19 + -2.078171747e-06, -2.320550948e-06, -2.588257530e-06, -2.883470643e-06, // 20..=23 + -3.208459020e-06, -3.565570978e-06, -3.957220997e-06, -4.385879038e-06, // 24..=27 + -4.854050530e-06, -5.364252502e-06, -5.918994248e-06, -6.520755960e-06, // 28..=31 + -7.171964626e-06, -7.874960829e-06, -8.631964192e-06, -9.445050637e-06, // 32..=35 + -1.031611009e-05, -1.124680875e-05, -1.223855270e-05, -1.329243969e-05, // 36..=39 + -1.440921824e-05, -1.558924305e-05, -1.683242772e-05, -1.813820381e-05, // 40..=43 + -1.950545993e-05, -2.093250441e-05, -2.241701623e-05, -2.395598858e-05, // 44..=47 + -2.554569073e-05, -2.718161704e-05, -2.885844333e-05, -3.056998685e-05, // 48..=51 + -3.230916263e-05, -3.406793985e-05, -3.583733633e-05, -3.760734762e-05, // 52..=55 + -3.936696885e-05, -4.110412556e-05, -4.280570283e-05, -4.445751256e-05, // 56..=59 + -4.604430433e-05, -4.754976908e-05, -4.895655002e-05, -5.024627535e-05, // 60..=63 + 5.139957648e-05, 5.239612074e-05, 5.321469871e-05, 5.383323878e-05, // 64..=67 + 5.422891263e-05, 5.437819709e-05, 5.425697600e-05, 5.384063843e-05, // 68..=71 + 5.310418419e-05, 5.202236207e-05, 5.056979353e-05, 4.872112549e-05, // 72..=75 + 4.645117951e-05, 4.373511547e-05, 4.054862075e-05, 3.686808850e-05, // 76..=79 + 3.267079956e-05, 2.793515523e-05, 2.264085742e-05, 1.676913780e-05, // 80..=83 + 1.030297699e-05, 3.227306706e-06, -4.470633485e-06, -1.280130618e-05, // 84..=87 + -2.177240640e-05, -3.138873581e-05, -4.165195787e-05, -5.256036457e-05, // 88..=91 + -6.410864444e-05, -7.628766616e-05, -8.908427117e-05, -1.024810626e-04, // 92..=95 + -1.164562127e-04, -1.309833024e-04, -1.460311323e-04, -1.615635992e-04, // 96..=99 + -1.775395358e-04, -1.939126523e-04, -2.106313768e-04, -2.276388550e-04, // 100..=103 + -2.448728774e-04, -2.622658503e-04, -2.797449124e-04, -2.972317743e-04, // 104..=107 + -3.146430245e-04, -3.318900708e-04, -3.488793736e-04, -3.655125911e-04, // 108..=111 + -3.816867538e-04, -3.972945851e-04, -4.122247046e-04, -4.263620067e-04, // 112..=115 + -4.395879805e-04, -4.517810594e-04, -4.628172028e-04, -4.725702747e-04, // 116..=119 + -4.809123348e-04, -4.877146275e-04, -4.928477574e-04, -4.961824161e-04, // 120..=123 + -4.975944757e-04, -4.969481961e-04, -4.941228544e-04, -4.889960401e-04, // 124..=127 + 4.814492422e-04, 4.713678791e-04, 4.586426076e-04, 4.431701091e-04, // 128..=131 + 4.248536134e-04, 4.036037717e-04, 3.793396754e-04, 3.519894381e-04, // 132..=135 + 3.214911267e-04, 2.877934603e-04, 2.508567995e-04, 2.106537577e-04, // 136..=139 + 1.671699720e-04, 1.204049113e-04, 7.037253090e-05, 1.710198012e-05, // 140..=143 + -3.936182839e-05, -9.895755647e-05, -1.616069785e-04, -2.272142592e-04, // 144..=147 + -2.956659591e-04, -3.668301215e-04, -4.405563814e-04, -5.166754709e-04, // 148..=151 + -5.949990009e-04, -6.753197522e-04, -7.574109477e-04, -8.410271257e-04, // 152..=155 + -9.259034996e-04, -1.011756598e-03, -1.098284614e-03, -1.185167348e-03, // 156..=159 + -1.272067428e-03, -1.358630019e-03, -1.444484224e-03, -1.529243193e-03, // 160..=163 + -1.612505526e-03, -1.693855622e-03, -1.772865304e-03, -1.849094522e-03, // 164..=167 + -1.922092517e-03, -1.991399564e-03, -2.056547208e-03, -2.117061289e-03, // 168..=171 + -2.172462177e-03, -2.222266514e-03, -2.265989315e-03, -2.303145360e-03, // 172..=175 + -2.333251061e-03, -2.355825622e-03, -2.370394068e-03, -2.376487479e-03, // 176..=179 + -2.373647178e-03, -2.361423569e-03, -2.339380793e-03, -2.307097195e-03, // 180..=183 + -2.264167881e-03, -2.210205887e-03, -2.144844970e-03, -2.067740774e-03, // 184..=187 + -1.978572691e-03, -1.877046190e-03, -1.762894331e-03, -1.635878929e-03, // 188..=191 + 1.495792647e-03, 1.342460280e-03, 1.175740734e-03, 9.955273708e-04, // 192..=195 + 8.017504588e-04, 5.943773431e-04, 3.734139318e-04, 1.389056415e-04, // 196..=199 + -1.090620208e-04, -3.703625989e-04, -6.448282511e-04, -9.322494152e-04, // 200..=203 + -1.232374110e-03, -1.544908970e-03, -1.869517611e-03, -2.205822384e-03, // 204..=207 + -2.553403843e-03, -2.911801683e-03, -3.280514618e-03, -3.659002949e-03, // 208..=211 + -4.046686925e-03, -4.442950245e-03, -4.847140983e-03, -5.258570891e-03, // 212..=215 + -5.676518660e-03, -6.100233644e-03, -6.528933067e-03, -6.961807609e-03, // 216..=219 + -7.398022339e-03, -7.836719044e-03, -8.277016692e-03, -8.718019351e-03, // 220..=223 + -9.158811532e-03, -9.598465636e-03, -1.003604382e-02, -1.047059800e-02, // 224..=227 + -1.090117730e-02, -1.132682897e-02, -1.174659748e-02, -1.215953380e-02, // 228..=231 + -1.256469358e-02, -1.296114177e-02, -1.334795821e-02, -1.372423489e-02, // 232..=235 + -1.408908330e-02, -1.444163360e-02, -1.478104480e-02, -1.510649733e-02, // 236..=239 + -1.541720331e-02, -1.571240649e-02, -1.599138230e-02, -1.625344716e-02, // 240..=243 + -1.649795473e-02, -1.672429405e-02, -1.693190821e-02, -1.712027565e-02, // 244..=247 + -1.728892699e-02, -1.743743755e-02, -1.756543480e-02, -1.767260395e-02, // 248..=251 + -1.775865816e-02, -1.782339066e-02, -1.786663756e-02, -1.788828894e-02, // 252..=255 + 1.788828894e-02, 1.786663756e-02, 1.782339066e-02, 1.775865816e-02, // 256..=259 + 1.767260395e-02, 1.756543480e-02, 1.743743755e-02, 1.728892699e-02, // 260..=263 + 1.712027565e-02, 1.693190821e-02, 1.672429405e-02, 1.649795473e-02, // 264..=267 + 1.625344716e-02, 1.599138230e-02, 1.571240649e-02, 1.541720331e-02, // 268..=271 + 1.510649733e-02, 1.478104480e-02, 1.444163360e-02, 1.408908330e-02, // 272..=275 + 1.372423489e-02, 1.334795821e-02, 1.296114177e-02, 1.256469358e-02, // 276..=279 + 1.215953380e-02, 1.174659748e-02, 1.132682897e-02, 1.090117730e-02, // 280..=283 + 1.047059800e-02, 1.003604382e-02, 9.598465636e-03, 9.158811532e-03, // 284..=287 + 8.718019351e-03, 8.277016692e-03, 7.836719044e-03, 7.398022339e-03, // 288..=291 + 6.961807609e-03, 6.528933067e-03, 6.100233644e-03, 5.676518660e-03, // 292..=295 + 5.258570891e-03, 4.847140983e-03, 4.442950245e-03, 4.046686925e-03, // 296..=299 + 3.659002949e-03, 3.280514618e-03, 2.911801683e-03, 2.553403843e-03, // 300..=303 + 2.205822384e-03, 1.869517611e-03, 1.544908970e-03, 1.232374110e-03, // 304..=307 + 9.322494152e-04, 6.448282511e-04, 3.703625989e-04, 1.090620208e-04, // 308..=311 + -1.389056415e-04, -3.734139318e-04, -5.943773431e-04, -8.017504588e-04, // 312..=315 + -9.955273708e-04, -1.175740734e-03, -1.342460280e-03, -1.495792647e-03, // 316..=319 + 1.635878929e-03, 1.762894331e-03, 1.877046190e-03, 1.978572691e-03, // 320..=323 + 2.067740774e-03, 2.144844970e-03, 2.210205887e-03, 2.264167881e-03, // 324..=327 + 2.307097195e-03, 2.339380793e-03, 2.361423569e-03, 2.373647178e-03, // 328..=331 + 2.376487479e-03, 2.370394068e-03, 2.355825622e-03, 2.333251061e-03, // 332..=335 + 2.303145360e-03, 2.265989315e-03, 2.222266514e-03, 2.172462177e-03, // 336..=339 + 2.117061289e-03, 2.056547208e-03, 1.991399564e-03, 1.922092517e-03, // 340..=343 + 1.849094522e-03, 1.772865304e-03, 1.693855622e-03, 1.612505526e-03, // 344..=347 + 1.529243193e-03, 1.444484224e-03, 1.358630019e-03, 1.272067428e-03, // 348..=351 + 1.185167348e-03, 1.098284614e-03, 1.011756598e-03, 9.259034996e-04, // 352..=355 + 8.410271257e-04, 7.574109477e-04, 6.753197522e-04, 5.949990009e-04, // 356..=359 + 5.166754709e-04, 4.405563814e-04, 3.668301215e-04, 2.956659591e-04, // 360..=363 + 2.272142592e-04, 1.616069785e-04, 9.895755647e-05, 3.936182839e-05, // 364..=367 + -1.710198012e-05, -7.037253090e-05, -1.204049113e-04, -1.671699720e-04, // 368..=371 + -2.106537577e-04, -2.508567995e-04, -2.877934603e-04, -3.214911267e-04, // 372..=375 + -3.519894381e-04, -3.793396754e-04, -4.036037717e-04, -4.248536134e-04, // 376..=379 + -4.431701091e-04, -4.586426076e-04, -4.713678791e-04, -4.814492422e-04, // 380..=383 + 4.889960401e-04, 4.941228544e-04, 4.969481961e-04, 4.975944757e-04, // 384..=387 + 4.961824161e-04, 4.928477574e-04, 4.877146275e-04, 4.809123348e-04, // 388..=391 + 4.725702747e-04, 4.628172028e-04, 4.517810594e-04, 4.395879805e-04, // 392..=395 + 4.263620067e-04, 4.122247046e-04, 3.972945851e-04, 3.816867538e-04, // 396..=399 + 3.655125911e-04, 3.488793736e-04, 3.318900708e-04, 3.146430245e-04, // 400..=403 + 2.972317743e-04, 2.797449124e-04, 2.622658503e-04, 2.448728774e-04, // 404..=407 + 2.276388550e-04, 2.106313768e-04, 1.939126523e-04, 1.775395358e-04, // 408..=411 + 1.615635992e-04, 1.460311323e-04, 1.309833024e-04, 1.164562127e-04, // 412..=415 + 1.024810626e-04, 8.908427117e-05, 7.628766616e-05, 6.410864444e-05, // 416..=419 + 5.256036457e-05, 4.165195787e-05, 3.138873581e-05, 2.177240640e-05, // 420..=423 + 1.280130618e-05, 4.470633485e-06, -3.227306706e-06, -1.030297699e-05, // 424..=427 + -1.676913780e-05, -2.264085742e-05, -2.793515523e-05, -3.267079956e-05, // 428..=431 + -3.686808850e-05, -4.054862075e-05, -4.373511547e-05, -4.645117951e-05, // 432..=435 + -4.872112549e-05, -5.056979353e-05, -5.202236207e-05, -5.310418419e-05, // 436..=439 + -5.384063843e-05, -5.425697600e-05, -5.437819709e-05, -5.422891263e-05, // 440..=443 + -5.383323878e-05, -5.321469871e-05, -5.239612074e-05, -5.139957648e-05, // 444..=447 + 5.024627535e-05, 4.895655002e-05, 4.754976908e-05, 4.604430433e-05, // 448..=451 + 4.445751256e-05, 4.280570283e-05, 4.110412556e-05, 3.936696885e-05, // 452..=455 + 3.760734762e-05, 3.583733633e-05, 3.406793985e-05, 3.230916263e-05, // 456..=459 + 3.056998685e-05, 2.885844333e-05, 2.718161704e-05, 2.554569073e-05, // 460..=463 + 2.395598858e-05, 2.241701623e-05, 2.093250441e-05, 1.950545993e-05, // 464..=467 + 1.813820381e-05, 1.683242772e-05, 1.558924305e-05, 1.440921824e-05, // 468..=471 + 1.329243969e-05, 1.223855270e-05, 1.124680875e-05, 1.031611009e-05, // 472..=475 + 9.445050637e-06, 8.631964192e-06, 7.874960829e-06, 7.171964626e-06, // 476..=479 + 6.520755960e-06, 5.918994248e-06, 5.364252502e-06, 4.854050530e-06, // 480..=483 + 4.385879038e-06, 3.957220997e-06, 3.565570978e-06, 3.208459020e-06, // 484..=487 + 2.883470643e-06, 2.588257530e-06, 2.320550948e-06, 2.078171747e-06, // 488..=491 + 1.859034001e-06, 1.661159786e-06, 1.482681114e-06, 1.321840614e-06, // 492..=495 + 1.176999604e-06, 1.046637067e-06, 9.293416952e-07, 8.238164355e-07, // 496..=499 + 7.288739425e-07, 6.434325428e-07, 5.665120852e-07, 4.972276315e-07, // 500..=503 + 4.347855338e-07, 3.784752209e-07, 3.276689142e-07, 2.818143514e-07, // 504..=507 + 2.404238444e-07, 2.030677564e-07, 1.693738625e-07, 1.390191784e-07, // 508..=511 +]; + +#[cfg(test)] +mod tests { + use super::*; + + /// Verbatim §D.8 anchor rows, read directly off the staged PDF + /// pages (decimal commas rendered as points). One row from each + /// side of every page boundary (p.238/239/.../246) plus the + /// first / centre / last rows, so a transcription slip at any + /// page seam or table end is caught against an independently + /// read literal: (index, Perfect, Non-Perfect). + const PAGE_ANCHORS: &[(usize, f64, f64)] = &[ + // p.238 first row and last row (indices 0..=49). + (0, 1.140033200e-10, -1.390191784e-07), + (49, 7.678809700e-06, -2.718161704e-05), + // p.238 → p.239 seam. + (50, 7.533601500e-06, -2.885844333e-05), + (112, 3.803557300e-06, -3.816867538e-04), + // p.239 → p.240 seam. + (113, 7.916183300e-06, -3.972945851e-04), + (175, -1.182107000e-03, -2.303145360e-03), + // p.240 → p.241 seam. + (176, -1.251459700e-03, -2.333251061e-03), + (238, -1.492061500e-02, -1.478104480e-02), + // p.241 → p.242 seam. + (239, -1.533170500e-02, -1.510649733e-02), + // Table centre: the antisymmetry pivot. + (255, -1.892151600e-02, -1.788828894e-02), + (256, 1.892151600e-02, 1.788828894e-02), + (301, 2.022614600e-03, 3.280514618e-03), + // p.242 → p.243 seam. + (302, 1.683383000e-03, 2.911801683e-03), + (364, -1.097435100e-04, 2.272142592e-04), + // p.243 → p.244 seam. + (365, -1.152534400e-04, 1.616069785e-04), + (427, 3.382472000e-05, -1.030297699e-05), + // p.244 → p.245 seam. + (428, 3.411568400e-05, -1.676913780e-05), + (490, -1.144180200e-06, 2.320550948e-06), + // p.245 → p.246 seam, and the table's final row. + (491, -1.119829700e-06, 2.078171747e-06), + (510, -7.138742100e-11, 1.693738625e-07), + (511, -1.140033200e-10, 1.390191784e-07), + ]; + + #[test] + fn page_anchor_rows_match_the_staged_pdf_verbatim() { + for &(i, pr, npr) in PAGE_ANCHORS { + assert_eq!( + RA_COEFF_LOSSLESS[i].to_bits(), + pr.to_bits(), + "RA_COEFF_LOSSLESS[{i}] != PDF Perfect-Reconstruction row {i}" + ); + assert_eq!( + RA_COEFF_LOSSY[i].to_bits(), + npr.to_bits(), + "RA_COEFF_LOSSY[{i}] != PDF Non-Perfect-Reconstruction row {i}" + ); + } + } + + #[test] + fn both_tables_are_exactly_antisymmetric() { + // The printed §D.8 digits satisfy coeff[i] == -coeff[511-i] + // for every row of both 32-band columns (verified across the + // staged pages); a transcription error in either half breaks + // the pairing against the verbatim other half. + for i in 0..FIR_COEFF_LEN { + let j = FIR_COEFF_LEN - 1 - i; + assert_eq!( + RA_COEFF_LOSSLESS[i].to_bits(), + (-RA_COEFF_LOSSLESS[j]).to_bits(), + "RA_COEFF_LOSSLESS[{i}] != -RA_COEFF_LOSSLESS[{j}]" + ); + assert_eq!( + RA_COEFF_LOSSY[i].to_bits(), + (-RA_COEFF_LOSSY[j]).to_bits(), + "RA_COEFF_LOSSY[{i}] != -RA_COEFF_LOSSY[{j}]" + ); + } + } + + #[test] + fn tables_are_finite_and_bounded_by_the_printed_peak() { + // Every printed magnitude is at most the centre rows + // (1.892151600e-02 perfect / 1.788828894e-02 non-perfect). + for i in 0..FIR_COEFF_LEN { + assert!(RA_COEFF_LOSSLESS[i].is_finite()); + assert!(RA_COEFF_LOSSY[i].is_finite()); + assert!( + RA_COEFF_LOSSLESS[i].abs() <= 1.892151600e-02, + "RA_COEFF_LOSSLESS[{i}] exceeds the printed peak" + ); + assert!( + RA_COEFF_LOSSY[i].abs() <= 1.788828894e-02, + "RA_COEFF_LOSSY[{i}] exceeds the printed peak" + ); + } + } + + #[test] + fn peak_magnitude_sits_at_the_table_centre() { + // The §D.8 magnitudes ramp up to the centre rows 255/256 in + // both columns; the maxima land exactly there. + let max_pr = (0..FIR_COEFF_LEN) + .max_by(|&a, &b| { + RA_COEFF_LOSSLESS[a] + .abs() + .total_cmp(&RA_COEFF_LOSSLESS[b].abs()) + }) + .unwrap(); + let max_npr = (0..FIR_COEFF_LEN) + .max_by(|&a, &b| RA_COEFF_LOSSY[a].abs().total_cmp(&RA_COEFF_LOSSY[b].abs())) + .unwrap(); + assert!( + max_pr == 255 || max_pr == 256, + "lossless peak at {max_pr}, expected the 255/256 centre" + ); + assert!( + max_npr == 255 || max_npr == 256, + "lossy peak at {max_npr}, expected the 255/256 centre" + ); + } + + #[test] + fn the_two_tables_are_distinct_sets() { + // §C.2.5 branches between two different coefficient sets; + // they must not have collapsed into one during generation. + assert!( + (0..FIR_COEFF_LEN).any(|i| RA_COEFF_LOSSLESS[i] != RA_COEFF_LOSSY[i]), + "perfect and non-perfect tables are identical" + ); + } + + #[test] + fn fir_coeff_len_matches_the_c25_fir_bound() { + // §C.2.5's FIR step iterates `for (j=0; j<512; j+=64)` over + // 512-tap prCoeff sets; the table length is fixed at 512. + assert_eq!(FIR_COEFF_LEN, 512); + assert_eq!(RA_COEFF_LOSSLESS.len(), FIR_COEFF_LEN); + assert_eq!(RA_COEFF_LOSSY.len(), FIR_COEFF_LEN); + } +} diff --git a/crates/vendor/oxideav-dts/src/header.rs b/crates/vendor/oxideav-dts/src/header.rs new file mode 100644 index 00000000..11a5e4a7 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/header.rs @@ -0,0 +1,4707 @@ +//! DTS Coherent Acoustics frame-sync header parser. +//! +//! All field layouts and value-range bounds in this module come +//! verbatim from the mirrored multimedia.cx snapshot at +//! `docs/audio/dts/wiki/DTS.wiki`, which in turn mirrors the ETSI +//! TS 102 114 §5.3 frame-header description. The wiki notes four +//! sync encodings: +//! +//! ```text +//! 7F FE 80 01 — raw big-endian +//! FE 7F 01 80 — raw little-endian (byte-swapped) +//! 1F FF E8 00 07 Fx — 14-bit packed big-endian +//! FF 1F 00 E8 Fx 07 — 14-bit packed little-endian +//! ``` +//! +//! Round 1 fully parses the two 16-bit raw variants and returns +//! [`Error::UnsupportedFourteenBit`] for the 14-bit variants from +//! [`parse_frame_header`]. Round 2 adds [`parse_frame_header_14bit`] +//! plus a [`crate::unpack_14bit_to_16bit`] primitive that converts a +//! 14-bit-packed buffer into its 16-bit-equivalent raw-BE form so the +//! existing parser can consume both encodings uniformly. +//! +//! ## Field layout (after the 32-bit sync, MSB-first) +//! +//! | Bits | Name | Notes | +//! | ---- | --------------------- | ---------------------------------- | +//! | 1 | FTYPE | 0 = termination, 1 = normal | +//! | 5 | SHORT (sample count) | raw value; samples-in-block = +1 | +//! | 1 | CRC_PRESENT | | +//! | 7 | NBLKS (block count) | raw 5..=127 | +//! | 14 | FSIZE-1 | frame size in bytes = +1, 95..=16384 | +//! | 6 | AMODE (channel cfg) | 0..=15 standard, 16..=63 user | +//! | 4 | SFREQ | sample-freq index (tables missing) | +//! | 5 | RATE | bitrate index (tables missing) | +//! | 1 | DOWNMIX | embedded downmix-coefficients flag | +//! | 1 | DYNRANGE | embedded dynamic-range data flag | +//! | 1 | TIMSTP | timestamp-field-present flag | +//! | 1 | AUXDATA | auxiliary-data-field-present flag | +//! | 1 | HDCD | HDCD-encoded-source flag | +//! | 3 | EXT_DESCR | extension-audio-descriptor (0..=7) | +//! | 1 | EXT_CODING | extension-audio-coding flag | +//! | 1 | ASPF | audio-sync-word in subframes flag | +//! | 2 | LFE | LFE channel mode (0..=3) | +//! | 1 | PRED_HISTORY | predictor-history-enabled flag | +//! | 16 | HEADER_CRC | only present when CRC_PRESENT == 1 | +//! | 1 | MULTIRATE_INTER | multirate-interpolation-filter selector | +//! | 4 | VERSION | encoder version (raw 0..=15) | +//! | 2 | COPY_HISTORY | copy-history code (0..=3) | +//! | 3 | PCMR | source-PCM-resolution index (0..=7) | +//! | 1 | FRONT_SUM | front-channel sum/difference flag | +//! | 1 | SURROUND_SUM | surround-channel sum/difference flag | +//! | 4 | DIALNORM | dialog normalization (dB of recovery) | +//! +//! Round 3 (2026-05-21) surfaced the first batch through +//! [`DtsFrameHeader`]. Round 5 (2026-05-25) extends the typed +//! header through the seven additional post-CRC fields the wiki +//! enumerates (MULTIRATE_INTER, VERSION, COPY_HISTORY, PCMR, +//! FRONT_SUM, SURROUND_SUM, DIALNORM). These 16 bits always +//! follow the HEADER_CRC slot (or the predictor-history bit when +//! `crc_present == 0`), so the parser consumes them +//! unconditionally. The value-table fields (DIALNORM dB, COPY_HISTORY +//! provenance, PCMR resolution mapping) are surfaced as raw indices +//! because the wiki snapshot enumerates the bit widths but not the +//! per-code semantic mapping — those tables remain a `docs/` +//! follow-up. + +use crate::bitreader::BitReader; +use crate::filter_bank::FilterBankSelection; +use crate::unpack14::{unpack_14bit_to_16bit, FourteenBitByteOrder}; +use crate::{Error, Result}; + +/// The four documented DTS Core syncword encodings (per the wiki +/// snapshot's "How to distinguish different versions" table). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum SyncWordEncoding { + /// `7F FE 80 01` — native big-endian raw 16-bit-per-word DTS. + /// The wiki notes this is the **native** DTS byte order. + RawBigEndian, + /// `FE 7F 01 80` — byte-swapped little-endian raw 16-bit-per-word + /// DTS. Commonly seen inside DTS-in-WAV / CD-DA encapsulation. + RawLittleEndian, + /// `1F FF E8 00 07 Fx` — 14-bit big-endian packed DTS. The + /// `unpack14` module (round 2) converts this into the raw-BE + /// form for [`parse_frame_header_14bit`]. + FourteenBitBigEndian, + /// `FF 1F 00 E8 Fx 07` — 14-bit little-endian packed DTS. The + /// `unpack14` module (round 2) converts this into the raw-BE + /// form for [`parse_frame_header_14bit`]. + FourteenBitLittleEndian, +} + +impl SyncWordEncoding { + /// Byte length of the on-wire sync sequence for this encoding, + /// directly read from the wiki snapshot's + /// "How to distinguish different versions" table + /// (`docs/audio/dts/wiki/DTS.wiki`): + /// + /// | Encoding | Sync sequence | Bytes | + /// | ------------------------- | ------------------------ | ----- | + /// | `RawBigEndian` | `7F FE 80 01` | 4 | + /// | `RawLittleEndian` | `FE 7F 01 80` | 4 | + /// | `FourteenBitBigEndian` | `1F FF E8 00 07 Fx` | 6 | + /// | `FourteenBitLittleEndian` | `FF 1F 00 E8 Fx 07` | 6 | + /// + /// The 14-bit variants are 6 bytes because the last container + /// (`07 Fx` / `Fx 07`) carries the upper bits of the 32-bit + /// syncword inside a 14-bit-payload container, and matching the + /// full sync requires inspecting the four high bits of that + /// trailing container per [`crate::parse_frame_header_14bit`]'s + /// detection rule. + /// + /// This accessor is the wiki-derived counterpart to a + /// [`crate::SyncMatch::sync_byte_length`] call. It does not + /// reflect the **frame** byte length (that is + /// [`crate::DtsFrameHeader::frame_size_bytes`]) — only the bytes + /// the sync sequence itself occupies on the wire. + #[inline] + pub fn sync_byte_length(self) -> usize { + match self { + SyncWordEncoding::RawBigEndian | SyncWordEncoding::RawLittleEndian => 4, + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian => 6, + } + } + + /// Whether this encoding is one of the two raw 16-bit-per-word + /// forms (the native DTS encodings per the wiki). + /// + /// Equivalent to `matches!(self, RawBigEndian | RawLittleEndian)` + /// and provided so demuxer / re-muxer code can branch on the + /// "raw vs 14-bit container" distinction without spelling the + /// `matches!` out at every call site. + #[inline] + pub fn is_raw_16bit(self) -> bool { + matches!( + self, + SyncWordEncoding::RawBigEndian | SyncWordEncoding::RawLittleEndian + ) + } + + /// Whether this encoding is one of the two 14-bit-packed + /// container forms (the wiki's "DTS Music CD" / "DTS-in-WAV" + /// 14-bit forms). + /// + /// Equivalent to `!self.is_raw_16bit()` but spelled out + /// affirmatively for readability at call sites that need the + /// 14-bit branch (e.g. the [`crate::FrameIterator`]'s + /// `UnsupportedFourteenBit` guard). + #[inline] + pub fn is_14bit_packed(self) -> bool { + matches!( + self, + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian + ) + } +} + +/// Frame-type flag (FTYPE bit, 1 bit wide). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FrameType { + /// `FTYPE == 0` — termination frame. Per the wiki this marks the + /// last frame in a continuous stream. + Termination, + /// `FTYPE == 1` — normal frame. + Normal, +} + +/// LFE-channel mode (`LFE`, 2 bits wide). +/// +/// The wiki snapshot lists the field as a 2-bit code without naming +/// the four values. ETSI TS 102 114 §5.3.1 documents the codes as +/// "no LFE channel" (0), "128-sample-decimated LFE" (1), +/// "64-sample-decimated LFE" (2), and "reserved/invalid" (3); the +/// wiki snapshot itself does not include those labels, so this enum +/// keeps the names neutral — `code` is the raw 2-bit value and +/// [`Self::is_present`] discriminates "no LFE" (code 0) from the +/// three present-LFE codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LfeMode { + /// Raw LFE code 0. The wiki implies this is "no LFE channel" + /// because the LFE field is the gate to the LFE-stream + /// subblocks; this implementation does not assert it. + None, + /// Raw LFE code 1 — present, mode-1 (see `docs/audio/dts/wiki/`). + Mode1, + /// Raw LFE code 2 — present, mode-2. + Mode2, + /// Raw LFE code 3 — reserved-or-mode-3 per the wiki snapshot. + Mode3, +} + +impl LfeMode { + /// Construct from the raw 2-bit code (`0..=3`). + fn from_raw(code: u8) -> Self { + match code & 0b11 { + 0 => LfeMode::None, + 1 => LfeMode::Mode1, + 2 => LfeMode::Mode2, + _ => LfeMode::Mode3, + } + } + + /// Recover the raw 2-bit LFE code. + pub fn code(self) -> u8 { + match self { + LfeMode::None => 0, + LfeMode::Mode1 => 1, + LfeMode::Mode2 => 2, + LfeMode::Mode3 => 3, + } + } + + /// Whether *any* LFE channel is present. Codes 1..=3 all signal a + /// present LFE channel per the wiki; only code 0 marks its + /// absence. + pub fn is_present(self) -> bool { + !matches!(self, LfeMode::None) + } +} + +/// Targeted transmission bit-rate decoded from the 5-bit `RATE` +/// header field. +/// +/// The mapping is **ETSI TS 102 114 V1.3.1 §5.3.1, Table 5-7** +/// ("RATE parameter versus targeted bit-rate"), transcribed in +/// `docs/audio/dts/dts-core-extracts.md` §1. Table 5-7 enumerates 25 +/// fixed targeted rates (codes `0b00000`..=`0b11000`), one *open*-mode +/// code (`0b11101`), and marks every other code invalid. Per the +/// spec the field names the *targeted* transmission rate, which may +/// be greater than or equal to the actual coded bit-rate; *open* mode +/// permits rates that are not table entries. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum TargetedBitRate { + /// One of the 25 fixed targeted rates from Table 5-7, in bits per + /// second (e.g. code `0b01111` → `768_000`). The `1 411,2 kbit/s` + /// entry (code `0b10110`) is represented exactly as `1_411_200`. + Fixed(u32), + /// `RATE == 0b11101` — *open* mode. The frame's targeted bit-rate + /// is not constrained to a Table 5-7 entry, so no fixed bps value + /// applies. + Open, + /// A reserved / invalid `RATE` code: any code Table 5-7 does not + /// list among the 25 fixed values or the open code. + Invalid, +} + +/// Bits-per-second values for the 25 fixed `RATE` codes +/// (`0b00000`..=`0b11000`), in code order, per ETSI TS 102 114 +/// §5.3.1 Table 5-7 (`docs/audio/dts/dts-core-extracts.md` §1). +/// ETSI lists the rates in kbit/s; this table converts each to bits +/// per second (the decimal-comma `1 411,2 kbit/s` entry → `1_411_200`). +const RATE_TABLE_BPS: [u32; 25] = [ + 32_000, 56_000, 64_000, 96_000, 112_000, 128_000, 192_000, 224_000, 256_000, 320_000, 384_000, + 448_000, 512_000, 576_000, 640_000, 768_000, 960_000, 1_024_000, 1_152_000, 1_280_000, + 1_344_000, 1_408_000, 1_411_200, 1_472_000, 1_536_000, +]; + +/// Resolve a raw 5-bit `RATE` index to its [`TargetedBitRate`] per +/// Table 5-7. Codes `0..=24` are the fixed rates; `29` (`0b11101`) +/// is the open code; everything else is invalid. +fn targeted_bit_rate_from_index(rate_index: u8) -> TargetedBitRate { + match rate_index { + 0..=24 => TargetedBitRate::Fixed(RATE_TABLE_BPS[rate_index as usize]), + 29 => TargetedBitRate::Open, + _ => TargetedBitRate::Invalid, + } +} + +// --------------------------------------------------------------- +// Core audio sampling frequency — ETSI TS 102 114 V1.3.1 §5.3.1 +// Table 5-5 (PDF p.19). +// --------------------------------------------------------------- +// +// SFREQ is a 4-bit field; six of the sixteen codes are documented as +// "Invalid". The "Source Sampling Frequency" column of Table 5-5 is +// the rate of the *original* PCM input to the encoder; for the +// resampled-base-band core (>48 kHz inputs are split into core + +// extended bands) the spec further notes that the encoder can only +// process Fs_core ≤ 48 kHz, so the table's >48 kHz rows describe the +// source rate, not the core-stream rate. This module surfaces the +// SFREQ→Hz mapping exactly as Table 5-5 lists it. + +/// Core-audio sampling frequency decoded from the 4-bit `SFREQ` +/// header field per **ETSI TS 102 114 V1.3.1 §5.3.1, Table 5-5** +/// (PDF p.19). Seven of the sixteen codes are documented as +/// *Invalid*. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum SampleFrequency { + /// One of the nine fixed source-sampling-frequency codes from + /// Table 5-5 (in Hertz; e.g. `SFREQ == 0b1101` → `48_000`). + Fixed(u32), + /// A reserved / invalid `SFREQ` code: codes `0b0000`, `0b0100`, + /// `0b0101`, `0b1001`, `0b1010`, `0b1110`, `0b1111` per Table 5-5. + Invalid, +} + +/// Sampling-frequency-in-Hertz values for the nine fixed `SFREQ` codes +/// of ETSI §5.3.1 Table 5-5. Indexed by `SFREQ` directly; `Invalid` +/// entries are `0` and never reached through [`sample_frequency_from_index`]. +/// The nine non-zero values are (in code order, excluding invalid +/// rows): 8 000 / 16 000 / 32 000 / 11 025 / 22 050 / 44 100 / 12 000 +/// / 24 000 / 48 000 — matching the spec's listed `Source Sampling +/// Frequency` column verbatim. +const SAMPLE_FREQUENCY_TABLE: [u32; 16] = [ + 0, // 0b0000 Invalid + 8_000, // 0b0001 8 kHz + 16_000, // 0b0010 16 kHz + 32_000, // 0b0011 32 kHz + 0, // 0b0100 Invalid + 0, // 0b0101 Invalid + 11_025, // 0b0110 11,025 kHz + 22_050, // 0b0111 22,05 kHz + 44_100, // 0b1000 44,1 kHz + 0, // 0b1001 Invalid + 0, // 0b1010 Invalid + 12_000, // 0b1011 12 kHz + 24_000, // 0b1100 24 kHz + 48_000, // 0b1101 48 kHz + 0, // 0b1110 Invalid + 0, // 0b1111 Invalid +]; + +/// Resolve a raw 4-bit `SFREQ` index to its [`SampleFrequency`] per +/// Table 5-5. The ten fixed rows map to `Fixed(hz)`; the six other +/// codes map to `Invalid`. +fn sample_frequency_from_index(sfreq_index: u8) -> SampleFrequency { + if sfreq_index >= 16 { + return SampleFrequency::Invalid; + } + let hz = SAMPLE_FREQUENCY_TABLE[sfreq_index as usize]; + if hz == 0 { + SampleFrequency::Invalid + } else { + SampleFrequency::Fixed(hz) + } +} + +// --------------------------------------------------------------- +// Audio Channel Arrangement (AMODE) — ETSI TS 102 114 V1.3.1 +// §5.3.1 Table 5-4 (PDF p.18). +// --------------------------------------------------------------- +// +// AMODE is a 6-bit field. Codes `0b000000..=0b001111` (0..=15) are +// the sixteen standard arrangements; codes `0b010000..=0b111111` +// (16..=63) are *User defined* (per Table 5-4's last row). The +// arrangement column names channels using the spec's NOTE legend +// (L = left, R = right, C = centre, S = surround, F = front, +// R = rear, T = total, OV = overhead, A = first mono, B = second +// mono). The CHS column is the channel count for each row. +// +// The LFE channel is **not** part of AMODE — it is gated by the +// separate 2-bit LFE field (already surfaced via [`LfeMode`]). +// +// The CHS-by-AMODE-code mapping below is transcribed verbatim from +// Table 5-4 (in code order, 0..=15): +// 0=1, 1=2, 2=2, 3=2, 4=2, 5=3, 6=3, 7=4, +// 8=4, 9=5, 10=6, 11=6, 12=6, 13=7, 14=8, 15=8. + +/// Standard audio channel arrangement decoded from the 6-bit `AMODE` +/// header field per **ETSI TS 102 114 V1.3.1 §5.3.1, Table 5-4** +/// (PDF p.18). Codes `0..=15` are the sixteen standard arrangements; +/// `16..=63` are *User defined* and surfaced as +/// [`Self::UserDefined`] with the raw code preserved. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum AmodeArrangement { + /// `0b000000` — A (mono, 1 channel). + Mono, + /// `0b000001` — A + B (dual mono, 2 channels). + DualMono, + /// `0b000010` — L + R (stereo, 2 channels). + Stereo, + /// `0b000011` — (L+R) + (L-R) (sum-difference, 2 channels). + SumDifference, + /// `0b000100` — LT + RT (left/right total, 2 channels). + LtRt, + /// `0b000101` — C + L + R (3 channels). + ClR, + /// `0b000110` — L + R + S (3 channels). + LrS, + /// `0b000111` — C + L + R + S (4 channels). + ClRS, + /// `0b001000` — L + R + SL + SR (4 channels). + LrSlSr, + /// `0b001001` — C + L + R + SL + SR (5 channels). + ClRSlSr, + /// `0b001010` — CL + CR + L + R + SL + SR (6 channels). + ClCrLRSlSr, + /// `0b001011` — C + L + R + LR + RR + OV (6 channels). + ClRLrRrOv, + /// `0b001100` — CF + CR + LF + RF + LR + RR (6 channels). + CfCrLfRfLrRr, + /// `0b001101` — CL + C + CR + L + R + SL + SR (7 channels). + ClCCrLRSlSr, + /// `0b001110` — CL + CR + L + R + SL1 + SL2 + SR1 + SR2 + /// (8 channels). + ClCrLRSl1Sl2Sr1Sr2, + /// `0b001111` — CL + C + CR + L + R + SL + S + SR (8 channels). + ClCCrLRSlSSr, + /// `0b010000..=0b111111` — user-defined arrangement. The raw + /// 6-bit AMODE code is preserved; the channel count is not + /// derivable from the spec table. + UserDefined(u8), +} + +impl AmodeArrangement { + /// Channel count (CHS column of Table 5-4) for the sixteen + /// standard arrangements. Returns `None` for [`Self::UserDefined`] + /// codes (the spec does not enumerate a CHS for those). + pub fn channel_count(self) -> Option { + match self { + AmodeArrangement::Mono => Some(1), + AmodeArrangement::DualMono + | AmodeArrangement::Stereo + | AmodeArrangement::SumDifference + | AmodeArrangement::LtRt => Some(2), + AmodeArrangement::ClR | AmodeArrangement::LrS => Some(3), + AmodeArrangement::ClRS | AmodeArrangement::LrSlSr => Some(4), + AmodeArrangement::ClRSlSr => Some(5), + AmodeArrangement::ClCrLRSlSr + | AmodeArrangement::ClRLrRrOv + | AmodeArrangement::CfCrLfRfLrRr => Some(6), + AmodeArrangement::ClCCrLRSlSr => Some(7), + AmodeArrangement::ClCrLRSl1Sl2Sr1Sr2 | AmodeArrangement::ClCCrLRSlSSr => Some(8), + AmodeArrangement::UserDefined(_) => None, + } + } + + /// Bitstream-order channel indices of the **front left / right** pair + /// carrying the §C.2.4 `(L+R, L-R)` sum/difference encoding when the + /// `SUMF` flag is set (or unconditionally for + /// [`Self::SumDifference`]). Derived from the Table 5-4 channel + /// ordering documented on each variant: the first tuple element is + /// the channel that decodes to front-left (`L' = L + R`), the second + /// decodes to front-right (`R' = L - R`). + /// + /// Returns `None` for arrangements with no distinct front L/R pair + /// ([`Self::Mono`], [`Self::DualMono`]) or whose channel layout is + /// not enumerated by the spec ([`Self::UserDefined`]). The + /// [`AmodeArrangement::ClRLrRrOv`] / [`AmodeArrangement::CfCrLfRfLrRr`] + /// six-channel arrangements are also `None` — their front-pair + /// labelling is not an unambiguous `L`/`R` in the Table 5-4 ordering. + #[must_use] + pub fn front_lr_channels(self) -> Option<(usize, usize)> { + match self { + // L, R lead the arrangement. + AmodeArrangement::Stereo + | AmodeArrangement::SumDifference + | AmodeArrangement::LtRt + | AmodeArrangement::LrS + | AmodeArrangement::LrSlSr => Some((0, 1)), + // A leading centre channel offsets L, R by one. + AmodeArrangement::ClR | AmodeArrangement::ClRS | AmodeArrangement::ClRSlSr => { + Some((1, 2)) + } + // CL + CR lead, then L, R. + AmodeArrangement::ClCrLRSlSr => Some((2, 3)), + // CL + C + CR lead, then L, R. + AmodeArrangement::ClCCrLRSlSr | AmodeArrangement::ClCCrLRSlSSr => Some((3, 4)), + // CL + CR lead, then L, R (8-channel SL1/SL2/SR1/SR2 form). + AmodeArrangement::ClCrLRSl1Sl2Sr1Sr2 => Some((2, 3)), + AmodeArrangement::Mono + | AmodeArrangement::DualMono + | AmodeArrangement::ClRLrRrOv + | AmodeArrangement::CfCrLfRfLrRr + | AmodeArrangement::UserDefined(_) => None, + } + } + + /// Bitstream-order channel indices of the **left / right surround** + /// pair carrying the §C.2.4 `(L+R, L-R)` sum/difference encoding when + /// the `SUMS` flag is set. First element decodes to surround-left, + /// second to surround-right. + /// + /// Returns `None` for arrangements without a distinct surround L/R + /// pair (mono/stereo, single-surround arrangements such as + /// [`Self::LrS`] / [`Self::ClRS`], and [`Self::UserDefined`]). + #[must_use] + pub fn surround_lr_channels(self) -> Option<(usize, usize)> { + match self { + // L + R + SL + SR. + AmodeArrangement::LrSlSr => Some((2, 3)), + // C + L + R + SL + SR. + AmodeArrangement::ClRSlSr => Some((3, 4)), + // CL + CR + L + R + SL + SR. + AmodeArrangement::ClCrLRSlSr => Some((4, 5)), + // CL + C + CR + L + R + SL + SR. + AmodeArrangement::ClCCrLRSlSr => Some((5, 6)), + AmodeArrangement::Mono + | AmodeArrangement::DualMono + | AmodeArrangement::Stereo + | AmodeArrangement::SumDifference + | AmodeArrangement::LtRt + | AmodeArrangement::ClR + | AmodeArrangement::LrS + | AmodeArrangement::ClRS + | AmodeArrangement::ClRLrRrOv + | AmodeArrangement::CfCrLfRfLrRr + // The 8-channel SL1/SL2/SR1/SR2 and C-plus-single-S forms have + // no single unambiguous surround L/R pair in Table 5-4. + | AmodeArrangement::ClCrLRSl1Sl2Sr1Sr2 + | AmodeArrangement::ClCCrLRSlSSr + | AmodeArrangement::UserDefined(_) => None, + } + } +} + +/// Resolve a raw 6-bit `AMODE` index to its [`AmodeArrangement`] per +/// Table 5-4. Codes `0..=15` are the sixteen standard arrangements; +/// codes `16..=63` are user-defined (preserved as +/// [`AmodeArrangement::UserDefined`]). +fn amode_arrangement_from_index(amode: u8) -> AmodeArrangement { + match amode { + 0 => AmodeArrangement::Mono, + 1 => AmodeArrangement::DualMono, + 2 => AmodeArrangement::Stereo, + 3 => AmodeArrangement::SumDifference, + 4 => AmodeArrangement::LtRt, + 5 => AmodeArrangement::ClR, + 6 => AmodeArrangement::LrS, + 7 => AmodeArrangement::ClRS, + 8 => AmodeArrangement::LrSlSr, + 9 => AmodeArrangement::ClRSlSr, + 10 => AmodeArrangement::ClCrLRSlSr, + 11 => AmodeArrangement::ClRLrRrOv, + 12 => AmodeArrangement::CfCrLfRfLrRr, + 13 => AmodeArrangement::ClCCrLRSlSr, + 14 => AmodeArrangement::ClCrLRSl1Sl2Sr1Sr2, + 15 => AmodeArrangement::ClCCrLRSlSSr, + // Codes 16..=63 are the user-defined range per Table 5-4's + // final row. Codes >63 cannot reach this function because the + // AMODE field is only 6 bits wide; we mask defensively. + code => AmodeArrangement::UserDefined(code & 0b0011_1111), + } +} + +// --------------------------------------------------------------- +// Source PCM Resolution (PCMR) — ETSI TS 102 114 V1.3.1 +// §5.3.1 Table 5-17 (PDF p.23). +// --------------------------------------------------------------- +// +// PCMR is a 3-bit field. Table 5-17 lists six valid codes plus an +// "Others" row marked invalid. Each valid code carries a (bits, ES) +// pair where ES is a single auxiliary flag indicating that the L/R +// surround channels of the source were mastered in DTS-ES format. +// Code-to-(bits, ES) (from Table 5-17, in code order 0..=7): +// 0b000=(16,0), 0b001=(16,1), 0b010=(20,0), 0b011=(20,1), +// 0b110=(24,0), 0b101=(24,1), others=Invalid. + +/// Source-PCM-resolution decoded from the 3-bit `PCMR` header field +/// per **ETSI TS 102 114 V1.3.1 §5.3.1, Table 5-17** (PDF p.23). +/// The `es` flag indicates that the L/R surround channels were +/// mastered in DTS-ES format (ES=1). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum SourcePcmResolution { + /// One of the six valid Table 5-17 rows: `bits` per source PCM + /// sample plus the auxiliary `es` flag. + Valid { + /// Source-PCM bits-per-sample (16, 20, or 24). + bits: u8, + /// DTS-ES indicator (ES column of Table 5-17). + es: bool, + }, + /// A reserved / invalid `PCMR` code: codes `0b100` (4) and + /// `0b111` (7) per Table 5-17's "Others" row. + Invalid, +} + +/// Resolve a raw 3-bit `PCMR` index to its [`SourcePcmResolution`] +/// per Table 5-17. The six valid codes map to `Valid { bits, es }`; +/// codes `4` and `7` map to `Invalid`. +fn source_pcm_resolution_from_index(pcmr_index: u8) -> SourcePcmResolution { + match pcmr_index & 0b111 { + 0b000 => SourcePcmResolution::Valid { + bits: 16, + es: false, + }, + 0b001 => SourcePcmResolution::Valid { bits: 16, es: true }, + 0b010 => SourcePcmResolution::Valid { + bits: 20, + es: false, + }, + 0b011 => SourcePcmResolution::Valid { bits: 20, es: true }, + 0b110 => SourcePcmResolution::Valid { + bits: 24, + es: false, + }, + 0b101 => SourcePcmResolution::Valid { bits: 24, es: true }, + _ => SourcePcmResolution::Invalid, + } +} + +// --------------------------------------------------------------- +// Dialog Normalization Gain (DIALNORM/UNSPEC) — ETSI TS 102 114 +// V1.3.1 §5.3.1 Table 5-20 (PDF p.24). +// --------------------------------------------------------------- +// +// The 4-bit field that follows SURROUND_SUM in the post-CRC header +// window is named `DIALNORM` when `VERNUM` is 6 or 7, and `UNSPEC` +// otherwise. Table 5-20 documents the (VERNUM, DIALNORM) → Dialog +// Normalization Gain (DNG, in decibels) mapping for the two named +// VERNUM rows: +// +// VERNUM=7 → codes 0..15 → DNG dB 0, -1, -2, -3, -4, -5, -6, -7, +// -8, -9,-10,-11,-12,-13,-14,-15 +// VERNUM=6 → codes 0..15 → DNG dB-16,-17,-18,-19,-20,-21,-22,-23, +// -24,-25,-26,-27,-28,-29,-30,-31 +// +// For every other VERNUM (`0,1,2,3,4,5,8,9,...,15`), §5.3.1 specifies +// that the 4-bit field is `UNSPEC`, the decoder must still extract +// the bits, and the Dialog Normalization Gain is fixed at 0 dB +// (the spec's "DNG=0 indicates No Dialog Normalization" sentence on +// PDF p.23). The two rows + the "all other VERNUM => DNG=0" +// convention give a total function on every (VERNUM, DIALNORM) pair +// the 4-bit fields can encode. + +/// Dialog Normalization Gain decoded from the 4-bit `DIALNORM`/`UNSPEC` +/// header field per **ETSI TS 102 114 V1.3.1 §5.3.1, Table 5-20** +/// (PDF p.24), routed through the 4-bit `VERNUM` field that precedes +/// it in the post-CRC window. +/// +/// The dB value is always non-positive: Table 5-20 enumerates 0 dB +/// down to −31 dB across the two named VERNUM rows, and the spec's +/// `DNG = 0` convention for all other VERNUM values makes 0 dB the +/// only other reachable value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum DialogNormalization { + /// `VERNUM` was 6 or 7: the 4-bit field is `DIALNORM` per + /// Table 5-20. The contained value is the Dialog Normalization + /// Gain in decibels (always ≤ 0). For example + /// `(VERNUM=7, DIALNORM=0)` → `Fixed(0)`, + /// `(VERNUM=7, DIALNORM=15)` → `Fixed(-15)`, + /// `(VERNUM=6, DIALNORM=0)` → `Fixed(-16)`, + /// `(VERNUM=6, DIALNORM=15)` → `Fixed(-31)`. + Fixed(i8), + /// `VERNUM` was outside {6, 7}: the 4-bit field is `UNSPEC` per + /// §5.3.1. The spec says the decoder must still extract the bits + /// (the parser does, into [`DtsFrameHeader::dialog_normalization`]) + /// but must apply no dialog-normalization gain. Equivalent to + /// `Fixed(0)` for playback purposes — the variant preserves the + /// `UNSPEC` distinction for callers that care about the original + /// field meaning. + Unspecified, +} + +impl DialogNormalization { + /// Dialog Normalization Gain in decibels. + /// + /// Returns the spec's `DNG` value: the contained `i8` for the + /// [`Self::Fixed`] variant, and `0` for [`Self::Unspecified`] + /// (per §5.3.1's "DNG=0 indicates No Dialog Normalization" for + /// non-{6,7} VERNUM values). + pub fn gain_db(self) -> i8 { + match self { + DialogNormalization::Fixed(db) => db, + DialogNormalization::Unspecified => 0, + } + } +} + +/// Resolve a `(VERNUM, DIALNORM)` pair to its [`DialogNormalization`] +/// per Table 5-20. +/// +/// Only the low 4 bits of each argument are consulted — both fields +/// are 4-bit wires in the post-CRC header window. +fn dialog_normalization_from_codes(vernum: u8, dialnorm: u8) -> DialogNormalization { + let dialnorm = dialnorm & 0b1111; + match vernum & 0b1111 { + 7 => DialogNormalization::Fixed(-(dialnorm as i8)), + 6 => DialogNormalization::Fixed(-(dialnorm as i8) - 16), + _ => DialogNormalization::Unspecified, + } +} + +/// Parsed DTS Core frame-sync header. +/// +/// Round 1 surfaces only the structural fields whose semantics are +/// unambiguous in the wiki snapshot. The sample-rate / channel-count +/// *value* tables are not in `docs/` yet — see [`Self::sample_rate_hz`] +/// and [`Self::channel_count`] for the `Option` semantics. The +/// bitrate table (ETSI §5.3.1 Table 5-7) landed in round 185, so +/// [`Self::bit_rate_bps`] / [`Self::targeted_bit_rate`] now resolve. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DtsFrameHeader { + /// Which of the four documented sync encodings was found at + /// offset zero. + pub sync_word_encoding: SyncWordEncoding, + /// Decoded frame type (termination vs normal). + pub frame_type: FrameType, + /// Samples per sub-block (the wiki's "Deficit sample count + 1", + /// nominally 32 for a normal frame). + pub sample_count_per_block: u8, + /// Whether the header CRC field is present (the 16-bit field + /// that follows the predictor-history bit). Round 1 does not + /// verify it; the flag is exposed so a future round can. + pub crc_present: bool, + /// Number of sub-blocks in the frame (raw NBLKS, 5..=127). + pub blocks_per_frame: u8, + /// Frame size in bytes (`FSIZE-1 + 1`, 95..=16384). + pub frame_size_bytes: u16, + /// Channel-configuration code (AMODE, 0..=63). 0..=15 are + /// standard layouts (per ETSI §5.3.1 Table 5-4 — resolved by + /// [`Self::amode_arrangement`] / [`Self::channel_count`]); + /// 16..=63 are user-defined. + pub amode: u8, + /// Sample-frequency index (SFREQ, 0..=15) per ETSI §5.3.1 + /// Table 5-5. Resolved by [`Self::sample_rate_hz`] / + /// [`Self::sample_frequency`] (six codes are documented as + /// `Invalid` per the spec). + pub sfreq_index: u8, + /// Transmission-bitrate index (RATE, 0..=31). Resolves to a + /// targeted bit-rate via ETSI §5.3.1 Table 5-7 — see + /// [`Self::bit_rate_bps`] / [`Self::targeted_bit_rate`]. + pub rate_index: u8, + /// Embedded-downmix-coefficients flag (`DOWNMIX`, 1 bit). + pub downmix: bool, + /// Embedded-dynamic-range-data flag (`DYNF` / `DYNRANGE`, 1 bit). + /// Per ETSI §5.3.1 Table 5-8 (`docs/audio/dts/dts-core-extracts.md` + /// §1): `false` → dynamic-range coefficients not present; + /// `true` → present at the start of each subframe. + pub dynamic_range: bool, + /// Timestamp-field-present flag (`TIMEF` / `TIMSTP`, 1 bit). Per + /// ETSI §5.3.1 Table 5-9 (`docs/audio/dts/dts-core-extracts.md` + /// §1): `false` → time stamps not present; `true` → present at + /// the end of the core audio data. Round 3 surfaces the flag but + /// does not interpret the optional timestamp payload itself. + pub time_stamp: bool, + /// Auxiliary-data-field-present flag (`AUXDATA`, 1 bit). + pub aux_data: bool, + /// HDCD-encoded-source flag (`HDCD`, 1 bit). + pub hdcd: bool, + /// Extension-audio-descriptor (`EXT_DESCR`, 3 bits, 0..=7). The + /// wiki snapshot does not enumerate the value semantics; the raw + /// 3-bit code is preserved verbatim. + pub ext_descr: u8, + /// Extension-audio-coding flag (`EXT_CODING`, 1 bit). Indicates + /// whether an extension substream (X96 / XCH / XXCH / EXSS) is + /// muxed alongside the Core stream. + pub ext_coding: bool, + /// Audio-sync-word-in-subframes flag (`ASPF`, 1 bit). + pub aspf: bool, + /// LFE-channel mode (`LFE`, 2 bits). See [`LfeMode`]. + pub lfe: LfeMode, + /// Predictor-history-enabled flag (`PRED_HISTORY`, 1 bit). + pub predictor_history: bool, + /// 16-bit header-CRC value (`HEADER_CRC`, the spec's `HCRC`). + /// Present iff [`Self::crc_present`] is `true`; `None` otherwise. + /// The algorithm is the Annex B CRC-CCITT ([`crate::dts_crc16`], + /// `docs/audio/dts/dts-crc16.md`), but for the core `HCRC` / + /// `AHCRC` / `SICRC` / `OCRC` fields the spec explicitly states + /// "The CRC value test **shall not be applied**" — they are + /// informational placeholders, unlike the genuinely verified + /// aux / Rev2-aux / extension-substream check words. The field is + /// therefore surfaced raw for pass-through callers and + /// [`Self::verify_header_crc`] keeps returning `None`. + pub header_crc: Option, + /// Multirate-interpolation-filter selector (`MULTIRATE_INTER`, + /// 1 bit). This bit **is** the spec's `FILTS` ("Multirate + /// Interpolator Switch") field of §5.3.1: per ETSI TS 102 114 + /// V1.3.1 §5.3.1 Table 5-15 (resolved in + /// `docs/audio/dts/dts-qmf-driver.md` §1) it selects which of the + /// two §D.8 32-band interpolation FIR coefficient sets the §C.2.5 + /// `QMFInterpolation()` driver convolves against: + /// + /// | `multirate_inter` / `FILTS` | 32-band interpolation filter | + /// |-----------------------------|------------------------------| + /// | `0` (`false`) | Non-Perfect Reconstruction (`raCoeffLossy`) | + /// | `1` (`true`) | Perfect Reconstruction (`raCoeffLossLess`) | + /// + /// The header-field table (§5.3.1 Table 5-15) and the §C.2.5 + /// driver pseudocode agree bit-for-bit — there is no inverted + /// convention. [`Self::filter_bank_selection`] bridges this bit + /// directly to [`crate::FilterBankSelection`] for the §C.2.5 + /// FIR step. + pub multirate_inter: bool, + /// Encoder version code (`VERSION`, 4 bits, 0..=15). The wiki + /// snapshot does not enumerate which integer values correspond + /// to which encoder revisions; round 5 surfaces the raw 4-bit + /// code for pass-through callers. + pub version: u8, + /// Copy-history code (`COPY_HISTORY`, 2 bits, 0..=3). The wiki + /// snapshot does not document the per-code semantics; raw value + /// preserved. + pub copy_history: u8, + /// Source-PCM-resolution index (`PCMR`, 3 bits, 0..=7) per ETSI + /// §5.3.1 Table 5-17. Resolved by + /// [`Self::source_pcm_bits_per_sample`] / + /// [`Self::source_pcm_resolution`] (six codes are valid, two + /// (`0b100` / `0b111`) are documented as `Invalid`). + pub source_pcm_resolution_index: u8, + /// Front-channel sum/difference flag (`FRONT_SUM`, 1 bit). For + /// stereo encodings, signals that the front L/R channels were + /// transmitted as a sum/difference pair rather than discrete + /// channels. The semantic interpretation is documented in the + /// spec; the bit itself is surfaced verbatim. + pub front_sum: bool, + /// Surround-channel sum/difference flag (`SURROUND_SUM`, 1 bit). + /// Same convention as [`Self::front_sum`] but for the surround + /// channel pair. + pub surround_sum: bool, + /// Dialog-normalization code (`DIALNORM` for `VERNUM ∈ {6, 7}`, + /// otherwise `UNSPEC`; 4 bits, 0..=15). Resolved to a Dialog + /// Normalization Gain in dB via [`Self::dialog_normalization_db`] + /// /[`Self::dialog_normalization_gain`] per ETSI TS 102 114 V1.3.1 + /// §5.3.1 Table 5-20 (PDF p.24), with the spec's `UNSPEC` → 0 dB + /// convention applied for `VERNUM ∉ {6, 7}`. + pub dialog_normalization: u8, +} + +impl DtsFrameHeader { + /// Resolve [`Self::sfreq_index`] to a sample-rate in Hertz per + /// ETSI TS 102 114 V1.3.1 §5.3.1 Table 5-5 (PDF p.19). + /// + /// Returns `Some(hz)` for the nine valid `SFREQ` codes (e.g. + /// code `0b1101` → `Some(48_000)`), and `None` for the seven + /// codes Table 5-5 lists as *Invalid* (`0b0000`, `0b0100`, + /// `0b0101`, `0b1001`, `0b1010`, `0b1110`, `0b1111`). Callers + /// that need to distinguish "valid rate" from "invalid code" + /// should use [`Self::sample_frequency`]. + /// + /// Per Table 5-5's note, the value is the **source** sampling + /// frequency. For inputs ≤ 48 kHz the source rate equals the + /// core rate; for >48 kHz inputs the encoder splits the spectrum + /// into a core band ≤ 48 kHz plus extended bands carrying the + /// remainder. + pub fn sample_rate_hz(&self) -> Option { + match self.sample_frequency() { + SampleFrequency::Fixed(hz) => Some(hz), + SampleFrequency::Invalid => None, + } + } + + /// Resolve [`Self::sfreq_index`] to its [`SampleFrequency`] per + /// ETSI TS 102 114 §5.3.1 Table 5-5. + /// + /// Richer counterpart to [`Self::sample_rate_hz`]: preserves the + /// `Fixed` / `Invalid` distinction the `Option` accessor + /// collapses to `None`. + pub fn sample_frequency(&self) -> SampleFrequency { + sample_frequency_from_index(self.sfreq_index) + } + + /// Resolve [`Self::rate_index`] to its targeted transmission + /// bit-rate per ETSI TS 102 114 §5.3.1 Table 5-7. + /// + /// This is the richer counterpart to [`Self::bit_rate_bps`]: it + /// preserves the *open*-mode (`RATE == 0b11101`) and *invalid* + /// distinctions that the `Option` accessor collapses to + /// `None`. See [`TargetedBitRate`]. + pub fn targeted_bit_rate(&self) -> TargetedBitRate { + targeted_bit_rate_from_index(self.rate_index) + } + + /// Resolve [`Self::rate_index`] to a targeted transmission + /// bit-rate in bits per second. + /// + /// Returns `Some(bps)` for the 25 fixed `RATE` codes of ETSI + /// §5.3.1 Table 5-7 (e.g. code `0b01111` → `Some(768_000)`), and + /// `None` for the *open*-mode code (`0b11101`, where no fixed rate + /// applies) and for any reserved / invalid code. Callers that need + /// to distinguish open from invalid should use + /// [`Self::targeted_bit_rate`]. + pub fn bit_rate_bps(&self) -> Option { + match self.targeted_bit_rate() { + TargetedBitRate::Fixed(bps) => Some(bps), + TargetedBitRate::Open | TargetedBitRate::Invalid => None, + } + } + + /// Resolve [`Self::amode`] to a count of audio channels (LFE + /// excluded; the LFE field is surfaced separately via + /// [`Self::lfe`] / [`LfeMode::is_present`]) per ETSI TS 102 114 + /// V1.3.1 §5.3.1 Table 5-4 (PDF p.18). + /// + /// Returns `Some(chs)` for the sixteen standard AMODE codes + /// (e.g. code `2` → `Some(2)` for L+R stereo), and `None` for + /// codes `16..=63` which Table 5-4's final row marks *User + /// defined* — those codes carry no fixed channel-count in the + /// spec table. Callers that want to inspect the full arrangement + /// (per-channel placement, sum/difference encoding, etc.) should + /// use [`Self::amode_arrangement`]. + pub fn channel_count(&self) -> Option { + self.amode_arrangement().channel_count() + } + + /// Resolve [`Self::amode`] to its [`AmodeArrangement`] per ETSI + /// TS 102 114 §5.3.1 Table 5-4. + /// + /// Richer counterpart to [`Self::channel_count`]: returns the + /// full arrangement enum (named per Table 5-4) so callers can + /// branch on the playback layout (mono / dual-mono / stereo / + /// sum-difference / LtRt / various multichannel arrangements) + /// rather than just the channel count. User-defined codes + /// (`16..=63`) round-trip the raw 6-bit value through + /// [`AmodeArrangement::UserDefined`]. + pub fn amode_arrangement(&self) -> AmodeArrangement { + amode_arrangement_from_index(self.amode) + } + + /// Resolve [`Self::source_pcm_resolution_index`] to the source + /// PCM bits-per-sample value the encoder declared per ETSI + /// TS 102 114 V1.3.1 §5.3.1 Table 5-17 (PDF p.23). + /// + /// Returns `Some(bits)` for the six valid `PCMR` codes (e.g. + /// code `0b000` → `Some(16)`), and `None` for the two codes + /// Table 5-17's "Others" row marks *Invalid* (`0b100` and + /// `0b111`). The auxiliary DTS-ES flag is dropped by this + /// accessor; callers that need both halves should use + /// [`Self::source_pcm_resolution`]. + pub fn source_pcm_bits_per_sample(&self) -> Option { + match self.source_pcm_resolution() { + SourcePcmResolution::Valid { bits, .. } => Some(bits), + SourcePcmResolution::Invalid => None, + } + } + + /// Resolve [`Self::source_pcm_resolution_index`] to its + /// [`SourcePcmResolution`] per ETSI TS 102 114 §5.3.1 Table 5-17. + /// + /// Richer counterpart to [`Self::source_pcm_bits_per_sample`]: + /// preserves the DTS-ES (`es`) flag that Table 5-17 stores + /// alongside the bits-per-sample column. + pub fn source_pcm_resolution(&self) -> SourcePcmResolution { + source_pcm_resolution_from_index(self.source_pcm_resolution_index) + } + + /// Resolve [`Self::multirate_inter`] (the `FILTS` "Multirate + /// Interpolator Switch" bit) to the §D.8 32-band interpolation + /// FIR coefficient set the §C.2.5 `QMFInterpolation()` driver + /// must convolve against. + /// + /// Per **ETSI TS 102 114 V1.3.1 §5.3.1 Table 5-15** (resolved in + /// `docs/audio/dts/dts-qmf-driver.md` §1, which establishes that + /// the `MULTIRATE_INTER` header field *is* the spec's `FILTS` + /// field and that the header table and the §C.2.5 driver + /// pseudocode agree bit-for-bit): + /// + /// - `multirate_inter == false` (`FILTS == 0`) → + /// [`FilterBankSelection::NonPerfectReconstruction`] + /// (`raCoeffLossy`); + /// - `multirate_inter == true` (`FILTS == 1`) → + /// [`FilterBankSelection::PerfectReconstruction`] + /// (`raCoeffLossLess`). + /// + /// This is the bridge from a parsed header to the typed + /// [`crate::FilterBankSelection`] consumed by the §C.2.5 FIR + /// step (`crate::QmfSynthesis::synthesize`); it is + /// equivalent to + /// `FilterBankSelection::from_filts(u8::from(self.multirate_inter))`. + #[must_use] + pub fn filter_bank_selection(&self) -> FilterBankSelection { + FilterBankSelection::from_filts(u8::from(self.multirate_inter)) + } + + /// The §C.2.5 `QMFInterpolation()` output gain `rScale` — the + /// single post-filterbank float→PCM full-scale conversion factor + /// applied at the driver's `naCh[nChIndex++] = int(rScale*raZ[i])` + /// step. + /// + /// Per `docs/audio/dts/dts-qmf-driver.md` §2, the §C.2.5 output + /// `rScale` is **not** a normatively-fixed numeric constant + /// (§C.2.5 is one informative implementation among many). What the + /// spec pins down is its purpose: it brings the normalized + /// floating-point filterbank output `raZ[i]` (nominal range ≈ + /// ±1.0, with the QMF `1/N` normalization already folded into the + /// §C.2.5 `raCosMod` scalers) up to the signed-integer full-scale + /// range of the source PCM resolution declared by `PCMR` (§5.3.1 + /// Table 5-17). For a real-valued implementation that keeps `raZ` + /// at unit scale, `rScale = 2^(PCMR_bits − 1)` (e.g. 32768.0 for + /// 16-bit source PCM). + /// + /// This accessor returns that canonical derivation + /// `2^(bits − 1)` for the six valid `PCMR` codes (16/20/24-bit → + /// `Some(32768.0 / 524288.0 / 8388608.0)`), and `None` for the + /// two reserved/invalid codes ([`SourcePcmResolution::Invalid`]), + /// matching the `Option` semantics of + /// [`Self::source_pcm_bits_per_sample`]. Implementations that + /// apply their own headroom/clip-guard factor or carry a + /// different internal `raZ` normalization should pass their own + /// `rScale` to `crate::QmfSynthesis::synthesize` instead. + #[must_use] + pub fn output_r_scale(&self) -> Option { + self.source_pcm_bits_per_sample() + .map(|bits| (2.0_f64).powi(i32::from(bits) - 1)) + } + + /// The §5.3.1 `SHORT` (Deficit Sample Count) padding a + /// **termination frame** asks the decoder to append, in PCM + /// samples per channel — or `None` for a normal frame. + /// + /// Per ETSI TS 102 114 §5.3.1 (PDF p.18): a termination frame + /// "carries n×32 core audio samples where block length n is + /// adjusted to just fall short of the video end point", and "On + /// completion of a termination frame, (SHORT+1) PCM core samples + /// must be padded to the output buffers of each channel. The + /// padded samples may be zeros or they may be copies of adjacent + /// samples." `SHORT` is valid in `[0, 30]` for termination frames + /// (Table 5-3; `31` indicates a normal frame), so the returned + /// pad is `1..=31` samples. + /// + /// The decode chain ([`crate::decode_core_frame`] / + /// [`crate::CoreStreamDecoder`]) returns only the **decoded** + /// samples; appending the presentation pad (and choosing its + /// fill) is the caller's output-layer decision, made with this + /// accessor. [`Self::sample_count_per_block`] stores the wire + /// field as `SHORT + 1`. + pub fn termination_pad_samples(&self) -> Option { + match self.frame_type { + FrameType::Termination => Some(self.sample_count_per_block), + FrameType::Normal => None, + } + } + + /// Resolve [`Self::dialog_normalization`] to a Dialog + /// Normalization Gain in decibels per **ETSI TS 102 114 V1.3.1 + /// §5.3.1, Table 5-20** (PDF p.24), routed through + /// [`Self::version`] (the `VERNUM` field that precedes DIALNORM + /// in the post-CRC header window). + /// + /// Always returns `Some`: + /// - `VERNUM == 7` → `Some(-(DIALNORM as i8))` (codes 0..=15 → 0 dB + /// down to −15 dB). + /// - `VERNUM == 6` → `Some(-(DIALNORM as i8) - 16)` (codes 0..=15 → + /// −16 dB down to −31 dB). + /// - Any other `VERNUM` → `Some(0)` per §5.3.1's "DNG=0 indicates + /// No Dialog Normalization" for VERNUM ∉ {6, 7}. The field is + /// `UNSPEC` in this branch (the parser still surfaces the raw + /// bits via [`Self::dialog_normalization`]). + /// + /// Callers that need to distinguish the `Fixed` Table-5-20 mapping + /// from the `Unspecified` zero-gain convention should use + /// [`Self::dialog_normalization_gain`]. + pub fn dialog_normalization_db(&self) -> Option { + Some(self.dialog_normalization_gain().gain_db()) + } + + /// Resolve [`Self::dialog_normalization`] to its + /// [`DialogNormalization`] per ETSI TS 102 114 §5.3.1 Table 5-20. + /// + /// Richer counterpart to [`Self::dialog_normalization_db`]: + /// preserves the distinction between the [`DialogNormalization::Fixed`] + /// row of Table 5-20 (`VERNUM ∈ {6, 7}`) and the + /// [`DialogNormalization::Unspecified`] `UNSPEC` convention + /// (all other `VERNUM` values). + pub fn dialog_normalization_gain(&self) -> DialogNormalization { + dialog_normalization_from_codes(self.version, self.dialog_normalization) + } + + /// Verify the 16-bit [`Self::header_crc`] against the bits + /// covered by the DTS Core header-CRC contract. + /// + /// Always returns `None`, for two spec-mandated reasons + /// (`docs/audio/dts/dts-crc16.md` "Where CRC-16 is applied"): + /// + /// - the Annex B algorithm itself is now documented (and + /// available as [`crate::dts_crc16`]), but §5.3.1 explicitly + /// states "The CRC value test **shall not be applied**" for the + /// core `HCRC` field (likewise `AHCRC` / `SICRC` / `OCRC`) — + /// the core check words are informational placeholders, not + /// integrity gates; + /// - the exact protected span for `HCRC` is not normatively + /// pinned by the spec text ("core frame-header data"), so no + /// `Some(bool)` verdict could be computed defensibly anyway. + /// + /// The caller can use [`Self::header_crc`] directly for + /// pass-through scenarios (e.g. re-muxing), and + /// [`crate::dts_crc16`] to checksum any span it chooses. The + /// genuinely verified DTS check words are the aux + /// ([`crate::AuxData::crc_valid`]) and Rev2-aux + /// ([`crate::Rev2AuxChunk::crc_valid`]) CRCs. + pub fn verify_header_crc(&self) -> Option { + // Normatively "shall not be applied"; see the doc comment. + let _ = self.header_crc?; + None + } + + /// Total bit-length of the frame-sync header window, counted from + /// the first bit of the syncword to the first bit of the SUBFRAMES + /// region the wiki marks as `'''TODO'''`. + /// + /// The value is fully derived from the bit-table in the wiki + /// snapshot (`docs/audio/dts/wiki/DTS.wiki`): + /// + /// | Region | Bits | + /// | ----------------------------------- | ------------------- | + /// | Sync (32-bit raw / 14-bit packed) | 32 | + /// | Base: FTYPE..RATE | 1+5+1+7+14+6+4+5=43 | + /// | Trailing flags: DOWNMIX..PRED_HIST | 1+1+1+1+1+3+1+1+2+1=13 | + /// | Optional HEADER_CRC | 16 (iff `crc_present`) | + /// | Post-CRC: MULTIRATE_INTER..DIALNORM | 1+4+2+3+1+1+4=16 | + /// + /// Total: 32 + 43 + 13 + 16 + (16 if `crc_present`) = + /// `104` bits when `crc_present == 0`, `120` bits when + /// `crc_present == 1`. Both totals are exact multiples of 8, so + /// the SUBFRAMES region (the wiki's `'''TODO'''` cell) starts on + /// a byte boundary. + /// + /// The value is in **raw 16-bit-stream bits** for raw-BE / raw-LE + /// encodings. For the 14-bit-packed encodings the value still + /// reflects the unpacked-bitstream count (i.e. what the parser + /// consumed *after* [`crate::unpack_14bit_to_16bit`] has run); the + /// container-byte advance for 14-bit input is a separate quantity + /// (see `README.md`'s round-6 docs gap #7). + pub fn header_bit_length(&self) -> u32 { + // Sync(32) + base(43) + trailing(13) + post_crc(16) = 104. + // Plus optional HEADER_CRC(16) when crc_present == true. + const BASE_BITS: u32 = 32 + 43 + 13 + 16; + if self.crc_present { + BASE_BITS + 16 + } else { + BASE_BITS + } + } + + /// Total byte-length of the frame-sync header window — the + /// byte offset within the (raw-16-bit-equivalent) frame buffer at + /// which the SUBFRAMES region the wiki marks `'''TODO'''` + /// begins. + /// + /// Equivalent to `header_bit_length() / 8`. Always 13 + /// (`crc_present == false`) or 15 (`crc_present == true`) + /// because both totals are exact multiples of 8 by construction. + /// + /// Useful for downstream subframe / payload decoders that need to + /// know where the header ends and the SUBFRAMES region begins + /// within a frame slice obtained from + /// [`crate::iter_frames`] or directly from + /// [`crate::parse_frame_header`]. + /// + /// For 14-bit-packed input the value reflects the unpacked-stream + /// byte count, not the container-byte count. + pub fn header_byte_length(&self) -> usize { + // header_bit_length() is always a multiple of 8 by the + // arithmetic above; the assertion is for documentation / + // debug builds only. + let bits = self.header_bit_length(); + debug_assert_eq!(bits % 8, 0, "DTS header window must be byte-aligned"); + (bits / 8) as usize + } + + /// Container-byte distance from this frame's syncword to the next + /// frame's syncword, for a given wire encoding. + /// + /// Derived from **ETSI TS 102 114 V1.3.1 §5.3.1** (the `FSIZE` + /// definition) and the 14-bit container-byte advance rule + /// transcribed in `docs/audio/dts/dts-core-extracts.md` §3.3: + /// + /// - For the raw 16-bit encodings (`RawBigEndian` / + /// `RawLittleEndian`) the answer is just + /// [`Self::frame_size_bytes`]: `FSIZE+1` already counts bytes of + /// the on-wire 16-bit-word stream, which is the same as the + /// container-byte stream when no 14-bit re-packing is in effect. + /// - For the 14-bit-packed encodings (`FourteenBitBigEndian` / + /// `FourteenBitLittleEndian`) the same `FSIZE+1` logical bytes + /// are carried in 14-bit-payload containers. Per §3.3 each + /// container word (= 16 container bits = **2 container bytes**) + /// carries 14 logical bits, so the span occupies + /// `ceil((FSIZE+1) * 8 / 14)` container **words** = + /// `2 * ceil((FSIZE+1) * 8 / 14)` container bytes (the partial + /// final word is padded out — the ETSI "28-bit-word boundary" + /// invariant in §6.1.3.1 / §6.3.x guarantees the next syncword + /// re-aligns on a 28-bit (i.e. two-container-word) boundary). + /// + /// The return type is [`u32`] because `frame_size_bytes` tops out + /// at 16 384, the 14-bit scaling factor is 16 / 14 ≈ 1.143, and + /// `16_384 * 16 / 14 + 1` comfortably fits. + /// + /// This accessor is the analytical half of round-6 docs gap #7 + /// (see `README.md`'s "Docs gaps"): it gives a multi-frame iterator + /// the byte-count it needs to step from one 14-bit-packed sync to + /// the next. The empirical half — actually walking a 14-bit + /// container stream through [`crate::FrameIterator`] — is a + /// follow-up that needs a streaming 14-bit-to-16-bit unpacker for + /// the header window of each frame (because the parser reads its + /// fields from the unpacked stream); this accessor lets that + /// follow-up land without the formula having to be re-derived + /// against `dts-core-extracts.md` §3.3 from scratch. + /// + /// # Examples + /// + /// ``` + /// use oxideav_dts::{parse_frame_header, SyncWordEncoding}; + /// + /// // A 1024-byte raw-BE frame: container advance equals + /// // frame_size_bytes exactly. + /// # let bytes: &[u8] = &[]; + /// # if let Ok(hdr) = parse_frame_header(bytes) { + /// assert_eq!( + /// hdr.frame_size_container_bytes(SyncWordEncoding::RawBigEndian), + /// hdr.frame_size_bytes as u32, + /// ); + /// // The same frame's 14-bit-packed container distance is + /// // ceil(1024 * 8 / 14) words = 586 words = 1172 bytes. + /// # } + /// ``` + pub fn frame_size_container_bytes(&self, encoding: SyncWordEncoding) -> u32 { + let logical_bytes = self.frame_size_bytes as u32; + if encoding.is_raw_16bit() { + // FSIZE+1 already counts on-wire container bytes for the + // raw encodings (the wiki notes raw-LE is the + // 16-bit-word-swap of raw-BE; byte count is preserved). + return logical_bytes; + } + // 14-bit-packed: 14 logical bits per 16 container bits. + // ceil(logical_bytes * 8 / 14) container words; one word = 2 + // container bytes. Equivalent integer form: round the + // logical-bit count up to the next multiple of 14, then + // multiply by 2/14 = 1/7. + let logical_bits = logical_bytes * 8; + let container_words = logical_bits.div_ceil(14); + container_words * 2 + } +} + +/// Serialise a [`DtsFrameHeader`] back into the raw **little-endian** +/// on-wire byte representation of the frame-sync header window. +/// +/// The output always begins with the canonical raw-LE sync `FE 7F 01 +/// 80` regardless of the [`DtsFrameHeader::sync_word_encoding`] field +/// — the encoder emits the raw-LE form the parser already accepts via +/// the [`SyncWordEncoding::RawLittleEndian`] branch. +/// +/// The wiki snapshot describes raw little-endian as "byte-swapped at +/// the 16-bit-word level" of the raw big-endian stream (see +/// `docs/audio/dts/wiki/DTS.wiki`'s sync table — `7F FE 80 01` ↔ +/// `FE 7F 01 80`). This encoder therefore: +/// +/// 1. calls [`encode_frame_header_be`] to build the 13 or 15 raw-BE +/// bytes, +/// 2. zero-pads them to **16 bytes** (the parser's minimum input +/// length for the raw-LE branch — the parser word-swaps a 16-byte +/// window and consumes 104 or 120 bits from it), +/// 3. byte-swaps each 16-bit word in place to produce the raw-LE +/// output. +/// +/// The output is therefore always exactly 16 bytes long (regardless +/// of `header.crc_present`). The trailing 3 or 1 zero bytes correspond +/// to the first 24 or 8 bits of the SUBFRAMES region of a real DTS +/// frame; a caller muxing the encoder output back into a stream +/// should overwrite those bytes with the actual SUBFRAMES content +/// (after byte-swapping their 16-bit-word view to match). +/// +/// The round-trip property: +/// +/// ```text +/// parse_frame_header(&encode_frame_header_le(&hdr)) == Ok(hdr') +/// where hdr'.sync_word_encoding == SyncWordEncoding::RawBigEndian +/// hdr' == hdr on every other field +/// ``` +/// +/// holds exactly (no padding step needed by the caller). The parser +/// reports `RawBigEndian` because its normalisation step word-swaps +/// the raw-LE input back into a raw-BE scratch buffer before reading +/// the bit-table; the `sync_word_encoding` field is therefore the only +/// field that does not round-trip through the BE encoder, just as +/// `encode_frame_header_be` already documents. +/// +/// Returns the same [`Error`] variants as [`encode_frame_header_be`] +/// for invalid headers (`BlockCountOutOfRange`, `FrameSizeOutOfRange`, +/// `FieldOutOfRange`). +pub fn encode_frame_header_le(header: &DtsFrameHeader) -> Result> { + let mut be = encode_frame_header_be(header)?; + // Zero-pad to the parser's minimum raw-LE input length (16). The + // BE encoder returns 13 or 15 bytes; the LE branch of the parser + // requires a 16-byte window so it can word-swap it before reading + // the bit-table. Pad with zeros — the parser only consumes the + // first `header_bit_length()` bits. + be.resize(16, 0); + debug_assert_eq!(be.len() % 2, 0, "raw-LE encoder works on 16-bit words"); + // Word-swap pairs in place. + for pair in be.chunks_exact_mut(2) { + pair.swap(0, 1); + } + Ok(be) +} + +/// Serialise a [`DtsFrameHeader`] back into the raw-BE on-wire byte +/// representation of the frame-sync header window. +/// +/// The output is exactly [`DtsFrameHeader::header_byte_length`] bytes +/// long (13 or 15, depending on [`DtsFrameHeader::crc_present`]), and +/// always begins with the 4-byte raw-BE sync `7F FE 80 01` regardless +/// of the [`DtsFrameHeader::sync_word_encoding`] field — the encoder +/// emits the canonical raw-BE form the parser already understands, so +/// a caller that needs the raw-LE / 14-bit-BE / 14-bit-LE encoding can +/// post-process the output (byte-swap pairs for raw-LE via +/// [`encode_frame_header_le`], repack 16→14-bit for the 14-bit +/// variants via [`crate::pack_16bit_to_14bit`]). +/// +/// The bit layout is the wiki bit-table from +/// `docs/audio/dts/wiki/DTS.wiki`, MSB-first, in the same order +/// [`parse_frame_header`] consumes: +/// +/// 1. 32-bit sync `0x7FFE_8001`. +/// 2. Base block (43 bits): FTYPE(1), SHORT(5), CRC_PRESENT(1), +/// NBLKS(7), FSIZE-1(14), AMODE(6), SFREQ(4), RATE(5). +/// 3. Trailing flags (13 bits): DOWNMIX(1), DYNRANGE(1), TIMSTP(1), +/// AUXDATA(1), HDCD(1), EXT_DESCR(3), EXT_CODING(1), ASPF(1), +/// LFE(2), PRED_HISTORY(1). +/// 4. Optional HEADER_CRC (16 bits) iff `header.crc_present` is set. +/// 5. Post-CRC window (16 bits): MULTIRATE_INTER(1), VERSION(4), +/// COPY_HISTORY(2), PCMR(3), FRONT_SUM(1), SURROUND_SUM(1), +/// DIALNORM(4). +/// +/// The encoder validates the same field bounds [`parse_frame_header`] +/// enforces and is otherwise the inverse of [`parse_frame_header`]. +/// The round-trip property: +/// +/// ```text +/// parse_frame_header(&pad15(encode_frame_header_be(&hdr))) == Ok(hdr') +/// where hdr'.sync_word_encoding == SyncWordEncoding::RawBigEndian +/// hdr' == hdr on every other field +/// and pad15(v) = v padded with zero bytes to length 15 +/// ``` +/// +/// holds because the parser conservatively requires 15 bytes of input +/// (the worst-case `crc_present == 1` window) regardless of the +/// `crc_present` bit, while the encoder emits the actual +/// [`DtsFrameHeader::header_byte_length`] bytes (13 or 15). Callers +/// muxing the encoder output back into a stream should append the +/// SUBFRAMES region they already had (the parser tolerates any +/// trailing bytes); callers re-parsing a bare header should pad with +/// up to two zero bytes. +/// +/// Returns: +/// - [`Error::BlockCountOutOfRange`] if `header.blocks_per_frame < 5` +/// or > 127 (NBLKS is a 7-bit field). +/// - [`Error::FrameSizeOutOfRange`] if `header.frame_size_bytes < 95` +/// or > 16384 (FSIZE-1 is a 14-bit field). +/// - [`Error::FieldOutOfRange`] if any other field is too large for +/// its documented bit width (AMODE > 63, SFREQ > 15, RATE > 31, +/// EXT_DESCR > 7, VERSION > 15, COPY_HISTORY > 3, PCMR > 7, +/// DIALNORM > 15, sample_count_per_block == 0 or > 32). +/// +/// The encoder is the bounded primitive added in round 141; it +/// closes the parse/encode round-trip the wiki bit-table enables +/// without needing any of the docs-blocked value tables. Payload / +/// SUBFRAMES content remains the caller's responsibility — this +/// helper only owns the frame-sync header window. +pub fn encode_frame_header_be(header: &DtsFrameHeader) -> Result> { + // Field-width validation. The parser enforces NBLKS and FSIZE + // bounds; this encoder additionally enforces every field fits its + // declared bit width so a caller cannot smuggle bits past the + // boundary into the next field. + if header.blocks_per_frame < 5 || header.blocks_per_frame > 127 { + return Err(Error::BlockCountOutOfRange { + blocks: header.blocks_per_frame, + }); + } + if !(95..=16384).contains(&header.frame_size_bytes) { + return Err(Error::FrameSizeOutOfRange { + frame_size: header.frame_size_bytes, + }); + } + // sample_count_per_block is stored as +1 of the SHORT field. The + // SHORT field is 5 bits so the valid range is 0..=31, and the + // stored value must be 1..=32. + if header.sample_count_per_block == 0 || header.sample_count_per_block > 32 { + return Err(Error::FieldOutOfRange { + field: "sample_count_per_block", + value: header.sample_count_per_block as u32, + max: 32, + }); + } + if header.amode > 63 { + return Err(Error::FieldOutOfRange { + field: "amode", + value: header.amode as u32, + max: 63, + }); + } + if header.sfreq_index > 15 { + return Err(Error::FieldOutOfRange { + field: "sfreq_index", + value: header.sfreq_index as u32, + max: 15, + }); + } + if header.rate_index > 31 { + return Err(Error::FieldOutOfRange { + field: "rate_index", + value: header.rate_index as u32, + max: 31, + }); + } + if header.ext_descr > 7 { + return Err(Error::FieldOutOfRange { + field: "ext_descr", + value: header.ext_descr as u32, + max: 7, + }); + } + if header.version > 15 { + return Err(Error::FieldOutOfRange { + field: "version", + value: header.version as u32, + max: 15, + }); + } + if header.copy_history > 3 { + return Err(Error::FieldOutOfRange { + field: "copy_history", + value: header.copy_history as u32, + max: 3, + }); + } + if header.source_pcm_resolution_index > 7 { + return Err(Error::FieldOutOfRange { + field: "source_pcm_resolution_index", + value: header.source_pcm_resolution_index as u32, + max: 7, + }); + } + if header.dialog_normalization > 15 { + return Err(Error::FieldOutOfRange { + field: "dialog_normalization", + value: header.dialog_normalization as u32, + max: 15, + }); + } + // header_crc presence must agree with crc_present (encoder is + // strict: silently dropping the value or silently emitting a + // garbage 16-bit field would defeat the round-trip property). + if header.crc_present != header.header_crc.is_some() { + return Err(Error::FieldOutOfRange { + field: "header_crc", + value: header.header_crc.unwrap_or(0) as u32, + max: 0, + }); + } + + // Walk the same bit-table the parser consumes. We accumulate into + // a small bit-vector and chunk to bytes MSB-first; the layout is + // identical to the test helper `build_be_header` used in this + // module's existing test grid (the helper now lives in + // `#[cfg(test)]`, so externalising the logic as a public + // primitive does not duplicate runtime code). + let mut bits: Vec = Vec::with_capacity(120); + + fn push(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + + // Sync (32 bits) — canonical raw-BE 0x7FFE_8001. + push(&mut bits, 0x7FFE_8001, 32); + // Base 43 bits. + push( + &mut bits, + match header.frame_type { + FrameType::Termination => 0, + FrameType::Normal => 1, + }, + 1, + ); + // SHORT = sample_count_per_block - 1. + push(&mut bits, (header.sample_count_per_block - 1) as u32, 5); + push(&mut bits, header.crc_present as u32, 1); + push(&mut bits, header.blocks_per_frame as u32, 7); + // FSIZE-1. + push(&mut bits, (header.frame_size_bytes - 1) as u32, 14); + push(&mut bits, header.amode as u32, 6); + push(&mut bits, header.sfreq_index as u32, 4); + push(&mut bits, header.rate_index as u32, 5); + // Trailing 13 bits. + push(&mut bits, header.downmix as u32, 1); + push(&mut bits, header.dynamic_range as u32, 1); + push(&mut bits, header.time_stamp as u32, 1); + push(&mut bits, header.aux_data as u32, 1); + push(&mut bits, header.hdcd as u32, 1); + push(&mut bits, header.ext_descr as u32, 3); + push(&mut bits, header.ext_coding as u32, 1); + push(&mut bits, header.aspf as u32, 1); + push(&mut bits, header.lfe.code() as u32, 2); + push(&mut bits, header.predictor_history as u32, 1); + // Optional HEADER_CRC. + if let Some(crc) = header.header_crc { + push(&mut bits, crc as u32, 16); + } + // Post-CRC 16 bits. + push(&mut bits, header.multirate_inter as u32, 1); + push(&mut bits, header.version as u32, 4); + push(&mut bits, header.copy_history as u32, 2); + push(&mut bits, header.source_pcm_resolution_index as u32, 3); + push(&mut bits, header.front_sum as u32, 1); + push(&mut bits, header.surround_sum as u32, 1); + push(&mut bits, header.dialog_normalization as u32, 4); + + // The bit-table sums to 104 or 120 bits — both exact multiples of + // 8 by the same arithmetic `header_bit_length()` documents. Assert + // we wrote exactly `header_byte_length()` * 8 bits. + debug_assert_eq!( + bits.len() as u32, + header.header_bit_length(), + "encoder wrote a different bit-count than header_bit_length() reports" + ); + + let mut bytes = Vec::with_capacity(bits.len() / 8); + for chunk in bits.chunks(8) { + let mut b: u8 = 0; + for (i, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - i); + } + } + bytes.push(b); + } + Ok(bytes) +} + +/// Serialise a [`DtsFrameHeader`] back into the 14-bit-packed +/// **big-endian** on-wire byte representation of the frame-sync header +/// window. +/// +/// This is the natural composition of [`encode_frame_header_be`] (which +/// produces the canonical raw-BE header bytes) and +/// [`crate::pack_16bit_to_14bit`] (which re-packs an MSB-first 16-bit +/// bit stream into 14-bit-payload containers per the wiki's "sign bit +/// extension" rule). The output always begins with the wiki-documented +/// 14-bit-BE sync prefix `1F FF E8 00 …` and represents the same +/// bit-content as [`encode_frame_header_be`] would, repacked into +/// 14-bit containers. +/// +/// ## Output length +/// +/// The output is always **18 bytes** long — the minimum input length +/// [`parse_frame_header_14bit`] accepts (nine 14-bit containers = +/// 126 payload bits, unpacking to 16 raw-BE bytes which covers the +/// worst-case 120-bit `crc_present == 1` header window). Both +/// `crc_present` states emit the same length so callers can mux the +/// output into a 14-bit container stream without branching on the +/// flag. The encoder pads the raw-BE header to 15 bytes (= 120 bits = +/// 9 × 14-bit containers minus 6 padding bits per container) before +/// packing: +/// +/// | `crc_present` | raw-BE bytes | padded raw-BE | 14-bit containers | output bytes | +/// | ------------- | ------------ | ------------- | ----------------- | ------------ | +/// | `false` | 13 | 15 | 9 | 18 | +/// | `true` | 15 | 15 | 9 | 18 | +/// +/// For the no-CRC case the two trailing zero bytes of the padded +/// raw-BE input land in what would be the first 16 bits of the +/// SUBFRAMES region of a real DTS frame; the parser only consumes +/// `header.header_bit_length()` bits from the unpacked stream, so the +/// padded zeros are inert for parsing purposes. A caller muxing the +/// encoder output back into a stream should overwrite them (after +/// unpacking) with the actual SUBFRAMES bytes. +/// +/// ## Round-trip +/// +/// `parse_frame_header_14bit(&encode_frame_header_14bit_be(&hdr))` recovers +/// `hdr` on every field except [`DtsFrameHeader::sync_word_encoding`], +/// which the parser reports as +/// [`SyncWordEncoding::FourteenBitBigEndian`] regardless of the input +/// header's value. This is the same round-trip behaviour +/// [`encode_frame_header_be`] / [`encode_frame_header_le`] document for +/// their respective sync encodings. +/// +/// ## Errors +/// +/// Returns the same [`Error`] variants as [`encode_frame_header_be`] +/// for invalid headers (`BlockCountOutOfRange`, `FrameSizeOutOfRange`, +/// `FieldOutOfRange`). +pub fn encode_frame_header_14bit_be(header: &DtsFrameHeader) -> Result> { + let mut raw_be = encode_frame_header_be(header)?; + // Pad the raw-BE bytes to 15 bytes (120 bits) so the pack step + // emits exactly 9 containers = 18 bytes — the parser's minimum + // 14-bit input length. 15 bytes is also the maximum + // `header_byte_length()` value (the `crc_present == true` case), so + // no header bits are dropped. For the no-CRC case the BE encoder + // emits 13 bytes; we extend with 2 zero bytes that land in what + // would be the first 16 bits of the SUBFRAMES region of a real + // frame, which the parser does not consume. + raw_be.resize(15, 0); + let (packed, _payload_bit_count) = + crate::unpack14::pack_16bit_to_14bit(&raw_be, FourteenBitByteOrder::BigEndian); + debug_assert_eq!( + packed.len(), + 18, + "14-bit-BE encoded header must be exactly 18 bytes (9 containers)" + ); + Ok(packed) +} + +/// Serialise a [`DtsFrameHeader`] back into the 14-bit-packed +/// **little-endian** on-wire byte representation of the frame-sync +/// header window. +/// +/// Same composition as [`encode_frame_header_14bit_be`] but with +/// [`FourteenBitByteOrder::LittleEndian`] selected for the pack step, +/// so each 16-bit container is emitted in little-endian byte order. The +/// output always begins with the wiki-documented 14-bit-LE sync prefix +/// `FF 1F 00 E8 …`. +/// +/// Output length is the same as [`encode_frame_header_14bit_be`]: always +/// 18 bytes (regardless of `crc_present`). The 14-bit-LE output is +/// exactly the pairwise byte-swap of the 14-bit-BE output (each +/// two-byte container swapped independently), matching the wiki's +/// relationship between `1F FF E8 00 …` (BE) and `FF 1F 00 E8 …` (LE). +/// +/// ## Round-trip +/// +/// `parse_frame_header_14bit(&encode_frame_header_14bit_le(&hdr))` +/// recovers `hdr` on every field except +/// [`DtsFrameHeader::sync_word_encoding`], which the parser reports as +/// [`SyncWordEncoding::FourteenBitLittleEndian`] regardless of the +/// input header's value. +/// +/// ## Errors +/// +/// Returns the same [`Error`] variants as [`encode_frame_header_be`] +/// for invalid headers. +pub fn encode_frame_header_14bit_le(header: &DtsFrameHeader) -> Result> { + let mut raw_be = encode_frame_header_be(header)?; + // Same 15-byte padding rule as `encode_frame_header_14bit_be`. + raw_be.resize(15, 0); + let (packed, _payload_bit_count) = + crate::unpack14::pack_16bit_to_14bit(&raw_be, FourteenBitByteOrder::LittleEndian); + debug_assert_eq!( + packed.len(), + 18, + "14-bit-LE encoded header must be exactly 18 bytes (9 containers)" + ); + Ok(packed) +} + +/// Parse a single DTS Core frame-sync header from the start of +/// `bytes`. +/// +/// The buffer must begin with one of the two **raw 16-bit** sync +/// sequences (`7F FE 80 01` or its byte-swapped form +/// `FE 7F 01 80`) and contain at least 15 bytes total: a 4-byte +/// sync plus the worst-case 88-bit header (= 11 bytes), which +/// applies when `CRC_PRESENT == 1` and the 16 round-5 post-CRC +/// bits are included. Returns: +/// - [`Error::UnexpectedEof`] on a short buffer. +/// - [`Error::NoSync`] if no documented sync sequence matches at +/// offset zero. +/// - [`Error::UnsupportedFourteenBit`] if a 14-bit-packed sync is +/// found at offset zero — callers with 14-bit input should use +/// [`parse_frame_header_14bit`] (or pre-unpack with +/// [`crate::unpack_14bit_to_16bit`]) instead. +/// +/// The parser is non-allocating and side-effect free. +pub fn parse_frame_header(bytes: &[u8]) -> Result { + let sync = detect_sync(bytes)?; + match sync { + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian => { + return Err(Error::UnsupportedFourteenBit); + } + _ => {} + } + + // Normalise the buffer so that we always read the header from + // a slice whose first 4 bytes are the big-endian sync. For + // RawLittleEndian we byte-swap each 16-bit word in a small + // scratch buffer; only the first ~16 bytes are needed. + let normalised: Vec; + let header_bytes: &[u8] = match sync { + SyncWordEncoding::RawBigEndian => bytes, + SyncWordEncoding::RawLittleEndian => { + // We need 4 sync bytes + ceil(82 / 8) = 11 header bytes. + // Round up to 16 (eight 16-bit words) so any 16-bit + // word straddle stays inside the slice. + let needed = 16; + if bytes.len() < needed { + return Err(Error::UnexpectedEof); + } + let mut scratch = Vec::with_capacity(needed); + for chunk in bytes[..needed].chunks_exact(2) { + scratch.push(chunk[1]); + scratch.push(chunk[0]); + } + normalised = scratch; + &normalised + } + // unreachable: 14-bit branches returned above. + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian => { + unreachable!() + } + }; + + // Need at least 4 sync + 11 header bytes = 15 bytes to read the + // worst-case header bits the round-5 parser consumes + // (32 sync + 43 base + 13 trailing + 16 optional CRC + 16 + // post-CRC = 120 bits = exactly 15 bytes when CRC_PRESENT == 1; + // 104 bits = 13 bytes otherwise). We accept 15. + if header_bytes.len() < 15 { + return Err(Error::UnexpectedEof); + } + + let mut br = BitReader::from_byte_offset(header_bytes, 4); + + let ftype_raw = br.read_bit()?; + let frame_type = if ftype_raw { + FrameType::Normal + } else { + FrameType::Termination + }; + let sample_count_minus_one = br.read_bits(5)? as u8; + let sample_count_per_block = sample_count_minus_one + 1; + let crc_present = br.read_bit()?; + let nblks = br.read_bits(7)? as u8; + if nblks < 5 { + return Err(Error::BlockCountOutOfRange { blocks: nblks }); + } + let fsize_minus_one = br.read_bits(14)? as u16; + let frame_size_bytes = fsize_minus_one + 1; + if frame_size_bytes < 95 { + return Err(Error::FrameSizeOutOfRange { + frame_size: frame_size_bytes, + }); + } + let amode = br.read_bits(6)? as u8; + let sfreq_index = br.read_bits(4)? as u8; + let rate_index = br.read_bits(5)? as u8; + + // Round 3: 13 bits of trailing single-bit / small-field flags. + // Per the wiki snapshot, in this order: + // 1 DOWNMIX, 1 DYNRANGE, 1 TIMSTP, 1 AUXDATA, 1 HDCD, + // 3 EXT_DESCR, 1 EXT_CODING, 1 ASPF, 2 LFE, 1 PRED_HISTORY. + let downmix = br.read_bit()?; + let dynamic_range = br.read_bit()?; + let time_stamp = br.read_bit()?; + let aux_data = br.read_bit()?; + let hdcd = br.read_bit()?; + let ext_descr = br.read_bits(3)? as u8; + let ext_coding = br.read_bit()?; + let aspf = br.read_bit()?; + let lfe_raw = br.read_bits(2)? as u8; + let lfe = LfeMode::from_raw(lfe_raw); + let predictor_history = br.read_bit()?; + + // Round 3: optional 16-bit HEADER_CRC field — present iff + // CRC_PRESENT was set above. + let header_crc = if crc_present { + Some(br.read_bits(16)? as u16) + } else { + None + }; + + // Round 5: 16 bits of post-CRC trailing fields. Per the wiki, + // in MSB-first order: + // 1 MULTIRATE_INTER, 4 VERSION, 2 COPY_HISTORY, + // 3 PCMR, 1 FRONT_SUM, 1 SURROUND_SUM, 4 DIALNORM. + // These bits always follow the predictor-history (when + // crc_present == 0) or the HEADER_CRC field (when set), so they + // are consumed unconditionally. + let multirate_inter = br.read_bit()?; + let version = br.read_bits(4)? as u8; + let copy_history = br.read_bits(2)? as u8; + let source_pcm_resolution_index = br.read_bits(3)? as u8; + let front_sum = br.read_bit()?; + let surround_sum = br.read_bit()?; + let dialog_normalization = br.read_bits(4)? as u8; + + Ok(DtsFrameHeader { + sync_word_encoding: sync, + frame_type, + sample_count_per_block, + crc_present, + blocks_per_frame: nblks, + frame_size_bytes, + amode, + sfreq_index, + rate_index, + downmix, + dynamic_range, + time_stamp, + aux_data, + hdcd, + ext_descr, + ext_coding, + aspf, + lfe, + predictor_history, + header_crc, + multirate_inter, + version, + copy_history, + source_pcm_resolution_index, + front_sum, + surround_sum, + dialog_normalization, + }) +} + +/// Parse a single DTS Core frame-sync header from a 14-bit-packed +/// buffer. +/// +/// The buffer must start with one of the two 14-bit sync sequences +/// documented in `docs/audio/dts/wiki/DTS.wiki` +/// (`1F FF E8 00 07 Fx` for big-endian containers, +/// `FF 1F 00 E8 Fx 07` for little-endian containers). The function +/// runs [`crate::unpack_14bit_to_16bit`] to convert the input into +/// the raw-BE 16-bit form and then delegates to +/// [`parse_frame_header`]. +/// +/// Returns: +/// - [`Error::NoSync`] if the buffer does not start with a 14-bit +/// sync (callers should route raw 16-bit inputs to +/// [`parse_frame_header`] instead). +/// - [`Error::UnexpectedEof`] if the buffer has an odd length, or +/// if the unpacked stream is shorter than the 15 bytes the +/// header parser requires. +/// - the same out-of-range / EOF errors as [`parse_frame_header`] +/// once the unpack succeeds. +/// +/// The unpacker output is byte-aligned every four containers +/// (4 × 14 = 56 bits); the header parser walks at most +/// sync + 56 header bits + 16 CRC bits = 104 bits → 13 bytes for +/// raw-BE input. The 14-bit-packed input therefore needs at least +/// `ceil(104 / 14) * 2 = 16` bytes (= eight 14-bit containers = +/// 112 bits ≥ 104). We require 18 bytes to keep a small margin and +/// to ensure the unpacked stream meets the 15-byte minimum the +/// raw-BE parser asserts up-front. +pub fn parse_frame_header_14bit(bytes: &[u8]) -> Result { + let sync = detect_sync(bytes)?; + let order = match FourteenBitByteOrder::from_sync(sync) { + Some(o) => o, + None => { + // Caller supplied a raw 16-bit sync to the 14-bit entry + // point. Report NoSync to keep the two entry points' + // accepted-input sets disjoint and unambiguous. + return Err(Error::NoSync); + } + }; + // Need at least 18 input bytes (= 9 containers = 126 payload + // bits = 15.75 unpacked bytes, rounded up to 16) so the parser + // can read its 15-byte header window. + if bytes.len() < 18 { + return Err(Error::UnexpectedEof); + } + let unpacked = unpack_14bit_to_16bit(bytes, order)?; + if unpacked.len() < 15 { + return Err(Error::UnexpectedEof); + } + // After unpacking, the stream is raw-BE; delegate to the + // existing parser. We override the returned sync_word_encoding + // so callers see the original 14-bit variant rather than the + // synthesised RawBigEndian one. + let mut hdr = parse_frame_header(&unpacked)?; + hdr.sync_word_encoding = sync; + Ok(hdr) +} + +/// Detect which of the four documented sync sequences (if any) +/// appears at the start of `bytes`. Public to the crate so tests can +/// exercise sync detection independently of header decoding. +/// +/// For the two raw (16-bit) variants this is a literal byte-pattern +/// match against the wiki's documented prefixes. +/// +/// For the two 14-bit variants the detector matches on the **lower +/// 14 bits** of each of the first three 16-bit containers, ignoring +/// the upper 2 bits of each container. This mirrors the unpacker +/// semantics (`docs/audio/dts/wiki/DTS.wiki` says the upper 2 bits +/// are sign-extension, which is informative-only when interpreting +/// the bytes as audio samples). The wiki's literal documented +/// prefixes (`1F FF E8 00 07 Fx` BE and `FF 1F 00 E8 Fx 07` LE) are +/// one specific instantiation of those payloads; sign-extended +/// instantiations encoding the same payloads are also valid 14-bit +/// DTS sync. +pub(crate) fn detect_sync(bytes: &[u8]) -> Result { + if bytes.len() < 4 { + return Err(Error::UnexpectedEof); + } + // Raw 16-bit sequences (4 bytes). + if bytes[..4] == [0x7F, 0xFE, 0x80, 0x01] { + return Ok(SyncWordEncoding::RawBigEndian); + } + if bytes[..4] == [0xFE, 0x7F, 0x01, 0x80] { + return Ok(SyncWordEncoding::RawLittleEndian); + } + // 14-bit sequences (6 bytes = three 16-bit containers carrying + // 42 payload bits). The DTS syncword is 32 payload bits + // (0x7FFE8001); a 14-bit-packed stream encodes those 32 bits + // across containers 0/1 in full (14 + 14 = 28 bits) and the top + // 4 bits of container 2 (28..32). Container 2's bottom 10 bits + // carry frame-header data (FTYPE..NBLKS_high) and must NOT + // participate in sync detection — earlier round-1 code matched + // them too, which incidentally only accepted frames whose + // FTYPE/deficit/CRC/NBLKS_high happened to be `1/31/1/000`. + // + // We confirm bits 0..31 of the unpacked payload equal + // 0x7FFE_8001 by: + // container 0 lower 14 bits == 0x1FFF (covers bits 0..13) + // container 1 lower 14 bits == 0x2800 (covers bits 14..27) + // container 2 lower 14 bits, top 4 == 0b0001 (covers bits 28..31) + if bytes.len() >= 6 { + let c0_be = u16::from_be_bytes([bytes[0], bytes[1]]) & 0x3FFF; + let c1_be = u16::from_be_bytes([bytes[2], bytes[3]]) & 0x3FFF; + let c2_be = u16::from_be_bytes([bytes[4], bytes[5]]) & 0x3FFF; + // c2's top 4 bits within its 14-bit payload: shift right 10 + // and mask to 4 bits. + if c0_be == 0x1FFF && c1_be == 0x2800 && ((c2_be >> 10) & 0xF) == 0x1 { + return Ok(SyncWordEncoding::FourteenBitBigEndian); + } + let c0_le = u16::from_le_bytes([bytes[0], bytes[1]]) & 0x3FFF; + let c1_le = u16::from_le_bytes([bytes[2], bytes[3]]) & 0x3FFF; + let c2_le = u16::from_le_bytes([bytes[4], bytes[5]]) & 0x3FFF; + if c0_le == 0x1FFF && c1_le == 0x2800 && ((c2_le >> 10) & 0xF) == 0x1 { + return Ok(SyncWordEncoding::FourteenBitLittleEndian); + } + } + Err(Error::NoSync) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a synthetic raw-BE DTS frame header with explicit field + /// values, in the bit order documented above. + /// + /// `extra_bits` are the 13 trailing header bits the parser + /// consumes after RATE in round 3 (downmix .. predictor history), + /// passed as a `u32` (only the bottom 13 bits used) so callers + /// can spell the bit-pattern out literally. If + /// `header_crc` is `Some`, the 16-bit CRC is emitted after the + /// 13 trailing bits and `crc_present` should be `1`. `post_crc` + /// (round 5) carries the 16 bits the wiki documents after the + /// optional CRC field (multirate_inter, version, copy_history, + /// PCMR, front_sum, surround_sum, dialnorm) MSB-first. + #[allow(clippy::too_many_arguments)] + fn build_be_header( + ftype: u32, + sample_count_m1: u32, // 5 bits + crc_present: u32, // 1 bit + nblks: u32, // 7 bits + fsize_m1: u32, // 14 bits + amode: u32, // 6 bits + sfreq: u32, // 4 bits + rate: u32, // 5 bits + extra_bits: u32, // 13 bits (downmix..predictor) + header_crc: Option, // 16 bits, only when crc_present == 1 + post_crc: u32, // 16 bits (round-5 trailing window) + ) -> Vec { + // We will accumulate a bit-vector MSB-first and then chunk to + // bytes. + let mut bv: Vec = Vec::new(); + + fn push(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + + // 32-bit sync = 0x7FFE8001 + push(&mut bv, 0x7FFE_8001, 32); + push(&mut bv, ftype, 1); + push(&mut bv, sample_count_m1, 5); + push(&mut bv, crc_present, 1); + push(&mut bv, nblks, 7); + push(&mut bv, fsize_m1, 14); + push(&mut bv, amode, 6); + push(&mut bv, sfreq, 4); + push(&mut bv, rate, 5); + push(&mut bv, extra_bits, 13); + if let Some(crc) = header_crc { + push(&mut bv, crc, 16); + } + // Round 5: 16 post-CRC bits always emitted (the wiki shows + // them following the HEADER_CRC slot whether or not CRC is + // present). + push(&mut bv, post_crc, 16); + // pad to whole bytes + while bv.len() % 8 != 0 { + bv.push(false); + } + // pad to 16 bytes so the LE byte-swap path always has 16 + // bytes too if a caller chooses to reuse this builder. + let mut bytes = Vec::with_capacity(bv.len() / 8); + for chunk in bv.chunks(8) { + let mut b: u8 = 0; + for (i, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - i); + } + } + bytes.push(b); + } + while bytes.len() < 16 { + bytes.push(0); + } + bytes + } + + #[test] + fn detect_raw_be_sync() { + let mut buf = vec![0; 16]; + buf[0] = 0x7F; + buf[1] = 0xFE; + buf[2] = 0x80; + buf[3] = 0x01; + assert_eq!(detect_sync(&buf).unwrap(), SyncWordEncoding::RawBigEndian); + } + + #[test] + fn detect_raw_le_sync() { + let buf = [0xFE, 0x7F, 0x01, 0x80]; + assert_eq!( + detect_sync(&buf).unwrap(), + SyncWordEncoding::RawLittleEndian + ); + } + + #[test] + fn detect_14bit_be_sync() { + let buf = [0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xFA]; + assert_eq!( + detect_sync(&buf).unwrap(), + SyncWordEncoding::FourteenBitBigEndian + ); + } + + #[test] + fn detect_14bit_le_sync() { + let buf = [0xFF, 0x1F, 0x00, 0xE8, 0xF3, 0x07]; + assert_eq!( + detect_sync(&buf).unwrap(), + SyncWordEncoding::FourteenBitLittleEndian + ); + } + + #[test] + fn detect_no_sync_returns_error() { + let buf = [0xDE, 0xAD, 0xBE, 0xEF]; + assert_eq!(detect_sync(&buf).unwrap_err(), Error::NoSync); + } + + #[test] + fn detect_short_buffer_returns_eof() { + assert_eq!(detect_sync(&[0x7F]).unwrap_err(), Error::UnexpectedEof); + } + + #[test] + fn parse_normal_frame_be_typical() { + // Typical values seen on a 48 kHz 1509 kbps 5.1 frame + // (per the wiki's general bit-layout description; we do + // not yet know the actual SFREQ/RATE/AMODE *codes* for + // those Hz/bps/channels — pick arbitrary codes since the + // parser only roundtrips the raw indices). + let bytes = build_be_header( + 1, // FTYPE = normal + 31, // sample_count_m1 = 31 → 32 samples/block + 1, // CRC present + 15, // NBLKS = 15 (16 blocks) + 1023, // FSIZE-1 = 1023 → frame size = 1024 bytes + 9, // AMODE = 9 (raw index) + 13, // SFREQ = 13 + 25, // RATE = 25 + 0b1_0100_1010_0011, // extra trailing 13 bits + Some(0xC0DE), // CRC field present + 0, // round-5 post-CRC bits (all zero) + ); + + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.sync_word_encoding, SyncWordEncoding::RawBigEndian); + assert_eq!(hdr.frame_type, FrameType::Normal); + assert_eq!(hdr.sample_count_per_block, 32); + assert!(hdr.crc_present); + assert_eq!(hdr.blocks_per_frame, 15); + assert_eq!(hdr.frame_size_bytes, 1024); + assert_eq!(hdr.amode, 9); + assert_eq!(hdr.sfreq_index, 13); + assert_eq!(hdr.rate_index, 25); + // Round 3: trailing-13-bit flags decoded MSB-first from + // 0b1_0100_1010_0011 → downmix=1, dyn=0, time=1, aux=0, + // hdcd=0, ext_descr=101=5, ext_coding=0, aspf=0, lfe=01, + // predictor=1. + assert!(hdr.downmix); + assert!(!hdr.dynamic_range); + assert!(hdr.time_stamp); + assert!(!hdr.aux_data); + assert!(!hdr.hdcd); + assert_eq!(hdr.ext_descr, 0b101); + assert!(!hdr.ext_coding); + assert!(!hdr.aspf); + assert_eq!(hdr.lfe, LfeMode::Mode1); + assert!(hdr.predictor_history); + // CRC field present. + assert_eq!(hdr.header_crc, Some(0xC0DE)); + // Round 5: post-CRC window all zeros. + assert!(!hdr.multirate_inter); + assert_eq!(hdr.version, 0); + assert_eq!(hdr.copy_history, 0); + assert_eq!(hdr.source_pcm_resolution_index, 0); + assert!(!hdr.front_sum); + assert!(!hdr.surround_sum); + assert_eq!(hdr.dialog_normalization, 0); + } + + #[test] + fn parse_termination_frame_be() { + let bytes = build_be_header(0, 0, 0, 5, 94, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.frame_type, FrameType::Termination); + assert_eq!(hdr.sample_count_per_block, 1); + assert!(!hdr.crc_present); + assert_eq!(hdr.blocks_per_frame, 5); + assert_eq!(hdr.frame_size_bytes, 95); + // All trailing flags zero by construction. + assert!(!hdr.downmix); + assert!(!hdr.dynamic_range); + assert!(!hdr.time_stamp); + assert!(!hdr.aux_data); + assert!(!hdr.hdcd); + assert_eq!(hdr.ext_descr, 0); + assert!(!hdr.ext_coding); + assert!(!hdr.aspf); + assert_eq!(hdr.lfe, LfeMode::None); + assert!(!hdr.predictor_history); + // crc_present == 0 means no CRC field follows. + assert_eq!(hdr.header_crc, None); + // Round 5: post-CRC window all zeros. + assert!(!hdr.multirate_inter); + assert_eq!(hdr.version, 0); + assert_eq!(hdr.copy_history, 0); + assert_eq!(hdr.source_pcm_resolution_index, 0); + assert!(!hdr.front_sum); + assert!(!hdr.surround_sum); + assert_eq!(hdr.dialog_normalization, 0); + } + + #[test] + fn parse_rejects_nblks_below_5() { + let bytes = build_be_header(1, 31, 1, 4, 1023, 0, 0, 0, 0, Some(0), 0); + assert_eq!( + parse_frame_header(&bytes).unwrap_err(), + Error::BlockCountOutOfRange { blocks: 4 } + ); + } + + #[test] + fn parse_rejects_frame_size_below_95() { + let bytes = build_be_header(1, 31, 1, 16, 93, 0, 0, 0, 0, Some(0), 0); + assert_eq!( + parse_frame_header(&bytes).unwrap_err(), + Error::FrameSizeOutOfRange { frame_size: 94 } + ); + } + + #[test] + fn parse_accepts_largest_documented_values() { + // NBLKS = 127, FSIZE-1 = 16383 → 16384 bytes, AMODE = 63, + // SFREQ = 15, RATE = 31 — all the max-index values the + // wiki allows for these fields. Also exercises the + // largest documented trailing-field codes: ext_descr=7, + // lfe code 3 (Mode3), and all flag bits set. + let bytes = build_be_header( + 1, + 31, + 1, + 127, + 16383, + 63, + 15, + 31, + 0b1_1111_1111_1111, + Some(0xFFFF), + 0xFFFF, + ); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.blocks_per_frame, 127); + assert_eq!(hdr.frame_size_bytes, 16384); + assert_eq!(hdr.amode, 63); + assert_eq!(hdr.sfreq_index, 15); + assert_eq!(hdr.rate_index, 31); + assert!(hdr.downmix); + assert!(hdr.dynamic_range); + assert!(hdr.time_stamp); + assert!(hdr.aux_data); + assert!(hdr.hdcd); + assert_eq!(hdr.ext_descr, 0b111); + assert!(hdr.ext_coding); + assert!(hdr.aspf); + assert_eq!(hdr.lfe, LfeMode::Mode3); + assert!(hdr.predictor_history); + assert_eq!(hdr.header_crc, Some(0xFFFF)); + // Round 5: max-value post-CRC window decodes to ext fields + // at their max codes. + assert!(hdr.multirate_inter); + assert_eq!(hdr.version, 0b1111); + assert_eq!(hdr.copy_history, 0b11); + assert_eq!(hdr.source_pcm_resolution_index, 0b111); + assert!(hdr.front_sum); + assert!(hdr.surround_sum); + assert_eq!(hdr.dialog_normalization, 0b1111); + } + + #[test] + fn parse_short_buffer_returns_eof() { + let mut bytes = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0), 0); + bytes.truncate(8); + assert_eq!( + parse_frame_header(&bytes).unwrap_err(), + Error::UnexpectedEof + ); + } + + #[test] + fn parse_le_byteswapped_matches_be() { + // Build BE bytes then byte-swap each 16-bit word; the + // parsed structural fields must match the BE version + // exactly (only the sync_word_encoding differs). + let be = build_be_header( + 1, + 31, + 1, + 16, + 1023, + 9, + 13, + 25, + 0b1_0100_1010_0011, + Some(0xBEEF), + 0xCAFE, + ); + let mut le = Vec::with_capacity(be.len()); + for chunk in be.chunks_exact(2) { + le.push(chunk[1]); + le.push(chunk[0]); + } + // Sanity-check the sync was swapped to the LE variant. + assert_eq!(&le[..4], &[0xFE, 0x7F, 0x01, 0x80]); + let hdr_be = parse_frame_header(&be).unwrap(); + let hdr_le = parse_frame_header(&le).unwrap(); + assert_eq!(hdr_le.sync_word_encoding, SyncWordEncoding::RawLittleEndian); + assert_eq!(hdr_le.frame_type, hdr_be.frame_type); + assert_eq!(hdr_le.sample_count_per_block, hdr_be.sample_count_per_block); + assert_eq!(hdr_le.crc_present, hdr_be.crc_present); + assert_eq!(hdr_le.blocks_per_frame, hdr_be.blocks_per_frame); + assert_eq!(hdr_le.frame_size_bytes, hdr_be.frame_size_bytes); + assert_eq!(hdr_le.amode, hdr_be.amode); + assert_eq!(hdr_le.sfreq_index, hdr_be.sfreq_index); + assert_eq!(hdr_le.rate_index, hdr_be.rate_index); + // Round 3 fields must also round-trip identically through + // the LE byte-swap path. + assert_eq!(hdr_le.downmix, hdr_be.downmix); + assert_eq!(hdr_le.dynamic_range, hdr_be.dynamic_range); + assert_eq!(hdr_le.time_stamp, hdr_be.time_stamp); + assert_eq!(hdr_le.aux_data, hdr_be.aux_data); + assert_eq!(hdr_le.hdcd, hdr_be.hdcd); + assert_eq!(hdr_le.ext_descr, hdr_be.ext_descr); + assert_eq!(hdr_le.ext_coding, hdr_be.ext_coding); + assert_eq!(hdr_le.aspf, hdr_be.aspf); + assert_eq!(hdr_le.lfe, hdr_be.lfe); + assert_eq!(hdr_le.predictor_history, hdr_be.predictor_history); + assert_eq!(hdr_le.header_crc, hdr_be.header_crc); + // Round 5: post-CRC fields must also round-trip identically + // through the LE byte-swap path. + assert_eq!(hdr_le.multirate_inter, hdr_be.multirate_inter); + assert_eq!(hdr_le.version, hdr_be.version); + assert_eq!(hdr_le.copy_history, hdr_be.copy_history); + assert_eq!( + hdr_le.source_pcm_resolution_index, + hdr_be.source_pcm_resolution_index + ); + assert_eq!(hdr_le.front_sum, hdr_be.front_sum); + assert_eq!(hdr_le.surround_sum, hdr_be.surround_sum); + assert_eq!(hdr_le.dialog_normalization, hdr_be.dialog_normalization); + } + + #[test] + fn parse_14bit_be_returns_unsupported() { + let mut buf = vec![0; 16]; + buf[..6].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xFA]); + assert_eq!( + parse_frame_header(&buf).unwrap_err(), + Error::UnsupportedFourteenBit + ); + } + + #[test] + fn parse_14bit_le_returns_unsupported() { + let mut buf = vec![0; 16]; + buf[..6].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF3, 0x07]); + assert_eq!( + parse_frame_header(&buf).unwrap_err(), + Error::UnsupportedFourteenBit + ); + } + + /// Build a 14-bit BE-packed buffer carrying the same DTS frame + /// the `build_be_header` helper produces in raw-BE form. + #[allow(clippy::too_many_arguments)] + fn build_14bit_packed_header( + order: FourteenBitByteOrder, + ftype: u32, + sample_count_m1: u32, + crc_present: u32, + nblks: u32, + fsize_m1: u32, + amode: u32, + sfreq: u32, + rate: u32, + extra_bits: u32, + header_crc: Option, + post_crc: u32, + ) -> Vec { + // Step 1: build the equivalent raw-BE byte buffer using the + // existing helper. + let raw_be = build_be_header( + ftype, + sample_count_m1, + crc_present, + nblks, + fsize_m1, + amode, + sfreq, + rate, + extra_bits, + header_crc, + post_crc, + ); + // Step 2: walk the raw bit stream MSB-first, emitting 14-bit + // payloads packed into 16-bit containers in the requested + // byte order. + let mut packed: Vec = Vec::new(); + let mut bit_pos: usize = 0; + let total_bits = raw_be.len() * 8; + while bit_pos + 14 <= total_bits { + let mut payload: u16 = 0; + for i in 0..14 { + let abs = bit_pos + i; + let bit = (raw_be[abs / 8] >> (7 - (abs % 8))) & 1; + payload = (payload << 1) | bit as u16; + } + // Sign-extend bit 13 into bits 14..16 per the wiki's + // "upper two bits are sign bit extension" rule. + let container = if payload & 0x2000 != 0 { + payload | 0xC000 + } else { + payload & 0x3FFF + }; + let bytes = match order { + FourteenBitByteOrder::BigEndian => container.to_be_bytes(), + FourteenBitByteOrder::LittleEndian => container.to_le_bytes(), + }; + packed.extend_from_slice(&bytes); + bit_pos += 14; + } + packed + } + + #[test] + fn parse_frame_header_14bit_be_matches_raw_be() { + let raw = build_be_header( + 1, + 31, + 1, + 16, + 1023, + 9, + 13, + 25, + 0b1_0100_1010_0011, + Some(0xC0DE), + 0x9876, + ); + let packed = build_14bit_packed_header( + FourteenBitByteOrder::BigEndian, + 1, + 31, + 1, + 16, + 1023, + 9, + 13, + 25, + 0b1_0100_1010_0011, + Some(0xC0DE), + 0x9876, + ); + let hdr_raw = parse_frame_header(&raw).unwrap(); + let hdr_packed = parse_frame_header_14bit(&packed).unwrap(); + assert_eq!( + hdr_packed.sync_word_encoding, + SyncWordEncoding::FourteenBitBigEndian, + ); + // Every structural field must agree with the raw-BE parse. + assert_eq!(hdr_packed.frame_type, hdr_raw.frame_type); + assert_eq!( + hdr_packed.sample_count_per_block, + hdr_raw.sample_count_per_block, + ); + assert_eq!(hdr_packed.crc_present, hdr_raw.crc_present); + assert_eq!(hdr_packed.blocks_per_frame, hdr_raw.blocks_per_frame); + assert_eq!(hdr_packed.frame_size_bytes, hdr_raw.frame_size_bytes); + assert_eq!(hdr_packed.amode, hdr_raw.amode); + assert_eq!(hdr_packed.sfreq_index, hdr_raw.sfreq_index); + assert_eq!(hdr_packed.rate_index, hdr_raw.rate_index); + // Round 3: trailing flags + optional CRC must round-trip + // identically through 14-bit packing. + assert_eq!(hdr_packed.downmix, hdr_raw.downmix); + assert_eq!(hdr_packed.dynamic_range, hdr_raw.dynamic_range); + assert_eq!(hdr_packed.time_stamp, hdr_raw.time_stamp); + assert_eq!(hdr_packed.aux_data, hdr_raw.aux_data); + assert_eq!(hdr_packed.hdcd, hdr_raw.hdcd); + assert_eq!(hdr_packed.ext_descr, hdr_raw.ext_descr); + assert_eq!(hdr_packed.ext_coding, hdr_raw.ext_coding); + assert_eq!(hdr_packed.aspf, hdr_raw.aspf); + assert_eq!(hdr_packed.lfe, hdr_raw.lfe); + assert_eq!(hdr_packed.predictor_history, hdr_raw.predictor_history); + assert_eq!(hdr_packed.header_crc, hdr_raw.header_crc); + // Round 5: post-CRC fields equivalent through 14-bit + // packing too. + assert_eq!(hdr_packed.multirate_inter, hdr_raw.multirate_inter); + assert_eq!(hdr_packed.version, hdr_raw.version); + assert_eq!(hdr_packed.copy_history, hdr_raw.copy_history); + assert_eq!( + hdr_packed.source_pcm_resolution_index, + hdr_raw.source_pcm_resolution_index + ); + assert_eq!(hdr_packed.front_sum, hdr_raw.front_sum); + assert_eq!(hdr_packed.surround_sum, hdr_raw.surround_sum); + assert_eq!( + hdr_packed.dialog_normalization, + hdr_raw.dialog_normalization + ); + } + + #[test] + fn parse_frame_header_14bit_le_matches_raw_be() { + let raw = build_be_header(0, 0, 0, 5, 94, 0, 0, 0, 0, None, 0); + let packed = build_14bit_packed_header( + FourteenBitByteOrder::LittleEndian, + 0, + 0, + 0, + 5, + 94, + 0, + 0, + 0, + 0, + None, + 0, + ); + let hdr_raw = parse_frame_header(&raw).unwrap(); + let hdr_packed = parse_frame_header_14bit(&packed).unwrap(); + assert_eq!( + hdr_packed.sync_word_encoding, + SyncWordEncoding::FourteenBitLittleEndian, + ); + assert_eq!(hdr_packed.frame_type, FrameType::Termination); + assert_eq!(hdr_packed.frame_type, hdr_raw.frame_type); + assert_eq!(hdr_packed.blocks_per_frame, hdr_raw.blocks_per_frame); + assert_eq!(hdr_packed.frame_size_bytes, hdr_raw.frame_size_bytes); + // No CRC when crc_present == 0. + assert_eq!(hdr_packed.header_crc, None); + // Round 5: post-CRC bits all zero by construction. + assert!(!hdr_packed.multirate_inter); + assert_eq!(hdr_packed.version, 0); + assert_eq!(hdr_packed.dialog_normalization, 0); + } + + /// `parse_frame_header_14bit` must reject a raw-16-bit buffer + /// with `NoSync` so the two entry points stay disjoint. + #[test] + fn parse_frame_header_14bit_rejects_raw_be_input() { + let raw = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, None, 0); + assert_eq!(parse_frame_header_14bit(&raw).unwrap_err(), Error::NoSync,); + } + + #[test] + fn parse_frame_header_14bit_short_buffer_returns_eof() { + // Just the 6-byte sync prefix is below the 18-byte minimum. + let buf = [0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF0]; + assert_eq!( + parse_frame_header_14bit(&buf).unwrap_err(), + Error::UnexpectedEof, + ); + } + + #[test] + fn parse_frame_header_14bit_value_resolvers_resolve_through_14bit_path() { + let packed = build_14bit_packed_header( + FourteenBitByteOrder::BigEndian, + 1, + 31, + 1, + 16, + 1023, + 9, + 13, + 25, + 0, + Some(0), + 0, + ); + let hdr = parse_frame_header_14bit(&packed).unwrap(); + // Round 202: SFREQ=13 → 48 kHz; AMODE=9 → ClRSlSr (5 channels); + // PCMR=0 → 16-bit. RATE code 25 (0b11001) is documented as + // invalid per Table 5-7, so bit_rate_bps() stays None (for a + // now-documented reason — invalid code, not a missing table). + assert_eq!(hdr.sample_rate_hz(), Some(48_000)); + assert_eq!(hdr.bit_rate_bps(), None); + assert_eq!(hdr.targeted_bit_rate(), TargetedBitRate::Invalid); + assert_eq!(hdr.channel_count(), Some(5)); + assert_eq!(hdr.amode_arrangement(), AmodeArrangement::ClRSlSr); + assert_eq!(hdr.source_pcm_bits_per_sample(), Some(16)); + // Round 241: Table 5-20. `post_crc == 0` puts VERNUM = 0 (UNSPEC) + // and DIALNORM = 0, so the gain is 0 dB by the §5.3.1 convention. + assert_eq!(hdr.dialog_normalization_db(), Some(0)); + assert_eq!( + hdr.dialog_normalization_gain(), + DialogNormalization::Unspecified, + ); + } + + #[test] + fn parse_no_sync_returns_no_sync() { + let buf = [0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(parse_frame_header(&buf).unwrap_err(), Error::NoSync); + } + + #[test] + fn value_resolvers_resolve_per_round_202_tables() { + let bytes = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0), 0); + let hdr = parse_frame_header(&bytes).unwrap(); + // Round 202: SFREQ=13 → 48 kHz (Table 5-5); AMODE=9 → + // ClRSlSr / 5-channel arrangement (Table 5-4); PCMR=0 → + // 16-bit (Table 5-17). + assert_eq!(hdr.sample_rate_hz(), Some(48_000)); + assert_eq!(hdr.channel_count(), Some(5)); + assert_eq!(hdr.amode_arrangement(), AmodeArrangement::ClRSlSr); + assert_eq!(hdr.source_pcm_bits_per_sample(), Some(16)); + // RATE code 25 (0b11001) is an invalid Table 5-7 code → + // bit_rate_bps() None, targeted_bit_rate() Invalid. + assert_eq!(hdr.bit_rate_bps(), None); + assert_eq!(hdr.targeted_bit_rate(), TargetedBitRate::Invalid); + // Round 241: Table 5-20. `post_crc == 0` puts VERNUM = 0 (UNSPEC) + // and DIALNORM = 0, so the gain is 0 dB by the §5.3.1 convention. + assert_eq!(hdr.dialog_normalization_db(), Some(0)); + assert_eq!( + hdr.dialog_normalization_gain(), + DialogNormalization::Unspecified, + ); + } + + /// Every fixed `RATE` code (0..=24) resolves to the Table 5-7 + /// bit-rate; the open code (29) and all reserved codes resolve to + /// `Open` / `Invalid` respectively. The expected bps values are + /// transcribed from ETSI §5.3.1 Table 5-7 + /// (`docs/audio/dts/dts-core-extracts.md` §1). + #[test] + fn rate_table_5_7_resolves_every_code() { + // (code, expected fixed bps) for the 25 documented rates. + let fixed: [(u8, u32); 25] = [ + (0, 32_000), + (1, 56_000), + (2, 64_000), + (3, 96_000), + (4, 112_000), + (5, 128_000), + (6, 192_000), + (7, 224_000), + (8, 256_000), + (9, 320_000), + (10, 384_000), + (11, 448_000), + (12, 512_000), + (13, 576_000), + (14, 640_000), + (15, 768_000), + (16, 960_000), + (17, 1_024_000), + (18, 1_152_000), + (19, 1_280_000), + (20, 1_344_000), + (21, 1_408_000), + (22, 1_411_200), + (23, 1_472_000), + (24, 1_536_000), + ]; + for (code, bps) in fixed { + // Build a minimal valid header carrying this RATE code. + let bytes = build_be_header(1, 31, 0, 16, 1023, 2, 13, code as u32, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.rate_index, code); + assert_eq!( + hdr.targeted_bit_rate(), + TargetedBitRate::Fixed(bps), + "RATE code {code} should map to {bps} bps", + ); + assert_eq!(hdr.bit_rate_bps(), Some(bps)); + } + // Open code (0b11101 = 29). + let open = build_be_header(1, 31, 0, 16, 1023, 2, 13, 29, 0, None, 0); + let hdr = parse_frame_header(&open).unwrap(); + assert_eq!(hdr.targeted_bit_rate(), TargetedBitRate::Open); + assert_eq!(hdr.bit_rate_bps(), None); + // Every reserved code (25..=28, 30, 31) is Invalid. + for code in [25u8, 26, 27, 28, 30, 31] { + let bytes = build_be_header(1, 31, 0, 16, 1023, 2, 13, code as u32, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!( + hdr.targeted_bit_rate(), + TargetedBitRate::Invalid, + "RATE code {code} should be Invalid", + ); + assert_eq!(hdr.bit_rate_bps(), None); + } + } + + // --------------------------------------------------------------- + // Round 3 — trailing-13-bit field + optional 16-bit HEADER_CRC. + // --------------------------------------------------------------- + + /// Walk every 2-bit LFE code (0..=3) and verify the [`LfeMode`] + /// round-trips through the parser. + #[test] + fn lfe_mode_codes_round_trip() { + for code in 0..=3u32 { + // extra_bits layout (13 bits MSB-first): 11 leading + // zeros + 2-bit LFE code + 0 predictor. + // bits 0..10 = 0 (downmix..aspf, 11 bits total) + // bits 11..12 = lfe code (we shift left 1 so + // predictor bit stays 0) + let extra = (code & 0b11) << 1; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, extra, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.lfe.code(), code as u8, "code {code}"); + assert_eq!(hdr.lfe.is_present(), code != 0, "is_present({code})"); + // Spot-check the typed enum mapping. + let expected = match code { + 0 => LfeMode::None, + 1 => LfeMode::Mode1, + 2 => LfeMode::Mode2, + _ => LfeMode::Mode3, + }; + assert_eq!(hdr.lfe, expected, "enum mapping for code {code}"); + } + } + + /// When `crc_present == 0` the parser must NOT consume the + /// optional 16-bit CRC field; `header_crc` must be `None`. + #[test] + fn header_crc_absent_when_crc_present_bit_is_zero() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(!hdr.crc_present); + assert_eq!(hdr.header_crc, None); + // verify_header_crc returns None when there is nothing to + // verify. + assert_eq!(hdr.verify_header_crc(), None); + } + + /// When `crc_present == 1` the parser captures the 16-bit field + /// verbatim; verification still returns `None` because §5.3.1 + /// mandates "The CRC value test shall not be applied" for the + /// core HCRC (the Annex B algorithm itself is available as + /// `dts_crc16` for callers that checksum their own spans). + #[test] + fn header_crc_present_returns_raw_field_and_unverified() { + let bytes = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0x1234), 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(hdr.crc_present); + assert_eq!(hdr.header_crc, Some(0x1234)); + // Normative "shall not be applied" -> None by design. + assert_eq!(hdr.verify_header_crc(), None); + } + + /// All-zeros 13-bit trailing window decodes to all-false flags, + /// `ext_descr == 0`, and `LfeMode::None`. + #[test] + fn trailing_bits_all_zero_decodes_clean() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(!hdr.downmix); + assert!(!hdr.dynamic_range); + assert!(!hdr.time_stamp); + assert!(!hdr.aux_data); + assert!(!hdr.hdcd); + assert_eq!(hdr.ext_descr, 0); + assert!(!hdr.ext_coding); + assert!(!hdr.aspf); + assert_eq!(hdr.lfe, LfeMode::None); + assert!(!hdr.predictor_history); + } + + /// All-ones 13-bit trailing window decodes to all-true flags, + /// `ext_descr == 7`, and `LfeMode::Mode3`. + #[test] + fn trailing_bits_all_one_decodes_max() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0b1_1111_1111_1111, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(hdr.downmix); + assert!(hdr.dynamic_range); + assert!(hdr.time_stamp); + assert!(hdr.aux_data); + assert!(hdr.hdcd); + assert_eq!(hdr.ext_descr, 0b111); + assert!(hdr.ext_coding); + assert!(hdr.aspf); + assert_eq!(hdr.lfe, LfeMode::Mode3); + assert!(hdr.predictor_history); + } + + // --------------------------------------------------------------- + // Round 5 — post-CRC 16-bit trailing field window: + // MULTIRATE_INTER + VERSION + COPY_HISTORY + PCMR + FRONT_SUM + // + SURROUND_SUM + DIALNORM. + // --------------------------------------------------------------- + + /// The bit packing of the post-CRC window is (MSB-first): + /// bit 15 = MULTIRATE_INTER, bits 14..11 = VERSION, + /// bits 10..9 = COPY_HISTORY, bits 8..6 = PCMR, + /// bit 5 = FRONT_SUM, bit 4 = SURROUND_SUM, + /// bits 3..0 = DIALNORM. + /// + /// Pick a value that exercises every sub-field at a non-extreme + /// code and confirm the parser decomposes it correctly: + /// MULTIRATE_INTER = 1, VERSION = 0b1010 = 10, + /// COPY_HISTORY = 0b01 = 1, PCMR = 0b011 = 3, FRONT_SUM = 1, + /// SURROUND_SUM = 0, DIALNORM = 0b1100 = 12. + /// Packed: 1 1010 01 011 1 0 1100 = 0b1101001011101100 = 0xD2EC. + #[test] + fn post_crc_window_decomposes_into_individual_fields() { + let bytes = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0), 0xD2EC); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(hdr.multirate_inter); + assert_eq!(hdr.version, 0b1010); + assert_eq!(hdr.copy_history, 0b01); + assert_eq!(hdr.source_pcm_resolution_index, 0b011); + assert!(hdr.front_sum); + assert!(!hdr.surround_sum); + assert_eq!(hdr.dialog_normalization, 0b1100); + } + + /// All-zero post-CRC window decodes to all-zero / all-false + /// across every sub-field. + #[test] + fn post_crc_window_all_zero_decodes_to_zero_fields() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(!hdr.multirate_inter); + assert_eq!(hdr.version, 0); + assert_eq!(hdr.copy_history, 0); + assert_eq!(hdr.source_pcm_resolution_index, 0); + assert!(!hdr.front_sum); + assert!(!hdr.surround_sum); + assert_eq!(hdr.dialog_normalization, 0); + } + + /// All-ones post-CRC window decodes to every sub-field at its + /// maximum code. + #[test] + fn post_crc_window_all_one_decodes_to_max_fields() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0xFFFF); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(hdr.multirate_inter); + assert_eq!(hdr.version, 0b1111); + assert_eq!(hdr.copy_history, 0b11); + assert_eq!(hdr.source_pcm_resolution_index, 0b111); + assert!(hdr.front_sum); + assert!(hdr.surround_sum); + assert_eq!(hdr.dialog_normalization, 0b1111); + } + + /// Walk every 3-bit PCMR code (0..=7) and confirm the parser + /// preserves the raw index and that the round-202 resolver + /// follows Table 5-17. + #[test] + fn pcmr_index_round_trips_for_every_3bit_code() { + for code in 0..=7u32 { + // Pack PCMR into the post-CRC window with every other + // bit cleared so we isolate the field under test. + let post_crc = (code & 0b111) << 6; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!( + hdr.source_pcm_resolution_index, code as u8, + "PCMR code {code}" + ); + // Round 202: resolver follows Table 5-17 (codes 4 and 7 + // are Invalid; the other six map to (bits, es) pairs). + let exp = source_pcm_resolution_from_index(code as u8); + assert_eq!(hdr.source_pcm_resolution(), exp); + match exp { + SourcePcmResolution::Valid { bits, .. } => { + assert_eq!(hdr.source_pcm_bits_per_sample(), Some(bits)); + } + SourcePcmResolution::Invalid => { + assert_eq!(hdr.source_pcm_bits_per_sample(), None); + } + } + } + } + + /// Walk every 4-bit DIALNORM code (0..=15) and confirm the + /// parser preserves the raw index. With `post_crc == code`, the + /// VERNUM nibble at bits 14..11 of the post-CRC word is 0 + /// (UNSPEC) and the DIALNORM nibble at bits 3..0 carries `code`. + /// Per Table 5-20's UNSPEC branch (§5.3.1), the gain is 0 dB for + /// every code. + #[test] + fn dialnorm_code_round_trips_for_every_4bit_value() { + for code in 0..=15u32 { + let post_crc = code & 0b1111; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.dialog_normalization, code as u8, "DIALNORM code {code}"); + assert_eq!(hdr.version, 0, "VERNUM nibble must be 0 by construction"); + // VERNUM=0 → UNSPEC → DNG = 0 dB regardless of DIALNORM. + assert_eq!( + hdr.dialog_normalization_db(), + Some(0), + "UNSPEC (VERNUM ∉ {{6,7}}) → DNG = 0 dB per §5.3.1", + ); + assert_eq!( + hdr.dialog_normalization_gain(), + DialogNormalization::Unspecified, + ); + } + } + + /// Round 241: every Table 5-20 row, exhaustively. For each + /// `VERNUM ∈ {6, 7}` and `DIALNORM ∈ 0..=15`, build the post-CRC + /// word that places `VERNUM` at bits 14..11 and `DIALNORM` at + /// bits 3..0, then parse and assert that the resolver returns the + /// Table 5-20 row's DNG (dB) verbatim. The remaining fourteen + /// `VERNUM` values are exercised by + /// [`dialnorm_unspec_branch_is_zero_db_for_every_other_vernum`]. + #[test] + fn dialnorm_resolver_covers_table_5_20_verbatim() { + // VERNUM == 7: codes 0..=15 → 0 dB down to -15 dB. + for code in 0u32..=15 { + let post_crc = (7u32 << 11) | code; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.version, 7); + assert_eq!(hdr.dialog_normalization, code as u8); + let expected = -(code as i8); + assert_eq!( + hdr.dialog_normalization_db(), + Some(expected), + "VERNUM=7 DIALNORM={code} → DNG {expected} dB", + ); + assert_eq!( + hdr.dialog_normalization_gain(), + DialogNormalization::Fixed(expected), + ); + } + // VERNUM == 6: codes 0..=15 → -16 dB down to -31 dB. + for code in 0u32..=15 { + let post_crc = (6u32 << 11) | code; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.version, 6); + assert_eq!(hdr.dialog_normalization, code as u8); + let expected = -(code as i8) - 16; + assert_eq!( + hdr.dialog_normalization_db(), + Some(expected), + "VERNUM=6 DIALNORM={code} → DNG {expected} dB", + ); + assert_eq!( + hdr.dialog_normalization_gain(), + DialogNormalization::Fixed(expected), + ); + } + } + + /// Round 241: the §5.3.1 UNSPEC branch. For every `VERNUM` + /// outside `{6, 7}` and every 4-bit `DIALNORM` code, the resolver + /// returns `Some(0)` / `Unspecified` (the spec's "DNG=0 indicates + /// No Dialog Normalization" convention for non-named VERNUM + /// values, PDF p.23). + #[test] + fn dialnorm_unspec_branch_is_zero_db_for_every_other_vernum() { + for vernum in 0u32..=15 { + if vernum == 6 || vernum == 7 { + continue; + } + for code in 0u32..=15 { + let post_crc = (vernum << 11) | code; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.version, vernum as u8); + assert_eq!(hdr.dialog_normalization, code as u8); + assert_eq!( + hdr.dialog_normalization_db(), + Some(0), + "VERNUM={vernum} DIALNORM={code} → DNG = 0 dB (UNSPEC)", + ); + assert_eq!( + hdr.dialog_normalization_gain(), + DialogNormalization::Unspecified, + ); + } + } + } + + /// Round 241: pure-function check on + /// [`dialog_normalization_from_codes`]. Confirms the helper + /// reproduces Table 5-20's boundary rows exactly and that only + /// the low 4 bits of each input are consulted. + #[test] + fn dialog_normalization_from_codes_boundary_rows() { + // VERNUM=7 boundary corners: (7, 0) → 0 dB; (7, 15) → -15 dB. + assert_eq!( + dialog_normalization_from_codes(7, 0), + DialogNormalization::Fixed(0), + ); + assert_eq!( + dialog_normalization_from_codes(7, 15), + DialogNormalization::Fixed(-15), + ); + // VERNUM=6 boundary corners: (6, 0) → -16 dB; (6, 15) → -31 dB. + assert_eq!( + dialog_normalization_from_codes(6, 0), + DialogNormalization::Fixed(-16), + ); + assert_eq!( + dialog_normalization_from_codes(6, 15), + DialogNormalization::Fixed(-31), + ); + // UNSPEC branch: a sample of non-{6,7} codes. + for v in [0u8, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15] { + assert_eq!( + dialog_normalization_from_codes(v, 0), + DialogNormalization::Unspecified, + ); + assert_eq!( + dialog_normalization_from_codes(v, 15), + DialogNormalization::Unspecified, + ); + } + // High bits of either input are masked off — the resolver + // consults only the documented 4-bit wire widths. + assert_eq!( + dialog_normalization_from_codes(0xF7, 0xF0), + DialogNormalization::Fixed(0), + ); + assert_eq!( + dialog_normalization_from_codes(0xF6, 0xFF), + DialogNormalization::Fixed(-31), + ); + } + + /// Round 241: [`DialogNormalization::gain_db`] returns the spec's + /// DNG value for both variants — the contained `i8` for `Fixed`, + /// and `0` for `Unspecified`. + #[test] + fn dialog_normalization_gain_db_is_zero_for_unspecified() { + assert_eq!(DialogNormalization::Unspecified.gain_db(), 0); + for db in -31i8..=0 { + assert_eq!(DialogNormalization::Fixed(db).gain_db(), db); + } + } + + /// Round 241: the resolver's range across every reachable + /// `(VERNUM, DIALNORM)` pair is exactly `{0, -1, ..., -31}`. + /// Cross-checks the Table 5-20 + UNSPEC implementation against + /// the spec's stated dynamic range + /// (§5.3.1: "Dialog Normalization Gain ... in dB"). + #[test] + fn dialnorm_resolver_range_is_0_down_to_minus_31() { + let mut seen = [false; 32]; + for v in 0u8..=15 { + for d in 0u8..=15 { + let db = match dialog_normalization_from_codes(v, d) { + DialogNormalization::Fixed(db) => db, + DialogNormalization::Unspecified => 0, + }; + assert!((-31..=0).contains(&db), "DNG out of spec range"); + let bucket = (-db) as usize; + seen[bucket] = true; + } + } + // Every dB value in [-31..=0] must be reachable. + for (i, hit) in seen.iter().enumerate() { + assert!(*hit, "DNG {} dB not reachable", -(i as i32)); + } + } + + /// Walk every 4-bit VERSION code (0..=15) and confirm the parser + /// preserves the raw index. + #[test] + fn version_code_round_trips_for_every_4bit_value() { + for code in 0..=15u32 { + let post_crc = (code & 0b1111) << 11; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.version, code as u8, "VERSION code {code}"); + } + } + + /// Walk every 2-bit COPY_HISTORY code (0..=3) and confirm the + /// parser preserves the raw index. + #[test] + fn copy_history_code_round_trips_for_every_2bit_value() { + for code in 0..=3u32 { + let post_crc = (code & 0b11) << 9; + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, post_crc); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.copy_history, code as u8, "COPY_HISTORY code {code}"); + } + } + + /// The post-CRC bits are consumed unconditionally — both when + /// the optional HEADER_CRC slot is emitted (`crc_present == 1`) + /// and when it is skipped (`crc_present == 0`). Build the same + /// post-CRC payload twice with crc_present flipped and confirm + /// every post-CRC field matches. + #[test] + fn post_crc_window_decodes_regardless_of_crc_present_flag() { + let payload = 0xD2EC; + let with_crc = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xBEEF), payload); + let without_crc = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, payload); + let hdr_with = parse_frame_header(&with_crc).unwrap(); + let hdr_without = parse_frame_header(&without_crc).unwrap(); + assert_eq!(hdr_with.header_crc, Some(0xBEEF)); + assert_eq!(hdr_without.header_crc, None); + // Post-CRC sub-fields must agree. + assert_eq!(hdr_with.multirate_inter, hdr_without.multirate_inter); + assert_eq!(hdr_with.version, hdr_without.version); + assert_eq!(hdr_with.copy_history, hdr_without.copy_history); + assert_eq!( + hdr_with.source_pcm_resolution_index, + hdr_without.source_pcm_resolution_index + ); + assert_eq!(hdr_with.front_sum, hdr_without.front_sum); + assert_eq!(hdr_with.surround_sum, hdr_without.surround_sum); + assert_eq!( + hdr_with.dialog_normalization, + hdr_without.dialog_normalization + ); + } + + // --------------------------------------------------------------- + // Round 138 — header_bit_length() / header_byte_length() + // + // The wiki bit-table sums to 104 bits when `crc_present == 0` and + // 120 bits when `crc_present == 1`. Both totals are exact + // multiples of 8 by construction, so the SUBFRAMES region marked + // `'''TODO'''` in the wiki begins on a byte boundary either way. + // --------------------------------------------------------------- + + /// `header_bit_length()` returns exactly 104 bits when the + /// optional HEADER_CRC slot is NOT present. + #[test] + fn header_bit_length_104_when_crc_absent() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(!hdr.crc_present); + assert_eq!(hdr.header_bit_length(), 104); + assert_eq!(hdr.header_byte_length(), 13); + } + + /// `header_bit_length()` returns exactly 120 bits when the + /// optional HEADER_CRC slot IS present. + #[test] + fn header_bit_length_120_when_crc_present() { + let bytes = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xBEEF), 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert!(hdr.crc_present); + assert_eq!(hdr.header_bit_length(), 120); + assert_eq!(hdr.header_byte_length(), 15); + } + + /// The header-length value matches the byte position the parser's + /// internal bit reader is left at after a full parse. We confirm + /// this by walking the same bit-table independently and observing + /// the sum agrees with the public accessor. + #[test] + fn header_bit_length_matches_manual_wiki_table_sum() { + // Wiki sub-totals from `docs/audio/dts/wiki/DTS.wiki`. + let sync = 32u32; + let base = 1 + 5 + 1 + 7 + 14 + 6 + 4 + 5; // 43 + let trailing = 1 + 1 + 1 + 1 + 1 + 3 + 1 + 1 + 2 + 1; // 13 + let post_crc = 1 + 4 + 2 + 3 + 1 + 1 + 4; // 16 + let crc_slot = 16; // optional + + let absent = build_be_header(1, 31, 0, 5, 94, 0, 0, 0, 0, None, 0); + let hdr_absent = parse_frame_header(&absent).unwrap(); + assert_eq!( + hdr_absent.header_bit_length(), + sync + base + trailing + post_crc, + ); + + let present = build_be_header(1, 31, 1, 5, 94, 0, 0, 0, 0, Some(0), 0); + let hdr_present = parse_frame_header(&present).unwrap(); + assert_eq!( + hdr_present.header_bit_length(), + sync + base + trailing + post_crc + crc_slot, + ); + } + + /// `header_byte_length()` is always a multiple of 8 bits (i.e. + /// the SUBFRAMES region starts on a byte boundary), for both + /// CRC-absent and CRC-present frames and for every combination of + /// the structural fields surfaced through `build_be_header`. + #[test] + fn header_byte_length_is_always_byte_aligned() { + for crc in [0, 1] { + for nblks in [5u32, 16, 127] { + for fsize_m1 in [94u32, 1023, 16383] { + let crc_payload = if crc == 1 { Some(0) } else { None }; + let bytes = + build_be_header(1, 31, crc, nblks, fsize_m1, 0, 0, 0, 0, crc_payload, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!( + hdr.header_bit_length() % 8, + 0, + "bit length must be byte-aligned (crc={crc} nblks={nblks} fsize_m1={fsize_m1})" + ); + assert_eq!( + hdr.header_byte_length() * 8, + hdr.header_bit_length() as usize, + ); + } + } + } + } + + /// The 14-bit-packed entry point exposes the same + /// `header_bit_length()` value as the equivalent raw-BE frame: + /// the byte-length is in unpacked-bitstream bits per the doc + /// comment, not in 14-bit container bits. + #[test] + fn header_bit_length_14bit_matches_raw_be() { + let raw = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xC0DE), 0); + let packed = build_14bit_packed_header( + FourteenBitByteOrder::BigEndian, + 1, + 31, + 1, + 16, + 1023, + 9, + 13, + 25, + 0, + Some(0xC0DE), + 0, + ); + let hdr_raw = parse_frame_header(&raw).unwrap(); + let hdr_packed = parse_frame_header_14bit(&packed).unwrap(); + assert_eq!(hdr_raw.header_bit_length(), 120); + assert_eq!(hdr_packed.header_bit_length(), 120); + assert_eq!( + hdr_raw.header_byte_length(), + hdr_packed.header_byte_length() + ); + } + + // --------------------------------------------------------------- + // Round 189 — frame_size_container_bytes(): 14-bit container-byte + // advance rule per ETSI TS 102 114 V1.3.1 §5.3.1 + the + // 14↔16-bit advance synthesis in + // `docs/audio/dts/dts-core-extracts.md` §3.3. + // + // For the raw encodings the advance equals `frame_size_bytes` + // verbatim (FSIZE+1 already counts container bytes). For the + // 14-bit encodings the same logical span occupies + // `ceil(frame_size_bytes * 8 / 14)` container words = twice that + // many container bytes (one container word = 2 container bytes + // = 16 container bits = 14 logical bits). + // --------------------------------------------------------------- + + /// Returning the bare `frame_size_bytes` for the two raw 16-bit + /// encodings is the explicit `FSIZE+1` contract from + /// `docs/audio/dts/dts-core-extracts.md` §3.1. + #[test] + fn frame_size_container_bytes_raw_equals_frame_size_bytes() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.frame_size_bytes, 1024); + assert_eq!( + hdr.frame_size_container_bytes(SyncWordEncoding::RawBigEndian), + 1024, + ); + assert_eq!( + hdr.frame_size_container_bytes(SyncWordEncoding::RawLittleEndian), + 1024, + ); + } + + /// 14-bit container advance for a 1024-byte logical frame is + /// `ceil(1024 * 8 / 14)` container words = `ceil(8192 / 14)` = + /// 586 words = 1172 container bytes (the ETSI §6.1.3.1 /§6.3.x + /// "28-bit-word boundary" invariant — two container words per + /// 28 logical bits — guarantees the next syncword re-aligns). + #[test] + fn frame_size_container_bytes_14bit_1024_logical_is_1172_container() { + let bytes = build_be_header(1, 31, 0, 16, 1023, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.frame_size_bytes, 1024); + assert_eq!( + hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian), + 1172, + ); + assert_eq!( + hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitLittleEndian), + 1172, + ); + } + + /// Minimum-frame (FSIZE+1 = 95) and maximum-frame (FSIZE+1 = + /// 16384) advances exercise the formula's bottom and top. + /// `ceil(95 * 8 / 14)` = `ceil(760 / 14)` = 55 words = 110 + /// container bytes; `ceil(16384 * 8 / 14)` = `ceil(131072 / 14)` + /// = 9363 words = 18726 container bytes (the last container word + /// carries 760 mod 14 = 4 leftover bits in the minimum case + /// and 131072 mod 14 = 10 leftover bits in the maximum case; + /// each leftover bit-count is non-zero so the closed-form + /// ceiling rounds up to a full container word). + #[test] + fn frame_size_container_bytes_14bit_min_and_max() { + let bytes_min = build_be_header(1, 31, 0, 5, 94, 0, 0, 0, 0, None, 0); + let hdr_min = parse_frame_header(&bytes_min).unwrap(); + assert_eq!(hdr_min.frame_size_bytes, 95); + assert_eq!( + hdr_min.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian), + 110, + ); + + let bytes_max = build_be_header(1, 31, 0, 127, 16383, 0, 0, 0, 0, None, 0); + let hdr_max = parse_frame_header(&bytes_max).unwrap(); + assert_eq!(hdr_max.frame_size_bytes, 16384); + assert_eq!( + hdr_max.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian), + 18726, + ); + } + + /// Encoding-agnostic invariant: the 14-bit container advance is + /// always strictly greater than the raw advance (because 16 + /// container bits carry only 14 logical bits, so any non-zero + /// logical span needs at least one extra container byte) and the + /// difference is bounded by `ceil(frame_size_bytes * 2 / 14) + + /// 1` — i.e. the 14/16 scaling overhead plus at most one + /// rounding-up word. + #[test] + fn frame_size_container_bytes_14bit_is_strictly_greater_than_raw() { + for fsize_m1 in [94u32, 511, 1023, 2047, 4095, 8191, 16383] { + let bytes = build_be_header(1, 31, 0, 16, fsize_m1, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + let raw = hdr.frame_size_container_bytes(SyncWordEncoding::RawBigEndian); + let packed = hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian); + assert!( + packed > raw, + "14-bit container advance must exceed raw (fsize_bytes={}, raw={}, packed={})", + hdr.frame_size_bytes, + raw, + packed, + ); + // Upper bound: scaling factor is exactly 16/14, plus + // at most one extra container word (2 bytes) of + // round-up. + let ub = (raw * 16).div_ceil(14) + 2; + assert!( + packed <= ub, + "14-bit advance overshoots scaling bound (fsize_bytes={}, raw={}, packed={}, ub={})", + hdr.frame_size_bytes, + raw, + packed, + ub, + ); + } + } + + /// BE vs LE container-byte advance is identical: 14-bit-LE is the + /// pairwise byte-swap of 14-bit-BE per the wiki, so the + /// container-byte count is invariant under the BE/LE flip (and + /// likewise for the raw pair, where raw-LE = 16-bit-word-swap + /// of raw-BE). + #[test] + fn frame_size_container_bytes_be_le_equivalence() { + for fsize_m1 in [94u32, 1023, 16383] { + let bytes = build_be_header(1, 31, 0, 16, fsize_m1, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!( + hdr.frame_size_container_bytes(SyncWordEncoding::RawBigEndian), + hdr.frame_size_container_bytes(SyncWordEncoding::RawLittleEndian), + ); + assert_eq!( + hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian), + hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitLittleEndian), + ); + } + } + + /// The container-byte advance is always even for the 14-bit + /// encodings because a partial 14-bit word is padded out to a + /// full 16-bit (two-byte) container by the §3.3 / §6.1.3.1 + /// "28-bit-word boundary" invariant — the next syncword + /// re-aligns on a two-container-word boundary so the per-frame + /// step lands on an even byte count. + #[test] + fn frame_size_container_bytes_14bit_is_even() { + for fsize_m1 in [94u32, 100, 511, 1023, 1535, 16383] { + let bytes = build_be_header(1, 31, 0, 16, fsize_m1, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + let n = hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian); + assert_eq!( + n % 2, + 0, + "14-bit container advance must be even (fsize_bytes={}, advance={})", + hdr.frame_size_bytes, + n, + ); + } + } + + /// Manual closed-form cross-check: `frame_size_container_bytes` + /// for a 14-bit encoding must equal + /// `2 * ceil(frame_size_bytes * 8 / 14)` (i.e. the rounded-up + /// container-word count times two container bytes per word). + #[test] + fn frame_size_container_bytes_14bit_matches_closed_form() { + for fsize_m1 in [94u32, 200, 400, 1023, 8192, 16383] { + let bytes = build_be_header(1, 31, 0, 16, fsize_m1, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes).unwrap(); + let logical_bits = (hdr.frame_size_bytes as u32) * 8; + let expected = 2 * logical_bits.div_ceil(14); + let actual = hdr.frame_size_container_bytes(SyncWordEncoding::FourteenBitBigEndian); + assert_eq!( + expected, actual, + "closed-form mismatch (fsize_bytes={}, expected={}, actual={})", + hdr.frame_size_bytes, expected, actual, + ); + } + } + + // --------------------------------------------------------------- + // Round 141 — encode_frame_header_be(): parse ↔ encode round-trip. + // + // The encoder is the inverse of `parse_frame_header` against the + // wiki bit-table. Every structural field round-trips bit-exact; + // the encoder's output is always exactly `header_byte_length()` + // bytes long and starts with the canonical raw-BE sync regardless + // of the source `sync_word_encoding`. + // --------------------------------------------------------------- + + /// A synthesised non-trivial header with every structural field + /// set to a distinctive value round-trips through encode → parse + /// with bit-exact equality on every field except + /// `sync_word_encoding` (the encoder always emits raw-BE). + #[test] + fn encode_round_trip_non_trivial_with_crc() { + let bytes_in = build_be_header( + 1, // FTYPE + 31, // SHORT + 1, // CRC present + 16, // NBLKS + 1023, // FSIZE-1 + 9, // AMODE + 13, // SFREQ + 25, // RATE + 0b1_0100_1010_0011, // 13 trailing bits + Some(0xC0DE), // HEADER_CRC + 0xD2EC, // 16 post-CRC bits + ); + let hdr = parse_frame_header(&bytes_in).unwrap(); + + let encoded = encode_frame_header_be(&hdr).expect("encode must succeed"); + // header_byte_length() reports 15 because crc_present is set. + assert_eq!(encoded.len(), hdr.header_byte_length()); + assert_eq!(encoded.len(), 15); + + // The encoded output begins with the canonical raw-BE sync. + assert_eq!(&encoded[..4], &[0x7F, 0xFE, 0x80, 0x01]); + + // The header bytes byte-for-byte match the synthesised input + // (build_be_header pads to 16 bytes; encode_frame_header_be + // emits exactly 15 because crc_present is set). + assert_eq!(&encoded[..], &bytes_in[..encoded.len()]); + + // Re-parse the encoded bytes and confirm every field is + // identical except `sync_word_encoding`. + let mut hdr_round = parse_frame_header(&encoded).unwrap(); + assert_eq!(hdr_round.sync_word_encoding, SyncWordEncoding::RawBigEndian); + hdr_round.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_round, hdr); + } + + /// Termination frame with no CRC: encoder emits exactly 13 bytes. + /// The 13-byte header window is shorter than the 15-byte minimum + /// the parser requires (the parser always reads up to the + /// worst-case CRC-present 120-bit window before discriminating); + /// for the round-trip we pad the encoder output with two + /// scratch-SUBFRAMES bytes (the actual SUBFRAMES region begins + /// immediately after the 13-byte header anyway). + #[test] + fn encode_round_trip_termination_no_crc_minimal() { + let bytes_in = build_be_header(0, 0, 0, 5, 94, 0, 0, 0, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + + let encoded = encode_frame_header_be(&hdr).unwrap(); + assert_eq!(encoded.len(), 13); + assert_eq!(encoded.len(), hdr.header_byte_length()); + + // Bytes 0..13 must match the synthesised input (build_be_header + // pads to 16 but the meaningful header window is 13 bytes). + assert_eq!(&encoded[..], &bytes_in[..13]); + + let mut padded = encoded.clone(); + padded.extend_from_slice(&[0u8; 2]); + let hdr_round = parse_frame_header(&padded).unwrap(); + assert_eq!(hdr_round.frame_type, FrameType::Termination); + assert_eq!(hdr_round.sample_count_per_block, 1); + assert!(!hdr_round.crc_present); + assert_eq!(hdr_round.blocks_per_frame, 5); + assert_eq!(hdr_round.frame_size_bytes, 95); + // Sync_word_encoding is the only differing field by design. + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// Field-bound enforcement: NBLKS < 5 is rejected by the encoder + /// (mirrors the parser bound). + #[test] + fn encode_rejects_nblks_below_5() { + let hdr = synth_hdr(|h| h.blocks_per_frame = 4); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!(err, Error::BlockCountOutOfRange { blocks: 4 }); + } + + /// Field-bound enforcement: NBLKS > 127 cannot fit the 7-bit + /// field. + #[test] + fn encode_rejects_nblks_above_127() { + let hdr = synth_hdr(|h| h.blocks_per_frame = 128); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!(err, Error::BlockCountOutOfRange { blocks: 128 }); + } + + /// Field-bound enforcement: FSIZE < 95 is rejected (parser bound). + #[test] + fn encode_rejects_frame_size_below_95() { + let hdr = synth_hdr(|h| h.frame_size_bytes = 94); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!(err, Error::FrameSizeOutOfRange { frame_size: 94 }); + } + + /// Field-bound enforcement: FSIZE > 16384 cannot fit the 14-bit + /// FSIZE-1 field (max 16383+1 = 16384). + #[test] + fn encode_rejects_frame_size_above_16384() { + let hdr = synth_hdr(|h| h.frame_size_bytes = 16385); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!(err, Error::FrameSizeOutOfRange { frame_size: 16385 }); + } + + /// Field-bound enforcement: AMODE > 63 cannot fit the 6-bit field. + #[test] + fn encode_rejects_amode_above_63() { + let hdr = synth_hdr(|h| h.amode = 64); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!( + err, + Error::FieldOutOfRange { + field: "amode", + value: 64, + max: 63 + } + ); + } + + /// Field-bound enforcement: PCMR > 7 cannot fit the 3-bit field. + #[test] + fn encode_rejects_pcmr_above_7() { + let hdr = synth_hdr(|h| h.source_pcm_resolution_index = 8); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!( + err, + Error::FieldOutOfRange { + field: "source_pcm_resolution_index", + value: 8, + max: 7, + } + ); + } + + /// Field-bound enforcement: VERSION > 15 cannot fit the 4-bit + /// field. + #[test] + fn encode_rejects_version_above_15() { + let hdr = synth_hdr(|h| h.version = 16); + let err = encode_frame_header_be(&hdr).unwrap_err(); + assert_eq!( + err, + Error::FieldOutOfRange { + field: "version", + value: 16, + max: 15, + } + ); + } + + /// Field-bound enforcement: `header_crc.is_some()` must match + /// `crc_present`. A `Some(_)` payload with `crc_present == false` + /// is rejected so a silent emit-or-drop bug cannot break the + /// round-trip. + #[test] + fn encode_rejects_crc_payload_without_crc_present() { + let hdr = synth_hdr(|h| { + h.crc_present = false; + h.header_crc = Some(0x1234); + }); + let err = encode_frame_header_be(&hdr).unwrap_err(); + match err { + Error::FieldOutOfRange { field, .. } => assert_eq!(field, "header_crc"), + other => panic!("expected FieldOutOfRange{{field: header_crc}}, got {other:?}"), + } + } + + /// Mirror: `crc_present == true` with `header_crc == None` is + /// also rejected (no silent zeroing of the field). + #[test] + fn encode_rejects_crc_present_without_payload() { + let hdr = synth_hdr(|h| { + h.crc_present = true; + h.header_crc = None; + }); + let err = encode_frame_header_be(&hdr).unwrap_err(); + match err { + Error::FieldOutOfRange { field, .. } => assert_eq!(field, "header_crc"), + other => panic!("expected FieldOutOfRange{{field: header_crc}}, got {other:?}"), + } + } + + /// Encoding a header parsed from the raw-LE input still emits the + /// canonical raw-BE on-wire bytes — only `sync_word_encoding` + /// differs in the re-parsed result. + #[test] + fn encode_normalises_le_input_to_raw_be_output() { + let raw_be = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + // Byte-swap each pair to obtain the raw-LE form. + let raw_le: Vec = raw_be.chunks_exact(2).flat_map(|c| [c[1], c[0]]).collect(); + let hdr_le = parse_frame_header(&raw_le).unwrap(); + assert_eq!(hdr_le.sync_word_encoding, SyncWordEncoding::RawLittleEndian); + + let encoded = encode_frame_header_be(&hdr_le).unwrap(); + // First 4 bytes are the canonical raw-BE sync, NOT the + // byte-swapped LE form. + assert_eq!(&encoded[..4], &[0x7F, 0xFE, 0x80, 0x01]); + + // Pad to the parser's 15-byte minimum (encoded.len() is 13 + // because crc_present is false here). + let mut padded = encoded.clone(); + padded.extend_from_slice(&[0u8; 2]); + let hdr_round = parse_frame_header(&padded).unwrap(); + assert_eq!(hdr_round.sync_word_encoding, SyncWordEncoding::RawBigEndian); + // Every other field is preserved. + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr_le.sync_word_encoding; + assert_eq!(hdr_norm, hdr_le); + } + + /// Exhaustive grid: for every documented LFE code, both CRC + /// states, and a representative {NBLKS, FSIZE} pair, the encoded + /// output round-trips back through the parser. + #[test] + fn encode_round_trip_grid_lfe_crc_states() { + for crc_state in [false, true] { + for lfe_code in 0u8..=3 { + for &(nblks, fsize) in &[(5u8, 95u16), (16u8, 1024u16), (127u8, 16384u16)] { + let crc_arg = if crc_state { Some(0xBEEF) } else { None }; + let bytes = build_be_header( + 1, + 31, + crc_state as u32, + nblks as u32, + (fsize - 1) as u32, + 9, + 13, + 25, + // Stuff in the LFE code at the right offset + // within the 13-bit trailing slot: positions + // MSB-first are 1+1+1+1+1+3+1+1+(2)+1 = LFE at + // bit-offset 9..11 (0-indexed from MSB), so + // the 2-bit field sits at bit (12 - 9 .. 12 - + // 9 + 2) within the 13-bit value, i.e. shift + // left by 1. Easier: encode through the + // accessor route rather than hand-bitfiddling. + ((lfe_code as u32) & 0b11) << 1, + crc_arg, + 0, + ); + let hdr = parse_frame_header(&bytes).unwrap(); + assert_eq!(hdr.lfe.code(), lfe_code); + assert_eq!(hdr.crc_present, crc_state); + + let encoded = encode_frame_header_be(&hdr).unwrap(); + assert_eq!(encoded.len(), hdr.header_byte_length()); + + // Pad the encoded output to the parser's 15-byte + // minimum: for crc_present=false the encoder + // emits 13 bytes, while the parser conservatively + // requires the 120-bit worst-case window. + let mut padded = encoded.clone(); + while padded.len() < 15 { + padded.push(0); + } + let hdr_round = parse_frame_header(&padded).unwrap(); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + } + } + } + + /// `encode_frame_header_be(parse(b))` reproduces the prefix of the + /// real ffmpeg fixture byte-for-byte (the public FFMPEG fixture + /// lives in `tests/black_box_ffmpeg.rs`; we re-inline the first + /// 16 bytes here for a unit-test-level assertion). + #[test] + fn encode_reproduces_ffmpeg_fixture_header_prefix() { + // Same bytes as in tests/black_box_ffmpeg.rs. + let ffmpeg_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let hdr = parse_frame_header(&ffmpeg_bytes).unwrap(); + // ffmpeg's frame has crc_present == false, so the header + // window is 13 bytes long. + assert!(!hdr.crc_present); + assert_eq!(hdr.header_byte_length(), 13); + + let encoded = encode_frame_header_be(&hdr).unwrap(); + assert_eq!(encoded.len(), 13); + assert_eq!(&encoded[..], &ffmpeg_bytes[..13]); + } + + /// Helper for the bounds tests: build a baseline well-formed + /// header from `build_be_header` defaults and then let the caller + /// mutate a single field before encoding. + fn synth_hdr(mutate: impl FnOnce(&mut DtsFrameHeader)) -> DtsFrameHeader { + let bytes = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let mut h = parse_frame_header(&bytes).unwrap(); + mutate(&mut h); + h + } + + // --------------------------------------------------------------- + // Round 145 — encode_frame_header_le(): raw-LE encoder variant. + // + // The raw-LE encoder is `encode_frame_header_be` + zero-pad to 16 + // bytes + word-swap pairs. The output always starts with the + // canonical raw-LE sync `FE 7F 01 80` and is exactly 16 bytes + // long; the parser's raw-LE branch consumes the first + // `header_bit_length()` bits (104 or 120) and ignores the trailing + // zero padding. + // --------------------------------------------------------------- + + /// The first 4 bytes of the encoder output are the canonical + /// raw-LE sync regardless of the input header's + /// `sync_word_encoding`. + #[test] + fn encode_le_emits_canonical_raw_le_sync() { + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_le(&hdr).unwrap(); + assert_eq!(&encoded[..4], &[0xFE, 0x7F, 0x01, 0x80]); + } + + /// Encoder output is always exactly 16 bytes regardless of + /// `crc_present` (the parser's raw-LE branch reads a 16-byte + /// window). + #[test] + fn encode_le_is_always_16_bytes() { + // crc_present == false: BE encoder emits 13 bytes; LE pads to + // 16. + let bytes_no_crc = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr_no_crc = parse_frame_header(&bytes_no_crc).unwrap(); + assert!(!hdr_no_crc.crc_present); + assert_eq!(encode_frame_header_le(&hdr_no_crc).unwrap().len(), 16); + + // crc_present == true: BE encoder emits 15 bytes; LE pads to + // 16. + let bytes_crc = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr_crc = parse_frame_header(&bytes_crc).unwrap(); + assert!(hdr_crc.crc_present); + assert_eq!(encode_frame_header_le(&hdr_crc).unwrap().len(), 16); + } + + /// Bit-for-bit equivalence with the manual word-swap of the BE + /// encoder output: `LE == swap16(BE.padded_to_16())`. + #[test] + fn encode_le_equals_word_swapped_be_padded() { + let bytes_in = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let be = encode_frame_header_be(&hdr).unwrap(); + let le = encode_frame_header_le(&hdr).unwrap(); + // Pad BE to 16 and word-swap. + let mut expected = be.clone(); + expected.resize(16, 0); + for pair in expected.chunks_exact_mut(2) { + pair.swap(0, 1); + } + assert_eq!(le, expected); + } + + /// `parse_frame_header(&encode_frame_header_le(&hdr))` round-trips + /// every field except `sync_word_encoding` (which always reports + /// `RawBigEndian` after parsing because the parser word-swaps the + /// LE input back into raw-BE scratch — but the input's first 4 + /// bytes were the raw-LE sync, so the parser reports + /// `RawLittleEndian`). + #[test] + fn encode_le_round_trips_through_parser_no_crc() { + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded_le = encode_frame_header_le(&hdr).unwrap(); + let hdr_round = parse_frame_header(&encoded_le).unwrap(); + // The parser reports RawLittleEndian because that's the sync + // it detected at the start of the input. + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::RawLittleEndian + ); + // Every other field is preserved. + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// Same as above with crc_present == true. + #[test] + fn encode_le_round_trips_through_parser_with_crc() { + let bytes_in = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded_le = encode_frame_header_le(&hdr).unwrap(); + let hdr_round = parse_frame_header(&encoded_le).unwrap(); + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::RawLittleEndian + ); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// `encode_frame_header_le` inherits the same field-bound checks + /// as `encode_frame_header_be` (they share the underlying call). + /// Spot-check NBLKS bound here so a future refactor can't drop + /// the validation silently. + #[test] + fn encode_le_rejects_nblks_below_5() { + let hdr = synth_hdr(|h| h.blocks_per_frame = 4); + let err = encode_frame_header_le(&hdr).unwrap_err(); + assert_eq!(err, Error::BlockCountOutOfRange { blocks: 4 }); + } + + /// Spot-check the `header_crc` / `crc_present` mismatch is also + /// rejected by the LE wrapper. + #[test] + fn encode_le_rejects_crc_payload_mismatch() { + let hdr = synth_hdr(|h| { + h.crc_present = false; + h.header_crc = Some(0xBEEF); + }); + let err = encode_frame_header_le(&hdr).unwrap_err(); + match err { + Error::FieldOutOfRange { field, .. } => assert_eq!(field, "header_crc"), + other => panic!("expected FieldOutOfRange{{field: header_crc}}, got {other:?}"), + } + } + + /// Exhaustive grid: every documented LFE code × both CRC states × + /// representative {NBLKS, FSIZE} pairs round-trip through the LE + /// encoder. + #[test] + fn encode_le_round_trip_grid_lfe_crc_states() { + for crc_state in [false, true] { + for lfe_code in 0u8..=3 { + for &(nblks, fsize) in &[(5u8, 95u16), (16u8, 1024u16), (127u8, 16384u16)] { + let crc_arg = if crc_state { Some(0xBEEF) } else { None }; + let bytes = build_be_header( + 1, + 31, + crc_state as u32, + nblks as u32, + (fsize - 1) as u32, + 9, + 13, + 25, + ((lfe_code as u32) & 0b11) << 1, + crc_arg, + 0, + ); + let hdr = parse_frame_header(&bytes).unwrap(); + let encoded = encode_frame_header_le(&hdr).unwrap(); + assert_eq!(encoded.len(), 16); + assert_eq!(&encoded[..4], &[0xFE, 0x7F, 0x01, 0x80]); + let hdr_round = parse_frame_header(&encoded).unwrap(); + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::RawLittleEndian + ); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + } + } + } + + /// Reproducing the real ffmpeg fixture's first 16 bytes as a + /// raw-LE on-wire payload: byte-swap the BE bytes pairwise and + /// confirm the encoder matches. + #[test] + fn encode_le_reproduces_ffmpeg_fixture_byte_swapped() { + let ffmpeg_be: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let hdr = parse_frame_header(&ffmpeg_be).unwrap(); + assert!(!hdr.crc_present); + + // Manual byte-swap of the BE fixture (the on-wire raw-LE form + // a Wave-container-encapsulated DTS-on-CD would carry). + let mut expected = [0u8; 16]; + for i in 0..8 { + expected[i * 2] = ffmpeg_be[i * 2 + 1]; + expected[i * 2 + 1] = ffmpeg_be[i * 2]; + } + let encoded = encode_frame_header_le(&hdr).unwrap(); + // The BE encoder for this header returns 13 bytes (crc absent), + // padded to 16 with three zero bytes. The trailing 3 bytes of + // the BE-padded-to-16 buffer are `00 00 00`; after word-swap + // the trailing 3 bytes of the LE output are also `00 00 00`. + // The ffmpeg fixture's bytes 13..16 are `00 03 ef 7f` (real + // SUBFRAMES content) — those bytes won't match our zero + // padding. Compare only the first 13 bytes (the header window + // proper) plus byte 13 of `expected`... actually the LE + // encoder pads bytes 13..16 with zeros, so word-swap puts + // zeros at LE bytes 12..16 only if BE bytes 12..16 were also + // zero — which they aren't (BE byte 12 is `0x00`, byte 13 is + // `0x03` from real fixture). So only compare the first 12 + // bytes (6 full 16-bit words) which are unambiguous. + assert_eq!(&encoded[..12], &expected[..12]); + // Byte 12 of BE is part of the header (it's BE byte 12 = `0x00` + // = first byte of post-CRC window's continuation, the header's + // last byte). Encoder padded BE to 16 with zeros at indices + // 13..16, so LE encoder's index 13 corresponds to BE's index + // 12. expected[13] is the byte-swap of (BE[12], BE[13]) at + // index 1 = BE[12] = 0x00; encoded[13] is the byte-swap of + // (BE[12], 0) at index 1 = BE[12] = 0x00. They match. + assert_eq!(encoded[13], expected[13]); + } + + // --------------------------------------------------------------- + // Round 148 — encode_frame_header_14bit_{be,le}(): 14-bit-packed + // encoder variants. + // + // The 14-bit encoders compose `encode_frame_header_be` with the + // round-145 `pack_16bit_to_14bit` primitive. The raw-BE 13- or + // 15-byte header window is zero-padded to 16 bytes so the pack + // step emits 9 14-bit containers = 18 bytes — the parser's + // minimum input length for the 14-bit branch. Both encoders emit + // exactly 18 bytes regardless of `crc_present`; the 14-bit-LE + // output is the pairwise byte-swap of the 14-bit-BE output. + // --------------------------------------------------------------- + + use crate::header::{encode_frame_header_14bit_be, encode_frame_header_14bit_le}; + + /// 14-bit-BE encoder output is exactly 18 bytes regardless of + /// `crc_present` (matches the parser's minimum 14-bit input length). + #[test] + fn encode_14bit_be_is_always_18_bytes() { + let bytes_no_crc = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr_no_crc = parse_frame_header(&bytes_no_crc).unwrap(); + assert!(!hdr_no_crc.crc_present); + assert_eq!(encode_frame_header_14bit_be(&hdr_no_crc).unwrap().len(), 18); + + let bytes_crc = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr_crc = parse_frame_header(&bytes_crc).unwrap(); + assert!(hdr_crc.crc_present); + assert_eq!(encode_frame_header_14bit_be(&hdr_crc).unwrap().len(), 18); + } + + /// 14-bit-LE encoder output is also always 18 bytes. + #[test] + fn encode_14bit_le_is_always_18_bytes() { + let bytes_no_crc = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr_no_crc = parse_frame_header(&bytes_no_crc).unwrap(); + assert_eq!(encode_frame_header_14bit_le(&hdr_no_crc).unwrap().len(), 18); + + let bytes_crc = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr_crc = parse_frame_header(&bytes_crc).unwrap(); + assert_eq!(encode_frame_header_14bit_le(&hdr_crc).unwrap().len(), 18); + } + + /// The first 4 bytes of the 14-bit-BE output match the wiki's + /// `1F FF E8 00` sync-prefix. The wiki documents the sync as + /// `1F FF E8 00 07 Fx` (6 bytes); the trailing `Fx` byte is + /// the upper 4 bits of the FTYPE/SHORT/CRC_PRESENT/NBLKS_high + /// continuation, which depends on the frame's specific header. + #[test] + fn encode_14bit_be_starts_with_wiki_sync_prefix() { + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_14bit_be(&hdr).unwrap(); + // Wiki's first 4 bytes are unambiguous (they're the sign- + // extended 14-bit re-packing of the 32-bit raw-BE sync). + assert_eq!(&encoded[..4], &[0x1F, 0xFF, 0xE8, 0x00]); + } + + /// The first 4 bytes of the 14-bit-LE output match the wiki's + /// `FF 1F 00 E8` sync-prefix. + #[test] + fn encode_14bit_le_starts_with_wiki_sync_prefix() { + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_14bit_le(&hdr).unwrap(); + assert_eq!(&encoded[..4], &[0xFF, 0x1F, 0x00, 0xE8]); + } + + /// 14-bit-LE output is the pairwise byte-swap of the 14-bit-BE + /// output (each 16-bit container is swapped independently — the + /// payload bits are identical, only the container byte order + /// differs). + #[test] + fn encode_14bit_le_equals_pairwise_byte_swap_of_be() { + let bytes_in = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let be = encode_frame_header_14bit_be(&hdr).unwrap(); + let le = encode_frame_header_14bit_le(&hdr).unwrap(); + assert_eq!(be.len(), le.len()); + assert_eq!(be.len() % 2, 0, "container-aligned"); + let mut expected = be.clone(); + for pair in expected.chunks_exact_mut(2) { + pair.swap(0, 1); + } + assert_eq!(le, expected); + } + + /// `parse_frame_header_14bit(&encode_frame_header_14bit_be(&hdr))` + /// round-trips every field except `sync_word_encoding`. + #[test] + fn encode_14bit_be_round_trips_through_parser_no_crc() { + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_14bit_be(&hdr).unwrap(); + assert_eq!(encoded.len(), 18); + let hdr_round = parse_frame_header_14bit(&encoded).unwrap(); + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::FourteenBitBigEndian + ); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// Same as above with crc_present == true (15-byte raw-BE header + /// re-packed into 18-byte 14-bit container window). + #[test] + fn encode_14bit_be_round_trips_through_parser_with_crc() { + let bytes_in = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_14bit_be(&hdr).unwrap(); + assert_eq!(encoded.len(), 18); + let hdr_round = parse_frame_header_14bit(&encoded).unwrap(); + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::FourteenBitBigEndian + ); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// 14-bit-LE round-trip through `parse_frame_header_14bit`. + #[test] + fn encode_14bit_le_round_trips_through_parser_no_crc() { + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_14bit_le(&hdr).unwrap(); + let hdr_round = parse_frame_header_14bit(&encoded).unwrap(); + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::FourteenBitLittleEndian + ); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// Same with crc_present == true. + #[test] + fn encode_14bit_le_round_trips_through_parser_with_crc() { + let bytes_in = build_be_header(1, 31, 1, 16, 1023, 9, 13, 25, 0, Some(0xCAFE), 0xD2EC); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded = encode_frame_header_14bit_le(&hdr).unwrap(); + let hdr_round = parse_frame_header_14bit(&encoded).unwrap(); + assert_eq!( + hdr_round.sync_word_encoding, + SyncWordEncoding::FourteenBitLittleEndian + ); + let mut hdr_norm = hdr_round; + hdr_norm.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm, hdr); + } + + /// Bound-validation inheritance: NBLKS<5 from the BE encoder also + /// rejects the 14-bit-BE wrapper. + #[test] + fn encode_14bit_be_rejects_nblks_below_5() { + let hdr = synth_hdr(|h| h.blocks_per_frame = 4); + let err = encode_frame_header_14bit_be(&hdr).unwrap_err(); + assert_eq!(err, Error::BlockCountOutOfRange { blocks: 4 }); + } + + /// Bound-validation inheritance: FSIZE above 16384 also rejects + /// the 14-bit-LE wrapper. + #[test] + fn encode_14bit_le_rejects_frame_size_above_16384() { + let hdr = synth_hdr(|h| h.frame_size_bytes = 16385); + let err = encode_frame_header_14bit_le(&hdr).unwrap_err(); + assert_eq!(err, Error::FrameSizeOutOfRange { frame_size: 16385 }); + } + + /// Bound-validation inheritance: the crc-present-without-payload + /// mismatch is also rejected by the 14-bit-BE wrapper. + #[test] + fn encode_14bit_be_rejects_crc_payload_mismatch() { + let hdr = synth_hdr(|h| { + h.crc_present = false; + h.header_crc = Some(0xBEEF); + }); + let err = encode_frame_header_14bit_be(&hdr).unwrap_err(); + match err { + Error::FieldOutOfRange { field, .. } => assert_eq!(field, "header_crc"), + other => panic!("expected FieldOutOfRange{{field: header_crc}}, got {other:?}"), + } + } + + /// Exhaustive grid: every documented LFE code × both CRC states × + /// representative {NBLKS, FSIZE} pairs round-trip through each of + /// the 14-bit encoders. + #[test] + fn encode_14bit_round_trip_grid_lfe_crc_states() { + for crc_state in [false, true] { + for lfe_code in 0u8..=3 { + for &(nblks, fsize) in &[(5u8, 95u16), (16u8, 1024u16), (127u8, 16384u16)] { + let crc_arg = if crc_state { Some(0xBEEF) } else { None }; + let bytes = build_be_header( + 1, + 31, + crc_state as u32, + nblks as u32, + (fsize - 1) as u32, + 9, + 13, + 25, + ((lfe_code as u32) & 0b11) << 1, + crc_arg, + 0, + ); + let hdr = parse_frame_header(&bytes).unwrap(); + + // BE variant. + let encoded_be = encode_frame_header_14bit_be(&hdr).unwrap(); + assert_eq!(encoded_be.len(), 18); + assert_eq!(&encoded_be[..4], &[0x1F, 0xFF, 0xE8, 0x00]); + let hdr_round_be = parse_frame_header_14bit(&encoded_be).unwrap(); + assert_eq!( + hdr_round_be.sync_word_encoding, + SyncWordEncoding::FourteenBitBigEndian + ); + let mut hdr_norm_be = hdr_round_be; + hdr_norm_be.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm_be, hdr); + + // LE variant. + let encoded_le = encode_frame_header_14bit_le(&hdr).unwrap(); + assert_eq!(encoded_le.len(), 18); + assert_eq!(&encoded_le[..4], &[0xFF, 0x1F, 0x00, 0xE8]); + let hdr_round_le = parse_frame_header_14bit(&encoded_le).unwrap(); + assert_eq!( + hdr_round_le.sync_word_encoding, + SyncWordEncoding::FourteenBitLittleEndian + ); + let mut hdr_norm_le = hdr_round_le; + hdr_norm_le.sync_word_encoding = hdr.sync_word_encoding; + assert_eq!(hdr_norm_le, hdr); + + // Cross-check: BE and LE outputs are pairwise byte- + // swaps of each other. + let mut swapped = encoded_be.clone(); + for pair in swapped.chunks_exact_mut(2) { + pair.swap(0, 1); + } + assert_eq!(encoded_le, swapped); + } + } + } + } + + /// Cross-reference with the existing `unpack_14bit_to_16bit` round- + /// trip: unpacking the 14-bit-BE encoder output and reading the + /// first `header_byte_length()` bytes equals the BE encoder output + /// (padded to the multiple-of-8 boundary the unpacker emits). + #[test] + fn encode_14bit_be_unpacks_back_to_raw_be_header_prefix() { + use crate::FourteenBitByteOrder; + let bytes_in = build_be_header(1, 31, 0, 16, 1023, 9, 13, 25, 0, None, 0); + let hdr = parse_frame_header(&bytes_in).unwrap(); + let encoded_14 = encode_frame_header_14bit_be(&hdr).unwrap(); + let unpacked = + crate::unpack_14bit_to_16bit(&encoded_14, FourteenBitByteOrder::BigEndian).unwrap(); + let raw_be = encode_frame_header_be(&hdr).unwrap(); + // Unpacked stream starts with the canonical raw-BE sync and the + // first 13 bytes equal the BE encoder output (since BE encoder + // emits exactly 13 bytes for crc_present == false). + assert_eq!(&unpacked[..raw_be.len()], &raw_be[..]); + assert_eq!(&unpacked[..4], &[0x7F, 0xFE, 0x80, 0x01]); + } + + // --------------------------------------------------------------- + // Round 202 — SFREQ / AMODE / PCMR resolvers + // (ETSI §5.3.1 Tables 5-5 / 5-4 / 5-17). + // --------------------------------------------------------------- + + #[test] + fn sample_frequency_from_index_covers_table_5_5_verbatim() { + // Per ETSI TS 102 114 §5.3.1 Table 5-5: nine valid rows, seven + // invalid rows. Each row checked individually so any future + // table edit fails this test loudly. + assert_eq!( + sample_frequency_from_index(0b0000), + SampleFrequency::Invalid + ); + assert_eq!( + sample_frequency_from_index(0b0001), + SampleFrequency::Fixed(8_000) + ); + assert_eq!( + sample_frequency_from_index(0b0010), + SampleFrequency::Fixed(16_000) + ); + assert_eq!( + sample_frequency_from_index(0b0011), + SampleFrequency::Fixed(32_000) + ); + assert_eq!( + sample_frequency_from_index(0b0100), + SampleFrequency::Invalid + ); + assert_eq!( + sample_frequency_from_index(0b0101), + SampleFrequency::Invalid + ); + assert_eq!( + sample_frequency_from_index(0b0110), + SampleFrequency::Fixed(11_025) + ); + assert_eq!( + sample_frequency_from_index(0b0111), + SampleFrequency::Fixed(22_050) + ); + assert_eq!( + sample_frequency_from_index(0b1000), + SampleFrequency::Fixed(44_100) + ); + assert_eq!( + sample_frequency_from_index(0b1001), + SampleFrequency::Invalid + ); + assert_eq!( + sample_frequency_from_index(0b1010), + SampleFrequency::Invalid + ); + assert_eq!( + sample_frequency_from_index(0b1011), + SampleFrequency::Fixed(12_000) + ); + assert_eq!( + sample_frequency_from_index(0b1100), + SampleFrequency::Fixed(24_000) + ); + assert_eq!( + sample_frequency_from_index(0b1101), + SampleFrequency::Fixed(48_000) + ); + assert_eq!( + sample_frequency_from_index(0b1110), + SampleFrequency::Invalid + ); + assert_eq!( + sample_frequency_from_index(0b1111), + SampleFrequency::Invalid + ); + } + + #[test] + fn sample_frequency_table_has_exactly_nine_valid_codes() { + let valid: usize = (0u8..16) + .filter(|&code| matches!(sample_frequency_from_index(code), SampleFrequency::Fixed(_))) + .count(); + // Table 5-5 enumerates nine valid rows: 8/16/32/11.025/22.05/44.1/12/24/48 kHz. + assert_eq!(valid, 9); + } + + #[test] + fn sample_rate_hz_returns_some_for_valid_and_none_for_invalid() { + // Build a synthetic header for each of the sixteen SFREQ codes + // and verify sample_rate_hz() round-trips through the parser. + for code in 0..16u32 { + let header_bytes = build_be_header( + /* ftype */ 1, /* sample_count_m1 */ 31, /* crc_present */ 0, + /* nblks */ 15, /* fsize_m1 */ 1023, /* amode */ 2, + /* sfreq */ code, /* rate */ 15, /* extra_bits */ 0, + /* header_crc */ None, /* post_crc */ 0, + ); + let hdr = parse_frame_header(&header_bytes).unwrap_or_else(|e| { + panic!("parse_frame_header failed for sfreq={code:04b}: {e:?}") + }); + assert_eq!(hdr.sfreq_index, code as u8); + match sample_frequency_from_index(code as u8) { + SampleFrequency::Fixed(hz) => { + assert_eq!(hdr.sample_rate_hz(), Some(hz)); + assert_eq!(hdr.sample_frequency(), SampleFrequency::Fixed(hz)); + } + SampleFrequency::Invalid => { + assert_eq!(hdr.sample_rate_hz(), None); + assert_eq!(hdr.sample_frequency(), SampleFrequency::Invalid); + } + } + } + } + + #[test] + fn amode_arrangement_from_index_covers_table_5_4_verbatim() { + // Per ETSI TS 102 114 §5.3.1 Table 5-4: sixteen standard + // arrangements at codes 0..=15, plus a user-defined band at + // codes 16..=63. + assert_eq!(amode_arrangement_from_index(0), AmodeArrangement::Mono); + assert_eq!(amode_arrangement_from_index(1), AmodeArrangement::DualMono); + assert_eq!(amode_arrangement_from_index(2), AmodeArrangement::Stereo); + assert_eq!( + amode_arrangement_from_index(3), + AmodeArrangement::SumDifference + ); + assert_eq!(amode_arrangement_from_index(4), AmodeArrangement::LtRt); + assert_eq!(amode_arrangement_from_index(5), AmodeArrangement::ClR); + assert_eq!(amode_arrangement_from_index(6), AmodeArrangement::LrS); + assert_eq!(amode_arrangement_from_index(7), AmodeArrangement::ClRS); + assert_eq!(amode_arrangement_from_index(8), AmodeArrangement::LrSlSr); + assert_eq!(amode_arrangement_from_index(9), AmodeArrangement::ClRSlSr); + assert_eq!( + amode_arrangement_from_index(10), + AmodeArrangement::ClCrLRSlSr + ); + assert_eq!( + amode_arrangement_from_index(11), + AmodeArrangement::ClRLrRrOv + ); + assert_eq!( + amode_arrangement_from_index(12), + AmodeArrangement::CfCrLfRfLrRr + ); + assert_eq!( + amode_arrangement_from_index(13), + AmodeArrangement::ClCCrLRSlSr + ); + assert_eq!( + amode_arrangement_from_index(14), + AmodeArrangement::ClCrLRSl1Sl2Sr1Sr2 + ); + assert_eq!( + amode_arrangement_from_index(15), + AmodeArrangement::ClCCrLRSlSSr + ); + // Codes 16..=63 are user-defined; spot-check a few. + for code in [16u8, 17, 31, 32, 47, 62, 63] { + assert_eq!( + amode_arrangement_from_index(code), + AmodeArrangement::UserDefined(code), + "user-defined code {code} must round-trip" + ); + } + } + + #[test] + fn front_lr_channels_follow_table_5_4_ordering() { + use AmodeArrangement::*; + // L, R lead the arrangement → (0, 1). + for a in [Stereo, SumDifference, LtRt, LrS, LrSlSr] { + assert_eq!(a.front_lr_channels(), Some((0, 1)), "{a:?}"); + } + // A leading centre offsets L, R by one → (1, 2). + for a in [ClR, ClRS, ClRSlSr] { + assert_eq!(a.front_lr_channels(), Some((1, 2)), "{a:?}"); + } + // CL + CR lead → L, R at (2, 3). + assert_eq!(ClCrLRSlSr.front_lr_channels(), Some((2, 3))); + assert_eq!(ClCrLRSl1Sl2Sr1Sr2.front_lr_channels(), Some((2, 3))); + // CL + C + CR lead → L, R at (3, 4). + assert_eq!(ClCCrLRSlSr.front_lr_channels(), Some((3, 4))); + assert_eq!(ClCCrLRSlSSr.front_lr_channels(), Some((3, 4))); + // No distinct front L/R pair. + for a in [Mono, DualMono, ClRLrRrOv, CfCrLfRfLrRr, UserDefined(20)] { + assert_eq!(a.front_lr_channels(), None, "{a:?}"); + } + } + + #[test] + fn surround_lr_channels_follow_table_5_4_ordering() { + use AmodeArrangement::*; + assert_eq!(LrSlSr.surround_lr_channels(), Some((2, 3))); + assert_eq!(ClRSlSr.surround_lr_channels(), Some((3, 4))); + assert_eq!(ClCrLRSlSr.surround_lr_channels(), Some((4, 5))); + assert_eq!(ClCCrLRSlSr.surround_lr_channels(), Some((5, 6))); + // Arrangements with no distinct surround L/R pair. + for a in [ + Mono, + DualMono, + Stereo, + SumDifference, + LtRt, + ClR, + LrS, + ClRS, + ClRLrRrOv, + CfCrLfRfLrRr, + ClCrLRSl1Sl2Sr1Sr2, + ClCCrLRSlSSr, + UserDefined(33), + ] { + assert_eq!(a.surround_lr_channels(), None, "{a:?}"); + } + } + + #[test] + fn amode_channel_count_matches_table_5_4_chs_column() { + // Per Table 5-4 CHS column (in code order 0..=15): + // 1,2,2,2,2,3,3,4,4,5,6,6,6,7,8,8. + let expected: [u8; 16] = [1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, 7, 8, 8]; + for (code, exp) in expected.iter().enumerate() { + assert_eq!( + amode_arrangement_from_index(code as u8).channel_count(), + Some(*exp), + "channel_count for AMODE={code:02} (binary={code:06b})" + ); + } + // User-defined codes have no fixed CHS in the spec. + for code in [16u8, 32, 63] { + assert_eq!( + amode_arrangement_from_index(code).channel_count(), + None, + "user-defined AMODE={code} must report None CHS" + ); + } + } + + #[test] + fn channel_count_returns_some_for_standard_and_none_for_user_defined() { + // Walk all 64 possible AMODE codes through the parser and + // check the channel_count() round-trip. + for code in 0..64u32 { + let header_bytes = build_be_header( + /* ftype */ 1, /* sample_count_m1 */ 31, /* crc_present */ 0, + /* nblks */ 15, /* fsize_m1 */ 1023, /* amode */ code, + /* sfreq */ 13, /* rate */ 15, /* extra_bits */ 0, + /* header_crc */ None, /* post_crc */ 0, + ); + let hdr = parse_frame_header(&header_bytes) + .unwrap_or_else(|e| panic!("parse_frame_header failed for amode={code}: {e:?}")); + assert_eq!(hdr.amode, code as u8); + assert_eq!( + hdr.amode_arrangement(), + amode_arrangement_from_index(code as u8) + ); + let exp = amode_arrangement_from_index(code as u8).channel_count(); + assert_eq!(hdr.channel_count(), exp, "channel_count for amode={code}"); + } + } + + #[test] + fn source_pcm_resolution_from_index_covers_table_5_17_verbatim() { + // Per ETSI TS 102 114 §5.3.1 Table 5-17: six valid (bits, es) + // pairs at codes {0,1,2,3,5,6}; codes {4,7} are invalid. + assert_eq!( + source_pcm_resolution_from_index(0b000), + SourcePcmResolution::Valid { + bits: 16, + es: false + } + ); + assert_eq!( + source_pcm_resolution_from_index(0b001), + SourcePcmResolution::Valid { bits: 16, es: true } + ); + assert_eq!( + source_pcm_resolution_from_index(0b010), + SourcePcmResolution::Valid { + bits: 20, + es: false + } + ); + assert_eq!( + source_pcm_resolution_from_index(0b011), + SourcePcmResolution::Valid { bits: 20, es: true } + ); + assert_eq!( + source_pcm_resolution_from_index(0b100), + SourcePcmResolution::Invalid + ); + assert_eq!( + source_pcm_resolution_from_index(0b101), + SourcePcmResolution::Valid { bits: 24, es: true } + ); + assert_eq!( + source_pcm_resolution_from_index(0b110), + SourcePcmResolution::Valid { + bits: 24, + es: false + } + ); + assert_eq!( + source_pcm_resolution_from_index(0b111), + SourcePcmResolution::Invalid + ); + } + + #[test] + fn source_pcm_bits_per_sample_returns_some_for_valid_and_none_for_invalid() { + // The PCMR field lives 6 bits deep in the 16-bit post-CRC + // window (the wiki layout: MSB→LSB is + // multirate_inter[1] | version[4] | copy_history[2] | + // PCMR[3] | front_sum[1] | surround_sum[1] | dialnorm[4]). + // PCMR therefore occupies bits 6..=8 (1-indexed from LSB: + // positions 8..=6 from MSB). + for code in 0..8u32 { + let post_crc = code << 6; // place PCMR at bits 8..=6 (MSB→LSB). + let header_bytes = build_be_header( + /* ftype */ 1, /* sample_count_m1 */ 31, /* crc_present */ 0, + /* nblks */ 15, /* fsize_m1 */ 1023, /* amode */ 2, + /* sfreq */ 13, /* rate */ 15, /* extra_bits */ 0, + /* header_crc */ None, /* post_crc */ post_crc, + ); + let hdr = parse_frame_header(&header_bytes) + .unwrap_or_else(|e| panic!("parse_frame_header failed for pcmr={code}: {e:?}")); + assert_eq!(hdr.source_pcm_resolution_index, code as u8); + assert_eq!( + hdr.source_pcm_resolution(), + source_pcm_resolution_from_index(code as u8) + ); + match source_pcm_resolution_from_index(code as u8) { + SourcePcmResolution::Valid { bits, .. } => { + assert_eq!(hdr.source_pcm_bits_per_sample(), Some(bits)); + } + SourcePcmResolution::Invalid => { + assert_eq!(hdr.source_pcm_bits_per_sample(), None); + } + } + } + } + + #[test] + fn ffmpeg_fixture_resolves_to_48k_stereo_16bit() { + // The bundled ffmpeg-encoded fixture in tests/black_box_ffmpeg.rs + // has sfreq_index=13 (48 kHz), amode=2 (stereo), and + // source_pcm_resolution_index=0 (16-bit, ES=0). Mirror the + // resolution path here so the unit-test layer fails as loudly + // as the integration-test layer would. + let header_bytes = build_be_header( + /* ftype */ 1, /* sample_count_m1 */ 31, /* crc_present */ 0, + /* nblks */ 15, /* fsize_m1 */ 1023, + /* amode */ 2, // Stereo (L+R). + /* sfreq */ 13, // 48 kHz. + /* rate */ 15, // 768 kb/s. + /* extra_bits */ 0, /* header_crc */ None, + /* post_crc */ 0, // PCMR=0 → 16-bit, ES=0. + ); + let hdr = parse_frame_header(&header_bytes).unwrap(); + assert_eq!(hdr.sample_rate_hz(), Some(48_000)); + assert_eq!(hdr.channel_count(), Some(2)); + assert_eq!(hdr.amode_arrangement(), AmodeArrangement::Stereo); + assert_eq!(hdr.source_pcm_bits_per_sample(), Some(16)); + assert_eq!( + hdr.source_pcm_resolution(), + SourcePcmResolution::Valid { + bits: 16, + es: false + } + ); + } + + // --------------------------------------------------------------- + // Round 335 — §C.2.5 QMF driver wiring: filter_bank_selection() + // (FILTS / MULTIRATE_INTER polarity per §5.3.1 Table 5-15, + // dts-qmf-driver.md §1) and output_r_scale() (post-filterbank + // rScale derivation per dts-qmf-driver.md §2). + // --------------------------------------------------------------- + + /// `multirate_inter == false` (`FILTS == 0`) resolves to the + /// Non-Perfect Reconstruction §D.8 set per §5.3.1 Table 5-15. + #[test] + fn filter_bank_selection_false_is_non_perfect_reconstruction() { + let h = synth_hdr(|h| h.multirate_inter = false); + assert_eq!( + h.filter_bank_selection(), + FilterBankSelection::NonPerfectReconstruction + ); + // It picks the lossy §D.8 column. + assert!(core::ptr::eq( + h.filter_bank_selection().coefficients(), + &crate::fir_coeff::RA_COEFF_LOSSY, + )); + } + + /// `multirate_inter == true` (`FILTS == 1`) resolves to the + /// Perfect Reconstruction §D.8 set per §5.3.1 Table 5-15. + #[test] + fn filter_bank_selection_true_is_perfect_reconstruction() { + let h = synth_hdr(|h| h.multirate_inter = true); + assert_eq!( + h.filter_bank_selection(), + FilterBankSelection::PerfectReconstruction + ); + assert!(core::ptr::eq( + h.filter_bank_selection().coefficients(), + &crate::fir_coeff::RA_COEFF_LOSSLESS, + )); + } + + /// `filter_bank_selection()` is exactly the bit-for-bit bridge the + /// driver doc §1 establishes: equivalent to + /// `from_filts(u8::from(multirate_inter))` for both polarities, + /// and round-trips through the canonical `filts()` value. + #[test] + fn filter_bank_selection_is_the_from_filts_bridge() { + for bit in [false, true] { + let h = synth_hdr(|h| h.multirate_inter = bit); + assert_eq!( + h.filter_bank_selection(), + FilterBankSelection::from_filts(u8::from(bit)) + ); + // The selection's canonical FILTS value mirrors the bit. + assert_eq!(h.filter_bank_selection().filts(), u8::from(bit)); + } + } + + /// `output_r_scale()` returns `2^(bits-1)` for each valid PCMR + /// resolution per the dts-qmf-driver.md §2 derivation, and `None` + /// for the two reserved/invalid PCMR codes. + #[test] + fn output_r_scale_is_full_scale_for_each_valid_pcmr() { + // Drive every PCMR code 0..=7 and check the rScale derivation + // tracks source_pcm_bits_per_sample(). + for code in 0..8u8 { + let h = synth_hdr(|h| h.source_pcm_resolution_index = code); + match h.source_pcm_bits_per_sample() { + Some(16) => assert_eq!(h.output_r_scale(), Some(32768.0)), + Some(20) => assert_eq!(h.output_r_scale(), Some(524_288.0)), + Some(24) => assert_eq!(h.output_r_scale(), Some(8_388_608.0)), + Some(other) => panic!("unexpected PCMR bits {other} for code {code}"), + None => assert_eq!(h.output_r_scale(), None), + } + } + } + + /// The two reserved PCMR codes (`0b100`, `0b111`) yield no rScale, + /// mirroring `source_pcm_bits_per_sample()`'s `None`. + #[test] + fn output_r_scale_is_none_for_reserved_pcmr_codes() { + for code in [0b100u8, 0b111u8] { + let h = synth_hdr(|h| h.source_pcm_resolution_index = code); + assert_eq!(h.source_pcm_bits_per_sample(), None); + assert_eq!(h.output_r_scale(), None); + } + } + + /// End-to-end: a parsed header drives `QmfSynthesis::synthesize` + /// directly through `filter_bank_selection()` + `output_r_scale()` + /// — the §C.2.5 driver's two header-sourced parameters — producing + /// the same PCM as feeding the resolved values manually. + #[test] + fn header_drives_qmf_synthesis_end_to_end() { + use crate::cos_mod::NUM_SUBBAND; + use crate::qmf_synth::QmfSynthesis; + + // PCMR=0 → 16-bit → rScale 32768; multirate_inter=true → + // perfect reconstruction. + let h = synth_hdr(|h| { + h.multirate_inter = true; + h.source_pcm_resolution_index = 0; + }); + let filter = h.filter_bank_selection(); + let r_scale = h.output_r_scale().expect("valid PCMR yields rScale"); + assert_eq!(filter, FilterBankSelection::PerfectReconstruction); + assert_eq!(r_scale, 32768.0); + + // A large impulse across several rows so the §D.8 perfect- + // reconstruction FIR tail (its leading taps are ~1e-10) + // truncates to non-zero integer PCM at the 16-bit gain. + let mut row0 = [0.0_f64; NUM_SUBBAND]; + row0[0] = 1.0e6; + let mut rows = vec![[0.0_f64; NUM_SUBBAND]; 16]; + rows[0] = row0; + + let mut via_header = QmfSynthesis::new(); + let mut hdr_out = Vec::new(); + via_header + .synthesize(&rows, 4, filter, r_scale, &mut hdr_out) + .unwrap(); + + // Manually with the values the driver doc resolves them to. + let mut manual = QmfSynthesis::new(); + let mut man_out = Vec::new(); + manual + .synthesize( + &rows, + 4, + FilterBankSelection::PerfectReconstruction, + 32768.0, + &mut man_out, + ) + .unwrap(); + + assert_eq!(hdr_out, man_out); + assert!(hdr_out.iter().any(|&s| s != 0)); + } + + // --------------------------------------------------------------- + // Round 337 — MultiChannelQmf header convenience: the per-frame + // multi-channel driver sources its two frame-wide §C.2.5 parameters + // (FILTS via filter_bank_selection(), output rScale via + // output_r_scale()) directly from a parsed header. + // --------------------------------------------------------------- + + /// `MultiChannelQmf::synthesize_planar_from_header` sources `FILTS` + /// and `rScale` from a parsed header and matches a manual planar + /// call with the resolved values. + #[test] + fn multichannel_from_header_matches_manual_resolution() { + use crate::cos_mod::NUM_SUBBAND; + use crate::qmf_multichannel::MultiChannelQmf; + + // multirate_inter=true → perfect; PCMR=0 → 16-bit → rScale 32768. + let h = synth_hdr(|h| { + h.multirate_inter = true; + h.source_pcm_resolution_index = 0; + }); + + let rows: Vec<[f64; NUM_SUBBAND]> = (0..6) + .map(|s| { + let mut r = [0.0; NUM_SUBBAND]; + r[0] = (s as f64 + 1.0) * 1e6; + r + }) + .collect(); + let refs: Vec<&[[f64; NUM_SUBBAND]]> = vec![rows.as_slice()]; + let n_subs = [3usize]; + + let mut mc_h = MultiChannelQmf::new(1); + let mut via_header = vec![Vec::new()]; + let outcome = mc_h + .synthesize_planar_from_header(&h, &refs, &n_subs, &mut via_header) + .unwrap(); + assert_eq!(outcome, Some(())); + + let mut mc_m = MultiChannelQmf::new(1); + let mut manual = vec![Vec::new()]; + mc_m.synthesize_planar( + &refs, + &n_subs, + FilterBankSelection::PerfectReconstruction, + 32768.0, + &mut manual, + ) + .unwrap(); + + assert_eq!(via_header, manual); + assert!(via_header[0].iter().any(|&s| s != 0)); + } + + /// A reserved PCMR code yields `Ok(None)` from the multi-channel + /// header convenience (no full-scale gain defined) and leaves the + /// output untouched. + #[test] + fn multichannel_from_header_reserved_pcmr_yields_none() { + use crate::cos_mod::NUM_SUBBAND; + use crate::qmf_multichannel::MultiChannelQmf; + + let h = synth_hdr(|h| h.source_pcm_resolution_index = 0b100); // reserved + + let rows = vec![[0.0_f64; NUM_SUBBAND]; 2]; + let refs: Vec<&[[f64; NUM_SUBBAND]]> = vec![rows.as_slice()]; + let n_subs = [4usize]; + + let mut mc = MultiChannelQmf::new(1); + let mut out = vec![Vec::new()]; + let outcome = mc + .synthesize_planar_from_header(&h, &refs, &n_subs, &mut out) + .unwrap(); + assert_eq!(outcome, None); + assert!(out[0].is_empty()); + } +} diff --git a/crates/vendor/oxideav-dts/src/inverse_adpcm.rs b/crates/vendor/oxideav-dts/src/inverse_adpcm.rs new file mode 100644 index 00000000..454b6c70 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/inverse_adpcm.rs @@ -0,0 +1,715 @@ +//! DTS Coherent Acoustics — §C.2.2 Inverse ADPCM. +//! +//! Round 228 (2026-06-04) lands the §C.2.2 inverse-ADPCM predictor, +//! the per-sample reconstruction step that turns a subband's +//! residual (error) sample stream into the reconstructed subband +//! sample stream when the subband's `PMODE == 1` flag indicates +//! ADPCM prediction is active. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), Annex C (informative) +//! §C.2.2 "Inverse ADPCM" (staged PDF p.183) at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. The +//! spec's normative pseudocode reads (reproduced as documented): +//! +//! ```text +//! void InverseADPCM(void) { +//! // NumADPCMCoeff =4, the number of ADPCM coefficients. +//! // raADPCMCoeff[] are the ADPCM coefficients extracted +//! // from the bit stream. +//! // raSample[NumADPCMCoeff], ..., raSample[-1] are the +//! // history from last subframe or subsubframe. It must +//! // updated each time before reverse ADPCM is run for a +//! // block of samples for each subband. +//! for (m=0; m Result<()> { + if history.len() != NUM_ADPCM_COEFF { + return Err(Error::InverseAdpcmShapeMismatch { + history_len: history.len(), + coeffs_len: coeffs.len(), + }); + } + if coeffs.len() != NUM_ADPCM_COEFF { + return Err(Error::InverseAdpcmShapeMismatch { + history_len: history.len(), + coeffs_len: coeffs.len(), + }); + } + // The spec's `raSample[]` is a logical array indexed from -4 to + // nNumSample-1. We materialise the negative slice as the + // `history` argument; the predictor reads + // `raSample[m - n - 1]` for n in [0, 4): + // n=0 -> raSample[m-1] + // n=1 -> raSample[m-2] + // n=2 -> raSample[m-3] + // n=3 -> raSample[m-4] + // For m=0 these all fall into the negative-index history: + // raSample[-1] = history[3], raSample[-2] = history[2], + // raSample[-3] = history[1], raSample[-4] = history[0]. + // For m=1 the n=0 slot is the just-reconstructed samples[0], + // and the n=1..3 slots are history[3], history[2], history[1]. + // The fully-loaded "no history needed" regime begins at m=4. + for m in 0..samples.len() { + let mut acc = samples[m]; + for n in 0..NUM_ADPCM_COEFF { + // raSample[m - n - 1] + let past = if (n + 1) <= m { + samples[m - n - 1] + } else { + // We need raSample at logical index m - n - 1 which + // is in the negative range. The most-recent history + // slot (history[3] == raSample[-1]) is fetched when + // m=0, n=0. Generally: + // logical = m - n - 1 (negative for n+1 > m) + // history slot = NUM_ADPCM_COEFF + logical + // = NUM_ADPCM_COEFF + m - n - 1 + history[NUM_ADPCM_COEFF + m - n - 1] + }; + acc = acc.wrapping_add(coeffs[n].wrapping_mul(past)); + } + samples[m] = acc; + } + Ok(()) +} + +/// Apply the §C.2.2 inverse-ADPCM predictor to a single subband's +/// residual sample stream in place, in floating-point (f64) +/// arithmetic. Same predictor structure as +/// [`inverse_adpcm_decode_i32`]; chosen by callers that consume the +/// reconstructed subband samples in floating-point. +/// +/// # Errors +/// +/// Returns [`Error::InverseAdpcmShapeMismatch`] under the same +/// conditions as [`inverse_adpcm_decode_i32`]. +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::{inverse_adpcm_decode_f64, NUM_ADPCM_COEFF}; +/// +/// let history = [0.0_f64; NUM_ADPCM_COEFF]; +/// let coeffs = [0.0_f64; NUM_ADPCM_COEFF]; +/// let mut samples = [1.5_f64, 2.5, -0.5]; +/// inverse_adpcm_decode_f64(&history, &coeffs, &mut samples).unwrap(); +/// assert_eq!(samples, [1.5, 2.5, -0.5]); +/// ``` +pub fn inverse_adpcm_decode_f64( + history: &[f64], + coeffs: &[f64], + samples: &mut [f64], +) -> Result<()> { + if history.len() != NUM_ADPCM_COEFF || coeffs.len() != NUM_ADPCM_COEFF { + return Err(Error::InverseAdpcmShapeMismatch { + history_len: history.len(), + coeffs_len: coeffs.len(), + }); + } + for m in 0..samples.len() { + let mut acc = samples[m]; + for n in 0..NUM_ADPCM_COEFF { + let past = if (n + 1) <= m { + samples[m - n - 1] + } else { + history[NUM_ADPCM_COEFF + m - n - 1] + }; + acc += coeffs[n] * past; + } + samples[m] = acc; + } + Ok(()) +} + +/// Update the rolling four-sample history buffer with the +/// last [`NUM_ADPCM_COEFF`] reconstructed samples of a decode block, +/// so the next block can pick up where this one left off. +/// +/// After [`inverse_adpcm_decode_i32`] has reconstructed a block, the +/// caller passes the just-reconstructed `samples` slice together +/// with the existing `history` buffer; this function slides the +/// last four reconstructed samples into `history`, preserving the +/// spec's `history[0] == raSample[-4]`, `history[3] == raSample[-1]` +/// ordering for the next call. +/// +/// If `samples.len() >= NUM_ADPCM_COEFF`, the four-sample tail of +/// `samples` becomes the new history. If `samples.len() < +/// NUM_ADPCM_COEFF`, the existing history is shifted left by +/// `samples.len()` slots and the residual `samples` are appended +/// (the predictor's short-block recovery mode). +/// +/// This is a convenience helper; the spec only states the +/// invariant ("It must updated each time before reverse ADPCM is +/// run for a block of samples for each subband") and the +/// implementation strategy is determined by the rolling history +/// semantics described above. +pub fn update_history_i32(history: &mut [i32], samples: &[i32]) { + debug_assert_eq!(history.len(), NUM_ADPCM_COEFF); + if samples.len() >= NUM_ADPCM_COEFF { + let tail = &samples[samples.len() - NUM_ADPCM_COEFF..]; + history.copy_from_slice(tail); + } else { + // Short block: shift history left by samples.len() and + // append the residual. + let shift = samples.len(); + history.copy_within(shift.., 0); + history[NUM_ADPCM_COEFF - shift..].copy_from_slice(samples); + } +} + +/// Floating-point counterpart to [`update_history_i32`]. +pub fn update_history_f64(history: &mut [f64], samples: &[f64]) { + debug_assert_eq!(history.len(), NUM_ADPCM_COEFF); + if samples.len() >= NUM_ADPCM_COEFF { + let tail = &samples[samples.len() - NUM_ADPCM_COEFF..]; + history.copy_from_slice(tail); + } else { + let shift = samples.len(); + history.copy_within(shift.., 0); + history[NUM_ADPCM_COEFF - shift..].copy_from_slice(samples); + } +} + +/// Returns `true` if the §C.2.2 inverse-ADPCM predictor must be +/// applied to a subband given its `PMODE` flag. +/// +/// The spec's gating sentence reads: "Inverse ADPCM process is +/// executed for each sample in a subband whose `PMODE == 1`." +/// A `PMODE == 0` subband bypasses the predictor entirely; the +/// dequantised residual *is* the reconstructed subband sample. +pub fn inverse_adpcm_required(pmode: u8) -> bool { + pmode == 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + // ---------------------------------------------------------------- + // i32 single-block decode — predictor property tests + // ---------------------------------------------------------------- + + #[test] + fn zero_coeffs_make_predictor_identity() { + // All coefficients zero: the residuals pass through + // unchanged regardless of history. + let history = [1i32, 2, 3, 4]; + let coeffs = [0i32; NUM_ADPCM_COEFF]; + let mut samples = [10i32, 20, 30, 40, 50]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples, [10, 20, 30, 40, 50]); + } + + #[test] + fn zero_history_with_only_first_coeff_runs_off_residuals() { + // History = 0, coeffs = (1, 0, 0, 0): each output equals + // residual + previous_output. With residuals (1, 0, 0, 0) + // the predictor produces (1, 1, 1, 1) — a step. + let history = [0i32; NUM_ADPCM_COEFF]; + let coeffs = [1i32, 0, 0, 0]; + let mut samples = [1i32, 0, 0, 0]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples, [1, 1, 1, 1]); + } + + #[test] + fn coeff0_with_priming_history_seeds_first_output() { + // For m = 0 the only history slot reached is raSample[-1] = + // history[3]. With coeffs = (c, 0, 0, 0) and a zero residual + // we should see samples[0] = c * history[3]. + let history = [0i32, 0, 0, 7]; + let coeffs = [3i32, 0, 0, 0]; + let mut samples = [0i32]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples[0], 21); + } + + #[test] + fn each_coeff_taps_the_right_history_slot() { + // For m = 0 with the four coefficients (1, 10, 100, 1000) + // and history = (a, b, c, d) = (1, 2, 3, 4) we expect + // samples[0] = 0 + // + 1 * raSample[-1] = 1 * d = 4 + // + 10 * raSample[-2] = 10 * c = 30 + // + 100 * raSample[-3] = 100 * b = 200 + // + 1000* raSample[-4] = 1000 * a = 1000 + // = 1234 + let history = [1i32, 2, 3, 4]; + let coeffs = [1i32, 10, 100, 1000]; + let mut samples = [0i32]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples[0], 1234); + } + + #[test] + fn negative_indices_use_history_until_m_is_four() { + // For m=0..3 the predictor needs the negative slice of + // history; for m=4 onwards no history is consulted (the + // four preceding samples are all inside `samples`). + // This test confirms the per-m boundary by zeroing the + // history and observing that samples[4] = 0 + Σ c[n] * 0 = 0 + // when c is any vector and the leading samples are zero. + let history = [0i32; NUM_ADPCM_COEFF]; + let coeffs = [7i32, 11, 13, 17]; + let mut samples = [0i32, 0, 0, 0, 0]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples, [0; 5]); + } + + #[test] + fn predictor_uses_just_reconstructed_sample_immediately() { + // m = 0: samples[0] = 1 + c * history[3] = 1 + 2*0 = 1. + // m = 1: samples[1] = 0 + c * samples[0] = 0 + 2*1 = 2. + // m = 2: samples[2] = 0 + c * samples[1] = 0 + 2*2 = 4. + // m = 3: samples[3] = 0 + c * samples[2] = 0 + 2*4 = 8. + // Verifies that the predictor sees the freshly-written + // samples[m] as the n=0 history of step m+1. + let history = [0i32; NUM_ADPCM_COEFF]; + let coeffs = [2i32, 0, 0, 0]; + let mut samples = [1i32, 0, 0, 0]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples, [1, 2, 4, 8]); + } + + #[test] + fn empty_block_is_no_op() { + let history = [1i32, 2, 3, 4]; + let coeffs = [5i32, 6, 7, 8]; + let mut samples: [i32; 0] = []; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + // History unchanged (we don't update it in the predictor itself). + assert_eq!(history, [1, 2, 3, 4]); + } + + #[test] + fn wrapping_arithmetic_at_i32_max() { + // The accumulator must wrap, not panic. Use coeffs + // (i32::MAX, 0, 0, 0) and history (.., .., .., 2) so + // c[0] * history[3] = i32::MAX * 2 = -2 (wrapping); plus a + // residual of 1 gives -1. + let history = [0i32, 0, 0, 2]; + let coeffs = [i32::MAX, 0, 0, 0]; + let mut samples = [1i32]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + let expected = 1i32.wrapping_add(i32::MAX.wrapping_mul(2)); + assert_eq!(samples[0], expected); + } + + #[test] + fn wrapping_arithmetic_at_i32_min_times_negative_one() { + // i32::MIN * -1 overflows; wrapping_mul yields i32::MIN. + let history = [0i32, 0, 0, i32::MIN]; + let coeffs = [-1i32, 0, 0, 0]; + let mut samples = [0i32]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples[0], i32::MIN); + } + + #[test] + fn history_length_mismatch_short() { + let short_history = [0i32; 3]; + let coeffs = [0i32; NUM_ADPCM_COEFF]; + let mut samples = [0i32; 4]; + let err = inverse_adpcm_decode_i32(&short_history, &coeffs, &mut samples).unwrap_err(); + assert!(matches!( + err, + Error::InverseAdpcmShapeMismatch { + history_len: 3, + coeffs_len: 4, + } + )); + } + + #[test] + fn history_length_mismatch_long() { + let long_history = [0i32; 5]; + let coeffs = [0i32; NUM_ADPCM_COEFF]; + let mut samples = [0i32; 4]; + let err = inverse_adpcm_decode_i32(&long_history, &coeffs, &mut samples).unwrap_err(); + assert!(matches!( + err, + Error::InverseAdpcmShapeMismatch { + history_len: 5, + coeffs_len: 4, + } + )); + } + + #[test] + fn coeffs_length_mismatch_short() { + let history = [0i32; NUM_ADPCM_COEFF]; + let short_coeffs = [0i32; 3]; + let mut samples = [0i32; 4]; + let err = inverse_adpcm_decode_i32(&history, &short_coeffs, &mut samples).unwrap_err(); + assert!(matches!( + err, + Error::InverseAdpcmShapeMismatch { + history_len: 4, + coeffs_len: 3, + } + )); + } + + #[test] + fn coeffs_length_mismatch_long() { + let history = [0i32; NUM_ADPCM_COEFF]; + let long_coeffs = [0i32; 7]; + let mut samples = [0i32; 4]; + let err = inverse_adpcm_decode_i32(&history, &long_coeffs, &mut samples).unwrap_err(); + assert!(matches!( + err, + Error::InverseAdpcmShapeMismatch { + history_len: 4, + coeffs_len: 7, + } + )); + } + + #[test] + fn negative_coefficients_apply_sign_correctly() { + // history = (0, 0, 0, 5), coeffs = (-1, 0, 0, 0), residual = 0 + // m=0: 0 + (-1) * 5 = -5 + let history = [0i32, 0, 0, 5]; + let coeffs = [-1i32, 0, 0, 0]; + let mut samples = [0i32]; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples[0], -5); + } + + #[test] + fn long_block_predictor_matches_manual_unroll() { + // Hand-compute the §C.2.2 result for an 8-sample block with + // non-trivial history and coeffs, then cross-check against + // the helper. + let history = [1i32, -2, 3, -4]; + let coeffs = [5i32, -6, 7, -8]; + let residuals = [10i32, 20, -30, 40, -50, 60, -70, 80]; + + // Manual computation, sample by sample. + let mut expected = residuals; + for m in 0..expected.len() { + let mut acc = expected[m]; + for n in 0..NUM_ADPCM_COEFF { + let past = if (n + 1) <= m { + expected[m - n - 1] + } else { + history[NUM_ADPCM_COEFF + m - n - 1] + }; + acc = acc.wrapping_add(coeffs[n].wrapping_mul(past)); + } + expected[m] = acc; + } + + let mut samples = residuals; + inverse_adpcm_decode_i32(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples, expected); + } + + // ---------------------------------------------------------------- + // f64 single-block decode tests + // ---------------------------------------------------------------- + + #[test] + fn f64_zero_coeffs_make_predictor_identity() { + let history = [1.0_f64, 2.0, 3.0, 4.0]; + let coeffs = [0.0_f64; NUM_ADPCM_COEFF]; + let mut samples = [10.0_f64, 20.0, 30.0]; + inverse_adpcm_decode_f64(&history, &coeffs, &mut samples).unwrap(); + assert_eq!(samples, [10.0, 20.0, 30.0]); + } + + #[test] + fn f64_each_coeff_taps_the_right_history_slot() { + let history = [1.0_f64, 2.0, 3.0, 4.0]; + let coeffs = [1.0_f64, 10.0, 100.0, 1000.0]; + let mut samples = [0.0_f64]; + inverse_adpcm_decode_f64(&history, &coeffs, &mut samples).unwrap(); + // 1*4 + 10*3 + 100*2 + 1000*1 = 4 + 30 + 200 + 1000 = 1234 + assert!((samples[0] - 1234.0).abs() < 1e-9); + } + + #[test] + fn f64_uses_just_reconstructed_sample_immediately() { + let history = [0.0_f64; NUM_ADPCM_COEFF]; + let coeffs = [0.5_f64, 0.0, 0.0, 0.0]; + let mut samples = [1.0_f64, 0.0, 0.0, 0.0]; + inverse_adpcm_decode_f64(&history, &coeffs, &mut samples).unwrap(); + // 1.0, 0.5, 0.25, 0.125 + assert!((samples[0] - 1.0).abs() < 1e-12); + assert!((samples[1] - 0.5).abs() < 1e-12); + assert!((samples[2] - 0.25).abs() < 1e-12); + assert!((samples[3] - 0.125).abs() < 1e-12); + } + + #[test] + fn f64_history_length_mismatch() { + let short_history = [0.0_f64; 2]; + let coeffs = [0.0_f64; NUM_ADPCM_COEFF]; + let mut samples = [0.0_f64; 1]; + let err = inverse_adpcm_decode_f64(&short_history, &coeffs, &mut samples).unwrap_err(); + assert!(matches!( + err, + Error::InverseAdpcmShapeMismatch { + history_len: 2, + coeffs_len: 4, + } + )); + } + + #[test] + fn f64_coeffs_length_mismatch() { + let history = [0.0_f64; NUM_ADPCM_COEFF]; + let long_coeffs = [0.0_f64; 5]; + let mut samples = [0.0_f64; 1]; + let err = inverse_adpcm_decode_f64(&history, &long_coeffs, &mut samples).unwrap_err(); + assert!(matches!( + err, + Error::InverseAdpcmShapeMismatch { + history_len: 4, + coeffs_len: 5, + } + )); + } + + #[test] + fn f64_empty_block_is_no_op() { + let history = [1.0_f64, 2.0, 3.0, 4.0]; + let coeffs = [5.0_f64, 6.0, 7.0, 8.0]; + let mut samples: [f64; 0] = []; + inverse_adpcm_decode_f64(&history, &coeffs, &mut samples).unwrap(); + } + + // ---------------------------------------------------------------- + // History-update helper tests + // ---------------------------------------------------------------- + + #[test] + fn update_history_long_block_takes_last_four_samples() { + let mut history = [1i32, 2, 3, 4]; + let samples = [10i32, 20, 30, 40, 50, 60, 70, 80]; + update_history_i32(&mut history, &samples); + // Last four samples become the new history. + assert_eq!(history, [50, 60, 70, 80]); + } + + #[test] + fn update_history_exact_four_takes_all_samples() { + let mut history = [1i32, 2, 3, 4]; + let samples = [10i32, 20, 30, 40]; + update_history_i32(&mut history, &samples); + assert_eq!(history, [10, 20, 30, 40]); + } + + #[test] + fn update_history_short_block_shifts_left() { + // 2-sample short block: shift left by 2, append samples. + // Before: [a, b, c, d] -> shift left by 2 -> [c, d, ?, ?] + // Then append samples [e, f] -> [c, d, e, f]. + let mut history = [1i32, 2, 3, 4]; + let samples = [10i32, 20]; + update_history_i32(&mut history, &samples); + assert_eq!(history, [3, 4, 10, 20]); + } + + #[test] + fn update_history_one_sample_short_block() { + let mut history = [1i32, 2, 3, 4]; + let samples = [99i32]; + update_history_i32(&mut history, &samples); + // Shift left by 1, append [99] + assert_eq!(history, [2, 3, 4, 99]); + } + + #[test] + fn update_history_empty_block_leaves_history_untouched() { + let mut history = [1i32, 2, 3, 4]; + let samples: [i32; 0] = []; + update_history_i32(&mut history, &samples); + assert_eq!(history, [1, 2, 3, 4]); + } + + #[test] + fn update_history_f64_long_block() { + let mut history = [1.0_f64, 2.0, 3.0, 4.0]; + let samples = [10.0_f64, 20.0, 30.0, 40.0, 50.0, 60.0]; + update_history_f64(&mut history, &samples); + assert_eq!(history, [30.0, 40.0, 50.0, 60.0]); + } + + #[test] + fn update_history_f64_short_block() { + let mut history = [1.0_f64, 2.0, 3.0, 4.0]; + let samples = [99.0_f64]; + update_history_f64(&mut history, &samples); + assert_eq!(history, [2.0, 3.0, 4.0, 99.0]); + } + + // ---------------------------------------------------------------- + // Dispatch predicate tests + // ---------------------------------------------------------------- + + #[test] + fn inverse_adpcm_required_only_when_pmode_is_one() { + assert!(inverse_adpcm_required(1)); + assert!(!inverse_adpcm_required(0)); + // PMODE is a 1-bit field per §5.4.1; values > 1 are + // out-of-domain but the predicate still returns false. + for pmode in 2u8..=255 { + assert!(!inverse_adpcm_required(pmode), "pmode={pmode}"); + } + } + + // ---------------------------------------------------------------- + // End-to-end two-block continuation sweep + // ---------------------------------------------------------------- + + #[test] + fn two_block_decode_matches_single_long_block_decode() { + // Property: decoding two consecutive blocks with the history + // updated between them should produce the same reconstructed + // sequence as decoding the concatenated residual stream as a + // single long block (with the initial history fed in once). + let initial_history = [1i32, 2, 3, 4]; + let coeffs = [2i32, -1, 3, 0]; + let residuals: Vec = (1i32..=12).collect(); + + // Single long-block reference run. + let mut single = residuals.clone(); + inverse_adpcm_decode_i32(&initial_history, &coeffs, &mut single).unwrap(); + + // Two-block run: decode first 7 samples, slide history, then + // decode remaining 5 samples. + let mut history = initial_history; + let mut block_a: Vec = residuals[..7].to_vec(); + inverse_adpcm_decode_i32(&history, &coeffs, &mut block_a).unwrap(); + update_history_i32(&mut history, &block_a); + + let mut block_b: Vec = residuals[7..].to_vec(); + inverse_adpcm_decode_i32(&history, &coeffs, &mut block_b).unwrap(); + + let mut chained = block_a; + chained.extend_from_slice(&block_b); + assert_eq!(chained, single); + } +} diff --git a/crates/vendor/oxideav-dts/src/iter.rs b/crates/vendor/oxideav-dts/src/iter.rs new file mode 100644 index 00000000..305c783b --- /dev/null +++ b/crates/vendor/oxideav-dts/src/iter.rs @@ -0,0 +1,2388 @@ +//! Multi-frame iteration over a DTS Core byte stream. +//! +//! Round 6 (2026-05-25) adds two demuxer-friendly helpers on top of +//! the single-frame [`crate::parse_frame_header`] / +//! [`crate::parse_frame_header_14bit`] entry points: +//! +//! - [`find_next_sync`] — scan a byte buffer for the next DTS sync +//! sequence starting at an arbitrary offset, returning both the +//! offset and the [`crate::SyncWordEncoding`] that matched. Useful +//! for callers that need to resynchronise after lost bytes, drop +//! leading container padding, or walk a raw `.dts` file frame by +//! frame. +//! - [`FrameIterator`] / [`iter_frames`] — walk a byte buffer one +//! frame at a time. Each successful step parses the frame's header, +//! reports its byte range, and advances by +//! [`crate::DtsFrameHeader::frame_size_bytes`] (for raw 16-bit +//! encodings; see the function docs for the 14-bit advance rule). +//! +//! Round 159 (2026-05-27) adds an error-tolerant counterpart to +//! [`FrameIterator`]: +//! +//! - [`FrameIteratorResync`] / [`iter_frames_resync`] — when a +//! candidate sync turns out to be a false positive (random payload +//! bytes that happened to match a 4-byte sync sequence and whose +//! subsequent header bits fail the structural NBLKS / FSIZE bounds, +//! or whose declared `frame_size_bytes` overruns end-of-buffer), +//! surface a [`ResyncEvent`] reporting the discarded offset + cause +//! and continue scanning from `offset + 1` instead of terminating. +//! Useful for stream-integrity tooling that needs to walk a +//! partially-corrupted `.dts` stream past a malformed-syncword +//! patch. +//! +//! Neither helper depends on the [`oxideav-core`] integration; both +//! are available in the `--no-default-features` build alongside the +//! standalone parsers. +//! +//! ## What stays out of scope +//! +//! - Container-stream parsing: a raw `.dts` file is a concatenation +//! of self-delimited Core frames, which is the only shape these +//! helpers walk. AVI / MP4 / Matroska sample carriage stays in +//! their respective container crates; the helpers here operate on +//! raw codec bytes only. +//! - PCM sample emission. The iterator surfaces parsed +//! [`crate::DtsFrameHeader`] records and the raw frame byte slice; +//! subband / QMF / Huffman decoding remains gated on the spec +//! tables in the docs gaps. +//! - 14-bit byte-advance for [`FrameIterator`]. The iterator only +//! advances on the **raw 16-bit** sync variants because the +//! `frame_size_bytes` field is documented as the byte length of +//! the unpacked stream — for 14-bit-packed containers the +//! corresponding container-byte advance would be +//! `frame_size_bytes * 8 / 14` rounded up to the next even byte, +//! which the wiki snapshot does **not** spell out. The 14-bit +//! single-frame [`crate::parse_frame_header_14bit`] entry point +//! remains available for callers that have already partitioned +//! their 14-bit input into frame-sized slices. See `README.md`'s +//! round-6 docs gap #7. + +use crate::header::{detect_sync, parse_frame_header}; +use crate::{parse_frame_header_14bit, DtsFrameHeader, Error, Result, SyncWordEncoding}; + +/// Result of a [`find_next_sync`] lookup. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SyncMatch { + /// Absolute byte offset (within the input slice passed to + /// [`find_next_sync`]) where the sync sequence starts. + pub offset: usize, + /// Which of the four documented sync encodings was found. + pub encoding: SyncWordEncoding, +} + +impl SyncMatch { + /// Byte length of the sync sequence at [`Self::offset`], + /// delegated to [`SyncWordEncoding::sync_byte_length`]. + /// + /// Equivalent to `self.encoding.sync_byte_length()`. Provided as a + /// thin accessor so the common pattern "advance the cursor past + /// the matched sync" reads naturally — + /// `cursor = sync_match.offset + sync_match.sync_byte_length()` + /// — without spelling the field access out. + #[inline] + pub fn sync_byte_length(&self) -> usize { + self.encoding.sync_byte_length() + } + + /// Half-open byte range of the sync sequence at this match + /// (`offset..offset + sync_byte_length()`). + /// + /// Provided so callers that want to highlight the matched bytes + /// (e.g. for stream-integrity tooling that emits a per-sync + /// hex-window report) can slice the input directly: + /// `&bytes[sync_match.sync_byte_range()]`. + #[inline] + pub fn sync_byte_range(&self) -> core::ops::Range { + self.offset..self.offset + self.sync_byte_length() + } +} + +/// First-byte gate for the four documented DTS sync sequences. +/// +/// All four sync words begin with a distinct first byte that does +/// not appear in mid-sync positions of any of the other three: +/// +/// | Encoding | First byte | Source | +/// | ------------------------- | ---------- | --------------------------------- | +/// | `RawBigEndian` | `0x7F` | wiki snapshot `7F FE 80 01` | +/// | `RawLittleEndian` | `0xFE` | wiki snapshot `FE 7F 01 80` | +/// | `FourteenBitBigEndian` | `0x1F` | wiki snapshot `1F FF E8 00 07 Fx` | +/// | `FourteenBitLittleEndian` | `0xFF` | wiki snapshot `FF 1F 00 E8 Fx 07` | +/// +/// `find_next_sync` uses this as a one-byte filter to skip the +/// 4-byte raw-sync check + 6-byte 14-bit-sync check on positions +/// whose first byte cannot start any documented sync. On random +/// payload bytes this short-circuits ~98.4% of positions (4 of 256 +/// possible first bytes match) before any multi-byte comparison +/// fires. +#[inline] +fn is_sync_first_byte_candidate(b: u8) -> bool { + // Equivalent to `matches!(b, 0x7F | 0xFE | 0x1F | 0xFF)`. Written + // as a single bitmask check so a release-mode compile lowers to a + // bt / cmp pair rather than a four-way branch. + matches!(b, 0x7F | 0xFE | 0x1F | 0xFF) +} + +/// Scan `bytes[start..]` for the next DTS Core sync sequence. +/// +/// Returns the byte offset (in the original `bytes` slice, not in +/// `bytes[start..]`) and the matched [`SyncWordEncoding`] of the +/// first sync found at or after `start`, or `None` if no sync +/// appears before end-of-buffer. +/// +/// All four documented sync sequences are accepted: +/// +/// - `7F FE 80 01` — raw big-endian (4 bytes). +/// - `FE 7F 01 80` — raw little-endian (4 bytes). +/// - `1F FF E8 00 07 Fx` — 14-bit packed big-endian (6 bytes). +/// - `FF 1F 00 E8 Fx 07` — 14-bit packed little-endian (6 bytes). +/// +/// The 14-bit variants are matched via the lower-14-bit payloads of +/// the first three containers (`0x1FFF`, `0x2800`, +/// top-4-of-`0x07F?`), matching the same widened detection rule the +/// single-frame parser uses (see [`detect_sync`] in `header.rs`). +/// +/// The scan is `O(n)`: every byte is visited at most twice (once for +/// the 4-byte raw-sync check, once for the 6-byte 14-bit-sync +/// check). Calling [`find_next_sync`] in a loop with the previous +/// `offset + 1` is the standard resync pattern. +/// +/// Round 165 (2026-05-27) added a one-byte first-byte gate +/// ([`is_sync_first_byte_candidate`]) before the multi-byte +/// [`detect_sync`] call so positions whose first byte cannot start +/// any documented sync (252 of 256 possible bytes) skip the +/// multi-byte comparison entirely. The walk order, returned offset, +/// and matched encoding are unchanged from the round-6 implementation +/// — round 165 also adds a `find_next_sync_matches_pre_optimization_reference` +/// equivalence test to prove that every byte sequence the old loop +/// would accept the new loop also accepts (and at the same offset +/// with the same encoding tag). +pub fn find_next_sync(bytes: &[u8], start: usize) -> Option { + if start >= bytes.len() { + return None; + } + let mut i = start; + // We need at least 4 bytes for the shortest sync (raw 16-bit) and + // 6 bytes for the longest (14-bit packed). Stop the scan at the + // last position that could still hold a raw sync. + let last = bytes.len(); + while i + 4 <= last { + // First-byte gate: 252 of 256 possible bytes fail this and + // skip the multi-byte detect_sync call. The walk order is + // preserved (every position is still visited in order) so + // the returned offset / encoding are identical to the + // pre-round-165 implementation. + if !is_sync_first_byte_candidate(bytes[i]) { + i += 1; + continue; + } + if let Ok(enc) = detect_sync(&bytes[i..]) { + return Some(SyncMatch { + offset: i, + encoding: enc, + }); + } + i += 1; + } + None +} + +/// One step of a [`FrameIterator`] over a raw-16-bit DTS Core +/// stream. +#[derive(Debug, Clone, Copy)] +pub struct FrameView<'a> { + /// Parsed header. + pub header: DtsFrameHeader, + /// Absolute byte offset of the frame's first sync byte within + /// the input passed to [`iter_frames`]. + pub offset: usize, + /// Byte length of the frame (from + /// [`DtsFrameHeader::frame_size_bytes`]). Always 95..=16384. + pub len: usize, + /// Borrowed frame bytes (`bytes[offset..offset + len]`). + pub data: &'a [u8], +} + +impl<'a> FrameView<'a> { + /// SUBFRAMES region of the frame: the bytes that follow the + /// fully-decoded frame-sync header. + /// + /// Equivalent to `&self.data[self.header.header_byte_length()..]`. + /// The wiki snapshot (`docs/audio/dts/wiki/DTS.wiki`) marks this + /// region as `'''TODO'''`; subband / QMF / Huffman / VQ decoding + /// remains gated on the §5.3.1 value tables and the §5.4 polyphase + /// filterbank landing in `docs/`. The helper is exposed so + /// downstream code (re-muxers, payload-CRC validators, future + /// subframe decoders) can carve out the region without recomputing + /// the header boundary. + /// + /// Always non-empty for well-formed frames: the smallest documented + /// frame size is 95 B and the largest header window is 15 B + /// (`crc_present == true`), so at least 80 B of SUBFRAMES region + /// is guaranteed. + pub fn payload(&self) -> &'a [u8] { + &self.data[self.header.header_byte_length()..] + } + + /// Locate and parse the frame's §5.7.1 Auxiliary Data chunk (the + /// optional end-of-frame time stamp + dynamic downmix + /// coefficients), returning `Ok(None)` when the frame carries no + /// DWORD-aligned `nSYNCAUX` word. Composition of + /// [`crate::parse_aux_data`] over the frame's own bytes + header. + pub fn aux_data(&self) -> crate::Result> { + crate::parse_aux_data(self.data, &self.header) + } + + /// Locate and parse the frame's §5.7.2 Rev2 Auxiliary Data Chunk + /// (embedded-ES downmix scale + broadcast DRC / dialnorm), + /// returning `Ok(None)` when the frame carries no DWORD-aligned + /// `nSYNCRev2AUX` word. Composition of [`crate::parse_rev2_aux`] + /// over the frame's own bytes + header. + pub fn rev2_aux(&self) -> crate::Result> { + crate::parse_rev2_aux(self.data, &self.header) + } +} + +/// Iterator that walks a raw-16-bit DTS Core byte buffer frame by +/// frame. +/// +/// Each [`Iterator::next`] step: +/// 1. Calls [`find_next_sync`] from the current cursor to handle any +/// leading garbage / inter-frame padding the source may have +/// introduced. The iterator does NOT assume `bytes[0]` is a sync +/// byte; it scans for one. +/// 2. Calls [`parse_frame_header`] at the located offset. A parse +/// failure (no sync within reach, truncated header, NBLKS or +/// FSIZE out of range) is returned as the next item's `Err` +/// variant; subsequent calls then return `None`. +/// 3. On success, yields a [`FrameView`] borrowing the frame's +/// bytes from the input and advances the cursor by +/// `header.frame_size_bytes` so the following step parses the +/// next sync. +/// +/// The iterator only accepts raw 16-bit encodings (`RawBigEndian` / +/// `RawLittleEndian`). When [`find_next_sync`] locates a 14-bit +/// sync, the iterator yields a single [`Error::UnsupportedFourteenBit`] +/// item and then terminates. Callers walking 14-bit container +/// streams must externally partition the input into frame-sized +/// slices and feed each slice to [`crate::parse_frame_header_14bit`] +/// directly. See `README.md`'s round-6 docs gap #7 for the +/// container-byte-advance rule. +#[derive(Debug)] +pub struct FrameIterator<'a> { + bytes: &'a [u8], + cursor: usize, + done: bool, +} + +impl<'a> FrameIterator<'a> { + /// Construct an iterator positioned at byte 0 of `bytes`. + pub fn new(bytes: &'a [u8]) -> Self { + FrameIterator { + bytes, + cursor: 0, + done: false, + } + } + + /// Current cursor offset. After a successful step the cursor + /// points at the next frame's expected sync byte (or one past + /// end-of-buffer if the previous frame was the last). After a + /// failure the cursor stays at the point of failure. + pub fn cursor(&self) -> usize { + self.cursor + } +} + +impl<'a> Iterator for FrameIterator<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let sync_match = find_next_sync(self.bytes, self.cursor)?; + match sync_match.encoding { + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian => { + // 14-bit container streams need an out-of-band + // container-byte advance rule which the wiki snapshot + // does not enumerate. Surface the limitation and + // terminate so the caller switches to the + // single-frame 14-bit entry point. + self.done = true; + return Some(Err(Error::UnsupportedFourteenBit)); + } + _ => {} + } + let off = sync_match.offset; + let parse_result = parse_frame_header(&self.bytes[off..]); + let hdr = match parse_result { + Ok(h) => h, + Err(e) => { + self.done = true; + self.cursor = off; + return Some(Err(e)); + } + }; + let len = hdr.frame_size_bytes as usize; + if off + len > self.bytes.len() { + // Header says the frame extends past end-of-buffer. We + // still surface the header (the caller may want to know + // the truncation occurred at this offset with this + // size), but mark the iterator done. + self.done = true; + self.cursor = off; + return Some(Err(Error::UnexpectedEof)); + } + let view = FrameView { + header: hdr, + offset: off, + len, + data: &self.bytes[off..off + len], + }; + self.cursor = off + len; + Some(Ok(view)) + } +} + +/// Convenience constructor — equivalent to [`FrameIterator::new`]. +pub fn iter_frames(bytes: &[u8]) -> FrameIterator<'_> { + FrameIterator::new(bytes) +} + +/// Reason a [`FrameIteratorResync`] step discarded a candidate sync +/// position and resumed scanning one byte further. +/// +/// Surfaced through [`ResyncEvent::cause`]; callers can route on the +/// variant (e.g. log truncated tails differently from false-positive +/// syncs mid-buffer). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResyncCause { + /// The header bits that follow the sync failed one of the + /// structural bounds the spec mandates (NBLKS < 5 → [`Error::BlockCountOutOfRange`], + /// frame size < 95 → [`Error::FrameSizeOutOfRange`]) — strong + /// evidence the sync was a coincidental byte sequence inside a + /// previous frame's payload rather than the start of a real + /// frame. + StructuralBoundFailed(Error), + /// The candidate sync sat too close to end-of-buffer for the + /// header bit-width to fit — the parser returned + /// [`Error::UnexpectedEof`] while reading the header itself. + HeaderEof, + /// The header parsed successfully but its declared + /// `frame_size_bytes` runs past end-of-buffer. The fail-fast + /// [`FrameIterator`] reports this as + /// [`Error::UnexpectedEof`] and terminates; the resync iterator + /// treats the candidate as a false-positive sync and resumes + /// scanning at `offset + 1`. A genuine truncated tail will + /// produce one or more `FrameLengthOverrunsBuffer` events near + /// the end of the input and no further successful frames. + FrameLengthOverrunsBuffer { + /// `header.frame_size_bytes` at the discarded position. + declared_len: u16, + }, + /// A 14-bit sync was encountered. The fail-fast iterator yields + /// [`Error::UnsupportedFourteenBit`] and terminates; the resync + /// iterator instead surfaces this event and continues scanning + /// past the 14-bit sync, so a stream with intermixed encodings + /// (e.g. a raw-16-bit stream with a few stray 14-bit-shaped byte + /// sequences in payload) still walks to completion. + FourteenBitSyncSkipped, +} + +/// One step of [`FrameIteratorResync`] when a candidate sync turned +/// out to be a false positive. +/// +/// `offset` and `encoding` come from the underlying +/// [`find_next_sync`] match; `cause` documents which check rejected +/// the candidate (see [`ResyncCause`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResyncEvent { + /// Absolute byte offset of the discarded sync candidate within + /// the iterator's input. + pub offset: usize, + /// Sync encoding that matched at `offset`. + pub encoding: SyncWordEncoding, + /// Why the candidate was rejected. + pub cause: ResyncCause, +} + +/// Error-tolerant counterpart to [`FrameIterator`]. +/// +/// The fail-fast [`FrameIterator`] surfaces a structural-bound +/// failure at a candidate sync (NBLKS < 5, FSIZE < 95) or a +/// header-truncation by yielding the parser's [`Error`] and +/// terminating — appropriate for callers walking a known-good raw +/// `.dts` stream where any malformed sync is a hard fault. +/// +/// `FrameIteratorResync` instead treats those cases as **evidence +/// the candidate sync was a false positive** (a coincidental +/// 4-byte sequence inside another frame's payload that matched the +/// 32-bit syncword), yielding a [`ResyncEvent`] documenting the +/// offset + cause and advancing the cursor by one byte so the scan +/// resumes past the spurious match. This lets stream-integrity +/// tooling walk a partially-corrupted stream past malformed-sync +/// patches and recover frames after the damage. +/// +/// Iteration logic per step: +/// 1. Find the next sync at or after the current cursor via +/// [`find_next_sync`]. If none, the iterator ends (yields +/// `None`). +/// 2. If the sync is a 14-bit variant: yield +/// [`ResyncCause::FourteenBitSyncSkipped`] and advance the +/// cursor to `offset + 1` (rather than terminating like +/// [`FrameIterator`] does). +/// 3. Parse the header at the matched offset. On structural +/// failure (`BlockCountOutOfRange` / `FrameSizeOutOfRange` / +/// `UnexpectedEof` while reading the header bits) yield a +/// [`ResyncEvent`] with the appropriate [`ResyncCause`] and +/// advance the cursor to `offset + 1`. +/// 4. If the header parses but `frame_size_bytes` overruns +/// end-of-buffer, yield +/// [`ResyncCause::FrameLengthOverrunsBuffer`] with the declared +/// length and advance the cursor to `offset + 1`. A genuine +/// truncated tail will therefore emit one or more overrun +/// events and then iteration ends naturally when no further +/// sync is found. +/// 5. Otherwise yield `Ok(FrameView)` and advance the cursor by +/// `frame_size_bytes`. +/// +/// This iterator is only meaningful for the raw 16-bit encodings +/// (the [`FrameIterator`] docs spell out the 14-bit container-byte +/// advance gap). 14-bit syncs are skipped per step (2) above +/// rather than walked, so a raw-16-bit stream that contains stray +/// 14-bit-shaped byte sequences in payload still walks past them. +#[derive(Debug)] +pub struct FrameIteratorResync<'a> { + bytes: &'a [u8], + cursor: usize, +} + +impl<'a> FrameIteratorResync<'a> { + /// Construct a resync iterator positioned at byte 0 of `bytes`. + pub fn new(bytes: &'a [u8]) -> Self { + FrameIteratorResync { bytes, cursor: 0 } + } + + /// Current cursor offset (advances on every yielded step, + /// whether `Ok` or `Err`). + pub fn cursor(&self) -> usize { + self.cursor + } +} + +impl<'a> Iterator for FrameIteratorResync<'a> { + type Item = core::result::Result, ResyncEvent>; + + fn next(&mut self) -> Option { + let sync_match = find_next_sync(self.bytes, self.cursor)?; + let off = sync_match.offset; + if matches!( + sync_match.encoding, + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian + ) { + self.cursor = off + 1; + return Some(Err(ResyncEvent { + offset: off, + encoding: sync_match.encoding, + cause: ResyncCause::FourteenBitSyncSkipped, + })); + } + match parse_frame_header(&self.bytes[off..]) { + Ok(hdr) => { + let len = hdr.frame_size_bytes as usize; + if off + len > self.bytes.len() { + self.cursor = off + 1; + return Some(Err(ResyncEvent { + offset: off, + encoding: sync_match.encoding, + cause: ResyncCause::FrameLengthOverrunsBuffer { + declared_len: hdr.frame_size_bytes, + }, + })); + } + let view = FrameView { + header: hdr, + offset: off, + len, + data: &self.bytes[off..off + len], + }; + self.cursor = off + len; + Some(Ok(view)) + } + Err(e) => { + let cause = match e { + Error::UnexpectedEof => ResyncCause::HeaderEof, + Error::BlockCountOutOfRange { .. } | Error::FrameSizeOutOfRange { .. } => { + ResyncCause::StructuralBoundFailed(e) + } + // `find_next_sync` already filtered out `NoSync`; + // the 14-bit branch is handled above so + // `UnsupportedFourteenBit` can't fire here; + // `FieldOutOfRange` is encoder-only. Treat any + // other variant as a structural false-positive + // so the iterator stays total over future enum + // extensions. + _ => ResyncCause::StructuralBoundFailed(e), + }; + self.cursor = off + 1; + Some(Err(ResyncEvent { + offset: off, + encoding: sync_match.encoding, + cause, + })) + } + } + } +} + +/// Convenience constructor — equivalent to +/// [`FrameIteratorResync::new`]. +/// +/// See [`FrameIteratorResync`] for the resync vs fail-fast +/// trade-off and the per-step yield contract. +pub fn iter_frames_resync(bytes: &[u8]) -> FrameIteratorResync<'_> { + FrameIteratorResync::new(bytes) +} + +/// Scan an entire byte buffer and return every documented DTS sync +/// occurrence it contains. +/// +/// This is the bulk-scan counterpart to [`find_next_sync`]: instead of +/// returning the first hit at or after a cursor, it walks the buffer +/// from start to end and collects every position where one of the four +/// documented sync sequences appears. Useful for tooling that needs to +/// validate stream integrity, count frames without parsing them, or +/// build an index of resync points up front. +/// +/// The scan honours the same matching rules as [`find_next_sync`]: +/// +/// - `7F FE 80 01` — raw big-endian (4 bytes). +/// - `FE 7F 01 80` — raw little-endian (4 bytes). +/// - `1F FF E8 00 07 Fx` — 14-bit packed big-endian (6 bytes, matched +/// on the lower 14 bits of each container per [`detect_sync`]). +/// - `FF 1F 00 E8 Fx 07` — 14-bit packed little-endian (6 bytes, +/// matched on the lower 14 bits of each container). +/// +/// Each yielded [`SyncMatch`] reports the absolute byte offset of the +/// first sync byte plus the matched encoding. Overlapping matches are +/// not possible because the four sync sequences differ in their first +/// two bytes (`7F`/`FE`/`1F`/`FF`), but the scan still advances +/// one byte at a time so adjacent sync occurrences (one ending and the +/// next starting on consecutive bytes) are both reported. +/// +/// The scan is `O(n)` — each byte is visited at most twice by +/// [`detect_sync`] (one 4-byte raw check, one 6-byte 14-bit check). +/// Callers that only need the first sync should prefer +/// [`find_next_sync`] to avoid materialising the result vector. +/// +/// ## Example +/// +/// ``` +/// use oxideav_dts::{find_all_syncs, SyncWordEncoding}; +/// +/// let mut buf = vec![0u8; 32]; +/// buf[0..4].copy_from_slice(&[0x7F, 0xFE, 0x80, 0x01]); +/// buf[8..12].copy_from_slice(&[0xFE, 0x7F, 0x01, 0x80]); +/// +/// let matches = find_all_syncs(&buf); +/// assert_eq!(matches.len(), 2); +/// assert_eq!(matches[0].offset, 0); +/// assert_eq!(matches[0].encoding, SyncWordEncoding::RawBigEndian); +/// assert_eq!(matches[1].offset, 8); +/// assert_eq!(matches[1].encoding, SyncWordEncoding::RawLittleEndian); +/// ``` +pub fn find_all_syncs(bytes: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut cursor = 0usize; + while let Some(m) = find_next_sync(bytes, cursor) { + out.push(m); + // Advance by one byte so consecutive (non-overlapping) syncs + // are both reported. The four documented sync prefixes start + // with `7F`/`FE`/`1F`/`FF`, all distinct, so no two sync + // sequences can overlap; a one-byte step is therefore both + // sufficient and minimal. + cursor = m.offset + 1; + } + out +} + +/// Lazy streaming iterator over every DTS sync sequence in a byte +/// buffer. +/// +/// Same matching rules and walk order as [`find_all_syncs`] — the +/// only difference is that this iterator does **not** materialise the +/// full result vector. It walks the buffer one [`find_next_sync`] +/// hop at a time, yielding each [`SyncMatch`] as it is found and +/// stopping when [`find_next_sync`] returns `None`. +/// +/// Useful when the caller only needs to: +/// +/// - act on syncs in order (e.g. stream-integrity tooling that prints +/// each resync point to a log as it walks), +/// - stop early after the first N matches (`iter_syncs(bytes).take(8)`), +/// - or chain a filter / sniff through standard +/// [`Iterator`] combinators (e.g. +/// `iter_syncs(bytes).filter(|m| m.encoding.is_raw_16bit())`) +/// +/// without paying the upfront allocation that [`find_all_syncs`] +/// incurs. For a workload that consumes every match anyway, +/// `find_all_syncs(bytes)` and `iter_syncs(bytes).collect()` produce +/// the same `Vec` at the same `O(n)` cost — pick the bulk +/// helper when the result is needed as a slice, the iterator when the +/// caller is fine with element-by-element consumption. +/// +/// The iterator is non-overlapping: after yielding a match at +/// `offset`, scanning resumes at `offset + 1`, identical to +/// [`find_all_syncs`]. The four documented sync prefixes have +/// distinct first bytes (`7F` / `FE` / `1F` / `FF`), so no two real +/// sync sequences can overlap — a one-byte step is both sufficient +/// and minimal. Adjacent syncs (one ending and the next starting on +/// consecutive bytes) are both reported. +/// +/// ## Example +/// +/// ``` +/// use oxideav_dts::{iter_syncs, SyncWordEncoding}; +/// +/// let mut buf = vec![0u8; 32]; +/// buf[0..4].copy_from_slice(&[0x7F, 0xFE, 0x80, 0x01]); +/// buf[8..12].copy_from_slice(&[0xFE, 0x7F, 0x01, 0x80]); +/// +/// let mut it = iter_syncs(&buf); +/// let first = it.next().unwrap(); +/// assert_eq!(first.offset, 0); +/// assert_eq!(first.encoding, SyncWordEncoding::RawBigEndian); +/// let second = it.next().unwrap(); +/// assert_eq!(second.offset, 8); +/// assert_eq!(second.encoding, SyncWordEncoding::RawLittleEndian); +/// assert!(it.next().is_none()); +/// ``` +#[derive(Debug)] +pub struct SyncIterator<'a> { + bytes: &'a [u8], + cursor: usize, +} + +impl<'a> SyncIterator<'a> { + /// Construct an iterator positioned at byte 0 of `bytes`. + pub fn new(bytes: &'a [u8]) -> Self { + SyncIterator { bytes, cursor: 0 } + } + + /// Current scan cursor. After yielding a match at `offset` the + /// cursor advances to `offset + 1`; after exhausting the input it + /// rests at the byte position [`find_next_sync`] gave up at. + pub fn cursor(&self) -> usize { + self.cursor + } +} + +impl<'a> Iterator for SyncIterator<'a> { + type Item = SyncMatch; + + fn next(&mut self) -> Option { + let m = find_next_sync(self.bytes, self.cursor)?; + // Same one-byte advance as `find_all_syncs`: the four sync + // prefixes have distinct first bytes so non-overlapping + // matches at adjacent offsets are still both reported. + self.cursor = m.offset + 1; + Some(m) + } +} + +/// Convenience constructor — equivalent to [`SyncIterator::new`]. +/// +/// See [`SyncIterator`] for the streaming vs bulk-scan trade-off +/// against [`find_all_syncs`]. +pub fn iter_syncs(bytes: &[u8]) -> SyncIterator<'_> { + SyncIterator::new(bytes) +} + +// --------------------------------------------------------------------- +// Round 192 — 14-bit container-byte frame iterator (`iter_frames_14bit`) +// +// The fail-fast [`FrameIterator`] from round 6 walks raw-16-bit streams +// only; a 14-bit sync at the cursor yields `Error::UnsupportedFourteenBit` +// and terminates because the iterator's `frame_size_bytes` advance only +// makes sense in the unpacked domain. Round 189 added the analytical +// half — [`DtsFrameHeader::frame_size_container_bytes`] — which converts +// the unpacked-domain frame size to a container-domain byte count for +// the 14-bit encodings. Round 192 wires that accessor into a dedicated +// 14-bit frame iterator: each step calls [`parse_frame_header_14bit`] +// at the matched container offset (which internally unpacks just enough +// containers to read the 13/15-byte unpacked header window), then steps +// the cursor by `frame_size_container_bytes(encoding)` container bytes. +// +// We deliberately introduce a separate [`FrameView14`] type rather than +// reusing [`FrameView`] because the semantics of `len` differ: in the +// 14-bit case `len` is a container-byte advance derived from the +// 14-bit `frame_size_container_bytes` formula (not the raw +// `header.frame_size_bytes` field), and `data` borrows the +// container-byte window — not the unpacked logical bytes. Sharing the +// `FrameView` type would require silently overloading `len`'s meaning, +// which is exactly the kind of footgun the wiki/`docs/` ambiguity asks +// us to avoid. +// --------------------------------------------------------------------- + +/// One step of a [`FrameIterator14`] over a 14-bit-packed DTS Core +/// container stream. +/// +/// Field semantics differ from [`FrameView`] in two specific ways: +/// +/// - `len` is the **container-byte** advance produced by +/// [`DtsFrameHeader::frame_size_container_bytes`] — not the +/// `header.frame_size_bytes` field (which is the unpacked logical +/// byte count). The cursor steps by `len` container bytes to land on +/// the next sync. +/// - `data` is the container-byte window of the frame (`bytes[offset.. +/// offset + len]`), not the unpacked logical bytes. Callers that +/// want to decode the SUBFRAMES region must run +/// [`crate::unpack_14bit_to_16bit`] on `data` first. +#[derive(Debug, Clone, Copy)] +pub struct FrameView14<'a> { + /// Parsed header. `header.sync_word_encoding` is one of + /// [`SyncWordEncoding::FourteenBitBigEndian`] / + /// [`SyncWordEncoding::FourteenBitLittleEndian`] (the iterator + /// only walks 14-bit streams). + pub header: DtsFrameHeader, + /// Absolute container-byte offset of the frame's first sync byte + /// within the input passed to [`iter_frames_14bit`]. + pub offset: usize, + /// Container-byte length of the frame, equal to + /// `header.frame_size_container_bytes(header.sync_word_encoding)`. + /// Always even (per ETSI §6.1.3.1's 28-bit / two-container-word + /// boundary invariant) and strictly greater than the unpacked + /// `header.frame_size_bytes` count (because 14 logical bits per + /// 16 container bits scales the span up by 16/14 ≈ 1.143). + pub len: usize, + /// Borrowed container bytes (`bytes[offset..offset + len]`). + pub data: &'a [u8], +} + +/// Iterator that walks a 14-bit-packed DTS Core container stream +/// frame by frame. +/// +/// Each [`Iterator::next`] step: +/// 1. Calls [`find_next_sync`] from the current cursor and accepts +/// only 14-bit syncs ([`SyncWordEncoding::FourteenBitBigEndian`] / +/// [`SyncWordEncoding::FourteenBitLittleEndian`]). A raw 16-bit +/// sync at the cursor yields a single [`Error::UnsupportedRaw16Bit`] +/// item (the symmetric counterpart to the round-6 [`FrameIterator`] +/// behaviour on 14-bit syncs) and the iterator terminates. +/// 2. Calls [`parse_frame_header_14bit`] at the located offset. The +/// parser internally unpacks the first ~18 container bytes (= +/// 9 containers = 126 payload bits ≥ the 120-bit worst-case header +/// window) and reads the 13 or 15-byte unpacked header window. A +/// parse failure (truncated header, NBLKS or FSIZE out of range) +/// is returned as the next item's `Err` variant; subsequent calls +/// then return `None`. +/// 3. On success, computes the container-byte advance via +/// [`DtsFrameHeader::frame_size_container_bytes`] (the round-189 +/// analytical formula: +/// `2 * ceil(frame_size_bytes * 8 / 14)` for the 14-bit +/// encodings), yields a [`FrameView14`] borrowing the frame's +/// container bytes from the input, and advances the cursor by +/// that count so the following step parses the next sync. +/// +/// Just like [`FrameIterator`], this iterator resyncs after leading +/// garbage by calling [`find_next_sync`] up-front rather than +/// assuming `bytes[0]` is the first sync byte. A stream with +/// intermixed BE / LE 14-bit syncs walks correctly because each step +/// re-reads the matched encoding from the [`SyncMatch`]. +/// +/// See [`FrameIterator`] for the raw-16-bit counterpart and the +/// round-6 docs gap #7 for the formula's derivation. +#[derive(Debug)] +pub struct FrameIterator14<'a> { + bytes: &'a [u8], + cursor: usize, + done: bool, +} + +impl<'a> FrameIterator14<'a> { + /// Construct an iterator positioned at byte 0 of `bytes`. + pub fn new(bytes: &'a [u8]) -> Self { + FrameIterator14 { + bytes, + cursor: 0, + done: false, + } + } + + /// Current container-byte cursor. After a successful step the + /// cursor points at the next frame's expected sync byte (or one + /// past end-of-buffer if the previous frame was the last). After + /// a failure the cursor stays at the point of failure. + pub fn cursor(&self) -> usize { + self.cursor + } +} + +impl<'a> Iterator for FrameIterator14<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let sync_match = find_next_sync(self.bytes, self.cursor)?; + match sync_match.encoding { + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian => {} + // Symmetric counterpart to the round-6 `iter_frames` + // behaviour on 14-bit syncs: this iterator only walks + // 14-bit container streams, so a raw 16-bit sync is + // out-of-domain. Surface the mismatch and terminate. + _ => { + self.done = true; + return Some(Err(Error::UnsupportedRaw16Bit)); + } + } + let off = sync_match.offset; + let enc = sync_match.encoding; + let parse_result = parse_frame_header_14bit(&self.bytes[off..]); + let hdr = match parse_result { + Ok(h) => h, + Err(e) => { + self.done = true; + self.cursor = off; + return Some(Err(e)); + } + }; + // Round-189 analytical formula: container-byte advance for the + // 14-bit encodings is `2 * ceil(frame_size_bytes * 8 / 14)`. + let len = hdr.frame_size_container_bytes(enc) as usize; + if off + len > self.bytes.len() { + self.done = true; + self.cursor = off; + return Some(Err(Error::UnexpectedEof)); + } + let view = FrameView14 { + header: hdr, + offset: off, + len, + data: &self.bytes[off..off + len], + }; + self.cursor = off + len; + Some(Ok(view)) + } +} + +/// Convenience constructor — equivalent to [`FrameIterator14::new`]. +/// +/// Walks a 14-bit-packed DTS Core container stream (BE or LE — each +/// frame's encoding is re-read from the matched sync, so mixed-encoding +/// inputs walk correctly). For raw 16-bit streams use [`iter_frames`]. +pub fn iter_frames_14bit(bytes: &[u8]) -> FrameIterator14<'_> { + FrameIterator14::new(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::FrameType; + + const RAW_BE_SYNC: [u8; 4] = [0x7F, 0xFE, 0x80, 0x01]; + const RAW_LE_SYNC: [u8; 4] = [0xFE, 0x7F, 0x01, 0x80]; + + #[test] + fn find_next_sync_at_offset_zero() { + let mut buf = vec![0u8; 16]; + buf[..4].copy_from_slice(&RAW_BE_SYNC); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(m.offset, 0); + assert_eq!(m.encoding, SyncWordEncoding::RawBigEndian); + } + + #[test] + fn find_next_sync_skips_leading_garbage() { + let mut buf = vec![0xAAu8; 32]; + buf[7..11].copy_from_slice(&RAW_BE_SYNC); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(m.offset, 7); + assert_eq!(m.encoding, SyncWordEncoding::RawBigEndian); + } + + #[test] + fn find_next_sync_le_variant() { + let mut buf = vec![0u8; 16]; + buf[3..7].copy_from_slice(&RAW_LE_SYNC); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(m.offset, 3); + assert_eq!(m.encoding, SyncWordEncoding::RawLittleEndian); + } + + #[test] + fn find_next_sync_14bit_be() { + let mut buf = vec![0u8; 16]; + buf[5..11].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xFA]); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(m.offset, 5); + assert_eq!(m.encoding, SyncWordEncoding::FourteenBitBigEndian); + } + + #[test] + fn find_next_sync_14bit_le() { + let mut buf = vec![0u8; 16]; + buf[5..11].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF3, 0x07]); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(m.offset, 5); + assert_eq!(m.encoding, SyncWordEncoding::FourteenBitLittleEndian); + } + + #[test] + fn find_next_sync_honours_start_offset() { + // Two syncs in the buffer; start past the first. + let mut buf = vec![0xAAu8; 32]; + buf[2..6].copy_from_slice(&RAW_BE_SYNC); + buf[20..24].copy_from_slice(&RAW_BE_SYNC); + let m = find_next_sync(&buf, 7).unwrap(); + assert_eq!(m.offset, 20); + } + + #[test] + fn find_next_sync_returns_none_when_absent() { + let buf = vec![0xAAu8; 64]; + assert_eq!(find_next_sync(&buf, 0), None); + } + + #[test] + fn find_next_sync_returns_none_when_start_past_end() { + let buf = vec![0u8; 16]; + assert_eq!(find_next_sync(&buf, 100), None); + } + + #[test] + fn find_next_sync_returns_none_when_only_partial_sync_at_tail() { + // Three bytes of an in-progress raw sync (`7F FE 80`) but no + // fourth byte — the scanner walks up to length-4, finds + // nothing, and returns None. + let mut buf = vec![0xAAu8; 8]; + buf[5..8].copy_from_slice(&[0x7F, 0xFE, 0x80]); + assert_eq!(find_next_sync(&buf, 0), None); + } + + // --------------------------------------------------------------- + // Round 138 — FrameView::payload() + // --------------------------------------------------------------- + + /// Hand-build a 95-byte raw-BE termination frame (NBLKS=5, + /// FSIZE=95, crc_present=0) padded with zeros for the SUBFRAMES + /// region; then confirm `FrameView::payload()` returns exactly + /// `frame.len - header_byte_length()` bytes (= 95 - 13 = 82). + #[test] + fn frame_view_payload_slice_length_matches_header_boundary_no_crc() { + // The base 13-byte header window is followed by 82 bytes of + // SUBFRAMES region we fill with a distinctive 0xCD pattern + // so the slice content can be checked too. + let mut buf = vec![0u8; 95]; + // Sync. + buf[0..4].copy_from_slice(&[0x7F, 0xFE, 0x80, 0x01]); + // FTYPE=0 (termination, MSB), SHORT=0, CRC_PRESENT=0, + // NBLKS=5 (7 bits), FSIZE-1=94 (14 bits = frame_size 95), + // AMODE=0 (6 bits), SFREQ=0 (4 bits), RATE=0 (5 bits) + + // 13 zero trailing bits + 16 zero post-CRC bits = 75 bits + // after the sync = 13 bytes total. + // Easier: just call build_be_header equivalent through + // parse_frame_header on a synthesised buffer. + // Bytes layout (after sync): + // byte 4: FTYPE(1)=0 SHORT(5)=0 CRC_PRESENT(1)=0 NBLKS_hi(1)=0 -> 0b00000000 = 0x00 + // byte 5: NBLKS_lo(6)=000101 FSIZE_hi(2)=00 -> 0b00010100 = 0x14 + // byte 6: FSIZE_mid(8)=00010111 -> 0x17 (FSIZE-1 = 94 = 0b00_00000101_1110, hi 2 bits=00, mid 8=00010111? — recompute below) + // Rather than hand-bit-fiddle, sidestep the layout: use + // `parse_frame_header` on a 95-byte buffer we synthesise via + // the header.rs test helper indirectly by re-deriving the + // bit layout here. Simpler: directly construct via a tiny + // local builder. + fn push(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + let mut bv: Vec = Vec::new(); + push(&mut bv, 0x7FFE_8001, 32); + push(&mut bv, 0, 1); // ftype = termination + push(&mut bv, 0, 5); // sample_count_m1 + push(&mut bv, 0, 1); // crc_present + push(&mut bv, 5, 7); // nblks + push(&mut bv, 94, 14); // fsize-1 = 94 -> frame_size = 95 + push(&mut bv, 0, 6); // amode + push(&mut bv, 0, 4); // sfreq + push(&mut bv, 0, 5); // rate + push(&mut bv, 0, 13); // trailing flags + push(&mut bv, 0, 16); // post-CRC window + while bv.len() % 8 != 0 { + bv.push(false); + } + // Convert bit-vector to bytes (MSB-first). + for (i, chunk) in bv.chunks(8).enumerate() { + let mut b: u8 = 0; + for (k, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - k); + } + } + buf[i] = b; + } + // Fill the SUBFRAMES region (bytes 13..95) with a pattern so + // the payload-slice contents can be verified. + for byte in buf.iter_mut().skip(13) { + *byte = 0xCD; + } + + let mut it = iter_frames(&buf); + let view = it.next().expect("frame must yield").expect("must parse"); + assert_eq!(view.offset, 0); + assert_eq!(view.len, 95); + assert_eq!(view.header.frame_size_bytes, 95); + assert!(!view.header.crc_present); + assert_eq!(view.header.header_byte_length(), 13); + + let payload = view.payload(); + assert_eq!(payload.len(), 95 - 13); + assert!(payload.iter().all(|&b| b == 0xCD)); + // No more frames in the buffer. + assert!(it.next().is_none()); + } + + /// `FrameView::payload()` shifts by 2 bytes (15 vs 13) when + /// `crc_present == 1` because the optional HEADER_CRC slot + /// extends the header window from 104 to 120 bits. + #[test] + fn frame_view_payload_offset_shifts_when_crc_present() { + // Build a 95-byte crc-present termination frame. + let mut buf = vec![0u8; 95]; + fn push(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + let mut bv: Vec = Vec::new(); + push(&mut bv, 0x7FFE_8001, 32); + push(&mut bv, 0, 1); + push(&mut bv, 0, 5); + push(&mut bv, 1, 1); // crc_present = true + push(&mut bv, 5, 7); + push(&mut bv, 94, 14); + push(&mut bv, 0, 6); + push(&mut bv, 0, 4); + push(&mut bv, 0, 5); + push(&mut bv, 0, 13); + push(&mut bv, 0xABCD, 16); // header_crc + push(&mut bv, 0, 16); // post-CRC window + while bv.len() % 8 != 0 { + bv.push(false); + } + for (i, chunk) in bv.chunks(8).enumerate() { + let mut b: u8 = 0; + for (k, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - k); + } + } + buf[i] = b; + } + // Fill bytes 15..95 with a distinct pattern. + for byte in buf.iter_mut().skip(15) { + *byte = 0xEF; + } + + let view = iter_frames(&buf).next().unwrap().unwrap(); + assert!(view.header.crc_present); + assert_eq!(view.header.header_byte_length(), 15); + let payload = view.payload(); + assert_eq!(payload.len(), 95 - 15); + assert!(payload.iter().all(|&b| b == 0xEF)); + } + + // --------------------------------------------------------------- + // Round 151 — find_all_syncs() bulk-scan helper. + // + // Counterpart to find_next_sync() that returns every sync match in + // a buffer, useful for stream-integrity tooling that needs to know + // about every resync point up front rather than walking one at a + // time. + // --------------------------------------------------------------- + + #[test] + fn find_all_syncs_empty_buffer_returns_empty_vec() { + let buf: [u8; 0] = []; + assert!(find_all_syncs(&buf).is_empty()); + } + + #[test] + fn find_all_syncs_no_sync_returns_empty_vec() { + let buf = vec![0xAAu8; 64]; + assert!(find_all_syncs(&buf).is_empty()); + } + + #[test] + fn find_all_syncs_returns_single_match_for_single_sync() { + let mut buf = vec![0u8; 16]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); + let matches = find_all_syncs(&buf); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].offset, 0); + assert_eq!(matches[0].encoding, SyncWordEncoding::RawBigEndian); + } + + #[test] + fn find_all_syncs_mixed_raw_be_and_le_in_one_buffer() { + // Stream pattern: BE at 0, LE at 8, BE at 16, LE at 24. + let mut buf = vec![0xAAu8; 32]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); + buf[8..12].copy_from_slice(&RAW_LE_SYNC); + buf[16..20].copy_from_slice(&RAW_BE_SYNC); + buf[24..28].copy_from_slice(&RAW_LE_SYNC); + let matches = find_all_syncs(&buf); + assert_eq!(matches.len(), 4); + assert_eq!(matches[0].offset, 0); + assert_eq!(matches[0].encoding, SyncWordEncoding::RawBigEndian); + assert_eq!(matches[1].offset, 8); + assert_eq!(matches[1].encoding, SyncWordEncoding::RawLittleEndian); + assert_eq!(matches[2].offset, 16); + assert_eq!(matches[2].encoding, SyncWordEncoding::RawBigEndian); + assert_eq!(matches[3].offset, 24); + assert_eq!(matches[3].encoding, SyncWordEncoding::RawLittleEndian); + } + + #[test] + fn find_all_syncs_with_all_four_encodings() { + // Pack one of each documented sync encoding into a single + // buffer; find_all_syncs must report all four. + let mut buf = vec![0xAAu8; 48]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); + buf[8..12].copy_from_slice(&RAW_LE_SYNC); + buf[16..22].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xFA]); + buf[24..30].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF3, 0x07]); + let matches = find_all_syncs(&buf); + assert_eq!(matches.len(), 4); + assert_eq!(matches[0].encoding, SyncWordEncoding::RawBigEndian); + assert_eq!(matches[1].encoding, SyncWordEncoding::RawLittleEndian); + assert_eq!(matches[2].encoding, SyncWordEncoding::FourteenBitBigEndian); + assert_eq!( + matches[3].encoding, + SyncWordEncoding::FourteenBitLittleEndian + ); + } + + #[test] + fn find_all_syncs_consecutive_back_to_back_frames() { + // Two raw-BE syncs adjacent at offsets 0 and 4 — no gap. + let mut buf = vec![0u8; 16]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); + buf[4..8].copy_from_slice(&RAW_BE_SYNC); + let matches = find_all_syncs(&buf); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].offset, 0); + assert_eq!(matches[1].offset, 4); + } + + #[test] + fn find_all_syncs_walks_full_buffer_independent_of_starting_garbage() { + // 5 bytes of garbage, then BE sync at 5, more garbage, LE sync + // at 20, trailing tail of garbage. + let mut buf = vec![0xAAu8; 32]; + buf[5..9].copy_from_slice(&RAW_BE_SYNC); + buf[20..24].copy_from_slice(&RAW_LE_SYNC); + let matches = find_all_syncs(&buf); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].offset, 5); + assert_eq!(matches[1].offset, 20); + } + + /// Cross-check parity with `find_next_sync` looped from `offset+1` + /// — `find_all_syncs` must agree with the explicit loop on every + /// match. + #[test] + fn find_all_syncs_matches_find_next_sync_loop() { + let mut buf = vec![0xAAu8; 64]; + buf[3..7].copy_from_slice(&RAW_BE_SYNC); + buf[15..19].copy_from_slice(&RAW_LE_SYNC); + buf[40..44].copy_from_slice(&RAW_BE_SYNC); + + // find_next_sync loop reference. + let mut reference: Vec = Vec::new(); + let mut cursor = 0usize; + while let Some(m) = find_next_sync(&buf, cursor) { + reference.push(m); + cursor = m.offset + 1; + } + + let bulk = find_all_syncs(&buf); + assert_eq!(bulk, reference); + } + + // --------------------------------------------------------------- + // Round 151 — iter_frames coverage for raw-LE streams. + // + // The iterator's `frame_size_bytes`-based advance is documented as + // applying to "raw 16-bit encodings" (both RawBigEndian AND + // RawLittleEndian — the FSIZE field is the byte length of the + // raw 16-bit-per-word on-wire stream, which is byte-equivalent + // between BE and LE because the LE form is just a pairwise + // byte-swap of the BE form). The existing + // `multi_frame_iter.rs::iter_frames_walks_all_five_frames_in_fixture` + // test exercises the BE path against the ffmpeg fixture; the + // raw-LE path is exercised here by byte-swapping that fixture + // (synthesised inline so the iterator's LE walk is covered without + // adding a new fixture file). + // --------------------------------------------------------------- + + /// Build a multi-frame raw-LE byte buffer by byte-swapping a + /// raw-BE frame buffer pairwise. The frame layout (count, sizes, + /// SUBFRAMES contents) is preserved because raw-LE is defined by + /// the wiki as the 16-bit-word-swapped form of raw-BE. + fn build_raw_le_two_frame_stream() -> Vec { + // Two back-to-back 96-byte termination frames (NBLKS=5, + // FSIZE=96). We hand-build the BE form via the same bit- + // table the parser consumes and then word-swap to LE. + let mut be = Vec::with_capacity(2 * 96); + for _frame in 0..2 { + let mut bv: Vec = Vec::new(); + fn push(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + push(&mut bv, 0x7FFE_8001, 32); + push(&mut bv, 0, 1); // ftype = termination + push(&mut bv, 0, 5); // sample_count_m1 + push(&mut bv, 0, 1); // crc_present + push(&mut bv, 5, 7); // nblks + push(&mut bv, 95, 14); // fsize-1 = 95 -> frame_size = 96 + push(&mut bv, 0, 6); // amode + push(&mut bv, 0, 4); // sfreq + push(&mut bv, 0, 5); // rate + push(&mut bv, 0, 13); // trailing flags + push(&mut bv, 0, 16); // post-CRC window + while bv.len() % 8 != 0 { + bv.push(false); + } + // Convert MSB-first bit-vector to bytes. + let mut frame_bytes = vec![0u8; 96]; + for (i, chunk) in bv.chunks(8).enumerate() { + let mut b: u8 = 0; + for (k, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - k); + } + } + frame_bytes[i] = b; + } + // Fill bytes 13..96 (SUBFRAMES region) with a distinct + // per-frame pattern so payload checks can differentiate. + for byte in frame_bytes.iter_mut().skip(13) { + *byte = 0xCD; + } + be.extend_from_slice(&frame_bytes); + } + // Word-swap each 16-bit pair to produce the raw-LE form. + for pair in be.chunks_exact_mut(2) { + pair.swap(0, 1); + } + be + } + + #[test] + fn iter_frames_walks_raw_le_two_frame_stream() { + let buf = build_raw_le_two_frame_stream(); + // Sanity: starts with the canonical raw-LE sync. + assert_eq!(&buf[..4], &[0xFE, 0x7F, 0x01, 0x80]); + // Second frame's sync at offset 96. + assert_eq!(&buf[96..100], &[0xFE, 0x7F, 0x01, 0x80]); + + let frames: Vec<_> = iter_frames(&buf) + .collect::, _>>() + .expect("raw-LE multi-frame stream must walk"); + assert_eq!(frames.len(), 2); + + for (i, frame) in frames.iter().enumerate() { + assert_eq!( + frame.header.sync_word_encoding, + SyncWordEncoding::RawLittleEndian, + "frame {i} encoding", + ); + assert_eq!(frame.offset, i * 96); + assert_eq!(frame.len, 96); + assert_eq!(frame.header.frame_size_bytes, 96); + assert_eq!(frame.header.blocks_per_frame, 5); + assert_eq!(frame.header.frame_type, FrameType::Termination); + assert!(!frame.header.crc_present); + // The byte-length accessor returns the unpacked-bitstream + // (raw-BE) header byte count regardless of the on-wire + // sync encoding — 13 bytes when `crc_present == 0`. + assert_eq!(frame.header.header_byte_length(), 13); + } + } + + /// The raw-LE walk is robust to leading garbage in the same way + /// the raw-BE walk is — `find_next_sync` skips past unrelated + /// bytes and lands on the first LE sync. + #[test] + fn iter_frames_raw_le_handles_leading_garbage() { + let stream = build_raw_le_two_frame_stream(); + let mut prefixed = Vec::with_capacity(7 + stream.len()); + prefixed.extend_from_slice(&[0xAA; 7]); + prefixed.extend_from_slice(&stream); + + let frames: Vec<_> = iter_frames(&prefixed) + .collect::, _>>() + .expect("garbage-prefixed raw-LE stream must still walk"); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].offset, 7); + assert_eq!(frames[1].offset, 7 + 96); + for f in &frames { + assert_eq!( + f.header.sync_word_encoding, + SyncWordEncoding::RawLittleEndian + ); + } + } + + // ----------------------------------------------------------------- + // Round 159 — FrameIteratorResync. + // + // Error-tolerant counterpart to FrameIterator: a candidate sync + // whose subsequent header bits fail the structural NBLKS / FSIZE + // bounds (or whose declared frame_size_bytes overruns + // end-of-buffer) is treated as a false-positive sync rather than a + // hard fault; the iterator yields a ResyncEvent and continues + // scanning from offset + 1. Useful for walking partially-corrupted + // streams past malformed-sync patches. + // ----------------------------------------------------------------- + + /// Bit-vector helper used by the synthetic-frame builders below. + /// Pushes the low `width` bits of `value` MSB-first into `bv`. + fn push_bits(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + + /// Pack an MSB-first bool vector into bytes, panicking if the + /// length is not a multiple of 8. + fn bits_to_bytes(bv: &[bool]) -> Vec { + assert_eq!(bv.len() % 8, 0, "bit count must be multiple of 8"); + let mut out = vec![0u8; bv.len() / 8]; + for (i, chunk) in bv.chunks(8).enumerate() { + let mut b: u8 = 0; + for (k, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - k); + } + } + out[i] = b; + } + out + } + + /// Build a minimum-size (95-byte) termination raw-BE frame whose + /// SUBFRAMES region is filled with `fill`. Header fields chosen so + /// the parser accepts the frame. + fn build_min_frame_be(fill: u8) -> Vec { + let mut bv: Vec = Vec::new(); + push_bits(&mut bv, 0x7FFE_8001, 32); + push_bits(&mut bv, 0, 1); // ftype = termination + push_bits(&mut bv, 0, 5); // sample_count_m1 + push_bits(&mut bv, 0, 1); // crc_present + push_bits(&mut bv, 5, 7); // nblks = 5 + push_bits(&mut bv, 94, 14); // fsize-1 = 94 → frame_size = 95 + push_bits(&mut bv, 0, 6); // amode + push_bits(&mut bv, 0, 4); // sfreq + push_bits(&mut bv, 0, 5); // rate + push_bits(&mut bv, 0, 13); // trailing flags + push_bits(&mut bv, 0, 16); // post-CRC window + while bv.len() % 8 != 0 { + bv.push(false); + } + let mut buf = bits_to_bytes(&bv); + buf.resize(95, fill); + for byte in buf.iter_mut().skip(13) { + *byte = fill; + } + buf + } + + /// On a well-formed stream the resync walker is byte-for-byte + /// equivalent to the fail-fast iterator: every step yields `Ok` + /// and the frame views match. + #[test] + fn resync_walks_clean_stream_identically_to_fail_fast() { + // Build a 3-frame raw-BE stream by concatenating three + // build_min_frame_be calls. Each frame is 95 B → 3 × 95 = 285 B. + let mut stream = Vec::with_capacity(3 * 95); + for fill in [0x11u8, 0x22, 0x33] { + stream.extend_from_slice(&build_min_frame_be(fill)); + } + assert_eq!(stream.len(), 285); + + let strict: Vec> = iter_frames(&stream) + .collect::, _>>() + .expect("clean stream must walk"); + let resync: Vec> = iter_frames_resync(&stream) + .collect::, ResyncEvent>>() + .expect("clean stream must walk through resync iter too"); + assert_eq!(strict.len(), 3); + assert_eq!(resync.len(), 3); + for (a, b) in strict.iter().zip(resync.iter()) { + assert_eq!(a.offset, b.offset); + assert_eq!(a.len, b.len); + assert_eq!(a.header, b.header); + } + } + + /// A coincidental 4-byte raw-BE sync pattern in the SUBFRAMES + /// region of a preceding frame triggers a false-positive sync at + /// that offset. The fail-fast iterator can't see it (it advances + /// by frame_size_bytes), but a stand-alone sync planted at an + /// arbitrary offset followed by zero bytes will fail + /// structural-bound checks: the 7 NBLKS bits decode as 0 → after + /// the +1 increment that is < 5 → BlockCountOutOfRange. The + /// resync iterator must surface that as a `StructuralBoundFailed` + /// ResyncEvent and continue, recovering the real frame that + /// follows. + #[test] + fn resync_skips_false_positive_sync_in_garbage_and_recovers_real_frame() { + // Layout: [garbage with embedded false sync 0..32] + + // [real frame at offset 32, 95 B]. + let real = build_min_frame_be(0xCC); + let mut buf = vec![0xAAu8; 32]; + // Plant the canonical raw-BE sync at offset 8, followed by all + // zeros for the next ~11 bytes — the parser will read + // NBLKS == 0 → BlockCountOutOfRange. + buf[8..12].copy_from_slice(&RAW_BE_SYNC); + for byte in buf.iter_mut().take(32).skip(12) { + *byte = 0; + } + buf.extend_from_slice(&real); + // Sanity: fail-fast iter_frames terminates with an error at + // the false sync without recovering the real frame. + let mut strict = iter_frames(&buf); + match strict.next() { + Some(Err(Error::BlockCountOutOfRange { .. })) => {} + other => panic!("fail-fast must surface BlockCountOutOfRange, got {other:?}"), + } + assert!(strict.next().is_none(), "fail-fast iterator terminates"); + + // Resync iterator: one Err event for the false sync, then the + // real frame, then end. + let mut it = iter_frames_resync(&buf); + let first = it.next().expect("must yield event"); + match first { + Err(ResyncEvent { + offset: 8, + encoding: SyncWordEncoding::RawBigEndian, + cause: ResyncCause::StructuralBoundFailed(Error::BlockCountOutOfRange { .. }), + }) => {} + other => panic!("expected StructuralBoundFailed at offset 8, got {other:?}"), + } + let second = it.next().expect("must yield real frame"); + let view = second.expect("real frame must parse"); + assert_eq!(view.offset, 32); + assert_eq!(view.len, 95); + assert_eq!( + view.header.sync_word_encoding, + SyncWordEncoding::RawBigEndian + ); + assert_eq!(view.header.frame_size_bytes, 95); + assert!(it.next().is_none()); + } + + /// A false-positive sync at an offset whose declared frame size + /// overruns end-of-buffer must surface as + /// `FrameLengthOverrunsBuffer`, not terminate the iterator. After + /// the event the scan resumes past the spurious sync and finds the + /// next real frame (or ends naturally if none). + #[test] + fn resync_overrun_event_lets_iterator_continue() { + // Build a buffer: real frame at offset 0 (95 B); then plant a + // raw-BE sync at offset 96 with a header declaring a huge + // frame_size (overruns end-of-buffer); then the genuine next + // frame at offset 96 + 5 = 101. + // + // To engineer the "header parses OK but length overruns" case + // we use a real 95-byte termination frame but place it inside + // a buffer that is exactly 96+95 = 191 B starting at offset 96 + // — but if we plant the well-formed frame at offset 96, the + // resync iterator would correctly walk it. So instead we plant + // a HAND-CRAFTED header at offset 96 with FSIZE that exceeds + // remaining buffer space. + + let real = build_min_frame_be(0x11); + let mut buf = real.clone(); // frame at offset 0..95. + // 1 byte of garbage so the false sync is well-separated. + buf.push(0xAA); + let false_sync_off = buf.len(); // == 96 + // Craft a header with FSIZE that overruns the remaining buffer. + let total_after_false_sync = 50; // we'll make remaining = 50. + // Declared frame_size = 200 > 50. + let mut bv: Vec = Vec::new(); + push_bits(&mut bv, 0x7FFE_8001, 32); + push_bits(&mut bv, 0, 1); + push_bits(&mut bv, 0, 5); + push_bits(&mut bv, 0, 1); + push_bits(&mut bv, 5, 7); // nblks = 5 + push_bits(&mut bv, 199, 14); // fsize-1 = 199 → frame_size = 200 + push_bits(&mut bv, 0, 6); + push_bits(&mut bv, 0, 4); + push_bits(&mut bv, 0, 5); + push_bits(&mut bv, 0, 13); + push_bits(&mut bv, 0, 16); + while bv.len() % 8 != 0 { + bv.push(false); + } + let hdr_bytes = bits_to_bytes(&bv); + buf.extend_from_slice(&hdr_bytes); + // Pad with garbage until we have `total_after_false_sync` + // bytes after the false sync (so the header is fully readable + // — 13 B — but the declared 200-byte frame overruns). + while buf.len() - false_sync_off < total_after_false_sync { + buf.push(0xBB); + } + assert_eq!(buf.len() - false_sync_off, total_after_false_sync); + + // Walk with the resync iterator. + let events: Vec<_> = iter_frames_resync(&buf).collect(); + // Expect: Ok(real frame at 0), Err(FrameLengthOverrunsBuffer at + // 96, declared_len=200), then end (find_next_sync from 97 + // finds nothing — the canonical sync was only planted at 96). + assert_eq!(events.len(), 2); + let frame = events[0].as_ref().unwrap(); + assert_eq!(frame.offset, 0); + assert_eq!(frame.len, 95); + + match events[1] { + Err(ResyncEvent { + offset: 96, + encoding: SyncWordEncoding::RawBigEndian, + cause: ResyncCause::FrameLengthOverrunsBuffer { declared_len: 200 }, + }) => {} + ref other => panic!("expected overrun event at 96 with len 200, got {other:?}"), + } + } + + /// A 14-bit sync encountered by the resync iterator is reported + /// via `FourteenBitSyncSkipped` rather than terminating the walk. + /// The iterator continues past the 14-bit sync so subsequent raw + /// frames are still recovered. + #[test] + fn resync_skips_fourteen_bit_sync_and_keeps_walking() { + let real = build_min_frame_be(0xDD); + let mut buf = Vec::new(); + // 14-bit-BE sync at offset 0 (6 bytes, lower-14-bits match + // 0x1FFF / 0x2800 / 0x07Fx). + buf.extend_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xFA]); + // 2 bytes of garbage so the canonical raw-BE sync starts on a + // byte boundary the scanner will reach without colliding. + buf.extend_from_slice(&[0xAA, 0xAA]); + let real_off = buf.len(); + buf.extend_from_slice(&real); + + let mut it = iter_frames_resync(&buf); + match it.next() { + Some(Err(ResyncEvent { + offset: 0, + encoding: SyncWordEncoding::FourteenBitBigEndian, + cause: ResyncCause::FourteenBitSyncSkipped, + })) => {} + other => panic!("expected 14-bit-BE skip at offset 0, got {other:?}"), + } + // Next step recovers the real raw-BE frame. + let view = it.next().expect("must yield frame").expect("must parse"); + assert_eq!(view.offset, real_off); + assert_eq!( + view.header.sync_word_encoding, + SyncWordEncoding::RawBigEndian + ); + assert!(it.next().is_none()); + } + + /// Cursor progresses correctly across mixed events: a real frame + /// advances by frame_size_bytes; a ResyncEvent advances by exactly + /// one byte. + #[test] + fn resync_cursor_advances_one_byte_on_event_and_frame_size_on_ok() { + let real = build_min_frame_be(0x44); + let mut buf = vec![0xAAu8; 4]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); // false sync at 0. + // Pad so the false-sync header reads zeros → BlockCountOutOfRange. + buf.resize(20, 0); + // Real frame at offset 20. + let real_off = buf.len(); + buf.extend_from_slice(&real); + + let mut it = iter_frames_resync(&buf); + // Step 1: false sync at 0; cursor must advance to 1. + let _ = it.next(); + assert_eq!(it.cursor(), 1); + // Step 2: real frame at 20; cursor advances by frame_size_bytes. + let _ = it.next(); + assert_eq!(it.cursor(), real_off + 95); + // Step 3: no more syncs. + assert!(it.next().is_none()); + } + + /// An empty buffer yields nothing. + #[test] + fn resync_empty_buffer_yields_none() { + let buf: [u8; 0] = []; + assert!(iter_frames_resync(&buf).next().is_none()); + } + + /// A buffer with no sync at all yields nothing. + #[test] + fn resync_no_sync_yields_none() { + let buf = vec![0xAAu8; 64]; + assert!(iter_frames_resync(&buf).next().is_none()); + } + + /// Multiple consecutive false-positive raw-BE sync sequences are + /// each reported (in order) and the iterator finally terminates + /// when no more syncs exist. + #[test] + fn resync_multiple_consecutive_false_positives_each_reported() { + let mut buf = vec![0u8; 64]; + // Three false sync sequences at offsets 0, 16, 32 with all + // zeros following → each fails NBLKS bound. + for &off in &[0usize, 16, 32] { + buf[off..off + 4].copy_from_slice(&RAW_BE_SYNC); + } + let events: Vec<_> = iter_frames_resync(&buf).collect(); + assert_eq!(events.len(), 3); + for (i, &expected_off) in [0usize, 16, 32].iter().enumerate() { + match events[i] { + Err(ResyncEvent { + offset, + encoding: SyncWordEncoding::RawBigEndian, + cause: ResyncCause::StructuralBoundFailed(Error::BlockCountOutOfRange { .. }), + }) if offset == expected_off => {} + ref other => panic!("event {i} mismatch: {other:?}"), + } + } + } + + /// A genuine truncated tail surfaces as a single + /// `FrameLengthOverrunsBuffer` event with the declared length; + /// the iterator then ends because no subsequent sync exists in + /// the truncated buffer. + #[test] + fn resync_truncated_tail_surfaces_overrun_event_then_ends() { + // Build a valid-header buffer whose declared frame_size_bytes + // is 95 but the buffer holds only 50 bytes total. + let real = build_min_frame_be(0xEE); + let truncated = &real[..50]; + let events: Vec<_> = iter_frames_resync(truncated).collect(); + assert_eq!(events.len(), 1); + match events[0] { + Err(ResyncEvent { + offset: 0, + encoding: SyncWordEncoding::RawBigEndian, + cause: ResyncCause::FrameLengthOverrunsBuffer { declared_len: 95 }, + }) => {} + ref other => panic!("expected overrun event, got {other:?}"), + } + } + + /// A header that's truncated mid-bits (sync present, body too + /// short to read all 104 header bits) yields `HeaderEof`. The + /// iterator then ends because no subsequent sync follows. + #[test] + fn resync_truncated_header_surfaces_header_eof() { + // Sync + 4 bytes (so the parser sees 8 bytes total — far less + // than the 13-byte minimum header window). + let buf = [0x7F, 0xFE, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00]; + let events: Vec<_> = iter_frames_resync(&buf).collect(); + assert_eq!(events.len(), 1); + match events[0] { + Err(ResyncEvent { + offset: 0, + encoding: SyncWordEncoding::RawBigEndian, + cause: ResyncCause::HeaderEof, + }) => {} + ref other => panic!("expected HeaderEof, got {other:?}"), + } + } + + /// The convenience constructor `iter_frames_resync` is equivalent + /// to `FrameIteratorResync::new`. + #[test] + fn iter_frames_resync_matches_struct_new() { + let real = build_min_frame_be(0x55); + let a: Vec<_> = iter_frames_resync(&real).collect(); + let b: Vec<_> = FrameIteratorResync::new(&real).collect(); + assert_eq!(a.len(), b.len()); + for (x, y) in a.iter().zip(b.iter()) { + // FrameView lifetimes are anonymous; compare via fields. + match (x, y) { + (Ok(fx), Ok(fy)) => { + assert_eq!(fx.offset, fy.offset); + assert_eq!(fx.len, fy.len); + assert_eq!(fx.header, fy.header); + } + (Err(ex), Err(ey)) => assert_eq!(ex, ey), + _ => panic!("variants differ"), + } + } + } + + // ---------------------------------------------------------------- + // Round 165 — find_next_sync / find_all_syncs first-byte gate. + // + // The round-165 optimisation gates the multi-byte detect_sync() + // check behind a one-byte filter + // (`is_sync_first_byte_candidate`): only bytes 0x7F / 0xFE / 0x1F + // / 0xFF can start one of the four documented sync sequences. + // The tests below verify (a) the filter accepts exactly those + // four bytes, (b) the optimised scan agrees with a brute-force + // pre-round-165-style reference on every input, including + // pathological payloads packed with first-byte-candidate bytes + // whose multi-byte continuation is non-sync, and (c) the + // optimisation does not change the documented walk order or + // returned offsets. + // ---------------------------------------------------------------- + + /// Brute-force, pre-round-165-style reference scanner. Walks + /// every position 1-by-1 and calls `detect_sync` without a + /// first-byte gate. Used as the equivalence oracle for the + /// optimised `find_next_sync`. + fn reference_find_next_sync(bytes: &[u8], start: usize) -> Option { + use crate::header::detect_sync; + if start >= bytes.len() { + return None; + } + let mut i = start; + while i + 4 <= bytes.len() { + if let Ok(enc) = detect_sync(&bytes[i..]) { + return Some(SyncMatch { + offset: i, + encoding: enc, + }); + } + i += 1; + } + None + } + + fn reference_find_all_syncs(bytes: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut cursor = 0usize; + while let Some(m) = reference_find_next_sync(bytes, cursor) { + out.push(m); + cursor = m.offset + 1; + } + out + } + + /// The filter accepts exactly the four first bytes of the four + /// documented sync sequences. Every other byte (252 of 256) + /// must short-circuit. + #[test] + fn first_byte_candidate_accepts_exactly_four_bytes() { + let mut accepted: Vec = Vec::new(); + for b in 0u16..=255 { + if is_sync_first_byte_candidate(b as u8) { + accepted.push(b as u8); + } + } + accepted.sort(); + assert_eq!(accepted, vec![0x1F, 0x7F, 0xFE, 0xFF]); + } + + /// First-byte gate must accept the actual first byte of every + /// documented sync prefix. (Belt-and-braces; the previous test + /// asserts the same thing inversely, but spelling it out per + /// encoding makes regressions easier to read.) + #[test] + fn first_byte_candidate_accepts_documented_sync_prefixes() { + assert!(is_sync_first_byte_candidate(0x7F)); // raw BE + assert!(is_sync_first_byte_candidate(0xFE)); // raw LE + assert!(is_sync_first_byte_candidate(0x1F)); // 14-bit BE + assert!(is_sync_first_byte_candidate(0xFF)); // 14-bit LE + // Spot-check a few adjacent bytes that look similar but are + // explicitly NOT sync prefixes. + assert!(!is_sync_first_byte_candidate(0x7E)); + assert!(!is_sync_first_byte_candidate(0xFD)); + assert!(!is_sync_first_byte_candidate(0x80)); + assert!(!is_sync_first_byte_candidate(0x00)); + } + + /// Optimised `find_next_sync` agrees with the pre-round-165 + /// reference on every position of a buffer densely packed with + /// first-byte candidates whose multi-byte continuation is + /// deliberately non-sync. The first-byte gate must NOT smuggle + /// in false-positive matches: positions where bytes[i] is one + /// of {0x7F, 0xFE, 0x1F, 0xFF} but the following 3-5 bytes do + /// not match the full sync sequence must still return `None` + /// (or the next genuine sync further along). + #[test] + fn find_next_sync_matches_pre_optimization_reference_on_candidate_dense_payload() { + // Build a 256 B buffer where every fourth byte is a sync + // first-byte candidate but the continuation never matches. + let mut buf = vec![0u8; 256]; + let candidates = [0x7Fu8, 0xFE, 0x1F, 0xFF]; + for (i, b) in buf.iter_mut().enumerate() { + if i % 4 == 0 { + *b = candidates[(i / 4) % 4]; + } else { + // Bytes 1..4 deliberately != real continuation bytes. + *b = 0x55; + } + } + // Reference and optimised must both return None. + assert_eq!(find_next_sync(&buf, 0), None); + assert_eq!(reference_find_next_sync(&buf, 0), None); + + // Now embed a real raw-BE sync at offset 100 and confirm + // both implementations find it at the same offset with the + // same encoding tag. + buf[100..104].copy_from_slice(&RAW_BE_SYNC); + let opt = find_next_sync(&buf, 0).unwrap(); + let r = reference_find_next_sync(&buf, 0).unwrap(); + assert_eq!(opt, r); + assert_eq!(opt.offset, 100); + assert_eq!(opt.encoding, SyncWordEncoding::RawBigEndian); + } + + /// Cross-check the optimised `find_next_sync` against the + /// reference on a deterministic pseudo-random buffer. The LCG + /// seed is fixed so the test is reproducible. + #[test] + fn find_next_sync_matches_reference_on_pseudo_random_buffer() { + // 4 KB linear-congruential pseudo-random payload (Knuth's + // MMIX-friendly multiplier). + let mut buf = vec![0u8; 4096]; + let mut state: u64 = 0xDEAD_BEEF_CAFE_BABE; + for b in buf.iter_mut() { + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + *b = (state >> 32) as u8; + } + // Sweep every possible start offset and verify per-call agreement. + for start in 0..buf.len() { + assert_eq!( + find_next_sync(&buf, start), + reference_find_next_sync(&buf, start), + "find_next_sync diverged at start={start}" + ); + } + } + + /// Cross-check `find_all_syncs` against the brute-force + /// reference on the same pseudo-random buffer plus several + /// embedded real syncs. Both must agree on the full list of + /// (offset, encoding) pairs. + #[test] + fn find_all_syncs_matches_reference_on_random_buffer_with_embedded_syncs() { + let mut buf = vec![0u8; 4096]; + let mut state: u64 = 0x0123_4567_89AB_CDEF; + for b in buf.iter_mut() { + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + *b = (state >> 32) as u8; + } + // Embed real syncs at known positions across all four + // encodings. + buf[100..104].copy_from_slice(&RAW_BE_SYNC); + buf[500..504].copy_from_slice(&RAW_LE_SYNC); + // 14-bit BE prefix per wiki: `1F FF E8 00 07 F?`. + buf[1000..1006].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF0]); + // 14-bit LE prefix per wiki: `FF 1F 00 E8 F? 07`. + buf[2000..2006].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF0, 0x07]); + + let opt = find_all_syncs(&buf); + let r = reference_find_all_syncs(&buf); + assert_eq!(opt, r, "find_all_syncs diverged from reference"); + // Sanity: the four embedded syncs are recovered with the + // right encodings. + let pairs: Vec<(usize, SyncWordEncoding)> = + opt.iter().map(|m| (m.offset, m.encoding)).collect(); + assert!(pairs.contains(&(100, SyncWordEncoding::RawBigEndian))); + assert!(pairs.contains(&(500, SyncWordEncoding::RawLittleEndian))); + assert!(pairs.contains(&(1000, SyncWordEncoding::FourteenBitBigEndian))); + assert!(pairs.contains(&(2000, SyncWordEncoding::FourteenBitLittleEndian))); + } + + /// First-byte gate must not change the answer when the input is + /// densely packed with `0xFF` bytes (a common payload pattern in + /// silent-encoded audio): every position has a first-byte + /// candidate but very few have a valid sync continuation. + #[test] + fn find_next_sync_handles_all_ones_payload_with_one_embedded_sync() { + let mut buf = vec![0xFFu8; 256]; + // Embed a real raw-LE sync at offset 50. + buf[50..54].copy_from_slice(&RAW_LE_SYNC); + let opt = find_next_sync(&buf, 0).unwrap(); + let r = reference_find_next_sync(&buf, 0).unwrap(); + assert_eq!(opt, r); + assert_eq!(opt.offset, 50); + assert_eq!(opt.encoding, SyncWordEncoding::RawLittleEndian); + } + + /// All-zero payload (no first-byte candidates anywhere) returns + /// `None` from both implementations after a single full pass. + /// Confirms the gate's early-exit path doesn't infinite-loop or + /// skip end-of-buffer bookkeeping. + #[test] + fn find_next_sync_handles_all_zero_payload() { + let buf = vec![0u8; 4096]; + assert_eq!(find_next_sync(&buf, 0), None); + assert_eq!(reference_find_next_sync(&buf, 0), None); + } + + /// Sweep `start` across every offset of a moderately-sized + /// buffer containing two real syncs. The optimised and + /// reference scanners must agree on the result for every start. + #[test] + fn find_next_sync_start_sweep_matches_reference_with_two_real_syncs() { + let mut buf = vec![0xAAu8; 200]; + buf[20..24].copy_from_slice(&RAW_BE_SYNC); + buf[100..104].copy_from_slice(&RAW_LE_SYNC); + for start in 0..buf.len() { + assert_eq!( + find_next_sync(&buf, start), + reference_find_next_sync(&buf, start), + "divergence at start={start}" + ); + } + } + + // --------------------------------------------------------------- + // Round 179 — SyncWordEncoding::sync_byte_length / + // SyncMatch::sync_byte_length / sync_byte_range / + // SyncIterator + iter_syncs + // --------------------------------------------------------------- + + /// Wiki sync table directly enumerates the four sync sequences. + /// Length 4 for the two raw encodings (`7F FE 80 01` / + /// `FE 7F 01 80`); length 6 for the two 14-bit-packed encodings + /// (`1F FF E8 00 07 Fx` / `FF 1F 00 E8 Fx 07`). + #[test] + fn sync_word_encoding_byte_length_matches_wiki_sync_table() { + assert_eq!(SyncWordEncoding::RawBigEndian.sync_byte_length(), 4); + assert_eq!(SyncWordEncoding::RawLittleEndian.sync_byte_length(), 4); + assert_eq!(SyncWordEncoding::FourteenBitBigEndian.sync_byte_length(), 6); + assert_eq!( + SyncWordEncoding::FourteenBitLittleEndian.sync_byte_length(), + 6 + ); + } + + /// `is_raw_16bit` accepts exactly the two raw encodings; + /// `is_14bit_packed` accepts exactly the other two. The two + /// predicates are mutually exclusive and jointly exhaustive over + /// the documented sync encodings. + #[test] + fn sync_word_encoding_raw_vs_packed_predicates_partition_the_enum() { + for enc in [ + SyncWordEncoding::RawBigEndian, + SyncWordEncoding::RawLittleEndian, + SyncWordEncoding::FourteenBitBigEndian, + SyncWordEncoding::FourteenBitLittleEndian, + ] { + assert_ne!( + enc.is_raw_16bit(), + enc.is_14bit_packed(), + "exactly one predicate must hold for {enc:?}" + ); + } + assert!(SyncWordEncoding::RawBigEndian.is_raw_16bit()); + assert!(SyncWordEncoding::RawLittleEndian.is_raw_16bit()); + assert!(!SyncWordEncoding::FourteenBitBigEndian.is_raw_16bit()); + assert!(!SyncWordEncoding::FourteenBitLittleEndian.is_raw_16bit()); + assert!(SyncWordEncoding::FourteenBitBigEndian.is_14bit_packed()); + assert!(SyncWordEncoding::FourteenBitLittleEndian.is_14bit_packed()); + } + + /// `SyncMatch::sync_byte_length` delegates to the encoding; the + /// resulting half-open range carries the expected number of + /// bytes for each of the four documented encodings. + #[test] + fn sync_match_sync_byte_range_carries_wiki_documented_byte_count() { + let cases = [ + (SyncWordEncoding::RawBigEndian, 4usize), + (SyncWordEncoding::RawLittleEndian, 4), + (SyncWordEncoding::FourteenBitBigEndian, 6), + (SyncWordEncoding::FourteenBitLittleEndian, 6), + ]; + for (enc, expected_len) in cases { + let m = SyncMatch { + offset: 17, + encoding: enc, + }; + assert_eq!(m.sync_byte_length(), expected_len); + let r = m.sync_byte_range(); + assert_eq!(r.start, 17); + assert_eq!(r.end, 17 + expected_len); + } + } + + /// `sync_byte_range` lets the caller slice the matched bytes + /// straight out of the input. For a raw-BE sync at offset 0 + /// that slice is `7F FE 80 01`. + #[test] + fn sync_match_sync_byte_range_slices_raw_be_sync_bytes() { + let mut buf = vec![0u8; 16]; + buf[..4].copy_from_slice(&RAW_BE_SYNC); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(&buf[m.sync_byte_range()], &RAW_BE_SYNC); + } + + /// `sync_byte_range` for a 14-bit-BE sync at offset 5 reproduces + /// the wiki's `1F FF E8 00 07 F0` prefix. + #[test] + fn sync_match_sync_byte_range_slices_14bit_be_sync_bytes() { + let mut buf = vec![0u8; 32]; + buf[5..11].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF0]); + let m = find_next_sync(&buf, 0).unwrap(); + assert_eq!(m.encoding, SyncWordEncoding::FourteenBitBigEndian); + assert_eq!(m.sync_byte_range(), 5..11); + assert_eq!( + &buf[m.sync_byte_range()], + &[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF0] + ); + } + + /// `iter_syncs` and `find_all_syncs` must agree element-by-element + /// on a buffer that contains all four documented sync encodings. + /// This is the streaming/bulk equivalence contract — collect() + /// of the iterator equals the vector returned by the bulk helper. + #[test] + fn iter_syncs_collects_to_same_vec_as_find_all_syncs_on_mixed_encoding_buffer() { + let mut buf = vec![0u8; 64]; + buf[2..6].copy_from_slice(&RAW_BE_SYNC); + buf[12..16].copy_from_slice(&RAW_LE_SYNC); + buf[24..30].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF5]); + buf[40..46].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF7, 0x07]); + let bulk = find_all_syncs(&buf); + let streamed: Vec = iter_syncs(&buf).collect(); + assert_eq!(bulk, streamed); + assert_eq!(streamed.len(), 4); + assert_eq!(streamed[0].encoding, SyncWordEncoding::RawBigEndian); + assert_eq!(streamed[1].encoding, SyncWordEncoding::RawLittleEndian); + assert_eq!(streamed[2].encoding, SyncWordEncoding::FourteenBitBigEndian); + assert_eq!( + streamed[3].encoding, + SyncWordEncoding::FourteenBitLittleEndian + ); + } + + /// Streaming + bulk agree on an empty-result buffer (no syncs). + /// The iterator yields `None` on the first `next()` call. + #[test] + fn iter_syncs_returns_none_on_buffer_without_any_syncs() { + let buf = vec![0xAAu8; 256]; + let mut it = iter_syncs(&buf); + assert!(it.next().is_none()); + assert_eq!(find_all_syncs(&buf), Vec::::new()); + } + + /// `take(N)` correctly limits the iterator without forcing a + /// full scan — the next call after the take window stops yielding + /// even if more syncs exist downstream. + #[test] + fn iter_syncs_take_window_stops_after_n_matches() { + let mut buf = vec![0u8; 256]; + // Plant five raw-BE syncs at 10, 30, 50, 70, 90. + for off in [10usize, 30, 50, 70, 90] { + buf[off..off + 4].copy_from_slice(&RAW_BE_SYNC); + } + let first_three: Vec = iter_syncs(&buf).take(3).collect(); + assert_eq!(first_three.len(), 3); + assert_eq!(first_three[0].offset, 10); + assert_eq!(first_three[1].offset, 30); + assert_eq!(first_three[2].offset, 50); + } + + /// `filter` combinator: select only the raw-16-bit syncs from a + /// mixed-encoding buffer using `SyncWordEncoding::is_raw_16bit`. + #[test] + fn iter_syncs_filter_by_is_raw_16bit_excludes_14bit_matches() { + let mut buf = vec![0u8; 64]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); + buf[10..16].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF1]); + buf[20..24].copy_from_slice(&RAW_LE_SYNC); + buf[30..36].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF2, 0x07]); + let raws: Vec = iter_syncs(&buf) + .filter(|m| m.encoding.is_raw_16bit()) + .collect(); + assert_eq!(raws.len(), 2); + assert!(raws.iter().all(|m| m.encoding.is_raw_16bit())); + assert_eq!(raws[0].offset, 0); + assert_eq!(raws[1].offset, 20); + } + + /// `SyncIterator::cursor()` exposes the scan position. After + /// yielding the only match in the buffer, the cursor advances to + /// `offset + 1`; after the iterator is exhausted, it sits at the + /// position `find_next_sync` gave up at. + #[test] + fn sync_iterator_cursor_reflects_scan_position() { + let mut buf = vec![0u8; 32]; + buf[10..14].copy_from_slice(&RAW_BE_SYNC); + let mut it = iter_syncs(&buf); + assert_eq!(it.cursor(), 0); + let m = it.next().unwrap(); + assert_eq!(m.offset, 10); + // After yielding the match at offset 10, the cursor advanced + // by one so the next scan starts at offset 11 (the + // non-overlapping resume position documented for + // find_all_syncs). + assert_eq!(it.cursor(), 11); + assert!(it.next().is_none()); + } + + /// `iter_syncs` agrees with the reference `find_all_syncs` on a + /// 4 KB pseudo-random buffer with four embedded real syncs (one + /// of each encoding). Equivalence is checked element-by-element + /// so the streaming iterator inherits the bulk helper's + /// reference-validation coverage. + #[test] + fn iter_syncs_matches_reference_on_pseudo_random_buffer_with_embedded_syncs() { + let mut buf = vec![0u8; 4096]; + let mut state: u64 = 0x4242_4242_4242_4242; + for b in buf.iter_mut() { + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + *b = (state >> 32) as u8; + } + buf[200..204].copy_from_slice(&RAW_BE_SYNC); + buf[800..804].copy_from_slice(&RAW_LE_SYNC); + buf[1500..1506].copy_from_slice(&[0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF2]); + buf[3000..3006].copy_from_slice(&[0xFF, 0x1F, 0x00, 0xE8, 0xF4, 0x07]); + let streamed: Vec = iter_syncs(&buf).collect(); + let reference = reference_find_all_syncs(&buf); + assert_eq!(streamed, reference); + } + + // --------------------------------------------------------------- + // Round 192 — iter_frames_14bit (14-bit container-stream walker) + // + // Each test builds a raw-BE frame (95 bytes, the minimum FSIZE+1 + // the spec allows), packs it through `pack_16bit_to_14bit` into a + // 14-bit-packed container buffer, and exercises the iterator + // against the resulting bytes. The container-byte advance is + // verified to equal `frame_size_container_bytes(encoding)` + // (= 110 bytes for FSIZE+1 = 95: ceil(95 * 8 / 14) * 2 = 110). + // --------------------------------------------------------------- + + use crate::{pack_16bit_to_14bit, FourteenBitByteOrder}; + + /// Build a 95-byte raw-BE single-frame buffer: FTYPE=0 + /// (termination), SHORT=0, CRC_PRESENT=0, NBLKS=5, FSIZE-1=94 (=> + /// frame_size_bytes = 95), all other fields zero. Mirrors the + /// hand-built buffers used by the round-138 payload tests above. + fn build_minimum_raw_be_frame() -> Vec { + fn push(bv: &mut Vec, value: u32, width: u32) { + for i in (0..width).rev() { + bv.push(((value >> i) & 1) == 1); + } + } + let mut bv: Vec = Vec::new(); + push(&mut bv, 0x7FFE_8001, 32); + push(&mut bv, 0, 1); // ftype = termination + push(&mut bv, 0, 5); + push(&mut bv, 0, 1); // crc_present + push(&mut bv, 5, 7); // nblks + push(&mut bv, 94, 14); // fsize-1 = 94 -> frame_size = 95 + push(&mut bv, 0, 6); // amode + push(&mut bv, 0, 4); // sfreq + push(&mut bv, 0, 5); // rate + push(&mut bv, 0, 13); + push(&mut bv, 0, 16); // post-CRC window + while bv.len() % 8 != 0 { + bv.push(false); + } + let mut buf = vec![0u8; 95]; + for (i, chunk) in bv.chunks(8).enumerate() { + let mut b: u8 = 0; + for (k, bit) in chunk.iter().enumerate() { + if *bit { + b |= 1 << (7 - k); + } + } + buf[i] = b; + } + // Fill SUBFRAMES region with a distinctive pattern so the + // container-domain frame slice can be cross-checked through + // a round-trip unpack later. + for byte in buf.iter_mut().skip(13) { + *byte = 0xC1; + } + buf + } + + /// `iter_frames_14bit` parses a single 14-bit-BE frame and + /// reports the round-189 container-byte advance. + #[test] + fn iter_frames_14bit_walks_single_be_frame() { + let raw = build_minimum_raw_be_frame(); + let (packed, _bits) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + // 95 logical bytes => ceil(95 * 8 / 14) = 55 container words + // => 110 container bytes. + assert_eq!(packed.len(), 110); + let mut it = iter_frames_14bit(&packed); + let view = it.next().expect("frame must yield").expect("must parse"); + assert_eq!(view.offset, 0); + assert_eq!(view.len, 110); + assert_eq!(view.data.len(), 110); + assert_eq!( + view.header.sync_word_encoding, + SyncWordEncoding::FourteenBitBigEndian + ); + assert_eq!(view.header.frame_size_bytes, 95); + assert_eq!(view.header.blocks_per_frame, 5); + assert_eq!(view.header.frame_type, FrameType::Termination); + // No further frames. + assert!(it.next().is_none()); + } + + /// Same single-frame round-trip via the LE container order. + #[test] + fn iter_frames_14bit_walks_single_le_frame() { + let raw = build_minimum_raw_be_frame(); + let (packed, _bits) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::LittleEndian); + assert_eq!(packed.len(), 110); + let view = iter_frames_14bit(&packed) + .next() + .expect("frame must yield") + .expect("must parse"); + assert_eq!(view.offset, 0); + assert_eq!(view.len, 110); + assert_eq!( + view.header.sync_word_encoding, + SyncWordEncoding::FourteenBitLittleEndian + ); + assert_eq!(view.header.frame_size_bytes, 95); + } + + /// Two back-to-back 14-bit-BE frames: the iterator must advance + /// by exactly `frame_size_container_bytes(enc) = 110` between + /// frames and yield both. + #[test] + fn iter_frames_14bit_walks_two_back_to_back_be_frames() { + let raw = build_minimum_raw_be_frame(); + let (packed_one, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + // Concatenate: two back-to-back container-packed frames. + let mut stream = Vec::new(); + stream.extend_from_slice(&packed_one); + stream.extend_from_slice(&packed_one); + assert_eq!(stream.len(), 220); + + let frames: Vec<_> = iter_frames_14bit(&stream) + .collect::>>() + .expect("both frames must parse"); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].offset, 0); + assert_eq!(frames[0].len, 110); + assert_eq!(frames[1].offset, 110); + assert_eq!(frames[1].len, 110); + // Same encoding through both steps. + assert_eq!( + frames[1].header.sync_word_encoding, + SyncWordEncoding::FourteenBitBigEndian + ); + } + + /// Leading garbage before the first 14-bit sync must resync + /// rather than terminate, matching `iter_frames`'s contract for + /// raw streams. + #[test] + fn iter_frames_14bit_handles_leading_garbage_before_first_sync() { + let raw = build_minimum_raw_be_frame(); + let (packed, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + let mut buf: Vec = vec![0xAA; 17]; + buf.extend_from_slice(&packed); + let view = iter_frames_14bit(&buf) + .next() + .expect("first frame must yield") + .expect("must parse after resync"); + assert_eq!(view.offset, 17); + assert_eq!(view.len, 110); + } + + /// A raw 16-bit sync at the cursor is out-of-domain for this + /// iterator: it yields `Error::UnsupportedRaw16Bit` and + /// terminates, mirroring the round-6 `iter_frames` behaviour on + /// 14-bit syncs in the other direction. + #[test] + fn iter_frames_14bit_rejects_raw_16bit_sync() { + let mut buf = vec![0u8; 32]; + buf[0..4].copy_from_slice(&RAW_BE_SYNC); + let mut it = iter_frames_14bit(&buf); + match it.next() { + Some(Err(Error::UnsupportedRaw16Bit)) => {} + other => panic!("expected UnsupportedRaw16Bit, got {other:?}"), + } + assert!(it.next().is_none(), "iterator terminates after rejection"); + } + + /// An empty buffer yields no frames. + #[test] + fn iter_frames_14bit_empty_buffer_yields_nothing() { + let buf: [u8; 0] = []; + let mut it = iter_frames_14bit(&buf); + assert!(it.next().is_none()); + } + + /// A buffer that contains no sync at all yields no frames. + #[test] + fn iter_frames_14bit_no_sync_yields_nothing() { + let buf = vec![0xAAu8; 256]; + let mut it = iter_frames_14bit(&buf); + assert!(it.next().is_none()); + } + + /// A truncated 14-bit-BE frame where the declared container span + /// runs past end-of-buffer must report `Error::UnexpectedEof` on + /// the truncation, just like `iter_frames` for raw-16-bit. + #[test] + fn iter_frames_14bit_truncated_tail_reports_eof() { + let raw = build_minimum_raw_be_frame(); + let (packed, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + // Truncate to 100 container bytes (< the 110 the header + // declares for FSIZE+1 = 95). + let truncated = &packed[..100]; + let mut it = iter_frames_14bit(truncated); + match it.next() { + Some(Err(Error::UnexpectedEof)) => {} + other => panic!("expected UnexpectedEof on truncation, got {other:?}"), + } + } + + /// Cross-check: feeding the iterator's `data` slice (the + /// container-byte window) back into [`parse_frame_header_14bit`] + /// recovers the same header. This proves the iterator's window + /// is correctly sized for the parser's input contract. + #[test] + fn iter_frames_14bit_data_slice_round_trips_through_parser() { + let raw = build_minimum_raw_be_frame(); + let (packed, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + let view = iter_frames_14bit(&packed).next().unwrap().unwrap(); + let reparsed = parse_frame_header_14bit(view.data).unwrap(); + assert_eq!(reparsed, view.header); + } + + /// `FrameIterator14::cursor()` advances by exactly + /// `frame_size_container_bytes` after a successful step (BE + /// container). + #[test] + fn iter_frames_14bit_cursor_advances_by_container_byte_count() { + let raw = build_minimum_raw_be_frame(); + let (packed, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + let mut stream = Vec::new(); + stream.extend_from_slice(&packed); + stream.extend_from_slice(&packed); + + let mut it = iter_frames_14bit(&stream); + assert_eq!(it.cursor(), 0); + it.next().unwrap().unwrap(); + assert_eq!(it.cursor(), 110); + it.next().unwrap().unwrap(); + assert_eq!(it.cursor(), 220); + assert!(it.next().is_none()); + } +} diff --git a/crates/vendor/oxideav-dts/src/join_scale.rs b/crates/vendor/oxideav-dts/src/join_scale.rs new file mode 100644 index 00000000..4d0090c2 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/join_scale.rs @@ -0,0 +1,153 @@ +//! §D.3 Scale Factor table for Joint Intensity Coding (`JScaleTbl`), +//! the look-up the DTS Core §5.4.1 side-information `JOIN_SCALES` walk +//! feeds when a channel enables joint-intensity coding (`JOINX[ch] > 0`). +//! +//! Transcribed verbatim from ETSI TS 102 114 V1.3.1 (2011-08) Annex D +//! §D.3 "Scale Factor for Joint Intensity Coding" (staged at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`, PDF +//! p.195). The printed table is laid out as four index/value groups per +//! row (indices `0..=31`, `32..=63`, `64..=95`, `96..=128`); this module +//! preserves the single `Scale Factor` column, re-indexed row-major over +//! the full `0..=128` range. +//! +//! Per the §5.4.1 Table 5-28 pseudocode the `JOIN_SCALES` walk decodes +//! one `QSCALES` quantization index per joint sub-band, biases it by 64, +//! and looks the biased index up in this table: +//! +//! ```text +//! nQSelect = JOIN_SHUFF[ch]; // 3-bit code-book selector +//! for (n = nSUBS[ch]; n < nSUBS[nSourceCh]; n++) { +//! QSCALES.ppQ[nQSelect]->InverseQ(InputFrame, nJScale); +//! nJScale = nJScale + 64; // bias +//! JScaleTbl.LookUp(nJScale, JOIN_SCALES[ch][n]); +//! } +//! ``` +//! +//! The resulting `JOIN_SCALES[ch][n]` scalar multiplies the sub-band +//! samples copied from the source channel (`JOINX[ch] - 1`) to the +//! current channel during the §C.2.3 joint-subband reconstruction. +//! +//! Index `64` maps to the unity scale factor `1.0`, confirming the bias: +//! a `QSCALES` quantization index of `0` (the differential zero) resolves +//! to `1.0`, i.e. "copy the source sub-band unchanged". +//! +//! This module is feature-independent (no `oxideav-core` dep), so it is +//! available under both the default and `--no-default-features` builds. + +/// Number of entries in the §D.3 joint-intensity scale table — +/// `0..=128`, i.e. 129 entries. The `JOIN_SCALES` walk biases a +/// `QSCALES` index by 64 before indexing, so the reachable index range +/// depends on the code-book symbol range. +pub const JOIN_SCALE_LEN: usize = 129; + +/// The §D.3 unity-gain index (`JOIN_SCALES == 1.0`). A `QSCALES` symbol +/// of `0` biased by 64 lands here, meaning "copy the source sub-band +/// with no scaling". +pub const JOIN_SCALE_UNITY_INDEX: usize = 64; + +/// §D.3 "Scale Factor for Joint Intensity Coding" table (`JScaleTbl`), +/// indexed by the biased `nJScale` (`InverseQ` symbol + 64). Entry `i` +/// is the linear scale factor applied to a sub-band sample copied from +/// the source channel to a jointly-coded channel (see [`join_scale`]). +/// +/// Transcribed from ETSI TS 102 114 V1.3.1 §D.3, "Scale Factor" column, +/// indices `0..=128`. +pub static JOIN_SCALE_FACTOR: [f64; JOIN_SCALE_LEN] = [ + 0.025088, 0.026624, 0.02816, 0.029824, 0.031616, 0.033472, 0.035456, 0.037568, 0.039808, + 0.042176, 0.044672, 0.047296, 0.050112, 0.05312, 0.056256, 0.059584, 0.063104, 0.066816, + 0.070784, 0.075008, 0.079424, 0.08416, 0.089152, 0.0944, 0.099968, 0.10592, 0.112192, 0.118848, + 0.125888, 0.133376, 0.141248, 0.149632, 0.158464, 0.167872, 0.177856, 0.188352, 0.199552, + 0.211328, 0.223872, 0.23712, 0.2512, 0.266048, 0.281856, 0.29856, 0.316224, 0.334976, 0.354816, + 0.375808, 0.39808, 0.421696, 0.446656, 0.473152, 0.501184, 0.53088, 0.562368, 0.595648, + 0.630976, 0.668352, 0.707968, 0.749888, 0.794304, 0.841408, 0.891264, 0.944064, 1.0, 1.05926, + 1.12205, 1.18848, 1.25894, 1.3335, 1.41254, 1.49626, 1.5849, 1.67878, 1.7783, 1.88365, 1.99526, + 2.11347, 2.23872, 2.37139, 2.51187, 2.66074, 2.81837, 2.98541, 3.1623, 3.34963, 3.54816, + 3.7584, 3.98106, 4.21696, 4.46682, 4.73152, 5.0119, 5.30886, 5.62342, 5.95661, 6.30957, + 6.68346, 7.07949, 7.49894, 7.9433, 8.41395, 8.91251, 9.44064, 10.0, 10.5925, 11.2202, 11.885, + 12.5892, 13.3352, 14.1254, 14.9624, 15.849, 16.788, 17.7828, 18.8365, 19.9526, 21.1349, + 22.3872, 23.7137, 25.1188, 26.6072, 28.1838, 29.8538, 31.6228, 33.4965, 35.4813, 37.5837, + 39.8107, +]; + +/// Look up the §D.3 joint-intensity scale factor for a biased index +/// `nJScale` (the `QSCALES` `InverseQ` symbol plus the fixed `+64` +/// bias), returning the linear scale factor `JOIN_SCALES[ch][n]`. +/// +/// Returns `None` when `n_j_scale` falls outside `0..=128` — a +/// well-formed stream keeps the biased index inside the table by +/// construction, so an out-of-range index signals a corrupt or +/// misaligned bit stream rather than a silent clamp. +#[must_use] +pub fn join_scale(n_j_scale: i32) -> Option { + if !(0..JOIN_SCALE_LEN as i32).contains(&n_j_scale) { + return None; + } + Some(JOIN_SCALE_FACTOR[n_j_scale as usize]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn table_has_129_entries() { + assert_eq!(JOIN_SCALE_FACTOR.len(), 129); + assert_eq!(JOIN_SCALE_LEN, 129); + } + + #[test] + fn unity_at_index_64() { + // §D.3: index 64 -> Scale Factor 1.0. The +64 bias means a + // QSCALES symbol of 0 resolves to unity ("copy unchanged"). + assert_eq!(join_scale(64), Some(1.0)); + assert_eq!(JOIN_SCALE_UNITY_INDEX, 64); + } + + #[test] + fn anchor_rows_match_spec() { + // Verbatim §D.3 anchor values from the staged PDF. + assert_eq!(join_scale(0), Some(0.025088)); + assert_eq!(join_scale(32), Some(0.158464)); + assert_eq!(join_scale(64), Some(1.0)); + assert_eq!(join_scale(96), Some(6.30957)); + assert_eq!(join_scale(104), Some(10.0)); + assert_eq!(join_scale(128), Some(39.8107)); + } + + #[test] + fn out_of_range_returns_none() { + assert_eq!(join_scale(-1), None); + assert_eq!(join_scale(129), None); + assert_eq!(join_scale(1000), None); + } + + #[test] + fn table_is_strictly_monotone_increasing() { + // The §D.3 scale factor rises monotonically with the index + // (each successor is a fixed ~+0.5 dB step), so every entry is + // strictly larger than its predecessor. + for i in 1..JOIN_SCALE_LEN { + assert!( + JOIN_SCALE_FACTOR[i] > JOIN_SCALE_FACTOR[i - 1], + "entry {i} not greater than predecessor" + ); + } + } + + #[test] + fn scale_tracks_half_db_ramp() { + // Cross-check the transcribed column against its implied dB + // ramp: the anchor at index 64 is unity (0 dB) and index 104 is + // 10x (+20 dB), so the step is 0.5 dB per index. Verify each + // entry ~= 10^((i-64)*0.5/20) to within the spec's rounding. + for (i, &actual) in JOIN_SCALE_FACTOR.iter().enumerate() { + let db = (i as f64 - 64.0) * 0.5; + let predicted = 10f64.powf(db / 20.0); + let rel = (predicted - actual).abs() / actual; + assert!( + rel < 0.02, + "index {i}: rel err {rel} (pred {predicted}, got {actual})" + ); + } + } +} diff --git a/crates/vendor/oxideav-dts/src/joint_subband.rs b/crates/vendor/oxideav-dts/src/joint_subband.rs new file mode 100644 index 00000000..2d357993 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/joint_subband.rs @@ -0,0 +1,674 @@ +//! DTS Coherent Acoustics — §C.2.3 Joint Subband Coding. +//! +//! Round 223 (2026-06-03) lands the §C.2.3 joint-subband decode, the +//! per-channel reconstruction step that copies the high-end subband +//! samples of a source channel into a destination channel and scales +//! them by the destination channel's per-subband +//! `JOIN_SCALES[ch][n]` factor. The encoder uses joint-subband coding +//! to drop redundant high-frequency content from a destination +//! channel: only the source channel's high subbands are coded on the +//! wire; the decoder re-synthesises the destination's high subbands +//! from them at unpack time. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), Annex C (informative) +//! §C.2.3 "Joint Subband Coding" (PDF p.184) — staged at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. The +//! reproduced normative spec pseudocode is: +//! +//! ```text +//! for (ch=0; ch0 ){ // Joint subband coding enabled. +//! nSourceCh = JOINX[ch]-1; // Get source channel. JOINX counts +//! // channels as 1,2,3,4,5, so minus 1. +//! for (n=nSUBS[ch]; n 0`; `JOINX[ch] == 0` means the +//! destination channel does not import joint-coded subbands from +//! any source. +//! - `nSourceCh = JOINX[ch] - 1`: `JOINX` is one-based per the +//! pseudocode's inline comment ("counts channels as 1,2,3,4,5, so +//! minus 1"); the array indexing into `aPrmCh[]` is zero-based. +//! - The imported subband range is exactly +//! `n ∈ [nSUBS[ch], nSUBS[nSourceCh])`: the destination's own +//! subbands `0..nSUBS[ch]` are unchanged; subbands above the source +//! channel's `nSUBS[nSourceCh]` upper bound are not touched (they +//! remain whatever they were before the joint-subband step, i.e. +//! zero for an inactive subband). +//! - Per-subband scaling: a single scalar `JOIN_SCALES[ch][n]` is +//! broadcast over all `8 * nSSC` samples in that subband. +//! - The operation is unconditionally **write**, not accumulate: the +//! destination subband is replaced by the scaled copy. +//! +//! # Scope +//! +//! This module exposes the matrix copy + scale itself, not the +//! dispatch. The caller (a future subframe walker) is responsible +//! for: +//! +//! - Reading the `JOINX[ch]` per-channel selector from the AUDIO +//! CODING HEADER (`JOINX` is per-channel and 0 when joint-subband +//! coding is disabled for that channel; > 0 when enabled, with the +//! one-based source-channel index). +//! - Reading the per-channel-per-subband `JOIN_SCALES[ch][n]` scale +//! factors from the bit stream (the §5.4.x joint-scale Huffman / +//! linear decode for the active range +//! `n ∈ [nSUBS[ch], nSUBS[nSourceCh])`). +//! - Translating the one-based `JOINX[ch]` to the zero-based +//! `nSourceCh = JOINX[ch] - 1` (see [`joint_source_channel`]). +//! - Reading the per-channel `nSUBS[ch]` active-subband count for both +//! the destination and source channels and confirming +//! `nSUBS[ch] < nSUBS[nSourceCh]` (an empty `[nSUBS[ch], +//! nSUBS[nSourceCh])` range yields a no-op decode but is otherwise +//! well-formed: see [`joint_subband_decode_range_i32`] / +//! [`joint_subband_decode_range_f64`]). +//! +//! Both the integer (i32) and floating-point (f64) decode flavours are +//! exposed because the §C.2.3 spec text does not constrain the +//! arithmetic type — the inverse-quantisation path that feeds the +//! joint-subband decode may run in either; the decoder picks per its +//! precision requirements. The i32 flavour uses `i32::wrapping_mul` +//! to mirror the §C.2.3 pseudocode's C-style `int` overflow +//! semantics; callers that require saturating arithmetic perform the +//! conversion before invoking the primitive. + +use crate::{Error, Result}; + +/// Resolve the one-based `JOINX[ch]` field to the zero-based +/// source-channel index `nSourceCh` per §C.2.3. +/// +/// Returns `None` when `joinx == 0` (joint-subband coding disabled +/// for the channel — the §C.2.3 `if (JOINX[ch] > 0)` predicate +/// rejects this code, so no source channel is named). Returns +/// `Some(joinx - 1)` for `joinx > 0`. +/// +/// The dispatch predicate matches the §C.2.3 pseudocode's +/// `if (JOINX[ch] > 0)` gate directly: callers route through this +/// helper to obtain the source-channel index in one step. +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::joint_source_channel; +/// +/// // JOINX[ch] == 0 -> joint-subband coding disabled. +/// assert_eq!(joint_source_channel(0), None); +/// // JOINX[ch] == 1 -> source channel index 0 (first primary channel). +/// assert_eq!(joint_source_channel(1), Some(0)); +/// // JOINX[ch] == 5 -> source channel index 4 (fifth primary channel). +/// assert_eq!(joint_source_channel(5), Some(4)); +/// ``` +#[must_use] +pub fn joint_source_channel(joinx: u8) -> Option { + if joinx == 0 { + None + } else { + Some(joinx - 1) + } +} + +/// Returns `true` when channel `ch`'s `JOINX[ch]` selector enables +/// joint-subband coding per §C.2.3. +/// +/// This is the §C.2.3 dispatch predicate (`JOINX[ch] > 0`). Callers +/// that already hold a `JOINX[ch]` value can branch on this directly +/// before calling [`joint_subband_decode_range_i32`] / +/// [`joint_subband_decode_range_f64`]. +#[must_use] +pub fn joint_subband_required(joinx: u8) -> bool { + joinx > 0 +} + +/// Decode the §C.2.3 joint-subband copy + scale for **one +/// destination channel** across the active-subband range +/// `[n_subs_dst, n_subs_src)`. +/// +/// `dst_subbands` is the destination channel's per-subband +/// `aSubband[n].aSample[]` slice-of-slices, ordered subband +/// `0..n_subs_src` (i.e. enough storage to hold subbands up to the +/// source's upper bound). `src_subbands` is the source channel's +/// per-subband sample slice-of-slices, same layout. `scales` carries +/// one `JOIN_SCALES[ch][n]` value per subband in the imported range +/// `[n_subs_dst, n_subs_src)` (i.e. `scales.len() == n_subs_src - +/// n_subs_dst`). +/// +/// For each `n ∈ [n_subs_dst, n_subs_src)` the destination subband +/// is overwritten with `scales[n - n_subs_dst] * src_subbands[n]`, +/// sample-by-sample. Subbands outside that range are not touched. +/// +/// # Errors +/// +/// - [`Error::JointSubbandShapeMismatch`] if any structural +/// invariant of the §C.2.3 pseudocode is violated: +/// `n_subs_dst > n_subs_src` (the imported range would run +/// backwards), `dst_subbands.len() < n_subs_src` or +/// `src_subbands.len() < n_subs_src` (the per-channel subband +/// arrays do not extend up to `n_subs_src`), +/// `scales.len() != n_subs_src - n_subs_dst` (the scales slice does +/// not cover the imported range exactly), or a per-subband length +/// disagreement between the destination and source samples for +/// any `n ∈ [n_subs_dst, n_subs_src)`. +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::joint_subband_decode_range_i32; +/// +/// // nSUBS[dst] = 1, nSUBS[src] = 3 -> import subbands 1 and 2 from src. +/// // Each subband carries 8 * nSSC = 4 samples; subband 0 of dst is +/// // left alone. +/// let mut dst_s0 = [0i32; 4]; // dst subband 0 (unchanged) +/// let mut dst_s1 = [0i32; 4]; // dst subband 1 (will be overwritten) +/// let mut dst_s2 = [0i32; 4]; // dst subband 2 (will be overwritten) +/// let mut dst: [&mut [i32]; 3] = [&mut dst_s0, &mut dst_s1, &mut dst_s2]; +/// +/// let src_s0 = [0i32; 4]; // not in import range +/// let src_s1 = [10i32, 20, 30, 40]; +/// let src_s2 = [-1i32, -2, -3, -4]; +/// let src: [&[i32]; 3] = [&src_s0, &src_s1, &src_s2]; +/// +/// // JOIN_SCALES[dst][1] = 2, JOIN_SCALES[dst][2] = 3 +/// let scales = [2i32, 3]; +/// joint_subband_decode_range_i32(&mut dst, &src, &scales, 1, 3).unwrap(); +/// +/// assert_eq!(dst_s0, [0, 0, 0, 0]); // untouched +/// assert_eq!(dst_s1, [20, 40, 60, 80]); // 2 * src_s1 +/// assert_eq!(dst_s2, [-3, -6, -9, -12]); // 3 * src_s2 +/// ``` +pub fn joint_subband_decode_range_i32( + dst_subbands: &mut [&mut [i32]], + src_subbands: &[&[i32]], + scales: &[i32], + n_subs_dst: usize, + n_subs_src: usize, +) -> Result<()> { + validate_joint_shape( + dst_subbands.len(), + src_subbands.len(), + scales.len(), + n_subs_dst, + n_subs_src, + )?; + for n in n_subs_dst..n_subs_src { + let dst = &mut dst_subbands[n]; + let src = src_subbands[n]; + if dst.len() != src.len() { + return Err(Error::JointSubbandShapeMismatch { + dst_len: dst.len(), + src_len: src.len(), + }); + } + let scale = scales[n - n_subs_dst]; + for (d, s) in dst.iter_mut().zip(src.iter()) { + *d = scale.wrapping_mul(*s); + } + } + Ok(()) +} + +/// Floating-point counterpart to [`joint_subband_decode_range_i32`]. +/// +/// Same matrix copy + scale; chosen by callers that consume the +/// reconstructed-subband samples in floating-point. +/// +/// # Errors +/// +/// Same error contract as the i32 variant. +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::joint_subband_decode_range_f64; +/// +/// let mut dst_s0 = [0.0_f64; 2]; +/// let mut dst_s1 = [0.0_f64; 2]; +/// let mut dst: [&mut [f64]; 2] = [&mut dst_s0, &mut dst_s1]; +/// +/// let src_s0 = [0.0_f64; 2]; +/// let src_s1 = [0.5_f64, 1.5]; +/// let src: [&[f64]; 2] = [&src_s0, &src_s1]; +/// +/// let scales = [2.0_f64]; +/// joint_subband_decode_range_f64(&mut dst, &src, &scales, 1, 2).unwrap(); +/// assert_eq!(dst_s0, [0.0, 0.0]); +/// assert_eq!(dst_s1, [1.0, 3.0]); +/// ``` +pub fn joint_subband_decode_range_f64( + dst_subbands: &mut [&mut [f64]], + src_subbands: &[&[f64]], + scales: &[f64], + n_subs_dst: usize, + n_subs_src: usize, +) -> Result<()> { + validate_joint_shape( + dst_subbands.len(), + src_subbands.len(), + scales.len(), + n_subs_dst, + n_subs_src, + )?; + for n in n_subs_dst..n_subs_src { + let dst = &mut dst_subbands[n]; + let src = src_subbands[n]; + if dst.len() != src.len() { + return Err(Error::JointSubbandShapeMismatch { + dst_len: dst.len(), + src_len: src.len(), + }); + } + let scale = scales[n - n_subs_dst]; + for (d, s) in dst.iter_mut().zip(src.iter()) { + *d = scale * *s; + } + } + Ok(()) +} + +/// Shape-check the §C.2.3 join-range geometry common to both the +/// integer and floating-point primitives. +fn validate_joint_shape( + dst_outer: usize, + src_outer: usize, + scales_len: usize, + n_subs_dst: usize, + n_subs_src: usize, +) -> Result<()> { + // The §C.2.3 loop `for (n = nSUBS[ch]; n < nSUBS[nSourceCh]; n++)` + // is well-defined only when the destination's active-subband upper + // bound does not exceed the source's. An empty range + // `nSUBS[ch] == nSUBS[nSourceCh]` is permitted (it yields a no-op + // decode); strict `>` is the error. + if n_subs_dst > n_subs_src { + return Err(Error::JointSubbandShapeMismatch { + dst_len: n_subs_dst, + src_len: n_subs_src, + }); + } + // Both per-channel subband arrays must extend up to `n_subs_src` + // — the imported range `[n_subs_dst, n_subs_src)` must have valid + // backing storage in both `aPrmCh[ch].aSubband[n]` (destination, + // written) and `aPrmCh[nSourceCh].aSubband[n]` (source, read). + if dst_outer < n_subs_src { + return Err(Error::JointSubbandShapeMismatch { + dst_len: dst_outer, + src_len: n_subs_src, + }); + } + if src_outer < n_subs_src { + return Err(Error::JointSubbandShapeMismatch { + dst_len: n_subs_src, + src_len: src_outer, + }); + } + // `JOIN_SCALES[ch][n]` is indexed by the inner loop variable `n` + // over the imported range exactly — neither shorter nor longer. + let expected_scales = n_subs_src - n_subs_dst; + if scales_len != expected_scales { + return Err(Error::JointSubbandShapeMismatch { + dst_len: scales_len, + src_len: expected_scales, + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------- + // Dispatch / source-channel-resolution tests + // ----------------------------------------------------------- + + #[test] + fn source_channel_zero_means_disabled() { + assert_eq!(joint_source_channel(0), None); + assert!(!joint_subband_required(0)); + } + + #[test] + fn source_channel_resolves_one_based_to_zero_based() { + // Per the §C.2.3 inline comment: "counts channels as 1,2,3,4,5, + // so minus 1." Walk the documented 1..=5 range to confirm. + for joinx in 1u8..=5 { + assert_eq!(joint_source_channel(joinx), Some(joinx - 1)); + assert!(joint_subband_required(joinx)); + } + } + + #[test] + fn source_channel_handles_full_u8_range() { + // Defensive: a future round may surface a wider JOINX field; + // the resolver still handles the full u8 domain (the only + // special-cased code is 0). + for joinx in 1u8..=u8::MAX { + assert_eq!(joint_source_channel(joinx), Some(joinx - 1)); + assert!(joint_subband_required(joinx)); + } + } + + // ----------------------------------------------------------- + // i32 range decode — happy-path matrix tests + // ----------------------------------------------------------- + + #[test] + fn i32_basic_copy_and_scale() { + // nSUBS[dst] = 0 -> all subbands of dst are imported from src. + // Two subbands, three samples each. + let mut dst_s0 = [0i32; 3]; + let mut dst_s1 = [0i32; 3]; + let mut dst: [&mut [i32]; 2] = [&mut dst_s0, &mut dst_s1]; + let src_s0 = [1i32, 2, 3]; + let src_s1 = [4i32, 5, 6]; + let src: [&[i32]; 2] = [&src_s0, &src_s1]; + let scales = [2i32, 3]; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 2).unwrap(); + assert_eq!(dst_s0, [2, 4, 6]); + assert_eq!(dst_s1, [12, 15, 18]); + } + + #[test] + fn i32_leaves_subbands_below_n_subs_dst_untouched() { + // nSUBS[dst] = 1: subband 0 of dst must remain unchanged. + let mut dst_s0 = [7i32, 8, 9]; + let mut dst_s1 = [0i32; 3]; + let mut dst: [&mut [i32]; 2] = [&mut dst_s0, &mut dst_s1]; + let src_s0 = [1i32, 2, 3]; // not imported + let src_s1 = [4i32, 5, 6]; + let src: [&[i32]; 2] = [&src_s0, &src_s1]; + let scales = [1i32]; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 1, 2).unwrap(); + assert_eq!(dst_s0, [7, 8, 9]); + assert_eq!(dst_s1, [4, 5, 6]); + } + + #[test] + fn i32_empty_range_is_no_op() { + // nSUBS[dst] == nSUBS[src]: the §C.2.3 loop has zero + // iterations. Decoder is well-formed and the destination is + // not touched. + let mut dst_s0 = [9i32, 9, 9]; + let mut dst: [&mut [i32]; 1] = [&mut dst_s0]; + let src_s0 = [1i32, 1, 1]; + let src: [&[i32]; 1] = [&src_s0]; + let scales: [i32; 0] = []; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 1, 1).unwrap(); + assert_eq!(dst_s0, [9, 9, 9]); + } + + #[test] + fn i32_zero_scale_zeroes_destination() { + // JOIN_SCALES[ch][n] = 0 -> the imported subband is zeroed. + let mut dst_s0 = [99i32; 4]; + let mut dst: [&mut [i32]; 1] = [&mut dst_s0]; + let src_s0 = [1i32, 2, 3, 4]; + let src: [&[i32]; 1] = [&src_s0]; + let scales = [0i32]; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 1).unwrap(); + assert_eq!(dst_s0, [0; 4]); + } + + #[test] + fn i32_negative_scale_inverts_sign() { + let mut dst_s0 = [0i32; 4]; + let mut dst: [&mut [i32]; 1] = [&mut dst_s0]; + let src_s0 = [1i32, -2, 3, -4]; + let src: [&[i32]; 1] = [&src_s0]; + let scales = [-1i32]; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 1).unwrap(); + assert_eq!(dst_s0, [-1, 2, -3, 4]); + } + + #[test] + fn i32_wrapping_multiplication_does_not_panic() { + // The §C.2.3 pseudocode uses `int * int` C semantics: wraps + // on overflow. Confirm the i32 variant uses wrapping_mul, so + // the worst-case `i32::MIN * -1` (which is undefined in + // safe-Rust as `*`) does not panic. + let mut dst_s0 = [0i32; 1]; + let mut dst: [&mut [i32]; 1] = [&mut dst_s0]; + let src_s0 = [i32::MIN]; + let src: [&[i32]; 1] = [&src_s0]; + let scales = [-1i32]; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 1).unwrap(); + // i32::MIN.wrapping_mul(-1) == i32::MIN + assert_eq!(dst_s0[0], i32::MIN); + } + + #[test] + fn i32_writes_only_inside_range() { + // nSUBS[dst] = 2, nSUBS[src] = 4: only subbands 2 and 3 of dst + // are overwritten. Subbands 0 and 1 of dst stay as-is; we + // don't need source data for them. + let mut dst_s0 = [100i32; 2]; + let mut dst_s1 = [101i32; 2]; + let mut dst_s2 = [0i32; 2]; + let mut dst_s3 = [0i32; 2]; + let mut dst: [&mut [i32]; 4] = [&mut dst_s0, &mut dst_s1, &mut dst_s2, &mut dst_s3]; + let src_s0 = [0i32; 2]; + let src_s1 = [0i32; 2]; + let src_s2 = [10i32, 20]; + let src_s3 = [-5i32, 7]; + let src: [&[i32]; 4] = [&src_s0, &src_s1, &src_s2, &src_s3]; + let scales = [2i32, 3]; + joint_subband_decode_range_i32(&mut dst, &src, &scales, 2, 4).unwrap(); + assert_eq!(dst_s0, [100, 100]); + assert_eq!(dst_s1, [101, 101]); + assert_eq!(dst_s2, [20, 40]); + assert_eq!(dst_s3, [-15, 21]); + } + + // ----------------------------------------------------------- + // i32 range decode — error-path shape tests + // ----------------------------------------------------------- + + #[test] + fn i32_rejects_dst_above_src() { + // n_subs_dst > n_subs_src: the loop is undefined per §C.2.3. + let mut dst: [&mut [i32]; 0] = []; + let src: [&[i32]; 0] = []; + let scales: [i32; 0] = []; + let err = joint_subband_decode_range_i32(&mut dst, &src, &scales, 5, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 5, + src_len: 2, + } + )); + } + + #[test] + fn i32_rejects_dst_outer_too_short() { + // dst_subbands.len() < n_subs_src: storage doesn't reach the + // upper bound of the imported range. + let mut s0 = [0i32; 2]; + let mut dst: [&mut [i32]; 1] = [&mut s0]; + let src_s0 = [0i32; 2]; + let src_s1 = [0i32; 2]; + let src: [&[i32]; 2] = [&src_s0, &src_s1]; + let scales = [1i32, 1]; + let err = joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 1, + src_len: 2, + } + )); + } + + #[test] + fn i32_rejects_src_outer_too_short() { + let mut s0 = [0i32; 2]; + let mut s1 = [0i32; 2]; + let mut dst: [&mut [i32]; 2] = [&mut s0, &mut s1]; + let src_s0 = [0i32; 2]; + let src: [&[i32]; 1] = [&src_s0]; + let scales = [1i32, 1]; + let err = joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 2, + src_len: 1, + } + )); + } + + #[test] + fn i32_rejects_scales_length_mismatch() { + // scales.len() must equal n_subs_src - n_subs_dst exactly. + let mut s0 = [0i32; 2]; + let mut s1 = [0i32; 2]; + let mut dst: [&mut [i32]; 2] = [&mut s0, &mut s1]; + let src_s0 = [0i32; 2]; + let src_s1 = [0i32; 2]; + let src: [&[i32]; 2] = [&src_s0, &src_s1]; + let scales = [1i32; 3]; // expected 2 + let err = joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 3, + src_len: 2, + } + )); + } + + #[test] + fn i32_rejects_inner_length_mismatch() { + let mut s0 = [0i32; 2]; + let mut s1 = [0i32; 3]; // inner mismatch with src_s1 + let mut dst: [&mut [i32]; 2] = [&mut s0, &mut s1]; + let src_s0 = [0i32; 2]; + let src_s1 = [0i32; 2]; + let src: [&[i32]; 2] = [&src_s0, &src_s1]; + let scales = [1i32, 1]; + let err = joint_subband_decode_range_i32(&mut dst, &src, &scales, 0, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 3, + src_len: 2, + } + )); + } + + // ----------------------------------------------------------- + // f64 range decode — happy-path + error-path + // ----------------------------------------------------------- + + #[test] + fn f64_basic_copy_and_scale() { + let mut dst_s0 = [0.0_f64; 3]; + let mut dst_s1 = [0.0_f64; 3]; + let mut dst: [&mut [f64]; 2] = [&mut dst_s0, &mut dst_s1]; + let src_s0 = [1.0_f64, 2.0, 3.0]; + let src_s1 = [4.0_f64, 5.0, 6.0]; + let src: [&[f64]; 2] = [&src_s0, &src_s1]; + let scales = [0.5_f64, -0.25]; + joint_subband_decode_range_f64(&mut dst, &src, &scales, 0, 2).unwrap(); + assert_eq!(dst_s0, [0.5, 1.0, 1.5]); + assert_eq!(dst_s1, [-1.0, -1.25, -1.5]); + } + + #[test] + fn f64_empty_range_is_no_op() { + let mut dst_s0 = [42.0_f64; 2]; + let mut dst: [&mut [f64]; 1] = [&mut dst_s0]; + let src_s0 = [0.0_f64; 2]; + let src: [&[f64]; 1] = [&src_s0]; + let scales: [f64; 0] = []; + joint_subband_decode_range_f64(&mut dst, &src, &scales, 1, 1).unwrap(); + assert_eq!(dst_s0, [42.0, 42.0]); + } + + #[test] + fn f64_propagates_shape_errors() { + let mut dst_s0 = [0.0_f64; 2]; + let mut dst: [&mut [f64]; 1] = [&mut dst_s0]; + let src_s0 = [0.0_f64; 2]; + let src_s1 = [0.0_f64; 2]; + let src: [&[f64]; 2] = [&src_s0, &src_s1]; + let scales = [1.0_f64, 1.0]; + let err = joint_subband_decode_range_f64(&mut dst, &src, &scales, 0, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 1, + src_len: 2, + } + )); + } + + #[test] + fn f64_inner_length_mismatch() { + let mut s0 = [0.0_f64; 2]; + let mut s1 = [0.0_f64; 4]; + let mut dst: [&mut [f64]; 2] = [&mut s0, &mut s1]; + let src_s0 = [0.0_f64; 2]; + let src_s1 = [0.0_f64; 3]; + let src: [&[f64]; 2] = [&src_s0, &src_s1]; + let scales = [1.0_f64, 1.0]; + let err = joint_subband_decode_range_f64(&mut dst, &src, &scales, 0, 2).unwrap_err(); + assert!(matches!( + err, + Error::JointSubbandShapeMismatch { + dst_len: 4, + src_len: 3, + } + )); + } + + // ----------------------------------------------------------- + // End-to-end §C.2.3 sweep — hand-computed expected + // ----------------------------------------------------------- + + #[test] + fn full_sweep_matches_spec_pseudocode_directly() { + // nSUBS[dst] = 2, nSUBS[src] = 5, 8 * nSSC = 8. + // Cross-check against an independent hand-computed expected. + let n_dst_subs = 2usize; + let n_src_subs = 5usize; + let n_samples = 8usize; + + let mut dst_storage: Vec> = + (0..n_src_subs).map(|_| vec![999_i32; n_samples]).collect(); + let src_storage: Vec> = (0..n_src_subs) + .map(|s| (0..n_samples).map(|i| (s * 10 + i) as i32).collect()) + .collect(); + let scales: Vec = (n_dst_subs..n_src_subs).map(|n| n as i32).collect(); + + // Hand-compute the expected. + let mut expected: Vec> = dst_storage.clone(); + for n in n_dst_subs..n_src_subs { + let scale = scales[n - n_dst_subs]; + for (i, sample) in src_storage[n].iter().enumerate() { + expected[n][i] = scale.wrapping_mul(*sample); + } + } + + { + let mut dst_slices: Vec<&mut [i32]> = + dst_storage.iter_mut().map(|v| v.as_mut_slice()).collect(); + let src_slices: Vec<&[i32]> = src_storage.iter().map(|v| v.as_slice()).collect(); + joint_subband_decode_range_i32( + &mut dst_slices, + &src_slices, + &scales, + n_dst_subs, + n_src_subs, + ) + .unwrap(); + } + assert_eq!(dst_storage, expected); + } +} diff --git a/crates/vendor/oxideav-dts/src/lfe_fir_coeff.rs b/crates/vendor/oxideav-dts/src/lfe_fir_coeff.rs new file mode 100644 index 00000000..0bf32071 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/lfe_fir_coeff.rs @@ -0,0 +1,448 @@ +//! §D.8 LFE interpolation FIR coefficient tables for the DTS Core +//! low-frequency-effects (LFE) reconstruction path. +//! +//! Transcribed verbatim from ETSI TS 102 114 V1.3.1 (2011-08) +//! Annex D §D.8 "32-Band Interpolation and LFE Interpolation FIR" +//! (staged at `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`, +//! PDF p.238-246), columns "64 x Interpolation" and "128 x +//! Interpolation" (indices 0..=511; the spec table prints decimal +//! commas, rendered here as decimal points). These are the two +//! 512-tap (`NumFIRCoef = 512`) coefficient sets the §C.2.6 +//! `InterpolationFIR(nDecimationSelect)` LFE driver selects, per the +//! resolution in `docs/audio/dts/dts-qmf-driver.md` §3: +//! +//! - [`RA_COEFF_LFE64`] is the spec pseudocode's `raCoeff64` +//! identifier — the **64x interpolation** set, selected when +//! `nDecimationSelect == 0` (decimation factor 64). +//! - [`RA_COEFF_LFE128`] is the spec pseudocode's `raCoeff128` +//! identifier — the **128x interpolation** set, selected when +//! `nDecimationSelect == 1` (decimation factor 128). +//! +//! [`crate::LfeInterpolationSelection::coefficients`] resolves the +//! typed selector to the matching table. +//! +//! Unlike the two 32-band synthesis-QMF columns of the same §D.8 +//! table ([`crate::RA_COEFF_LOSSLESS`] / [`crate::RA_COEFF_LOSSY`], +//! which are *anti-symmetric*, `coeff[i] == -coeff[511 - i]`), the +//! two LFE columns are **symmetric** about the centre +//! (`coeff[i] == coeff[511 - i]` for every `i`) — the expected +//! structure of a symmetric linear-phase interpolation prototype. +//! The test suite verifies that whole-table property plus verbatim +//! anchor rows (`docs/audio/dts/tables/dts-d8-fir.meta.md` +//! "Sample values"). +//! +//! # Scope: tables only +//! +//! This module lands the §D.8 LFE coefficient *data* and the typed +//! [`crate::LfeInterpolationSelection`] selector. The §C.2.6 +//! `InterpolationFIR()` driver's per-sample polyphase convolution loop +//! body consumes these tables and is implemented in +//! [`crate::LfeInterpolator`] (`src/lfe_synth.rs`), transcribed from +//! `docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §1. + +/// Number of taps in each §D.8 LFE interpolation FIR set, fixed by +/// the §C.2.6 `InterpolationFIR()` driver's `NumFIRCoef = 512` +/// (`docs/audio/dts/dts-qmf-driver.md` §3). Matches the 512-tap +/// length of the two 32-band columns ([`crate::FIR_COEFF_LEN`]). +pub const LFE_FIR_COEFF_LEN: usize = 512; + +/// §D.8 "64 x Interpolation" 512-tap LFE interpolation FIR — the +/// §C.2.6 pseudocode's `raCoeff64` set, selected by +/// `nDecimationSelect == 0` (ETSI TS 102 114 V1.3.1 §D.8, staged +/// PDF p.238-246). +#[rustfmt::skip] +pub static RA_COEFF_LFE64: [f64; LFE_FIR_COEFF_LEN] = [ + 2.658434387e-04, 8.179365250e-05, 9.439323912e-05, 1.082170274e-04, // 0..=3 + 1.233371440e-04, 1.397485757e-04, 1.575958013e-04, 1.769922383e-04, // 4..=7 + 1.981738606e-04, 2.211847313e-04, 2.460231190e-04, 2.726115927e-04, // 8..=11 + 3.013863170e-04, 3.328395542e-04, 3.658991191e-04, 4.018281470e-04, // 12..=15 + 4.401875485e-04, 4.812776169e-04, 5.252459669e-04, 5.721592461e-04, // 16..=19 + 6.222130032e-04, 6.755515351e-04, 7.324148901e-04, 7.928516716e-04, // 20..=23 + 8.570110658e-04, 9.251192096e-04, 9.974770946e-04, 1.073930296e-03, // 24..=27 + 1.155023579e-03, 1.240676851e-03, 1.331258914e-03, 1.426893868e-03, // 28..=31 + 1.527829794e-03, 1.634211512e-03, 1.746327500e-03, 1.864377526e-03, // 32..=35 + 1.988604199e-03, 2.119151875e-03, 2.256359672e-03, 2.400433412e-03, // 36..=39 + 2.551567042e-03, 2.710093278e-03, 2.876190469e-03, 3.050152911e-03, // 40..=43 + 3.232272575e-03, 3.422776936e-03, 3.621967277e-03, 3.830091329e-03, // 44..=47 + 4.047499038e-03, 4.274417181e-03, 4.511159845e-03, 4.758012015e-03, // 48..=51 + 5.015311297e-03, 5.283284001e-03, 5.562345497e-03, 5.852684379e-03, // 52..=55 + 6.154712290e-03, 6.468691397e-03, 6.794991903e-03, 7.133882027e-03, // 56..=59 + 7.485736627e-03, 7.850865833e-03, 8.229630999e-03, 8.622321300e-03, // 60..=63 + 9.029330686e-03, 9.450953454e-03, 9.887560271e-03, 1.033949479e-02, // 64..=67 + 1.080708485e-02, 1.129068248e-02, 1.179065090e-02, 1.230732165e-02, // 68..=71 + 1.284105983e-02, 1.339218579e-02, 1.396108977e-02, 1.454808749e-02, // 72..=75 + 1.515355054e-02, 1.577781141e-02, 1.642123051e-02, 1.708412915e-02, // 76..=79 + 1.776690222e-02, 1.846982725e-02, 1.919330470e-02, 1.993762329e-02, // 80..=83 + 2.070316114e-02, 2.149021253e-02, 2.229913883e-02, 2.313023806e-02, // 84..=87 + 2.398385666e-02, 2.486028522e-02, 2.575986087e-02, 2.668286115e-02, // 88..=91 + 2.762960829e-02, 2.860039286e-02, 2.959549613e-02, 3.061520495e-02, // 92..=95 + 3.165979683e-02, 3.272953629e-02, 3.382468969e-02, 3.494550660e-02, // 96..=99 + 3.609224036e-02, 3.726511076e-02, 3.846437484e-02, 3.969023004e-02, // 100..=103 + 4.094288871e-02, 4.222255200e-02, 4.352942482e-02, 4.486365616e-02, // 104..=107 + 4.622544348e-02, 4.761491716e-02, 4.903224111e-02, 5.047753453e-02, // 108..=111 + 5.195093155e-02, 5.345252529e-02, 5.498242006e-02, 5.654069409e-02, // 112..=115 + 5.812742189e-02, 5.974265561e-02, 6.138643622e-02, 6.305878609e-02, // 116..=119 + 6.475970894e-02, 6.648923457e-02, 6.824731827e-02, 7.003392279e-02, // 120..=123 + 7.184901088e-02, 7.369252294e-02, 7.556436211e-02, 7.746443897e-02, // 124..=127 + 7.939263433e-02, 8.134882897e-02, 8.333285898e-02, 8.534456789e-02, // 128..=131 + 8.738376945e-02, 8.945026249e-02, 9.154383838e-02, 9.366425127e-02, // 132..=135 + 9.581124038e-02, 9.798453748e-02, 1.001838669e-01, 1.024089083e-01, // 136..=139 + 1.046593264e-01, 1.069347933e-01, 1.092349365e-01, 1.115593687e-01, // 140..=143 + 1.139076948e-01, 1.162794977e-01, 1.186743453e-01, 1.210917681e-01, // 144..=147 + 1.235313043e-01, 1.259924471e-01, 1.284746826e-01, 1.309774816e-01, // 148..=151 + 1.335003078e-01, 1.360425949e-01, 1.386037618e-01, 1.411831975e-01, // 152..=155 + 1.437802613e-01, 1.463943720e-01, 1.490248144e-01, 1.516709626e-01, // 156..=159 + 1.543320864e-01, 1.570075154e-01, 1.596965194e-01, 1.623983532e-01, // 160..=163 + 1.651122719e-01, 1.678375006e-01, 1.705732346e-01, 1.733186990e-01, // 164..=167 + 1.760730892e-01, 1.788355410e-01, 1.816052496e-01, 1.843813360e-01, // 168..=171 + 1.871629506e-01, 1.899491698e-01, 1.927391142e-01, 1.955319196e-01, // 172..=175 + 1.983266175e-01, 2.011223286e-01, 2.039180547e-01, 2.067128718e-01, // 176..=179 + 2.095058411e-01, 2.122959495e-01, 2.150822729e-01, 2.178637981e-01, // 180..=183 + 2.206395119e-01, 2.234084606e-01, 2.261696160e-01, 2.289219648e-01, // 184..=187 + 2.316644788e-01, 2.343961596e-01, 2.371159792e-01, 2.398228943e-01, // 188..=191 + 2.425158769e-01, 2.451938838e-01, 2.478559017e-01, 2.505008876e-01, // 192..=195 + 2.531278133e-01, 2.557355762e-01, 2.583232224e-01, 2.608896792e-01, // 196..=199 + 2.634339035e-01, 2.659549415e-01, 2.684516609e-01, 2.709231377e-01, // 200..=203 + 2.733682692e-01, 2.757860720e-01, 2.781755328e-01, 2.805356979e-01, // 204..=207 + 2.828655839e-01, 2.851640880e-01, 2.874303460e-01, 2.896633744e-01, // 208..=211 + 2.918621898e-01, 2.940258980e-01, 2.961534858e-01, 2.982441187e-01, // 212..=215 + 3.002967536e-01, 3.023106754e-01, 3.042849004e-01, 3.062185347e-01, // 216..=219 + 3.081108034e-01, 3.099608123e-01, 3.117676973e-01, 3.135308027e-01, // 220..=223 + 3.152491748e-01, 3.169221282e-01, 3.185488880e-01, 3.201287389e-01, // 224..=227 + 3.216609657e-01, 3.231448531e-01, 3.245797157e-01, 3.259649575e-01, // 228..=231 + 3.272998929e-01, 3.285838962e-01, 3.298164308e-01, 3.309969604e-01, // 232..=235 + 3.321248591e-01, 3.331996202e-01, 3.342207968e-01, 3.351879120e-01, // 236..=239 + 3.361004293e-01, 3.369580209e-01, 3.377602994e-01, 3.385068178e-01, // 240..=243 + 3.391972482e-01, 3.398312926e-01, 3.404086530e-01, 3.409290314e-01, // 244..=247 + 3.413922191e-01, 3.417979777e-01, 3.421461284e-01, 3.424364924e-01, // 248..=251 + 3.426689506e-01, 3.428434134e-01, 3.429597318e-01, 3.430179358e-01, // 252..=255 + 3.430179358e-01, 3.429597318e-01, 3.428434134e-01, 3.426689506e-01, // 256..=259 + 3.424364924e-01, 3.421461284e-01, 3.417979777e-01, 3.413922191e-01, // 260..=263 + 3.409290314e-01, 3.404086530e-01, 3.398312926e-01, 3.391972482e-01, // 264..=267 + 3.385068178e-01, 3.377602994e-01, 3.369580209e-01, 3.361004293e-01, // 268..=271 + 3.351879120e-01, 3.342207968e-01, 3.331996202e-01, 3.321248591e-01, // 272..=275 + 3.309969604e-01, 3.298164308e-01, 3.285838962e-01, 3.272998929e-01, // 276..=279 + 3.259649575e-01, 3.245797157e-01, 3.231448531e-01, 3.216609657e-01, // 280..=283 + 3.201287389e-01, 3.185488880e-01, 3.169221282e-01, 3.152491748e-01, // 284..=287 + 3.135308027e-01, 3.117676973e-01, 3.099608123e-01, 3.081108034e-01, // 288..=291 + 3.062185347e-01, 3.042849004e-01, 3.023106754e-01, 3.002967536e-01, // 292..=295 + 2.982441187e-01, 2.961534858e-01, 2.940258980e-01, 2.918621898e-01, // 296..=299 + 2.896633744e-01, 2.874303460e-01, 2.851640880e-01, 2.828655839e-01, // 300..=303 + 2.805356979e-01, 2.781755328e-01, 2.757860720e-01, 2.733682692e-01, // 304..=307 + 2.709231377e-01, 2.684516609e-01, 2.659549415e-01, 2.634339035e-01, // 308..=311 + 2.608896792e-01, 2.583232224e-01, 2.557355762e-01, 2.531278133e-01, // 312..=315 + 2.505008876e-01, 2.478559017e-01, 2.451938838e-01, 2.425158769e-01, // 316..=319 + 2.398228943e-01, 2.371159792e-01, 2.343961596e-01, 2.316644788e-01, // 320..=323 + 2.289219648e-01, 2.261696160e-01, 2.234084606e-01, 2.206395119e-01, // 324..=327 + 2.178637981e-01, 2.150822729e-01, 2.122959495e-01, 2.095058411e-01, // 328..=331 + 2.067128718e-01, 2.039180547e-01, 2.011223286e-01, 1.983266175e-01, // 332..=335 + 1.955319196e-01, 1.927391142e-01, 1.899491698e-01, 1.871629506e-01, // 336..=339 + 1.843813360e-01, 1.816052496e-01, 1.788355410e-01, 1.760730892e-01, // 340..=343 + 1.733186990e-01, 1.705732346e-01, 1.678375006e-01, 1.651122719e-01, // 344..=347 + 1.623983532e-01, 1.596965194e-01, 1.570075154e-01, 1.543320864e-01, // 348..=351 + 1.516709626e-01, 1.490248144e-01, 1.463943720e-01, 1.437802613e-01, // 352..=355 + 1.411831975e-01, 1.386037618e-01, 1.360425949e-01, 1.335003078e-01, // 356..=359 + 1.309774816e-01, 1.284746826e-01, 1.259924471e-01, 1.235313043e-01, // 360..=363 + 1.210917681e-01, 1.186743453e-01, 1.162794977e-01, 1.139076948e-01, // 364..=367 + 1.115593687e-01, 1.092349365e-01, 1.069347933e-01, 1.046593264e-01, // 368..=371 + 1.024089083e-01, 1.001838669e-01, 9.798453748e-02, 9.581124038e-02, // 372..=375 + 9.366425127e-02, 9.154383838e-02, 8.945026249e-02, 8.738376945e-02, // 376..=379 + 8.534456789e-02, 8.333285898e-02, 8.134882897e-02, 7.939263433e-02, // 380..=383 + 7.746443897e-02, 7.556436211e-02, 7.369252294e-02, 7.184901088e-02, // 384..=387 + 7.003392279e-02, 6.824731827e-02, 6.648923457e-02, 6.475970894e-02, // 388..=391 + 6.305878609e-02, 6.138643622e-02, 5.974265561e-02, 5.812742189e-02, // 392..=395 + 5.654069409e-02, 5.498242006e-02, 5.345252529e-02, 5.195093155e-02, // 396..=399 + 5.047753453e-02, 4.903224111e-02, 4.761491716e-02, 4.622544348e-02, // 400..=403 + 4.486365616e-02, 4.352942482e-02, 4.222255200e-02, 4.094288871e-02, // 404..=407 + 3.969023004e-02, 3.846437484e-02, 3.726511076e-02, 3.609224036e-02, // 408..=411 + 3.494550660e-02, 3.382468969e-02, 3.272953629e-02, 3.165979683e-02, // 412..=415 + 3.061520495e-02, 2.959549613e-02, 2.860039286e-02, 2.762960829e-02, // 416..=419 + 2.668286115e-02, 2.575986087e-02, 2.486028522e-02, 2.398385666e-02, // 420..=423 + 2.313023806e-02, 2.229913883e-02, 2.149021253e-02, 2.070316114e-02, // 424..=427 + 1.993762329e-02, 1.919330470e-02, 1.846982725e-02, 1.776690222e-02, // 428..=431 + 1.708412915e-02, 1.642123051e-02, 1.577781141e-02, 1.515355054e-02, // 432..=435 + 1.454808749e-02, 1.396108977e-02, 1.339218579e-02, 1.284105983e-02, // 436..=439 + 1.230732165e-02, 1.179065090e-02, 1.129068248e-02, 1.080708485e-02, // 440..=443 + 1.033949479e-02, 9.887560271e-03, 9.450953454e-03, 9.029330686e-03, // 444..=447 + 8.622321300e-03, 8.229630999e-03, 7.850865833e-03, 7.485736627e-03, // 448..=451 + 7.133882027e-03, 6.794991903e-03, 6.468691397e-03, 6.154712290e-03, // 452..=455 + 5.852684379e-03, 5.562345497e-03, 5.283284001e-03, 5.015311297e-03, // 456..=459 + 4.758012015e-03, 4.511159845e-03, 4.274417181e-03, 4.047499038e-03, // 460..=463 + 3.830091329e-03, 3.621967277e-03, 3.422776936e-03, 3.232272575e-03, // 464..=467 + 3.050152911e-03, 2.876190469e-03, 2.710093278e-03, 2.551567042e-03, // 468..=471 + 2.400433412e-03, 2.256359672e-03, 2.119151875e-03, 1.988604199e-03, // 472..=475 + 1.864377526e-03, 1.746327500e-03, 1.634211512e-03, 1.527829794e-03, // 476..=479 + 1.426893868e-03, 1.331258914e-03, 1.240676851e-03, 1.155023579e-03, // 480..=483 + 1.073930296e-03, 9.974770946e-04, 9.251192096e-04, 8.570110658e-04, // 484..=487 + 7.928516716e-04, 7.324148901e-04, 6.755515351e-04, 6.222130032e-04, // 488..=491 + 5.721592461e-04, 5.252459669e-04, 4.812776169e-04, 4.401875485e-04, // 492..=495 + 4.018281470e-04, 3.658991191e-04, 3.328395542e-04, 3.013863170e-04, // 496..=499 + 2.726115927e-04, 2.460231190e-04, 2.211847313e-04, 1.981738606e-04, // 500..=503 + 1.769922383e-04, 1.575958013e-04, 1.397485757e-04, 1.233371440e-04, // 504..=507 + 1.082170274e-04, 9.439323912e-05, 8.179365250e-05, 2.658434387e-04, // 508..=511 +]; + +/// §D.8 "128 x Interpolation" 512-tap LFE interpolation FIR — the +/// §C.2.6 pseudocode's `raCoeff128` set, selected by +/// `nDecimationSelect == 1` (ETSI TS 102 114 V1.3.1 §D.8, staged +/// PDF p.238-246). +#[rustfmt::skip] +pub static RA_COEFF_LFE128: [f64; LFE_FIR_COEFF_LEN] = [ + 5.316857100e-04, 1.635869100e-04, 1.887860900e-04, 2.164336300e-04, // 0..=3 + 2.466738200e-04, 2.794966000e-04, 3.151909600e-04, 3.539837500e-04, // 4..=7 + 3.963469100e-04, 4.423685900e-04, 4.920452500e-04, 5.452220800e-04, // 8..=11 + 6.027714100e-04, 6.656776500e-04, 7.317967800e-04, 8.036546600e-04, // 12..=15 + 8.803732300e-04, 9.625531400e-04, 1.050489840e-03, 1.144316160e-03, // 16..=19 + 1.244423330e-03, 1.351100280e-03, 1.464826870e-03, 1.585700080e-03, // 20..=23 + 1.714018640e-03, 1.850234690e-03, 1.994950230e-03, 2.147856400e-03, // 24..=27 + 2.310042500e-03, 2.481348810e-03, 2.662512240e-03, 2.853781920e-03, // 28..=31 + 3.055653300e-03, 3.268416510e-03, 3.492647550e-03, 3.728747140e-03, // 32..=35 + 3.977200480e-03, 4.238294900e-03, 4.512710030e-03, 4.800856580e-03, // 36..=39 + 5.103122910e-03, 5.420174920e-03, 5.752369300e-03, 6.100293250e-03, // 40..=43 + 6.464532110e-03, 6.845539900e-03, 7.243919190e-03, 7.660165890e-03, // 44..=47 + 8.094980380e-03, 8.548815730e-03, 9.022301060e-03, 9.516004470e-03, // 48..=51 + 1.003060210e-02, 1.056654565e-02, 1.112466771e-02, 1.170534454e-02, // 52..=55 + 1.230939943e-02, 1.293735672e-02, 1.358995494e-02, 1.426773332e-02, // 56..=59 + 1.497144438e-02, 1.570170000e-02, 1.645922661e-02, 1.724460535e-02, // 60..=63 + 1.805862412e-02, 1.890186779e-02, 1.977507770e-02, 2.067894675e-02, // 64..=67 + 2.161412500e-02, 2.258131653e-02, 2.358125709e-02, 2.461459488e-02, // 68..=71 + 2.568206564e-02, 2.678431384e-02, 2.792212367e-02, 2.909611352e-02, // 72..=75 + 3.030703776e-02, 3.155555204e-02, 3.284239396e-02, 3.416819125e-02, // 76..=79 + 3.553372994e-02, 3.693958372e-02, 3.838652745e-02, 3.987516090e-02, // 80..=83 + 4.140623659e-02, 4.298033938e-02, 4.459818453e-02, 4.626038298e-02, // 84..=87 + 4.796761274e-02, 4.972046614e-02, 5.151961371e-02, 5.336561054e-02, // 88..=91 + 5.525910854e-02, 5.720067024e-02, 5.919086933e-02, 6.123027951e-02, // 92..=95 + 6.331945211e-02, 6.545893103e-02, 6.764923781e-02, 6.989086419e-02, // 96..=99 + 7.218432426e-02, 7.453006506e-02, 7.692859322e-02, 7.938029617e-02, // 100..=103 + 8.188561350e-02, 8.444493264e-02, 8.705867827e-02, 8.972713351e-02, // 104..=107 + 9.245070815e-02, 9.522963315e-02, 9.806428105e-02, 1.009548605e-01, // 108..=111 + 1.039016470e-01, 1.069048345e-01, 1.099646092e-01, 1.130811572e-01, // 112..=115 + 1.162546203e-01, 1.194850579e-01, 1.227726117e-01, 1.261173040e-01, // 116..=119 + 1.295191795e-01, 1.329781860e-01, 1.364943385e-01, 1.400675476e-01, // 120..=123 + 1.436977387e-01, 1.473847479e-01, 1.511284113e-01, 1.549285650e-01, // 124..=127 + 1.587849557e-01, 1.626973301e-01, 1.666653752e-01, 1.706887931e-01, // 128..=131 + 1.747671962e-01, 1.789001823e-01, 1.830873191e-01, 1.873281151e-01, // 132..=135 + 1.916220933e-01, 1.959686577e-01, 2.003673166e-01, 2.048173845e-01, // 136..=139 + 2.093182206e-01, 2.138691545e-01, 2.184694260e-01, 2.231182903e-01, // 140..=143 + 2.278149277e-01, 2.325585187e-01, 2.373482138e-01, 2.421830446e-01, // 144..=147 + 2.470620573e-01, 2.519843280e-01, 2.569487989e-01, 2.619544268e-01, // 148..=151 + 2.670000792e-01, 2.720846236e-01, 2.772069275e-01, 2.823657692e-01, // 152..=155 + 2.875599265e-01, 2.927881181e-01, 2.980490029e-01, 3.033412695e-01, // 156..=159 + 3.086635172e-01, 3.140144050e-01, 3.193923831e-01, 3.247960210e-01, // 160..=163 + 3.302238286e-01, 3.356742859e-01, 3.411457539e-01, 3.466366828e-01, // 164..=167 + 3.521454632e-01, 3.576703668e-01, 3.632097244e-01, 3.687619269e-01, // 168..=171 + 3.743250966e-01, 3.798975349e-01, 3.854774535e-01, 3.910630047e-01, // 172..=175 + 3.966524303e-01, 4.022437930e-01, 4.078352153e-01, 4.134248793e-01, // 176..=179 + 4.190107882e-01, 4.245910645e-01, 4.301636219e-01, 4.357266724e-01, // 180..=183 + 4.412781000e-01, 4.468160272e-01, 4.523383081e-01, 4.578429461e-01, // 184..=187 + 4.633280039e-01, 4.687913656e-01, 4.742309451e-01, 4.796448052e-01, // 188..=191 + 4.850307405e-01, 4.903867543e-01, 4.957108200e-01, 5.010007620e-01, // 192..=195 + 5.062545538e-01, 5.114701390e-01, 5.166453719e-01, 5.217782855e-01, // 196..=199 + 5.268667936e-01, 5.319088101e-01, 5.369022489e-01, 5.418450832e-01, // 200..=203 + 5.467353463e-01, 5.515710115e-01, 5.563499928e-01, 5.610702634e-01, // 204..=207 + 5.657299161e-01, 5.703269839e-01, 5.748594403e-01, 5.793255568e-01, // 208..=211 + 5.837231875e-01, 5.880505443e-01, 5.923057795e-01, 5.964869261e-01, // 212..=215 + 6.005923152e-01, 6.046201587e-01, 6.085684896e-01, 6.124358177e-01, // 216..=219 + 6.162202954e-01, 6.199202538e-01, 6.235341430e-01, 6.270602942e-01, // 220..=223 + 6.304970384e-01, 6.338429451e-01, 6.370964646e-01, 6.402561665e-01, // 224..=227 + 6.433205605e-01, 6.462883353e-01, 6.491580606e-01, 6.519285440e-01, // 228..=231 + 6.545983553e-01, 6.571664810e-01, 6.596315503e-01, 6.619924903e-01, // 232..=235 + 6.642482877e-01, 6.663978696e-01, 6.684402227e-01, 6.703743935e-01, // 236..=239 + 6.721994877e-01, 6.739146709e-01, 6.755192280e-01, 6.770122051e-01, // 240..=243 + 6.783930659e-01, 6.796611548e-01, 6.808158755e-01, 6.818566918e-01, // 244..=247 + 6.827830076e-01, 6.835945249e-01, 6.842908263e-01, 6.848715544e-01, // 248..=251 + 6.853365302e-01, 6.856853962e-01, 6.859180331e-01, 6.860344410e-01, // 252..=255 + 6.860344410e-01, 6.859180331e-01, 6.856853962e-01, 6.853365302e-01, // 256..=259 + 6.848715544e-01, 6.842908263e-01, 6.835945249e-01, 6.827830076e-01, // 260..=263 + 6.818566918e-01, 6.808158755e-01, 6.796611548e-01, 6.783930659e-01, // 264..=267 + 6.770122051e-01, 6.755192280e-01, 6.739146709e-01, 6.721994877e-01, // 268..=271 + 6.703743935e-01, 6.684402227e-01, 6.663978696e-01, 6.642482877e-01, // 272..=275 + 6.619924903e-01, 6.596315503e-01, 6.571664810e-01, 6.545983553e-01, // 276..=279 + 6.519285440e-01, 6.491580606e-01, 6.462883353e-01, 6.433205605e-01, // 280..=283 + 6.402561665e-01, 6.370964646e-01, 6.338429451e-01, 6.304970384e-01, // 284..=287 + 6.270602942e-01, 6.235341430e-01, 6.199202538e-01, 6.162202954e-01, // 288..=291 + 6.124358177e-01, 6.085684896e-01, 6.046201587e-01, 6.005923152e-01, // 292..=295 + 5.964869261e-01, 5.923057795e-01, 5.880505443e-01, 5.837231875e-01, // 296..=299 + 5.793255568e-01, 5.748594403e-01, 5.703269839e-01, 5.657299161e-01, // 300..=303 + 5.610702634e-01, 5.563499928e-01, 5.515710115e-01, 5.467353463e-01, // 304..=307 + 5.418450832e-01, 5.369022489e-01, 5.319088101e-01, 5.268667936e-01, // 308..=311 + 5.217782855e-01, 5.166453719e-01, 5.114701390e-01, 5.062545538e-01, // 312..=315 + 5.010007620e-01, 4.957108200e-01, 4.903867543e-01, 4.850307405e-01, // 316..=319 + 4.796448052e-01, 4.742309451e-01, 4.687913656e-01, 4.633280039e-01, // 320..=323 + 4.578429461e-01, 4.523383081e-01, 4.468160272e-01, 4.412781000e-01, // 324..=327 + 4.357266724e-01, 4.301636219e-01, 4.245910645e-01, 4.190107882e-01, // 328..=331 + 4.134248793e-01, 4.078352153e-01, 4.022437930e-01, 3.966524303e-01, // 332..=335 + 3.910630047e-01, 3.854774535e-01, 3.798975349e-01, 3.743250966e-01, // 336..=339 + 3.687619269e-01, 3.632097244e-01, 3.576703668e-01, 3.521454632e-01, // 340..=343 + 3.466366828e-01, 3.411457539e-01, 3.356742859e-01, 3.302238286e-01, // 344..=347 + 3.247960210e-01, 3.193923831e-01, 3.140144050e-01, 3.086635172e-01, // 348..=351 + 3.033412695e-01, 2.980490029e-01, 2.927881181e-01, 2.875599265e-01, // 352..=355 + 2.823657692e-01, 2.772069275e-01, 2.720846236e-01, 2.670000792e-01, // 356..=359 + 2.619544268e-01, 2.569487989e-01, 2.519843280e-01, 2.470620573e-01, // 360..=363 + 2.421830446e-01, 2.373482138e-01, 2.325585187e-01, 2.278149277e-01, // 364..=367 + 2.231182903e-01, 2.184694260e-01, 2.138691545e-01, 2.093182206e-01, // 368..=371 + 2.048173845e-01, 2.003673166e-01, 1.959686577e-01, 1.916220933e-01, // 372..=375 + 1.873281151e-01, 1.830873191e-01, 1.789001823e-01, 1.747671962e-01, // 376..=379 + 1.706887931e-01, 1.666653752e-01, 1.626973301e-01, 1.587849557e-01, // 380..=383 + 1.549285650e-01, 1.511284113e-01, 1.473847479e-01, 1.436977387e-01, // 384..=387 + 1.400675476e-01, 1.364943385e-01, 1.329781860e-01, 1.295191795e-01, // 388..=391 + 1.261173040e-01, 1.227726117e-01, 1.194850579e-01, 1.162546203e-01, // 392..=395 + 1.130811572e-01, 1.099646092e-01, 1.069048345e-01, 1.039016470e-01, // 396..=399 + 1.009548605e-01, 9.806428105e-02, 9.522963315e-02, 9.245070815e-02, // 400..=403 + 8.972713351e-02, 8.705867827e-02, 8.444493264e-02, 8.188561350e-02, // 404..=407 + 7.938029617e-02, 7.692859322e-02, 7.453006506e-02, 7.218432426e-02, // 408..=411 + 6.989086419e-02, 6.764923781e-02, 6.545893103e-02, 6.331945211e-02, // 412..=415 + 6.123027951e-02, 5.919086933e-02, 5.720067024e-02, 5.525910854e-02, // 416..=419 + 5.336561054e-02, 5.151961371e-02, 4.972046614e-02, 4.796761274e-02, // 420..=423 + 4.626038298e-02, 4.459818453e-02, 4.298033938e-02, 4.140623659e-02, // 424..=427 + 3.987516090e-02, 3.838652745e-02, 3.693958372e-02, 3.553372994e-02, // 428..=431 + 3.416819125e-02, 3.284239396e-02, 3.155555204e-02, 3.030703776e-02, // 432..=435 + 2.909611352e-02, 2.792212367e-02, 2.678431384e-02, 2.568206564e-02, // 436..=439 + 2.461459488e-02, 2.358125709e-02, 2.258131653e-02, 2.161412500e-02, // 440..=443 + 2.067894675e-02, 1.977507770e-02, 1.890186779e-02, 1.805862412e-02, // 444..=447 + 1.724460535e-02, 1.645922661e-02, 1.570170000e-02, 1.497144438e-02, // 448..=451 + 1.426773332e-02, 1.358995494e-02, 1.293735672e-02, 1.230939943e-02, // 452..=455 + 1.170534454e-02, 1.112466771e-02, 1.056654565e-02, 1.003060210e-02, // 456..=459 + 9.516004470e-03, 9.022301060e-03, 8.548815730e-03, 8.094980380e-03, // 460..=463 + 7.660165890e-03, 7.243919190e-03, 6.845539900e-03, 6.464532110e-03, // 464..=467 + 6.100293250e-03, 5.752369300e-03, 5.420174920e-03, 5.103122910e-03, // 468..=471 + 4.800856580e-03, 4.512710030e-03, 4.238294900e-03, 3.977200480e-03, // 472..=475 + 3.728747140e-03, 3.492647550e-03, 3.268416510e-03, 3.055653300e-03, // 476..=479 + 2.853781920e-03, 2.662512240e-03, 2.481348810e-03, 2.310042500e-03, // 480..=483 + 2.147856400e-03, 1.994950230e-03, 1.850234690e-03, 1.714018640e-03, // 484..=487 + 1.585700080e-03, 1.464826870e-03, 1.351100280e-03, 1.244423330e-03, // 488..=491 + 1.144316160e-03, 1.050489840e-03, 9.625531400e-04, 8.803732300e-04, // 492..=495 + 8.036546600e-04, 7.317967800e-04, 6.656776500e-04, 6.027714100e-04, // 496..=499 + 5.452220800e-04, 4.920452500e-04, 4.423685900e-04, 3.963469100e-04, // 500..=503 + 3.539837500e-04, 3.151909600e-04, 2.794966000e-04, 2.466738200e-04, // 504..=507 + 2.164336300e-04, 1.887860900e-04, 1.635869100e-04, 5.316857100e-04, // 508..=511 +]; + +#[cfg(test)] +mod tests { + use super::*; + + /// Verbatim §D.8 LFE anchor rows from + /// `docs/audio/dts/tables/dts-d8-fir.meta.md` "Sample values" + /// (decimal commas rendered as points): (index, 64x, 128x). A + /// transcription slip at the table head, the symmetry pivot, or + /// the tail is caught against the independently-read literal. + const LFE_ANCHORS: &[(usize, f64, f64)] = &[ + (0, 2.658434387e-04, 5.316857100e-04), + (1, 8.179365250e-05, 1.635869100e-04), + (255, 3.430179358e-01, 6.860344410e-01), + (256, 3.430179358e-01, 6.860344410e-01), + (510, 8.179365250e-05, 1.635869100e-04), + (511, 2.658434387e-04, 5.316857100e-04), + ]; + + #[test] + fn anchor_rows_match_the_meta_samples_verbatim() { + for &(i, c64, c128) in LFE_ANCHORS { + assert_eq!( + RA_COEFF_LFE64[i].to_bits(), + c64.to_bits(), + "RA_COEFF_LFE64[{i}] != meta 64x sample row {i}" + ); + assert_eq!( + RA_COEFF_LFE128[i].to_bits(), + c128.to_bits(), + "RA_COEFF_LFE128[{i}] != meta 128x sample row {i}" + ); + } + } + + #[test] + fn both_lfe_tables_are_exactly_symmetric() { + // The printed §D.8 LFE digits satisfy coeff[i] == coeff[511-i] + // for every row of both LFE columns (verified in + // dts-d8-fir.meta.md "Verification"); a transcription error + // in either half breaks the pairing against the verbatim + // other half. + for i in 0..LFE_FIR_COEFF_LEN { + let j = LFE_FIR_COEFF_LEN - 1 - i; + assert_eq!( + RA_COEFF_LFE64[i].to_bits(), + RA_COEFF_LFE64[j].to_bits(), + "RA_COEFF_LFE64[{i}] != RA_COEFF_LFE64[{j}]" + ); + assert_eq!( + RA_COEFF_LFE128[i].to_bits(), + RA_COEFF_LFE128[j].to_bits(), + "RA_COEFF_LFE128[{i}] != RA_COEFF_LFE128[{j}]" + ); + } + } + + #[test] + fn lfe_tables_are_finite_and_peak_at_the_centre() { + // Both LFE columns ramp up monotonically in magnitude to the + // centre rows 255/256 (the symmetric prototype's peak), unlike + // the anti-symmetric 32-band columns. Every value is finite and + // bounded by the printed centre peak. + let max64 = (0..LFE_FIR_COEFF_LEN) + .max_by(|&a, &b| RA_COEFF_LFE64[a].abs().total_cmp(&RA_COEFF_LFE64[b].abs())) + .unwrap(); + let max128 = (0..LFE_FIR_COEFF_LEN) + .max_by(|&a, &b| { + RA_COEFF_LFE128[a] + .abs() + .total_cmp(&RA_COEFF_LFE128[b].abs()) + }) + .unwrap(); + for i in 0..LFE_FIR_COEFF_LEN { + assert!(RA_COEFF_LFE64[i].is_finite()); + assert!(RA_COEFF_LFE128[i].is_finite()); + assert!( + RA_COEFF_LFE64[i].abs() <= 3.430179358e-01, + "RA_COEFF_LFE64[{i}] exceeds the printed centre peak" + ); + assert!( + RA_COEFF_LFE128[i].abs() <= 6.860344410e-01, + "RA_COEFF_LFE128[{i}] exceeds the printed centre peak" + ); + } + assert!( + max64 == 255 || max64 == 256, + "64x peak at {max64}, expected the 255/256 centre" + ); + assert!( + max128 == 255 || max128 == 256, + "128x peak at {max128}, expected the 255/256 centre" + ); + } + + #[test] + fn the_two_lfe_tables_are_distinct_sets() { + // §C.2.6 branches between two different LFE coefficient sets + // (64x and 128x); they must not have collapsed into one. + assert!( + (0..LFE_FIR_COEFF_LEN).any(|i| RA_COEFF_LFE64[i] != RA_COEFF_LFE128[i]), + "64x and 128x LFE tables are identical" + ); + } + + #[test] + fn lfe_tables_are_distinct_from_the_32_band_columns() { + // The §D.8 table carries four columns; the LFE pair must not + // alias either 32-band column (which are anti-symmetric, so a + // distinctness check also guards against a column mix-up). + assert!( + (0..LFE_FIR_COEFF_LEN).any(|i| RA_COEFF_LFE64[i] != crate::RA_COEFF_LOSSY[i]), + "64x LFE table aliases the non-perfect 32-band column" + ); + assert!( + (0..LFE_FIR_COEFF_LEN).any(|i| RA_COEFF_LFE128[i] != crate::RA_COEFF_LOSSLESS[i]), + "128x LFE table aliases the perfect 32-band column" + ); + } + + #[test] + fn lfe_fir_coeff_len_matches_the_c26_num_fir_coef() { + // §C.2.6's driver fixes NumFIRCoef = 512 + // (dts-qmf-driver.md §3); both LFE tables are that length. + assert_eq!(LFE_FIR_COEFF_LEN, 512); + assert_eq!(RA_COEFF_LFE64.len(), LFE_FIR_COEFF_LEN); + assert_eq!(RA_COEFF_LFE128.len(), LFE_FIR_COEFF_LEN); + } +} diff --git a/crates/vendor/oxideav-dts/src/lfe_interp.rs b/crates/vendor/oxideav-dts/src/lfe_interp.rs new file mode 100644 index 00000000..94065b09 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/lfe_interp.rs @@ -0,0 +1,321 @@ +//! Typed selector for the §C.2.6 `InterpolationFIR()` 512-tap LFE +//! interpolation FIR coefficient set. +//! +//! `InterpolationFIR(int nDecimationSelect)` (ETSI TS 102 114 V1.3.1 +//! Annex C §C.2.6, PDF p.186, per `docs/audio/dts/dts-qmf-driver.md` +//! §3) drives the DTS Core low-frequency-effects (LFE) reconstruction +//! path. It takes an `nDecimationSelect` parameter (the LFE decimation +//! factor) that selects between two named §D.8 coefficient sets, per +//! the resolution in `dts-qmf-driver.md` §3: +//! +//! ```text +//! | nDecimationSelect | Decimation factor | Coefficient set | +//! | 1 | 128 | raCoeff128 | +//! | else (0) | 64 | raCoeff64 | +//! ``` +//! +//! The two coefficient sets (`raCoeff64`, the 64x-interpolation LFE +//! FIR, and `raCoeff128`, the 128x-interpolation LFE FIR) are defined +//! in §D.8 "32-Band Interpolation and LFE Interpolation FIR" (staged +//! PDF p.238-246) and transcribed at the crate root as +//! [`crate::RA_COEFF_LFE64`] / [`crate::RA_COEFF_LFE128`]. Both are +//! 512 taps (`NumFIRCoef = 512`, §C.2.6). +//! +//! This module exposes the §C.2.6 selection step as a typed +//! [`LfeInterpolationSelection`] enum plus a +//! [`LfeInterpolationSelection::from_decimation_select`] resolver that +//! mirrors the spec's `if (nDecimationSelect == 1) … else …` branch, +//! and [`LfeInterpolationSelection::coefficients`] resolves the +//! selection to the matching §D.8 512-tap table — exactly the +//! companion of [`crate::FilterBankSelection`] for the LFE path. +//! +//! # Driver-body scope +//! +//! This module lands the table *selection* and the table *data*. The +//! §C.2.6 `InterpolationFIR()` per-sample polyphase convolution loop +//! **body** is implemented in [`crate::LfeInterpolator`] +//! (`src/lfe_synth.rs`), transcribed from +//! `docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §1. LFE +//! samples are pre-scaled at dequant time +//! (`LFECh.rLFE[k] = LFE[n]*rScale` with `rScale = nScale*0.035`), so +//! the convolution body carries no §C.2.5-style output `rScale`. + +/// The two named 512-tap LFE-interpolation FIR coefficient sets +/// referenced by `InterpolationFIR()` per ETSI TS 102 114 V1.3.1 +/// Annex C §C.2.6 (resolved in `docs/audio/dts/dts-qmf-driver.md` +/// §3). +/// +/// Each variant names exactly one of the two §D.8 LFE-interpolation +/// columns ("64 x Interpolation" / "128 x Interpolation", +/// PDF p.238-246), transcribed as [`crate::RA_COEFF_LFE64`] / +/// [`crate::RA_COEFF_LFE128`] and reachable through +/// [`Self::coefficients`]. The variant names mirror the spec +/// pseudocode's identifiers (`raCoeff64` for the 64x set, +/// `raCoeff128` for the 128x set) rendered in idiomatic Rust. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum LfeInterpolationSelection { + /// The §C.2.6 `raCoeff64` 512-tap **64x-interpolation** LFE FIR + /// (§D.8). Selected by `nDecimationSelect == 0` (decimation + /// factor 64) per the §C.2.6 driver's `else` branch. + Decimation64, + /// The §C.2.6 `raCoeff128` 512-tap **128x-interpolation** LFE FIR + /// (§D.8). Selected by `nDecimationSelect == 1` (decimation + /// factor 128) per the §C.2.6 driver's `if` branch. + Decimation128, +} + +impl LfeInterpolationSelection { + /// Resolve a §C.2.6 `nDecimationSelect` value to the named §D.8 + /// LFE coefficient set it picks, per the driver mapping in + /// `dts-qmf-driver.md` §3: + /// `if (nDecimationSelect == 1) raCoeff128; else raCoeff64;`. + /// + /// The resolved table mapping distinguishes only the + /// `nDecimationSelect == 1` (128x) case from everything else + /// (64x), matching the spec's `1` / `else` split exactly — so this + /// resolver accepts an arbitrary `u8` and groups every non-`1` + /// value into [`LfeInterpolationSelection::Decimation64`]. + #[must_use] + pub fn from_decimation_select(n_decimation_select: u8) -> Self { + if n_decimation_select == 1 { + LfeInterpolationSelection::Decimation128 + } else { + LfeInterpolationSelection::Decimation64 + } + } + + /// Inverse of [`Self::from_decimation_select`]: the **canonical** + /// `nDecimationSelect` value the §C.2.6 driver reads to select + /// this coefficient set. + /// + /// Returns `0` for [`LfeInterpolationSelection::Decimation64`] + /// (the `else` branch's canonical representative; the driver + /// collapses every non-`1` value to the same branch, so `0` is the + /// natural choice) and `1` for + /// [`LfeInterpolationSelection::Decimation128`]. + #[must_use] + pub fn decimation_select(self) -> u8 { + match self { + LfeInterpolationSelection::Decimation64 => 0, + LfeInterpolationSelection::Decimation128 => 1, + } + } + + /// The LFE decimation factor this selection corresponds to — `64` + /// for [`LfeInterpolationSelection::Decimation64`] and `128` for + /// [`LfeInterpolationSelection::Decimation128`], per the + /// `dts-qmf-driver.md` §3 mapping. + #[must_use] + pub fn decimation_factor(self) -> u32 { + match self { + LfeInterpolationSelection::Decimation64 => 64, + LfeInterpolationSelection::Decimation128 => 128, + } + } + + /// The §C.2.6 coefficient-table identifier this selection names, + /// as written in the staged driver resolution (`raCoeff64` or + /// `raCoeff128`). + /// + /// Returned as a `&'static str` so callers can format spec- + /// referencing diagnostics without reaching into the enum + /// variants; the strings match the pseudocode's identifiers + /// verbatim. + #[must_use] + pub fn spec_table_name(self) -> &'static str { + match self { + LfeInterpolationSelection::Decimation64 => "raCoeff64", + LfeInterpolationSelection::Decimation128 => "raCoeff128", + } + } + + /// The §D.8 512-tap LFE coefficient table this selection picks — + /// the §C.2.6 driver's `prCoeff` after the + /// `if (nDecimationSelect == 1) … else …` selection, ready for the + /// LFE interpolation step. + /// + /// Returns [`crate::RA_COEFF_LFE64`] (the §D.8 "64 x + /// Interpolation" column) for + /// [`LfeInterpolationSelection::Decimation64`] and + /// [`crate::RA_COEFF_LFE128`] (the "128 x Interpolation" column) + /// for [`LfeInterpolationSelection::Decimation128`], both + /// transcribed verbatim from the staged PDF p.238-246. + #[must_use] + pub fn coefficients(self) -> &'static [f64; crate::lfe_fir_coeff::LFE_FIR_COEFF_LEN] { + match self { + LfeInterpolationSelection::Decimation64 => &crate::lfe_fir_coeff::RA_COEFF_LFE64, + LfeInterpolationSelection::Decimation128 => &crate::lfe_fir_coeff::RA_COEFF_LFE128, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------- + // from_decimation_select — selection per §C.2.6 mapping. + // ----------------------------------------------------------- + + #[test] + fn from_decimation_select_one_picks_128x() { + // dts-qmf-driver.md §3: nDecimationSelect == 1 → raCoeff128. + assert_eq!( + LfeInterpolationSelection::from_decimation_select(1), + LfeInterpolationSelection::Decimation128 + ); + } + + #[test] + fn from_decimation_select_zero_picks_64x() { + // dts-qmf-driver.md §3: else (0) → raCoeff64. + assert_eq!( + LfeInterpolationSelection::from_decimation_select(0), + LfeInterpolationSelection::Decimation64 + ); + } + + #[test] + fn from_decimation_select_treats_every_non_one_value_as_64x() { + // The §C.2.6 mapping is `1` vs `else`; every value other than + // 1 picks the 64x set. Verify across the full u8 range. + for n in 0u16..=255 { + if n == 1 { + continue; + } + assert_eq!( + LfeInterpolationSelection::from_decimation_select(n as u8), + LfeInterpolationSelection::Decimation64, + "nDecimationSelect={n} should pick the 64x else branch" + ); + } + } + + // ----------------------------------------------------------- + // decimation_select — canonical inverse. + // ----------------------------------------------------------- + + #[test] + fn decimation_select_round_trips_64x() { + let sel = LfeInterpolationSelection::Decimation64; + assert_eq!(sel.decimation_select(), 0); + assert_eq!( + LfeInterpolationSelection::from_decimation_select(sel.decimation_select()), + sel + ); + } + + #[test] + fn decimation_select_round_trips_128x() { + let sel = LfeInterpolationSelection::Decimation128; + assert_eq!(sel.decimation_select(), 1); + assert_eq!( + LfeInterpolationSelection::from_decimation_select(sel.decimation_select()), + sel + ); + } + + // ----------------------------------------------------------- + // decimation_factor — the spec's decimation factor. + // ----------------------------------------------------------- + + #[test] + fn decimation_factor_is_64_or_128() { + assert_eq!( + LfeInterpolationSelection::Decimation64.decimation_factor(), + 64 + ); + assert_eq!( + LfeInterpolationSelection::Decimation128.decimation_factor(), + 128 + ); + } + + // ----------------------------------------------------------- + // spec_table_name — pseudocode identifier passthrough. + // ----------------------------------------------------------- + + #[test] + fn spec_table_name_matches_the_c26_identifiers() { + assert_eq!( + LfeInterpolationSelection::Decimation64.spec_table_name(), + "raCoeff64" + ); + assert_eq!( + LfeInterpolationSelection::Decimation128.spec_table_name(), + "raCoeff128" + ); + assert_ne!( + LfeInterpolationSelection::Decimation64.spec_table_name(), + LfeInterpolationSelection::Decimation128.spec_table_name() + ); + } + + // ----------------------------------------------------------- + // coefficients — §D.8 LFE table resolution. + // ----------------------------------------------------------- + + #[test] + fn coefficients_for_64x_is_the_lfe64_table() { + assert!(core::ptr::eq( + LfeInterpolationSelection::Decimation64.coefficients(), + &crate::lfe_fir_coeff::RA_COEFF_LFE64, + )); + } + + #[test] + fn coefficients_for_128x_is_the_lfe128_table() { + assert!(core::ptr::eq( + LfeInterpolationSelection::Decimation128.coefficients(), + &crate::lfe_fir_coeff::RA_COEFF_LFE128, + )); + } + + #[test] + fn coefficients_composed_with_from_decimation_select_reproduces_the_mapping() { + // from_decimation_select + coefficients together are the + // §C.2.6 driver's two-line table selection. + assert!(core::ptr::eq( + LfeInterpolationSelection::from_decimation_select(0).coefficients(), + &crate::lfe_fir_coeff::RA_COEFF_LFE64, + )); + assert!(core::ptr::eq( + LfeInterpolationSelection::from_decimation_select(1).coefficients(), + &crate::lfe_fir_coeff::RA_COEFF_LFE128, + )); + } + + // ----------------------------------------------------------- + // Trait derives. + // ----------------------------------------------------------- + + #[test] + fn variants_are_copyable_and_comparable() { + let a = LfeInterpolationSelection::Decimation64; + let b = a; + assert_eq!(a, b); + assert_ne!(a, LfeInterpolationSelection::Decimation128); + + use core::hash::{Hash, Hasher}; + let mut h1 = std::collections::hash_map::DefaultHasher::new(); + LfeInterpolationSelection::Decimation64.hash(&mut h1); + let mut h2 = std::collections::hash_map::DefaultHasher::new(); + LfeInterpolationSelection::Decimation128.hash(&mut h2); + assert_ne!(h1.finish(), h2.finish()); + } + + #[test] + fn variants_have_stable_debug_output() { + let s = format!("{:?}", LfeInterpolationSelection::Decimation64); + assert!( + s.contains("Decimation64"), + "Debug should name the variant, got {s:?}" + ); + let s = format!("{:?}", LfeInterpolationSelection::Decimation128); + assert!( + s.contains("Decimation128"), + "Debug should name the variant, got {s:?}" + ); + } +} diff --git a/crates/vendor/oxideav-dts/src/lfe_synth.rs b/crates/vendor/oxideav-dts/src/lfe_synth.rs new file mode 100644 index 00000000..7cb76274 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/lfe_synth.rs @@ -0,0 +1,648 @@ +//! §C.2.6 `InterpolationFIR()` driver body — the DTS Core +//! low-frequency-effects (LFE) polyphase upsampling convolution loop. +//! +//! Transcribed from ETSI TS 102 114 V1.3.1 Annex C §C.2.6 +//! (`InterpolationFIR()`, printed PDF p.186), staged as pseudocode in +//! `docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §1. The +//! companion table *selection* step lives in +//! [`crate::LfeInterpolationSelection`]; this module is the +//! **convolution body** that consumes the selected §D.8 512-tap +//! coefficient set and the decimated LFE sample stream and emits the +//! upsampled per-channel PCM. +//! +//! # Polyphase structure +//! +//! The 512-tap kernel is read as a polyphase bank of `nDeciFactor` +//! phases, each `NumFIRCoef / nDeciFactor` taps long. Per the doc §1 +//! "Polyphase structure", for phase `k ∈ [0, nDeciFactor)`: +//! +//! ```text +//! output[nDeciFactor*j + k] +//! = Σ_{J = 0 .. taps_per_phase - 1} prCoeff[k + J*nDeciFactor] +//! * rLFE[j - J] +//! ``` +//! +//! where `taps_per_phase = NumFIRCoef / nDeciFactor`: +//! +//! - 128× filter (`nDecimationSelect == 1`, `raCoeff128`): +//! `512 / 128 = 4` taps per phase. +//! - 64× filter (`nDecimationSelect == 0`, `raCoeff64`): +//! `512 / 64 = 8` taps per phase. +//! +//! Each decimated input sample `rLFE[j]` therefore expands to exactly +//! `nDeciFactor` interpolated output samples. +//! +//! # The spurious-increment transcription artefact +//! +//! The spec PDF's `InterpolationFIR()` pseudocode prints a trailing +//! `nDeciIndex++;` *inside* the outer for-loop body, just before the +//! closing brace. The doc §1 "Implementation note" flags this as a +//! long-standing transcription artefact: the C `for` loop already +//! increments `nDeciIndex`, so reproducing the inner increment +//! literally would read every *second* decimated sample and emit zeros +//! for the gaps. This implementation follows the doc's resolution and +//! does **not** emit the inner increment — every decimated sample is +//! consumed, in order. +//! +//! # Persistent per-channel history +//! +//! `rLFE[j - J]` for `J ≥ 1` reaches *before* the current sub-frame's +//! first decimated sample. Per the doc §1 "History buffer requirement", +//! the decoder must carry the previous sub-frame's last +//! `taps_per_phase - 1` decimated samples across sub-frame boundaries +//! (≥ 7 for the 64× filter, ≥ 3 for the 128× filter). [`LfeInterpolator`] +//! owns that history and starts it cleared (the spec's per-channel LFE +//! filter has no history before the first sub-frame). + +use crate::lfe_fir_coeff::LFE_FIR_COEFF_LEN; +use crate::lfe_interp::LfeInterpolationSelection; + +/// The longest polyphase history any §C.2.6 LFE filter needs: +/// `taps_per_phase - 1` decimated samples. The 64× filter +/// (`512 / 64 = 8` taps per phase) needs `8 - 1 = 7`; the 128× filter +/// needs `4 - 1 = 3`. Both fit in this fixed-size ring. +pub const LFE_HISTORY_LEN: usize = 7; + +/// Errors from the §C.2.6 LFE interpolation driver. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum LfeInterpError { + /// The supplied output slice is shorter than + /// `decimated.len() * nDeciFactor`, the exact upsampled length the + /// §C.2.6 loop produces. + OutputSliceTooShort { + /// Output samples required (`decimated.len() * nDeciFactor`). + required: usize, + /// Output samples actually available. + available: usize, + }, +} + +impl core::fmt::Display for LfeInterpError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + LfeInterpError::OutputSliceTooShort { + required, + available, + } => write!( + f, + "LFE interpolation output slice too short: need {required}, have {available}" + ), + } + } +} + +impl std::error::Error for LfeInterpError {} + +/// Persistent per-channel §C.2.6 LFE interpolation filter — the +/// `LFECh` object whose `InterpolationFIR(nDecimationSelect)` call +/// upsamples one sub-frame's decimated LFE samples to PCM rate. +/// +/// Holds the `taps_per_phase - 1` decimated-sample history the +/// polyphase convolution reads across sub-frame boundaries (the doc §1 +/// "History buffer requirement"). Construct once per LFE channel and +/// drive each sub-frame's samples through [`LfeInterpolator::process`] +/// (or [`LfeInterpolator::interpolate`]) in order so the inter-sub-frame +/// tail carries correctly. +#[derive(Debug, Clone)] +pub struct LfeInterpolator { + /// The decimated-sample history `rLFE[-1], rLFE[-2], …`, most-recent + /// first: `history[0]` is the previous sub-frame's last decimated + /// sample, `history[1]` the one before it, and so on. Sized for the + /// deepest (64×) filter; the 128× filter reads only the first 3 + /// entries. Cleared at construction. + history: [f64; LFE_HISTORY_LEN], +} + +impl Default for LfeInterpolator { + fn default() -> Self { + Self::new() + } +} + +impl LfeInterpolator { + /// Construct a fresh per-channel LFE interpolation filter with a + /// cleared decimated-sample history, matching the spec's + /// per-channel LFE filter state before the first sub-frame. + #[must_use] + pub fn new() -> Self { + Self { + history: [0.0; LFE_HISTORY_LEN], + } + } + + /// Borrow the current decimated-sample history (most-recent first). + /// Exposed for callers that want to inspect or checkpoint the + /// inter-sub-frame tail; [`LfeInterpolator::process`] maintains it + /// automatically. + #[must_use] + pub fn history(&self) -> &[f64; LFE_HISTORY_LEN] { + &self.history + } + + /// Run the §C.2.6 `InterpolationFIR()` polyphase convolution over + /// one sub-frame's decimated LFE samples, writing the upsampled PCM + /// into `output` and advancing the per-channel history. + /// + /// `decimated[j]` is the spec's `rLFE[j]` for the current sub-frame + /// (already scaled at dequant time: `LFECh.rLFE[k] = LFE[n]*rScale` + /// with `rScale = nScale*0.035`, per the §5.5 LFE phase). `selection` + /// resolves `nDecimationSelect` to one of the two §D.8 512-tap + /// coefficient sets and to the decimation factor (`64` / `128`). + /// + /// Exactly `decimated.len() * nDeciFactor` output samples are + /// produced, in time order, as `f64` (the spec casts to integer at + /// store time — [`LfeInterpolator::interpolate`] does the cast for a + /// PCM caller). The history is advanced by the last + /// `taps_per_phase - 1` decimated samples so the next sub-frame's + /// convolution sees the correct tail. + /// + /// # Errors + /// + /// Returns [`LfeInterpError::OutputSliceTooShort`] if `output` is + /// shorter than `decimated.len() * nDeciFactor`. The history is not + /// advanced on error. + pub fn process( + &mut self, + decimated: &[f64], + selection: LfeInterpolationSelection, + output: &mut [f64], + ) -> Result { + let n_deci_factor = selection.decimation_factor() as usize; + // taps_per_phase = NumFIRCoef / nDeciFactor (doc §1): 4 for the + // 128× filter, 8 for the 64× filter. + let taps_per_phase = LFE_FIR_COEFF_LEN / n_deci_factor; + let pr_coeff = selection.coefficients(); + + let required = decimated.len() * n_deci_factor; + if output.len() < required { + return Err(LfeInterpError::OutputSliceTooShort { + required, + available: output.len(), + }); + } + + // Polyphase convolution. For decimated index j and phase k: + // output[nDeciFactor*j + k] + // = Σ_{J=0..taps_per_phase-1} prCoeff[k + J*nDeciFactor] + // * rLFE[j - J] + // rLFE[j - J] for J > j reaches into the carried history: + // rLFE[-1] = history[0], rLFE[-2] = history[1], … + for (j, _) in decimated.iter().enumerate() { + for k in 0..n_deci_factor { + let mut acc = 0.0_f64; + for big_j in 0..taps_per_phase { + // rLFE[j - big_j] + let sample = if big_j <= j { + decimated[j - big_j] + } else { + // History index: rLFE[-1] is history[0], so + // rLFE[j - big_j] with (big_j - j) >= 1 maps to + // history[big_j - j - 1]. + self.history[big_j - j - 1] + }; + acc += pr_coeff[k + big_j * n_deci_factor] * sample; + } + output[n_deci_factor * j + k] = acc; + } + } + + // Advance the history: the new most-recent samples are the last + // taps_per_phase - 1 of this sub-frame, most-recent first. If the + // sub-frame is shorter than that, the remaining slots come from + // the old history (shifted down). + self.advance_history(decimated, taps_per_phase - 1); + + Ok(required) + } + + /// Convenience over [`LfeInterpolator::process`] that allocates the + /// output `Vec` and returns it — the upsampled LFE PCM at full + /// sample rate for one sub-frame. + pub fn process_to_vec( + &mut self, + decimated: &[f64], + selection: LfeInterpolationSelection, + ) -> Vec { + let n_deci_factor = selection.decimation_factor() as usize; + let mut out = vec![0.0_f64; decimated.len() * n_deci_factor]; + // The output is sized exactly, so process() cannot return the + // too-short error. + let _ = self.process(decimated, selection, &mut out); + out + } + + /// Like [`LfeInterpolator::process_to_vec`] but casts each upsampled + /// sample to `i32` (the spec's `naCh[nInterpIndex++] = (int)rTmp` + /// store), truncating toward zero — the integer PCM the LFE channel + /// contributes to the decoded output. + pub fn interpolate( + &mut self, + decimated: &[f64], + selection: LfeInterpolationSelection, + ) -> Vec { + self.process_to_vec(decimated, selection) + .into_iter() + // `(int)rTmp` in C truncates toward zero. + .map(|v| v as i32) + .collect() + } + + /// Slide the `keep` most-recent decimated samples into the history, + /// most-recent first. Handles a sub-frame shorter than `keep` by + /// retaining older history entries behind the new ones. + fn advance_history(&mut self, decimated: &[f64], keep: usize) { + // Build the new history most-recent-first: the last samples of + // this sub-frame, then (if the sub-frame was shorter than `keep`) + // the previous history entries. + let mut new_history = [0.0_f64; LFE_HISTORY_LEN]; + let n = decimated.len(); + for (slot, h) in new_history.iter_mut().take(keep).enumerate() { + if slot < n { + // decimated[n - 1 - slot]: most-recent first. + *h = decimated[n - 1 - slot]; + } else { + // Older than this sub-frame: pull from the old history. + // The (slot - n)-th old entry, shifted past the n new + // ones we just consumed. + let old_idx = slot - n; + *h = self.history[old_idx]; + } + } + self.history = new_history; + } +} + +/// The §5.5 LFE-phase quantiser step constant: `rScale = nScale * 0.035` +/// where `nScale` is the [`crate::RMS_7BIT`] (§D.1.2) entry the 8-bit +/// `LFEscaleIndex` selects. Per the §5.5 LFE phase pseudocode in +/// `docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §2.2. +pub const LFE_SCALE_STEP: f64 = 0.035; + +/// Errors from the §5.5 LFE-phase dequant + interpolation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum LfeChannelError { + /// The 8-bit `LFEscaleIndex` selected a reserved/invalid + /// [`crate::RMS_7BIT`] entry (§D.1.2 indices 125..=127), so no + /// quantiser scale is defined. + ReservedScaleIndex { + /// The offending 8-bit scale index. + index: u8, + }, + /// `lfe_mode` had no LFE channel present (raw code 0), so there is + /// no LFE phase to decode. + NoLfeChannel, +} + +impl core::fmt::Display for LfeChannelError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + LfeChannelError::ReservedScaleIndex { index } => { + write!(f, "LFE scale index {index} is reserved (RMS_7BIT §D.1.2)") + } + LfeChannelError::NoLfeChannel => write!(f, "no LFE channel present (LFF == 0)"), + } + } +} + +impl std::error::Error for LfeChannelError {} + +/// The §5.5 LFE channel — composes the §5.5 LFE-phase dequant +/// (`LFEscaleIndex → pLFE_RMS → nScale; rScale = nScale·0.035`) with +/// the §C.2.6 [`LfeInterpolator`] convolution, owning the +/// inter-sub-frame decimated-sample history. +/// +/// Per `docs/audio/dts/dts-lfe-interpolation-and-audio-walker.md` §2.2, +/// the LFE phase (present only when the frame header's `LFF` flag is +/// non-zero) reads `2·LFF·nSSC` 8-bit two's-complement decimated LFE +/// samples and an 8-bit `LFEscaleIndex`, dequantises +/// `rLFE[n] = LFE[n]·nScale·0.035`, then calls +/// `InterpolationFIR(LFF)` to upsample to PCM rate. `LFF == 1` selects +/// the 128× filter, `LFF == 2` the 64× filter (the §C.2.6 +/// `nDecimationSelect == 1 ? 128× : 64×` split with `nDecimationSelect +/// = LFF`). +#[derive(Debug, Clone)] +pub struct LfeChannel { + interp: LfeInterpolator, +} + +impl Default for LfeChannel { + fn default() -> Self { + Self::new() + } +} + +impl LfeChannel { + /// Construct a fresh LFE channel with a cleared interpolation + /// history. + #[must_use] + pub fn new() -> Self { + Self { + interp: LfeInterpolator::new(), + } + } + + /// Borrow the underlying §C.2.6 interpolator (for history + /// inspection / checkpointing). + #[must_use] + pub fn interpolator(&self) -> &LfeInterpolator { + &self.interp + } + + /// Resolve the §C.2.6 decimation selection from the frame header's + /// `LFF` value (the §5.5 LFE phase's `InterpolationFIR(LFF)` call): + /// `LFF == 1 → 128×`, every other non-zero `LFF → 64×`. + #[must_use] + pub fn selection_for_lff(lff: u8) -> LfeInterpolationSelection { + LfeInterpolationSelection::from_decimation_select(lff) + } + + /// Run the §5.5 LFE phase for one sub-frame: dequantise the 8-bit + /// two's-complement `lfe_samples` with the `scale_index`-selected + /// §D.1.2 RMS scale and the `0.035` quantiser step, then upsample + /// via the §C.2.6 polyphase convolution. + /// + /// `lfe_samples` are the raw `LFE[n]` bytes the §5.5 walker read + /// (`2·LFF·nSSC` of them); `scale_index` is the 8-bit + /// `LFEscaleIndex`; `lff` is the frame header's non-zero `LFF` + /// selecting the decimation factor. Returns the integer PCM + /// (`(int)rTmp`, truncate-toward-zero) the LFE channel contributes, + /// `lfe_samples.len() * (64 | 128)` samples long, and advances the + /// inter-sub-frame history. + /// + /// # Errors + /// + /// [`LfeChannelError::NoLfeChannel`] if `lff == 0`; + /// [`LfeChannelError::ReservedScaleIndex`] if `scale_index` selects a + /// reserved §D.1.2 entry (125..=127). + pub fn decode_subframe( + &mut self, + lfe_samples: &[i8], + scale_index: u8, + lff: u8, + ) -> Result, LfeChannelError> { + if lff == 0 { + return Err(LfeChannelError::NoLfeChannel); + } + // §D.1.2 reserves indices 125..=127 (the RMS_7BIT tail). + if (scale_index as usize) >= crate::side_info::RMS_7BIT.len() - 3 { + return Err(LfeChannelError::ReservedScaleIndex { index: scale_index }); + } + let n_scale = crate::side_info::RMS_7BIT[scale_index as usize] as f64; + let r_scale = n_scale * LFE_SCALE_STEP; + + // rLFE[n] = LFE[n] * rScale. + let decimated: Vec = lfe_samples + .iter() + .map(|&s| f64::from(s) * r_scale) + .collect(); + + let selection = Self::selection_for_lff(lff); + Ok(self.interp.interpolate(&decimated, selection)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A fresh interpolator starts with a cleared decimated-sample + /// history. + #[test] + fn new_starts_with_cleared_history() { + let lfe = LfeInterpolator::new(); + assert!(lfe.history().iter().all(|&v| v == 0.0)); + } + + /// The output length is exactly `decimated.len() * nDeciFactor` for + /// both decimation factors. + #[test] + fn output_length_is_decimated_times_factor() { + for sel in [ + LfeInterpolationSelection::Decimation64, + LfeInterpolationSelection::Decimation128, + ] { + let mut lfe = LfeInterpolator::new(); + let decimated = vec![0.0_f64; 5]; + let out = lfe.process_to_vec(&decimated, sel); + assert_eq!(out.len(), 5 * sel.decimation_factor() as usize); + } + } + + /// A too-short output slice is rejected before any sample is written + /// and the history is left untouched. + #[test] + fn rejects_short_output_slice() { + let mut lfe = LfeInterpolator::new(); + let decimated = vec![1.0_f64; 3]; + let mut out = vec![0.0_f64; 3 * 64 - 1]; // one short for 64× + let err = lfe + .process( + &decimated, + LfeInterpolationSelection::Decimation64, + &mut out, + ) + .unwrap_err(); + assert_eq!( + err, + LfeInterpError::OutputSliceTooShort { + required: 3 * 64, + available: 3 * 64 - 1, + } + ); + assert!(lfe.history().iter().all(|&v| v == 0.0)); + } + + /// An all-zero decimated input with cleared history yields all-zero + /// PCM: every convolution accumulator sums zero products. + #[test] + fn zero_input_yields_zero_pcm() { + for sel in [ + LfeInterpolationSelection::Decimation64, + LfeInterpolationSelection::Decimation128, + ] { + let mut lfe = LfeInterpolator::new(); + let out = lfe.interpolate(&[0.0; 4], sel); + assert_eq!(out.len(), 4 * sel.decimation_factor() as usize); + assert!(out.iter().all(|&s| s == 0)); + } + } + + /// The phase-0 output of a single unit impulse (with cleared + /// history) is exactly the polyphase phase-0 lead tap `prCoeff[0]`, + /// and phase `k`'s first output is `prCoeff[k]` — the J = 0 term of + /// the convolution with `rLFE[0] = 1`, all later taps reaching zero + /// history. + #[test] + fn unit_impulse_reproduces_phase_lead_taps() { + let sel = LfeInterpolationSelection::Decimation128; + let n = sel.decimation_factor() as usize; + let coeff = sel.coefficients(); + let mut lfe = LfeInterpolator::new(); + let out = lfe.process_to_vec(&[1.0], sel); + assert_eq!(out.len(), n); + // output[k] = Σ_J coeff[k + J*n] * rLFE[0 - J]; only J=0 survives + // (rLFE[-1..] = 0), so output[k] = coeff[k]. + for k in 0..n { + assert!( + (out[k] - coeff[k]).abs() < 1e-12, + "phase {k}: got {}, want {}", + out[k], + coeff[k] + ); + } + } + + /// The history carries across calls: splitting a decimated stream + /// into two `process` calls must equal one call over the + /// concatenation, because the polyphase convolution reads the + /// previous sub-frame's tail. + #[test] + fn split_calls_match_single_concatenated_call() { + let sel = LfeInterpolationSelection::Decimation64; + // A deterministic non-trivial decimated stream of 12 samples. + let decimated: Vec = (0..12).map(|i| ((i * 5 + 3) % 9) as f64 - 4.0).collect(); + + // Single call. + let mut single_lfe = LfeInterpolator::new(); + let single = single_lfe.process_to_vec(&decimated, sel); + + // Split 5 + 7, reusing the same filter. + let mut split_lfe = LfeInterpolator::new(); + let mut split = split_lfe.process_to_vec(&decimated[..5], sel); + split.extend(split_lfe.process_to_vec(&decimated[5..], sel)); + + assert_eq!(single.len(), split.len()); + for (i, (a, b)) in single.iter().zip(split.iter()).enumerate() { + assert!((a - b).abs() < 1e-9, "sample {i}: {a} vs {b}"); + } + assert!(single.iter().any(|&s| s != 0.0)); + } + + /// History depth is honoured: after a sub-frame, the stored history + /// holds the last `taps_per_phase - 1` decimated samples, + /// most-recent first. + #[test] + fn history_holds_last_samples_most_recent_first() { + let sel = LfeInterpolationSelection::Decimation64; // 8 taps/phase → keep 7 + let decimated: Vec = (0..10).map(|i| i as f64 + 1.0).collect(); + let mut lfe = LfeInterpolator::new(); + let _ = lfe.process_to_vec(&decimated, sel); + // keep = 7: history[0] = decimated[9] = 10, history[1] = 9, … + for slot in 0..7 { + assert_eq!(lfe.history()[slot], decimated[9 - slot]); + } + } + + /// A sub-frame shorter than the history depth retains older entries + /// behind the new ones (no history is dropped prematurely). + #[test] + fn short_subframe_retains_older_history() { + let sel = LfeInterpolationSelection::Decimation64; // keep 7 + let mut lfe = LfeInterpolator::new(); + // First a long sub-frame to seed the history. + let first: Vec = (0..8).map(|i| (i + 1) as f64).collect(); + let _ = lfe.process_to_vec(&first, sel); + // history = [8,7,6,5,4,3,2] + // Now a short sub-frame of 2 samples [100, 200]. + let _ = lfe.process_to_vec(&[100.0, 200.0], sel); + // New history most-recent first: [200, 100, then old[0..5] = 8,7,6,5,4] + assert_eq!(lfe.history()[0], 200.0); + assert_eq!(lfe.history()[1], 100.0); + assert_eq!(lfe.history()[2], 8.0); + assert_eq!(lfe.history()[3], 7.0); + assert_eq!(lfe.history()[4], 6.0); + assert_eq!(lfe.history()[5], 5.0); + assert_eq!(lfe.history()[6], 4.0); + } + + // ----------------------------------------------------------- + // §5.5 LFE phase — LfeChannel dequant + interpolation. + // ----------------------------------------------------------- + + /// `LFF == 0` has no LFE channel, so decode_subframe declines. + #[test] + fn lfe_channel_declines_no_lfe() { + let mut ch = LfeChannel::new(); + assert_eq!( + ch.decode_subframe(&[0, 0], 0, 0).unwrap_err(), + LfeChannelError::NoLfeChannel + ); + } + + /// A reserved §D.1.2 scale index (125..=127) is rejected. + #[test] + fn lfe_channel_rejects_reserved_scale_index() { + let mut ch = LfeChannel::new(); + for idx in [125u8, 126, 127] { + assert_eq!( + ch.decode_subframe(&[0], idx, 1).unwrap_err(), + LfeChannelError::ReservedScaleIndex { index: idx } + ); + } + } + + /// `selection_for_lff` maps `LFF` per the §C.2.6 / §5.5 split: + /// `1 → 128×`, `2 → 64×`. + #[test] + fn lfe_channel_selection_for_lff() { + assert_eq!( + LfeChannel::selection_for_lff(1), + LfeInterpolationSelection::Decimation128 + ); + assert_eq!( + LfeChannel::selection_for_lff(2), + LfeInterpolationSelection::Decimation64 + ); + } + + /// The decoded LFE PCM has length `lfe_samples.len() * nDeciFactor` + /// and an all-zero input yields silence. + #[test] + fn lfe_channel_decodes_zero_input_to_silence() { + let mut ch = LfeChannel::new(); + // LFF == 1 → 128×. + let pcm = ch.decode_subframe(&[0, 0, 0], 10, 1).unwrap(); + assert_eq!(pcm.len(), 3 * 128); + assert!(pcm.iter().all(|&s| s == 0)); + } + + /// A non-zero LFE sample is dequantised by `nScale·0.035` and feeds + /// the polyphase phase-lead taps: the phase-0 first output equals + /// `(int)(LFE[0]·nScale·0.035·prCoeff[0])`. + #[test] + fn lfe_channel_applies_rms_scale_and_step() { + let scale_index = 60u8; // a mid-table RMS_7BIT entry + let n_scale = crate::side_info::RMS_7BIT[scale_index as usize] as f64; + let r_scale = n_scale * LFE_SCALE_STEP; + let lfe0 = 5_i8; + let lff = 1u8; // 128× + let sel = LfeInterpolationSelection::Decimation128; + let coeff = sel.coefficients(); + let expected0 = (f64::from(lfe0) * r_scale * coeff[0]) as i32; + + let mut ch = LfeChannel::new(); + let pcm = ch.decode_subframe(&[lfe0], scale_index, lff).unwrap(); + assert_eq!(pcm[0], expected0); + } + + /// The integer cast truncates toward zero (the spec's `(int)rTmp`), + /// not floor: a small negative accumulator rounds toward zero. + #[test] + fn integer_cast_truncates_toward_zero() { + // Build an impulse whose phase-0 lead tap is negative, then scale + // so the magnitude is < 1: (int) of -0.x is 0, not -1. + let sel = LfeInterpolationSelection::Decimation128; + let coeff = sel.coefficients(); + // Find a phase with a non-zero coefficient to scale. + let k0 = (0..(sel.decimation_factor() as usize)) + .find(|&k| coeff[k] != 0.0) + .unwrap(); + let scale = 0.5 / coeff[k0].abs(); // make |output[k0]| ≈ 0.5 + let mut lfe = LfeInterpolator::new(); + let out_i = lfe.interpolate(&[scale], sel); + // |0.5| truncates to 0 regardless of sign. + assert_eq!(out_i[k0], 0); + } +} diff --git a/crates/vendor/oxideav-dts/src/lib.rs b/crates/vendor/oxideav-dts/src/lib.rs new file mode 100644 index 00000000..6bdd1a66 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/lib.rs @@ -0,0 +1,1167 @@ +//! # oxideav-dts +//! +//! Pure-Rust DTS Coherent Acoustics decoder for the +//! [oxideav](https://github.com/OxideAV/oxideav) framework. +//! +//! **Status:** clean-room rebuild round 6 (frame-header parser + +//! 14-bit sync unpacking + trailing-flag fields + optional +//! 16-bit header CRC field + 16-bit post-CRC trailing window +//! [multirate-inter / version / copy-history / PCMR / front-sum / +//! surround-sum / dialnorm] + `oxideav-core` `Decoder` +//! integration + multi-frame iterator / resync helper). +//! +//! Round 1 (2026-05-21) landed a structural [`DtsFrameHeader`] parser +//! for the DTS Core frame sync header (per the multimedia.cx wiki +//! snapshot at `docs/audio/dts/wiki/DTS.wiki`, which mirrors the +//! ETSI TS 102 114 §5.3 bit layout). Round 2 (2026-05-21) adds a +//! 14-bit unpacker so both 14-bit container forms decode through +//! the same structural parser as the two 16-bit raw forms. +//! Round 3 (2026-05-21) extends the typed header through the 13 +//! trailing single-bit / small-field flags after RATE (downmix, +//! dynamic-range, time-stamp, aux-data, HDCD, ext-audio-descr, +//! ext-audio-coding, ASPF, 2-bit LFE mode, predictor-history) plus +//! the optional 16-bit `HEADER_CRC` field that follows when +//! [`DtsFrameHeader::crc_present`] is set. (The Annex B CRC +//! algorithm landed in round 408 as [`dts_crc16`]; the core `HCRC` +//! stays unverified by design — §5.3.1 states "The CRC value test +//! shall not be applied" — so +//! [`DtsFrameHeader::verify_header_crc`] returns `None` and the raw +//! 16-bit field is surfaced for pass-through callers.) +//! Round 4 (2026-05-22) wires the crate into `oxideav-core`'s +//! [`oxideav_core::Decoder`] surface (behind a default-on `registry` +//! cargo feature) plus a standalone [`probe_dts`] helper. The +//! `DtsDecoderHandle` returned by the factory parses the frame +//! header eagerly inside `send_packet`; `receive_frame` returns +//! `Error::Unsupported` because PCM output is gated on the +//! SFREQ/RATE/AMODE value tables landing in `docs/`. Bitstream / +//! subframe decoding is **not** part of this round. +//! +//! Round 5 (2026-05-25) extends [`DtsFrameHeader`] through the +//! 16-bit post-CRC trailing window the wiki snapshot enumerates +//! after `HEADER_CRC`: `multirate_inter` (1), `version` (4), +//! `copy_history` (2), `source_pcm_resolution_index` (3), +//! `front_sum` (1), `surround_sum` (1), `dialog_normalization` +//! (4). These bits are consumed unconditionally (the wiki shows +//! them following the HEADER_CRC slot whether or not CRC was +//! emitted). The PCMR→bits-per-sample and DIALNORM→dB mappings +//! are still missing from `docs/`, so +//! [`DtsFrameHeader::source_pcm_bits_per_sample`] and +//! [`DtsFrameHeader::dialog_normalization_db`] return `None` +//! until those tables land. Bitstream / subframe decoding is +//! still **not** part of this round. +//! +//! Round 6 (2026-05-25) adds a multi-frame iterator and a resync +//! helper on top of the existing single-frame parsers: +//! [`find_next_sync`] scans a byte buffer for the next DTS sync +//! sequence at or after a given offset (all four documented +//! encodings); [`iter_frames`] walks a raw-16-bit DTS Core byte +//! stream frame by frame, using each frame's +//! [`DtsFrameHeader::frame_size_bytes`] to advance to the next +//! sync. A new 5-frame ffmpeg-generated fixture +//! (`tests/fixtures/dts_5_frames.bin`, 5 120 bytes) exercises the +//! iterator end-to-end and confirms every frame's header decodes +//! identically. The iterator is documented as raw-16-bit-only +//! because the wiki snapshot does not enumerate the 14-bit +//! container-byte advance rule (see `README.md`'s round-6 docs +//! gap #7); the iterator therefore yields +//! [`Error::UnsupportedFourteenBit`] and terminates if a 14-bit +//! sync is encountered. +//! +//! Round 141 (2026-05-26) closes the parse↔encode round-trip on the +//! frame-sync header window: [`encode_frame_header_be`] serialises a +//! parsed [`DtsFrameHeader`] back into the raw-BE on-wire bytes +//! prescribed by the wiki bit-table (exactly +//! [`DtsFrameHeader::header_byte_length`] bytes long — 13 or 15 — and +//! always beginning with the canonical raw-BE sync). The encoder is +//! the inverse of [`parse_frame_header`] and validates the same +//! structural bounds plus per-field bit-width bounds via the new +//! [`Error::FieldOutOfRange`] variant. +//! +//! Round 151 (2026-05-26) adds [`find_all_syncs`], the bulk-scan +//! counterpart to [`find_next_sync`]: instead of returning the first +//! sync at or after a cursor, it walks the entire input buffer and +//! returns every documented sync occurrence (all four encodings) as a +//! `Vec`. Useful for stream-integrity tooling that needs to +//! know about every resync point up front rather than walking one at a +//! time. Same `O(n)` cost as a `find_next_sync` loop from cursor + 1; +//! the bulk helper just materialises the result. The round also closes +//! a missing coverage gap by testing [`iter_frames`] against a raw-LE +//! multi-frame stream — the iterator already supported raw-LE because +//! `frame_size_bytes` is byte-equivalent across both raw encodings +//! (per the wiki), but the previous test grid only exercised raw-BE +//! via the bundled ffmpeg fixture. +//! +//! Round 159 (2026-05-27) adds an error-tolerant counterpart to +//! [`iter_frames`]: [`iter_frames_resync`] / [`FrameIteratorResync`] +//! treat a candidate sync whose subsequent header bits fail the +//! structural NBLKS / FSIZE bounds (or whose declared +//! `frame_size_bytes` overruns end-of-buffer) as a false-positive +//! sync rather than a hard fault — they surface a [`ResyncEvent`] +//! documenting the discarded offset + cause and continue scanning +//! one byte further instead of terminating. This lets demuxers and +//! stream-integrity tooling walk a partially-corrupted `.dts` +//! stream past malformed-sync patches and recover frames after the +//! damage. The fail-fast [`iter_frames`] remains exactly as +//! documented (returns the first parse error and terminates). +//! +//! Round 165 (2026-05-27) gates the inner-loop multi-byte +//! [`crate::SyncWordEncoding`] detection of [`find_next_sync`] (and +//! therefore [`find_all_syncs`], [`iter_frames`], and +//! [`iter_frames_resync`]) behind a one-byte first-byte filter. The +//! four documented sync sequences all begin with one of `0x7F` / +//! `0xFE` / `0x1F` / `0xFF` (distinct first bytes per the wiki +//! bit-table), so 252 of 256 possible payload bytes are +//! short-circuited before the multi-byte comparison fires. On +//! random payload the inner loop visits ~98.4% of positions with a +//! single byte read + branch rather than the previous 4-byte raw +//! check + 6-byte 14-bit check. The walk order, returned offsets, +//! and matched encoding tags are unchanged from round 6 — round 165 +//! also adds 8 new tests (171 total) including a +//! `find_next_sync_matches_pre_optimization_reference_on_candidate_dense_payload` +//! equivalence harness, a pseudo-random-buffer cross-check against +//! a brute-force pre-optimisation reference, an all-`0xFF` payload +//! stress test (every position is a first-byte candidate so the +//! gate's negative-filter property must hold from the +//! multi-byte side), and an exhaustive 256-input check that the +//! first-byte filter accepts exactly `{0x1F, 0x7F, 0xFE, 0xFF}`. +//! +//! Round 179 (2026-05-29) adds a streaming counterpart to +//! [`find_all_syncs`] plus a small accessor surface on +//! [`SyncWordEncoding`] and [`SyncMatch`], all derived directly from +//! the wiki snapshot's "How to distinguish different versions" +//! sync-sequence table: +//! +//! - [`iter_syncs`] / [`SyncIterator`] — a lazy `Iterator` over every sync sequence in a byte buffer. Same +//! matching rules, walk order, and `O(n)` cost as +//! [`find_all_syncs`], but without the upfront `Vec` +//! allocation. Lets stream-integrity tooling consume matches one +//! at a time, stop early after a [`Iterator::take`] window, or +//! route through standard combinators (e.g. +//! `iter_syncs(bytes).filter(|m| m.encoding.is_raw_16bit())`). +//! - [`SyncWordEncoding::sync_byte_length`] — wiki-table-derived +//! byte count of the on-wire sync sequence (4 for the raw +//! encodings, 6 for the 14-bit-packed encodings). +//! [`SyncWordEncoding::is_raw_16bit`] and +//! [`SyncWordEncoding::is_14bit_packed`] are convenience +//! predicates so callers can branch on the raw-vs-packed +//! distinction without spelling out the four-arm `matches!`. +//! - [`SyncMatch::sync_byte_length`] and +//! [`SyncMatch::sync_byte_range`] — thin accessors that delegate +//! to the encoding's wiki-derived length so the common pattern +//! "advance past the matched sync" or "highlight the matched +//! bytes" reads naturally +//! (`cursor = m.offset + m.sync_byte_length()` / +//! `&bytes[m.sync_byte_range()]`). +//! +//! No new docs gap is introduced. The byte-length values are read +//! verbatim from `docs/audio/dts/wiki/DTS.wiki`'s sync table. The +//! then-open #928 / #1055 / #1084 docs gaps (SFREQ / RATE / AMODE +//! tables, HEADER_CRC polynomial, PCMR / DIALNORM tables, +//! 14-bit container-byte advance rule) have since been resolved +//! (the CRC algorithm by `docs/audio/dts/dts-crc16.md`, round 408). +//! +//! Round 148 (2026-05-26) completes the encoder surface across all +//! four documented sync encodings. The two new primitives, +//! [`encode_frame_header_14bit_be`] and [`encode_frame_header_14bit_le`], +//! compose [`encode_frame_header_be`] with [`pack_16bit_to_14bit`]: +//! the raw-BE 13 or 15-byte header window is zero-padded to 16 bytes +//! (the minimum the parser's 14-bit pre-unpack step needs to land a +//! 16-byte raw-BE window) and re-packed into 14-bit-payload containers +//! in the requested byte order. Both encoders emit exactly **18 bytes** +//! (nine 14-bit containers carrying 126 payload bits) regardless of +//! `crc_present`; the trailing 16 padding bits land in what would be +//! the first SUBFRAMES bits of a real frame and are inert for +//! parsing. The 14-bit-LE output is the pairwise byte-swap of the +//! 14-bit-BE output, matching the wiki's `1F FF E8 00 …` vs +//! `FF 1F 00 E8 …` sync-prefix relationship. The +//! `parse_frame_header_14bit(encode_frame_header_14bit_(hdr))` +//! round-trip recovers `hdr` on every field except `sync_word_encoding` +//! (the parser reports the encoding it detected at the input). +//! +//! Round 145 (2026-05-26) extends the encoder side with two new +//! primitives: [`encode_frame_header_le`] emits the raw-LE on-wire +//! header window (canonical sync `FE 7F 01 80`, always 16 bytes long +//! — the parser's minimum input length for the raw-LE branch — i.e. +//! `encode_frame_header_be` zero-padded to 16 and 16-bit-word-swapped); +//! and [`pack_16bit_to_14bit`] is the inverse of +//! [`unpack_14bit_to_16bit`], packing an MSB-first 16-bit-equivalent +//! byte stream into 14-bit-payload containers with the wiki's "sign +//! bit extension" rule applied to the upper 2 bits of each container. +//! `pack_16bit_to_14bit` returns the packed bytes plus the +//! `payload_bit_count` so callers can recover the exact pre-pack bit +//! length on the receiving end; together with the existing +//! `unpack_14bit_to_16bit` it completes the bidirectional 14↔16-bit +//! container conversion the wiki snapshot prescribes. The 14-bit +//! sync prefix bytes `1F FF E8 00 …` (BE) and `FF 1F 00 E8 …` (LE) +//! the wiki documents are reproduced byte-for-byte by feeding the +//! 32-bit raw-BE syncword `7F FE 80 01` into `pack_16bit_to_14bit` +//! (the last container of the wiki's example carries 10 bits of the +//! following field that are not part of the syncword — that's why +//! the wiki shows `07 Fx` rather than a literal trailing byte). +//! +//! Round 138 (2026-05-26) surfaces the header→SUBFRAMES boundary +//! through two new accessors and one [`FrameView`] helper: +//! [`DtsFrameHeader::header_bit_length`] returns the bit-count the +//! parser consumed (104 or 120 depending on `crc_present`, both +//! exact multiples of 8 by the wiki bit-table arithmetic); +//! [`DtsFrameHeader::header_byte_length`] returns the byte-count +//! (13 or 15); and [`FrameView::payload`] carves out the SUBFRAMES +//! region (`data[header_byte_length()..]`) so downstream re-muxers +//! and the future subframe decoder can address the payload window +//! without recomputing the header boundary. The values are +//! fully derived from the wiki bit-table in +//! `docs/audio/dts/wiki/DTS.wiki`; no new doc dependency. +//! +//! The parser distinguishes the four documented bitstream encodings +//! via the 32-bit (or 40-bit) syncword (see [`SyncWordEncoding`]) and +//! decodes the structural fields whose semantics are spelled out +//! verbatim in the wiki: +//! +//! - frame type (termination vs normal), +//! - per-block sample count (`deficit + 1`), +//! - CRC-present flag, +//! - number of blocks in the frame (5..=128), +//! - frame size in bytes (95..=16384), +//! - channel configuration index (0..=15 standard, 16..=63 +//! user-defined), +//! - sample-frequency index (4 bits), +//! - transmission-bitrate index (5 bits). +//! +//! The structural parser returns the raw indices and exposes +//! `Option` resolvers for the value tables. The transmission-bitrate +//! table landed in round 185 from ETSI TS 102 114 §5.3.1 Table 5-7 +//! (`docs/audio/dts/dts-core-extracts.md` §1): [`TargetedBitRate`] / +//! [`DtsFrameHeader::targeted_bit_rate`] / +//! [`DtsFrameHeader::bit_rate_bps`] now resolve. Round 202 landed +//! the sample-frequency, channel-configuration, and source-PCM- +//! resolution tables (ETSI §5.3.1 Tables 5-5 / 5-4 / 5-17 from the +//! staged PDF): [`SampleFrequency`] / [`DtsFrameHeader::sample_rate_hz`] +//! / [`DtsFrameHeader::sample_frequency`], [`AmodeArrangement`] / +//! [`DtsFrameHeader::channel_count`] / +//! [`DtsFrameHeader::amode_arrangement`], and [`SourcePcmResolution`] +//! / [`DtsFrameHeader::source_pcm_bits_per_sample`] / +//! [`DtsFrameHeader::source_pcm_resolution`] now resolve. The +//! dialog-normalization table (Table 5-20) is still pending, so +//! [`DtsFrameHeader::dialog_normalization_db`] returns `None` until +//! it lands in `docs/`. See `README.md`'s "Docs gaps" section. +//! +//! ## What does *not* belong here +//! +//! - Container muxing (Wav / MP4 / Matroska carriage). +//! - DTS-HD / EXSS / XLL / X96 / XCH extension substreams. +//! - PCM decoding (subband + QMF + Huffman, all deferred to future +//! rounds). +//! +//! ## Public API +//! +//! - [`DtsFrameHeader`] — typed parse result. +//! - [`SyncWordEncoding`] — the four documented sync variants. +//! - [`FrameType`] — termination vs normal. +//! - [`TargetedBitRate`] — `RATE`-field resolution (fixed / open / +//! invalid) per ETSI §5.3.1 Table 5-7 (added in round 185). +//! - [`SampleFrequency`] — `SFREQ`-field resolution (fixed / invalid) +//! per ETSI §5.3.1 Table 5-5 (added in round 202). +//! - [`AmodeArrangement`] — `AMODE`-field resolution (sixteen +//! standard arrangements + user-defined codes) per ETSI §5.3.1 +//! Table 5-4 (added in round 202). +//! - [`SourcePcmResolution`] — `PCMR`-field resolution (valid +//! bits + ES flag, or invalid) per ETSI §5.3.1 Table 5-17 (added +//! in round 202). +//! - [`parse_frame_header`] — non-allocating single-frame parser +//! for the two raw 16-bit syncs. +//! - [`parse_frame_header_14bit`] — single-frame parser for the two +//! 14-bit packed syncs (added in round 2). +//! - [`encode_frame_header_be`] — inverse of [`parse_frame_header`] +//! that emits the wiki bit-table back into raw-BE bytes (added in +//! round 141). +//! - [`encode_frame_header_le`] — raw-LE encoder variant +//! (`encode_frame_header_be` zero-padded to 16 bytes + 16-bit-word +//! swap; added in round 145). +//! - [`encode_frame_header_14bit_be`] / [`encode_frame_header_14bit_le`] +//! — 14-bit-packed encoder variants (`encode_frame_header_be` padded +//! to 16 bytes then re-packed through [`pack_16bit_to_14bit`]; added +//! in round 148). Both emit exactly 18 bytes regardless of +//! `crc_present`, matching the parser's minimum 14-bit input length. +//! - [`unpack_14bit_to_16bit`] / [`pack_16bit_to_14bit`] / +//! [`FourteenBitByteOrder`] — the 14↔16-bit container conversion +//! primitives. `unpack_14bit_to_16bit` added in round 2; +//! `pack_16bit_to_14bit` added in round 145. +//! - [`find_next_sync`] / [`find_all_syncs`] / [`iter_frames`] / +//! [`FrameIterator`] / [`FrameView`] / [`SyncMatch`] — multi-frame +//! walker + resync helpers. `find_next_sync` / `iter_frames` / +//! `FrameIterator` / `FrameView` / `SyncMatch` added in round 6; +//! `find_all_syncs` added in round 151. +//! - [`iter_frames_14bit`] / [`FrameIterator14`] / [`FrameView14`] — +//! 14-bit-packed container-stream frame walker (added in round 192). +//! Walks the same kind of self-delimited Core stream as +//! [`iter_frames`] but in the 14-bit-per-container-word domain; uses +//! the round-189 [`DtsFrameHeader::frame_size_container_bytes`] +//! accessor for the per-frame container-byte advance. +//! - [`iter_syncs`] / [`SyncIterator`] — lazy streaming counterpart +//! to [`find_all_syncs`] (added in round 179). +//! - [`iter_frames_resync`] / [`FrameIteratorResync`] / +//! [`ResyncEvent`] / [`ResyncCause`] — error-tolerant walker that +//! skips past false-positive sync candidates (added in round 159). +//! - [`precal_cos_mod`] / [`COS_MOD_LEN`] / [`COS_MOD_BLOCK1_START`] / +//! [`COS_MOD_BLOCK2_START`] / [`COS_MOD_BLOCK3_START`] / +//! [`COS_MOD_BLOCK4_START`] — the 544-entry cosine-modulation +//! matrix used by the §C.2.5 32-band synthesis QMF (added in +//! round 208). Per-block start indices match the four-block +//! decomposition of `PreCalCosMod()` in +//! `docs/audio/dts/dts-core-extracts.md` §2.3. +//! - [`cos_mod_stage`] / [`NUM_SUBBAND`] — the cosine-modulation +//! stage of `QMFInterpolation()` (§C.2.5, PDF p.185, per +//! `docs/audio/dts/dts-core-extracts.md` §2.4): consumes one +//! per-sample subband vector `raXin[0..32]` plus the +//! [`precal_cos_mod`] matrix, returns the 32 leading entries +//! `raX[0..32]` written into the synthesis filter's shift +//! register before the 512-tap FIR convolution. Independent of +//! the §D.8 FIR coefficient tables (round-208 docs gap #9), so +//! it ships ahead of the full `QMFInterpolation()` driver. Added +//! in round 255. +//! - [`assemble_xin`] / [`shift_x_history`] / [`X_HISTORY_LEN`] / +//! [`QmfAssembleError`] — the FIR-independent per-sample +//! raXin assembly + raX shift-register update steps of +//! `QMFInterpolation()` (§C.2.5, PDF p.185, per +//! `docs/audio/dts/dts-core-extracts.md` §2.4 lines 182-183 and +//! 217). [`assemble_xin`] builds the input vector +//! [`cos_mod_stage`] consumes (active subbands copied, +//! inactive tail zero-filled); [`shift_x_history`] rotates the +//! 512-entry raX register by 32 entries to make room for the +//! next per-sample cosine-modulation output. Both ship ahead of +//! the §D.8-dependent FIR step they bracket. Added in round 259. +//! - [`shift_z_output`] / [`Z_OUTPUT_LEN`] — the FIR-independent +//! post-PCM rotate of the 64-entry `raZ[]` output accumulator +//! (§C.2.5, PDF p.185, per `docs/audio/dts/dts-core-extracts.md` +//! §2.4 lines 218-219): shifts the high block `raZ[32..64]` down +//! into `raZ[0..32]` and zero-fills the freed high block for the +//! next per-sample FIR accumulation. Pure index manipulation — +//! reads no §D.8 FIR coefficients — so it ships ahead of the FIR +//! convolution that fills `raZ[]`. Added in round 271. +//! - [`write_pcm_output`] / [`PCM_OUTPUT_PER_SAMPLE`] — the +//! FIR-independent PCM-output step of `QMFInterpolation()` +//! (§C.2.5, PDF p.185, per `docs/audio/dts/dts-core-extracts.md` +//! §2.4 lines 213-214): consumes the 32 low entries `raZ[0..32]`, +//! scales each by the per-channel `rScale` multiplier, applies the +//! spec's `int()` truncate-toward-zero cast, and writes 32 +//! integer PCM samples into the channel buffer at the running +//! `nChIndex` cursor (returning the advanced cursor). Reads no +//! §D.8 FIR coefficients — it consumes the already-accumulated +//! `raZ[0..32]` — so it ships ahead of the FIR step that fills +//! them. Added in round 274. +//! - [`FilterBankSelection`] — typed selector for the §C.2.5 +//! `QMFInterpolation()` 512-tap FIR coefficient set (the §D.8 +//! `raCoeffLossy` / `raCoeffLossLess` named tables). +//! `from_filts(u8) -> Self` resolves the §C.2.5 `FILTS` flag per +//! the pseudocode's `if (FILTS==0) prCoeff = raCoeffLossy; else +//! prCoeff = raCoeffLossLess;` branch +//! (`docs/audio/dts/dts-core-extracts.md` §2.4 lines 175-178). +//! FIR-coefficient-independent: names the two sets but does not +//! read any coefficient values (round-208 docs gap #9 still +//! blocks the value transcription). Added in round 263. +//! - [`sum_difference_decode_i32`] / [`sum_difference_decode_f64`] / +//! [`sum_difference_decode_subband_pair_i32`] / +//! [`sum_difference_decode_subband_pair_f64`] / +//! [`front_sum_difference_required`] / +//! [`surround_sum_difference_required`] — §C.2.4 sum/difference +//! matrix decoder for the front-channel (`SUMF` or `AMODE == 3`) +//! and surround-channel (`SUMS`) joint encodings (added in +//! round 214). Single-pair, subband-pair, and dispatch-predicate +//! primitives transcribed verbatim from ETSI TS 102 114 V1.3.1 +//! Annex C §C.2.4 (PDF p.184). +//! - [`joint_subband_decode_range_i32`] / +//! [`joint_subband_decode_range_f64`] / +//! [`joint_subband_required`] / [`joint_source_channel`] — §C.2.3 +//! joint-subband copy + per-subband scale, the destination +//! channel's high-end-subband reconstruction step from the source +//! channel `JOINX[ch] - 1` over the subband range +//! `[nSUBS[ch], nSUBS[nSourceCh])` (added in round 223). i32 and +//! f64 variants, dispatch predicate, and source-channel resolver, +//! transcribed verbatim from ETSI TS 102 114 V1.3.1 Annex C §C.2.3 +//! (PDF p.184). +//! - [`inverse_adpcm_decode_i32`] / [`inverse_adpcm_decode_f64`] / +//! [`update_history_i32`] / [`update_history_f64`] / +//! [`inverse_adpcm_required`] / [`NUM_ADPCM_COEFF`] — §C.2.2 +//! inverse-ADPCM predictor (added in round 228). Fixed-order +//! (4-tap) FIR over the reconstructed signal, run per-subband +//! whenever `PMODE == 1`. i32 and f64 decode variants, the +//! rolling four-sample history slide between decode blocks, and +//! the dispatch predicate, transcribed verbatim from ETSI TS 102 +//! 114 V1.3.1 Annex C §C.2.2 (PDF p.183). +//! - [`decode_block_code`] / [`block_code_offset`] / +//! [`block_code_max_code`] — §C.2.1 block-code modulus / +//! integer-division decoder (added in round 232). Turns one +//! mixed-radix code word into the array of quantisation indices +//! the rest of the §C.2 chain consumes. Worked-example matched: +//! `code=64`, `n_levels=3`, four elements → `[0, -1, 0, +1]`. +//! Transcribed verbatim from ETSI TS 102 114 V1.3.1 Annex C §C.2.1 +//! (PDF p.182–183). +//! - [`decode_block_code_table`] / [`d6_book_for_levels`] / +//! [`D6BlockBook`] / [`D6_BOOK_3`]..[`D6_BOOK_25`] / +//! [`D6_BLOCK_ELEMENTS`] — the §C.2.1 *table-look-up* block-code +//! decoder variant and the Annex D §D.6 "Block Code Books" it walks +//! (added in round 309, the named round-232 follow-up). The seven +//! §D.6 4-element books (`3/5/7/9/13/17/25` levels) are transcribed +//! from ETSI TS 102 114 V1.3.1 Annex D §D.6 (PDF p.231–236); the +//! decoder follows the §C.2.1 Table C-1 last-element-first walk and +//! produces output identical to [`decode_block_code`] (cross-checked +//! over the full §D.6 code domains). +//! - [`decode_core_frame`] / [`CoreStreamDecoder`] / [`SubframePcmDecoder`] +//! — the §5.3/§5.4/§5.5 + §C.2.5 raw-bytes-to-PCM reconstruction. +//! [`decode_core_frame`] decodes one frame with cleared filter history +//! (single-frame semantics); [`CoreStreamDecoder`] persists the +//! per-channel §C.2.5 filter tail across frames so a multi-frame +//! elementary stream reconstructs without a per-frame filter-warmup +//! transient. Round 356 validated [`CoreStreamDecoder`] against a +//! black-box `ffmpeg -c:a dca` reference decode of the bundled 5-frame +//! fixture: carrying the inter-frame filter tail makes channel-0 PCM +//! shape-identical to the reference (Pearson correlation 1.0 over the +//! whole stream) versus 0.73 with the filter reset per frame. +//! - [`DMIX_TABLE`] / [`INV_DMIX_TABLE`] / [`dmix_scale`] / +//! [`inv_dmix_scale`] / [`decode_dmix_code`] — the §D.11 downmix +//! scale-factor tables (Q15 `DmixTable`, 241 entries; Q16 +//! `InvDmixTbl`, 201 entries for `DmixTblIndex >= 40`) plus the +//! §5.7.1 Table 5-31 9-bit dynamic-downmix coefficient-code +//! resolution (phase MSB, one-biased low byte, `0` → exact `0.0`). +//! Transcribed from ETSI TS 102 114 V1.3.1 §D.11 (PDF p.256-259) +//! and unit-verified against the spec's own closed-form dB-ramp +//! derivation. Added in round 406. +//! - [`dts_crc16`] / [`dts_crc16_update`] / [`DTS_CRC16_POLY`] / +//! [`DTS_CRC16_INIT`] / [`DTS_CRC16_TABLE`] — the Annex B +//! (normative) CRC-16 every DTS check word uses (CRC-CCITT: +//! polynomial `0x1021`, init `0xFFFF`, MSB-first, no reflection, no +//! final XOR), in one-shot, incremental, and compile-time-table +//! forms. Per `docs/audio/dts/dts-crc16.md`. Added in round 408. +//! - [`scan_hf_vq_indices_at`] / [`unpack_hfreq_vq_entry`] / +//! [`adpcm_vq_coeff`] + the `HFREQ_VQ_*` / `ADPCM_VQ_*` constants — +//! the §D.10 VQ code books' wire facts (index widths, book sizes, +//! vector lengths, the §D.10.2 two-signed-bytes ÷ 2⁴ / §D.10.1 +//! ÷ 2¹³ entry scalings), plus the structural §5.5 phase-1 index +//! scanner. Added in round 408; the §D.10.2 divisor and intra-entry +//! order corrected in round 439 per the staged recovery record +//! (`docs/audio/dts/tables/dts-d10-2-hfreq-vq.meta.md`). +//! - [`HfVqCodebook`] / [`AdpcmVqCodebook`] / [`VqCodebooks`] + +//! [`SubframePcmDecoder::set_vq_codebooks`] — the §D.10 code books +//! and their decode paths. Rounds 408/434 landed the full HF-VQ +//! (`nVQSUB < nSUBS`) and inverse-ADPCM (`PMODE != 0`, §C.2.2, +//! [`AdpcmHistory`], §5.3.1 `HFLAG` frame gate) machinery behind a +//! drop-in container while the books' numeric contents were the +//! long-standing `docs/audio/dts/dts-d10-vq-tables-GAP.md` gap; +//! round 439 ships the **real books built in** +//! ([`VqCodebooks::builtin`], transcribed from the staged +//! clean-room tables `docs/audio/dts/tables/dts-d10-*.csv`), the +//! default of every decoder — §D.10 frames now decode out of the +//! box, black-box-validated against a reference decode of the +//! §D.10-bearing fixture. +//! - [`dts_dynrng_to_db`] / [`dts_dynrng_to_linear`] — the §5.4.1 / +//! §5.7.2 DRC code resolution: the 8-bit `RANGE` / +//! `subsubFrameDRC_Rev2AUX[]` byte is **signed Q2 two's-complement** +//! (`dB = (int8)code × 0.25`, linear gain `10^(dB/20)`), per +//! `docs/audio/dts/dts-drc-dynrng.md`. The §D.4 table +//! ([`DRC_RANGE_MULTIPLIER`] / [`drc_range`]) remains available as +//! reference data keyed by its offset-binary printed Index +//! (`Index = signed_code + 127`) — do not raw-index it with wire +//! codes. Added in round 408 (which also switched the +//! `decode_core_frame` `DYNF` application and +//! `Rev2Drc::multipliers` to the signed-Q2 function). +//! - [`find_aux_data`] / [`parse_aux_data`] / [`parse_aux_data_at`] / +//! [`AuxData`] / [`DownmixType`] / [`DynamicDownmix`] — the §5.7.1 +//! Auxiliary Data chunk (Table 5-31): DWORD-aligned backward sync +//! search (`0x9A1105A0`), the 36-bit decode time stamp with its +//! `0b1011` markers, and the dynamic (embedded) downmix +//! coefficient table resolved through §D.11 +//! ([`DynamicDownmix::coefficient_matrix`]) and applicable to +//! planar PCM ([`DynamicDownmix::apply_planar`]). Also reachable +//! from the frame iterator via `FrameView::aux_data`. Added in +//! round 406. +//! - [`find_rev2_aux`] / [`parse_rev2_aux`] / [`parse_rev2_aux_at`] / +//! [`Rev2AuxChunk`] / [`Rev2Drc`] — the §5.7.2 Rev2 Auxiliary Data +//! Chunk (Table 5-33): DWORD-aligned backward sync search +//! (`0x7004C070`), the embedded-ES downmix scale (§D.11), the +//! size-gated broadcast metadata (per-subsubframe DRC values for +//! version 1 + `DIALNORM_rev2aux`), and the size-located +//! `nRev2AUXCRC16`. Also reachable via `FrameView::rev2_aux`. +//! Added in round 406. +//! - [`Error`] — crate-local error type. +//! +//! Behind the default-on `registry` cargo feature (round 4): +//! +//! - [`register`] / [`register_codecs`] — wire the DTS decoder factory +//! plus `dts` / `dtsc` FourCC tags into an +//! [`oxideav_core::RuntimeContext`] / [`oxideav_core::CodecRegistry`]. +//! - [`make_decoder`] — factory that builds a boxed +//! [`oxideav_core::Decoder`] (the [`DtsDecoderHandle`]). +//! - [`DtsDecoderHandle`] — the decoder handle. `send_packet` eagerly +//! parses the frame header (unpacking 14-bit container frames); +//! `receive_frame` runs the full §5.3/§5.4/§5.5 + §C.2.5 +//! reconstruction — including §D.10 VQ/ADPCM frames via the +//! built-in code books — to a planar S32 `AudioFrame`. +//! - [`probe_dts`] — standalone confidence helper (1.0 / 0.5 / 0.0). +//! - [`CODEC_ID_STR`] — canonical codec id `"dts"`. +//! +//! The crate `forbid`s `unsafe`. + +#![forbid(unsafe_code)] +#![warn(missing_debug_implementations)] +#![warn(missing_docs)] + +mod audio_array; +mod audio_data; +mod audio_header; +mod audio_huff; +mod aux_data; +mod bitreader; +mod block_code; +mod cos_mod; +mod crc16; +#[doc(hidden)] +pub mod d10_tables; +mod d10_vq; +mod d6_block_book; +mod dmix_coeff; +mod drc_range; +mod dsync; +mod filter_bank; +mod fir_coeff; +mod header; +mod inverse_adpcm; +mod iter; +mod join_scale; +mod joint_subband; +mod lfe_fir_coeff; +mod lfe_interp; +mod lfe_synth; +mod optional_info; +mod qmf_assemble; +mod qmf_multichannel; +mod qmf_synth; +mod rev2_aux; +mod side_info; +mod step_size; +mod subframe; +mod subframe_pcm; +mod sum_diff; +#[cfg(test)] +mod test_util; +mod unpack14; + +#[cfg(feature = "registry")] +mod registry; + +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::audio_array::{ + decode_audio_data_subframe_at, decode_audio_data_subframe_partial_at, + decode_audio_data_subframe_vq_at, decode_lfe_phase_at, AdpcmContext, AudioArrayDecodeError, + AudioArrayError, HfVqFill, SubbandSampleMatrix, +}; +// Stable: the persistent §C.2.2 reconstruction history a caller may +// inspect/reset around the recovered-§D.10 ADPCM decode path. +pub use crate::audio_array::AdpcmHistory; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::audio_data::{ + audio_quant_type, terminal_sel_index, AudioQuantType, ABITS_MAX_BLOCK_CODE, ABITS_MAX_SEL, + ABITS_TABLE_LEN, CODEBOOK_GROUP_SIZE, QUANT_LEVELS, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::audio_header::{decode_audio_coding_header_at, AudioCodingHeader, SEL_PLANE_LEN}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::audio_huff::{decode_audio_huff_at, AudioHuffCodebook}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::aux_data::{ + find_aux_data, parse_aux_data, parse_aux_data_at, AuxData, DownmixType, DynamicDownmix, + AUX_SYNC_WORD, AUX_TIME_STAMP_MARKER, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::block_code::{block_code_max_code, block_code_offset, decode_block_code}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::cos_mod::{ + cos_mod_stage, precal_cos_mod, COS_MOD_BLOCK1_START, COS_MOD_BLOCK2_START, + COS_MOD_BLOCK3_START, COS_MOD_BLOCK4_START, COS_MOD_LEN, NUM_SUBBAND, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::crc16::{ + dts_crc16, dts_crc16_update, DTS_CRC16_INIT, DTS_CRC16_POLY, DTS_CRC16_TABLE, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::d10_vq::{ + adpcm_vq_coeff, scan_hf_vq_indices_at, unpack_hfreq_vq_entry, ADPCM_VQ_BOOK_SIZE, + ADPCM_VQ_COEFF_DIVISOR, ADPCM_VQ_INDEX_BITS, ADPCM_VQ_VECTOR_LEN, HFREQ_VQ_BOOK_SIZE, + HFREQ_VQ_ELEMENT_DIVISOR, HFREQ_VQ_ENTRIES_PER_VECTOR, HFREQ_VQ_INDEX_BITS, + HFREQ_VQ_VECTOR_LEN, +}; +// Stable: the §D.10 VQ code books. `VqCodebooks::builtin()` (the +// decoder default) carries the real books, transcribed from the +// staged clean-room tables; the caller-supplied constructors remain. +pub use crate::d10_vq::{AdpcmVqCodebook, HfVqCodebook, VqCodebookShapeError, VqCodebooks}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::d6_block_book::{ + d6_book_for_levels, decode_block_code_table, D6BlockBook, D6_BLOCK_ELEMENTS, D6_BOOK_13, + D6_BOOK_17, D6_BOOK_25, D6_BOOK_3, D6_BOOK_5, D6_BOOK_7, D6_BOOK_9, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::dmix_coeff::{ + decode_dmix_code, dmix_scale, inv_dmix_scale, DMIX_TABLE, DMIX_TABLE_LEN, + DMIX_TABLE_UNITY_INDEX, INV_DMIX_INDEX_OFFSET, INV_DMIX_TABLE, INV_DMIX_TABLE_LEN, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::drc_range::{ + drc_range, dts_dynrng_to_db, dts_dynrng_to_linear, DRC_RANGE_LEN, DRC_RANGE_MULTIPLIER, + DRC_RANGE_UNITY_INDEX, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::dsync::{decode_dsync_at, dsync_present, DSYNC_WIRE_BITS, DSYNC_WORD}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::filter_bank::FilterBankSelection; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::fir_coeff::{FIR_COEFF_LEN, RA_COEFF_LOSSLESS, RA_COEFF_LOSSY}; +pub use crate::header::{ + encode_frame_header_14bit_be, encode_frame_header_14bit_le, encode_frame_header_be, + encode_frame_header_le, parse_frame_header, parse_frame_header_14bit, AmodeArrangement, + DialogNormalization, DtsFrameHeader, FrameType, LfeMode, SampleFrequency, SourcePcmResolution, + SyncWordEncoding, TargetedBitRate, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::inverse_adpcm::{ + inverse_adpcm_decode_f64, inverse_adpcm_decode_i32, inverse_adpcm_required, update_history_f64, + update_history_i32, NUM_ADPCM_COEFF, +}; +pub use crate::iter::{ + find_all_syncs, find_next_sync, iter_frames, iter_frames_14bit, iter_frames_resync, iter_syncs, + FrameIterator, FrameIterator14, FrameIteratorResync, FrameView, FrameView14, ResyncCause, + ResyncEvent, SyncIterator, SyncMatch, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::join_scale::{ + join_scale, JOIN_SCALE_FACTOR, JOIN_SCALE_LEN, JOIN_SCALE_UNITY_INDEX, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::joint_subband::{ + joint_source_channel, joint_subband_decode_range_f64, joint_subband_decode_range_i32, + joint_subband_required, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::lfe_fir_coeff::{LFE_FIR_COEFF_LEN, RA_COEFF_LFE128, RA_COEFF_LFE64}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::lfe_interp::LfeInterpolationSelection; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::lfe_synth::{ + LfeChannel, LfeChannelError, LfeInterpError, LfeInterpolator, LFE_HISTORY_LEN, LFE_SCALE_STEP, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::optional_info::{decode_optional_info_at, OptionalInfo, MAX_AUX_BYTE_COUNT}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::qmf_assemble::{ + assemble_xin, fir_step, shift_x_history, shift_z_output, write_pcm_output, QmfAssembleError, + PCM_OUTPUT_PER_SAMPLE, X_HISTORY_LEN, Z_OUTPUT_LEN, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::qmf_multichannel::{MultiChannelQmf, MultiChannelQmfError}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::qmf_synth::QmfSynthesis; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::rev2_aux::{ + find_rev2_aux, parse_rev2_aux, parse_rev2_aux_at, Rev2AuxChunk, Rev2Drc, REV2_AUX_SYNC_WORD, + REV2_DRC_VERSION_SINGLE_BAND, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::side_info::{ + decode_abits_at, decode_adj_at, decode_join_scale_at, decode_scales_at, + decode_subsubframe_count_at, decode_tmode_at, AbitsCodebook, ScaleFactorAdjustment, + ScalesCodebook, SubsubframeCount, TmodeCodebook, RMS_6BIT, RMS_7BIT, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::step_size::{ + dequant_scale, dequant_subsubframe, scale_subsubframe_samples, transient_scale_index, + StepSizeTable, RATE_LOSSLESS, SAMPLES_PER_SUBSUBFRAME, STEP_SIZE_FIRST_INVALID, + STEP_SIZE_LOSSLESS, STEP_SIZE_LOSSY, STEP_SIZE_SCALE_SHIFT, STEP_SIZE_TABLE_LEN, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::subframe::{ + decode_primary_side_info_at, decode_primary_side_info_tail_at, ChannelSideInfo, + ChannelSideInfoParams, PrimarySideInfo, SideInfoTail, MAX_PRIMARY_CHANNELS, +}; +pub use crate::subframe_pcm::{ + decode_core_frame, decode_core_frame_with_info, CoreFrameDecodeError, CoreStreamDecoder, + Subframe, SubframePcm, SubframePcmDecoder, SubframePcmError, PCM_PER_SUBBAND_ROW, +}; +// internal — exposed for tests/fuzz; not part of the stable API +#[doc(hidden)] +pub use crate::sum_diff::{ + front_sum_difference_required, sum_difference_decode_f64, sum_difference_decode_i32, + sum_difference_decode_subband_pair_f64, sum_difference_decode_subband_pair_i32, + surround_sum_difference_required, +}; +pub use crate::unpack14::{pack_16bit_to_14bit, unpack_14bit_to_16bit, FourteenBitByteOrder}; + +#[cfg(feature = "registry")] +pub use crate::registry::{ + make_decoder, probe_dts, register, register_codecs, DtsDecoderHandle, CODEC_ID_STR, +}; + +// `oxideav_core::register!("dts", register)` lives inside the +// `registry` submodule; its `__oxideav_entry` wrapper needs to be +// reachable at the crate root so `oxideav-meta`'s build-time +// discovery (which calls `::__oxideav_entry(ctx)`) finds it. +// internal — exposed for tests/fuzz; not part of the stable API +#[cfg(feature = "registry")] +#[doc(hidden)] +pub use crate::registry::__oxideav_entry; + +/// Crate-local error type. Round 1 surfaces only the parser-related +/// variants; future rounds will extend this enum as decoding stages +/// land. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Error { + /// The input buffer was too short for the field being read. + UnexpectedEof, + /// None of the four documented DTS sync words matched the first + /// 4–5 bytes of the input. + NoSync, + /// A 14-bit DTS sync was detected at the 16-bit-input entry + /// point [`parse_frame_header`]. Round 2 added a dedicated + /// [`parse_frame_header_14bit`] entry point plus + /// [`unpack_14bit_to_16bit`] for callers that want to convert + /// 14-bit-packed bytes into the raw-BE form. This variant + /// remains for callers that route by sync up-front. + UnsupportedFourteenBit, + /// A raw 16-bit DTS sync was detected at the 14-bit-input entry + /// point [`crate::iter_frames_14bit`]. Symmetric counterpart to + /// [`Self::UnsupportedFourteenBit`]: the 14-bit iterator only + /// walks 14-bit-packed container streams, so a raw-16-bit sync at + /// the cursor is out-of-domain. Callers walking raw 16-bit input + /// should switch to [`crate::iter_frames`]. + UnsupportedRaw16Bit, + /// The decoded `NBLKS` field reported fewer than 5 blocks per + /// frame — the wiki/spec disallow this. + BlockCountOutOfRange { + /// Decoded number of blocks (after the +1 increment). + blocks: u8, + }, + /// The decoded frame-size field reported fewer than 95 bytes — + /// the wiki/spec disallow this. + FrameSizeOutOfRange { + /// Decoded frame size in bytes (after the +1 increment). + frame_size: u16, + }, + /// A field passed to [`crate::encode_frame_header_be`] does not + /// fit the bit-width the wiki bit-table documents (e.g. AMODE > 63 + /// for a 6-bit field, VERSION > 15 for a 4-bit field, or a + /// `header_crc: Some(_)` paired with `crc_present == false`). + /// Only the encoder returns this variant; the parser cannot + /// produce out-of-range values because every field is read from + /// the bit-vector through a width-bounded read. + FieldOutOfRange { + /// Static name of the offending [`crate::DtsFrameHeader`] + /// field. + field: &'static str, + /// Caller-supplied value. + value: u32, + /// Maximum value the field's documented bit-width can hold. + /// For the `header_crc` mismatch variant this is set to `0` + /// (the value being out-of-range is the `Some` vs `None` + /// disagreement with `crc_present`, not the integer payload). + max: u32, + }, + /// A Primary Audio Coding Side Information field + /// (§5.4.1 Table 5-28) carried a value the ETSI spec marks as + /// reserved/invalid: a `BHUFF` / `SHUFF` selector equal to `7`, + /// a `SCALES` accumulator that walked outside the documented + /// range of the §D.1.1 / §D.1.2 RMS square-root table (e.g. + /// index 63 in the 6-bit table, or indices 125..=127 in the + /// 7-bit table, both written as "invalid" in the staged PDF), + /// or a [`crate::decode_primary_side_info_at`] loop bound + /// outside its §5.3.2 range (`"nPCHS"` above 5 per PDF p.25, + /// `"nSUBS"` above the `NumSubband = 32` cap, or `"VQSUB"` + /// above the channel's `nSUBS`). + InvalidSideInfo { + /// Static name of the offending side-info field: `"BHUFF"`, + /// `"SHUFF"`, `"SCALES"`, `"nPCHS"`, `"nSUBS"`, or + /// `"VQSUB"`. + field: &'static str, + /// The reserved value the bit stream carried. + value: u32, + }, + /// The bit stream's prefix did not match any entry in the named + /// Annex D Huffman codebook (`A12`, `B12`, `C12`, `D12`, `E12`, + /// `A5`, `B5`, `C5`, `A7`, `B7`, `A4`, `B4`, `C4`, `D4`) within + /// the maximum documented code length. Surfaced by the ABITS / + /// SCALES / TMODE side-info decoders when the input is + /// structurally corrupt or the wrong codebook was selected + /// upstream. + HuffmanDecodeFailed { + /// Static name of the codebook that failed to match. + table: &'static str, + }, + /// The left and right slice arguments to a §C.2.4 sum/difference + /// decoder ([`crate::sum_difference_decode_i32`], + /// [`crate::sum_difference_decode_f64`], or one of their + /// subband-pair counterparts) had different lengths. The §C.2.4 + /// pseudocode requires a one-to-one pairing of left- and right- + /// channel samples. + SumDiffLengthMismatch { + /// Length of the left-channel slice. + left_len: usize, + /// Length of the right-channel slice. + right_len: usize, + }, + /// The slice arguments to a §C.2.3 joint-subband decoder + /// ([`crate::joint_subband_decode_range_i32`] or + /// [`crate::joint_subband_decode_range_f64`]) violated one of the + /// §C.2.3 pseudocode's structural invariants: `n_subs_dst > + /// n_subs_src` (the imported range would run backwards), a + /// per-channel subband array shorter than `n_subs_src` (no + /// storage for the imported range), a `scales` slice whose length + /// disagrees with `n_subs_src - n_subs_dst`, or a per-subband + /// destination/source sample-length disagreement. `dst_len` / + /// `src_len` carry the lengths that disagreed (the meaning is + /// context-dependent: see the documented invariants on each + /// constructor site). + JointSubbandShapeMismatch { + /// Length-like value on the destination side of the disagreement. + dst_len: usize, + /// Length-like value on the source side of the disagreement. + src_len: usize, + }, + /// A slice argument to a §C.2.2 inverse-ADPCM decoder + /// ([`crate::inverse_adpcm_decode_i32`] or + /// [`crate::inverse_adpcm_decode_f64`]) had a length that disagrees + /// with the spec's fixed `NumADPCMCoeff = 4` invariant. The + /// predictor requires a four-sample history (carrying + /// `raSample[-4..0]`) and four ADPCM coefficients + /// (`raADPCMCoeff[0..4]`); either slice having any other length is + /// out-of-domain. + InverseAdpcmShapeMismatch { + /// Caller-supplied history-buffer length (spec requires 4). + history_len: usize, + /// Caller-supplied coefficient-array length (spec requires 4). + coeffs_len: usize, + }, + /// The `n_levels` argument to [`crate::decode_block_code`] was + /// less than 2. A one-level alphabet has only the index `0` and + /// the §C.2.1 mixed-radix recurrence is undefined for + /// `n_levels < 2` (division by zero / one would never advance). + BlockCodeLevelsOutOfRange { + /// Caller-supplied `n_levels` value (spec requires `>= 2`). + n_levels: u32, + }, + /// A §C.2.1 block-code word produced a non-zero residual after + /// consuming every output element via the spec's + /// `nCode % nNumLevel` / `nCode /= nNumLevel` recurrence. The + /// spec text treats this as a fatal "ERROR: block code look-up + /// fail" condition (PDF p.183); the Rust API surfaces it as a + /// recoverable error carrying the residual + block dimensions + /// so the caller can distinguish bit-stream corruption from a + /// structural decoder bug. + BlockCodeResidual { + /// The residual code-word value after the last extraction. + residual: u32, + /// Element count passed to the decoder (`output.len()`). + n_elements: usize, + /// Quantisation-level count passed to the decoder. + n_levels: u32, + }, + /// An `ABITS` bit-allocation index passed to the §D.2 + /// quantization-step-size lookup ([`crate::StepSizeTable::step_size`] + /// or a `dequant_*` composer) was one of the `27..=31` indices the + /// staged §D.2.1 / §D.2.2 tables write "invalid" (PDF p.193-194), + /// or was `>= 32`. A defined `ABITS` index is `0..=26`. + InvalidStepSize { + /// The reserved / out-of-range `ABITS` index. + abits: u8, + }, + /// A slice argument to a §5.5 subsubframe dequantizer + /// ([`crate::scale_subsubframe_samples`] / + /// [`crate::dequant_subsubframe`]) was not exactly + /// [`crate::SAMPLES_PER_SUBSUBFRAME`] (= 8) samples long. The §5.5 + /// `Audio Data` block scales one subband analysis subwindow of + /// eight samples per call (PDF p.31-32). + SampleCountMismatch { + /// The number of samples the §5.5 loop requires (always 8). + expected: usize, + /// The slice length the caller actually supplied. + found: usize, + }, + /// The bytes at the offset handed to + /// [`crate::parse_aux_data_at`] were not the §5.7.1 DWORD-aligned + /// auxiliary-data sync word `0x9A1105A0` (`nSYNCAUX`). + AuxSyncMismatch { + /// The 32-bit word actually read at the offset. + found: u32, + }, + /// One of the two 4-bit markers bracketing the §5.7.1 Table 5-31 + /// 36-bit `nAUXTimeStamp` halves was not the documented `0b1011` + /// (`nMaker==1011`), indicating a false-positive sync match or a + /// corrupt auxiliary-data chunk. + AuxTimeStampMarkerMismatch { + /// The 4-bit value actually read. + found: u8, + }, + /// The §5.7.1 `DeriveNumDwnMixCodeCoeffs()` input-channel count + /// (`nPriCh = anNumCh[AMODE]`) could not be resolved because the + /// frame's `AMODE` is a user-defined code (`16..=63`) whose + /// channel count Table 5-4 does not define. + AuxChannelCountUnresolved { + /// The unresolvable 6-bit `AMODE` code. + amode: u8, + }, + /// The bytes at the offset handed to + /// [`crate::parse_rev2_aux_at`] were not the §5.7.2 DWORD-aligned + /// Rev2 auxiliary sync word `0x7004C070` (`nSYNCRev2AUX`). + Rev2AuxSyncMismatch { + /// The 32-bit word actually read at the offset. + found: u32, + }, + /// The §5.7.2 `nRev2AUXDataByteSize` field was outside its valid + /// `3..=128` range ("Error: Invalid range of Rev 2 Auxiliary Data + /// Chunk Size"), or the chunk's declared fields overran the + /// size-located `nRev2AUXCRC16` position. + Rev2AuxSizeOutOfRange { + /// The decoded chunk byte size (after the `+1` increment). + size: u8, + }, + /// The §5.7.2 `nEmbESDownMixScaleIndex` was outside its valid + /// `40..=240` range (the encode side limits the `ESDmixScale` + /// parameters to `[-40 dB, 0 dB]`). + Rev2AuxEsScaleIndexOutOfRange { + /// The out-of-range 8-bit index. + index: u8, + }, + /// The §5.7.2 per-subsubframe DRC value count could not be + /// derived: one 8-bit value is transmitted per 256-sample + /// subsubframe (Table 5-34), but the frame's block count is not a + /// whole number of subsubframes (`32·(NBLKS+1)` not a multiple of + /// 256). + Rev2AuxDrcCountUnresolved { + /// The frame's `NBLKS + 1` block count. + blocks: u8, + }, + /// A §5.7.1 Table 5-31 dynamic-downmix coefficient code word + /// passed to [`crate::decode_dmix_code`] was wider than the + /// documented 9-bit field, or its one-biased low byte resolved + /// past the end of the 241-entry §D.11 `DmixTable` (the + /// pseudocode's `if (nTmp > nTblSize) return false;` arm). + DownmixCodeOutOfRange { + /// The offending 9-bit (or wider) code word. + code: u16, + }, + /// The planar PCM handed to + /// [`crate::DynamicDownmix::apply_planar`] did not match the + /// coefficient table's shape: the plane count must equal the + /// §5.7.1 `nPriCh` input channel count, and every plane must have + /// the same sample count. + DownmixInputShapeMismatch { + /// The expected plane count (or, for the unequal-length case, + /// the first plane's sample count). + expected: usize, + /// The offending count actually supplied. + found: usize, + }, + /// The §5.5 Table 5-29 `DSYNC` subsubframe synchronization check + /// word ([`crate::decode_dsync_at`]) read a 16-bit value other than + /// `0xffff` (PDF p.32: `if ( DSYNC != 0xffff )`). The spec text only + /// emits a diagnostic and keeps decoding; this crate surfaces it as + /// a recoverable typed error carrying the bad word and the + /// zero-based subsubframe index it trailed, mirroring the spec's + /// `"DSYNC error at end of subsubframe #%d"` message — it is the + /// only in-band integrity check the Core profile provides for the + /// audio-data array. + DsyncMismatch { + /// The 16-bit value read where `0xffff` was expected. + found: u16, + /// The zero-based `nSubSubFrame` index the trailer followed. + n_subsubframe: u8, + }, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::UnexpectedEof => write!(f, "oxideav-dts: unexpected end of input"), + Error::NoSync => { + write!(f, "oxideav-dts: no DTS sync word found at offset 0") + } + Error::UnsupportedFourteenBit => write!( + f, + "oxideav-dts: 14-bit DTS sync detected at the 16-bit-input \ + entry point; call parse_frame_header_14bit (or \ + unpack_14bit_to_16bit + parse_frame_header) instead" + ), + Error::UnsupportedRaw16Bit => write!( + f, + "oxideav-dts: raw 16-bit DTS sync detected at the \ + 14-bit-input entry point; call iter_frames (or \ + parse_frame_header) instead" + ), + Error::BlockCountOutOfRange { blocks } => write!( + f, + "oxideav-dts: NBLKS={blocks} is out of the documented 5..=128 \ + range (spec mandates >=5)" + ), + Error::FrameSizeOutOfRange { frame_size } => write!( + f, + "oxideav-dts: frame size {frame_size} B is out of the documented \ + 95..=16384 range (spec mandates >=95)" + ), + Error::FieldOutOfRange { field, value, max } => write!( + f, + "oxideav-dts: field `{field}` value {value} exceeds the wiki \ + bit-table maximum {max}" + ), + Error::InvalidSideInfo { field, value } => write!( + f, + "oxideav-dts: side-info field `{field}` value {value} is \ + reserved/invalid per ETSI TS 102 114 §5.4.1 (Table 5-24/5-25 \ + selector 7, or §D.1.1/§D.1.2 RMS table index marked invalid)" + ), + Error::HuffmanDecodeFailed { table } => write!( + f, + "oxideav-dts: bit stream did not match any entry in Annex D \ + Huffman codebook `{table}` within the documented maximum \ + code length" + ), + Error::SumDiffLengthMismatch { + left_len, + right_len, + } => write!( + f, + "oxideav-dts: §C.2.4 sum/difference decode requires matched \ + left/right slice lengths; got left={left_len} right={right_len}" + ), + Error::JointSubbandShapeMismatch { dst_len, src_len } => write!( + f, + "oxideav-dts: §C.2.3 joint-subband decode shape mismatch; got \ + dst-side={dst_len} src-side={src_len} (see ETSI TS 102 114 \ + §C.2.3 for the structural invariants)" + ), + Error::InverseAdpcmShapeMismatch { + history_len, + coeffs_len, + } => write!( + f, + "oxideav-dts: §C.2.2 inverse-ADPCM decode requires a 4-sample \ + history and 4 ADPCM coefficients (NumADPCMCoeff = 4); got \ + history_len={history_len} coeffs_len={coeffs_len}" + ), + Error::BlockCodeLevelsOutOfRange { n_levels } => write!( + f, + "oxideav-dts: §C.2.1 block-code decode requires n_levels >= 2; \ + got n_levels={n_levels}" + ), + Error::BlockCodeResidual { + residual, + n_elements, + n_levels, + } => write!( + f, + "oxideav-dts: §C.2.1 block-code decode residual {residual} != 0 \ + after extracting {n_elements} element(s) from a base-{n_levels} \ + block (code-word out of range for the declared block dimensions)" + ), + Error::InvalidStepSize { abits } => write!( + f, + "oxideav-dts: §D.2 quantization step-size lookup: ABITS index \ + {abits} is reserved/invalid (defined range is 0..=26)" + ), + Error::SampleCountMismatch { expected, found } => write!( + f, + "oxideav-dts: §5.5 subsubframe dequantization expects {expected} \ + samples per subband analysis subwindow, got {found}" + ), + Error::AuxSyncMismatch { found } => write!( + f, + "oxideav-dts: §5.7.1 auxiliary-data parse expected the \ + DWORD-aligned sync word 0x9a1105a0, read 0x{found:08x}" + ), + Error::AuxTimeStampMarkerMismatch { found } => write!( + f, + "oxideav-dts: §5.7.1 auxiliary time-stamp marker mismatch: \ + read 0b{found:04b}, expected 0b1011" + ), + Error::AuxChannelCountUnresolved { amode } => write!( + f, + "oxideav-dts: §5.7.1 dynamic-downmix coefficient count cannot \ + be derived: AMODE {amode} is a user-defined channel \ + arrangement with no Table 5-4 channel count" + ), + Error::Rev2AuxSyncMismatch { found } => write!( + f, + "oxideav-dts: §5.7.2 Rev2 auxiliary-data parse expected the \ + DWORD-aligned sync word 0x7004c070, read 0x{found:08x}" + ), + Error::Rev2AuxSizeOutOfRange { size } => write!( + f, + "oxideav-dts: §5.7.2 Rev2 auxiliary chunk size {size} B is \ + invalid (valid range 3..=128, and the declared fields must \ + fit in front of the size-located CRC)" + ), + Error::Rev2AuxEsScaleIndexOutOfRange { index } => write!( + f, + "oxideav-dts: §5.7.2 embedded-ES downmix scale index {index} \ + is outside the valid 40..=240 range ([-40 dB, 0 dB])" + ), + Error::Rev2AuxDrcCountUnresolved { blocks } => write!( + f, + "oxideav-dts: §5.7.2 Rev2AUX DRC value count cannot be \ + derived: {blocks} blocks per frame is not a whole number of \ + 256-sample subsubframes" + ), + Error::DownmixCodeOutOfRange { code } => write!( + f, + "oxideav-dts: §5.7.1 dynamic-downmix coefficient code 0x{code:03x} \ + is out of domain (must be a 9-bit word whose one-biased low byte \ + indexes the 241-entry §D.11 DmixTable)" + ), + Error::DownmixInputShapeMismatch { expected, found } => write!( + f, + "oxideav-dts: §5.7.1 downmix fold shape mismatch: expected \ + {expected}, got {found} (plane count must equal the nPriCh \ + input channel count and all planes must be equal-length)" + ), + Error::DsyncMismatch { + found, + n_subsubframe, + } => write!( + f, + "oxideav-dts: §5.5 DSYNC error at end of subsubframe #{n_subsubframe}: \ + read 0x{found:04x}, expected 0xffff" + ), + } + } +} + +impl std::error::Error for Error {} + +/// Convenience alias for [`Result`] specialised to this crate's +/// [`Error`]. +pub type Result = core::result::Result; diff --git a/crates/vendor/oxideav-dts/src/optional_info.rs b/crates/vendor/oxideav-dts/src/optional_info.rs new file mode 100644 index 00000000..451bfa33 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/optional_info.rs @@ -0,0 +1,209 @@ +//! §5.6 "Unpack Optional Information" (Table 5-30): the flag-gated +//! region that follows the last audio-data array of a Core frame. +//! +//! Transcribed from ETSI TS 102 114 V1.3.1 (2011-08) §5.6 +//! (Table 5-30 + field descriptions, PDF p.33-34), staged at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`: +//! +//! ```text +//! if ( TIMEF==1 ) // Present only when TIMEF=1. +//! TIMES = ExtractBits(32); +//! if ( AUXF==1 ) // Present only if AUXF=1. +//! AUXCT = ExtractBits(6); +//! else +//! AUXCT = 0; // Clear it. +//! ByteAlign = ExtractBits(0 ... 7); +//! for (int n=0; n, + /// `AUXD`: the raw auxiliary data bytes (`AUXCT` of them; empty + /// when `AUXF == 0`). Their §5.7.1 content is parsed with + /// [`crate::parse_aux_data`] over the whole frame (the spec's + /// recommended sync-word navigation). + pub aux_bytes: Vec, + /// `OCRC` (present when `CPF == 1 && DYNF != 0`): the optional + /// CRC check word. Per §5.6 "The CRC value test shall not be + /// applied" — surfaced raw. + pub ocrc: Option, +} + +/// Walk the §5.6 Table 5-30 optional-information region of a Core +/// frame from `bit_offset` (the cursor left by the last §5.5 +/// audio-data array), gated by the frame header's `TIMEF` / `AUXF` / +/// `CPF` / `DYNF` flags. +/// +/// Returns the decoded region plus the number of bits consumed from +/// `bit_offset`. +/// +/// # Errors +/// +/// [`crate::Error::UnexpectedEof`] when a gated field would walk past +/// the end of `bytes`. +pub fn decode_optional_info_at( + bytes: &[u8], + bit_offset: usize, + header: &DtsFrameHeader, +) -> Result<(OptionalInfo, usize)> { + let mut br = BitReader::from_byte_offset(bytes, 0); + br.skip_bits(bit_offset as u32)?; + + let time_code_stamp = if header.time_stamp { + Some(br.read_bits(32)?) + } else { + None + }; + + let aux_count = if header.aux_data { + br.read_bits(6)? as usize + } else { + 0 + }; + + // ByteAlign = ExtractBits(0 ... 7): zero-pad to the next byte + // boundary before the AUXD byte array (see the module docs for + // the ZeroPadAux DWORD-alignment caveat). + let misalign = (br.absolute_bit_position() % 8) as u32; + if misalign != 0 { + br.skip_bits(8 - misalign)?; + } + + let mut aux_bytes = Vec::with_capacity(aux_count); + for _ in 0..aux_count { + aux_bytes.push(br.read_bits(8)? as u8); + } + + let ocrc = if header.crc_present && header.dynamic_range { + Some(br.read_bits(16)? as u16) + } else { + None + }; + + let consumed = br.absolute_bit_position() - bit_offset; + Ok(( + OptionalInfo { + time_code_stamp, + aux_bytes, + ocrc, + }, + consumed, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{synth_header, BitWriter}; + + /// 13-bit flag-window bit positions (MSB-first within the window): + /// downmix, dynamic_range, time_stamp, aux_data, hdcd, + /// ext_descr(3), ext_coding, aspf, lfe(2), predictor_history. + const DYNF: u64 = 1 << 11; + const TIMEF: u64 = 1 << 10; + const AUXF: u64 = 1 << 9; + + #[test] + fn all_flags_clear_consumes_nothing() { + let header = synth_header(2, 0); + let bytes = [0xFFu8; 8]; + let (info, consumed) = decode_optional_info_at(&bytes, 24, &header).unwrap(); + assert_eq!(info.time_code_stamp, None); + assert_eq!(info.aux_bytes, Vec::::new()); + assert_eq!(info.ocrc, None); + assert_eq!(consumed, 0); + } + + #[test] + fn times_and_aux_bytes_walk_with_byte_align() { + let header = synth_header(2, TIMEF | AUXF); + assert!(header.time_stamp); + assert!(header.aux_data); + // Region begins 3 bits into a byte: TIMES(32) + AUXCT(6) + // leaves the cursor at bit 41 -> 7 align bits precede AUXD. + let mut w = BitWriter::new(); + w.push_bits(0b101, 3); // pre-region audio bits + w.push_bits(0xDEAD_BEEF, 32); // TIMES + w.push_bits(2, 6); // AUXCT = 2 + w.align(8); // ByteAlign + w.push_bits(0xAB, 8); + w.push_bits(0xCD, 8); + let bytes = w.into_bytes(); + let (info, consumed) = decode_optional_info_at(&bytes, 3, &header).unwrap(); + assert_eq!(info.time_code_stamp, Some(0xDEAD_BEEF)); + assert_eq!(info.aux_bytes, vec![0xAB, 0xCD]); + assert_eq!(info.ocrc, None); + // 32 + 6 + 7 (align from bit 41 to 48) + 16 = 61. + assert_eq!(consumed, 61); + } + + #[test] + fn ocrc_requires_both_cpf_and_dynf() { + // DYNF alone (CPF == 0 in the synthetic header): no OCRC. + let header = synth_header(2, DYNF); + assert!(header.dynamic_range); + assert!(!header.crc_present); + let bytes = [0x12u8, 0x34]; + let (info, consumed) = decode_optional_info_at(&bytes, 0, &header).unwrap(); + assert_eq!(info.ocrc, None); + assert_eq!(consumed, 0); + } + + #[test] + fn ocrc_read_when_cpf_and_dynf_set() { + let mut header = synth_header(2, DYNF); + header.crc_present = true; // CPF (field-level override) + let bytes = [0x12u8, 0x34]; + let (info, consumed) = decode_optional_info_at(&bytes, 0, &header).unwrap(); + assert_eq!(info.ocrc, Some(0x1234)); + assert_eq!(consumed, 16); + } + + #[test] + fn truncated_region_reports_eof() { + let header = synth_header(2, TIMEF); + let bytes = [0u8; 3]; // < 32 bits for TIMES + assert_eq!( + decode_optional_info_at(&bytes, 0, &header), + Err(crate::Error::UnexpectedEof) + ); + } +} diff --git a/crates/vendor/oxideav-dts/src/qmf_assemble.rs b/crates/vendor/oxideav-dts/src/qmf_assemble.rs new file mode 100644 index 00000000..465f9250 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/qmf_assemble.rs @@ -0,0 +1,1334 @@ +//! Per-sample input assembly + shift-register update for the DTS +//! Core 32-band synthesis QMF. +//! +//! These two FIR-independent primitives bracket round 255's +//! [`crate::cos_mod_stage`] inside the §C.2.5 `QMFInterpolation()` +//! per-sample loop body, transcribed verbatim in +//! `docs/audio/dts/dts-core-extracts.md` §2.4 (ETSI TS 102 114 +//! V1.3.1 Annex C §C.2.5, staged PDF p.185). +//! +//! Per the staged §2.4 pseudocode, the body of the outer +//! `for (nSubIndex=nStart; nSubIndex=32; i--) raX[i] = raX[i-32]; +//! for (i=0; i=32; i--) raX[i] = raX[i-32]; +/// ``` +/// +/// — the loop's upper bound (`i = 511`) fixes `raX[]` at 512 +/// entries, matching the 512-tap `prCoeff` set (`raCoeffLossy` / +/// `raCoeffLossLess`, §D.8) the FIR step that drives `raX[]` +/// consumes. +pub const X_HISTORY_LEN: usize = 512; + +/// Length of the synthesis filter's `raZ[]` output accumulator, per +/// §C.2.5 / `dts-core-extracts.md` §2.4. +/// +/// The staged pseudocode indexes `raZ[]` at both `raZ[i]` and +/// `raZ[32+i]` for `i ∈ 0..32` (the FIR step writes `raZ[i]` and +/// `raZ[32+i]`; the PCM step reads `raZ[0..32]`; the rotate step +/// reads `raZ[i+32]` and writes `raZ[32+i]`): +/// +/// ```text +/// for (i=0; i NUM_SUBBAND`, and `Err(QmfAssembleError::SampleSliceTooShort)` +/// if `subband_samples.len() < n_subs` (the caller didn't supply +/// one scalar per active subband). The per-call zero-fill of the +/// inactive tail is guaranteed even when the caller's +/// `subband_samples` slice is longer than `n_subs` — the spec's +/// zero-fill step ignores the high end past `nSUBS`. +pub fn assemble_xin( + subband_samples: &[f64], + n_subs: usize, +) -> Result<[f64; NUM_SUBBAND], QmfAssembleError> { + if n_subs > NUM_SUBBAND { + return Err(QmfAssembleError::SubsOutOfRange { n_subs }); + } + if subband_samples.len() < n_subs { + return Err(QmfAssembleError::SampleSliceTooShort { + provided: subband_samples.len(), + required: n_subs, + }); + } + + // Step (a)(1): active subbands raXin[0..nSUBS]. Bulk-copy is + // semantically identical to the spec's + // `for (i=0; i=32; i--) raX[i] = raX[i-32];`). +/// +/// Rotates the 512-entry `raX[]` register by 32 entries toward the +/// high end: after the call, `raX[32..512]` holds what `raX[0..480]` +/// held on entry, and `raX[0..32]` is left untouched. The §C.2.5 +/// driver overwrites `raX[0..32]` with the next per-sample +/// cosine-modulation output ([`crate::cos_mod_stage`]) before the +/// following FIR step reads it. +/// +/// The shift runs from `i = 511` down to `i = 32` (inclusive) so +/// each write reads a slot that has not yet been overwritten — +/// directly translating the spec's reverse-iteration pseudocode. +/// `raX[0..32]` is left undefined after the shift (the spec's next +/// per-sample step writes those slots from `cos_mod_stage`'s +/// output before the FIR convolution reads them); callers are +/// expected to immediately overwrite that range before the next +/// FIR step. +/// +/// This primitive is independent of the §D.8 `raCoeffLossy` / +/// `raCoeffLossLess` 512-tap FIR coefficient tables: it only +/// rotates the shift register's content, never reads any +/// coefficients. +pub fn shift_x_history(ra_x: &mut [f64; X_HISTORY_LEN]) { + // Walk from i=511 down to i=32 (inclusive), writing + // raX[i] = raX[i - 32]. The reverse iteration is essential — + // forward iteration would overwrite low-index entries before + // the high-index entries that depend on them are read. + for i in (NUM_SUBBAND..X_HISTORY_LEN).rev() { + ra_x[i] = ra_x[i - NUM_SUBBAND]; + } +} + +/// Per-sample rotate of the synthesis filter's `raZ[]` output +/// accumulator, per `dts-core-extracts.md` §2.4 lines 218-219: +/// +/// ```text +/// for (i=0; i Result { + let end = n_ch_index.checked_add(PCM_OUTPUT_PER_SAMPLE).ok_or( + QmfAssembleError::OutputSliceTooShort { + n_ch_index, + available: na_ch.len(), + }, + )?; + if end > na_ch.len() { + return Err(QmfAssembleError::OutputSliceTooShort { + n_ch_index, + available: na_ch.len(), + }); + } + + // for (i=0; i<32; i++) naCh[nChIndex++] = int(rScale*raZ[i]); + // + // The C `int()` cast truncates toward zero, so scale then trunc + // then narrow to i32. Only raZ[0..32] is read — the accumulator's + // high block raZ[32..64] holds the *next* iteration's pre-rotate + // partial sums and is not part of this iteration's PCM output. + for i in 0..PCM_OUTPUT_PER_SAMPLE { + let scaled = r_scale * ra_z[i]; + na_ch[n_ch_index + i] = scaled.trunc() as i32; + } + + Ok(end) +} + +/// Error returned by [`assemble_xin`] when its inputs violate the +/// spec's preconditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum QmfAssembleError { + /// `n_subs` exceeds the §C.2.5 `NumSubband = 32` cap, so the + /// active-subband loop would write past the end of the + /// `raXin[0..32]` vector. + SubsOutOfRange { + /// The out-of-range `n_subs` value the caller passed. + n_subs: usize, + }, + /// The caller supplied fewer than `n_subs` per-subband samples, + /// so the assembly loop would read past the end of + /// `subband_samples`. + SampleSliceTooShort { + /// `subband_samples.len()` — the number of per-subband + /// scalars the caller supplied. + provided: usize, + /// The minimum length the §C.2.5 step requires + /// (`n_subs`). + required: usize, + }, + /// The channel output buffer `na_ch` does not have room for the + /// 32 PCM samples the §C.2.5 PCM-output step writes starting at + /// `n_ch_index`, so the write loop would run past the end of the + /// buffer. + OutputSliceTooShort { + /// The running output cursor `nChIndex` the write would + /// start at. + n_ch_index: usize, + /// `na_ch.len()` — the number of `i32` slots the caller's + /// channel buffer provides. + available: usize, + }, +} + +impl core::fmt::Display for QmfAssembleError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + QmfAssembleError::SubsOutOfRange { n_subs } => { + write!( + f, + "n_subs={n_subs} exceeds NumSubband={NUM_SUBBAND} for the §C.2.5 32-band synthesis QMF" + ) + } + QmfAssembleError::SampleSliceTooShort { provided, required } => { + write!( + f, + "subband_samples.len()={provided} is shorter than the n_subs={required} per-sample scalars required by §C.2.5" + ) + } + QmfAssembleError::OutputSliceTooShort { + n_ch_index, + available, + } => { + write!( + f, + "na_ch.len()={available} has no room for the 32 §C.2.5 PCM samples at n_ch_index={n_ch_index}" + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------- + // assemble_xin() — per-sample raXin assembly. + // ----------------------------------------------------------- + + #[test] + fn assemble_xin_full_active_count_copies_all_thirty_two_entries() { + // nSUBS = 32: every slot is an active subband, no + // zero-fill tail. + let samples: Vec = (0..NUM_SUBBAND).map(|i| (i as f64) + 0.5).collect(); + let ra_xin = assemble_xin(&samples, NUM_SUBBAND).expect("assemble succeeds at n_subs=32"); + for (i, v) in ra_xin.iter().enumerate() { + assert_eq!(*v, (i as f64) + 0.5, "raXin[{i}] = {v} mismatch"); + } + } + + #[test] + fn assemble_xin_zero_active_count_produces_silent_input() { + // nSUBS = 0: spec's first loop does nothing, second loop + // zeros the entire raXin[0..32]. Valid by the §C.2.5 + // signature. + let samples: Vec = vec![]; + let ra_xin = assemble_xin(&samples, 0).expect("assemble succeeds at n_subs=0"); + for (i, v) in ra_xin.iter().enumerate() { + assert_eq!(*v, 0.0, "raXin[{i}] = {v} should be zero"); + } + } + + #[test] + fn assemble_xin_partial_active_count_zero_fills_inactive_tail() { + // nSUBS = 5: raXin[0..5] gets the supplied samples, + // raXin[5..32] = 0.0. + let samples = [1.0, 2.0, 3.0, 4.0, 5.0]; + let ra_xin = assemble_xin(&samples, 5).expect("assemble succeeds at n_subs=5"); + for (i, expected) in samples.iter().enumerate() { + assert_eq!(ra_xin[i], *expected, "raXin[{i}] active mismatch"); + } + for (i, v) in ra_xin.iter().enumerate().skip(5) { + assert_eq!(*v, 0.0, "raXin[{i}] should be zero-filled"); + } + } + + #[test] + fn assemble_xin_ignores_extra_trailing_samples_past_n_subs() { + // §C.2.5's inactive-fill step zeros raXin past nSUBS even + // if the caller's sample slice has more entries; the + // assembly must follow nSUBS exactly, not the slice + // length. + let samples: Vec = (0..NUM_SUBBAND).map(|i| 100.0 + i as f64).collect(); + let ra_xin = assemble_xin(&samples, 7).expect("assemble succeeds at n_subs=7"); + for (i, v) in ra_xin.iter().enumerate().take(7) { + assert_eq!(*v, 100.0 + i as f64, "raXin[{i}] active mismatch"); + } + for (i, v) in ra_xin.iter().enumerate().skip(7) { + assert_eq!( + *v, 0.0, + "raXin[{i}] should be zero past nSUBS regardless of slice length" + ); + } + } + + #[test] + fn assemble_xin_rejects_n_subs_past_thirty_two() { + // n_subs = 33 would write past raXin[31]; the §C.2.5 + // signature caps nSUBS at NumSubband = 32. + let samples = [0.0_f64; 64]; + let err = assemble_xin(&samples, 33).expect_err("n_subs=33 rejected"); + assert_eq!(err, QmfAssembleError::SubsOutOfRange { n_subs: 33 }); + } + + #[test] + fn assemble_xin_rejects_short_sample_slice() { + // n_subs = 4 but only 2 samples supplied. + let samples = [10.0, 20.0]; + let err = assemble_xin(&samples, 4).expect_err("short slice rejected"); + assert_eq!( + err, + QmfAssembleError::SampleSliceTooShort { + provided: 2, + required: 4 + } + ); + } + + #[test] + fn assemble_xin_accepts_exact_length_sample_slice() { + // Boundary: subband_samples.len() == n_subs (no trailing + // slack) — spec only requires nSUBS scalars. + let samples = [7.5, 8.25, 9.125]; + let ra_xin = assemble_xin(&samples, 3).expect("exact-length slice accepted"); + assert_eq!(ra_xin[0], 7.5); + assert_eq!(ra_xin[1], 8.25); + assert_eq!(ra_xin[2], 9.125); + for v in ra_xin.iter().skip(3) { + assert_eq!(*v, 0.0); + } + } + + #[test] + fn assemble_xin_preserves_negative_and_subnormal_values() { + // Bit-identical pass-through for the active range: + // §C.2.5 reads `raXin[i] = aSubband[i].raSample[...]` + // without scaling. Cover signed + subnormal inputs to + // confirm the copy preserves them verbatim. + let samples = [-1.5_f64, f64::MIN_POSITIVE / 4.0, -0.0, 3.0]; + let ra_xin = assemble_xin(&samples, 4).expect("assemble succeeds"); + for (i, sample) in samples.iter().enumerate() { + assert_eq!( + ra_xin[i].to_bits(), + sample.to_bits(), + "raXin[{i}] bit-mismatch" + ); + } + } + + #[test] + fn assemble_xin_inactive_tail_is_positive_zero() { + // The spec writes `raXin[i] = 0.0;` — positive zero. A + // negative-zero in the tail would be a spec deviation that + // could perturb the cosine-modulation stage's behaviour at + // `i=0`'s asymmetric B[k] = raXin[0] * raCosMod[…] step. + let samples = [1.0]; + let ra_xin = assemble_xin(&samples, 1).expect("assemble succeeds"); + for (i, v) in ra_xin.iter().enumerate().skip(1) { + assert_eq!( + v.to_bits(), + 0.0_f64.to_bits(), + "raXin[{i}] should be +0.0 bit-pattern" + ); + } + } + + // ----------------------------------------------------------- + // shift_x_history() — post-PCM shift of the raX[] register. + // ----------------------------------------------------------- + + #[test] + fn shift_x_history_moves_low_half_to_high_half_by_thirty_two() { + // After the shift, raX[32..512] should hold what + // raX[0..480] held on entry. + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in ra_x.iter_mut().enumerate() { + *slot = i as f64; + } + let snapshot = ra_x; + shift_x_history(&mut ra_x); + for (i, v) in ra_x.iter().enumerate().skip(NUM_SUBBAND) { + assert_eq!( + *v, + snapshot[i - NUM_SUBBAND], + "raX[{i}] should equal pre-shift raX[{}]", + i - NUM_SUBBAND + ); + } + } + + #[test] + fn shift_x_history_leaves_first_thirty_two_entries_untouched() { + // raX[0..32] is not written by the shift (the spec's loop + // condition is `i >= 32`); the driver overwrites them + // immediately afterwards via cos_mod_stage(). + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in ra_x.iter_mut().enumerate() { + *slot = (i as f64) * 0.25; + } + let snapshot_low: Vec = ra_x[..NUM_SUBBAND].to_vec(); + shift_x_history(&mut ra_x); + for (i, expected) in snapshot_low.iter().enumerate() { + assert_eq!( + ra_x[i], *expected, + "raX[{i}] should be unchanged by the shift" + ); + } + } + + #[test] + fn shift_x_history_is_identity_on_uniform_register() { + // If every entry already holds the same value, the shift + // is a no-op (each slot is replaced with an equal value). + let mut ra_x = [4.25_f64; X_HISTORY_LEN]; + shift_x_history(&mut ra_x); + for (i, v) in ra_x.iter().enumerate() { + assert_eq!(*v, 4.25, "raX[{i}] = {v} should stay 4.25"); + } + } + + #[test] + fn shift_x_history_zeroes_propagate_into_low_block() { + // A common §C.2.5 startup state: raX[] = 0.0 everywhere. + // The shift is then a no-op and the register stays silent. + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + shift_x_history(&mut ra_x); + for (i, v) in ra_x.iter().enumerate() { + assert_eq!(*v, 0.0, "raX[{i}] = {v} should stay 0"); + } + } + + #[test] + fn shift_x_history_top_block_after_shift_is_from_pre_shift_indices_448_to_479() { + // Spot check: after the shift, raX[480..512] holds what + // raX[448..480] held on entry. + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in ra_x.iter_mut().enumerate() { + *slot = (i as f64) - 256.0; + } + shift_x_history(&mut ra_x); + for (i, v) in ra_x.iter().enumerate().skip(480) { + let expected = ((i - NUM_SUBBAND) as f64) - 256.0; + assert_eq!( + *v, + expected, + "raX[{i}] should be pre-shift raX[{}] = {expected}", + i - NUM_SUBBAND + ); + } + } + + #[test] + fn shift_x_history_reverse_iteration_does_not_chain_overwrites() { + // Sanity check: if the implementation walked forward + // instead of in reverse, raX[32] would be overwritten with + // raX[0], then raX[64] would be overwritten with raX[32] + // (which is now raX[0]), etc. — every i ≡ 0 (mod 32) slot + // would collapse to raX[0]'s original value. + // Construct an input where this failure mode would be + // visible (distinct values at the 32-step boundaries) and + // confirm the reverse-walking implementation handles it + // correctly. + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in ra_x.iter_mut().enumerate() { + *slot = i as f64; // raX[i] = i, all 512 values distinct + } + shift_x_history(&mut ra_x); + // raX[64] should hold the pre-shift raX[32] = 32, not + // raX[0] = 0 (the forward-iteration mistake). + assert_eq!(ra_x[64], 32.0, "raX[64] reverse-shift mismatch"); + assert_eq!(ra_x[96], 64.0, "raX[96] reverse-shift mismatch"); + // And the top of the register holds the highest pre-shift + // index that's still in range. + assert_eq!(ra_x[511], (511 - NUM_SUBBAND) as f64); + } + + #[test] + fn shift_x_history_repeated_calls_walk_block_by_block() { + // Two consecutive shifts displace the low block by 64 + // entries; three shifts displace it by 96; etc. — confirm + // the primitive composes correctly across per-sample + // iterations. + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in ra_x.iter_mut().enumerate() { + *slot = i as f64; + } + // Reserve raX[0..32] = sentinel before the first shift + // (matching what cos_mod_stage would write into raX[0..32] + // before the next per-sample iteration writes again). + for v in ra_x.iter_mut().take(NUM_SUBBAND) { + *v = -1.0; + } + shift_x_history(&mut ra_x); + // After 1 shift, raX[64] = pre-shift raX[32] = 32. + assert_eq!(ra_x[64], 32.0); + // Reserve raX[0..32] = sentinel again. + for v in ra_x.iter_mut().take(NUM_SUBBAND) { + *v = -2.0; + } + shift_x_history(&mut ra_x); + // After 2 shifts, raX[96] holds what raX[64] held one + // shift ago, which held what raX[32] held before any + // shift — i.e., raX[96] = 32. + assert_eq!(ra_x[96], 32.0); + } + + // ----------------------------------------------------------- + // shift_z_output() — post-PCM rotate of the raZ[] accumulator. + // ----------------------------------------------------------- + + #[test] + fn shift_z_output_moves_high_block_down_into_low_block() { + // After the rotate, raZ[0..32] should hold what raZ[32..64] + // held on entry (the spec's `raZ[i] = raZ[i+32]`). + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().enumerate() { + *slot = i as f64; + } + let snapshot = ra_z; + shift_z_output(&mut ra_z); + for (i, v) in ra_z.iter().enumerate().take(NUM_SUBBAND) { + assert_eq!( + *v, + snapshot[i + NUM_SUBBAND], + "raZ[{i}] should equal pre-rotate raZ[{}]", + i + NUM_SUBBAND + ); + } + } + + #[test] + fn shift_z_output_zeros_the_high_block() { + // After the rotate, raZ[32..64] should all be +0.0 (the + // spec's `raZ[i+32] = 0.0`), readying it for the next + // per-sample FIR accumulation. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().enumerate() { + *slot = (i as f64) + 1.0; // all non-zero so a missed clear is visible + } + shift_z_output(&mut ra_z); + for (i, v) in ra_z.iter().enumerate().skip(NUM_SUBBAND) { + assert_eq!( + v.to_bits(), + 0.0_f64.to_bits(), + "raZ[{i}] should be cleared to +0.0" + ); + } + } + + #[test] + fn shift_z_output_high_block_is_positive_zero_not_negative() { + // The spec writes `(real)0.0` — positive zero. A negative + // zero in the cleared block could perturb a later FIR + // accumulation that starts from `raZ[32+i]`. + let mut ra_z = [-3.0_f64; Z_OUTPUT_LEN]; + shift_z_output(&mut ra_z); + for (i, v) in ra_z.iter().enumerate().skip(NUM_SUBBAND) { + assert_eq!( + v.to_bits(), + 0.0_f64.to_bits(), + "raZ[{i}] should be the +0.0 bit-pattern, not -0.0" + ); + } + } + + #[test] + fn shift_z_output_low_block_is_independent_of_prior_low_values() { + // raZ[0..32] is fully overwritten by raZ[32..64]; the + // pre-rotate low-block content must not leak through. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + // Distinct sentinel in the low block; a known ramp in the + // high block. + for slot in ra_z.iter_mut().take(NUM_SUBBAND) { + *slot = 999.0; + } + for (i, slot) in ra_z.iter_mut().enumerate().skip(NUM_SUBBAND) { + *slot = (i - NUM_SUBBAND) as f64 - 100.0; + } + shift_z_output(&mut ra_z); + for (i, v) in ra_z.iter().enumerate().take(NUM_SUBBAND) { + assert_eq!( + *v, + (i as f64) - 100.0, + "raZ[{i}] should come from the high block, not the prior low block" + ); + } + } + + #[test] + fn shift_z_output_on_all_zero_accumulator_is_a_no_op() { + // A common §C.2.5 startup state: raZ[] = 0.0 everywhere. + // The rotate leaves it silent. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + shift_z_output(&mut ra_z); + for (i, v) in ra_z.iter().enumerate() { + assert_eq!(*v, 0.0, "raZ[{i}] = {v} should stay 0"); + } + } + + #[test] + fn shift_z_output_preserves_signed_and_subnormal_high_block_values() { + // The down-shift is a verbatim copy — signed and subnormal + // f64s in the high block must arrive bit-identically in the + // low block. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + let specials = [-1.5_f64, f64::MIN_POSITIVE / 4.0, -0.0, 7.25]; + for (k, s) in specials.iter().enumerate() { + ra_z[NUM_SUBBAND + k] = *s; + } + shift_z_output(&mut ra_z); + for (k, s) in specials.iter().enumerate() { + assert_eq!( + ra_z[k].to_bits(), + s.to_bits(), + "raZ[{k}] bit-mismatch after down-shift" + ); + } + } + + #[test] + fn shift_z_output_two_rotates_walk_the_accumulator_block_by_block() { + // Simulate two per-sample iterations: between the rotates the + // driver's FIR step would refill raZ[32..64]. Confirm the + // first rotate exposes the original high block and the second + // rotate exposes whatever the (simulated) FIR step wrote. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().enumerate().skip(NUM_SUBBAND) { + *slot = (i - NUM_SUBBAND) as f64; // raZ[32..64] = 0..32 + } + shift_z_output(&mut ra_z); + // raZ[0..32] now holds 0..32; raZ[32..64] cleared. + assert_eq!(ra_z[0], 0.0); + assert_eq!(ra_z[31], 31.0); + // Simulated FIR step for the next iteration refills the high + // block with a fresh ramp. + for (i, slot) in ra_z.iter_mut().enumerate().skip(NUM_SUBBAND) { + *slot = 1000.0 + (i - NUM_SUBBAND) as f64; + } + shift_z_output(&mut ra_z); + // The second rotate exposes the freshly-written high block. + assert_eq!(ra_z[0], 1000.0); + assert_eq!(ra_z[31], 1031.0); + for v in ra_z.iter().skip(NUM_SUBBAND) { + assert_eq!(*v, 0.0, "high block should be cleared after second rotate"); + } + } + + // ----------------------------------------------------------- + // fir_step() — §C.2.5 512-tap FIR convolution step (c). + // ----------------------------------------------------------- + + /// Verbatim line-for-line transcription of the §C.2.5 FIR + /// pseudocode (C-style index walk with the paired `i++, k--` + /// updates and the `j += 64` stride), used as a bit-exact + /// reference against the production [`fir_step`]. + fn reference_fir_step( + ra_x: &[f64; X_HISTORY_LEN], + pr_coeff: &[f64; X_HISTORY_LEN], + ra_z: &mut [f64; Z_OUTPUT_LEN], + ) { + // for (k=31,i=0; i<32; i++,k--) + // for (j=0; j<512; j+=64) + // raZ[i] += prCoeff[i+j] * ( raX[i+j]-raX[j+k]); + let mut k: isize = 31; + let mut i: isize = 0; + while i < 32 { + let mut j: isize = 0; + while j < 512 { + ra_z[i as usize] += + pr_coeff[(i + j) as usize] * (ra_x[(i + j) as usize] - ra_x[(j + k) as usize]); + j += 64; + } + i += 1; + k -= 1; + } + // for (k=31,i=0; i<32; i++,k--) + // for (j=0; j<512; j+=64) + // raZ[32+i] += prCoeff[32+i+j] * (-raX[i+j]-raX[j+k]); + let mut k: isize = 31; + let mut i: isize = 0; + while i < 32 { + let mut j: isize = 0; + while j < 512 { + ra_z[(32 + i) as usize] += pr_coeff[(32 + i + j) as usize] + * (-ra_x[(i + j) as usize] - ra_x[(j + k) as usize]); + j += 64; + } + i += 1; + k -= 1; + } + } + + /// Deterministic pseudo-random `raX[]` fill (multiplicative LCG; + /// no external randomness) spanning distinct signs/magnitudes. + fn pseudo_random_x(seed: u64) -> [f64; X_HISTORY_LEN] { + let mut state = seed; + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + for slot in ra_x.iter_mut() { + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + // Map the top 32 bits onto roughly [-1, 1). + *slot = ((state >> 32) as i64 - (1_i64 << 31)) as f64 / (1_i64 << 31) as f64; + } + ra_x + } + + #[test] + fn fir_step_on_silent_register_leaves_accumulator_unchanged() { + // raX[] = 0 → every (raX[i+j] - raX[j+k]) term is zero; the + // accumulator (pre-loaded with sentinels) must be unchanged + // for both §D.8 coefficient sets. + use crate::fir_coeff::{RA_COEFF_LOSSLESS, RA_COEFF_LOSSY}; + let ra_x = [0.0_f64; X_HISTORY_LEN]; + for table in [&RA_COEFF_LOSSLESS, &RA_COEFF_LOSSY] { + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().enumerate() { + *slot = 100.0 + i as f64; + } + let snapshot = ra_z; + fir_step(&ra_x, table, &mut ra_z); + assert_eq!(ra_z, snapshot, "silent raX must not move the accumulator"); + } + } + + #[test] + fn fir_step_accumulates_into_ra_z_instead_of_overwriting() { + // The pseudocode writes `raZ[…] += …`; pre-loaded partials + // from the previous per-sample iteration must survive and the + // accumulation must walk in the spec's order. Run both the + // production step and the verbatim reference from the SAME + // non-zero accumulator and require bit-identical results. + use crate::fir_coeff::RA_COEFF_LOSSLESS; + let ra_x = pseudo_random_x(7); + let mut got = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in got.iter_mut().enumerate() { + *slot = 1000.0 + i as f64; + } + let preload = got; + let mut expected = got; + fir_step(&ra_x, &RA_COEFF_LOSSLESS, &mut got); + reference_fir_step(&ra_x, &RA_COEFF_LOSSLESS, &mut expected); + for i in 0..Z_OUTPUT_LEN { + assert_eq!( + got[i].to_bits(), + expected[i].to_bits(), + "raZ[{i}] mismatch vs reference on preloaded accumulator" + ); + assert_ne!( + got[i], preload[i], + "raZ[{i}] must have received a contribution" + ); + } + // Exact-arithmetic spot check: a single dyadic tap on a + // preloaded slot adds exactly. + let mut pr_coeff = [0.0_f64; X_HISTORY_LEN]; + pr_coeff[64] = 1.0; + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + ra_x[64] = 5.0; + ra_x[95] = 2.0; + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + ra_z[0] = 1000.0; + fir_step(&ra_x, &pr_coeff, &mut ra_z); + assert_eq!(ra_z[0], 1003.0, "raZ[0] = 1000.0 + 1.0*(5.0-2.0)"); + } + + #[test] + fn fir_step_matches_verbatim_reference_bit_exactly_on_both_d8_tables() { + // Bit-exact (to_bits) agreement with the line-for-line + // §C.2.5 transcription across ramp, alternating-sign, and + // pseudo-random raX[] fills, for both §D.8 sets. + use crate::fir_coeff::{RA_COEFF_LOSSLESS, RA_COEFF_LOSSY}; + let mut ramp = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in ramp.iter_mut().enumerate() { + *slot = (i as f64) * 0.001 - 0.25; + } + let mut alternating = [0.0_f64; X_HISTORY_LEN]; + for (i, slot) in alternating.iter_mut().enumerate() { + *slot = if i % 2 == 0 { 1.0 } else { -1.0 }; + } + let inputs = [ramp, alternating, pseudo_random_x(1), pseudo_random_x(42)]; + for table in [&RA_COEFF_LOSSLESS, &RA_COEFF_LOSSY] { + for (n, ra_x) in inputs.iter().enumerate() { + let mut got = [0.0_f64; Z_OUTPUT_LEN]; + let mut expected = [0.0_f64; Z_OUTPUT_LEN]; + fir_step(ra_x, table, &mut got); + reference_fir_step(ra_x, table, &mut expected); + for i in 0..Z_OUTPUT_LEN { + assert_eq!( + got[i].to_bits(), + expected[i].to_bits(), + "raZ[{i}] mismatch vs reference on input #{n}" + ); + } + } + } + } + + #[test] + fn fir_step_low_half_tap_maps_pr_coeff_i_plus_j() { + // Isolate prCoeff[64]: it is read exactly once, by the first + // loop at (i=0, j=64, k=31), contributing + // prCoeff[64] * (raX[64] - raX[95]) to raZ[0]. + let mut pr_coeff = [0.0_f64; X_HISTORY_LEN]; + pr_coeff[64] = 1.0; + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + ra_x[64] = 5.0; + ra_x[95] = 2.0; // j + k = 64 + 31 + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + fir_step(&ra_x, &pr_coeff, &mut ra_z); + assert_eq!(ra_z[0], 3.0, "raZ[0] = prCoeff[64]*(raX[64]-raX[95])"); + for (i, v) in ra_z.iter().enumerate().skip(1) { + assert_eq!(*v, 0.0, "raZ[{i}] must receive no contribution"); + } + } + + #[test] + fn fir_step_high_half_tap_maps_pr_coeff_32_plus_i_plus_j() { + // Isolate prCoeff[32]: it is read exactly once, by the second + // loop at (i=0, j=0, k=31), contributing + // prCoeff[32] * (-raX[0] - raX[31]) to raZ[32]. + let mut pr_coeff = [0.0_f64; X_HISTORY_LEN]; + pr_coeff[32] = 1.0; + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + ra_x[0] = 2.0; + ra_x[31] = 3.0; // j + k = 0 + 31 + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + fir_step(&ra_x, &pr_coeff, &mut ra_z); + assert_eq!(ra_z[32], -5.0, "raZ[32] = prCoeff[32]*(-raX[0]-raX[31])"); + for (i, v) in ra_z.iter().enumerate() { + if i != 32 { + assert_eq!(*v, 0.0, "raZ[{i}] must receive no contribution"); + } + } + } + + #[test] + fn fir_step_uses_each_coefficient_exactly_eight_taps_per_output() { + // With prCoeff ≡ 1 and raX ≡ c: the first loop's terms are + // (c - c) = 0, so raZ[0..32] stays silent; the second loop's + // terms are (-c - c) = -2c, eight j-steps each, so + // raZ[32..64] = 8 * (-2c) = -16c — confirming the 8-tap + // (j = 0, 64, …, 448) walk per output slot. + let pr_coeff = [1.0_f64; X_HISTORY_LEN]; + let ra_x = [0.5_f64; X_HISTORY_LEN]; + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + fir_step(&ra_x, &pr_coeff, &mut ra_z); + for (i, v) in ra_z.iter().enumerate().take(NUM_SUBBAND) { + assert_eq!(*v, 0.0, "raZ[{i}]: (c - c) terms must cancel"); + } + for (i, v) in ra_z.iter().enumerate().skip(NUM_SUBBAND) { + assert_eq!(*v, -16.0 * 0.5, "raZ[{i}]: 8 taps of -2c expected"); + } + } + + #[test] + fn fir_step_is_linear_in_the_shift_register() { + // The step is a fixed linear map of raX[] (sums of products + // against constant coefficients); scaling the register by a + // power of two scales the accumulation exactly. + use crate::fir_coeff::RA_COEFF_LOSSY; + let ra_x = pseudo_random_x(99); + let mut doubled = ra_x; + for slot in doubled.iter_mut() { + *slot *= 2.0; + } + let mut z1 = [0.0_f64; Z_OUTPUT_LEN]; + let mut z2 = [0.0_f64; Z_OUTPUT_LEN]; + fir_step(&ra_x, &RA_COEFF_LOSSY, &mut z1); + fir_step(&doubled, &RA_COEFF_LOSSY, &mut z2); + for i in 0..Z_OUTPUT_LEN { + assert_eq!( + z2[i].to_bits(), + (2.0 * z1[i]).to_bits(), + "raZ[{i}]: doubling raX must exactly double the contribution" + ); + } + } + + // ----------------------------------------------------------- + // write_pcm_output() — per-sample PCM-output step. + // ----------------------------------------------------------- + + #[test] + fn write_pcm_output_emits_thirty_two_samples_and_advances_cursor() { + // raZ[0..32] = i, rScale = 1.0 → naCh[i] = int(1.0*i) = i. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().take(NUM_SUBBAND).enumerate() { + *slot = i as f64; + } + let mut na_ch = [0_i32; NUM_SUBBAND]; + let next = write_pcm_output(&ra_z, 1.0, &mut na_ch, 0).expect("fits exactly"); + assert_eq!(next, NUM_SUBBAND, "cursor advances by 32"); + for (i, v) in na_ch.iter().enumerate() { + assert_eq!(*v, i as i32, "naCh[{i}] mismatch"); + } + } + + #[test] + fn write_pcm_output_applies_scale_before_cast() { + // rScale = 4.0, raZ[i] = i → naCh[i] = int(4.0*i) = 4*i. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().take(NUM_SUBBAND).enumerate() { + *slot = i as f64; + } + let mut na_ch = [0_i32; NUM_SUBBAND]; + write_pcm_output(&ra_z, 4.0, &mut na_ch, 0).expect("fits"); + for (i, v) in na_ch.iter().enumerate() { + assert_eq!(*v, 4 * i as i32, "naCh[{i}] = 4*{i} expected"); + } + } + + #[test] + fn write_pcm_output_truncates_toward_zero() { + // int() in C discards the fractional part toward zero for both + // signs: int(2.9) = 2, int(-2.9) = -2, int(-0.4) = 0. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + ra_z[0] = 2.9; + ra_z[1] = -2.9; + ra_z[2] = -0.4; + ra_z[3] = 0.99; + ra_z[4] = -0.99; + let mut na_ch = [0_i32; NUM_SUBBAND]; + write_pcm_output(&ra_z, 1.0, &mut na_ch, 0).expect("fits"); + assert_eq!(na_ch[0], 2, "int(2.9) = 2"); + assert_eq!(na_ch[1], -2, "int(-2.9) = -2"); + assert_eq!(na_ch[2], 0, "int(-0.4) = 0 (toward zero)"); + assert_eq!(na_ch[3], 0, "int(0.99) = 0"); + assert_eq!(na_ch[4], 0, "int(-0.99) = 0"); + } + + #[test] + fn write_pcm_output_scaling_then_truncation_order_matters() { + // The cast happens AFTER the scale: int(0.5 * 3.0) = int(1.5) + // = 1, not int(0.5)*3 = 0. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + ra_z[0] = 3.0; + let mut na_ch = [0_i32; NUM_SUBBAND]; + write_pcm_output(&ra_z, 0.5, &mut na_ch, 0).expect("fits"); + assert_eq!(na_ch[0], 1, "int(0.5 * 3.0) = int(1.5) = 1"); + } + + #[test] + fn write_pcm_output_writes_at_running_cursor_and_returns_new_cursor() { + // A buffer of three iterations' worth; write into the second + // slot-block and confirm the first and third are untouched. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for (i, slot) in ra_z.iter_mut().take(NUM_SUBBAND).enumerate() { + *slot = (100 + i) as f64; + } + let mut na_ch = [-1_i32; 3 * NUM_SUBBAND]; + let next = write_pcm_output(&ra_z, 1.0, &mut na_ch, NUM_SUBBAND).expect("fits"); + assert_eq!(next, 2 * NUM_SUBBAND, "cursor advances 32→64"); + // First block untouched. + for v in &na_ch[..NUM_SUBBAND] { + assert_eq!(*v, -1, "first block must not be written"); + } + // Second block holds the output. + for (i, v) in na_ch[NUM_SUBBAND..2 * NUM_SUBBAND].iter().enumerate() { + assert_eq!(*v, 100 + i as i32, "naCh[{}] mismatch", NUM_SUBBAND + i); + } + // Third block untouched. + for v in &na_ch[2 * NUM_SUBBAND..] { + assert_eq!(*v, -1, "third block must not be written"); + } + } + + #[test] + fn write_pcm_output_reads_only_low_block_not_high_accumulator() { + // raZ[32..64] holds the next iteration's pre-rotate partials; + // they must NOT leak into this iteration's output. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + for slot in ra_z.iter_mut().skip(NUM_SUBBAND) { + *slot = 9999.0; // high block — must be ignored + } + let mut na_ch = [0_i32; NUM_SUBBAND]; + write_pcm_output(&ra_z, 1.0, &mut na_ch, 0).expect("fits"); + for (i, v) in na_ch.iter().enumerate() { + assert_eq!( + *v, 0, + "naCh[{i}] must come from raZ[0..32]=0, not the high block" + ); + } + } + + #[test] + fn write_pcm_output_rejects_buffer_too_short() { + let ra_z = [0.0_f64; Z_OUTPUT_LEN]; + let mut na_ch = [0_i32; NUM_SUBBAND - 1]; // one slot short + let err = write_pcm_output(&ra_z, 1.0, &mut na_ch, 0).unwrap_err(); + assert_eq!( + err, + QmfAssembleError::OutputSliceTooShort { + n_ch_index: 0, + available: NUM_SUBBAND - 1, + } + ); + } + + #[test] + fn write_pcm_output_rejects_cursor_past_room() { + // Buffer has room for exactly 32 samples but the cursor starts + // at 1, so 1+32 = 33 > 32. + let ra_z = [0.0_f64; Z_OUTPUT_LEN]; + let mut na_ch = [0_i32; NUM_SUBBAND]; + let err = write_pcm_output(&ra_z, 1.0, &mut na_ch, 1).unwrap_err(); + assert_eq!( + err, + QmfAssembleError::OutputSliceTooShort { + n_ch_index: 1, + available: NUM_SUBBAND, + } + ); + } + + #[test] + fn write_pcm_output_negative_scale_flips_sign() { + // A negative rScale negates each sample before the cast. + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + ra_z[0] = 5.0; + ra_z[1] = -3.0; + let mut na_ch = [0_i32; NUM_SUBBAND]; + write_pcm_output(&ra_z, -2.0, &mut na_ch, 0).expect("fits"); + assert_eq!(na_ch[0], -10, "int(-2.0 * 5.0) = -10"); + assert_eq!(na_ch[1], 6, "int(-2.0 * -3.0) = 6"); + } + + #[test] + fn write_pcm_output_per_sample_constant_is_num_subband() { + assert_eq!(PCM_OUTPUT_PER_SAMPLE, NUM_SUBBAND); + assert_eq!(PCM_OUTPUT_PER_SAMPLE, 32); + } + + #[test] + fn write_pcm_output_error_renders_human_readable_message() { + let err = QmfAssembleError::OutputSliceTooShort { + n_ch_index: 64, + available: 80, + }; + let msg = format!("{err}"); + assert!(msg.contains("64"), "message names the cursor: {msg}"); + assert!( + msg.contains("80"), + "message names the available length: {msg}" + ); + } + + // ----------------------------------------------------------- + // Constants + // ----------------------------------------------------------- + + #[test] + fn z_output_len_is_sixty_four() { + // §C.2.5 / §2.4 lines 218-219 index raZ[] at raZ[i] and + // raZ[i+32] for i in 0..32, so the accumulator spans + // 2 * NumSubband = 64 entries. + assert_eq!(Z_OUTPUT_LEN, 64); + } + + #[test] + fn z_output_len_is_twice_num_subband() { + // The rotate writes raZ[i] = raZ[i+32] and raZ[32+i] = 0.0; + // both index ranges fit exactly when Z_OUTPUT_LEN = 2 * + // NUM_SUBBAND. + assert_eq!(Z_OUTPUT_LEN, 2 * NUM_SUBBAND); + } + + #[test] + fn x_history_len_is_five_hundred_twelve() { + // §C.2.5 / §2.4 line 217 caps `raX[]` at 512 entries, + // matching the 512-tap §D.8 FIR set the driver consumes. + assert_eq!(X_HISTORY_LEN, 512); + } + + #[test] + fn x_history_len_is_a_whole_multiple_of_num_subband() { + // The shift step writes `raX[i] = raX[i-32]`, which would + // overrun or under-fill the register if X_HISTORY_LEN + // weren't a multiple of NUM_SUBBAND. 512 = 16 * 32. + assert_eq!(X_HISTORY_LEN % NUM_SUBBAND, 0); + assert_eq!(X_HISTORY_LEN / NUM_SUBBAND, 16); + } + + // ----------------------------------------------------------- + // Error rendering + // ----------------------------------------------------------- + + #[test] + fn subs_out_of_range_error_renders_human_readable_message() { + let err = QmfAssembleError::SubsOutOfRange { n_subs: 64 }; + let s = format!("{err}"); + assert!(s.contains("64"), "message should include the bad n_subs"); + assert!( + s.contains("NumSubband"), + "message should reference the spec's cap" + ); + } + + #[test] + fn sample_slice_too_short_error_renders_provided_and_required() { + let err = QmfAssembleError::SampleSliceTooShort { + provided: 2, + required: 5, + }; + let s = format!("{err}"); + assert!(s.contains("2"), "message should include provided length"); + assert!(s.contains("5"), "message should include required length"); + } +} diff --git a/crates/vendor/oxideav-dts/src/qmf_multichannel.rs b/crates/vendor/oxideav-dts/src/qmf_multichannel.rs new file mode 100644 index 00000000..ee948eeb --- /dev/null +++ b/crates/vendor/oxideav-dts/src/qmf_multichannel.rs @@ -0,0 +1,594 @@ +//! Per-frame multi-channel 32-band synthesis QMF driver — the +//! channel-loop wrapper around the §C.2.5 `QMFInterpolation()` +//! per-channel call. +//! +//! The §C.2.5 normative driver (transcribed verbatim in +//! `docs/audio/dts/dts-core-extracts.md` §2.4, ETSI TS 102 114 V1.3.1 +//! Annex C §C.2.5, staged PDF p.185) is invoked **once per channel**: +//! +//! ```text +//! aPrmCh[ch].QMFInterpolation(FILTS, nSUBS[ch]); +//! ``` +//! +//! where `aPrmCh[ch]` is the persistent per-channel filter object +//! ([`crate::QmfSynthesis`]) and `nSUBS[ch]` is that channel's count of +//! active subbands (§C.2.5: "higher subbands zero; joint-intensity-coded +//! subbands take the source channel's value"). `FILTS` (the §5.3.1 +//! Table 5-15 "Multirate Interpolator Switch") is a single frame-header +//! flag shared by every channel, and the output `rScale` is the single +//! post-filterbank float→PCM gain derived from the frame header's `PCMR` +//! source resolution (`docs/audio/dts/dts-qmf-driver.md` §1/§2). Both +//! are constant across the frame's channels. +//! +//! [`MultiChannelQmf`] owns one [`QmfSynthesis`] per channel and runs +//! the §C.2.5 per-channel call for all of them over one block of +//! subband samples, so a caller that has decoded a frame's per-channel +//! subband samples reconstructs the whole frame's PCM in one call. The +//! per-channel filter state (`raX[]` / `raZ[]`) persists across calls +//! exactly as each underlying [`QmfSynthesis`] persists it, so feeding +//! a stream's frames in order carries every channel's inter-frame +//! filter tail correctly. +//! +//! # Scope: composition of landed primitives +//! +//! This module composes the already-landed [`QmfSynthesis`] driver (the +//! §C.2.5 per-sample loop body) across channels. It adds no new spec +//! step — the only spec construct it materialises is the channel loop +//! around `aPrmCh[ch].QMFInterpolation(...)` and the planar/interleaved +//! arrangement of the per-channel `naCh[]` outputs. The header-sourced +//! `FILTS` / `rScale` come from [`crate::DtsFrameHeader`] accessors +//! ([`crate::DtsFrameHeader::filter_bank_selection`] / +//! [`crate::DtsFrameHeader::output_r_scale`]) per the round-335 bridge. + +use crate::cos_mod::NUM_SUBBAND; +use crate::filter_bank::FilterBankSelection; +use crate::qmf_assemble::{QmfAssembleError, PCM_OUTPUT_PER_SAMPLE}; +use crate::qmf_synth::QmfSynthesis; + +/// Errors specific to the multi-channel synthesis driver, layered over +/// the per-channel [`QmfAssembleError`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum MultiChannelQmfError { + /// A per-channel synthesis step failed; carries the channel index + /// `ch` at which it failed and the underlying [`QmfAssembleError`]. + Channel { + /// 0-based channel index whose §C.2.5 call failed. + ch: usize, + /// The underlying per-channel error. + source: QmfAssembleError, + }, + /// The caller-supplied per-channel `n_subs` slice length did not + /// match the driver's channel count. + NSubsLenMismatch { + /// The driver's configured channel count. + channels: usize, + /// The length of the supplied `n_subs` slice. + got: usize, + }, + /// The caller-supplied per-channel subband-sample slice count did + /// not match the driver's channel count. + ChannelSlicesLenMismatch { + /// The driver's configured channel count. + channels: usize, + /// The number of per-channel slices supplied. + got: usize, + }, + /// Two channels carried a different number of sample rows. Every + /// channel of one frame block must carry the same number of + /// per-sample subband rows (the §C.2.5 outer loop runs + /// `nStart..nEnd` identically for every channel of a frame). + RowCountMismatch { + /// Row count of channel 0 (the reference). + expected: usize, + /// 0-based channel index whose row count differed. + ch: usize, + /// That channel's row count. + got: usize, + }, +} + +impl core::fmt::Display for MultiChannelQmfError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + MultiChannelQmfError::Channel { ch, source } => { + write!(f, "channel {ch} synthesis failed: {source}") + } + MultiChannelQmfError::NSubsLenMismatch { channels, got } => { + write!( + f, + "n_subs length {got} does not match channel count {channels}" + ) + } + MultiChannelQmfError::ChannelSlicesLenMismatch { channels, got } => { + write!( + f, + "channel-slices count {got} does not match channel count {channels}" + ) + } + MultiChannelQmfError::RowCountMismatch { expected, ch, got } => { + write!( + f, + "channel {ch} carried {got} sample rows, expected {expected}" + ) + } + } + } +} + +impl std::error::Error for MultiChannelQmfError {} + +/// Persistent per-frame, multi-channel 32-band synthesis QMF. +/// +/// Holds one [`QmfSynthesis`] per channel — the §C.2.5 `aPrmCh[ch]` +/// filter objects — and drives the per-channel `QMFInterpolation()` +/// call for every channel over one block of subband samples. The +/// per-channel filter state persists across [`MultiChannelQmf::synthesize_planar`] +/// / [`MultiChannelQmf::synthesize_interleaved`] calls, so a multi-frame +/// stream reuses one instance and feeds its frames in order. +#[derive(Debug, Clone)] +pub struct MultiChannelQmf { + /// One persistent §C.2.5 filter object per channel. + channels: Vec, +} + +impl MultiChannelQmf { + /// Construct a driver for `channels` channels, each with a freshly + /// cleared per-channel filter history (matching the §C.2.5 + /// per-channel filter's initial state before the first subframe). + /// + /// `channels` is the frame's audio-channel count — e.g. the value + /// from [`crate::DtsFrameHeader::channel_count`]. A zero-channel + /// driver is permitted (it produces no output) for callers that + /// resolve the channel count dynamically. + #[must_use] + pub fn new(channels: usize) -> Self { + Self { + channels: (0..channels).map(|_| QmfSynthesis::new()).collect(), + } + } + + /// The driver's channel count. + #[must_use] + pub fn channel_count(&self) -> usize { + self.channels.len() + } + + /// Borrow the per-channel [`QmfSynthesis`] filter objects (the + /// §C.2.5 `aPrmCh[]` array). Exposed for callers that want to + /// inspect or checkpoint a channel's inter-frame filter tail. + #[must_use] + pub fn channels(&self) -> &[QmfSynthesis] { + &self.channels + } + + /// Run the §C.2.5 per-channel `QMFInterpolation()` call for every + /// channel and append the result **planar** — channel 0's PCM + /// first, then channel 1's, and so on — to `output[ch]`. + /// + /// `channel_samples[ch]` is channel `ch`'s block of per-sample + /// subband rows (one `[f64; 32]` per `nSubIndex`, the same layout + /// [`QmfSynthesis::synthesize`] consumes). `n_subs[ch]` is that + /// channel's count of active subbands (`nSUBS[ch]`). `filter` and + /// `r_scale` are the frame-wide §C.2.5 parameters (the resolved + /// `FILTS` branch and the post-filterbank output `rScale`); see + /// [`MultiChannelQmf::synthesize_planar_from_header`] for sourcing + /// them from a parsed header. + /// + /// `output` must have one `Vec` per channel; each channel's + /// reconstructed PCM (`rows * 32` samples) is appended to its own + /// vec. The per-channel filter state persists for the next call. + /// + /// # Errors + /// + /// - [`MultiChannelQmfError::NSubsLenMismatch`] / + /// [`MultiChannelQmfError::ChannelSlicesLenMismatch`] if the + /// per-channel slice lengths do not match the channel count (or + /// the `output` length differs, which is reported as a + /// channel-slices mismatch against `output`). + /// - [`MultiChannelQmfError::RowCountMismatch`] if two channels + /// carry a different number of sample rows. + /// - [`MultiChannelQmfError::Channel`] wrapping the per-channel + /// [`QmfAssembleError`] if a channel's §C.2.5 call fails. + pub fn synthesize_planar( + &mut self, + channel_samples: &[&[[f64; NUM_SUBBAND]]], + n_subs: &[usize], + filter: FilterBankSelection, + r_scale: f64, + output: &mut [Vec], + ) -> Result<(), MultiChannelQmfError> { + self.check_lengths(channel_samples, n_subs, output.len())?; + + for (ch, q) in self.channels.iter_mut().enumerate() { + q.synthesize( + channel_samples[ch], + n_subs[ch], + filter, + r_scale, + &mut output[ch], + ) + .map_err(|source| MultiChannelQmfError::Channel { ch, source })?; + } + Ok(()) + } + + /// Run the §C.2.5 per-channel call for every channel and append the + /// result **interleaved** (sample-major: for each output sample + /// index, channel 0's value then channel 1's, …) to `output`. + /// + /// All channels emit the same number of PCM samples + /// (`rows * 32`), so the interleaving is well-defined: the output + /// length grows by `channels * rows * 32`. Arguments otherwise + /// match [`MultiChannelQmf::synthesize_planar`]. + /// + /// # Errors + /// + /// Same as [`MultiChannelQmf::synthesize_planar`]. With zero + /// channels this is a no-op. + pub fn synthesize_interleaved( + &mut self, + channel_samples: &[&[[f64; NUM_SUBBAND]]], + n_subs: &[usize], + filter: FilterBankSelection, + r_scale: f64, + output: &mut Vec, + ) -> Result<(), MultiChannelQmfError> { + let channels = self.channels.len(); + self.check_lengths(channel_samples, n_subs, channels)?; + if channels == 0 { + return Ok(()); + } + + // Every channel produces `rows * 32` samples (verified equal by + // check_lengths' row-count check). Synthesize each channel into + // a scratch planar buffer, then interleave. + let rows = channel_samples[0].len(); + let per_channel = rows * PCM_OUTPUT_PER_SAMPLE; + + let mut planar: Vec> = (0..channels) + .map(|_| Vec::with_capacity(per_channel)) + .collect(); + for (ch, q) in self.channels.iter_mut().enumerate() { + q.synthesize( + channel_samples[ch], + n_subs[ch], + filter, + r_scale, + &mut planar[ch], + ) + .map_err(|source| MultiChannelQmfError::Channel { ch, source })?; + } + + output.reserve(channels * per_channel); + for s in 0..per_channel { + for plane in &planar { + output.push(plane[s]); + } + } + Ok(()) + } + + /// Convenience: drive [`MultiChannelQmf::synthesize_planar`] with + /// the two frame-wide §C.2.5 parameters sourced directly from a + /// parsed [`crate::DtsFrameHeader`] — `FILTS` via + /// [`crate::DtsFrameHeader::filter_bank_selection`] and the output + /// `rScale` via [`crate::DtsFrameHeader::output_r_scale`] (the + /// round-335 header bridge). + /// + /// # Errors + /// + /// Returns `Ok(None)` if the header's `PCMR` code is one of the two + /// reserved values (so [`crate::DtsFrameHeader::output_r_scale`] + /// yields `None` and no full-scale gain is defined); otherwise + /// runs the synthesis and returns `Ok(Some(()))`, or the same + /// errors as [`MultiChannelQmf::synthesize_planar`]. + pub fn synthesize_planar_from_header( + &mut self, + header: &crate::header::DtsFrameHeader, + channel_samples: &[&[[f64; NUM_SUBBAND]]], + n_subs: &[usize], + output: &mut [Vec], + ) -> Result, MultiChannelQmfError> { + let Some(r_scale) = header.output_r_scale() else { + return Ok(None); + }; + let filter = header.filter_bank_selection(); + self.synthesize_planar(channel_samples, n_subs, filter, r_scale, output)?; + Ok(Some(())) + } + + /// Validate the per-channel slice lengths and equal-row-count + /// invariant before any per-channel synthesis runs, so a length + /// error leaves every channel's filter state untouched. + fn check_lengths( + &self, + channel_samples: &[&[[f64; NUM_SUBBAND]]], + n_subs: &[usize], + output_len: usize, + ) -> Result<(), MultiChannelQmfError> { + let channels = self.channels.len(); + if channel_samples.len() != channels { + return Err(MultiChannelQmfError::ChannelSlicesLenMismatch { + channels, + got: channel_samples.len(), + }); + } + if n_subs.len() != channels { + return Err(MultiChannelQmfError::NSubsLenMismatch { + channels, + got: n_subs.len(), + }); + } + if output_len != channels { + return Err(MultiChannelQmfError::ChannelSlicesLenMismatch { + channels, + got: output_len, + }); + } + // All channels of one frame block must carry the same row count. + if let Some(first) = channel_samples.first() { + let expected = first.len(); + for (ch, rows) in channel_samples.iter().enumerate().skip(1) { + if rows.len() != expected { + return Err(MultiChannelQmfError::RowCountMismatch { + expected, + ch, + got: rows.len(), + }); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A `channels`-channel planar synthesis must equal `channels` + /// independent single-channel [`QmfSynthesis`] runs — the driver is + /// a faithful per-channel composition with no cross-channel + /// coupling. + #[test] + fn planar_matches_independent_single_channel_runs() { + let channels = 3; + let filter = FilterBankSelection::PerfectReconstruction; + let r_scale = 32768.0; + let n_subs = [4usize, 6, 2]; + + // Distinct deterministic input per channel. + let make_rows = |seed: usize| -> Vec<[f64; NUM_SUBBAND]> { + (0..8) + .map(|s| { + let mut row = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in row.iter_mut().enumerate() { + *slot = (((s * 13 + i * 7 + seed * 5) % 17) as f64 - 8.0) * 1000.0; + } + row + }) + .collect() + }; + let rows: Vec> = (0..channels).map(make_rows).collect(); + let refs: Vec<&[[f64; NUM_SUBBAND]]> = rows.iter().map(|r| r.as_slice()).collect(); + + // Multi-channel planar. + let mut mc = MultiChannelQmf::new(channels); + let mut planar: Vec> = vec![Vec::new(); channels]; + mc.synthesize_planar(&refs, &n_subs, filter, r_scale, &mut planar) + .unwrap(); + + // Independent single-channel runs. + for ch in 0..channels { + let mut q = QmfSynthesis::new(); + let mut expect = Vec::new(); + q.synthesize(&rows[ch], n_subs[ch], filter, r_scale, &mut expect) + .unwrap(); + assert_eq!(planar[ch], expect, "channel {ch} mismatch"); + } + // Non-vacuous: at least one channel produced non-zero PCM. + assert!(planar.iter().any(|p| p.iter().any(|&s| s != 0))); + } + + /// Interleaved output is the planar output transposed sample-major: + /// `interleaved[s*channels + ch] == planar[ch][s]`. + #[test] + fn interleaved_is_planar_transposed() { + let channels = 2; + let filter = FilterBankSelection::NonPerfectReconstruction; + let r_scale = 8192.0; + let n_subs = [5usize, 3]; + + let rows0: Vec<[f64; NUM_SUBBAND]> = (0..4) + .map(|s| { + let mut r = [0.0; NUM_SUBBAND]; + r[0] = (s as f64 + 1.0) * 1e5; + r + }) + .collect(); + let rows1: Vec<[f64; NUM_SUBBAND]> = (0..4) + .map(|s| { + let mut r = [0.0; NUM_SUBBAND]; + r[1] = (s as f64 + 1.0) * -1e5; + r + }) + .collect(); + let refs: Vec<&[[f64; NUM_SUBBAND]]> = vec![rows0.as_slice(), rows1.as_slice()]; + + let mut mc_p = MultiChannelQmf::new(channels); + let mut planar = vec![Vec::new(); channels]; + mc_p.synthesize_planar(&refs, &n_subs, filter, r_scale, &mut planar) + .unwrap(); + + let mut mc_i = MultiChannelQmf::new(channels); + let mut interleaved = Vec::new(); + mc_i.synthesize_interleaved(&refs, &n_subs, filter, r_scale, &mut interleaved) + .unwrap(); + + let per_channel = planar[0].len(); + assert_eq!(interleaved.len(), channels * per_channel); + for s in 0..per_channel { + for (ch, plane) in planar.iter().enumerate() { + assert_eq!( + interleaved[s * channels + ch], + plane[s], + "interleave mismatch at sample {s} channel {ch}" + ); + } + } + assert!(interleaved.iter().any(|&s| s != 0)); + } + + /// Persisting one [`MultiChannelQmf`] across two calls equals a + /// single call over the concatenated per-channel input — every + /// channel's inter-frame filter tail (`raX[]`) carries across calls. + #[test] + fn split_calls_match_single_concatenated_call() { + let channels = 2; + let filter = FilterBankSelection::PerfectReconstruction; + let r_scale = 32768.0; + let n_subs = [6usize, 4]; + + let make_rows = |seed: usize| -> Vec<[f64; NUM_SUBBAND]> { + (0..10) + .map(|s| { + let mut row = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in row.iter_mut().take(6).enumerate() { + *slot = (((s * 9 + i * 5 + seed) % 13) as f64 - 6.0) * 2000.0; + } + row + }) + .collect() + }; + let rows: Vec> = (0..channels).map(make_rows).collect(); + let refs: Vec<&[[f64; NUM_SUBBAND]]> = rows.iter().map(|r| r.as_slice()).collect(); + + // Single call over all 10 rows. + let mut mc_single = MultiChannelQmf::new(channels); + let mut single = vec![Vec::new(); channels]; + mc_single + .synthesize_planar(&refs, &n_subs, filter, r_scale, &mut single) + .unwrap(); + + // Two calls split 4 + 6, reusing the same instance. + let first: Vec<&[[f64; NUM_SUBBAND]]> = rows.iter().map(|r| &r[..4]).collect(); + let second: Vec<&[[f64; NUM_SUBBAND]]> = rows.iter().map(|r| &r[4..]).collect(); + let mut mc_split = MultiChannelQmf::new(channels); + let mut split = vec![Vec::new(); channels]; + mc_split + .synthesize_planar(&first, &n_subs, filter, r_scale, &mut split) + .unwrap(); + mc_split + .synthesize_planar(&second, &n_subs, filter, r_scale, &mut split) + .unwrap(); + + assert_eq!(single, split); + assert!(single.iter().any(|p| p.iter().any(|&s| s != 0))); + } + + /// Length-mismatch errors are returned before any channel's filter + /// state is touched. + #[test] + fn length_mismatches_are_rejected_before_synthesis() { + let mut mc = MultiChannelQmf::new(2); + let rows = vec![[0.0_f64; NUM_SUBBAND]; 2]; + let refs: Vec<&[[f64; NUM_SUBBAND]]> = vec![rows.as_slice(), rows.as_slice()]; + + // n_subs too short. + let mut out = vec![Vec::new(); 2]; + assert_eq!( + mc.synthesize_planar( + &refs, + &[4], + FilterBankSelection::PerfectReconstruction, + 1.0, + &mut out + ) + .unwrap_err(), + MultiChannelQmfError::NSubsLenMismatch { + channels: 2, + got: 1 + } + ); + + // channel-slices count wrong. + let one: Vec<&[[f64; NUM_SUBBAND]]> = vec![rows.as_slice()]; + assert_eq!( + mc.synthesize_planar( + &one, + &[4, 4], + FilterBankSelection::PerfectReconstruction, + 1.0, + &mut out + ) + .unwrap_err(), + MultiChannelQmfError::ChannelSlicesLenMismatch { + channels: 2, + got: 1 + } + ); + + // No output written, filters untouched. + assert!(out.iter().all(|p| p.is_empty())); + assert!(mc + .channels() + .iter() + .all(|q| q.x_history().iter().all(|&v| v == 0.0))); + } + + /// Channels with differing row counts are rejected (the §C.2.5 + /// outer loop runs identically for every channel of one frame). + #[test] + fn unequal_row_counts_are_rejected() { + let mut mc = MultiChannelQmf::new(2); + let rows_a = vec![[0.0_f64; NUM_SUBBAND]; 4]; + let rows_b = vec![[0.0_f64; NUM_SUBBAND]; 3]; + let refs: Vec<&[[f64; NUM_SUBBAND]]> = vec![rows_a.as_slice(), rows_b.as_slice()]; + let mut out = vec![Vec::new(); 2]; + assert_eq!( + mc.synthesize_planar( + &refs, + &[4, 4], + FilterBankSelection::NonPerfectReconstruction, + 1.0, + &mut out + ) + .unwrap_err(), + MultiChannelQmfError::RowCountMismatch { + expected: 4, + ch: 1, + got: 3 + } + ); + } + + /// A zero-channel driver is a no-op for both layouts. + #[test] + fn zero_channels_is_a_noop() { + let mut mc = MultiChannelQmf::new(0); + assert_eq!(mc.channel_count(), 0); + + let mut planar: Vec> = Vec::new(); + mc.synthesize_planar( + &[], + &[], + FilterBankSelection::PerfectReconstruction, + 1.0, + &mut planar, + ) + .unwrap(); + assert!(planar.is_empty()); + + let mut interleaved = Vec::new(); + mc.synthesize_interleaved( + &[], + &[], + FilterBankSelection::PerfectReconstruction, + 1.0, + &mut interleaved, + ) + .unwrap(); + assert!(interleaved.is_empty()); + } +} diff --git a/crates/vendor/oxideav-dts/src/qmf_synth.rs b/crates/vendor/oxideav-dts/src/qmf_synth.rs new file mode 100644 index 00000000..7f3532c7 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/qmf_synth.rs @@ -0,0 +1,448 @@ +//! Fused 32-band synthesis QMF driver — the §C.2.5 +//! `QMFInterpolation()` per-channel outer loop, transcribed verbatim +//! in `docs/audio/dts/dts-core-extracts.md` §2.4 (ETSI TS 102 114 +//! V1.3.1 Annex C §C.2.5, staged PDF p.185). +//! +//! Rounds 255–285 landed every FIR-independent per-sample step of the +//! §C.2.5 loop body as a standalone primitive: +//! +//! * [`crate::assemble_xin`] — step (a), build `raXin[0..32]` from one +//! sample per active subband (inactive subbands zero-filled); +//! * [`crate::cos_mod_stage`] — step (b), the cosine-modulation +//! matrix multiply that refreshes `raX[0..32]`; +//! * [`crate::fir_step`] — step (c), the 512-tap §D.8 FIR convolution +//! that accumulates into `raZ[0..64]`; +//! * [`crate::write_pcm_output`] — step (d), `int(rScale·raZ[i])` for +//! the 32 low accumulator entries; +//! * [`crate::shift_x_history`] / [`crate::shift_z_output`] — step +//! (e), the per-sample shift-register / accumulator rotates. +//! +//! This module is the **fused driver** that composes those primitives +//! into the complete §C.2.5 outer loop: +//! +//! ```text +//! QMFInterpolation(FILTS, int nSUBS) { +//! if (FILTS==0) prCoeff = raCoeffLossy; else prCoeff = raCoeffLossLess; +//! nChIndex = 0; +//! for (nSubIndex=nStart; nSubIndex raX[0..32] +//! // (c) 512-tap FIR -> raZ[0..64] +//! // (d) int(rScale*raZ[i]) -> naCh[nChIndex++] (32 samples) +//! // (e) shift raX history by 32; rotate raZ down by 32 +//! } +//! } +//! ``` +//! +//! The driving call is, per channel (§C.2.5): +//! `aPrmCh[ch].QMFInterpolation(FILTS, nSUBS[ch]);`. +//! +//! # Persistent per-channel state +//! +//! The §C.2.5 pseudocode keeps `raX[]` (the 512-tap shift register) +//! and `raZ[]` (the 64-entry output accumulator) **across** the +//! per-sample iterations of one `QMFInterpolation()` call, and — since +//! `aPrmCh[ch]` is a persistent per-channel object the decoder calls +//! once per subframe — across successive subframes of the same +//! channel. [`QmfSynthesis`] is that per-channel object: it owns the +//! `raX[]` / `raZ[]` state and zero-initialises both at construction +//! (the spec's per-channel filter starts with a cleared history before +//! the first subframe). A caller that decodes a multi-subframe channel +//! constructs one [`QmfSynthesis`] for the channel and feeds each +//! subframe's subband samples through [`QmfSynthesis::synthesize`] in +//! order, so the inter-subframe filter tail (`raX[]`) carries +//! correctly. +//! +//! # FIR independence +//! +//! Only step (c) ([`crate::fir_step`]) reads the §D.8 coefficient +//! tables. This driver selects the table once per call from the +//! caller-supplied [`crate::FilterBankSelection`] (the resolved +//! `FILTS` branch) and threads it into every per-sample FIR step, +//! exactly as the spec hoists the `prCoeff = …` assignment out of the +//! per-sample loop. + +use crate::cos_mod::{cos_mod_stage, precal_cos_mod, COS_MOD_LEN, NUM_SUBBAND}; +use crate::filter_bank::FilterBankSelection; +use crate::qmf_assemble::{ + assemble_xin, fir_step, shift_x_history, shift_z_output, write_pcm_output, QmfAssembleError, + PCM_OUTPUT_PER_SAMPLE, X_HISTORY_LEN, Z_OUTPUT_LEN, +}; + +/// Persistent per-channel 32-band synthesis QMF state — the §C.2.5 +/// `aPrmCh[ch]` filter object that [`QmfSynthesis::synthesize`] drives +/// once per subframe. +/// +/// Holds the 512-tap shift register `raX[]` ([`X_HISTORY_LEN`]) and +/// the 64-entry output accumulator `raZ[]` ([`Z_OUTPUT_LEN`]) the +/// §C.2.5 per-sample loop carries across iterations and across +/// successive subframes of the same channel. Both arrays start cleared +/// ([`QmfSynthesis::new`]); the inter-subframe filter tail lives in +/// `raX[]`, so a multi-subframe channel must reuse one instance and +/// feed its subframes in order. +/// +/// The 544-entry cosine-modulation matrix `raCosMod[]` +/// ([`precal_cos_mod`]) is precomputed once at construction and reused +/// for every per-sample [`cos_mod_stage`] call (the spec's +/// `PreCalCosMod()` runs once before any `QMFInterpolation()` call). +#[derive(Debug, Clone)] +pub struct QmfSynthesis { + /// The §C.2.5 `raX[]` 512-tap synthesis shift register. The low 32 + /// entries `raX[0..32]` are refreshed every per-sample iteration by + /// [`cos_mod_stage`]; the FIR step convolves the full register + /// against the §D.8 coefficients; the post-PCM shift then rotates + /// the whole register up by 32, carrying the filter tail across + /// per-sample iterations and across subframes. + ra_x: [f64; X_HISTORY_LEN], + /// The §C.2.5 `raZ[]` 64-entry output accumulator. The FIR step + /// accumulates into it; the PCM step reads `raZ[0..32]`; the rotate + /// slides `raZ[32..64]` down into `raZ[0..32]` and clears the high + /// block. Carries each output sample's partial sums between the two + /// consecutive per-sample iterations that complete it. + ra_z: [f64; Z_OUTPUT_LEN], + /// The §C.2.5 544-entry `raCosMod[]` matrix + /// ([`precal_cos_mod`]), precomputed once and reused for every + /// per-sample [`cos_mod_stage`] call. + ra_cos_mod: [f64; COS_MOD_LEN], +} + +impl Default for QmfSynthesis { + fn default() -> Self { + Self::new() + } +} + +impl QmfSynthesis { + /// Construct a fresh per-channel synthesis filter with a cleared + /// history (`raX[] = raZ[] = 0`), matching the §C.2.5 per-channel + /// filter's initial state before the first subframe. The + /// cosine-modulation matrix is precomputed here once. + #[must_use] + pub fn new() -> Self { + Self { + ra_x: [0.0; X_HISTORY_LEN], + ra_z: [0.0; Z_OUTPUT_LEN], + ra_cos_mod: precal_cos_mod(), + } + } + + /// Run the §C.2.5 `QMFInterpolation()` outer loop over one block of + /// subband samples, appending the reconstructed PCM to `output`. + /// + /// `subband_samples[s]` is the §C.2.5 per-sample subband vector at + /// sample index `nSubIndex = nStart + s` — one `f64` per subband, + /// `aSubband[i].raSample[nSubIndex]` for `i ∈ 0..32`. Subbands at + /// or beyond `n_subs` are zero-filled by [`assemble_xin`] before + /// the cosine-modulation step (the spec's + /// `for (i=nSUBS; i NUM_SUBBAND`, and + /// [`QmfAssembleError::SampleSliceTooShort`] if any row carries + /// fewer than `n_subs` values. (The PCM-output step writes into a + /// scratch buffer sized for exactly 32 samples, so its + /// [`QmfAssembleError::OutputSliceTooShort`] precondition is + /// satisfied by construction and never surfaces here.) + pub fn synthesize( + &mut self, + subband_samples: &[[f64; NUM_SUBBAND]], + n_subs: usize, + filter: FilterBankSelection, + r_scale: f64, + output: &mut Vec, + ) -> Result<(), QmfAssembleError> { + if n_subs > NUM_SUBBAND { + return Err(QmfAssembleError::SubsOutOfRange { n_subs }); + } + + // Spec line 175-178: select prCoeff once, outside the + // per-sample loop. + let pr_coeff = filter.coefficients(); + + // Per-sample PCM scratch: write_pcm_output emits exactly + // PCM_OUTPUT_PER_SAMPLE (= 32) samples at n_ch_index = 0, so a + // 32-slot scratch always satisfies its length precondition. We + // copy the scratch into `output` after each iteration, which + // is the spec's `naCh[nChIndex++]` running append (nChIndex + // advances 32 per sample) flattened into the caller's buffer. + let mut scratch = [0_i32; PCM_OUTPUT_PER_SAMPLE]; + + output.reserve(subband_samples.len() * PCM_OUTPUT_PER_SAMPLE); + + for row in subband_samples { + // (a) raXin = active subbands then zero tail. + let ra_xin = assemble_xin(row, n_subs)?; + + // (b) cosine-modulation refreshes raX[0..32]; the high + // block raX[32..512] holds the carried history. + let low = cos_mod_stage(&ra_xin, &self.ra_cos_mod); + self.ra_x[..NUM_SUBBAND].copy_from_slice(&low); + + // (c) 512-tap FIR convolution accumulates into raZ[0..64]. + fir_step(&self.ra_x, pr_coeff, &mut self.ra_z); + + // (d) int(rScale*raZ[i]) for the 32 low accumulator entries. + // The scratch is exactly 32 long, so n_ch_index = 0 + // never trips OutputSliceTooShort. + write_pcm_output(&self.ra_z, r_scale, &mut scratch, 0)?; + output.extend_from_slice(&scratch); + + // (e) shift raX history up by 32 (freeing raX[0..32] for the + // next iteration's cos_mod write) and rotate raZ down by + // 32 (carrying the next sample's partials into raZ[0..32] + // and clearing raZ[32..64]). + shift_x_history(&mut self.ra_x); + shift_z_output(&mut self.ra_z); + } + + Ok(()) + } + + /// Borrow the current `raX[]` shift register (the §C.2.5 512-tap + /// synthesis history). Exposed for callers that want to inspect or + /// checkpoint the inter-subframe filter tail; the driver maintains + /// it automatically across [`QmfSynthesis::synthesize`] calls. + #[must_use] + pub fn x_history(&self) -> &[f64; X_HISTORY_LEN] { + &self.ra_x + } + + /// Borrow the current `raZ[]` output accumulator (the §C.2.5 + /// 64-entry partial-sum buffer). Exposed for the same + /// checkpoint/inspection use as [`QmfSynthesis::x_history`]. + #[must_use] + pub fn z_accumulator(&self) -> &[f64; Z_OUTPUT_LEN] { + &self.ra_z + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A fresh filter starts with a fully-cleared history, matching the + /// §C.2.5 per-channel filter state before the first subframe. + #[test] + fn new_starts_with_cleared_history() { + let q = QmfSynthesis::new(); + assert!(q.x_history().iter().all(|&v| v == 0.0)); + assert!(q.z_accumulator().iter().all(|&v| v == 0.0)); + // The cosine-modulation matrix is precomputed (non-trivial). + assert_eq!(q.ra_cos_mod.len(), COS_MOD_LEN); + } + + /// Each per-sample row appends exactly 32 PCM samples to the + /// output, per the §C.2.5 PCM-output step's `i < 32` loop. + #[test] + fn each_row_appends_32_pcm_samples() { + let mut q = QmfSynthesis::new(); + let rows = vec![[0.0_f64; NUM_SUBBAND]; 4]; + let mut out = Vec::new(); + q.synthesize( + &rows, + 32, + FilterBankSelection::NonPerfectReconstruction, + 1.0, + &mut out, + ) + .unwrap(); + assert_eq!(out.len(), 4 * PCM_OUTPUT_PER_SAMPLE); + } + + /// An all-zero subband input produces all-zero PCM, regardless of + /// the filter selection or scale: the cosine-modulation of a zero + /// vector is zero, the FIR convolution of a zero history is zero, + /// and `int(rScale·0) == 0`. + #[test] + fn zero_input_yields_zero_pcm() { + for filter in [ + FilterBankSelection::NonPerfectReconstruction, + FilterBankSelection::PerfectReconstruction, + ] { + let mut q = QmfSynthesis::new(); + let rows = vec![[0.0_f64; NUM_SUBBAND]; 8]; + let mut out = Vec::new(); + q.synthesize(&rows, 16, filter, 32768.0, &mut out).unwrap(); + assert_eq!(out.len(), 8 * PCM_OUTPUT_PER_SAMPLE); + assert!(out.iter().all(|&s| s == 0)); + } + } + + /// The fused driver must produce byte-identical output to a manual + /// hand-composition of the same per-sample primitives — this pins + /// the driver as a faithful composition with no hidden state or + /// reordering. We drive a small DC-impulse input through both. + #[test] + fn fused_driver_matches_manual_composition() { + let n_subs = 4; + let filter = FilterBankSelection::PerfectReconstruction; + // A large scale so the impulse response truncates to non-zero + // integer PCM (int(rScale·raZ) would round small responses to + // zero at a small scale, making the non-vacuity check fail + // without the equality check itself being wrong). + let r_scale = 1_000_000.0; + // A unit impulse in subband 0 followed by several silent rows — + // exercises the inter-sample history shift (the impulse's + // filter tail reaches later samples' output through raX[]). + let mut row0 = [0.0_f64; NUM_SUBBAND]; + row0[0] = 1.0; + let silent = [0.0_f64; NUM_SUBBAND]; + let rows = [row0, silent, silent, silent]; + + // Fused. + let mut q = QmfSynthesis::new(); + let mut fused = Vec::new(); + q.synthesize(&rows, n_subs, filter, r_scale, &mut fused) + .unwrap(); + + // Manual: replicate the §C.2.5 loop body with the same + // primitives and the same starting (cleared) state. + let cos = precal_cos_mod(); + let pr_coeff = filter.coefficients(); + let mut ra_x = [0.0_f64; X_HISTORY_LEN]; + let mut ra_z = [0.0_f64; Z_OUTPUT_LEN]; + let mut manual = Vec::new(); + let mut scratch = [0_i32; PCM_OUTPUT_PER_SAMPLE]; + for row in &rows { + let xin = assemble_xin(row, n_subs).unwrap(); + let low = cos_mod_stage(&xin, &cos); + ra_x[..NUM_SUBBAND].copy_from_slice(&low); + fir_step(&ra_x, pr_coeff, &mut ra_z); + write_pcm_output(&ra_z, r_scale, &mut scratch, 0).unwrap(); + manual.extend_from_slice(&scratch); + shift_x_history(&mut ra_x); + shift_z_output(&mut ra_z); + } + + assert_eq!(fused, manual); + // The impulse must produce at least one non-zero PCM sample + // (otherwise the test would pass vacuously on all-zero output). + assert!(fused.iter().any(|&s| s != 0)); + } + + /// Persisting one [`QmfSynthesis`] across two `synthesize` calls + /// must equal a single call over the concatenated input — the + /// inter-subframe filter tail (`raX[]`) carries across calls. + #[test] + fn split_calls_match_single_concatenated_call() { + let n_subs = 6; + let filter = FilterBankSelection::NonPerfectReconstruction; + let r_scale = 8192.0; + // Build a deterministic non-trivial input. + let mut rows = Vec::new(); + for s in 0..10 { + let mut row = [0.0_f64; NUM_SUBBAND]; + for (i, slot) in row.iter_mut().take(n_subs).enumerate() { + *slot = ((s * 7 + i * 3) % 11) as f64 - 5.0; + } + rows.push(row); + } + + // Single call over all 10 rows. + let mut q_single = QmfSynthesis::new(); + let mut single = Vec::new(); + q_single + .synthesize(&rows, n_subs, filter, r_scale, &mut single) + .unwrap(); + + // Two calls split 4 + 6, reusing the same filter instance. + let mut q_split = QmfSynthesis::new(); + let mut split = Vec::new(); + q_split + .synthesize(&rows[..4], n_subs, filter, r_scale, &mut split) + .unwrap(); + q_split + .synthesize(&rows[4..], n_subs, filter, r_scale, &mut split) + .unwrap(); + + assert_eq!(single, split); + assert!(single.iter().any(|&s| s != 0)); + } + + /// `n_subs > 32` is rejected before any sample is processed. + #[test] + fn rejects_n_subs_beyond_num_subband() { + let mut q = QmfSynthesis::new(); + let rows = vec![[0.0_f64; NUM_SUBBAND]; 1]; + let mut out = Vec::new(); + assert_eq!( + q.synthesize( + &rows, + 33, + FilterBankSelection::NonPerfectReconstruction, + 1.0, + &mut out + ) + .unwrap_err(), + QmfAssembleError::SubsOutOfRange { n_subs: 33 } + ); + // No output written and the history is untouched on the error. + assert!(out.is_empty()); + assert!(q.x_history().iter().all(|&v| v == 0.0)); + } + + /// An empty input block is a no-op: no PCM is appended and the + /// per-channel history is untouched (the §C.2.5 outer loop runs + /// zero iterations when `nStart == nEnd`). + #[test] + fn empty_input_is_a_noop() { + let mut q = QmfSynthesis::new(); + let mut out = Vec::new(); + q.synthesize( + &[], + 8, + FilterBankSelection::PerfectReconstruction, + 1.0, + &mut out, + ) + .unwrap(); + assert!(out.is_empty()); + assert!(q.x_history().iter().all(|&v| v == 0.0)); + } + + /// `n_subs = 0` is the spec's fully-silenced channel: `raXin` is + /// all-zero, so every PCM sample is zero even with arbitrary scale. + #[test] + fn zero_active_subbands_is_silence() { + let mut q = QmfSynthesis::new(); + let mut row = [0.0_f64; NUM_SUBBAND]; + // Even with non-zero values in the (inactive) subbands, n_subs=0 + // zero-fills the whole raXin. + row.iter_mut().for_each(|v| *v = 123.0); + let rows = vec![row; 3]; + let mut out = Vec::new(); + q.synthesize( + &rows, + 0, + FilterBankSelection::NonPerfectReconstruction, + 10000.0, + &mut out, + ) + .unwrap(); + assert_eq!(out.len(), 3 * PCM_OUTPUT_PER_SAMPLE); + assert!(out.iter().all(|&s| s == 0)); + } +} diff --git a/crates/vendor/oxideav-dts/src/registry.rs b/crates/vendor/oxideav-dts/src/registry.rs new file mode 100644 index 00000000..e1c491d8 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/registry.rs @@ -0,0 +1,856 @@ +//! `oxideav-core` integration: `Decoder` trait impl, `Frame` / `Error` +//! conversions, and the [`register`] / [`register_codecs`] entry +//! points. Includes the [`probe_dts`] confidence helper used by the +//! tag-keyed registry lookup. +//! +//! Gated behind the default-on `registry` Cargo feature. With the +//! feature off the rest of the crate still exposes the standalone +//! [`crate::parse_frame_header`] / [`crate::parse_frame_header_14bit`] +//! / [`crate::unpack_14bit_to_16bit`] APIs plus the [`crate::Error`] +//! type — none of which depend on `oxideav-core`. + +use oxideav_core::{ + AudioFrame, CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag, + Confidence, Decoder, Error as CoreError, Frame, Packet, ProbeContext, Result as CoreResult, + RuntimeContext, +}; + +use crate::header::{detect_sync, parse_frame_header, parse_frame_header_14bit}; +use crate::unpack14::{unpack_14bit_to_16bit, FourteenBitByteOrder}; +use crate::{DtsFrameHeader, Error as DtsError, SyncWordEncoding}; + +/// Canonical codec id string for the DTS Coherent Acoustics codec. +/// Matches the registration string used by `oxideav-mp4`'s +/// `from_sample_entry` mapping for `dtsc` / `dtsh` / `dtsl` / `dtse`. +pub const CODEC_ID_STR: &str = "dts"; + +impl From for CoreError { + fn from(e: DtsError) -> Self { + match e { + DtsError::UnexpectedEof => CoreError::NeedMore, + DtsError::NoSync => CoreError::InvalidData(e.to_string()), + DtsError::UnsupportedFourteenBit | DtsError::UnsupportedRaw16Bit => { + CoreError::Unsupported(e.to_string()) + } + DtsError::BlockCountOutOfRange { .. } | DtsError::FrameSizeOutOfRange { .. } => { + CoreError::InvalidData(e.to_string()) + } + // The encoder-only [`DtsError::FieldOutOfRange`] variant is + // not produced by any parser path, so the runtime decoder + // surface cannot emit it via `send_packet`. We still map it + // for completeness should a future caller invoke + // [`crate::encode_frame_header_be`] from inside the + // decoder path. + DtsError::FieldOutOfRange { .. } => CoreError::InvalidData(e.to_string()), + // Round 195 side-info decoder failures: bit-stream-format + // errors (reserved BHUFF/SHUFF/SCALES values or unmatched + // Huffman codeword) map to `InvalidData` so the surrounding + // demux/decoder path treats the packet as corrupt rather + // than as an unrecoverable codec-level limitation. + DtsError::InvalidSideInfo { .. } | DtsError::HuffmanDecodeFailed { .. } => { + CoreError::InvalidData(e.to_string()) + } + // Round 214 §C.2.4 sum/difference length-mismatch is a + // caller-side slice-shape violation; the runtime decoder + // path doesn't construct mismatched slices today, but if a + // future subframe walker plumbs the variant through + // `send_packet`, surface it as `InvalidData`. + DtsError::SumDiffLengthMismatch { .. } => CoreError::InvalidData(e.to_string()), + // Round 223 §C.2.3 joint-subband shape-mismatch is a + // caller-side slice-shape violation analogous to the + // sum/diff variant. Same `InvalidData` mapping rationale. + DtsError::JointSubbandShapeMismatch { .. } => CoreError::InvalidData(e.to_string()), + // Round 228 §C.2.2 inverse-ADPCM shape-mismatch is the + // same flavour of caller-side slice-shape violation: the + // history or coefficient slice has a length other than + // the spec's `NumADPCMCoeff = 4`. `InvalidData` for parity + // with the sum/diff and joint-subband mappings. + DtsError::InverseAdpcmShapeMismatch { .. } => CoreError::InvalidData(e.to_string()), + // Round 232 §C.2.1 block-code errors. `n_levels < 2` is a + // structural / caller-side violation analogous to the + // §C.2.2/3/4 shape-mismatch variants; a residual block code + // word indicates bit-stream corruption (the §C.2.1 success + // criterion `nCode == 0` is unmet). Both map to + // `InvalidData` for parity with the surrounding §C.2.x + // failure modes. + DtsError::BlockCodeLevelsOutOfRange { .. } | DtsError::BlockCodeResidual { .. } => { + CoreError::InvalidData(e.to_string()) + } + // Round 293 §D.2 / §5.5 dequantization errors. A reserved + // `ABITS` step-size index (`27..=31`) indicates a corrupt + // bit stream (the §5.5 `Audio Data` block only ever selects + // a quantizer for a defined `ABITS`), so `InvalidData`; the + // §5.5 eight-sample shape-mismatch is the same caller-side + // slice-shape violation as the §C.2.x variants above. + DtsError::InvalidStepSize { .. } | DtsError::SampleCountMismatch { .. } => { + CoreError::InvalidData(e.to_string()) + } + // Round 406 §5.7.1 downmix-fold shape mismatch: a + // caller-side plane-shape violation analogous to the + // §C.2.x shape-mismatch variants -> `InvalidData`. + DtsError::DownmixInputShapeMismatch { .. } => CoreError::InvalidData(e.to_string()), + // Round 406 §5.7.2 Rev2 auxiliary chunk errors: sync + // mismatch, invalid declared byte size, out-of-range + // embedded-ES scale index, or an underivable DRC value + // count all indicate a corrupt or false-positive Rev2 + // chunk -> `InvalidData`. + DtsError::Rev2AuxSyncMismatch { .. } + | DtsError::Rev2AuxSizeOutOfRange { .. } + | DtsError::Rev2AuxEsScaleIndexOutOfRange { .. } + | DtsError::Rev2AuxDrcCountUnresolved { .. } => CoreError::InvalidData(e.to_string()), + // Round 406 §5.7.1 auxiliary-data chunk errors: a sync + // mismatch at a caller-supplied offset, a time-stamp + // marker other than 0b1011, or an AMODE whose Table 5-4 + // channel count is undefined all indicate a corrupt or + // false-positive auxiliary chunk -> `InvalidData`. + DtsError::AuxSyncMismatch { .. } + | DtsError::AuxTimeStampMarkerMismatch { .. } + | DtsError::AuxChannelCountUnresolved { .. } => CoreError::InvalidData(e.to_string()), + // Round 406 §5.7.1 dynamic-downmix coefficient code out of + // domain: a 9-bit `panDwnMixCodeCoeffs[n]` word whose + // one-biased low byte walks past the §D.11 `DmixTable` + // indicates a corrupt auxiliary-data chunk, so + // `InvalidData` alongside the other bit-stream-corruption + // variants. + DtsError::DownmixCodeOutOfRange { .. } => CoreError::InvalidData(e.to_string()), + // Round 306 §5.5 DSYNC trailer mismatch: a subsubframe + // synchronization check word other than `0xffff` is the + // Core profile's in-band integrity signal for a corrupt + // audio-data array, so map it to `InvalidData` alongside the + // other bit-stream-corruption variants above. + DtsError::DsyncMismatch { .. } => CoreError::InvalidData(e.to_string()), + } + } +} + +/// Register the DTS Core decoder factory plus the `dts` and `dtsc` +/// FourCC tags into `reg`. The factory always succeeds at construction +/// time; the `Decoder::send_packet` impl is the point where structural +/// frame-header failures surface (so demuxers can route packets +/// without instantiating a decoder). +pub fn register_codecs(reg: &mut CodecRegistry) { + let caps = CodecCapabilities::audio("dts_sw").with_lossy(true); + reg.register( + CodecInfo::new(CodecId::new(CODEC_ID_STR)) + .capabilities(caps) + .decoder(make_decoder) + .probe(probe_dts_tag) + .tags([ + // `dts` — generic FourCC seen on some QuickTime sample + // entries and in raw-stream tag lookups. + CodecTag::fourcc(b"dts "), + // `dtsc` — DTS Coherent Acoustics ISO/IEC sample-entry + // FourCC (ETSI TS 102 114 §6 / ISO/IEC 14496-30). + CodecTag::fourcc(b"dtsc"), + ]), + ); +} + +/// Unified entry point: install the DTS codec into a [`RuntimeContext`]. +pub fn register(ctx: &mut RuntimeContext) { + register_codecs(&mut ctx.codecs); +} + +oxideav_core::register!("dts", register); + +/// Decoder factory for the DTS Core profile. +/// +/// Returns a handle whose [`Decoder::send_packet`] parses the frame +/// header eagerly (so structural failures — bad sync, NBLKS below 5, +/// frame size below 95 bytes, truncated header — surface at the +/// packet boundary) and caches the raw-16-bit frame bytes, and whose +/// [`Decoder::receive_frame`] runs the §5.3/§5.4/§5.5 + §C.2.5 +/// [`crate::decode_core_frame`] reconstruction to emit a planar S32 +/// [`AudioFrame`] for the common Core case. **14-bit container frames** +/// (both byte orders) are unpacked to the raw-16-bit-word domain in +/// [`Decoder::send_packet`] and decode through the identical chain. +/// §D.10 VQ/ADPCM frames (`nVQSUB < nSUBS` / `PMODE != 0`) decode +/// through the built-in Annex D code books +/// ([`crate::VqCodebooks::builtin`] — the decoder default since round +/// 439), so no Core frame class maps to +/// [`CoreError::Unsupported`] for missing book data anymore. +pub fn make_decoder(params: &CodecParameters) -> CoreResult> { + Ok(Box::new(DtsDecoderHandle { + codec_id: params.codec_id.clone(), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + })) +} + +/// In-process DTS Core decoder handle. +/// +/// Holds the most recently parsed [`DtsFrameHeader`] for diagnostic +/// inspection (e.g. by integration tests that want to confirm a +/// container routed a real DTS frame even though PCM output is not +/// yet wired up). +#[derive(Debug)] +pub struct DtsDecoderHandle { + codec_id: CodecId, + last_header: Option, + /// The most recent packet's full (unpacked, raw-16-bit) frame bytes, + /// kept so [`Decoder::receive_frame`] can run the §5.3/§5.4/§5.5 + + /// §C.2.5 reconstruction over the whole frame, not just the header. + last_frame_bytes: Option>, + /// The PTS carried by the most recent packet, forwarded onto the + /// emitted [`Frame`]. + last_pts: i64, + /// The persistent §C.2.5 stream decoder, carrying each channel's + /// inter-frame filter tail (`raX[]` / `raZ[]`) across packets — a + /// DTS elementary stream's QMF filter is continuous, so resetting it + /// per packet would inject a warmup transient at every frame + /// boundary. Lazily (re)constructed on the first packet and whenever + /// the channel count changes (a stream's `nPCHS` is constant in + /// practice, but the handle tolerates a mid-stream change by + /// restarting the filter for the new layout). + stream: Option, + eof: bool, +} + +impl DtsDecoderHandle { + /// Inspect the most recently parsed frame header (or `None` if + /// `send_packet` has not been called yet, or if every prior call + /// errored before producing a header). + pub fn last_header(&self) -> Option<&DtsFrameHeader> { + self.last_header.as_ref() + } +} + +impl Decoder for DtsDecoderHandle { + fn codec_id(&self) -> &CodecId { + &self.codec_id + } + + fn send_packet(&mut self, packet: &Packet) -> CoreResult<()> { + // Route by the syncword at offset 0. The two raw 16-bit + // variants go to `parse_frame_header`; the two 14-bit packed + // variants go to `parse_frame_header_14bit`. Anything else + // returns InvalidData via the From impl above. + let bytes = packet.data.as_slice(); + let sync = detect_sync(bytes).map_err(CoreError::from)?; + let hdr = match sync { + SyncWordEncoding::RawBigEndian | SyncWordEncoding::RawLittleEndian => { + let hdr = parse_frame_header(bytes).map_err(CoreError::from)?; + // Raw 16-bit frames are already in the domain + // `decode_core_frame` operates on; keep the bytes so + // receive_frame can reconstruct PCM. + self.last_frame_bytes = Some(bytes.to_vec()); + hdr + } + SyncWordEncoding::FourteenBitBigEndian | SyncWordEncoding::FourteenBitLittleEndian => { + // 14-bit container frames carry the same logical Core + // bitstream as the raw-16-bit forms, just packed 14 payload + // bits per 16-bit container word (§5.3.1 / wiki container + // rule). Unpack to the raw-16-bit-word domain that + // `decode_core_frame` operates on, then decode through the + // identical §5.3/§5.4/§5.5 + §C.2.5 chain. The unpacked + // buffer is bit-identical to the equivalent raw-16-bit + // frame (validated bit-exact on the bundled fixture for + // both container byte orders), so the reconstruction is the + // same up to the container transform. + let order = FourteenBitByteOrder::from_sync(sync).ok_or_else(|| { + CoreError::unsupported( + "oxideav-dts: 14-bit sync did not map to a container byte order", + ) + })?; + let unpacked = unpack_14bit_to_16bit(bytes, order).map_err(CoreError::from)?; + // Parse the header from the unpacked (now raw-16-bit) bytes + // so the cached header matches the cached payload domain. + let hdr = parse_frame_header(&unpacked).map_err(CoreError::from)?; + self.last_frame_bytes = Some(unpacked); + hdr + } + }; + self.last_header = Some(hdr); + self.last_pts = packet.pts.unwrap_or(0); + Ok(()) + } + + fn receive_frame(&mut self) -> CoreResult { + let Some(header) = self.last_header.take() else { + return if self.eof { + Err(CoreError::Eof) + } else { + Err(CoreError::NeedMore) + }; + }; + + // `send_packet` caches the raw-16-bit payload for both the raw and + // the (unpacked) 14-bit container forms, so a header without cached + // bytes means `receive_frame` was called without a prior successful + // `send_packet` — surface it rather than reconstructing stale bytes. + let Some(bytes) = self.last_frame_bytes.take() else { + return Err(CoreError::unsupported( + "oxideav-dts: no cached frame payload; call send_packet before \ + receive_frame", + )); + }; + + // Run the §5.3/§5.4/§5.5 + §C.2.5 reconstruction for the common + // Core case through the persistent §C.2.5 stream decoder so the + // per-channel filter tail carries across packets (a DTS + // elementary stream's QMF filter is continuous; resetting it per + // packet injects a warmup transient at every frame boundary). + // The frame's channel count (§5.3.2 nPCHS) sizes the filter; a + // mismatch with the running stream restarts it for the new + // layout. Joint-intensity frames (Table 5-28 JOINX > 0) decode + // through the §D.3 JScaleTbl + §C.2.3 sub-band copy, and §D.10 + // VQ/ADPCM frames through the built-in Annex D code books (the + // stream-decoder default), so decode errors here are structural + // (bad bitstream / reserved fields), not missing-feature. + let channels = frame_channel_count(&bytes, &header) + .map_err(|e| CoreError::unsupported(format!("oxideav-dts: {e}")))?; + let stream = match self.stream.take() { + Some(s) if s.channel_count() == channels => s, + // First packet, or a channel-count change: (re)start the + // continuous filter for this layout. + _ => crate::CoreStreamDecoder::new(channels), + }; + let mut stream = stream; + let mut pcm = stream + .decode_frame(&bytes, &header) + .map_err(|e| CoreError::unsupported(format!("oxideav-dts: {e}")))?; + // For an LFE-bearing frame (LFF != 0), append the §5.5/§C.2.6 LFE + // channel as a trailing plane. The §C.2.6 interpolation expands + // the decimated LFE samples to exactly the primary per-frame + // sample rate (2·LFF·nSSC·nDeciFactor == nSSC·256 for both LFF + // modes), so the LFE plane is the same length as the primary + // planes and slots in as one more channel. + let lfe = stream.take_last_lfe_pcm(); + if !lfe.is_empty() { + pcm.push(lfe); + } + self.stream = Some(stream); + + // Planar S32: one plane per channel, each sample little-endian. + let channels = pcm.len(); + let samples_per_channel = pcm.first().map_or(0, Vec::len) as u32; + let mut data: Vec> = Vec::with_capacity(channels); + for plane in &pcm { + let mut bytes = Vec::with_capacity(plane.len() * 4); + for &s in plane { + bytes.extend_from_slice(&s.to_le_bytes()); + } + data.push(bytes); + } + + Ok(Frame::Audio(AudioFrame { + samples: samples_per_channel, + pts: Some(self.last_pts), + data, + })) + } + + fn flush(&mut self) -> CoreResult<()> { + self.eof = true; + Ok(()) + } + + fn reset(&mut self) -> CoreResult<()> { + self.last_header = None; + self.last_frame_bytes = None; + self.last_pts = 0; + // Drop the persistent §C.2.5 stream decoder so the next packet + // starts a fresh continuous filter (cleared history) — a reset + // means the caller seeked / restarted, so the inter-frame filter + // tail must not bleed across the discontinuity. + self.stream = None; + self.eof = false; + Ok(()) + } +} + +/// Read the §5.3.2 Primary Audio Coding Header just enough to recover +/// the frame's primary-channel count (`nPCHS`), which sizes the +/// persistent §C.2.5 stream filter. Returns the §5.3.2 audio-coding +/// header decode error on a structurally-bad frame. +fn frame_channel_count(bytes: &[u8], header: &DtsFrameHeader) -> Result { + let header_bits = header.header_bit_length() as usize; + let (coding, _ach_bits) = + crate::decode_audio_coding_header_at(bytes, header_bits, header.crc_present)?; + Ok(coding.n_pchs) +} + +/// Standalone confidence probe for DTS Core bitstreams. +/// +/// Inspects the first few bytes of `bytes` and returns: +/// +/// * `1.0` — one of the four documented DTS Core sync sequences is +/// present at offset 0 and the buffer contains enough bytes to +/// parse the structural frame header successfully. +/// * `0.5` — a sync sequence is present at offset 0 but the buffer is +/// shorter than the 15 bytes the raw-BE parser needs (or 18 bytes +/// for the 14-bit packed variants). Used by demuxers that probe +/// against a peek-window before the full frame is available. +/// * `0.0` — neither sync sequence appears at offset 0. +/// +/// Suitable as the `decoder_options_schema`-independent first-pass +/// confidence used by [`oxideav_core::CodecRegistry::resolve_tag`]. +pub fn probe_dts(bytes: &[u8]) -> Confidence { + match detect_sync(bytes) { + Ok(sync) => { + let result = match sync { + SyncWordEncoding::RawBigEndian | SyncWordEncoding::RawLittleEndian => { + parse_frame_header(bytes) + } + SyncWordEncoding::FourteenBitBigEndian + | SyncWordEncoding::FourteenBitLittleEndian => parse_frame_header_14bit(bytes), + }; + match result { + Ok(_) => 1.0, + Err(DtsError::UnexpectedEof) => 0.5, + Err(_) => 0.0, + } + } + Err(DtsError::UnexpectedEof) => 0.5, + Err(_) => 0.0, + } +} + +/// Adaptor that bridges [`probe_dts`] into the registry's +/// [`oxideav_core::ProbeFn`] signature. +fn probe_dts_tag(ctx: &ProbeContext) -> Confidence { + match ctx.packet { + Some(pkt) => probe_dts(pkt), + // Tag matched but no packet sample is available: return + // confidence 1.0 so the lookup picks us, mirroring the + // "claim is unambiguous" convention CodecInfo::probe is + // documented under (None = always 1.0). + None => 1.0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxideav_core::{CodecResolver, Packet, TimeBase}; + + /// A real DTS Core frame header captured from `ffmpeg -c:a dca` + /// — same fixture used by the black-box integration test. + const REAL_DTS_FRAME_HEADER: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, 0xef, + 0x7f, + ]; + + #[test] + fn probe_returns_1_for_valid_be_header() { + assert_eq!(probe_dts(&REAL_DTS_FRAME_HEADER), 1.0); + } + + #[test] + fn probe_returns_0p5_for_truncated_be_header() { + // 5 bytes — enough for sync detection but not for the full + // 15-byte header window. + let truncated = &REAL_DTS_FRAME_HEADER[..5]; + assert_eq!(probe_dts(truncated), 0.5); + } + + #[test] + fn probe_returns_0p5_for_buffer_shorter_than_sync() { + // 2 bytes — `detect_sync` itself returns UnexpectedEof. + let truncated = &REAL_DTS_FRAME_HEADER[..2]; + assert_eq!(probe_dts(truncated), 0.5); + } + + #[test] + fn probe_returns_0_for_invalid_header() { + let garbage = [0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(probe_dts(&garbage), 0.0); + } + + #[test] + fn probe_returns_0_for_short_invalid_input() { + // Empty input — `detect_sync` returns UnexpectedEof → 0.5 + // because we cannot rule out a valid frame in the unseen + // bytes. (This mirrors the "truncated" semantics.) + assert_eq!(probe_dts(&[]), 0.5); + } + + #[test] + fn registry_resolves_dts_fourcc() { + let mut reg = CodecRegistry::new(); + register_codecs(&mut reg); + let tag = CodecTag::fourcc(b"dts "); + let ctx = ProbeContext::new(&tag); + let id = reg.resolve_tag(&ctx).expect("dts fourcc must resolve"); + assert_eq!(id, CodecId::new(CODEC_ID_STR)); + } + + #[test] + fn registry_resolves_dtsc_fourcc() { + let mut reg = CodecRegistry::new(); + register_codecs(&mut reg); + let tag = CodecTag::fourcc(b"dtsc"); + let ctx = ProbeContext::new(&tag); + let id = reg.resolve_tag(&ctx).expect("dtsc fourcc must resolve"); + assert_eq!(id, CodecId::new(CODEC_ID_STR)); + } + + #[test] + fn registry_decoder_factory_installs() { + let mut ctx = RuntimeContext::new(); + register(&mut ctx); + assert!(ctx.codecs.has_decoder(&CodecId::new(CODEC_ID_STR))); + } + + #[test] + fn send_packet_eagerly_parses_header() { + let pkt = Packet::new(0, TimeBase::new(1, 48_000), REAL_DTS_FRAME_HEADER.to_vec()); + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + handle.send_packet(&pkt).unwrap(); + let hdr = handle.last_header().expect("header must be cached"); + assert_eq!(hdr.frame_size_bytes, 1024); + assert_eq!(hdr.sfreq_index, 13); + assert_eq!(hdr.rate_index, 15); + // Round 5: the post-CRC window is captured on the same eager + // send_packet pass. The ffmpeg fixture encodes VERSION = 7 + // with every other post-CRC sub-field zeroed. + assert_eq!(hdr.version, 7); + assert_eq!(hdr.dialog_normalization, 0); + assert_eq!(hdr.source_pcm_resolution_index, 0); + } + + #[test] + fn send_packet_surfaces_no_sync_as_invalid_data() { + let pkt = Packet::new( + 0, + TimeBase::new(1, 48_000), + vec![0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ); + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + let err = handle.send_packet(&pkt).unwrap_err(); + assert!(matches!(err, CoreError::InvalidData(_)), "got {err:?}"); + } + + #[test] + fn send_packet_surfaces_short_buffer_as_need_more() { + let pkt = Packet::new(0, TimeBase::new(1, 48_000), vec![0x7F, 0xFE, 0x80]); + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + let err = handle.send_packet(&pkt).unwrap_err(); + assert!(matches!(err, CoreError::NeedMore), "got {err:?}"); + } + + #[test] + fn receive_frame_returns_unsupported_after_header_parse() { + let pkt = Packet::new(0, TimeBase::new(1, 48_000), REAL_DTS_FRAME_HEADER.to_vec()); + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + handle.send_packet(&pkt).unwrap(); + let err = handle.receive_frame().unwrap_err(); + assert!(matches!(err, CoreError::Unsupported(_)), "got {err:?}"); + } + + #[test] + fn receive_frame_returns_need_more_without_packet() { + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + let err = handle.receive_frame().unwrap_err(); + assert!(matches!(err, CoreError::NeedMore), "got {err:?}"); + } + + #[test] + fn receive_frame_returns_eof_after_flush_without_packet() { + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + handle.flush().unwrap(); + let err = handle.receive_frame().unwrap_err(); + assert!(matches!(err, CoreError::Eof), "got {err:?}"); + } + + #[test] + fn reset_clears_cached_header() { + let pkt = Packet::new(0, TimeBase::new(1, 48_000), REAL_DTS_FRAME_HEADER.to_vec()); + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + handle.send_packet(&pkt).unwrap(); + assert!(handle.last_header().is_some()); + handle.reset().unwrap(); + assert!(handle.last_header().is_none()); + assert!(!handle.eof); + } + + /// Pack `(value, width)` fields MSB-first. + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + /// A complete raw-16-bit Core frame (clean header + a one-channel + /// all-`ABITS==0` body) decodes end to end through the registry + /// `Decoder`, emitting a planar S32 `AudioFrame` of the right shape + /// instead of the historical `Unsupported`. + #[test] + fn receive_frame_decodes_common_core_case_to_audio() { + // Clean header: parse the fixture, clear DYNF/CPF/ASPF, re-encode. + let mut header = parse_frame_header(&REAL_DTS_FRAME_HEADER).unwrap(); + header.dynamic_range = false; + header.predictor_history = false; + header.aspf = false; + let mut bytes = crate::encode_frame_header_be(&header).unwrap(); + + // One-channel ACH (nSUBS=2, nVQSUB=2, BHUFF=6 Linear5Bit), then + // a one-subframe all-ABITS-0 side info, then a single DSYNC. + let mut body: Vec<(u32, u8)> = vec![ + (0, 4), + (0, 3), + (0, 5), + (1, 5), + (0, 3), + (0, 2), + (0, 3), + (6, 3), + ]; + body.push((0, 1)); + for _ in 1..5 { + body.push((0, 2)); + } + for _ in 5..10 { + body.push((0, 3)); + } + for _ in 0..10 { + body.push((0, 2)); + } + body.push((0, 2)); // SSC + body.push((0, 3)); // PSC + body.push((0, 1)); // PMODE[0][0] + body.push((0, 1)); // PMODE[0][1] + body.push((0, 5)); // ABITS[0][0] + body.push((0, 5)); // ABITS[0][1] + body.push((0xffff, 16)); // DSYNC + bytes.extend_from_slice(&pack_fields(&body)); + bytes.extend_from_slice(&[0u8; 4]); + + let pkt = Packet::new(0, TimeBase::new(1, 48_000), bytes); + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + handle.send_packet(&pkt).unwrap(); + let frame = handle + .receive_frame() + .expect("common Core case must decode"); + match frame { + Frame::Audio(a) => { + // One channel, one subframe, one subsubframe -> 256 samples. + assert_eq!(a.data.len(), 1); + assert_eq!(a.samples, 256); + assert_eq!(a.data[0].len(), 256 * 4); // S32 planar + assert!(a.data[0].iter().all(|&b| b == 0)); // all-zero PCM + } + other => panic!("expected an audio frame, got {other:?}"), + } + } + + /// The bundled 5-frame `ffmpeg -c:a dca` fixture (real 48 kHz stereo + /// DTS Core). Each 1024-byte frame is a self-delimited raw-16-bit + /// Core frame. + const FIXTURE_5_FRAMES: &[u8] = include_bytes!("../tests/fixtures/dts_5_frames.bin"); + + /// Driving two consecutive real Core frames through the registry + /// `Decoder` emits two 512-sample stereo S32 frames, and the handle's + /// persistent §C.2.5 stream filter carries the inter-frame tail: the + /// second frame's PCM differs from what a freshly-reset decoder would + /// produce for that same frame in isolation. This is the registry- + /// level expression of the round-356 CoreStreamDecoder continuity + /// fix. + #[test] + fn registry_decoder_carries_filter_state_across_real_packets() { + // Frame 0 and frame 1 are the first two 1024-byte frames. + let f0 = &FIXTURE_5_FRAMES[0..1024]; + let f1 = &FIXTURE_5_FRAMES[1024..2048]; + + let decode_two = || { + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + let tb = TimeBase::new(1, 48_000); + handle + .send_packet(&Packet::new(0, tb, f0.to_vec())) + .unwrap(); + let a0 = match handle.receive_frame().unwrap() { + Frame::Audio(a) => a, + other => panic!("expected audio, got {other:?}"), + }; + handle + .send_packet(&Packet::new(1, tb, f1.to_vec())) + .unwrap(); + let a1 = match handle.receive_frame().unwrap() { + Frame::Audio(a) => a, + other => panic!("expected audio, got {other:?}"), + }; + (a0, a1) + }; + + let (a0, a1) = decode_two(); + // Real 48 kHz stereo: 16 blocks * 32 samples = 512 samples/ch. + assert_eq!(a0.data.len(), 2); + assert_eq!(a0.samples, 512); + assert_eq!(a0.data[0].len(), 512 * 4); + assert_eq!(a1.samples, 512); + // Both frames carry real (non-silent) audio. + assert!(a1.data[0].iter().any(|&b| b != 0)); + + // Decode frame 1 in isolation with a fresh handle (no carried + // tail). Its PCM must differ from the streamed frame 1 — the + // persistent filter bled frame 0's tail into the streamed result. + let mut fresh = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + fresh + .send_packet(&Packet::new(1, TimeBase::new(1, 48_000), f1.to_vec())) + .unwrap(); + let isolated1 = match fresh.receive_frame().unwrap() { + Frame::Audio(a) => a, + other => panic!("expected audio, got {other:?}"), + }; + assert_ne!( + a1.data, isolated1.data, + "streamed frame 1 must carry frame 0's §C.2.5 filter tail, \ + differing from an isolated decode of frame 1" + ); + } + + /// A 14-bit-container Core frame decodes through the registry + /// `Decoder` to the **bit-exact** same PCM as its raw-16-bit form. + /// Each raw fixture frame is packed to a 14-bit container (both byte + /// orders), fed as a packet, and the emitted S32 planes are compared + /// byte-for-byte against a raw-16-bit decode of the same stream. This + /// exercises the round-398 §5.3.1 14-bit unpack path wired into + /// `send_packet` (the container transform is lossless, so the + /// reconstruction is identical up to the packing). + #[test] + fn registry_decodes_14bit_container_matching_raw() { + use crate::unpack14::pack_16bit_to_14bit; + + let raw_frames: Vec<&[u8]> = (0..5) + .map(|i| &FIXTURE_5_FRAMES[i * 1024..(i + 1) * 1024]) + .collect(); + let tb = TimeBase::new(1, 48_000); + + // Raw-16-bit baseline PCM (concatenated S32 planes per channel). + let mut raw_handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + let mut raw_pcm: Vec> = vec![Vec::new(), Vec::new()]; + for (i, f) in raw_frames.iter().enumerate() { + raw_handle + .send_packet(&Packet::new(i as u32, tb, f.to_vec())) + .unwrap(); + let a = match raw_handle.receive_frame().unwrap() { + Frame::Audio(a) => a, + other => panic!("expected audio, got {other:?}"), + }; + for (ch, plane) in raw_pcm.iter_mut().enumerate() { + plane.extend_from_slice(&a.data[ch]); + } + } + + for order in [ + FourteenBitByteOrder::BigEndian, + FourteenBitByteOrder::LittleEndian, + ] { + let mut handle = DtsDecoderHandle { + codec_id: CodecId::new(CODEC_ID_STR), + last_header: None, + last_frame_bytes: None, + last_pts: 0, + stream: None, + eof: false, + }; + let mut pcm: Vec> = vec![Vec::new(), Vec::new()]; + for (i, f) in raw_frames.iter().enumerate() { + let (packed, _bits) = pack_16bit_to_14bit(f, order); + handle + .send_packet(&Packet::new(i as u32, tb, packed)) + .unwrap(); + let a = match handle.receive_frame().unwrap() { + Frame::Audio(a) => a, + other => panic!("expected audio, got {other:?}"), + }; + for (ch, plane) in pcm.iter_mut().enumerate() { + plane.extend_from_slice(&a.data[ch]); + } + } + assert_eq!( + pcm, raw_pcm, + "{order:?} 14-bit container decode must be bit-exact vs the \ + raw-16-bit decode" + ); + } + } +} diff --git a/crates/vendor/oxideav-dts/src/rev2_aux.rs b/crates/vendor/oxideav-dts/src/rev2_aux.rs new file mode 100644 index 00000000..2389a9bf --- /dev/null +++ b/crates/vendor/oxideav-dts/src/rev2_aux.rs @@ -0,0 +1,702 @@ +//! §5.7.2 Rev2 Auxiliary Data Chunk (Table 5-33): the optional +//! DWORD-aligned broadcast-metadata block carrying the embedded-ES +//! downmix scale, per-subsubframe DRC values, and a dialog +//! normalization override. +//! +//! Transcribed from ETSI TS 102 114 V1.3.1 (2011-08) §5.7.2 +//! (Table 5-33 + §5.7.2.2 field descriptions, PDF p.37-41), staged at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. +//! +//! ## Navigation +//! +//! Per §5.7.2: the chunk begins at the DWORD-aligned sync word +//! `nSYNCRev2AUX = 0x7004C070` ("aligned on the 32-bit boundary from +//! the beginning of the core stream"), located by "searching forward +//! starting after all auxiliary data bytes AUXD are extracted; or +//! searching backward starting from the end of the audio frame". The +//! chunk "may be encoded in the stream even when AUXF=FALSE". +//! [`find_rev2_aux`] implements the backward search. +//! +//! ## Chunk layout (Table 5-33) +//! +//! ```text +//! nSYNCRev2AUX = ExtractBits(32); // 0x7004C070 +//! nRev2AUXDataByteSize = ExtractBits(7) + 1; // valid 3..=128 +//! bESMetaDataFlag = ExtractBits(1); +//! if (bESMetaDataFlag) +//! nEmbESDownMixScaleIndex = ExtractBits(8); // valid 40..=240 +//! if (nRev2AUXDataByteSize > 4) +//! bBroadcastMetadataPresent = ExtractBits(1); +//! else +//! bBroadcastMetadataPresent = FALSE; +//! if (bBroadcastMetadataPresent) { +//! bDRCMetadataPresent = ExtractBits(1); +//! bDialnormMetadata = ExtractBits(1); +//! if (bDRCMetaDataPresent) +//! DRCversion_Rev2AUX = ExtractBits(4); +//! nByteAlign0 = ExtractBits(…); // to byte boundary +//! if (bDRCMetaDataPresent) // DRCversion == 1 +//! for (s = 0; s < nSSC; s++) +//! subsubFrameDRC_Rev2AUX[s] = ExtractBits(8); +//! if (bDialnormMetadata) +//! DIALNORM_rev2aux = ExtractBits(5); // DNG = -value dB +//! } +//! ReservedRev2Aux, ByteAlignforRev2AuxCRC; // skipped via size +//! nRev2AUXCRC16 = ExtractBits(16); +//! ``` +//! +//! `nRev2AUXDataByteSize` counts the bytes "from the +//! nRev2AUXDataByteSize to nRev2AUXCRC16 inclusive", so the 16-bit +//! CRC sits at byte offset `nRev2AUXDataByteSize - 2` from the size +//! field — the parser reads it there (the spec's own "jump forward" +//! locator), which also skips the reserved field of "unspecified +//! duration". The check word uses the Annex B algorithm (CRC-CCITT, +//! polynomial `0x1021`, init `0xFFFF` — [`crate::dts_crc16`], +//! `docs/audio/dts/dts-crc16.md`) over the `nRev2AUXDataByteSize - 2` +//! bytes starting at the size field; the parser recomputes it and +//! reports the outcome in [`Rev2AuxChunk::crc_valid`]. +//! +//! One DRC value is transmitted per subsubframe of the frame +//! (Table 5-34: 512-sample frame → 2, 1024 → 4, 2048 → 8, i.e. +//! `32·(NBLKS+1)/256` — the frame's sample count divided by the +//! 256-sample subsubframe). Only `DRCversion_Rev2AUX == 1` +//! (single-band, one 8-bit value per subsubframe) is defined; for any +//! other version the spec instructs decoders to ignore the DRC +//! payload, whose layout is undefined — the parser then skips to the +//! size-located CRC and reports the DRC codes (and any dialnorm +//! field behind them) as unavailable. +//! +//! The spec converts each DRC byte "into a dB gain by function +//! dts_dynrng_to_db()", recovered as a closed form in +//! `docs/audio/dts/dts-drc-dynrng.md`: the byte is 8-bit signed Q2 +//! two's-complement, `dB = (int8)code × 0.25` +//! ([`crate::dts_dynrng_to_db`]). §5.7.2.2 states these values +//! "should be used instead of any dynamic range control coefficients +//! found in the legacy core stream (indicated by flag DYNF)"; +//! [`Rev2Drc::gains_db`] / [`Rev2Drc::multipliers`] resolve the codes +//! through that function, and the raw codes stay available. + +use crate::bitreader::BitReader; +use crate::header::DtsFrameHeader; +use crate::{dmix_scale, dts_dynrng_to_db, dts_dynrng_to_linear, Error, Result}; + +/// The §5.7.2 Rev2 auxiliary-data sync word (`nSYNCRev2AUX`), +/// DWORD-aligned from the beginning of the core frame. +pub const REV2_AUX_SYNC_WORD: u32 = 0x7004_C070; + +/// The §5.7.2 DRC version this parser can walk +/// (`DRCversion_Rev2AUX`): "Currently only DRCversion_Rev2AUX = 1 is +/// supported in the Rev2Aux chunk" (single-band, one 8-bit value per +/// subsubframe). +pub const REV2_DRC_VERSION_SINGLE_BAND: u8 = 1; + +/// The §5.7.2 Rev2AUX per-subsubframe dynamic-range-control payload. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rev2Drc { + /// The 4-bit `DRCversion_Rev2AUX` field ("the first version + /// starts at 0x1"). + pub version: u8, + /// One 8-bit `subsubFrameDRC_Rev2AUX[]` code per subsubframe of + /// the frame. Empty when `version` is not + /// [`REV2_DRC_VERSION_SINGLE_BAND`] (the payload layout is + /// undefined and the spec instructs decoders to ignore it). + pub codes: Vec, +} + +impl Rev2Drc { + /// The per-subsubframe DRC gains in dB, resolving each 8-bit code + /// through the §5.7.2 `dts_dynrng_to_db()` function + /// ([`crate::dts_dynrng_to_db`]: the code is 8-bit signed Q2 + /// two's-complement, `dB = (int8)code × 0.25`, per + /// `docs/audio/dts/dts-drc-dynrng.md`). + #[must_use] + pub fn gains_db(&self) -> Vec { + self.codes.iter().map(|&c| dts_dynrng_to_db(c)).collect() + } + + /// The per-subsubframe linear DRC multipliers + /// (`10^(gains_db/20)`, [`crate::dts_dynrng_to_linear`]) — the + /// gain applied to the decoded samples of each subsubframe. + /// §5.7.2.2: "the DRC values in the Rev2AUX data chunk should be + /// used instead of any dynamic range control coefficients found + /// in the legacy core stream" (the `DYNF`/`RANGE` path, which + /// shares the same signed-Q2 code space). Callers needing the raw + /// bytes can read [`Self::codes`] directly. + #[must_use] + pub fn multipliers(&self) -> Vec { + self.codes + .iter() + .map(|&c| dts_dynrng_to_linear(c)) + .collect() + } +} + +/// A parsed §5.7.2 Rev2 Auxiliary Data Chunk (Table 5-33). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rev2AuxChunk { + /// Byte offset of the DWORD-aligned `nSYNCRev2AUX` sync word + /// within the frame. + pub offset: usize, + /// `nRev2AUXDataByteSize`: the chunk size in bytes from the size + /// field to the CRC inclusive (valid `3..=128`). + pub data_byte_size: u8, + /// `nEmbESDownMixScaleIndex` (present when `bESMetaDataFlag` is + /// set): the 8-bit §D.11 `DmixTable[]` index (valid `40..=240`, + /// i.e. `[-40 dB, 0 dB]`) of the attenuation applied to the core + /// channels during embedded-ES down-mixing. + pub es_downmix_scale_index: Option, + /// The broadcast-metadata DRC payload (present when the + /// broadcast and DRC flags are set). + pub drc: Option, + /// `DIALNORM_rev2aux` (present when the broadcast and dialnorm + /// flags are set, and the DRC payload in front of it was + /// walkable): the 5-bit dialog-normalization parameter that + /// takes priority over the legacy `DIALNORM` header field. + pub dialnorm: Option, + /// The raw 16-bit `nRev2AUXCRC16` word, read at byte offset + /// `data_byte_size - 2` from the size field (Annex B CRC-CCITT, + /// see [`crate::dts_crc16`]). + pub crc16: u16, + /// Whether [`Self::crc16`] matches the Annex B CRC-16 recomputed + /// over the chunk's protected region — the + /// `nRev2AUXDataByteSize - 2` bytes starting at the size field + /// (i.e. everything after the sync word up to the byte before the + /// CRC). A `false` here means the chunk is corrupt or the + /// DWORD-aligned sync match was a false alias; the parse result + /// is surfaced either way so callers can decide. + pub crc_valid: bool, +} + +impl Rev2AuxChunk { + /// The embedded-ES downmix scale factor `ESDmixScale` as a real + /// value, resolved through the §D.11 `DmixTable`. + #[must_use] + pub fn es_downmix_scale(&self) -> Option { + self.es_downmix_scale_index + .and_then(|i| dmix_scale(usize::from(i))) + } + + /// The Table 5-36 dialog normalization gain in dB applied to the + /// decoder outputs: `DNG = -(DIALNORM_rev2aux)`, i.e. `0` to + /// `-31` dB. + #[must_use] + pub fn dialog_normalization_gain_db(&self) -> Option { + self.dialnorm.map(|v| -(v as i8)) + } +} + +/// Search a core frame for the DWORD-aligned §5.7.2 Rev2 auxiliary +/// sync word `0x7004C070`, returning its byte offset. +/// +/// Implements the spec's "searching backward starting from the end of +/// the audio frame" over 32-bit-aligned offsets (alignment counted +/// from the frame's first sync byte). +#[must_use] +pub fn find_rev2_aux(frame: &[u8]) -> Option { + let sync = REV2_AUX_SYNC_WORD.to_be_bytes(); + if frame.len() < 4 { + return None; + } + let mut offset = (frame.len() - 4) & !3; + loop { + if frame[offset..offset + 4] == sync { + return Some(offset); + } + if offset == 0 { + return None; + } + offset -= 4; + } +} + +/// Number of subsubframes in the frame (`32·(NBLKS+1)/256` — one DRC +/// byte is transmitted per subsubframe, Table 5-34). +fn subsubframes_per_frame(header: &DtsFrameHeader) -> Result { + // `blocks_per_frame` carries the raw `NBLKS` field; the block + // count is `NBLKS + 1` and each block is 32 PCM samples. + let blocks = usize::from(header.blocks_per_frame) + 1; + if blocks % 8 != 0 { + return Err(Error::Rev2AuxDrcCountUnresolved { + blocks: blocks as u8, + }); + } + Ok(blocks / 8) +} + +/// Parse the §5.7.2 Rev2 Auxiliary Data Chunk beginning at +/// `byte_offset` within `frame` (an offset produced by +/// [`find_rev2_aux`]). +/// +/// `header` supplies the frame's subsubframe count (one DRC value per +/// subsubframe). +/// +/// # Errors +/// +/// - [`Error::Rev2AuxSyncMismatch`] — the bytes at `byte_offset` are +/// not the `0x7004C070` sync word. +/// - [`Error::Rev2AuxSizeOutOfRange`] — `nRev2AUXDataByteSize` is +/// outside `3..=128`, or the declared fields overran the declared +/// chunk size. +/// - [`Error::Rev2AuxEsScaleIndexOutOfRange`] — +/// `nEmbESDownMixScaleIndex` is outside the valid `40..=240`. +/// - [`Error::Rev2AuxDrcCountUnresolved`] — the frame's block count +/// is not a whole number of 256-sample subsubframes. +/// - [`Error::UnexpectedEof`] — the chunk walked past the end of the +/// frame. +pub fn parse_rev2_aux_at( + frame: &[u8], + byte_offset: usize, + header: &DtsFrameHeader, +) -> Result { + let mut br = BitReader::from_byte_offset(frame, byte_offset); + let sync = br.read_bits(32)?; + if sync != REV2_AUX_SYNC_WORD { + return Err(Error::Rev2AuxSyncMismatch { found: sync }); + } + + let data_byte_size = (br.read_bits(7)? + 1) as u8; + if !(3..=128).contains(&data_byte_size) { + return Err(Error::Rev2AuxSizeOutOfRange { + size: data_byte_size, + }); + } + // The size counts bytes from the size field to the CRC inclusive; + // the CRC therefore sits `data_byte_size - 2` bytes past the size + // field (which begins right after the 4 sync bytes). + let crc_byte_offset = byte_offset + 4 + usize::from(data_byte_size) - 2; + if crc_byte_offset + 2 > frame.len() { + return Err(Error::UnexpectedEof); + } + + let es_downmix_scale_index = if br.read_bit()? { + let index = br.read_bits(8)? as u8; + if !(40..=240).contains(&index) { + return Err(Error::Rev2AuxEsScaleIndexOutOfRange { index }); + } + Some(index) + } else { + None + }; + + // "the bBroadcastMetaDataPresent flag is present in the stream if + // and only if the nRev2AUXDataByteSize > 4". + let broadcast = data_byte_size > 4 && br.read_bit()?; + + let mut drc = None; + let mut dialnorm = None; + if broadcast { + let drc_present = br.read_bit()?; + let dialnorm_present = br.read_bit()?; + let version = if drc_present { + br.read_bits(4)? as u8 + } else { + 0 + }; + // nByteAlign0: zero-pad to the next byte boundary (1 bit when + // the 4-bit version field was present, 5 bits when not). + let misalign = (br.absolute_bit_position() % 8) as u32; + if misalign != 0 { + br.skip_bits(8 - misalign)?; + } + let mut walkable = true; + if drc_present { + let codes = if version == REV2_DRC_VERSION_SINGLE_BAND { + let n = subsubframes_per_frame(header)?; + let mut codes = Vec::with_capacity(n); + for _ in 0..n { + codes.push(br.read_bits(8)? as u8); + } + codes + } else { + // Undefined payload layout: the spec instructs + // decoders to ignore the supplied DRC values; any + // dialnorm field behind them cannot be located. + walkable = false; + Vec::new() + }; + drc = Some(Rev2Drc { version, codes }); + } + if dialnorm_present && walkable { + dialnorm = Some(br.read_bits(5)? as u8); + } + } + + // The sequential walk must not have overrun the size-located CRC. + if br.absolute_bit_position() > crc_byte_offset * 8 { + return Err(Error::Rev2AuxSizeOutOfRange { + size: data_byte_size, + }); + } + + // ReservedRev2Aux ("unspecified duration") + ByteAlignforRev2AuxCRC + // are skipped by reading the CRC at its size-located offset. + let mut crc_reader = BitReader::from_byte_offset(frame, crc_byte_offset); + let crc16 = crc_reader.read_bits(16)? as u16; + // The size counts the protected span + CRC: the Annex B CRC-16 + // covers the `data_byte_size - 2` bytes starting at the size + // field (right after the 4 sync bytes) up to the CRC exclusive. + let crc_valid = crate::dts_crc16(&frame[byte_offset + 4..crc_byte_offset]) == crc16; + + Ok(Rev2AuxChunk { + offset: byte_offset, + data_byte_size, + es_downmix_scale_index, + drc, + dialnorm, + crc16, + crc_valid, + }) +} + +/// Locate ([`find_rev2_aux`]) and parse ([`parse_rev2_aux_at`]) the +/// §5.7.2 Rev2 Auxiliary Data Chunk of a core frame, returning +/// `Ok(None)` when the frame carries no DWORD-aligned Rev2 sync word. +/// +/// # Errors +/// +/// Propagates the [`parse_rev2_aux_at`] errors when a sync word is +/// found but the chunk does not parse. +pub fn parse_rev2_aux(frame: &[u8], header: &DtsFrameHeader) -> Result> { + match find_rev2_aux(frame) { + Some(offset) => parse_rev2_aux_at(frame, offset, header).map(Some), + None => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::{synth_header, synth_header_with_blocks, BitWriter}; + use crate::DMIX_TABLE_UNITY_INDEX; + + /// Build a frame: 32 bytes of non-sync filler, then the chunk + /// DWORD-aligned at offset 32, padded out to the declared size. + fn frame_with_chunk(chunk: &[u8]) -> Vec { + let mut frame = vec![0x55u8; 32]; + frame.extend_from_slice(chunk); + frame + } + + /// Assemble a chunk: sync + size + `body` bits, zero-padded so the + /// CRC lands exactly `size - 2` bytes after the size field, then + /// the CRC. + fn chunk(size: u8, body: impl FnOnce(&mut BitWriter), crc: u16) -> Vec { + let mut w = BitWriter::new(); + w.push_bits(u64::from(REV2_AUX_SYNC_WORD), 32); + w.push_bits(u64::from(size) - 1, 7); + body(&mut w); + let crc_bit = (4 + usize::from(size) - 2) * 8; + assert!(w.bit_len() <= crc_bit, "test chunk body overruns its size"); + while w.bit_len() < crc_bit { + w.push_bits(0, 1); + } + w.push_bits(u64::from(crc), 16); + w.into_bytes() + } + + /// Overwrite a test chunk's size-located 16-bit `nRev2AUXCRC16` + /// with the correct Annex B value over the covered span (the + /// `size - 2` bytes starting at the size field, byte 4 of the + /// chunk). + fn patch_crc(chunk: &mut [u8], size: u8) { + let crc_start = 4 + usize::from(size) - 2; + let crc = crate::dts_crc16(&chunk[4..crc_start]); + chunk[crc_start..crc_start + 2].copy_from_slice(&crc.to_be_bytes()); + } + + #[test] + fn parses_minimal_chunk() { + // size 3: size/ES-flag byte + 16-bit CRC, no broadcast flag. + let bytes = chunk(3, |w| w.push_bits(0, 1), 0xABCD); + let frame = frame_with_chunk(&bytes); + let header = synth_header(2, 0); + let chunk = parse_rev2_aux(&frame, &header).unwrap().unwrap(); + assert_eq!(chunk.offset, 32); + assert_eq!(chunk.data_byte_size, 3); + assert_eq!(chunk.es_downmix_scale_index, None); + assert_eq!(chunk.drc, None); + assert_eq!(chunk.dialnorm, None); + assert_eq!(chunk.crc16, 0xABCD); + // 0xABCD is not the Annex B CRC of the covered byte. + assert!(!chunk.crc_valid); + } + + #[test] + fn crc_valid_when_check_word_matches() { + // A broadcast-metadata chunk with the genuine Annex B check + // word over the size-located span verifies; corrupting any + // covered byte flips the verdict. + let header = synth_header(2, 0); + let mut bytes = chunk( + 8, + |w| { + w.push_bits(0, 1); // no ES metadata + w.push_bits(1, 1); // broadcast + w.push_bits(1, 1); // DRC + w.push_bits(0, 1); // no dialnorm + w.push_bits(u64::from(REV2_DRC_VERSION_SINGLE_BAND), 4); + w.align(8); + w.push_bits(0, 8); // DRC code, subsubframe 0 (unity) + w.push_bits(80, 8); // DRC code, subsubframe 1 + }, + 0, + ); + patch_crc(&mut bytes, 8); + let frame = frame_with_chunk(&bytes); + let parsed = parse_rev2_aux(&frame, &header).unwrap().unwrap(); + assert!(parsed.crc_valid); + assert_eq!(parsed.crc16, crate::dts_crc16(&bytes[4..4 + 8 - 2])); + + let mut corrupt = frame.clone(); + corrupt[32 + 6] ^= 0x01; // inside the covered span + let parsed = parse_rev2_aux_at(&corrupt, 32, &header).unwrap(); + assert!(!parsed.crc_valid); + } + + #[test] + fn parses_es_downmix_scale() { + let bytes = chunk( + 4, + |w| { + w.push_bits(1, 1); // bESMetaDataFlag + w.push_bits(DMIX_TABLE_UNITY_INDEX as u64, 8); + }, + 0x0102, + ); + let frame = frame_with_chunk(&bytes); + let chunk = parse_rev2_aux(&frame, &synth_header(2, 0)) + .unwrap() + .unwrap(); + assert_eq!(chunk.es_downmix_scale_index, Some(240)); + assert_eq!(chunk.es_downmix_scale(), Some(1.0)); + } + + #[test] + fn rejects_es_scale_index_out_of_range() { + // Valid range is 40..=240 ([-40 dB, 0 dB]). + let bytes = chunk( + 4, + |w| { + w.push_bits(1, 1); + w.push_bits(39, 8); + }, + 0, + ); + let frame = frame_with_chunk(&bytes); + assert_eq!( + parse_rev2_aux(&frame, &synth_header(2, 0)), + Err(Error::Rev2AuxEsScaleIndexOutOfRange { index: 39 }) + ); + } + + #[test] + fn parses_broadcast_drc_and_dialnorm() { + // 16-block frame -> 512 samples -> 2 subsubframes -> 2 DRC + // bytes (Table 5-34 row 1). + let header = synth_header(2, 0); + // Signed-Q2 wire codes: 0 -> 0 dB (unity), 1 -> +0.25 dB. + let drc_codes = [0u8, 1u8]; + let bytes = chunk( + 8, + |w| { + w.push_bits(0, 1); // no ES metadata + w.push_bits(1, 1); // bBroadcastMetadataPresent + w.push_bits(1, 1); // bDRCMetadataPresent + w.push_bits(1, 1); // bDialnormMetadata + w.push_bits(u64::from(REV2_DRC_VERSION_SINGLE_BAND), 4); + w.align(8); // nByteAlign0 (1 bit here) + for &c in &drc_codes { + w.push_bits(u64::from(c), 8); + } + w.push_bits(21, 5); // DIALNORM_rev2aux -> -21 dB + }, + 0xFEED, + ); + let frame = frame_with_chunk(&bytes); + let chunk = parse_rev2_aux(&frame, &header).unwrap().unwrap(); + let drc = chunk.drc.as_ref().expect("DRC present"); + assert_eq!(drc.version, 1); + assert_eq!(drc.codes, drc_codes); + assert_eq!(drc.gains_db(), vec![0.0, 0.25]); + let mult = drc.multipliers(); + assert_eq!(mult[0], 1.0); // signed-Q2 code 0 = unity + assert!((mult[1] - 10f64.powf(0.25 / 20.0)).abs() < 1e-12); // +0.25 dB + assert_eq!(chunk.dialnorm, Some(21)); + assert_eq!(chunk.dialog_normalization_gain_db(), Some(-21)); + assert_eq!(chunk.crc16, 0xFEED); + } + + #[test] + fn dialnorm_without_drc_follows_five_bit_alignment() { + // No DRC: nByteAlign0 is 5 bits (no 4-bit version field), the + // dialnorm field follows byte-aligned. + let bytes = chunk( + 6, + |w| { + w.push_bits(0, 1); // no ES metadata + w.push_bits(1, 1); // broadcast + w.push_bits(0, 1); // no DRC + w.push_bits(1, 1); // dialnorm + w.align(8); // 5 zero bits + w.push_bits(31, 5); + }, + 0x0F0F, + ); + let frame = frame_with_chunk(&bytes); + let chunk = parse_rev2_aux(&frame, &synth_header(2, 0)) + .unwrap() + .unwrap(); + assert_eq!(chunk.drc, None); + assert_eq!(chunk.dialnorm, Some(31)); + assert_eq!(chunk.dialog_normalization_gain_db(), Some(-31)); + } + + #[test] + fn unsupported_drc_version_skips_payload() { + // Version 2 payload layout is undefined: codes stay empty, + // the dialnorm behind it is unavailable, but the size-located + // CRC still reads. + let bytes = chunk( + 8, + |w| { + w.push_bits(0, 1); + w.push_bits(1, 1); // broadcast + w.push_bits(1, 1); // DRC + w.push_bits(1, 1); // dialnorm (unreachable) + w.push_bits(2, 4); // version 2 + }, + 0xD00D, + ); + let frame = frame_with_chunk(&bytes); + let chunk = parse_rev2_aux(&frame, &synth_header(2, 0)) + .unwrap() + .unwrap(); + assert_eq!( + chunk.drc, + Some(Rev2Drc { + version: 2, + codes: vec![] + }) + ); + assert_eq!(chunk.dialnorm, None); + assert_eq!(chunk.crc16, 0xD00D); + } + + #[test] + fn size_4_chunk_never_reads_broadcast_flag() { + // "the bBroadcastMetaDataPresent flag is present in the + // stream if and only if the nRev2AUXDataByteSize > 4": a + // size-4 chunk with a set bit where the flag would sit still + // parses with no broadcast metadata. + let bytes = chunk( + 4, + |w| { + w.push_bits(0, 1); // no ES metadata + w.push_bits(1, 1); // would-be broadcast flag: ignored + }, + 0x7777, + ); + let frame = frame_with_chunk(&bytes); + let chunk = parse_rev2_aux(&frame, &synth_header(2, 0)) + .unwrap() + .unwrap(); + assert_eq!(chunk.drc, None); + assert_eq!(chunk.dialnorm, None); + } + + #[test] + fn rejects_size_below_three() { + let mut w = BitWriter::new(); + w.push_bits(u64::from(REV2_AUX_SYNC_WORD), 32); + w.push_bits(1, 7); // size = 2 + w.push_bits(0, 1); + w.push_bits(0, 16); + let frame = frame_with_chunk(&w.into_bytes()); + assert_eq!( + parse_rev2_aux(&frame, &synth_header(2, 0)), + Err(Error::Rev2AuxSizeOutOfRange { size: 2 }) + ); + } + + #[test] + fn rejects_fields_overrunning_declared_size() { + // A size-5 chunk cannot hold broadcast DRC for 2 subsubframes: + // the sequential walk lands past the size-located CRC. + let bytes = chunk( + 5, + |w| { + w.push_bits(0, 1); + w.push_bits(1, 1); // broadcast + w.push_bits(1, 1); // DRC + w.push_bits(0, 1); + w.push_bits(1, 4); // version 1 + w.align(8); + // Body writer stops here; the parser's own walk wants + // 2 DRC bytes and overruns the CRC slot. + }, + 0, + ); + let frame = frame_with_chunk(&bytes); + assert_eq!( + parse_rev2_aux(&frame, &synth_header(2, 0)), + Err(Error::Rev2AuxSizeOutOfRange { size: 5 }) + ); + } + + #[test] + fn drc_count_needs_whole_subsubframes() { + // 12 blocks = 384 samples = 1.5 subsubframes: unresolvable. + let header = synth_header_with_blocks(2, 0, 11); + assert_eq!(header.blocks_per_frame, 11); // raw NBLKS -> 12 blocks + let bytes = chunk( + 8, + |w| { + w.push_bits(0, 1); + w.push_bits(1, 1); + w.push_bits(1, 1); + w.push_bits(0, 1); + w.push_bits(1, 4); + }, + 0, + ); + let frame = frame_with_chunk(&bytes); + assert_eq!( + parse_rev2_aux(&frame, &header), + Err(Error::Rev2AuxDrcCountUnresolved { blocks: 12 }) + ); + } + + #[test] + fn find_ignores_unaligned_sync() { + let mut frame = vec![0x55u8; 34]; + frame.extend_from_slice(&REV2_AUX_SYNC_WORD.to_be_bytes()); + assert_eq!(find_rev2_aux(&frame), None); + + let mut frame = vec![0x55u8; 40]; + frame.extend_from_slice(&REV2_AUX_SYNC_WORD.to_be_bytes()); + assert_eq!(find_rev2_aux(&frame), Some(40)); + } + + #[test] + fn parse_at_rejects_wrong_sync() { + let frame = vec![0u8; 8]; + assert_eq!( + parse_rev2_aux_at(&frame, 0, &synth_header(2, 0)), + Err(Error::Rev2AuxSyncMismatch { found: 0 }) + ); + } + + #[test] + fn truncated_chunk_reports_eof() { + // Declared size 128 but the frame ends long before the CRC. + let mut w = BitWriter::new(); + w.push_bits(u64::from(REV2_AUX_SYNC_WORD), 32); + w.push_bits(127, 7); // size = 128 + w.push_bits(0, 1); + let frame = frame_with_chunk(&w.into_bytes()); + assert_eq!( + parse_rev2_aux(&frame, &synth_header(2, 0)), + Err(Error::UnexpectedEof) + ); + } +} diff --git a/crates/vendor/oxideav-dts/src/side_info.rs b/crates/vendor/oxideav-dts/src/side_info.rs new file mode 100644 index 00000000..18ab502d --- /dev/null +++ b/crates/vendor/oxideav-dts/src/side_info.rs @@ -0,0 +1,1968 @@ +//! DTS Coherent Acoustics — Core Primary Audio Coding Side Information +//! (§5.4.1) ABITS / SCALES (a.k.a. ALLOC / SCFAC) bit-stream decoders. +//! +//! Round 195 (2026-05-31) lands the side-information half of the core +//! subframe decode path: extracting the per-channel × per-subband +//! ABITS bit-allocation index field and the per-channel × per-subband +//! SCALES scale-factor field from a packed bit stream, given the +//! channel-wide BHUFF (`nQSelect` for ABITS) and SHUFF +//! (`nQSelect` for SCALES) codebook-selector values read earlier +//! from the AUDIO CODING HEADER (clause §5.3.x). +//! +//! Everything in this module is transcribed verbatim from the locally +//! staged ETSI specification +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf` = +//! **ETSI TS 102 114 V1.3.1 (2011-08)**, *DTS Coherent Acoustics; Core +//! and Extensions with Additional Profiles*. The relevant clauses and +//! tables are: +//! +//! - §5.4.1 Table 5-28 ("Core side information") — the side-info +//! pseudocode listing the BHUFF / THUFF / SHUFF dispatch. +//! - §5.3.x Table 5-23 (THUFF → A4/B4/C4/D4), Table 5-24 (SHUFF → +//! {SA129..SE129, 6-bit linear, 7-bit linear, invalid}), Table 5-25 +//! (BHUFF → {A12..E12, linear-4-bit, linear-5-bit, invalid}), +//! Table 5-26 (SEL × ABITS → audio-data codebook), Table 5-27 +//! (ADJ → scale-factor adjustment value). +//! - Annex D §D.1.1 (6-bit RMS square-root table, 64 entries) and +//! §D.1.2 (7-bit RMS square-root table, 128 entries) — the +//! `pScaleTable` lookups in Table 5-28. +//! - Annex D §D.5.6 (12 Levels for BHUFF: tables A12, B12, C12, D12, +//! E12). +//! - Annex D §D.5.3 (5 Levels: tables A5, B5, C5) and §D.5.4 (7 Levels: +//! tables A7, B7, C7) — the SHUFF Huffman codebooks for the +//! {SA129, SB129, SC129, SD129, SE129} selectors. The spec routes +//! the SHUFF=0..4 codes through the 5-level tables for SA129/SB129/ +//! SC129 and the 7-level tables for SD129/SE129; §5.4.1's +//! `QSCALES.ppQ[nQSelect]->InverseQ(...)` is the dispatch table. +//! +//! The module is feature-independent (no `oxideav-core` dep), so it +//! is available under both the default and `--no-default-features` +//! build modes. +//! +//! # Scope +//! +//! This round only lands the **single-field** decode primitives plus +//! their backing tables; wiring them into a complete subframe walker +//! (which also requires the AUDIO CODING HEADER fields SUBFS, PCHS, +//! SUBS, VQSUB, JOINX, BHUFF/THUFF/SHUFF, plus the side-info loop +//! over `nPCHS × nSUBS[ch]`) is a separate follow-up. The decoders +//! exposed here take the caller-supplied `nQSelect`, codebook +//! selector, and bit-stream cursor; everything they read from the +//! bit stream is per the §5.4.1 pseudocode. + +use crate::bitreader::BitReader; +use crate::{Error, Result}; + +// --------------------------------------------------------------- +// Table 5-25 — Codebooks for Encoding Bit Allocation Index ABITS +// (BHUFF[ch] selector, §5.3.x). +// --------------------------------------------------------------- +// +// | BHUFF[ch] | Codebook (clause D.5.6) | +// | --------- | ----------------------- | +// | 0 | A12 | +// | 1 | B12 | +// | 2 | C12 | +// | 3 | D12 | +// | 4 | E12 | +// | 5 | Linear 4-bit | +// | 6 | Linear 5-bit | +// | 7 | Invalid | + +/// Codebook selector for the bit-allocation-index (ABITS) field, per +/// §5.3.x Table 5-25. `BHUFF[ch] == 7` is reserved/invalid. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AbitsCodebook { + /// `BHUFF=0` — Annex D §D.5.6 Table A12 (Huffman, 12 levels). + A12, + /// `BHUFF=1` — Annex D §D.5.6 Table B12 (Huffman, 12 levels). + B12, + /// `BHUFF=2` — Annex D §D.5.6 Table C12 (Huffman, 12 levels). + C12, + /// `BHUFF=3` — Annex D §D.5.6 Table D12 (Huffman, 12 levels). + D12, + /// `BHUFF=4` — Annex D §D.5.6 Table E12 (Huffman, 12 levels). + E12, + /// `BHUFF=5` — Linear 4-bit (raw 4-bit ABITS index, 0..=15). + Linear4Bit, + /// `BHUFF=6` — Linear 5-bit (raw 5-bit ABITS index, 0..=31). + Linear5Bit, +} + +impl AbitsCodebook { + /// Resolve a raw 3-bit `BHUFF[ch]` field to a codebook variant per + /// Table 5-25. `BHUFF == 7` is rejected as `Error::InvalidSideInfo`. + pub fn from_bhuff(bhuff: u8) -> Result { + match bhuff { + 0 => Ok(Self::A12), + 1 => Ok(Self::B12), + 2 => Ok(Self::C12), + 3 => Ok(Self::D12), + 4 => Ok(Self::E12), + 5 => Ok(Self::Linear4Bit), + 6 => Ok(Self::Linear5Bit), + // `7` is the spec's documented "Invalid" entry. + _ => Err(Error::InvalidSideInfo { + field: "BHUFF", + value: bhuff as u32, + }), + } + } +} + +// --------------------------------------------------------------- +// Annex D §D.5.6 — 12-level Huffman codebooks for BHUFF (ABITS). +// --------------------------------------------------------------- +// +// Each entry is `(quantization_level, code_length, code)`. The +// codeword is the low `code_length` bits of `code`, MSB-first in the +// bit-stream (matches the `BitReader::read_bits` convention used +// elsewhere in this crate). The codebooks below are transcribed +// verbatim from the staged PDF p.201-202. +// +// Per Table 5-25 the indexed level is `ABITS` itself (range 1..=12 in +// these codebooks; ABITS=0 is "no bits allocated" and is not +// transmitted via Huffman per the §5.4.1 pseudocode — it never +// appears in the ABITS table because the BHUFF dispatch is skipped +// when no bits would be allocated). + +/// Entry in a small Huffman codebook: `(symbol, code_length, code)`. +/// The codeword is read MSB-first from the bit stream; matching +/// happens by progressively reading bits and comparing against this +/// table by `code_length`. +type HuffmanEntry = (i16, u8, u16); + +/// Annex D §D.5.6 Table A12. +const TABLE_A12: &[HuffmanEntry] = &[ + (1, 1, 0), + (2, 2, 2), + (3, 3, 6), + (4, 4, 14), + (5, 5, 30), + (6, 6, 62), + (7, 8, 255), + (8, 8, 254), + (9, 9, 507), + (10, 9, 506), + (11, 9, 505), + (12, 9, 504), +]; + +/// Annex D §D.5.6 Table B12. +const TABLE_B12: &[HuffmanEntry] = &[ + (1, 1, 1), + (2, 2, 0), + (3, 3, 2), + (4, 5, 15), + (5, 5, 12), + (6, 6, 29), + (7, 7, 57), + (8, 7, 56), + (9, 7, 55), + (10, 7, 54), + (11, 7, 53), + (12, 7, 52), +]; + +/// Annex D §D.5.6 Table C12. +const TABLE_C12: &[HuffmanEntry] = &[ + (1, 2, 0), + (2, 3, 7), + (3, 3, 5), + (4, 3, 4), + (5, 3, 2), + (6, 4, 13), + (7, 4, 12), + (8, 4, 6), + (9, 5, 15), + (10, 6, 29), + (11, 7, 57), + (12, 7, 56), +]; + +/// Annex D §D.5.6 Table D12. +const TABLE_D12: &[HuffmanEntry] = &[ + (1, 2, 3), + (2, 2, 2), + (3, 2, 0), + (4, 3, 2), + (5, 4, 6), + (6, 5, 14), + (7, 6, 30), + (8, 7, 62), + (9, 8, 126), + (10, 9, 254), + (11, 10, 511), + (12, 10, 510), +]; + +/// Annex D §D.5.6 Table E12. +const TABLE_E12: &[HuffmanEntry] = &[ + (1, 1, 1), + (2, 2, 0), + (3, 3, 2), + (4, 4, 6), + (5, 5, 14), + (6, 7, 63), + (7, 7, 61), + (8, 8, 124), + (9, 8, 121), + (10, 8, 120), + (11, 9, 251), + (12, 9, 250), +]; + +/// Maximum code length over every codebook in this module. The +/// decoder reads bits one at a time up to this bound; an unmatched +/// pattern after that many bits is a stream-format failure. +const MAX_HUFFMAN_CODE_LEN: u32 = 14; + +/// Walk a Huffman codebook one bit at a time, MSB-first, returning +/// the matching `symbol` when a code of the prefix-matched length is +/// found. Returns `Error::HuffmanDecodeFailed` when no entry matches +/// within [`MAX_HUFFMAN_CODE_LEN`] bits. +fn decode_huffman( + br: &mut BitReader<'_>, + codebook: &[HuffmanEntry], + table_name: &'static str, +) -> Result { + let mut value: u32 = 0; + let mut bits_read: u8 = 0; + while bits_read < MAX_HUFFMAN_CODE_LEN as u8 { + let bit = br.read_bits(1)?; + value = (value << 1) | bit; + bits_read += 1; + // Try every entry whose code_length matches what we've read. + for &(symbol, code_len, code) in codebook { + if code_len == bits_read && value == code as u32 { + return Ok(symbol); + } + } + } + Err(Error::HuffmanDecodeFailed { table: table_name }) +} + +/// Decode a single ABITS field from the bit stream given the channel- +/// wide `BHUFF[ch]` codebook selector. Implements the BHUFF dispatch +/// in §5.4.1 Table 5-28: +/// +/// ```text +/// nQSelect = BHUFF[ch]; +/// for (n=0; nInverseQ(InputFrame, ABITS[ch][n]); +/// } +/// ``` +/// +/// Returns the per-subband ABITS index for one subband. The caller is +/// responsible for the `nVQSUB[ch]` loop (subbands ≥ `nVQSUB[ch]` are +/// VQ-encoded and have no ABITS). +/// Public entry point: decode an ABITS field starting at `bit_offset` +/// in `bytes`, returning `(decoded_abits, bits_consumed)`. The bit +/// offset is measured from the MSB of `bytes[0]`, matching the +/// MSB-first convention used elsewhere in this crate. Use this when +/// the caller is driving the side-info loop directly (e.g. from a +/// future subframe walker) and only needs the dispatch + Huffman +/// decode, not the full §5.4.1 SCALES / TMODE bookkeeping. +pub fn decode_abits_at( + bytes: &[u8], + bit_offset: usize, + codebook: AbitsCodebook, +) -> Result<(u8, usize)> { + // BitReader::from_byte_offset takes byte offsets; for the typical + // case where the caller is positioned mid-byte we need an + // arbitrary bit offset. Bit-shifting the buffer by `bit_offset` + // would copy; instead we synthesise a leading byte alignment by + // letting BitReader::from_byte_offset start at the byte that + // contains `bit_offset` and then skipping the remaining + // intra-byte bits via a no-op read. + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let start = bit_offset; + let value = decode_abits(&mut br, codebook)?; + let bits_consumed = br.absolute_bit_position() - start; + Ok((value, bits_consumed)) +} + +pub(crate) fn decode_abits(br: &mut BitReader<'_>, codebook: AbitsCodebook) -> Result { + match codebook { + AbitsCodebook::A12 => decode_huffman(br, TABLE_A12, "A12").map(|s| s as u8), + AbitsCodebook::B12 => decode_huffman(br, TABLE_B12, "B12").map(|s| s as u8), + AbitsCodebook::C12 => decode_huffman(br, TABLE_C12, "C12").map(|s| s as u8), + AbitsCodebook::D12 => decode_huffman(br, TABLE_D12, "D12").map(|s| s as u8), + AbitsCodebook::E12 => decode_huffman(br, TABLE_E12, "E12").map(|s| s as u8), + AbitsCodebook::Linear4Bit => br.read_bits(4).map(|v| v as u8), + AbitsCodebook::Linear5Bit => br.read_bits(5).map(|v| v as u8), + } +} + +// --------------------------------------------------------------- +// Table 5-23 — Selection of Huffman Codebook for Encoding TMODE +// (THUFF[ch] selector, §5.3.2 / staged PDF p.26). +// --------------------------------------------------------------- +// +// | THUFF[ch] | Huffman Codebook | +// | --------- | ---------------- | +// | 0 | A4 | +// | 1 | B4 | +// | 2 | C4 | +// | 3 | D4 | +// +// THUFF[ch] is a 2-bit wire field — the §5.3.2 Table 5-21 Core audio +// coding header pseudocode (staged PDF p.24) reads +// `THUFF[ch] = ExtractBits(2);` ("2 bits per channel") — so all four +// wire values resolve to a documented codebook. Unlike BHUFF/SHUFF +// there is no reserved/invalid row, and the resolver below is total +// over the masked 2-bit input. + +/// Codebook selector for the transient-mode (TMODE) field, per §5.3.2 +/// Table 5-23 (staged PDF p.26). All four 2-bit `THUFF[ch]` values +/// are valid (no reserved row). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TmodeCodebook { + /// `THUFF=0` — Annex D §D.5.2 Table A4 (Huffman, 4 levels). + A4, + /// `THUFF=1` — Annex D §D.5.2 Table B4 (Huffman, 4 levels). + B4, + /// `THUFF=2` — Annex D §D.5.2 Table C4 (Huffman, 4 levels). + C4, + /// `THUFF=3` — Annex D §D.5.2 Table D4 (Huffman, 4 levels — + /// every code is 2 bits, so this variant is equivalent to a raw + /// 2-bit read). + D4, +} + +impl TmodeCodebook { + /// Resolve a raw 2-bit `THUFF[ch]` field to a codebook variant + /// per Table 5-23. Only the low 2 bits of the input are + /// consulted (matching the `ExtractBits(2)` wire width fixed by + /// Table 5-21), so the mapping is total. + pub fn from_thuff(thuff: u8) -> Self { + match thuff & 0b11 { + 0 => Self::A4, + 1 => Self::B4, + 2 => Self::C4, + _ => Self::D4, + } + } + + /// The canonical 2-bit `THUFF[ch]` wire value for this variant + /// (the inverse of [`Self::from_thuff`]). + pub fn thuff(self) -> u8 { + match self { + Self::A4 => 0, + Self::B4 => 1, + Self::C4 => 2, + Self::D4 => 3, + } + } +} + +// --------------------------------------------------------------- +// Annex D §D.5.2 — "4 Levels (For TMODE)" Huffman codebooks. +// Transcribed verbatim from the staged PDF p.198. +// --------------------------------------------------------------- + +/// Annex D §D.5.2 Table A4. +const TABLE_A4: &[HuffmanEntry] = &[(0, 1, 0), (1, 2, 2), (2, 3, 6), (3, 3, 7)]; + +/// Annex D §D.5.2 Table B4. +const TABLE_B4: &[HuffmanEntry] = &[(0, 2, 2), (1, 3, 6), (2, 3, 7), (3, 1, 0)]; + +/// Annex D §D.5.2 Table C4. +const TABLE_C4: &[HuffmanEntry] = &[(0, 3, 6), (1, 3, 7), (2, 1, 0), (3, 2, 2)]; + +/// Annex D §D.5.2 Table D4. All four codes are 2 bits and equal +/// their quantization level, so this codebook degenerates to a raw +/// 2-bit field. +const TABLE_D4: &[HuffmanEntry] = &[(0, 2, 0), (1, 2, 1), (2, 2, 2), (3, 2, 3)]; + +/// Decode a single TMODE field from the bit stream given the +/// channel-wide `THUFF[ch]` codebook selector. Implements the THUFF +/// dispatch in §5.4.1 Table 5-28 (staged PDF p.28): +/// +/// ```text +/// nQSelect = THUFF[ch]; +/// for (n=0; n 0 ) // Present only if bits allocated +/// QTMODE.ppQ[nQSelect]->InverseQ(InputFrame, TMODE[ch][n]); +/// ``` +/// +/// Returns `(tmode, bits_consumed)` where `tmode` is in `0..=3`: +/// `0` means no transient in the subframe for this subband, and a +/// non-zero value means the transition occurred in subsubframe +/// `TMODE[ch][n] + 1` (PDF p.30 field description). The +/// `ABITS[ch][n] > 0` / `nSSC > 1` transmission conditions are the +/// caller's responsibility (see PDF p.30: TMODE is not transmitted +/// when only one subsubframe is present, for VQ-encoded high- +/// frequency subbands, or for subbands without bit allocation). +pub fn decode_tmode_at( + bytes: &[u8], + bit_offset: usize, + codebook: TmodeCodebook, +) -> Result<(u8, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let start = bit_offset; + let value = decode_tmode(&mut br, codebook)?; + let bits_consumed = br.absolute_bit_position() - start; + Ok((value, bits_consumed)) +} + +pub(crate) fn decode_tmode(br: &mut BitReader<'_>, codebook: TmodeCodebook) -> Result { + let (table, name) = match codebook { + TmodeCodebook::A4 => (TABLE_A4, "A4"), + TmodeCodebook::B4 => (TABLE_B4, "B4"), + TmodeCodebook::C4 => (TABLE_C4, "C4"), + TmodeCodebook::D4 => (TABLE_D4, "D4"), + }; + decode_huffman(br, table, name).map(|s| s as u8) +} + +// --------------------------------------------------------------- +// Table 5-24 — Code Books and Square Root Tables for Scale Factors +// (SHUFF[ch] selector, §5.3.x). +// --------------------------------------------------------------- +// +// | SHUFF[ch] | Code Book | Square Root Table | +// | --------- | --------------- | ----------------------- | +// | 0 | SA129 | 6 bit (clause D.1.1) | +// | 1 | SB129 | 6 bit (clause D.1.1) | +// | 2 | SC129 | 6 bit (clause D.1.1) | +// | 3 | SD129 | 6 bit (clause D.1.1) | +// | 4 | SE129 | 6 bit (clause D.1.1) | +// | 5 | 6-bit linear | 6 bit (clause D.1.1) | +// | 6 | 7-bit linear | 7 bit (clause D.1.2) | +// | 7 | Invalid | Invalid | +// +// The five 129-entry SA/SB/SC/SD/SE codebooks themselves are NOT +// transcribed in the staged PDF as "129"-suffixed tables; the spec +// instead routes them through the Annex D §D.5.x small-Huffman +// codebooks for the 5- and 7-level cases. Per Table 5-28's +// `nScaleSum += nScale; pScaleTable->LookUp(nScaleSum, …)` flow, the +// transmitted Huffman codeword is a **difference** between two +// consecutive scale-factor quantisation indexes, not the absolute +// index. The decoder accumulates `nScaleSum` across the loop and +// looks the running sum up in the 6- or 7-bit square-root table. +// +// Round 195 surfaces the 6- and 7-bit linear paths plus the Annex D +// §D.5.3 (5-level: A5/B5/C5) and §D.5.4 (7-level: A7/B7/C7) Huffman +// codebooks used by SHUFF=0..4 to encode the **scale-factor +// difference** symbols. The staged ETSI PDF p.198-200 has the small- +// Huffman codebooks; the dispatch from SHUFF to (5-level or 7-level) +// is identified by the (signed) range of differences the codebook +// covers: SA/SB/SC129 use 5-level (-2..=2) differences and SD/SE129 +// use 7-level (-3..=3) differences in the staged tables. The full +// 129-level SA129..SE129 mapping itself remains a docs-completeness +// follow-up because the spec's staged Annex D in this revision +// elides the 129-entry tables; see README "Docs gaps" for the file +// citation. + +/// Codebook selector for the scale-factor (SCALES) field, per §5.3.x +/// Table 5-24. `SHUFF[ch] == 7` is reserved/invalid. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScalesCodebook { + /// `SHUFF=0` — SA129 difference codebook (Annex D §D.5.3 + /// Table A5 for the 5-level difference symbol set). Lookup table: + /// 6-bit RMS (§D.1.1). + Sa129, + /// `SHUFF=1` — SB129 difference codebook (Annex D §D.5.3 + /// Table B5). Lookup table: 6-bit RMS (§D.1.1). + Sb129, + /// `SHUFF=2` — SC129 difference codebook (Annex D §D.5.3 + /// Table C5). Lookup table: 6-bit RMS (§D.1.1). + Sc129, + /// `SHUFF=3` — SD129 difference codebook (Annex D §D.5.4 + /// Table A7). Lookup table: 6-bit RMS (§D.1.1). + Sd129, + /// `SHUFF=4` — SE129 difference codebook (Annex D §D.5.4 + /// Table B7). Lookup table: 6-bit RMS (§D.1.1). + Se129, + /// `SHUFF=5` — Linear 6-bit (raw 6-bit absolute SCALES index, + /// 0..=63). Lookup table: 6-bit RMS (§D.1.1). + Linear6Bit, + /// `SHUFF=6` — Linear 7-bit (raw 7-bit absolute SCALES index, + /// 0..=127). Lookup table: 7-bit RMS (§D.1.2). + Linear7Bit, +} + +impl ScalesCodebook { + /// Resolve a raw 3-bit `SHUFF[ch]` field to a codebook variant + /// per Table 5-24. `SHUFF == 7` is rejected as + /// `Error::InvalidSideInfo`. + pub fn from_shuff(shuff: u8) -> Result { + match shuff { + 0 => Ok(Self::Sa129), + 1 => Ok(Self::Sb129), + 2 => Ok(Self::Sc129), + 3 => Ok(Self::Sd129), + 4 => Ok(Self::Se129), + 5 => Ok(Self::Linear6Bit), + 6 => Ok(Self::Linear7Bit), + _ => Err(Error::InvalidSideInfo { + field: "SHUFF", + value: shuff as u32, + }), + } + } + + /// `true` for the five Huffman variants (`SA129..SE129`); the + /// transmitted symbols are scale-factor index **differences** and + /// the running accumulator `nScaleSum` is what indexes into the + /// square-root table. `false` for the two linear variants, whose + /// symbols are absolute scale-factor indexes themselves. + pub fn is_huffman_encoded(self) -> bool { + matches!( + self, + Self::Sa129 | Self::Sb129 | Self::Sc129 | Self::Sd129 | Self::Se129 + ) + } + + /// `true` iff the codebook routes through the §D.1.2 7-bit RMS + /// square-root table (only `Linear7Bit` does); the other six + /// route through the §D.1.1 6-bit RMS table per Table 5-24. + pub fn uses_7bit_rms_table(self) -> bool { + matches!(self, Self::Linear7Bit) + } +} + +/// Annex D §D.5.3 Table A5. +const TABLE_A5: &[HuffmanEntry] = &[(0, 1, 0), (1, 2, 2), (-1, 3, 6), (2, 4, 14), (-2, 4, 15)]; + +/// Annex D §D.5.3 Table B5. +const TABLE_B5: &[HuffmanEntry] = &[(0, 2, 2), (1, 2, 0), (-1, 2, 1), (2, 3, 6), (-2, 3, 7)]; + +/// Annex D §D.5.3 Table C5. +const TABLE_C5: &[HuffmanEntry] = &[(0, 1, 0), (1, 3, 4), (-1, 3, 5), (2, 3, 6), (-2, 3, 7)]; + +/// Annex D §D.5.4 Table A7. +const TABLE_A7: &[HuffmanEntry] = &[ + (0, 1, 0), + (1, 3, 6), + (-1, 3, 5), + (2, 3, 4), + (-2, 4, 14), + (3, 5, 31), + (-3, 5, 30), +]; + +/// Annex D §D.5.4 Table B7. +const TABLE_B7: &[HuffmanEntry] = &[ + (0, 2, 3), + (1, 2, 1), + (-1, 2, 0), + (2, 3, 4), + (-2, 4, 11), + (3, 5, 21), + (-3, 5, 20), +]; + +/// Per Table 5-24, the SHUFF=0..2 entries (SA129/SB129/SC129) route +/// through the 5-level codebooks; SHUFF=3..4 (SD129/SE129) route +/// through the 7-level codebooks. The dispatch is by codebook variant. +fn scales_huffman_codebook(codebook: ScalesCodebook) -> (&'static [HuffmanEntry], &'static str) { + match codebook { + ScalesCodebook::Sa129 => (TABLE_A5, "A5"), + ScalesCodebook::Sb129 => (TABLE_B5, "B5"), + ScalesCodebook::Sc129 => (TABLE_C5, "C5"), + ScalesCodebook::Sd129 => (TABLE_A7, "A7"), + ScalesCodebook::Se129 => (TABLE_B7, "B7"), + // Unreachable: the caller dispatches linear variants + // separately via the `is_huffman_encoded()` check. + ScalesCodebook::Linear6Bit | ScalesCodebook::Linear7Bit => (&[], ""), + } +} + +// --------------------------------------------------------------- +// Annex D §D.1.1 — 6-bit Quantization (Nominal 2,2 dB Step). +// 64 entries; index 63 is reserved/invalid. +// Transcribed verbatim from PDF p.191. +// --------------------------------------------------------------- + +/// 6-bit scale-factor square-root quantisation levels, per Annex D +/// §D.1.1. `RMS_6BIT[63]` is reserved/invalid per the staged table +/// (the spec writes "invalid"); reading scale_index 63 from the +/// stream surfaces `Error::InvalidSideInfo { field: "SCALES" }`. +pub const RMS_6BIT: [u32; 64] = [ + 1, 2, 2, 3, 3, 4, 6, 7, 10, 12, 16, 20, 26, 34, 44, 56, 72, 93, 120, 155, 200, 257, 331, 427, + 550, 708, 912, 1175, 1514, 1950, 2512, 3236, 4169, 5370, 6918, 8913, 11482, 14791, 19055, + 24547, 31623, 40738, 52481, 67608, 87096, 112202, 144544, 186209, 239883, 309030, 398107, + 512861, 660693, 851138, 1096478, 1412538, 1819701, 2344229, 3019952, 3890451, 5011872, 6456542, + 8317638, // index 63 is "invalid" per the spec; the value placed at the + // reserved slot is the next continuation of the geometric + // progression purely so unrelated tests/`len()` arithmetic + // doesn't see a sentinel. + 0, +]; + +// --------------------------------------------------------------- +// Annex D §D.1.2 — 7-bit Quantization (Nominal 1,1 dB Step). +// 128 entries; indices 125..=127 are reserved/invalid. +// Transcribed verbatim from PDF p.191-192. +// --------------------------------------------------------------- + +/// 7-bit scale-factor square-root quantisation levels, per Annex D +/// §D.1.2. Indices 125, 126, 127 are reserved/invalid per the +/// staged table. +pub const RMS_7BIT: [u32; 128] = [ + 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 8, 10, 11, 12, 14, 16, 18, 20, 23, 26, 30, 34, 38, + 44, 50, 56, 64, 72, 82, 93, 106, 120, 136, 155, 176, 200, 226, 257, 292, 331, 376, 427, 484, + 550, 624, 708, 804, 912, 1035, 1175, 1334, 1514, 1718, 1950, 2213, 2512, 2851, 3236, 3673, + 4169, 4732, 5370, 6095, 6918, 7852, 8913, 10116, 11482, 13032, 14791, 16788, 19055, 21627, + 24547, 27861, 31623, 35892, 40738, 46238, 52481, 59566, 67608, 76736, 87096, 98855, 112202, + 127350, 144544, 164059, 186209, 211349, 239883, 272270, 309030, 350752, 398107, 451856, 512861, + 582103, 660693, 749894, 851138, 966051, 1096478, 1244515, 1412538, 1603245, 1819701, 2065380, + 2344229, 2660725, 3019952, 3427678, 3890451, 4415704, 5011872, 5688529, 6456542, 7328245, + 8317638, + // indices 125/126/127 are "invalid" per the spec; zero so the + // reserved slot is recognisable in a debugger and arithmetic + // doesn't pull in a phantom value. + 0, 0, 0, +]; + +/// Decode a single SCALES field given the channel-wide `SHUFF[ch]` +/// codebook and the running scale-index accumulator `n_scale_sum` +/// (= `nScaleSum` in §5.4.1 Table 5-28). Implements one iteration of: +/// +/// ```text +/// nQSelect = SHUFF[ch]; +/// if (nQSelect == 6) pScaleTable = &RMS7Bit; // 7-bit (D.1.2) +/// else pScaleTable = &RMS6Bit; // 6-bit (D.1.1) +/// nScaleSum = 0; +/// for (n=0; n 0) { +/// QSCALES.ppQ[nQSelect]->InverseQ(InputFrame, nScale); +/// if (nQSelect < 5) // Huffman encoded -> difference +/// nScaleSum += nScale; +/// else // linear -> absolute +/// nScaleSum = nScale; +/// pScaleTable->LookUp(nScaleSum, SCALES[ch][n][0]); +/// if (TMODE[ch][n] > 0) { // transient -> 2nd factor +/// QSCALES.ppQ[nQSelect]->InverseQ(InputFrame, nScale); +/// if (nQSelect < 5) nScaleSum += nScale; +/// else nScaleSum = nScale; +/// pScaleTable->LookUp(nScaleSum, SCALES[ch][n][1]); +/// } +/// } +/// ``` +/// +/// Returns the resolved scale-factor value (the +/// `pScaleTable->LookUp(...)` output, i.e. the actual quantisation +/// level from §D.1.1 / §D.1.2) and the updated `n_scale_sum`. The +/// caller passes `0` for the first call in a SCALES loop and the +/// returned `n_scale_sum` for subsequent calls. +/// +/// The check on `ABITS[ch][n] > 0` is the caller's responsibility +/// (subbands with no allocated bits skip the SCALES decode entirely +/// per §5.4.1). The check on `TMODE[ch][n] > 0` for a second scale +/// factor is also caller-driven (call `decode_scales` twice with the +/// updated `n_scale_sum` for transient subbands). +/// Public entry point: decode a SCALES field starting at `bit_offset` +/// in `bytes`, returning `(scale_factor, updated_n_scale_sum, +/// bits_consumed)`. The `n_scale_sum` parameter is the running +/// accumulator; pass `0` for the first call in a SCALES loop and the +/// returned `updated_n_scale_sum` for subsequent calls. +pub fn decode_scales_at( + bytes: &[u8], + bit_offset: usize, + codebook: ScalesCodebook, + n_scale_sum: i32, +) -> Result<(u32, i32, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let start = bit_offset; + let (scale, new_sum) = decode_scales(&mut br, codebook, n_scale_sum)?; + let bits_consumed = br.absolute_bit_position() - start; + Ok((scale, new_sum, bits_consumed)) +} + +/// Decode a single §5.4.1 `JOIN_SCALES` field starting at `bit_offset`, +/// returning `(join_scale_factor, biased_index, bits_consumed)`. +/// +/// Per the §5.4.1 Table 5-28 joint-intensity walk (staged PDF p.29): +/// +/// ```text +/// nQSelect = JOIN_SHUFF[ch]; +/// for (n = nSUBS[ch]; n < nSUBS[nSourceCh]; n++) { +/// QSCALES.ppQ[nQSelect]->InverseQ(InputFrame, nJScale); +/// nJScale = nJScale + 64; // fixed +64 bias +/// JScaleTbl.LookUp(nJScale, JOIN_SCALES[ch][n]); +/// } +/// ``` +/// +/// Unlike the regular SCALES walk, the `JOIN_SCALES` loop does **not** +/// carry a running `nScaleSum` accumulator: each decoded `QSCALES` +/// symbol is biased by a fixed `+64` and directly indexes the §D.3 +/// [`crate::join_scale`] table ([`crate::JOIN_SCALE_FACTOR`]). The raw +/// symbol is therefore taken as-is — the Huffman code books emit a +/// signed value in a small window around zero, and the `+64` bias maps +/// the zero symbol to the D.3 unity entry (index 64). +/// +/// `codebook` is [`ScalesCodebook::from_shuff`] of the channel's 3-bit +/// `JOIN_SHUFF[ch]`. The Huffman variants (`SA129..SE129`) decode one +/// entropy symbol; the two linear variants read a raw 6- or 7-bit +/// absolute index (which is likewise biased by 64 before the D.3 lookup). +/// +/// # Errors +/// +/// * [`Error::InvalidSideInfo`] with field `"JOIN_SCALES"` when the +/// biased index falls outside the §D.3 table (`0..=128`) — a +/// well-formed stream keeps it in range by construction; +/// * [`Error::UnexpectedEof`] when the buffer ends mid-symbol. +pub fn decode_join_scale_at( + bytes: &[u8], + bit_offset: usize, + codebook: ScalesCodebook, +) -> Result<(f64, i32, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let start = bit_offset; + let (factor, biased) = decode_join_scale(&mut br, codebook)?; + let bits_consumed = br.absolute_bit_position() - start; + Ok((factor, biased, bits_consumed)) +} + +/// In-place §5.4.1 `JOIN_SCALES` field decode against a live +/// [`BitReader`]. Returns `(join_scale_factor, biased_index)`. See +/// [`decode_join_scale_at`] for the field semantics. +pub(crate) fn decode_join_scale( + br: &mut BitReader<'_>, + codebook: ScalesCodebook, +) -> Result<(f64, i32)> { + // Extract the raw QSCALES symbol. Huffman variants emit a signed + // difference symbol; linear variants read a raw absolute index. + let symbol: i32 = if codebook.is_huffman_encoded() { + let (table, name) = scales_huffman_codebook(codebook); + decode_huffman(br, table, name)? as i32 + } else { + let n_bits = if codebook.uses_7bit_rms_table() { 7 } else { 6 }; + br.read_bits(n_bits)? as i32 + }; + + // Bias by +64 and look the biased index up in the §D.3 table. + let biased = symbol + 64; + let factor = crate::join_scale::join_scale(biased).ok_or(Error::InvalidSideInfo { + field: "JOIN_SCALES", + value: biased as u32, + })?; + + Ok((factor, biased)) +} + +pub(crate) fn decode_scales( + br: &mut BitReader<'_>, + codebook: ScalesCodebook, + n_scale_sum: i32, +) -> Result<(u32, i32)> { + // 1. Extract the bit-stream symbol via the codebook dispatch. + let symbol: i32 = if codebook.is_huffman_encoded() { + let (table, name) = scales_huffman_codebook(codebook); + decode_huffman(br, table, name)? as i32 + } else { + // Linear: read 6 or 7 raw bits as an unsigned absolute index. + let n_bits = if codebook.uses_7bit_rms_table() { 7 } else { 6 }; + br.read_bits(n_bits)? as i32 + }; + + // 2. Update the running accumulator per the §5.4.1 dispatch: + // Huffman entries are differences; linear entries are absolute. + let new_scale_sum: i32 = if codebook.is_huffman_encoded() { + n_scale_sum + symbol + } else { + symbol + }; + + // 3. Look up the resulting scale factor in the appropriate + // square-root table per Table 5-24. + let table: &[u32] = if codebook.uses_7bit_rms_table() { + &RMS_7BIT + } else { + &RMS_6BIT + }; + + // Bounds-check the accumulator before indexing. A Huffman-encoded + // stream whose accumulated differences walk outside [0, table.len) + // is a stream-format failure (the encoder is required to keep + // scale-factor indexes inside the table by construction). + let idx_signed = new_scale_sum; + let len_signed = table.len() as i32; + if !(0..len_signed).contains(&idx_signed) { + return Err(Error::InvalidSideInfo { + field: "SCALES", + value: idx_signed as u32, + }); + } + let idx = idx_signed as usize; + + // Reject the spec-reserved indices (63 in §D.1.1; 125..=127 in + // §D.1.2) explicitly so the sentinel 0 we placed in the const + // doesn't leak through as a "valid" scale factor. + let invalid_in_6bit = !codebook.uses_7bit_rms_table() && idx == 63; + let invalid_in_7bit = codebook.uses_7bit_rms_table() && idx >= 125; + if invalid_in_6bit || invalid_in_7bit { + return Err(Error::InvalidSideInfo { + field: "SCALES", + value: idx as u32, + }); + } + + Ok((table[idx], new_scale_sum)) +} + +// --------------------------------------------------------------- +// Table 5-27 — Scale Factor Adjustment Index (ADJ) +// (§5.4.1 / §5.3.x, staged PDF p.27) +// --------------------------------------------------------------- +// +// Verbatim from the staged PDF p.27, Table 5-27 "Scale Factor +// Adjustment Index": +// +// | ADJ | Adjustment Value | +// | --- | ---------------- | +// | 0 | 1,0000 | +// | 1 | 1,1250 | +// | 2 | 1,2500 | +// | 3 | 1,4375 | +// +// (ETSI decimal-comma convention: `1,4375` = 1.4375.) PDF p.25 +// (Core audio coding header pseudocode, Table 5-21 entry "Look up +// ADJ table") fixes the wire width at **two bits** for every +// occurrence — the listing reads `ADJ = ExtractBits(2);` on every +// branch (ABITS=1 with SEL=0; ABITS=2..=5 with SEL<3; ABITS=6..=10 +// with SEL<7). The spec note under Table 5-27 reads: "This table +// shows the scale factor adjustment index values if Huffman coding +// is used to encode the subband quantization indexes" — the +// multiplier is applied to the scale factor (SCALES) for that +// (channel, subband) pair before the inverse quantiser runs. +// +// The two-bit wire encoding covers exactly the four documented +// rows: `0b00`..=`0b11` map to the four `Adj0..=Adj3` variants. +// The mapping is total over a masked 2-bit input, so +// `from_index(0..=3)` is total and the `decode_adj_at` reader +// always returns a typed variant inside a well-formed bit stream. + +/// Scale-factor adjustment multiplier decoded from the 2-bit `ADJ` +/// header field per **ETSI TS 102 114 V1.3.1 §5.4.1, Table 5-27** +/// (staged PDF p.27). +/// +/// `ADJ` is read from the bit stream during Core Audio Coding +/// Header processing (§5.3.x pseudocode, PDF p.25) whenever the +/// per-subband `SEL[ch][n]` codebook selector falls into a Huffman +/// range — the adjustment multiplier is applied to the SCALES +/// value for that `(channel, subband)` before the inverse +/// quantiser runs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ScaleFactorAdjustment { + /// `ADJ = 0b00` — multiplier 1.0000 (Table 5-27 row 1, the + /// identity adjustment). + Adj0, + /// `ADJ = 0b01` — multiplier 1.1250 (Table 5-27 row 2). + Adj1, + /// `ADJ = 0b10` — multiplier 1.2500 (Table 5-27 row 3). + Adj2, + /// `ADJ = 0b11` — multiplier 1.4375 (Table 5-27 row 4). + Adj3, +} + +impl ScaleFactorAdjustment { + /// Resolve a raw 2-bit `ADJ` field to a variant per Table 5-27. + /// + /// Only the low 2 bits of `adj` are consulted — the wire field + /// is 2 bits per Table 5-21's `ExtractBits(2)` notation + /// (PDF p.25), so any caller passing a wider integer is masked + /// down before dispatch. The mapping is total: every 2-bit + /// value `0..=3` corresponds to one of the four documented + /// rows. + pub fn from_index(adj: u8) -> Self { + match adj & 0b11 { + 0b00 => Self::Adj0, + 0b01 => Self::Adj1, + 0b10 => Self::Adj2, + // The remaining 2-bit pattern `0b11` is the only one + // left, so this branch is exhaustive on a masked 2-bit + // input. + _ => Self::Adj3, + } + } + + /// The 2-bit ADJ wire code (`0..=3`) corresponding to this + /// variant; the inverse of [`Self::from_index`]. + pub fn code(self) -> u8 { + match self { + Self::Adj0 => 0b00, + Self::Adj1 => 0b01, + Self::Adj2 => 0b10, + Self::Adj3 => 0b11, + } + } + + /// Adjustment multiplier per Table 5-27 (`f32`). + /// + /// All four multipliers have exact binary representations: + /// `1.0`, `1.125 = 9 / 8`, `1.25 = 5 / 4`, and + /// `1.4375 = 23 / 16`. The constants below are therefore + /// representable as `f32` (and as `f64`) with no rounding. + pub fn multiplier(self) -> f32 { + match self { + Self::Adj0 => 1.0000, + Self::Adj1 => 1.1250, + Self::Adj2 => 1.2500, + Self::Adj3 => 1.4375, + } + } + + /// Adjustment multiplier per Table 5-27 (`f64`). + /// + /// Provided for callers that hold SCALES in `f64`; the four + /// constants are exactly representable so the `f32` and `f64` + /// projections are numerically identical. + pub fn multiplier_f64(self) -> f64 { + match self { + Self::Adj0 => 1.0000, + Self::Adj1 => 1.1250, + Self::Adj2 => 1.2500, + Self::Adj3 => 1.4375, + } + } + + /// The adjustment multiplier as a rational with denominator 16: + /// `(numerator, 16)`. Every Table 5-27 multiplier is a multiple + /// of `1/16`, so a `u8` numerator over the fixed denominator + /// `16` is an exact representation for integer-arithmetic + /// callers: `Adj0 → 16/16`, `Adj1 → 18/16`, `Adj2 → 20/16`, + /// `Adj3 → 23/16`. + pub fn multiplier_rational(self) -> (u8, u8) { + let num = match self { + Self::Adj0 => 16, // 1.0000 = 16/16 + Self::Adj1 => 18, // 1.1250 = 18/16 + Self::Adj2 => 20, // 1.2500 = 20/16 + Self::Adj3 => 23, // 1.4375 = 23/16 + }; + (num, 16) + } +} + +/// Public entry point: decode an `ADJ` field starting at +/// `bit_offset` in `bytes`, returning `(adjustment, bits_consumed)`. +/// +/// The bit offset is measured from the MSB of `bytes[0]`, matching +/// the MSB-first convention used elsewhere in this crate. The field +/// width is fixed at 2 bits per Table 5-21 (PDF p.25). Returns +/// [`Error::UnexpectedEof`] when fewer than 2 bits remain after +/// `bit_offset`. +pub fn decode_adj_at(bytes: &[u8], bit_offset: usize) -> Result<(ScaleFactorAdjustment, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let start = bit_offset; + let raw = br.read_bits(2)? as u8; + let adj = ScaleFactorAdjustment::from_index(raw); + let bits_consumed = br.absolute_bit_position() - start; + Ok((adj, bits_consumed)) +} + +// --------------------------------------------------------------- +// §5.4.1 Table 5-28 — Subsubframe Count (SSC, nSSC) and Partial +// Subsubframe Sample Count (PSC). +// (Staged PDF p.28 — first three rows of the Primary Audio Side +// Information pseudocode; field descriptions on PDF p.29–p.30.) +// --------------------------------------------------------------- +// +// Verbatim from PDF p.28, top of Table 5-28: +// +// SSC = ExtractBits(2); // 2 bits +// nSSC = SSC + 1; +// PSC = ExtractBits(3); // 3 bits +// +// Field descriptions, PDF p.29 ("SSC (Subsubframe Count)") and +// p.30 ("PSC (Partial Subsubframe Sample Count)"): +// +// * **SSC.** "Indicates that there are `nSSC = SSC + 1` subsubframes +// in the current audio subframe." Wire width 2 bits → the count +// `nSSC` ranges over `1..=4` (the four valid Core-profile +// subsubframe counts). +// * **PSC.** "Indicates the number of subband samples held in a +// partial subsubframe for each of the active subbands. A partial +// subsubframe is one which has less than 8 subband samples. It +// exists only in a termination frame and is always at the end of +// the last normal subsubframe. A DSYNC word will always occur +// after a partial subsubframe." Wire width 3 bits → `0..=7`. A +// normal (non-termination) subsubframe carries 8 samples per +// active subband, so `PSC < 8`; the spec leaves `PSC = 0` as the +// "no partial subsubframe present" sentinel that termination +// frames may emit but that is structurally always meaningful only +// in termination frames. +// +// The downstream §5.4.1 loops (e.g. `for (n=0; n 0` marks the trailing partial subsubframe + /// of a termination frame; a value of `0` means no partial + /// subsubframe is present at the tail of this audio subframe. + pub psc: u8, +} + +impl SubsubframeCount { + /// Maximum 2-bit `SSC` wire value (`0b11 = 3`); decoded + /// `n_ssc` is therefore at most `4`. + pub const MAX_SSC: u8 = 0b11; + /// Maximum 3-bit `PSC` wire value (`0b111 = 7`). + pub const MAX_PSC: u8 = 0b111; + /// Total wire width of the `SSC` + `PSC` prefix, in bits: + /// `2 + 3 = 5`. + pub const WIRE_BITS: u32 = 5; + + /// Construct from the raw 2-bit `SSC` and 3-bit `PSC` fields. + /// + /// Only the low 2 / 3 bits of the inputs are consulted; any + /// higher bits in the caller-supplied integers are masked off + /// (matching the `ExtractBits(2)` / `ExtractBits(3)` semantics + /// of the §5.4.1 pseudocode). The mapping is total. + pub fn new(ssc: u8, psc: u8) -> Self { + Self { + ssc: ssc & Self::MAX_SSC, + psc: psc & Self::MAX_PSC, + } + } + + /// Decoded subsubframe count `nSSC = SSC + 1` (`1..=4`). + /// + /// Per PDF p.29 field description: "Indicates that there are + /// `nSSC = SSC + 1` subsubframes in the current audio + /// subframe." This is the count consumed by every downstream + /// §5.4.1 loop iterating over subsubframes. + pub fn n_ssc(self) -> u8 { + self.ssc + 1 + } + + /// Convenience: subband-sample stride for a *normal* (non- + /// partial) subsubframe span, equal to `8 * nSSC` (the quantity + /// used by Annex C §C.2.3 / §C.2.4 / §C.2.5 to size per- + /// subband sample arrays). Returns a `usize` for direct use as + /// a loop bound. Result fits in `5..=32` for any valid `ssc`. + pub fn samples_per_subsubframe_normal(self) -> usize { + // 8 samples per active subband per subsubframe, times nSSC + // subsubframes per audio subframe. Max value 8 * 4 = 32 + // fits comfortably in u8, but we promote to usize because + // downstream callers index into per-subband sample slices. + 8usize * self.n_ssc() as usize + } + + /// Returns `Some(psc)` when this audio subframe ends with a + /// partial subsubframe (`psc > 0`, termination-frame signal + /// per PDF p.30), or `None` when no partial tail is present + /// (`psc == 0`). + /// + /// The returned value is the partial subsubframe's sample + /// count per active subband (so `< 8`, since a partial + /// subsubframe by definition holds fewer than 8 subband + /// samples). + pub fn partial_sample_count(self) -> Option { + if self.psc == 0 { + None + } else { + Some(self.psc) + } + } + + /// Returns `true` if this prefix signals a termination frame + /// tail (i.e. `psc > 0`, per PDF p.30). + pub fn is_termination_tail(self) -> bool { + self.psc != 0 + } +} + +/// Decode the 5-bit `SSC` + `PSC` head of the §5.4.1 Primary +/// Audio Side Information block from `bytes`, starting at +/// `bit_offset` (MSB-first from `bytes[0]`). +/// +/// Returns `(SubsubframeCount, bits_consumed)` on success. The +/// width of the prefix is exactly [`SubsubframeCount::WIRE_BITS`] +/// (5 bits) per Table 5-28: SSC is read first as a 2-bit +/// `ExtractBits(2)`, then PSC as a 3-bit `ExtractBits(3)`. +/// +/// Returns [`Error::UnexpectedEof`] when fewer than 5 bits remain +/// after `bit_offset`. +pub fn decode_subsubframe_count_at( + bytes: &[u8], + bit_offset: usize, +) -> Result<(SubsubframeCount, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + let start = bit_offset; + // SSC = ExtractBits(2) + let ssc = br.read_bits(2)? as u8; + // PSC = ExtractBits(3) + let psc = br.read_bits(3)? as u8; + let prefix = SubsubframeCount::new(ssc, psc); + let bits_consumed = br.absolute_bit_position() - start; + Ok((prefix, bits_consumed)) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------- + // BHUFF / ABITS decode tests + // ----------------------------------------------------------- + + #[test] + fn bhuff_dispatch_rejects_reserved_value() { + assert!(matches!( + AbitsCodebook::from_bhuff(7), + Err(Error::InvalidSideInfo { + field: "BHUFF", + value: 7 + }) + )); + // 8..=255 would also be out-of-3-bit-range but the caller + // is responsible for masking BHUFF to its 3-bit width before + // dispatch; we still reject everything above 6. + assert!(AbitsCodebook::from_bhuff(8).is_err()); + } + + #[test] + fn bhuff_dispatch_resolves_six_documented_codes() { + // Cover the full {0..=6} grid of Table 5-25. + assert_eq!(AbitsCodebook::from_bhuff(0).unwrap(), AbitsCodebook::A12); + assert_eq!(AbitsCodebook::from_bhuff(1).unwrap(), AbitsCodebook::B12); + assert_eq!(AbitsCodebook::from_bhuff(2).unwrap(), AbitsCodebook::C12); + assert_eq!(AbitsCodebook::from_bhuff(3).unwrap(), AbitsCodebook::D12); + assert_eq!(AbitsCodebook::from_bhuff(4).unwrap(), AbitsCodebook::E12); + assert_eq!( + AbitsCodebook::from_bhuff(5).unwrap(), + AbitsCodebook::Linear4Bit + ); + assert_eq!( + AbitsCodebook::from_bhuff(6).unwrap(), + AbitsCodebook::Linear5Bit + ); + } + + /// Pack a series of (code, code_length) pairs into a byte stream + /// MSB-first. Trailing bits are zero-padded. + fn pack_codes(codes: &[(u16, u8)]) -> Vec { + let total_bits: usize = codes.iter().map(|(_, len)| *len as usize).sum(); + let total_bytes = total_bits.div_ceil(8); + let mut out = vec![0u8; total_bytes]; + let mut bit_pos: usize = 0; + for &(code, len) in codes { + for i in (0..len).rev() { + let bit = ((code >> i) & 1) as u8; + let byte_idx = bit_pos / 8; + let bit_in_byte = 7 - (bit_pos % 8); + out[byte_idx] |= bit << bit_in_byte; + bit_pos += 1; + } + } + out + } + + #[test] + fn decode_abits_a12_walks_every_symbol() { + // A12 entries: (symbol, code_length, code). Pack each + // (code, code_length) pair in order, then decode them back. + let codes: Vec<(u16, u8)> = TABLE_A12.iter().map(|&(_, l, c)| (c, l)).collect(); + let stream = pack_codes(&codes); + let mut br = BitReader::new(&stream); + for &(expected_symbol, _, _) in TABLE_A12 { + let got = decode_abits(&mut br, AbitsCodebook::A12).unwrap(); + assert_eq!(got as i16, expected_symbol); + } + } + + #[test] + fn decode_abits_every_huffman_codebook_walks_every_symbol() { + // Exhaustive cross-check: each of A12/B12/C12/D12/E12 must + // round-trip every symbol it lists. + for (cb, table) in [ + (AbitsCodebook::A12, TABLE_A12), + (AbitsCodebook::B12, TABLE_B12), + (AbitsCodebook::C12, TABLE_C12), + (AbitsCodebook::D12, TABLE_D12), + (AbitsCodebook::E12, TABLE_E12), + ] { + let codes: Vec<(u16, u8)> = table.iter().map(|&(_, l, c)| (c, l)).collect(); + let stream = pack_codes(&codes); + let mut br = BitReader::new(&stream); + for &(expected_symbol, _, _) in table { + let got = decode_abits(&mut br, cb).unwrap(); + assert_eq!( + got as i16, expected_symbol, + "codebook {:?} mis-decoded symbol {}", + cb, expected_symbol + ); + } + } + } + + #[test] + fn decode_abits_linear_4bit_returns_raw_field() { + // 4-bit linear: high nibble of byte 0 = 0xA = 10. + let stream = [0xA0]; + let mut br = BitReader::new(&stream); + assert_eq!( + decode_abits(&mut br, AbitsCodebook::Linear4Bit).unwrap(), + 10 + ); + } + + #[test] + fn decode_abits_linear_5bit_returns_raw_field() { + // 5-bit linear: top 5 bits of 0b10011_000 = 0b10011 = 19. + let stream = [0b1001_1000]; + let mut br = BitReader::new(&stream); + assert_eq!( + decode_abits(&mut br, AbitsCodebook::Linear5Bit).unwrap(), + 19 + ); + } + + #[test] + fn decode_abits_short_buffer_surfaces_eof() { + // The shortest A12 code is 1 bit; an empty buffer fails before + // it can read even that single bit. + let stream: [u8; 0] = []; + let mut br = BitReader::new(&stream); + assert_eq!( + decode_abits(&mut br, AbitsCodebook::A12).unwrap_err(), + Error::UnexpectedEof + ); + } + + #[test] + fn huffman_codebooks_are_complete_prefix_codes() { + // Sanity check: each of the ten Annex D codebooks transcribed + // in this module must satisfy Kraft's inequality with equality + // (sum_i 2^{-len_i} == 1.0) for it to be a complete prefix + // code — i.e. every infinite bit stream maps to exactly one + // symbol. The ETSI tables are designed this way and our + // decoder relies on the property (a "no Huffman entry matched" + // failure cannot fire on bit-stream input once the codebook + // dispatch picked one of these tables; only EOF can fail). + for (name, table) in [ + ("A12", TABLE_A12), + ("B12", TABLE_B12), + ("C12", TABLE_C12), + ("D12", TABLE_D12), + ("E12", TABLE_E12), + ("A5", TABLE_A5), + ("B5", TABLE_B5), + ("C5", TABLE_C5), + ("A7", TABLE_A7), + ("B7", TABLE_B7), + ("A4", TABLE_A4), + ("B4", TABLE_B4), + ("C4", TABLE_C4), + ("D4", TABLE_D4), + ] { + let kraft: f64 = table + .iter() + .map(|&(_, len, _)| 2f64.powi(-(len as i32))) + .sum(); + assert!( + (kraft - 1.0).abs() < 1e-9, + "codebook {name} fails Kraft equality (sum = {kraft})", + ); + } + } + + // ----------------------------------------------------------- + // THUFF / TMODE decode tests + // ----------------------------------------------------------- + + #[test] + fn thuff_dispatch_resolves_all_four_documented_codes() { + // Cover the full {0..=3} grid of Table 5-23. + assert_eq!(TmodeCodebook::from_thuff(0), TmodeCodebook::A4); + assert_eq!(TmodeCodebook::from_thuff(1), TmodeCodebook::B4); + assert_eq!(TmodeCodebook::from_thuff(2), TmodeCodebook::C4); + assert_eq!(TmodeCodebook::from_thuff(3), TmodeCodebook::D4); + } + + #[test] + fn thuff_dispatch_masks_high_bits_and_round_trips() { + // Only the low 2 bits are consulted (Table 5-21 fixes the + // wire width at ExtractBits(2)); every u8 input resolves to + // the variant of its low 2 bits, and thuff() inverts it. + for raw in 0..=u8::MAX { + let cb = TmodeCodebook::from_thuff(raw); + assert_eq!(cb, TmodeCodebook::from_thuff(raw & 0b11)); + assert_eq!(cb.thuff(), raw & 0b11); + assert_eq!(TmodeCodebook::from_thuff(cb.thuff()), cb); + } + } + + #[test] + fn decode_tmode_every_codebook_walks_every_symbol() { + // Each of A4/B4/C4/D4 must round-trip every symbol it lists + // (the four §D.5.2 tables, staged PDF p.198). + for (cb, table) in [ + (TmodeCodebook::A4, TABLE_A4), + (TmodeCodebook::B4, TABLE_B4), + (TmodeCodebook::C4, TABLE_C4), + (TmodeCodebook::D4, TABLE_D4), + ] { + let codes: Vec<(u16, u8)> = table.iter().map(|&(_, l, c)| (c, l)).collect(); + let stream = pack_codes(&codes); + let mut br = BitReader::new(&stream); + for &(expected_symbol, _, _) in table { + let got = decode_tmode(&mut br, cb).unwrap(); + assert_eq!( + got as i16, expected_symbol, + "codebook {:?} mis-decoded symbol {}", + cb, expected_symbol + ); + } + } + } + + #[test] + fn decode_tmode_at_reports_code_length_and_offset_handling() { + // A4's symbol 0 is the 1-bit code `0`; symbol 3 is the + // 3-bit code `111`. Pack `0` then `111` then `10` (= symbol + // 1) back-to-back starting at bit offset 3 after three + // leading filler 1-bits: 0b111_0_111_1, 0b0_0000000. + let stream = [0b1110_1111, 0b0000_0000]; + let (s0, used0) = decode_tmode_at(&stream, 3, TmodeCodebook::A4).unwrap(); + assert_eq!((s0, used0), (0, 1)); + let (s1, used1) = decode_tmode_at(&stream, 4, TmodeCodebook::A4).unwrap(); + assert_eq!((s1, used1), (3, 3)); + let (s2, used2) = decode_tmode_at(&stream, 7, TmodeCodebook::A4).unwrap(); + assert_eq!((s2, used2), (1, 2)); + } + + #[test] + fn decode_tmode_d4_is_a_raw_two_bit_field() { + // Table D4 maps every 2-bit code to itself; one byte packs + // four consecutive symbols 0b00_01_10_11 -> 0, 1, 2, 3. + let stream = [0b0001_1011]; + let mut br = BitReader::new(&stream); + for expected in 0..=3u8 { + assert_eq!(decode_tmode(&mut br, TmodeCodebook::D4).unwrap(), expected); + } + } + + #[test] + fn decode_tmode_short_buffer_surfaces_eof() { + let stream: [u8; 0] = []; + assert_eq!( + decode_tmode_at(&stream, 0, TmodeCodebook::B4).unwrap_err(), + Error::UnexpectedEof + ); + } + + // ----------------------------------------------------------- + // SHUFF / SCALES decode tests + // ----------------------------------------------------------- + + #[test] + fn shuff_dispatch_rejects_reserved_value() { + assert!(matches!( + ScalesCodebook::from_shuff(7), + Err(Error::InvalidSideInfo { + field: "SHUFF", + value: 7 + }) + )); + } + + #[test] + fn shuff_dispatch_resolves_seven_documented_codes() { + for (raw, expected) in [ + (0u8, ScalesCodebook::Sa129), + (1, ScalesCodebook::Sb129), + (2, ScalesCodebook::Sc129), + (3, ScalesCodebook::Sd129), + (4, ScalesCodebook::Se129), + (5, ScalesCodebook::Linear6Bit), + (6, ScalesCodebook::Linear7Bit), + ] { + assert_eq!(ScalesCodebook::from_shuff(raw).unwrap(), expected); + } + } + + #[test] + fn shuff_uses_7bit_table_only_for_linear7() { + for cb in [ + ScalesCodebook::Sa129, + ScalesCodebook::Sb129, + ScalesCodebook::Sc129, + ScalesCodebook::Sd129, + ScalesCodebook::Se129, + ScalesCodebook::Linear6Bit, + ] { + assert!(!cb.uses_7bit_rms_table(), "{:?} routes through D.1.1", cb); + } + assert!(ScalesCodebook::Linear7Bit.uses_7bit_rms_table()); + } + + #[test] + fn rms_table_lengths_match_spec_widths() { + // §D.1.1: 64 entries (index 0..=62 valid, 63 invalid). + // §D.1.2: 128 entries (index 0..=124 valid, 125..=127 invalid). + assert_eq!(RMS_6BIT.len(), 64); + assert_eq!(RMS_7BIT.len(), 128); + } + + #[test] + fn rms_table_anchor_values_match_spec_pdf() { + // Spot-check anchor entries against the staged PDF p.191-192. + assert_eq!(RMS_6BIT[0], 1); // (0,0 dB) + assert_eq!(RMS_6BIT[1], 2); // (6,0 dB) + assert_eq!(RMS_6BIT[31], 3236); // (70,2 dB) + assert_eq!(RMS_6BIT[62], 8317638); // (138,4 dB) + + assert_eq!(RMS_7BIT[0], 1); // (0,0 dB) + assert_eq!(RMS_7BIT[31], 64); // (36,1 dB) + // Index 63 is the bottom of column 1 in the §D.1.2 table; the + // staged PDF p.192 shows index 63 = 3673 ((71,3 dB)) and + // index 64 = 4169 (top of column 2, (72,4 dB)). + assert_eq!(RMS_7BIT[63], 3673); + assert_eq!(RMS_7BIT[64], 4169); + assert_eq!(RMS_7BIT[124], 8317638); // (138,4 dB) + } + + #[test] + fn decode_scales_linear6_returns_absolute_lookup() { + // Pack a raw 6-bit absolute index = 5, followed by a 6-bit + // index = 10 (in the same byte stream). The accumulator is + // overwritten each call (linear path). + // Bit layout: 000101_001010_00 = 0b00010100_10100000 = 0x14 0xA0. + let stream = [0x14, 0xA0]; + let mut br = BitReader::new(&stream); + let (val, sum) = decode_scales(&mut br, ScalesCodebook::Linear6Bit, 0).unwrap(); + assert_eq!(val, RMS_6BIT[5]); // RMS_6BIT[5] = 4 + assert_eq!(sum, 5); + let (val, sum) = decode_scales(&mut br, ScalesCodebook::Linear6Bit, sum).unwrap(); + assert_eq!(val, RMS_6BIT[10]); // RMS_6BIT[10] = 16 + assert_eq!(sum, 10); // linear: accumulator overwritten, not summed. + } + + #[test] + fn decode_scales_linear7_returns_absolute_lookup() { + // Pack a 7-bit absolute index = 31. 0011111_0 = 0x3E. + let stream = [0x3E]; + let mut br = BitReader::new(&stream); + let (val, sum) = decode_scales(&mut br, ScalesCodebook::Linear7Bit, 0).unwrap(); + assert_eq!(val, RMS_7BIT[31]); // RMS_7BIT[31] = 64 + assert_eq!(sum, 31); + } + + #[test] + fn decode_scales_sa129_accumulates_differences() { + // SA129 -> TABLE_A5. Symbols carry signed differences. + // Pack the sequence (+1, +1, +1, -1) which equals + // (TABLE_A5[1].code, TABLE_A5[0].code, TABLE_A5[0].code, + // TABLE_A5[2].code) by their (symbol -> entry) mapping: + // +1 -> (1, 2, 2) + // 0 -> (0, 1, 0) (we'll use this for "no movement") + // -1 -> (-1, 3, 6) + // Actually use +1, +1, -1 (skip the zero-movement step): + // +1 = code 0b10 (len 2) + // +1 = code 0b10 (len 2) + // -1 = code 0b110 (len 3) + // Stream bits: 10_10_110_0 = 0b10101100 = 0xAC. + let stream = [0xAC]; + let mut br = BitReader::new(&stream); + + let (val1, sum1) = decode_scales(&mut br, ScalesCodebook::Sa129, 0).unwrap(); + // 0 + 1 = 1; RMS_6BIT[1] = 2. + assert_eq!(sum1, 1); + assert_eq!(val1, RMS_6BIT[1]); + + let (val2, sum2) = decode_scales(&mut br, ScalesCodebook::Sa129, sum1).unwrap(); + // 1 + 1 = 2; RMS_6BIT[2] = 2. + assert_eq!(sum2, 2); + assert_eq!(val2, RMS_6BIT[2]); + + let (val3, sum3) = decode_scales(&mut br, ScalesCodebook::Sa129, sum2).unwrap(); + // 2 + (-1) = 1; RMS_6BIT[1] = 2. + assert_eq!(sum3, 1); + assert_eq!(val3, RMS_6BIT[1]); + } + + #[test] + fn decode_scales_negative_accumulator_rejected() { + // SA129 starting at 0, transmit -1 (code 0b110 len 3 + pad). + // Resulting accumulator = -1, out of [0, 64) → error. + let stream = [0b1100_0000]; + let mut br = BitReader::new(&stream); + let err = decode_scales(&mut br, ScalesCodebook::Sa129, 0).unwrap_err(); + assert!(matches!( + err, + Error::InvalidSideInfo { + field: "SCALES", + .. + } + )); + } + + #[test] + fn decode_scales_reserved_indices_rejected() { + // Linear6Bit reading raw index 63 must reject (spec-reserved). + // 0b111111_00 = 0xFC. + let stream = [0xFC]; + let mut br = BitReader::new(&stream); + let err = decode_scales(&mut br, ScalesCodebook::Linear6Bit, 0).unwrap_err(); + assert!(matches!( + err, + Error::InvalidSideInfo { + field: "SCALES", + value: 63 + } + )); + + // Linear7Bit reading raw index 125 must reject. 0b1111101_0 = 0xFA. + let stream = [0xFA]; + let mut br = BitReader::new(&stream); + let err = decode_scales(&mut br, ScalesCodebook::Linear7Bit, 0).unwrap_err(); + assert!(matches!( + err, + Error::InvalidSideInfo { + field: "SCALES", + value: 125 + } + )); + } + + #[test] + fn decode_scales_sd129_uses_7level_table_with_difference_semantics() { + // SD129 -> TABLE_A7. Symbols ±3 in addition to A5's ±2 range. + // Pack +3 (code 31, len 5) then -3 (code 30, len 5). + // Stream bits: 11111_11110_000000 = 0b11111111 0b10000000 = 0xFF 0x80. + let stream = [0xFF, 0x80]; + let mut br = BitReader::new(&stream); + + let (val1, sum1) = decode_scales(&mut br, ScalesCodebook::Sd129, 0).unwrap(); + assert_eq!(sum1, 3); // 0 + 3 + assert_eq!(val1, RMS_6BIT[3]); // RMS_6BIT[3] = 3 + + let (val2, sum2) = decode_scales(&mut br, ScalesCodebook::Sd129, sum1).unwrap(); + assert_eq!(sum2, 0); // 3 + (-3) + assert_eq!(val2, RMS_6BIT[0]); // RMS_6BIT[0] = 1 + } + + // ----------------------------------------------------------- + // Table 5-27 — Scale Factor Adjustment Index (ADJ) tests + // ----------------------------------------------------------- + + #[test] + fn adj_table_5_27_row_by_row() { + // Every documented (ADJ, Adjustment Value) row from Table 5-27. + let rows: [(u8, ScaleFactorAdjustment, f32); 4] = [ + (0b00, ScaleFactorAdjustment::Adj0, 1.0000), + (0b01, ScaleFactorAdjustment::Adj1, 1.1250), + (0b10, ScaleFactorAdjustment::Adj2, 1.2500), + (0b11, ScaleFactorAdjustment::Adj3, 1.4375), + ]; + for (code, expected_variant, expected_value) in rows { + let v = ScaleFactorAdjustment::from_index(code); + assert_eq!(v, expected_variant, "from_index({code:#04b})"); + assert_eq!(v.code(), code, "code() round-trip for {v:?}"); + assert_eq!( + v.multiplier(), + expected_value, + "multiplier() (f32) for {v:?}" + ); + assert_eq!( + v.multiplier_f64(), + expected_value as f64, + "multiplier_f64() for {v:?}" + ); + } + } + + #[test] + fn adj_from_index_masks_high_bits() { + // The wire field is 2 bits; widths beyond are masked off. + // `0b1100` & 0b11 == 0b00 → Adj0; `0b1111` & 0b11 == 0b11 → Adj3. + assert_eq!( + ScaleFactorAdjustment::from_index(0b1100), + ScaleFactorAdjustment::Adj0 + ); + assert_eq!( + ScaleFactorAdjustment::from_index(0b1111), + ScaleFactorAdjustment::Adj3 + ); + assert_eq!( + ScaleFactorAdjustment::from_index(0xFF), + ScaleFactorAdjustment::Adj3 + ); + assert_eq!( + ScaleFactorAdjustment::from_index(0xFC), + ScaleFactorAdjustment::Adj0 + ); + } + + #[test] + fn adj_multiplier_rational_matches_table_5_27() { + // Every Table 5-27 row, rationalised with denominator 16. + assert_eq!(ScaleFactorAdjustment::Adj0.multiplier_rational(), (16, 16)); + assert_eq!(ScaleFactorAdjustment::Adj1.multiplier_rational(), (18, 16)); + assert_eq!(ScaleFactorAdjustment::Adj2.multiplier_rational(), (20, 16)); + assert_eq!(ScaleFactorAdjustment::Adj3.multiplier_rational(), (23, 16)); + + // Each rational equals the f32 multiplier exactly (every + // numerator is a multiple of 1/16, and 1/16 is exact in + // IEEE-754 binary32). + for v in [ + ScaleFactorAdjustment::Adj0, + ScaleFactorAdjustment::Adj1, + ScaleFactorAdjustment::Adj2, + ScaleFactorAdjustment::Adj3, + ] { + let (num, den) = v.multiplier_rational(); + assert_eq!( + v.multiplier(), + num as f32 / den as f32, + "rational == f32 for {v:?}" + ); + } + } + + #[test] + fn decode_adj_at_byte_aligned() { + // Four ADJ fields packed back-to-back in one byte: + // 0b00 0b01 0b10 0b11 = 0b00_01_10_11 = 0x1B. + let stream = [0x1B]; + let mut bit_offset = 0; + for expected in [ + ScaleFactorAdjustment::Adj0, + ScaleFactorAdjustment::Adj1, + ScaleFactorAdjustment::Adj2, + ScaleFactorAdjustment::Adj3, + ] { + let (v, n) = decode_adj_at(&stream, bit_offset).unwrap(); + assert_eq!(v, expected, "decode_adj_at(@{bit_offset})"); + assert_eq!(n, 2, "bits_consumed @{bit_offset}"); + bit_offset += n; + } + assert_eq!(bit_offset, 8); + } + + #[test] + fn decode_adj_at_unaligned_bit_offset() { + // 5 leading filler bits, then the ADJ pair `0b10` (Adj2), + // then trailing bits. Layout: + // bit 0..5 = 0b11111 (filler, ignored) + // bit 5..7 = 0b10 (ADJ) + // bit 7 = 0b1 (filler, ignored) + // Combined: 0b1111_1101 = 0xFD. + let stream = [0xFD]; + let (v, n) = decode_adj_at(&stream, 5).unwrap(); + assert_eq!(v, ScaleFactorAdjustment::Adj2); + assert_eq!(n, 2); + } + + #[test] + fn decode_adj_at_crosses_byte_boundary() { + // Place the 2-bit ADJ across the byte boundary: bit 7 of + // byte 0 (MSB-most) carries the ADJ MSB; bit 0 of byte 1 + // carries the ADJ LSB. Pick `0b11` (Adj3). + // byte 0: 0b0000_0001 = 0x01 (ADJ MSB in bit 7) + // byte 1: 0b1000_0000 = 0x80 (ADJ LSB in bit 0) + let stream = [0x01, 0x80]; + let (v, n) = decode_adj_at(&stream, 7).unwrap(); + assert_eq!(v, ScaleFactorAdjustment::Adj3); + assert_eq!(n, 2); + } + + #[test] + fn decode_adj_at_reports_eof_when_buffer_short() { + // Only 1 bit left after `bit_offset` → EOF on the 2-bit read. + let stream = [0x00]; + let err = decode_adj_at(&stream, 7).unwrap_err(); + assert!(matches!(err, Error::UnexpectedEof)); + } + + #[test] + fn adj_code_round_trips_every_value() { + for code in 0u8..=3 { + let v = ScaleFactorAdjustment::from_index(code); + assert_eq!(v.code(), code); + } + } + + // ----------------------------------------------------------- + // §5.4.1 Table 5-28 — SSC / nSSC / PSC tests (Round 249, + // staged PDF p.28–p.30). + // ----------------------------------------------------------- + + #[test] + fn subsubframe_count_n_ssc_covers_every_ssc_value() { + // SSC = 0..=3 → nSSC = 1..=4 per PDF p.29 ("nSSC = SSC + 1"). + let cases = [(0u8, 1u8), (1, 2), (2, 3), (3, 4)]; + for (raw_ssc, expected_n_ssc) in cases { + let prefix = SubsubframeCount::new(raw_ssc, 0); + assert_eq!(prefix.ssc, raw_ssc); + assert_eq!(prefix.n_ssc(), expected_n_ssc); + } + } + + #[test] + fn subsubframe_count_samples_per_subsubframe_normal_is_8_times_n_ssc() { + // The 8 * nSSC quantity is the §C.2.3 / §C.2.4 / §C.2.5 + // per-subband sample stride, also referenced in this + // crate's `sum_diff.rs` and `joint_subband.rs` doc-comments + // as the "8 * nSSC" inner-loop bound. + let cases = [(0u8, 8usize), (1, 16), (2, 24), (3, 32)]; + for (raw_ssc, expected_samples) in cases { + let prefix = SubsubframeCount::new(raw_ssc, 0); + assert_eq!(prefix.samples_per_subsubframe_normal(), expected_samples); + } + } + + #[test] + fn subsubframe_count_masks_high_bits_of_ssc_and_psc() { + // ExtractBits(2) / ExtractBits(3) semantics: only the low + // 2 / 3 bits should reach the typed prefix. + let prefix = SubsubframeCount::new(0b1111_1101, 0b1111_1010); + assert_eq!(prefix.ssc, 0b01); + assert_eq!(prefix.psc, 0b010); + // All-ones inputs collapse to the max wire values. + let max = SubsubframeCount::new(0xFF, 0xFF); + assert_eq!(max.ssc, SubsubframeCount::MAX_SSC); + assert_eq!(max.psc, SubsubframeCount::MAX_PSC); + assert_eq!(max.n_ssc(), 4); + } + + #[test] + fn subsubframe_count_partial_sample_count_signals_termination_tail() { + // PSC == 0 → no partial subsubframe at the tail of the + // current audio subframe (PDF p.30: partial subsubframe + // "exists only in a termination frame"). + let normal = SubsubframeCount::new(2, 0); + assert_eq!(normal.partial_sample_count(), None); + assert!(!normal.is_termination_tail()); + // PSC > 0 → termination tail; the returned count is the + // partial subsubframe's sample count per active subband. + for psc in 1u8..=7 { + let tail = SubsubframeCount::new(2, psc); + assert_eq!(tail.partial_sample_count(), Some(psc)); + assert!(tail.is_termination_tail()); + } + } + + #[test] + fn subsubframe_count_wire_bits_constant_matches_table_5_28() { + // SSC = ExtractBits(2); PSC = ExtractBits(3); total 5 bits + // per the first two rows of Table 5-28 (PDF p.28). + assert_eq!(SubsubframeCount::WIRE_BITS, 5); + } + + #[test] + fn decode_subsubframe_count_at_byte_aligned() { + // Five-bit prefix at bit-offset 0: pack SSC=0b10 and + // PSC=0b011 into the top 5 bits of byte 0. + // bit 0..=1 (MSB-first) = SSC = 0b10 + // bit 2..=4 = PSC = 0b011 + // bit 5..=7 = 0b000 (zero padding) + // → 0b10011000 = 0x98 + let stream = [0x98]; + let (prefix, n) = decode_subsubframe_count_at(&stream, 0).unwrap(); + assert_eq!(prefix.ssc, 0b10); + assert_eq!(prefix.psc, 0b011); + assert_eq!(prefix.n_ssc(), 3); + assert_eq!(n, 5); + } + + #[test] + fn decode_subsubframe_count_at_non_byte_aligned() { + // Three filler bits, then SSC=0b11, PSC=0b101. + // bit 0..=2 = 0b111 + // bit 3..=4 = SSC = 0b11 + // bit 5..=7 = PSC = 0b101 + // → 0b1111_1101 = 0xFD + let stream = [0xFD]; + let (prefix, n) = decode_subsubframe_count_at(&stream, 3).unwrap(); + assert_eq!(prefix.ssc, 0b11); + assert_eq!(prefix.psc, 0b101); + assert_eq!(prefix.n_ssc(), 4); + assert_eq!(prefix.partial_sample_count(), Some(5)); + assert_eq!(n, 5); + } + + #[test] + fn decode_subsubframe_count_at_crosses_byte_boundary() { + // Place the 5-bit prefix straddling the byte boundary, + // starting at bit-offset 5 within byte 0: + // byte 0: 0b0000_0010 = 0x02 → bit 5..=7 = 0b010, + // so SSC MSB=0, SSC LSB=1, + // PSC MSB=0 (bit 7). + // Actually: bit 5..=6 = SSC (=0b01), bit 7 = top bit of PSC. + // We pick SSC=0b01 and PSC=0b001 ⇒ bit 7 of byte 0 = 0, + // bit 0..=1 of byte 1 = 0b01. + // byte 0 bits = 0b0000_0010 = 0x02 (SSC LSB at bit 6, + // PSC top bit at bit 7 = 0) + // byte 1 bits = 0b0100_0000 = 0x40 (PSC mid bit at bit 0 = 0, + // PSC LSB at bit 1 = 1) + // Wait: redo it. At bit-offset 5 (MSB-first counting from + // byte 0 bit 7 = position 0), bits read are: + // pos 5 → byte 0 bit 2 = SSC[1] (MSB of SSC) + // pos 6 → byte 0 bit 1 = SSC[0] (LSB of SSC) + // pos 7 → byte 0 bit 0 = PSC[2] (MSB of PSC) + // pos 8 → byte 1 bit 7 = PSC[1] + // pos 9 → byte 1 bit 6 = PSC[0] (LSB of PSC) + // For SSC=0b01 and PSC=0b001: + // byte 0 bit 2 = 0, byte 0 bit 1 = 1, byte 0 bit 0 = 0 + // → byte 0 = 0b0000_0010 = 0x02 + // byte 1 bit 7 = 0, byte 1 bit 6 = 1 + // → byte 1 = 0b0100_0000 = 0x40 + let stream = [0x02, 0x40]; + let (prefix, n) = decode_subsubframe_count_at(&stream, 5).unwrap(); + assert_eq!(prefix.ssc, 0b01); + assert_eq!(prefix.psc, 0b001); + assert_eq!(prefix.n_ssc(), 2); + assert_eq!(n, 5); + } + + #[test] + fn decode_subsubframe_count_at_reports_eof_when_buffer_short() { + // Only 4 bits left after `bit_offset` → EOF before the + // full 5-bit prefix can be consumed. + let stream = [0x00]; + let err = decode_subsubframe_count_at(&stream, 4).unwrap_err(); + assert!(matches!(err, Error::UnexpectedEof)); + } + + #[test] + fn decode_subsubframe_count_at_covers_every_ssc_psc_pair() { + // Exhaustively pack each of the 4 * 8 = 32 (SSC, PSC) + // combinations into a single byte at bit-offset 0 + // (SSC occupies bits 0..=1, PSC occupies bits 2..=4, low + // 3 bits are zero padding), then decode and check. + for ssc in 0u8..=3 { + for psc in 0u8..=7 { + let byte = (ssc << 6) | (psc << 3); + let stream = [byte]; + let (prefix, n) = decode_subsubframe_count_at(&stream, 0).unwrap(); + assert_eq!(prefix.ssc, ssc); + assert_eq!(prefix.psc, psc); + assert_eq!(n, 5); + assert_eq!(prefix.n_ssc(), ssc + 1); + assert_eq!( + prefix.samples_per_subsubframe_normal(), + 8 * (ssc as usize + 1) + ); + assert_eq!(prefix.is_termination_tail(), psc != 0); + } + } + } + + #[test] + fn decode_join_scale_huffman_zero_symbol_is_unity() { + // The SA129 Huffman table (A5) codes symbol 0 as a 1-bit `0`. + // After the +64 bias that lands on the §D.3 unity entry (1.0). + let stream = pack_codes(&[(0, 1)]); + let (factor, biased, bits) = + decode_join_scale_at(&stream, 0, ScalesCodebook::Sa129).unwrap(); + assert_eq!(biased, 64); + assert_eq!(factor, 1.0); + assert_eq!(bits, 1); + } + + #[test] + fn decode_join_scale_huffman_walks_every_a5_symbol() { + // Each SA129 (A5) symbol biases by +64 and must land on a valid + // §D.3 entry (all of {-2,-1,0,1,2} + 64 are inside 0..=128). + for &(symbol, len, code) in TABLE_A5 { + let stream = pack_codes(&[(code, len)]); + let (factor, biased, _) = + decode_join_scale_at(&stream, 0, ScalesCodebook::Sa129).unwrap(); + assert_eq!(biased, symbol as i32 + 64); + assert_eq!( + factor, + crate::JOIN_SCALE_FACTOR[(symbol as i32 + 64) as usize] + ); + } + } + + #[test] + fn decode_join_scale_linear6_reads_raw_index() { + // Linear-6-bit: a raw 6-bit absolute index. Index 0 biases to + // 64 (unity); index 63 biases to 127 (a high §D.3 factor). + let stream = pack_codes(&[(0, 6)]); + let (factor, biased, bits) = + decode_join_scale_at(&stream, 0, ScalesCodebook::Linear6Bit).unwrap(); + assert_eq!(biased, 64); + assert_eq!(factor, 1.0); + assert_eq!(bits, 6); + + let stream = pack_codes(&[(63, 6)]); + let (factor, biased, _) = + decode_join_scale_at(&stream, 0, ScalesCodebook::Linear6Bit).unwrap(); + assert_eq!(biased, 127); + assert_eq!(factor, crate::JOIN_SCALE_FACTOR[127]); + } + + #[test] + fn decode_join_scale_linear7_out_of_range_rejected() { + // Linear-7-bit index 65 biases to 129, which is outside the + // §D.3 table (0..=128); the decode must reject it rather than + // index out of bounds. + let stream = pack_codes(&[(65, 7)]); + let err = decode_join_scale_at(&stream, 0, ScalesCodebook::Linear7Bit).unwrap_err(); + assert!(matches!( + err, + Error::InvalidSideInfo { + field: "JOIN_SCALES", + value: 129 + } + )); + } +} diff --git a/crates/vendor/oxideav-dts/src/step_size.rs b/crates/vendor/oxideav-dts/src/step_size.rs new file mode 100644 index 00000000..a6468d6b --- /dev/null +++ b/crates/vendor/oxideav-dts/src/step_size.rs @@ -0,0 +1,582 @@ +//! DTS Coherent Acoustics — §D.2 quantization step-size tables and the +//! §5.5 inverse-quantization scale composition. +//! +//! Round 293 (2026-06-14) lands the dequantization bridge between the +//! quantization-index decode (the §C.2.1 block-code / Annex D Huffman +//! `AUDIO[m]` indices) and the §C.2.2 inverse-ADPCM / §C.2.5 QMF +//! synthesis inputs: the per-subband real scale factor `rScale` and +//! the `aSample[m] = rScale · AUDIO[m]` sample scaling that the §5.5 +//! "Primary Audio Data Arrays" `Audio Data` block (Table 5-29) +//! applies once per subsubframe. +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), staged PDF at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. +//! +//! Two clauses combine here: +//! +//! * **Annex D §D.2 "Quantization Step Size"** (staged PDF p.193-194) +//! gives two 32-entry tables indexed by the `ABITS` bit-allocation +//! index — §D.2.1 *Lossy Quantization* and §D.2.2 *Lossless +//! Quantization* — each entry tabulated as the integer +//! `Step-size × 2²²`. The §5.5 pseudocode selects between them by +//! the frame-header `RATE` field: +//! +//! ```text +//! if (RATE == 0x1f) pStepSizeTable = &StepSizeLossLess; // Lossless +//! else pStepSizeTable = &StepSizeLossy; // Lossy +//! ``` +//! +//! * **§5.5 Table 5-29 `Audio Data`** (staged PDF p.31-32) composes +//! the looked-up `rStepSize` with the §D.1.1 / §D.1.2 RMS +//! square-root `SCALES[ch][n][0..2]` factor into the per-sample +//! real multiplier, transient-aware: +//! +//! ```text +//! pStepSizeTable->LookUp(nABITS, rStepSize); +//! nTmode = TMODE[ch][n]; +//! if (nTmode == 0) nTmode = nSSC; // No transient +//! if (nSubSubFrame < nTmode) // Pre-transient +//! rScale = rStepSize * SCALES[ch][n][0]; // First scale factor +//! else // After-transient +//! rScale = rStepSize * SCALES[ch][n][1]; // Second scale factor +//! rScale *= arADJ[ch][SEL[ch][nABITS-1]]; // 1 unless Huffman +//! for (m=0; m<8; m++, nSample++) +//! aPrmCh[ch].aSubband[n].aSample[nSample] = rScale * AUDIO[m]; +//! ``` +//! +//! This module exposes the two §D.2 tables verbatim, real-valued +//! step-size accessors that divide out the §D.2 `× 2²²` scaling, the +//! §5.5 transient-aware `rScale` composition, and the eight-sample +//! `rScale · AUDIO[m]` scaling. The `arADJ[][]` adjustment multiplier +//! (the round-241 [`crate::ScaleFactorAdjustment`]) is a caller-passed +//! factor — the §5.5 pseudocode comments it is "assumed 1 unless +//! changed by bit stream when SEL indicates Huffman code", so callers +//! that did not read an `ADJ` field pass `1.0`. +//! +//! # Scope and follow-ups +//! +//! The §C.2.1 block-code / Huffman decode that *produces* `AUDIO[m]` +//! is already landed ([`crate::decode_block_code`] and the +//! [`crate::side_info`] Huffman decoders); the §D.2 step-size table +//! selection by `RATE` is exposed here as +//! [`StepSizeTable::for_rate`]. The §C.2.2 inverse-ADPCM step that the +//! §5.5 pseudocode runs *after* this scaling (when `PMODE != 0`) is +//! the separate round-228 [`crate::inverse_adpcm_decode_f64`]; this +//! module stops at the `rScale · AUDIO[m]` product, leaving the +//! ADPCM pass and the DSYNC trailer to the per-subsubframe driver +//! that composes them. + +use crate::side_info::ScaleFactorAdjustment; +use crate::subframe::ChannelSideInfo; +use crate::{Error, Result}; + +/// Number of `ABITS` bit-allocation indices the §D.2 step-size tables +/// tabulate (PDF p.193-194: indices `0..=31`, of which `27..=31` are +/// written "invalid"). +pub const STEP_SIZE_TABLE_LEN: usize = 32; + +/// The `× 2²²` fixed-point scaling the §D.2 tables apply to every +/// tabulated step size (PDF p.193-194 column header +/// "Step-size × 2²²"). A real step size is recovered by +/// `entry as f64 / 2f64.powi(STEP_SIZE_SCALE_SHIFT)`. +pub const STEP_SIZE_SCALE_SHIFT: i32 = 22; + +/// Sentinel placed in the `27..=31` slots the §D.2 tables write as +/// "invalid". Reading these indices through a real-valued accessor +/// surfaces [`Error::InvalidStepSize`] rather than returning `0.0`, +/// so structurally-corrupt `ABITS` values fail loudly. +const STEP_SIZE_INVALID: u32 = 0; + +/// First `ABITS` index the §D.2 tables mark "invalid" (PDF p.193-194). +/// Indices `0..STEP_SIZE_FIRST_INVALID` carry a defined step size; +/// indices `STEP_SIZE_FIRST_INVALID..32` are reserved. +pub const STEP_SIZE_FIRST_INVALID: usize = 27; + +// --------------------------------------------------------------- +// Annex D §D.2.1 — Lossy Quantization (staged PDF p.193). +// Each entry is `Step-size × 2²²`. Indices 27..=31 are "invalid". +// Transcribed verbatim from the staged PDF. +// --------------------------------------------------------------- + +/// §D.2.1 lossy-quantization step sizes, tabulated as +/// `Step-size × 2²²` and indexed by the `ABITS` bit-allocation index. +/// Selected when the frame-header `RATE != 0x1f` (§5.5). Slots +/// `27..=31` hold a zero sentinel because the PDF writes them +/// "invalid". +pub const STEP_SIZE_LOSSY: [u32; STEP_SIZE_TABLE_LEN] = [ + 0, // ABITS 0 — "0,0" nominal: no bits allocated. + 6710886, + 4194304, + 3355443, + 2474639, + 2097152, + 1761608, + 1426063, + 796918, + 461373, + 251658, + 146801, + 79692, + 46137, + 27263, + 16777, + 10486, + 5872, + 3355, + 1887, + 1258, + 713, + 336, + 168, + 84, + 42, + 21, + STEP_SIZE_INVALID, // 27 invalid + STEP_SIZE_INVALID, // 28 invalid + STEP_SIZE_INVALID, // 29 invalid + STEP_SIZE_INVALID, // 30 invalid + STEP_SIZE_INVALID, // 31 invalid +]; + +// --------------------------------------------------------------- +// Annex D §D.2.2 — Lossless Quantization (staged PDF p.194). +// Each entry is `Step-size × 2²²`. Indices 27..=31 are "invalid". +// Transcribed verbatim from the staged PDF. +// --------------------------------------------------------------- + +/// §D.2.2 lossless-quantization step sizes, tabulated as +/// `Step-size × 2²²` and indexed by the `ABITS` bit-allocation index. +/// Selected when the frame-header `RATE == 0x1f` (§5.5). Slots +/// `27..=31` hold a zero sentinel because the PDF writes them +/// "invalid". +pub const STEP_SIZE_LOSSLESS: [u32; STEP_SIZE_TABLE_LEN] = [ + 0, // ABITS 0 — "0,0" nominal: no bits allocated. + 4194304, + 2097152, + 1384120, + 1048576, + 696254, + 524288, + 348127, + 262144, + 131072, + 65431, + 33026, + 16450, + 8208, + 4100, + 2049, + 1024, + 512, + 256, + 128, + 64, + 32, + 16, + 8, + 4, + 2, + 1, + STEP_SIZE_INVALID, // 27 invalid + STEP_SIZE_INVALID, // 28 invalid + STEP_SIZE_INVALID, // 29 invalid + STEP_SIZE_INVALID, // 30 invalid + STEP_SIZE_INVALID, // 31 invalid +]; + +/// Which §D.2 step-size table the §5.5 `RATE` test selects. +/// +/// ```text +/// if (RATE == 0x1f) pStepSizeTable = &StepSizeLossLess; // Lossless +/// else pStepSizeTable = &StepSizeLossy; // Lossy +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StepSizeTable { + /// §D.2.1 lossy quantization ([`STEP_SIZE_LOSSY`]) — `RATE != 0x1f`. + Lossy, + /// §D.2.2 lossless quantization ([`STEP_SIZE_LOSSLESS`]) — + /// `RATE == 0x1f`. + Lossless, +} + +/// The §5.5 lossless-quantization `RATE` sentinel (`0x1f`). +pub const RATE_LOSSLESS: u8 = 0x1f; + +impl StepSizeTable { + /// Resolve the §5.5 `RATE`-driven table selection: `RATE == 0x1f` + /// (= [`RATE_LOSSLESS`]) selects [`StepSizeTable::Lossless`], every + /// other `RATE` value selects [`StepSizeTable::Lossy`]. + #[must_use] + pub fn for_rate(rate: u8) -> Self { + if rate == RATE_LOSSLESS { + StepSizeTable::Lossless + } else { + StepSizeTable::Lossy + } + } + + /// The raw `Step-size × 2²²` integer table this selection points + /// at (§D.2.1 or §D.2.2). + #[must_use] + pub fn raw_table(self) -> &'static [u32; STEP_SIZE_TABLE_LEN] { + match self { + StepSizeTable::Lossy => &STEP_SIZE_LOSSY, + StepSizeTable::Lossless => &STEP_SIZE_LOSSLESS, + } + } + + /// Look up the real-valued step size for an `ABITS` index, undoing + /// the §D.2 `× 2²²` fixed-point scaling. + /// + /// Returns [`Error::InvalidStepSize`] when `abits` is one of the + /// `27..=31` indices the §D.2 tables write "invalid", or when + /// `abits >= 32`. + pub fn step_size(self, abits: u8) -> Result { + let idx = abits as usize; + if idx >= STEP_SIZE_TABLE_LEN || idx >= STEP_SIZE_FIRST_INVALID { + return Err(Error::InvalidStepSize { abits }); + } + let raw = self.raw_table()[idx]; + Ok(f64::from(raw) / 2f64.powi(STEP_SIZE_SCALE_SHIFT)) + } +} + +/// The §5.5 transient-aware scale-factor selection: given the +/// per-subband `TMODE` value, the subframe's `nSSC` subsubframe count, +/// and the current `nSubSubFrame` index, decide which of the two +/// `SCALES[ch][n][0..2]` factors the §5.5 pseudocode multiplies in. +/// +/// ```text +/// nTmode = TMODE[ch][n]; +/// if (nTmode == 0) nTmode = nSSC; // No transient +/// if (nSubSubFrame < nTmode) idx = 0; // Pre-transient: first factor +/// else idx = 1; // After-transient: second factor +/// ``` +/// +/// Returns `0` to select `SCALES[ch][n][0]` (pre-transient / no +/// transient) or `1` to select `SCALES[ch][n][1]` (post-transient). +#[must_use] +pub fn transient_scale_index(tmode: u8, n_ssc: usize, subsubframe: usize) -> usize { + let n_tmode = if tmode == 0 { + n_ssc + } else { + usize::from(tmode) + }; + if subsubframe < n_tmode { + 0 + } else { + 1 + } +} + +/// Compose the §5.5 per-subband real scale `rScale` for one +/// subsubframe: +/// +/// ```text +/// rScale = rStepSize * SCALES[ch][n][transient-selected]; +/// rScale *= arADJ[ch][SEL[ch][nABITS-1]]; // adjustment multiplier +/// ``` +/// +/// * `table` is the §5.5 `RATE`-selected step-size table. +/// * `abits` is `ABITS[ch][n]`; the matching §D.2 step size is looked +/// up internally. +/// * `scale` is the §D.1.1 / §D.1.2 RMS square-root value already +/// resolved for `SCALES[ch][n][transient-selected]` — the §5.5 +/// pseudocode's `(real)SCALES[ch][n][…]` cast (passed as the raw +/// integer the RMS table returns). +/// * `adj` is the round-241 [`ScaleFactorAdjustment`] multiplier; pass +/// [`ScaleFactorAdjustment::Adj0`] when no `ADJ` field was read +/// (the §5.5 default of `arADJ == 1`). +/// +/// Returns [`Error::InvalidStepSize`] when `abits` is an invalid §D.2 +/// index. +pub fn dequant_scale( + table: StepSizeTable, + abits: u8, + scale: u32, + adj: ScaleFactorAdjustment, +) -> Result { + let step = table.step_size(abits)?; + Ok(step * f64::from(scale) * adj.multiplier_f64()) +} + +/// Apply the §5.5 `aSample[m] = rScale · AUDIO[m]` scaling to one +/// subsubframe's eight quantization indices, writing the resulting +/// subband samples into `out`. +/// +/// `audio` carries the eight §C.2.1 / Huffman-decoded `AUDIO[m]` +/// quantization indices for the current `(ch, n, subsubframe)`; `out` +/// receives the eight scaled subband samples. Both slices must hold +/// exactly [`SAMPLES_PER_SUBSUBFRAME`] entries, matching the §5.5 +/// `for (m=0; m<8; m++)` loop. +/// +/// Returns [`Error::SampleCountMismatch`] when either slice is not +/// exactly eight samples long. +pub fn scale_subsubframe_samples(audio: &[i32], r_scale: f64, out: &mut [f64]) -> Result<()> { + if audio.len() != SAMPLES_PER_SUBSUBFRAME || out.len() != SAMPLES_PER_SUBSUBFRAME { + return Err(Error::SampleCountMismatch { + expected: SAMPLES_PER_SUBSUBFRAME, + found: audio.len().max(out.len()), + }); + } + for (dst, &index) in out.iter_mut().zip(audio.iter()) { + *dst = r_scale * f64::from(index); + } + Ok(()) +} + +/// Number of subband samples in one subsubframe (one subband analysis +/// subwindow): the §5.5 / §C.1 fixed `8` samples per subsubframe ("A +/// subsubframe consists of eight subband samples … for each +/// subband", PDF p.181). +pub const SAMPLES_PER_SUBSUBFRAME: usize = 8; + +/// End-to-end §5.5 dequantization of one `(ch, n, subsubframe)` +/// subsubframe: resolve the transient-aware scale index, look up the +/// matching `SCALES[ch][n][…]` factor from the side-info, compose the +/// §5.5 `rScale`, and write the eight `rScale · AUDIO[m]` subband +/// samples into `out`. +/// +/// This composes [`transient_scale_index`], [`dequant_scale`], and +/// [`scale_subsubframe_samples`] against the round-281 +/// [`ChannelSideInfo`] plane: `abits = side.abits[n]`, +/// `tmode = side.tmode[n]`, and the scale factor is +/// `side.scales[n][transient-selected]`. +/// +/// * `side` is the decoded §5.4.1 side information for channel `ch`. +/// * `n` is the subband index (`0..n_vqsub`; high-frequency VQ +/// subbands take the separate §5.5 VQ path, not this one). +/// * `n_ssc` is the subframe's subsubframe count (`SSC + 1`). +/// * `subsubframe` is the current subsubframe index (`0..n_ssc`). +/// * `table` is the §5.5 `RATE`-selected step-size table. +/// * `adj` is the §5.5 `arADJ` multiplier (unity by default). +/// * `audio` is the eight decoded `AUDIO[m]` quantization indices. +/// * `out` receives the eight scaled subband samples. +/// +/// Returns [`Error::InvalidStepSize`] for an invalid `ABITS`, or +/// [`Error::SampleCountMismatch`] when a slice is not eight samples. +#[allow(clippy::too_many_arguments)] +pub fn dequant_subsubframe( + side: &ChannelSideInfo, + n: usize, + n_ssc: usize, + subsubframe: usize, + table: StepSizeTable, + adj: ScaleFactorAdjustment, + audio: &[i32], + out: &mut [f64], +) -> Result<()> { + let abits = side.abits[n]; + let scale_idx = transient_scale_index(side.tmode[n], n_ssc, subsubframe); + let scale = side.scales[n][scale_idx]; + let r_scale = dequant_scale(table, abits, scale, adj)?; + scale_subsubframe_samples(audio, r_scale, out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rate_selects_table() { + assert_eq!(StepSizeTable::for_rate(0x1f), StepSizeTable::Lossless); + assert_eq!(StepSizeTable::for_rate(0x00), StepSizeTable::Lossy); + assert_eq!(StepSizeTable::for_rate(0x1e), StepSizeTable::Lossy); + assert_eq!(StepSizeTable::for_rate(0x0a), StepSizeTable::Lossy); + } + + #[test] + fn tables_have_32_entries() { + assert_eq!(STEP_SIZE_LOSSY.len(), STEP_SIZE_TABLE_LEN); + assert_eq!(STEP_SIZE_LOSSLESS.len(), STEP_SIZE_TABLE_LEN); + } + + #[test] + fn abits_zero_is_zero_step() { + // PDF p.193-194 both list ABITS 0 -> step-size 0,0. + assert_eq!(StepSizeTable::Lossy.step_size(0).unwrap(), 0.0); + assert_eq!(StepSizeTable::Lossless.step_size(0).unwrap(), 0.0); + } + + #[test] + fn lossy_step_sizes_match_nominal() { + // Spot-check the §D.2.1 nominal column: entry / 2^22 must land + // near the tabulated "Nominal Step-size". + // ABITS 2 -> 4194304 / 2^22 = 1,0. + assert!((StepSizeTable::Lossy.step_size(2).unwrap() - 1.0).abs() < 1e-9); + // ABITS 5 -> 2097152 / 2^22 = 0,50. + assert!((StepSizeTable::Lossy.step_size(5).unwrap() - 0.5).abs() < 1e-9); + // ABITS 1 -> 6710886 / 2^22 ≈ 1,6 (nominal). + assert!((StepSizeTable::Lossy.step_size(1).unwrap() - 1.6).abs() < 1e-3); + // ABITS 11 -> 146801 / 2^22 ≈ 0,035 (nominal). + assert!((StepSizeTable::Lossy.step_size(11).unwrap() - 0.035).abs() < 1e-4); + } + + #[test] + fn lossless_step_sizes_match_nominal() { + // §D.2.2: ABITS 1 -> 4194304 / 2^22 = 1,0. + assert!((StepSizeTable::Lossless.step_size(1).unwrap() - 1.0).abs() < 1e-9); + // ABITS 4 -> 1048576 / 2^22 = 0,25. + assert!((StepSizeTable::Lossless.step_size(4).unwrap() - 0.25).abs() < 1e-9); + // ABITS 8 -> 262144 / 2^22 = 0,0625. + assert!((StepSizeTable::Lossless.step_size(8).unwrap() - 0.0625).abs() < 1e-9); + // ABITS 26 -> 1 / 2^22 ≈ 2,384e-7 (the smallest defined). + assert!((StepSizeTable::Lossless.step_size(26).unwrap() - 2.384e-7).abs() < 1e-10); + } + + #[test] + fn invalid_abits_indices_error() { + for abits in 27u8..=31 { + assert_eq!( + StepSizeTable::Lossy.step_size(abits), + Err(Error::InvalidStepSize { abits }) + ); + assert_eq!( + StepSizeTable::Lossless.step_size(abits), + Err(Error::InvalidStepSize { abits }) + ); + } + // Out-of-range index past the table also errors. + assert_eq!( + StepSizeTable::Lossy.step_size(40), + Err(Error::InvalidStepSize { abits: 40 }) + ); + } + + #[test] + fn transient_index_no_transient_uses_first_factor() { + // TMODE == 0 -> nTmode = nSSC, so every subsubframe is + // pre-transient -> factor 0. + for ssf in 0..4 { + assert_eq!(transient_scale_index(0, 4, ssf), 0); + } + } + + #[test] + fn transient_index_splits_at_tmode() { + // TMODE = 2 -> nTmode = 2: subsubframes 0,1 pre (factor 0), + // 2,3 post (factor 1). (Spec: transition in subsubframe + // TMODE+1, i.e. index 2.) + assert_eq!(transient_scale_index(2, 4, 0), 0); + assert_eq!(transient_scale_index(2, 4, 1), 0); + assert_eq!(transient_scale_index(2, 4, 2), 1); + assert_eq!(transient_scale_index(2, 4, 3), 1); + } + + #[test] + fn dequant_scale_composes_step_scale_adj() { + // Lossy ABITS 2 step = 1.0; SCALES = 200; ADJ unity -> + // rScale = 1.0 * 200 * 1.0 = 200. + let r = dequant_scale(StepSizeTable::Lossy, 2, 200, ScaleFactorAdjustment::Adj0).unwrap(); + assert!((r - 200.0).abs() < 1e-9); + // ABITS 5 step = 0.5; SCALES = 10 -> rScale = 5.0. + let r = dequant_scale(StepSizeTable::Lossy, 5, 10, ScaleFactorAdjustment::Adj0).unwrap(); + assert!((r - 5.0).abs() < 1e-9); + } + + #[test] + fn scale_samples_applies_product() { + let audio = [1, -1, 2, -2, 4, -4, 8, -8]; + let mut out = [0.0f64; SAMPLES_PER_SUBSUBFRAME]; + scale_subsubframe_samples(&audio, 3.0, &mut out).unwrap(); + let expected = [3.0, -3.0, 6.0, -6.0, 12.0, -12.0, 24.0, -24.0]; + for (got, want) in out.iter().zip(expected.iter()) { + assert!((got - want).abs() < 1e-9); + } + } + + #[test] + fn scale_samples_zero_scale_zeros_all() { + // ABITS 0 produces a zero step size -> zero rScale -> all + // samples zero regardless of AUDIO[m]. + let audio = [100, -100, 50, -50, 25, -25, 12, -12]; + let mut out = [9.9f64; SAMPLES_PER_SUBSUBFRAME]; + let r = dequant_scale(StepSizeTable::Lossy, 0, 500, ScaleFactorAdjustment::Adj0).unwrap(); + assert_eq!(r, 0.0); + scale_subsubframe_samples(&audio, r, &mut out).unwrap(); + assert!(out.iter().all(|&v| v == 0.0)); + } + + #[test] + fn scale_samples_length_mismatch_errors() { + let audio = [1, 2, 3]; + let mut out = [0.0f64; SAMPLES_PER_SUBSUBFRAME]; + assert!(matches!( + scale_subsubframe_samples(&audio, 1.0, &mut out), + Err(Error::SampleCountMismatch { .. }) + )); + let audio8 = [0i32; SAMPLES_PER_SUBSUBFRAME]; + let mut out_short = [0.0f64; 4]; + assert!(matches!( + scale_subsubframe_samples(&audio8, 1.0, &mut out_short), + Err(Error::SampleCountMismatch { .. }) + )); + } + + #[test] + fn dequant_subsubframe_end_to_end() { + // Build a side-info plane with one active subband. + let mut side = ChannelSideInfo::cleared(); + side.abits[0] = 2; // lossy step = 1.0 + side.tmode[0] = 0; // no transient -> factor 0 + side.scales[0][0] = 10; + side.scales[0][1] = 999; // must be ignored (no transient) + + let audio = [1, 2, 3, 4, 5, 6, 7, 8]; + let mut out = [0.0f64; SAMPLES_PER_SUBSUBFRAME]; + dequant_subsubframe( + &side, + 0, + 4, + 0, + StepSizeTable::Lossy, + ScaleFactorAdjustment::Adj0, + &audio, + &mut out, + ) + .unwrap(); + // rScale = 1.0 * 10 * 1.0 = 10 -> samples = 10*AUDIO[m]. + for (i, &v) in out.iter().enumerate() { + assert!((v - 10.0 * (i as f64 + 1.0)).abs() < 1e-9); + } + } + + #[test] + fn dequant_subsubframe_transient_picks_second_factor() { + let mut side = ChannelSideInfo::cleared(); + side.abits[0] = 5; // lossy step = 0.5 + side.tmode[0] = 2; // transient -> nTmode = 2 + side.scales[0][0] = 4; // pre-transient factor + side.scales[0][1] = 8; // post-transient factor + + let audio = [2i32; SAMPLES_PER_SUBSUBFRAME]; + // Pre-transient subsubframe 0 -> factor 0 (4): rScale = 0.5*4 = 2. + let mut out = [0.0f64; SAMPLES_PER_SUBSUBFRAME]; + dequant_subsubframe( + &side, + 0, + 4, + 0, + StepSizeTable::Lossy, + ScaleFactorAdjustment::Adj0, + &audio, + &mut out, + ) + .unwrap(); + assert!(out.iter().all(|&v| (v - 4.0).abs() < 1e-9)); // 2 * 2.0 + + // Post-transient subsubframe 3 -> factor 1 (8): rScale = 0.5*8 = 4. + dequant_subsubframe( + &side, + 0, + 4, + 3, + StepSizeTable::Lossy, + ScaleFactorAdjustment::Adj0, + &audio, + &mut out, + ) + .unwrap(); + assert!(out.iter().all(|&v| (v - 8.0).abs() < 1e-9)); // 2 * 4.0 + } +} diff --git a/crates/vendor/oxideav-dts/src/subframe.rs b/crates/vendor/oxideav-dts/src/subframe.rs new file mode 100644 index 00000000..152061db --- /dev/null +++ b/crates/vendor/oxideav-dts/src/subframe.rs @@ -0,0 +1,985 @@ +//! DTS Coherent Acoustics — §5.4.1 Primary Audio Coding Side +//! Information subframe walker (ETSI TS 102 114 V1.3.1, Table 5-28). +//! +//! Round 281 (2026-06-12) composes the previously-landed single-field +//! primitives — the round-249 `SSC`/`PSC` prefix, the round-195 +//! ABITS / SCALES decoders, and the new round-281 TMODE decoder — +//! into the §5.4.1 side-information decode walk: one call consumes +//! the SSC/PSC prefix, the PMODE plane, the PVQ indices, the ABITS +//! plane, the TMODE plane, and the SCALES plane (including the +//! transient second scale factor and the high-frequency-VQ-subband +//! tail loop) for every primary audio channel, in the exact field +//! order Table 5-28 fixes (staged PDF p.28-29, +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`). +//! +//! # Inputs +//! +//! The per-channel loop bounds and codebook selectors come from the +//! §5.3.2 Primary Audio Coding Header (Table 5-21, staged PDF +//! p.24-25): `nPCHS = PCHS + 1` (≤ 5 primary channels per p.25), +//! `nSUBS[ch] = SUBS[ch] + 2`, `nVQSUB[ch] = VQSUB[ch] + 1`, and the +//! `BHUFF[ch]` / `THUFF[ch]` / `SHUFF[ch]` codebook selectors. The +//! Table 5-21 header decoder itself is a separate follow-up; this +//! walker takes the resolved values as [`ChannelSideInfoParams`]. +//! +//! # Scope +//! +//! [`decode_primary_side_info_at`] covers Table 5-28 from +//! `SSC = ExtractBits(2)` through the end of the SCALES block (the +//! high-frequency VQ subband loop); the companion +//! [`decode_primary_side_info_tail_at`] handles the `RANGE` (`DYNF`) +//! and `SICRC` (`CPF`) tail that follows. Two pieces remain +//! follow-ups, each blocked on material outside this round: +//! +//! * the ADPCM prediction-coefficient lookup +//! (`ADPCMCoeffVQ.LookUp(nVQIndex, PVQ[ch][n])`) needs the clause +//! D.10.1 vector codebook — the raw 12-bit `nVQIndex` is captured +//! in [`ChannelSideInfo::pvq_index`] so the lookup can be applied +//! later without re-walking the bit stream; +//! * the `JOIN_SHUFF` / `JOIN_SCALES` block (transmitted only when +//! `JOINX[ch] > 0`) needs the joint-scale-factor table to resolve +//! the biased index into a multiplier — the tail decoder declines +//! (`Error::InvalidSideInfo { field: "JOINX", .. }`) rather than +//! guess the variable `JOIN_SCALES` bit count. +//! +//! The `RANGE` code the tail decoder captures is 8-bit signed Q2 +//! (`dB = (int8)code × 0.25`, [`crate::dts_dynrng_to_db`] / +//! [`crate::dts_dynrng_to_linear`], per +//! `docs/audio/dts/dts-drc-dynrng.md`); the §5.4.1 pseudocode applies +//! that linear gain to every reconstructed PCM sample *after* QMF +//! synthesis. The `bits_consumed` cursor of the SCALES-block walk +//! points exactly where the JOIN_SHUFF / RANGE / SICRC reads begin. + +use crate::bitreader::BitReader; +use crate::cos_mod::NUM_SUBBAND; +use crate::side_info::{ + decode_abits, decode_scales, decode_tmode, AbitsCodebook, ScalesCodebook, SubsubframeCount, + TmodeCodebook, +}; +use crate::{Error, Result}; + +/// Maximum number of primary audio channels in one core frame, per +/// the §5.3.2 `PCHS` field description (staged PDF p.25): "there are +/// `nPCHS = PCHS+1 ≤ 5` primary audio channels in the current +/// frame". Channels beyond the fifth are extended channels packed in +/// separate extension data arrays, not in the §5.4.1 side-info block. +pub const MAX_PRIMARY_CHANNELS: usize = 5; + +/// Per-channel loop bounds + codebook selectors for the §5.4.1 walk, +/// resolved from the §5.3.2 Primary Audio Coding Header (Table 5-21). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChannelSideInfoParams { + /// `nSUBS[ch] = SUBS[ch] + 2` — the number of active subbands + /// for this channel (Table 5-21 / PDF p.25 "Subband Activity + /// Count"). Must be ≤ [`NUM_SUBBAND`](crate::NUM_SUBBAND) (= 32). + pub n_subs: usize, + /// `nVQSUB[ch] = VQSUB[ch] + 1` — the first high-frequency + /// VQ-encoded subband (PDF p.25 "High Frequency VQ Start + /// Subband"). Subbands `0..n_vqsub` carry ABITS/TMODE/SCALES; + /// subbands `n_vqsub..n_subs` are VQ-encoded and carry only the + /// single SCALES factor of the Table 5-28 "High frequency VQ + /// subbands" loop. Must be ≤ `n_subs`. + pub n_vqsub: usize, + /// `BHUFF[ch]` resolved through Table 5-25 + /// ([`AbitsCodebook::from_bhuff`]). + pub abits_codebook: AbitsCodebook, + /// `THUFF[ch]` resolved through Table 5-23 + /// ([`TmodeCodebook::from_thuff`]). + pub tmode_codebook: TmodeCodebook, + /// `SHUFF[ch]` resolved through Table 5-24 + /// ([`ScalesCodebook::from_shuff`]). + pub scales_codebook: ScalesCodebook, +} + +/// Decoded §5.4.1 side information for one primary audio channel. +/// +/// Every plane is a fixed [`NUM_SUBBAND`](crate::NUM_SUBBAND)-slot array so downstream +/// consumers can index by subband without re-checking the per-channel +/// bounds; slots at or beyond the corresponding loop bound keep the +/// all-zero / `None` initial value (matching the spec's explicit +/// "Clear SCALES" / `TMODE[ch][n] = 0` initialisation and the +/// `ABITS = 0` "no bits allocated" convention of Table 5-26). +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct ChannelSideInfo { + /// `PMODE[ch][n]` — 1 when ADPCM prediction is active for + /// subband `n` (PDF p.30). Read for `n < n_subs`; zero beyond. + pub pmode: [u8; NUM_SUBBAND], + /// Raw 12-bit `nVQIndex` into the clause D.10.1 ADPCM + /// prediction-coefficient vector codebook, `Some` exactly for the + /// subbands whose `PMODE` bit is set ("Transmitted only when + /// ADPCM active", Table 5-28). The D.10.1 table lookup itself is + /// a follow-up; the index is preserved so it can be applied + /// without re-walking the bit stream. + pub pvq_index: [Option; NUM_SUBBAND], + /// `ABITS[ch][n]` — the bit-allocation index selecting the + /// mid-tread linear quantizer for subband `n` (Table 5-26). Read + /// for `n < n_vqsub`; zero (= no bits allocated) beyond. + pub abits: [u8; NUM_SUBBAND], + /// `TMODE[ch][n]` — 0 for no transient; a non-zero value means + /// the transition occurred in subsubframe `TMODE[ch][n] + 1` + /// (PDF p.30). Decoded only when `nSSC > 1`, only for + /// `n < n_vqsub`, and only where `ABITS[ch][n] > 0`; zero + /// everywhere else per the spec's explicit clear. + pub tmode: [u8; NUM_SUBBAND], + /// `SCALES[ch][n][0..2]` — the resolved scale factors (the + /// §D.1.1 / §D.1.2 RMS square-root-table values, not the raw + /// quantisation indexes). `scales[n][0]` is the only factor for + /// non-transient subbands (and the pre-transient factor + /// otherwise); `scales[n][1]` is the post-transient factor, + /// present only where `TMODE[ch][n] > 0`. Slots the spec's + /// "Clear SCALES" initialisation covers but no decode reaches + /// stay `0` — unambiguous, because every documented RMS table + /// value is ≥ 1. + pub scales: [[u32; 2]; NUM_SUBBAND], +} + +impl ChannelSideInfo { + pub(crate) fn cleared() -> Self { + Self { + pmode: [0; NUM_SUBBAND], + pvq_index: [None; NUM_SUBBAND], + abits: [0; NUM_SUBBAND], + tmode: [0; NUM_SUBBAND], + scales: [[0; 2]; NUM_SUBBAND], + } + } +} + +/// Decoded §5.4.1 Primary Audio Coding Side Information block (the +/// SSC/PSC prefix plus one [`ChannelSideInfo`] per primary channel). +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct PrimarySideInfo { + /// The 5-bit `SSC` / `PSC` prefix (round-249 + /// [`SubsubframeCount`]). Its `n_ssc() > 1` value is what gated + /// the TMODE plane during the walk. + pub subsubframe_count: SubsubframeCount, + /// Per-channel planes, in channel order `ch = 0..nPCHS`. + pub channels: Vec, +} + +/// Walk the §5.4.1 Primary Audio Coding Side Information block +/// (Table 5-28, staged PDF p.28-29) from `bytes` starting at +/// `bit_offset` (MSB-first from `bytes[0]`), given one +/// [`ChannelSideInfoParams`] per primary channel (`params.len()` = +/// `nPCHS`). +/// +/// Field order, exactly as Table 5-28 fixes it: +/// +/// 1. `SSC = ExtractBits(2)`, `PSC = ExtractBits(3)`; +/// 2. the PMODE plane — 1 bit per `(ch, n)` for `n < nSUBS[ch]`, +/// all channels before any later field; +/// 3. the PVQ plane — `nVQIndex = ExtractBits(12)` for every +/// `(ch, n)` whose PMODE bit is set; +/// 4. the ABITS plane — `BHUFF[ch]`-codebook decode per `(ch, n)` +/// for `n < nVQSUB[ch]`; +/// 5. the TMODE plane — cleared to zero; when `nSSC > 1`, +/// `THUFF[ch]`-codebook decode per `(ch, n)` for `n < nVQSUB[ch]` +/// where `ABITS[ch][n] > 0`. (The staged listing's outer +/// `for (ch=…)` brace placement re-opens an inner channel loop; +/// the field semantics — clear all channels, then one decode pass +/// per channel — follow the field description on PDF p.30 and the +/// "variable bits" sizing of the single decode pass.) +/// 6. the SCALES plane — per channel: `nScaleSum = 0`, then for +/// `n < nVQSUB[ch]` with `ABITS[ch][n] > 0` one `SHUFF[ch]`- +/// codebook decode (plus a second when `TMODE[ch][n] > 0`), then +/// the "High frequency VQ subbands" loop for +/// `n ∈ [nVQSUB[ch], nSUBS[ch])` — one factor per subband, the +/// running `nScaleSum` accumulator carrying across both loops. +/// +/// Returns `(PrimarySideInfo, bits_consumed)`; the cursor +/// `bit_offset + bits_consumed` is the first bit of the Table 5-28 +/// tail this walker does not cover (`JOIN_SHUFF` onward — see the +/// module docs for the follow-up boundary). +/// +/// # Errors +/// +/// * [`Error::InvalidSideInfo`] with field `"nPCHS"` when +/// `params.len() > 5` (PDF p.25: `nPCHS ≤ 5`), `"nSUBS"` when a +/// channel's `n_subs` exceeds [`NUM_SUBBAND`](crate::NUM_SUBBAND), or `"VQSUB"` when +/// `n_vqsub > n_subs`; +/// * [`Error::InvalidSideInfo`] with field `"SCALES"` when a +/// scale-factor index walks outside its RMS table (per the +/// round-195 single-field decoder); +/// * [`Error::UnexpectedEof`] when the buffer ends mid-walk; +/// * [`Error::HuffmanDecodeFailed`] on a corrupt Huffman prefix. +pub fn decode_primary_side_info_at( + bytes: &[u8], + bit_offset: usize, + params: &[ChannelSideInfoParams], +) -> Result<(PrimarySideInfo, usize)> { + // Validate the per-channel loop bounds before touching the bit + // stream so a bad header surfaces as a typed error, not as a + // mis-aligned walk. + if params.len() > MAX_PRIMARY_CHANNELS { + return Err(Error::InvalidSideInfo { + field: "nPCHS", + value: params.len() as u32, + }); + } + for p in params { + if p.n_subs > NUM_SUBBAND { + return Err(Error::InvalidSideInfo { + field: "nSUBS", + value: p.n_subs as u32, + }); + } + if p.n_vqsub > p.n_subs { + return Err(Error::InvalidSideInfo { + field: "VQSUB", + value: p.n_vqsub as u32, + }); + } + } + + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + + // 1. SSC = ExtractBits(2); nSSC = SSC + 1; PSC = ExtractBits(3). + let ssc = br.read_bits(2)? as u8; + let psc = br.read_bits(3)? as u8; + let subsubframe_count = SubsubframeCount::new(ssc, psc); + + let mut channels: Vec = vec![ChannelSideInfo::cleared(); params.len()]; + + // 2. PMODE plane: + // for (ch=0; ch0) { + // nVQIndex = ExtractBits(12); + // ADPCMCoeffVQ.LookUp(nVQIndex, PVQ[ch][n]) // 4 coefficients + // } + // The D.10.1 coefficient lookup is deferred; the raw index is + // captured (see module docs). + for (ch, p) in params.iter().enumerate() { + for n in 0..p.n_subs { + if channels[ch].pmode[n] > 0 { + channels[ch].pvq_index[n] = Some(br.read_bits(12)? as u16); + } + } + } + + // 4. ABITS plane: + // for (ch…) { nQSelect = BHUFF[ch]; + // for (n=0; nInverseQ(InputFrame, ABITS[ch][n]) } + for (ch, p) in params.iter().enumerate() { + for n in 0..p.n_vqsub { + channels[ch].abits[n] = decode_abits(&mut br, p.abits_codebook)?; + } + } + + // 5. TMODE plane. Already cleared to zero ("TMODE[ch][n] = 0"); + // decoded only when more than one subsubframe is present: + // if (nSSC>1) for (ch…) { nQSelect = THUFF[ch]; + // for (n=0; n 0) // Present only if bits allocated + // QTMODE.ppQ[nQSelect]->InverseQ(InputFrame, TMODE[ch][n]) } + if subsubframe_count.n_ssc() > 1 { + for (ch, p) in params.iter().enumerate() { + for n in 0..p.n_vqsub { + if channels[ch].abits[n] > 0 { + channels[ch].tmode[n] = decode_tmode(&mut br, p.tmode_codebook)?; + } + } + } + } + + // 6. SCALES plane. Per channel: clear (done), reset the running + // accumulator, decode one factor per bit-allocated subband + // (two when a transient splits the subframe), then the high- + // frequency VQ subband tail — the accumulator carries across + // both loops, exactly as Table 5-28's single `nScaleSum` + // variable does. + for (ch, p) in params.iter().enumerate() { + let mut n_scale_sum: i32 = 0; + for n in 0..p.n_vqsub { + if channels[ch].abits[n] > 0 { + let (scale, sum) = decode_scales(&mut br, p.scales_codebook, n_scale_sum)?; + n_scale_sum = sum; + channels[ch].scales[n][0] = scale; + // Two scale factors transmitted if there is a transient. + if channels[ch].tmode[n] > 0 { + let (scale, sum) = decode_scales(&mut br, p.scales_codebook, n_scale_sum)?; + n_scale_sum = sum; + channels[ch].scales[n][1] = scale; + } + } + } + // High frequency VQ subbands: one factor each, no ABITS / + // TMODE gate (no transient is permitted for VQ subbands). + for n in p.n_vqsub..p.n_subs { + let (scale, sum) = decode_scales(&mut br, p.scales_codebook, n_scale_sum)?; + n_scale_sum = sum; + channels[ch].scales[n][0] = scale; + } + } + + let bits_consumed = br.absolute_bit_position() - bit_offset; + Ok(( + PrimarySideInfo { + subsubframe_count, + channels, + }, + bits_consumed, + )) +} + +/// The §5.4.1 Table 5-28 side-information **tail** that follows the +/// SCALES block: the optional `RANGE` (dynamic-range) and `SICRC` +/// (side-info CRC) fields. +/// +/// Produced by [`decode_primary_side_info_tail_at`]. The cursor it +/// reports lands on the first bit of the §5.5 Audio Data region. +#[derive(Debug, Clone, PartialEq, Default)] +#[non_exhaustive] +pub struct SideInfoTail { + /// The 8-bit code of the §5.4.1 `RANGE` dynamic-range field — + /// `Some` iff the frame header's `DYNF != 0`, `None` otherwise. + /// The code is **8-bit signed Q2 two's-complement** + /// (`dB = (int8)code × 0.25`); feed it to + /// [`crate::dts_dynrng_to_linear`] to obtain the linear multiplier + /// the §5.4.1 pseudocode applies to every reconstructed PCM sample + /// **after** QMF synthesis. (Do not use it to raw-index the + /// offset-binary §D.4 presentation table — see + /// [`crate::dts_dynrng_to_db`].) + pub range_index: Option, + /// Whether a 16-bit `SICRC` side-info CRC word was present and + /// skipped (`true` iff the frame header's `CPF == 1`). Per §5.4.1 + /// "the CRC value test shall not be applied", so the word is + /// consumed for framing but not verified. + pub side_info_crc_present: bool, + /// The §5.4.1 `JOIN_SCALES[ch][n]` factors, one entry per primary + /// channel. `join_scales[ch]` is the joint-intensity scale-factor + /// vector for the imported sub-band range + /// `[nSUBS[ch], nSUBS[nSourceCh])` of a jointly-coded destination + /// channel (`JOINX[ch] > 0`); it is **empty** for channels whose + /// `JOINX[ch] == 0` (no joint import). The `k`-th entry corresponds + /// to imported sub-band `nSUBS[ch] + k`. These multiply the + /// sub-band samples copied from the source channel + /// (`JOINX[ch] - 1`) during the §C.2.3 joint-subband + /// reconstruction. + pub join_scales: Vec>, +} + +/// Walk the §5.4.1 Table 5-28 side-information **tail** — the +/// `JOIN_SHUFF` / `JOIN_SCALES` / `RANGE` / `SICRC` fields that follow +/// the SCALES block — starting at `bit_offset` (the cursor +/// [`decode_primary_side_info_at`] leaves a caller at). +/// +/// Field order, exactly as Table 5-28 fixes it (staged PDF p.29): +/// +/// 1. `for (ch) if (JOINX[ch]>0) JOIN_SHUFF[ch] = ExtractBits(3)`; +/// 2. `for (ch) if (JOINX[ch]>0) { … JOIN_SCALES … }` (variable bits); +/// 3. `if (DYNF != 0) { nIndex = ExtractBits(8); RANGEtbl.LookUp(…) }`; +/// 4. `if (CPF == 1) SICRC = ExtractBits(16)`. +/// +/// `joinx` is the §5.3.2 per-channel `JOINX` array (length `nPCHS`); +/// `n_subs` is the per-channel `nSUBS[ch]` active-subband count (same +/// length); `dynf` / `cpf` are the §5.3.1 frame-header `DYNF` / `CPF` +/// flags. +/// +/// # Joint-intensity walk +/// +/// When a channel carries `JOINX[ch] > 0`, this walker decodes the +/// `JOIN_SHUFF[ch]` (3-bit code-book selector) then, for each imported +/// sub-band `n ∈ [nSUBS[ch], nSUBS[nSourceCh])`, one `JOIN_SCALES[ch][n]` +/// via [`crate::decode_join_scale_at`] (the §D.3 `JScaleTbl` look-up of +/// the biased `QSCALES` index). The resolved factors are returned in +/// [`SideInfoTail::join_scales`]; a channel with `JOINX[ch] == 0` +/// contributes an empty vector. +/// +/// `nSourceCh = JOINX[ch] - 1` per §5.4.1; the imported range runs from +/// the destination channel's `nSUBS[ch]` up to (but not including) the +/// source channel's `nSUBS[nSourceCh]`. When the source's `nSUBS` does +/// not exceed the destination's, the range is empty and no +/// `JOIN_SCALES` bits are read for that channel. +/// +/// The `RANGE` (DYNF) and `SICRC` (CPF) fields are both fully specified +/// and handled: `RANGE`'s 8-bit signed-Q2 code is captured (the +/// [`crate::dts_dynrng_to_linear`] gain is applied later, post-QMF) +/// and `SICRC`'s 16 bits are consumed for framing. +/// +/// Returns `(SideInfoTail, bits_consumed)`. +/// +/// # Errors +/// +/// * [`Error::InvalidSideInfo`] with field `"JOIN_SHUFF"` when a +/// channel's selector is the reserved value 7, or `"JOINX"` when a +/// source channel index is out of range, or `"JOIN_SCALES"` when a +/// biased index lands outside the §D.3 table; +/// * [`Error::UnexpectedEof`] when the buffer ends mid-walk. +pub fn decode_primary_side_info_tail_at( + bytes: &[u8], + bit_offset: usize, + joinx: &[u8], + n_subs: &[usize], + dynf: bool, + cpf: bool, +) -> Result<(SideInfoTail, usize)> { + let byte_offset = bit_offset / 8; + let intra_byte = bit_offset % 8; + let mut br = BitReader::from_byte_offset(bytes, byte_offset); + if intra_byte > 0 { + br.read_bits(intra_byte as u32)?; + } + + let n_pchs = joinx.len(); + let mut join_scales: Vec> = vec![Vec::new(); n_pchs]; + + // 1. for (ch) if (JOINX[ch] > 0) JOIN_SHUFF[ch] = ExtractBits(3). + // The selectors are read for every joint channel first (Table + // 5-28 groups all JOIN_SHUFF reads before the JOIN_SCALES loop). + let mut join_codebook: Vec> = vec![None; n_pchs]; + for (ch, &j) in joinx.iter().enumerate() { + if j > 0 { + let shuff = br.read_bits(3)? as u8; + join_codebook[ch] = Some(crate::ScalesCodebook::from_shuff(shuff).map_err(|_| { + Error::InvalidSideInfo { + field: "JOIN_SHUFF", + value: shuff as u32, + } + })?); + } + } + + // 2. for (ch) if (JOINX[ch] > 0) decode JOIN_SCALES[ch][n] for each + // imported sub-band n ∈ [nSUBS[ch], nSUBS[nSourceCh]). + for (ch, &j) in joinx.iter().enumerate() { + let Some(codebook) = join_codebook[ch] else { + continue; + }; + let source_ch = (j - 1) as usize; + // The source channel and both nSUBS bounds must exist. A joint + // reference to a non-existent source channel is a malformed + // header. + if source_ch >= n_subs.len() || ch >= n_subs.len() { + return Err(Error::InvalidSideInfo { + field: "JOINX", + value: j as u32, + }); + } + let n_subs_dst = n_subs[ch]; + let n_subs_src = n_subs[source_ch]; + // The import range runs [nSUBS[ch], nSUBS[nSourceCh]); when the + // source is not wider than the destination it is empty. + if n_subs_src > n_subs_dst { + let mut factors = Vec::with_capacity(n_subs_src - n_subs_dst); + for _ in n_subs_dst..n_subs_src { + let (factor, _biased) = crate::side_info::decode_join_scale(&mut br, codebook)?; + factors.push(factor); + } + join_scales[ch] = factors; + } + } + + // 3. if (DYNF != 0) { nIndex = ExtractBits(8); RANGEtbl.LookUp(…) } + let range_index = if dynf { + Some(br.read_bits(8)? as u8) + } else { + None + }; + + // 4. if (CPF == 1) SICRC = ExtractBits(16). The CRC value test + // shall not be applied (§5.4.1); the 16 bits are consumed for + // framing only. + let side_info_crc_present = cpf; + if cpf { + let _sicrc = br.read_bits(16)?; + } + + let bits_consumed = br.absolute_bit_position() - bit_offset; + Ok(( + SideInfoTail { + range_index, + side_info_crc_present, + join_scales, + }, + bits_consumed, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::side_info::{RMS_6BIT, RMS_7BIT}; + + /// Pack a series of (value, bit_width) fields into a byte stream + /// MSB-first. Trailing bits are zero-padded. + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + fn linear_params(n_subs: usize, n_vqsub: usize) -> ChannelSideInfoParams { + ChannelSideInfoParams { + n_subs, + n_vqsub, + abits_codebook: AbitsCodebook::Linear5Bit, + tmode_codebook: TmodeCodebook::D4, + scales_codebook: ScalesCodebook::Linear6Bit, + } + } + + #[test] + fn single_channel_linear_walk_decodes_every_plane() { + // 1 channel, nSUBS=4, nVQSUB=2, all-linear codebooks, + // nSSC=1 (no TMODE plane), no ADPCM. + let stream = pack_fields(&[ + (0, 2), // SSC = 0 -> nSSC = 1 + (0, 3), // PSC = 0 + (0, 1), + (0, 1), + (0, 1), + (0, 1), // PMODE[0][0..4] = 0 + (3, 5), // ABITS[0][0] = 3 (Linear5Bit) + (0, 5), // ABITS[0][1] = 0 -> no SCALES for n=1 + (10, 6), // SCALES[0][0][0] index 10 (Linear6Bit) + (20, 6), // SCALES[0][2][0] index 20 (HF VQ subband) + (30, 6), // SCALES[0][3][0] index 30 (HF VQ subband) + ]); + let params = [linear_params(4, 2)]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + + assert_eq!(info.subsubframe_count.n_ssc(), 1); + assert_eq!(info.subsubframe_count.psc, 0); + assert_eq!(info.channels.len(), 1); + let ch = &info.channels[0]; + assert_eq!(&ch.pmode[..4], &[0, 0, 0, 0]); + assert!(ch.pvq_index.iter().all(Option::is_none)); + assert_eq!(&ch.abits[..4], &[3, 0, 0, 0]); + assert!(ch.tmode.iter().all(|&t| t == 0)); + assert_eq!(ch.scales[0], [RMS_6BIT[10], 0]); + assert_eq!(ch.scales[1], [0, 0]); // ABITS=0 -> skipped + assert_eq!(ch.scales[2], [RMS_6BIT[20], 0]); + assert_eq!(ch.scales[3], [RMS_6BIT[30], 0]); + // 5 prefix + 4 PMODE + 2*5 ABITS + 3*6 SCALES = 37 bits. + assert_eq!(bits, 37); + } + + #[test] + fn pmode_plane_for_all_channels_precedes_pvq_plane() { + // Two channels; Table 5-28 reads every channel's PMODE bits + // before any PVQ index. ch0: PMODE = [1, 0]; ch1: PMODE = + // [0, 1]. The two 12-bit PVQ indices then follow in channel + // order: 0xABC for ch0/n=0, 0x123 for ch1/n=1. + let stream = pack_fields(&[ + (0, 2), + (0, 3), // SSC/PSC + (0b10, 2), + (0b01, 2), // PMODE ch0 = [1,0], ch1 = [0,1] + (0xABC, 12), + (0x123, 12), // PVQ ch0/n0, ch1/n1 + (0, 5), + (0, 5), + (0, 5), + (0, 5), // ABITS both ch (nVQSUB=2 each) all 0 + // ABITS all-zero -> no TMODE, no coded SCALES; + // nVQSUB == nSUBS -> no HF SCALES either. + ]); + let params = [linear_params(2, 2), linear_params(2, 2)]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + assert_eq!(&info.channels[0].pmode[..2], &[1, 0]); + assert_eq!(&info.channels[1].pmode[..2], &[0, 1]); + assert_eq!(info.channels[0].pvq_index[0], Some(0xABC)); + assert_eq!(info.channels[0].pvq_index[1], None); + assert_eq!(info.channels[1].pvq_index[0], None); + assert_eq!(info.channels[1].pvq_index[1], Some(0x123)); + // 5 + 4 PMODE + 24 PVQ + 20 ABITS = 53 bits. + assert_eq!(bits, 53); + } + + #[test] + fn tmode_decoded_when_multiple_subsubframes_and_bits_allocated() { + // nSSC = 2 (SSC=1) so the TMODE plane is present; one + // channel, nVQSUB = nSUBS = 3, ABITS = [2, 0, 1]: TMODE is + // decoded for n=0 and n=2 only (n=1 has no bits allocated). + // D4 codes equal their symbol, 2 bits each. TMODE[0]=1 means + // a transient -> a second scale factor for n=0. + let stream = pack_fields(&[ + (1, 2), // SSC = 1 -> nSSC = 2 + (0, 3), // PSC = 0 + (0, 3), // PMODE[0][0..3] = 0 + (2, 5), + (0, 5), + (1, 5), // ABITS = [2, 0, 1] + (1, 2), + (0, 2), // TMODE[0]=1 (D4), TMODE[2]=0 + (5, 6), // SCALES[0][0] index 5 (pre-transient) + (7, 6), // SCALES[0][1] index 7 (post-transient) + (40, 6), // SCALES[2][0] index 40 + ]); + let params = [linear_params(3, 3)]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + let ch = &info.channels[0]; + assert_eq!(info.subsubframe_count.n_ssc(), 2); + assert_eq!(&ch.abits[..3], &[2, 0, 1]); + assert_eq!(&ch.tmode[..3], &[1, 0, 0]); + assert_eq!(ch.scales[0], [RMS_6BIT[5], RMS_6BIT[7]]); + assert_eq!(ch.scales[1], [0, 0]); + assert_eq!(ch.scales[2], [RMS_6BIT[40], 0]); + // 5 + 3 PMODE + 15 ABITS + 4 TMODE + 18 SCALES = 45 bits. + assert_eq!(bits, 45); + } + + #[test] + fn tmode_plane_skipped_when_single_subsubframe() { + // Same geometry as above but SSC = 0 -> nSSC = 1: per Table + // 5-28 / PDF p.30 the TMODE plane is not transmitted at all, + // so the SCALES reads start right after ABITS and only one + // scale factor per subband is read. + let stream = pack_fields(&[ + (0, 2), + (0, 3), // SSC = 0 -> nSSC = 1, PSC = 0 + (0, 3), // PMODE + (2, 5), + (0, 5), + (1, 5), // ABITS = [2, 0, 1] + (5, 6), // SCALES[0][0] + (40, 6), // SCALES[2][0] + ]); + let params = [linear_params(3, 3)]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + let ch = &info.channels[0]; + assert!(ch.tmode.iter().all(|&t| t == 0)); + assert_eq!(ch.scales[0], [RMS_6BIT[5], 0]); + assert_eq!(ch.scales[2], [RMS_6BIT[40], 0]); + // 5 + 3 + 15 + 12 = 35 bits. + assert_eq!(bits, 35); + } + + #[test] + fn huffman_walk_accumulates_scale_sum_across_hf_vq_tail() { + // BHUFF=A12 / THUFF=A4 / SHUFF=SA129 (difference codebook + // A5 + 6-bit RMS). One channel, nSUBS=3, nVQSUB=2, nSSC=1. + // A12: symbol 2 = code 10 (2 bits). A5 differences: +2 is + // code 1110 (4 bits), +1 is code 10 (2 bits), 0 is code 0 + // (1 bit). nScaleSum walks 0 -> +2 -> +3 -> +3 across the + // coded subband and the two HF VQ subbands — one running + // accumulator across both loops, per Table 5-28. + let stream = pack_fields(&[ + (0, 2), + (0, 3), // SSC=0, PSC=0 + (0, 3), // PMODE[0..3] = 0 + (0b10, 2), // ABITS[0] = 2 (A12) + (0b10, 2), // ABITS[1] = 2 (A12) + (0b1110, 4), // SCALES[0][0]: diff +2 -> sum 2 + (0b10, 2), // SCALES[1][0]: diff +1 -> sum 3 + (0b0, 1), // SCALES[2][0] (HF): diff 0 -> sum 3 + ]); + let params = [ChannelSideInfoParams { + n_subs: 3, + n_vqsub: 2, + abits_codebook: AbitsCodebook::A12, + tmode_codebook: TmodeCodebook::A4, + scales_codebook: ScalesCodebook::Sa129, + }]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + let ch = &info.channels[0]; + assert_eq!(&ch.abits[..3], &[2, 2, 0]); + assert_eq!(ch.scales[0][0], RMS_6BIT[2]); + assert_eq!(ch.scales[1][0], RMS_6BIT[3]); + assert_eq!(ch.scales[2][0], RMS_6BIT[3]); + assert_eq!(bits, 5 + 3 + 4 + 7); + } + + #[test] + fn scale_sum_accumulator_resets_per_channel() { + // Two channels on the SA129 difference codebook. ch0 + // accumulates to +3; ch1's first difference (+1) must be + // applied to a fresh nScaleSum of 0 (per-channel + // `nScaleSum = 0;` in Table 5-28), not to ch0's +3. + let stream = pack_fields(&[ + (0, 2), + (0, 3), + (0, 1), // PMODE ch0[0] (nSUBS = 1) + (0, 2), // PMODE ch1[0..2] (nSUBS = 2) + (0b10, 2), // ABITS ch0[0] = 2 (A12) + (0b10, 2), // ABITS ch1[0] = 2 (A12) + (0b1110, 4), // ch0 SCALES[0][0]: diff +2 -> sum 2 + (0b10, 2), // ch1 SCALES[0][0]: diff +1 -> fresh sum 1 + (0b10, 2), // ch1 SCALES[1][0] (HF VQ): diff +1 -> sum 2 + ]); + // Layout note: ch0 has nSUBS = nVQSUB = 1 so its SCALES block + // is the single +2 difference (no HF tail); ch1 (nSUBS = 2, + // nVQSUB = 1) has one coded subband followed by one HF VQ + // subband. + let ch0 = ChannelSideInfoParams { + n_subs: 1, + n_vqsub: 1, + abits_codebook: AbitsCodebook::A12, + tmode_codebook: TmodeCodebook::A4, + scales_codebook: ScalesCodebook::Sa129, + }; + let ch1 = ChannelSideInfoParams { + n_subs: 2, + n_vqsub: 1, + ..ch0 + }; + let (info, _) = decode_primary_side_info_at(&stream, 0, &[ch0, ch1]).unwrap(); + // ch0: sum walked 0 -> 2. + assert_eq!(info.channels[0].scales[0][0], RMS_6BIT[2]); + // ch1: fresh accumulator 0 -> +1 -> +2 (not 3 -> 4 -> 5). + assert_eq!(info.channels[1].scales[0][0], RMS_6BIT[1]); + assert_eq!(info.channels[1].scales[1][0], RMS_6BIT[2]); + } + + #[test] + fn linear7_scales_route_through_7bit_rms_table() { + // SHUFF=6 (Linear7Bit) reads 7-bit absolute indexes and + // resolves them through the §D.1.2 7-bit RMS table. + let stream = pack_fields(&[ + (0, 2), + (0, 3), + (0, 1), // PMODE + (1, 5), // ABITS[0] = 1 (Linear5Bit) + (99, 7), // SCALES[0][0] index 99 (7-bit table) + ]); + let params = [ChannelSideInfoParams { + n_subs: 1, + n_vqsub: 1, + abits_codebook: AbitsCodebook::Linear5Bit, + tmode_codebook: TmodeCodebook::D4, + scales_codebook: ScalesCodebook::Linear7Bit, + }]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + assert_eq!(info.channels[0].scales[0][0], RMS_7BIT[99]); + assert_eq!(bits, 5 + 1 + 5 + 7); + } + + #[test] + fn walk_starts_at_arbitrary_bit_offset() { + // Prepend 3 filler bits; the walk must produce the same + // result as the aligned variant and report the same + // bits_consumed. + let aligned = pack_fields(&[(0, 2), (0, 3), (0, 1), (1, 5), (10, 6)]); + let shifted = pack_fields(&[ + (0b101, 3), // filler + (0, 2), + (0, 3), + (0, 1), + (1, 5), + (10, 6), + ]); + let params = [linear_params(1, 1)]; + let (a, bits_a) = decode_primary_side_info_at(&aligned, 0, ¶ms).unwrap(); + let (b, bits_b) = decode_primary_side_info_at(&shifted, 3, ¶ms).unwrap(); + assert_eq!(a, b); + assert_eq!(bits_a, bits_b); + assert_eq!(bits_a, 17); + } + + #[test] + fn empty_channel_list_consumes_only_the_prefix() { + let stream = pack_fields(&[(0b10, 2), (0b011, 3)]); + let (info, bits) = decode_primary_side_info_at(&stream, 0, &[]).unwrap(); + assert_eq!(bits, SubsubframeCount::WIRE_BITS as usize); + assert!(info.channels.is_empty()); + assert_eq!(info.subsubframe_count.n_ssc(), 3); + assert_eq!(info.subsubframe_count.psc, 0b011); + } + + #[test] + fn too_many_channels_rejected_as_npchs() { + // PDF p.25: nPCHS = PCHS + 1 <= 5 primary channels. + let params = vec![linear_params(1, 1); 6]; + assert_eq!( + decode_primary_side_info_at(&[0u8; 64], 0, ¶ms).unwrap_err(), + Error::InvalidSideInfo { + field: "nPCHS", + value: 6 + } + ); + } + + #[test] + fn out_of_range_subband_bounds_rejected() { + // n_subs beyond NumSubband = 32. + assert_eq!( + decode_primary_side_info_at(&[0u8; 64], 0, &[linear_params(33, 1)]).unwrap_err(), + Error::InvalidSideInfo { + field: "nSUBS", + value: 33 + } + ); + // n_vqsub beyond n_subs. + assert_eq!( + decode_primary_side_info_at(&[0u8; 64], 0, &[linear_params(4, 5)]).unwrap_err(), + Error::InvalidSideInfo { + field: "VQSUB", + value: 5 + } + ); + } + + #[test] + fn truncated_stream_surfaces_eof_mid_walk() { + // The single-channel linear walk needs 17 bits; 2 bytes hold + // only 16. + let stream = pack_fields(&[ + (0, 2), + (0, 3), + (0, 1), + (1, 5), // ABITS[0] = 1; SCALES read now needs 6 more bits + (0, 5), // ...but only 5 remain. + ]); + assert_eq!(stream.len(), 2); + assert_eq!( + decode_primary_side_info_at(&stream, 0, &[linear_params(1, 1)]).unwrap_err(), + Error::UnexpectedEof + ); + } + + #[test] + fn full_subband_count_walk_uses_all_32_slots() { + // nSUBS = nVQSUB = 32 (the NumSubband maximum), all-linear, + // every subband bit-allocated: 32 PMODE bits, 32 × 5 ABITS + // bits, 32 × 6 SCALES bits. + let mut fields: Vec<(u32, u8)> = vec![(0, 2), (0, 3)]; + fields.extend(std::iter::repeat_n((0u32, 1u8), 32)); // PMODE + fields.extend(std::iter::repeat_n((1u32, 5u8), 32)); // ABITS = 1 + fields.extend((0..32).map(|n| (n as u32, 6u8))); // SCALES idx n + let stream = pack_fields(&fields); + let params = [linear_params(32, 32)]; + let (info, bits) = decode_primary_side_info_at(&stream, 0, ¶ms).unwrap(); + let ch = &info.channels[0]; + assert!(ch.abits.iter().all(|&a| a == 1)); + for (n, scales) in ch.scales.iter().enumerate() { + assert_eq!(scales[0], RMS_6BIT[n]); + } + assert_eq!(bits, 5 + 32 + 160 + 192); + } + + #[test] + fn tail_empty_when_no_dynf_no_cpf() { + // JOINX all zero, DYNF=0, CPF=0 -> the tail is empty. + let stream = pack_fields(&[(0, 8)]); // any padding + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 0, &[0, 0], &[3, 3], false, false).unwrap(); + assert_eq!(bits, 0); + assert_eq!(tail.range_index, None); + assert!(!tail.side_info_crc_present); + assert!(tail.join_scales.iter().all(Vec::is_empty)); + } + + #[test] + fn tail_captures_range_index_when_dynf() { + // DYNF=1 -> 8-bit RANGE code; CPF=0. Signed-Q2 code 80 = + // +20 dB = 10.0x linear. + let stream = pack_fields(&[(80, 8)]); + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 0, &[0], &[3], true, false).unwrap(); + assert_eq!(bits, 8); + assert_eq!(tail.range_index, Some(80)); + assert!(!tail.side_info_crc_present); + let gain = crate::dts_dynrng_to_linear(tail.range_index.unwrap()); + assert!((gain - 10.0).abs() < 1e-12); + } + + #[test] + fn tail_skips_sicrc_when_cpf() { + // DYNF=0, CPF=1 -> 16-bit SICRC consumed, value not surfaced. + let stream = pack_fields(&[(0xABCD, 16)]); + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 0, &[0], &[3], false, true).unwrap(); + assert_eq!(bits, 16); + assert_eq!(tail.range_index, None); + assert!(tail.side_info_crc_present); + } + + #[test] + fn tail_range_then_sicrc_when_both() { + // DYNF=1 and CPF=1 -> 8-bit RANGE then 16-bit SICRC, in + // order. Signed-Q2 code 0 -> unity (0 dB). + let stream = pack_fields(&[(0, 8), (0x1234, 16)]); + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 0, &[0, 0], &[3, 3], true, true).unwrap(); + assert_eq!(bits, 24); + assert_eq!(tail.range_index, Some(0)); + assert!(tail.side_info_crc_present); + assert_eq!(crate::dts_dynrng_to_linear(0), 1.0); + } + + #[test] + fn tail_decodes_joint_intensity_scales() { + // ch0 is the source (JOINX=0, nSUBS=5); ch1 is jointly coded + // (JOINX=1 -> source ch0, nSUBS=3). The imported range is + // [nSUBS[1]=3, nSUBS[0]=5) = subbands 3 and 4, so ch1 needs a + // JOIN_SHUFF selector and 2 JOIN_SCALES values. + // + // JOIN_SHUFF[1] = 5 (linear-6-bit), then two 6-bit indices: + // index 0 -> biased 64 -> §D.3 unity (1.0), index 63 -> biased + // 127 -> §D.3 entry 127. + let stream = pack_fields(&[ + (5, 3), // JOIN_SHUFF[1] = linear-6-bit + (0, 6), // JOIN_SCALES[1][3] index 0 -> 1.0 + (63, 6), // JOIN_SCALES[1][4] index 63 -> JOIN_SCALE_FACTOR[127] + ]); + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 0, &[0, 1], &[5, 3], false, false).unwrap(); + assert_eq!(bits, 3 + 6 + 6); + assert!(tail.join_scales[0].is_empty()); // source channel: no import + assert_eq!(tail.join_scales[1].len(), 2); + assert_eq!(tail.join_scales[1][0], 1.0); + assert_eq!(tail.join_scales[1][1], crate::JOIN_SCALE_FACTOR[127]); + } + + #[test] + fn tail_rejects_reserved_join_shuff() { + // JOIN_SHUFF = 7 is the reserved/invalid selector. + let stream = pack_fields(&[(7, 3)]); + assert_eq!( + decode_primary_side_info_tail_at(&stream, 0, &[0, 1], &[5, 3], false, false) + .unwrap_err(), + Error::InvalidSideInfo { + field: "JOIN_SHUFF", + value: 7, + } + ); + } + + #[test] + fn tail_joint_empty_range_reads_no_scales() { + // ch1 JOINX=1 but its nSUBS (5) is not exceeded by the source + // ch0 nSUBS (5) -> empty import range -> JOIN_SHUFF is still read + // but no JOIN_SCALES values follow. + let stream = pack_fields(&[(0, 3)]); // JOIN_SHUFF[1] = SA129 + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 0, &[0, 1], &[5, 5], false, false).unwrap(); + assert_eq!(bits, 3); + assert!(tail.join_scales[1].is_empty()); + } + + #[test] + fn tail_honours_nonzero_bit_offset() { + // The tail decoder must respect a non-byte-aligned start cursor + // (the SCALES block rarely ends on a byte boundary). + let stream = pack_fields(&[(0b101, 3), (80, 8)]); // 3-bit prefix, then RANGE + let (tail, bits) = + decode_primary_side_info_tail_at(&stream, 3, &[0], &[3], true, false).unwrap(); + assert_eq!(bits, 8); + assert_eq!(tail.range_index, Some(80)); + } +} diff --git a/crates/vendor/oxideav-dts/src/subframe_pcm.rs b/crates/vendor/oxideav-dts/src/subframe_pcm.rs new file mode 100644 index 00000000..ad012a40 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/subframe_pcm.rs @@ -0,0 +1,2300 @@ +//! DTS Coherent Acoustics — §5.5 + §C.2.5 end-to-end subframe→PCM +//! bridge (ETSI TS 102 114 V1.3.1). +//! +//! Round 346 (2026-06-20) composes the two already-landed halves of the +//! Core reconstruction chain into one per-subframe call: +//! +//! 1. the round-340 §5.5 [`crate::decode_audio_data_subframe_at`] walk, which +//! turns the §5.4.1 side information + the §5.5 `Audio Data` arrays +//! into the per-channel subband-sample matrices +//! `aPrmCh[ch].aSubband[n].aSample[m]`, and +//! 2. the round-330 §C.2.5 [`MultiChannelQmf`] driver, which runs the +//! per-channel `aPrmCh[ch].QMFInterpolation(FILTS, nSUBS[ch])` 32-band +//! synthesis filterbank over those matrices to produce PCM. +//! +//! The bridge is the missing composition step the crate README's "Not +//! yet implemented" tail named first: *"The §5.5 `Audio Data` walker +//! that composes the side-info, dispatch, dequantization, ADPCM, and QMF +//! primitives into reconstructed subband samples — and thus PCM +//! output."* The walker (#1) and the synthesis (#2) both landed in +//! prior rounds; this module is the one-call subframe driver that wires +//! the walker's output directly into the synthesis input. +//! +//! # The per-subframe loop (§5.4 + §5.5 + §C.2.5) +//! +//! For one audio subframe the spec runs (PDF p.28-33, then the §C.2.5 +//! driver per channel): +//! +//! ```text +//! // §5.5 Audio Data: nSSC subsubframes of 8 samples each -> +//! // aPrmCh[ch].aSubband[n].aSample[0 .. nSSC*8] +//! decode_audio_data_subframe_at(...); +//! // §C.2.5 Filter Bank Reconstruction, once per channel: +//! for (ch=0; ch 0`): its last subsubframe carries `PSC < 8` samples per +//! subband, so the subframe yields `((nSSC-1)*8 + PSC) * 32` PCM +//! samples (see [`SubframePcmDecoder::decode_subframe_partial`]). +//! +//! # Persistence across subframes +//! +//! [`SubframePcmDecoder`] owns one persistent [`MultiChannelQmf`] so a +//! caller decoding a frame's subframes (or a stream's frames) in order +//! carries each channel's inter-subframe filter tail (`raX[]` / `raZ[]`) +//! exactly as the §C.2.5 driver requires. Construct it once for the +//! frame's channel count, then call [`SubframePcmDecoder::decode_subframe`] +//! for each subframe. +//! +//! # Scope +//! +//! The walker's §D.10.1 ADPCM-coefficient-VQ (`PMODE != 0`) and §D.10.2 +//! high-frequency-VQ (`nVQSUB < nSUBS`) sub-paths are fully implemented +//! (round 434) and — since round 439 — **enabled by default**: every +//! decoder starts with the real §D.10 books +//! ([`VqCodebooks::builtin`], transcribed from the staged clean-room +//! tables `docs/audio/dts/tables/dts-d10-*.csv`), so those frames +//! reconstruct to PCM end-to-end out of the box (phase-1 HF-VQ fill, +//! §C.2.2 prediction with the persistent [`AdpcmHistory`] and the +//! §5.3.1 `HFLAG` frame gate). A caller may still swap or strip the +//! books ([`SubframePcmDecoder::set_vq_codebooks`]); with +//! [`VqCodebooks::none`] such frames surface the typed +//! [`AudioArrayError::VqCodebookUnavailable`] error before any §5.5 +//! bit is read, exactly as in the pre-round-439 bookless state. +//! +//! Joint-intensity subband coding (`JOINX[ch] > 0`) is not applied here: +//! the §C.2.3 joint-subband decode is landed +//! ([`crate::joint_subband_decode_range_f64`]) but it needs the +//! `JOIN_SCALES[ch][n]` Huffman factors, whose §5.4.x bit-stream decode +//! is not yet wired. [`SubframePcmDecoder::decode_subframe`] therefore +//! surfaces [`SubframePcmError::JointSubbandUnsupported`] when any +//! channel carries `JOINX[ch] > 0`, rather than silently skipping the +//! joint step. + +use crate::audio_array::{ + decode_audio_data_subframe_vq_at, decode_lfe_phase_at, AdpcmContext, AdpcmHistory, + AudioArrayDecodeError, AudioArrayError, HfVqFill, SubbandSampleMatrix, +}; +use crate::audio_header::AudioCodingHeader; +use crate::cos_mod::NUM_SUBBAND; +use crate::d10_vq::{scan_hf_vq_indices_at, VqCodebooks}; +use crate::filter_bank::FilterBankSelection; +use crate::header::{AmodeArrangement, DtsFrameHeader}; +use crate::qmf_multichannel::{MultiChannelQmf, MultiChannelQmfError}; +use crate::step_size::StepSizeTable; +use crate::subframe::{ChannelSideInfo, SideInfoTail}; + +/// One subframe's reconstructed PCM, planar (one `Vec` per +/// channel). Every channel's vec has the same length — `nSSC * 256` +/// samples (`nSSC` subsubframes × 8 samples × 32 PCM samples per +/// subband-sample row), or `((nSSC-1)·8 + PSC) · 32` samples when a +/// termination frame's subframe ends in a §5.4.1 partial subsubframe. +pub type SubframePcm = Vec>; + +/// PCM samples one §C.2.5 subband-sample row expands to (the driver +/// emits 32 PCM samples per row — the `NumSubband` bands of one +/// polyphase output block). +pub const PCM_PER_SUBBAND_ROW: usize = NUM_SUBBAND; + +/// Errors from the §5.5 + §C.2.5 end-to-end subframe→PCM bridge. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum SubframePcmError { + /// The §5.5 [`crate::decode_audio_data_subframe_at`] walk failed: a + /// bit-stream-level error, or an Annex D VQ-codebook blocker + /// (`PMODE != 0` / `nVQSUB < nSUBS`). Carries the underlying + /// [`AudioArrayDecodeError`]. + AudioData(AudioArrayDecodeError), + /// The §C.2.5 [`MultiChannelQmf`] synthesis failed (a length or + /// row-count mismatch between the walker's matrices and the driver's + /// channel count, or a per-channel synthesis error). + Synthesis(MultiChannelQmfError), + /// A channel carried `JOINX[ch] > 0` (joint-intensity subband + /// coding). The §C.2.3 joint-subband decode is landed but its + /// `JOIN_SCALES` Huffman side-info decode is not yet wired, so the + /// bridge declines rather than producing incorrect PCM. Carries the + /// 0-based channel index and the one-based `JOINX` source. + JointSubbandUnsupported { + /// 0-based destination channel carrying `JOINX > 0`. + ch: usize, + /// The one-based `JOINX[ch]` source-channel selector. + joinx: u8, + }, + /// The frame header's `PCMR` source-PCM-resolution code is one of + /// the two reserved values, so the §C.2.5 output `rScale` (the + /// post-filterbank float→PCM full-scale gain derived from `PCMR`) is + /// undefined and no PCM can be produced. Carries the raw `PCMR` + /// index. + ReservedPcmResolution { + /// The raw §5.3.1 Table 5-17 `PCMR` index. + pcmr: u8, + }, + /// The caller-supplied per-channel side-info / loop-bound slices did + /// not all agree on the channel count. Carries the channel count the + /// driver expected (the [`SubframePcmDecoder`]'s configured count) + /// and the mismatching slice length. + ChannelCountMismatch { + /// The driver's configured channel count. + expected: usize, + /// The mismatching supplied slice length. + got: usize, + }, + /// The §C.2.3 joint-intensity sub-band copy could not be applied + /// because the supplied `JOIN_SCALES` factors, source-channel index, + /// or `nSUBS` bounds were structurally inconsistent with the decoded + /// sub-band matrices. Carries the 0-based destination channel. + JointSubbandShape { + /// 0-based destination channel whose joint import failed. + ch: usize, + }, + /// A subframe's §5.4.1 `PSC` (Partial Subsubframe Sample Count) was + /// non-zero but the §5.3.1 frame header's `FTYPE` says **normal** + /// frame. Per PDF p.30 a partial subsubframe "exists only in a + /// termination frame", so a normal frame signalling one is + /// structurally invalid and the decode declines rather than + /// truncating a normal frame's audio. Carries the 0-based subframe + /// index and the offending wire `PSC`. + PartialSubsubframeInNormalFrame { + /// 0-based subframe index whose side info carried `PSC > 0`. + subframe: usize, + /// The offending 3-bit wire `PSC` value (`1..=7`). + psc: u8, + }, +} + +impl core::fmt::Display for SubframePcmError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SubframePcmError::AudioData(e) => write!(f, "audio-data walk failed: {e}"), + SubframePcmError::Synthesis(e) => write!(f, "QMF synthesis failed: {e}"), + SubframePcmError::JointSubbandUnsupported { ch, joinx } => write!( + f, + "channel {ch} carries JOINX={joinx} (joint-intensity subband \ + coding); the JOIN_SCALES side-info decode is not yet wired" + ), + SubframePcmError::ReservedPcmResolution { pcmr } => write!( + f, + "frame header PCMR index {pcmr} is reserved; the output rScale \ + is undefined" + ), + SubframePcmError::ChannelCountMismatch { expected, got } => write!( + f, + "channel-count mismatch: driver expects {expected}, slice carries {got}" + ), + SubframePcmError::JointSubbandShape { ch } => write!( + f, + "channel {ch} joint-intensity sub-band copy failed: JOIN_SCALES / \ + nSUBS bounds inconsistent with the decoded sub-band matrices" + ), + SubframePcmError::PartialSubsubframeInNormalFrame { subframe, psc } => write!( + f, + "subframe {subframe} signals a partial subsubframe (PSC={psc}) \ + but the frame header's FTYPE is normal; a partial subsubframe \ + exists only in a termination frame (§5.4.1, PDF p.30)" + ), + } + } +} + +impl std::error::Error for SubframePcmError {} + +impl From for SubframePcmError { + fn from(e: AudioArrayDecodeError) -> Self { + SubframePcmError::AudioData(e) + } +} + +impl From for SubframePcmError { + fn from(e: MultiChannelQmfError) -> Self { + SubframePcmError::Synthesis(e) + } +} + +/// Persistent per-frame §5.5 + §C.2.5 subframe→PCM decoder. +/// +/// Owns one [`MultiChannelQmf`] for the frame's channel count, so the +/// per-channel filter state (`raX[]` / `raZ[]`) carries across +/// subframes (and across frames if the same decoder instance is reused +/// for a stream). Construct once with the channel count from the frame +/// header, then call [`SubframePcmDecoder::decode_subframe`] for each of +/// the `nSUBFS` subframes the §5.3.2 header declares. +#[derive(Debug, Clone)] +pub struct SubframePcmDecoder { + qmf: MultiChannelQmf, + /// The persistent §5.5/§C.2.6 LFE channel (the `LFECh` filter + /// object). Drives the §5.5 LFE phase (§2.2) when the frame header's + /// `LFF` is non-zero, carrying the §C.2.6 inter-subframe + /// interpolation history. Idle (and never read from the bitstream) + /// for LFF-absent frames. + lfe: crate::LfeChannel, + /// The LFE PCM decoded from the most recent [`Self::decode_subframe`] + /// call (empty when the frame had no LFE channel). Surfaced via + /// [`Self::take_last_lfe_pcm`] so the primary-channel return tuple is + /// unchanged. + last_lfe_pcm: Vec, + /// The §D.10 VQ code books. Default: the built-in real books + /// ([`VqCodebooks::builtin`]); a caller may swap or strip them + /// ([`Self::set_vq_codebooks`]). + vq_codebooks: VqCodebooks, + /// The persistent §C.2.2 per-subband reconstruction history that + /// primes the inverse-ADPCM predictor across subframe (and, per + /// the §5.3.1 `HFLAG` gate, frame) boundaries. + adpcm_history: AdpcmHistory, +} + +impl SubframePcmDecoder { + /// Construct a decoder for `channels` primary audio channels — the + /// §5.3.2 `nPCHS` (e.g. [`AudioCodingHeader::n_pchs`]). Each + /// channel's §C.2.5 filter starts with cleared history. + #[must_use] + pub fn new(channels: usize) -> Self { + Self { + qmf: MultiChannelQmf::new(channels), + lfe: crate::LfeChannel::new(), + last_lfe_pcm: Vec::new(), + vq_codebooks: VqCodebooks::builtin(), + adpcm_history: AdpcmHistory::new(channels), + } + } + + /// Replace the §D.10 VQ code books ([`VqCodebooks`]). The decoder + /// starts with the built-in real books ([`VqCodebooks::builtin`]), + /// so the high-frequency-VQ (`nVQSUB < nSUBS`) and inverse-ADPCM + /// (`PMODE != 0`) §5.5 sub-paths decode by default; supplying + /// [`VqCodebooks::none`] strips them, restoring the typed + /// [`AudioArrayError::VqCodebookUnavailable`] blocker on those + /// sub-paths. + pub fn set_vq_codebooks(&mut self, books: VqCodebooks) { + self.vq_codebooks = books; + } + + /// The currently attached §D.10 books (default: the built-in real + /// books). + #[must_use] + pub fn vq_codebooks(&self) -> &VqCodebooks { + &self.vq_codebooks + } + + /// Borrow the persistent §C.2.2 reconstruction history. + #[must_use] + pub fn adpcm_history(&self) -> &AdpcmHistory { + &self.adpcm_history + } + + /// Zero the §C.2.2 reconstruction history — the §5.3.1 + /// `HFLAG = 0` entry-point state ("these frames can be coded + /// without the previous frame predictor history … Otherwise, the + /// history will be ignored"). The frame-level walks + /// ([`decode_core_frame`] / [`CoreStreamDecoder::decode_frame`]) + /// apply this automatically from the frame header; it is public + /// for callers driving the per-subframe API directly. + pub fn reset_adpcm_history(&mut self) { + self.adpcm_history.clear(); + } + + /// Take the LFE PCM decoded by the most recent + /// [`Self::decode_subframe`] / [`Self::decode_frame`] call, leaving + /// the decoder's buffer empty. Returns an empty `Vec` when the last + /// frame carried no LFE channel (`LFF == 0`). The samples are the + /// §5.5 LFE phase (§2.2) output: `2·LFF·nSSC·(64 | 128)` interpolated + /// PCM samples per decoded subframe. + #[must_use] + pub fn take_last_lfe_pcm(&mut self) -> Vec { + core::mem::take(&mut self.last_lfe_pcm) + } + + /// The configured channel count (`nPCHS`). + #[must_use] + pub fn channel_count(&self) -> usize { + self.qmf.channel_count() + } + + /// Borrow the persistent §C.2.5 driver (e.g. to inspect a channel's + /// inter-subframe filter tail). + #[must_use] + pub fn qmf(&self) -> &MultiChannelQmf { + &self.qmf + } + + /// Decode one §5.4/§5.5 audio subframe to planar PCM, end to end. + /// + /// Runs the §5.5 [`crate::decode_audio_data_subframe_at`] walk to get the + /// per-channel subband-sample matrices, then the §C.2.5 + /// [`MultiChannelQmf`] synthesis to turn them into PCM. The + /// per-channel filter state persists into the next call. + /// + /// * `bytes` / `bit_offset` — the bit stream positioned at the first + /// §5.5 `Audio Data` bit of this subframe (after the subframe's + /// §5.4.1 side information). + /// * `header` — the parsed §5.3.1 [`DtsFrameHeader`]; supplies the + /// frame-wide `FILTS` ([`DtsFrameHeader::filter_bank_selection`]) + /// and the output `rScale` ([`DtsFrameHeader::output_r_scale`]). + /// * `coding` — the §5.3.2 [`AudioCodingHeader`]; supplies the + /// `SEL` / `arADJ` planes and the per-channel `nSUBS` / `nVQSUB` + /// loop bounds and `JOINX`. + /// * `side` — the per-channel decoded §5.4.1 [`ChannelSideInfo`]. + /// * `n_ssc` — this subframe's subsubframe count (`SSC + 1`). + /// * `aspf` — the §5.3.1 Audio Sync-Word Insertion Flag. + /// + /// Returns `(SubframePcm, bits_consumed)`: planar PCM (one + /// `Vec` per channel, `n_ssc * 256` samples each) plus the + /// number of §5.5 bits the audio-data walk consumed (so the caller + /// can advance to the next subframe). + /// + /// # Errors + /// + /// * [`SubframePcmError::ChannelCountMismatch`] if `side`'s length + /// differs from the configured channel count; + /// * [`SubframePcmError::JointSubbandUnsupported`] if any channel + /// carries `JOINX[ch] > 0`; + /// * [`SubframePcmError::ReservedPcmResolution`] if the header's + /// `PCMR` code is reserved; + /// * [`SubframePcmError::AudioData`] for any §5.5 walk failure + /// (including the §D.10 VQ blockers); + /// * [`SubframePcmError::Synthesis`] for any §C.2.5 driver failure. + #[allow(clippy::too_many_arguments)] + pub fn decode_subframe( + &mut self, + bytes: &[u8], + bit_offset: usize, + header: &DtsFrameHeader, + coding: &AudioCodingHeader, + side: &[ChannelSideInfo], + n_ssc: usize, + aspf: bool, + ) -> Result<(SubframePcm, usize), SubframePcmError> { + self.decode_subframe_with_joint(bytes, bit_offset, header, coding, side, n_ssc, aspf, &[]) + } + + /// Like [`Self::decode_subframe`] but also applies the §C.2.3 + /// joint-intensity sub-band copy when `join_scales` is non-empty. + /// + /// `join_scales[ch]` is the [`crate::SideInfoTail::join_scales`] + /// vector for destination channel `ch` (empty for channels whose + /// `JOINX[ch] == 0`). After the §5.5 audio-data walk fills every + /// channel's sub-band matrix, each jointly-coded channel imports + /// sub-bands `[nSUBS[ch], nSUBS[nSourceCh])` from its source channel + /// (`JOINX[ch] - 1`), each scaled by the matching `JOIN_SCALES` + /// factor, **before** the §C.2.5 QMF synthesis runs — and the QMF's + /// per-channel active-subband count is widened to the source + /// channel's `nSUBS` for those channels, per the §C.2.5 driving-call + /// note ("For joint intensity coded subbands, it must be set to that + /// of the source channel, in order to reflect the true subband + /// activity"), so the imported sub-bands actually reach the output. + #[allow(clippy::too_many_arguments)] + pub fn decode_subframe_with_joint( + &mut self, + bytes: &[u8], + bit_offset: usize, + header: &DtsFrameHeader, + coding: &AudioCodingHeader, + side: &[ChannelSideInfo], + n_ssc: usize, + aspf: bool, + join_scales: &[Vec], + ) -> Result<(SubframePcm, usize), SubframePcmError> { + self.decode_subframe_partial( + bytes, + bit_offset, + header, + coding, + side, + n_ssc, + 0, + aspf, + join_scales, + ) + } + + /// Like [`Self::decode_subframe_with_joint`] but with the §5.4.1 + /// `PSC` (Partial Subsubframe Sample Count) of a **termination + /// frame** applied: when `psc ∈ 1..=7`, the last of this + /// subframe's `n_ssc` subsubframes is *partial* — it carries `psc` + /// subband samples per active subband instead of 8, so the + /// subframe reconstructs to `((n_ssc - 1) * 8 + psc) * 32` PCM + /// samples per channel and the §5.5 bit budget shrinks exactly by + /// the untransmitted samples (see + /// [`crate::decode_audio_data_subframe_partial_at`] for the + /// spec-clause derivation). `psc = 0` is the normal-frame case and + /// reproduces [`Self::decode_subframe_with_joint`] verbatim. + /// + /// The spec ties `PSC > 0` to termination frames only ("It exists + /// only in a termination frame", PDF p.30); this per-subframe + /// entry point trusts the caller on that frame-level gate (the + /// frame walk [`decode_core_frame`] enforces it, surfacing + /// [`SubframePcmError::PartialSubsubframeInNormalFrame`]). + /// + /// When the frame carries an LFE channel, the §5.5 LFE phase is + /// extracted and interpolated at its spec-literal size (Table + /// 5-29: `2·LFF·nSSC` decimated samples — the count has no `PSC` + /// term, so the LFE always covers whole subsubframes) and the + /// interpolated plane is then truncated to the primary channels' + /// PCM length, keeping every output plane aligned on the valid + /// prefix of the terminated subframe. + /// + /// # Errors + /// + /// See [`Self::decode_subframe_with_joint`]. + #[allow(clippy::too_many_arguments)] + pub fn decode_subframe_partial( + &mut self, + bytes: &[u8], + bit_offset: usize, + header: &DtsFrameHeader, + coding: &AudioCodingHeader, + side: &[ChannelSideInfo], + n_ssc: usize, + psc: u8, + aspf: bool, + join_scales: &[Vec], + ) -> Result<(SubframePcm, usize), SubframePcmError> { + let channels = self.qmf.channel_count(); + if side.len() != channels { + return Err(SubframePcmError::ChannelCountMismatch { + expected: channels, + got: side.len(), + }); + } + if coding.n_pchs != channels { + return Err(SubframePcmError::ChannelCountMismatch { + expected: channels, + got: coding.n_pchs, + }); + } + + // The §C.2.5 output rScale must be defined (PCMR not reserved) + // before any decode work runs, so a reserved-PCMR frame fails + // cleanly without disturbing the persistent filter state. + let Some(r_scale) = header.output_r_scale() else { + return Err(SubframePcmError::ReservedPcmResolution { + pcmr: header.source_pcm_resolution_index, + }); + }; + let filter: FilterBankSelection = header.filter_bank_selection(); + + // Joint-intensity subband coding (JOINX > 0) is applied below + // once the §5.5 walk has filled every channel's sub-band matrix, + // but only when the caller supplied the JOIN_SCALES factors (via + // decode_subframe_with_joint). A JOINX > 0 channel with no + // supplied factors is declined rather than silently dropped. + if join_scales.is_empty() { + for (ch, &joinx) in coding.joinx.iter().enumerate().take(channels) { + if joinx > 0 { + return Err(SubframePcmError::JointSubbandUnsupported { ch, joinx }); + } + } + } + + // Per-channel loop bounds for the §5.5 walk and the §C.2.5 + // driver come straight off the §5.3.2 header. + let n_subs = coding.n_subs(); + let n_vqsub = coding.n_vqsub(); + + let table = StepSizeTable::for_rate(header.rate_index); + + // Subband-sample rows this subframe reconstructs: the last + // subsubframe is partial (psc rows) on a termination-frame + // subframe, full (8 rows) otherwise. + let rows = if psc > 0 { + (n_ssc - 1) * 8 + usize::from(psc) + } else { + n_ssc * 8 + }; + + // (0a) §D.10 book-availability gates, checked BEFORE any bit + // is read so a blocked frame fails cleanly without disturbing + // the persistent LFE / filter / history state. Both books are + // present by default (`VqCodebooks::builtin`); the gates fire + // only when a caller stripped them (`VqCodebooks::none`). + let has_hf_vq = (0..channels).any(|ch| n_vqsub[ch] < n_subs[ch]); + if has_hf_vq && self.vq_codebooks.hfreq.is_none() { + let ch = (0..channels) + .find(|&ch| n_vqsub[ch] < n_subs[ch]) + .unwrap_or(0); + return Err(SubframePcmError::AudioData( + AudioArrayError::VqCodebookUnavailable { + ch, + n: n_vqsub[ch], + high_frequency_vq: true, + } + .into(), + )); + } + if self.vq_codebooks.adpcm.is_none() { + for (ch, ch_side) in side.iter().enumerate() { + if let Some(n) = ch_side.pmode[..n_vqsub[ch]].iter().position(|&p| p != 0) { + return Err(SubframePcmError::AudioData( + AudioArrayError::VqCodebookUnavailable { + ch, + n, + high_frequency_vq: false, + } + .into(), + )); + } + } + } + + // (0b) §5.5 phase 1 — high-frequency VQ subbands: the 10-bit + // `nVQIndex` fields precede the LFE phase (Table 5-29). Empty + // for the common Core case (`nVQSUB == nSUBS` everywhere). + let mut cursor = bit_offset; + let hf_indices: Option>> = if has_hf_vq { + let (indices, hf_bits) = scan_hf_vq_indices_at(bytes, cursor, &n_vqsub, &n_subs) + .map_err(|e| SubframePcmError::AudioData(e.into()))?; + cursor += hf_bits; + Some(indices) + } else { + None + }; + + // (0c) §5.5 LFE phase (§2.2): present only when the header's + // `LFF` is non-zero; it follows the phase-1 HF-VQ region and + // precedes the audio-data phase. Its bits count toward the + // subframe's total so the caller advances correctly. The Table + // 5-29 sample count (`2·LFF·nSSC`) has no PSC term — the LFE + // plane always covers whole subsubframes — so on a partial + // (termination) subframe the interpolated plane is truncated + // below to the primary channels' PCM length. + let lff = header.lfe.code(); + if lff != 0 { + let (mut lfe_pcm, lfe_bits) = + decode_lfe_phase_at(bytes, cursor, lff, n_ssc, &mut self.lfe)?; + lfe_pcm.truncate(rows * PCM_PER_SUBBAND_ROW); + self.last_lfe_pcm = lfe_pcm; + cursor += lfe_bits; + } else { + self.last_lfe_pcm = Vec::new(); + } + + // (1) §5.5 Audio Data -> per-channel subband-sample matrices + // (`rows` per channel; the §5.4.1 PSC truncation of the last + // subsubframe is applied inside the walk, bit-exactly), with + // the recovered-book sub-paths enabled where supplied: the + // phase-1 HF-VQ fill and the §C.2.2 inverse-ADPCM prediction + // (whose reconstruction history persists across subframes; the + // §5.3.1 HFLAG frame gate is applied by the frame-level walk). + let hf_fill = match (&self.vq_codebooks.hfreq, &hf_indices) { + (Some(book), Some(indices)) => Some(HfVqFill { + book: book.as_ref(), + indices: indices.as_slice(), + }), + _ => None, + }; + let adpcm_ctx = self.vq_codebooks.adpcm.as_ref().map(|book| AdpcmContext { + book: book.as_ref(), + history: &mut self.adpcm_history, + }); + let (matrices, audio_bits): (Vec, usize) = + decode_audio_data_subframe_vq_at( + bytes, + cursor, + side, + |ch, abits| coding.sel(ch, abits), + |ch, abits| coding.adj(ch, abits), + &n_vqsub, + &n_subs, + n_ssc, + psc, + table, + aspf, + hf_fill, + adpcm_ctx, + )?; + let bits_consumed = (cursor - bit_offset) + audio_bits; + + // (1b) §C.2.3 joint-intensity sub-band copy. For each + // destination channel with JOINX[ch] > 0 and supplied + // JOIN_SCALES factors, overwrite its imported sub-band columns + // [nSUBS[ch], nSUBS[src]) with the source channel's sub-band + // samples scaled by the matching JOIN_SCALES factor. This runs + // on the sub-band matrices before QMF synthesis. + let mut matrices = matrices; + if !join_scales.is_empty() { + apply_joint_subband(&mut matrices, coding, &n_subs, join_scales)?; + } + + // (1b') Effective per-channel active-subband counts after the + // joint import. The §C.2.5 driving-call comment is explicit + // (staged PDF p.184): "nSUBS[ch] indicates the number of active + // subbands. Subbands above it are all zeros. For joint intensity + // coded subbands, it must be set to that of the source channel, + // in order to reflect the true subband activity." A jointly- + // coded destination channel therefore synthesizes (and, below, + // sum/difference-matrixes) over the source channel's count — + // otherwise the §C.2.3 import into [nSUBS[ch], nSUBS[src]) would + // be zero-filled away by the QMF's inactive-subband clear. The + // degenerate empty-range joint (source not wider than the + // destination) keeps the destination's own count. + let mut eff_n_subs = n_subs.clone(); + for (ch, &joinx) in coding.joinx.iter().enumerate().take(channels) { + if let Some(src) = crate::joint_source_channel(joinx) { + let src = usize::from(src); + if src < n_subs.len() && n_subs[src] > eff_n_subs[ch] { + eff_n_subs[ch] = n_subs[src]; + } + } + } + + // (1c) §C.2.4 sum/difference decoding. When the front-sum flag + // (`SUMF`) is set — or unconditionally for AMODE == 3, per the + // spec's "This decoding is also required when AMODE = 3" — the + // front L/R channels are stored as (L+R, L-R) and must be matrixed + // back on the reconstructed sub-band samples (all active subbands, + // all sub-subframe samples) before QMF synthesis. `SUMS` does the + // same for the surround L/R pair. This runs after §C.2.3 joint + // subband and before §C.2.5, matching the Annex C ordering. + let arrangement = header.amode_arrangement(); + let apply_front = + header.front_sum || matches!(arrangement, AmodeArrangement::SumDifference); + if apply_front { + if let Some((l, r)) = arrangement.front_lr_channels() { + apply_sum_difference(&mut matrices, l, r, &eff_n_subs)?; + } + } + if header.surround_sum { + if let Some((l, r)) = arrangement.surround_lr_channels() { + apply_sum_difference(&mut matrices, l, r, &eff_n_subs)?; + } + } + + // (2) §C.2.5 per-channel 32-band synthesis -> planar PCM. + let channel_samples: Vec<&[[f64; NUM_SUBBAND]]> = + matrices.iter().map(|m| m.as_slice()).collect(); + let mut pcm: SubframePcm = vec![Vec::new(); channels]; + self.qmf + .synthesize_planar(&channel_samples, &eff_n_subs, filter, r_scale, &mut pcm)?; + + Ok((pcm, bits_consumed)) + } + + /// Decode all `nSUBFS` subframes of one core frame to a single block + /// of planar PCM, appending each subframe's output (in order) onto + /// the per-channel vectors so the persistent §C.2.5 filter tail + /// carries across subframe boundaries (§5.3.2 `nSUBFS`; §C.2.5 + /// per-channel filter continuity). + /// + /// `bytes` is the frame's bit-stream buffer; `first_audio_bit` is the + /// bit offset of the **first** subframe's §5.5 `Audio Data` region + /// (the cursor a caller is left at after the first subframe's §5.4.1 + /// side info). Each [`Subframe`] supplies that subframe's already- + /// decoded §5.4.1 [`ChannelSideInfo`], its `n_ssc`, and the byte gap + /// (`side_info_bits`) the caller must skip between this subframe's + /// §5.5 region and the next subframe's §5.5 region — i.e. the bits of + /// the *next* subframe's side info, which this driver does not itself + /// decode (that §5.4.x region — `JOIN_SHUFF` onward — is not yet + /// transcribed). The last subframe's `side_info_bits` is ignored. + /// + /// Returns the concatenated planar PCM (one `Vec` per channel, + /// `Σ nSSC · 256` samples each) plus the total bits consumed from + /// `first_audio_bit`. This driver assumes whole subsubframes + /// (`PSC = 0` in every supplied subframe); for a termination + /// frame's partial subsubframe use + /// [`Self::decode_subframe_partial`] per subframe or the + /// header-driven [`decode_core_frame`] walk, which reads each + /// subframe's own `SSC`/`PSC` prefix. + /// + /// # Errors + /// + /// The same errors as [`SubframePcmDecoder::decode_subframe`], plus + /// [`SubframePcmError::ChannelCountMismatch`] if a subframe's + /// side-info channel count disagrees with the driver. A failure on + /// the *k*-th subframe leaves the PCM from subframes `0..k` already + /// appended (the §C.2.5 filter state is likewise advanced through + /// `k-1`); callers that need all-or-nothing semantics should clone + /// the decoder first. + pub fn decode_frame( + &mut self, + bytes: &[u8], + first_audio_bit: usize, + header: &DtsFrameHeader, + coding: &AudioCodingHeader, + subframes: &[Subframe<'_>], + aspf: bool, + ) -> Result<(SubframePcm, usize), SubframePcmError> { + let channels = self.qmf.channel_count(); + let mut pcm: SubframePcm = vec![Vec::new(); channels]; + let mut bit = first_audio_bit; + // Accumulate the per-subframe LFE PCM across the whole frame so + // take_last_lfe_pcm() returns the frame's full LFE output (each + // decode_subframe call leaves only its own subframe's LFE PCM). + let mut frame_lfe: Vec = Vec::new(); + + for (k, sf) in subframes.iter().enumerate() { + let (block, audio_bits) = + self.decode_subframe(bytes, bit, header, coding, sf.side, sf.n_ssc, aspf)?; + frame_lfe.append(&mut self.last_lfe_pcm); + for (ch, samples) in block.into_iter().enumerate() { + pcm[ch].extend(samples); + } + bit += audio_bits; + // Skip the next subframe's §5.4.1 side-info region (the bits + // the caller pre-measured); the last subframe has no + // successor side info to skip. + if k + 1 < subframes.len() { + bit += sf.side_info_bits; + } + } + + self.last_lfe_pcm = frame_lfe; + Ok((pcm, bit - first_audio_bit)) + } +} + +/// One subframe's already-decoded inputs for +/// [`SubframePcmDecoder::decode_frame`]. +/// +/// The driver decodes each subframe's §5.5 `Audio Data` region from the +/// shared bit-stream buffer; this struct carries the per-subframe §5.4.1 +/// side information the audio-data walk needs plus the framing offsets +/// the driver uses to step from one subframe's §5.5 region to the next. +#[derive(Debug, Clone, Copy)] +pub struct Subframe<'a> { + /// This subframe's decoded §5.4.1 per-channel side information (the + /// round-281 [`crate::decode_primary_side_info_at`] output). + pub side: &'a [ChannelSideInfo], + /// This subframe's subsubframe count `nSSC = SSC + 1` (§5.4.1). + pub n_ssc: usize, + /// The bit length of the **next** subframe's §5.4.1 side-info region + /// — the gap the driver skips after this subframe's §5.5 region to + /// reach the next subframe's §5.5 region. Ignored for the last + /// subframe of the frame. + pub side_info_bits: usize, +} + +/// Why a Core frame could not be decoded straight from its bytes by +/// [`decode_core_frame`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreFrameDecodeError { + /// The frame carries a Table 5-28 joint-intensity side-info tail + /// this crate does not yet decode: some channel has `JOINX > 0`, + /// so a variable-length `JOIN_SHUFF` / `JOIN_SCALES` block (gated on + /// the unstaged joint-scale table) sits between a subframe's side + /// info and its §5.5 `Audio Data` region, and the audio-data bit + /// offset cannot be located. The `DYNF` (`RANGE`) and `CPF` + /// (`SICRC`) tail fields are decoded (see [`decode_core_frame`]); + /// only joint-intensity surfaces here. + UnsupportedSideInfoTail { + /// `DYNF != 0` — embedded dynamic-range `RANGE` field present. + /// Retained for source compatibility; no longer a decline + /// reason (the `RANGE` field is decoded and applied post-QMF). + dynamic_range: bool, + /// `CPF != 0` — a 16-bit `SICRC` side-info CRC trailer present. + /// Retained for source compatibility; no longer a decline + /// reason (the `SICRC` word is consumed for framing). + side_info_crc: bool, + /// Some channel carries `JOINX > 0` — a `JOIN_SHUFF`/`JOIN_SCALES` + /// block present. This is the sole remaining decline reason. + joint_intensity: bool, + }, + /// A §5.3.2 / §5.4.1 / §5.5 decode step failed. Carries the + /// underlying [`SubframePcmError`] (or a wrapped bit-stream + /// [`crate::Error`] for the header/side-info walks). + Decode(SubframePcmError), + /// A structural bit-stream error in the §5.3.2 audio-coding-header or + /// §5.4.1 side-info walk (EOF, reserved selector, …). + Bitstream(crate::Error), +} + +impl core::fmt::Display for CoreFrameDecodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CoreFrameDecodeError::UnsupportedSideInfoTail { + dynamic_range, + side_info_crc, + joint_intensity, + } => write!( + f, + "frame carries an undecoded §5.4.x side-info tail \ + (DYNF={dynamic_range}, CPF/SICRC={side_info_crc}, \ + JOINX>0={joint_intensity}); only the empty-tail common \ + Core case is decoded to PCM" + ), + CoreFrameDecodeError::Decode(e) => write!(f, "{e}"), + CoreFrameDecodeError::Bitstream(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for CoreFrameDecodeError {} + +impl From for CoreFrameDecodeError { + fn from(e: SubframePcmError) -> Self { + CoreFrameDecodeError::Decode(e) + } +} + +impl From for CoreFrameDecodeError { + fn from(e: crate::Error) -> Self { + CoreFrameDecodeError::Bitstream(e) + } +} + +/// Decode one whole DTS Core frame to planar PCM straight from its +/// bytes, for the common Core case (§5.3 / §5.4 / §5.5 + §C.2.5). +/// +/// This is the top-level orchestrator that chains the landed stages: +/// +/// 1. the §5.3.2 [`crate::decode_audio_coding_header_at`] (Table 5-21) +/// from the bit just after the §5.3.1 frame header +/// ([`DtsFrameHeader::header_bit_length`]); +/// 2. for each of the `nSUBFS` subframes, the §5.4.1 +/// [`crate::decode_primary_side_info_at`] (Table 5-28) walk, then the +/// §5.5 + §C.2.5 [`SubframePcmDecoder::decode_subframe`] reconstruction; +/// the per-channel §C.2.5 filter tail carries across subframes. +/// +/// `header` is the already-parsed §5.3.1 frame header; `bytes` is the +/// frame's unpacked (16-bit-word-domain) bit-stream buffer. +/// +/// # Scope +/// +/// Joint-intensity sub-band coding (`JOINX > 0`) **is** decoded: each +/// subframe's [`crate::decode_primary_side_info_tail_at`] resolves the +/// Table 5-28 `JOIN_SHUFF` / `JOIN_SCALES` tail (the §D.3 joint-scale +/// table), and the §C.2.3 sub-band copy imports the source channel's +/// sub-bands, scaled by `JOIN_SCALES`, before QMF synthesis. +/// +/// The frame header's `DYNF` (embedded dynamic range) and `CPF` +/// (side-info CRC) tail fields are likewise handled: each subframe's +/// tail walk consumes the 8-bit signed-Q2 `RANGE` code (`DYNF != 0`) +/// and the 16-bit `SICRC` word (`CPF == 1`), and the +/// [`crate::dts_dynrng_to_linear`] gain is applied to that subframe's +/// reconstructed PCM after QMF synthesis (per §5.4.1). +/// +/// §D.10 frames (`nVQSUB < nSUBS` / `PMODE != 0`) decode through the +/// built-in code books; the typed VQ-book blocker surfaces (as +/// [`CoreFrameDecodeError::Decode`]) only for a caller-stripped +/// decoder ([`VqCodebooks::none`]). +/// +/// Returns planar PCM (one `Vec` per channel, `Σ nSSC · 256` +/// samples each; a termination frame's trailing partial subsubframe +/// shrinks its subframe's contribution to `((nSSC-1)·8 + PSC) · 32` — +/// the frame total is always `(NBLKS + 1) · 32`). +/// +/// # Errors +/// +/// * [`CoreFrameDecodeError::Bitstream`] for a §5.3.2 / §5.4.1 walk +/// failure; +/// * [`CoreFrameDecodeError::Decode`] for a §5.5 / §C.2.5 failure +/// (including the §D.10 VQ blockers and a reserved `PCMR`). +pub fn decode_core_frame( + bytes: &[u8], + header: &DtsFrameHeader, +) -> Result { + // §5.3.2 Primary Audio Coding Header begins right after the §5.3.1 + // frame header; the channel count it declares sizes the per-channel + // §C.2.5 filter bank. A fresh per-call decoder gives single-frame + // semantics (cleared filter history) — for a multi-frame elementary + // stream use [`CoreStreamDecoder`], which persists the per-channel + // §C.2.5 filter tail across frame boundaries (the spec's filter is a + // continuous per-channel object, not reset between frames). + let header_bits = header.header_bit_length() as usize; + let cpf = header.crc_present; + let (coding, _ach_bits) = crate::decode_audio_coding_header_at(bytes, header_bits, cpf)?; + let mut decoder = SubframePcmDecoder::new(coding.n_pchs); + decoder.decode_core_frame_into(bytes, header) +} + +/// [`decode_core_frame`] plus the frame's §5.6 Table 5-30 +/// optional-information region: after the last audio-data array the +/// walk continues through the flag-gated `TIMES` (time code stamp, +/// `TIMEF`), `AUXCT`/`AUXD` (auxiliary bytes, `AUXF`), and `OCRC` +/// (`CPF && DYNF`) fields via [`crate::decode_optional_info_at`], +/// returning them alongside the planar PCM. +/// +/// Same single-frame semantics (cleared filter history) as +/// [`decode_core_frame`]; use +/// [`CoreStreamDecoder::decode_frame_with_info`] for the multi-frame +/// path. +/// +/// # Errors +/// +/// See [`decode_core_frame`]; a truncated optional-information region +/// additionally surfaces as [`CoreFrameDecodeError::Bitstream`]. +pub fn decode_core_frame_with_info( + bytes: &[u8], + header: &DtsFrameHeader, +) -> Result<(SubframePcm, crate::OptionalInfo), CoreFrameDecodeError> { + let header_bits = header.header_bit_length() as usize; + let cpf = header.crc_present; + let (coding, _ach_bits) = crate::decode_audio_coding_header_at(bytes, header_bits, cpf)?; + let mut decoder = SubframePcmDecoder::new(coding.n_pchs); + decoder.decode_core_frame_with_info_into(bytes, header) +} + +/// Persistent §5.3/§5.4/§5.5 + §C.2.5 Core-stream decoder. +/// +/// The §C.2.5 `aPrmCh[ch]` synthesis filter is a **continuous** +/// per-channel object whose 512-tap history (`raX[]`) and output +/// accumulator (`raZ[]`) carry across subframe **and frame** +/// boundaries of a contiguous elementary stream — the decoder does not +/// reset the filter at each frame. [`decode_core_frame`] (a fresh +/// per-call decoder) therefore reconstructs each frame as if it were +/// the first frame of a stream, which produces a filter-warmup +/// transient at every frame boundary instead of only the stream's true +/// start. For multi-frame decode use this type: construct it once for +/// the stream's channel count and feed every frame in order through +/// [`CoreStreamDecoder::decode_frame`], so each channel's inter-frame +/// filter tail carries correctly. +/// +/// Validated against a black-box `ffmpeg -c:a dca` reference decode of +/// the bundled 5-frame fixture: carrying the filter state across frames +/// makes our channel-0 PCM **shape-identical** to the reference +/// (Pearson correlation 1.0 over the whole stream), versus 0.73 when +/// the filter is reset per frame. (The two differ only by the +/// implementation-defined output `rScale` constant — see +/// [`DtsFrameHeader::output_r_scale`] and the round-356 report.) +#[derive(Debug, Clone)] +pub struct CoreStreamDecoder { + decoder: SubframePcmDecoder, +} + +impl CoreStreamDecoder { + /// Construct a stream decoder for `channels` primary audio channels + /// (the §5.3.2 `nPCHS`). Every channel's §C.2.5 filter starts with a + /// cleared history; that history then carries across every + /// [`CoreStreamDecoder::decode_frame`] call. + #[must_use] + pub fn new(channels: usize) -> Self { + Self { + decoder: SubframePcmDecoder::new(channels), + } + } + + /// The configured channel count (`nPCHS`). + #[must_use] + pub fn channel_count(&self) -> usize { + self.decoder.channel_count() + } + + /// Borrow the persistent per-subframe decoder (e.g. to inspect a + /// channel's inter-frame §C.2.5 filter tail via + /// [`SubframePcmDecoder::qmf`]). + #[must_use] + pub fn subframe_decoder(&self) -> &SubframePcmDecoder { + &self.decoder + } + + /// Replace the §D.10 VQ code books for the whole stream — see + /// [`SubframePcmDecoder::set_vq_codebooks`] (the built-in real + /// books are the default). The §C.2.2 reconstruction history + /// carries across frames per each frame header's `HFLAG` gate + /// (§5.3.1: history used when `HFLAG = 1`, ignored — zeroed — + /// otherwise). + pub fn set_vq_codebooks(&mut self, books: VqCodebooks) { + self.decoder.set_vq_codebooks(books); + } + + /// The currently attached §D.10 books (default: the built-in real + /// books). + #[must_use] + pub fn vq_codebooks(&self) -> &VqCodebooks { + self.decoder.vq_codebooks() + } + + /// Take the LFE PCM decoded by the most recent + /// [`Self::decode_frame`] call (empty when the frame carried no LFE + /// channel, `LFF == 0`). See + /// [`SubframePcmDecoder::take_last_lfe_pcm`]. The LFE PCM is at the + /// same per-frame sample rate as the primary channels (the §C.2.6 + /// interpolation expands each decimated sample by exactly the factor + /// that matches the primary `nSSC·256` per-subframe length). + #[must_use] + pub fn take_last_lfe_pcm(&mut self) -> Vec { + self.decoder.take_last_lfe_pcm() + } + + /// Decode one whole Core frame to planar PCM, carrying the + /// per-channel §C.2.5 filter tail into the next call. + /// + /// Identical reconstruction to [`decode_core_frame`] except the + /// filter state is **not** reset: a frame's first output samples see + /// the previous frame's filter tail, exactly as the §C.2.5 + /// continuous per-channel filter requires for a contiguous stream. + /// + /// `bytes` is one frame's bit-stream buffer; `header` its parsed + /// §5.3.1 header. The frame's §5.3.2 audio-coding-header channel + /// count must equal this decoder's configured channel count. + /// + /// # Errors + /// + /// The same errors as [`decode_core_frame`], plus + /// [`CoreFrameDecodeError::Decode`] wrapping a + /// [`SubframePcmError::ChannelCountMismatch`] if the frame's + /// `nPCHS` disagrees with the configured channel count. + pub fn decode_frame( + &mut self, + bytes: &[u8], + header: &DtsFrameHeader, + ) -> Result { + self.decoder.decode_core_frame_into(bytes, header) + } + + /// [`Self::decode_frame`] plus the frame's §5.6 Table 5-30 + /// optional-information region (`TIMES` / `AUXD` / `OCRC`), + /// walked from the end-of-audio bit cursor. See + /// [`SubframePcmDecoder::decode_core_frame_with_info_into`]. + /// + /// # Errors + /// + /// See [`Self::decode_frame`]; a truncated optional-information + /// region additionally surfaces as + /// [`CoreFrameDecodeError::Bitstream`]. + pub fn decode_frame_with_info( + &mut self, + bytes: &[u8], + header: &DtsFrameHeader, + ) -> Result<(SubframePcm, crate::OptionalInfo), CoreFrameDecodeError> { + self.decoder.decode_core_frame_with_info_into(bytes, header) + } +} + +impl SubframePcmDecoder { + /// Decode one whole Core frame to planar PCM using this persistent + /// decoder's per-channel §C.2.5 filter state (carried across calls). + /// + /// This is the per-frame body shared by [`decode_core_frame`] (which + /// calls it on a fresh decoder, giving single-frame semantics) and + /// [`CoreStreamDecoder::decode_frame`] (which calls it on a + /// stream-lifetime decoder, carrying the inter-frame filter tail). + /// + /// # Errors + /// + /// See [`decode_core_frame`]. + pub fn decode_core_frame_into( + &mut self, + bytes: &[u8], + header: &DtsFrameHeader, + ) -> Result { + self.decode_core_frame_cursor(bytes, header) + .map(|(pcm, _)| pcm) + } + + /// [`Self::decode_core_frame_into`] plus the §5.6 Table 5-30 + /// optional-information region that follows the last audio-data + /// array: the walk continues from the end-of-audio bit cursor + /// through the flag-gated `TIMES` / `AUXCT` / `AUXD` / `OCRC` + /// fields ([`crate::decode_optional_info_at`]). + /// + /// # Errors + /// + /// See [`decode_core_frame`]; a truncated optional-information + /// region additionally surfaces as + /// [`CoreFrameDecodeError::Bitstream`]. + pub fn decode_core_frame_with_info_into( + &mut self, + bytes: &[u8], + header: &DtsFrameHeader, + ) -> Result<(SubframePcm, crate::OptionalInfo), CoreFrameDecodeError> { + let (pcm, end_bit) = self.decode_core_frame_cursor(bytes, header)?; + let (info, _info_bits) = crate::decode_optional_info_at(bytes, end_bit, header) + .map_err(CoreFrameDecodeError::Bitstream)?; + Ok((pcm, info)) + } + + /// Shared §5.3.2 → §5.4.1 → §5.5 + §C.2.5 frame walk, returning + /// the planar PCM plus the bit cursor at the end of the last + /// audio-data array (where the §5.6 Table 5-30 region begins). + fn decode_core_frame_cursor( + &mut self, + bytes: &[u8], + header: &DtsFrameHeader, + ) -> Result<(SubframePcm, usize), CoreFrameDecodeError> { + // §5.3.2 Primary Audio Coding Header begins right after the + // §5.3.1 frame header. The §5.3.1 CRC-present flag + // (CPF == `crc_present`) controls the optional 16-bit SICRC + // trailer of every subframe's §5.4.1 side info. + let header_bits = header.header_bit_length() as usize; + let cpf = header.crc_present; + let (coding, ach_bits) = crate::decode_audio_coding_header_at(bytes, header_bits, cpf)?; + + let channels = coding.n_pchs; + if channels != self.channel_count() { + return Err(CoreFrameDecodeError::Decode( + SubframePcmError::ChannelCountMismatch { + expected: self.channel_count(), + got: channels, + }, + )); + } + let mut pcm: SubframePcm = vec![Vec::new(); channels]; + + // The §5.4.1 side-info walk needs the per-channel + // ChannelSideInfoParams. + let params: Vec<_> = coding.channel_params.clone(); + + // §5.7.2.2: when a Rev2AUX chunk carries broadcast DRC values, + // they "should be used instead of any dynamic range control + // coefficients found in the legacy core stream (indicated by + // flag DYNF)". Look the chunk up front so the per-subframe + // legacy RANGE gain can be suppressed; gate on the verified + // Annex B CRC so a false DWORD-aligned sync alias inside the + // audio payload cannot hijack the gain path. + let frame_end = bytes.len().min(usize::from(header.frame_size_bytes)); + let rev2_drc: Option> = crate::parse_rev2_aux(&bytes[..frame_end], header) + .ok() + .flatten() + .filter(|chunk| chunk.crc_valid) + .and_then(|chunk| chunk.drc) + .filter(|drc| { + drc.version == crate::REV2_DRC_VERSION_SINGLE_BAND && !drc.codes.is_empty() + }) + .map(|drc| drc.multipliers()); + + // §5.3.1 HFLAG (Predictor History Flag Switch): "When + // generating ADPCM predictions for current frame, the decoder + // will use reconstruction history of the previous frame if + // HFLAG = 1. Otherwise, the history will be ignored" — an + // entry-point frame is coded without the previous frame's + // predictor history, so the persistent §C.2.2 history is + // zeroed before this frame's first subframe. (Within the + // frame the history always carries across subframes.) + if !header.predictor_history { + self.adpcm_history.clear(); + } + + let n_subs = coding.n_subs(); + let mut bit = header_bits + ach_bits; + for subframe_index in 0..coding.n_subframes { + // §5.4.1 side info (Table 5-28) through the end of the + // SCALES block. + let (side, side_bits) = crate::decode_primary_side_info_at(bytes, bit, ¶ms)?; + bit += side_bits; + + // The Table 5-28 JOIN_SHUFF / JOIN_SCALES / RANGE (DYNF) / + // SICRC (CPF) tail sits between the SCALES block and the §5.5 + // region. The joint-intensity JOIN_SCALES factors (if any) + // feed the §C.2.3 sub-band copy below. + let (tail, tail_bits): (SideInfoTail, usize) = crate::decode_primary_side_info_tail_at( + bytes, + bit, + &coding.joinx, + &n_subs, + header.dynamic_range, + cpf, + )?; + bit += tail_bits; + + let n_ssc = side.subsubframe_count.n_ssc() as usize; + // §5.4.1 PSC: a partial (fewer-than-8-sample) trailing + // subsubframe "exists only in a termination frame" (PDF + // p.30) — a normal frame signalling one is structurally + // invalid and declines rather than truncating its audio. + let psc = side.subsubframe_count.psc; + if psc > 0 && header.frame_type != crate::header::FrameType::Termination { + return Err(CoreFrameDecodeError::Decode( + SubframePcmError::PartialSubsubframeInNormalFrame { + subframe: subframe_index, + psc, + }, + )); + } + let (mut block, audio_bits) = self.decode_subframe_partial( + bytes, + bit, + header, + &coding, + &side.channels, + n_ssc, + psc, + header.aspf, + &tail.join_scales, + )?; + + // §5.4.1: when DYNF != 0, multiply every reconstructed PCM + // sample of this subframe by the linear DRC gain, applied + // after QMF synthesis. The 8-bit RANGE code is signed Q2 + // (dB = (int8)code · 0.25; see dts_dynrng_to_db and + // docs/audio/dts/dts-drc-dynrng.md) — NOT a raw index into + // the offset-binary §D.4 presentation table. Suppressed + // (the field is still consumed for framing) when a + // CRC-verified Rev2AUX DRC payload overrides it (§5.7.2.2). + if let Some(code) = tail.range_index { + if rev2_drc.is_none() { + apply_range(&mut block, crate::dts_dynrng_to_linear(code)); + } + } + + for (ch, samples) in block.into_iter().enumerate() { + pcm[ch].extend(samples); + } + bit += audio_bits; + } + + // §5.7.2 Table 5-34: one Rev2AUX DRC value per 256-sample + // subsubframe of the frame, each applied to its own window of + // the reconstructed PCM (replacing the per-subframe legacy + // gain suppressed above). + if let Some(multipliers) = &rev2_drc { + apply_rev2_drc(&mut pcm, multipliers); + } + + Ok((pcm, bit)) + } +} + +/// Apply the §5.7.2 Rev2AUX per-subsubframe DRC multipliers to the +/// frame's reconstructed planar PCM, in place: plane `ch` is split +/// into `multipliers.len()` equal consecutive windows (Table 5-34: one +/// 8-bit DRC value per 256-sample subsubframe) and window `k` is +/// scaled by `multipliers[k]` with the same round-to-nearest / +/// `i32`-saturating convention as the legacy `RANGE` gain. +/// +/// A plane whose length is not an exact multiple of the value count +/// (only possible on a malformed stream whose `NBLKS` disagrees with +/// the decoded subframe structure) is left untouched rather than +/// scaled with a guessed window split. +fn apply_rev2_drc(pcm: &mut SubframePcm, multipliers: &[f64]) { + for channel in pcm.iter_mut() { + if multipliers.is_empty() || channel.len() % multipliers.len() != 0 { + continue; + } + let window = channel.len() / multipliers.len(); + if window == 0 { + continue; + } + for (chunk, &m) in channel.chunks_mut(window).zip(multipliers) { + if m == 1.0 { + continue; + } + for sample in chunk.iter_mut() { + let scaled = (*sample as f64 * m).round(); + *sample = if scaled >= i32::MAX as f64 { + i32::MAX + } else if scaled <= i32::MIN as f64 { + i32::MIN + } else { + scaled as i32 + }; + } + } + } +} + +/// Apply the §5.4.1 `RANGE` dynamic-range multiplier (the signed-Q2 +/// [`crate::dts_dynrng_to_linear`] gain) to every reconstructed PCM +/// sample of one subframe, in place, after QMF synthesis. Results are +/// rounded to the nearest integer and saturated to the `i32` range. +fn apply_range(block: &mut SubframePcm, range: f64) { + if range == 1.0 { + return; + } + for channel in block.iter_mut() { + for sample in channel.iter_mut() { + let scaled = (*sample as f64 * range).round(); + *sample = if scaled >= i32::MAX as f64 { + i32::MAX + } else if scaled <= i32::MIN as f64 { + i32::MIN + } else { + scaled as i32 + }; + } + } +} + +/// Apply the §C.2.3 joint-intensity sub-band copy to the per-channel +/// sub-band matrices, in place, before QMF synthesis. +/// +/// For every destination channel `ch` with `JOINX[ch] > 0` and a +/// non-empty `join_scales[ch]`, each imported sub-band column `n ∈ +/// [nSUBS[ch], nSUBS[nSourceCh])` of every sample row is overwritten +/// with the source channel's (`nSourceCh = JOINX[ch] - 1`) sub-band +/// sample scaled by the matching `JOIN_SCALES[ch][n]` factor. +/// +/// The `join_scales[ch]` vector supplies one factor per imported +/// sub-band, ordered from `nSUBS[ch]`; its length must equal +/// `nSUBS[nSourceCh] - nSUBS[ch]`. Structural inconsistencies (source +/// channel out of range, mismatched factor count, matrices shorter than +/// `nSUBS[nSourceCh]`) surface as +/// [`SubframePcmError::JointSubbandShape`]. +fn apply_joint_subband( + matrices: &mut [SubbandSampleMatrix], + coding: &AudioCodingHeader, + n_subs: &[usize], + join_scales: &[Vec], +) -> Result<(), SubframePcmError> { + let channels = matrices.len(); + for ch in 0..channels { + let factors = join_scales.get(ch).map(Vec::as_slice).unwrap_or(&[]); + if factors.is_empty() { + continue; + } + let joinx = coding.joinx.get(ch).copied().unwrap_or(0); + let Some(source_ch) = crate::joint_source_channel(joinx).map(usize::from) else { + return Err(SubframePcmError::JointSubbandShape { ch }); + }; + if source_ch >= channels || ch >= n_subs.len() || source_ch >= n_subs.len() { + return Err(SubframePcmError::JointSubbandShape { ch }); + } + let n_subs_dst = n_subs[ch]; + let n_subs_src = n_subs[source_ch]; + // The import range must run forward and match the factor count. + if n_subs_src < n_subs_dst || factors.len() != n_subs_src - n_subs_dst { + return Err(SubframePcmError::JointSubbandShape { ch }); + } + if n_subs_src > NUM_SUBBAND { + return Err(SubframePcmError::JointSubbandShape { ch }); + } + // Destination and source are distinct channels of one Vec here + // (a self-referential joint has an empty import range and never + // reaches this point). Split the slice so the source (immutable) + // and destination (mutable) borrows do not overlap. + let rows = matrices[ch].len(); + if matrices[source_ch].len() != rows { + return Err(SubframePcmError::JointSubbandShape { ch }); + } + let (lo, hi) = if ch < source_ch { + (ch, source_ch) + } else { + (source_ch, ch) + }; + let (left, right) = matrices.split_at_mut(hi); + let (dst_ch, src_ch): (&mut SubbandSampleMatrix, &SubbandSampleMatrix) = if ch == lo { + (&mut left[ch], &right[0]) + } else { + (&mut right[0], &left[source_ch]) + }; + for (dst_row, src_row) in dst_ch.iter_mut().zip(src_ch.iter()) { + for (k, &factor) in factors.iter().enumerate() { + let n = n_subs_dst + k; + dst_row[n] = factor * src_row[n]; + } + } + } + Ok(()) +} + +/// Apply the §C.2.4 sum/difference matrix to one channel pair's +/// reconstructed sub-band samples, in place, before QMF synthesis. +/// +/// For each active sub-band (`n ∈ [0, nSUBS)`) and each sub-subframe +/// sample row, the pair `(l, r)` — with `l` the front/surround **left** +/// channel and `r` the **right** — is matrixed as +/// `(L', R') = (L + R, L - R)`, reading the pre-update value of the left +/// sample for both outputs (the §C.2.4 pseudocode's read-old/write-new +/// ordering). The active sub-band bound is the smaller of the two +/// channels' `nSUBS` (they are equal in a well-formed stream) clamped to +/// the 32-band matrix width. +/// +/// Structural inconsistencies (channel index out of range, `l == r`, or +/// the two channels' matrices carrying a different sample-row count) +/// surface as [`SubframePcmError::JointSubbandShape`] — the same +/// caller-side sub-band-matrix shape-violation variant the §C.2.3 copy +/// uses. +fn apply_sum_difference( + matrices: &mut [SubbandSampleMatrix], + l: usize, + r: usize, + n_subs: &[usize], +) -> Result<(), SubframePcmError> { + let channels = matrices.len(); + if l >= channels || r >= channels || l == r { + return Err(SubframePcmError::JointSubbandShape { ch: l.min(r) }); + } + let n_active = n_subs + .get(l) + .copied() + .unwrap_or(0) + .min(n_subs.get(r).copied().unwrap_or(0)) + .min(NUM_SUBBAND); + if n_active == 0 { + return Ok(()); + } + if matrices[l].len() != matrices[r].len() { + return Err(SubframePcmError::JointSubbandShape { ch: l.min(r) }); + } + // Split the backing Vec so the two distinct channel matrices can be + // borrowed mutably at once (mirrors the §C.2.3 copy's split pattern). + let (lo, hi) = if l < r { (l, r) } else { (r, l) }; + let (lower, upper) = matrices.split_at_mut(hi); + // `left_m` is the front/surround-left channel `l`, `right_m` is `r`. + let (left_m, right_m): (&mut SubbandSampleMatrix, &mut SubbandSampleMatrix) = if l == lo { + (&mut lower[l], &mut upper[0]) + } else { + (&mut upper[0], &mut lower[r]) + }; + for (left_row, right_row) in left_m.iter_mut().zip(right_m.iter_mut()) { + for n in 0..n_active { + let lv = left_row[n]; + let rv = right_row[n]; + left_row[n] = lv + rv; + right_row[n] = lv - rv; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::side_info::ScaleFactorAdjustment; + use crate::step_size::SAMPLES_PER_SUBSUBFRAME; + use crate::subframe::ChannelSideInfo; + + /// Pack a list of `(value, width)` MSB-first into bytes. + fn pack_fields(fields: &[(u32, u8)]) -> Vec { + let total_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + let mut out = vec![0u8; total_bits.div_ceil(8)]; + let mut bit_pos = 0usize; + for &(value, width) in fields { + for i in (0..width).rev() { + let bit = ((value >> i) & 1) as u8; + out[bit_pos / 8] |= bit << (7 - (bit_pos % 8)); + bit_pos += 1; + } + } + out + } + + /// A single-channel single-subsubframe NFE subframe reconstructs to + /// PCM end to end, and the PCM equals running the §5.5 walk + the + /// §C.2.5 driver by hand. + #[test] + fn nfe_subframe_round_trips_to_pcm() { + // ABITS 8 -> NFE width 5; SEL 7 selects the terminal NFE entry. + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; + ch.scales[0][0] = 4; + let side = vec![ch]; + + let vals = [3i32, -3, 5, -5, 7, -7, 2, -2]; + let mut fields: Vec<(u32, u8)> = vals.iter().map(|&v| ((v as u32) & 0x1f, 5u8)).collect(); + fields.push((0xffff, 16)); // DSYNC + let stream = pack_fields(&fields); + + // Build a parsed header carrying FILTS / PCMR via the public + // parser: reuse the registry test fixture's real BE header + // (PCMR index 0 -> 16-bit -> rScale 32768, FILTS = 0). + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let header = crate::parse_frame_header(&hdr_bytes).unwrap(); + assert_eq!(header.output_r_scale(), Some(32768.0)); + + // A one-channel AudioCodingHeader with nSUBS=nVQSUB=1, JOINX=0, + // SEL[ch][ABITS 8-1] = 7 (terminal NFE). Build it through the + // public test constructor. + let coding = AudioCodingHeader::single_channel_for_test(1, 1, 7); + + let mut dec = SubframePcmDecoder::new(1); + let (pcm, bits) = dec + .decode_subframe(&stream, 0, &header, &coding, &side, 1, false) + .unwrap(); + + // Reference: walk + driver by hand. + let table = StepSizeTable::for_rate(header.rate_index); + let (mats, ref_bits) = crate::audio_array::decode_audio_data_subframe_at( + &stream, + 0, + &side, + |_, _| 7, + |_, _| ScaleFactorAdjustment::Adj0, + &[1], + &[1], + 1, + table, + false, + ) + .unwrap(); + let refs: Vec<&[[f64; NUM_SUBBAND]]> = mats.iter().map(|m| m.as_slice()).collect(); + let mut mc = MultiChannelQmf::new(1); + let mut expect = vec![Vec::new(); 1]; + mc.synthesize_planar( + &refs, + &[1], + header.filter_bank_selection(), + 32768.0, + &mut expect, + ) + .unwrap(); + + assert_eq!(bits, ref_bits); + assert_eq!(pcm, expect); + // One subsubframe of 8 rows -> 8 * 32 = 256 PCM samples. + assert_eq!(pcm[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + assert!(pcm[0].iter().any(|&s| s != 0)); + } + + /// A reserved PCMR code fails cleanly without disturbing the filter + /// state. + #[test] + fn reserved_pcmr_declines() { + // PCMR index 4 (0b100) is one of the reserved codes -> rScale + // None. Construct a header with that PCMR via the test setter. + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let mut header = crate::parse_frame_header(&hdr_bytes).unwrap(); + header.source_pcm_resolution_index = 4; // reserved + assert_eq!(header.output_r_scale(), None); + + let side = vec![ChannelSideInfo::cleared()]; + let coding = AudioCodingHeader::single_channel_for_test(1, 1, 0); + let mut dec = SubframePcmDecoder::new(1); + let err = dec + .decode_subframe(&[0u8; 4], 0, &header, &coding, &side, 1, false) + .unwrap_err(); + assert!(matches!( + err, + SubframePcmError::ReservedPcmResolution { pcmr: 4 } + )); + // Filter untouched. + assert!(dec + .qmf() + .channels() + .iter() + .all(|q| q.x_history().iter().all(|&v| v == 0.0))); + } + + /// A JOINX > 0 channel is declined. + #[test] + fn joint_subband_declined() { + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let header = crate::parse_frame_header(&hdr_bytes).unwrap(); + let side = vec![ChannelSideInfo::cleared()]; + let mut coding = AudioCodingHeader::single_channel_for_test(1, 1, 0); + coding.set_joinx_for_test(0, 2); + let mut dec = SubframePcmDecoder::new(1); + let err = dec + .decode_subframe(&[0u8; 4], 0, &header, &coding, &side, 1, false) + .unwrap_err(); + assert!(matches!( + err, + SubframePcmError::JointSubbandUnsupported { ch: 0, joinx: 2 } + )); + } + + /// The §C.2.3 joint copy overwrites the destination channel's + /// imported sub-band columns with the source channel's samples, + /// scaled by the matching JOIN_SCALES factor, and leaves the + /// non-imported columns untouched. + #[test] + fn apply_joint_subband_scales_imported_columns() { + // 2 channels, 2 sample rows. ch0 (source) nSUBS=4, ch1 (dst) + // nSUBS=2 -> import subbands 2 and 3 from ch0 into ch1. + let mut ch0 = vec![[0.0f64; NUM_SUBBAND]; 2]; + let mut ch1 = vec![[0.0f64; NUM_SUBBAND]; 2]; + // Source subband samples in columns 2 and 3. + ch0[0][2] = 10.0; + ch0[0][3] = -4.0; + ch0[1][2] = 5.0; + ch0[1][3] = 8.0; + // Destination pre-existing values in columns 0/1 (kept) and a + // stray in column 2 (must be overwritten). + ch1[0][0] = 99.0; + ch1[0][2] = 7.0; + let mut matrices = vec![ch0, ch1]; + + let mut coding = AudioCodingHeader::two_channel_for_test((4, 4), (2, 2), 0); + coding.set_joinx_for_test(1, 1); // ch1 sources ch0 (JOINX=1) + + // JOIN_SCALES[1] = [2.0, 3.0] for imported subbands 2 and 3. + let join_scales = vec![Vec::new(), vec![2.0, 3.0]]; + let n_subs = [4usize, 2usize]; + + apply_joint_subband(&mut matrices, &coding, &n_subs, &join_scales).unwrap(); + + // Imported columns are source * factor. + assert_eq!(matrices[1][0][2], 20.0); // 10 * 2 + assert_eq!(matrices[1][0][3], -12.0); // -4 * 3 + assert_eq!(matrices[1][1][2], 10.0); // 5 * 2 + assert_eq!(matrices[1][1][3], 24.0); // 8 * 3 + // Non-imported destination columns untouched. + assert_eq!(matrices[1][0][0], 99.0); + // Source channel untouched. + assert_eq!(matrices[0][0][2], 10.0); + } + + /// A JOIN_SCALES vector whose length disagrees with the import range + /// is rejected as a shape error. + #[test] + fn apply_joint_subband_rejects_wrong_factor_count() { + let mut matrices = vec![ + vec![[0.0f64; NUM_SUBBAND]; 1], + vec![[0.0f64; NUM_SUBBAND]; 1], + ]; + let mut coding = AudioCodingHeader::two_channel_for_test((4, 4), (2, 2), 0); + coding.set_joinx_for_test(1, 1); + // Import range is [2, 4) = 2 subbands, but only 1 factor given. + let join_scales = vec![Vec::new(), vec![2.0]]; + let n_subs = [4usize, 2usize]; + let err = apply_joint_subband(&mut matrices, &coding, &n_subs, &join_scales).unwrap_err(); + assert!(matches!(err, SubframePcmError::JointSubbandShape { ch: 1 })); + } + + /// A channel-count mismatch between the decoder and the side-info + /// slice is rejected before any decode. + #[test] + fn channel_count_mismatch_rejected() { + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let header = crate::parse_frame_header(&hdr_bytes).unwrap(); + let side = vec![ChannelSideInfo::cleared(), ChannelSideInfo::cleared()]; + let coding = AudioCodingHeader::single_channel_for_test(1, 1, 0); + let mut dec = SubframePcmDecoder::new(1); + let err = dec + .decode_subframe(&[0u8; 4], 0, &header, &coding, &side, 1, false) + .unwrap_err(); + assert!(matches!( + err, + SubframePcmError::ChannelCountMismatch { + expected: 1, + got: 2 + } + )); + } + + /// A no-bits subframe yields all-zero PCM of the right length. + #[test] + fn no_bits_subframe_zero_pcm() { + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let header = crate::parse_frame_header(&hdr_bytes).unwrap(); + let side = vec![ChannelSideInfo::cleared()]; // ABITS all 0 + let coding = AudioCodingHeader::single_channel_for_test(1, 1, 0); + // nSSC = 2 -> two DSYNC trailers (last subsubframe only; ASPF + // false means only the final one). + let stream = pack_fields(&[(0xffff, 16)]); + let mut dec = SubframePcmDecoder::new(1); + let (pcm, _) = dec + .decode_subframe(&stream, 0, &header, &coding, &side, 1, false) + .unwrap(); + assert_eq!(pcm.len(), 1); + assert_eq!(pcm[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + assert!(pcm[0].iter().all(|&s| s == 0)); + } + + /// An LFE-present frame (`LFF != 0`) consumes the §5.5 LFE phase + /// before the audio-data phase, and the decoded LFE PCM has the same + /// per-subframe length as the primary channels (`nSSC·256`): the + /// §C.2.6 interpolation expands `2·LFF·nSSC` decimated samples by + /// `nDeciFactor` to exactly that length. The cursor stays aligned, so + /// the trailing DSYNC still validates. + #[test] + fn lfe_present_subframe_consumes_lfe_phase_and_matches_primary_length() { + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let mut header = crate::parse_frame_header(&hdr_bytes).unwrap(); + header.lfe = crate::LfeMode::Mode1; // LFF == 1 -> 128× + let side = vec![ChannelSideInfo::cleared()]; // ABITS all 0 + let coding = AudioCodingHeader::single_channel_for_test(1, 1, 0); + + // §5.5 region for nSSC = 1, LFF = 1: + // LFE phase: 2·1·1 = 2 sample bytes + 1 scale-index byte + // audio-data phase: ABITS all 0 -> just a 16-bit DSYNC. + let mut fields: Vec<(u32, u8)> = vec![(0, 8), (0, 8), (10, 8)]; + fields.push((0xffff, 16)); + let stream = pack_fields(&fields); + + let mut dec = SubframePcmDecoder::new(1); + let (pcm, bits) = dec + .decode_subframe(&stream, 0, &header, &coding, &side, 1, false) + .unwrap(); + + // Primary channel: nSSC·256 samples, all zero (no audio bits). + assert_eq!(pcm.len(), 1); + assert_eq!(pcm[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + // The cursor consumed LFE (3 bytes) + DSYNC (2 bytes) = 40 bits. + assert_eq!(bits, (3 + 2) * 8); + // LFE PCM is the same per-subframe length as the primary channel. + let lfe = dec.take_last_lfe_pcm(); + assert_eq!(lfe.len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + // All-zero LFE samples -> silence. + assert!(lfe.iter().all(|&s| s == 0)); + // Taking it again yields empty (it was moved out). + assert!(dec.take_last_lfe_pcm().is_empty()); + } + + /// `decode_frame` over two NFE subframes equals running + /// `decode_subframe` twice on the same persistent decoder — the + /// §C.2.5 filter tail carries across the subframe boundary and the + /// PCM is concatenated in order. + #[test] + fn decode_frame_concatenates_and_carries_filter_state() { + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let header = crate::parse_frame_header(&hdr_bytes).unwrap(); + let coding = AudioCodingHeader::single_channel_for_test(1, 1, 7); + + // Two subframes, each: 8 NFE 5-bit values + a 16-bit DSYNC. The + // second subframe's §5.5 region directly follows the first (no + // inter-subframe side info in this synthetic stream, so + // side_info_bits = 0). + let mut ch = ChannelSideInfo::cleared(); + ch.abits[0] = 8; + ch.scales[0][0] = 4; + let side = vec![ch]; + + let mk_sf = |base: i32| -> Vec<(u32, u8)> { + let mut f: Vec<(u32, u8)> = (0..8).map(|i| (((base + i) as u32) & 0x1f, 5u8)).collect(); + f.push((0xffff, 16)); + f + }; + let mut fields = mk_sf(1); + let sf0_bits: usize = fields.iter().map(|(_, w)| *w as usize).sum(); + fields.extend(mk_sf(-4)); + let stream = pack_fields(&fields); + + let subframes = [ + Subframe { + side: &side, + n_ssc: 1, + side_info_bits: 0, // next subframe's §5.5 immediately follows + }, + Subframe { + side: &side, + n_ssc: 1, + side_info_bits: 0, + }, + ]; + + let mut frame_dec = SubframePcmDecoder::new(1); + let (frame_pcm, frame_bits) = frame_dec + .decode_frame(&stream, 0, &header, &coding, &subframes, false) + .unwrap(); + + // Reference: two decode_subframe calls on one persistent decoder. + let mut seq_dec = SubframePcmDecoder::new(1); + let (b0, n0) = seq_dec + .decode_subframe(&stream, 0, &header, &coding, &side, 1, false) + .unwrap(); + let (b1, n1) = seq_dec + .decode_subframe(&stream, n0, &header, &coding, &side, 1, false) + .unwrap(); + let mut expect = b0; + for (ch, samples) in b1.into_iter().enumerate() { + expect[ch].extend(samples); + } + + assert_eq!(frame_pcm, expect); + assert_eq!(frame_bits, n0 + n1); + // Each subframe is one subsubframe -> 256 PCM samples; two -> 512. + assert_eq!( + frame_pcm[0].len(), + 2 * SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW + ); + // Sanity: the first subframe's §5.5 region was sf0_bits long. + assert_eq!(n0, sf0_bits); + assert!(frame_pcm[0].iter().any(|&s| s != 0)); + } + + /// Encode a clean §5.3.1 header (single channel, byte-aligned, with + /// `dynamic_range`/`predictor_history`/`aspf` as given) by parsing + /// the fixture, mutating the flags, and re-encoding. Returns the + /// encoded header bytes (a body packed separately concatenates + /// straight onto them; the caller parses the assembled buffer). + fn encode_clean_header(dynf: bool, cpf: bool) -> Vec { + let hdr_bytes: [u8; 16] = [ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]; + let mut header = crate::parse_frame_header(&hdr_bytes).unwrap(); + header.dynamic_range = dynf; + // CPF is the §5.3.1 CRC-Present-Flag (`crc_present`), the flag + // that gates HCRC / AHCRC / SICRC — NOT `predictor_history`. + header.crc_present = cpf; + // When CPF is set the header carries a 16-bit HCRC; supply a + // value so the BE encoder serialises the field (its value is not + // verified on decode per §5.3.1). + header.header_crc = if cpf { Some(0) } else { None }; + header.aspf = false; + crate::encode_frame_header_be(&header).unwrap() + } + + /// A one-channel, one-subframe, all-`ABITS==0` (NoBits) Core frame + /// decodes end to end from raw bytes through `decode_core_frame` to + /// all-zero PCM of the right length. + #[test] + fn decode_core_frame_no_bits_round_trips() { + let mut bytes = encode_clean_header(false, false); + + // §5.3.2 Audio Coding Header (Table 5-21), one channel: + // SUBFS=0 -> 1 subframe; PCHS=0 -> 1 channel; + // SUBS=0 -> nSUBS=2; VQSUB=1 -> nVQSUB=2 (== nSUBS, no HF VQ); + // JOINX=0; THUFF=0; SHUFF=0; BHUFF=0. + // SEL plane: ABITS1 1 bit, ABITS2-5 4×2 bits, ABITS6-10 5×3 bits. + // With every SEL=0, every group transmits a 2-bit ADJ -> 10 ADJ. + let mut body: Vec<(u32, u8)> = vec![ + (0, 4), // SUBFS + (0, 3), // PCHS + (0, 5), // SUBS -> nSUBS 2 + (1, 5), // VQSUB -> nVQSUB 2 + (0, 3), // JOINX + (0, 2), // THUFF + (0, 3), // SHUFF + (6, 3), // BHUFF=6 -> Linear5Bit (5-bit ABITS reads) + ]; + body.push((0, 1)); // SEL ABITS1 + for _ in 1..5 { + body.push((0, 2)); + } + for _ in 5..10 { + body.push((0, 3)); + } + for _ in 0..10 { + body.push((0, 2)); // ADJ + } + + // §5.4.1 side info (Table 5-28), one subframe: + // SSC=0 -> nSSC=1; PSC=0; PMODE[0][0..2]=0 (2 bits); + // no PVQ (PMODE all 0); ABITS[0][0..2]=0 (2× the BHUFF=6 + // Linear5Bit code -> 5 bits each, value 0); nSSC==1 so no + // TMODE plane; all ABITS 0 so no SCALES factors for the two + // primary subbands, and nVQSUB==nSUBS so no HF VQ scales. + body.push((0, 2)); // SSC + body.push((0, 3)); // PSC + body.push((0, 1)); // PMODE[0][0] + body.push((0, 1)); // PMODE[0][1] + body.push((0, 5)); // ABITS[0][0] (BHUFF=6 Linear5Bit) = 0 + body.push((0, 5)); // ABITS[0][1] = 0 + + // §5.5 Audio Data: nSSC=1, all ABITS 0 -> NoBits -> no audio + // bits, then the single DSYNC trailer. + body.push((0xffff, 16)); + + let body_bytes = pack_fields(&body); + bytes.extend_from_slice(&body_bytes); + // A little trailing slack so the header parser's lookahead is + // always satisfied. + bytes.extend_from_slice(&[0u8; 4]); + + let header = crate::parse_frame_header(&bytes).unwrap(); + assert!(!header.dynamic_range); + assert!(!header.crc_present); + assert_eq!(header.header_bit_length() % 8, 0); + + let pcm = decode_core_frame(&bytes, &header).unwrap(); + assert_eq!(pcm.len(), 1); + // One subframe, one subsubframe -> 8 rows -> 256 PCM samples. + assert_eq!(pcm[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + assert!(pcm[0].iter().all(|&s| s == 0)); + } + + /// The §5.3.2 one-channel NoBits ACH body shared by the tail tests: + /// SUBFS=0/PCHS=0/SUBS=0(nSUBS 2)/VQSUB=1(nVQSUB 2)/JOINX=0, all + /// codebook selectors 0 except BHUFF=6 (Linear5Bit), the SEL plane, + /// and the 10 ADJ groups. When `cpf` is set a 16-bit AHCRC trailer + /// is appended (consumed by `decode_audio_coding_header_at`). + fn nobits_ach_body(cpf: bool) -> Vec<(u32, u8)> { + let mut body: Vec<(u32, u8)> = vec![ + (0, 4), // SUBFS + (0, 3), // PCHS + (0, 5), // SUBS -> nSUBS 2 + (1, 5), // VQSUB -> nVQSUB 2 + (0, 3), // JOINX + (0, 2), // THUFF + (0, 3), // SHUFF + (6, 3), // BHUFF=6 -> Linear5Bit + ]; + body.push((0, 1)); // SEL ABITS1 + for _ in 1..5 { + body.push((0, 2)); + } + for _ in 5..10 { + body.push((0, 3)); + } + for _ in 0..10 { + body.push((0, 2)); // ADJ + } + if cpf { + body.push((0, 16)); // AHCRC + } + body + } + + /// The §5.4.1 one-subframe NoBits side-info SCALES block (SSC/PSC, + /// 2 PMODE bits, 2 zero ABITS Linear5Bit reads — no SCALES, no HF + /// VQ since nVQSUB==nSUBS). + fn nobits_side_info() -> Vec<(u32, u8)> { + vec![ + (0, 2), // SSC + (0, 3), // PSC + (0, 1), // PMODE[0][0] + (0, 1), // PMODE[0][1] + (0, 5), // ABITS[0][0] = 0 + (0, 5), // ABITS[0][1] = 0 + ] + } + + /// A frame whose header sets `CPF` (a 16-bit `SICRC` side-info tail) + /// now decodes end to end: the `SICRC` word is consumed for framing + /// (its CRC test is not applied per §5.4.1) and the §5.5 region lands + /// at the right cursor, yielding all-zero PCM of the right length. + #[test] + fn decode_core_frame_consumes_sicrc_tail() { + let mut bytes = encode_clean_header(false, true); // DYNF=0, CPF=1 + let mut body = nobits_ach_body(true); + body.extend(nobits_side_info()); + body.push((0xABCD, 16)); // SICRC (CPF=1) — consumed, not verified + body.push((0xffff, 16)); // §5.5 DSYNC + let body_bytes = pack_fields(&body); + bytes.extend_from_slice(&body_bytes); + bytes.extend_from_slice(&[0u8; 4]); + + let header = crate::parse_frame_header(&bytes).unwrap(); + assert!(header.crc_present); + + let pcm = decode_core_frame(&bytes, &header).unwrap(); + assert_eq!(pcm.len(), 1); + assert_eq!(pcm[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + assert!(pcm[0].iter().all(|&s| s == 0)); + } + + /// A frame whose header sets `DYNF` carries an 8-bit `RANGE` code in + /// each subframe's side-info tail; `decode_core_frame` consumes it + /// and (for a non-unity code) the signed-Q2 linear gain scales the + /// PCM. With an all-zero (NoBits) subframe the PCM is zero + /// regardless of `RANGE`, which proves only the framing/cursor is + /// correct — the `apply_range` value is covered by + /// `range_unity_is_noop` / `range_scales_pcm`. + #[test] + fn decode_core_frame_consumes_range_tail() { + let mut bytes = encode_clean_header(true, false); // DYNF=1, CPF=0 + let mut body = nobits_ach_body(false); + body.extend(nobits_side_info()); + body.push((0, 8)); // RANGE code 0 -> unity (no SICRC, CPF=0) + body.push((0xffff, 16)); // §5.5 DSYNC + let body_bytes = pack_fields(&body); + bytes.extend_from_slice(&body_bytes); + bytes.extend_from_slice(&[0u8; 4]); + + let header = crate::parse_frame_header(&bytes).unwrap(); + assert!(header.dynamic_range); + + let pcm = decode_core_frame(&bytes, &header).unwrap(); + assert_eq!(pcm.len(), 1); + assert_eq!(pcm[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + assert!(pcm[0].iter().all(|&s| s == 0)); + } + + /// A frame with a single `JOINX = 1` channel that references itself + /// (source == destination, equal `nSUBS`) has an empty joint import + /// range: the JOIN_SHUFF selector is read but no JOIN_SCALES follow, + /// and the frame decodes normally (no more decline). This exercises + /// the JOIN_SHUFF read on the decode path without needing a source + /// channel wider than the destination. + #[test] + fn decode_core_frame_joint_self_empty_range_decodes() { + let mut bytes = encode_clean_header(false, false); + // ACH mirrors decode_core_frame_no_bits_round_trips but sets + // JOINX[0] = 1 (source channel 0 == self); nSUBS[0] == nSUBS[src] + // so the joint import range is empty. The JOIN_SHUFF selector is + // still read from the side-info tail; no JOIN_SCALES follow. + let mut body: Vec<(u32, u8)> = vec![ + (0, 4), // SUBFS + (0, 3), // PCHS + (0, 5), // SUBS -> nSUBS = 2 + (1, 5), // VQSUB -> nVQSUB = 2 (== nSUBS, Core case) + (1, 3), // JOINX = 1 (source channel 0 == self) + (0, 2), // THUFF + (0, 3), // SHUFF + (6, 3), // BHUFF=6 -> Linear5Bit + ]; + body.push((0, 1)); // SEL ABITS1 + for _ in 1..5 { + body.push((0, 2)); + } + for _ in 5..10 { + body.push((0, 3)); + } + for _ in 0..10 { + body.push((0, 2)); // ADJ + } + + // §5.4.1 side info, one subframe (as in the no-bits round trip). + body.push((0, 2)); // SSC + body.push((0, 3)); // PSC + body.push((0, 1)); // PMODE[0][0] + body.push((0, 1)); // PMODE[0][1] + body.push((0, 5)); // ABITS[0][0] = 0 + body.push((0, 5)); // ABITS[0][1] = 0 + + // §5.4.1 tail: JOINX[0] > 0 -> a 3-bit JOIN_SHUFF[0] precedes the + // (absent) RANGE/SICRC. The empty import range emits no + // JOIN_SCALES. + body.push((0, 3)); // JOIN_SHUFF[0] = SA129 + + // §5.5 Audio Data: all ABITS 0 -> NoBits -> just the DSYNC. + body.push((0xffff, 16)); + + let body_bytes = pack_fields(&body); + bytes.extend_from_slice(&body_bytes); + bytes.extend_from_slice(&[0u8; 4]); + + let header = crate::parse_frame_header(&bytes).unwrap(); + // No longer declined: the empty-range joint frame decodes. + let pcm = decode_core_frame(&bytes, &header).unwrap(); + assert_eq!(pcm.len(), 1); + // All-zero side info -> all-zero subband samples -> silent PCM. + assert!(pcm[0].iter().all(|&s| s == 0)); + } + + /// `apply_range` with the unity code (signed-Q2 `0` = 0 dB) leaves + /// the PCM untouched. + #[test] + fn range_unity_is_noop() { + let mut block: SubframePcm = vec![vec![100, -200, 0, i32::MAX, i32::MIN]]; + apply_range(&mut block, crate::dts_dynrng_to_linear(0)); // 1.0 + assert_eq!(block[0], vec![100, -200, 0, i32::MAX, i32::MIN]); + } + + /// `apply_range` scales every sample by the signed-Q2 linear gain + /// with round-to-nearest and `i32` saturation. + #[test] + fn range_scales_pcm() { + // Signed-Q2 code -80 -> -20 dB -> 0.1; code +80 -> +20 dB -> 10.0. + let minus_20_db = 0u8.wrapping_sub(80); + let mut down: SubframePcm = vec![vec![1000, -1000, 5]]; + apply_range(&mut down, crate::dts_dynrng_to_linear(minus_20_db)); // 0.1 + assert_eq!(down[0], vec![100, -100, 1]); // 5*0.1=0.5 -> round 1 + + let mut up: SubframePcm = vec![vec![10, -10, i32::MAX]]; + apply_range(&mut up, crate::dts_dynrng_to_linear(80)); // 10.0 + assert_eq!(up[0], vec![100, -100, i32::MAX]); // saturates + } + + /// Build a complete one-channel all-`ABITS==0` (NoBits) raw-BE Core + /// frame — the same proven layout `decode_core_frame_no_bits_round_trips` + /// uses — with a signed-Q2 `RANGE` code optionally injected so + /// the `apply_range` path is exercised even though the §5.5 audio + /// data is silent. When `dynf` is `false` no `RANGE` field is + /// present and the frame decodes to all-zero PCM. + fn build_nobits_frame(dynf: bool, range_index: u8) -> Vec { + let mut header = crate::parse_frame_header(&[ + 0x7f, 0xfe, 0x80, 0x01, 0xfc, 0x3c, 0x3f, 0xf0, 0xb5, 0xe0, 0x01, 0x38, 0x00, 0x03, + 0xef, 0x7f, + ]) + .unwrap(); + header.dynamic_range = dynf; + header.crc_present = false; + header.header_crc = None; + header.aspf = false; + let mut bytes = crate::encode_frame_header_be(&header).unwrap(); + + // §5.3.2 ACH: one channel, nSUBS=2/nVQSUB=2, BHUFF=6 Linear5Bit, + // SEL plane all zero (every group transmits a 2-bit ADJ), 10 ADJ. + let mut body: Vec<(u32, u8)> = vec![ + (0, 4), // SUBFS -> 1 subframe + (0, 3), // PCHS -> 1 channel + (0, 5), // SUBS -> nSUBS 2 + (1, 5), // VQSUB -> nVQSUB 2 + (0, 3), // JOINX + (0, 2), // THUFF + (0, 3), // SHUFF + (6, 3), // BHUFF=6 Linear5Bit + ]; + body.push((0, 1)); // SEL ABITS1 + for _ in 1..5 { + body.push((0, 2)); + } + for _ in 5..10 { + body.push((0, 3)); + } + for _ in 0..10 { + body.push((0, 2)); // ADJ + } + + // §5.4.1 side info: SSC/PSC, 2 PMODE bits, 2 zero ABITS reads. + body.push((0, 2)); // SSC -> nSSC 1 + body.push((0, 3)); // PSC + body.push((0, 1)); // PMODE[0][0] + body.push((0, 1)); // PMODE[0][1] + body.push((0, 5)); // ABITS[0][0] = 0 + body.push((0, 5)); // ABITS[0][1] = 0 + + // Table 5-28 tail: an 8-bit RANGE index when DYNF (CPF=0 so no + // SICRC), then the §5.5 DSYNC trailer. + if dynf { + body.push((range_index as u32, 8)); + } + body.push((0xffff, 16)); // DSYNC + + bytes.extend_from_slice(&pack_fields(&body)); + bytes.extend_from_slice(&[0u8; 4]); + bytes + } + + /// [`CoreStreamDecoder::decode_frame`] reproduces the standalone + /// [`decode_core_frame`] result frame-for-frame (the per-frame body + /// is the shared [`SubframePcmDecoder::decode_core_frame_into`]); the + /// difference is only in the persistent filter state carried between + /// calls, which an all-zero stream cannot expose, so this pins the + /// per-frame equivalence. + #[test] + fn core_stream_decode_matches_decode_core_frame_per_frame() { + let f0 = build_nobits_frame(false, 0); + let f1 = build_nobits_frame(false, 0); + let h0 = crate::parse_frame_header(&f0).unwrap(); + let h1 = crate::parse_frame_header(&f1).unwrap(); + + let mut stream = CoreStreamDecoder::new(1); + let s0 = stream.decode_frame(&f0, &h0).unwrap(); + let s1 = stream.decode_frame(&f1, &h1).unwrap(); + assert_eq!(stream.channel_count(), 1); + + // Each frame matches the fresh-per-frame decode (silent stream: + // the carried filter tail is zero, so the two paths agree). + assert_eq!(s0, decode_core_frame(&f0, &h0).unwrap()); + assert_eq!(s1, decode_core_frame(&f1, &h1).unwrap()); + assert_eq!(s0[0].len(), SAMPLES_PER_SUBSUBFRAME * PCM_PER_SUBBAND_ROW); + assert!(s0[0].iter().all(|&v| v == 0)); + } + + /// [`CoreStreamDecoder`] reuses one persistent per-channel §C.2.5 + /// filter across frames rather than resetting it — the structural + /// precondition for inter-frame filter continuity. (The end-to-end + /// proof that this makes our PCM shape-identical to a black-box + /// `ffmpeg -c:a dca` reference decode of a real multi-frame stream is + /// the `decodes_real_fixture_stream_matching_ffmpeg_shape` + /// integration test; with non-zero §5.5 audio the carried tail + /// changes the next frame's leading samples, which an all-`ABITS==0` + /// synthetic frame cannot exercise.) + #[test] + fn core_stream_reuses_persistent_filter_across_frames() { + let f0 = build_nobits_frame(false, 0); + let f1 = build_nobits_frame(false, 0); + let h0 = crate::parse_frame_header(&f0).unwrap(); + let h1 = crate::parse_frame_header(&f1).unwrap(); + let mut stream = CoreStreamDecoder::new(1); + + // The same filter object (and its history) must survive a decode: + // a silent stream leaves the history all-zero, so we assert the + // decoder neither panics nor reallocates the channel filters. + let _ = stream.decode_frame(&f0, &h0).unwrap(); + assert_eq!(stream.subframe_decoder().qmf().channel_count(), 1); + let _ = stream.decode_frame(&f1, &h1).unwrap(); + assert_eq!(stream.subframe_decoder().qmf().channel_count(), 1); + assert!(stream + .subframe_decoder() + .qmf() + .channels() + .iter() + .all(|q| q.x_history().iter().all(|&v| v == 0.0))); + } + + /// A [`CoreStreamDecoder`] built for the wrong channel count rejects + /// a frame whose `nPCHS` disagrees, without panicking. + #[test] + fn core_stream_channel_count_mismatch_rejected() { + let frame = build_nobits_frame(false, 0); + let header = crate::parse_frame_header(&frame).unwrap(); + // The frame is one channel; a 2-channel decoder must decline. + let mut stream = CoreStreamDecoder::new(2); + let err = stream.decode_frame(&frame, &header).unwrap_err(); + assert!(matches!( + err, + CoreFrameDecodeError::Decode(SubframePcmError::ChannelCountMismatch { + expected: 2, + got: 1 + }) + )); + } + + /// A pure unit test of the §C.2.4 matrix: feeding a two-channel pair + /// whose sub-band samples are `(L+R, L-R)` back through + /// `apply_sum_difference` recovers `(2L, 2R)` — the matrix is + /// self-inverse up to the factor of two the encoder's scale factors + /// absorb. Only the active sub-band columns are touched. + #[test] + fn apply_sum_difference_recovers_double_original() { + // Two "original" channels, 3 sample rows, 2 active sub-bands. + let l = [[1.0_f64, 2.0], [3.0, 4.0], [5.0, 6.0]]; + let r = [[10.0_f64, 20.0], [30.0, 40.0], [50.0, 60.0]]; + let mut matrices: Vec = vec![vec![[0.0; NUM_SUBBAND]; 3]; 2]; + for row in 0..3 { + for n in 0..2 { + // Encoder side: store (L+R) in ch0, (L-R) in ch1. + matrices[0][row][n] = l[row][n] + r[row][n]; + matrices[1][row][n] = l[row][n] - r[row][n]; + } + // A high (inactive) sub-band that must be left untouched. + matrices[0][row][20] = 7.0; + matrices[1][row][20] = 9.0; + } + apply_sum_difference(&mut matrices, 0, 1, &[2, 2]).unwrap(); + for row in 0..3 { + for n in 0..2 { + assert_eq!( + matrices[0][row][n], + 2.0 * l[row][n], + "ch0 row {row} sub {n}" + ); + assert_eq!( + matrices[1][row][n], + 2.0 * r[row][n], + "ch1 row {row} sub {n}" + ); + } + // Inactive sub-band 20 (>= nSUBS) untouched. + assert_eq!(matrices[0][row][20], 7.0); + assert_eq!(matrices[1][row][20], 9.0); + } + } + + /// End-to-end: forcing the `SUMF` flag on a real fixture frame routes + /// the §C.2.4 front sum/difference decode through the full + /// reconstruction chain. The bundled fixture's two channels are + /// identical at the sub-band level (`L == R`), so the matrix produces + /// `(L+R, L-R) = (2L, 0)`: the difference channel decodes to **exact + /// silence**, and — since the §C.2.5 QMF is linear over cleared + /// per-frame history — the sum channel is twice the un-summed decode + /// (within the ±1 truncation of the integer output cast). + #[test] + fn sumf_forced_zeros_difference_channel_on_real_fixture() { + const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/dts_5_frames.bin"); + let frame = &FIXTURE[0..1024]; + let header = crate::parse_frame_header(frame).unwrap(); + // The fixture is AMODE 2 (Stereo) with the sum flags clear. + assert_eq!(header.amode_arrangement(), AmodeArrangement::Stereo); + assert!(!header.front_sum && !header.surround_sum); + + // Baseline decode (fresh, cleared history). + let base = decode_core_frame(frame, &header).unwrap(); + assert_eq!(base.len(), 2); + assert_eq!(base[0], base[1], "fixture channels are identical"); + assert!(base[0].iter().any(|&s| s != 0), "baseline is non-silent"); + + // Force SUMF and decode the same bytes. + let mut sumf_header = header; + sumf_header.front_sum = true; + let sumf = decode_core_frame(frame, &sumf_header).unwrap(); + + // Difference channel (ch1 = L - R = 0) is exactly silent. + assert!( + sumf[1].iter().all(|&s| s == 0), + "SUMF difference channel must decode to exact silence when L == R" + ); + // Sum channel (ch0 = L + R = 2L) ~= twice the baseline, within the + // integer output cast's ±1 truncation slack. + for (i, (&s, &b)) in sumf[0].iter().zip(&base[0]).enumerate() { + let diff = (i64::from(s) - 2 * i64::from(b)).abs(); + assert!( + diff <= 1, + "sum channel sample {i}: got {s}, expected ~{} (2x baseline)", + 2 * b + ); + } + assert!(sumf[0].iter().any(|&s| s != 0), "sum channel is non-silent"); + } +} diff --git a/crates/vendor/oxideav-dts/src/sum_diff.rs b/crates/vendor/oxideav-dts/src/sum_diff.rs new file mode 100644 index 00000000..6b317a61 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/sum_diff.rs @@ -0,0 +1,622 @@ +//! DTS Coherent Acoustics — §C.2.4 Sum/Difference Decoding. +//! +//! Round 214 (2026-06-03) lands the §C.2.4 sum/difference matrix +//! decoder, the inverse of the encoder-side joint sum/difference +//! coding that the `FRONT_SUM` (`SUMF`) and `SURROUND_SUM` (`SUMS`) +//! header flags signal (and that the `AMODE == 3` Sum/Difference +//! channel-arrangement code implies for the front pair). +//! +//! Source: ETSI TS 102 114 V1.3.1 (2011-08), Annex C (informative) +//! §C.2.4 "Sum/Difference Decoding" (PDF p.184) — staged at +//! `docs/audio/dts/etsi-ts-102114-dts-coherent-acoustics.pdf`. The +//! reproduced normative spec pseudocode is: +//! +//! ```text +//! // SUMF — front L/R (also when AMODE == 3, Sum/Difference) +//! for (n=0; n Result<()> { + if left.len() != right.len() { + return Err(Error::SumDiffLengthMismatch { + left_len: left.len(), + right_len: right.len(), + }); + } + for (l, r) in left.iter_mut().zip(right.iter_mut()) { + let prev_left = *l; + let prev_right = *r; + *l = prev_left.wrapping_add(prev_right); + *r = prev_left.wrapping_sub(prev_right); + } + Ok(()) +} + +/// Decode one (left, right) sample pair in place via the §C.2.4 +/// sum/difference matrix, in floating-point arithmetic. Same matrix +/// as [`sum_difference_decode_i32`]; chosen by callers that consume +/// the reconstructed-subband samples in floating-point. +/// +/// # Errors +/// +/// Returns [`Error::SumDiffLengthMismatch`] if `left.len() != +/// right.len()`. +/// +/// # Example +/// +/// ```rust +/// use oxideav_dts::sum_difference_decode_f64; +/// +/// let mut left = [15.0_f64]; +/// let mut right = [5.0_f64]; +/// sum_difference_decode_f64(&mut left, &mut right).unwrap(); +/// assert_eq!(left[0], 20.0); +/// assert_eq!(right[0], 10.0); +/// ``` +pub fn sum_difference_decode_f64(left: &mut [f64], right: &mut [f64]) -> Result<()> { + if left.len() != right.len() { + return Err(Error::SumDiffLengthMismatch { + left_len: left.len(), + right_len: right.len(), + }); + } + for (l, r) in left.iter_mut().zip(right.iter_mut()) { + let prev_left = *l; + let prev_right = *r; + *l = prev_left + prev_right; + *r = prev_left - prev_right; + } + Ok(()) +} + +/// Decode the §C.2.4 sum/difference matrix across **all active +/// subbands × all sub-sub-frame samples** for a single channel pair. +/// The two argument slice-of-slices each carry `n_subs` inner slices +/// (one per active subband, ordered subband 0..`n_subs`), each +/// inner slice holding the `8 * n_ssc` sub-sub-frame samples for +/// that subband × that channel. +/// +/// This is the direct shape of the §C.2.4 pseudocode loop: +/// +/// ```text +/// for (n=0; n Result<()> { + if left_subbands.len() != right_subbands.len() { + return Err(Error::SumDiffLengthMismatch { + left_len: left_subbands.len(), + right_len: right_subbands.len(), + }); + } + for (left_band, right_band) in left_subbands.iter_mut().zip(right_subbands.iter_mut()) { + sum_difference_decode_i32(left_band, right_band)?; + } + Ok(()) +} + +/// Floating-point counterpart to [`sum_difference_decode_subband_pair_i32`]. +/// +/// # Errors +/// +/// Same error contract as the i32 variant. +pub fn sum_difference_decode_subband_pair_f64( + left_subbands: &mut [&mut [f64]], + right_subbands: &mut [&mut [f64]], +) -> Result<()> { + if left_subbands.len() != right_subbands.len() { + return Err(Error::SumDiffLengthMismatch { + left_len: left_subbands.len(), + right_len: right_subbands.len(), + }); + } + for (left_band, right_band) in left_subbands.iter_mut().zip(right_subbands.iter_mut()) { + sum_difference_decode_f64(left_band, right_band)?; + } + Ok(()) +} + +/// Returns `true` if the §C.2.4 front-channel sum/difference decode +/// must be applied given the frame-header's `FRONT_SUM` flag and +/// `AMODE` field. +/// +/// Per §C.2.4: the decoding is required when `SUMF` is set, and +/// **also** when `AMODE == 3` (Sum/Difference channel arrangement). +/// The two triggers compose disjunctively. +/// +/// `amode` is the raw 6-bit `AMODE` value from the frame header +/// (`DtsFrameHeader::amode`); the function checks `amode == 3` directly +/// rather than going through the [`crate::AmodeArrangement`] enum so +/// the call site can dispatch without resolving the user-defined +/// codes (`16..=63`). +pub fn front_sum_difference_required(front_sum: bool, amode: u8) -> bool { + front_sum || amode == 3 +} + +/// Returns `true` if the §C.2.4 surround-channel sum/difference decode +/// must be applied given the `SURROUND_SUM` (`SUMS`) flag. +/// +/// Unlike the front-pair case, the spec does not name an `AMODE` code +/// that forces the surround decode independent of `SUMS`; the +/// function therefore reduces to a pass-through of the flag. +pub fn surround_sum_difference_required(surround_sum: bool) -> bool { + surround_sum +} + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------- + // i32 single-pair decode — matrix property tests + // ----------------------------------------------------------- + + #[test] + fn single_pair_basic_decode_recovers_2l_2r() { + // Encoder: (L, R) -> (L+R, L-R) + // Decoder: (L+R, L-R) -> (2L, 2R) + let (l, r) = (10i32, 5i32); + let mut enc_left = [l + r]; + let mut enc_right = [l - r]; + sum_difference_decode_i32(&mut enc_left, &mut enc_right).unwrap(); + assert_eq!(enc_left[0], 2 * l); + assert_eq!(enc_right[0], 2 * r); + } + + #[test] + fn single_pair_zero_inputs_decode_to_zero() { + let mut left = [0i32; 8]; + let mut right = [0i32; 8]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + for v in left.iter().chain(right.iter()) { + assert_eq!(*v, 0); + } + } + + #[test] + fn single_pair_left_only_decode() { + // (L+R, L-R) with R=0 -> (L, L) ... but here we feed raw + // (L, R) = (5, 0) to confirm the matrix: + // left_out = 5 + 0 = 5 + // right_out = 5 - 0 = 5 + let mut left = [5i32]; + let mut right = [0i32]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + assert_eq!(left[0], 5); + assert_eq!(right[0], 5); + } + + #[test] + fn single_pair_right_only_decode() { + // (0, R) -> (R, -R): matrix turns a pure-right input into a + // sign-mirrored output pair. + let mut left = [0i32]; + let mut right = [3i32]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + assert_eq!(left[0], 3); + assert_eq!(right[0], -3); + } + + #[test] + fn single_pair_uses_pre_update_left_for_right() { + // Verify the spec's read-old-then-write ordering: the right + // channel reads the **pre-update** value of left, not the + // post-update one. If we wrote left first and then computed + // right as left-right, we'd get (l+r, (l+r)-r) = (l+r, l) — + // which is wrong; the correct result is (l+r, l-r). + let mut left = [10i32]; + let mut right = [3i32]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + assert_eq!(left[0], 13); + assert_eq!(right[0], 7); + } + + #[test] + fn single_pair_length_mismatch_reports_lengths() { + let mut left = [0i32; 4]; + let mut right = [0i32; 5]; + let err = sum_difference_decode_i32(&mut left, &mut right).unwrap_err(); + assert!(matches!( + err, + Error::SumDiffLengthMismatch { + left_len: 4, + right_len: 5, + } + )); + } + + #[test] + fn single_pair_empty_slices_succeed() { + let mut left: [i32; 0] = []; + let mut right: [i32; 0] = []; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + } + + #[test] + fn single_pair_decoded_twice_yields_2x_pair() { + // Applying the matrix twice should multiply each component + // by 2 (mod wrapping): from (L, R) -> (L+R, L-R) -> ((L+R)+(L-R), (L+R)-(L-R)) + // = (2L, 2R). + // The encoder runs it once; if the decoder ran it twice we'd + // see the 2x scaling. This test cross-checks the matrix + // self-product = 2I. + let (l, r) = (7i32, 11i32); + let mut left = [l]; + let mut right = [r]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + assert_eq!(left[0], 2 * l); + assert_eq!(right[0], 2 * r); + } + + #[test] + fn single_pair_negative_inputs_decode_correctly() { + let mut left = [-5i32, -10, -100]; + let mut right = [3i32, -7, 50]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + assert_eq!(left, [-2, -17, -50]); + assert_eq!(right, [-8, -3, -150]); + } + + #[test] + fn single_pair_wrapping_arithmetic_at_i32_max() { + // Wrapping behaviour at the i32 boundary: the spec's + // C `int` semantics wrap on overflow; we use + // `i32::wrapping_add` so the test must not panic. + let mut left = [i32::MAX]; + let mut right = [1i32]; + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + // i32::MAX.wrapping_add(1) = i32::MIN + assert_eq!(left[0], i32::MIN); + // i32::MAX.wrapping_sub(1) = i32::MAX - 1 + assert_eq!(right[0], i32::MAX - 1); + } + + #[test] + fn single_pair_walk_long_slice() { + // Independent per-sample property: each (l[i], r[i]) pair + // satisfies (l_out, r_out) = (l_in + r_in, l_in - r_in) + // regardless of position. + let l_in: Vec = (0i32..256).map(|i| i - 128).collect(); + let r_in: Vec = (0i32..256).map(|i| (i * 2) - 100).collect(); + let mut left = l_in.clone(); + let mut right = r_in.clone(); + sum_difference_decode_i32(&mut left, &mut right).unwrap(); + for i in 0..256 { + assert_eq!(left[i], l_in[i].wrapping_add(r_in[i])); + assert_eq!(right[i], l_in[i].wrapping_sub(r_in[i])); + } + } + + // ----------------------------------------------------------- + // f64 single-pair decode tests + // ----------------------------------------------------------- + + #[test] + fn single_pair_f64_basic() { + let mut left = [1.5_f64, 2.5]; + let mut right = [0.25_f64, -0.5]; + sum_difference_decode_f64(&mut left, &mut right).unwrap(); + assert!((left[0] - 1.75).abs() < 1e-12); + assert!((right[0] - 1.25).abs() < 1e-12); + assert!((left[1] - 2.0).abs() < 1e-12); + assert!((right[1] - 3.0).abs() < 1e-12); + } + + #[test] + fn single_pair_f64_self_product_is_2i() { + // Matrix^2 = 2 I, same property as the i32 variant — but + // here we get exact 2x scaling because the test inputs are + // small dyadic rationals. + let l_in = 0.25_f64; + let r_in = 0.125_f64; + let mut left = [l_in]; + let mut right = [r_in]; + sum_difference_decode_f64(&mut left, &mut right).unwrap(); + sum_difference_decode_f64(&mut left, &mut right).unwrap(); + assert!((left[0] - 2.0 * l_in).abs() < 1e-12); + assert!((right[0] - 2.0 * r_in).abs() < 1e-12); + } + + #[test] + fn single_pair_f64_length_mismatch() { + let mut left = [0.0_f64; 3]; + let mut right = [0.0_f64; 7]; + let err = sum_difference_decode_f64(&mut left, &mut right).unwrap_err(); + assert!(matches!( + err, + Error::SumDiffLengthMismatch { + left_len: 3, + right_len: 7, + } + )); + } + + // ----------------------------------------------------------- + // Subband-pair (slice-of-slices) decode tests + // ----------------------------------------------------------- + + #[test] + fn subband_pair_walks_all_active_subbands_i32() { + // Three subbands, two samples each (mocking nSUBS=3, 8*nSSC=2). + let mut subband_a_l = [1i32, 2]; + let mut subband_a_r = [3i32, 4]; + let mut subband_b_l = [5i32, 6]; + let mut subband_b_r = [7i32, 8]; + let mut subband_c_l = [9i32, 10]; + let mut subband_c_r = [11i32, 12]; + // Capture expected outputs before consuming the &mut. + let exp_a_l = [1 + 3, 2 + 4]; + let exp_a_r = [1 - 3, 2 - 4]; + let exp_b_l = [5 + 7, 6 + 8]; + let exp_b_r = [5 - 7, 6 - 8]; + let exp_c_l = [9 + 11, 10 + 12]; + let exp_c_r = [9 - 11, 10 - 12]; + { + let mut left: [&mut [i32]; 3] = [&mut subband_a_l, &mut subband_b_l, &mut subband_c_l]; + let mut right: [&mut [i32]; 3] = [&mut subband_a_r, &mut subband_b_r, &mut subband_c_r]; + sum_difference_decode_subband_pair_i32(&mut left, &mut right).unwrap(); + } + // Verify each subband decoded independently per §C.2.4. + assert_eq!(subband_a_l, exp_a_l); + assert_eq!(subband_a_r, exp_a_r); + assert_eq!(subband_b_l, exp_b_l); + assert_eq!(subband_b_r, exp_b_r); + assert_eq!(subband_c_l, exp_c_l); + assert_eq!(subband_c_r, exp_c_r); + } + + #[test] + fn subband_pair_empty_subband_list_is_no_op() { + let mut left: [&mut [i32]; 0] = []; + let mut right: [&mut [i32]; 0] = []; + sum_difference_decode_subband_pair_i32(&mut left, &mut right).unwrap(); + } + + #[test] + fn subband_pair_outer_length_mismatch() { + let mut s_a = [0i32; 4]; + let mut s_b = [0i32; 4]; + let mut left: [&mut [i32]; 2] = [&mut s_a, &mut s_b]; + let mut t = [0i32; 4]; + let mut right: [&mut [i32]; 1] = [&mut t]; + let err = sum_difference_decode_subband_pair_i32(&mut left, &mut right).unwrap_err(); + assert!(matches!( + err, + Error::SumDiffLengthMismatch { + left_len: 2, + right_len: 1, + } + )); + } + + #[test] + fn subband_pair_per_subband_length_mismatch() { + let mut s_a = [0i32; 4]; + let mut s_b = [0i32; 4]; + let mut left: [&mut [i32]; 2] = [&mut s_a, &mut s_b]; + let mut t_a = [0i32; 4]; + let mut t_b = [0i32; 3]; + let mut right: [&mut [i32]; 2] = [&mut t_a, &mut t_b]; + let err = sum_difference_decode_subband_pair_i32(&mut left, &mut right).unwrap_err(); + // The error reports the *inner* per-subband length pair (the + // first one that disagrees). + assert!(matches!( + err, + Error::SumDiffLengthMismatch { + left_len: 4, + right_len: 3, + } + )); + } + + #[test] + fn subband_pair_f64_walks_independently() { + let mut s_a_l = [1.0_f64, 2.0]; + let mut s_a_r = [3.0_f64, 4.0]; + let mut s_b_l = [10.0_f64]; + let mut s_b_r = [20.0_f64]; + { + let mut left: [&mut [f64]; 2] = [&mut s_a_l, &mut s_b_l]; + let mut right: [&mut [f64]; 2] = [&mut s_a_r, &mut s_b_r]; + sum_difference_decode_subband_pair_f64(&mut left, &mut right).unwrap(); + } + assert_eq!(s_a_l, [4.0, 6.0]); + assert_eq!(s_a_r, [-2.0, -2.0]); + assert_eq!(s_b_l, [30.0]); + assert_eq!(s_b_r, [-10.0]); + } + + // ----------------------------------------------------------- + // Dispatch predicate tests + // ----------------------------------------------------------- + + #[test] + fn front_sum_required_when_flag_set() { + // SUMF = true forces the decode regardless of AMODE. + for amode in 0u8..=15 { + assert!(front_sum_difference_required(true, amode), "amode={amode}"); + } + } + + #[test] + fn front_sum_required_when_amode_is_three() { + // AMODE == 3 (Sum/Difference channel arrangement) forces the + // decode even when SUMF = false. Per §C.2.4: "This decoding is + // also required when AMODE = 3." + assert!(front_sum_difference_required(false, 3)); + } + + #[test] + fn front_sum_not_required_when_flag_clear_and_amode_not_three() { + // Spot-check every standard AMODE != 3 with SUMF = false. + for amode in 0u8..=15 { + if amode == 3 { + continue; + } + assert!( + !front_sum_difference_required(false, amode), + "amode={amode} should not require decode" + ); + } + // Same for the user-defined range (16..=63) — none of those + // codes carries an implicit Sum/Difference signal in the spec. + for amode in 16u8..=63 { + assert!( + !front_sum_difference_required(false, amode), + "user-defined amode={amode} should not require decode" + ); + } + } + + #[test] + fn surround_sum_required_when_flag_set() { + assert!(surround_sum_difference_required(true)); + assert!(!surround_sum_difference_required(false)); + } + + // ----------------------------------------------------------- + // End-to-end full §C.2.4 sweep + // ----------------------------------------------------------- + + #[test] + fn full_sweep_matches_spec_pseudocode_directly() { + // Hand-compute the §C.2.4 result for nSUBS = 4, 8*nSSC = 8 + // and a deterministic input, then cross-check against the + // subband-pair helper. + let n_subs: i32 = 4; + let n_samples: i32 = 8; + let mut left_storage: Vec> = (0..n_subs) + .map(|s| (0..n_samples).map(|i| s * 100 + i).collect()) + .collect(); + let mut right_storage: Vec> = (0..n_subs) + .map(|s| (0..n_samples).map(|i| -(s * 100 + i) / 2).collect()) + .collect(); + let expected_left: Vec> = left_storage + .iter() + .zip(right_storage.iter()) + .map(|(l, r)| l.iter().zip(r.iter()).map(|(a, b)| a + b).collect()) + .collect(); + let expected_right: Vec> = left_storage + .iter() + .zip(right_storage.iter()) + .map(|(l, r)| l.iter().zip(r.iter()).map(|(a, b)| a - b).collect()) + .collect(); + + { + let mut left_slices: Vec<&mut [i32]> = + left_storage.iter_mut().map(|v| v.as_mut_slice()).collect(); + let mut right_slices: Vec<&mut [i32]> = + right_storage.iter_mut().map(|v| v.as_mut_slice()).collect(); + sum_difference_decode_subband_pair_i32(&mut left_slices, &mut right_slices).unwrap(); + } + assert_eq!(left_storage, expected_left); + assert_eq!(right_storage, expected_right); + } +} diff --git a/crates/vendor/oxideav-dts/src/test_util.rs b/crates/vendor/oxideav-dts/src/test_util.rs new file mode 100644 index 00000000..e0e4e870 --- /dev/null +++ b/crates/vendor/oxideav-dts/src/test_util.rs @@ -0,0 +1,80 @@ +//! Test-only helpers shared by the in-module unit-test suites: +//! an MSB-first bit writer for constructing synthetic bitstream +//! windows, and a minimal parseable raw-BE frame-header builder. + +use crate::header::DtsFrameHeader; +use crate::parse_frame_header; + +/// Minimal MSB-first bit writer for constructing synthetic bitstream +/// chunks in tests. +pub(crate) struct BitWriter { + bytes: Vec, + bit_len: usize, +} + +impl BitWriter { + pub(crate) fn new() -> Self { + BitWriter { + bytes: Vec::new(), + bit_len: 0, + } + } + + /// Append the low `n` bits of `value`, MSB-first. + pub(crate) fn push_bits(&mut self, value: u64, n: u32) { + for i in (0..n).rev() { + let bit = (value >> i) & 1; + if self.bit_len % 8 == 0 { + self.bytes.push(0); + } + let byte = self.bytes.last_mut().unwrap(); + *byte |= (bit as u8) << (7 - (self.bit_len % 8)); + self.bit_len += 1; + } + } + + /// Zero-pad until the running bit length is a multiple of `bits`. + pub(crate) fn align(&mut self, bits: usize) { + while self.bit_len % bits != 0 { + self.push_bits(0, 1); + } + } + + /// Current bit length. + pub(crate) fn bit_len(&self) -> usize { + self.bit_len + } + + /// Finish, zero-padding to a whole byte. + pub(crate) fn into_bytes(mut self) -> Vec { + self.align(8); + self.bytes + } +} + +/// Build a parseable raw-BE frame header with the requested `AMODE`, +/// 13-bit flag window (`downmix .. predictor_history`; the 2-bit +/// `LFF` field sits one bit above `predictor_history`), and `NBLKS` +/// field (blocks per frame = `nblks + 1`). +pub(crate) fn synth_header_with_blocks(amode: u64, extra_13: u64, nblks: u64) -> DtsFrameHeader { + let mut w = BitWriter::new(); + w.push_bits(0x7FFE_8001, 32); // raw-BE sync + w.push_bits(1, 1); // FTYPE normal + w.push_bits(31, 5); // SHORT deficit -> 32 samples/block + w.push_bits(0, 1); // CPF + w.push_bits(nblks, 7); // NBLKS + w.push_bits(127, 14); // FSIZE -> 128 bytes + w.push_bits(amode, 6); // AMODE + w.push_bits(13, 4); // SFREQ 48 kHz + w.push_bits(10, 5); // RATE + w.push_bits(extra_13, 13); // downmix .. predictor_history + w.push_bits(0, 16); // post-CRC trailing window + let mut bytes = w.into_bytes(); + bytes.resize(16, 0); // parser reads a 16-byte window + parse_frame_header(&bytes).expect("synthetic header parses") +} + +/// [`synth_header_with_blocks`] with the default 16-block frame. +pub(crate) fn synth_header(amode: u64, extra_13: u64) -> DtsFrameHeader { + synth_header_with_blocks(amode, extra_13, 15) +} diff --git a/crates/vendor/oxideav-dts/src/unpack14.rs b/crates/vendor/oxideav-dts/src/unpack14.rs new file mode 100644 index 00000000..15381b0f --- /dev/null +++ b/crates/vendor/oxideav-dts/src/unpack14.rs @@ -0,0 +1,605 @@ +//! 14-bit DTS bitstream → 16-bit-equivalent unpacker. +//! +//! ## Why this exists +//! +//! Per the multimedia.cx wiki snapshot +//! (`docs/audio/dts/wiki/DTS.wiki`, section "14-bit words"): +//! +//! > This kind of bitstream is packed into 16-bit sample words so +//! > that the amplitude is reduced by 12 dB in the event that the +//! > data is inadvertently interpreted as uncompressed audio +//! > samples. The upper two bits are basically sign bit extension, +//! > as defined by twos-complement format. +//! +//! Each 16-bit container in a 14-bit-packed DTS stream carries 14 +//! bits of payload in the **lower** 14 bits; the upper two bits are +//! a sign-extension of bit 13 of the payload. The payloads of +//! successive containers concatenate MSB-first to form the same +//! bitstream that a raw 16-bit-packed DTS stream would carry. +//! +//! ## Verification with the documented sync sequences +//! +//! The wiki lists four sync byte sequences: +//! +//! ```text +//! raw BE : 7F FE 80 01 +//! raw LE : FE 7F 01 80 +//! 14-bit BE : 1F FF E8 00 07 Fx +//! 14-bit LE : FF 1F 00 E8 Fx 07 +//! ``` +//! +//! Reading the 14-bit BE prefix as three 16-bit BE words gives +//! `0x1FFF`, `0xE800`, `0x07Fx`. Masking each to its lower 14 bits +//! yields the 14-bit payloads `0x1FFF`, `0x2800`, `0x07Fx`. +//! Concatenating those three 14-bit values MSB-first produces a +//! 42-bit stream whose first 32 bits are `0x7FFE8001` — exactly the +//! raw BE syncword. The 14-bit LE form is identical except each +//! 16-bit container is byte-swapped before the lower-14 mask. This +//! is the contract the unpacker implements. +//! +//! ## Output shape +//! +//! For every `8` bytes of 14-bit-packed input (= four 16-bit +//! containers carrying 56 payload bits) the unpacker emits `7` +//! bytes (= 56 unpacked bits). The output is byte-aligned because +//! 14 × 4 = 56 is a multiple of 8; this is also the smallest such +//! cycle, so the unpacker walks the input four containers at a +//! time. Input lengths that are not a multiple of 8 are still +//! handled — any trailing fractional bits are flushed as a final +//! padding byte. +//! +//! The unpacker is non-allocating beyond a single `Vec` for the +//! result and is pure CPU (no I/O). +//! +//! ## What lives in `docs/` +//! +//! Only the wiki snapshot above. The 14-bit sign-extension rule and +//! the lower-14-bit payload convention are stated verbatim in that +//! file; no external library source was consulted to write this +//! unpacker. + +use crate::header::SyncWordEncoding; +use crate::{Error, Result}; + +/// Byte-order of the 14-bit-packed input. +/// +/// In `BigEndian` mode each pair of input bytes is read as a 16-bit +/// big-endian word; in `LittleEndian` mode each pair is read as +/// little-endian. The two forms differ only in the byte order of the +/// 16-bit containers — the payload extraction (lower 14 bits, +/// MSB-first concatenation) is identical for both. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FourteenBitByteOrder { + /// Big-endian container words (`1F FF E8 00 07 Fx`). + BigEndian, + /// Little-endian container words (`FF 1F 00 E8 Fx 07`). + LittleEndian, +} + +impl FourteenBitByteOrder { + /// Map a [`SyncWordEncoding`] to the corresponding byte order, + /// returning `None` for the two raw (non-14-bit) variants. + pub fn from_sync(sync: SyncWordEncoding) -> Option { + match sync { + SyncWordEncoding::FourteenBitBigEndian => Some(FourteenBitByteOrder::BigEndian), + SyncWordEncoding::FourteenBitLittleEndian => Some(FourteenBitByteOrder::LittleEndian), + _ => None, + } + } +} + +/// Pack a 16-bit-equivalent (raw big-endian) DTS byte buffer back into +/// the 14-bit-packed container form. +/// +/// This is the inverse of [`unpack_14bit_to_16bit`]. The input is read +/// as an MSB-first bit stream; successive 14-bit chunks are written +/// into the **lower** 14 bits of 16-bit containers, with the upper 2 +/// bits filled by a copy of payload bit 13 (so the resulting container +/// represents the 14-bit payload as a two's-complement value as the +/// wiki snapshot prescribes — "The upper two bits are basically sign +/// bit extension"). Each container is then emitted as two bytes in the +/// requested [`FourteenBitByteOrder`]. +/// +/// The input is treated as an opaque bit stream — there is no DTS +/// header awareness here. The number of payload bits packed is exactly +/// `input.len() * 8`; if that count is not a multiple of 14 the final +/// container is zero-padded on the right (least-significant bits of the +/// payload) and a documented `payload_bit_count` is returned alongside +/// the byte buffer so callers can recover the exact pre-pack bit +/// length on the receiving end if needed. +/// +/// The output length is exactly `ceil(input.len() * 8 / 14) * 2` bytes +/// — two bytes per container. For the four-byte raw-BE sync +/// `7F FE 80 01` (32 bits) this gives `ceil(32 / 14) = 3` containers = +/// 6 bytes, which matches the wiki's `1F FF E8 00 07 Fx` (BE) and +/// `FF 1F 00 E8 Fx 07` (LE) six-byte sync prefixes. +/// +/// ## Round-trip +/// +/// `unpack_14bit_to_16bit(pack_16bit_to_14bit(b, o).0, o)` returns a +/// buffer whose first `b.len()` bytes equal `b` (any trailing fractional +/// padding emitted by the pack step is consumed by the unpacker's +/// `bits_in_buf > 0` flush as the final padding byte). The empty input +/// yields the empty output and `payload_bit_count = 0`. +/// +/// The function is pure / side-effect free and allocates exactly one +/// `Vec`. +pub fn pack_16bit_to_14bit(input: &[u8], order: FourteenBitByteOrder) -> (Vec, usize) { + let payload_bits = input.len() * 8; + if payload_bits == 0 { + return (Vec::new(), 0); + } + let containers = payload_bits.div_ceil(14); + let mut out = Vec::with_capacity(containers * 2); + + // Walk the input as an MSB-first bit stream, emitting one + // 14-bit-payload container per iteration. `cursor` is the absolute + // bit offset into `input` of the next payload bit to consume. + let mut cursor: usize = 0; + for _ in 0..containers { + let mut payload: u16 = 0; + for bit_index in 0..14 { + let abs = cursor + bit_index; + let bit = if abs < payload_bits { + let byte = input[abs / 8]; + (byte >> (7 - (abs % 8))) & 1 + } else { + // Past the end — zero-pad the last container on the + // right (least-significant payload bits). + 0 + }; + payload = (payload << 1) | bit as u16; + } + cursor += 14; + + // Sign-extend bit 13 (the MSB of the 14-bit payload) into the + // upper 2 bits to satisfy the wiki's "sign bit extension" + // contract. `payload & 0x2000` is the sign bit; if set, set + // both bits 14 and 15; otherwise leave them clear. + let container: u16 = if payload & 0x2000 != 0 { + payload | 0xC000 + } else { + payload & 0x3FFF + }; + let bytes = match order { + FourteenBitByteOrder::BigEndian => container.to_be_bytes(), + FourteenBitByteOrder::LittleEndian => container.to_le_bytes(), + }; + out.extend_from_slice(&bytes); + } + + (out, payload_bits) +} + +/// Unpack a 14-bit-packed DTS byte buffer into the equivalent +/// 16-bit-packed (raw big-endian) byte buffer. +/// +/// Every pair of input bytes is read as a 16-bit container in the +/// requested [`FourteenBitByteOrder`]; the lower 14 bits of each +/// container are concatenated MSB-first and re-packed into a +/// big-endian byte stream. The output is suitable to feed straight +/// into [`crate::parse_frame_header`]. +/// +/// The input length must be even (each 14-bit container occupies +/// exactly two input bytes); an odd length returns +/// [`Error::UnexpectedEof`]. The empty input yields an empty +/// output. +/// +/// The function is pure / side-effect free and allocates exactly +/// one `Vec` whose final length is at most +/// `ceil(input.len() / 2 * 14 / 8)`. +pub fn unpack_14bit_to_16bit(input: &[u8], order: FourteenBitByteOrder) -> Result> { + if input.len() % 2 != 0 { + return Err(Error::UnexpectedEof); + } + let containers = input.len() / 2; + // Output capacity: 14 bits per container, rounded up to whole + // bytes. + let out_bits = containers * 14; + let out_bytes = out_bits.div_ceil(8); + let mut out = Vec::with_capacity(out_bytes); + + // Walk each container, accumulating the 14-bit payload into a + // little buffer; flush full bytes (MSB-first) as soon as the + // buffer holds >= 8 bits. + // + // `buf` holds up to 7+14 = 21 bits at the time of an accumulate; + // a u32 is comfortably wide enough. + let mut buf: u32 = 0; + let mut bits_in_buf: u32 = 0; + + for pair in input.chunks_exact(2) { + let word = match order { + FourteenBitByteOrder::BigEndian => u16::from_be_bytes([pair[0], pair[1]]), + FourteenBitByteOrder::LittleEndian => u16::from_le_bytes([pair[0], pair[1]]), + }; + // Lower 14 bits = the payload. The upper 2 bits are a + // sign-extension of bit 13 per the wiki and are discarded. + let payload = (word & 0x3FFF) as u32; + // Shift the existing buffer left to make room and OR in the + // new 14 bits. + buf = (buf << 14) | payload; + bits_in_buf += 14; + + // Flush whole bytes off the top of the buffer. + while bits_in_buf >= 8 { + let shift = bits_in_buf - 8; + let byte = ((buf >> shift) & 0xFF) as u8; + out.push(byte); + // Clear the bits we just emitted so subsequent shifts + // don't carry them along. + buf &= (1u32 << shift) - 1; + bits_in_buf -= 8; + } + } + + // Flush any trailing fractional bits as a final padding byte + // (MSB-aligned). After consuming containers in groups of four, + // bits_in_buf will be 0 at the boundary, so this branch only + // fires when containers % 4 != 0. + if bits_in_buf > 0 { + let byte = ((buf << (8 - bits_in_buf)) & 0xFF) as u8; + out.push(byte); + } + + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The 14-bit BE prefix from the wiki — three containers + /// `1F FF`, `E8 00`, `07 F0` — unpacks to the raw BE syncword + /// `7F FE 80 01` as the first 4 bytes, with `00` as the + /// fractional trailing byte (the low nibble of `07 F0` carries + /// the first 4 bits of the next payload field, which here is + /// zero in our padded fixture). + #[test] + fn unpack_be_syncword_prefix_yields_raw_be_sync() { + // Use 07 F0 (low nibble = 0) so the trailing bits flush to a + // clean `0x00` padding byte for the assertion. + let packed = [0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF0]; + let unpacked = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::BigEndian).unwrap(); + // 3 containers × 14 = 42 bits → ceil(42/8) = 6 bytes out. + assert_eq!(unpacked.len(), 6); + // First four bytes: the raw BE syncword. + assert_eq!(&unpacked[..4], &[0x7F, 0xFE, 0x80, 0x01]); + // Remaining 10 bits encode `0000_0001_11` from the bottom of + // payload #2 (`...01`) and the top of payload #3 + // (`00_0111_11`) → bits 32..42 = `0000_0001_1100_0111_11`. + // Splitting MSB-first into bytes 4..6: + // byte 4 = `0000_0001` = 0x01 + // byte 5 = `1100_0111` = 0xC7 + // wait — let's recompute carefully and assert by recompute + // instead of by hand here. + // The structural assertion is the syncword above; the + // trailing bytes are dictated by the unpacker contract and + // are exercised by the round-trip test below. + } + + /// The 14-bit LE prefix from the wiki — three containers + /// `FF 1F`, `00 E8`, `F0 07` — must unpack to the same raw BE + /// syncword as the BE prefix above (since the payloads are + /// identical; only container byte-order differs). + #[test] + fn unpack_le_syncword_prefix_yields_raw_be_sync() { + let packed = [0xFF, 0x1F, 0x00, 0xE8, 0xF0, 0x07]; + let unpacked = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::LittleEndian).unwrap(); + assert_eq!(&unpacked[..4], &[0x7F, 0xFE, 0x80, 0x01]); + } + + /// Round-trip a known bit-pattern: pack a stream of 14-bit + /// payloads MSB-first, then unpack and verify the lower-14-bit + /// chunks read back identically. This exercises the 4-container + /// (= 7-byte) alignment cycle without depending on hand-computed + /// trailing bytes. + #[test] + fn unpack_roundtrip_against_synthetic_payloads_be() { + let payloads: [u16; 8] = [ + 0x1FFF, 0x2800, 0x07F0, 0x1234, 0x0ABC, 0x3FFF, 0x0000, 0x2AAA, + ]; + // Pack: sign-extend each 14-bit payload into the upper 2 + // bits (mirroring the wiki's "sign bit extension" rule), then + // write as 16-bit BE. + let mut packed = Vec::with_capacity(payloads.len() * 2); + for &p in &payloads { + let signed = if p & 0x2000 != 0 { + 0xC000u16 | p + } else { + p & 0x3FFF + }; + packed.extend_from_slice(&signed.to_be_bytes()); + } + let unpacked = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::BigEndian).unwrap(); + // 8 containers × 14 = 112 bits → 14 bytes. + assert_eq!(unpacked.len(), 14); + // Walk the unpacked bit stream and verify each 14-bit slice. + for (i, &expected) in payloads.iter().enumerate() { + let start = i * 14; + let got = read_bits_msb(&unpacked, start, 14); + assert_eq!(got, expected as u32, "payload {i} mismatch"); + } + } + + /// Same payloads, LE container order. + #[test] + fn unpack_roundtrip_against_synthetic_payloads_le() { + let payloads: [u16; 8] = [ + 0x1FFF, 0x2800, 0x07F0, 0x1234, 0x0ABC, 0x3FFF, 0x0000, 0x2AAA, + ]; + let mut packed = Vec::with_capacity(payloads.len() * 2); + for &p in &payloads { + let signed = if p & 0x2000 != 0 { + 0xC000u16 | p + } else { + p & 0x3FFF + }; + packed.extend_from_slice(&signed.to_le_bytes()); + } + let unpacked = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::LittleEndian).unwrap(); + assert_eq!(unpacked.len(), 14); + for (i, &expected) in payloads.iter().enumerate() { + let start = i * 14; + let got = read_bits_msb(&unpacked, start, 14); + assert_eq!(got, expected as u32, "payload {i} mismatch"); + } + } + + /// The upper-2-bit sign extension is documented as informative + /// only — the unpacker MUST mask those bits away. Build a + /// container with junk in bits 14..16 and verify the same payload + /// is recovered. + #[test] + fn unpack_discards_upper_sign_bits() { + // Two payloads: 0x1FFF and 0x2800. We stuff garbage into + // the upper 2 bits of each container (0b11 and 0b10 + // respectively, neither of which matches the "correct" sign + // extension of `00` / `11` for these specific payloads). + let packed = [ + 0xDF, 0xFF, // (1<<15)|(1<<14)|0x1FFF = 0xDFFF — top bits garbage + 0xA8, 0x00, // (1<<15)|0x2800 = 0xA800 — top bit garbage + ]; + let unpacked = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::BigEndian).unwrap(); + // 2 containers × 14 = 28 bits → 4 bytes out. + assert_eq!(unpacked.len(), 4); + // Top 14 bits = 0x1FFF, next 14 bits = 0x2800. + assert_eq!(read_bits_msb(&unpacked, 0, 14), 0x1FFF); + assert_eq!(read_bits_msb(&unpacked, 14, 14), 0x2800); + } + + #[test] + fn unpack_odd_length_returns_eof() { + let packed = [0x1F, 0xFF, 0xE8]; + assert_eq!( + unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::BigEndian).unwrap_err(), + Error::UnexpectedEof, + ); + } + + #[test] + fn unpack_empty_yields_empty() { + let out = unpack_14bit_to_16bit(&[], FourteenBitByteOrder::BigEndian).unwrap(); + assert!(out.is_empty()); + } + + /// Four-container alignment: 4 × 14 = 56 bits = 7 bytes, no + /// trailing padding. + #[test] + fn unpack_four_container_alignment_emits_exactly_seven_bytes() { + let packed = [0x1F, 0xFF, 0xE8, 0x00, 0x07, 0xF0, 0x12, 0x34]; + let out = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::BigEndian).unwrap(); + assert_eq!(out.len(), 7); + } + + #[test] + fn from_sync_routes_correctly() { + assert_eq!( + FourteenBitByteOrder::from_sync(SyncWordEncoding::FourteenBitBigEndian), + Some(FourteenBitByteOrder::BigEndian), + ); + assert_eq!( + FourteenBitByteOrder::from_sync(SyncWordEncoding::FourteenBitLittleEndian), + Some(FourteenBitByteOrder::LittleEndian), + ); + assert_eq!( + FourteenBitByteOrder::from_sync(SyncWordEncoding::RawBigEndian), + None, + ); + assert_eq!( + FourteenBitByteOrder::from_sync(SyncWordEncoding::RawLittleEndian), + None, + ); + } + + // -------- pack_16bit_to_14bit tests (round 145) -------- + + /// The raw-BE syncword `7F FE 80 01` packed BE must reproduce the + /// first two containers of the wiki's 14-bit BE sync prefix bytes + /// `1F FF E8 00 07 Fx` — i.e. `1F FF E8 00`. The third container + /// `07 Fx` in the wiki notation includes 10 bits of the *next* + /// field after the 32-bit syncword (FTYPE / SHORT / CRC_PRESENT / + /// NBLKS_high), where the wiki example happens to have those 10 + /// bits start with `1_1111_1xxxx`. For a bare 32-bit syncword with + /// zero padding (no following header bits), container 3 is + /// `0001 0000 0000 00` = `0x0400`, BE = `04 00`. Together the + /// six bytes are `1F FF E8 00 04 00`. + #[test] + fn pack_be_raw_syncword_reproduces_wiki_14bit_be_prefix() { + let raw = [0x7F, 0xFE, 0x80, 0x01]; + let (packed, bits) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + // 32 input bits → ceil(32 / 14) = 3 containers → 6 bytes. + assert_eq!(packed.len(), 6); + assert_eq!(bits, 32); + assert_eq!(packed.as_slice(), &[0x1F, 0xFF, 0xE8, 0x00, 0x04, 0x00]); + } + + /// Same payload, LE container order — first two containers must + /// reproduce the wiki's LE prefix `FF 1F 00 E8`, with the third + /// container = `04 00` LE-swapped to `00 04`. Together: `FF 1F 00 + /// E8 00 04`. + #[test] + fn pack_le_raw_syncword_reproduces_wiki_14bit_le_prefix() { + let raw = [0x7F, 0xFE, 0x80, 0x01]; + let (packed, bits) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::LittleEndian); + assert_eq!(packed.len(), 6); + assert_eq!(bits, 32); + assert_eq!(packed.as_slice(), &[0xFF, 0x1F, 0x00, 0xE8, 0x00, 0x04]); + } + + /// Pack a 6-byte input whose first 4 bytes are the raw-BE + /// syncword and whose 5th byte's top nibble is `0xF` (i.e. the + /// next-field bits are `1111_1111 = 0xFF...`). This reproduces + /// the wiki's `0x07 0xFx` third container exactly because the 10 + /// bits after the syncword now genuinely are `11_1111_1xxx`. + #[test] + fn pack_be_with_wiki_post_sync_pattern_reproduces_07f_prefix() { + // After syncword, next 10 bits MUST be `11_1111_1xxx` to + // produce `0x07F`. Bit pattern: first byte after sync = + // `1111_1111`, next byte high 2 bits = `1x` (only the top + // matters for the 10-bit window — bits 32..42). + // Container 3 payload = bits 28..42: + // bits 28..32 of sync = 0b0001 + // bits 32..40 = first post-sync byte = 0xFF = 0b1111_1111 + // bits 40..42 = top 2 bits of second post-sync byte + // = 0b0001_1111_1111_<2 bits>. With top 2 bits = 0b00: + // payload = 0b0001_1111_1111_00 = 0x07FC. BE bytes: 0x07, + // 0xFC. + let raw = [0x7F, 0xFE, 0x80, 0x01, 0xFF, 0x00]; + let (packed, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + assert_eq!(&packed[..4], &[0x1F, 0xFF, 0xE8, 0x00]); + // Container 3 must satisfy the wiki's `0x07_F` pattern: top + // 12 bits = `0000_0111_1111` = 0x07F. + let container3 = u16::from_be_bytes([packed[4], packed[5]]); + assert_eq!(container3 >> 4, 0x07F); + } + + /// `unpack(pack(b))` recovers `b` on its first `b.len()` bytes for + /// every byte order. Inputs whose bit length is not a multiple of + /// 14 may have trailing padding bytes after the round-trip but the + /// first `b.len()` bytes must equal `b`. + #[test] + fn pack_then_unpack_roundtrip_be() { + let inputs: &[&[u8]] = &[ + &[], + &[0x7F, 0xFE, 0x80, 0x01], + &[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], + &[ + 0x7F, 0xFE, 0x80, 0x01, 0x80, 0x01, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, + 0x11, 0x22, + ], + ]; + for input in inputs { + let (packed, bits) = pack_16bit_to_14bit(input, FourteenBitByteOrder::BigEndian); + assert_eq!(bits, input.len() * 8); + let unpacked = unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::BigEndian).unwrap(); + assert!( + unpacked.len() >= input.len(), + "unpacked too short for input.len()={}: got {}", + input.len(), + unpacked.len() + ); + assert_eq!( + &unpacked[..input.len()], + *input, + "round-trip failed for input {input:?}" + ); + } + } + + /// Same as `pack_then_unpack_roundtrip_be` but with LE container + /// byte order. + #[test] + fn pack_then_unpack_roundtrip_le() { + let inputs: &[&[u8]] = &[ + &[], + &[0x7F, 0xFE, 0x80, 0x01], + &[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], + &[ + 0x7F, 0xFE, 0x80, 0x01, 0x80, 0x01, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, + 0x11, 0x22, + ], + ]; + for input in inputs { + let (packed, bits) = pack_16bit_to_14bit(input, FourteenBitByteOrder::LittleEndian); + assert_eq!(bits, input.len() * 8); + let unpacked = + unpack_14bit_to_16bit(&packed, FourteenBitByteOrder::LittleEndian).unwrap(); + assert!( + unpacked.len() >= input.len(), + "unpacked too short for input.len()={}: got {}", + input.len(), + unpacked.len() + ); + assert_eq!( + &unpacked[..input.len()], + *input, + "round-trip failed for input {input:?}" + ); + } + } + + /// `pack_16bit_to_14bit(b, BE)` and `pack_16bit_to_14bit(b, LE)` + /// differ only in the byte order of each container — pair-swapping + /// the BE output produces the LE output. + #[test] + fn pack_be_le_differ_only_by_container_byteswap() { + let raw = [0x7F, 0xFE, 0x80, 0x01, 0x12, 0x34, 0x56, 0x78]; + let (be, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::BigEndian); + let (le, _) = pack_16bit_to_14bit(&raw, FourteenBitByteOrder::LittleEndian); + assert_eq!(be.len(), le.len()); + for pair in 0..(be.len() / 2) { + assert_eq!(be[pair * 2], le[pair * 2 + 1]); + assert_eq!(be[pair * 2 + 1], le[pair * 2]); + } + } + + /// Empty input yields empty output and `payload_bit_count == 0` + /// (documented contract). + #[test] + fn pack_empty_yields_empty() { + let (out, bits) = pack_16bit_to_14bit(&[], FourteenBitByteOrder::BigEndian); + assert!(out.is_empty()); + assert_eq!(bits, 0); + } + + /// Confirm the sign-extension contract: for an input whose first + /// 14 bits have bit 13 (the payload sign bit) set, the upper 2 + /// bits of the corresponding container must be `0b11`. For an + /// input where bit 13 is clear, the upper 2 bits must be `0b00`. + #[test] + fn pack_sign_extends_top_payload_bit() { + // Sign bit set: 14 bits of `0b11_1111_0101_0101` = 0x3F55. + // Source bytes: top 14 bits of 0xFD_54_xx where: + // first byte = 0xFD = 0b1111_1101 + // second byte = 0x54 = 0b0101_0100 + // First 14 bits MSB-first: 0b11_1111_0101_0101 = 0x3F55. Good. + let raw_pos = [0xFD, 0x54]; + let (packed_pos, _) = pack_16bit_to_14bit(&raw_pos, FourteenBitByteOrder::BigEndian); + // First container: payload = 0x3F55, top 2 bits set → 0xFF55. + assert_eq!(u16::from_be_bytes([packed_pos[0], packed_pos[1]]), 0xFF55); + + // Sign bit clear: 14 bits of `0b00_1010_1010_1010` = 0x0AAA. + // First byte = 0b0010_1010 = 0x2A, second = 0b1010_1000 = 0xA8. + // First 14 bits: 0b00_1010_1010_1010 = 0x0AAA. Bit 13 = 0. + let raw_neg = [0x2A, 0xA8]; + let (packed_neg, _) = pack_16bit_to_14bit(&raw_neg, FourteenBitByteOrder::BigEndian); + // First container: payload = 0x0AAA, top 2 bits clear → 0x0AAA. + assert_eq!(u16::from_be_bytes([packed_neg[0], packed_neg[1]]), 0x0AAA); + } + + /// Helper: read `n` bits MSB-first starting at absolute bit + /// offset `start` from a big-endian byte stream. Mirrors the + /// `BitReader` semantics but is duplicated here to keep this + /// module testable in isolation. + fn read_bits_msb(bytes: &[u8], start: usize, n: usize) -> u32 { + let mut v: u32 = 0; + for i in 0..n { + let abs = start + i; + let byte = bytes[abs / 8]; + let bit = (byte >> (7 - (abs % 8))) & 1; + v = (v << 1) | bit as u32; + } + v + } +} diff --git a/crates/vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin b/crates/vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin new file mode 100644 index 0000000000000000000000000000000000000000..9f2509948d8c4aa6f4ca806ffb12bf3f088c9a41 GIT binary patch literal 5120 zcmaKwc{tSF`^P`CFl3k!F(HGji6MH(He(5utrab1sL)Eu6NMQ@D1=fe+1q)d?LlE? zgoF%Is5E2AQpV2g^Z8BB_xWAV_qx7)zxQ9~e%<$VpU=6j^S;kHAF_l35jo&zU$7yz z0BnZLmi?@aohOSZ000I4HUr}pbkwb}jhXvYpWkW0xS*4DBxux{n za-WCml8{>jeiY`vQ_z4^rWSD^1>95YWEi;=c3N( zrb&wc#4awU=4TYfPYlXS`3tdU_*MTxl&{5Gr3eb>K=Rk7EB}3JfHR zyaKqRLU2sN>IWE|Z>Gq^VX&wd@-YYha2UY0u7tYID09G~?*Lu|9ny1>&=o{5ut@?o zi^aw2eBpEmji$~8r9L){A&QqS8xS-C(O`Tc#Kk#*goc44@Wkg*COrX;?NS&;0}w^_ zUSIJr$6?F3i*7fCOed{Zg3{SKa&k~I?|fsD8-8WO^@=@0%#tnl4=mPUy8VVyI% zz-g<QwsoV&*DsM~6}z4(*w)nSGBrVo;L|qKhpN*u;>ZTK=e3th7F*YMYrWm1 z`XVg#SrE4+?`oju8UsKpeSeH>~vkNcfjqe=5~ki+hGh5;kv z!MQVx@uh6P1ysh;*HRd1Qf<~l%uxQ}0N@c2=9sur1Cc;Hv4UJB%hf8o`z7{3vFaRD z50k#cz=lUNy}^ikNI9LTmKF1q-3?#+YP5}If&EEuA|U4ob|$km&;v_ zYbVgpS#0vufQKCXviaarvcf9}^&KtjjlIHmdY5(bE=-)4S{(keyg0^jj;h{%!Q!|s z88i`ygm0DsrOyWzsHCPG2bWB$1l^>{Q{S-MDY8+sXEIN)V9!?jyC&lQ3|L;B95u4s zCfF^jrY50Pq59U}2=IM~N{o?^eC(2AB6G@KrDgeB2Ez$XL0y47`_yA)In%S7i0I=L z1uDU|wBlqzU)Pm76CcSw@vAXKL-ZO9MXU42#s0L=6)S6ZBqh%!#6%YFtvTAfXq06@ zZBi6&bLt;?)Rm7Ys?yF-+98{W$>S;qh=ZZibkah^z+7jepvqcNcamY_8B zX=3zzosxf(rm2xzzYlIg*XhNge&vdtwCmtWK6Ohn!d(Y}2kF1iKNg%c04D)J=Pxl! z4~2y|9qz`8gK94xZguW(hU?0?pi_RqlK(&dQfW%2skQS6Sfi&;> zCLIa=U$F)JS0hh(my$HLXl(cLvis_qk%pa^IY%9E@S?21(mg ztHLd6fg3Fnx<^S!06#`=SM;=}hb_)zijkX=h2xAW-S{e891pkLm4QCj&yA*)~-+Hjza$edUw0WLSQ(?`VZPd>r!*EswQntLNlj0=5Poe0jyV zxc{2{!Wuo~@`FEt*7fk}=rMAgPxi_SuF4F4M>%II!|K$V?nmq#GccQ)SX-D&R7m7h z4rikgNq%WCbku318GMGE??hr*D;)!n-z8qx$UaM9PO>xwm?(KJlS*x<)6O-QWR-ty2`by8tlHbRf zCH2nu$0AK+eHq*)|3~Xs+-2n;>@9?{)65~AjZPTdc~oQmB%4u%p}u~J!JRYObZdR^ zr?+>guiD3LyeIZ5Ubsenq@;IfBPBC2ytjcwd3uT)<@$Kgci-fu3e&@7D7D(ynZdpX z*?02F2D-n(Ui~I;KA7@!+pHRSlI)Fa16IG_y9Y4JPWg3og z(QKAJQ&{kMm(s%OyAB(p? zu6o*?n!dk|JN={AbBEXVsfiiLV1B&Llh=U;JO%vXtiI(sn4#rsv&Dv}hntOawBWh8 zbWrpw{Yz+f1w_if=|76i-S~%8|9`C8(%~n){QuPdf3knm{{_XL`k$l9q+=`s=1N;o z7(yasR5l&C;!^2;Q8;ZC`=O_;Qf575;dh>AN4dmR0+WM;>zvJiDZu`=Eq5?&(u0a_(pD(o04X92$ z>EecV^YftC5NR33Lo;7;qi$3dzfCTW^T_MR*$k~gX`evNRv`xly2K@`?XGo&zI0y7snoB`P_2F(ns;-Z0+Ahc z;Ww?SW9HLO`8e(Zjk_#nj9*JDI;FtXPnx-zLDWn7f~Ic=Q)$5S6{KC_fbD#iIVC&} zkVA0M`^Lz&(5Vn>RZXnTdkH7V0+C)oinz7+stkC~cm777&C_#(9uoZ{=Wk`cNSs;b zLPlG0Q7xmUYO`joo=I|-KmxO-@(HW`x0Ws&kUMrK3F(7Ko_8(V-;37KLF&e;L zptj!GEqK6nDTW4la7ZbbL-dBkPBB%33bw&c_`yP0kaDL|L)8q+5Xz+A&}uH>A&)uPXuJ^t;%CS@du1D|RN_@VYj~MX0Vk7O zpfkB#t1%jP1)fET=xestfZf&>_wibaxT|%-hUGwOIQ8;E1^g8YnXyV>6}GU>nRwe8 zUQe?vmlcA3(6J>M<@FiCu8CTzUqU473{Qsu`%;%QK)=C?|T@w?R6@Om1}0EZrWYiRq$LJl@( zU>rzKMq5_ux!r_}$Rf4PFa7tJ+0e@f<#Uyy+jf`(YuI-xs_F)glFv>J$XvX4IfQuu z8FBwi8el6Qu|OLJHUX9pQ%SjyD9YAg_0U7e52Z`V8gb(C)CU=}GHeBK{TkYR8SJR@ zX(&mb^4`7k^`LkyC%zC@c&;e5+0nlTDM4+v_ZsXtwYjFTI&T=v+HWzB$U>`G$s?W< z6E@DYDvaE6Vp$H{`$fmriNTdHsE06J&fB8UQKmXD63Pwc3nN+%y(Lx zuk*yZyCZP6By({(2S7l~BxgkggpRC^0r>;@7B7#>@rcBIl&8_tr0%Zf1Y+HzVcb~0(0jq@BSl!a}i_W{*nCJ%f{81JqudIxgROo zgt=j8OR&yz`bG$$L|j02x|0fE4%2Xbz`!6Hg`} z;gD-c%Fgp?Zm2~v79@X%R_zpy9X)Y8Z??`HR??{KdwAY?qLJ@d0o5PcZ@9X3us5r@ zHv(U$E?-c$`^T5Z^4B$mGt_(31hW4gm>{|HWKd3s;ZsGVm-uX()@hw+O%6rAdz61I zU3=55HC4Yeb&E?6s|W4 zZ9>~+F4N%?^KorYHnbG&+jV+Cx!`_Zm3r_TVt&g)JAFlX*r|I{Ss@OGclW*7{qkZf Q1c/src` stays meaningful and a +# refresh is a re-run of this script rather than a merge. Never hand-edit +# anything under crates/vendor: change it upstream and re-vendor. +# +# Usage: +# ./scripts/vendor-oxideav.sh # re-vendor at the pinned revisions +# ./scripts/vendor-oxideav.sh --update # move the pins to upstream HEAD +# +# After --update, review the diff and run the tests: these are decoders, and a +# regression is silent audio rather than a build failure. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENDOR="$ROOT/crates/vendor" +CACHE="$ROOT/target/vendor-src" +SELF="$ROOT/scripts/vendor-oxideav.sh" + +# The pinned revisions. `--update` rewrites these lines in place. +PIN_oxideav_core=defa866dffdd224424d75ac7a38be868723395a5 +PIN_oxideav_ac3=8acf106d50d58f359946c086d1b393a060eaf5f6 +PIN_oxideav_dts=528203ed608223c5137843009054e05920af5c50 +PIN_oxideav_aac=719f1f594aef3465ecf2d718685bc27f8e797423 + +CRATES="oxideav-core oxideav-ac3 oxideav-dts oxideav-aac" + +UPDATE=0 +[ "${1:-}" = "--update" ] && UPDATE=1 + +command -v git >/dev/null 2>&1 || { echo -e "${RED}[ERROR]${NC} git is required."; exit 1; } +command -v rsync >/dev/null 2>&1 || { echo -e "${RED}[ERROR]${NC} rsync is required."; exit 1; } + +mkdir -p "$CACHE" "$VENDOR" + +for crate in $CRATES; do + pin_var="PIN_$(echo "$crate" | tr '-' '_')" + pin="${!pin_var}" + src="$CACHE/$crate" + dst="$VENDOR/$crate" + + echo -e "${BLUE}[INFO]${NC} $crate" + + if [ -d "$src/.git" ]; then + git -C "$src" fetch --quiet origin + else + git clone --quiet "https://github.com/OxideAV/$crate.git" "$src" + fi + + if [ "$UPDATE" = "1" ]; then + branch="$(git -C "$src" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')" + branch="${branch:-master}" + pin="$(git -C "$src" rev-parse "origin/$branch")" + # Rewrite our own pin line so the new revision is committed with the code. + sed -i.bak "s|^${pin_var}=.*|${pin_var}=${pin}|" "$SELF" && rm -f "$SELF.bak" + echo -e "${YELLOW}[PIN]${NC} $crate → ${pin:0:7}" + fi + + git -C "$src" checkout --quiet "$pin" + + # Only src/ and the legal/provenance files. Upstream's tests/ directory + # carries whole fixture corpora (350 KB for dts alone) and its benches pull + # criterion; neither ships, and neither is ours to maintain. + rm -rf "$dst" + mkdir -p "$dst" + rsync -a --delete "$src/src/" "$dst/src/" + cp "$src/LICENSE" "$dst/LICENSE" + cp "$src/README.md" "$dst/README.md" + + # The inline #[cfg(test)] modules inside src/ do come along, and some of them + # `include_bytes!` a fixture out of tests/. Carry exactly those files — a few + # KB — so `cargo test -p ` still verifies the decoder we ship against + # real bitstreams. Discovered by grep rather than listed, so a refresh that + # adds a fixture picks it up instead of failing to build. + grep -rhoE 'include_bytes!\("\.\./tests/[^"]+"\)' "$dst/src" 2>/dev/null \ + | sed -E 's|include_bytes!\("\.\./||; s|"\)$||' | sort -u \ + | while read -r fixture; do + [ -f "$src/$fixture" ] || { echo -e "${RED}[ERROR]${NC} missing $crate/$fixture"; exit 1; } + mkdir -p "$dst/$(dirname "$fixture")" + cp "$src/$fixture" "$dst/$fixture" + echo " fixture: $fixture ($(wc -c < "$src/$fixture" | tr -d ' ') bytes)" + done + + # The manifest is upstream's, with three mechanical changes: sibling + # oxideav deps become path deps, publishing is off (we do not own these + # names on crates.io), and lints are allowed because normalising 6 MB of + # foreign code to our clippy settings would destroy the diffability that is + # the whole reason for vendoring verbatim. + awk ' + /^\[dev-dependencies\]/ { skip = 1; next } + /^\[\[bench\]\]/ { skip = 1; next } + /^\[/ { skip = 0 } + skip { next } + /^name = / && !done_pub { print; print "publish = false"; done_pub = 1; next } + { print } + ' "$src/Cargo.toml" \ + | sed -E 's|^(oxideav-[a-z0-9]+) = "[^"]*"$|\1 = { path = "../\1" }|' \ + | sed -E 's|^(oxideav-[a-z0-9]+) = \{ version = "[^"]*", (.*)\}$|\1 = { path = "../\1", \2}|' \ + > "$dst/Cargo.toml" + + cat >> "$dst/Cargo.toml" <<'LINTS' + +# Vendored verbatim — see scripts/vendor-oxideav.sh. Upstream does not build +# under this repository's `-D warnings`, and making it would mean carrying a +# patch set across every refresh. +[lints.rust] +warnings = "allow" + +[lints.clippy] +all = "allow" +LINTS + + { + echo "# Written by scripts/vendor-oxideav.sh. Do not edit, and do not" + echo "# hand-edit the vendored sources beside it — change them upstream" + echo "# and re-run the script." + echo "source = \"https://github.com/OxideAV/$crate\"" + echo "commit = \"$pin\"" + echo "describe = \"$(git -C "$src" describe --tags --always 2>/dev/null || echo unknown)\"" + echo "version = \"$(sed -n 's/^version = "\(.*\)"$/\1/p' "$src/Cargo.toml" | head -1)\"" + echo "vendored_at = \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"" + echo "patches = []" + } > "$dst/VENDOR.toml" + + echo -e " ${GREEN}✓${NC} ${pin:0:7} → crates/vendor/$crate ($(du -sh "$dst/src" | cut -f1))" +done + +echo -e "${GREEN}[SUCCESS]${NC} Vendored $(echo $CRATES | wc -w | tr -d ' ') crates." + +cd "$ROOT" +if git rev-parse --git-dir >/dev/null 2>&1; then + if [ -z "$(git status --porcelain -- crates/vendor scripts/vendor-oxideav.sh)" ]; then + echo -e "${BLUE}[INFO]${NC} Unchanged; nothing to commit." + else + echo -e "${BLUE}[INFO]${NC} The vendored tree changed — review and commit it." + fi +fi From b5bde6a60d43748857b8fa5dc13e0f94227f136e Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 12:04:18 +0300 Subject: [PATCH 02/38] feat(transcode): decode AC-3, E-AC-3 and DTS to linear PCM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decode half of the feature: identify the three codecs, index an elementary stream's frames, and drive the vendored decoders to interleaved S16. `TranscodeCodec` is static identification and compiles in every build, so a server without a decoder still knows what an AC-3 track is and can say so honestly. Only the decode path is behind `transcode-ac3` / `transcode-dts` — the same split `mediainfo` draws between knowing what a provider is and being able to call one. `is_decodable()` is what lets the DIDL writer ask "can this build handle this?" without carrying a pile of #[cfg] at the call site. The index reads headers only and streams the file through a fixed buffer: each frame declares its own length, so the walk hops frame to frame without decoding, and the AC-3 track of a two-hour film is around 170 MB. It exists to make the transcoded resource *seekable* — decoded PCM is constant-bitrate, so an exact sample count is an exact Content-Length and a byte offset divides back into a frame. Leading tags and damaged frames resync rather than failing the file. AC-3 and E-AC-3 share a syncword and are told apart by bsid, which both syntaxes place at bit 40; base AC-3 reaches it through crc1+fscod+frmsizecod and Annex E through strmtyp+substreamid+frmsiz+fscod+numblkscod+acmod+lfeon. One probe serves both, so a stream may even switch mid-file. DTS frames carry raw NBLKS and run (NBLKS + 1) x 32 samples. Channel count is measured from a decoded frame rather than predicted from acmod: what matters downstream is what the decoder actually emits after its downmix. That downmix is the decoder's own, because AC-3 carries the §7.8 coefficients in the bitstream — summing channels ourselves would discard the mix the encoder intended and clip besides. A frame that fails to decode yields silence of the length the index promised. The Content-Length is already committed by then, so a decode error has to cost a tick rather than a truncated download. Verified: 16 tests, including both fixtures decoding to exactly the byte count their index predicted and to non-silent audio. Builds clean with no features, with `transcode` alone, and with each decoder alone. --- crates/vuio-core/Cargo.toml | 42 ++ crates/vuio-core/src/media.rs | 2 + .../vuio-core/src/media/transcode/frames.rs | 390 ++++++++++++++++++ crates/vuio-core/src/media/transcode/mod.rs | 161 ++++++++ crates/vuio-core/src/media/transcode/pcm.rs | 224 ++++++++++ crates/vuio-core/src/media/transcode/wav.rs | 113 +++++ 6 files changed, 932 insertions(+) create mode 100644 crates/vuio-core/src/media/transcode/frames.rs create mode 100644 crates/vuio-core/src/media/transcode/mod.rs create mode 100644 crates/vuio-core/src/media/transcode/pcm.rs create mode 100644 crates/vuio-core/src/media/transcode/wav.rs diff --git a/crates/vuio-core/Cargo.toml b/crates/vuio-core/Cargo.toml index 1f6c97c1..7ef784ac 100644 --- a/crates/vuio-core/Cargo.toml +++ b/crates/vuio-core/Cargo.toml @@ -38,6 +38,9 @@ default = [ "mcp", "metadata", "mediainfo", + "transcode-ac3", + "transcode-dts", + "transcode-aac", "web-ui", ] @@ -90,6 +93,36 @@ web-ui = ["dep:vuio-web", "dashboard"] # the feature, and nothing here opens a socket until the operator presses the # button. Drop it for a build that must never reach the internet. mediainfo = ["dep:reqwest"] + +# Decode AC-3, E-AC-3 and DTS so a TV that cannot play them is offered a second, +# already-decoded resource beside the original and produces sound. +# +# On by default for the reason at the top of this section: the person whose TV +# plays a film silently is exactly the person who will never think to recompile, +# so shipping this off would mean it helps almost nobody. The flags are here so a +# deployment that knows its renderers — a vehicle, an appliance, a Chromecast-only +# install — can drop ~6 MB of decoder it will never reach. +# +# `transcode` is the plumbing with no decoder attached: the config section, the +# second ``, the WAV writer. Nothing selects it alone, but it is what makes +# `--no-default-features --features transcode` build, and it keeps the CI +# feature-matrix honest about which half of this is codec and which is server. +# It carries `oxideav-core` because that crate is the trait and type layer — +# `Decoder`, `Packet`, `AudioFrame`, `SampleFormat` — with no codec in it, and +# the plumbing is written against those types whether or not a decoder exists to +# hand it any. +transcode = ["dep:oxideav-core"] + +# One crate, one decoder, both codecs: E-AC-3 is Annex E of the same A/52 +# specification and `Ac3Decoder` dispatches on the per-packet bsid. A separate +# `eac3` flag would gate a match arm and save nothing. +transcode-ac3 = ["transcode", "dep:oxideav-ac3"] +transcode-dts = ["transcode", "dep:oxideav-dts"] + +# The AAC-LC encoder, for `[transcode] audio_format = "aac"`. LPCM is the +# default output and needs no encoder, so this is separable — but it is also +# what the HLS audio path will re-encode into, so it ships on. +transcode-aac = ["transcode", "dep:oxideav-aac"] unstable-internals = [] [dependencies] @@ -144,6 +177,15 @@ symphonia = { version = "0.6", default-features = false, features = ["all"], opt getrandom = { version = "0.4", optional = true } plist = { version = "1.10", optional = true } hex = { version = "0.4", optional = true } +# The AC-3 / E-AC-3 / DTS decoders and the AAC-LC encoder, vendored verbatim +# under crates/vendor by `scripts/vendor-oxideav.sh`. Path deps with no version: +# these are our copies, not registry crates, and `publish = false` on each says +# so. They add no external crates — thiserror, serde_json and bytemuck were +# already in the tree. +oxideav-core = { path = "../vendor/oxideav-core", optional = true } +oxideav-ac3 = { path = "../vendor/oxideav-ac3", optional = true } +oxideav-dts = { path = "../vendor/oxideav-dts", optional = true } +oxideav-aac = { path = "../vendor/oxideav-aac", optional = true } rusqlite = { version = "0.40.2", features = ["bundled", "collation"] } # Only the `mediainfo` feature uses this, and only to talk to public metadata APIs # over TLS. `rustls-no-provider` rather than a provider-selecting feature because diff --git a/crates/vuio-core/src/media.rs b/crates/vuio-core/src/media.rs index ddc03b03..386a70de 100644 --- a/crates/vuio-core/src/media.rs +++ b/crates/vuio-core/src/media.rs @@ -20,6 +20,8 @@ mod policy; pub mod remux; mod result; mod scanner; +#[cfg(feature = "transcode")] +pub mod transcode; pub use policy::ScanPolicy; #[cfg(test)] diff --git a/crates/vuio-core/src/media/transcode/frames.rs b/crates/vuio-core/src/media/transcode/frames.rs new file mode 100644 index 00000000..b9699f2f --- /dev/null +++ b/crates/vuio-core/src/media/transcode/frames.rs @@ -0,0 +1,390 @@ +//! Locating compressed frames in an elementary stream. +//! +//! A transcoded resource has to answer two questions before a single sample is +//! decoded: how long is it, and where do I start for a byte range? Both come +//! from this index. Decoded PCM is constant-bitrate, so an exact total sample +//! count is an exact `Content-Length`, and a byte offset divides straight back +//! into a sample — which is what lets the transcoded resource support seeking +//! instead of being a one-shot chunked stream. +//! +//! Building it reads headers only. Each frame's header declares its own byte +//! length, so the walk hops frame to frame without decoding anything, and the +//! file is streamed through a fixed buffer rather than read into memory — the +//! AC-3 track of a two-hour film is around 170 MB. + +use anyhow::{bail, Context, Result}; +use std::io::Read; + +use super::TranscodeCodec; + +/// How much of the file to hold at once while walking headers. +/// +/// Only has to exceed the largest legal frame (DTS caps at 16 384 bytes) by +/// enough that the walk is not re-filling constantly. +const CHUNK: usize = 256 * 1024; + +/// Bytes of header needed to determine a frame's length and duration. +/// AC-3/E-AC-3 need six (bsid lives at byte 5); DTS needs its full 14-byte +/// header, and `parse_frame_header` is handed more than that anyway. +const MIN_HEADER: usize = 16; + +/// One compressed frame located in the stream. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IndexedFrame { + /// Byte offset of the frame's syncword from the start of the stream. + pub offset: u64, + /// Frame length in bytes, as the frame's own header declares it. + pub len: u32, + /// Sample frames this yields when decoded — per channel, not per sample. + pub samples: u32, +} + +/// Every frame of one elementary stream, with the totals that describe it. +#[derive(Debug, Clone)] +pub struct FrameIndex { + /// Which codec the frames are in. + pub codec: TranscodeCodec, + /// Sample rate declared by the first frame, in Hz. + pub sample_rate: u32, + /// The frames, in stream order. + pub frames: Vec, + /// Sum of every frame's `samples` — the exact decoded length. + pub total_samples: u64, +} + +impl FrameIndex { + /// Walk `reader` from its current position, indexing every frame. + /// + /// Bytes that do not parse as a frame header are skipped by scanning for the + /// next syncword: an elementary file can open with an ID3 tag, and a stream + /// pulled off a disc can carry a damaged frame in the middle. Neither should + /// cost the whole file. + pub fn build(codec: TranscodeCodec, reader: &mut R) -> Result { + let mut buf: Vec = Vec::with_capacity(CHUNK); + let mut frames = Vec::new(); + let mut total_samples: u64 = 0; + let mut sample_rate = 0u32; + // Absolute offset of buf[0] within the stream. + let mut base: u64 = 0; + let mut pos: usize = 0; + let mut eof = false; + + loop { + // Refill: drop what has been consumed, then top up to CHUNK. + if pos > 0 { + buf.drain(..pos); + base += pos as u64; + pos = 0; + } + while !eof && buf.len() < CHUNK { + let mut tmp = [0u8; 64 * 1024]; + let n = reader.read(&mut tmp).context("reading elementary stream")?; + if n == 0 { + eof = true; + break; + } + buf.extend_from_slice(&tmp[..n]); + } + if buf.len().saturating_sub(pos) < MIN_HEADER { + break; + } + + match parse_header(codec, &buf[pos..]) { + Ok(Some(hdr)) => { + // A frame whose tail is past the buffer end needs a refill, + // unless there is nothing left to read — then it is truncated + // and the index simply stops before it. + if pos + hdr.len as usize > buf.len() { + if eof { + break; + } + // Force a drain-and-refill without consuming the header. + if pos == 0 { + // Already at the front and still short: the declared + // length exceeds anything legal, so resync past it. + pos += 2; + continue; + } + buf.drain(..pos); + base += pos as u64; + pos = 0; + while !eof && buf.len() < CHUNK { + let mut tmp = [0u8; 64 * 1024]; + let n = reader.read(&mut tmp).context("reading elementary stream")?; + if n == 0 { + eof = true; + break; + } + buf.extend_from_slice(&tmp[..n]); + } + continue; + } + if sample_rate == 0 { + sample_rate = hdr.sample_rate; + } + frames.push(IndexedFrame { + offset: base + pos as u64, + len: hdr.len, + samples: hdr.samples, + }); + total_samples += u64::from(hdr.samples); + pos += hdr.len as usize; + } + Ok(None) | Err(_) => { + // Not a frame here. Scan for the next syncword rather than + // giving up: leading tags and single damaged frames are both + // recoverable, and a file that is not this codec at all + // simply produces no frames and is rejected below. + match next_sync(codec, &buf[pos + 1..]) { + Some(skip) => pos += 1 + skip, + None => { + pos = buf.len().saturating_sub(MIN_HEADER.saturating_sub(1)); + if eof { + break; + } + } + } + } + } + } + + if frames.is_empty() { + bail!("no {} frames found in the stream", codec.as_str()); + } + if sample_rate == 0 { + bail!("{} stream declares no usable sample rate", codec.as_str()); + } + + Ok(Self { + codec, + sample_rate, + frames, + total_samples, + }) + } + + /// Duration in seconds, from the exact sample count. + pub fn duration_secs(&self) -> f64 { + self.total_samples as f64 / self.sample_rate as f64 + } + + /// Index of the frame containing `sample`, and the samples to discard from + /// its front to land exactly on `sample`. + /// + /// Returns the last frame when `sample` is past the end, so a range request + /// beyond the stream produces silence rather than an error. + pub fn locate(&self, sample: u64) -> (usize, u32) { + let mut acc: u64 = 0; + for (i, f) in self.frames.iter().enumerate() { + let next = acc + u64::from(f.samples); + if sample < next { + return (i, (sample - acc) as u32); + } + acc = next; + } + (self.frames.len().saturating_sub(1), 0) + } +} + +/// What a frame header tells us. +struct Header { + len: u32, + samples: u32, + sample_rate: u32, +} + +#[cfg(feature = "transcode-ac3")] +/// Blocks per E-AC-3 syncframe, indexed by `numblkscod` (Table E1.2). +const EAC3_BLOCKS: [u32; 4] = [1, 2, 3, 6]; +#[cfg(feature = "transcode-ac3")] +/// Samples per audio block — the 256-point half of the 512-point TDAC window. +const SAMPLES_PER_BLOCK: u32 = 256; +#[cfg(feature = "transcode-ac3")] +/// §E.2.3.1.4 `fscod2` rates, used only when `fscod == 3` (half-rate streams). +const EAC3_HALF_RATES: [u32; 3] = [24_000, 22_050, 16_000]; +#[cfg(feature = "transcode-ac3")] +/// Base A/52 `fscod` rates (Table 5.6). +const AC3_RATES: [u32; 3] = [48_000, 44_100, 32_000]; + +/// A codec this build has no decoder for is also a codec it will not index: +/// an index exists to promise a `Content-Length` we can then deliver, and +/// promising one we cannot decode would be worse than declining up front. +#[cfg(not(feature = "transcode-ac3"))] +fn parse_ac3_family(_data: &[u8]) -> Result> { + bail!("this build of vuio-core was compiled without the `transcode-ac3` feature") +} + +#[cfg(not(feature = "transcode-dts"))] +fn parse_dts(_data: &[u8]) -> Result> { + bail!("this build of vuio-core was compiled without the `transcode-dts` feature") +} + +fn parse_header(codec: TranscodeCodec, data: &[u8]) -> Result> { + match codec { + TranscodeCodec::Ac3 | TranscodeCodec::Eac3 => parse_ac3_family(data), + TranscodeCodec::Dts => parse_dts(data), + } +} + +/// AC-3 and E-AC-3 share a syncword and are told apart by `bsid`, which both +/// syntaxes place at bit 40 — byte 5's top five bits. Base AC-3 gets there via +/// `crc1(16) fscod(2) frmsizecod(6)`, Annex E via +/// `strmtyp(2) substreamid(3) frmsiz(11) fscod(2) numblkscod(2) acmod(3) lfeon(1)`. +/// Both land on 40, so one probe serves both and a stream may even switch. +#[cfg(feature = "transcode-ac3")] +fn parse_ac3_family(data: &[u8]) -> Result> { + if data.len() < 6 { + return Ok(None); + } + if data[0] != 0x0B || data[1] != 0x77 { + return Ok(None); + } + let bsid = data[5] >> 3; + + if bsid <= oxideav_ac3::eac3::bsi::BSID_BASE_AC3_MAX { + // Base AC-3: the vendored parser owns Table 5.18. + let si = oxideav_ac3::syncinfo::parse(data) + .map_err(|e| anyhow::anyhow!("ac3 syncinfo: {e}"))?; + return Ok(Some(Header { + len: si.frame_length, + // Six blocks, always, in base AC-3 (§2.2). + samples: 6 * SAMPLES_PER_BLOCK, + sample_rate: si.sample_rate, + })); + } + if bsid > 16 { + return Ok(None); + } + + // Annex E, Table E1.2. + let frmsiz = (u32::from(data[2] & 0x07) << 8) | u32::from(data[3]); + let len = (frmsiz + 1) * 2; + let fscod = (data[4] >> 6) & 0x03; + let next2 = (data[4] >> 4) & 0x03; + let (sample_rate, blocks) = if fscod == 3 { + // fscod2 replaces numblkscod, and the block count is implicitly six. + let Some(rate) = EAC3_HALF_RATES.get(next2 as usize) else { + return Ok(None); + }; + (*rate, 6) + } else { + (AC3_RATES[fscod as usize], EAC3_BLOCKS[next2 as usize]) + }; + + Ok(Some(Header { + len, + samples: blocks * SAMPLES_PER_BLOCK, + sample_rate, + })) +} + +#[cfg(feature = "transcode-dts")] +fn parse_dts(data: &[u8]) -> Result> { + let hdr = match oxideav_dts::parse_frame_header(data) { + Ok(h) => h, + Err(_) => return Ok(None), + }; + let Some(sample_rate) = hdr.sample_rate_hz() else { + return Ok(None); + }; + // `blocks_per_frame` carries the raw NBLKS field; the block count is + // NBLKS + 1 and each block is 32 PCM samples (§5.3.1, and the same + // arithmetic the vendored crate uses for its Rev2 subsubframe count). + let blocks = u32::from(hdr.blocks_per_frame) + 1; + Ok(Some(Header { + len: u32::from(hdr.frame_size_bytes), + samples: blocks * 32, + sample_rate, + })) +} + +/// Offset of the next plausible syncword in `data`, if any. +#[allow(unused_variables)] +fn next_sync(codec: TranscodeCodec, data: &[u8]) -> Option { + match codec { + #[cfg(feature = "transcode-ac3")] + TranscodeCodec::Ac3 | TranscodeCodec::Eac3 => { + oxideav_ac3::syncinfo::find_syncword(data, 0) + } + #[cfg(feature = "transcode-dts")] + TranscodeCodec::Dts => oxideav_dts::find_next_sync(data, 0).map(|m| m.offset), + #[allow(unreachable_patterns)] + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "transcode-ac3")] + const AC3_FIXTURE: &[u8] = + include_bytes!("../../../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); + #[cfg(feature = "transcode-dts")] + const DTS_FIXTURE: &[u8] = + include_bytes!("../../../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); + + #[cfg(feature = "transcode-ac3")] + #[test] + fn indexes_every_frame_of_a_real_ac3_stream() { + let idx = FrameIndex::build(TranscodeCodec::Ac3, &mut &AC3_FIXTURE[..]).unwrap(); + assert_eq!(idx.sample_rate, 48_000); + // 48 kHz / 192 kbps → Table 5.18 frmsizecod 20 → 768-byte frames. + assert!(idx.frames.len() >= 4, "got {} frames", idx.frames.len()); + for f in &idx.frames { + assert_eq!(f.len, 768); + assert_eq!(f.samples, 1536, "base AC-3 is always six 256-sample blocks"); + } + // Offsets must be contiguous — no gaps, no overlap. + for pair in idx.frames.windows(2) { + assert_eq!(pair[0].offset + u64::from(pair[0].len), pair[1].offset); + } + assert_eq!(idx.total_samples, 1536 * idx.frames.len() as u64); + } + + #[cfg(feature = "transcode-dts")] + #[test] + fn indexes_every_frame_of_a_real_dts_stream() { + let idx = FrameIndex::build(TranscodeCodec::Dts, &mut &DTS_FIXTURE[..]).unwrap(); + assert_eq!(idx.frames.len(), 5, "the fixture is five frames"); + assert!(idx.sample_rate > 0); + for pair in idx.frames.windows(2) { + assert_eq!(pair[0].offset + u64::from(pair[0].len), pair[1].offset); + } + assert_eq!( + idx.total_samples, + idx.frames.iter().map(|f| u64::from(f.samples)).sum::() + ); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn a_leading_junk_prefix_is_skipped_rather_than_failing() { + // An .ac3 file can open with an ID3 tag. Anything before the first + // syncword must be walked past, not treated as a parse failure. + let mut stream = vec![0xFFu8; 300]; + stream.extend_from_slice(AC3_FIXTURE); + let idx = FrameIndex::build(TranscodeCodec::Ac3, &mut &stream[..]).unwrap(); + assert_eq!(idx.frames[0].offset, 300); + assert_eq!(idx.total_samples, 1536 * idx.frames.len() as u64); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn locate_maps_a_sample_back_to_its_frame_and_offset() { + let idx = FrameIndex::build(TranscodeCodec::Ac3, &mut &AC3_FIXTURE[..]).unwrap(); + assert_eq!(idx.locate(0), (0, 0)); + assert_eq!(idx.locate(1535), (0, 1535)); + assert_eq!(idx.locate(1536), (1, 0)); + assert_eq!(idx.locate(1536 + 7), (1, 7)); + // Past the end clamps to the last frame instead of erroring. + let (i, _) = idx.locate(u64::MAX); + assert_eq!(i, idx.frames.len() - 1); + } + + #[test] + fn a_stream_that_is_not_this_codec_is_rejected() { + let junk = vec![0u8; 4096]; + assert!(FrameIndex::build(TranscodeCodec::Ac3, &mut &junk[..]).is_err()); + } +} diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs new file mode 100644 index 00000000..2d651d3c --- /dev/null +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -0,0 +1,161 @@ +//! Decoding AC-3, E-AC-3 and DTS to linear PCM. +//! +//! These three codecs are licensed, and a TV sold without the licence plays the +//! picture and nothing else. Symphonia identifies all three and demuxes them +//! fine — it has no decoder for any of them, which is the entire gap this module +//! closes, using the decoders vendored under `crates/vendor`. +//! +//! The split here follows the one in [`crate::mediainfo`]: [`TranscodeCodec`] is +//! static identification and always compiles, so a build with no decoder still +//! knows what an AC-3 track *is* and can say so; only the decode path itself is +//! behind `transcode-ac3` / `transcode-dts`. That is what lets the DIDL writer +//! ask "can this build decode this?" without a pile of `#[cfg]` at the call +//! site — it asks [`TranscodeCodec::is_decodable`] and gets an honest answer in +//! every build. + +mod frames; +mod pcm; +mod wav; + +pub use frames::{FrameIndex, IndexedFrame}; +pub use pcm::PcmDecoder; +pub use wav::{wav_header, WAV_HEADER_LEN}; + +/// An audio codec VuIO can decode but many renderers cannot play. +/// +/// Deliberately not "every codec symphonia knows": this is the set that is both +/// commonly present in a library and commonly missing from a TV. AAC, MP3, FLAC +/// and PCM all play everywhere and never need this path; TrueHD is not decoded +/// by anything we vendor, so claiming it here would advertise a resource that +/// cannot be produced. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TranscodeCodec { + /// AC-3, "Dolby Digital" (ATSC A/52). + Ac3, + /// E-AC-3, "Dolby Digital Plus" (A/52 Annex E). + Eac3, + /// DTS Coherent Acoustics, Core profile. + Dts, +} + +impl TranscodeCodec { + /// Identify from the codec name stored in `media_files.codec`. + /// + /// The stored value comes from Symphonia's registry short name, so the + /// spellings accepted here are the ones the scanner actually writes; the + /// container-flavoured aliases are accepted too, because MKV `CodecID` + /// strings reach some of the same columns. + pub fn from_stored_codec(name: &str) -> Option { + match name.trim().to_ascii_lowercase().as_str() { + "ac3" | "ac-3" | "a_ac3" | "dolby digital" => Some(Self::Ac3), + "eac3" | "e-ac-3" | "eac-3" | "a_eac3" | "ec-3" => Some(Self::Eac3), + "dca" | "dts" | "a_dts" => Some(Self::Dts), + _ => None, + } + } + + /// The short name to record in the database and report in diagnostics. + pub fn as_str(self) -> &'static str { + match self { + Self::Ac3 => "ac3", + Self::Eac3 => "eac3", + Self::Dts => "dca", + } + } + + /// Whether *this build* can decode it. + /// + /// The answer is a compile-time constant, but it is asked at runtime by the + /// DIDL writer and the streaming handler so neither has to carry `#[cfg]`. + /// A build without the matching feature answers `false` and simply never + /// advertises the second resource. + pub fn is_decodable(self) -> bool { + match self { + Self::Ac3 | Self::Eac3 => cfg!(feature = "transcode-ac3"), + Self::Dts => cfg!(feature = "transcode-dts"), + } + } +} + +/// Build a decoder for `codec`, or `None` when this build cannot decode it. +/// +/// `want_channels` is passed through to the decoder rather than applied +/// afterwards: AC-3 carries the §7.8 downmix coefficients in the bitstream, so +/// asking the decoder for two channels produces the mix the encoder intended, +/// which a naive channel-summing downmix outside the decoder would not. +#[cfg(feature = "transcode")] +#[cfg_attr( + not(any(feature = "transcode-ac3", feature = "transcode-dts")), + allow(unused_variables) +)] +pub(crate) fn make_decoder( + codec: TranscodeCodec, + sample_rate: u32, + want_channels: Option, +) -> anyhow::Result> { + #[cfg(any(feature = "transcode-ac3", feature = "transcode-dts"))] + use oxideav_core::{CodecId, CodecParameters, SampleFormat}; + + match codec { + #[cfg(feature = "transcode-ac3")] + TranscodeCodec::Ac3 | TranscodeCodec::Eac3 => { + let mut params = CodecParameters::audio(CodecId::new(codec.as_str())); + params.sample_rate = Some(sample_rate); + params.channels = want_channels; + params.sample_format = Some(SampleFormat::S16); + // Both codecs run through the same decoder — E-AC-3 is Annex E of the + // same specification and dispatch is on the per-packet bsid — but the + // eac3 factory registers the eac3 codec id, which is what the frame's + // own reported id has to match for the registry-facing accessors. + match codec { + TranscodeCodec::Eac3 => oxideav_ac3::decoder::make_eac3_decoder(¶ms), + _ => oxideav_ac3::decoder::make_decoder(¶ms), + } + .map_err(|e| anyhow::anyhow!("AC-3 decoder: {e}")) + } + #[cfg(feature = "transcode-dts")] + TranscodeCodec::Dts => { + let mut params = CodecParameters::audio(CodecId::new("dts")); + params.sample_rate = Some(sample_rate); + params.channels = want_channels; + params.sample_format = Some(SampleFormat::S16); + oxideav_dts::make_decoder(¶ms).map_err(|e| anyhow::anyhow!("DTS decoder: {e}")) + } + #[allow(unreachable_patterns)] + other => anyhow::bail!( + "this build of vuio-core was compiled without a decoder for {}", + other.as_str() + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stored_codec_names_map_to_the_three_codecs() { + assert_eq!(TranscodeCodec::from_stored_codec("ac3"), Some(TranscodeCodec::Ac3)); + assert_eq!(TranscodeCodec::from_stored_codec("EAC3"), Some(TranscodeCodec::Eac3)); + assert_eq!(TranscodeCodec::from_stored_codec("A_EAC3"), Some(TranscodeCodec::Eac3)); + assert_eq!(TranscodeCodec::from_stored_codec("dca"), Some(TranscodeCodec::Dts)); + assert_eq!(TranscodeCodec::from_stored_codec(" dts "), Some(TranscodeCodec::Dts)); + // Codecs every renderer already plays must never route through here. + assert_eq!(TranscodeCodec::from_stored_codec("aac"), None); + assert_eq!(TranscodeCodec::from_stored_codec("flac"), None); + // Vendored has no TrueHD decoder, so it must not claim one. + assert_eq!(TranscodeCodec::from_stored_codec("truehd"), None); + } + + #[test] + fn decodability_tracks_the_compiled_features() { + assert_eq!( + TranscodeCodec::Ac3.is_decodable(), + cfg!(feature = "transcode-ac3") + ); + assert_eq!( + TranscodeCodec::Dts.is_decodable(), + cfg!(feature = "transcode-dts") + ); + } +} diff --git a/crates/vuio-core/src/media/transcode/pcm.rs b/crates/vuio-core/src/media/transcode/pcm.rs new file mode 100644 index 00000000..f44c1608 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/pcm.rs @@ -0,0 +1,224 @@ +//! Turning compressed frames into interleaved little-endian S16. +//! +//! The vendored decoders emit exactly that layout in `AudioFrame::data[0]` when +//! asked for [`SampleFormat::S16`], so this is a thin driver over the push/pull +//! `Decoder` trait rather than any DSP of its own — the one substantive choice +//! is asking the decoder for the channel count we want instead of mixing down +//! afterwards. AC-3 carries the §7.8 downmix coefficients in the bitstream, so +//! the decoder's own two-channel output is the mix the encoder intended; summing +//! channels outside it would throw that away and clip besides. + +use anyhow::{bail, Context, Result}; + +use super::TranscodeCodec; + +/// Bytes each sample occupies per channel. +const BYTES_PER_SAMPLE: usize = 2; + +/// A decoder bound to one stream, with its output shape resolved. +pub struct PcmDecoder { + inner: Box, + codec: TranscodeCodec, + sample_rate: u32, + channels: u16, +} + +impl PcmDecoder { + /// Open a decoder for `codec` and resolve its output shape from `first_frame`. + /// + /// The channel count is measured from a real decoded frame rather than + /// predicted from the stream's `acmod`: what matters downstream is how many + /// channels the decoder actually emits after any downmix, and asking is both + /// shorter and impossible to get wrong. The decoded bytes of that first + /// frame are returned so the probe is not paid for twice. + pub fn open( + codec: TranscodeCodec, + sample_rate: u32, + want_channels: Option, + first_frame: &[u8], + ) -> Result<(Self, Vec)> { + let inner = super::make_decoder(codec, sample_rate, want_channels)?; + let mut me = Self { + inner, + codec, + sample_rate, + // Provisional: replaced by the measurement below before anyone sees it. + channels: want_channels.unwrap_or(2), + }; + + let (pcm, samples) = me.decode_measured(first_frame)?; + if samples == 0 || pcm.is_empty() { + bail!( + "{} decoder produced no samples for the first frame", + codec.as_str() + ); + } + let stride = pcm.len() / (samples as usize * BYTES_PER_SAMPLE); + if stride == 0 || stride > 8 { + bail!( + "{} decoder reported {samples} samples in {} bytes, which is not a sane channel count", + codec.as_str(), + pcm.len() + ); + } + me.channels = stride as u16; + Ok((me, pcm)) + } + + /// Channels in the decoded output. + pub fn channels(&self) -> u16 { + self.channels + } + + /// Sample rate of the decoded output, in Hz. + pub fn sample_rate(&self) -> u32 { + self.sample_rate + } + + /// Decode one compressed frame into interleaved S16. + /// + /// A frame that fails to decode yields silence of the length the index said + /// it would occupy — the caller has already committed to a `Content-Length` + /// built from that index, so a mid-stream error must not change how many + /// bytes the response carries. One corrupt frame in a film is a tick; a + /// short body is a truncated download. + pub fn decode_or_silence(&mut self, frame: &[u8], expect_samples: u32) -> Vec { + let want = expect_samples as usize * self.channels as usize * BYTES_PER_SAMPLE; + match self.decode_measured(frame) { + Ok((mut pcm, _)) => { + pcm.resize(want, 0); + pcm + } + Err(_) => vec![0u8; want], + } + } + + /// Feed one frame and collect everything it produces. + fn decode_measured(&mut self, frame: &[u8]) -> Result<(Vec, u32)> { + use oxideav_core::{Frame, Packet, TimeBase}; + + let packet = Packet::new(0, TimeBase::new(1, self.sample_rate as i64), frame.to_vec()); + self.inner + .send_packet(&packet) + .map_err(|e| anyhow::anyhow!("{}: {e}", self.codec.as_str())) + .context("feeding a frame to the decoder")?; + + let mut out = Vec::new(); + let mut samples = 0u32; + // `receive_frame` returns `NeedMore` once the packet is drained, which is + // the normal exit, not an error. + while let Ok(frame) = self.inner.receive_frame() { + if let Frame::Audio(af) = frame { + if let Some(plane) = af.data.first() { + out.extend_from_slice(plane); + } + samples += af.samples; + } + } + Ok((out, samples)) + } +} + +#[cfg(test)] +mod tests { + use super::super::frames::FrameIndex; + use super::*; + + #[cfg(feature = "transcode-ac3")] + const AC3_FIXTURE: &[u8] = + include_bytes!("../../../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); + #[cfg(feature = "transcode-dts")] + const DTS_FIXTURE: &[u8] = + include_bytes!("../../../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); + + /// Decode a whole fixture, returning the PCM and the index that described it. + fn decode_all(codec: TranscodeCodec, bytes: &[u8]) -> (Vec, FrameIndex, u16) { + let idx = FrameIndex::build(codec, &mut &bytes[..]).unwrap(); + let first = &bytes[idx.frames[0].offset as usize..][..idx.frames[0].len as usize]; + let (mut dec, head) = PcmDecoder::open(codec, idx.sample_rate, Some(2), first).unwrap(); + let channels = dec.channels(); + let mut pcm = head; + pcm.resize( + idx.frames[0].samples as usize * channels as usize * 2, + 0, + ); + for f in &idx.frames[1..] { + let raw = &bytes[f.offset as usize..][..f.len as usize]; + pcm.extend_from_slice(&dec.decode_or_silence(raw, f.samples)); + } + (pcm, idx, channels) + } + + fn rms(pcm: &[u8]) -> f64 { + let mut sum = 0.0f64; + let mut n = 0u64; + for c in pcm.chunks_exact(2) { + let v = i16::from_le_bytes([c[0], c[1]]) as f64; + sum += v * v; + n += 1; + } + if n == 0 { + 0.0 + } else { + (sum / n as f64).sqrt() + } + } + + /// The load-bearing guarantee: the byte count the index predicts is the byte + /// count the decoder produces. `Content-Length` is computed from the former + /// before a single sample is decoded, so if these ever disagree every + /// transcoded response is truncated or over-long. + #[cfg(feature = "transcode-ac3")] + #[test] + fn ac3_decodes_to_exactly_the_length_the_index_predicted() { + let (pcm, idx, channels) = decode_all(TranscodeCodec::Ac3, AC3_FIXTURE); + assert_eq!( + pcm.len() as u64, + idx.total_samples * channels as u64 * 2, + "decoded PCM length must match the indexed sample count" + ); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn ac3_decodes_to_audible_stereo() { + let (pcm, _, channels) = decode_all(TranscodeCodec::Ac3, AC3_FIXTURE); + assert_eq!(channels, 2, "a stereo source asked for stereo stays stereo"); + // The fixture is a 440 Hz sine, so anything near zero means we produced + // a correctly-sized block of silence instead of decoding. + assert!(rms(&pcm) > 100.0, "decoded RMS {} is silence", rms(&pcm)); + } + + #[cfg(feature = "transcode-dts")] + #[test] + fn dts_decodes_to_exactly_the_length_the_index_predicted() { + let (pcm, idx, channels) = decode_all(TranscodeCodec::Dts, DTS_FIXTURE); + assert_eq!( + pcm.len() as u64, + idx.total_samples * channels as u64 * 2, + "decoded PCM length must match the indexed sample count" + ); + } + + #[cfg(feature = "transcode-dts")] + #[test] + fn dts_decodes_to_audible_audio() { + let (pcm, _, _) = decode_all(TranscodeCodec::Dts, DTS_FIXTURE); + assert!(rms(&pcm) > 10.0, "decoded RMS {} is silence", rms(&pcm)); + } + + /// A corrupt frame must cost its own duration in silence and nothing more, + /// because the response length was already promised. + #[cfg(feature = "transcode-ac3")] + #[test] + fn a_corrupt_frame_yields_silence_of_the_right_length() { + let idx = FrameIndex::build(TranscodeCodec::Ac3, &mut &AC3_FIXTURE[..]).unwrap(); + let first = &AC3_FIXTURE[idx.frames[0].offset as usize..][..idx.frames[0].len as usize]; + let (mut dec, _) = + PcmDecoder::open(TranscodeCodec::Ac3, idx.sample_rate, Some(2), first).unwrap(); + let garbage = vec![0u8; 768]; + let out = dec.decode_or_silence(&garbage, 1536); + assert_eq!(out.len(), 1536 * 2 * 2); + assert!(out.iter().all(|&b| b == 0)); + } +} diff --git a/crates/vuio-core/src/media/transcode/wav.rs b/crates/vuio-core/src/media/transcode/wav.rs new file mode 100644 index 00000000..2def9148 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/wav.rs @@ -0,0 +1,113 @@ +//! The 44-byte canonical RIFF/WAVE header. +//! +//! Hand-written rather than muxed. The decoders emit interleaved little-endian +//! S16, which is exactly a WAV `data` chunk's payload, so the whole container is +//! this header followed by a copy — and we already hand-write fMP4 boxes in +//! [`crate::media::remux::fmp4_writer`] for the same reason. +//! +//! Fixed-layout on purpose: a renderer asking for a byte range needs the header +//! length to be a constant it can subtract, and PCM's constant bitrate is what +//! makes the whole resource seekable. + +/// Length of the header [`wav_header`] writes. PCM data begins here. +pub const WAV_HEADER_LEN: u64 = 44; + +/// Bytes per sample per channel. S16 throughout — it is what the decoders emit +/// and what every renderer that accepts LPCM accepts. +pub(crate) const BYTES_PER_SAMPLE: u64 = 2; + +/// Total size of the WAV resource carrying `total_samples` frames of `channels`. +/// +/// `total_samples` counts sample *frames* (one per channel-tuple), matching +/// `AudioFrame::samples`, so a stereo second at 48 kHz is 48 000 — not 96 000. +pub(crate) fn wav_size(total_samples: u64, channels: u16) -> u64 { + WAV_HEADER_LEN + pcm_size(total_samples, channels) +} + +/// Size of the PCM payload alone. +pub(crate) fn pcm_size(total_samples: u64, channels: u16) -> u64 { + total_samples * channels as u64 * BYTES_PER_SAMPLE +} + +/// Build the header for a stream of `total_samples` frames. +/// +/// RIFF sizes are 32-bit, so a stream whose payload will not fit is described +/// with the largest size the format can express rather than a wrapped one. That +/// is ~6.2 hours of 48 kHz stereo; past it a player sees a truncated duration +/// instead of a corrupt one, which is the better of the two failures. RF64 would +/// lift the limit and is not worth a container dependency for a case no real +/// library hits. +pub fn wav_header(sample_rate: u32, channels: u16, total_samples: u64) -> [u8; 44] { + let data_len = u32::try_from(pcm_size(total_samples, channels)).unwrap_or(u32::MAX - 36); + let byte_rate = sample_rate as u64 * channels as u64 * BYTES_PER_SAMPLE; + let block_align = channels * BYTES_PER_SAMPLE as u16; + + let mut h = [0u8; 44]; + h[0..4].copy_from_slice(b"RIFF"); + h[4..8].copy_from_slice(&(36 + data_len).to_le_bytes()); + h[8..12].copy_from_slice(b"WAVE"); + h[12..16].copy_from_slice(b"fmt "); + h[16..20].copy_from_slice(&16u32.to_le_bytes()); // PCM fmt chunk length + h[20..22].copy_from_slice(&1u16.to_le_bytes()); // WAVE_FORMAT_PCM + h[22..24].copy_from_slice(&channels.to_le_bytes()); + h[24..28].copy_from_slice(&sample_rate.to_le_bytes()); + h[28..32].copy_from_slice(&(byte_rate.min(u32::MAX as u64) as u32).to_le_bytes()); + h[32..34].copy_from_slice(&block_align.to_le_bytes()); + h[34..36].copy_from_slice(&16u16.to_le_bytes()); // bits per sample + h[36..40].copy_from_slice(b"data"); + h[40..44].copy_from_slice(&data_len.to_le_bytes()); + h +} + +#[cfg(test)] +mod tests { + use super::*; + + fn le32(h: &[u8; 44], at: usize) -> u32 { + u32::from_le_bytes([h[at], h[at + 1], h[at + 2], h[at + 3]]) + } + fn le16(h: &[u8; 44], at: usize) -> u16 { + u16::from_le_bytes([h[at], h[at + 1]]) + } + + #[test] + fn describes_one_second_of_48k_stereo() { + let h = wav_header(48_000, 2, 48_000); + assert_eq!(&h[0..4], b"RIFF"); + assert_eq!(&h[8..12], b"WAVE"); + assert_eq!(&h[36..40], b"data"); + assert_eq!(le32(&h, 40), 48_000 * 2 * 2, "payload bytes"); + assert_eq!(le32(&h, 4), 36 + 48_000 * 2 * 2, "riff size excludes its own 8"); + assert_eq!(le16(&h, 20), 1, "WAVE_FORMAT_PCM"); + assert_eq!(le16(&h, 22), 2, "channels"); + assert_eq!(le32(&h, 24), 48_000, "sample rate"); + assert_eq!(le32(&h, 28), 48_000 * 2 * 2, "byte rate"); + assert_eq!(le16(&h, 32), 4, "block align"); + assert_eq!(le16(&h, 34), 16, "bits per sample"); + } + + #[test] + fn header_length_and_total_size_agree_with_the_declared_payload() { + let h = wav_header(44_100, 2, 44_100 * 3); + assert_eq!(h.len() as u64, WAV_HEADER_LEN); + assert_eq!( + wav_size(44_100 * 3, 2), + WAV_HEADER_LEN + le32(&h, 40) as u64 + ); + } + + #[test] + fn a_payload_too_large_for_riff_saturates_rather_than_wrapping() { + // ~6.2 h of 48 kHz stereo is where a 32-bit size runs out. The header + // must not describe a wrapped, far-too-small payload. + let huge = u64::MAX / 8; + let h = wav_header(48_000, 2, huge); + assert_eq!(le32(&h, 40), u32::MAX - 36); + assert_eq!(le32(&h, 4), u32::MAX, "riff size stays consistent with data"); + } + + #[test] + fn mono_and_stereo_sizes_differ_by_exactly_the_channel_count() { + assert_eq!(pcm_size(1_000, 1) * 2, pcm_size(1_000, 2)); + } +} From a8a30ea86dbeea4aeb8b1d8461b7db87565538c9 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 12:18:47 +0300 Subject: [PATCH 03/38] feat(streaming): serve AC-3, E-AC-3 and DTS as playable LPCM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET/HEAD /media/{id}/transcode/audio.wav, the [transcode] config section, and the elementary extensions (.ac3/.eac3/.ec3/.dts) that were not in the supported-media table at all — VuIO could neither play nor even list those files before, which is why they were left out. A separate handler from serve_media rather than a mode of it, because almost every assumption differs: there is no file to open, the length comes from a plan rather than from stat, the DLNA headers must declare a conversion (DLNA.ORG_CI=1, hardcoded to 0 on the passthrough path), and the work is CPU-bound and therefore rationed. What it keeps is the contract a renderer cannot recover from if it is broken: an exact Content-Length, Accept-Ranges, and real 206s. Decoded PCM is constant-bitrate, so a byte offset divides cleanly back into a sample and a seek is a genuine seek rather than a restart. That is what the frame index buys, and why AudioPlan resolves total samples, sample rate and channel count before a byte is sent — a renderer asks for the size and the bytes on separate connections and will not tolerate the two disagreeing. Seeking decodes one frame of pre-roll and discards it. AC-3 and DTS frames overlap by half a window, so the sample at a seek point is reconstructed from state the previous frame carried; without the pre-roll every seek ticks. Plans are cached per (id, size, mtime): a renderer issues HEAD, then GET, then ranges as someone scrubs, and building a plan re-reads the track's headers. Keying on size and mtime means a file replaced in place is re-indexed rather than served offsets it no longer has. max_concurrent refuses rather than queues. A renderer told to wait looks like a file that will not open, and the streams already playing would lose CPU to it. A HEAD releases its slot immediately so a renderer that probes before playing cannot starve one that is playing. Decoding runs on a blocking thread behind a bounded channel, so a slow reader slows the decoder instead of pulling a film into memory. Verified: 8 integration tests against the real router, including a range being byte-identical to that slice of the full decode, HEAD and GET agreeing on length, and the fixture decoding to non-silent audio. 498 unit tests pass. --- config.example.toml | 18 + crates/vuio-core/src/config/generator.rs | 4 +- crates/vuio-core/src/config/loading.rs | 20 + crates/vuio-core/src/config/mod.rs | 6 +- crates/vuio-core/src/config/model.rs | 134 +++++++ crates/vuio-core/src/config/template.toml | 18 + crates/vuio-core/src/config/validation.rs | 19 + crates/vuio-core/src/lifecycle/runner.rs | 4 + crates/vuio-core/src/media/transcode/mod.rs | 4 + crates/vuio-core/src/media/transcode/plan.rs | 219 +++++++++++ .../vuio-core/src/media/transcode/session.rs | 165 ++++++++ .../src/platform/filesystem/manager.rs | 8 + crates/vuio-core/src/state.rs | 9 + crates/vuio-core/src/web/admin.rs | 68 ++++ crates/vuio-core/src/web/mod.rs | 11 + crates/vuio-core/src/web/streaming.rs | 2 +- .../vuio-core/src/web/transcode_streaming.rs | 364 ++++++++++++++++++ .../tests/audio_integration_tests.rs | 4 + crates/vuio-core/tests/issue_24_pagination.rs | 4 + .../vuio-core/tests/mcp_integration_tests.rs | 2 + .../tests/mediainfo_integration_tests.rs | 2 + .../tests/metrics_integration_tests.rs | 2 + .../tests/music_browse_integration_tests.rs | 2 + crates/vuio-core/tests/samsungtv_browse.rs | 2 + .../tests/transcode_integration_tests.rs | 340 ++++++++++++++++ .../tests/web_ui_integration_tests.rs | 2 + 26 files changed, 1428 insertions(+), 5 deletions(-) create mode 100644 crates/vuio-core/src/media/transcode/plan.rs create mode 100644 crates/vuio-core/src/media/transcode/session.rs create mode 100644 crates/vuio-core/src/web/transcode_streaming.rs create mode 100644 crates/vuio-core/tests/transcode_integration_tests.rs diff --git a/config.example.toml b/config.example.toml index d1d4a225..837c5d00 100644 --- a/config.example.toml +++ b/config.example.toml @@ -121,3 +121,21 @@ port = 8090 # Must differ from server.port # Whether a fetched title outranks the one read from the file's own tags. # prefer_online_titles = true # request_timeout_seconds = 15 + +# Audio for TVs that cannot decode AC-3, Dolby Digital Plus or DTS. +# Those codecs are licensed, and a set sold without the licence plays the +# picture and nothing else. When this is on, such a film is offered twice — the +# original and an already-decoded version — and the TV plays whichever it can. +# A TV that was already fine is unaffected. +[transcode] +enabled = true +# What the decoded version is delivered as. +# lpcm uncompressed, seekable, about 1.5 Mbps +# aac about a tenth of that, lossy, and cannot be scrubbed +audio_format = "lpcm" +# Which version is listed first, for a TV that takes what it is given rather +# than choosing. Switch to "transcoded" only if a TV still plays silently. +prefer = "original" +# Decodes allowed at once. Past this a further request is refused rather than +# queued, so the streams already playing keep up. +max_concurrent = 2 diff --git a/crates/vuio-core/src/config/generator.rs b/crates/vuio-core/src/config/generator.rs index aab92401..b5cade59 100644 --- a/crates/vuio-core/src/config/generator.rs +++ b/crates/vuio-core/src/config/generator.rs @@ -452,7 +452,7 @@ mod tests { use crate::config::{ AppConfig, DatabaseConfig, ManagementConfig, McpConfig, MediaConfig, MediaInfoConfig, MonitoredDirectoryConfig, NetworkConfig, NetworkInterfaceConfig, ServerConfig, - ValidationMode, WebUiConfig, + TranscodeConfig, ValidationMode, WebUiConfig, }; use uuid::Uuid; @@ -504,6 +504,7 @@ mod tests { mediainfo: MediaInfoConfig::default(), web_ui: WebUiConfig::default(), mcp: McpConfig::default(), + transcode: TranscodeConfig::default(), }; // Generate TOML @@ -617,6 +618,7 @@ mod tests { mediainfo: MediaInfoConfig::default(), web_ui: WebUiConfig::default(), mcp: McpConfig::default(), + transcode: TranscodeConfig::default(), }; // Generate TOML diff --git a/crates/vuio-core/src/config/loading.rs b/crates/vuio-core/src/config/loading.rs index 3eb70753..9114ec18 100644 --- a/crates/vuio-core/src/config/loading.rs +++ b/crates/vuio-core/src/config/loading.rs @@ -225,6 +225,25 @@ impl AppConfig { read_only: env_flag("VUIO_MCP_READ_ONLY").unwrap_or(false), require_auth: env_flag("VUIO_MCP_REQUIRE_AUTH").unwrap_or(false), }, + transcode: TranscodeConfig { + enabled: env_flag("VUIO_TRANSCODE_ENABLED").unwrap_or(true), + // An unrecognised value falls back to the default rather than + // refusing to start: this is a container that may have been + // handed a typo through a compose file, and a media server that + // will not boot is worse than one that plays LPCM. + audio_format: std::env::var("VUIO_TRANSCODE_AUDIO_FORMAT") + .ok() + .and_then(|v| TranscodeAudioFormat::parse(&v)) + .unwrap_or_default(), + prefer: std::env::var("VUIO_TRANSCODE_PREFER") + .ok() + .and_then(|v| TranscodePreference::parse(&v)) + .unwrap_or_default(), + max_concurrent: std::env::var("VUIO_TRANSCODE_MAX_CONCURRENT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or_else(default_transcode_max_concurrent), + }, }) } @@ -346,6 +365,7 @@ impl AppConfig { }; Self { + transcode: TranscodeConfig::default(), server: ServerConfig { port: platform_config .preferred_ports diff --git a/crates/vuio-core/src/config/mod.rs b/crates/vuio-core/src/config/mod.rs index 2d49567a..2a45c8e8 100644 --- a/crates/vuio-core/src/config/mod.rs +++ b/crates/vuio-core/src/config/mod.rs @@ -15,13 +15,13 @@ pub mod validation; use model::{ default_cache_mb, default_full_rescan_interval_hours, default_mediainfo_providers, default_mediainfo_timeout_seconds, - default_min_confidence, default_session_ttl_hours, default_unavailable_root_grace_hours, - default_web_ui_port, + default_min_confidence, default_session_ttl_hours, default_transcode_max_concurrent, + default_unavailable_root_grace_hours, default_web_ui_port, }; pub use model::{ AppConfig, ConfigOverrides, DatabaseConfig, ManagementConfig, McpConfig, MediaConfig, MediaInfoConfig, MonitoredDirectoryConfig, NetworkConfig, NetworkInterfaceConfig, ServerConfig, - ValidationMode, WebUiConfig, + TranscodeAudioFormat, TranscodeConfig, TranscodePreference, ValidationMode, WebUiConfig, }; use crate::platform::config::PlatformConfig; diff --git a/crates/vuio-core/src/config/model.rs b/crates/vuio-core/src/config/model.rs index d1677fe4..eecce994 100644 --- a/crates/vuio-core/src/config/model.rs +++ b/crates/vuio-core/src/config/model.rs @@ -63,6 +63,13 @@ pub(super) fn default_true() -> bool { true } +/// Two at once: enough that a second TV starting a film does not get a refusal, +/// low enough that a small box is not asked to run four decoders and serve the +/// library at the same time. +pub(super) fn default_transcode_max_concurrent() -> usize { + 2 +} + pub(super) fn default_web_ui_port() -> u16 { 8090 } @@ -170,6 +177,8 @@ pub struct AppConfig { pub web_ui: WebUiConfig, #[serde(default)] pub mcp: McpConfig, + #[serde(default)] + pub transcode: TranscodeConfig, } /// The Model Context Protocol server, which lets an AI agent browse, search and @@ -208,6 +217,131 @@ impl Default for McpConfig { } } +/// Decoding AC-3, E-AC-3 and DTS for renderers that cannot play them. +/// +/// Defaulted as a whole, like `[mcp]` and `[web_ui]`: a config file written +/// before this existed has no `[transcode]` table and must keep loading. +/// +/// Parsed in every build, including one compiled without any decoder — the same +/// rule `[mediainfo]` follows. A server that cannot decode still reads the +/// section, still reports it to the dashboard, and simply never advertises a +/// transcoded resource; an operator moving a config file between builds should +/// not have it rejected by the leaner one. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TranscodeConfig { + /// Offer a decoded resource beside the original for AC-3/E-AC-3/DTS items. + #[serde(default = "default_true")] + pub enabled: bool, + /// What the decoded resource is delivered as. + #[serde(default)] + pub audio_format: TranscodeAudioFormat, + /// Which resource is listed first in the DIDL response. + #[serde(default)] + pub prefer: TranscodePreference, + /// Ceiling on simultaneous transcode sessions. + /// + /// Decoding is the only CPU-bound work this server does, and a shared folder + /// can be opened by every TV in the house at once. Past this, a request is + /// refused rather than joining a queue that would starve the ones already + /// playing. + #[serde(default = "default_transcode_max_concurrent")] + pub max_concurrent: usize, +} + +impl Default for TranscodeConfig { + fn default() -> Self { + Self { + enabled: true, + audio_format: TranscodeAudioFormat::default(), + prefer: TranscodePreference::default(), + max_concurrent: default_transcode_max_concurrent(), + } + } +} + +/// What a transcoded audio resource is delivered as. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TranscodeAudioFormat { + /// Linear PCM in a WAV container. + /// + /// The default, and the better resource where bandwidth allows: PCM is + /// constant-bitrate, so the response carries an exact `Content-Length` and + /// supports byte-range seeking, and every renderer that accepts LPCM at all + /// accepts 16-bit stereo. It costs about 1.5 Mbps, which is nothing on the + /// wired or 5 GHz LAN these devices sit on and noticeable over 2.4 GHz. + #[default] + Lpcm, + /// AAC-LC in ADTS framing. + /// + /// A tenth of the bitrate, at the price of a lossy re-encode and a + /// non-seekable response — the encoder's output size is not known ahead of + /// time, so the resource is streamed without a `Content-Length` and a + /// renderer cannot scrub within it. + Aac, +} + +impl TranscodeAudioFormat { + /// Parse an environment-variable or TOML value, case- and space-insensitively. + /// + /// `None` for anything unrecognised, so the caller decides between falling + /// back and refusing — Docker falls back, a config file refuses. + pub fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "lpcm" | "pcm" | "wav" => Some(Self::Lpcm), + "aac" => Some(Self::Aac), + _ => None, + } + } + + /// The value as it is written in a config file. + pub fn as_str(self) -> &'static str { + match self { + Self::Lpcm => "lpcm", + Self::Aac => "aac", + } + } +} + +/// Which of an item's two resources is listed first. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TranscodePreference { + /// The original file first, the decoded resource second. + /// + /// The default, because it is the choice that cannot make anything worse. A + /// renderer that matches on protocolInfo picks whichever it can actually + /// play; one that blindly takes the first resource behaves exactly as it did + /// before this feature existed. + #[default] + Original, + /// The decoded resource first. + /// + /// For a renderer that takes the first resource without checking and cannot + /// play the original — it plays silently otherwise, and no amount of correct + /// protocolInfo will change its mind. + Transcoded, +} + +impl TranscodePreference { + /// Parse an environment-variable or TOML value, case- and space-insensitively. + pub fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "original" | "source" => Some(Self::Original), + "transcoded" | "decoded" => Some(Self::Transcoded), + _ => None, + } + } + + /// The value as it is written in a config file. + pub fn as_str(self) -> &'static str { + match self { + Self::Original => "original", + Self::Transcoded => "transcoded", + } + } +} + /// The second HTTP listener, carrying the Svelte browser interface. /// /// Defaulted as a whole, like `[management]` and `[mediainfo]`: a config file diff --git a/crates/vuio-core/src/config/template.toml b/crates/vuio-core/src/config/template.toml index 41c4e1bf..72635b64 100644 --- a/crates/vuio-core/src/config/template.toml +++ b/crates/vuio-core/src/config/template.toml @@ -82,5 +82,23 @@ backup_enabled = false [mediainfo] enabled = false +# Audio for TVs that cannot decode AC-3, Dolby Digital Plus or DTS. +# Those codecs are licensed, and a set sold without the licence plays the +# picture and nothing else. When this is on, such a film is offered twice — the +# original and an already-decoded version — and the TV plays whichever it can. +# A TV that was already fine is unaffected. +[transcode] +enabled = true +# What the decoded version is delivered as. +# lpcm uncompressed, seekable, about 1.5 Mbps +# aac about a tenth of that, lossy, and cannot be scrubbed +audio_format = "lpcm" +# Which version is listed first, for a TV that takes what it is given rather +# than choosing. Switch to "transcoded" only if a TV still plays silently. +prefer = "original" +# Decodes allowed at once. Past this a further request is refused rather than +# queued, so the streams already playing keep up. +max_concurrent = 2 + # Platform-specific notes: # PLACEHOLDER_PLATFORM_NOTES diff --git a/crates/vuio-core/src/config/validation.rs b/crates/vuio-core/src/config/validation.rs index cb25fc3f..ae20802b 100644 --- a/crates/vuio-core/src/config/validation.rs +++ b/crates/vuio-core/src/config/validation.rs @@ -18,10 +18,28 @@ impl ConfigValidator { Self::validate_media_config(config)?; Self::validate_database_config(config)?; Self::validate_management(config)?; + Self::validate_transcode(config)?; Self::validate_platform_specific(config)?; Ok(()) } + /// Validate the transcoding section. + /// + /// Only `max_concurrent` can be wrong in a way worth catching: the two enums + /// are rejected by serde before they reach here, and `enabled` on a build + /// with no decoder is not an error — it is a config file that outlives the + /// binary it was written for, which is exactly what the section is defaulted + /// for. + pub fn validate_transcode(config: &AppConfig) -> Result<()> { + if config.transcode.enabled && config.transcode.max_concurrent == 0 { + return Err(anyhow!( + "[transcode] max_concurrent is 0, so every transcode would be refused. \ + Set it to at least 1, or set enabled = false to turn the feature off." + )); + } + Ok(()) + } + /// Validate management/authentication configuration. /// /// These values are only consumed once, by `AuthState::load` at startup, which @@ -440,6 +458,7 @@ impl ConfigValidator { Self::validate_media_config_flexible(config)?; Self::validate_database_config(config)?; Self::validate_management(config)?; + Self::validate_transcode(config)?; Self::validate_platform_specific(config)?; Ok(()) } diff --git a/crates/vuio-core/src/lifecycle/runner.rs b/crates/vuio-core/src/lifecycle/runner.rs index d2ba6e4c..c570885b 100644 --- a/crates/vuio-core/src/lifecycle/runner.rs +++ b/crates/vuio-core/src/lifecycle/runner.rs @@ -186,6 +186,10 @@ where discovered_tvs: Arc::new(renderer_cache), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(crate::radio::RadioManager::new()), + #[cfg(feature = "transcode")] + transcode: Arc::new(crate::media::transcode::TranscodeState::new( + config.transcode.max_concurrent, + )), cancellation: cancellation.clone(), background_tasks: background_tasks.clone(), }; diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index 2d651d3c..01a86735 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -15,10 +15,14 @@ mod frames; mod pcm; +mod plan; +mod session; mod wav; pub use frames::{FrameIndex, IndexedFrame}; pub use pcm::PcmDecoder; +pub use plan::{AudioPlan, Seeked}; +pub use session::{IndexKey, TranscodeState}; pub use wav::{wav_header, WAV_HEADER_LEN}; /// An audio codec VuIO can decode but many renderers cannot play. diff --git a/crates/vuio-core/src/media/transcode/plan.rs b/crates/vuio-core/src/media/transcode/plan.rs new file mode 100644 index 00000000..b87a4c20 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/plan.rs @@ -0,0 +1,219 @@ +//! What a transcoded resource will be, resolved before a byte of it is sent. +//! +//! A DLNA renderer asks for the size first and the bytes second, often from a +//! different connection, and it will not tolerate the two disagreeing. So +//! everything the response's shape depends on — total samples, sample rate, +//! channel count — is settled up front, here, and the streaming half only ever +//! fills in a length that was already promised. +//! +//! Building a plan costs one pass over the file's headers plus one decoded +//! frame. That is why plans are cached (see [`super::session`]): a renderer's +//! `HEAD`, `GET` and range requests for one file should pay it once. + +use anyhow::{Context, Result}; +use std::fs::File; +use std::io::{BufReader, Read, Seek, SeekFrom}; +use std::path::Path; + +use super::wav::{pcm_size, wav_size, WAV_HEADER_LEN}; +use super::{FrameIndex, PcmDecoder, TranscodeCodec}; + +/// The decoded shape of one file, and the frame table to produce it. +#[derive(Debug)] +pub struct AudioPlan { + /// The file this plan describes. + /// + /// Carried because the plan outlives the request that built it — it is + /// cached by file id — and the streaming half re-opens the file to read + /// frames rather than holding a handle for the life of the cache entry. + pub source_path: std::path::PathBuf, + /// Codec of the source. + pub codec: TranscodeCodec, + /// Where every frame is and how long it decodes to. + pub index: FrameIndex, + /// Channels the decoder emits — measured, not predicted. + pub channels: u16, +} + +/// Channels to ask the decoder for. +/// +/// Stereo, always. A renderer that cannot decode AC-3 is not a renderer with a +/// 5.1 speaker set waiting behind it, and asking the decoder for two channels +/// gets the §7.8 downmix the encoder authored rather than one we invent. A +/// source already at or below stereo is unaffected — the decoder reports what it +/// actually emitted and [`AudioPlan::channels`] records that. +const TARGET_CHANNELS: u16 = 2; + +impl AudioPlan { + /// Index `path` and probe its decoded shape. + /// + /// Blocking: it reads the whole file's headers and decodes one frame, so + /// callers on an async task must wrap it in `spawn_blocking`. + pub fn build(path: &Path, codec: TranscodeCodec) -> Result { + let file = File::open(path) + .with_context(|| format!("opening {} for transcoding", path.display()))?; + let mut reader = BufReader::with_capacity(256 * 1024, file); + let index = FrameIndex::build(codec, &mut reader)?; + + // Re-open at the first frame to probe the decoder's output shape. + let first = index.frames[0]; + let mut file = reader.into_inner(); + file.seek(SeekFrom::Start(first.offset)) + .context("seeking to the first frame")?; + let mut buf = vec![0u8; first.len as usize]; + file.read_exact(&mut buf) + .context("reading the first frame")?; + + let (decoder, _) = PcmDecoder::open( + codec, + index.sample_rate, + Some(TARGET_CHANNELS), + &buf, + )?; + + Ok(Self { + source_path: path.to_path_buf(), + codec, + channels: decoder.channels(), + index, + }) + } + + /// Total size of the WAV resource, header included. + pub fn wav_size(&self) -> u64 { + wav_size(self.index.total_samples, self.channels) + } + + /// Sample rate of the decoded output, in Hz. + pub fn sample_rate(&self) -> u32 { + self.index.sample_rate + } + + /// The WAV header describing this resource. + pub fn wav_header(&self) -> [u8; 44] { + super::wav_header(self.sample_rate(), self.channels, self.index.total_samples) + } + + /// Bytes each decoded sample frame occupies. + fn stride(&self) -> u64 { + self.channels as u64 * 2 + } + + /// Turn a byte offset into the resource into the frame to start decoding at, + /// and how many decoded bytes to drop from that frame's output. + /// + /// Offsets inside the 44-byte header resolve to the very start, because a + /// range that begins mid-header still has to be served the rest of it. + pub fn seek(&self, byte_offset: u64) -> Seeked { + if byte_offset < WAV_HEADER_LEN { + return Seeked { + header_skip: byte_offset as usize, + frame: 0, + pcm_skip: 0, + }; + } + let pcm_offset = byte_offset - WAV_HEADER_LEN; + let sample = pcm_offset / self.stride(); + let within = (pcm_offset % self.stride()) as usize; + let (frame, samples_into_frame) = self.index.locate(sample); + Seeked { + header_skip: WAV_HEADER_LEN as usize, + frame, + pcm_skip: samples_into_frame as usize * self.stride() as usize + within, + } + } + + /// Decoded bytes produced by frame `i`. + pub fn frame_bytes(&self, i: usize) -> usize { + pcm_size(u64::from(self.index.frames[i].samples), self.channels) as usize + } +} + +/// Where a byte offset lands in the resource. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Seeked { + /// Bytes of the WAV header already passed — 44 once past it entirely. + pub header_skip: usize, + /// Index of the frame to begin decoding at. + pub frame: usize, + /// Decoded bytes to discard from that frame's output. + pub pcm_skip: usize, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "transcode-ac3")] + const AC3_FIXTURE: &[u8] = + include_bytes!("../../../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); + + #[cfg(feature = "transcode-ac3")] + fn fixture_plan() -> (AudioPlan, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("sine.ac3"); + std::fs::write(&path, AC3_FIXTURE).unwrap(); + (AudioPlan::build(&path, TranscodeCodec::Ac3).unwrap(), dir) + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn a_plan_describes_the_whole_resource_before_anything_is_decoded() { + let (plan, _dir) = fixture_plan(); + assert_eq!(plan.channels, 2); + assert_eq!(plan.sample_rate(), 48_000); + assert_eq!( + plan.wav_size(), + WAV_HEADER_LEN + plan.index.total_samples * 2 * 2 + ); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn offset_zero_starts_at_the_header_and_the_first_frame() { + let (plan, _dir) = fixture_plan(); + assert_eq!( + plan.seek(0), + Seeked { + header_skip: 0, + frame: 0, + pcm_skip: 0 + } + ); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn a_range_inside_the_header_still_gets_the_rest_of_it() { + let (plan, _dir) = fixture_plan(); + let s = plan.seek(20); + assert_eq!(s.header_skip, 20); + assert_eq!(s.frame, 0); + assert_eq!(s.pcm_skip, 0); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn a_range_past_the_header_lands_on_the_right_frame_and_sample() { + let (plan, _dir) = fixture_plan(); + // One whole AC-3 frame of decoded stereo is 1536 * 2 * 2 bytes. + let one_frame = 1536 * 2 * 2; + let s = plan.seek(WAV_HEADER_LEN + one_frame); + assert_eq!(s.header_skip, WAV_HEADER_LEN as usize, "header is done"); + assert_eq!(s.frame, 1, "exactly the second frame"); + assert_eq!(s.pcm_skip, 0); + + // Half a frame in: same frame, half its output discarded. + let s = plan.seek(WAV_HEADER_LEN + one_frame + one_frame / 2); + assert_eq!(s.frame, 1); + assert_eq!(s.pcm_skip, (one_frame / 2) as usize); + } + + #[cfg(feature = "transcode-ac3")] + #[test] + fn every_frame_reports_the_byte_count_its_sample_count_implies() { + let (plan, _dir) = fixture_plan(); + let total: usize = (0..plan.index.frames.len()).map(|i| plan.frame_bytes(i)).sum(); + assert_eq!(total as u64 + WAV_HEADER_LEN, plan.wav_size()); + } +} diff --git a/crates/vuio-core/src/media/transcode/session.rs b/crates/vuio-core/src/media/transcode/session.rs new file mode 100644 index 00000000..1d353cae --- /dev/null +++ b/crates/vuio-core/src/media/transcode/session.rs @@ -0,0 +1,165 @@ +//! Per-server transcoding state: the index cache and the concurrency ceiling. +//! +//! Both exist for the same reason — decoding is the only CPU-bound work this +//! server does, and a shared folder can be opened by every renderer in the house +//! at once. +//! +//! The cache matters more than it looks. A renderer typically issues a `HEAD`, +//! then a `GET`, then one or more range requests as someone scrubs, and building +//! an index re-reads the whole track each time. Holding a handful of indexes +//! turns that into one read per file rather than one per request. + +use std::collections::HashMap; +use std::sync::Arc; + +use tokio::sync::{Mutex, Semaphore}; + +use super::AudioPlan; + +/// How many indexes to keep. A two-hour AC-3 track indexes to roughly 3 MB, so +/// this is single-digit megabytes for a household's worth of open streams. +const MAX_CACHED_INDEXES: usize = 8; + +/// Identifies a cached index. The file's size and modification time are part of +/// the key so replacing a file in place invalidates its index rather than +/// serving byte offsets into a file that no longer has them. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct IndexKey { + /// Database id of the file. + pub id: i64, + /// Size in bytes at the time the index was built. + pub size: u64, + /// Modification time, as seconds since the epoch. + pub modified: i64, +} + +/// Shared transcoding state, held by `AppState`. +#[derive(Debug)] +pub struct TranscodeState { + cache: Mutex, + permits: Arc, +} + +#[derive(Debug, Default)] +struct Cache { + entries: HashMap>, + /// Insertion order, oldest first. A plain queue rather than a true LRU: with + /// a cap of eight the difference is not measurable, and this needs no + /// bookkeeping on the read path. + order: Vec, +} + +impl Default for TranscodeState { + fn default() -> Self { + Self::new(2) + } +} + +impl TranscodeState { + /// Build state allowing `max_concurrent` simultaneous transcodes. + /// + /// Zero is treated as one. Configuration validation rejects it, but this is + /// reachable from a `Default` and refusing every request would be a strange + /// way to express "misconfigured". + pub fn new(max_concurrent: usize) -> Self { + Self { + cache: Mutex::new(Cache::default()), + permits: Arc::new(Semaphore::new(max_concurrent.max(1))), + } + } + + /// Take a transcoding slot, or `None` when all of them are in use. + /// + /// Deliberately non-blocking: a renderer that waits in a queue for a slot + /// looks to its user like a file that will not open, and meanwhile the + /// streams already playing lose CPU to it. + pub fn try_acquire(&self) -> Option { + self.permits.clone().try_acquire_owned().ok() + } + + /// The plan for `key`, if one was built recently. + pub async fn cached(&self, key: &IndexKey) -> Option> { + self.cache.lock().await.entries.get(key).cloned() + } + + /// Remember `index` under `key`, evicting the oldest entry if full. + pub async fn remember(&self, key: IndexKey, index: Arc) { + let mut cache = self.cache.lock().await; + if cache.entries.insert(key, index).is_none() { + cache.order.push(key); + while cache.order.len() > MAX_CACHED_INDEXES { + let oldest = cache.order.remove(0); + cache.entries.remove(&oldest); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::media::transcode::TranscodeCodec; + + fn index() -> Arc { + Arc::new(AudioPlan { + source_path: std::path::PathBuf::from("/dev/null"), + codec: TranscodeCodec::Ac3, + channels: 2, + index: crate::media::transcode::FrameIndex { + codec: TranscodeCodec::Ac3, + sample_rate: 48_000, + frames: Vec::new(), + total_samples: 0, + }, + }) + } + + fn key(id: i64) -> IndexKey { + IndexKey { + id, + size: 1, + modified: 1, + } + } + + #[tokio::test] + async fn an_index_survives_until_evicted_by_newer_ones() { + let state = TranscodeState::new(1); + state.remember(key(1), index()).await; + assert!(state.cached(&key(1)).await.is_some()); + + for id in 2..=(MAX_CACHED_INDEXES as i64 + 1) { + state.remember(key(id), index()).await; + } + assert!(state.cached(&key(1)).await.is_none(), "oldest is evicted"); + assert!(state.cached(&key(2)).await.is_some()); + } + + #[tokio::test] + async fn a_file_replaced_in_place_does_not_reuse_its_index() { + let state = TranscodeState::new(1); + state.remember(key(1), index()).await; + let rewritten = IndexKey { + id: 1, + size: 999, + modified: 2, + }; + assert!(state.cached(&rewritten).await.is_none()); + } + + #[tokio::test] + async fn slots_are_handed_out_up_to_the_ceiling_and_then_refused() { + let state = TranscodeState::new(2); + let a = state.try_acquire().expect("first slot"); + let _b = state.try_acquire().expect("second slot"); + assert!(state.try_acquire().is_none(), "third is refused, not queued"); + drop(a); + assert!(state.try_acquire().is_some(), "a finished stream frees its slot"); + } + + #[tokio::test] + async fn a_zero_ceiling_still_serves_one_rather_than_nothing() { + let state = TranscodeState::new(0); + assert!(state.try_acquire().is_some()); + } +} diff --git a/crates/vuio-core/src/platform/filesystem/manager.rs b/crates/vuio-core/src/platform/filesystem/manager.rs index e5dbf6e2..a6c36419 100644 --- a/crates/vuio-core/src/platform/filesystem/manager.rs +++ b/crates/vuio-core/src/platform/filesystem/manager.rs @@ -258,6 +258,14 @@ pub const SUPPORTED_MEDIA_TYPES: &[(&str, &str)] = &[ ("m4a", "audio/mp4"), ("opus", "audio/opus"), ("aiff", "audio/aiff"), + // Elementary Dolby and DTS streams. Indexed because VuIO can decode them + // (see `media::transcode`) — before that it could neither play nor + // usefully list them, so they were left out. The MIME types are the + // registered ones: RFC 4184 for AC-3, RFC 4598 for E-AC-3. + ("ac3", "audio/ac3"), + ("eac3", "audio/eac3"), + ("ec3", "audio/eac3"), + ("dts", "audio/vnd.dts"), // Image formats ("jpg", "image/jpeg"), ("jpeg", "image/jpeg"), diff --git a/crates/vuio-core/src/state.rs b/crates/vuio-core/src/state.rs index aaeee1ba..e53839c3 100644 --- a/crates/vuio-core/src/state.rs +++ b/crates/vuio-core/src/state.rs @@ -239,6 +239,13 @@ pub struct AppState { pub upnp_subscriptions: Arc>>, pub radio: Arc, + /// Index cache and concurrency ceiling for decoding AC-3/E-AC-3/DTS. + /// + /// Lives here rather than in the handler because it is shared across + /// requests by design: a renderer's `HEAD`, `GET` and range requests for one + /// file should build its frame index once, not once each. + #[cfg(feature = "transcode")] + pub transcode: Arc, pub cancellation: tokio_util::sync::CancellationToken, pub background_tasks: tokio_util::task::TaskTracker, } @@ -271,6 +278,8 @@ impl Clone for AppState { discovered_tvs: self.discovered_tvs.clone(), upnp_subscriptions: self.upnp_subscriptions.clone(), radio: self.radio.clone(), + #[cfg(feature = "transcode")] + transcode: self.transcode.clone(), cancellation: self.cancellation.clone(), background_tasks: self.background_tasks.clone(), } diff --git a/crates/vuio-core/src/web/admin.rs b/crates/vuio-core/src/web/admin.rs index 5e4b6426..87c2c373 100644 --- a/crates/vuio-core/src/web/admin.rs +++ b/crates/vuio-core/src/web/admin.rs @@ -415,6 +415,64 @@ const WEB_UI_FIELDS: &[FieldSpec] = &[ ), ]; +const TRANSCODE_FIELDS: &[FieldSpec] = &[ + noted( + optional( + "transcode.enabled", + "Offer decoded audio", + FieldKind::Bool, + Impact::Live, + "For films whose audio is AC-3, Dolby Digital Plus or DTS, list a second, \ + already-decoded version beside the original so a TV without those licences \ + can play it with sound.", + ), + "Both versions are offered and the TV picks. One that can already play the original \ + is unaffected.", + ), + noted( + optional( + "transcode.audio_format", + "Decoded audio format", + FieldKind::Enum { + options: &["lpcm", "aac"], + free_form: false, + }, + Impact::Live, + "LPCM is uncompressed and seekable but costs about 1.5 Mbps. AAC is roughly a \ + tenth of that, at the price of a lossy re-encode and no scrubbing.", + ), + "LPCM is the better choice on a wired or 5 GHz network, which is where most of these \ + devices sit.", + ), + noted( + optional( + "transcode.prefer", + "List first", + FieldKind::Enum { + options: &["original", "transcoded"], + free_form: false, + }, + Impact::Live, + "Which of the two versions is listed first for a TV that takes whichever it is \ + given rather than choosing.", + ), + "Leave on \u{201c}original\u{201d} unless a TV plays these films silently: that is the \ + symptom of one that takes the first version without checking whether it can decode it.", + ), + noted( + optional( + "transcode.max_concurrent", + "Simultaneous decodes", + FieldKind::Int { min: 1, max: 32 }, + Impact::Live, + "How many decodes may run at once. Beyond this, a further request is refused \ + rather than queued.", + ), + "Decoding is the only CPU-heavy work this server does. Raising it on a small box \ + trades stutter on the streams already playing for the chance to start another.", + ), +]; + const MCP_FIELDS: &[FieldSpec] = &[ noted( optional( @@ -575,6 +633,16 @@ const SECTIONS: &[SectionSpec] = &[ directories: false, panel: false, }, + SectionSpec { + id: "transcode", + title: "Audio for older TVs", + blurb: "Films often carry AC-3, Dolby Digital Plus or DTS audio, and a TV sold without \ + those licences plays the picture and nothing else. VuIO can decode them and \ + offer a second, playable version beside the original.", + fields: TRANSCODE_FIELDS, + directories: false, + panel: false, + }, SectionSpec { id: "mcp", title: "AI assistants", diff --git a/crates/vuio-core/src/web/mod.rs b/crates/vuio-core/src/web/mod.rs index 471eea3a..b81bb4d8 100644 --- a/crates/vuio-core/src/web/mod.rs +++ b/crates/vuio-core/src/web/mod.rs @@ -17,6 +17,8 @@ pub mod radio; pub mod soap; pub mod streaming; pub mod subtitles; +#[cfg(feature = "transcode")] +pub mod transcode_streaming; #[cfg(feature = "dashboard")] pub mod ui; pub mod xml; @@ -235,6 +237,15 @@ pub fn create_router( get(radio::serve_stream_with_extension::), ); + // Public for the same reason `/media/{id}` is: a TV playing the decoded + // version of a film has nowhere to put a login either. + #[cfg(feature = "transcode")] + let router = router.route( + "/media/{id}/transcode/audio.wav", + get(transcode_streaming::serve_transcoded_wav::) + .head(transcode_streaming::serve_transcoded_wav::), + ); + #[cfg(feature = "casting")] let router = router .route( diff --git a/crates/vuio-core/src/web/streaming.rs b/crates/vuio-core/src/web/streaming.rs index 773046ff..1739f247 100644 --- a/crates/vuio-core/src/web/streaming.rs +++ b/crates/vuio-core/src/web/streaming.rs @@ -316,7 +316,7 @@ pub(crate) fn media_id_from_path_segment(segment: &str) -> Option { } // Helper function to parse range header manually -fn parse_range_header(range_str: &str, file_size: u64) -> Result<(u64, u64), AppError> { +pub(crate) fn parse_range_header(range_str: &str, file_size: u64) -> Result<(u64, u64), AppError> { if file_size == 0 { return Err(AppError::InvalidRange); } diff --git a/crates/vuio-core/src/web/transcode_streaming.rs b/crates/vuio-core/src/web/transcode_streaming.rs new file mode 100644 index 00000000..08a11560 --- /dev/null +++ b/crates/vuio-core/src/web/transcode_streaming.rs @@ -0,0 +1,364 @@ +//! Serving AC-3, E-AC-3 and DTS as audio a renderer can actually play. +//! +//! A separate handler from [`super::streaming::serve_media`] rather than a mode +//! of it, because almost every assumption differs. That one opens a file and +//! streams bytes out of it; this one has no file to open — the bytes do not +//! exist until they are decoded — so its length comes from a plan, its DLNA +//! headers have to declare a conversion (`DLNA.ORG_CI=1`, hardcoded to `0` +//! there), and its work is CPU-bound and therefore rationed. +//! +//! What it keeps is the contract a renderer depends on: an exact +//! `Content-Length`, `Accept-Ranges: bytes`, and real 206 responses. Decoded PCM +//! is constant-bitrate, so a byte offset divides cleanly back into a sample and +//! a seek is genuinely a seek. That is worth the pass over the file's headers it +//! takes to know where the frames are. + +use axum::{ + body::Body, + extract::{Path, State}, + http::{header, HeaderMap, Method, StatusCode}, + response::{IntoResponse, Response}, +}; +use std::sync::Arc; +use tracing::{debug, warn}; + +use crate::media::transcode::{AudioPlan, IndexKey, PcmDecoder, TranscodeCodec}; +use crate::{database::DatabaseManager, error::AppError, state::AppState}; + +use super::streaming::{media_id_from_path_segment, parse_range_header}; + +/// How many decoded frames may sit between the decoder and the socket. +/// +/// Small on purpose. This is the backpressure that stops a renderer which opens +/// a stream and then reads slowly from pulling a whole film through the decoder +/// and into memory; a handful of frames is a fraction of a second of audio. +const PIPELINE_DEPTH: usize = 8; + +/// `GET`/`HEAD /media/{id}/transcode/audio.wav`. +pub async fn serve_transcoded_wav( + State(state): State>, + Path(id): Path, + method: Method, + headers: HeaderMap, +) -> Result { + let Some(file_id) = media_id_from_path_segment(&id) else { + return Err(AppError::NotFound); + }; + if !state.config.transcode.enabled { + return Err(AppError::NotFound); + } + + let file = state + .database + .get_file_location_by_id(file_id) + .await? + .ok_or(AppError::NotFound)?; + + // The codec comes from what the scanner recorded, not from opening the file: + // this handler is reached by a renderer that was told the resource exists, + // and re-probing here would repeat work the scan already did. + let Some(codec) = codec_for(&file.mime_type, &file.filename) else { + return Err(AppError::NotFound); + }; + if !codec.is_decodable() { + debug!( + "refusing to transcode {} — this build has no {} decoder", + file.filename, + codec.as_str() + ); + return Err(AppError::NotFound); + } + + // Ration the CPU before doing any of it. A refusal here is deliberate: a + // renderer told to wait looks to its user like a file that will not open, + // and the streams already playing would lose CPU to it meanwhile. + let Some(permit) = state.transcode.try_acquire() else { + warn!( + "refusing to transcode {}: all {} transcode slots are in use", + file.filename, state.config.transcode.max_concurrent + ); + return Ok(( + StatusCode::SERVICE_UNAVAILABLE, + [(header::RETRY_AFTER, "5")], + "All transcoding slots are in use.", + ) + .into_response()); + }; + + let plan = plan_for(&state, file_id, &file.path, codec).await?; + let total = plan.wav_size(); + + // Range handling is byte-identical to the passthrough path — the resource + // just happens not to exist on disk. + let (start, end) = match headers.get(header::RANGE) { + Some(value) => { + let text = value.to_str().map_err(|_| AppError::InvalidRange)?; + parse_range_header(text, total)? + } + None => (0, total.saturating_sub(1)), + }; + let len = end.saturating_sub(start) + 1; + let partial = headers.contains_key(header::RANGE); + + let mut response = Response::builder() + .status(if partial { + StatusCode::PARTIAL_CONTENT + } else { + StatusCode::OK + }) + .header(header::CONTENT_TYPE, "audio/vnd.wave; codec=1") + .header(header::CONTENT_LENGTH, len) + .header(header::ACCEPT_RANGES, "bytes") + .header("transferMode.dlna.org", "Streaming") + // CI=1 says this resource was converted rather than served as stored. + // OP=11 keeps byte-range seeking, which constant-bitrate PCM genuinely + // supports — the whole reason the frame index exists. + .header( + "contentFeatures.dlna.org", + "DLNA.ORG_OP=11;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000", + ); + if partial { + response = response.header( + header::CONTENT_RANGE, + format!("bytes {start}-{end}/{total}"), + ); + } + + if method == Method::HEAD { + // The permit is dropped here: a HEAD decoded nothing and holding a slot + // for it would let a renderer that probes before playing starve one that + // is playing. + drop(permit); + return Ok(response.body(Body::empty())?); + } + + Ok(response.body(pcm_body(plan, start, len, permit))?) +} + +/// Fetch a cached plan, or build one off the async runtime. +async fn plan_for( + state: &AppState, + file_id: i64, + path: &std::path::Path, + codec: TranscodeCodec, +) -> Result, AppError> { + let metadata = tokio::fs::metadata(path).await?; + let key = IndexKey { + id: file_id, + size: metadata.len(), + modified: metadata + .modified() + .ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + .unwrap_or(0), + }; + + if let Some(plan) = state.transcode.cached(&key).await { + return Ok(plan); + } + + let owned = path.to_path_buf(); + let plan = tokio::task::spawn_blocking(move || AudioPlan::build(&owned, codec)) + .await + .map_err(|e| AppError::Internal(anyhow::anyhow!("transcode planner panicked: {e}")))? + .map_err(AppError::Internal)?; + let plan = Arc::new(plan); + state.transcode.remember(key, plan.clone()).await; + Ok(plan) +} + +/// Build the response body: the WAV header, then decoded PCM, clipped to the +/// requested byte range. +/// +/// Decoding runs on a blocking thread and hands frames over a bounded channel, +/// so a renderer that reads slowly slows the decoder down instead of filling +/// memory. The permit rides along and is released when the body is dropped — +/// which is also what happens when a renderer disconnects mid-stream. +fn pcm_body( + plan: Arc, + start: u64, + len: u64, + permit: tokio::sync::OwnedSemaphorePermit, +) -> Body { + let (tx, rx) = tokio::sync::mpsc::channel::>(PIPELINE_DEPTH); + + tokio::task::spawn_blocking(move || { + let _permit = permit; + let seeked = plan.seek(start); + let mut remaining = len; + + // The header, from wherever the range began inside it. + if seeked.header_skip < 44 { + let header = plan.wav_header(); + let slice = &header[seeked.header_skip..]; + let take = slice.len().min(remaining as usize); + if tx + .blocking_send(Ok(bytes::Bytes::copy_from_slice(&slice[..take]))) + .is_err() + { + return; + } + remaining -= take as u64; + } + + if remaining == 0 { + return; + } + + let file = match std::fs::File::open(&plan.source_path) { + Ok(f) => f, + Err(e) => { + let _ = tx.blocking_send(Err(e)); + return; + } + }; + let mut source = std::io::BufReader::with_capacity(256 * 1024, file); + + // A decoder must be primed with the frame it starts on, and AC-3/DTS + // frames overlap by half a window, so the sample right at a seek point + // is reconstructed from state the previous frame carried. Starting one + // frame early and discarding its output removes the transient that + // would otherwise tick at the start of every seek. + let preroll = seeked.frame.saturating_sub(1); + let mut decoder = match prime(&plan, &mut source, preroll) { + Ok(d) => d, + Err(e) => { + let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); + return; + } + }; + + let mut skip = seeked.pcm_skip; + for i in preroll..plan.index.frames.len() { + if remaining == 0 { + break; + } + let frame = plan.index.frames[i]; + let mut raw = vec![0u8; frame.len as usize]; + if read_frame(&mut source, frame.offset, &mut raw).is_err() { + break; + } + let pcm = decoder.decode_or_silence(&raw, frame.samples); + + // Frames before the seek point are decoded for their state only. + if i < seeked.frame { + continue; + } + let pcm = if skip >= pcm.len() { + skip -= pcm.len(); + continue; + } else { + let out = &pcm[skip..]; + skip = 0; + out + }; + let take = pcm.len().min(remaining as usize); + if tx + .blocking_send(Ok(bytes::Bytes::copy_from_slice(&pcm[..take]))) + .is_err() + { + return; + } + remaining -= take as u64; + } + + // A renderer promised `len` bytes must receive `len` bytes. If the file + // shrank under us, or a frame would not read, pad rather than truncate: + // a short body is a failed transfer, silence is a glitch. + while remaining > 0 { + let chunk = remaining.min(64 * 1024) as usize; + if tx + .blocking_send(Ok(bytes::Bytes::from(vec![0u8; chunk]))) + .is_err() + { + return; + } + remaining -= chunk as u64; + } + }); + + Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) +} + +/// Decode every frame up to `upto` so the decoder carries the right state. +fn prime( + plan: &AudioPlan, + source: &mut R, + upto: usize, +) -> anyhow::Result { + let first = plan.index.frames[upto]; + let mut raw = vec![0u8; first.len as usize]; + read_frame(source, first.offset, &mut raw)?; + let (decoder, _) = PcmDecoder::open( + plan.codec, + plan.index.sample_rate, + Some(plan.channels), + &raw, + )?; + Ok(decoder) +} + +fn read_frame( + source: &mut R, + offset: u64, + into: &mut [u8], +) -> std::io::Result<()> { + source.seek(std::io::SeekFrom::Start(offset))?; + source.read_exact(into) +} + +/// Which codec a library entry holds, from what the scanner recorded. +/// +/// The MIME type is the primary signal because it is what the scanner assigned +/// and what the DIDL writer will have advertised. The extension is a fallback +/// for a library indexed before those MIME types existed. +pub(crate) fn codec_for(mime: &str, filename: &str) -> Option { + match mime { + "audio/ac3" => return Some(TranscodeCodec::Ac3), + "audio/eac3" => return Some(TranscodeCodec::Eac3), + "audio/vnd.dts" => return Some(TranscodeCodec::Dts), + _ => {} + } + let ext = filename.rsplit('.').next()?.to_ascii_lowercase(); + match ext.as_str() { + "ac3" => Some(TranscodeCodec::Ac3), + "eac3" | "ec3" => Some(TranscodeCodec::Eac3), + "dts" => Some(TranscodeCodec::Dts), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn codec_is_read_from_the_mime_the_scanner_assigned() { + assert_eq!(codec_for("audio/ac3", "x.bin"), Some(TranscodeCodec::Ac3)); + assert_eq!(codec_for("audio/eac3", "x.bin"), Some(TranscodeCodec::Eac3)); + assert_eq!( + codec_for("audio/vnd.dts", "x.bin"), + Some(TranscodeCodec::Dts) + ); + } + + #[test] + fn a_library_indexed_before_those_mime_types_falls_back_to_the_extension() { + assert_eq!( + codec_for("application/octet-stream", "Movie.AC3"), + Some(TranscodeCodec::Ac3) + ); + assert_eq!( + codec_for("application/octet-stream", "track.ec3"), + Some(TranscodeCodec::Eac3) + ); + } + + #[test] + fn anything_that_already_plays_everywhere_is_not_claimed() { + assert_eq!(codec_for("audio/mpeg", "song.mp3"), None); + assert_eq!(codec_for("audio/flac", "song.flac"), None); + assert_eq!(codec_for("video/x-matroska", "film.mkv"), None); + } +} diff --git a/crates/vuio-core/tests/audio_integration_tests.rs b/crates/vuio-core/tests/audio_integration_tests.rs index 32224249..228601f8 100644 --- a/crates/vuio-core/tests/audio_integration_tests.rs +++ b/crates/vuio-core/tests/audio_integration_tests.rs @@ -343,6 +343,8 @@ async fn test_cover_art_retrieval_and_xml() { discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; @@ -510,6 +512,8 @@ https://cast1.asurahosting.com/proxy/julien/stream discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; diff --git a/crates/vuio-core/tests/issue_24_pagination.rs b/crates/vuio-core/tests/issue_24_pagination.rs index 812620f9..c3613cc1 100644 --- a/crates/vuio-core/tests/issue_24_pagination.rs +++ b/crates/vuio-core/tests/issue_24_pagination.rs @@ -144,6 +144,8 @@ async fn issue_24_philips_probe_reports_full_total_and_supports_followup_pages() discovered_tvs: Arc::new(RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; @@ -249,6 +251,8 @@ async fn dlna_browse_returns_naturally_sorted_episodes() { discovered_tvs: Arc::new(RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; diff --git a/crates/vuio-core/tests/mcp_integration_tests.rs b/crates/vuio-core/tests/mcp_integration_tests.rs index 789f2ede..0f34f02a 100644 --- a/crates/vuio-core/tests/mcp_integration_tests.rs +++ b/crates/vuio-core/tests/mcp_integration_tests.rs @@ -137,6 +137,8 @@ async fn make_test_state() -> (TempDir, AppState) { discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; diff --git a/crates/vuio-core/tests/mediainfo_integration_tests.rs b/crates/vuio-core/tests/mediainfo_integration_tests.rs index 5a24fdf8..0d9f9a54 100644 --- a/crates/vuio-core/tests/mediainfo_integration_tests.rs +++ b/crates/vuio-core/tests/mediainfo_integration_tests.rs @@ -292,6 +292,8 @@ async fn state_with(database: Arc, temp: &TempDir) -> AppState (TempDir, AppState) { discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; diff --git a/crates/vuio-core/tests/samsungtv_browse.rs b/crates/vuio-core/tests/samsungtv_browse.rs index f0564cdb..efbdc235 100644 --- a/crates/vuio-core/tests/samsungtv_browse.rs +++ b/crates/vuio-core/tests/samsungtv_browse.rs @@ -172,6 +172,8 @@ async fn samsungtv_state_with_video(temp: &tempfile::TempDir) -> AppState { discovered_tvs: Arc::new(RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), } diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs new file mode 100644 index 00000000..435462f0 --- /dev/null +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -0,0 +1,340 @@ +//! Serving AC-3 as audio a renderer without a Dolby licence can play. +//! +//! These drive the real router, so what they assert is what a TV receives. The +//! contract that matters is the one a DLNA renderer depends on and cannot +//! recover from if it is wrong: the `Content-Length` promised to a `HEAD` is the +//! number of bytes a `GET` delivers, and a byte range returns exactly the slice +//! of the full decode that lives at that offset. + +#![cfg(feature = "transcode-ac3")] + +use axum::{ + body::Body, + extract::ConnectInfo, + http::{header, Method, Request, StatusCode}, +}; +use std::net::SocketAddr; +use std::sync::Arc; +use tempfile::{tempdir, TempDir}; +use tower::ServiceExt; + +use vuio_core::config::{AppConfig, MonitoredDirectoryConfig, ValidationMode}; +use vuio_core::database::sqlite::SqliteDatabase; +use vuio_core::database::{DatabaseManager, MediaFile, MediaRepository}; +use vuio_core::platform::filesystem::create_platform_filesystem_manager; +use vuio_core::platform::PlatformInfo; +use vuio_core::state::AppState; +use vuio_core::web::diagnostics::WebHandlerMetrics; +use vuio_core::web::{create_router, Surface}; + +/// A real 48 kHz stereo AC-3 bitstream — the same ffmpeg-encoded 440 Hz sine the +/// vendored decoder validates itself against. Using it here rather than a +/// synthetic stub is the point: the response is only meaningful if the bytes +/// really decode. +const AC3: &[u8] = include_bytes!("../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); + +/// One AC-3 file in a library, and the router over it. +async fn library() -> (TempDir, AppState, i64) { + let temp = tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + let path = root.join("sine.ac3"); + std::fs::write(&path, AC3).unwrap(); + + let database = Arc::new( + SqliteDatabase::new(temp.path().join("transcode.db")) + .await + .unwrap(), + ); + database.initialize().await.unwrap(); + database + .bulk_store_media_files(&[MediaFile::new( + path.clone(), + AC3.len() as u64, + "audio/ac3".to_string(), + )]) + .await + .unwrap(); + let id = database + .collect_all_media_files() + .await + .unwrap() + .first() + .unwrap() + .id + .unwrap(); + + let mut config = AppConfig::default(); + config.media.directories = vec![MonitoredDirectoryConfig { + path: root.to_string_lossy().into_owned(), + recursive: true, + case_sensitive: None, + extensions: None, + exclude_patterns: None, + validation_mode: ValidationMode::Skip, + }]; + let config = Arc::new(config); + + let state = AppState { + media_directories: Arc::new(tokio::sync::RwLock::new(config.media.directories.clone())), + unavailable_roots: Arc::new(tokio::sync::RwLock::new(std::collections::HashSet::new())), + config: config.clone(), + config_source: Arc::new(Default::default()), + http_binding: Arc::new(vuio_core::state::HttpBinding::new(8080)), + live_config: Arc::new(vuio_core::state::LiveConfig::new(config)), + database, + auth: Arc::new(vuio_core::web::auth::AuthState::testing()), + platform_info: Arc::new(PlatformInfo::detect().await.unwrap()), + filesystem_manager: Arc::from(create_platform_filesystem_manager()), + content_update_id: Arc::new(std::sync::atomic::AtomicU32::new(1)), + web_metrics: Arc::new(WebHandlerMetrics::new()), + runtime_diagnostics: Arc::new( + vuio_core::platform::diagnostics::SystemDiagnosticsSampler::new(), + ), + lifecycle_stats: Arc::new(vuio_core::lifecycle::ApplicationStats::new()), + bookmarks: Arc::new(tokio::sync::Mutex::new( + vuio_core::runtime_state::BookmarkRegistry::new( + vuio_core::runtime_state::BOOKMARK_MAX_ENTRIES, + ), + )), + log_file_path: temp.path().join("vuio.log"), + browse_cache: Arc::new(tokio::sync::Mutex::new( + vuio_core::runtime_state::BrowseResponseCache::new(), + )), + active_monitors: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + active_casts: Arc::new(tokio::sync::Mutex::new( + vuio_core::runtime_state::ActiveCastRegistry::new(), + )), + #[cfg(feature = "mediainfo")] + mediainfo_job: Arc::new(tokio::sync::Mutex::new(Default::default())), + #[cfg(feature = "casting")] + discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), + upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), + cancellation: tokio_util::sync::CancellationToken::new(), + background_tasks: tokio_util::task::TaskTracker::new(), + }; + + (temp, state, id) +} + +fn peer() -> SocketAddr { + "127.0.0.1:50000".parse().unwrap() +} + +async fn request( + state: &AppState, + id: i64, + method: Method, + range: Option<&str>, +) -> (StatusCode, axum::http::HeaderMap, Vec) { + let mut builder = Request::builder() + .method(method) + .uri(format!("/media/{id}/transcode/audio.wav")) + .extension(ConnectInfo(peer())); + if let Some(range) = range { + builder = builder.header(header::RANGE, range); + } + let response = create_router(state.clone(), Surface::Primary) + .oneshot(builder.body(Body::empty()).unwrap()) + .await + .unwrap(); + let status = response.status(); + let headers = response.headers().clone(); + let body = axum::body::to_bytes(response.into_body(), 64 * 1024 * 1024) + .await + .unwrap() + .to_vec(); + (status, headers, body) +} + +fn header_u64(headers: &axum::http::HeaderMap, name: header::HeaderName) -> u64 { + headers[&name].to_str().unwrap().parse().unwrap() +} + +#[tokio::test] +async fn an_ac3_file_is_served_as_playable_wav() { + let (_temp, state, id) = library().await; + let (status, headers, body) = request(&state, id, Method::GET, None).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "audio/vnd.wave; codec=1"); + assert_eq!(headers[header::ACCEPT_RANGES], "bytes"); + + // A renderer matching on protocolInfo has to be told this was converted, or + // it may assume the bytes are the stored file. + let features = headers["contentFeatures.dlna.org"].to_str().unwrap(); + assert!( + features.contains("DLNA.ORG_CI=1"), + "a transcoded resource must declare the conversion: {features}" + ); + + assert_eq!(&body[0..4], b"RIFF"); + assert_eq!(&body[8..12], b"WAVE"); + assert_eq!(&body[36..40], b"data"); + assert_eq!( + body.len() as u64, + header_u64(&headers, header::CONTENT_LENGTH), + "the body must be exactly as long as the header promised" + ); + + // 16-bit stereo at 48 kHz, and the payload length the header declares must + // be the payload actually delivered. + assert_eq!(u16::from_le_bytes([body[22], body[23]]), 2, "channels"); + assert_eq!( + u32::from_le_bytes([body[24], body[25], body[26], body[27]]), + 48_000, + "sample rate" + ); + let declared = u32::from_le_bytes([body[40], body[41], body[42], body[43]]) as usize; + assert_eq!(declared, body.len() - 44); + + // The fixture is a 440 Hz sine, so silence here would mean we produced a + // correctly-shaped empty response instead of decoding anything. + let mut sum = 0f64; + for c in body[44..].chunks_exact(2) { + let v = i16::from_le_bytes([c[0], c[1]]) as f64; + sum += v * v; + } + let rms = (sum / ((body.len() - 44) as f64 / 2.0)).sqrt(); + assert!(rms > 100.0, "decoded audio is silent (rms {rms})"); +} + +#[tokio::test] +async fn head_promises_the_length_that_get_delivers() { + let (_temp, state, id) = library().await; + let (head_status, head_headers, head_body) = request(&state, id, Method::HEAD, None).await; + let (_, get_headers, get_body) = request(&state, id, Method::GET, None).await; + + assert_eq!(head_status, StatusCode::OK); + assert!(head_body.is_empty(), "HEAD carries no body"); + assert_eq!( + header_u64(&head_headers, header::CONTENT_LENGTH), + header_u64(&get_headers, header::CONTENT_LENGTH), + ); + assert_eq!(get_body.len() as u64, header_u64(&head_headers, header::CONTENT_LENGTH)); +} + +#[tokio::test] +async fn a_byte_range_returns_exactly_that_slice_of_the_full_decode() { + let (_temp, state, id) = library().await; + let (_, _, whole) = request(&state, id, Method::GET, None).await; + + // A range starting inside the audio, deliberately not on a frame boundary, + // so the seek has to land mid-frame and discard the right number of samples. + let start = 44 + 1536 * 2 * 2 + 5000; + let end = start + 20_000; + let (status, headers, part) = + request(&state, id, Method::GET, Some(&format!("bytes={start}-{end}"))).await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + headers[header::CONTENT_RANGE].to_str().unwrap(), + format!("bytes {start}-{end}/{}", whole.len()) + ); + assert_eq!(part.len(), end - start + 1); + assert_eq!( + part, + whole[start..=end], + "a range must be byte-identical to that slice of the whole" + ); +} + +#[tokio::test] +async fn a_range_spanning_the_header_boundary_is_still_exact() { + let (_temp, state, id) = library().await; + let (_, _, whole) = request(&state, id, Method::GET, None).await; + + // Straddles the 44-byte header and the first samples — the case where the + // header and the decode both have to contribute to one response. + let (status, _, part) = request(&state, id, Method::GET, Some("bytes=20-2043")).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!(part.len(), 2024); + assert_eq!(part, whole[20..=2043]); +} + +#[tokio::test] +async fn a_range_past_the_end_is_refused_rather_than_truncated() { + let (_temp, state, id) = library().await; + let (_, _, whole) = request(&state, id, Method::GET, None).await; + let past = whole.len() + 10; + let (status, _, _) = + request(&state, id, Method::GET, Some(&format!("bytes={past}-"))).await; + assert_eq!(status, StatusCode::RANGE_NOT_SATISFIABLE); +} + +#[tokio::test] +async fn turning_the_feature_off_withdraws_the_resource() { + let (_temp, mut state, id) = library().await; + let mut config = (*state.config).clone(); + config.transcode.enabled = false; + state.config = Arc::new(config); + + let (status, _, _) = request(&state, id, Method::GET, None).await; + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn a_file_that_needs_no_transcoding_has_no_transcoded_resource() { + let (temp, state, _) = library().await; + let mp3 = temp.path().join("media").join("song.mp3"); + std::fs::write(&mp3, b"not really an mp3").unwrap(); + state + .database + .bulk_store_media_files(&[MediaFile::new(mp3, 17, "audio/mpeg".to_string())]) + .await + .unwrap(); + let id = state + .database + .collect_all_media_files() + .await + .unwrap() + .iter() + .find(|f| f.filename == "song.mp3") + .unwrap() + .id + .unwrap(); + + let (status, _, _) = request(&state, id, Method::GET, None).await; + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn concurrent_transcodes_are_capped_rather_than_queued() { + let (_temp, state, id) = library().await; + // One slot, so the second request must be refused outright. + let state = AppState { + transcode: Arc::new(vuio_core::media::transcode::TranscodeState::new(1)), + ..state + }; + + let router = create_router(state.clone(), Surface::Primary); + let first = router + .clone() + .oneshot( + Request::builder() + .uri(format!("/media/{id}/transcode/audio.wav")) + .extension(ConnectInfo(peer())) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(first.status(), StatusCode::OK); + + // The first response's body still holds the permit until it is consumed. + let second = router + .oneshot( + Request::builder() + .uri(format!("/media/{id}/transcode/audio.wav")) + .extension(ConnectInfo(peer())) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(second.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!(second.headers()[header::RETRY_AFTER], "5"); +} diff --git a/crates/vuio-core/tests/web_ui_integration_tests.rs b/crates/vuio-core/tests/web_ui_integration_tests.rs index 1b5e0759..ad1b4e49 100644 --- a/crates/vuio-core/tests/web_ui_integration_tests.rs +++ b/crates/vuio-core/tests/web_ui_integration_tests.rs @@ -124,6 +124,8 @@ async fn library() -> (TempDir, AppState, PathBuf) { discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), cancellation: tokio_util::sync::CancellationToken::new(), background_tasks: tokio_util::task::TaskTracker::new(), }; From 298038258e988acc7e4a4d72e78e1e9b05e03818 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 12:23:47 +0300 Subject: [PATCH 04/38] feat(discovery): advertise a decoded beside the original MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An AC-3/E-AC-3/DTS item now carries two resources in its DIDL: the file as stored, and the decoded alternative at /media/{id}/transcode/audio.wav. Both are offered and the renderer picks. That is the design, not a compromise. There is no reliable table of which television model licensed which codec — brands dropped DTS at different times, and the same model differs by region — so any guess we made would be wrong for someone, and wrong in the direction of silence. Offering both and letting protocolInfo matching decide is what the standard is for. The decoded resource carries a different MIME precisely so that matching can tell them apart, and DLNA.ORG_CI=1 to say the bytes were produced rather than stored; every other this server writes says CI=0. `prefer` handles the renderers that take the first resource without checking. It defaults to "original", which cannot make anything worse: a set that can already play AC-3 sees exactly the DIDL it saw before. Someone whose TV still plays silently flips it. The alternative is only advertised when this build can actually decode the codec — is_decodable() is a compile-time constant asked at runtime — because advertising a resource we cannot produce turns a silent film into a broken one. Both DIDL writers emit it: the indexed path and the older fallback. They were already near-duplicates, and an item must not lose its alternative depending on which one happened to serve it. The browse cache is keyed by client profile already, so per-profile output cannot cross-contaminate. Verified: 12 integration tests, including two elements for an AC-3 item browsed as a Samsung set, the ordering flipping with `prefer`, and exactly one with the feature off. --- crates/vuio-core/src/web/mod.rs | 60 ++++++++ .../src/web/soap/content_directory.rs | 2 + crates/vuio-core/src/web/soap/music.rs | 1 + crates/vuio-core/src/web/xml.rs | 2 +- crates/vuio-core/src/web/xml/browse.rs | 33 +++++ crates/vuio-core/src/web/xml/rendering.rs | 90 ++++++++++++ .../tests/transcode_integration_tests.rs | 135 ++++++++++++++++++ 7 files changed, 322 insertions(+), 1 deletion(-) diff --git a/crates/vuio-core/src/web/mod.rs b/crates/vuio-core/src/web/mod.rs index b81bb4d8..c783c9e4 100644 --- a/crates/vuio-core/src/web/mod.rs +++ b/crates/vuio-core/src/web/mod.rs @@ -24,6 +24,66 @@ pub mod ui; pub mod xml; use crate::{database::DatabaseManager, state::AppState}; + +/// Whether this item is one a renderer may be unable to play unaided. +/// +/// True only when the codec is AC-3, E-AC-3 or DTS *and* this build can decode +/// it — advertising a resource we cannot produce would turn a silent film into +/// a broken one. The recorded codec is consulted first because it is what a +/// container's audio track will be identified by; the MIME type and filename +/// cover an elementary stream, including one indexed before those MIME types +/// existed. +#[cfg_attr(not(feature = "transcode"), allow(unused_variables))] +pub(crate) fn item_needs_transcode(codec: Option<&str>, mime: &str, filename: &str) -> bool { + #[cfg(not(feature = "transcode"))] + { + false + } + #[cfg(feature = "transcode")] + { + use crate::media::transcode::TranscodeCodec; + codec + .and_then(TranscodeCodec::from_stored_codec) + .or_else(|| transcode_streaming::codec_for(mime, filename)) + .is_some_and(TranscodeCodec::is_decodable) + } +} + +/// How this server should advertise a decoded alternative, if at all. +/// +/// One place decides, so the two DIDL writers cannot drift apart on it, and the +/// feature gate lives here rather than in the XML. +pub(crate) fn transcode_advert( + state: &AppState, +) -> Option { + #[cfg(not(feature = "transcode"))] + { + let _ = state; + None + } + #[cfg(feature = "transcode")] + { + use crate::config::{TranscodeAudioFormat, TranscodePreference}; + let config = state.current_config(); + if !config.transcode.enabled { + return None; + } + Some(xml::TranscodeAdvert { + // The MIME differs from the original's, which is what lets a + // renderer that matches against its own sink protocolInfo pick the + // one it can actually decode. + mime: match config.transcode.audio_format { + TranscodeAudioFormat::Lpcm => "audio/vnd.wave", + TranscodeAudioFormat::Aac => "audio/aac", + }, + path: match config.transcode.audio_format { + TranscodeAudioFormat::Lpcm => "transcode/audio.wav", + TranscodeAudioFormat::Aac => "transcode/audio.aac", + }, + first: config.transcode.prefer == TranscodePreference::Transcoded, + }) + } +} use axum::{ extract::DefaultBodyLimit, middleware, diff --git a/crates/vuio-core/src/web/soap/content_directory.rs b/crates/vuio-core/src/web/soap/content_directory.rs index 146cc94b..50a71155 100644 --- a/crates/vuio-core/src/web/soap/content_directory.rs +++ b/crates/vuio-core/src/web/soap/content_directory.rs @@ -191,6 +191,7 @@ impl ContentDirectoryHandler { autoplay_enabled: state.current_config().media.autoplay_enabled, update_id: current_update_id, bookmarks, + transcode: crate::web::transcode_advert(state), }; let canonical_parent = canonical_browse_path.to_string_lossy().into_owned(); let mime_family = media_type_filter.to_owned(); @@ -377,6 +378,7 @@ impl ContentDirectoryHandler { autoplay_enabled: state.current_config().media.autoplay_enabled, update_id: state.content_update_id.load(Ordering::SeqCst), bookmarks: state.bookmarks.lock().await.snapshot(), + transcode: crate::web::transcode_advert(state), }; let starting_index = params.starting_index as usize; let requested_count = browse_page_limit(params); diff --git a/crates/vuio-core/src/web/soap/music.rs b/crates/vuio-core/src/web/soap/music.rs index 2d087ca8..9102994e 100644 --- a/crates/vuio-core/src/web/soap/music.rs +++ b/crates/vuio-core/src/web/soap/music.rs @@ -635,6 +635,7 @@ pub(super) async fn render_context( autoplay_enabled: state.current_config().media.autoplay_enabled, update_id: state.content_update_id.load(Ordering::SeqCst), bookmarks, + transcode: crate::web::transcode_advert(state), } } diff --git a/crates/vuio-core/src/web/xml.rs b/crates/vuio-core/src/web/xml.rs index b57a50c1..97e78af5 100644 --- a/crates/vuio-core/src/web/xml.rs +++ b/crates/vuio-core/src/web/xml.rs @@ -18,7 +18,7 @@ pub use browse::*; pub use descriptions::*; pub use rendering::{ container_class, generate_indexed_browse_response, generate_indexed_items_response, - BrowseRenderContext, ContainerSpec, + BrowseRenderContext, TranscodeAdvert, ContainerSpec, }; #[cfg(test)] diff --git a/crates/vuio-core/src/web/xml/browse.rs b/crates/vuio-core/src/web/xml/browse.rs index 16b92aa4..953a2e3a 100644 --- a/crates/vuio-core/src/web/xml/browse.rs +++ b/crates/vuio-core/src/web/xml/browse.rs @@ -235,6 +235,29 @@ pub async fn generate_browse_response( file.size.to_string() }; + // The same second resource the indexed path offers. Kept in step + // with `rendering.rs` deliberately: an item reached through this + // fallback must not lose the alternative it would have been given + // through the other. + let transcoded = crate::web::transcode_advert(state) + .filter(|_| !is_radio) + .filter(|_| { + crate::web::item_needs_transcode( + file.stream.codec.as_deref(), + &file.mime_type, + &file.filename, + ) + }); + if let Some(advert) = transcoded.filter(|a| a.first) { + let _ = advert.write_didl( + &mut didl, + &server_ip, + state.http_binding.port(), + file_id, + duration_secs, + ); + } + let _ = write!( &mut didl, r#", + /// Whether, and how, to offer a decoded alternative for AC-3/DTS items. + /// + /// `None` in a build with no decoder, or with `[transcode] enabled = false` + /// — either way the writers emit exactly the one `` they always did. + pub transcode: Option, +} + +/// How a decoded alternative resource should be advertised. +/// +/// A plain value rather than a read of the config, so the XML writers stay +/// feature-blind and a test can set up either case directly. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TranscodeAdvert { + /// MIME type of the decoded resource. + pub mime: &'static str, + /// Path suffix under `/media/{id}/` that serves it. + pub path: &'static str, + /// Whether the decoded resource is listed before the original. + pub first: bool, +} + +impl TranscodeAdvert { + /// Write the decoded alternative for `file_id`. + /// + /// `DLNA.ORG_CI=1` is the conversion indicator: a renderer matching on + /// protocolInfo has to be told these bytes were produced rather than stored, + /// and every other `` this server writes says `CI=0`. + fn write( + &self, + output: &mut W, + context: &BrowseRenderContext, + file_id: i64, + duration: Option, + ) -> std::fmt::Result { + self.write_didl( + output, + &context.server_ip, + context.server_port, + file_id, + duration, + ) + } + + /// The same, for the fallback writer, which carries its parts loose rather + /// than in a context. + pub(crate) fn write_didl( + &self, + output: &mut W, + server_ip: &str, + server_port: u16, + file_id: i64, + duration: Option, + ) -> std::fmt::Result { + write!( + output, + r#"http://{}:{}/media/{}/{}", + server_ip, server_port, file_id, self.path + ) + } } /// UPnP container classes. @@ -377,6 +450,20 @@ pub(super) fn write_media_view( _ => mime, } }; + // A second resource for an item whose audio this renderer may not be able to + // decode. Both are offered and the renderer picks — which is the whole point: + // there is no reliable table of which television model licensed which codec, + // and guessing wrong is worse than letting it choose. `prefer` decides the + // order, for the renderers that take the first without looking. + let transcoded = context + .transcode + .filter(|_| !is_radio) + .filter(|_| crate::web::item_needs_transcode(file.codec(), mime, file.filename())); + let item_duration = file.duration_secs().map(|value| value as u64); + if let Some(advert) = transcoded.filter(|a| a.first) { + advert.write(output, context, file_id, item_duration)?; + } + write!( output, r#"( ">http://{}:{}/media/{}", context.server_ip, context.server_port, file_id )?; + if let Some(advert) = transcoded.filter(|a| !a.first) { + advert.write(output, context, file_id, item_duration)?; + } if context.client == crate::web::client::DlnaClientProfile::LgTv && has_srt { write!( output, diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs index 435462f0..0339f787 100644 --- a/crates/vuio-core/tests/transcode_integration_tests.rs +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -338,3 +338,138 @@ async fn concurrent_transcodes_are_capped_rather_than_queued() { assert_eq!(second.status(), StatusCode::SERVICE_UNAVAILABLE); assert_eq!(second.headers()[header::RETRY_AFTER], "5"); } + +// --------------------------------------------------------------------------- +// What a television is actually told +// +// The stream above is only reachable if the browse response mentions it. These +// drive the real SOAP endpoint, because the DIDL is generated in two places — +// the indexed path and the fallback — and an item must not lose its alternative +// depending on which one served it. +// --------------------------------------------------------------------------- + +use axum::extract::State; +use axum::http::{HeaderMap, HeaderValue}; +use vuio_core::web::soap::content_directory_control; + +fn browse_request(object_id: &str) -> String { + format!( + r#" + + + + {object_id} + BrowseDirectChildren + * + 0 + 50 + + + +"# + ) +} + +/// Browse the audio tree as a Samsung set — one of the profiles that actually +/// dropped DTS support, and the one with the most quirks around ``. +async fn browse_audio(state: &AppState) -> String { + let mut headers = HeaderMap::new(); + headers.insert( + "soapaction", + HeaderValue::from_static("\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\""), + ); + headers.insert( + header::USER_AGENT, + HeaderValue::from_static("DLNADOC/1.50 SEC_HHP_[TV]UE40D7000/1.0"), + ); + let response = + content_directory_control(State(state.clone()), headers, browse_request("audio/!all")).await; + assert_eq!(response.status(), StatusCode::OK); + // DIDL is double-escaped inside the SOAP body; unescape enough to read it. + String::from_utf8( + axum::body::to_bytes(response.into_body(), 1024 * 1024) + .await + .unwrap() + .to_vec(), + ) + .unwrap() + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") +} + +#[tokio::test] +async fn an_ac3_item_is_offered_both_the_original_and_a_decoded_resource() { + let (_temp, state, id) = library().await; + let didl = browse_audio(&state).await; + + assert_eq!( + didl.matches("http://127.0.0.1:8080/media/{id}")) || didl.contains(&format!("/media/{id}"))); +} + +#[tokio::test] +async fn the_original_is_listed_first_by_default() { + let (_temp, state, _) = library().await; + let didl = browse_audio(&state).await; + let original = didl.find("audio/ac3").expect("original res"); + let decoded = didl.find("audio/vnd.wave").expect("decoded res"); + assert!( + original < decoded, + "a renderer that takes the first resource must keep getting the original:\n{didl}" + ); +} + +#[tokio::test] +async fn prefer_transcoded_puts_the_decoded_resource_first() { + let (_temp, mut state, _) = library().await; + let mut config = (*state.config).clone(); + config.transcode.prefer = vuio_core::config::TranscodePreference::Transcoded; + let config = Arc::new(config); + state.config = config.clone(); + state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); + + let didl = browse_audio(&state).await; + let original = didl.find("audio/ac3").expect("original res"); + let decoded = didl.find("audio/vnd.wave").expect("decoded res"); + assert!( + decoded < original, + "the decoded resource must come first when asked for:\n{didl}" + ); +} + +#[tokio::test] +async fn with_the_feature_off_an_item_has_exactly_one_resource() { + let (_temp, mut state, _) = library().await; + let mut config = (*state.config).clone(); + config.transcode.enabled = false; + let config = Arc::new(config); + state.config = config.clone(); + state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); + + let didl = browse_audio(&state).await; + assert_eq!( + didl.matches(" Date: Mon, 24 Aug 2026 12:27:04 +0300 Subject: [PATCH 05/38] feat(transcode): add AAC-LC as the compressed output format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `audio_format = "aac"` now serves /media/{id}/transcode/audio.aac, so the config key offers two real choices rather than one working value and one 404. The trade is explicit in what the resource claims. AAC output size is not known until it has been produced, so there is no honest Content-Length, and the response therefore carries no Accept-Ranges and DLNA.ORG_OP=00: no seeking, rather than advertising ranges it would then refuse. LPCM stays the default precisely because it keeps those. ADTS rather than a container — every frame carries its own header, so the stream is self-describing from any point and needs no muxer, no seek table and no rewrite at the end. The decoder already emits interleaved S16, which is what the encoder takes, so this is a pipe rather than a conversion. Bitrate is left at the encoder's 64 kbps per channel, the conventional AAC-LC operating point and about a tenth of the same audio as LPCM. Deliberately not a config key: it is a knob whose wrong setting is audible and which nothing here benefits from tuning. Also moves the enabled check to the live config rather than the startup snapshot, so the admin UI's Live impact for [transcode] is honest. Verified: 14 integration tests including ADTS framing on the wire, the absent length and range headers, and the DIDL advertising whichever resource the config selected. --- crates/vuio-core/src/media/transcode/aac.rs | 151 ++++++++++++++ crates/vuio-core/src/media/transcode/mod.rs | 4 + crates/vuio-core/src/web/mod.rs | 6 + .../vuio-core/src/web/transcode_streaming.rs | 188 ++++++++++++++---- .../tests/transcode_integration_tests.rs | 70 ++++++- 5 files changed, 381 insertions(+), 38 deletions(-) create mode 100644 crates/vuio-core/src/media/transcode/aac.rs diff --git a/crates/vuio-core/src/media/transcode/aac.rs b/crates/vuio-core/src/media/transcode/aac.rs new file mode 100644 index 00000000..d9fedd06 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/aac.rs @@ -0,0 +1,151 @@ +//! Re-encoding decoded PCM as AAC-LC in ADTS framing. +//! +//! The alternative to LPCM, for a network where 1.5 Mbps of uncompressed audio +//! is not free. It is a lossy re-encode of an already-lossy source, which is why +//! it is not the default, and its output size is not known before it is produced, +//! which is why the resource it serves carries no `Content-Length` and cannot be +//! scrubbed. Those are real costs; the config key exists so an operator who is +//! paying them is choosing to. +//! +//! ADTS rather than a container: every frame carries its own header, so the +//! stream is self-describing from any point and needs no muxer, no seek table +//! and no rewrite at the end. + +use anyhow::{Context, Result}; + +/// One AAC-LC encoder bound to a stream's shape. +pub struct AacEncoder { + inner: Box, + channels: u16, + sample_rate: u32, +} + +impl AacEncoder { + /// Build an encoder producing `channels` at `sample_rate`. + /// + /// The bitrate is the vendored encoder's default of 64 kbps per channel — + /// the conventional AAC-LC "good quality" operating point, and around a + /// tenth of the LPCM the same audio would cost. Left unconfigurable + /// deliberately: it is one more knob whose wrong setting is audible, and + /// nothing about this path benefits from tuning it. + pub fn new(sample_rate: u32, channels: u16) -> Result { + use oxideav_core::{CodecId, CodecParameters, SampleFormat}; + + let mut params = CodecParameters::audio(CodecId::new("aac")); + params.sample_rate = Some(sample_rate); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + + let inner = oxideav_aac::codec_encoder::make_encoder(¶ms) + .map_err(|e| anyhow::anyhow!("AAC encoder: {e}")) + .context("configuring the AAC encoder")?; + + Ok(Self { + inner, + channels, + sample_rate, + }) + } + + /// Feed interleaved S16 and collect whatever ADTS frames come out. + /// + /// The encoder buffers to its own 1024-sample frame length, so a call may + /// well produce nothing; that is normal, not an error. + pub fn push(&mut self, pcm: &[u8]) -> Result> { + use oxideav_core::{AudioFrame, Frame}; + + let samples = pcm.len() / (self.channels as usize * 2); + if samples == 0 { + return Ok(Vec::new()); + } + let frame = Frame::Audio(AudioFrame { + samples: samples as u32, + pts: None, + data: vec![pcm.to_vec()], + }); + self.inner + .send_frame(&frame) + .map_err(|e| anyhow::anyhow!("AAC encode: {e}"))?; + Ok(self.drain()) + } + + /// Flush the encoder's lookahead and overlap, ending the stream cleanly. + pub fn finish(&mut self) -> Vec { + if self.inner.flush().is_err() { + return Vec::new(); + } + self.drain() + } + + /// Sample rate the encoder was configured for. + pub fn sample_rate(&self) -> u32 { + self.sample_rate + } + + fn drain(&mut self) -> Vec { + let mut out = Vec::new(); + // `receive_packet` returns `NeedMore` once drained, which is the normal + // exit rather than a failure. + while let Ok(packet) = self.inner.receive_packet() { + out.extend_from_slice(&packet.data); + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A second of 440 Hz stereo, interleaved S16 — the encoder's input shape. + fn sine(seconds: u32, sample_rate: u32) -> Vec { + let total = seconds * sample_rate; + let mut pcm = Vec::with_capacity(total as usize * 4); + for n in 0..total { + let t = n as f64 / sample_rate as f64; + let v = ((t * 440.0 * std::f64::consts::TAU).sin() * 12_000.0) as i16; + pcm.extend_from_slice(&v.to_le_bytes()); + pcm.extend_from_slice(&v.to_le_bytes()); + } + pcm + } + + #[test] + fn encodes_pcm_into_adts_frames() { + let mut enc = AacEncoder::new(48_000, 2).unwrap(); + let mut out = enc.push(&sine(1, 48_000)).unwrap(); + out.extend_from_slice(&enc.finish()); + + assert!(!out.is_empty(), "a second of audio must produce frames"); + // Every ADTS frame opens with the 12-bit syncword 0xFFF. + assert_eq!(out[0], 0xFF, "ADTS syncword high byte"); + assert_eq!(out[1] & 0xF0, 0xF0, "ADTS syncword low nibble"); + } + + #[test] + fn the_result_is_far_smaller_than_the_pcm_it_came_from() { + let pcm = sine(1, 48_000); + let mut enc = AacEncoder::new(48_000, 2).unwrap(); + let mut out = enc.push(&pcm).unwrap(); + out.extend_from_slice(&enc.finish()); + // The whole reason to offer this format at all. + assert!( + out.len() * 4 < pcm.len(), + "AAC {} bytes vs PCM {} bytes — expected a large saving", + out.len(), + pcm.len() + ); + } + + #[test] + fn an_empty_push_is_not_an_error() { + let mut enc = AacEncoder::new(48_000, 2).unwrap(); + assert!(enc.push(&[]).unwrap().is_empty()); + } + + #[test] + fn a_channel_count_the_encoder_cannot_express_is_refused_at_construction() { + // Seven channels has no Table 1.19 default configuration. + assert!(AacEncoder::new(48_000, 7).is_err()); + } +} diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index 01a86735..0e045c65 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -13,12 +13,16 @@ //! site — it asks [`TranscodeCodec::is_decodable`] and gets an honest answer in //! every build. +#[cfg(feature = "transcode-aac")] +mod aac; mod frames; mod pcm; mod plan; mod session; mod wav; +#[cfg(feature = "transcode-aac")] +pub use aac::AacEncoder; pub use frames::{FrameIndex, IndexedFrame}; pub use pcm::PcmDecoder; pub use plan::{AudioPlan, Seeked}; diff --git a/crates/vuio-core/src/web/mod.rs b/crates/vuio-core/src/web/mod.rs index c783c9e4..846c023a 100644 --- a/crates/vuio-core/src/web/mod.rs +++ b/crates/vuio-core/src/web/mod.rs @@ -305,6 +305,12 @@ pub fn create_router( get(transcode_streaming::serve_transcoded_wav::) .head(transcode_streaming::serve_transcoded_wav::), ); + #[cfg(feature = "transcode-aac")] + let router = router.route( + "/media/{id}/transcode/audio.aac", + get(transcode_streaming::serve_transcoded_aac::) + .head(transcode_streaming::serve_transcoded_aac::), + ); #[cfg(feature = "casting")] let router = router diff --git a/crates/vuio-core/src/web/transcode_streaming.rs b/crates/vuio-core/src/web/transcode_streaming.rs index 08a11560..64096742 100644 --- a/crates/vuio-core/src/web/transcode_streaming.rs +++ b/crates/vuio-core/src/web/transcode_streaming.rs @@ -34,58 +34,120 @@ use super::streaming::{media_id_from_path_segment, parse_range_header}; /// and into memory; a handful of frames is a fraction of a second of audio. const PIPELINE_DEPTH: usize = 8; -/// `GET`/`HEAD /media/{id}/transcode/audio.wav`. -pub async fn serve_transcoded_wav( +/// `GET`/`HEAD /media/{id}/transcode/audio.aac`. +/// +/// The compressed alternative. Unlike the WAV resource this one is chunked: the +/// encoder's output size is not known until it has produced it, so there is no +/// honest `Content-Length` to send and therefore no byte-range seeking either. +/// A renderer gets a stream it can play from the start and not scrub within, +/// which is the trade the operator made by choosing `audio_format = "aac"`. +#[cfg(feature = "transcode-aac")] +pub async fn serve_transcoded_aac( State(state): State>, Path(id): Path, method: Method, - headers: HeaderMap, ) -> Result { - let Some(file_id) = media_id_from_path_segment(&id) else { - return Err(AppError::NotFound); + let (file, codec) = resolve(&state, &id).await?; + let Some(permit) = state.transcode.try_acquire() else { + return Ok(busy(&state, &file.filename)); }; - if !state.config.transcode.enabled { - return Err(AppError::NotFound); - } - - let file = state - .database - .get_file_location_by_id(file_id) - .await? - .ok_or(AppError::NotFound)?; + let plan = plan_for(&state, file.id, &file.path, codec).await?; - // The codec comes from what the scanner recorded, not from opening the file: - // this handler is reached by a renderer that was told the resource exists, - // and re-probing here would repeat work the scan already did. - let Some(codec) = codec_for(&file.mime_type, &file.filename) else { - return Err(AppError::NotFound); - }; - if !codec.is_decodable() { - debug!( - "refusing to transcode {} — this build has no {} decoder", - file.filename, - codec.as_str() + let response = Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "audio/aac") + // No Accept-Ranges: saying "bytes" and then refusing every range is + // worse than never claiming it. + .header("transferMode.dlna.org", "Streaming") + // OP=00 — no seeking, in either the time or byte dimension. CI=1 as + // ever, because these bytes were produced rather than stored. + .header( + "contentFeatures.dlna.org", + "DLNA.ORG_OP=00;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000", ); - return Err(AppError::NotFound); + + if method == Method::HEAD { + drop(permit); + return Ok(response.body(Body::empty())?); } + Ok(response.body(aac_body(plan, permit))?) +} + +/// Decode the whole track and re-encode it, frame by frame. +#[cfg(feature = "transcode-aac")] +fn aac_body(plan: Arc, permit: tokio::sync::OwnedSemaphorePermit) -> Body { + use crate::media::transcode::AacEncoder; + + let (tx, rx) = tokio::sync::mpsc::channel::>(PIPELINE_DEPTH); + + tokio::task::spawn_blocking(move || { + let _permit = permit; + let file = match std::fs::File::open(&plan.source_path) { + Ok(f) => f, + Err(e) => { + let _ = tx.blocking_send(Err(e)); + return; + } + }; + let mut source = std::io::BufReader::with_capacity(256 * 1024, file); + + let mut decoder = match prime(&plan, &mut source, 0) { + Ok(d) => d, + Err(e) => { + let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); + return; + } + }; + let mut encoder = match AacEncoder::new(plan.sample_rate(), plan.channels) { + Ok(e) => e, + Err(e) => { + let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); + return; + } + }; + + for frame in &plan.index.frames { + let mut raw = vec![0u8; frame.len as usize]; + if read_frame(&mut source, frame.offset, &mut raw).is_err() { + break; + } + let pcm = decoder.decode_or_silence(&raw, frame.samples); + match encoder.push(&pcm) { + Ok(adts) if adts.is_empty() => continue, + Ok(adts) => { + if tx.blocking_send(Ok(bytes::Bytes::from(adts))).is_err() { + return; + } + } + Err(_) => break, + } + } + let tail = encoder.finish(); + if !tail.is_empty() { + let _ = tx.blocking_send(Ok(bytes::Bytes::from(tail))); + } + }); + + Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) +} + +/// `GET`/`HEAD /media/{id}/transcode/audio.wav`. +pub async fn serve_transcoded_wav( + State(state): State>, + Path(id): Path, + method: Method, + headers: HeaderMap, +) -> Result { + let (file, codec) = resolve(&state, &id).await?; // Ration the CPU before doing any of it. A refusal here is deliberate: a // renderer told to wait looks to its user like a file that will not open, // and the streams already playing would lose CPU to it meanwhile. let Some(permit) = state.transcode.try_acquire() else { - warn!( - "refusing to transcode {}: all {} transcode slots are in use", - file.filename, state.config.transcode.max_concurrent - ); - return Ok(( - StatusCode::SERVICE_UNAVAILABLE, - [(header::RETRY_AFTER, "5")], - "All transcoding slots are in use.", - ) - .into_response()); + return Ok(busy(&state, &file.filename)); }; - let plan = plan_for(&state, file_id, &file.path, codec).await?; + let plan = plan_for(&state, file.id, &file.path, codec).await?; let total = plan.wav_size(); // Range handling is byte-identical to the passthrough path — the resource @@ -135,6 +197,58 @@ pub async fn serve_transcoded_wav( Ok(response.body(pcm_body(plan, start, len, permit))?) } +/// Look the item up and confirm this build can decode it. +/// +/// A 404 for anything that is not decodable here, rather than an error: the URL +/// describes a resource that, for this file and this build, simply does not +/// exist. Only a renderer that was told about it should be asking. +async fn resolve( + state: &AppState, + id: &str, +) -> Result<(crate::database::FileLocation, TranscodeCodec), AppError> { + let Some(file_id) = media_id_from_path_segment(id) else { + return Err(AppError::NotFound); + }; + if !state.current_config().transcode.enabled { + return Err(AppError::NotFound); + } + let file = state + .database + .get_file_location_by_id(file_id) + .await? + .ok_or(AppError::NotFound)?; + + // The codec comes from what the scanner recorded, not from opening the file: + // re-probing here would repeat work the scan already did. + let Some(codec) = codec_for(&file.mime_type, &file.filename) else { + return Err(AppError::NotFound); + }; + if !codec.is_decodable() { + debug!( + "refusing to transcode {} — this build has no {} decoder", + file.filename, + codec.as_str() + ); + return Err(AppError::NotFound); + } + Ok((file, codec)) +} + +/// Every slot is busy. +fn busy(state: &AppState, filename: &str) -> Response { + warn!( + "refusing to transcode {}: all {} transcode slots are in use", + filename, + state.current_config().transcode.max_concurrent + ); + ( + StatusCode::SERVICE_UNAVAILABLE, + [(header::RETRY_AFTER, "5")], + "All transcoding slots are in use.", + ) + .into_response() +} + /// Fetch a cached plan, or build one off the async runtime. async fn plan_for( state: &AppState, diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs index 0339f787..0adbd275 100644 --- a/crates/vuio-core/tests/transcode_integration_tests.rs +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -270,7 +270,11 @@ async fn turning_the_feature_off_withdraws_the_resource() { let (_temp, mut state, id) = library().await; let mut config = (*state.config).clone(); config.transcode.enabled = false; - state.config = Arc::new(config); + let config = Arc::new(config); + state.config = config.clone(); + // The handler reads the live config, not the startup snapshot, so turning + // this off takes effect on reload rather than at the next restart. + state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); let (status, _, _) = request(&state, id, Method::GET, None).await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -473,3 +477,67 @@ async fn with_the_feature_off_an_item_has_exactly_one_resource() { ); assert!(!didl.contains("DLNA.ORG_CI=1")); } + +// --------------------------------------------------------------------------- +// The compressed alternative +// --------------------------------------------------------------------------- + +#[cfg(feature = "transcode-aac")] +#[tokio::test] +async fn the_aac_resource_streams_adts_without_claiming_to_be_seekable() { + let (_temp, state, id) = library().await; + let response = create_router(state.clone(), Surface::Primary) + .oneshot( + Request::builder() + .uri(format!("/media/{id}/transcode/audio.aac")) + .extension(ConnectInfo(peer())) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers()[header::CONTENT_TYPE], "audio/aac"); + // Its length is not knowable in advance, so it must not claim otherwise — + // and must not advertise ranges it would then refuse. + assert!(response.headers().get(header::CONTENT_LENGTH).is_none()); + assert!(response.headers().get(header::ACCEPT_RANGES).is_none()); + let features = response.headers()["contentFeatures.dlna.org"] + .to_str() + .unwrap() + .to_owned(); + assert!(features.contains("DLNA.ORG_OP=00"), "no seeking: {features}"); + assert!(features.contains("DLNA.ORG_CI=1"), "converted: {features}"); + + let body = axum::body::to_bytes(response.into_body(), 16 * 1024 * 1024) + .await + .unwrap(); + assert!(!body.is_empty()); + assert_eq!(body[0], 0xFF, "ADTS syncword"); + assert_eq!(body[1] & 0xF0, 0xF0, "ADTS syncword"); + // The point of choosing AAC over LPCM. + assert!( + body.len() < AC3.len() * 4, + "AAC output should be far smaller than the equivalent PCM" + ); +} + +#[cfg(feature = "transcode-aac")] +#[tokio::test] +async fn choosing_aac_advertises_the_aac_resource() { + let (_temp, mut state, id) = library().await; + let mut config = (*state.config).clone(); + config.transcode.audio_format = vuio_core::config::TranscodeAudioFormat::Aac; + let config = Arc::new(config); + state.config = config.clone(); + state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); + + let didl = browse_audio(&state).await; + assert!( + didl.contains(&format!("/media/{id}/transcode/audio.aac")), + "the advertised resource must be the one the config selected:\n{didl}" + ); + assert!(didl.contains("audio/aac")); + assert!(!didl.contains("audio.wav")); +} From 11768f0f94c15593683e32f58d7c674281bce7ee Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 12:33:24 +0300 Subject: [PATCH 06/38] docs(transcode): document the feature and re-measure the crate counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds [transcode] to docs/configuration.md, the decoder rows to the vuio-core feature table, the two new routes to CLAUDE.md, and a CLAUDE.md rule for crates/vendor mirroring the existing one for the vuio-web bundle. The feature table's numbers were stale — 217/148 against a tree that now resolves 248/142 — so every row is re-measured rather than leaving fresh numbers beside old ones. Two rows were missing entirely (mediainfo, web-ui). The zeroes now carry their explanation: dashboard and mcp shed compiled code rather than dependencies, and metadata shares symphonia with casting so dropping it alone sheds nothing. The decoders cost 4 crates, not a subtree: thiserror, serde_json and bytemuck were already in the tree, so what they add is about 6 MB of compiled decoder. CI's feature-matrix loop gains the four new features and the mediainfo it had been missing since that feature landed. It also gains a leg that builds and tests with the decoders dropped — these ship on by default, which makes their absence the path that rots unnoticed, so proving a feature builds is no longer the only thing that job needs to prove. The Docker image ships with the decoders, since someone whose television plays a film silently will not rebuild an image to fix it. VUIO_CARGO_FLAGS is there for an operator who knows their renderers and wants them out. --- .github/workflows/ci.yml | 12 +++++++- Dockerfile | 11 ++++++- crates/vuio-core/README.md | 19 +++++++++--- crates/vuio-core/src/media/transcode/mod.rs | 8 +++++ docs/configuration.md | 34 +++++++++++++++++++++ 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ad1b2cc..72634ba8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,11 +233,21 @@ jobs: run: cargo check -p vuio-core --no-default-features - name: Build each feature alone run: | - for feature in casting dashboard diagnostics mcp metadata web-ui; do + for feature in casting dashboard diagnostics mcp mediainfo metadata web-ui \ + transcode transcode-ac3 transcode-dts transcode-aac; do echo "::group::$feature" cargo check -p vuio-core --no-default-features --features "$feature" echo "::endgroup::" done + # The transcode features are on by default, which makes their *absence* + # the path that rots unnoticed. Everything else here proves a feature + # builds; this proves the server still builds and behaves without one. + - name: Build with the decoders dropped + run: | + cargo check -p vuio-core --no-default-features \ + --features casting,dashboard,diagnostics,mcp,mediainfo,metadata,web-ui + cargo test -p vuio-core --lib --no-default-features \ + --features casting,dashboard,diagnostics,mcp,mediainfo,metadata,web-ui,unstable-internals - name: Build with everything run: cargo check -p vuio-core --all-features diff --git a/Dockerfile b/Dockerfile index 3ec73d48..1dd0d1cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,10 +37,19 @@ RUN RUST_TARGET=$(cat /tmp/rust_target) && \ # Copy the workspace and build the server. Only this layer rebuilds when source # changes. `--bin vuio` skips the generate_test_media helper binary. +# +# VUIO_CARGO_FLAGS is for trimming the image. The default build is the whole +# server, including the AC-3/E-AC-3/DTS decoders — the person whose television +# plays a film silently is not going to rebuild an image to fix it. An operator +# who knows their renderers and wants the ~6 MB of decoder out can pass, e.g.: +# +# --build-arg VUIO_CARGO_FLAGS="--no-default-features \ +# --features casting,dashboard,diagnostics,mcp,mediainfo,metadata,web-ui" +ARG VUIO_CARGO_FLAGS="" COPY Cargo.toml Cargo.lock ./ COPY crates ./crates RUN RUST_TARGET=$(cat /tmp/rust_target) && \ - cargo build --release --locked --target $RUST_TARGET --package vuio-cli --bin vuio && \ + cargo build --release --locked --target $RUST_TARGET --package vuio-cli --bin vuio $VUIO_CARGO_FLAGS && \ cp target/$RUST_TARGET/release/vuio /tmp/vuio # ---- Final Stage ---- diff --git a/crates/vuio-core/README.md b/crates/vuio-core/README.md index 262db7c1..d18692b4 100644 --- a/crates/vuio-core/README.md +++ b/crates/vuio-core/README.md @@ -81,16 +81,25 @@ firmware has less to read. | Feature | Gives up when off | Crates | | --- | --- | --- | -| `casting` | Chromecast, AirPlay and DLNA renderer control | 56 | -| `metadata` | tags and embedded cover art (files keep filename titles) | 13 | -| `diagnostics` | system and disk metrics on the status endpoints | 1 | +| `casting` | Chromecast, AirPlay and DLNA renderer control | 44 | +| `mediainfo` | titles, synopses, ratings and artwork from public APIs | 29 | +| `transcode-ac3` / `-dts` / `-aac` | AC-3, E-AC-3 and DTS decoded for renderers that cannot play them | 4 | +| `diagnostics` | system and disk metrics on the status endpoints | 3 | +| `web-ui` | the Svelte interface on the second listener | 1 | +| `metadata` | tags and embedded cover art (files keep filename titles) | 0 | | `dashboard` | the built-in web UI | 0 | | `mcp` | the Model Context Protocol server | 0 | Counts are crates removed from the dependency graph for `aarch64-unknown-linux-musl`, measured with `cargo tree`. The default build -resolves **217** crates and `--no-default-features` resolves **148**. The last -two shed compiled code rather than dependencies. +resolves **248** crates and `--no-default-features` resolves **142**. + +The zeroes are not mistakes. `dashboard` and `mcp` shed compiled code rather +than dependencies. `metadata` shares symphonia with `casting`, so dropping it +alone sheds nothing and dropping both sheds the pair. The three decoders are +vendored under `crates/vendor` and their own dependencies — `thiserror`, +`serde_json`, `bytemuck` — were already in the tree, so they cost four crates +and about 6 MB of compiled decoder rather than a new dependency subtree. What is never gated, because it is what a media server *is*: SSDP discovery, mDNS/DNS-SD advertisement, UPnP ContentDirectory, HTTP range streaming, media diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index 0e045c65..c33fe8f3 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -23,10 +23,18 @@ mod wav; #[cfg(feature = "transcode-aac")] pub use aac::AacEncoder; +// These are the vocabulary of this module's public surface — the element type +// of `FrameIndex::frames`, the result of `AudioPlan::seek`, the header length a +// caller subtracts from an offset. Nothing inside the crate spells some of them +// (field access needs no import), which reads as unused in a build where these +// modules are only `pub(crate)` rather than `pub`. +#[allow(unused_imports)] pub use frames::{FrameIndex, IndexedFrame}; pub use pcm::PcmDecoder; +#[allow(unused_imports)] pub use plan::{AudioPlan, Seeked}; pub use session::{IndexKey, TranscodeState}; +#[allow(unused_imports)] pub use wav::{wav_header, WAV_HEADER_LEN}; /// An audio codec VuIO can decode but many renderers cannot play. diff --git a/docs/configuration.md b/docs/configuration.md index 55563e2c..66c144c2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -138,6 +138,40 @@ session_ttl_hours = 12 # Browser authentication session lifetime allowed_networks = [] # Allowed CIDR blocks (empty restricts to private/loopback) ``` +### `[transcode]` + +Audio for renderers that cannot decode AC-3, Dolby Digital Plus or DTS. Those +codecs are licensed, and a television sold without the licence plays the picture +and nothing else. + +When this is on, an item whose audio is one of the three is listed **twice** in +the browse response — the file as stored, and a decoded alternative at +`/media/{id}/transcode/audio.wav`. Both are offered and the renderer picks the +one it can play. A renderer that was already fine is unaffected. + +```toml +[transcode] +enabled = true # Offer the decoded alternative +audio_format = "lpcm" # "lpcm" or "aac" +prefer = "original" # Which resource is listed first: "original" or "transcoded" +max_concurrent = 2 # Simultaneous decodes; further requests are refused, not queued +``` + +| Key | Effect | +| --- | --- | +| `enabled` | Live. Off leaves the browse response exactly as it was. | +| `audio_format` | `lpcm` is uncompressed, carries an exact `Content-Length` and supports byte-range seeking, and costs about 1.5 Mbps. `aac` is roughly a tenth of that, at the price of a lossy re-encode, no `Content-Length` and no scrubbing. | +| `prefer` | Some renderers take the first resource without checking whether they can decode it. The default keeps the original first, which cannot make anything worse. Switch to `transcoded` if a set still plays silently. | +| `max_concurrent` | Decoding is the only CPU-bound work this server does. Past this ceiling a request gets `503` with `Retry-After` rather than joining a queue that would starve the streams already playing. | + +Environment variables: `VUIO_TRANSCODE_ENABLED`, `VUIO_TRANSCODE_AUDIO_FORMAT`, +`VUIO_TRANSCODE_PREFER`, `VUIO_TRANSCODE_MAX_CONCURRENT`. + +The section is read in every build, including one compiled without the decoders, +so a config file moved between builds is never rejected by the leaner one. A +build without them simply never advertises the alternative — see the feature +table in `crates/vuio-core/README.md`. + --- ## Related Documentation From cf4c955d84abb8b379b6dc669678018602ee23a6 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 12:43:23 +0300 Subject: [PATCH 07/38] fix(transcode): index .ac3/.dts files and decode seeks from clean state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs a live server found that the unit tests could not. The scanner never saw these files. Adding the MIME types was not enough: the extension allowlist is a separate list with four independent copies — the platform default, the Docker env default, the generated config template and the documented example. Every integration test injects MediaFile rows straight into the database, so all of them passed against a scanner that indexed nothing. Seeking decoded the primed frame twice. Opening a decoder necessarily decodes a frame, and the loop then fed that same frame in again, pushing its samples through the IMDCT overlap buffer a second time — so every frame after a seek landed somewhere a sequential decode never goes. It cancels out at frame 1, where the pre-roll is frame 0 and a whole-file decode primes on frame 0 too, which is exactly where the existing range test seeked. `prime` now hands back the frame it already decoded instead of it being decoded again. The range tests now assert the real contract rather than a stronger one that happened to hold: a ranged response is the same audio as that slice of the whole to within one LSB, not byte-for-byte. A seek reaches the decoder's floating-point state by a different route, so a sample on a rounding boundary can quantise to the adjacent integer — 1/32768, about -90 dBFS, and the same tolerance the decoder's own conformance suite uses. The comparison respects sample phase, because a range may begin mid-sample and pairing bytes from the wrong phase reports a 1-LSB difference as 256. The new test seeks into frames 2, 5 and 8, where the pre-roll is real. Verified against a running server: 206s with exact lengths and <= 1 LSB for a mid-file range, a header-straddling range and a range to the last byte; 416 past the end; both elements present in a Samsung browse; the AAC variant at 3244 bytes against 98348 of LPCM. --- config.example.toml | 2 + crates/vuio-core/src/config/loading.rs | 7 ++ crates/vuio-core/src/config/template.toml | 2 + crates/vuio-core/src/platform/config.rs | 5 ++ .../vuio-core/src/web/transcode_streaming.rs | 67 +++++++++++----- .../tests/transcode_integration_tests.rs | 80 +++++++++++++++++-- 6 files changed, 137 insertions(+), 26 deletions(-) diff --git a/config.example.toml b/config.example.toml index 837c5d00..19bf1a05 100644 --- a/config.example.toml +++ b/config.example.toml @@ -34,6 +34,8 @@ watch_for_changes = true supported_extensions = [ "mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v", "mpg", "mpeg", "3gp", "ogv", "mp3", "flac", "wav", "aac", "ogg", "wma", "m4a", "opus", "ape", + # Elementary Dolby and DTS streams, decoded for renderers that cannot play them. + "ac3", "eac3", "ec3", "dts", "jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg", "heif", "heic", "avif" ] # cleanup_deleted_files = true # Drop files the startup scan finds gone diff --git a/crates/vuio-core/src/config/loading.rs b/crates/vuio-core/src/config/loading.rs index 9114ec18..6104f10e 100644 --- a/crates/vuio-core/src/config/loading.rs +++ b/crates/vuio-core/src/config/loading.rs @@ -125,6 +125,13 @@ impl AppConfig { "aac".to_string(), "ogg".to_string(), "wma".to_string(), + // Elementary Dolby and DTS streams. Listed because VuIO can now + // decode them for renderers that cannot — before that there was + // nothing useful to do with one, which is why they were absent. + "ac3".to_string(), + "eac3".to_string(), + "ec3".to_string(), + "dts".to_string(), "jpg".to_string(), "jpeg".to_string(), "png".to_string(), diff --git a/crates/vuio-core/src/config/template.toml b/crates/vuio-core/src/config/template.toml index 72635b64..93c235b7 100644 --- a/crates/vuio-core/src/config/template.toml +++ b/crates/vuio-core/src/config/template.toml @@ -56,6 +56,8 @@ autoplay_enabled = true supported_extensions = [ "mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v", "3gp", "mp3", "flac", "wav", "aac", "ogg", "wma", + # Elementary Dolby and DTS streams, decoded for renderers that cannot play them. + "ac3", "eac3", "ec3", "dts", "jpg", "jpeg", "png", "gif", "bmp", "webp", "heif", "heic", "avif" ] diff --git a/crates/vuio-core/src/platform/config.rs b/crates/vuio-core/src/platform/config.rs index a9e68f73..1540d9d3 100644 --- a/crates/vuio-core/src/platform/config.rs +++ b/crates/vuio-core/src/platform/config.rs @@ -465,6 +465,11 @@ impl PlatformConfig { "aac".to_string(), "ogg".to_string(), "wma".to_string(), + // Elementary Dolby and DTS streams, decodable since `media::transcode`. + "ac3".to_string(), + "eac3".to_string(), + "ec3".to_string(), + "dts".to_string(), "m4a".to_string(), "opus".to_string(), "ape".to_string(), diff --git a/crates/vuio-core/src/web/transcode_streaming.rs b/crates/vuio-core/src/web/transcode_streaming.rs index 64096742..e1ba1513 100644 --- a/crates/vuio-core/src/web/transcode_streaming.rs +++ b/crates/vuio-core/src/web/transcode_streaming.rs @@ -91,13 +91,16 @@ fn aac_body(plan: Arc, permit: tokio::sync::OwnedSemaphorePermit) -> }; let mut source = std::io::BufReader::with_capacity(256 * 1024, file); - let mut decoder = match prime(&plan, &mut source, 0) { + let (mut decoder, primed) = match prime(&plan, &mut source, 0) { Ok(d) => d, Err(e) => { let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); return; } }; + // As above: the first frame is already decoded, and re-feeding it would + // push its samples through the overlap buffer a second time. + let mut primed = Some(primed); let mut encoder = match AacEncoder::new(plan.sample_rate(), plan.channels) { Ok(e) => e, Err(e) => { @@ -106,12 +109,20 @@ fn aac_body(plan: Arc, permit: tokio::sync::OwnedSemaphorePermit) -> } }; - for frame in &plan.index.frames { - let mut raw = vec![0u8; frame.len as usize]; - if read_frame(&mut source, frame.offset, &mut raw).is_err() { - break; - } - let pcm = decoder.decode_or_silence(&raw, frame.samples); + for (i, frame) in plan.index.frames.iter().enumerate() { + let pcm = match primed.take() { + Some(mut pcm) => { + pcm.resize(plan.frame_bytes(i), 0); + pcm + } + None => { + let mut raw = vec![0u8; frame.len as usize]; + if read_frame(&mut source, frame.offset, &mut raw).is_err() { + break; + } + decoder.decode_or_silence(&raw, frame.samples) + } + }; match encoder.push(&pcm) { Ok(adts) if adts.is_empty() => continue, Ok(adts) => { @@ -335,13 +346,18 @@ fn pcm_body( // frame early and discarding its output removes the transient that // would otherwise tick at the start of every seek. let preroll = seeked.frame.saturating_sub(1); - let mut decoder = match prime(&plan, &mut source, preroll) { + let (mut decoder, primed) = match prime(&plan, &mut source, preroll) { Ok(d) => d, Err(e) => { let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); return; } }; + // Priming already decoded frame `preroll`. Feeding it again would run + // its samples through the overlap buffer twice, and every frame after + // it would then differ from the same frame in a sequential decode — so + // a range would not be the slice of the whole that it claims to be. + let mut primed = Some(primed); let mut skip = seeked.pcm_skip; for i in preroll..plan.index.frames.len() { @@ -349,11 +365,19 @@ fn pcm_body( break; } let frame = plan.index.frames[i]; - let mut raw = vec![0u8; frame.len as usize]; - if read_frame(&mut source, frame.offset, &mut raw).is_err() { - break; - } - let pcm = decoder.decode_or_silence(&raw, frame.samples); + let pcm = match primed.take() { + Some(mut pcm) => { + pcm.resize(plan.frame_bytes(i), 0); + pcm + } + None => { + let mut raw = vec![0u8; frame.len as usize]; + if read_frame(&mut source, frame.offset, &mut raw).is_err() { + break; + } + decoder.decode_or_silence(&raw, frame.samples) + } + }; // Frames before the seek point are decoded for their state only. if i < seeked.frame { @@ -395,22 +419,25 @@ fn pcm_body( Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) } -/// Decode every frame up to `upto` so the decoder carries the right state. +/// Open a decoder positioned at frame `at`, returning it and that frame's PCM. +/// +/// The PCM comes back rather than being dropped because opening a decoder +/// necessarily decodes a frame, and decoding the same frame again to get its +/// samples would advance the overlap state a second time. fn prime( plan: &AudioPlan, source: &mut R, - upto: usize, -) -> anyhow::Result { - let first = plan.index.frames[upto]; + at: usize, +) -> anyhow::Result<(PcmDecoder, Vec)> { + let first = plan.index.frames[at]; let mut raw = vec![0u8; first.len as usize]; read_frame(source, first.offset, &mut raw)?; - let (decoder, _) = PcmDecoder::open( + PcmDecoder::open( plan.codec, plan.index.sample_rate, Some(plan.channels), &raw, - )?; - Ok(decoder) + ) } fn read_frame( diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs index 0adbd275..3afc0754 100644 --- a/crates/vuio-core/tests/transcode_integration_tests.rs +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -150,6 +150,49 @@ async fn request( (status, headers, body) } +/// Assert a ranged response is the same audio as that slice of the whole. +/// +/// "Same" to within one LSB per sample, not byte-for-byte. A seek primes the +/// decoder one frame early rather than replaying the file from the start, so +/// the IMDCT's floating-point state is reached by a different route and a +/// sample sitting exactly on a rounding boundary can quantise to the adjacent +/// integer. That is 1/32768 — around -90 dBFS, inaudible, and the tolerance the +/// decoder's own conformance tests use. Anything larger means the seek landed +/// somewhere else entirely. +/// +/// `range_start` is needed because a byte range may begin mid-sample, and +/// pairing bytes from the wrong phase turns a 1-LSB difference into a 256-LSB +/// one. The partial sample at each end is compared as bytes instead. +fn assert_same_audio(part: &[u8], whole_slice: &[u8], range_start: usize, what: &str) { + assert_eq!(part.len(), whole_slice.len(), "{what}: length"); + + // Byte offset into the PCM payload, and how far into a 4-byte stereo sample + // frame the range begins. + let phase = range_start.saturating_sub(44) % 4; + let lead = if phase == 0 { 0 } else { 4 - phase }; + let lead = lead.min(part.len()); + + let mut worst = 0i32; + let mut worst_at = 0usize; + for (i, (a, b)) in part[lead..] + .chunks_exact(2) + .zip(whole_slice[lead..].chunks_exact(2)) + .enumerate() + { + let x = i16::from_le_bytes([a[0], a[1]]) as i32; + let y = i16::from_le_bytes([b[0], b[1]]) as i32; + if (x - y).abs() > worst { + worst = (x - y).abs(); + worst_at = i; + } + } + assert!( + worst <= 1, + "{what}: samples differ by up to {worst} LSB (first worst at sample {worst_at}) \ + — the seek landed in the wrong place, not merely rounded differently" + ); +} + fn header_u64(headers: &axum::http::HeaderMap, name: header::HeaderName) -> u64 { headers[&name].to_str().unwrap().parse().unwrap() } @@ -235,11 +278,34 @@ async fn a_byte_range_returns_exactly_that_slice_of_the_full_decode() { format!("bytes {start}-{end}/{}", whole.len()) ); assert_eq!(part.len(), end - start + 1); - assert_eq!( - part, - whole[start..=end], - "a range must be byte-identical to that slice of the whole" - ); + assert_same_audio(&part, &whole[start..=end], start, "mid-frame range"); +} + +/// The case that only shows up deeper into the file. +/// +/// Seeking starts the decoder one frame early so the IMDCT overlap is warm. +/// Opening a decoder necessarily decodes that frame, and if it is then fed to +/// the decoder a *second* time its samples run through the overlap buffer twice +/// — every frame after it lands somewhere a sequential decode never goes, and +/// the range stops being the audio its Content-Range claims. +/// +/// It cancels out at frame 1, where the pre-roll is frame 0 and the whole-file +/// decode primes on frame 0 too, which is why the seeks here are deliberately +/// several frames in. A live server caught this; the frame-1 test did not. +#[tokio::test] +async fn a_deep_seek_matches_the_sequential_decode_frame_for_frame() { + let (_temp, state, id) = library().await; + let (_, _, whole) = request(&state, id, Method::GET, None).await; + + for frame in [2usize, 5, 8] { + let start = 44 + frame * 1536 * 2 * 2 + 777; + let end = start + 8_000; + assert!(end < whole.len(), "fixture is long enough for frame {frame}"); + let (status, _, part) = + request(&state, id, Method::GET, Some(&format!("bytes={start}-{end}"))).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_same_audio(&part, &whole[start..=end], start, &format!("range into frame {frame}")); + } } #[tokio::test] @@ -252,7 +318,9 @@ async fn a_range_spanning_the_header_boundary_is_still_exact() { let (status, _, part) = request(&state, id, Method::GET, Some("bytes=20-2043")).await; assert_eq!(status, StatusCode::PARTIAL_CONTENT); assert_eq!(part.len(), 2024); - assert_eq!(part, whole[20..=2043]); + // The WAV header is copied, not decoded, so those bytes must match exactly. + assert_eq!(&part[..24], &whole[20..44], "the tail of the WAV header"); + assert_same_audio(&part[24..], &whole[44..=2043], 44, "audio after the header"); } #[tokio::test] From e9b7392000a70a60dda214c1f47504b1fd90af9e Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 12:53:08 +0300 Subject: [PATCH 08/38] u --- .github/workflows/ci.yml | 4 +- crates/vuio-core/src/web/xml/browse.rs | 4 +- .../tests/transcode_integration_tests.rs | 51 ++++++++++++++----- scripts/check.sh | 4 +- scripts/run-tests.ps1 | 6 +-- scripts/run-tests.sh | 6 +-- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72634ba8..697080f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: with: components: rustfmt - name: Check formatting - run: cargo fmt --all -- --check + run: cargo fmt -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web -- --check # =================================================================== # The web interface ships compiled into the binary. @@ -130,7 +130,7 @@ jobs: target/ key: clippy-${{ matrix.os }}-${{ hashFiles('**/Cargo.lock') }} - name: Run Clippy - run: cargo clippy --all-targets --all-features -- -D clippy::all -D warnings + run: cargo clippy -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web --all-targets --all-features -- -D clippy::all -D warnings # =================================================================== # Guard the published API of vuio-core. diff --git a/crates/vuio-core/src/web/xml/browse.rs b/crates/vuio-core/src/web/xml/browse.rs index 953a2e3a..c979d845 100644 --- a/crates/vuio-core/src/web/xml/browse.rs +++ b/crates/vuio-core/src/web/xml/browse.rs @@ -251,7 +251,7 @@ pub async fn generate_browse_response( if let Some(advert) = transcoded.filter(|a| a.first) { let _ = advert.write_didl( &mut didl, - &server_ip, + server_ip, state.http_binding.port(), file_id, duration_secs, @@ -294,7 +294,7 @@ pub async fn generate_browse_response( if let Some(advert) = transcoded.filter(|a| !a.first) { let _ = advert.write_didl( &mut didl, - &server_ip, + server_ip, state.http_binding.port(), file_id, duration_secs, diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs index 3afc0754..a3ceada2 100644 --- a/crates/vuio-core/tests/transcode_integration_tests.rs +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -257,7 +257,10 @@ async fn head_promises_the_length_that_get_delivers() { header_u64(&head_headers, header::CONTENT_LENGTH), header_u64(&get_headers, header::CONTENT_LENGTH), ); - assert_eq!(get_body.len() as u64, header_u64(&head_headers, header::CONTENT_LENGTH)); + assert_eq!( + get_body.len() as u64, + header_u64(&head_headers, header::CONTENT_LENGTH) + ); } #[tokio::test] @@ -269,8 +272,13 @@ async fn a_byte_range_returns_exactly_that_slice_of_the_full_decode() { // so the seek has to land mid-frame and discard the right number of samples. let start = 44 + 1536 * 2 * 2 + 5000; let end = start + 20_000; - let (status, headers, part) = - request(&state, id, Method::GET, Some(&format!("bytes={start}-{end}"))).await; + let (status, headers, part) = request( + &state, + id, + Method::GET, + Some(&format!("bytes={start}-{end}")), + ) + .await; assert_eq!(status, StatusCode::PARTIAL_CONTENT); assert_eq!( @@ -300,11 +308,24 @@ async fn a_deep_seek_matches_the_sequential_decode_frame_for_frame() { for frame in [2usize, 5, 8] { let start = 44 + frame * 1536 * 2 * 2 + 777; let end = start + 8_000; - assert!(end < whole.len(), "fixture is long enough for frame {frame}"); - let (status, _, part) = - request(&state, id, Method::GET, Some(&format!("bytes={start}-{end}"))).await; + assert!( + end < whole.len(), + "fixture is long enough for frame {frame}" + ); + let (status, _, part) = request( + &state, + id, + Method::GET, + Some(&format!("bytes={start}-{end}")), + ) + .await; assert_eq!(status, StatusCode::PARTIAL_CONTENT); - assert_same_audio(&part, &whole[start..=end], start, &format!("range into frame {frame}")); + assert_same_audio( + &part, + &whole[start..=end], + start, + &format!("range into frame {frame}"), + ); } } @@ -328,8 +349,7 @@ async fn a_range_past_the_end_is_refused_rather_than_truncated() { let (_temp, state, id) = library().await; let (_, _, whole) = request(&state, id, Method::GET, None).await; let past = whole.len() + 10; - let (status, _, _) = - request(&state, id, Method::GET, Some(&format!("bytes={past}-"))).await; + let (status, _, _) = request(&state, id, Method::GET, Some(&format!("bytes={past}-"))).await; assert_eq!(status, StatusCode::RANGE_NOT_SATISFIABLE); } @@ -455,7 +475,8 @@ async fn browse_audio(state: &AppState) -> String { HeaderValue::from_static("DLNADOC/1.50 SEC_HHP_[TV]UE40D7000/1.0"), ); let response = - content_directory_control(State(state.clone()), headers, browse_request("audio/!all")).await; + content_directory_control(State(state.clone()), headers, browse_request("audio/!all")) + .await; assert_eq!(response.status(), StatusCode::OK); // DIDL is double-escaped inside the SOAP body; unescape enough to read it. String::from_utf8( @@ -495,7 +516,10 @@ async fn an_ac3_item_is_offered_both_the_original_and_a_decoded_resource() { ); // The original must survive untouched — a TV that can play AC-3 should see // exactly what it saw before. - assert!(didl.contains(&format!(">http://127.0.0.1:8080/media/{id}")) || didl.contains(&format!("/media/{id}"))); + assert!( + didl.contains(&format!(">http://127.0.0.1:8080/media/{id}")) + || didl.contains(&format!("/media/{id}")) + ); } #[tokio::test] @@ -575,7 +599,10 @@ async fn the_aac_resource_streams_adts_without_claiming_to_be_seekable() { .to_str() .unwrap() .to_owned(); - assert!(features.contains("DLNA.ORG_OP=00"), "no seeking: {features}"); + assert!( + features.contains("DLNA.ORG_OP=00"), + "no seeking: {features}" + ); assert!(features.contains("DLNA.ORG_CI=1"), "converted: {features}"); let body = axum::body::to_bytes(response.into_body(), 16 * 1024 * 1024) diff --git a/scripts/check.sh b/scripts/check.sh index 4a3290bf..f74cb514 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -1,3 +1,3 @@ #!/bin/bash -cargo fmt --all -- --check -cargo clippy --all-targets --all-features -- -D warnings \ No newline at end of file +cargo fmt -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web -- --check +cargo clippy -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/scripts/run-tests.ps1 b/scripts/run-tests.ps1 index f775287b..dc1a683b 100644 --- a/scripts/run-tests.ps1 +++ b/scripts/run-tests.ps1 @@ -236,15 +236,15 @@ function Invoke-QualityChecks { Write-Status "Running code quality checks..." # Format check - $result = & cargo fmt --all -- --check + $result = & cargo fmt -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web -- --check if ($LASTEXITCODE -ne 0) { - Write-Error "Code formatting issues found. Run 'cargo fmt' to fix." + Write-Error "Code formatting issues found. Run 'cargo fmt -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web' to fix." return $false } Write-Success "Code formatting is correct" # Clippy lints - $result = & cargo clippy --all-targets --all-features -- -D warnings + $result = & cargo clippy -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web --all-targets --all-features -- -D warnings if ($LASTEXITCODE -ne 0) { Write-Error "Clippy lints failed" return $false diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 18eae487..66b265d4 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -211,15 +211,15 @@ run_quality_checks() { print_status "Running code quality checks..." # Format check - if cargo fmt --all -- --check; then + if cargo fmt -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web -- --check; then print_success "Code formatting is correct" else - print_error "Code formatting issues found. Run 'cargo fmt' to fix." + print_error "Code formatting issues found. Run 'cargo fmt -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web' to fix." return 1 fi # Clippy lints - if cargo clippy --all-targets --all-features -- -D warnings; then + if cargo clippy -p vuio-core -p vuio-cli -p vuio-cast -p vuio-web --all-targets --all-features -- -D warnings; then print_success "Clippy lints passed" else print_error "Clippy lints failed" From b34ebfb0b44644aa7fbba3c2b19942091552c389 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 13:04:49 +0300 Subject: [PATCH 09/38] u --- .claude-plugin/marketplace.json | 2 +- claude/mcpb/manifest.json | 2 +- claude/plugin/.claude-plugin/plugin.json | 2 +- crates/vendor/oxideav-aac/src/gain_control.rs | 11 +- .../vendor/oxideav-aac/src/sbr_freq_bands.rs | 29 +- crates/vendor/oxideav-dts/src/audio_array.rs | 4 +- crates/vuio-bench/Cargo.toml | 4 +- crates/vuio-cli/Cargo.toml | 4 +- crates/vuio-core/Cargo.toml | 4 +- crates/vuio-core/src/casting/airplay/raop.rs | 2 +- crates/vuio-core/src/media/transcode/pcm.rs | 2 +- crates/vuio-core/src/platform/error.rs | 2 +- .../tests/transcode_integration_tests.rs | 8 +- crates/vuio-web/Cargo.toml | 2 +- docs/api.md | 2 +- docs/install.md | 2 +- docs/kubernetes.md | 2 +- packaging/docker/builddocker.sh | 2 +- packaging/linux/generate-repo.sh | 2 +- phas4plan.txt | 333 ++++++++++++++++++ 20 files changed, 375 insertions(+), 46 deletions(-) create mode 100644 phas4plan.txt diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 38f4b228..dd860cbd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "vuio", "displayName": "VuIO Media Server", "description": "Browse, search and cast your VuIO media library — and control the TVs and speakers on your network — from Claude.", - "version": "0.0.44", + "version": "0.0.45", "author": { "name": "vyrti", "url": "https://github.com/vuiodev" diff --git a/claude/mcpb/manifest.json b/claude/mcpb/manifest.json index b1d423f2..4e865f8d 100644 --- a/claude/mcpb/manifest.json +++ b/claude/mcpb/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "vuio", "display_name": "VuIO Media Server", - "version": "0.0.44", + "version": "0.0.45", "description": "Browse, search and cast your VuIO media library from Claude.", "long_description": "Connects Claude to a VuIO media server on your network. Search the library, browse folders, build playlists, and cast to DLNA, Chromecast and AirPlay devices.\n\nThis bundle runs `vuio mcp`, which bridges Claude's stdio connection to a VuIO server that is already running. It does not start a server or open the library database itself — point it at the machine that does.", "author": { diff --git a/claude/plugin/.claude-plugin/plugin.json b/claude/plugin/.claude-plugin/plugin.json index aa03c42f..acbfc8d8 100644 --- a/claude/plugin/.claude-plugin/plugin.json +++ b/claude/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "vuio", "displayName": "VuIO Media Server", - "version": "0.0.44", + "version": "0.0.45", "description": "Browse, search and cast your VuIO media library — and control the TVs and speakers on your network — from Claude.", "author": { "name": "vyrti", diff --git a/crates/vendor/oxideav-aac/src/gain_control.rs b/crates/vendor/oxideav-aac/src/gain_control.rs index e9813adb..000372b6 100644 --- a/crates/vendor/oxideav-aac/src/gain_control.rs +++ b/crates/vendor/oxideav-aac/src/gain_control.rs @@ -274,8 +274,7 @@ fn gmf_long( ) -> (Vec, Vec) { // GMF spans 0..512 for the long sequences. let mut gmf = vec![0.0f64; 512]; - let pfmd_next: Vec; - match seq { + let pfmd_next = match seq { WindowSequence::OnlyLong => { let a0 = alev0(band, 0); for (j, slot) in gmf.iter_mut().enumerate() { @@ -286,7 +285,7 @@ fn gmf_long( }; } // PFMD_B(j) = FMD_0,B(j), 0 ≤ j ≤ 255. - pfmd_next = fmd[0][..256].to_vec(); + fmd[0][..256].to_vec() } WindowSequence::LongStart => { let a0 = alev0(band, 0); @@ -303,7 +302,7 @@ fn gmf_long( }; } // PFMD_B(j) = FMD_1,B(j), 0 ≤ j ≤ 31. - pfmd_next = fmd[1][..32].to_vec(); + fmd[1][..32].to_vec() } WindowSequence::LongStop => { let a0 = alev0(band, 0); @@ -320,10 +319,10 @@ fn gmf_long( }; } // PFMD_B(j) = FMD_1,B(j), 0 ≤ j ≤ 255. - pfmd_next = fmd[1][..256].to_vec(); + fmd[1][..256].to_vec() } WindowSequence::EightShort => unreachable!("gmf_long called for short sequence"), - } + }; (gmf, pfmd_next) } diff --git a/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs b/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs index 168e2746..507583ff 100644 --- a/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs +++ b/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs @@ -205,17 +205,18 @@ pub fn master_table( /// Figure 4.39 — `fMaster` for `bs_freq_scale == 0`. fn master_linear(k0_val: i32, k2_val: i32, bs_alter_scale: bool) -> Result> { - let dk; - let num_bands; - if !bs_alter_scale { - dk = 1; + let (dk, num_bands) = if !bs_alter_scale { + let dk = 1; // numBands = 2 * INT( (k2 - k0) / (dk * 2) ) - num_bands = 2 * int_trunc((k2_val - k0_val) as f64 / (dk as f64 * 2.0)); + ( + dk, + 2 * int_trunc((k2_val - k0_val) as f64 / (dk as f64 * 2.0)), + ) } else { - dk = 2; + let dk = 2; // numBands = 2 * NINT( (k2 - k0) / (dk * 2) ) - num_bands = 2 * nint((k2_val - k0_val) as f64 / (dk as f64 * 2.0)); - } + (dk, 2 * nint((k2_val - k0_val) as f64 / (dk as f64 * 2.0))) + }; if num_bands <= 0 { return Err(Error::SbrFreqBandInvalid); } @@ -266,15 +267,11 @@ fn master_warped( // temp2 = {1.0, 1.3}; warp = temp2[bs_alter_scale]. let warp = if bs_alter_scale { 1.3 } else { 1.0 }; - let two_regions; - let k1; - if (k2_val as f64) / (k0_val as f64) > 2.2449 { - two_regions = true; - k1 = 2 * k0_val; + let (two_regions, k1) = if (k2_val as f64) / (k0_val as f64) > 2.2449 { + (true, 2 * k0_val) } else { - two_regions = false; - k1 = k2_val; - } + (false, k2_val) + }; // Lower region. let v_k0 = warped_region(k0_val, k1, bands, 1.0)?; diff --git a/crates/vendor/oxideav-dts/src/audio_array.rs b/crates/vendor/oxideav-dts/src/audio_array.rs index d8dd574c..4f3572ad 100644 --- a/crates/vendor/oxideav-dts/src/audio_array.rs +++ b/crates/vendor/oxideav-dts/src/audio_array.rs @@ -422,9 +422,7 @@ impl AdpcmHistory { /// Zero every subband's history (the §5.3.1 `HFLAG = 0` frame /// gate: "Otherwise, the history will be ignored"). pub fn clear(&mut self) { - for ch in &mut self.per_channel { - *ch = [[0.0; NUM_ADPCM_COEFF]; NUM_SUBBAND]; - } + self.per_channel.fill([[0.0; NUM_ADPCM_COEFF]; NUM_SUBBAND]); } /// The four-sample history of one `(ch, n)` subband, oldest diff --git a/crates/vuio-bench/Cargo.toml b/crates/vuio-bench/Cargo.toml index b65eff99..52d45e2b 100644 --- a/crates/vuio-bench/Cargo.toml +++ b/crates/vuio-bench/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-bench" -version = "0.0.44" +version = "0.0.45" edition = "2021" authors = ["vyrti"] description = "Generates large VuIO libraries for performance work. Not published." @@ -25,4 +25,4 @@ tokio = { version = "1.53", features = ["rt-multi-thread", "macros"] } # because it opens every internal module and carries no stability promise. This # crate is `publish = false` and exists only to drive the database from the # inside, which is the same category as core's own dev-dependency on itself. -vuio-core = { path = "../vuio-core", version = "0.0.44", features = ["unstable-internals"] } +vuio-core = { path = "../vuio-core", version = "0.0.45", features = ["unstable-internals"] } diff --git a/crates/vuio-cli/Cargo.toml b/crates/vuio-cli/Cargo.toml index 7e96ebc7..0df1434f 100644 --- a/crates/vuio-cli/Cargo.toml +++ b/crates/vuio-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-cli" -version = "0.0.44" +version = "0.0.45" edition = "2021" authors = ["vyrti"] description = "VuIO media server command-line application" @@ -41,5 +41,5 @@ serde_json = "1.0" tokio = { version = "1.53", features = ["rt-multi-thread", "macros", "signal", "io-std", "io-util"] } tracing = "0.1" uuid = { version = "1.24", features = ["v4"] } -vuio-core = { path = "../vuio-core", version = "0.0.44" } +vuio-core = { path = "../vuio-core", version = "0.0.45" } diff --git a/crates/vuio-core/Cargo.toml b/crates/vuio-core/Cargo.toml index 7ef784ac..bb2c2a55 100644 --- a/crates/vuio-core/Cargo.toml +++ b/crates/vuio-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-core" -version = "0.0.44" +version = "0.0.45" edition = "2021" rust-version = "1.95" authors = ["vyrti"] @@ -160,7 +160,7 @@ sysinfo = { version = "0.39", default-features = false, features = ["system", "d socket2 = { version = "0.6", features = ["all"] } mdns-sd = { version = "0.21", default-features = false, features = ["async"] } vuio-cast = { path = "../vuio-cast", version = "0.0.4", default-features = false, optional = true } -vuio-web = { path = "../vuio-web", version = "0.0.44", optional = true } +vuio-web = { path = "../vuio-web", version = "0.0.45", optional = true } jwalk = "0.9" tokio-stream = "0.1" hap-crypto = { version = "1.4", optional = true } diff --git a/crates/vuio-core/src/casting/airplay/raop.rs b/crates/vuio-core/src/casting/airplay/raop.rs index f5388e58..157d6847 100644 --- a/crates/vuio-core/src/casting/airplay/raop.rs +++ b/crates/vuio-core/src/casting/airplay/raop.rs @@ -63,7 +63,7 @@ fn pcm_to_uncompressed_alac(frames: &[u8]) -> Vec { writer.write(0, 1); // hasSize writer.write(0, 2); // unused writer.write(1, 1); // isNotCompressed - for frame in frames.chunks_exact(BYTES_PER_FRAME) { + for frame in frames.as_chunks::().0 { let left = u16::from_le_bytes([frame[0], frame[1]]); let right = u16::from_le_bytes([frame[2], frame[3]]); writer.write(u32::from(left), 16); diff --git a/crates/vuio-core/src/media/transcode/pcm.rs b/crates/vuio-core/src/media/transcode/pcm.rs index f44c1608..943aa9f0 100644 --- a/crates/vuio-core/src/media/transcode/pcm.rs +++ b/crates/vuio-core/src/media/transcode/pcm.rs @@ -152,7 +152,7 @@ mod tests { fn rms(pcm: &[u8]) -> f64 { let mut sum = 0.0f64; let mut n = 0u64; - for c in pcm.chunks_exact(2) { + for c in pcm.as_chunks::<2>().0 { let v = i16::from_le_bytes([c[0], c[1]]) as f64; sum += v * v; n += 1; diff --git a/crates/vuio-core/src/platform/error.rs b/crates/vuio-core/src/platform/error.rs index cf271383..4cd76011 100644 --- a/crates/vuio-core/src/platform/error.rs +++ b/crates/vuio-core/src/platform/error.rs @@ -435,7 +435,7 @@ impl LinuxError { pub fn recovery_actions(&self) -> Vec { match self { LinuxError::InsufficientCapabilities { port } => vec![ - format!("sudo setcap 'cap_net_bind_service=+ep' $(which vuio)"), + "sudo setcap 'cap_net_bind_service=+ep' $(which vuio)".to_string(), format!("Use alternative port instead of {}", port), "Run with sudo (not recommended for production)".to_string(), ], diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs index a3ceada2..4bc9c279 100644 --- a/crates/vuio-core/tests/transcode_integration_tests.rs +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -175,8 +175,10 @@ fn assert_same_audio(part: &[u8], whole_slice: &[u8], range_start: usize, what: let mut worst = 0i32; let mut worst_at = 0usize; for (i, (a, b)) in part[lead..] - .chunks_exact(2) - .zip(whole_slice[lead..].chunks_exact(2)) + .as_chunks::<2>() + .0 + .iter() + .zip(whole_slice[lead..].as_chunks::<2>().0) .enumerate() { let x = i16::from_le_bytes([a[0], a[1]]) as i32; @@ -237,7 +239,7 @@ async fn an_ac3_file_is_served_as_playable_wav() { // The fixture is a 440 Hz sine, so silence here would mean we produced a // correctly-shaped empty response instead of decoding anything. let mut sum = 0f64; - for c in body[44..].chunks_exact(2) { + for c in body[44..].as_chunks::<2>().0 { let v = i16::from_le_bytes([c[0], c[1]]) as f64; sum += v * v; } diff --git a/crates/vuio-web/Cargo.toml b/crates/vuio-web/Cargo.toml index 65710e53..266acf29 100644 --- a/crates/vuio-web/Cargo.toml +++ b/crates/vuio-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vuio-web" -version = "0.0.44" +version = "0.0.45" edition = "2021" rust-version = "1.95" authors = ["vyrti"] diff --git a/docs/api.md b/docs/api.md index d5068a8d..7260f314 100644 --- a/docs/api.md +++ b/docs/api.md @@ -98,7 +98,7 @@ configuration file actually writes. Backs the dashboard's Admin tab. "read_only_reason": null, "auth_enabled": false, "is_docker": false, - "version": "0.0.44", + "version": "0.0.45", // Where the server is actually accepting, which is what every advertised URL uses. "bound_addr": "0.0.0.0:8080", "desired_addr": null, diff --git a/docs/install.md b/docs/install.md index 45133744..876c3b69 100644 --- a/docs/install.md +++ b/docs/install.md @@ -242,7 +242,7 @@ Deploy VuIO to a Kubernetes cluster using the official Helm chart from GHCR. For ```bash # Install directly from GitHub Container Registry -helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.44 +helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.45 ``` Or install from local source: diff --git a/docs/kubernetes.md b/docs/kubernetes.md index 17915785..3386db08 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -11,7 +11,7 @@ VuIO provides an official Helm 3 chart to deploy the media server directly onto Install the chart directly from GitHub Container Registry without cloning the repository: ```bash -helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.44 +helm install vuio oci://ghcr.io/vuiodev/charts/vuio --version 0.0.45 ``` ### Local Installation diff --git a/packaging/docker/builddocker.sh b/packaging/docker/builddocker.sh index ffe9bb05..ce6e048d 100755 --- a/packaging/docker/builddocker.sh +++ b/packaging/docker/builddocker.sh @@ -1,6 +1,6 @@ export GITHUB_ORG="vuiodev" export IMAGE_NAME="vuio" -export VERSION_TAG="v0.0.44" +export VERSION_TAG="v0.0.45" docker login ghcr.io diff --git a/packaging/linux/generate-repo.sh b/packaging/linux/generate-repo.sh index 3e61f4ec..8a98ec0e 100755 --- a/packaging/linux/generate-repo.sh +++ b/packaging/linux/generate-repo.sh @@ -250,7 +250,7 @@ for arch in x86_64 aarch64; do zstd -d "$pkg" -o pkgtemp/pkg.tar --quiet 2>/dev/null && \ tar -xf pkgtemp/pkg.tar -C pkgtemp .PKGINFO 2>/dev/null || true pname="vuio" - pver="0.0.44-1" + pver="0.0.45-1" pdesc="Cross-platform DLNA media server" purl="https://github.com/vuiodev/vuio" psize="15000000" diff --git a/phas4plan.txt b/phas4plan.txt new file mode 100644 index 00000000..debd18a5 --- /dev/null +++ b/phas4plan.txt @@ -0,0 +1,333 @@ +================================================================================ +PHASE 4 — AC-3 / E-AC-3 / DTS INSIDE FILMS +Video files with undecodable audio tracks, for TVs that play them silently +================================================================================ + +CONTEXT + +Phases 1-3 (branch feat/transcode-ac3-dts, 6 commits) shipped the decode core +and made it reachable for *elementary* streams: a standalone .ac3/.eac3/.dts +file is indexed, offered as a second , and served as LPCM or AAC. + +That is the foundation, not the payoff. The common shape of the problem is a +film — Movie.mkv with an AC-3 or DTS track — where the TV shows the picture and +produces no sound. Phase 4 is what actually fixes that. + +The seam is already marked in the code. media/remux/mkv_demuxer.rs:14-27: + + /// This is a passthrough remuxer, not a transcoder: everything here is real + /// content Symphonia can identify (E-AC-3/AC-3/DTS/TrueHD audio, VP9/AV1 + /// video, ...), but with no decoder/encoder in the pipeline those tracks are + /// marked `Unsupported` and left out of what gets offered to the browser + /// rather than shipped as a broken stream. + pub enum TrackCodec { Avc, Hevc, Aac, #[default] Unsupported } + +There is now a decoder in the pipeline. That comment is the work order. + + +-------------------------------------------------------------------------------- +WHAT ALREADY EXISTS AND IS REUSED +-------------------------------------------------------------------------------- + +From phases 1-3 (all shared, no changes needed): + + media/transcode/mod.rs TranscodeCodec, make_decoder(), is_decodable() + media/transcode/pcm.rs PcmDecoder — packets in, interleaved S16 out + media/transcode/aac.rs AacEncoder — S16 in, ADTS AAC-LC out + media/transcode/session.rs TranscodeState: plan cache + concurrency ceiling + web/mod.rs item_needs_transcode(codec, mime, filename) + — already consults MediaFileView::codec() FIRST, + which is exactly what a container track needs + web/mod.rs transcode_advert() -> Option + web/xml/rendering.rs TranscodeAdvert::write / write_didl + config [transcode] enabled/audio_format/prefer/max_concurrent + +Already in the repo, from the existing browser remux path: + + media/remux/mkv_demuxer.rs MkvDemuxer::inspect(), extract_track_packets() + media/remux/fmp4_writer.rs Fmp4Writer::build_segment() — 849 lines, + writes avcC / hvcC / mp4a+esds (:225-365) + media/remux/hls.rs master + media playlist generation + web/remux_streaming.rs /media/{id}/hls/* handlers (199 lines) + web/radio.rs:141-171 the async_stream chunked-body pattern + +NOT reused, and why: + - oxideav-mkv / oxideav-mp4 muxers: oxideav's Muxer trait requires + Write + Seek, so it cannot write into an HTTP body. Fmp4Writer already + produces fragmented MP4, which needs neither seek nor a known length. + - oxideav-mpegts: demuxer only. There is no TS muxer in the family. + => Phase 4 vendors NOTHING NEW. All four crates are already in crates/vendor. + + +-------------------------------------------------------------------------------- +STEP 1 — THE DATABASE PREREQUISITE (do this first; everything blocks on it) +-------------------------------------------------------------------------------- + +Deciding "does this film need a decoded alternative?" must be a DB read. Probing +files while rendering a browse page is not an option — a folder of 400 films +would open 400 files per Browse request. + +Two existing gaps make that impossible today: + +1. platform/filesystem/metadata.rs:184-204 derives stream.codec from + + symphonia::default::get_codecs().get_audio_decoder(audio.codec) + .map(|registered| registered.codec.info.short_name.to_owned()) + + which returns None for any codec symphonia cannot DECODE. So an AC-3 track + stores a NULL codec even though symphonia identified it perfectly well. + => Add a CodecId -> &'static str fallback map covering at least + CODEC_ID_AC3 -> "ac3", CODEC_ID_EAC3 -> "eac3", CODEC_ID_DCA -> "dca", + CODEC_ID_TRUEHD -> "truehd". Note truehd is recorded but NOT decodable — + TranscodeCodec::from_stored_codec already returns None for it, and must + keep doing so. + +2. media/scanner.rs:737-739 only probes audio files: + + if media_file.mime_type.starts_with("audio/") { + let _ = crate::platform::filesystem::extract_audio_metadata(...).await; + } + + Video files get no stream info at all. + => Extend to video/*, capturing the audio track's codec id only. This is a + header probe, not a decode. Measure the added scan time on a real library + and record it in the commit message — this is the one place phase 4 makes + every user pay something. + +3. Bump TAGS_VERSION (metadata.rs:40) so existing libraries re-probe on the next + scan rather than sitting on NULL codecs forever. + +Test: a scanned MKV with an AC-3 track has codec == "ac3" in media_files, and +item_needs_transcode() returns true for it without opening the file. + +Once this lands, the advertising from phase 3 starts firing for films with +no further change — which is why the URL it points at must exist first. Sequence +Step 1 and Step 3 in the same PR, or gate on TranscodeCodec::is_decodable(). + + +-------------------------------------------------------------------------------- +STEP 2 — TEACH THE DEMUXER ABOUT THE THREE CODECS +-------------------------------------------------------------------------------- + +media/remux/mkv_demuxer.rs: + + - Extend TrackCodec: Avc | Hevc | Aac | Ac3 | Eac3 | Dts | Unsupported. + Add the variants UNGATED. The enum is Serialize/Deserialize and the variants + are additive; a build with no decoder should still NAME the track ("AC-3, + cannot be decoded by this build") rather than reporting Unsupported, which + is a worse diagnostic and a worse log line. + + - Audio classification (~:166-180) currently: + + let (codec_kind, codec_name) = if a.codec == CODEC_ID_AAC { + (TrackCodec::Aac, "AAC") + } else { + (TrackCodec::Unsupported, "Audio") + }; + + becomes a match over CODEC_ID_AAC / AC3 / EAC3 / DCA. + + - extra_data is currently captured only for AAC. AC-3/DTS need none for + decoding (the decoders read the bitstream headers), so leave that as is — + but the RE-ENCODED AAC track needs its own AudioSpecificConfig, which comes + from the encoder's output_params, not from the source. + + - browser_audio_tracks (~:68-83) stops dropping these tracks when the matching + decoder feature is compiled in. Gate on TranscodeCodec::is_decodable() so a + build without transcode-dts still drops DTS rather than offering a track it + cannot produce. + +media/remux/hls.rs:8-14 and its test at :190-204 + (test_master_playlist_excludes_unsupported_audio_codecs) asserts the OLD + behaviour. It must be rewritten, not deleted: the new contract is "excluded + when this build cannot decode it", which is still worth a test. + + +-------------------------------------------------------------------------------- +STEP 3 — A PACKET SOURCE THAT ISN'T AN ELEMENTARY STREAM +-------------------------------------------------------------------------------- + +Phase 1-3's FrameIndex/AudioPlan walk sync words in a raw stream. A container +track's packets come from symphonia instead. Generalise: + + media/transcode/source.rs (new) + + /// Where compressed audio frames come from. + enum PacketSource { + /// A raw .ac3/.eac3/.dts file, framed by walking sync words. + Elementary(FrameIndex), + /// One track inside a container symphonia can demux. + Container { track_id: u32, /* symphonia reader */ }, + } + +Keep AudioPlan as the thing that resolves total samples / sample rate / channels +before any bytes are sent — that contract is what makes the LPCM resource +seekable and must not be weakened. For a container: + + - Total samples: prefer symphonia's track n_frames when the container declares + it (MKV usually does via Duration + TimestampScale; MP4 via stts). If absent, + fall back to a demux-only counting pass — same cost class as the elementary + header walk, and it is cached in TranscodeState either way. + - IMPORTANT: if total samples cannot be determined, the resource must degrade + to chunked with DLNA.ORG_OP=00, never guess a Content-Length. A wrong + Content-Length is a truncated download; an absent one is only a lost seek. + +The audio-only resources from phase 2 (/transcode/audio.wav|.aac) then work for +films too, and codec_for() in web/transcode_streaming.rs gains a DB-codec branch +alongside its MIME/extension branches. That alone is worth shipping: it fixes +"play the film's soundtrack on a hi-fi", and it exercises the container path +before the video work lands on top of it. + + +-------------------------------------------------------------------------------- +STEP 4 — THE BROWSER PATH (HLS) +-------------------------------------------------------------------------------- + +The smaller half, and the one with a working harness around it already. + +web/remux_streaming.rs:151-180, build_segment_response(): + - Add a decode+re-encode variant for audio segments whose track is + Ac3/Eac3/Dts: extract_track_packets -> PcmDecoder -> AacEncoder -> + Fmp4Writer::build_segment with the encoder's own AudioSpecificConfig. + - Requires transcode-aac (already a default feature). + + - MOVE SEGMENT BUILDING TO spawn_blocking. It currently runs synchronously on + the request task, which is survivable for a byte copy and is not for a + decode+encode of a 4-second segment (SEGMENT_DURATION_SECS, :26). This is a + prerequisite, not a nicety: without it one seeking client stalls the whole + runtime's worker. + + - Take a TranscodeState permit per segment build, so HLS and the DLNA path + share one concurrency ceiling rather than each having their own. + + - Segment caching becomes worth it here in a way it was not for a copy: the + same segment is rebuilt on every seek and every re-buffer. Key on + (file id, track, segment index) and reuse the existing Cache-Control. + +web/ui/js/video-player.js:54 already routes .mkv to /hls/master.m3u8, so the +browser side needs no change once the audio track stops being dropped. + + +-------------------------------------------------------------------------------- +STEP 5 — THE DLNA PATH (progressive fMP4) — THE ACTUAL DELIVERABLE +-------------------------------------------------------------------------------- + +New route, beside the phase-2 ones: + + GET|HEAD /media/{id}/transcode/video.mp4 + +Shape: + - Fragmented MP4: one init segment (ftyp + moov with the video track's avcC or + hvcC copied verbatim, plus an mp4a track from the AAC encoder's params), + then a continuous moof/mdat stream. + - Video is PASSTHROUGH. Nothing is re-encoded — only the audio track is + decoded and re-encoded. This is what keeps the CPU cost bounded and the + picture bit-identical. + - Chunked body via async_stream (web/radio.rs:141-171 is the proven pattern), + fed from a spawn_blocking producer over a bounded channel so a slow TV + applies backpressure instead of buffering a film into RAM. + - Headers: no Content-Length, no Accept-Ranges, + contentFeatures.dlna.org: DLNA.ORG_OP=00;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=... + OP=00 because there is no seeking. Do not claim otherwise. + + advertising: extend TranscodeAdvert so a video item advertises +video/mp4 -> transcode/video.mp4 while an audio item keeps audio.wav/audio.aac. +The selection belongs in web/mod.rs::transcode_advert() next to the existing +audio_format switch, so both DIDL writers stay feature-blind. + +Track selection for multi-audio films: transcode the track the container marks +DEFAULT; failing that the first audio track. Do not try to be clever about +language — a wrong guess is worse than a predictable one. If it turns out to +matter, the follow-up is one per audio track, which the DIDL already +supports (phase 3 proved multiple render fine). + + +-------------------------------------------------------------------------------- +RISKS, HONESTLY +-------------------------------------------------------------------------------- + +1. DO TVs ACTUALLY PLAY PROGRESSIVE fMP4 OVER DLNA? This is the largest product + risk in the whole feature and it is not answerable from the code. Support is + uneven and brand-specific. VERIFY THIS EARLY — before building Step 5 — with + a hand-built fMP4 served from a stub route to a real television. If it fails + broadly, the fallback is a progressive MPEG-TS mux, which nothing in the tree + provides and which would mean writing one (~600-900 lines) or vendoring a + fifth crate. Knowing this costs an afternoon; discovering it after Step 5 + costs the step. + +2. No seeking on the video resource. A film you cannot scrub is a real + regression in user experience versus direct play. Partial mitigation: keep + the original first (prefer = "original" default), so a TV that CAN + play AC-3 keeps its seekable direct-play resource and only the ones that + cannot fall back to the stream. Full mitigation is time-seek + (DLNA.ORG_OP=01 + TimeSeekRange.dlna.org), which is a follow-up: the + demuxer can seek by timestamp and the decoder can be primed the same way + phase 2 primes for byte ranges. + +3. CPU. Video passthrough keeps this bounded, but a 5.1 DTS track decoded and + re-encoded in real time on a Raspberry Pi is not free. max_concurrent + already exists and already refuses rather than queues. Measure on the + slowest target that matters before defaulting anything on. + +4. No real test fixtures exist. test-media/movie1.mkv is a 26-byte stub. Phase + 1-3 solved this for audio by using the vendored crates' own fixtures and by + synthesizing with oxideav-ac3's ENCODER. For phase 4 the same trick extends: + build a small MKV at test time from a synthesized H.264 or MPEG-1 video + track plus an AC-3 track encoded by oxideav-ac3. Prefer that over committing + a binary film clip. + +5. The unit tests will lie to you. Phase 1-3 learned this the hard way: every + integration test injected MediaFile rows directly into the database and so + passed against a scanner that indexed nothing, and a range test that seeked + to frame 1 masked a real decoder-state bug. FOR PHASE 4, DRIVE A REAL SERVER + WITH A REAL FILE BEFORE BELIEVING ANY OF IT. + + +-------------------------------------------------------------------------------- +SUGGESTED PR SPLIT +-------------------------------------------------------------------------------- + +PR A Step 1 + Step 2 + Step 3. + DB codec identification, demuxer awareness, container packet source. + Ships a working feature on its own: a film's soundtrack becomes playable + as audio.wav/audio.aac. Low risk, fully testable, no new dependencies. + +PR B Step 4. HLS audio for the browser player, plus the spawn_blocking and + caching fixes to build_segment_response that it forces. + +PR C Step 5. The progressive video resource. GATE THIS ON RISK 1 BEING + ANSWERED FIRST. + + +-------------------------------------------------------------------------------- +VERIFICATION (all of it self-run, no manual steps) +-------------------------------------------------------------------------------- + + cargo build + cargo check -p vuio-core --no-default-features + cargo check -p vuio-core --no-default-features --features transcode + cargo check -p vuio-core --no-default-features --features transcode-ac3 + cargo check -p vuio-core --no-default-features --features transcode-dts + cargo check -p vuio-core --no-default-features --features transcode-aac + cargo test --workspace + + Integration, in crates/vuio-core/tests/ following the oneshot pattern in + web_ui_integration_tests.rs:1-130: + - a scanned MKV with an AC-3 track records codec "ac3" and advertises two + elements + - the same MKV with the feature off advertises exactly one + - a build without transcode-dts drops a DTS track from the HLS master + playlist rather than offering a broken one + - the video.mp4 resource returns a parseable ftyp+moov followed by at least + two moof/mdat pairs, with the video track's avcC byte-identical to the + source's + + Live, against ./target/debug/vuio with CONTAINER=1 VUIO_MEDIA_DIRS=... and + the server killed in the same command: + - Browse as a Samsung UA, confirm both elements + - curl the video.mp4 resource, confirm it parses and carries both tracks + - confirm /media/{id} direct play is byte-identical to the source file + + Last, before committing: cargo fmt and cargo clippy, scoped to the + first-party crates (-p vuio-core -p vuio-cli -p vuio-cast -p vuio-web) so the + vendored tree under crates/vendor is not linted. From 1b4d980ec334eabd8e19e5fbe899060bd2493e2c Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 13:35:54 +0300 Subject: [PATCH 10/38] u --- compare.md | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 compare.md diff --git a/compare.md b/compare.md new file mode 100644 index 00000000..420c696b --- /dev/null +++ b/compare.md @@ -0,0 +1,138 @@ +# Feature Comparison: VuIO vs Jellyfin, Plex, Emby & MiniDLNA + +A comprehensive technical and functional comparison between **VuIO** and popular media server solutions: **Jellyfin**, **Plex Media Server**, **Emby**, **MiniDLNA (ReadyMedia)**, and **Universal Media Server (UMS)**. + +--- + +## 1. High-Level Overview & Philosophy + +| Aspect | VuIO | Jellyfin | Plex | Emby | MiniDLNA | Universal Media Server | +|---|---|---|---|---|---|---| +| **Core Philosophy** | Ultra-lightweight, high-performance, single-binary, AI-native media hub | Full-featured open-source self-hosted Netflix-style media platform | Commercial media ecosystem with cloud accounts and client apps | Commercial media platform with open/closed hybrid model | Minimalist background DLNA daemon | Java-based transcoding DLNA/UPnP server | +| **Language & Runtime** | Pure **Rust** (Tokio, Axum, SQLite) | **C# / .NET** (ASP.NET Core) | **C++** (Proprietary) | **C# / .NET** | **C** | **Java** (JVM) | +| **License** | **MIT / Apache-2.0** (100% FOSS) | **GPL-2.0** (100% FOSS) | **Proprietary** / Freemium (Plex Pass) | **Proprietary** / Freemium (Emby Premiere) | **GPL-2.0** (100% FOSS) | **GPL-2.0** (100% FOSS) | +| **RAM Usage (Idle / Active)** | **~25 MB – 80 MB** | 400 MB – 1.5 GB+ | 350 MB – 1.2 GB+ | 300 MB – 1.0 GB+ | **~15 MB – 50 MB** | 500 MB – 2.0 GB+ | +| **Distribution** | **Single standalone binary** (~15 MB) | Large runtime (~300 MB+ installed) | Large installer / image (~400 MB+) | Large installer / image (~300 MB+) | Lightweight binary + config | Large JAR + dependencies | +| **External Dependencies** | **None** (Self-contained) | .NET Runtime, FFmpeg | Proprietary codecs, Transcoder | FFmpeg, .NET | libjpeg, libsqlite3, libav | Java JRE, FFmpeg, MPlayer | +| **Cloud Account Required** | ❌ **No** (100% local) | ❌ **No** (100% local) | ⚠️ **Yes** (Plex.tv account & auth) | ⚠️ Optional (Emby Connect) | ❌ **No** | ❌ **No** | + +--- + +## 2. Comprehensive Feature Matrix + +### 📺 Protocols, Playback & Streaming + +| Feature | VuIO | Jellyfin | Plex | Emby | MiniDLNA | Universal Media Server | +|---|---|---|---|---|---|---| +| **DLNA / UPnP Media Server** | ✅ Full (SSDP, mDNS, DIDL-Lite, browse cache) | ✅ Basic | ✅ Basic | ✅ Basic | ✅ Full | ✅ Full | +| **Google Cast / Chromecast** | ✅ Native direct casting (Rust `ring` TLS) | ✅ Via web/app client | ✅ Via web/app client | ✅ Via web/app client | ❌ None | ⚠️ Limited | +| **AirPlay Video / Audio** | ✅ Native AirPlay discovery & streaming | ❌ Via third-party | ❌ Via third-party | ❌ Via third-party | ❌ None | ❌ None | +| **HLS Stream In-Browser** | ✅ Yes (segmented on-the-fly HLS) | ✅ Yes | ✅ Yes | ✅ Yes | ❌ None | ✅ Yes | +| **HTTP Byte-Range Streaming** | ✅ Yes (Sub-millisecond 4K seek) | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | +| **Live Radio Broadcasting** | ✅ Yes (Synchronous playout clock + P2P discovery) | ❌ None | ⚠️ Live TV / Audio plugins | ⚠️ Live TV plugins | ❌ None | ⚠️ Web streams | +| **Sidecar SRT to WebVTT** | ✅ Dynamic on-the-fly (Zero disk I/O) | ✅ Yes | ✅ Yes | ✅ Yes | ❌ None | ⚠️ Transcode | +| **Audio Transcoding** | ✅ AC-3, E-AC-3, DTS → LPCM / AAC-LC | ✅ Full FFmpeg | ✅ Full FFmpeg | ✅ Full FFmpeg | ❌ None | ✅ Full FFmpeg | +| **Video Passthrough Remuxing** | ✅ Zero-copy H.264/HEVC remuxing | ✅ Yes | ✅ Yes | ✅ Yes | ❌ None | ✅ Yes | +| **Heavy Video Re-encoding** | ❌ Deliberately omitted (Low CPU focus) | ✅ Full (GPU NVENC/VAAPI/QSV) | ✅ Full (GPU NVENC/QSV - Paid) | ✅ Full (GPU NVENC/QSV - Paid) | ❌ None | ✅ Full (CPU/GPU) | + +--- + +### 🤖 AI Agent, Automation & Developer Experience + +| Feature | VuIO | Jellyfin | Plex | Emby | MiniDLNA | Universal Media Server | +|---|---|---|---|---|---|---| +| **Model Context Protocol (MCP)** | ✅ **Native MCP (2026-07-28)** for Claude, ChatGPT, LLM agents | ❌ None | ❌ None | ❌ None | ❌ None | ❌ None | +| **AI Assistant Control** | ✅ Browse, search, play, cast via AI agents | ❌ Custom scripts only | ❌ Custom scripts only | ❌ Custom scripts only | ❌ None | ❌ None | +| **REST API** | ✅ Clean, lightweight JSON API | ✅ Large REST API | ✅ XML/JSON API | ✅ Large REST API | ❌ None | ⚠️ Web API | +| **OpenAPI / Swagger Spec** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes | ❌ No | ❌ No | +| **Live Config Hot-Reload** | ✅ 21 of 25 settings live reload (0 restart) | ⚠️ Partial | ⚠️ Partial | ⚠️ Partial | ❌ Requires restart | ⚠️ Partial | +| **Self-Updating Binary** | ✅ Built-in (`vuio --update`) | ❌ Package manager only | ⚠️ In-app (Plex Pass) | ⚠️ In-app | ❌ Package manager only | ⚠️ In-app | + +--- + +### 📚 Metadata & Library Management + +| Feature | VuIO | Jellyfin | Plex | Emby | MiniDLNA | Universal Media Server | +|---|---|---|---|---|---|---| +| **Metadata Scraping Providers** | ✅ TMDb, OMDb, TVmaze, MusicBrainz, Discogs, Last.fm, Genius, AniList, Jikan, Kitsu | ✅ TMDb, TheTVDB, OMDb, MusicBrainz | ✅ Plex Media Agent (Proprietary) | ✅ TMDb, TheTVDB, MusicBrainz | ❌ Embedded tags only | ⚠️ TMDb, MusicBrainz | +| **Anime-Specific Metadata** | ✅ AniList, Jikan (MyAnimeList), Kitsu | ⚠️ Via community plugins | ⚠️ Via third-party agents | ⚠️ Via community plugins | ❌ None | ❌ None | +| **Music Tag Extraction** | ✅ ID3v1/v2, FLAC/Vorbis, MP4/AAC, RIFF tags | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Basic ID3/FLAC | ✅ Basic | +| **Playlist Formats** | ✅ M3U, M3U8, PLS auto-discovery | ✅ M3U, Web playlists | ✅ Proprietary | ✅ M3U, Web playlists | ✅ M3U, PLS | ✅ M3U, PLS | +| **Live Filesystem Watcher** | ✅ Real-time async notify watcher | ✅ Yes | ✅ Yes | ✅ Yes | ✅ inotify (Linux only) | ✅ Yes | +| **Database Engine** | ✅ Embedded SQLite with WAL mode & memory cache | SQLite / EF Core | SQLite (Custom tuned) | SQLite | SQLite | H2 / SQLite | + +--- + +### 🌐 User Interface & Client Ecosystem + +| Feature | VuIO | Jellyfin | Plex | Emby | MiniDLNA | Universal Media Server | +|---|---|---|---|---|---|---| +| **Web Interface** | ✅ Dual UI: Modern Svelte (`:8090`) + Light Dashboard (`:8080`) | ✅ Modern Vue/React Web UI | ✅ Feature-rich Web UI | ✅ Feature-rich Web UI | ❌ Basic status page only | ✅ Basic web interface | +| **Dedicated Mobile Apps** | ⚠️ Use standard DLNA / Cast / Web UI | ✅ iOS & Android (FOSS) | ✅ iOS & Android (Paid/IAP) | ✅ iOS & Android (Paid/IAP) | ❌ Standard DLNA apps | ❌ Standard DLNA apps | +| **Dedicated Smart TV Apps** | ⚠️ Native DLNA / AirPlay / Cast to all TVs | ✅ Android TV, Roku, Apple TV, LG webOS, Tizen | ✅ All Smart TV app stores | ✅ All Smart TV app stores | ⚠️ Native DLNA | ⚠️ Native DLNA | +| **Multi-User Profiles** | ⚠️ Network CIDR / admin auth token | ✅ Granular user accounts & watch history | ✅ Home users & cloud sharing | ✅ Granular user accounts | ❌ None | ❌ None | + +--- + +### 📊 Observability, Cloud-Native & Deployment + +| Feature | VuIO | Jellyfin | Plex | Emby | MiniDLNA | Universal Media Server | +|---|---|---|---|---|---|---| +| **Prometheus Metrics** | ✅ Native (`/metrics` Prometheus exposition) | ⚠️ Via community plugin | ⚠️ Via third-party exporters | ⚠️ Via plugin | ❌ None | ❌ None | +| **Kubernetes Health Probes** | ✅ Native `/healthz` & `/readyz` | ⚠️ Web UI HTTP probe | ⚠️ Web UI HTTP probe | ⚠️ Web UI HTTP probe | ❌ None | ❌ None | +| **Helm Chart** | ✅ Official Helm chart (`oci://ghcr.io/...`) | ⚠️ Community charts | ⚠️ Community charts | ⚠️ Community charts | ❌ None | ❌ None | +| **Log Streaming Endpoint** | ✅ Native `/logs` for Grafana Loki/Alloy | ❌ File/Systemd logs only | ❌ File/Plex logs only | ❌ File logs only | ❌ Systemd only | ❌ File logs only | +| **Docker Footprint** | ✅ Minimal scratch/distroless container (~25 MB) | ⚠️ ~500 MB – 1 GB | ⚠️ ~800 MB+ | ⚠️ ~600 MB+ | ⚠️ ~50 MB | ⚠️ ~600 MB+ | +| **Linux Distro Packages** | ✅ DEB, RPM, APK (Alpine), Arch (Pacman), Musl, Tarballs | ⚠️ DEB, RPM, Flatpak | ⚠️ DEB, RPM, Snap | ⚠️ DEB, RPM, Flatpak | ✅ Distro repos | ⚠️ Tarball / Flatpak | + +--- + +## 3. In-Depth Head-to-Head Comparison + +### VuIO vs Jellyfin +* **Resource Efficiency**: VuIO uses ~25 MB of RAM and boots in milliseconds, whereas Jellyfin runs on the .NET runtime consuming 400 MB to 1+ GB RAM. +* **Architecture**: VuIO is a single compiled binary without external dependencies (no runtime or separate FFmpeg executable needed). Jellyfin is a full web application platform. +* **Modern Protocols**: VuIO includes native Chromecast and AirPlay senders directly in the backend and supports the AI Model Context Protocol (MCP). Jellyfin relies on client apps and standard web streams. +* **When to choose Jellyfin**: If you want multi-user parental control profiles, remote watch-together synchronization, or heavy on-the-fly video resolution/bitrate downscaling for remote mobile streaming. +* **When to choose VuIO**: If you want a blazingly fast, lightweight local media server that streams directly to TVs, Chromecast, and AirPlay with zero bloat and full AI agent interoperability. + +--- + +### VuIO vs Plex +* **Privacy & Telemetry**: VuIO has **zero telemetry, zero phone-home, and no account requirements**. Plex requires authenticating through `plex.tv` cloud servers and collects user metrics. +* **Licensing & Cost**: VuIO is 100% Free and Open Source (MIT / Apache-2.0). Plex locks features (hardware transcoding, mobile playback, offline sync, DVR) behind the paid **Plex Pass**. +* **Simplicity**: VuIO can be launched by running a single binary pointing at a folder: `./vuio /media`. Plex requires complex installation, claiming servers, and cloud account linking. +* **When to choose Plex**: If you want turnkey commercial client applications on every app store with cloud-managed remote access sharing with friends/family. +* **When to choose VuIO**: If you value privacy, open-source software, low memory usage, and zero cloud dependency. + +--- + +### VuIO vs MiniDLNA (ReadyMedia) +* **Modern Web Interface & Experience**: MiniDLNA has no user interface (only a raw plain-text status page). VuIO provides a modern Svelte 5 web player interface with audio/video scrubbing and album art. +* **Casting & Playback**: MiniDLNA only speaks DLNA/UPnP. VuIO streams to DLNA, Google Cast / Chromecast, AirPlay, and in-browser HLS. +* **Metadata**: MiniDLNA only extracts embedded ID3/MP4 tags and cannot scrape posters, summaries, or ratings from TMDb, OMDb, MusicBrainz, or AniList. +* **Maintainability**: MiniDLNA is written in legacy C with manual memory management; VuIO is written in memory-safe asynchronous Rust. +* **When to choose VuIO**: VuIO is the modern, drop-in replacement for MiniDLNA with rich web capabilities, multi-protocol casting, and metadata enrichment while maintaining the same lightweight resource footprint. + +--- + +### VuIO vs Universal Media Server (UMS) +* **Runtime & Overhead**: UMS is built in Java and requires a heavy Java Runtime Environment (JRE), consuming high RAM and CPU cycles. VuIO is native machine code. +* **AI & Cloud-Native Integration**: VuIO provides first-class Prometheus metrics, Kubernetes probes, log streaming, and AI MCP integration. UMS is a desktop-focused Java utility. + +--- + +## 4. Summary: When Should You Use VuIO? + +``` + VU IO + "The High-Performance, AI-Ready Modern Media Hub" + ────────────────────────────────────────────────────────────────── + ✔ Low Resource Consumption (~25 MB RAM, instant startup) + ✔ Single Standalone Binary (Zero runtime dependencies) + ✔ Multi-Protocol Streaming (DLNA + Chromecast + AirPlay + Web HLS) + ✔ AI Agent & MCP Support (Claude, ChatGPT, Local LLM integration) + ✔ Rich Metadata Enrichment (TMDb, OMDb, MusicBrainz, AniList) + ✔ Live Radio Broadcasting (P2P synced station streaming) + ✔ 100% Local & Private (No cloud accounts, no paywalls, open source) +``` From 243de7d9952ec0d52cb032f67a9d4b809444c24f Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 17:41:05 +0300 Subject: [PATCH 11/38] u --- .github/workflows/ci.yml | 2 +- Cargo.toml | 13 + config.example.toml | 10 +- crates/vuio-core/Cargo.toml | 12 +- crates/vuio-core/README.md | 2 +- crates/vuio-core/src/database/mod.rs | 23 + .../src/database/sqlite/media_repo/bulk.rs | 14 +- .../vuio-core/src/database/sqlite/schema.rs | 27 +- .../vuio-core/src/database/sqlite/session.rs | 3 + crates/vuio-core/src/database/sqlite/tests.rs | 2 + .../vuio-core/src/media/remux/fmp4_writer.rs | 295 +++++- crates/vuio-core/src/media/remux/hls.rs | 66 +- .../vuio-core/src/media/remux/mkv_demuxer.rs | 110 +- crates/vuio-core/src/media/scanner.rs | 7 + crates/vuio-core/src/media/transcode/aac.rs | 141 +++ .../vuio-core/src/media/transcode/frames.rs | 10 + crates/vuio-core/src/media/transcode/mod.rs | 16 +- crates/vuio-core/src/media/transcode/pcm.rs | 29 +- crates/vuio-core/src/media/transcode/plan.rs | 165 ++- .../src/media/transcode/rendition.rs | 154 +++ .../vuio-core/src/media/transcode/session.rs | 112 +- .../vuio-core/src/media/transcode/source.rs | 507 +++++++++ crates/vuio-core/src/media/transcode/video.rs | 421 ++++++++ .../src/platform/filesystem/metadata.rs | 114 +- .../vuio-core/src/platform/filesystem/mod.rs | 9 + crates/vuio-core/src/web/mod.rs | 70 +- crates/vuio-core/src/web/remux_streaming.rs | 173 +++- .../vuio-core/src/web/transcode_streaming.rs | 267 ++--- crates/vuio-core/src/web/video_streaming.rs | 334 ++++++ crates/vuio-core/src/web/xml.rs | 6 +- crates/vuio-core/src/web/xml/browse.rs | 7 + crates/vuio-core/src/web/xml/rendering.rs | 73 +- crates/vuio-core/tests/common/mod.rs | 383 +++++++ .../vuio-core/tests/film_transcode_tests.rs | 978 ++++++++++++++++++ docs/configuration.md | 45 +- 35 files changed, 4254 insertions(+), 346 deletions(-) create mode 100644 crates/vuio-core/src/media/transcode/rendition.rs create mode 100644 crates/vuio-core/src/media/transcode/source.rs create mode 100644 crates/vuio-core/src/media/transcode/video.rs create mode 100644 crates/vuio-core/src/web/video_streaming.rs create mode 100644 crates/vuio-core/tests/common/mod.rs create mode 100644 crates/vuio-core/tests/film_transcode_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 697080f2..5147a3f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -234,7 +234,7 @@ jobs: - name: Build each feature alone run: | for feature in casting dashboard diagnostics mcp mediainfo metadata web-ui \ - transcode transcode-ac3 transcode-dts transcode-aac; do + demux transcode transcode-ac3 transcode-dts transcode-aac; do echo "::group::$feature" cargo check -p vuio-core --no-default-features --features "$feature" echo "::endgroup::" diff --git a/Cargo.toml b/Cargo.toml index b18bb144..aeaf9861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,19 @@ default-members = [ ] resolver = "2" +# The vendored codecs are the only CPU-bound code in the tree, and an +# unoptimized AAC encoder turns a four-second test segment into a minute of +# wall clock. Optimizing those four packages alone leaves `cargo build` on the +# code actually being worked on as fast as it was. +[profile.dev.package.oxideav-core] +opt-level = 2 +[profile.dev.package.oxideav-ac3] +opt-level = 2 +[profile.dev.package.oxideav-dts] +opt-level = 2 +[profile.dev.package.oxideav-aac] +opt-level = 2 + [profile.release] opt-level = 3 lto = "fat" diff --git a/config.example.toml b/config.example.toml index 19bf1a05..3ddf0895 100644 --- a/config.example.toml +++ b/config.example.toml @@ -131,13 +131,17 @@ port = 8090 # Must differ from server.port # A TV that was already fine is unaffected. [transcode] enabled = true -# What the decoded version is delivered as. +# What the decoded version of an *audio* item is delivered as. # lpcm uncompressed, seekable, about 1.5 Mbps # aac about a tenth of that, lossy, and cannot be scrubbed +# A film is always delivered as fragmented MP4 — the picture copied through +# untouched, the soundtrack re-encoded — and is seekable by time rather than by +# byte, which works the same whatever its audio codec was. audio_format = "lpcm" # Which version is listed first, for a TV that takes what it is given rather # than choosing. Switch to "transcoded" only if a TV still plays silently. prefer = "original" -# Decodes allowed at once. Past this a further request is refused rather than -# queued, so the streams already playing keep up. +# Decodes allowed at once, shared between TVs and the browser player. Past this +# a further request is refused rather than queued, so the streams already +# playing keep up. max_concurrent = 2 diff --git a/crates/vuio-core/Cargo.toml b/crates/vuio-core/Cargo.toml index bb2c2a55..14dec055 100644 --- a/crates/vuio-core/Cargo.toml +++ b/crates/vuio-core/Cargo.toml @@ -55,7 +55,7 @@ default = [ # real-time decode on a small box is tight. casting = [ "dep:vuio-cast", - "dep:symphonia", + "demux", "symphonia/opt-simd", "dep:hap-crypto", "dep:hap-transport", @@ -70,7 +70,15 @@ casting = [ dashboard = [] diagnostics = ["dep:sysinfo"] mcp = [] -metadata = ["dep:symphonia"] +metadata = ["demux"] + +# Internal, and not meant to be selected on its own: "symphonia is in this +# build, so a container can be demuxed". Both `metadata` and `casting` need +# exactly that and nothing more of each other — reading a film's audio track out +# of its MKV is the same operation whether the caller wanted its tags or wanted +# to remux it — so the code that does it asks for this rather than for +# `any(metadata, casting)` at every call site. +demux = ["dep:symphonia"] # The Svelte browser interface, served on its own listener (port 8090 by # default) beside the built-in dashboard on the main port. Both surfaces are diff --git a/crates/vuio-core/README.md b/crates/vuio-core/README.md index d18692b4..e11304e0 100644 --- a/crates/vuio-core/README.md +++ b/crates/vuio-core/README.md @@ -83,7 +83,7 @@ firmware has less to read. | --- | --- | --- | | `casting` | Chromecast, AirPlay and DLNA renderer control | 44 | | `mediainfo` | titles, synopses, ratings and artwork from public APIs | 29 | -| `transcode-ac3` / `-dts` / `-aac` | AC-3, E-AC-3 and DTS decoded for renderers that cannot play them | 4 | +| `transcode-ac3` / `-dts` / `-aac` | AC-3, E-AC-3 and DTS decoded for renderers that cannot play them, in a film as well as on its own | 4 | | `diagnostics` | system and disk metrics on the status endpoints | 3 | | `web-ui` | the Svelte interface on the second listener | 1 | | `metadata` | tags and embedded cover art (files keep filename titles) | 0 | diff --git a/crates/vuio-core/src/database/mod.rs b/crates/vuio-core/src/database/mod.rs index 90abbaaa..bc2972ff 100644 --- a/crates/vuio-core/src/database/mod.rs +++ b/crates/vuio-core/src/database/mod.rs @@ -216,7 +216,20 @@ pub struct AudioTags { /// a track before fetching it. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct StreamInfo { + /// The audio track's codec, as a short name (`ac3`, `flac`, `aac`, …). + /// + /// This is the field that decides whether an item needs a decoded + /// alternative offered beside it, so it is filled in for video files too — + /// a film's audio track is exactly the case the whole transcoding path + /// exists for. pub codec: Option, + /// The video track's codec, where the file has one (`h264`, `hevc`, …). + /// + /// Separate from `codec` because the two answer different questions: + /// whether a decoded alternative is *needed* is about the audio, whether one + /// can be *produced* is about the video, which the alternative copies + /// through rather than re-encoding. + pub video_codec: Option, pub sample_rate: Option, pub channels: Option, pub bits_per_sample: Option, @@ -464,6 +477,9 @@ impl MediaFileView for MediaFile { fn codec(&self) -> Option<&str> { self.stream.codec.as_deref() } + fn video_codec(&self) -> Option<&str> { + self.stream.video_codec.as_deref() + } fn sample_rate(&self) -> Option { self.stream.sample_rate } @@ -533,6 +549,13 @@ pub trait MediaFileView { fn codec(&self) -> Option<&str> { None } + /// The video track's codec, for a view that carries one. + /// + /// Defaults to `None`, which the browse path reads as "not recorded" and + /// treats as remuxable — see [`crate::web::item_can_remux_video`]. + fn video_codec(&self) -> Option<&str> { + None + } fn sample_rate(&self) -> Option { None } diff --git a/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs b/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs index 932b5315..6a48b3cf 100644 --- a/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs +++ b/crates/vuio-core/src/database/sqlite/media_repo/bulk.rs @@ -22,10 +22,11 @@ INSERT INTO media_files ( disc_number, disc_total, track_total, composer, comment, bpm, compilation, sort_title, sort_artist, sort_album, release_date, musicbrainz_track_id, musicbrainz_album_id, musicbrainz_artist_id, - codec, sample_rate, channels, bits_per_sample, bit_rate, tags_version -) VALUES (?39, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, + codec, sample_rate, channels, bits_per_sample, bit_rate, tags_version, + video_codec +) VALUES (?40, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, - ?36, ?37, ?38)"; + ?36, ?37, ?38, ?39)"; pub(in crate::database::sqlite) const UPDATE_MEDIA: &str = "\ UPDATE media_files SET @@ -38,10 +39,10 @@ UPDATE media_files SET sort_artist = ?27, sort_album = ?28, release_date = ?29, musicbrainz_track_id = ?30, musicbrainz_album_id = ?31, musicbrainz_artist_id = ?32, codec = ?33, sample_rate = ?34, channels = ?35, bits_per_sample = ?36, - bit_rate = ?37, tags_version = ?38 -WHERE id = ?39"; + bit_rate = ?37, tags_version = ?38, video_codec = ?39 +WHERE id = ?40"; -/// The thirty-eight stored fields, in the order both statements bind them. +/// The thirty-nine stored fields, in the order both statements bind them. pub(in crate::database::sqlite) fn bind_media_file(file: &MediaFile) -> Vec { let path = file.path.to_string_lossy().into_owned(); let parent = SqliteDatabase::parent_directory(&path).unwrap_or_default(); @@ -92,6 +93,7 @@ pub(in crate::database::sqlite) fn bind_media_file(file: &MediaFile) -> Vec Vec<(i64, String)> { (4, migration_v4()), (5, MIGRATION_V5.to_owned()), (6, MIGRATION_V6.to_owned()), + (7, MIGRATION_V7.to_owned()), ] } @@ -472,7 +478,22 @@ media_files.sort_title, media_files.sort_artist, media_files.sort_album, \ media_files.release_date, media_files.musicbrainz_track_id, \ media_files.musicbrainz_album_id, media_files.musicbrainz_artist_id, \ media_files.codec, media_files.sample_rate, media_files.channels, \ -media_files.bits_per_sample, media_files.bit_rate, media_files.tags_version"; +media_files.bits_per_sample, media_files.bit_rate, media_files.tags_version, \ +media_files.video_codec"; + +/// v6 → v7: the video track's codec. +/// +/// A film with an AC-3 or DTS track is offered a remuxed alternative whose +/// picture is copied through untouched, which only works for the video codecs +/// the fMP4 writer can describe. Deciding that has to be a column read for the +/// same reason the audio codec is: a folder of four hundred films would +/// otherwise be four hundred file opens per Browse response. +/// +/// Existing rows get NULL, and `TAGS_VERSION` moved with this change, so the +/// next scan fills them in. +const MIGRATION_V7: &str = r#" +ALTER TABLE media_files ADD COLUMN video_codec TEXT; +"#; /// Positions within [`MEDIA_COLUMNS`], shared by the owned decoder and the /// borrowed views so the two can never drift apart. @@ -514,6 +535,7 @@ pub(super) mod column { pub const BITS_PER_SAMPLE: usize = 34; pub const BIT_RATE: usize = 35; pub const TAGS_VERSION: usize = 36; + pub const VIDEO_CODEC: usize = 37; } /// Open one connection and put it in the state every caller expects. @@ -737,6 +759,7 @@ pub(super) fn media_file_from_row(row: &Row<'_>) -> rusqlite::Result }, stream: StreamInfo { codec: row.get(column::CODEC)?, + video_codec: row.get(column::VIDEO_CODEC)?, sample_rate: optional_u32(row, column::SAMPLE_RATE)?, channels: optional_u32(row, column::CHANNELS)?.map(|value| value as u16), bits_per_sample: optional_u32(row, column::BITS_PER_SAMPLE)?.map(|value| value as u16), diff --git a/crates/vuio-core/src/database/sqlite/session.rs b/crates/vuio-core/src/database/sqlite/session.rs index adbde536..1d8eac08 100644 --- a/crates/vuio-core/src/database/sqlite/session.rs +++ b/crates/vuio-core/src/database/sqlite/session.rs @@ -148,6 +148,9 @@ impl MediaFileView for SqliteMediaFileView<'_> { fn codec(&self) -> Option<&str> { self.optional_text(column::CODEC) } + fn video_codec(&self) -> Option<&str> { + self.optional_text(column::VIDEO_CODEC) + } fn sample_rate(&self) -> Option { self.optional_integer(column::SAMPLE_RATE) .map(|value| value as u32) diff --git a/crates/vuio-core/src/database/sqlite/tests.rs b/crates/vuio-core/src/database/sqlite/tests.rs index 81b0d4a8..c4d9e279 100644 --- a/crates/vuio-core/src/database/sqlite/tests.rs +++ b/crates/vuio-core/src/database/sqlite/tests.rs @@ -201,6 +201,8 @@ async fn a_v1_database_migrates_forward_without_losing_anything() { assert_eq!(file.artist.as_deref(), Some("Artist")); // New columns exist and are empty until a scan re-reads the file. assert_eq!(file.tags.disc_number, None); + assert_eq!(file.stream.codec, None); + assert_eq!(file.stream.video_codec, None, "v7's column"); assert_eq!(file.tags_version, 0); // Everything a rebuild would have thrown away. diff --git a/crates/vuio-core/src/media/remux/fmp4_writer.rs b/crates/vuio-core/src/media/remux/fmp4_writer.rs index 9db82a05..3e755246 100644 --- a/crates/vuio-core/src/media/remux/fmp4_writer.rs +++ b/crates/vuio-core/src/media/remux/fmp4_writer.rs @@ -13,6 +13,14 @@ pub struct SampleInfo { pub composition_time_offset: i32, } +/// One track's run of samples inside a movie fragment. +pub struct TrackRun<'a> { + pub track_id: u32, + /// Decode time of this run's first sample, in the track's own timescale. + pub base_decode_time: u64, + pub samples: &'a [SampleInfo], +} + pub struct Fmp4Writer; impl Fmp4Writer { @@ -27,9 +35,31 @@ impl Fmp4Writer { Self::wrap_box(&box_data) } - /// Build `moov` (Movie Header) init segment box for a track. + /// The `mvhd` timescale, and therefore the unit of every movie-level duration. + const MOVIE_TIMESCALE: u32 = 1000; + + /// Build `moov` (Movie Header) init segment box for a single track. + /// + /// Used by the HLS path, where every rendition is its own single-track + /// stream: MSE cannot initialise a `SourceBuffer` for one track from a `moov` + /// that also describes another. pub fn build_moov(track: &TrackInfo) -> Vec { + Self::build_moov_for(&[track], None) + } + + /// Build `moov` for one or more tracks, optionally declaring the movie's + /// total duration. + /// + /// A progressive stream needs both: one `moov` describing the video and audio + /// tracks together, and a duration, because a renderer with no + /// `Content-Length` to divide has nothing else to draw a scrub bar from. + /// `duration_ms` is written into `mvhd`, into each `tkhd`, and into `mehd` — + /// the last being the one a fragmented file is actually meant to carry, and + /// the one players look for. + pub fn build_moov_for(tracks: &[&TrackInfo], duration_ms: Option) -> Vec { let mut moov_body = Vec::new(); + let movie_duration = duration_ms.unwrap_or(0).min(u64::from(u32::MAX)) as u32; + let next_track_id = tracks.iter().map(|t| t.id).max().unwrap_or(1) + 1; // 1. mvhd (Movie Header) let mut mvhd = Vec::new(); @@ -38,8 +68,8 @@ impl Fmp4Writer { mvhd.extend_from_slice(&[0; 3]); // flags mvhd.extend_from_slice(&[0; 4]); // creation_time mvhd.extend_from_slice(&[0; 4]); // modification_time - mvhd.extend_from_slice(&(1000u32).to_be_bytes()); // timescale = 1000 Hz - mvhd.extend_from_slice(&(0u32).to_be_bytes()); // duration = 0 for fMP4 init + mvhd.extend_from_slice(&Self::MOVIE_TIMESCALE.to_be_bytes()); + mvhd.extend_from_slice(&movie_duration.to_be_bytes()); mvhd.extend_from_slice(&(0x00010000u32).to_be_bytes()); // rate = 1.0 mvhd.extend_from_slice(&(0x0100u16).to_be_bytes()); // volume = 1.0 mvhd.extend_from_slice(&[0; 10]); // reserved @@ -50,16 +80,16 @@ impl Fmp4Writer { mvhd.extend_from_slice(&[0; 12]); mvhd.extend_from_slice(&(0x40000000u32).to_be_bytes()); mvhd.extend_from_slice(&[0; 24]); // pre_defined - mvhd.extend_from_slice(&(2u32).to_be_bytes()); // next_track_id + mvhd.extend_from_slice(&next_track_id.to_be_bytes()); moov_body.extend_from_slice(&Self::wrap_box(&mvhd)); - // 2. trak (Track Atom) - let trak = Self::build_trak(track); - moov_body.extend_from_slice(&trak); + // 2. trak (Track Atom), one per track + for track in tracks { + moov_body.extend_from_slice(&Self::build_trak(track, movie_duration)); + } // 3. mvex (Movie Extends Atom) - let mvex = Self::build_mvex(track.id); - moov_body.extend_from_slice(&mvex); + moov_body.extend_from_slice(&Self::build_mvex(tracks, duration_ms)); let mut moov = Vec::new(); moov.extend_from_slice(b"moov"); @@ -79,7 +109,7 @@ impl Fmp4Writer { } } - fn build_trak(track: &TrackInfo) -> Vec { + fn build_trak(track: &TrackInfo, movie_duration: u32) -> Vec { let mut trak_body = Vec::new(); // tkhd (Track Header) @@ -91,7 +121,7 @@ impl Fmp4Writer { tkhd.extend_from_slice(&[0; 4]); // modification_time tkhd.extend_from_slice(&track.id.to_be_bytes()); // track_id tkhd.extend_from_slice(&[0; 4]); // reserved - tkhd.extend_from_slice(&[0; 4]); // duration + tkhd.extend_from_slice(&movie_duration.to_be_bytes()); // duration, mvhd units tkhd.extend_from_slice(&[0; 8]); // reserved tkhd.extend_from_slice(&[0; 2]); // layer tkhd.extend_from_slice(&[0; 2]); // alternate_group @@ -262,7 +292,15 @@ impl Fmp4Writer { } Self::wrap_box(&sample_entry) } - TrackCodec::Aac | TrackCodec::Unsupported => { + // Everything else gets an `mp4a` entry. The three decoded codecs are + // here only for exhaustiveness: a track of theirs reaches this writer + // already restated as AAC (see `aac_track`), because what the fragment + // will carry is the re-encoded stream, not the source. + TrackCodec::Aac + | TrackCodec::Ac3 + | TrackCodec::Eac3 + | TrackCodec::Dts + | TrackCodec::Unsupported => { let mut sample_entry = Vec::new(); sample_entry.extend_from_slice(b"mp4a"); sample_entry.extend_from_slice(&[0; 6]); // reserved @@ -365,27 +403,65 @@ impl Fmp4Writer { esds } - fn build_mvex(track_id: u32) -> Vec { - let mut trex = Vec::new(); - trex.extend_from_slice(b"trex"); - trex.extend_from_slice(&[0; 4]); // version + flags - trex.extend_from_slice(&track_id.to_be_bytes()); - trex.extend_from_slice(&(1u32).to_be_bytes()); // default_sample_description_index - trex.extend_from_slice(&[0; 12]); // default duration, size, flags - + fn build_mvex(tracks: &[&TrackInfo], duration_ms: Option) -> Vec { let mut mvex = Vec::new(); mvex.extend_from_slice(b"mvex"); - mvex.extend_from_slice(&Self::wrap_box(&trex)); + + // mehd (Movie Extends Header) — the fragmented file's own statement of + // how long it is. `mvhd`'s duration describes the samples in the `moov`, + // of which a fragmented file has none, so this is the box a player reads + // to know the total. Omitted when the length is genuinely unknown rather + // than declared as zero, which some players read as "empty". + if let Some(duration_ms) = duration_ms.filter(|d| *d > 0) { + let mut mehd = Vec::new(); + mehd.extend_from_slice(b"mehd"); + mehd.push(1); // version 1 (64-bit fragment_duration) + mehd.extend_from_slice(&[0; 3]); // flags + mehd.extend_from_slice(&duration_ms.to_be_bytes()); + mvex.extend_from_slice(&Self::wrap_box(&mehd)); + } + + for track in tracks { + let mut trex = Vec::new(); + trex.extend_from_slice(b"trex"); + trex.extend_from_slice(&[0; 4]); // version + flags + trex.extend_from_slice(&track.id.to_be_bytes()); + trex.extend_from_slice(&(1u32).to_be_bytes()); // default_sample_description_index + trex.extend_from_slice(&[0; 12]); // default duration, size, flags + mvex.extend_from_slice(&Self::wrap_box(&trex)); + } Self::wrap_box(&mvex) } - /// Build `moof` (Movie Fragment) box for a sequence of samples. + /// Build `moof` (Movie Fragment) box for one track's run of samples. pub fn build_moof( sequence_number: u32, track_id: u32, base_decode_time: u64, samples: &[SampleInfo], data_offset: u32, + ) -> Vec { + Self::build_moof_multi( + sequence_number, + &[TrackRun { + track_id, + base_decode_time, + samples, + }], + &[data_offset], + ) + } + + /// Build `moof` for one or more tracks sharing a single `mdat`. + /// + /// One `traf` per run, in the order given, and each run's `data_offset` must + /// point at where that run's samples start inside the following `mdat` — + /// measured, as `tfhd`'s `default-base-is-moof` flag says, from the first + /// byte of this `moof`. + pub fn build_moof_multi( + sequence_number: u32, + runs: &[TrackRun<'_>], + data_offsets: &[u32], ) -> Vec { let mut moof_body = Vec::new(); @@ -396,7 +472,22 @@ impl Fmp4Writer { mfhd.extend_from_slice(&sequence_number.to_be_bytes()); moof_body.extend_from_slice(&Self::wrap_box(&mfhd)); - // traf (Track Fragment) + for (run, data_offset) in runs.iter().zip(data_offsets) { + moof_body.extend_from_slice(&Self::build_traf(run, *data_offset)); + } + + let mut moof = Vec::new(); + moof.extend_from_slice(b"moof"); + moof.extend_from_slice(&moof_body); + Self::wrap_box(&moof) + } + + fn build_traf(run: &TrackRun<'_>, data_offset: u32) -> Vec { + let TrackRun { + track_id, + base_decode_time, + samples, + } = *run; let mut traf_body = Vec::new(); // tfhd (Track Fragment Header) @@ -458,12 +549,7 @@ impl Fmp4Writer { let mut traf = Vec::new(); traf.extend_from_slice(b"traf"); traf.extend_from_slice(&traf_body); - moof_body.extend_from_slice(&Self::wrap_box(&traf)); - - let mut moof = Vec::new(); - moof.extend_from_slice(b"moof"); - moof.extend_from_slice(&moof_body); - Self::wrap_box(&moof) + Self::wrap_box(&traf) } /// Build `mdat` (Media Data) box wrapping packet byte payloads. @@ -493,9 +579,128 @@ impl Fmp4Writer { fallback_base_decode_time: u64, packets: &[MediaPacket], ) -> Vec { + let samples = Self::samples_for(track, packets); + let base_decode_time = packets + .first() + .map(|p| p.dts) + .unwrap_or(fallback_base_decode_time); + let run = TrackRun { + track_id: track.id, + base_decode_time, + samples: &samples, + }; + + // Build moof first with a placeholder data_offset of 0 to measure its size. + let moof_placeholder = Self::build_moof_multi(sequence_number, &[run], &[0]); + // data_offset = moof box size + 8 bytes for the mdat header + let data_offset = moof_placeholder.len() as u32 + 8; + + // Rebuild with the correct data_offset + let run = TrackRun { + track_id: track.id, + base_decode_time, + samples: &samples, + }; + let moof = Self::build_moof_multi(sequence_number, &[run], &[data_offset]); + let mdat = Self::build_mdat(packets); + + let mut segment = Vec::with_capacity(moof.len() + mdat.len()); + segment.extend_from_slice(&moof); + segment.extend_from_slice(&mdat); + segment + } + + /// Build one fragment carrying several tracks — the progressive layout. + /// + /// A `moof` with a `traf` per track, then a single `mdat` holding each + /// track's samples in the same order. Both trafs in one fragment rather than + /// alternating single-track fragments: it is what every muxer emits for a + /// progressive file, and the format a television is most likely to have been + /// tested against. + /// + /// `fallback_base_decode_times` supplies a run's base decode time only when + /// that track contributed no packets to this fragment. + pub fn build_multi_track_segment( + sequence_number: u32, + tracks: &[(&TrackInfo, &[MediaPacket])], + fallback_base_decode_times: &[u64], + ) -> Vec { + let sample_sets: Vec> = tracks + .iter() + .map(|(track, packets)| Self::samples_for(track, packets)) + .collect(); + let bases: Vec = tracks + .iter() + .enumerate() + .map(|(i, (_, packets))| { + packets + .first() + .map(|p| p.dts) + .unwrap_or_else(|| fallback_base_decode_times.get(i).copied().unwrap_or(0)) + }) + .collect(); + + fn runs<'a>( + tracks: &[(&TrackInfo, &[MediaPacket])], + bases: &[u64], + sets: &'a [Vec], + ) -> Vec> { + tracks + .iter() + .enumerate() + .map(|(i, (track, _))| TrackRun { + track_id: track.id, + base_decode_time: bases[i], + samples: &sets[i], + }) + .collect() + } + + // Measure the moof with placeholder offsets, then rebuild with the real + // ones. The box's size does not depend on the offsets it carries — every + // `data_offset` is a fixed-width 32-bit field — so one measuring pass is + // enough. + let placeholder = Self::build_moof_multi( + sequence_number, + &runs(tracks, &bases, &sample_sets), + &vec![0u32; tracks.len()], + ); + let mut offset = placeholder.len() as u32 + 8; + let mut data_offsets = Vec::with_capacity(tracks.len()); + for (_, packets) in tracks { + data_offsets.push(offset); + offset += packets.iter().map(|p| p.data.len() as u32).sum::(); + } + + let moof = Self::build_moof_multi( + sequence_number, + &runs(tracks, &bases, &sample_sets), + &data_offsets, + ); + + let payload: usize = tracks + .iter() + .map(|(_, packets)| packets.iter().map(|p| p.data.len()).sum::()) + .sum(); + let mut mdat = Vec::with_capacity(8 + payload); + mdat.extend_from_slice(b"mdat"); + for (_, packets) in tracks { + for packet in *packets { + mdat.extend_from_slice(&packet.data); + } + } + let mdat = Self::wrap_box(&mdat); + + let mut segment = Vec::with_capacity(moof.len() + mdat.len()); + segment.extend_from_slice(&moof); + segment.extend_from_slice(&mdat); + segment + } + + fn samples_for(track: &TrackInfo, packets: &[MediaPacket]) -> Vec { let timescale = Self::timescale_for(track); - let samples: Vec = packets + packets .iter() .enumerate() .map(|(i, p)| { @@ -523,26 +728,7 @@ impl Fmp4Writer { composition_time_offset: (p.pts as i64 - p.dts as i64) as i32, } }) - .collect(); - - let base_decode_time = packets - .first() - .map(|p| p.dts) - .unwrap_or(fallback_base_decode_time); - - // Build moof first with a placeholder data_offset of 0 to measure its size. - let moof_placeholder = Self::build_moof(sequence_number, track.id, base_decode_time, &samples, 0); - // data_offset = moof box size + 8 bytes for the mdat header - let data_offset = moof_placeholder.len() as u32 + 8; - - // Rebuild with the correct data_offset - let moof = Self::build_moof(sequence_number, track.id, base_decode_time, &samples, data_offset); - let mdat = Self::build_mdat(packets); - - let mut segment = Vec::with_capacity(moof.len() + mdat.len()); - segment.extend_from_slice(&moof); - segment.extend_from_slice(&mdat); - segment + .collect() } fn wrap_box(contents: &[u8]) -> Vec { @@ -578,6 +764,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![], }; let moov = Fmp4Writer::build_moov(&track); @@ -611,6 +798,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![], }; assert_eq!(Fmp4Writer::timescale_for(&track), 90_000); @@ -629,6 +817,7 @@ mod tests { channels: Some(2), width: None, height: None, + is_default: false, extra_data: vec![], }; assert_eq!(Fmp4Writer::timescale_for(&track), 48_000); @@ -647,6 +836,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![], }; let packets = vec![MediaPacket { @@ -733,6 +923,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![], }; let moov = Fmp4Writer::build_moov(&track); @@ -755,6 +946,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![0x01, 0x64, 0x00, 0x28, 0xAB, 0xCD], // fake AVCDecoderConfigurationRecord }; let moov = Fmp4Writer::build_moov(&track); @@ -778,6 +970,7 @@ mod tests { channels: None, width: Some(3840), height: Some(2160), + is_default: false, extra_data: vec![0x01, 0x02, 0x20, 0x00, 0x00, 0x00], // fake HEVCDecoderConfigurationRecord }; let moov = Fmp4Writer::build_moov(&track); @@ -799,6 +992,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![], }; let moov = Fmp4Writer::build_moov(&track); @@ -820,6 +1014,7 @@ mod tests { channels: Some(2), width: None, height: None, + is_default: false, extra_data: audio_specific_config.clone(), }; let moov = Fmp4Writer::build_moov(&track); diff --git a/crates/vuio-core/src/media/remux/hls.rs b/crates/vuio-core/src/media/remux/hls.rs index 1aace286..7875a002 100644 --- a/crates/vuio-core/src/media/remux/hls.rs +++ b/crates/vuio-core/src/media/remux/hls.rs @@ -7,10 +7,12 @@ pub struct HlsGenerator; impl HlsGenerator { /// Generate HLS Master Playlist (`master.m3u8`) for an MKV file containing multi-audio tracks. /// - /// Only tracks this remuxer can actually pass through into fMP4 are offered: a video - /// track must be AVC/HEVC, and only AAC audio tracks are listed as selectable - /// renditions (E-AC-3/AC-3/DTS/TrueHD/etc. audio has no in-browser decoder, so - /// serving it would just be a silent/broken rendition). + /// Only tracks this build can actually produce are offered: a video track must be + /// AVC/HEVC, and an audio track must be either AAC (passed through) or one of the + /// three codecs the vendored decoders handle, re-encoded to AAC on the way out. A + /// build compiled without the matching decoder — or a codec nothing here decodes, + /// TrueHD being the one that turns up in real libraries — drops the rendition + /// rather than offering one that would arrive silent. pub fn build_master_playlist(_media_id: &str, tracks: &[TrackInfo]) -> String { let Some(video_track) = browser_video_track(tracks) else { // No browser-playable video track: a variant-less master playlist fails @@ -148,6 +150,7 @@ mod tests { channels: None, width: Some(1920), height: Some(1080), + is_default: false, extra_data: vec![0x01, 0x64, 0x00, 0x28], } } @@ -164,7 +167,14 @@ mod tests { channels: Some(if codec_kind == TrackCodec::Aac { 2 } else { 6 }), width: None, height: None, - extra_data: vec![0x11, 0x90], + is_default: false, + // Only an AAC source carries an AudioSpecificConfig; a decoded track's + // config comes from the encoder, and the writer must cope with neither. + extra_data: if codec_kind == TrackCodec::Aac { + vec![0x11, 0x90] + } else { + Vec::new() + }, } } @@ -185,13 +195,12 @@ mod tests { } #[test] - fn test_master_playlist_excludes_unsupported_audio_codecs() { - // Mirrors a real WEB-DL release: AVC video with only E-AC-3/AC-3 audio tracks — - // none of those tracks can be decoded in-browser, so none should be offered. + fn test_master_playlist_excludes_audio_this_build_cannot_produce() { + // TrueHD: identified, named, and decoded by nothing vendored. A rendition + // pointing at it would arrive silent, so it must not be listed — in any build. let tracks = vec![ video_track(TrackCodec::Avc), - audio_track(2, TrackCodec::Unsupported, "5.1 Atmos", "eng"), - audio_track(3, TrackCodec::Unsupported, "Stereo", "eng"), + audio_track(2, TrackCodec::Unsupported, "TrueHD Atmos", "eng"), ]; let master = HlsGenerator::build_master_playlist("test-id", &tracks); @@ -200,6 +209,43 @@ mod tests { assert!(master.contains("video/index.m3u8")); } + /// The contract that replaced "AAC only": a rendition is offered exactly when + /// this build can produce it. Compiled with the decoder, an AC-3 track becomes a + /// selectable rendition; compiled without, it disappears rather than being + /// advertised and then failing. + #[test] + fn test_master_playlist_offers_ac3_only_when_this_build_can_decode_it() { + let tracks = vec![ + video_track(TrackCodec::Avc), + audio_track(2, TrackCodec::Ac3, "5.1 English", "eng"), + ]; + let master = HlsGenerator::build_master_playlist("test-id", &tracks); + + if TrackCodec::Ac3.is_playable() { + assert!(master.contains("#EXT-X-MEDIA:TYPE=AUDIO")); + assert!(master.contains("NAME=\"5.1 English\"")); + assert!(master.contains("audio/0/index.m3u8")); + // Re-encoded, so the rendition really is AAC-LC whatever the source was. + assert!(master.contains("mp4a.40.2"), "{master}"); + } else { + assert!(!master.contains("#EXT-X-MEDIA:TYPE=AUDIO"), "{master}"); + } + } + + #[test] + fn test_master_playlist_offers_dts_only_when_this_build_can_decode_it() { + let tracks = vec![ + video_track(TrackCodec::Avc), + audio_track(2, TrackCodec::Dts, "DTS", "eng"), + ]; + let master = HlsGenerator::build_master_playlist("test-id", &tracks); + assert_eq!( + master.contains("#EXT-X-MEDIA:TYPE=AUDIO"), + TrackCodec::Dts.is_playable(), + "a DTS rendition must appear exactly when this build can decode DTS" + ); + } + #[test] fn test_master_playlist_no_supported_video_is_variant_less() { let tracks = vec![video_track(TrackCodec::Unsupported)]; diff --git a/crates/vuio-core/src/media/remux/mkv_demuxer.rs b/crates/vuio-core/src/media/remux/mkv_demuxer.rs index 046172ce..9d515fe6 100644 --- a/crates/vuio-core/src/media/remux/mkv_demuxer.rs +++ b/crates/vuio-core/src/media/remux/mkv_demuxer.rs @@ -11,21 +11,67 @@ pub enum TrackKind { Other, } -/// The subset of codecs this remuxer can pass through into browser-playable fMP4. +/// What one track is in, as far as this pipeline is concerned. /// -/// This is a passthrough remuxer, not a transcoder: everything here is real content -/// Symphonia can identify (E-AC-3/AC-3/DTS/TrueHD audio, VP9/AV1 video, ...), but with -/// no decoder/encoder in the pipeline those tracks are marked `Unsupported` and left out -/// of what gets offered to the browser rather than shipped as a broken stream. +/// Video is passthrough — AVC and HEVC go into fMP4 as the bytes they already +/// are — and so is AAC audio. AC-3, E-AC-3 and DTS are the three the vendored +/// decoders handle: named here rather than lumped into `Unsupported` even in a +/// build with no decoder compiled in, because "AC-3, which this build cannot +/// decode" is a diagnostic and "unsupported" is a shrug. Whether a *named* +/// codec can actually be produced is a separate question, asked through +/// [`TrackCodec::is_playable`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum TrackCodec { Avc, Hevc, Aac, + /// AC-3, "Dolby Digital" (ATSC A/52). + Ac3, + /// E-AC-3, "Dolby Digital Plus" (A/52 Annex E). + Eac3, + /// DTS Coherent Acoustics. + Dts, #[default] Unsupported, } +impl TrackCodec { + /// The decoder this track needs, if it needs one at all. + /// + /// `None` covers both ends: a codec that is already playable everywhere + /// (AAC), and one nothing here can do anything with. + #[cfg(feature = "transcode")] + pub fn transcode_codec(self) -> Option { + use crate::media::transcode::TranscodeCodec; + match self { + Self::Ac3 => Some(TranscodeCodec::Ac3), + Self::Eac3 => Some(TranscodeCodec::Eac3), + Self::Dts => Some(TranscodeCodec::Dts), + _ => None, + } + } + + /// Whether this build can put this track in front of a browser or a TV — + /// either by passing it through untouched, or by decoding it. + /// + /// The answer is a compile-time constant reached at runtime, so the + /// playlist writer and the segment handler agree without either carrying a + /// `#[cfg]`: a build without `transcode-dts` drops a DTS rendition rather + /// than offering one it would then fail to produce. + pub fn is_playable(self) -> bool { + match self { + Self::Avc | Self::Hevc | Self::Aac => true, + #[cfg(feature = "transcode")] + Self::Ac3 | Self::Eac3 | Self::Dts => self + .transcode_codec() + .is_some_and(|codec| codec.is_decodable() && cfg!(feature = "transcode-aac")), + #[cfg(not(feature = "transcode"))] + Self::Ac3 | Self::Eac3 | Self::Dts => false, + Self::Unsupported => false, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrackInfo { pub id: u32, @@ -38,6 +84,13 @@ pub struct TrackInfo { pub channels: Option, pub width: Option, pub height: Option, + /// Whether the container marks this the default track of its kind. + /// + /// Which of a film's audio tracks to carry, when it has several. Language is + /// deliberately not consulted: a wrong guess is worse than a predictable + /// one, and the fix if it turns out to matter is one resource per track. + #[serde(default)] + pub is_default: bool, /// Raw decoder config record for `codec_kind`: an AVC/HEVCDecoderConfigurationRecord /// for video (Matroska's CodecPrivate for these codecs already *is* this record), or /// the raw AudioSpecificConfig for AAC. Empty when `codec_kind` is `Unsupported`. @@ -80,7 +133,7 @@ pub fn browser_video_track(tracks: &[TrackInfo]) -> Option<&TrackInfo> { pub fn browser_audio_tracks(tracks: &[TrackInfo]) -> Vec<&TrackInfo> { tracks .iter() - .filter(|t| t.track_kind == TrackKind::Audio && t.codec_kind == TrackCodec::Aac) + .filter(|t| t.track_kind == TrackKind::Audio && t.codec_kind.is_playable()) .collect() } @@ -91,13 +144,15 @@ impl MkvDemuxer { /// info such as duration. #[cfg(feature = "casting")] pub fn inspect(path: &Path) -> Result { - use symphonia::core::codecs::audio::well_known::CODEC_ID_AAC; + use symphonia::core::codecs::audio::well_known::{ + CODEC_ID_AAC, CODEC_ID_AC3, CODEC_ID_DCA, CODEC_ID_EAC3, CODEC_ID_TRUEHD, + }; use symphonia::core::codecs::video::well_known::extra_data::{ VIDEO_EXTRA_DATA_ID_AVC_DECODER_CONFIG, VIDEO_EXTRA_DATA_ID_HEVC_DECODER_CONFIG, }; use symphonia::core::codecs::video::well_known::{CODEC_ID_H264, CODEC_ID_HEVC}; use symphonia::core::formats::probe::Hint; - use symphonia::core::formats::FormatOptions; + use symphonia::core::formats::{FormatOptions, TrackFlags}; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::units::Timestamp; @@ -171,14 +226,23 @@ impl MkvDemuxer { channels: None, width: v.width.map(u32::from), height: v.height.map(u32::from), + is_default: t.flags.contains(TrackFlags::DEFAULT), extra_data, }); } else if let Some(a) = params.audio() { - let (codec_kind, codec_name) = if a.codec == CODEC_ID_AAC { - (TrackCodec::Aac, "AAC") - } else { - (TrackCodec::Unsupported, "Audio") + let (codec_kind, codec_name) = match a.codec { + CODEC_ID_AAC => (TrackCodec::Aac, "AAC"), + CODEC_ID_AC3 => (TrackCodec::Ac3, "AC-3"), + CODEC_ID_EAC3 => (TrackCodec::Eac3, "E-AC-3"), + CODEC_ID_DCA => (TrackCodec::Dts, "DTS"), + CODEC_ID_TRUEHD => (TrackCodec::Unsupported, "TrueHD"), + _ => (TrackCodec::Unsupported, "Audio"), }; + // Only AAC carries a decoder config worth keeping. AC-3 and DTS + // frames describe themselves in their own headers, so their + // decoders need nothing from the container — and the AAC track + // re-encoded *from* one of them gets its `AudioSpecificConfig` + // from the encoder's shape, not from the source. let extra_data = if codec_kind == TrackCodec::Aac { a.extra_data.as_ref().map(|d| d.to_vec()).unwrap_or_default() } else { @@ -196,6 +260,7 @@ impl MkvDemuxer { channels: a.channels.clone().map(|c| c.count() as u8), width: None, height: None, + is_default: t.flags.contains(TrackFlags::DEFAULT), extra_data, }); } @@ -295,6 +360,9 @@ impl MkvDemuxer { let mut packets = Vec::new(); let mut accumulated_ticks: u64 = 0; + // Where the segment began on the presentation timeline, for the elapsed + // check below. + let mut first_pts: Option = None; loop { if packets.len() >= MAX_PACKETS_PER_SEGMENT { @@ -328,6 +396,18 @@ impl MkvDemuxer { if packets.is_empty() && !is_keyframe { continue; } + // Matroska stores no per-block duration: a `SimpleBlock` is a + // timestamp and a payload, and symphonia can only report a + // duration where the track declares `DefaultDuration` or the + // codec implies one. Accumulating durations alone therefore + // runs to the packet ceiling on any track that declares + // neither — which is a segment holding the whole film. The + // elapsed presentation time is the check that does not depend + // on the container being generous. + let elapsed = pts.saturating_sub(*first_pts.get_or_insert(pts)); + if elapsed >= target_ticks && !packets.is_empty() { + break; + } accumulated_ticks += dur; packets.push(MediaPacket { track_id: packet.track_id, @@ -385,7 +465,7 @@ impl MkvDemuxer { /// each frame's composition offset (negative for frames presented before their decode /// position, which `trun` version 1 stores as a signed value). #[cfg(feature = "casting")] -fn derive_decode_timestamps(packets: &mut [MediaPacket]) { +pub(crate) fn derive_decode_timestamps(packets: &mut [MediaPacket]) { let mut decode_times: Vec = packets.iter().map(|p| p.pts).collect(); decode_times.sort_unstable(); for (packet, dts) in packets.iter_mut().zip(decode_times) { @@ -398,7 +478,7 @@ fn derive_decode_timestamps(packets: &mut [MediaPacket]) { /// timescale). Uses 128-bit arithmetic since `ticks * numer * output_timescale` can /// exceed 64 bits for a multi-hour file's later timestamps. #[cfg(feature = "casting")] -fn rescale_ticks(ticks: i64, time_base: symphonia::core::units::TimeBase, output_timescale: u32) -> u64 { +pub(crate) fn rescale_ticks(ticks: i64, time_base: symphonia::core::units::TimeBase, output_timescale: u32) -> u64 { let ticks = i128::from(ticks.max(0)); let numer = i128::from(time_base.numer.get()); let denom = i128::from(time_base.denom.get()); @@ -413,7 +493,7 @@ fn rescale_ticks(ticks: i64, time_base: symphonia::core::units::TimeBase, output /// Assumes a 4-byte NAL length prefix, which is what Matroska muxers use in practice /// (the AVC/HEVCDecoderConfigurationRecord's `lengthSizeMinusOne` is almost always 3). #[cfg(feature = "casting")] -fn packet_is_keyframe(data: &[u8], codec: TrackCodec) -> bool { +pub(crate) fn packet_is_keyframe(data: &[u8], codec: TrackCodec) -> bool { match codec { TrackCodec::Avc => nal_units(data, 4).any(|nal| !nal.is_empty() && (nal[0] & 0x1F) == 5), TrackCodec::Hevc => { diff --git a/crates/vuio-core/src/media/scanner.rs b/crates/vuio-core/src/media/scanner.rs index 7a8d0a30..dd432276 100644 --- a/crates/vuio-core/src/media/scanner.rs +++ b/crates/vuio-core/src/media/scanner.rs @@ -736,6 +736,13 @@ impl MediaScanner { if media_file.mime_type.starts_with("audio/") { let _ = crate::platform::filesystem::extract_audio_metadata(&mut media_file).await; + } else if media_file.mime_type.starts_with("video/") { + // Films get a header probe too, for one field: the audio track's + // codec. Deciding "does this need a decoded alternative?" has to be + // a database read — a folder of 400 films would otherwise open 400 + // files to render one Browse response. Stream properties only: a + // video's titling stays with the filename. + let _ = crate::platform::filesystem::extract_stream_info(&mut media_file).await; } if matches!(ext.to_lowercase().as_str(), "m3u" | "m3u8" | "pls") || media_file.mime_type == "audio/radio" { diff --git a/crates/vuio-core/src/media/transcode/aac.rs b/crates/vuio-core/src/media/transcode/aac.rs index d9fedd06..fbb6d3cd 100644 --- a/crates/vuio-core/src/media/transcode/aac.rs +++ b/crates/vuio-core/src/media/transcode/aac.rs @@ -93,6 +93,92 @@ impl AacEncoder { } } +/// ISO/IEC 14496-3 Table 1.18 sampling frequency indices. +const SAMPLING_FREQUENCIES: [u32; 13] = [ + 96_000, 88_200, 64_000, 48_000, 44_100, 32_000, 24_000, 22_050, 16_000, 12_000, 11_025, 8_000, + 7_350, +]; + +/// The raw `AudioSpecificConfig` for AAC-LC at this shape. +/// +/// An ADTS stream needs none of this — every frame carries its own header — but +/// an MP4 does: the `esds` box in the init segment is where a player learns the +/// stream's rate and channel layout, and it is written *before* a single frame +/// has been encoded. So it is derived from the shape the encoder was configured +/// with rather than read back out of its output. `asc_matches_the_encoders_own_adts_header` +/// is what keeps the two from drifting. +/// +/// A rate outside Table 1.18 uses the escape index (15) and an explicit 24-bit +/// rate, which makes the config four bytes instead of two. +pub fn audio_specific_config(sample_rate: u32, channels: u16) -> Vec { + const AAC_LC: u32 = 2; + let channel_configuration = u32::from(channels).min(7); + + let mut bits: Vec<(u32, u32)> = vec![(AAC_LC, 5)]; + match SAMPLING_FREQUENCIES.iter().position(|r| *r == sample_rate) { + Some(index) => bits.push((index as u32, 4)), + None => { + bits.push((0x0F, 4)); + bits.push((sample_rate, 24)); + } + } + bits.push((channel_configuration, 4)); + // GASpecificConfig: frameLengthFlag = 0 (1024 samples), dependsOnCoreCoder + // = 0, extensionFlag = 0. + bits.push((0, 3)); + + let mut out = Vec::new(); + let mut acc: u32 = 0; + let mut used = 0u32; + for (value, width) in bits { + for i in (0..width).rev() { + acc = (acc << 1) | ((value >> i) & 1); + used += 1; + if used == 8 { + out.push(acc as u8); + acc = 0; + used = 0; + } + } + } + if used > 0 { + out.push((acc << (8 - used)) as u8); + } + out +} + +/// The payloads of an ADTS stream, with each frame's header removed. +/// +/// ADTS framing is what makes the encoder's output self-describing over a bare +/// socket, and exactly what an MP4 sample must not contain: the `stsd` entry +/// already says everything the header repeats, and a decoder handed the header +/// as sample data reads it as spectral coefficients. +pub fn adts_payloads(stream: &[u8]) -> Vec<&[u8]> { + let mut frames = Vec::new(); + let mut pos = 0usize; + while pos + 7 <= stream.len() { + let header = &stream[pos..]; + if header[0] != 0xFF || (header[1] & 0xF0) != 0xF0 { + // Not a syncword. The encoder does not produce these, so rather than + // resynchronising, stop: a stream that has gone wrong here would + // produce garbage samples, not merely a lost frame. + break; + } + let frame_len = ((u32::from(header[3]) & 0x03) << 11) + | (u32::from(header[4]) << 3) + | (u32::from(header[5]) >> 5); + let frame_len = frame_len as usize; + // `protection_absent == 0` adds a two-byte CRC after the fixed header. + let header_len = if header[1] & 0x01 == 0 { 9 } else { 7 }; + if frame_len < header_len || pos + frame_len > stream.len() { + break; + } + frames.push(&stream[pos + header_len..pos + frame_len]); + pos += frame_len; + } + frames +} + #[cfg(test)] mod tests { use super::*; @@ -148,4 +234,59 @@ mod tests { // Seven channels has no Table 1.19 default configuration. assert!(AacEncoder::new(48_000, 7).is_err()); } + + /// The init segment's `esds` and the segments' samples come from different + /// requests and different code paths, and a player that disagrees with one + /// of them produces noise rather than an error. This is the check that they + /// describe the same stream: the config written into the container has to + /// match what the encoder itself declares in every ADTS header it emits. + #[test] + fn asc_matches_the_encoders_own_adts_header() { + for (rate, channels) in [(48_000u32, 2u16), (44_100, 2), (32_000, 1)] { + let mut enc = AacEncoder::new(rate, channels).unwrap(); + let mut out = enc.push(&sine(1, rate)).unwrap(); + out.extend_from_slice(&enc.finish()); + + let profile = (out[2] >> 6) & 0x03; + let frequency_index = (out[2] >> 2) & 0x0F; + let channel_configuration = ((out[2] & 0x01) << 2) | (out[3] >> 6); + + let asc = audio_specific_config(rate, channels); + assert_eq!( + asc[0] >> 3, + profile + 1, + "audioObjectType at {rate} Hz: ASC vs ADTS profile" + ); + assert_eq!( + ((asc[0] & 0x07) << 1) | (asc[1] >> 7), + frequency_index, + "samplingFrequencyIndex at {rate} Hz" + ); + assert_eq!( + (asc[1] >> 3) & 0x0F, + channel_configuration, + "channelConfiguration at {channels} channels" + ); + } + } + + #[test] + fn adts_framing_is_stripped_leaving_the_payloads() { + let mut enc = AacEncoder::new(48_000, 2).unwrap(); + let mut stream = enc.push(&sine(1, 48_000)).unwrap(); + stream.extend_from_slice(&enc.finish()); + + let payloads = adts_payloads(&stream); + assert!(payloads.len() > 40, "a second is ~47 frames of 1024 samples"); + let total: usize = payloads.iter().map(|f| f.len()).sum(); + assert_eq!( + total + payloads.len() * 7, + stream.len(), + "every byte is either a seven-byte header or payload" + ); + // Nothing may start with a syncword any more — that is the header we removed. + for payload in payloads { + assert!(!(payload[0] == 0xFF && payload[1] & 0xF0 == 0xF0)); + } + } } diff --git a/crates/vuio-core/src/media/transcode/frames.rs b/crates/vuio-core/src/media/transcode/frames.rs index b9699f2f..c81b80e1 100644 --- a/crates/vuio-core/src/media/transcode/frames.rs +++ b/crates/vuio-core/src/media/transcode/frames.rs @@ -219,6 +219,16 @@ fn parse_dts(_data: &[u8]) -> Result> { bail!("this build of vuio-core was compiled without the `transcode-dts` feature") } +/// Sample frames one compressed frame decodes to, read from its own header. +/// +/// The container path's equivalent of [`IndexedFrame::samples`]: symphonia hands +/// over a packet, and this is how its decoded length is known before it is +/// decoded — which is what lets a frame that fails to decode be replaced by +/// silence of exactly the right length instead of shortening the track. +pub(crate) fn frame_samples(codec: TranscodeCodec, data: &[u8]) -> Option { + parse_header(codec, data).ok().flatten().map(|h| h.samples) +} + fn parse_header(codec: TranscodeCodec, data: &[u8]) -> Result> { match codec { TranscodeCodec::Ac3 | TranscodeCodec::Eac3 => parse_ac3_family(data), diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index c33fe8f3..1e2f7339 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -18,11 +18,16 @@ mod aac; mod frames; mod pcm; mod plan; +#[cfg(all(feature = "transcode-aac", feature = "demux"))] +mod rendition; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] +mod video; mod session; +mod source; mod wav; #[cfg(feature = "transcode-aac")] -pub use aac::AacEncoder; +pub use aac::{adts_payloads, audio_specific_config, AacEncoder}; // These are the vocabulary of this module's public surface — the element type // of `FrameIndex::frames`, the result of `AudioPlan::seek`, the header length a // caller subtracts from an offset. Nothing inside the crate spells some of them @@ -33,7 +38,14 @@ pub use frames::{FrameIndex, IndexedFrame}; pub use pcm::PcmDecoder; #[allow(unused_imports)] pub use plan::{AudioPlan, Seeked}; -pub use session::{IndexKey, TranscodeState}; +#[cfg(all(feature = "transcode-aac", feature = "demux"))] +#[allow(unused_imports)] +pub use rendition::{fit_channels, reencode_to_aac, AAC_FRAME_SAMPLES}; +pub use session::{IndexKey, SegmentKey, TranscodeState}; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] +pub use video::ProgressiveStream; +#[allow(unused_imports)] +pub use source::{PacketSource, PcmStream}; #[allow(unused_imports)] pub use wav::{wav_header, WAV_HEADER_LEN}; diff --git a/crates/vuio-core/src/media/transcode/pcm.rs b/crates/vuio-core/src/media/transcode/pcm.rs index 943aa9f0..f1891d76 100644 --- a/crates/vuio-core/src/media/transcode/pcm.rs +++ b/crates/vuio-core/src/media/transcode/pcm.rs @@ -77,19 +77,26 @@ impl PcmDecoder { /// Decode one compressed frame into interleaved S16. /// - /// A frame that fails to decode yields silence of the length the index said - /// it would occupy — the caller has already committed to a `Content-Length` - /// built from that index, so a mid-stream error must not change how many - /// bytes the response carries. One corrupt frame in a film is a tick; a - /// short body is a truncated download. - pub fn decode_or_silence(&mut self, frame: &[u8], expect_samples: u32) -> Vec { - let want = expect_samples as usize * self.channels as usize * BYTES_PER_SAMPLE; + /// `expect_samples` is what the frame's header said it would decode to. A + /// frame that fails yields silence of exactly that length — the caller has + /// already committed to a `Content-Length` built from the same headers, so a + /// mid-stream error must not change how many bytes the response carries. One + /// corrupt frame in a film is a tick; a short body is a truncated download. + /// + /// `None` means the header would not parse, which leaves nothing to pad to: + /// the decoder's own output is taken as it comes, and a failure costs the + /// frame rather than substituting for it. + pub fn decode_or_silence(&mut self, frame: &[u8], expect_samples: Option) -> Vec { + let want = expect_samples + .map(|samples| samples as usize * self.channels as usize * BYTES_PER_SAMPLE); match self.decode_measured(frame) { Ok((mut pcm, _)) => { - pcm.resize(want, 0); + if let Some(want) = want { + pcm.resize(want, 0); + } pcm } - Err(_) => vec![0u8; want], + Err(_) => vec![0u8; want.unwrap_or(0)], } } @@ -144,7 +151,7 @@ mod tests { ); for f in &idx.frames[1..] { let raw = &bytes[f.offset as usize..][..f.len as usize]; - pcm.extend_from_slice(&dec.decode_or_silence(raw, f.samples)); + pcm.extend_from_slice(&dec.decode_or_silence(raw, Some(f.samples))); } (pcm, idx, channels) } @@ -217,7 +224,7 @@ mod tests { let (mut dec, _) = PcmDecoder::open(TranscodeCodec::Ac3, idx.sample_rate, Some(2), first).unwrap(); let garbage = vec![0u8; 768]; - let out = dec.decode_or_silence(&garbage, 1536); + let out = dec.decode_or_silence(&garbage, Some(1536)); assert_eq!(out.len(), 1536 * 2 * 2); assert!(out.iter().all(|&b| b == 0)); } diff --git a/crates/vuio-core/src/media/transcode/plan.rs b/crates/vuio-core/src/media/transcode/plan.rs index b87a4c20..04f1268b 100644 --- a/crates/vuio-core/src/media/transcode/plan.rs +++ b/crates/vuio-core/src/media/transcode/plan.rs @@ -6,20 +6,21 @@ //! channel count — is settled up front, here, and the streaming half only ever //! fills in a length that was already promised. //! -//! Building a plan costs one pass over the file's headers plus one decoded -//! frame. That is why plans are cached (see [`super::session`]): a renderer's -//! `HEAD`, `GET` and range requests for one file should pay it once. +//! Building a plan costs one pass over an elementary file's headers, or one +//! container probe, plus one decoded frame. That is why plans are cached (see +//! [`super::session`]): a renderer's `HEAD`, `GET` and range requests for one +//! file should pay it once. use anyhow::{Context, Result}; use std::fs::File; use std::io::{BufReader, Read, Seek, SeekFrom}; use std::path::Path; +use super::source::PacketSource; use super::wav::{pcm_size, wav_size, WAV_HEADER_LEN}; use super::{FrameIndex, PcmDecoder, TranscodeCodec}; -/// The decoded shape of one file, and the frame table to produce it. -#[derive(Debug)] +/// The decoded shape of one file, and how to reach the frames that produce it. pub struct AudioPlan { /// The file this plan describes. /// @@ -29,12 +30,24 @@ pub struct AudioPlan { pub source_path: std::path::PathBuf, /// Codec of the source. pub codec: TranscodeCodec, - /// Where every frame is and how long it decodes to. - pub index: FrameIndex, + /// Where the compressed frames come from, and what they add up to. + pub source: PacketSource, /// Channels the decoder emits — measured, not predicted. pub channels: u16, } +impl std::fmt::Debug for AudioPlan { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AudioPlan") + .field("source_path", &self.source_path) + .field("codec", &self.codec) + .field("sample_rate", &self.sample_rate()) + .field("channels", &self.channels) + .field("total_samples", &self.total_samples()) + .finish() + } +} + /// Channels to ask the decoder for. /// /// Stereo, always. A renderer that cannot decode AC-3 is not a renderer with a @@ -42,14 +55,14 @@ pub struct AudioPlan { /// gets the §7.8 downmix the encoder authored rather than one we invent. A /// source already at or below stereo is unaffected — the decoder reports what it /// actually emitted and [`AudioPlan::channels`] records that. -const TARGET_CHANNELS: u16 = 2; +pub(crate) const TARGET_CHANNELS: u16 = 2; impl AudioPlan { - /// Index `path` and probe its decoded shape. + /// Index a raw `.ac3`/`.eac3`/`.dts` file and probe its decoded shape. /// /// Blocking: it reads the whole file's headers and decodes one frame, so /// callers on an async task must wrap it in `spawn_blocking`. - pub fn build(path: &Path, codec: TranscodeCodec) -> Result { + pub fn elementary(path: &Path, codec: TranscodeCodec) -> Result { let file = File::open(path) .with_context(|| format!("opening {} for transcoding", path.display()))?; let mut reader = BufReader::with_capacity(256 * 1024, file); @@ -64,43 +77,83 @@ impl AudioPlan { file.read_exact(&mut buf) .context("reading the first frame")?; - let (decoder, _) = PcmDecoder::open( + let (decoder, _) = PcmDecoder::open(codec, index.sample_rate, Some(TARGET_CHANNELS), &buf)?; + + Ok(Self { + source_path: path.to_path_buf(), codec, - index.sample_rate, - Some(TARGET_CHANNELS), - &buf, - )?; + channels: decoder.channels(), + source: PacketSource::Elementary(index), + }) + } + + /// Probe the audio track of a container — a film — and its decoded shape. + /// + /// Blocking, and much cheaper than the elementary path: the container's own + /// track declarations answer everything the header walk had to be run to + /// find out, so this reads the file's front matter and one packet. + #[cfg(feature = "demux")] + pub fn container(path: &Path) -> Result { + let (mut format, audio, codec) = super::source::probe_container_audio(path)?; + let (first, _) = super::source::next_track_packet(format.as_mut(), audio.track_id)? + .ok_or_else(|| anyhow::anyhow!("{} has no audio packets", path.display()))?; + let (decoder, _) = + PcmDecoder::open(codec, audio.sample_rate, Some(TARGET_CHANNELS), &first)?; Ok(Self { source_path: path.to_path_buf(), codec, channels: decoder.channels(), - index, + source: PacketSource::Container(audio), }) } + /// Total decoded sample frames, when they can be known before decoding. + pub fn total_samples(&self) -> Option { + self.source.total_samples() + } + /// Total size of the WAV resource, header included. - pub fn wav_size(&self) -> u64 { - wav_size(self.index.total_samples, self.channels) + /// + /// `None` when the source will not say how long it is. The resource then has + /// no `Content-Length` and no seeking — an honest loss, where a guessed + /// length would be a truncated download. + pub fn wav_size(&self) -> Option { + self.total_samples() + .map(|samples| wav_size(samples, self.channels)) } /// Sample rate of the decoded output, in Hz. pub fn sample_rate(&self) -> u32 { - self.index.sample_rate + self.source.sample_rate() + } + + /// Duration in seconds, when the length is known. + pub fn duration_secs(&self) -> Option { + let rate = self.sample_rate(); + self.total_samples() + .filter(|_| rate > 0) + .map(|samples| samples as f64 / f64::from(rate)) } /// The WAV header describing this resource. + /// + /// A source of unknown length is described with the largest payload RIFF can + /// express, which is what a player shows as "unknown" rather than as zero. pub fn wav_header(&self) -> [u8; 44] { - super::wav_header(self.sample_rate(), self.channels, self.index.total_samples) + super::wav_header( + self.sample_rate(), + self.channels, + self.total_samples().unwrap_or(u64::MAX / 8), + ) } /// Bytes each decoded sample frame occupies. - fn stride(&self) -> u64 { + pub fn stride(&self) -> u64 { self.channels as u64 * 2 } - /// Turn a byte offset into the resource into the frame to start decoding at, - /// and how many decoded bytes to drop from that frame's output. + /// Turn a byte offset into the resource into where decoding starts. /// /// Offsets inside the 44-byte header resolve to the very start, because a /// range that begins mid-header still has to be served the rest of it. @@ -108,24 +161,21 @@ impl AudioPlan { if byte_offset < WAV_HEADER_LEN { return Seeked { header_skip: byte_offset as usize, - frame: 0, - pcm_skip: 0, + start_sample: 0, + byte_skip: 0, }; } let pcm_offset = byte_offset - WAV_HEADER_LEN; - let sample = pcm_offset / self.stride(); - let within = (pcm_offset % self.stride()) as usize; - let (frame, samples_into_frame) = self.index.locate(sample); Seeked { header_skip: WAV_HEADER_LEN as usize, - frame, - pcm_skip: samples_into_frame as usize * self.stride() as usize + within, + start_sample: pcm_offset / self.stride(), + byte_skip: (pcm_offset % self.stride()) as usize, } } - /// Decoded bytes produced by frame `i`. - pub fn frame_bytes(&self, i: usize) -> usize { - pcm_size(u64::from(self.index.frames[i].samples), self.channels) as usize + /// Decoded bytes `samples` sample frames occupy at this plan's channel count. + pub fn pcm_bytes(&self, samples: u64) -> u64 { + pcm_size(samples, self.channels) } } @@ -134,10 +184,10 @@ impl AudioPlan { pub struct Seeked { /// Bytes of the WAV header already passed — 44 once past it entirely. pub header_skip: usize, - /// Index of the frame to begin decoding at. - pub frame: usize, - /// Decoded bytes to discard from that frame's output. - pub pcm_skip: usize, + /// The decoded sample frame output begins at. + pub start_sample: u64, + /// Bytes to drop from that sample frame, for a range beginning mid-sample. + pub byte_skip: usize, } #[cfg(test)] @@ -153,7 +203,10 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("sine.ac3"); std::fs::write(&path, AC3_FIXTURE).unwrap(); - (AudioPlan::build(&path, TranscodeCodec::Ac3).unwrap(), dir) + ( + AudioPlan::elementary(&path, TranscodeCodec::Ac3).unwrap(), + dir, + ) } #[cfg(feature = "transcode-ac3")] @@ -164,20 +217,20 @@ mod tests { assert_eq!(plan.sample_rate(), 48_000); assert_eq!( plan.wav_size(), - WAV_HEADER_LEN + plan.index.total_samples * 2 * 2 + Some(WAV_HEADER_LEN + plan.total_samples().unwrap() * 2 * 2) ); } #[cfg(feature = "transcode-ac3")] #[test] - fn offset_zero_starts_at_the_header_and_the_first_frame() { + fn offset_zero_starts_at_the_header_and_the_first_sample() { let (plan, _dir) = fixture_plan(); assert_eq!( plan.seek(0), Seeked { header_skip: 0, - frame: 0, - pcm_skip: 0 + start_sample: 0, + byte_skip: 0 } ); } @@ -188,32 +241,36 @@ mod tests { let (plan, _dir) = fixture_plan(); let s = plan.seek(20); assert_eq!(s.header_skip, 20); - assert_eq!(s.frame, 0); - assert_eq!(s.pcm_skip, 0); + assert_eq!(s.start_sample, 0); + assert_eq!(s.byte_skip, 0); } #[cfg(feature = "transcode-ac3")] #[test] - fn a_range_past_the_header_lands_on_the_right_frame_and_sample() { + fn a_range_past_the_header_lands_on_the_right_sample() { let (plan, _dir) = fixture_plan(); // One whole AC-3 frame of decoded stereo is 1536 * 2 * 2 bytes. let one_frame = 1536 * 2 * 2; let s = plan.seek(WAV_HEADER_LEN + one_frame); assert_eq!(s.header_skip, WAV_HEADER_LEN as usize, "header is done"); - assert_eq!(s.frame, 1, "exactly the second frame"); - assert_eq!(s.pcm_skip, 0); + assert_eq!(s.start_sample, 1536, "exactly the second frame's first sample"); + assert_eq!(s.byte_skip, 0); - // Half a frame in: same frame, half its output discarded. - let s = plan.seek(WAV_HEADER_LEN + one_frame + one_frame / 2); - assert_eq!(s.frame, 1); - assert_eq!(s.pcm_skip, (one_frame / 2) as usize); + // Half a frame in, and one byte past a sample boundary: the sample is + // rounded down and the odd bytes are dropped from its front. + let s = plan.seek(WAV_HEADER_LEN + one_frame + one_frame / 2 + 1); + assert_eq!(s.start_sample, 1536 + 1536 / 2); + assert_eq!(s.byte_skip, 1); } #[cfg(feature = "transcode-ac3")] #[test] - fn every_frame_reports_the_byte_count_its_sample_count_implies() { + fn the_declared_size_is_the_payload_the_samples_imply() { let (plan, _dir) = fixture_plan(); - let total: usize = (0..plan.index.frames.len()).map(|i| plan.frame_bytes(i)).sum(); - assert_eq!(total as u64 + WAV_HEADER_LEN, plan.wav_size()); + let samples = plan.total_samples().unwrap(); + assert_eq!( + plan.wav_size().unwrap(), + WAV_HEADER_LEN + plan.pcm_bytes(samples) + ); } } diff --git a/crates/vuio-core/src/media/transcode/rendition.rs b/crates/vuio-core/src/media/transcode/rendition.rs new file mode 100644 index 00000000..d2c8a187 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/rendition.rs @@ -0,0 +1,154 @@ +//! Restating a film's undecodable audio track as an AAC one. +//! +//! Both delivery paths need the same thing and must do it identically: the +//! browser's HLS renditions and the television's progressive `video.mp4` each +//! carry the film's picture untouched beside an audio track that had to be +//! decoded and re-encoded to exist at all. What differs between them is +//! framing and headers; what happens to the samples is here. +//! +//! The one number worth understanding is the encoder's delay. Its MDCT window +//! spans the previous hop and the current one, so the frame emitted for input +//! samples `[n, n+1024)` is only fully reconstructed once the *next* frame has +//! been overlap-added — a decoder's output therefore trails its input by +//! exactly one frame. Placing the run 1024 samples earlier on the decode +//! timeline cancels that, and is the difference between lip-sync and a +//! twenty-one millisecond lag. + +use anyhow::Result; + +use super::{AacEncoder, PcmDecoder, TranscodeCodec}; +use crate::media::remux::MediaPacket; + +/// Samples per AAC-LC frame, and therefore the encoder's delay. +pub const AAC_FRAME_SAMPLES: u64 = 1024; + +/// Bytes per sample per channel in the decoder's output. +const BYTES_PER_SAMPLE: usize = 2; + +/// Decode `packets` and re-encode them as AAC, as samples for an MP4 track. +/// +/// `packets` must be one track's packets in order, with timestamps already in +/// the output timescale — which for an audio track is its sample rate, so a +/// timestamp *is* a sample index. `channels` is what the output track declares, +/// and what the samples are made to match: a mono source asked to be stereo is +/// widened here rather than being allowed to contradict the `esds` box that has +/// already gone out in the init segment. +/// +/// The returned packets carry `pts == dts` and a duration of one AAC frame. +/// Audio has no reordering, so there is nothing for a composition offset to +/// express. +pub fn reencode_to_aac( + codec: TranscodeCodec, + packets: &[MediaPacket], + sample_rate: u32, + channels: u16, + track_id: u32, +) -> Result> { + let Some(first) = packets.first() else { + return Ok(Vec::new()); + }; + + let (mut decoder, primed) = + PcmDecoder::open(codec, sample_rate, Some(channels), &first.data)?; + let decoded_channels = decoder.channels(); + let mut encoder = AacEncoder::new(sample_rate, channels)?; + + let mut adts = encoder.push(&fit_channels(&primed, decoded_channels, channels))?; + for packet in &packets[1..] { + let expect = super::frames::frame_samples(codec, &packet.data); + let pcm = decoder.decode_or_silence(&packet.data, expect); + adts.extend_from_slice(&encoder.push(&fit_channels(&pcm, decoded_channels, channels))?); + } + adts.extend_from_slice(&encoder.finish()); + + // One frame early, to cancel the encoder's delay. Clamped at zero for a run + // that already starts at the beginning of the film, where there is no + // earlier timeline to move onto — the residual lag there is one frame, + // twenty-one milliseconds at 48 kHz. + let mut dts = first.pts.saturating_sub(AAC_FRAME_SAMPLES); + let mut out = Vec::new(); + for payload in super::adts_payloads(&adts) { + out.push(MediaPacket { + track_id, + pts: dts, + dts, + duration: AAC_FRAME_SAMPLES, + // Every AAC frame is independently decodable after the previous + // frame's overlap, so every one is a random-access point. + is_keyframe: true, + data: payload.to_vec(), + }); + dts += AAC_FRAME_SAMPLES; + } + Ok(out) +} + +/// Fit interleaved S16 with `have` channels into `want` channels. +/// +/// The decoder is asked for the channel count the output declares and normally +/// obliges — AC-3 carries the §7.8 downmix coefficients, so its own two-channel +/// output is the mix the encoder authored. A source already narrower than the +/// request (mono AC-3 does exist) comes back narrower, and is widened here by +/// duplication so that what reaches the encoder always matches what the `esds` +/// box promised. +pub fn fit_channels(pcm: &[u8], have: u16, want: u16) -> Vec { + if have == want || have == 0 || want == 0 { + return pcm.to_vec(); + } + let have = have as usize; + let want = want as usize; + let frames = pcm.len() / (have * BYTES_PER_SAMPLE); + let mut out = Vec::with_capacity(frames * want * BYTES_PER_SAMPLE); + for frame in 0..frames { + let base = frame * have * BYTES_PER_SAMPLE; + for channel in 0..want { + // Widening repeats the last channel there is; narrowing takes the + // leading ones, which for interleaved audio is the front pair. + let source = channel.min(have - 1); + let at = base + source * BYTES_PER_SAMPLE; + out.extend_from_slice(&pcm[at..at + BYTES_PER_SAMPLE]); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mono_is_widened_by_duplication_and_the_frame_count_is_kept() { + let mono: Vec = [1i16, 2, 3] + .iter() + .flat_map(|v| v.to_le_bytes()) + .collect(); + let stereo = fit_channels(&mono, 1, 2); + assert_eq!(stereo.len(), mono.len() * 2); + let values: Vec = stereo + .as_chunks::<2>() + .0 + .iter() + .map(|c| i16::from_le_bytes(*c)) + .collect(); + assert_eq!(values, vec![1, 1, 2, 2, 3, 3]); + } + + #[test] + fn a_matching_channel_count_is_passed_through_untouched() { + let pcm = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; + assert_eq!(fit_channels(&pcm, 2, 2), pcm); + } + + #[test] + fn narrowing_keeps_the_leading_channels() { + let five_one: Vec = (1i16..=6).flat_map(|v| v.to_le_bytes()).collect(); + let stereo = fit_channels(&five_one, 6, 2); + let values: Vec = stereo + .as_chunks::<2>() + .0 + .iter() + .map(|c| i16::from_le_bytes(*c)) + .collect(); + assert_eq!(values, vec![1, 2]); + } +} diff --git a/crates/vuio-core/src/media/transcode/session.rs b/crates/vuio-core/src/media/transcode/session.rs index 1d353cae..5c0d6208 100644 --- a/crates/vuio-core/src/media/transcode/session.rs +++ b/crates/vuio-core/src/media/transcode/session.rs @@ -33,13 +33,45 @@ pub struct IndexKey { pub modified: i64, } +/// Identifies a cached fMP4 segment. +/// +/// Segments are cached where the elementary index is not, and for a different +/// reason: a copy is cheap to redo, but a decode-and-re-encode is not, and +/// seeking or re-buffering asks for the same segment again and again. Keyed on +/// the track as well as the file because a film's renditions are built +/// independently and a browser may be pulling two of them at once. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SegmentKey { + /// Database id of the file. + pub id: i64, + /// Container track id. + pub track: u32, + /// Segment index within the rendition. + pub seq: u32, +} + +/// How many segments to keep, and how much memory they may occupy between them. +/// +/// A segment of 1080p video is single-digit megabytes, so a count alone would +/// bound the wrong thing on a large file and the wrong thing on a small one. +const MAX_CACHED_SEGMENTS: usize = 24; +const MAX_CACHED_SEGMENT_BYTES: usize = 48 * 1024 * 1024; + /// Shared transcoding state, held by `AppState`. #[derive(Debug)] pub struct TranscodeState { cache: Mutex, + segments: Mutex, permits: Arc, } +#[derive(Debug, Default)] +struct SegmentCache { + entries: HashMap, + order: Vec, + bytes: usize, +} + #[derive(Debug, Default)] struct Cache { entries: HashMap>, @@ -64,6 +96,7 @@ impl TranscodeState { pub fn new(max_concurrent: usize) -> Self { Self { cache: Mutex::new(Cache::default()), + segments: Mutex::new(SegmentCache::default()), permits: Arc::new(Semaphore::new(max_concurrent.max(1))), } } @@ -93,6 +126,30 @@ impl TranscodeState { } } } + + /// The bytes of segment `key`, if it was built recently. + pub async fn cached_segment(&self, key: &SegmentKey) -> Option { + self.segments.lock().await.entries.get(key).cloned() + } + + /// Remember a built segment, evicting oldest-first past either ceiling. + pub async fn remember_segment(&self, key: SegmentKey, segment: bytes::Bytes) { + let mut cache = self.segments.lock().await; + let len = segment.len(); + if cache.entries.insert(key, segment).is_none() { + cache.order.push(key); + cache.bytes += len; + } + while cache.order.len() > MAX_CACHED_SEGMENTS || cache.bytes > MAX_CACHED_SEGMENT_BYTES { + let Some(oldest) = cache.order.first().copied() else { + break; + }; + cache.order.remove(0); + if let Some(evicted) = cache.entries.remove(&oldest) { + cache.bytes = cache.bytes.saturating_sub(evicted.len()); + } + } + } } #[cfg(test)] @@ -105,12 +162,14 @@ mod tests { source_path: std::path::PathBuf::from("/dev/null"), codec: TranscodeCodec::Ac3, channels: 2, - index: crate::media::transcode::FrameIndex { - codec: TranscodeCodec::Ac3, - sample_rate: 48_000, - frames: Vec::new(), - total_samples: 0, - }, + source: crate::media::transcode::PacketSource::Elementary( + crate::media::transcode::FrameIndex { + codec: TranscodeCodec::Ac3, + sample_rate: 48_000, + frames: Vec::new(), + total_samples: 0, + }, + ), }) } @@ -162,4 +221,45 @@ mod tests { let state = TranscodeState::new(0); assert!(state.try_acquire().is_some()); } + + #[tokio::test] + async fn segments_are_evicted_once_they_outgrow_their_memory_ceiling() { + let state = TranscodeState::new(1); + let key = |seq| SegmentKey { + id: 1, + track: 2, + seq, + }; + // Four segments of 16 MB: the fourth must push the first out, because + // the byte ceiling binds long before the entry count does. + for seq in 0..4 { + state + .remember_segment(key(seq), bytes::Bytes::from(vec![0u8; 16 * 1024 * 1024])) + .await; + } + assert!(state.cached_segment(&key(0)).await.is_none()); + assert!(state.cached_segment(&key(3)).await.is_some()); + } + + #[tokio::test] + async fn a_segment_is_found_again_under_the_key_that_stored_it() { + let state = TranscodeState::new(1); + let key = SegmentKey { + id: 7, + track: 2, + seq: 3, + }; + state + .remember_segment(key, bytes::Bytes::from_static(b"segment")) + .await; + assert_eq!( + state.cached_segment(&key).await.as_deref(), + Some(&b"segment"[..]) + ); + // A different rendition of the same file is a different segment. + assert!(state + .cached_segment(&SegmentKey { track: 3, ..key }) + .await + .is_none()); + } } diff --git a/crates/vuio-core/src/media/transcode/source.rs b/crates/vuio-core/src/media/transcode/source.rs new file mode 100644 index 00000000..71f4fd94 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/source.rs @@ -0,0 +1,507 @@ +//! Where compressed audio frames come from. +//! +//! Phases 1-3 had one answer: a raw `.ac3`/`.dts` file, framed by walking sync +//! words. That is the rarer shape of the problem. The common one is a film — +//! `Movie.mkv` with an AC-3 or DTS track — where the frames are inside a +//! container and symphonia is what gets them out. +//! +//! Both answers have to satisfy the same contract, because the resource built on +//! top of them does: the decoder must be openable at an arbitrary point in the +//! stream, and it must produce, from there on, the same samples a decode from +//! the beginning would have produced. [`PcmStream`] is that contract — open at a +//! sample, then pull blocks — and the two variants below are the two ways of +//! honouring it. + +use anyhow::{bail, Context, Result}; +use std::path::Path; + +use super::{FrameIndex, PcmDecoder, TranscodeCodec}; + +/// Seconds of audio decoded and thrown away before a seek point. +/// +/// AC-3 and DTS frames overlap by half a window, so the sample sitting exactly +/// at a seek point is reconstructed partly from state the previous frames +/// carried. Decoding a little run-up and discarding it removes the transient +/// that would otherwise tick at the start of every seek. A quarter of a second +/// is a handful of frames — far more overlap than any of these codecs carries, +/// and still nothing next to the seek it follows. +#[cfg(feature = "demux")] +const PREROLL_SECS: f64 = 0.25; + +/// How a file's audio frames are reached. +pub enum PacketSource { + /// A raw `.ac3`/`.eac3`/`.dts` file, framed by walking sync words. + /// + /// Every frame's offset, length and decoded sample count is known before a + /// byte is decoded, which is what makes the resource exactly as long as its + /// `Content-Length` claims and seekable to the sample. + Elementary(FrameIndex), + /// One audio track inside a container symphonia can demux. + #[cfg(feature = "demux")] + Container(ContainerAudio), +} + +/// What one container audio track declares about itself. +#[cfg(feature = "demux")] +pub struct ContainerAudio { + /// The track to demux, as symphonia numbers them. + pub track_id: u32, + /// Sample rate declared by the track, in Hz. + pub sample_rate: u32, + /// Total decoded sample frames, when the container says enough to know. + /// + /// `None` is not a failure — it means the resource degrades to a chunked + /// body with no `Content-Length` and no seeking, which loses a scrub bar. + /// Guessing instead would lose the download: a `Content-Length` a renderer + /// cannot be given is a truncated transfer. + pub total_samples: Option, +} + +impl PacketSource { + /// Sample rate of the source, in Hz. + pub fn sample_rate(&self) -> u32 { + match self { + Self::Elementary(index) => index.sample_rate, + #[cfg(feature = "demux")] + Self::Container(audio) => audio.sample_rate, + } + } + + /// Total decoded sample frames, if they can be known up front. + pub fn total_samples(&self) -> Option { + match self { + Self::Elementary(index) => Some(index.total_samples), + #[cfg(feature = "demux")] + Self::Container(audio) => audio.total_samples, + } + } +} + +/// A decoded PCM stream, positioned at a sample and pulled block by block. +/// +/// Blocking throughout: it reads files and decodes. Callers on an async task run +/// it inside `spawn_blocking`. +pub enum PcmStream { + Elementary(ElementaryStream), + #[cfg(feature = "demux")] + Container(ContainerStream), +} + +impl PcmStream { + /// Open at `start_sample`, with the decoder already warmed on the frames + /// before it. + pub fn open(plan: &super::AudioPlan, start_sample: u64) -> Result { + match &plan.source { + PacketSource::Elementary(index) => Ok(Self::Elementary(ElementaryStream::open( + &plan.source_path, + plan.codec, + index, + plan.channels, + start_sample, + )?)), + #[cfg(feature = "demux")] + PacketSource::Container(audio) => Ok(Self::Container(ContainerStream::open( + &plan.source_path, + plan.codec, + audio, + plan.channels, + start_sample, + )?)), + } + } + + /// The next block of interleaved little-endian S16, or `None` at the end. + /// + /// A block is whatever one compressed frame decodes to; blocks are not a + /// fixed size and callers must not assume one. + pub fn next_block(&mut self) -> Option> { + match self { + Self::Elementary(stream) => stream.next_block(), + #[cfg(feature = "demux")] + Self::Container(stream) => stream.next_block(), + } + } +} + +/// The elementary-stream reader: seek to a byte offset, decode forward. +pub struct ElementaryStream { + source: std::io::BufReader, + decoder: PcmDecoder, + index: FrameIndex, + /// Next frame to read, as an index into `index.frames`. + next: usize, + /// Frame at which output starts; earlier ones are decoded for state only. + from: usize, + /// PCM from the frame the decoder was opened on, not yet handed out. + primed: Option>, + /// Decoded bytes still to discard from the front of the output. + skip: usize, + channels: u16, +} + +impl ElementaryStream { + fn open( + path: &Path, + codec: TranscodeCodec, + index: &FrameIndex, + channels: u16, + start_sample: u64, + ) -> Result { + let (from, samples_into_frame) = index.locate(start_sample); + // Priming already decodes the frame it opens on. Feeding that frame to + // the decoder a second time would run its samples through the overlap + // buffer twice, and every frame after it would then land somewhere a + // sequential decode never goes — so the range would stop being the slice + // of the whole that it claims to be. + let preroll = from.saturating_sub(1); + + let file = std::fs::File::open(path) + .with_context(|| format!("opening {} for transcoding", path.display()))?; + let mut source = std::io::BufReader::with_capacity(256 * 1024, file); + let first = index.frames[preroll]; + let mut raw = vec![0u8; first.len as usize]; + read_at(&mut source, first.offset, &mut raw)?; + let (decoder, primed) = + PcmDecoder::open(codec, index.sample_rate, Some(channels), &raw)?; + + Ok(Self { + source, + decoder, + index: index.clone(), + next: preroll, + from, + primed: Some(primed), + skip: samples_into_frame as usize * channels as usize * 2, + channels, + }) + } + + fn next_block(&mut self) -> Option> { + loop { + if self.next >= self.index.frames.len() { + return None; + } + let i = self.next; + let frame = self.index.frames[i]; + let want = frame.samples as usize * self.channels as usize * 2; + let pcm = match self.primed.take() { + Some(mut pcm) => { + pcm.resize(want, 0); + pcm + } + None => { + let mut raw = vec![0u8; frame.len as usize]; + if read_at(&mut self.source, frame.offset, &mut raw).is_err() { + return None; + } + self.decoder.decode_or_silence(&raw, Some(frame.samples)) + } + }; + self.next += 1; + + // Frames before the seek point were decoded for their state only. + if i < self.from { + continue; + } + if self.skip >= pcm.len() { + self.skip -= pcm.len(); + continue; + } + let out = pcm[self.skip..].to_vec(); + self.skip = 0; + return Some(out); + } + } +} + +fn read_at( + source: &mut R, + offset: u64, + into: &mut [u8], +) -> std::io::Result<()> { + source.seek(std::io::SeekFrom::Start(offset))?; + source.read_exact(into) +} + +/// The container reader: seek symphonia by time, decode forward. +/// +/// A container track has no byte index to divide into — its frames are +/// interleaved with video and scattered across clusters — so the seek is by +/// time and the landing point is refined by counting samples from the timestamp +/// of the first packet after it. That leaves a seek accurate to within the +/// rounding of one packet timestamp rather than exact to the sample, which is +/// inaudible and, importantly, does not affect the resource's length: that comes +/// from the plan, and the streaming half pads or clips to it either way. +#[cfg(feature = "demux")] +pub struct ContainerStream { + format: Box, + track_id: u32, + codec: TranscodeCodec, + decoder: PcmDecoder, + /// PCM decoded while positioning, not yet handed out. + pending: Option>, +} + +#[cfg(feature = "demux")] +impl ContainerStream { + fn open( + path: &Path, + codec: TranscodeCodec, + audio: &ContainerAudio, + channels: u16, + start_sample: u64, + ) -> Result { + use symphonia::core::formats::{SeekMode, SeekTo}; + use symphonia::core::units::Time; + + let mut format = open_format(path)?; + let time_base = track_time_base(format.as_ref(), audio.track_id); + + let start_secs = start_sample as f64 / f64::from(audio.sample_rate.max(1)); + // Sample zero needs no seek, and asking for one on a track whose first + // packet is already under the reader would only risk moving it. + if start_sample > 0 { + let target = (start_secs - PREROLL_SECS).max(0.0); + // A reader that cannot seek leaves itself at the start, which is + // still correct — just slower, because the discard loop below then + // walks there. The landing point is read back from the first + // packet's timestamp either way, so a failed seek needs no special + // case here. + let _ = format.seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::try_from_secs_f64(target).unwrap_or(Time::ZERO), + track_id: Some(audio.track_id), + }, + ); + } + + // Prime on the first packet the reader hands back, and take its + // timestamp as where we actually landed. + let (first_packet, first_pts) = next_track_packet(format.as_mut(), audio.track_id) + .context("reading the first packet of the audio track")? + .ok_or_else(|| anyhow::anyhow!("the audio track carries no packets"))?; + let position = if start_sample > 0 { + sample_at(first_pts, time_base, audio.sample_rate) + } else { + 0 + }; + let (decoder, primed) = + PcmDecoder::open(codec, audio.sample_rate, Some(channels), &first_packet)?; + + let mut me = Self { + format, + track_id: audio.track_id, + codec, + decoder, + pending: Some(primed), + }; + + // Walk forward to the requested sample, discarding as we go. The blocks + // dropped here are what warms the decoder's overlap state. + let stride = channels as usize * 2; + let mut to_drop = start_sample.saturating_sub(position) as usize * stride; + while to_drop > 0 { + let Some(block) = me.next_block() else { break }; + if to_drop >= block.len() { + to_drop -= block.len(); + } else { + me.pending = Some(block[to_drop..].to_vec()); + to_drop = 0; + } + } + Ok(me) + } + + fn next_block(&mut self) -> Option> { + if let Some(pending) = self.pending.take() { + return Some(pending); + } + loop { + let (data, _) = next_track_packet(self.format.as_mut(), self.track_id).ok()??; + // The frame's own header says how long it decodes to, which is what + // lets a corrupt frame inside a film cost its own duration in + // silence rather than shortening the track and shifting everything + // after it out of sync with the picture. + let expect = super::frames::frame_samples(self.codec, &data); + let pcm = self.decoder.decode_or_silence(&data, expect); + if !pcm.is_empty() { + return Some(pcm); + } + // A packet that produced nothing is not the end of the track; keep + // pulling until the reader itself runs out. + } + } +} + +/// Open `path` with symphonia, hinting the extension. +#[cfg(feature = "demux")] +pub(crate) fn open_format( + path: &Path, +) -> Result> { + use symphonia::core::formats::probe::Hint; + use symphonia::core::formats::FormatOptions; + use symphonia::core::io::MediaSourceStream; + use symphonia::core::meta::MetadataOptions; + + let file = std::fs::File::open(path) + .with_context(|| format!("opening {} for transcoding", path.display()))?; + let stream = MediaSourceStream::new(Box::new(file), Default::default()); + let mut hint = Hint::new(); + if let Some(extension) = path.extension().and_then(|value| value.to_str()) { + hint.with_extension(extension); + } + symphonia::default::get_probe() + .probe( + &hint, + stream, + FormatOptions::default(), + MetadataOptions::default(), + ) + .with_context(|| format!("probing {}", path.display())) +} + +#[cfg(feature = "demux")] +fn track_time_base( + format: &dyn symphonia::core::formats::FormatReader, + track_id: u32, +) -> Option { + format + .tracks() + .iter() + .find(|t| t.id == track_id) + .and_then(|t| t.time_base) +} + +/// The next packet belonging to `track_id`, with its presentation timestamp. +#[cfg(feature = "demux")] +pub(crate) fn next_track_packet( + format: &mut dyn symphonia::core::formats::FormatReader, + track_id: u32, +) -> Result, i64)>> { + loop { + match format.next_packet() { + Ok(Some(packet)) => { + if packet.track_id != track_id { + continue; + } + return Ok(Some((packet.data.to_vec(), packet.pts.get()))); + } + Ok(None) => return Ok(None), + Err(symphonia::core::errors::Error::IoError(e)) + if e.kind() == std::io::ErrorKind::UnexpectedEof => + { + return Ok(None) + } + Err(e) => return Err(anyhow::anyhow!("demuxing the audio track: {e}")), + } + } +} + +/// Which decoded sample a packet timestamp lands on. +#[cfg(feature = "demux")] +fn sample_at( + pts: i64, + time_base: Option, + sample_rate: u32, +) -> u64 { + use symphonia::core::units::Timestamp; + + let Some(time_base) = time_base else { + return 0; + }; + time_base + .calc_time(Timestamp::new(pts.max(0))) + .map(|time| (time.as_secs_f64() * f64::from(sample_rate)).round() as u64) + .unwrap_or(0) +} + +/// Find the audio track to transcode, and describe it. +/// +/// Multi-audio films pick the track the container marks DEFAULT, then the first +/// audio track. Deliberately not clever about language: a wrong guess is worse +/// than a predictable one, and the fix if it turns out to matter is one resource +/// per track rather than a better guess. +#[cfg(feature = "demux")] +pub(crate) fn probe_container_audio( + path: &Path, +) -> Result<(Box, ContainerAudio, TranscodeCodec)> { + use symphonia::core::formats::TrackFlags; + + let format = open_format(path)?; + let audio_tracks: Vec<_> = format + .tracks() + .iter() + .filter(|t| { + t.codec_params + .as_ref() + .is_some_and(|params| params.audio().is_some()) + }) + .collect(); + let track = audio_tracks + .iter() + .find(|t| t.flags.contains(TrackFlags::DEFAULT)) + .or_else(|| audio_tracks.first()) + .ok_or_else(|| anyhow::anyhow!("{} has no audio track", path.display()))?; + + let params = track.codec_params.as_ref().and_then(|p| p.audio()).unwrap(); + let Some(codec) = codec_of(params.codec) else { + bail!( + "the audio track of {} is not one this server decodes", + path.display() + ); + }; + let Some(sample_rate) = params.sample_rate.filter(|rate| *rate > 0) else { + bail!("the audio track of {} declares no sample rate", path.display()); + }; + + // `num_frames` is exact where a container carries it (MP4's `stts` does). + // Matroska usually does not, and its Segment duration is the next best + // thing: it is what every player already trusts for the scrub bar, and the + // streaming half pads or clips to whatever length is settled on here, so a + // few milliseconds of disagreement cannot truncate a transfer. + // + // Not done here: a demux-only counting pass. It would be exact, and it + // would read the whole film — twenty gigabytes of I/O — to answer a `HEAD`. + let total_samples = track + .num_frames + .or_else(|| { + let time_base = track.time_base?; + let duration = track.duration?; + time_base + .calc_duration(duration) + .map(|time| (time.as_secs_f64() * f64::from(sample_rate)).round() as u64) + }) + .or_else(|| { + let media_info = format.media_info(); + let time_base = media_info.time_base?; + let duration = media_info.duration?; + time_base + .calc_duration(duration) + .map(|time| (time.as_secs_f64() * f64::from(sample_rate)).round() as u64) + }) + .filter(|samples| *samples > 0); + + let audio = ContainerAudio { + track_id: track.id, + sample_rate, + total_samples, + }; + drop(audio_tracks); + Ok((format, audio, codec)) +} + +/// Which of the three a symphonia audio codec id is, if any. +#[cfg(feature = "demux")] +pub(crate) fn codec_of( + codec: symphonia::core::codecs::audio::AudioCodecId, +) -> Option { + use symphonia::core::codecs::audio::well_known::{CODEC_ID_AC3, CODEC_ID_DCA, CODEC_ID_EAC3}; + + match codec { + CODEC_ID_AC3 => Some(TranscodeCodec::Ac3), + CODEC_ID_EAC3 => Some(TranscodeCodec::Eac3), + CODEC_ID_DCA => Some(TranscodeCodec::Dts), + _ => None, + } +} diff --git a/crates/vuio-core/src/media/transcode/video.rs b/crates/vuio-core/src/media/transcode/video.rs new file mode 100644 index 00000000..0a827f01 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/video.rs @@ -0,0 +1,421 @@ +//! A film, remuxed on the fly, with its audio decoded on the way past. +//! +//! The deliverable of phase 4. A television that cannot decode AC-3 or DTS shows +//! the picture and produces no sound; what it is offered instead is this — the +//! same file, in fragmented MP4, with the video track copied out bit for bit and +//! only the audio track decoded and re-encoded as AAC. The picture is never +//! touched, which is what keeps the CPU cost proportional to the soundtrack +//! rather than to the film. +//! +//! Nothing about the output is written twice or seeked back into, because a +//! fragmented MP4 has no index to fix up at the end: an init segment describing +//! both tracks, then `moof`/`mdat` pairs forever. That is what lets it go +//! straight down an HTTP body of unknown length. +//! +//! Seeking is by time, not by byte. Byte-seeking would need the output's length +//! and layout known before it exists, and the audio half of it is a lossy +//! re-encode whose frame sizes are not predictable from anything — so a +//! byte offset cannot be turned into a position in a film without producing the +//! film first. A time seek needs none of that: it is a new response, built from +//! the same source at a different point, and the demuxer already seeks by +//! timestamp. See `crate::web::video_streaming` for the DLNA side of that. + +use anyhow::Result; +use std::path::Path; + +use super::{AacEncoder, PcmDecoder, TranscodeCodec}; +use crate::media::remux::{ + packet_is_keyframe, rescale_ticks, Fmp4Writer, MediaPacket, TrackCodec, TrackInfo, TrackKind, +}; + +/// Seconds of video per movie fragment. +/// +/// Short enough that a renderer starts playing promptly and that a dropped +/// connection wastes little work; long enough that the per-fragment box overhead +/// stays negligible against the samples it wraps. +const FRAGMENT_SECS: f64 = 2.0; + +/// Timescale for the video track: the MPEG-TS/HLS/DASH convention, and divisible +/// by every common frame rate. +const VIDEO_TIMESCALE: u32 = 90_000; + +/// Channels the re-encoded audio track carries. +const DECODED_CHANNELS: u16 = 2; + +/// A film being rewritten as it is read. +pub struct ProgressiveStream { + format: Box, + video: TrackSink, + audio: Option, + /// Total length of the film, for `mehd`. + duration_secs: Option, + sequence: u32, + /// Set once the first video sample has been taken, so audio that predates it + /// is dropped rather than started early. + started: bool, + finished: bool, +} + +struct TrackSink { + track: TrackInfo, + time_base: Option, + pending: Vec, + /// Presentation time of the fragment's first sample, in the track timescale. + fragment_start: Option, + /// Where the next fragment starts if this track contributes nothing to it. + next_decode_time: u64, +} + +struct AudioSink { + sink: TrackSink, + /// The container track this consumes. The output track keeps the source's + /// id — one track in, one track out — so these are the same number; naming + /// it separately keeps the two roles distinguishable at the call site. + source_id: u32, + /// `None` when the source is already AAC and is passed through untouched. + codec: Option, + decode: Option, +} + +struct AudioDecode { + codec: TranscodeCodec, + decoder: PcmDecoder, + encoder: AacEncoder, + decoded_channels: u16, + /// Decode time of the next AAC frame, in samples. + next_dts: Option, +} + +impl ProgressiveStream { + /// Open `path` positioned at `start_secs`, ready to emit fragments. + /// + /// `video` and `audio` come from the same probe the caller used to decide + /// this resource exists at all, so nothing here re-inspects the file. + pub fn open( + path: &Path, + video: &TrackInfo, + audio: Option<&TrackInfo>, + start_secs: f64, + duration_secs: Option, + ) -> Result { + use symphonia::core::formats::{SeekMode, SeekTo}; + use symphonia::core::units::Time; + + let mut format = super::source::open_format(path)?; + + if start_secs > 0.0 { + // Coarse, not Accurate: a stream has to open on a random-access + // point or the renderer has no reference frame to decode the first + // picture against. Coarse lands on the container's own cue point at + // or before the requested time, which is a keyframe by construction. + let _ = format.seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::try_from_secs_f64(start_secs).unwrap_or(Time::ZERO), + track_id: Some(video.id), + }, + ); + } + + let video_tb = track_time_base(format.as_ref(), video.id); + // An audio track this build cannot produce is no audio track: better a + // silent film with a picture that plays than a stream carrying samples + // the renderer will read as noise. + let audio = audio.and_then(|track| { + let codec = track.codec_kind.transcode_codec(); + if codec.is_none() && track.codec_kind != TrackCodec::Aac { + return None; + } + let audio_tb = track_time_base(format.as_ref(), track.id); + Some(AudioSink { + source_id: track.id, + codec, + sink: TrackSink::new(aac_track(track), audio_tb), + decode: None, + }) + }); + + Ok(Self { + format, + video: TrackSink::new(video.clone(), video_tb), + audio, + duration_secs, + sequence: 0, + started: false, + finished: false, + }) + } + + /// `ftyp` + `moov`: the init segment describing both tracks. + pub fn init_segment(&self) -> Vec { + let mut tracks: Vec<&TrackInfo> = vec![&self.video.track]; + if let Some(audio) = &self.audio { + tracks.push(&audio.sink.track); + } + let duration_ms = self + .duration_secs + .filter(|d| *d > 0.0) + .map(|d| (d * 1000.0).round() as u64); + + let mut init = Fmp4Writer::build_ftyp(); + init.extend_from_slice(&Fmp4Writer::build_moov_for(&tracks, duration_ms)); + init + } + + /// The next `moof`+`mdat`, or `None` at the end of the film. + pub fn next_fragment(&mut self) -> Option> { + if self.finished { + return None; + } + let fragment_ticks = (FRAGMENT_SECS * f64::from(VIDEO_TIMESCALE)).round() as u64; + + loop { + let packet = match self.format.next_packet() { + Ok(Some(packet)) => packet, + _ => { + // End of the film: flush the encoder's tail and emit whatever + // is held, then stop. + self.finished = true; + self.flush_audio(); + return self.emit(); + } + }; + let track_id = packet.track_id; + let pts = packet.pts.get(); + let data = packet.data.to_vec(); + + if track_id == self.video.track.id { + let ticks = self.video.rescale(pts, VIDEO_TIMESCALE); + let keyframe = packet_is_keyframe(&data, self.video.track.codec_kind); + // Nothing before the first random-access point is decodable: it + // depends on references a renderer starting here will not have. + if !self.started && !keyframe { + continue; + } + self.started = true; + + let elapsed = ticks.saturating_sub(*self.video.fragment_start.get_or_insert(ticks)); + if elapsed >= fragment_ticks && !self.video.pending.is_empty() { + let fragment = self.emit(); + self.video.push(ticks, data, true); + self.video.fragment_start = Some(ticks); + if fragment.is_some() { + return fragment; + } + continue; + } + self.video.push(ticks, data, keyframe); + continue; + } + + // Audio that predates the first picture is dropped rather than + // started early: a renderer given sound before it has a frame to show + // has nothing to synchronise it against. + if !self.started { + continue; + } + if let Err(error) = self.take_audio(track_id, pts, &data) { + tracing::debug!(%error, "dropping an audio packet that would not re-encode"); + } + } + } + + /// Feed one audio packet, if it belongs to the track being carried. + fn take_audio(&mut self, track_id: u32, pts: i64, data: &[u8]) -> Result<()> { + let Some(audio) = self.audio.as_mut() else { + return Ok(()); + }; + if track_id != audio.source_id { + return Ok(()); + } + let sample_rate = audio.sink.track.sample_rate.unwrap_or(48_000); + let ticks = audio.sink.rescale(pts, sample_rate); + + let Some(codec) = audio.codec else { + // Already AAC. The container's frames are MP4 samples as they stand. + audio.sink.push(ticks, data.to_vec(), true); + return Ok(()); + }; + + let decode = match audio.decode.as_mut() { + Some(decode) => decode, + None => { + let (decoder, primed) = + PcmDecoder::open(codec, sample_rate, Some(DECODED_CHANNELS), data)?; + let decoded_channels = decoder.channels(); + let encoder = AacEncoder::new(sample_rate, DECODED_CHANNELS)?; + audio.decode = Some(AudioDecode { + codec, + decoder, + encoder, + decoded_channels, + // One frame early, cancelling the encoder's delay: its MDCT + // window spans the previous hop and this one, so a decoder's + // output trails its input by exactly one frame. + next_dts: Some(ticks.saturating_sub(super::AAC_FRAME_SAMPLES)), + }); + let decode = audio.decode.as_mut().unwrap(); + let pcm = super::fit_channels(&primed, decoded_channels, DECODED_CHANNELS); + let adts = decode.encoder.push(&pcm)?; + push_aac(&mut audio.sink, decode, &adts); + return Ok(()); + } + }; + + let expect = super::frames::frame_samples(decode.codec, data); + let pcm = decode.decoder.decode_or_silence(data, expect); + let pcm = super::fit_channels(&pcm, decode.decoded_channels, DECODED_CHANNELS); + let adts = decode.encoder.push(&pcm)?; + push_aac(&mut audio.sink, decode, &adts); + Ok(()) + } + + fn flush_audio(&mut self) { + let Some(audio) = self.audio.as_mut() else { + return; + }; + let Some(decode) = audio.decode.as_mut() else { + return; + }; + let tail = decode.encoder.finish(); + push_aac(&mut audio.sink, decode, &tail); + } + + /// Wrap whatever both tracks hold into one fragment. + fn emit(&mut self) -> Option> { + let video_packets = self.video.take(); + let audio_packets = self + .audio + .as_mut() + .map(|audio| audio.sink.take()) + .unwrap_or_default(); + if video_packets.is_empty() && audio_packets.is_empty() { + return None; + } + + self.sequence += 1; + let mut tracks: Vec<(&TrackInfo, &[MediaPacket])> = + vec![(&self.video.track, &video_packets)]; + let mut fallbacks = vec![self.video.next_decode_time]; + if let Some(audio) = &self.audio { + tracks.push((&audio.sink.track, &audio_packets)); + fallbacks.push(audio.sink.next_decode_time); + } + let fragment = Fmp4Writer::build_multi_track_segment(self.sequence, &tracks, &fallbacks); + + // Remember where each track's timeline reached, so a fragment a track + // contributes nothing to still declares a sane base decode time. + self.video.advance(&video_packets); + if let Some(audio) = self.audio.as_mut() { + audio.sink.advance(&audio_packets); + } + Some(fragment) + } +} + +/// Append the AAC frames in `adts` to the audio track's pending run. +fn push_aac(sink: &mut TrackSink, decode: &mut AudioDecode, adts: &[u8]) { + for payload in super::adts_payloads(adts) { + let dts = decode.next_dts.unwrap_or(0); + sink.pending.push(MediaPacket { + track_id: sink.track.id, + pts: dts, + dts, + duration: super::AAC_FRAME_SAMPLES, + is_keyframe: true, + data: payload.to_vec(), + }); + decode.next_dts = Some(dts + super::AAC_FRAME_SAMPLES); + } +} + +impl TrackSink { + fn new(track: TrackInfo, time_base: Option) -> Self { + Self { + track, + time_base, + pending: Vec::new(), + fragment_start: None, + next_decode_time: 0, + } + } + + fn rescale(&self, ticks: i64, output_timescale: u32) -> u64 { + match self.time_base { + Some(time_base) => rescale_ticks(ticks, time_base, output_timescale), + None => ticks.max(0) as u64, + } + } + + fn push(&mut self, pts: u64, data: Vec, is_keyframe: bool) { + self.pending.push(MediaPacket { + track_id: self.track.id, + pts, + dts: pts, + duration: 0, + is_keyframe, + data, + }); + } + + fn take(&mut self) -> Vec { + let mut packets = std::mem::take(&mut self.pending); + // Matroska stores presentation timestamps in decode order and no decode + // timestamps at all; ISO-BMFF needs the opposite. Sorting this run's + // presentation timestamps recovers the decode timeline exactly, because + // each frame is decoded once and presented once. + crate::media::remux::derive_decode_timestamps(&mut packets); + packets + } + + /// Remember where this track's decode timeline reached. + /// + /// Only ever read as the base decode time of a fragment this track + /// contributed nothing to, which happens when one track runs out before the + /// other. The last sample's own duration is unknown for a passthrough + /// packet, so the gap to the previous sample stands in for it — which is the + /// same estimate the fragment writer uses for its final sample. + fn advance(&mut self, emitted: &[MediaPacket]) { + let Some(last) = emitted.last() else { return }; + let step = emitted + .len() + .checked_sub(2) + .map(|i| last.dts.saturating_sub(emitted[i].dts)) + .filter(|gap| *gap > 0) + .or(Some(last.duration).filter(|d| *d > 0)) + .unwrap_or(1); + self.next_decode_time = last.dts + step; + } +} + +/// The audio track as it will be written into the output. +/// +/// A decoded track is restated as the AAC it becomes: an `mp4a` sample entry +/// whose `esds` carries the encoder's own `AudioSpecificConfig`, at the +/// encoder's channel count rather than the source's 5.1. A track that is already +/// AAC keeps everything it had, including the config the container carried. +fn aac_track(track: &TrackInfo) -> TrackInfo { + if track.codec_kind == TrackCodec::Aac { + return track.clone(); + } + let sample_rate = track.sample_rate.unwrap_or(48_000); + TrackInfo { + codec: format!("{} → AAC", track.codec), + codec_kind: TrackCodec::Aac, + channels: Some(DECODED_CHANNELS as u8), + extra_data: super::audio_specific_config(sample_rate, DECODED_CHANNELS), + track_kind: TrackKind::Audio, + ..track.clone() + } +} + +fn track_time_base( + format: &dyn symphonia::core::formats::FormatReader, + track_id: u32, +) -> Option { + format + .tracks() + .iter() + .find(|t| t.id == track_id) + .and_then(|t| t.time_base) +} diff --git a/crates/vuio-core/src/platform/filesystem/metadata.rs b/crates/vuio-core/src/platform/filesystem/metadata.rs index 1b7284b6..685a4070 100644 --- a/crates/vuio-core/src/platform/filesystem/metadata.rs +++ b/crates/vuio-core/src/platform/filesystem/metadata.rs @@ -32,7 +32,7 @@ use symphonia::core::meta::{MetadataOptions, MetadataRevision, RawValue, Standar /// written by an older extractor even though the file itself has not changed. /// The file is opened and parsed on every scan regardless, so a bump costs one /// database write per record and no extra I/O. -pub(crate) const TAGS_VERSION: u32 = 1; +pub(crate) const TAGS_VERSION: u32 = 2; /// Longest tag value kept in `media_tags`. /// @@ -44,6 +44,46 @@ const MAX_TAG_VALUE_LEN: usize = 4096; /// Tags whose values are large enough to be worth storing nowhere. const OVERSIZED_TAGS: &[&str] = &["Lyrics", "AcoustIdFingerprint", "CdToc"]; +/// Read only the stream properties of a file, leaving its titling alone. +/// +/// For video. A film's title comes from its filename (and, where the operator +/// enabled it, from the metadata fetcher); running the tag reader's +/// artist/album/track-number logic over it would fill a library's video rows +/// with whatever a muxer happened to write. What is wanted here is one field: +/// which codec the audio track is in, so the browse path can decide whether to +/// offer a decoded alternative without opening the file. +/// +/// This is a header probe. It reads the container's front matter and its track +/// declarations; it demuxes nothing and decodes nothing. +pub(crate) async fn extract_stream_info( + media_file: &mut MediaFile, +) -> Result<(), Box> { + let path = media_file.path.clone(); + match tokio::task::spawn_blocking(move || probe_metadata(&path)).await { + Ok(Ok(probed)) => { + media_file.stream = probed.stream; + if media_file.duration.is_none() { + media_file.duration = probed.duration; + } + } + Ok(Err(error)) => { + tracing::debug!( + path = %media_file.path.display(), + %error, + "Failed to probe video stream properties" + ); + } + Err(error) => { + tracing::debug!( + path = %media_file.path.display(), + %error, + "Failed to execute blocking stream probe" + ); + } + } + Ok(()) +} + pub(crate) async fn extract_audio_metadata( media_file: &mut MediaFile, ) -> Result<(), Box> { @@ -196,14 +236,21 @@ fn probe_metadata(path: &Path) -> anyhow::Result { let mut format = open_format(path)?; let mut probed = ProbedMetadata::default(); + // The video track's codec, where there is one. Recorded so the browse path + // can tell a film whose picture can be copied through from one whose cannot + // without opening either. + if let Some(track) = format.default_track(TrackType::Video) { + if let Some(video) = track.codec_params.as_ref().and_then(|params| params.video()) { + probed.stream.video_codec = video_codec_short_name(video.codec); + } + } + // Stream properties come off the default audio track. A container with no // audio track still has usable tags, so this is not an error. if let Some(track) = format.default_track(TrackType::Audio) { let num_frames = track.num_frames; if let Some(audio) = track.codec_params.as_ref().and_then(|params| params.audio()) { - probed.stream.codec = symphonia::default::get_codecs() - .get_audio_decoder(audio.codec) - .map(|registered| registered.codec.info.short_name.to_owned()); + probed.stream.codec = audio_codec_short_name(audio.codec); probed.stream.sample_rate = audio.sample_rate; probed.stream.channels = audio .channels @@ -234,6 +281,65 @@ fn probe_metadata(path: &Path) -> anyhow::Result { Ok(probed) } +/// The short name to record for an identified audio codec. +/// +/// Symphonia's registry is asked first, so anything it can decode keeps the +/// exact spelling it has always been stored under. The fallback below covers +/// the codecs it *identifies but cannot decode* — which, before this existed, +/// stored a NULL codec and so were indistinguishable from a file whose track +/// nothing recognised. Those are precisely the codecs a television is most +/// likely to be missing a licence for, so they are the ones the browse path +/// most needs to know about. +/// +/// TrueHD is named here and is deliberately *not* decodable: nothing vendored +/// decodes it, [`crate::media::transcode::TranscodeCodec::from_stored_codec`] +/// returns `None` for it, and recording it is worth doing anyway so a +/// diagnostic can say what the track is instead of shrugging. +fn audio_codec_short_name( + codec: symphonia::core::codecs::audio::AudioCodecId, +) -> Option { + use symphonia::core::codecs::audio::well_known::*; + + if let Some(registered) = symphonia::default::get_codecs().get_audio_decoder(codec) { + return Some(registered.codec.info.short_name.to_owned()); + } + let name = match codec { + CODEC_ID_AC3 => "ac3", + CODEC_ID_EAC3 => "eac3", + CODEC_ID_DCA => "dca", + CODEC_ID_TRUEHD => "truehd", + CODEC_ID_AC4 => "ac4", + CODEC_ID_WMA => "wma", + CODEC_ID_OPUS => "opus", + _ => return None, + }; + Some(name.to_owned()) +} + +/// The short name to record for an identified video codec. +/// +/// Nothing here decodes video, so symphonia's decoder registry has no opinion +/// and this is a plain table. Only the names the remuxer acts on need to be +/// distinguishable; the rest are recorded so a diagnostic can say what a file +/// holds instead of shrugging. +fn video_codec_short_name( + codec: symphonia::core::codecs::video::VideoCodecId, +) -> Option { + use symphonia::core::codecs::video::well_known::*; + + let name = match codec { + CODEC_ID_H264 => "h264", + CODEC_ID_HEVC => "hevc", + CODEC_ID_VP8 => "vp8", + CODEC_ID_VP9 => "vp9", + CODEC_ID_AV1 => "av1", + CODEC_ID_MPEG2 => "mpeg2video", + CODEC_ID_MPEG4 => "mpeg4", + _ => return None, + }; + Some(name.to_owned()) +} + fn absorb_revision(revision: &MetadataRevision, probed: &mut ProbedMetadata) { for tag in &revision.media.tags { let key = match &tag.std { diff --git a/crates/vuio-core/src/platform/filesystem/mod.rs b/crates/vuio-core/src/platform/filesystem/mod.rs index 6d4222fa..2d8cae99 100644 --- a/crates/vuio-core/src/platform/filesystem/mod.rs +++ b/crates/vuio-core/src/platform/filesystem/mod.rs @@ -32,6 +32,15 @@ pub(crate) async fn extract_audio_metadata( Ok(()) } +/// Likewise for video: with no probe there is no codec to record, and the +/// browse path simply never offers a decoded alternative for a film. +#[cfg(not(feature = "metadata"))] +pub(crate) async fn extract_stream_info( + _media_file: &mut MediaFile, +) -> Result<(), Box> { + Ok(()) +} + /// Records written without a tag reader carry version 0, so enabling the /// feature later re-reads them on the next scan. #[cfg(not(feature = "metadata"))] diff --git a/crates/vuio-core/src/web/mod.rs b/crates/vuio-core/src/web/mod.rs index 846c023a..15dc83dc 100644 --- a/crates/vuio-core/src/web/mod.rs +++ b/crates/vuio-core/src/web/mod.rs @@ -19,6 +19,8 @@ pub mod streaming; pub mod subtitles; #[cfg(feature = "transcode")] pub mod transcode_streaming; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] +pub mod video_streaming; #[cfg(feature = "dashboard")] pub mod ui; pub mod xml; @@ -49,6 +51,29 @@ pub(crate) fn item_needs_transcode(codec: Option<&str>, mime: &str, filename: &s } } +/// Whether a film's picture can be copied into the remuxed alternative. +/// +/// Needing an alternative and being able to produce one are different +/// questions, asked of different tracks. The audio decides the first; the video +/// decides the second, because the alternative copies the picture through +/// rather than re-encoding it and can only do that for the codecs the fMP4 +/// writer knows how to describe. A film with a VP9 or MPEG-2 picture and an AC-3 +/// soundtrack therefore gets no second resource — advertising one and answering +/// 404 would be worse than the silence it was meant to fix. +/// +/// A record written before the scanner recorded video codecs carries `None`. +/// Those are treated as remuxable: the next scan fills the column in, and until +/// it does, the far more common case is the one that works. +pub(crate) fn item_can_remux_video(video_codec: Option<&str>) -> bool { + match video_codec { + None => true, + Some(codec) => matches!( + codec.trim().to_ascii_lowercase().as_str(), + "h264" | "avc" | "avc1" | "hevc" | "h265" | "hvc1" + ), + } +} + /// How this server should advertise a decoded alternative, if at all. /// /// One place decides, so the two DIDL writers cannot drift apart on it, and the @@ -72,14 +97,37 @@ pub(crate) fn transcode_advert( // The MIME differs from the original's, which is what lets a // renderer that matches against its own sink protocolInfo pick the // one it can actually decode. - mime: match config.transcode.audio_format { - TranscodeAudioFormat::Lpcm => "audio/vnd.wave", - TranscodeAudioFormat::Aac => "audio/aac", - }, - path: match config.transcode.audio_format { - TranscodeAudioFormat::Lpcm => "transcode/audio.wav", - TranscodeAudioFormat::Aac => "transcode/audio.aac", + audio: match config.transcode.audio_format { + TranscodeAudioFormat::Lpcm => xml::AdvertResource { + mime: "audio/vnd.wave", + path: "transcode/audio.wav", + // Constant-bitrate PCM: a byte offset divides straight back + // into a sample, so this is a real seek. + op: "11", + }, + TranscodeAudioFormat::Aac => xml::AdvertResource { + mime: "audio/aac", + path: "transcode/audio.aac", + // A lossy re-encode has no length until it exists, so there + // is nothing to seek within. + op: "00", + }, }, + // A film is offered the film, not its soundtrack: the same picture, + // with an audio track the renderer can actually decode. Time seek + // only — see `web::video_streaming` for why byte seek is not on + // offer and why time seek is enough. + #[cfg(all(feature = "transcode-aac", feature = "casting"))] + video: Some(xml::AdvertResource { + mime: "video/mp4", + path: "transcode/video.mp4", + op: "01", + }), + // With no remuxer or no encoder there is nothing to offer a film. + // Offering it `audio.wav` instead would replace a silent film with + // no film at all. + #[cfg(not(all(feature = "transcode-aac", feature = "casting")))] + video: None, first: config.transcode.prefer == TranscodePreference::Transcoded, }) } @@ -311,6 +359,14 @@ pub fn create_router( get(transcode_streaming::serve_transcoded_aac::) .head(transcode_streaming::serve_transcoded_aac::), ); + // The film itself, remuxed with its audio decoded. Needs the demuxer as + // well as the encoder, which is why it rides on `casting` too. + #[cfg(all(feature = "transcode-aac", feature = "casting"))] + let router = router.route( + "/media/{id}/transcode/video.mp4", + get(video_streaming::serve_transcoded_video::) + .head(video_streaming::serve_transcoded_video::), + ); #[cfg(feature = "casting")] let router = router diff --git a/crates/vuio-core/src/web/remux_streaming.rs b/crates/vuio-core/src/web/remux_streaming.rs index 5a69f515..8a9f0e16 100644 --- a/crates/vuio-core/src/web/remux_streaming.rs +++ b/crates/vuio-core/src/web/remux_streaming.rs @@ -1,10 +1,19 @@ //! HLS remux streaming endpoints for MKV web playback. //! -//! Every rendition (the video track, and each browser-playable — i.e. AAC — audio -//! track) is served as its own self-contained single-track fMP4 stream: its own -//! `index.m3u8`, its own `init.mp4` (a single-track `moov`, not one shared between -//! renditions), and its own numbered segments. MSE requires this: a `SourceBuffer` for -//! one track cannot be initialised from a `moov` that also describes another track. +//! Every rendition (the video track, and each audio track this build can put in +//! front of a browser) is served as its own self-contained single-track fMP4 +//! stream: its own `index.m3u8`, its own `init.mp4` (a single-track `moov`, not +//! one shared between renditions), and its own numbered segments. MSE requires +//! this: a `SourceBuffer` for one track cannot be initialised from a `moov` that +//! also describes another track. +//! +//! Video is always a copy. Audio is a copy when it is already AAC and a decode +//! plus a re-encode when it is AC-3, E-AC-3 or DTS — codecs no browser has ever +//! shipped a decoder for, and which this path used to drop, leaving the film +//! playing silently in the tab. That changes what a segment costs: `moof`+`mdat` +//! around a byte copy became four seconds of audio through a decoder and an +//! encoder, which is why segments are now built off the runtime, rationed by the +//! same permit pool as the DLNA path, and cached. use crate::{ database::DatabaseManager, @@ -112,9 +121,44 @@ pub async fn serve_hls_audio_playlist( .into_response()) } +/// The track as the browser will actually receive it. +/// +/// A video track, and an AAC audio track, arrive as they are. An AC-3, E-AC-3 or +/// DTS track does not exist in a browser at all, so what arrives is the AAC it +/// was re-encoded into — and the init segment has to describe *that*: an `mp4a` +/// entry whose `esds` carries the encoder's `AudioSpecificConfig`, at the +/// encoder's channel count rather than the source's 5.1. +/// +/// Both ends of the HLS path call this, which is the point: the `esds` a player +/// initialises its decoder from and the samples it then feeds that decoder are +/// built from one description, in two different requests. +fn rendition_track(track: &TrackInfo) -> TrackInfo { + #[cfg(all(feature = "transcode-aac", feature = "demux"))] + if track.codec_kind.transcode_codec().is_some() { + let sample_rate = track.sample_rate.unwrap_or(48_000); + let channels = DECODED_CHANNELS; + return TrackInfo { + codec: format!("{} → AAC", track.codec), + codec_kind: crate::media::remux::TrackCodec::Aac, + channels: Some(channels as u8), + extra_data: crate::media::transcode::audio_specific_config(sample_rate, channels), + ..track.clone() + }; + } + track.clone() +} + +/// Channels a decoded rendition is delivered in. +/// +/// Stereo. A browser tab is not a 5.1 speaker set, and the decoders apply the +/// bitstream's own downmix coefficients when asked for two channels, which is a +/// better mix than anything computed after the fact. +#[cfg(all(feature = "transcode-aac", feature = "demux"))] +const DECODED_CHANNELS: u16 = 2; + fn init_segment_response(track: &TrackInfo) -> Response { let mut init_bytes = Fmp4Writer::build_ftyp(); - init_bytes.extend_from_slice(&Fmp4Writer::build_moov(track)); + init_bytes.extend_from_slice(&Fmp4Writer::build_moov(&rendition_track(track))); ( [ @@ -148,8 +192,14 @@ pub async fn serve_hls_audio_init_segment( /// Extract packets for `track` starting at `seq * SEGMENT_DURATION_SECS` and mux them /// into an fMP4 segment (`moof` + `mdat`). Shared by the video- and audio-segment /// routes — they differ only in which track they resolve `{id}`/`{idx}` to. -fn build_segment_response(path: &std::path::Path, track: &TrackInfo, seq: u32) -> Response { - let timescale = Fmp4Writer::timescale_for(track); +/// +/// Blocking. Demuxing was always file I/O and parsing; a decoded rendition adds +/// a decode and an encode of four seconds of audio on top. Callers run it under +/// `spawn_blocking`, without which one seeking browser takes a runtime worker +/// out of service for the duration. +fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> Vec { + let out_track = rendition_track(track); + let timescale = Fmp4Writer::timescale_for(&out_track); let start_secs = seq as f64 * SEGMENT_DURATION_SECS as f64; let packets = MkvDemuxer::extract_track_packets( @@ -162,38 +212,119 @@ fn build_segment_response(path: &std::path::Path, track: &TrackInfo, seq: u32) - ) .unwrap_or_default(); + #[cfg(all(feature = "transcode-aac", feature = "demux"))] + let packets = match track.codec_kind.transcode_codec() { + Some(codec) => crate::media::transcode::reencode_to_aac( + codec, + &packets, + out_track.sample_rate.unwrap_or(48_000), + DECODED_CHANNELS, + out_track.id, + ) + .unwrap_or_default(), + None => packets, + }; + // Seeking lands at (or before) `start_secs`, not exactly on it, so the fragment's // base decode time comes from the packets themselves (`build_segment` takes it from // the first one's decode timestamp). The nominal `seq`-derived position is only a // fallback for a segment that came back empty. let nominal_decode_time = (start_secs * timescale as f64).round() as u64; - let segment_bytes = Fmp4Writer::build_segment(seq + 1, track, nominal_decode_time, &packets); + Fmp4Writer::build_segment(seq + 1, &out_track, nominal_decode_time, &packets) +} - ( - [ - (header::CONTENT_TYPE, "video/mp4"), - (header::CACHE_CONTROL, "public, max-age=3600"), - ], - segment_bytes, - ) - .into_response() +/// Build (or recall) one segment and wrap it in a response. +/// +/// Three things happen here that did not when this path only copied bytes. The +/// build runs on a blocking thread. It takes a transcoding permit, from the same +/// pool the DLNA path draws on, so the two share one CPU ceiling rather than +/// each keeping its own. And the result is cached: a scrub or a re-buffer asks +/// for the same segment again, and rebuilding it means decoding those four +/// seconds again. +#[cfg_attr(not(feature = "transcode"), allow(unused_variables))] +async fn segment_response( + state: &AppState, + file_id: i64, + path: &std::path::Path, + track: &TrackInfo, + seq: u32, +) -> Result { + let headers = [ + (header::CONTENT_TYPE, "video/mp4"), + (header::CACHE_CONTROL, "public, max-age=3600"), + ]; + + #[cfg(feature = "transcode")] + let key = crate::media::transcode::SegmentKey { + id: file_id, + track: track.id, + seq, + }; + #[cfg(feature = "transcode")] + if let Some(cached) = state.transcode.cached_segment(&key).await { + return Ok((headers, cached).into_response()); + } + + // Only a decoded rendition is rationed. A passthrough copy is file I/O, and + // refusing it under load would stop a browser from playing a film the CPU + // was never being asked to work on. + #[cfg(feature = "transcode")] + let _permit = if track.codec_kind.transcode_codec().is_some() { + match state.transcode.try_acquire() { + Some(permit) => Some(permit), + None => { + return Ok(( + axum::http::StatusCode::SERVICE_UNAVAILABLE, + [(header::RETRY_AFTER, "5")], + "All transcoding slots are in use.", + ) + .into_response()) + } + } + } else { + None + }; + + let owned_path = path.to_path_buf(); + let owned_track = track.clone(); + let bytes = tokio::task::spawn_blocking(move || { + build_segment_bytes(&owned_path, &owned_track, seq) + }) + .await + .map_err(|e| AppError::Internal(anyhow::anyhow!("segment builder panicked: {e}")))?; + let bytes = bytes::Bytes::from(bytes); + + #[cfg(feature = "transcode")] + state.transcode.remember_segment(key, bytes.clone()).await; + #[cfg(not(feature = "transcode"))] + let _ = file_id; + + Ok((headers, bytes).into_response()) } pub async fn serve_hls_video_segment( State(state): State>, Path((id, seq)): Path<(String, u32)>, ) -> Result { + let file_id = crate::web::streaming::media_id_from_path_segment(&id).ok_or(AppError::NotFound)?; let (path, info) = load_file_info(&state, &id).await?; - let video_track = browser_video_track(&info.tracks).ok_or(AppError::NotFound)?; - Ok(build_segment_response(&path, video_track, seq)) + let video_track = browser_video_track(&info.tracks) + .ok_or(AppError::NotFound)? + .clone(); + segment_response(&state, file_id, &path, &video_track, seq).await } pub async fn serve_hls_audio_segment( State(state): State>, Path((id, audio_idx, seq)): Path<(String, usize, u32)>, ) -> Result { + let file_id = crate::web::streaming::media_id_from_path_segment(&id).ok_or(AppError::NotFound)?; let (path, info) = load_file_info(&state, &id).await?; let audio_tracks = browser_audio_tracks(&info.tracks); - let track = audio_tracks.get(audio_idx).copied().ok_or(AppError::NotFound)?; - Ok(build_segment_response(&path, track, seq)) + let track = audio_tracks + .get(audio_idx) + .copied() + .ok_or(AppError::NotFound)? + .clone(); + segment_response(&state, file_id, &path, &track, seq).await } diff --git a/crates/vuio-core/src/web/transcode_streaming.rs b/crates/vuio-core/src/web/transcode_streaming.rs index e1ba1513..b7bda490 100644 --- a/crates/vuio-core/src/web/transcode_streaming.rs +++ b/crates/vuio-core/src/web/transcode_streaming.rs @@ -22,7 +22,7 @@ use axum::{ use std::sync::Arc; use tracing::{debug, warn}; -use crate::media::transcode::{AudioPlan, IndexKey, PcmDecoder, TranscodeCodec}; +use crate::media::transcode::{AudioPlan, IndexKey, PcmStream, TranscodeCodec}; use crate::{database::DatabaseManager, error::AppError, state::AppState}; use super::streaming::{media_id_from_path_segment, parse_range_header}; @@ -47,11 +47,11 @@ pub async fn serve_transcoded_aac( Path(id): Path, method: Method, ) -> Result { - let (file, codec) = resolve(&state, &id).await?; + let file = resolve(&state, &id).await?; let Some(permit) = state.transcode.try_acquire() else { return Ok(busy(&state, &file.filename)); }; - let plan = plan_for(&state, file.id, &file.path, codec).await?; + let plan = plan_for(&state, &file).await?; let response = Response::builder() .status(StatusCode::OK) @@ -82,25 +82,13 @@ fn aac_body(plan: Arc, permit: tokio::sync::OwnedSemaphorePermit) -> tokio::task::spawn_blocking(move || { let _permit = permit; - let file = match std::fs::File::open(&plan.source_path) { - Ok(f) => f, - Err(e) => { - let _ = tx.blocking_send(Err(e)); - return; - } - }; - let mut source = std::io::BufReader::with_capacity(256 * 1024, file); - - let (mut decoder, primed) = match prime(&plan, &mut source, 0) { - Ok(d) => d, + let mut stream = match PcmStream::open(&plan, 0) { + Ok(stream) => stream, Err(e) => { let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); return; } }; - // As above: the first frame is already decoded, and re-feeding it would - // push its samples through the overlap buffer a second time. - let mut primed = Some(primed); let mut encoder = match AacEncoder::new(plan.sample_rate(), plan.channels) { Ok(e) => e, Err(e) => { @@ -109,20 +97,7 @@ fn aac_body(plan: Arc, permit: tokio::sync::OwnedSemaphorePermit) -> } }; - for (i, frame) in plan.index.frames.iter().enumerate() { - let pcm = match primed.take() { - Some(mut pcm) => { - pcm.resize(plan.frame_bytes(i), 0); - pcm - } - None => { - let mut raw = vec![0u8; frame.len as usize]; - if read_frame(&mut source, frame.offset, &mut raw).is_err() { - break; - } - decoder.decode_or_silence(&raw, frame.samples) - } - }; + while let Some(pcm) = stream.next_block() { match encoder.push(&pcm) { Ok(adts) if adts.is_empty() => continue, Ok(adts) => { @@ -149,7 +124,7 @@ pub async fn serve_transcoded_wav( method: Method, headers: HeaderMap, ) -> Result { - let (file, codec) = resolve(&state, &id).await?; + let file = resolve(&state, &id).await?; // Ration the CPU before doing any of it. A refusal here is deliberate: a // renderer told to wait looks to its user like a file that will not open, @@ -158,8 +133,27 @@ pub async fn serve_transcoded_wav( return Ok(busy(&state, &file.filename)); }; - let plan = plan_for(&state, file.id, &file.path, codec).await?; - let total = plan.wav_size(); + let plan = plan_for(&state, &file).await?; + + // A source that will not say how long it is gets a chunked body: no length, + // no ranges, `DLNA.ORG_OP=00`. That loses the scrub bar; a guessed length + // would lose the transfer, because a `Content-Length` a renderer cannot be + // given reads as a truncated download rather than a shorter film. + let Some(total) = plan.wav_size() else { + let response = Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "audio/vnd.wave; codec=1") + .header("transferMode.dlna.org", "Streaming") + .header( + "contentFeatures.dlna.org", + "DLNA.ORG_OP=00;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000", + ); + if method == Method::HEAD { + drop(permit); + return Ok(response.body(Body::empty())?); + } + return Ok(response.body(pcm_body(plan, 0, u64::MAX, permit))?); + }; // Range handling is byte-identical to the passthrough path — the resource // just happens not to exist on disk. @@ -208,6 +202,27 @@ pub async fn serve_transcoded_wav( Ok(response.body(pcm_body(plan, start, len, permit))?) } +/// One library entry, resolved to something transcodable. +pub(crate) struct Resolved { + pub id: i64, + pub path: std::path::PathBuf, + pub filename: String, + /// The codec of the audio to be decoded. + pub codec: TranscodeCodec, + /// How its frames are reached. + pub kind: SourceKind, +} + +/// Whether the frames are the file, or are inside it. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum SourceKind { + /// A raw `.ac3`/`.eac3`/`.dts` file. + Elementary, + /// One track inside a container. + #[cfg(feature = "demux")] + Container, +} + /// Look the item up and confirm this build can decode it. /// /// A 404 for anything that is not decodable here, rather than an error: the URL @@ -216,7 +231,7 @@ pub async fn serve_transcoded_wav( async fn resolve( state: &AppState, id: &str, -) -> Result<(crate::database::FileLocation, TranscodeCodec), AppError> { +) -> Result { let Some(file_id) = media_id_from_path_segment(id) else { return Err(AppError::NotFound); }; @@ -225,13 +240,18 @@ async fn resolve( } let file = state .database - .get_file_location_by_id(file_id) + .get_file_by_id(file_id) .await? .ok_or(AppError::NotFound)?; - // The codec comes from what the scanner recorded, not from opening the file: - // re-probing here would repeat work the scan already did. - let Some(codec) = codec_for(&file.mime_type, &file.filename) else { + // Everything here comes from what the scanner recorded, not from opening the + // file: re-probing to answer "is there a second resource?" would repeat work + // the scan already did, once per item of every Browse response. + let Some((codec, kind)) = source_for( + file.stream.codec.as_deref(), + &file.mime_type, + &file.filename, + ) else { return Err(AppError::NotFound); }; if !codec.is_decodable() { @@ -242,7 +262,13 @@ async fn resolve( ); return Err(AppError::NotFound); } - Ok((file, codec)) + Ok(Resolved { + id: file_id, + path: file.path, + filename: file.filename, + codec, + kind, + }) } /// Every slot is busy. @@ -261,15 +287,13 @@ fn busy(state: &AppState, filename: &str) -> Response { } /// Fetch a cached plan, or build one off the async runtime. -async fn plan_for( +pub(crate) async fn plan_for( state: &AppState, - file_id: i64, - path: &std::path::Path, - codec: TranscodeCodec, + file: &Resolved, ) -> Result, AppError> { - let metadata = tokio::fs::metadata(path).await?; + let metadata = tokio::fs::metadata(&file.path).await?; let key = IndexKey { - id: file_id, + id: file.id, size: metadata.len(), modified: metadata .modified() @@ -283,11 +307,17 @@ async fn plan_for( return Ok(plan); } - let owned = path.to_path_buf(); - let plan = tokio::task::spawn_blocking(move || AudioPlan::build(&owned, codec)) - .await - .map_err(|e| AppError::Internal(anyhow::anyhow!("transcode planner panicked: {e}")))? - .map_err(AppError::Internal)?; + let owned = file.path.clone(); + let codec = file.codec; + let kind = file.kind; + let plan = tokio::task::spawn_blocking(move || match kind { + SourceKind::Elementary => AudioPlan::elementary(&owned, codec), + #[cfg(feature = "demux")] + SourceKind::Container => AudioPlan::container(&owned), + }) + .await + .map_err(|e| AppError::Internal(anyhow::anyhow!("transcode planner panicked: {e}")))? + .map_err(AppError::Internal)?; let plan = Arc::new(plan); state.transcode.remember(key, plan.clone()).await; Ok(plan) @@ -331,66 +361,26 @@ fn pcm_body( return; } - let file = match std::fs::File::open(&plan.source_path) { - Ok(f) => f, - Err(e) => { - let _ = tx.blocking_send(Err(e)); - return; - } - }; - let mut source = std::io::BufReader::with_capacity(256 * 1024, file); - - // A decoder must be primed with the frame it starts on, and AC-3/DTS - // frames overlap by half a window, so the sample right at a seek point - // is reconstructed from state the previous frame carried. Starting one - // frame early and discarding its output removes the transient that - // would otherwise tick at the start of every seek. - let preroll = seeked.frame.saturating_sub(1); - let (mut decoder, primed) = match prime(&plan, &mut source, preroll) { - Ok(d) => d, + let mut stream = match PcmStream::open(&plan, seeked.start_sample) { + Ok(stream) => stream, Err(e) => { let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); return; } }; - // Priming already decoded frame `preroll`. Feeding it again would run - // its samples through the overlap buffer twice, and every frame after - // it would then differ from the same frame in a sequential decode — so - // a range would not be the slice of the whole that it claims to be. - let mut primed = Some(primed); - - let mut skip = seeked.pcm_skip; - for i in preroll..plan.index.frames.len() { - if remaining == 0 { - break; - } - let frame = plan.index.frames[i]; - let pcm = match primed.take() { - Some(mut pcm) => { - pcm.resize(plan.frame_bytes(i), 0); - pcm - } - None => { - let mut raw = vec![0u8; frame.len as usize]; - if read_frame(&mut source, frame.offset, &mut raw).is_err() { - break; - } - decoder.decode_or_silence(&raw, frame.samples) - } - }; - // Frames before the seek point are decoded for their state only. - if i < seeked.frame { - continue; - } + // A range may begin partway through a sample frame, which no decoder + // can be positioned on — so the odd bytes come off the front here. + let mut skip = seeked.byte_skip; + while remaining > 0 { + let Some(pcm) = stream.next_block() else { break }; let pcm = if skip >= pcm.len() { skip -= pcm.len(); continue; } else { - let out = &pcm[skip..]; - skip = 0; - out + &pcm[skip..] }; + skip = 0; let take = pcm.len().min(remaining as usize); if tx .blocking_send(Ok(bytes::Bytes::copy_from_slice(&pcm[..take]))) @@ -419,36 +409,6 @@ fn pcm_body( Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) } -/// Open a decoder positioned at frame `at`, returning it and that frame's PCM. -/// -/// The PCM comes back rather than being dropped because opening a decoder -/// necessarily decodes a frame, and decoding the same frame again to get its -/// samples would advance the overlap state a second time. -fn prime( - plan: &AudioPlan, - source: &mut R, - at: usize, -) -> anyhow::Result<(PcmDecoder, Vec)> { - let first = plan.index.frames[at]; - let mut raw = vec![0u8; first.len as usize]; - read_frame(source, first.offset, &mut raw)?; - PcmDecoder::open( - plan.codec, - plan.index.sample_rate, - Some(plan.channels), - &raw, - ) -} - -fn read_frame( - source: &mut R, - offset: u64, - into: &mut [u8], -) -> std::io::Result<()> { - source.seek(std::io::SeekFrom::Start(offset))?; - source.read_exact(into) -} - /// Which codec a library entry holds, from what the scanner recorded. /// /// The MIME type is the primary signal because it is what the scanner assigned @@ -470,10 +430,57 @@ pub(crate) fn codec_for(mime: &str, filename: &str) -> Option { } } +/// Which codec an item's audio is in, and where its frames live. +/// +/// The elementary check comes first, and on the file's own identity: an `.ac3` +/// file *is* the bitstream, so its frames are found by walking sync words and +/// the resource it produces is seekable to the sample. Anything else with a +/// recorded AC-3/E-AC-3/DTS codec is a container holding a track — a film — and +/// is demuxed instead. A file with neither has no decoded resource at all. +pub(crate) fn source_for( + stored_codec: Option<&str>, + mime: &str, + filename: &str, +) -> Option<(TranscodeCodec, SourceKind)> { + if let Some(codec) = codec_for(mime, filename) { + return Some((codec, SourceKind::Elementary)); + } + #[cfg(feature = "demux")] + { + stored_codec + .and_then(TranscodeCodec::from_stored_codec) + .map(|codec| (codec, SourceKind::Container)) + } + // Without symphonia there is nothing that can open a container, so a film's + // audio track is simply out of reach and no resource is offered for it. + #[cfg(not(feature = "demux"))] + { + let _ = stored_codec; + None + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn a_raw_bitstream_is_framed_by_walking_it_and_a_film_is_demuxed() { + assert_eq!( + source_for(Some("ac3"), "audio/ac3", "track.ac3"), + Some((TranscodeCodec::Ac3, SourceKind::Elementary)) + ); + #[cfg(feature = "demux")] + assert_eq!( + source_for(Some("ac3"), "video/x-matroska", "Film.mkv"), + Some((TranscodeCodec::Ac3, SourceKind::Container)) + ); + // A film whose audio is already playable everywhere gets nothing. + assert_eq!(source_for(Some("aac"), "video/mp4", "Film.mp4"), None); + // Nor does one nothing vendored decodes. + assert_eq!(source_for(Some("truehd"), "video/x-matroska", "Film.mkv"), None); + } + #[test] fn codec_is_read_from_the_mime_the_scanner_assigned() { assert_eq!(codec_for("audio/ac3", "x.bin"), Some(TranscodeCodec::Ac3)); diff --git a/crates/vuio-core/src/web/video_streaming.rs b/crates/vuio-core/src/web/video_streaming.rs new file mode 100644 index 00000000..59fa312c --- /dev/null +++ b/crates/vuio-core/src/web/video_streaming.rs @@ -0,0 +1,334 @@ +//! A film served as fragmented MP4, with the soundtrack decoded on the way past. +//! +//! What phase 4 is for. The television plays the picture and the AC-3 or DTS +//! track produces nothing, so it is offered a second resource: the same film, +//! the same video bitstream copied through untouched, and an AAC audio track +//! made by decoding the original and re-encoding it. +//! +//! ## Why this resource has no `Content-Length` +//! +//! It does not exist until it is produced, and unlike the LPCM resource next +//! door its length cannot be worked out in advance. Video passthrough is +//! predictable only if every sample's size is known, which for Matroska means +//! reading the whole film; the AAC half is not predictable at all, because a +//! lossy encoder's frame sizes depend on the audio. So the body is chunked and +//! its length is unstated. A guessed `Content-Length` would be far worse: a +//! renderer that is promised bytes it never receives reports a failed transfer, +//! where an unstated length costs only the byte-seek nobody could honour anyway. +//! +//! ## How it is seekable regardless +//! +//! By time. `TimeSeekRange.dlna.org` is the DLNA mechanism for exactly this +//! case, and `DLNA.ORG_OP=01` is how a renderer is told to use it: byte seeking +//! unsupported, time seeking supported. A seek is a fresh response built from +//! the same film at a different point — the demuxer seeks by timestamp to the +//! keyframe at or before the request, and the fragments that follow carry the +//! real timeline, so the renderer's position display stays true. +//! +//! Because the seek is by time rather than by byte, it works the same for every +//! audio codec: nothing in it depends on the audio being predictable in size, or +//! on it being passed through rather than re-encoded. A film with an AC-3 track +//! and a film with an AAC one are scrubbed identically. + +use axum::{ + body::Body, + extract::{Path, Query, State}, + http::{header, HeaderMap, Method, StatusCode}, + response::{IntoResponse, Response}, +}; +use tracing::{debug, warn}; + +use crate::media::remux::{browser_video_track, FileInfo, MkvDemuxer, TrackInfo, TrackKind}; +use crate::media::transcode::ProgressiveStream; +use crate::{database::DatabaseManager, error::AppError, state::AppState}; + +use super::streaming::media_id_from_path_segment; + +/// How many fragments may sit between the muxer and the socket. +/// +/// This is the backpressure that stops a television which opens a stream and +/// then reads slowly from pulling a whole film through the decoder and into +/// memory. Two fragments is about four seconds of video. +const PIPELINE_DEPTH: usize = 2; + +/// DLNA flags: streaming and background transfer modes, connection stalling, +/// and the DLNA 1.5 marker. Identical to the other transcoded resources — what +/// differs between them is `DLNA.ORG_OP`, which states what can actually be +/// seeked and is therefore set per resource rather than shared. +const DLNA_FLAGS: &str = "DLNA.ORG_FLAGS=01700000000000000000000000000000"; + +/// `?t=` — the same seek, for callers with no DLNA header to send. +#[derive(serde::Deserialize, Default)] +pub struct VideoQuery { + t: Option, +} + +/// `GET`/`HEAD /media/{id}/transcode/video.mp4`. +pub async fn serve_transcoded_video( + State(state): State>, + Path(id): Path, + method: Method, + Query(query): Query, + headers: HeaderMap, +) -> Result { + let (path, filename, info) = resolve(&state, &id).await?; + let video = browser_video_track(&info.tracks) + .ok_or(AppError::NotFound)? + .clone(); + let audio = default_audio_track(&info.tracks).cloned(); + + let duration = info.duration_secs.filter(|d| *d > 0.0); + let requested = headers + .get("TimeSeekRange.dlna.org") + .and_then(|value| value.to_str().ok()) + .and_then(parse_npt_start) + .or(query.t); + // A seek past the end is clamped rather than refused: a renderer that has + // drifted a little past a film's declared duration should see the last + // moment of it, not an error. + let start = requested + .unwrap_or(0.0) + .max(0.0) + .min(duration.map(|d| (d - 0.1).max(0.0)).unwrap_or(f64::MAX)); + + let mut response = Response::builder() + .status(if requested.is_some() { + StatusCode::PARTIAL_CONTENT + } else { + StatusCode::OK + }) + .header(header::CONTENT_TYPE, "video/mp4") + .header(header::CACHE_CONTROL, "no-cache") + .header("transferMode.dlna.org", "Streaming") + // OP=01: time seek yes, byte seek no. Saying otherwise would be worse + // than saying nothing — a renderer that byte-seeks a resource which + // cannot honour it stops playing rather than falling back. + .header( + "contentFeatures.dlna.org", + format!("DLNA.ORG_OP=01;DLNA.ORG_CI=1;{DLNA_FLAGS}"), + ); + if let Some(duration) = duration { + // A renderer with no length to divide has nothing else to draw a scrub + // bar from. `mehd` in the init segment says the same thing; this is for + // the ones that read headers and not boxes. + response = response.header("X-Content-Duration", format!("{duration:.3}")); + if requested.is_some() { + response = response.header( + "TimeSeekRange.dlna.org", + format!("npt={start:.3}-{duration:.3}/{duration:.3}"), + ); + } + } + + if method == Method::HEAD { + return Ok(response.body(Body::empty())?); + } + + // Ration the CPU only once there is real work to do. A `HEAD` decoded + // nothing, and holding a slot for a renderer that only probes would starve + // one that is playing. + let Some(permit) = state.transcode.try_acquire() else { + return Ok(busy(&state, &filename)); + }; + + Ok(response.body(fmp4_body(path, video, audio, start, duration, permit))?) +} + +/// Mux the film on a blocking thread, handing fragments over a bounded channel. +/// +/// The permit rides along and is released when the body is dropped — which is +/// also what happens when a television disconnects, or seeks, mid-film. +fn fmp4_body( + path: std::path::PathBuf, + video: TrackInfo, + audio: Option, + start: f64, + duration: Option, + permit: tokio::sync::OwnedSemaphorePermit, +) -> Body { + let (tx, rx) = tokio::sync::mpsc::channel::>(PIPELINE_DEPTH); + + tokio::task::spawn_blocking(move || { + let _permit = permit; + let mut stream = + match ProgressiveStream::open(&path, &video, audio.as_ref(), start, duration) { + Ok(stream) => stream, + Err(e) => { + let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); + return; + } + }; + + if tx + .blocking_send(Ok(bytes::Bytes::from(stream.init_segment()))) + .is_err() + { + return; + } + while let Some(fragment) = stream.next_fragment() { + if tx + .blocking_send(Ok(bytes::Bytes::from(fragment))) + .is_err() + { + return; + } + } + }); + + Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) +} + +/// Look the item up and confirm it is a film with a track worth decoding. +async fn resolve( + state: &AppState, + id: &str, +) -> Result<(std::path::PathBuf, String, FileInfo), AppError> { + let Some(file_id) = media_id_from_path_segment(id) else { + return Err(AppError::NotFound); + }; + if !state.current_config().transcode.enabled { + return Err(AppError::NotFound); + } + let file = state + .database + .get_file_location_by_id(file_id) + .await? + .ok_or(AppError::NotFound)?; + + let info = MkvDemuxer::inspect(&file.path).map_err(|error| { + debug!("cannot inspect {} for remuxing: {error}", file.filename); + AppError::NotFound + })?; + Ok((file.path, file.filename, info)) +} + +/// Which audio track to carry. +/// +/// The one the container marks default, then the first this build can produce. +/// Deliberately not clever about language: a wrong guess is worse than a +/// predictable one, and the fix if it turns out to matter is one `` per +/// audio track, which the DIDL already supports. +fn default_audio_track(tracks: &[TrackInfo]) -> Option<&TrackInfo> { + let playable = |t: &&TrackInfo| t.track_kind == TrackKind::Audio && t.codec_kind.is_playable(); + tracks + .iter() + .find(|t| playable(t) && t.is_default) + .or_else(|| tracks.iter().find(playable)) +} + +fn busy(state: &AppState, filename: &str) -> Response { + warn!( + "refusing to remux {}: all {} transcode slots are in use", + filename, + state.current_config().transcode.max_concurrent + ); + ( + StatusCode::SERVICE_UNAVAILABLE, + [(header::RETRY_AFTER, "5")], + "All transcoding slots are in use.", + ) + .into_response() +} + +/// The start time of a `TimeSeekRange.dlna.org` header, in seconds. +/// +/// Two spellings are legal and both turn up: decimal seconds (`npt=120.5-`) and +/// `hh:mm:ss.fff` (`npt=0:02:00.500-`). A header naming a byte range as well is +/// answered on its time half, which is the half this resource can honour. +pub(crate) fn parse_npt_start(header: &str) -> Option { + let npt = header + .split(&[' ', ';'][..]) + .find_map(|part| part.trim().strip_prefix("npt="))?; + let start = npt.split('-').next()?.trim(); + if start.is_empty() { + return None; + } + if !start.contains(':') { + return start.parse::().ok(); + } + let mut seconds = 0f64; + for part in start.split(':') { + seconds = seconds * 60.0 + part.parse::().ok()?; + } + Some(seconds) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::media::remux::TrackCodec; + + fn audio(id: u32, codec_kind: TrackCodec, is_default: bool) -> TrackInfo { + TrackInfo { + id, + track_kind: TrackKind::Audio, + codec: "AC-3".into(), + codec_kind, + language: None, + name: None, + sample_rate: Some(48_000), + channels: Some(6), + width: None, + height: None, + is_default, + extra_data: Vec::new(), + } + } + + #[test] + fn a_multi_audio_film_carries_the_track_the_container_marks_default() { + let tracks = vec![ + audio(2, TrackCodec::Ac3, false), + audio(3, TrackCodec::Ac3, true), + audio(4, TrackCodec::Ac3, false), + ]; + assert_eq!(default_audio_track(&tracks).map(|t| t.id), Some(3)); + } + + #[test] + fn with_no_default_marked_the_first_playable_track_is_carried() { + // The first track here is TrueHD: named, and decoded by nothing + // vendored. Carrying it would produce a stream of noise. + let tracks = vec![ + audio(2, TrackCodec::Unsupported, false), + audio(3, TrackCodec::Ac3, false), + ]; + let expected = if TrackCodec::Ac3.is_playable() { + Some(3) + } else { + None + }; + assert_eq!(default_audio_track(&tracks).map(|t| t.id), expected); + } + + /// A default-marked track this build cannot produce must not win over one it + /// can: the point of the preference is which track to carry, not whether to + /// carry a broken one. + #[test] + fn a_default_track_this_build_cannot_decode_is_passed_over() { + let tracks = vec![ + audio(2, TrackCodec::Unsupported, true), + audio(3, TrackCodec::Aac, false), + ]; + assert_eq!(default_audio_track(&tracks).map(|t| t.id), Some(3)); + } + + #[test] + fn npt_is_read_in_both_of_its_legal_spellings() { + assert_eq!(parse_npt_start("npt=120.5-"), Some(120.5)); + assert_eq!(parse_npt_start("npt=0:02:00.500-"), Some(120.5)); + assert_eq!(parse_npt_start("npt=00:00:30-00:01:00"), Some(30.0)); + // A header that also names bytes is answered on the half we can honour. + assert_eq!( + parse_npt_start("npt=10.0-100.0/100.0 bytes=1024-2048/2048"), + Some(10.0) + ); + } + + #[test] + fn a_header_with_no_start_time_is_not_a_seek() { + assert_eq!(parse_npt_start("npt=-30"), None); + assert_eq!(parse_npt_start("bytes=0-100"), None); + assert_eq!(parse_npt_start(""), None); + } +} diff --git a/crates/vuio-core/src/web/xml.rs b/crates/vuio-core/src/web/xml.rs index 97e78af5..fee304ac 100644 --- a/crates/vuio-core/src/web/xml.rs +++ b/crates/vuio-core/src/web/xml.rs @@ -16,9 +16,13 @@ mod rendering; pub use browse::*; pub use descriptions::*; +// `AdvertResource` is only spelled by the code that builds an advert, which is +// behind `transcode`; it stays exported so the XML writers' vocabulary is one +// list rather than a conditional one. +#[allow(unused_imports)] pub use rendering::{ container_class, generate_indexed_browse_response, generate_indexed_items_response, - BrowseRenderContext, TranscodeAdvert, ContainerSpec, + AdvertResource, BrowseRenderContext, TranscodeAdvert, ContainerSpec, }; #[cfg(test)] diff --git a/crates/vuio-core/src/web/xml/browse.rs b/crates/vuio-core/src/web/xml/browse.rs index c979d845..57110e7b 100644 --- a/crates/vuio-core/src/web/xml/browse.rs +++ b/crates/vuio-core/src/web/xml/browse.rs @@ -247,6 +247,11 @@ pub async fn generate_browse_response( &file.mime_type, &file.filename, ) + }) + .filter(|advert| advert.resource_for(&file.mime_type).is_some()) + .filter(|_| { + !file.mime_type.starts_with("video/") + || crate::web::item_can_remux_video(file.stream.video_codec.as_deref()) }); if let Some(advert) = transcoded.filter(|a| a.first) { let _ = advert.write_didl( @@ -254,6 +259,7 @@ pub async fn generate_browse_response( server_ip, state.http_binding.port(), file_id, + &file.mime_type, duration_secs, ); } @@ -297,6 +303,7 @@ pub async fn generate_browse_response( server_ip, state.http_binding.port(), file_id, + &file.mime_type, duration_secs, ); } diff --git a/crates/vuio-core/src/web/xml/rendering.rs b/crates/vuio-core/src/web/xml/rendering.rs index 15d0041a..63d325f3 100644 --- a/crates/vuio-core/src/web/xml/rendering.rs +++ b/crates/vuio-core/src/web/xml/rendering.rs @@ -150,31 +150,61 @@ pub struct BrowseRenderContext { pub transcode: Option, } +/// One resource this server can produce in place of an item it may not play. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AdvertResource { + /// MIME type of the produced resource. + pub mime: &'static str, + /// Path suffix under `/media/{id}/` that serves it. + pub path: &'static str, + /// The `DLNA.ORG_OP` value this resource can actually honour. + /// + /// Stated per resource, and it differs: constant-bitrate LPCM divides a byte + /// offset straight back into a sample, so it is `11`; a re-encoded AAC + /// stream has no length to seek within, so it is `00`; a remuxed film is + /// `01`, time seek only. A resource that claims an operation it cannot + /// perform is worse than one that claims none — a renderer which byte-seeks + /// and gets nothing usable stops playing rather than falling back. + pub op: &'static str, +} + /// How a decoded alternative resource should be advertised. /// /// A plain value rather than a read of the config, so the XML writers stay -/// feature-blind and a test can set up either case directly. +/// feature-blind and a test can set up either case directly. It carries both +/// answers because one DIDL response mixes films and music: the item's own MIME +/// type selects between them at the point the `` is written. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TranscodeAdvert { - /// MIME type of the decoded resource. - pub mime: &'static str, - /// Path suffix under `/media/{id}/` that serves it. - pub path: &'static str, + /// What an audio item is offered instead. + pub audio: AdvertResource, + /// What a video item is offered instead, where this build can produce one. + /// + /// `None` in a build with no remuxer or no encoder. A film then gets no + /// second resource at all rather than being offered its own soundtrack in + /// place of itself. + pub video: Option, /// Whether the decoded resource is listed before the original. pub first: bool, } impl TranscodeAdvert { + /// The resource to offer for an item of `mime`, if there is one. + pub(crate) fn resource_for(&self, mime: &str) -> Option { + if mime.starts_with("video/") { + self.video + } else { + Some(self.audio) + } + } + /// Write the decoded alternative for `file_id`. - /// - /// `DLNA.ORG_CI=1` is the conversion indicator: a renderer matching on - /// protocolInfo has to be told these bytes were produced rather than stored, - /// and every other `` this server writes says `CI=0`. fn write( &self, output: &mut W, context: &BrowseRenderContext, file_id: i64, + mime: &str, duration: Option, ) -> std::fmt::Result { self.write_didl( @@ -182,24 +212,33 @@ impl TranscodeAdvert { &context.server_ip, context.server_port, file_id, + mime, duration, ) } /// The same, for the fallback writer, which carries its parts loose rather /// than in a context. + /// + /// `DLNA.ORG_CI=1` is the conversion indicator: a renderer matching on + /// protocolInfo has to be told these bytes were produced rather than stored, + /// and every other `` this server writes says `CI=0`. pub(crate) fn write_didl( &self, output: &mut W, server_ip: &str, server_port: u16, file_id: i64, + mime: &str, duration: Option, ) -> std::fmt::Result { + let Some(resource) = self.resource_for(mime) else { + return Ok(()); + }; write!( output, - r#"http://{}:{}/media/{}/{}", - server_ip, server_port, file_id, self.path + server_ip, server_port, file_id, resource.path ) } } @@ -458,10 +497,14 @@ pub(super) fn write_media_view( let transcoded = context .transcode .filter(|_| !is_radio) - .filter(|_| crate::web::item_needs_transcode(file.codec(), mime, file.filename())); + .filter(|_| crate::web::item_needs_transcode(file.codec(), mime, file.filename())) + .filter(|advert| advert.resource_for(mime).is_some()) + .filter(|_| { + !mime.starts_with("video/") || crate::web::item_can_remux_video(file.video_codec()) + }); let item_duration = file.duration_secs().map(|value| value as u64); if let Some(advert) = transcoded.filter(|a| a.first) { - advert.write(output, context, file_id, item_duration)?; + advert.write(output, context, file_id, mime, item_duration)?; } write!( @@ -515,7 +558,7 @@ pub(super) fn write_media_view( context.server_ip, context.server_port, file_id )?; if let Some(advert) = transcoded.filter(|a| !a.first) { - advert.write(output, context, file_id, item_duration)?; + advert.write(output, context, file_id, mime, item_duration)?; } if context.client == crate::web::client::DlnaClientProfile::LgTv && has_srt { write!( diff --git a/crates/vuio-core/tests/common/mod.rs b/crates/vuio-core/tests/common/mod.rs new file mode 100644 index 00000000..e97165e9 --- /dev/null +++ b/crates/vuio-core/tests/common/mod.rs @@ -0,0 +1,383 @@ +//! A Matroska file, built at test time, with real audio inside it. +//! +//! Phase 4 is about films, and `test-media/movie1.mkv` is a 26-byte stub. The +//! alternative to committing a binary film clip is this: an EBML writer small +//! enough to read, fed the vendored decoders' own conformance fixtures, so the +//! MKV a test scans and streams carries AC-3 or DTS frames that really decode. +//! +//! The video track is synthesised rather than encoded — nothing in this tree +//! decodes video, and nothing needs to. What matters about it is what the +//! passthrough path actually touches: an `avcC` that must arrive byte-identical +//! in the output's `stsd`, and length-prefixed NAL units whose type says which +//! samples are random-access points. + +#![allow(dead_code)] + +/// A plausible AVCDecoderConfigurationRecord: High profile, level 3.0, 4-byte +/// NAL length prefixes, one SPS and one PPS. The payload bytes are not a real +/// sequence parameter set and do not need to be — no test here decodes a +/// picture, and every stage that handles this record copies it verbatim. +pub const AVCC: &[u8] = &[ + 0x01, 0x64, 0x00, 0x1E, // configurationVersion, profile, compat, level + 0xFF, // lengthSizeMinusOne = 3 + 0xE1, // numOfSequenceParameterSets = 1 + 0x00, 0x0A, // SPS length + 0x67, 0x64, 0x00, 0x1E, 0xAC, 0xD9, 0x40, 0xA0, 0x2F, 0xF9, // SPS + 0x01, // numOfPictureParameterSets + 0x00, 0x04, // PPS length + 0x68, 0xEB, 0xE3, 0xCB, // PPS +]; + +/// One video sample: a single length-prefixed NAL unit of `len` bytes. +/// +/// `keyframe` picks the NAL type the keyframe detector looks for — 5 (IDR) or 1 +/// (non-IDR slice) — which is what decides where an HLS segment may begin. +pub fn video_sample(keyframe: bool, len: usize, fill: u8) -> Vec { + let nal_type: u8 = if keyframe { 0x65 } else { 0x41 }; + let mut nal = vec![nal_type]; + nal.extend(std::iter::repeat_n(fill, len.saturating_sub(1))); + let mut sample = (nal.len() as u32).to_be_bytes().to_vec(); + sample.extend_from_slice(&nal); + sample +} + +/// One track to write into the file. +pub struct Track { + pub number: u64, + /// Matroska `CodecID`, e.g. `V_MPEG4/ISO/AVC` or `A_AC3`. + pub codec_id: &'static str, + pub codec_private: Vec, + pub kind: TrackKind, + /// The samples, in order, each with the millisecond it is presented at. + pub samples: Vec<(u64, Vec)>, + /// Whether every sample is a random-access point. + pub all_keyframes: bool, + /// Whether the container marks this the default track of its kind. + pub is_default: bool, +} + +pub enum TrackKind { + Video { width: u64, height: u64 }, + Audio { sample_rate: f64, channels: u64 }, +} + +/// Serialize `tracks` into a Matroska file `duration_ms` long. +/// +/// One cluster per second, each opening with a `Timestamp` and holding a +/// `SimpleBlock` per sample. A `SeekHead` at the front points at the `Cues` at +/// the back, which is what makes the file seekable: symphonia stops scanning +/// top-level elements at the first cluster, so a `Cues` element it was not told +/// about in advance is one it never reads. +pub fn build_mkv(tracks: &[Track], duration_ms: f64) -> Vec { + const ID_SEEK_HEAD: u32 = 0x114D9B74; + const ID_INFO: u32 = 0x1549A966; + const ID_TRACKS: u32 = 0x1654AE6B; + const ID_CLUSTER: u32 = 0x1F43B675; + const ID_CUES: u32 = 0x1C53BB6B; + + // --- Info --- + let mut info = Vec::new(); + info.extend(uint_el(0x2AD7B1, 1_000_000)); // TimestampScale: 1 ms per tick + info.extend(float_el(0x4489, duration_ms)); // Duration, in ticks + info.extend(str_el(0x4D80, "vuio-test")); + info.extend(str_el(0x5741, "vuio-test")); + let info = master(ID_INFO, &info); + + // --- Tracks --- + let mut entries = Vec::new(); + for track in tracks { + let mut entry = Vec::new(); + entry.extend(uint_el(0xD7, track.number)); // TrackNumber + entry.extend(uint_el(0x73C5, track.number)); // TrackUID + entry.extend(uint_el( + 0x83, + match track.kind { + TrackKind::Video { .. } => 1, + TrackKind::Audio { .. } => 2, + }, + )); // TrackType + entry.extend(uint_el(0x88, u64::from(track.is_default))); // FlagDefault + entry.extend(str_el(0x86, track.codec_id)); // CodecID + if !track.codec_private.is_empty() { + entry.extend(bin_el(0x63A2, &track.codec_private)); // CodecPrivate + } + match track.kind { + TrackKind::Video { width, height } => { + let mut video = Vec::new(); + video.extend(uint_el(0xB0, width)); + video.extend(uint_el(0xBA, height)); + entry.extend(master(0xE0, &video)); + } + TrackKind::Audio { + sample_rate, + channels, + } => { + let mut audio = Vec::new(); + audio.extend(float_el(0xB5, sample_rate)); + audio.extend(uint_el(0x9F, channels)); + entry.extend(master(0xE1, &audio)); + } + } + entries.extend(master(0xAE, &entry)); + } + let tracks_el = master(ID_TRACKS, &entries); + + // --- Clusters, one per second of content --- + // Every sample from every track, in presentation order, so the interleaving + // is what a real muxer would produce and a demuxer walking forward sees both + // tracks advance together. + let mut all: Vec<(u64, u64, &Vec, bool)> = Vec::new(); + for track in tracks { + for (ms, data) in &track.samples { + all.push((*ms, track.number, data, track.all_keyframes)); + } + } + all.sort_by_key(|(ms, number, _, _)| (*ms, *number)); + + let mut clusters = Vec::new(); + // (cluster timestamp in ms, byte offset from the start of the segment's data) + let mut cue_points: Vec<(u64, u64)> = Vec::new(); + let seek_head_len = seek_head_placeholder_len(); + let mut cursor = seek_head_len + info.len() as u64 + tracks_el.len() as u64; + + let mut index = 0usize; + while index < all.len() { + let cluster_ms = all[index].0 / 1000 * 1000; + let mut body = Vec::new(); + body.extend(uint_el(0xE7, cluster_ms)); // Timestamp + while index < all.len() && all[index].0 < cluster_ms + 1000 { + let (ms, number, data, keyframe) = all[index]; + let mut block = vint(number); // track number, as a vint + block.extend_from_slice(&((ms as i64 - cluster_ms as i64) as i16).to_be_bytes()); + // Bit 7 is the keyframe flag; a SimpleBlock carries no other state + // this writer needs. + block.push(if keyframe || is_keyframe_sample(data) { + 0x80 + } else { + 0x00 + }); + block.extend_from_slice(data); + body.extend(bin_el(0xA3, &block)); // SimpleBlock + index += 1; + } + let cluster = master(ID_CLUSTER, &body); + cue_points.push((cluster_ms, cursor)); + cursor += cluster.len() as u64; + clusters.extend(cluster); + } + + // --- Cues, one point per cluster, for the first track --- + let cues_pos = cursor; + let cue_track = tracks.first().map(|t| t.number).unwrap_or(1); + let mut cues_body = Vec::new(); + for (time, position) in &cue_points { + let mut point = Vec::new(); + point.extend(uint_el(0xB3, *time)); // CueTime + let mut positions = Vec::new(); + positions.extend(uint_el(0xF7, cue_track)); // CueTrack + positions.extend(uint_el(0xF1, *position)); // CueClusterPosition + point.extend(master(0xB7, &positions)); + cues_body.extend(master(0xBB, &point)); + } + let cues = master(ID_CUES, &cues_body); + + // --- SeekHead, now that the positions it points at are known --- + let seek_head = seek_head(&[ + (ID_INFO, seek_head_len), + (ID_TRACKS, seek_head_len + info.len() as u64), + (ID_CUES, cues_pos), + ]); + assert_eq!( + seek_head.len() as u64, + seek_head_len, + "the SeekHead must be exactly as long as its placeholder, or every \ + position after it moves" + ); + + let mut segment_body = Vec::new(); + segment_body.extend(seek_head); + segment_body.extend(info); + segment_body.extend(tracks_el); + segment_body.extend(clusters); + segment_body.extend(cues); + + let mut out = ebml_header(); + out.extend(master(0x18538067, &segment_body)); // Segment + out +} + +fn is_keyframe_sample(data: &[u8]) -> bool { + data.len() > 4 && (data[4] & 0x1F) == 5 +} + +fn ebml_header() -> Vec { + let mut body = Vec::new(); + body.extend(uint_el(0x4286, 1)); // EBMLVersion + body.extend(uint_el(0x42F7, 1)); // EBMLReadVersion + body.extend(uint_el(0x42F2, 4)); // EBMLMaxIDLength + body.extend(uint_el(0x42F3, 8)); // EBMLMaxSizeLength + body.extend(str_el(0x4282, "matroska")); + body.extend(uint_el(0x4287, 4)); // DocTypeVersion + body.extend(uint_el(0x4285, 2)); // DocTypeReadVersion + master(0x1A45DFA3, &body) +} + +/// Every `SeekPosition` is written at a fixed eight bytes, so the element's +/// length does not depend on the values it ends up carrying — which is what +/// allows one layout pass instead of iterating to a fixed point. +fn seek_head(entries: &[(u32, u64)]) -> Vec { + let mut body = Vec::new(); + for (id, position) in entries { + let mut seek = Vec::new(); + seek.extend(bin_el(0x53AB, &id_bytes(*id))); // SeekID + seek.extend(uint_el_fixed(0x53AC, *position, 8)); // SeekPosition + body.extend(master(0x4DBB, &seek)); + } + master(0x114D9B74, &body) +} + +fn seek_head_placeholder_len() -> u64 { + seek_head(&[(0x1549A966, 0), (0x1654AE6B, 0), (0x1C53BB6B, 0)]).len() as u64 +} + +/// The bytes of an EBML element ID, written verbatim as the class ID they are. +fn id_bytes(id: u32) -> Vec { + let bytes = id.to_be_bytes(); + let first = bytes.iter().position(|b| *b != 0).unwrap_or(3); + bytes[first..].to_vec() +} + +/// An EBML unsigned length or value, in the smallest width that fits. +fn vint(value: u64) -> Vec { + for width in 1..=8u32 { + // The all-ones value of each width is reserved as "unknown length". + let capacity = (1u64 << (7 * width)) - 1; + if value < capacity { + let marked = value | (1u64 << (7 * width)); + return marked.to_be_bytes()[8 - width as usize..].to_vec(); + } + } + unreachable!("no EBML length exceeds eight bytes") +} + +fn element(id: u32, payload: &[u8]) -> Vec { + let mut out = id_bytes(id); + out.extend(vint(payload.len() as u64)); + out.extend_from_slice(payload); + out +} + +fn master(id: u32, body: &[u8]) -> Vec { + element(id, body) +} + +fn bin_el(id: u32, data: &[u8]) -> Vec { + element(id, data) +} + +fn str_el(id: u32, value: &str) -> Vec { + element(id, value.as_bytes()) +} + +fn uint_el(id: u32, value: u64) -> Vec { + let bytes = value.to_be_bytes(); + let first = bytes.iter().position(|b| *b != 0).unwrap_or(7); + element(id, &bytes[first..]) +} + +fn uint_el_fixed(id: u32, value: u64, width: usize) -> Vec { + element(id, &value.to_be_bytes()[8 - width..]) +} + +fn float_el(id: u32, value: f64) -> Vec { + element(id, &value.to_be_bytes()) +} + +// ── A server over a temporary library ────────────────────────────────────── + +use std::sync::Arc; +use vuio_core::config::{AppConfig, MonitoredDirectoryConfig, ValidationMode}; +use vuio_core::database::sqlite::SqliteDatabase; +use vuio_core::database::{DatabaseManager, MediaFile, MediaRepository}; +use vuio_core::state::AppState; + +/// Bring up the real server state over `root`, with `files` already indexed. +/// +/// The rows are written by the scanner's own path where that matters (see +/// [`scan_into`]); this is for tests that only need an item to exist. +pub async fn state_over(temp: &std::path::Path, root: &std::path::Path) -> AppState { + let database = Arc::new(SqliteDatabase::new(temp.join("library.db")).await.unwrap()); + database.initialize().await.unwrap(); + + let mut config = AppConfig::default(); + config.media.directories = vec![MonitoredDirectoryConfig { + path: root.to_string_lossy().into_owned(), + recursive: true, + case_sensitive: None, + extensions: None, + exclude_patterns: None, + validation_mode: ValidationMode::Skip, + }]; + let config = Arc::new(config); + + AppState { + media_directories: Arc::new(tokio::sync::RwLock::new(config.media.directories.clone())), + unavailable_roots: Arc::new(tokio::sync::RwLock::new(std::collections::HashSet::new())), + config: config.clone(), + config_source: Arc::new(Default::default()), + http_binding: Arc::new(vuio_core::state::HttpBinding::new(8080)), + live_config: Arc::new(vuio_core::state::LiveConfig::new(config)), + database, + auth: Arc::new(vuio_core::web::auth::AuthState::testing()), + platform_info: Arc::new(vuio_core::platform::PlatformInfo::detect().await.unwrap()), + filesystem_manager: Arc::from( + vuio_core::platform::filesystem::create_platform_filesystem_manager(), + ), + content_update_id: Arc::new(std::sync::atomic::AtomicU32::new(1)), + web_metrics: Arc::new(vuio_core::web::diagnostics::WebHandlerMetrics::new()), + runtime_diagnostics: Arc::new( + vuio_core::platform::diagnostics::SystemDiagnosticsSampler::new(), + ), + lifecycle_stats: Arc::new(vuio_core::lifecycle::ApplicationStats::new()), + bookmarks: Arc::new(tokio::sync::Mutex::new( + vuio_core::runtime_state::BookmarkRegistry::new( + vuio_core::runtime_state::BOOKMARK_MAX_ENTRIES, + ), + )), + log_file_path: temp.join("vuio.log"), + browse_cache: Arc::new(tokio::sync::Mutex::new( + vuio_core::runtime_state::BrowseResponseCache::new(), + )), + active_monitors: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + active_casts: Arc::new(tokio::sync::Mutex::new( + vuio_core::runtime_state::ActiveCastRegistry::new(), + )), + #[cfg(feature = "mediainfo")] + mediainfo_job: Arc::new(tokio::sync::Mutex::new(Default::default())), + #[cfg(feature = "casting")] + discovered_tvs: Arc::new(vuio_core::runtime_state::RendererCache::new()), + upnp_subscriptions: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), + radio: Arc::new(Default::default()), + #[cfg(feature = "transcode")] + transcode: Arc::new(Default::default()), + cancellation: tokio_util::sync::CancellationToken::new(), + background_tasks: tokio_util::task::TaskTracker::new(), + } +} + +/// Run the real scanner over the state's configured directories. +/// +/// Tests that assert on what the *scanner* records — a film's audio codec, say — +/// must go through it rather than injecting rows, because injecting rows is +/// exactly how a scanner that indexes nothing goes unnoticed. +pub async fn scan_into(state: &AppState) -> Vec { + let directories = state.media_directories.read().await.clone(); + let scanner = vuio_core::media::MediaScanner::with_database(state.database.clone()); + for directory in &directories { + scanner + .scan_directory(std::path::Path::new(&directory.path)) + .await + .unwrap(); + } + state.database.collect_all_media_files().await.unwrap() +} diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs new file mode 100644 index 00000000..12e710cf --- /dev/null +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -0,0 +1,978 @@ +//! A film with an audio track the television cannot decode. +//! +//! The shape of the problem phase 4 exists for: `Movie.mkv` with AC-3 inside it, +//! where the picture plays and nothing comes out of the speakers. Everything +//! here drives the real router over a real Matroska file built by +//! `common::build_mkv`, so what is asserted is what a renderer receives. + +#![cfg(all(feature = "transcode-ac3", feature = "casting"))] + +mod common; + +use common::{build_mkv, video_sample, Track, TrackKind, AVCC}; +use std::sync::Arc; +use tower::ServiceExt; +use vuio_core::database::MediaRepository; + +/// The vendored AC-3 conformance fixture: 48 kHz stereo, 440 Hz, 768-byte +/// frames of 1536 samples each. +const AC3: &[u8] = include_bytes!("../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); +const AC3_FRAME_LEN: usize = 768; +const AC3_FRAME_SAMPLES: u64 = 1536; +const AC3_FRAME_MS: f64 = AC3_FRAME_SAMPLES as f64 / 48.0; + +/// Build a film: `AC3` looped to fill `seconds`, beside a 25 fps video track +/// with a keyframe every second. +pub fn film(seconds: f64) -> Vec { + let frames: Vec<&[u8]> = AC3.chunks_exact(AC3_FRAME_LEN).collect(); + let audio_count = (seconds * 1000.0 / AC3_FRAME_MS).round() as usize; + let audio_samples: Vec<(u64, Vec)> = (0..audio_count) + .map(|i| { + ( + (i as f64 * AC3_FRAME_MS).round() as u64, + frames[i % frames.len()].to_vec(), + ) + }) + .collect(); + + let video_count = (seconds * 25.0).round() as usize; + let video_samples: Vec<(u64, Vec)> = (0..video_count) + .map(|i| { + let keyframe = i % 25 == 0; + ( + (i as f64 * 40.0).round() as u64, + video_sample(keyframe, 96, i as u8), + ) + }) + .collect(); + + build_mkv( + &[ + Track { + number: 1, + codec_id: "V_MPEG4/ISO/AVC", + codec_private: AVCC.to_vec(), + kind: TrackKind::Video { + width: 640, + height: 360, + }, + samples: video_samples, + all_keyframes: false, + is_default: true, + }, + Track { + number: 2, + codec_id: "A_AC3", + codec_private: Vec::new(), + kind: TrackKind::Audio { + sample_rate: 48_000.0, + channels: 2, + }, + samples: audio_samples, + all_keyframes: true, + is_default: true, + }, + ], + seconds * 1000.0, + ) +} + +#[test] +fn the_fixture_really_is_a_matroska_file_with_an_ac3_track() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("Film.mkv"); + std::fs::write(&path, film(12.0)).unwrap(); + + let info = vuio_core::media::remux::MkvDemuxer::inspect(&path).expect("probe the fixture"); + let duration = info + .duration_secs + .expect("the container declares a duration"); + assert!( + (duration - 12.0).abs() < 0.1, + "duration came back as {duration}" + ); + + use vuio_core::media::remux::{TrackCodec, TrackKind as K}; + let video = info + .tracks + .iter() + .find(|t| t.track_kind == K::Video) + .expect("a video track"); + assert_eq!(video.codec_kind, TrackCodec::Avc); + assert_eq!( + video.extra_data, AVCC, + "the avcC must survive the container unchanged" + ); + + let audio = info + .tracks + .iter() + .find(|t| t.track_kind == K::Audio) + .expect("an audio track"); + assert_eq!( + audio.codec_kind, + TrackCodec::Ac3, + "AC-3 must be named, not lumped in with Unsupported" + ); + assert_eq!(audio.sample_rate, Some(48_000)); +} + +// ── Step 1: what the scanner records ────────────────────────────────────── + +use axum::{ + body::Body, + extract::ConnectInfo, + http::{header, Method, Request, StatusCode}, +}; +use vuio_core::web::{create_router, Surface}; + +/// A scanned film, and the server over it. +async fn scanned_film(seconds: f64) -> (tempfile::TempDir, vuio_core::state::AppState, i64) { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(root.join("Film.mkv"), film(seconds)).unwrap(); + + let state = common::state_over(temp.path(), &root).await; + let files = common::scan_into(&state).await; + let film = files + .iter() + .find(|f| f.filename == "Film.mkv") + .expect("the scanner indexed the film"); + let id = film.id.unwrap(); + (temp, state, id) +} + +/// The database prerequisite everything else in phase 4 rests on: a film's audio +/// codec has to be a column, not something learned by opening the file, because +/// a folder of four hundred films would otherwise open four hundred files to +/// render one Browse response. +#[tokio::test] +async fn a_scanned_film_records_the_codec_of_its_audio_track() { + let (_temp, state, id) = scanned_film(6.0).await; + let file = state + .database + .get_file_by_id(id) + .await + .unwrap() + .expect("the film is in the database"); + + assert_eq!( + file.stream.codec.as_deref(), + Some("ac3"), + "symphonia identifies AC-3 perfectly well; it just cannot decode it, which used to leave this NULL" + ); + assert_eq!(file.stream.sample_rate, Some(48_000)); + assert!(file.mime_type.starts_with("video/"), "{}", file.mime_type); +} + +// ── Step 3: the film's soundtrack, decoded ──────────────────────────────── + +async fn get( + state: &vuio_core::state::AppState, + uri: &str, + method: Method, + range: Option<&str>, +) -> (StatusCode, axum::http::HeaderMap, Vec) { + let mut builder = Request::builder() + .method(method) + .uri(uri) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )); + if let Some(range) = range { + builder = builder.header(header::RANGE, range); + } + let response = create_router(state.clone(), Surface::Primary) + .oneshot(builder.body(Body::empty()).unwrap()) + .await + .unwrap(); + let status = response.status(); + let headers = response.headers().clone(); + let body = axum::body::to_bytes(response.into_body(), 256 * 1024 * 1024) + .await + .unwrap() + .to_vec(); + (status, headers, body) +} + +#[tokio::test] +async fn a_films_soundtrack_is_served_as_playable_wav() { + let (_temp, state, id) = scanned_film(6.0).await; + let (status, headers, body) = get( + &state, + &format!("/media/{id}/transcode/audio.wav"), + Method::GET, + None, + ) + .await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "audio/vnd.wave; codec=1"); + assert_eq!(&body[0..4], b"RIFF"); + assert_eq!(&body[8..12], b"WAVE"); + assert_eq!(u16::from_le_bytes([body[22], body[23]]), 2, "channels"); + assert_eq!( + u32::from_le_bytes([body[24], body[25], body[26], body[27]]), + 48_000, + "sample rate" + ); + assert_eq!( + body.len() as u64, + headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse::() + .unwrap(), + "the body must be exactly as long as the header promised" + ); + + // Six seconds of 48 kHz stereo, within a frame either way. + let payload = (body.len() - 44) as f64; + let seconds = payload / (48_000.0 * 4.0); + assert!( + (seconds - 6.0).abs() < 0.1, + "decoded {seconds:.3}s of audio from a six-second film" + ); + + // The fixture is a 440 Hz sine looped, so silence means the container path + // produced a correctly-shaped empty response instead of decoding anything. + let mut sum = 0f64; + for c in body[44..].as_chunks::<2>().0 { + let v = i16::from_le_bytes([c[0], c[1]]) as f64; + sum += v * v; + } + let rms = (sum / ((body.len() - 44) as f64 / 2.0)).sqrt(); + assert!(rms > 100.0, "the decoded soundtrack is silent (rms {rms})"); +} + +#[tokio::test] +async fn a_films_soundtrack_is_byte_range_seekable() { + let (_temp, state, id) = scanned_film(6.0).await; + let uri = format!("/media/{id}/transcode/audio.wav"); + let (_, headers, whole) = get(&state, &uri, Method::GET, None).await; + assert_eq!(headers[header::ACCEPT_RANGES], "bytes"); + + // Two seconds in, deliberately not on a frame boundary. + let start = 44 + 48_000 * 4 * 2 + 1234; + let end = start + 40_000; + let (status, headers, part) = get( + &state, + &uri, + Method::GET, + Some(&format!("bytes={start}-{end}")), + ) + .await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + headers[header::CONTENT_RANGE].to_str().unwrap(), + format!("bytes {start}-{end}/{}", whole.len()) + ); + assert_eq!(part.len(), end - start + 1); + + // A container seek lands by timestamp rather than by byte, so the samples + // are the same audio rather than the same bytes: compare energy, which a + // seek to the wrong place would not preserve. + let energy = |pcm: &[u8]| -> f64 { + let samples = pcm.as_chunks::<2>().0; + let sum: f64 = samples + .iter() + .map(|c| { + let v = i16::from_le_bytes([c[0], c[1]]) as f64; + v * v + }) + .sum(); + (sum / samples.len() as f64).sqrt() + }; + let here = energy(&part); + let there = energy(&whole[start..=end]); + assert!( + here > 100.0 && (here - there).abs() / there < 0.15, + "range RMS {here:.1} vs whole-decode RMS {there:.1} — the seek landed somewhere else, or produced silence" + ); +} + +#[tokio::test] +async fn head_promises_the_length_a_film_get_delivers() { + let (_temp, state, id) = scanned_film(6.0).await; + let uri = format!("/media/{id}/transcode/audio.wav"); + let (status, head_headers, head_body) = get(&state, &uri, Method::HEAD, None).await; + let (_, get_headers, get_body) = get(&state, &uri, Method::GET, None).await; + + assert_eq!(status, StatusCode::OK); + assert!(head_body.is_empty(), "HEAD carries no body"); + assert_eq!( + head_headers[header::CONTENT_LENGTH], + get_headers[header::CONTENT_LENGTH] + ); + assert_eq!( + get_body.len().to_string(), + head_headers[header::CONTENT_LENGTH].to_str().unwrap() + ); +} + +#[cfg(feature = "transcode-aac")] +#[tokio::test] +async fn a_films_soundtrack_is_also_available_as_aac() { + let (_temp, state, id) = scanned_film(4.0).await; + let (status, headers, body) = get( + &state, + &format!("/media/{id}/transcode/audio.aac"), + Method::GET, + None, + ) + .await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "audio/aac"); + assert!(!body.is_empty()); + assert_eq!(body[0], 0xFF, "ADTS syncword high byte"); + assert_eq!(body[1] & 0xF0, 0xF0, "ADTS syncword low nibble"); +} + +// ── Step 4: the browser path ────────────────────────────────────────────── + +/// Walk an ISO-BMFF byte string, yielding each top-level box's type and body. +fn boxes(data: &[u8]) -> Vec<(String, &[u8])> { + let mut out = Vec::new(); + let mut pos = 0usize; + while pos + 8 <= data.len() { + let size = u32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as usize; + let name = String::from_utf8_lossy(&data[pos + 4..pos + 8]).into_owned(); + if size < 8 || pos + size > data.len() { + break; + } + out.push((name, &data[pos + 8..pos + size])); + pos += size; + } + out +} + +/// Find the first box named `name` anywhere in `data`, descending into the +/// container boxes on the way. +fn find_box<'a>(data: &'a [u8], name: &str) -> Option<&'a [u8]> { + const CONTAINERS: &[&str] = &[ + "moov", "trak", "mdia", "minf", "stbl", "stsd", "mvex", "moof", "traf", "avc1", "hvc1", + "mp4a", + ]; + for (found, body) in boxes(data) { + if found == name { + return Some(body); + } + if CONTAINERS.contains(&found.as_str()) { + // `stsd` and the sample entries carry fixed preambles before their + // children; skipping into them is what makes `avcC` reachable. + let inner = match found.as_str() { + "stsd" => &body[8..], + "avc1" | "hvc1" => &body[78..], + "mp4a" => &body[28..], + _ => body, + }; + if let Some(hit) = find_box(inner, name) { + return Some(hit); + } + } + } + None +} + +#[tokio::test] +async fn the_master_playlist_offers_the_films_ac3_track_as_a_rendition() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, _, body) = get( + &state, + &format!("/media/{id}/hls/master.m3u8"), + Method::GET, + None, + ) + .await; + let playlist = String::from_utf8(body).unwrap(); + + assert_eq!(status, StatusCode::OK); + assert!( + playlist.contains("#EXT-X-MEDIA:TYPE=AUDIO"), + "the AC-3 track must be offered now that it can be decoded:\n{playlist}" + ); + assert!(playlist.contains("audio/0/index.m3u8"), "{playlist}"); + assert!( + playlist.contains("mp4a.40.2"), + "the rendition arrives as AAC-LC whatever the source was:\n{playlist}" + ); +} + +#[tokio::test] +async fn the_audio_init_segment_describes_aac_not_the_source_codec() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, _, init) = get( + &state, + &format!("/media/{id}/hls/audio/0/init.mp4"), + Method::GET, + None, + ) + .await; + assert_eq!(status, StatusCode::OK); + + let kinds: Vec = boxes(&init).into_iter().map(|(name, _)| name).collect(); + assert_eq!( + kinds, + vec!["ftyp", "moov"], + "an init segment is ftyp + moov" + ); + + assert!( + find_box(&init, "mp4a").is_some(), + "a browser initialises an AAC decoder from an mp4a entry, not an ac-3 one" + ); + let esds = find_box(&init, "esds").expect("an esds carrying the AudioSpecificConfig"); + // The ES_Descriptor tree ends in a DecoderSpecificInfo (tag 0x05) holding + // the raw config; the two bytes there must be the ones the encoder's own + // ADTS headers declare, or the browser decodes at the wrong rate. + let asc_at = esds + .windows(2) + .position(|w| w[0] == 0x05 && w[1] == 2) + .expect("a two-byte DecoderSpecificInfo"); + let asc = &esds[asc_at + 2..asc_at + 4]; + assert_eq!(asc[0] >> 3, 2, "audioObjectType 2 is AAC-LC"); + assert_eq!( + ((asc[0] & 0x07) << 1) | (asc[1] >> 7), + 3, + "samplingFrequencyIndex 3 is 48 kHz" + ); + assert_eq!((asc[1] >> 3) & 0x0F, 2, "two channels"); +} + +#[tokio::test] +async fn an_audio_segment_carries_real_re_encoded_aac() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, _, segment) = get( + &state, + &format!("/media/{id}/hls/audio/0/segment/1"), + Method::GET, + None, + ) + .await; + assert_eq!(status, StatusCode::OK); + + let kinds: Vec = boxes(&segment).into_iter().map(|(name, _)| name).collect(); + assert_eq!( + kinds, + vec!["moof", "mdat"], + "a media segment is moof + mdat" + ); + + let trun = find_box(&segment, "trun").expect("a trun"); + let sample_count = u32::from_be_bytes(trun[4..8].try_into().unwrap()); + assert!( + sample_count > 150, + "four seconds at 1024 samples a frame is ~187 frames, got {sample_count}" + ); + + let mdat = boxes(&segment) + .into_iter() + .find(|(name, _)| name == "mdat") + .unwrap() + .1; + assert!(!mdat.is_empty(), "the segment carries samples"); + // The ADTS headers must be gone: an MP4 sample that begins with a syncword + // is a header the decoder will read as spectral data. + assert!( + !(mdat[0] == 0xFF && mdat[1] & 0xF0 == 0xF0), + "ADTS framing leaked into an MP4 sample" + ); + + // The second segment begins four seconds in, one AAC frame early to cancel + // the encoder's delay. + let tfdt = find_box(&segment, "tfdt").expect("a tfdt"); + let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); + assert_eq!( + base, + 4 * 48_000 - 1024, + "the run must sit one frame before the segment boundary" + ); +} + +#[tokio::test] +async fn a_video_segment_still_passes_the_picture_through_untouched() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, _, init) = get( + &state, + &format!("/media/{id}/hls/video/init.mp4"), + Method::GET, + None, + ) + .await; + let avcc = find_box(&init, "avcC").expect("an avcC in the video init segment"); + assert_eq!( + avcc, AVCC, + "the decoder configuration must be the source's, byte for byte" + ); + + let (status, _, segment) = get( + &state, + &format!("/media/{id}/hls/video/segment/0"), + Method::GET, + None, + ) + .await; + assert_eq!(status, StatusCode::OK); + let trun = find_box(&segment, "trun").expect("a trun"); + let sample_count = u32::from_be_bytes(trun[4..8].try_into().unwrap()); + assert!( + (95..=105).contains(&sample_count), + "four seconds at 25 fps is ~100 frames, got {sample_count}" + ); +} + +/// A rebuilt segment costs a decode; the same segment asked for twice must not. +#[tokio::test] +async fn a_segment_asked_for_twice_is_only_built_once() { + let (_temp, state, id) = scanned_film(8.0).await; + let uri = format!("/media/{id}/hls/audio/0/segment/0"); + let (_, _, first) = get(&state, &uri, Method::GET, None).await; + let (_, _, second) = get(&state, &uri, Method::GET, None).await; + assert_eq!(first, second, "a cached segment must be the same bytes"); + assert!(!first.is_empty()); +} + +// ── Step 5: the film itself, remuxed ────────────────────────────────────── + +async fn video_mp4( + state: &vuio_core::state::AppState, + id: i64, + method: Method, + time_seek: Option<&str>, +) -> (StatusCode, axum::http::HeaderMap, Vec) { + let mut builder = Request::builder() + .method(method) + .uri(format!("/media/{id}/transcode/video.mp4")) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )); + if let Some(npt) = time_seek { + builder = builder.header("TimeSeekRange.dlna.org", npt); + } + let response = create_router(state.clone(), Surface::Primary) + .oneshot(builder.body(Body::empty()).unwrap()) + .await + .unwrap(); + let status = response.status(); + let headers = response.headers().clone(); + let body = axum::body::to_bytes(response.into_body(), 256 * 1024 * 1024) + .await + .unwrap() + .to_vec(); + (status, headers, body) +} + +#[tokio::test] +async fn the_remuxed_film_is_a_parseable_fmp4_with_both_tracks() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, headers, body) = video_mp4(&state, id, Method::GET, None).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "video/mp4"); + let features = headers["contentFeatures.dlna.org"].to_str().unwrap(); + assert!(features.contains("DLNA.ORG_CI=1"), "{features}"); + assert!( + features.contains("DLNA.ORG_OP=01"), + "time seek yes, byte seek no: {features}" + ); + assert!( + !headers.contains_key(header::ACCEPT_RANGES), + "claiming byte ranges and then refusing them is worse than never claiming" + ); + assert!( + !headers.contains_key(header::CONTENT_LENGTH), + "the length of this resource is not knowable before it exists" + ); + + let top: Vec = boxes(&body).into_iter().map(|(name, _)| name).collect(); + assert_eq!(&top[..2], &["ftyp", "moov"], "an init segment comes first"); + let fragments = top[2..].chunks(2).collect::>(); + assert!( + fragments.len() >= 2, + "expected several moof/mdat pairs, got {top:?}" + ); + for pair in &fragments { + assert_eq!(pair, &["moof", "mdat"], "in {top:?}"); + } + + // Two tracks, and the picture must arrive as the picture: the source's own + // decoder configuration record, byte for byte. That is what "passthrough" + // means, and the whole reason the CPU cost is bounded. + let moov = boxes(&body) + .into_iter() + .find(|(name, _)| name == "moov") + .unwrap() + .1; + let traks = boxes(moov).iter().filter(|(n, _)| n == "trak").count(); + assert_eq!(traks, 2, "one video track and one audio track"); + assert_eq!( + find_box(&body, "avcC"), + Some(AVCC), + "the video track is copied, not re-encoded" + ); + assert!( + find_box(&body, "mp4a").is_some(), + "the AC-3 track must arrive as AAC — nothing else would play" + ); + // `mehd` is what a fragmented file carries its total duration in, and what a + // renderer with no Content-Length draws a scrub bar from. + let mehd = find_box(&body, "mehd").expect("a mehd declaring the film's length"); + let duration_ms = u64::from_be_bytes(mehd[4..12].try_into().unwrap()); + assert!( + (7_900..=8_100).contains(&duration_ms), + "mehd says {duration_ms} ms for an eight-second film" + ); +} + +#[tokio::test] +async fn a_head_describes_the_film_without_decoding_it() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, headers, body) = video_mp4(&state, id, Method::HEAD, None).await; + assert_eq!(status, StatusCode::OK); + assert!(body.is_empty()); + assert_eq!(headers[header::CONTENT_TYPE], "video/mp4"); + assert_eq!( + headers["X-Content-Duration"] + .to_str() + .unwrap() + .parse::() + .unwrap() + .round(), + 8.0 + ); +} + +/// The requirement that separates this from a stream you can only watch from the +/// beginning: a television must be able to scrub a film whose audio had to be +/// re-encoded, exactly as it can one whose audio it could already play. +#[tokio::test] +async fn a_time_seek_starts_the_film_where_it_was_asked_to() { + let (_temp, state, id) = scanned_film(12.0).await; + let (status, headers, body) = video_mp4(&state, id, Method::GET, Some("npt=6.000-")).await; + + assert_eq!( + status, + StatusCode::PARTIAL_CONTENT, + "a time-seek request is answered as partial content" + ); + let seek = headers["TimeSeekRange.dlna.org"].to_str().unwrap(); + assert!( + seek.starts_with("npt=6.000-") && seek.ends_with("/12.000"), + "the response must state the range it is answering: {seek}" + ); + + // A seek is a fresh stream: init segment, then fragments — and the + // fragments carry the real timeline, so the renderer's position is right. + let top: Vec = boxes(&body).into_iter().map(|(name, _)| name).collect(); + assert_eq!(&top[..2], &["ftyp", "moov"]); + + let tfdt = find_box(&body, "tfdt").expect("a tfdt in the first fragment"); + let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); + let seconds = base as f64 / 90_000.0; + assert!( + (5.0..=6.1).contains(&seconds), + "the first fragment starts at {seconds:.3}s — a seek to 6s must land on the keyframe at or before it, never after" + ); + + // And it must be shorter than the whole film, or the seek did nothing. + let (_, _, whole) = video_mp4(&state, id, Method::GET, None).await; + assert!( + body.len() < whole.len(), + "seeking to the middle produced {} bytes against {} for the whole film", + body.len(), + whole.len() + ); +} + +#[tokio::test] +async fn the_same_seek_expressed_as_a_clock_time_lands_in_the_same_place() { + let (_temp, state, id) = scanned_film(12.0).await; + let (_, _, decimal) = video_mp4(&state, id, Method::GET, Some("npt=6.000-")).await; + let (_, _, clock) = video_mp4(&state, id, Method::GET, Some("npt=0:00:06.000-")).await; + assert_eq!( + decimal, clock, + "npt=6.000 and npt=0:00:06.000 are the same instant" + ); +} + +#[tokio::test] +async fn a_seek_past_the_end_is_clamped_rather_than_refused() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, _, body) = video_mp4(&state, id, Method::GET, Some("npt=600.0-")).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + // Still a well-formed file, even if it carries almost nothing. + let top: Vec = boxes(&body).into_iter().map(|(name, _)| name).collect(); + assert_eq!(&top[..2], &["ftyp", "moov"]); +} + +/// A film with several soundtracks: the one the container marks default is the +/// one carried. The output track keeps the source's track number, so the `tkhd` +/// in the init segment says which was chosen. +#[tokio::test] +async fn a_multi_audio_film_carries_the_track_the_container_marks_default() { + for default_track in [2u64, 3] { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(root.join("Film.mkv"), multi_audio_film(6.0, default_track)).unwrap(); + + let state = common::state_over(temp.path(), &root).await; + let id = common::scan_into(&state) + .await + .iter() + .find(|f| f.filename == "Film.mkv") + .unwrap() + .id + .unwrap(); + + let (status, _, body) = video_mp4(&state, id, Method::GET, None).await; + assert_eq!(status, StatusCode::OK); + + let moov = boxes(&body) + .into_iter() + .find(|(name, _)| name == "moov") + .unwrap() + .1; + let audio_trak = boxes(moov) + .into_iter() + .filter(|(name, _)| name == "trak") + .find(|(_, body)| find_box(body, "mp4a").is_some()) + .expect("an audio track") + .1; + let tkhd = find_box(audio_trak, "tkhd").expect("a tkhd"); + // version(1) + flags(3) + creation(4) + modification(4), then track_id. + let track_id = u32::from_be_bytes(tkhd[12..16].try_into().unwrap()); + assert_eq!( + u64::from(track_id), + default_track, + "the default-marked track must be the one carried" + ); + } +} + +/// A film with two AC-3 soundtracks, one of which the container marks default. +fn multi_audio_film(seconds: f64, default_track: u64) -> Vec { + let frames: Vec<&[u8]> = AC3.chunks_exact(AC3_FRAME_LEN).collect(); + let audio_count = (seconds * 1000.0 / AC3_FRAME_MS).round() as usize; + let audio_samples = |offset: usize| -> Vec<(u64, Vec)> { + (0..audio_count) + .map(|i| { + ( + (i as f64 * AC3_FRAME_MS).round() as u64, + frames[(i + offset) % frames.len()].to_vec(), + ) + }) + .collect() + }; + let video_count = (seconds * 25.0).round() as usize; + let video_samples: Vec<(u64, Vec)> = (0..video_count) + .map(|i| { + ( + (i as f64 * 40.0).round() as u64, + video_sample(i % 25 == 0, 96, i as u8), + ) + }) + .collect(); + + let audio = |number: u64, offset: usize| Track { + number, + codec_id: "A_AC3", + codec_private: Vec::new(), + kind: TrackKind::Audio { + sample_rate: 48_000.0, + channels: 2, + }, + samples: audio_samples(offset), + all_keyframes: true, + is_default: number == default_track, + }; + + build_mkv( + &[ + Track { + number: 1, + codec_id: "V_MPEG4/ISO/AVC", + codec_private: AVCC.to_vec(), + kind: TrackKind::Video { + width: 640, + height: 360, + }, + samples: video_samples, + all_keyframes: false, + is_default: true, + }, + audio(2, 0), + audio(3, 1), + ], + seconds * 1000.0, + ) +} + +#[tokio::test] +async fn a_film_advertises_the_remuxed_film_and_not_its_soundtrack() { + let (_temp, state, id) = scanned_film(8.0).await; + let didl = browse_didl(&state).await; + + assert!( + didl.contains(&format!("/media/{id}/transcode/video.mp4")), + "a film's alternative is the film:\n{didl}" + ); + assert!( + !didl.contains("transcode/audio.wav"), + "offering a film's soundtrack in place of the film would lose the picture:\n{didl}" + ); + assert!( + didl.contains("DLNA.ORG_OP=01;DLNA.ORG_CI=1"), + "the advertised operations must be the ones the resource honours:\n{didl}" + ); + // The original stays, and stays first by default, so a television that can + // decode AC-3 keeps its byte-seekable direct-play resource. + let original = didl + .find(&format!("/media/{id}")) + .unwrap_or_else(|| panic!("no direct-play resource in:\n{didl}")); + let transcoded = didl.find("transcode/video.mp4").unwrap(); + assert!( + original < transcoded, + "the original is listed first:\n{didl}" + ); +} + +/// Needing an alternative and being able to produce one are different +/// questions, and this is the one the *video* track answers. A film whose +/// picture cannot be copied through gets no second resource: advertising one +/// and then answering 404 is worse than the silence it was meant to fix. +#[tokio::test] +async fn a_film_whose_picture_cannot_be_copied_is_not_offered_an_alternative() { + let (_temp, state, id) = scanned_film(4.0).await; + + // Rewrite the recorded video codec to one the fMP4 writer cannot describe. + // The audio is untouched, so this is a film that still *needs* a decoded + // alternative and simply cannot be given one. + let mut file = state.database.get_file_by_id(id).await.unwrap().unwrap(); + assert_eq!(file.stream.video_codec.as_deref(), Some("h264")); + file.stream.video_codec = Some("vp9".into()); + state + .database + .bulk_update_media_files(&[file]) + .await + .unwrap(); + + let didl = browse_didl(&state).await; + assert!( + !didl.contains("transcode/"), + "a VP9 picture cannot be passed through, so nothing may be advertised:\n{didl}" + ); +} + +#[tokio::test] +async fn a_scanned_film_records_the_codec_of_its_video_track_too() { + let (_temp, state, id) = scanned_film(4.0).await; + let file = state.database.get_file_by_id(id).await.unwrap().unwrap(); + assert_eq!(file.stream.video_codec.as_deref(), Some("h264")); + assert_eq!( + file.stream.codec.as_deref(), + Some("ac3"), + "the audio codec is the one that decides an alternative is needed" + ); +} + +#[tokio::test] +async fn with_the_feature_off_a_film_has_exactly_one_resource() { + let (_temp, mut state, _) = scanned_film(4.0).await; + let mut config = (*state.config).clone(); + config.transcode.enabled = false; + let config = Arc::new(config); + state.config = config.clone(); + state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); + + let didl = browse_didl(&state).await; + assert!(!didl.contains("transcode/"), "{didl}"); +} + +/// Browse the root folder as a television would, and return the DIDL. +async fn browse_didl(state: &vuio_core::state::AppState) -> String { + let body = r#" + + +videoBrowseDirectChildren +*050 +"#; + let request = Request::builder() + .method(Method::POST) + .uri("/control/ContentDirectory") + .header(header::CONTENT_TYPE, "text/xml") + .header( + "SOAPAction", + "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"", + ) + .header(header::USER_AGENT, "SEC_HHP_Samsung TV") + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(Body::from(body)) + .unwrap(); + let response = create_router(state.clone(), Surface::Primary) + .oneshot(request) + .await + .unwrap(); + let bytes = axum::body::to_bytes(response.into_body(), 8 * 1024 * 1024) + .await + .unwrap(); + String::from_utf8_lossy(&bytes) + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") +} + +/// What the video probe adds to a scan. +/// +/// Ignored by default: it is a measurement, not an assertion, and the number it +/// prints belongs in a commit message rather than in a pass/fail. Run with +/// `cargo test -- --ignored measure_probe_cost --nocapture`. +#[test] +#[ignore] +fn measure_probe_cost() { + use std::time::Instant; + + let dir = tempfile::tempdir().unwrap(); + let short = dir.path().join("Short.mkv"); + std::fs::write(&short, film(12.0)).unwrap(); + let long = dir.path().join("Long.mkv"); + std::fs::write(&long, film(7200.0)).unwrap(); + + for (name, path) in [("12s", &short), ("2h", &long)] { + let size = std::fs::metadata(path).unwrap().len(); + // Warm the page cache first, so this measures parsing rather than the + // first read of a cold file. + let _ = vuio_core::media::remux::MkvDemuxer::inspect(path); + let started = Instant::now(); + const RUNS: u32 = 100; + for _ in 0..RUNS { + let _ = vuio_core::media::remux::MkvDemuxer::inspect(path); + } + let each = started.elapsed() / RUNS; + eprintln!("{name} film ({size} bytes): {each:?} per probe"); + } +} + +/// Writes the fixture where it can be inspected by hand. Ignored by default — +/// it exists so `cargo test -- --ignored dump_fixture` produces a file to open +/// in ffprobe or mpv when this writer is being changed. +#[test] +#[ignore] +fn dump_fixture() { + let out = std::env::var("VUIO_FIXTURE_OUT").unwrap_or_else(|_| "/tmp/vuio-film.mkv".into()); + std::fs::write(&out, film(12.0)).unwrap(); + eprintln!("wrote {out}"); +} + +#[test] +#[ignore] +fn dump_long_fixture() { + let out = std::env::var("VUIO_FIXTURE_OUT").unwrap_or_else(|_| "/tmp/vuio-long.mkv".into()); + std::fs::write(&out, film(7200.0)).unwrap(); + eprintln!("wrote {out}"); +} diff --git a/docs/configuration.md b/docs/configuration.md index 66c144c2..4dbc16cf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -140,14 +140,45 @@ allowed_networks = [] # Allowed CIDR blocks (empty restricts to private/loo ### `[transcode]` -Audio for renderers that cannot decode AC-3, Dolby Digital Plus or DTS. Those +Sound for renderers that cannot decode AC-3, Dolby Digital Plus or DTS. Those codecs are licensed, and a television sold without the licence plays the picture and nothing else. When this is on, an item whose audio is one of the three is listed **twice** in -the browse response — the file as stored, and a decoded alternative at -`/media/{id}/transcode/audio.wav`. Both are offered and the renderer picks the -one it can play. A renderer that was already fine is unaffected. +the browse response — the file as stored, and an alternative the renderer can +decode. Both are offered and the renderer picks. A renderer that was already +fine is unaffected. + +What the alternative is depends on what the item is: + +| Item | Alternative | Seeking | +| --- | --- | --- | +| A standalone `.ac3`/`.eac3`/`.dts` file, or an album track | `/media/{id}/transcode/audio.wav` (or `audio.aac`) | Byte ranges for LPCM; none for AAC | +| A film — `Movie.mkv` with an AC-3 or DTS track | `/media/{id}/transcode/video.mp4` | By time, via `TimeSeekRange.dlna.org` | + +The film case is the common one, and the one worth understanding. The picture is +**copied through untouched** — nothing re-encodes video, which is what keeps the +CPU cost proportional to the soundtrack and the image bit-identical to the +source. Only the audio track is decoded and re-encoded, as AAC, into a fragmented +MP4 streamed as it is produced. + +That resource carries no `Content-Length`, because its length cannot be known +before it exists, so it declares `DLNA.ORG_OP=01`: **byte seeking no, time +seeking yes**. A renderer scrubs it by sending `TimeSeekRange.dlna.org: npt=…`, +which VuIO answers with a fresh stream starting at the keyframe at or before the +requested moment. That mechanism does not depend on the audio codec at all, so a +film whose soundtrack had to be re-encoded scrubs exactly like one whose +soundtrack the television could already play. + +A film is only offered the alternative when its picture can be copied — that is, +when the video track is H.264 or HEVC. One with, say, a VP9 or MPEG-2 picture is +left with its single original resource rather than being pointed at a URL that +would answer `404`. + +In the browser, the same decoding happens inside the built-in player: an MKV's +AC-3 or DTS track now appears as a selectable audio rendition in the HLS +playlist instead of being dropped, which is what used to make a film play +silently in a tab. ```toml [transcode] @@ -162,7 +193,11 @@ max_concurrent = 2 # Simultaneous decodes; further requests are refused, not | `enabled` | Live. Off leaves the browse response exactly as it was. | | `audio_format` | `lpcm` is uncompressed, carries an exact `Content-Length` and supports byte-range seeking, and costs about 1.5 Mbps. `aac` is roughly a tenth of that, at the price of a lossy re-encode, no `Content-Length` and no scrubbing. | | `prefer` | Some renderers take the first resource without checking whether they can decode it. The default keeps the original first, which cannot make anything worse. Switch to `transcoded` if a set still plays silently. | -| `max_concurrent` | Decoding is the only CPU-bound work this server does. Past this ceiling a request gets `503` with `Retry-After` rather than joining a queue that would starve the streams already playing. | +| `max_concurrent` | Decoding is the only CPU-bound work this server does. Past this ceiling a request gets `503` with `Retry-After` rather than joining a queue that would starve the streams already playing. The browser's HLS renditions draw on the same ceiling, so one number bounds the machine rather than one per delivery path. | + +`audio_format` applies to audio items only. A film's alternative is always +fragmented MP4 with an AAC track, because that is what a television will play +back as a film. Environment variables: `VUIO_TRANSCODE_ENABLED`, `VUIO_TRANSCODE_AUDIO_FORMAT`, `VUIO_TRANSCODE_PREFER`, `VUIO_TRANSCODE_MAX_CONCURRENT`. From 87ce1d304d009a706c5fdea6b907afaba6738060 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 18:21:31 +0300 Subject: [PATCH 12/38] u --- Cargo.toml | 9 +- crates/vendor/oxideav-aac/Cargo.toml | 27 - crates/vendor/oxideav-aac/LICENSE | 21 - crates/vendor/oxideav-aac/README.md | 1252 ---- crates/vendor/oxideav-aac/VENDOR.toml | 9 - crates/vendor/oxideav-aac/src/adts.rs | 289 - crates/vendor/oxideav-aac/src/adts_crc.rs | 566 -- crates/vendor/oxideav-aac/src/asc.rs | 819 --- crates/vendor/oxideav-aac/src/bsac_arith.rs | 413 -- crates/vendor/oxideav-aac/src/bsac_decode.rs | 861 --- crates/vendor/oxideav-aac/src/bsac_layer.rs | 499 -- crates/vendor/oxideav-aac/src/bsac_tables.rs | 1020 --- crates/vendor/oxideav-aac/src/cce.rs | 1289 ---- crates/vendor/oxideav-aac/src/channel_map.rs | 751 --- .../vendor/oxideav-aac/src/codec_decoder.rs | 972 --- .../vendor/oxideav-aac/src/codec_encoder.rs | 333 - crates/vendor/oxideav-aac/src/crc.rs | 449 -- crates/vendor/oxideav-aac/src/decode.rs | 1305 ---- .../oxideav-aac/src/decoded_spectrum.rs | 353 -- crates/vendor/oxideav-aac/src/dequant.rs | 476 -- .../vendor/oxideav-aac/src/element_decode.rs | 1482 ----- crates/vendor/oxideav-aac/src/encoder.rs | 2991 --------- crates/vendor/oxideav-aac/src/encoder_tns.rs | 421 -- crates/vendor/oxideav-aac/src/ep_config.rs | 586 -- crates/vendor/oxideav-aac/src/ep_fec.rs | 652 -- crates/vendor/oxideav-aac/src/ep_frame.rs | 1390 ---- crates/vendor/oxideav-aac/src/ep_rs.rs | 434 -- crates/vendor/oxideav-aac/src/error.rs | 1244 ---- .../oxideav-aac/src/extension_payload.rs | 856 --- crates/vendor/oxideav-aac/src/filterbank.rs | 1549 ----- crates/vendor/oxideav-aac/src/gain_control.rs | 907 --- .../oxideav-aac/src/gain_control_data.rs | 305 - crates/vendor/oxideav-aac/src/hcr.rs | 677 -- crates/vendor/oxideav-aac/src/hcr_decode.rs | 777 --- crates/vendor/oxideav-aac/src/ics_body.rs | 837 --- crates/vendor/oxideav-aac/src/ics_info.rs | 1100 ---- .../oxideav-aac/src/intensity_stereo.rs | 621 -- crates/vendor/oxideav-aac/src/ipqf.rs | 298 - crates/vendor/oxideav-aac/src/latm.rs | 1902 ------ crates/vendor/oxideav-aac/src/lib.rs | 641 -- crates/vendor/oxideav-aac/src/ltp.rs | 976 --- crates/vendor/oxideav-aac/src/ms_stereo.rs | 684 -- crates/vendor/oxideav-aac/src/pce.rs | 440 -- crates/vendor/oxideav-aac/src/pcm.rs | 198 - crates/vendor/oxideav-aac/src/pns.rs | 701 --- crates/vendor/oxideav-aac/src/predictor.rs | 752 --- crates/vendor/oxideav-aac/src/ps_data.rs | 705 --- crates/vendor/oxideav-aac/src/ps_decoder.rs | 307 - crates/vendor/oxideav-aac/src/ps_decorr.rs | 519 -- crates/vendor/oxideav-aac/src/ps_huffman.rs | 472 -- crates/vendor/oxideav-aac/src/ps_hybrid.rs | 508 -- crates/vendor/oxideav-aac/src/ps_map.rs | 282 - crates/vendor/oxideav-aac/src/ps_stereo.rs | 541 -- crates/vendor/oxideav-aac/src/pulse_data.rs | 190 - .../vendor/oxideav-aac/src/raw_data_block.rs | 666 -- crates/vendor/oxideav-aac/src/rvlc.rs | 407 -- crates/vendor/oxideav-aac/src/sbr_decoder.rs | 1111 ---- crates/vendor/oxideav-aac/src/sbr_dequant.rs | 259 - crates/vendor/oxideav-aac/src/sbr_element.rs | 561 -- .../vendor/oxideav-aac/src/sbr_env_adjust.rs | 1029 --- crates/vendor/oxideav-aac/src/sbr_envelope.rs | 410 -- .../vendor/oxideav-aac/src/sbr_extension.rs | 493 -- .../vendor/oxideav-aac/src/sbr_freq_bands.rs | 753 --- crates/vendor/oxideav-aac/src/sbr_grid.rs | 464 -- crates/vendor/oxideav-aac/src/sbr_header.rs | 350 -- crates/vendor/oxideav-aac/src/sbr_hf_gen.rs | 556 -- crates/vendor/oxideav-aac/src/sbr_huffman.rs | 1011 --- crates/vendor/oxideav-aac/src/sbr_limiter.rs | 170 - crates/vendor/oxideav-aac/src/sbr_lp.rs | 331 - .../vendor/oxideav-aac/src/sbr_noise_table.rs | 564 -- crates/vendor/oxideav-aac/src/sbr_qmf.rs | 1005 --- .../vendor/oxideav-aac/src/sbr_reconstruct.rs | 414 -- .../vendor/oxideav-aac/src/sbr_time_grid.rs | 340 - crates/vendor/oxideav-aac/src/scalable.rs | 1541 ----- .../oxideav-aac/src/scale_factor_data.rs | 1579 ----- crates/vendor/oxideav-aac/src/section_data.rs | 631 -- .../oxideav-aac/src/spectral_codebook.rs | 543 -- .../vendor/oxideav-aac/src/spectral_data.rs | 927 --- .../oxideav-aac/src/spectrum_huffman.rs | 5593 ----------------- crates/vendor/oxideav-aac/src/ssr.rs | 549 -- .../vendor/oxideav-aac/src/ssr_filterbank.rs | 454 -- crates/vendor/oxideav-aac/src/swb_offset.rs | 1418 ----- crates/vendor/oxideav-aac/src/tns_coef.rs | 1239 ---- crates/vendor/oxideav-aac/src/tns_data.rs | 480 -- crates/vendor/oxideav-aac/src/tns_frame.rs | 1035 --- crates/vendor/oxideav-aac/src/tns_max.rs | 750 --- crates/vuio-bench/Cargo.toml | 10 +- crates/vuio-bench/src/aac_bench.rs | 504 ++ crates/vuio-core/Cargo.toml | 4 +- crates/vuio-core/src/media/transcode/aac.rs | 85 +- scripts/vendor-oxideav.sh | 3 +- 91 files changed, 562 insertions(+), 66335 deletions(-) delete mode 100644 crates/vendor/oxideav-aac/Cargo.toml delete mode 100644 crates/vendor/oxideav-aac/LICENSE delete mode 100644 crates/vendor/oxideav-aac/README.md delete mode 100644 crates/vendor/oxideav-aac/VENDOR.toml delete mode 100644 crates/vendor/oxideav-aac/src/adts.rs delete mode 100644 crates/vendor/oxideav-aac/src/adts_crc.rs delete mode 100644 crates/vendor/oxideav-aac/src/asc.rs delete mode 100644 crates/vendor/oxideav-aac/src/bsac_arith.rs delete mode 100644 crates/vendor/oxideav-aac/src/bsac_decode.rs delete mode 100644 crates/vendor/oxideav-aac/src/bsac_layer.rs delete mode 100644 crates/vendor/oxideav-aac/src/bsac_tables.rs delete mode 100644 crates/vendor/oxideav-aac/src/cce.rs delete mode 100644 crates/vendor/oxideav-aac/src/channel_map.rs delete mode 100644 crates/vendor/oxideav-aac/src/codec_decoder.rs delete mode 100644 crates/vendor/oxideav-aac/src/codec_encoder.rs delete mode 100644 crates/vendor/oxideav-aac/src/crc.rs delete mode 100644 crates/vendor/oxideav-aac/src/decode.rs delete mode 100644 crates/vendor/oxideav-aac/src/decoded_spectrum.rs delete mode 100644 crates/vendor/oxideav-aac/src/dequant.rs delete mode 100644 crates/vendor/oxideav-aac/src/element_decode.rs delete mode 100644 crates/vendor/oxideav-aac/src/encoder.rs delete mode 100644 crates/vendor/oxideav-aac/src/encoder_tns.rs delete mode 100644 crates/vendor/oxideav-aac/src/ep_config.rs delete mode 100644 crates/vendor/oxideav-aac/src/ep_fec.rs delete mode 100644 crates/vendor/oxideav-aac/src/ep_frame.rs delete mode 100644 crates/vendor/oxideav-aac/src/ep_rs.rs delete mode 100644 crates/vendor/oxideav-aac/src/error.rs delete mode 100644 crates/vendor/oxideav-aac/src/extension_payload.rs delete mode 100644 crates/vendor/oxideav-aac/src/filterbank.rs delete mode 100644 crates/vendor/oxideav-aac/src/gain_control.rs delete mode 100644 crates/vendor/oxideav-aac/src/gain_control_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/hcr.rs delete mode 100644 crates/vendor/oxideav-aac/src/hcr_decode.rs delete mode 100644 crates/vendor/oxideav-aac/src/ics_body.rs delete mode 100644 crates/vendor/oxideav-aac/src/ics_info.rs delete mode 100644 crates/vendor/oxideav-aac/src/intensity_stereo.rs delete mode 100644 crates/vendor/oxideav-aac/src/ipqf.rs delete mode 100644 crates/vendor/oxideav-aac/src/latm.rs delete mode 100644 crates/vendor/oxideav-aac/src/lib.rs delete mode 100644 crates/vendor/oxideav-aac/src/ltp.rs delete mode 100644 crates/vendor/oxideav-aac/src/ms_stereo.rs delete mode 100644 crates/vendor/oxideav-aac/src/pce.rs delete mode 100644 crates/vendor/oxideav-aac/src/pcm.rs delete mode 100644 crates/vendor/oxideav-aac/src/pns.rs delete mode 100644 crates/vendor/oxideav-aac/src/predictor.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_decoder.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_decorr.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_huffman.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_hybrid.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_map.rs delete mode 100644 crates/vendor/oxideav-aac/src/ps_stereo.rs delete mode 100644 crates/vendor/oxideav-aac/src/pulse_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/raw_data_block.rs delete mode 100644 crates/vendor/oxideav-aac/src/rvlc.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_decoder.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_dequant.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_element.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_env_adjust.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_envelope.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_extension.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_freq_bands.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_grid.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_header.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_hf_gen.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_huffman.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_limiter.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_lp.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_noise_table.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_qmf.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_reconstruct.rs delete mode 100644 crates/vendor/oxideav-aac/src/sbr_time_grid.rs delete mode 100644 crates/vendor/oxideav-aac/src/scalable.rs delete mode 100644 crates/vendor/oxideav-aac/src/scale_factor_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/section_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/spectral_codebook.rs delete mode 100644 crates/vendor/oxideav-aac/src/spectral_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/spectrum_huffman.rs delete mode 100644 crates/vendor/oxideav-aac/src/ssr.rs delete mode 100644 crates/vendor/oxideav-aac/src/ssr_filterbank.rs delete mode 100644 crates/vendor/oxideav-aac/src/swb_offset.rs delete mode 100644 crates/vendor/oxideav-aac/src/tns_coef.rs delete mode 100644 crates/vendor/oxideav-aac/src/tns_data.rs delete mode 100644 crates/vendor/oxideav-aac/src/tns_frame.rs delete mode 100644 crates/vendor/oxideav-aac/src/tns_max.rs create mode 100644 crates/vuio-bench/src/aac_bench.rs diff --git a/Cargo.toml b/Cargo.toml index aeaf9861..94d52f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ members = [ "crates/vendor/oxideav-core", "crates/vendor/oxideav-ac3", "crates/vendor/oxideav-dts", - "crates/vendor/oxideav-aac", ] # `vuio-bench` is deliberately not a default member. Cargo unifies features across @@ -33,18 +32,14 @@ default-members = [ ] resolver = "2" -# The vendored codecs are the only CPU-bound code in the tree, and an -# unoptimized AAC encoder turns a four-second test segment into a minute of -# wall clock. Optimizing those four packages alone leaves `cargo build` on the -# code actually being worked on as fast as it was. +# The vendored codecs are the only CPU-bound code in the tree. Optimizing those +# packages alone leaves `cargo build` on the code actually being worked on as fast as it was. [profile.dev.package.oxideav-core] opt-level = 2 [profile.dev.package.oxideav-ac3] opt-level = 2 [profile.dev.package.oxideav-dts] opt-level = 2 -[profile.dev.package.oxideav-aac] -opt-level = 2 [profile.release] opt-level = 3 diff --git a/crates/vendor/oxideav-aac/Cargo.toml b/crates/vendor/oxideav-aac/Cargo.toml deleted file mode 100644 index af5f3501..00000000 --- a/crates/vendor/oxideav-aac/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "oxideav-aac" -publish = false -version = "0.1.6" -edition = "2021" -rust-version = "1.80" -license = "MIT" -repository = "https://github.com/OxideAV/oxideav-aac" -authors = ["Mark Karpeles"] -description = "Pure-Rust AAC-LC decoder and encoder for oxideav — ADTS framing, Huffman books 1-11, IMDCT, M/S stereo, TNS, PNS" - -readme = "README.md" -homepage = "https://github.com/OxideAV/oxideav-aac" -keywords = ["multimedia", "audio", "aac", "codec", "pure-rust"] -categories = ["multimedia::encoding", "multimedia::audio"] - -[dependencies] -oxideav-core = { path = "../oxideav-core" } - -# Vendored verbatim — see scripts/vendor-oxideav.sh. Upstream does not build -# under this repository's `-D warnings`, and making it would mean carrying a -# patch set across every refresh. -[lints.rust] -warnings = "allow" - -[lints.clippy] -all = "allow" diff --git a/crates/vendor/oxideav-aac/LICENSE b/crates/vendor/oxideav-aac/LICENSE deleted file mode 100644 index ffe2468a..00000000 --- a/crates/vendor/oxideav-aac/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Karpelès Lab Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/crates/vendor/oxideav-aac/README.md b/crates/vendor/oxideav-aac/README.md deleted file mode 100644 index 62f7fd22..00000000 --- a/crates/vendor/oxideav-aac/README.md +++ /dev/null @@ -1,1252 +0,0 @@ -# oxideav-aac - -[![CI](https://github.com/OxideAV/oxideav-aac/actions/workflows/ci.yml/badge.svg)](https://github.com/OxideAV/oxideav-aac/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/oxideav-aac.svg)](https://crates.io/crates/oxideav-aac) [![docs.rs](https://docs.rs/oxideav-aac/badge.svg)](https://docs.rs/oxideav-aac) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) - -A pure-Rust **AAC** (Advanced Audio Coding) codec for the -[oxideav](https://github.com/OxideAV/oxideav-workspace) framework. - -Every numeric constant, bit layout, and clause reference is sourced from -the staged ISO/IEC 13818-7 and ISO/IEC 14496-3 specifications under -`docs/audio/aac/`. - -## Status - -The crate implements the full AAC-LC decode chain end to end — from -ADTS bitstream parse through the per-tool reconstruction to interleaved -16-bit PCM — **plus the complete §4.6.18 SBR back-end (HE-AAC v1) and -the subpart-8 Parametric Stereo tool (HE-AAC v2)**, and **wires them -into the framework's runtime `Decoder` trait** (`register()` installs -an AAC decoder under id `"aac"`; see `codec_decoder` below). The PCM -is validated byte-exactly (within the 1-LSB IMDCT-rounding bound) -against the staged `expected.wav` corpus — **including the HE-AAC v1 -SBR fixture, which decodes 99.98% sample-exact with a max error of -1 LSB** at the doubled output rate, and the **HE-AAC v2 fixture, whose -PS stereo reconstruction lands at a 5e-5 per-channel error-to-signal -RMS** against the reference decode. The §4.6.18.4.3 **downsampled** -output mode (core-rate SBR, auto-selected from a core-rate -`extensionSamplingFrequency` ASC) and the §4.6.18.8 **low power** SBR -tool (real-valued filterbanks + aliasing detection/reduction) are -selectable on every decode entry point. -The crate also ships an **end-to-end AAC-LC encoder** (`encoder` / -`codec_encoder`): PCM → §4.6.11.3.1 forward-MDCT analysis with -§4.6.11.3.2 block switching (transient-driven -`ONLY_LONG → LONG_START → EIGHT_SHORT → LONG_STOP`, short frames -grouped per §4.5.2.3.4 on the band-envelope similarity of adjacent -windows), the exact §4.6.2 inverse quantizer under a masking-spread -psychoacoustics-lite model with a bidirectional rate loop, -measured-bit-cost codebook/section choice (a DP over section -boundaries priced with the real tuple writer), per-band §4.6.8.1 -M/S joint stereo (long frames per sfb, short frames per -`(window group, sfb)` under the pair's joint grouping), and -**every Table 1.19 default channel layout** — 1–6 and 8 (7.1) -channels as SCE / `common_window`-CPE / §4.5.2.1.3-conforming LFE -element plans — assembled into ADTS through the Phase-2 bit-exact -wire writers. Every stream is round-tripped through the crate's own -decoder (multitone 128 kbps at 0.016 err/sig RMS; staged-fixture -transcodes at 0.0008–0.003; multichannel layouts pinned with one -distinct tone per speaker); `register()` installs the encoder -alongside the decoder under id `"aac"`. - -### ISO/IEC 14496-26 conformance (normative corpus) - -`tests/iso_14496_26_conformance.rs` decodes members of the normative -MPEG-4 Audio conformance corpus end to end against their reference -waveforms (corpus located via `OXIDEAV_ISO_14496_26_DIR`, -skip-if-absent; sourcing, per-member checksums and the member-level -fetch recipe are in `docs/audio/aac/iso-14496-26-conformance.md` — the -ISO-copyright bitstreams are never committed). Measured state: - -* **ER AAC LD** — 15 vectors across 22.05/24/32/44.1/48 kHz at both - frame lengths: **47 003 / 47 004 access units decode** (the single - residual is `er_ad1103_22_ep0` AU 367, which the staged corpus - screen records as failing under every width hypothesis). PCM: the - LD-512 `er_ad1000*` family is reference-exact at err/sig ≈ 4.4e-5; - the LD-480 `er_ad1103np*` family lands at ≈ 1.3e-4 outside its - TNS/PNS access units (PNS noise phase is generator-defined; the - deployed LD TNS record is a still-untraced extra-spec wire — see - "Not yet supported"). The 32 kHz members pin the §4.5.4 - Tables 4.144/4.145 band tables end to end. -* **CCE** — the twelve `am05_*` vectors (AAC Main + one - `coupling_channel_element()` in every access unit): **1 370 / 1 370 - AUs decode**, and all six `am05_48` output channels match their - per-speaker references at ≈ 1e-4 err/sig — pinning the CCE gain - path (conformance-settled `cc_scale^(−ge)` exponent), the §4.6.6 - Main-profile predictor at its normative fixed-precision arithmetic, - M/S + intensity + TNS interplay, and the §8.5.2.2 PCE reorder. -* **SBR-CRC** — the four `al_sbr_{e,i}_32_*` vectors (the corpus's - only `EXT_SBR_DATA_CRC` carriers): **1 600 / 1 600 payload CRCs - verify** during a full decode, including the §4.5.2.8.1 pre-header - prefix (upsampling-only state) and the whole-payload coverage - region (`bs_fill_bits` included). - -### Bitstream parsing - -- **ADTS fixed header** (`adts`) — ISO/IEC 13818-7 §1.A.2: sync, - profile, sampling-frequency index, channel configuration, frame - length, raw-data-block count, CRC presence flag. -- **ADTS `error_check()` + SBR CRC verification** (`adts_crc`) — the - ISO/IEC 13818-7:2004 §8.1.1.1 protected-bit region walk (all 56 - header bits; the first 192 bits of every SCE / CPE / CCE / LFE with - the 3-bit `id_syn_ele` excluded and zero-padding of short elements; - the additional first-128-bits of every CPE's *second* - `individual_channel_stream`; all PCE / DSE bits) fed into the - ISO/IEC 11172-3 §2.4.3.1 CRC-16 (`0x8005`, all-ones init) that - 13818-7 §8.1.1.2 cites. Both frame forms verify: the Table 1.A.8 - single-raw-data-block `crc_check` and the Table 1.A.9 / 1.A.10 - multi-RDB split (headers + `raw_data_block_position` table under - one CRC, one CRC per block). Wired into - `StreamDecoder::decode_adts_frame` / `decode_all` and the runtime - `Decoder`; `protect_adts_frame` / `protect_adts_stream` produce the - protected form (a protected rewrite of a staged fixture decodes - byte-identically through a black-box validator binary — which, - notably, does not verify the CRC *value*, so the code convention is - pinned to the documented §2.4.3.1 parameters). The same module - hosts the SBR `bs_sbr_crc_bits` CRC-10 (`G10`, zero init) computed - over the Table 4.62 coverage region; the FIL walk verifies every - `EXT_SBR_DATA_CRC` payload. Corruption of any covered bit surfaces - `Error::AdtsCrcMismatch` / `Error::SbrCrcMismatch`; fill bits and - the beyond-window element bits are provably uncovered. -- **AudioSpecificConfig** (`asc`) — ISO/IEC 14496-3 §1.6.2.1 including - the §4.4.1 GASpecificConfig body for all General Audio object types, - the hierarchical SBR (AOT 5) / PS (AOT 29) wrappers, the - `extensionFlag` subtree, the `epConfig` field, and the Table 1.15 - trailing `syncExtensionType == 0x2b7` implicit-SBR probe. A - carrier-bounded `parse_bits_bounded` entry point is exposed for LATM - `StreamMuxConfig` callers. -- **program_config_element** (`pce`) — §4.4.1.1, used standalone and - inline inside `asc`. -- **raw_data_block()** walker (`raw_data_block`) — §4.4.2.1: visits each - `id_syn_ele` and stops at `END`. FIL / DSE / PCE bodies are fully - consumed; the channel-element body is composed by the modules below. -- **Channel-element body** (`ics_body`) — Table 4.50: `global_gain` → - `ics_info` → `section_data` → `scale_factor_data` → optional - `pulse_data` / `tns_data` / `gain_control_data`, surfacing the start - bit-offset for the spectral data. -- **spectral_data()** (`spectral_data`) — Table 4.56 wire walker and - bit-exact writer, dispatching onto the Huffman codebooks. -- **extension_payload()** (`extension_payload`) — §4.4.2.7 / Table 4.51 - parser + encoder for the `EXT_FILL`, `EXT_FILL_DATA`, and - `EXT_DYNAMIC_RANGE` branches. The two SBR-data extension types - decode through the `parse_with_sbr` entry (feeding the §4.6.18 - back-end); the plain `parse` entry without an SBR context rejects - them (`Error::UnsupportedExtensionSbr`). -- **Error-protection CRC generator** (`crc`) — §1.8.4.5: the full - family of MPEG-4 Audio CRC generation polynomials (`CRC4`..`CRC32`, - including the `CRC8` LATM `StreamMuxConfig()` `crcCheckSum` and the - 16-bit `x¹⁶+x¹⁵+x²+1`), a zero-init MSB-first shift-register - (`crc_bits` / `crc_bytes`) implementing the §1.8.4.5 - `M(x)·xᵏ = Q(x)·G(x) + R(x)` remainder with the normative - output-bit inversion ("written in a reversed manner, i.e. each bit - is inverted"), and the [`crc::stream_mux_config_crc`] LATM helper. - Cross-checked against an independent GF(2) long-division reference - and the codeword-divisibility property. The ADTS - `adts_error_check()` region-selection CRC uses a different code - convention (ISO/IEC 11172-3 §2.4.3.1, all-ones init, no output - inversion) and lives in the dedicated `adts_crc` module above. -- **RVLC error-resilient scalefactor coding** (`rvlc`, - `scale_factor_data::ErScaleFactorData`) — §4.6.16.2 the - reversible-variable-length-coding replacement for the §4.6.3 - noiseless coding of scalefactors, used when - `aacScalefactorDataResilienceFlag == 1`. The `rvlc` module - transcribes the symmetric (bit-palindrome) RVLC codebook - (Table 4.166, deltas `-7..=+7` with `±7` the `ESC_FLAG`), the eight - asymmetric *forbidden* codewords (Table 4.167) whose appearance is - surfaced as the §4.6.16.2.1 in-band error-detection event, and the - 54-entry RVLC-ESC Huffman codebook (Table 4.168) — every codebook - proven prefix-free and round-tripping, and independently - cross-validated against the staged packed binary-tree node tables. - `ErScaleFactorData::parse` / `::write` decode and re-encode the whole - Table 4.53 RVLC branch: the `sf_concealment` / `rev_global_gain` / - `length_of_rvlc_sf` (11 bits for `EIGHT_SHORT_SEQUENCE`, else 9) - header, the RVLC base-delta band loop (first PNS band keeping the - 9-bit PCM seed), the optional `sf_escapes_present` / - `length_of_rvlc_escapes` second pass folding each escape into its - `±ESC_FLAG` base (`+7 + esc` / `-7 - esc` per §4.6.16.2.1), and the - `dpcm_is_last_position` / `dpcm_noise_last_position` backward seeds. - Both `length_of_*` fields are validated against the bits actually - consumed. The reconstructed records share the non-resilient - `ScaleFactorData` shape, so the §4.6.2.3.2 forward DPCM - `accumulate()` pass consumes them unchanged — pinned by a test that - an RVLC stream and the Huffman stream carrying the same deltas - accumulate to identical absolute scalefactors. The - resilience-flag dispatch from `ics_body` is now wired (see the - error-resilient ICS body below); the RVLC bitstream path itself is - decoded end to end. -- **Error-resilient channel-element body** - (`ics_body::IcsBody::parse_er` / `::parse_with_ics_info_er`, - `section_data::SectionData::parse_er` / `::write_er`) — ISO/IEC - 14496-3 §4.4.6 Tables 4.50 / 4.52, the ER General-Audio object types - (AOTs 17 / 19 / 20 / 23). Drives all three resilience branches off - the `AacResilienceFlags` triplet: `section_data()` through the 5-bit - `sect_cb` branch (carrying the §4.6.16.4 virtual codebooks 16..=31, - whose `ESC_HCB` / `>= 16` runs take the fixed `sect_len_incr = 1` - single-band coding) when `aacSectionDataResilienceFlag` is set; - `scale_factor_data()` through the RVLC `ErScaleFactorData` branch - (its reconstruction mirrored into the shared `scale_factor_data` - field so the §4.6.2.3.2 accumulate pass is branch-agnostic, with the - RVLC seeds retained in `er_scale_factor_data`) when - `aacScalefactorDataResilienceFlag` is set; and the - `length_of_reordered_spectral_data` (14-bit) + - `length_of_longest_codeword` (6-bit) HCR length fields in - `reordered_spectral_lengths` when `aacSpectralDataResilienceFlag` is - set. The trailing `reordered_spectral_data()` (HCR) payload is the - caller's responsibility, exactly as `spectral_data()` is on the - non-resilient path. -- **HCR segmentation / pre-sorting scaffold** (`hcr`) — ISO/IEC - 14496-3 §4.6.16.3.3 / §4.6.16.3.5. The deterministic, header-only - half of Huffman codeword reordering: the Table 4.170 `maxCwLen` - table, the §4.6.16.3.3.1 `codebookPriority[32]` table + the - `assignedUnitNr` pre-sorting metric, the - `segmentWidth = min(maxCwLen, length_of_longest_codeword)` - derivation, the §4.6.16.3.2 length-field clamps, and the - `Segmentation` layout that instantiates PCW segments until the - `length_of_reordered_spectral_data` buffer is exhausted (folding the - trailing bits into the last segment). `ReorderPlan::build` then runs - the §4.6.16.3.3.4 `ReorderSpectralData()` writing scheme (PCWs - forward from each segment start, then the non-PCW set / trial loop - with the per-set `ToggleWriteDirection()` and the modulo-shift - `segment = (trial + codewordBase) % numberOfSegments`) to resolve, - for each codeword, the ordered global buffer bit positions - (MSB-first) that carry its bits — pinned by a bijection invariant - (every buffer bit covered exactly once). -- **HCR payload codec** (`hcr_decode`) — §4.6.16.3.3.4 / §4.6.16.3.4, - both directions of the `reordered_spectral_data()` payload itself. - `encode_reordered_spectral_data` enumerates the frame's codeword - units (the §4.5.2.3.2 unit: Huffman codeword + sign bits + escape - sequences, two or four lines) in the §4.6.16.3.3.1 pre-sorted order - — the unit-based window interleave (Table 4.169; the §4.5.2.3.5 - grouping interleave does not apply under HCR) stably ordered by - `assignedUnitNr` — encodes each unit and scatters the bits over the - segment grid via `ReorderPlan`. `decode_reordered_spectral_data` - inverts the walk without transmitted lengths: PCWs decode forward - from their own segment starts, then the non-PCW sets run the same - direction-toggling modulo-shift trial loop, each codeword consuming - segment free-region bits until its Huffman unit completes (prefix - codes make "incomplete" exactly detectable, so lengths are - discovered where the writer defined them). The §4.6.16.4 virtual - codebooks (16..=31) decode as book 11 with their own `maxCwLen` - segment widths. Round-tripped bit-exactly over long / eight-short - (two window groups), spectrum-less-band mixes, escape-bearing book - 11, virtual codebooks, and slack-padded buffers; corrupt payloads - surface errors, never panics. Threading the ER triplet from - `GASpecificConfig` through the stream drivers (the ER top-level - payloads) remains open, and an HCR-bearing conformance stream is - still wanted as an external cross-check. - -### Numeric reconstruction (AAC-LC tool chain) - -- **Spectrum Huffman codebooks 1..=11** (`spectrum_huffman`, - `spectral_codebook`) — the complete Annex 4.A spectrum book set, - including the ESC book 11, with §4.6.3.3 index↔tuple translation and - sign-bit / escape-sequence handling. -- **Inverse quantization + scalefactors** (`dequant`) — §4.6.1.3 - non-uniform inverse quantizer and §4.6.2.3.3 scalefactor gain. -- **Decoded spectrum** (`decoded_spectrum`) — §4.6.3.3 `quant_to_spec()` - de-interleaver plus the per-channel pipeline composing pulse fix-up → - scalefactor accumulation → inverse quantization + rescale → - de-interleave → TNS. -- **TNS** (`tns_data`, `tns_coef`, `tns_frame`, `tns_max`, - `swb_offset`) — §4.6.9 Temporal Noise Shaping: wire parse, - coefficient inverse-quantisation + conversion to LPC, the all-pole IIR - pass, and the per-frame region-slicing orchestration. -- **Filterbank** (`filterbank`) — §4.6.11 stateful per-channel IMDCT - with sine / KBD windows, all four `window_sequence` shapes, eight-short - internal overlap-add, and inter-frame overlap-add. Pinned by streaming - TDAC perfect-reconstruction tests. Covers **all four §4.5.1.1 - frame-length families**: the 1024/128- and 960/120-line - block-switching families (`N = 2048/256` and `1920/240`) and the - long-only ER AAC LD 512/480-line families (`N = 1024/960`), where - the `window_shape == 1` bit selects the §4.6.17.2.3 Table 4.171 - **low-overlap window** in place of KBD (power-complementarity and - streaming TDAC pinned per family). -- **Channel-pair / noise tools** — M/S stereo de-matrix (`ms_stereo`, - §4.6.8.1), intensity stereo (`intensity_stereo`, §4.6.8.2), and - Perceptual Noise Substitution (`pns`, §4.6.13). PNS produces - energy-exact bands; only the per-coefficient phase is RNG-defined per - §4.6.13.3, so its output is not byte-exact against any one decoder — - the staged `docs/audio/aac/pns-gen-rand-vector.md` analysis pins the - normative half (band selection, energy DPCM, measured-energy - normalisation, correlated-CPE same-vector rule — all implemented) - and shows the generator recurrence/seed/threading to be deliberately - unspecified, so cross-decoder PNS checks are energy-domain by - design. -- **Coupling channel element** (`cce`) — §4.6.8.3 / Table 4.8. The CCE - coupling header (`CouplingHeader`: `ind_sw_cce_flag`, - `num_coupled_elements`, the per-target `cc_target_is_cpe` / - `cc_target_tag_select` / `cc_l` / `cc_r` list with the Table 4.153 - shared-vs-split `num_gain_element_lists` derivation, `cc_domain` / - `gain_element_sign` / `gain_element_scale`) and the trailing gain-list - block (`CouplingGains`: per-target `common_gain_element` or per-`(g, - sfb)` `dpcm_gain_element` running-sum lists — the §4.6.8.3.3 - `ind_sw_cce_flag ⇒ common-gain-only` constraint and the embedded-SCE - `sfb_cb` `ZERO_HCB` skip — reusing the §4.A.1 scalefactor Huffman - codebook `hcod_sf`). `CouplingChannelElement` ties the whole Table 4.8 - element (header → embedded `individual_channel_stream(0,0)` body + - spectrum → gain lists) together, and `CouplingGains::cc_gain` computes - the §4.6.8.3.3 `couple_channel()` factor `cc_gain = cc_sign · - cc_scale^gain` (Table 4.154 `cc_scale_table`, implicit list-0 natural - scaling). `CouplingGains::couple_channel` applies the §4.6.8.3.3 - per-band scale-and-add — the spec's group / window-group / sfb / - coefficient loop multiplying the embedded-SCE spectrum by the - per-`(g, sfb)` `cc_gain` and adding it onto a target channel's - window-major spectrum (implicit list 0 in natural scaling, `ZERO_HCB` - bands skipped). **The cross-element application is wired into the - stream decoder**: the raw-data-block walk is two-pass (parse every - channel element, then decode), each CCE's embedded SCE is decoded - through a per-instance-tag `CceDecoder` slot (its own pulse / dequant - / PNS / TNS, plus its own persistent §4.6.11 filterbank for the - independently-switched case), and the `decode_coupling_channel()` - target walk matches `cc_target_is_cpe` / `cc_target_tag_select`, - assigns the Table 4.153 gain lists (shared / left / right / both), - and injects the scaled spectrum at the signalled `cc_domain` stage - (before / after the target's TNS; window-state match enforced) or — - for an independently switched CCE — the scaled time signal after the - target's filterbank. Validated end to end with writer-assembled CCE - streams against the filterbank-linearity identity - `decode([target, CCE]) = decode([target]) + cc_gain·decode([embedded])` - (natural scaling, gain-list ×2, independently-switched, and - CPE-left-only layouts, ≤ 2 LSB stacked-rounding deviation). A - writer-assembled CCE fixture cycling all three coupling shapes - (dpcm + sign split / ind-switched / shared natural, both domains, - PCE-declared `valid_cc_elements`) is staged in the docs corpus - (`aac-cce-writer-assembled`). The two long-standing §4.6.8.3.3 - wire questions are both settled: the **exponent** is negated — - `cc_gain = cc_sign · cc_scale^(−gain_element)` — confirmed by the - ISO/IEC 14496-26 `am05_*` conformance vectors (all three editions - print the positive exponent, which misses every coupled target by - ~1e-1 err/sig), and the **`gain_element_sign` split** follows the - 2001 / 13818-7:2004 `couple_channel()` text as ruled in - `docs/audio/aac/cce-gain-sign-split.md` §3 — `cc_sign` off **each - transmitted dpcm delta** (`1 − 2·(dpcm & 1)`), accumulator fed with - `dpcm >> 1`, and a `common_gain_element` **never** sign-split (the - 14496-3:2009 page prints two conflicting fragments; its - accumulated-value variant is an editorial defect of that edition). -- **Frequency-domain prediction** (`predictor`) — §4.6.6 MPEG-2 - backward-adaptive intra-channel predictor for the AAC **Main** object - type (AOT 1). A bank of second-order lattice predictors (one per MDCT - line up to the §4.6.6.2 `PRED_SFB_MAX` limit) reconstructs - `x_rec = x_est + y_rec` on the signalled bands. Implements the - §4.6.6.3.2.1 lattice `predict()` + LMS adaptation - (`α = 0.90625`, `a = b = 0.953125`), the §4.6.6.3.2.3 - `flt_round_inf()` 16-bit-float rounding applied to every stored state - variable and the predicted value, and the §4.6.6.3.3 reset (the 30 - Table 4.97 cyclic groups + the short-block reset-all). Wired into - `element_decode`: the bank runs every long frame *before* TNS (and is - mutually exclusive with LTP by object type), persisting the - backward-adaptive state across frames. -- **Long-Term Prediction** (`ltp`) — §4.6.7 long-window LTP: the - Table 4.98 coefficient codebook, the §4.6.7.3 `predict()` single-tap - time-domain predictor (`x_est(i) = ltp_coef·x_rec(i − ltp_lag)`) over - a per-channel `x_rec` reconstruction history, the windowed analysis - `MDCT(x_est)` (the §4.6.15.3.3 / §4.6.11.3.1 forward transform, now a - reusable `filterbank` primitive), and the per-sfb - `X_rec = X_est + Y_rec` combination on the bands flagged by - `ltp_long_used`. LTP is restricted to long windows for the AAC LTP - object type (§4.6.7.1, 2009 edition). The ISO/IEC 14496-3:**2001** - short-window synthesis (`LtpState::apply_short_2001`) is also - implemented per the 2001 §4.6.7.3 pseudo-code — per flagged - subwindow, `lag_w = ltp_lag + ltp_short_lag[w]`, the 256-point - windowed `MDCT(x_est)`, and the `X_rec = X_est + Y_rec` add on the - first 8 SFBs — with the one quantity the 2001 text never fixes - (the per-subwindow `x_rec` index origin; see the staged - `docs/audio/aac/short-window-ltp-blocked.md` §5) taken as an - explicit caller parameter rather than an invented convention. The - **ER AAC LD branch is implemented**: the 10-bit lag with the - `ltp_lag_update` repeat state (`ltp_prev_lag`) and the §4.6.7.3 - `M = N/2` lag offset, applied at the LD transform lengths. -- **TNS analysis filter** (`tns_coef::tns_ma_filter`, - `tns_frame::tns_analysis_frame`) — §4.6.7.4.1 / Figure 4.30: the - all-zero (moving-average, FIR) inverse of the §4.6.9.3 all-pole - synthesis filter, `y(n) = x(n) + Σ lpc[k]·x(n−k)`. Run over the same - per-window region walk as `tns_decode_frame`; analysis ∘ synthesis is - the identity over a shared region, which is the §4.6.7.4.1 - noise-shaping invariant. -- **Element decode driver** (`element_decode`) — `ElementDecoder` chains - the whole stack per element: `decode_sce` for SCE / LFE and - `decode_cpe` for a CPE (pulse → dequant → `quant_to_spec()` → M/S → - intensity → PNS → **LTP → TNS** → filterbank), carrying the - per-channel overlap-add tail **and the §4.6.7.3 LTP reconstruction - history** across frames. LTP runs in the §4.6.7.4.1 / Figure 4.30 - block order — long-term synthesis (with the all-zero TNS analysis - filter applied to `X_est`) *before* the §4.6.9 TNS synthesis filter, - so the single synthesis pass shapes the residual while undoing the - analysis on the LTP contribution. - -### Frame-length families — §4.5.1.1 / §4.6.17 (960, LD 512/480) - -All four `frameLengthFlag` frame geometries decode end to end, keyed -by `swb_offset::FrameFamily` (resolved from the AOT + flag; the -LATM/LOAS driver installs it per layer from the ASC, -`StreamDecoder::set_frame_family` serves raw callers — ADTS can only -carry the default 1024-line family): - -- **AAC-LC at 960/120 lines** (`frameLengthFlag == 1`) — the - bracketed "values for 1920/240" columns of Tables 4.129–4.141, the - `N = 1920/240` transform pair with all four window sequences, both - window shapes, grouping, TNS and the full joint-stereo/noise tool - chain; 960 PCM samples per channel per frame. Verified **bit-exact - against a black-box decoder binary** on writer-assembled streams, - and staged with mutation coverage as `aac-lc-960-writer-loas`. -- **ER AAC LD at 512/480 lines** (AOT 23, §4.6.17) — Tables - 4.142–4.147 (with the §4.5.1.1 nearest-defined-table rule for the - rates those tables omit), long-only frames (a non-`ONLY_LONG` - `window_sequence` is rejected, §4.6.17.2.2), the §4.6.17.2.3 - Table 4.171 **low-overlap window** on the `window_shape == 1` bit, - the §4.6.17.2.5 LD `TNS_MAX_BANDS` tables, the §4.6.7 **LD LTP** - branch (10-bit lag, `ltp_lag_update` repeat via a per-channel - `ltp_prev_lag`, `M = N/2` lag offset at the LD transform lengths), - and the Table 4.19 `er_raw_data_block()` element walk shared with - ER AAC LC; 512/480 PCM samples per channel per frame. The LD-512 - geometry (every swb band boundary, both window shapes, the - transform/overlap-add) is verified **bit-exact against two - independent black-box decoder binaries**; LD-480 against one (the - other binary decodes 480-line streams on the wrong 512-line - frequency grid — probed and documented in the fixture notes). - Staged fixtures: `aac-ld-512-writer-loas`, `aac-ld-480-writer-loas`. -- **LD TNS wire — RESOLVED against the conformance corpus**: the - divergence between the literal Table 4.54 / Table 4.155 field - widths and the deployed LD TNS wire was settled by the ISO/IEC - 14496-26 screen recorded in `docs/audio/aac/er-ld-tns-divergence.md` - §0 — the normative LD wire transmits `n_filt` in **1 bit** (the - reduced Table 4.155 column; the literal 2-bit keying hard-fails 792 - of the corpus's 2 017 TNS-bearing AUs). The LD families read the - 1 / 4 / 3 column via `TnsData::parse_family` / `write_family`; - because the corpus never transmits a `length` / `order` field - (`n_filt == 0` throughout, so 4 / 3 vs 6 / 5 is undetermined), the - §0.6 configurability recommendation is kept via the explicit-width - `TnsData::parse_widths` / `write_widths` entry points. -- An SBR payload on a 960-line or LD stream is rejected before its - body is parsed (`Error::SbrUnsupportedFrameFamily`) — the §4.6.18 - tool here is defined over the 1024-line core, and the §4.6.19 LD - SBR tool belongs to ELD (out of scope). - -### Scalable AAC (AOT 6) / ER AAC scalable (AOT 20) — §4.4.2.2 / §4.5.2.2 - -The AAC-only scalable combinations decode end to end (`scalable`): -one `aac_scalable_main_element()` plus up to seven extension -elements, each on its own elementary stream / LATM layer -(mono-only, stereo-only and mixed mono→stereo stacks, Table 4.87). - -- **Syntax** — Tables 4.13–4.18: the main/extension headers - (window geometry hoisted out of `ics_info()`, per-channel TNS on - the first mono and first stereo layer, per-channel LTP on the main - layer, the §4.6.8.1.4 *incremental* `ms_data()` over - `last_max_sfb_ms..max_sfb`, per-channel `diff_control_data_lr()` - with the Table 4.18 `ms_used` gating), and the Table 4.50 - `scale_flag == 1` ICS form (`IcsBody::parse_scale` — no inline - `ics_info()`, no tool dispatch trio). For AOT 20 the §4.4.6 - resilience triplet selects the ER wire branches per channel - (5-bit-`sect_cb` sections, RVLC scalefactors, inline HCR - `reordered_spectral_data()`); the element syntax itself is - unchanged (§4.5.2.4), pinned bit-identical to the AOT-6 decode of - the same spectra. `ScalableFrame::parse` / `::write` round-trip - the whole per-layer payload stack byte-exactly. -- **Layer combination** (§4.5.2.2.4 SIAQ, `ScalableDecoder`): the - dequantized spectra of all layers sum per output path under the - Table 4.91–4.93 per-band tool rules — a lower layer's PNS band - survives only while every higher layer decodes the band to zero - (§4.6.13.6), intensity accumulates the M/L channel with positions - from the highest layer, invalid combinations surface - `Error::ScalableLayerCombination` — then the §4.6.14.2.1 FSS merges - the combined mono spectrum into the stereo pair (`L/R += 2·M''` on - clear `diff_control_lr` bits, `M = M'' + M'` on M/S bands; long - and short windows), the cumulative-mask M/S butterfly, the - scalable-invariant intensity reconstruction - (`invert_intensity() = +1`), correlated PNS via `ms_used`, the - §4.6.9.5 / Table 4.158 **serial TNS** layout (first mono layer's - filter serves the low bands up to the highest mono `max_sfb`, - first stereo layer's filters serve L/R, with the lower-boundary - override rule) and the §4.6.11 filterbank. §4.6.7.5 **base-layer - LTP** runs on the lowest layer only, its reconstruction history - fed by a parallel first-layer-alone synthesis chain (pinned by a - history-isolation test). Both the 1024- and 960-line families. -- **Transport**: the LATM/LOAS driver recognises AOT-6/20 layers, - collects each program's layer payloads per access unit and decodes - them combined through a persistent per-program `ScalableDecoder` - (`ScalableConfig::from_layer_ascs` validates the layer stack; - `dependsOnCoreCoder == 1` — a CELP core — and TwinVQ lower layers - are out of scope, `Error::ScalableUnsupportedCore`). -- Single-layer scalable streams are pinned **bit-identical** to the - equivalent SCE / common-window CPE decodes; multi-layer stacks are - pinned against references composed from the crate's own - reconstruction primitives; every branch carries a deterministic - bit-flip / truncation battery (`tests/scalable_*.rs`). - -### Error protection (EP) tool — §1.8 - -The MPEG-4 Audio unequal-error-protection layer, from the out-of-band -configuration to the LOAS EP carrier (`ep_config` / `ep_fec` / -`ep_rs` / `ep_frame`): - -- **`ErrorProtectionSpecificConfig()`** (Table 1.49) — parse + - bit-exact write with reserved-field rejection, the §1.8.4.2 - `class_optional` expansion (pinned against the spec's own - Table 1.57/1.58 example), and ASC integration: `epConfig == 2 / 3` - now parse the inline config and the `directMapping` bit. -- **SRCPC** (§1.8.4.6) — the rate-1/4 systematic recursive - convolutional encoder (Figure 1.10 equations), the Table 1.61 - puncture family 8/8..8/32, §1.8.4.6.2 termination (the `u = d` - tail rule, proven identical to the whole Table 1.60 listing) and a - hard-decision 16-state Viterbi decoder correcting channel errors. -- **In-band header FEC** (§1.8.4.3 Table 1.59) — majority, BCH(7,4), - BCH(15,7), Golay(23,12), BCH(31,16) with the normative generators - and bounded-distance correction; CRC4 + terminated SRCPC 8/16 for - 17+ bits; the extended `header_protection` path. -- **Shortened Reed-Solomon** (§1.8.4.7) — `SRS(255−l, 255−2k−l)` - over the spec's GF(2⁸) (`m(x) = x⁸+x⁴+x³+x²+1`; the generated - antilog table is pinned against Table 1.62 rows), the part split - with zero-padded last part, lowest-order-first parity, and the - syndrome / Berlekamp-Massey / Chien / Forney correction chain - (`k` byte errors per part corrected, `k+1` rejected). -- **`ep_frame()`** (§1.8.2.2, `EpFrameCodec`) — the FEC-protected - `choice_of_pred` + `class_attrib()` header (in-band Table 1.55 - rate / Table 1.56 CRC escapes, `num_stuffing_bits`), per-class - §1.8.4.5 CRC (the family now reaches down to CRC1) + SRCPC / SRS - protection, §1.8.4.4 RS chains, the "until the end" class-length - recovery (§1.8.4.1), §1.8.4.9 class-reordered transmission, and - the §1.8.4.8 recursive interleaver (`k = m·D + min(m, d) + n`; - bitwise for SRCPC, bytewise for RS) in modes 0 / 1 / 2 with the - per-class mode-2 `interleave_switch`. Encode ↔ decode round-trips - across the configuration matrix; errors are corrected through the - whole frame; a full bit-flip battery never panics. Two - under-specified corners (an escaped rate on an RS class; byte-wise - interleave over a non-octet-aligned Y stream) are rejected rather - than guessed. -- **LOAS EP carrier** (§1.7) — the `EPAudioSyncStream()` BCH(36,18) - `headerParity` (§1.7.2.2.2 generator; generate + verify), - `EPMuxElement(1, 1)` (majority-protected `epUsePreviousMuxConfig`, - Golay-protected `epSpecificConfigLength`, Table 1.59-protected - inline config with threaded reuse), and - `LoasDecoder::decode_all_ep` — the recovered `ep_frame()` class - concatenation is the plain `AudioMuxElement()` bit stream - (§1.7.3.2.1: the sensitivity-category instances ride in syntax - order), so payloads (scalable programs included) ride the - existing decode paths. A writer-assembled EP stream decodes - **byte-identical** to its plain LOAS equivalent and survives - correctable channel errors. - -### SSR gain control (§4.6.12) — complete decode pipeline - -The §4.6.12 SSR (Scalable Sample Rate, AOT 3) gain-control tool is -implemented **end to end** — front-half filterbank, gain -reconstruction, and IPQF synthesis — and wired into the decode driver -(ADTS profile 2 routes every SCE / CPE channel through it), validated -independent of any external SSR implementation: - -- **Gain-control reconstruction** (`gain_control`) — §4.6.12.3.1–3. The - §4.6.12.3.1 gain-control data decoding (the Table 4.108 `AdjLoc()` = - `8·AC` and Table 4.109 `AdjLev()` = `AV − 4` tables, the `NADW` / - `ALOC` / `ALEV` ladder with the step-(3) `ALOC(0)=0` / `ALEV(0)` rule - and the step-(4) per-window-sequence endpoint), the §4.6.12.3.2 - gain-control function setting (the `M_{W,B,j}` index, the `FMD` - fragment-modification function with the `Inter(a,b,j)` geometric-blend - ramp, the per-sequence `GMF` composition threading the cross-frame - `PFMD`, and the inversion `AD(j) = 1/GMF(j)`), and the §4.6.12.3.3 - windowing + overlapping (`GainBandState::window_overlap` applies - `T = AD·U` then overlap-adds per `window_sequence` into the band sample - data `V_B`, threading the cross-frame `PT_B` tail). All four - `window_sequence` shapes are covered; the spec initial values - `PFMD ≡ 1.0` / `PT ≡ 0.0` are honoured, and the input-read vs - produced `PFMD` lengths (which differ per sequence) are tracked - separately with a persistent 256-entry carry. -- **IPQF synthesis filter** (`ipqf`) — §4.6.12.3.4. The Table 4.110 - length-96 prototype `Q(j)` (the symmetric `Q(j) = Q(95 − j)` half - mirrored to 96), the cosine modulation - `Q_B(j) = Q(j)·cos((2B+1)(2j−3)π/16)`, the 4× upsampling - `Ṽ_B(j) = V_B(j/4)`, and the streaming convolution - `AS(n) = Σ_B Σ_j Q_B(j)·Ṽ_B(n−j)` as a polyphase bank (`Ipqf`) that - retains a 24-deep per-band history across frames — pinned by an - impulse-response test against the direct §4.6.12.3.4 convolution - (`AS(n) = Q_0(n)`). -- **Per-channel driver** (`ssr`) — `SsrGainControl::decode_frame` - composes the four-band `GainBandState` and the `Ipqf` into one - persistent per-channel pipeline: the four per-band IMDCT outputs - `U_{W,B}` plus the decoded `gain_control_data()` → the §4.6.12.3.3 - per-band windowing/overlap → the §4.6.12.3.4 IPQF synthesis → the PCM - `AS(n)` (1024 samples/frame for the steady `ONLY_LONG` / `EIGHT_SHORT` - case). PQF band 0 is never gain-controlled. - -- **Front-half filterbank** (`ssr_filterbank`) — §4.6.12.1 / - 13818-7 §16.1, closing the previously docs-gapped spectrum→band - mapping: the frequency-ascending spectrum splits into four - *contiguous* PQF-band quarters (the PQF's band `B` covers the `B`-th - quarter, Annex C.2.1.1), the "even" bands — the spec's ordinal - 2nd/4th, i.e. 0-based 1 and 3, exactly the bands the ×4 decimation - spectrally inverts — are reversed, and each band runs a 256-line - (long) / 8 × 32-line (short) IMDCT under the quarter-scale - §4.6.11.3.2 window geometry (`N_l/N_s = 512/64`; the KBD windows are - generated with the α = 4 / α = 6 running-sum construction and pinned - against the normative Table 4.A.14 / 4.A.13 listings). The split + - reversal convention is pinned by a tone-placement test against the - Annex C.2.1.1 analysis-PQF definition. -- **Per-channel pipeline + driver wiring** (`ssr::SsrChannelDecoder`, - `element_decode`) — the complete spectrum → PCM chain (front half → - §4.6.12.3 gain compensation/overlap → IPQF), replacing the §4.6.11 - filterbank for AOT 3 in the decode driver (per-channel-slot state, - `gain_control_data()` from the channel body; note the §4.6.12.3.3 - variable frame lengths — 1472 / 576 PCM samples for `LONG_START` / - `LONG_STOP`). Validated by full round-trip tests against the Annex - C.2.1.1 analysis PQF: steady long frames and a complete - window-transition chain reconstruct at err/sig < 1e-3 (the PQF - pair's near-perfect-reconstruction bound, both window shapes), gain - ladders applied encoder-side cancel end to end, and an - ADTS-profile-2 stream decodes through the public `StreamDecoder` - (mono + stereo). - -### SBR bitstream decode (HE-AAC) - -The full SBR side-info path is now decoded from the `extension_payload` -SBR element down to the reconstructed quantized envelope / noise-floor -scalefactors — every numeric table sourced from the ISO/IEC 14496-3 -spec PDF (the §4.A normative Huffman grids and the §4.4.2.8 syntax -tables), independent of any external SBR table extraction. - -- **SBR Huffman codebooks** (`sbr_huffman`) — §4.A.6.1, all ten - normative envelope / noise codebooks (Tables 4.A.79–4.A.88) - transcribed from the spec codeword grids and validated complete + - prefix-free. `sbr_huff_dec()` reads MSB-first and returns the signed - DPCM delta (`index − LAV`); `env_tables()` / `noise_tables()` pick the - `(t_huff, f_huff)` pair from the §4.6.18.3 coupling / channel / - `bs_amp_res` selection (the freq-direction noise tables alias the - 3.0 dB envelope freq tables per Table 4.A.78 Note 2). -- **`sbr_header()`** (`sbr_header`) — §4.4.2.8 Table 4.63: the - fixed-width header plus the two optional extra blocks, with the - Table 4.63 Note 3 defaults (Tables 4.105–4.111) applied when an extra - flag is clear. `band_geometry_changed()` flags a §4.6.18.3.3 reset, - and `derive_bands()` chains into the band-setup pipeline below. -- **`sbr_grid()` / `sbr_dtdf()` / `sbr_invf()`** (`sbr_grid`) — - §4.4.2.8 Tables 4.69–4.71: all four `bs_frame_class` layouts (FIXFIX - / FIXVAR / VARFIX / VARVAR) with the envelope count, variable / - relative borders, `ptr_bits = ceil(log2(num_env + 1))` pointer, - reversed FIXVAR freq-res order, single-envelope FIXFIX `bs_amp_res` - override, and `bs_num_noise` derivation; the delta-direction flags; - and the per-noise-band 2-bit inverse-filtering modes. -- **`sbr_envelope()` / `sbr_noise()`** (`sbr_envelope`) — §4.4.2.8 - Tables 4.72–4.73: the raw `bs_data_*` delta arrays, with the - fixed-width absolute start value (5/6/7-bit per the coupling / - channel / `bs_amp_res` context; noise always 5-bit) and the - frequency- vs time-direction Huffman deltas, over `NHigh` / `NLow` - envelope bands and `NQ` noise bands. -- **Envelope / noise DPCM reconstruction** (`sbr_reconstruct`) — - §4.6.18.3.5: inverts the delta coding to the quantized scalefactors - `E_Q(k,l)` / `Q(k,l)`. Frequency deltas accumulate from the start - value; time deltas add to the reference envelope (previous in-frame, - or the prior frame's last envelope for `l == 0`) with the `i(k)` - high↔low band remap when the reference resolution differs; the - coupled second channel's `δ = 0.5` is applied as an integer ×2 on the - even transmitted values, threading cross-frame state. -- **Element framing** (`sbr_element`) — §4.4.2.8 Tables 4.65 / 4.66 / - 4.74: `SbrElement::parse_single` / `parse_pair` decode a whole SBR - data element in spec order — the optional `bs_data_extra` field, the - per-channel grid / dtdf / invf / envelope / noise blocks (coupled - shared-grid vs. independent-grid layouts, second coupled channel in - balance mode), the `sbr_sinusoidal_coding()` add-harmonic flags, and - the `bs_extended_data` block (id + raw body captured for a later PS - pass). The single-envelope FIXFIX `bs_amp_res` override is applied - before envelope decode. -- **`sbr_extension_data()`** (`sbr_extension`) — §4.4.2.8 Table 4.62: - the top-level walker that ties the header + element framing into a - whole SBR extension payload, in spec order — the optional 10-bit - `bs_sbr_crc_bits` (for the `EXT_SBR_DATA_CRC` type), the - `bs_header_flag` + `sbr_header()`, then `sbr_data(id_aac, bs_amp_res)` - dispatching onto `parse_single` (ID_SCE) / `parse_pair` (ID_CPE) with - the band tables derived from the active header at the SBR internal - rate (`FsSBR = 2·core`), and the trailing `bs_fill_bits` alignment - (`num_align_bits = (8·cnt − 4 − num_sbr_bits) % 8`). A clear - `bs_header_flag` reuses the threaded previous header (the - non-scalable core fixes `sbr_layer == SBR_NOT_SCALABLE`, so the flag - is always present). Reachable from the natural FIL entry point via - `extension_payload::ExtensionPayload::parse_with_sbr`, which routes - the SBR extension types here (the default `parse` still rejects them, - keeping the byte-exact AAC-LC corpus path untouched). - -The SBR *bitstream* side info is decoded end to end — CRC field, -header, element framing, band tables, and envelope / noise DPCM -reconstruction — and the **back-end DSP is now implemented too** (see -the next section). - -### SBR back-end (HE-AAC v1) — §4.6.18 - -The complete SBR reconstruction chain, from the core decoder's time -signal to dual-rate PCM, validated **99.98% sample-exact (max error -1 LSB)** against the staged HE-AAC v1 `expected.wav`: - -- **QMF filterbanks** (`sbr_qmf`) — §4.6.18.4 / Figures 4.42–4.44: the - Table 4.A.89 640-tap prototype window (transcribed digit-for-digit - from the spec PDF), the 32-band complex analysis bank, the 64-band - real-output synthesis bank (dual-rate), and the downsampled - 32-channel synthesis variant. Pinned by near-perfect-reconstruction - properties (< 1e-4 error ratios). -- **Dequantization + stereo decoding** (`sbr_dequant`) — §4.6.18.3.5: - `EOrig = 64·2^(E/a)`, `QOrig = 2^(6 − Q)`, and the coupled-pair pan - split with `panOffset = [24, 12]` (energy-sum-preserving). -- **Time / frequency grid** (`sbr_time_grid`) — §4.6.18.3.3: the - `tE` / `tQ` border vectors for all four frame classes, the - Table 4.174 `middleBorder` and the Table 4.176 `lA`. -- **HF generation** (`sbr_hf_gen`) — §4.6.18.6: the Figure 4.48 patch - construction, the covariance-method second-order inverse filtering - (`εInv = 1e-6`, `|α| ≥ 4` reset), the Table 4.175 chirp-factor - blend, and the patched `XHigh` generator. -- **Limiter band table** (`sbr_limiter`) — §4.6.18.3.2.3 / - Figure 4.41, fed by the patch borders (closing the previously - deferred limiter-table item). -- **Envelope adjustment** (`sbr_env_adjust` + `sbr_noise_table`) — - §4.6.18.7: mapping, `ECurr` estimation (both `bs_interpol_freq` - regimes), amplitude-domain gains (the spec PDF's typeset equations - carry square roots the plain text layer drops), the limiter / - boost compensation, `hSmooth` smoothing with cross-frame tails, the - Table 4.A.91 noise table with the running `fIndexNoise`, and the - sinusoid injection with the `(−1)^(m+kx)` alternation. -- **Frame driver** (`sbr_decoder`) — §4.6.18.5 / Figure 4.47: the - `tHFGen = 8`-slot `XLow` history, header-reset handling, the - `lTemp` splice of the previous frame's `Y'`, the coupled-pair invf - sharing, and the pure-upsampling path for SBR-less frames. -- **Stream wiring** (`decode`) — the ADTS `StreamDecoder` walks FIL - extension payloads via `extension_payload::parse_with_sbr`, attaches - each SBR payload to its preceding SCE / CPE, threads the - `sbr_header()` reuse state per element slot, and (once SBR-active) - emits every frame at the doubled rate — 2048 samples/channel — with - SBR-less frames upsampled so the output rate never flaps. The - runtime `Decoder` trait surfaces the dual-rate frames unchanged - (pinned byte-identical to the raw `StreamDecoder`). -- **Downsampled output mode** (§4.6.18.4.3) — selectable end to end: - `SbrDecoder::set_downsampled` / `StreamDecoder::set_sbr_downsampled` - / the `sbr_downsampled` codec option run the 32-channel synthesis - bank so an SBR-active stream is emitted at the *core* rate (1024 - samples per channel per frame; the SBR range above the core Nyquist - is discarded by construction, the bands below it are kept). The - LATM driver selects the mode automatically when an explicitly - signalled ASC carries `extensionSamplingFrequency == - samplingFrequency` (the §4.6.18.2.6 in-band core-rate declaration), - and PS composes (stereo through two downsampled banks). Validated - on the HE-AAC v1 fixture at **1.8e-4** per-channel err/sig RMS - against a band-limited 2:1 decimation of the reference decode - (v2 PS at 1.95e-4), byte-identical between the LATM and forced-ADTS - paths. -- **Low power SBR tool** (§4.6.18.8) — selectable end to end - (`SbrDecoder::set_low_power` / `StreamDecoder::set_sbr_low_power` / - the `sbr_low_power` codec option; composes with the downsampled - output): the §4.6.18.8.2 real-valued filterbank trio (`sbr_qmf`), - the §4.6.18.8.3 aliasing detection (`sbr_lp` + the reflection - coefficients in `sbr_hf_gen`: the Figure 4.53 degree walk, the - patch-carried `degPatched`, the Figure 4.54 gain groups), the - §4.6.18.8.4 ×2 energy estimation, and the §4.6.18.8.5 aliasing - reduction (`GLimBoost → GA`, exact group-energy restoration), - no-smoothing rule, modified real-valued sinusoid injection - (−0.00815 neighbour correction, first-16 rule, `kx − 1` / `kx + M` - spill) and modified `X` assembly. Validated on the HE-AAC v1 - fixture: sub-crossover content at **9e-5** err/sig RMS against the - reference with per-frame full-band energy within 0.05% (the - real-valued HF path is energy-normative, not phase-normative). A - PS payload in this mode is rejected (`Error::SbrLowPowerPs` — the - subpart-8 tool needs the complex QMF domain). All four mode - combinations survive a deterministic corruption battery - (`tests/sbr_mode_mutations.rs`). - -### SBR frequency band setup (HE-AAC) - -- **SBR frequency band tables** (`sbr_freq_bands`) — §4.6.18.3.2 the - static, header-only half of the Spectral Band Replication band setup, - computed directly from the closed-form spec algorithm (no QMF back-end - required): - - `k0` / `k2` — §4.6.18.3.2.1 the low and high QMF subband - boundaries. `k0 = startMin + offset(bs_start_freq)` with the - per-`FsSBR` `offset` table and the `startMin = NINT(c·128/FsSBR)` - thresholds; `k2` covers the `bs_stop_freq < 14` `stopDkSort` - accumulation path and the `bs_stop_freq == 14 / 15` - `min(64, 2·k0)` / `min(64, 3·k0)` shortcuts. - - `master_table` — §4.6.18.3.2.1 `fMaster`, both the Figure 4.39 - linear path (`bs_freq_scale == 0`, the `dk`/`vDk`/`k2Diff` - away-from-zero correction walk) and the Figure 4.40 warped path - (`bs_freq_scale > 0`, the `bands`/`warp` log-spaced regions with - the single-/two-region split at `k2/k0 > 2.2449` and the - `min(vDk1) < max(vDk0)` smoothing step). - - `HiLoTables::derive` — §4.6.18.3.2.2 the derived `fTableHigh`, - `fTableLow` (the `i(k) = 2k − (1−(−1)^NHigh)/2` decimation), and - `fTableNoise` (the `NQ = max(1, NINT(bs_noise_bands·log2(k2/kx)))` - band count plus its `i(k)` recursion), along with the `M` and - `k_x` outputs every later SBR stage keys off. - - The §4.6.18.3.6 requirements are enforced (`k2 > k0`, - `numBands > 0`, `vDk > 0`, `bs_xover_band < NMaster`), surfacing - `Error::SbrFreqBandInvalid` on violation. The §4.6.18.3.2.3 - limiter band table is out of scope here — its `bs_limiter_bands > - 0` path consumes the §4.6.18.6 patch borders that need the QMF - patching back-end. - -### Parametric Stereo (HE-AAC v2) — subpart 8 / Annex 8.A - -The complete §8.6.4 PS tool, reconstructing a stereo image from the -mono SBR signal, validated **5e-5 per-channel error-to-signal RMS** -against the staged HE-AAC v2 MP4 fixture (filterbank-rounding level): - -- **Bitstream** (`ps_data`, `ps_huffman`) — §8.4.2 Tables 8.9–8.14: - the persistent `enable_ps_header` configuration, FIX/VAR framing - (Table 8.29), per-envelope IID/ICC/IPD/OPD delta rows on all ten - Annex 8.B codebooks (each verified a complete prefix code; the six - IID/ICC books cross-checked leaf-for-leaf against the staged - `ps-huffbook-*.csv` trees), and the §8.5.2 time/frequency DPCM - resolution with range checks and modulo-8 phase wrap. -- **Hybrid filterbank** (`ps_hybrid`) — §8.6.4.3: both configurations - (71 / 91 sub-subbands) on the Table 8.37/8.38 13-tap prototypes, - with the Figure 8.20 merge/reorder, the odd-QMF-band inversion, and - the Annex 8.A.3 zero-delay alignment (6 look-ahead `XLow` slots + 6 - history slots). Analysis→synthesis reconstructs exactly. -- **De-correlation** (`ps_decorr`) — §8.6.4.5: the 3-link complex - all-pass chain behind `z⁻²·φ_fract`, the Table 8.40/8.41 centre - frequencies, the 14-/1-slot delays above `NR_ALLPASS_BANDS`, and - the transient duck (peak decay / smoothing / γ = 1.5) per stereo - band, with the Annex 8.A.3 partial + full resets. -- **Stereo processing** (`ps_stereo`, `ps_map`) — §8.6.4.6: Table - 8.25/8.26/8.28 dequantization (cross-validated against the staged - Q30 tables), mixing procedures Ra and Rb, IPD/OPD three-position - smoothing, the Table 8.48/8.49 `b(k)` maps + conjugate channels, - the Table 8.45/8.46 10↔20↔34 re-mappings, and the §8.6.4.6.4 - border interpolation with hold semantics. -- **Frame driver + wiring** (`ps_decoder`, `sbr_decoder`) — Annex - 8.A: inactive (mono) until the first header'd `ps_data()`, - parameter hold over payload-less frames, band-count switches, and - the per-frame de-correlator reset above `k_x + M`. A PS-carrying - SCE renders stereo through two synthesis banks in both the - SBR-processed and pure-upsampling paths, end to end through the - ADTS / LATM / raw `StreamDecoder` entries and the runtime - `Decoder`. - -### LATM / LOAS transport framing - -The §1.7 low-overhead transport layer is now decoded from the LOAS -sync frame down to the recovered MPEG-4 Audio access units — every -field sourced from the ISO/IEC 14496-3 §1.7 syntax tables. - -- **`StreamMuxConfig()`** (`latm::StreamMuxConfig`) — §1.7.3.1 - Table 1.42 plus `LatmGetValue()` (Table 1.43). Decodes the whole - multiplex configuration: the `audioMuxVersion` / `audioMuxVersionA` - version flags (with the `audioMuxVersion == 1` `taraBufferFullness` - and per-ASC length-prefix + `fillBits` extensions), - `allStreamsSameTimeFraming`, `numSubFrames` / `numProgram` / - per-program `numLayer`, and the per-`streamID[prog][lay]` - `LayerConfig` table — each layer carrying its inline - `AudioSpecificConfig()` (parsed via the `asc` module's - `parse_bits` / `parse_bits_bounded` entry points) or the resolved - `useSameConfig` inheritance, the `frameLengthType`, and the type-0 - `latmBufferFullness` / CELP-core `coreFrameOffset` or type-1 - `frameLength`. The `crcCheckSum` is recomputed against the - configuration prefix via the §1.8.4.5 `CRC8` generator and - validated. The reserved `audioMuxVersionA == 1` branch and the - CELP (`3`/`4`/`5`) / HVXC (`6`/`7`) `frameLengthType` values index - frame-length tables for object types this AAC-focused crate does not - decode, so they surface dedicated errors. -- **`AudioMuxElement()`** (`latm::AudioMuxElement`) — §1.7.3.1 - Tables 1.41 / 1.44 / 1.45. Recovers a whole multiplexed element: the - `muxConfigPresent` `useSameStreamMux` branch (inline - `StreamMuxConfig()` vs. inherited previous config), the per-subframe - `PayloadLengthInfo()` + `PayloadMux()` loop over `numSubFrames + 1` - frames (both the `allStreamsSameTimeFraming` program/layer walk and - the `numChunk` chunk layout with its `streamIndx` + `AuEndFlag`), the - `frameLengthType`-0 `MuxSlotLengthBytes` 8-bit-escape byte count and - the `frameLengthType`-1 fixed `(frameLength + 20) * 8` bits, the - `otherData` skip, and the trailing `ByteAlign()`. Each access unit is - returned as a `MuxPayload` carrying the raw §4.4.2.1 - `raw_data_block()` bytes. -- **`AudioSyncStream()` / `EPAudioSyncStream()`** - (`latm::AudioSyncStream`, `latm::EpAudioSyncHeader`) — §1.7.2.1 - Tables 1.36 / 1.37. `AudioSyncStream` scans a LOAS byte buffer for - the 11-bit `0x2B7` syncword, reads the 13-bit `audioMuxLengthBytes`, - and decodes the byte-aligned `AudioMuxElement(1)` body, exposing an - `Iterator` of `LoasFrame`s with the `StreamMuxConfig` threaded across - frames for `useSameStreamMux` inheritance. `EpAudioSyncHeader` - decodes the `EPAudioSyncStream` FEC header (`0x4DE1` syncword, - `futureUse`, `audioMuxLengthBytes`, `frameCounter`, `headerParity`) - and reports the byte-aligned `EPMuxElement` body offset. - -- **`LoasDecoder`** (`latm::LoasDecoder`) — the end-to-end LATM/LOAS → - PCM driver. `decode_all` walks the `AudioSyncStream`, and for every - recovered `MuxPayload` drives the payload's §4.4.2.1 `raw_data_block()` - through the shared `decode::StreamDecoder::decode_raw_data_block` core, - configuring the decode from the layer's `AudioSpecificConfig` (AOT / - `samplingFrequencyIndex` / resolved sample rate). One `StreamDecoder` - is held per `streamID[prog][lay]` so each multiplexed stream's - §4.6.11 overlap / §4.6.7 LTP / §4.6.6 predictor state threads - independently. An SBR-signalling ASC (explicit AOT-5 wrapper or - implicit AAC-LC-only) rides the same §4.6.18 auto-detect the ADTS - path uses and emits dual-rate output, and a PS payload synthesizes - stereo through the Annex 8.A tool. Pinned against the - `aac-latm-stream` fixture - (stereo, 44.1 kHz) to a §8 PCM-RMS error ratio of 0.0004, proven - bit-identical to a hand-fed `decode_raw_data_block` pass, and — for - a re-multiplexed HE-AAC v1 stream (both signalling modes) — - byte-identical to the ADTS decode. - -The runtime `Decoder` (`codec_decoder::AacDecoder`) auto-detects its -carrier on the first packet and routes LOAS packets through `LoasDecoder` -(see "Runtime `Decoder` registration" below). The `EPMuxElement()` EP-tool -payload de-interleave decodes via `LoasDecoder::decode_all_ep` (see -the EP section below). - -### Stream decode + PCM output - -- **Integer-PCM rendering** (`pcm`) — §4.6.11 filterbank `f64` time - signal → 16-bit signed PCM: `nint` (the §1.3 `NINT()` round-half- - away-from-zero operator), `to_s16` (round + saturate), `channel_to_s16`, - and `interleave_s16` (element-order interleave). The conversion is the - only output-rendering step (no resampler / dither), so it is fully - spec-determined. The **canonical channel reorder** for default - `channelConfiguration` layouts (Table 1.19, see `channel_map` below) is - applied to the per-channel buffers *before* this interleave. -- **Default-config channel reorder** (`channel_map`) — ISO/IEC 14496-3 - §1.6.3.5 / Table 1.19. A `raw_data_block()` lists its channel elements - in bitstream order, so the decoder produces channels in element order - (e.g. a 5.1 stream as `C, L, R, Ls, Rs, LFE` for `SCE, CPE, CPE, LFE`); - `channel_map::reorder_channels` permutes them into the canonical - interleaved order that `oxideav_core::ChannelLayout` adopts (the - WAVE_FORMAT_EXTENSIBLE / BS.775 convention — 5.1 becomes - `L, R, C, LFE, Ls, Rs`). The driver threads the signalled - `channelConfiguration` through `decode_raw_data_block` and applies the - reorder for every default config **1–7** — mono / stereo are identity - permutations and config 7 (the Table 1.19 7.1 arrangement: centre + - inner Lc/Rc centre-front pair + outer L/R front pair + surround pair - + LFE) rank-sorts to `L, R, C, LFE, Lc, Rc, Ls, Rs`. **Config 0 - (PCE-defined) layouts are also mapped**: `channel_map::pce_speaker_assignment` implements the - ISO/IEC 13818-7 §8.5.2.2 rules — the front list center-outward - (lone SCE = center, SCE pairs L-then-R, two front pairs = the - Table 42 inner Lc/Rc + outer L/R arrangement), the side list front - to back, the back list outside-in (outer pair = side surround, - inner = rear; a final unpaired SCE = rear center), one LFE — keyed - by `(element kind, instance tag)` so the block's element order is - irrelevant; unmappable shapes fall back to element order. The - decode driver captures an in-band PCE (§8.5.2.2 persistence), - `StreamDecoder::set_program_config` installs an out-of-band - (ASC-inline) one, and the LATM driver does so automatically. The - whole path is validated end to end in `tests/multichannel_mp4.rs` - (a minimal ISO 14496-12 sample-table + esds walk): the 5.1 - config-6 fixture at 2.4e-4–8.9e-4 per-channel err/sig RMS, the - **7.1 PCE fixture at 2e-5–2.9e-4**, and the **hexagonal custom - 6.0 PCE fixture at 2.2e-4–7.8e-4** — every speaker carries a - distinct source tone, so the per-channel ratios pin the mapping - (silent LFEs reproduced exactly). -- **Stream-level ADTS decode driver** (`decode`) — `StreamDecoder` walks - the §4.4.2.1 `raw_data_block()` of each ADTS frame above the - per-element driver, keying one `ElementDecoder` per - `(syntactic-element-id, element_instance_tag)` slot so every element's - §4.6.11 overlap / §4.6.7 LTP / §4.6.6 predictor state persists across - frames, and renders to element-order interleaved s16 PCM. `decode_all` - walks a whole raw-ADTS buffer (ID3v2-skip + `aac_frame_length` - framing). **The decoded PCM is validated against the staged - `expected.wav` corpus**: the two PNS-free ADTS fixtures - (`aac-lc-mono-8000-16kbps-adts`, `aac-lc-intensity-stereo`) are - **99.9% byte-exact** to the reference s16 output with a **max error of - 1 LSB** — the residual is purely the difference between this crate's - `f64` direct-sum IMDCT and a `float32` fast transform. The PNS-bearing - fixtures are compared in the PCM RMS domain (per the fixtures-doc §8), - where the error-to-signal RMS ratio stays below 0.1%; full - byte-exactness on those is precluded by the §4.6.13.3 spec-undefined - noise-phase RNG (energy is normative, phase is not). A - `coupling_channel_element()` (CCE) carried in the block is **decoded - and applied**: the block walk is two-pass, so the §4.6.8.3.3 - coupling contribution lands on the addressed SCE / CPE channels at - the signalled `cc_domain` stage whether the CCE precedes or follows - its targets (see the `cce` bullet above). Multi-`raw_data_block` - frames decode as consecutive 1024-sample blocks (per-block channel - render + time concatenation), and `decode_adts_frame` verifies the - whole §8.1.1 `error_check()` CRC layer when - `protection_absent == 0` (see `adts_crc` above). -- **Runtime `Decoder` registration** (`codec_decoder`) — `AacDecoder` - adapts the persistent `StreamDecoder` / `LoasDecoder` into the - framework's packet-in / frame-out `oxideav_core::Decoder` trait. It - **auto-detects the carrier** on the first packet — the `0xFFF` ADTS - syncword vs. the `0x2B7` LOAS `AudioSyncStream` syncword — and then - routes every later packet the same way: ADTS frames through - `StreamDecoder::decode_frame` (ID3v2-skip + `aac_frame_length` - framing; one or many frames per packet), LOAS packets through - `LoasDecoder::decode_all` (one or many sync frames per packet, with - the `StreamMuxConfig` and per-stream state threaded across packets). - `receive_frame` returns one interleaved-S16 `AudioFrame` (1024 - samples/channel) per decoded access unit, `flush` drains to `Eof`, - and `reset` drops both backends and re-arms carrier detection for a - clean post-seek restart. `register()` installs it under id `"aac"`, - claiming the MP4 object-type `0x40`, WAVEFORMATEX `0x00FF` / `0x1601`, - the `mp4a` / `aac ` FourCCs, and the Matroska `A_AAC` CodecID; the - probe scores a structurally-confirmed ADTS header at 1.0 and a bare - LOAS syncword at 0.9 to win shared tags. Both carrier outputs are - pinned byte-identical to their underlying `StreamDecoder` / - `LoasDecoder`. - -### AAC-LC encoder - -- **`encoder`** — the §4.5/§4.6-written-forward AAC-LC encode chain. - `StreamEncoder` consumes interleaved S16 PCM hop by hop - (`encode_frame` / `finish` / one-shot `encode_all`) and emits one - complete ADTS frame per 1024-sample hop with a 1024-sample encoder - delay. Per hop: an energy-jump transient detector over the - `[hist | cur]` subblock grid drives the §4.6.11.3.2 - `ONLY_LONG → LONG_START → EIGHT_SHORT → LONG_STOP` state machine; - the §4.6.11.3.1 forward MDCT runs under the same composite windows - the decoder synthesizes with (one 2048-point transform for long - sequences, eight 256-point transforms at `448 + j·128` for short); - `EIGHT_SHORT` frames merge envelope-alike adjacent windows into - §4.5.2.3.4 window groups (one scalefactor/section track per group, - §4.5.2.3.5 interleaved transmission order; the emitted 7-bit mask - is pinned as the exact inverse of the decoder-side derivation); - per-band scalefactors follow a masking-spread rule (band target - magnitude `42·(peak_b/peak_frame)^½`, sub-step bands culled to - `ZERO_HCB`) with the DPCM ±60 track threaded across window groups; - a bidirectional rate loop (±4 scalefactor ladder + ±1..3 fine pass) - fits each frame to the bitrate-derived byte budget; codebooks and - sections come from a **measured-bit-cost dynamic program** (every - candidate same-class run priced at its `section_data()` header - overhead plus the cheapest Table 4.95 book, actual codeword + - sign + escape bits measured with the real tuple writer — pinned - never-larger than the classic smallest-LAV + merge rule), and - long frames additionally price a §4.4.6.3 `pulse_data()` variant - (band outliers reduced to the rest-of-band floor, restored - bit-exactly by the decoder's §4.6.3.3 fix-up; kept only when the - measured channel stream is smaller). - Stereo pairs code per-band §4.6.8.1 M/S (`m=(l+r)/2`, `s=(l−r)/2` - where the transform concentrates band energy, emitted as - `ms_mask_present` 2 / 1+mask / 0) inside a `common_window` CPE — - long frames per sfb, short frames per `(window group, sfb)` under - the pair's **joint** grouping (decided once on the pair envelope; - independent decisions would desync the shared `ics_info`). - **Every Table 1.19 default channel layout encodes** — 1–6 and 8 - (7.1) channels: the element-plan `raw_data_block()` assembly (SCE - / `common_window` CPE / LFE with per-kind instance tags), - canonical-order input permuted by the exact inverse of the - decoder's §1.6.3.5 reorder, and §4.5.2.1.3-conforming LFE elements - (always ONLY_LONG / sine through the frame's block switching, no - TNS, only the lowest 12 spectral lines transmitted). - Round-trips through the crate's own decoder: multitone 128 kbps at - 0.016 err/sig RMS, staged-fixture transcodes at 0.0008–0.003, - identical-channel stereo at 1.02× the mono stream size (short-run - identical channels decode L exactly equal to R), multichannel - layouts pinned with one distinct tone per speaker, and the - wire-level window-sequence walk pins the exact - `LongStart → EightShort → LongStop` pattern around a percussive - burst; a deterministic bit-flip/truncation battery covers the - multichannel and grouped-short streams. -- **`codec_encoder`** — the frame-in / packet-out - `oxideav_core::Encoder` adaptor (`make_encoder`, honouring - `sample_rate` / `channels` (1–6, 8) / `bit_rate`, default - 64 kbps/channel); registered alongside the decoder under id - `"aac"`, and re-exported as `encoder::make_encoder` per the - workspace dual-API convention. - -### ER BSAC (AOT 22) — noiseless-coder bring-up (§4.4.2.6 / §4.5.2.6 / §4.6.4) - -The Bit-Sliced Arithmetic Coding decoder roster is implemented and -its front half is **conformance-pinned against the ISO/IEC 14496-26 -`er_bs*` corpus**; the spectral bit-slice probability *selection* -of the deployed encoder diverges from the printed spec and is the -component still open (see below): - -- **Numeric tables** (`bsac_tables`) — Tables 4.A.31–4.A.77 - transcribed from the staged spec PDF: the `cband_si_type` - parameter matrix, the scalefactor / `cband_si` / stereo / PNS - cumulative-frequency models, the Table 4.A.34 context-position - map, the Table 4.A.35/36 `min_p0`/`max_p0` budget clamps, and the - 22 spectral probability tables with the printed alias scheme - resolved. (The 2001 and 2009 editions print *different* alias - schemes — tables 11–22 onto 9/10 alternating with sub-MSB zero - rows from 7/8 in 2009, everything onto 10 with zero rows from 8 - in 2001 — plus one conflicting cell in table 7; both were - transcribed and tested.) -- **Arithmetic decoder** (`bsac_arith`) — the §4.5.2.6.2.7.4 - procedure exactly as listed (`decode_symbol` over 14-bit cumfreq - models, binary `decode_bit`, the `half[]` renorm schedule, 30-bit - init, zero-stuffing segment reader), round-tripped against a - spec-inverse test encoder over every model. -- **Layer geometry** (`bsac_layer`) — the §4.5.2.6.2.4/5 roster: - base sub-layer split, per-layer coding-band / spectral / sfb - coverage (the literal `end_sfb = sfb + 1` one-band lookahead, - corpus-confirmed), `layer_si_maxlen`, the rate-anchored - `layer_bit_offset` derivation with the overflow/underflow - redistribution, and the SBA `terminal_layer` marks. -- **Block decode + reconstruction** (`bsac_decode`) — the - `bsac_header()` / `general_header()` raw-bit parse, the full - layer walk (side info, first-pass spectra, the - `bsac_lower_spectra()` refinement, budget carry between layers), - bit-slice reassembly with interleaved sign decode, and the AAC - back end (§4.6.2 dequant, group de-interleave, §4.6.8 stereo - hooks, §4.6.9 TNS, §4.6.11 filterbank) behind a persistent - `BsacDecoder`. -- **What the corpus pins** (`tests/bsac_bringup.rs`, corpus-gated): - on `er_bs01_48_ep0` the silent access units decode - **sample-exact** against the reference waveform (headers, layer - roster, arithmetic side-info decode all in sync), and on content - frames a TDAC oracle (the §4.6.11 perfect-reconstruction - property recovers each frame's exact transmitted spectrum from - the reference PCM) confirms the decoded `cband_si` MSB plane and - scalefactor gains match the deployed encoder precisely. -- **The open divergence**: the §4.6.4.2.3 spectral-bit probability - selection as printed decodes the wrong sliced bits partway into - the first coding band (both editions' alias readings, several - structural variants, and every printed row under every position - mapping tried were tested against the oracle truth). A - constraint solver over the real stream proves a consistent - context→p0 dictionary *exists* — the symbol order, sign - interleave and context classes are right — but its values match - no printed row (the all-zero-context position demands - `p0 ∈ {0x3700..=0x3a00}`; every plausible row prints `0x3b00+` - there). A clean-room behavioural trace of the deployed p0 - selection (or the corrigendum text) is the standing docs ask; - `tests/bsac_bringup.rs` carries the divergence locator and the - solver instrument, and `tests/iso_14496_26_conformance.rs` - reports the structural decode rate (618/703 AUs on - `er_bs01_48_ep0`) without asserting PCM until the rule lands. - -## Not yet supported - -- **The deployed ER AAC LD `tns_data()` filter record.** The - ISO/IEC 14496-26 LD conformance bitstreams transmit an - extra-spec TNS record: the corpus-resolved 1-bit-`n_filt` reading - (`docs/audio/aac/er-ld-tns-divergence.md` §0, implemented here) - reconciles the *structure* — every AU parses to its boundary — but - the reference waveforms show the record carries a real - variable-length filter (per-AU record lengths ≈ 19–61 bits, in - 3-bit increments) whose layout matches no Table 4.54/4.155 - reading (a grammar search over length 4/6 × order 3/5 × 1–2 - filters × optional direction/compress fields, decoded *and* - applied, reconciles none of it). TNS-bearing LD AUs (~6 % of the - `er_ad1103*` family) therefore decode with wrong PCM until a - behavioural trace of the deployed record lands; the conformance - harness masks them (and bounds the LTP-setup vectors coarsely, - since LD LTP history includes those AUs). -- Encoder-side tool remainders — the end-to-end AAC-LC encoder (see - `encoder` below) covers block switching with §4.5.2.3.4 short-frame - grouping, M/S on both frame shapes, the scalefactor/quantizer rate - loop with measured-bit-cost codebook/section choice, every - Table 1.19 default channel layout, and opt-in §4.6.13 PNS emission - (`StreamEncoder::set_pns` — off by default because a single-frame - spectral statistic cannot tell true noise from noise-shaped - deterministic content such as sweeps; default-on awaits a - cross-frame tonality measure) and default-on §4.6.9 TNS emission - (`encoder_tns`: per-window Levinson-Durbin prediction-gain decision - under a time-domain temporal-envelope gate, PARCOR quantised on - the §4.6.9.3 4-bit arcsine grid, and the §4.6.7.4.1 all-zero - analysis pass derived from the *wire* coefficients so the - decoder's all-pole synthesis is its exact inverse) and opt-in - §4.6.8.2 intensity-stereo emission - (`StreamEncoder::set_intensity_stereo` — correlated high bands - transmitted once with codebook 15/14 + `is_pos` on the §4.6.8.1.4 - track; off by default because intensity coding discards the - pair's side information) and measured §4.4.6.3 `pulse_data()` - emission (long-frame outlier-over-floor bands, kept only when the - whole channel stream prices smaller; the decode-side fix-up - restores the identical quantized spectrum), keeps - PNS and IS long-frame-only (CPE PNS emits the §4.6.13.3 `ms_used` - correlated-noise signalling — shared random vector — for - both-channels-noise bands correlating above 0.5), and has no - PCE-driven custom layouts (7-channel and beyond-7.1 shapes; the - Table 1.19 defaults 1–6 and 8 all encode). -- SSR remainders — the §4.6.12 gain-control tool is now implemented - and wired **end to end** (front-half filterbank, gain - reconstruction, IPQF — see the "SSR gain control" section above), - and a writer-assembled AOT-3 fixture driving non-unity gain - ladders through all four window sequences (with the §4.6.12.3.3 - variable 1024/1472/576 frame lengths) is staged in the docs corpus - (`aac-ssr-gain-control-adts`; no encoder for AOT 3 exists anywhere, - so a captured conformance stream remains welcome — a black-box - validator binary reports SSR gain control unimplemented, so there - is no external oracle for the ladders). Still open: the 13818-7 - SSR-profile *bandwidth-scalable* output modes (decoding only 1–3 - PQF bands at a reduced rate) are not selectable — the decoder - always reconstructs the full-rate signal. (The Main frequency-domain predictor, - §4.6.6, is now - fully wired into `element_decode` for the AAC Main object type on long - windows — see `predictor` above. LTP, §4.6.7, is likewise wired in - with the §4.6.7.4.1 / Figure 4.30 TNS-analysis-in-loop ordering. - The ISO/IEC 14496-3:**2001** Table 4.55 short-window LTP *syntax* — - the per-short-window `ltp_short_used` / `ltp_short_lag_present` / - `ltp_short_lag` loop that the 2009 edition removed ("LTP is - restricted to long windows only", §4.6.7.1 2009) — is parsed and - re-encoded under the explicit `LtpEdition::Iso2001` selector - (`parse_ltp_data_edition` / `write_ltp_data_edition`; the two - editions are wire-incompatible there and nothing in-band signals - which one a stream follows). The short-window *synthesis* stays - unimplemented: the 2001 §4.6.7.3 text defines the `x_rec` buffer - arrangement once but never fixes the per-subframe index origin for - the eight windows, and no LTP fixture exists to disambiguate. The - ER AAC LD long-window LTP — 10-bit lag, `ltp_lag_update` repeat, - `M = N/2` — is implemented and exercised by the staged LD - fixtures.) -- SBR/PS remainders — the §4.6.18 SBR tool **and** the subpart-8 PS - tool are **implemented end to end** (see the sections above) and - wired into the ADTS / LATM `StreamDecoder` paths and the runtime - `Decoder`, validated against the HE-AAC v1 (99.98% sample-exact) - and HE-AAC v2 (5e-5 RMS) fixtures, with the 10-bit - `bs_sbr_crc_bits` of every `EXT_SBR_DATA_CRC` payload now - **verified** (§4.4.2.8.1 `G10`, zero init, over the Table 4.62 - region — see `adts_crc`; a derived type-14 fixture is staged as - `he-aac-v1-sbrcrc-adts`). The §4.6.18.4.3 downsampled-output mode - and the §4.6.18.8 low-power variant are now **both selectable end - to end** (see the SBR back-end section above). Still open: SBR is - defined here over the 1024-line core only — an SBR payload on a - 960-line or LD stream is rejected - (`Error::SbrUnsupportedFrameFamily`; the §4.6.19 LD SBR tool is - ELD's and stays out of scope) — and low-power PS is undefined by - design (the subpart-8 tool needs the complex QMF domain, so LP + - PS is rejected). The coupling-channel (CCE) tool is - decoded **and applied** end to end (`cce` + the two-pass stream walk; - see the tool-chain section above), validated against the - filterbank-linearity identity on writer-assembled CCE streams; a - third-party CCE-bearing conformance fixture would still be a welcome - external cross-check. -- **ER BSAC (AOT 22) PCM** — the noiseless-coder front half - (headers, layer geometry, arithmetic side-info decode) is - conformance-pinned, but the deployed encoder's spectral bit-slice - probability selection diverges from every reading of the printed - §4.6.4.2.3 / Table 4.A.34 selection (see the BSAC section above), - so reconstructed PCM does not yet match the reference waveforms. - Also out of scope until then: SBA-mode segment scheduling - (`sba_mode == 1`), BSAC LTP, BSAC PNS (the noise-energy PCM - conventions need a working spectral decode to pin), the - `zero_code` extended part (channel / SBR / MPEG-Surround - extensions), and the §4.5.2.6.1 multi-ES `bsac_payload()` - large-step-layer reassembly (the `er_bs02`-style carriage). -- Error-resilience remainders — the ER story is now wired end to end - for ER AAC LC (AOT 17), **ER AAC LTP (AOT 19)** and ER AAC LD - (AOT 23): the ER channel-element body - (`ics_body::IcsBody::parse_er`) selects all three §4.4.6 - resilience branches, the `reordered_spectral_data()` payload is - decoded and encoded (`hcr_decode`), and the §4.4.2.3 Table 4.19 - `er_raw_data_block()` driver - (`StreamDecoder::decode_er_raw_data_block`) walks the fixed - per-`channelConfiguration` element sequence for all three AOTs — - reachable from LATM (the LOAS driver routes AOT-17/19/23 layers - there with the ASC's resilience triplet and the ASC-resolved - §4.5.1.1 frame family). AOT 17 is pinned bit-identical to the - equivalent non-resilient decode of the same spectra - (`aac-er-hcr-loas`); AOT 19 — the §4.6.7 LTP tool over the - Table 4.19 walk (11-bit lag, `M = 0`, per-element `x_rec` history - across frames) — is pinned bit-identical to the equivalent AOT-4 - decode with LTP active (SCE and pair-LTP CPE, plain and HCR - spectra); AOT 23 is pinned by the staged LD fixtures (see the - frame-length families section above). **ER AAC scalable (AOT 20) - now decodes end to end** (see the scalable section above), and the - §1.8 EP tool — `ErrorProtectionSpecificConfig()`, SRCPC / RS / - interleaving, the `ep_frame()` codec and the LOAS `EPMuxElement` / - `EPAudioSyncStream` carrier — is implemented (see the EP section - above). Still open: the §4.5.2.4 Table 4.148/4.149 per-element - category *split* of the codec payloads themselves (reassembling an - er_raw_data_block whose bits arrive as separate - error-sensitivity-category instances under `epConfig == 1` / - `directMapping` — the ep_frame class concatenation covers the - in-order case), and an encoder-produced HCR conformance stream as - an external cross-check. -- LATM/LOAS transport framing (§1.7) — the `StreamMuxConfig()`, - `AudioMuxElement()`, `PayloadLengthInfo()`, `PayloadMux()`, - `LatmGetValue()`, `AudioSyncStream()` and `EPAudioSyncStream()` - bitstream walkers are now implemented and tested (see the - LATM / LOAS transport section above), with the `crcCheckSum` - recomputed against the §1.8.4.5 `CRC8` generator in the `crc` - module. The runtime `Decoder` LOAS entry point that routes the - recovered `MuxPayload` raw-data-blocks into a `StreamDecoder` is now - wired (`latm::LoasDecoder` + the `codec_decoder` carrier - auto-detection above). The `EPMuxElement()` EP-tool payload - de-interleave is now implemented (`LoasDecoder::decode_all_ep`; see - the EP section above). (ADTS `adts_error_check()` CRC validation — - the 192/128-bit region selection with the double-protection edge - cases plus the ISO/IEC 11172-3 §2.4.3.1 code — landed in the - dedicated `adts_crc` module; see the bitstream-parsing section.) - -## License - -MIT — see [LICENSE](./LICENSE). diff --git a/crates/vendor/oxideav-aac/VENDOR.toml b/crates/vendor/oxideav-aac/VENDOR.toml deleted file mode 100644 index bd9fe575..00000000 --- a/crates/vendor/oxideav-aac/VENDOR.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Written by scripts/vendor-oxideav.sh. Do not edit, and do not -# hand-edit the vendored sources beside it — change them upstream -# and re-run the script. -source = "https://github.com/OxideAV/oxideav-aac" -commit = "719f1f594aef3465ecf2d718685bc27f8e797423" -describe = "v0.1.6-77-g719f1f5" -version = "0.1.6" -vendored_at = "2026-08-24T08:53:00Z" -patches = [] diff --git a/crates/vendor/oxideav-aac/src/adts.rs b/crates/vendor/oxideav-aac/src/adts.rs deleted file mode 100644 index 4331d29e..00000000 --- a/crates/vendor/oxideav-aac/src/adts.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! ADTS — *Audio Data Transport Stream* — fixed-header parser. -//! -//! ISO/IEC 13818-7 §1.A.2.2.1 defines the ADTS fixed-header (28 bits) -//! and §1.A.2.2.2 the variable-header (28 bits), followed by an -//! optional 16-bit CRC and one or more `raw_data_block()` payloads. -//! The header layout, MSB first: -//! -//! | bits | field | -//! |------|------------------------------------------------| -//! | 12 | `syncword` — required `0xFFF` | -//! | 1 | `ID` — MPEG-4 (`0`) vs MPEG-2 (`1`) | -//! | 2 | `layer` — required `0b00` | -//! | 1 | `protection_absent` — `1` ⇒ no CRC follows | -//! | 2 | `profile_ObjectType` — ADTS profile field | -//! | 4 | `sampling_frequency_index` | -//! | 1 | `private_bit` | -//! | 3 | `channel_configuration` | -//! | 1 | `original_copy` | -//! | 1 | `home` | -//! | 1 | `copyright_identification_bit` | -//! | 1 | `copyright_identification_start` | -//! | 13 | `aac_frame_length` — total frame bytes | -//! | 11 | `adts_buffer_fullness` | -//! | 2 | `number_of_raw_data_blocks_in_frame` (N − 1) | -//! -//! followed by either: -//! -//! * `protection_absent == 1` ⇒ no CRC; payload starts at byte 7. -//! * `protection_absent == 0` ⇒ 16-bit CRC, payload starts at byte 9. -//! -//! Note the field ADTS calls `profile_ObjectType` is **one less** than -//! the `audioObjectType` defined in ISO/IEC 14496-3 Table 1.16. ADTS -//! `profile_ObjectType == 1` is therefore AAC LC (AOT 2). -//! -//! The `sampling_frequency_index` mapping follows ISO/IEC 14496-3 -//! Table 1.18; this module exposes [`AdtsHeader::sample_rate`] which -//! resolves the index to a frequency in Hz. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::{Error, Result}; - -/// Fixed sync pattern at the start of every ADTS frame. -pub const ADTS_SYNCWORD: u16 = 0x0FFF; - -/// Header byte count when `protection_absent == 1` (no CRC). -pub const ADTS_HEADER_BYTES_NO_CRC: usize = 7; - -/// Header byte count when `protection_absent == 0` (16-bit CRC after -/// the fixed/variable-header pair). -pub const ADTS_HEADER_BYTES_WITH_CRC: usize = 9; - -/// ISO/IEC 14496-3 Table 1.18 — `samplingFrequencyIndex`. -/// -/// Indices 13 and 14 are reserved. Index 15 signals an explicit -/// 24-bit rate in `AudioSpecificConfig` but is *not* legal in an -/// ADTS header (the ADTS field is 4 bits). -pub const ADTS_SAMPLE_RATES_HZ: [u32; 13] = [ - 96_000, 88_200, 64_000, 48_000, 44_100, 32_000, 24_000, 22_050, 16_000, 12_000, 11_025, 8_000, - 7_350, -]; - -/// Resolved ADTS fixed + variable header. See module docs for the -/// per-field bit layout. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AdtsHeader { - /// MPEG version indicator. `false` ⇒ MPEG-4 (`ID = 0`), `true` ⇒ - /// MPEG-2 (`ID = 1`). The decoder behaviour is otherwise - /// identical; the bit only affects which extensions are legal in - /// downstream `raw_data_block()` payloads (PNS / LTP are MPEG-4 - /// only). - pub mpeg_version_mpeg2: bool, - - /// `true` ⇒ no 16-bit CRC follows the variable header. The frame - /// payload starts at byte 7 instead of byte 9. - pub protection_absent: bool, - - /// 2-bit ADTS `profile_ObjectType` field as read from the wire. - /// This is `audioObjectType − 1` per ISO/IEC 13818-7 §1.A.2 (so - /// `0` = Main, `1` = LC, `2` = SSR, `3` = (LTP) reserved in - /// 13818-7 but used by 14496-3). - pub profile: u8, - - /// 4-bit `sampling_frequency_index`. Use [`AdtsHeader::sample_rate`] - /// for the resolved Hz value. - pub sampling_frequency_index: u8, - - /// 3-bit `channel_configuration`. ISO/IEC 14496-3 Table 1.19: - /// `0` ⇒ defined by an inline PCE in the payload, `1` ⇒ mono, - /// `2` ⇒ stereo, …, `7` ⇒ 7.1 surround. - pub channel_configuration: u8, - - /// 13-bit `aac_frame_length` — total frame size in bytes, - /// including the header itself and (if present) the CRC. - pub aac_frame_length: u16, - - /// 11-bit `adts_buffer_fullness`. `0x7FF` is the spec-mandated - /// "VBR / unknown" sentinel. Phase 1 does not enforce buffer - /// modelling. - pub adts_buffer_fullness: u16, - - /// Number of `raw_data_block()` payloads contained in this frame. - /// The wire field is `N − 1`; this is the resolved count (≥ 1). - pub number_of_raw_data_blocks_in_frame: u8, -} - -impl AdtsHeader { - /// Parse an ADTS fixed + variable header from the start of `data`. - /// On success returns the [`AdtsHeader`] and the byte offset where - /// the first `raw_data_block()` payload begins (7 if - /// `protection_absent == 1`, 9 otherwise). - /// - /// CRC validation is **deferred**: when `protection_absent == 0` - /// this routine confirms the CRC bytes are present (i.e. the - /// input is at least 9 bytes long) but does not verify the CRC - /// value itself. - pub fn parse(data: &[u8]) -> Result<(Self, usize)> { - if data.len() < ADTS_HEADER_BYTES_NO_CRC { - return Err(Error::UnexpectedEnd); - } - - let mut br = BitReader::new(data); - - // 12-bit syncword - let sync = br.read_u32(12).map_err(|_| Error::UnexpectedEnd)? as u16; - if sync != ADTS_SYNCWORD { - return Err(Error::AdtsSyncNotFound); - } - - // 1-bit ID, 2-bit layer, 1-bit protection_absent - let mpeg_version_mpeg2 = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let layer = br.read_u32(2).map_err(|_| Error::UnexpectedEnd)?; - if layer != 0 { - return Err(Error::AdtsLayerNonZero); - } - let protection_absent = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - - // 2-bit profile, 4-bit sampling_frequency_index, 1-bit private_bit - let profile = br.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; - let sampling_frequency_index = br.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; - if sampling_frequency_index >= 13 { - return Err(Error::AdtsReservedSampleRateIndex); - } - let _private_bit = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - - // 3-bit channel_configuration, 1-bit original_copy, 1-bit home - let channel_configuration = br.read_u32(3).map_err(|_| Error::UnexpectedEnd)? as u8; - let _original_copy = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let _home = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - - // 1-bit copyright_identification_bit, 1-bit copyright_identification_start - let _copyright_identification_bit = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let _copyright_identification_start = br.read_bit().map_err(|_| Error::UnexpectedEnd)?; - - // 13-bit aac_frame_length, 11-bit adts_buffer_fullness, - // 2-bit number_of_raw_data_blocks_in_frame - let aac_frame_length = br.read_u32(13).map_err(|_| Error::UnexpectedEnd)? as u16; - let adts_buffer_fullness = br.read_u32(11).map_err(|_| Error::UnexpectedEnd)? as u16; - let raw_blocks_minus_one = br.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; - let number_of_raw_data_blocks_in_frame = raw_blocks_minus_one + 1; - - let payload_offset = if protection_absent { - ADTS_HEADER_BYTES_NO_CRC - } else { - ADTS_HEADER_BYTES_WITH_CRC - }; - - if (aac_frame_length as usize) < payload_offset { - return Err(Error::AdtsFrameLengthTooSmall); - } - - // When a CRC is present, confirm the trailing two bytes fit - // in `data`. We do not validate the CRC value in Phase 1. - if !protection_absent && data.len() < ADTS_HEADER_BYTES_WITH_CRC { - return Err(Error::UnexpectedEnd); - } - - Ok(( - AdtsHeader { - mpeg_version_mpeg2, - protection_absent, - profile, - sampling_frequency_index, - channel_configuration, - aac_frame_length, - adts_buffer_fullness, - number_of_raw_data_blocks_in_frame, - }, - payload_offset, - )) - } - - /// Resolved sample rate in Hz, from the - /// `sampling_frequency_index` via ISO/IEC 14496-3 Table 1.18. - pub fn sample_rate(&self) -> u32 { - // `parse` already rejects reserved indices, so the cast - // cannot index out of bounds for any successfully-parsed - // header. Defensive check kept regardless. - ADTS_SAMPLE_RATES_HZ - .get(self.sampling_frequency_index as usize) - .copied() - .unwrap_or(0) - } - - /// `audioObjectType` for the carried payload — the ADTS wire - /// field is one less than the `audioObjectType` defined by - /// ISO/IEC 14496-3 Table 1.16, so this returns `profile + 1`. - pub fn audio_object_type(&self) -> u8 { - self.profile + 1 - } - - /// Length in bytes of the `raw_data_block()` region that follows - /// the header (and CRC, if present): `aac_frame_length` minus - /// header overhead. - pub fn payload_len(&self) -> usize { - let header = if self.protection_absent { - ADTS_HEADER_BYTES_NO_CRC - } else { - ADTS_HEADER_BYTES_WITH_CRC - }; - (self.aac_frame_length as usize).saturating_sub(header) - } - - /// Serialise the fixed + variable header pair (7 bytes) — the - /// byte-exact inverse of [`AdtsHeader::parse`] for a - /// `protection_absent == 1` header. - /// - /// The four fields [`AdtsHeader::parse`] discards (`private_bit`, - /// `original_copy`, `home`, `copyright_identification_bit` / - /// `_start`) are written as `0`, matching what every fixture - /// header in the staged corpus carries. Encoders needing a CRC - /// (`protection_absent == 0`) must append the §1.A.2.2.3 16-bit - /// `crc_check` themselves after the returned 7 bytes; this - /// routine intentionally emits only the header pair so it stays - /// a pure function of the struct. - /// - /// Returns [`Error::AdtsEncodeInvalid`] when a field exceeds its - /// wire width or violates a normative constraint: - /// - /// * `profile > 3` (2-bit field), - /// * `sampling_frequency_index >= 13` (Table 1.18 reserved), - /// * `channel_configuration > 7` (3-bit field), - /// * `aac_frame_length >= 8192` (13-bit field) or smaller than - /// the header overhead itself, - /// * `adts_buffer_fullness > 0x7FF` (11-bit field), - /// * `number_of_raw_data_blocks_in_frame` outside `1..=4` - /// (2-bit `N − 1` field). - pub fn write(&self) -> Result<[u8; ADTS_HEADER_BYTES_NO_CRC]> { - if self.profile > 3 - || self.sampling_frequency_index >= 13 - || self.channel_configuration > 7 - || self.aac_frame_length >= (1 << 13) - || self.adts_buffer_fullness > 0x7FF - || !(1..=4).contains(&self.number_of_raw_data_blocks_in_frame) - { - return Err(Error::AdtsEncodeInvalid); - } - let overhead = if self.protection_absent { - ADTS_HEADER_BYTES_NO_CRC - } else { - ADTS_HEADER_BYTES_WITH_CRC - }; - if (self.aac_frame_length as usize) < overhead { - return Err(Error::AdtsEncodeInvalid); - } - - let mut bw = BitWriter::new(); - bw.write_u32(ADTS_SYNCWORD as u32, 12); - bw.write_bit(self.mpeg_version_mpeg2); - bw.write_u32(0, 2); // layer — required 0b00 - bw.write_bit(self.protection_absent); - bw.write_u32(self.profile as u32, 2); - bw.write_u32(self.sampling_frequency_index as u32, 4); - bw.write_bit(false); // private_bit - bw.write_u32(self.channel_configuration as u32, 3); - bw.write_bit(false); // original_copy - bw.write_bit(false); // home - bw.write_bit(false); // copyright_identification_bit - bw.write_bit(false); // copyright_identification_start - bw.write_u32(self.aac_frame_length as u32, 13); - bw.write_u32(self.adts_buffer_fullness as u32, 11); - bw.write_u32((self.number_of_raw_data_blocks_in_frame - 1) as u32, 2); - let bytes = bw.finish(); - debug_assert_eq!(bytes.len(), ADTS_HEADER_BYTES_NO_CRC); - let mut out = [0u8; ADTS_HEADER_BYTES_NO_CRC]; - out.copy_from_slice(&bytes); - Ok(out) - } -} diff --git a/crates/vendor/oxideav-aac/src/adts_crc.rs b/crates/vendor/oxideav-aac/src/adts_crc.rs deleted file mode 100644 index 15c274e5..00000000 --- a/crates/vendor/oxideav-aac/src/adts_crc.rs +++ /dev/null @@ -1,566 +0,0 @@ -//! ADTS `error_check()` and SBR `bs_sbr_crc_bits` CRC verification. -//! -//! Two independent CRC mechanisms protect an AAC bitstream, each with -//! its own polynomial, initial value, and covered region: -//! -//! 1. the **ADTS `crc_check`** — a 16-bit CRC present when the ADTS -//! fixed header signals `protection_absent == 0`. The protected-bit -//! region is normatively described by ISO/IEC 13818-7:2004 §8.1.1.1 -//! (semantics of `adts_error_check()` and the multi-raw-data-block -//! split variants, Tables 1.A.8–1.A.10 of ISO/IEC 14496-3:2009); -//! the CRC code itself is cited by 13818-7 §8.1.1.2 to ISO/IEC -//! 11172-3 §2.4.3.1: generator polynomial -//! `G(x) = x¹⁶ + x¹⁵ + x² + 1` (`0x8005`), initial register value -//! all-ones (`0xFFFF`), bits fed MSB-first in order of appearance, -//! no final inversion. -//! 2. the **SBR extension CRC** (`bs_sbr_crc_bits`) — a 10-bit CRC -//! carried at the head of an `EXT_SBR_DATA_CRC` (extension type 14) -//! fill payload. ISO/IEC 14496-3:2009 §4.4.2.8.1 (repeated in -//! §4.5.2.8.1): generator polynomial -//! `G10(x) = x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1`, initial value **zero**, -//! covering every `sbr_extension_data()` bit after the CRC field up -//! to (but excluding) the trailing `bs_fill_bits` alignment — i.e. -//! `num_sbr_bits − 10` bits (Table 4.62). -//! -//! Both codes run on the same MSB-first shift register: for each -//! message bit, the feedback is the incoming bit XORed with the -//! register's top bit; on feedback the register (shifted left one) -//! is XORed with the low-order generator terms. No zero-augmentation -//! flush and no output inversion follow — the register value after -//! the last message bit is the checksum. With a zero initial value -//! this equals the polynomial remainder `M(x)·xᵏ mod G(x)`, matching -//! the §4.4.2.8.1 "remainder" wording for the SBR code; the ADTS code -//! differs only by its all-ones initialisation. (The §1.8.4.5 CRC -//! family implemented in [`crate::crc`] is a *different* convention — -//! zero init **plus** a normative output-bit inversion — and covers -//! the LATM `crcCheckSum` / EP-tool codes, not these two.) -//! -//! ## ADTS protected-bit region (13818-7:2004 §8.1.1.1) -//! -//! For `adts_error_check()` (single raw data block, -//! `number_of_raw_data_blocks_in_frame == 0` on the wire) the bits fed -//! into the CRC, in order of appearance, are: -//! -//! * **all 56 bits** of `adts_fixed_header()` + `adts_variable_header()`; -//! * the **first 192 bits** of every SCE / CPE / CCE / LFE channel -//! element — *excluding* the 3-bit `id_syn_ele`, zero-padded to 192 -//! when the element is shorter; -//! * **additionally** the first 128 bits of the *second* -//! `individual_channel_stream` of every CPE (zero-padded to 128; -//! when the second ICS starts before the element's 192nd bit the -//! overlap is protected twice, each time in order of appearance); -//! * **all** bits of every `program_config_element()` and -//! `data_stream_element()` (again excluding `id_syn_ele`). -//! -//! Fill elements, the END marker, and the `crc_check` field itself are -//! not covered. -//! -//! `adts_raw_data_block_error_check()` (multi-RDB form, one 16-bit CRC -//! after each `raw_data_block()`) covers the same per-element regions -//! scoped to its block, *without* re-including the headers; the -//! headers plus every 16-bit `raw_data_block_position` are covered -//! once by `adts_header_error_check()`. -//! -//! ## Provenance -//! -//! Region selection and code parameters are transcribed from the -//! staged format specifications (ISO/IEC 14496-3:2009 Tables -//! 1.A.5–1.A.10 / Table 4.62 / §4.4.2.8.1 and ISO/IEC 13818-7:2004 -//! §8.1.1) via the clean-room region analysis in -//! `docs/audio/aac/aac-crc-regions.md`. ISO/IEC 11172-3 itself is not -//! staged; the `0x8005` / `0xFFFF` shift-register parameters are the -//! §2.4.3.1 values as recorded there. - -use oxideav_core::bits::BitReader; - -use crate::cce::CouplingChannelElement; -use crate::ics_body::IcsBody; -use crate::raw_data_block::{Element, IdSynEle, Walker}; -use crate::spectral_data::SpectralData; -use crate::{Error, Result}; - -/// Low-order terms of the ADTS generator `x¹⁶ + x¹⁵ + x² + 1` -/// (ISO/IEC 11172-3 §2.4.3.1 via 13818-7:2004 §8.1.1.2). -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub const ADTS_CRC_POLY: u32 = 0x8005; - -/// ADTS CRC initial register value (all ones). -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub const ADTS_CRC_INIT: u32 = 0xFFFF; - -/// Low-order terms of the SBR generator `x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1` -/// (ISO/IEC 14496-3:2009 §4.4.2.8.1). -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub const SBR_CRC_POLY: u32 = 0x0233; - -/// MSB-first CRC shift register (see module docs for the feedback -/// convention shared by the ADTS and SBR codes). -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -#[derive(Debug, Clone, Copy)] -pub struct CrcRegister { - reg: u32, - poly: u32, - mask: u32, - top: u32, -} - -impl CrcRegister { - /// A register configured for the ADTS `crc_check` code: 16 bits, - /// generator `0x8005`, initial value `0xFFFF`. - pub fn adts() -> Self { - CrcRegister { - reg: ADTS_CRC_INIT, - poly: ADTS_CRC_POLY, - mask: 0xFFFF, - top: 0x8000, - } - } - - /// A register configured for the SBR `bs_sbr_crc_bits` code: 10 - /// bits, generator `G10` (`0x233`), initial value zero. - pub fn sbr() -> Self { - CrcRegister { - reg: 0, - poly: SBR_CRC_POLY, - mask: 0x03FF, - top: 0x0200, - } - } - - /// Feed one message bit (MSB-first order). - #[inline] - pub fn feed_bit(&mut self, bit: bool) { - let feedback = ((self.reg & self.top) != 0) ^ bit; - self.reg = (self.reg << 1) & self.mask; - if feedback { - self.reg ^= self.poly; - } - } - - /// Feed `n` zero bits (the §8.1.1.1 zero-padding of short - /// elements). - pub fn feed_zeros(&mut self, n: u64) { - for _ in 0..n { - self.feed_bit(false); - } - } - - /// Feed the bit range `[start_bit, end_bit)` of `data`, MSB-first - /// within each byte. Bits past the end of `data` are fed as zero - /// (a region that overruns its buffer only ever does so via the - /// normative zero-padding). - pub fn feed_bit_range(&mut self, data: &[u8], start_bit: u64, end_bit: u64) { - for pos in start_bit..end_bit { - let byte = (pos / 8) as usize; - let bit = data.get(byte).is_some_and(|b| b & (0x80 >> (pos % 8)) != 0); - self.feed_bit(bit); - } - } - - /// The current register value (the checksum once the whole - /// protected region has been fed). - pub fn value(&self) -> u16 { - self.reg as u16 - } -} - -/// Compute the 10-bit SBR CRC over the bit range `[start_bit, -/// end_bit)` of `data` — the `sbr_extension_data()` payload bits -/// after the `bs_sbr_crc_bits` field, before the `bs_fill_bits` -/// (ISO/IEC 14496-3:2009 Table 4.62 / §4.4.2.8.1). -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub fn sbr_crc(data: &[u8], start_bit: u64, end_bit: u64) -> u16 { - let mut reg = CrcRegister::sbr(); - reg.feed_bit_range(data, start_bit, end_bit); - reg.value() -} - -/// One §8.1.1.1 protected region of a `raw_data_block()` payload: -/// the bit range `[start_bit, end_bit)` of the payload buffer, capped -/// and zero-padded to `pad_to` bits when a protection length applies -/// (192 for a channel element, 128 for a CPE's second ICS; `None` -/// feeds the whole range, the PCE / DSE case). -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ProtectedRegion { - /// First protected bit (absolute bit offset into the payload). - pub start_bit: u64, - /// One past the last payload bit of the region (the element end; - /// the fed length is additionally capped by `pad_to`). - pub end_bit: u64, - /// Normative protection length: feed `min(end_bit - start_bit, - /// pad_to)` payload bits, then zeros up to `pad_to`. - pub pad_to: Option, -} - -impl ProtectedRegion { - fn feed(&self, reg: &mut CrcRegister, payload: &[u8]) { - let len = self.end_bit.saturating_sub(self.start_bit); - match self.pad_to { - Some(pad) => { - let take = len.min(u64::from(pad)); - reg.feed_bit_range(payload, self.start_bit, self.start_bit + take); - reg.feed_zeros(u64::from(pad) - take); - } - None => reg.feed_bit_range(payload, self.start_bit, self.end_bit), - } - } -} - -/// Compute the single-RDB `adts_error_check()` CRC (ISO/IEC 14496-3 -/// Table 1.A.8, region per 13818-7:2004 §8.1.1.1): the 56 header bits -/// followed by every protected element region of the one -/// `raw_data_block()`. -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub fn adts_single_crc(header: &[u8], payload: &[u8], regions: &[ProtectedRegion]) -> u16 { - let mut reg = CrcRegister::adts(); - reg.feed_bit_range(header, 0, 56); - for r in regions { - r.feed(&mut reg, payload); - } - reg.value() -} - -/// Compute the multi-RDB `adts_header_error_check()` CRC (Table -/// 1.A.9): the 56 header bits followed by every 16-bit -/// `raw_data_block_position`, in order. -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub fn adts_header_crc(header: &[u8], positions: &[u16]) -> u16 { - let mut reg = CrcRegister::adts(); - reg.feed_bit_range(header, 0, 56); - for &p in positions { - for i in (0..16).rev() { - reg.feed_bit((p >> i) & 1 != 0); - } - } - reg.value() -} - -/// Compute one multi-RDB `adts_raw_data_block_error_check()` CRC -/// (Table 1.A.10): the protected element regions of a single -/// `raw_data_block()`, headers *not* re-included. -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub fn adts_rdb_crc(payload: &[u8], regions: &[ProtectedRegion]) -> u16 { - let mut reg = CrcRegister::adts(); - for r in regions { - r.feed(&mut reg, payload); - } - reg.value() -} - -/// Walk one `raw_data_block()` off `reader` (parse-only — no -/// reconstruction) and collect its §8.1.1.1 protected regions in -/// order of appearance: per channel element the post-`id_syn_ele` -/// 192-bit window (SCE / CPE / CCE / LFE), per CPE additionally the -/// second ICS's 128-bit window, and the full body of every PCE / DSE. -/// -/// The reader is left positioned after the block's END marker (byte -/// aligned), exactly where a multi-RDB `adts_raw_data_block_error_ -/// check()` field or the next block begins. Returns the regions; an -/// exhausted payload before an explicit END terminates the block the -/// same way the decode driver treats it. -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub fn collect_block_regions( - reader: &mut BitReader<'_>, - aot: u8, - fs: u8, -) -> Result> { - let mut regions = Vec::new(); - loop { - let elem_start = reader.bit_position(); - let Some(elem) = Walker::new(reader).next_element()? else { - return Ok(regions); - }; - match elem { - Element::ChannelElement { - kind: IdSynEle::Sce | IdSynEle::Lfe, - .. - } => { - let body = IcsBody::parse(reader, aot, fs, false)?; - let ics = body.ics_info.clone().ok_or(Error::ElementDecodeInvalid)?; - SpectralData::parse(reader, &ics, &body.section_data, fs)?; - regions.push(ProtectedRegion { - start_bit: elem_start + 3, - end_bit: reader.bit_position(), - pad_to: Some(192), - }); - } - Element::ChannelElement { - kind: IdSynEle::Cpe, - .. - } => { - let parsed = crate::decode::parse_cpe(reader, aot, fs)?; - let end = reader.bit_position(); - regions.push(ProtectedRegion { - start_bit: elem_start + 3, - end_bit: end, - pad_to: Some(192), - }); - regions.push(ProtectedRegion { - start_bit: parsed.second_ics_start_bit, - end_bit: end, - pad_to: Some(128), - }); - } - Element::ChannelElement { - kind: IdSynEle::Cce, - element_instance_tag, - } => { - CouplingChannelElement::parse_after_tag(reader, element_instance_tag, aot, fs)?; - regions.push(ProtectedRegion { - start_bit: elem_start + 3, - end_bit: reader.bit_position(), - pad_to: Some(192), - }); - } - Element::ChannelElement { .. } => return Err(Error::ElementDecodeInvalid), - Element::Data { .. } | Element::ProgramConfig(_) => { - regions.push(ProtectedRegion { - start_bit: elem_start + 3, - end_bit: reader.bit_position(), - pad_to: None, - }); - } - Element::Fill { .. } => {} - Element::End => return Ok(regions), - } - } -} - -/// Rewrite one `protection_absent == 1` single-raw-data-block ADTS -/// frame into its CRC-protected form: `protection_absent` cleared, -/// `aac_frame_length` grown by the 2 CRC bytes, and the Table 1.A.8 -/// `crc_check` computed over the §8.1.1.1 region inserted between the -/// header and the payload. Every other header bit (including the -/// fields [`AdtsHeader::parse`] does not surface) is preserved -/// verbatim. -/// -/// A frame that already carries a CRC is returned unchanged. A -/// multi-raw-data-block frame is rejected with -/// [`Error::NotImplemented`] (the Table 1.A.9/1.A.10 split form needs -/// a `raw_data_block_position` policy this helper does not invent). -pub fn protect_adts_frame(frame: &[u8]) -> Result> { - let (header, payload_offset) = crate::adts::AdtsHeader::parse(frame)?; - let frame_len = header.aac_frame_length as usize; - if frame_len < payload_offset || frame.len() < frame_len { - return Err(Error::UnexpectedEnd); - } - let frame = &frame[..frame_len]; - if !header.protection_absent { - return Ok(frame.to_vec()); - } - if header.number_of_raw_data_blocks_in_frame != 1 { - return Err(Error::NotImplemented); - } - let new_len = header.aac_frame_length + 2; - if new_len >= (1 << 13) { - return Err(Error::AdtsEncodeInvalid); - } - // Patch the header bytes in place: clear protection_absent (bit 0 - // of byte 1) and re-pack the 13-bit aac_frame_length (low 2 bits - // of byte 3, byte 4, top 3 bits of byte 5). - let mut h = [0u8; 7]; - h.copy_from_slice(&frame[..7]); - h[1] &= 0xFE; - h[3] = (h[3] & 0xFC) | ((new_len >> 11) as u8 & 0x03); - h[4] = (new_len >> 3) as u8; - h[5] = (h[5] & 0x1F) | (((new_len & 0x07) as u8) << 5); - - let payload = &frame[payload_offset..]; - let mut reader = BitReader::new(payload); - let regions = collect_block_regions( - &mut reader, - header.audio_object_type(), - header.sampling_frequency_index, - )?; - let crc = adts_single_crc(&h, payload, ®ions); - - let mut out = Vec::with_capacity(frame.len() + 2); - out.extend_from_slice(&h); - out.extend_from_slice(&crc.to_be_bytes()); - out.extend_from_slice(payload); - Ok(out) -} - -/// [`protect_adts_frame`] applied to every frame of a raw ADTS byte -/// stream (`aac_frame_length`-delimited walk to exhaustion). -pub fn protect_adts_stream(data: &[u8]) -> Result> { - let mut out = Vec::with_capacity(data.len()); - let mut pos = 0usize; - while pos + crate::adts::ADTS_HEADER_BYTES_NO_CRC <= data.len() { - let (header, _) = crate::adts::AdtsHeader::parse(&data[pos..])?; - let frame_len = header.aac_frame_length as usize; - if pos + frame_len > data.len() { - return Err(Error::UnexpectedEnd); - } - out.extend_from_slice(&protect_adts_frame(&data[pos..pos + frame_len])?); - pos += frame_len; - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Independent long-division reference: the MSB-feedback register - /// with initial value `I` over an `n`-bit message `M` computes, by - /// linearity, the remainder `(M(x)·xᵏ + I(x)·xⁿ) mod G(x)` — the - /// dividend is the k-zero-extended message with the init bits - /// XORed onto its leading `k` positions. - fn reference(poly_low: u32, k: u32, init: u32, bits: &[bool]) -> u32 { - let full = u64::from(poly_low) | (1u64 << k); - let mut dividend: Vec = bits.to_vec(); - dividend.extend(std::iter::repeat(false).take(k as usize)); - for (i, d) in dividend.iter_mut().enumerate().take(k as usize) { - *d ^= (init >> (k as usize - 1 - i)) & 1 != 0; - } - let mut reg: u64 = 0; - let topbit = 1u64 << k; - for &b in ÷nd { - reg = (reg << 1) | u64::from(b); - if reg & topbit != 0 { - reg ^= full; - } - } - (reg & ((1u64 << k) - 1)) as u32 - } - - fn to_bits(bytes: &[u8]) -> Vec { - bytes - .iter() - .flat_map(|&b| (0..8).rev().map(move |i| (b >> i) & 1 != 0)) - .collect() - } - - #[test] - fn adts_register_matches_long_division_reference() { - for msg in [ - &[][..], - &[0x00][..], - &[0xFF, 0xF1][..], - &[0x12, 0x34, 0x56, 0x78, 0x9A][..], - &[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03][..], - ] { - let bits = to_bits(msg); - let mut reg = CrcRegister::adts(); - for &b in &bits { - reg.feed_bit(b); - } - assert_eq!( - u32::from(reg.value()), - reference(ADTS_CRC_POLY, 16, ADTS_CRC_INIT, &bits), - "message {msg:x?}" - ); - } - } - - #[test] - fn sbr_register_is_plain_remainder() { - // Zero init ⇒ the register equals M(x)·x¹⁰ mod G10(x). - for msg in [&[0x5Au8, 0x33][..], &[0xFF, 0x00, 0xAB, 0xCD][..]] { - let bits = to_bits(msg); - let mut reg = CrcRegister::sbr(); - for &b in &bits { - reg.feed_bit(b); - } - assert_eq!( - u32::from(reg.value()), - reference(SBR_CRC_POLY, 10, 0, &bits), - "message {msg:x?}" - ); - } - } - - #[test] - fn sbr_poly_matches_crate_crc10_generator() { - // §4.4.2.8.1's G10 is the same polynomial as the §1.8.4.5 - // CRC10 row; only the init / inversion conventions differ. - assert_eq!( - u64::from(SBR_CRC_POLY), - crate::crc::CrcPoly::Crc10.generator() - ); - assert_eq!( - u64::from(ADTS_CRC_POLY), - crate::crc::CrcPoly::Crc16.generator() - ); - } - - #[test] - fn empty_message_yields_init_for_adts() { - // No message bits: the register never moves. - let reg = CrcRegister::adts(); - assert_eq!(u32::from(reg.value()), ADTS_CRC_INIT); - assert_eq!(CrcRegister::sbr().value(), 0); - } - - #[test] - fn appending_checksum_cancels_the_register() { - // Defining property of the MSB-feedback register: feeding the - // message and then its own checksum drives the register to 0. - for msg in [&[0x53u8, 0x91, 0x2C][..], &[0xFF, 0xF9, 0x5C, 0x80][..]] { - let bits = to_bits(msg); - let mut reg = CrcRegister::adts(); - for &b in &bits { - reg.feed_bit(b); - } - let crc = reg.value(); - for i in (0..16).rev() { - reg.feed_bit((crc >> i) & 1 != 0); - } - assert_eq!(reg.value(), 0, "message {msg:x?}"); - } - } - - #[test] - fn region_pads_short_elements_with_zeros() { - // A 40-bit element padded to 192 must equal feeding the 40 - // payload bits + 152 explicit zeros. - let payload = [0xA5u8; 8]; - let region = ProtectedRegion { - start_bit: 3, - end_bit: 43, - pad_to: Some(192), - }; - let mut a = CrcRegister::adts(); - region.feed(&mut a, &payload); - let mut b = CrcRegister::adts(); - b.feed_bit_range(&payload, 3, 43); - b.feed_zeros(152); - assert_eq!(a.value(), b.value()); - } - - #[test] - fn region_caps_long_elements_at_pad_to() { - // A 300-bit element only contributes its first 192 bits. - let payload = [0x3Cu8; 64]; - let region = ProtectedRegion { - start_bit: 5, - end_bit: 305, - pad_to: Some(192), - }; - let mut a = CrcRegister::adts(); - region.feed(&mut a, &payload); - let mut b = CrcRegister::adts(); - b.feed_bit_range(&payload, 5, 5 + 192); - assert_eq!(a.value(), b.value()); - } - - #[test] - fn header_crc_covers_positions() { - let header = [0xFFu8, 0xF1, 0x50, 0x80, 0x2F, 0xFF, 0xFC]; - let a = adts_header_crc(&header, &[]); - let b = adts_header_crc(&header, &[0x1234]); - assert_ne!(a, b, "positions must alter the header CRC"); - } -} diff --git a/crates/vendor/oxideav-aac/src/asc.rs b/crates/vendor/oxideav-aac/src/asc.rs deleted file mode 100644 index 080b64b8..00000000 --- a/crates/vendor/oxideav-aac/src/asc.rs +++ /dev/null @@ -1,819 +0,0 @@ -//! `AudioSpecificConfig` parser. -//! -//! ISO/IEC 14496-3 §1.6.2.1 Table 1.15 defines the canonical -//! `AudioSpecificConfig()` (ASC) as the out-of-band descriptor for -//! an MPEG-4 audio elementary stream. It carries -//! `audioObjectType`, `samplingFrequencyIndex` (and the 24-bit -//! escape rate when index is `0xf`), `channelConfiguration`, and a -//! per-AOT body (Table 1.17). -//! -//! Phase 1 parses the wrapper plus the body for **AOTs that route -//! to `GASpecificConfig`** (§4.4.1 Table 4.1) — the General Audio -//! branch covering all AAC variants: 1 (Main), 2 (LC), 3 (SSR), 4 -//! (LTP), 6 (scalable), 7 (TwinVQ), 17 (ER AAC LC), 19 (ER AAC -//! LTP), 20 (ER AAC scalable), 21 (ER TwinVQ), 22 (ER BSAC), 23 -//! (ER AAC LD). The hierarchical SBR (AOT 5) and PS (AOT 29) -//! outer-wrappers are also recognised: the parser reads the inner -//! `samplingFrequencyIndex` + (re-read) `audioObjectType` and -//! records `sbr_present` / `ps_present` so a later HE-AAC round can -//! drive SBR setup off the parsed ASC. -//! -//! All other AOTs return [`Error::UnsupportedAot`] so the spec -//! gap is explicit at the call site. -//! -//! ## What round 192 adds -//! -//! * The Table 1.15 trailing `syncExtensionType == 0x2b7` probe used -//! for *backward-compatible* implicit SBR / PS signalling in the -//! AudioSpecificConfig (§1.6.5, §1.6.6). After the per-AOT body -//! and `epConfig`, when `extensionAudioObjectType != 5` and the -//! carrier has `>= 16` bits remaining, the parser reads an 11-bit -//! `syncExtensionType` value: if it equals `0x2b7` it consumes a -//! nested `GetAudioObjectType()` and (when the resolved extension -//! AOT is `5`) the `sbrPresentFlag`, optional -//! `extensionSamplingFrequencyIndex` (with the same 24-bit escape -//! as the outer ASC), and a second 11-bit `syncExtensionType` -//! gated on `>= 12` further bits — if it equals `0x548` the -//! `psPresentFlag` follows. The AOT-22 (ER BSAC) extension branch -//! is also parsed: `sbrPresentFlag` (+ optional -//! `extensionSamplingFrequencyIndex`) then a mandatory 4-bit -//! `extensionChannelConfiguration`. The probe result lands in -//! [`AudioSpecificConfig::trailing_sbr_probe`] as -//! [`SbrExtensionProbe`]; when the probe resolves SBR or PS, -//! `asc.sbr_present` / `asc.ps_present` are updated to reflect -//! the implicit signalling. This entry point is exposed as -//! [`AudioSpecificConfig::parse_bits_bounded`] for carriers that -//! know the ASC bit length (LATM `StreamMuxConfig`, esds AudioObj -//! descriptor); the byte-slice [`AudioSpecificConfig::parse`] -//! computes the bound automatically. The original bit-level -//! [`AudioSpecificConfig::parse_bits`] keeps its no-probe -//! semantics so existing callers that pass a BitReader carrying -//! trailing carrier bytes are not surprised by a stray 11-bit -//! match. -//! -//! ## What is *not* parsed yet -//! -//! * `AOT 5` / `AOT 29` *implicit-extension* path **via the FIL -//! extension_payload**: when the outer AOT is 2 (LC) and the -//! SBR/PS extension is announced via the FIL `extension_payload` -//! inside the raw_data_block stream (not the ASC trailing probe), -//! the ASC alone does not carry the information — the decoder -//! must look at the FIL stream. Round 192 only resolves the -//! *ASC-side* implicit signalling (the Table 1.15 -//! `syncExtensionType == 0x2b7` probe). When neither signalling -//! form is present, the ASC parser correctly records -//! `sbr_present = false` / `ps_present = false` because no ASC -//! bit said otherwise. -//! -//! ## What round 177 adds -//! -//! * `GASpecificConfig` `extensionFlag == 1` body (Table 4.1): -//! AOT 22 (ER BSAC) emits a 5-bit `numOfSubFrame` + 11-bit -//! `layer_length`; AOTs 17 / 19 / 20 / 23 emit the 1-bit -//! `aacSectionDataResilienceFlag` + 1-bit -//! `aacScalefactorDataResilienceFlag` + 1-bit -//! `aacSpectralDataResilienceFlag` triplet; every AOT closes the -//! body with a 1-bit `extensionFlag3` (the Version 3 body behind it -//! is reserved per the spec's own "tbd in version 3" comment, so the -//! bit is surfaced but the body is rejected with -//! [`Error::UnsupportedAscExtensionFlag3`] when set). -//! * `epConfig` for ER object types (Table 1.15) — the 2-bit -//! `epConfig` field that follows the AOT body for AOTs 17, 19, 20, -//! 21, 22, 23, 24, 25, 26, 27, 39. `epConfig == 2` or -//! `epConfig == 3` further triggers the -//! `ErrorProtectionSpecificConfig()` body, which Phase 1 does not -//! parse — the ASC parser surfaces -//! [`Error::UnsupportedEpConfig`] in that case rather than -//! silently returning a partial ASC. - -use oxideav_core::bits::BitReader; - -use crate::adts::ADTS_SAMPLE_RATES_HZ; -use crate::pce::Pce; -use crate::{Error, Result}; - -/// Outer `audioObjectType` values for which the ASC body is -/// `GASpecificConfig` per Table 1.17. -const GA_AOTS: &[u8] = &[1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23]; - -/// AOTs that signal SBR (5) or SBR + PS (29) as an outer wrapper -/// around an inner GA AOT (typically 2 = LC). The ASC walks the -/// extension sample-rate/index and re-reads `GetAudioObjectType` -/// before dispatching to the inner body. -const SBR_AOT: u8 = 5; -const PS_AOT: u8 = 29; - -/// AOTs whose `GASpecificConfig` extension-flag body emits the 5-bit -/// `numOfSubFrame` + 11-bit `layer_length` pair (Table 4.1). -const GA_EXTENSION_NUM_OF_SUBFRAME_AOTS: &[u8] = &[22]; - -/// AOTs whose `GASpecificConfig` extension-flag body emits the three -/// error-resilience flags (Table 4.1). -const GA_EXTENSION_RESILIENCE_AOTS: &[u8] = &[17, 19, 20, 23]; - -/// AOTs whose ASC trailing body carries the 2-bit `epConfig` field -/// (Table 1.15 outer `switch (audioObjectType)` for the ER object -/// types). -const EP_CONFIG_AOTS: &[u8] = &[17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39]; - -/// Outer 11-bit `syncExtensionType` marker that introduces the Table -/// 1.15 trailing implicit-SBR signalling block. -pub const SYNC_EXTENSION_TYPE_SBR: u16 = 0x2b7; - -/// Inner 11-bit `syncExtensionType` marker that introduces the -/// `psPresentFlag` inside the SBR (`extensionAudioObjectType == 5`) -/// branch of the Table 1.15 trailing probe. -pub const SYNC_EXTENSION_TYPE_PS: u16 = 0x548; - -/// Width of the `syncExtensionType` field (Table 1.15). -pub const SYNC_EXTENSION_TYPE_BITS: u32 = 11; - -/// `extensionAudioObjectType` value that signals HE-AAC SBR inside -/// the trailing probe (Table 1.15). -pub const TRAILING_EXTENSION_AOT_SBR: u8 = 5; - -/// `extensionAudioObjectType` value that signals ER BSAC inside the -/// trailing probe (Table 1.15). -pub const TRAILING_EXTENSION_AOT_BSAC: u8 = 22; - -/// The raw `frameLengthFlag` of `GASpecificConfig` — ISO/IEC 14496-3 -/// §4.5.1.1 semantics. The flag's meaning is AOT-dependent: for every -/// GA AOT except AAC SSR and ER AAC LD it selects 1024 vs 960 IMDCT -/// lines; for ER AAC LD (AOT 23) the same flag selects 512 vs 480 -/// (use [`crate::swb_offset::FrameFamily::from_aot_and_flag`] to -/// resolve the actual frame geometry). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FrameLength { - /// `frameLengthFlag == 0` — 1024 lines (512 for ER AAC LD). - Long1024, - /// `frameLengthFlag == 1` — 960 lines (480 for ER AAC LD). - Long960, -} - -impl FrameLength { - /// Resolved sample count per output channel for the non-LD GA - /// AOTs. For ER AAC LD resolve through - /// [`crate::swb_offset::FrameFamily::from_aot_and_flag`] instead - /// (the same flag means 512/480 there). - pub fn samples(self) -> u32 { - match self { - FrameLength::Long1024 => 1024, - FrameLength::Long960 => 960, - } - } - - /// Resolve the §4.5.1.1 frame-length family for `aot`. - pub fn family(self, aot: u8) -> crate::swb_offset::FrameFamily { - crate::swb_offset::FrameFamily::from_aot_and_flag(aot, self == FrameLength::Long960) - } -} - -/// Parsed `GASpecificConfig` body (Table 4.1). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct GaSpecificConfig { - /// Resolved frame length (1024 vs 960 lines). - pub frame_length: FrameLength, - /// `dependsOnCoreCoder` bit. `false` for plain AAC LC. - pub depends_on_core_coder: bool, - /// `coreCoderDelay` (14 bits, only present when - /// `dependsOnCoreCoder == 1`). - pub core_coder_delay: Option, - /// `extensionFlag` bit. Shall be `false` for AOTs 1, 2, 3, 4, - /// 6, 7; shall be `true` for AOTs 17, 19, 20, 21, 22, 23. - pub extension_flag: bool, - /// Inline `program_config_element()` (only present when the - /// surrounding ASC's `channelConfiguration == 0`). - pub pce: Option, - /// `layerNr` (3 bits, only present when AOT ∈ {6, 20}). - pub layer_nr: Option, - /// Parsed extension-flag body (only populated when - /// `extension_flag == true`). - pub extension_body: Option, -} - -/// Parsed body of the `if (extensionFlag)` branch of `GASpecificConfig` -/// (Table 4.1). Carries AOT-dependent subfields plus the always-present -/// `extensionFlag3` bit. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct GaExtensionBody { - /// `numOfSubFrame` (5 bits) + `layer_length` (11 bits). Only - /// present when `audioObjectType == 22` (ER BSAC). - pub bsac_layer: Option, - /// Error-resilience triplet. Only present when - /// `audioObjectType ∈ {17, 19, 20, 23}` (ER AAC LC / ER AAC LTP / - /// ER AAC scalable / ER AAC LD). - pub resilience: Option, - /// `extensionFlag3` (1 bit). Always present at the tail of the - /// extension-flag body. ISO/IEC 14496-3:2009 reserves the body - /// behind this flag with the comment "tbd in version 3"; Phase 1 - /// surfaces the bit but rejects the body itself with - /// [`Error::UnsupportedAscExtensionFlag3`] when the flag is set. - pub extension_flag3: bool, -} - -/// `numOfSubFrame` + `layer_length` pair from Table 4.1, only emitted -/// when the surrounding `audioObjectType == 22` (ER BSAC). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BsacLayerSpec { - /// 5-bit `numOfSubFrame` field. - pub num_of_sub_frame: u8, - /// 11-bit `layer_length` field. - pub layer_length: u16, -} - -/// `aacSection / Scalefactor / Spectral DataResilienceFlag` triplet from -/// Table 4.1, only emitted when the surrounding `audioObjectType ∈ -/// {17, 19, 20, 23}`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct AacResilienceFlags { - /// `aacSectionDataResilienceFlag`. Routes `section_data()` through - /// the §4.4.6 RVLC branch in a downstream round. - pub section_data: bool, - /// `aacScalefactorDataResilienceFlag`. Routes `scale_factor_data()` - /// through the §4.4.6 RVLC branch in a downstream round. - pub scalefactor_data: bool, - /// `aacSpectralDataResilienceFlag`. Routes `spectral_data()` through - /// the §4.4.6 HCR / reordered branch in a downstream round. - pub spectral_data: bool, -} - -/// Result of the Table 1.15 trailing `syncExtensionType == 0x2b7` -/// implicit-SBR / PS / BSAC-extension probe (§1.6.5). -/// -/// Only ever populated when the ASC parser reaches the trailing-bits -/// branch — i.e. the outer `audioObjectType` is **not** the -/// hierarchical SBR wrapper (5) or PS wrapper (29) (those already -/// emit `sbr_present` / `ps_present` from their explicit-signalling -/// path), at least 16 bits remain in the ASC carrier, and the next -/// 11 bits equal [`SYNC_EXTENSION_TYPE_SBR`] (`0x2b7`). -/// -/// `extension_audio_object_type` is the resolved nested AOT -/// (`GetAudioObjectType()` after the `0x2b7` sync). Round 192 -/// implements the bodies for `extension_audio_object_type == 5` -/// (HE-AAC SBR with the optional `0x548` PS sub-probe) and -/// `extension_audio_object_type == 22` (ER BSAC); any other resolved -/// extension AOT surfaces as -/// [`crate::Error::UnsupportedTrailingExtensionAot`] at parse time -/// (the body bit-layout is not defined by Table 1.15 for those). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SbrExtensionProbe { - /// Resolved `extensionAudioObjectType` immediately after the - /// 11-bit `syncExtensionType == 0x2b7` marker. Currently - /// constrained to `5` (HE-AAC) or `22` (ER BSAC). - pub extension_audio_object_type: u8, - /// `sbrPresentFlag` (1 bit). Present for both the `ext_aot == 5` - /// and `ext_aot == 22` branches. - pub sbr_present_flag: bool, - /// `extensionSamplingFrequencyIndex` (4 bits). Only present when - /// `sbr_present_flag == true`; when the wire value is `0xf` the - /// 24-bit `extensionSamplingFrequency` escape follows and the - /// resolved rate lands in - /// [`SbrExtensionProbe::extension_sample_rate`]. - pub extension_sampling_frequency_index: Option, - /// Resolved extension sample rate in Hz (Table 1.18 lookup, or - /// the 24-bit escape value when `extension_sampling_frequency_index - /// == Some(0xf)`). - pub extension_sample_rate: Option, - /// `psPresentFlag` (1 bit). Only present when the SBR (`ext_aot - /// == 5`) branch ran, at least 12 further bits were available, and - /// the second 11-bit `syncExtensionType` equalled - /// [`SYNC_EXTENSION_TYPE_PS`] (`0x548`). - pub ps_present_flag: Option, - /// `extensionChannelConfiguration` (4 bits). Only present when - /// the resolved extension AOT is `22` (ER BSAC). - pub extension_channel_configuration: Option, -} - -/// Parsed `AudioSpecificConfig`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AudioSpecificConfig { - /// Outer `audioObjectType` *as encoded on the wire* (before any - /// SBR/PS unwrap). For HE-AAC v1 signalled hierarchically this - /// is `5`; for HE-AAC v2 it is `29`. - pub outer_aot: u8, - /// Inner / effective `audioObjectType` after unwrapping the - /// AOT-5 (SBR) and AOT-29 (PS) hierarchical containers. For - /// plain AAC-LC this equals `outer_aot`. - pub aot: u8, - /// 4-bit `samplingFrequencyIndex` (the *core* index — for - /// hierarchical HE-AAC this is the inner AAC's index, half the - /// SBR output rate). - pub sampling_frequency_index: u8, - /// Resolved core sample rate. Resolves - /// [`AudioSpecificConfig::sampling_frequency_index`] via - /// Table 1.18, or reads the explicit 24-bit - /// `samplingFrequency` field when the index is `0xf`. - pub sample_rate: u32, - /// `channelConfiguration` (4 bits). `0` ⇔ defined by an inline - /// PCE inside `GASpecificConfig`. - pub channel_configuration: u8, - /// `true` ⇔ the ASC explicitly signalled SBR (outer AOT 5 or - /// 29). Does **not** capture implicit SBR signalling carried in - /// the FIL `extension_payload` of the AAC bitstream. - pub sbr_present: bool, - /// `true` ⇔ the ASC explicitly signalled PS (outer AOT 29). - pub ps_present: bool, - /// `extensionSamplingFrequencyIndex` (only present when - /// `outer_aot ∈ {5, 29}`). - pub extension_sampling_frequency_index: Option, - /// Resolved extension sample rate (SBR output rate). Present - /// when `extension_sampling_frequency_index` is set. - pub extension_sample_rate: Option, - /// `extensionChannelConfiguration` (only present when - /// `outer_aot ∈ {5, 29}` *and* the inner AOT is `22` = - /// ER BSAC). - pub extension_channel_configuration: Option, - /// Parsed body for the inner AOT. For GA AOTs this is - /// populated; for other AOTs (which Phase 1 rejects with - /// [`Error::UnsupportedAot`]) this is never returned. - pub ga_body: GaSpecificConfig, - /// `epConfig` (2 bits) for the ER object types listed in the - /// Table 1.15 outer `switch (audioObjectType)` (AOTs 17, 19, 20, - /// 21, 22, 23, 24, 25, 26, 27, 39). `None` for every other AOT. - /// When the field is `2` or `3`, the spec mandates parsing the - /// trailing `ErrorProtectionSpecificConfig()` body — Phase 1 - /// does **not** parse that body and surfaces - /// [`Error::UnsupportedEpConfig`] at the call site. - pub ep_config: Option, - - /// The parsed `ErrorProtectionSpecificConfig()` (§1.8.2.1 - /// Table 1.49) when `epConfig == 2 || epConfig == 3`. - pub error_protection: Option, - - /// `directMapping` (1 bit, Table 1.15) when `epConfig == 3`: the - /// §1.8.1 EP-class ↔ error-sensitivity-category-instance mapping - /// selector. - pub direct_mapping: Option, - /// Result of the Table 1.15 trailing `syncExtensionType == 0x2b7` - /// implicit-SBR probe (§1.6.5). Only ever populated when the - /// outer `audioObjectType` is not the explicit SBR (5) or PS - /// (29) wrapper, the carrier had at least 16 bits remaining - /// after the per-AOT body + `epConfig`, and the next 11 bits - /// equalled [`SYNC_EXTENSION_TYPE_SBR`]. When the probe resolves - /// SBR or PS, [`AudioSpecificConfig::sbr_present`] / - /// [`AudioSpecificConfig::ps_present`] are also updated to - /// reflect the implicit signalling. Only populated by - /// [`AudioSpecificConfig::parse`] (which knows the byte-slice - /// bound) and the new [`AudioSpecificConfig::parse_bits_bounded`] - /// entry point; the older - /// [`AudioSpecificConfig::parse_bits`] keeps its no-probe - /// semantics. - pub trailing_sbr_probe: Option, -} - -impl AudioSpecificConfig { - /// Parse an `AudioSpecificConfig` from `data`. Returns the - /// resolved ASC and the bit-length consumed (so the caller can - /// skip the rest of the carrier — `esds` payload, LATM - /// StreamMuxConfig, etc.). - /// - /// The byte-slice bound is also forwarded into the Table 1.15 - /// trailing `syncExtensionType == 0x2b7` implicit-SBR probe - /// (§1.6.5), so the bit-length returned here already reflects - /// any consumed trailing-probe fields. - pub fn parse(data: &[u8]) -> Result<(Self, u64)> { - let mut reader = BitReader::new(data); - let asc_bit_length = (data.len() as u64).saturating_mul(8); - let asc = Self::parse_bits_bounded(&mut reader, 0, asc_bit_length)?; - Ok((asc, reader.bit_position())) - } - - /// Parse from a pre-existing [`BitReader`] given the - /// `origin_bit_offset` (the absolute bit position of the start - /// of the ASC) and an explicit `asc_bit_length` (the total - /// bit-length of the ASC inside the carrier, as conveyed by - /// e.g. LATM `StreamMuxConfig`'s `audioSpecificConfig` length - /// field). The trailing Table 1.15 `syncExtensionType == 0x2b7` - /// probe consumes bits up to that bound. - pub fn parse_bits_bounded( - reader: &mut BitReader<'_>, - origin_bit_offset: u64, - asc_bit_length: u64, - ) -> Result { - let start_bit = reader.bit_position(); - let mut asc = Self::parse_bits_core(reader, origin_bit_offset)?; - let consumed = reader.bit_position().saturating_sub(start_bit); - // The Table 1.15 trailing-probe guard `extensionAudioObjectType - // != 5` translates into "skip the probe when the explicit - // hierarchical SBR (outer AOT 5) or PS (outer AOT 29) wrapper - // already established `extensionAudioObjectType == 5`". For - // every other outer AOT the spec defaults - // `extensionAudioObjectType = 0` (per §1.6.5), so the - // `!= 5` predicate is satisfied and the probe runs. - let already_hierarchical_sbr = asc.outer_aot == SBR_AOT || asc.outer_aot == PS_AOT; - if !already_hierarchical_sbr && consumed < asc_bit_length { - let remaining = asc_bit_length - consumed; - if let Some(probe) = parse_trailing_sbr_probe(reader, remaining)? { - if probe.extension_audio_object_type == TRAILING_EXTENSION_AOT_SBR { - if probe.sbr_present_flag { - asc.sbr_present = true; - asc.extension_sampling_frequency_index = - probe.extension_sampling_frequency_index; - asc.extension_sample_rate = probe.extension_sample_rate; - } - if probe.ps_present_flag == Some(true) { - asc.ps_present = true; - } - } else if probe.extension_audio_object_type == TRAILING_EXTENSION_AOT_BSAC { - if probe.sbr_present_flag { - asc.sbr_present = true; - asc.extension_sampling_frequency_index = - probe.extension_sampling_frequency_index; - asc.extension_sample_rate = probe.extension_sample_rate; - } - asc.extension_channel_configuration = probe.extension_channel_configuration; - } - asc.trailing_sbr_probe = Some(probe); - } - } - Ok(asc) - } - - /// Parse from a pre-existing [`BitReader`] given the - /// `origin_bit_offset` (the absolute bit position of the start - /// of the ASC). Used by carriers that embed an ASC inside a - /// wider bit-stream — LATM `StreamMuxConfig` being the obvious - /// case, where the ASC starts at a non-byte-aligned position - /// relative to the LATM packet's first bit. The - /// `origin_bit_offset` is forwarded into PCE parsing so the - /// Table 4.2 `byte_alignment()` note is honoured. - /// - /// This entry point does **not** invoke the Table 1.15 trailing - /// `syncExtensionType == 0x2b7` implicit-SBR probe: the - /// `BitReader` may carry trailing carrier bytes that are not - /// part of the ASC, and probing into them would mis-interpret - /// garbage as a `0x2b7` marker. Carriers that know the exact - /// ASC bit-length should call - /// [`AudioSpecificConfig::parse_bits_bounded`] instead. - pub fn parse_bits(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result { - Self::parse_bits_core(reader, origin_bit_offset) - } - - fn parse_bits_core(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result { - // Outer audioObjectType + samplingFrequencyIndex (+ escape) - let outer_aot = read_aot(reader)?; - let sampling_frequency_index = read_u8(reader, 4)?; - let core_sample_rate = if sampling_frequency_index == 0xf { - read_u32(reader, 24)? - } else { - resolve_sample_rate_index(sampling_frequency_index)? - }; - let channel_configuration = read_u8(reader, 4)?; - - // Hierarchical SBR / PS unwrap. - let mut sbr_present = false; - let mut ps_present = false; - let mut ext_sfi = None; - let mut ext_rate = None; - let mut ext_chan_cfg = None; - let mut effective_aot = outer_aot; - - if outer_aot == SBR_AOT || outer_aot == PS_AOT { - sbr_present = true; - if outer_aot == PS_AOT { - ps_present = true; - } - let sfi = read_u8(reader, 4)?; - let rate = if sfi == 0xf { - read_u32(reader, 24)? - } else { - resolve_sample_rate_index(sfi)? - }; - ext_sfi = Some(sfi); - ext_rate = Some(rate); - effective_aot = read_aot(reader)?; - if effective_aot == 22 { - ext_chan_cfg = Some(read_u8(reader, 4)?); - } - } - - // Body dispatch — Phase 1 only handles GA. - if !GA_AOTS.contains(&effective_aot) { - return Err(Error::UnsupportedAot(effective_aot)); - } - let ga_body = parse_ga_specific_config( - reader, - channel_configuration, - effective_aot, - origin_bit_offset, - )?; - - // Table 1.15 outer `switch (audioObjectType)` — `epConfig` - // for ER object types. `epConfig == 2 || epConfig == 3` - // triggers the `ErrorProtectionSpecificConfig()` body which - // Phase 1 does not parse. - let mut error_protection = None; - let mut direct_mapping = None; - let ep_config = if EP_CONFIG_AOTS.contains(&effective_aot) { - let v = read_u8(reader, 2)?; - // Table 1.15: epConfig 2 / 3 carry the inline - // ErrorProtectionSpecificConfig(); epConfig 3 additionally - // signals the §1.8.1 directMapping selector. - if v == 2 || v == 3 { - error_protection = Some(crate::ep_config::ErrorProtectionSpecificConfig::parse( - reader, - )?); - } - if v == 3 { - direct_mapping = Some(read_bit(reader)?); - } - Some(v) - } else { - None - }; - - Ok(AudioSpecificConfig { - outer_aot, - aot: effective_aot, - sampling_frequency_index, - sample_rate: core_sample_rate, - channel_configuration, - sbr_present, - ps_present, - extension_sampling_frequency_index: ext_sfi, - extension_sample_rate: ext_rate, - extension_channel_configuration: ext_chan_cfg, - ga_body, - ep_config, - error_protection, - direct_mapping, - trailing_sbr_probe: None, - }) - } - - /// Number of audio channels implied by the - /// `channelConfiguration` (Table 1.19); `0` means "defined by - /// PCE" and returns the PCE-derived count. - pub fn channel_count(&self) -> usize { - match self.channel_configuration { - 0 => self - .ga_body - .pce - .as_ref() - .map(Pce::channel_count) - .unwrap_or(0), - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 4, - 5 => 5, - 6 => 6, // 5.1 — LFE counts as one channel - 7 => 8, // 7.1 — LFE counts as one channel - _ => 0, - } - } -} - -fn parse_ga_specific_config( - reader: &mut BitReader<'_>, - channel_configuration: u8, - aot: u8, - origin_bit_offset: u64, -) -> Result { - // Table 4.1 — GASpecificConfig. - let frame_length_flag = read_bit(reader)?; - let frame_length = if frame_length_flag { - FrameLength::Long960 - } else { - FrameLength::Long1024 - }; - let depends_on_core_coder = read_bit(reader)?; - let core_coder_delay = if depends_on_core_coder { - Some(read_u32(reader, 14)? as u16) - } else { - None - }; - let extension_flag = read_bit(reader)?; - - let pce = if channel_configuration == 0 { - Some(Pce::parse(reader, origin_bit_offset)?) - } else { - None - }; - - let layer_nr = if aot == 6 || aot == 20 { - Some(read_u8(reader, 3)?) - } else { - None - }; - - let extension_body = if extension_flag { - Some(parse_ga_extension_body(reader, aot)?) - } else { - None - }; - - Ok(GaSpecificConfig { - frame_length, - depends_on_core_coder, - core_coder_delay, - extension_flag, - pce, - layer_nr, - extension_body, - }) -} - -/// Parse the `if (extensionFlag)` body of `GASpecificConfig()` per -/// Table 4.1. Subfield gating mirrors the AOT lists in the spec -/// listing exactly: `numOfSubFrame` / `layer_length` only for -/// `audioObjectType == 22`; the resilience triplet only for -/// `audioObjectType ∈ {17, 19, 20, 23}`; `extensionFlag3` always. -fn parse_ga_extension_body(reader: &mut BitReader<'_>, aot: u8) -> Result { - let bsac_layer = if GA_EXTENSION_NUM_OF_SUBFRAME_AOTS.contains(&aot) { - let num_of_sub_frame = read_u8(reader, 5)?; - let layer_length = read_u32(reader, 11)? as u16; - Some(BsacLayerSpec { - num_of_sub_frame, - layer_length, - }) - } else { - None - }; - - let resilience = if GA_EXTENSION_RESILIENCE_AOTS.contains(&aot) { - let section_data = read_bit(reader)?; - let scalefactor_data = read_bit(reader)?; - let spectral_data = read_bit(reader)?; - Some(AacResilienceFlags { - section_data, - scalefactor_data, - spectral_data, - }) - } else { - None - }; - - let extension_flag3 = read_bit(reader)?; - if extension_flag3 { - return Err(Error::UnsupportedAscExtensionFlag3); - } - - Ok(GaExtensionBody { - bsac_layer, - resilience, - extension_flag3, - }) -} - -/// Probe the Table 1.15 trailing `syncExtensionType == 0x2b7` / -/// `0x548` chain for implicit SBR / PS / BSAC-extension signalling -/// (§1.6.5, §1.6.6). -/// -/// Returns `Ok(None)` if any of the following holds (each is a -/// normative "no implicit signalling present" outcome — never an -/// error): -/// -/// * Fewer than `SYNC_EXTENSION_TYPE_BITS + 5 = 16` bits remain -/// (the spec's outer `bits_to_decode() >= 16` guard). -/// * The next 11 bits are not [`SYNC_EXTENSION_TYPE_SBR`] (0x2b7). -/// -/// When the outer 0x2b7 marker fires but the resolved -/// `extensionAudioObjectType` is neither `5` nor `22`, the parser -/// returns [`Error::UnsupportedTrailingExtensionAot`] — Table 1.15 -/// does not specify a body layout for any other extension AOT and -/// the bit-reader cannot advance. -/// -/// The `remaining_bits` parameter is the upper bound of bits the -/// probe is allowed to consume from the carrier (typically the -/// ASC's `bits_to_decode()`). The function never reads more than -/// `remaining_bits` bits; an UnexpectedEnd surfaces if a sub-field -/// extends past it. -fn parse_trailing_sbr_probe( - reader: &mut BitReader<'_>, - remaining_bits: u64, -) -> Result> { - // Outer §1.6.2.1 guard: at least 16 bits required to even - // attempt the probe (`syncExtensionType` + the minimum 5-bit - // `GetAudioObjectType()` base it gates). - if remaining_bits < (SYNC_EXTENSION_TYPE_BITS as u64 + 5) { - return Ok(None); - } - let sync = read_u32(reader, SYNC_EXTENSION_TYPE_BITS)? as u16; - if sync != SYNC_EXTENSION_TYPE_SBR { - return Ok(None); - } - - let extension_audio_object_type = read_aot(reader)?; - match extension_audio_object_type { - TRAILING_EXTENSION_AOT_SBR => parse_trailing_sbr_branch(reader, remaining_bits), - TRAILING_EXTENSION_AOT_BSAC => parse_trailing_bsac_branch(reader), - other => Err(Error::UnsupportedTrailingExtensionAot(other)), - } -} - -/// `extensionAudioObjectType == 5` body of the trailing probe: -/// `sbrPresentFlag` + optional `extensionSamplingFrequencyIndex` / -/// `extensionSamplingFrequency` + optional second `syncExtensionType -/// == 0x548` + `psPresentFlag` (Table 1.15). -fn parse_trailing_sbr_branch( - reader: &mut BitReader<'_>, - initial_remaining_bits: u64, -) -> Result> { - let sbr_present_flag = read_bit(reader)?; - let mut extension_sampling_frequency_index = None; - let mut extension_sample_rate = None; - let mut ps_present_flag = None; - if sbr_present_flag { - let sfi = read_u8(reader, 4)?; - let rate = if sfi == 0xf { - read_u32(reader, 24)? - } else { - resolve_sample_rate_index(sfi)? - }; - extension_sampling_frequency_index = Some(sfi); - extension_sample_rate = Some(rate); - // §1.6.2.1 inner guard: at least 12 further bits required - // to attempt the PS sub-probe (11-bit syncExtensionType + - // 1-bit psPresentFlag). - let consumed_so_far = SYNC_EXTENSION_TYPE_BITS as u64 - + 5 // GetAudioObjectType base - + 1 // sbrPresentFlag - + 4 // extensionSamplingFrequencyIndex - + if sfi == 0xf { 24 } else { 0 }; - let still_available = initial_remaining_bits.saturating_sub(consumed_so_far); - if still_available >= 12 { - let inner_sync = read_u32(reader, SYNC_EXTENSION_TYPE_BITS)? as u16; - if inner_sync == SYNC_EXTENSION_TYPE_PS { - ps_present_flag = Some(read_bit(reader)?); - } - } - } - Ok(Some(SbrExtensionProbe { - extension_audio_object_type: TRAILING_EXTENSION_AOT_SBR, - sbr_present_flag, - extension_sampling_frequency_index, - extension_sample_rate, - ps_present_flag, - extension_channel_configuration: None, - })) -} - -/// `extensionAudioObjectType == 22` body of the trailing probe: -/// `sbrPresentFlag` + optional `extensionSamplingFrequencyIndex` / -/// `extensionSamplingFrequency` + mandatory -/// `extensionChannelConfiguration` (Table 1.15). -fn parse_trailing_bsac_branch(reader: &mut BitReader<'_>) -> Result> { - let sbr_present_flag = read_bit(reader)?; - let mut extension_sampling_frequency_index = None; - let mut extension_sample_rate = None; - if sbr_present_flag { - let sfi = read_u8(reader, 4)?; - let rate = if sfi == 0xf { - read_u32(reader, 24)? - } else { - resolve_sample_rate_index(sfi)? - }; - extension_sampling_frequency_index = Some(sfi); - extension_sample_rate = Some(rate); - } - let extension_channel_configuration = Some(read_u8(reader, 4)?); - Ok(Some(SbrExtensionProbe { - extension_audio_object_type: TRAILING_EXTENSION_AOT_BSAC, - sbr_present_flag, - extension_sampling_frequency_index, - extension_sample_rate, - ps_present_flag: None, - extension_channel_configuration, - })) -} - -/// Table 1.16 — `GetAudioObjectType()`. 5-bit base, with the `31` -/// escape unlocking a 6-bit extension. -fn read_aot(reader: &mut BitReader<'_>) -> Result { - let base = read_u8(reader, 5)?; - if base == 31 { - let ext = read_u8(reader, 6)?; - // Per spec the result is `32 + audioObjectTypeExt`. AOTs - // above 41 are not defined in ISO/IEC 14496-3:2009; the - // parser preserves the wire value and the body dispatch - // will reject it. - let aot = 32u16 + ext as u16; - if aot > u8::MAX as u16 { - return Err(Error::UnsupportedAot(0)); - } - Ok(aot as u8) - } else { - Ok(base) - } -} - -fn resolve_sample_rate_index(idx: u8) -> Result { - if (idx as usize) >= ADTS_SAMPLE_RATES_HZ.len() { - return Err(Error::AdtsReservedSampleRateIndex); - } - Ok(ADTS_SAMPLE_RATES_HZ[idx as usize]) -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -fn read_u32(reader: &mut BitReader<'_>, n: u32) -> Result { - reader.read_u32(n).map_err(|_| Error::UnexpectedEnd) -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} diff --git a/crates/vendor/oxideav-aac/src/bsac_arith.rs b/crates/vendor/oxideav-aac/src/bsac_arith.rs deleted file mode 100644 index 0f914ee2..00000000 --- a/crates/vendor/oxideav-aac/src/bsac_arith.rs +++ /dev/null @@ -1,413 +0,0 @@ -//! BSAC arithmetic decoder — ISO/IEC 14496-3:2009 §4.5.2.6.2.7. -//! -//! The ER BSAC noiseless coder replaces the AAC Huffman machinery -//! with a single arithmetic code over the whole -//! `bsac_raw_data_block()` (or, in SBA mode, over each segment). -//! The spec normatively lists the decoding procedure as C source -//! (§4.5.2.6.2.7.4); this module transcribes it exactly: -//! -//! * [`ArithDecoder::decode_symbol`] — the general multi-symbol -//! decode over a 14-bit cumulative-frequency model (`cband_si`, -//! scalefactors, stereo / PNS side info). -//! * [`ArithDecoder::decode_bit`] — the binary decode over a 14-bit -//! `p0` (spectral bit slices and sign bits). -//! -//! Both return the **estimated codeword length** (`est_cw_len`) the -//! spec defines — the renormalization shift that will be consumed -//! before the *next* symbol — which the §4.5.2.6.2.5 layer budget -//! (`available_len[]`) bookkeeping subtracts per decoded symbol. -//! -//! The register discipline follows the listing: `value` and `range` -//! are 32-bit quantities (held in `u64` here — the products -//! `range · cum_freq` stay under 2^30, so the arithmetic is -//! identical), `range` starts at 1 with `est_cw_len = 30`, and -//! renormalization scans the `half[]` table (2^29 … 2^14). -//! -//! Reads past the end of the segment buffer return the -//! §4.5.2.6.2.2.1 zero stuffing (a conforming stream never consumes -//! more than 32 such bits; the layer budgets bound all decode -//! loops, so the reader simply keeps yielding zeros). - -/// The §4.5.2.6.2.7.1 `half[]` table: 32-bit fixed-point values of -/// ½ at descending magnitudes (2^29 down to 2^14). -const HALF: [u64; 16] = [ - 0x2000_0000, - 0x1000_0000, - 0x0800_0000, - 0x0400_0000, - 0x0200_0000, - 0x0100_0000, - 0x0080_0000, - 0x0040_0000, - 0x0020_0000, - 0x0010_0000, - 0x0008_0000, - 0x0004_0000, - 0x0002_0000, - 0x0001_0000, - 0x0000_8000, - 0x0000_4000, -]; - -/// MSB-first bit reader over one arithmetic segment: a bit window -/// `[start_bit, end_bit)` of the frame buffer, followed by the -/// §4.5.2.6.2.2.1 zero stuffing (zeros for every read past the -/// window). -#[derive(Debug, Clone)] -pub struct SegmentReader<'a> { - data: &'a [u8], - /// Absolute next bit position within `data`. - pos: u64, - /// Absolute end of the segment window within `data`. - end: u64, - /// Bits consumed beyond `end` (the zero-stuffing tail). - overrun: u64, -} - -impl<'a> SegmentReader<'a> { - /// A reader over bits `[start_bit, end_bit)` of `data`. - /// `end_bit` is clamped to the buffer size. - pub fn new(data: &'a [u8], start_bit: u64, end_bit: u64) -> Self { - let cap = (data.len() as u64) * 8; - SegmentReader { - data, - pos: start_bit.min(cap), - end: end_bit.min(cap), - overrun: 0, - } - } - - /// Read `n` bits MSB-first (zeros past the window end). - fn read_bits(&mut self, n: u32) -> u64 { - let mut v = 0u64; - for _ in 0..n { - let bit = if self.pos < self.end { - let byte = self.data[(self.pos >> 3) as usize]; - u64::from((byte >> (7 - (self.pos & 7))) & 1) - } else { - self.overrun += 1; - 0 - }; - self.pos += 1; - v = (v << 1) | bit; - } - v - } - - /// Bits consumed past the segment window (the zero-stuffing - /// depth). A conforming stream stays at or under 32. - pub fn overrun(&self) -> u64 { - self.overrun - } -} - -/// The §4.5.2.6.2.7 arithmetic decoder registers. -#[derive(Debug, Clone)] -pub struct ArithDecoder { - value: u64, - range: u64, - est_cw_len: u32, -} - -impl Default for ArithDecoder { - fn default() -> Self { - Self::new() - } -} - -impl ArithDecoder { - /// §4.5.2.6.2.7.2 initialization: `value = 0`, `range = 1`, - /// `est_cw_len = 30`. Called at the start of every segment. - pub fn new() -> Self { - ArithDecoder { - value: 0, - range: 1, - est_cw_len: 30, - } - } - - /// Renormalize against `half[]`: the returned `est_cw_len` is - /// the shift consumed before the next symbol. - fn renormalize(&mut self) -> u32 { - let mut est = 0u32; - while est < HALF.len() as u32 && self.range < HALF[est as usize] { - est += 1; - } - self.est_cw_len = est; - est - } - - /// The renormalization shift the next decode will consume. - pub fn pending_est(&self) -> u32 { - self.est_cw_len - } - - /// §4.5.2.6.2.7.4 `decode_symbol()`: general arithmetic decode - /// over a cumulative-frequency model (14-bit fixed point, - /// strictly decreasing, last entry 0). Returns - /// `(symbol, est_cw_len)`. - pub fn decode_symbol( - &mut self, - reader: &mut SegmentReader<'_>, - cum_freq: &[u16], - ) -> (usize, u32) { - if self.est_cw_len > 0 { - self.range <<= self.est_cw_len; - self.value = (self.value << self.est_cw_len) | reader.read_bits(self.est_cw_len); - } - self.range >>= 14; - let cum = self.value.checked_div(self.range).unwrap_or(0); - // The listing's `for (sym = 0; cum_freq[sym] > cum; sym++)` - // — the last entry is 0 <= cum, so it terminates in range. - let mut sym = 0usize; - while sym + 1 < cum_freq.len() && u64::from(cum_freq[sym]) > cum { - sym += 1; - } - self.value -= self.range * u64::from(cum_freq[sym]); - let width = if sym > 0 { - u64::from(cum_freq[sym - 1]) - u64::from(cum_freq[sym]) - } else { - 16384 - u64::from(cum_freq[sym]) - }; - self.range *= width; - (sym, self.renormalize()) - } - - /// §4.5.2.6.2.7.4 `decode_symbol2()`: binary arithmetic decode - /// with `p0` the 14-bit probability of the "0" symbol. Returns - /// `(bit, est_cw_len)`. - pub fn decode_bit(&mut self, reader: &mut SegmentReader<'_>, p0: u16) -> (u8, u32) { - if self.est_cw_len > 0 { - self.range <<= self.est_cw_len; - self.value = (self.value << self.est_cw_len) | reader.read_bits(self.est_cw_len); - } - self.range >>= 14; - let p0 = u64::from(p0); - let bit; - if p0 * self.range <= self.value { - bit = 1; - self.value -= self.range * p0; - self.range *= 16384 - p0; - } else { - bit = 0; - self.range *= p0; - } - (bit, self.renormalize()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Spec-inverse arithmetic *encoder*, derived from the - /// §4.5.2.6.2.7.4 decoder listing: the decoder's `value` at - /// step `k` equals the stream prefix (as an integer) minus the - /// accumulated `range · cum_freq` subtractions shifted by the - /// renormalization schedule, so the codeword is - /// `Σ sub_k · 2^(A_L − A_k)` with `A_k` the bits consumed when - /// symbol `k` decodes (final `value` chosen 0). Test-only: it - /// exists to prove the decoder self-consistent on every model. - struct Encoder { - range: u64, - est: u32, - /// (subtrahend, alignment in bits when it applies). - subs: Vec<(u64, u64)>, - /// Bits consumed so far (A_k); starts at the 30-bit init. - align: u64, - } - - impl Encoder { - fn new() -> Self { - Encoder { - range: 1, - est: 30, - subs: Vec::new(), - align: 0, - } - } - - fn renorm(&mut self) { - let mut est = 0u32; - while est < HALF.len() as u32 && self.range < HALF[est as usize] { - est += 1; - } - self.est = est; - } - - fn push(&mut self, sub: u64, width: u64) { - self.range <<= self.est; - self.align += u64::from(self.est); - self.range >>= 14; - if sub > 0 { - self.subs.push((sub, self.align)); - } - self.range *= width; - self.renorm(); - } - - fn encode_symbol(&mut self, cum_freq: &[u16], sym: usize) { - let sub_base = u64::from(cum_freq[sym]); - let width = if sym > 0 { - u64::from(cum_freq[sym - 1]) - sub_base - } else { - 16384 - sub_base - }; - let rs_now = (self.range << self.est) >> 14; - self.push(rs_now * sub_base, width); - } - - fn encode_bit(&mut self, p0: u16, bit: u8) { - let p0 = u64::from(p0); - let rs_now = (self.range << self.est) >> 14; - if bit == 1 { - self.push(rs_now * p0, 16384 - p0); - } else { - self.push(0, p0); - } - } - - /// Assemble the codeword bytes (MSB-first bit order): the - /// integer `Σ sub_k · 2^(total_bits − A_k)` emitted as - /// `total_bits` bits (the final `value` is chosen 0, which - /// is always inside the final range). - fn finish(self) -> Vec { - let total_bits = self.align as usize; - // One accumulator slot per stream bit, MSB-first; - // sub_k's bit j lands at index `A_k - 1 - j`. - let mut acc = vec![0u32; total_bits]; - for (sub, align) in &self.subs { - let mut v = *sub; - let mut j = 0usize; - while v > 0 { - acc[*align as usize - 1 - j] += (v & 1) as u32; - v >>= 1; - j += 1; - } - } - // Carry-propagate from the LSB end. - let mut carry = 0u32; - for slot in acc.iter_mut().rev() { - let s = *slot + carry; - *slot = s & 1; - carry = s >> 1; - } - assert_eq!(carry, 0, "test encoder codeword overflow"); - let mut out = vec![0u8; total_bits.div_ceil(8)]; - for (i, &b) in acc.iter().enumerate() { - if b != 0 { - out[i / 8] |= 1 << (7 - (i % 8)); - } - } - out - } - } - - fn roundtrip_symbols(model: &[u16], syms: &[usize]) { - let mut enc = Encoder::new(); - for &s in syms { - enc.encode_symbol(model, s); - } - let bytes = enc.finish(); - let mut rd = SegmentReader::new(&bytes, 0, (bytes.len() as u64) * 8); - let mut dec = ArithDecoder::new(); - for (i, &s) in syms.iter().enumerate() { - let (got, _est) = dec.decode_symbol(&mut rd, model); - assert_eq!(got, s, "symbol {i}"); - } - } - - fn roundtrip_bits(p0s: &[u16], bits: &[u8]) { - assert_eq!(p0s.len(), bits.len()); - let mut enc = Encoder::new(); - for (&p, &b) in p0s.iter().zip(bits) { - enc.encode_bit(p, b); - } - let bytes = enc.finish(); - let mut rd = SegmentReader::new(&bytes, 0, (bytes.len() as u64) * 8); - let mut dec = ArithDecoder::new(); - for (i, (&p, &b)) in p0s.iter().zip(bits).enumerate() { - let (got, _est) = dec.decode_bit(&mut rd, p); - assert_eq!(got, b, "bit {i}"); - } - } - - #[test] - fn symbol_roundtrip_over_every_model() { - use crate::bsac_tables::*; - let mut models: Vec<&[u16]> = vec![ - &MS_USED_MODEL, - &STEREO_INFO_MODEL, - &NOISE_FLAG_MODEL, - &NOISE_MODE_MODEL, - &CBAND_SI_MODEL_CBAND0, - ]; - models.extend(CBAND_SI_MODELS.iter().copied()); - models.extend(SCF_MODELS.iter().flatten().copied()); - let mut seed = 0xC0FFEEu32; - for model in models { - let mut syms = Vec::new(); - for _ in 0..40 { - seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); - syms.push((seed >> 11) as usize % model.len()); - } - roundtrip_symbols(model, &syms); - } - } - - #[test] - fn bit_roundtrip_over_spectral_probabilities() { - use crate::bsac_tables::spectral_p0; - let mut seed = 0xBEEFu32; - let mut p0s = Vec::new(); - let mut bits = Vec::new(); - for cband_si in [1u8, 4, 7, 9, 12, 15, 22] { - let plane = crate::bsac_tables::CBAND_SI_MSB_PLANE[cband_si as usize]; - for snf in 1..=plane { - for hbv in [0u32, 1, 3, 16] { - let rel = plane - snf; - if hbv != 0 && (rel == 0 || (rel < 31 && hbv >= (1 << rel))) { - continue; - } - for pos in [0usize, 7, 33, 64] { - let pos = if rel == 0 { pos.min(14) } else { pos }; - seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); - p0s.push(spectral_p0(cband_si, snf, hbv, pos)); - bits.push(((seed >> 13) & 1) as u8); - } - } - } - } - roundtrip_bits(&p0s, &bits); - } - - #[test] - fn mixed_symbol_and_bit_roundtrip() { - use crate::bsac_tables::{MS_USED_MODEL, SCF_MODELS, SIGN_P0}; - let scf = SCF_MODELS[3].unwrap(); - let mut enc = Encoder::new(); - enc.encode_symbol(scf, 5); - enc.encode_bit(SIGN_P0, 1); - enc.encode_symbol(&MS_USED_MODEL, 1); - enc.encode_bit(0x3f00, 0); - enc.encode_bit(0x0100, 1); - enc.encode_symbol(scf, 15); - let bytes = enc.finish(); - let mut rd = SegmentReader::new(&bytes, 0, (bytes.len() as u64) * 8); - let mut dec = ArithDecoder::new(); - assert_eq!(dec.decode_symbol(&mut rd, scf).0, 5); - assert_eq!(dec.decode_bit(&mut rd, SIGN_P0).0, 1); - assert_eq!(dec.decode_symbol(&mut rd, &MS_USED_MODEL).0, 1); - assert_eq!(dec.decode_bit(&mut rd, 0x3f00).0, 0); - assert_eq!(dec.decode_bit(&mut rd, 0x0100).0, 1); - assert_eq!(dec.decode_symbol(&mut rd, scf).0, 15); - } - - #[test] - fn zero_stuffing_supplies_zero_bits() { - let mut rd = SegmentReader::new(&[0xff], 0, 8); - assert_eq!(rd.read_bits(8), 0xff); - assert_eq!(rd.read_bits(8), 0); - assert_eq!(rd.overrun(), 8); - } -} diff --git a/crates/vendor/oxideav-aac/src/bsac_decode.rs b/crates/vendor/oxideav-aac/src/bsac_decode.rs deleted file mode 100644 index 9ab13d16..00000000 --- a/crates/vendor/oxideav-aac/src/bsac_decode.rs +++ /dev/null @@ -1,861 +0,0 @@ -//! ER BSAC (AOT 22) decoder — ISO/IEC 14496-3:2009 §4.4.2.6 / -//! §4.5.2.6 / §4.6.4. -//! -//! Decodes a `bsac_raw_data_block()` end to end: the raw-bit -//! headers (Tables 4.34–4.36), the §4.5.2.6.2.5 layer roster, the -//! arithmetic-coded side information (`cband_si`, scalefactors, -//! stereo / PNS decisions) and the bit-sliced spectral data -//! (Tables 4.37–4.43 driving [`crate::bsac_arith`] over the -//! [`crate::bsac_tables`] models), then reconstructs PCM through -//! the standard AAC back end — §4.6.2 inverse quantization, the -//! §4.6.8.1 M/S and §4.6.8.2 intensity tools, §4.6.9 TNS and the -//! §4.6.11 filterbank — exactly as §4.6.4.1 prescribes ("the BSAC -//! noiseless coding module is an alternative to the AAC coding -//! module, with all other modules of the AAC-based coder remaining -//! unchanged"). -//! -//! Not yet covered (surfaced as [`Error::BsacUnsupportedTool`]): -//! long-term prediction (`ltp_data_present == 1`), the -//! `zero_code`-prefixed extended part (BSAC channel extension / -//! SBR / MPEG-Surround payloads), and perceptual noise -//! substitution (`pns_data_present == 1`) pending an external -//! vector to pin its arithmetic-PCM offset conventions. - -use crate::bsac_arith::{ArithDecoder, SegmentReader}; -use crate::bsac_layer::{BsacGeometry, LayerInfo, BSAC_FRAME_LEN}; -use crate::bsac_tables::{ - clamp_p0, context_position, spectral_p0, CBAND_SI_MODELS, CBAND_SI_MODEL_CBAND0, - CBAND_SI_MSB_PLANE, CBAND_SI_TYPES, MS_USED_MODEL, SCF_MODELS, SIGN_P0, STEREO_INFO_MODEL, -}; -use crate::dequant::{inverse_quantize, scale_factor_gain}; -use crate::filterbank::Filterbank; -use crate::ics_info::{IcsInfo, WindowSequence, WindowShape}; -use crate::ms_stereo::{apply_ms_stereo, ChannelPairSpectra, MsMaskPresent}; -use crate::pcm::channel_to_s16; -use crate::swb_offset::FrameFamily; -use crate::tns_data::TnsData; -use crate::tns_frame::tns_decode_frame_ics; -use crate::{Error, Result}; - -use oxideav_core::bits::BitReader; - -/// Parsed `bsac_header()` — Table 4.35. -#[derive(Debug, Clone)] -pub struct BsacHeader { - /// `frame_length` (11 bits) — whole frame length in bytes. - pub frame_length: usize, - /// `header_length` (4 bits) — header length escape field - /// (§4.5.2.6.2.2.3: values 1..=14 mean `(header_length + 7)` - /// bytes; 0 / 15 defer to the decoded header length). - pub header_length: u8, - /// `sba_mode` (1 bit) — segmented binary arithmetic coding. - pub sba_mode: bool, - /// `top_layer` (6 bits). - pub top_layer: usize, - /// `base_snf_thr` (2 bits). - pub base_snf_thr: u8, - /// `max_scalefactor[ch]` (8 bits each). - pub max_scalefactor: Vec, - /// `base_band` (5 bits). - pub base_band: usize, - /// `cband_si_type[ch]` (5 bits each). - pub cband_si_type: Vec, - /// `base_scf_model[ch]` (3 bits each). - pub base_scf_model: Vec, - /// `enh_scf_model[ch]` (3 bits each). - pub enh_scf_model: Vec, - /// `max_sfb_si_len[ch]` (4 bits each, raw — the +5 offset is - /// applied in the layer geometry). - pub max_sfb_si_len: Vec, -} - -/// Parsed `general_header()` — Table 4.36. -#[derive(Debug, Clone)] -pub struct GeneralHeader { - /// `window_sequence` (2 bits). - pub window_sequence: WindowSequence, - /// `window_shape` (1 bit). - pub window_shape: WindowShape, - /// `max_sfb` (4 bits short / 6 bits long). - pub max_sfb: usize, - /// `scale_factor_grouping` (7 bits, `EIGHT_SHORT` only). - pub scale_factor_grouping: u8, - /// `pns_data_present` (1 bit). - pub pns_data_present: bool, - /// `pns_start_sfb` (6 bits, when PNS is present). - pub pns_start_sfb: usize, - /// `ms_mask_present` (2 bits, `nch == 2` only): 0 independent, - /// 1 `ms_used` mask, 2 all ones, 3 `stereo_info` mask. - pub ms_mask_present: u8, - /// Per-channel §4.6.9 TNS record. - pub tns: Vec>, -} - -/// One decoded `bsac_raw_data_block()`: quantized spectra plus the -/// side information the AAC back end consumes. -#[derive(Debug, Clone)] -pub struct DecodedBlock { - /// The `bsac_header()`. - pub header: BsacHeader, - /// The `general_header()`. - pub general: GeneralHeader, - /// Signed quantized spectra, `[ch][g][group line]` in the - /// §4.5.2.6.2.6 (possibly interleaved) group order. - pub sample: Vec>>, - /// Absolute scalefactors, `[ch][g][sfb]` (`None` where no band - /// side info was decoded). - pub scf: Vec>>>, - /// `ms_used[g][sfb]` (derived: `stereo_info == 1` counts). - pub ms_used: Vec>, - /// `stereo_info[g][sfb]` (0 independent / 1 M/S / 2 IS in - /// phase / 3 IS out of phase). - pub stereo_info: Vec>, - /// Intensity position per `[g][sfb]` (`stereo_info >= 2`). - pub is_position: Vec>, - /// The layer geometry the block decoded under. - pub geometry: BsacGeometry, -} - -/// Per-(channel, group) bit-slice state. -#[derive(Debug, Clone, Default)] -struct LineState { - /// Decoded bit-plane mask: bit `p-1` set = the plane-`p` sliced - /// bit decoded 1. The magnitude equals the mask value. - mask: Vec, - /// Sign decoded (1 = negative). - sign_neg: Vec, - /// `sign_is_coded[]`. - sign_coded: Vec, - /// First-pass significance (`cur_snf`). - cur_snf: Vec, - /// Secondary-pass significance (`unc_snf`). - unc_snf: Vec, -} - -/// Which significance array a `bsac_spectral_data()` pass drives. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SnfKind { - /// The first coding pass (`bsac_layer_spectra`). - Cur, - /// The secondary passes (`bsac_lower_spectra` / - /// `bsac_higher_spectra`). - Unc, -} - -/// The whole-block arithmetic decode driver. -struct BlockCtx<'a> { - nch: usize, - header: BsacHeader, - general: GeneralHeader, - geo: BsacGeometry, - arith: ArithDecoder, - reader: SegmentReader<'a>, - /// Remaining budget of the current layer (bits). - avail: i64, - /// `cband_si[ch][g][cband]`. - cband_si: Vec>>, - /// Per-(ch, g) line state. - lines: Vec>, - scf: Vec>>>, - stereo_side_info_coded: Vec>, - ms_used: Vec>, - stereo_info: Vec>, - is_position: Vec>, -} - -impl<'a> BlockCtx<'a> { - fn layer_data_available(&self) -> bool { - self.avail > 0 - } - - fn decode_symbol(&mut self, model: &[u16]) -> usize { - let (sym, est) = self.arith.decode_symbol(&mut self.reader, model); - self.avail -= i64::from(est); - sym - } - - fn decode_bit(&mut self, p0: u16) -> u8 { - let (bit, est) = self.arith.decode_bit(&mut self.reader, p0); - self.avail -= i64::from(est); - bit - } - - /// Table 4.38 `layer_cband_si()`. - fn layer_cband_si(&mut self, layer: &LayerInfo) -> Result<()> { - let g = layer.group; - for ch in 0..self.nch { - let params = &CBAND_SI_TYPES[self.header.cband_si_type[ch] as usize]; - for cband in layer.start_cband..layer.end_cband { - let (model, largest): (&[u16], u8) = if cband == 0 { - (&CBAND_SI_MODEL_CBAND0, params.largest_cband0) - } else { - ( - CBAND_SI_MODELS[params.other_model as usize], - params.largest_other, - ) - }; - let si = self.decode_symbol(model); - if si > usize::from(largest) { - return Err(Error::BsacBitError); - } - self.cband_si[ch][g][cband] = si as u8; - // §4.5.2.6.2.5: cur_snf of the layer's new lines - // initializes to the coding band's MSB plane. - let plane = i32::from(CBAND_SI_MSB_PLANE[si]); - let start = cband * 32; - let end = (cband * 32 + 32).min(self.geo.group_len[g]); - for i in start..end { - self.lines[ch][g].cur_snf[i] = plane; - } - } - } - Ok(()) - } - - /// The scalefactor-model symbol for the current layer. - fn scf_symbol(&mut self, ch: usize, layer_idx: usize) -> Result { - let model_idx = if layer_idx < self.geo.slayer_size { - self.header.base_scf_model[ch] - } else { - self.header.enh_scf_model[ch] - } as usize; - match SCF_MODELS[model_idx] { - Some(model) => Ok(self.decode_symbol(model)), - // Model 0 is "not used" (Table 4.A.32): no symbol is - // coded; the differential is zero. - None => Ok(0), - } - } - - /// Table 4.39 `layer_sfb_si()`. - fn layer_sfb_si(&mut self, layer_idx: usize, layer: &LayerInfo) -> Result<()> { - let g = layer.group; - let pns = self.general.pns_data_present; - let msp = self.general.ms_mask_present; - for ch in 0..self.nch { - for sfb in layer.start_sfb..layer.end_sfb { - if self.nch == 1 { - if pns && sfb >= self.general.pns_start_sfb { - // PNS decode needs the noise-energy PCM - // conventions pinned by an external vector. - return Err(Error::BsacUnsupportedTool); - } - } else if !self.stereo_side_info_coded[g][sfb] { - if msp != 2 { - if msp == 1 { - let ms = self.decode_symbol(&MS_USED_MODEL); - self.ms_used[g][sfb] = ms == 1; - } else if msp == 3 { - let si = self.decode_symbol(&STEREO_INFO_MODEL) as u8; - self.stereo_info[g][sfb] = si; - self.ms_used[g][sfb] = si == 1; - } - if pns && sfb >= self.general.pns_start_sfb { - return Err(Error::BsacUnsupportedTool); - } - } - self.stereo_side_info_coded[g][sfb] = true; - } - // Per-channel scalefactor / intensity position. - if self.stereo_info[g][sfb] >= 2 && ch == 1 { - let idx = self.scf_symbol(ch, layer_idx)? as i32; - // §4.6.4.4.3 zig-zag: odd → −(idx+1)/2, even → - // idx/2. - self.is_position[g][sfb] = if idx % 2 == 1 { - -(idx + 1) / 2 - } else { - idx / 2 - }; - } else { - let diff = self.scf_symbol(ch, layer_idx)? as i32; - let scf = i32::from(self.header.max_scalefactor[ch]) - diff; - if !(0..=255).contains(&scf) { - return Err(Error::BsacBitError); - } - self.scf[ch][g][sfb] = Some(scf as u8); - } - } - } - Ok(()) - } - - /// Table 4.43 `bsac_spectral_data()` over `regions` - /// (`(group, start_index, end_index)`), down to (exclusive) - /// `thr_snf`, driving the selected significance array. - fn spectral_data(&mut self, regions: &[(usize, usize, usize)], thr_snf: i32, kind: SnfKind) { - if !self.layer_data_available() { - return; - } - // maxsnf over the region. - let mut maxsnf = 0i32; - for &(g, s, e) in regions { - for ch in 0..self.nch { - let st = &self.lines[ch][g]; - let arr = match kind { - SnfKind::Cur => &st.cur_snf, - SnfKind::Unc => &st.unc_snf, - }; - for &v in arr[s..e.min(arr.len())].iter() { - maxsnf = maxsnf.max(v); - } - } - } - let mut snf = maxsnf; - while snf > thr_snf { - for &(g, s, e) in regions { - let e = e.min(self.geo.group_len[g]); - for i in s..e { - for ch in 0..self.nch { - { - let st = &self.lines[ch][g]; - let v = match kind { - SnfKind::Cur => st.cur_snf[i], - SnfKind::Unc => st.unc_snf[i], - }; - if v < snf { - continue; - } - } - let cband_si = self.cband_si[ch][g][i / 32]; - let mask_i = self.lines[ch][g].mask[i]; - let sign_coded = self.lines[ch][g].sign_coded[i]; - if mask_i == 0 || sign_coded { - // Decode one sliced bit. - let hbv = mask_i >> snf; - let p0 = if hbv != 0 { - spectral_p0(cband_si, snf as u8, hbv, 0) - } else { - let a = i % 4; - let bit_at = |j: isize| -> u8 { - if j < 0 { - 0 - } else { - ((self.lines[ch][g].mask[j as usize] >> (snf - 1)) & 1) - as u8 - } - }; - let hb = |j: usize| -> u8 { - if j >= self.geo.group_len[g] { - 0 - } else { - u8::from(self.lines[ch][g].mask[j] >> snf != 0) - } - }; - let prev = [ - bit_at(i as isize - 3), - bit_at(i as isize - 2), - bit_at(i as isize - 1), - ]; - let base = i - a; - let flags = [hb(base), hb(base + 1), hb(base + 2), hb(base + 3)]; - spectral_p0( - cband_si, - snf as u8, - 0, - context_position(a, prev, flags), - ) - }; - let p0 = clamp_p0(p0, self.avail); - let bit = self.decode_bit(p0); - if bit != 0 { - self.lines[ch][g].mask[i] |= 1 << (snf - 1); - } - } - if self.lines[ch][g].mask[i] != 0 && !self.lines[ch][g].sign_coded[i] { - if !self.layer_data_available() { - return; - } - let sign = self.decode_bit(SIGN_P0); - self.lines[ch][g].sign_neg[i] = sign == 1; - self.lines[ch][g].sign_coded[i] = true; - } - { - let st = &mut self.lines[ch][g]; - match kind { - SnfKind::Cur => st.cur_snf[i] -= 1, - SnfKind::Unc => st.unc_snf[i] -= 1, - } - } - if !self.layer_data_available() { - return; - } - } - } - } - snf -= 1; - } - } -} - -/// Decode one `bsac_raw_data_block()` into quantized spectra + side -/// info. -/// -/// `fs` / `fs_index` — the sampling rate from the ASC; `nch` — the -/// channel count (1 or 2). -pub fn decode_bsac_raw_data_block( - frame: &[u8], - fs: u32, - fs_index: u8, - nch: usize, -) -> Result { - if !(1..=2).contains(&nch) || frame.is_empty() { - return Err(Error::BsacInvalidHeader); - } - let mut br = BitReader::new(frame); - fn rd(br: &mut BitReader<'_>, n: u32) -> Result { - br.read_u32(n).map_err(|_| Error::UnexpectedEnd) - } - - // Table 4.34 / 4.35: frame_length + bsac_header(). - let frame_length = rd(&mut br, 11)? as usize; - if frame_length > frame.len() || frame_length == 0 { - return Err(Error::BsacInvalidHeader); - } - let header_length = rd(&mut br, 4)? as u8; - let sba_mode = rd(&mut br, 1)? != 0; - let top_layer = rd(&mut br, 6)? as usize; - let base_snf_thr = rd(&mut br, 2)? as u8; - let mut max_scalefactor = Vec::with_capacity(nch); - for _ in 0..nch { - max_scalefactor.push(rd(&mut br, 8)? as u8); - } - let base_band = rd(&mut br, 5)? as usize; - let (mut cband_si_type, mut base_scf_model, mut enh_scf_model, mut max_sfb_si_len) = - (Vec::new(), Vec::new(), Vec::new(), Vec::new()); - for _ in 0..nch { - let t = rd(&mut br, 5)? as u8; - if usize::from(t) >= CBAND_SI_TYPES.len() { - return Err(Error::BsacInvalidHeader); - } - cband_si_type.push(t); - base_scf_model.push(rd(&mut br, 3)? as u8); - enh_scf_model.push(rd(&mut br, 3)? as u8); - max_sfb_si_len.push(rd(&mut br, 4)? as u8); - } - - // Table 4.36: general_header(). - let _reserved = rd(&mut br, 1)?; - let window_sequence = match rd(&mut br, 2)? { - 0 => WindowSequence::OnlyLong, - 1 => WindowSequence::LongStart, - 2 => WindowSequence::EightShort, - _ => WindowSequence::LongStop, - }; - let window_shape = if rd(&mut br, 1)? != 0 { - WindowShape::Kbd - } else { - WindowShape::Sine - }; - let short = window_sequence == WindowSequence::EightShort; - let (max_sfb, scale_factor_grouping) = if short { - let m = rd(&mut br, 4)? as usize; - let g = rd(&mut br, 7)? as u8; - (m, g) - } else { - (rd(&mut br, 6)? as usize, 0) - }; - let pns_data_present = rd(&mut br, 1)? != 0; - let pns_start_sfb = if pns_data_present { - rd(&mut br, 6)? as usize - } else { - 0 - }; - let ms_mask_present = if nch == 2 { rd(&mut br, 2)? as u8 } else { 0 }; - let mut tns = Vec::with_capacity(nch); - for _ in 0..nch { - if rd(&mut br, 1)? != 0 { - tns.push(Some( - TnsData::parse(&mut br, window_sequence).map_err(|_| Error::BsacInvalidHeader)?, - )); - } else { - tns.push(None); - } - // ltp_data_present. - if rd(&mut br, 1)? != 0 { - return Err(Error::BsacUnsupportedTool); - } - } - let consumed = br.bit_position() as i64; - // header_length escapes (§4.5.2.6.2.2.3): 1..=14 → (hl+7) - // bytes; 0 / 15 → the byte-aligned actual length. - let header_bits: i64 = if (1..=14).contains(&header_length) { - (i64::from(header_length) + 7) * 8 - } else { - (consumed + 7) / 8 * 8 - }; - if header_bits < (consumed + 7) / 8 * 8 || header_bits > (frame_length as i64) * 8 { - return Err(Error::BsacInvalidHeader); - } - - let geo = BsacGeometry::derive( - fs, - fs_index, - window_sequence, - scale_factor_grouping, - max_sfb, - nch, - top_layer, - base_band, - header_bits, - frame_length, - &cband_si_type, - &max_sfb_si_len, - )?; - - let header = BsacHeader { - frame_length, - header_length, - sba_mode, - top_layer, - base_snf_thr, - max_scalefactor, - base_band, - cband_si_type, - base_scf_model, - enh_scf_model, - max_sfb_si_len, - }; - let general = GeneralHeader { - window_sequence, - window_shape, - max_sfb, - scale_factor_grouping, - pns_data_present, - pns_start_sfb, - ms_mask_present, - tns, - }; - - let ngroups = geo.num_window_groups; - let mut ctx = BlockCtx { - nch, - geo, - arith: ArithDecoder::new(), - reader: SegmentReader::new(frame, 0, 0), - avail: 0, - cband_si: vec![Vec::new(); nch], - lines: vec![Vec::new(); nch], - scf: vec![vec![vec![None; max_sfb]; ngroups]; nch], - stereo_side_info_coded: vec![vec![false; max_sfb]; ngroups], - ms_used: vec![vec![false; max_sfb]; ngroups], - stereo_info: vec![vec![0u8; max_sfb]; ngroups], - is_position: vec![vec![0i32; max_sfb]; ngroups], - header, - general, - }; - for ch in 0..nch { - for g in 0..ngroups { - let len = ctx.geo.group_len[g]; - ctx.cband_si[ch].push(vec![0u8; len.div_ceil(32)]); - ctx.lines[ch].push(LineState { - mask: vec![0; len], - sign_neg: vec![false; len], - sign_coded: vec![false; len], - cur_snf: vec![0; len], - unc_snf: vec![0; len], - }); - } - } - - // §4.6.4.3.3: ms_mask_present == 2 sets every ms_used without - // decoding. - if nch == 2 && ctx.general.ms_mask_present == 2 { - for row in ctx.ms_used.iter_mut() { - row.fill(true); - } - } - - if ctx.header.sba_mode { - // SBA re-initializes the arithmetic code per segment; the - // segment split + higher-spectra scheduling lands with an - // SBA-bearing conformance vector. - return Err(Error::BsacUnsupportedTool); - } - // Non-SBA: one arithmetic segment from the header end to the - // frame end. - ctx.reader = SegmentReader::new(frame, header_bits as u64, (frame_length as u64) * 8); - ctx.arith = ArithDecoder::new(); - - let total_layers = ctx.geo.layers.len(); - // Suffix sums of the static layer budgets: the Table 4.33 - // `data_available()` gate — an enhancement layer decodes only - // while frame bits remain. - let mut suffix_avail = vec![0i64; total_layers + 1]; - for k in (0..total_layers).rev() { - suffix_avail[k] = suffix_avail[k + 1] + ctx.geo.layers[k].available_len; - } - // `prev_end[g]`: the highest end_index of any processed layer, - // per group — the §4.5.2.6.2.2 lower-spectra region. - let mut prev_end = vec![0usize; ngroups]; - let mut carry: i64 = -1; // segment start: 1 termination bit. - #[allow(clippy::needless_range_loop)] // ctx.geo.layers cannot be - // iterated while ctx is mutably borrowed inside the body. - for layer_idx in 0..total_layers { - let layer = ctx.geo.layers[layer_idx].clone(); - // Table 4.33: base sub-layers ride inside - // bsac_base_element() unconditionally; enhancement layers - // are gated on data_available(). - if layer_idx >= ctx.geo.slayer_size && carry + suffix_avail[layer_idx] <= 0 { - break; - } - ctx.avail = carry + layer.available_len; - // Side info. - ctx.layer_cband_si(&layer)?; - ctx.layer_sfb_si(layer_idx, &layer)?; - // First pass: the layer's new spectra. - let thr = if layer_idx < ctx.geo.slayer_size { - i32::from(ctx.header.base_snf_thr) - } else { - 0 - }; - let regions = [(layer.group, layer.start_index, layer.end_index)]; - ctx.spectral_data(®ions, thr, SnfKind::Cur); - // Store cur_snf → unc_snf for the layer's range. - for ch in 0..nch { - let st = &mut ctx.lines[ch][layer.group]; - let e = layer.end_index.min(st.cur_snf.len()); - for i in layer.start_index..e { - st.unc_snf[i] = st.cur_snf[i]; - } - } - // Secondary pass: refine every earlier line. - let lower: Vec<(usize, usize, usize)> = (0..ngroups) - .filter(|&g| prev_end[g] > 0) - .map(|g| (g, 0, prev_end[g])) - .collect(); - ctx.spectral_data(&lower, 0, SnfKind::Unc); - prev_end[layer.group] = prev_end[layer.group].max(layer.end_index); - carry = ctx.avail; - } - - // Assemble the signed samples. - let mut sample = vec![Vec::with_capacity(ngroups); nch]; - for (ch, sample_ch) in sample.iter_mut().enumerate().take(nch) { - for g in 0..ngroups { - let st = &ctx.lines[ch][g]; - let buf: Vec = st - .mask - .iter() - .zip(st.sign_neg.iter()) - .map(|(&m, &neg)| { - let v = m as i32; - if neg { - -v - } else { - v - } - }) - .collect(); - sample_ch.push(buf); - } - } - Ok(DecodedBlock { - header: ctx.header, - general: ctx.general, - sample, - scf: ctx.scf, - ms_used: ctx.ms_used, - stereo_info: ctx.stereo_info, - is_position: ctx.is_position, - geometry: ctx.geo, - }) -} - -/// Persistent ER BSAC stream decoder: one AU (`bsac_raw_data_block`) -/// in, one PCM frame out, carrying the §4.6.11 overlap-add state -/// across frames. -#[derive(Debug)] -pub struct BsacDecoder { - fs: u32, - fs_index: u8, - nch: usize, - filterbanks: Vec, -} - -impl BsacDecoder { - /// A decoder for `nch` channels at `fs` Hz (Table 1.18 index - /// `fs_index`). - pub fn new(fs: u32, fs_index: u8, nch: usize) -> Result { - if !(1..=2).contains(&nch) { - return Err(Error::BsacInvalidHeader); - } - Ok(BsacDecoder { - fs, - fs_index, - nch, - filterbanks: (0..nch).map(|_| Filterbank::new()).collect(), - }) - } - - /// Decode one access unit to interleaved 16-bit PCM - /// (1024 samples per channel). - pub fn decode_frame(&mut self, au: &[u8]) -> Result> { - let block = decode_bsac_raw_data_block(au, self.fs, self.fs_index, self.nch)?; - let spectra = reconstruct_spectra(&block, self.fs_index, self.nch)?; - let info = block_ics_info(&block, self.fs_index)?; - let mut channels = Vec::with_capacity(self.nch); - for (ch, mut spec) in spectra.into_iter().enumerate() { - if let Some(tns) = &block.general.tns[ch] { - tns_decode_frame_ics(&mut spec, tns, &info, 22, self.fs_index)?; - } - let time = self.filterbanks[ch].synthesize(&spec, &info)?; - channels.push(channel_to_s16(&time)); - } - let mut out = Vec::with_capacity(BSAC_FRAME_LEN * self.nch); - for i in 0..BSAC_FRAME_LEN { - for chan in &channels { - out.push(chan[i]); - } - } - Ok(out) - } - - /// Drop all cross-frame state (post-seek restart). - pub fn reset(&mut self) { - for fb in &mut self.filterbanks { - *fb = Filterbank::new(); - } - } -} - -/// The `IcsInfo` equivalent of a decoded block (drives the shared -/// TNS / filterbank / stereo primitives). -fn block_ics_info(block: &DecodedBlock, fs_index: u8) -> Result { - let short = block.general.window_sequence == WindowSequence::EightShort; - let num_swb = if short { - crate::ics_info::NUM_SWB_SHORT_WINDOW[fs_index as usize] - } else { - crate::ics_info::NUM_SWB_LONG_WINDOW[fs_index as usize] - }; - Ok(IcsInfo { - family: FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: block.general.window_sequence, - window_shape: block.general.window_shape, - max_sfb: block.general.max_sfb as u8, - scale_factor_grouping: if short { - Some(block.general.scale_factor_grouping) - } else { - None - }, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: if short { 8 } else { 1 }, - num_window_groups: block.geometry.num_window_groups as u8, - window_group_length: block.geometry.window_group_length.clone(), - num_swb, - }) -} - -/// Inverse-quantize + de-interleave one block into per-channel -/// window-major spectra, then run the §4.6.8.1 / §4.6.8.2 stereo -/// tools. -fn reconstruct_spectra(block: &DecodedBlock, fs_index: u8, nch: usize) -> Result>> { - let geo = &block.geometry; - let short = block.general.window_sequence == WindowSequence::EightShort; - let max_sfb = block.general.max_sfb; - let mut spectra = Vec::with_capacity(nch); - for ch in 0..nch { - let mut spec = vec![0.0f64; BSAC_FRAME_LEN]; - let mut window_base = 0usize; // first window of the group - for g in 0..geo.num_window_groups { - let wgl = geo.window_group_length[g] as usize; - let buf = &block.sample[ch][g]; - for sfb in 0..max_sfb { - let (s, e) = (geo.swb_offset[g][sfb], geo.swb_offset[g][sfb + 1]); - let Some(scf) = block.scf[ch][g][sfb] else { - continue; - }; - let gain = scale_factor_gain(scf); - for (gi, &q) in buf.iter().enumerate().take(e.min(buf.len())).skip(s) { - if q == 0 { - continue; - } - let x = inverse_quantize(q) * gain; - let out_idx = if short { - // §4.5.2.6.2.6: within a group, 4-line - // chunks interleave across the group's - // windows: group index - // `4·(chunk·wgl + w) + j` carries window - // `w`'s line `4·chunk + j`. - let chunk = gi / (4 * wgl); - let rem = gi % (4 * wgl); - let w = rem / 4; - let j = rem % 4; - (window_base + w) * 128 + chunk * 4 + j - } else { - gi - }; - spec[out_idx] = x; - } - } - window_base += wgl; - } - spectra.push(spec); - } - - if nch == 2 { - let info = block_ics_info(block, fs_index)?; - // Intensity stereo (stereo_info 2 / 3) reconstructs the - // right channel from the left before the M/S de-matrix - // (which skips intensity bands). - let ms_present = match block.general.ms_mask_present { - 0 => MsMaskPresent::AllZeros, - 2 => MsMaskPresent::AllOnes, - _ => MsMaskPresent::Mask, - }; - // Per-band codebook shadows for the shared primitives: - // intensity bands flag 15 (in phase) / 14 (out of phase) on - // the right channel. - let mut right_cb = vec![vec![1u8; max_sfb]; geo.num_window_groups]; - let mut is_pos = vec![vec![0i32; max_sfb]; geo.num_window_groups]; - let mut any_is = false; - for g in 0..geo.num_window_groups { - for sfb in 0..max_sfb { - match block.stereo_info[g][sfb] { - 2 => { - right_cb[g][sfb] = crate::section_data::INTENSITY_HCB; - is_pos[g][sfb] = block.is_position[g][sfb]; - any_is = true; - } - 3 => { - right_cb[g][sfb] = crate::section_data::INTENSITY_HCB2; - is_pos[g][sfb] = block.is_position[g][sfb]; - any_is = true; - } - _ => {} - } - } - } - if any_is { - let (left, right) = spectra.split_at_mut(1); - let mut pair = crate::intensity_stereo::IntensityPairSpectra { - left: &left[0], - right: &mut right[0], - right_sfb_cb: &right_cb, - is_pos: &is_pos, - }; - crate::intensity_stereo::apply_intensity_stereo( - &mut pair, - block.general.ms_mask_present != 0, - &block.ms_used, - &info, - fs_index, - )?; - } - let left_cb = vec![vec![1u8; max_sfb]; geo.num_window_groups]; - let (left, right) = spectra.split_at_mut(1); - let mut pair = ChannelPairSpectra { - left: &mut left[0], - right: &mut right[0], - left_sfb_cb: &left_cb, - right_sfb_cb: &right_cb, - }; - apply_ms_stereo(&mut pair, ms_present, &block.ms_used, &info, fs_index)?; - } - Ok(spectra) -} diff --git a/crates/vendor/oxideav-aac/src/bsac_layer.rs b/crates/vendor/oxideav-aac/src/bsac_layer.rs deleted file mode 100644 index fd91b055..00000000 --- a/crates/vendor/oxideav-aac/src/bsac_layer.rs +++ /dev/null @@ -1,499 +0,0 @@ -//! BSAC fine-grain scalability layer geometry — ISO/IEC -//! 14496-3:2009 §4.5.2.6.2.4 / §4.5.2.6.2.5. -//! -//! A `bsac_raw_data_block()` is a stack of scalability layers: the -//! base layer (split into `slayer_size` sub-layers, one per base -//! coding band) followed by `top_layer` enhancement layers of -//! ~1 kbit/s/ch each. Every layer covers a slice of the spectrum -//! (`layer_start_index .. layer_end_index` in its window group), a -//! run of 32-line coding bands, a run of scalefactor bands whose -//! side info it carries, and a bit budget (`available_len`) cut out -//! of the frame by the §4.5.2.6.2.5 `layer_bit_offset` derivation. -//! [`BsacGeometry::derive`] computes the whole roster from the -//! header fields, transcribing the spec pseudo-code (including its -//! evident loop-variable typos, noted inline). - -use crate::ics_info::WindowSequence; -use crate::swb_offset::{long_window_offsets, short_window_offsets}; -use crate::{Error, Result}; - -/// Frame length of the 1024-line family this decoder covers. -pub const BSAC_FRAME_LEN: usize = 1024; - -/// Short-window length. -const SHORT_LEN: usize = 128; - -/// §4.5.2.6.2.5: `max_cband0_si_len` — the fixed maximum length of -/// the 0th coding band's side information. -const MAX_CBAND0_SI_LEN: u32 = 11; - -/// One scalability layer's coverage and budget. -#[derive(Debug, Clone, Default)] -pub struct LayerInfo { - /// `layer_group[layer]` — the window group whose spectrum the - /// layer extends. - pub group: usize, - /// `layer_start_cband[layer]` .. `layer_end_cband[layer]`. - pub start_cband: usize, - /// Exclusive end coding band. - pub end_cband: usize, - /// `layer_start_index[layer]` .. `layer_end_index[layer]` - /// (group-local spectral lines). - pub start_index: usize, - /// Exclusive end line. - pub end_index: usize, - /// `layer_start_sfb[layer]` .. `layer_end_sfb[layer]`. - pub start_sfb: usize, - /// Exclusive end scalefactor band. - pub end_sfb: usize, - /// `layer_si_maxlen[layer]` in bits. - pub si_maxlen: u32, - /// `layer_bit_offset[layer]` — the layer's first bit within the - /// frame. - pub bit_offset: i64, - /// `available_len[layer]` in bits (before the segment-start - /// `-1` termination adjustment, which the decode driver - /// applies). - pub available_len: i64, - /// §4.6.4.6.3 `terminal_layer[layer]` — the layer ends an SBA - /// segment (always true for the last layer). - pub terminal: bool, -} - -/// The §4.5.2.6.2.4 / §4.5.2.6.2.5 derived geometry for one -/// `bsac_raw_data_block()`. -#[derive(Debug, Clone)] -pub struct BsacGeometry { - /// Number of window groups (1 for long sequences). - pub num_window_groups: usize, - /// Windows per group (sums to 8 for `EIGHT_SHORT`). - pub window_group_length: Vec, - /// Per-group scaled band offsets: `swb_offset[g][sfb] = - /// swb_offset_window[sfb] · window_group_length[g]`, length - /// `max_sfb + 1`. - pub swb_offset: Vec>, - /// Per-group group-buffer length (`1024` long, `wgl · 128` - /// short). - pub group_len: Vec, - /// `last_index[g]` — the spectral cap from `max_sfb`. - pub last_index: Vec, - /// Number of base sub-layers. - pub slayer_size: usize, - /// The header's `top_layer`. - pub top_layer: usize, - /// Per-layer coverage/budget, `slayer_size + top_layer` - /// entries. - pub layers: Vec, -} - -impl BsacGeometry { - /// Derive the whole layer roster. - /// - /// * `fs` / `fs_index` — sampling frequency (Hz / Table 1.18 - /// index). - /// * `window_sequence` + `scale_factor_grouping` + `max_sfb` — - /// from `general_header()`. - /// * `nch` — channels in the block (1 or 2). - /// * `top_layer` / `base_band` — from `bsac_header()`. - /// * `header_bits` — `layer_bit_offset[0]`, the total header - /// length in bits (byte-aligned). - /// * `frame_length` — the frame length in bytes. - /// * `cband_si_type` / `max_sfb_si_len` — per channel, from - /// `bsac_header()` (`max_sfb_si_len` raw, offset +5 applied - /// here). - #[allow(clippy::too_many_arguments)] - pub fn derive( - fs: u32, - fs_index: u8, - window_sequence: WindowSequence, - scale_factor_grouping: u8, - max_sfb: usize, - nch: usize, - top_layer: usize, - base_band: usize, - header_bits: i64, - frame_length: usize, - cband_si_type: &[u8], - max_sfb_si_len: &[u8], - ) -> Result { - // §4.5.2.6.2.4 grouping (identical to the AAC derivation). - let short = window_sequence == WindowSequence::EightShort; - let (num_window_groups, window_group_length) = if short { - let mut wgl: Vec = vec![1]; - for i in 0..7 { - if (scale_factor_grouping >> (6 - i)) & 1 == 0 { - wgl.push(1); - } else { - *wgl.last_mut().unwrap() += 1; - } - } - (wgl.len(), wgl) - } else { - (1, vec![1u8]) - }; - let window_offsets: &[u16] = if short { - short_window_offsets(fs_index)? - } else { - long_window_offsets(fs_index)? - }; - if max_sfb + 1 > window_offsets.len() { - return Err(Error::BsacInvalidHeader); - } - let mut swb_offset = Vec::with_capacity(num_window_groups); - let mut group_len = Vec::with_capacity(num_window_groups); - let mut last_index = Vec::with_capacity(num_window_groups); - for &wgl_u8 in window_group_length.iter().take(num_window_groups) { - let wgl = wgl_u8 as usize; - let offsets: Vec = (0..=max_sfb) - .map(|sfb| window_offsets[sfb] as usize * if short { wgl } else { 1 }) - .collect(); - last_index.push(offsets[max_sfb]); - swb_offset.push(offsets); - group_len.push(if short { - wgl * SHORT_LEN - } else { - BSAC_FRAME_LEN - }); - } - - // §4.5.2.6.2.5: slayer_size + per-group base band limit. - let mut end_index = vec![0usize; num_window_groups]; - let mut end_cband = vec![0usize; num_window_groups]; - let mut slayer_size = 0usize; - for g in 0..num_window_groups { - if short { - let wgl = window_group_length[g] as usize; - let mut ei = base_band * 4 * wgl; - if fs == 44_100 || fs == 48_000 { - if ei % 32 >= 16 { - ei = ei / 32 * 32 + 20; - } else if ei % 32 >= 4 { - ei = ei / 32 * 32 + 8; - } - } else if fs == 22_050 || fs == 24_000 || fs == 32_000 { - ei = ei / 16 * 16; - } else if fs == 11_025 || fs == 12_000 || fs == 16_000 { - ei = ei / 32 * 32; - } else { - ei = ei / 64 * 64; - } - end_index[g] = ei; - end_cband[g] = ei.div_ceil(32); - } else { - end_cband[g] = base_band; - } - slayer_size += end_cband[g]; - } - if slayer_size == 0 { - return Err(Error::BsacInvalidHeader); - } - - let total_layers = slayer_size + top_layer; - let mut layers = vec![LayerInfo::default(); total_layers]; - - // layer_group[]: base sub-layers walk the groups' cbands in - // order; enhancement layers cycle through the groups - // window-by-window (period `num_windows` — 8 for short, 1 - // for long; the spec writes the period-8 copy explicitly). - { - let mut layer = 0usize; - for (g, &nc) in end_cband.iter().enumerate().take(num_window_groups) { - for _ in 1..=nc { - layers[layer].group = g; - layer += 1; - } - } - let mut seq = Vec::new(); - for (g, &wgl) in window_group_length - .iter() - .enumerate() - .take(num_window_groups) - { - for _ in 0..wgl { - seq.push(g); - } - } - for (k, layer) in layers.iter_mut().enumerate().skip(slayer_size) { - layer.group = seq[(k - slayer_size) % seq.len()]; - } - } - - // Base sub-layers: one coding band each. - { - let mut layer = 0usize; - let mut end_index_run = vec![0usize; num_window_groups]; - for (g, &nc) in end_cband.iter().enumerate().take(num_window_groups) { - for cband in 0..nc { - layers[layer].start_cband = cband; - layers[layer].end_cband = cband + 1; - layers[layer].start_index = cband * 32; - layers[layer].end_index = (cband + 1) * 32; - end_index_run[g] = (cband + 1) * 32; - layer += 1; - } - } - // Enhancement layers extend the band limit at the - // rate-dependent §4.5.2.6.2.5 step. - let mut end_cband_run = end_cband.clone(); - let mut end_index_g = end_index_run; - for layer_info in layers.iter_mut().skip(slayer_size) { - let g = layer_info.group; - layer_info.start_index = end_index_g[g]; - let mut ei = end_index_g[g]; - if fs == 44_100 || fs == 48_000 { - if ei % 32 == 0 { - ei += 8; - } else { - ei += 12; - } - } else if fs == 22_050 || fs == 24_000 || fs == 32_000 { - ei += 16; - } else if fs == 11_025 || fs == 12_000 || fs == 16_000 { - ei += 32; - } else { - ei += 64; - } - if ei > last_index[g] { - ei = last_index[g]; - } - end_index_g[g] = ei; - layer_info.end_index = ei; - layer_info.start_cband = end_cband_run[g]; - end_cband_run[g] = ei.div_ceil(32); - layer_info.end_cband = end_cband_run[g]; - } - } - - // layer_start_sfb / layer_end_sfb (transcribed literally, - // `layer_end_sfb = sfb + 1` at the first band whose start - // offset reaches the layer's end index). - { - let mut end_sfb = vec![0usize; num_window_groups]; - for layer_info in layers.iter_mut() { - let g = layer_info.group; - layer_info.start_sfb = end_sfb[g]; - layer_info.end_sfb = max_sfb; - for (sfb, &off) in swb_offset[g].iter().enumerate().take(max_sfb) { - if layer_info.end_index <= off { - // Transcribed literally (`= sfb + 1`); the - // one-band lookahead is corpus-confirmed — - // the `= sfb` reading desyncs the arithmetic - // stream on frames that the `+ 1` reading - // decodes exactly. - layer_info.end_sfb = sfb + 1; - break; - } - } - end_sfb[g] = layer_info.end_sfb; - } - } - - // layer_si_maxlen. - for layer_info in layers.iter_mut() { - let mut si = 0u32; - for cband in layer_info.start_cband..layer_info.end_cband { - for &cst in cband_si_type.iter().take(nch) { - if cband == 0 { - si += MAX_CBAND0_SI_LEN; - } else { - si += u32::from( - crate::bsac_tables::CBAND_SI_TYPES - .get(cst as usize) - .ok_or(Error::BsacInvalidHeader)? - .max_len, - ); - } - } - } - for _sfb in layer_info.start_sfb..layer_info.end_sfb { - for &msl in max_sfb_si_len.iter().take(nch) { - si += u32::from(msl) + 5; - } - } - layer_info.si_maxlen = si; - } - - // layer_bit_offset: rate anchors for the enhancement - // layers, then top-down si-budget adjustments, the base - // sub-layer split, and the header overflow/underflow - // redistribution — §4.5.2.6.2.5, transcribed with the - // evident typos fixed (`slayer--` for `layer--`, `layer <=` - // for `m <=`). - let frame_bits = (frame_length as i64) * 8; - let mut bit_offset = vec![0i64; total_layers + 1]; - for (k, off) in bit_offset - .iter_mut() - .enumerate() - .take(total_layers + 1) - .skip(slayer_size) - { - let layer_bitrate = (nch as i64) * (((k - slayer_size) as i64) * 1000 + 16_000); - let mut v = layer_bitrate * (BSAC_FRAME_LEN as i64); - v = v / (fs as i64) / 8 * 8; - *off = v.min(frame_bits); - } - // The frame may carry more bytes than the top layer's - // nominal rate anchor (the encoder's bit reservoir); the - // stream end is the frame end, so the last boundary extends - // to it — the slack feeds the top layer's secondary - // (refinement) pass. - bit_offset[total_layers] = frame_bits; - for k in (slayer_size..total_layers).rev() { - let candidate = bit_offset[k + 1] - i64::from(layers[k].si_maxlen); - if candidate < bit_offset[k] { - bit_offset[k] = candidate; - } - } - for k in (0..slayer_size).rev() { - bit_offset[k] = bit_offset[k + 1] - i64::from(layers[k].si_maxlen); - } - let overflow = header_bits - bit_offset[0]; - bit_offset[0] = header_bits; - if overflow > 0 { - let mut overflow = overflow; - for k in (slayer_size..total_layers).rev() { - let mut layer_bit_size = bit_offset[k + 1] - bit_offset[k]; - layer_bit_size -= i64::from(layers[k].si_maxlen); - if layer_bit_size >= overflow { - layer_bit_size = overflow; - overflow = 0; - } else { - overflow -= layer_bit_size; - } - for off in bit_offset.iter_mut().take(k + 1).skip(1) { - *off += layer_bit_size; - } - if overflow <= 0 { - break; - } - } - } else { - let underflow = -overflow; - let share = underflow / (slayer_size as i64); - let extra = underflow % (slayer_size as i64); - for m in 1..slayer_size { - bit_offset[m] = bit_offset[m - 1] + i64::from(layers[m - 1].si_maxlen) + share; - if (m as i64) <= extra { - bit_offset[m] += 1; - } - } - } - for (k, layer_info) in layers.iter_mut().enumerate() { - layer_info.bit_offset = bit_offset[k]; - layer_info.available_len = bit_offset[k + 1] - bit_offset[k]; - } - - // §4.6.4.6.3 terminal_layer[]: a layer ends its segment when - // the next layer starts a different coding band run; the - // last layer always terminates. - for k in 0..total_layers { - layers[k].terminal = if k + 1 < total_layers { - layers[k].start_cband != layers[k + 1].start_cband - } else { - true - }; - } - - Ok(BsacGeometry { - num_window_groups, - window_group_length, - swb_offset, - group_len, - last_index, - slayer_size, - top_layer, - layers, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A 48 kHz mono long-window geometry with the header values of - /// a real conformance frame (`top_layer = 48`, `base_band = - /// 10`): the base splits into 10 sub-layers of one coding band, - /// enhancement layers extend by the 8/12-line 48 kHz step, and - /// the budgets partition the frame exactly. - #[test] - fn long_mono_layer_roster() { - let geo = BsacGeometry::derive( - 48_000, - 3, - WindowSequence::OnlyLong, - 0, - 40, - 1, - 48, - 10, - 72, - 171, - &[27], - &[0], - ) - .unwrap(); - assert_eq!(geo.slayer_size, 10); - assert_eq!(geo.layers.len(), 58); - // Base sub-layers: one 32-line cband each. - for (k, l) in geo.layers.iter().take(10).enumerate() { - assert_eq!(l.group, 0); - assert_eq!((l.start_cband, l.end_cband), (k, k + 1)); - assert_eq!((l.start_index, l.end_index), (32 * k, 32 * k + 32)); - } - // First enhancement layer starts at the base band limit. - assert_eq!(geo.layers[10].start_index, 320); - assert_eq!(geo.layers[10].end_index, 328); - assert_eq!(geo.layers[11].start_index, 328); - assert_eq!(geo.layers[11].end_index, 340); - // Budgets tile the frame: offsets ascend and the last layer - // ends at or before the frame end. - for w in geo.layers.windows(2) { - assert_eq!(w[0].bit_offset + w[0].available_len, w[1].bit_offset); - } - let last = geo.layers.last().unwrap(); - assert!(last.bit_offset + last.available_len <= 171 * 8); - assert_eq!(geo.layers[0].bit_offset, 72); - // sfb coverage is monotone and capped. - for l in &geo.layers { - assert!(l.start_sfb <= l.end_sfb && l.end_sfb <= 40); - } - // Non-SBA streams still mark segment boundaries; the last - // layer always terminates. - assert!(geo.layers.last().unwrap().terminal); - } - - /// The short-window 48 kHz band-limit rounding of - /// §4.5.2.6.2.5 (`% 32 >= 16 → +20`, `% 32 >= 4 → +8`). - #[test] - fn short_window_base_band_rounding() { - let geo = BsacGeometry::derive( - 48_000, - 3, - WindowSequence::EightShort, - 0, // 8 groups of 1 window - 14, - 1, - 8, - 10, - 72, - 400, - &[5], - &[2], - ) - .unwrap(); - assert_eq!(geo.num_window_groups, 8); - // base_band·4·1 = 40 → 40 % 32 = 8 (>= 4) → 32 + 8 = 40. - assert_eq!(geo.layers[0].end_index, 32); - // Each group contributes ceil(40/32) = 2 sub-layers. - assert_eq!(geo.slayer_size, 16); - for (k, l) in geo.layers.iter().take(16).enumerate() { - assert_eq!(l.group, k / 2); - assert_eq!(l.start_cband, k % 2); - } - // Enhancement layers cycle the 8 groups round-robin. - for k in 0..8 { - assert_eq!(geo.layers[16 + k].group, k); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/bsac_tables.rs b/crates/vendor/oxideav-aac/src/bsac_tables.rs deleted file mode 100644 index 11effaeb..00000000 --- a/crates/vendor/oxideav-aac/src/bsac_tables.rs +++ /dev/null @@ -1,1020 +0,0 @@ -//! Numeric tables for the ER BSAC noiseless coder — ISO/IEC -//! 14496-3:2009 §4.A.5 (Tables 4.A.31–4.A.77), transcribed from the -//! staged specification PDF. -//! -//! Three table families live here: -//! -//! * **General arithmetic models** — 14-bit cumulative-frequency -//! arrays consumed by the §4.5.2.6.2.7.4 `decode_symbol()` -//! procedure: the scalefactor models (Tables 4.A.37–4.A.43, -//! selected via Table 4.A.32), the `cband_si` models (Tables -//! 4.A.44–4.A.50 for coding bands past the 0th, Table 4.A.51 for -//! the 0th, selected via Table 4.A.31), and the stereo / PNS -//! side-info models (Tables 4.A.52–4.A.55). Every array is -//! strictly decreasing and ends in 0 (the `cum_freq[sym] > cum` -//! symbol search walks it in order). -//! * **Binary probability tables** — the 22 spectral bit-slice -//! tables (Tables 4.A.56–4.A.77), each a set of `p0` rows (14-bit -//! probability of the "0" symbol) indexed by the significance -//! distance from the coding band's MSB plane, the neighbouring -//! lines' context (Table 4.A.34 position), and the line's own -//! decoded higher bits. Tables 11–22 are normative aliases of -//! tables 9 / 10 at higher MSB planes; tables 9 / 10 alias their -//! zero-context sub-MSB rows onto tables 7 / 8 (the spec states -//! the aliases verbatim). [`spectral_p0`] resolves the whole -//! scheme. -//! * **Context / clamp tables** — the Table 4.A.34 position map -//! ([`context_position`]), and the Table 4.A.35 / 4.A.36 -//! `min_p0` / `max_p0` clamps applied when a layer's remaining -//! budget drops under 14 bits ([`clamp_p0`]). - -/// Table 4.A.31 row: parameters of one `cband_si_type`. -#[derive(Debug, Clone, Copy)] -pub struct CbandSiTypeParams { - /// `max_cband_si_len` — the side-info bit allowance used by the - /// §4.5.2.6.2.5 `layer_si_maxlen` accumulation. - pub max_len: u8, - /// Largest decodable `cband_si` for the 0th coding band. - pub largest_cband0: u8, - /// Largest decodable `cband_si` for every other coding band. - pub largest_other: u8, - /// Index into [`CBAND_SI_MODELS`] for the non-0th coding bands - /// (the 0th band always uses [`CBAND_SI_MODEL_CBAND0`]). - pub other_model: u8, -} - -/// Table 4.A.31 — `cband_si_type` parameters (32 rows). -pub const CBAND_SI_TYPES: [CbandSiTypeParams; 32] = [ - CbandSiTypeParams { - max_len: 6, - largest_cband0: 6, - largest_other: 4, - other_model: 0, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 6, - largest_other: 6, - other_model: 1, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 8, - largest_other: 4, - other_model: 0, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 8, - largest_other: 6, - other_model: 1, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 8, - largest_other: 8, - other_model: 2, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 10, - largest_other: 4, - other_model: 0, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 10, - largest_other: 6, - other_model: 1, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 10, - largest_other: 8, - other_model: 2, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 10, - largest_other: 10, - other_model: 3, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 12, - largest_other: 4, - other_model: 0, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 12, - largest_other: 6, - other_model: 1, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 12, - largest_other: 8, - other_model: 2, - }, - CbandSiTypeParams { - max_len: 8, - largest_cband0: 12, - largest_other: 12, - other_model: 4, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 14, - largest_other: 4, - other_model: 0, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 14, - largest_other: 6, - other_model: 1, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 14, - largest_other: 8, - other_model: 2, - }, - CbandSiTypeParams { - max_len: 8, - largest_cband0: 14, - largest_other: 12, - other_model: 4, - }, - CbandSiTypeParams { - max_len: 9, - largest_cband0: 14, - largest_other: 14, - other_model: 5, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 15, - largest_other: 4, - other_model: 0, - }, - CbandSiTypeParams { - max_len: 5, - largest_cband0: 15, - largest_other: 6, - other_model: 1, - }, - CbandSiTypeParams { - max_len: 6, - largest_cband0: 15, - largest_other: 8, - other_model: 2, - }, - CbandSiTypeParams { - max_len: 8, - largest_cband0: 15, - largest_other: 12, - other_model: 4, - }, - CbandSiTypeParams { - max_len: 10, - largest_cband0: 15, - largest_other: 15, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 8, - largest_cband0: 16, - largest_other: 12, - other_model: 4, - }, - CbandSiTypeParams { - max_len: 10, - largest_cband0: 16, - largest_other: 16, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 9, - largest_cband0: 17, - largest_other: 14, - other_model: 5, - }, - CbandSiTypeParams { - max_len: 10, - largest_cband0: 17, - largest_other: 17, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 10, - largest_cband0: 18, - largest_other: 18, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 12, - largest_cband0: 19, - largest_other: 19, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 12, - largest_cband0: 20, - largest_other: 20, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 12, - largest_cband0: 21, - largest_other: 21, - other_model: 6, - }, - CbandSiTypeParams { - max_len: 12, - largest_cband0: 22, - largest_other: 22, - other_model: 6, - }, -]; - -/// Table 4.A.32 — largest differential value decodable under each -/// `scf_model` (model 0 is "not used": no scalefactor decoding). -pub const SCF_MODEL_LARGEST: [u8; 8] = [0, 3, 7, 15, 15, 31, 31, 63]; - -/// Table 4.A.33 — MSB plane per `cband_si` (0..=22). The MSB plane -/// is the highest bit-slice a coefficient in the coding band -/// carries; `cband_si == 0` means the band decodes to all zeros. -pub const CBAND_SI_MSB_PLANE: [u8; 23] = [ - 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 11, 12, 13, 14, 15, -]; - -/// Table 4.A.35 — minimum `p0` when the layer's available length is -/// `1..=13` bits (index 0 unused). -pub const MIN_P0: [u16; 14] = [ - 0, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, - 0x0004, 0x0002, -]; - -/// Table 4.A.36 — maximum `p0` when the layer's available length is -/// `1..=13` bits (index 0 unused). -pub const MAX_P0: [u16; 14] = [ - 0, 0x2000, 0x3000, 0x3800, 0x3c00, 0x3e00, 0x3f00, 0x3f80, 0x3fc0, 0x3fe0, 0x3ff0, 0x3ff8, - 0x3ffc, 0x3ffe, -]; - -/// §4.6.4.2.3: clamp a spectral-bit `p0` onto the Table 4.A.35 / -/// 4.A.36 band when fewer than 14 bits remain in the layer. -pub fn clamp_p0(p0: u16, available_len: i64) -> u16 { - if (1..14).contains(&available_len) { - let i = available_len as usize; - p0.clamp(MIN_P0[i], MAX_P0[i]) - } else { - p0 - } -} - -/// The §4.5.2.6.2.2.13 sign-bit probability: `p0 = 0.5` as a 14-bit -/// fixed-point number. -pub const SIGN_P0: u16 = 0x2000; - -/// Scalefactor arithmetic model 1 (Table 4.A.37). -pub const SCF_MODEL_1: [u16; 4] = [0x0752, 0x03cd, 0x014d, 0x0000]; - -/// Scalefactor arithmetic model 2 (Table 4.A.38). -pub const SCF_MODEL_2: [u16; 8] = [ - 0x112f, 0x0de7, 0x0a8b, 0x07c1, 0x047a, 0x023a, 0x00d4, 0x0000, -]; - -/// Scalefactor arithmetic model 3 (Table 4.A.39). -pub const SCF_MODEL_3: [u16; 16] = [ - 0x1f67, 0x1c5f, 0x18d8, 0x1555, 0x1215, 0x0eb4, 0x0adc, 0x0742, 0x0408, 0x01e6, 0x00df, 0x0052, - 0x0032, 0x0023, 0x000c, 0x0000, -]; - -/// Scalefactor arithmetic model 4 (Table 4.A.40). -pub const SCF_MODEL_4: [u16; 16] = [ - 0x250f, 0x22b8, 0x2053, 0x1deb, 0x1b05, 0x186d, 0x15df, 0x12d9, 0x0f77, 0x0c01, 0x0833, 0x050d, - 0x0245, 0x008c, 0x0033, 0x0000, -]; - -/// Scalefactor arithmetic model 5 (Table 4.A.41). -pub const SCF_MODEL_5: [u16; 32] = [ - 0x08a8, 0x074e, 0x0639, 0x0588, 0x048c, 0x03cf, 0x032e, 0x0272, 0x01bc, 0x013e, 0x00e4, 0x0097, - 0x0069, 0x0043, 0x002f, 0x0029, 0x0020, 0x001b, 0x0018, 0x0015, 0x0012, 0x000f, 0x000d, 0x000c, - 0x000a, 0x0009, 0x0007, 0x0006, 0x0004, 0x0003, 0x0001, 0x0000, -]; - -/// Scalefactor arithmetic model 6 (Table 4.A.42). -pub const SCF_MODEL_6: [u16; 32] = [ - 0x0c2a, 0x099f, 0x0809, 0x06ec, 0x0603, 0x053d, 0x0491, 0x040e, 0x0394, 0x030a, 0x02a5, 0x0259, - 0x0202, 0x01bc, 0x0170, 0x0133, 0x0102, 0x00c9, 0x0097, 0x0073, 0x004f, 0x0037, 0x0022, 0x0016, - 0x000f, 0x000b, 0x0009, 0x0007, 0x0005, 0x0003, 0x0001, 0x0000, -]; - -/// Scalefactor arithmetic model 7 (Table 4.A.43). -pub const SCF_MODEL_7: [u16; 64] = [ - 0x3b5e, 0x3a90, 0x39d3, 0x387c, 0x3702, 0x3566, 0x33a7, 0x321c, 0x2f90, 0x2cf2, 0x29fe, 0x26fa, - 0x23e4, 0x20df, 0x1e0d, 0x1ac4, 0x1804, 0x159a, 0x131e, 0x10e7, 0x0e5b, 0x0c9c, 0x0b78, 0x0a21, - 0x08fd, 0x07b7, 0x06b5, 0x062c, 0x055d, 0x04f6, 0x04d4, 0x044b, 0x038e, 0x02e2, 0x029d, 0x0236, - 0x0225, 0x01f2, 0x01cf, 0x01ad, 0x019c, 0x0179, 0x0168, 0x0157, 0x0146, 0x0135, 0x0123, 0x0112, - 0x0101, 0x00f0, 0x00df, 0x00ce, 0x00bc, 0x00ab, 0x009a, 0x0089, 0x0078, 0x0067, 0x0055, 0x0044, - 0x0033, 0x0022, 0x0011, 0x0000, -]; - -/// cband_si arithmetic model 0 (Table 4.A.44). -pub const CBAND_SI_MODEL_0: [u16; 5] = [0x3ef6, 0x3b59, 0x1b12, 0x12a3, 0x0000]; - -/// cband_si arithmetic model 1 (Table 4.A.45). -pub const CBAND_SI_MODEL_1: [u16; 7] = [0x3d51, 0x33ae, 0x1cff, 0x0fb7, 0x07e4, 0x022b, 0x0000]; - -/// cband_si arithmetic model 2 (Table 4.A.46). -pub const CBAND_SI_MODEL_2: [u16; 9] = [ - 0x3a47, 0x2aec, 0x1e05, 0x1336, 0x0e7d, 0x0860, 0x05e0, 0x044a, 0x0000, -]; - -/// cband_si arithmetic model 3 (Table 4.A.47). -pub const CBAND_SI_MODEL_3: [u16; 11] = [ - 0x36be, 0x27ae, 0x20f4, 0x1749, 0x14d5, 0x0d46, 0x0ad3, 0x0888, 0x0519, 0x020b, 0x0000, -]; - -/// cband_si arithmetic model 4 (Table 4.A.48). -pub const CBAND_SI_MODEL_4: [u16; 13] = [ - 0x3983, 0x2e77, 0x2b03, 0x1ee8, 0x1df9, 0x1307, 0x11e4, 0x0b4d, 0x094c, 0x0497, 0x0445, 0x0040, - 0x0000, -]; - -/// cband_si arithmetic model 5 (Table 4.A.49). -pub const CBAND_SI_MODEL_5: [u16; 15] = [ - 0x306f, 0x249e, 0x1f56, 0x1843, 0x161a, 0x102d, 0x0f6c, 0x0c81, 0x0af2, 0x07a8, 0x071a, 0x0454, - 0x0413, 0x0016, 0x0000, -]; - -/// cband_si arithmetic model 6 (Table 4.A.50). -pub const CBAND_SI_MODEL_6: [u16; 23] = [ - 0x31af, 0x2001, 0x162d, 0x127e, 0x0f05, 0x0c34, 0x0b8f, 0x0a61, 0x0955, 0x0825, 0x07dd, 0x06a9, - 0x0688, 0x055b, 0x054b, 0x02f7, 0x0198, 0x0077, 0x0010, 0x000c, 0x0008, 0x0004, 0x0000, -]; - -/// cband_si arithmetic model for the 0th coding band (Table 4.A.51). -pub const CBAND_SI_MODEL_CBAND0: [u16; 23] = [ - 0x3ff8, 0x3ff0, 0x3fe8, 0x3fe0, 0x3fd7, 0x3f31, 0x3cd7, 0x3bc9, 0x3074, 0x2bcf, 0x231b, 0x13db, - 0x0d51, 0x0603, 0x044c, 0x0080, 0x0030, 0x0028, 0x0020, 0x0018, 0x0010, 0x0008, 0x0000, -]; - -/// MS_used model (Table 4.A.52). -pub const MS_USED_MODEL: [u16; 2] = [0x2ccd, 0x0000]; - -/// stereo_info model (Table 4.A.53). -pub const STEREO_INFO_MODEL: [u16; 4] = [0x3666, 0x1000, 0x0666, 0x0000]; - -/// noise_flag arithmetic model (Table 4.A.54). -pub const NOISE_FLAG_MODEL: [u16; 2] = [0x2000, 0x0000]; - -/// noise_mode arithmetic model (Table 4.A.55). -pub const NOISE_MODE_MODEL: [u16; 4] = [0x3000, 0x2000, 0x1000, 0x0000]; - -/// BSAC probability table 1 (MSB plane 1), MSB row (Table 4.A.56). -pub const PROB_T1_MSB: [u16; 15] = [ - 0x3900, 0x3a00, 0x2f00, 0x3b00, 0x2f00, 0x3700, 0x2c00, 0x3b00, 0x3000, 0x3600, 0x2d00, 0x3900, - 0x2f00, 0x3700, 0x2c00, -]; - -/// BSAC probability table 2 (MSB plane 1), MSB row (Table 4.A.57). -pub const PROB_T2_MSB: [u16; 15] = [ - 0x2800, 0x2800, 0x2500, 0x2900, 0x2600, 0x2700, 0x2300, 0x2a00, 0x2700, 0x2800, 0x2400, 0x2800, - 0x2500, 0x2600, 0x2200, -]; - -/// BSAC probability table 3 (MSB plane 2), MSB row (Table 4.A.58). -pub const PROB_T3_MSB: [u16; 15] = [ - 0x3d00, 0x3d00, 0x3300, 0x3d00, 0x3300, 0x3b00, 0x3300, 0x3d00, 0x3200, 0x3b00, 0x3100, 0x3e00, - 0x3700, 0x3c00, 0x3300, -]; - -/// BSAC probability table 3, MSB-1, zero higher bits (Table 4.A.58). -pub const PROB_T3_ZERO_1: [u16; 65] = [ - 0x3700, 0x3a00, 0x2800, 0x3b00, 0x2600, 0x2c00, 0x2400, 0x3a00, 0x2500, 0x2b00, 0x2400, 0x3100, - 0x2300, 0x2900, 0x2300, 0x3000, 0x2c00, 0x1d00, 0x2200, 0x1a00, 0x1c00, 0x1600, 0x2700, 0x2200, - 0x1a00, 0x1d00, 0x1900, 0x1c00, 0x1e00, 0x2c00, 0x2400, 0x1900, 0x1e00, 0x1f00, 0x1c00, 0x2b00, - 0x2400, 0x2900, 0x2700, 0x2400, 0x1300, 0x1a00, 0x2000, 0x1800, 0x2300, 0x2500, 0x1f00, 0x2c00, - 0x2300, 0x3600, 0x2800, 0x3100, 0x2500, 0x1400, 0x1200, 0x1800, 0x1400, 0x2100, 0x2200, 0x1000, - 0x1e00, 0x3000, 0x2600, 0x1200, 0x2200, -]; - -/// BSAC probability table 3, MSB-1, non-zero higher bits (Table 4.A.58). -pub const PROB_T3_NZ_1: [u16; 1] = [0x3100]; - -/// BSAC probability table 4 (MSB plane 2), MSB row (Table 4.A.59). -pub const PROB_T4_MSB: [u16; 15] = [ - 0x3900, 0x3a00, 0x2e00, 0x3a00, 0x2f00, 0x3400, 0x2a00, 0x3a00, 0x3000, 0x3500, 0x2c00, 0x3600, - 0x2b00, 0x3100, 0x2500, -]; - -/// BSAC probability table 4, MSB-1, zero higher bits (Table 4.A.59). -pub const PROB_T4_ZERO_1: [u16; 65] = [ - 0x1e00, 0x1d00, 0x1c00, 0x1d00, 0x1c00, 0x1d00, 0x1b00, 0x1d00, 0x1e00, 0x1e00, 0x1a00, 0x1e00, - 0x1c00, 0x1d00, 0x1b00, 0x1a00, 0x1a00, 0x1800, 0x1800, 0x1800, 0x1700, 0x1700, 0x1800, 0x1a00, - 0x1700, 0x1700, 0x1900, 0x1800, 0x1600, 0x1700, 0x1600, 0x1500, 0x1700, 0x1800, 0x1600, 0x1c00, - 0x1700, 0x1900, 0x1700, 0x1500, 0x1c00, 0x1500, 0x1600, 0x0f00, 0x1800, 0x1400, 0x1700, 0x1a00, - 0x1a00, 0x1e00, 0x1800, 0x1c00, 0x1b00, 0x1500, 0x1300, 0x1500, 0x1400, 0x1600, 0x1500, 0x1700, - 0x1600, 0x1b00, 0x1800, 0x1400, 0x1400, -]; - -/// BSAC probability table 4, MSB-1, non-zero higher bits (Table 4.A.59). -pub const PROB_T4_NZ_1: [u16; 1] = [0x3600]; - -/// BSAC probability table 5 (MSB plane 3), MSB row (Table 4.A.60). -pub const PROB_T5_MSB: [u16; 15] = [ - 0x3d00, 0x3d00, 0x3200, 0x3d00, 0x3300, 0x3d00, 0x3600, 0x3d00, 0x3500, 0x3c00, 0x3500, 0x3f00, - 0x3b00, 0x3f00, 0x3d00, -]; - -/// BSAC probability table 5, MSB-1, zero higher bits (Table 4.A.60). -pub const PROB_T5_ZERO_1: [u16; 65] = [ - 0x3c00, 0x3d00, 0x2b00, 0x3d00, 0x2900, 0x3500, 0x2c00, 0x3d00, 0x2b00, 0x3400, 0x2b00, 0x3800, - 0x2b00, 0x3700, 0x2a00, 0x3900, 0x3400, 0x2400, 0x2a00, 0x1c00, 0x1f00, 0x1600, 0x3500, 0x2500, - 0x1a00, 0x2a00, 0x2200, 0x2b00, 0x2a00, 0x3500, 0x2600, 0x1a00, 0x2600, 0x2500, 0x2700, 0x3500, - 0x2d00, 0x3800, 0x3200, 0x2e00, 0x1800, 0x1600, 0x2900, 0x2500, 0x3100, 0x2c00, 0x2300, 0x3600, - 0x3000, 0x3c00, 0x3300, 0x3b00, 0x3400, 0x1700, 0x1a00, 0x1c00, 0x1900, 0x2900, 0x2a00, 0x2400, - 0x2700, 0x3c00, 0x3600, 0x1d00, 0x3100, -]; - -/// BSAC probability table 5, MSB-1, non-zero higher bits (Table 4.A.60). -pub const PROB_T5_NZ_1: [u16; 1] = [0x3100]; - -/// BSAC probability table 5, MSB-2, zero higher bits (Table 4.A.60). -pub const PROB_T5_ZERO_2: [u16; 65] = [ - 0x3400, 0x3800, 0x2700, 0x3900, 0x2700, 0x2f00, 0x2200, 0x3800, 0x2500, 0x2d00, 0x2000, 0x3300, - 0x2000, 0x2900, 0x1e00, 0x2b00, 0x2300, 0x1a00, 0x1a00, 0x1b00, 0x1800, 0x1700, 0x1e00, 0x1c00, - 0x1b00, 0x1c00, 0x1b00, 0x1a00, 0x1800, 0x1d00, 0x1b00, 0x1800, 0x1900, 0x1b00, 0x1a00, 0x1d00, - 0x1e00, 0x1f00, 0x1b00, 0x1e00, 0x1200, 0x1400, 0x1a00, 0x1300, 0x1c00, 0x1b00, 0x1900, 0x2000, - 0x1e00, 0x3000, 0x2900, 0x2d00, 0x2500, 0x1300, 0x1700, 0x1400, 0x1300, 0x1e00, 0x1f00, 0x1100, - 0x1900, 0x2100, 0x1e00, 0x1500, 0x1a00, -]; - -/// BSAC probability table 5, MSB-2, non-zero higher bits (Table 4.A.60). -pub const PROB_T5_NZ_2: [u16; 3] = [0x2a00, 0x2b00, 0x2800]; - -/// BSAC probability table 6 (MSB plane 3), MSB row (Table 4.A.61). -pub const PROB_T6_MSB: [u16; 15] = [ - 0x3800, 0x3a00, 0x2d00, 0x3a00, 0x2d00, 0x3600, 0x2d00, 0x3a00, 0x2d00, 0x3600, 0x2b00, 0x3a00, - 0x2800, 0x3600, 0x2700, -]; - -/// BSAC probability table 6, MSB-1, zero higher bits (Table 4.A.61). -pub const PROB_T6_ZERO_1: [u16; 65] = [ - 0x2b00, 0x3000, 0x2500, 0x2f00, 0x2600, 0x2d00, 0x2400, 0x3000, 0x2500, 0x2b00, 0x2400, 0x2d00, - 0x2500, 0x2800, 0x2500, 0x2a00, 0x2900, 0x2300, 0x2200, 0x1e00, 0x1b00, 0x1900, 0x2600, 0x2300, - 0x1f00, 0x1d00, 0x2200, 0x1b00, 0x1800, 0x2100, 0x2100, 0x1d00, 0x1d00, 0x1f00, 0x1f00, 0x2900, - 0x2600, 0x2a00, 0x2100, 0x2300, 0x1800, 0x1a00, 0x1d00, 0x2000, 0x1c00, 0x1a00, 0x1e00, 0x2900, - 0x2800, 0x2f00, 0x2300, 0x2f00, 0x2600, 0x1d00, 0x1700, 0x1d00, 0x1c00, 0x1e00, 0x2100, 0x1700, - 0x2200, 0x2300, 0x2300, 0x1400, 0x1a00, -]; - -/// BSAC probability table 6, MSB-1, non-zero higher bits (Table 4.A.61). -pub const PROB_T6_NZ_1: [u16; 1] = [0x3000]; - -/// BSAC probability table 6, MSB-2, zero higher bits (Table 4.A.61). -pub const PROB_T6_ZERO_2: [u16; 65] = [ - 0x1900, 0x1900, 0x1900, 0x1b00, 0x1700, 0x1b00, 0x1a00, 0x1000, 0x1900, 0x1600, 0x1800, 0x1e00, - 0x1900, 0x1a00, 0x1700, 0x1b00, 0x1700, 0x1500, 0x1500, 0x1500, 0x1700, 0x1400, 0x1900, 0x1700, - 0x1600, 0x1600, 0x1200, 0x1300, 0x1200, 0x1600, 0x1500, 0x1500, 0x1300, 0x1600, 0x1600, 0x1c00, - 0x1400, 0x1700, 0x1600, 0x1400, 0x1400, 0x1400, 0x1500, 0x1400, 0x1300, 0x1300, 0x1500, 0x1800, - 0x1600, 0x1f00, 0x1a00, 0x1e00, 0x1800, 0x1700, 0x1600, 0x1600, 0x1300, 0x1400, 0x1300, 0x1100, - 0x1500, 0x1600, 0x1500, 0x1200, 0x1300, -]; - -/// BSAC probability table 6, MSB-2, non-zero higher bits (Table 4.A.61). -pub const PROB_T6_NZ_2: [u16; 3] = [0x2b00, 0x2800, 0x2700]; - -/// BSAC probability table 7 (MSB plane 4), MSB row (Table 4.A.62). -pub const PROB_T7_MSB: [u16; 15] = [ - 0x3d00, 0x3d00, 0x3500, 0x3e00, 0x3500, 0x3f00, 0x3b00, 0x3e00, 0x3200, 0x3f00, 0x3a00, 0x3f00, - 0x3d00, 0x3f00, 0x3b00, -]; - -/// BSAC probability table 7, MSB-1, zero higher bits (Table 4.A.62). -pub const PROB_T7_ZERO_1: [u16; 65] = [ - 0x3f00, 0x3f00, 0x3200, 0x3f00, 0x3500, 0x3e00, 0x3700, 0x3f00, 0x2d00, 0x3c00, 0x3000, 0x3f00, - 0x3700, 0x3e00, 0x3400, 0x3f00, 0x3900, 0x2600, 0x2f00, 0x1e00, 0x2400, 0x1500, 0x3700, 0x3100, - 0x1b00, 0x2600, 0x2300, 0x3a00, 0x3900, 0x3e00, 0x2b00, 0x2200, 0x2800, 0x2f00, 0x2500, 0x3e00, - 0x3700, 0x3e00, 0x3d00, 0x3900, 0x1a00, 0x3300, 0x2500, 0x2800, 0x3c00, 0x3800, 0x2c00, 0x3d00, - 0x3800, 0x3f00, 0x3b00, 0x3f00, 0x3a00, 0x1e00, 0x1b00, 0x1800, 0x1800, 0x3b00, 0x3a00, 0x1200, - 0x2f00, 0x3f00, 0x3b00, 0x1b00, 0x3500, -]; - -/// BSAC probability table 7, MSB-1, non-zero higher bits (Table 4.A.62). -pub const PROB_T7_NZ_1: [u16; 1] = [0x2f00]; - -/// BSAC probability table 7, MSB-2, zero higher bits (Table 4.A.62). -pub const PROB_T7_ZERO_2: [u16; 65] = [ - 0x3c00, 0x3e00, 0x3000, 0x3e00, 0x3100, 0x3a00, 0x3100, 0x3d00, 0x2c00, 0x3900, 0x2e00, 0x3c00, - 0x2d00, 0x3c00, 0x3100, 0x3d00, 0x3100, 0x2100, 0x2c00, 0x2600, 0x2800, 0x1d00, 0x2b00, 0x2800, - 0x2800, 0x2400, 0x2200, 0x2100, 0x2300, 0x2d00, 0x2500, 0x1f00, 0x2100, 0x2b00, 0x2700, 0x3200, - 0x2d00, 0x3400, 0x2a00, 0x3500, 0x1800, 0x1800, 0x1f00, 0x1e00, 0x2e00, 0x2a00, 0x2400, 0x3000, - 0x2b00, 0x3e00, 0x3d00, 0x3d00, 0x3a00, 0x1e00, 0x2b00, 0x2600, 0x1900, 0x3400, 0x3500, 0x1c00, - 0x2600, 0x3300, 0x2a00, 0x1c00, 0x2b00, -]; - -/// BSAC probability table 7, MSB-2, non-zero higher bits (Table 4.A.62). -pub const PROB_T7_NZ_2: [u16; 3] = [0x2800, 0x2900, 0x2400]; - -/// BSAC probability table 7, MSB-3 (others), zero higher bits (Table 4.A.62). -pub const PROB_T7_ZERO_3: [u16; 65] = [ - 0x3500, 0x3b00, 0x2900, 0x3b00, 0x2a00, 0x3100, 0x2700, 0x3b00, 0x2600, 0x2f00, 0x2400, 0x3400, - 0x2300, 0x2d00, 0x2000, 0x3300, 0x2700, 0x1c00, 0x2400, 0x1c00, 0x1c00, 0x1900, 0x2700, 0x2800, - 0x1b00, 0x1d00, 0x2000, 0x1b00, 0x1a00, 0x2300, 0x1d00, 0x1700, 0x1e00, 0x2400, 0x2100, 0x2b00, - 0x2100, 0x2800, 0x2000, 0x2300, 0x1b00, 0x1500, 0x1b00, 0x1400, 0x1a00, 0x1a00, 0x2000, 0x2a00, - 0x2200, 0x3700, 0x2f00, 0x3200, 0x2a00, 0x1700, 0x1700, 0x1600, 0x1900, 0x2500, 0x2300, 0x1500, - 0x1900, 0x2500, 0x2200, 0x1400, 0x1b00, -]; - -/// BSAC probability table 7, MSB-3 (others), non-zero higher bits (Table 4.A.62). -pub const PROB_T7_NZ_3: [u16; 7] = [0x2d00, 0x2500, 0x2300, 0x2500, 0x2500, 0x2600, 0x2400]; - -/// BSAC probability table 8 (MSB plane 4), MSB row (Table 4.A.63). -pub const PROB_T8_MSB: [u16; 15] = [ - 0x3b00, 0x3c00, 0x3400, 0x3c00, 0x3400, 0x3a00, 0x3000, 0x3c00, 0x3200, 0x3a00, 0x3100, 0x3c00, - 0x3000, 0x3900, 0x2f00, -]; - -/// BSAC probability table 8, MSB-1, zero higher bits (Table 4.A.63). -pub const PROB_T8_ZERO_1: [u16; 65] = [ - 0x3500, 0x3800, 0x2c00, 0x3900, 0x2c00, 0x3400, 0x2b00, 0x3800, 0x2e00, 0x3400, 0x2d00, 0x3600, - 0x2a00, 0x3300, 0x2800, 0x3100, 0x3100, 0x2600, 0x2900, 0x2000, 0x2300, 0x1f00, 0x2d00, 0x2600, - 0x2000, 0x2600, 0x2300, 0x2500, 0x2100, 0x2c00, 0x2400, 0x1d00, 0x2500, 0x2400, 0x2400, 0x3000, - 0x2800, 0x3000, 0x2900, 0x2200, 0x1e00, 0x1c00, 0x2500, 0x1d00, 0x2300, 0x2300, 0x2500, 0x3300, - 0x2c00, 0x3700, 0x2b00, 0x3400, 0x2c00, 0x1e00, 0x1c00, 0x2100, 0x1b00, 0x2900, 0x2a00, 0x1d00, - 0x2600, 0x3200, 0x2a00, 0x2000, 0x2400, -]; - -/// BSAC probability table 8, MSB-1, non-zero higher bits (Table 4.A.63). -pub const PROB_T8_NZ_1: [u16; 1] = [0x3200]; - -/// BSAC probability table 8, MSB-2, zero higher bits (Table 4.A.63). -pub const PROB_T8_ZERO_2: [u16; 65] = [ - 0x2900, 0x2e00, 0x2600, 0x2f00, 0x2600, 0x2d00, 0x2600, 0x2e00, 0x2500, 0x2b00, 0x2600, 0x2f00, - 0x2300, 0x2a00, 0x2300, 0x2800, 0x2800, 0x2100, 0x2400, 0x2000, 0x2000, 0x1b00, 0x2400, 0x1f00, - 0x1c00, 0x2100, 0x2200, 0x1d00, 0x1c00, 0x1f00, 0x1c00, 0x1900, 0x1e00, 0x2100, 0x2100, 0x2900, - 0x2200, 0x2300, 0x2100, 0x1c00, 0x1a00, 0x1a00, 0x2100, 0x2100, 0x1c00, 0x1c00, 0x1f00, 0x2700, - 0x2500, 0x2d00, 0x2700, 0x2a00, 0x2300, 0x1c00, 0x1d00, 0x1a00, 0x1a00, 0x1b00, 0x1d00, 0x1800, - 0x2000, 0x2300, 0x1f00, 0x1900, 0x1c00, -]; - -/// BSAC probability table 8, MSB-2, non-zero higher bits (Table 4.A.63). -pub const PROB_T8_NZ_2: [u16; 3] = [0x2b00, 0x2900, 0x2800]; - -/// BSAC probability table 8, MSB-3 (others), zero higher bits (Table 4.A.63). -pub const PROB_T8_ZERO_3: [u16; 65] = [ - 0x1c00, 0x1e00, 0x1b00, 0x1e00, 0x1c00, 0x1e00, 0x1900, 0x1a00, 0x1f00, 0x1f00, 0x1900, 0x2000, - 0x1a00, 0x1f00, 0x1700, 0x1b00, 0x1a00, 0x1900, 0x1800, 0x1900, 0x1800, 0x1600, 0x1900, 0x1a00, - 0x1900, 0x1700, 0x1800, 0x1700, 0x1800, 0x1600, 0x1700, 0x1400, 0x1600, 0x1800, 0x1a00, 0x1c00, - 0x1c00, 0x1c00, 0x1700, 0x1700, 0x1500, 0x1500, 0x1600, 0x1600, 0x1500, 0x1400, 0x1700, 0x1b00, - 0x1a00, 0x2300, 0x1c00, 0x1d00, 0x1a00, 0x1600, 0x1600, 0x1500, 0x1400, 0x1800, 0x1500, 0x1300, - 0x1700, 0x1900, 0x1600, 0x1400, 0x1400, -]; - -/// BSAC probability table 8, MSB-3 (others), non-zero higher bits (Table 4.A.63). -pub const PROB_T8_NZ_3: [u16; 7] = [0x2800, 0x2500, 0x2500, 0x2700, 0x2500, 0x2600, 0x2500]; - -/// BSAC probability table 9 (MSB plane 5), MSB row (Table 4.A.64). -pub const PROB_T9_MSB: [u16; 15] = [ - 0x3d00, 0x3e00, 0x3300, 0x3e00, 0x3500, 0x3e00, 0x3700, 0x3e00, 0x3400, 0x3e00, 0x3500, 0x3f00, - 0x3d00, 0x3f00, 0x3c00, -]; - -/// BSAC probability table 9, MSB-1, non-zero higher bits (Table 4.A.64). -pub const PROB_T9_NZ_1: [u16; 1] = [0x2e00]; - -/// BSAC probability table 9, MSB-2, non-zero higher bits (Table 4.A.64). -pub const PROB_T9_NZ_2: [u16; 3] = [0x2900, 0x2a00, 0x2700]; - -/// BSAC probability table 9, MSB-3, non-zero higher bits (Table 4.A.64). -pub const PROB_T9_NZ_3: [u16; 7] = [0x2d00, 0x2500, 0x2400, 0x2500, 0x2400, 0x2500, 0x2300]; - -/// BSAC probability table 9, others, non-zero higher bits (Table 4.A.64). -pub const PROB_T9_NZ_4: [u16; 16] = [ - 0x2800, 0x2500, 0x2300, 0x2300, 0x2200, 0x2200, 0x2200, 0x2200, 0x2200, 0x2200, 0x2200, 0x2100, - 0x2000, 0x2200, 0x2100, 0x2000, -]; - -/// BSAC probability table 10 (MSB plane 5), MSB row (Table 4.A.65). -pub const PROB_T10_MSB: [u16; 15] = [ - 0x3b00, 0x3c00, 0x3400, 0x3c00, 0x3200, 0x3900, 0x2e00, 0x3d00, 0x3400, 0x3900, 0x2f00, 0x3c00, - 0x2d00, 0x3700, 0x2d00, -]; - -/// BSAC probability table 10, MSB-1, non-zero higher bits (Table 4.A.65). -pub const PROB_T10_NZ_1: [u16; 1] = [0x3100]; - -/// BSAC probability table 10, MSB-2, non-zero higher bits (Table 4.A.65). -pub const PROB_T10_NZ_2: [u16; 3] = [0x2b00, 0x2a00, 0x2900]; - -/// BSAC probability table 10, MSB-3, non-zero higher bits (Table 4.A.65). -pub const PROB_T10_NZ_3: [u16; 7] = [0x2700, 0x2600, 0x2500, 0x2500, 0x2500, 0x2200, 0x2200]; - -/// BSAC probability table 10, others, non-zero higher bits (Table 4.A.65). -pub const PROB_T10_NZ_4: [u16; 16] = [ - 0x2200, 0x2300, 0x2300, 0x2300, 0x2200, 0x2300, 0x2200, 0x2300, 0x2200, 0x2200, 0x2200, 0x2200, - 0x2200, 0x2000, 0x2100, 0x2200, -]; - -/// The seven Table 4.A.44–4.A.50 `cband_si` models, indexed by the -/// Table 4.A.31 `other_model` column. -pub const CBAND_SI_MODELS: [&[u16]; 7] = [ - &CBAND_SI_MODEL_0, - &CBAND_SI_MODEL_1, - &CBAND_SI_MODEL_2, - &CBAND_SI_MODEL_3, - &CBAND_SI_MODEL_4, - &CBAND_SI_MODEL_5, - &CBAND_SI_MODEL_6, -]; - -/// The Table 4.A.37–4.A.43 scalefactor models, indexed by -/// `scf_model` (Table 4.A.32; model 0 has no table). -pub const SCF_MODELS: [Option<&[u16]>; 8] = [ - None, - Some(&SCF_MODEL_1), - Some(&SCF_MODEL_2), - Some(&SCF_MODEL_3), - Some(&SCF_MODEL_4), - Some(&SCF_MODEL_5), - Some(&SCF_MODEL_6), - Some(&SCF_MODEL_7), -]; - -/// Table 4.A.34 — position of the probability value inside a -/// zero-higher-bits row, from the neighbour context. -/// -/// * `a = i % 4` — the line's offset in its aligned 4-line group. -/// * `b`, `c`, `d` — the current-plane sliced bits already decoded -/// for lines `i-3`, `i-2`, `i-1` (only the in-group ones apply: -/// `d` from `a >= 1`, `c` from `a >= 2`, `b` from `a >= 3`). -/// * `e`, `f`, `g`, `h` — whether the higher bits of lines -/// `i-a+3`, `i-a+2`, `i-a+1`, `i-a` are non-zero. Flags of lines -/// at or after `i` are 0 by construction (their higher bits for -/// the *current* plane are what is being decoded), which is -/// exactly how the table's absent cells are shaped. -/// -/// Returns the row position `0..=64`. -pub fn context_position(a: usize, prev_bits: [u8; 3], group_higher_nonzero: [u8; 4]) -> usize { - debug_assert!(a < 4); - // `prev_bits = [b, c, d]` — the current-plane bits of lines - // i-3, i-2, i-1; `group_higher_nonzero = [h, g, f, e]` — the - // higher-bits-non-zero flags of the aligned group lines - // i-a .. i-a+3 in line order. - let [b, c, d] = prev_bits; - let [h, g, f, e] = group_higher_nonzero; - // Column index within the printed table: (h, g, f, e) walked as - // a 4-bit number h·8 + g·4 + f·2 + e. - let col = - (usize::from(h) << 3) | (usize::from(g) << 2) | (usize::from(f) << 1) | usize::from(e); - match a { - 0 => { - // h refers to line i itself: always 0 here. 8 columns. - const ROW: [usize; 8] = [0, 15, 22, 29, 32, 39, 42, 45]; - ROW[col & 7] - } - 1 => { - // g refers to line i: 0. Columns h∈{0,1} × f,e. - const ROW_D0: [[usize; 4]; 2] = [[1, 16, 23, 30], [46, 53, 56, 59]]; - const ROW_D1: [[usize; 4]; 2] = [[2, 17, 24, 31], [46, 53, 56, 59]]; - let h_i = usize::from(h); - let fe = col & 3; - if d == 0 { - ROW_D0[h_i][fe] - } else { - ROW_D1[h_i][fe] - } - } - 2 => { - // f refers to line i: 0. Columns (h, g) × e. - // Row selected by (c, d). - const ROWS: [[usize; 8]; 4] = [ - // (h,g,e) order: 000,001,010,011,100,101,110,111 - [3, 18, 33, 40, 47, 54, 60, 63], // c=0, d=0 - [4, 19, 33, 40, 48, 55, 60, 63], // c=0, d=1 - [5, 20, 34, 41, 47, 54, 60, 63], // c=1, d=0 - [6, 21, 34, 41, 48, 55, 60, 63], // c=1, d=1 - ]; - let row = ((c as usize) << 1) | d as usize; - let hge = ((usize::from(h)) << 2) | ((usize::from(g)) << 1) | usize::from(e); - ROWS[row][hge] - } - _ => { - // a == 3: e refers to line i: 0. Columns (h, g, f). - // Row selected by (b, c, d). - const ROWS: [[usize; 8]; 8] = [ - [7, 25, 35, 43, 49, 57, 61, 64], // 000 - [8, 25, 36, 43, 50, 57, 62, 64], // 001 - [9, 26, 35, 43, 51, 58, 61, 64], // 010 - [10, 26, 36, 43, 52, 58, 62, 64], // 011 - [11, 27, 37, 44, 49, 57, 61, 64], // 100 - [12, 27, 38, 44, 50, 57, 62, 64], // 101 - [13, 28, 37, 44, 51, 58, 61, 64], // 110 - [14, 28, 38, 44, 52, 58, 62, 64], // 111 - ]; - let row = ((b as usize) << 2) | ((c as usize) << 1) | d as usize; - let hgf = ((usize::from(h)) << 2) | ((usize::from(g)) << 1) | usize::from(f); - ROWS[row][hgf] - } - } -} - -/// One probability table's explicit rows (base tables 1..=10; the -/// aliased tables 11..=22 resolve onto 9 / 10 in [`spectral_p0`]). -struct ProbTable { - /// The MSB-plane row (15 positions — higher-bit flags are all - /// zero at the MSB by construction). - msb: &'static [u16], - /// Zero-higher-bits rows for `rel = 1..` (65 positions each). - /// Tables 9 / 10 leave this empty and alias tables 7 / 8. - zero: &'static [&'static [u16]], - /// Non-zero-higher-bits rows for `rel = 1..` (sizes - /// `min(2^rel - 1, 16)`). - nz: &'static [&'static [u16]], -} - -const PROB_TABLES: [ProbTable; 10] = [ - ProbTable { - msb: &PROB_T1_MSB, - zero: &[], - nz: &[], - }, - ProbTable { - msb: &PROB_T2_MSB, - zero: &[], - nz: &[], - }, - ProbTable { - msb: &PROB_T3_MSB, - zero: &[&PROB_T3_ZERO_1], - nz: &[&PROB_T3_NZ_1], - }, - ProbTable { - msb: &PROB_T4_MSB, - zero: &[&PROB_T4_ZERO_1], - nz: &[&PROB_T4_NZ_1], - }, - ProbTable { - msb: &PROB_T5_MSB, - zero: &[&PROB_T5_ZERO_1, &PROB_T5_ZERO_2], - nz: &[&PROB_T5_NZ_1, &PROB_T5_NZ_2], - }, - ProbTable { - msb: &PROB_T6_MSB, - zero: &[&PROB_T6_ZERO_1, &PROB_T6_ZERO_2], - nz: &[&PROB_T6_NZ_1, &PROB_T6_NZ_2], - }, - ProbTable { - msb: &PROB_T7_MSB, - zero: &[&PROB_T7_ZERO_1, &PROB_T7_ZERO_2, &PROB_T7_ZERO_3], - nz: &[&PROB_T7_NZ_1, &PROB_T7_NZ_2, &PROB_T7_NZ_3], - }, - ProbTable { - msb: &PROB_T8_MSB, - zero: &[&PROB_T8_ZERO_1, &PROB_T8_ZERO_2, &PROB_T8_ZERO_3], - nz: &[&PROB_T8_NZ_1, &PROB_T8_NZ_2, &PROB_T8_NZ_3], - }, - ProbTable { - msb: &PROB_T9_MSB, - zero: &[], - nz: &[&PROB_T9_NZ_1, &PROB_T9_NZ_2, &PROB_T9_NZ_3, &PROB_T9_NZ_4], - }, - ProbTable { - msb: &PROB_T10_MSB, - zero: &[], - nz: &[ - &PROB_T10_NZ_1, - &PROB_T10_NZ_2, - &PROB_T10_NZ_3, - &PROB_T10_NZ_4, - ], - }, -]; - -/// Resolve a `cband_si` (1..=22) to `(base probability table 1..=10, -/// MSB plane)` per Table 4.A.33 and the Table 4.A.66–4.A.77 alias -/// notes ("Same as BSAC probability Table 9/10, but MSB plane = M"). -fn resolve_table(cband_si: u8) -> (usize, u8) { - debug_assert!((1..=22).contains(&cband_si)); - let plane = CBAND_SI_MSB_PLANE[cband_si as usize]; - // 2009 alias scheme. NOTE: the 2001 edition prints a different - // scheme (tables 11..=22 all onto table 10, and the sub-MSB - // zero rows of 9/10 onto table 8) — both readings were tested - // against the 14496-26 conformance streams and neither matches - // the deployed encoder's selection; see the crate README's - // BSAC divergence note. - let base = match cband_si { - 1..=10 => cband_si, - 11 | 13 => 9, - 12 | 14 => 10, - _ => 9, // tables 15..=22 alias table 9 at planes 8..=15 - }; - (base as usize, plane) -} - -/// The spectral bit-slice `p0` — the probability of the "0" symbol -/// for one sliced bit, per §4.6.4.2.3. -/// -/// * `cband_si` — the coding band's side info (1..=22; 0 never -/// decodes spectral bits). -/// * `snf` — the significance (bit plane, 1-based) being decoded. -/// * `hbv` — the line's own decoded higher bits (the -/// `higher_bit_vector`, bits above `snf` as an integer). -/// * `pos` — the Table 4.A.34 context position (only consulted when -/// `hbv == 0`). -pub fn spectral_p0(cband_si: u8, snf: u8, hbv: u32, pos: usize) -> u16 { - let (base, plane) = resolve_table(cband_si); - let t = &PROB_TABLES[base - 1]; - debug_assert!(snf >= 1 && snf <= plane); - let rel = usize::from(plane - snf); - if hbv != 0 { - // Non-zero decoded higher bits: index by min(hbv, 16) - 1. - let rows = if t.nz.is_empty() { &[] } else { t.nz }; - let row = rows[rel.min(rows.len()) - 1]; - let idx = (hbv.min(16) as usize - 1).min(row.len() - 1); - row[idx] - } else if rel == 0 { - t.msb[pos.min(t.msb.len() - 1)] - } else { - // Zero rows: tables 9 / 10 (and the 11..=22 aliases on - // them) borrow tables 7 / 8 for the sub-MSB rows. - let (rows, cap) = if t.zero.is_empty() { - let borrowed = if base == 9 { - &PROB_TABLES[6] - } else { - &PROB_TABLES[7] - }; - (borrowed.zero, borrowed.zero.len()) - } else { - (t.zero, t.zero.len()) - }; - rows[rel.min(cap) - 1][pos] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn cumulative_frequency_models_are_well_formed() { - let mut all: Vec<&[u16]> = vec![ - &MS_USED_MODEL, - &STEREO_INFO_MODEL, - &NOISE_FLAG_MODEL, - &NOISE_MODE_MODEL, - &CBAND_SI_MODEL_CBAND0, - ]; - all.extend(CBAND_SI_MODELS.iter().copied()); - all.extend(SCF_MODELS.iter().flatten().copied()); - for model in all { - assert!(model[0] < 0x4000, "cum freq must sit under 2^14"); - assert!( - model.windows(2).all(|w| w[0] > w[1]), - "cum freqs strictly decreasing" - ); - assert_eq!(*model.last().unwrap(), 0, "last cum freq is 0"); - } - } - - #[test] - fn model_sizes_cover_their_largest_symbols() { - for (i, p) in CBAND_SI_TYPES.iter().enumerate() { - assert!( - CBAND_SI_MODELS[p.other_model as usize].len() > p.largest_other as usize, - "type {i}: other model too small" - ); - assert!( - CBAND_SI_MODEL_CBAND0.len() > p.largest_cband0 as usize, - "type {i}: cband0 model too small" - ); - } - for (m, largest) in SCF_MODEL_LARGEST.iter().enumerate() { - if let Some(model) = SCF_MODELS[m] { - assert_eq!( - model.len(), - usize::from(*largest) + 1, - "scf model {m} size vs Table 4.A.32 largest" - ); - } - } - } - - /// Every Table 4.A.34 position 0..=64 is reachable, and every - /// reachable context yields a position <= 64. - #[test] - fn context_positions_cover_the_table() { - let mut seen = [false; 65]; - for a in 0..4usize { - for bits in 0..8u8 { - let (b, c, d) = ((bits >> 2) & 1, (bits >> 1) & 1, bits & 1); - // Only the in-group predecessors apply; zero the rest - // like the decoder does. - let (b, c, d) = match a { - 0 => (0, 0, 0), - 1 => (0, 0, d), - 2 => (0, c, d), - _ => (b, c, d), - }; - for flags in 0..16u8 { - let (h, g, f, _e) = ( - (flags >> 3) & 1, - (flags >> 2) & 1, - (flags >> 1) & 1, - flags & 1, - ); - // Flags at or after line i are structurally 0 - // (the last group line's flag e never survives - // the mask below). - let (h, g, f, e) = match a { - 0 => (0, 0, 0, 0), - 1 => (h, 0, 0, 0), - 2 => (h, g, 0, 0), - _ => (h, g, f, 0), - }; - let pos = context_position(a, [b, c, d], [h, g, f, e]); - assert!(pos <= 64); - seen[pos] = true; - } - } - } - // The e..h flags of *later* in-group lines can be non-zero - // too (their hbv from earlier planes) — walk the full flag - // space for coverage. - for a in 0..4usize { - for bits in 0..8u8 { - let (b, c, d) = ((bits >> 2) & 1, (bits >> 1) & 1, bits & 1); - for flags in 0..16u8 { - let (h, g, f, e) = ( - (flags >> 3) & 1, - (flags >> 2) & 1, - (flags >> 1) & 1, - flags & 1, - ); - let pos = context_position(a, [b, c, d], [h, g, f, e]); - assert!(pos <= 64); - seen[pos] = true; - } - } - } - assert!(seen.iter().all(|&s| s), "all 65 positions reachable"); - } - - /// [`spectral_p0`] resolves every `(cband_si, snf, hbv, pos)` - /// combination without panicking, always inside (0, 2^14). - #[test] - fn spectral_p0_covers_every_context() { - for cband_si in 1u8..=22 { - let plane = CBAND_SI_MSB_PLANE[cband_si as usize]; - for snf in 1..=plane { - let rel = plane - snf; - let max_hbv: u32 = if rel >= 31 { u32::MAX } else { (1 << rel) - 1 }; - for hbv in 0..=max_hbv.min(40) { - let poss: &[usize] = if rel == 0 { &[0, 7, 14] } else { &[0, 32, 64] }; - for &pos in poss { - let p0 = spectral_p0(cband_si, snf, hbv, pos); - assert!( - p0 > 0 && p0 < 0x4000, - "cband_si {cband_si} snf {snf} hbv {hbv} pos {pos}: {p0:#x}" - ); - } - } - } - } - } - - #[test] - fn p0_clamps_are_consistent() { - for len in 1..14usize { - assert!(MIN_P0[len] <= MAX_P0[len]); - } - assert_eq!(clamp_p0(0x3fff, 1), 0x2000); - assert_eq!(clamp_p0(0x0001, 1), 0x2000); - assert_eq!(clamp_p0(0x1234, 14), 0x1234); - } - - /// Spot-check transcription anchors against the printed spec - /// listings. - #[test] - fn transcription_anchors() { - assert_eq!(CBAND_SI_MODEL_0[0], 0x3ef6); // Table 4.A.44 - assert_eq!(CBAND_SI_MODEL_6[0], 0x31af); // Table 4.A.50 - assert_eq!(CBAND_SI_MODEL_CBAND0[0], 0x3ff8); // Table 4.A.51 - assert_eq!(MS_USED_MODEL[0], 0x2ccd); // Table 4.A.52 - assert_eq!(STEREO_INFO_MODEL, [0x3666, 0x1000, 0x0666, 0]); // 4.A.53 - assert_eq!(NOISE_FLAG_MODEL[0], 0x2000); // Table 4.A.54 - assert_eq!(SCF_MODEL_7[0], 0x3b5e); // Table 4.A.43 - assert_eq!(SCF_MODEL_7[63], 0); - assert_eq!(PROB_T1_MSB[0], 0x3900); // Table 4.A.56 - assert_eq!(PROB_T1_MSB[14], 0x2c00); - assert_eq!(PROB_T7_MSB[0], 0x3d00); // Table 4.A.62 - assert_eq!(PROB_T7_NZ_1[0], 0x2f00); // the 2F00 uppercase cell - assert_eq!(PROB_T9_NZ_4[15], 0x2000); // Table 4.A.64 last cell - assert_eq!(PROB_T10_NZ_4[15], 0x2200); // Table 4.A.65 last cell - } -} diff --git a/crates/vendor/oxideav-aac/src/cce.rs b/crates/vendor/oxideav-aac/src/cce.rs deleted file mode 100644 index 5d766554..00000000 --- a/crates/vendor/oxideav-aac/src/cce.rs +++ /dev/null @@ -1,1289 +0,0 @@ -//! `coupling_channel_element()` — ISO/IEC 14496-3 §4.6.8.3 / Table 4.8. -//! -//! The coupling channel element (CCE, `id_syn_ele == 0b010`) carries an -//! embedded `single_channel_element()` whose decoded spectrum is scaled -//! by a list of *gain elements* and added onto one or more target -//! channels (SCE / CPE) signalled by the coupling header. This module -//! owns the **coupling header + gain-list** half of Table 4.8: -//! -//! ```text -//! coupling_channel_element() { -//! element_instance_tag; 4 uimsbf // consumed by the walker -//! ind_sw_cce_flag; 1 uimsbf -//! num_coupled_elements; 3 uimsbf -//! num_gain_element_lists = 0; -//! for (c = 0; c < num_coupled_elements+1; c++) { -//! num_gain_element_lists++; -//! cc_target_is_cpe[c]; 1 uimsbf -//! cc_target_tag_select[c]; 4 uimsbf -//! if (cc_target_is_cpe[c]) { -//! cc_l[c]; 1 uimsbf -//! cc_r[c]; 1 uimsbf -//! if (cc_l[c] && cc_r[c]) num_gain_element_lists++; -//! } -//! } -//! cc_domain; 1 uimsbf -//! gain_element_sign; 1 uimsbf -//! gain_element_scale; 2 uimsbf -//! individual_channel_stream(0,0); // the embedded SCE body -//! for (c=1; c> 1`), per the ISO/IEC 14496-3:2001 / -//! 13818-7:2004 `couple_channel()` text as ruled in -//! `docs/audio/aac/cce-gain-sign-split.md` §3. A `common_gain_element` -//! is **never** sign-split (`cc_sign = 1` forced in that branch — so an -//! independently switched CCE, which must use common gains only, always -//! couples in phase). The first coupled target (`list_index == 0`) is -//! not transmitted: its gains are all `0`, i.e. the CCE is added in its -//! natural scaling (`cc_gain == 1`). -//! -//! ## Provenance -//! -//! Table 4.8 syntax, the §4.6.8.3.3 `decode_coupling_channel()` / -//! `couple_channel()` pseudocode, the Table 4.153 shared-gain-list table, -//! and the Table 4.154 `cc_scale_table` are all from ISO/IEC 14496-3 -//! staged under `docs/audio/aac/`. The gain elements reuse the -//! §4.A.1 scalefactor Huffman codebook (codebook 12) via -//! [`crate::scale_factor_data::hcod_sf_decode`] / -//! [`crate::scale_factor_data::hcod_sf_encode`], exactly as the spec -//! directs ("gain_element values are differentially encoded using the -//! Huffman table for scalefactors"). - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::ics_body::IcsBody; -use crate::ics_info::IcsInfo; -use crate::scale_factor_data::{hcod_sf_decode, hcod_sf_encode}; -use crate::section_data::ZERO_HCB; -use crate::spectral_data::SpectralData; -use crate::{Error, Result}; - -/// Field width of `ind_sw_cce_flag` (Table 4.8). -pub const IND_SW_CCE_FLAG_BITS: u32 = 1; -/// Field width of `num_coupled_elements` (Table 4.8). -pub const NUM_COUPLED_ELEMENTS_BITS: u32 = 3; -/// Field width of `cc_target_tag_select` (Table 4.8). -pub const CC_TARGET_TAG_SELECT_BITS: u32 = 4; -/// Field width of `gain_element_scale` (Table 4.8). -pub const GAIN_ELEMENT_SCALE_BITS: u32 = 2; - -/// Table 4.154 — the four `cc_scale` amplitude resolutions selected by -/// the 2-bit `gain_element_scale`. `cc_scale = 2^(1/8 · 2^scale)`: -/// `2^(1/8)`, `2^(1/4)`, `2^(1/2)`, `2^1` (step sizes 0.75 / 1.5 / 3.0 / -/// 6.0 dB). -pub const CC_SCALE_TABLE: [f64; 4] = [ - 1.090_507_732_665_257_7, // 2^(1/8) - 1.189_207_115_002_721, // 2^(1/4) - std::f64::consts::SQRT_2, // 2^(1/2) - 2.0, // 2^1 -]; - -/// One coupled target of a CCE (Table 4.8 inner loop, one `c`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CoupledTarget { - /// `cc_target_is_cpe[c]` — the coupled target is a CPE (`true`) or a - /// SCE (`false`). - pub is_cpe: bool, - /// `cc_target_tag_select[c]` — the `element_instance_tag` of the - /// coupled SCE / CPE. - pub tag_select: u8, - /// `cc_l[c]` — a gain list applies to the CPE's left channel. Always - /// `false` for a SCE target. - pub cc_l: bool, - /// `cc_r[c]` — a gain list applies to the CPE's right channel. Always - /// `false` for a SCE target. - pub cc_r: bool, -} - -impl CoupledTarget { - /// The number of `num_gain_element_lists` slots this target - /// contributes (Table 4.8): one per target, plus a *second* slot for - /// a CPE target whose `cc_l && cc_r` (the shared-vs-split gain-list - /// distinction, Table 4.153). - fn gain_list_increment(&self) -> u32 { - if self.is_cpe && self.cc_l && self.cc_r { - 2 - } else { - 1 - } - } -} - -/// Parsed `coupling_channel_element()` header (everything before the -/// embedded `individual_channel_stream(0,0)`). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CouplingHeader { - /// `ind_sw_cce_flag` — independently switched (`true`) vs dependently - /// switched (`false`). An independently switched CCE must only use - /// `common_gain_element` and is decoded to the time domain before - /// coupling (§4.6.8.3.3); a dependently switched CCE shares the - /// target window state and couples in the frequency domain. - pub ind_sw_cce_flag: bool, - /// `num_coupled_elements` — the number of coupled targets is - /// `num_coupled_elements + 1` (minimum value `0` ⇒ one target). - pub num_coupled_elements: u8, - /// The `num_coupled_elements + 1` coupled targets. - pub targets: Vec, - /// `cc_domain` — coupling performed before (`false`) or after - /// (`true`) TNS decoding of the coupled target channels. - pub cc_domain: bool, - /// `gain_element_sign` — the transmitted gain elements carry - /// in-phase / out-of-phase coupling information (`true`) or not - /// (`false`). - pub gain_element_sign: bool, - /// `gain_element_scale` — 2-bit index into [`CC_SCALE_TABLE`]. - pub gain_element_scale: u8, - /// `num_gain_element_lists` derived by the Table 4.8 loop. This is - /// the number of transmitted gain lists; the trailing gain loop runs - /// over `1 ..= num_gain_element_lists - 1` (list 0 is the implicit - /// natural-scaling target). - pub num_gain_element_lists: u32, -} - -impl CouplingHeader { - /// Parse the Table 4.8 coupling header. `reader` is positioned at - /// `ind_sw_cce_flag` (i.e. the caller — typically the - /// [`crate::raw_data_block`] walker — already consumed the 4-bit - /// `element_instance_tag`). - pub fn parse(reader: &mut BitReader<'_>) -> Result { - let ind_sw_cce_flag = read_bit(reader)?; - let num_coupled_elements = read_u8(reader, NUM_COUPLED_ELEMENTS_BITS)?; - - let mut num_gain_element_lists: u32 = 0; - let mut targets = Vec::with_capacity(usize::from(num_coupled_elements) + 1); - for _c in 0..(u32::from(num_coupled_elements) + 1) { - num_gain_element_lists += 1; - let is_cpe = read_bit(reader)?; - let tag_select = read_u8(reader, CC_TARGET_TAG_SELECT_BITS)?; - let (cc_l, cc_r) = if is_cpe { - let cc_l = read_bit(reader)?; - let cc_r = read_bit(reader)?; - if cc_l && cc_r { - num_gain_element_lists += 1; - } - (cc_l, cc_r) - } else { - (false, false) - }; - targets.push(CoupledTarget { - is_cpe, - tag_select, - cc_l, - cc_r, - }); - } - - let cc_domain = read_bit(reader)?; - let gain_element_sign = read_bit(reader)?; - let gain_element_scale = read_u8(reader, GAIN_ELEMENT_SCALE_BITS)?; - - Ok(CouplingHeader { - ind_sw_cce_flag, - num_coupled_elements, - targets, - cc_domain, - gain_element_sign, - gain_element_scale, - num_gain_element_lists, - }) - } - - /// Write the Table 4.8 coupling header (mirror of [`Self::parse`]), - /// **not** including the leading `element_instance_tag` (the caller / - /// frame assembler owns that, exactly as the walker consumes it on - /// the parse side). - /// - /// Rejects an inconsistent record: a `targets` count that disagrees - /// with `num_coupled_elements + 1`, a `gain_element_scale > 3`, or a - /// SCE target carrying a `cc_l` / `cc_r` flag. - pub fn write(&self, writer: &mut BitWriter) -> Result<()> { - if self.targets.len() != usize::from(self.num_coupled_elements) + 1 { - return Err(Error::CceInvalid); - } - if self.gain_element_scale > 3 { - return Err(Error::CceInvalid); - } - let mut derived_lists: u32 = 0; - for t in &self.targets { - if !t.is_cpe && (t.cc_l || t.cc_r) { - return Err(Error::CceInvalid); - } - derived_lists += t.gain_list_increment(); - } - if derived_lists != self.num_gain_element_lists { - return Err(Error::CceInvalid); - } - - writer.write_bit(self.ind_sw_cce_flag); - writer.write_u32( - u32::from(self.num_coupled_elements), - NUM_COUPLED_ELEMENTS_BITS, - ); - for t in &self.targets { - writer.write_bit(t.is_cpe); - writer.write_u32(u32::from(t.tag_select), CC_TARGET_TAG_SELECT_BITS); - if t.is_cpe { - writer.write_bit(t.cc_l); - writer.write_bit(t.cc_r); - } - } - writer.write_bit(self.cc_domain); - writer.write_bit(self.gain_element_sign); - writer.write_u32(u32::from(self.gain_element_scale), GAIN_ELEMENT_SCALE_BITS); - Ok(()) - } -} - -/// One decoded per-band coupling gain of a `dpcm_gain_element` list — -/// the §4.6.8.3.3 (2001 / 13818-7:2004) `couple_channel()` gain-decode -/// output for one `(g, sfb)`: the `cc_sign` out-of-phase flag split off -/// the transmitted DPCM delta, and the accumulated `gain_element` -/// exponent (see `docs/audio/aac/cce-gain-sign-split.md` §3). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct DpcmGain { - /// `cc_sign == −1` (out-of-phase coupling) for this band. Set from - /// the delta LSB (`dpcm & 1`) when `gain_element_sign == 1`; always - /// `false` when the sign bit is clear. - pub negative: bool, - /// The accumulated `gain_element[g][sfb]` exponent — - /// `a += dpcm >> 1` under `gain_element_sign == 1`, `a += dpcm` - /// otherwise. - pub gain: i32, -} - -/// The decoded gain list for one coupled target (Table 4.8 trailing -/// loop, one `c`). Either a single `common_gain_element` applied to -/// every band, or a per-`(g, sfb)` `dpcm_gain_element` list decoded by -/// the §4.6.8.3.3 forward running sum. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum GainList { - /// `cge == 1`: one `common_gain_element` reused over every window - /// group and scalefactor band (§4.6.8.3.3). Never sign-split — the - /// 2001 / 13818-7 text forces `cc_sign = 1` in this branch. - Common(i32), - /// `cge == 0`: the per-band decoded gain grid, indexed - /// `gains[g][sfb]`. Only the non-`ZERO_HCB` bands are transmitted; - /// `ZERO_HCB` bands hold the running accumulator value with an - /// in-phase sign (no delta is read there). - Dpcm(Vec>), -} - -/// The whole trailing gain-list block of a CCE (Table 4.8), one -/// [`GainList`] per transmitted list (`1 ..= num_gain_element_lists`). -/// -/// The implicit list 0 (natural scaling) is **not** stored — the -/// [`Self::cc_gain`] helper returns `1.0` for it. -#[derive(Debug, Clone, PartialEq)] -pub struct CouplingGains { - /// The `gain_element_scale`-selected `cc_scale` from Table 4.154. - pub cc_scale: f64, - /// `gain_element_sign` from the coupling header (informational — - /// the in-phase / out-of-phase split is resolved per band at parse - /// time into [`DpcmGain::negative`], per the - /// `docs/audio/aac/cce-gain-sign-split.md` §3 ruling; the writer - /// keys off the [`CouplingHeader`] it is handed). - pub gain_element_sign: bool, - /// The transmitted gain lists, in `c = 1 ..= num_gain_element_lists` - /// order (`lists[0]` is the `c == 1` list). - pub lists: Vec, -} - -impl CouplingGains { - /// Parse the Table 4.8 trailing gain-list loop. `reader` is - /// positioned immediately after the embedded - /// `individual_channel_stream(0,0)`. - /// - /// * `header` — the already-parsed [`CouplingHeader`]. - /// * `num_window_groups` / `max_sfb` — from the embedded SCE's - /// `ics_info()`. - /// * `sfb_cb` — the embedded SCE's per-`(g, sfb)` section codebooks - /// ([`crate::section_data::SectionData::sfb_cb`]); the §4.6.8.3.3 - /// `Note` requires the CCE's *own* codebooks here, not the coupled - /// target's. - pub fn parse( - reader: &mut BitReader<'_>, - header: &CouplingHeader, - num_window_groups: usize, - max_sfb: usize, - sfb_cb: &[Vec], - ) -> Result { - let cc_scale = CC_SCALE_TABLE[usize::from(header.gain_element_scale & 0x3)]; - let mut lists = Vec::new(); - for _c in 1..header.num_gain_element_lists { - let cge = if header.ind_sw_cce_flag { - true - } else { - read_bit(reader)? - }; - if cge { - let common = i32::from(hcod_sf_decode(reader)?); - lists.push(GainList::Common(common)); - } else { - // An independently switched CCE must only use the common - // gain element (§4.6.8.3.3); a per-band list here is - // ill-formed. `cge` is already forced true above for that - // case, so reaching the else branch with ind_sw set is - // impossible, but guard against a hand-built record. - if header.ind_sw_cce_flag { - return Err(Error::CceInvalid); - } - // §4.6.8.3.3 (2001 / 13818-7:2004) gain-decode loop — - // under `gain_element_sign` the out-of-phase flag is - // split off **each transmitted delta** (`cc_sign = - // 1 − 2·(dpcm & 1)`) and the accumulator is fed with - // the remaining magnitude (`a += dpcm >> 1`, arithmetic - // shift); with the sign bit clear the delta accumulates - // whole. Ruled in - // `docs/audio/aac/cce-gain-sign-split.md` §3 (the - // 14496-3:2009 fragment that splits the *accumulated* - // value is an editorial defect of that edition). - let mut acc: i32 = 0; - let mut grid = vec![vec![DpcmGain::default(); max_sfb]; num_window_groups]; - for (g, row) in grid.iter_mut().enumerate() { - let cb_row = sfb_cb.get(g).ok_or(Error::CceInvalid)?; - for (sfb, cell) in row.iter_mut().enumerate() { - let cb = *cb_row.get(sfb).ok_or(Error::CceInvalid)?; - if cb != ZERO_HCB { - let dpcm = i32::from(hcod_sf_decode(reader)?); - if header.gain_element_sign { - acc += dpcm >> 1; - *cell = DpcmGain { - negative: (dpcm & 1) != 0, - gain: acc, - }; - } else { - acc += dpcm; - *cell = DpcmGain { - negative: false, - gain: acc, - }; - } - } else { - // ZERO_HCB band carries the running value but - // contributes no coupling (cc_gain unused). - *cell = DpcmGain { - negative: false, - gain: acc, - }; - } - } - } - lists.push(GainList::Dpcm(grid)); - } - } - Ok(CouplingGains { - cc_scale, - gain_element_sign: header.gain_element_sign, - lists, - }) - } - - /// Write the trailing gain-list loop (mirror of [`Self::parse`]). - /// `sfb_cb` must be the same embedded-SCE codebook grid the parse - /// consumed so the `ZERO_HCB` bands are skipped identically. - pub fn write( - &self, - writer: &mut BitWriter, - header: &CouplingHeader, - sfb_cb: &[Vec], - ) -> Result<()> { - if self.lists.len() + 1 != header.num_gain_element_lists as usize { - return Err(Error::CceInvalid); - } - for list in &self.lists { - match list { - GainList::Common(common) => { - if !header.ind_sw_cce_flag { - // common_gain_element_present[c] = 1 - writer.write_bit(true); - } - let dpcm = i8::try_from(*common).map_err(|_| Error::CceInvalid)?; - let (len, cw) = hcod_sf_encode(dpcm)?; - writer.write_u32(cw, u32::from(len)); - } - GainList::Dpcm(grid) => { - if header.ind_sw_cce_flag { - return Err(Error::CceInvalid); - } - // common_gain_element_present[c] = 0 - writer.write_bit(false); - // Exact inverse of the §4.6.8.3.3 gain-decode loop: - // under `gain_element_sign` each delta packs the - // out-of-phase flag into its LSB - // (`dpcm = ((gain − prev) << 1) | negative`, which - // `dpcm >> 1` / `dpcm & 1` recover for every signed - // delta); with the sign bit clear the delta is the - // plain gain difference and an out-of-phase band is - // unrepresentable (rejected). - let mut prev: i32 = 0; - for (g, row) in grid.iter().enumerate() { - let cb_row = sfb_cb.get(g).ok_or(Error::CceInvalid)?; - for (sfb, cell) in row.iter().enumerate() { - let cb = *cb_row.get(sfb).ok_or(Error::CceInvalid)?; - if cb != ZERO_HCB { - let delta = cell.gain - prev; - let dpcm = if header.gain_element_sign { - (delta << 1) | i32::from(cell.negative) - } else { - if cell.negative { - return Err(Error::CceInvalid); - } - delta - }; - let dpcm = i8::try_from(dpcm).map_err(|_| Error::CceInvalid)?; - let (len, cw) = hcod_sf_encode(dpcm)?; - writer.write_u32(cw, u32::from(len)); - prev = cell.gain; - } - } - } - } - } - } - Ok(()) - } - - /// The §4.6.8.3.3 `couple_channel()` per-band gain factor for a given - /// transmitted gain list and `(g, sfb)`. - /// - /// `list_index` is the §4.6.8.3.3 `couple_channel()` `gain_list_index` - /// (`0` = the implicit natural-scaling target → `cc_gain == 1.0`; - /// `1 ..= num_gain_element_lists - 1` index [`Self::lists`]). - /// - /// Returns `cc_gain = cc_sign · cc_scale^(−gain_element)`: - /// * for a [`GainList::Dpcm`] band, `cc_sign` and `gain_element` - /// are the per-band values the parse loop split off the DPCM - /// deltas (`docs/audio/aac/cce-gain-sign-split.md` §3 — the - /// 2001 / 13818-7:2004 `couple_channel()` gain decode); - /// * for a [`GainList::Common`] list, `cc_sign = 1` always — the - /// ruled text never sign-splits a `common_gain_element`, so an - /// independently switched CCE (common gains only) couples in - /// phase regardless of `gain_element_sign`. - /// - /// The **negated** exponent is the conformance-settled reading of - /// the §4.6.8.3.3 `cc_scale^gain_element` expression. All three - /// staged editions print a positive exponent, but the ISO/IEC - /// 14496-26 `am05_*` vectors (the only normative CCE bitstreams; - /// every AU carries `common_gain_element = −1` lists) reconstruct - /// their reference waveforms only with `cc_scale^(−ge)` — with the - /// printed positive exponent every coupled target channel misses by - /// ~1e-1 err/sig, with the negated form all six channels land at - /// ~1e-4. This resolves the question - /// `docs/audio/aac/cce-gain-sign-split.md` §4 left open (a - /// black-box validator had measured the negated exponent; the - /// conformance corpus now confirms it as the normative wire - /// convention). The §3 sign-split ruling is orthogonal (the - /// corpus's `gain_element_sign` is always 0) and is implemented in - /// the parse loop. - pub fn cc_gain(&self, list_index: usize, g: usize, sfb: usize) -> Result { - if list_index == 0 { - // The first coupled target's gains are not transmitted; the - // CCE adds in its natural scaling (gain = 0 ⇒ cc_gain = 1). - return Ok(1.0); - } - let list = self.lists.get(list_index - 1).ok_or(Error::CceInvalid)?; - let (cc_sign, gain) = match list { - GainList::Common(common) => (1.0, *common), - GainList::Dpcm(grid) => { - let cell = grid - .get(g) - .and_then(|row| row.get(sfb)) - .ok_or(Error::CceInvalid)?; - (if cell.negative { -1.0 } else { 1.0 }, cell.gain) - } - }; - Ok(cc_sign * self.cc_scale.powi(-gain)) - } - - /// §4.6.8.3.3 `couple_channel(source_spectrum, dest_spectrum, - /// gain_list_index)` — scale the CCE's embedded-SCE spectrum by the - /// `gain_list_index` gain list and **add** it onto one target - /// channel's window-major spectrum in place. - /// - /// This is the per-band scale-and-add the spec pseudocode defines: - /// - /// ```text - /// for (g = 0; g < num_window_groups; g++) - /// for (b = 0; b < window_group_length[g]; b++) - /// for (sfb = 0; sfb < max_sfb; sfb++) - /// if (sfb_cb[g][sfb] != ZERO_HCB) - /// for (i = swb_offset[sfb]; i < swb_offset[sfb+1]; i++) - /// dest[g][b][sfb][i] += cc_gain(idx,g,sfb) * source[g][b][sfb][i]; - /// ``` - /// - /// `cc_gain` per band is [`Self::cc_gain`] (`cc_sign · cc_scale^(−gain)`); - /// the implicit list 0 (`list_index == 0`) couples in natural scaling - /// (`cc_gain == 1`) onto every non-`ZERO_HCB` band. - /// - /// * `source` / `dest` — window-major spectra - /// (`num_windows × window_len`), identical geometry. `source` is the - /// decoded embedded-SCE spectrum; `dest` is the addressed SCE / CPE - /// channel's spectrum at the §4.6.8.3.3 `cc_domain` stage (before or - /// after TNS). - /// * `list_index` — the §4.6.8.3.3 `couple_channel()` `gain_list_index` - /// the [`CouplingHeader`] walk assigns to this target. - /// * `sfb_cb` — the **embedded SCE's** per-`(g, sfb)` section - /// codebooks, per the §4.6.8.3.3 Note (`sfb_cb` is the CCE's own - /// codebook data, not the coupled target's). Drives the `ZERO_HCB` - /// band skip and, for a `GainList::Dpcm` list, the gain lookup. - /// * `window_group_length` / `max_sfb` — the embedded SCE's - /// `ics_info()` group geometry. - /// * `offsets` — the `swb_offset` table for the embedded SCE's window - /// length (`window_len + 1` entries; `offsets[sfb]..offsets[sfb+1]` - /// is band `sfb`). - /// - /// Returns [`Error::CceInvalid`] on any geometry mismatch (source / - /// dest length, group / band shapes) so a malformed coupling does not - /// corrupt the target out of bounds. - #[allow(clippy::too_many_arguments)] - pub fn couple_channel( - &self, - source: &[f64], - dest: &mut [f64], - list_index: usize, - sfb_cb: &[Vec], - window_group_length: &[u8], - max_sfb: usize, - offsets: &[u16], - ) -> Result<()> { - if source.len() != dest.len() { - return Err(Error::CceInvalid); - } - if offsets.is_empty() { - return Err(Error::CceInvalid); - } - // The last `swb_offset` entry is the window length (the first - // coefficient past the last band). The window-major spectrum is - // `num_windows * window_len` long. - let window_len = usize::from(*offsets.last().expect("non-empty checked above")); - if window_len == 0 || source.len() % window_len != 0 { - return Err(Error::CceInvalid); - } - let num_swb = offsets.len() - 1; - if max_sfb > num_swb { - return Err(Error::CceInvalid); - } - if sfb_cb.len() != window_group_length.len() { - return Err(Error::CceInvalid); - } - - let mut window_base = 0usize; - for (g, &wgl) in window_group_length.iter().enumerate() { - let cb_row = sfb_cb.get(g).ok_or(Error::CceInvalid)?; - if cb_row.len() < max_sfb { - return Err(Error::CceInvalid); - } - let wgl = usize::from(wgl); - for sfb in 0..max_sfb { - if cb_row[sfb] == ZERO_HCB { - // §4.6.8.3.3: ZERO_HCB bands carry no coupling - // contribution (and, for a DPCM list, were not - // transmitted — the accumulator simply skipped them). - continue; - } - let start = usize::from(offsets[sfb]); - let end = usize::from(offsets[sfb + 1]); - let cc_gain = self.cc_gain(list_index, g, sfb)?; - for b in 0..wgl { - let base = (window_base + b) - .checked_mul(window_len) - .ok_or(Error::CceInvalid)?; - let dst_end = base + end; - if dst_end > dest.len() { - return Err(Error::CceInvalid); - } - for i in start..end { - dest[base + i] += cc_gain * source[base + i]; - } - } - } - window_base += wgl; - } - Ok(()) - } -} - -/// A fully-parsed `coupling_channel_element()` (Table 4.8): the coupling -/// header, the embedded `individual_channel_stream(0,0)` (body + -/// spectrum), and the trailing gain lists. -/// -/// This is the single entry point a `raw_data_block()` walker uses to -/// **consume a whole CCE** from the bitstream (advancing the reader past -/// it). The decode loop can then either drop the element (a CCE -/// contributes no output channel of its own) or, once the cross-element -/// coupling is wired, scale [`Self::spectral`] by [`Self::gains`] and add -/// it onto the addressed target channels (§4.6.8.3.3 `couple_channel()`). -#[derive(Debug, Clone, PartialEq)] -pub struct CouplingChannelElement { - /// `element_instance_tag` (4 bits) — the CCE's own instance tag. - pub element_instance_tag: u8, - /// The Table 4.8 coupling header. - pub header: CouplingHeader, - /// The embedded `individual_channel_stream(0,0)` body (Table 4.50), - /// up to but not including `spectral_data()`. - pub body: IcsBody, - /// `ics_info()` of the embedded SCE (cloned out of [`Self::body`] for - /// convenience; the embedded body always reads its own `ics_info`). - pub ics_info: IcsInfo, - /// The embedded SCE's `spectral_data()` (Table 4.56). - pub spectral: SpectralData, - /// The Table 4.8 trailing gain lists. - pub gains: CouplingGains, -} - -impl CouplingChannelElement { - /// Parse a whole `coupling_channel_element()` (Table 4.8). `reader` - /// is positioned at `element_instance_tag` (i.e. immediately after - /// the `raw_data_block()` walker read the 3-bit `id_syn_ele == CCE`). - /// - /// * `aot` — the surrounding ASC's effective `audioObjectType`. - /// * `fs_index` — the `samplingFrequencyIndex`. - /// - /// Walks, in spec order: the 4-bit instance tag, the - /// [`CouplingHeader`], the embedded `individual_channel_stream(0,0)` - /// ([`IcsBody`] + [`SpectralData`]), and the [`CouplingGains`] - /// gain-list loop keyed off the embedded SCE's `sfb_cb`. - pub fn parse(reader: &mut BitReader<'_>, aot: u8, fs_index: u8) -> Result { - let element_instance_tag = read_u8(reader, 4)?; - Self::parse_after_tag(reader, element_instance_tag, aot, fs_index) - } - - /// Parse a `coupling_channel_element()` whose 4-bit - /// `element_instance_tag` was already consumed by the surrounding - /// `raw_data_block()` walker (which returns the tag in its - /// `ChannelElement` event). `reader` is positioned at - /// `ind_sw_cce_flag`; `element_instance_tag` is the walker-supplied - /// tag. Otherwise identical to [`Self::parse`]. - pub fn parse_after_tag( - reader: &mut BitReader<'_>, - element_instance_tag: u8, - aot: u8, - fs_index: u8, - ) -> Result { - Self::parse_after_tag_family( - reader, - crate::swb_offset::FrameFamily::Lc1024, - element_instance_tag, - aot, - fs_index, - ) - } - - /// [`Self::parse_after_tag`] under an explicit §4.5.1.1 - /// frame-length family (a 960-line `raw_data_block()` may carry a - /// CCE like any other; the ER payloads — including LD — have no - /// CCE at all per §4.5.2.4, so the LD families never reach here). - pub fn parse_after_tag_family( - reader: &mut BitReader<'_>, - family: crate::swb_offset::FrameFamily, - element_instance_tag: u8, - aot: u8, - fs_index: u8, - ) -> Result { - let header = CouplingHeader::parse(reader)?; - // Embedded individual_channel_stream(0,0): common_window = 0 and - // scale_flag = 0 per Table 4.8. - let body = IcsBody::parse_family(reader, family, aot, fs_index, false)?; - let ics_info = body.ics_info.clone().ok_or(Error::CceInvalid)?; - let spectral = SpectralData::parse(reader, &ics_info, &body.section_data, fs_index)?; - let gains = CouplingGains::parse( - reader, - &header, - usize::from(ics_info.num_window_groups), - usize::from(ics_info.max_sfb), - &body.section_data.sfb_cb, - )?; - Ok(CouplingChannelElement { - element_instance_tag, - header, - body, - ics_info, - spectral, - gains, - }) - } -} - -/// Helper: read a 1-bit flag, mapping underflow to [`Error::UnexpectedEnd`]. -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} - -/// Helper: read an `n`-bit `uimsbf` field as a `u8`. -fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { - Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Table 4.154 values are `2^(2^scale / 8)`. - #[test] - fn cc_scale_table_matches_spec_resolutions() { - for (scale, &v) in CC_SCALE_TABLE.iter().enumerate() { - let expected = 2f64.powf((1u32 << scale) as f64 / 8.0); - assert!( - (v - expected).abs() < 1e-12, - "cc_scale[{scale}] = {v} != {expected}" - ); - } - } - - /// A header with a single SCE target derives `num_gain_element_lists - /// == 1` (only the implicit list 0 — no trailing gains). - #[test] - fn single_sce_target_has_one_gain_list() { - // ind_sw=0, num_coupled=0, target0: is_cpe=0 tag=0, - // cc_domain=0 sign=0 scale=0 - let mut writer = BitWriter::new(); - writer.write_bit(false); // ind_sw_cce_flag - writer.write_u32(0, 3); // num_coupled_elements - writer.write_bit(false); // cc_target_is_cpe[0] - writer.write_u32(0, 4); // cc_target_tag_select[0] - writer.write_bit(false); // cc_domain - writer.write_bit(false); // gain_element_sign - writer.write_u32(0, 2); // gain_element_scale - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - let h = CouplingHeader::parse(&mut reader).unwrap(); - assert_eq!(h.num_gain_element_lists, 1); - assert_eq!(h.targets.len(), 1); - assert!(!h.targets[0].is_cpe); - } - - /// A CPE target with `cc_l && cc_r` adds a second gain list slot - /// (Table 4.153: split left/right lists). - #[test] - fn cpe_target_with_both_channels_adds_a_list() { - let mut writer = BitWriter::new(); - writer.write_bit(false); // ind_sw_cce_flag - writer.write_u32(0, 3); // num_coupled_elements (=> 1 target) - writer.write_bit(true); // cc_target_is_cpe[0] - writer.write_u32(3, 4); // cc_target_tag_select[0] - writer.write_bit(true); // cc_l[0] - writer.write_bit(true); // cc_r[0] - writer.write_bit(false); // cc_domain - writer.write_bit(false); // gain_element_sign - writer.write_u32(1, 2); // gain_element_scale - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - let h = CouplingHeader::parse(&mut reader).unwrap(); - // 1 (target) + 1 (cc_l && cc_r) = 2. - assert_eq!(h.num_gain_element_lists, 2); - assert!(h.targets[0].is_cpe); - assert!(h.targets[0].cc_l && h.targets[0].cc_r); - assert_eq!(h.targets[0].tag_select, 3); - } - - /// The header round-trips through write → parse. - #[test] - fn header_round_trips() { - let h = CouplingHeader { - ind_sw_cce_flag: true, - num_coupled_elements: 1, - targets: vec![ - CoupledTarget { - is_cpe: false, - tag_select: 2, - cc_l: false, - cc_r: false, - }, - CoupledTarget { - is_cpe: true, - tag_select: 5, - cc_l: true, - cc_r: false, - }, - ], - cc_domain: true, - gain_element_sign: true, - gain_element_scale: 2, - num_gain_element_lists: 2, - }; - let mut writer = BitWriter::new(); - h.write(&mut writer).unwrap(); - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - let parsed = CouplingHeader::parse(&mut reader).unwrap(); - assert_eq!(parsed, h); - } - - /// `write` rejects a SCE target carrying a `cc_l` flag. - #[test] - fn write_rejects_sce_target_with_cc_flag() { - let h = CouplingHeader { - ind_sw_cce_flag: false, - num_coupled_elements: 0, - targets: vec![CoupledTarget { - is_cpe: false, - tag_select: 0, - cc_l: true, - cc_r: false, - }], - cc_domain: false, - gain_element_sign: false, - gain_element_scale: 0, - num_gain_element_lists: 1, - }; - let mut writer = BitWriter::new(); - assert_eq!(h.write(&mut writer), Err(Error::CceInvalid)); - } - - /// `cc_gain` for the implicit list 0 is the natural scaling 1.0. - #[test] - fn cc_gain_list_zero_is_unity() { - let gains = CouplingGains { - cc_scale: CC_SCALE_TABLE[3], - gain_element_sign: false, - lists: vec![], - }; - assert_eq!(gains.cc_gain(0, 0, 0).unwrap(), 1.0); - } - - /// `cc_gain` applies `cc_scale^(−gain)` (conformance-settled - /// exponent sign) for a common-gain list with the sign bit clear. - #[test] - fn cc_gain_common_no_sign() { - let gains = CouplingGains { - cc_scale: 2.0, // scale index 3 => 2^1 - gain_element_sign: false, - lists: vec![GainList::Common(3)], - }; - // gain = 3, cc_sign = 1 => 2^-3 = 1/8. - assert!((gains.cc_gain(1, 0, 0).unwrap() - 0.125).abs() < 1e-12); - } - - /// A `common_gain_element` is never sign-split, even when the - /// header's `gain_element_sign` is set — the 2001 / 13818-7:2004 - /// `couple_channel()` forces `cc_sign = 1` in the common branch - /// (`docs/audio/aac/cce-gain-sign-split.md` §3), which also makes - /// every independently switched CCE couple in phase. - #[test] - fn cc_gain_common_never_sign_split() { - let gains = CouplingGains { - cc_scale: 2.0, - gain_element_sign: true, - lists: vec![GainList::Common(3)], - }; - // gain_element = 3, cc_sign = +1 => +2^-3, not a split raw - // value. - assert!((gains.cc_gain(1, 0, 0).unwrap() - 0.125).abs() < 1e-12); - } - - /// The sign-split DPCM decode takes `cc_sign` from each **delta** - /// LSB and accumulates `dpcm >> 1` (§3 ruling): the worked - /// `[3, 3]` sequence from `cce-gain-sign-split.md` §2.2 must land - /// at `{−cc_scale^−1, −cc_scale^−2}` under the negated exponent - /// (per-band signs both negative, exponents 1 then 2) — not the - /// `{−1, +3}` split of the 2009 fragment-A misprint. - #[test] - fn cc_gain_dpcm_delta_split() { - let sfb_cb = vec![vec![2u8, 2u8]]; - let header = CouplingHeader { - ind_sw_cce_flag: false, - num_coupled_elements: 1, - targets: vec![ - CoupledTarget { - is_cpe: false, - tag_select: 0, - cc_l: false, - cc_r: false, - }, - CoupledTarget { - is_cpe: false, - tag_select: 1, - cc_l: false, - cc_r: false, - }, - ], - cc_domain: false, - gain_element_sign: true, - gain_element_scale: 3, // cc_scale = 2 - num_gain_element_lists: 2, - }; - // Transmit the deltas [3, 3] directly. - let mut writer = BitWriter::new(); - writer.write_bit(false); // common_gain_element_present = 0 - for _ in 0..2 { - let (len, cw) = hcod_sf_encode(3).unwrap(); - writer.write_u32(cw, u32::from(len)); - } - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - let gains = CouplingGains::parse(&mut reader, &header, 1, 2, &sfb_cb).unwrap(); - // delta 3 => negative (3 & 1), a += 1 twice => gains 1, 2. - assert_eq!( - gains.lists, - vec![GainList::Dpcm(vec![vec![ - DpcmGain { - negative: true, - gain: 1 - }, - DpcmGain { - negative: true, - gain: 2 - }, - ]])] - ); - assert!((gains.cc_gain(1, 0, 0).unwrap() + 0.5).abs() < 1e-12); - assert!((gains.cc_gain(1, 0, 1).unwrap() + 0.25).abs() < 1e-12); - } - - /// The sign-split writer is the exact inverse of the parse loop, - /// including negative deltas (arithmetic-shift packing) and an - /// interior `ZERO_HCB` skip. - #[test] - fn dpcm_sign_split_round_trips() { - let sfb_cb = vec![vec![2u8, ZERO_HCB, 4u8, 4u8]]; - let header = CouplingHeader { - ind_sw_cce_flag: false, - num_coupled_elements: 1, - targets: vec![ - CoupledTarget { - is_cpe: false, - tag_select: 0, - cc_l: false, - cc_r: false, - }, - CoupledTarget { - is_cpe: false, - tag_select: 1, - cc_l: false, - cc_r: false, - }, - ], - cc_domain: false, - gain_element_sign: true, - gain_element_scale: 1, - num_gain_element_lists: 2, - }; - let grid = vec![vec![ - DpcmGain { - negative: true, - gain: -2, - }, - // ZERO_HCB carry cell (not transmitted). - DpcmGain { - negative: false, - gain: -2, - }, - DpcmGain { - negative: false, - gain: 1, - }, - DpcmGain { - negative: true, - gain: 1, - }, - ]]; - let gains = CouplingGains { - cc_scale: CC_SCALE_TABLE[1], - gain_element_sign: true, - lists: vec![GainList::Dpcm(grid.clone())], - }; - let mut writer = BitWriter::new(); - gains.write(&mut writer, &header, &sfb_cb).unwrap(); - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - let parsed = CouplingGains::parse(&mut reader, &header, 1, 4, &sfb_cb).unwrap(); - assert_eq!(parsed.lists, vec![GainList::Dpcm(grid)]); - } - - /// An out-of-phase band under a clear `gain_element_sign` is - /// unrepresentable on the wire and must be rejected by the writer, - /// not silently dropped. - #[test] - fn write_rejects_negative_band_without_sign_bit() { - let sfb_cb = vec![vec![2u8]]; - let header = CouplingHeader { - ind_sw_cce_flag: false, - num_coupled_elements: 1, - targets: vec![ - CoupledTarget { - is_cpe: false, - tag_select: 0, - cc_l: false, - cc_r: false, - }, - CoupledTarget { - is_cpe: false, - tag_select: 1, - cc_l: false, - cc_r: false, - }, - ], - cc_domain: false, - gain_element_sign: false, - gain_element_scale: 0, - num_gain_element_lists: 2, - }; - let gains = CouplingGains { - cc_scale: CC_SCALE_TABLE[0], - gain_element_sign: false, - lists: vec![GainList::Dpcm(vec![vec![DpcmGain { - negative: true, - gain: 0, - }]])], - }; - let mut writer = BitWriter::new(); - assert_eq!( - gains.write(&mut writer, &header, &sfb_cb), - Err(Error::CceInvalid) - ); - } - - /// A dependently switched per-band DPCM list round-trips through - /// write → parse against a fixed `sfb_cb` grid, and the forward - /// accumulator reconstructs the absolute gains. - #[test] - fn dpcm_gain_list_round_trips() { - // One window group, three bands; band 1 is ZERO_HCB (skipped). - let sfb_cb = vec![vec![2u8, ZERO_HCB, 4u8]]; - let header = CouplingHeader { - ind_sw_cce_flag: false, - num_coupled_elements: 1, - targets: vec![ - CoupledTarget { - is_cpe: false, - tag_select: 0, - cc_l: false, - cc_r: false, - }, - CoupledTarget { - is_cpe: false, - tag_select: 1, - cc_l: false, - cc_r: false, - }, - ], - cc_domain: false, - gain_element_sign: false, - gain_element_scale: 0, - num_gain_element_lists: 2, - }; - // Absolute gains: band0 = +2 (dpcm +2), band1 carries acc (2, - // not transmitted), band2 = +5 (dpcm +3). - let grid = vec![vec![ - DpcmGain { - negative: false, - gain: 2, - }, - DpcmGain { - negative: false, - gain: 2, - }, - DpcmGain { - negative: false, - gain: 5, - }, - ]]; - let gains = CouplingGains { - cc_scale: CC_SCALE_TABLE[0], - gain_element_sign: false, - lists: vec![GainList::Dpcm(grid.clone())], - }; - let mut writer = BitWriter::new(); - gains.write(&mut writer, &header, &sfb_cb).unwrap(); - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - let parsed = CouplingGains::parse(&mut reader, &header, 1, 3, &sfb_cb).unwrap(); - assert_eq!(parsed.lists.len(), 1); - match &parsed.lists[0] { - GainList::Dpcm(g) => assert_eq!(g, &grid), - other => panic!("expected Dpcm, got {other:?}"), - } - } - - /// `couple_channel` for the implicit list 0 (natural scaling) adds - /// the source spectrum onto the target unchanged on every - /// non-`ZERO_HCB` band, and skips the `ZERO_HCB` band entirely. - #[test] - fn couple_channel_list_zero_adds_natural_scaling() { - // One window group, one window of length 8; two bands of width 4. - // Band 0 is a spectrum book (couples), band 1 is ZERO_HCB (skip). - let offsets = [0u16, 4, 8]; - let sfb_cb = vec![vec![2u8, ZERO_HCB]]; - let wgl = [1u8]; - let gains = CouplingGains { - cc_scale: CC_SCALE_TABLE[3], - gain_element_sign: false, - lists: vec![], - }; - let source = vec![1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; - let mut dest = vec![10.0f64; 8]; - gains - .couple_channel(&source, &mut dest, 0, &sfb_cb, &wgl, 2, &offsets) - .unwrap(); - // Band 0 (indices 0..4): dest += 1*source. - assert_eq!(&dest[0..4], &[11.0, 12.0, 13.0, 14.0]); - // Band 1 (indices 4..8) is ZERO_HCB → untouched. - assert_eq!(&dest[4..8], &[10.0, 10.0, 10.0, 10.0]); - } - - /// `couple_channel` applies a non-unity common gain - /// (`cc_scale^(−gain)`) onto every coupled band. - #[test] - fn couple_channel_common_gain_scales() { - let offsets = [0u16, 4]; - let sfb_cb = vec![vec![2u8]]; - let wgl = [1u8]; - // gain element −1, sign clear, scale index 3 (cc_scale = 2) ⇒ - // cc_gain = 2^(−(−1)) = 2 (the am05 conformance vectors carry - // exactly this −1 common gain). - let gains = CouplingGains { - cc_scale: 2.0, - gain_element_sign: false, - lists: vec![GainList::Common(-1)], - }; - let source = vec![1.0f64, 2.0, 3.0, 4.0]; - let mut dest = vec![0.0f64; 4]; - gains - .couple_channel(&source, &mut dest, 1, &sfb_cb, &wgl, 1, &offsets) - .unwrap(); - assert_eq!(dest, vec![2.0, 4.0, 6.0, 8.0]); - } - - /// `couple_channel` walks the multi-window short-block grid: a window - /// group of length 2 applies the same per-sfb gain to both windows. - #[test] - fn couple_channel_multi_window_group() { - // num_windows = 2, window_len = 4, one group of length 2, one band. - let offsets = [0u16, 4]; - let sfb_cb = vec![vec![2u8]]; - let wgl = [2u8]; - let gains = CouplingGains { - cc_scale: 2.0, - gain_element_sign: false, - lists: vec![GainList::Common(0)], // cc_gain = 2^0 = 1 - }; - let source = vec![1.0f64, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0]; - let mut dest = vec![0.0f64; 8]; - gains - .couple_channel(&source, &mut dest, 1, &sfb_cb, &wgl, 1, &offsets) - .unwrap(); - // Both windows of the group are coupled at gain 1. - assert_eq!(dest, source); - } - - /// `couple_channel` per-band DPCM gains scale each band independently. - #[test] - fn couple_channel_dpcm_per_band_gains() { - let offsets = [0u16, 2, 4]; - let sfb_cb = vec![vec![2u8, 2u8]]; - let wgl = [1u8]; - // Absolute gains: band0 = 0 (cc_gain 1), band1 = −1 (cc_gain 2 - // under the conformance-settled negated exponent). - let gains = CouplingGains { - cc_scale: 2.0, - gain_element_sign: false, - lists: vec![GainList::Dpcm(vec![vec![ - DpcmGain { - negative: false, - gain: 0, - }, - DpcmGain { - negative: false, - gain: -1, - }, - ]])], - }; - let source = vec![3.0f64, 3.0, 3.0, 3.0]; - let mut dest = vec![0.0f64; 4]; - gains - .couple_channel(&source, &mut dest, 1, &sfb_cb, &wgl, 2, &offsets) - .unwrap(); - // Band 0 (0..2): ×1; band 1 (2..4): ×2. - assert_eq!(dest, vec![3.0, 3.0, 6.0, 6.0]); - } - - /// `couple_channel` rejects a source / dest length mismatch. - #[test] - fn couple_channel_rejects_length_mismatch() { - let offsets = [0u16, 4]; - let sfb_cb = vec![vec![2u8]]; - let gains = CouplingGains { - cc_scale: 2.0, - gain_element_sign: false, - lists: vec![], - }; - let source = vec![0.0f64; 4]; - let mut dest = vec![0.0f64; 8]; - assert_eq!( - gains.couple_channel(&source, &mut dest, 0, &sfb_cb, &[1u8], 1, &offsets), - Err(Error::CceInvalid) - ); - } - - /// An independently switched CCE forces `cge == 1`: no - /// `common_gain_element_present` bit is read, and the gain list is a - /// single common element per target. - #[test] - fn ind_sw_cce_uses_common_gain_only() { - let header = CouplingHeader { - ind_sw_cce_flag: true, - num_coupled_elements: 1, - targets: vec![ - CoupledTarget { - is_cpe: false, - tag_select: 0, - cc_l: false, - cc_r: false, - }, - CoupledTarget { - is_cpe: false, - tag_select: 1, - cc_l: false, - cc_r: false, - }, - ], - cc_domain: false, - gain_element_sign: false, - gain_element_scale: 0, - num_gain_element_lists: 2, - }; - let gains = CouplingGains { - cc_scale: CC_SCALE_TABLE[0], - gain_element_sign: false, - lists: vec![GainList::Common(1)], - }; - let mut writer = BitWriter::new(); - gains.write(&mut writer, &header, &[]).unwrap(); - let bytes = writer.into_bytes(); - let mut reader = BitReader::new(&bytes); - // No common_gain_element_present bit is present; parse must read - // exactly one hcod_sf codeword for the single list. - let parsed = CouplingGains::parse(&mut reader, &header, 1, 1, &[]).unwrap(); - assert_eq!(parsed.lists, vec![GainList::Common(1)]); - } -} diff --git a/crates/vendor/oxideav-aac/src/channel_map.rs b/crates/vendor/oxideav-aac/src/channel_map.rs deleted file mode 100644 index 93c74043..00000000 --- a/crates/vendor/oxideav-aac/src/channel_map.rs +++ /dev/null @@ -1,751 +0,0 @@ -//! Canonical multichannel output ordering — ISO/IEC 14496-3 Table 1.19. -//! -//! A `raw_data_block()` lists its channel elements (SCE / CPE / LFE) in -//! **bitstream order**, and [`crate::decode::StreamDecoder`] decodes each -//! element's time signal into that same element order. For the default -//! `channelConfiguration` values 1–7 (Table 1.19) the spec fixes which -//! loudspeaker each element feeds, but the loudspeaker order is *not* the -//! order a downstream interleaved-PCM sink expects: a 5.1 decoder emits -//! its elements as `SCE(C), CPE(L,R), CPE(Ls,Rs), LFE` — speaker order -//! `[C, L, R, Ls, Rs, LFE]` — whereas the canonical interleaved layout -//! is `[L, R, C, LFE, Ls, Rs]` (the WAVE_FORMAT_EXTENSIBLE / BS.775 -//! convention that [`oxideav_core::ChannelLayout::Surround51`] adopts). -//! -//! This module owns the mapping from a `channelConfiguration` to: -//! -//! * the canonical [`ChannelLayout`] it denotes ([`layout_for_config`]), -//! and -//! * the **permutation** that reorders the element-order channel buffers -//! into that layout's canonical order ([`reorder_permutation`]). -//! -//! ## Element → speaker mapping (Table 1.19) -//! -//! Table 1.19's "channel to speaker mapping" column, read against the -//! "audio syntactic elements, listed in order received" column, gives the -//! per-element speaker assignment used here: -//! -//! | cfg | elements (in order) | element speaker order | -//! |-----|--------------------------------|----------------------------------| -//! | 1 | SCE | `[C]` | -//! | 2 | CPE | `[L, R]` | -//! | 3 | SCE, CPE | `[C, L, R]` | -//! | 4 | SCE, CPE, SCE | `[C, L, R, Cs]` | -//! | 5 | SCE, CPE, CPE | `[C, L, R, Ls, Rs]` | -//! | 6 | SCE, CPE, CPE, LFE | `[C, L, R, Ls, Rs, LFE]` | -//! -//! Each `ChannelPosition` in that element order is then matched to its -//! slot in the canonical layout (`ChannelLayout::positions()`), producing -//! the index permutation. The reorder is applied by the decode driver -//! before interleaving (see [`crate::decode`]). -//! -//! | 7 | SCE, CPE, CPE, CPE, LFE | `[C, Lc, Rc, L, R, Ls, Rs, LFE]` | -//! -//! Config 7 is the Table 1.19 7.1 arrangement (centre + inner -//! left/right *centre front* pair + outer left/right front pair + -//! surround pair + LFE); its canonical interleave follows the same -//! WAVE/BS.775 rank order as everything else, giving -//! `[L, R, C, LFE, Lc, Rc, Ls, Rs]`. `channelConfiguration == 0` -//! (custom layout) is handled by the §8.5.2.2 PCE mapping below -//! ([`pce_speaker_assignment`] / [`pce_reorder_permutation`]), driven -//! by the `program_config_element` the decoder captured; without an -//! active PCE the driver keeps bitstream element order. -//! -//! ## Clean-room provenance -//! -//! The element list and speaker mapping are transcribed from ISO/IEC -//! 14496-3:2009 §1.6.3.5 Table 1.19. The canonical interleaved order is -//! the WAVE_FORMAT_EXTENSIBLE / ITU-R BS.775 convention already encoded -//! in [`oxideav_core::ChannelLayout`]. - -use crate::pce::{ElementSelect, Pce}; -use oxideav_core::{ChannelLayout, ChannelPosition}; - -/// The canonical [`ChannelLayout`] denoted by a Table 1.19 -/// `channelConfiguration`, for the default values this crate reorders -/// (1–6). Returns `None` for `0` (PCE-defined), `7` (amendment-specific -/// 7.1), and any reserved value `≥ 8`. -#[must_use] -pub fn layout_for_config(channel_configuration: u8) -> Option { - Some(match channel_configuration { - 1 => ChannelLayout::Mono, - 2 => ChannelLayout::Stereo, - 3 => ChannelLayout::Surround30, - 4 => ChannelLayout::Surround40, - 5 => ChannelLayout::Surround50, - 6 => ChannelLayout::Surround51, - _ => return None, - }) -} - -/// The Table 1.19 per-element speaker order for a default -/// `channelConfiguration` — the loudspeaker each decoded channel feeds, -/// in the order the elements appear in the `raw_data_block()`. -/// -/// Returns `None` for `0` (PCE-defined — see -/// [`pce_speaker_assignment`]) and reserved values. -#[must_use] -pub fn element_speaker_order(channel_configuration: u8) -> Option<&'static [ChannelPosition]> { - use ChannelPosition::*; - Some(match channel_configuration { - 1 => &[FrontCenter], - 2 => &[FrontLeft, FrontRight], - 3 => &[FrontCenter, FrontLeft, FrontRight], - 4 => &[FrontCenter, FrontLeft, FrontRight, BackCenter], - 5 => &[FrontCenter, FrontLeft, FrontRight, SideLeft, SideRight], - 6 => &[ - FrontCenter, - FrontLeft, - FrontRight, - SideLeft, - SideRight, - LowFrequency, - ], - // Table 1.19 value 7 — 7+1: centre front; left, right CENTRE - // front (the inner pair); left, right OUTSIDE front; left, - // right surround rear (the same surround wording as configs - // 5/6, mapped to the side-surround positions this crate uses - // there); LFE. - 7 => &[ - FrontCenter, - FrontLeftOfCenter, - FrontRightOfCenter, - FrontLeft, - FrontRight, - SideLeft, - SideRight, - LowFrequency, - ], - _ => return None, - }) -} - -/// The permutation that reorders element-order channel buffers into the -/// canonical [`ChannelLayout`] order for a default `channelConfiguration`. -/// -/// The returned vector `perm` has one entry per output channel: output -/// slot `i` (in canonical layout order) is sourced from element-order -/// channel `perm[i]`. Applying it is `out[i] = channels[perm[i]]`. -/// -/// Returns `None` when no reordering is defined for this configuration -/// (`0` — PCE-defined — and reserved values); the caller keeps the -/// bitstream element order. An identity permutation (configs 1 and 2, -/// where element order already matches the canonical order) is -/// returned as `Some(vec![0, 1, …])` so the caller can still validate -/// the channel count. -#[must_use] -pub fn reorder_permutation(channel_configuration: u8) -> Option> { - let element_order = element_speaker_order(channel_configuration)?; - // Sort the element-order channels by their canonical WAVE/BS.775 - // interleave rank. For configs 1–6 this reproduces exactly the - // `ChannelLayout::positions()` order of `layout_for_config` (the - // named layouts list their speakers in mask order); config 7 has - // no named `ChannelLayout` but ranks the same way. - let mut perm: Vec = (0..element_order.len()).collect(); - let ranks: Vec = element_order - .iter() - .map(|&p| canonical_rank(p)) - .collect::>>()?; - perm.sort_by_key(|&i| ranks[i]); - Some(perm) -} - -/// Apply [`reorder_permutation`] to a set of element-order channel -/// buffers, returning the reordered set. When no permutation is defined -/// for `channel_configuration`, or the channel count does not match the -/// permutation length, the input order is preserved (returned unchanged). -/// -/// This is the entry point the decode driver calls once a frame's -/// element-order channels are assembled. -#[must_use] -pub fn reorder_channels(channel_configuration: u8, channels: Vec>) -> Vec> { - let Some(perm) = reorder_permutation(channel_configuration) else { - return channels; - }; - if perm.len() != channels.len() { - // Element count disagrees with the signalled configuration (a - // malformed or PCE-overridden stream); leave the order untouched - // rather than drop or duplicate a channel. - return channels; - } - // `perm[i]` is the source slot for output slot `i`. - apply_permutation(&perm, channels) -} - -// ===== PCE-defined layouts (`channelConfiguration == 0`) ===== -// -// ISO/IEC 13818-7 §8.5.2.2 (the PCE channel-configuration rules the -// 14496-3 GA payload inherits): the PCE carries a *list of front -// channels* "using the rule center outwards, left before right" (a -// center-channel SCE first, other SCEs in L/R pairs), then a list of -// *side channels* (CPEs or SCE pairs) "in the order of front to -// back", then a list of *back channels* "listed from outside in" -// (SCEs paired except that a final unpaired SCE is the rear center), -// then the LFE list. Each list references its elements by -// `*_element_is_cpe` + `*_element_tag_select`, so the mapping is by -// (element kind, instance tag), independent of the order the elements -// appear in the `raw_data_block()`. - -/// Which channel-element type a PCE list entry (or a decoded element) -/// is — the key half of the PCE (kind, tag) element reference. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PceElementKind { - /// `single_channel_element()`. - Sce, - /// `channel_pair_element()`. - Cpe, - /// `lfe_channel_element()`. - Lfe, -} - -/// Canonical interleave rank of a [`ChannelPosition`] — the -/// WAVE_FORMAT_EXTENSIBLE / BS.775 speaker-mask bit order this crate's -/// default-config reorder already targets. Lower rank interleaves -/// first. -fn canonical_rank(pos: ChannelPosition) -> Option { - use ChannelPosition::*; - Some(match pos { - FrontLeft => 0, - FrontRight => 1, - FrontCenter => 2, - LowFrequency => 3, - BackLeft => 4, - BackRight => 5, - FrontLeftOfCenter => 6, - FrontRightOfCenter => 7, - BackCenter => 8, - SideLeft => 9, - SideRight => 10, - _ => return None, - }) -} - -/// One PCE-addressed element with its speaker assignment: the -/// `(kind, instance tag)` reference and the position(s) its decoded -/// channel(s) feed, in the element's own channel order (`[left, -/// right]` for a CPE). -type PceAssignment = (PceElementKind, u8, Vec); - -/// Group a PCE element list into L/R pairs plus at most one unpaired -/// (center) SCE, preserving list order. CPEs are pairs by -/// construction; consecutive SCEs pair up left-then-right -/// (§8.5.2.2). Returns `(pairs, lone_sce_tag)` where each pair is -/// two `(is_cpe, tag)` halves (both halves of a CPE share its tag), -/// or `None` when the list leaves half an SCE pair over (an -/// ambiguous layout this crate leaves in element order). -#[allow(clippy::type_complexity)] -fn pair_up(list: &[ElementSelect], lone_first: bool) -> Option<(Vec<[(bool, u8); 2]>, Option)> { - let sce_count = list.iter().filter(|e| !e.is_cpe).count(); - // At most one SCE can be unpaired; §8.5.2.2 puts a front center - // first, while the back list's lone SCE (rear center) is last. - // Encoders are seen emitting the front center *last* too, so the - // rule keyed here is simply the parity: an odd SCE count means - // exactly one lone (center) SCE, taken at the position - // `lone_first` prefers when there is a choice. - let mut lone: Option = None; - let mut expect_lone = sce_count % 2 == 1; - let mut pairs: Vec<[(bool, u8); 2]> = Vec::new(); - let mut pending_sce: Option = None; - let sce_positions: Vec = (0..list.len()).filter(|&i| !list[i].is_cpe).collect(); - let lone_index = if expect_lone { - if lone_first { - sce_positions.first().copied() - } else { - sce_positions.last().copied() - } - } else { - None - }; - for (i, e) in list.iter().enumerate() { - if e.is_cpe { - pairs.push([(true, e.tag_select), (true, e.tag_select)]); - } else if expect_lone && Some(i) == lone_index { - lone = Some(e.tag_select); - expect_lone = false; - } else if let Some(left) = pending_sce.take() { - pairs.push([(false, left), (false, e.tag_select)]); - } else { - pending_sce = Some(e.tag_select); - } - } - if pending_sce.is_some() { - return None; // half an SCE pair left over - } - Some((pairs, lone)) -} - -/// Push one L/R pair's two assignment halves. -fn push_pair( - out: &mut Vec, - pair: [(bool, u8); 2], - left: ChannelPosition, - right: ChannelPosition, -) { - let [(l_cpe, l_tag), (r_cpe, r_tag)] = pair; - if l_cpe { - // One CPE carries both halves. - debug_assert!(r_cpe && l_tag == r_tag); - out.push((PceElementKind::Cpe, l_tag, vec![left, right])); - } else { - out.push((PceElementKind::Sce, l_tag, vec![left])); - out.push((PceElementKind::Sce, r_tag, vec![right])); - } -} - -/// Derive the §8.5.2.2 element→speaker assignment of a PCE-defined -/// layout. -/// -/// Returns `None` (caller keeps bitstream element order) for layouts -/// this crate cannot express in canonical positions: more than two -/// front pairs, more than one side pair, more than two back pairs, -/// more than one LFE, or a list shape §8.5.2.2 does not describe. -/// -/// Position choices, mirroring Table 42's named speakers: -/// -/// * front: the lone SCE (odd SCE count) is the front center; one -/// pair is the ordinary L/R; with two pairs, the first-listed -/// (inner — "center outwards") pair is the left/right *center* -/// front (`FrontLeftOfCenter` / `FrontRightOfCenter`) and the -/// second the outside L/R (the Table 42 index-7 arrangement). -/// * side: a single pair is the side surround `SideLeft`/`SideRight`. -/// * back: with two pairs ("listed from outside in") the first is -/// the side-most surround pair (`SideLeft`/`SideRight`) and the -/// second the rear `BackLeft`/`BackRight`; a single pair is the -/// rear `BackLeft`/`BackRight` when something else fixes the side -/// image (a side pair or a rear-center SCE), else the -/// `SideLeft`/`SideRight` surround pair of the 5.1-style layouts -/// (matching this crate's Table 1.19 config-5/6 mapping); a final -/// unpaired SCE is the `BackCenter`. -/// * every LFE-list entry is `LowFrequency` (at most one). -pub fn pce_speaker_assignment(pce: &Pce) -> Option> { - use ChannelPosition::*; - let mut out: Vec = Vec::new(); - - // Front list: center outwards. - let (front_pairs, front_center) = pair_up(&pce.front_elements, true)?; - if let Some(tag) = front_center { - out.push((PceElementKind::Sce, tag, vec![FrontCenter])); - } - match front_pairs.len() { - 0 => {} - 1 => push_pair(&mut out, front_pairs[0], FrontLeft, FrontRight), - 2 => { - push_pair( - &mut out, - front_pairs[0], - FrontLeftOfCenter, - FrontRightOfCenter, - ); - push_pair(&mut out, front_pairs[1], FrontLeft, FrontRight); - } - _ => return None, - } - - // Side list: front to back; only one distinct side position pair. - let (side_pairs, side_lone) = pair_up(&pce.side_elements, false)?; - if side_lone.is_some() || side_pairs.len() > 1 { - return None; - } - let have_side = side_pairs.len() == 1; - if have_side { - push_pair(&mut out, side_pairs[0], SideLeft, SideRight); - } - - // Back list: outside in; a final lone SCE is the rear center. - let (back_pairs, back_center) = pair_up(&pce.back_elements, false)?; - match back_pairs.len() { - 0 => {} - 1 => { - if have_side || back_center.is_some() { - push_pair(&mut out, back_pairs[0], BackLeft, BackRight); - } else { - // The single surround pair of a 5.1-style layout — - // the same SideLeft/SideRight this crate's Table 1.19 - // config-5/6 mapping uses. - push_pair(&mut out, back_pairs[0], SideLeft, SideRight); - } - } - 2 => { - if have_side { - return None; // three distinct surround pairs - } - push_pair(&mut out, back_pairs[0], SideLeft, SideRight); - push_pair(&mut out, back_pairs[1], BackLeft, BackRight); - } - _ => return None, - } - if let Some(tag) = back_center { - out.push((PceElementKind::Sce, tag, vec![BackCenter])); - } - - // LFE list. - match pce.lfe_element_tag_selects.len() { - 0 => {} - 1 => out.push(( - PceElementKind::Lfe, - pce.lfe_element_tag_selects[0], - vec![LowFrequency], - )), - _ => return None, // §8.5.2.3: no mapping for multiple LFEs - } - - // Every position must be distinct (and canonical-rankable). - let mut seen = [false; 11]; - for (_, _, positions) in &out { - for &p in positions { - let r = canonical_rank(p)?; - if seen[r] { - return None; - } - seen[r] = true; - } - } - Some(out) -} - -/// The permutation that reorders a PCE-defined frame's element-order -/// channel buffers into canonical interleave order. -/// -/// `elements` describes the decoded frame in bitstream order: one -/// `(kind, instance tag, channel count)` triple per channel element. -/// Every element must be referenced by the PCE exactly once with a -/// matching channel count, and the PCE's whole audio-element set must -/// appear in the frame; otherwise `None` is returned and the caller -/// keeps element order. -pub fn pce_reorder_permutation( - pce: &Pce, - elements: &[(PceElementKind, u8, usize)], -) -> Option> { - let mut assignment = pce_speaker_assignment(pce)?; - // Per decoded channel (element order): its canonical rank. - let mut ranks: Vec = Vec::new(); - for &(kind, tag, n_ch) in elements { - let idx = assignment - .iter() - .position(|&(k, t, _)| k == kind && t == tag)?; - let (_, _, positions) = assignment.swap_remove(idx); - if positions.len() != n_ch { - return None; // e.g. a PS-widened SCE — keep element order - } - for p in positions { - ranks.push(canonical_rank(p)?); - } - } - if !assignment.is_empty() { - return None; // PCE promises channels the frame did not carry - } - // Output slot i takes the source channel with the i-th smallest - // rank. Ranks are distinct by construction. - let mut perm: Vec = (0..ranks.len()).collect(); - perm.sort_by_key(|&i| ranks[i]); - Some(perm) -} - -/// Apply a permutation produced by [`pce_reorder_permutation`] to a -/// set of element-order channel buffers (same contract as -/// [`reorder_channels`]: `out[i] = channels[perm[i]]`). -#[must_use] -pub fn apply_permutation(perm: &[usize], channels: Vec>) -> Vec> { - if perm.len() != channels.len() { - return channels; - } - let mut slots: Vec>> = channels.into_iter().map(Some).collect(); - let mut out = Vec::with_capacity(perm.len()); - for &src in perm { - out.push( - slots[src] - .take() - .expect("permutation is a bijection over the channel slots"), - ); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::ChannelPosition::*; - - #[test] - fn mono_and_stereo_are_identity() { - assert_eq!(reorder_permutation(1), Some(vec![0])); - assert_eq!(reorder_permutation(2), Some(vec![0, 1])); - } - - #[test] - fn surround30_moves_center_to_third_slot() { - // element order [C, L, R] -> canonical [L, R, C] - assert_eq!(reorder_permutation(3), Some(vec![1, 2, 0])); - } - - #[test] - fn surround40_keeps_back_center_last() { - // element order [C, L, R, Cs] -> canonical [L, R, C, Cs] - assert_eq!(reorder_permutation(4), Some(vec![1, 2, 0, 3])); - } - - #[test] - fn surround50_orders_front_then_surround() { - // element order [C, L, R, Ls, Rs] -> canonical [L, R, C, Ls, Rs] - assert_eq!(reorder_permutation(5), Some(vec![1, 2, 0, 3, 4])); - } - - #[test] - fn surround51_interleaves_lfe_before_surround() { - // element order [C, L, R, Ls, Rs, LFE] -> canonical - // [L, R, C, LFE, Ls, Rs] - assert_eq!(reorder_permutation(6), Some(vec![1, 2, 0, 5, 3, 4])); - } - - #[test] - fn config_zero_and_reserved_are_unmapped() { - assert_eq!(reorder_permutation(0), None); - assert_eq!(reorder_permutation(8), None); - assert_eq!(reorder_permutation(15), None); - assert_eq!(layout_for_config(0), None); - // Config 7 reorders but denotes no named core layout. - assert_eq!(layout_for_config(7), None); - } - - #[test] - fn config_seven_lands_wave_rank_order() { - // element order [C, Lc, Rc, L, R, Ls, Rs, LFE] → canonical - // [L, R, C, LFE, Lc, Rc, Ls, Rs] (WAVE mask rank order). - assert_eq!(reorder_permutation(7), Some(vec![3, 4, 0, 7, 1, 2, 5, 6])); - } - - #[test] - fn permutation_matches_layout_positions() { - // The permutation must land each element on the layout slot whose - // ChannelPosition equals the element's Table 1.19 speaker. - for cfg in 1..=6u8 { - let perm = reorder_permutation(cfg).unwrap(); - let elem = element_speaker_order(cfg).unwrap(); - let layout = layout_for_config(cfg).unwrap(); - let canonical = layout.positions(); - assert_eq!(perm.len(), canonical.len(), "cfg {cfg} length"); - assert_eq!(canonical.len(), elem.len(), "cfg {cfg} element count"); - for (out_slot, &src) in perm.iter().enumerate() { - assert_eq!( - elem[src], canonical[out_slot], - "cfg {cfg}: output slot {out_slot} mismatched speaker" - ); - } - } - } - - #[test] - fn layout_channel_counts_agree_with_element_order() { - for cfg in 1..=6u8 { - let layout = layout_for_config(cfg).unwrap(); - let elem = element_speaker_order(cfg).unwrap(); - assert_eq!( - usize::from(layout.channel_count()), - elem.len(), - "cfg {cfg} channel count" - ); - } - } - - #[test] - fn reorder_channels_permutes_buffers() { - // 5.1 element order [C, L, R, Ls, Rs, LFE] tagged by a sentinel - // sample so we can see where each lands. - let channels: Vec> = vec![ - vec![0], // C - vec![1], // L - vec![2], // R - vec![3], // Ls - vec![4], // Rs - vec![5], // LFE - ]; - let out = reorder_channels(6, channels); - // canonical [L, R, C, LFE, Ls, Rs] = [1, 2, 0, 5, 3, 4] - let got: Vec = out.iter().map(|c| c[0]).collect(); - assert_eq!(got, vec![1, 2, 0, 5, 3, 4]); - } - - #[test] - fn reorder_channels_passthrough_on_unmapped_config() { - let channels: Vec> = vec![vec![9], vec![8]]; - let out = reorder_channels(0, channels.clone()); - assert_eq!(out, channels); - } - - #[test] - fn reorder_channels_passthrough_on_count_mismatch() { - // cfg 6 expects 6 channels; a 4-channel input is left untouched. - let channels: Vec> = vec![vec![0], vec![1], vec![2], vec![3]]; - let out = reorder_channels(6, channels.clone()); - assert_eq!(out, channels); - } - - // ===== §8.5.2.2 PCE-defined layouts ===== - - fn sce(tag: u8) -> ElementSelect { - ElementSelect { - is_cpe: false, - tag_select: tag, - } - } - fn cpe(tag: u8) -> ElementSelect { - ElementSelect { - is_cpe: true, - tag_select: tag, - } - } - fn pce_with( - front: Vec, - side: Vec, - back: Vec, - lfe: Vec, - ) -> Pce { - Pce { - element_instance_tag: 0, - object_type: 1, - sampling_frequency_index: 3, - front_elements: front, - side_elements: side, - back_elements: back, - lfe_element_tag_selects: lfe, - assoc_data_tag_selects: vec![], - valid_cc_elements: vec![], - mono_mixdown_element_number: None, - stereo_mixdown_element_number: None, - matrix_mixdown: None, - comment_field: vec![], - } - } - - #[test] - fn pce_5_1_matches_config_6_order() { - // front [SCE0(C), CPE0(L/R)], back [CPE1(Ls/Rs)], lfe [0] — - // the PCE spelling of the Table 1.19 config-6 layout. Element - // order SCE, CPE0, CPE1, LFE must permute exactly like - // config 6: [L, R, C, LFE, Ls, Rs]. - let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![cpe(1)], vec![0]); - use PceElementKind::*; - let perm = - pce_reorder_permutation(&pce, &[(Sce, 0, 1), (Cpe, 0, 2), (Cpe, 1, 2), (Lfe, 0, 1)]) - .expect("5.1 PCE maps"); - assert_eq!(perm, vec![1, 2, 0, 5, 3, 4]); - } - - #[test] - fn pce_7_1_two_back_pairs_outside_in() { - // The staged 7.1 fixture's PCE shape: front [SCE0, CPE0], - // back [CPE1, CPE2] ("outside in": CPE1 the side-most - // surround pair, CPE2 the rear pair), lfe [0]. Element order - // SCE, CPE0, CPE1, CPE2, LFE → canonical - // [FL FR FC LFE BL BR SL SR] = - // [Cpe0.l, Cpe0.r, Sce, Lfe, Cpe2.l, Cpe2.r, Cpe1.l, Cpe1.r]. - let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![cpe(1), cpe(2)], vec![0]); - use PceElementKind::*; - let perm = pce_reorder_permutation( - &pce, - &[ - (Sce, 0, 1), - (Cpe, 0, 2), - (Cpe, 1, 2), - (Cpe, 2, 2), - (Lfe, 0, 1), - ], - ) - .expect("7.1 PCE maps"); - assert_eq!(perm, vec![1, 2, 0, 7, 5, 6, 3, 4]); - } - - #[test] - fn pce_hexagonal_lone_sces_are_centers() { - // The staged hexagonal fixture's PCE: front [CPE0, SCE0] - // (the lone front SCE is the center wherever it is listed), - // back [CPE1, SCE1] (a final unpaired back SCE is the rear - // center — §8.5.2.2). Element order CPE0, SCE0, CPE1, SCE1 → - // canonical [FL FR FC BL BR BC]. - let pce = pce_with(vec![cpe(0), sce(0)], vec![], vec![cpe(1), sce(1)], vec![]); - use PceElementKind::*; - let perm = - pce_reorder_permutation(&pce, &[(Cpe, 0, 2), (Sce, 0, 1), (Cpe, 1, 2), (Sce, 1, 1)]) - .expect("hexagonal PCE maps"); - assert_eq!(perm, vec![0, 1, 2, 3, 4, 5], "already canonical order"); - - // The same layout with the block elements in a different - // order still lands canonically (mapping is by (kind, tag)). - let perm = - pce_reorder_permutation(&pce, &[(Sce, 1, 1), (Sce, 0, 1), (Cpe, 1, 2), (Cpe, 0, 2)]) - .unwrap(); - // element-order channels: [BC, FC, BL, BR, FL, FR] → - // canonical FL FR FC BL BR BC = sources [4, 5, 1, 2, 3, 0]. - assert_eq!(perm, vec![4, 5, 1, 2, 3, 0]); - } - - #[test] - fn pce_sce_pair_forms_lr() { - // Two SCEs in the front list (even count) form one L/R pair. - let pce = pce_with(vec![sce(0), sce(1)], vec![], vec![], vec![]); - let assign = pce_speaker_assignment(&pce).unwrap(); - assert_eq!( - assign, - vec![ - (PceElementKind::Sce, 0, vec![FrontLeft]), - (PceElementKind::Sce, 1, vec![FrontRight]), - ] - ); - } - - #[test] - fn pce_side_pair_moves_single_back_pair_to_rear() { - // side [CPE1] + back [CPE2]: the back pair is the rear - // BL/BR (the side pair holds SL/SR). - let pce = pce_with(vec![sce(0), cpe(0)], vec![cpe(1)], vec![cpe(2)], vec![]); - let assign = pce_speaker_assignment(&pce).unwrap(); - let find = |tag: u8| { - assign - .iter() - .find(|&&(k, t, _)| k == PceElementKind::Cpe && t == tag) - .map(|(_, _, p)| p.clone()) - .unwrap() - }; - assert_eq!(find(1), vec![SideLeft, SideRight]); - assert_eq!(find(2), vec![BackLeft, BackRight]); - } - - #[test] - fn pce_unmappable_layouts_fall_back() { - // Three front pairs: no canonical positions — None. - let pce = pce_with(vec![cpe(0), cpe(1), cpe(2)], vec![], vec![], vec![]); - assert!(pce_speaker_assignment(&pce).is_none()); - // Two LFEs: §8.5.2.3 defines no mapping. - let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![], vec![0, 1]); - assert!(pce_speaker_assignment(&pce).is_none()); - } - - #[test] - fn pce_permutation_rejects_mismatches() { - use PceElementKind::*; - let pce = pce_with(vec![sce(0), cpe(0)], vec![], vec![], vec![]); - // Channel-count mismatch (a PS-widened SCE): None. - assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 2), (Cpe, 0, 2)]).is_none()); - // An element the PCE does not reference: None. - assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 1), (Cpe, 0, 2), (Cpe, 5, 2)]).is_none()); - // A referenced element missing from the frame: None. - assert!(pce_reorder_permutation(&pce, &[(Sce, 0, 1)]).is_none()); - } - - #[test] - fn every_speaker_in_canonical_appears_in_element_order() { - // Guards the bijection assumption reorder_channels relies on. - for cfg in 1..=6u8 { - let elem = element_speaker_order(cfg).unwrap(); - let layout = layout_for_config(cfg).unwrap(); - for &pos in layout.positions() { - assert!( - elem.contains(&pos), - "cfg {cfg}: canonical speaker {pos:?} missing from element order" - ); - } - } - // Sanity: a position only present in a higher layout is absent. - let elem5 = element_speaker_order(5).unwrap(); - assert!(!elem5.contains(&LowFrequency)); - } -} diff --git a/crates/vendor/oxideav-aac/src/codec_decoder.rs b/crates/vendor/oxideav-aac/src/codec_decoder.rs deleted file mode 100644 index 72b94f98..00000000 --- a/crates/vendor/oxideav-aac/src/codec_decoder.rs +++ /dev/null @@ -1,972 +0,0 @@ -//! `oxideav_core::Decoder` wiring for AAC-LC carried in ADTS. -//! -//! The crate's [`decode::StreamDecoder`](crate::decode::StreamDecoder) -//! already walks one ADTS frame's §4.4.2.1 `raw_data_block()` to -//! interleaved 16-bit PCM end-to-end, carrying every channel element's -//! §4.6.11 overlap-add / §4.6.7 LTP / §4.6.6 predictor state across -//! frames. This module adapts that path into the framework's packet-in / -//! frame-out [`oxideav_core::Decoder`] trait so containers (the MP4 -//! `mp4a` object-type, the AVI / WAVEFORMATEX `0x00FF` raw-AAC tag, the -//! Matroska `A_AAC` CodecID, …) can route ADTS-framed AAC streams via the -//! registry. -//! -//! ## Trait-API adaptation -//! -//! The framework trait is *packet-in, frame-out*: -//! -//! * [`send_packet`](Decoder::send_packet) accepts one [`Packet`] whose -//! `data` is **one or more complete ADTS frames** — each an ADTS -//! fixed/variable header (+ optional 16-bit CRC) followed by its -//! `aac_frame_length`-delimited `raw_data_block()`. A leading ID3v2 tag -//! (the streaming-mux convention) is skipped. Every ADTS frame in the -//! packet is decoded in order against the persistent -//! [`StreamDecoder`](crate::decode::StreamDecoder), so the per-element -//! filterbank / LTP / predictor state threads across packet boundaries -//! exactly as it does across the frames of a contiguous stream. -//! * [`receive_frame`](Decoder::receive_frame) returns one -//! [`AudioFrame`] per decoded access unit: [`FRAME_LEN`] = 1024 -//! samples per channel for the default frame family (960 / 512 / -//! 480 under the §4.5.1.1 families a LATM-carried ASC can select, -//! 2048 for a dual-rate SBR frame), interleaved little-endian -//! `i16` in element order ([`SampleFormat::S16`]). -//! * [`flush`](Decoder::flush) marks end-of-stream so subsequent -//! `receive_frame` calls return [`Error::Eof`] once the pending queue -//! drains. -//! * [`reset`](Decoder::reset) drops the persistent -//! [`StreamDecoder`](crate::decode::StreamDecoder) (and with it all -//! §4.6.11 overlap / §4.6.7 LTP / §4.6.6 predictor memory) so the next -//! `send_packet` decodes as if it were the first — the trait contract -//! for a stateful, overlap-add codec after a container seek. -//! -//! ## Output format -//! -//! The decoder emits **interleaved** S16 PCM in `Frame::Audio`: -//! `data.len() == 1`, the single plane holding -//! `samples_per_channel * channels * 2` little-endian `i16` bytes in the -//! §4.4.2.1 element order an SCE/LFE contributes one channel, a CPE two. -//! The §4.6.11 [`pcm`](crate::pcm) output stage has already applied the -//! §1.3 `NINT()` round-half-away-from-zero and the 16-bit saturation, so -//! this layer only widens each `i16` to its two little-endian bytes. -//! -//! ## Registration -//! -//! [`register_codecs`] installs the codec under id `"aac"` and claims the -//! container tags an AAC stream is looked up under: the MP4 object-type -//! `0x40` (`Audio ISO/IEC 14496-3`), the WAVEFORMATEX `0x00FF` -//! (raw AAC) and `0x1601` (MPEG-4 ADTS AAC), the `mp4a` / `aac ` FourCCs, -//! and the Matroska `A_AAC` CodecID. A probe scores the ADTS syncword on -//! the first packet so a genuine ADTS stream out-ranks a non-ADTS -//! claimant on a shared tag. -//! -//! ## Provenance -//! -//! Every byte-layout and clause reference is from ISO/IEC 13818-7 / -//! 14496-3 staged under `docs/audio/aac/`; the trait adaptation composes -//! the crate's own [`decode::StreamDecoder`](crate::decode::StreamDecoder) -//! with the framework surface and reads no external decoder. - -use std::collections::VecDeque; - -use oxideav_core::{ - AudioFrame, CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag, - Confidence, Decoder, Error, Frame, Packet, ProbeContext, Result, SampleFormat, -}; - -use crate::adts::{AdtsHeader, ADTS_HEADER_BYTES_NO_CRC}; -use crate::decode::{DecodedFrame, StreamDecoder}; -use crate::latm::{LoasDecoder, AUDIO_SYNC_STREAM_SYNCWORD}; - -/// Codec id under which [`register_codecs`] installs this decoder. -pub const CODEC_ID_STR: &str = "aac"; - -/// MP4 object-type indicator for `Audio ISO/IEC 14496-3` (AAC). The OTI -/// every MP4 / ISO-BMFF `esds` AudioObject descriptor carries for an AAC -/// elementary stream. -pub const MP4_OBJECT_TYPE_AAC: u8 = 0x40; - -/// WAVEFORMATEX `wFormatTag` for raw AAC (`WAVE_FORMAT_RAW_AAC1`). -pub const WAVE_FORMAT_RAW_AAC1: u16 = 0x00FF; - -/// WAVEFORMATEX `wFormatTag` for MPEG-4 ADTS AAC (`WAVE_FORMAT_MPEG_ADTS_AAC`). -pub const WAVE_FORMAT_MPEG_ADTS_AAC: u16 = 0x1601; - -/// Build a boxed AAC [`Decoder`] from `params`. -/// -/// `params.sample_rate` and `params.channels` seed the returned -/// decoder's [`output_params`](AacDecoder)-equivalent stream description; -/// the real per-frame sample rate and channel count are re-derived from -/// each ADTS frame header on `send_packet`, so the values supplied here -/// are a hint only. The decoder is always built — AAC carries its full -/// configuration in-band (the ADTS header), so no parameter is mandatory. -pub fn make_decoder(params: &CodecParameters) -> Result> { - let sample_rate = params.sample_rate.unwrap_or(44_100); - let channels = params.channels.unwrap_or(2); - - let mut out_params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); - out_params.sample_rate = Some(sample_rate); - out_params.channels = Some(channels); - out_params.sample_format = Some(SampleFormat::S16); - - let mut dec = AacDecoder::new(CodecId::new(CODEC_ID_STR), out_params); - // `{"sbr_downsampled": "true"}` selects the §4.6.18.4.3 - // downsampled SBR output mode: HE-AAC streams are emitted at the - // core sampling rate instead of the doubled SBR rate. - if let Some(v) = params.options.get("sbr_downsampled") { - dec.set_sbr_downsampled(matches!(v, "true" | "1")); - } - // `{"sbr_low_power": "true"}` selects the §4.6.18.8 low-power SBR - // tool (real-valued filterbanks; HE-AAC v2 PS streams are - // rejected in this mode). - if let Some(v) = params.options.get("sbr_low_power") { - dec.set_sbr_low_power(matches!(v, "true" | "1")); - } - Ok(Box::new(dec)) -} - -/// Packet-to-frame adaptor wrapping [`StreamDecoder`] in the framework -/// [`Decoder`] trait. -/// -/// State carried across packets: -/// -/// * `stream` — the persistent [`StreamDecoder`] whose per-element slots -/// thread the §4.6.11 overlap-add tail / §4.6.7 LTP history / §4.6.6 -/// predictor state across the frames of the stream. -/// * `pending` queues the [`AudioFrame`]s produced by the last -/// `send_packet` (one per decoded ADTS frame); `receive_frame` pops the -/// front. -/// * `eof` — set by [`Decoder::flush`]; once `pending` drains and `eof` -/// is set, `receive_frame` returns [`Error::Eof`]. -pub struct AacDecoder { - codec_id: CodecId, - output: CodecParameters, - stream: StreamDecoder, - loas: LoasDecoder, - /// The transport syntax detected from the first non-empty packet: - /// raw ADTS (`0xFFF` syncword) or LOAS `AudioSyncStream` (`0x2B7` - /// syncword). `None` until the first packet picks one; once set, every - /// later packet is routed the same way. - transport: Option, - pending: VecDeque, - eof: bool, - /// The caller-selected §4.6.18.4.3 downsampled SBR output mode, - /// kept so [`Decoder::reset`] re-applies it to the fresh backends. - sbr_downsampled: bool, - /// The caller-selected §4.6.18.8 low-power SBR mode, kept so - /// [`Decoder::reset`] re-applies it to the fresh backends. - sbr_low_power: bool, -} - -/// The carrier syntax an [`AacDecoder`] auto-detects on its first packet. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Transport { - /// Raw ADTS frames (`0xFFF` 12-bit syncword), routed through - /// [`StreamDecoder::decode_frame`]. - Adts, - /// LOAS `AudioSyncStream` (`0x2B7` 11-bit syncword), routed through - /// [`LoasDecoder::decode_all`]. - Loas, -} - -impl std::fmt::Debug for AacDecoder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AacDecoder") - .field("codec_id", &self.codec_id) - .field("transport", &self.transport) - .field("pending", &self.pending.len()) - .field("eof", &self.eof) - .finish() - } -} - -impl AacDecoder { - fn new(codec_id: CodecId, output: CodecParameters) -> Self { - Self { - codec_id, - output, - stream: StreamDecoder::new(), - loas: LoasDecoder::new(), - transport: None, - pending: VecDeque::new(), - eof: false, - sbr_downsampled: false, - sbr_low_power: false, - } - } - - /// Select the §4.6.18.4.3 downsampled SBR output mode on both - /// transport backends: HE-AAC (SBR-active) streams are synthesized - /// through the 32-channel QMF bank and emitted at the *core* - /// sampling rate (1024 samples per channel per block) instead of - /// the doubled SBR rate. Select before the first packet. Also - /// reachable at construction via the `sbr_downsampled` codec - /// option ([`make_decoder`]). - pub fn set_sbr_downsampled(&mut self, downsampled: bool) { - self.sbr_downsampled = downsampled; - self.stream.set_sbr_downsampled(downsampled); - self.loas.set_sbr_downsampled(downsampled); - } - - /// Select the §4.6.18.8 low-power SBR mode on both transport - /// backends (real-valued filterbanks + LP adjustment chain; - /// HE-AAC v2 PS streams are rejected in this mode). Select before - /// the first packet. Also reachable at construction via the - /// `sbr_low_power` codec option ([`make_decoder`]). - pub fn set_sbr_low_power(&mut self, low_power: bool) { - self.sbr_low_power = low_power; - self.stream.set_sbr_low_power(low_power); - self.loas.set_sbr_low_power(low_power); - } - - /// The parameter set this decoder advertises for its output stream. - /// Updated from each decoded ADTS frame header so a caller reading it - /// after the first packet sees the on-the-wire sample rate / channel - /// count rather than the at-construction hints. - pub fn output_params(&self) -> &CodecParameters { - &self.output - } - - /// Convert one [`DecodedFrame`]'s interleaved `i16` PCM to an - /// interleaved-S16 [`AudioFrame`] (single plane, little-endian). - fn decoded_to_audio(decoded: &DecodedFrame, pts: Option) -> AudioFrame { - let mut bytes = Vec::with_capacity(decoded.pcm.len() * 2); - for &s in &decoded.pcm { - bytes.extend_from_slice(&s.to_le_bytes()); - } - AudioFrame { - // Per-channel sample count from the interleaved buffer: - // 1024 for the plain AAC path, 2048 for an SBR (HE-AAC) - // dual-rate frame (1024 again in the downsampled SBR - // mode). A fill-only frame (`channels == 0`) carries no - // samples. - samples: decoded.pcm.len().checked_div(decoded.channels).unwrap_or(0) as u32, - pts, - data: vec![bytes], - } - } - - /// Queue a decoded frame's PCM and refresh the advertised output - /// params; a fill-only frame (`channels == 0`) produces no audio. - fn queue_decoded(&mut self, decoded: &DecodedFrame, pts: Option) -> bool { - if decoded.channels > 0 { - self.output.sample_rate = Some(decoded.sample_rate); - self.output.channels = Some(decoded.channels as u16); - self.pending.push_back(Self::decoded_to_audio(decoded, pts)); - true - } else { - false - } - } - - /// Route an ADTS-framed packet (`data` already ID3-stripped) through - /// the [`StreamDecoder`], queuing one [`AudioFrame`] per ADTS frame. - fn send_adts(&mut self, data: &[u8], pts: Option) -> Result<()> { - let mut pos = 0usize; - let mut produced_any = false; - while pos + ADTS_HEADER_BYTES_NO_CRC <= data.len() { - let (header, payload_offset) = AdtsHeader::parse(&data[pos..]) - .map_err(|e| Error::other(format!("oxideav-aac: adts header: {e}")))?; - let frame_len = header.aac_frame_length as usize; - if frame_len < payload_offset || pos + frame_len > data.len() { - return Err(Error::other( - "oxideav-aac: ADTS frame length overruns packet", - )); - } - // decode_adts_frame re-parses the header and verifies the - // §8.1.1 error_check() CRC layer when protection is - // present (payload_offset only bounds the frame here). - let decoded = self - .stream - .decode_adts_frame(&data[pos..pos + frame_len]) - .map_err(|e| Error::other(format!("oxideav-aac: decode_adts_frame: {e}")))?; - produced_any |= self.queue_decoded(&decoded, pts); - pos += frame_len; - } - - if !produced_any && pos == 0 { - return Err(Error::other( - "oxideav-aac: packet held no complete ADTS frame", - )); - } - Ok(()) - } - - /// Route a LOAS `AudioSyncStream` packet (`data` already ID3-stripped) - /// through the [`LoasDecoder`], queuing one [`AudioFrame`] per - /// recovered access unit. A packet may carry one or several LOAS sync - /// frames; the persistent [`LoasDecoder`] threads the - /// `StreamMuxConfig` (and per-stream decode state) across packets. - fn send_loas(&mut self, data: &[u8], pts: Option) -> Result<()> { - let decoded_frames = self - .loas - .decode_all(data) - .map_err(|e| Error::other(format!("oxideav-aac: loas decode: {e}")))?; - let mut produced_any = false; - for decoded in &decoded_frames { - produced_any |= self.queue_decoded(decoded, pts); - } - if !produced_any && decoded_frames.is_empty() { - return Err(Error::other( - "oxideav-aac: packet held no complete LOAS sync frame", - )); - } - Ok(()) - } -} - -impl Decoder for AacDecoder { - fn codec_id(&self) -> &CodecId { - &self.codec_id - } - - fn send_packet(&mut self, packet: &Packet) -> Result<()> { - if self.eof { - return Err(Error::other("oxideav-aac: cannot send_packet after flush")); - } - - let data = skip_id3v2(&packet.data); - - // Pick the carrier from the first non-empty packet, then route - // every later packet the same way. - let transport = match self.transport { - Some(t) => t, - None => { - let Some(t) = detect_transport(data) else { - return Err(Error::other( - "oxideav-aac: packet has neither an ADTS nor a LOAS syncword", - )); - }; - self.transport = Some(t); - t - } - }; - - match transport { - Transport::Adts => self.send_adts(data, packet.pts), - Transport::Loas => self.send_loas(data, packet.pts), - } - } - - fn receive_frame(&mut self) -> Result { - if let Some(audio) = self.pending.pop_front() { - return Ok(Frame::Audio(audio)); - } - if self.eof { - return Err(Error::Eof); - } - Err(Error::NeedMore) - } - - fn flush(&mut self) -> Result<()> { - self.eof = true; - Ok(()) - } - - fn reset(&mut self) -> Result<()> { - // Drop every per-element overlap / LTP / predictor slot (for both - // carriers) so the next send_packet decodes from a clean state, - // and re-arm transport auto-detection. - self.stream = StreamDecoder::new(); - self.loas = LoasDecoder::new(); - self.stream.set_sbr_downsampled(self.sbr_downsampled); - self.loas.set_sbr_downsampled(self.sbr_downsampled); - self.stream.set_sbr_low_power(self.sbr_low_power); - self.loas.set_sbr_low_power(self.sbr_low_power); - self.transport = None; - self.pending.clear(); - self.eof = false; - Ok(()) - } -} - -/// Detect the AAC carrier syntax from the first bytes of a packet -/// (already ID3v2-stripped). -/// -/// * ADTS — 12-bit `0xFFF` syncword: `byte0 == 0xFF` and the top four -/// bits of `byte1` are set. -/// * LOAS `AudioSyncStream` — 11-bit `0x2B7` syncword: the first 11 bits -/// equal `0x2B7` (`byte0 == 0x56`, top three bits of `byte1` set). -/// -/// Returns `None` when neither syncword matches. -fn detect_transport(data: &[u8]) -> Option { - if data.len() < 2 { - return None; - } - if data[0] == 0xFF && (data[1] & 0xF0) == 0xF0 { - return Some(Transport::Adts); - } - // 0x2B7 = 0b010_1011_0111: byte0 = 0b0101_0110 = 0x56, byte1 top 3 = - // 0b111. Confirm via the 11-bit syncword constant. - let first11 = (u32::from(data[0]) << 3) | (u32::from(data[1]) >> 5); - if first11 == AUDIO_SYNC_STREAM_SYNCWORD { - return Some(Transport::Loas); - } - None -} - -/// Skip a leading ID3v2 tag (`"ID3"` + 6-byte header + syncsafe size + -/// optional footer) if present; otherwise return the input unchanged. -/// Mirrors [`crate::decode`]'s stream-level skip so a packet that carries -/// a leading tag (the streaming-mux convention) decodes cleanly. -fn skip_id3v2(data: &[u8]) -> &[u8] { - if data.len() < 10 || &data[..3] != b"ID3" { - return data; - } - let size = data[6..10] - .iter() - .fold(0usize, |acc, &b| (acc << 7) | usize::from(b & 0x7f)); - let footer = if data[5] & 0x10 != 0 { 10 } else { 0 }; - let total = 10 + size + footer; - if total >= data.len() { - data - } else { - &data[total..] - } -} - -/// Probe the [`ADTS syncword`](crate::adts::ADTS_SYNCWORD) on the first -/// packet to disambiguate the shared container tags. -/// -/// * Sync OK (and a parseable fixed header) → `1.0` (definitive ADTS AAC). -/// * Leading ID3v2 then sync OK → `1.0` (streaming-mux ADTS). -/// * Packet present but no ADTS sync → `0.2` (not us, but the same -/// tag also covers non-ADTS — raw `raw_data_block()` / LATM — carriage -/// we can still attempt, so don't refuse outright). -/// * No packet hint → `0.5`. -fn probe_aac(ctx: &ProbeContext) -> Confidence { - let Some(pkt) = ctx.packet else { - return 0.5; - }; - let pkt = skip_id3v2(pkt); - if pkt.len() < 2 { - return 0.2; - } - // 12-bit ADTS syncword 0xFFF: byte 0 == 0xFF and the top 4 bits of - // byte 1 are 1. `AdtsHeader::parse` confirms the rest of the fixed - // header is structurally valid before we commit to the definitive - // score. - if pkt[0] == 0xFF && (pkt[1] & 0xF0) == 0xF0 && AdtsHeader::parse(pkt).is_ok() { - return 1.0; - } - // 11-bit LOAS AudioSyncStream syncword 0x2B7. A bare syncword match - // is a strong-but-not-definitive AAC signal (the AudioMuxElement - // body is validated on the first decode), so score it just below the - // structurally-confirmed ADTS hit. - if detect_transport(pkt) == Some(Transport::Loas) { - return 0.9; - } - 0.2 -} - -/// Install the AAC decoder factory into `reg`. -/// -/// Claims the container tags an AAC elementary stream is routed under: -/// -/// * **MP4 object-type `0x40`** — the `esds` AudioObject descriptor OTI -/// for `Audio ISO/IEC 14496-3`. -/// * **WAVEFORMATEX `0x00FF`** (`WAVE_FORMAT_RAW_AAC1`) and **`0x1601`** -/// (`WAVE_FORMAT_MPEG_ADTS_AAC`) — the Win32 `mmreg.h` raw-AAC and -/// ADTS-AAC format tags used by AVI / WAVE carriage. -/// * **FourCCs `mp4a` / `aac `** and the **Matroska `A_AAC`** CodecID. -/// -/// The encoder factory is -/// [`crate::codec_encoder::make_encoder`] — the frame-in / -/// packet-out adaptor over the `encoder` module's PCM→ADTS -/// [`crate::encoder::StreamEncoder`]. -/// -/// The probe ([`probe_aac`]) scores the ADTS syncword so a genuine ADTS -/// stream out-ranks a non-ADTS claimant on any shared tag. -pub fn register_codecs(reg: &mut CodecRegistry) { - let info = CodecInfo::new(CodecId::new(CODEC_ID_STR)) - .capabilities( - CodecCapabilities::audio("aac") - .with_decode() - .with_encode() - .with_lossy(true), - ) - .decoder(make_decoder) - .encoder(crate::codec_encoder::make_encoder) - .probe(probe_aac) - .tags([ - CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC), - CodecTag::wave_format(WAVE_FORMAT_RAW_AAC1), - CodecTag::wave_format(WAVE_FORMAT_MPEG_ADTS_AAC), - CodecTag::fourcc(b"mp4a"), - CodecTag::fourcc(b"aac "), - CodecTag::matroska("A_AAC"), - ]); - reg.register(info); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::decode::FRAME_LEN; - use oxideav_core::TimeBase; - - fn build_params(sample_rate: u32, channels: u16) -> CodecParameters { - let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); - p.sample_rate = Some(sample_rate); - p.channels = Some(channels); - p.sample_format = Some(SampleFormat::S16); - p - } - - /// Read a fixture's whole `input.aac` byte buffer, or `None` when the - /// workspace `docs/` tree is absent (standalone-crate CI checkouts). - fn fixture_bytes(name: &str) -> Option> { - let path = format!( - "{}/../../docs/audio/aac/fixtures/{name}/input.aac", - env!("CARGO_MANIFEST_DIR") - ); - if !std::path::Path::new(&path).exists() { - eprintln!("skip: staged ADTS fixture not present at {path}"); - return None; - } - Some(std::fs::read(&path).expect("read staged ADTS fixture")) - } - - /// Slice a raw-ADTS byte buffer into one packet per ADTS frame, the - /// way a demuxer would emit them on the wire. - fn split_into_packets(bytes: &[u8]) -> Vec { - let bytes = skip_id3v2(bytes); - let tb = TimeBase::new(1, 44_100); - let mut packets = Vec::new(); - let mut pos = 0usize; - let mut pts: i64 = 0; - while pos + ADTS_HEADER_BYTES_NO_CRC <= bytes.len() { - let Ok((header, _)) = AdtsHeader::parse(&bytes[pos..]) else { - break; - }; - let fl = header.aac_frame_length as usize; - if fl == 0 || pos + fl > bytes.len() { - break; - } - let mut pkt = Packet::new(0, tb, bytes[pos..pos + fl].to_vec()); - pkt.pts = Some(pts); - packets.push(pkt); - pts += FRAME_LEN as i64; - pos += fl; - } - packets - } - - /// Read a fixture's whole `input.` byte buffer, or `None` when - /// the workspace `docs/` tree is absent. - fn fixture_bytes_ext(name: &str, ext: &str) -> Option> { - let path = format!( - "{}/../../docs/audio/aac/fixtures/{name}/input.{ext}", - env!("CARGO_MANIFEST_DIR") - ); - if !std::path::Path::new(&path).exists() { - eprintln!("skip: staged fixture not present at {path}"); - return None; - } - Some(std::fs::read(&path).expect("read staged fixture")) - } - - #[test] - fn detect_transport_recognises_adts_and_loas() { - // ADTS: 0xFFF syncword. - assert_eq!(detect_transport(&[0xFF, 0xF1, 0x00]), Some(Transport::Adts)); - // LOAS AudioSyncStream: 0x2B7 in the first 11 bits → 0x56, top 3 - // bits of byte 1 set. - assert_eq!(detect_transport(&[0x56, 0xE0, 0x00]), Some(Transport::Loas)); - // Neither. - assert_eq!(detect_transport(&[0x00, 0x00]), None); - assert_eq!(detect_transport(&[0xFF]), None); - } - - #[test] - fn loas_packet_decodes_through_trait() { - let Some(buf) = fixture_bytes_ext("aac-latm-stream", "latm") else { - return; - }; - // Feed the whole LOAS buffer as one packet (a demuxer that hands - // the elementary stream in bulk). - let mut pkt = Packet::new(0, TimeBase::new(1, 44_100), buf.clone()); - pkt.pts = Some(0); - - let mut dec = make_decoder(&build_params(44_100, 2)).expect("decoder"); - dec.send_packet(&pkt).expect("send_packet (loas)"); - - let mut frames = 0usize; - let mut samples_total = 0usize; - while let Ok(Frame::Audio(a)) = dec.receive_frame() { - assert_eq!(a.samples as usize, FRAME_LEN); - // interleaved stereo → FRAME_LEN * 2 channels * 2 bytes. - assert_eq!(a.data[0].len(), FRAME_LEN * 2 * 2); - frames += 1; - samples_total += a.data[0].len() / 2; - } - assert!(frames > 0, "LOAS packet produced no frames"); - // 32 access units × 1024 × 2 channels. - assert_eq!(samples_total, 65_536); - } - - #[test] - fn loas_trait_matches_loas_decoder_pcm() { - let Some(buf) = fixture_bytes_ext("aac-latm-stream", "latm") else { - return; - }; - // Reference: bare LoasDecoder. - let mut reference = LoasDecoder::new(); - let ref_frames = reference.decode_all(&buf).expect("LoasDecoder"); - let mut ref_pcm: Vec = Vec::new(); - for f in &ref_frames { - ref_pcm.extend_from_slice(&f.pcm); - } - - // Trait path: one bulk packet. - let mut pkt = Packet::new(0, TimeBase::new(1, 44_100), buf); - pkt.pts = Some(0); - let mut dec = make_decoder(&build_params(44_100, 2)).expect("decoder"); - dec.send_packet(&pkt).expect("send_packet"); - let mut trait_pcm: Vec = Vec::new(); - while let Ok(Frame::Audio(a)) = dec.receive_frame() { - for c in a.data[0].chunks_exact(2) { - trait_pcm.push(i16::from_le_bytes([c[0], c[1]])); - } - } - assert_eq!(trait_pcm, ref_pcm, "LOAS trait diverged from LoasDecoder"); - } - - #[test] - fn probe_scores_loas_sync() { - // 0x2B7 syncword (0x56, top 3 bits of next byte set). - let pkt = [0x56u8, 0xE0, 0x00, 0x00]; - let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); - let ctx = ProbeContext::new(&tag).packet(&pkt); - assert!((probe_aac(&ctx) - 0.9).abs() < f32::EPSILON); - } - - #[test] - fn make_decoder_builds_and_reports_id() { - let dec = make_decoder(&build_params(44_100, 2)).expect("decoder builds"); - assert_eq!(dec.codec_id().as_str(), CODEC_ID_STR); - } - - #[test] - fn make_decoder_defaults_without_hints() { - let p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); - let _ = make_decoder(&p).expect("default-params decoder builds"); - } - - #[test] - fn receive_without_packet_is_need_more() { - let mut dec = make_decoder(&build_params(44_100, 2)).unwrap(); - match dec.receive_frame() { - Err(Error::NeedMore) => {} - other => panic!("expected NeedMore, got {other:?}"), - } - } - - #[test] - fn mono_fixture_decodes_one_frame_per_packet() { - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let packets = split_into_packets(&buf); - assert!(!packets.is_empty(), "fixture yielded zero packets"); - - let mut dec = make_decoder(&build_params(8_000, 1)).expect("decoder"); - let mut frames = 0usize; - for pkt in &packets { - dec.send_packet(pkt).expect("send_packet"); - loop { - match dec.receive_frame() { - Ok(Frame::Audio(a)) => { - assert_eq!(a.samples as usize, FRAME_LEN); - assert_eq!(a.data.len(), 1, "interleaved single plane"); - // mono → FRAME_LEN samples * 1 channel * 2 bytes. - assert_eq!(a.data[0].len(), FRAME_LEN * 2); - assert_eq!(a.pts, pkt.pts); - frames += 1; - } - Ok(other) => panic!("expected Audio, got {other:?}"), - Err(Error::NeedMore) => break, - Err(e) => panic!("receive_frame: {e}"), - } - } - } - assert_eq!(frames, packets.len(), "one frame per packet"); - } - - #[test] - fn stereo_fixture_decodes_two_channel_planes() { - let Some(buf) = fixture_bytes("aac-lc-intensity-stereo") else { - return; - }; - let packets = split_into_packets(&buf); - let mut dec = make_decoder(&build_params(44_100, 2)).expect("decoder"); - dec.send_packet(&packets[0]).expect("send_packet 0"); - let Frame::Audio(a) = dec.receive_frame().expect("frame 0") else { - panic!("expected AudioFrame"); - }; - assert_eq!(a.samples as usize, FRAME_LEN); - // interleaved stereo → FRAME_LEN * 2 channels * 2 bytes. - assert_eq!(a.data[0].len(), FRAME_LEN * 2 * 2); - } - - #[test] - fn trait_decode_matches_stream_decoder_pcm() { - // The trait wrapper must produce byte-identical PCM to the - // StreamDecoder it adapts (same persistent state, same order). - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let mut reference = StreamDecoder::new(); - let ref_frames = reference.decode_all(&buf).expect("reference decode_all"); - let mut ref_pcm: Vec = Vec::new(); - for f in &ref_frames { - ref_pcm.extend_from_slice(&f.pcm); - } - - let packets = split_into_packets(&buf); - let mut dec = make_decoder(&build_params(8_000, 1)).expect("decoder"); - let mut trait_pcm: Vec = Vec::new(); - for pkt in &packets { - dec.send_packet(pkt).expect("send_packet"); - while let Ok(Frame::Audio(a)) = dec.receive_frame() { - for c in a.data[0].chunks_exact(2) { - trait_pcm.push(i16::from_le_bytes([c[0], c[1]])); - } - } - } - assert_eq!( - trait_pcm, ref_pcm, - "trait decode diverged from StreamDecoder" - ); - } - - #[test] - fn flush_then_receive_yields_eof_after_drain() { - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let packets = split_into_packets(&buf); - let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); - dec.send_packet(&packets[0]).unwrap(); - dec.flush().unwrap(); - let _ = dec.receive_frame().expect("pending frame drains"); - match dec.receive_frame() { - Err(Error::Eof) => {} - other => panic!("expected Eof, got {other:?}"), - } - } - - #[test] - fn send_after_flush_is_rejected() { - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let packets = split_into_packets(&buf); - let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); - dec.flush().unwrap(); - assert!(dec.send_packet(&packets[0]).is_err()); - } - - #[test] - fn reset_re_enables_send_and_restores_clean_state() { - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let packets = split_into_packets(&buf); - - // Decode the first frame fresh, capture its PCM. - let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); - dec.send_packet(&packets[0]).unwrap(); - let Frame::Audio(first_clean) = dec.receive_frame().unwrap() else { - panic!("audio"); - }; - - // Advance a few frames (building overlap state), flush, reset. - for pkt in packets.iter().take(4) { - dec.send_packet(pkt).unwrap(); - while let Ok(Frame::Audio(_)) = dec.receive_frame() {} - } - dec.flush().unwrap(); - dec.reset().unwrap(); - - // After reset the first frame decodes byte-identically again — - // proving the overlap / state was wiped. - dec.send_packet(&packets[0]).unwrap(); - let Frame::Audio(first_again) = dec.receive_frame().unwrap() else { - panic!("audio"); - }; - assert_eq!( - first_again.data, first_clean.data, - "reset did not restore the initial decode state" - ); - } - - #[test] - fn multi_frame_packet_emits_one_audio_frame_each() { - // A packet carrying two concatenated ADTS frames must yield two - // AudioFrames (the streaming case where a demuxer batches frames). - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let packets = split_into_packets(&buf); - assert!(packets.len() >= 2); - let mut joined = packets[0].data.clone(); - joined.extend_from_slice(&packets[1].data); - let mut pkt = Packet::new(0, TimeBase::new(1, 8_000), joined); - pkt.pts = Some(0); - - let mut dec = make_decoder(&build_params(8_000, 1)).unwrap(); - dec.send_packet(&pkt).unwrap(); - let mut n = 0usize; - while let Ok(Frame::Audio(_)) = dec.receive_frame() { - n += 1; - } - assert_eq!(n, 2, "two ADTS frames in one packet → two AudioFrames"); - } - - // ───────────────────── probe + registration ───────────────────── - - /// Pack a minimal, structurally-valid 7-byte ADTS fixed/variable - /// header (protection_absent, LC mono 44.1 kHz, `aac_frame_length` - /// covering just the header) MSB-first so the probe vector cannot - /// drift out of sync with `AdtsHeader::parse`. - fn synth_adts_header() -> [u8; 7] { - let mut bits: Vec = Vec::new(); - let mut push = |val: u32, n: u32| { - for i in (0..n).rev() { - bits.push(((val >> i) & 1) as u8); - } - }; - push(0xFFF, 12); // syncword - push(0, 1); // ID = MPEG-4 - push(0, 2); // layer - push(1, 1); // protection_absent - push(1, 2); // profile = LC (AOT 2) - push(4, 4); // sampling_frequency_index = 44100 - push(0, 1); // private_bit - push(1, 3); // channel_configuration = mono - push(0, 1); // original_copy - push(0, 1); // home - push(0, 1); // copyright_identification_bit - push(0, 1); // copyright_identification_start - push(7, 13); // aac_frame_length = 7 (header only) - push(0x7FF, 11); // adts_buffer_fullness = VBR sentinel - push(0, 2); // number_of_raw_data_blocks_in_frame - 1 - let mut out = [0u8; 7]; - for (i, chunk) in bits.chunks(8).enumerate() { - let mut b = 0u8; - for (j, &bit) in chunk.iter().enumerate() { - b |= bit << (7 - j); - } - out[i] = b; - } - out - } - - #[test] - fn probe_scores_adts_sync() { - let hdr = synth_adts_header(); - let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); - let ctx = ProbeContext::new(&tag).packet(&hdr); - // Confirm the test vector is a well-formed ADTS header first. - assert!(AdtsHeader::parse(&hdr).is_ok(), "test ADTS header invalid"); - assert!((probe_aac(&ctx) - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn probe_scores_low_for_non_adts() { - let pkt = [0x00u8, 0x00, 0x00, 0x00]; - let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); - let ctx = ProbeContext::new(&tag).packet(&pkt); - assert!(probe_aac(&ctx) < 0.5); - } - - #[test] - fn probe_default_without_packet() { - let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); - let ctx = ProbeContext::new(&tag); - assert!((probe_aac(&ctx) - 0.5).abs() < f32::EPSILON); - } - - #[test] - fn probe_uses_fixture_first_bytes() { - let Some(buf) = fixture_bytes("aac-lc-mono-8000-16kbps-adts") else { - return; - }; - let tag = CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC); - let ctx = ProbeContext::new(&tag).packet(&buf); - assert!((probe_aac(&ctx) - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn register_installs_decoder_factory() { - let mut reg = CodecRegistry::new(); - register_codecs(&mut reg); - assert!(reg.has_decoder(&CodecId::new(CODEC_ID_STR))); - let _ = reg - .first_decoder(&build_params(44_100, 2)) - .expect("registry-built decoder"); - } - - #[test] - fn register_claims_all_tags() { - let mut reg = CodecRegistry::new(); - register_codecs(&mut reg); - for tag in [ - CodecTag::mp4_object_type(MP4_OBJECT_TYPE_AAC), - CodecTag::wave_format(WAVE_FORMAT_RAW_AAC1), - CodecTag::wave_format(WAVE_FORMAT_MPEG_ADTS_AAC), - CodecTag::fourcc(b"mp4a"), - CodecTag::fourcc(b"aac "), - CodecTag::matroska("A_AAC"), - ] { - let ctx = ProbeContext::new(&tag); - assert_eq!( - reg.resolve_tag_ref(&ctx).map(|c| c.as_str()), - Some(CODEC_ID_STR), - "tag {tag:?} did not resolve to aac", - ); - } - } - - /// The `sbr_downsampled` codec option: the HE-AAC v1 fixture - /// decodes at the core 22.05 kHz rate with 1024 samples per - /// channel per frame, and the mode survives `reset()`. - #[test] - fn sbr_downsampled_option_emits_core_rate() { - let Some(buf) = fixture_bytes("he-aac-v1-stereo-44100-32kbps-adts") else { - return; - }; - let packets = split_into_packets(&buf); - assert!(packets.len() > 2); - - let mut params = build_params(22_050, 2); - params.options.insert("sbr_downsampled", "true"); - let mut dec = make_decoder(¶ms).unwrap(); - - let run = |dec: &mut Box, pkts: &[Packet]| -> Vec { - let mut frames = Vec::new(); - for pkt in pkts { - dec.send_packet(pkt).unwrap(); - while let Ok(Frame::Audio(f)) = dec.receive_frame() { - frames.push(f); - } - } - frames - }; - let frames = run(&mut dec, &packets[..2]); - assert_eq!(frames.len(), 2); - for f in &frames { - assert_eq!(f.samples, 1024, "downsampled SBR frame length"); - assert_eq!(f.data[0].len(), 1024 * 2 * 2); - } - - // reset() keeps the selected mode. - dec.reset().unwrap(); - let frames2 = run(&mut dec, &packets[..2]); - assert_eq!(frames2.len(), 2); - assert_eq!(frames2[0].samples, 1024); - assert_eq!( - frames2[0].data[0], frames[0].data[0], - "post-reset decode differs" - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/codec_encoder.rs b/crates/vendor/oxideav-aac/src/codec_encoder.rs deleted file mode 100644 index 15ad7b28..00000000 --- a/crates/vendor/oxideav-aac/src/codec_encoder.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! `oxideav_core::Encoder` wiring for the AAC-LC encoder. -//! -//! Adapts [`crate::encoder::StreamEncoder`] (PCM → ADTS, see the -//! `encoder` module for the §4.5/§4.6 analysis chain) into the -//! framework's frame-in / packet-out [`oxideav_core::Encoder`] trait so -//! pipelines and muxers can drive AAC encoding via the registry. -//! -//! ## Trait-API adaptation -//! -//! * [`send_frame`](Encoder::send_frame) accepts [`Frame::Audio`] -//! frames carrying **interleaved little-endian `i16`** -//! (`SampleFormat::S16`, one data plane) at any per-frame sample -//! count. Samples buffer internally; every completed 1024-sample -//! hop becomes one ADTS frame. -//! * [`receive_packet`](Encoder::receive_packet) returns one -//! [`Packet`] per encoded ADTS frame ([`Error::NeedMore`] while the -//! buffer holds less than a hop). `pts` counts input samples -//! (time base `1/sample_rate`); the packet's `duration` is 1024. -//! Every AAC frame is independently decodable after the previous -//! frame's overlap, and each packet is flagged as a keyframe (the -//! ADTS stream is random-access at any frame boundary after a -//! 1-frame warmup). -//! * [`flush`](Encoder::flush) zero-pads the pending partial hop (if -//! any) into a final content frame and appends the encoder's -//! overlap-flush frame; subsequent `receive_packet` calls drain -//! those then return [`Error::Eof`]. -//! -//! ## Registration -//! -//! [`crate::codec_decoder::register_codecs`] installs -//! [`make_encoder`] alongside the decoder under codec id `"aac"`. -//! The historical direct factory path is also re-exported as -//! [`crate::encoder::make_encoder`]. - -use std::collections::VecDeque; - -use oxideav_core::{ - CodecId, CodecParameters, Encoder, Error, Frame, Packet, Result, SampleFormat, TimeBase, -}; - -use crate::codec_decoder::CODEC_ID_STR; -use crate::encoder::{EncoderConfig, StreamEncoder, FRAME_LEN}; - -/// Default target bitrate (bits/second) when `params.bit_rate` is -/// absent: 64 kbps per channel, the conventional "good quality" -/// AAC-LC operating point. -pub const DEFAULT_BITRATE_PER_CHANNEL: u32 = 64_000; - -/// Build a boxed AAC [`Encoder`] from `params`. -/// -/// Honoured parameters: -/// -/// * `sample_rate` (default 44 100) — must be an ISO/IEC 14496-3 -/// Table 1.18 rate with a §4.5.4 long-window band table -/// (96 000 … 8 000 Hz). -/// * `channels` (default 2) — any count with a Table 1.19 default -/// `channelConfiguration`: 1, 2, 3, 4, 5, 6 (5.1) or 8 (7.1); -/// input interleaved in the canonical [`crate::channel_map`] -/// order the decoder emits. 7 has no default configuration and is -/// rejected. -/// * `bit_rate` (default 64 kbps × channels) — the rate-loop target. -/// * `sample_format` — must be [`SampleFormat::S16`] (or unset). -/// -/// Anything unsupported surfaces as [`Error::Unsupported`] / -/// [`Error::invalid`] at construction time, per the registry's -/// init-time-fallback contract. -pub fn make_encoder(params: &CodecParameters) -> Result> { - let sample_rate = params.sample_rate.unwrap_or(44_100); - let channels = params.channels.unwrap_or(2); - if let Some(fmt) = params.sample_format { - if fmt != SampleFormat::S16 { - return Err(Error::unsupported( - "oxideav-aac encoder accepts interleaved S16 input only", - )); - } - } - if !(1..=6).contains(&channels) && channels != 8 { - return Err(Error::unsupported( - "oxideav-aac encoder supports the Table 1.19 default channel \ - configurations: 1-6 or 8 channels", - )); - } - let bitrate = params - .bit_rate - .map(|b| b.min(u64::from(u32::MAX)) as u32) - .unwrap_or(DEFAULT_BITRATE_PER_CHANNEL * u32::from(channels)); - let config = EncoderConfig { - sample_rate, - channels: channels as u8, - bitrate, - }; - let stream = StreamEncoder::new(config) - .map_err(|e| Error::invalid(format!("oxideav-aac encoder config: {e}")))?; - - let mut out_params = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); - out_params.sample_rate = Some(sample_rate); - out_params.channels = Some(channels); - out_params.sample_format = Some(SampleFormat::S16); - out_params.bit_rate = Some(u64::from(bitrate)); - - Ok(Box::new(AacEncoder { - codec_id: CodecId::new(CODEC_ID_STR), - out_params, - stream, - time_base: TimeBase::new(1, i64::from(sample_rate)), - pending_pcm: Vec::new(), - packets: VecDeque::new(), - samples_emitted: 0, - flushed: false, - })) -} - -/// Frame-to-packet adaptor wrapping [`StreamEncoder`] in the -/// framework [`Encoder`] trait. -struct AacEncoder { - codec_id: CodecId, - out_params: CodecParameters, - stream: StreamEncoder, - time_base: TimeBase, - /// Interleaved samples not yet forming a whole 1024-sample hop. - pending_pcm: Vec, - /// Encoded ADTS frames awaiting `receive_packet`. - packets: VecDeque, - /// Per-channel input samples consumed into emitted packets — - /// drives `pts`. - samples_emitted: i64, - flushed: bool, -} - -impl AacEncoder { - /// Encode every complete hop sitting in `pending_pcm`. - fn drain_hops(&mut self) -> Result<()> { - let ch = usize::from(self.out_params.channels.unwrap_or(1)).max(1); - let hop = FRAME_LEN * ch; - while self.pending_pcm.len() >= hop { - let chunk: Vec = self.pending_pcm.drain(..hop).collect(); - let bytes = self - .stream - .encode_frame(&chunk) - .map_err(|e| Error::invalid(format!("oxideav-aac encode: {e}")))?; - self.push_packet(bytes); - } - Ok(()) - } - - fn push_packet(&mut self, bytes: Vec) { - let pkt = Packet::new(0, self.time_base, bytes) - .with_pts(self.samples_emitted) - .with_duration(FRAME_LEN as i64) - .with_keyframe(true); - self.samples_emitted += FRAME_LEN as i64; - self.packets.push_back(pkt); - } -} - -impl Encoder for AacEncoder { - fn codec_id(&self) -> &CodecId { - &self.codec_id - } - - fn output_params(&self) -> &CodecParameters { - &self.out_params - } - - fn send_frame(&mut self, frame: &Frame) -> Result<()> { - if self.flushed { - return Err(Error::invalid("send_frame after flush")); - } - let audio = match frame { - Frame::Audio(a) => a, - _ => return Err(Error::invalid("oxideav-aac encoder accepts audio frames")), - }; - let plane = match audio.data.as_slice() { - [p] => p, - _ => { - return Err(Error::invalid( - "oxideav-aac encoder expects one interleaved S16 plane", - )) - } - }; - if plane.len() % 2 != 0 { - return Err(Error::invalid("odd byte count in S16 plane")); - } - self.pending_pcm.extend( - plane - .chunks_exact(2) - .map(|b| i16::from_le_bytes([b[0], b[1]])), - ); - self.drain_hops() - } - - fn receive_packet(&mut self) -> Result { - if let Some(pkt) = self.packets.pop_front() { - return Ok(pkt); - } - if self.flushed { - Err(Error::Eof) - } else { - Err(Error::NeedMore) - } - } - - fn flush(&mut self) -> Result<()> { - if self.flushed { - return Ok(()); - } - // Zero-pad any partial hop into a final content frame… - if !self.pending_pcm.is_empty() { - let chunk: Vec = std::mem::take(&mut self.pending_pcm); - let bytes = self - .stream - .encode_frame(&chunk) - .map_err(|e| Error::invalid(format!("oxideav-aac encode: {e}")))?; - self.push_packet(bytes); - } - // …then emit the overlap-flush frame. - let bytes = self - .stream - .finish() - .map_err(|e| Error::invalid(format!("oxideav-aac flush: {e}")))?; - self.push_packet(bytes); - self.flushed = true; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::AudioFrame; - - fn params(rate: u32, channels: u16, bitrate: Option) -> CodecParameters { - let mut p = CodecParameters::audio(CodecId::new(CODEC_ID_STR)); - p.sample_rate = Some(rate); - p.channels = Some(channels); - p.sample_format = Some(SampleFormat::S16); - p.bit_rate = bitrate; - p - } - - fn tone_frame(samples: usize, channels: usize) -> Frame { - let mut bytes = Vec::with_capacity(samples * channels * 2); - for i in 0..samples { - let v = (8000.0 * (0.05 * i as f64).sin()) as i16; - for _ in 0..channels { - bytes.extend_from_slice(&v.to_le_bytes()); - } - } - Frame::Audio(AudioFrame { - samples: samples as u32, - pts: None, - data: vec![bytes], - }) - } - - #[test] - fn encoder_builds_and_reports_output_params() { - let enc = make_encoder(¶ms(44_100, 2, Some(128_000))).expect("builds"); - assert_eq!(enc.codec_id().as_str(), "aac"); - let out = enc.output_params(); - assert_eq!(out.sample_rate, Some(44_100)); - assert_eq!(out.channels, Some(2)); - assert_eq!(out.bit_rate, Some(128_000)); - } - - #[test] - fn encoder_rejects_unsupported_shapes() { - // 7 channels has no Table 1.19 default configuration; 6 - // (5.1) and 8 (7.1) do and build. - assert!(make_encoder(¶ms(44_100, 7, None)).is_err()); - assert!(make_encoder(¶ms(44_100, 9, None)).is_err()); - assert!(make_encoder(¶ms(44_100, 6, None)).is_ok()); - assert!(make_encoder(¶ms(44_100, 8, None)).is_ok()); - assert!(make_encoder(¶ms(44_055, 1, None)).is_err()); - let mut p = params(44_100, 2, None); - p.sample_format = Some(SampleFormat::F32); - assert!(make_encoder(&p).is_err()); - } - - #[test] - fn frames_in_packets_out_with_flush() { - let mut enc = make_encoder(¶ms(44_100, 1, Some(96_000))).unwrap(); - // 2.5 hops of input. - enc.send_frame(&tone_frame(2_560, 1)).unwrap(); - // Two whole hops → two packets. - let p0 = enc.receive_packet().unwrap(); - assert_eq!(p0.pts, Some(0)); - assert_eq!(p0.duration, Some(1024)); - assert!(p0.flags.keyframe); - assert!(p0.data.starts_with(&[0xFF])); - let p1 = enc.receive_packet().unwrap(); - assert_eq!(p1.pts, Some(1024)); - assert!(matches!(enc.receive_packet(), Err(Error::NeedMore))); - // Flush: the padded half hop + the overlap-flush frame. - enc.flush().unwrap(); - let p2 = enc.receive_packet().unwrap(); - assert_eq!(p2.pts, Some(2048)); - let p3 = enc.receive_packet().unwrap(); - assert_eq!(p3.pts, Some(3072)); - assert!(matches!(enc.receive_packet(), Err(Error::Eof))); - } - - #[test] - fn registry_round_trip_decodes_encoder_output() { - let mut enc = make_encoder(¶ms(44_100, 1, Some(128_000))).unwrap(); - let n = 4 * FRAME_LEN; - enc.send_frame(&tone_frame(n, 1)).unwrap(); - enc.flush().unwrap(); - let mut stream_bytes = Vec::new(); - loop { - match enc.receive_packet() { - Ok(p) => stream_bytes.extend_from_slice(&p.data), - Err(Error::Eof) => break, - Err(e) => panic!("unexpected: {e}"), - } - } - - // Feed the whole ADTS stream to the registered decoder. - let mut dec = crate::codec_decoder::make_decoder(¶ms(44_100, 1, None)).unwrap(); - let pkt = Packet::new(0, TimeBase::new(1, 44_100), stream_bytes); - dec.send_packet(&pkt).unwrap(); - let mut decoded_samples = 0usize; - loop { - match dec.receive_frame() { - Ok(Frame::Audio(a)) => decoded_samples += a.samples as usize, - Ok(_) => panic!("non-audio frame"), - Err(_) => break, - } - } - // n/1024 content frames + 1 flush frame, 1024 samples each. - assert_eq!(decoded_samples, n + FRAME_LEN); - } -} diff --git a/crates/vendor/oxideav-aac/src/crc.rs b/crates/vendor/oxideav-aac/src/crc.rs deleted file mode 100644 index 813eeccc..00000000 --- a/crates/vendor/oxideav-aac/src/crc.rs +++ /dev/null @@ -1,449 +0,0 @@ -//! Error-protection CRC generator — ISO/IEC 14496-3 §1.8.4.5. -//! -//! §1.8.4.5 defines a family of cyclic-redundancy-check codes used by -//! the MPEG-4 Audio error-protection (EP) tool and by the LATM -//! `StreamMuxConfig()` `crcCheckSum` field (§1.7.3.1, Table 1.42: -//! "This CRC uses the generation polynomial CRC8, as defined in -//! subclause 1.8.4.5 and covers the entire StreamMuxConfig() up to but -//! excluding the crcCheckPresent bit"). -//! -//! ## Generation polynomials (§1.8.4.5) -//! -//! Each `k`-bit CRC has a generator polynomial `G(x)` of degree `k`: -//! -//! | `k` | `G(x)` | -//! |------|-----------------------------------------------------------------| -//! | 4 | x⁴ + x³ + x² + 1 | -//! | 5 | x⁵ + x⁴ + x² + 1 | -//! | 6 | x⁶ + x⁵ + x⁴ + x² + x + 1 | -//! | 7 | x⁷ + x³ + x + 1 | -//! | 8 | x⁸ + x⁴ + x³ + x² + 1 | -//! | 9 | x⁹ + x⁴ + x³ + x² + x + 1 | -//! | 10 | x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1 | -//! | 11 | x¹¹ + x¹⁰ + x⁹ + x⁵ + x + 1 | -//! | 12 | x¹² + x¹¹ + x³ + x² + x + 1 | -//! | 13 | x¹³ + x¹² + x¹¹ + x⁸ + x⁷ + x⁴ + x² + 1 | -//! | 14 | x¹⁴ + x¹³ + x¹⁰ + x⁵ + x³ + x + 1 | -//! | 15 | x¹⁵ + x¹⁴ + x¹³ + x¹⁰ + x⁸ + x⁵ + x² + x + 1 | -//! | 16 | x¹⁶ + x¹⁵ + x² + 1 | -//! | 24 | x²⁴ + x²³ + x⁶ + x⁵ + x + 1 | -//! | 32 | x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1 | -//! -//! ## Encoding procedure (§1.8.4.5) -//! -//! With these polynomials the CRC encoding proceeds as follows. Let -//! `M(x)` be the information bits (highest order = first bit -//! transmitted) and `k` the number of CRC bits. Compute the remainder -//! `R(x)` satisfying -//! -//! ```text -//! M(x)·xᵏ = Q(x)·G(x) + R(x) -//! ``` -//! -//! i.e. `R(x)` is the degree-`(k−1)` remainder of the message shifted -//! left by `k` bits (`M(x)·xᵏ`) divided by `G(x)`, with a zero initial -//! register and no input reflection (MSB-first). The transmitted CRC -//! word is then -//! -//! ```text -//! W(x) = M(x)·xᵏ + R(x) -//! ``` -//! -//! with the normative final step: "The CRC bits are written in a -//! reversed manner, i. e. each bit is inverted." So the `k` remainder -//! bits are **bit-inverted** (one's complement) before transmission. -//! [`crc_bits`] returns the post-inversion value — the exact bits a -//! conforming bitstream carries in `crcCheckSum` — so a decoder -//! validates simply by recomputing over the protected region and -//! comparing for equality with the field it read off the wire. -//! -//! ## Scope -//! -//! This module is the §1.8.4.5 generator only. It does **not** apply -//! the §1.8.4.6 SRCPC convolutional FEC stage, nor does it implement -//! the ADTS (`adts_error_check()`) region selection, whose CRC is -//! cited by ISO/IEC 13818-7 to a different normative reference -//! (ISO/IEC 11172-3 §2.4.3.1) and is therefore not covered here. - -/// A CRC generation polynomial from ISO/IEC 14496-3 §1.8.4.5. -/// -/// Each variant fixes both the bit width `k` and the generator -/// polynomial `G(x)`. The polynomial is stored as the low `k` bits of -/// the generator (the implicit `xᵏ` leading term is dropped, as is -/// conventional for a shift-register CRC). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CrcPoly { - /// 1-bit CRC: x + 1 (§1.8.4.5, EP-tool class CRCs). - Crc1, - /// 2-bit CRC: x² + x + 1. - Crc2, - /// 3-bit CRC: x³ + x + 1. - Crc3, - /// 4-bit CRC: x⁴ + x³ + x² + 1. - Crc4, - /// 5-bit CRC: x⁵ + x⁴ + x² + 1. - Crc5, - /// 6-bit CRC: x⁶ + x⁵ + x⁴ + x² + x + 1. - Crc6, - /// 7-bit CRC: x⁷ + x³ + x + 1. - Crc7, - /// 8-bit CRC: x⁸ + x⁴ + x³ + x² + 1. Used by LATM - /// `StreamMuxConfig()` `crcCheckSum`. - Crc8, - /// 9-bit CRC: x⁹ + x⁴ + x³ + x² + x + 1. - Crc9, - /// 10-bit CRC: x¹⁰ + x⁹ + x⁵ + x⁴ + x + 1. - Crc10, - /// 11-bit CRC: x¹¹ + x¹⁰ + x⁹ + x⁵ + x + 1. - Crc11, - /// 12-bit CRC: x¹² + x¹¹ + x³ + x² + x + 1. - Crc12, - /// 13-bit CRC: x¹³ + x¹² + x¹¹ + x⁸ + x⁷ + x⁴ + x² + 1. - Crc13, - /// 14-bit CRC: x¹⁴ + x¹³ + x¹⁰ + x⁵ + x³ + x + 1. - Crc14, - /// 15-bit CRC: x¹⁵ + x¹⁴ + x¹³ + x¹⁰ + x⁸ + x⁵ + x² + x + 1. - Crc15, - /// 16-bit CRC: x¹⁶ + x¹⁵ + x² + 1. - Crc16, - /// 24-bit CRC: x²⁴ + x²³ + x⁶ + x⁵ + x + 1. - Crc24, - /// 32-bit CRC: x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + - /// x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1. - Crc32, -} - -impl CrcPoly { - /// The CRC width `k` in bits. - pub const fn width(self) -> u32 { - match self { - CrcPoly::Crc1 => 1, - CrcPoly::Crc2 => 2, - CrcPoly::Crc3 => 3, - CrcPoly::Crc4 => 4, - CrcPoly::Crc5 => 5, - CrcPoly::Crc6 => 6, - CrcPoly::Crc7 => 7, - CrcPoly::Crc8 => 8, - CrcPoly::Crc9 => 9, - CrcPoly::Crc10 => 10, - CrcPoly::Crc11 => 11, - CrcPoly::Crc12 => 12, - CrcPoly::Crc13 => 13, - CrcPoly::Crc14 => 14, - CrcPoly::Crc15 => 15, - CrcPoly::Crc16 => 16, - CrcPoly::Crc24 => 24, - CrcPoly::Crc32 => 32, - } - } - - /// The generator polynomial `G(x)` as the low `k` bits (the - /// implicit leading `xᵏ` term is not stored). Bit `i` is set iff - /// the term `xⁱ` is present in `G(x)`. - /// - /// Derived directly from the §1.8.4.5 polynomial listing — e.g. - /// `Crc8` (x⁸ + x⁴ + x³ + x² + 1) drops the `x⁸` and keeps - /// `x⁴ + x³ + x² + x⁰`, i.e. bits 4, 3, 2, 0 ⇒ `0b0001_1101`. - pub const fn generator(self) -> u64 { - match self { - // x+1 → bit 0 - CrcPoly::Crc1 => bits(&[0]), - // x²+x+1 → bits 1,0 - CrcPoly::Crc2 => bits(&[1, 0]), - // x³+x+1 → bits 1,0 - CrcPoly::Crc3 => bits(&[1, 0]), - // x⁴+x³+x²+1 → bits 3,2,0 - CrcPoly::Crc4 => bits(&[3, 2, 0]), - // x⁵+x⁴+x²+1 → bits 4,2,0 - CrcPoly::Crc5 => bits(&[4, 2, 0]), - // x⁶+x⁵+x⁴+x²+x+1 → bits 5,4,2,1,0 - CrcPoly::Crc6 => bits(&[5, 4, 2, 1, 0]), - // x⁷+x³+x+1 → bits 3,1,0 - CrcPoly::Crc7 => bits(&[3, 1, 0]), - // x⁸+x⁴+x³+x²+1 → bits 4,3,2,0 - CrcPoly::Crc8 => bits(&[4, 3, 2, 0]), - // x⁹+x⁴+x³+x²+x+1 → bits 4,3,2,1,0 - CrcPoly::Crc9 => bits(&[4, 3, 2, 1, 0]), - // x¹⁰+x⁹+x⁵+x⁴+x+1 → bits 9,5,4,1,0 - CrcPoly::Crc10 => bits(&[9, 5, 4, 1, 0]), - // x¹¹+x¹⁰+x⁹+x⁵+x+1 → bits 10,9,5,1,0 - CrcPoly::Crc11 => bits(&[10, 9, 5, 1, 0]), - // x¹²+x¹¹+x³+x²+x+1 → bits 11,3,2,1,0 - CrcPoly::Crc12 => bits(&[11, 3, 2, 1, 0]), - // x¹³+x¹²+x¹¹+x⁸+x⁷+x⁴+x²+1 → bits 12,11,8,7,4,2,0 - CrcPoly::Crc13 => bits(&[12, 11, 8, 7, 4, 2, 0]), - // x¹⁴+x¹³+x¹⁰+x⁵+x³+x+1 → bits 13,10,5,3,1,0 - CrcPoly::Crc14 => bits(&[13, 10, 5, 3, 1, 0]), - // x¹⁵+x¹⁴+x¹³+x¹⁰+x⁸+x⁵+x²+x+1 → bits 14,13,10,8,5,2,1,0 - CrcPoly::Crc15 => bits(&[14, 13, 10, 8, 5, 2, 1, 0]), - // x¹⁶+x¹⁵+x²+1 → bits 15,2,0 - CrcPoly::Crc16 => bits(&[15, 2, 0]), - // x²⁴+x²³+x⁶+x⁵+x+1 → bits 23,6,5,1,0 - CrcPoly::Crc24 => bits(&[23, 6, 5, 1, 0]), - // x³²+x²⁶+x²³+x²²+x¹⁶+x¹²+x¹¹+x¹⁰+x⁸+x⁷+x⁵+x⁴+x²+x+1 - // → bits 26,23,22,16,12,11,10,8,7,5,4,2,1,0 - CrcPoly::Crc32 => bits(&[26, 23, 22, 16, 12, 11, 10, 8, 7, 5, 4, 2, 1, 0]), - } - } - - /// Mask of the low `k` bits: `(1 << k) - 1`. - const fn mask(self) -> u64 { - let k = self.width(); - if k >= 64 { - u64::MAX - } else { - (1u64 << k) - 1 - } - } -} - -/// Build a generator bitmask from a list of present term exponents -/// (each `< k`). Used by [`CrcPoly::generator`]. -const fn bits(exponents: &[u32]) -> u64 { - let mut acc = 0u64; - let mut i = 0; - while i < exponents.len() { - acc |= 1u64 << exponents[i]; - i += 1; - } - acc -} - -/// Compute the §1.8.4.5 CRC over `message_bits`, MSB-first. -/// -/// `message_bits` is the protected bit sequence `M(x)` in transmission -/// order: `message_bits[0]` is the highest-order coefficient (the -/// first bit transmitted). The returned value is the `k`-bit -/// `crcCheckSum` exactly as it appears on the wire — the degree-`(k−1)` -/// remainder of `M(x)·xᵏ ÷ G(x)` with the normative final one's -/// complement applied ("written in a reversed manner, i. e. each bit -/// is inverted"). Only the low `poly.width()` bits are significant. -pub fn crc_bits(poly: CrcPoly, message_bits: &[bool]) -> u64 { - let k = poly.width(); - let gen = poly.generator(); - let mask = poly.mask(); - let top = 1u64 << (k - 1); - - // Standard MSB-first shift register: zero init, no input - // reflection. Feeding the message bits and then `k` implicit zero - // bits (the `·xᵏ` shift) leaves the remainder R(x) in `reg`. - let mut reg: u64 = 0; - for &bit in message_bits { - let high = (reg & top) != 0; - reg = (reg << 1) & mask; - if high { - reg ^= gen; - } - if bit { - reg ^= 1; // fold the incoming message bit into x⁰ - } - } - // Flush k zero bits so the register holds M(x)·xᵏ mod G(x). - for _ in 0..k { - let high = (reg & top) != 0; - reg = (reg << 1) & mask; - if high { - reg ^= gen; - } - } - - // §1.8.4.5: "The CRC bits are written in a reversed manner, i. e. - // each bit is inverted." One's-complement the k remainder bits. - (!reg) & mask -} - -/// Convenience wrapper: compute the §1.8.4.5 CRC over a whole-byte -/// `message`, MSB-first within each byte. -/// -/// Equivalent to [`crc_bits`] fed `message.len() * 8` bits in -/// big-endian bit order. -pub fn crc_bytes(poly: CrcPoly, message: &[u8]) -> u64 { - let k = poly.width(); - let gen = poly.generator(); - let mask = poly.mask(); - let top = 1u64 << (k - 1); - - let mut reg: u64 = 0; - for &byte in message { - for i in (0..8).rev() { - let bit = (byte >> i) & 1 != 0; - let high = (reg & top) != 0; - reg = (reg << 1) & mask; - if high { - reg ^= gen; - } - if bit { - reg ^= 1; - } - } - } - for _ in 0..k { - let high = (reg & top) != 0; - reg = (reg << 1) & mask; - if high { - reg ^= gen; - } - } - (!reg) & mask -} - -/// Compute the LATM `StreamMuxConfig()` `crcCheckSum` (§1.7.3.1, -/// Table 1.42) over the protected bit region. -/// -/// Per Table 1.42 the CRC "uses the generation polynomial CRC8, as -/// defined in subclause 1.8.4.5 and covers the entire -/// StreamMuxConfig() up to but excluding the crcCheckPresent bit". -/// `config_bits` must therefore be exactly that prefix of the -/// `StreamMuxConfig()` bitstream (from `audioMuxVersion` through the -/// last bit before `crcCheckPresent`), in transmission (MSB-first) -/// order. The returned 8-bit value is the on-wire `crcCheckSum`; a -/// decoder validates by comparing it for equality against the field -/// it read. -pub fn stream_mux_config_crc(config_bits: &[bool]) -> u8 { - crc_bits(CrcPoly::Crc8, config_bits) as u8 -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Reference long-division CRC: compute the remainder of - /// `M(x)·xᵏ ÷ G(x)` over GF(2) directly, with the full `(k+1)`-bit - /// generator (leading `xᵏ` term included). Independent of the - /// shift-register implementation in `crc_bits`, so it cross-checks - /// the register arithmetic against the textbook polynomial-division - /// definition from §1.8.4.5. Returns the pre-inversion remainder. - fn reference_remainder(poly: CrcPoly, message_bits: &[bool]) -> u64 { - let k = poly.width(); - let full_gen = poly.generator() | (1u64 << k); // include xᵏ - // Build the dividend M(x)·xᵏ as a big sequence of bits. - let mut dividend: Vec = message_bits.to_vec(); - dividend.extend(std::iter::repeat(false).take(k as usize)); - - // Long division over GF(2), MSB-first, tracking a window of the - // most recent (k+1) bits implicitly via a running register. - let mut reg: u64 = 0; - let topbit = 1u64 << k; - for &bit in ÷nd { - reg = (reg << 1) | (bit as u64); - if reg & topbit != 0 { - reg ^= full_gen; - } - } - reg & ((1u64 << k) - 1) - } - - fn to_bits(bytes: &[u8]) -> Vec { - let mut v = Vec::with_capacity(bytes.len() * 8); - for &b in bytes { - for i in (0..8).rev() { - v.push((b >> i) & 1 != 0); - } - } - v - } - - #[test] - fn generator_masks_match_spec_exponents() { - // Spot-check the headline polynomials against §1.8.4.5. - assert_eq!(CrcPoly::Crc8.generator(), 0b0001_1101); // x⁴+x³+x²+1 - assert_eq!(CrcPoly::Crc16.generator(), (1 << 15) | (1 << 2) | 1); - assert_eq!(CrcPoly::Crc4.generator(), 0b1101); // x³+x²+1 - // Every generator must fit within its width and carry the x⁰ - // term (all listed polynomials have a constant 1). - for p in [ - CrcPoly::Crc4, - CrcPoly::Crc5, - CrcPoly::Crc6, - CrcPoly::Crc7, - CrcPoly::Crc8, - CrcPoly::Crc9, - CrcPoly::Crc10, - CrcPoly::Crc11, - CrcPoly::Crc12, - CrcPoly::Crc13, - CrcPoly::Crc14, - CrcPoly::Crc15, - CrcPoly::Crc16, - CrcPoly::Crc24, - CrcPoly::Crc32, - ] { - assert!(p.generator() & 1 == 1, "{p:?} missing x⁰ term"); - assert!(p.generator() <= p.mask(), "{p:?} generator exceeds width"); - } - } - - #[test] - fn crc_bits_matches_reference_long_division() { - let messages: [&[u8]; 5] = [ - &[], - &[0x00], - &[0xFF], - &[0x12, 0x34, 0x56, 0x78], - &[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03], - ]; - for p in [ - CrcPoly::Crc4, - CrcPoly::Crc8, - CrcPoly::Crc12, - CrcPoly::Crc16, - CrcPoly::Crc24, - CrcPoly::Crc32, - ] { - let mask = p.mask(); - for m in messages { - let bits = to_bits(m); - let got = crc_bits(p, &bits); - let expect = (!reference_remainder(p, &bits)) & mask; - assert_eq!(got, expect, "poly {p:?} message {m:x?}"); - } - } - } - - #[test] - fn crc_bytes_agrees_with_crc_bits() { - let m: &[u8] = &[0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0xFF]; - for p in [CrcPoly::Crc8, CrcPoly::Crc16, CrcPoly::Crc32] { - assert_eq!(crc_bytes(p, m), crc_bits(p, &to_bits(m))); - } - } - - #[test] - fn inversion_is_present() { - // The spec mandates the output bits be inverted. For an empty - // message the pre-inversion remainder is 0, so the on-wire CRC - // must be all-ones within the width. - for p in [CrcPoly::Crc4, CrcPoly::Crc8, CrcPoly::Crc16] { - assert_eq!(crc_bits(p, &[]), p.mask()); - } - } - - #[test] - fn appending_crc_makes_codeword_divisible_modulo_inversion() { - // A defining property: M(x)·xᵏ + R(x) is divisible by G(x). - // We store the *inverted* R(x), so re-derive R(x) and verify - // the codeword M·xᵏ + R divides cleanly. - let m: &[u8] = &[0x53, 0x91, 0x2C]; - for p in [CrcPoly::Crc8, CrcPoly::Crc16] { - let mut bits = to_bits(m); - let on_wire = crc_bits(p, &bits); - let r = (!on_wire) & p.mask(); // undo inversion → true R(x) - // Append the k remainder bits (MSB-first) to the message. - for i in (0..p.width()).rev() { - bits.push((r >> i) & 1 != 0); - } - // The remainder of the full codeword ÷ G(x) must be zero. - assert_eq!(reference_remainder(p, &bits), 0, "poly {p:?}"); - } - } - - #[test] - fn stream_mux_config_crc_is_crc8() { - let bits = to_bits(&[0x00, 0x10, 0x07, 0x00]); - assert_eq!( - stream_mux_config_crc(&bits) as u64, - crc_bits(CrcPoly::Crc8, &bits) - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/decode.rs b/crates/vendor/oxideav-aac/src/decode.rs deleted file mode 100644 index 8507f13a..00000000 --- a/crates/vendor/oxideav-aac/src/decode.rs +++ /dev/null @@ -1,1305 +0,0 @@ -//! Stream-level ADTS decode driver — raw_data_block walk to interleaved -//! 16-bit PCM. -//! -//! [`crate::element_decode::ElementDecoder`] decodes *one* channel -//! element per call and carries that element's §4.6.11 overlap-add tail -//! across frames. This module is the layer above it: it walks the -//! §4.4.2.1 `raw_data_block()` of one ADTS frame -//! ([`crate::raw_data_block::Walker`]), dispatches each `id_syn_ele` -//! onto a per-element-slot [`ElementDecoder`] (keyed by `(syntactic -//! element id, element_instance_tag)` so each element's filterbank state -//! is independent), composes the channel-element bodies via -//! [`crate::ics_body`] / [`crate::spectral_data`], and renders the -//! frame's per-channel time signals to the element-order interleaved -//! 16-bit PCM layout via [`crate::pcm`]. -//! -//! Scope: AAC-LC (and the other General-Audio object types the -//! per-tool chain covers) carried in ADTS — including -//! multi-`raw_data_block` frames (each block renders one consecutive -//! 1024-sample hop) and the `error_check()` CRC layer (verified by -//! [`StreamDecoder::decode_adts_frame`] via [`crate::adts_crc`]) — -//! with the channel elements the staged-fixture encoders emit -//! (SCE / LFE / CPE, plus the consumed-and-ignored FIL / DSE / -//! PCE). A `coupling_channel_element()` (CCE) is parsed via -//! [`crate::cce::CouplingChannelElement`] **and applied**: the walk is -//! two-pass — every channel element of a block is parsed first, each -//! CCE's embedded `single_channel_element()` is decoded through its -//! per-instance-tag [`CceDecoder`] slot, and the §4.6.8.3.3 -//! `decode_coupling_channel()` target walk then injects the scaled -//! spectra (or, for an independently switched CCE, the time signal) -//! into the addressed SCE / CPE channels at the signalled `cc_domain` -//! stage. The CCE contributes no output channel of its own. SBR / PS -//! up-sampling ride the FIL extension walk (§4.6.18 back-end). -//! -//! ## Provenance -//! -//! The §4.4.2.1 `raw_data_block()` walk, the §4.4.2.3 `channel_pair_ -//! element()` `common_window` / `ms_mask_present` header, and the -//! §4.6.11 PCM output contract are from ISO/IEC 14496-3 / 13818-7 staged -//! under `docs/audio/aac/`. No part of the byte ordering or the element -//! dispatch comes from any external decoder. - -use std::collections::HashMap; - -use oxideav_core::bits::BitReader; - -use crate::adts::AdtsHeader; -use crate::asc::AacResilienceFlags; -use crate::cce::CouplingChannelElement; -use crate::channel_map::PceElementKind; -use crate::element_decode::{ - CceDecoder, ChannelInput, CouplingApply, CpeJointStereo, DecodedCce, ElementDecoder, -}; -use crate::extension_payload::{ExtensionPayload, ExtensionPayloadOrSbr}; -use crate::ics_body::IcsBody; -use crate::ics_info::IcsInfo; -use crate::ms_stereo::MsMaskPresent; -use crate::pce::Pce; -use crate::pcm::interleave_s16; -use crate::raw_data_block::{Element, IdSynEle, Walker}; -use crate::sbr_decoder::SbrDecoder; -use crate::sbr_extension::SbrExtensionData; -use crate::sbr_header::SbrHeader; -use crate::spectral_data::SpectralData; -use crate::swb_offset::FrameFamily; -use crate::{Error, Result}; - -/// Map a channel element's [`IdSynEle`] to its §8.5.2.2 PCE reference -/// kind. `None` for elements a PCE never addresses as an output -/// channel (CCE contributes no output channel here). -fn pce_kind(kind: IdSynEle) -> Option { - match kind { - IdSynEle::Sce => Some(PceElementKind::Sce), - IdSynEle::Cpe => Some(PceElementKind::Cpe), - IdSynEle::Lfe => Some(PceElementKind::Lfe), - _ => None, - } -} - -/// The §4.6.11 per-frame sample count for the default 1024-line -/// transform family. The other §4.5.1.1 families emit 960 / 512 / -/// 480 samples per frame per channel -/// ([`crate::swb_offset::FrameFamily::frame_len`]). -pub const FRAME_LEN: usize = 1024; - -/// One decoded ADTS frame: the interleaved 16-bit PCM plus the geometry -/// needed to interpret it. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DecodedFrame { - /// Interleaved 16-bit PCM, `channels` samples per time index. For a - /// default `channelConfiguration` (Table 1.19, values 1–6) the - /// channels are in the canonical [`crate::channel_map`] output order - /// (e.g. 5.1 is `L, R, C, LFE, Ls, Rs`); for the unmapped configs - /// (`0` PCE-defined, `7`) they stay in `raw_data_block` element order - /// (an SCE/LFE contributes one channel, a CPE two). Length is - /// `FRAME_LEN * channels` for the plain AAC path, or - /// `2 * FRAME_LEN * channels` once the stream is SBR-active - /// (HE-AAC dual-rate output; `FRAME_LEN * channels` again when - /// the §4.6.18.4.3 downsampled SBR mode is selected). - pub pcm: Vec, - /// Number of interleaved channels this frame produced. - pub channels: usize, - /// The frame's sampling rate in Hz: the ADTS-signalled core rate, - /// doubled once the stream is SBR-active (kept at the core rate - /// in the §4.6.18.4.3 downsampled SBR mode). - pub sample_rate: u32, -} - -/// Stateful whole-stream ADTS decoder. -/// -/// Holds one [`ElementDecoder`] per `(element-id, instance-tag)` slot so -/// every channel element's §4.6.11 overlap-add tail, §4.6.7 LTP history, -/// and §4.6.6 predictor state persist across the frames of the stream. -/// Construct one [`StreamDecoder`] per stream and feed it ADTS frames in -/// order via [`Self::decode_frame`], or hand it the whole byte buffer -/// via [`Self::decode_all`]. -#[derive(Debug, Default)] -pub struct StreamDecoder { - decoders: HashMap<(u8, u8), ElementDecoder>, - /// One §4.6.8.3.3 CCE decoder per coupling-element instance tag - /// (its independently-switched filterbank overlap and PNS state - /// persist across frames). - cce_decoders: HashMap, - /// One §4.6.18 SBR back-end per channel-element slot (HE-AAC). - sbr: HashMap<(u8, u8), SbrDecoder>, - /// The threaded previous `sbr_header()` per slot (the - /// `bs_header_flag == 0` reuse path). - sbr_prev_header: HashMap<(u8, u8), SbrHeader>, - /// Latched once any frame carries SBR data: from then on every - /// frame is emitted at the SBR output rate (doubled, or the core - /// rate in downsampled mode) — SBR-less frames go through the - /// §4.6.18.5 pure-upsampling path so the output rate never flaps. - sbr_active: bool, - /// §4.6.18.4.3 downsampled SBR output mode: SBR frames are - /// synthesized through the 32-channel bank and emitted at the - /// *core* rate (1024 samples per channel per block). Installed on - /// every SBR back-end this decoder creates - /// ([`Self::set_sbr_downsampled`]). - sbr_downsampled: bool, - /// §4.6.18.8 low-power SBR mode: real-valued filterbanks and the - /// LP adjustment chain on every SBR back-end this decoder creates - /// ([`Self::set_sbr_low_power`]). - sbr_low_power: bool, - /// The active `program_config_element()` for - /// `channelConfiguration == 0` streams — captured from an in-band - /// PCE (§8.5.2.2: it takes effect at the block carrying it and - /// persists) or installed by [`Self::set_program_config`] when the - /// PCE rides inline in an out-of-band `AudioSpecificConfig`. - program_config: Option, - /// The §4.5.1.1 frame-length family every block of this stream - /// decodes under. ADTS cannot signal anything but the 1024-line - /// family (the default); a LATM / raw caller with an - /// `AudioSpecificConfig` installs the ASC-resolved family via - /// [`Self::set_frame_family`] before the first block. - family: FrameFamily, -} - -impl StreamDecoder { - /// A fresh stream decoder with no element state. - #[must_use] - pub fn new() -> Self { - StreamDecoder::default() - } - - /// Install the program configuration of a - /// `channelConfiguration == 0` stream whose - /// `program_config_element()` rides *outside* the AAC payload — - /// inline in the `AudioSpecificConfig` (the MP4 / LATM case, - /// [`crate::asc::GaSpecificConfig::pce`]) or an `adif_header()`. - /// An in-band PCE inside a later `raw_data_block()` replaces it - /// (§8.5.2.2 persistence). The active PCE drives the §8.5.2.2 - /// element→speaker canonical output reorder; without one, a - /// config-0 stream is emitted in bitstream element order. - pub fn set_program_config(&mut self, pce: Pce) { - self.program_config = Some(pce); - } - - /// Select the §4.6.18.4.3 downsampled SBR output mode: every SBR - /// back-end runs the 32-channel synthesis bank, so an SBR-active - /// stream is emitted at the *core* sampling rate (1024 samples per - /// channel per block) instead of the doubled `fs_sbr` rate. The - /// reconstructed SBR bands below the core Nyquist are kept; the - /// range above it is discarded by construction. An explicitly - /// signalled `AudioSpecificConfig` whose `extensionSamplingFrequency` - /// equals the core rate is the in-band request for this mode - /// (§4.6.18.2.6, `FsSBR` definition). - /// - /// Select the mode before decoding: back-ends already created for - /// earlier frames keep their rate (the QMF history is - /// rate-specific). - pub fn set_sbr_downsampled(&mut self, downsampled: bool) { - self.sbr_downsampled = downsampled; - } - - /// Install the §4.5.1.1 frame-length family (from - /// `GASpecificConfig.frameLengthFlag` + the AOT) for every later - /// block. Affects the SWB tables, transform lengths and the - /// per-frame PCM sample count (1024 / 960 / 512 / 480). ADTS - /// cannot signal anything but the default 1024-line family; a - /// LATM / raw caller with an `AudioSpecificConfig` selects the - /// ASC-resolved family before the first block (the per-element - /// state is keyed to the family at slot creation). - pub fn set_frame_family(&mut self, family: FrameFamily) { - self.family = family; - } - - /// The active §4.5.1.1 frame-length family. - pub fn frame_family(&self) -> FrameFamily { - self.family - } - - /// Select the §4.6.18.8 low-power SBR mode: every SBR back-end - /// runs the real-valued filterbanks with the LP adjustment chain - /// (×2 energy estimation, aliasing detection/reduction, modified - /// sinusoid injection, no gain smoothing). Composable with - /// [`Self::set_sbr_downsampled`]. An HE-AAC v2 (PS) stream is - /// rejected in this mode ([`crate::Error::SbrLowPowerPs`]) — the - /// subpart-8 tool needs the complex QMF domain. Select before - /// decoding. - pub fn set_sbr_low_power(&mut self, low_power: bool) { - self.sbr_low_power = low_power; - } - - /// Decode one ADTS frame's `raw_data_block()` payload to interleaved - /// 16-bit PCM. - /// - /// `header` is the parsed [`AdtsHeader`]; `payload` is the - /// `raw_data_block()` bytes (the frame body *after* the - /// fixed/variable header and the optional CRC — i.e. starting at the - /// header's `payload_offset`). The channel elements update this - /// decoder's per-slot state, so frames must be fed in stream order. - /// - /// A frame that yields no channel element (e.g. fill-only) returns a - /// [`DecodedFrame`] with `channels == 0` and an empty `pcm`. - pub fn decode_frame(&mut self, header: &AdtsHeader, payload: &[u8]) -> Result { - self.decode_raw_data_block( - header.audio_object_type(), - header.sampling_frequency_index, - header.sample_rate(), - header.channel_configuration, - header.number_of_raw_data_blocks_in_frame, - payload, - ) - } - - /// Decode one `raw_data_block()` payload to interleaved 16-bit PCM, - /// driven by an explicit `(audioObjectType, samplingFrequencyIndex, - /// sampleRate)` configuration rather than an ADTS header. - /// - /// This is the transport-independent core that [`Self::decode_frame`] - /// (ADTS) and the LATM/LOAS driver - /// ([`crate::latm::LoasDecoder`]) both call: each recovers the AAC - /// configuration from its own framing (the ADTS fixed header, or the - /// LATM `AudioSpecificConfig`) and hands the same §4.4.2.1 - /// `raw_data_block()` bytes here. `aot` is the §1.6.2.1 - /// `audioObjectType` (already escaped past the ADTS `profile + 1` - /// adjustment), `fs_index` is the Table 1.18 - /// `samplingFrequencyIndex`, `sample_rate` is the resolved rate the - /// returned [`DecodedFrame`] reports, `channel_configuration` is the - /// Table 1.19 default-layout selector that drives the §1.6.3.5 - /// element→speaker output reorder (see [`crate::channel_map`]), and - /// `num_raw_data_blocks` is the resolved block count `N` (ADTS carries - /// `N - 1`; LATM carries one block per payload, i.e. `N == 1`). - pub fn decode_raw_data_block( - &mut self, - aot: u8, - fs_index: u8, - sample_rate: u32, - channel_configuration: u8, - num_raw_data_blocks: u8, - payload: &[u8], - ) -> Result { - let fs = fs_index; - let family = self.family; - let mut reader = BitReader::new(payload); - - // Per channel-element outputs in element order: the decoded - // core time signals plus any SBR extension payload that - // followed the element in a FIL. - struct ElementOut { - key: (u8, u8), - kind: IdSynEle, - channels: Vec>, - sbr: Option>, - } - // A channel element parsed off the bitstream but not yet - // decoded. Decoding is deferred until the whole block is - // walked so §4.6.8.3.3 coupling channel elements — which may - // appear before or after the SCE / CPE targets they address — - // can contribute at the right stage of every target's chain. - struct ParsedSce { - body: IcsBody, - ics: IcsInfo, - spectral: SpectralData, - } - enum ParsedChannel { - Single(Box), - Pair(Box), - } - struct PendingElement { - key: (u8, u8), - kind: IdSynEle, - block: u8, - parsed: ParsedChannel, - sbr: Option>, - } - let mut pending: Vec = Vec::new(); - let mut cces: Vec<(u8, CouplingChannelElement)> = Vec::new(); - let fs_sbr = sample_rate.saturating_mul(2); - - // `num_raw_data_blocks` is the resolved count `N`. The walker - // returns `None` when the payload is exhausted before an explicit - // END (real-world encoders pad the frame but do not always - // round-trip a trailing END marker after the last element); treat - // that as end-of-block, the same as an `Element::End`. - 'blocks: for block in 0..num_raw_data_blocks { - while let Some(elem) = Walker::new(&mut reader).next_element_keep_fill()? { - match elem { - Element::ChannelElement { - kind: kind @ (IdSynEle::Sce | IdSynEle::Lfe), - element_instance_tag, - } => { - let body = IcsBody::parse_family(&mut reader, family, aot, fs, false)?; - let ics = body.ics_info.clone().ok_or(Error::ElementDecodeInvalid)?; - let spectral = - SpectralData::parse(&mut reader, &ics, &body.section_data, fs)?; - pending.push(PendingElement { - key: (kind_id(kind), element_instance_tag), - kind, - block, - parsed: ParsedChannel::Single(Box::new(ParsedSce { - body, - ics, - spectral, - })), - sbr: None, - }); - } - Element::ChannelElement { - kind: IdSynEle::Cpe, - element_instance_tag, - } => { - let parsed = parse_cpe_family(&mut reader, family, aot, fs)?; - pending.push(PendingElement { - key: (kind_id(IdSynEle::Cpe), element_instance_tag), - kind: IdSynEle::Cpe, - block, - parsed: ParsedChannel::Pair(Box::new(parsed)), - sbr: None, - }); - } - Element::ChannelElement { - kind: IdSynEle::Cce, - element_instance_tag, - } => { - // §4.6.8.3 / Table 4.8: parse the whole coupling - // channel element (header + embedded - // single_channel_element + gain lists). Its - // embedded spectrum is decoded once per block - // below and coupled onto the addressed SCE / CPE - // targets per §4.6.8.3.3. - let cce = CouplingChannelElement::parse_after_tag_family( - &mut reader, - family, - element_instance_tag, - aot, - fs, - )?; - cces.push((block, cce)); - } - Element::ChannelElement { kind, .. } => { - // Any other channel-element id has no decode path. - return Err(unsupported_element(kind)); - } - Element::Fill { payload_bytes } => { - // The FIL body was left unconsumed: walk the - // Table 4.51 extension_payload() chain, routing - // any SBR payload onto the preceding channel - // element (§4.4.2.7: an SBR FIL directly follows - // the SCE/CPE it extends). - let target = pending - .last() - .filter(|el| matches!(el.kind, IdSynEle::Sce | IdSynEle::Cpe)) - .map(|el| (el.kind, el.key)); - if let Some(ext) = - self.consume_fill(&mut reader, payload, payload_bytes, fs_sbr, target)? - { - if let Some(el) = pending.last_mut() { - el.sbr = Some(ext); - } - } - } - Element::Data { .. } => {} - Element::ProgramConfig(pce) => { - // §8.5.2.2: the configuration takes effect at - // the raw_data_block() containing the PCE and - // persists until a new PCE arrives. - self.program_config = Some(pce); - } - Element::End => continue 'blocks, - } - } - } - - // §4.6.8.3.3 — decode each CCE's embedded - // single_channel_element() into its cc_spectrum (and, for an - // independently switched CCE, its time signal), through the - // per-instance-tag persistent CCE decoder slot. - let mut decoded_cces: Vec = Vec::with_capacity(cces.len()); - for (_, cce) in &cces { - let dec = self - .cce_decoders - .entry(cce.element_instance_tag) - .or_insert_with(|| CceDecoder::new_family(family)); - decoded_cces.push(dec.decode(cce, aot, fs)?); - } - - // Decode the pending channel elements in element order, with - // each channel's coupling contributions injected at the - // §4.6.8.3.3 cc_domain stage. The elements stay tagged with - // their raw_data_block index: a multi-RDB ADTS frame carries N - // *consecutive* 1024-sample blocks of the same program, so - // each block renders its own channel set and the per-block PCM - // is concatenated in time below. - let mut elements: Vec<(u8, ElementOut)> = Vec::new(); - for pe in pending { - let channels = match &pe.parsed { - ParsedChannel::Single(sce) => { - let coupling = - coupling_for(&cces, &decoded_cces, pe.block, pe.kind, pe.key.1, 0); - let ch = ChannelInput { - body: &sce.body, - ics_info: &sce.ics, - spectral: &sce.spectral, - }; - let dec = self - .decoders - .entry(pe.key) - .or_insert_with(|| ElementDecoder::new_family(family)); - vec![dec.decode_sce_coupled(&ch, aot, fs, &coupling)?] - } - ParsedChannel::Pair(cpe) => { - let left_coupling = - coupling_for(&cces, &decoded_cces, pe.block, pe.kind, pe.key.1, 0); - let right_coupling = - coupling_for(&cces, &decoded_cces, pe.block, pe.kind, pe.key.1, 1); - let (left, right, joint) = cpe.channel_inputs(); - let dec = self - .decoders - .entry(pe.key) - .or_insert_with(|| ElementDecoder::new_family(family)); - let (l, r) = dec.decode_cpe_coupled( - &left, - &right, - joint, - aot, - fs, - &left_coupling, - &right_coupling, - )?; - vec![l, r] - } - }; - elements.push(( - pe.block, - ElementOut { - key: pe.key, - kind: pe.kind, - channels, - sbr: pe.sbr, - }, - )); - } - - // HE-AAC: once any frame carries SBR data the stream is emitted - // at the doubled rate; frames without SBR go through the pure - // upsampling path so the rate never flaps. - if elements.iter().any(|(_, e)| e.sbr.is_some()) { - self.sbr_active = true; - } - let out_rate = if self.sbr_active && !self.sbr_downsampled { - fs_sbr - } else { - sample_rate - }; - - // Render block by block; each block contributes one hop of - // interleaved PCM (all blocks of a frame must agree on the - // channel count). - let mut pcm: Vec = Vec::new(); - let mut frame_channels: Option = None; - for block in 0..num_raw_data_blocks { - let mut channels: Vec> = Vec::new(); - // Per decoded element: (kind, instance tag, contributed - // channel count) — the descriptor list the §8.5.2.2 PCE - // reorder keys on for `channelConfiguration == 0`. - let mut element_desc: Vec<(PceElementKind, u8, usize)> = Vec::new(); - for (_, el) in elements.iter().filter(|(b, _)| *b == block) { - if self.sbr_active { - let n_ch = el.channels.len(); - let dec = match self.sbr.entry(el.key) { - std::collections::hash_map::Entry::Occupied(e) => e.into_mut(), - std::collections::hash_map::Entry::Vacant(v) => { - let mut d = SbrDecoder::new(fs_sbr, n_ch)?; - d.set_downsampled(self.sbr_downsampled)?; - d.set_low_power(self.sbr_low_power)?; - v.insert(d) - } - }; - let core: Vec<&[f64]> = el.channels.iter().map(Vec::as_slice).collect(); - let up = match &el.sbr { - Some(ext) => dec.process_frame(ext, &core)?, - None => dec.upsample_frame(&core)?, - }; - if let Some(kind) = pce_kind(el.kind) { - element_desc.push((kind, el.key.1, up.len())); - } - channels.extend(up); - } else { - if let Some(kind) = pce_kind(el.kind) { - element_desc.push((kind, el.key.1, el.channels.len())); - } - channels.extend(el.channels.iter().cloned()); - } - } - - // §1.6.3.5 / Table 1.19: a default `channelConfiguration` - // (1–7) fixes which loudspeaker each decoded element feeds. - // Reorder the element-order channel buffers into the - // canonical interleaved layout (a no-op for mono/stereo). A - // `channelConfiguration == 0` block is reordered by the - // active §8.5.2.2 PCE instead, when one is installed and it - // maps onto canonical positions; otherwise element order is - // kept. - let channels = - if channel_configuration == 0 { - match self.program_config.as_ref().and_then(|pce| { - crate::channel_map::pce_reorder_permutation(pce, &element_desc) - }) { - Some(perm) => crate::channel_map::apply_permutation(&perm, channels), - None => channels, - } - } else { - crate::channel_map::reorder_channels(channel_configuration, channels) - }; - - match frame_channels { - None => frame_channels = Some(channels.len()), - Some(n) if n != channels.len() => { - // The blocks of one ADTS frame carry the same - // program; a channel-count flip mid-frame is - // structurally inconsistent. - return Err(Error::ElementDecodeInvalid); - } - Some(_) => {} - } - pcm.extend(interleave_s16(&channels)?); - } - - Ok(DecodedFrame { - pcm, - channels: frame_channels.unwrap_or(0), - sample_rate: out_rate, - }) - } - - /// Walk a FIL element's Table 4.51 `extension_payload()` chain - /// (the body was left unconsumed by - /// [`Walker::next_element_keep_fill`]). `payload` is the byte - /// buffer `reader` was constructed over (needed to recompute the - /// §4.4.2.8.1 SBR CRC over its coverage region); `target` is the - /// preceding SCE / CPE this FIL would extend (its `id_syn_ele` + - /// slot key), or `None` when the FIL follows no channel element. - /// Returns the decoded SBR payload, if any; the threaded - /// `sbr_header()` reuse state is updated per slot. An - /// `EXT_SBR_DATA_CRC` payload whose recomputed CRC-10 disagrees - /// with the transmitted `bs_sbr_crc_bits` is rejected with - /// [`Error::SbrCrcMismatch`]. - fn consume_fill( - &mut self, - reader: &mut BitReader<'_>, - payload: &[u8], - payload_bytes: u32, - fs_sbr: u32, - target: Option<(IdSynEle, (u8, u8))>, - ) -> Result>> { - let mut remaining = payload_bytes; - let mut result = None; - while remaining > 0 { - match target { - None => { - // No preceding channel element: only the non-SBR - // payload types are meaningful here. - let p = ExtensionPayload::parse(reader, remaining)?; - let n = p.byte_length().max(1); - remaining = remaining.saturating_sub(n); - } - Some(_) if self.family != FrameFamily::Lc1024 => { - // The §4.6.18 SBR tool in this crate is defined - // over the 1024-line core frame (32-subband - // analysis / 2048-sample output); a 960-line or - // LD core cannot feed it, so an SBR extension - // type is rejected before its body is even - // parsed. Non-SBR payload types stay usable. - match ExtensionPayload::parse(reader, remaining) { - Ok(p) => { - let n = p.byte_length().max(1); - remaining = remaining.saturating_sub(n); - } - Err(Error::UnsupportedExtensionSbr(_)) => { - return Err(Error::SbrUnsupportedFrameFamily); - } - Err(e) => return Err(e), - } - } - Some((id_aac, slot)) => { - let prev = self.sbr_prev_header.get(&slot).copied(); - match ExtensionPayload::parse_with_sbr(reader, remaining, id_aac, fs_sbr, prev)? - { - ExtensionPayloadOrSbr::Payload(p) => { - let n = p.byte_length().max(1); - remaining = remaining.saturating_sub(n); - } - ExtensionPayloadOrSbr::Sbr(ext) => { - ext.verify_crc(payload)?; - self.sbr_prev_header.insert(slot, ext.header); - result = Some(ext); - remaining = 0; - } - ExtensionPayloadOrSbr::SbrPreHeader { crc, crc_region } => { - // §4.5.2.8.1: SBR payloads before the first - // sbr_header() — verify the CRC over the - // whole-payload region, then run upsampling - // and delay adjustment only (the None SBR - // slot below selects the §4.6.18.5 pure - // upsampling path). No header is threaded. - if let (Some(crc), Some((s, e))) = (crc, crc_region) { - if crate::adts_crc::sbr_crc(payload, s, e) != crc { - return Err(Error::SbrCrcMismatch); - } - } - self.sbr_active = true; - remaining = 0; - } - } - } - } - } - Ok(result) - } - - /// Decode one whole ADTS frame — fixed/variable header, the - /// optional `error_check()` CRC layer, and the `raw_data_block()` - /// payload(s) — to interleaved 16-bit PCM. - /// - /// `frame` must start at the ADTS syncword and carry at least - /// `aac_frame_length` bytes (trailing bytes are ignored). Unlike - /// [`Self::decode_frame`] (which receives the payload with the CRC - /// layer already stripped and therefore cannot verify it), this - /// entry point *verifies* the ISO/IEC 13818-7:2004 §8.1.1 CRCs - /// when `protection_absent == 0`: - /// - /// * single raw data block — the Table 1.A.8 `adts_error_check()` - /// 16-bit `crc_check` over the 56 header bits plus every - /// §8.1.1.1 protected element region; - /// * multiple raw data blocks — the Table 1.A.9 - /// `adts_header_error_check()` (headers + the 16-bit - /// `raw_data_block_position` table) followed by one Table 1.A.10 - /// `adts_raw_data_block_error_check()` per block, each read from - /// its byte-aligned slot after the block it protects. - /// - /// A mismatch surfaces [`Error::AdtsCrcMismatch`] before any - /// decoder state is touched. - pub fn decode_adts_frame(&mut self, frame: &[u8]) -> Result { - let (header, payload_offset) = AdtsHeader::parse(frame)?; - let frame_len = header.aac_frame_length as usize; - if frame_len < payload_offset || frame.len() < frame_len { - return Err(Error::UnexpectedEnd); - } - let frame = &frame[..frame_len]; - if header.protection_absent { - return self.decode_frame(&header, &frame[payload_offset..]); - } - let aot = header.audio_object_type(); - let fs = header.sampling_frequency_index; - if header.number_of_raw_data_blocks_in_frame == 1 { - // Table 1.A.8 adts_error_check(): one 16-bit crc_check at - // bytes 7..9 covering headers + the block's regions. - let crc = u16::from_be_bytes([frame[7], frame[8]]); - let payload = &frame[crate::adts::ADTS_HEADER_BYTES_WITH_CRC..]; - let mut reader = BitReader::new(payload); - let regions = crate::adts_crc::collect_block_regions(&mut reader, aot, fs)?; - if crate::adts_crc::adts_single_crc(&frame[..7], payload, ®ions) != crc { - return Err(Error::AdtsCrcMismatch); - } - return self.decode_frame(&header, payload); - } - // Multi-RDB form (Tables 1.A.9 / 1.A.10): N − 1 16-bit - // raw_data_block_position entries + the 16-bit header CRC, - // then each raw_data_block() followed by its own 16-bit CRC. - let n = usize::from(header.number_of_raw_data_blocks_in_frame); - let after_positions = 7 + 2 * (n - 1); - if frame.len() < after_positions + 2 { - return Err(Error::UnexpectedEnd); - } - let positions: Vec = (0..n - 1) - .map(|i| u16::from_be_bytes([frame[7 + 2 * i], frame[8 + 2 * i]])) - .collect(); - let header_crc = u16::from_be_bytes([frame[after_positions], frame[after_positions + 1]]); - if crate::adts_crc::adts_header_crc(&frame[..7], &positions) != header_crc { - return Err(Error::AdtsCrcMismatch); - } - let payload = &frame[after_positions + 2..]; - let mut reader = BitReader::new(payload); - // Verify each block's CRC, splicing the CRC fields out so the - // block walk below sees the contiguous raw_data_block() - // sequence it expects. - let mut clean = Vec::with_capacity(payload.len()); - for _ in 0..n { - let start_byte = (reader.bit_position() / 8) as usize; - let regions = crate::adts_crc::collect_block_regions(&mut reader, aot, fs)?; - let end_bit = reader.bit_position(); - if end_bit % 8 != 0 { - // A block that did not end on its §4.4.2.1 - // byte_alignment() cannot be followed by the - // byte-aligned CRC slot. - return Err(Error::UnexpectedEnd); - } - let rdb_crc = reader.read_u32(16).map_err(|_| Error::UnexpectedEnd)? as u16; - if crate::adts_crc::adts_rdb_crc(payload, ®ions) != rdb_crc { - return Err(Error::AdtsCrcMismatch); - } - clean.extend_from_slice(&payload[start_byte..(end_bit / 8) as usize]); - } - self.decode_raw_data_block( - aot, - fs, - header.sample_rate(), - header.channel_configuration, - header.number_of_raw_data_blocks_in_frame, - &clean, - ) - } - - /// Decode a whole raw-ADTS byte buffer to a vector of per-frame - /// interleaved PCM. - /// - /// Skips a leading ID3v2 tag if present, then walks consecutive ADTS - /// frames (`aac_frame_length`-delimited) to exhaustion, verifying - /// the `error_check()` CRC layer of every `protection_absent == 0` - /// frame (see [`Self::decode_adts_frame`]). A truncated trailing - /// frame (fewer bytes than its `aac_frame_length`) is rejected with - /// [`Error::UnexpectedEnd`]. - pub fn decode_all(&mut self, data: &[u8]) -> Result> { - let data = skip_id3v2(data); - let mut frames = Vec::new(); - let mut pos = 0usize; - while pos + crate::adts::ADTS_HEADER_BYTES_NO_CRC <= data.len() { - let (header, payload_offset) = AdtsHeader::parse(&data[pos..])?; - let frame_len = header.aac_frame_length as usize; - if frame_len < payload_offset || pos + frame_len > data.len() { - return Err(Error::UnexpectedEnd); - } - frames.push(self.decode_adts_frame(&data[pos..pos + frame_len])?); - pos += frame_len; - } - Ok(frames) - } - - /// Decode one §4.4.2.3 Table 4.19 `er_raw_data_block()` payload - /// (the ER General-Audio top-level payload) to interleaved 16-bit - /// PCM. - /// - /// The ER object types do not use the tagged `raw_data_block()` - /// element walk: the channel-element sequence is fixed by - /// `channelConfiguration` (1..=7). Each element body is parsed - /// through the error-resilient Table 4.50 branches selected by the - /// ASC's [`AacResilienceFlags`] triplet, and — when - /// `aacSpectralDataResilienceFlag` is set — the spectrum arrives - /// as the two HCR length fields plus the - /// `reordered_spectral_data()` payload decoded by - /// [`crate::hcr_decode::decode_reordered_spectral_data`]. - /// - /// Scope: the ER AAC LC (AOT 17), ER AAC LTP (AOT 19) and ER AAC - /// LD (AOT 23) object types — the three §4.4.2.3 Table 4.19 - /// payloads. ER AAC scalable (AOT 20) rides its own layered - /// `aac_scalable_main_element()` walk (see [`crate::scalable`]) - /// and is rejected here with [`Error::NotImplemented`]. For - /// AOT 19 the §4.6.7 LTP tool is live: `ics_info()` carries the - /// Table 4.55 non-LD `ltp_data()` branch (11-bit lag, `M = 0`), - /// and the per-element [`crate::element_decode::ElementDecoder`] - /// slots persist the §4.6.7.3 `x_rec` reconstruction history - /// across frames exactly as the non-ER AOT-4 walk does. The - /// trailing - /// `extension_payload()` loop is consumed permissively (ignored), - /// matching the FIL handling of the non-ER walk; `epConfig` 2 / 3 - /// physical-payload preprocessing (§4.5.2.4) is out of scope (the - /// ASC parser already rejects those configurations). - pub fn decode_er_raw_data_block( - &mut self, - aot: u8, - fs_index: u8, - sample_rate: u32, - channel_configuration: u8, - resilience: AacResilienceFlags, - payload: &[u8], - ) -> Result { - // AOT 17 (ER AAC LC), AOT 19 (ER AAC LTP) and AOT 23 (ER AAC - // LD) share the Table 4.19 er_raw_data_block(). LD differs in - // the 512/480-line frame family this decoder was configured - // with (§4.6.17) and its delta-coded ltp_data() branch; AOT 19 - // adds the plain §4.6.7 LTP tool (Table 4.55 non-LD branch) - // whose per-element reconstruction history the decoder slots - // below already thread. ER AAC scalable (AOT 20) uses the - // layered aac_scalable_main_element() walk instead and stays - // out of this entry point. - if aot != 17 && aot != 19 && aot != 23 { - return Err(Error::NotImplemented); - } - // An LD stream must run an LD family and vice versa — a - // mismatch means the caller never installed the ASC-resolved - // family, which would silently mis-decode every band. - if (aot == 23) != self.family.is_ld() { - return Err(Error::ElementDecodeInvalid); - } - let fs = fs_index; - let family = self.family; - // Table 4.19: the fixed element sequence per channelConfiguration. - let sequence: &[IdSynEle] = match channel_configuration { - 1 => &[IdSynEle::Sce], - 2 => &[IdSynEle::Cpe], - 3 => &[IdSynEle::Sce, IdSynEle::Cpe], - 4 => &[IdSynEle::Sce, IdSynEle::Cpe, IdSynEle::Sce], - 5 => &[IdSynEle::Sce, IdSynEle::Cpe, IdSynEle::Cpe], - 6 => &[IdSynEle::Sce, IdSynEle::Cpe, IdSynEle::Cpe, IdSynEle::Lfe], - 7 => &[ - IdSynEle::Sce, - IdSynEle::Cpe, - IdSynEle::Cpe, - IdSynEle::Cpe, - IdSynEle::Lfe, - ], - _ => return Err(Error::ElementDecodeInvalid), - }; - - let mut reader = BitReader::new(payload); - let mut channels: Vec> = Vec::new(); - for &kind in sequence { - let element_instance_tag = reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; - let key = (kind_id(kind), element_instance_tag); - match kind { - IdSynEle::Sce | IdSynEle::Lfe => { - let body = - IcsBody::parse_er_family(&mut reader, family, aot, fs, false, resilience)?; - let ics = body.ics_info.clone().ok_or(Error::ElementDecodeInvalid)?; - let spectral = - parse_er_spectral(&mut reader, &body, &ics, fs, resilience, false)?; - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - let dec = self - .decoders - .entry(key) - .or_insert_with(|| ElementDecoder::new_family(family)); - channels.push(dec.decode_sce(&ch, aot, fs)?); - } - IdSynEle::Cpe => { - let common_window = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let dec_out = if common_window { - // §4.4.2.3 shared ics_info + Table 4.4 ms_mask. - let ics = IcsInfo::parse_family(&mut reader, family, aot, fs, true)?; - let ms_bits = reader.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; - let ms_mask_present = MsMaskPresent::from_bits(ms_bits)?; - let mut ms_used: Vec> = Vec::new(); - if ms_mask_present == MsMaskPresent::Mask { - for _g in 0..usize::from(ics.num_window_groups) { - let mut row = Vec::with_capacity(usize::from(ics.max_sfb)); - for _sfb in 0..usize::from(ics.max_sfb) { - row.push(reader.read_bit().map_err(|_| Error::UnexpectedEnd)?); - } - ms_used.push(row); - } - } - let left_body = IcsBody::parse_with_ics_info_er( - &mut reader, - &ics, - aot, - false, - resilience, - )?; - let left_spectral = - parse_er_spectral(&mut reader, &left_body, &ics, fs, resilience, true)?; - let right_body = IcsBody::parse_with_ics_info_er( - &mut reader, - &ics, - aot, - false, - resilience, - )?; - let right_spectral = parse_er_spectral( - &mut reader, - &right_body, - &ics, - fs, - resilience, - true, - )?; - let left = ChannelInput { - body: &left_body, - ics_info: &ics, - spectral: &left_spectral, - }; - let right = ChannelInput { - body: &right_body, - ics_info: &ics, - spectral: &right_spectral, - }; - let joint = CpeJointStereo { - ms_mask_present, - ms_used, - }; - let dec = self - .decoders - .entry(key) - .or_insert_with(|| ElementDecoder::new_family(family)); - dec.decode_cpe(&left, &right, &joint, aot, fs)? - } else { - let left_body = IcsBody::parse_er_family( - &mut reader, - family, - aot, - fs, - false, - resilience, - )?; - let left_ics = left_body - .ics_info - .clone() - .ok_or(Error::ElementDecodeInvalid)?; - let left_spectral = parse_er_spectral( - &mut reader, - &left_body, - &left_ics, - fs, - resilience, - true, - )?; - let right_body = IcsBody::parse_er_family( - &mut reader, - family, - aot, - fs, - false, - resilience, - )?; - let right_ics = right_body - .ics_info - .clone() - .ok_or(Error::ElementDecodeInvalid)?; - let right_spectral = parse_er_spectral( - &mut reader, - &right_body, - &right_ics, - fs, - resilience, - true, - )?; - let left = ChannelInput { - body: &left_body, - ics_info: &left_ics, - spectral: &left_spectral, - }; - let right = ChannelInput { - body: &right_body, - ics_info: &right_ics, - spectral: &right_spectral, - }; - let dec = self - .decoders - .entry(key) - .or_insert_with(|| ElementDecoder::new_family(family)); - dec.decode_cpe(&left, &right, &CpeJointStereo::default(), aot, fs)? - }; - channels.push(dec_out.0); - channels.push(dec_out.1); - } - _ => return Err(Error::ElementDecodeInvalid), - } - } - // Trailing extension_payload() loop + byte_alignment(): consumed - // permissively (nothing this decoder acts on rides there yet). - - // Table 1.19 canonical output reorder, same as the non-ER walk. - let channels = crate::channel_map::reorder_channels(channel_configuration, channels); - let pcm = interleave_s16(&channels)?; - Ok(DecodedFrame { - pcm, - channels: channels.len(), - sample_rate, - }) - } - - // (the per-CPE parse lives in the free `parse_cpe` below so the - // two-pass §4.6.8.3.3 coupling walk can defer decoding) -} - -/// A parsed `channel_pair_element()` awaiting decode: both channels' -/// bodies + spectra and the Table 4.4 joint-stereo header. For the -/// `common_window == 1` form the shared `ics_info` is cloned into both -/// per-channel slots (the clone carries `ltp_data_pair`, so the -/// channel-1 LTP selection is unaffected). -pub(crate) struct ParsedCpe { - joint: CpeJointStereo, - left_body: IcsBody, - left_ics: IcsInfo, - left_spectral: SpectralData, - right_body: IcsBody, - right_ics: IcsInfo, - right_spectral: SpectralData, - /// Absolute bit position (in the reader's buffer) where the second - /// `individual_channel_stream()` begins — the anchor of the - /// 13818-7:2004 §8.1.1.1 128-bit second-ICS ADTS-CRC region. - pub(crate) second_ics_start_bit: u64, -} - -impl ParsedCpe { - /// Borrow the two [`ChannelInput`]s plus the joint-stereo header. - fn channel_inputs(&self) -> (ChannelInput<'_>, ChannelInput<'_>, &CpeJointStereo) { - ( - ChannelInput { - body: &self.left_body, - ics_info: &self.left_ics, - spectral: &self.left_spectral, - }, - ChannelInput { - body: &self.right_body, - ics_info: &self.right_ics, - spectral: &self.right_spectral, - }, - &self.joint, - ) - } -} - -/// Parse one CPE body (after the walker consumed its element-instance -/// tag): the §4.4.2.3 `common_window` fork, the Table 4.4 -/// `ms_mask_present` / `ms_used` joint-stereo header (shared form), and -/// both channels' `individual_channel_stream()` + `spectral_data()`. -pub(crate) fn parse_cpe(reader: &mut BitReader<'_>, aot: u8, fs: u8) -> Result { - parse_cpe_family(reader, FrameFamily::Lc1024, aot, fs) -} - -/// [`parse_cpe`] under an explicit §4.5.1.1 frame-length family. -pub(crate) fn parse_cpe_family( - reader: &mut BitReader<'_>, - family: FrameFamily, - aot: u8, - fs: u8, -) -> Result { - let common_window = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - if common_window { - // §4.4.2.3: shared ics_info, then the Table 4.4 ms_mask. - let ics = IcsInfo::parse_family(reader, family, aot, fs, true)?; - let ms_bits = reader.read_u32(2).map_err(|_| Error::UnexpectedEnd)? as u8; - let ms_mask_present = MsMaskPresent::from_bits(ms_bits)?; - let mut ms_used: Vec> = Vec::new(); - if ms_mask_present == MsMaskPresent::Mask { - for _g in 0..usize::from(ics.num_window_groups) { - let mut row = Vec::with_capacity(usize::from(ics.max_sfb)); - for _sfb in 0..usize::from(ics.max_sfb) { - row.push(reader.read_bit().map_err(|_| Error::UnexpectedEnd)?); - } - ms_used.push(row); - } - } - let left_body = IcsBody::parse_with_ics_info(reader, &ics, aot, false)?; - let left_spectral = SpectralData::parse(reader, &ics, &left_body.section_data, fs)?; - let second_ics_start_bit = reader.bit_position(); - let right_body = IcsBody::parse_with_ics_info(reader, &ics, aot, false)?; - let right_spectral = SpectralData::parse(reader, &ics, &right_body.section_data, fs)?; - Ok(ParsedCpe { - joint: CpeJointStereo { - ms_mask_present, - ms_used, - }, - left_body, - left_ics: ics.clone(), - left_spectral, - right_body, - right_ics: ics, - right_spectral, - second_ics_start_bit, - }) - } else { - // Non-shared CPE: each channel carries its own ics_info; no - // M/S mask, so the joint-stereo tools do not run. - let left_body = IcsBody::parse_family(reader, family, aot, fs, false)?; - let left_ics = left_body - .ics_info - .clone() - .ok_or(Error::ElementDecodeInvalid)?; - let left_spectral = SpectralData::parse(reader, &left_ics, &left_body.section_data, fs)?; - let second_ics_start_bit = reader.bit_position(); - let right_body = IcsBody::parse_family(reader, family, aot, fs, false)?; - let right_ics = right_body - .ics_info - .clone() - .ok_or(Error::ElementDecodeInvalid)?; - let right_spectral = SpectralData::parse(reader, &right_ics, &right_body.section_data, fs)?; - Ok(ParsedCpe { - joint: CpeJointStereo::default(), - left_body, - left_ics, - left_spectral, - right_body, - right_ics, - right_spectral, - second_ics_start_bit, - }) - } -} - -/// Parse one ER channel's spectrum: the plain Table 4.56 -/// `spectral_data()` when `aacSpectralDataResilienceFlag` is clear, or -/// the §4.6.16.3 `reordered_spectral_data()` payload (whose two length -/// fields the ER body already captured) decoded through -/// [`crate::hcr_decode::decode_reordered_spectral_data`]. -fn parse_er_spectral( - reader: &mut BitReader<'_>, - body: &IcsBody, - ics: &IcsInfo, - fs: u8, - resilience: AacResilienceFlags, - is_cpe: bool, -) -> Result { - if !resilience.spectral_data { - return SpectralData::parse(reader, ics, &body.section_data, fs); - } - let (len_reordered, len_longest) = body - .reordered_spectral_lengths - .ok_or(Error::ElementDecodeInvalid)?; - let len = crate::hcr::clamp_reordered_length(len_reordered, is_cpe); - // Gather the (not necessarily byte-aligned) payload bits. - let mut buf = vec![0u8; usize::from(len).div_ceil(8)]; - for i in 0..usize::from(len) { - if reader.read_bit().map_err(|_| Error::UnexpectedEnd)? { - buf[i / 8] |= 0x80 >> (i % 8); - } - } - crate::hcr_decode::decode_reordered_spectral_data( - &buf, - len, - len_longest, - ics, - &body.section_data, - fs, - ) -} - -/// §4.6.8.3.3 `decode_coupling_channel()` — collect the coupling -/// contributions addressed at one target channel. -/// -/// Walks every CCE of the same raw data block, replaying the spec's -/// target loop to assign `list_index` values: an SCE target consumes -/// one gain list; a CPE target consumes one shared list (`cc_l == cc_r -/// == 0`, applied to both channels), or one list per flagged channel. -/// `channel` selects the target channel of a CPE (`0` left, `1` -/// right); an SCE target only ever matches `channel == 0`. -fn coupling_for<'a>( - cces: &'a [(u8, CouplingChannelElement)], - decoded: &'a [DecodedCce], - block: u8, - kind: IdSynEle, - tag: u8, - channel: usize, -) -> Vec> { - let mut out = Vec::new(); - for ((cce_block, cce), dec) in cces.iter().zip(decoded.iter()) { - if *cce_block != block { - continue; - } - let mut list_index = 0usize; - for t in &cce.header.targets { - if !t.is_cpe { - if kind == IdSynEle::Sce && tag == t.tag_select && channel == 0 { - out.push(CouplingApply { - cce, - decoded: dec, - list_index, - }); - } - list_index += 1; - } else { - let addressed = kind == IdSynEle::Cpe && tag == t.tag_select; - if !t.cc_l && !t.cc_r { - // Table 4.153 shared list: both channels couple - // with the same gain list. - if addressed { - out.push(CouplingApply { - cce, - decoded: dec, - list_index, - }); - } - list_index += 1; - } - if t.cc_l { - if addressed && channel == 0 { - out.push(CouplingApply { - cce, - decoded: dec, - list_index, - }); - } - list_index += 1; - } - if t.cc_r { - if addressed && channel == 1 { - out.push(CouplingApply { - cce, - decoded: dec, - list_index, - }); - } - list_index += 1; - } - } - } - } - out -} - -/// Map a channel-element `id_syn_ele` to the slot key's first component -/// (the element decoders are keyed independently per syntactic-element -/// id so an SCE tag 0 and a CPE tag 0 never collide). -fn kind_id(kind: IdSynEle) -> u8 { - match kind { - IdSynEle::Sce => 0, - IdSynEle::Cpe => 1, - IdSynEle::Lfe => 3, - _ => 9, - } -} - -fn unsupported_element(kind: IdSynEle) -> Error { - // CCE (coupling) has no decode path; surface the element-decode - // failure mode rather than a parse error so the caller can tell a - // structural-OK-but-unsupported element apart from a malformed one. - let _ = kind; - Error::ElementDecodeInvalid -} - -/// Skip a leading ID3v2 tag (`"ID3"` + 6-byte header + syncsafe size + -/// optional footer) if present; otherwise return the input unchanged. -fn skip_id3v2(data: &[u8]) -> &[u8] { - if data.len() < 10 || &data[..3] != b"ID3" { - return data; - } - let size = data[6..10] - .iter() - .fold(0usize, |acc, &b| (acc << 7) | usize::from(b & 0x7f)); - let footer = if data[5] & 0x10 != 0 { 10 } else { 0 }; - let total = 10 + size + footer; - if total >= data.len() { - data - } else { - &data[total..] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn skip_id3v2_passes_through_non_id3() { - let data = [0xFFu8, 0xF1, 0x00, 0x00]; - assert_eq!(skip_id3v2(&data), &data); - } - - #[test] - fn skip_id3v2_strips_a_tag() { - // "ID3", ver 4.0, no flags, syncsafe size = 4 → 10 + 4 = 14 - // bytes of tag, then a sentinel payload byte. - let mut data = vec![b'I', b'D', b'3', 4, 0, 0, 0, 0, 0, 4]; - data.extend_from_slice(&[0; 4]); - data.push(0xAB); - assert_eq!(skip_id3v2(&data), &[0xABu8]); - } - - #[test] - fn skip_id3v2_keeps_tag_when_size_overruns() { - // A declared size larger than the buffer leaves the data as-is - // rather than panicking. - let data = vec![b'I', b'D', b'3', 4, 0, 0, 0x7f, 0x7f, 0x7f, 0x7f]; - assert_eq!(skip_id3v2(&data), &data[..]); - } - - #[test] - fn kind_id_separates_sce_and_cpe() { - assert_ne!(kind_id(IdSynEle::Sce), kind_id(IdSynEle::Cpe)); - assert_ne!(kind_id(IdSynEle::Lfe), kind_id(IdSynEle::Cpe)); - } -} diff --git a/crates/vendor/oxideav-aac/src/decoded_spectrum.rs b/crates/vendor/oxideav-aac/src/decoded_spectrum.rs deleted file mode 100644 index 2a040ea9..00000000 --- a/crates/vendor/oxideav-aac/src/decoded_spectrum.rs +++ /dev/null @@ -1,353 +0,0 @@ -//! Per-channel "decoded spectrum" pipeline stage — ISO/IEC 14496-3 -//! §4.6.3.3 `quant_to_spec()` + the parse → dequant → scalefactor → -//! TNS composition. -//! -//! This module chains the per-tool reconstruction primitives into -//! the channel-level stage that ends one step short of the -//! filterbank (§4.6.11 IMDCT + window-overlap-add, a follow-up -//! round): -//! -//! 1. **Pulse fix-up** (§4.6.3.3) — when `pulse_data_present`, fold -//! the `±pulse_amp` corrections into `x_quant` via -//! [`crate::swb_offset::apply_pulse_data`] (long windows only, -//! per Table 4.50 Note 1). -//! 2. **Scalefactor accumulation** (§4.6.2.3.2 / §4.6.8.1.4 / -//! §4.6.13) — [`crate::scale_factor_data::accumulate`]. -//! 3. **Inverse quantization + rescaling** (§4.6.1.3 / §4.6.2.3.3) -//! — [`crate::dequant::rescale_spectrum`]. -//! 4. **De-interleaving** (§4.6.3.3 `quant_to_spec()`) — from the -//! §4.5.2.3.5 group-interleaved transmission order to the -//! window-major `spec[w][k]` layout that TNS and the filterbank -//! consume ([`quant_to_spec`]). -//! 5. **TNS** (§4.6.9) — when `tns_data_present`, -//! [`crate::tns_frame::tns_decode_frame`] over the de-interleaved -//! spectrum. -//! -//! ## §4.6.3.3 `quant_to_spec()` -//! -//! ```text -//! quant_to_spec() { -//! k = 0; -//! for (g = 0; g < num_window_groups; g++ ) { -//! j = 0; -//! for (sfb = 0; sfb < num_swb; sfb++) { -//! width = swb_offset[sfb+1] - swb_offset[sfb]; -//! for (win = 0; win < window_group_length[g]; win++) { -//! for (bin = 0; bin < width; bin++) { -//! spec[win+k][bin+j] = x_quant[g][win][sfb][bin]; -//! } -//! } -//! j += width; -//! } -//! k += window_group_length[g]; -//! } -//! } -//! ``` -//! -//! The interleaved source reads linearly in exactly the loop order -//! (`g`, `sfb`, `win`, `bin`) because §4.5.2.3.5 stores each -//! virtual scalefactor band as the concatenated per-window -//! scalefactor-window-band slices. For the long window sequences -//! (`num_window_groups == 1`, `window_group_length[0] == 1`) the -//! mapping degenerates to an identity copy. -//! -//! ## Scope -//! -//! Intensity stereo (§4.6.8.2), M/S (§4.6.8.1), and PNS (§4.6.13) -//! reconstruction are channel-*pair* / noise-synthesis tools that -//! slot between steps 4 and 5 (de-interleave → joint-stereo → TNS). -//! The M/S de-matrix is implemented as -//! [`crate::ms_stereo::apply_ms_stereo`], a CPE-level pass over the -//! two channels' de-interleaved spectra; this single-channel stage -//! does not invoke it (the caller runs it on the pair before each -//! channel's TNS). Intensity stereo and PNS synthesis remain -//! follow-ups, so intensity / `NOISE_HCB` bands still come out as -//! silence here. The Main-profile predictor (§4.6.7) and LTP -//! (§4.6.6) are likewise deferred. - -use crate::dequant::rescale_spectrum; -use crate::ics_body::IcsBody; -use crate::ics_info::IcsInfo; -use crate::scale_factor_data::accumulate; -use crate::spectral_data::SpectralData; -use crate::swb_offset::apply_pulse_data_family; -use crate::tns_frame::tns_decode_frame_ics; -use crate::{Error, Result}; - -/// §4.6.3.3 `quant_to_spec()` — de-interleave per-group -/// transmission-order coefficient buffers into the window-major -/// `spec[w][k]` layout (windows concatenated: -/// `spec[w * window_len + k]`). -/// -/// * `groups` — one buffer per window group in the §4.5.2.3.5 -/// interleaved order, each spanning the full group -/// (`window_group_length[g] × 128` short, `1024` long) — the -/// shape produced by [`SpectralData::parse`] and preserved by -/// [`rescale_spectrum`]. -/// * `ics_info` / `fs_index` — grouping and the Table 4.129-family -/// `swb_offset` table. -/// -/// Errors: -/// -/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] — `fs_index` -/// outside the SWB-table range. -/// * [`Error::QuantToSpecInvalid`] — group count or a group buffer -/// length disagreeing with the `ics_info` grouping, or a grouping -/// whose `window_group_length` sum is not `num_windows`. -pub fn quant_to_spec(groups: &[Vec], ics_info: &IcsInfo, fs_index: u8) -> Result> { - let window_len = ics_info.window_len()?; - let offsets = ics_info.swb_offsets(fs_index)?; - let num_swb = offsets.len() - 1; - let num_windows = ics_info.num_windows as usize; - let num_groups = ics_info.num_window_groups as usize; - - if groups.len() != num_groups - || ics_info.window_group_length.len() != num_groups - || ics_info - .window_group_length - .iter() - .map(|&w| w as usize) - .sum::() - != num_windows - { - return Err(Error::QuantToSpecInvalid); - } - - let mut spec = vec![0.0f64; num_windows * window_len]; - // `k` in the pseudocode: index of the group's first window. - let mut window_base = 0usize; - for (g, group) in groups.iter().enumerate() { - let wgl = ics_info.window_group_length[g] as usize; - if group.len() != wgl * window_len { - return Err(Error::QuantToSpecInvalid); - } - // The interleaved buffer reads linearly in (sfb, win, bin) - // order; `j` is the in-window coefficient offset of the - // current scalefactor window band. - let mut src = group.iter(); - let mut j = 0usize; - for sfb in 0..num_swb { - let width = (offsets[sfb + 1] - offsets[sfb]) as usize; - for win in 0..wgl { - let dst = (window_base + win) * window_len + j; - for bin in 0..width { - // group.len() == wgl * window_len == wgl * sum of - // widths, so the iterator yields exactly enough. - spec[dst + bin] = *src.next().expect("group length checked above"); - } - } - j += width; - } - window_base += wgl; - } - Ok(spec) -} - -/// Decode one channel's spectrum: pulse fix-up → scalefactor -/// accumulation → inverse quantization + rescaling → -/// `quant_to_spec()` → TNS. -/// -/// * `body` — the parsed Table 4.50 channel body -/// ([`IcsBody::parse`] / [`IcsBody::parse_with_ics_info`]). -/// * `ics_info` — the channel's `ics_info()`; pass -/// `body.ics_info.as_ref().unwrap()` for the inline form or the -/// externally-held shared `IcsInfo` for the CPE -/// `common_window == 1` form. -/// * `spectral` — the channel's parsed Table 4.56 spectrum -/// ([`SpectralData::parse`] resumed at -/// `body.spectral_data_bit_offset`). -/// * `aot` / `fs_index` — `audioObjectType` and -/// `samplingFrequencyIndex`, driving the TNS clamp tables and the -/// `swb_offset` selection. -/// -/// Returns the window-major decoded spectrum (`num_windows × -/// window_len` = 1024 coefficients, window `w` at -/// `spec[w * window_len ..]`) — the §4.6.11 filterbank's input. -/// -/// Errors propagate from the composed stages: see -/// [`apply_pulse_data`], [`accumulate`], [`rescale_spectrum`], -/// [`quant_to_spec`], and [`tns_decode_frame`]. -pub fn decode_channel_spectrum( - body: &IcsBody, - ics_info: &IcsInfo, - spectral: &SpectralData, - aot: u8, - fs_index: u8, -) -> Result> { - // 1. §4.6.3.3 pulse fix-up on the quantised spectrum (long - // windows only — the parser already rejects the EIGHT_SHORT - // combination, and a long sequence has exactly one group). - let x_quant: &SpectralData = &if let Some(pd) = &body.pulse_data { - let mut patched = spectral.clone(); - let group0 = patched.x_quant.first_mut().ok_or(Error::DequantInvalid)?; - apply_pulse_data_family(group0, ics_info.family, fs_index, pd)?; - patched - } else { - spectral.clone() - }; - - // 2. §4.6.2.3.2 scalefactor accumulation. - let scale_factors = accumulate( - &body.scale_factor_data, - &body.section_data.sfb_cb, - body.global_gain, - )?; - - // 3. §4.6.1.3 + §4.6.2.3.3 inverse quantization + rescaling. - let rescaled = rescale_spectrum( - x_quant, - &scale_factors, - &body.section_data.sfb_cb, - ics_info, - fs_index, - )?; - - // 4. §4.6.3.3 quant_to_spec() de-interleaving. - let mut spec = quant_to_spec(&rescaled, ics_info, fs_index)?; - - // 5. §4.6.9 TNS. - if let Some(tns) = &body.tns_data { - tns_decode_frame_ics(&mut spec, tns, ics_info, aot, fs_index)?; - } - Ok(spec) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - - fn long_ics_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], - } - } - - fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { - let num_window_groups = window_group_length.len() as u8; - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups, - window_group_length, - num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[4], - } - } - - // ===== quant_to_spec ===== - - #[test] - fn quant_to_spec_long_is_identity() { - let info = long_ics_info(10); - let group: Vec = (0..1024).map(|i| i as f64 * 0.5 - 100.0).collect(); - let spec = quant_to_spec(core::slice::from_ref(&group), &info, 4).unwrap(); - assert_eq!(spec, group); - } - - #[test] - fn quant_to_spec_short_deinterleaves_grouped_windows() { - // Grouping 5 + 3 at fs_index 4 (short band 0 is 4 wide, - // band 1 is 4 wide, ...). Place markers at known - // (g, sfb, win, bin) coordinates and verify their - // window-major destinations. - let info = short_ics_info(2, vec![5, 3]); - let mut g0 = vec![0.0f64; 5 * 128]; - let mut g1 = vec![0.0f64; 3 * 128]; - // (g=0, sfb=0, win=2, bin=1): interleaved index - // 0 + 2*4 + 1 = 9 -> spec window 2, coefficient 1. - g0[9] = 1.0; - // (g=0, sfb=1, win=4, bin=3): interleaved index - // 5*4 + 4*4 + 3 = 39 -> spec window 4, coefficient 4+3. - g0[39] = 2.0; - // (g=1, sfb=0, win=0, bin=0): -> spec window 5 (groups 0..4 - // are group 0), coefficient 0. - g1[0] = 3.0; - // (g=1, sfb=1, win=2, bin=2): interleaved index - // 3*4 + 2*4 + 2 = 22 -> spec window 7, coefficient 6. - g1[22] = 4.0; - let spec = quant_to_spec(&[g0, g1], &info, 4).unwrap(); - assert_eq!(spec.len(), 1024); - assert_eq!(spec[2 * 128 + 1], 1.0); - assert_eq!(spec[4 * 128 + 7], 2.0); - assert_eq!(spec[5 * 128], 3.0); - assert_eq!(spec[7 * 128 + 6], 4.0); - let placed = spec.iter().filter(|&&v| v != 0.0).count(); - assert_eq!(placed, 4); - } - - #[test] - fn quant_to_spec_short_full_table_round_trips_every_coefficient() { - // Tag every interleaved coefficient with a unique value and - // verify the de-interleave is a permutation reaching all - // 1024 slots. - let info = short_ics_info(14, vec![1, 2, 1, 4]); - let mut groups = Vec::new(); - let mut tag = 1.0f64; - for &wgl in &info.window_group_length { - let mut g = vec![0.0f64; wgl as usize * 128]; - for slot in g.iter_mut() { - *slot = tag; - tag += 1.0; - } - groups.push(g); - } - let spec = quant_to_spec(&groups, &info, 4).unwrap(); - let mut seen: Vec = spec.clone(); - seen.sort_by(|a, b| a.partial_cmp(b).unwrap()); - let want: Vec = (1..=1024).map(|i| i as f64).collect(); - assert_eq!(seen, want); - } - - #[test] - fn quant_to_spec_rejects_shape_mismatches() { - let info = short_ics_info(2, vec![5, 3]); - // Wrong group count. - assert!(matches!( - quant_to_spec(&[vec![0.0; 5 * 128]], &info, 4), - Err(Error::QuantToSpecInvalid) - )); - // Wrong group buffer length. - assert!(matches!( - quant_to_spec(&[vec![0.0; 5 * 128], vec![0.0; 2 * 128]], &info, 4), - Err(Error::QuantToSpecInvalid) - )); - // Grouping that does not sum to num_windows. - let bad = short_ics_info(2, vec![5, 2]); - assert!(matches!( - quant_to_spec(&[vec![0.0; 5 * 128], vec![0.0; 2 * 128]], &bad, 4), - Err(Error::QuantToSpecInvalid) - )); - // Unsupported fs_index propagates. - let info = long_ics_info(2); - assert!(matches!( - quant_to_spec(&[vec![0.0; 1024]], &info, 12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/dequant.rs b/crates/vendor/oxideav-aac/src/dequant.rs deleted file mode 100644 index 34d46325..00000000 --- a/crates/vendor/oxideav-aac/src/dequant.rs +++ /dev/null @@ -1,476 +0,0 @@ -//! §4.6.1.3 inverse quantization + §4.6.2.3.3 scalefactor -//! application — ISO/IEC 14496-3. -//! -//! The first numeric reconstruction stage after the Table 4.56 -//! `spectral_data()` wire walk: convert the quantised integer -//! spectrum `x_quant` into the rescaled real-valued spectrum -//! `x_rescal` that the downstream tools (TNS, filterbank) consume. -//! -//! ## §4.6.1.3 — inverse quantization -//! -//! The encoder's non-uniform quantizer is inverted per coefficient: -//! -//! ```text -//! x_invquant = Sign(x_quant) * |x_quant|^(4/3) -//! ``` -//! -//! The maximum allowed absolute amplitude for `x_quant` is 8191 -//! ([`crate::spectral_codebook::MAX_QUANT`]); the wire walker -//! already enforces it, so [`inverse_quantize`] accepts any `i32` -//! and leaves range policing to the parser. -//! -//! ## §4.6.2.3.3 — applying scalefactors -//! -//! Every scalefactor band is rescaled by the gain of its absolute -//! scalefactor: -//! -//! ```text -//! gain = 2^(0.25 * (sf[g][sfb] - SF_OFFSET)) SF_OFFSET = 100 -//! x_rescal[...] = x_invquant[...] * gain -//! ``` -//! -//! per the §4.6.2.3.3 pseudocode, with the same gain applied to all -//! grouped short windows of a (virtual) scalefactor band. The -//! band → coefficient mapping is the §4.5.2.3.4 -//! [`crate::spectral_data::sect_sfb_offset`] derivation, so the -//! whole operation runs directly over the §4.5.2.3.5 interleaved -//! transmission-order buffers produced by -//! [`crate::spectral_data::SpectralData::parse`]. -//! -//! Bands whose codebook carries no spectrum keep a `0.0` output: -//! `ZERO_HCB` bands and bands at or above `max_sfb` transmit -//! nothing (and the wire walker leaves their `x_quant` at 0), while -//! `NOISE_HCB` / intensity bands are reconstructed by the PNS / -//! intensity-stereo tools (§4.6.13 / §4.6.8) which are not part of -//! this stage — their [`AbsoluteScaleFactorEntry::NoiseNrg`] / -//! [`AbsoluteScaleFactorEntry::IsPos`] records are consumed (to -//! keep the wire-order lockstep) but produce no rescaled energy -//! here. - -use crate::ics_info::IcsInfo; -use crate::scale_factor_data::{AbsoluteScaleFactorEntry, AbsoluteScaleFactors}; -use crate::section_data::{Codebook, ZERO_HCB}; -use crate::spectral_data::{sect_sfb_offset, SpectralData}; -use crate::{Error, Result}; - -/// `SF_OFFSET` per §4.6.2.3.3 — the scalefactor that maps to unit -/// gain. "The constant SF_OFFSET must be set to 100." -pub const SF_OFFSET: i32 = 100; - -/// §4.6.1.3 inverse quantization of one coefficient: -/// `Sign(x_quant) · |x_quant|^(4/3)`. -#[inline] -pub fn inverse_quantize(x_quant: i32) -> f64 { - // |x|^(4/3) computed as |x| · |x|^(1/3): `cbrt` is correctly - // rounded, so perfect cubes (and 0 / ±1) invert exactly, and the - // general case avoids the representation error of the literal - // exponent 4/3. - let abs = f64::from(x_quant.unsigned_abs()); - let mag = abs * abs.cbrt(); - if x_quant < 0 { - -mag - } else { - mag - } -} - -/// §4.6.2.3.3 `get_scale_factor_gain()`: -/// `2^(0.25 · (sf − SF_OFFSET))`. -#[inline] -pub fn scale_factor_gain(sf: u8) -> f64 { - (0.25 * f64::from(i32::from(sf) - SF_OFFSET)).exp2() -} - -/// Run §4.6.1.3 inverse quantization and §4.6.2.3.3 scalefactor -/// application over one channel's quantised spectrum. -/// -/// * `spectral` — the per-group interleaved `x_quant` buffers from -/// [`SpectralData::parse`] (or the same shape with the §4.6.3.3 -/// pulse fix-up already folded in via -/// [`crate::swb_offset::apply_pulse_data`]). -/// * `scale_factors` — the absolute per-band records from -/// [`crate::scale_factor_data::accumulate`]. -/// * `sfb_cb` — the per-`(g, sfb)` codebook map from -/// [`crate::section_data::SectionData::parse`]. -/// * `ics_info` / `fs_index` — drive the §4.5.2.3.4 band → -/// coefficient mapping and the group buffer shapes. -/// -/// Returns the rescaled spectrum `x_rescal` in the same per-group -/// interleaved layout (and lengths) as the input `x_quant`. -/// -/// Errors: -/// -/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] / -/// [`Error::SpectralDataInvalid`] — propagated from -/// [`sect_sfb_offset`] (`fs_index` out of range, `max_sfb` above -/// `num_swb`). -/// * [`Error::DequantInvalid`] — structural mismatch between the -/// three inputs: group counts disagreeing with -/// `num_window_groups`, a group buffer length disagreeing with -/// `window_group_length[g] × 128` (or 1024 long), or a -/// scalefactor-entry sequence that does not match the -/// non-`ZERO_HCB` codebook classification of `sfb_cb` (including -/// the reserved codebook 12, which carries a scalefactor on the -/// wire but has no spectrum semantics to rescale). -pub fn rescale_spectrum( - spectral: &SpectralData, - scale_factors: &AbsoluteScaleFactors, - sfb_cb: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, -) -> Result>> { - let offsets = sect_sfb_offset(ics_info, fs_index)?; - let num_groups = ics_info.num_window_groups as usize; - if spectral.x_quant.len() != num_groups - || scale_factors.entries.len() != num_groups - || sfb_cb.len() != num_groups - { - return Err(Error::DequantInvalid); - } - - let mut out = Vec::with_capacity(num_groups); - for (g, group_offsets) in offsets.iter().enumerate() { - let x_quant = &spectral.x_quant[g]; - let window_len = ics_info.window_len().map_err(|_| Error::DequantInvalid)?; - let expected_len = if ics_info.window_sequence.is_eight_short() { - ics_info.window_group_length[g] as usize * window_len - } else { - window_len - }; - if x_quant.len() != expected_len || sfb_cb[g].len() != ics_info.max_sfb as usize { - return Err(Error::DequantInvalid); - } - - let mut rescal = vec![0.0f64; x_quant.len()]; - let mut entries = scale_factors.entries[g].iter(); - for (sfb, &cb) in sfb_cb[g].iter().enumerate() { - if cb == ZERO_HCB { - continue; - } - let entry = entries.next().ok_or(Error::DequantInvalid)?; - let kind = Codebook::from_value(cb); - match entry { - AbsoluteScaleFactorEntry::Sf(sf) - if matches!( - kind, - Codebook::Quad { .. } | Codebook::Pair { .. } | Codebook::Esc - ) => - { - let gain = scale_factor_gain(*sf); - let start = group_offsets[sfb] as usize; - let end = group_offsets[sfb + 1] as usize; - for k in start..end { - rescal[k] = inverse_quantize(x_quant[k]) * gain; - } - } - // PNS / intensity bands transmit no spectrum; their - // §4.6.13 / §4.6.8 reconstruction happens in the - // dedicated tools, not the rescale stage. Consume - // the record to keep the wire-order lockstep. - AbsoluteScaleFactorEntry::NoiseNrg(_) if kind.is_noise() => {} - AbsoluteScaleFactorEntry::IsPos(_) if kind.is_intensity() => {} - _ => return Err(Error::DequantInvalid), - } - } - if entries.next().is_some() { - return Err(Error::DequantInvalid); - } - out.push(rescal); - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - use crate::section_data::{INTENSITY_HCB, NOISE_HCB}; - - fn long_ics_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], - } - } - - // ===== inverse_quantize ===== - - #[test] - fn inverse_quantize_pins_exact_cubes() { - // |x|^(4/3) is exact when |x| is a perfect cube: - // 8 = 2^3 -> 2^4, 27 = 3^3 -> 3^4, 64 = 4^3 -> 4^4, - // 729 = 3^6 -> 3^8, 4096 = 2^12 -> 2^16. - assert_eq!(inverse_quantize(0), 0.0); - assert_eq!(inverse_quantize(1), 1.0); - assert_eq!(inverse_quantize(-1), -1.0); - assert_eq!(inverse_quantize(8), 16.0); - assert_eq!(inverse_quantize(-8), -16.0); - assert_eq!(inverse_quantize(27), 81.0); - assert_eq!(inverse_quantize(-27), -81.0); - assert_eq!(inverse_quantize(64), 256.0); - assert_eq!(inverse_quantize(729), 6561.0); - assert_eq!(inverse_quantize(4096), 65536.0); - assert_eq!(inverse_quantize(-4096), -65536.0); - } - - #[test] - fn inverse_quantize_is_odd_and_monotonic_up_to_max_quant() { - let mut prev = 0.0; - for x in 1..=8191 { - let y = inverse_quantize(x); - assert!(y > prev, "monotonic at {x}"); - assert_eq!(inverse_quantize(-x), -y, "odd symmetry at {x}"); - prev = y; - } - // 8191^(4/3) is a bit above 8191 * 8191^(1/3) ~ 164k. - assert!(prev > 160_000.0 && prev < 170_000.0); - } - - // ===== scale_factor_gain ===== - - #[test] - fn scale_factor_gain_pins_exact_powers() { - // sf = SF_OFFSET -> 1; every +4 doubles, every -4 halves. - assert_eq!(scale_factor_gain(100), 1.0); - assert_eq!(scale_factor_gain(104), 2.0); - assert_eq!(scale_factor_gain(108), 4.0); - assert_eq!(scale_factor_gain(96), 0.5); - assert_eq!(scale_factor_gain(92), 0.25); - // sf = 0 -> 2^-25; sf = 255 -> 2^38.75. - assert_eq!(scale_factor_gain(0), (-25.0f64).exp2()); - assert_eq!(scale_factor_gain(255), 38.75f64.exp2()); - // Quarter-step: sf = 101 -> 2^0.25. - assert_eq!(scale_factor_gain(101), 0.25f64.exp2()); - } - - // ===== rescale_spectrum ===== - - /// One long window, two bands on a spectrum book: band gains are - /// applied per band over the swb ranges. - #[test] - fn rescale_applies_per_band_gain_over_swb_ranges() { - // fs_index 4 long: bands 0 and 1 are 4 coefficients each. - let info = long_ics_info(2); - let sfb_cb = vec![vec![1u8, 1]]; - let mut x_quant = vec![0i32; 1024]; - x_quant[..8].copy_from_slice(&[1, -1, 0, 8, -8, 1, 0, -1]); - let spectral = SpectralData { - x_quant: vec![x_quant], - }; - // Band 0 at sf 104 (gain 2), band 1 at sf 96 (gain 0.5). - let sf = AbsoluteScaleFactors { - entries: vec![vec![ - AbsoluteScaleFactorEntry::Sf(104), - AbsoluteScaleFactorEntry::Sf(96), - ]], - }; - let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); - assert_eq!(out.len(), 1); - assert_eq!(out[0].len(), 1024); - // Band 0: x_invquant * 2. - assert_eq!(out[0][..4], [2.0, -2.0, 0.0, 32.0]); - // Band 1: x_invquant * 0.5. - assert_eq!(out[0][4..8], [-8.0, 0.5, 0.0, -0.5]); - // Above max_sfb: all zero. - assert!(out[0][8..].iter().all(|&v| v == 0.0)); - } - - #[test] - fn rescale_leaves_noise_and_intensity_bands_at_zero() { - let info = long_ics_info(3); - let sfb_cb = vec![vec![NOISE_HCB, INTENSITY_HCB, 2]]; - let mut x_quant = vec![0i32; 1024]; - // Only band 2 (coefficients 8..12) carries spectrum. - x_quant[8..12].copy_from_slice(&[1, 1, -1, 0]); - let spectral = SpectralData { - x_quant: vec![x_quant], - }; - let sf = AbsoluteScaleFactors { - entries: vec![vec![ - AbsoluteScaleFactorEntry::NoiseNrg(-50), - AbsoluteScaleFactorEntry::IsPos(3), - AbsoluteScaleFactorEntry::Sf(100), - ]], - }; - let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); - assert!(out[0][..8].iter().all(|&v| v == 0.0)); - assert_eq!(out[0][8..12], [1.0, 1.0, -1.0, 0.0]); - } - - #[test] - fn rescale_skips_zero_hcb_bands_without_consuming_entries() { - let info = long_ics_info(3); - let sfb_cb = vec![vec![ZERO_HCB, 1, ZERO_HCB]]; - let mut x_quant = vec![0i32; 1024]; - x_quant[4..8].copy_from_slice(&[1, 0, 0, -1]); - let spectral = SpectralData { - x_quant: vec![x_quant], - }; - let sf = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::Sf(108)]], - }; - let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); - assert_eq!(out[0][4..8], [4.0, 0.0, 0.0, -4.0]); - assert!(out[0][..4].iter().all(|&v| v == 0.0)); - assert!(out[0][8..].iter().all(|&v| v == 0.0)); - } - - /// EIGHT_SHORT grouping: the same gain covers all grouped short - /// windows of a virtual band (§4.6.2.3.3 "all coefficients in - /// grouped scalefactor window bands ... same scalefactor"). - #[test] - fn rescale_short_grouped_band_shares_one_gain() { - let info = IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb: 1, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups: 2, - window_group_length: vec![5, 3], - num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[4], - }; - let sfb_cb = vec![vec![1u8], vec![1u8]]; - // fs 4 short band 0 is 4 wide; virtual band = wgl * 4. - let mut g0 = vec![0i32; 5 * 128]; - for (i, slot) in g0.iter_mut().take(20).enumerate() { - *slot = if i % 2 == 0 { 1 } else { -1 }; - } - let mut g1 = vec![0i32; 3 * 128]; - for slot in g1.iter_mut().take(12) { - *slot = 8; - } - let spectral = SpectralData { - x_quant: vec![g0, g1], - }; - let sf = AbsoluteScaleFactors { - entries: vec![ - vec![AbsoluteScaleFactorEntry::Sf(104)], - vec![AbsoluteScaleFactorEntry::Sf(96)], - ], - }; - let out = rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4).unwrap(); - for (i, &v) in out[0].iter().take(20).enumerate() { - let want = if i % 2 == 0 { 2.0 } else { -2.0 }; - assert_eq!(v, want, "g0[{i}]"); - } - assert!(out[0][20..].iter().all(|&v| v == 0.0)); - for (i, &v) in out[1].iter().take(12).enumerate() { - assert_eq!(v, 8.0, "g1[{i}]"); - } - assert!(out[1][12..].iter().all(|&v| v == 0.0)); - } - - #[test] - fn rescale_rejects_entry_codebook_mismatch() { - let info = long_ics_info(1); - let sfb_cb = vec![vec![1u8]]; - let spectral = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - // IsPos entry against a spectrum book. - let sf = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::IsPos(0)]], - }; - assert!(matches!( - rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), - Err(Error::DequantInvalid) - )); - } - - #[test] - fn rescale_rejects_reserved_codebook_12() { - let info = long_ics_info(1); - let sfb_cb = vec![vec![12u8]]; - let spectral = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - let sf = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], - }; - assert!(matches!( - rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), - Err(Error::DequantInvalid) - )); - } - - #[test] - fn rescale_rejects_surplus_and_missing_entries() { - let info = long_ics_info(1); - let sfb_cb = vec![vec![1u8]]; - let spectral = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - let missing = AbsoluteScaleFactors { - entries: vec![vec![]], - }; - assert!(matches!( - rescale_spectrum(&spectral, &missing, &sfb_cb, &info, 4), - Err(Error::DequantInvalid) - )); - let surplus = AbsoluteScaleFactors { - entries: vec![vec![ - AbsoluteScaleFactorEntry::Sf(100), - AbsoluteScaleFactorEntry::Sf(100), - ]], - }; - assert!(matches!( - rescale_spectrum(&spectral, &surplus, &sfb_cb, &info, 4), - Err(Error::DequantInvalid) - )); - } - - #[test] - fn rescale_rejects_wrong_group_buffer_length() { - let info = long_ics_info(1); - let sfb_cb = vec![vec![1u8]]; - let spectral = SpectralData { - x_quant: vec![vec![0i32; 512]], - }; - let sf = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], - }; - assert!(matches!( - rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), - Err(Error::DequantInvalid) - )); - } - - #[test] - fn rescale_rejects_group_count_mismatch() { - let info = long_ics_info(1); - let sfb_cb = vec![vec![1u8], vec![1u8]]; - let spectral = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - let sf = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], - }; - assert!(matches!( - rescale_spectrum(&spectral, &sf, &sfb_cb, &info, 4), - Err(Error::DequantInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/element_decode.rs b/crates/vendor/oxideav-aac/src/element_decode.rs deleted file mode 100644 index 12a86d0c..00000000 --- a/crates/vendor/oxideav-aac/src/element_decode.rs +++ /dev/null @@ -1,1482 +0,0 @@ -//! Channel-element decode driver — the §4.6 block-order chain that -//! turns a parsed `single_channel_element()` (SCE / LFE) or -//! `channel_pair_element()` (CPE) into PCM-domain samples. -//! -//! Every per-tool reconstruction primitive landed in earlier rounds; -//! what was missing was the element-level glue that runs them in the -//! ISO/IEC 14496-3 §4.6 block order and carries the per-channel -//! filterbank overlap state across frames. This module is that glue. -//! -//! ## §4.6 block order -//! -//! For a single channel the per-channel chain is (§4.6, Figure 4.1 / -//! the "Decoder block diagram"): -//! -//! 1. **Noiseless decoding** — `spectral_data()` (Table 4.56), already -//! parsed into [`crate::spectral_data::SpectralData`]. -//! 2. **Pulse fix-up** (§4.6.3.3) — fold the `±pulse_amp` corrections -//! into the quantised spectrum (long windows only, Table 4.50 -//! Note 1). -//! 3. **Inverse quantisation** (§4.6.1.3) + **scalefactor application** -//! (§4.6.2.3.3) — [`crate::dequant::rescale_spectrum`] over the -//! §4.6.2.3.2-accumulated absolute scalefactors. -//! 4. **De-interleave** (§4.6.3.3 `quant_to_spec()`) — group-interleaved -//! transmission order → window-major `spec[w][k]` -//! ([`crate::decoded_spectrum::quant_to_spec`]). -//! 5. **Joint stereo / noise** (CPE only, §4.6.8 / §4.6.13) — M/S -//! de-matrix (§4.6.8.1), then intensity stereo (§4.6.8.2), then PNS -//! (§4.6.13). The spec applies these *before* TNS (§4.6.13.5: noise -//! is injected prior to the TNS step) and on the de-interleaved -//! pre-TNS spectrum, which is exactly the -//! [`crate::ms_stereo::ChannelPairSpectra`] / -//! [`crate::intensity_stereo::IntensityPairSpectra`] / -//! [`crate::pns::PnsChannel`] contract. -//! 6. **TNS** (§4.6.9) — [`crate::tns_frame::tns_decode_frame`] in -//! place on the window-major spectrum. -//! 7. **Filterbank** (§4.6.11) — IMDCT + window + inter-frame -//! overlap-add ([`crate::filterbank::Filterbank::synthesize`]), -//! emitting `LONG_WINDOW_LEN` (1024) PCM samples per channel per -//! frame. -//! -//! Because the joint-stereo / noise tools (step 5) sit *between* -//! `quant_to_spec()` and TNS, the CPE path cannot reuse the -//! single-channel [`crate::decoded_spectrum::decode_channel_spectrum`] -//! (which runs TNS internally at the end of its own chain). This module -//! therefore composes the finer-grained primitives directly: -//! [`reconstruct_pre_pair`] runs steps 2–4 for one channel, the pair -//! tools run on both pre-TNS spectra, then [`finish_channel`] runs -//! steps 6–7 per channel. -//! -//! ## Scope -//! -//! * **LTP (§4.6.7)** is wired in for long windows: [`finish_channel`] -//! runs the §4.6.7.4.1 / Figure 4.30 block order — long-term -//! synthesis (with the all-zero TNS analysis filter on `X_est`) -//! *before* the §4.6.9 TNS synthesis filter — and advances the -//! per-channel [`crate::ltp::LtpState`] reconstruction history each -//! frame. Short-window LTP and the ER AAC LD `M = N/2` lag offset -//! remain out of scope (the predictor is left off for those, per the -//! §4.6.7.1 long-window restriction). -//! * **Frequency-domain prediction (§4.6.6)** is wired in for the AAC -//! Main object type (AOT 1): [`finish_channel`] runs the -//! §4.6.6.3.2.1 backward-adaptive predictor bank -//! ([`crate::predictor::PredictorBank`]) on every long frame *before* -//! §4.6.7 LTP / §4.6.9 TNS, adding `x_est + y_rec` on the signalled -//! bands and resetting the signalled group / the whole bank on a short -//! block. The per-channel bank persists across frames so the LMS -//! coefficients keep adapting. Prediction and LTP are mutually -//! exclusive by object type (AOT 1 carries no `ltp_data`), so only one -//! predictor ever fires per channel. -//! * **SSR gain control (§4.6.12)** is wired in for the SSR object -//! type (AOT 3): [`finish_channel`] replaces the §4.6.11 filterbank -//! with the per-channel [`crate::ssr::SsrChannelDecoder`] pipeline — -//! the §4.6.12.1 four-band front-half filterbank, the §4.6.12.3 gain -//! compensation/overlap driven by the frame's `gain_control_data()`, -//! and the IPQF synthesis. Note the §4.6.12.3.3 variable per-frame -//! output length (1472 / 576 for `LONG_START` / `LONG_STOP`). -//! * PNS output is RNG-defined per §4.6.13.3 (only the per-band L2 norm -//! is spec-determined); the driver uses the default -//! [`crate::pns::gen_rand_vector`] LCG, seeded once per decoder so the -//! noise is reproducible across a decode run. - -use crate::cce::CouplingChannelElement; -use crate::decoded_spectrum::quant_to_spec; -use crate::dequant::rescale_spectrum; -use crate::filterbank::Filterbank; -use crate::ics_body::IcsBody; -use crate::ics_info::IcsInfo; -use crate::intensity_stereo::{apply_intensity_stereo, IntensityPairSpectra}; -use crate::ltp::LtpState; -use crate::ms_stereo::{apply_ms_stereo, ChannelPairSpectra, MsMaskPresent}; -use crate::pns::{apply_pns, apply_pns_pair, gen_rand_vector, PnsChannel}; -use crate::predictor::PredictorBank; -use crate::scale_factor_data::{accumulate, AbsoluteScaleFactorEntry, AbsoluteScaleFactors}; -use crate::section_data::ZERO_HCB; -use crate::spectral_data::SpectralData; -use crate::ssr::SsrChannelDecoder; -use crate::swb_offset::apply_pulse_data; -use crate::tns_frame::{tns_analysis_frame_ics, tns_decode_frame_ics}; -use crate::{Error, Result}; - -/// One channel's parsed Table 4.50 body plus its Table 4.56 spectrum, -/// bundled so the element driver can take them by reference. -#[derive(Debug)] -pub struct ChannelInput<'a> { - /// The parsed `individual_channel_stream()` body - /// ([`IcsBody::parse`] / [`IcsBody::parse_with_ics_info`]). - pub body: &'a IcsBody, - /// The channel's `ics_info()`. For an SCE / LFE or a non-shared - /// CPE this is `body.ics_info`; for a `common_window == 1` CPE this - /// is the shared `ics_info` the caller parsed once. - pub ics_info: &'a IcsInfo, - /// The channel's parsed `spectral_data()` - /// ([`SpectralData::parse`]). - pub spectral: &'a SpectralData, -} - -/// Expand a wire-order [`AbsoluteScaleFactors`] into the band-indexed -/// `track[g][sfb]` layout (size `num_window_groups × max_sfb`) the -/// §4.6.8.2 / §4.6.13 synthesis passes consume. -/// -/// `accumulate()` returns one record per non-`ZERO_HCB` band in -/// wire (low-frequency-first) order; the joint-stereo / noise tools -/// instead index by `(g, sfb)`. This walks `sfb_cb[g][sfb]` in lock-step -/// with the wire records and scatters the requested track value into the -/// `(g, sfb)` slot, leaving non-matching bands at `default`. -/// -/// `pick` maps an [`AbsoluteScaleFactorEntry`] to the track value of -/// interest (`is_pos` or `noise_nrg`), or `None` for a record that -/// belongs to a different track (in which case the slot stays -/// `default`). -fn band_indexed_track( - abs: &AbsoluteScaleFactors, - sfb_cb: &[Vec], - max_sfb: usize, - default: i32, - pick: F, -) -> Result>> -where - F: Fn(&AbsoluteScaleFactorEntry) -> Option, -{ - if abs.entries.len() != sfb_cb.len() { - return Err(Error::ElementDecodeInvalid); - } - let mut out: Vec> = Vec::with_capacity(sfb_cb.len()); - for (group_records, group_cb) in abs.entries.iter().zip(sfb_cb.iter()) { - if group_cb.len() < max_sfb { - return Err(Error::ElementDecodeInvalid); - } - let mut row = vec![default; max_sfb]; - let mut rec = group_records.iter(); - for (sfb, &cb) in group_cb.iter().enumerate() { - if cb == ZERO_HCB { - continue; - } - // Every non-ZERO_HCB band consumes exactly one wire record, - // in lock-step with the accumulate() walk. - let entry = rec.next().ok_or(Error::ElementDecodeInvalid)?; - if sfb < max_sfb { - if let Some(v) = pick(entry) { - row[sfb] = v; - } - } - } - out.push(row); - } - Ok(out) -} - -/// Band-indexed `is_pos[g][sfb]` (§4.6.8.1.4), default `0` on -/// non-intensity bands. -pub(crate) fn is_pos_table( - abs: &AbsoluteScaleFactors, - sfb_cb: &[Vec], - max_sfb: usize, -) -> Result>> { - band_indexed_track(abs, sfb_cb, max_sfb, 0, |e| match e { - AbsoluteScaleFactorEntry::IsPos(p) => Some(i32::from(*p)), - _ => None, - }) -} - -/// Band-indexed `noise_nrg[g][sfb]` (§4.6.13.3), default `0` on -/// non-noise bands. -pub(crate) fn noise_nrg_table( - abs: &AbsoluteScaleFactors, - sfb_cb: &[Vec], - max_sfb: usize, -) -> Result>> { - band_indexed_track(abs, sfb_cb, max_sfb, 0, |e| match e { - AbsoluteScaleFactorEntry::NoiseNrg(n) => Some(*n), - _ => None, - }) -} - -/// Run §4.6 steps 2–4 for one channel: pulse fix-up → scalefactor -/// accumulation → inverse quantisation + rescaling → `quant_to_spec()`. -/// -/// Returns the window-major **pre-TNS** spectrum (the joint-stereo / -/// noise tools' input) alongside the accumulated absolute scalefactors -/// (so the caller can derive the band-indexed `is_pos` / `noise_nrg` -/// tracks without re-running the accumulator). -fn reconstruct_pre_pair( - ch: &ChannelInput<'_>, - fs_index: u8, -) -> Result<(Vec, AbsoluteScaleFactors)> { - // 2. §4.6.3.3 pulse fix-up on the quantised spectrum (long windows - // only — the parser already rejects pulse on EIGHT_SHORT, and a - // long sequence has exactly one group). - let x_quant: SpectralData = if let Some(pd) = &ch.body.pulse_data { - let mut patched = ch.spectral.clone(); - let group0 = patched.x_quant.first_mut().ok_or(Error::DequantInvalid)?; - apply_pulse_data(group0, fs_index, pd)?; - patched - } else { - ch.spectral.clone() - }; - - // 3a. §4.6.2.3.2 scalefactor accumulation. - let abs = accumulate( - &ch.body.scale_factor_data, - &ch.body.section_data.sfb_cb, - ch.body.global_gain, - )?; - - // 3b. §4.6.1.3 + §4.6.2.3.3 inverse quantisation + rescaling. - let rescaled = rescale_spectrum( - &x_quant, - &abs, - &ch.body.section_data.sfb_cb, - ch.ics_info, - fs_index, - )?; - - // 4. §4.6.3.3 quant_to_spec() de-interleaving. - let spec = quant_to_spec(&rescaled, ch.ics_info, fs_index)?; - Ok((spec, abs)) -} - -/// Run the §4.6.7.4.1 / §4.6.9 / §4.6.11 tail for one channel in the -/// Figure 4.30 block order: **LTP long-term synthesis** (§4.6.7) → -/// **TNS synthesis** (§4.6.9) → **filterbank** (§4.6.11), then update -/// the per-channel LTP reconstruction history (§4.6.7.3). -/// -/// Figure 4.30 places long-term synthesis *before* the TNS synthesis -/// filter; because the transmitted residual `Y_rec` in `spec` is in the -/// noise-shaped (pre-synthesis) domain, the LTP-predicted spectrum -/// `X_est` is first passed through the matching all-zero **TNS analysis -/// filter** ([`tns_analysis_frame`]) so the `X_rec = X_est + Y_rec` add -/// is like-for-like. The single TNS synthesis pass that follows then -/// shapes the residual while undoing the analysis on the LTP -/// contribution (the §4.6.7.4.1 inverse-filter relationship). -/// -/// `ltp` is the channel's parsed [`crate::ics_info::LtpData`] (from -/// `ics_info.ltp_data` for an SCE / CPE channel 0, or `ltp_data_pair` -/// for the shared-window CPE channel 1); `None` when -/// `ltp_data_present == 0`, in which case no prediction is added but the -/// history is still advanced so it stays continuous across frames. -#[allow(clippy::too_many_arguments)] -fn finish_channel( - spec: &mut [f64], - body: &IcsBody, - ics_info: &IcsInfo, - ltp: Option<&crate::ics_info::LtpData>, - aot: u8, - fs_index: u8, - fb: &mut Filterbank, - ltp_state: &mut LtpState, - predictor_bank: &mut Option, - ssr: &mut Option>, - coupling: &[CouplingApply<'_>], -) -> Result> { - // §4.6.6 MPEG-2 frequency-domain prediction (AAC Main, AOT 1 only). - // The backward-adaptive predictor bank is run on EVERY frame so its - // coefficients keep tracking the signal statistics, whether or not - // prediction is signalled this frame; a short block resets the whole - // bank. The bank is created lazily on the first Main frame. - if aot == 1 { - let bank = match predictor_bank { - Some(b) => b, - None => { - *predictor_bank = Some(PredictorBank::new(fs_index)?); - predictor_bank.as_mut().expect("just inserted") - } - }; - bank.apply_long(spec, ics_info, ics_info.predictor_data.as_ref(), fs_index)?; - } - - // §4.6.7 long-term synthesis (long windows only). The analysis - // filter applied to X_est mirrors this frame's TNS; an order-0 / - // filter-less TNS makes tns_analysis_frame a no-op, so a channel - // without TNS gets the plain X_est + Y_rec add. - if let Some(ltp) = ltp { - let prev_shape = fb.prev_shape(); - let tns = body.tns_data.as_ref(); - ltp_state.apply_long_with_analysis(spec, ics_info, ltp, prev_shape, fs_index, |x_est| { - if let Some(tns) = tns { - tns_analysis_frame_ics(x_est, tns, ics_info, aot, fs_index)?; - } - Ok(()) - })?; - } - - // §4.6.8.3.3 dependently-switched coupling with cc_domain == 0: - // the CCE spectra are scaled and added *before* the target's TNS - // decoding. - apply_freq_coupling(spec, ics_info, fs_index, coupling, false)?; - - // §4.6.9 TNS synthesis. - if let Some(tns) = &body.tns_data { - tns_decode_frame_ics(spec, tns, ics_info, aot, fs_index)?; - } - - // §4.6.8.3.3 dependently-switched coupling with cc_domain == 1: - // scaled and added *after* the target's TNS decoding. - apply_freq_coupling(spec, ics_info, fs_index, coupling, true)?; - - // §4.6.12 — the SSR object type (AOT 3) replaces the §4.6.11 - // filterbank with the four-band gain-control pipeline: the - // §4.6.12.1 front-half filterbank (band split + 256/32-line - // IMDCTs), the §4.6.12.3 gain compensation/overlap driven by this - // frame's gain_control_data(), and the IPQF synthesis. LTP and the - // §4.6.6 predictor are other object types' tools, so the state - // advance below does not apply. - let mut out = if aot == 3 { - let dec = ssr.get_or_insert_with(Default::default); - dec.decode_frame(spec, ics_info, body.gain_control_data.as_ref())? - } else { - // §4.6.11 filterbank → PCM, then advance the LTP history with - // this frame's output and aliased IMDCT tail (§4.6.7.3). - let out = fb.synthesize(spec, ics_info)?; - ltp_state.push_frame(&out, fb.aliased_tail()); - out - }; - - // §4.6.8.3.3 independently-switched coupling: the CCE was decoded - // all the way to the time domain and is scaled and added here. - apply_time_coupling(&mut out, coupling)?; - Ok(out) -} - -/// One §4.6.8.3.3 coupling contribution addressed at a single target -/// channel: the parsed CCE (gain lists + embedded-SCE geometry), its -/// decoded embedded spectrum / time signal, and the `list_index` the -/// `decode_coupling_channel()` target walk assigned to this channel. -#[derive(Debug, Clone, Copy)] -pub struct CouplingApply<'a> { - /// The parsed `coupling_channel_element()`. - pub cce: &'a CouplingChannelElement, - /// The CCE's decoded embedded `single_channel_element()` - /// ([`CceDecoder::decode`]). - pub decoded: &'a DecodedCce, - /// The §4.6.8.3.3 `couple_channel()` gain-list index for this - /// target channel. - pub list_index: usize, -} - -/// §4.6.8.3.3 — apply every *dependently switched* coupling -/// contribution whose `cc_domain` matches `after_tns` onto the target -/// spectrum in place. -/// -/// A dependently switched CCE "must have a window state that matches -/// all of the target SCE and CPE channels" — a `window_sequence` / -/// window-group-geometry mismatch is rejected with -/// [`Error::CceInvalid`] rather than mis-addressing bands. -fn apply_freq_coupling( - spec: &mut [f64], - ics_info: &IcsInfo, - fs_index: u8, - coupling: &[CouplingApply<'_>], - after_tns: bool, -) -> Result<()> { - for c in coupling { - if c.cce.header.ind_sw_cce_flag || c.cce.header.cc_domain != after_tns { - continue; - } - let cce_ics = &c.cce.ics_info; - if cce_ics.window_sequence != ics_info.window_sequence - || cce_ics.num_window_groups != ics_info.num_window_groups - || cce_ics.window_group_length != ics_info.window_group_length - { - return Err(Error::CceInvalid); - } - let offsets = cce_ics.swb_offsets(fs_index)?; - c.cce.gains.couple_channel( - &c.decoded.spectrum, - spec, - c.list_index, - &c.cce.body.section_data.sfb_cb, - &cce_ics.window_group_length, - usize::from(cce_ics.max_sfb), - offsets, - )?; - } - Ok(()) -} - -/// §4.6.8.3.3 — apply every *independently switched* coupling -/// contribution onto the target's time signal in place. An -/// independently switched CCE only carries `common_gain_element`s, so -/// the whole frame is scaled by one `cc_gain`. -fn apply_time_coupling(out: &mut [f64], coupling: &[CouplingApply<'_>]) -> Result<()> { - for c in coupling { - if !c.cce.header.ind_sw_cce_flag { - continue; - } - let time = c.decoded.time.as_deref().ok_or(Error::CceInvalid)?; - if time.len() != out.len() { - // The SSR variable-length frames cannot take a 1024-sample - // time coupling; surface the mismatch instead of adding a - // misaligned signal. - return Err(Error::CceInvalid); - } - let cc_gain = c.cce.gains.cc_gain(c.list_index, 0, 0)?; - for (o, &t) in out.iter_mut().zip(time.iter()) { - *o += cc_gain * t; - } - } - Ok(()) -} - -/// The decoded embedded `single_channel_element()` of one CCE -/// (§4.6.8.3.3 `cc_spectrum`), ready to be coupled onto targets. -#[derive(Debug, Clone)] -pub struct DecodedCce { - /// The fully decoded spectrum (pulse → dequant → `quant_to_spec()` - /// → PNS → the CCE's *own* TNS), window-major — the §4.6.8.3.3 - /// `cc_spectrum[]` buffer a dependently switched CCE couples from. - pub spectrum: Vec, - /// The time-domain signal (through the CCE's own §4.6.11 - /// filterbank) — present only for an independently switched CCE, - /// which §4.6.8.3.3 requires to be "decoded all the way to the - /// time domain … before it is scaled and added". - pub time: Option>, -} - -/// Stateful per-CCE-slot decoder for the embedded -/// `single_channel_element()` of a `coupling_channel_element()` -/// (§4.6.8.3.3). Keyed per `element_instance_tag` by the stream -/// driver so the independently-switched filterbank overlap and the -/// PNS generator persist across frames. -#[derive(Debug, Clone)] -pub struct CceDecoder { - /// The CCE's own §4.6.11 filterbank (independently-switched CCEs - /// synthesize to the time domain with their own window state). - fb: Filterbank, - /// §4.6.13.3 generator state for noise bands in the embedded SCE. - pns_state: u32, -} - -impl Default for CceDecoder { - fn default() -> Self { - Self::new() - } -} - -impl CceDecoder { - /// A fresh CCE decoder with zeroed filterbank overlap. - #[must_use] - pub fn new() -> Self { - Self::new_family(crate::swb_offset::FrameFamily::Lc1024) - } - - /// A fresh CCE decoder for an arbitrary §4.5.1.1 frame-length - /// family. - #[must_use] - pub fn new_family(family: crate::swb_offset::FrameFamily) -> Self { - CceDecoder { - fb: Filterbank::new_family(family), - pns_state: 0x0001_2345, - } - } - - /// Decode the CCE's embedded `single_channel_element()` to the - /// §4.6.8.3.3 `cc_spectrum[]` (and, for an independently switched - /// CCE, on to the time domain through this slot's persistent - /// filterbank). - pub fn decode( - &mut self, - cce: &CouplingChannelElement, - aot: u8, - fs_index: u8, - ) -> Result { - let ch = ChannelInput { - body: &cce.body, - ics_info: &cce.ics_info, - spectral: &cce.spectral, - }; - let (mut spec, abs) = reconstruct_pre_pair(&ch, fs_index)?; - - // §4.6.13 PNS on the embedded single channel. - let max_sfb = usize::from(cce.ics_info.max_sfb); - let noise_nrg = noise_nrg_table(&abs, &cce.body.section_data.sfb_cb, max_sfb)?; - let state = &mut self.pns_state; - let mut pns_chan = PnsChannel { - spec: &mut spec, - sfb_cb: &cce.body.section_data.sfb_cb, - noise_nrg: &noise_nrg, - }; - apply_pns(&mut pns_chan, &cce.ics_info, fs_index, |out| { - gen_rand_vector(out, state) - })?; - - // The CCE's own §4.6.9 TNS (the embedded ICS is decoded like - // any other; the target's TNS relationship is what cc_domain - // selects). - if let Some(tns) = &cce.body.tns_data { - tns_decode_frame_ics(&mut spec, tns, &cce.ics_info, aot, fs_index)?; - } - - // Independently switched: decode to the time domain through - // this slot's persistent filterbank. - let time = if cce.header.ind_sw_cce_flag { - Some(self.fb.synthesize(&spec, &cce.ics_info)?) - } else { - None - }; - Ok(DecodedCce { - spectrum: spec, - time, - }) - } -} - -/// The shared `channel_pair_element()` joint-stereo header (Table 4.4) -/// the caller reads after `common_window`. -/// -/// Only meaningful when `common_window == 1`. For -/// `common_window == 0` both channels carry their own `ics_info()` and -/// no M/S mask is transmitted, so the joint-stereo tools do not run. -#[derive(Debug, Clone)] -pub struct CpeJointStereo { - /// Decoded `ms_mask_present` (§4.6.8.1.1, Table 4.4): `00` - /// all-zeros, `01` per-band `ms_used` mask, `10` all-ones; `11` is - /// reserved (the caller rejects it before constructing this). - pub ms_mask_present: MsMaskPresent, - /// `ms_used[g][sfb]` when `ms_mask_present == 01`; empty otherwise. - /// Each row must cover `max_sfb`. - pub ms_used: Vec>, -} - -impl Default for CpeJointStereo { - /// The `common_window == 0` / no-joint-stereo default: all-zeros - /// M/S mask (an identity de-matrix) and no per-band `ms_used`. - fn default() -> Self { - CpeJointStereo { - ms_mask_present: MsMaskPresent::AllZeros, - ms_used: Vec::new(), - } - } -} - -/// Stateful per-element decoder: holds one [`Filterbank`] per channel -/// slot (so the inter-frame overlap-add tail and previous-block window -/// shape persist across frames) and the PNS generator state. -/// -/// Construct one [`ElementDecoder`] per channel element of the stream -/// (one for an SCE / LFE, one for a CPE) and call [`Self::decode_sce`] -/// / [`Self::decode_cpe`] once per frame. -#[derive(Debug, Clone)] -pub struct ElementDecoder { - /// Per-channel filterbanks. `[0]` for the SCE / LFE or the CPE's - /// first channel; `[1]` for the CPE's second channel. - filterbanks: [Filterbank; 2], - /// Per-channel §4.6.7.3 LTP reconstruction history, advanced once - /// per frame (whether or not LTP fired) so the predictor buffer - /// stays continuous. Same channel-slot indexing as `filterbanks`. - ltp_states: [LtpState; 2], - /// Per-channel §4.6.6 frequency-domain predictor bank (AAC Main, - /// AOT 1). `None` until the first Main frame creates the bank for the - /// stream's sampling rate; thereafter the backward-adaptive state - /// persists and is advanced every frame. Same channel-slot indexing - /// as `filterbanks`. - predictor_banks: [Option; 2], - /// Per-channel §4.6.12 SSR pipeline (AOT 3), replacing the §4.6.11 - /// filterbank for the SSR object type. `None` until the first SSR - /// frame; thereafter the gain-control / IPQF / window-shape state - /// persists across frames. Same channel-slot indexing as - /// `filterbanks`. - ssr_decoders: [Option>; 2], - /// §4.6.13.3 default generator state, advanced across every noise - /// band of every frame so the noise is reproducible per decode run. - pns_state: u32, -} - -impl Default for ElementDecoder { - fn default() -> Self { - Self::new() - } -} - -impl ElementDecoder { - /// A fresh element decoder with zeroed filterbank overlap and a - /// fixed PNS generator seed. - pub fn new() -> Self { - Self::new_family(crate::swb_offset::FrameFamily::Lc1024) - } - - /// A fresh element decoder whose per-channel filterbank and LTP - /// state run an arbitrary §4.5.1.1 frame-length family. - pub fn new_family(family: crate::swb_offset::FrameFamily) -> Self { - ElementDecoder { - filterbanks: [ - Filterbank::new_family(family), - Filterbank::new_family(family), - ], - ltp_states: [LtpState::new_family(family), LtpState::new_family(family)], - predictor_banks: [None, None], - ssr_decoders: [None, None], - // Any non-zero seed yields a non-degenerate sequence; the - // §4.6.13.3 normalisation makes the per-band energy - // independent of the seed, so this choice only fixes the - // (spec-undefined) per-coefficient phase. - pns_state: 0x0001_2345, - } - } - - /// A fresh element decoder with an explicit PNS generator seed. - /// Per §4.6.13.3 the seed only affects the noise *phase*, not the - /// (spec-determined) per-band energy. - pub fn with_pns_seed(seed: u32) -> Self { - ElementDecoder { - filterbanks: [Filterbank::new(), Filterbank::new()], - ltp_states: [LtpState::new(), LtpState::new()], - predictor_banks: [None, None], - ssr_decoders: [None, None], - pns_state: seed, - } - } - - /// Decode one single-channel element (SCE) or LFE channel to PCM. - /// - /// Runs the full §4.6 single-channel chain (pulse → dequant → - /// `quant_to_spec()` → PNS → TNS → filterbank). M/S and intensity - /// stereo are channel-*pair* tools and do not apply to an SCE; PNS - /// (§4.6.13) does, so a single-channel noise band is synthesised - /// here. - /// - /// Returns `LONG_WINDOW_LEN` (1024) PCM-domain samples for the - /// frame. - pub fn decode_sce(&mut self, ch: &ChannelInput<'_>, aot: u8, fs_index: u8) -> Result> { - self.decode_sce_coupled(ch, aot, fs_index, &[]) - } - - /// [`Self::decode_sce`] with §4.6.8.3.3 coupling contributions: - /// each [`CouplingApply`] is scaled and added at its signalled - /// stage (before / after TNS for a dependently switched CCE, on - /// the time signal for an independently switched one). - pub fn decode_sce_coupled( - &mut self, - ch: &ChannelInput<'_>, - aot: u8, - fs_index: u8, - coupling: &[CouplingApply<'_>], - ) -> Result> { - let (mut spec, abs) = reconstruct_pre_pair(ch, fs_index)?; - let max_sfb = ch.ics_info.max_sfb as usize; - - // §4.6.13 PNS on the single channel (no pair correlation). - let noise_nrg = noise_nrg_table(&abs, &ch.body.section_data.sfb_cb, max_sfb)?; - let state = &mut self.pns_state; - let mut pns_chan = PnsChannel { - spec: &mut spec, - sfb_cb: &ch.body.section_data.sfb_cb, - noise_nrg: &noise_nrg, - }; - apply_pns(&mut pns_chan, ch.ics_info, fs_index, |out| { - gen_rand_vector(out, state) - })?; - - let ltp = ltp_for_channel(ch.ics_info, false); - finish_channel( - &mut spec, - ch.body, - ch.ics_info, - ltp, - aot, - fs_index, - &mut self.filterbanks[0], - &mut self.ltp_states[0], - &mut self.predictor_banks[0], - &mut self.ssr_decoders[0], - coupling, - ) - } - - /// Decode one channel-pair element (CPE) to a `(left, right)` pair - /// of PCM frames. - /// - /// * `left` / `right` — the two channels' parsed bodies + spectra. - /// For the shared-info form both [`ChannelInput::ics_info`] point - /// at the same shared `ics_info`. - /// * `joint` — the Table 4.4 joint-stereo header - /// ([`CpeJointStereo`]); pass [`CpeJointStereo::default`] (mask - /// all-zeros, no `ms_used`) for a `common_window == 0` pair, where - /// no joint-stereo tools run. - /// - /// Runs the full §4.6 chain with the joint-stereo / noise tools in - /// block order: per-channel pulse → dequant → `quant_to_spec()`, - /// then M/S (§4.6.8.1) → intensity (§4.6.8.2) → PNS (§4.6.13) on the - /// pre-TNS pair, then per-channel TNS (§4.6.9) → filterbank - /// (§4.6.11). - /// - /// Both channels must share `window_sequence` (the `common_window` - /// geometry the §4.6.8 tools require) when any joint-stereo tool is - /// active; a mismatch surfaces as [`Error::ElementDecodeInvalid`]. - pub fn decode_cpe( - &mut self, - left: &ChannelInput<'_>, - right: &ChannelInput<'_>, - joint: &CpeJointStereo, - aot: u8, - fs_index: u8, - ) -> Result<(Vec, Vec)> { - self.decode_cpe_coupled(left, right, joint, aot, fs_index, &[], &[]) - } - - /// [`Self::decode_cpe`] with §4.6.8.3.3 coupling contributions, - /// one list per target channel (`cc_l` / `cc_r` and the shared - /// Table 4.153 layout decide which lists the stream driver builds - /// for each side). - #[allow(clippy::too_many_arguments)] - pub fn decode_cpe_coupled( - &mut self, - left: &ChannelInput<'_>, - right: &ChannelInput<'_>, - joint: &CpeJointStereo, - aot: u8, - fs_index: u8, - left_coupling: &[CouplingApply<'_>], - right_coupling: &[CouplingApply<'_>], - ) -> Result<(Vec, Vec)> { - // The §4.6.8 joint-stereo tools de-matrix the two channels - // band-for-band, so they require a shared window geometry. The - // shared-info CPE form guarantees this; reject a mismatch the - // non-shared form might present. - if left.ics_info.window_sequence != right.ics_info.window_sequence - || left.ics_info.num_window_groups != right.ics_info.num_window_groups - || left.ics_info.window_group_length != right.ics_info.window_group_length - { - return Err(Error::ElementDecodeInvalid); - } - // The joint-stereo geometry keys off the shared (here: left) - // ics_info's max_sfb; the pair tools validate both channels' - // sfb_cb against it. - let geom = left.ics_info; - let max_sfb = geom.max_sfb as usize; - - let (mut left_spec, left_abs) = reconstruct_pre_pair(left, fs_index)?; - let (mut right_spec, right_abs) = reconstruct_pre_pair(right, fs_index)?; - - // §4.6.8.1 M/S de-matrix (suppressed on intensity / noise bands - // by apply_ms_stereo itself). - let ms_used_slice: &[Vec] = if joint.ms_mask_present == MsMaskPresent::Mask { - validate_ms_used(&joint.ms_used, geom)?; - &joint.ms_used - } else { - &[] - }; - { - let mut pair = ChannelPairSpectra { - left: &mut left_spec, - right: &mut right_spec, - left_sfb_cb: &left.body.section_data.sfb_cb, - right_sfb_cb: &right.body.section_data.sfb_cb, - }; - apply_ms_stereo( - &mut pair, - joint.ms_mask_present, - ms_used_slice, - geom, - fs_index, - )?; - } - - // §4.6.8.2 intensity stereo: right derived from left on - // intensity bands. invert_intensity reads the per-band M/S mask - // only when ms_mask_present == 01 (Mask). - let right_is_pos = is_pos_table(&right_abs, &right.body.section_data.sfb_cb, max_sfb)?; - let is_mask = joint.ms_mask_present == MsMaskPresent::Mask; - { - let mut pair = IntensityPairSpectra { - left: &left_spec, - right: &mut right_spec, - right_sfb_cb: &right.body.section_data.sfb_cb, - is_pos: &right_is_pos, - }; - apply_intensity_stereo(&mut pair, is_mask, ms_used_slice, geom, fs_index)?; - } - - // §4.6.13 PNS with the shared-vector correlation rule. PNS and - // M/S are mutually exclusive per band (§4.6.13.5), so a noise - // band was skipped by the M/S de-matrix above; here it is filled. - let left_nrg = noise_nrg_table(&left_abs, &left.body.section_data.sfb_cb, max_sfb)?; - let right_nrg = noise_nrg_table(&right_abs, &right.body.section_data.sfb_cb, max_sfb)?; - let all_shared = joint.ms_mask_present == MsMaskPresent::AllOnes; - { - let mut left_chan = PnsChannel { - spec: &mut left_spec, - sfb_cb: &left.body.section_data.sfb_cb, - noise_nrg: &left_nrg, - }; - let mut right_chan = PnsChannel { - spec: &mut right_spec, - sfb_cb: &right.body.section_data.sfb_cb, - noise_nrg: &right_nrg, - }; - let state = &mut self.pns_state; - apply_pns_pair( - &mut left_chan, - &mut right_chan, - is_mask, - all_shared, - ms_used_slice, - geom, - fs_index, - |out| gen_rand_vector(out, state), - )?; - } - - // §4.6.7 LTP + §4.6.9 TNS + §4.6.11 filterbank, per channel. - // Channel 0 reads the first ltp_data; channel 1 of a shared- - // window CPE reads ltp_data_pair (the second ltp_data_present - // subtree, Table 4.4), falling back to its own ltp_data in the - // non-shared form where each channel carries separate side info. - let left_ltp = ltp_for_channel(left.ics_info, false); - let right_ltp = ltp_for_channel(right.ics_info, true); - let out_left = finish_channel( - &mut left_spec, - left.body, - left.ics_info, - left_ltp, - aot, - fs_index, - &mut self.filterbanks[0], - &mut self.ltp_states[0], - &mut self.predictor_banks[0], - &mut self.ssr_decoders[0], - left_coupling, - )?; - let out_right = finish_channel( - &mut right_spec, - right.body, - right.ics_info, - right_ltp, - aot, - fs_index, - &mut self.filterbanks[1], - &mut self.ltp_states[1], - &mut self.predictor_banks[1], - &mut self.ssr_decoders[1], - right_coupling, - )?; - Ok((out_left, out_right)) - } -} - -/// Select the parsed §4.6.7.2 [`crate::ics_info::LtpData`] that drives -/// one channel's long-term prediction, or `None` when LTP is off for -/// that channel this frame (`ltp_data_present == 0`). -/// -/// * `is_pair_slot == false` (SCE, CPE channel 0) reads the primary -/// `ltp_data` subtree. -/// * `is_pair_slot == true` (CPE channel 1) reads the second -/// `ltp_data_pair` subtree carried after `common_window == 1` -/// (Table 4.4). In the non-shared CPE form the second channel parses -/// its own `ics_info()` with the side info in `ltp_data` and -/// `ltp_data_pair == None`; the fall-through keeps that case working. -fn ltp_for_channel(ics_info: &IcsInfo, is_pair_slot: bool) -> Option<&crate::ics_info::LtpData> { - if is_pair_slot { - if let Some(pair) = ics_info.ltp_data_pair.as_ref() { - return Some(pair); - } - } - ics_info.ltp_data.as_ref() -} - -/// Validate that an `ms_used[g][sfb]` mask covers -/// `num_window_groups × max_sfb`. The pair tools re-check this, but -/// surfacing the element-level [`Error::ElementDecodeInvalid`] gives the -/// caller a single, element-scoped failure mode. -fn validate_ms_used(ms_used: &[Vec], ics_info: &IcsInfo) -> Result<()> { - let num_groups = ics_info.num_window_groups as usize; - let max_sfb = ics_info.max_sfb as usize; - if ms_used.len() != num_groups { - return Err(Error::ElementDecodeInvalid); - } - for row in ms_used { - if row.len() < max_sfb { - return Err(Error::ElementDecodeInvalid); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - use crate::scale_factor_data::{ScaleFactorData, ScaleFactorEntry}; - use crate::section_data::{Section, SectionData, INTENSITY_HCB, NOISE_HCB}; - - // ---- band-indexed track expansion ---- - - fn sfb_cb_one_group(cbs: &[u8]) -> Vec> { - vec![cbs.to_vec()] - } - - #[test] - fn band_indexed_track_scatters_by_wire_order() { - // Group 0: bands [ZERO, INTENSITY_HCB, NOISE_HCB, spectrum=2]. - // Wire records skip ZERO; so records are - // [IsPos, NoiseNrg, Sf] for sfb 1, 2, 3. - let sfb_cb = sfb_cb_one_group(&[ZERO_HCB, INTENSITY_HCB, NOISE_HCB, 2]); - let abs = AbsoluteScaleFactors { - entries: vec![vec![ - AbsoluteScaleFactorEntry::IsPos(7), - AbsoluteScaleFactorEntry::NoiseNrg(42), - AbsoluteScaleFactorEntry::Sf(120), - ]], - }; - let is_pos = is_pos_table(&abs, &sfb_cb, 4).unwrap(); - assert_eq!(is_pos[0], vec![0, 7, 0, 0]); - let nrg = noise_nrg_table(&abs, &sfb_cb, 4).unwrap(); - assert_eq!(nrg[0], vec![0, 0, 42, 0]); - } - - #[test] - fn band_indexed_track_rejects_record_shortfall() { - // Two non-ZERO bands but only one wire record. - let sfb_cb = sfb_cb_one_group(&[INTENSITY_HCB, NOISE_HCB]); - let abs = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::IsPos(1)]], - }; - assert!(matches!( - is_pos_table(&abs, &sfb_cb, 2), - Err(Error::ElementDecodeInvalid) - )); - } - - #[test] - fn band_indexed_track_rejects_group_count_mismatch() { - let sfb_cb = vec![vec![2u8], vec![2u8]]; - let abs = AbsoluteScaleFactors { - entries: vec![vec![AbsoluteScaleFactorEntry::Sf(100)]], - }; - assert!(matches!( - noise_nrg_table(&abs, &sfb_cb, 1), - Err(Error::ElementDecodeInvalid) - )); - } - - // ---- end-to-end element decode ---- - - fn long_ics_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], - } - } - - /// Build a minimal single-group long-window channel body whose - /// `section_data` assigns codebook `cb` to bands `0..max_sfb` and - /// whose `scale_factor_data` carries one DPCM record per non-ZERO - /// band. No pulse / TNS / gain-control tools. - fn make_body(max_sfb: u8, cb: u8, sf_deltas: &[i16]) -> IcsBody { - let sfb_cb = vec![vec![cb; max_sfb as usize]]; - let sections = vec![vec![Section { - codebook: cb, - start: 0, - end: max_sfb, - }]]; - let section_data = SectionData { sections, sfb_cb }; - // For a NOISE_HCB / INTENSITY band the record variant differs; - // make_body is only used with spectrum books (Dpcm) and the - // single-noise-band case below, where the first record is the - // 9-bit PNS PCM seed. - let entries: Vec = if cb == NOISE_HCB { - // The first noise band of the frame carries the 9-bit PCM - // seed; later noise bands carry Huffman DPCM deltas. - sf_deltas - .iter() - .enumerate() - .map(|(i, &d)| { - if i == 0 { - ScaleFactorEntry::NoisePcm(d as u16) - } else { - ScaleFactorEntry::NoiseDpcm(d as i8) - } - }) - .collect() - } else { - sf_deltas - .iter() - .map(|&d| ScaleFactorEntry::Dpcm(d as i8)) - .collect() - }; - let scale_factor_data = ScaleFactorData { - entries: vec![entries], - }; - IcsBody { - global_gain: 100, - ics_info: Some(long_ics_info(max_sfb)), - section_data, - scale_factor_data, - pulse_data_present: false, - pulse_data: None, - tns_data_present: false, - tns_data: None, - gain_control_data_present: false, - gain_control_data: None, - spectral_data_bit_offset: 0, - er_scale_factor_data: None, - reordered_spectral_lengths: None, - } - } - - /// A spectral-data block with `value` in every coefficient of bands - /// `0..max_sfb` (long window, fs_index 4: bands are 4 wide at the - /// low end). Just fills the full 1024-coefficient group buffer. - fn make_spectral(value: i32) -> SpectralData { - SpectralData { - x_quant: vec![vec![value; 1024]], - } - } - - #[test] - fn decode_sce_produces_finite_pcm() { - let body = make_body(4, 2, &[0, 0, 0, 0]); - let ics = body.ics_info.clone().unwrap(); - let spectral = make_spectral(3); - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - let mut dec = ElementDecoder::new(); - let pcm = dec.decode_sce(&ch, 2, 4).unwrap(); - assert_eq!(pcm.len(), 1024); - assert!(pcm.iter().all(|v| v.is_finite())); - // The first frame overlaps against a zero tail, so the right - // half of the windowed block is folded into the next frame. - // A constant non-zero spectrum yields non-silent PCM. - assert!(pcm.iter().any(|&v| v != 0.0)); - } - - #[test] - fn decode_sce_overlap_couples_frames() { - let body = make_body(4, 2, &[0, 0, 0, 0]); - let ics = body.ics_info.clone().unwrap(); - let spectral = make_spectral(3); - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - let mut dec = ElementDecoder::new(); - let f0 = dec.decode_sce(&ch, 2, 4).unwrap(); - let f1 = dec.decode_sce(&ch, 2, 4).unwrap(); - // The second frame carries the first frame's overlap tail, so - // for identical input the two frames differ only by the - // (now non-zero) overlap contribution at frame start. - assert_ne!(f0, f1); - } - - // ---- SSR (AOT 3) §4.6.12 routing ---- - - /// AOT 3 routes the channel through the §4.6.12 SSR pipeline - /// instead of the §4.6.11 filterbank: same body/spectrum, different - /// synthesis, and the SSR output is exactly what a hand-driven - /// [`SsrChannelDecoder`] produces from the same decoded spectrum — - /// frame after frame (state threads). - #[test] - fn decode_sce_ssr_matches_direct_pipeline_and_threads_state() { - let body = make_body(4, 2, &[0, 0, 0, 0]); - let ics = body.ics_info.clone().unwrap(); - let spectral = make_spectral(3); - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - let mut dec = ElementDecoder::new(); - let mut lc = ElementDecoder::new(); - let mut direct = SsrChannelDecoder::new(); - for frame in 0..3 { - let f_ssr = dec.decode_sce(&ch, 3, 4).unwrap(); - assert_eq!(f_ssr.len(), 1024); - assert!(f_ssr.iter().all(|v| v.is_finite())); - // Bit-identical to the direct §4.6.12 pipeline on the same - // decoded (post-TNS) spectrum. - let (spec, _) = reconstruct_pre_pair(&ch, 4).unwrap(); - let expect = direct.decode_frame(&spec, &ics, None).unwrap(); - assert_eq!(f_ssr, expect, "frame {frame}"); - // …and different from the §4.6.11 LC synthesis. - let f_lc = lc.decode_sce(&ch, 2, 4).unwrap(); - assert_ne!(f_lc, f_ssr, "frame {frame}"); - } - } - - /// A frame carrying `gain_control_data()` decodes through the - /// §4.6.12.3 gain compensation: its PCM differs from the same - /// frame without the ladder. - #[test] - fn decode_sce_ssr_gain_control_data_changes_output() { - use crate::gain_control_data::{GainAdjust, GainBand, GainControlData, GainWindow}; - // 40 active scalefactor bands so the spectrum reaches well past - // coefficient 256 — PQF band 1 (the gain-controlled one) must - // carry signal for the ladder to matter. - let plain = make_body(40, 2, &[0; 40]); - let mut gained = make_body(40, 2, &[0; 40]); - gained.gain_control_data_present = true; - gained.gain_control_data = Some(GainControlData { - max_band: 1, - bands: vec![GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 7, // AdjLev = 3 ⇒ ALEV = 8. - aloccode: 0, - }], - }], - }], - }); - let ics = plain.ics_info.clone().unwrap(); - let spectral = make_spectral(3); - let ch_plain = ChannelInput { - body: &plain, - ics_info: &ics, - spectral: &spectral, - }; - let ch_gained = ChannelInput { - body: &gained, - ics_info: &ics, - spectral: &spectral, - }; - let mut a = ElementDecoder::new(); - let mut b = ElementDecoder::new(); - let fa = a.decode_sce(&ch_plain, 3, 4).unwrap(); - let fb = b.decode_sce(&ch_gained, 3, 4).unwrap(); - assert_eq!(fa.len(), fb.len()); - assert_ne!(fa, fb, "gain ladder must alter the SSR synthesis"); - } - - /// A CPE decodes both channels through per-slot SSR pipelines. - #[test] - fn decode_cpe_ssr_both_channels() { - let left_body = make_body(4, 2, &[0, 0, 0, 0]); - let right_body = make_body(4, 2, &[0, 0, 0, 0]); - let ics = left_body.ics_info.clone().unwrap(); - let left_spec = make_spectral(5); - let right_spec = make_spectral(2); - let left = ChannelInput { - body: &left_body, - ics_info: &ics, - spectral: &left_spec, - }; - let right = ChannelInput { - body: &right_body, - ics_info: &ics, - spectral: &right_spec, - }; - let mut dec = ElementDecoder::new(); - let (l, r) = dec - .decode_cpe(&left, &right, &CpeJointStereo::default(), 3, 4) - .unwrap(); - assert_eq!(l.len(), 1024); - assert_eq!(r.len(), 1024); - assert!(l.iter().chain(r.iter()).all(|v| v.is_finite())); - assert_ne!(l, r); - } - - #[test] - fn decode_cpe_ms_reconstructs_left_right() { - // common_window: shared ics_info. Channel 0 = mid, channel 1 = - // side; ms_mask_present = all-ones (10). With a constant - // spectrum m, s the de-matrix gives l = m + s, r = m - s. - let left_body = make_body(4, 2, &[0, 0, 0, 0]); - let right_body = make_body(4, 2, &[0, 0, 0, 0]); - let ics = left_body.ics_info.clone().unwrap(); - let left_spec = make_spectral(5); - let right_spec = make_spectral(2); - let left = ChannelInput { - body: &left_body, - ics_info: &ics, - spectral: &left_spec, - }; - let right = ChannelInput { - body: &right_body, - ics_info: &ics, - spectral: &right_spec, - }; - let joint = CpeJointStereo { - ms_mask_present: MsMaskPresent::AllOnes, - ms_used: vec![], - }; - let mut dec = ElementDecoder::new(); - let (l, r) = dec.decode_cpe(&left, &right, &joint, 2, 4).unwrap(); - assert_eq!(l.len(), 1024); - assert_eq!(r.len(), 1024); - assert!(l.iter().all(|v| v.is_finite())); - assert!(r.iter().all(|v| v.is_finite())); - // The reconstructed channels differ (l = m+s, r = m-s with - // s != 0), so the PCM frames are not identical. - assert_ne!(l, r); - } - - #[test] - fn decode_cpe_mask_off_is_independent_channels() { - // ms_mask_present = all-zeros: M/S is a no-op, each channel - // passes through independently. - let left_body = make_body(4, 2, &[0, 0, 0, 0]); - let right_body = make_body(4, 2, &[0, 0, 0, 0]); - let ics = left_body.ics_info.clone().unwrap(); - let same = make_spectral(4); - let left = ChannelInput { - body: &left_body, - ics_info: &ics, - spectral: &same, - }; - let right = ChannelInput { - body: &right_body, - ics_info: &ics, - spectral: &same, - }; - let joint = CpeJointStereo::default(); - let mut dec = ElementDecoder::new(); - let (l, r) = dec.decode_cpe(&left, &right, &joint, 2, 4).unwrap(); - // Identical input, identical (independent) filterbanks → equal. - assert_eq!(l, r); - } - - #[test] - fn decode_cpe_rejects_window_sequence_mismatch() { - let left_body = make_body(4, 2, &[0, 0, 0, 0]); - let mut right_body = make_body(4, 2, &[0, 0, 0, 0]); - // Give the right channel a different window sequence. - let mut right_ics = right_body.ics_info.clone().unwrap(); - right_ics.window_sequence = WindowSequence::LongStop; - right_body.ics_info = Some(right_ics.clone()); - let left_ics = left_body.ics_info.clone().unwrap(); - let left_spec = make_spectral(1); - let right_spec = make_spectral(1); - let left = ChannelInput { - body: &left_body, - ics_info: &left_ics, - spectral: &left_spec, - }; - let right = ChannelInput { - body: &right_body, - ics_info: &right_ics, - spectral: &right_spec, - }; - let joint = CpeJointStereo::default(); - let mut dec = ElementDecoder::new(); - assert!(matches!( - dec.decode_cpe(&left, &right, &joint, 2, 4), - Err(Error::ElementDecodeInvalid) - )); - } - - #[test] - fn decode_sce_synthesizes_noise_band() { - // A NOISE_HCB band carries no spectrum (silence on entry); PNS - // fills it to the §4.6.13.3 target norm. With one noise band the - // decoded PCM must be non-silent. - let body = make_body(4, NOISE_HCB, &[10, 0, 0, 0]); - let ics = body.ics_info.clone().unwrap(); - // Noise bands carry no x_quant (spectrum-less); leave zeros. - let spectral = make_spectral(0); - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - let mut dec = ElementDecoder::new(); - let pcm = dec.decode_sce(&ch, 2, 4).unwrap(); - assert!(pcm.iter().all(|v| v.is_finite())); - assert!( - pcm.iter().any(|&v| v != 0.0), - "PNS-filled noise band should produce non-silent PCM" - ); - } - - // ---- §4.6.7.4.1 LTP wiring ---- - - use crate::ics_info::LtpData; - - /// Attach long-window LTP side info to a body's `ics_info`: the - /// `ltp_data_present` flag plus an `ltp_data` carrying `coef` / `lag` - /// and `long_used` bands. - fn with_ltp(mut body: IcsBody, coef: u8, lag: u16, long_used: Vec) -> IcsBody { - let mut ics = body.ics_info.clone().unwrap(); - ics.ltp_data_present = true; - ics.ltp_data = Some(LtpData { - lag_update: None, - lag: Some(lag), - coef, - long_used, - short: None, - }); - body.ics_info = Some(ics); - body - } - - #[test] - fn ltp_off_first_frame_zero_history_matches_no_ltp() { - // §4.6.7.3 init: with all-zero history the predictor is zero, so - // an LTP-flagged first frame must decode identically to one with - // LTP off (X_est == 0 ⇒ X_rec == Y_rec). - let plain = make_body(4, 2, &[0, 0, 0, 0]); - let ltp_body = with_ltp(make_body(4, 2, &[0, 0, 0, 0]), 7, 50, vec![true; 4]); - let spectral = make_spectral(3); - - let p_ics = plain.ics_info.clone().unwrap(); - let l_ics = ltp_body.ics_info.clone().unwrap(); - let plain_ch = ChannelInput { - body: &plain, - ics_info: &p_ics, - spectral: &spectral, - }; - let ltp_ch = ChannelInput { - body: <p_body, - ics_info: &l_ics, - spectral: &spectral, - }; - let f_plain = ElementDecoder::new().decode_sce(&plain_ch, 2, 4).unwrap(); - let f_ltp = ElementDecoder::new().decode_sce(<p_ch, 2, 4).unwrap(); - for (a, b) in f_plain.iter().zip(f_ltp.iter()) { - assert!((a - b).abs() < 1e-12, "first-frame LTP add must be zero"); - } - } - - #[test] - fn ltp_fires_on_second_frame_and_diverges() { - // After a non-silent first frame seeds the §4.6.7.3 history, the - // second frame's predictor is non-zero on the flagged bands, so - // an LTP-active decoder diverges from an LTP-off one — proof the - // driver wires predict() → MDCT → add into the chain. - let plain = make_body(4, 2, &[0, 0, 0, 0]); - let ltp_body = with_ltp(make_body(4, 2, &[0, 0, 0, 0]), 5, 30, vec![true; 4]); - let spectral = make_spectral(4); - let p_ics = plain.ics_info.clone().unwrap(); - let l_ics = ltp_body.ics_info.clone().unwrap(); - let plain_ch = ChannelInput { - body: &plain, - ics_info: &p_ics, - spectral: &spectral, - }; - let ltp_ch = ChannelInput { - body: <p_body, - ics_info: &l_ics, - spectral: &spectral, - }; - - let mut dec_plain = ElementDecoder::new(); - let mut dec_ltp = ElementDecoder::new(); - // Frame 0 — identical (zero history). - let _ = dec_plain.decode_sce(&plain_ch, 2, 4).unwrap(); - let _ = dec_ltp.decode_sce(<p_ch, 2, 4).unwrap(); - // Frame 1 — LTP now has non-zero history to predict from. - let f1_plain = dec_plain.decode_sce(&plain_ch, 2, 4).unwrap(); - let f1_ltp = dec_ltp.decode_sce(<p_ch, 2, 4).unwrap(); - assert!(f1_ltp.iter().all(|v| v.is_finite())); - let diff = f1_plain - .iter() - .zip(f1_ltp.iter()) - .any(|(a, b)| (a - b).abs() > 1e-9); - assert!(diff, "second-frame LTP should change the output"); - } - - #[test] - fn ltp_with_tns_stays_finite() { - // LTP active on a TNS-carrying channel exercises the - // §4.6.7.4.1 analysis-filter-in-loop path; the decode must stay - // finite across two frames. - use crate::tns_data::{TnsData, TnsFilter, TnsWindow}; - let mut body = with_ltp(make_body(20, 2, &[0i16; 20]), 4, 64, vec![true; 20]); - body.tns_data_present = true; - body.tns_data = Some(TnsData { - windows: vec![TnsWindow { - coef_res: false, - filters: vec![TnsFilter { - length: 10, - order: 3, - direction: false, - coef_compress: false, - coef: vec![1, 7, 2], - }], - }], - }); - let ics = body.ics_info.clone().unwrap(); - let spectral = make_spectral(3); - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - let mut dec = ElementDecoder::new(); - let f0 = dec.decode_sce(&ch, 2, 4).unwrap(); - let f1 = dec.decode_sce(&ch, 2, 4).unwrap(); - assert!(f0.iter().all(|v| v.is_finite())); - assert!(f1.iter().all(|v| v.is_finite())); - // Second frame predicts from a seeded history → not identical. - assert_ne!(f0, f1); - } - - /// Attach a §4.6.6 Main `predictor_data()` to a channel body's - /// `ics_info`, enabling prediction on bands `0..max_sfb`. - fn with_main_prediction(mut body: IcsBody, max_sfb: u8) -> IcsBody { - use crate::ics_info::PredictorData; - let ics = body.ics_info.as_mut().unwrap(); - ics.predictor_data_present = true; - ics.predictor_data = Some(PredictorData { - reset: false, - reset_group_number: None, - prediction_used: vec![true; max_sfb as usize], - }); - body - } - - #[test] - fn decode_sce_main_aot_runs_predictor() { - // AOT 1 (Main) with predictor_data_present must run the §4.6.6 - // backward-adaptive bank; decode must stay finite across frames - // and the predictor state must build up so successive frames - // diverge from the AOT-2 (LC, no predictor) decode of the same - // input. - let body = with_main_prediction(make_body(20, 2, &[0i16; 20]), 20); - let ics = body.ics_info.clone().unwrap(); - let spectral = make_spectral(3); - let ch = ChannelInput { - body: &body, - ics_info: &ics, - spectral: &spectral, - }; - - // Main (AOT 1): the predictor bank fires. - let mut main_dec = ElementDecoder::new(); - let mut main_frames = Vec::new(); - for _ in 0..6 { - let f = main_dec.decode_sce(&ch, 1, 4).unwrap(); - assert!(f.iter().all(|v| v.is_finite())); - main_frames.push(f); - } - - // LC (AOT 2): no §4.6.6 predictor, same input. - let lc_body = make_body(20, 2, &[0i16; 20]); - let lc_ics = lc_body.ics_info.clone().unwrap(); - let lc_ch = ChannelInput { - body: &lc_body, - ics_info: &lc_ics, - spectral: &spectral, - }; - let mut lc_dec = ElementDecoder::new(); - let mut lc_frames = Vec::new(); - for _ in 0..6 { - lc_frames.push(lc_dec.decode_sce(&lc_ch, 2, 4).unwrap()); - } - - // Once the lattice has adapted, the predicted spectrum diverges - // from the un-predicted one, so the late Main frames differ from - // their LC counterparts. - assert_ne!( - main_frames.last().unwrap(), - lc_frames.last().unwrap(), - "Main predictor produced no spectral change" - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/encoder.rs b/crates/vendor/oxideav-aac/src/encoder.rs deleted file mode 100644 index 807b3dcc..00000000 --- a/crates/vendor/oxideav-aac/src/encoder.rs +++ /dev/null @@ -1,2991 +0,0 @@ -//! End-to-end AAC-LC encoder — ISO/IEC 14496-3 §4.5/§4.6 written -//! forward. -//! -//! This module drives the crate's Phase-2 bit-exact wire writers -//! ([`crate::ics_body::IcsBody::write`], -//! [`crate::section_data::SectionData::write`], -//! [`crate::scale_factor_data::ScaleFactorData::write`], -//! [`crate::spectral_data::SpectralData::write`], -//! [`crate::raw_data_block::FrameAssembler`], -//! [`crate::adts::AdtsHeader::write`]) from PCM input, producing an -//! ADTS stream the crate's own [`crate::decode::StreamDecoder`] -//! round-trips. -//! -//! ## What the wire format fixes vs. what the encoder chooses -//! -//! ISO/IEC 14496-3 normatively defines the *decoder*: the §4.6.2 -//! inverse quantizer `x = sign(q)·|q|^(4/3)·2^(0.25·(sf−100))`, the -//! §4.6.3 noiseless coding, and the §4.6.11 filterbank. Everything on -//! the analysis side — the psychoacoustic model, the -//! scalefactor/quantizer search, the codebook choice — is an encoder -//! degree of freedom; any choice that yields conforming syntax is a -//! conforming encoder. The choices here are deliberately simple and -//! fully derived from the normative decoder equations: -//! -//! * **Window decision (block switching, §4.6.11.3.2)** — an -//! energy-jump transient detector on each incoming hop drives the -//! `ONLY_LONG → LONG_START → EIGHT_SHORT → LONG_STOP` state -//! machine: a 128-sample subblock whose energy jumps ≥12× over the -//! running average of its predecessors (above an absolute floor) -//! marks the hop transient; the frame *before* the transient hop -//! becomes `LONG_START`, the transient hop's frame `EIGHT_SHORT` -//! (extended while transients continue), and the run exits through -//! `LONG_STOP`. All windows are the §4.6.11.3.2 sine shape. Within -//! an `EIGHT_SHORT` frame the §4.5.2.3.4 `scale_factor_grouping` -//! decision ([`decide_short_grouping`]) merges envelope-alike -//! adjacent windows into shared window groups (one scalefactor / -//! section track per group, §4.5.2.3.5 interleaved transmission -//! order) — the attack window's energy jump keeps it in its own -//! group. -//! * **Analysis filterbank** — the §4.6.11.3.1 forward MDCT (the -//! transform whose windowed overlap-add against the decoder's IMDCT -//! is unity — the same [`crate::filterbank::forward_mdct`] the -//! §4.6.7 LTP loop uses). Long frames run one 2048-point transform -//! under the sequence's composite window; `EIGHT_SHORT` frames run -//! eight 256-point transforms at offsets `448 + j·128` within the -//! window region. Frame `f` covers input samples -//! `[f·1024 − 1024, f·1024 + 1024)`; the leading frame is primed -//! with zeros, giving the standard 1024-sample encoder delay. -//! * **Quantizer** — the exact inverse of §4.6.2: -//! `q = round((|x| / 2^(0.25·(sf−100)))^(3/4))`, with -//! round-half-away-from-zero (the §1.3 `NINT` convention). -//! * **Psychoacoustics-lite** — a masking-spread rule: each -//! scalefactor band's `sf` is chosen so the band's *peak* -//! coefficient quantizes to a target magnitude -//! `M_b = M · (peak_b / peak_frame)^½` (`sf = 100 + 4·log2(peak_b) -//! − (16/3)·log2(M_b)`, the inversion of the dequant gain ladder). -//! The square-root spread interpolates between constant-SNR -//! (every band equally precise relative to itself — wasteful on -//! the leakage skirts of tonal signals) and a flat noise floor -//! (all precision on the loudest band): a band 40 dB below the -//! frame peak is quantized ~20 dB more coarsely, and a band whose -//! target falls below one quantizer step is culled to `ZERO_HCB` -//! outright — a first-order simultaneous-masking model. -//! * **Rate loop** — an outer loop adds a uniform offset to every -//! band's scalefactor (coarsening all quantizers by 1.5 dB per -//! step, the §4.6.2.3.3 quarter-step ladder ×2) until the assembled -//! frame fits the per-frame byte budget derived from the requested -//! bitrate. -//! * **Codebook / section choice** — measured bit cost: a dynamic -//! program over section boundaries picks, for every candidate run -//! of same-class bands, the single Table 4.95 book (1..=11) whose -//! *actual* coded size — Huffman codewords + sign bits + escapes, -//! measured with the real tuple writer — plus the `section_data()` -//! header overhead is minimal (see [`optimize_group_sections`]). -//! This subsumes the classic smallest-LAV-fit + merge-equal-books -//! rule and additionally exploits the signed/unsigned sibling -//! books and header-saving LAV upgrades. -//! * **Pulse escape (§4.4.6.3, measured)** — a long-frame band whose -//! few outlier lines force it onto a large-LAV book or into -//! §4.6.3.3 escape sequences is also priced with a Table 4.7 -//! `pulse_data()` variant: up to four outliers are reduced toward -//! zero to the magnitude floor of the rest of the band (4-bit -//! `amp` reach) and the `(offset, amp)` chain rides the pulse -//! record ([`extract_pulse_candidate`] picks the best-saving band -//! by per-band measured cost). The decoder's §4.6.3.3 fix-up -//! restores the *identical* quantized spectrum before -//! dequantization, so the choice is purely a noiseless-coding one -//! and is settled end-to-end by [`channel_wire_bits`] — the -//! variant is kept only when the whole channel stream measures -//! smaller. -//! * **Stereo** — a CPE with `common_window == 1` (one shared -//! `ics_info()`) and per-band §4.6.8.1 M/S coding: for each -//! scalefactor band the encoder forms `m = (l+r)/2`, -//! `s = (l−r)/2` (the exact forward matrix of the normative -//! `l = m+s` / `r = m−s` de-matrix) and selects M/S when it moves -//! the band's energy into one dominant channel — i.e. when -//! `min(e_m, e_s) ≤ (e_l + e_r) / 8` (the transformed pair is at -//! least ~9 dB lopsided, so the quiet one culls or codes cheaply). -//! The mask is emitted as `ms_mask_present = 2` when every band -//! flags (identical / phase-inverted channels), `1` + explicit -//! mask when mixed, `0` when no band benefits. `EIGHT_SHORT` -//! frames decide per `(window group, sfb)` under the pair's joint -//! grouping — the Table 4.5 `ms_used[g][sfb]` granularity -//! ([`ms_decide_short`]). -//! * **Intensity stereo (§4.6.8.2, opt-in)** — with -//! [`StreamEncoder::set_intensity_stereo`], a high-frequency -//! long-frame CPE band whose channels correlate above -//! [`IS_CORR_MIN`] is transmitted once: the right channel's band -//! becomes the intensity pseudo codebook (15 in-phase / 14 -//! out-of-phase) carrying only `is_pos = 2·log2(e_l/e_r)` on the -//! §4.6.8.1.4 DPCM track; the decoder derives -//! `r = ±0.5^(0.25·is_pos)·l` (§4.6.8.2.3). IS bands are excluded -//! from the M/S mask (per-band mutual exclusion; a set `ms_used` -//! bit would signal phase reversal instead). -//! * **TNS (§4.6.9, default on)** — per analysis window, the -//! [`crate::encoder_tns`] pass measures the prediction gain of an -//! LPC over the coverable spectral region (Levinson-Durbin on the -//! coefficient autocorrelation); a window whose gain clears the -//! threshold transmits one upward Table 4.54 filter (PARCOR -//! quantised on the §4.6.9.3 4-bit arcsine grid) and the spectrum -//! is passed through the §4.6.7.4.1 all-zero analysis filter -//! derived from the *wire* coefficients — the exact inverse of the -//! decoder's §4.6.9.3 all-pole synthesis, run per channel in the -//! L/R domain before the M/S forward matrix (mirroring the -//! decoder's M/S-then-TNS order). See [`StreamEncoder::set_tns`]. -//! * **PNS (§4.6.13, opt-in)** — with [`StreamEncoder::set_pns`], a -//! long-frame band whose energy is spread across most of its -//! coefficients (density `(Σ|x|)²/(width·Σx²)` above 0.4 — dense -//! noise measures `≈2/π`, `k` spectral lines `≈k/width`) is -//! transmitted as a `NOISE_HCB` band carrying only its energy -//! (`noise_nrg = round(4·log2‖band‖₂)`, the `2^(0.25·nrg)` ladder) -//! on the §4.6.13 DPCM track; the decoder re-synthesises the band -//! from its own generator at exactly that L2 norm. In a CPE the -//! decision runs per channel *before* the M/S matrix (mutual -//! exclusion, §4.6.13.5); a both-channels-noise band correlating -//! above [`PNS_CORR_MIN`] sets its `ms_used` bit — the §4.6.13.3 -//! correlated-noise signal (same random vector both channels), not -//! an M/S flag. Off by default: -//! a single-frame statistic cannot tell true noise from -//! noise-shaped deterministic content (sweeps, dense leakage -//! floors), which substitutes with the right energy but the wrong -//! waveform — the default-on decision awaits a cross-frame -//! tonality measure. -//! -//! ## Conformance envelope -//! -//! The assembled frame respects the wire-format hard limits: -//! scalefactors clamp to `0..=255` (8-bit `global_gain` seed) with -//! consecutive DPCM deltas in `−60..=+60` (Table 4.A.1's codeword -//! range), quantized magnitudes cap at -//! [`crate::spectral_codebook::MAX_QUANT`] (8191, the §4.6.3.3 ESC -//! ceiling), and `aac_frame_length` stays within its 13-bit field. - -use crate::adts::{AdtsHeader, ADTS_HEADER_BYTES_NO_CRC, ADTS_SAMPLE_RATES_HZ}; -use crate::encoder_tns::detect_and_apply_tns; -use crate::filterbank::{ - forward_mdct, long_sequence_window, short_window_j, SHORT_SEQ_HOP, SHORT_SEQ_START, -}; -use crate::ics_body::IcsBody; -use crate::ics_info::{ - IcsInfo, WindowSequence, WindowShape, NUM_SWB_LONG_WINDOW, NUM_SWB_SHORT_WINDOW, -}; -use crate::pulse_data::{Pulse, PulseData, MAX_PULSES}; -use crate::raw_data_block::{FrameAssembler, IdSynEle}; -use crate::scale_factor_data::{ - differentiate, AbsoluteScaleFactorEntry, AbsoluteScaleFactors, NOISE_OFFSET, -}; -use crate::section_data::{ - Section, SectionData, INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB, ZERO_HCB, -}; -use crate::spectral_codebook::MAX_QUANT; -use crate::spectral_data::SpectralData; -use crate::swb_offset::{ - long_window_offsets, short_window_offsets, LONG_WINDOW_LEN, SHORT_WINDOW_LEN, -}; -use crate::tns_data::TnsData; -use crate::{Error, Result}; - -use oxideav_core::bits::BitWriter; - -/// Historical direct-factory endpoint (the crate convention's -/// `::encoder::make_encoder` path) — re-exported from -/// [`crate::codec_encoder`]. -pub use crate::codec_encoder::make_encoder; - -/// Samples per channel per AAC frame (the 1024-line transform -/// family this crate implements). -pub const FRAME_LEN: usize = LONG_WINDOW_LEN as usize; - -/// The long transform length `N = 2048`. -const LONG_TRANSFORM_LEN: usize = 2 * FRAME_LEN; - -/// §4.6.2.3.3 `SF_OFFSET` — the scalefactor of unit gain. -const SF_OFFSET: i32 = 100; - -/// Table 4.53 DPCM delta bound (the Table 4.A.1 codeword range). -const MAX_SF_DELTA: i32 = 60; - -/// Target magnitude the *loudest* band's peak coefficient quantizes -/// to before the rate loop engages; quieter bands scale down with -/// the square-root masking spread. -const TARGET_PEAK_MAG: f64 = 42.0; - -/// Masking-spread exponent: a band's target magnitude is -/// `TARGET_PEAK_MAG · (peak_b / peak_frame)^SPREAD`. `0` would be -/// constant-SNR, `1` a flat noise floor; `½` splits the difference. -const SPREAD: f64 = 0.5; - -/// Cull threshold: a band whose spread target magnitude falls below -/// this fraction of one quantizer step carries no audible content -/// relative to the frame and is sent as `ZERO_HCB`. -const MIN_TARGET_MAG: f64 = 0.7; - -/// Upper bound on rate-loop iterations. Each iteration coarsens -/// every quantizer by 3 dB (sf offset +4), so 48 iterations span -/// ~144 dB — beyond that the frame is all-zero anyway. -const MAX_RATE_ITERATIONS: usize = 48; - -/// Deepest refinement the rate loop applies when a frame comes in -/// under budget: −32 scalefactors ≈ 24 dB of extra precision -/// (magnitudes ×2^6 over the [`TARGET_PEAK_MAG`] baseline). -const MAX_REFINE_OFFSET: i32 = 32; - -/// Minimum §4.5.4 band width (coefficients) for the §4.6.13 PNS -/// noise-likeness statistic to be meaningful; narrower bands always -/// code spectrally. -const PNS_MIN_WIDTH: usize = 8; - -/// PNS density floor on the `(Σ|x|)² / (width·Σx²)` statistic: dense -/// Gaussian-like noise measures `≈ 2/π ≈ 0.64` (`(E|x|)²/E[x²]`), -/// while `k` dominant spectral lines measure `≈ k/width` — a band -/// only counts as noise when its energy is spread across most of its -/// coefficients, so leakage skirts and harmonic combs keep spectral -/// coding. -const PNS_DENSITY_MIN: f64 = 0.4; - -/// Configuration for [`StreamEncoder`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EncoderConfig { - /// Output sample rate in Hz. Must be one of the ISO/IEC 14496-3 - /// Table 1.18 rates expressible in an ADTS header (index 0..=12). - pub sample_rate: u32, - /// Channel count. Every count with a Table 1.19 default - /// `channelConfiguration` is accepted: `1` (SCE) / `2` (CPE) / - /// `3` (SCE + CPE) / `4` (SCE + CPE + SCE) / `5` (SCE + 2 CPE) / - /// `6` (5.1: SCE + 2 CPE + LFE) / `8` (7.1: SCE + 3 CPE + LFE). - /// `7` has no default configuration (a PCE-defined layout, out - /// of scope) and is rejected. Input PCM is interleaved in the - /// canonical [`crate::channel_map`] order the decoder emits - /// (5.1 = `L R C LFE Ls Rs`), and the encoder derives the - /// bitstream element order from the Table 1.19 layout. - pub channels: u8, - /// Target bitrate in bits/second. Drives the per-frame byte - /// budget of the rate loop. The output is not strictly CBR — each - /// frame independently fits its budget — but averages at or below - /// this rate. - pub bitrate: u32, -} - -impl EncoderConfig { - /// Resolve `sample_rate` to its Table 1.18 - /// `sampling_frequency_index`. Index 12 (7350 Hz) is excluded: - /// the §4.5.4 scalefactor-band tables only cover indices - /// 0..=11 (7350 Hz content conventionally ships under index 11, - /// see the staged corpus notes). - fn fs_index(&self) -> Result { - ADTS_SAMPLE_RATES_HZ - .iter() - .position(|&r| r == self.sample_rate) - .filter(|&i| i < NUM_SWB_LONG_WINDOW.len()) - .map(|i| i as u8) - .ok_or(Error::EncoderInvalidConfig) - } - - /// Per-frame payload byte budget from the bitrate: - /// `bitrate · 1024 / sample_rate` bits, minus the 7-byte ADTS - /// header, floored at a minimum that always allows a syntactically - /// valid (silent) frame. - fn frame_budget_bytes(&self) -> usize { - let bits = (self.bitrate as u64 * FRAME_LEN as u64) / self.sample_rate.max(1) as u64; - let bytes = (bits / 8) as usize; - bytes.saturating_sub(ADTS_HEADER_BYTES_NO_CRC).max(16) - } - - /// The Table 1.19 default `channelConfiguration` for this channel - /// count (see [`EncoderConfig::channels`]). - fn channel_configuration(&self) -> Result { - match self.channels { - n @ 1..=6 => Ok(n), - 8 => Ok(7), - _ => Err(Error::EncoderInvalidConfig), - } - } -} - -/// One channel element of the §4.4.2.1 `raw_data_block()` plan, with -/// element-order channel indices. -#[derive(Debug, Clone, Copy)] -enum ElementPlan { - Sce(usize), - Cpe(usize, usize), - Lfe(usize), -} - -/// The Table 1.19 element sequence for a default -/// `channelConfiguration`, indexing element-order channels. -fn element_plan(channel_configuration: u8) -> Result> { - use ElementPlan::*; - Ok(match channel_configuration { - 1 => vec![Sce(0)], - 2 => vec![Cpe(0, 1)], - 3 => vec![Sce(0), Cpe(1, 2)], - 4 => vec![Sce(0), Cpe(1, 2), Sce(3)], - 5 => vec![Sce(0), Cpe(1, 2), Cpe(3, 4)], - 6 => vec![Sce(0), Cpe(1, 2), Cpe(3, 4), Lfe(5)], - 7 => vec![Sce(0), Cpe(1, 2), Cpe(3, 4), Cpe(5, 6), Lfe(7)], - _ => return Err(Error::EncoderInvalidConfig), - }) -} - -/// §4.5.2.1.3: only the lowest 12 spectral coefficients of an LFE -/// element may be non-zero. -const LFE_MAX_LINES: usize = 12; - -/// A streaming AAC-LC encoder producing one ADTS frame per -/// 1024-sample input hop. -/// -/// Feed interleaved `i16` PCM via [`StreamEncoder::encode_all`] (one -/// shot), or drive [`StreamEncoder::encode_frame`] hop by hop and -/// finish with [`StreamEncoder::finish`] to flush the analysis -/// overlap. The encoder delay is exactly [`FRAME_LEN`] samples: the -/// first decoded frame of the round-trip is silence, and decoded -/// frame `f ≥ 1` reconstructs input hop `f − 1`. -#[derive(Debug, Clone)] -pub struct StreamEncoder { - config: EncoderConfig, - fs_index: u8, - /// The Table 1.19 `channelConfiguration` derived from the channel - /// count (lands in the ADTS header and fixes the element plan). - channel_configuration: u8, - /// Canonical-input channel index feeding each *element-order* - /// channel slot — the inverse of the decoder's - /// [`crate::channel_map::reorder_permutation`], so encode ∘ - /// decode is channel-identity. - element_src: Vec, - /// Element-order slots that belong to an LFE element (§4.5.2.1.3 - /// restrictions apply: long-only analysis, no TNS, ≤ 12 lines). - lfe_slot: Vec, - /// Per-channel previous input hop (the left half of the next - /// analysis window), [`FRAME_LEN`] samples each, in *element - /// order*. Starts all-zero (the priming frame). - history: Vec>, - /// `window_sequence` of the previously emitted frame — drives - /// the §4.6.11.3.2 block-switching state machine. - prev_seq: WindowSequence, - /// The previous frame flagged a transient in what is now the - /// history hop, so this frame *must* be `EIGHT_SHORT_SEQUENCE` - /// (the `LONG_START → EIGHT_SHORT` contract). - short_pending: bool, - /// §4.6.13 PNS emission toggle — see [`StreamEncoder::set_pns`]. - pns_enabled: bool, - /// §4.6.9 TNS emission toggle — see [`StreamEncoder::set_tns`]. - tns_enabled: bool, - /// §4.6.8.2 intensity-stereo emission toggle — see - /// [`StreamEncoder::set_intensity_stereo`]. - is_enabled: bool, -} - -impl StreamEncoder { - /// Build an encoder for `config`. - /// - /// Errors with [`Error::EncoderInvalidConfig`] when the sample - /// rate is not a Table 1.18 ADTS rate, the channel count has no - /// Table 1.19 default configuration (see - /// [`EncoderConfig::channels`]), or the bitrate is 0. - pub fn new(config: EncoderConfig) -> Result { - let fs_index = config.fs_index()?; - let channel_configuration = config.channel_configuration()?; - if config.bitrate == 0 { - return Err(Error::EncoderInvalidConfig); - } - let ch = config.channels as usize; - // canonical[i] = element[perm[i]] on the decode side, so the - // element slot j sources canonical input channel i with - // perm[i] == j. - let perm = crate::channel_map::reorder_permutation(channel_configuration) - .ok_or(Error::EncoderInvalidConfig)?; - let mut element_src = vec![0usize; ch]; - for (i, &j) in perm.iter().enumerate() { - element_src[j] = i; - } - let mut lfe_slot = vec![false; ch]; - for elem in element_plan(channel_configuration)? { - if let ElementPlan::Lfe(slot) = elem { - lfe_slot[slot] = true; - } - } - Ok(Self { - config, - fs_index, - channel_configuration, - element_src, - lfe_slot, - history: vec![vec![0.0; FRAME_LEN]; ch], - prev_seq: WindowSequence::OnlyLong, - short_pending: false, - pns_enabled: false, - tns_enabled: true, - is_enabled: false, - }) - } - - /// Enable / disable §4.6.13 PNS emission (default **off**). - /// - /// When enabled, long frames transmit dense noise-like bands - /// (see the module docs) as `NOISE_HCB` energies instead of - /// spectra — a large bitrate win on noise content, validated - /// energy-exact through the decoder's §4.6.13.3 synthesis. In a - /// CPE the decision runs per channel on the pre-M/S spectra - /// (PNS and M/S are mutually exclusive per band, §4.6.13.5); - /// a band both channels noise-code whose content correlates - /// above [`PNS_CORR_MIN`] additionally sets its `ms_used` bit, - /// signalling the decoder to synthesise the *same* random - /// vector into both channels (§4.6.13.3 correlated noise). It - /// stays opt-in because a *single-frame* spectral statistic - /// cannot distinguish true noise from noise-shaped deterministic - /// content (a frequency sweep, a dense leakage floor): those - /// substitute with the right energy but the wrong waveform. - /// Turning the default on awaits a cross-frame tonality / - /// predictability measure. - pub fn set_pns(&mut self, enabled: bool) { - self.pns_enabled = enabled; - } - - /// Enable / disable §4.6.9 TNS emission (default **on**). - /// - /// When enabled, each analysis window whose spectrum shows a - /// prediction gain above the [`crate::encoder_tns::TNS_GAIN_MIN`] - /// threshold (a strongly non-flat temporal envelope) transmits a - /// Table 4.54 noise-shaping filter, and the spectrum is passed - /// through the matching §4.6.7.4.1 all-zero analysis filter - /// before quantisation. The decoder's §4.6.9.3 all-pole synthesis - /// pass is the exact inverse of the applied (wire-quantised) - /// filter, so TNS is transparent to the reconstruction while - /// confining quantisation noise under the signal's temporal - /// envelope. Safe to leave on: windows without a clear envelope - /// simply carry no filter. - pub fn set_tns(&mut self, enabled: bool) { - self.tns_enabled = enabled; - } - - /// Enable / disable §4.6.8.2 intensity-stereo emission (default - /// **off**). - /// - /// When enabled, a high-frequency scalefactor band of a - /// long-frame CPE whose two channels are strongly correlated - /// (normalised cross-correlation above - /// [`IS_CORR_MIN`]) is transmitted **once**: the left channel - /// carries its spectrum, the right channel's band becomes the - /// pseudo codebook `INTENSITY_HCB` (15, in-phase) or - /// `INTENSITY_HCB2` (14, out-of-phase) with an intensity - /// position `is_pos = 2·log2(e_l/e_r)` on the §4.6.8.1.4 DPCM - /// track, and the decoder derives - /// `r = ±0.5^(0.25·is_pos) · l` per §4.6.8.2.3. Such bands are - /// excluded from the M/S mask (M/S, IS and PNS are mutually - /// exclusive per band, and a set `ms_used` bit on an intensity - /// band would signal the §4.6.8.2.3 phase reversal instead). - /// Off by default: intensity coding discards the side - /// information of the pair (only the energy ratio survives), a - /// perceptual trade appropriate for low-rate coding but not for - /// transparent transcodes. - pub fn set_intensity_stereo(&mut self, enabled: bool) { - self.is_enabled = enabled; - } - - /// The configuration this encoder was built with. - pub fn config(&self) -> &EncoderConfig { - &self.config - } - - /// Encode one 1024-sample-per-channel hop of interleaved `i16` - /// PCM into one complete ADTS frame. - /// - /// `interleaved` must hold at most `1024 × channels` samples; a - /// shorter slice (the stream tail) is zero-padded. The analysis - /// window spans the previous hop and this one, so the emitted - /// frame carries the overlap-add contribution of both. - pub fn encode_frame(&mut self, interleaved: &[i16]) -> Result> { - let ch = self.config.channels as usize; - if interleaved.len() > FRAME_LEN * ch || interleaved.len() % ch != 0 { - return Err(Error::EncoderInvalidConfig); - } - // De-interleave onto the ±32768 axis the §4.6.11 output - // contract uses (no rescaling — the decoder's PCM stage - // rounds these values back to i16 directly), permuting the - // canonical input order into bitstream element order - // (`element_src` — the inverse of the decoder's Table 1.19 - // output reorder). - let mut cur: Vec> = vec![vec![0.0; FRAME_LEN]; ch]; - for (j, chan) in cur.iter_mut().enumerate() { - let src = self.element_src[j]; - for (n, slot) in chan.iter_mut().take(interleaved.len() / ch).enumerate() { - *slot = f64::from(interleaved[n * ch + src]); - } - } - let frame = self.encode_hop(&cur)?; - self.history = cur; - Ok(frame) - } - - /// Flush the final analysis overlap: emits one trailing ADTS - /// frame whose window covers the last real hop and a zero hop. - pub fn finish(&mut self) -> Result> { - let ch = self.config.channels as usize; - let zeros: Vec> = vec![vec![0.0; FRAME_LEN]; ch]; - let frame = self.encode_hop(&zeros)?; - self.history = zeros; - Ok(frame) - } - - /// One-shot convenience: encode a whole interleaved `i16` buffer - /// to a complete ADTS stream (`⌈n/1024⌉ + 1` frames — the `+1` - /// is the [`StreamEncoder::finish`] flush). - pub fn encode_all(&mut self, interleaved: &[i16]) -> Result> { - let ch = self.config.channels as usize; - if interleaved.len() % ch != 0 { - return Err(Error::EncoderInvalidConfig); - } - let mut out = Vec::new(); - let hop = FRAME_LEN * ch; - let mut chunks = interleaved.chunks(hop); - // Always emit at least one content frame (an empty input - // yields one silent frame + the flush frame). - let first = chunks.next().unwrap_or(&[]); - out.extend_from_slice(&self.encode_frame(first)?); - for chunk in chunks { - out.extend_from_slice(&self.encode_frame(chunk)?); - } - out.extend_from_slice(&self.finish()?); - Ok(out) - } - - /// Window `[history | cur]`, transform, quantize under the rate - /// loop, and wrap the raw data block in an ADTS header. - fn encode_hop(&mut self, cur: &[Vec]) -> Result> { - let ch = self.config.channels as usize; - - // §4.6.11.3.2 block-switching state machine. A transient in - // `cur` means the *next* frame (whose window's left half is - // `cur`) must be EIGHT_SHORT; this frame becomes the - // LONG_START lead-in (or stays short if a short run is - // already active). A pending short from the previous hop - // forces EIGHT_SHORT now; a short run with no continuation - // exits through LONG_STOP. - // LFE channels neither trigger nor follow block switching — - // §4.5.2.1.3 fixes their window_sequence to ONLY_LONG. - let transient = self - .history - .iter() - .zip(cur.iter()) - .zip(self.lfe_slot.iter()) - .any(|((h, c), &lfe)| !lfe && detect_transient(h, c)); - let seq = if self.short_pending { - WindowSequence::EightShort - } else if transient && self.prev_seq != WindowSequence::EightShort { - WindowSequence::LongStart - } else if self.prev_seq == WindowSequence::EightShort { - if transient { - WindowSequence::EightShort - } else { - WindowSequence::LongStop - } - } else { - WindowSequence::OnlyLong - }; - self.short_pending = transient; - - // Per-channel analysis transform for the chosen sequence - // (LFE channels always analyze ONLY_LONG — their own window - // chain stays long/sine per §4.5.2.1.3, independent of the - // frame's switching state). - let mut spectra: Vec> = Vec::with_capacity(ch); - for ((hist, chan), &lfe) in self - .history - .iter() - .zip(cur.iter()) - .zip(self.lfe_slot.iter()) - { - let ch_seq = if lfe { WindowSequence::OnlyLong } else { seq }; - spectra.push(analyze_channel(hist, chan, ch_seq)?); - } - self.prev_seq = seq; - - // §4.6.9 TNS: per-channel decision + analysis filtering, - // BEFORE the M/S forward matrix — the decoder applies TNS - // synthesis per channel *after* the M/S de-matrix - // (§4.6.9.3's place in the §4.6 tool chain), so the encoder's - // analysis pass runs in the L/R domain. The filtering mutates - // the spectra once, outside the rate loop (the filter choice - // is independent of the scalefactor offset). - let max_sfb = if seq == WindowSequence::EightShort { - NUM_SWB_SHORT_WINDOW[self.fs_index as usize] - } else { - NUM_SWB_LONG_WINDOW[self.fs_index as usize] - }; - let mut tns: Vec> = vec![None; ch]; - if self.tns_enabled { - for (j, (spec, slot)) in spectra.iter_mut().zip(tns.iter_mut()).enumerate() { - if self.lfe_slot[j] { - continue; // §4.5.2.1.3: no TNS on an LFE element - } - let permit = tns_temporal_permits(&self.history[j], &cur[j], seq); - *slot = detect_and_apply_tns(spec, seq, max_sfb, self.fs_index, &permit)?; - } - } - - // Rate loop: uniform scalefactor offset in ±4 steps (3 dB - // per step on the §4.6.2.3.3 quarter-step ladder). Coarsen - // until the raw data block fits the budget; when it already - // fits, refine (spend the remaining budget on precision) as - // long as the finer frame still fits, down to - // `-MAX_REFINE_OFFSET`. - let budget = self.config.frame_budget_bytes(); - let mut sf_offset = 0i32; - let mut raw_block = self.assemble_raw_block(seq, &spectra, &tns, sf_offset)?; - let mut iterations = 0usize; - if raw_block.len() > budget { - while raw_block.len() > budget && iterations < MAX_RATE_ITERATIONS { - sf_offset += 4; - raw_block = self.assemble_raw_block(seq, &spectra, &tns, sf_offset)?; - iterations += 1; - } - } else { - while sf_offset > -MAX_REFINE_OFFSET && iterations < MAX_RATE_ITERATIONS { - let finer = self.assemble_raw_block(seq, &spectra, &tns, sf_offset - 4)?; - if finer.len() > budget { - break; - } - sf_offset -= 4; - raw_block = finer; - iterations += 1; - } - } - // Fine pass: the ±4 ladder can leave up to 3 scalefactors of - // precision unspent at the budget boundary (a full −4 step - // un-zeros a whole swath of near-threshold coefficients at - // once). Try the intermediate offsets, finest first. - if raw_block.len() <= budget && sf_offset > -MAX_REFINE_OFFSET { - for fine in [3i32, 2, 1] { - let cand = self.assemble_raw_block(seq, &spectra, &tns, sf_offset - fine)?; - if cand.len() <= budget { - raw_block = cand; - break; - } - } - } - - // ADTS wrap. aac_frame_length is 13 bits; the budget floor - // (16 bytes) and MAX_RATE_ITERATIONS guarantee headroom for - // every realistic configuration, but validate regardless. - let frame_len = ADTS_HEADER_BYTES_NO_CRC + raw_block.len(); - if frame_len >= (1 << 13) { - return Err(Error::EncoderFrameOverflow); - } - let header = AdtsHeader { - mpeg_version_mpeg2: false, - protection_absent: true, - profile: 1, // AAC LC: profile_ObjectType = AOT − 1 = 1 - sampling_frequency_index: self.fs_index, - channel_configuration: self.channel_configuration, - aac_frame_length: frame_len as u16, - adts_buffer_fullness: 0x7FF, // VBR sentinel - number_of_raw_data_blocks_in_frame: 1, - }; - let mut out = Vec::with_capacity(frame_len); - out.extend_from_slice(&header.write()?); - out.extend_from_slice(&raw_block); - Ok(out) - } - - /// Assemble one `raw_data_block()` — the Table 1.19 element plan - /// for the configuration (SCE / `common_window` CPE / LFE - /// elements, then END) at the given rate-loop scalefactor offset. - /// - /// `tns` carries the per-channel §4.6.9 filter records decided - /// once per hop (the spectra arrive already analysis-filtered); - /// they land in each channel's `tns_data_present` / `tns_data` - /// wire slots. Element instance tags count up per element kind, - /// so the decoder's per-`(id, tag)` state slots stay distinct. - fn assemble_raw_block( - &self, - seq: WindowSequence, - spectra: &[Vec], - tns: &[Option], - sf_offset: i32, - ) -> Result> { - let mut asm = FrameAssembler::new(); - let plan = element_plan(self.channel_configuration)?; - let mut sce_tag = 0u8; - let mut cpe_tag = 0u8; - let mut lfe_tag = 0u8; - for elem in plan { - let mut body_bits = BitWriter::new(); - match elem { - ElementPlan::Sce(ch) => { - asm.push_channel_header(IdSynEle::Sce, sce_tag)?; - sce_tag += 1; - self.assemble_sce(&mut body_bits, &spectra[ch], &tns[ch], seq, sf_offset)?; - } - ElementPlan::Lfe(ch) => { - asm.push_channel_header(IdSynEle::Lfe, lfe_tag)?; - lfe_tag += 1; - self.assemble_lfe(&mut body_bits, &spectra[ch], sf_offset)?; - } - ElementPlan::Cpe(l, r) => { - asm.push_channel_header(IdSynEle::Cpe, cpe_tag)?; - cpe_tag += 1; - self.assemble_cpe( - &mut body_bits, - &spectra[l], - &spectra[r], - &tns[l], - &tns[r], - seq, - sf_offset, - )?; - } - } - let nbits = body_bits.bit_position(); - asm.push_channel_body_bits(&body_bits.finish(), nbits)?; - } - Ok(asm.push_end()) - } - - /// Quantize a spectrum for this frame: the grouped short-window - /// path for `EIGHT_SHORT`, the long path otherwise. - #[allow(clippy::too_many_arguments)] - fn quantize_seq( - &self, - spec: &[f64], - seq: WindowSequence, - sf_offset: i32, - peak: f64, - pns_bands: &[bool], - is_bands: &[IsBand], - ) -> Result { - if seq == WindowSequence::EightShort { - quantize_channel_short(spec, self.fs_index, sf_offset, peak) - } else { - quantize_channel( - spec, - seq, - self.fs_index, - sf_offset, - peak, - pns_bands, - is_bands, - ) - } - } - - /// One SCE body: quantize (with the mono blanket PNS allowance - /// when enabled on a long frame) and write - /// `individual_channel_stream(0)`. - fn assemble_sce( - &self, - body_bits: &mut BitWriter, - spec: &[f64], - tns: &Option, - seq: WindowSequence, - sf_offset: i32, - ) -> Result<()> { - // §4.6.13 PNS emission is opt-in (set_pns) and long-frame - // only; a lone channel grants a blanket per-band allowance - // (the noise-likeness test in quantize_group decides). - let pns_long = self.pns_enabled && seq != WindowSequence::EightShort; - let num_swb_long = NUM_SWB_LONG_WINDOW[self.fs_index as usize] as usize; - let peak = spec.iter().fold(0.0f64, |m, &v| m.max(v.abs())); - let pns_bands = if pns_long { - vec![true; num_swb_long] - } else { - Vec::new() - }; - let mut chan = self.quantize_seq(spec, seq, sf_offset, peak, &pns_bands, &[])?; - attach_tns(&mut chan, tns); - chan.body.write(body_bits, 2, self.fs_index, false)?; - chan.spectral.write( - body_bits, - &chan.info, - &chan.body.section_data, - self.fs_index, - ) - } - - /// One LFE body under the §4.5.2.1.3 restrictions: the element is - /// a plain `individual_channel_stream(0)`, always - /// `ONLY_LONG_SEQUENCE` with the sine window (the analysis stage - /// already ran this channel long), no TNS / PNS / prediction, and - /// only the lowest [`LFE_MAX_LINES`] spectral lines non-zero. - fn assemble_lfe(&self, body_bits: &mut BitWriter, spec: &[f64], sf_offset: i32) -> Result<()> { - let mut lfe_spec = spec.to_vec(); - for c in lfe_spec[LFE_MAX_LINES..].iter_mut() { - *c = 0.0; - } - let peak = lfe_spec.iter().fold(0.0f64, |m, &v| m.max(v.abs())); - let chan = self.quantize_seq( - &lfe_spec, - WindowSequence::OnlyLong, - sf_offset, - peak, - &[], - &[], - )?; - chan.body.write(body_bits, 2, self.fs_index, false)?; - chan.spectral.write( - body_bits, - &chan.info, - &chan.body.section_data, - self.fs_index, - ) - } - - /// One `common_window` CPE body: the per-band IS / PNS / M/S - /// decisions, joint quantization on the pair peak, and the - /// shared-`ics_info` wire assembly. - #[allow(clippy::too_many_arguments)] - fn assemble_cpe( - &self, - body_bits: &mut BitWriter, - l_spec: &[f64], - r_spec: &[f64], - tns_l: &Option, - tns_r: &Option, - seq: WindowSequence, - sf_offset: i32, - ) -> Result<()> { - let pns_long = self.pns_enabled && seq != WindowSequence::EightShort; - // §4.6.8.2: per-band intensity decision first (opt-in, - // long frames) — an IS band is transmitted once via - // the left channel and must not also be M/S-coded - // (mutual exclusion; a set ms_used bit on an - // intensity band signals the §4.6.8.2.3 phase - // reversal, not an M/S de-matrix). - let is_bands: Vec = if self.is_enabled && seq != WindowSequence::EightShort { - is_decide(l_spec, r_spec, self.fs_index)? - } else { - Vec::new() - }; - // §4.6.13 per-band PNS decision on the original l/r - // spectra (a noise band must not be M/S-transformed — - // mutual exclusion, §4.6.13.5 — and IS wins where the - // two overlap). - let pns = if pns_long { - pns_decide_pair(l_spec, r_spec, &is_bands, self.fs_index)? - } else { - PairPns::default() - }; - // §4.6.8.1: per-band M/S decision, then quantize the coding - // spectra (m/s on flagged bands, l/r elsewhere). Both coding - // channels share the pair's loudest peak for the masking - // spread, so the side channel's noise floor is judged - // against the pair, not against its own (often tiny) peak. - // The mask is one row per window group (`ms_used[g][sfb]`); - // long sequences have one group, EIGHT_SHORT frames decide - // per (group, sfb) under the jointly-decided grouping. - let (ms_rows, mut left, mut right); - if seq == WindowSequence::EightShort { - // A common_window CPE shares one ics_info, so the - // §4.5.2.3.4 grouping is decided ONCE on the pair - // envelope (per-coefficient L/R energy sum) and imposed - // on both channels — independent decisions could - // diverge and desync the shared wire layout. - let combined: Vec = l_spec - .iter() - .zip(r_spec.iter()) - .map(|(&l, &r)| (l * l + r * r).sqrt()) - .collect(); - let offsets = short_window_offsets(self.fs_index)?; - let num_swb = NUM_SWB_SHORT_WINDOW[self.fs_index as usize] as usize; - let wgl = decide_short_grouping(&combined, offsets, num_swb); - let rows = ms_decide_short(l_spec, r_spec, offsets, num_swb, &wgl); - let (code_l, code_r) = if rows.iter().flatten().any(|&b| b) { - apply_ms_short(l_spec, r_spec, &rows, offsets, &wgl) - } else { - (l_spec.to_vec(), r_spec.to_vec()) - }; - let pair_peak = code_l - .iter() - .chain(code_r.iter()) - .fold(0.0f64, |m, &v| m.max(v.abs())); - left = quantize_channel_short_grouped( - &code_l, - self.fs_index, - sf_offset, - pair_peak, - wgl.clone(), - )?; - right = - quantize_channel_short_grouped(&code_r, self.fs_index, sf_offset, pair_peak, wgl)?; - ms_rows = rows; - } else { - let mut ms_used = ms_decide(l_spec, r_spec, self.fs_index)?; - for (sfb, band) in is_bands.iter().enumerate() { - if band.is_some() { - if let Some(m) = ms_used.get_mut(sfb) { - *m = false; - } - } - } - // A band either channel will noise-code is excluded - // from the M/S transform; a both-channels-noise band - // whose content correlates re-sets its ms_used bit, - // which per §4.6.13.3 signals the decoder to draw the - // *same* random vector for both channels (correlated - // noise) rather than an M/S de-matrix. - for (sfb, m) in ms_used.iter_mut().enumerate() { - let l_n = pns.l_noise.get(sfb).copied().unwrap_or(false); - let r_n = pns.r_noise.get(sfb).copied().unwrap_or(false); - if l_n || r_n { - *m = pns.shared.get(sfb).copied().unwrap_or(false); - } - } - let ms_transform: Vec = ms_used - .iter() - .enumerate() - .map(|(sfb, &m)| { - m && !pns.l_noise.get(sfb).copied().unwrap_or(false) - && !pns.r_noise.get(sfb).copied().unwrap_or(false) - }) - .collect(); - let (code_l, code_r) = if ms_transform.iter().any(|&b| b) { - apply_ms(l_spec, r_spec, &ms_transform, self.fs_index)? - } else { - (l_spec.to_vec(), r_spec.to_vec()) - }; - let pair_peak = code_l - .iter() - .chain(code_r.iter()) - .fold(0.0f64, |m, &v| m.max(v.abs())); - left = self.quantize_seq(&code_l, seq, sf_offset, pair_peak, &pns.l_noise, &[])?; - right = - self.quantize_seq(&code_r, seq, sf_offset, pair_peak, &pns.r_noise, &is_bands)?; - ms_rows = vec![ms_used]; - } - attach_tns(&mut left, tns_l); - attach_tns(&mut right, tns_r); - - // §4.4.2.3: common_window = 1, one shared ics_info, - // then the two-bit ms_mask_present (+ mask when 1: one bit - // per (window group, sfb), group-major — Table 4.5). - body_bits.write_bit(true); - left.info.write(body_bits, 2, self.fs_index, true)?; - let any_ms = ms_rows.iter().flatten().any(|&b| b); - let all_ms = !ms_rows.is_empty() - && ms_rows.iter().all(|row| !row.is_empty()) - && ms_rows.iter().flatten().all(|&b| b); - if all_ms { - body_bits.write_u32(2, 2); // all ones, no mask bits - } else if any_ms { - body_bits.write_u32(1, 2); - for &b in ms_rows.iter().flatten() { - body_bits.write_bit(b); - } - } else { - body_bits.write_u32(0, 2); - } - for chan in [&left, &right] { - chan.body - .write_with_ics_info(body_bits, &chan.info, 2, false)?; - chan.spectral.write( - body_bits, - &chan.info, - &chan.body.section_data, - self.fs_index, - )?; - } - - Ok(()) - } -} - -/// Attach a channel's §4.6.9 TNS record to its wire body. -fn attach_tns(chan: &mut QuantizedChannel, slot: &Option) { - if let Some(t) = slot { - chan.body.tns_data_present = true; - chan.body.tns_data = Some(t.clone()); - } -} - -/// Subblock length of the transient detector — one short-window hop -/// (128 samples), so a detected attack aligns with the short-window -/// grid it triggers. -const TRANSIENT_SUBBLOCK: usize = SHORT_SEQ_HOP; - -/// Energy jump (×) a subblock must show over the running average of -/// the preceding subblocks to count as a transient attack. -const TRANSIENT_RATIO: f64 = 12.0; - -/// Absolute per-subblock energy floor below which an attack is -/// ignored (silence-to-quiet transitions don't warrant short -/// windows): a 128-sample block at ~±180 amplitude. -const TRANSIENT_FLOOR: f64 = 128.0 * 180.0 * 180.0; - -/// Minimum established (pre-attack) average subblock energy for the -/// detector to arm. Below this the context is effectively silence -/// and an onset codes acceptably with the long-window pair (its -/// left flank is silence — there is no signal to smear pre-echo -/// into), so the detector stays quiet rather than switching on -/// every stream/passage onset. -const TRANSIENT_ARM: f64 = TRANSIENT_FLOOR / TRANSIENT_RATIO; - -/// Detect a transient attack inside one channel's next hop. -/// -/// The 2048-sample context `[hist | cur]` is split into sixteen -/// 128-sample subblocks; an attack fires when a subblock **in the -/// `cur` half** has energy that (a) clears the absolute -/// [`TRANSIENT_FLOOR`], and (b) jumps [`TRANSIENT_RATIO`]× above the -/// **maximum** energy of the preceding eight subblocks (one hop of -/// context) — provided that maximum itself clears [`TRANSIENT_ARM`] -/// (an established signal level to jump *from*). Using the recent -/// max rather than a mean keeps beat nulls in steady multi-tone -/// content from arming spurious triggers, and keeps zeroed history -/// (stream start) from diluting the reference: an onset out of true -/// digital silence codes acceptably with the long-window pair (its -/// left flank is silence — there is nothing to smear pre-echo into), -/// so the detector deliberately stays quiet there. -fn detect_transient(hist: &[f64], cur: &[f64]) -> bool { - let energies: Vec = hist - .chunks(TRANSIENT_SUBBLOCK) - .chain(cur.chunks(TRANSIENT_SUBBLOCK)) - .map(|b| b.iter().map(|&v| v * v).sum()) - .collect(); - let hist_blocks = hist.len() / TRANSIENT_SUBBLOCK; - for (j, &e) in energies.iter().enumerate().skip(hist_blocks) { - let ctx = &energies[j.saturating_sub(hist_blocks.max(1))..j]; - let reference = ctx.iter().fold(0.0f64, |m, &v| m.max(v)); - if e > TRANSIENT_FLOOR && reference > TRANSIENT_ARM && e > TRANSIENT_RATIO * reference { - return true; - } - } - false -} - -/// TNS temporal-envelope gate: minimum `max / mean` subblock-energy -/// flatness ratio of a transform window's time region for TNS to be -/// considered on it. A steady tone (or dense steady multitone) -/// measures close to 1; a burst-and-decay envelope inside the window -/// measures well above. See [`tns_temporal_permits`]. -const TNS_TEMPORAL_RATIO: f64 = 3.0; - -/// Absolute per-window mean subblock energy floor below which the -/// TNS gate stays closed (silence / near-silence windows carry no -/// audible envelope to protect). One 128-sample subblock at ~±90 -/// amplitude. -const TNS_TEMPORAL_FLOOR: f64 = 128.0 * 90.0 * 90.0; - -/// §4.6.9.1 temporal gate for the encode-side TNS decision: per -/// transform window, `true` iff the window's raw time samples show a -/// strongly non-flat energy envelope. -/// -/// The window's time region (2048 samples for a long sequence; the -/// 256-sample `SHORT_SEQ_START + j·SHORT_SEQ_HOP` slice per short -/// window) is split into 16 subblocks whose energies are reduced to -/// the `max / mean` flatness ratio; the gate opens above -/// [`TNS_TEMPORAL_RATIO`] (with a [`TNS_TEMPORAL_FLOOR`] silence -/// guard). This is the *time-domain* half of the TNS decision — the -/// spectral prediction gain alone also fires on steady tonal windows -/// (their leakage skirts are highly predictable) where the temporal -/// envelope is flat and shaping buys nothing; measuring the envelope -/// directly on the input samples keeps TNS to the transient / -/// speech-like windows it exists for (§4.6.9.1's duality argument -/// run forward). -fn tns_temporal_permits(hist: &[f64], cur: &[f64], seq: WindowSequence) -> Vec { - let region = |i: usize| -> f64 { - if i < FRAME_LEN { - hist[i] - } else { - cur[i - FRAME_LEN] - } - }; - let flatness_permits = |base: usize, len: usize| -> bool { - let sub = len / 16; - let energies: Vec = (0..16) - .map(|j| { - (0..sub) - .map(|m| { - let v = region(base + j * sub + m); - v * v - }) - .sum() - }) - .collect(); - let mean = energies.iter().sum::() / 16.0; - let max = energies.iter().fold(0.0f64, |a, &b| a.max(b)); - // Normalise the floor to the subblock length (the constant is - // stated for a 128-sample subblock). - let floor = TNS_TEMPORAL_FLOOR * sub as f64 / 128.0; - mean > floor && max > TNS_TEMPORAL_RATIO * mean - }; - if seq == WindowSequence::EightShort { - (0..8) - .map(|j| { - flatness_permits( - SHORT_SEQ_START + j * SHORT_SEQ_HOP, - 2 * SHORT_WINDOW_LEN as usize, - ) - }) - .collect() - } else { - vec![flatness_permits(0, LONG_TRANSFORM_LEN)] - } -} - -/// Run the §4.6.11.3.1 analysis transform for one channel under the -/// chosen `window_sequence`, over the 2048-sample region -/// `[hist | cur]`. -/// -/// * Long sequences: one 2048-point MDCT under the -/// [`long_sequence_window`] (sine shape throughout — this encoder -/// never switches shapes, so left/right inheritance is trivial). -/// * `EIGHT_SHORT`: eight 256-point MDCTs at offsets -/// `448 + j·128` inside the region ([`SHORT_SEQ_START`] / -/// [`SHORT_SEQ_HOP`]), each under its [`short_window_j`]; -/// concatenated window-major (`8 × 128` coefficients). -fn analyze_channel(hist: &[f64], cur: &[f64], seq: WindowSequence) -> Result> { - debug_assert_eq!(hist.len(), FRAME_LEN); - debug_assert_eq!(cur.len(), FRAME_LEN); - let region = |i: usize| -> f64 { - if i < FRAME_LEN { - hist[i] - } else { - cur[i - FRAME_LEN] - } - }; - if seq == WindowSequence::EightShort { - let short_len = SHORT_WINDOW_LEN as usize; // 128 - let n_s = 2 * short_len; // 256 - let mut out = Vec::with_capacity(8 * short_len); - for j in 0..8 { - let w = short_window_j(j, WindowShape::Sine, WindowShape::Sine); - let base = SHORT_SEQ_START + j * SHORT_SEQ_HOP; - let seg: Vec = (0..n_s).map(|m| region(base + m) * w[m]).collect(); - out.extend_from_slice(&forward_mdct(&seg, n_s)); - } - Ok(out) - } else { - let w = long_sequence_window(seq, WindowShape::Sine, WindowShape::Sine)?; - let z: Vec = (0..LONG_TRANSFORM_LEN).map(|m| region(m) * w[m]).collect(); - Ok(forward_mdct(&z, LONG_TRANSFORM_LEN)) - } -} - -/// One quantized channel, ready for wire assembly. -struct QuantizedChannel { - info: IcsInfo, - body: IcsBody, - spectral: SpectralData, -} - -/// One band's §4.6.8.2 intensity-stereo decision: `None` codes the -/// band normally; `Some((codebook, is_pos))` transmits the right -/// channel's band as the intensity book (15 in-phase / 14 -/// out-of-phase) at the given position on the `0.5^(0.25·is_pos)` -/// gain ladder. -type IsBand = Option<(u8, i32)>; - -/// Lowest spectral line an intensity-coded band may start at: -/// intensity stereo exploits the ear's insensitivity to phase at -/// high frequencies (§4.6.8.2.1), so the bottom quarter of the -/// spectrum always keeps discrete coding. -const IS_MIN_SPECTRAL_LINE: usize = FRAME_LEN / 4; - -/// Minimum normalised cross-correlation `|Σ l·r| / sqrt(Σl²·Σr²)` -/// for a band to qualify for intensity coding. Deliberately strict: -/// a genuine intensity image (shared content at a per-channel gain) -/// measures ≈ 1.0, while the leakage skirts of two *different* -/// tones — deterministic, slowly-decaying magnitude profiles — were -/// measured correlating as high as 0.93 on synthetic two-tone -/// content; IS-coding those would substitute the wrong (if masked) -/// waveform for no bit win over the cull they get anyway. -pub const IS_CORR_MIN: f64 = 0.95; - -/// Relative peak floor for the intensity decision: a band whose -/// loudest coefficient (either channel) sits more than ~50 dB below -/// the pair's frame peak carries only leakage floor — the *distant* -/// skirts of any two windowed tones are smooth deterministic decays -/// that correlate near 1.0 regardless of the tones' relation -/// (measured 0.98 between two unrelated tones' far tails), so -/// correlation alone cannot vet an image down there, and a band that -/// quiet codes for almost nothing (or culls) discretely anyway. -const IS_PEAK_FLOOR_RATIO: f64 = 3e-3; - -/// §4.6.8.2 per-band intensity-stereo decision (encode side) for a -/// long-frame channel pair. -/// -/// A band qualifies when it lies above [`IS_MIN_SPECTRAL_LINE`], -/// both channels carry energy, and the normalised cross-correlation -/// clears [`IS_CORR_MIN`]. The transmitted position quantises the -/// energy ratio onto the §4.6.8.2.3 gain ladder — -/// `0.5^(0.25·is_pos) = sqrt(e_r/e_l)` ⇒ `is_pos = 2·log2(e_l/e_r)` -/// — and the codebook carries the phase: `INTENSITY_HCB` (15) when -/// the channels correlate positively, `INTENSITY_HCB2` (14) when -/// they anti-correlate. -fn is_decide(l_spec: &[f64], r_spec: &[f64], fs_index: u8) -> Result> { - let offsets = long_window_offsets(fs_index)?; - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - let frame_peak = l_spec - .iter() - .chain(r_spec.iter()) - .fold(0.0f64, |m, &v| m.max(v.abs())); - let peak_floor = frame_peak * IS_PEAK_FLOOR_RATIO; - let mut out: Vec = vec![None; num_swb]; - for (sfb, slot) in out.iter_mut().enumerate() { - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - if start < IS_MIN_SPECTRAL_LINE { - continue; - } - let mut e_l = 0.0f64; - let mut e_r = 0.0f64; - let mut dot = 0.0f64; - let mut band_peak = 0.0f64; - for k in start..end { - e_l += l_spec[k] * l_spec[k]; - e_r += r_spec[k] * r_spec[k]; - dot += l_spec[k] * r_spec[k]; - band_peak = band_peak.max(l_spec[k].abs()).max(r_spec[k].abs()); - } - if e_l <= 0.0 || e_r <= 0.0 || band_peak < peak_floor { - continue; - } - let corr = dot.abs() / (e_l * e_r).sqrt(); - if corr < IS_CORR_MIN { - continue; - } - let pos = (2.0 * (e_l / e_r).log2()).round(); - // Keep the position within a range the ±60-delta track can - // plausibly reach; a >±30 dB imbalance codes better discretely. - if !(-80.0..=80.0).contains(&pos) { - continue; - } - let cb = if dot >= 0.0 { - INTENSITY_HCB - } else { - INTENSITY_HCB2 - }; - *slot = Some((cb, pos as i32)); - } - Ok(out) -} - -/// Minimum normalised cross-correlation for a both-channels-noise -/// band to be flagged *correlated* (§4.6.13.3): the decoder then -/// draws the **same** random vector for both channels. Positive -/// correlation only — the shared vector reproduces positively -/// correlated noise, so anti-correlated noise stays on independent -/// draws. -pub const PNS_CORR_MIN: f64 = 0.5; - -/// Per-band §4.6.13 PNS decision for a channel pair (encode side). -#[derive(Debug, Default)] -struct PairPns { - /// Left channel per-band PNS allowance (noise-like content). - l_noise: Vec, - /// Right channel per-band PNS allowance. - r_noise: Vec, - /// Both channels noise **and** correlated above - /// [`PNS_CORR_MIN`] — emitted as a set `ms_used` bit - /// (§4.6.13.3 correlated-noise signalling). - shared: Vec, -} - -/// Decide the §4.6.13 noise bands of a long-frame channel pair on -/// the original (pre-M/S) spectra. -/// -/// Per band: each channel qualifies through the same -/// [`is_noise_like`] density statistic the mono path uses; a band -/// where **both** qualify additionally measures its normalised -/// cross-correlation — above [`PNS_CORR_MIN`] the band is flagged -/// `shared`, which the CPE assembler emits as a set `ms_used` bit so -/// the decoder synthesises the same random vector into both channels -/// (§4.6.13.3; no M/S de-matrix is performed on such a band — PNS -/// and M/S are mutually exclusive, §4.6.13.5). Bands claimed by -/// intensity stereo (`is_bands`) are skipped — M/S, IS and PNS are -/// pairwise exclusive on a band. -fn pns_decide_pair( - l_spec: &[f64], - r_spec: &[f64], - is_bands: &[IsBand], - fs_index: u8, -) -> Result { - let offsets = long_window_offsets(fs_index)?; - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - let mut out = PairPns { - l_noise: vec![false; num_swb], - r_noise: vec![false; num_swb], - shared: vec![false; num_swb], - }; - for sfb in 0..num_swb { - if is_bands.get(sfb).copied().flatten().is_some() { - continue; // intensity wins the band - } - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - let l_band = &l_spec[start..end]; - let r_band = &r_spec[start..end]; - let l_n = is_noise_like(l_band); - let r_n = is_noise_like(r_band); - out.l_noise[sfb] = l_n; - out.r_noise[sfb] = r_n; - if l_n && r_n { - let e_l: f64 = l_band.iter().map(|&v| v * v).sum(); - let e_r: f64 = r_band.iter().map(|&v| v * v).sum(); - let dot: f64 = l_band.iter().zip(r_band).map(|(&a, &b)| a * b).sum(); - if e_l > 0.0 && e_r > 0.0 && dot / (e_l * e_r).sqrt() >= PNS_CORR_MIN { - out.shared[sfb] = true; - } - } - } - Ok(out) -} - -/// §4.6.8.1 per-band M/S decision for a channel pair. -/// -/// A band selects M/S coding when the mid/side transform -/// (`m = (l+r)/2`, `s = (l−r)/2`) concentrates its energy: with -/// `e_m + e_s = (e_l + e_r)/2` (exact, by the transform's geometry), -/// requiring `min(e_m, e_s) ≤ (e_l + e_r)/8` means the quieter -/// transformed channel holds at most a quarter of the transformed -/// energy (≥ ~5 dB below its partner) — it will cull or code -/// cheaply while the dominant channel carries the band once instead -/// of twice. -fn ms_decide(l_spec: &[f64], r_spec: &[f64], fs_index: u8) -> Result> { - let offsets = long_window_offsets(fs_index)?; - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - let mut used = Vec::with_capacity(num_swb); - for sfb in 0..num_swb { - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - let mut e_lr = 0.0f64; - let mut e_m = 0.0f64; - let mut e_s = 0.0f64; - for k in start..end { - let (l, r) = (l_spec[k], r_spec[k]); - e_lr += l * l + r * r; - let m = 0.5 * (l + r); - let s = 0.5 * (l - r); - e_m += m * m; - e_s += s * s; - } - used.push(e_lr > 0.0 && e_m.min(e_s) <= e_lr / 8.0); - } - Ok(used) -} - -/// Forward M/S matrix: on flagged bands the coding pair is -/// `(m, s) = ((l+r)/2, (l−r)/2)` — the exact inverse of the -/// decoder's §4.6.8.1.3 `l = m+s` / `r = m−s` de-matrix — and the -/// identity elsewhere. -fn apply_ms( - l_spec: &[f64], - r_spec: &[f64], - ms_used: &[bool], - fs_index: u8, -) -> Result<(Vec, Vec)> { - let offsets = long_window_offsets(fs_index)?; - let mut code_l = l_spec.to_vec(); - let mut code_r = r_spec.to_vec(); - for (sfb, &used) in ms_used.iter().enumerate() { - if !used { - continue; - } - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - for k in start..end { - let m = 0.5 * (l_spec[k] + r_spec[k]); - let s = 0.5 * (l_spec[k] - r_spec[k]); - code_l[k] = m; - code_r[k] = s; - } - } - Ok((code_l, code_r)) -} - -/// §4.6.8.1 per-`(window group, sfb)` M/S decision for an -/// `EIGHT_SHORT_SEQUENCE` channel pair under the shared grouping -/// `wgl` — the same energy-concentration criterion as [`ms_decide`], -/// summed over every window of the group (the mask granularity the -/// Table 4.5 `ms_used[g][sfb]` wire provides). `l_spec` / `r_spec` -/// are window-major 8 × 128 buffers. -fn ms_decide_short( - l_spec: &[f64], - r_spec: &[f64], - offsets: &[u16], - num_swb: usize, - wgl: &[u8], -) -> Vec> { - let short_len = SHORT_WINDOW_LEN as usize; - let mut rows = Vec::with_capacity(wgl.len()); - let mut win_base = 0usize; - for &len in wgl { - let mut row = Vec::with_capacity(num_swb); - for sfb in 0..num_swb { - let mut e_lr = 0.0f64; - let mut e_m = 0.0f64; - let mut e_s = 0.0f64; - for w in win_base..win_base + len as usize { - for k in offsets[sfb] as usize..offsets[sfb + 1] as usize { - let (l, r) = (l_spec[w * short_len + k], r_spec[w * short_len + k]); - e_lr += l * l + r * r; - let m = 0.5 * (l + r); - let s = 0.5 * (l - r); - e_m += m * m; - e_s += s * s; - } - } - row.push(e_lr > 0.0 && e_m.min(e_s) <= e_lr / 8.0); - } - rows.push(row); - win_base += len as usize; - } - rows -} - -/// Forward M/S butterfly on the flagged `(group, sfb)` bands of a -/// window-major short-sequence pair — the short-frame counterpart of -/// [`apply_ms`], walking every window of a flagged group. -fn apply_ms_short( - l_spec: &[f64], - r_spec: &[f64], - rows: &[Vec], - offsets: &[u16], - wgl: &[u8], -) -> (Vec, Vec) { - let short_len = SHORT_WINDOW_LEN as usize; - let mut code_l = l_spec.to_vec(); - let mut code_r = r_spec.to_vec(); - let mut win_base = 0usize; - for (row, &len) in rows.iter().zip(wgl) { - for (sfb, &used) in row.iter().enumerate() { - if !used { - continue; - } - for w in win_base..win_base + len as usize { - for k in offsets[sfb] as usize..offsets[sfb + 1] as usize { - let i = w * short_len + k; - let m = 0.5 * (l_spec[i] + r_spec[i]); - let s = 0.5 * (l_spec[i] - r_spec[i]); - code_l[i] = m; - code_r[i] = s; - } - } - } - win_base += len as usize; - } - (code_l, code_r) -} - -/// §4.6.2 forward quantizer for one coefficient at scalefactor `sf`: -/// `q = sign(x) · NINT((|x| · 2^(−0.25·(sf−100)))^(3/4))`, the exact -/// inverse of the normative `|q|^(4/3) · 2^(0.25·(sf−100))` -/// (round-half-away-from-zero per §1.3 `NINT`). -fn quantize_coef(x: f64, sf: i32) -> i32 { - let gain = (0.25 * f64::from(sf - SF_OFFSET)).exp2(); - let mag = (x.abs() / gain).powf(0.75).round(); - let mag = mag.min(f64::from(MAX_QUANT)) as i32; - if x < 0.0 { - -mag - } else { - mag - } -} - -/// The masking-spread scalefactor for a band whose peak coefficient -/// is `peak` in a frame whose loudest band peaks at `frame_peak`: -/// solve `(peak / 2^(0.25·(sf−100)))^(3/4) = M_b` for `sf` with -/// `M_b = TARGET_PEAK_MAG · (peak/frame_peak)^SPREAD`, i.e. -/// `sf = 100 + 4·log2(peak) − (16/3)·log2(M_b)`. -/// -/// Returns `None` when the band's spread target falls below -/// [`MIN_TARGET_MAG`] — such a band quantizes to silence anyway and -/// is culled to `ZERO_HCB` by the caller. -fn band_scalefactor(peak: f64, frame_peak: f64, sf_offset: i32) -> Option { - if peak <= 0.0 || frame_peak <= 0.0 { - return None; - } - let target = TARGET_PEAK_MAG * (peak / frame_peak).powf(SPREAD); - if target < MIN_TARGET_MAG { - return None; - } - let sf = f64::from(SF_OFFSET) + 4.0 * peak.log2() - (16.0 / 3.0) * target.log2(); - Some((sf.round() as i32 + sf_offset).clamp(0, 255)) -} - -/// Smallest Table 4.95 spectrum codebook whose LAV covers `qmax`. -/// `1`/`3` are the 4-tuple books (LAV 1 / 2), `5`/`7`/`9` the pair -/// books (LAV 4 / 7 / 12), `11` the ESC book. -fn codebook_for(qmax: i32) -> u8 { - match qmax { - 0 => ZERO_HCB, - 1 => 1, - 2 => 3, - 3..=4 => 5, - 5..=7 => 7, - 8..=12 => 9, - _ => 11, - } -} - -/// Per-band quantization result for one window group. -struct GroupQuant { - x_quant: Vec, - sfb_cb: Vec, - sfs: Vec>, - /// §4.6.13 noise energies for PNS bands (`sfb_cb == NOISE_HCB`): - /// the band's target L2 norm on the `2^(0.25·noise_nrg)` ladder. - noise: Vec>, - /// §4.6.8.2 intensity positions for IS bands (`sfb_cb == 14/15`, - /// right channel of a CPE only): the position on the - /// `0.5^(0.25·is_pos)` gain ladder. - is_pos: Vec>, -} - -/// Quantize the `num_swb` scalefactor bands of one window group. -/// -/// `spec` is the group's coefficient buffer (1024 lines for a long -/// sequence, `window_group_length × 128` interleaved lines for a -/// short group); `offsets` the -/// matching §4.5.4 band-offset table. `prev_sf` threads the DPCM ±60 -/// clamp across groups in wire order — the §4.6.2.3.2 accumulator is -/// a single track for the whole channel. -/// -/// Pass 1 picks the masking-spread scalefactor per band; pass 2 -/// re-quantizes with the clamped value and derives the codebook. A -/// band whose coefficients all quantize to zero (or whose target is -/// culled) stays `ZERO_HCB` and transmits no scalefactor. -#[allow(clippy::too_many_arguments)] -fn quantize_group( - spec: &[f64], - offsets: &[u16], - num_swb: usize, - sf_offset: i32, - frame_peak: f64, - prev_sf: &mut Option, - pns_bands: &[bool], -) -> GroupQuant { - let mut x_quant = vec![0i32; spec.len()]; - let mut sfb_cb = vec![ZERO_HCB; num_swb]; - let mut sfs: Vec> = vec![None; num_swb]; - let mut noise: Vec> = vec![None; num_swb]; - for sfb in 0..num_swb { - let start = offsets[sfb] as usize; - let end = (offsets[sfb + 1] as usize).min(spec.len()); - let peak = spec[start..end].iter().fold(0.0f64, |m, &v| m.max(v.abs())); - let Some(mut sf) = band_scalefactor(peak, frame_peak, sf_offset) else { - continue; // culled: below the frame's masking floor - }; - // §4.6.13 PNS: a wide band with no dominant spectral line is - // transmitted as a noise energy instead of coefficients. The - // per-band allowance comes from the caller (blanket for mono, - // the pre-M/S pair decision for a CPE channel). - if pns_bands.get(sfb).copied().unwrap_or(false) && is_noise_like(&spec[start..end]) { - let nrg: f64 = spec[start..end].iter().map(|&x| x * x).sum(); - // Target L2 norm 2^(0.25·noise_nrg) == sqrt(nrg). - let noise_nrg = (4.0 * nrg.sqrt().log2()).round() as i32; - sfb_cb[sfb] = NOISE_HCB; - noise[sfb] = Some(noise_nrg); - continue; - } - if let Some(p) = *prev_sf { - sf = sf.clamp(p - MAX_SF_DELTA, p + MAX_SF_DELTA).clamp(0, 255); - } - // Raise sf until the band's peak fits the ESC ceiling (a - // +4 step scales magnitudes by 2^(-3/4)). - let mut qmax = quantize_coef(peak, sf).abs(); - while qmax >= MAX_QUANT && sf < 255 { - sf = (sf + 4).min(255); - qmax = quantize_coef(peak, sf).abs(); - } - if qmax == 0 { - continue; // all-zero band -> ZERO_HCB, no scalefactor - } - let mut band_max = 0i32; - for k in start..end { - let q = quantize_coef(spec[k], sf); - x_quant[k] = q; - band_max = band_max.max(q.abs()); - } - if band_max == 0 { - continue; - } - sfb_cb[sfb] = codebook_for(band_max); - sfs[sfb] = Some(sf); - *prev_sf = Some(sf); - } - GroupQuant { - x_quant, - sfb_cb, - sfs, - noise, - is_pos: vec![None; num_swb], - } -} - -/// Rewrite the right channel's IS-selected bands (§4.6.8.2 encode -/// side): the band's codebook becomes the transmitted intensity book -/// (15 in-phase / 14 out-of-phase), its coefficients are dropped -/// (intensity bands carry no spectral data — the decoder derives -/// them from the left channel), its spectrum scalefactor is retired, -/// and the intensity position lands on the §4.6.8.1.4 `is_pos` -/// track. -fn apply_is_overrides(group: &mut GroupQuant, is_bands: &[IsBand], offsets: &[u16]) { - for (sfb, band) in is_bands.iter().enumerate().take(group.sfb_cb.len()) { - let Some((cb, pos)) = band else { - continue; - }; - let start = offsets[sfb] as usize; - let end = (offsets[sfb + 1] as usize).min(group.x_quant.len()); - for q in &mut group.x_quant[start..end] { - *q = 0; - } - group.sfb_cb[sfb] = *cb; - group.sfs[sfb] = None; - group.noise[sfb] = None; - group.is_pos[sfb] = Some(*pos); - } -} - -/// §4.6.13 noise-likeness test on the `(Σ|x|)² / (width·Σx²)` -/// density statistic (see [`PNS_DENSITY_MIN`]): `true` only when the -/// band's energy is spread across most of its coefficients the way a -/// dense noise band's is. Bands narrower than [`PNS_MIN_WIDTH`] -/// never qualify (the statistic is meaningless on a handful of -/// coefficients). -fn is_noise_like(band: &[f64]) -> bool { - let width = band.len(); - if width < PNS_MIN_WIDTH { - return false; - } - let l1: f64 = band.iter().map(|&v| v.abs()).sum(); - let l2_sq: f64 = band.iter().map(|&v| v * v).sum(); - if l2_sq <= 0.0 { - return false; - } - (l1 * l1) / (width as f64 * l2_sq) > PNS_DENSITY_MIN -} - -/// Build the per-band absolute scalefactor / noise-energy records -/// and the frame's `global_gain`, then run the §4.6.2.3.2 / §4.6.13 -/// inverse DPCM ([`differentiate`]) to obtain the transmitted entry -/// set. -/// -/// `global_gain` is the first coded spectrum band's scalefactor -/// (making its delta 0). The §4.6.13 noise track is seeded at -/// `global_gain − NOISE_OFFSET − 256` with the first PNS band's -/// delta a 9-bit *unsigned* PCM (`0..=511`) and later noise deltas -/// Huffman `±60`; each requested `noise_nrg` is clamped into the -/// nearest feasible value on that track (a few 1.5 dB steps of -/// clamp at worst — noise energy is far less sensitive than a -/// spectral gain). -fn scalefactor_track(groups: &[GroupQuant]) -> (u8, AbsoluteScaleFactors) { - let global_gain = groups - .iter() - .flat_map(|g| g.sfs.iter().copied().flatten()) - .next() - .unwrap_or(SF_OFFSET) as u8; - let mut last_nrg = i32::from(global_gain) - NOISE_OFFSET - 256; - let mut first_noise = true; - // §4.6.8.1.4: the intensity-position track seeds at 0 and takes - // the same Huffman ±60 deltas as scalefactors; requested - // positions are clamped onto the feasible track like the noise - // energies above. - let mut last_is = 0i32; - let mut entries = Vec::with_capacity(groups.len()); - for g in groups { - let mut group_out = Vec::new(); - for sfb in 0..g.sfb_cb.len() { - if let Some(sf) = g.sfs[sfb] { - group_out.push(AbsoluteScaleFactorEntry::Sf(sf as u8)); - } else if let Some(nrg) = g.noise[sfb] { - let delta = nrg - last_nrg; - let clamped = if first_noise { - delta.clamp(0, 511) - } else { - delta.clamp(-MAX_SF_DELTA, MAX_SF_DELTA) - }; - first_noise = false; - last_nrg += clamped; - group_out.push(AbsoluteScaleFactorEntry::NoiseNrg(last_nrg)); - } else if let Some(pos) = g.is_pos[sfb] { - let delta = (pos - last_is).clamp(-MAX_SF_DELTA, MAX_SF_DELTA); - last_is += delta; - group_out.push(AbsoluteScaleFactorEntry::IsPos(last_is as i16)); - } - } - entries.push(group_out); - } - (global_gain, AbsoluteScaleFactors { entries }) -} - -/// Exact §4.6.3.3 wire cost, in bits, of coding one band's -/// coefficient range with spectrum book `cb` — Huffman codewords + -/// sign bits + escape sequences, measured by running the actual -/// [`crate::spectral_data`] tuple writer into a scratch buffer. -/// `None` when the book cannot carry the band (a magnitude beyond -/// the book's Table 4.95 LAV; book 11 escapes up to `MAX_QUANT`). -fn band_bits(cb: u8, coeffs: &[i32]) -> Option { - let row = crate::spectral_codebook::table_4_95(cb).ok()?; - let dim = row.dimension? as usize; - let mut bw = BitWriter::new(); - let mut k = 0; - while k + dim <= coeffs.len() { - crate::spectral_data::write_tuple(&mut bw, cb, dim, &coeffs[k..k + dim]).ok()?; - k += dim; - } - if k != coeffs.len() { - return None; // band width not a whole number of tuples - } - Some(bw.bit_position() as u32) -} - -/// The §4.4.2.7-adjacent `section_data()` header cost of one section -/// spanning `len` bands: 4 bits `sect_cb` plus the `sect_len_incr` -/// escape run (5-bit fields / escape 31 for long sequences, 3-bit / -/// escape 7 for `EIGHT_SHORT`). -fn section_header_bits(len: u32, long: bool) -> u32 { - let (esc, w) = if long { (31, 5) } else { (7, 3) }; - 4 + w * (len / esc + 1) -} - -/// A band's sectioning class — sections may only span bands of one -/// class (the special codebooks are semantic, not a coding choice, -/// and a `ZERO_HCB` band folded into a spectrum section would owe a -/// scalefactor the track never assigned). -#[derive(PartialEq, Eq, Clone, Copy)] -enum BandClass { - /// `ZERO_HCB` — no spectrum, no scalefactor. - Zero, - /// `NOISE_HCB` / intensity books — the codebook is fixed by the - /// tool decision; adjacent equal books merge. - Fixed(u8), - /// Spectrum bands (provisional book 1..=11) — the section book - /// is a free choice among every book that covers the run. - Spectral, -} - -/// Choose one window group's sections + codebooks by measured bit -/// cost (§4.6.3.1 leaves both entirely to the encoder). -/// -/// Dynamic program over section boundaries: for every candidate run -/// of same-class bands the cost is the [`section_header_bits`] -/// overhead plus — for spectral runs — the cheapest single Table -/// 4.95 book (1..=11, measured per band via [`band_bits`], covering -/// the whole run) summed over the run's bands. This subsumes the -/// classic "smallest LAV fit + merge equal books" rule and beats it -/// wherever a signed/unsigned sibling book codes the actual -/// distribution cheaper, or one step up in LAV lets two sections -/// merge for less than the saved header. -/// -/// `ranges` maps each band to its coefficient range inside the -/// group's (interleaved) buffer; `sfb_cb` carries the per-band class -/// in (provisional books on spectral bands) and the chosen books -/// out. -fn optimize_group_sections( - x_quant: &[i32], - ranges: &[(usize, usize)], - sfb_cb: &mut [u8], - long: bool, -) -> Result> { - let n = sfb_cb.len(); - if n == 0 { - return Ok(Vec::new()); - } - let class: Vec = sfb_cb - .iter() - .map(|&cb| match cb { - ZERO_HCB => BandClass::Zero, - NOISE_HCB | INTENSITY_HCB | INTENSITY_HCB2 => BandClass::Fixed(cb), - _ => BandClass::Spectral, - }) - .collect(); - // Per-band cost under each spectrum book (None = book can't - // carry the band). - let books: [u8; 11] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; - let cost: Vec<[Option; 11]> = (0..n) - .map(|b| { - let mut row = [None; 11]; - if class[b] == BandClass::Spectral { - let (s, e) = ranges[b]; - for (i, &cb) in books.iter().enumerate() { - row[i] = band_bits(cb, &x_quant[s..e]); - } - } - row - }) - .collect(); - - // dp[b] = (bits, cut, book) for the cheapest sectioning of bands - // 0..b, where `cut` is the start of the final section and `book` - // its codebook. - let mut dp: Vec<(u64, usize, u8)> = vec![(u64::MAX, 0, 0); n + 1]; - dp[0] = (0, 0, 0); - for b in 1..=n { - for a in (0..b).rev() { - // The run a..b must be one class (and one fixed book). - if class[a] != class[b - 1] { - break; - } - let header = u64::from(section_header_bits((b - a) as u32, long)); - let run = match class[a] { - BandClass::Zero => Some((0u8, 0u64)), - BandClass::Fixed(cb) => { - if sfb_cb[a..b].iter().any(|&c| c != cb) { - None // e.g. mixed intensity phases - } else { - Some((cb, 0)) - } - } - BandClass::Spectral => { - let mut best: Option<(u8, u64)> = None; - for (i, &cb) in books.iter().enumerate() { - let mut sum = 0u64; - let mut ok = true; - for c in cost[a..b].iter() { - match c[i] { - Some(bits) => sum += u64::from(bits), - None => { - ok = false; - break; - } - } - } - if ok && best.map(|(_, s)| sum < s).unwrap_or(true) { - best = Some((cb, sum)); - } - } - best - } - }; - let Some((book, run_bits)) = run else { - continue; - }; - let total = dp[a].0.saturating_add(header + run_bits); - if total < dp[b].0 { - dp[b] = (total, a, book); - } - } - } - if dp[n].0 == u64::MAX { - return Err(Error::SpectralDataEncodeInvalid); - } - - // Walk the cuts back into sections and stamp the chosen books. - let mut bounds = Vec::new(); - let mut b = n; - while b > 0 { - let (_, a, book) = dp[b]; - bounds.push((a, b, book)); - b = a; - } - bounds.reverse(); - let mut sections = Vec::with_capacity(bounds.len()); - for (a, b, book) in bounds { - for cb in sfb_cb[a..b].iter_mut() { - *cb = book; - } - sections.push(Section { - codebook: book, - start: a as u8, - end: b as u8, - }); - } - Ok(sections) -} - -/// Wrap quantized groups + an `ics_info` into the wire record set. -/// The per-group band ranges come from the §4.5.2.3.4 -/// `sect_sfb_offset` derivation, so grouped short-window buffers -/// (band widths × `window_group_length`) resolve correctly. -fn finish_channel( - info: IcsInfo, - groups: Vec, - fs_index: u8, - pulse_data: Option, -) -> Result { - let (global_gain, abs) = scalefactor_track(&groups); - let long = info.window_sequence != WindowSequence::EightShort; - let per_group_offsets = crate::spectral_data::sect_sfb_offset(&info, fs_index)?; - let mut sections = Vec::with_capacity(groups.len()); - let mut sfb_cb = Vec::with_capacity(groups.len()); - let mut x_quant = Vec::with_capacity(groups.len()); - for (mut g, offsets) in groups.into_iter().zip(per_group_offsets.iter()) { - let ranges: Vec<(usize, usize)> = (0..g.sfb_cb.len()) - .map(|sfb| { - ( - (offsets[sfb] as usize).min(g.x_quant.len()), - (offsets[sfb + 1] as usize).min(g.x_quant.len()), - ) - }) - .collect(); - sections.push(optimize_group_sections( - &g.x_quant, - &ranges, - &mut g.sfb_cb, - long, - )?); - sfb_cb.push(g.sfb_cb); - x_quant.push(g.x_quant); - } - let scale_factor_data = differentiate(&abs, &sfb_cb, global_gain)?; - let body = IcsBody { - global_gain, - ics_info: Some(info.clone()), - section_data: SectionData { sections, sfb_cb }, - scale_factor_data, - pulse_data_present: pulse_data.is_some(), - pulse_data, - tns_data_present: false, - tns_data: None, - gain_control_data_present: false, - gain_control_data: None, - spectral_data_bit_offset: 0, - er_scale_factor_data: None, - reordered_spectral_lengths: None, - }; - let spectral = SpectralData { x_quant }; - Ok(QuantizedChannel { - info, - body, - spectral, - }) -} - -/// Exact wire size, in bits, of one channel's -/// `individual_channel_stream()` — the [`IcsBody`] side info -/// (sections, scalefactors, pulse / TNS dispatch) plus the -/// `spectral_data()` payload — measured by running the real writers -/// into a scratch buffer. Used to settle encoder tool decisions -/// (pulse escape) by measured cost, the same philosophy as -/// [`optimize_group_sections`]. -fn channel_wire_bits(chan: &QuantizedChannel, fs_index: u8) -> Result { - let mut bw = BitWriter::new(); - chan.body.write(&mut bw, 2, fs_index, false)?; - chan.spectral - .write(&mut bw, &chan.info, &chan.body.section_data, fs_index)?; - Ok(bw.bit_position()) -} - -/// Wire cost, in bits, of one Table 4.7 pulse record carrying `n` -/// pulses: 2-bit `number_pulse` + 6-bit `pulse_start_sfb` + 9 bits -/// per `(offset, amp)` entry (the `pulse_data_present` dispatch bit -/// itself is paid on both variants). -fn pulse_record_bits(n: usize) -> u64 { - 8 + 9 * n as u64 -} - -/// §4.4.6.3 / Table 4.7 pulse-escape candidate for one long-frame -/// quantized spectrum. -/// -/// The pulse tool pays off when a band's few outlier lines force the -/// whole band (and through sectioning, its neighbours) onto a large- -/// LAV codebook or into §4.6.3.3 escape sequences: transmitting the -/// outliers' excess as `(offset, amp)` fix-ups lets the residual -/// band code on the book the *rest* of its lines need. The decoder's -/// §4.6.3.3 reconstruction ([`crate::swb_offset::apply_pulse_data`], -/// `x_quant[k] ±= amp` on the transmitted sign) restores the exact -/// original quantized values, so the choice is purely one of -/// noiseless-coding cost. -/// -/// Candidate search, per spectrum-coded band (`ZERO_HCB` bands -/// transmit no scalefactor and `NOISE_HCB` / intensity bands no -/// coefficients — excluded): for every outlier count `j` in -/// `1..=`[`MAX_PULSES`], reduce the band's `j` largest-magnitude -/// lines to the magnitude floor set by its `(j+1)`-th largest -/// (clamped to the 4-bit `amp` reach, keeping the residual >= 1 so -/// the transmitted sign survives), price the reduced band at its -/// cheapest Table 4.95 book via [`band_bits`] plus the -/// [`pulse_record_bits`] overhead, and keep the best-measuring `j`. -/// The best band across the spectrum wins (Table 4.7's single -/// `pulse_start_sfb` + 5-bit offset deltas make one band the -/// realistic carrier; cross-band chains are almost never -/// addressable). Pulses must ascend with in-band gaps <= 31 and the -/// first offset within 31 of the band start. -/// -/// Returns the pulse record and the reduced spectrum, or `None` when -/// no band measures a saving. The caller re-prices the whole channel -/// with [`channel_wire_bits`] (capturing section-merge effects) and -/// keeps the variant only when the full stream measures smaller. -fn extract_pulse_candidate( - x_quant: &[i32], - sfb_cb: &[u8], - offsets: &[u16], -) -> Option<(PulseData, Vec)> { - // (measured saving, carrier sfb, [(line index, amp)]). - #[allow(clippy::type_complexity)] - let mut best: Option<(i64, usize, Vec<(usize, i32)>)> = None; - for sfb in 0..sfb_cb.len().min(offsets.len().saturating_sub(1)).min(64) { - match sfb_cb[sfb] { - ZERO_HCB | NOISE_HCB | INTENSITY_HCB | INTENSITY_HCB2 => continue, - _ => {} - } - let (s, e) = ( - offsets[sfb] as usize, - (offsets[sfb + 1] as usize).min(x_quant.len()), - ); - if e <= s { - continue; - } - let band = &x_quant[s..e]; - // Baseline: the band's cheapest book as-is. - let Some(base_cost) = cheapest_band_bits(band) else { - continue; - }; - // Magnitude-descending line order. - let mut by_mag: Vec = (0..band.len()).collect(); - by_mag.sort_by_key(|&i| std::cmp::Reverse(band[i].abs())); - for j in 1..=MAX_PULSES.min(band.len().saturating_sub(1)) { - let floor = band[by_mag[j]].abs().max(1); - // The j selected lines, ascending, with their amp. - let mut lines: Vec<(usize, i32)> = by_mag[..j] - .iter() - .map(|&i| (i, (band[i].abs() - floor).min(15))) - .filter(|&(_, amp)| amp >= 1) - .collect(); - if lines.len() < j { - continue; // an outlier is out of amp reach parity with the floor - } - lines.sort_by_key(|&(i, _)| i); - // Table 4.7 addressability. - if lines[0].0 > 0x1f { - continue; - } - if lines.windows(2).any(|w| w[1].0 - w[0].0 > 0x1f) { - continue; - } - let mut reduced = band.to_vec(); - for &(i, amp) in &lines { - if reduced[i] > 0 { - reduced[i] -= amp; - } else { - reduced[i] += amp; - } - } - let Some(cost) = cheapest_band_bits(&reduced) else { - continue; - }; - let saving = i64::from(base_cost) - i64::from(cost) - pulse_record_bits(j) as i64; - if saving > 0 && best.as_ref().map(|(bs, _, _)| saving > *bs).unwrap_or(true) { - best = Some(( - saving, - sfb, - lines.iter().map(|&(i, amp)| (s + i, amp)).collect(), - )); - } - } - } - let (_, start_sfb, lines) = best?; - let mut reduced = x_quant.to_vec(); - let mut pulses = Vec::with_capacity(lines.len()); - let mut prev_k = offsets[start_sfb] as usize; - for &(k, amp) in &lines { - pulses.push(Pulse { - offset: (k - prev_k) as u8, - amp: amp as u8, - }); - prev_k = k; - if reduced[k] > 0 { - reduced[k] -= amp; - } else { - reduced[k] += amp; - } - } - Some(( - PulseData { - pulse_start_sfb: start_sfb as u8, - pulses, - }, - reduced, - )) -} - -/// Cheapest single Table 4.95 book cost for one band's coefficients -/// (books 1..=11 via [`band_bits`]). -fn cheapest_band_bits(coeffs: &[i32]) -> Option { - (1u8..=11).filter_map(|cb| band_bits(cb, coeffs)).min() -} - -/// Quantize one channel's 1024-line long-sequence spectrum into a -/// complete `individual_channel_stream()` record set. `seq` must be -/// one of the three long sequences (it lands in the `ics_info`); -/// `frame_peak` anchors the masking spread and cull — the channel's -/// own peak for mono, the pair's loudest peak for a jointly-coded -/// CPE. `pns_bands` grants the per-band §4.6.13 allowance (empty = -/// PNS off); a non-empty `is_bands` (the right channel of an -/// intensity-coding CPE) rewrites the selected bands into §4.6.8.2 -/// intensity records after quantization. When the spectrum carries -/// escape-magnitude lines, a §4.4.6.3 `pulse_data()` variant is -/// priced against the plain coding and kept if it measures smaller -/// (the decode-side §4.6.3.3 fix-up restores the identical quantized -/// spectrum, so the choice never changes the reconstruction). -#[allow(clippy::too_many_arguments)] -fn quantize_channel( - spec: &[f64], - seq: WindowSequence, - fs_index: u8, - sf_offset: i32, - frame_peak: f64, - pns_bands: &[bool], - is_bands: &[IsBand], -) -> Result { - debug_assert_eq!(spec.len(), FRAME_LEN); - debug_assert!(seq != WindowSequence::EightShort); - let offsets = long_window_offsets(fs_index)?; - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - let mut prev_sf: Option = None; - let mut group = quantize_group( - spec, - offsets, - num_swb, - sf_offset, - frame_peak, - &mut prev_sf, - pns_bands, - ); - if !is_bands.is_empty() { - apply_is_overrides(&mut group, is_bands, offsets); - } - let pulse_candidate = extract_pulse_candidate(&group.x_quant, &group.sfb_cb, offsets); - let info = IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: seq, - window_shape: WindowShape::Sine, - max_sfb: num_swb as u8, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: num_swb as u8, - }; - // Measured pulse decision: price the reduced-spectrum + - // pulse_data() variant against the plain coding with the real - // writers and keep the smaller stream. - if let Some((pd, reduced)) = pulse_candidate { - let mut pulsed_group = GroupQuant { - x_quant: reduced, - sfb_cb: group.sfb_cb.clone(), - sfs: group.sfs.clone(), - noise: group.noise.clone(), - is_pos: group.is_pos.clone(), - }; - // Re-derive the reduced bands' provisional books (the DP - // re-decides anyway; this keeps the class metadata honest). - for sfb in 0..pulsed_group.sfb_cb.len() { - match pulsed_group.sfb_cb[sfb] { - ZERO_HCB | NOISE_HCB | INTENSITY_HCB | INTENSITY_HCB2 => continue, - _ => {} - } - let (s, e) = ( - offsets[sfb] as usize, - (offsets[sfb + 1] as usize).min(pulsed_group.x_quant.len()), - ); - let band_max = pulsed_group.x_quant[s..e] - .iter() - .map(|q| q.abs()) - .max() - .unwrap_or(0); - if band_max > 0 { - pulsed_group.sfb_cb[sfb] = codebook_for(band_max); - } - } - let plain = finish_channel(info.clone(), vec![group], fs_index, None)?; - let pulsed = finish_channel(info, vec![pulsed_group], fs_index, Some(pd))?; - return if channel_wire_bits(&pulsed, fs_index)? < channel_wire_bits(&plain, fs_index)? { - Ok(pulsed) - } else { - Ok(plain) - }; - } - finish_channel(info, vec![group], fs_index, None) -} - -/// Maximum mean per-band log-energy distance (natural log) between -/// two adjacent short windows for the §4.5.2.3.4 grouping decision -/// to merge them into one window group. `ln 4 ≈ 1.39` — the windows' -/// band envelopes agree within ~6 dB on average. -const GROUP_MERGE_LOG_DIST: f64 = 1.386; - -/// Energy floor (one squared unit coefficient) added to both sides -/// of the grouping log-ratio so empty bands compare as equal instead -/// of dividing by zero. -const GROUP_MERGE_EPS: f64 = 1.0; - -/// §4.5.2.3.4 `scale_factor_grouping` decision for one channel's -/// `EIGHT_SHORT_SEQUENCE` spectrum (8 × 128 window-major -/// coefficients): merge adjacent windows whose per-band energy -/// envelopes agree within [`GROUP_MERGE_LOG_DIST`] on average. -/// -/// Grouped windows share one scalefactor / section track — the whole -/// point of the tool (§4.6.2.3.2: "to achieve a most efficient -/// coding, several subsequent windows... can be grouped") — so the -/// merge criterion mirrors what sharing costs: windows with matching -/// band envelopes lose nothing to a common scalefactor, while an -/// attack window's jump keeps it in its own group. Returns the -/// `window_group_length` vector (summing to 8). -fn decide_short_grouping(spec: &[f64], offsets: &[u16], num_swb: usize) -> Vec { - let short_len = SHORT_WINDOW_LEN as usize; - let band_energy = |w: usize, sfb: usize| -> f64 { - let base = w * short_len; - spec[base + offsets[sfb] as usize..base + offsets[sfb + 1] as usize] - .iter() - .map(|&v| v * v) - .sum() - }; - let mut lengths: Vec = vec![1]; - for w in 1..8 { - let dist: f64 = (0..num_swb) - .map(|sfb| { - let a = band_energy(w - 1, sfb) + GROUP_MERGE_EPS; - let b = band_energy(w, sfb) + GROUP_MERGE_EPS; - (a / b).ln().abs() - }) - .sum::() - / num_swb.max(1) as f64; - if dist <= GROUP_MERGE_LOG_DIST { - *lengths.last_mut().expect("non-empty") += 1; - } else { - lengths.push(1); - } - } - lengths -} - -/// The 7-bit `scale_factor_grouping` mask for a `window_group_length` -/// vector: bit `6 − (w − 1)` is set when window `w` (1..=7) stays in -/// the previous window's group — the inverse of the §4.5.2.3.4 -/// derivation in [`crate::ics_info::derive_window_grouping`]. -fn grouping_mask(window_group_length: &[u8]) -> u8 { - let mut mask = 0u8; - let mut w = 0usize; - for &len in window_group_length { - for j in 0..len as usize { - if j > 0 { - mask |= 1 << (6 - (w - 1)); - } - w += 1; - } - } - mask -} - -/// Quantize one channel's `EIGHT_SHORT_SEQUENCE` spectrum (8 x 128 -/// window-major coefficients) into a complete record set. The -/// §4.5.2.3.4 grouping decision ([`decide_short_grouping`]) merges -/// envelope-alike adjacent windows into shared window groups — one -/// scalefactor / section track per group instead of eight — and each -/// group's coefficients are laid out in the §4.5.2.3.5 interleaved -/// `(sfb, window, bin)` transmission order. -fn quantize_channel_short( - spec: &[f64], - fs_index: u8, - sf_offset: i32, - frame_peak: f64, -) -> Result { - let offsets = short_window_offsets(fs_index)?; - let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; - let window_group_length = decide_short_grouping(spec, offsets, num_swb); - quantize_channel_short_grouped(spec, fs_index, sf_offset, frame_peak, window_group_length) -} - -/// [`quantize_channel_short`] under an *imposed* grouping — the -/// `common_window` CPE path must quantize both channels under one -/// shared `ics_info`, so the §4.5.2.3.4 grouping decision is made -/// once (jointly, on the pair envelope) and both channels' section / -/// scalefactor / interleave layouts follow it. -fn quantize_channel_short_grouped( - spec: &[f64], - fs_index: u8, - sf_offset: i32, - frame_peak: f64, - window_group_length: Vec, -) -> Result { - let short_len = SHORT_WINDOW_LEN as usize; - debug_assert_eq!(spec.len(), 8 * short_len); - let offsets = short_window_offsets(fs_index)?; - let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; - let mask = grouping_mask(&window_group_length); - let mut prev_sf: Option = None; - let mut groups = Vec::with_capacity(window_group_length.len()); - let mut win_base = 0usize; - for &len in &window_group_length { - let wgl = len as usize; - // §4.5.2.3.5 interleave: for each band, the group's windows' - // band coefficients ride consecutively. - let mut buf = Vec::with_capacity(wgl * short_len); - for sfb in 0..num_swb { - let (s, e) = (offsets[sfb] as usize, offsets[sfb + 1] as usize); - for w in 0..wgl { - let base = (win_base + w) * short_len; - buf.extend_from_slice(&spec[base + s..base + e]); - } - } - // The group's sect_sfb_offset table: band widths × wgl. - let mut scaled = Vec::with_capacity(num_swb + 1); - let mut acc = 0u16; - scaled.push(0u16); - for sfb in 0..num_swb { - acc += (offsets[sfb + 1] - offsets[sfb]) * len as u16; - scaled.push(acc); - } - groups.push(quantize_group( - &buf, - &scaled, - num_swb, - sf_offset, - frame_peak, - &mut prev_sf, - &[], // PNS stays long-frame-only for now - )); - win_base += wgl; - } - let info = IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb: num_swb as u8, - scale_factor_grouping: Some(mask), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups: window_group_length.len() as u8, - window_group_length, - num_swb: num_swb as u8, - }; - // §4.4.6.3: pulse_data is illegal on EIGHT_SHORT_SEQUENCE. - finish_channel(info, groups, fs_index, None) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::dequant::{inverse_quantize, scale_factor_gain}; - - #[test] - fn quantize_coef_inverts_dequant_within_rounding() { - // For any q and sf, dequantizing then re-quantizing recovers - // q exactly (the quantizer is the exact inverse map). - for sf in [40i32, 100, 156, 200] { - for q in [-8190i32, -1000, -12, -1, 0, 1, 7, 40, 999, 8190] { - let x = inverse_quantize(q) * scale_factor_gain(sf as u8); - assert_eq!(quantize_coef(x, sf), q, "sf={sf} q={q}"); - } - } - } - - #[test] - fn band_scalefactor_hits_target_magnitude() { - // A frame-loudest peak quantized with its own - // band_scalefactor lands within rounding of TARGET_PEAK_MAG. - for peak in [1.0f64, 100.0, 3.2e4, 6.7e7] { - let sf = band_scalefactor(peak, peak, 0).expect("loudest band never culls"); - let q = quantize_coef(peak, sf).abs(); - let lo = (TARGET_PEAK_MAG / 2.0_f64.powf(0.375)).floor() as i32; - let hi = (TARGET_PEAK_MAG * 2.0_f64.powf(0.375)).ceil() as i32; - assert!( - (lo..=hi).contains(&q), - "peak={peak} sf={sf} q={q} not in [{lo},{hi}]" - ); - } - } - - #[test] - fn band_scalefactor_spreads_and_culls() { - let frame_peak = 1.0e6f64; - // A band 40 dB down gets a ~20 dB smaller target: the target - // is 42·(10^-2)^0.5 = 4.2, so its peak quantizes to ~4. - let sf = band_scalefactor(frame_peak * 1e-2, frame_peak, 0).unwrap(); - let q = quantize_coef(frame_peak * 1e-2, sf).abs(); - assert!((2..=8).contains(&q), "spread target off: q={q}"); - // A band ~90 dB down is culled outright (target < 0.7). - assert_eq!(band_scalefactor(frame_peak * 3.2e-5, frame_peak, 0), None); - // Zero-peak bands cull. - assert_eq!(band_scalefactor(0.0, frame_peak, 0), None); - } - - #[test] - fn codebook_selection_covers_table_4_95_lavs() { - assert_eq!(codebook_for(0), ZERO_HCB); - assert_eq!(codebook_for(1), 1); - assert_eq!(codebook_for(2), 3); - assert_eq!(codebook_for(4), 5); - assert_eq!(codebook_for(7), 7); - assert_eq!(codebook_for(12), 9); - assert_eq!(codebook_for(13), 11); - assert_eq!(codebook_for(8191), 11); - } - - #[test] - fn config_rejects_bad_parameters() { - assert!(StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: 1, - bitrate: 64_000, - }) - .is_ok()); - assert!(matches!( - StreamEncoder::new(EncoderConfig { - sample_rate: 44_056, // not a Table 1.18 rate - channels: 1, - bitrate: 64_000, - }), - Err(Error::EncoderInvalidConfig) - )); - // Every channel count with a Table 1.19 default - // configuration builds; 0 / 7 / 9 have none and reject. - for ok in [3u8, 4, 5, 6, 8] { - assert!( - StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: ok, - bitrate: 64_000 * u32::from(ok), - }) - .is_ok(), - "channels {ok}" - ); - } - for bad in [0u8, 7, 9] { - assert!( - matches!( - StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: bad, - bitrate: 64_000, - }), - Err(Error::EncoderInvalidConfig) - ), - "channels {bad}" - ); - } - assert!(matches!( - StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: 1, - bitrate: 0, - }), - Err(Error::EncoderInvalidConfig) - )); - } - - /// Deterministic band fill for the sectioning tests. - fn fill_band(buf: &mut [i32], range: (usize, usize), max: i32, seed: &mut u32) { - for slot in buf[range.0..range.1].iter_mut() { - *seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); - *slot = ((*seed >> 8) % (2 * max + 1) as u32) as i32 - max; - } - } - - /// Total wire bits of one long group under given sections / - /// books: `section_data()` + `spectral_data()`, measured with - /// the real writers. - fn measure_group( - sections: Vec
, - sfb_cb: Vec, - x_quant: Vec, - num_swb: usize, - fs_index: u8, - ) -> u64 { - let info = IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb: num_swb as u8, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: num_swb as u8, - }; - let sd = SectionData { - sections: vec![sections], - sfb_cb: vec![sfb_cb], - }; - let spectral = SpectralData { - x_quant: vec![x_quant], - }; - let mut bw = BitWriter::new(); - sd.write(&mut bw, WindowSequence::OnlyLong, num_swb as u8) - .unwrap(); - spectral.write(&mut bw, &info, &sd, fs_index).unwrap(); - bw.bit_position() - } - - /// [`band_bits`] agrees with the real `spectral_data()` writer: - /// a one-section stream's spectral bits equal the summed band - /// costs. - #[test] - fn band_bits_matches_wire_writer() { - let fs_index = 4u8; - let offsets = long_window_offsets(fs_index).unwrap(); - let mut buf = vec![0i32; FRAME_LEN]; - let mut seed = 0xB17u32; - for sfb in 0..6 { - fill_band( - &mut buf, - (offsets[sfb] as usize, offsets[sfb + 1] as usize), - 7, - &mut seed, - ); - } - for cb in [7u8, 8, 9, 10, 11] { - let per_band: u32 = (0..6) - .map(|sfb| { - band_bits(cb, &buf[offsets[sfb] as usize..offsets[sfb + 1] as usize]).unwrap() - }) - .sum(); - let sections = vec![Section { - codebook: cb, - start: 0, - end: 6, - }]; - let wire = measure_group(sections.clone(), vec![cb; 6], buf.clone(), 6, fs_index); - let header = u64::from(section_header_bits(6, true)); - assert_eq!(wire, header + u64::from(per_band), "cb {cb}"); - } - // A signed book rejects magnitudes past its LAV; the quad - // books reject a pair-only width mismatch never (widths are - // multiples of 4), but LAV 1 caps at |1|. - assert!(band_bits(1, &[2, 0, 0, 0]).is_none()); - assert!(band_bits(3, &[3, 0, 0, 0]).is_none()); - } - - /// The measured-cost DP never codes a group larger than the - /// classic smallest-LAV + merge-equal-books sectioning, over a - /// spread of band shapes (zero runs, alternating magnitudes, - /// escape bands). - #[test] - fn optimizer_never_loses_to_naive_sections() { - let fs_index = 4u8; - let offsets = long_window_offsets(fs_index).unwrap(); - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - for (case, seed0) in [(0u32, 1u32), (1, 0xACE), (2, 0x5EED), (3, 77)] { - let mut buf = vec![0i32; FRAME_LEN]; - let mut seed = seed0; - for sfb in 0..num_swb { - let range = (offsets[sfb] as usize, offsets[sfb + 1] as usize); - let max = match case { - 0 => [0, 1, 1, 2, 0, 0, 4, 7, 1][sfb % 9], - 1 => [1, 12, 1, 30, 0, 2][sfb % 6], - 2 => (sfb as i32) % 5, - _ => [7, 7, 0, 0, 0, 12, 1, 1][sfb % 8], - }; - if max > 0 { - fill_band(&mut buf, range, max, &mut seed); - } - } - // Provisional per-band books (the DP input). - let provisional: Vec = (0..num_swb) - .map(|sfb| { - let band = &buf[offsets[sfb] as usize..offsets[sfb + 1] as usize]; - codebook_for(band.iter().map(|&v| v.abs()).max().unwrap_or(0)) - }) - .collect(); - // Naive: keep the smallest-LAV books, merge equal runs. - let mut naive_sections: Vec
= Vec::new(); - for (sfb, &cb) in provisional.iter().enumerate() { - match naive_sections.last_mut() { - Some(s) if s.codebook == cb => s.end = (sfb + 1) as u8, - _ => naive_sections.push(Section { - codebook: cb, - start: sfb as u8, - end: (sfb + 1) as u8, - }), - } - } - let naive = measure_group( - naive_sections, - provisional.clone(), - buf.clone(), - num_swb, - fs_index, - ); - // Optimized. - let ranges: Vec<(usize, usize)> = (0..num_swb) - .map(|sfb| (offsets[sfb] as usize, offsets[sfb + 1] as usize)) - .collect(); - let mut books = provisional; - let sections = optimize_group_sections(&buf, &ranges, &mut books, true).unwrap(); - // Every chosen book covers its bands (the writer would - // reject otherwise) and the wire is never larger. - let opt = measure_group(sections, books, buf, num_swb, fs_index); - assert!(opt <= naive, "case {case}: opt {opt} > naive {naive}"); - } - } - - /// [`decide_short_grouping`] merges alike windows and splits at - /// an attack; [`grouping_mask`] is the exact inverse of the - /// §4.5.2.3.4 mask derivation. - #[test] - fn short_grouping_decision_and_mask() { - let fs_index = 4u8; - let offsets = short_window_offsets(fs_index).unwrap(); - let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; - let short_len = SHORT_WINDOW_LEN as usize; - // Eight identical windows: one group of 8, mask all-ones. - let mut spec = vec![0.0f64; 8 * short_len]; - for w in 0..8 { - for k in 0..short_len { - spec[w * short_len + k] = 1000.0 * ((k as f64) * 0.37).sin(); - } - } - assert_eq!(decide_short_grouping(&spec, offsets, num_swb), vec![8]); - assert_eq!(grouping_mask(&[8]), 0x7F); - // A 60 dB attack at window 3 splits the run there. - for k in 0..short_len { - for w in 3..8 { - spec[w * short_len + k] *= 1000.0; - } - } - let lengths = decide_short_grouping(&spec, offsets, num_swb); - assert_eq!(lengths, vec![3, 5]); - assert_eq!(grouping_mask(&lengths), 0b110_1111); - // No grouping at all round-trips to mask 0. - assert_eq!(grouping_mask(&[1; 8]), 0); - // Every mask agrees with the decoder-side derivation. - for lengths in [vec![8u8], vec![3, 5], vec![1; 8], vec![2, 1, 4, 1]] { - let mask = grouping_mask(&lengths); - let (_, n, derived, _) = crate::ics_info::derive_window_grouping( - WindowSequence::EightShort, - Some(mask), - fs_index as usize, - ); - assert_eq!(derived, lengths); - assert_eq!(n as usize, lengths.len()); - } - } - - /// A grouped short channel's wire records round-trip through the - /// crate's own parsers: the ics_info grouping, the per-group - /// section spans, and the §4.5.2.3.5 interleaved spectrum come - /// back exactly. - #[test] - fn short_grouping_wire_roundtrip() { - use oxideav_core::bits::BitReader; - let fs_index = 4u8; - let short_len = SHORT_WINDOW_LEN as usize; - // Windows 0..3 carry pattern A, 3..8 a 40 dB louder pattern B - // (the grouping decision splits at the jump). - let mut spec = vec![0.0f64; 8 * short_len]; - for w in 0..8 { - let (gain, phase) = if w < 3 { - (300.0, 0.31) - } else { - (30000.0, 0.11) - }; - for k in 0..short_len { - spec[w * short_len + k] = gain * ((k as f64) * phase).sin(); - } - } - let frame_peak = spec.iter().fold(0.0f64, |m, &v| m.max(v.abs())); - let chan = quantize_channel_short(&spec, fs_index, 0, frame_peak).unwrap(); - assert_eq!(chan.info.window_group_length, vec![3, 5]); - - let mut bw = BitWriter::new(); - chan.body.write(&mut bw, 2, fs_index, false).unwrap(); - chan.spectral - .write(&mut bw, &chan.info, &chan.body.section_data, fs_index) - .unwrap(); - let bytes = bw.finish(); - let mut reader = BitReader::new(&bytes); - let body = IcsBody::parse(&mut reader, 2, fs_index, false).unwrap(); - let ics = body.ics_info.as_ref().unwrap(); - assert_eq!(ics.scale_factor_grouping, Some(0b110_1111)); - assert_eq!(ics.num_window_groups, 2); - assert_eq!(ics.window_group_length, vec![3, 5]); - let spectral = SpectralData::parse(&mut reader, ics, &body.section_data, fs_index).unwrap(); - assert_eq!(spectral, chan.spectral); - assert_eq!(body.section_data, chan.body.section_data); - assert_eq!(body.scale_factor_data, chan.body.scale_factor_data); - } - - /// The `common_window` CPE short path imposes ONE grouping on - /// both channels even when their independent decisions would - /// diverge — a divergent pair would desync the shared-`ics_info` - /// wire layout (verified: reverting to per-channel decisions - /// fails this test). The frame must decode consistently through - /// the stream decoder with the burst energy on the right - /// channels. - #[test] - fn cpe_short_grouping_is_joint() { - let fs_index = 4u8; - let short_len = SHORT_WINDOW_LEN as usize; - let offsets = short_window_offsets(fs_index).unwrap(); - let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; - // L jumps 60 dB at window 2, R at window 5: the independent - // groupings differ (the premise of the regression). - let mut l_spec = vec![0.0f64; 8 * short_len]; - let mut r_spec = vec![0.0f64; 8 * short_len]; - for w in 0..8 { - for k in 0..short_len { - let l_gain = if w < 2 { 30.0 } else { 30000.0 }; - let r_gain = if w < 5 { 30.0 } else { 30000.0 }; - l_spec[w * short_len + k] = l_gain * ((k as f64) * 0.23).sin(); - r_spec[w * short_len + k] = r_gain * ((k as f64) * 0.19).sin(); - } - } - let gl = decide_short_grouping(&l_spec, offsets, num_swb); - let gr = decide_short_grouping(&r_spec, offsets, num_swb); - assert_ne!(gl, gr, "premise: independent groupings diverge"); - - // Encode a stereo stream engineered to hit EIGHT_SHORT with - // per-channel attacks at different windows, and decode it - // with the crate's own decoder — a desynced CPE would fail - // to parse (or reconstruct garbage). - let n = 4 * FRAME_LEN; - let mut pcm = Vec::with_capacity(n * 2); - for i in 0..n { - let t = i as f64; - let in_l = (FRAME_LEN + 256..FRAME_LEN + 640).contains(&i); - let in_r = (FRAME_LEN + 640..FRAME_LEN + 1024).contains(&i); - let base = 400.0 * (0.09 * t).sin(); - let l = base + if in_l { 20000.0 * (0.5 * t).sin() } else { 0.0 }; - let r = base - + if in_r { - 20000.0 * (0.43 * t).sin() - } else { - 0.0 - }; - pcm.push(l.round() as i16); - pcm.push(r.round() as i16); - } - let mut enc = StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: 2, - bitrate: 192_000, - }) - .unwrap(); - let stream = enc.encode_all(&pcm).unwrap(); - let mut dec = crate::decode::StreamDecoder::new(); - let frames = dec.decode_all(&stream).unwrap(); - assert!(frames.len() >= 4); - // The burst energy must come back on the right channels. - let mut decoded = Vec::new(); - for f in &frames { - decoded.extend_from_slice(&f.pcm); - } - let aligned = &decoded[FRAME_LEN * 2..]; - let window_energy = |c: usize, range: core::ops::Range| -> f64 { - range.map(|i| f64::from(aligned[i * 2 + c]).powi(2)).sum() - }; - let l_burst = window_energy(0, FRAME_LEN + 256..FRAME_LEN + 640); - let r_quiet = window_energy(1, FRAME_LEN + 256..FRAME_LEN + 640); - assert!( - l_burst > 20.0 * r_quiet, - "left burst region not reconstructed: {l_burst:.0} vs {r_quiet:.0}" - ); - } - - /// Short-frame M/S: identical (and phase-inverted) channels - /// flag every `(group, sfb)` cell, independent channels flag - /// almost nothing, and an identical-channel transient stream - /// decodes with L exactly equal to R (all-M/S ⇒ `s ≡ 0` ⇒ the - /// de-matrix reproduces one channel twice). - #[test] - fn cpe_short_frame_ms_coding() { - let fs_index = 4u8; - let short_len = SHORT_WINDOW_LEN as usize; - let offsets = short_window_offsets(fs_index).unwrap(); - let num_swb = NUM_SWB_SHORT_WINDOW[fs_index as usize] as usize; - let mut spec = vec![0.0f64; 8 * short_len]; - let mut seed = 0x515u32; - for v in spec.iter_mut() { - seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); - *v = f64::from(seed >> 16) - 32768.0; - } - let inverted: Vec = spec.iter().map(|&v| -v).collect(); - let wgl = vec![2u8, 3, 3]; - let same = ms_decide_short(&spec, &spec, offsets, num_swb, &wgl); - assert!(same.iter().flatten().all(|&b| b), "identical pair all-M/S"); - let anti = ms_decide_short(&spec, &inverted, offsets, num_swb, &wgl); - assert!(anti.iter().flatten().all(|&b| b), "inverted pair all-M/S"); - let mut other = vec![0.0f64; 8 * short_len]; - for v in other.iter_mut() { - seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); - *v = f64::from(seed >> 16) - 32768.0; - } - let indep = ms_decide_short(&spec, &other, offsets, num_swb, &wgl); - let flagged = indep.iter().flatten().filter(|&&b| b).count(); - let total = indep.iter().flatten().count(); - assert!( - flagged * 4 < total, - "independent pair flagged {flagged}/{total}" - ); - // apply ∘ decide on the identical pair zeroes the side chain. - let (code_l, code_r) = apply_ms_short(&spec, &spec, &same, offsets, &wgl); - assert_eq!(code_l, spec); - assert!(code_r.iter().all(|&v| v == 0.0)); - - // End to end: identical channels with a percussive burst - // (short frames engaged) decode to L == R exactly. - let n = 4 * FRAME_LEN; - let mut pcm = Vec::with_capacity(n * 2); - for i in 0..n { - let t = i as f64; - let burst = (FRAME_LEN + 256..FRAME_LEN + 640).contains(&i); - let v = 500.0 * (0.07 * t).sin() - + if burst { - 18000.0 * (0.6 * t).sin() - } else { - 0.0 - }; - let s = v.round() as i16; - pcm.push(s); - pcm.push(s); - } - let mut enc = StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: 2, - bitrate: 128_000, - }) - .unwrap(); - let stream = enc.encode_all(&pcm).unwrap(); - let mut dec = crate::decode::StreamDecoder::new(); - let frames = dec.decode_all(&stream).unwrap(); - for (f, frame) in frames.iter().enumerate() { - for i in 0..(frame.pcm.len() / 2) { - assert_eq!( - frame.pcm[2 * i], - frame.pcm[2 * i + 1], - "frame {f} sample {i}: identical channels must decode identical" - ); - } - } - } - - #[test] - fn silent_input_yields_valid_minimal_frames() { - let mut enc = StreamEncoder::new(EncoderConfig { - sample_rate: 44_100, - channels: 1, - bitrate: 64_000, - }) - .unwrap(); - let stream = enc.encode_all(&[0i16; FRAME_LEN]).unwrap(); - // Two frames (content + flush), each parseable. - let (h0, off) = AdtsHeader::parse(&stream).unwrap(); - assert_eq!(h0.channel_configuration, 1); - assert_eq!(off, ADTS_HEADER_BYTES_NO_CRC); - let second = &stream[h0.aac_frame_length as usize..]; - let (h1, _) = AdtsHeader::parse(second).unwrap(); - assert_eq!( - h0.aac_frame_length as usize + h1.aac_frame_length as usize, - stream.len() - ); - } - - /// [`extract_pulse_candidate`] on a band with outlier lines: the - /// reduced spectrum plus the §4.6.3.3 fix-up - /// ([`crate::swb_offset::apply_pulse_data`]) restores the exact - /// original quantized values, and the measured per-band saving - /// is real (the reduced band's cheapest book costs at least the - /// pulse record less). - #[test] - fn pulse_candidate_reduction_is_exactly_invertible() { - let fs_index = 3u8; // 48 kHz - let offsets = long_window_offsets(fs_index).unwrap(); - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - let mut x_quant = vec![0i32; FRAME_LEN]; - let mut sfb_cb = vec![ZERO_HCB; num_swb]; - // Band 29 (32 lines at 48 kHz): a dense small-magnitude - // spectrum with two outliers (one negative) — the outliers - // alone force the whole band onto the ESC book. - let (s, e) = (offsets[29] as usize, offsets[30] as usize); - for (k, slot) in x_quant.iter_mut().enumerate().take(e).skip(s) { - *slot = 1 - ((k as i32) & 2); - } - x_quant[s + 1] = 17; - x_quant[s + 5] = -19; - sfb_cb[29] = codebook_for(19); - let (pd, reduced) = - extract_pulse_candidate(&x_quant, &sfb_cb, offsets).expect("outliers must qualify"); - assert_eq!(pd.pulse_start_sfb, 29); - assert_eq!(pd.pulses.len(), 2); - // The residual codes without the outliers' book demand - // (amp reach is 15, so 17 → 2 and −19 → −4, signs kept). - assert_eq!(reduced[s + 1], 2); - assert_eq!(reduced[s + 5], -4); - // Decode-side fix-up restores the original spectrum exactly. - let mut restored = reduced.clone(); - crate::swb_offset::apply_pulse_data(&mut restored, fs_index, &pd).unwrap(); - assert_eq!(restored, x_quant); - // The candidate's saving is real end to end. - let plain_bits = cheapest_band_bits(&x_quant[s..e]).unwrap() as u64; - let pulsed_bits = - cheapest_band_bits(&reduced[s..e]).unwrap() as u64 + pulse_record_bits(pd.pulses.len()); - assert!( - pulsed_bits < plain_bits, - "pulsed {pulsed_bits} vs plain {plain_bits}" - ); - } - - /// A spectrum-wide no-outlier profile yields no candidate, and a - /// candidate that does not measure smaller is dropped by the - /// [`quantize_channel`] decision (the emitted stream stays - /// pulse-free on flat content). - #[test] - fn pulse_candidate_requires_a_measured_win() { - let fs_index = 3u8; - let offsets = long_window_offsets(fs_index).unwrap(); - let num_swb = NUM_SWB_LONG_WINDOW[fs_index as usize] as usize; - // Flat band: every line the same magnitude — no outlier. - let mut x_quant = vec![0i32; FRAME_LEN]; - let mut sfb_cb = vec![ZERO_HCB; num_swb]; - let (s, e) = (offsets[10] as usize, offsets[11] as usize); - x_quant[s..e].fill(3); - sfb_cb[10] = codebook_for(3); - assert!(extract_pulse_candidate(&x_quant, &sfb_cb, offsets).is_none()); - } - - /// End-to-end pulse emission through [`quantize_channel`]: a - /// long-frame spectrum whose loud band carries one outlier line - /// over a low floor selects the pulse variant, the wire record - /// round-trips, and the §4.6.3.3 fix-up on the transmitted - /// spectrum reproduces the pulse-free quantization exactly (the - /// reconstruction is bit-identical by construction). - #[test] - fn quantize_channel_emits_measured_pulse_data() { - let fs_index = 3u8; // 48 kHz - let offsets = long_window_offsets(fs_index).unwrap(); - // Craft a spectrum: wide band 29 carries a moderate peak - // (the frame peak lives in band 20 so band 29's masking - // target lands near the escape threshold) plus a dense low - // floor across the band. - let mut spec = vec![0.0f64; FRAME_LEN]; - let (s29, e29) = (offsets[29] as usize, offsets[30] as usize); - spec[offsets[20] as usize] = 1.0; // frame peak, own band - spec[s29 + 3] = 0.17; // the outlier line - for (k, slot) in spec.iter_mut().enumerate().take(e29).skip(s29) { - if k != s29 + 3 { - *slot = 0.0095; // the band floor - } - } - let chan = - quantize_channel(&spec, WindowSequence::OnlyLong, fs_index, 0, 1.0, &[], &[]).unwrap(); - let plain = quantize_group( - &spec, - offsets, - NUM_SWB_LONG_WINDOW[fs_index as usize] as usize, - 0, - 1.0, - &mut None, - &[], - ); - assert!( - chan.body.pulse_data_present, - "outlier-over-floor band must select the pulse variant" - ); - let pd = chan.body.pulse_data.as_ref().unwrap(); - assert!((1..=MAX_PULSES).contains(&pd.pulses.len())); - // The transmitted spectrum restores to the plain quantization. - let mut restored = chan.spectral.x_quant[0].clone(); - crate::swb_offset::apply_pulse_data(&mut restored, fs_index, pd).unwrap(); - assert_eq!(restored, plain.x_quant); - // And the pulse variant is the smaller stream. - let plain_chan = { - let info = chan.info.clone(); - finish_channel(info, vec![plain], fs_index, None).unwrap() - }; - assert!( - channel_wire_bits(&chan, fs_index).unwrap() - < channel_wire_bits(&plain_chan, fs_index).unwrap() - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/encoder_tns.rs b/crates/vendor/oxideav-aac/src/encoder_tns.rs deleted file mode 100644 index a2510ebd..00000000 --- a/crates/vendor/oxideav-aac/src/encoder_tns.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! Encoder-side §4.6.9 Temporal Noise Shaping — decision, PARCOR -//! quantisation, and the analysis filtering pass. -//! -//! TNS is defined normatively from the decoder side only: §4.6.9.3 -//! specifies how transmitted filter coefficients are inverse-quantised -//! (`tns_decode_coef`), stepped up to LPC, and slid across the -//! spectrum as an **all-pole synthesis filter**. The encoder's job is -//! the inverse: pick a prediction filter over the spectral -//! coefficients, quantise it into the Table 4.54 wire fields, and run -//! the **all-zero analysis filter** (§4.6.7.4.1's `tns_ma_filter`, -//! the exact inverse of the synthesis filter over a shared region) on -//! the spectrum before quantisation, so the decoder's synthesis pass -//! reconstructs the original while shaping the quantisation noise in -//! time. -//! -//! Everything analysis-side (the autocorrelation, the Levinson-Durbin -//! recursion, the activation threshold) is an encoder degree of -//! freedom — any filter whose wire record is Table 4.54-conforming is -//! a conforming encode. The *applied* filter, however, must be -//! bit-identical to the one the decoder will derive from the wire, so -//! this module quantises the reflection coefficients first -//! ([`crate::tns_coef::tns_encode_coef`]) and then filters through -//! [`crate::tns_frame::tns_analysis_frame`], which re-derives the LPC -//! from the **wire** values exactly as `tns_decode_frame` does. The -//! encoder/decoder filter pair is therefore the §4.6.9.3 -//! analysis∘synthesis identity by construction. -//! -//! ## Decision rule -//! -//! Per transform window the encoder computes the autocorrelation of -//! the coverable spectral region (the same -//! `min(num_swb, TNS_MAX_BANDS, max_sfb)`-clamped region the §4.6.9.3 -//! walk will filter), runs Levinson-Durbin up to -//! [`TNS_ENC_MAX_ORDER`], and activates TNS only when the resulting -//! prediction gain `r(0) / err(order)` clears [`TNS_GAIN_MIN`]. A -//! high prediction gain over *frequency* coefficients means the -//! signal's *temporal* envelope inside the window is strongly -//! non-flat (the time/frequency duality TNS exploits, §4.6.9.1) — -//! exactly the windows where unshaped quantisation noise smears -//! audibly. Trailing reflection coefficients below -//! [`TNS_COEF_TRIM`] are trimmed to keep the order (and the 4-bit -//! coefficient payload) minimal. - -use crate::ics_info::WindowSequence; -use crate::swb_offset::{ - long_window_offsets, short_window_offsets, LONG_WINDOW_LEN, SHORT_WINDOW_LEN, -}; -use crate::tns_coef::tns_encode_coef; -use crate::tns_data::{num_windows, TnsData, TnsFilter, TnsWindow}; -use crate::tns_frame::tns_analysis_frame; -use crate::tns_max::{clamp_tns_band, tns_max_order, AOT_AAC_LC}; -use crate::Result; - -/// Encoder-side cap on the TNS filter order. Table 4.102 allows up -/// to 12 for AAC LC long windows (7 short), but each tap costs 4 -/// wire bits and the marginal gain past order 8 is small for a -/// first-order envelope model; the Levinson recursion below stops -/// early anyway once the prediction error stops shrinking. -pub const TNS_ENC_MAX_ORDER: usize = 8; - -/// Minimum §4.6.9.1 prediction gain (`r(0) / err`) for TNS to -/// activate on a window. Below ~1.4 the temporal envelope is close -/// enough to flat that the side-info bits outweigh the shaping win. -pub const TNS_GAIN_MIN: f64 = 1.4; - -/// Reflection-coefficient trim threshold: trailing PARCOR values -/// with `|k|` below this contribute negligible shaping and are -/// dropped to shorten the transmitted order. -pub const TNS_COEF_TRIM: f64 = 0.1; - -/// `coef_res` the encoder always transmits: `true` selects the 4-bit -/// (`coef_res_bits == 4`) resolution of §4.6.9.3, the finer of the -/// two grids. -const TNS_COEF_RES: bool = true; - -/// One window's TNS decision: the reflection coefficients that -/// survived the gain threshold and trim, ready for quantisation. -struct WindowDecision { - /// PARCOR reflection coefficients, order `parcor.len()`. - parcor: Vec, -} - -/// Autocorrelation `r[0..=max_lag]` of `region`. -fn autocorrelation(region: &[f64], max_lag: usize) -> Vec { - let n = region.len(); - (0..=max_lag.min(n.saturating_sub(1))) - .map(|lag| (0..n - lag).map(|i| region[i] * region[i + lag]).sum()) - .collect() -} - -/// Levinson-Durbin recursion on the autocorrelation `r`, returning -/// the reflection (PARCOR) coefficients and the final prediction -/// error. The per-step update matches the §4.6.9.3 step-up -/// ([`crate::tns_coef::lpc_step_up`]) convention — `a_m[i] = -/// a_{m-1}[i] + k_m · a_{m-1}[m-i]`, `a_m[m] = k_m` — so the -/// returned `k` values, once quantised and stepped up by the -/// decoder, reproduce this exact predictor. The analysis filter is -/// then `y(n) = x(n) + Σ a[i]·x(n-i)` (the §4.6.7.4.1 -/// `tns_ma_filter` polarity), i.e. `a[]` is the prediction-*error* -/// filter tail. -fn levinson(r: &[f64], max_order: usize) -> (Vec, f64) { - let mut err = r[0]; - if err <= 0.0 { - return (Vec::new(), err); - } - let order = max_order.min(r.len().saturating_sub(1)); - let mut a = vec![0.0f64; order + 1]; - a[0] = 1.0; - let mut k_out = Vec::with_capacity(order); - let mut b = vec![0.0f64; order + 1]; - for m in 1..=order { - // acc = r[m] + Σ_{i=1}^{m-1} a[i]·r[m-i] - let mut acc = r[m]; - for i in 1..m { - acc += a[i] * r[m - i]; - } - let k = -acc / err; - if !k.is_finite() || k.abs() >= 1.0 { - // Numerically degenerate (r not positive definite at - // this order) — stop with the taps found so far. - break; - } - // Step-up update, mirroring lpc_step_up so the decoder's - // reconstruction of `a` from the k's is this exact array. - for i in 1..m { - b[i] = a[i] + k * a[m - i]; - } - a[1..m].copy_from_slice(&b[1..m]); - a[m] = k; - k_out.push(k); - err *= 1.0 - k * k; - if err <= 0.0 { - break; - } - } - (k_out, err) -} - -/// Decide TNS for one window's coverable region. Returns `None` -/// when the prediction gain does not clear [`TNS_GAIN_MIN`] or the -/// trim leaves no taps. -fn decide_window(region: &[f64], max_order: usize) -> Option { - if region.len() < 2 * max_order.max(1) { - return None; - } - let r = autocorrelation(region, max_order); - if r[0] <= 0.0 { - return None; - } - let (mut parcor, err) = levinson(&r, max_order); - if parcor.is_empty() || err <= 0.0 { - return None; - } - let gain = r[0] / err; - if gain < TNS_GAIN_MIN { - return None; - } - while parcor.last().is_some_and(|k| k.abs() < TNS_COEF_TRIM) { - parcor.pop(); - } - if parcor.is_empty() { - return None; - } - Some(WindowDecision { parcor }) -} - -/// Detect and apply §4.6.9 TNS to one channel's analysis spectrum in -/// place. -/// -/// `spec` is the window-major forward-MDCT spectrum -/// (`num_windows × window_len`, the encoder's analysis output before -/// quantisation), `seq` / `max_sfb` / `fs_index` the surrounding -/// `ics_info()` parameters (the encoder transmits `max_sfb == -/// num_swb`). `permit` is the caller's per-window **temporal** gate -/// (length [`num_windows`], see below); for every permitted window -/// whose coverable region clears the [`TNS_GAIN_MIN`] -/// prediction-gain threshold, one upward filter covering the full -/// §4.6.9.3-clamped band range is quantised into Table 4.54 wire -/// fields; the whole-frame [`TnsData`] is then run through -/// [`tns_analysis_frame`] — deriving the LPC from the **wire** -/// coefficient values exactly as the decoder's `tns_decode_frame` -/// will — so the applied analysis filter and the decoder's synthesis -/// filter are exact inverses. -/// -/// ## Why a temporal gate -/// -/// Spectral prediction gain alone over-fires: a *steady tonal* -/// window also shows LPC gain over its MDCT coefficients (the smooth -/// leakage skirts around each spectral line are highly predictable) -/// even though its temporal envelope is flat — exactly the windows -/// where TNS buys nothing and merely re-shapes (and, at spectral -/// peaks, locally amplifies) the quantisation noise of a -/// peak-anchored rate allocation. The §4.6.9.1 duality says TNS pays -/// off when the *time-domain* envelope inside the window is strongly -/// non-flat, which the encoder can measure directly on its input -/// samples — so the caller derives `permit[w]` from the raw -/// subblock-energy flatness of window `w`'s time region (see -/// `StreamEncoder`'s hop driver) and this module only spends -/// prediction-gain analysis on permitted windows. -/// -/// Returns `Ok(None)` (spectrum untouched) when no window activates. -pub fn detect_and_apply_tns( - spec: &mut [f64], - seq: WindowSequence, - max_sfb: u8, - fs_index: u8, - permit: &[bool], -) -> Result> { - let nw = num_windows(seq); - let (window_len, offsets) = if seq.is_eight_short() { - (SHORT_WINDOW_LEN as usize, short_window_offsets(fs_index)?) - } else { - (LONG_WINDOW_LEN as usize, long_window_offsets(fs_index)?) - }; - let num_swb = offsets.len() - 1; - // The §4.6.9.3 region for a full-length (length == num_swb, - // bottom == 0) upward filter: [swb_offset[0], - // swb_offset[min(num_swb, TNS_MAX_BANDS, max_sfb)]). - let top = clamp_tns_band(num_swb as u8, max_sfb, AOT_AAC_LC, seq, fs_index)? as usize; - let end = offsets[top] as usize; - let max_order = TNS_ENC_MAX_ORDER.min(tns_max_order(AOT_AAC_LC, seq, fs_index)? as usize); - - let mut windows = Vec::with_capacity(nw); - let mut any = false; - for w in 0..nw { - let region = &spec[w * window_len..w * window_len + end]; - let decision = if permit.get(w).copied().unwrap_or(false) { - decide_window(region, max_order) - } else { - None - }; - let filters = match decision { - Some(d) => { - // Quantise PARCOR → wire coef[] (4-bit grid). The - // analysis pass below re-derives the LPC from these - // wire values, so the filter actually applied is the - // quantised one the decoder will invert. - let coef = tns_encode_coef(4, 0, &d.parcor)? - .into_iter() - .map(|c| c as u8) - .collect::>(); - any = true; - vec![TnsFilter { - length: num_swb as u8, - order: coef.len() as u8, - direction: false, - coef_compress: false, - coef, - }] - } - None => Vec::new(), - }; - windows.push(TnsWindow { - coef_res: TNS_COEF_RES, - filters, - }); - } - if !any { - return Ok(None); - } - let tns = TnsData { windows }; - tns_analysis_frame(spec, &tns, seq, max_sfb, AOT_AAC_LC, fs_index)?; - Ok(Some(tns)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::WindowSequence; - use crate::tns_frame::tns_decode_frame; - - /// Deterministic pseudo-noise in [-1, 1). - fn noise(n: usize, seed: u32) -> Vec { - let mut state = seed; - (0..n) - .map(|_| { - state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); - (state as i32) as f64 / 2_147_483_648.0 - }) - .collect() - } - - /// Run `x` through the all-pole filter `1 / (1 + Σ a[i] z^-i)` - /// (the synthesis polarity), producing a strongly correlated - /// sequence whose optimal prediction-error filter is `a`. - fn all_pole(x: &[f64], a: &[f64]) -> Vec { - let mut y = vec![0.0f64; x.len()]; - for n in 0..x.len() { - let mut v = x[n]; - for (i, &ai) in a.iter().enumerate() { - let d = i + 1; - if n >= d { - v -= ai * y[n - d]; - } - } - y[n] = v; - } - y - } - - #[test] - fn levinson_recovers_ar1_reflection() { - // AR(1) with pole 0.8: prediction-error filter a = [-0.8], - // reflection k1 = -0.8. - let x = noise(4096, 0xC0FF_EE00); - let y = all_pole(&x, &[-0.8]); - let r = autocorrelation(&y, 4); - let (k, err) = levinson(&r, 4); - assert!(!k.is_empty()); - assert!( - (k[0] + 0.8).abs() < 0.05, - "k1 = {} should approximate -0.8", - k[0] - ); - // Prediction gain ≈ 1/(1 - 0.64) ≈ 2.8. - let gain = r[0] / err; - assert!(gain > 2.0, "gain {gain} too low for AR(1) 0.8"); - } - - #[test] - fn white_region_stays_untns() { - // A flat (white) region has prediction gain ≈ 1 — below the - // threshold — so no filter fires. - let region = noise(512, 0xDEAD_BEEF); - assert!(decide_window(®ion, TNS_ENC_MAX_ORDER).is_none()); - } - - #[test] - fn correlated_region_activates_and_analysis_whitens() { - // Long window, fs 48 kHz. Fill the coverable region with a - // strongly correlated AR process; TNS must fire, the analysis - // pass must reduce the region's energy (whitening), and the - // decoder's tns_decode_frame must restore the original - // spectrum exactly (the §4.6.9.3 analysis∘synthesis - // identity on the shared quantised filter). - let fs = 3u8; - let seq = WindowSequence::OnlyLong; - let n = LONG_WINDOW_LEN as usize; - let x = noise(n, 0x1234_5678); - let mut spec = all_pole(&x, &[-1.2, 0.5]); - // Scale to a realistic coefficient magnitude. - for v in spec.iter_mut() { - *v *= 1000.0; - } - let original = spec.clone(); - let max_sfb = (long_window_offsets(fs).unwrap().len() - 1) as u8; - - let tns = detect_and_apply_tns(&mut spec, seq, max_sfb, fs, &[true]) - .unwrap() - .expect("correlated spectrum must activate TNS"); - assert_eq!(tns.windows.len(), 1); - assert_eq!(tns.windows[0].filters.len(), 1); - let f = &tns.windows[0].filters[0]; - assert!(f.order >= 1); - assert_eq!(f.coef.len(), f.order as usize); - assert!(!f.direction); - - let e = |s: &[f64]| s.iter().map(|&v| v * v).sum::(); - assert!( - e(&spec) < 0.8 * e(&original), - "analysis should whiten: {} vs {}", - e(&spec), - e(&original) - ); - - // Round-trip: the decoder synthesis restores the original. - tns_decode_frame(&mut spec, &tns, seq, max_sfb, AOT_AAC_LC, fs).unwrap(); - for (a, b) in spec.iter().zip(original.iter()) { - assert!((a - b).abs() < 1e-6, "synthesis must invert analysis"); - } - } - - #[test] - fn short_windows_decide_independently() { - // Eight short windows: give window 3 a correlated region and - // leave the rest white — only window 3 fires. - let fs = 3u8; - let seq = WindowSequence::EightShort; - let wlen = SHORT_WINDOW_LEN as usize; - let mut spec = vec![0.0f64; 8 * wlen]; - for w in 0..8 { - let seed = 0x9E37_79B9u32.wrapping_add(w as u32); - let x = noise(wlen, seed); - let win = if w == 3 { - all_pole(&x, &[-1.4, 0.6]) - } else { - x - }; - for (i, v) in win.iter().enumerate() { - spec[w * wlen + i] = v * 500.0; - } - } - let max_sfb = (short_window_offsets(fs).unwrap().len() - 1) as u8; - let tns = detect_and_apply_tns(&mut spec, seq, max_sfb, fs, &[true; 8]) - .unwrap() - .expect("window 3 must activate"); - assert_eq!(tns.windows.len(), 8); - assert!(!tns.windows[3].filters.is_empty(), "window 3 fires"); - for w in [0usize, 1, 2, 4, 5, 6, 7] { - assert!( - tns.windows[w].filters.is_empty(), - "white window {w} must not fire" - ); - } - // Short-window field caps: order ≤ 7, length fits 4 bits. - let f = &tns.windows[3].filters[0]; - assert!(f.order <= 7); - assert!(f.length <= 15); - } - - #[test] - fn silent_spectrum_never_activates() { - let fs = 4u8; - let mut spec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let max_sfb = (long_window_offsets(fs).unwrap().len() - 1) as u8; - let tns = detect_and_apply_tns(&mut spec, WindowSequence::OnlyLong, max_sfb, fs, &[true]) - .unwrap(); - assert!(tns.is_none()); - assert!(spec.iter().all(|&v| v == 0.0)); - } -} diff --git a/crates/vendor/oxideav-aac/src/ep_config.rs b/crates/vendor/oxideav-aac/src/ep_config.rs deleted file mode 100644 index 14d7efc6..00000000 --- a/crates/vendor/oxideav-aac/src/ep_config.rs +++ /dev/null @@ -1,586 +0,0 @@ -//! `ErrorProtectionSpecificConfig()` — ISO/IEC 14496-3 §1.8.2.1 -//! Table 1.49, the out-of-band half of the §1.8 error-protection (EP) -//! tool, plus the §1.8.4.2 pre-defined-set derivation. -//! -//! The EP tool protects an access unit as a sequence of *classes* -//! (§1.8.1): each class carries a CRC (§1.8.4.5), an FEC — SRCPC -//! (§1.8.4.6) or shortened Reed-Solomon (§1.8.4.7) — and optional -//! interleaving (§1.8.4.8). Everything constant across frames rides -//! this configuration; the per-frame remainder (choice of pre-defined -//! set, escaped class parameters, stuffing count) rides the in-band -//! `ep_header()` (§1.8.2.2 / §1.8.4.3). -//! -//! The `class_optional` unwrapping (§1.8.4.2) expands every wire -//! pre-defined set with `N` optional classes into `2^N` transmission -//! sets — from "all optional classes present" (`j == 0`) down to -//! "none present"; [`ErrorProtectionSpecificConfig::expand`] is that -//! algorithm verbatim, and the in-band `choice_of_pred` indexes the -//! **expanded** list. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::crc::CrcPoly; -use crate::{Error, Result}; - -/// Per-class parameters of one wire pre-defined set (Table 1.49 inner -/// loop). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EpClass { - /// `length_escape` — `true` ⇒ the class length is signalled - /// in-band with `number_of_bits_for_length` bits (0 = the - /// §1.8.4.1 "until the end" class). - pub length_escape: bool, - /// `rate_escape` — `true` ⇒ the code rate is signalled in-band. - pub rate_escape: bool, - /// `crclen_escape` — `true` ⇒ the CRC length is signalled - /// in-band. - pub crclen_escape: bool, - /// `concatenate_flag` — present on the wire only when - /// `number_of_concatenated_frame != 1` (§1.8.4.4); `false` - /// otherwise. - pub concatenate_flag: bool, - /// `fec_type` (2 bits): `0` SRCPC; `1` RS (last / independent); - /// `2` RS concatenated with the next class. - pub fec_type: u8, - /// `termination_switch` — present iff `fec_type == 0` - /// (§1.8.4.6.2). - pub termination_switch: Option, - /// `interleave_switch` (2 bits) — present iff - /// `interleave_type == 2` (Table 1.64). - pub interleave_switch: Option, - /// `class_optional` — the §1.8.4.2 expansion flag. - pub class_optional: bool, - /// `number_of_bits_for_length` (4 bits) iff `length_escape`. - pub number_of_bits_for_length: Option, - /// `class_length` (16 bits) iff `!length_escape`. **Bits** for - /// SRCPC classes; must be a whole number of octets for RS classes - /// (§1.8.3.1 `fec_type`). - pub class_length: Option, - /// `class_rate` iff `!rate_escape` — 5 bits for SRCPC (0..=24 ⇒ - /// rate 8/8..8/32), 7 bits for RS (the number of correctable - /// bytes `k`, §1.8.4.7). - pub class_rate: Option, - /// `class_crclen` (5 bits) iff `!crclen_escape` — 0..=18 ⇒ CRC - /// length 0..=16 / 24 / 32 (§1.8.3.1). - pub class_crclen: Option, -} - -impl EpClass { - /// Resolve the §1.8.3.1 `class_crclen` code (0..=18) to a CRC bit - /// width. - pub fn crclen_bits(code: u8) -> Result { - Ok(match code { - 0..=16 => u32::from(code), - 17 => 24, - 18 => 32, - _ => return Err(Error::EpConfigInvalid), - }) - } - - /// The §1.8.4.5 generator for a CRC width produced by - /// [`EpClass::crclen_bits`] (widths 1..=16, 24, 32). - pub fn crc_poly(width: u32) -> Result> { - Ok(Some(match width { - 0 => return Ok(None), - 1 => CrcPoly::Crc1, - 2 => CrcPoly::Crc2, - 3 => CrcPoly::Crc3, - 4 => CrcPoly::Crc4, - 5 => CrcPoly::Crc5, - 6 => CrcPoly::Crc6, - 7 => CrcPoly::Crc7, - 8 => CrcPoly::Crc8, - 9 => CrcPoly::Crc9, - 10 => CrcPoly::Crc10, - 11 => CrcPoly::Crc11, - 12 => CrcPoly::Crc12, - 13 => CrcPoly::Crc13, - 14 => CrcPoly::Crc14, - 15 => CrcPoly::Crc15, - 16 => CrcPoly::Crc16, - 24 => CrcPoly::Crc24, - 32 => CrcPoly::Crc32, - _ => return Err(Error::EpConfigInvalid), - })) - } -} - -/// One pre-defined set (Table 1.49 outer loop): the class list plus -/// the §1.8.4.9 output reordering. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EpPredefinedSet { - /// The per-class parameter list. - pub classes: Vec, - /// `class_reordered_output` (§1.8.4.9). - pub class_reordered_output: bool, - /// `class_output_order[j]` (6 bits each) iff reordered: the j-th - /// EP-frame class is output as the `class_output_order[j]`-th - /// class to the audio decoder. - pub class_output_order: Vec, -} - -/// Parsed `ErrorProtectionSpecificConfig()` (Table 1.49). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ErrorProtectionSpecificConfig { - /// `interleave_type` (2 bits): 0 none, 1 intra-frame, 2 per-class - /// fine tuning; 3 reserved (rejected). - pub interleave_type: u8, - /// `bit_stuffing` (3 bits): 1 ⇒ `num_stuffing_bits` rides - /// `class_attrib()`. - pub bit_stuffing: u8, - /// `number_of_concatenated_frame` (3 bits): source frames per EP - /// frame; 0 is reserved (Table 1.54). - pub number_of_concatenated_frame: u8, - /// The wire pre-defined sets (before §1.8.4.2 expansion). - pub sets: Vec, - /// `header_protection`: extended in-band header FEC (§1.8.4.3). - pub header_protection: bool, - /// `header_rate` (5 bits) iff `header_protection`. - pub header_rate: Option, - /// `header_crclen` (5 bits) iff `header_protection`. - pub header_crclen: Option, -} - -impl ErrorProtectionSpecificConfig { - /// Parse a Table 1.49 configuration. - pub fn parse(reader: &mut BitReader<'_>) -> Result { - let number_of_predefined_set = read_u8(reader, 8)?; - let interleave_type = read_u8(reader, 2)?; - if interleave_type == 3 { - // §1.8.3.1: reserved. - return Err(Error::EpConfigInvalid); - } - let bit_stuffing = read_u8(reader, 3)?; - let number_of_concatenated_frame = read_u8(reader, 3)?; - if number_of_concatenated_frame == 0 { - // Table 1.54: codeword 000 is reserved. - return Err(Error::EpConfigInvalid); - } - let mut sets = Vec::with_capacity(usize::from(number_of_predefined_set)); - for _i in 0..number_of_predefined_set { - let number_of_class = read_u8(reader, 6)?; - let mut classes = Vec::with_capacity(usize::from(number_of_class)); - for _j in 0..number_of_class { - let length_escape = read_bit(reader)?; - let rate_escape = read_bit(reader)?; - let crclen_escape = read_bit(reader)?; - let concatenate_flag = if number_of_concatenated_frame != 1 { - read_bit(reader)? - } else { - false - }; - let fec_type = read_u8(reader, 2)?; - if fec_type == 3 { - return Err(Error::EpConfigInvalid); - } - let termination_switch = if fec_type == 0 { - Some(read_bit(reader)?) - } else { - None - }; - let interleave_switch = if interleave_type == 2 { - let v = read_u8(reader, 2)?; - // Table 1.64: width-28 intraclass interleaving is - // SRCPC-only. - if v == 2 && fec_type != 0 { - return Err(Error::EpConfigInvalid); - } - Some(v) - } else { - None - }; - let class_optional = read_bit(reader)?; - let (number_of_bits_for_length, class_length) = if length_escape { - (Some(read_u8(reader, 4)?), None) - } else { - (None, Some(read_u16(reader, 16)?)) - }; - let class_rate = if !rate_escape { - let bits = if fec_type != 0 { 7 } else { 5 }; - let v = read_u8(reader, bits)?; - if fec_type == 0 && v > 24 { - // §1.8.3.1: 0..=24 map onto 8/8..8/32. - return Err(Error::EpConfigInvalid); - } - Some(v) - } else { - None - }; - let class_crclen = if !crclen_escape { - let v = read_u8(reader, 5)?; - EpClass::crclen_bits(v)?; - Some(v) - } else { - None - }; - classes.push(EpClass { - length_escape, - rate_escape, - crclen_escape, - concatenate_flag, - fec_type, - termination_switch, - interleave_switch, - class_optional, - number_of_bits_for_length, - class_length, - class_rate, - class_crclen, - }); - } - let class_reordered_output = read_bit(reader)?; - let mut class_output_order = Vec::new(); - if class_reordered_output { - for _j in 0..number_of_class { - let v = read_u8(reader, 6)?; - if v >= number_of_class { - return Err(Error::EpConfigInvalid); - } - class_output_order.push(v); - } - // The order must be a permutation of 0..number_of_class. - let mut seen = vec![false; usize::from(number_of_class)]; - for &v in &class_output_order { - if core::mem::replace(&mut seen[usize::from(v)], true) { - return Err(Error::EpConfigInvalid); - } - } - } - sets.push(EpPredefinedSet { - classes, - class_reordered_output, - class_output_order, - }); - } - let header_protection = read_bit(reader)?; - let (header_rate, header_crclen) = if header_protection { - let rate = read_u8(reader, 5)?; - if rate > 24 { - return Err(Error::EpConfigInvalid); - } - let crclen = read_u8(reader, 5)?; - EpClass::crclen_bits(crclen)?; - (Some(rate), Some(crclen)) - } else { - (None, None) - }; - Ok(ErrorProtectionSpecificConfig { - interleave_type, - bit_stuffing, - number_of_concatenated_frame, - sets, - header_protection, - header_rate, - header_crclen, - }) - } - - /// Emit the Table 1.49 configuration — the bit-exact inverse of - /// [`ErrorProtectionSpecificConfig::parse`]. - pub fn write(&self, w: &mut BitWriter) -> Result<()> { - if self.sets.len() > 255 - || self.interleave_type > 2 - || self.number_of_concatenated_frame == 0 - || self.number_of_concatenated_frame > 7 - || self.bit_stuffing > 7 - { - return Err(Error::EpConfigInvalid); - } - w.write_u32(self.sets.len() as u32, 8); - w.write_u32(u32::from(self.interleave_type), 2); - w.write_u32(u32::from(self.bit_stuffing), 3); - w.write_u32(u32::from(self.number_of_concatenated_frame), 3); - for set in &self.sets { - if set.classes.len() > 63 { - return Err(Error::EpConfigInvalid); - } - w.write_u32(set.classes.len() as u32, 6); - for c in &set.classes { - w.write_bit(c.length_escape); - w.write_bit(c.rate_escape); - w.write_bit(c.crclen_escape); - if self.number_of_concatenated_frame != 1 { - w.write_bit(c.concatenate_flag); - } - if c.fec_type > 2 { - return Err(Error::EpConfigInvalid); - } - w.write_u32(u32::from(c.fec_type), 2); - if c.fec_type == 0 { - w.write_bit(c.termination_switch.ok_or(Error::EpConfigInvalid)?); - } - if self.interleave_type == 2 { - w.write_u32( - u32::from(c.interleave_switch.ok_or(Error::EpConfigInvalid)?), - 2, - ); - } - w.write_bit(c.class_optional); - if c.length_escape { - let n = c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?; - if n > 15 { - return Err(Error::EpConfigInvalid); - } - w.write_u32(u32::from(n), 4); - } else { - w.write_u32(u32::from(c.class_length.ok_or(Error::EpConfigInvalid)?), 16); - } - if !c.rate_escape { - let bits = if c.fec_type != 0 { 7 } else { 5 }; - w.write_u32(u32::from(c.class_rate.ok_or(Error::EpConfigInvalid)?), bits); - } - if !c.crclen_escape { - w.write_u32(u32::from(c.class_crclen.ok_or(Error::EpConfigInvalid)?), 5); - } - } - w.write_bit(set.class_reordered_output); - if set.class_reordered_output { - if set.class_output_order.len() != set.classes.len() { - return Err(Error::EpConfigInvalid); - } - for &v in &set.class_output_order { - w.write_u32(u32::from(v), 6); - } - } - } - w.write_bit(self.header_protection); - if self.header_protection { - w.write_u32( - u32::from(self.header_rate.ok_or(Error::EpConfigInvalid)?), - 5, - ); - w.write_u32( - u32::from(self.header_crclen.ok_or(Error::EpConfigInvalid)?), - 5, - ); - } - Ok(()) - } - - /// §1.8.4.2 — expand the `class_optional` flags into the - /// transmission pre-defined sets the in-band `choice_of_pred` - /// indexes. - /// - /// Each wire set with `N` optional classes yields `2^N` sets, from - /// "all optional classes present" (`j == 0`) to "none present" - /// (`j == 2^N − 1`); bit `k` of `j` clears the `k`-th optional - /// class. The expanded sets carry `class_optional == false` - /// throughout. - pub fn expand(&self) -> Result> { - let mut out = Vec::new(); - for set in &self.sets { - let opt_idx: Vec = set - .classes - .iter() - .enumerate() - .filter(|(_, c)| c.class_optional) - .map(|(i, _)| i) - .collect(); - let nco = opt_idx.len(); - if nco > 16 { - // 2^N sets would be unbounded; a conforming config - // never needs this many optional classes. - return Err(Error::EpConfigInvalid); - } - for j in 0u32..(1u32 << nco) { - let mut classes = Vec::with_capacity(set.classes.len()); - let mut kept_index = Vec::with_capacity(set.classes.len()); - for (i, c) in set.classes.iter().enumerate() { - let keep = match opt_idx.iter().position(|&o| o == i) { - Some(k) => j & (1 << k) == 0, - None => true, - }; - if keep { - let mut cc = c.clone(); - cc.class_optional = false; - classes.push(cc); - kept_index.push(i); - } - } - // The output order shrinks with the dropped classes: - // surviving entries keep their relative order. - let class_output_order = if set.class_reordered_output { - let mut order: Vec = Vec::with_capacity(classes.len()); - // Rank the surviving original output positions. - let mut kept_orders: Vec = kept_index - .iter() - .map(|&i| set.class_output_order[i]) - .collect(); - let mut sorted = kept_orders.clone(); - sorted.sort_unstable(); - for v in kept_orders.iter_mut() { - let rank = sorted.iter().position(|&s| s == *v).unwrap_or(0) as u8; - order.push(rank); - } - order - } else { - Vec::new() - }; - out.push(EpPredefinedSet { - classes, - class_reordered_output: set.class_reordered_output, - class_output_order, - }); - } - } - if out.is_empty() { - return Err(Error::EpConfigInvalid); - } - Ok(out) - } -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} - -fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { - Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -fn read_u16(reader: &mut BitReader<'_>, bits: u32) -> Result { - Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u16) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn simple_class(len: u16, rate: u8, crclen: u8) -> EpClass { - EpClass { - length_escape: false, - rate_escape: false, - crclen_escape: false, - concatenate_flag: false, - fec_type: 0, - termination_switch: Some(true), - interleave_switch: None, - class_optional: false, - number_of_bits_for_length: None, - class_length: Some(len), - class_rate: Some(rate), - class_crclen: Some(crclen), - } - } - - #[test] - fn roundtrip_two_sets() { - let cfg = ErrorProtectionSpecificConfig { - interleave_type: 0, - bit_stuffing: 0, - number_of_concatenated_frame: 1, - sets: vec![ - EpPredefinedSet { - classes: vec![simple_class(40, 8, 6), simple_class(100, 0, 0)], - class_reordered_output: false, - class_output_order: Vec::new(), - }, - EpPredefinedSet { - classes: vec![simple_class(24, 24, 8)], - class_reordered_output: false, - class_output_order: Vec::new(), - }, - ], - header_protection: false, - header_rate: None, - header_crclen: None, - }; - let mut w = BitWriter::new(); - cfg.write(&mut w).unwrap(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let parsed = ErrorProtectionSpecificConfig::parse(&mut r).unwrap(); - assert_eq!(parsed, cfg); - } - - /// The §1.8.4.2 example: pred #0 with optional classes A, C, E of - /// {A, B, C, D, E} and pred #1 with optional F of {F, G} expand - /// into the Table 1.58 ten sets. - #[test] - fn expansion_matches_table_1_58() { - // Give every class a distinct length so the expanded sets are - // recognisable. - let mk = |len: u16, opt: bool| EpClass { - class_optional: opt, - ..simple_class(len, 0, 0) - }; - let cfg = ErrorProtectionSpecificConfig { - interleave_type: 0, - bit_stuffing: 0, - number_of_concatenated_frame: 1, - sets: vec![ - EpPredefinedSet { - // A=1(opt) B=2 C=3(opt) D=4 E=5(opt) - classes: vec![ - mk(1, true), - mk(2, false), - mk(3, true), - mk(4, false), - mk(5, true), - ], - class_reordered_output: false, - class_output_order: Vec::new(), - }, - EpPredefinedSet { - // F=6(opt) G=7 - classes: vec![mk(6, true), mk(7, false)], - class_reordered_output: false, - class_output_order: Vec::new(), - }, - ], - header_protection: false, - header_rate: None, - header_crclen: None, - }; - let expanded = cfg.expand().unwrap(); - let lens: Vec> = expanded - .iter() - .map(|s| s.classes.iter().map(|c| c.class_length.unwrap()).collect()) - .collect(); - // Table 1.58 columns (A..G as 1..7). - assert_eq!( - lens, - vec![ - vec![1, 2, 3, 4, 5], // all present - vec![2, 3, 4, 5], // A absent - vec![1, 2, 4, 5], // C absent - vec![2, 4, 5], // A, C absent - vec![1, 2, 3, 4], // E absent - vec![2, 3, 4], // A, E absent - vec![1, 2, 4], // C, E absent - vec![2, 4], // A, C, E absent - vec![6, 7], // pred #1, F present - vec![7], // pred #1, F absent - ] - ); - } - - #[test] - fn reserved_fields_rejected() { - // interleave_type == 3. - let mut w = BitWriter::new(); - w.write_u32(1, 8); // number_of_predefined_set - w.write_u32(3, 2); // interleave_type (reserved) - w.write_u32(0, 3); - w.write_u32(1, 3); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert_eq!( - ErrorProtectionSpecificConfig::parse(&mut r).unwrap_err(), - Error::EpConfigInvalid - ); - - // number_of_concatenated_frame == 0 (Table 1.54 reserved). - let mut w = BitWriter::new(); - w.write_u32(1, 8); - w.write_u32(0, 2); - w.write_u32(0, 3); - w.write_u32(0, 3); // reserved - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert_eq!( - ErrorProtectionSpecificConfig::parse(&mut r).unwrap_err(), - Error::EpConfigInvalid - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/ep_fec.rs b/crates/vendor/oxideav-aac/src/ep_fec.rs deleted file mode 100644 index feec9a7c..00000000 --- a/crates/vendor/oxideav-aac/src/ep_fec.rs +++ /dev/null @@ -1,652 +0,0 @@ -//! §1.8.4.6 SRCPC convolutional FEC and the §1.8.4.3 in-band-header -//! block codes of the MPEG-4 error-protection tool. -//! -//! ## SRCPC (§1.8.4.6) -//! -//! A systematic recursive convolutional code of rate 1/4 (Figure -//! 1.10) punctured to 8/8..8/32 (Table 1.61). Per input bit `u` with -//! state `(m1, m2, m3, m4)` and feedback `d = m4 ⊕ m2 ⊕ m1`: -//! -//! ```text -//! v1 = u -//! v2 = m3 ⊕ m2 ⊕ m1 ⊕ u -//! v3 = m3 ⊕ m1 ⊕ u -//! v4 = m3 ⊕ m2 ⊕ u -//! next state: (u ⊕ d, m1, m2, m3) -//! ``` -//! -//! Puncturing runs with period 8: bit `7 − (t mod 8)` of `Pr(i)` -//! decides whether `v(i+1)` at time `t` is emitted; surviving bits go -//! out in `v1..v4` order per time step. `Pr(0) == 0xFF` for every -//! rate, so the code stays systematic. Termination (§1.8.4.6.2) -//! appends four tail input bits `u = d` driving the state to zero -//! (the Table 1.60 tail-bit listing is the closed form of exactly -//! that rule — pinned by a test). -//! -//! Decoding is hard-decision Viterbi over the 16-state trellis -//! (§1.8.4.6.4); an error-free stream round-trips exactly, and up to -//! the code's correction capability transmission errors are repaired. -//! -//! ## In-band header FEC (§1.8.4.3, Table 1.59) -//! -//! The `choice_of_pred` / `class_attrib()` header parts are protected -//! by a length-selected block code: 3× repetition (1–2 bits), -//! BCH(7,4) (3–4), BCH(15,7) (5–7), Golay(23,12) (8–12), BCH(31,16) -//! (13–16), or — for 17+ bits — CRC4 + terminated SRCPC 8/16. The -//! parity of the polynomial codes is `R(x)` of -//! `M(x)·x^deg(G) = Q(x)G(x) + R(x)` with the §1.8.4.3 generators; -//! decode-side correction is bounded-distance (exhaustive syndrome -//! search up to the code's design correction capability). - -use crate::crc::{crc_bits, CrcPoly}; -use crate::{Error, Result}; - -/// Number of tail input bits appended by §1.8.4.6.2 termination. -pub const SRCPC_TAIL_BITS: usize = 4; - -/// The nine-step per-output-line puncture progression of Table 1.61 -/// (`00, 80, 88, A8, AA, EA, EE, FE, FF`): entry `j` keeps `j` of the -/// eight period positions. -const PUNCTURE_STEPS: [u8; 9] = [0x00, 0x80, 0x88, 0xA8, 0xAA, 0xEA, 0xEE, 0xFE, 0xFF]; - -/// The Table 1.61 puncture pattern `[Pr(0), Pr(1), Pr(2), Pr(3)]` for -/// `class_rate` 0..=24 (rate 8/8 .. 8/32). -pub fn puncture_pattern(class_rate: u8) -> Result<[u8; 4]> { - if class_rate > 24 { - return Err(Error::EpConfigInvalid); - } - let extra = usize::from(class_rate); - Ok([ - 0xFF, - PUNCTURE_STEPS[extra.min(8)], - PUNCTURE_STEPS[extra.saturating_sub(8).min(8)], - PUNCTURE_STEPS[extra.saturating_sub(16).min(8)], - ]) -} - -/// Number of coded bits the SRCPC emits for `n_info` information bits -/// at `class_rate` (0..=24), with or without the four termination -/// tail steps. -pub fn srcpc_coded_len(n_info: usize, class_rate: u8, terminated: bool) -> Result { - let p = puncture_pattern(class_rate)?; - let steps = n_info + if terminated { SRCPC_TAIL_BITS } else { 0 }; - let per_period: usize = p.iter().map(|&b| b.count_ones() as usize).sum(); - let full = steps / 8; - let mut len = full * per_period; - for t in (full * 8)..steps { - for &line in &p { - if line & (0x80 >> (t % 8)) != 0 { - len += 1; - } - } - } - Ok(len) -} - -/// The §1.8.4.6.1 encoder state `(m1, m2, m3, m4)` packed as bits -/// 0..=3 of a nibble. -#[inline] -fn step(state: u8, u: bool) -> (u8, [bool; 4]) { - let m1 = state & 1 != 0; - let m2 = state & 2 != 0; - let m3 = state & 4 != 0; - let m4 = state & 8 != 0; - let d = m4 ^ m2 ^ m1; - let v = [u, m3 ^ m2 ^ m1 ^ u, m3 ^ m1 ^ u, m3 ^ m2 ^ u]; - let next = (u8::from(u ^ d)) | (state << 1) & 0b1110; - (next, v) -} - -/// Feedback bit `d` for a state (drives the §1.8.4.6.2 tail inputs). -#[inline] -fn feedback(state: u8) -> bool { - let m1 = state & 1 != 0; - let m2 = state & 2 != 0; - let m4 = state & 8 != 0; - m4 ^ m2 ^ m1 -} - -/// SRCPC-encode `info` at `class_rate` (0..=24 ⇒ 8/8..8/32), -/// optionally terminated. The encoder always starts from the all-zero -/// state (§1.8.4.6.1). -pub fn srcpc_encode(info: &[bool], class_rate: u8, terminated: bool) -> Result> { - let p = puncture_pattern(class_rate)?; - let mut out = Vec::with_capacity(srcpc_coded_len(info.len(), class_rate, terminated)?); - let mut state = 0u8; - let mut t = 0usize; - let emit = |state: &mut u8, u: bool, t: usize, out: &mut Vec| { - let (next, v) = step(*state, u); - *state = next; - for (i, &line) in p.iter().enumerate() { - if line & (0x80 >> (t % 8)) != 0 { - out.push(v[i]); - } - } - }; - for &u in info { - emit(&mut state, u, t, &mut out); - t += 1; - } - if terminated { - for _ in 0..SRCPC_TAIL_BITS { - let u = feedback(state); - emit(&mut state, u, t, &mut out); - t += 1; - } - debug_assert_eq!(state, 0, "termination must return to state 0"); - } - Ok(out) -} - -/// Hard-decision Viterbi decode of an SRCPC stream (§1.8.4.6.4): -/// recovers `n_info` information bits from `coded`, correcting -/// transmission errors up to the punctured code's capability. -/// -/// `coded.len()` must equal -/// [`srcpc_coded_len`]`(n_info, class_rate, terminated)`. -pub fn srcpc_decode( - coded: &[bool], - n_info: usize, - class_rate: u8, - terminated: bool, -) -> Result> { - let p = puncture_pattern(class_rate)?; - let steps = n_info + if terminated { SRCPC_TAIL_BITS } else { 0 }; - if coded.len() != srcpc_coded_len(n_info, class_rate, terminated)? { - return Err(Error::EpFrameInvalid); - } - - const INF: u32 = u32::MAX / 2; - let mut metric = [INF; 16]; - metric[0] = 0; - // survivors[t][s] = (previous state, input bit) — tail steps have - // a forced input, still recorded uniformly. - let mut survivors: Vec<[(u8, bool); 16]> = Vec::with_capacity(steps); - - let mut pos = 0usize; - for t in 0..steps { - // The emitted lines at this step. - let mut lines: [bool; 4] = [false; 4]; - let mut n_lines = 0usize; - for (i, &line) in p.iter().enumerate() { - lines[i] = line & (0x80 >> (t % 8)) != 0; - if lines[i] { - n_lines += 1; - } - } - let received = &coded[pos..pos + n_lines]; - pos += n_lines; - - let mut next_metric = [INF; 16]; - let mut surv = [(0u8, false); 16]; - for s in 0u8..16 { - if metric[usize::from(s)] >= INF { - continue; - } - let inputs: &[bool] = if t >= n_info { - // Termination steps: the input is forced to d(state). - if feedback(s) { - &[true] - } else { - &[false] - } - } else { - &[false, true] - }; - for &u in inputs { - let (next, v) = step(s, u); - let mut m = metric[usize::from(s)]; - let mut ri = 0usize; - for (i, &on) in lines.iter().enumerate() { - if on { - if v[i] != received[ri] { - m += 1; - } - ri += 1; - } - } - let slot = usize::from(next); - if m < next_metric[slot] { - next_metric[slot] = m; - surv[slot] = (s, u); - } - } - } - metric = next_metric; - survivors.push(surv); - } - - // Terminated streams end in state 0; otherwise take the best. - let mut state: u8 = if terminated { - if metric[0] >= INF { - return Err(Error::EpFrameInvalid); - } - 0 - } else { - let (best, m) = metric - .iter() - .enumerate() - .min_by_key(|(_, &m)| m) - .map(|(s, &m)| (s as u8, m)) - .unwrap_or((0, INF)); - if m >= INF { - return Err(Error::EpFrameInvalid); - } - best - }; - - let mut bits = vec![false; steps]; - for t in (0..steps).rev() { - let (prev, u) = survivors[t][usize::from(state)]; - bits[t] = u; - state = prev; - } - bits.truncate(n_info); - Ok(bits) -} - -/// One §1.8.4.3 basic block code, selected by the protected length. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HeaderFec { - /// 1–2 bits: majority (each bit repeated 3 times). - Majority, - /// 3–4 bits: BCH(7,4), g = x³ + x + 1. - Bch7, - /// 5–7 bits: BCH(15,7), g = x⁸ + x⁷ + x⁶ + x⁴ + 1. - Bch15, - /// 8–12 bits: Golay(23,12), g = x¹¹ + x⁹ + x⁷ + x⁶ + x⁵ + x + 1. - Golay23, - /// 13–16 bits: BCH(31,16), - /// g = x¹⁵ + x¹¹ + x¹⁰ + x⁹ + x⁸ + x⁷ + x⁵ + x³ + x² + x + 1. - Bch31, - /// 17+ bits: CRC4 + terminated SRCPC 8/16. - Srcpc16, -} - -impl HeaderFec { - /// The Table 1.59 length-driven selection. - pub fn for_len(l: usize) -> Result { - Ok(match l { - 0 => return Err(Error::EpFrameInvalid), - 1..=2 => HeaderFec::Majority, - 3..=4 => HeaderFec::Bch7, - 5..=7 => HeaderFec::Bch15, - 8..=12 => HeaderFec::Golay23, - 13..=16 => HeaderFec::Bch31, - _ => HeaderFec::Srcpc16, - }) - } - - /// `(generator polynomial bits above x⁰ .. as u32 with implicit - /// leading term INCLUDED, parity bit count, correction capability)` - /// for the polynomial codes. - fn poly(self) -> Option<(u32, usize, usize)> { - match self { - // x³+x+1 → 0b1011, 3 parity bits, t = 1. - HeaderFec::Bch7 => Some((0b1011, 3, 1)), - // x⁸+x⁷+x⁶+x⁴+1 → 1_1101_0001, 8 parity bits, t = 2. - HeaderFec::Bch15 => Some((0b1_1101_0001, 8, 2)), - // x¹¹+x⁹+x⁷+x⁶+x⁵+x+1 → 1010_1110_0011, 11 parity, t = 3. - HeaderFec::Golay23 => Some((0b1010_1110_0011, 11, 3)), - // x¹⁵+x¹¹+x¹⁰+x⁹+x⁸+x⁷+x⁵+x³+x²+x+1, 15 parity, t = 3. - HeaderFec::Bch31 => Some((0b1000_1111_1010_1111, 15, 3)), - _ => None, - } - } - - /// Number of parity bits appended for `l` protected bits. - pub fn parity_bits(self, l: usize) -> Result { - Ok(match self { - HeaderFec::Majority => 2 * l, - HeaderFec::Srcpc16 => { - // CRC4 + terminated SRCPC 8/16 over (l + 4) info bits; - // parity = coded − l. - srcpc_coded_len(l + 4, 8, true)? - l - } - other => other.poly().map(|(_, p, _)| p).unwrap_or(0), - }) - } -} - -/// Polynomial-division parity `R(x)` of `M(x)·x^deg(G) mod G(x)`, -/// MSB-first over `info` (§1.8.4.3). -fn poly_parity(info: &[bool], gen: u32, parity: usize) -> Vec { - let top = 1u32 << parity; // the implicit leading term position - let mut reg: u32 = 0; - for &bit in info { - reg = (reg << 1) | u32::from(bit); - if reg & top != 0 { - reg ^= gen; - } - } - for _ in 0..parity { - reg <<= 1; - if reg & top != 0 { - reg ^= gen; - } - } - (0..parity) - .map(|i| reg & (1 << (parity - 1 - i)) != 0) - .collect() -} - -/// Encode a §1.8.4.3 header part: returns the parity bit sequence to -/// transmit after the `l` information bits (`Npred_parity` / -/// `Nattrib_parity`). -pub fn header_fec_encode(info: &[bool]) -> Result> { - let fec = HeaderFec::for_len(info.len())?; - Ok(match fec { - HeaderFec::Majority => { - let mut v = Vec::with_capacity(info.len() * 2); - v.extend_from_slice(info); - v.extend_from_slice(info); - v - } - HeaderFec::Srcpc16 => { - // CRC4 over the info, then terminated SRCPC 8/16 over - // info + CRC; the parity is everything past the - // systematic prefix of the coded stream... the coded - // stream is emitted interleaved per time step, so the - // whole codeword replaces info + parity: return the full - // codeword minus the leading l systematic copies is not - // separable. Instead the parity field carries the coded - // stream's non-systematic remainder: we transmit the - // complete coded stream in place of info+parity, so the - // parity here is the coded stream with the systematic - // prefix removed positionally. See `header_fec_decode`, - // which reassembles the same layout. - let crc = crc_bits(CrcPoly::Crc4, info); - let mut m: Vec = info.to_vec(); - for i in (0..4).rev() { - m.push(crc & (1 << i) != 0); - } - let coded = srcpc_encode(&m, 8, true)?; - // Systematic v1 bits occupy known positions; the parity - // field is the stream with those positions removed — the - // decoder re-merges them. - let mut parity = Vec::with_capacity(coded.len() - info.len()); - for (idx, chunk) in coded.chunks(2).enumerate() { - // rate 8/16 keeps v1 and v2 every step. - if idx < info.len() { - // chunk[0] is systematic (v1) — drop, it equals - // info[idx]. - parity.push(chunk[1]); - } else { - parity.push(chunk[0]); - parity.push(chunk[1]); - } - } - parity - } - other => { - let (gen, p, _) = other.poly().ok_or(Error::EpFrameInvalid)?; - poly_parity(info, gen, p) - } - }) -} - -/// Decode a §1.8.4.3 header part: `info` are the received (possibly -/// corrupted) information bits, `parity` the received parity bits. -/// Returns the corrected information bits; uncorrectable words -/// surface [`Error::EpFrameInvalid`]. -pub fn header_fec_decode(info: &[bool], parity: &[bool]) -> Result> { - let l = info.len(); - let fec = HeaderFec::for_len(l)?; - if parity.len() != fec.parity_bits(l)? { - return Err(Error::EpFrameInvalid); - } - match fec { - HeaderFec::Majority => { - let mut out = Vec::with_capacity(l); - for i in 0..l { - let votes = u8::from(info[i]) + u8::from(parity[i]) + u8::from(parity[l + i]); - out.push(votes >= 2); - } - Ok(out) - } - HeaderFec::Srcpc16 => { - // Re-merge the coded stream: v1 comes from `info` for the - // first l steps, both bits from `parity` afterwards. - let mut coded = Vec::with_capacity(l + parity.len()); - let mut pi = 0usize; - for &i_bit in info.iter().take(l) { - coded.push(i_bit); - coded.push(parity[pi]); - pi += 1; - } - coded.extend_from_slice(&parity[pi..]); - let decoded = srcpc_decode(&coded, l + 4, 8, true)?; - let (msg, crc_bits_rx) = decoded.split_at(l); - let want = crc_bits(CrcPoly::Crc4, msg); - let mut got = 0u64; - for &b in crc_bits_rx { - got = (got << 1) | u64::from(b); - } - if got != want { - return Err(Error::EpFrameInvalid); - } - Ok(msg.to_vec()) - } - other => { - let (gen, p, t) = other.poly().ok_or(Error::EpFrameInvalid)?; - let mut word: Vec = Vec::with_capacity(l + p); - word.extend_from_slice(info); - word.extend_from_slice(parity); - if poly_syndrome_ok(&word, gen, p) { - return Ok(info.to_vec()); - } - // Bounded-distance decoding: search error patterns of - // weight <= t over the (shortened) codeword. - let n = word.len(); - let mut positions: Vec = Vec::with_capacity(t); - if search_errors(&mut word, gen, p, t, 0, n, &mut positions) { - return Ok(word[..l].to_vec()); - } - Err(Error::EpFrameInvalid) - } - } -} - -/// `true` iff the codeword (info ‖ parity) has an all-zero syndrome -/// under `gen`. -fn poly_syndrome_ok(word: &[bool], gen: u32, parity: usize) -> bool { - let top = 1u32 << parity; - let mut reg: u32 = 0; - for &bit in word { - reg = (reg << 1) | u32::from(bit); - if reg & top != 0 { - reg ^= gen; - } - } - reg == 0 -} - -/// Recursive bounded-distance search: flip up to `budget` bits from -/// index `from` and test the syndrome. On success the corrected word -/// is left in `word` and `true` is returned. -fn search_errors( - word: &mut [bool], - gen: u32, - parity: usize, - budget: usize, - from: usize, - n: usize, - positions: &mut Vec, -) -> bool { - if budget == 0 { - return false; - } - for i in from..n { - word[i] = !word[i]; - positions.push(i); - if poly_syndrome_ok(word, gen, parity) - || search_errors(word, gen, parity, budget - 1, i + 1, n, positions) - { - return true; - } - positions.pop(); - word[i] = !word[i]; - } - false -} - -#[cfg(test)] -mod tests { - use super::*; - - fn prand_bits(n: usize, mut seed: u32) -> Vec { - let mut v = Vec::with_capacity(n); - for _ in 0..n { - seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); - v.push(seed & 0x8000_0000 != 0); - } - v - } - - /// The Table 1.60 tail-bit listing is the closed form of the - /// `u = d` termination rule. - #[test] - fn termination_matches_table_1_60() { - // Table 1.60 rows: state (m4 m3 m2 m1) -> tail (un-3..un). - let table: [(u8, [u8; 4]); 16] = [ - (0b0000, [0, 0, 0, 0]), - (0b0001, [1, 1, 0, 1]), - (0b0010, [1, 0, 1, 0]), - (0b0011, [0, 1, 1, 1]), - (0b0100, [0, 1, 0, 0]), - (0b0101, [1, 0, 0, 1]), - (0b0110, [1, 1, 1, 0]), - (0b0111, [0, 0, 1, 1]), - (0b1000, [1, 0, 0, 0]), - (0b1001, [0, 1, 0, 1]), - (0b1010, [0, 0, 1, 0]), - (0b1011, [1, 1, 1, 1]), - (0b1100, [1, 1, 0, 0]), - (0b1101, [0, 0, 0, 1]), - (0b1110, [0, 1, 1, 0]), - (0b1111, [1, 0, 1, 1]), - ]; - for (packed, tail) in table { - // Repack (m4 m3 m2 m1) into the module's bit-0 = m1 layout. - let mut state = 0u8; - if packed & 0b0001 != 0 { - state |= 1; // m1 - } - if packed & 0b0010 != 0 { - state |= 2; // m2 - } - if packed & 0b0100 != 0 { - state |= 4; // m3 - } - if packed & 0b1000 != 0 { - state |= 8; // m4 - } - let mut s = state; - for (step_i, &want) in tail.iter().enumerate() { - let u = feedback(s); - assert_eq!(u8::from(u), want, "state {packed:04b} tail step {step_i}"); - let (next, _) = step(s, u); - s = next; - } - assert_eq!(s, 0, "state {packed:04b} did not terminate"); - } - } - - #[test] - fn puncture_patterns_match_table_1_61() { - // Spot rows straight from Table 1.61. - assert_eq!(puncture_pattern(0).unwrap(), [0xFF, 0x00, 0x00, 0x00]); // 8/8 - assert_eq!(puncture_pattern(3).unwrap(), [0xFF, 0xA8, 0x00, 0x00]); // 8/11 - assert_eq!(puncture_pattern(5).unwrap(), [0xFF, 0xEA, 0x00, 0x00]); // 8/13 - assert_eq!(puncture_pattern(8).unwrap(), [0xFF, 0xFF, 0x00, 0x00]); // 8/16 - assert_eq!(puncture_pattern(9).unwrap(), [0xFF, 0xFF, 0x80, 0x00]); // 8/17 - assert_eq!(puncture_pattern(16).unwrap(), [0xFF, 0xFF, 0xFF, 0x00]); // 8/24 - assert_eq!(puncture_pattern(17).unwrap(), [0xFF, 0xFF, 0xFF, 0x80]); // 8/25 - assert_eq!(puncture_pattern(24).unwrap(), [0xFF, 0xFF, 0xFF, 0xFF]); // 8/32 - } - - #[test] - fn srcpc_roundtrip_all_rates() { - for rate in [0u8, 1, 3, 8, 12, 17, 24] { - for terminated in [false, true] { - let info = prand_bits(97, 0xC0FFEE ^ u32::from(rate)); - let coded = srcpc_encode(&info, rate, terminated).unwrap(); - assert_eq!( - coded.len(), - srcpc_coded_len(info.len(), rate, terminated).unwrap() - ); - // Rate 8/8 is purely systematic. - if rate == 0 { - let systematic: Vec = coded - .iter() - .copied() - .take(if terminated { - info.len() + 4 - } else { - info.len() - }) - .collect(); - assert_eq!(&systematic[..info.len()], &info[..]); - } - let decoded = srcpc_decode(&coded, info.len(), rate, terminated).unwrap(); - assert_eq!(decoded, info, "rate {rate} terminated {terminated}"); - } - } - } - - #[test] - fn srcpc_corrects_errors() { - // Rate 8/16 (one parity bit per info bit), terminated: a few - // well-spread bit errors are corrected by the Viterbi pass. - let info = prand_bits(120, 0xDEAD); - let mut coded = srcpc_encode(&info, 8, true).unwrap(); - for &pos in &[10usize, 77, 150, 220] { - coded[pos] = !coded[pos]; - } - let decoded = srcpc_decode(&coded, info.len(), 8, true).unwrap(); - assert_eq!(decoded, info); - } - - #[test] - fn header_fec_roundtrip_all_classes() { - for l in [1usize, 2, 3, 4, 5, 7, 8, 12, 13, 16, 17, 30] { - let info = prand_bits(l, 0xBEEF ^ l as u32); - let parity = header_fec_encode(&info).unwrap(); - assert_eq!( - parity.len(), - HeaderFec::for_len(l).unwrap().parity_bits(l).unwrap(), - "len {l}" - ); - let decoded = header_fec_decode(&info, &parity).unwrap(); - assert_eq!(decoded, info, "len {l}"); - } - } - - #[test] - fn header_fec_corrects_errors() { - // Golay(23,12): three errors are correctable. - let info = prand_bits(12, 0x1234); - let parity = header_fec_encode(&info).unwrap(); - let mut rx_info = info.clone(); - let mut rx_parity = parity.clone(); - rx_info[3] = !rx_info[3]; - rx_info[9] = !rx_info[9]; - rx_parity[5] = !rx_parity[5]; - assert_eq!(header_fec_decode(&rx_info, &rx_parity).unwrap(), info); - - // Majority: one flip per repeated position corrects. - let info = prand_bits(2, 0x9); - let parity = header_fec_encode(&info).unwrap(); - let mut rx_info = info.clone(); - rx_info[0] = !rx_info[0]; - assert_eq!(header_fec_decode(&rx_info, &parity).unwrap(), info); - - // BCH(15,7): two errors. - let info = prand_bits(6, 0x77); - let parity = header_fec_encode(&info).unwrap(); - let mut rx_parity = parity.clone(); - rx_parity[0] = !rx_parity[0]; - rx_parity[6] = !rx_parity[6]; - assert_eq!(header_fec_decode(&info, &rx_parity).unwrap(), info); - } -} diff --git a/crates/vendor/oxideav-aac/src/ep_frame.rs b/crates/vendor/oxideav-aac/src/ep_frame.rs deleted file mode 100644 index dec38662..00000000 --- a/crates/vendor/oxideav-aac/src/ep_frame.rs +++ /dev/null @@ -1,1390 +0,0 @@ -//! `ep_frame()` — ISO/IEC 14496-3 §1.8.2.2 (Tables 1.50–1.53) and the -//! §1.8.4 decoding machinery of the error-protection tool: the -//! FEC-protected in-band header (`choice_of_pred` + `class_attrib()`, -//! §1.8.4.3), per-class CRC (§1.8.4.5) + SRCPC (§1.8.4.6) / shortened -//! Reed-Solomon (§1.8.4.7) protection, the §1.8.4.8 recursive -//! interleaver (modes 0 / 1 / 2) and the §1.8.4.9 class-reordered -//! output. -//! -//! [`EpFrameCodec`] is built from a parsed -//! [`ErrorProtectionSpecificConfig`]; [`EpFrameCodec::encode`] turns a -//! class-partitioned access unit into one error-protected `ep_frame()` -//! and [`EpFrameCodec::decode`] inverts it, verifying every CRC and -//! correcting transmission errors through the FEC layers. The -//! concatenation of the decoded classes is the `epConfig == 0` payload -//! (§1.8.1); §1.8.4.9 output reordering is applied on the decode side. -//! -//! Implementation notes on the two spec points the staged text leaves -//! loose (kept conservative; both surface [`Error::EpFrameInvalid`] -//! rather than guessing): -//! -//! * an escaped (`rate_escape == 1`) rate on an RS class has no -//! in-band code table (Table 1.55 is the SRCPC puncture table), so -//! it is rejected; -//! * the byte-wise recursive interleaving of an RS class (§1.8.4.8.2) -//! is supported when the accumulated `Y` stream is a whole number of -//! octets (the matrix then works in byte cells exactly as Figure -//! 1.18 draws it); a non-aligned `Y` is rejected. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::crc::crc_bits; -use crate::ep_config::{EpClass, EpPredefinedSet, ErrorProtectionSpecificConfig}; -use crate::ep_fec::{ - header_fec_decode, header_fec_encode, srcpc_coded_len, srcpc_decode, srcpc_encode, -}; -use crate::ep_rs::{srs_decode, srs_encode}; -use crate::{Error, Result}; - -/// Table 1.55 — the 3-bit in-band `class_code_rate` codes mapped onto -/// the out-of-band `class_rate` scale (0..=24). -pub const INBAND_RATE_TO_CLASS_RATE: [u8; 8] = [0, 3, 4, 6, 8, 12, 16, 24]; - -/// Table 1.56 — the 3-bit in-band `class_crc_count` codes mapped onto -/// CRC bit counts. -pub const INBAND_CRC_BITS: [u32; 8] = [0, 6, 8, 10, 12, 14, 16, 32]; - -/// One frame's worth of class content plus the per-frame escaped -/// parameters. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EpFrameData { - /// Index into the §1.8.4.2 **expanded** pre-defined-set list. - pub choice_of_pred: usize, - /// Per-class information bits, in class-index order (their - /// concatenation is the `epConfig == 0` payload). - pub classes: Vec>, - /// In-band `class_code_rate` values (Table 1.55 codes) for - /// classes with `rate_escape == 1`; `None` on fixed-rate classes. - pub rate_codes: Vec>, - /// In-band `class_crc_count` values (Table 1.56 codes) for - /// classes with `crclen_escape == 1`; `None` on fixed-CRC classes. - pub crc_codes: Vec>, -} - -/// Resolved per-class parameters for one frame. -#[derive(Debug, Clone)] -struct ClassRt { - /// Information length in bits (`None` = "until the end"). - len_bits: Option, - /// Field width of the in-band `class_bit_count` (escaped classes). - len_field_bits: Option, - /// SRCPC `class_rate` (0..=24) or RS correctable-byte count. - rate: u8, - rate_escaped: bool, - /// CRC width in bits. - crc_bits: u32, - crc_escaped: bool, - fec_type: u8, - terminated: bool, - interleave_switch: u8, -} - -/// Codec for one EP-tool configuration. -#[derive(Debug, Clone)] -pub struct EpFrameCodec { - cfg: ErrorProtectionSpecificConfig, - sets: Vec, -} - -impl EpFrameCodec { - /// Build a codec from a parsed configuration (running the - /// §1.8.4.2 expansion once). - pub fn new(cfg: ErrorProtectionSpecificConfig) -> Result { - let sets = cfg.expand()?; - Ok(EpFrameCodec { cfg, sets }) - } - - /// The §1.8.4.2 expanded pre-defined sets (the `choice_of_pred` - /// index space). - pub fn sets(&self) -> &[EpPredefinedSet] { - &self.sets - } - - /// `Npred = ceil(log2(number of expanded sets))` (Table 1.51). - pub fn npred(&self) -> u32 { - let n = self.sets.len(); - if n <= 1 { - 0 - } else { - usize::BITS - (n - 1).leading_zeros() - } - } - - fn resolve_class(&self, c: &EpClass) -> Result { - let (len_bits, len_field_bits) = if c.length_escape { - let w = u32::from(c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?); - if w == 0 { - (None, None) // "until the end" - } else { - (None, Some(w)) - } - } else { - ( - Some(usize::from(c.class_length.ok_or(Error::EpConfigInvalid)?)), - None, - ) - }; - let rate = c.class_rate.unwrap_or(0); - let crc = match c.class_crclen { - Some(code) => EpClass::crclen_bits(code)?, - None => 0, - }; - Ok(ClassRt { - len_bits, - len_field_bits, - rate, - rate_escaped: c.rate_escape, - crc_bits: crc, - crc_escaped: c.crclen_escape, - fec_type: c.fec_type, - terminated: c.termination_switch.unwrap_or(false), - interleave_switch: c.interleave_switch.unwrap_or(0), - }) - } - - /// Coded bit length of one class's `ep_encoded_class` given its - /// resolved parameters and info length. RS chains are handled by - /// the caller (the chained parity rides the last member). - fn coded_len(&self, rt: &ClassRt, info_bits: usize) -> Result { - let with_crc = info_bits + rt.crc_bits as usize; - Ok(match rt.fec_type { - 0 => srcpc_coded_len(with_crc, rt.rate, rt.terminated)?, - 1 | 2 => { - if with_crc % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - let bytes = with_crc / 8; - let two_k = 2 * usize::from(rt.rate); - if two_k == 0 { - with_crc - } else { - if two_k >= 255 { - return Err(Error::EpConfigInvalid); - } - let parts = bytes.div_ceil(255 - two_k); - with_crc + 8 * two_k * parts - } - } - _ => return Err(Error::EpConfigInvalid), - }) - } - - /// Protect one class (CRC + FEC). RS chaining is resolved before - /// this call (the info of a chain arrives concatenated). - fn protect_class(&self, rt: &ClassRt, info: &[bool]) -> Result> { - // §1.8.4.5 CRC first (the crc module applies the normative - // output inversion). - let mut with_crc: Vec = info.to_vec(); - if rt.crc_bits > 0 { - let poly = EpClass::crc_poly(rt.crc_bits)?.ok_or(Error::EpFrameInvalid)?; - let crc = crc_bits(poly, info); - for i in (0..rt.crc_bits).rev() { - with_crc.push(crc & (1u64 << i) != 0); - } - } - match rt.fec_type { - 0 => srcpc_encode(&with_crc, rt.rate, rt.terminated), - 1 | 2 => { - if with_crc.len() % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - let bytes = bits_to_bytes(&with_crc); - let parity = srs_encode(&bytes, usize::from(rt.rate))?; - let mut out = with_crc; - out.extend(bytes_to_bits(&parity)); - Ok(out) - } - _ => Err(Error::EpConfigInvalid), - } - } - - /// Undo [`Self::protect_class`]: FEC-decode (with correction) and - /// verify + strip the CRC. - fn unprotect_class(&self, rt: &ClassRt, coded: &[bool], info_bits: usize) -> Result> { - let with_crc_len = info_bits + rt.crc_bits as usize; - let mut with_crc: Vec = match rt.fec_type { - 0 => srcpc_decode(coded, with_crc_len, rt.rate, rt.terminated)?, - 1 | 2 => { - if with_crc_len % 8 != 0 || coded.len() < with_crc_len { - return Err(Error::EpFrameInvalid); - } - let mut data = bits_to_bytes(&coded[..with_crc_len]); - let parity = bits_to_bytes(&coded[with_crc_len..]); - srs_decode(&mut data, &parity, usize::from(rt.rate))?; - bytes_to_bits(&data) - } - _ => return Err(Error::EpConfigInvalid), - }; - if rt.crc_bits > 0 { - let poly = EpClass::crc_poly(rt.crc_bits)?.ok_or(Error::EpFrameInvalid)?; - let rx_crc = with_crc.split_off(info_bits); - let want = crc_bits(poly, &with_crc); - let mut got = 0u64; - for &b in &rx_crc { - got = (got << 1) | u64::from(b); - } - if got != want { - return Err(Error::EpFrameInvalid); - } - } else { - with_crc.truncate(info_bits); - } - Ok(with_crc) - } - - /// Encode one frame to a byte-aligned `ep_frame()`. - pub fn encode(&self, frame: &EpFrameData) -> Result> { - let set = self - .sets - .get(frame.choice_of_pred) - .ok_or(Error::EpFrameInvalid)?; - let n = set.classes.len(); - if frame.classes.len() != n || frame.rate_codes.len() != n || frame.crc_codes.len() != n { - return Err(Error::EpFrameInvalid); - } - // Resolve runtime parameters (folding the in-band escapes in). - let mut rts = Vec::with_capacity(n); - for j in 0..n { - let mut rt = self.resolve_class(&set.classes[j])?; - if rt.rate_escaped { - if rt.fec_type != 0 { - return Err(Error::EpFrameInvalid); - } - let code = frame.rate_codes[j].ok_or(Error::EpFrameInvalid)?; - rt.rate = *INBAND_RATE_TO_CLASS_RATE - .get(usize::from(code)) - .ok_or(Error::EpFrameInvalid)?; - } else if frame.rate_codes[j].is_some() { - return Err(Error::EpFrameInvalid); - } - if rt.crc_escaped { - let code = frame.crc_codes[j].ok_or(Error::EpFrameInvalid)?; - rt.crc_bits = *INBAND_CRC_BITS - .get(usize::from(code)) - .ok_or(Error::EpFrameInvalid)?; - } else if frame.crc_codes[j].is_some() { - return Err(Error::EpFrameInvalid); - } - // Fixed-length classes must match the provided content. - if let Some(l) = rt.len_bits { - if frame.classes[j].len() != l { - return Err(Error::EpFrameInvalid); - } - } else if let Some(w) = rt.len_field_bits { - if frame.classes[j].len() >= (1usize << w) { - return Err(Error::EpFrameInvalid); - } - } - rts.push(rt); - } - - // ---- Protect the classes (§1.8.4.4 RS chains resolved by - // concatenating fec_type == 2 members with their successor). - let mut coded: Vec> = vec![Vec::new(); n]; - let mut j = 0usize; - while j < n { - if rts[j].fec_type == 2 { - // Chain: classes j..=last share one RS code. - let mut last = j; - while last < n && rts[last].fec_type == 2 { - last += 1; - } - if last >= n { - return Err(Error::EpFrameInvalid); - } - // §1.8.3.1: all chain members share class_rate. - #[allow(clippy::needless_range_loop)] - for m in j..=last { - if rts[m].rate != rts[last].rate || rts[m].crc_bits % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - } - // Per-class CRCs, then one RS over the concatenation. - let mut chain: Vec = Vec::new(); - let mut member_coded: Vec> = Vec::new(); - #[allow(clippy::needless_range_loop)] - for m in j..=last { - let mut with_crc = frame.classes[m].clone(); - if rts[m].crc_bits > 0 { - let poly = - EpClass::crc_poly(rts[m].crc_bits)?.ok_or(Error::EpFrameInvalid)?; - let crc = crc_bits(poly, &frame.classes[m]); - for i in (0..rts[m].crc_bits).rev() { - with_crc.push(crc & (1u64 << i) != 0); - } - } - if with_crc.len() % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - chain.extend_from_slice(&with_crc); - member_coded.push(with_crc); - } - let parity = srs_encode(&bits_to_bytes(&chain), usize::from(rts[last].rate))?; - // Every member transmits its own CRC-protected bits; - // the parity rides the chain's last member. - for (idx, m) in (j..=last).enumerate() { - coded[m] = member_coded[idx].clone(); - } - coded[last].extend(bytes_to_bits(&parity)); - j = last + 1; - } else { - coded[j] = self.protect_class(&rts[j], &frame.classes[j])?; - j += 1; - } - } - - // ---- In-band header bits. - let npred = self.npred(); - let mut pred_bits: Vec = Vec::new(); - for i in (0..npred).rev() { - pred_bits.push(frame.choice_of_pred & (1usize << i) != 0); - } - let mut attrib_bits: Vec = Vec::new(); - for jj in 0..n { - let k = if set.class_reordered_output { - usize::from(set.class_output_order[jj]) - } else { - jj - }; - if let Some(w) = rts[k].len_field_bits { - let v = frame.classes[k].len(); - for i in (0..w).rev() { - attrib_bits.push(v & (1usize << i) != 0); - } - } - if rts[k].rate_escaped { - let code = frame.rate_codes[k].ok_or(Error::EpFrameInvalid)?; - for i in (0..3).rev() { - attrib_bits.push(code & (1u8 << i) != 0); - } - } - if rts[k].crc_escaped { - let code = frame.crc_codes[k].ok_or(Error::EpFrameInvalid)?; - for i in (0..3).rev() { - attrib_bits.push(code & (1u8 << i) != 0); - } - } - } - - // The transmitted class order (Table 1.53). - let tx_order: Vec = (0..n) - .map(|jj| { - if set.class_reordered_output { - usize::from(set.class_output_order[jj]) - } else { - jj - } - }) - .collect(); - - match self.cfg.interleave_type { - 0 => self.assemble_mode0(&pred_bits, &attrib_bits, &tx_order, &coded, frame), - 1 | 2 => { - self.assemble_interleaved(&pred_bits, &attrib_bits, &tx_order, &rts, &coded, frame) - } - _ => Err(Error::EpConfigInvalid), - } - } - - /// interleave_type == 0: `ep_header()`, `ep_encoded_classes()`, - /// `stuffing_bits` (Table 1.50). - fn assemble_mode0( - &self, - pred_bits: &[bool], - attrib_bits: &[bool], - tx_order: &[usize], - coded: &[Vec], - frame: &EpFrameData, - ) -> Result> { - let mut bits: Vec = Vec::new(); - bits.extend_from_slice(pred_bits); - if !pred_bits.is_empty() { - bits.extend(self.header_parity(pred_bits)?); - } - // class_attrib() + num_stuffing_bits — the stuffing count - // depends on the total length, which the attrib field itself - // is part of; everything except the 3-bit count is fixed, so - // the count solves directly. - let mut fixed = bits.len() + attrib_bits.len(); - if self.cfg.bit_stuffing == 1 { - fixed += 3; - } - let attrib_parity_len = if attrib_bits.is_empty() && self.cfg.bit_stuffing != 1 { - 0 - } else { - // parity spans class_attrib() incl. num_stuffing_bits. - let l = attrib_bits.len() + if self.cfg.bit_stuffing == 1 { 3 } else { 0 }; - crate::ep_fec::HeaderFec::for_len(l)?.parity_bits(l)? - }; - fixed += attrib_parity_len; - let classes_len: usize = coded.iter().map(Vec::len).sum(); - let total_no_stuff = fixed + classes_len; - let nstuff = if self.cfg.bit_stuffing == 1 { - (8 - (total_no_stuff % 8)) % 8 - } else { - 0 - }; - let mut attrib_full: Vec = attrib_bits.to_vec(); - if self.cfg.bit_stuffing == 1 { - for i in (0..3).rev() { - attrib_full.push(nstuff & (1usize << i) != 0); - } - } - bits.extend_from_slice(&attrib_full); - if !attrib_full.is_empty() { - bits.extend(self.header_parity(&attrib_full)?); - } - for &k in tx_order { - bits.extend_from_slice(&coded[k]); - } - bits.resize(bits.len() + nstuff, false); - let _ = frame; - if self.cfg.bit_stuffing == 1 && bits.len() % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - Ok(bits_to_bytes_padded(&bits)) - } - - /// interleave_type == 1 / 2: the §1.8.4.8.2 multi-stage assembly. - fn assemble_interleaved( - &self, - pred_bits: &[bool], - attrib_bits: &[bool], - tx_order: &[usize], - rts: &[ClassRt], - coded: &[Vec], - frame: &EpFrameData, - ) -> Result> { - let mode2 = self.cfg.interleave_type == 2; - let n = tx_order.len(); - // Stuffing count: the total bit count is invariant under - // interleaving, so it solves exactly as in mode 0. - let mut fixed = pred_bits.len(); - if !pred_bits.is_empty() { - fixed += self.header_parity_len(pred_bits.len())?; - } - let attrib_l = attrib_bits.len() + if self.cfg.bit_stuffing == 1 { 3 } else { 0 }; - fixed += attrib_l; - if attrib_l > 0 { - fixed += self.header_parity_len(attrib_l)?; - } - let classes_len: usize = coded.iter().map(Vec::len).sum(); - let total_no_stuff = fixed + classes_len; - let nstuff = if self.cfg.bit_stuffing == 1 { - (8 - (total_no_stuff % 8)) % 8 - } else { - 0 - }; - - // ---- Class stage. - let mut buf_y: Vec = Vec::new(); - let mut buf_no: Vec = Vec::new(); - if mode2 { - // Forward pass: switch-3 (concatenate) and switch-0 - // (non-interleaved) classes. - for &k in tx_order.iter().take(n) { - match rts[k].interleave_switch { - 3 => buf_y.extend_from_slice(&coded[k]), - 0 => buf_no.extend_from_slice(&coded[k]), - _ => {} - } - } - } - for jj in (0..n).rev() { - let k = tx_order[jj]; - let sw = if mode2 { rts[k].interleave_switch } else { 1 }; - if mode2 && (sw == 0 || sw == 3) { - continue; - } - // Width selection (Tables 1.63 / 1.64). - let bytewise = rts[k].fec_type != 0; - let w_units = if mode2 && sw == 2 { - if bytewise { - return Err(Error::EpConfigInvalid); - } - 28 - } else if bytewise { - if coded[k].len() % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - coded[k].len() / 8 - } else if mode2 { - coded[k].len() - } else { - // Mode 1 SRCPC: 28 bits. - 28 - }; - buf_y = if bytewise { - if buf_y.len() % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - let x = bits_to_bytes(&coded[k]); - let y = bits_to_bytes(&buf_y); - bytes_to_bits(&interleave_units(&x, &y, w_units)?) - } else { - interleave_units(&coded[k], &buf_y, w_units)? - }; - } - buf_y.extend_from_slice(&buf_no); - buf_y.resize(buf_y.len() + nstuff, false); - - // ---- Header stages: class_attrib (+ its parity) then - // choice_of_pred (+ its parity), width = codeword length (or - // 28 for the SRCPC header case). - let mut attrib_full: Vec = attrib_bits.to_vec(); - if self.cfg.bit_stuffing == 1 { - for i in (0..3).rev() { - attrib_full.push(nstuff & (1usize << i) != 0); - } - } - if !attrib_full.is_empty() { - let mut x = attrib_full.clone(); - x.extend(self.header_parity(&attrib_full)?); - let w = self.header_width(attrib_full.len())?; - buf_y = interleave_units(&x, &buf_y, w)?; - } - if !pred_bits.is_empty() { - let mut x = pred_bits.to_vec(); - x.extend(self.header_parity(pred_bits)?); - let w = self.header_width(pred_bits.len())?; - buf_y = interleave_units(&x, &buf_y, w)?; - } - let _ = frame; - Ok(bits_to_bytes_padded(&buf_y)) - } - - /// Header parity via the Table 1.59 basic set, or the extended - /// §1.8.4.3 protection when configured and the part exceeds 16 - /// bits. - fn header_parity(&self, part: &[bool]) -> Result> { - if self.cfg.header_protection && part.len() > 16 { - let rate = self.cfg.header_rate.ok_or(Error::EpConfigInvalid)?; - let crc = EpClass::crclen_bits(self.cfg.header_crclen.ok_or(Error::EpConfigInvalid)?)?; - let mut with_crc = part.to_vec(); - if crc > 0 { - let poly = EpClass::crc_poly(crc)?.ok_or(Error::EpFrameInvalid)?; - let v = crc_bits(poly, part); - for i in (0..crc).rev() { - with_crc.push(v & (1u64 << i) != 0); - } - } - let coded = srcpc_encode(&with_crc, rate, true)?; - // The parity is the codeword past the systematic prefix - // is interleaved per-step; transmit the whole codeword - // minus the raw part positionally — same convention as - // ep_fec::header_fec_encode's SRCPC branch, generalised: - // here we simply append the full codeword after the part - // is *not* separately transmitted... To keep the wire - // shape "part then parity", the parity carries the coded - // stream with the leading systematic copies of the part - // removed positionally. - let mut parity = Vec::with_capacity(coded.len() - part.len()); - let p = crate::ep_fec::puncture_pattern(rate)?; - let mut pos = 0usize; - let steps = with_crc.len() + crate::ep_fec::SRCPC_TAIL_BITS; - for t in 0..steps { - for (i, &line) in p.iter().enumerate() { - if line & (0x80 >> (t % 8)) != 0 { - let bit = coded[pos]; - pos += 1; - let systematic_of_part = i == 0 && t < part.len(); - if !systematic_of_part { - parity.push(bit); - } - } - } - } - Ok(parity) - } else { - header_fec_encode(part) - } - } - - /// Bit length of [`Self::header_parity`] for an `l`-bit part. - fn header_parity_len(&self, l: usize) -> Result { - if self.cfg.header_protection && l > 16 { - let rate = self.cfg.header_rate.ok_or(Error::EpConfigInvalid)?; - let crc = EpClass::crclen_bits(self.cfg.header_crclen.ok_or(Error::EpConfigInvalid)?)? - as usize; - Ok(srcpc_coded_len(l + crc, rate, true)? - l) - } else { - crate::ep_fec::HeaderFec::for_len(l)?.parity_bits(l) - } - } - - /// Decode a header part protected by [`Self::header_parity`]. - fn header_unprotect(&self, part: &[bool], parity: &[bool]) -> Result> { - if self.cfg.header_protection && part.len() > 16 { - let rate = self.cfg.header_rate.ok_or(Error::EpConfigInvalid)?; - let crc = EpClass::crclen_bits(self.cfg.header_crclen.ok_or(Error::EpConfigInvalid)?)?; - // Re-merge the positional layout of header_parity. - let p = crate::ep_fec::puncture_pattern(rate)?; - let l = part.len(); - let with_crc_len = l + crc as usize; - let steps = with_crc_len + crate::ep_fec::SRCPC_TAIL_BITS; - let mut coded = Vec::with_capacity(l + parity.len()); - let mut pi = 0usize; - let mut ii = 0usize; - for t in 0..steps { - for (i, &line) in p.iter().enumerate() { - if line & (0x80 >> (t % 8)) != 0 { - if i == 0 && t < l { - coded.push(part[ii]); - ii += 1; - } else { - if pi >= parity.len() { - return Err(Error::EpFrameInvalid); - } - coded.push(parity[pi]); - pi += 1; - } - } - } - } - let decoded = srcpc_decode(&coded, with_crc_len, rate, true)?; - let (msg, rx_crc) = decoded.split_at(l); - if crc > 0 { - let poly = EpClass::crc_poly(crc)?.ok_or(Error::EpFrameInvalid)?; - let want = crc_bits(poly, msg); - let mut got = 0u64; - for &b in rx_crc { - got = (got << 1) | u64::from(b); - } - if got != want { - return Err(Error::EpFrameInvalid); - } - } - Ok(msg.to_vec()) - } else { - header_fec_decode(part, parity) - } - } - - /// Interleaver width for a header part (§1.8.4.8.2.1: the block - /// codeword length in bits, or 28 when SRCPC protects it). - fn header_width(&self, l: usize) -> Result { - if (self.cfg.header_protection && l > 16) - || matches!( - crate::ep_fec::HeaderFec::for_len(l)?, - crate::ep_fec::HeaderFec::Srcpc16 - ) - { - Ok(28) - } else { - Ok(l + self.header_parity_len(l)?) - } - } - - /// Decode one byte-aligned `ep_frame()`. - pub fn decode(&self, data: &[u8]) -> Result { - let total_bits = data.len() * 8; - let all_bits: Vec = (0..total_bits) - .map(|i| data[i / 8] & (0x80 >> (i % 8)) != 0) - .collect(); - - match self.cfg.interleave_type { - 0 => self.decode_mode0(&all_bits), - 1 | 2 => self.decode_interleaved(&all_bits), - _ => Err(Error::EpConfigInvalid), - } - } - - /// Read + verify the two header parts from a bit reader position. - fn read_headers(&self, bits: &[bool], pos: &mut usize) -> Result<(usize, usize, Vec)> { - // choice_of_pred (+ parity). - let npred = self.npred() as usize; - let choice = if npred > 0 { - let part = take(bits, pos, npred)?; - let parity = take(bits, pos, self.header_parity_len(npred)?)?; - let corrected = self.header_unprotect(&part, &parity)?; - let mut v = 0usize; - for &b in &corrected { - v = (v << 1) | usize::from(b); - } - v - } else { - 0 - }; - let set = self.sets.get(choice).ok_or(Error::EpFrameInvalid)?; - // class_attrib() length is fixed by the chosen set. - let mut attrib_l = 0usize; - for c in &set.classes { - if c.length_escape { - let w = usize::from(c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?); - attrib_l += w; // 0 for "until the end" - } - if c.rate_escape { - attrib_l += 3; - } - if c.crclen_escape { - attrib_l += 3; - } - } - if self.cfg.bit_stuffing == 1 { - attrib_l += 3; - } - let attrib = if attrib_l > 0 { - let part = take(bits, pos, attrib_l)?; - let parity = take(bits, pos, self.header_parity_len(attrib_l)?)?; - self.header_unprotect(&part, &parity)? - } else { - Vec::new() - }; - Ok((choice, attrib_l, attrib)) - } - - /// Parse the decoded `class_attrib()` bits into per-class in-band - /// values (`Table 1.52` order) + the stuffing count. - #[allow(clippy::type_complexity)] - fn parse_attrib( - &self, - choice: usize, - attrib: &[bool], - ) -> Result<(Vec>, Vec>, Vec>, usize)> { - let set = &self.sets[choice]; - let n = set.classes.len(); - let mut lens: Vec> = vec![None; n]; - let mut rates: Vec> = vec![None; n]; - let mut crcs: Vec> = vec![None; n]; - let mut pos = 0usize; - for jj in 0..n { - let k = if set.class_reordered_output { - usize::from(set.class_output_order[jj]) - } else { - jj - }; - let c = &set.classes[k]; - if c.length_escape { - let w = usize::from(c.number_of_bits_for_length.ok_or(Error::EpConfigInvalid)?); - if w > 0 { - let v = take(attrib, &mut pos, w)?; - let mut acc = 0usize; - for &b in &v { - acc = (acc << 1) | usize::from(b); - } - lens[k] = Some(acc); - } - } - if c.rate_escape { - let v = take(attrib, &mut pos, 3)?; - let mut acc = 0u8; - for &b in &v { - acc = (acc << 1) | u8::from(b); - } - rates[k] = Some(acc); - } - if c.crclen_escape { - let v = take(attrib, &mut pos, 3)?; - let mut acc = 0u8; - for &b in &v { - acc = (acc << 1) | u8::from(b); - } - crcs[k] = Some(acc); - } - } - let nstuff = if self.cfg.bit_stuffing == 1 { - let v = take(attrib, &mut pos, 3)?; - let mut acc = 0usize; - for &b in &v { - acc = (acc << 1) | usize::from(b); - } - acc - } else { - 0 - }; - Ok((lens, rates, crcs, nstuff)) - } - - /// Resolve every class's runtime parameters + coded length; the - /// "until the end" class absorbs the remaining budget. - #[allow(clippy::too_many_arguments)] - fn resolve_frame( - &self, - choice: usize, - lens: &[Option], - rates: &[Option], - crcs: &[Option], - budget_bits: usize, - ) -> Result<(Vec, Vec, Vec)> { - let set = &self.sets[choice]; - let n = set.classes.len(); - let mut rts = Vec::with_capacity(n); - for j in 0..n { - let mut rt = self.resolve_class(&set.classes[j])?; - if rt.rate_escaped { - if rt.fec_type != 0 { - return Err(Error::EpFrameInvalid); - } - let code = rates[j].ok_or(Error::EpFrameInvalid)?; - rt.rate = *INBAND_RATE_TO_CLASS_RATE - .get(usize::from(code)) - .ok_or(Error::EpFrameInvalid)?; - } - if rt.crc_escaped { - let code = crcs[j].ok_or(Error::EpFrameInvalid)?; - rt.crc_bits = *INBAND_CRC_BITS - .get(usize::from(code)) - .ok_or(Error::EpFrameInvalid)?; - } - rts.push(rt); - } - // Info lengths: fixed, in-band, or until-the-end. - let mut info_lens: Vec> = Vec::with_capacity(n); - let mut open: Option = None; - for (j, rt) in rts.iter().enumerate() { - let l = match (rt.len_bits, rt.len_field_bits) { - (Some(l), _) => Some(l), - (None, Some(_)) => Some(lens[j].ok_or(Error::EpFrameInvalid)?), - (None, None) => { - if open.is_some() { - // Only one until-the-end class can exist. - return Err(Error::EpFrameInvalid); - } - open = Some(j); - None - } - }; - info_lens.push(l); - } - // Coded lengths of the closed classes (RS chains share their - // parity; compute chain-aware totals). - let mut coded_lens: Vec = vec![0; n]; - let mut consumed = 0usize; - let mut j = 0usize; - while j < n { - if rts[j].fec_type == 2 { - let mut last = j; - while last < n && rts[last].fec_type == 2 { - last += 1; - } - if last >= n { - return Err(Error::EpFrameInvalid); - } - if (j..=last).any(|m| info_lens[m].is_none()) { - // An until-the-end class inside an RS chain is - // not resolvable. - return Err(Error::EpFrameInvalid); - } - let mut chain_bits = 0usize; - for m in j..=last { - let with_crc = info_lens[m].unwrap_or(0) + rts[m].crc_bits as usize; - if with_crc % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - coded_lens[m] = with_crc; - chain_bits += with_crc; - } - let two_k = 2 * usize::from(rts[last].rate); - if two_k > 0 { - if two_k >= 255 { - return Err(Error::EpConfigInvalid); - } - let parts = (chain_bits / 8).div_ceil(255 - two_k); - coded_lens[last] += 8 * two_k * parts; - } - for &cl in coded_lens.iter().take(last + 1).skip(j) { - consumed += cl; - } - j = last + 1; - } else { - if let Some(l) = info_lens[j] { - coded_lens[j] = self.coded_len(&rts[j], l)?; - consumed += coded_lens[j]; - } - j += 1; - } - } - if let Some(open_j) = open { - let remaining = budget_bits - .checked_sub(consumed) - .ok_or(Error::EpFrameInvalid)?; - // Search the info length whose coded length fills the - // remainder exactly (§1.8.4.1: the boundary is known from - // the access-unit length). - let rt = &rts[open_j]; - let mut found = None; - // The coded length grows monotonically with the info - // length; scan candidates. - let max_info = remaining; - let mut lo = 0usize; - let mut hi = max_info; - while lo <= hi { - let mid = (lo + hi) / 2; - let cl = self.coded_len(rt, mid); - match cl { - Ok(cl) => match cl.cmp(&remaining) { - core::cmp::Ordering::Equal => { - found = Some(mid); - break; - } - core::cmp::Ordering::Less => lo = mid + 1, - core::cmp::Ordering::Greater => { - if mid == 0 { - break; - } - hi = mid - 1; - } - }, - Err(_) => { - // RS byte alignment: step to the next octet. - lo = mid + 1; - } - } - } - // The binary search can miss non-monotone byte-alignment - // gaps for RS classes; fall back to a linear scan near - // the boundary. - if found.is_none() { - for cand in 0..=max_info { - if let Ok(cl) = self.coded_len(rt, cand) { - if cl == remaining { - found = Some(cand); - break; - } - } - if cand > 4096 && rt.fec_type == 0 { - break; - } - } - } - let info = found.ok_or(Error::EpFrameInvalid)?; - info_lens[open_j] = Some(info); - coded_lens[open_j] = remaining; - } else if consumed != budget_bits { - return Err(Error::EpFrameInvalid); - } - let infos: Vec = info_lens.into_iter().map(|l| l.unwrap_or(0)).collect(); - Ok((rts, infos, coded_lens)) - } - - fn decode_mode0(&self, bits: &[bool]) -> Result { - let mut pos = 0usize; - let (choice, _attrib_l, attrib) = self.read_headers(bits, &mut pos)?; - let (lens, rates, crcs, nstuff) = self.parse_attrib(choice, &attrib)?; - let budget = bits - .len() - .checked_sub(pos + nstuff) - .ok_or(Error::EpFrameInvalid)?; - // Without bit stuffing the byte carrier can hold up to 7 - // slack bits that are not part of the frame; with stuffing the - // budget is exact. Try the exact budget first, then shrink. - let mut last_err = Error::EpFrameInvalid; - let slack_range = if self.cfg.bit_stuffing == 1 { 0 } else { 7 }; - for slack in 0..=slack_range { - let Some(b) = budget.checked_sub(slack) else { - break; - }; - match self.try_decode_classes(choice, &lens, &rates, &crcs, bits, pos, b) { - Ok(mut frame) => { - frame.choice_of_pred = choice; - return Ok(frame); - } - Err(e) => last_err = e, - } - } - Err(last_err) - } - - #[allow(clippy::too_many_arguments)] - fn try_decode_classes( - &self, - choice: usize, - lens: &[Option], - rates: &[Option], - crcs: &[Option], - bits: &[bool], - mut pos: usize, - budget: usize, - ) -> Result { - let set = &self.sets[choice]; - let n = set.classes.len(); - let (rts, infos, coded_lens) = self.resolve_frame(choice, lens, rates, crcs, budget)?; - // Slice the transmitted classes. - let mut coded: Vec> = vec![Vec::new(); n]; - for jj in 0..n { - let k = if set.class_reordered_output { - usize::from(set.class_output_order[jj]) - } else { - jj - }; - coded[k] = take(bits, &mut pos, coded_lens[k])?; - } - self.unprotect_all(&rts, &infos, coded, rates, crcs, choice) - } - - /// FEC/CRC-decode all classes (chain-aware) and assemble the - /// frame data. - fn unprotect_all( - &self, - rts: &[ClassRt], - infos: &[usize], - coded: Vec>, - rates: &[Option], - crcs: &[Option], - choice: usize, - ) -> Result { - let n = rts.len(); - let mut classes: Vec> = vec![Vec::new(); n]; - let mut j = 0usize; - while j < n { - if rts[j].fec_type == 2 { - let mut last = j; - while last < n && rts[last].fec_type == 2 { - last += 1; - } - if last >= n { - return Err(Error::EpFrameInvalid); - } - // Reassemble the chain: members' CRC-protected bits + - // the parity on the last member. - let mut chain: Vec = Vec::new(); - for (m, c) in coded.iter().enumerate().take(last + 1).skip(j) { - let with_crc = infos[m] + rts[m].crc_bits as usize; - if c.len() < with_crc { - return Err(Error::EpFrameInvalid); - } - chain.extend_from_slice(&c[..with_crc]); - } - let parity_bits = &coded[last][infos[last] + rts[last].crc_bits as usize..]; - let mut data = bits_to_bytes(&chain); - let parity = bits_to_bytes(parity_bits); - srs_decode(&mut data, &parity, usize::from(rts[last].rate))?; - let chain_bits = bytes_to_bits(&data); - let mut off = 0usize; - for m in j..=last { - let with_crc = infos[m] + rts[m].crc_bits as usize; - let seg = &chain_bits[off..off + with_crc]; - off += with_crc; - let mut info = seg[..infos[m]].to_vec(); - if rts[m].crc_bits > 0 { - let poly = - EpClass::crc_poly(rts[m].crc_bits)?.ok_or(Error::EpFrameInvalid)?; - let want = crc_bits(poly, &info); - let mut got = 0u64; - for &b in &seg[infos[m]..] { - got = (got << 1) | u64::from(b); - } - if got != want { - return Err(Error::EpFrameInvalid); - } - } - core::mem::swap(&mut classes[m], &mut info); - } - j = last + 1; - } else { - classes[j] = self.unprotect_class(&rts[j], &coded[j], infos[j])?; - j += 1; - } - } - Ok(EpFrameData { - choice_of_pred: choice, - classes, - rate_codes: rates.to_vec(), - crc_codes: crcs.to_vec(), - }) - } - - fn decode_interleaved(&self, bits: &[bool]) -> Result { - let mode2 = self.cfg.interleave_type == 2; - // Reverse the header stages: choice_of_pred first. - let npred = self.npred() as usize; - let mut stream: Vec = bits.to_vec(); - let choice = if npred > 0 { - let xl = npred + self.header_parity_len(npred)?; - let w = self.header_width(npred)?; - let (x, y) = deinterleave_units_bits(&stream, xl, w)?; - stream = y; - let corrected = self.header_unprotect(&x[..npred], &x[npred..])?; - let mut v = 0usize; - for &b in &corrected { - v = (v << 1) | usize::from(b); - } - v - } else { - 0 - }; - let set = self.sets.get(choice).ok_or(Error::EpFrameInvalid)?; - let mut attrib_l = 0usize; - for c in &set.classes { - if c.length_escape { - attrib_l += usize::from(c.number_of_bits_for_length.unwrap_or(0)); - } - if c.rate_escape { - attrib_l += 3; - } - if c.crclen_escape { - attrib_l += 3; - } - } - if self.cfg.bit_stuffing == 1 { - attrib_l += 3; - } - let attrib = if attrib_l > 0 { - let xl = attrib_l + self.header_parity_len(attrib_l)?; - let w = self.header_width(attrib_l)?; - let (x, y) = deinterleave_units_bits(&stream, xl, w)?; - stream = y; - self.header_unprotect(&x[..attrib_l], &x[attrib_l..])? - } else { - Vec::new() - }; - let (lens, rates, crcs, nstuff) = self.parse_attrib(choice, &attrib)?; - - // The class stream: everything minus trailing slack/stuffing. - let mut last_err = Error::EpFrameInvalid; - let slack_range = if self.cfg.bit_stuffing == 1 { 0 } else { 7 }; - for slack in 0..=slack_range { - let Some(budget) = stream.len().checked_sub(nstuff + slack) else { - break; - }; - match self.try_decode_interleaved_classes( - choice, - &lens, - &rates, - &crcs, - &stream[..budget], - mode2, - ) { - Ok(frame) => return Ok(frame), - Err(e) => last_err = e, - } - } - Err(last_err) - } - - fn try_decode_interleaved_classes( - &self, - choice: usize, - lens: &[Option], - rates: &[Option], - crcs: &[Option], - class_stream: &[bool], - mode2: bool, - ) -> Result { - let set = &self.sets[choice]; - let n = set.classes.len(); - let (rts, infos, coded_lens) = - self.resolve_frame(choice, lens, rates, crcs, class_stream.len())?; - let tx_order: Vec = (0..n) - .map(|jj| { - if set.class_reordered_output { - usize::from(set.class_output_order[jj]) - } else { - jj - } - }) - .collect(); - // Undo the class-stage interleaving: the encoder ran the - // reverse loop last-to-first, so decode unwinds first-to-last. - // In mode 2 the non-interleaved (switch-0) classes were - // appended AFTER the interleave stages — split them off the - // tail before unwinding. - let mut buf_no_len = 0usize; - if mode2 { - for (k, rt) in rts.iter().enumerate() { - if rt.interleave_switch == 0 { - buf_no_len += coded_lens[k]; - } - } - } - if buf_no_len > class_stream.len() { - return Err(Error::EpFrameInvalid); - } - let (inter_part, buf_no) = class_stream.split_at(class_stream.len() - buf_no_len); - let mut stream = inter_part.to_vec(); - let mut coded: Vec> = vec![Vec::new(); n]; - // Interleaved classes, in the encoder's reverse-of-reverse - // order (i.e. transmitted forward order). - for &k in tx_order.iter().take(n) { - let sw = if mode2 { rts[k].interleave_switch } else { 1 }; - if mode2 && (sw == 0 || sw == 3) { - continue; - } - let bytewise = rts[k].fec_type != 0; - let w_units = if mode2 && sw == 2 { - 28 - } else if bytewise { - if coded_lens[k] % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - coded_lens[k] / 8 - } else if mode2 { - coded_lens[k] - } else { - 28 - }; - if bytewise { - if stream.len() % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - let z = bits_to_bytes(&stream); - let (x, y) = deinterleave_units(&z, coded_lens[k] / 8, w_units)?; - coded[k] = bytes_to_bits(&x); - stream = bytes_to_bits(&y); - } else { - let (x, y) = deinterleave_units_bits(&stream, coded_lens[k], w_units)?; - coded[k] = x; - stream = y; - } - } - if mode2 { - // The remaining stream is the innermost BUF_Y: the - // switch-3 concatenated classes in forward order. - let mut pos = 0usize; - for &k in tx_order.iter().take(n) { - if rts[k].interleave_switch == 3 { - coded[k] = take(&stream, &mut pos, coded_lens[k])?; - } - } - if pos != stream.len() { - return Err(Error::EpFrameInvalid); - } - // The switch-0 classes ride the tail suffix. - let mut pos = 0usize; - for &k in tx_order.iter().take(n) { - if rts[k].interleave_switch == 0 { - coded[k] = take(buf_no, &mut pos, coded_lens[k])?; - } - } - if pos != buf_no.len() { - return Err(Error::EpFrameInvalid); - } - } else if !stream.is_empty() { - return Err(Error::EpFrameInvalid); - } - self.unprotect_all(&rts, &infos, coded, rates, crcs, choice) - } -} - -/// §1.8.4.8.1 recursive interleaver over generic units: X row-major, -/// Y filling the residual cells column-wise, output read column-major -/// with `k = m·D + min(m, d) + n`. -fn interleave_units(x: &[T], y: &[T], w: usize) -> Result> { - if w == 0 { - return Err(Error::EpFrameInvalid); - } - let total = x.len() + y.len(); - let d_rows = total / w; - let d = total - d_rows * w; - let col_height = |m: usize| d_rows + usize::from(m < d); - let k_of = |m: usize, n: usize| m * d_rows + m.min(d) + n; - - let dp = x.len() / w; - let dpr = x.len() - dp * w; - - let mut out = vec![T::default(); total]; - // X: row-major. - for (i, &v) in x.iter().enumerate() { - let m = i % w; - let n = i / w; - out[k_of(m, n)] = v; - } - // Y: column-wise into the residual cells. - let mut yi = 0usize; - for m in 0..w { - let start = dp + usize::from(m < dpr); - for n in start..col_height(m) { - if yi >= y.len() { - return Err(Error::EpFrameInvalid); - } - out[k_of(m, n)] = y[yi]; - yi += 1; - } - } - if yi != y.len() { - return Err(Error::EpFrameInvalid); - } - Ok(out) -} - -/// Inverse of [`interleave_units`] given `lx` and the width. -fn deinterleave_units(z: &[T], lx: usize, w: usize) -> Result<(Vec, Vec)> { - if w == 0 || lx > z.len() { - return Err(Error::EpFrameInvalid); - } - let total = z.len(); - let d_rows = total / w; - let d = total - d_rows * w; - let col_height = |m: usize| d_rows + usize::from(m < d); - let k_of = |m: usize, n: usize| m * d_rows + m.min(d) + n; - let dp = lx / w; - let dpr = lx - dp * w; - let mut x = vec![T::default(); lx]; - for (i, xv) in x.iter_mut().enumerate() { - let m = i % w; - let n = i / w; - *xv = z[k_of(m, n)]; - } - let mut y = Vec::with_capacity(total - lx); - for m in 0..w { - let start = dp + usize::from(m < dpr); - for n in start..col_height(m) { - y.push(z[k_of(m, n)]); - } - } - Ok((x, y)) -} - -fn deinterleave_units_bits(z: &[bool], lx: usize, w: usize) -> Result<(Vec, Vec)> { - deinterleave_units(z, lx, w) -} - -fn take(bits: &[bool], pos: &mut usize, n: usize) -> Result> { - if *pos + n > bits.len() { - return Err(Error::EpFrameInvalid); - } - let v = bits[*pos..*pos + n].to_vec(); - *pos += n; - Ok(v) -} - -fn bits_to_bytes(bits: &[bool]) -> Vec { - debug_assert_eq!(bits.len() % 8, 0); - bits.chunks(8) - .map(|c| c.iter().fold(0u8, |acc, &b| (acc << 1) | u8::from(b))) - .collect() -} - -fn bits_to_bytes_padded(bits: &[bool]) -> Vec { - let mut v = Vec::with_capacity(bits.len().div_ceil(8)); - for chunk in bits.chunks(8) { - let mut b = 0u8; - for (i, &bit) in chunk.iter().enumerate() { - if bit { - b |= 0x80 >> i; - } - } - v.push(b); - } - v -} - -fn bytes_to_bits(bytes: &[u8]) -> Vec { - let mut v = Vec::with_capacity(bytes.len() * 8); - for &b in bytes { - for i in 0..8 { - v.push(b & (0x80 >> i) != 0); - } - } - v -} - -/// Emit a parsed frame back through a [`BitWriter`] (whole bytes). -pub fn write_frame(w: &mut BitWriter, frame_bytes: &[u8]) { - for &b in frame_bytes { - w.write_u32(u32::from(b), 8); - } -} - -/// Convenience: read the remaining whole bytes of a reader. -pub fn read_remaining_bytes(reader: &mut BitReader<'_>, total_len: usize) -> Result> { - let pos = reader.bit_position() as usize; - if pos % 8 != 0 { - return Err(Error::EpFrameInvalid); - } - let mut out = Vec::with_capacity(total_len - pos / 8); - for _ in (pos / 8)..total_len { - out.push(reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)? as u8); - } - Ok(out) -} diff --git a/crates/vendor/oxideav-aac/src/ep_rs.rs b/crates/vendor/oxideav-aac/src/ep_rs.rs deleted file mode 100644 index b57e934c..00000000 --- a/crates/vendor/oxideav-aac/src/ep_rs.rs +++ /dev/null @@ -1,434 +0,0 @@ -//! §1.8.4.7 shortened Reed-Solomon codes of the MPEG-4 -//! error-protection tool. -//! -//! `SRS(255−l, 255−2k−l)` over GF(2⁸) built on the primitive -//! polynomial `m(x) = x⁸ + x⁴ + x³ + x² + 1` (the Table 1.62 α-power -//! listing is exactly the antilog table of that polynomial — pinned -//! by tests against printed rows). The generator is -//! `g(x) = (x−α)(x−α²)…(x−α^2k)`; a class longer than `255−2k` octets -//! splits into parts (`l_i = 255−2k` except the zero-padded last), -//! each part's parity is `p(x) = x^2k·u(x) mod g(x)` with the -//! **lowest-order coefficient as the first octet** (§1.8.4.7), and -//! all parities are appended after the class data (Figure 1.11). -//! -//! Decoding runs the standard algebraic chain over the spec's field: -//! syndromes `S_j = r(α^j)`, Berlekamp-Massey for the error locator, -//! Chien search, Forney evaluation — correcting up to `k` byte errors -//! per part; an uncorrectable part surfaces -//! [`Error::EpFrameInvalid`]. - -use crate::{Error, Result}; - -/// GF(2⁸) tables for `m(x) = x⁸ + x⁴ + x³ + x² + 1` (0x11D). -struct Gf { - exp: [u8; 512], - log: [u8; 256], -} - -fn gf() -> &'static Gf { - use std::sync::OnceLock; - static GF: OnceLock = OnceLock::new(); - GF.get_or_init(|| { - let mut exp = [0u8; 512]; - let mut log = [0u8; 256]; - let mut v: u16 = 1; - #[allow(clippy::needless_range_loop)] - for i in 0..255 { - exp[i] = v as u8; - log[v as usize] = i as u8; - v <<= 1; - if v & 0x100 != 0 { - v ^= 0x11D; - } - } - for i in 255..512 { - exp[i] = exp[i - 255]; - } - Gf { exp, log } - }) -} - -#[inline] -fn gf_mul(a: u8, b: u8) -> u8 { - if a == 0 || b == 0 { - return 0; - } - let g = gf(); - g.exp[usize::from(g.log[usize::from(a)]) + usize::from(g.log[usize::from(b)])] -} - -#[inline] -fn gf_inv(a: u8) -> Result { - if a == 0 { - return Err(Error::EpFrameInvalid); - } - let g = gf(); - Ok(g.exp[255 - usize::from(g.log[usize::from(a)])]) -} - -/// α^i (`0 <= i`), the Table 1.62 antilog. -pub fn alpha_pow(i: usize) -> u8 { - gf().exp[i % 255] -} - -/// The §1.8.4.7 generator polynomial `g(x) = ∏_{i=1..2k} (x − α^i)`, -/// lowest-order coefficient first, length `2k + 1` (monic). -fn generator(two_k: usize) -> Vec { - let mut g = vec![0u8; two_k + 1]; - g[0] = 1; - let mut deg = 0usize; - for i in 1..=two_k { - let a = alpha_pow(i); - // g = g * (x + α^i) (− == + in GF(2^8)). - deg += 1; - for j in (1..=deg).rev() { - g[j] = g[j - 1] ^ gf_mul(g[j], a); - } - g[0] = gf_mul(g[0], a); - } - g -} - -/// Parity octets (`2k`, lowest order first) for one part `u` of at -/// most `255 − 2k` octets: `p(x) = x^2k · u(x) mod g(x)` with the -/// first octet of `u` as the lowest-order coefficient (§1.8.4.7). -fn part_parity(part: &[u8], two_k: usize) -> Vec { - let g = generator(two_k); - // Work highest-order-first for the long division: u(x)·x^2k has - // coefficients [0; 2k] ++ part (lowest first). Highest order is - // the LAST octet of `part`. - let mut rem = vec![0u8; two_k]; // remainder, highest order at [0] - for &coeff in part.iter().rev() { - let factor = rem[0] ^ coeff; - // Shift left by one (multiply by x) and subtract factor·g. - for i in 0..two_k { - let next = if i + 1 < two_k { rem[i + 1] } else { 0 }; - rem[i] = next ^ gf_mul(factor, g[two_k - 1 - i]); - } - } - // rem[0] is the highest-order remainder coefficient; the wire - // wants lowest order first. - rem.reverse(); - rem -} - -/// §1.8.4.7 part split of a class of `len` octets under `2k` parity -/// octets per part: every part is `255 − 2k` long except the last -/// (`len mod (255 − 2k)`, zero-padded for the computation). -fn part_lengths(len: usize, two_k: usize) -> Result> { - let cap = 255 - two_k; - if cap == 0 || len == 0 { - return Err(Error::EpFrameInvalid); - } - let n = len.div_ceil(cap); - let mut parts = Vec::with_capacity(n); - for i in 0..n { - if i + 1 < n { - parts.push(cap); - } else { - let last = len - cap * (n - 1); - parts.push(last); - } - } - Ok(parts) -} - -/// SRS-encode a class: returns the parity octets to append after the -/// class data (all parts' parities in part order, Figure 1.11). -/// -/// `k` is the per-codeword correction capability (`class_rate` for -/// `fec_type == 1 / 2`); `k == 0` yields no parity. -pub fn srs_encode(class_data: &[u8], k: usize) -> Result> { - if k == 0 { - return Ok(Vec::new()); - } - let two_k = 2 * k; - if two_k >= 255 { - return Err(Error::EpConfigInvalid); - } - let parts = part_lengths(class_data.len(), two_k)?; - let cap = 255 - two_k; - let mut out = Vec::with_capacity(two_k * parts.len()); - let mut pos = 0usize; - for (i, &plen) in parts.iter().enumerate() { - let mut part = class_data[pos..pos + plen].to_vec(); - pos += plen; - if i + 1 == parts.len() && plen < cap { - // §1.8.4.7: zero-pad the short last part for the - // computation only. - part.resize(cap, 0); - } - out.extend_from_slice(&part_parity(&part, two_k)); - } - Ok(out) -} - -/// SRS-decode a class in place: `class_data` are the received data -/// octets, `parity` the received parity octets ([`srs_encode`] -/// layout). Corrects up to `k` byte errors per part (errors in the -/// parity octets included); an uncorrectable part is -/// [`Error::EpFrameInvalid`]. -pub fn srs_decode(class_data: &mut [u8], parity: &[u8], k: usize) -> Result<()> { - if k == 0 { - return Ok(()); - } - let two_k = 2 * k; - if two_k >= 255 { - return Err(Error::EpConfigInvalid); - } - let parts = part_lengths(class_data.len(), two_k)?; - if parity.len() != two_k * parts.len() { - return Err(Error::EpFrameInvalid); - } - let cap = 255 - two_k; - let mut pos = 0usize; - for (i, &plen) in parts.iter().enumerate() { - // Codeword c(x): parity (lowest orders 0..2k) then data - // (orders 2k..). Build lowest-order-first. - let mut cw = vec![0u8; 255]; - cw[..two_k].copy_from_slice(&parity[i * two_k..(i + 1) * two_k]); - let part = &class_data[pos..pos + plen]; - for (j, &b) in part.iter().enumerate() { - cw[two_k + j] = b; - } - // (zero padding of a short last part occupies the top orders - // implicitly.) - let corrected = rs_correct(&mut cw, k)?; - let _ = corrected; - // Verify the padding stayed zero (errors located there would - // mean a miscorrection for a conforming stream). - for j in plen..cap { - if cw[two_k + j] != 0 { - return Err(Error::EpFrameInvalid); - } - } - class_data[pos..pos + plen].copy_from_slice(&cw[two_k..two_k + plen]); - pos += plen; - } - Ok(()) -} - -/// Correct one 255-octet codeword (lowest-order coefficient first) in -/// place; returns the number of corrected byte errors. -fn rs_correct(cw: &mut [u8], k: usize) -> Result { - let two_k = 2 * k; - // Syndromes S_j = c(α^j), j = 1..=2k. - let mut synd = vec![0u8; two_k]; - let mut any = false; - for (j, s) in synd.iter_mut().enumerate() { - let a = alpha_pow(j + 1); - let mut acc = 0u8; - // Horner from the highest order down. - for &c in cw.iter().rev() { - acc = gf_mul(acc, a) ^ c; - } - *s = acc; - any |= acc != 0; - } - if !any { - return Ok(0); - } - - // Berlekamp-Massey for the error locator Λ(x) (lowest order - // first, Λ(0) = 1). - let mut lambda = vec![0u8; two_k + 1]; - let mut prev = vec![0u8; two_k + 1]; - lambda[0] = 1; - prev[0] = 1; - let mut l = 0usize; - let mut m = 1usize; - let mut b = 1u8; - for n in 0..two_k { - // Discrepancy. - let mut delta = synd[n]; - for i in 1..=l { - delta ^= gf_mul(lambda[i], synd[n - i]); - } - if delta == 0 { - m += 1; - } else if 2 * l <= n { - let t = lambda.clone(); - let coef = gf_mul(delta, gf_inv(b)?); - for i in 0..=two_k { - if i >= m && prev[i - m] != 0 { - lambda[i] ^= gf_mul(coef, prev[i - m]); - } - } - prev = t; - l = n + 1 - l; - b = delta; - m = 1; - } else { - let coef = gf_mul(delta, gf_inv(b)?); - for i in 0..=two_k { - if i >= m && prev[i - m] != 0 { - lambda[i] ^= gf_mul(coef, prev[i - m]); - } - } - m += 1; - } - } - if l > k { - return Err(Error::EpFrameInvalid); - } - - // Chien search: error at position p iff Λ(α^{-p}) == 0. - let mut err_pos = Vec::with_capacity(l); - for p in 0..255usize { - let x = alpha_pow((255 - p) % 255); // α^{-p} - let mut acc = 0u8; - for i in (0..=l).rev() { - acc = gf_mul(acc, x) ^ lambda[i]; - } - if acc == 0 { - err_pos.push(p); - } - } - if err_pos.len() != l { - return Err(Error::EpFrameInvalid); - } - - // Forney: error magnitudes from the evaluator - // Ω(x) = S(x)·Λ(x) mod x^{2k}. - let mut omega = vec![0u8; two_k]; - for i in 0..two_k { - let mut acc = 0u8; - for j in 0..=i.min(l) { - if lambda[j] != 0 && i >= j { - acc ^= gf_mul(lambda[j], synd[i - j]); - } - } - omega[i] = acc; - } - // Λ'(x): formal derivative (odd-power terms). Forney with the - // first syndrome at j = 1: e_p = Ω(X_p⁻¹) / Λ'(X_p⁻¹). - for &p in &err_pos { - let x_inv = alpha_pow((255 - p) % 255); - // Ω(x_inv), Horner highest order down. - let mut om = 0u8; - for i in (0..two_k).rev() { - om = gf_mul(om, x_inv) ^ omega[i]; - } - // Λ'(x_inv) = Σ_{i odd, i <= l} Λ_i · x_inv^{i−1}. - let mut dl = 0u8; - for i in (1..=l).step_by(2) { - dl ^= gf_mul(lambda[i], gf_pow(x_inv, i - 1)); - } - if dl == 0 { - return Err(Error::EpFrameInvalid); - } - let magnitude = gf_mul(om, gf_inv(dl)?); - cw[p] ^= magnitude; - } - - // Re-verify. - for j in 1..=two_k { - let a = alpha_pow(j); - let mut acc = 0u8; - for &c in cw.iter().rev() { - acc = gf_mul(acc, a) ^ c; - } - if acc != 0 { - return Err(Error::EpFrameInvalid); - } - } - Ok(l) -} - -/// `x^i` in GF(2⁸). -fn gf_pow(x: u8, i: usize) -> u8 { - let mut acc = 1u8; - for _ in 0..i { - acc = gf_mul(acc, x); - } - acc -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Spot-check the generated antilog table against printed rows of - /// Table 1.62. - #[test] - fn alpha_table_matches_table_1_62() { - assert_eq!(alpha_pow(0), 0b0000_0001); - assert_eq!(alpha_pow(1), 0b0000_0010); - assert_eq!(alpha_pow(8), 0b0001_1101); - assert_eq!(alpha_pow(63), 0b1010_0001); - assert_eq!(alpha_pow(64), 0b0101_1111); - assert_eq!(alpha_pow(127), 0b1100_1100); - assert_eq!(alpha_pow(128), 0b1000_0101); - assert_eq!(alpha_pow(175), 0b1111_1111); - assert_eq!(alpha_pow(191), 0b0100_0001); - assert_eq!(alpha_pow(254), 0b1000_1110); - } - - fn prand_bytes(n: usize, mut seed: u32) -> Vec { - let mut v = Vec::with_capacity(n); - for _ in 0..n { - seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); - v.push((seed >> 16) as u8); - } - v - } - - #[test] - fn srs_roundtrip_clean() { - for (len, k) in [(10usize, 2usize), (100, 4), (300, 8), (251, 2), (600, 1)] { - let data = prand_bytes(len, 0xA5A5 ^ len as u32); - let parity = srs_encode(&data, k).unwrap(); - let n_parts = len.div_ceil(255 - 2 * k); - assert_eq!(parity.len(), 2 * k * n_parts, "len {len} k {k}"); - let mut rx = data.clone(); - srs_decode(&mut rx, &parity, k).unwrap(); - assert_eq!(rx, data, "len {len} k {k}"); - } - } - - #[test] - fn srs_corrects_byte_errors() { - let data = prand_bytes(120, 0x5EED); - let k = 4; - let parity = srs_encode(&data, k).unwrap(); - // Up to k errors in the data part. - let mut rx = data.clone(); - rx[3] ^= 0x41; - rx[57] ^= 0xFF; - rx[100] ^= 0x01; - rx[119] ^= 0x80; - srs_decode(&mut rx, &parity, k).unwrap(); - assert_eq!(rx, data); - - // Errors in the parity octets are located and ignored for the - // data reconstruction. - let mut rx = data.clone(); - let mut bad_parity = parity.clone(); - bad_parity[0] ^= 0x10; - bad_parity[5] ^= 0x22; - srs_decode(&mut rx, &bad_parity, k).unwrap(); - assert_eq!(rx, data); - - // k + 1 errors are uncorrectable. - let mut rx = data.clone(); - for (i, b) in rx.iter_mut().enumerate().take(k + 1) { - *b ^= 0x11 + i as u8; - } - assert!(srs_decode(&mut rx, &parity, k).is_err()); - } - - #[test] - fn srs_multi_part_correction() { - // 300 octets with k = 8 → parts of 239 + 61; errors in both - // parts correct independently. - let data = prand_bytes(300, 0x77); - let k = 8; - let parity = srs_encode(&data, k).unwrap(); - let mut rx = data.clone(); - for &p in &[0usize, 100, 238, 239, 250, 299] { - rx[p] ^= 0x5A; - } - srs_decode(&mut rx, &parity, k).unwrap(); - assert_eq!(rx, data); - } -} diff --git a/crates/vendor/oxideav-aac/src/error.rs b/crates/vendor/oxideav-aac/src/error.rs deleted file mode 100644 index 4ce425c1..00000000 --- a/crates/vendor/oxideav-aac/src/error.rs +++ /dev/null @@ -1,1244 +0,0 @@ -//! Crate-local error type. - -/// Errors returned by `oxideav-aac` Phase 1 surface. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Error { - /// Decode / encode body is not implemented yet (Phase 1 skeleton). - NotImplemented, - - /// ADTS sync pattern (`syncword = 0xFFF`, 12 bits) not found at the - /// expected position. ISO/IEC 13818-7 §1.A.2.2.1. - AdtsSyncNotFound, - - /// ADTS layer field must be `00` per ISO/IEC 13818-7 §1.A.2.2.1 - /// (the *Layer* field is reserved for MPEG-1/2 layer signalling - /// and is required zero in ADTS). Decoder rejects non-zero. - AdtsLayerNonZero, - - /// Reserved `sampling_frequency_index` value (13 or 14). ISO/IEC - /// 14496-3 Table 1.18 marks indices 13 and 14 as reserved; index - /// 15 signals an explicit 24-bit rate (only present in - /// `AudioSpecificConfig`, never in an ADTS header — the ADTS - /// field is 4 bits so the legal range is 0..=12). - AdtsReservedSampleRateIndex, - - /// ADTS `aac_frame_length` is smaller than the fixed header - /// itself (7 bytes without CRC, 9 bytes with CRC). Such a frame - /// is malformed and cannot wrap any payload. - AdtsFrameLengthTooSmall, - - /// An in-memory [`crate::adts::AdtsHeader`] cannot be - /// serialised: a field exceeds its ADTS wire width or violates - /// a normative constraint (reserved sampling-frequency index, - /// `aac_frame_length` below the header overhead, raw-data-block - /// count outside `1..=4`). - AdtsEncodeInvalid, - - /// [`crate::encoder::StreamEncoder`] configuration is invalid: - /// the sample rate is not a Table 1.18 ADTS rate, the channel - /// count is not 1 or 2, the bitrate is 0, or an input slice - /// exceeds the per-frame hop / is not a whole number of - /// interleaved sample tuples. - EncoderInvalidConfig, - - /// The assembled encoder frame exceeds the 13-bit ADTS - /// `aac_frame_length` ceiling even after the rate loop. - EncoderFrameOverflow, - - /// The bit-reader hit end-of-stream while parsing. - UnexpectedEnd, - - /// Encountered an `id_syn_ele` value the walker cannot advance - /// past in Phase 1. Carries the raw 3-bit value (0..=7) — the - /// caller can map it back to ISO/IEC 14496-3 Table 4.71 names. - /// Phase 1 can step past FIL (`0b110`), DSE (`0b100`), and PCE - /// (`0b101`); the channel elements (SCE/CPE/CCE/LFE) still - /// require body parsing that is deferred. - UnsupportedElementSkip(u8), - - /// `AudioSpecificConfig` carried an `audioObjectType` whose - /// body Phase 1 does not parse. The General Audio AOTs handled - /// by Phase 1 are 1 (Main), 2 (LC), 3 (SSR), 4 (LTP), 6 - /// (scalable), 7 (TwinVQ), 17 (ER AAC LC), 19 (ER AAC LTP), 20 - /// (ER AAC scalable), 21 (ER TwinVQ), 22 (ER BSAC), 23 (ER AAC - /// LD); SBR (5) and PS (29) hierarchical wrappers are - /// unwrapped before this check. Any other AOT — CELP, HVXC, - /// SSC, USAC, ELD, ALS, SLS, … — currently surfaces here. - UnsupportedAot(u8), - - /// [`crate::ics_info::IcsInfo::parse`] was called with a - /// `sampling_frequency_index` outside the standard 0..=11 - /// range covered by the `NUM_SWB_{LONG,SHORT}_WINDOW` tables. - /// The 24-bit explicit-rate escape (`samplingFrequencyIndex - /// == 0xf`) does not select an SWB table directly — the caller - /// must resolve the explicit rate to the nearest standard - /// index before invoking the ics_info parser. - IcsInfoUnsupportedSampleRateIndex(u8), - - /// An `EIGHT_SHORT_SEQUENCE` (or any short-window geometry) was - /// requested for an ER AAC LD frame family. §4.6.17.2.2: the low - /// delay coder has no block switching, so the 512/480-line - /// families define no short-window tables at all — a stream - /// signalling a non-`ONLY_LONG` window sequence under AOT 23 is - /// malformed. - LdShortWindow, - - /// An SBR extension payload arrived on a stream running a - /// non-1024-line §4.5.1.1 frame family. The §4.6.18 SBR tool in - /// this crate covers the 1024-line core (32-subband QMF analysis, - /// 2048-sample dual-rate output); SBR over a 960-line core (and - /// the §4.6.19 LD SBR tool) is out of scope. - SbrUnsupportedFrameFamily, - - /// [`crate::ics_info::IcsInfo::write`] was handed an in-memory - /// [`crate::ics_info::IcsInfo`] whose field combination cannot - /// be represented on the wire under ISO/IEC 14496-3 Table 4.6 / - /// Table 4.55. Examples: `max_sfb` exceeds its field width - /// (`> 15` for `EIGHT_SHORT_SEQUENCE`, `> 63` otherwise); - /// `scale_factor_grouping == None` for `EIGHT_SHORT_SEQUENCE` or - /// `Some(_)` for any other window sequence; a predictor / LTP - /// body slot is populated while the dispatching - /// `predictor_data_present` bit is zero, or vice versa; a - /// non-Main AOT has `predictor_data` set instead of `ltp_data`; - /// the paired-channel `ltp_data_present_pair` slot is populated - /// while `common_window == false`; a `prediction_used[]` / - /// `long_used[]` length differs from the spec-cap - /// (`min(max_sfb, PRED_SFB_MAX[fs_index])` or - /// `min(max_sfb, MAX_LTP_LONG_SFB)`); or a numeric field - /// (`ltp_coef`, `ltp_lag`, `reset_group_number`) exceeds the - /// width of its wire slot. A conforming AAC encoder never builds - /// such a structure; this surfaces caller bugs at the boundary - /// between psychoacoustic / windowing-decision code and bitstream - /// emission. - IcsInfoEncodeInvalid, - - /// [`crate::section_data::SectionData::parse`] read a section - /// run-length (`sect_len`) that would extend a section past - /// `max_sfb`. ISO/IEC 13818-7 §6.3 Table 17 terminates the - /// per-group loop at `k < max_sfb`; a conforming encoder never - /// emits a `sect_len` that overshoots, so this signals a - /// malformed `section_data()`. - SectionDataOverrun, - - /// [`crate::section_data::SectionData::write`] was handed an - /// in-memory [`crate::section_data::SectionData`] whose - /// per-group section list violates an invariant the encoder - /// cannot represent on the wire — non-contiguous bands - /// (`start != 0`, `end[i] != start[i+1]`, or last `end != - /// max_sfb`), a `sect_cb` greater than the 4-bit field, or a - /// zero-length section that the §6.3 escape cannot terminate - /// while preserving parser round-trip. A conforming AAC encoder - /// never builds such a structure; this surfaces caller bugs at - /// the boundary between scalefactor-grouping and section - /// emission. - SectionDataEncodeInvalid, - - /// [`crate::pulse_data::PulseData::write`] was handed an - /// in-memory [`crate::pulse_data::PulseData`] whose field set - /// cannot be represented on the wire under ISO/IEC 14496-3 - /// §4.4.6.3 Table 4.7. Examples: `pulses` is empty (the loop - /// bound is `number_pulse + 1 >= 1`) or exceeds the 2-bit - /// `number_pulse` field cap (`pulses.len() > 4`); - /// `pulse_start_sfb > 0x3f` (6-bit overflow); a `Pulse::offset > - /// 0x1f` (5-bit overflow) or `Pulse::amp > 0x0f` (4-bit - /// overflow). A conforming AAC encoder never builds such a - /// structure; this surfaces caller bugs at the boundary between - /// the pulse-selection psychoacoustic stage and bitstream - /// emission. - PulseDataEncodeInvalid, - - /// [`crate::tns_data::TnsData::write`] was handed an in-memory - /// [`crate::tns_data::TnsData`] whose field combination cannot - /// be represented on the wire under ISO/IEC 14496-3 §4.4.6 / - /// Table 4.54 (with the §4.6.9.2 Table 4.155 size switch). - /// Examples: `windows.len()` differs from `num_windows` for the - /// surrounding `window_sequence` (1 for long sequences, 8 for - /// `EIGHT_SHORT_SEQUENCE`); per-window `filters.len()` exceeds - /// the `n_filt` field cap (1 on `EIGHT_SHORT_SEQUENCE`, 3 - /// otherwise); a filter's `length` exceeds the `length` field - /// cap (15 / 63); a filter's `order` exceeds the `order` field - /// cap (7 / 31); the `coef[]` length differs from `order`; a - /// coefficient magnitude exceeds the `(1 << coef_bits) - 1` - /// cap (where `coef_bits = (3 + coef_res) - coef_compress`); a - /// zero-`order` filter carries a non-default `direction` / - /// `coef_compress` that would silently be dropped on the wire - /// (those fields are not transmitted when `order == 0`). A - /// conforming AAC encoder never builds such a structure; this - /// surfaces caller bugs at the boundary between the TNS - /// psychoacoustic-decision stage and bitstream emission. - TnsDataEncodeInvalid, - - /// [`crate::scale_factor_data::ScaleFactorData::write`] was - /// handed an in-memory record set whose shape cannot be - /// represented on the wire under ISO/IEC 14496-3 §4.4.6 / - /// Table 4.53 (non-resilient branch). Examples: the outer - /// `entries.len()` does not match the supplied `sfb_cb.len()`; - /// a group's entry count differs from the non-`ZERO_HCB` band - /// count of the matching `sfb_cb` group; an entry variant - /// does not match its band's codebook classification - /// (e.g. [`crate::scale_factor_data::ScaleFactorEntry::Intensity`] - /// paired with a spectrum band, or - /// [`crate::scale_factor_data::ScaleFactorEntry::NoisePcm`] re-used - /// after the §4.4.6 frame-scope `noise_pcm_flag` has already - /// cleared, or - /// [`crate::scale_factor_data::ScaleFactorEntry::NoiseDpcm`] used - /// on the first PNS band of the frame); a DPCM delta falls - /// outside `-60..=+60` (Table 4.150); or a `NoisePcm` magnitude - /// exceeds the 9-bit field cap (`> 0x1ff`). A conforming AAC - /// encoder never builds such a structure; this surfaces caller - /// bugs at the boundary between the rate-allocation / - /// scalefactor-quantisation stage and bitstream emission. - ScaleFactorDataEncodeInvalid, - - /// An RVLC encode primitive ([`crate::rvlc::rvlc_encode`] / - /// [`crate::rvlc::rvlc_esc_encode`]) was handed a value outside - /// its codebook domain: a Table 4.166 RVLC delta outside - /// `-7..=+7`, or a Table 4.168 escape magnitude index outside - /// `0..=53` (ISO/IEC 14496-3 §4.6.16.2). A conforming - /// error-resilient encoder never builds such a value; this - /// surfaces caller bugs at the scalefactor-quantisation / - /// emission boundary. - RvlcEncodeInvalid, - - /// [`crate::rvlc::rvlc_decode`] read a Table 4.167 *asymmetric* - /// (forbidden) codeword from the error-resilient - /// `scale_factor_data()` RVLC part (ISO/IEC 14496-3 §4.6.16.2.1). - /// Because the RVLC code tree leaves some nodes unused, hitting - /// one is an in-band *error-detection* event — the stream's RVLC - /// scalefactor data is corrupt. - RvlcForbiddenCodeword, - - /// [`crate::rvlc::rvlc_esc_decode`] walked the full 20-bit - /// Table 4.168 RVLC-ESC depth without matching any codeword - /// (ISO/IEC 14496-3 §4.6.16.2). The escape part of the - /// error-resilient `scale_factor_data()` is corrupt. - RvlcEscInvalid, - - /// The error-resilient `scale_factor_data()` RVLC branch - /// (ISO/IEC 14496-3 Table 4.53 / §4.6.16.2) violated a - /// structural invariant: the decoded RVLC part did not consume - /// exactly `length_of_rvlc_sf` bits, the escape part did not - /// consume exactly `length_of_rvlc_escapes` bits, an escape was - /// signalled for a non-`ESC_FLAG` band, or an in-memory record - /// set handed to the writer cannot be represented (variant / - /// codebook mismatch, escape magnitude out of range, or the - /// `rev_global_gain` / DPCM-last seeds out of their field caps). - RvlcScaleFactorDataInvalid, - - /// [`crate::pce::Pce::write`] was handed an in-memory - /// [`crate::pce::Pce`] whose field combination cannot be - /// represented on the wire under ISO/IEC 14496-3 §4.4.1.1 / - /// Table 4.2. Examples: `element_instance_tag > 0x0f` (4-bit - /// field cap); `object_type > 0x03` (2-bit field cap); - /// `sampling_frequency_index > 0x0f` (4-bit field cap); - /// `front_elements.len() > 0x0f`, `side_elements.len() > 0x0f`, - /// or `back_elements.len() > 0x0f` (4-bit `num_*` field caps); - /// `lfe_element_tag_selects.len() > 0x03` (2-bit `num_lfe` - /// field cap); `assoc_data_tag_selects.len() > 0x07` (3-bit - /// `num_assoc` field cap); `valid_cc_elements.len() > 0x0f` - /// (4-bit `num_valid_cc` field cap); a `tag_select` inside any - /// per-element list exceeds the 4-bit cap; a `matrix_mixdown` - /// `idx > 0x03` (2-bit field cap); `mono_mixdown_element_number` - /// or `stereo_mixdown_element_number` `> 0x0f` (4-bit caps); or - /// `comment_field.len() > 0xff` (8-bit `comment_field_bytes` - /// length prefix). A conforming AAC encoder never builds such a - /// structure; this surfaces caller bugs at the boundary between - /// channel-layout selection and bitstream emission. - PceEncodeInvalid, - - /// [`crate::raw_data_block::FrameAssembler`] was handed an - /// element whose field combination cannot be represented on the - /// wire under ISO/IEC 14496-3 §4.4.2.1. Examples: - /// [`crate::raw_data_block::FrameAssembler::push_channel_header`] - /// was called with an `IdSynEle` other than `SCE` / `CPE` / `CCE` - /// / `LFE` (those have their own dedicated `push_*` entry points - /// because each carries a bespoke wire layout — FIL goes through - /// [`crate::raw_data_block::FrameAssembler::push_fill`], DSE - /// through - /// [`crate::raw_data_block::FrameAssembler::push_data`], END - /// through - /// [`crate::raw_data_block::FrameAssembler::push_end`], and PCE - /// has no writer yet); a channel-element `element_instance_tag` - /// or DSE `element_instance_tag` exceeds the 4-bit field cap - /// (`> 0x0f`); a FIL payload exceeds the 269-byte ceiling - /// (`15 + 255 − 1`) imposed by the §4.4.2.7 8-bit `esc_count` - /// field; a DSE payload exceeds the 510-byte ceiling - /// (`255 + 255`) imposed by the §4.4.2.5 8-bit `esc_count` - /// field; or - /// [`crate::raw_data_block::FrameAssembler::push_channel_body_bits`] - /// was called with `bit_count > bits.len() * 8`. Long fill / - /// data payloads (above the per-element ceilings) split - /// naturally across multiple back-to-back FIL / DSE elements - /// with the same `tag`; that splitting is the caller's - /// responsibility, not the assembler's. - RawDataBlockEncodeInvalid, - - /// `epConfig` (from the Table 1.15 outer `switch (audioObjectType)` - /// for the ER object types) selected value `2` or `3`, which - /// mandates parsing the trailing `ErrorProtectionSpecificConfig()` - /// body. Phase 1 does not parse the error-protection - /// configuration; the carried `u8` is the literal 2-bit - /// `epConfig` field value as read from the wire. `epConfig == 0` - /// (no EP) and `epConfig == 1` (EP defined by EP class mapping - /// table only — no trailing body) are accepted and surfaced via - /// [`crate::asc::AudioSpecificConfig::ep_config`]. - UnsupportedEpConfig(u8), - - /// An `ErrorProtectionSpecificConfig()` (§1.8.2.1 Table 1.49) - /// carries a reserved or inconsistent field: `interleave_type == - /// 3`, `number_of_concatenated_frame == 0` (Table 1.54), `fec_type - /// == 3`, an SRCPC `class_rate > 24`, a `class_crclen > 18`, a - /// width-28 intraclass `interleave_switch` on an RS class - /// (Table 1.64), or a `class_output_order` that is not a - /// permutation. - EpConfigInvalid, - - /// An EP-tool frame (`ep_frame()`, §1.8.2.2) violates its - /// configuration: a `choice_of_pred` beyond the expanded set - /// list, a class overrunning the frame, a failed class CRC, an - /// uncorrectable FEC codeword, or a malformed EPMuxElement / - /// EPAudioSyncStream carrier. - EpFrameInvalid, - - /// A scalable-AAC (§4.4.2.2 / §4.5.2.2) layer configuration or - /// per-layer payload violates a normative shape: an empty or - /// over-long layer list (one main + at most 7 extension layers, - /// §4.5.2.2.4), a mono layer following a stereo layer - /// (Table 4.87), a payload count that does not match the - /// configured layer count, a reserved `ms_mask_present == 3` - /// (§4.6.8.1.2), an LD frame family (the scalable object types - /// are defined over the 1024/960-line families only), or a - /// per-layer element that overruns its payload. - ScalableInvalid, - - /// The scalable configuration signals a non-AAC lower layer — - /// `dependsOnCoreCoder == 1` (a CELP core, §4.5.2.2.5) or a - /// TwinVQ layer (§4.5.2.2.6). This crate decodes the AAC-only - /// scalable combinations (§4.5.2.2.4); the CELP / TwinVQ - /// base-layer codecs belong to other subparts. - ScalableUnsupportedCore, - - /// An invalid per-band tool combination between two scalable - /// layers per Tables 4.91–4.93 (e.g. a plain-coded band followed - /// by a PNS band in the next layer, or an intensity band on top - /// of a plain-coded stereo band). - ScalableLayerCombination, - - /// `extensionFlag3` was set to `1` inside the `GASpecificConfig` - /// `extensionFlag` body (Table 4.1). ISO/IEC 14496-3:2009 reserves - /// the body behind this flag with the comment "tbd in version 3"; - /// since the body bit-layout is not defined, Phase 1 cannot - /// advance the bit-reader and rejects the ASC. - UnsupportedAscExtensionFlag3, - - /// The Table 1.15 trailing `syncExtensionType == 0x2b7` probe - /// resolved an `extensionAudioObjectType` whose body bit-layout - /// is not specified by ISO/IEC 14496-3:2009 §1.6.2.1. The carrier - /// only spells out two values: `5` (HE-AAC SBR with the optional - /// `0x548` PS sub-probe) and `22` (ER BSAC with mandatory - /// `extensionChannelConfiguration`); any other extension AOT - /// resolved by `GetAudioObjectType()` inside the probe surfaces - /// here. The carried `u8` is the resolved extension AOT. - UnsupportedTrailingExtensionAot(u8), - - /// `extension_payload()` dispatched on an `extension_type` value - /// whose body needs the SBR back-end this crate does not yet - /// provide. The carried `u8` is the literal 4-bit - /// `extension_type` value as read from the wire — one of - /// `0b1101` (`EXT_SBR_DATA`) or `0b1110` (`EXT_SBR_DATA_CRC`) - /// per ISO/IEC 13818-7 Table 40. - UnsupportedExtensionSbr(u8), - - /// `extension_payload()` dispatched on a reserved - /// `extension_type` value (any 4-bit value not in - /// `{0b0000, 0b0001, 0b1011, 0b1101, 0b1110}`). ISO/IEC - /// 14496-3 Table 4.59 and ISO/IEC 13818-7 Table 40 list these - /// values as "reserved"; this crate has no body layout to - /// advance the bit-reader by. - UnsupportedExtensionType(u8), - - /// [`crate::extension_payload::ExtensionPayload`] parse / write - /// hit a structural invariant violation: - /// - /// * The dispatching FIL `cnt` is 0 (no room for the 4-bit - /// `extension_type` field). - /// * For `EXT_FILL` (parser / writer): an `other_bits` byte - /// buffer whose length does not match the - /// `8 * (cnt - 1) + 4` body-bits ceiling. - /// * For `EXT_FILL_DATA` (parser): a `fill_nibble` that is not - /// normatively `0b0000`, or a `fill_byte` that is not - /// normatively `0b10100101`. - /// * For `EXT_DYNAMIC_RANGE` (parser): the Table 4.52 derived - /// byte count `n` disagrees with the dispatching FIL `cnt`. - /// * For `EXT_DYNAMIC_RANGE` (writer): a numeric field - /// overflows its Table 4.52 cap (`pce_instance_tag > 0x0f`, - /// `drc_tag_reserved_bits > 0x0f`, `drc_band_incr > 0x0f`, - /// `drc_bands_reserved_bits > 0x0f`, `prog_ref_level > - /// 0x7f`, `dyn_rng_ctl > 0x7f`), an internal - /// shape-mismatch (`band_top.len() != 1 + band_incr`, - /// `bands.len() != drc_num_bands`), or an - /// `excluded_channels.exclude_mask.len()` that is not a - /// positive multiple of 7 (Table 4.53 emits exclusion bits - /// in fixed groups of 7). - ExtensionPayloadInvalid, - - /// [`crate::gain_control_data::GainControlData::write`] was - /// handed an in-memory - /// [`crate::gain_control_data::GainControlData`] whose field - /// combination cannot be represented on the wire under ISO/IEC - /// 14496-3 §4.4.6.5 / Table 4.12. Examples: `max_band > 0x03` - /// (2-bit field cap); `bands.len() != max_band` (the outer - /// band-loop count must match the dispatched wire value); - /// `band.windows.len()` differs from the per-`window_sequence` - /// count (1 for `OnlyLong`, 2 for `LongStart` / `LongStop`, 8 for - /// `EightShort`); a per-`(bd, wd)` `adjustments.len() > 7` - /// (3-bit `adjust_num` field cap); a `GainAdjust::alevcode > - /// 0x0f` (4-bit field cap); or a `GainAdjust::aloccode` exceeds - /// the per-slot width-derived cap (5 bits for `OnlyLong wd=0`, - /// 4 bits for `LongStart / LongStop wd=0`, 2 bits for - /// `EightShort` and the `wd=1` slot of `LongStart`, 5 bits for - /// the `wd=1` slot of `LongStop`). A conforming AAC SSR encoder - /// never builds such a structure; this surfaces caller bugs at - /// the boundary between the SSR PQF gain-control psychoacoustic - /// stage and bitstream emission. - GainControlDataEncodeInvalid, - - /// [`crate::scale_factor_data::differentiate`] was handed an - /// [`crate::scale_factor_data::AbsoluteScaleFactors`] whose - /// shape or numeric values cannot be encoded back to a - /// well-formed `scale_factor_data()` block. Examples: outer - /// length differs from `sfb_cb.len()`; a group's - /// per-band-classification list differs from the matching - /// `sfb_cb` group; the spectrum-track delta `sf - last_sf` - /// falls outside Table 4.150's `-60..=+60`; the intensity-track - /// delta `is_pos - last_is` falls outside `-60..=+60`; the - /// PNS-track delta `nrg - last_nrg` (for PNS bands after the - /// first) falls outside `-60..=+60`; or the first PNS band's - /// initial seed magnitude (`first_nrg - (global_gain - - /// NOISE_OFFSET - 256)`) does not fit the 9-bit Table 4.53 - /// `dpcm_noise_nrg` uimsbf field (`0..=511`). A conforming AAC - /// rate-allocation stage never produces such a structure; this - /// surfaces caller bugs at the boundary between absolute - /// scalefactor quantisation and DPCM differential coding. - ScaleFactorAccumulatorInvalid, - - /// [`crate::spectral_codebook::table_4_95`] (or any other - /// public accessor in that module) was called with a `codebook` - /// value `> 31`. ISO/IEC 14496-3 Table 4.95 only defines rows - /// `0..=31`. - SpectralCodebookOutOfRange(u8), - - /// [`crate::spectral_codebook::decode_index_to_tuple`] / - /// [`crate::spectral_codebook::encode_tuple_to_index`] / - /// [`crate::spectral_codebook::apply_sign_bits`] / - /// [`crate::spectral_codebook::derive_sign_bits`] was called - /// with a codebook whose Table 4.95 row carries no - /// `unsigned_cb` / `dimension` / `lav` (`0`, `12`, `13`, `14`, - /// `15`). Those are non-spectral books (`ZERO_HCB`, reserved, - /// PNS, intensity stereo); §4.6.3.3 does not translate any - /// codeword index for them. - SpectralCodebookHasNoTuple(u8), - - /// [`crate::spectral_codebook::decode_index_to_tuple`] was - /// called with a codeword index `idx >= mod^dim` where `mod = - /// lav + 1` (unsigned) or `2 * lav + 1` (signed). A conforming - /// Huffman decoder never produces such an index; this surfaces - /// an incoherence between the Huffman tree and Table 4.95. - SpectralCodebookIndexOutOfRange(u8), - - /// [`crate::spectral_codebook::encode_tuple_to_index`] / - /// [`crate::spectral_codebook::derive_sign_bits`] was called - /// with a tuple shorter than the codebook's dimension, or with - /// an entry outside the codebook's representable range - /// (`0..=lav` unsigned, `-lav..=+lav` signed). A conforming AAC - /// encoder never produces such a tuple. - SpectralCodebookTupleOutOfRange(u8), - - /// [`crate::spectral_codebook::apply_sign_bits`] was called - /// with a `signs` slice whose length disagrees with the count - /// of non-zero coefficients in the unsigned-codebook tuple, or - /// with a non-empty `signs` slice on a signed codebook. - SpectralCodebookSignBitsMismatch(u8), - - /// [`crate::spectral_codebook::decode_esc_value`] / - /// [`crate::spectral_codebook::encode_esc_value`] was called - /// with arguments outside the §4.6.3.3 ESC range: `prefix_len > - /// 9`, `escape_word` not fitting `(prefix_len + 4)` bits, a - /// decoded value exceeding `MAX_QUANT` (`8191`), or an encoder - /// value `< 16` (which is in-band, not ESC-encoded). - SpectralCodebookEscOutOfRange, - - /// [`crate::tns_coef::tns_decode_coef`] / - /// [`crate::tns_coef::tns_encode_coef`] / - /// [`crate::tns_coef::iqfac`] / [`crate::tns_coef::iqfac_m`] / - /// [`crate::tns_coef::sign_extend_coef`] / - /// [`crate::tns_coef::pack_coef`] was called with an argument - /// outside the §4.6.9.3 / §C.6 legal range. Examples: - /// `coef_res_bits` not in `{3, 4}` (the spec's `coef_res[w] + 3` - /// envelope); `coef_compress > 1` (a 1-bit wire flag); a wire - /// `coef[i]` value that does not fit in `coef_res2 = - /// coef_res_bits - coef_compress` bits; a `pack_coef` `value` - /// outside `-(1 << (coef_res2-1))..=(1 << (coef_res2-1)) - 1`; - /// or an encode-side PARCOR coefficient `|r| > 1.0` (or NaN / - /// ±∞) — `arcsin` is undefined outside `[-1, 1]`. - TnsCoefOutOfRange, - - /// [`crate::tns_frame::tns_decode_frame`] was called with a - /// frame-level argument combination that violates the §4.6.9.3 - /// `tns_decode_frame()` preconditions: the `spec` buffer length - /// differs from `num_windows × window_len` (8 × 128 for - /// `EIGHT_SHORT_SEQUENCE`, 1 × 1024 otherwise); the - /// [`crate::tns_data::TnsData`] window count disagrees with the - /// `window_sequence`; or a filter's `coef` vector is shorter than - /// the `TNS_MAX_ORDER`-clamped `tns_order` it must supply. A - /// [`crate::tns_data::TnsData`] produced by - /// [`crate::tns_data::TnsData::parse`] under the same - /// `window_sequence` never trips the structural checks — this - /// surfaces caller-fabricated structures. - TnsFrameInvalid, - - /// [`crate::spectral_data::SpectralData::parse`] (or the - /// [`crate::spectral_data::sect_sfb_offset`] helper) found a - /// structural violation of Table 4.56 / §4.5.2.3.4: `max_sfb` - /// exceeding `num_swb` for the active window sequence, a - /// [`crate::section_data::SectionData`] whose group count - /// disagrees with the [`crate::ics_info::IcsInfo`], a section - /// carrying the reserved codebook 12 into `spectral_data()`, - /// or a section span that is not a whole number of - /// `QUAD_LEN` / `PAIR_LEN` n-tuples. - SpectralDataInvalid, - - /// [`crate::spectral_data::SpectralData::write`] was handed a - /// coefficient buffer that cannot be represented on the wire: - /// per-group buffer lengths disagreeing with - /// `window_group_length[g] × window_len`, a non-zero coefficient - /// inside a `ZERO_HCB` / `NOISE_HCB` / intensity section (or - /// above `max_sfb`), or a magnitude exceeding the section - /// codebook's LAV (`MAX_QUANT` = 8191 for the ESC book). - SpectralDataEncodeInvalid, - - /// [`crate::dequant::rescale_spectrum`] found a structural - /// mismatch between its inputs: group counts disagreeing with - /// `num_window_groups`, a per-group `x_quant` buffer length - /// disagreeing with the `ics_info` grouping, or an - /// [`crate::scale_factor_data::AbsoluteScaleFactorEntry`] - /// sequence that does not match the non-`ZERO_HCB` codebook - /// classification of `sfb_cb` (including the reserved codebook - /// 12, which has no spectrum semantics to rescale). Inputs - /// produced by the wire parsers plus - /// [`crate::scale_factor_data::accumulate`] under one shared - /// `ics_info` / `section_data` never trip this — it surfaces - /// caller-fabricated structures. - DequantInvalid, - - /// [`crate::decoded_spectrum::quant_to_spec`] was handed a group - /// buffer set whose shape disagrees with the `ics_info` - /// grouping: wrong group count, a group buffer length that is - /// not `window_group_length[g] × window_len`, or a - /// `window_group_length[]` whose sum is not `num_windows`. - QuantToSpecInvalid, - - /// [`crate::filterbank::Filterbank::synthesize`] was handed a - /// window-major spectrum whose length disagrees with the - /// [`crate::ics_info::IcsInfo`] `window_sequence`: a long - /// sequence (`ONLY_LONG` / `LONG_START` / `LONG_STOP`) requires - /// exactly [`crate::swb_offset::LONG_WINDOW_LEN`] (1024) - /// coefficients, an `EIGHT_SHORT` sequence requires `8 ×` - /// [`crate::swb_offset::SHORT_WINDOW_LEN`] (1024 total). The - /// §4.6.11.3.1 IMDCT cannot run against any other length. - FilterbankInvalid, - - /// [`crate::ms_stereo::apply_ms_stereo`] was handed a channel - /// pair whose shapes disagree with the shared - /// [`crate::ics_info::IcsInfo`]: the two window-major spectra - /// have different lengths, a length that is not - /// `num_windows × window_len`, an `ms_used` mask whose group - /// count is not `num_window_groups` (or a per-group row shorter - /// than `max_sfb`), or a per-channel `sfb_cb` whose group/band - /// extents do not cover `max_sfb`. The §4.6.8.1.3 de-matrix is - /// undefined without a consistent group/band geometry across - /// both channels. - MsStereoInvalid, - /// [`crate::intensity_stereo::apply_intensity_stereo`] was handed a - /// channel pair whose shapes disagree with the shared - /// [`crate::ics_info::IcsInfo`]: the two window-major spectra have - /// different lengths, a length that is not - /// `num_windows × window_len`, an `ms_used` mask whose group count - /// is not `num_window_groups` (or a per-group row shorter than - /// `max_sfb`), a right-channel `sfb_cb` that does not cover - /// `max_sfb`, or an `is_pos[g][sfb]` table whose group/band extents - /// do not cover every intensity-coded band. The §4.6.8.2.3 scale - /// `is_intensity · invert_intensity · 0.5^(0.25·is_pos)` is - /// undefined without a consistent group/band geometry and an - /// intensity-stereo position for every intensity band. - IntensityStereoInvalid, - /// [`crate::pns::apply_pns`] / [`crate::pns::apply_pns_pair`] was - /// handed a channel (or pair) whose shapes disagree with the - /// [`crate::ics_info::IcsInfo`]: a window-major spectrum whose - /// length is not `num_windows × window_len`, a - /// `window_group_length` whose sum is not `num_windows`, a - /// `max_sfb` beyond the active window's band count, a `sfb_cb` or - /// `noise_nrg` table whose group/band extents do not cover - /// `max_sfb`, two paired channels with differing window geometry, - /// or (for the pair) an `ms_used` mask whose group count is not - /// `num_window_groups` (or a per-group row shorter than `max_sfb`). - /// The §4.6.13.3 noise synthesis is undefined without a consistent - /// group/band geometry and a `noise_nrg` for every noise band. - PnsInvalid, - /// [`crate::ltp::LtpState::apply_long`] was handed §4.6.7 Long-Term - /// Prediction inputs that are mutually inconsistent: an `ltp_coef` - /// index outside the Table 4.98 codebook (`> 7`), an active - /// `ltp_long_used` mask with no transmitted `ltp_lag`, or a channel - /// spectrum whose length is not `LONG_WINDOW_LEN` (1024). The - /// §4.6.7.3 `X_rec = X_est + Y_rec` combination is undefined without - /// a valid predictor coefficient, lag, and long-window spectrum. - LtpInvalid, - /// [`crate::element_decode`] was asked to decode a channel element - /// whose component shapes are mutually inconsistent: a channel-pair - /// element (`CPE`) whose two channels disagree on `window_sequence` - /// (so the shared `common_window` geometry the §4.6.8 joint-stereo - /// tools require is violated), an `ms_used` row that does not cover - /// `num_window_groups × max_sfb`, or a per-channel - /// `AbsoluteScaleFactors` whose wire-order record count does not - /// match its `sfb_cb` non-`ZERO_HCB` band count when expanded to the - /// band-indexed `is_pos[g][sfb]` / `noise_nrg[g][sfb]` layout the - /// §4.6.8.2 / §4.6.13 synthesis passes consume. The element-level - /// §4.6 block-order chain (de-quantise → M/S → intensity → PNS → - /// TNS → filterbank) cannot run without a consistent geometry across - /// the composed stages. - ElementDecodeInvalid, - /// [`crate::predictor::PredictorBank`] was handed §4.6.6 - /// frequency-domain-prediction inputs that are mutually inconsistent: - /// a long-window scalefactor-band offset table too short to cover - /// `PRED_SFB_MAX` for the sampling rate, a reconstructed spectrum - /// shorter than the per-line predictor bank, or a - /// `predictor_reset_group_number` outside the Table 4.97 range - /// (`1 ..= 30`; the values `0` and `31` are reserved). The - /// §4.6.6.3.2.1 `x_rec = x_est + y_rec` reconstruction and the - /// §4.6.6.3.3 reset are undefined without a full predictor bank and a - /// valid reset group. - PredictorInvalid, - /// SBR frequency-band-table derivation - /// ([`crate::sbr_freq_bands`], §4.6.18.3.2) was handed parameters - /// that violate a normative constraint: - /// - /// * `bs_start_freq` / `bs_stop_freq` outside their 4-bit ranges - /// (`0 ..= 15` each), an unsupported `FsSBR` (no offset / - /// `startMin` / `stopMin` row in §4.6.18.3.2.1), or - /// `bs_freq_scale` / `bs_alter_scale` / `bs_noise_bands` - /// outside their signalled ranges. - /// * A derived geometry that breaks a §4.6.18.3.6 requirement: - /// `k2 <= k0` (`fMaster` undefined), `numBands <= 0`, - /// `k2 - k0` over the per-rate subband-count cap, `k_x > 32`, - /// `k_x + M > 64`, or `bs_xover_band >= NMaster`. - /// - /// The §4.6.18.3.2.1 master table and the §4.6.18.3.2.2 derived - /// high / low / noise tables are undefined for such inputs. - SbrFreqBandInvalid, - /// SBR envelope / noise Huffman decode ([`crate::sbr_huffman`], - /// §4.A.6.1 `sbr_huff_dec()`) could not match a codeword: either no - /// table entry matched within the maximum SBR codeword length, or - /// the bitstream ran out before a codeword completed. Both signal a - /// corrupt or truncated SBR extension payload. - SbrHuffInvalid, - /// Parametric Stereo `ps_data()` parse ([`crate::ps_data`] / - /// [`crate::ps_huffman`], ISO/IEC 14496-3:2009 §8.4.2 Table 8.9): - /// a PS Huffman codeword failed to match within the Annex 8.B - /// maximum length, the bitstream ran out mid-element, a reserved - /// `iid_mode` / `icc_mode` was signalled, or a differentially - /// decoded IID/ICC index left its Table 8.24/8.27 range. All - /// signal a corrupt or truncated PS extension payload. - PsDataInvalid, - /// SBR time-frequency grid parse ([`crate::sbr_grid`], §4.4.2.8 - /// Tables 4.69–4.71) failed: the bitstream ran out mid-grid, or a - /// frame class signalled an envelope count outside the - /// §4.6.18.3.6 limit ([`crate::sbr_grid::SBR_MAX_NUM_ENV`]). Both - /// signal a corrupt SBR data element. - SbrGridInvalid, - /// SBR QMF filterbank ([`crate::sbr_qmf`], §4.6.18.4) was handed a - /// slot buffer of the wrong length: the analysis bank consumes - /// exactly 32 time samples per slot, the synthesis bank exactly 64 - /// complex subband samples (32 for the downsampled variant). - SbrQmfInvalid, - /// The §4.6.18.8 low-power SBR tool operates on real-valued - /// subband signals, so the subpart-8 Parametric Stereo tool — - /// whose de-correlation and phase parameters need the - /// complex-valued QMF domain — cannot run on top of it. Decode - /// HE-AAC v2 streams with the high-quality (complex) SBR mode. - SbrLowPowerPs, - /// Integer-PCM rendering ([`crate::pcm`], §4.6.11 output → - /// §1.3 `NINT()`-rounded 16-bit word) was handed per-channel time - /// signals of disagreeing length. [`crate::pcm::interleave_s16`] - /// requires every channel buffer to carry the same per-frame sample - /// count (the §4.6.11 transform length) so the interleave is - /// well-defined. - PcmInvalid, - /// LATM `StreamMuxConfig()` ([`crate::latm`], ISO/IEC 14496-3 - /// §1.7.3 Table 1.42) signalled `audioMuxVersion == 1` with - /// `audioMuxVersionA == 1`, which the spec marks reserved-for- - /// future-extensions (`/* tbd */`). No syntax is defined for that - /// branch, so the multiplex cannot be parsed. - LatmAudioMuxVersionAReserved, - /// LATM `StreamMuxConfig()` ([`crate::latm`], §1.7.3 Table 1.42) - /// signalled a per-layer `frameLengthType` this decoder does not - /// carry payload framing for. Only `0` (variable-length, byte - /// count in `PayloadLengthInfo()`) and `1` (fixed `frameLength` - /// bits) are supported; the CELP (`3`/`4`/`5`) and HVXC - /// (`6`/`7`) types index frame-length tables this AAC-focused - /// decoder does not implement. Carries the offending value. - LatmUnsupportedFrameLengthType(u8), - /// LATM multiplex configuration ([`crate::latm`], §1.7.3) exceeded - /// one of the spec signalling caps: `numProgram > 15`, - /// `numLayer > 7`, `numChunk > 15`, `streamCnt > 15`, or - /// `numSubFrames` produced more PayloadMux frames than the bound. - /// The fields are bit-limited on the wire so this only fires on a - /// derived-count overflow or an internal inconsistency. - LatmConfigOutOfRange, - /// LATM `AudioMuxElement()` ([`crate::latm`], §1.7.3 Table 1.41) - /// with `muxConfigPresent == 1` set `useSameStreamMux == 1` (apply - /// previous configuration) but no `StreamMuxConfig()` had been - /// decoded yet on this stream. The first in-band element must - /// carry the configuration. - LatmNoPreviousMuxConfig, - /// LATM transport ([`crate::latm`], §1.7.3 Table 1.42) carried a - /// `crcCheckSum` whose recomputed §1.8.4.5 `CRC8` value did not - /// match the transmitted byte, indicating a corrupt - /// `StreamMuxConfig()`. - LatmCrcMismatch, - /// LOAS `AudioSyncStream()` / `EPAudioSyncStream()` - /// ([`crate::latm`], §1.7.2 Tables 1.36 / 1.37) sync search failed: - /// the `0x2B7` / `0x4DE1` syncword was not found, or the - /// `audioMuxLengthBytes` payload ran past the end of the buffer. - LoasSyncInvalid, - /// **No longer emitted.** A LATM/LOAS `AudioSpecificConfig` that - /// signals SBR ([`crate::latm::LoasDecoder`]) now decodes through - /// the shared §4.6.18 SBR back-end instead of being pre-rejected - /// (a PS-signalling stream decodes its HE-AAC v1 layer). The - /// variant is kept so existing `match` arms stay valid. - LatmSbrUnsupported, - /// `coupling_channel_element()` parse / reconstruction - /// ([`crate::cce`], ISO/IEC 14496-3 §4.6.8.3 / Table 4.8) was handed - /// a structurally inconsistent CCE: - /// - /// * a `num_coupled_elements` / `cc_target_is_cpe` / `cc_l` / `cc_r` - /// combination that derives a `num_gain_element_lists` other than - /// the count of transmitted gain lists, - /// * an `ind_sw_cce_flag == 1` (independently switched) element that - /// carries a per-band `dpcm_gain_element` list instead of the - /// §4.6.8.3.3-required single `common_gain_element` per target, or - /// * a coupled-target geometry (`num_window_groups` / `max_sfb` / - /// `swb_offset`) whose gain list does not cover the embedded - /// `single_channel_element()`'s band layout. - /// - /// The §4.6.8.3.3 `couple_channel()` scaling-and-add is undefined for - /// such inputs. - CceInvalid, - - /// An ADTS frame with `protection_absent == 0` carried a - /// `crc_check` (or, in the multi-raw-data-block form, an - /// `adts_header_error_check()` / `adts_raw_data_block_error_check()` - /// field) that does not match the CRC recomputed over the - /// ISO/IEC 13818-7:2004 §8.1.1.1 protected-bit region with the - /// ISO/IEC 11172-3 §2.4.3.1 code (16 bits, generator `0x8005`, - /// all-ones init). The protected header / element bits are - /// corrupt. - AdtsCrcMismatch, - - /// An `EXT_SBR_DATA_CRC` fill extension carried a - /// `bs_sbr_crc_bits` value that does not match the 10-bit CRC - /// (generator `G10 = x¹⁰+x⁹+x⁵+x⁴+x+1`, zero init — ISO/IEC - /// 14496-3:2009 §4.4.2.8.1) recomputed over the - /// `sbr_extension_data()` payload bits after the CRC field - /// (Table 4.62, `num_sbr_bits − 10` bits before `bs_fill_bits`). - /// The SBR side info is corrupt. - SbrCrcMismatch, - - /// A `bsac_header()` / `general_header()` field is out of its - /// legal range (ISO/IEC 14496-3:2009 §4.5.2.6.2.2.4/5): a - /// `cband_si_type` past Table 4.A.31, a `max_sfb` past the - /// §4.5.4 band table, a zero base-layer coverage, or a - /// `frame_length` too small for the headers. - BsacInvalidHeader, - - /// The arithmetic-decoded BSAC side information violates a - /// normative bound (§4.6.4.5 "bit_error_is_generated"): a - /// `cband_si` above the Table 4.A.31 largest value, or a - /// stereo / noise decision outside its model. - BsacBitError, - - /// The `bsac_raw_data_block()` uses a tool this decoder does - /// not implement yet (long-term prediction, or the extended - /// part's channel / SBR / SAC extensions). - BsacUnsupportedTool, -} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Error::NotImplemented => { - write!(f, "oxideav-aac: feature not implemented in Phase 1") - } - Error::AdtsSyncNotFound => { - write!(f, "ADTS sync word (0xFFF) not found") - } - Error::AdtsLayerNonZero => { - write!(f, "ADTS layer field must be 0") - } - Error::AdtsReservedSampleRateIndex => { - write!( - f, - "ADTS sampling_frequency_index is reserved (13, 14, or 15)" - ) - } - Error::AdtsFrameLengthTooSmall => { - write!(f, "ADTS aac_frame_length is smaller than the header") - } - Error::AdtsEncodeInvalid => { - write!( - f, - "ADTS header field exceeds its wire width or violates a normative constraint" - ) - } - Error::EncoderInvalidConfig => { - write!(f, "AAC encoder configuration or input slice is invalid") - } - Error::EncoderFrameOverflow => { - write!( - f, - "encoded AAC frame exceeds the 13-bit aac_frame_length ceiling" - ) - } - Error::UnexpectedEnd => { - write!(f, "unexpected end of bitstream") - } - Error::UnsupportedElementSkip(id) => { - write!( - f, - "raw_data_block walker cannot advance past id_syn_ele {} in Phase 1", - id - ) - } - Error::UnsupportedAot(aot) => { - write!( - f, - "AudioSpecificConfig audioObjectType {} is not handled in Phase 1", - aot - ) - } - Error::IcsInfoUnsupportedSampleRateIndex(idx) => { - write!( - f, - "ics_info sampling_frequency_index {} is outside the 0..=11 SWB-table range", - idx - ) - } - Error::SbrUnsupportedFrameFamily => { - write!( - f, - "SBR extension on a non-1024-line frame family: the §4.6.18 tool covers the 1024-line core only" - ) - } - Error::LdShortWindow => { - write!( - f, - "ER AAC LD: the 512/480-line families are long-only (§4.6.17.2.2) — no short-window geometry exists" - ) - } - Error::IcsInfoEncodeInvalid => { - write!( - f, - "ics_info encode: in-memory IcsInfo violates a Table 4.6 / 4.55 wire-field invariant" - ) - } - Error::SectionDataOverrun => { - write!( - f, - "section_data sect_len overruns max_sfb (malformed bitstream)" - ) - } - Error::SectionDataEncodeInvalid => { - write!( - f, - "section_data encode: per-group sections must be contiguous [0, max_sfb), sect_cb < 16, sect_len > 0" - ) - } - Error::PulseDataEncodeInvalid => { - write!( - f, - "pulse_data encode: pulses.len() in 1..=4, pulse_start_sfb < 64, pulse_offset < 32, pulse_amp < 16" - ) - } - Error::TnsDataEncodeInvalid => { - write!( - f, - "tns_data encode: in-memory TnsData violates a Table 4.54 / 4.155 wire-field invariant" - ) - } - Error::ScaleFactorDataEncodeInvalid => { - write!( - f, - "scale_factor_data encode: in-memory record set violates a Table 4.53 / 4.150 wire-field invariant" - ) - } - Error::RvlcEncodeInvalid => { - write!( - f, - "rvlc encode: value outside the Table 4.166 (-7..=+7) / Table 4.168 (0..=53) codebook domain" - ) - } - Error::RvlcForbiddenCodeword => { - write!( - f, - "rvlc decode: read a Table 4.167 asymmetric (forbidden) codeword — RVLC scalefactor data is corrupt (§4.6.16.2.1)" - ) - } - Error::RvlcEscInvalid => { - write!( - f, - "rvlc-esc decode: 20-bit Table 4.168 walk matched no codeword — RVLC escape data is corrupt (§4.6.16.2)" - ) - } - Error::RvlcScaleFactorDataInvalid => { - write!( - f, - "error-resilient scale_factor_data: RVLC branch violates a Table 4.53 / §4.6.16.2 structural invariant" - ) - } - Error::PceEncodeInvalid => { - write!( - f, - "pce encode: in-memory Pce violates a Table 4.2 wire-field invariant" - ) - } - Error::RawDataBlockEncodeInvalid => { - write!( - f, - "raw_data_block encode: element field violates a §4.4.2.1 / §4.4.2.5 / §4.4.2.7 wire-field invariant" - ) - } - Error::GainControlDataEncodeInvalid => { - write!( - f, - "gain_control_data encode: in-memory GainControlData violates a Table 4.12 wire-field invariant" - ) - } - Error::ScaleFactorAccumulatorInvalid => { - write!( - f, - "scale_factor accumulator: absolute-to-DPCM differentiation produced a delta outside Table 4.150 / Table 4.53 ranges" - ) - } - Error::UnsupportedEpConfig(value) => { - write!( - f, - "AudioSpecificConfig epConfig {} requires ErrorProtectionSpecificConfig parsing (Phase 1 supports only epConfig 0 and 1)", - value - ) - } - Error::EpConfigInvalid => { - write!( - f, - "ErrorProtectionSpecificConfig: reserved or inconsistent field (Table 1.49 / 1.54 / 1.64)" - ) - } - Error::EpFrameInvalid => { - write!( - f, - "EP-tool frame violates its configuration (ep_frame() vs ErrorProtectionSpecificConfig)" - ) - } - Error::ScalableInvalid => { - write!( - f, - "scalable AAC: layer configuration or per-layer payload violates the §4.4.2.2 / §4.5.2.2 shape" - ) - } - Error::ScalableUnsupportedCore => { - write!( - f, - "scalable AAC: CELP core / TwinVQ lower layers are out of scope (AAC-only combinations per §4.5.2.2.4)" - ) - } - Error::ScalableLayerCombination => { - write!( - f, - "scalable AAC: invalid per-band tool combination between layers (Tables 4.91-4.93)" - ) - } - Error::UnsupportedAscExtensionFlag3 => { - write!( - f, - "GASpecificConfig extensionFlag3 body is reserved (\"tbd in version 3\") and cannot be parsed" - ) - } - Error::UnsupportedTrailingExtensionAot(aot) => { - write!( - f, - "AudioSpecificConfig trailing syncExtensionType=0x2b7 probe resolved extensionAudioObjectType {} (only 5 and 22 have a Table 1.15 body)", - aot - ) - } - Error::UnsupportedExtensionSbr(value) => { - write!( - f, - "extension_payload extension_type 0x{:x} selects EXT_SBR_DATA / EXT_SBR_DATA_CRC; SBR back-end is not implemented", - value - ) - } - Error::UnsupportedExtensionType(value) => { - write!( - f, - "extension_payload extension_type 0x{:x} is reserved (no body layout defined)", - value - ) - } - Error::ExtensionPayloadInvalid => { - write!( - f, - "extension_payload: Table 4.51 / 4.52 / 4.53 / 4.59 wire-field invariant violated" - ) - } - Error::SpectralCodebookOutOfRange(cb) => { - write!( - f, - "spectral codebook {} is outside Table 4.95 (legal range 0..=31)", - cb - ) - } - Error::SpectralCodebookHasNoTuple(cb) => { - write!( - f, - "spectral codebook {} is non-spectral (Table 4.95 row carries no dim / lav)", - cb - ) - } - Error::SpectralCodebookIndexOutOfRange(cb) => { - write!( - f, - "spectral codebook {}: codeword index out of Table 4.95 range", - cb - ) - } - Error::SpectralCodebookTupleOutOfRange(cb) => { - write!( - f, - "spectral codebook {}: tuple length or value outside Table 4.95 dimension / lav", - cb - ) - } - Error::SpectralCodebookSignBitsMismatch(cb) => { - write!( - f, - "spectral codebook {}: sign-bit count disagrees with non-zero coefficients in tuple", - cb - ) - } - Error::SpectralCodebookEscOutOfRange => { - write!( - f, - "spectral codebook 11/16..=31 ESC sequence: prefix_len, escape_word, or magnitude outside §4.6.3.3 range" - ) - } - Error::TnsCoefOutOfRange => { - write!( - f, - "tns_coef: coef_res_bits / coef_compress / wire coef / PARCOR value outside §4.6.9.3 / §C.6 legal range" - ) - } - Error::TnsFrameInvalid => { - write!( - f, - "tns_decode_frame: spec length, TnsData window count, or per-filter coef length violates a §4.6.9.3 precondition" - ) - } - Error::SpectralDataInvalid => { - write!( - f, - "spectral_data: max_sfb / section layout / codebook violates a Table 4.56 or §4.5.2.3.4 structural constraint" - ) - } - Error::SpectralDataEncodeInvalid => { - write!( - f, - "spectral_data encode: coefficient buffer shape, zero-section content, or magnitude range cannot be represented per Table 4.56" - ) - } - Error::DequantInvalid => { - write!( - f, - "rescale_spectrum: x_quant / scalefactor-entry / sfb_cb layout violates a §4.6.1.3 / §4.6.2.3.3 precondition" - ) - } - Error::QuantToSpecInvalid => { - write!( - f, - "quant_to_spec: group buffer shape disagrees with the §4.5.2.3.4 ics_info grouping" - ) - } - Error::FilterbankInvalid => { - write!( - f, - "filterbank: window-major spectrum length disagrees with the §4.6.11 window_sequence" - ) - } - Error::MsStereoInvalid => { - write!( - f, - "M/S stereo: channel-pair spectra / ms_used / sfb_cb shapes disagree with the §4.6.8.1 ics_info geometry" - ) - } - Error::IntensityStereoInvalid => { - write!( - f, - "intensity stereo: channel-pair spectra / ms_used / right sfb_cb / is_pos shapes disagree with the §4.6.8.2 ics_info geometry" - ) - } - Error::PnsInvalid => { - write!( - f, - "PNS: channel spectrum / sfb_cb / noise_nrg / ms_used shapes disagree with the §4.6.13 ics_info geometry" - ) - } - Error::LtpInvalid => { - write!( - f, - "LTP: ltp_coef index, ltp_lag presence, or long-window spectrum length disagree with the §4.6.7 decoding process" - ) - } - Error::ElementDecodeInvalid => { - write!( - f, - "element decode: channel-element component shapes (window_sequence pairing, ms_used extent, or scalefactor-record count) are mutually inconsistent for the §4.6 block-order chain" - ) - } - Error::PredictorInvalid => { - write!( - f, - "predictor: long-window offset table, spectrum length, or reset-group number disagree with the §4.6.6 frequency-domain prediction process" - ) - } - Error::SbrFreqBandInvalid => { - write!( - f, - "SBR frequency bands: bs_start_freq/bs_stop_freq/bs_freq_scale, FsSBR, or the derived k0/k2 geometry violate a §4.6.18.3.2 / §4.6.18.3.6 constraint" - ) - } - Error::SbrHuffInvalid => { - write!( - f, - "SBR Huffman decode: no §4.A.6.1 codeword matched (corrupt or truncated SBR envelope/noise payload)" - ) - } - Error::PsDataInvalid => { - write!( - f, - "PS ps_data(): §8.4.2 Table 8.9 parse failed (unmatched Annex 8.B codeword, truncated payload, reserved iid/icc mode, or out-of-range index)" - ) - } - Error::SbrGridInvalid => { - write!( - f, - "SBR grid: §4.4.2.8 sbr_grid/sbr_dtdf/sbr_invf ran out of bits or signalled an out-of-range envelope count" - ) - } - Error::SbrQmfInvalid => { - write!( - f, - "SBR QMF: §4.6.18.4 filterbank slot buffer has the wrong length (analysis takes 32 samples, synthesis 64 complex bands, downsampled 32)" - ) - } - Error::SbrLowPowerPs => { - write!( - f, - "SBR low power: the §4.6.18.8 real-valued tool cannot carry the complex-domain subpart-8 PS tool; use the high-quality SBR mode for HE-AAC v2" - ) - } - Error::PcmInvalid => { - write!( - f, - "PCM interleave: per-channel time signals disagree in length" - ) - } - Error::LatmAudioMuxVersionAReserved => { - write!( - f, - "LATM StreamMuxConfig: audioMuxVersionA == 1 is reserved (§1.7.3 Table 1.42 /* tbd */ branch)" - ) - } - Error::LatmUnsupportedFrameLengthType(t) => { - write!( - f, - "LATM StreamMuxConfig: frameLengthType {t} (CELP/HVXC table-indexed framing) is unsupported; only 0 and 1 are carried" - ) - } - Error::LatmConfigOutOfRange => { - write!( - f, - "LATM StreamMuxConfig: a multiplex count (numProgram/numLayer/numChunk/streamCnt/numSubFrames) exceeded the §1.7.3 signalling cap" - ) - } - Error::LatmNoPreviousMuxConfig => { - write!( - f, - "LATM AudioMuxElement: useSameStreamMux == 1 but no previous StreamMuxConfig() has been decoded" - ) - } - Error::LatmCrcMismatch => { - write!( - f, - "LATM StreamMuxConfig: recomputed §1.8.4.5 CRC8 does not match the transmitted crcCheckSum" - ) - } - Error::LoasSyncInvalid => { - write!( - f, - "LOAS AudioSyncStream: §1.7.2 0x2B7/0x4DE1 syncword not found or audioMuxLengthBytes overruns the buffer" - ) - } - Error::LatmSbrUnsupported => { - write!( - f, - "LATM AudioSpecificConfig signalled SBR/PS, which the core LATM PCM driver does not decode" - ) - } - Error::CceInvalid => { - write!( - f, - "coupling_channel_element() has an inconsistent gain-list / target geometry (§4.6.8.3)" - ) - } - Error::AdtsCrcMismatch => { - write!( - f, - "ADTS crc_check mismatch: recomputed §8.1.1.1-region CRC-16 disagrees with the transmitted value" - ) - } - Error::SbrCrcMismatch => { - write!( - f, - "SBR bs_sbr_crc_bits mismatch: recomputed §4.4.2.8.1 CRC-10 disagrees with the transmitted value" - ) - } - Error::BsacInvalidHeader => { - write!( - f, - "bsac_header()/general_header() field out of range (§4.5.2.6.2.2.4)" - ) - } - Error::BsacBitError => { - write!( - f, - "BSAC arithmetic side info violates a normative bound (§4.6.4.5 bit error)" - ) - } - Error::BsacUnsupportedTool => { - write!( - f, - "bsac_raw_data_block() uses a tool this decoder does not implement (LTP / extended part)" - ) - } - } - } -} - -impl std::error::Error for Error {} diff --git a/crates/vendor/oxideav-aac/src/extension_payload.rs b/crates/vendor/oxideav-aac/src/extension_payload.rs deleted file mode 100644 index 86311f86..00000000 --- a/crates/vendor/oxideav-aac/src/extension_payload.rs +++ /dev/null @@ -1,856 +0,0 @@ -//! `extension_payload()` parser + encoder primitive — ISO/IEC -//! 14496-3 §4.4.2.7 / Table 4.51 plus the DRC -//! `dynamic_range_info()` body (Table 4.52) and the -//! `excluded_channels()` helper (Table 4.53), with -//! `extension_type` values per Table 4.59 (and ISO/IEC 13818-7 -//! Table 40, which extends the 14496-3 table with the SBR-data -//! values). -//! -//! `extension_payload()` is the structured body inside a FIL -//! element (`fill_element()`). The outer FIL surfaces a byte -//! count `cnt`; the `extension_payload(cnt)` reads exactly `cnt` -//! bytes — the first 4 bits select an `extension_type`, the -//! remaining bits carry the type-specific body. Three of the four -//! well-known `extension_type` values have fully fixed-width -//! Table 4.51 / 4.52 layouts and are implemented here: -//! -//! * `EXT_FILL` (`0b0000`) — bitstream filler. Body is -//! `8 * (cnt - 1) + 4` `other_bits`. No normative value -//! constraint per Table 4.51's `default` branch. -//! * `EXT_FILL_DATA` (`0b0001`) — bitstream data as filler. -//! Body is a 4-bit `fill_nibble` (normatively `0b0000`) -//! followed by `cnt - 1` × 8-bit `fill_byte` (each normatively -//! `0b10100101`). -//! * `EXT_DYNAMIC_RANGE` (`0b1011`) — dynamic range control. -//! Body is the Table 4.52 `dynamic_range_info()` block (see -//! [`DynamicRangeInfo`]). -//! -//! The SBR-data extension types defined by ISO/IEC 13818-7 Table 40 -//! are surfaced as [`Error::UnsupportedExtensionSbr`] by the default -//! [`ExtensionPayload::parse`] (so the byte-exact AAC-LC decode path -//! stays untouched). The dedicated [`ExtensionPayload::parse_with_sbr`] -//! entry instead routes them into the §4.4.2.8 -//! [`crate::sbr_extension::SbrExtensionData`] side-info walker (the SBR -//! back-end DSP is still not applied): -//! -//! * `EXT_SBR_DATA` (`0b1101`). -//! * `EXT_SBR_DATA_CRC` (`0b1110`). -//! -//! All other (reserved) values surface as -//! [`Error::UnsupportedExtensionType`] carrying the literal 4-bit -//! value as read from the wire. -//! -//! ## Why a parser / writer pair, and why now -//! -//! The Phase 1 `raw_data_block()` walker (round 121) recognises -//! FIL but skips its payload bytes opaque. Round 160's -//! `FrameAssembler::push_fill` accepts an opaque payload byte -//! slice. Neither side decodes or encodes the structured -//! `extension_payload()` body — and the FIL element is where the -//! DRC metadata (per-band gain factors), encoder-identifier fill -//! bytes, and the SBR enhancement bytes ride. This module is -//! the §4.4.2.7 wire-level decode/encode for the three non-SBR -//! extension types whose body layouts are fully specified by -//! fixed-width fields (no Huffman, no spectral context). The -//! intent is that downstream rounds plug this module into -//! `FrameAssembler::push_fill` / -//! `Walker::next_element` to surface a typed `extension_payload` -//! per FIL element. -//! -//! ## Returned byte count -//! -//! Per Table 4.51, `extension_payload()` returns the byte count -//! it consumed. Table 4.52's `dynamic_range_info()` returns its -//! own byte count starting from `n = 1` (the leading byte -//! containing the 4-bit `extension_type` nibble plus four of the -//! body's "presence" flags); each subsequent 8-bit-wide field set -//! is `n++`. [`ExtensionPayload::parse`] and [`ExtensionPayload::write`] -//! both expose this byte count via the returned -//! [`ExtensionPayload::bytes_consumed`] / [`ExtensionPayload::byte_length`] -//! accessors. -//! -//! ## What this module does *not* cover -//! -//! * No application of the DRC `(dyn_rng_sgn, dyn_rng_ctl)` gain -//! factors to the reconstructed audio. §4.5.2.13 specifies the -//! companding curve; this module surfaces the raw fields only. -//! * No semantic validation of `pce_instance_tag` against the -//! surrounding PCE (the surrounding PCE may not be known at -//! parse time — e.g. when the DRC FIL precedes the PCE in -//! independent-program multiplexes). -//! * The SBR-data extension types (Table 40 -//! `EXT_SBR_DATA` / `EXT_SBR_DATA_CRC`) are surfaced as -//! [`Error::UnsupportedExtensionSbr`] — their bodies are the -//! `sbr_extension_data()` syntax which needs the QMF / patching -//! back-end. This module's writer / parser deliberately does -//! *not* consume bits for these types so a future SBR round can -//! take over without a wire-format incompatibility. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::{Error, Result}; - -/// Width in bits of the wire `extension_type` field. ISO/IEC -/// 14496-3 Table 4.51. -pub const EXTENSION_TYPE_BITS: u32 = 4; - -/// Symbolic `extension_type` values per ISO/IEC 14496-3 Table 4.59 -/// plus the ISO/IEC 13818-7 Table 40 SBR-data extensions. -/// -/// Every variant maps to a single 4-bit wire value; the raw value -/// is exposed via [`ExtensionType::as_u8`] for round-tripping. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExtensionType { - /// `EXT_FILL` (`0b0000`) — bitstream filler. - Fill, - /// `EXT_FILL_DATA` (`0b0001`) — bitstream data as filler. - /// Normative payload: 4-bit `fill_nibble == 0b0000` followed - /// by `cnt - 1` × 8-bit `fill_byte == 0b10100101`. - FillData, - /// `EXT_DYNAMIC_RANGE` (`0b1011`) — dynamic range control. - /// Body is the Table 4.52 `dynamic_range_info()` block. - DynamicRange, - /// `EXT_SBR_DATA` (`0b1101`) — SBR enhancement (ISO/IEC - /// 13818-7 Table 40). This crate does not parse the - /// `sbr_extension_data()` body yet. - SbrData, - /// `EXT_SBR_DATA_CRC` (`0b1110`) — SBR enhancement with CRC - /// (ISO/IEC 13818-7 Table 40). This crate does not parse the - /// `sbr_extension_data()` body yet. - SbrDataCrc, -} - -impl ExtensionType { - /// Map a 4-bit wire value (`0..=15`) to the corresponding - /// [`ExtensionType`], or surface a structural error. - /// - /// Returns: - /// - /// * [`Error::UnsupportedExtensionSbr`] for `0b1101` - /// (`EXT_SBR_DATA`) and `0b1110` (`EXT_SBR_DATA_CRC`) — the - /// bodies are the SBR `sbr_extension_data()` syntax which - /// this crate does not parse. - /// * [`Error::UnsupportedExtensionType`] carrying the raw - /// 4-bit value for any other value not in - /// `{0b0000, 0b0001, 0b1011, 0b1101, 0b1110}`. Table 4.59 / - /// Table 40 list these as "reserved". - pub fn from_bits(value: u8) -> Result { - match value { - 0b0000 => Ok(ExtensionType::Fill), - 0b0001 => Ok(ExtensionType::FillData), - 0b1011 => Ok(ExtensionType::DynamicRange), - 0b1101 | 0b1110 => Err(Error::UnsupportedExtensionSbr(value)), - other if other <= 0x0f => Err(Error::UnsupportedExtensionType(other)), - // unreachable in practice — `read_u32(4)` produces 0..=15 - _ => Err(Error::UnsupportedExtensionType(value)), - } - } - - /// Like [`Self::from_bits`] but maps the two SBR wire values to - /// their [`ExtensionType`] variants instead of an error, so the - /// [`ExtensionPayload::parse_with_sbr`] entry can dispatch them into - /// the SBR side-info walker. Reserved values still error. - pub fn from_bits_allow_sbr(value: u8) -> Result { - match value { - 0b0000 => Ok(ExtensionType::Fill), - 0b0001 => Ok(ExtensionType::FillData), - 0b1011 => Ok(ExtensionType::DynamicRange), - 0b1101 => Ok(ExtensionType::SbrData), - 0b1110 => Ok(ExtensionType::SbrDataCrc), - other => Err(Error::UnsupportedExtensionType(other)), - } - } - - /// Convert back to the 4-bit wire value used by Table 4.51. - pub fn as_u8(self) -> u8 { - match self { - ExtensionType::Fill => 0b0000, - ExtensionType::FillData => 0b0001, - ExtensionType::DynamicRange => 0b1011, - ExtensionType::SbrData => 0b1101, - ExtensionType::SbrDataCrc => 0b1110, - } - } -} - -/// Normative `fill_byte` literal per ISO/IEC 14496-3 §4.4.2.7 / -/// Table 4.51 (`must be '10100101'`). Surfaced as a public constant -/// so callers and tests can refer to the same magic value. -pub const FILL_DATA_BYTE: u8 = 0b1010_0101; - -/// Normative `fill_nibble` literal per ISO/IEC 14496-3 §4.4.2.7 / -/// Table 4.51 (`must be '0000'`). -pub const FILL_DATA_NIBBLE: u8 = 0b0000; - -/// Parsed `extension_payload()` body (Table 4.51 dispatch). -/// -/// The body always carries the byte count it consumed (the `n` -/// returned by Table 4.51) so the surrounding FIL `cnt` can be -/// decremented in lockstep with the spec. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ExtensionPayload { - /// `EXT_FILL` — opaque filler. Carries the raw `8 * (cnt - 1) + 4` - /// "other_bits" packed MSB-first into a byte vector. The last - /// byte's low 4 bits are unused if `cnt > 0` (since the body - /// is not a whole number of bytes). - Fill { - /// Total bytes consumed by this `extension_payload`, - /// including the 4-bit `extension_type` nibble (so the - /// useful body is `8 * (cnt - 1) + 4` bits). - cnt: u32, - /// `other_bits` packed MSB-first. Empty when `cnt == 1` - /// (a 4-bit-only EXT_FILL whose body is 4 unused bits). - other_bits: Vec, - }, - /// `EXT_FILL_DATA` — normative-pattern filler. Carries the - /// byte count (FIL `cnt`) so the body length is implicit. - FillData { - /// Total bytes consumed (the FIL `cnt`). - cnt: u32, - }, - /// `EXT_DYNAMIC_RANGE` — DRC metadata per Table 4.52. - DynamicRange(DynamicRangeInfo), -} - -/// The result of [`ExtensionPayload::parse_with_sbr`]: either a standard -/// (non-SBR) extension payload, or a decoded SBR side-info element. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ExtensionPayloadOrSbr { - /// A non-SBR extension payload (`EXT_FILL` / `EXT_FILL_DATA` / - /// `EXT_DYNAMIC_RANGE`). - Payload(ExtensionPayload), - /// A decoded `sbr_extension_data()` (`EXT_SBR_DATA` / - /// `EXT_SBR_DATA_CRC`). Boxed because the SBR side-info element is - /// much larger than the other variants. - Sbr(Box), - /// An SBR payload received **before any `sbr_header()`** — the - /// stream opens with `bs_header_flag == 0` payloads and no header - /// has been threaded yet. Per ISO/IEC 14496-3:2009 §4.5.2.8.1 - /// ("As long as no SBR header part is present, the SBR decoder - /// performs upsampling and delay adjustment only") the `sbr_data()` - /// body cannot be parsed (its band tables come from the missing - /// header), so the payload is skipped whole; the caller should run - /// the §4.6.18.5 pure-upsampling path for the covered element. The - /// ISO/IEC 14496-26 `al_sbr_{e,i}_32_*` conformance vectors open - /// this way. - /// - /// `crc` / `crc_region` carry the `EXT_SBR_DATA_CRC` checksum and - /// its covered bit range (everything after the 10-bit CRC field up - /// to the end of the fill payload, per the §4.5.2.8.1 coverage - /// statement — with no parsed `sbr_data()` the `bs_fill_bits` - /// boundary is unknowable, and the whole-payload region is the - /// normative coverage); `None` for the plain `EXT_SBR_DATA` type. - SbrPreHeader { - /// Transmitted `bs_sbr_crc_bits`, when the CRC variant. - crc: Option, - /// Covered `[start, end)` bit range in the parse buffer. - crc_region: Option<(u64, u64)>, - }, -} - -/// Parsed `dynamic_range_info()` body (Table 4.52). All fields are -/// surfaced verbatim from the wire; the §4.5.2.13 companding curve -/// that maps `(dyn_rng_sgn, dyn_rng_ctl)` pairs to dB attenuations -/// is *not* applied here. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DynamicRangeInfo { - /// Optional PCE element-tag selector. `Some((tag, reserved))` - /// when `pce_tag_present == 1`. Both fields are 4 bits. - pub pce_tag: Option, - /// Optional excluded-channels list. `Some(_)` when - /// `excluded_chns_present == 1`. - pub excluded_channels: Option, - /// Optional per-band partitioning. `Some(_)` when - /// `drc_bands_present == 1`. When `None`, the spec sets - /// `drc_num_bands = 1` and there is a single - /// `(dyn_rng_sgn[0], dyn_rng_ctl[0])` pair below. - pub drc_bands: Option, - /// Optional 7-bit `prog_ref_level` reference level - /// (`Some((level, reserved))` when `prog_ref_level_present - /// == 1`). `reserved` is the trailing 1-bit reserved field. - pub prog_ref_level: Option, - /// Per-band `(dyn_rng_sgn, dyn_rng_ctl)` records, in wire - /// order. Length equals the resolved `drc_num_bands` - /// (`drc_bands.is_none()` ⇒ 1; otherwise - /// `1 + drc_bands.band_incr`). - pub bands: Vec, -} - -/// 4-bit `pce_instance_tag` + 4-bit `drc_tag_reserved_bits` pair -/// per Table 4.52. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PceTagFields { - /// `pce_instance_tag` — selects the surrounding PCE this DRC - /// applies to. 4 bits. - pub pce_instance_tag: u8, - /// `drc_tag_reserved_bits` — 4 bits, value not constrained by - /// the spec. - pub reserved: u8, -} - -/// `excluded_channels()` body (Table 4.53). Carries the resolved -/// `exclude_mask[]` bits packed MSB-first into a `Vec`. The -/// wire length is implied by the trailing -/// `additional_excluded_chns[n-1] == 0` flag — every 7 -/// `exclude_mask` bits are followed by a 1-bit continuation flag, -/// repeating until the continuation flag reads 0. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExcludedChannels { - /// `exclude_mask[i]` for `i = 0..(7 * n_groups)`, where - /// `n_groups` is the number of 8-bit-wide groups consumed. - pub exclude_mask: Vec, -} - -/// `drc_band_incr` + `drc_bands_reserved_bits` + `drc_band_top[]` -/// payload per Table 4.52. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DrcBands { - /// `drc_band_incr` — 4 bits. Resolved - /// `drc_num_bands = 1 + drc_band_incr`. - pub band_incr: u8, - /// `drc_bands_reserved_bits` — 4 bits, value not constrained - /// by the spec. - pub reserved: u8, - /// `drc_band_top[i]` — 8 bits per band. Length equals - /// `1 + band_incr`. - pub band_top: Vec, -} - -/// 7-bit `prog_ref_level` + 1-bit `prog_ref_level_reserved_bits` -/// pair per Table 4.52. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ProgRefLevelFields { - /// `prog_ref_level` — 7 bits. Reference level for downstream - /// loudness normalisation. - pub level: u8, - /// `prog_ref_level_reserved_bits` — 1 bit. - pub reserved: bool, -} - -/// Per-band `(dyn_rng_sgn, dyn_rng_ctl)` pair per Table 4.52. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DrcBandRecord { - /// `dyn_rng_sgn[i]` — 1-bit sign. `true` ⇒ negative gain (cut). - pub dyn_rng_sgn: bool, - /// `dyn_rng_ctl[i]` — 7-bit magnitude in 0.25 dB steps per - /// §4.5.2.13. Surfaced verbatim here. - pub dyn_rng_ctl: u8, -} - -impl ExtensionPayload { - /// Parse an `extension_payload(cnt)` from `reader`. - /// - /// `cnt` is the FIL element's payload byte count after the - /// §4.4.2.7 `esc_count` escape resolution (the same value the - /// existing [`crate::raw_data_block::Walker`] computes via - /// `read_fill_count`). `cnt == 0` is rejected as - /// [`Error::ExtensionPayloadInvalid`] — Table 4.51's - /// `extension_type` field itself is 4 bits, so a zero-byte FIL - /// has no room for it. - pub fn parse(reader: &mut BitReader<'_>, cnt: u32) -> Result { - if cnt == 0 { - return Err(Error::ExtensionPayloadInvalid); - } - let raw = read_u8(reader, EXTENSION_TYPE_BITS)?; - let ty = ExtensionType::from_bits(raw)?; - match ty { - ExtensionType::Fill => parse_fill(reader, cnt), - ExtensionType::FillData => parse_fill_data(reader, cnt), - ExtensionType::DynamicRange => parse_dynamic_range(reader, cnt), - // `from_bits` already converted these to errors. - ExtensionType::SbrData | ExtensionType::SbrDataCrc => unreachable!(), - } - } - - /// Parse an `extension_payload(cnt)`, routing the two SBR extension - /// types (`EXT_SBR_DATA` / `EXT_SBR_DATA_CRC`) into the - /// [`crate::sbr_extension::SbrExtensionData`] side-info walker rather - /// than rejecting them. - /// - /// Unlike [`Self::parse`] (which surfaces - /// [`Error::UnsupportedExtensionSbr`] for the SBR types so the - /// byte-exact AAC-LC decode path stays untouched), this entry decodes - /// the SBR bitstream side info: the §4.4.2.8 `sbr_extension_data()` - /// header + element framing keyed off the surrounding channel - /// element. The SBR back-end DSP (QMF / HF patching / envelope - /// adjustment) is still not applied — this only recovers the decoded - /// side info. - /// - /// * `id_aac` — the AAC core element this FIL follows - /// ([`crate::raw_data_block::IdSynEle::Sce`] / `Cpe`); selects the - /// single- vs pair-element `sbr_data()` dispatch. - /// * `fs_sbr` — the SBR internal sample rate (twice the core rate). - /// * `prev_header` — the threaded previous `sbr_header()` for the - /// `bs_header_flag == 0` reuse path (`None` on the first payload). - /// - /// A non-SBR extension type returns - /// [`ExtensionPayloadOrSbr::Payload`] with the same body - /// [`Self::parse`] would produce. - pub fn parse_with_sbr( - reader: &mut BitReader<'_>, - cnt: u32, - id_aac: crate::raw_data_block::IdSynEle, - fs_sbr: u32, - prev_header: Option, - ) -> Result { - if cnt == 0 { - return Err(Error::ExtensionPayloadInvalid); - } - let nibble_start = reader.bit_position(); - let raw = read_u8(reader, EXTENSION_TYPE_BITS)?; - let ty = ExtensionType::from_bits_allow_sbr(raw)?; - match ty { - ExtensionType::Fill => Ok(ExtensionPayloadOrSbr::Payload(parse_fill(reader, cnt)?)), - ExtensionType::FillData => Ok(ExtensionPayloadOrSbr::Payload(parse_fill_data( - reader, cnt, - )?)), - ExtensionType::DynamicRange => Ok(ExtensionPayloadOrSbr::Payload(parse_dynamic_range( - reader, cnt, - )?)), - ExtensionType::SbrData | ExtensionType::SbrDataCrc => { - let crc_flag = ty == ExtensionType::SbrDataCrc; - if prev_header.is_none() { - // Peek the CRC field + bs_header_flag without - // committing: a header-less payload before the - // first sbr_header() cannot be parsed (§4.5.2.8.1 - // — upsampling and delay adjustment only), so the - // payload is skipped whole with its CRC surfaced. - let crc = if crc_flag { - Some(reader.read_u32(10).map_err(|_| Error::UnexpectedEnd)? as u16) - } else { - None - }; - let region_start = reader.bit_position(); - let header_flag = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - if !header_flag { - let end = nibble_start + u64::from(cnt) * 8; - let mut pos = reader.bit_position(); - while pos < end { - let step = (end - pos).min(32) as u32; - reader.read_u32(step).map_err(|_| Error::UnexpectedEnd)?; - pos += u64::from(step); - } - return Ok(ExtensionPayloadOrSbr::SbrPreHeader { - crc, - crc_region: crc.map(|_| (region_start, end)), - }); - } - // A header is present after all: re-parse through - // the normal path from the header flag onward. - let sbr = crate::sbr_extension::SbrExtensionData::parse_after_prefix( - reader, - id_aac, - crc, - nibble_start, - fs_sbr, - Some(cnt), - None, - )?; - return Ok(ExtensionPayloadOrSbr::Sbr(Box::new(sbr))); - } - let sbr = crate::sbr_extension::SbrExtensionData::parse( - reader, - id_aac, - crc_flag, - fs_sbr, - Some(cnt), - prev_header, - )?; - Ok(ExtensionPayloadOrSbr::Sbr(Box::new(sbr))) - } - } - } - - /// Encode an `extension_payload()` body onto `writer` — the - /// bit-exact inverse of [`ExtensionPayload::parse`]. - /// - /// Returns the byte count consumed (matching Table 4.51's - /// returned `n`). Surfaces caller-side field violations as - /// [`Error::ExtensionPayloadInvalid`]. - pub fn write(&self, writer: &mut BitWriter) -> Result { - match self { - ExtensionPayload::Fill { cnt, other_bits } => write_fill(writer, *cnt, other_bits), - ExtensionPayload::FillData { cnt } => write_fill_data(writer, *cnt), - ExtensionPayload::DynamicRange(drc) => write_dynamic_range(writer, drc), - } - } - - /// Total byte count this `extension_payload` consumed on the - /// wire — Table 4.51's returned `n`. - pub fn byte_length(&self) -> u32 { - match self { - ExtensionPayload::Fill { cnt, .. } => *cnt, - ExtensionPayload::FillData { cnt } => *cnt, - ExtensionPayload::DynamicRange(drc) => drc.byte_length(), - } - } -} - -impl DynamicRangeInfo { - /// Byte count this DRC body consumes — Table 4.52's returned - /// `n`, including the 4-bit `extension_type` nibble that the - /// outer `extension_payload()` writes immediately before the - /// DRC body. - pub fn byte_length(&self) -> u32 { - // Start from n = 1 (the leading byte containing the 4-bit - // extension_type + 4 presence flags). - let mut n: u32 = 1; - if self.pce_tag.is_some() { - n += 1; - } - if let Some(ex) = &self.excluded_channels { - // Each group is 7 mask bits + 1 continuation bit = 1 byte. - n += excluded_group_count(ex.exclude_mask.len()) as u32; - } - if let Some(b) = &self.drc_bands { - // drc_band_incr + reserved = 1 byte, then 1 byte per - // drc_band_top entry. - n += 1 + b.band_top.len() as u32; - } - if self.prog_ref_level.is_some() { - n += 1; - } - // 1 byte per (dyn_rng_sgn + dyn_rng_ctl). - n += self.bands.len() as u32; - n - } - - /// Resolved `drc_num_bands` per Table 4.52. Always equals - /// `bands.len()`. - pub fn num_bands(&self) -> usize { - self.bands.len() - } -} - -// =================================================================== -// EXT_FILL parser / writer -// =================================================================== - -fn parse_fill(reader: &mut BitReader<'_>, cnt: u32) -> Result { - // Table 4.51 default branch: - // for (i = 0; i < 8*(cnt-1) + 4; i++) other_bits[i]; - // 4 of those bits are already consumed (the extension_type - // nibble — except wait, no: the 8*(cnt-1)+4 count is the bits - // AFTER the extension_type. Re-reading the spec carefully — - // Table 4.51 reads extension_type FIRST, then enters the - // switch; the default branch's loop counts the body AFTER the - // type nibble. The total bits consumed is then - // 4 + 8*(cnt-1) + 4 = 8 * cnt — consistent with returning cnt. - let body_bits = 8u32 - .checked_mul(cnt.saturating_sub(1)) - .ok_or(Error::ExtensionPayloadInvalid)? - .checked_add(4) - .ok_or(Error::ExtensionPayloadInvalid)?; - let other_bits = read_packed_bits(reader, body_bits)?; - Ok(ExtensionPayload::Fill { cnt, other_bits }) -} - -fn write_fill(writer: &mut BitWriter, cnt: u32, other_bits: &[u8]) -> Result { - if cnt == 0 { - return Err(Error::ExtensionPayloadInvalid); - } - let body_bits = 8u32 - .checked_mul(cnt.saturating_sub(1)) - .ok_or(Error::ExtensionPayloadInvalid)? - .checked_add(4) - .ok_or(Error::ExtensionPayloadInvalid)?; - let expected_bytes = (body_bits as usize).div_ceil(8); - if other_bits.len() != expected_bytes { - return Err(Error::ExtensionPayloadInvalid); - } - writer.write_u32(ExtensionType::Fill.as_u8() as u32, EXTENSION_TYPE_BITS); - write_packed_bits(writer, other_bits, body_bits)?; - Ok(cnt) -} - -// =================================================================== -// EXT_FILL_DATA parser / writer -// =================================================================== - -fn parse_fill_data(reader: &mut BitReader<'_>, cnt: u32) -> Result { - // Table 4.51: - // fill_nibble; 4 bits /* must be '0000' */ - // for (i = 0; i < cnt - 1; i++) - // fill_byte[i]; 8 bits /* must be '10100101' */ - let nibble = read_u8(reader, 4)?; - if nibble != FILL_DATA_NIBBLE { - return Err(Error::ExtensionPayloadInvalid); - } - let body_bytes = cnt.saturating_sub(1) as usize; - for _ in 0..body_bytes { - let b = read_u8(reader, 8)?; - if b != FILL_DATA_BYTE { - return Err(Error::ExtensionPayloadInvalid); - } - } - Ok(ExtensionPayload::FillData { cnt }) -} - -fn write_fill_data(writer: &mut BitWriter, cnt: u32) -> Result { - if cnt == 0 { - return Err(Error::ExtensionPayloadInvalid); - } - writer.write_u32(ExtensionType::FillData.as_u8() as u32, EXTENSION_TYPE_BITS); - writer.write_u32(FILL_DATA_NIBBLE as u32, 4); - let body_bytes = cnt.saturating_sub(1) as usize; - for _ in 0..body_bytes { - writer.write_u32(FILL_DATA_BYTE as u32, 8); - } - Ok(cnt) -} - -// =================================================================== -// EXT_DYNAMIC_RANGE parser / writer -// =================================================================== - -fn parse_dynamic_range(reader: &mut BitReader<'_>, cnt: u32) -> Result { - let pce_tag_present = read_bit(reader)?; - let pce_tag = if pce_tag_present { - let pce_instance_tag = read_u8(reader, 4)?; - let reserved = read_u8(reader, 4)?; - Some(PceTagFields { - pce_instance_tag, - reserved, - }) - } else { - None - }; - - let excluded_chns_present = read_bit(reader)?; - let excluded_channels = if excluded_chns_present { - Some(parse_excluded_channels(reader)?) - } else { - None - }; - - let drc_bands_present = read_bit(reader)?; - let drc_bands = if drc_bands_present { - let band_incr = read_u8(reader, 4)?; - let reserved = read_u8(reader, 4)?; - let num_bands = 1usize + band_incr as usize; - let mut band_top = Vec::with_capacity(num_bands); - for _ in 0..num_bands { - band_top.push(read_u8(reader, 8)?); - } - Some(DrcBands { - band_incr, - reserved, - band_top, - }) - } else { - None - }; - - let prog_ref_level_present = read_bit(reader)?; - let prog_ref_level = if prog_ref_level_present { - let level = read_u8(reader, 7)?; - let reserved = read_bit(reader)?; - Some(ProgRefLevelFields { level, reserved }) - } else { - None - }; - - let num_bands = drc_bands - .as_ref() - .map(|b| 1 + b.band_incr as usize) - .unwrap_or(1); - let mut bands = Vec::with_capacity(num_bands); - for _ in 0..num_bands { - let dyn_rng_sgn = read_bit(reader)?; - let dyn_rng_ctl = read_u8(reader, 7)?; - bands.push(DrcBandRecord { - dyn_rng_sgn, - dyn_rng_ctl, - }); - } - - let drc = DynamicRangeInfo { - pce_tag, - excluded_channels, - drc_bands, - prog_ref_level, - bands, - }; - if drc.byte_length() != cnt { - // The dispatching FIL `cnt` and the derived Table 4.52 `n` - // must agree byte-for-byte — Table 4.52 normatively - // returns the byte count to the caller. A mismatch - // indicates a malformed bitstream. - return Err(Error::ExtensionPayloadInvalid); - } - Ok(ExtensionPayload::DynamicRange(drc)) -} - -fn write_dynamic_range(writer: &mut BitWriter, drc: &DynamicRangeInfo) -> Result { - // Caller-side invariant checks (every numeric field cap from - // Table 4.52). - if let Some(p) = &drc.pce_tag { - if p.pce_instance_tag > 0x0f || p.reserved > 0x0f { - return Err(Error::ExtensionPayloadInvalid); - } - } - if let Some(b) = &drc.drc_bands { - if b.band_incr > 0x0f || b.reserved > 0x0f { - return Err(Error::ExtensionPayloadInvalid); - } - if b.band_top.len() != 1 + b.band_incr as usize { - return Err(Error::ExtensionPayloadInvalid); - } - } - if let Some(p) = &drc.prog_ref_level { - if p.level > 0x7f { - return Err(Error::ExtensionPayloadInvalid); - } - } - let expected_bands = drc - .drc_bands - .as_ref() - .map(|b| 1 + b.band_incr as usize) - .unwrap_or(1); - if drc.bands.len() != expected_bands { - return Err(Error::ExtensionPayloadInvalid); - } - for r in &drc.bands { - if r.dyn_rng_ctl > 0x7f { - return Err(Error::ExtensionPayloadInvalid); - } - } - - writer.write_u32( - ExtensionType::DynamicRange.as_u8() as u32, - EXTENSION_TYPE_BITS, - ); - - writer.write_bit(drc.pce_tag.is_some()); - if let Some(p) = &drc.pce_tag { - writer.write_u32(p.pce_instance_tag as u32, 4); - writer.write_u32(p.reserved as u32, 4); - } - - writer.write_bit(drc.excluded_channels.is_some()); - if let Some(ex) = &drc.excluded_channels { - write_excluded_channels(writer, ex)?; - } - - writer.write_bit(drc.drc_bands.is_some()); - if let Some(b) = &drc.drc_bands { - writer.write_u32(b.band_incr as u32, 4); - writer.write_u32(b.reserved as u32, 4); - for &top in &b.band_top { - writer.write_u32(top as u32, 8); - } - } - - writer.write_bit(drc.prog_ref_level.is_some()); - if let Some(p) = &drc.prog_ref_level { - writer.write_u32(p.level as u32, 7); - writer.write_bit(p.reserved); - } - - for r in &drc.bands { - writer.write_bit(r.dyn_rng_sgn); - writer.write_u32(r.dyn_rng_ctl as u32, 7); - } - - Ok(drc.byte_length()) -} - -// =================================================================== -// excluded_channels() helper (Table 4.53) -// =================================================================== - -fn parse_excluded_channels(reader: &mut BitReader<'_>) -> Result { - // Table 4.53: each iteration reads 7 exclude_mask bits + 1 - // additional_excluded_chns continuation flag = 1 byte. Stop - // when the continuation flag reads 0. - let mut exclude_mask = Vec::new(); - loop { - for _ in 0..7 { - exclude_mask.push(read_bit(reader)?); - } - let cont = read_bit(reader)?; - if !cont { - break; - } - } - Ok(ExcludedChannels { exclude_mask }) -} - -fn write_excluded_channels(writer: &mut BitWriter, ex: &ExcludedChannels) -> Result<()> { - if ex.exclude_mask.is_empty() || ex.exclude_mask.len() % 7 != 0 { - // Table 4.53 emits exclude_mask bits in fixed groups of 7; - // any non-multiple-of-7 length cannot round-trip through - // [`parse_excluded_channels`]. - return Err(Error::ExtensionPayloadInvalid); - } - let groups = ex.exclude_mask.len() / 7; - for g in 0..groups { - for i in 0..7 { - writer.write_bit(ex.exclude_mask[g * 7 + i]); - } - // The continuation flag is 1 for every group except the - // last, which carries 0 to terminate. - let last = g + 1 == groups; - writer.write_bit(!last); - } - Ok(()) -} - -/// Resolved byte count for an `excluded_channels()` body carrying -/// the given total `exclude_mask` bit count. Exposed so callers can -/// pre-size `cnt` without round-tripping through -/// [`DynamicRangeInfo::byte_length`]. -pub fn excluded_group_count(exclude_mask_len: usize) -> usize { - // The spec emits 7-bit groups; the byte count equals the group - // count (each group is 7 mask bits + 1 continuation bit). - exclude_mask_len.div_ceil(7) -} - -// =================================================================== -// Helpers -// =================================================================== - -fn read_packed_bits(reader: &mut BitReader<'_>, n_bits: u32) -> Result> { - let n_bytes = (n_bits as usize).div_ceil(8); - let mut out = vec![0u8; n_bytes]; - let mut remaining = n_bits; - let mut idx = 0; - while remaining >= 8 { - out[idx] = read_u8(reader, 8)?; - idx += 1; - remaining -= 8; - } - if remaining > 0 { - // Pack the trailing partial byte into the top bits of the - // last output byte. - let partial = read_u8(reader, remaining)?; - out[idx] = partial << (8 - remaining); - } - Ok(out) -} - -fn write_packed_bits(writer: &mut BitWriter, bytes: &[u8], n_bits: u32) -> Result<()> { - let mut remaining = n_bits; - let mut idx = 0; - while remaining >= 8 { - writer.write_u32(bytes[idx] as u32, 8); - idx += 1; - remaining -= 8; - } - if remaining > 0 { - // The trailing partial byte stores its bits in the top - // `remaining` bits; recover them with a right-shift. - let partial = bytes[idx] >> (8 - remaining); - writer.write_u32(partial as u32, remaining); - } - Ok(()) -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} diff --git a/crates/vendor/oxideav-aac/src/filterbank.rs b/crates/vendor/oxideav-aac/src/filterbank.rs deleted file mode 100644 index caf9865c..00000000 --- a/crates/vendor/oxideav-aac/src/filterbank.rs +++ /dev/null @@ -1,1549 +0,0 @@ -//! §4.6.11 Filterbank and block switching — the inverse modified -//! discrete cosine transform (IMDCT), the analysis/synthesis windows -//! (sine and Kaiser-Bessel-derived), and the overlap-add that maps a -//! window-major decoded spectrum back to the time domain. -//! -//! This is the last stage of the per-channel decode chain -//! ([`crate::decoded_spectrum::decode_channel_spectrum`]) for a -//! single channel: it consumes the `num_windows × window_len = 1024` -//! window-major coefficients and emits 1024 PCM-domain samples per -//! frame after overlap-adding against the previous frame's tail. -//! -//! Spec basis (ISO/IEC 14496-3:2001, §4.6.11): -//! -//! * §4.6.11.3.1 — the IMDCT -//! `x[n] = (2/N) · Σ_k spec[k] · cos((2π/N)·(n + n0)·(k + 1/2))` -//! for `0 ≤ n < N`, with `n0 = (N/2 + 1)/2`. `N` is the -//! *transform* window length (2048 for long sequences, 256 for each -//! of the eight short windows). The crate carries the spectrum at -//! `N/2` resolution (1024 long, 128 short) as -//! [`crate::swb_offset::LONG_WINDOW_LEN`] / -//! [`crate::swb_offset::SHORT_WINDOW_LEN`]. -//! * §4.6.11.3.2 — windowing and block switching. The sine window is -//! `W_SIN(n) = sin((π/N)·(n + 1/2))`; the KBD window is the -//! normalized running sum of the Kaiser-Bessel kernel `W'(n, α)` -//! with `α = 4` for the long transform and `α = 6` for the short -//! transform. The four `window_sequence` shapes -//! (`ONLY_LONG`, `LONG_START`, `EIGHT_SHORT`, `LONG_STOP`) compose -//! left/right window halves; the left half's shape is inherited -//! from the *previous* block's `window_shape`. -//! * §4.6.11.3.3 — the inter-block overlap-add -//! `out[n] = z[i][n] + z[i-1][n + N/2]` for `0 ≤ n < N/2`, -//! `N = 2048`, valid for all four sequences. -//! -//! The frame-length-960 (`N = 1920 / 240`) variant of the spec is -//! out of scope: the rest of the crate's `swb_offset` tables and -//! transmission-order machinery are wired to the 1024-coefficient -//! layout, so this module mirrors that and only implements the 2048 -//! transform family. - -use crate::ics_info::{IcsInfo, WindowSequence, WindowShape}; -use crate::swb_offset::{FrameFamily, LONG_WINDOW_LEN, SHORT_WINDOW_LEN}; -use crate::Error; - -/// `N` for a long-sequence transform (§4.6.11.3.1): 2 × -/// [`LONG_WINDOW_LEN`]. -const LONG_TRANSFORM_LEN: usize = 2 * LONG_WINDOW_LEN as usize; // 2048 -/// `N` for a single short-sequence transform: 2 × -/// [`SHORT_WINDOW_LEN`]. -const SHORT_TRANSFORM_LEN: usize = 2 * SHORT_WINDOW_LEN as usize; // 256 -/// `M = N_l / N_s` = number of short windows in an `EIGHT_SHORT` -/// sequence. -const NUM_SHORT_WINDOWS: usize = 8; -/// `N_l` — the long transform length, used as the frame's PCM stride. -const N_L: usize = LONG_TRANSFORM_LEN; // 2048 -/// `N_s` — the short transform length. -const N_S: usize = SHORT_TRANSFORM_LEN; // 256 - -/// Result of [`Filterbank::synthesize`]: one frame of -/// `LONG_WINDOW_LEN` (1024) PCM-domain samples for a single channel. -type Result = core::result::Result; - -/// §4.6.11.3.1 — inverse MDCT for a length-`n_transform` window. -/// -/// `spec` holds the `N/2` transmitted coefficients; the returned -/// vector holds the `N` time-domain values -/// `x[n] = (2/N) · Σ_k spec[k] · cos((2π/N)·(n + n0)·(k + 1/2))`. -/// -/// `n0 = (N/2 + 1)/2` is the §4.6.11.3.1 phase offset. The `2/N` -/// scale and the half-coefficient phase are the only normalization -/// the spec attaches to the inverse transform; the energy-correcting -/// window then follows in the per-sequence windowing step. -pub(crate) fn imdct(spec: &[f64], n_transform: usize) -> Vec { - let half = n_transform / 2; - debug_assert_eq!(spec.len(), half); - let n0 = (half + 1) as f64 / 2.0; - let scale = 2.0 / n_transform as f64; - let phase_step = 2.0 * core::f64::consts::PI / n_transform as f64; - let mut out = vec![0.0f64; n_transform]; - for (n, slot) in out.iter_mut().enumerate() { - let np = n as f64 + n0; - let mut acc = 0.0f64; - for (k, &c) in spec.iter().enumerate() { - acc += c * (phase_step * np * (k as f64 + 0.5)).cos(); - } - *slot = scale * acc; - } - out -} - -/// §4.6.15.3.3 / §4.6.11.3.1 — the forward (analysis) MDCT for a -/// length-`n_transform` window. -/// -/// `time` holds the `N` windowed time-domain values `z[n]`; the -/// returned vector holds the `N/2` spectral coefficients -/// `X[k] = 2 · Σ_n z[n] · cos((2π/N)·(n + n0)·(k + 1/2))`, -/// `0 ≤ k < N/2`, with the §4.6.11.3.1 phase `n0 = (N/2 + 1)/2`. -/// -/// This is the exact analysis pair of [`imdct`]: the IMDCT carries the -/// `2/N` scale, the analysis here carries the matching factor `2`, so -/// the windowed-and-overlap-added round trip is unity for a -/// power-complementary §4.6.11.3.2 window. The same transform is the -/// `MDCT(x_est)` of the §4.6.7.3 Long-Term-Prediction loop. -pub(crate) fn forward_mdct(time: &[f64], n_transform: usize) -> Vec { - let half = n_transform / 2; - debug_assert_eq!(time.len(), n_transform); - let n0 = (half + 1) as f64 / 2.0; - let step = 2.0 * core::f64::consts::PI / n_transform as f64; - (0..half) - .map(|k| { - 2.0 * time - .iter() - .enumerate() - .map(|(n, &t)| t * (step * (n as f64 + n0) * (k as f64 + 0.5)).cos()) - .sum::() - }) - .collect() -} - -/// §4.6.11.3.2 — build the `ONLY_LONG_SEQUENCE` analysis window -/// `[W_LEFT_l | W_RIGHT_l]` at the family's long transform length, -/// with the family's window style (the LD families map -/// `window_shape == 1` to the §4.6.17.2.3 low-overlap window). -/// -/// Exposed for the §4.6.7.3 LTP loop, which windows the predicted time -/// signal `x_est` with the current long window before the analysis -/// [`forward_mdct`]. (LTP is restricted to long windows, §4.6.7.1.) -pub(crate) fn long_only_window_family( - family: FrameFamily, - left_shape: WindowShape, - right_shape: WindowShape, -) -> Vec { - let n_l = family.long_transform_len(); - let halves = window_halves_style( - n_l, - left_shape, - right_shape, - WindowStyle::for_family(family), - ); - let half_l = n_l / 2; - let mut w = vec![0.0f64; n_l]; - w[..half_l].copy_from_slice(&halves.left); - for (m, &rv) in halves.right.iter().enumerate() { - w[half_l + m] = rv; - } - w -} - -/// §4.6.11.3.2 — assemble the length-2048 window for any of the -/// three long-transform sequences. The window is shared between the -/// decoder's synthesis ([`Filterbank::long_window`] delegates here) -/// and the encoder's analysis (the §4.6.11 filterbank is its own -/// transpose up to the TDAC fold, so the same window applies on both -/// sides). Returns [`Error::FilterbankInvalid`] for -/// `EIGHT_SHORT_SEQUENCE` — use [`short_window_j`] per short window -/// instead. -pub(crate) fn long_sequence_window( - sequence: WindowSequence, - left_shape: WindowShape, - right_shape: WindowShape, -) -> Result> { - long_sequence_window_n(N_L, N_S, sequence, left_shape, right_shape) -} - -/// §4.6.11.3.2 — the [`long_sequence_window`] construction generalized -/// to an arbitrary `(n_l, n_s)` transform family. The SSR gain-control -/// filterbank (§4.6.12.1) runs the same window geometry at -/// `(512, 64)` — one quarter of the standard family — per band. -pub(crate) fn long_sequence_window_n( - n_l: usize, - n_s: usize, - sequence: WindowSequence, - left_shape: WindowShape, - right_shape: WindowShape, -) -> Result> { - let kind = match sequence { - WindowSequence::OnlyLong => LongKind::OnlyLong, - WindowSequence::LongStart => LongKind::Start, - WindowSequence::LongStop => LongKind::Stop, - WindowSequence::EightShort => return Err(Error::FilterbankInvalid), - }; - Ok(build_long_window_n(n_l, n_s, left_shape, right_shape, kind)) -} - -/// §4.6.11.3.2 c) — the length-256 window of short window `j` -/// (`0..8`) inside an `EIGHT_SHORT_SEQUENCE` frame: window 0's left -/// half inherits the previous block's shape, all other halves use -/// this block's shape. -pub(crate) fn short_window_j( - j: usize, - left_shape: WindowShape, - right_shape: WindowShape, -) -> Vec { - short_window_n(N_S, j, left_shape, right_shape) -} - -/// §4.6.11.3.2 c) — [`short_window_j`] generalized to an arbitrary -/// short-transform length `n_s` (64 for the SSR §4.6.12.1 per-band -/// family). -pub(crate) fn short_window_n( - n_s: usize, - j: usize, - left_shape: WindowShape, - right_shape: WindowShape, -) -> Vec { - let this_left = if j == 0 { left_shape } else { right_shape }; - let halves = window_halves(n_s, this_left, right_shape); - let mut w = vec![0.0f64; n_s]; - w[..n_s / 2].copy_from_slice(&halves.left); - for (m, &rv) in halves.right.iter().enumerate() { - w[n_s / 2 + m] = rv; - } - w -} - -/// §4.6.11.3.2 c) — offset of short window 0 inside the 2048-sample -/// frame window region: `(N_l − N_s)/4 = 448`. -pub(crate) const SHORT_SEQ_START: usize = (N_L - N_S) / 4; - -/// §4.6.11.3.2 c) — hop between successive short windows: -/// `N_s/2 = 128`. -pub(crate) const SHORT_SEQ_HOP: usize = N_S / 2; - -/// Modified Bessel function of the first kind, order 0, via its power -/// series `I0(x) = Σ_k ((x/2)^k / k!)^2` (§4.6.11.3.2). The series -/// converges quickly for the `x = π·α` arguments the KBD window uses -/// (`α ∈ {4, 6}`), so a fixed term cap with an early-out on negligible -/// terms is exact to f64 precision. -fn bessel_i0(x: f64) -> f64 { - let half_x = x / 2.0; - let mut term = 1.0f64; // k = 0 term: (half_x^0 / 0!)^2 = 1 - let mut sum = 1.0f64; - let mut k = 1.0f64; - loop { - // term_k = term_{k-1} · (half_x / k)^2 - term *= (half_x / k) * (half_x / k); - sum += term; - if term <= sum * 1e-18 { - break; - } - k += 1.0; - if k > 256.0 { - break; - } - } - sum -} - -/// §4.6.11.3.2 — the Kaiser-Bessel kernel -/// `W'(n, α) = I0(π·α·sqrt(1 − ((n − N/4)/(N/4))^2)) / I0(π·α)` -/// for `0 ≤ n ≤ N/2`, evaluated over `0..=half` (`half = N/2`). -fn kbd_kernel(half: usize, alpha: f64) -> Vec { - let quarter = half as f64 / 2.0; // N/4 - let denom = bessel_i0(core::f64::consts::PI * alpha); - (0..=half) - .map(|n| { - let t = (n as f64 - quarter) / quarter; - let radicand = (1.0 - t * t).max(0.0); - bessel_i0(core::f64::consts::PI * alpha * radicand.sqrt()) / denom - }) - .collect() -} - -/// §4.6.11.3.2 — the left half of the KBD window: -/// `W_KBD_LEFT(n) = sqrt( Σ_{p=0..n} W'(p) / Σ_{p=0..N/2} W'(p) )` -/// for `0 ≤ n < N/2`. Returns the `half = N/2` left-half samples. -/// -/// `alpha` is 4 for the long transform and 6 for the short transform. -fn kbd_left(half: usize, alpha: f64) -> Vec { - let kernel = kbd_kernel(half, alpha); - let total: f64 = kernel.iter().sum(); - let mut running = 0.0f64; - let mut out = Vec::with_capacity(half); - for &w in kernel.iter().take(half) { - running += w; - out.push((running / total).sqrt()); - } - out -} - -/// §4.6.11.3.2 — the sine window left half -/// `W_SIN_LEFT(n) = sin((π/N)·(n + 1/2))`, `0 ≤ n < N/2`. Returns the -/// `half = N/2` samples. -fn sine_left(half: usize) -> Vec { - let n_transform = (2 * half) as f64; - (0..half) - .map(|n| (core::f64::consts::PI / n_transform * (n as f64 + 0.5)).sin()) - .collect() -} - -/// One transform's analysis/synthesis window halves, each `half = N/2` -/// long. The right half of a sine/KBD window is the mirror of its -/// left half (`W_RIGHT(n) = W_LEFT(N − 1 − n)`), so we store left -/// halves and index the right half by mirror at apply time. -struct WindowHalves { - /// Left half, indices `0..half`. - left: Vec, - /// Right half, indices `0..half`; element `m` is the window value - /// at transform position `half + m`. - right: Vec, -} - -/// §4.6.17.2.3 Table 4.171 — the ER AAC LD *low-overlap* window's -/// left half. Over the full length-`N` window: -/// -/// ```text -/// W(i) = 0 i in [0, 3N/16) -/// sin(π(i − 3N/16 + 0.5) / (N/4)) i in [3N/16, 5N/16) -/// 1 i in [5N/16, 11N/16) -/// sin(π(i − 9N/16 + 0.5) / (N/4)) i in [11N/16, 13N/16) -/// 0 i in [13N/16, N) -/// ``` -/// -/// The two sine segments' arguments sum to π at mirrored positions -/// (`i` and `N − 1 − i`), so the right half is the exact spatial -/// mirror of this left half — the same mirror convention every other -/// window shape uses — and the TDAC partners inside the rise region -/// have arguments summing to π/2, making the window -/// power-complementary (`sin² + cos² = 1`), as §4.6.11.3.2 requires -/// for perfect reconstruction. -fn low_overlap_left(half: usize) -> Vec { - let n = 2 * half; // full window length N (1024 or 960) - let rise_start = 3 * n / 16; - let rise_end = 5 * n / 16; - let quarter = n as f64 / 4.0; - (0..half) - .map(|i| { - if i < rise_start { - 0.0 - } else if i < rise_end { - (core::f64::consts::PI * (i as f64 - rise_start as f64 + 0.5) / quarter).sin() - } else { - 1.0 - } - }) - .collect() -} - -/// Which window family the `window_shape` bit selects between — -/// §4.6.11.3.2 (sine / KBD) for the general families, §4.6.17.2.3 -/// Table 4.171 (sine / low-overlap) for ER AAC LD. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) enum WindowStyle { - /// `window_shape == 1` selects the Kaiser-Bessel-derived window. - Standard, - /// `window_shape == 1` selects the §4.6.17.2.3 low-overlap - /// window (ER AAC LD). - LowDelay, -} - -impl WindowStyle { - /// The style a [`FrameFamily`] mandates. - pub(crate) fn for_family(family: FrameFamily) -> Self { - if family.is_ld() { - WindowStyle::LowDelay - } else { - WindowStyle::Standard - } - } -} - -/// Build the left half for the requested `shape` at transform length -/// `n_transform` under a [`WindowStyle`]. -/// -/// The KBD kernel alpha follows the transform's *role*: the long -/// transform of a family uses `α = 4`, the short transform `α = 6`. -/// §4.6.11.3.2 states this for the 2048/256 (1920/240) family; for the -/// SSR 512/64 family the same pair reproduces the normative -/// Table 4.A.14 / Table 4.A.13 window listings (each printed value -/// matches the α = 4 / α = 6 running-sum construction to the tables' -/// print precision — pinned by the `ssr_kbd_*` tests below). Under -/// [`WindowStyle::LowDelay`] the `window_shape == 1` bit selects the -/// §4.6.17.2.3 low-overlap window instead of KBD. -fn half_window_style(n_transform: usize, shape: WindowShape, style: WindowStyle) -> Vec { - let half = n_transform / 2; - match (shape, style) { - (WindowShape::Sine, _) => sine_left(half), - (WindowShape::Kbd, WindowStyle::LowDelay) => low_overlap_left(half), - (WindowShape::Kbd, WindowStyle::Standard) => { - let alpha = match n_transform { - // Long transforms: 2048 (1920) per §4.6.11.3.2; 512 per - // the Table 4.A.14 SSR window fit. - 2048 | 1920 | 512 => 4.0, - // Short transforms: 256 (240) per §4.6.11.3.2; 64 per - // the Table 4.A.13 SSR window fit. - _ => 6.0, - }; - kbd_left(half, alpha) - } - } -} - -/// §4.6.11.3.2 — assemble a transform's window from a `left` shape -/// (inherited from the previous block) and a `right` shape (this -/// block's `window_shape`). For a sine/KBD window the right half is -/// the spatial mirror of that shape's *left* half, so we build the -/// `right`-shape left half and reverse it. -fn window_halves( - n_transform: usize, - left_shape: WindowShape, - right_shape: WindowShape, -) -> WindowHalves { - window_halves_style(n_transform, left_shape, right_shape, WindowStyle::Standard) -} - -/// [`window_halves`] with an explicit [`WindowStyle`]. -fn window_halves_style( - n_transform: usize, - left_shape: WindowShape, - right_shape: WindowShape, - style: WindowStyle, -) -> WindowHalves { - let left = half_window_style(n_transform, left_shape, style); - let mut right = half_window_style(n_transform, right_shape, style); - right.reverse(); - WindowHalves { left, right } -} - -/// The stateful per-channel §4.6.11 filterbank. One instance per -/// decoded channel; [`Filterbank::synthesize`] is called once per -/// frame and carries the overlap-add tail (`z[i-1][n + N/2]`) plus the -/// previous block's `window_shape` (which determines the left-half -/// shape of the next block, §4.6.11.3.2) across calls. -#[derive(Clone, Debug)] -pub struct Filterbank { - /// The §4.5.1.1 frame-length family this filterbank synthesizes - /// (transform lengths, overlap length, and — for LD — the - /// §4.6.17.2.3 window style). Fixed at construction; a frame - /// whose `ics_info.family` disagrees is rejected. - family: FrameFamily, - /// `z[i-1][N/2 .. N]` — the right half of the previous frame's - /// windowed time signal, added to the left half of this frame's - /// windowed signal (§4.6.11.3.3). `family.frame_len()` long. - overlap: Vec, - /// `window_shape` of the previous block, governing the left-half - /// window shape of the next block. [`None`] before the first - /// frame: per §4.6.11.3.2 the first block's left and right halves - /// share its own `window_shape`. - prev_shape: Option, -} - -impl Default for Filterbank { - fn default() -> Self { - Self::new() - } -} - -impl Filterbank { - /// A fresh filterbank with a zeroed overlap buffer and no - /// previous-block shape (so the first frame uses its own - /// `window_shape` for both halves, per §4.6.11.3.2). - pub fn new() -> Self { - Self::new_family(FrameFamily::Lc1024) - } - - /// A fresh filterbank for an arbitrary §4.5.1.1 [`FrameFamily`]: - /// the 1024 / 960 block-switching families or the long-only LD - /// 512 / 480 families (whose `window_shape == 1` selects the - /// §4.6.17.2.3 low-overlap window in place of KBD). - pub fn new_family(family: FrameFamily) -> Self { - Filterbank { - family, - overlap: vec![0.0f64; family.frame_len()], - prev_shape: None, - } - } - - /// The [`FrameFamily`] this filterbank was constructed for. - pub fn family(&self) -> FrameFamily { - self.family - } - - /// §4.6.7.3 — the current frame's *aliased half window* - /// `x_rec(0 … N/2 − 1)`: the right half of the just-synthesized - /// frame's windowed (pre-overlap-add) time signal `z[i][N/2 … N]`. - /// - /// After a [`Self::synthesize`] call the internal overlap buffer - /// holds exactly this tail (it is reused as the *next* frame's - /// overlap-add term, §4.6.11.3.3). The LTP reconstruction history - /// ([`crate::ltp::LtpState`]) needs the same vector — its - /// `x_rec(0 … N/2 − 1)` region — so the element driver reads it here - /// after each synthesis and feeds it to - /// [`crate::ltp::LtpState::push_frame`]. Before the first frame this - /// is the zero buffer, matching the §4.6.7.3 zero initialisation. - pub fn aliased_tail(&self) -> &[f64] { - &self.overlap - } - - /// §4.6.11.3.2 — the previous block's `window_shape`, which governs - /// the left-half shape of the *next* block's analysis/synthesis - /// window. [`None`] before the first frame (the first block uses its - /// own shape for both halves). - /// - /// The §4.6.7.4.1 LTP analysis MDCT must window `x_est` with the - /// same composite long window the filterbank uses for this frame, so - /// the element driver reads the previous shape here before - /// synthesizing. - pub fn prev_shape(&self) -> Option { - self.prev_shape - } - - /// §4.6.11 — synthesize one frame of `LONG_WINDOW_LEN` (1024) PCM - /// samples from `spec`, the window-major decoded spectrum produced - /// by [`crate::decoded_spectrum::decode_channel_spectrum`]. - /// - /// `spec` must be: - /// - /// * `LONG_WINDOW_LEN` (1024) coefficients for `ONLY_LONG`, - /// `LONG_START`, `LONG_STOP`; - /// * `8 × SHORT_WINDOW_LEN` (1024 total) for `EIGHT_SHORT`, - /// laid out window-major: window `w` at `spec[w * 128 ..]`. - /// - /// The result is the §4.6.11.3.3 overlap-added output; the method - /// updates the internal overlap tail and previous-block shape for - /// the next call. - /// - /// Errors: [`Error::FilterbankInvalid`] if `spec.len()` disagrees - /// with `ics_info.window_sequence`. - pub fn synthesize(&mut self, spec: &[f64], ics_info: &IcsInfo) -> Result> { - if ics_info.family != self.family { - return Err(Error::FilterbankInvalid); - } - let z = self.windowed_signal(spec, ics_info)?; - debug_assert_eq!(z.len(), self.family.long_transform_len()); - - // §4.6.11.3.3 overlap-add: out[n] = z[i][n] + z[i-1][n + N/2]. - let half = self.family.frame_len(); - let out: Vec = z[..half] - .iter() - .zip(self.overlap.iter()) - .map(|(&zn, &on)| zn + on) - .collect(); - - // Retain z[i][N/2 .. N] as next frame's z[i-1][n + N/2]. - self.overlap.clear(); - self.overlap.extend_from_slice(&z[half..]); - - // §4.6.11.3.2: the left-half shape of the *next* block is this - // block's window_shape. - self.prev_shape = Some(ics_info.window_shape); - Ok(out) - } - - /// §4.6.11.3.1 + §4.6.11.3.2 — produce the full-length (`N_l = - /// 2048`) windowed time signal `z[i][n]` for this frame, before - /// the inter-block overlap-add. Dispatches on `window_sequence`. - fn windowed_signal(&self, spec: &[f64], ics_info: &IcsInfo) -> Result> { - let left_shape = self.prev_shape.unwrap_or(ics_info.window_shape); - let right_shape = ics_info.window_shape; - match ics_info.window_sequence { - WindowSequence::OnlyLong => { - self.long_windowed(spec, left_shape, right_shape, LongKind::OnlyLong) - } - WindowSequence::LongStart => { - self.long_windowed(spec, left_shape, right_shape, LongKind::Start) - } - WindowSequence::LongStop => { - self.long_windowed(spec, left_shape, right_shape, LongKind::Stop) - } - WindowSequence::EightShort => self.short_windowed(spec, left_shape, right_shape), - } - } - - /// §4.6.11.3.2 a)/b)/d) — the three long-transform sequences. Each - /// runs a single length-2048 IMDCT and applies a composite window - /// whose left half (`ONLY_LONG`, `LONG_START`) or right half - /// (`LONG_STOP`) is the full long half-window, and whose other - /// half is shaped by the start/stop transition (a short half-window - /// flanked by a flat `1.0` plateau and a zero region). - fn long_windowed( - &self, - spec: &[f64], - left_shape: WindowShape, - right_shape: WindowShape, - kind: LongKind, - ) -> Result> { - if spec.len() != self.family.frame_len() { - return Err(Error::FilterbankInvalid); - } - let x = imdct(spec, self.family.long_transform_len()); - let w = self.long_window(left_shape, right_shape, kind)?; - let z: Vec = x.iter().zip(w.iter()).map(|(&xv, &wv)| xv * wv).collect(); - Ok(z) - } - - /// §4.6.11.3.2 — assemble the length-2048 window vector for a - /// long-transform sequence. - /// - /// * `OnlyLong` (a): `[W_LEFT_l | W_RIGHT_l]`. - /// * `Start` (b): left half is `W_LEFT_l`; the right half is a - /// flat `1.0` plateau over `[N_l/2, (3N_l − N_s)/4)`, the short - /// right half-window over `[(3N_l − N_s)/4, (3N_l + N_s)/4)`, and - /// `0.0` over `[(3N_l + N_s)/4, N_l)`. - /// * `Stop` (d): the left half is `0.0` over `[0, (N_l − N_s)/4)`, - /// the short left half-window over `[(N_l − N_s)/4, (N_l + - /// N_s)/4)`, and a flat `1.0` plateau over `[(N_l + N_s)/4, - /// N_l/2)`; the right half is `W_RIGHT_l`. - fn long_window( - &self, - left_shape: WindowShape, - right_shape: WindowShape, - kind: LongKind, - ) -> Result> { - let n_l = self.family.long_transform_len(); - match self.family.short_transform_len() { - Some(n_s) => Ok(build_long_window_style( - n_l, - n_s, - left_shape, - right_shape, - kind, - WindowStyle::for_family(self.family), - )), - // LD: long-only — Start / Stop transitions do not exist - // (§4.6.17.2.2), so only the OnlyLong composite is legal. - None => match kind { - LongKind::OnlyLong => { - let halves = - window_halves_style(n_l, left_shape, right_shape, WindowStyle::LowDelay); - let half_l = n_l / 2; - let mut w = vec![0.0f64; n_l]; - w[..half_l].copy_from_slice(&halves.left); - for (m, &rv) in halves.right.iter().enumerate() { - w[half_l + m] = rv; - } - Ok(w) - } - _ => Err(Error::LdShortWindow), - }, - } - } -} - -/// §4.6.11.3.2 — [`build_long_window`] generalized to an arbitrary -/// `(n_l, n_s)` transform family; every breakpoint is the spec's -/// `N_l`/`N_s` expression evaluated at the caller's lengths (the -/// standard family passes `(2048, 256)`, the SSR §4.6.12.1 per-band -/// family `(512, 64)`). -fn build_long_window_n( - n_l: usize, - n_s: usize, - left_shape: WindowShape, - right_shape: WindowShape, - kind: LongKind, -) -> Vec { - build_long_window_style( - n_l, - n_s, - left_shape, - right_shape, - kind, - WindowStyle::Standard, - ) -} - -/// [`build_long_window_n`] with an explicit [`WindowStyle`] (the LD -/// families map `window_shape == 1` to the §4.6.17.2.3 low-overlap -/// window; the LD long-only path never reaches the Start / Stop -/// composites, but the parameterization keeps the construction -/// uniform). -fn build_long_window_style( - n_l: usize, - n_s: usize, - left_shape: WindowShape, - right_shape: WindowShape, - kind: LongKind, - style: WindowStyle, -) -> Vec { - let long = window_halves_style(n_l, left_shape, right_shape, style); - let short = window_halves_style(n_s, left_shape, right_shape, style); - let half_l = n_l / 2; - let mut w = vec![0.0f64; n_l]; - - // Left half is always the plain long left half for OnlyLong / - // Start; Stop replaces it with the start-transition mirror. - match kind { - LongKind::OnlyLong | LongKind::Start => { - w[..half_l].copy_from_slice(&long.left); - } - LongKind::Stop => { - // 0.0 over [0, (N_l − N_s)/4); short left half over - // [(N_l − N_s)/4, (N_l + N_s)/4); 1.0 over - // [(N_l + N_s)/4, N_l/2). - let a = (n_l - n_s) / 4; - for (m, &sv) in short.left.iter().enumerate() { - w[a + m] = sv; - } - for slot in w.iter_mut().take(half_l).skip(a + n_s / 2) { - *slot = 1.0; - } - } - } - - match kind { - LongKind::OnlyLong => { - for (m, &rv) in long.right.iter().enumerate() { - w[half_l + m] = rv; - } - } - LongKind::Start => { - // 1.0 over [N_l/2, (3N_l − N_s)/4); short right half - // over [(3N_l − N_s)/4, (3N_l + N_s)/4); 0.0 after. - let b = (3 * n_l - n_s) / 4; - for slot in w.iter_mut().take(b).skip(half_l) { - *slot = 1.0; - } - for (m, &rv) in short.right.iter().enumerate() { - w[b + m] = rv; - } - // [(3N_l + N_s)/4, N_l) stays 0.0 from the vec init. - } - LongKind::Stop => { - for (m, &rv) in long.right.iter().enumerate() { - w[half_l + m] = rv; - } - } - } - w -} - -impl Filterbank { - /// §4.6.11.3.2 c) — the `EIGHT_SHORT` sequence: eight length-256 - /// IMDCTs, each windowed with a short window, then overlapped and - /// added into the 2048-sample frame with leading/trailing zeros. - /// - /// Window-shape inheritance (§4.6.11.3.2): the *first* short - /// window's left half uses the previous block's shape; every - /// later short window's left half — and every short window's right - /// half — uses this block's `window_shape`. - fn short_windowed( - &self, - spec: &[f64], - left_shape: WindowShape, - right_shape: WindowShape, - ) -> Result> { - let n_s = self - .family - .short_transform_len() - .ok_or(Error::LdShortWindow)?; - let n_l = self.family.long_transform_len(); - let short_len = n_s / 2; // 128 (120) - if spec.len() != NUM_SHORT_WINDOWS * short_len { - return Err(Error::FilterbankInvalid); - } - - // Per-window windowed length-N_s time signals. - let mut windowed: Vec> = Vec::with_capacity(NUM_SHORT_WINDOWS); - for j in 0..NUM_SHORT_WINDOWS { - let coeffs = &spec[j * short_len..(j + 1) * short_len]; - let x = imdct(coeffs, n_s); - // W_0 left half inherits the previous block's shape; all - // other windows' left halves use this block's shape. - let this_left = if j == 0 { left_shape } else { right_shape }; - let halves = window_halves(n_s, this_left, right_shape); - let mut z = vec![0.0f64; n_s]; - for n in 0..n_s / 2 { - z[n] = x[n] * halves.left[n]; - } - for n in n_s / 2..n_s { - z[n] = x[n] * halves.right[n - n_s / 2]; - } - windowed.push(z); - } - - // §4.6.11.3.2 c) overlap-add of the eight short windows into a - // N_l-sample frame. Short window `j` starts at offset - // `(N_l − N_s)/4 + j·N_s/2` (each successive short window is - // hopped by N_s/2 = 128 (120) samples) — the spec's piecewise - // z_{i,n} is exactly this 50%-overlap-add with the first - // window placed at (N_l − N_s)/4 = 448 (420). - let mut z = vec![0.0f64; n_l]; - let start = (n_l - n_s) / 4; // 448 (420) - let hop = n_s / 2; // 128 (120) - for (j, win) in windowed.iter().enumerate() { - let base = start + j * hop; - for (n, &v) in win.iter().enumerate() { - z[base + n] += v; - } - } - Ok(z) - } -} - -/// Discriminates the three long-transform `window_sequence` shapes -/// inside [`Filterbank::long_window`]. -#[derive(Clone, Copy)] -enum LongKind { - OnlyLong, - Start, - Stop, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::IcsInfo; - - fn long_info(shape: WindowShape, seq: WindowSequence) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: seq, - window_shape: shape, - max_sfb: 49, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: 49, - } - } - - fn short_info(shape: WindowShape) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: shape, - max_sfb: 14, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups: 8, - window_group_length: vec![1; 8], - num_swb: 14, - } - } - - #[test] - fn sine_window_endpoints() { - // W_SIN_LEFT(n) = sin((π/N)(n + 1/2)); for N = 2048 the first - // sample is sin(π·0.5/2048) and the last left-half sample is - // sin(π·1023.5/2048) ≈ sin(π/2 · 0.9995…). - let left = sine_left(1024); - assert_eq!(left.len(), 1024); - let expect0 = (core::f64::consts::PI * 0.5 / 2048.0).sin(); - assert!((left[0] - expect0).abs() < 1e-15); - // The window rises monotonically to ~1.0 at the centre. - assert!(left[1023] > 0.9999 && left[1023] <= 1.0); - for w in 1..1024 { - assert!(left[w] > left[w - 1]); - } - } - - #[test] - fn sine_window_unit_power_overlap() { - // The sine window satisfies the Princen-Bradley condition: - // W(n)^2 + W(n + N/2)^2 = 1 for a symmetric sine window. Build - // a full OnlyLong sine window and check the squared-sum of the - // overlapping halves is 1. - let half = sine_left(1024); - for n in 0..1024 { - // Right half mirrors the left: W(N-1-n) = W_left(n). - let wl = half[n]; - let wr = half[1023 - n]; // W(1024 + n) = W_left(1023 - n) - let s = wl * wl + wr * wr; - assert!((s - 1.0).abs() < 1e-12, "n={n} sum={s}"); - } - } - - #[test] - fn kbd_window_unit_power_overlap() { - // The KBD window is constructed precisely so that - // W(n)^2 + W(n + N/2)^2 = 1 (it is the canonical - // perfect-reconstruction window). Verify against the long α=4 - // KBD window. - let left = kbd_left(1024, 4.0); - assert_eq!(left.len(), 1024); - for n in 0..1024 { - let wl = left[n]; - let wr = left[1023 - n]; - let s = wl * wl + wr * wr; - assert!((s - 1.0).abs() < 1e-12, "n={n} sum={s}"); - } - // KBD is monotonically increasing on its left half. - for n in 1..1024 { - assert!(left[n] >= left[n - 1]); - } - } - - #[test] - fn bessel_i0_known_values() { - // I0(0) = 1; I0(1) ≈ 1.2660658777520084; - // I0(2) ≈ 2.2795853023360673 (standard tabulated values). - assert!((bessel_i0(0.0) - 1.0).abs() < 1e-15); - assert!((bessel_i0(1.0) - 1.266_065_877_752_008_4).abs() < 1e-12); - assert!((bessel_i0(2.0) - 2.279_585_302_336_067_3).abs() < 1e-12); - } - - #[test] - fn imdct_dc_coefficient() { - // A single non-zero spec[0] is a pure cosine basis function. - // For N=8, half=4, n0=(4+1)/2=2.5: x[n] = (2/8)·cos((2π/8)(n+2.5)(0.5)). - let n = 8usize; - let spec = [1.0, 0.0, 0.0, 0.0]; - let x = imdct(&spec, n); - let scale = 2.0 / 8.0; - let n0 = 2.5; - for (idx, &xv) in x.iter().enumerate() { - let expect = - scale * (2.0 * core::f64::consts::PI / 8.0 * (idx as f64 + n0) * 0.5).cos(); - assert!((xv - expect).abs() < 1e-15, "n={idx}"); - } - } - - /// Time-domain aliasing cancellation (TDAC): for a windowed MDCT/ - /// IMDCT pair, two consecutive identical frames overlap-add to - /// reconstruct the windowed input exactly in the steady state. We - /// drive the filterbank with the production analysis [`forward_mdct`] - /// of a known signal and confirm perfect reconstruction over the - /// second frame. (The analysis/synthesis pair is unity for a - /// power-complementary §4.6.11.3.2 window.) - use super::forward_mdct; - - /// The full symmetric (sine) `OnlyLong` window, length `N`. - fn long_sine_window() -> Vec { - let left = sine_left(1024); - let mut w = vec![0.0; LONG_TRANSFORM_LEN]; - w[..1024].copy_from_slice(&left); - for m in 0..1024 { - w[1024 + m] = left[1023 - m]; - } - w - } - - #[test] - fn tdac_perfect_reconstruction_sine_long() { - // Streaming time-domain aliasing cancellation. A long input is - // analysed by a 50%-overlap forward MDCT (analysis window = - // sine), each frame carried through the decoder's IMDCT + - // synthesis window + overlap-add. For a power-complementary - // window the central frames reconstruct the input exactly. - // - // The forward analysis used here is the transpose of the - // decoder's §4.6.11.3.1 IMDCT basis with NO scale (the IMDCT - // carries the 2/N), so the analysis/synthesis pair satisfies - // TDAC for the sine window. - let n = LONG_TRANSFORM_LEN; // 2048 - let hop = n / 2; // 1024 - let win = long_sine_window(); - - // A long deterministic input; reconstruct the central hop. - let total = 5 * hop; - let input: Vec = (0..total) - .map(|i| (0.013 * i as f64).sin() + 0.5 * (0.07 * i as f64).cos()) - .collect(); - - let info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); - let mut fb = Filterbank::new(); - - // Run four overlapping analysis frames (starts 0, 1024, 2048, - // 3072), feeding each frame's MDCT to the filterbank. Collect - // the decoder's per-frame outputs. - let mut outputs = Vec::new(); - for f in 0..4 { - let base = f * hop; - let frame: Vec = (0..n) - .map(|m| { - let idx = base + m; - if idx < total { - input[idx] * win[m] - } else { - 0.0 - } - }) - .collect(); - let spec = forward_mdct(&frame, n); - outputs.push(fb.synthesize(&spec, &info).unwrap()); - } - - // The decoder output for frame f covers input samples - // [f·hop, f·hop + hop). The steady-state frames f = 1, 2 - // reconstruct the input (their window region is fully covered - // by both the analysis-window taper and the overlap from the - // neighbouring frames). - for (f, out) in outputs.iter().enumerate().take(3).skip(1) { - let base = f * hop; - for k in 0..hop { - let recon = out[k]; - let expect = input[base + k]; - assert!( - (recon - expect).abs() < 1e-9, - "frame={f} k={k} recon={recon} expect={expect}" - ); - } - } - } - - /// Family-parameterized streaming TDAC harness: analyse a - /// deterministic input with the 50%-overlap forward MDCT under - /// the family's own long window, run the decoder filterbank, and - /// require exact reconstruction on the steady-state frames. - fn tdac_long_family(family: crate::swb_offset::FrameFamily, shape: WindowShape) { - let n = family.long_transform_len(); - let hop = n / 2; - let style = WindowStyle::for_family(family); - let win = { - let left = half_window_style(n, shape, style); - let mut w = vec![0.0; n]; - w[..hop].copy_from_slice(&left); - for m in 0..hop { - w[hop + m] = left[hop - 1 - m]; - } - w - }; - let total = 5 * hop; - let input: Vec = (0..total) - .map(|i| (0.017 * i as f64).sin() + 0.4 * (0.043 * i as f64).cos()) - .collect(); - let mut info = long_info(shape, WindowSequence::OnlyLong); - info.family = family; - info.num_swb = 40; // geometry-irrelevant here - let mut fb = Filterbank::new_family(family); - let mut outputs = Vec::new(); - for f in 0..4 { - let base = f * hop; - let frame: Vec = (0..n) - .map(|m| { - let idx = base + m; - if idx < total { - input[idx] * win[m] - } else { - 0.0 - } - }) - .collect(); - let spec = forward_mdct(&frame, n); - let out = fb.synthesize(&spec, &info).unwrap(); - assert_eq!(out.len(), family.frame_len()); - outputs.push(out); - } - for (f, out) in outputs.iter().enumerate().take(3).skip(1) { - let base = f * hop; - for k in 0..hop { - assert!( - (out[k] - input[base + k]).abs() < 1e-9, - "{:?} {:?} frame={f} k={k}", - family, - shape - ); - } - } - } - - #[test] - fn tdac_lc960_sine_and_kbd() { - tdac_long_family(crate::swb_offset::FrameFamily::Lc960, WindowShape::Sine); - tdac_long_family(crate::swb_offset::FrameFamily::Lc960, WindowShape::Kbd); - } - - #[test] - fn tdac_ld512_sine_and_low_overlap() { - // Under the LD families the window_shape == 1 bit selects the - // §4.6.17.2.3 low-overlap window (Table 4.171). - tdac_long_family(crate::swb_offset::FrameFamily::Ld512, WindowShape::Sine); - tdac_long_family(crate::swb_offset::FrameFamily::Ld512, WindowShape::Kbd); - } - - #[test] - fn tdac_ld480_sine_and_low_overlap() { - tdac_long_family(crate::swb_offset::FrameFamily::Ld480, WindowShape::Sine); - tdac_long_family(crate::swb_offset::FrameFamily::Ld480, WindowShape::Kbd); - } - - #[test] - fn low_overlap_window_regions_and_pr() { - // §4.6.17.2.3: zeros over [0, 3N/16), sine rise over - // [3N/16, 5N/16), flat 1.0 over [5N/16, N/2) on the left - // half; power-complementary at the TDAC partners. - for n in [1024usize, 960] { - let half = n / 2; - let left = low_overlap_left(half); - assert_eq!(left.len(), half); - for (i, &v) in left.iter().enumerate().take(3 * n / 16) { - assert_eq!(v, 0.0, "N={n} i={i}"); - } - for (i, &v) in left.iter().enumerate().take(half).skip(5 * n / 16) { - assert_eq!(v, 1.0, "N={n} i={i}"); - } - // Monotone rise inside [3N/16, 5N/16). - for i in 3 * n / 16 + 1..5 * n / 16 { - assert!(left[i] > left[i - 1], "N={n} i={i}"); - } - // Princen-Bradley: W(n)² + W(N/2−1−n)² = 1 over the half. - for i in 0..half { - let s = left[i] * left[i] + left[half - 1 - i] * left[half - 1 - i]; - assert!((s - 1.0).abs() < 1e-12, "N={n} i={i} s={s}"); - } - } - } - - #[test] - fn ld_filterbank_rejects_non_only_long() { - use crate::swb_offset::FrameFamily; - let mut fb = Filterbank::new_family(FrameFamily::Ld512); - let mut info = long_info(WindowShape::Sine, WindowSequence::LongStart); - info.family = FrameFamily::Ld512; - let spec = vec![0.0; 512]; - assert!(matches!( - fb.synthesize(&spec, &info), - Err(Error::LdShortWindow) - )); - } - - #[test] - fn family_mismatch_rejected() { - use crate::swb_offset::FrameFamily; - let mut fb = Filterbank::new_family(FrameFamily::Lc1024); - let mut info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); - info.family = FrameFamily::Lc960; - let spec = vec![0.0; 960]; - assert!(matches!( - fb.synthesize(&spec, &info), - Err(Error::FilterbankInvalid) - )); - } - - #[test] - fn eight_short_lc960_tdac() { - // The 960-family EIGHT_SHORT: 8 × 120-line windows (240-point - // transforms) at start 420, hop 120. A steady sine input - // through forward-MDCT analysis per short window must - // reconstruct inside the fully-overlapped interior region of - // the frame's central section. - use crate::swb_offset::FrameFamily; - let family = FrameFamily::Lc960; - let n_s = 240usize; - let hop = 120usize; - let start = 420usize; - let win = { - let left = half_window_style(n_s, WindowShape::Sine, WindowStyle::Standard); - let mut w = vec![0.0; n_s]; - w[..hop].copy_from_slice(&left); - for m in 0..hop { - w[hop + m] = left[hop - 1 - m]; - } - w - }; - // Input signal over the frame's 1920-sample window region. - let input: Vec = (0..1920).map(|i| (0.05 * i as f64).sin() * 0.7).collect(); - // Analyse the eight short windows. - let mut spec = Vec::with_capacity(8 * hop); - for j in 0..8 { - let base = start + j * hop; - let frame: Vec = (0..n_s).map(|m| input[base + m] * win[m]).collect(); - spec.extend(forward_mdct(&frame, n_s)); - } - let mut info = short_info(WindowShape::Sine); - info.family = family; - info.num_swb = 14; - let mut fb = Filterbank::new_family(family); - // Prime the overlap with the previous frame's tail = zeros; the - // first output frame covers window-region samples [0, 960). - let out = fb.synthesize(&spec, &info).unwrap(); - assert_eq!(out.len(), 960); - // Interior of the short-window train that lands in the first - // output half: [start + hop, 960) = [540, 960) is covered by - // two overlapping short windows each (TDAC-complete). - for k in 540..960 { - assert!( - (out[k] - input[k]).abs() < 1e-9, - "k={k} out={} in={}", - out[k], - input[k] - ); - } - } - - #[test] - fn tdac_perfect_reconstruction_kbd_long() { - // Same streaming TDAC check with the KBD (α=4) long window. - let n = LONG_TRANSFORM_LEN; - let hop = n / 2; - let win = { - let left = kbd_left(1024, 4.0); - let mut w = vec![0.0; n]; - w[..1024].copy_from_slice(&left); - for m in 0..1024 { - w[1024 + m] = left[1023 - m]; - } - w - }; - let total = 5 * hop; - let input: Vec = (0..total) - .map(|i| 0.3 * (0.02 * i as f64).cos() - 0.6 * (0.05 * i as f64).sin()) - .collect(); - let info = long_info(WindowShape::Kbd, WindowSequence::OnlyLong); - let mut fb = Filterbank::new(); - let mut outputs = Vec::new(); - for f in 0..4 { - let base = f * hop; - let frame: Vec = (0..n) - .map(|m| { - let idx = base + m; - if idx < total { - input[idx] * win[m] - } else { - 0.0 - } - }) - .collect(); - let spec = forward_mdct(&frame, n); - outputs.push(fb.synthesize(&spec, &info).unwrap()); - } - for (f, out) in outputs.iter().enumerate().take(3).skip(1) { - let base = f * hop; - for k in 0..hop { - assert!((out[k] - input[base + k]).abs() < 1e-9, "frame={f} k={k}"); - } - } - } - - #[test] - fn eight_short_internal_tdac() { - // §4.6.11.3.2 c): the eight short windows overlap-add inside - // the frame with a 128-sample hop, the first window placed at - // offset (N_l − N_s)/4 = 448. Drive the eight short MDCTs from - // a streaming short-window analysis of a continuous input and - // confirm the frame's interior reconstructs that input over - // the fully-overlapped central short windows. - let n_s = SHORT_TRANSFORM_LEN; // 256 - let hop = n_s / 2; // 128 - let sine_short = { - let left = sine_left(hop); - let mut w = vec![0.0; n_s]; - w[..hop].copy_from_slice(&left); - for m in 0..hop { - w[hop + m] = left[hop - 1 - m]; - } - w - }; - // A continuous input long enough to cover all eight short - // windows once placed at start=448, hop=128: last window starts - // at 448 + 7·128 = 1344, ends at 1600. - let total = N_L; - let input: Vec = (0..total) - .map(|i| (0.05 * i as f64).sin() + 0.4 * (0.11 * i as f64).cos()) - .collect(); - let start = (N_L - N_S) / 4; // 448 - - // Build the eight short windows' MDCTs from the windowed input - // segments at the same offsets the decoder overlaps them. - let mut spec = Vec::with_capacity(NUM_SHORT_WINDOWS * SHORT_WINDOW_LEN as usize); - for j in 0..NUM_SHORT_WINDOWS { - let base = start + j * hop; - let seg: Vec = (0..n_s).map(|m| input[base + m] * sine_short[m]).collect(); - let s = forward_mdct(&seg, n_s); - spec.extend_from_slice(&s); - } - - let info = short_info(WindowShape::Sine); - let mut fb = Filterbank::new(); - let out = fb.synthesize(&spec, &info).unwrap(); - - // The output frame is z[0:1024]; overlap with the (zero) prior - // frame leaves the interior intact. The central short windows - // j=1..6 are fully overlapped by their neighbours, so the - // reconstructed signal equals the input over their shared - // central hops: input indices [start + hop, start + 7·hop). - // The decoder output covers input [0, 1024); the short-window - // region [start, 1600) is partly past 1024, so check the - // covered central hops [start+hop, 1024). - for idx in (start + hop)..1024 { - assert!( - (out[idx] - input[idx]).abs() < 1e-9, - "idx={idx} out={} input={}", - out[idx], - input[idx] - ); - } - } - - #[test] - fn synthesize_long_length_and_shape() { - let info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); - let mut fb = Filterbank::new(); - let spec = vec![0.25f64; LONG_WINDOW_LEN as usize]; - let out = fb.synthesize(&spec, &info).unwrap(); - assert_eq!(out.len(), LONG_WINDOW_LEN as usize); - assert!(out.iter().all(|v| v.is_finite())); - } - - #[test] - fn synthesize_eight_short_length() { - let info = short_info(WindowShape::Sine); - let mut fb = Filterbank::new(); - let spec = vec![0.1f64; NUM_SHORT_WINDOWS * SHORT_WINDOW_LEN as usize]; - let out = fb.synthesize(&spec, &info).unwrap(); - assert_eq!(out.len(), LONG_WINDOW_LEN as usize); - assert!(out.iter().all(|v| v.is_finite())); - } - - #[test] - fn synthesize_rejects_wrong_length() { - let info = long_info(WindowShape::Sine, WindowSequence::OnlyLong); - let mut fb = Filterbank::new(); - let spec = vec![0.0f64; 512]; - assert!(matches!( - fb.synthesize(&spec, &info), - Err(Error::FilterbankInvalid) - )); - let sinfo = short_info(WindowShape::Sine); - let mut fb2 = Filterbank::new(); - let bad = vec![0.0f64; 1000]; - assert!(matches!( - fb2.synthesize(&bad, &sinfo), - Err(Error::FilterbankInvalid) - )); - } - - #[test] - fn start_window_plateau_and_zero_regions() { - // LONG_START: left half is the long left window, then a flat - // 1.0 plateau, then the short right half, then zeros. - let fb = Filterbank::new(); - let w = fb - .long_window(WindowShape::Sine, WindowShape::Sine, LongKind::Start) - .unwrap(); - assert_eq!(w.len(), N_L); - // Plateau region [1024, 1472) is all 1.0. - for v in w.iter().take(1472).skip(1024) { - assert!((*v - 1.0).abs() < 1e-15); - } - // Tail [1600, 2048) is all 0.0. (3N_l + N_s)/4 = 1600. - for v in w.iter().take(N_L).skip(1600) { - assert_eq!(*v, 0.0); - } - // The short-right transition [1472, 1600) falls from 1 to 0. - assert!(w[1472] > w[1599]); - } - - #[test] - fn stop_window_zero_and_plateau_regions() { - // LONG_STOP: leading zeros, short left half, 1.0 plateau, then - // the long right window. - let fb = Filterbank::new(); - let w = fb - .long_window(WindowShape::Sine, WindowShape::Sine, LongKind::Stop) - .unwrap(); - assert_eq!(w.len(), N_L); - // Leading [0, 448) zeros. (N_l − N_s)/4 = 448. - for v in w.iter().take(448) { - assert_eq!(*v, 0.0); - } - // Plateau [576, 1024) all 1.0. (N_l + N_s)/4 = 576. - for v in w.iter().take(1024).skip(576) { - assert!((*v - 1.0).abs() < 1e-15); - } - // The short-left transition [448, 576) rises from 0 to 1. - assert!(w[448] < w[575]); - } - - #[test] - fn first_frame_uses_own_shape_for_left_half() { - // Before any frame, prev_shape is None, so the first frame's - // left half uses its own window_shape (KBD here). Confirm the - // left half equals the KBD left window, not the sine one. - let info = long_info(WindowShape::Kbd, WindowSequence::OnlyLong); - let fb = Filterbank::new(); - let w = fb - .windowed_signal(&vec![0.0; LONG_WINDOW_LEN as usize], &info) - .unwrap(); - // All-zero spectrum → zero time signal regardless, so instead - // inspect the window directly. - let _ = w; - let win = fb - .long_window(WindowShape::Kbd, WindowShape::Kbd, LongKind::OnlyLong) - .unwrap(); - let kbd = kbd_left(1024, 4.0); - for n in 0..1024 { - assert!((win[n] - kbd[n]).abs() < 1e-15); - } - } - - /// Table 4.A.13 — the normative Kaiser-Bessel window for the AAC - /// SSR object type `EIGHT_SHORT_SEQUENCE` (`N = 64`): all 32 - /// tabulated left-half values, transcribed from the spec PDF. The - /// running-sum KBD construction with the short-transform `α = 6` - /// reproduces every entry to the table's print precision. - #[test] - fn ssr_kbd_short_window_matches_table_4_a_13() { - // Verbatim table transcription — keep every printed digit, - // including redundant trailing zeros. - #[allow(clippy::excessive_precision)] - const TABLE_4_A_13: [(usize, f64); 32] = [ - (0, 0.0000875914060105), - (1, 0.0009321760265333), - (2, 0.0032114611466596), - (3, 0.0081009893216786), - (4, 0.0171240286619181), - (5, 0.0320720743527833), - (6, 0.0548307856028528), - (7, 0.0871361822564870), - (8, 0.1302923415174603), - (9, 0.1848955425508276), - (10, 0.2506163195331889), - (11, 0.3260874142923209), - (12, 0.4089316830907141), - (13, 0.4959414909423747), - (14, 0.5833939894958904), - (15, 0.6674601983218376), - (16, 0.7446454751465113), - (17, 0.8121892962974020), - (18, 0.8683559394406505), - (19, 0.9125649996381605), - (20, 0.9453396205809574), - (21, 0.9680864942677585), - (22, 0.9827581789763112), - (23, 0.9914756203467121), - (24, 0.9961964092194694), - (25, 0.9984956609571091), - (26, 0.9994855586984285), - (27, 0.9998533730714648), - (28, 0.9999671864476404), - (29, 0.9999948432453556), - (30, 0.9999995655238333), - (31, 0.9999999961638728), - ]; - let left = half_window_style(64, WindowShape::Kbd, WindowStyle::Standard); - assert_eq!(left.len(), 32); - for &(i, expect) in &TABLE_4_A_13 { - assert!( - (left[i] - expect).abs() < 1e-8, - "Table 4.A.13 w({i}): got {} expect {expect}", - left[i] - ); - } - // Discriminator: the long-transform α = 4 does NOT fit. - let alt = kbd_left(32, 4.0); - assert!((alt[0] - TABLE_4_A_13[0].1).abs() > 1e-4); - } - - /// Table 4.A.14 — the normative Kaiser-Bessel window for the SSR - /// object type's other window sequences (`N = 512`): a spread of - /// tabulated left-half values transcribed from the spec PDF. The - /// running-sum KBD construction with the long-transform `α = 4` - /// reproduces each to the table's print precision. - #[test] - fn ssr_kbd_long_window_matches_table_4_a_14() { - // Verbatim table transcription — keep every printed digit, - // including redundant trailing zeros. - #[allow(clippy::excessive_precision)] - const TABLE_4_A_14_SPREAD: [(usize, f64); 15] = [ - (0, 0.0005851230124487), - (1, 0.0009642149851497), - (2, 0.0013558207534965), - (16, 0.0116765080854300), - (32, 0.0405466983507029), - (64, 0.1811734433685097), - (96, 0.4325622561631607), - (128, 0.7110428359000029), - (160, 0.9058173183656508), - (192, 0.9845850806232530), - (224, 0.9992757396582338), - (240, 0.9999442511639580), - (250, 0.9999962619864214), - (254, 0.9999995351446231), - (255, 0.9999998288155155), - ]; - let left = half_window_style(512, WindowShape::Kbd, WindowStyle::Standard); - assert_eq!(left.len(), 256); - for &(i, expect) in &TABLE_4_A_14_SPREAD { - assert!( - (left[i] - expect).abs() < 1e-8, - "Table 4.A.14 w({i}): got {} expect {expect}", - left[i] - ); - } - // Discriminator: the short-transform α = 6 does NOT fit. - let alt = kbd_left(256, 6.0); - assert!((alt[0] - TABLE_4_A_14_SPREAD[0].1).abs() > 1e-4); - } - - /// The generalized `(n_l, n_s)` long-window builder reproduces the - /// standard-family construction exactly, and the SSR family's - /// breakpoints land at the quarter-scaled positions. - #[test] - fn generalized_long_window_matches_standard_and_scales() { - for kind in [LongKind::OnlyLong, LongKind::Start, LongKind::Stop] { - let std = Filterbank::new() - .long_window(WindowShape::Sine, WindowShape::Sine, kind) - .unwrap(); - let gen = build_long_window_n(2048, 256, WindowShape::Sine, WindowShape::Sine, kind); - assert_eq!(std, gen); - } - // SSR LONG_START at (512, 64): 1.0 plateau over [256, 368), - // short descent over [368, 400), zero over [400, 512). - let w = build_long_window_n( - 512, - 64, - WindowShape::Sine, - WindowShape::Sine, - LongKind::Start, - ); - assert_eq!(w.len(), 512); - for v in w.iter().take(368).skip(256) { - assert!((*v - 1.0).abs() < 1e-15); - } - assert!(w[368] < 1.0 && w[368] > w[399]); - for v in w.iter().skip(400) { - assert_eq!(*v, 0.0); - } - // SSR LONG_STOP mirrors: zero over [0, 112), ascent [112, 144), - // plateau [144, 256). - let w = build_long_window_n( - 512, - 64, - WindowShape::Sine, - WindowShape::Sine, - LongKind::Stop, - ); - for v in w.iter().take(112) { - assert_eq!(*v, 0.0); - } - for v in w.iter().take(256).skip(144) { - assert!((*v - 1.0).abs() < 1e-15); - } - } - - /// The SSR-family windows are TDAC power-complementary at every - /// steady overlap: `w(n)² + w(n + N/2)²` over the flanks sums to 1 - /// for the 512 `ONLY_LONG` window (both shapes), which is the - /// §4.6.11 perfect-reconstruction condition the §4.6.12.3.3 - /// per-band overlap relies on. - #[test] - fn ssr_only_long_window_is_power_complementary() { - for shape in [WindowShape::Sine, WindowShape::Kbd] { - let w = build_long_window_n(512, 64, shape, shape, LongKind::OnlyLong); - for n in 0..256 { - let s = w[n] * w[n] + w[n + 256] * w[n + 256]; - assert!( - (s - 1.0).abs() < 1e-10, - "{shape:?} w²({n}) + w²({}) = {s}", - n + 256 - ); - } - } - } -} diff --git a/crates/vendor/oxideav-aac/src/gain_control.rs b/crates/vendor/oxideav-aac/src/gain_control.rs deleted file mode 100644 index 000372b6..00000000 --- a/crates/vendor/oxideav-aac/src/gain_control.rs +++ /dev/null @@ -1,907 +0,0 @@ -//! SSR gain-control reconstruction — ISO/IEC 14496-3 §4.6.12. -//! -//! This is the §4.6.12 *back-end* of the SSR (Scalable Sample Rate, -//! AOT 3) gain-control tool, the counterpart to the -//! [`crate::gain_control_data`] wire parser. Where that module reads -//! the Table 4.12 `(max_band, adjust_num, alevcode, aloccode)` side -//! info off the bitstream, this module turns that side info plus the -//! per-band IMDCT output into the reconstructed PCM time signal: -//! -//! 1. **Gain-control data decoding** (§4.6.12.3.1) — -//! [`BandGainFunction::reconstruct`] maps the wire codes to the -//! `NADW` / `ALOC` / `ALEV` ladder via the Table 4.108 `AdjLoc()` -//! and Table 4.109 `AdjLev()` tables. -//! 2. **Gain-control function setting** (§4.6.12.3.2) — the same call -//! builds the `FMD` fragment-modification function, threads the -//! cross-frame `PFMD`, composes the per-sequence `GMF` gain -//! modification function, and inverts it to the gain-control -//! function `AD(j) = 1/GMF(j)`. -//! 3. **Gain-control windowing & overlapping** (§4.6.12.3.3) — -//! [`GainBandState::window_overlap`] applies `AD` to the band -//! spectrum `U`, then overlap-adds against the previous frame's -//! tail `PT` to produce the band sample data `V`. -//! -//! The IPQF synthesis filter (§4.6.12.3.4) that recombines the four -//! `V` bands into the output PCM lives in the `ipqf` module. -//! -//! ## Per-band, per-frame state -//! -//! Two quantities thread across frames, *per IPQF band*: -//! -//! * `PFMD_B(j)` — the previous frame's fragment-modification function, -//! used to scale the left half of this frame's `GMF` (§4.6.12.3.2 -//! step 3). Its initial value is `1.0` (spec note). -//! * `PT_B(j)` — the previous frame's gain-controlled block sample -//! data tail, overlap-added into this frame's `V` (§4.6.12.3.3 -//! step 2). Its initial value is `0.0` (spec note). -//! -//! [`GainBandState`] carries both for one band; the four-band decoder -//! holds a `[GainBandState; 4]`. -//! -//! ## Provenance -//! -//! Every table and formula is from ISO/IEC 14496-3:2001 §4.6.12 -//! (Tables 4.108 / 4.109, the §4.6.12.3.1–3 equations) staged under -//! `docs/audio/aac/`. No external SSR implementation was consulted. - -use crate::gain_control_data::{GainBand, GainControlData}; -use crate::ics_info::WindowSequence; - -/// `AdjLoc(AC)` — ISO/IEC 14496-3 Table 4.108. The 32 tabulated -/// values are exactly `8 · AC` for `AC ∈ 0..=31`. -#[must_use] -pub fn adj_loc(ac: u8) -> u32 { - 8 * u32::from(ac) -} - -/// `AdjLev(AV)` — ISO/IEC 14496-3 Table 4.109. The 16 tabulated -/// values are exactly `AV − 4` for `AV ∈ 0..=15`. -#[must_use] -pub fn adj_lev(av: u8) -> i32 { - i32::from(av) - 4 -} - -/// Number of gain-control windows `N(window_sequence)` — the per-band -/// window count over which the gain ladder is transmitted (Table 4.12 -/// / §4.6.12.3.1). Long sequences carry one or two windows; the short -/// sequence carries eight. -#[must_use] -pub fn num_windows(seq: WindowSequence) -> usize { - match seq { - WindowSequence::OnlyLong => 1, - WindowSequence::LongStart | WindowSequence::LongStop => 2, - WindowSequence::EightShort => 8, - } -} - -/// `ALOC_{W,B}(NADW + 1)` — the §4.6.12.3.1 step (4) endpoint location -/// for the gain ladder of window `w` under `seq`. -/// -/// ```text -/// 256, W == 0 if ONLY_LONG_SEQUENCE -/// 112, W == 0 -/// if LONG_START_SEQUENCE -/// 32, W == 1 -/// ALOC(NADW+1) = -/// 32, 0..=7 if EIGHT_SHORT_SEQUENCE -/// -/// 112, W == 0 -/// if LONG_STOP_SEQUENCE -/// 256, W == 1 -/// ``` -#[must_use] -pub fn endpoint_aloc(seq: WindowSequence, w: usize) -> u32 { - match seq { - WindowSequence::OnlyLong => 256, - WindowSequence::LongStart => { - if w == 0 { - 112 - } else { - 32 - } - } - WindowSequence::EightShort => 32, - WindowSequence::LongStop => { - if w == 0 { - 112 - } else { - 256 - } - } - } -} - -/// The §4.6.12.3.2 upper bound (inclusive) on `j` for the `FMD` -/// fragment-modification function of window `w` under `seq`. This is -/// the largest sample index over which `M`/`FMD` are defined. -#[must_use] -fn fmd_last_j(seq: WindowSequence, w: usize) -> usize { - match seq { - WindowSequence::OnlyLong => 255, - WindowSequence::LongStart => { - if w == 0 { - 111 - } else { - 31 - } - } - WindowSequence::EightShort => 31, - WindowSequence::LongStop => { - if w == 0 { - 111 - } else { - 255 - } - } - } -} - -/// The §4.6.12.3.1 reconstructed gain ladder for one `(window, band)` -/// slot: the `ALOC` / `ALEV` arrays indexed `0..=NADW+1`. -#[derive(Debug, Clone, PartialEq)] -struct Ladder { - /// `ALOC_{W,B}(m)`, `0 ≤ m ≤ NADW + 1`. - aloc: Vec, - /// `ALEV_{W,B}(m)`, `0 ≤ m ≤ NADW + 1`. Each entry is a power of - /// two `2^AdjLev(...)` (or the unit endpoint / `NADW == 0` value). - alev: Vec, -} - -impl Ladder { - /// Reconstruct the §4.6.12.3.1 ladder for window `w` of band `b` - /// (1-based spec band) from the per-window wire record. - /// - /// `window` carries the `adjust_num[B][W]` ladder entries (the - /// `(alevcode, aloccode)` pairs) for this `(b, w)` slot. - fn reconstruct( - window: &crate::gain_control_data::GainWindow, - seq: WindowSequence, - w: usize, - ) -> Self { - let nadw = window.adjustments.len(); - // ALOC / ALEV have NADW + 2 entries: indices 0..=NADW+1. - let mut aloc = Vec::with_capacity(nadw + 2); - let mut alev = Vec::with_capacity(nadw + 2); - - // Step (3): ALOC(0) = 0; ALEV(0) = 1 if NADW == 0 else ALEV(1). - // ALEV(0) is back-patched once ALEV(1) is known. - aloc.push(0); - alev.push(1.0); // placeholder; patched below for NADW > 0. - - // Steps (1)/(2): the transmitted ladder entries, m = 1..=NADW. - for adj in &window.adjustments { - aloc.push(adj_loc(adj.aloccode)); - alev.push(2f64.powi(adj_lev(adj.alevcode))); - } - - // Step (4): the endpoint, m = NADW + 1. - aloc.push(endpoint_aloc(seq, w)); - alev.push(1.0); - - // Patch ALEV(0): equals ALEV(1) when NADW > 0. - if nadw > 0 { - alev[0] = alev[1]; - } - - Ladder { aloc, alev } - } - - /// `M_{W,B,j} = max{ m : ALOC(m) ≤ j }` (§4.6.12.3.2 step 1). - /// - /// `ALOC` is monotonically increasing with `ALOC(0) = 0`, so for - /// any `j ≥ 0` at least `m = 0` qualifies; the answer is the index - /// of the last `ALOC` entry not exceeding `j`. - fn m_at(&self, j: u32) -> usize { - let mut m = 0usize; - for (idx, &loc) in self.aloc.iter().enumerate() { - if loc <= j { - m = idx; - } else { - break; - } - } - m - } -} - -/// `Inter(a, b, j) = 2^(((8 − j)·log2(a) + j·log2(b)) / 8)` -/// (§4.6.12.3.2) — the geometric interpolation between gain levels -/// `a` and `b` over the eight-sample ramp `0 ≤ j ≤ 8`. With `a`, `b` -/// powers of two the exponent is the linear blend of their `log2`s. -#[must_use] -fn inter(a: f64, b: f64, j: u32) -> f64 { - let la = a.log2(); - let lb = b.log2(); - let jf = j as f64; - let exp = ((8.0 - jf) * la + jf * lb) / 8.0; - 2f64.powf(exp) -} - -/// The fully-reconstructed §4.6.12.3.2 gain-control function for one -/// band of one frame: the per-window `AD_{W,B}(j) = 1 / GMF_{W,B}(j)` -/// arrays plus the `PFMD_B(j)` to thread into the next frame. -#[derive(Debug, Clone, PartialEq)] -pub struct BandGainFunction { - /// `AD_{W,B}(j)` per window. For long sequences the single (or - /// `w == 0`) window spans `0..512`; `EIGHT_SHORT_SEQUENCE` has - /// eight windows each spanning `0..64`. - pub ad: Vec>, - /// `PFMD_B(j)` for the next frame (§4.6.12.3.2 step 3). - pub pfmd_next: Vec, -} - -/// Build the §4.6.12.3.1–2 fragment-modification function `FMD_{W,B}` -/// for window `w` of one band. -fn fmd_window(ladder: &Ladder, seq: WindowSequence, w: usize) -> Vec { - let last = fmd_last_j(seq, w); - let mut fmd = vec![0.0f64; last + 1]; - for (j, slot) in fmd.iter_mut().enumerate() { - let m = ladder.m_at(j as u32); - let loc_m = ladder.aloc[m]; - let alev_m = ladder.alev[m]; - let alev_m1 = ladder.alev[m + 1]; - // FMD(j) = Inter(ALEV(M), ALEV(M+1), j − ALOC(M)) if - // ALOC(M) ≤ j ≤ ALOC(M) + 7, else ALEV(M+1). - let jj = j as u32; - *slot = if jj <= loc_m + 7 { - inter(alev_m, alev_m1, jj - loc_m) - } else { - alev_m1 - }; - } - fmd -} - -/// `ALEV_{W,B}(0)` for window `w` — the front gain used in the -/// §4.6.12.3.2 step-3 `GMF` composition. Reconstructs only the head of -/// the ladder. -fn alev0(band: &GainBand, w: usize) -> f64 { - let window = &band.windows[w]; - if window.adjustments.is_empty() { - 1.0 - } else { - 2f64.powi(adj_lev(window.adjustments[0].alevcode)) - } -} - -/// Compose the §4.6.12.3.2 step-3 gain-modification function `GMF` for -/// a non-`EIGHT_SHORT` band and thread `PFMD`. -fn gmf_long( - fmd: &[Vec], - band: &GainBand, - pfmd_prev: &[f64], - seq: WindowSequence, -) -> (Vec, Vec) { - // GMF spans 0..512 for the long sequences. - let mut gmf = vec![0.0f64; 512]; - let pfmd_next = match seq { - WindowSequence::OnlyLong => { - let a0 = alev0(band, 0); - for (j, slot) in gmf.iter_mut().enumerate() { - *slot = if j <= 255 { - a0 * pfmd_prev[j] - } else { - fmd[0][j - 256] - }; - } - // PFMD_B(j) = FMD_0,B(j), 0 ≤ j ≤ 255. - fmd[0][..256].to_vec() - } - WindowSequence::LongStart => { - let a0 = alev0(band, 0); - let a1 = alev0(band, 1); - for (j, slot) in gmf.iter_mut().enumerate() { - *slot = if j <= 255 { - a0 * a1 * pfmd_prev[j] - } else if j <= 367 { - a1 * fmd[0][j - 256] - } else if j <= 399 { - fmd[1][j - 368] - } else { - 1.0 - }; - } - // PFMD_B(j) = FMD_1,B(j), 0 ≤ j ≤ 31. - fmd[1][..32].to_vec() - } - WindowSequence::LongStop => { - let a0 = alev0(band, 0); - let a1 = alev0(band, 1); - for (j, slot) in gmf.iter_mut().enumerate() { - *slot = if j <= 111 { - 1.0 - } else if j <= 143 { - a0 * a1 * pfmd_prev[j - 112] - } else if j <= 255 { - a1 * fmd[0][j - 144] - } else { - fmd[1][j - 256] - }; - } - // PFMD_B(j) = FMD_1,B(j), 0 ≤ j ≤ 255. - fmd[1][..256].to_vec() - } - WindowSequence::EightShort => unreachable!("gmf_long called for short sequence"), - }; - (gmf, pfmd_next) -} - -/// Compose the §4.6.12.3.2 step-3 `EIGHT_SHORT_SEQUENCE` gain -/// modification: eight 64-sample `GMF` windows, threading `PFMD`. -fn gmf_short(fmd: &[Vec], band: &GainBand, pfmd_prev: &[f64]) -> (Vec>, Vec) { - let mut gmf: Vec> = Vec::with_capacity(8); - for w in 0..8 { - let a0 = alev0(band, w); - let mut g = vec![0.0f64; 64]; - for (j, slot) in g.iter_mut().enumerate() { - *slot = if j <= 31 { - if w == 0 { - a0 * pfmd_prev[j] - } else { - a0 * fmd[w - 1][j] - } - } else { - fmd[w][j - 32] - }; - } - gmf.push(g); - } - // PFMD_B(j) = FMD_7,B(j), 0 ≤ j ≤ 31. - let pfmd_next = fmd[7][..32].to_vec(); - (gmf, pfmd_next) -} - -impl BandGainFunction { - /// Reconstruct the §4.6.12.3.1–2 gain-control function `AD` for one - /// band of one frame. - /// - /// * `band` — the band's per-window ladder records (the - /// `bands[b - 1]` entry of the wire [`GainControlData`], spec band - /// `b ∈ 1..=3`). - /// * `seq` — the frame's `window_sequence`. - /// * `pfmd_prev` — `PFMD_B(j)` carried from the previous frame - /// (initial `1.0`). Length is 256 for the long sequences, 32 for - /// the short sequence. - /// - /// Returns the per-window `AD_{W,B}(j) = 1 / GMF_{W,B}(j)` arrays - /// and the `pfmd_next` to thread into the next frame. - #[must_use] - pub fn reconstruct(band: &GainBand, seq: WindowSequence, pfmd_prev: &[f64]) -> Self { - let n_win = num_windows(seq); - // Per-window FMD. - let fmd: Vec> = (0..n_win) - .map(|w| { - let ladder = Ladder::reconstruct(&band.windows[w], seq, w); - fmd_window(&ladder, seq, w) - }) - .collect(); - - match seq { - WindowSequence::EightShort => { - let (gmf, pfmd_next) = gmf_short(&fmd, band, pfmd_prev); - let ad = gmf - .iter() - .map(|g| g.iter().map(|&v| 1.0 / v).collect()) - .collect(); - BandGainFunction { ad, pfmd_next } - } - _ => { - let (gmf, pfmd_next) = gmf_long(&fmd, band, pfmd_prev, seq); - let ad = vec![gmf.iter().map(|&v| 1.0 / v).collect()]; - BandGainFunction { ad, pfmd_next } - } - } - } - - /// An identity gain function (`AD ≡ 1`) for a band with no gain - /// control active — the §4.6.12.3.3 `B == 0` case (band 0 never - /// carries a ladder) and any band beyond `max_band`. - /// - /// `seq` selects the window layout: one 512-sample window for the - /// long sequences, eight 64-sample windows for the short sequence. - #[must_use] - pub fn identity(seq: WindowSequence) -> Self { - match seq { - WindowSequence::EightShort => BandGainFunction { - ad: vec![vec![1.0; 64]; 8], - pfmd_next: vec![1.0; 32], - }, - _ => BandGainFunction { - ad: vec![vec![1.0; 512]], - pfmd_next: vec![1.0; 256], - }, - } - } -} - -/// One IPQF band's cross-frame gain-control state (§4.6.12.3.2–3): the -/// `PFMD_B(j)` fragment-modification carry and the `PT_B(j)` -/// gain-controlled block sample data tail. -/// -/// Construct with [`GainBandState::new`] (spec initial values: `PFMD ≡ -/// 1.0`, `PT ≡ 0.0`), then call [`GainBandState::window_overlap`] once -/// per frame; it returns the 256-sample-stride band sample data `V_B` -/// for this frame and advances both carries. -#[derive(Debug, Clone, PartialEq)] -pub struct GainBandState { - /// `PFMD_B(j)` — 256 entries (only the first - /// [`pfmd_len`]`(seq)` are read by the next frame). - pfmd: Vec, - /// `PT_B(j)` — 256 entries (only the written prefix is meaningful - /// for the next frame's overlap). - pt: Vec, -} - -impl Default for GainBandState { - fn default() -> Self { - Self::new() - } -} - -impl GainBandState { - /// A fresh band state with the §4.6.12 spec initial values: - /// `PFMD_B(j) = 1.0` and `PT_B(j) = 0.0`. - #[must_use] - pub fn new() -> Self { - GainBandState { - pfmd: vec![1.0; 256], - pt: vec![0.0; 256], - } - } - - /// The §4.6.12.3.3 gain-control windowing + overlapping for one band - /// of one frame. - /// - /// * `band` — this band's wire ladder (`None` for band 0 or a band - /// beyond `max_band`: gain control is inactive and `T = U`). - /// * `u` — the band spectrum data `U_{W,B}(j)`, the non-overlapped - /// per-band IMDCT output. For the long sequences this is a single - /// 512-sample window; for `EIGHT_SHORT_SEQUENCE` it is eight - /// 64-sample windows concatenated (window `w` at `u[64·w .. 64·w + - /// 64]`). - /// * `seq` — the frame's `window_sequence`. - /// - /// Returns the band sample data `V_B(j)` (the variable-length - /// per-frame fragment: 256 for `ONLY_LONG` / `EIGHT_SHORT`, 368 for - /// `LONG_START`, 144 for `LONG_STOP`) and updates the `PFMD` / `PT` - /// carries in place. - #[must_use] - pub fn window_overlap( - &mut self, - band: Option<&GainBand>, - u: &[f64], - seq: WindowSequence, - ) -> Vec { - // (1) windowing: T = AD · U (or T = U when gain control is off). - // The produced `pfmd_next` is the 32- or 256-entry prefix the - // next frame reads; write it into the persistent 256-buffer so - // the buffer never shrinks (any branch can read its prefix). - let t = match band { - Some(b) => { - let g = BandGainFunction::reconstruct(b, seq, &self.pfmd); - self.store_pfmd(&g.pfmd_next); - apply_gain(&g.ad, u, seq) - } - None => { - // Band 0 / inactive: T = U, PFMD threads as the identity. - let g = BandGainFunction::identity(seq); - self.store_pfmd(&g.pfmd_next); - u.to_vec() - } - }; - - // (2) overlapping: produce V_B and update PT_B. - self.overlap(&t, seq) - } - - /// Write the produced `PFMD` prefix into the persistent 256-entry - /// buffer (the buffer never shrinks, so any following frame can read - /// the prefix it needs). - fn store_pfmd(&mut self, produced: &[f64]) { - self.pfmd[..produced.len()].copy_from_slice(produced); - } - - /// The §4.6.12.3.3 step-(2) overlap for the gain-controlled block - /// sample data `t` (`T_{W,B}` concatenated window-major). - fn overlap(&mut self, t: &[f64], seq: WindowSequence) -> Vec { - match seq { - WindowSequence::OnlyLong => { - // V(j) = PT(j) + T0(j), 0..256; PT(j) = T0(j+256), 0..256. - let v = add_slices(&self.pt[..256], &t[..256]); - self.pt[..256].copy_from_slice(&t[256..512]); - v - } - WindowSequence::LongStart => { - // V(j) = PT(j) + T0(j), 0..256; - // V(j+256) = T0(j+256), 0..112; ⇒ V spans 0..368. - // PT(j) = T0(j+368), 0..32. - let mut v = vec![0.0f64; 368]; - add_into(&mut v[..256], &self.pt[..256], &t[..256]); - v[256..368].copy_from_slice(&t[256..368]); - self.pt[..32].copy_from_slice(&t[368..400]); - v - } - WindowSequence::EightShort => { - // V(j) = PT(j) + T0(j), W==0, 0..32; - // V(32W+j) = T_{W-1}(j+32) + T_W(j), 1..=7, 0..32; - // PT(j) = T7(j+32), 0..32. ⇒ V spans 0..256. - let mut v = vec![0.0f64; 256]; - // Window w occupies t[64·w .. 64·w + 64]. - add_into(&mut v[..32], &self.pt[..32], &t[..32]); - for w in 1..=7 { - let prev = &t[64 * (w - 1) + 32..64 * (w - 1) + 64]; - let cur = &t[64 * w..64 * w + 32]; - add_into(&mut v[32 * w..32 * w + 32], prev, cur); - } - self.pt[..32].copy_from_slice(&t[64 * 7 + 32..64 * 7 + 64]); - v - } - WindowSequence::LongStop => { - // V(j) = PT(j) + T0(j+112), 0..32; - // V(j+32) = T0(j+144), 0..112; ⇒ V spans 0..144. - // PT(j) = T0(j+256), 0..256. - let mut v = vec![0.0f64; 144]; - add_into(&mut v[..32], &self.pt[..32], &t[112..144]); - v[32..144].copy_from_slice(&t[144..256]); - self.pt[..256].copy_from_slice(&t[256..512]); - v - } - } - } -} - -/// Element-wise sum of two equal-length slices into a fresh `Vec`. -fn add_slices(a: &[f64], b: &[f64]) -> Vec { - a.iter().zip(b.iter()).map(|(&x, &y)| x + y).collect() -} - -/// Element-wise `dst[i] = a[i] + b[i]` over equal-length slices. -fn add_into(dst: &mut [f64], a: &[f64], b: &[f64]) { - for (d, (&x, &y)) in dst.iter_mut().zip(a.iter().zip(b.iter())) { - *d = x + y; - } -} - -/// Apply the §4.6.12.3.3 step-(1) gain `T_{W,B}(j) = AD_{W,B}(j) · -/// U_{W,B}(j)` window-major, returning the concatenated `T`. -fn apply_gain(ad: &[Vec], u: &[f64], seq: WindowSequence) -> Vec { - match seq { - WindowSequence::EightShort => { - let mut t = vec![0.0f64; u.len()]; - for (w, ad_w) in ad.iter().enumerate() { - for (j, &g) in ad_w.iter().enumerate() { - let idx = 64 * w + j; - t[idx] = g * u[idx]; - } - } - t - } - _ => ad[0].iter().zip(u.iter()).map(|(&g, &x)| g * x).collect(), - } -} - -/// The §4.6.12.3.2 `PFMD_B` **input** length a frame of `seq` reads -/// from the previous frame. -/// -/// The step-3 `GMF` composition reads `PFMD_B(j)` over `0..256` for -/// `ONLY_LONG` / `LONG_START` (their left half spans the full 256), but -/// only `0..32` for `LONG_STOP` (the `112 ≤ j ≤ 143` region) and -/// `EIGHT_SHORT` (the `W == 0`, `0 ≤ j ≤ 31` region). A -/// [`GainBandState`] keeps the full 256-entry buffer, so any branch can -/// always read the prefix it needs. -#[must_use] -pub fn pfmd_len(seq: WindowSequence) -> usize { - match seq { - WindowSequence::OnlyLong | WindowSequence::LongStart => 256, - WindowSequence::LongStop | WindowSequence::EightShort => 32, - } -} - -/// The §4.6.12.3.2 `PFMD_B` **output** length a frame of `seq` produces -/// for the next frame. -/// -/// `ONLY_LONG` / `LONG_STOP` emit `FMD(0..256)` (256 entries); -/// `LONG_START` / `EIGHT_SHORT` emit `FMD(0..32)` (32 entries). In a -/// legal `window_sequence` chain the produced length always matches -/// what the following frame's [`pfmd_len`] reads (`LONG_START` → -/// `EIGHT_SHORT`, `EIGHT_SHORT` → `LONG_STOP`, etc.). -#[must_use] -pub fn pfmd_produced_len(seq: WindowSequence) -> usize { - match seq { - WindowSequence::OnlyLong | WindowSequence::LongStop => 256, - WindowSequence::LongStart | WindowSequence::EightShort => 32, - } -} - -/// Look up the band's wire ladder from a [`GainControlData`] record for -/// spec band `b ∈ 1..=3`, or `None` when `b > max_band` (the band is -/// not gain-controlled, so its gain function is the identity). -#[must_use] -pub fn band_record(gcd: &GainControlData, b: usize) -> Option<&GainBand> { - if b == 0 || b > gcd.max_band as usize { - None - } else { - gcd.bands.get(b - 1) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn adj_loc_is_eight_times() { - assert_eq!(adj_loc(0), 0); - assert_eq!(adj_loc(1), 8); - assert_eq!(adj_loc(15), 120); - assert_eq!(adj_loc(31), 248); - } - - #[test] - fn adj_lev_is_offset_minus_four() { - assert_eq!(adj_lev(0), -4); - assert_eq!(adj_lev(4), 0); - assert_eq!(adj_lev(15), 11); - } - - #[test] - fn endpoint_aloc_per_sequence() { - assert_eq!(endpoint_aloc(WindowSequence::OnlyLong, 0), 256); - assert_eq!(endpoint_aloc(WindowSequence::LongStart, 0), 112); - assert_eq!(endpoint_aloc(WindowSequence::LongStart, 1), 32); - assert_eq!(endpoint_aloc(WindowSequence::EightShort, 3), 32); - assert_eq!(endpoint_aloc(WindowSequence::LongStop, 0), 112); - assert_eq!(endpoint_aloc(WindowSequence::LongStop, 1), 256); - } - - #[test] - fn inter_endpoints_are_exact() { - // Inter(a, b, 0) == a, Inter(a, b, 8) == b. - assert!((inter(2.0, 8.0, 0) - 2.0).abs() < 1e-12); - assert!((inter(2.0, 8.0, 8) - 8.0).abs() < 1e-12); - // Geometric midpoint at j == 4: sqrt(a·b). - assert!((inter(2.0, 8.0, 4) - (2.0f64 * 8.0).sqrt()).abs() < 1e-12); - } - - #[test] - fn empty_ladder_gives_unit_gain() { - // A band with an all-empty (adjust_num == 0) ladder produces - // AD ≡ 1 everywhere (GMF ≡ 1). - let band = GainBand { - windows: vec![crate::gain_control_data::GainWindow::default()], - }; - let pfmd = vec![1.0f64; 256]; - let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); - assert_eq!(g.ad.len(), 1); - assert_eq!(g.ad[0].len(), 512); - for &v in &g.ad[0] { - assert!((v - 1.0).abs() < 1e-12, "expected unit gain, got {v}"); - } - // PFMD threads forward as FMD_0 == 1. - assert!(g.pfmd_next.iter().all(|&v| (v - 1.0).abs() < 1e-12)); - } - - #[test] - fn identity_matches_empty_ladder() { - let band = GainBand { - windows: vec![crate::gain_control_data::GainWindow::default(); 8], - }; - let pfmd = vec![1.0f64; 32]; - let recon = BandGainFunction::reconstruct(&band, WindowSequence::EightShort, &pfmd); - let ident = BandGainFunction::identity(WindowSequence::EightShort); - assert_eq!(recon.ad.len(), ident.ad.len()); - for (r, i) in recon.ad.iter().zip(ident.ad.iter()) { - for (&rv, &iv) in r.iter().zip(i.iter()) { - assert!((rv - iv).abs() < 1e-12); - } - } - } - - #[test] - fn overlap_only_long_is_tdac_add() { - // Identity gain (band 0 / inactive): T == U. A 512-sample U; - // first frame V(j) = 0 + U(j) (PT starts 0); PT becomes - // U(256..512). Second frame with the same U: V(j) = - // U(256+j) + U(j). - let mut st = GainBandState::new(); - let u: Vec = (0..512).map(|j| (j as f64) * 0.01).collect(); - let v0 = st.window_overlap(None, &u, WindowSequence::OnlyLong); - assert_eq!(v0.len(), 256); - for j in 0..256 { - assert!((v0[j] - u[j]).abs() < 1e-12); - } - let v1 = st.window_overlap(None, &u, WindowSequence::OnlyLong); - for j in 0..256 { - assert!((v1[j] - (u[256 + j] + u[j])).abs() < 1e-12); - } - } - - #[test] - fn overlap_lengths_per_sequence() { - let u_long = vec![1.0f64; 512]; - let u_short = vec![1.0f64; 512]; // eight 64-sample windows. - assert_eq!( - GainBandState::new() - .window_overlap(None, &u_long, WindowSequence::OnlyLong) - .len(), - 256 - ); - assert_eq!( - GainBandState::new() - .window_overlap(None, &u_long, WindowSequence::LongStart) - .len(), - 368 - ); - assert_eq!( - GainBandState::new() - .window_overlap(None, &u_short, WindowSequence::EightShort) - .len(), - 256 - ); - assert_eq!( - GainBandState::new() - .window_overlap(None, &u_long, WindowSequence::LongStop) - .len(), - 144 - ); - } - - #[test] - fn overlap_eight_short_overlaps_adjacent_windows() { - // Identity gain. Each short window is constant c_w. The overlap - // V(32W+j) = T_{W-1}(j+32) + T_W(j) = c_{W-1} + c_W for the - // overlapped region, and PT becomes c_7. - let mut st = GainBandState::new(); - let mut u = vec![0.0f64; 512]; - for w in 0..8 { - for j in 0..64 { - u[64 * w + j] = (w as f64) + 1.0; - } - } - let v = st.window_overlap(None, &u, WindowSequence::EightShort); - assert_eq!(v.len(), 256); - // First segment: PT(0)=0 + T0 = 1. - assert!((v[0] - 1.0).abs() < 1e-12); - // Segment W=1: T0 + T1 = 1 + 2 = 3. - assert!((v[32] - 3.0).abs() < 1e-12); - // Segment W=7: T6 + T7 = 7 + 8 = 15. - assert!((v[32 * 7] - 15.0).abs() < 1e-12); - // PT now holds T7 = 8. - assert!((st.pt[0] - 8.0).abs() < 1e-12); - } - - #[test] - fn gain_then_overlap_scales_band() { - use crate::gain_control_data::{GainAdjust, GainWindow}; - // A constant band U ≡ 1.0; a single gain change makes AD ≠ 1 in - // the [256..) region (where the FMD lands). The V output picks - // up AD·U in that region. - let band = GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 6, // AdjLev=2 ⇒ ALEV=4 ⇒ AD=1/4 in ramp. - aloccode: 0, // ALOC=0. - }], - }], - }; - let mut st = GainBandState::new(); - let u = vec![1.0f64; 512]; - let v = st.window_overlap(Some(&band), &u, WindowSequence::OnlyLong); - assert_eq!(v.len(), 256); - // V is finite and the gain has been applied (not all 1.0). - assert!(v.iter().all(|x| x.is_finite())); - } - - #[test] - fn single_gain_change_scales_segment() { - use crate::gain_control_data::{GainAdjust, GainWindow}; - // One gain change at aloccode=2 (ALOC=16), alevcode=6 - // (AdjLev=2 ⇒ ALEV=4). NADW=1. - // ALOC = [0, 16, 256], ALEV = [4, 4, 1] (ALEV(0)=ALEV(1)=4). - // For j in 0..16, M=0, ALOC(0)=0, ramp Inter(4,4,j)=4 over the - // first 8 then flat ALEV(1)=4 ⇒ FMD=4 throughout 0..16. - let band = GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 6, - aloccode: 2, - }], - }], - }; - let pfmd = vec![1.0f64; 256]; - let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); - // GMF(256) = FMD_0(0) = ALEV at j=0 region. Since ALOC(1)=16, - // M(0)=0, ALEV(0)=4, ALEV(1)=4 ⇒ FMD(0)=4 ⇒ AD = 1/4. - assert!((g.ad[0][256] - 0.25).abs() < 1e-9, "AD={}", g.ad[0][256]); - // Beyond ALOC(NADW+1)=256 region: at j large, M=1 (ALOC(1)=16), - // ALEV(1)=4, ALEV(2)=1, j-16 > 7 ⇒ FMD = ALEV(2) = 1 ⇒ AD=1. - assert!((g.ad[0][511] - 1.0).abs() < 1e-9, "AD={}", g.ad[0][511]); - } - - /// A band carrying a ladder reconstructs a finite, strictly-positive - /// `AD` over the full window for every `window_sequence` — the - /// `GMF`/`AD` reciprocal pair is well-defined (no zero or infinity). - #[test] - fn ad_is_finite_positive_all_sequences() { - use crate::gain_control_data::{GainAdjust, GainWindow}; - for &seq in &[ - WindowSequence::OnlyLong, - WindowSequence::LongStart, - WindowSequence::LongStop, - WindowSequence::EightShort, - ] { - let n_win = num_windows(seq); - // Each window carries one mid-range gain change. - let windows = (0..n_win) - .map(|_| GainWindow { - adjustments: vec![GainAdjust { - alevcode: 7, // AdjLev=3 ⇒ ALEV=8. - aloccode: 1, // ALOC=8. - }], - }) - .collect(); - let band = GainBand { windows }; - let pfmd = vec![1.0f64; pfmd_len(seq)]; - let g = BandGainFunction::reconstruct(&band, seq, &pfmd); - for win in &g.ad { - for &v in win { - assert!(v.is_finite() && v > 0.0, "AD={v} for {seq:?}"); - } - } - // PFMD threads with the right produced length. - assert_eq!(g.pfmd_next.len(), pfmd_produced_len(seq)); - } - } - - /// The §4.6.12.3.2 inversion is exact: `AD(j) · GMF(j) == 1`. We - /// recover `GMF` as `1/AD` and confirm it round-trips to `AD`. - #[test] - fn ad_times_gmf_is_one() { - use crate::gain_control_data::{GainAdjust, GainWindow}; - let band = GainBand { - windows: vec![GainWindow { - adjustments: vec![ - GainAdjust { - alevcode: 8, - aloccode: 2, - }, - GainAdjust { - alevcode: 2, - aloccode: 10, - }, - ], - }], - }; - let pfmd = vec![1.0f64; 256]; - let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); - for &ad in &g.ad[0] { - let gmf = 1.0 / ad; - assert!((ad * gmf - 1.0).abs() < 1e-12); - } - } - - /// Pre-stream defaults: a first frame with `PFMD ≡ 1.0` and a band - /// whose only gain change sits at `ALOC = 0` scales the left-half - /// `GMF` region by `ALEV(0)` (the §4.6.12.3.2 step-3 `ONLY_LONG` - /// branch `ALEV(0)·PFMD`). - #[test] - fn long_left_half_scaled_by_alev0() { - use crate::gain_control_data::{GainAdjust, GainWindow}; - // alevcode=7 ⇒ AdjLev=3 ⇒ ALEV=8; aloccode=0 ⇒ ALOC=0. - // ALEV(0)=ALEV(1)=8. GMF(j) for j in 0..256 = ALEV(0)·PFMD(j) - // = 8·1 = 8 ⇒ AD = 1/8. - let band = GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 7, - aloccode: 0, - }], - }], - }; - let pfmd = vec![1.0f64; 256]; - let g = BandGainFunction::reconstruct(&band, WindowSequence::OnlyLong, &pfmd); - for &ad in &g.ad[0][..256] { - assert!((ad - 0.125).abs() < 1e-12, "AD={ad}"); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/gain_control_data.rs b/crates/vendor/oxideav-aac/src/gain_control_data.rs deleted file mode 100644 index 549dc829..00000000 --- a/crates/vendor/oxideav-aac/src/gain_control_data.rs +++ /dev/null @@ -1,305 +0,0 @@ -//! `gain_control_data()` parser + encoder primitive — ISO/IEC 14496-3 -//! §4.4.6.5 / Table 4.12. -//! -//! `gain_control_data()` is the wire record of the SSR (Scalable -//! Sample Rate, AOT 3) gain-control tool. SSR splits each AAC frame -//! through a 4-band polyphase quadrature filterbank (PQF) **before** -//! the MDCT, and applies a per-band, per-window gain-adjustment -//! ladder to attenuate pre-echo artefacts. The decoder reads the -//! ladder out of `gain_control_data()` and reverses it after the -//! per-band IMDCTs. The block rides inside an -//! `individual_channel_stream()` between `tns_data()` and -//! `spectral_data()`, gated by the dispatching -//! `gain_control_data_present` flag (Tables 4.44 / 4.50). -//! -//! ## Wire layout (Table 4.12) -//! -//! ```text -//! gain_control_data() { -//! max_band; 2 bits -//! for (bd = 1; bd <= max_band; bd++) { -//! for (wd = 0; wd < N(window_sequence); wd++) { -//! adjust_num[bd][wd]; 3 bits -//! for (ad = 0; ad < adjust_num[bd][wd]; ad++) { -//! alevcode[bd][wd][ad]; 4 bits -//! aloccode[bd][wd][ad]; W(seq, wd) bits -//! } -//! } -//! } -//! } -//! ``` -//! -//! Per Table 4.12 the per-window count `N(window_sequence)` and the -//! per-`(window_sequence, wd)` `aloccode` width `W(seq, wd)` are: -//! -//! | `window_sequence` | N | `W(seq, wd=0)` | `W(seq, wd≥1)` | -//! |--------------------------|---|----------------|----------------| -//! | `ONLY_LONG_SEQUENCE` | 1 | 5 | n/a | -//! | `LONG_START_SEQUENCE` | 2 | 4 | 2 | -//! | `EIGHT_SHORT_SEQUENCE` | 8 | 2 | 2 | -//! | `LONG_STOP_SEQUENCE` | 2 | 4 | 5 | -//! -//! `alevcode` is always 4 bits; `adjust_num` is always 3 bits (so -//! per `(bd, wd)` slot the ladder length is `0..=7`). -//! -//! The outer band loop iterates `1..=max_band` (note the **`bd = -//! 1`** start — band 0 carries no gain ladder by spec). -//! `max_band ∈ 0..=3` (2-bit field); when `max_band == 0` the body -//! collapses to just the 2-bit field. Per the §4.6.12 SSR backend -//! the legal `max_band` for a decoder targeting 4-band PQF output -//! is `0..=3`; the wire-format itself does not constrain values -//! further than the field width. -//! -//! ## What this module covers -//! -//! * [`GainControlData::parse`] — read a Table 4.12 block from a -//! [`BitReader`], surfacing the raw wire fields without applying -//! the §4.6.12 SSR gain-reconstruction (the actual ladder -//! application needs the SSR PQF backend, which is not part of -//! Phase 2). -//! * [`GainControlData::write`] — the inverse: serialise a -//! [`GainControlData`] onto a [`BitWriter`] in bit-exact -//! Table 4.12 form. Caller-side field overflow surfaces as -//! [`Error::GainControlDataEncodeInvalid`]. -//! -//! ## What this module does *not* cover -//! -//! * The §4.6.12 ladder-application loop (per-window gain envelope -//! reconstruction from `(alevcode, aloccode)` pairs into -//! sample-domain attenuation factors) is deferred until the SSR -//! PQF / IMDCT back-end lands. -//! * The normative §4.6.12 constraint that the SSR profile's -//! `gain_control_data_present` flag is **0** for AOTs other than -//! 3 (SSR) is the responsibility of the dispatching -//! `individual_channel_stream()` (not yet wired up); the parser -//! and writer here surface the literal Table 4.12 bytes -//! regardless of the surrounding AOT so future round work has -//! access to the raw decoded record. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::ics_info::WindowSequence; -use crate::{Error, Result}; - -/// Width in bits of the `max_band` field. Table 4.12. -pub const MAX_BAND_BITS: u32 = 2; - -/// Width in bits of the `adjust_num` field. Table 4.12. -pub const ADJUST_NUM_BITS: u32 = 3; - -/// Width in bits of the `alevcode` field. Table 4.12. -pub const ALEVCODE_BITS: u32 = 4; - -/// Maximum value of the `max_band` field (2-bit width cap). -pub const MAX_BAND_CAP: u8 = 0x03; - -/// Maximum value of the `adjust_num` field (3-bit width cap). Each -/// per-`(bd, wd)` slot can carry between 0 and 7 ladder entries. -pub const MAX_ADJUST_NUM: u8 = 0x07; - -/// Maximum value of the `alevcode` field (4-bit width cap). -pub const MAX_ALEVCODE: u8 = 0x0f; - -/// Per-window count `N(window_sequence)` from Table 4.12. -/// -/// * `ONLY_LONG_SEQUENCE` → 1 -/// * `LONG_START_SEQUENCE` → 2 -/// * `EIGHT_SHORT_SEQUENCE` → 8 -/// * `LONG_STOP_SEQUENCE` → 2 -pub fn num_windows(window_sequence: WindowSequence) -> usize { - match window_sequence { - WindowSequence::OnlyLong => 1, - WindowSequence::LongStart => 2, - WindowSequence::EightShort => 8, - WindowSequence::LongStop => 2, - } -} - -/// Width in bits of the `aloccode` field at the given -/// `(window_sequence, wd)` position per Table 4.12. -/// -/// * `ONLY_LONG_SEQUENCE` — always 5 (only `wd == 0` is reached). -/// * `LONG_START_SEQUENCE` — 4 if `wd == 0`, else 2. -/// * `EIGHT_SHORT_SEQUENCE` — always 2. -/// * `LONG_STOP_SEQUENCE` — 4 if `wd == 0`, else 5. -/// -/// Returns `0` for `wd` indices outside the per-sequence range — the -/// caller is responsible for honouring [`num_windows`] when stepping -/// the inner loop. -pub fn aloccode_bits(window_sequence: WindowSequence, wd: usize) -> u32 { - match window_sequence { - WindowSequence::OnlyLong => { - if wd == 0 { - 5 - } else { - 0 - } - } - WindowSequence::LongStart => match wd { - 0 => 4, - 1 => 2, - _ => 0, - }, - WindowSequence::EightShort => { - if wd < 8 { - 2 - } else { - 0 - } - } - WindowSequence::LongStop => match wd { - 0 => 4, - 1 => 5, - _ => 0, - }, - } -} - -/// Single `(alevcode, aloccode)` ladder entry within one -/// `(bd, wd)` slot. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct GainAdjust { - /// `alevcode[bd][wd][ad]` — 4-bit unsigned level code. - pub alevcode: u8, - /// `aloccode[bd][wd][ad]` — unsigned location code; field width - /// is selected by [`aloccode_bits`] from the surrounding - /// `window_sequence` and `wd` index. - pub aloccode: u8, -} - -/// Per-window ladder for a single `(bd, wd)` slot. The vector length -/// is the wire `adjust_num[bd][wd]` value (`0..=7`). -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct GainWindow { - /// Ladder entries in wire order. `len()` equals - /// `adjust_num[bd][wd]`. - pub adjustments: Vec, -} - -/// Per-band collection of per-window ladders for one `bd` value. -/// -/// `windows.len()` must equal [`num_windows`] for the surrounding -/// `window_sequence`. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct GainBand { - /// Per-`wd` ladder entries. `windows[wd]` is the - /// `(bd, wd)` slot. - pub windows: Vec, -} - -/// Parsed `gain_control_data()` block (Table 4.12). -/// -/// `bands.len()` equals `max_band` (the **wire** field value); the -/// per-spec `bd = 1..=max_band` outer loop maps onto `bands[bd - 1]`. -/// `bands` is empty when `max_band == 0` (the body collapses to a -/// bare 2-bit zero). -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct GainControlData { - /// `max_band` per Table 4.12 — 2-bit field, `0..=3`. The length - /// of `bands` is `max_band`. - pub max_band: u8, - /// Per-band ladders, indexed `bands[bd - 1]` for spec band - /// `bd ∈ 1..=max_band`. `bands.len() == max_band as usize`. - pub bands: Vec, -} - -impl GainControlData { - /// Parse a `gain_control_data()` block from `reader`, using - /// `window_sequence` to choose the per-window count and the - /// `aloccode` field widths. - /// - /// Returns [`Error::UnexpectedEnd`] on bit-reader underflow. - /// Never returns an encode-side variant — every field of - /// Table 4.12 is fixed-width and unconditionally well-formed up - /// to bit-position arithmetic. - pub fn parse(reader: &mut BitReader<'_>, window_sequence: WindowSequence) -> Result { - let max_band = read_u8(reader, MAX_BAND_BITS)?; - let n_win = num_windows(window_sequence); - let mut bands = Vec::with_capacity(max_band as usize); - for _bd in 1..=max_band as usize { - let mut windows = Vec::with_capacity(n_win); - for wd in 0..n_win { - let adjust_num = read_u8(reader, ADJUST_NUM_BITS)?; - let aloc_bits = aloccode_bits(window_sequence, wd); - let mut adjustments = Vec::with_capacity(adjust_num as usize); - for _ad in 0..adjust_num as usize { - let alevcode = read_u8(reader, ALEVCODE_BITS)?; - let aloccode = read_u8(reader, aloc_bits)?; - adjustments.push(GainAdjust { alevcode, aloccode }); - } - windows.push(GainWindow { adjustments }); - } - bands.push(GainBand { windows }); - } - Ok(GainControlData { max_band, bands }) - } - - /// Encode `gain_control_data()` onto `writer`, the bit-exact - /// inverse of [`GainControlData::parse`]. - /// - /// Returns [`Error::GainControlDataEncodeInvalid`] if any of the - /// following caller-side invariants are violated: - /// - /// * `max_band > MAX_BAND_CAP` (2-bit `max_band` overflow). - /// * `bands.len() != max_band as usize` (the outer band-loop - /// count must match the dispatched wire value). - /// * Any `band.windows.len() != num_windows(window_sequence)` - /// (the per-band window count must match the wire dispatch). - /// * Any `window.adjustments.len() > MAX_ADJUST_NUM as usize` - /// (3-bit `adjust_num` overflow). - /// * Any `GainAdjust::alevcode > MAX_ALEVCODE` (4-bit overflow). - /// * Any `GainAdjust::aloccode` exceeds the - /// `(1 << aloccode_bits(seq, wd)) - 1` cap for its slot. - pub fn write(&self, writer: &mut BitWriter, window_sequence: WindowSequence) -> Result<()> { - if self.max_band > MAX_BAND_CAP { - return Err(Error::GainControlDataEncodeInvalid); - } - if self.bands.len() != self.max_band as usize { - return Err(Error::GainControlDataEncodeInvalid); - } - let n_win = num_windows(window_sequence); - for band in &self.bands { - if band.windows.len() != n_win { - return Err(Error::GainControlDataEncodeInvalid); - } - for (wd, window) in band.windows.iter().enumerate() { - if window.adjustments.len() > MAX_ADJUST_NUM as usize { - return Err(Error::GainControlDataEncodeInvalid); - } - let aloc_bits = aloccode_bits(window_sequence, wd); - let aloc_cap: u32 = if aloc_bits == 0 { - 0 - } else { - (1u32 << aloc_bits) - 1 - }; - for adj in &window.adjustments { - if adj.alevcode > MAX_ALEVCODE { - return Err(Error::GainControlDataEncodeInvalid); - } - if adj.aloccode as u32 > aloc_cap { - return Err(Error::GainControlDataEncodeInvalid); - } - } - } - } - - writer.write_u32(self.max_band as u32, MAX_BAND_BITS); - for band in &self.bands { - for (wd, window) in band.windows.iter().enumerate() { - let adjust_num = window.adjustments.len() as u32; - writer.write_u32(adjust_num, ADJUST_NUM_BITS); - let aloc_bits = aloccode_bits(window_sequence, wd); - for adj in &window.adjustments { - writer.write_u32(adj.alevcode as u32, ALEVCODE_BITS); - writer.write_u32(adj.aloccode as u32, aloc_bits); - } - } - } - Ok(()) - } -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} diff --git a/crates/vendor/oxideav-aac/src/hcr.rs b/crates/vendor/oxideav-aac/src/hcr.rs deleted file mode 100644 index 32db4aa0..00000000 --- a/crates/vendor/oxideav-aac/src/hcr.rs +++ /dev/null @@ -1,677 +0,0 @@ -//! Huffman codeword reordering (HCR) for AAC spectral data — ISO/IEC -//! 14496-3 §4.6.16.3. -//! -//! HCR is the error-resilience tool selected by -//! `aacSpectralDataResilienceFlag` (the Table 4.50 spectral branch -//! parsed by [`crate::ics_body::IcsBody::parse_er`]): the -//! `reordered_spectral_data()` block carries the same Huffman -//! codewords as a non-resilient `spectral_data()`, but the *priority* -//! codewords (PCWs) are placed at known segment boundaries so a bit -//! error inside one codeword cannot propagate into them. -//! -//! ## What this module covers (round 375) -//! -//! The deterministic, header-only **scaffolding** of HCR — the parts -//! that depend only on the two transmitted length fields, the active -//! `section_data()` codebooks, and the window geometry, *not* on the -//! reordered bit payload itself: -//! -//! * The §4.6.16.3.3.1 pre-sorting **priority metric** -//! ([`codebook_priority`], [`assigned_unit_nr`]) — the -//! `codebookPriority[32]` table and the `assignedUnitNr` formula -//! that determines which codewords become PCWs. -//! * The §4.6.16.3.3.2 **segment width / instantiation** math -//! ([`MAX_CW_LEN`], [`segment_width`], [`Segmentation::new`]) — the -//! `segmentWidth = min(maxCwLen, length_of_longest_codeword)` -//! derivation and the segment count / last-segment-remainder rule -//! sized by `length_of_reordered_spectral_data`. -//! -//! The full §4.6.16.3.4 reordered-payload **decode** (the PCW / -//! non-PCW `WriteCodewordToSegment` trial loop inverted to recover the -//! codeword bit positions) keys off this scaffold and is a later -//! milestone; it needs an HCR-bearing conformance stream to validate -//! bit-exactly. -//! -//! ## Provenance -//! -//! Every constant and formula here is from ISO/IEC 14496-3 -//! §4.6.16.3.3 / §4.6.16.3.5 (Table 4.170, the `codebookPriority[32]` -//! and `assignedUnitNr` listings) staged under `docs/audio/aac/`. The -//! `maxCwLen` column of Table 4.170 is a numeric data table; the -//! pre-sorting metric is the spec's own arithmetic. No external HCR -//! implementation was consulted. - -/// `maxCwLen[cb]` — the maximum Huffman codeword length, in bits, for -/// each spectral codebook (ISO/IEC 14496-3 Table 4.170). -/// -/// Indexed by the raw `sect_cb` value (`0..=31`): the base §4.A.1 -/// books are `0..=11`, the §4.6.16.4 virtual codebooks (used only in -/// the error-resilient `section_data()` 5-bit branch) are `16..=31`. -/// Codebook `0` (`ZERO_HCB`) and the reserved gaps `12..=15` carry no -/// codeword, so their entry is `0`. -pub const MAX_CW_LEN: [u8; 32] = [ - 0, // 0 ZERO_HCB - 11, // 1 - 9, // 2 - 20, // 3 - 16, // 4 - 13, // 5 - 11, // 6 - 14, // 7 - 12, // 8 - 17, // 9 - 14, // 10 - 49, // 11 ESC_HCB - 0, // 12 reserved - 0, // 13 NOISE_HCB (no spectral codeword) - 0, // 14 INTENSITY_HCB2 (no spectral codeword) - 0, // 15 INTENSITY_HCB (no spectral codeword) - 14, // 16 virtual - 17, // 17 virtual - 21, // 18 virtual - 21, // 19 virtual - 25, // 20 virtual - 25, // 21 virtual - 29, // 22 virtual - 29, // 23 virtual - 29, // 24 virtual - 29, // 25 virtual - 33, // 26 virtual - 33, // 27 virtual - 33, // 28 virtual - 37, // 29 virtual - 37, // 30 virtual - 41, // 31 virtual -]; - -/// `codebookPriority[32]` — the §4.6.16.3.3.1 pre-sorting priority -/// assigned to each codebook (ISO/IEC 14496-3 §4.6.16.3.3.1). -/// -/// Higher values are pre-sorted earlier (become PCWs). The `x` -/// entries in the spec — codebooks `0` (`ZERO_HCB`) and the reserved -/// `12..=15` — carry no spectral codeword and never participate in -/// reordering, so they are mapped to `0`. -/// -/// The spec listing is: -/// `{x,21,21,20,20,19,19,18,18,17,17,0,x,x,x,x,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1}`. -pub const CODEBOOK_PRIORITY: [u8; 32] = [ - 0, // 0 (x — no codeword) - 21, // 1 - 21, // 2 - 20, // 3 - 20, // 4 - 19, // 5 - 19, // 6 - 18, // 7 - 18, // 8 - 17, // 9 - 17, // 10 - 0, // 11 ESC_HCB - 0, // 12 (x) - 0, // 13 (x) - 0, // 14 (x) - 0, // 15 (x) - 16, // 16 - 15, // 17 - 14, // 18 - 13, // 19 - 12, // 20 - 11, // 21 - 10, // 22 - 9, // 23 - 8, // 24 - 7, // 25 - 6, // 26 - 5, // 27 - 4, // 28 - 3, // 29 - 2, // 30 - 1, // 31 -]; - -/// The §4.6.16.3.3.1 pre-sorting priority of a raw codebook value. -/// -/// Returns `0` for a codebook that carries no spectral codeword -/// (`ZERO_HCB`, the reserved `12..=15`, and any value `>= 32`). -#[must_use] -pub fn codebook_priority(cb: u8) -> u8 { - CODEBOOK_PRIORITY.get(cb as usize).copied().unwrap_or(0) -} - -/// `maxCwLen` for a raw codebook value (Table 4.170). Returns `0` for -/// a codebook that carries no spectral codeword or an out-of-range -/// value. -#[must_use] -pub fn max_cw_len(cb: u8) -> u8 { - MAX_CW_LEN.get(cb as usize).copied().unwrap_or(0) -} - -/// The §4.6.16.3.3.1 `assignedUnitNr` metric for one unit (a group of -/// four spectral lines = two 2-D or one 4-D codeword). -/// -/// ```text -/// assignedUnitNr = ( codebookPriority[cb] * maxNrOfLinesInWindow -/// + nrOfFirstLineInUnit ) * maxNrOfWindows + window -/// ``` -/// -/// * `cb` — the codebook of the unit (its priority drives the -/// energy-based second pre-sorting step). -/// * `max_lines_in_window` — `1024` for one long window, `128` for -/// eight short windows. -/// * `nr_of_first_line_in_unit` — the first spectral line index of -/// the unit (a multiple of 4: `0..=1020` long, `0..=124` short). -/// * `max_windows` — `1` long, `8` short. -/// * `window` — `0` long, `0..=7` short. -/// -/// Units sorted ascending by this number give the pre-sorted codeword -/// order (PCWs first). -#[must_use] -pub fn assigned_unit_nr( - cb: u8, - max_lines_in_window: u32, - nr_of_first_line_in_unit: u32, - max_windows: u32, - window: u32, -) -> u32 { - (u32::from(codebook_priority(cb)) * max_lines_in_window + nr_of_first_line_in_unit) - * max_windows - + window -} - -/// The §4.6.16.3.3.2 per-codebook segment width: -/// `segmentWidth = min(maxCwLen, length_of_longest_codeword)`. -/// -/// `length_of_longest_codeword` is the transmitted 6-bit field -/// (clamped to `49` for the reserved `50..=63` per §4.6.16.3.2). A -/// codebook with no codeword (`maxCwLen == 0`) yields a zero-width -/// segment. -#[must_use] -pub fn segment_width(cb: u8, length_of_longest_codeword: u8) -> u8 { - max_cw_len(cb).min(clamp_longest_codeword(length_of_longest_codeword)) -} - -/// Clamp the transmitted `length_of_longest_codeword` to its valid -/// range per §4.6.16.3.2: values `50..=63` are reserved and a current -/// decoder replaces them with `49`. -#[must_use] -pub fn clamp_longest_codeword(length_of_longest_codeword: u8) -> u8 { - length_of_longest_codeword.min(49) -} - -/// Clamp the transmitted `length_of_reordered_spectral_data` to its -/// valid range per §4.6.16.3.2. -/// -/// The maximum is `6144` bits for an SCE / CCE / LFE and `12288` bits -/// for a CPE; larger values are reserved and a current decoder -/// replaces them with the valid maximum. -#[must_use] -pub fn clamp_reordered_length(length_of_reordered_spectral_data: u16, is_cpe: bool) -> u16 { - let max = if is_cpe { 12288 } else { 6144 }; - length_of_reordered_spectral_data.min(max) -} - -/// The §4.6.16.3.3.2 segment layout for one `reordered_spectral_data()` -/// block: the per-segment bit widths derived from the active codebooks -/// and the two transmitted length fields. -/// -/// "Segments are instantiated until the available buffer is -/// exhausted, whereas the size of this buffer is given by -/// `length_of_reordered_spectral_data`. The remaining bits at the end -/// of the buffer increase the size of the last segment." -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Segmentation { - /// Per-segment bit width, in segment-instantiation order. The - /// final entry absorbs any remaining buffer bits, so it may exceed - /// its codebook's `segmentWidth`. - pub segment_bits: Vec, - /// `length_of_reordered_spectral_data` (clamped) — the total - /// buffer size in bits. `segment_bits` sums to exactly this. - pub total_bits: u32, -} - -impl Segmentation { - /// Build the segment layout from the pre-sorted PCW segment widths - /// and the (clamped) reordered-buffer length. - /// - /// `pcw_segment_widths` is the ordered list of - /// `segmentWidth = min(maxCwLen, length_of_longest_codeword)` for - /// each priority codeword in pre-sorted order. Segments are taken - /// in order while their cumulative width fits the buffer; once the - /// next full segment would overflow (or the list is exhausted), the - /// remaining buffer bits are folded into the last instantiated - /// segment. - /// - /// Returns an empty layout (`segment_bits` empty, `total_bits` as - /// given) when the buffer is zero-length. - #[must_use] - pub fn new(pcw_segment_widths: &[u8], total_bits: u32) -> Self { - let mut segment_bits: Vec = Vec::new(); - if total_bits == 0 { - return Segmentation { - segment_bits, - total_bits, - }; - } - - let mut used: u32 = 0; - for &w in pcw_segment_widths { - let w = u32::from(w); - if used + w > total_bits { - // The next full segment would overrun the buffer; stop - // instantiating new segments. The remainder folds into - // the last one below. - break; - } - segment_bits.push(w); - used += w; - } - - // The remaining bits at the end of the buffer increase the size - // of the last segment (§4.6.16.3.3.2). If no segment fit at all - // (every width exceeds the whole buffer, or the width list is - // empty), the whole buffer is one segment. - let remainder = total_bits - used; - if remainder > 0 { - if let Some(last) = segment_bits.last_mut() { - *last += remainder; - } else { - segment_bits.push(remainder); - } - } - - Segmentation { - segment_bits, - total_bits, - } - } - - /// `numberOfSegments` — the count of instantiated segments. - #[must_use] - pub fn number_of_segments(&self) -> usize { - self.segment_bits.len() - } - - /// The global bit offset of segment `i`'s first bit within the - /// reordered buffer (the running sum of preceding segment widths). - #[must_use] - pub(crate) fn segment_start(&self, i: usize) -> u32 { - self.segment_bits[..i].iter().sum() - } -} - -/// Write direction within a segment (§4.6.16.3.3.3). PCWs and -/// odd-numbered sets use [`Direction::Forward`] (left-to-right); -/// the direction toggles from set to set. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Direction { - /// Left-to-right: fill from the leftmost remaining bit of the - /// segment's free region. - Forward, - /// Right-to-left: fill from the rightmost remaining bit. - Backward, -} - -impl Direction { - /// Toggle the write direction (`ToggleWriteDirection()`). - #[must_use] - pub fn toggled(self) -> Self { - match self { - Direction::Forward => Direction::Backward, - Direction::Backward => Direction::Forward, - } - } -} - -/// The fully-resolved bit placement of every codeword in a -/// `reordered_spectral_data()` block — for each codeword, the ordered -/// list of global bit positions (within the reordered buffer) that -/// carry its bits, most-significant-bit first. -/// -/// This is the inverse of the §4.6.16.3.3.4 `ReorderSpectralData()` -/// writing scheme: it runs the same PCW-then-non-PCW set / trial loop -/// to determine *where* each codeword's bits land, so a decoder that -/// already knows each codeword's bit length (PCWs are decoded first -/// from the segment starts, then the non-PCW lengths become known) can -/// gather a codeword's scattered bits back into a contiguous codeword -/// for Huffman decoding. -/// -/// The bit lengths themselves come from Huffman-decoding the codewords -/// in place (the §4.6.16.3.4 decode references the ordinary -/// §4.6.3.3 spectral decode); this structure only resolves geometry -/// once those lengths are known. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ReorderPlan { - /// `codeword_bits[c]` — the global buffer bit positions of - /// codeword `c`, in codeword-MSB-first order. - pub codeword_bits: Vec>, -} - -impl ReorderPlan { - /// Resolve the bit placement for `codeword_lengths` codewords over - /// `seg` segments, following the §4.6.16.3.3.4 writing scheme. - /// - /// * `codeword_lengths[c]` — the bit length of codeword `c`, in - /// pre-sorted order (PCWs first). `codeword_lengths.len()` is - /// `numberOfCodewords`. - /// * `seg` — the [`Segmentation`] giving `numberOfSegments` and the - /// per-segment bit widths. - /// - /// `numberOfSets = ceil(numberOfCodewords / numberOfSegments)`. The - /// first `numberOfSegments` codewords are the PCWs (set 0), each - /// written forward from its own segment's start; the rest are - /// non-PCWs distributed by the set / trial loop with the per-set - /// direction toggle. - /// - /// Returns `None` if the codewords do not fit the buffer (the sum - /// of `codeword_lengths` exceeds `total_bits`, or a segment - /// overflows) — a conforming stream always fits by construction. - #[must_use] - pub fn build(codeword_lengths: &[u32], seg: &Segmentation) -> Option { - let num_segments = seg.number_of_segments(); - let num_codewords = codeword_lengths.len(); - if num_segments == 0 { - return if num_codewords == 0 { - Some(ReorderPlan { - codeword_bits: Vec::new(), - }) - } else { - None - }; - } - - // Per-segment free-region cursors. Segment `s` spans local bits - // `[0, width)`. `low[s]` counts bits consumed from the low end - // (forward writes), `high[s]` counts bits consumed from the high - // end (backward writes). The free region is the local-bit range - // `[low[s], width - high[s])`; free bit count is - // `width - low[s] - high[s]`. - let widths: Vec = seg.segment_bits.clone(); - let mut low: Vec = vec![0; num_segments]; - let mut high: Vec = vec![0; num_segments]; - let seg_start: Vec = (0..num_segments).map(|s| seg.segment_start(s)).collect(); - - let mut codeword_bits: Vec> = vec![Vec::new(); num_codewords]; - // remainingBitsInCodeword[] - let mut remaining: Vec = codeword_lengths.to_vec(); - - // Inlined `WriteCodewordToSegment(cw, sg, dir)`: write up to - // `remaining[cw]` bits of codeword `cw` into the free region of - // segment `sg` in `dir`, recording the global bit positions - // MSB-first. Returns bits written. - macro_rules! write_cw_to_seg { - ($cw:expr, $sg:expr, $dir:expr) => {{ - let cw = $cw; - let sg = $sg; - let dir = $dir; - let free = widths[sg] - low[sg] - high[sg]; - let n = remaining[cw].min(free); - for _ in 0..n { - let local = match dir { - Direction::Forward => { - let l = low[sg]; - low[sg] += 1; - l - } - Direction::Backward => { - // Outermost free bit from the right. - let l = widths[sg] - 1 - high[sg]; - high[sg] += 1; - l - } - }; - codeword_bits[cw].push(seg_start[sg] + local); - } - remaining[cw] -= n; - n - }}; - } - - // First step: write PCWs (set 0). Codeword `i` → segment `i`, - // forward. - for codeword in 0..num_segments.min(num_codewords) { - write_cw_to_seg!(codeword, codeword, Direction::Forward); - } - - // numberOfSets = ceil(numberOfCodewords / numberOfSegments). - let num_sets = num_codewords.div_ceil(num_segments); - - // Second step: write non-PCWs (sets 1..num_sets). - let mut write_direction = Direction::Forward; - for set in 1..num_sets { - write_direction = write_direction.toggled(); - for trial in 0..num_segments { - for codeword_base in 0..num_segments { - let segment = (trial + codeword_base) % num_segments; - let codeword = codeword_base + set * num_segments; - if codeword >= num_codewords { - continue; - } - if remaining[codeword] > 0 { - write_cw_to_seg!(codeword, segment, write_direction); - } - } - } - } - - // Every codeword must be fully placed for a conforming stream. - if remaining.iter().any(|&r| r > 0) { - return None; - } - - Some(ReorderPlan { codeword_bits }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn max_cw_len_table_spot_checks() { - assert_eq!(max_cw_len(0), 0); - assert_eq!(max_cw_len(1), 11); - assert_eq!(max_cw_len(3), 20); - assert_eq!(max_cw_len(11), 49); - // No-codeword books. - assert_eq!(max_cw_len(13), 0); - assert_eq!(max_cw_len(15), 0); - // Virtual codebooks. - assert_eq!(max_cw_len(16), 14); - assert_eq!(max_cw_len(31), 41); - assert_eq!(max_cw_len(200), 0); - } - - #[test] - fn codebook_priority_table_spot_checks() { - assert_eq!(codebook_priority(0), 0); - assert_eq!(codebook_priority(1), 21); - assert_eq!(codebook_priority(2), 21); - assert_eq!(codebook_priority(10), 17); - assert_eq!(codebook_priority(11), 0); - assert_eq!(codebook_priority(16), 16); - assert_eq!(codebook_priority(31), 1); - assert_eq!(codebook_priority(99), 0); - } - - #[test] - fn assigned_unit_nr_long_window() { - // Long window: max_lines=1024, max_windows=1, window=0. - // unit at line 0, cb 1 (priority 21): 21*1024 + 0 = 21504. - assert_eq!(assigned_unit_nr(1, 1024, 0, 1, 0), 21504); - // Same line, higher cb 11 (priority 0): just the line offset. - assert_eq!(assigned_unit_nr(11, 1024, 0, 1, 0), 0); - // A higher-priority codebook sorts ahead of a lower one at the - // same line. - assert!(assigned_unit_nr(1, 1024, 4, 1, 0) > assigned_unit_nr(31, 1024, 4, 1, 0)); - } - - #[test] - fn assigned_unit_nr_short_window_interleaves_window() { - // Short window: max_lines=128, max_windows=8. Two units at the - // same line + codebook but different windows order by window. - let a = assigned_unit_nr(5, 128, 8, 8, 0); - let b = assigned_unit_nr(5, 128, 8, 8, 3); - assert_eq!(b - a, 3); - } - - #[test] - fn segment_width_is_min_of_maxcwlen_and_longest() { - // cb 3 maxCwLen 20, longest 16 → 16. - assert_eq!(segment_width(3, 16), 16); - // cb 2 maxCwLen 9, longest 16 → 9. - assert_eq!(segment_width(2, 16), 9); - // longest in reserved range clamps to 49. - assert_eq!(segment_width(11, 60), 49); - } - - #[test] - fn clamp_reordered_length_per_element_kind() { - assert_eq!(clamp_reordered_length(7000, false), 6144); - assert_eq!(clamp_reordered_length(7000, true), 7000); - assert_eq!(clamp_reordered_length(20000, true), 12288); - assert_eq!(clamp_reordered_length(100, false), 100); - } - - #[test] - fn segmentation_folds_remainder_into_last_segment() { - // Three PCW segments of width 10, 10, 10; buffer of 35 bits. - // All three fit (30 bits); the trailing 5 bits fold into the - // last segment → 10, 10, 15. - let seg = Segmentation::new(&[10, 10, 10], 35); - assert_eq!(seg.segment_bits, vec![10, 10, 15]); - assert_eq!(seg.number_of_segments(), 3); - assert_eq!(seg.segment_bits.iter().sum::(), 35); - } - - #[test] - fn segmentation_stops_before_overrun() { - // Widths 10, 10, 10 but only 25 bits of buffer: two full - // segments fit (20 bits); the third would overrun, so the - // remaining 5 bits fold into the second segment → 10, 15. - let seg = Segmentation::new(&[10, 10, 10], 25); - assert_eq!(seg.segment_bits, vec![10, 15]); - assert_eq!(seg.segment_bits.iter().sum::(), 25); - } - - #[test] - fn segmentation_single_segment_when_first_width_exceeds_buffer() { - // First width 50 > buffer 30: no full segment fits; the whole - // buffer becomes one segment. - let seg = Segmentation::new(&[50, 50], 30); - assert_eq!(seg.segment_bits, vec![30]); - assert_eq!(seg.number_of_segments(), 1); - } - - #[test] - fn segmentation_zero_buffer_is_empty() { - let seg = Segmentation::new(&[10, 10], 0); - assert!(seg.segment_bits.is_empty()); - assert_eq!(seg.number_of_segments(), 0); - assert_eq!(seg.total_bits, 0); - } - - #[test] - fn segmentation_exact_fit_no_remainder() { - let seg = Segmentation::new(&[8, 8, 8], 24); - assert_eq!(seg.segment_bits, vec![8, 8, 8]); - assert_eq!(seg.segment_bits.iter().sum::(), 24); - } - - // ---- ReorderPlan: bit-placement geometry ---- - - /// Assert the placement is a bijection over `[0, total_bits)`: every - /// codeword bit position is distinct and the union covers every - /// buffer bit exactly once (true whenever the codewords fully fill - /// the buffer). - fn assert_bijective(plan: &ReorderPlan, total_bits: u32) { - let mut seen = vec![false; total_bits as usize]; - let mut count = 0u32; - for cw in &plan.codeword_bits { - for &p in cw { - assert!(p < total_bits, "position {p} out of range"); - assert!(!seen[p as usize], "position {p} written twice"); - seen[p as usize] = true; - count += 1; - } - } - assert_eq!(count, total_bits, "not every buffer bit was covered"); - } - - #[test] - fn reorder_pcws_start_at_segment_boundaries() { - // 3 segments of 8 bits, 3 PCWs each exactly 8 bits long → each - // codeword fills its own segment, forward, starting at the - // segment boundary. - let seg = Segmentation::new(&[8, 8, 8], 24); - let plan = ReorderPlan::build(&[8, 8, 8], &seg).unwrap(); - assert_eq!(plan.codeword_bits[0], (0..8).collect::>()); - assert_eq!(plan.codeword_bits[1], (8..16).collect::>()); - assert_eq!(plan.codeword_bits[2], (16..24).collect::>()); - assert_bijective(&plan, 24); - } - - #[test] - fn reorder_nonpcws_fill_gaps_with_direction_toggle() { - // 2 segments of 10 bits = 20-bit buffer. Codewords: - // PCWs (set 0): cw0=4 bits, cw1=4 bits (start of each segment). - // Set 1 (backward): cw2=6, cw3=6 — fill the remaining 6 bits of - // each segment from the right. - let seg = Segmentation::new(&[10, 10], 20); - let plan = ReorderPlan::build(&[4, 4, 6, 6], &seg).unwrap(); - // cw0 forward at segment 0 start. - assert_eq!(plan.codeword_bits[0], vec![0, 1, 2, 3]); - // cw1 forward at segment 1 start (offset 10). - assert_eq!(plan.codeword_bits[1], vec![10, 11, 12, 13]); - // cw2 is the first non-PCW: set 1, trial 0, codeword_base 0 → - // segment 0, backward → bits 9,8,7,6,5,4. - assert_eq!(plan.codeword_bits[2], vec![9, 8, 7, 6, 5, 4]); - // cw3 → segment 1, backward → bits 19,18,17,16,15,14. - assert_eq!(plan.codeword_bits[3], vec![19, 18, 17, 16, 15, 14]); - assert_bijective(&plan, 20); - } - - #[test] - fn reorder_codeword_spanning_multiple_segments() { - // 3 segments of 5 bits = 15-bit buffer. PCWs cw0,cw1,cw2 each 3 - // bits (forward from each segment start, 2 free bits left each). - // Set 1 (backward): cw3=4, cw4=4, cw5=4 — each is longer than one - // segment's 2-bit remainder, so it spans into the next segment - // across trials (modulo shift). - let seg = Segmentation::new(&[5, 5, 5], 15); - let plan = ReorderPlan::build(&[3, 3, 3, 2, 2, 2], &seg).unwrap(); - // Total bits placed equals buffer size, bijective. - assert_bijective(&plan, 15); - // PCWs at segment starts. - assert_eq!(plan.codeword_bits[0], vec![0, 1, 2]); - assert_eq!(plan.codeword_bits[1], vec![5, 6, 7]); - assert_eq!(plan.codeword_bits[2], vec![10, 11, 12]); - } - - #[test] - fn reorder_partial_codeword_continues_next_trial() { - // 2 segments of 6 bits = 12-bit buffer. PCWs cw0=2, cw1=2 leave - // 4 free bits per segment. Set 1 backward: cw2=6, cw3=2. cw2 (6 - // bits) into segment 0's 4 free bits (trial 0) writes 4 bits; - // the remaining 2 spill into segment 1 on trial 1. cw3 (2 bits) - // goes into segment 1 trial 0. - let seg = Segmentation::new(&[6, 6], 12); - let plan = ReorderPlan::build(&[2, 2, 6, 2], &seg).unwrap(); - assert_bijective(&plan, 12); - assert_eq!(plan.codeword_bits[2].len(), 6); - assert_eq!(plan.codeword_bits[3].len(), 2); - } - - #[test] - fn reorder_rejects_overfull_buffer() { - // Codewords summing past the buffer don't fit. - let seg = Segmentation::new(&[8, 8], 16); - assert!(ReorderPlan::build(&[8, 8, 4], &seg).is_none()); - } - - #[test] - fn reorder_empty_block() { - let seg = Segmentation::new(&[], 0); - let plan = ReorderPlan::build(&[], &seg).unwrap(); - assert!(plan.codeword_bits.is_empty()); - } -} diff --git a/crates/vendor/oxideav-aac/src/hcr_decode.rs b/crates/vendor/oxideav-aac/src/hcr_decode.rs deleted file mode 100644 index d95025d4..00000000 --- a/crates/vendor/oxideav-aac/src/hcr_decode.rs +++ /dev/null @@ -1,777 +0,0 @@ -//! `reordered_spectral_data()` payload codec — ISO/IEC 14496-3 -//! §4.6.16.3.3 / §4.6.16.3.4: the Huffman-codeword-reordering (HCR) -//! bitstream payload, both directions. -//! -//! The [`crate::hcr`] module owns the deterministic geometry half of -//! the tool (the Table 4.170 `maxCwLen` table, the pre-sorting metric, -//! the [`crate::hcr::Segmentation`] layout, and the -//! [`crate::hcr::ReorderPlan`] writing-scheme walk). This module binds -//! that geometry to the actual spectral payload: -//! -//! * [`encode_reordered_spectral_data`] — the §4.6.16.3.3.4 -//! `ReorderSpectralData()` encoder: enumerate the frame's codewords -//! in §4.6.16.3.3.1 pre-sorted order, Huffman-encode each (the -//! codeword plus sign bits plus escape sequences: the §4.5.2.3.2 HCR -//! codeword unit), and scatter the bits over the segment grid with the -//! PCW-then-non-PCW set / trial loop. -//! * [`decode_reordered_spectral_data`] — the §4.6.16.3.4 decode: the -//! inverse walk. Codeword lengths are *not* transmitted; the PCWs -//! are decoded first, each from the start of its own segment (the -//! §4.6.16.3.3.2 `segmentWidth ≥` every same-book codeword -//! guarantees they fit), then the non-PCW sets are decoded through -//! the same trial loop the writer used — a codeword consumes bits -//! from a segment's free region (in the set's direction) until its -//! Huffman unit completes or the segment exhausts, in which case its -//! remainder continues in the next trial's segment. Because every -//! spectrum codebook is a complete prefix code, an incomplete bit -//! prefix is exactly distinguishable (bit-source underflow) from a -//! completed codeword, so the decoder discovers each codeword's -//! length precisely where the writer defined it. -//! -//! ## Codeword enumeration and pre-sorting (§4.6.16.3.3.1) -//! -//! A *unit* covers four spectral lines of one window: one 4-D codeword -//! or two 2-D codewords in natural (ascending-frequency) order. Unit -//! groups are collected ascending in spectral direction with the -//! windows of one spectral region in temporal order (the unit-based -//! window interleaving of Table 4.169 — §4.5.2.3.5 grouping interleave -//! does *not* apply under HCR), then stably ordered by the -//! `assignedUnitNr` metric (codebook priority first). The §4.6.16.4 -//! virtual codebooks 16..=31 carry ordinary codebook-11 spectrum (their -//! `maxCwLen` differs for the segment widths only). -//! -//! ## Provenance -//! -//! Everything follows the §4.6.16.3 text and pseudocode plus the -//! §4.5.2.3.2 codeword-unit definition ("the whole data necessary to -//! decode two or four lines … includes Huffman codeword, sign bits, -//! and escape sequences"), staged under `docs/audio/aac/`. No external -//! HCR implementation was consulted. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::hcr::{assigned_unit_nr, segment_width, Direction, Segmentation}; -use crate::ics_info::IcsInfo; -use crate::section_data::SectionData; -use crate::spectral_data::{ - decode_codeword, read_and_apply_signs, read_escape_sequence, write_tuple, SpectralData, -}; -#[cfg(test)] -use crate::swb_offset::{long_window_offsets, short_window_offsets}; -use crate::swb_offset::{LONG_WINDOW_LEN, SHORT_WINDOW_LEN}; -use crate::{Error, Result}; - -/// The `ESC_FLAG` magnitude of the escape book (§4.6.3.3). -const ESC_FLAG: i32 = 16; - -/// One HCR codeword in pre-sorted order: the §4.5.2.3.2 unit of "the -/// whole data necessary to decode two or four lines". -#[derive(Debug, Clone, Copy)] -struct HcrCodeword { - /// The section codebook (1..=11 spectrum books, or a §4.6.16.4 - /// virtual codebook 16..=31) — drives the segment width. - sect_cb: u8, - /// The codebook whose Huffman tables encode the lines (the virtual - /// codebooks decode as book 11). - decode_cb: u8, - /// Tuple dimension: 4 (books 1..=4) or 2. - dim: usize, - /// Window group index. - group: usize, - /// Index within the group's transmission-order buffer of the first - /// line of this codeword. - buf_index: usize, -} - -/// Enumerate the frame's spectral codewords in §4.6.16.3.3.1 -/// pre-sorted order. -/// -/// Walks every window of every group over the active scalefactor bands -/// (`sfb_cb[g][sfb]`), skipping the spectrum-less books (`ZERO`, -/// `NOISE`, intensity), and sorts stably by the `assignedUnitNr` -/// metric. `buf_index` targets the §4.5.2.3.5 transmission-order group -/// buffer layout [`SpectralData`] uses (sfb-major, window-in-group, -/// line), which keeps the rest of the decode chain unchanged. -fn enumerate_presorted( - ics_info: &IcsInfo, - section_data: &SectionData, - fs_index: u8, -) -> Result> { - let short = ics_info.window_sequence.is_eight_short(); - let window_len = ics_info.window_len()?; - let offsets = ics_info.swb_offsets(fs_index)?; - let max_lines = window_len as u32; - let max_windows: u32 = if short { 8 } else { 1 }; - let max_sfb = usize::from(ics_info.max_sfb); - if max_sfb > offsets.len() - 1 { - return Err(Error::SpectralDataInvalid); - } - if section_data.sfb_cb.len() != usize::from(ics_info.num_window_groups) { - return Err(Error::SpectralDataInvalid); - } - - let mut cws: Vec<(u32, HcrCodeword)> = Vec::new(); - let mut window_base = 0usize; // absolute index of the group's first window - for (g, cb_row) in section_data.sfb_cb.iter().enumerate() { - if cb_row.len() < max_sfb { - return Err(Error::SpectralDataInvalid); - } - let wgl = usize::from(ics_info.window_group_length[g]); - // Transmission-order offset of band `sfb` for window-in-group - // `b`: sum over earlier bands of `wgl · width`, plus - // `b · width(sfb)`. - let mut band_base = 0usize; - for sfb in 0..max_sfb { - let start = usize::from(offsets[sfb]); - let end = usize::from(offsets[sfb + 1]); - let width = end - start; - let cb = cb_row[sfb]; - let spec = classify_hcr(cb)?; - if let Some((decode_cb, dim)) = spec { - for b in 0..wgl { - let window = (window_base + b) as u32; - for line_off in (0..width).step_by(dim) { - let line = (start + line_off) as u32; - // A unit covers four lines; both 2-D codewords - // of one unit share its assignedUnitNr and keep - // their natural order (stable sort below). - let unit_line = line & !3; - let key = assigned_unit_nr(cb, max_lines, unit_line, max_windows, window); - cws.push(( - key, - HcrCodeword { - sect_cb: cb, - decode_cb, - dim, - group: g, - buf_index: band_base + b * width + line_off, - }, - )); - } - } - } - band_base += wgl * width; - } - window_base += wgl; - } - - cws.sort_by_key(|&(key, _)| key); - Ok(cws.into_iter().map(|(_, cw)| cw).collect()) -} - -/// Classify a section codebook for HCR: `None` for the spectrum-less -/// books, `(decode_cb, dim)` for the spectrum books, an error for the -/// reserved book 12. -fn classify_hcr(cb: u8) -> Result> { - match cb { - 0 | 13 | 14 | 15 => Ok(None), - 1..=4 => Ok(Some((cb, 4))), - 5..=11 => Ok(Some((cb, 2))), - // §4.6.16.4 virtual codebooks: ordinary book-11 spectrum with - // a limited value range (the limit shapes maxCwLen only). - 16..=31 => Ok(Some((11, 2))), - _ => Err(Error::SpectralDataInvalid), - } -} - -/// Encode one codeword unit (Huffman codeword + sign bits + escape -/// sequences) to a fresh bit vector. -fn encode_codeword_bits(cw: &HcrCodeword, values: &[i32]) -> Result<(Vec, u32)> { - let mut w = BitWriter::new(); - write_tuple(&mut w, cw.decode_cb, cw.dim, values)?; - let bits = w.bit_position() as u32; - Ok((w.finish(), bits)) -} - -/// §4.6.16.3.3.4 `ReorderSpectralData()` — encode a frame's spectrum -/// as a `reordered_spectral_data()` payload. -/// -/// Returns `(payload_bytes, length_of_reordered_spectral_data, -/// length_of_longest_codeword)`. The payload length is exactly the sum -/// of the codeword lengths (the writer transmits no slack), stored -/// MSB-first. -/// -/// `spectral` must use the same transmission-order layout -/// [`SpectralData::write`] consumes; `section_data.sfb_cb` may carry -/// §4.6.16.4 virtual codebooks (16..=31). -pub fn encode_reordered_spectral_data( - spectral: &SpectralData, - ics_info: &IcsInfo, - section_data: &SectionData, - fs_index: u8, -) -> Result<(Vec, u16, u8)> { - let cws = enumerate_presorted(ics_info, section_data, fs_index)?; - if spectral.x_quant.len() != usize::from(ics_info.num_window_groups) { - return Err(Error::SpectralDataEncodeInvalid); - } - - // Encode every codeword unit to its bit string. - let mut encoded: Vec<(Vec, u32)> = Vec::with_capacity(cws.len()); - let mut longest = 0u32; - for cw in &cws { - let buf = spectral - .x_quant - .get(cw.group) - .ok_or(Error::SpectralDataEncodeInvalid)?; - let vals = buf - .get(cw.buf_index..cw.buf_index + cw.dim) - .ok_or(Error::SpectralDataEncodeInvalid)?; - let e = encode_codeword_bits(cw, vals)?; - longest = longest.max(e.1); - encoded.push(e); - } - if longest > 49 { - // §4.6.16.3.2: valid lengths are 0..=49; the codeword units of - // the spectrum books never exceed this by construction. - return Err(Error::SpectralDataEncodeInvalid); - } - let total_bits: u32 = encoded.iter().map(|e| e.1).sum(); - if total_bits > 12288 { - return Err(Error::SpectralDataEncodeInvalid); - } - - // Segment grid + the writing-scheme bit placement. - let widths: Vec = cws - .iter() - .map(|cw| segment_width(cw.sect_cb, longest as u8)) - .collect(); - let seg = Segmentation::new(&widths, total_bits); - let lengths: Vec = encoded.iter().map(|e| e.1).collect(); - let plan = - crate::hcr::ReorderPlan::build(&lengths, &seg).ok_or(Error::SpectralDataEncodeInvalid)?; - - // Scatter the codeword bits to their planned buffer positions. - let mut out = vec![0u8; (total_bits as usize).div_ceil(8)]; - for (c, (bytes, len)) in encoded.iter().enumerate() { - for bit in 0..*len { - let set = bytes[(bit / 8) as usize] & (0x80 >> (bit % 8)) != 0; - if set { - let pos = plan.codeword_bits[c][bit as usize]; - out[(pos / 8) as usize] |= 0x80 >> (pos % 8); - } - } - } - Ok((out, total_bits as u16, longest as u8)) -} - -/// The per-segment cursor pair of the §4.6.16.3.3.3 walk: bits -/// consumed from the low (forward) and high (backward) ends. -struct SegCursor { - start: u32, - width: u32, - low: u32, - high: u32, -} - -impl SegCursor { - fn free(&self) -> u32 { - self.width - self.low - self.high - } - - /// Collect the segment's free bits in `dir` order (the order the - /// writer would have placed a codeword's bits). - fn free_bits(&self, payload: &[u8], dir: Direction) -> Vec { - let read = |local: u32| { - let pos = self.start + local; - payload[(pos / 8) as usize] & (0x80 >> (pos % 8)) != 0 - }; - match dir { - Direction::Forward => (self.low..self.width - self.high).map(read).collect(), - Direction::Backward => (self.low..self.width - self.high).rev().map(read).collect(), - } - } - - /// Consume `n` bits from the `dir` end. - fn consume(&mut self, n: u32, dir: Direction) { - match dir { - Direction::Forward => self.low += n, - Direction::Backward => self.high += n, - } - } -} - -/// The in-flight decode state of one codeword: the bits gathered so -/// far and, once complete, the decoded lines. -struct CodewordState { - bits: Vec, - done: bool, - values: [i32; 4], -} - -/// Try to decode a whole codeword unit from `bits`. Returns -/// `Ok(Some((consumed_bits, values)))` when the unit completes within -/// `bits`, `Ok(None)` when more bits are needed (bit-source -/// underflow), or a hard error for a genuinely invalid unit. -fn try_decode_unit(cw: &HcrCodeword, bits: &[bool]) -> Result> { - // Pack MSB-first. - let mut bytes = vec![0u8; bits.len().div_ceil(8)]; - for (i, &b) in bits.iter().enumerate() { - if b { - bytes[i / 8] |= 0x80 >> (i % 8); - } - } - let mut r = BitReader::new(&bytes); - // Mirror the SpectralData::parse per-tuple sequence: hcod → sign - // bits → escape sequences. - let step = (|| -> Result<[i32; 4]> { - let idx = decode_codeword(&mut r, cw.decode_cb)?; - let tuple = crate::spectral_codebook::decode_index_to_tuple(cw.decode_cb, idx)?; - let mut tuple = read_and_apply_signs(&mut r, cw.decode_cb, cw.dim, tuple)?; - if cw.decode_cb == 11 { - for v in tuple.iter_mut().take(cw.dim) { - if v.abs() == ESC_FLAG { - let mag = read_escape_sequence(&mut r)? as i32; - *v = if *v < 0 { -mag } else { mag }; - } - } - } - Ok(tuple) - })(); - match step { - Ok(tuple) => { - let consumed = r.bit_position() as u32; - if consumed as usize > bits.len() { - // The packed byte buffer is padded to a byte boundary; - // a "completion" that consumed padding bits is phantom - // — the genuine continuation bits arrive in a later - // trial's segment. - return Ok(None); - } - Ok(Some((consumed, tuple))) - } - Err(Error::UnexpectedEnd) => Ok(None), - Err(e) => Err(e), - } -} - -/// §4.6.16.3.4 — decode a `reordered_spectral_data()` payload back to -/// the transmission-order [`SpectralData`]. -/// -/// * `payload` — the reordered buffer, MSB-first; -/// `length_of_reordered_spectral_data` (already clamped per -/// §4.6.16.3.2 by the caller if reserved) selects the bit count. -/// * `length_of_longest_codeword` — the transmitted 6-bit field -/// (clamped internally per §4.6.16.3.2). -/// -/// The decode runs the exact §4.6.16.3.3.4 walk with the codeword -/// lengths discovered by Huffman completion; see the module notes. -pub fn decode_reordered_spectral_data( - payload: &[u8], - length_of_reordered_spectral_data: u16, - length_of_longest_codeword: u8, - ics_info: &IcsInfo, - section_data: &SectionData, - fs_index: u8, -) -> Result { - let total_bits = u32::from(length_of_reordered_spectral_data); - if (payload.len() as u32) * 8 < total_bits { - return Err(Error::UnexpectedEnd); - } - let cws = enumerate_presorted(ics_info, section_data, fs_index)?; - - // Segment grid, exactly as the writer derived it. - let widths: Vec = cws - .iter() - .map(|cw| segment_width(cw.sect_cb, length_of_longest_codeword)) - .collect(); - let seg = Segmentation::new(&widths, total_bits); - let num_segments = seg.number_of_segments(); - if num_segments == 0 { - if cws.is_empty() { - return empty_spectral(ics_info); - } - return Err(Error::SpectralDataInvalid); - } - - let mut cursors: Vec = (0..num_segments) - .map(|s| SegCursor { - start: seg.segment_start(s), - width: seg.segment_bits[s], - low: 0, - high: 0, - }) - .collect(); - let mut states: Vec = cws - .iter() - .map(|_| CodewordState { - bits: Vec::new(), - done: false, - values: [0; 4], - }) - .collect(); - - // Feed a codeword from one segment: append free bits, try to - // complete; consume what the codeword actually used (all free bits - // if it is still incomplete). - let feed = |state: &mut CodewordState, - cw: &HcrCodeword, - cursor: &mut SegCursor, - dir: Direction| - -> Result<()> { - if state.done || cursor.free() == 0 { - return Ok(()); - } - let already = state.bits.len() as u32; - let fresh = cursor.free_bits(payload, dir); - state.bits.extend_from_slice(&fresh); - match try_decode_unit(cw, &state.bits)? { - Some((consumed, values)) => { - if consumed < already { - return Err(Error::SpectralDataInvalid); - } - cursor.consume(consumed - already, dir); - state.bits.truncate(consumed as usize); - state.values = values; - state.done = true; - } - None => { - // Uses every free bit of this segment and continues. - cursor.consume(fresh.len() as u32, dir); - } - } - Ok(()) - }; - - // First step: decode PCWs (set 0), codeword i forward from segment i. - for i in 0..num_segments.min(cws.len()) { - feed(&mut states[i], &cws[i], &mut cursors[i], Direction::Forward)?; - if !states[i].done { - // A PCW always fits its own segment (§4.6.16.3.3.2); not - // completing means the stream is corrupt. - return Err(Error::SpectralDataInvalid); - } - } - - // Second step: the non-PCW sets with the per-set direction toggle - // and the modulo-shift trial loop. - let num_sets = cws.len().div_ceil(num_segments); - let mut direction = Direction::Forward; - for set in 1..num_sets { - direction = direction.toggled(); - for trial in 0..num_segments { - for codeword_base in 0..num_segments { - let segment = (trial + codeword_base) % num_segments; - let codeword = codeword_base + set * num_segments; - if codeword >= cws.len() { - continue; - } - feed( - &mut states[codeword], - &cws[codeword], - &mut cursors[segment], - direction, - )?; - } - } - // §4.6.16.3.3.3: after at most N trials every codeword of the - // set is complete on a conforming stream. - for base in 0..num_segments { - let codeword = base + set * num_segments; - if codeword < cws.len() && !states[codeword].done { - return Err(Error::SpectralDataInvalid); - } - } - } - - // Scatter the decoded lines into the transmission-order buffers. - let mut spectral = empty_spectral(ics_info)?; - for (cw, state) in cws.iter().zip(states.iter()) { - let buf = spectral - .x_quant - .get_mut(cw.group) - .ok_or(Error::SpectralDataInvalid)?; - let dst = buf - .get_mut(cw.buf_index..cw.buf_index + cw.dim) - .ok_or(Error::SpectralDataInvalid)?; - dst.copy_from_slice(&state.values[..cw.dim]); - } - Ok(spectral) -} - -/// An all-zero transmission-order [`SpectralData`] with the group -/// buffer geometry of `ics_info`. -fn empty_spectral(ics_info: &IcsInfo) -> Result { - let short = ics_info.window_sequence.is_eight_short(); - let x_quant = ics_info - .window_group_length - .iter() - .map(|&wgl| { - let len = if short { - usize::from(wgl) * SHORT_WINDOW_LEN as usize - } else { - LONG_WINDOW_LEN as usize - }; - vec![0i32; len] - }) - .collect(); - Ok(SpectralData { x_quant }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape, NUM_SWB_LONG_WINDOW}; - use crate::section_data::Section; - - const FS: u8 = 4; // 44.1 kHz - - fn long_ics(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: NUM_SWB_LONG_WINDOW[FS as usize], - } - } - - /// An `EIGHT_SHORT` ics_info with two groups (3 + 5 windows). - fn short_ics(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups: 2, - window_group_length: vec![3, 5], - num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[FS as usize], - } - } - - fn section_data_for(sfb_cb_rows: Vec>) -> SectionData { - let sections = sfb_cb_rows - .iter() - .map(|row| { - // One section per band keeps the geometry simple. - row.iter() - .enumerate() - .map(|(sfb, &cb)| Section { - codebook: cb, - start: sfb as u8, - end: sfb as u8 + 1, - }) - .collect() - }) - .collect(); - SectionData { - sections, - sfb_cb: sfb_cb_rows, - } - } - - /// Deterministic pseudo-random value in `-max..=max`. - fn prand(state: &mut u32, max: i32) -> i32 { - *state = state.wrapping_mul(1664525).wrapping_add(1013904223); - let span = 2 * max + 1; - ((*state >> 8) % span as u32) as i32 - max - } - - /// Fill the active bands of a transmission-order spectrum with - /// bounded pseudo-random values per the band's codebook LAV. - fn fill_spectrum(ics: &IcsInfo, sd: &SectionData, seed: u32) -> SpectralData { - let mut state = seed; - let mut spectral = empty_spectral(ics).unwrap(); - let short = ics.window_sequence.is_eight_short(); - let offsets = if short { - short_window_offsets(FS).unwrap() - } else { - long_window_offsets(FS).unwrap() - }; - for (g, row) in sd.sfb_cb.iter().enumerate() { - let wgl = usize::from(ics.window_group_length[g]); - let mut base = 0usize; - for (sfb, &cb) in row.iter().enumerate().take(usize::from(ics.max_sfb)) { - let width = usize::from(offsets[sfb + 1] - offsets[sfb]); - let max = match cb { - 0 | 13 | 14 | 15 => 0, - 1 | 2 => 1, - 3 | 4 => 2, - 5 | 6 => 4, - 7 | 8 => 7, - 9 | 10 => 12, - // ESC book: exercise escapes with magnitudes > 16. - 11 => 40, - _ => 15, - }; - if max > 0 { - for i in 0..wgl * width { - spectral.x_quant[g][base + i] = prand(&mut state, max); - } - } - base += wgl * width; - } - } - spectral - } - - /// Round-trip: encode → decode reproduces the exact quantized - /// spectrum, across a codebook mix that forces multiple sets and - /// non-PCW segment spanning (long window). - #[test] - fn round_trips_long_window_mixed_codebooks() { - let ics = long_ics(12); - let sd = section_data_for(vec![vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11]]); - let spectral = fill_spectrum(&ics, &sd, 0xC0FFEE); - let (payload, len_bits, longest) = - encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); - assert!(len_bits > 0 && longest > 0); - let back = - decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); - assert_eq!(back.x_quant, spectral.x_quant); - } - - /// Round-trip with ZERO_HCB holes and an intensity band mixed in - /// (no spectrum transmitted for those bands). - #[test] - fn round_trips_with_spectrumless_bands() { - let ics = long_ics(10); - let sd = section_data_for(vec![vec![3, 0, 5, 15, 11, 0, 9, 1, 0, 7]]); - let spectral = fill_spectrum(&ics, &sd, 0xBADF00D); - let (payload, len_bits, longest) = - encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); - let back = - decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); - assert_eq!(back.x_quant, spectral.x_quant); - } - - /// Eight-short round-trip with two window groups: the §4.6.16.3.3.1 - /// unit-based window interleave (not the §4.5.2.3.5 grouping - /// interleave) must be applied consistently on both sides. - #[test] - fn round_trips_eight_short_two_groups() { - let ics = short_ics(8); - let sd = section_data_for(vec![ - vec![1, 3, 5, 7, 9, 11, 2, 4], - vec![11, 9, 7, 5, 3, 1, 4, 2], - ]); - let spectral = fill_spectrum(&ics, &sd, 0x5EED); - let (payload, len_bits, longest) = - encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); - let back = - decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); - assert_eq!(back.x_quant, spectral.x_quant); - } - - /// The payload survives trailing slack: a buffer longer than the - /// codeword bits (larger transmitted length) still decodes — the - /// slack widens the last segment, exactly as §4.6.16.3.3.2 - /// specifies. - #[test] - fn decodes_with_trailing_slack_bits() { - let ics = long_ics(6); - let sd = section_data_for(vec![vec![2, 4, 6, 8, 10, 11]]); - let spectral = fill_spectrum(&ics, &sd, 0xABCDEF); - let (payload, len_bits, longest) = - encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); - // Re-plan with 16 slack bits: the writer must scatter into the - // wider grid and the decoder must follow. - let slack_bits = len_bits + 16; - let cws = enumerate_presorted(&ics, &sd, FS).unwrap(); - let widths: Vec = cws - .iter() - .map(|cw| segment_width(cw.sect_cb, longest)) - .collect(); - let seg = Segmentation::new(&widths, u32::from(slack_bits)); - let mut encoded = Vec::new(); - for cw in &cws { - let vals = &spectral.x_quant[cw.group][cw.buf_index..cw.buf_index + cw.dim]; - encoded.push(encode_codeword_bits(cw, vals).unwrap()); - } - let lengths: Vec = encoded.iter().map(|e| e.1).collect(); - let plan = crate::hcr::ReorderPlan::build(&lengths, &seg).unwrap(); - let mut wide = vec![0u8; (slack_bits as usize).div_ceil(8)]; - for (c, (bytes, len)) in encoded.iter().enumerate() { - for bit in 0..*len { - if bytes[(bit / 8) as usize] & (0x80 >> (bit % 8)) != 0 { - let pos = plan.codeword_bits[c][bit as usize]; - wide[(pos / 8) as usize] |= 0x80 >> (pos % 8); - } - } - } - let back = - decode_reordered_spectral_data(&wide, slack_bits, longest, &ics, &sd, FS).unwrap(); - assert_eq!(back.x_quant, spectral.x_quant); - let _ = payload; - } - - /// Virtual codebooks (16..=31) decode as book 11 with their own - /// segment widths. - #[test] - fn round_trips_virtual_codebooks() { - let ics = long_ics(6); - // VCB 17 pairs with small magnitudes; VCB 31 with escapes. - let sd = section_data_for(vec![vec![17, 31, 1, 16, 20, 11]]); - let mut spectral = empty_spectral(&ics).unwrap(); - let offsets = long_window_offsets(FS).unwrap(); - let mut state = 0x1234u32; - for sfb in 0..6usize { - let (a, b) = (usize::from(offsets[sfb]), usize::from(offsets[sfb + 1])); - let max = match sfb { - 0 | 3 => 3, // VCB 17 / 16: modest values - 1 | 4 => 30, // VCB 31 / 20: escapes - 2 => 1, // book 1 quads - _ => 40, // book 11 - }; - for i in a..b { - spectral.x_quant[0][i] = prand(&mut state, max); - } - } - let (payload, len_bits, longest) = - encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); - let back = - decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS).unwrap(); - assert_eq!(back.x_quant, spectral.x_quant); - } - - /// A corrupt payload surfaces an error, not a panic: flip bits in - /// the PCW region. - #[test] - fn corrupt_payload_errors_cleanly() { - let ics = long_ics(8); - let sd = section_data_for(vec![vec![1, 2, 3, 4, 5, 6, 7, 8]]); - let spectral = fill_spectrum(&ics, &sd, 0xFEED); - let (mut payload, len_bits, longest) = - encode_reordered_spectral_data(&spectral, &ics, &sd, FS).unwrap(); - for byte in payload.iter_mut().take(4) { - *byte ^= 0xFF; - } - // Either decodes to different values or errors — it must not - // panic, and it must not silently return the original. - if let Ok(back) = decode_reordered_spectral_data(&payload, len_bits, longest, &ics, &sd, FS) - { - assert_ne!(back.x_quant, spectral.x_quant); - } - } - - /// Pre-sorting puts the ESC-book codewords first (priority 0) and - /// the book-1/2 codewords last (priority 21). - #[test] - fn presort_orders_esc_first() { - let ics = long_ics(3); - let sd = section_data_for(vec![vec![1, 11, 5]]); - let cws = enumerate_presorted(&ics, &sd, FS).unwrap(); - assert!(!cws.is_empty()); - assert_eq!(cws.first().unwrap().sect_cb, 11); - assert_eq!(cws.last().unwrap().sect_cb, 1); - } -} diff --git a/crates/vendor/oxideav-aac/src/ics_body.rs b/crates/vendor/oxideav-aac/src/ics_body.rs deleted file mode 100644 index 1c7fee37..00000000 --- a/crates/vendor/oxideav-aac/src/ics_body.rs +++ /dev/null @@ -1,837 +0,0 @@ -//! `individual_channel_stream()` body walker — ISO/IEC 14496-3 §4.4.6 / -//! Table 4.50. -//! -//! This module composes the existing per-tool parsers / writers -//! (`global_gain`, [`crate::ics_info`], [`crate::section_data`], -//! [`crate::scale_factor_data`], [`crate::pulse_data`], -//! [`crate::tns_data`], [`crate::gain_control_data`]) into the -//! Table 4.50 channel-element body, **up to but not including** -//! `spectral_data()`. -//! -//! ## Why "up to but not including" -//! -//! `spectral_data()` (Table 4.56) is the per-band Huffman-coded -//! quantised MDCT-coefficient block. Its walker lives in the -//! dedicated [`crate::spectral_data`] module (round 281): this body -//! walker stops at the bit position immediately after -//! `gain_control_data()` (or the dispatching -//! `gain_control_data_present` bit when the tool is omitted) and -//! surfaces that position as [`IcsBody::spectral_data_bit_offset`], -//! from which [`crate::spectral_data::SpectralData::parse`] consumes -//! the spectrum in place — see `tests/spectral_data.rs` for the -//! sequential composition. Keeping the two stages separate mirrors -//! the CPE shared-`ics_info` split: the caller owns the reader and -//! decides when to hand off. -//! -//! This is consistent with the round-200 README ("Phase 2 in -//! progress + channel-element body walker still pending") — the -//! Walker in [`crate::raw_data_block`] emits a `ChannelElement` -//! event but does not consume the body, so the caller has to -//! re-bind a [`BitReader`] to the body region and call this module -//! to parse the structural per-tool layout. -//! -//! ## Table 4.50 layout (the non-scalable branch) -//! -//! ```text -//! individual_channel_stream(common_window, scale_flag) { -//! global_gain; 8 uimsbf -//! if (!common_window && !scale_flag) { -//! ics_info(); -//! } -//! section_data(); -//! scale_factor_data(); -//! if (!scale_flag) { -//! pulse_data_present; 1 uimsbf -//! if (pulse_data_present) pulse_data(); -//! tns_data_present; 1 uimsbf -//! if (tns_data_present) tns_data(); -//! gain_control_data_present; 1 uimsbf -//! if (gain_control_data_present) gain_control_data(); -//! } -//! if (!aacSpectralDataResilienceFlag) { -//! spectral_data(); // NOT covered here -//! } else { -//! length_of_reordered_spectral_data; -//! length_of_longest_codeword; -//! reordered_spectral_data(); // NOT covered here -//! } -//! } -//! ``` -//! -//! Per Table 4.50 the `common_window` flag is set by the surrounding -//! `channel_pair_element()` (Table 4.4) when the two channels of the -//! CPE share the `ics_info()`; in that case the *first* call to -//! `individual_channel_stream()` reads the shared `ics_info()` (the -//! caller of this module does that — by, say, invoking -//! [`crate::ics_info::IcsInfo::parse`] directly — and then calls -//! [`IcsBody::parse_with_ics_info`]). For the single-channel form -//! (SCE / LFE) `common_window == false` and the body reads its own -//! `ics_info()` inline; the caller invokes [`IcsBody::parse`] and the -//! module both reads `ics_info()` and surfaces it. -//! -//! `scale_flag` is set by scalable streams (AOT 6) when the -//! `aac_scalable_main_header()` carries side-info that already -//! dispatched the pulse / TNS / gain-control tools. Phase 2 does not -//! yet support the scalable extension; this module rejects -//! `scale_flag == true` with [`crate::Error::NotImplemented`] so the -//! existing SCE / CPE / LFE callers keep their bit-exact round-trip. -//! -//! ## What this module covers -//! -//! * [`IcsBody::parse`] — reads `global_gain`, the inline -//! `ics_info()`, `section_data()`, `scale_factor_data()`, the three -//! `*_present` dispatch bits, and the dispatched -//! `pulse_data()` / `tns_data()` / `gain_control_data()` bodies. -//! * [`IcsBody::parse_with_ics_info`] — same minus the `ics_info()` -//! read; the caller supplies the parsed [`crate::ics_info::IcsInfo`] -//! that the CPE-shared-info path already produced. -//! * [`IcsBody::write`] — the symmetric writer that round-trips the -//! parsed `IcsBody` back to a bit-exact Table 4.50 prefix -//! (everything up to and including `gain_control_data_present` / -//! its body). The `spectral_data()` portion is the caller's -//! responsibility (typically `push_channel_body_bits` on a -//! [`crate::raw_data_block::FrameAssembler`]). -//! * [`IcsBody::write_with_ics_info`] — same minus the `ics_info()` -//! write. -//! -//! Field validity: -//! -//! * Pulse-data is only legal when `window_sequence != EIGHT_SHORT` -//! per Table 4.50 / Table 4.7; the parser surfaces -//! [`crate::Error::PulseDataEncodeInvalid`] on a violation, the -//! writer rejects the same shape before emitting. -//! * Gain-control-data is only legal when `audioObjectType == 3` -//! (SSR) per the §4.6.12 normative constraint; the parser does not -//! enforce this (it surfaces the dispatching bit verbatim so a -//! non-SSR stream with the bit set still round-trips) but the -//! writer does, to keep the FrameAssembler emitting only -//! conforming streams. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::asc::AacResilienceFlags; -use crate::gain_control_data::GainControlData; -use crate::ics_info::{IcsInfo, WindowSequence}; -use crate::pulse_data::PulseData; -use crate::scale_factor_data::{ErScaleFactorData, ScaleFactorData}; -use crate::section_data::SectionData; -use crate::swb_offset::FrameFamily; -use crate::tns_data::TnsData; -use crate::{Error, Result}; - -/// Field width of `global_gain` (Table 4.50). -pub const GLOBAL_GAIN_BITS: u32 = 8; - -/// AOT value for AAC SSR (the only AOT that uses -/// `gain_control_data()`). -pub const AOT_AAC_SSR: u8 = 3; - -/// Parsed `individual_channel_stream()` body per Table 4.50, up to -/// but not including `spectral_data()`. -/// -/// The trailing `spectral_data()` block is the caller's -/// responsibility — see the module docs for the rationale. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IcsBody { - /// `global_gain` — 8-bit `uimsbf` per Table 4.50. The seed for - /// the §4.6.2.3.2 scalefactor DPCM accumulator - /// (`last_sf = global_gain` at the top of every frame). - pub global_gain: u8, - /// `ics_info()` (Table 4.6). `None` only when - /// [`IcsBody::parse_with_ics_info`] / [`IcsBody::write_with_ics_info`] - /// were used and the caller's `IcsInfo` is held outside this - /// struct (CPE shared-info form). - pub ics_info: Option, - /// `section_data()` (ISO/IEC 13818-7 §6.3 Table 17). - pub section_data: SectionData, - /// `scale_factor_data()` (Table 4.53, non-resilient branch). - pub scale_factor_data: ScaleFactorData, - /// `pulse_data_present` (1 bit). When `true`, [`Self::pulse_data`] - /// carries the dispatched Table 4.7 record. - pub pulse_data_present: bool, - /// `pulse_data()` (Table 4.7). Populated when - /// `pulse_data_present == true`. - pub pulse_data: Option, - /// `tns_data_present` (1 bit). When `true`, [`Self::tns_data`] - /// carries the dispatched Table 4.54 record. - pub tns_data_present: bool, - /// `tns_data()` (Table 4.54). Populated when - /// `tns_data_present == true`. - pub tns_data: Option, - /// `gain_control_data_present` (1 bit). When `true`, - /// [`Self::gain_control_data`] carries the dispatched Table 4.12 - /// record. - pub gain_control_data_present: bool, - /// `gain_control_data()` (Table 4.12). Populated when - /// `gain_control_data_present == true`. - pub gain_control_data: Option, - /// Bit position of the *first* `spectral_data()` bit, measured - /// from the start of this `individual_channel_stream()` body - /// (i.e. the bit reader's position when [`IcsBody::parse`] was - /// invoked is `0` here). Useful for callers that need to slice - /// the spectrum block out of a parent buffer or hand it to a - /// spectral-data parser without re-walking the body. - pub spectral_data_bit_offset: u64, - /// The error-resilient `scale_factor_data()` record (RVLC branch, - /// Table 4.53) when the body was parsed via [`IcsBody::parse_er`] / - /// [`IcsBody::parse_with_ics_info_er`] with - /// `aacScalefactorDataResilienceFlag == 1`. `None` on the - /// non-resilient path. The reconstructed absolute-delta records - /// are mirrored into [`Self::scale_factor_data`] so the shared - /// §4.6.2.3.2 accumulate pass consumes the body unchanged - /// regardless of which branch produced it; this field preserves - /// the extra RVLC backward seeds (`rev_global_gain`, - /// `dpcm_*_last_position`). - pub er_scale_factor_data: Option, - /// The §4.4.2.7 Table 4.50 spectral-resilience length fields, - /// present only when the body was parsed via the ER path with - /// `aacSpectralDataResilienceFlag == 1`: - /// `(length_of_reordered_spectral_data, length_of_longest_codeword)`. - /// The `reordered_spectral_data()` (HCR) payload that follows is - /// the caller's responsibility (same contract as the non-resilient - /// `spectral_data()` block); these two counts size that payload. - pub reordered_spectral_lengths: Option<(u16, u8)>, -} - -impl IcsBody { - /// Parse a Table 4.50 channel-element body whose `ics_info()` is - /// inline (the single-channel `SCE` / `LFE` form, or the - /// non-shared `CPE` form). - /// - /// * `reader` — positioned at the first bit of the - /// `individual_channel_stream()` body (i.e. at `global_gain`). - /// * `audio_object_type` — the surrounding ASC's effective AOT - /// (post SBR/PS unwrap). Drives the Table 4.6 / 4.55 predictor - /// branch and the SSR-only `gain_control_data` gate. - /// * `sampling_frequency_index` — the surrounding ASC's - /// `samplingFrequencyIndex` (the *core* index for hierarchical - /// SBR / PS). - /// * `scale_flag` — Table 4.50's outer `scale_flag` (set by - /// scalable AAC, AOT 6). The Phase 2 surface rejects - /// `scale_flag == true` with [`Error::NotImplemented`]. - /// - /// Errors propagate from the underlying per-tool parsers: - /// [`Error::UnexpectedEnd`] on bit-reader underflow, - /// [`Error::IcsInfoUnsupportedSampleRateIndex`] on an out-of-range - /// `fs_index`, [`Error::SectionDataOverrun`] on a non-conforming - /// `section_data()`, [`Error::PulseDataEncodeInvalid`] when the - /// stream sets `pulse_data_present == 1` on an - /// `EIGHT_SHORT_SEQUENCE` (Table 4.50 Note 1). - pub fn parse( - reader: &mut BitReader<'_>, - audio_object_type: u8, - sampling_frequency_index: u8, - scale_flag: bool, - ) -> Result { - // CPE-shared-info form is handled by parse_with_ics_info; the - // public `parse` always reads its own ics_info, which matches - // the SCE / LFE / non-common-window CPE case. - Self::parse_family( - reader, - FrameFamily::Lc1024, - audio_object_type, - sampling_frequency_index, - scale_flag, - ) - } - - /// [`IcsBody::parse`] under an explicit §4.5.1.1 frame-length - /// family (the inline `ics_info()` is parsed with - /// [`IcsInfo::parse_family`], so the 960 / LD band geometry and - /// the LD `ONLY_LONG` constraint apply). - pub fn parse_family( - reader: &mut BitReader<'_>, - family: FrameFamily, - audio_object_type: u8, - sampling_frequency_index: u8, - scale_flag: bool, - ) -> Result { - Self::parse_inner( - reader, - family, - audio_object_type, - sampling_frequency_index, - false, - scale_flag, - ) - } - - /// Parse a Table 4.50 channel-element body whose `ics_info()` was - /// already consumed by the surrounding shared-info `CPE` form. - /// - /// The supplied `ics_info` drives the same `num_window_groups` / - /// `max_sfb` / `window_sequence` dependencies the inline path - /// would otherwise derive. - /// - /// `scale_flag` semantics mirror [`IcsBody::parse`]. The returned - /// `IcsBody::ics_info` is `None` — the caller holds the shared - /// `IcsInfo` outside the per-channel body. - pub fn parse_with_ics_info( - reader: &mut BitReader<'_>, - ics_info: &IcsInfo, - audio_object_type: u8, - scale_flag: bool, - ) -> Result { - if scale_flag { - return Err(Error::NotImplemented); - } - let start = reader.bit_position(); - let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; - let section_data = SectionData::parse( - reader, - ics_info.window_sequence, - ics_info.num_window_groups, - ics_info.max_sfb, - )?; - let scale_factor_data = ScaleFactorData::parse(reader, §ion_data.sfb_cb)?; - - let tools = parse_tools(reader, ics_info, audio_object_type, start)?; - - Ok(IcsBody { - global_gain, - ics_info: None, - section_data, - scale_factor_data, - pulse_data_present: tools.pulse_data_present, - pulse_data: tools.pulse_data, - tns_data_present: tools.tns_data_present, - tns_data: tools.tns_data, - gain_control_data_present: tools.gain_control_data_present, - gain_control_data: tools.gain_control_data, - spectral_data_bit_offset: tools.spectral_data_bit_offset, - er_scale_factor_data: None, - reordered_spectral_lengths: None, - }) - } - - fn parse_inner( - reader: &mut BitReader<'_>, - family: FrameFamily, - audio_object_type: u8, - sampling_frequency_index: u8, - common_window: bool, - scale_flag: bool, - ) -> Result { - if scale_flag { - return Err(Error::NotImplemented); - } - let start = reader.bit_position(); - let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; - // Table 4.50: `if (!common_window && !scale_flag) ics_info();` - // — `parse` is the !common_window path (the caller of - // `parse_with_ics_info` covers the other branch). - let ics_info = IcsInfo::parse_family( - reader, - family, - audio_object_type, - sampling_frequency_index, - common_window, - )?; - let section_data = SectionData::parse( - reader, - ics_info.window_sequence, - ics_info.num_window_groups, - ics_info.max_sfb, - )?; - let scale_factor_data = ScaleFactorData::parse(reader, §ion_data.sfb_cb)?; - - let tools = parse_tools(reader, &ics_info, audio_object_type, start)?; - - Ok(IcsBody { - global_gain, - ics_info: Some(ics_info), - section_data, - scale_factor_data, - pulse_data_present: tools.pulse_data_present, - pulse_data: tools.pulse_data, - tns_data_present: tools.tns_data_present, - tns_data: tools.tns_data, - gain_control_data_present: tools.gain_control_data_present, - gain_control_data: tools.gain_control_data, - spectral_data_bit_offset: tools.spectral_data_bit_offset, - er_scale_factor_data: None, - reordered_spectral_lengths: None, - }) - } - - /// Parse an **error-resilient** Table 4.50 channel-element body - /// (the ER General Audio object types — AOTs 17 / 19 / 20 / 23 — - /// whose ASC carries the [`AacResilienceFlags`] triplet). - /// - /// Differs from [`IcsBody::parse`] in three spec-driven ways - /// (Table 4.50 / Table 4.52 / Table 4.53): - /// - /// * `section_data()` takes the [`SectionData::parse_er`] branch - /// when `resilience.section_data` is set (5-bit `sect_cb`). - /// * `scale_factor_data()` takes the RVLC - /// [`ErScaleFactorData::parse`] branch when - /// `resilience.scalefactor_data` is set; the reconstructed - /// absolute-delta records are mirrored into - /// [`Self::scale_factor_data`] and the RVLC seeds are retained - /// in [`Self::er_scale_factor_data`]. - /// * the trailing `spectral_data()` is replaced — when - /// `resilience.spectral_data` is set — by the - /// `length_of_reordered_spectral_data` (14-bit) + - /// `length_of_longest_codeword` (6-bit) pair captured in - /// [`Self::reordered_spectral_lengths`]; the - /// `reordered_spectral_data()` (HCR) payload that follows is the - /// caller's responsibility, exactly as `spectral_data()` is on - /// the non-resilient path. - pub fn parse_er( - reader: &mut BitReader<'_>, - audio_object_type: u8, - sampling_frequency_index: u8, - scale_flag: bool, - resilience: AacResilienceFlags, - ) -> Result { - Self::parse_er_family( - reader, - FrameFamily::Lc1024, - audio_object_type, - sampling_frequency_index, - scale_flag, - resilience, - ) - } - - /// [`IcsBody::parse_er`] under an explicit §4.5.1.1 frame-length - /// family — the ER AAC LD (AOT 23) payloads ride the same - /// Table 4.19 `er_raw_data_block()` as ER AAC LC, differing only - /// in the 512/480-line geometry this parameter selects. - pub fn parse_er_family( - reader: &mut BitReader<'_>, - family: FrameFamily, - audio_object_type: u8, - sampling_frequency_index: u8, - scale_flag: bool, - resilience: AacResilienceFlags, - ) -> Result { - if scale_flag { - return Err(Error::NotImplemented); - } - let start = reader.bit_position(); - let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; - let ics_info = IcsInfo::parse_family( - reader, - family, - audio_object_type, - sampling_frequency_index, - false, - )?; - let mut body = Self::finish_er_shared( - reader, - global_gain, - &ics_info, - audio_object_type, - resilience, - start, - )?; - body.ics_info = Some(ics_info); - Ok(body) - } - - /// Parse an error-resilient Table 4.50 body whose `ics_info()` was - /// already consumed by the surrounding shared-info CPE form. - /// - /// ER analogue of [`IcsBody::parse_with_ics_info`]; the resilience - /// branch semantics match [`IcsBody::parse_er`]. The returned - /// `ics_info` is `None` (the caller holds the shared `IcsInfo`). - pub fn parse_with_ics_info_er( - reader: &mut BitReader<'_>, - ics_info: &IcsInfo, - audio_object_type: u8, - scale_flag: bool, - resilience: AacResilienceFlags, - ) -> Result { - if scale_flag { - return Err(Error::NotImplemented); - } - let start = reader.bit_position(); - let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; - Self::finish_er_shared( - reader, - global_gain, - ics_info, - audio_object_type, - resilience, - start, - ) - } - - /// Shared tail of the ER parse: `section_data()` (ER branch) → - /// `scale_factor_data()` (RVLC branch) → tool dispatch → spectral - /// resilience length fields. `ics_info` carries the geometry; the - /// returned `IcsBody::ics_info` is `None` (the inline caller sets - /// it afterwards from its owned value). - fn finish_er_shared( - reader: &mut BitReader<'_>, - global_gain: u8, - ics_info: &IcsInfo, - audio_object_type: u8, - resilience: AacResilienceFlags, - start: u64, - ) -> Result { - let section_data = if resilience.section_data { - SectionData::parse_er( - reader, - ics_info.window_sequence, - ics_info.num_window_groups, - ics_info.max_sfb, - )? - } else { - SectionData::parse( - reader, - ics_info.window_sequence, - ics_info.num_window_groups, - ics_info.max_sfb, - )? - }; - - let (scale_factor_data, er_scale_factor_data) = if resilience.scalefactor_data { - let er = - ErScaleFactorData::parse(reader, §ion_data.sfb_cb, ics_info.window_sequence)?; - (er.data.clone(), Some(er)) - } else { - (ScaleFactorData::parse(reader, §ion_data.sfb_cb)?, None) - }; - - let tools = parse_tools(reader, ics_info, audio_object_type, start)?; - - // Table 4.50 ER spectral branch: when - // aacSpectralDataResilienceFlag is set, the body carries the - // two HCR length fields in place of starting spectral_data(). - let reordered_spectral_lengths = if resilience.spectral_data { - let len_reordered = reader.read_u32(14).map_err(|_| Error::UnexpectedEnd)? as u16; - let len_longest = reader.read_u32(6).map_err(|_| Error::UnexpectedEnd)? as u8; - Some((len_reordered, len_longest)) - } else { - None - }; - - let spectral_data_bit_offset = reader.bit_position() - start; - - Ok(IcsBody { - global_gain, - ics_info: None, - section_data, - scale_factor_data, - pulse_data_present: tools.pulse_data_present, - pulse_data: tools.pulse_data, - tns_data_present: tools.tns_data_present, - tns_data: tools.tns_data, - gain_control_data_present: tools.gain_control_data_present, - gain_control_data: tools.gain_control_data, - spectral_data_bit_offset, - er_scale_factor_data, - reordered_spectral_lengths, - }) - } - - /// Parse a Table 4.50 body with `scale_flag == 1` — the - /// `individual_channel_stream(1, 1)` form the scalable payloads - /// (Tables 4.13 / 4.14, AOTs 6 / 20) embed. - /// - /// Per Table 4.50 the scale-flag form reads neither `ics_info()` - /// (the window geometry lives in the `aac_scalable_main_header()`) - /// nor the pulse / TNS / gain-control dispatch trio (TNS rides in - /// the scalable headers; pulse and SSR gain control do not exist - /// in the scalable object types): the body is `global_gain` → - /// `section_data()` → `scale_factor_data()` → the spectral branch. - /// - /// * `ics_info` — the per-layer geometry (the header-transmitted - /// `window_sequence` / `window_shape` / grouping with **this - /// layer's** `max_sfb`). - /// * `resilience` — the ASC triplet for AOT 20 (ER AAC scalable); - /// pass `AacResilienceFlags::default()` for AOT 6. The branches - /// behave exactly as in [`IcsBody::parse_er`]: 5-bit `sect_cb` - /// `section_data()`, RVLC `scale_factor_data()`, and the HCR - /// length fields in place of `spectral_data()`. - /// - /// The trailing `spectral_data()` / `reordered_spectral_data()` is - /// the caller's responsibility, as on every other parse path. - pub fn parse_scale( - reader: &mut BitReader<'_>, - ics_info: &IcsInfo, - resilience: AacResilienceFlags, - ) -> Result { - let start = reader.bit_position(); - let global_gain = read_u8(reader, GLOBAL_GAIN_BITS)?; - let section_data = if resilience.section_data { - SectionData::parse_er( - reader, - ics_info.window_sequence, - ics_info.num_window_groups, - ics_info.max_sfb, - )? - } else { - SectionData::parse( - reader, - ics_info.window_sequence, - ics_info.num_window_groups, - ics_info.max_sfb, - )? - }; - let (scale_factor_data, er_scale_factor_data) = if resilience.scalefactor_data { - let er = - ErScaleFactorData::parse(reader, §ion_data.sfb_cb, ics_info.window_sequence)?; - (er.data.clone(), Some(er)) - } else { - (ScaleFactorData::parse(reader, §ion_data.sfb_cb)?, None) - }; - // Table 4.50: `if (!scale_flag) { pulse/tns/gain dispatch }` — - // all three tools are skipped on the scale-flag form. - let reordered_spectral_lengths = if resilience.spectral_data { - let len_reordered = reader.read_u32(14).map_err(|_| Error::UnexpectedEnd)? as u16; - let len_longest = reader.read_u32(6).map_err(|_| Error::UnexpectedEnd)? as u8; - Some((len_reordered, len_longest)) - } else { - None - }; - let spectral_data_bit_offset = reader.bit_position() - start; - Ok(IcsBody { - global_gain, - ics_info: None, - section_data, - scale_factor_data, - pulse_data_present: false, - pulse_data: None, - tns_data_present: false, - tns_data: None, - gain_control_data_present: false, - gain_control_data: None, - spectral_data_bit_offset, - er_scale_factor_data, - reordered_spectral_lengths, - }) - } - - /// Write a Table 4.50 `scale_flag == 1` body — the inverse of - /// [`IcsBody::parse_scale`], emitting `global_gain` → - /// `section_data()` → `scale_factor_data()` (→ the HCR length - /// fields when `resilience.spectral_data` is set). The trailing - /// spectrum block is the caller's responsibility. - pub fn write_scale( - &self, - writer: &mut BitWriter, - ics_info: &IcsInfo, - resilience: AacResilienceFlags, - ) -> Result<()> { - writer.write_u32(u32::from(self.global_gain), GLOBAL_GAIN_BITS); - if resilience.section_data { - self.section_data - .write_er(writer, ics_info.window_sequence, ics_info.max_sfb)?; - } else { - self.section_data - .write(writer, ics_info.window_sequence, ics_info.max_sfb)?; - } - if resilience.scalefactor_data { - let er = self - .er_scale_factor_data - .as_ref() - .ok_or(Error::ElementDecodeInvalid)?; - er.write(writer, &self.section_data.sfb_cb, ics_info.window_sequence)?; - } else { - self.scale_factor_data - .write(writer, &self.section_data.sfb_cb)?; - } - if resilience.spectral_data { - let (len_reordered, len_longest) = self - .reordered_spectral_lengths - .ok_or(Error::ElementDecodeInvalid)?; - writer.write_u32(u32::from(len_reordered), 14); - writer.write_u32(u32::from(len_longest), 6); - } - Ok(()) - } - - /// Write a Table 4.50 body whose `ics_info()` is inline. - /// - /// Mirrors [`IcsBody::parse`] — emits `global_gain`, `ics_info()`, - /// `section_data()`, `scale_factor_data()`, then the three - /// dispatching bits and their optional bodies. The trailing - /// `spectral_data()` is the caller's responsibility. - /// - /// Returns [`Error::IcsInfoEncodeInvalid`] if [`Self::ics_info`] - /// is `None` (use [`IcsBody::write_with_ics_info`] for the - /// CPE-shared-info case); other errors propagate from the - /// per-tool writers (e.g. [`Error::PulseDataEncodeInvalid`] when - /// `pulse_data_present == true` on `EIGHT_SHORT_SEQUENCE`). - pub fn write( - &self, - writer: &mut BitWriter, - audio_object_type: u8, - sampling_frequency_index: u8, - scale_flag: bool, - ) -> Result<()> { - if scale_flag { - return Err(Error::NotImplemented); - } - let ics_info = self.ics_info.as_ref().ok_or(Error::IcsInfoEncodeInvalid)?; - writer.write_u32(u32::from(self.global_gain), GLOBAL_GAIN_BITS); - ics_info.write(writer, audio_object_type, sampling_frequency_index, false)?; - self.section_data - .write(writer, ics_info.window_sequence, ics_info.max_sfb)?; - self.scale_factor_data - .write(writer, &self.section_data.sfb_cb)?; - self.write_tools(writer, ics_info, audio_object_type) - } - - /// Write a Table 4.50 body whose `ics_info()` was emitted - /// separately by the surrounding shared-info `CPE` form. - /// - /// The supplied `ics_info` drives the same per-tool field - /// dispatch the inline path would. The in-memory - /// [`Self::ics_info`] field is ignored (and is expected to be - /// `None` for round-trip consistency). - pub fn write_with_ics_info( - &self, - writer: &mut BitWriter, - ics_info: &IcsInfo, - audio_object_type: u8, - scale_flag: bool, - ) -> Result<()> { - if scale_flag { - return Err(Error::NotImplemented); - } - writer.write_u32(u32::from(self.global_gain), GLOBAL_GAIN_BITS); - self.section_data - .write(writer, ics_info.window_sequence, ics_info.max_sfb)?; - self.scale_factor_data - .write(writer, &self.section_data.sfb_cb)?; - self.write_tools(writer, ics_info, audio_object_type) - } - - fn write_tools( - &self, - writer: &mut BitWriter, - ics_info: &IcsInfo, - audio_object_type: u8, - ) -> Result<()> { - // pulse_data_present + body. - writer.write_bit(self.pulse_data_present); - if self.pulse_data_present { - // Table 4.50 Note 1: pulse_data is illegal on - // EIGHT_SHORT_SEQUENCE (the pulse-escape fix-up needs the - // long-window swb_offset_long table). - if ics_info.window_sequence == WindowSequence::EightShort { - return Err(Error::PulseDataEncodeInvalid); - } - let pd = self - .pulse_data - .as_ref() - .ok_or(Error::PulseDataEncodeInvalid)?; - pd.write(writer)?; - } else if self.pulse_data.is_some() { - // Slot populated while the dispatching bit is clear. - return Err(Error::PulseDataEncodeInvalid); - } - - // tns_data_present + body (family-aware widths — the LD - // families emit the reduced 1 / 4 / 3-bit column, mirroring - // the parse side). - writer.write_bit(self.tns_data_present); - if self.tns_data_present { - let td = self.tns_data.as_ref().ok_or(Error::TnsDataEncodeInvalid)?; - td.write_family(writer, ics_info.family, ics_info.window_sequence)?; - } else if self.tns_data.is_some() { - return Err(Error::TnsDataEncodeInvalid); - } - - // gain_control_data_present + body. The §4.6.12 normative - // constraint: AOT 3 (SSR) only. - writer.write_bit(self.gain_control_data_present); - if self.gain_control_data_present { - if audio_object_type != AOT_AAC_SSR { - return Err(Error::GainControlDataEncodeInvalid); - } - let gc = self - .gain_control_data - .as_ref() - .ok_or(Error::GainControlDataEncodeInvalid)?; - gc.write(writer, ics_info.window_sequence)?; - } else if self.gain_control_data.is_some() { - return Err(Error::GainControlDataEncodeInvalid); - } - Ok(()) - } -} - -/// Helper: read an 8-bit `uimsbf` field. -fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { - Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -/// Internal carrier for the pulse / tns / gain_control walk result. -struct ToolDispatch { - pulse_data_present: bool, - pulse_data: Option, - tns_data_present: bool, - tns_data: Option, - gain_control_data_present: bool, - gain_control_data: Option, - spectral_data_bit_offset: u64, -} - -/// Helper: walk the pulse / tns / gain_control dispatch trio after -/// `scale_factor_data()` and return the resulting slots plus the -/// `spectral_data_bit_offset` (measured from `start`). -fn parse_tools( - reader: &mut BitReader<'_>, - ics_info: &IcsInfo, - _audio_object_type: u8, - start: u64, -) -> Result { - let pulse_data_present = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let pulse_data = if pulse_data_present { - // Table 4.50 Note 1: pulse_data is illegal on - // EIGHT_SHORT_SEQUENCE. A conforming stream never sets the - // flag in that case; surface the violation so callers can - // reject the stream rather than crash downstream. - if ics_info.window_sequence == WindowSequence::EightShort { - return Err(Error::PulseDataEncodeInvalid); - } - Some(PulseData::parse(reader)?) - } else { - None - }; - - let tns_data_present = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let tns_data = if tns_data_present { - // Family-aware widths: the ER AAC LD families read the - // reduced 1 / 4 / 3-bit Table 4.155 column (the - // corpus-resolved AOT-23 wire — see - // docs/audio/aac/er-ld-tns-divergence.md §0); everything - // else takes the literal window_sequence dispatch. - Some(TnsData::parse_family( - reader, - ics_info.family, - ics_info.window_sequence, - )?) - } else { - None - }; - - let gain_control_data_present = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let gain_control_data = if gain_control_data_present { - // Per §4.6.12 the gain_control_data tool is AOT-3 (SSR) only; - // a conforming stream never sets the flag on any other AOT. - // The parser surfaces the literal bits regardless of AOT — - // the AOT-validity check is enforced on the writer side so - // we can ingest hostile streams without panicking, and the - // emitter side keeps us from emitting non-conforming streams. - Some(GainControlData::parse(reader, ics_info.window_sequence)?) - } else { - None - }; - - let spectral_data_bit_offset = reader.bit_position() - start; - Ok(ToolDispatch { - pulse_data_present, - pulse_data, - tns_data_present, - tns_data, - gain_control_data_present, - gain_control_data, - spectral_data_bit_offset, - }) -} diff --git a/crates/vendor/oxideav-aac/src/ics_info.rs b/crates/vendor/oxideav-aac/src/ics_info.rs deleted file mode 100644 index d8b3e955..00000000 --- a/crates/vendor/oxideav-aac/src/ics_info.rs +++ /dev/null @@ -1,1100 +0,0 @@ -//! `ics_info()` parser — ISO/IEC 14496-3 §4.4.6 Table 4.6. -//! -//! `ics_info()` carries the per-channel window-shape / window-sequence -//! decision plus the `max_sfb` (number of scalefactor bands actually -//! coded), scale-factor grouping mask for `EIGHT_SHORT_SEQUENCE`, and -//! either the MPEG-2 frequency-domain predictor side-info (AOT 1 -//! Main) or the LTP `ltp_data_present` flag(s) (every other GA AOT -//! that's not 3 = SSR — SSR uses `gain_control_data()` instead of -//! prediction). -//! -//! This parser is the **start** of Phase 2 (channel-element body -//! parsing). It does not consume `global_gain`, `section_data()`, -//! `scale_factor_data()`, `pulse_data()`, `tns_data()`, -//! `gain_control_data()`, or `spectral_data()` — those land in -//! later Phase 2 rounds. `ltp_data()` (Table 4.55) **is** parsed -//! when `ltp_data_present == 1`, because it is dispatched from -//! inside the Table 4.6 syntax itself; deferring it would leave -//! `IcsInfo` in an indeterminate bit-position. -//! -//! ## Derived values -//! -//! Beyond the literal wire fields the parser surfaces the -//! §4.5.2.3.4 / §4.5.2.6.2.4 derivations: -//! -//! * `num_windows` — `8` for `EIGHT_SHORT_SEQUENCE`, `1` otherwise. -//! * `num_window_groups` — `1` for long sequences; for -//! `EIGHT_SHORT_SEQUENCE` it is the number of groups implied by -//! the 7-bit `scale_factor_grouping` mask. The first short -//! window always starts a new group; for windows 1..=7 a `1` bit -//! at position `6 − i` (so bit 6 controls grouping of window 1, -//! …, bit 0 controls window 7) merges window `i+1` into the -//! current group, a `0` opens a new group. This matches the -//! spec's `bit_set(scale_factor_grouping, 6 − i)` pseudo-code. -//! * `window_group_length[g]` — number of short windows in group -//! `g`. Sum is always 8. -//! * `num_swb` — `num_swb_long_window[fs_index]` for long, or -//! `num_swb_short_window[fs_index]` for `EIGHT_SHORT_SEQUENCE`. -//! Sample-rate count tables ([`NUM_SWB_LONG_WINDOW`], -//! [`NUM_SWB_SHORT_WINDOW`]) cover the 12 valid ADTS -//! `sampling_frequency_index` values 0..=11. -//! -//! ## What is *not* in this round -//! -//! * `swb_offset_long_window[]` / `swb_offset_short_window[]` -//! tables — only the *count* of scalefactor bands is needed to -//! step through `ics_info()`. Spectral decoding (Phase 2 mid) -//! will pull in the offset tables. -//! * `sect_sfb_offset[g][section]` — derived from the offset -//! tables, not from `ics_info` proper; landed alongside -//! `section_data()` in a later round. -//! * The `aac_section_data_resilience_flag` / -//! `aac_scalefactor_data_resilience_flag` / -//! `aac_spectral_data_resilience_flag` extension chain (ER AOTs). -//! Surfaced by `GASpecificConfig` `extensionFlag == 1` parsing -//! that itself is a Phase 1 follow-up. -//! -//! ## Predictor / LTP dispatch (Table 4.6) -//! -//! When `window_sequence != EIGHT_SHORT_SEQUENCE`, an extra -//! `predictor_data_present` bit follows `max_sfb`. The branch -//! taken when that bit is 1 depends on `audioObjectType`: -//! -//! * `audioObjectType == 1` (Main) — read `predictor_reset` (1 bit); -//! if set, read `predictor_reset_group_number` (5 bits); then read -//! `prediction_used[sfb]` for `sfb in 0..min(max_sfb, PRED_SFB_MAX)`. -//! `PRED_SFB_MAX` is sample-rate dependent (see -//! [`PRED_SFB_MAX`]). -//! * Any other AOT (LC, SSR, LTP, scalable, TwinVQ, ER variants) — -//! read `ltp_data_present` (1 bit); if set, parse `ltp_data()` -//! per Table 4.55. If the surrounding element is a CPE with -//! `common_window == 1`, a *second* `ltp_data_present` (+ -//! optional `ltp_data()`) follows for the paired channel. -//! -//! The spec attaches a normative caveat: for plain LC streams the -//! `predictor_data_present` bit is required to be 0 by ISO/IEC -//! 14496-3 §1.5.1.1 (AOT 2 does not own a predictor). The parser -//! enforces nothing here — it surfaces whatever the wire said and -//! lets a higher-layer validator decide. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::swb_offset::{long_window_offsets_family, short_window_offsets_family, FrameFamily}; -use crate::{Error, Result}; - -/// Sentinel for the `EIGHT_SHORT_SEQUENCE` window-sequence value. -/// Exposed as a `pub const` so consumers can compare without -/// matching against [`WindowSequence`]. -pub const EIGHT_SHORT_SEQUENCE: u8 = 2; - -/// `window_sequence` enumeration — ISO/IEC 14496-3 §4.5.2.3.1.1 / -/// Table 4.128. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum WindowSequence { - /// `0` — one 1024-sample (or 960-sample if `frameLengthFlag`) - /// MDCT covering the full frame. - OnlyLong = 0, - /// `1` — long MDCT with a start window on the right half. - /// Always preceded by `OnlyLong` and followed by - /// `EightShort` in a transient-onset transition. - LongStart = 1, - /// `2` — eight 128-sample MDCTs; `scale_factor_grouping` and - /// `num_window_groups` are meaningful here. - EightShort = 2, - /// `3` — long MDCT with a stop window on the left half. Tail - /// of an `EightShort` burst. - LongStop = 3, -} - -impl WindowSequence { - /// Map a 2-bit wire value (0..=3) to the corresponding variant. - pub fn from_bits(bits: u8) -> Self { - match bits & 0b11 { - 0 => WindowSequence::OnlyLong, - 1 => WindowSequence::LongStart, - 2 => WindowSequence::EightShort, - _ => WindowSequence::LongStop, - } - } - - /// `true` ⇔ `EIGHT_SHORT_SEQUENCE`. - pub fn is_eight_short(self) -> bool { - matches!(self, WindowSequence::EightShort) - } -} - -/// `window_shape` enumeration — ISO/IEC 14496-3 §4.5.2.3.1.1. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum WindowShape { - /// `0` — sine window. Default for AAC-LC. - Sine = 0, - /// `1` — Kaiser-Bessel-derived (KBD) window. - Kbd = 1, -} - -impl WindowShape { - /// Map a 1-bit wire value (0..=1) to the variant. - pub fn from_bit(bit: bool) -> Self { - if bit { - WindowShape::Kbd - } else { - WindowShape::Sine - } - } -} - -/// `predictor_data()` body (Table 4.6, Main branch). Only the Main -/// AOT (`audioObjectType == 1`) ever instantiates this. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PredictorData { - /// `predictor_reset` bit. - pub reset: bool, - /// `predictor_reset_group_number` (5 bits) — only present when - /// `reset == true`. Identifies which group of predictors to - /// re-initialise this frame. - pub reset_group_number: Option, - /// `prediction_used[sfb]` for `sfb in 0..min(max_sfb, - /// PRED_SFB_MAX[fs_index])`. Each entry is a single bit. - pub prediction_used: Vec, -} - -/// `ltp_data()` body (Table 4.55). -/// -/// Two variants are distinguished by `audioObjectType == 23` -/// (`ER_AAC_LD`), which carries a delta-coded `ltp_lag_update` / -/// `ltp_lag` pair instead of an unconditional 11-bit `ltp_lag`. -/// For `EIGHT_SHORT_SEQUENCE` in the non-LD branch, -/// `ltp_long_used[]` is **absent** per the 2009 edition — the -/// parser emits an empty `long_used` vec in that case. The 2001 -/// edition instead carries a per-short-window -/// `ltp_short_used` / `ltp_short_lag_present` / `ltp_short_lag` -/// loop there (see [`LtpEdition`] and [`LtpShortWindow`]); those -/// records land in `short`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LtpData { - /// `ltp_lag_update` bit. Only present for `audioObjectType == - /// 23` (LD); `None` for every other AOT. - pub lag_update: Option, - /// `ltp_lag`. For LD this is 10 bits and may be absent when - /// `lag_update == false`; for non-LD this is 11 bits and is - /// always present. - pub lag: Option, - /// `ltp_coef` (3 bits) — index into the 8-entry LTP - /// coefficient codebook. - pub coef: u8, - /// `ltp_long_used[sfb]` for `sfb in 0..min(max_sfb, - /// MAX_LTP_LONG_SFB)`. Empty when the non-LD AOT is using - /// `EIGHT_SHORT_SEQUENCE` (both editions omit the long loop in - /// that case). - pub long_used: Vec, - /// ISO/IEC 14496-3:2001 Table 4.55 per-short-window LTP - /// records — `Some(v)` (with `v.len() == num_windows == 8`) - /// only when the non-LD `EIGHT_SHORT_SEQUENCE` branch is - /// parsed / written under [`LtpEdition::Iso2001`]. Always - /// `None` for long window sequences, the LD branch, and the - /// 2009 edition (which removed short-window LTP — §4.6.7.1 - /// "LTP is restricted to long windows only"). - pub short: Option>, -} - -/// One short window's LTP record from the ISO/IEC 14496-3:2001 -/// Table 4.55 `EIGHT_SHORT_SEQUENCE` branch. -/// -/// Wire layout (2001 edition only): `ltp_short_used[w]` (1 bit); -/// if set, `ltp_short_lag_present[w]` (1 bit); if *that* is set, -/// `ltp_short_lag[w]` (4 bits). Per §4.6.7.2 (2001) the 4-bit -/// field is "a 4-bit number specifying the relative delay for -/// each short window to ltp_lag from −8 to 7" — this crate reads -/// it as a 4-bit two's-complement integer (the standard MPEG -/// reading of an n-bit field whose documented range is -/// −2^(n−1)..2^(n−1)−1). When `ltp_short_lag_present == 0` the -/// relative delay is 0 per §4.6.7.3 (2001). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LtpShortWindow { - /// `ltp_short_used[w]` — whether LTP contributes to this short - /// window at all. - pub used: bool, - /// `ltp_short_lag_present[w]` — whether the 4-bit relative lag - /// was actually transmitted. Only meaningful when `used`; - /// always `false` otherwise. Kept distinct from `lag == 0` so a - /// re-encode reproduces the exact wire bits. - pub lag_present: bool, - /// The relative delay for this window, `−8..=7`, added to the - /// frame's `ltp_lag`. `0` when `lag_present == false`. - pub lag: i8, -} - -/// Which edition of the ISO/IEC 14496-3 Table 4.55 `ltp_data()` -/// syntax to apply for the non-LD `EIGHT_SHORT_SEQUENCE` branch. -/// -/// The 2001 edition transmits a per-short-window -/// `ltp_short_used` / `ltp_short_lag_present` / `ltp_short_lag` -/// loop after `ltp_coef`; the 2009 edition removed short-window -/// LTP entirely (§4.6.7.1: "LTP is restricted to long windows -/// only") and transmits nothing there. The two forms are -/// wire-incompatible for `EIGHT_SHORT_SEQUENCE` frames with -/// `ltp_data_present == 1`, and the bitstream itself does not -/// signal which edition the encoder followed, so the choice is an -/// out-of-band caller decision. Long window sequences and the LD -/// branch are identical in both editions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum LtpEdition { - /// ISO/IEC 14496-3:2009 Table 4.55 — no short-window LTP - /// fields (the form every contemporary stream follows). - #[default] - Iso2009, - /// ISO/IEC 14496-3:2001 Table 4.55 — per-short-window - /// `ltp_short_used[w]` loop for `EIGHT_SHORT_SEQUENCE`. - Iso2001, -} - -/// Per-Table 4.55 maximum number of scalefactor bands carrying -/// `ltp_long_used[]`. ISO/IEC 14496-3 §4.6.7.2. -pub const MAX_LTP_LONG_SFB: usize = 40; - -/// Number of short windows in an `EIGHT_SHORT_SEQUENCE` frame — -/// `num_windows == 8` per ISO/IEC 14496-3 §4.5.2.3.4, and the -/// iteration count of the 2001-edition Table 4.55 short-window -/// LTP loop. -pub const SHORT_WINDOWS_PER_FRAME: usize = 8; - -/// Per-Table 4.6 / Table 62 (ISO/IEC 13818-7 §13.3.1) -/// sample-rate-dependent `PRED_SFB_MAX` constant. Indexed by -/// ADTS `sampling_frequency_index` 0..=11. -/// -/// | idx | rate (Hz) | PRED_SFB_MAX | -/// |-------|--------------|--------------| -/// | 0 | 96 000 | 33 | -/// | 1 | 88 200 | 33 | -/// | 2 | 64 000 | 38 | -/// | 3 | 48 000 | 40 | -/// | 4 | 44 100 | 40 | -/// | 5 | 32 000 | 40 | -/// | 6 | 24 000 | 41 | -/// | 7 | 22 050 | 41 | -/// | 8 | 16 000 | 37 | -/// | 9 | 12 000 | 37 | -/// | 10 | 11 025 | 37 | -/// | 11 | 8 000 | 34 | -pub const PRED_SFB_MAX: [u8; 12] = [33, 33, 38, 40, 40, 40, 41, 41, 37, 37, 37, 34]; - -/// `num_swb_long_window[fs_index]` for the canonical 1024-line -/// long window — ISO/IEC 14496-3 Tables 4.129 / 4.131 / 4.132 / -/// 4.134 / 4.136 / 4.138 / 4.140, distilled to the count column. -/// -/// | idx | rate (Hz) | num_swb | source | -/// |-------|--------------|---------|----------------| -/// | 0 | 96 000 | 41 | Table 4.140 | -/// | 1 | 88 200 | 41 | Table 4.140 | -/// | 2 | 64 000 | 47 | Table 4.138 | -/// | 3 | 48 000 | 49 | Table 4.129 | -/// | 4 | 44 100 | 49 | Table 4.129 | -/// | 5 | 32 000 | 51 | Table 4.131 | -/// | 6 | 24 000 | 47 | Table 4.136 | -/// | 7 | 22 050 | 47 | Table 4.136 | -/// | 8 | 16 000 | 43 | Table 4.134 | -/// | 9 | 12 000 | 43 | Table 4.134 | -/// | 10 | 11 025 | 43 | Table 4.134 | -/// | 11 | 8 000 | 40 | Table 4.132 | -pub const NUM_SWB_LONG_WINDOW: [u8; 12] = [41, 41, 47, 49, 49, 51, 47, 47, 43, 43, 43, 40]; - -/// `num_swb_short_window[fs_index]` for the canonical 128-line -/// short window — ISO/IEC 14496-3 Tables 4.130 / 4.133 / 4.135 / -/// 4.137 / 4.139 / 4.141. -/// -/// | idx | rate (Hz) | num_swb | source | -/// |-------|--------------|---------|----------------| -/// | 0 | 96 000 | 12 | Table 4.141 | -/// | 1 | 88 200 | 12 | Table 4.141 | -/// | 2 | 64 000 | 12 | Table 4.139 | -/// | 3 | 48 000 | 14 | Table 4.130 | -/// | 4 | 44 100 | 14 | Table 4.130 | -/// | 5 | 32 000 | 14 | Table 4.130 | -/// | 6 | 24 000 | 15 | Table 4.137 | -/// | 7 | 22 050 | 15 | Table 4.137 | -/// | 8 | 16 000 | 15 | Table 4.135 | -/// | 9 | 12 000 | 15 | Table 4.135 | -/// | 10 | 11 025 | 15 | Table 4.135 | -/// | 11 | 8 000 | 15 | Table 4.133 | -pub const NUM_SWB_SHORT_WINDOW: [u8; 12] = [12, 12, 12, 14, 14, 14, 15, 15, 15, 15, 15, 15]; - -/// Parsed `ics_info()` (Table 4.6) plus the §4.5.2.3.4 derivations -/// that depend on it. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IcsInfo { - /// The §4.5.1.1 frame-length family this `ics_info()` was parsed - /// under (`frameLengthFlag` + AOT). Governs every derived band - /// geometry: the `num_swb` below, the SWB offset tables the - /// numeric chain reads, and the §4.6.11 transform lengths. - pub family: FrameFamily, - /// `ics_reserved_bit` — spec mandates `0`; the parser surfaces - /// the wire value without enforcement (some encoders set it - /// even though they shouldn't). - pub ics_reserved_bit: bool, - /// `window_sequence` (2 bits, Table 4.128). - pub window_sequence: WindowSequence, - /// `window_shape` (1 bit, Table 4.129 reference). - pub window_shape: WindowShape, - /// `max_sfb` — 4 bits in the `EIGHT_SHORT_SEQUENCE` branch, - /// 6 bits in every other branch. - pub max_sfb: u8, - /// `scale_factor_grouping` (7 bits) — only present when - /// `window_sequence == EIGHT_SHORT_SEQUENCE`. Bit `6 − i` - /// controls whether window `i + 1` joins the current group - /// (`1`) or opens a new group (`0`) for `i in 0..7`. - pub scale_factor_grouping: Option, - /// `predictor_data_present` (1 bit) — only present when - /// `window_sequence != EIGHT_SHORT_SEQUENCE`. - pub predictor_data_present: bool, - /// Main-AOT `predictor_data()` body (Table 4.6 Main branch). - /// Populated when `predictor_data_present == true` and - /// `audioObjectType == 1`. - pub predictor_data: Option, - /// First `ltp_data_present` bit — read when - /// `predictor_data_present == true` and `audioObjectType != - /// 1`. `false` if not read. - pub ltp_data_present: bool, - /// Channel's own `ltp_data()` body — populated when - /// `ltp_data_present == true`. - pub ltp_data: Option, - /// `common_window`-paired channel `ltp_data_present` bit — - /// only read when the caller passed `common_window == true` - /// AND `predictor_data_present == true` AND `audioObjectType - /// != 1`. `None` if not present. - pub ltp_data_present_pair: Option, - /// `ltp_data()` body for the paired channel — populated when - /// `ltp_data_present_pair == Some(true)`. - pub ltp_data_pair: Option, - - // Derived fields (§4.5.2.3.4) — populated unconditionally. - /// Number of MDCT windows in this frame (`8` for short, `1` - /// otherwise). - pub num_windows: u8, - /// Number of window-groups after scale-factor grouping. Always - /// `1` for long sequences; for `EIGHT_SHORT_SEQUENCE` it is in - /// `1..=8` per the [`Self::scale_factor_grouping`] mask. - pub num_window_groups: u8, - /// Number of windows in each group; `window_group_length[g]` - /// for `g in 0..num_window_groups`. Sum is always - /// `num_windows`. - pub window_group_length: Vec, - /// Total scalefactor window bands for this frame — - /// `NUM_SWB_LONG_WINDOW[fs_index]` for long sequences, - /// `NUM_SWB_SHORT_WINDOW[fs_index]` for short sequences. - pub num_swb: u8, -} - -impl IcsInfo { - /// Parse a single `ics_info()` from the bit-reader. - /// - /// * `audio_object_type` — the surrounding ASC's effective - /// `audioObjectType` (post SBR/PS unwrap). Used to pick - /// between the Main / LTP predictor branches. - /// * `sampling_frequency_index` — the surrounding ASC's - /// `samplingFrequencyIndex` (the *core* index for hierarchical - /// SBR/PS — ics_info follows the inner AAC framerate, not the - /// SBR output rate). Must be in `0..=11` (the 24-bit - /// explicit-rate escape from §1.6.2.1 is not supported here - /// because the SWB tables are indexed by the standard 12 - /// rates). - /// * `common_window` — `true` ⇔ the surrounding element is a - /// `channel_pair_element()` with the shared-info form - /// (`common_window == 1` per Table 4.5); controls whether - /// the second `ltp_data_present` (+ optional second - /// `ltp_data()`) is consumed. - pub fn parse( - reader: &mut BitReader<'_>, - audio_object_type: u8, - sampling_frequency_index: u8, - common_window: bool, - ) -> Result { - Self::parse_family( - reader, - FrameFamily::Lc1024, - audio_object_type, - sampling_frequency_index, - common_window, - ) - } - - /// [`IcsInfo::parse`] under an explicit §4.5.1.1 frame-length - /// family. The wire layout of `ics_info()` itself is - /// family-independent; the family drives the derived band counts - /// (`num_swb` comes from the family's own SWB tables) and the LD - /// constraint checks: an ER AAC LD stream has no block switching - /// (§4.6.17.2.2), so any `window_sequence` other than - /// `ONLY_LONG_SEQUENCE` under an LD family surfaces - /// [`Error::LdShortWindow`]. - pub fn parse_family( - reader: &mut BitReader<'_>, - family: FrameFamily, - audio_object_type: u8, - sampling_frequency_index: u8, - common_window: bool, - ) -> Result { - let fs_index = sampling_frequency_index as usize; - if fs_index >= NUM_SWB_LONG_WINDOW.len() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex( - sampling_frequency_index, - )); - } - - let ics_reserved_bit = read_bit(reader)?; - let window_sequence_bits = read_u8(reader, 2)?; - let window_sequence = WindowSequence::from_bits(window_sequence_bits); - let window_shape = WindowShape::from_bit(read_bit(reader)?); - - if family.is_ld() && window_sequence != WindowSequence::OnlyLong { - return Err(Error::LdShortWindow); - } - - let mut scale_factor_grouping = None; - let mut predictor_data_present = false; - let mut predictor_data = None; - let mut ltp_data_present = false; - let mut ltp_data = None; - let mut ltp_data_present_pair = None; - let mut ltp_data_pair = None; - - let max_sfb; - if window_sequence.is_eight_short() { - max_sfb = read_u8(reader, 4)?; - scale_factor_grouping = Some(read_u8(reader, 7)?); - } else { - max_sfb = read_u8(reader, 6)?; - predictor_data_present = read_bit(reader)?; - if predictor_data_present { - if audio_object_type == 1 { - // Main predictor side info. - let reset = read_bit(reader)?; - let reset_group_number = if reset { - Some(read_u8(reader, 5)?) - } else { - None - }; - let pred_sfb_max = PRED_SFB_MAX[fs_index] as u16; - let n = core::cmp::min(max_sfb as u16, pred_sfb_max) as usize; - let mut prediction_used = Vec::with_capacity(n); - for _ in 0..n { - prediction_used.push(read_bit(reader)?); - } - predictor_data = Some(PredictorData { - reset, - reset_group_number, - prediction_used, - }); - } else { - // LTP / other GA AOTs — Table 4.6 nests a - // dedicated `ltp_data_present` bit inside the - // `predictor_data_present` branch, so an AU can - // signal the branch with the channel's own LTP - // off (e.g. only the common_window pair bit - // follows). Corpus-confirmed by the ISO/IEC - // 14496-26 `er_ad1000*`/`er_ad1103*` LD vectors, - // which desynchronise without this bit. - ltp_data_present = read_bit(reader)?; - if ltp_data_present { - ltp_data = Some(parse_ltp_data( - reader, - audio_object_type, - window_sequence, - max_sfb, - )?); - } - if common_window { - let pair_flag = read_bit(reader)?; - ltp_data_present_pair = Some(pair_flag); - if pair_flag { - ltp_data_pair = Some(parse_ltp_data( - reader, - audio_object_type, - window_sequence, - max_sfb, - )?); - } - } - } - } else if common_window && audio_object_type != 1 { - // Spec note: when predictor_data_present == 0, the - // second ltp_data_present bit is also not - // transmitted (Table 4.6 only enters the LTP - // branch when predictor_data_present == 1). The - // pair-channel flag therefore stays absent. - } - } - - // §4.5.2.3.4 derivations. - let (num_windows, num_window_groups, window_group_length, num_swb) = - derive_window_grouping_family( - family, - window_sequence, - scale_factor_grouping, - sampling_frequency_index, - )?; - - Ok(IcsInfo { - family, - ics_reserved_bit, - window_sequence, - window_shape, - max_sfb, - scale_factor_grouping, - predictor_data_present, - predictor_data, - ltp_data_present, - ltp_data, - ltp_data_present_pair, - ltp_data_pair, - num_windows, - num_window_groups, - window_group_length, - num_swb, - }) - } - - /// The active per-window spectral length for this frame's - /// `window_sequence` under the frame's [`FrameFamily`]: the - /// family's short-window length (128 / 120) for - /// `EIGHT_SHORT_SEQUENCE`, the family's frame length - /// (1024 / 960 / 512 / 480) otherwise. The parser guarantees an - /// LD family never carries a short sequence, so the LD lookup - /// error is unreachable through parsed values. - pub fn window_len(&self) -> Result { - if self.window_sequence.is_eight_short() { - self.family.short_window_len().ok_or(Error::LdShortWindow) - } else { - Ok(self.family.frame_len()) - } - } - - /// The active `swb_offset` table for this frame's - /// `window_sequence` under the frame's [`FrameFamily`] at - /// `fs_index` — the short-window table for `EIGHT_SHORT_SEQUENCE`, - /// the long-window table otherwise. - pub fn swb_offsets(&self, fs_index: u8) -> Result<&'static [u16]> { - if self.window_sequence.is_eight_short() { - short_window_offsets_family(self.family, fs_index) - } else { - long_window_offsets_family(self.family, fs_index) - } - } - - /// Encode `ics_info()` onto `writer`, the inverse of - /// [`IcsInfo::parse`]. - /// - /// The writer mirrors Table 4.6 verbatim — `ics_reserved_bit` - /// (1 bit), `window_sequence` (2 bits), `window_shape` (1 bit), - /// then either `max_sfb` (4 bits) + `scale_factor_grouping` - /// (7 bits) for `EIGHT_SHORT_SEQUENCE`, or `max_sfb` (6 bits) + - /// `predictor_data_present` (1 bit) plus the per-AOT - /// predictor / LTP body for every other window sequence. - /// - /// The `audio_object_type` / `sampling_frequency_index` / - /// `common_window` parameters must match the values the parser - /// was (or would be) invoked with. They drive the branch the - /// encoder takes for the Main vs LTP predictor body and the - /// `prediction_used[]` cap (`PRED_SFB_MAX[fs_index]` for AOT 1). - /// - /// Returns [`Error::IcsInfoEncodeInvalid`] if the in-memory - /// [`IcsInfo`] violates a wire-field invariant: - /// - /// * `max_sfb` exceeds its field width - /// (`> 15` for `EIGHT_SHORT_SEQUENCE`, `> 63` otherwise). - /// * `scale_factor_grouping` is `None` for `EIGHT_SHORT_SEQUENCE`, - /// `Some(_)` otherwise, or its value exceeds 7 bits. - /// * `predictor_data_present == true` for `EIGHT_SHORT_SEQUENCE` - /// (Table 4.6 omits the bit on the short branch). - /// * `predictor_data` is `Some` while `audio_object_type != 1`, - /// or `None` while the predictor bit is set with AOT 1. - /// * Predictor `reset_group_number` doesn't match - /// `reset.is_some()` parity, or exceeds 5 bits. - /// * Predictor `prediction_used.len()` differs from `min(max_sfb, - /// PRED_SFB_MAX[fs_index])`. - /// * LTP body fields (lag width, `coef`, `long_used[]` length) do - /// not satisfy Table 4.55 (delegated to [`write_ltp_data`]). - /// * The paired-channel LTP slot is populated while - /// `common_window == false`, or while `predictor_data_present - /// == false`, or while `audio_object_type == 1`. - /// * `sampling_frequency_index` is outside `0..=11`. - pub fn write( - &self, - writer: &mut BitWriter, - audio_object_type: u8, - sampling_frequency_index: u8, - common_window: bool, - ) -> Result<()> { - let fs_index = sampling_frequency_index as usize; - if fs_index >= NUM_SWB_LONG_WINDOW.len() { - return Err(Error::IcsInfoEncodeInvalid); - } - // §4.6.17.2.2 — an LD-family ics_info can only carry - // ONLY_LONG_SEQUENCE (no block switching exists for LD). - if self.family.is_ld() && self.window_sequence != WindowSequence::OnlyLong { - return Err(Error::IcsInfoEncodeInvalid); - } - - writer.write_bit(self.ics_reserved_bit); - writer.write_u32(self.window_sequence as u32 & 0b11, 2); - writer.write_u32(self.window_shape as u32 & 0b1, 1); - - if self.window_sequence.is_eight_short() { - if self.max_sfb > 0x0f { - return Err(Error::IcsInfoEncodeInvalid); - } - let mask = self - .scale_factor_grouping - .ok_or(Error::IcsInfoEncodeInvalid)?; - if mask > 0x7f { - return Err(Error::IcsInfoEncodeInvalid); - } - // EIGHT_SHORT branch has neither predictor_data_present - // nor any LTP body — reject populated slots before they - // silently round-trip into a non-conforming stream. - if self.predictor_data_present - || self.predictor_data.is_some() - || self.ltp_data_present - || self.ltp_data.is_some() - || self.ltp_data_present_pair.is_some() - || self.ltp_data_pair.is_some() - { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_u32(self.max_sfb as u32, 4); - writer.write_u32(mask as u32, 7); - } else { - if self.max_sfb > 0x3f { - return Err(Error::IcsInfoEncodeInvalid); - } - if self.scale_factor_grouping.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_u32(self.max_sfb as u32, 6); - writer.write_bit(self.predictor_data_present); - - if self.predictor_data_present { - if audio_object_type == 1 { - // Main predictor side info. - if self.ltp_data_present - || self.ltp_data.is_some() - || self.ltp_data_present_pair.is_some() - || self.ltp_data_pair.is_some() - { - return Err(Error::IcsInfoEncodeInvalid); - } - let pd = self - .predictor_data - .as_ref() - .ok_or(Error::IcsInfoEncodeInvalid)?; - // reset_group_number parity matches reset bit. - if pd.reset != pd.reset_group_number.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - let pred_sfb_max = PRED_SFB_MAX[fs_index] as u16; - let expected = core::cmp::min(self.max_sfb as u16, pred_sfb_max) as usize; - if pd.prediction_used.len() != expected { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_bit(pd.reset); - if let Some(g) = pd.reset_group_number { - if g > 0x1f { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_u32(g as u32, 5); - } - for &b in &pd.prediction_used { - writer.write_bit(b); - } - } else { - // LTP / non-Main branch — Table 4.6 nests a - // dedicated `ltp_data_present` bit (mirror of the - // parse side). - if self.predictor_data.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_bit(self.ltp_data_present); - if self.ltp_data_present { - let ltp = self.ltp_data.as_ref().ok_or(Error::IcsInfoEncodeInvalid)?; - write_ltp_data( - writer, - ltp, - audio_object_type, - self.window_sequence, - self.max_sfb, - )?; - } else if self.ltp_data.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - if common_window { - let pair_flag = self - .ltp_data_present_pair - .ok_or(Error::IcsInfoEncodeInvalid)?; - writer.write_bit(pair_flag); - if pair_flag { - let ltp2 = self - .ltp_data_pair - .as_ref() - .ok_or(Error::IcsInfoEncodeInvalid)?; - write_ltp_data( - writer, - ltp2, - audio_object_type, - self.window_sequence, - self.max_sfb, - )?; - } else if self.ltp_data_pair.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - } else if self.ltp_data_present_pair.is_some() || self.ltp_data_pair.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - } - } else { - // predictor_data_present == 0: no predictor / LTP body - // is emitted at all (Table 4.6 only enters either - // branch under the predictor bit). Reject populated - // slots so a stale in-memory structure cannot - // silently desync from the wire. - if self.predictor_data.is_some() - || self.ltp_data_present - || self.ltp_data.is_some() - || self.ltp_data_present_pair.is_some() - || self.ltp_data_pair.is_some() - { - return Err(Error::IcsInfoEncodeInvalid); - } - } - } - - Ok(()) - } -} - -/// `ltp_data()` per Table 4.55 (2009 edition). Public to allow -/// standalone unit tests; in normal use it is invoked indirectly -/// via [`IcsInfo::parse`]. Equivalent to -/// [`parse_ltp_data_edition`] with [`LtpEdition::Iso2009`] and the -/// spec's `num_windows == 8` for `EIGHT_SHORT_SEQUENCE`. -pub fn parse_ltp_data( - reader: &mut BitReader<'_>, - audio_object_type: u8, - window_sequence: WindowSequence, - max_sfb: u8, -) -> Result { - parse_ltp_data_edition( - reader, - audio_object_type, - window_sequence, - max_sfb, - LtpEdition::Iso2009, - ) -} - -/// `ltp_data()` per Table 4.55, edition-selectable. -/// -/// [`LtpEdition::Iso2009`] behaves exactly like -/// [`parse_ltp_data`]. [`LtpEdition::Iso2001`] additionally reads -/// the per-short-window `ltp_short_used[w]` / -/// `ltp_short_lag_present[w]` / `ltp_short_lag[w]` loop (8 -/// iterations — `num_windows` for `EIGHT_SHORT_SEQUENCE` is -/// always 8, §4.5.2.3.4) when the non-LD branch sees a short -/// window sequence; the records land in [`LtpData::short`]. The -/// LD branch and all long window sequences are edition-invariant. -pub fn parse_ltp_data_edition( - reader: &mut BitReader<'_>, - audio_object_type: u8, - window_sequence: WindowSequence, - max_sfb: u8, - edition: LtpEdition, -) -> Result { - if audio_object_type == 23 { - // ER_AAC_LD branch. - let lag_update = read_bit(reader)?; - let lag = if lag_update { - Some(read_u16(reader, 10)?) - } else { - None - }; - let coef = read_u8(reader, 3)?; - let n = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); - let mut long_used = Vec::with_capacity(n); - for _ in 0..n { - long_used.push(read_bit(reader)?); - } - Ok(LtpData { - lag_update: Some(lag_update), - lag, - coef, - long_used, - short: None, - }) - } else { - let lag = read_u16(reader, 11)?; - let coef = read_u8(reader, 3)?; - let mut short = None; - let long_used = if window_sequence.is_eight_short() { - if edition == LtpEdition::Iso2001 { - // 2001 Table 4.55: for (w = 0; w < num_windows; w++) - // { ltp_short_used[w]; if set → - // ltp_short_lag_present[w]; if set → - // ltp_short_lag[w] (4 bits). } - let mut v = Vec::with_capacity(SHORT_WINDOWS_PER_FRAME); - for _ in 0..SHORT_WINDOWS_PER_FRAME { - let used = read_bit(reader)?; - let (lag_present, lag) = if used { - let lag_present = read_bit(reader)?; - let lag = if lag_present { - // 4-bit two's-complement −8..=7 (see - // LtpShortWindow docs). - let raw = read_u8(reader, 4)?; - ((raw << 4) as i8) >> 4 - } else { - 0 - }; - (lag_present, lag) - } else { - (false, 0) - }; - v.push(LtpShortWindow { - used, - lag_present, - lag, - }); - } - short = Some(v); - } - Vec::new() - } else { - let n = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); - let mut v = Vec::with_capacity(n); - for _ in 0..n { - v.push(read_bit(reader)?); - } - v - }; - Ok(LtpData { - lag_update: None, - lag: Some(lag), - coef, - long_used, - short, - }) - } -} - -/// Encode an `ltp_data()` (Table 4.55) body onto `writer`, the -/// inverse of [`parse_ltp_data`]. -/// -/// Mirrors the parser's two branches: -/// -/// * `audio_object_type == 23` (ER AAC LD) — write `ltp_lag_update` -/// (1 bit); if set, write `ltp_lag` (10 bits); then `ltp_coef` -/// (3 bits); then `ltp_long_used[sfb]` for `sfb in 0..min(max_sfb, -/// MAX_LTP_LONG_SFB)`. -/// * Every other AOT — write `ltp_lag` (11 bits, always), `ltp_coef` -/// (3 bits), then `ltp_long_used[]` *unless* the surrounding -/// `ics_info()` says `EIGHT_SHORT_SEQUENCE` (the spec omits the -/// loop in that case). -/// -/// Returns [`Error::IcsInfoEncodeInvalid`] when the in-memory -/// [`LtpData`] is inconsistent with the AOT or `window_sequence` -/// context (e.g. `lag_update == Some(_)` for a non-LD AOT, missing -/// `lag` for an LD `lag_update == true` slot, `coef > 7`, `lag` -/// exceeding its field width, or `long_used.len()` not matching -/// `min(max_sfb, MAX_LTP_LONG_SFB)` in the loop branch). -pub fn write_ltp_data( - writer: &mut BitWriter, - ltp: &LtpData, - audio_object_type: u8, - window_sequence: WindowSequence, - max_sfb: u8, -) -> Result<()> { - write_ltp_data_edition( - writer, - ltp, - audio_object_type, - window_sequence, - max_sfb, - LtpEdition::Iso2009, - ) -} - -/// Encode an `ltp_data()` (Table 4.55) body, edition-selectable — -/// the inverse of [`parse_ltp_data_edition`]. -/// -/// Under [`LtpEdition::Iso2001`] a non-LD `EIGHT_SHORT_SEQUENCE` -/// body must carry `short == Some(v)` with `v.len() == 8` -/// ([`SHORT_WINDOWS_PER_FRAME`]) and each [`LtpShortWindow`] -/// internally consistent (`!used ⇒ !lag_present`, -/// `!lag_present ⇒ lag == 0`, `lag ∈ −8..=7`); under -/// [`LtpEdition::Iso2009`] `short` must be `None` everywhere. -/// All other validation matches [`write_ltp_data`]. -pub fn write_ltp_data_edition( - writer: &mut BitWriter, - ltp: &LtpData, - audio_object_type: u8, - window_sequence: WindowSequence, - max_sfb: u8, - edition: LtpEdition, -) -> Result<()> { - if ltp.coef > 0x07 { - return Err(Error::IcsInfoEncodeInvalid); - } - // `short` is only representable on the wire in the 2001 - // non-LD EIGHT_SHORT branch; reject it anywhere else so an - // in-memory record can't silently drop fields. - let short_branch = audio_object_type != 23 - && window_sequence.is_eight_short() - && edition == LtpEdition::Iso2001; - if !short_branch && ltp.short.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - if audio_object_type == 23 { - let lag_update = ltp.lag_update.ok_or(Error::IcsInfoEncodeInvalid)?; - writer.write_bit(lag_update); - if lag_update { - let lag = ltp.lag.ok_or(Error::IcsInfoEncodeInvalid)?; - if lag > 0x3ff { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_u32(lag as u32, 10); - } else if ltp.lag.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_u32(ltp.coef as u32, 3); - let expected = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); - if ltp.long_used.len() != expected { - return Err(Error::IcsInfoEncodeInvalid); - } - for &b in <p.long_used { - writer.write_bit(b); - } - } else { - if ltp.lag_update.is_some() { - return Err(Error::IcsInfoEncodeInvalid); - } - let lag = ltp.lag.ok_or(Error::IcsInfoEncodeInvalid)?; - if lag > 0x7ff { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_u32(lag as u32, 11); - writer.write_u32(ltp.coef as u32, 3); - if window_sequence.is_eight_short() { - if !ltp.long_used.is_empty() { - return Err(Error::IcsInfoEncodeInvalid); - } - if edition == LtpEdition::Iso2001 { - // 2001 Table 4.55 per-short-window loop. - let short = ltp.short.as_ref().ok_or(Error::IcsInfoEncodeInvalid)?; - if short.len() != SHORT_WINDOWS_PER_FRAME { - return Err(Error::IcsInfoEncodeInvalid); - } - for w in short { - // Internal consistency: an unused window has no - // further fields; an absent lag means rel 0. - if !w.used && (w.lag_present || w.lag != 0) { - return Err(Error::IcsInfoEncodeInvalid); - } - if !w.lag_present && w.lag != 0 { - return Err(Error::IcsInfoEncodeInvalid); - } - if !(-8..=7).contains(&w.lag) { - return Err(Error::IcsInfoEncodeInvalid); - } - writer.write_bit(w.used); - if w.used { - writer.write_bit(w.lag_present); - if w.lag_present { - // 4-bit two's complement. - writer.write_u32((w.lag as u32) & 0x0f, 4); - } - } - } - } - } else { - let expected = core::cmp::min(max_sfb as usize, MAX_LTP_LONG_SFB); - if ltp.long_used.len() != expected { - return Err(Error::IcsInfoEncodeInvalid); - } - for &b in <p.long_used { - writer.write_bit(b); - } - } - } - Ok(()) -} - -/// Compute (`num_windows`, `num_window_groups`, -/// `window_group_length`, `num_swb`) per ISO/IEC 14496-3 -/// §4.5.2.3.4. Exposed publicly so encoder-side code or -/// pre-section_data setup can compute the same derivations -/// without re-parsing an `ics_info`. -pub fn derive_window_grouping( - window_sequence: WindowSequence, - scale_factor_grouping: Option, - fs_index: usize, -) -> (u8, u8, Vec, u8) { - if !window_sequence.is_eight_short() { - return (1, 1, vec![1], NUM_SWB_LONG_WINDOW[fs_index]); - } - derive_short_grouping(scale_factor_grouping, NUM_SWB_SHORT_WINDOW[fs_index]) -} - -/// [`derive_window_grouping`] under an explicit §4.5.1.1 frame-length -/// family: `num_swb` is read from the family's own SWB offset tables -/// ([`crate::swb_offset::long_window_offsets_family`] / -/// [`crate::swb_offset::short_window_offsets_family`]), so the 960 / -/// LD band counts come out right. Errors surface for rates a family -/// table does not define and for a short-window request under an LD -/// family. -pub fn derive_window_grouping_family( - family: FrameFamily, - window_sequence: WindowSequence, - scale_factor_grouping: Option, - fs_index: u8, -) -> Result<(u8, u8, Vec, u8)> { - if !window_sequence.is_eight_short() { - let num_swb = (long_window_offsets_family(family, fs_index)?.len() - 1) as u8; - return Ok((1, 1, vec![1], num_swb)); - } - let num_swb = (short_window_offsets_family(family, fs_index)?.len() - 1) as u8; - Ok(derive_short_grouping(scale_factor_grouping, num_swb)) -} - -/// Shared `EIGHT_SHORT_SEQUENCE` §4.5.2.3.4 grouping walk. -fn derive_short_grouping(scale_factor_grouping: Option, num_swb: u8) -> (u8, u8, Vec, u8) { - // EIGHT_SHORT_SEQUENCE: scale_factor_grouping must be present - // per Table 4.6. derive_window_grouping treats a missing mask - // as the "no grouping" form (one group per window) so it - // remains a pure function; callers that go through - // IcsInfo::parse always supply the mask. - let mask = scale_factor_grouping.unwrap_or(0); - let mut groups: Vec = vec![1]; - for i in 0..7u32 { - // bit_set(mask, 6 - i) — most-right bit is bit 0. - let bit = (mask >> (6 - i as u8)) & 1; - if bit == 0 { - groups.push(1); - } else { - let last = groups.last_mut().expect("at least one group"); - *last += 1; - } - } - let num_window_groups = groups.len() as u8; - (8, num_window_groups, groups, num_swb) -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -fn read_u16(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 16); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u16) -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} diff --git a/crates/vendor/oxideav-aac/src/intensity_stereo.rs b/crates/vendor/oxideav-aac/src/intensity_stereo.rs deleted file mode 100644 index ed361aac..00000000 --- a/crates/vendor/oxideav-aac/src/intensity_stereo.rs +++ /dev/null @@ -1,621 +0,0 @@ -//! §4.6.8.2 Intensity Stereo (IS) decoding — ISO/IEC 14496-3. -//! -//! Intensity stereo is the second joint-channel tool of a channel -//! pair (the first being M/S, [`crate::ms_stereo`]). Where M/S -//! reconstructs both channels from a mid/side basis, intensity stereo -//! derives the **right** channel entirely from the **left** channel -//! by a single per-band real scale, exploiting the ear's reduced -//! sensitivity to phase at high frequencies. The left channel is -//! untouched. -//! -//! ## §4.6.8.2.3 decoding process -//! -//! Intensity stereo is signalled by the pseudo codebooks -//! `INTENSITY_HCB` (15, in-phase) and `INTENSITY_HCB2` (14, -//! out-of-phase) appearing in the **right** channel's `sfb_cb` (their -//! use in a left channel is illegal). For each intensity-coded band a -//! transmitted *intensity stereo position* `is_pos[g][sfb]` replaces -//! the right channel's scalefactor; the §4.6.8.2.3 reconstruction is -//! -//! ```text -//! is_intensity(g,sfb) = +1 if right sfb_cb == INTENSITY_HCB (15) -//! -1 if right sfb_cb == INTENSITY_HCB2 (14) -//! 0 otherwise -//! invert_intensity(g,sfb)= 1 - 2*ms_used[g][sfb] if ms_mask_present == 1 -//! (and aot != AAC scalable) -//! +1 otherwise -//! scale = is_intensity(g,sfb) * invert_intensity(g,sfb) -//! * 0.5^(0.25 * is_pos[g][sfb]); -//! for (i = 0; i < swb_offset[sfb+1]-swb_offset[sfb]; i++) -//! r_spec[g][b][sfb][i] = scale * l_spec[g][b][sfb][i]; -//! ``` -//! -//! The `0.5^(0.25·is_pos)` magnitude is the same per-quarter-step gain -//! ladder as the §4.6.2.3.3 scalefactor gain `2^(0.25·(sf−100))` (the -//! intensity position plays the role of a scalefactor difference); the -//! `is_intensity` factor carries the in/out-of-phase sign of the -//! codebook and `invert_intensity` flips it when the band's `ms_used` -//! bit is set under a per-band M/S mask (`ms_mask_present == 1`). This -//! is a deterministic algebraic reconstruction — no rounding tables and -//! no RNG — so an intensity-coded band comes out byte-exact. -//! -//! ## Mutual exclusion (§4.6.8.1.3 note / §4.6.8.2.3 / §4.6.13.3) -//! -//! M/S, intensity stereo, and PNS are mutually exclusive on any one -//! `(group, sfb)`. This tool only ever rewrites the right channel of a -//! band whose **right** `sfb_cb` is an intensity book; M/S already -//! skips those bands (it consults the right channel's intensity status -//! via `is_intensity`). A band that is `NOISE_HCB` cannot also be an -//! intensity book, so no extra noise guard is needed here — the -//! `is_intensity` predicate is `0` for every non-intensity codebook -//! and the band is left as the inverse-quantised passthrough. -//! -//! ## Decoder block order -//! -//! Per §4.6 the channel-pair / noise tools run inverse-quant → M/S → -//! PNS → intensity → TNS on the **de-interleaved, window-major** -//! spectrum produced by [`crate::decoded_spectrum::quant_to_spec`] -//! (`spec[w * window_len + k]`), so this pass runs after -//! [`crate::ms_stereo::apply_ms_stereo`] and before the §4.6.9 TNS -//! filter. The per-band coefficient extent -//! `swb_offset[sfb+1]-swb_offset[sfb]` and the -//! `(group, in-group window) → absolute window` mapping match the rest -//! of the pipeline. -//! -//! ## Scope -//! -//! This module is the intensity-stereo / left-to-right derivation only. -//! The dependently-switched coupling channel contribution of the -//! "intensity stereo / coupling" tool (§4.6.8.2.1, fed by a CCE) and -//! PNS synthesis (§4.6.13) are separate follow-ups. The -//! `is_position[g][sfb]` track itself is produced upstream by the -//! §4.6.8.1.4 DPCM accumulator -//! ([`crate::scale_factor_data::accumulate`]). - -use crate::ics_info::IcsInfo; -use crate::section_data::{INTENSITY_HCB, INTENSITY_HCB2}; -#[cfg(test)] -use crate::swb_offset::{ - long_window_offsets, short_window_offsets, LONG_WINDOW_LEN, SHORT_WINDOW_LEN, -}; -use crate::{Error, Result}; - -/// §4.6.8.2.3 `is_intensity(g,sfb)` — the in/out-of-phase sign of an -/// intensity band, keyed on the **right** channel codebook. -/// -/// `+1` for `INTENSITY_HCB` (15, in-phase), `-1` for `INTENSITY_HCB2` -/// (14, out-of-phase), `0` for any non-intensity codebook (the band is -/// not intensity-coded and is left untouched). -pub fn is_intensity(right_cb: u8) -> i32 { - match right_cb { - INTENSITY_HCB => 1, - INTENSITY_HCB2 => -1, - _ => 0, - } -} - -/// §4.6.8.2.3 `invert_intensity(g,sfb)` — the phase-reversal factor. -/// -/// Returns `1 - 2*ms_used` (i.e. `+1` when `ms_used == false`, `-1` -/// when `true`) under a per-band M/S mask (`ms_mask_present == 1`) for -/// a non-scalable GA decoder; `+1` otherwise. Because M/S and -/// intensity are mutually exclusive on a band, a set `ms_used` bit on -/// an intensity band carries the §4.6.8.2.3 phase reversal rather than -/// an M/S de-matrix. -pub fn invert_intensity(per_band_mask: bool, ms_used: bool) -> i32 { - if per_band_mask { - 1 - 2 * (ms_used as i32) - } else { - 1 - } -} - -/// §4.6.8.2.3 `0.5^(0.25 * is_pos)` — the intensity-position gain. -/// -/// The same per-quarter-step ladder as the §4.6.2.3.3 scalefactor gain -/// but on a base of `1/2`; a larger position attenuates the derived -/// right channel. -pub fn intensity_gain(is_pos: i32) -> f64 { - 0.5f64.powf(0.25 * is_pos as f64) -} - -/// A channel pair's de-interleaved spectra plus the right-channel -/// codebooks and intensity positions the §4.6.8.2.3 derivation needs. -/// -/// `left` / `right` are the window-major decoded spectra -/// (`num_windows × window_len`) produced by -/// [`crate::decoded_spectrum::quant_to_spec`]. `left` is read only; -/// for every intensity-coded band `right` is overwritten with -/// `scale · left`. Bands whose right `sfb_cb` is not an intensity book -/// are left exactly as they arrive (the inverse-quantised passthrough). -/// -/// `right_sfb_cb` is the right channel's `sfb_cb[g][sfb]` (from its -/// [`crate::section_data::SectionData`]); it both selects which bands -/// are intensity-coded and supplies the in/out-of-phase sign. -/// -/// `is_pos` is the right channel's absolute `is_pos[g][sfb]` track -/// (§4.6.8.1.4, from [`crate::scale_factor_data::accumulate`]). Only -/// the entries at intensity-coded `(g, sfb)` are consulted. -#[derive(Debug)] -pub struct IntensityPairSpectra<'a> { - /// First ("left") channel spectrum — read only. - pub left: &'a [f64], - /// Second ("right") channel spectrum — derived from `left` on - /// intensity bands, untouched elsewhere. - pub right: &'a mut [f64], - /// Right channel `sfb_cb[g][sfb]`. - pub right_sfb_cb: &'a [Vec], - /// Right channel absolute `is_pos[g][sfb]` (§4.6.8.1.4). - pub is_pos: &'a [Vec], -} - -/// Apply the §4.6.8.2.3 intensity-stereo left→right derivation in place. -/// -/// * `pair` — the channel-pair spectra, right-channel codebooks, and -/// intensity positions ([`IntensityPairSpectra`]). -/// * `ms_mask_present` — `true` ⇔ the CPE carries a per-band `ms_used` -/// mask (`ms_mask_present == 1`); selects the §4.6.8.2.3 -/// `invert_intensity` phase-reversal branch. -/// * `ms_used` — `ms_used[g][sfb]` (one row per window group, each at -/// least `max_sfb` long). Consulted only when `ms_mask_present` is -/// `true`; pass an empty slice otherwise. -/// * `ics_info` — the shared `common_window` `ics_info()`; supplies -/// `num_window_groups`, `window_group_length`, `max_sfb`, and the -/// window geometry. -/// * `fs_index` — `samplingFrequencyIndex`, selecting the `swb_offset` -/// table. -/// -/// Returns [`Error::IntensityStereoInvalid`] if the buffer / mask / -/// `sfb_cb` / `is_pos` shapes disagree with `ics_info` (see the variant -/// docs). When no band is intensity-coded the right buffer is left -/// untouched. -pub fn apply_intensity_stereo( - pair: &mut IntensityPairSpectra<'_>, - ms_mask_present: bool, - ms_used: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, -) -> Result<()> { - let IntensityPairSpectra { - left, - right, - right_sfb_cb, - is_pos, - } = pair; - - let window_len = ics_info.window_len()?; - let offsets = ics_info.swb_offsets(fs_index)?; - let num_swb = offsets.len() - 1; - let num_windows = ics_info.num_windows as usize; - let num_groups = ics_info.num_window_groups as usize; - let max_sfb = ics_info.max_sfb as usize; - - // Geometry consistency: both channels share the common_window - // ics_info, so both spectra are num_windows × window_len. - let expected = num_windows * window_len; - if left.len() != expected || right.len() != expected { - return Err(Error::IntensityStereoInvalid); - } - if ics_info.window_group_length.len() != num_groups - || ics_info - .window_group_length - .iter() - .map(|&w| w as usize) - .sum::() - != num_windows - { - return Err(Error::IntensityStereoInvalid); - } - // max_sfb must not exceed the band count of the active window. - if max_sfb > num_swb { - return Err(Error::IntensityStereoInvalid); - } - // The right channel drives both the intensity predicate and the - // is_pos lookup, so both tables must cover every (g, sfb). - if right_sfb_cb.len() != num_groups || is_pos.len() != num_groups { - return Err(Error::IntensityStereoInvalid); - } - for g in 0..num_groups { - if right_sfb_cb[g].len() < max_sfb || is_pos[g].len() < max_sfb { - return Err(Error::IntensityStereoInvalid); - } - } - // A per-band mask needs a full ms_used[g][sfb]; otherwise it is - // ignored. - if ms_mask_present { - if ms_used.len() != num_groups { - return Err(Error::IntensityStereoInvalid); - } - for row in ms_used { - if row.len() < max_sfb { - return Err(Error::IntensityStereoInvalid); - } - } - } - - let mut window_base = 0usize; - for g in 0..num_groups { - let wgl = ics_info.window_group_length[g] as usize; - for sfb in 0..max_sfb { - let sign = is_intensity(right_sfb_cb[g][sfb]); - if sign == 0 { - // Not an intensity band: leave the right channel as the - // inverse-quantised / M/S-reconstructed passthrough. - continue; - } - let used = ms_mask_present && ms_used[g][sfb]; - let inv = invert_intensity(ms_mask_present, used); - let scale = sign as f64 * inv as f64 * intensity_gain(is_pos[g][sfb]); - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - for b in 0..wgl { - let base = (window_base + b) * window_len; - for i in start..end { - right[base + i] = scale * left[base + i]; - } - } - } - window_base += wgl; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - use crate::section_data::{NOISE_HCB, ZERO_HCB}; - - const FS_44100: u8 = 4; - const SPECTRUM_CB: u8 = 2; - - fn long_ics_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[FS_44100 as usize], - } - } - - fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { - let num_window_groups = window_group_length.len() as u8; - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups, - window_group_length, - num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[FS_44100 as usize], - } - } - - fn plain_cb(num_groups: usize, max_sfb: usize) -> Vec> { - vec![vec![SPECTRUM_CB; max_sfb]; num_groups] - } - - fn zero_pos(num_groups: usize, max_sfb: usize) -> Vec> { - vec![vec![0i32; max_sfb]; num_groups] - } - - #[allow(clippy::too_many_arguments)] - fn run( - left: &[f64], - right: &mut [f64], - ms_mask_present: bool, - ms_used: &[Vec], - right_sfb_cb: &[Vec], - is_pos: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, - ) -> Result<()> { - let mut pair = IntensityPairSpectra { - left, - right, - right_sfb_cb, - is_pos, - }; - apply_intensity_stereo(&mut pair, ms_mask_present, ms_used, ics_info, fs_index) - } - - #[test] - fn is_intensity_sign() { - assert_eq!(is_intensity(INTENSITY_HCB), 1); - assert_eq!(is_intensity(INTENSITY_HCB2), -1); - assert_eq!(is_intensity(SPECTRUM_CB), 0); - assert_eq!(is_intensity(NOISE_HCB), 0); - assert_eq!(is_intensity(ZERO_HCB), 0); - } - - #[test] - fn invert_intensity_branches() { - // No per-band mask: always +1 regardless of ms_used. - assert_eq!(invert_intensity(false, false), 1); - assert_eq!(invert_intensity(false, true), 1); - // Per-band mask: 1 - 2*ms_used. - assert_eq!(invert_intensity(true, false), 1); - assert_eq!(invert_intensity(true, true), -1); - } - - #[test] - fn intensity_gain_quarter_ladder() { - // 0.5^0 = 1. - assert!((intensity_gain(0) - 1.0).abs() < 1e-12); - // 0.5^(0.25*4) = 0.5^1 = 0.5. - assert!((intensity_gain(4) - 0.5).abs() < 1e-12); - // 0.5^(0.25*8) = 0.25. - assert!((intensity_gain(8) - 0.25).abs() < 1e-12); - // negative position amplifies: 0.5^(-1) = 2. - assert!((intensity_gain(-4) - 2.0).abs() < 1e-12); - } - - #[test] - fn in_phase_pos_zero_copies_left() { - // INTENSITY_HCB, is_pos = 0, no mask → scale = +1: right == left. - let info = long_ics_info(3); - let max_sfb = 3; - let off = long_window_offsets(FS_44100).unwrap(); - let n = LONG_WINDOW_LEN as usize; - let mut left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - for (k, (l, r)) in left.iter_mut().zip(right.iter_mut()).enumerate() { - if k >= off[0] as usize && k < off[3] as usize { - *l = (k as f64) * 0.5 - 3.0; - *r = 999.0; // garbage to be overwritten - } - } - let mut cb = plain_cb(1, max_sfb); - cb[0][1] = INTENSITY_HCB; // make sfb 1 intensity-coded - let pos = zero_pos(1, max_sfb); - - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - - // sfb 1 derived from left; sfb 0 and 2 untouched (still garbage). - for (r, l) in right - .iter() - .zip(left.iter()) - .take(off[2] as usize) - .skip(off[1] as usize) - { - assert!((r - l).abs() < 1e-12); - } - for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { - assert_eq!(r, 999.0); - } - for &r in right.iter().take(off[3] as usize).skip(off[2] as usize) { - assert_eq!(r, 999.0); - } - } - - #[test] - fn out_of_phase_negates() { - // INTENSITY_HCB2 (sign -1), is_pos = 0, no mask → scale = -1. - let info = long_ics_info(2); - let off = long_window_offsets(FS_44100).unwrap(); - let n = LONG_WINDOW_LEN as usize; - let mut left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { - *l = 7.0; - } - let mut cb = plain_cb(1, 2); - cb[0][0] = INTENSITY_HCB2; - let pos = zero_pos(1, 2); - - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - - for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { - assert!((r + 7.0).abs() < 1e-12); - } - } - - #[test] - fn position_scales_gain() { - // is_pos = 4 → gain 0.5; in-phase, no mask → right = 0.5 * left. - let info = long_ics_info(1); - let off = long_window_offsets(FS_44100).unwrap(); - let n = LONG_WINDOW_LEN as usize; - let mut left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { - *l = 16.0; - } - let mut cb = plain_cb(1, 1); - cb[0][0] = INTENSITY_HCB; - let mut pos = zero_pos(1, 1); - pos[0][0] = 4; - - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - - for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { - assert!((r - 8.0).abs() < 1e-12); - } - } - - #[test] - fn ms_used_inverts_phase_under_mask() { - // INTENSITY_HCB (sign +1) with a set ms_used bit under a - // per-band mask flips to -1: right = -left. - let info = long_ics_info(1); - let off = long_window_offsets(FS_44100).unwrap(); - let n = LONG_WINDOW_LEN as usize; - let mut left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { - *l = 3.0; - } - let mut cb = plain_cb(1, 1); - cb[0][0] = INTENSITY_HCB; - let pos = zero_pos(1, 1); - let ms_used = vec![vec![true]]; - - run( - &left, &mut right, true, &ms_used, &cb, &pos, &info, FS_44100, - ) - .unwrap(); - - for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { - assert!((r + 3.0).abs() < 1e-12); - } - } - - #[test] - fn ms_used_ignored_without_mask() { - // Same set ms_used bit but ms_mask_present == false → +1 (the - // mask is not consulted; invert_intensity is +1). - let info = long_ics_info(1); - let off = long_window_offsets(FS_44100).unwrap(); - let n = LONG_WINDOW_LEN as usize; - let mut left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - for l in left.iter_mut().take(off[1] as usize).skip(off[0] as usize) { - *l = 3.0; - } - let mut cb = plain_cb(1, 1); - cb[0][0] = INTENSITY_HCB; - let pos = zero_pos(1, 1); - - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - - for &r in right.iter().take(off[1] as usize).skip(off[0] as usize) { - assert!((r - 3.0).abs() < 1e-12); - } - } - - #[test] - fn non_intensity_bands_untouched() { - // No intensity codebook anywhere → right is left exactly as-is. - let info = long_ics_info(4); - let n = LONG_WINDOW_LEN as usize; - let left = vec![1.0f64; n]; - let mut right = vec![42.0f64; n]; - let cb = plain_cb(1, 4); // all spectrum books - let pos = zero_pos(1, 4); - - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - - assert!(right.iter().all(|&v| v == 42.0)); - } - - #[test] - fn short_window_grouping() { - // Two groups of 4 + 4 short windows; intensity on sfb 0 of - // group 1. Every window of that group derives right from left. - let wgl = vec![4u8, 4u8]; - let info = short_ics_info(2, wgl.clone()); - let off = short_window_offsets(FS_44100).unwrap(); - let wlen = SHORT_WINDOW_LEN as usize; - let n = 8 * wlen; - let mut left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - for w in 0..8 { - for k in (off[0] as usize)..(off[1] as usize) { - left[w * wlen + k] = (w as f64) + 1.0; - } - } - let mut cb = plain_cb(2, 2); - cb[1][0] = INTENSITY_HCB; // group 1 sfb 0 in-phase - let pos = zero_pos(2, 2); - - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - - // Group 0 (windows 0..4) untouched, group 1 (windows 4..8) - // derived as right = left (in-phase, pos 0). - for w in 0..4 { - for k in (off[0] as usize)..(off[1] as usize) { - assert_eq!(right[w * wlen + k], 0.0); - } - } - for w in 4..8 { - for k in (off[0] as usize)..(off[1] as usize) { - assert!((right[w * wlen + k] - left[w * wlen + k]).abs() < 1e-12); - } - } - } - - #[test] - fn rejects_length_mismatch() { - let info = long_ics_info(1); - let left = vec![0.0f64; 10]; - let mut right = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let cb = plain_cb(1, 1); - let pos = zero_pos(1, 1); - let e = run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100); - assert_eq!(e, Err(Error::IntensityStereoInvalid)); - } - - #[test] - fn rejects_short_is_pos() { - let info = long_ics_info(3); - let n = LONG_WINDOW_LEN as usize; - let left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - let cb = plain_cb(1, 3); - let pos = zero_pos(1, 2); // too short - let e = run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100); - assert_eq!(e, Err(Error::IntensityStereoInvalid)); - } - - #[test] - fn rejects_missing_ms_used_under_mask() { - let info = long_ics_info(1); - let n = LONG_WINDOW_LEN as usize; - let left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - let cb = plain_cb(1, 1); - let pos = zero_pos(1, 1); - // ms_mask_present true but ms_used empty → reject. - let e = run(&left, &mut right, true, &[], &cb, &pos, &info, FS_44100); - assert_eq!(e, Err(Error::IntensityStereoInvalid)); - } - - #[test] - fn rejects_max_sfb_over_num_swb() { - let mut info = long_ics_info(1); - info.max_sfb = 99; // exceeds long-window band count - let n = LONG_WINDOW_LEN as usize; - let left = vec![0.0f64; n]; - let mut right = vec![0.0f64; n]; - let cb = vec![vec![SPECTRUM_CB; 99]]; - let pos = vec![vec![0i32; 99]]; - let e = run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100); - assert_eq!(e, Err(Error::IntensityStereoInvalid)); - } - - #[test] - fn no_intensity_band_leaves_right_untouched() { - // Empty/degenerate: max_sfb 0 → nothing to scan. - let mut info = long_ics_info(0); - info.max_sfb = 0; - let n = LONG_WINDOW_LEN as usize; - let left = vec![1.0f64; n]; - let mut right = vec![5.0f64; n]; - let cb: Vec> = vec![vec![]]; - let pos: Vec> = vec![vec![]]; - run(&left, &mut right, false, &[], &cb, &pos, &info, FS_44100).unwrap(); - assert!(right.iter().all(|&v| v == 5.0)); - } -} diff --git a/crates/vendor/oxideav-aac/src/ipqf.rs b/crates/vendor/oxideav-aac/src/ipqf.rs deleted file mode 100644 index 5ce71771..00000000 --- a/crates/vendor/oxideav-aac/src/ipqf.rs +++ /dev/null @@ -1,298 +0,0 @@ -//! IPQF — the SSR inverse polyphase quadrature filter (ISO/IEC -//! 14496-3 §4.6.12.3.4). -//! -//! The IPQF is the final stage of the SSR (AOT 3) gain-control tool: it -//! recombines the four per-band gain-controlled sample streams `V_B` -//! (produced by [`crate::gain_control::GainBandState::window_overlap`]) -//! into a single full-rate PCM time signal `AS(n)`, cancelling the -//! aliasing the encoder's PQF analysis introduced. -//! -//! ## Synthesis filter (§4.6.12.3.4) -//! -//! The four bands are interpolated 4× (one band sample every fourth -//! output sample) and cosine-modulated through a length-96 prototype -//! filter: -//! -//! ```text -//! Ṽ_B(j) = V_B(k) if j == 4k, else 0 (4× upsample) -//! -//! Q_B(j) = Q(j) · cos( (2B+1)(2j−3)π / 16 ), 0 ≤ j ≤ 95 -//! -//! AS(n) = Σ_{B=0}^{3} Σ_{j=0}^{95} Q_B(j) · Ṽ_B(n − j) -//! ``` -//! -//! The length-96 prototype `Q(j)` is symmetric: `Q(0..=47)` are the -//! Table 4.110 values; `Q(48..=95)` mirror them as `Q(j) = Q(95 − j)`. -//! -//! Because the `Ṽ_B` interpolation places a band sample only at the -//! multiples of four, the inner sum over `j` touches band `B`'s history -//! at the strided positions `j ≡ n (mod 4)`. The synthesizer is run as -//! a streaming polyphase bank: it keeps a 96-tap (24 band-sample) ring -//! of recent `V_B` history per band so each call produces the next -//! block of `AS(n)` from the new band samples and the carried tail. -//! -//! ## Provenance -//! -//! The prototype coefficients are Table 4.110 of ISO/IEC 14496-3:2001 -//! (a numeric data table) staged under `docs/audio/aac/`; the -//! modulation and upsampling equations are the §4.6.12.3.4 normative -//! formulas. No external SSR / PQF implementation was consulted. - -use core::f64::consts::PI; - -/// The number of IPQF bands (§4.6.12.1): four uniform frequency bands. -pub const NUM_BANDS: usize = 4; - -/// The prototype filter length (§4.6.12.3.4): `Q(0..=95)`. -pub const PROTO_LEN: usize = 96; - -/// `Q(0)..=Q(47)` — the first half of the §4.6.12.3.4 prototype filter, -/// ISO/IEC 14496-3 Table 4.110. The second half `Q(48)..=Q(95)` is the -/// mirror `Q(j) = Q(95 − j)` (see [`prototype`]). -/// -/// The literals are the f64-exact shortest round-trip forms of the -/// Table 4.110 decimal values (the table prints ~17 significant -/// digits, more than an `f64` can distinguish; these are the canonical -/// shortest forms that decode to the identical bit pattern). -pub const Q_HALF: [f64; 48] = [ - 9.765529100757551e-5, - 1.3809589379038567e-4, - 9.840074925662353e-5, - -8.667154478233572e-5, - -4.6217998911921346e-4, - -1.0211814095158174e-3, - -1.6772149340010668e-3, - -2.253333895141108e-3, - -2.4987888343213967e-3, - -2.139081596676188e-3, - -9.559539745459777e-4, - 1.1172111530118943e-3, - 3.909130912734858e-3, - 6.963570342011867e-3, - 9.559544215947834e-3, - 1.081576654002136e-2, - 9.87705149917153e-3, - 6.156256729132736e-3, - -4.179394606362971e-4, - -9.212874309770764e-3, - -1.883077587336902e-2, - -2.7226498457701823e-2, - -3.2022840857588906e-2, - -3.099633252775461e-2, - -2.2656858741499447e-2, - -6.803111385896335e-3, - 1.5085400948280744e-2, - 3.975099338827274e-2, - 6.244536362943674e-2, - 7.762232774872133e-2, - 7.996833849613293e-2, - 6.561549306847558e-2, - 3.331365830088269e-2, - -1.4691563058190206e-2, - -7.230789047533415e-2, - -1.2993222541703875e-1, - -1.7551641029040532e-1, - -1.9626543957670528e-1, - -1.807333067021503e-1, - -1.2097653136035738e-1, - -1.4377370758549035e-2, - 1.3522730742860303e-1, - 3.1737852699301633e-1, - 5.159002179848223e-1, - 7.108002037976138e-1, - 8.80906324884448e-1, - 1.0068321641150089e0, - 1.0737914947736096e0, -]; - -/// The full length-96 prototype filter `Q(0..=95)` (§4.6.12.3.4): the -/// [`Q_HALF`] first half plus its mirror `Q(j) = Q(95 − j)`. -#[must_use] -pub fn prototype() -> [f64; PROTO_LEN] { - let mut q = [0.0f64; PROTO_LEN]; - q[..48].copy_from_slice(&Q_HALF); - for j in 48..PROTO_LEN { - q[j] = Q_HALF[95 - j]; - } - q -} - -/// The §4.6.12.3.4 synthesis-filter coefficient -/// `Q_B(j) = Q(j) · cos((2B+1)(2j−3)π/16)` for band `b`, tap `j`. -#[must_use] -fn synthesis_coef(q: &[f64; PROTO_LEN], b: usize, j: usize) -> f64 { - let angle = (2.0 * b as f64 + 1.0) * (2.0 * j as f64 - 3.0) * PI / 16.0; - q[j] * angle.cos() -} - -/// Streaming IPQF synthesizer: holds the per-band `V_B` history needed -/// to evaluate the length-96 `AS(n)` convolution across frame -/// boundaries. -/// -/// The interpolation `Ṽ_B(j) = V_B(j/4)` means tap `j` of the -/// convolution reads band sample `(n − j)/4` for `j ≡ n (mod 4)`. The -/// prototype spans `j ∈ 0..=95`, so the bank needs the last -/// `ceil(96/4) = 24` band samples per band; a 24-deep ring per band is -/// retained between [`Ipqf::synthesize`] calls. -#[derive(Debug, Clone)] -pub struct Ipqf { - /// The precomputed `Q_B(j)` matrix, `[band][tap]`. - coefs: [[f64; PROTO_LEN]; NUM_BANDS], - /// Per-band history of the most recent band samples, newest last. - /// Holds at least [`HISTORY`] entries once primed. - history: [Vec; NUM_BANDS], -} - -/// The number of past band samples the length-96 prototype reaches: -/// `ceil(PROTO_LEN / NUM_BANDS) = 24`. -const HISTORY: usize = PROTO_LEN.div_ceil(NUM_BANDS); - -impl Default for Ipqf { - fn default() -> Self { - Self::new() - } -} - -impl Ipqf { - /// A fresh synthesizer with the prototype-derived coefficients and - /// zero-initialised history (the spec's implicit pre-stream - /// silence). - #[must_use] - pub fn new() -> Self { - let q = prototype(); - let mut coefs = [[0.0f64; PROTO_LEN]; NUM_BANDS]; - for (b, band) in coefs.iter_mut().enumerate() { - for (j, slot) in band.iter_mut().enumerate() { - *slot = synthesis_coef(&q, b, j); - } - } - let history = core::array::from_fn(|_| vec![0.0f64; HISTORY]); - Ipqf { coefs, history } - } - - /// Synthesize `AS(n)` for `len` band-sample steps from the four - /// per-band input streams `bands[B]`. - /// - /// Each `bands[B]` supplies the next `len` band samples `V_B`. The - /// returned vector holds `NUM_BANDS · len` output samples — the - /// IPQF interpolates each band sample to four full-rate positions, - /// so `len` band steps produce `4·len` PCM samples. - /// - /// The convolution `AS(n) = Σ_B Σ_j Q_B(j)·Ṽ_B(n − j)` is evaluated - /// at every output position `n`, reading band `B`'s history at the - /// strided positions; the per-band history rings advance one band - /// sample per step. - /// - /// # Panics - /// - /// Panics if any `bands[B]` has fewer than `len` samples. - #[must_use] - pub fn synthesize(&mut self, bands: &[&[f64]; NUM_BANDS], len: usize) -> Vec { - let mut out = Vec::with_capacity(NUM_BANDS * len); - for step in 0..len { - // Push the new band sample of each band onto its ring. - for (hist, band) in self.history.iter_mut().zip(bands.iter()) { - hist.push(band[step]); - } - // For this band step we emit NUM_BANDS output samples - // n = 4·step + p, p = 0..NUM_BANDS. Output position n reads - // Ṽ_B(n − j): non-zero only when (n − j) ≡ 0 (mod 4), i.e. - // the band sample index is (n − j)/4. The newest band sample - // sits at history end (index L−1) and is the p-aligned - // phase, so tap j = 4·t + p reads the t-th most recent - // band sample. - for p in 0..NUM_BANDS { - let mut acc = 0.0f64; - for (coefs, hist) in self.coefs.iter().zip(self.history.iter()) { - let l = hist.len(); - let mut j = p; - while j < PROTO_LEN { - let t = j / NUM_BANDS; // how many band samples back - if t < l { - acc += coefs[j] * hist[l - 1 - t]; - } - j += NUM_BANDS; - } - } - out.push(acc); - } - // Trim the rings to the needed depth to bound memory. - for hist in &mut self.history { - let l = hist.len(); - if l > HISTORY { - hist.drain(0..l - HISTORY); - } - } - } - out - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn prototype_is_symmetric() { - let q = prototype(); - for j in 0..PROTO_LEN { - assert!((q[j] - q[95 - j]).abs() < 1e-15, "Q({j}) != Q({})", 95 - j); - } - // Spot-check the documented endpoints. - assert!((q[0] - 9.765529100757551e-5).abs() < 1e-18); - assert!((q[47] - 1.0737914947736096e0).abs() < 1e-15); - assert!((q[48] - 1.0737914947736096e0).abs() < 1e-15); - assert!((q[95] - 9.765529100757551e-5).abs() < 1e-18); - } - - #[test] - fn silence_produces_silence() { - let mut ipqf = Ipqf::new(); - let z = vec![0.0f64; 16]; - let bands: [&[f64]; NUM_BANDS] = [&z, &z, &z, &z]; - let out = ipqf.synthesize(&bands, 16); - assert_eq!(out.len(), NUM_BANDS * 16); - assert!(out.iter().all(|&x| x == 0.0)); - } - - #[test] - fn output_length_is_four_times_band_steps() { - let mut ipqf = Ipqf::new(); - let s: Vec = (0..10).map(|i| i as f64).collect(); - let bands: [&[f64]; NUM_BANDS] = [&s, &s, &s, &s]; - let out = ipqf.synthesize(&bands, 10); - assert_eq!(out.len(), 40); - assert!(out.iter().all(|x| x.is_finite())); - } - - #[test] - fn synthesis_coef_first_band_zero_tap() { - // Q_0(0) = Q(0)·cos((1)(−3)π/16). - let q = prototype(); - let expect = q[0] * ((-3.0) * PI / 16.0).cos(); - assert!((synthesis_coef(&q, 0, 0) - expect).abs() < 1e-15); - } - - #[test] - fn impulse_response_matches_direct_convolution() { - // Feed an impulse into band 0 and verify the streamed output - // equals the direct §4.6.12.3.4 convolution for the first - // several output samples: AS(n) = Σ_j Q_0(j)·Ṽ_0(n−j), with - // Ṽ_0(0)=1 (impulse) and 0 elsewhere ⇒ AS(n) = Q_0(n). - let q = prototype(); - let mut ipqf = Ipqf::new(); - let mut b0 = vec![0.0f64; 30]; - b0[0] = 1.0; // first band sample = 1 ⇒ Ṽ_0(0)=1. - let z = vec![0.0f64; 30]; - let bands: [&[f64]; NUM_BANDS] = [&b0, &z, &z, &z]; - let out = ipqf.synthesize(&bands, 30); - // AS(n) for n = 0..PROTO_LEN should equal Q_0(n). - for (n, &got) in out.iter().take(PROTO_LEN).enumerate() { - let expect = synthesis_coef(&q, 0, n); - assert!( - (got - expect).abs() < 1e-12, - "AS({n}) = {got} != Q_0({n}) = {expect}" - ); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/latm.rs b/crates/vendor/oxideav-aac/src/latm.rs deleted file mode 100644 index fbae51bb..00000000 --- a/crates/vendor/oxideav-aac/src/latm.rs +++ /dev/null @@ -1,1902 +0,0 @@ -//! LATM / LOAS transport framing — ISO/IEC 14496-3 §1.7. -//! -//! LATM (Low-overhead MPEG-4 Audio Transport Multiplex) is the -//! multiplex layer that packs one or more MPEG-4 Audio payloads plus -//! their [`AudioSpecificConfig`] (ASC) into a single multiplexed -//! element ([`AudioMuxElement`], §1.7.3.1 Table 1.41). LOAS -//! (Low Overhead Audio Stream) is the synchronization layer above it -//! ([`AudioSyncStream`], §1.7.2.1 Table 1.36), which prefixes each -//! multiplexed element with a `0x2B7` syncword and a 13-bit byte -//! length so the multiplex can be recovered from a transmission -//! channel that carries no framing of its own. -//! -//! This module decodes the transport structure end to end for the AAC -//! case — the configuration ([`StreamMuxConfig`], Table 1.42), the -//! per-subframe payload lengths ([`PayloadLengthInfo`], Table 1.44), -//! and the multiplexed AAC access units ([`PayloadMux`], Table 1.45) — -//! and hands the recovered raw-data-block byte slices to the -//! [`crate::decode::StreamDecoder`] / [`crate::raw_data_block`] layer. -//! -//! ## Scope -//! -//! The decode path supports the configurations that carry AAC: -//! `audioMuxVersion ∈ {0, 1}` (the `audioMuxVersion == 1` -//! `taraBufferFullness` / per-ASC length-prefix extensions are parsed), -//! `allStreamsSameTimeFraming` in both states, and the per-layer -//! `frameLengthType` values `0` (variable-length, byte count carried -//! in `PayloadLengthInfo()`) and `1` (fixed `frameLength` bits in -//! `StreamMuxConfig()`). The CELP (`3`/`4`/`5`) and HVXC (`6`/`7`) -//! frame-length-table-indexed types are surfaced as -//! [`Error::LatmUnsupportedFrameLengthType`] — they index frame-length -//! tables for object types this AAC-focused crate does not decode. The -//! `audioMuxVersionA == 1` reserved branch is -//! [`Error::LatmAudioMuxVersionAReserved`]. The `EPMuxElement()` -//! error-protected variant (Table 1.40) and the -//! `EPAudioSyncStream()` FEC header (Table 1.37) are parsed at the -//! framing level but the EP-tool payload de-interleave is out of -//! scope. - -use crate::asc::AudioSpecificConfig; -use crate::crc; -use crate::{Error, Result}; -use oxideav_core::bits::BitReader; - -/// §1.7.2.1 Table 1.36 `AudioSyncStream()` syncword (`0x2B7`, 11 bits). -pub const AUDIO_SYNC_STREAM_SYNCWORD: u32 = 0x2B7; - -/// §1.7.2.1 Table 1.37 `EPAudioSyncStream()` syncword (`0x4DE1`, -/// 16 bits). -pub const EP_AUDIO_SYNC_STREAM_SYNCWORD: u32 = 0x4DE1; - -/// §1.7.2.2.1: "The maximum byte-distance between two syncwords is -/// 8192 bytes", encoded in the 13-bit `audioMuxLengthBytes` field. -pub const MAX_AUDIO_MUX_LENGTH_BYTES: u32 = (1 << 13) - 1; - -/// §1.7.3 signalling caps: `numProgram` is 4-bit (max program index -/// 15), `numLayer` is 3-bit (max layer index 7), `streamIndx` is -/// 4-bit (max 15 streams), `numChunk` is 4-bit. -const MAX_PROGRAM_INDEX: u32 = 15; -const MAX_LAYER_INDEX: u32 = 7; -const MAX_STREAM_COUNT: usize = 16; - -/// One decoded scalable layer of a [`StreamMuxConfig`] program. -/// -/// Mirrors the per-`streamID[prog][lay]` state the Table 1.42 loop -/// builds: the parsed [`AudioSpecificConfig`] (or `None` when -/// `useSameConfig` pointed at an earlier layer's config), the -/// `frameLengthType`, and the framing parameter that type selects -/// (`latmBufferFullness` for type 0, `frameLength` bits for type 1). -#[derive(Debug, Clone)] -pub struct LayerConfig { - /// `progSIndx` — the program this layer belongs to. - pub prog: u8, - /// `laySIndx` — the layer index within the program. - pub lay: u8, - /// `streamID[prog][lay]` — the flat stream counter assigned in - /// transmission order. - pub stream_id: u8, - /// The layer's [`AudioSpecificConfig`]. `None` ⇔ `useSameConfig` - /// was set, meaning "apply the ASC most recently transmitted in a - /// previous layer or program" (§1.7.3.2.3). [`StreamMuxConfig`] - /// resolves this into [`LayerConfig::effective_asc`] on parse, so - /// callers always have a concrete config there. - pub asc: Option, - /// The effective ASC after resolving `useSameConfig` back to the - /// most recently transmitted config. Always populated. - pub effective_asc: AudioSpecificConfig, - /// `frameLengthType[streamID]` (§1.7.3.1 Table 1.42). - pub frame_length_type: u8, - /// `latmBufferFullness[streamID]` — present (8-bit) only for - /// `frameLengthType == 0`. - pub latm_buffer_fullness: Option, - /// `coreFrameOffset` — present (6-bit) only for - /// `frameLengthType == 0`, `!allStreamsSameTimeFraming`, and a - /// CELP-core / AAC-enhancement layer pairing. - pub core_frame_offset: Option, - /// `frameLength[streamID]` — present (9-bit) only for - /// `frameLengthType == 1`. The fixed payload length is - /// `(frameLength + 20) * 8` bits per §1.7.3.2.3. - pub frame_length: Option, -} - -impl LayerConfig { - /// §1.7.3.2.3: for `frameLengthType == 1` the fixed payload bit - /// length is `(frameLength + 20) * 8`. Returns `None` for every - /// other frame-length type (their length is carried in - /// `PayloadLengthInfo()` or is table-indexed). - pub fn fixed_payload_bits(&self) -> Option { - if self.frame_length_type == 1 { - self.frame_length - .map(|fl| (u32::from(fl) + 20).saturating_mul(8)) - } else { - None - } - } -} - -/// Decoded `StreamMuxConfig()` — ISO/IEC 14496-3 §1.7.3.1 Table 1.42. -/// -/// Carries the whole multiplex configuration: the version flags, the -/// time-framing mode, the per-program / per-layer [`LayerConfig`] -/// table, the `otherData` length, and the optional `crcCheckSum`. -#[derive(Debug, Clone)] -pub struct StreamMuxConfig { - /// `audioMuxVersion` (1 bit). - pub audio_mux_version: u8, - /// `audioMuxVersionA` (1 bit; `0` unless `audioMuxVersion == 1` - /// signalled it). A `1` here is the reserved `/* tbd */` branch, - /// rejected on parse. - pub audio_mux_version_a: u8, - /// `taraBufferFullness` — present only for `audioMuxVersion == 1`. - pub tara_buffer_fullness: Option, - /// `allStreamsSameTimeFraming` (1 bit). - pub all_streams_same_time_framing: bool, - /// `numSubFrames` (6 bits). `numSubFrames + 1` PayloadMux frames - /// are multiplexed. - pub num_sub_frames: u8, - /// `numProgram` (4 bits). `numProgram + 1` programs. - pub num_program: u8, - /// `numLayer[prog]` (3 bits) for each program — `num_layer[p] + 1` - /// layers in program `p`. - pub num_layer: Vec, - /// The flat per-stream layer table, in transmission order. - pub layers: Vec, - /// `otherDataPresent` (1 bit). - pub other_data_present: bool, - /// `otherDataLenBits` — the decoded length of the trailing - /// `otherData` field (in bits). `0` when `!otherDataPresent`. - pub other_data_len_bits: u32, - /// `crcCheckPresent` (1 bit). - pub crc_check_present: bool, - /// `crcCheckSum` (8 bits) when present. - pub crc_check_sum: Option, -} - -impl StreamMuxConfig { - /// `streamID[prog][lay]` lookup, mirroring the Table 1.42 - /// `streamID` assignment (`prog`-major, `lay`-minor flat counter). - pub fn stream_id(&self, prog: u8, lay: u8) -> Option { - self.layers - .iter() - .find(|l| l.prog == prog && l.lay == lay) - .map(|l| l.stream_id) - } - - /// The [`LayerConfig`] for a given flat `streamID`. - pub fn layer(&self, stream_id: u8) -> Option<&LayerConfig> { - self.layers.iter().find(|l| l.stream_id == stream_id) - } - - /// Parse a `StreamMuxConfig()` from `reader` (Table 1.42). - /// - /// `data` is the byte slice that backs `reader` (the same slice it - /// was constructed over); it is used only to re-read the config - /// prefix for CRC recomputation when `crcCheckPresent` is set. - /// - /// The reader is positioned at the `audioMuxVersion` bit and is - /// advanced to the bit after the configuration (the `crcCheckSum`, - /// or the last config bit when no CRC is present). The optional - /// `crcCheckSum` is recomputed against the configuration prefix and - /// validated; a mismatch is [`Error::LatmCrcMismatch`]. - pub fn parse(reader: &mut BitReader<'_>, data: &[u8]) -> Result { - let start_bit = reader.bit_position(); - - let audio_mux_version = read_u8(reader, 1)?; - let audio_mux_version_a = if audio_mux_version == 1 { - read_u8(reader, 1)? - } else { - 0 - }; - - if audio_mux_version_a != 0 { - // The Table 1.42 `else { /* tbd */ }` branch — no defined - // syntax. - return Err(Error::LatmAudioMuxVersionAReserved); - } - - let tara_buffer_fullness = if audio_mux_version == 1 { - Some(latm_get_value(reader)?) - } else { - None - }; - - let all_streams_same_time_framing = read_bit(reader)?; - let num_sub_frames = read_u8(reader, 6)?; - let num_program = read_u8(reader, 4)?; - if u32::from(num_program) > MAX_PROGRAM_INDEX { - return Err(Error::LatmConfigOutOfRange); - } - - let mut num_layer: Vec = Vec::with_capacity(usize::from(num_program) + 1); - let mut layers: Vec = Vec::new(); - // The "most recently transmitted" ASC, threaded across layers - // for `useSameConfig` resolution (§1.7.3.2.3). - let mut last_asc: Option = None; - let mut stream_cnt: u32 = 0; - - for prog in 0..=u32::from(num_program) { - let n_layer = read_u8(reader, 3)?; - if u32::from(n_layer) > MAX_LAYER_INDEX { - return Err(Error::LatmConfigOutOfRange); - } - num_layer.push(n_layer); - - for lay in 0..=u32::from(n_layer) { - if stream_cnt as usize >= MAX_STREAM_COUNT { - return Err(Error::LatmConfigOutOfRange); - } - let stream_id = stream_cnt as u8; - stream_cnt += 1; - - // useSameConfig — never present for the (0,0) layer. - let use_same_config = if prog == 0 && lay == 0 { - false - } else { - read_bit(reader)? - }; - - let asc = if use_same_config { - None - } else if audio_mux_version == 0 { - // audioMuxVersion == 0: the ASC has no explicit - // length prefix; it is parsed in place and its - // bit-length is implied by the ASC syntax. - let asc = AudioSpecificConfig::parse_bits(reader, start_bit)?; - Some(asc) - } else { - // audioMuxVersion == 1: `ascLen = LatmGetValue(); - // ascLen -= AudioSpecificConfig(); fillBits(ascLen)`. - // The ASC is length-prefixed, so we know the exact - // bit bound and can apply the §1.6.5 trailing - // implicit-SBR probe. - let asc_len = latm_get_value(reader)?; - let asc_start = reader.bit_position(); - let asc = AudioSpecificConfig::parse_bits_bounded( - reader, - asc_start, - u64::from(asc_len), - )?; - let consumed = reader.bit_position().saturating_sub(asc_start); - // fillBits = ascLen - (bits the ASC consumed). - let fill = u64::from(asc_len).saturating_sub(consumed); - if fill > 0 { - skip_bits(reader, fill)?; - } - Some(asc) - }; - - // Resolve useSameConfig into a concrete effective ASC. - let effective_asc = if let Some(a) = &asc { - last_asc = Some(a.clone()); - a.clone() - } else { - last_asc.clone().ok_or(Error::LatmNoPreviousMuxConfig)? - }; - - let frame_length_type = read_u8(reader, 3)?; - let mut latm_buffer_fullness = None; - let mut core_frame_offset = None; - let mut frame_length = None; - - match frame_length_type { - 0 => { - latm_buffer_fullness = Some(read_u8(reader, 8)?); - if !all_streams_same_time_framing { - // The CELP-core / AAC-enhancement pairing - // (§1.7.3.1 Table 1.42): AOT 6/20 (AAC SSR - // / ER AAC Scalable) layered above AOT 8/24 - // (CELP / ER CELP). - let this_aot = effective_asc.aot; - let prev_aot = layers.last().map(|l| l.effective_asc.aot); - let pairs = (this_aot == 6 || this_aot == 20) - && matches!(prev_aot, Some(8) | Some(24)); - if pairs { - core_frame_offset = Some(read_u8(reader, 6)?); - } - } - } - 1 => { - frame_length = Some(read_u16(reader, 9)?); - } - other => { - // `2` is reserved; `3`/`4`/`5` are CELP and - // `6`/`7` are HVXC, all table-indexed framing - // this AAC-focused decoder does not carry. - return Err(Error::LatmUnsupportedFrameLengthType(other)); - } - } - - layers.push(LayerConfig { - prog: prog as u8, - lay: lay as u8, - stream_id, - asc, - effective_asc, - frame_length_type, - latm_buffer_fullness, - core_frame_offset, - frame_length, - }); - } - } - - // otherDataPresent / otherDataLenBits. - let other_data_present = read_bit(reader)?; - let other_data_len_bits = if other_data_present { - if audio_mux_version == 1 { - latm_get_value(reader)? - } else { - // do { otherDataLenBits *= 256; esc; tmp(8); - // otherDataLenBits += tmp; } while (esc); - let mut acc: u32 = 0; - loop { - acc = acc.wrapping_mul(256); - let esc = read_bit(reader)?; - let tmp = read_u8(reader, 8)?; - acc = acc.wrapping_add(u32::from(tmp)); - if !esc { - break; - } - } - acc - } - } else { - 0 - }; - - // crcCheckPresent / crcCheckSum. The CRC covers the whole - // StreamMuxConfig() from `audioMuxVersion` up to but excluding - // crcCheckPresent — capture that prefix before reading the - // flag. - let crc_end_bit = reader.bit_position(); - let crc_check_present = read_bit(reader)?; - let crc_check_sum = if crc_check_present { - let sum = read_u8(reader, 8)?; - // Recompute over the config prefix and validate. - let prefix = read_back_bits(data, start_bit, crc_end_bit)?; - let expected = crc::stream_mux_config_crc(&prefix); - if expected != sum { - return Err(Error::LatmCrcMismatch); - } - Some(sum) - } else { - None - }; - - Ok(StreamMuxConfig { - audio_mux_version, - audio_mux_version_a, - tara_buffer_fullness, - all_streams_same_time_framing, - num_sub_frames, - num_program, - num_layer, - layers, - other_data_present, - other_data_len_bits, - crc_check_present, - crc_check_sum, - }) - } -} - -/// §1.7.3 signalling cap: `numChunk` is 4-bit (max chunk index 15). -const MAX_NUM_CHUNK_INDEX: u32 = 15; - -/// One recovered MPEG-4 Audio payload from a [`PayloadMux`] — the raw -/// access-unit bytes for a single `(subframe, prog, lay)` slot. For an -/// AAC layer these bytes are the §4.4.2.1 `raw_data_block()` that the -/// [`crate::decode::StreamDecoder`] / [`crate::raw_data_block`] layer -/// consumes. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MuxPayload { - /// Subframe index (`0 ..= numSubFrames`). - pub sub_frame: u8, - /// `prog` — the program this payload belongs to. - pub prog: u8, - /// `lay` — the layer within the program. - pub lay: u8, - /// `streamID[prog][lay]`. - pub stream_id: u8, - /// The raw payload bytes (one complete access unit for - /// `frameLengthType == 0`). - pub data: Vec, -} - -/// `MuxSlotLengthBytes[streamID]` decoded for one payload slot of a -/// [`PayloadLengthInfo`] (Table 1.44). For `frameLengthType == 0` this -/// is the running 8-bit-escape byte count; the bit length for -/// `frameLengthType == 1` comes from the layer's fixed `frameLength`. -#[derive(Debug, Clone, Copy)] -struct SlotLength { - prog: u8, - lay: u8, - stream_id: u8, - /// Payload length in **bits**. For type-0 this is `bytes * 8`; for - /// type-1 it is `(frameLength + 20) * 8`. - bits: u32, -} - -/// Decoded `AudioMuxElement()` — ISO/IEC 14496-3 §1.7.3.1 Table 1.41. -/// -/// Holds the (possibly inherited) [`StreamMuxConfig`] and the recovered -/// per-subframe payloads. Parsing supports `audioMuxVersionA == 0` -/// (the only defined branch) and `allStreamsSameTimeFraming` in both -/// states; non-same-time-framing uses the `numChunk` chunk layout of -/// Tables 1.44 / 1.45. -#[derive(Debug, Clone)] -pub struct AudioMuxElement { - /// `useSameStreamMux` (only present when `muxConfigPresent`). When - /// `true`, [`AudioMuxElement::config`] was inherited from the - /// previous element rather than parsed here. - pub use_same_stream_mux: bool, - /// The active multiplex configuration for this element. - pub config: StreamMuxConfig, - /// The recovered payloads in transmission order. - pub payloads: Vec, -} - -impl AudioMuxElement { - /// Parse an `AudioMuxElement()` (Table 1.41) from `reader`. - /// - /// `data` is the byte slice backing `reader` (forwarded to - /// [`StreamMuxConfig::parse`] for CRC recomputation). - /// `mux_config_present` is the `muxConfigPresent` flag the calling - /// layer supplies (LOAS [`AudioSyncStream`] passes `1`; an - /// out-of-band-configured transport passes `0`). `prev_config` is - /// the configuration decoded on the previous element, used when - /// `useSameStreamMux` is set or when `muxConfigPresent == 0`. - pub fn parse( - reader: &mut BitReader<'_>, - data: &[u8], - mux_config_present: bool, - prev_config: Option<&StreamMuxConfig>, - ) -> Result { - let (use_same_stream_mux, config) = if mux_config_present { - let use_same = read_bit(reader)?; - if use_same { - let cfg = prev_config.cloned().ok_or(Error::LatmNoPreviousMuxConfig)?; - (true, cfg) - } else { - (false, StreamMuxConfig::parse(reader, data)?) - } - } else { - // Out-of-band StreamMuxConfig(): apply the previous one. - let cfg = prev_config.cloned().ok_or(Error::LatmNoPreviousMuxConfig)?; - (false, cfg) - }; - - if config.audio_mux_version_a != 0 { - return Err(Error::LatmAudioMuxVersionAReserved); - } - - let mut payloads = Vec::new(); - for sub_frame in 0..=u32::from(config.num_sub_frames) { - let slots = payload_length_info(reader, &config)?; - payload_mux(reader, &config, sub_frame as u8, &slots, &mut payloads)?; - } - - // otherData: skip otherDataLenBits bits. - if config.other_data_present { - skip_bits(reader, u64::from(config.other_data_len_bits))?; - } - - // ByteAlign(). - reader.align_to_byte(); - - Ok(AudioMuxElement { - use_same_stream_mux, - config, - payloads, - }) - } -} - -/// `PayloadLengthInfo()` — §1.7.3.1 Table 1.44. Returns the decoded -/// per-slot payload bit-lengths in the order `PayloadMux()` will emit -/// them. -fn payload_length_info( - reader: &mut BitReader<'_>, - config: &StreamMuxConfig, -) -> Result> { - let mut slots = Vec::new(); - if config.all_streams_same_time_framing { - for prog in 0..=u32::from(config.num_program) { - let n_layer = config.num_layer[prog as usize]; - for lay in 0..=u32::from(n_layer) { - let stream_id = config - .stream_id(prog as u8, lay as u8) - .ok_or(Error::LatmConfigOutOfRange)?; - let layer = config.layer(stream_id).ok_or(Error::LatmConfigOutOfRange)?; - let bits = slot_bits(reader, layer)?; - slots.push(SlotLength { - prog: prog as u8, - lay: lay as u8, - stream_id, - bits, - }); - } - } - } else { - let num_chunk = read_u8(reader, 4)?; - if u32::from(num_chunk) > MAX_NUM_CHUNK_INDEX { - return Err(Error::LatmConfigOutOfRange); - } - for _ in 0..=u32::from(num_chunk) { - let stream_indx = read_u8(reader, 4)?; - let layer = config - .layer(stream_indx) - .ok_or(Error::LatmConfigOutOfRange)?; - let prog = layer.prog; - let lay = layer.lay; - let stream_id = layer.stream_id; - let frame_length_type = layer.frame_length_type; - let bits = slot_bits(reader, layer)?; - // For frameLengthType == 0 in the chunk layout the spec - // appends an AuEndFlag bit after MuxSlotLengthBytes. - if frame_length_type == 0 { - let _au_end_flag = read_bit(reader)?; - } - slots.push(SlotLength { - prog, - lay, - stream_id, - bits, - }); - } - } - Ok(slots) -} - -/// Decode the payload bit-length for one slot per its -/// `frameLengthType` (Table 1.44 inner body): the 8-bit-escape running -/// `MuxSlotLengthBytes` for type 0, or the fixed `(frameLength+20)*8` -/// for type 1. CELP/HVXC `MuxSlotLengthCoded` table indices are out of -/// scope and were already rejected when the config was parsed. -fn slot_bits(reader: &mut BitReader<'_>, layer: &LayerConfig) -> Result { - match layer.frame_length_type { - 0 => { - let mut bytes: u32 = 0; - loop { - let tmp = read_u8(reader, 8)?; - bytes = bytes.wrapping_add(u32::from(tmp)); - if tmp != 255 { - break; - } - } - Ok(bytes.saturating_mul(8)) - } - 1 => layer - .fixed_payload_bits() - .ok_or(Error::LatmConfigOutOfRange), - other => Err(Error::LatmUnsupportedFrameLengthType(other)), - } -} - -/// `PayloadMux()` — §1.7.3.1 Table 1.45. Reads each slot's payload -/// bytes in the same order `PayloadLengthInfo()` emitted them, pushing -/// one [`MuxPayload`] per slot. Payloads are byte-extracted; the spec -/// guarantees `frameLengthType == 0` payloads are an integer number of -/// bytes, and `AudioMuxElement()` byte-aligns the reader at each -/// subframe boundary in the common AAC case. -fn payload_mux( - reader: &mut BitReader<'_>, - config: &StreamMuxConfig, - sub_frame: u8, - slots: &[SlotLength], - out: &mut Vec, -) -> Result<()> { - // Walk in the order PayloadLengthInfo built the slots, which is the - // same program/layer (or chunk) order PayloadMux uses. - let _ = config; - for slot in slots { - let data = read_payload_bytes(reader, slot.bits)?; - out.push(MuxPayload { - sub_frame, - prog: slot.prog, - lay: slot.lay, - stream_id: slot.stream_id, - data, - }); - } - Ok(()) -} - -/// Read `bits` bits of payload as a byte vector. The common AAC case -/// (`frameLengthType == 0`, byte-aligned reader) is a fast `read_bytes` -/// path; a non-byte-multiple length or non-aligned reader falls back to -/// bit-by-bit assembly (MSB-first), with the trailing partial byte -/// left-justified. -fn read_payload_bytes(reader: &mut BitReader<'_>, bits: u32) -> Result> { - if bits % 8 == 0 && reader.is_byte_aligned() { - let n = (bits / 8) as usize; - return reader.read_bytes(n).map_err(|_| Error::UnexpectedEnd); - } - let full = bits / 8; - let rem = bits % 8; - let mut out = Vec::with_capacity((full + u32::from(rem != 0)) as usize); - for _ in 0..full { - out.push(read_u8(reader, 8)?); - } - if rem > 0 { - let v = read_u8(reader, rem)?; - out.push(v << (8 - rem)); - } - Ok(out) -} - -/// §1.7.3.1 Table 1.43 `LatmGetValue()`: a variable-length unsigned -/// integer carried as `bytesForValue` (2 bits) followed by -/// `bytesForValue + 1` bytes, big-endian. -pub fn latm_get_value(reader: &mut BitReader<'_>) -> Result { - let bytes_for_value = read_u8(reader, 2)?; - let mut value: u32 = 0; - for _ in 0..=u32::from(bytes_for_value) { - value = value.wrapping_mul(256); - let byte = read_u8(reader, 8)?; - value = value.wrapping_add(u32::from(byte)); - } - Ok(value) -} - -/// One decoded LOAS sync frame — ISO/IEC 14496-3 §1.7.2.1. -/// -/// Carries the framed `audioMuxLengthBytes` length, the recovered -/// [`AudioMuxElement`], and (for `EPAudioSyncStream`) the FEC header -/// fields. The byte offset of the frame within the LOAS buffer is also -/// recorded so callers can resume the sync search. -#[derive(Debug, Clone)] -pub struct LoasFrame { - /// `audioMuxLengthBytes` (13 bits) — the byte length of the framed - /// multiplexed element. - pub audio_mux_length_bytes: u16, - /// The recovered multiplexed element. - pub element: AudioMuxElement, - /// `frameCounter` (5 bits) — present only for `EPAudioSyncStream`. - pub frame_counter: Option, - /// Byte offset of the syncword within the LOAS buffer. - pub offset: usize, - /// Byte offset of the first byte after this sync frame. - pub next_offset: usize, -} - -/// LOAS `AudioSyncStream()` walker — ISO/IEC 14496-3 §1.7.2.1 -/// Table 1.36. -/// -/// Scans `data` for the 11-bit `0x2B7` syncword, then for each frame -/// reads the 13-bit `audioMuxLengthBytes` and decodes the byte-aligned -/// `AudioMuxElement(1)` over the next `audioMuxLengthBytes` bytes. The -/// syncword is searched on byte boundaries (AudioSyncStream frames are -/// byte-aligned per §1.7.2.2.1). -#[derive(Debug)] -pub struct AudioSyncStream<'a> { - data: &'a [u8], - pos: usize, - /// The most recently decoded [`StreamMuxConfig`], threaded across - /// frames for `useSameStreamMux` inheritance. - prev_config: Option, -} - -impl<'a> AudioSyncStream<'a> { - /// Create a walker over a LOAS `AudioSyncStream()` byte buffer. - pub fn new(data: &'a [u8]) -> Self { - AudioSyncStream { - data, - pos: 0, - prev_config: None, - } - } - - /// Decode the next `AudioSyncStream()` sync frame, advancing past - /// it. Returns `Ok(None)` at end of stream (no further syncword). - /// - /// On a successful decode the frame's [`StreamMuxConfig`] is - /// retained so a subsequent frame carrying `useSameStreamMux` can - /// inherit it. - pub fn next_frame(&mut self) -> Result> { - let Some(sync_off) = self.find_syncword(AUDIO_SYNC_STREAM_SYNCWORD, 11) else { - self.pos = self.data.len(); - return Ok(None); - }; - - // Read audioMuxLengthBytes (13 bits) starting after the 11-bit - // syncword. - let mut reader = BitReader::new(&self.data[sync_off..]); - reader.skip(11).map_err(|_| Error::LoasSyncInvalid)?; - let audio_mux_length_bytes = - reader.read_u32(13).map_err(|_| Error::LoasSyncInvalid)? as u16; - - // The AudioMuxElement(1) follows; it is byte-aligned because - // 11 + 13 = 24 bits = 3 whole bytes. - debug_assert_eq!(reader.bit_position(), 24); - let element_byte_start = sync_off + 3; - let element_byte_end = element_byte_start + usize::from(audio_mux_length_bytes); - if element_byte_end > self.data.len() { - return Err(Error::LoasSyncInvalid); - } - let element_bytes = &self.data[element_byte_start..element_byte_end]; - let mut elem_reader = BitReader::new(element_bytes); - let element = AudioMuxElement::parse( - &mut elem_reader, - element_bytes, - true, - self.prev_config.as_ref(), - )?; - - self.prev_config = Some(element.config.clone()); - self.pos = element_byte_end; - - Ok(Some(LoasFrame { - audio_mux_length_bytes, - element, - frame_counter: None, - offset: sync_off, - next_offset: element_byte_end, - })) - } - - /// Search for an `n`-bit syncword on byte boundaries from the - /// current position. Returns the byte offset of the syncword's - /// first byte, or `None` if not found before end of buffer. The - /// 11-bit `0x2B7` and 16-bit `0x4DE1` syncwords both begin on a - /// byte boundary in their respective frame layouts. - fn find_syncword(&self, syncword: u32, n: u32) -> Option { - let bytes_needed = n.div_ceil(8) as usize; - let mut off = self.pos; - while off + bytes_needed <= self.data.len() { - let mut r = BitReader::new(&self.data[off..]); - if let Ok(v) = r.read_u32(n) { - if v == syncword { - return Some(off); - } - } - off += 1; - } - None - } -} - -impl Iterator for AudioSyncStream<'_> { - type Item = Result; - - fn next(&mut self) -> Option { - match self.next_frame() { - Ok(Some(frame)) => Some(Ok(frame)), - Ok(None) => None, - Err(e) => { - // Stop iterating after surfacing the error. - self.pos = self.data.len(); - Some(Err(e)) - } - } - } -} - -/// Decoded `EPAudioSyncStream()` FEC header — ISO/IEC 14496-3 §1.7.2.1 -/// Table 1.37. -/// -/// Parses the 16-bit `0x4DE1` syncword, the 4-bit `futureUse`, the -/// 13-bit `audioMuxLengthBytes`, the 5-bit `frameCounter`, and the -/// 18-bit `headerParity`. The body is an `EPMuxElement(1, 1)` whose -/// EP-tool de-interleave is out of scope; this struct captures the -/// header so callers can frame the stream and recover the (byte-aligned) -/// element body bounds. -#[derive(Debug, Clone)] -pub struct EpAudioSyncHeader { - /// `futureUse` (4 bits). - pub future_use: u8, - /// `audioMuxLengthBytes` (13 bits). - pub audio_mux_length_bytes: u16, - /// `frameCounter` (5 bits). - pub frame_counter: u8, - /// `headerParity` (18 bits). - pub header_parity: u32, - /// Byte offset of the syncword. - pub offset: usize, - /// Byte offset of the first byte of the `EPMuxElement(1, 1)` body - /// (the header is `16 + 4 + 13 + 5 + 18 = 56` bits = 7 bytes, so the - /// body is byte-aligned). - pub body_offset: usize, -} - -impl EpAudioSyncHeader { - /// Parse one `EPAudioSyncStream()` FEC header from `data` starting - /// at `pos`, scanning for the `0x4DE1` syncword on byte boundaries. - /// Returns `Ok(None)` if no syncword is found. - pub fn parse(data: &[u8], pos: usize) -> Result> { - let walker = AudioSyncStream { - data, - pos, - prev_config: None, - }; - let Some(sync_off) = walker.find_syncword(EP_AUDIO_SYNC_STREAM_SYNCWORD, 16) else { - return Ok(None); - }; - let mut reader = BitReader::new(&data[sync_off..]); - reader.skip(16).map_err(|_| Error::LoasSyncInvalid)?; // syncword - let future_use = read_u8(&mut reader, 4)?; - let audio_mux_length_bytes = read_u16(&mut reader, 13)?; - let frame_counter = read_u8(&mut reader, 5)?; - let header_parity = reader.read_u32(18).map_err(|_| Error::UnexpectedEnd)?; - debug_assert_eq!(reader.bit_position(), 56); - Ok(Some(EpAudioSyncHeader { - future_use, - audio_mux_length_bytes, - frame_counter, - header_parity, - offset: sync_off, - body_offset: sync_off + 7, - })) - } -} - -/// Generator polynomial of the `EPAudioSyncStream()` `headerParity` -/// BCH(36,18) code (§1.7.2.2.2): -/// x¹⁸+x¹⁷+x¹⁶+x¹⁵+x⁹+x⁷+x⁶+x³+x²+x+1, stored without the leading -/// x¹⁸ term. -const EP_SYNC_BCH_GEN: u32 = (1 << 17) - | (1 << 16) - | (1 << 15) - | (1 << 9) - | (1 << 7) - | (1 << 6) - | (1 << 3) - | (1 << 2) - | (1 << 1) - | 1; - -/// Compute the §1.7.2.2.2 `headerParity` — the 18 parity bits of the -/// shortened BCH(36,18) over `audioMuxLengthBytes` (13 bits) followed -/// by `frameCounter` (5 bits), `R(x)` of `M(x)·x¹⁸ mod G(x)` per -/// §1.8.4.3. -pub fn ep_sync_header_parity(audio_mux_length_bytes: u16, frame_counter: u8) -> u32 { - let msg: u32 = - (u32::from(audio_mux_length_bytes & 0x1FFF) << 5) | u32::from(frame_counter & 0x1F); - let mut reg: u32 = 0; - let top = 1u32 << 17; - let feed = |reg: &mut u32, bit: bool| { - let high = *reg & top != 0; - *reg = (*reg << 1) & 0x3FFFF; - if high { - *reg ^= EP_SYNC_BCH_GEN; - } - if bit { - *reg ^= 1; - } - }; - for i in (0..18).rev() { - feed(&mut reg, msg & (1 << i) != 0); - } - for _ in 0..18 { - let high = reg & top != 0; - reg = (reg << 1) & 0x3FFFF; - if high { - reg ^= EP_SYNC_BCH_GEN; - } - } - reg -} - -impl EpAudioSyncHeader { - /// Verify the §1.7.2.2.2 BCH(36,18) `headerParity` against the - /// received `audioMuxLengthBytes` / `frameCounter`. - pub fn parity_ok(&self) -> bool { - ep_sync_header_parity(self.audio_mux_length_bytes, self.frame_counter) == self.header_parity - } -} - -/// Threaded cross-frame state of an `EPMuxElement()` stream: the -/// active EP-tool configuration and the previous `StreamMuxConfig`. -#[derive(Debug, Default)] -pub struct EpMuxState { - /// The active `ErrorProtectionSpecificConfig()` (threaded across - /// `epUsePreviousMuxConfig == 1` elements). - pub ep_config: Option, - /// The previous `StreamMuxConfig` for `useSameStreamMux`. - pub prev_config: Option, -} - -/// A decoded `EPMuxElement(1, 1)` (§1.7.3.1 Table 1.40): the EP-tool -/// configuration in force plus the recovered (error-corrected) -/// `AudioMuxElement()`. -#[derive(Debug)] -pub struct EpMuxElement { - /// `epUsePreviousMuxConfig` (majority-decoded). - pub use_previous_mux_config: bool, - /// The recovered inner `AudioMuxElement()`. - pub element: AudioMuxElement, -} - -impl EpMuxElement { - /// Parse an `EPMuxElement(epDataPresent = 1, muxConfigPresent = 1)` - /// from `data` (the whole element, byte-aligned), threading - /// `state` across elements. - /// - /// Layout per Table 1.40: `epUsePreviousMuxConfig` + its 2-bit - /// repetition parity (majority decides, §1.7.3.2.1); when clear, - /// the 10-bit `epSpecificConfigLength` protected by the Table 1.59 - /// Golay(23,12) 11-bit parity, then - /// `ErrorProtectionSpecificConfig()` + its Table 1.59 parity; - /// `ByteAlign()`; then `EPAudioMuxElement(1)` — the EP-tool - /// `ep_frame()` whose decoded class concatenation is the plain - /// `AudioMuxElement(1)` bit stream (the §1.7.3.2.1 sensitivity - /// category instances ride in syntax order). - pub fn parse(data: &[u8], state: &mut EpMuxState) -> Result { - let mut reader = BitReader::new(data); - // epUsePreviousMuxConfig + 2-bit repetition parity. - let b0 = read_bit(&mut reader)?; - let b1 = read_bit(&mut reader)?; - let b2 = read_bit(&mut reader)?; - let use_prev = (u8::from(b0) + u8::from(b1) + u8::from(b2)) >= 2; - if !use_prev { - // epSpecificConfigLength (10) + Golay parity (11). - let mut len_bits_field = [false; 10]; - for b in len_bits_field.iter_mut() { - *b = read_bit(&mut reader)?; - } - let mut parity = [false; 11]; - for b in parity.iter_mut() { - *b = read_bit(&mut reader)?; - } - let corrected = crate::ep_fec::header_fec_decode(&len_bits_field, &parity)?; - let mut cfg_len = 0usize; - for &b in &corrected { - cfg_len = (cfg_len << 1) | usize::from(b); - } - // ErrorProtectionSpecificConfig() (self-terminating) + - // Table 1.59 parity over its bits. - let cfg_start = reader.bit_position(); - let epsc = crate::ep_config::ErrorProtectionSpecificConfig::parse(&mut reader)?; - let consumed = (reader.bit_position() - cfg_start) as usize; - // `epSpecificConfigLength` indicates the size of the - // config; validate in bits (with a byte-unit fallback — - // the staged text does not name the unit). - if cfg_len != consumed && cfg_len != consumed.div_ceil(8) { - return Err(Error::EpFrameInvalid); - } - let cfg_bits = read_back_bits(data, cfg_start, cfg_start + consumed as u64)?; - let parity_len = crate::ep_fec::HeaderFec::for_len(consumed)?.parity_bits(consumed)?; - let mut cfg_parity = Vec::with_capacity(parity_len); - for _ in 0..parity_len { - cfg_parity.push(read_bit(&mut reader)?); - } - let corrected_cfg = crate::ep_fec::header_fec_decode(&cfg_bits, &cfg_parity)?; - if corrected_cfg != cfg_bits { - // The FEC corrected config bits: re-parse from the - // corrected sequence. - let mut bytes = vec![0u8; corrected_cfg.len().div_ceil(8)]; - for (i, &b) in corrected_cfg.iter().enumerate() { - if b { - bytes[i / 8] |= 0x80 >> (i % 8); - } - } - let mut r2 = BitReader::new(&bytes); - state.ep_config = Some(crate::ep_config::ErrorProtectionSpecificConfig::parse( - &mut r2, - )?); - } else { - state.ep_config = Some(epsc); - } - } - // ByteAlign(). - reader.align_to_byte(); - let epsc = state.ep_config.clone().ok_or(Error::EpFrameInvalid)?; - let codec = crate::ep_frame::EpFrameCodec::new(epsc)?; - let body = crate::ep_frame::read_remaining_bytes(&mut reader, data.len())?; - let frame = codec.decode(&body)?; - // The class concatenation is the AudioMuxElement(1) bits. - let mut au_bits: Vec = Vec::new(); - for c in &frame.classes { - au_bits.extend_from_slice(c); - } - let mut au_bytes = vec![0u8; au_bits.len().div_ceil(8)]; - for (i, &b) in au_bits.iter().enumerate() { - if b { - au_bytes[i / 8] |= 0x80 >> (i % 8); - } - } - let mut au_reader = BitReader::new(&au_bytes); - let element = - AudioMuxElement::parse(&mut au_reader, &au_bytes, true, state.prev_config.as_ref())?; - state.prev_config = Some(element.config.clone()); - Ok(EpMuxElement { - use_previous_mux_config: use_prev, - element, - }) - } -} - -// ---- bit helpers ----------------------------------------------------- - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -fn read_u16(reader: &mut BitReader<'_>, n: u32) -> Result { - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u16) -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} - -fn skip_bits(reader: &mut BitReader<'_>, n: u64) -> Result<()> { - // BitReader::skip takes a u32; chunk for safety on large fill runs. - let mut remaining = n; - while remaining > 0 { - let chunk = remaining.min(u64::from(u32::MAX)) as u32; - reader.skip(chunk).map_err(|_| Error::UnexpectedEnd)?; - remaining -= u64::from(chunk); - } - Ok(()) -} - -/// Re-read the bits of an already-consumed `[from_bit, to_bit)` range -/// of `data` as a `Vec` in MSB-first transmission order, for CRC -/// recomputation. A fresh reader is created over the backing buffer so -/// the original reader's position is untouched. -fn read_back_bits(data: &[u8], from_bit: u64, to_bit: u64) -> Result> { - debug_assert!(to_bit >= from_bit); - let count = (to_bit - from_bit) as usize; - let mut scratch = BitReader::new(data); - skip_bits(&mut scratch, from_bit)?; - let mut out = Vec::with_capacity(count); - for _ in 0..count { - out.push(scratch.read_bit().map_err(|_| Error::UnexpectedEnd)?); - } - Ok(out) -} - -// ---- LOAS → PCM decode driver ---------------------------------------- - -use std::collections::HashMap; - -use crate::decode::{DecodedFrame, StreamDecoder}; - -/// Whole-stream LATM/LOAS → PCM decoder. -/// -/// Walks a LOAS `AudioSyncStream()` byte buffer ([`AudioSyncStream`]), -/// and for every recovered access unit ([`MuxPayload`]) drives the -/// payload's §4.4.2.1 `raw_data_block()` through the -/// [`crate::decode::StreamDecoder`] core -/// ([`StreamDecoder::decode_raw_data_block`]) using the configuration the -/// LATM `StreamMuxConfig` carried in the layer's -/// [`AudioSpecificConfig`]. -/// -/// The LATM multiplex can carry several streams (`streamID[prog][lay]`); -/// each is given its own [`StreamDecoder`] so the per-stream filterbank -/// overlap-add tail, LTP history, and predictor state thread across the -/// frames of that stream independently. For the common single-program / -/// single-layer AAC case there is exactly one stream. -/// -/// ## Scope -/// -/// Targets the core (AAC-LC / Main / LTP) tool chain the -/// [`StreamDecoder`] covers, **plus §4.6.18 SBR (HE-AAC v1)** — the -/// shared `decode_raw_data_block` core auto-detects the `EXT_SBR_DATA` -/// FIL payloads in-band and doubles the output rate (or keeps the core -/// rate in the §4.6.18.4.3 downsampled mode), and a PS payload renders -/// stereo through the subpart-8 tool (HE-AAC v2). The -/// `audioObjectType` carried by the ASC must be a General Audio -/// type whose `raw_data_block()` the core driver understands; otherwise -/// the underlying decode surfaces its own element-level error. -#[derive(Debug, Default)] -pub struct LoasDecoder { - /// One [`StreamDecoder`] per `streamID`, so each multiplexed stream's - /// inter-frame state stays independent. - streams: HashMap, - /// One §4.5.2.2 [`crate::scalable::ScalableDecoder`] per *program* - /// for the scalable object types (AOTs 6 / 20), whose layers ride - /// separate `streamID`s but decode to one combined output. - scalable: HashMap, - /// Per-program buffer collecting the current subframe's scalable - /// layer payloads (in layer order) until the stack is complete. - scalable_pending: HashMap>>, - /// Caller-forced §4.6.18.4.3 downsampled SBR output (see - /// [`Self::set_sbr_downsampled`]); an explicitly signalled ASC - /// whose extension sampling frequency equals the core rate selects - /// the mode per stream regardless. - sbr_downsampled: bool, - /// Caller-forced §4.6.18.8 low-power SBR mode (see - /// [`Self::set_sbr_low_power`]). - sbr_low_power: bool, -} - -impl LoasDecoder { - /// A fresh LOAS decoder with no per-stream state. - #[must_use] - pub fn new() -> Self { - LoasDecoder::default() - } - - /// Force the §4.6.18.4.3 downsampled SBR output mode on every - /// stream decoder this LOAS driver creates: SBR-active streams are - /// emitted at the core sampling rate. Independent of the forced - /// mode, a layer whose explicitly signalled `AudioSpecificConfig` - /// carries `extensionSamplingFrequency == samplingFrequency` - /// selects the mode by itself (the SBR output rate the ASC - /// declares *is* the core rate). Select before decoding. - pub fn set_sbr_downsampled(&mut self, downsampled: bool) { - self.sbr_downsampled = downsampled; - } - - /// Force the §4.6.18.8 low-power SBR mode on every stream decoder - /// this LOAS driver creates (real-valued filterbanks + the LP - /// adjustment chain; PS streams are rejected in this mode). Select - /// before decoding. - pub fn set_sbr_low_power(&mut self, low_power: bool) { - self.sbr_low_power = low_power; - } - - /// Decode a whole LOAS `AudioSyncStream()` byte buffer to a vector of - /// per-access-unit interleaved PCM frames, in transmission order. - /// - /// Each [`LoasFrame`]'s `AudioMuxElement` may carry several - /// subframes / payloads; every payload is decoded and pushed in the - /// order [`AudioMuxElement::payloads`] presents them. A frame that - /// yields no channel element (fill-only) still contributes its - /// (empty) [`DecodedFrame`]. - pub fn decode_all(&mut self, data: &[u8]) -> Result> { - let mut out = Vec::new(); - let mut walker = AudioSyncStream::new(data); - while let Some(frame) = walker.next_frame()? { - for payload in &frame.element.payloads { - // A scalable (AOT 6 / 20) layer joins its program's - // pending stack; the stack decodes as one combined - // access unit when the last layer arrives (§4.5.2.2: - // one elementary stream per layer, one output). - let config = &frame.element.config; - let layer = config - .layer(payload.stream_id) - .ok_or(Error::LatmConfigOutOfRange)?; - if layer.effective_asc.aot == 6 || layer.effective_asc.aot == 20 { - if let Some(decoded) = self.push_scalable_payload(config, payload)? { - out.push(decoded); - } - continue; - } - let decoded = self.decode_payload(config, payload)?; - out.push(decoded); - } - } - Ok(out) - } - - /// Feed one scalable-program layer payload; returns the combined - /// [`DecodedFrame`] when the payload completes the program's layer - /// stack for the current access unit, `None` while the stack is - /// still filling. - /// - /// Layers must arrive in layer order within each access unit - /// (which is how `AudioMuxElement()` multiplexes them under - /// `allStreamsSameTimeFraming`); an out-of-order layer surfaces - /// [`Error::ScalableInvalid`]. - pub fn push_scalable_payload( - &mut self, - config: &StreamMuxConfig, - payload: &MuxPayload, - ) -> Result> { - let layer = config - .layer(payload.stream_id) - .ok_or(Error::LatmConfigOutOfRange)?; - let prog = layer.prog; - let n_layers = usize::from( - *config - .num_layer - .get(usize::from(prog)) - .ok_or(Error::LatmConfigOutOfRange)?, - ) + 1; - let pending = self.scalable_pending.entry(prog).or_default(); - if usize::from(layer.lay) != pending.len() { - self.scalable_pending.remove(&prog); - return Err(Error::ScalableInvalid); - } - pending.push(payload.data.clone()); - if pending.len() < n_layers { - return Ok(None); - } - let payloads = self.scalable_pending.remove(&prog).unwrap_or_default(); - - // Resolve the program's ScalableConfig from the layer ASCs. - let mut ascs: Vec<&crate::asc::AudioSpecificConfig> = Vec::with_capacity(n_layers); - for lay in 0..n_layers { - let sid = config - .stream_id(prog, lay as u8) - .ok_or(Error::LatmConfigOutOfRange)?; - let lc = config.layer(sid).ok_or(Error::LatmConfigOutOfRange)?; - ascs.push(&lc.effective_asc); - } - let cfg = crate::scalable::ScalableConfig::from_layer_ascs(&ascs)?; - // Reuse the persistent decoder while the configuration holds; - // a mid-stream StreamMuxConfig change rebuilds it (the - // overlap/LTP state is geometry-shaped). - let rebuild = !matches!(self.scalable.get(&prog), Some(d) if d.config() == &cfg); - if rebuild { - self.scalable - .insert(prog, crate::scalable::ScalableDecoder::new(cfg)?); - } - let dec = self.scalable.get_mut(&prog).expect("just inserted"); - let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect(); - dec.decode_frame(&refs).map(Some) - } - - /// Decode a whole `EPAudioSyncStream()` byte buffer (§1.7.2.1 - /// Table 1.37) to per-access-unit PCM frames: every `0x4DE1` sync - /// frame's BCH(36,18)-verified header is walked, its - /// `EPMuxElement(1, 1)` is EP-decoded ([`EpMuxElement::parse`] — - /// FEC-corrected, CRC-checked, de-interleaved) and the recovered - /// `AudioMuxElement()` payloads decode exactly as on the plain - /// LOAS path (scalable programs included). - pub fn decode_all_ep(&mut self, data: &[u8]) -> Result> { - let mut out = Vec::new(); - let mut ep_state = EpMuxState::default(); - let mut pos = 0usize; - while let Some(header) = EpAudioSyncHeader::parse(data, pos)? { - if !header.parity_ok() { - return Err(Error::EpFrameInvalid); - } - let body_end = header - .body_offset - .checked_add(usize::from(header.audio_mux_length_bytes)) - .ok_or(Error::UnexpectedEnd)?; - if body_end > data.len() { - return Err(Error::UnexpectedEnd); - } - let mux = EpMuxElement::parse(&data[header.body_offset..body_end], &mut ep_state)?; - for payload in &mux.element.payloads { - let config = &mux.element.config; - let layer = config - .layer(payload.stream_id) - .ok_or(Error::LatmConfigOutOfRange)?; - if layer.effective_asc.aot == 6 || layer.effective_asc.aot == 20 { - if let Some(decoded) = self.push_scalable_payload(config, payload)? { - out.push(decoded); - } - continue; - } - out.push(self.decode_payload(config, payload)?); - } - pos = body_end; - } - Ok(out) - } - - /// Decode one recovered [`MuxPayload`] to PCM, routing it to the - /// per-`streamID` [`StreamDecoder`] and configuring the decode from - /// the payload's layer [`AudioSpecificConfig`]. - pub fn decode_payload( - &mut self, - config: &StreamMuxConfig, - payload: &MuxPayload, - ) -> Result { - let layer = config - .layer(payload.stream_id) - .ok_or(Error::LatmConfigOutOfRange)?; - let asc = &layer.effective_asc; - // The scalable object types decode per *program*, not per - // stream: route through the layer-stack collector. While a - // multi-layer stack is still filling, an empty frame (0 - // channels) is returned — [`Self::decode_all`] instead calls - // [`Self::push_scalable_payload`] directly and skips these. - if asc.aot == 6 || asc.aot == 20 { - let sample_rate = asc.sample_rate; - return Ok(self - .push_scalable_payload(config, payload)? - .unwrap_or(DecodedFrame { - pcm: Vec::new(), - channels: 0, - sample_rate, - })); - } - // An SBR-signalling ASC (explicit AOT 5 wrapper or the implicit - // trailing probe) needs no pre-rejection: the shared - // `decode_raw_data_block` core auto-detects the `EXT_SBR_DATA` - // FIL payloads in-band and doubles the output rate (§4.6.18). - // The decode runs at the *core* configuration (`asc.aot` is the - // unwrapped core object type, `asc.sample_rate` the core rate); - // a PS payload renders stereo through the subpart-8 tool. - // §4.5.1.1 — resolve the frame-length family from the layer's - // ASC (`frameLengthFlag` semantics depend on the AOT: 1024/960 - // lines for the general GA types, 512/480 for ER AAC LD). - let family = crate::swb_offset::FrameFamily::from_aot_and_flag( - asc.aot, - asc.ga_body.frame_length == crate::asc::FrameLength::Long960, - ); - let dec = self.streams.entry(payload.stream_id).or_insert_with({ - let force_down = self.sbr_downsampled; - let force_lp = self.sbr_low_power; - move || { - let mut d = StreamDecoder::new(); - d.set_sbr_downsampled(force_down); - d.set_sbr_low_power(force_lp); - d.set_frame_family(family); - d - } - }); - // A mid-stream StreamMuxConfig replacement can change the - // layer's frame family; the per-element overlap/LTP state is - // family-shaped, so a mismatched decoder is rebuilt from - // scratch rather than fed the wrong geometry. - if dec.frame_family() != family { - let mut d = StreamDecoder::new(); - d.set_sbr_downsampled(self.sbr_downsampled); - d.set_sbr_low_power(self.sbr_low_power); - d.set_frame_family(family); - *dec = d; - } - // §4.6.18.2.6: FsSBR is twice the core rate; an explicit SBR - // ASC whose extensionSamplingFrequency equals the core rate is - // therefore declaring the §4.6.18.4.3 downsampled output. - if asc.sbr_present && asc.extension_sample_rate == Some(asc.sample_rate) { - dec.set_sbr_downsampled(true); - } - // A channelConfiguration-0 layer carries its layout in the - // ASC's inline program_config_element(); install it so the - // §8.5.2.2 canonical output reorder applies (an in-band PCE in - // a later raw_data_block() still supersedes it). - if asc.channel_configuration == 0 { - if let Some(pce) = &asc.ga_body.pce { - dec.set_program_config(pce.clone()); - } - } - // The ER General-Audio object types use the §4.4.2.3 Table 4.19 - // fixed-sequence er_raw_data_block() instead of the tagged - // element walk; route AOT 17 (ER AAC LC), AOT 19 (ER AAC LTP — - // the §4.6.7 LTP tool over the same Table 4.19 walk) and - // AOT 23 (ER AAC LD, §4.6.17 — the 512/480-line family - // installed above) there with the ASC's resilience triplet. - if asc.aot == 17 || asc.aot == 19 || asc.aot == 23 { - let resilience = asc - .ga_body - .extension_body - .as_ref() - .and_then(|ext| ext.resilience) - .unwrap_or_default(); - return dec.decode_er_raw_data_block( - asc.aot, - asc.sampling_frequency_index, - asc.sample_rate, - asc.channel_configuration, - resilience, - &payload.data, - ); - } - // LATM carries exactly one raw_data_block() per payload. - dec.decode_raw_data_block( - asc.aot, - asc.sampling_frequency_index, - asc.sample_rate, - asc.channel_configuration, - 1, - &payload.data, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::BitWriter; - - /// AAC-LC, 44.1 kHz (samplingFrequencyIndex 4), stereo - /// (channelConfiguration 2): AOT=2 (5 bits `00010`), freqIdx=4 - /// (`0100`), chanConfig=2 (`0010`), then GASpecificConfig - /// `frameLengthFlag=0 dependsOnCoreCoder=0 extensionFlag=0` - /// (`000`). 16 bits total = `0x12 0x10`. - const AAC_LC_ASC: [u8; 2] = [0x12, 0x10]; - - /// Append the §1.7.3 AAC-LC ASC bit-for-bit into `w`. - fn write_aac_lc_asc(w: &mut BitWriter) { - // 16 bits, MSB-first, exactly as AAC_LC_ASC encodes. - w.write_u32(u32::from(u16::from_be_bytes(AAC_LC_ASC)), 16); - } - - #[test] - fn latm_get_value_single_byte() { - // bytesForValue = 0 -> one byte. value = 0xFF. - let mut w = BitWriter::new(); - w.write_u32(0, 2); // bytesForValue - w.write_u32(0xFF, 8); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert_eq!(latm_get_value(&mut r).unwrap(), 0xFF); - } - - #[test] - fn latm_get_value_multi_byte() { - // bytesForValue = 2 -> three bytes, big-endian: 0x010203. - let mut w = BitWriter::new(); - w.write_u32(2, 2); - w.write_u32(0x01, 8); - w.write_u32(0x02, 8); - w.write_u32(0x03, 8); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert_eq!(latm_get_value(&mut r).unwrap(), 0x01_02_03); - } - - /// Build a minimal `audioMuxVersion == 0` AAC-LC StreamMuxConfig: - /// one program, one layer, allStreamsSameTimeFraming, - /// frameLengthType 0, latmBufferFullness 0xFF, no otherData, no - /// CRC. - fn build_min_smc() -> Vec { - let mut w = BitWriter::new(); - w.write_bit(false); // audioMuxVersion = 0 - w.write_bit(true); // allStreamsSameTimeFraming = 1 - w.write_u32(0, 6); // numSubFrames = 0 - w.write_u32(0, 4); // numProgram = 0 - w.write_u32(0, 3); // numLayer = 0 - // (prog 0, lay 0): no useSameConfig bit; ASC inline. - write_aac_lc_asc(&mut w); - w.write_u32(0, 3); // frameLengthType = 0 - w.write_u32(0xFF, 8); // latmBufferFullness = 0xFF - w.write_bit(false); // otherDataPresent = 0 - w.write_bit(false); // crcCheckPresent = 0 - w.finish() - } - - #[test] - fn stream_mux_config_minimal_aac_lc() { - let bytes = build_min_smc(); - let mut r = BitReader::new(&bytes); - let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); - assert_eq!(smc.audio_mux_version, 0); - assert_eq!(smc.audio_mux_version_a, 0); - assert!(smc.all_streams_same_time_framing); - assert_eq!(smc.num_sub_frames, 0); - assert_eq!(smc.num_program, 0); - assert_eq!(smc.num_layer, vec![0]); - assert_eq!(smc.layers.len(), 1); - let lay = &smc.layers[0]; - assert_eq!(lay.stream_id, 0); - assert_eq!(lay.frame_length_type, 0); - assert_eq!(lay.latm_buffer_fullness, Some(0xFF)); - assert_eq!(lay.effective_asc.aot, 2); - assert_eq!(lay.effective_asc.sampling_frequency_index, 4); - assert_eq!(lay.effective_asc.channel_configuration, 2); - assert!(!smc.other_data_present); - assert!(!smc.crc_check_present); - assert_eq!(smc.stream_id(0, 0), Some(0)); - } - - /// Push the low `n` bits of `v` (MSB-first) onto a bool vector, - /// mirroring `BitWriter::write_u32` so the test can hold the config - /// prefix as bits for an independent CRC recomputation. - fn push_bits(out: &mut Vec, v: u32, n: u32) { - for i in (0..n).rev() { - out.push((v >> i) & 1 == 1); - } - } - - #[test] - fn stream_mux_config_with_valid_crc() { - // Build the config prefix as a bit vector, compute its CRC, then - // emit prefix + crcCheckPresent + crcCheckSum. - let mut prefix: Vec = Vec::new(); - push_bits(&mut prefix, 0, 1); // audioMuxVersion = 0 - push_bits(&mut prefix, 1, 1); // allStreamsSameTimeFraming - push_bits(&mut prefix, 0, 6); // numSubFrames - push_bits(&mut prefix, 0, 4); // numProgram - push_bits(&mut prefix, 0, 3); // numLayer - push_bits(&mut prefix, u32::from(u16::from_be_bytes(AAC_LC_ASC)), 16); - push_bits(&mut prefix, 0, 3); // frameLengthType - push_bits(&mut prefix, 0xFF, 8); // latmBufferFullness - push_bits(&mut prefix, 0, 1); // otherDataPresent - let sum = crc::stream_mux_config_crc(&prefix); - - let mut w = BitWriter::new(); - for &b in &prefix { - w.write_bit(b); - } - w.write_bit(true); // crcCheckPresent - w.write_u32(u32::from(sum), 8); // crcCheckSum - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); - assert!(smc.crc_check_present); - assert_eq!(smc.crc_check_sum, Some(sum)); - } - - #[test] - fn stream_mux_config_bad_crc_rejected() { - let mut w = BitWriter::new(); - w.write_bit(false); - w.write_bit(true); - w.write_u32(0, 6); - w.write_u32(0, 4); - w.write_u32(0, 3); - write_aac_lc_asc(&mut w); - w.write_u32(0, 3); - w.write_u32(0xFF, 8); - w.write_bit(false); - w.write_bit(true); // crcCheckPresent - w.write_u32(0x00, 8); // deliberately wrong crcCheckSum - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(matches!( - StreamMuxConfig::parse(&mut r, &bytes), - Err(Error::LatmCrcMismatch) - )); - } - - #[test] - fn stream_mux_config_two_layers_use_same_config() { - // One program, two layers; the second layer sets - // useSameConfig, so it must inherit the first layer's ASC. - let mut w = BitWriter::new(); - w.write_bit(false); // audioMuxVersion = 0 - w.write_bit(true); // allStreamsSameTimeFraming - w.write_u32(0, 6); // numSubFrames - w.write_u32(0, 4); // numProgram = 0 - w.write_u32(1, 3); // numLayer = 1 -> two layers - // layer 0: no useSameConfig bit; inline ASC. - write_aac_lc_asc(&mut w); - w.write_u32(0, 3); // frameLengthType 0 - w.write_u32(0xFF, 8); // latmBufferFullness - // layer 1: useSameConfig = 1. - w.write_bit(true); // useSameConfig - w.write_u32(0, 3); // frameLengthType 0 - w.write_u32(0xFF, 8); // latmBufferFullness - w.write_bit(false); // otherDataPresent - w.write_bit(false); // crcCheckPresent - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); - assert_eq!(smc.layers.len(), 2); - assert!(smc.layers[0].asc.is_some()); - assert!(smc.layers[1].asc.is_none()); - // The inherited effective ASC matches the first layer. - assert_eq!( - smc.layers[1].effective_asc.aot, - smc.layers[0].effective_asc.aot - ); - assert_eq!(smc.stream_id(0, 1), Some(1)); - } - - #[test] - fn stream_mux_config_unsupported_frame_length_type() { - // frameLengthType = 3 (CELP) must be rejected. - let mut w = BitWriter::new(); - w.write_bit(false); - w.write_bit(true); - w.write_u32(0, 6); - w.write_u32(0, 4); - w.write_u32(0, 3); - write_aac_lc_asc(&mut w); - w.write_u32(3, 3); // frameLengthType = 3 (CELP) - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(matches!( - StreamMuxConfig::parse(&mut r, &bytes), - Err(Error::LatmUnsupportedFrameLengthType(3)) - )); - } - - #[test] - fn stream_mux_config_version1_reserved_a_rejected() { - // audioMuxVersion = 1, audioMuxVersionA = 1 -> reserved. - let mut w = BitWriter::new(); - w.write_bit(true); // audioMuxVersion = 1 - w.write_bit(true); // audioMuxVersionA = 1 - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(matches!( - StreamMuxConfig::parse(&mut r, &bytes), - Err(Error::LatmAudioMuxVersionAReserved) - )); - } - - #[test] - fn stream_mux_config_frame_length_type1_fixed_bits() { - // frameLengthType = 1, frameLength = 100 -> (100+20)*8 bits. - let mut w = BitWriter::new(); - w.write_bit(false); - w.write_bit(true); - w.write_u32(0, 6); - w.write_u32(0, 4); - w.write_u32(0, 3); - write_aac_lc_asc(&mut w); - w.write_u32(1, 3); // frameLengthType = 1 - w.write_u32(100, 9); // frameLength = 100 - w.write_bit(false); // otherDataPresent - w.write_bit(false); // crcCheckPresent - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let smc = StreamMuxConfig::parse(&mut r, &bytes).unwrap(); - let lay = &smc.layers[0]; - assert_eq!(lay.frame_length_type, 1); - assert_eq!(lay.frame_length, Some(100)); - assert_eq!(lay.fixed_payload_bits(), Some((100 + 20) * 8)); - } - - /// Write the minimal `audioMuxVersion == 0` AAC-LC StreamMuxConfig - /// (one prog, one layer, frameLengthType 0, no CRC) into `w` - /// without finishing — for embedding inside an AudioMuxElement. - fn write_min_smc_into(w: &mut BitWriter) { - w.write_bit(false); // audioMuxVersion = 0 - w.write_bit(true); // allStreamsSameTimeFraming - w.write_u32(0, 6); // numSubFrames = 0 - w.write_u32(0, 4); // numProgram = 0 - w.write_u32(0, 3); // numLayer = 0 - write_aac_lc_asc(w); - w.write_u32(0, 3); // frameLengthType = 0 - w.write_u32(0xFF, 8); // latmBufferFullness - w.write_bit(false); // otherDataPresent - w.write_bit(false); // crcCheckPresent - } - - #[test] - fn audio_mux_element_in_band_single_payload() { - // muxConfigPresent=1, useSameStreamMux=0, inline minimal SMC, - // one subframe carrying a 4-byte payload. - let payload: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF]; - let mut w = BitWriter::new(); - w.write_bit(false); // useSameStreamMux = 0 - write_min_smc_into(&mut w); - // PayloadLengthInfo: MuxSlotLengthBytes = 4 (single byte, < 255). - w.write_u32(4, 8); - // PayloadMux: 4 payload bytes. - for &b in &payload { - w.write_byte(b); - } - // otherDataPresent was 0; ByteAlign() pads. - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let ame = AudioMuxElement::parse(&mut r, &bytes, true, None).unwrap(); - assert!(!ame.use_same_stream_mux); - assert_eq!(ame.payloads.len(), 1); - let p = &ame.payloads[0]; - assert_eq!(p.sub_frame, 0); - assert_eq!(p.prog, 0); - assert_eq!(p.lay, 0); - assert_eq!(p.stream_id, 0); - assert_eq!(p.data, payload.to_vec()); - } - - #[test] - fn audio_mux_element_escape_length() { - // MuxSlotLengthBytes with one 0xFF escape: 255 + 3 = 258 bytes. - let len = 258usize; - let payload: Vec = (0..len).map(|i| (i & 0xFF) as u8).collect(); - let mut w = BitWriter::new(); - w.write_bit(false); // useSameStreamMux - write_min_smc_into(&mut w); - w.write_u32(255, 8); // escape - w.write_u32(3, 8); // + 3 = 258 - for &b in &payload { - w.write_byte(b); - } - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let ame = AudioMuxElement::parse(&mut r, &bytes, true, None).unwrap(); - assert_eq!(ame.payloads.len(), 1); - assert_eq!(ame.payloads[0].data, payload); - } - - #[test] - fn audio_mux_element_use_same_stream_mux_inherits() { - // First element carries the config; second sets - // useSameStreamMux and inherits it. - let mut w0 = BitWriter::new(); - w0.write_bit(false); // useSameStreamMux = 0 - write_min_smc_into(&mut w0); - w0.write_u32(2, 8); // 2-byte payload - w0.write_byte(0x11); - w0.write_byte(0x22); - let bytes0 = w0.finish(); - let mut r0 = BitReader::new(&bytes0); - let first = AudioMuxElement::parse(&mut r0, &bytes0, true, None).unwrap(); - - let mut w1 = BitWriter::new(); - w1.write_bit(true); // useSameStreamMux = 1 - w1.write_u32(3, 8); // 3-byte payload - w1.write_byte(0xAA); - w1.write_byte(0xBB); - w1.write_byte(0xCC); - let bytes1 = w1.finish(); - let mut r1 = BitReader::new(&bytes1); - let second = AudioMuxElement::parse(&mut r1, &bytes1, true, Some(&first.config)).unwrap(); - assert!(second.use_same_stream_mux); - assert_eq!(second.payloads.len(), 1); - assert_eq!(second.payloads[0].data, vec![0xAA, 0xBB, 0xCC]); - } - - #[test] - fn audio_mux_element_use_same_without_prev_rejected() { - let mut w = BitWriter::new(); - w.write_bit(true); // useSameStreamMux = 1, but no prev config - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(matches!( - AudioMuxElement::parse(&mut r, &bytes, true, None), - Err(Error::LatmNoPreviousMuxConfig) - )); - } - - #[test] - fn audio_mux_element_multiple_subframes() { - // numSubFrames = 1 -> two PayloadMux frames, each a separate - // PayloadLengthInfo + payload. - let mut w = BitWriter::new(); - w.write_bit(false); // useSameStreamMux - // StreamMuxConfig with numSubFrames = 1. - w.write_bit(false); // audioMuxVersion = 0 - w.write_bit(true); // allStreamsSameTimeFraming - w.write_u32(1, 6); // numSubFrames = 1 - w.write_u32(0, 4); // numProgram = 0 - w.write_u32(0, 3); // numLayer = 0 - write_aac_lc_asc(&mut w); - w.write_u32(0, 3); // frameLengthType = 0 - w.write_u32(0xFF, 8); // latmBufferFullness - w.write_bit(false); // otherDataPresent - w.write_bit(false); // crcCheckPresent - // subframe 0: 2 bytes. - w.write_u32(2, 8); - w.write_byte(0x01); - w.write_byte(0x02); - // subframe 1: 1 byte. - w.write_u32(1, 8); - w.write_byte(0x03); - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let ame = AudioMuxElement::parse(&mut r, &bytes, true, None).unwrap(); - assert_eq!(ame.payloads.len(), 2); - assert_eq!(ame.payloads[0].sub_frame, 0); - assert_eq!(ame.payloads[0].data, vec![0x01, 0x02]); - assert_eq!(ame.payloads[1].sub_frame, 1); - assert_eq!(ame.payloads[1].data, vec![0x03]); - } - - /// Build the byte body of a minimal in-band AudioMuxElement(1) - /// carrying `payload` (one subframe, frameLengthType 0). The - /// returned bytes are exactly the `audioMuxLengthBytes` body that a - /// LOAS frame wraps. - fn build_min_audio_mux_element(payload: &[u8]) -> Vec { - let mut w = BitWriter::new(); - w.write_bit(false); // useSameStreamMux = 0 - write_min_smc_into(&mut w); - // MuxSlotLengthBytes for payload.len() (< 255). - assert!(payload.len() < 255); - w.write_u32(payload.len() as u32, 8); - for &b in payload { - w.write_byte(b); - } - w.finish() - } - - #[test] - fn audio_sync_stream_single_frame() { - let payload: [u8; 5] = [0x21, 0x00, 0x03, 0x40, 0x80]; - let body = build_min_audio_mux_element(&payload); - - // AudioSyncStream frame: 0x2B7 (11 bits) + audioMuxLengthBytes - // (13 bits) + body. 11 + 13 = 24 bits = 3 bytes, so the body is - // byte-aligned. - let mut w = BitWriter::new(); - w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); - w.write_u32(body.len() as u32, 13); - w.write_bytes(&body); - let stream = w.finish(); - - let mut walker = AudioSyncStream::new(&stream); - let frame = walker.next_frame().unwrap().unwrap(); - assert_eq!(frame.offset, 0); - assert_eq!(usize::from(frame.audio_mux_length_bytes), body.len()); - assert_eq!(frame.element.payloads.len(), 1); - assert_eq!(frame.element.payloads[0].data, payload.to_vec()); - // No more frames. - assert!(walker.next_frame().unwrap().is_none()); - } - - #[test] - fn audio_sync_stream_skips_leading_garbage() { - let payload: [u8; 2] = [0xAB, 0xCD]; - let body = build_min_audio_mux_element(&payload); - let mut w = BitWriter::new(); - w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); - w.write_u32(body.len() as u32, 13); - w.write_bytes(&body); - let frame_bytes = w.finish(); - - // Prepend non-syncword garbage bytes. - let mut stream = vec![0x00, 0xAA, 0x55]; - stream.extend_from_slice(&frame_bytes); - - let mut walker = AudioSyncStream::new(&stream); - let frame = walker.next_frame().unwrap().unwrap(); - assert_eq!(frame.offset, 3); - assert_eq!(frame.element.payloads[0].data, payload.to_vec()); - } - - #[test] - fn audio_sync_stream_two_frames_via_iterator() { - let p0: [u8; 2] = [0x10, 0x20]; - let p1: [u8; 3] = [0x30, 0x40, 0x50]; - - let build = |payload: &[u8]| { - // First frame carries config inline; second uses - // useSameStreamMux to inherit it. - let body = build_min_audio_mux_element(payload); - let mut w = BitWriter::new(); - w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); - w.write_u32(body.len() as u32, 13); - w.write_bytes(&body); - w.finish() - }; - - let mut stream = build(&p0); - // Second frame: useSameStreamMux = 1 body. - let body1 = { - let mut w = BitWriter::new(); - w.write_bit(true); // useSameStreamMux = 1 - w.write_u32(p1.len() as u32, 8); // MuxSlotLengthBytes - for &b in &p1 { - w.write_byte(b); - } - w.finish() - }; - let mut w1 = BitWriter::new(); - w1.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); - w1.write_u32(body1.len() as u32, 13); - w1.write_bytes(&body1); - stream.extend_from_slice(&w1.finish()); - - let frames: Vec<_> = AudioSyncStream::new(&stream) - .collect::>>() - .unwrap(); - assert_eq!(frames.len(), 2); - assert_eq!(frames[0].element.payloads[0].data, p0.to_vec()); - assert_eq!(frames[1].element.payloads[0].data, p1.to_vec()); - // The second frame inherited the first frame's config. - assert!(frames[1].element.use_same_stream_mux); - } - - #[test] - fn audio_sync_stream_truncated_body_rejected() { - let payload: [u8; 4] = [0x01, 0x02, 0x03, 0x04]; - let body = build_min_audio_mux_element(&payload); - let mut w = BitWriter::new(); - w.write_u32(AUDIO_SYNC_STREAM_SYNCWORD, 11); - // Claim a longer body than is present. - w.write_u32((body.len() + 10) as u32, 13); - w.write_bytes(&body); - let stream = w.finish(); - - let mut walker = AudioSyncStream::new(&stream); - assert!(matches!(walker.next_frame(), Err(Error::LoasSyncInvalid))); - } - - #[test] - fn ep_audio_sync_header_parse() { - // 0x4DE1 (16) + futureUse(4)=0x5 + audioMuxLengthBytes(13)=100 - // + frameCounter(5)=7 + headerParity(18)=0x12345. - let mut w = BitWriter::new(); - w.write_u32(EP_AUDIO_SYNC_STREAM_SYNCWORD, 16); - w.write_u32(0x5, 4); - w.write_u32(100, 13); - w.write_u32(7, 5); - w.write_u32(0x12345, 18); - // A few body bytes (not parsed). - w.write_bytes(&[0xAA, 0xBB]); - let stream = w.finish(); - - let hdr = EpAudioSyncHeader::parse(&stream, 0).unwrap().unwrap(); - assert_eq!(hdr.offset, 0); - assert_eq!(hdr.future_use, 0x5); - assert_eq!(hdr.audio_mux_length_bytes, 100); - assert_eq!(hdr.frame_counter, 7); - assert_eq!(hdr.header_parity, 0x12345); - assert_eq!(hdr.body_offset, 7); - } - - #[test] - fn ep_audio_sync_header_not_found() { - let stream = [0x00u8, 0x11, 0x22, 0x33]; - assert!(EpAudioSyncHeader::parse(&stream, 0).unwrap().is_none()); - } -} diff --git a/crates/vendor/oxideav-aac/src/lib.rs b/crates/vendor/oxideav-aac/src/lib.rs deleted file mode 100644 index 19e83f87..00000000 --- a/crates/vendor/oxideav-aac/src/lib.rs +++ /dev/null @@ -1,641 +0,0 @@ -//! # oxideav-aac -//! -//! Pure-Rust AAC (Advanced Audio Coding) parsing — currently **Phase 1** -//! of the post-r111 orphan-rebuild lineage. Decode and encode bodies are -//! *not* wired up yet; this crate's public surface is limited to: -//! -//! * The [`adts`] module — ISO/IEC 13818-7 §1.A.2 *Audio Data Transport -//! Stream* fixed-header parser (sync, profile, sampling-frequency -//! index, channel configuration, frame length, raw-data-block count, -//! CRC presence flag). -//! * The [`asc`] module — ISO/IEC 14496-3 §1.6.2.1 *AudioSpecificConfig* -//! parser, including the §4.4.1 *GASpecificConfig* body for all -//! General Audio audio-object types (AOTs 1, 2, 3, 4, 6, 7, 17, 19, -//! 20, 21, 22, 23) and the hierarchical SBR (AOT 5) / PS (AOT 29) -//! outer-wrapper unwrap. Embeds an inline -//! [`pce::Pce`](pce::Pce) when `channelConfiguration == 0`. -//! **Round 177** extends the GA body with the `extensionFlag == 1` -//! subtree (Table 4.1: AOT 22's `numOfSubFrame` + `layer_length`; -//! the AOT-17 / 19 / 20 / 23 resilience triplet; the -//! always-present `extensionFlag3` tail bit) and the Table 1.15 -//! trailing `epConfig` 2-bit field for every ER AOT in -//! {17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39}. `epConfig == 2` -//! or `3` (which mandate `ErrorProtectionSpecificConfig()` parsing) -//! surface as [`Error::UnsupportedEpConfig`]; an `extensionFlag3 -//! == 1` body — whose layout is reserved by the spec — surfaces as -//! [`Error::UnsupportedAscExtensionFlag3`]. -//! **Round 192** adds the Table 1.15 trailing -//! `syncExtensionType == 0x2b7` implicit-SBR probe (§1.6.5): -//! when the outer AOT is **not** the explicit SBR (5) or PS (29) -//! wrapper and the carrier has at least 16 bits remaining, -//! [`AudioSpecificConfig::parse`] now reads an 11-bit -//! `syncExtensionType` field. On a `0x2b7` match it consumes the -//! nested `GetAudioObjectType()` plus either the SBR branch -//! (`sbrPresentFlag`, optional `extensionSamplingFrequencyIndex`, -//! then a second 11-bit `syncExtensionType == 0x548` gating a -//! 1-bit `psPresentFlag`) or the BSAC branch (`sbrPresentFlag`, -//! optional `extensionSamplingFrequencyIndex`, mandatory 4-bit -//! `extensionChannelConfiguration`). The probe result is exposed -//! as [`asc::AudioSpecificConfig::trailing_sbr_probe`] and the -//! implicitly-signalled SBR / PS / extension-sample-rate values -//! are also propagated to the top-level `sbr_present` / -//! `ps_present` / `extension_sampling_frequency_index` / -//! `extension_sample_rate` / `extension_channel_configuration` -//! fields. A carrier-bounded entry point -//! [`asc::AudioSpecificConfig::parse_bits_bounded`] is exposed so -//! LATM `StreamMuxConfig` (and any future esds AudioObj -//! descriptor) callers can pass the exact ASC bit length; -//! [`asc::AudioSpecificConfig::parse_bits`] preserves its -//! no-probe semantics for callers that hold a `BitReader` -//! carrying trailing carrier bytes. -//! * The [`pce`] module — ISO/IEC 14496-3 §4.4.1.1 *program_config_element* -//! parser. Used both standalone (inside [`raw_data_block`]) and inline -//! inside [`asc`]. -//! * The [`raw_data_block`] module — ISO/IEC 14496-3 §4.4.2.1 syntactic -//! *raw_data_block()* walker that visits each `id_syn_ele` in order -//! and stops cleanly at `END (0b111)`. Per-element bodies for -//! SCE / CPE / CCE / LFE are **not** parsed yet — the walker emits an -//! element-header event and the consumer is responsible for -//! advancing the bit-reader past the body (subsequent rounds will -//! internalise this). PCE is fully parsed. **Round 160** added the -//! matching encoder-side [`raw_data_block::FrameAssembler`] — the -//! bit-exact inverse, with a typed push-API -//! (`push_channel_header` / `push_channel_body_bits` / `push_fill` / -//! `push_data` / `push_pce` / `push_end`) that composes the existing -//! per-tool writers (`IcsInfo::write`, `SectionData::write`, …) into -//! a complete byte stream. **Round 165** adds -//! [`pce::Pce::write`] (the bit-exact inverse of the round-126 -//! `Pce::parse`) and the matching -//! [`raw_data_block::FrameAssembler::push_pce`] entry point, closing -//! the last per-element writer gap in the `raw_data_block()` frame -//! assembler. -//! * The [`ics_info`] module — ISO/IEC 14496-3 §4.4.6 / Table 4.6 -//! *ics_info()* parser. The first piece of Phase 2 -//! (channel-element body parsing) — surfaces the window-sequence / -//! shape, `max_sfb`, `scale_factor_grouping`, the Main predictor -//! side-info (AOT 1), and the LTP `ltp_data()` body -//! (Table 4.55) when the wire bit selects it, plus the -//! §4.5.2.3.4 derivations (`num_windows`, `num_window_groups`, -//! `window_group_length[]`, `num_swb`). **Round 140** added the -//! matching `IcsInfo::write` encoder primitive (and a public -//! `write_ltp_data` helper) — the second encode-side syntax-element -//! writer in the crate. Self-roundtrip (`write` → `parse`) is -//! bit-perfect across every branch the parser handles, including -//! the Main predictor + Table 4.55 LTP body for both the non-LD -//! and the ER-AAC-LD forms. -//! * The [`section_data`] module — ISO/IEC 14496-3 §4.4.6 / ISO/IEC -//! 13818-7 §6.3 Table 17 *section_data()* parser, **plus** (round -//! 137) the matching `SectionData::write` encoder primitive. The -//! parser assigns a Huffman codebook (`sect_cb`) to each run of -//! scalefactor bands per window group via run-length escape -//! coding, building the per-group `sfb_cb[g][sfb]` map that -//! `scale_factor_data()` (next round) consumes. The encoder is its -//! inverse: given the same `(window_sequence, max_sfb)` context it -//! emits a bit-exact Table 17 stream. Self-roundtrip -//! (`write` → `parse`) is bit-perfect across the long, EIGHT_SHORT, -//! single-escape, double-escape, and exact-multiple-of-`sect_esc_val` -//! branches. No Huffman decode yet — every field is fixed-width. -//! * The [`pulse_data`] module — ISO/IEC 14496-3 §4.4.6.3 / Table 4.7 -//! *pulse_data()* parser **and** encoder primitive (**new in round -//! 142**). The parser reads the 2-bit `number_pulse`, 6-bit -//! `pulse_start_sfb`, and `number_pulse + 1` `(5-bit pulse_offset, -//! 4-bit pulse_amp)` records into [`pulse_data::PulseData`]; the -//! writer serialises the same structure back bit-for-bit. Every -//! field is fixed-width — no Huffman tables, no `swb_offset` -//! dependence, and no surrounding-element state. The §4.6.13 -//! reconstruction loop (`k += swb_offset[pulse_start_sfb] + -//! pulse_offset[j]; x_quant[…] ±= pulse_amp[j]`) is **not** -//! performed; it needs `swb_offset_long_window[]` and the -//! post-Huffman `x_quant` array that arrive with `spectral_data()`. -//! * The [`scale_factor_data`] module — ISO/IEC 14496-3 §4.4.6 / -//! Table 4.53 (non-resilient branch) plus §4.6.3 / Table 4.A.1 -//! *scale_factor_data()* parser **and** encoder primitive -//! (round 149, the fifth encode-side syntax-element writer in -//! the crate). Carries the AAC scalefactor Huffman codebook -//! (codebook 12) — 121 entries indexed `0..=120` with -//! `index_offset = -60`, producing DPCM deltas in `-60..=+60`. The -//! parser walks the per-`(g, sfb)` non-`ZERO_HCB` subsequence -//! driven by [`section_data::SectionData::sfb_cb`] and dispatches -//! between `hcod_sf[]` (ordinary spectrum / PNS-after-first / both -//! intensity codebooks) and the 9-bit `dpcm_noise_nrg` PCM seed -//! (first PNS band of the frame). The writer serialises the same -//! structure back bit-for-bit and validates the in-memory record -//! variants against the codebook map. -//! -//! **Round 152** adds the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM -//! accumulator pair [`scale_factor_data::accumulate`] (decoder -//! side) / [`scale_factor_data::differentiate`] (encoder side) -//! that converts between transmitted DPCM deltas and absolute -//! per-band quantities. Three independent tracks: spectrum -//! scalefactors (seed `last_sf = global_gain`, range `0..=255`), -//! intensity stereo positions (seed `last_is = 0`), and PNS noise -//! energies (seed `last_nrg = global_gain - NOISE_OFFSET - 256`, -//! first PNS band carries a 9-bit `uimsbf` literal). The §4.4.6 -//! error-resilient branch (`aacScalefactorDataResilienceFlag == -//! 1`, RVLC with `rev_global_gain`, `sf_concealment`, -//! `length_of_rvlc_sf`) is still **not** implemented; ER AAC-LD / -//! scalable profiles that flip the resilience flag will need a -//! sibling `scale_factor_data_rvlc()` module. -//! * The [`tns_data`] module — ISO/IEC 14496-3 §4.4.6 / Table 4.54 -//! *tns_data()* parser **and** encoder primitive (**new in round -//! 146**). The parser walks every transform window of the -//! surrounding `window_sequence` and reads `n_filt[w]` -//! (1 or 2 bits per Table 4.155), an optional `coef_res[w]` -//! (when `n_filt[w] > 0`), then per-filter `length` (4 or 6 bits), -//! `order` (3 or 5 bits), and — when `order > 0` — `direction`, -//! `coef_compress`, and `order` × `coef[i]` magnitudes whose width -//! is `(3 + coef_res) − coef_compress` per §4.6.9.3. The writer -//! serialises the same structure back bit-for-bit. The §4.6.9.3 -//! `tns_decode_coef` LPC reconstruction (signed conversion, -//! `iqfac` arcsine inverse-quantisation, Levinson-style conversion -//! to LPC) lives in [`tns_coef`], as does the §4.6.9.3 -//! `tns_ar_filter` all-pole pass over a strided spectrum region. -//! What remains owed is the §4.6.9 `tns_decode_frame` orchestration -//! that slices the per-window spectrum by `swb_offset` / -//! `direction` / `length` and dispatches the filter — that walker -//! belongs with the per-AOT IMDCT reconstruction driver. -//! * The [`gain_control_data`] module — ISO/IEC 14496-3 §4.4.6.5 / -//! Table 4.12 *gain_control_data()* parser **and** encoder -//! primitive (**new in round 183**). Carries the SSR (AOT 3) -//! PQF-band gain-control ladder: 2-bit `max_band`, then for each -//! `bd ∈ 1..=max_band` a per-window `(3-bit adjust_num) + -//! adjust_num × (4-bit alevcode + W(seq, wd)-bit aloccode)` ladder -//! with the per-`window_sequence` window count `N ∈ {1, 2, 8, 2}` -//! and the per-`(seq, wd)` `aloccode` width table from Table 4.12 -//! (5 / 4-2 / 2 / 4-5). The §4.6.12 ladder-application loop that -//! reconstructs sample-domain attenuation factors is **not** -//! performed; it needs the SSR PQF / IMDCT back-end. -//! * The [`swb_offset`] module — ISO/IEC 14496-3 §4.5.4.1 / Tables -//! 4.129–4.141 *swb_offset_long_window[]* and -//! *swb_offset_short_window[]* lookup tables, **new in round 194**. -//! The per-band lowest-coefficient index for each of the 12 valid -//! `samplingFrequencyIndex` values is exposed as -//! [`swb_offset::SWB_OFFSET_LONG_WINDOW`] (each slot -//! `num_swb + 1` entries with trailing 1024 sentinel) and -//! [`swb_offset::SWB_OFFSET_SHORT_WINDOW`] (each slot -//! `num_swb + 1` entries with trailing 128 sentinel). Public -//! accessors [`swb_offset::long_window_offsets`] and -//! [`swb_offset::short_window_offsets`] bounds-check -//! `fs_index`. [`swb_offset::apply_pulse_data`] applies the -//! §4.6.13 pulse-escape reconstruction to a long-window -//! `x_quant` slice — the first reconstruction-layer entry point in -//! the crate, consuming a parsed [`pulse_data::PulseData`] block -//! and folding the `±pulse_amp` fix-up into the quantised -//! spectrum at the running coefficient index `k = swb_offset[fs][ -//! pulse_start_sfb] + Σ pulse_offset[i]`. The 960-line frame -//! variant (Tables 4.142–4.147) is **not** covered. -//! * The [`tns_max`] module — ISO/IEC 14496-3 §4.6.9.4 Tables -//! 4.102 / 4.103 decoder-side `TNS_MAX_ORDER` / `TNS_MAX_BANDS` -//! clamp tables and §4.6.17.2.5 Tables 4.119 / 4.120 LD-specific -//! `TNS_MAX_BANDS` tables, **new in round 200**. The accessors -//! [`tns_max::tns_max_order`] and [`tns_max::tns_max_bands`] -//! surface the per-AOT / per-window-sequence / per-`fs_index` -//! caps; [`tns_max::tns_max_bands_ld_480`] and -//! [`tns_max::tns_max_bands_ld_512`] handle the LD frame-size -//! split. The clamp helpers [`tns_max::clamp_tns_order`] and -//! [`tns_max::clamp_tns_band`] fold the §4.6.9.3 three-way -//! `min(band, TNS_MAX_BANDS, max_sfb)` and -//! `min(order, TNS_MAX_ORDER)` pseudocode into one call so the -//! eventual TNS reconstruction layer can consume them without -//! re-deriving the AOT dispatch. The Table 4.103 dispatch splits -//! AOT 3 (AAC SSR) into the PQF-filterbank columns; every other -//! AOT uses the non-PQF columns. -//! * The [`ics_body`] module — ISO/IEC 14496-3 §4.4.6 / Table 4.50 -//! `individual_channel_stream()` body walker, **new in round 207**. -//! Composes the existing per-tool parsers / writers (`global_gain`, -//! [`ics_info`], [`section_data`], [`scale_factor_data`], optional -//! [`pulse_data`] / [`tns_data`] / [`gain_control_data`]) into the -//! complete Table 4.50 channel-element body, **up to but not -//! including** `spectral_data()`. Surfaces the parsed structure plus -//! the `spectral_data_bit_offset` so the caller (e.g. a future -//! spectrum parser, or a frame-assembler that hands off the -//! spectrum-bit-slice via `push_channel_body_bits`) can resume the -//! walk at the right boundary. The shared-info `CPE` form -//! ([`ics_body::IcsBody::parse_with_ics_info`] / -//! [`ics_body::IcsBody::write_with_ics_info`]) accepts the -//! externally-held [`ics_info::IcsInfo`] for the per-channel body. -//! Table 4.50 Note 1's "pulse_data illegal on -//! `EIGHT_SHORT_SEQUENCE`" and the §4.6.12 "gain_control_data is -//! AOT-3 (SSR) only" normative constraints are enforced on the -//! writer side; the parser surfaces literal bits to keep hostile -//! streams from panicking. `scale_flag == true` (scalable AAC, AOT -//! 6) rejects with [`Error::NotImplemented`]. -//! * The [`spectral_codebook`] module — ISO/IEC 14496-3 §4.6.3.1 / -//! Table 4.95 Spectrum Huffman codebook parameter table plus the -//! §4.6.3.3 codeword-index → spectral-tuple translation, the -//! §4.6.3.3 sign-bit fix-up, and the §4.6.3.3 ESC sequence handler -//! for codebook 11 (and the extension books 16..=31), **new in -//! round 213**. `TABLE_4_95: [Table495Row; 32]` carries the four -//! normative columns (`unsigned_cb`, `dimension`, `lav`, -//! `esc_threshold`) for every codebook in `0..=31`; `table_4_95` -//! is the safe accessor. -//! [`spectral_codebook::decode_index_to_tuple`] is the §4.6.3.3 -//! pseudocode that translates a Huffman codeword index `idx` to a -//! `dim`-tuple of quantised spectral coefficients; -//! [`spectral_codebook::encode_tuple_to_index`] is its inverse. -//! The sign-bit fix-up -//! [`spectral_codebook::apply_sign_bits`] / -//! [`spectral_codebook::derive_sign_bits`] folds the -//! per-non-zero-coefficient sign bits the spec emits after an -//! unsigned-codebook codeword onto / from a signed tuple. The -//! ESC sequence [`spectral_codebook::decode_esc_value`] / -//! [`spectral_codebook::encode_esc_value`] expands codebook-11 -//! coefficients at the LAV cap into the §4.6.3.3 escape sequence -//! (`2^(N + 4) + escape_word`, capped at -//! [`spectral_codebook::MAX_QUANT`] = 8191 per §4.6.1.3). The -//! Huffman tables themselves (Tables 4.A.3 through 4.A.12) are -//! still owed — see [`spectrum_huffman`] for the first one. The -//! §4.4.6 `spectral_data()` wire walker that loops over -//! scalefactor bands and dispatches on the per-band codebook is -//! also **not** wired up; this module is the per-codeword -//! translation layer it will sit on top of. -//! * The [`spectrum_huffman`] module — the **wire layer** for the -//! §4.6.3 / Annex 4.A Huffman codebooks (**new in round 219**). -//! Round 219 landed the first of the eleven spectrum books: -//! **Table 4.A.2** (Spectrum Huffman Codebook 1, signed 4-tuple, -//! `LAV = 1`, 81 entries indexed `0..=80`, maximum codeword -//! length 11 bits; the zero-tuple at index 40 carries the -//! single-bit codeword `0`). Round 226 added Codebook 2 -//! (Table 4.A.3, same signed 4-tuple universe, 9-bit max), round -//! 231 added Codebook 3 (Table 4.A.4, the first **unsigned** book, -//! `LAV = 2`, 16-bit max; the zero magnitude tuple migrates to -//! index 0). Round 234 added Codebook 4 (Table 4.A.5, the second -//! unsigned dim-4 book, 12-bit max; the shortest codeword -//! `0b0000` parks at index 40 while index 0 carries a 4-bit -//! `0b0111`). Round 238 adds Codebook 5 (Table 4.A.6, the first -//! **pair** book: `unsigned = 0`, `dim = 2`, `LAV = 4` → `9^2 = -//! 81` entries, 13-bit max; the §4.6.3.3 polynomial puts the -//! zero-tuple `(0, 0)` at the centre index 40 — also the location -//! of the single-bit `0` shortest codeword — while the four -//! `(±4, ±4)` lattice corners take the four 13-bit codewords at -//! indices 0 / 8 / 72 / 80). Public API per book: -//! `HCODN_NUM_ENTRIES` = 81, -//! `HCODN_MAX_LEN` (codebook-specific), `hcodN_encode(idx) -> -//! (length, codeword)` (right-aligned in `u16`), `hcodN_decode` -//! reads MSB-first from a [`oxideav_core::bits::BitReader`] and -//! returns the codeword index, and `hcodN_write` is a convenience -//! wrapper over the encode + writer-emit pair. Every book is a -//! complete prefix code over `HCODN_MAX_LEN` bits, exhaustively -//! verified at unit-test time. Round 250 added Codebook 8 -//! (Table 4.A.9, the second **unsigned pair** book sharing -//! Codebook 7's `unsigned = 1`, `dim = 2`, `LAV = 7` → 64-entry -//! universe; 10-bit max; the zero-tuple at index 0 carries a -//! 5-bit `0b01110` and the shortest 3-bit `0` codeword migrates -//! to the interior tuple `(1, 1)` at index 9). Codebooks 9..=11 -//! (Tables 4.A.10 … 4.A.12) reuse the same module shape and are -//! owed in subsequent rounds; the `spectral_data()` driver that -//! dispatches per-band onto the chosen codebook arrives once all -//! eleven are in place. -//! * The [`dequant`] module — ISO/IEC 14496-3 §4.6.1.3 inverse -//! quantization (`Sign(x_quant) · |x_quant|^(4/3)`) and §4.6.2.3.3 -//! scalefactor application (`gain = 2^(0.25 · (sf − SF_OFFSET))`, -//! `SF_OFFSET = 100`), **new in round 284** — the first numeric -//! reconstruction stage after the wire walk. -//! [`dequant::rescale_spectrum`] applies both band-wise over the -//! §4.5.2.3.4 `sect_sfb_offset` ranges in the §4.5.2.3.5 -//! interleaved transmission order. -//! * The [`decoded_spectrum`] module — the §4.6.3.3 -//! `quant_to_spec()` de-interleaver (transmission order → -//! window-major `spec[w][k]`) and -//! [`decoded_spectrum::decode_channel_spectrum`], the per-channel -//! pipeline stage (pulse fix-up → scalefactor accumulation → -//! inverse quantization + rescaling → de-interleave → TNS), -//! **new in round 284**. Ends one step short of the §4.6.11 -//! filterbank. -//! * The [`extension_payload`] module — ISO/IEC 14496-3 §4.4.2.7 / -//! Table 4.51 *extension_payload()* parser **and** encoder -//! primitive (**new in round 187**). Implements the three -//! non-SBR `extension_type` branches whose body layouts are -//! fully specified by fixed-width fields: `EXT_FILL` (`0b0000`) -//! — the Table 4.51 default branch surfacing the -//! `8 * (cnt - 1) + 4` `other_bits` as a packed byte buffer; -//! `EXT_FILL_DATA` (`0b0001`) — the normative-pattern filler -//! with `fill_nibble == 0b0000` and `fill_byte == -//! 0b1010_0101`; and `EXT_DYNAMIC_RANGE` (`0b1011`) — the -//! Table 4.52 `dynamic_range_info()` block (optional -//! `pce_instance_tag`, optional Table 4.53 `excluded_channels()` -//! exclude-mask list, optional per-band partitioning, optional -//! `prog_ref_level`, and per-band `(dyn_rng_sgn, dyn_rng_ctl)` -//! records). The two SBR-data values from ISO/IEC 13818-7 -//! Table 40 (`EXT_SBR_DATA` `0b1101` and `EXT_SBR_DATA_CRC` -//! `0b1110`) surface as [`Error::UnsupportedExtensionSbr`] — -//! their bodies are `sbr_extension_data()` which needs the QMF / -//! patching back-end this crate does not yet provide. The -//! §4.5.2.13 DRC companding-curve application is **not** -//! performed; the raw `(dyn_rng_sgn, dyn_rng_ctl)` records are -//! surfaced verbatim for a later round. -//! -//! The decode path is fully wired: [`register`] installs an AAC -//! [`Decoder`](oxideav_core::Decoder) (id `"aac"`) via the -//! [`codec_decoder`] module, adapting the [`decode::StreamDecoder`] into -//! the framework's packet-in / frame-out trait. The encode path still -//! has no rate-control back-end — the bit-exact wire writers exist but -//! no `Encoder` is registered. -//! -//! ## Provenance -//! -//! Every numeric -//! constant, bit layout, and clause reference in this crate is sourced -//! from the staged ISO/IEC 13818-7 and ISO/IEC 14496-3 PDFs under -//! `docs/audio/aac/`. The fixture descriptions in -//! `docs/audio/aac/aac-fixtures-and-traces.md` were consulted as a -//! cross-reference against the spec wording. -//! -//! ## Status (Phase 1 + Phase 2 begin) -//! -//! * ADTS fixed header parsing: **complete** (sync + 7-byte body). -//! * ADTS CRC validation: deferred; the parser surfaces the -//! `protection_absent` flag but does not validate the trailing -//! 16-bit CRC when present. -//! * `raw_data_block()` walker: iterates `id_syn_ele` and stops at -//! `END`; FIL / DSE / PCE bodies are fully consumed. SCE / CPE / -//! CCE / LFE bodies now compose through the new [`ics_body`] -//! walker (Table 4.50): `global_gain` → [`ics_info`] → -//! [`section_data`] → [`scale_factor_data`] → optional -//! [`pulse_data`] / [`tns_data`] / [`gain_control_data`]. The -//! trailing channel-stream tool, Table 4.56 `spectral_data()`, is -//! covered by the [`spectral_data`] walker: `ics_body` surfaces -//! the start bit-offset and [`spectral_data::SpectralData::parse`] -//! consumes the spectrum from that position, completing the -//! Table 4.50 body. Driving that pair from the `raw_data_block()` -//! walker (plus the CPE `common_window` / `ms_mask_present` -//! header) is the remaining wiring; the `tests/docs_adts_corpus.rs` -//! driver demonstrates the full composition over the staged ADTS -//! fixture corpus. -//! * Numeric reconstruction (round 284): a parsed channel body now -//! decodes to a window-major real-valued spectrum via -//! [`decoded_spectrum::decode_channel_spectrum`] — §4.6.3.3 pulse -//! fix-up, §4.6.2.3.2 scalefactor accumulation, §4.6.1.3 inverse -//! quantization, §4.6.2.3.3 rescaling, §4.6.3.3 `quant_to_spec()`, -//! §4.6.9 TNS. The §4.6.11 filterbank (round 289) turns that -//! spectrum into PCM-domain samples. M/S (§4.6.8.1) stereo -//! reconstruction is [`ms_stereo::apply_ms_stereo`] (round 293), a -//! CPE-level de-matrix over the channel pair before TNS. Intensity -//! stereo (§4.6.8.2) reconstruction is -//! [`intensity_stereo::apply_intensity_stereo`] (round 300), the -//! deterministic left→right derivation -//! `r = is_intensity·invert_intensity·0.5^(0.25·is_pos)·l` that runs -//! after M/S and before TNS. PNS (§4.6.13) synthesis is -//! [`pns::apply_pns`] / [`pns::apply_pns_pair`] (round 307), the -//! noise-band fill `scale = 2^(0.25·noise_nrg)/sqrt(Σ spec²)` whose -//! per-band L2 norm is the spec-determined `2^(0.25·noise_nrg)` (only -//! the per-coefficient phase is RNG-defined, so the band energy — not -//! the exact samples — is byte-exact). The §4.6 element-level decode -//! driver [`element_decode::ElementDecoder`] (round 311) chains the -//! whole stack per channel element: `decode_sce` for SCE / LFE and -//! `decode_cpe` for a CPE run pulse → dequant → `quant_to_spec()` → -//! M/S → intensity → PNS → TNS → §4.6.11 filterbank to PCM, carrying -//! the per-channel overlap-add tail across frames. The stream-level -//! [`decode::StreamDecoder`] walks the §4.4.2.1 `raw_data_block()` -//! above that driver and renders to element-order interleaved 16-bit -//! PCM via the §4.6.11 [`pcm`] output stage (the §1.3 `NINT()` -//! round-half-away-from-zero + saturation). The decoded PCM is -//! validated against the staged `expected.wav` corpus: the two -//! PNS-free ADTS fixtures are 99.9 % byte-exact (max error 1 LSB — -//! the residual is the `f64` direct-sum vs a `float32` fast-transform -//! IMDCT difference), and the PNS-bearing fixtures match in the PCM -//! RMS domain below 0.1 % error-to-signal (full byte-exactness is -//! precluded only by the §4.6.13.3 spec-undefined noise phase). - -#![warn(missing_debug_implementations)] -#![warn(missing_docs)] - -use oxideav_core::RuntimeContext; - -pub mod adts; -pub mod adts_crc; -pub mod asc; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod bsac_arith; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod bsac_decode; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod bsac_layer; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod bsac_tables; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod cce; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod channel_map; -pub mod codec_decoder; -pub mod codec_encoder; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod crc; -pub mod decode; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod decoded_spectrum; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod dequant; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod element_decode; -pub mod encoder; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod encoder_tns; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod extension_payload; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod filterbank; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod gain_control; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod gain_control_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod hcr; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod hcr_decode; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ics_body; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ics_info; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod intensity_stereo; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ipqf; -pub mod latm; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ltp; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ms_stereo; -pub mod pce; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod pcm; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod pns; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod predictor; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_decoder; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_decorr; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_huffman; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_hybrid; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_map; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ps_stereo; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod pulse_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod raw_data_block; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod rvlc; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_decoder; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_dequant; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_element; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_env_adjust; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_envelope; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_extension; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_freq_bands; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_grid; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_header; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_hf_gen; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_huffman; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_limiter; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_lp; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_noise_table; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_qmf; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_reconstruct; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod sbr_time_grid; -// internal — exposed for tests/fuzz; not part of the stable API -pub mod scalable; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ep_config; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ep_fec; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ep_rs; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ep_frame; -#[doc(hidden)] -pub mod scale_factor_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod section_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod spectral_codebook; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod spectral_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod spectrum_huffman; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ssr; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod ssr_filterbank; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod swb_offset; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod tns_coef; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod tns_data; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod tns_frame; -// internal — exposed for tests/fuzz; not part of the stable API -#[doc(hidden)] -pub mod tns_max; - -mod error; - -pub use error::Error; - -/// Result alias used throughout the crate. -pub type Result = core::result::Result; - -/// Codec-registry entry point. Installs the AAC -/// [`Decoder`](oxideav_core::Decoder) (id `"aac"`) — the ADTS-framed -/// AAC-LC decode chain wired through [`codec_decoder::register_codecs`], -/// claiming the MP4 object-type / WAVEFORMATEX / FourCC / Matroska tags -/// an AAC elementary stream is routed under. No encoder is wired yet -/// (the crate has the bit-exact wire writers but no rate-control -/// encoder back-end). -pub fn register(ctx: &mut RuntimeContext) { - codec_decoder::register_codecs(&mut ctx.codecs); -} - -oxideav_core::register!("aac", register); diff --git a/crates/vendor/oxideav-aac/src/ltp.rs b/crates/vendor/oxideav-aac/src/ltp.rs deleted file mode 100644 index caa6a30d..00000000 --- a/crates/vendor/oxideav-aac/src/ltp.rs +++ /dev/null @@ -1,976 +0,0 @@ -//! Long-Term Prediction (LTP) synthesis — ISO/IEC 14496-3 §4.6.7. -//! -//! LTP is a forward-adaptive, single-tap time-domain predictor that -//! reduces inter-frame redundancy for signals with a clear pitch. -//! Because the predictor coefficients are transmitted as side -//! information (`ltp_data()`, Table 4.55, parsed by -//! [`crate::ics_info::LtpData`]), the decoder applies the predictor -//! without the round-off sensitivity of the backward-adaptive MPEG-2 -//! frequency-domain predictor (§4.6.6). -//! -//! ## Scope of this module -//! -//! This module implements the §4.6.7.3 **long-window** decoding -//! process, which is the only window family LTP supports for the AAC -//! LTP audio object type (§4.6.7.1 restricts LTP to long windows for -//! bitstream compatibility with MPEG-2 AAC). The three long sequences -//! (`ONLY_LONG_SEQUENCE`, `LONG_START_SEQUENCE`, `LONG_STOP_SEQUENCE`) -//! are handled; `EIGHT_SHORT_SEQUENCE` is a no-op here (LTP is disabled -//! and the per-window predictors are reset, §4.6.7.3 / the short-block -//! reset note). -//! -//! The decode steps, transcribed from the §4.6.7.3 pseudo code: -//! -//! ```text -//! x_est = predict(); // 1-tap time-domain prediction -//! X_est = MDCT(x_est); // windowed analysis transform -//! for (sfb = 0; sfb < num_sfb; sfb++) -//! if (ltp_data_present && ltp_long_used[sfb]) -//! X_rec = X_est + Y_rec; // add predicted spectrum -//! else -//! X_rec = Y_rec; // pass the transmitted spectrum -//! ``` -//! -//! * `predict()` forms `x_est(i) = ltp_coef · x_rec(i − M − ltp_lag)`, -//! `i = 0 … N−1`, with `M = 0` for every non-LD AOT and `M = N/2` -//! for ER AAC LD (§4.6.7.3; the LD lag is 10-bit with the -//! `ltp_lag_update` repeat, §4.6.7.2). `x_rec` is the per-channel -//! reconstruction history (see [`LtpState`]). -//! * `MDCT(x_est)` windows `x_est` with the current frame's §4.6.11 -//! long window and applies the §4.6.15.3.3 analysis transform -//! ([`crate::filterbank::forward_mdct`]). -//! * `Y_rec` is the decoded (de-interleaved, inverse-quantised) -//! spectrum; `X_est + Y_rec` replaces it on the sfb that carry -//! `ltp_long_used == 1`. -//! -//! Per §4.6.7.4.1 (Figure 4.30) the LTP add precedes TNS synthesis in -//! the decode chain, so the spectrum passed in / out here is the -//! pre-TNS reconstructed spectrum. - -use crate::filterbank::{forward_mdct, long_only_window_family, short_window_j}; -use crate::ics_info::{IcsInfo, LtpData, WindowSequence, WindowShape}; -#[cfg(test)] -use crate::swb_offset::long_window_offsets; -#[cfg(test)] -use crate::swb_offset::LONG_WINDOW_LEN; -use crate::swb_offset::{short_window_offsets, FrameFamily, SHORT_WINDOW_LEN}; -use crate::Error; - -type Result = core::result::Result; - -/// The short transform length `N_s = 2 · 128 = 256` (§4.6.11.3.1). -const SHORT_TRANSFORM_LEN: usize = 2 * SHORT_WINDOW_LEN as usize; - -/// ISO/IEC 14496-3:2001 §4.6.7.3 — the number of scalefactor bands a -/// short-window LTP contribution covers ("for (sfb = 0; sfb < 8; -/// sfb++)": the first 8 SFBs of each predicted subwindow only). -pub const LTP_SHORT_MAX_SFB: usize = 8; - -/// Table 4.98 — the 8-entry LTP coefficient codebook. `ltp_coef` -/// (3 bits) indexes this table; the value is the single-tap predictor -/// gain applied in [`LtpState::predict_long`]. -pub const LTP_COEF: [f64; 8] = [ - 0.570829, 0.696616, 0.813004, 0.911304, 0.984900, 1.067894, 1.194601, 1.369533, -]; - -/// Map a 3-bit `ltp_coef` index to its Table 4.98 gain. -/// -/// Errors: [`Error::LtpInvalid`] if `index > 7`. -pub fn ltp_coefficient(index: u8) -> Result { - LTP_COEF - .get(index as usize) - .copied() - .ok_or(Error::LtpInvalid) -} - -/// Per-channel LTP reconstruction-history buffer (§4.6.7.3). -/// -/// The predictor reads `x_rec(i − M − ltp_lag)`; the buffer therefore -/// has to retain enough past output to cover the maximum lag -/// (`ltp_lag ≤ 2047`) plus the current transform window. The layout, -/// per §4.6.7.3: -/// -/// * `x_rec(0 … N/2 − 1)` — the last aliased half window from the -/// current frame's IMDCT (the pre-overlap-add windowed tail); -/// * `x_rec(N/2 … N − 1)` — always all zeros; -/// * `x_rec(i < 0)` — the previous fully reconstructed time-domain -/// output of the decoder. -/// -/// [`Self::history`] stores the `i < 0` region in chronological order -/// (oldest first), so `x_rec(j)` for `j < 0` is -/// `history[history.len() + j]`. [`Self::aliased_tail`] stores -/// `x_rec(0 … N/2 − 1)`. At the start of decoding the whole buffer is -/// zero, matching the §4.6.7.3 initialisation. -#[derive(Clone, Debug, Default)] -pub struct LtpState { - /// The §4.5.1.1 frame-length family this channel decodes under. - /// Sets the transform length `N`, the aliased-tail length `N/2`, - /// the §4.6.7.3 LD lag offset `M = N/2`, and the history depth. - family: FrameFamily, - /// Previously reconstructed decoder output (the `i < 0` region), - /// oldest sample first. Capped at [`Self::history_cap`] samples. - history: Vec, - /// `x_rec(0 … N/2 − 1)` — the current frame's aliased IMDCT half - /// window, `family.frame_len()` samples. Empty before the first - /// frame (treated as zeros). - aliased_tail: Vec, - /// §4.6.7.2 (ER AAC LD) `ltp_prev_lag` — the last transmitted - /// `ltp_lag`, repeated when a frame signals - /// `ltp_lag_update == 0`. Zero before any lag was transmitted. - prev_lag: u16, -} - -impl LtpState { - /// Maximum 11-bit `ltp_lag` (§4.6.7.2), used to size the history - /// buffer so the deepest possible prediction still has data. - const MAX_LAG: usize = 2047; - - /// A fresh, all-zero LTP state (§4.6.7.3 initialisation) for the - /// 1024-line family. - pub fn new() -> Self { - Self::default() - } - - /// A fresh, all-zero LTP state for an arbitrary §4.5.1.1 family. - /// For the LD families this arms the §4.6.7.3 `M = N/2` lag - /// offset, the 10-bit lag range and the `ltp_prev_lag` repeat - /// mechanism (§4.6.17.2.6 scales the delay buffer with the frame, - /// 2048 / 1920 samples for N = 512 / 480). - pub fn new_family(family: FrameFamily) -> Self { - LtpState { - family, - ..Self::default() - } - } - - /// §4.6.7.3 — the LD lag offset `M`: `N/2` (== the frame length, - /// since `N` is the transform window length `2 × frame_len`) for - /// ER AAC LD, `0` otherwise. - fn lag_offset(&self) -> usize { - if self.family.is_ld() { - self.family.frame_len() - } else { - 0 - } - } - - /// Resolve this frame's effective `ltp_lag` and update the - /// `ltp_prev_lag` repeat state (§4.6.7.2, ER AAC LD): a - /// transmitted lag becomes the new `ltp_prev_lag`; an absent lag - /// (LD `ltp_lag_update == 0`) repeats the previous one. Non-LD - /// streams always transmit the 11-bit lag, so the repeat arm is - /// only reachable for LD. - fn resolve_lag(&mut self, ltp: &LtpData) -> Result { - match ltp.lag { - Some(lag) => { - self.prev_lag = lag; - Ok(lag) - } - None => { - if ltp.lag_update == Some(false) { - Ok(self.prev_lag) - } else { - // A missing lag without the LD repeat signal is a - // malformed in-memory record. - Err(Error::LtpInvalid) - } - } - } - } - - /// Number of past-output samples to retain. The predictor needs - /// `M + ltp_lag` samples before index 0 (`M = N/2` for LD), and - /// the deepest window read is `N − 1`, so `MAX_LAG + M + N` past - /// samples always suffice; the non-LD families keep the historic - /// `MAX_LAG + N` depth. - fn history_cap(&self) -> usize { - Self::MAX_LAG + self.lag_offset() + self.family.long_transform_len() - } - - /// Read `x_rec(j)` for any integer index `j` per the §4.6.7.3 - /// buffer arrangement. Out-of-range indices (deeper than the - /// retained history, or `j ≥ N`) read as zero, matching the - /// zero-initialised buffer. - fn x_rec(&self, j: isize) -> f64 { - let half = self.family.frame_len() as isize; // N/2 - if j < 0 { - // Previous fully reconstructed output, chronological. - let idx = self.history.len() as isize + j; - if idx < 0 { - 0.0 - } else { - self.history[idx as usize] - } - } else if j < half { - // Aliased IMDCT half window. - self.aliased_tail.get(j as usize).copied().unwrap_or(0.0) - } else { - // x_rec(N/2 … N−1) is always zero. - 0.0 - } - } - - /// §4.6.7.3 `predict()` — form the predicted time-domain signal - /// `x_est(i) = ltp_coef · x_rec(i − M − ltp_lag)`, `i = 0 … N−1`, - /// with `M = N/2` for the ER AAC LD families and `M = 0` - /// otherwise. - fn predict_long(&self, lag: u16, coef: f64) -> Vec { - let shift = lag as isize + self.lag_offset() as isize; - (0..self.family.long_transform_len() as isize) - .map(|i| coef * self.x_rec(i - shift)) - .collect() - } - - /// Update the history after a frame is fully reconstructed. - /// - /// * `output` — this frame's `LONG_WINDOW_LEN` (1024) PCM samples, - /// i.e. the §4.6.11.3.3 overlap-added output, which become the - /// `i < 0` region for subsequent frames. - /// * `aliased_tail` — this frame's `x_rec(0 … N/2 − 1)`, the - /// pre-overlap-add windowed IMDCT tail of length - /// `LONG_WINDOW_LEN`. - /// - /// Call once per frame, after synthesis, regardless of whether LTP - /// was active, so the predictor history stays continuous. - pub fn push_frame(&mut self, output: &[f64], aliased_tail: &[f64]) { - self.history.extend_from_slice(output); - let cap = self.history_cap(); - if self.history.len() > cap { - let excess = self.history.len() - cap; - self.history.drain(0..excess); - } - self.aliased_tail.clear(); - self.aliased_tail.extend_from_slice(aliased_tail); - } - - /// §4.6.7.3 — apply long-window LTP to one channel's reconstructed - /// spectrum in place. - /// - /// * `spec` — the `LONG_WINDOW_LEN` (1024) decoded coefficients - /// `Y_rec`, modified to `X_rec` on the predicted bands. - /// * `ics_info` — provides `window_sequence`, `window_shape` and - /// `max_sfb`; LTP only acts on the three long sequences. - /// * `ltp` — the parsed §4.6.7.2 side info for this channel. - /// * `prev_shape` — the previous block's `window_shape`, governing - /// the left half of this block's analysis window - /// (§4.6.11.3.2). `None` before the first frame, in which case - /// the block's own shape is used for both halves. - /// * `fs_index` — the sampling-frequency index, selecting the - /// §4.5.4 long-window scalefactor-band offsets. - /// - /// When LTP is inactive (short sequence, or `ltp.long_used` all - /// false) the spectrum is left untouched. Errors: - /// [`Error::LtpInvalid`] for an out-of-range `ltp_coef`, a missing - /// `ltp_lag`, or a spectrum length that is not `LONG_WINDOW_LEN`; - /// the [`Error`] surfaced by [`long_window_offsets`] for a bad - /// `fs_index`. - pub fn apply_long( - &mut self, - spec: &mut [f64], - ics_info: &IcsInfo, - ltp: &LtpData, - prev_shape: Option, - fs_index: u8, - ) -> Result<()> { - self.apply_long_with_analysis(spec, ics_info, ltp, prev_shape, fs_index, |_| Ok(())) - } - - /// §4.6.7.3 + §4.6.7.4.1 — the LTP long-window add with the - /// Figure 4.30 **TNS analysis filter** inserted between - /// `X_est = MDCT(x_est)` and the per-sfb `X_rec = X_est + Y_rec`. - /// - /// When TNS is active on the channel, the transmitted residual - /// `Y_rec` carried in `spec` lives in the noise-shaped (pre-TNS- - /// synthesis) domain. The LTP-predicted spectrum `X_est` is a clean - /// MDCT, so it has to be pushed through the same all-zero TNS - /// analysis filter before it can be added like-for-like. `analyze` - /// applies that filter in place to the freshly transformed `X_est` - /// (length `LONG_WINDOW_LEN`); pass a no-op closure when the channel - /// carries no TNS (which is what [`Self::apply_long`] does). - /// - /// The subsequent §4.6.9 TNS *synthesis* pass over the combined - /// `X_rec` (run by the element driver after this add) undoes the - /// analysis on the LTP contribution while shaping the residual, - /// per the §4.6.7.4.1 inverse-filter relationship. - /// - /// All other semantics match [`Self::apply_long`]. - pub fn apply_long_with_analysis( - &mut self, - spec: &mut [f64], - ics_info: &IcsInfo, - ltp: &LtpData, - prev_shape: Option, - fs_index: u8, - analyze: F, - ) -> Result<()> - where - F: FnOnce(&mut [f64]) -> Result<()>, - { - // §4.6.7.3 / short-block note: prediction is disabled for - // EIGHT_SHORT_SEQUENCE in the long-window LTP path. - if ics_info.window_sequence == WindowSequence::EightShort { - return Ok(()); - } - // The channel state and the frame side info must agree on the - // §4.5.1.1 family (transform length, LD lag offset). - if ics_info.family != self.family { - return Err(Error::LtpInvalid); - } - if spec.len() != self.family.frame_len() { - return Err(Error::LtpInvalid); - } - // The effective lag (with the LD ltp_prev_lag repeat) must be - // resolved on EVERY LTP-bearing frame — even one that flags no - // bands — so the repeat state tracks the wire exactly. - let lag = self.resolve_lag(ltp)?; - // No bands flagged → nothing to add. - if !ltp.long_used.iter().any(|&u| u) { - return Ok(()); - } - - let coef = ltp_coefficient(ltp.coef)?; - - // predict() → MDCT(x_est). - let n_transform = self.family.long_transform_len(); - let x_est = self.predict_long(lag, coef); - let left_shape = prev_shape.unwrap_or(ics_info.window_shape); - let window = long_only_window_family(self.family, left_shape, ics_info.window_shape); - let z: Vec = x_est - .iter() - .zip(window.iter()) - .map(|(&x, &w)| x * w) - .collect(); - let mut x_est_spec = forward_mdct(&z, n_transform); - - // §4.6.7.4.1 / Figure 4.30: TNS analysis filter on X_est. - analyze(&mut x_est_spec)?; - - // Per-sfb: X_rec = X_est + Y_rec where ltp_long_used[sfb]. - let offsets = crate::swb_offset::long_window_offsets_family(self.family, fs_index)?; - let num_sfb = ics_info.max_sfb as usize; - for sfb in 0..num_sfb { - if !ltp.long_used.get(sfb).copied().unwrap_or(false) { - continue; - } - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - for c in start..end.min(spec.len()) { - spec[c] += x_est_spec[c]; - } - } - Ok(()) - } - - /// ISO/IEC 14496-3:**2001** §4.6.7.3 — short-window LTP synthesis - /// for one channel's `EIGHT_SHORT_SEQUENCE` spectrum, in place. - /// - /// This is the reconstruction counterpart of the 2001-edition - /// `ltp_data()` short branch (`ltp_short_used[w]` / - /// `ltp_short_lag[w]`, parsed under - /// [`crate::ics_info::LtpEdition::Iso2001`]). The 2009 edition - /// **removed** short-window LTP entirely (§4.6.7.1 "LTP is - /// restricted to long windows only"), so this entry point is never - /// reached by the 2009 decode chain; it exists for 2001-edition - /// streams. Per the 2001 pseudo-code, for each of the eight - /// subwindows `w` flagged `ltp_short_used[w]`: - /// - /// ```text - /// x_est = predict(); // lag = ltp_lag + ltp_short_lag[w] - /// X_est = MDCT(x_est); // the 256-point short transform - /// for (sfb = 0; sfb < 8; sfb++) // first 8 SFBs only - /// X_rec = X_est + Y_rec; - /// ``` - /// - /// with the same Table 4.98 `ltp_coef` for every subwindow, and - /// `ltp_short_lag[w] ∈ −8..=7` a per-window *relative* delay added - /// to the frame's 11-bit `ltp_lag` (`0` when - /// `ltp_short_lag_present[w] == 0`). A negative combined lag - /// (possible only when `ltp_lag < 8`) is floored at `0` — the - /// history holds no future samples. - /// - /// ## The `window_origins` parameter — a documented spec ambiguity - /// - /// §4.6.7.3 (2001) states the `x_rec` buffer arrangement once, in - /// terms of a single long transform, and never respecifies the - /// **index origin of each subwindow** into that shared history — - /// i.e. which absolute history position subwindow `w`'s - /// `x_est(0)` reads from (see the staged analysis - /// `docs/audio/aac/short-window-ltp-blocked.md` §5; no encoder - /// emits this syntax and no reference decode exists to pin it). - /// Rather than invent a convention, this routine takes the - /// per-subwindow origin explicitly: subwindow `w` predicts - /// `x_est(i) = ltp_coef · x_rec(window_origins[w] + i − lag_w)` - /// for `i = 0..256`. When a fixture (or errata) eventually fixes - /// the origin rule, the caller encodes it here without touching - /// the pinned math. - /// - /// Errors: [`Error::LtpInvalid`] when `ics_info` is not - /// `EIGHT_SHORT_SEQUENCE`, `spec` is not the 8 × 128 window-major - /// short spectrum, `ltp.short` is missing / not 8 entries, the - /// frame `ltp_lag` is absent, or `ltp_coef` is out of range. - pub fn apply_short_2001( - &self, - spec: &mut [f64], - ics_info: &IcsInfo, - ltp: &LtpData, - prev_shape: Option, - fs_index: u8, - window_origins: &[isize; 8], - ) -> Result<()> { - if ics_info.window_sequence != WindowSequence::EightShort { - return Err(Error::LtpInvalid); - } - let wlen = SHORT_WINDOW_LEN as usize; - if spec.len() != 8 * wlen { - return Err(Error::LtpInvalid); - } - let Some(short) = ltp.short.as_ref() else { - return Err(Error::LtpInvalid); - }; - if short.len() != 8 { - return Err(Error::LtpInvalid); - } - if !short.iter().any(|s| s.used) { - return Ok(()); - } - let coef = ltp_coefficient(ltp.coef)?; - let lag = ltp.lag.ok_or(Error::LtpInvalid)? as isize; - let offsets = short_window_offsets(fs_index)?; - let num_sfb = LTP_SHORT_MAX_SFB - .min(ics_info.max_sfb as usize) - .min(offsets.len() - 1); - let left_shape = prev_shape.unwrap_or(ics_info.window_shape); - - for (w, sw) in short.iter().enumerate() { - if !sw.used { - continue; - } - // lag_w = ltp_lag + ltp_short_lag[w], floored at 0. - let lag_w = (lag + isize::from(sw.lag)).max(0); - let origin = window_origins[w]; - let x_est: Vec = (0..SHORT_TRANSFORM_LEN as isize) - .map(|i| coef * self.x_rec(origin + i - lag_w)) - .collect(); - // Window subwindow w (window 0's left half inherits the - // previous block's shape, §4.6.11.3.2) and run the - // 256-point analysis transform. - let window = short_window_j(w, left_shape, ics_info.window_shape); - let z: Vec = x_est - .iter() - .zip(window.iter()) - .map(|(&x, &wv)| x * wv) - .collect(); - let x_est_spec = forward_mdct(&z, SHORT_TRANSFORM_LEN); - // X_rec = X_est + Y_rec on the first 8 SFBs. - let base = w * wlen; - for sfb in 0..num_sfb { - let start = offsets[sfb] as usize; - let end = (offsets[sfb + 1] as usize).min(wlen); - for c in start..end { - spec[base + c] += x_est_spec[c]; - } - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::WindowSequence; - - fn long_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: true, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: 49, - } - } - - fn ltp_with(coef: u8, lag: u16, long_used: Vec) -> LtpData { - LtpData { - lag_update: None, - lag: Some(lag), - coef, - long_used, - short: None, - } - } - - #[test] - fn table_4_98_coefficients() { - // Table 4.98 endpoints and a mid value. - assert_eq!(ltp_coefficient(0).unwrap(), 0.570829); - assert_eq!(ltp_coefficient(4).unwrap(), 0.984900); - assert_eq!(ltp_coefficient(7).unwrap(), 1.369533); - assert!(ltp_coefficient(8).is_err()); - } - - #[test] - fn short_sequence_is_noop() { - let mut st = LtpState::new(); - let mut info = long_info(40); - info.window_sequence = WindowSequence::EightShort; - let ltp = ltp_with(0, 100, vec![true; 40]); - let mut spec = vec![1.0f64; LONG_WINDOW_LEN as usize]; - st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); - assert!(spec.iter().all(|&v| v == 1.0)); - } - - #[test] - fn no_bands_flagged_is_noop() { - let mut st = LtpState::new(); - // Seed history so a predictor would otherwise fire. - let out = vec![0.5f64; LONG_WINDOW_LEN as usize]; - let tail = vec![0.25f64; LONG_WINDOW_LEN as usize]; - st.push_frame(&out, &tail); - let info = long_info(40); - let ltp = ltp_with(0, 100, vec![false; 40]); - let mut spec = vec![1.0f64; LONG_WINDOW_LEN as usize]; - st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); - assert!(spec.iter().all(|&v| v == 1.0)); - } - - #[test] - fn zero_history_predicts_zero() { - // §4.6.7.3 initialisation: x_rec all zero ⇒ x_est all zero ⇒ - // X_est all zero ⇒ spectrum unchanged even with bands flagged. - let mut st = LtpState::new(); - let info = long_info(40); - let ltp = ltp_with(7, 50, vec![true; 40]); - let mut spec = vec![2.0f64; LONG_WINDOW_LEN as usize]; - st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); - for &v in &spec { - assert!((v - 2.0).abs() < 1e-12, "got {v}"); - } - } - - #[test] - fn predict_long_applies_lag_and_gain() { - // Drive the predictor from a known history. With lag L and the - // i<0 region holding a DC level d, x_est(i) = coef·d for all i - // whose source index i−L < 0 (i.e. i < L). Verify a handful of - // sample values directly via the private predictor. - let mut st = LtpState::new(); - let d = 1.0f64; - st.history = vec![d; st.history_cap()]; - let coef = ltp_coefficient(2).unwrap(); // 0.813004 - let lag = 64u16; - let x_est = st.predict_long(lag, coef); - // i=0: source index −64 (in history) ⇒ coef·d. - assert!((x_est[0] - coef * d).abs() < 1e-12); - // i=63: source −1 ⇒ coef·d. - assert!((x_est[63] - coef * d).abs() < 1e-12); - // i=64: source 0 ⇒ aliased_tail (empty) ⇒ 0. - assert!(x_est[64].abs() < 1e-12); - } - - #[test] - fn x_rec_regions_are_distinct() { - let mut st = LtpState::new(); - st.history = vec![3.0; 10]; - st.aliased_tail = vec![7.0; LONG_WINDOW_LEN as usize]; - // i<0 region: most-recent past = 3.0. - assert_eq!(st.x_rec(-1), 3.0); - // Beyond retained history reads zero. - assert_eq!(st.x_rec(-100), 0.0); - // 0..N/2 is the aliased tail. - assert_eq!(st.x_rec(0), 7.0); - assert_eq!(st.x_rec(LONG_WINDOW_LEN as isize - 1), 7.0); - // N/2..N is always zero. - assert_eq!(st.x_rec(LONG_WINDOW_LEN as isize), 0.0); - } - - #[test] - fn push_frame_caps_history() { - let mut st = LtpState::new(); - for _ in 0..4 { - let out = vec![1.0f64; LONG_WINDOW_LEN as usize]; - let tail = vec![0.0f64; LONG_WINDOW_LEN as usize]; - st.push_frame(&out, &tail); - } - assert!(st.history.len() <= st.history_cap()); - } - - // ===== ISO/IEC 14496-3:2001 §4.6.7.3 short-window LTP ===== - - use crate::ics_info::LtpShortWindow; - - fn short_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: true, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups: 8, - window_group_length: vec![1; 8], - num_swb: 14, - } - } - - fn short_ltp(coef: u8, lag: u16, windows: [Option; 8]) -> LtpData { - LtpData { - lag_update: None, - lag: Some(lag), - coef, - long_used: vec![], - short: Some( - windows - .iter() - .map(|w| match w { - Some(l) => LtpShortWindow { - used: true, - lag_present: *l != 0, - lag: *l, - }, - None => LtpShortWindow { - used: false, - lag_present: false, - lag: 0, - }, - }) - .collect(), - ), - } - } - - /// Natural subwindow-grid origins for the tests: subwindow w's - /// x_est(0) reads history position w·128 (one convention among - /// those the 2001 text admits — the routine deliberately takes - /// the origins from the caller; see the method docs). - fn grid_origins() -> [isize; 8] { - core::array::from_fn(|w| (w as isize) * SHORT_WINDOW_LEN as isize) - } - - #[test] - fn short_2001_rejects_bad_shapes() { - let st = LtpState::new(); - let ltp = short_ltp(0, 100, [Some(0); 8]); - let origins = grid_origins(); - // Long sequence rejected. - let mut spec = vec![0.0f64; 8 * SHORT_WINDOW_LEN as usize]; - let info = long_info(40); - assert!(st - .apply_short_2001(&mut spec, &info, <p, None, 3, &origins) - .is_err()); - // Wrong spectrum length rejected. - let sinfo = short_info(8); - let mut bad = vec![0.0f64; 100]; - assert!(st - .apply_short_2001(&mut bad, &sinfo, <p, None, 3, &origins) - .is_err()); - // Missing short records rejected. - let mut no_short = short_ltp(0, 100, [Some(0); 8]); - no_short.short = None; - assert!(st - .apply_short_2001(&mut spec, &sinfo, &no_short, None, 3, &origins) - .is_err()); - } - - #[test] - fn short_2001_no_used_window_is_noop() { - let mut st = LtpState::new(); - st.history = vec![1.0; st.history_cap()]; - st.aliased_tail = vec![0.5; LONG_WINDOW_LEN as usize]; - let info = short_info(8); - let ltp = short_ltp(3, 64, [None; 8]); - let mut spec = vec![2.0f64; 8 * SHORT_WINDOW_LEN as usize]; - st.apply_short_2001(&mut spec, &info, <p, None, 3, &grid_origins()) - .unwrap(); - assert!(spec.iter().all(|&v| v == 2.0)); - } - - #[test] - fn short_2001_zero_history_predicts_zero() { - let st = LtpState::new(); - let info = short_info(8); - let ltp = short_ltp(7, 64, [Some(0); 8]); - let mut spec = vec![1.5f64; 8 * SHORT_WINDOW_LEN as usize]; - st.apply_short_2001(&mut spec, &info, <p, None, 3, &grid_origins()) - .unwrap(); - for &v in &spec { - assert!((v - 1.5).abs() < 1e-12); - } - } - - #[test] - fn short_2001_only_used_windows_and_first_8_sfbs_change() { - // Non-trivial history; flag only subwindow 2. Its first-8-sfb - // region gains X_est energy, its upper bands stay untouched, - // and every other subwindow is untouched entirely. - let mut st = LtpState::new(); - st.history = (0..st.history_cap()) - .map(|i| ((i % 37) as f64) / 17.0 - 1.0) - .collect(); - st.aliased_tail = vec![0.25; LONG_WINDOW_LEN as usize]; - let fs = 3u8; - let info = short_info(14); - let mut flags = [None; 8]; - flags[2] = Some(0); - let ltp = short_ltp(4, 200, flags); - let wlen = SHORT_WINDOW_LEN as usize; - let mut spec = vec![0.0f64; 8 * wlen]; - st.apply_short_2001(&mut spec, &info, <p, None, fs, &grid_origins()) - .unwrap(); - - let offsets = short_window_offsets(fs).unwrap(); - let cutoff = offsets[LTP_SHORT_MAX_SFB] as usize; - // Subwindow 2, first 8 sfbs: changed. - let low = &spec[2 * wlen..2 * wlen + cutoff]; - assert!( - low.iter().any(|&v| v.abs() > 1e-9), - "flagged region changed" - ); - // Subwindow 2 above sfb 8: untouched. - assert!(spec[2 * wlen + cutoff..3 * wlen].iter().all(|&v| v == 0.0)); - // All other subwindows: untouched. - for w in [0usize, 1, 3, 4, 5, 6, 7] { - assert!( - spec[w * wlen..(w + 1) * wlen].iter().all(|&v| v == 0.0), - "unflagged subwindow {w} must stay silent" - ); - } - } - - #[test] - fn short_2001_relative_lag_shifts_the_source() { - // Same frame lag, different ltp_short_lag: the predictor must - // read a shifted history slice, so the two X_est contributions - // differ. History is an impulse train so any shift changes - // the windowed segment. - let mut st = LtpState::new(); - st.history = (0..st.history_cap()) - .map(|i| if i % 64 == 0 { 1.0 } else { 0.0 }) - .collect(); - st.aliased_tail = vec![0.0; LONG_WINDOW_LEN as usize]; - let info = short_info(8); - let wlen = SHORT_WINDOW_LEN as usize; - let run = |short_lag: i8| -> Vec { - let mut flags = [None; 8]; - flags[0] = Some(short_lag); - let ltp = short_ltp(4, 300, flags); - let mut spec = vec![0.0f64; 8 * wlen]; - st.apply_short_2001(&mut spec, &info, <p, None, 3, &grid_origins()) - .unwrap(); - spec[..wlen].to_vec() - }; - let a = run(0); - let b = run(7); - let c = run(-8); - assert!(a.iter().zip(&b).any(|(x, y)| (x - y).abs() > 1e-9)); - assert!(a.iter().zip(&c).any(|(x, y)| (x - y).abs() > 1e-9)); - } - - #[test] - fn short_2001_origin_convention_is_callers_choice() { - // The documented §4.6.7.3 (2001) ambiguity: the same frame - // under two different origin conventions produces different - // contributions — pinning that the routine faithfully defers - // the choice rather than hard-coding one. - let mut st = LtpState::new(); - st.history = (0..st.history_cap()) - .map(|i| ((i * 7919) % 251) as f64 / 125.0 - 1.0) - .collect(); - st.aliased_tail = vec![0.0; LONG_WINDOW_LEN as usize]; - let info = short_info(8); - let wlen = SHORT_WINDOW_LEN as usize; - let mut flags = [None; 8]; - flags[5] = Some(0); - let ltp = short_ltp(2, 500, flags); - let run = |origins: [isize; 8]| -> Vec { - let mut spec = vec![0.0f64; 8 * wlen]; - st.apply_short_2001(&mut spec, &info, <p, None, 3, &origins) - .unwrap(); - spec - }; - let grid = run(grid_origins()); - let zeroed = run([0; 8]); - assert!(grid.iter().zip(&zeroed).any(|(x, y)| (x - y).abs() > 1e-9)); - } - - #[test] - fn nonzero_history_modifies_flagged_bands_only() { - // With nonzero history, a flagged sfb gains X_est energy while - // an unflagged sfb is untouched. fs_index 3 (48 kHz) long - // offsets: sfb 0 = [0,4), so flag sfb 0 only and check bins - // 0..4 changed but a high bin is unchanged. - let mut st = LtpState::new(); - st.history = vec![1.0; st.history_cap()]; - st.aliased_tail = vec![0.5; LONG_WINDOW_LEN as usize]; - let mut used = vec![false; 40]; - used[0] = true; - let info = long_info(40); - let ltp = ltp_with(5, 30, used); - let baseline = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let mut spec = baseline.clone(); - st.apply_long(&mut spec, &info, <p, None, 3).unwrap(); - let offsets = long_window_offsets(3).unwrap(); - let sfb0_end = offsets[1] as usize; - let changed = (0..sfb0_end).any(|c| (spec[c] - baseline[c]).abs() > 1e-9); - assert!(changed, "flagged sfb 0 should change"); - // A bin well above sfb 0 must be unchanged. - assert!((spec[sfb0_end + 50] - baseline[sfb0_end + 50]).abs() < 1e-12); - } - - // ---- ER AAC LD (§4.6.7.3 M = N/2, §4.6.7.2 ltp_prev_lag) ---- - - fn ld_info(family: FrameFamily, max_sfb: u8) -> IcsInfo { - let mut info = long_info(max_sfb); - info.family = family; - info.num_swb = 36; - info - } - - fn ld_ltp(coef: u8, lag: Option, long_used: Vec) -> LtpData { - LtpData { - lag_update: Some(lag.is_some()), - lag, - coef, - long_used, - short: None, - } - } - - #[test] - fn ld_predict_reads_with_m_offset() { - // Place a single impulse in the history and verify the LD - // predictor reads it at i = M + lag − depth… i.e. that - // x_est(i) = coef · x_rec(i − M − lag) with M = frame_len. - let mut st = LtpState::new_family(FrameFamily::Ld512); - // history: 2000 zeros with an impulse 100 samples back - // (x_rec(−100) = 1.0). - let mut hist = vec![0.0f64; 2000]; - let hlen = hist.len(); - hist[hlen - 100] = 1.0; - st.history = hist; - let coef = ltp_coefficient(0).unwrap(); - // lag = 40, M = 512: x_est(i) = coef·x_rec(i − 552); the - // impulse at x_rec(−100) lands at i = 452. - let x_est = st.predict_long(40, coef); - assert_eq!(x_est.len(), 1024); // N = 1024 for LD512 - for (i, &v) in x_est.iter().enumerate() { - if i == 452 { - assert!((v - coef).abs() < 1e-15, "impulse at {i}: {v}"); - } else { - assert_eq!(v, 0.0, "unexpected non-zero at {i}"); - } - } - } - - #[test] - fn ld_480_predict_geometry() { - let mut st = LtpState::new_family(FrameFamily::Ld480); - let mut hist = vec![0.0f64; 2000]; - let hlen = hist.len(); - hist[hlen - 1] = 1.0; // x_rec(−1) = 1.0 - st.history = hist; - let coef = ltp_coefficient(3).unwrap(); - // M = 480, lag = 0: impulse lands at i = 479. - let x_est = st.predict_long(0, coef); - assert_eq!(x_est.len(), 960); - assert!((x_est[479] - coef).abs() < 1e-15); - assert_eq!(x_est[480], 0.0); - } - - #[test] - fn ld_prev_lag_repeat() { - // Frame 1 transmits lag 123 (ltp_lag_update == 1); frame 2 - // repeats it (ltp_lag_update == 0, no lag on the wire). Both - // frames must predict identically from the same history. - let mut info = ld_info(FrameFamily::Ld512, 36); - info.num_swb = 36; - let mut st = LtpState::new_family(FrameFamily::Ld512); - st.history = (0..2048).map(|i| ((i * 37) % 101) as f64 * 0.01).collect(); - st.aliased_tail = vec![0.0; 512]; - - let with_lag = ld_ltp(2, Some(123), vec![true; 36]); - let repeat = ld_ltp(2, None, vec![true; 36]); - - let mut spec_a = vec![0.0f64; 512]; - let mut st_a = st.clone(); - st_a.apply_long(&mut spec_a, &info, &with_lag, None, 3) - .unwrap(); - - // Same state, but resolve the transmitted lag first and then - // decode a repeat frame — must produce the same contribution. - let mut st_b = st.clone(); - let mut warmup = vec![0.0f64; 512]; - st_b.apply_long(&mut warmup, &info, &with_lag, None, 3) - .unwrap(); - let mut spec_b = vec![0.0f64; 512]; - st_b.apply_long(&mut spec_b, &info, &repeat, None, 3) - .unwrap(); - - assert!(spec_a.iter().any(|&v| v != 0.0), "LTP must contribute"); - for (a, b) in spec_a.iter().zip(spec_b.iter()) { - assert!((a - b).abs() < 1e-12); - } - } - - #[test] - fn ld_repeat_without_prior_lag_uses_zero() { - // ltp_lag_update == 0 before any transmitted lag: the - // §4.6.7.3 zero-initialised state gives ltp_prev_lag = 0. - let info = ld_info(FrameFamily::Ld512, 36); - let mut st = LtpState::new_family(FrameFamily::Ld512); - st.aliased_tail = vec![0.0; 512]; - let repeat = ld_ltp(2, None, vec![true; 36]); - let mut spec = vec![0.0f64; 512]; - st.apply_long(&mut spec, &info, &repeat, None, 3).unwrap(); - // Zero history → zero contribution, but no error. - assert!(spec.iter().all(|&v| v == 0.0)); - } - - #[test] - fn ld_family_mismatch_rejected() { - let info = ld_info(FrameFamily::Ld512, 36); - let mut st = LtpState::new(); // Lc1024 state - let ltp = ld_ltp(0, Some(1), vec![true; 36]); - let mut spec = vec![0.0f64; 512]; - assert!(matches!( - st.apply_long(&mut spec, &info, <p, None, 3), - Err(Error::LtpInvalid) - )); - } - - #[test] - fn missing_lag_without_repeat_signal_rejected() { - let info = long_info(40); - let mut st = LtpState::new(); - let ltp = LtpData { - lag_update: None, - lag: None, - coef: 0, - long_used: vec![true; 40], - short: None, - }; - let mut spec = vec![0.0f64; 1024]; - assert!(matches!( - st.apply_long(&mut spec, &info, <p, None, 3), - Err(Error::LtpInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/ms_stereo.rs b/crates/vendor/oxideav-aac/src/ms_stereo.rs deleted file mode 100644 index 6f73bc9c..00000000 --- a/crates/vendor/oxideav-aac/src/ms_stereo.rs +++ /dev/null @@ -1,684 +0,0 @@ -//! §4.6.8.1 M/S (mid/side) stereo de-matrix — ISO/IEC 14496-3. -//! -//! M/S joint channel coding operates on a channel pair. On a -//! per-spectral-coefficient basis the decoder reconstructs the -//! left/right vector by either the identity matrix (M/S off for the -//! band) or the inverse M/S matrix (M/S on): -//! -//! ```text -//! [ l ] [ 1 0 ] [ l ] [ l ] [ 1 1 ] [ m ] -//! [ r ] = [ 0 1 ] [ r ] or [ r ] = [ 1 -1 ] [ s ] -//! ``` -//! -//! With `m` carried in the left slot and `s` in the right slot, the -//! §4.6.8.1.3 decoding pseudo code is the in-place de-matrix: -//! -//! ```text -//! if (mask_present >= 1) { -//! for (g=0; g Result { - match bits { - 0 => Ok(MsMaskPresent::AllZeros), - 1 => Ok(MsMaskPresent::Mask), - 2 => Ok(MsMaskPresent::AllOnes), - _ => Err(Error::MsStereoInvalid), - } - } - - /// The wire value (`0`/`1`/`2`) — the inverse of [`Self::from_bits`]. - pub fn to_bits(self) -> u8 { - match self { - MsMaskPresent::AllZeros => 0, - MsMaskPresent::Mask => 1, - MsMaskPresent::AllOnes => 2, - } - } - - /// `mask_present >= 1` — whether the §4.6.8.1.3 outer guard is - /// entered at all. - pub fn is_active(self) -> bool { - !matches!(self, MsMaskPresent::AllZeros) - } -} - -/// `true` ⇔ the band's right-channel codebook is an intensity book -/// (`INTENSITY_HCB` / `INTENSITY_HCB2`) — the §4.6.8.2.3 -/// `is_intensity(g,sfb)` predicate restricted to "is it intensity at -/// all" (the M/S guard only needs the boolean, not the ±1 sign). -fn is_intensity_cb(cb: u8) -> bool { - cb == INTENSITY_HCB || cb == INTENSITY_HCB2 -} - -/// `true` ⇔ the band's codebook is `NOISE_HCB` — the §4.6.13.3 -/// `is_noise(g,sfb)` predicate. -fn is_noise_cb(cb: u8) -> bool { - cb == NOISE_HCB -} - -/// A channel pair's de-interleaved spectra plus the per-channel -/// codebook assignments the M/S de-matrix needs. -/// -/// `left` / `right` are the window-major decoded spectra -/// (`num_windows × window_len`) produced by -/// [`crate::decoded_spectrum::quant_to_spec`], **pre-TNS**. On entry -/// `left` holds the mid (`m`) and `right` the side (`s`) for every -/// M/S-active band; [`apply_ms_stereo`] overwrites them with the -/// reconstructed left/right channels. -/// -/// `left_sfb_cb` / `right_sfb_cb` are each channel's `sfb_cb[g][sfb]` -/// (from its [`crate::section_data::SectionData`]); they drive the -/// intensity (right) / noise (either) exclusions. -#[derive(Debug)] -pub struct ChannelPairSpectra<'a> { - /// First ("left") channel spectrum — mid on entry, left on return. - pub left: &'a mut [f64], - /// Second ("right") channel spectrum — side on entry, right on return. - pub right: &'a mut [f64], - /// Left channel `sfb_cb[g][sfb]`. - pub left_sfb_cb: &'a [Vec], - /// Right channel `sfb_cb[g][sfb]`. - pub right_sfb_cb: &'a [Vec], -} - -/// Apply the §4.6.8.1.3 M/S de-matrix in place to a channel pair. -/// -/// * `pair` — the channel-pair spectra and per-channel codebooks -/// ([`ChannelPairSpectra`]). -/// * `ms_mask_present` — the decoded CPE [`MsMaskPresent`]. -/// * `ms_used` — `ms_used[g][sfb]` (one row per window group, each -/// at least `max_sfb` long). Ignored when `ms_mask_present` is -/// [`MsMaskPresent::AllZeros`] or [`MsMaskPresent::AllOnes`]; pass -/// an empty slice in those cases. -/// * `ics_info` — the shared `common_window` `ics_info()`; supplies -/// `num_window_groups`, `window_group_length`, `max_sfb`, and the -/// window geometry. -/// * `fs_index` — `samplingFrequencyIndex`, selecting the -/// `swb_offset` table. -/// -/// When `ms_mask_present` is [`MsMaskPresent::AllZeros`] the buffers -/// are left untouched (identity matrix on every band). -/// -/// Returns [`Error::MsStereoInvalid`] if the buffer / mask / `sfb_cb` -/// shapes disagree with `ics_info` (see the variant docs). -pub fn apply_ms_stereo( - pair: &mut ChannelPairSpectra<'_>, - ms_mask_present: MsMaskPresent, - ms_used: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, -) -> Result<()> { - let ChannelPairSpectra { - left, - right, - left_sfb_cb, - right_sfb_cb, - } = pair; - let window_len = ics_info.window_len()?; - let offsets = ics_info.swb_offsets(fs_index)?; - let num_swb = offsets.len() - 1; - let num_windows = ics_info.num_windows as usize; - let num_groups = ics_info.num_window_groups as usize; - let max_sfb = ics_info.max_sfb as usize; - - // Geometry consistency: both channels share the common_window - // ics_info, so both spectra are num_windows × window_len. - let expected = num_windows * window_len; - if left.len() != expected || right.len() != expected { - return Err(Error::MsStereoInvalid); - } - if ics_info.window_group_length.len() != num_groups - || ics_info - .window_group_length - .iter() - .map(|&w| w as usize) - .sum::() - != num_windows - { - return Err(Error::MsStereoInvalid); - } - // max_sfb must not exceed the band count of the active window. - if max_sfb > num_swb { - return Err(Error::MsStereoInvalid); - } - if left_sfb_cb.len() != num_groups || right_sfb_cb.len() != num_groups { - return Err(Error::MsStereoInvalid); - } - // The per-band exclusions read sfb_cb[g][sfb] for sfb < max_sfb. - for cb in left_sfb_cb.iter().chain(right_sfb_cb.iter()) { - if cb.len() < max_sfb { - return Err(Error::MsStereoInvalid); - } - } - // `Mask` needs a full ms_used[g][sfb]; the other modes ignore it. - if ms_mask_present == MsMaskPresent::Mask { - if ms_used.len() != num_groups { - return Err(Error::MsStereoInvalid); - } - for row in ms_used { - if row.len() < max_sfb { - return Err(Error::MsStereoInvalid); - } - } - } - - if !ms_mask_present.is_active() { - // mask_present == 0: identity matrix everywhere, nothing to do. - return Ok(()); - } - let all_ones = ms_mask_present == MsMaskPresent::AllOnes; - - let mut window_base = 0usize; - for g in 0..num_groups { - let wgl = ics_info.window_group_length[g] as usize; - for sfb in 0..max_sfb { - let band_on = all_ones || ms_used[g][sfb]; - if !band_on { - continue; - } - // §4.6.8.1.3: intensity is keyed on the right channel, - // noise on either channel; both suppress M/S. - if is_intensity_cb(right_sfb_cb[g][sfb]) - || is_noise_cb(left_sfb_cb[g][sfb]) - || is_noise_cb(right_sfb_cb[g][sfb]) - { - continue; - } - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - for b in 0..wgl { - let base = (window_base + b) * window_len; - for i in start..end { - let l = left[base + i]; - let r = right[base + i]; - left[base + i] = l + r; - right[base + i] = l - r; - } - } - } - window_base += wgl; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - use crate::section_data::ZERO_HCB; - - const FS_44100: u8 = 4; - - fn long_ics_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[FS_44100 as usize], - } - } - - fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { - let num_window_groups = window_group_length.len() as u8; - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups, - window_group_length, - num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[FS_44100 as usize], - } - } - - /// `sfb_cb` rows defaulting to a real spectrum book (here `2`). - fn plain_cb(num_groups: usize, max_sfb: usize) -> Vec> { - vec![vec![2u8; max_sfb]; num_groups] - } - - /// Positional wrapper over [`apply_ms_stereo`] that bundles the - /// channel pair into a [`ChannelPairSpectra`], keeping the test - /// bodies terse. - #[allow(clippy::too_many_arguments)] - fn run( - left: &mut [f64], - right: &mut [f64], - ms_mask_present: MsMaskPresent, - ms_used: &[Vec], - left_sfb_cb: &[Vec], - right_sfb_cb: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, - ) -> Result<()> { - let mut pair = ChannelPairSpectra { - left, - right, - left_sfb_cb, - right_sfb_cb, - }; - apply_ms_stereo(&mut pair, ms_mask_present, ms_used, ics_info, fs_index) - } - - #[test] - fn from_bits_roundtrip() { - for (bits, m) in [ - (0u8, MsMaskPresent::AllZeros), - (1, MsMaskPresent::Mask), - (2, MsMaskPresent::AllOnes), - ] { - assert_eq!(MsMaskPresent::from_bits(bits).unwrap(), m); - assert_eq!(m.to_bits(), bits); - } - assert!(matches!( - MsMaskPresent::from_bits(3), - Err(Error::MsStereoInvalid) - )); - } - - #[test] - fn all_zeros_is_identity() { - let ics = long_ics_info(4); - let mut l = vec![1.0f64; 1024]; - let mut r = vec![2.0f64; 1024]; - let cb = plain_cb(1, 4); - run( - &mut l, - &mut r, - MsMaskPresent::AllZeros, - &[], - &cb, - &cb, - &ics, - FS_44100, - ) - .unwrap(); - assert!(l.iter().all(|&x| x == 1.0)); - assert!(r.iter().all(|&x| x == 2.0)); - } - - #[test] - fn all_ones_dematrixes_every_band() { - // max_sfb = 2; long-window band 0 = bins 0..4, band 1 = 4..8. - let ics = long_ics_info(2); - let offsets = long_window_offsets(FS_44100).unwrap(); - assert_eq!(offsets[0], 0); - let mut l = vec![0.0f64; 1024]; - let mut r = vec![0.0f64; 1024]; - // m = 3, s = 1 in the first two bands → l' = 4, r' = 2. - let band_end = offsets[2] as usize; - for x in l.iter_mut().take(band_end) { - *x = 3.0; - } - for x in r.iter_mut().take(band_end) { - *x = 1.0; - } - let cb = plain_cb(1, 2); - run( - &mut l, - &mut r, - MsMaskPresent::AllOnes, - &[], - &cb, - &cb, - &ics, - FS_44100, - ) - .unwrap(); - for i in 0..band_end { - assert_eq!(l[i], 4.0, "l'[{i}]"); - assert_eq!(r[i], 2.0, "r'[{i}]"); - } - // Bands at/above max_sfb are untouched. - assert_eq!(l[band_end], 0.0); - assert_eq!(r[band_end], 0.0); - } - - #[test] - fn mask_gates_per_band() { - let ics = long_ics_info(2); - let offsets = long_window_offsets(FS_44100).unwrap(); - let b0 = offsets[1] as usize; // end of band 0 - let b1 = offsets[2] as usize; // end of band 1 - let mut l = vec![0.0f64; 1024]; - let mut r = vec![0.0f64; 1024]; - for x in l.iter_mut().take(b1) { - *x = 5.0; - } - for x in r.iter_mut().take(b1) { - *x = 1.0; - } - let cb = plain_cb(1, 2); - // band 0 on, band 1 off. - let ms_used = vec![vec![true, false]]; - run( - &mut l, - &mut r, - MsMaskPresent::Mask, - &ms_used, - &cb, - &cb, - &ics, - FS_44100, - ) - .unwrap(); - // band 0 de-matrixed: l'=6, r'=4. - for i in 0..b0 { - assert_eq!(l[i], 6.0); - assert_eq!(r[i], 4.0); - } - // band 1 untouched. - for i in b0..b1 { - assert_eq!(l[i], 5.0); - assert_eq!(r[i], 1.0); - } - } - - #[test] - fn intensity_band_excluded() { - let ics = long_ics_info(1); - let offsets = long_window_offsets(FS_44100).unwrap(); - let b0 = offsets[1] as usize; - let mut l = vec![3.0f64; 1024]; - let mut r = vec![1.0f64; 1024]; - let left_cb = plain_cb(1, 1); - // Right channel band 0 is intensity (15) → no de-matrix. - let mut right_cb = plain_cb(1, 1); - right_cb[0][0] = INTENSITY_HCB; - run( - &mut l, - &mut r, - MsMaskPresent::AllOnes, - &[], - &left_cb, - &right_cb, - &ics, - FS_44100, - ) - .unwrap(); - for i in 0..b0 { - assert_eq!(l[i], 3.0); - assert_eq!(r[i], 1.0); - } - } - - #[test] - fn noise_band_excluded_from_either_channel() { - let ics = long_ics_info(1); - let offsets = long_window_offsets(FS_44100).unwrap(); - let b0 = offsets[1] as usize; - // Left channel band 0 is noise (13) → no de-matrix even though - // the right channel is a real spectrum. - let mut left_cb = plain_cb(1, 1); - left_cb[0][0] = NOISE_HCB; - let right_cb = plain_cb(1, 1); - let mut l = vec![3.0f64; 1024]; - let mut r = vec![1.0f64; 1024]; - run( - &mut l, - &mut r, - MsMaskPresent::AllOnes, - &[], - &left_cb, - &right_cb, - &ics, - FS_44100, - ) - .unwrap(); - for i in 0..b0 { - assert_eq!(l[i], 3.0); - assert_eq!(r[i], 1.0); - } - } - - #[test] - fn short_window_grouping_applies_per_window() { - // Two groups: lengths [3, 5] summing to 8 short windows. - let ics = short_ics_info(2, vec![3, 5]); - let offsets = short_window_offsets(FS_44100).unwrap(); - let win = SHORT_WINDOW_LEN as usize; - let bandlen = offsets[1] as usize; // band 0 width - let mut l = vec![0.0f64; 8 * win]; - let mut r = vec![0.0f64; 8 * win]; - // Seed band 0 of every window with m=2, s=1. - for w in 0..8 { - for i in 0..bandlen { - l[w * win + i] = 2.0; - r[w * win + i] = 1.0; - } - } - let cb = plain_cb(2, 2); - // group 0 band 0 on; everything else off. - let ms_used = vec![vec![true, false], vec![false, false]]; - run( - &mut l, - &mut r, - MsMaskPresent::Mask, - &ms_used, - &cb, - &cb, - &ics, - FS_44100, - ) - .unwrap(); - // Group 0 = windows 0,1,2 → de-matrixed (l'=3, r'=1). - for w in 0..3 { - for i in 0..bandlen { - assert_eq!(l[w * win + i], 3.0, "win {w} band0"); - assert_eq!(r[w * win + i], 1.0); - } - } - // Group 1 = windows 3..8 → untouched. - for w in 3..8 { - for i in 0..bandlen { - assert_eq!(l[w * win + i], 2.0, "win {w} band0"); - assert_eq!(r[w * win + i], 1.0); - } - } - } - - #[test] - fn shape_mismatch_rejected() { - let ics = long_ics_info(2); - let cb = plain_cb(1, 2); - let mut l = vec![0.0f64; 512]; // wrong length - let mut r = vec![0.0f64; 1024]; - assert!(matches!( - run( - &mut l, - &mut r, - MsMaskPresent::AllOnes, - &[], - &cb, - &cb, - &ics, - FS_44100, - ), - Err(Error::MsStereoInvalid) - )); - } - - #[test] - fn mask_mode_requires_full_ms_used() { - let ics = long_ics_info(3); - let cb = plain_cb(1, 3); - let mut l = vec![0.0f64; 1024]; - let mut r = vec![0.0f64; 1024]; - // ms_used row too short for max_sfb = 3. - let ms_used = vec![vec![true, false]]; - assert!(matches!( - run( - &mut l, - &mut r, - MsMaskPresent::Mask, - &ms_used, - &cb, - &cb, - &ics, - FS_44100, - ), - Err(Error::MsStereoInvalid) - )); - } - - #[test] - fn dematrix_is_exactly_invertible_for_integers() { - // l' = m+s, r' = m-s recovers (m,s) = ((l'+r')/2,(l'-r')/2). - let ics = long_ics_info(1); - let offsets = long_window_offsets(FS_44100).unwrap(); - let b0 = offsets[1] as usize; - let cb = plain_cb(1, 1); - let mut l = vec![0.0f64; 1024]; - let mut r = vec![0.0f64; 1024]; - for i in 0..b0 { - l[i] = (i as f64) * 0.5 - 7.0; // m - r[i] = 3.0 - (i as f64) * 0.25; // s - } - let m: Vec = l[..b0].to_vec(); - let s: Vec = r[..b0].to_vec(); - run( - &mut l, - &mut r, - MsMaskPresent::AllOnes, - &[], - &cb, - &cb, - &ics, - FS_44100, - ) - .unwrap(); - for i in 0..b0 { - assert_eq!(l[i], m[i] + s[i]); - assert_eq!(r[i], m[i] - s[i]); - } - } - - #[test] - fn zero_hcb_bands_still_dematrix() { - // A ZERO_HCB band carries no transmitted spectrum but is not - // intensity/noise, so the M/S guard does not exclude it (its - // coefficients are simply 0 on both sides → stays 0). - let ics = long_ics_info(1); - let offsets = long_window_offsets(FS_44100).unwrap(); - let b0 = offsets[1] as usize; - let mut left_cb = plain_cb(1, 1); - left_cb[0][0] = ZERO_HCB; - let mut right_cb = plain_cb(1, 1); - right_cb[0][0] = ZERO_HCB; - let mut l = vec![0.0f64; 1024]; - let mut r = vec![0.0f64; 1024]; - run( - &mut l, - &mut r, - MsMaskPresent::AllOnes, - &[], - &left_cb, - &right_cb, - &ics, - FS_44100, - ) - .unwrap(); - for i in 0..b0 { - assert_eq!(l[i], 0.0); - assert_eq!(r[i], 0.0); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/pce.rs b/crates/vendor/oxideav-aac/src/pce.rs deleted file mode 100644 index eb0cbbd3..00000000 --- a/crates/vendor/oxideav-aac/src/pce.rs +++ /dev/null @@ -1,440 +0,0 @@ -//! `program_config_element()` parser. -//! -//! ISO/IEC 14496-3 §4.4.1.1 Table 4.2 (identical to ISO/IEC 13818-7 -//! §8.5 Table 25 modulo the field rename `profile` → `object_type`). -//! A PCE describes a custom channel layout — element ordering, per- -//! element CPE/SCE selection, mix-down hints, and a free-form -//! comment field. It is emitted either: -//! -//! * **As the first element of a `raw_data_block()`** (`id_syn_ele == -//! PCE`), when the `channelConfiguration` is one of 1..=7 *and* the -//! encoder wants to override the implicit element layout. -//! * **Inline in [`AudioSpecificConfig`](crate::asc::AudioSpecificConfig)** -//! when `channelConfiguration == 0`. In that case the PCE has no -//! surrounding `id_syn_ele` prefix and the byte-alignment Note 1 -//! on Table 4.2 applies *relative to the start of the -//! `AudioSpecificConfig`*, not to the absolute byte position in -//! the bitstream. -//! -//! Phase 1 retains the entire PCE structure verbatim (every wire -//! field is preserved) so a later round can validate channel layouts -//! and matrix mix-down semantics without re-parsing. -//! -//! ## Byte alignment -//! -//! The `byte_alignment()` call inside Table 4.2 follows the final -//! `valid_cc_element_tag_select[i]` loop. The position to align *to* -//! depends on the call site: -//! -//! * **Standalone PCE in `raw_data_block()`** ⇒ align to the next -//! absolute byte boundary of the bit-reader. -//! * **PCE inline in `AudioSpecificConfig`** ⇒ align to the next byte -//! boundary *relative to the start of the ASC*. Since the ASC -//! itself usually starts on a byte boundary in the carrying -//! container (`esds` payload, LATM `StreamMuxConfig`, etc.), the -//! two definitions usually coincide; they differ only when the -//! ASC was started at a non-zero bit offset inside a larger -//! bit-stream. [`Pce::parse`] takes a `relative_origin_bit` -//! parameter that the caller passes when the ASC origin is not at -//! the bit-reader's current zero — see -//! [`AudioSpecificConfig`](crate::asc::AudioSpecificConfig) for -//! the ASC-origin handling. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::{Error, Result}; - -/// One entry in a per-element list (`front_element_*`, `side_*`, -/// `back_*`) of a PCE. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ElementSelect { - /// `true` ⇔ the element at this slot is a CPE (channel-pair - /// element); `false` ⇔ SCE (single-channel element). Matches the - /// `*_element_is_cpe[i]` wire bit. - pub is_cpe: bool, - /// 4-bit `*_element_tag_select[i]` — the - /// `element_instance_tag` value the matching SCE/CPE will carry - /// inside the `raw_data_block()`. - pub tag_select: u8, -} - -/// One entry in the `valid_cc_element_*` list of a PCE. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CcElementSelect { - /// `true` ⇔ the coupling channel element is independently - /// switched (`cc_element_is_ind_sw[i] == 1`). - pub is_ind_sw: bool, - /// 4-bit `valid_cc_element_tag_select[i]` — the tag the matching - /// CCE will carry inside the `raw_data_block()`. - pub tag_select: u8, -} - -/// Parsed `program_config_element()` (Table 4.2). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Pce { - /// 4-bit `element_instance_tag`. - pub element_instance_tag: u8, - /// 2-bit `object_type` (ISO/IEC 14496-3) — synonym of `profile` - /// in ISO/IEC 13818-7. `0` = Main, `1` = LC, `2` = SSR, `3` = - /// LTP. Note this is **the same scheme as ADTS** (one less than - /// the `audioObjectType` defined by Table 1.16). - pub object_type: u8, - /// 4-bit `sampling_frequency_index` (Table 1.18). The PCE is - /// allowed to override the surrounding context's - /// `samplingFrequencyIndex`; in practice the wire value usually - /// matches the ASC / ADTS value. - pub sampling_frequency_index: u8, - /// `num_front_channel_elements` × [`ElementSelect`]. - pub front_elements: Vec, - /// `num_side_channel_elements` × [`ElementSelect`]. - pub side_elements: Vec, - /// `num_back_channel_elements` × [`ElementSelect`]. - pub back_elements: Vec, - /// `num_lfe_channel_elements` × `lfe_element_tag_select[i]` - /// (4-bit each). - pub lfe_element_tag_selects: Vec, - /// `num_assoc_data_elements` × `assoc_data_element_tag_select[i]` - /// (4-bit each). - pub assoc_data_tag_selects: Vec, - /// `num_valid_cc_elements` × [`CcElementSelect`]. - pub valid_cc_elements: Vec, - /// `mono_mixdown_element_number` (4 bits) if - /// `mono_mixdown_present == 1`. - pub mono_mixdown_element_number: Option, - /// `stereo_mixdown_element_number` (4 bits) if - /// `stereo_mixdown_present == 1`. - pub stereo_mixdown_element_number: Option, - /// `(matrix_mixdown_idx, pseudo_surround_enable)` if - /// `matrix_mixdown_idx_present == 1`. - pub matrix_mixdown: Option<(u8, bool)>, - /// `comment_field_bytes` raw bytes (after the `byte_alignment()` - /// and the 8-bit `comment_field_bytes` length prefix). - pub comment_field: Vec, -} - -impl Pce { - /// Parse a PCE starting at the current bit-reader position. The - /// `byte_alignment()` clause inside Table 4.2 will align the - /// reader to the next byte boundary whose *absolute* bit - /// position is a multiple of 8 plus `origin_bit_offset`. Pass - /// `0` for a standalone PCE inside a `raw_data_block()`; pass - /// the ASC origin bit-position for a PCE inline in - /// `AudioSpecificConfig` (see module docs). - pub fn parse(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result { - // 4 + 2 + 4 = 10 bits of header - let element_instance_tag = read_u8(reader, 4)?; - let object_type = read_u8(reader, 2)?; - let sampling_frequency_index = read_u8(reader, 4)?; - - // Element counts. - let n_front = read_u8(reader, 4)? as usize; - let n_side = read_u8(reader, 4)? as usize; - let n_back = read_u8(reader, 4)? as usize; - let n_lfe = read_u8(reader, 2)? as usize; - let n_assoc = read_u8(reader, 3)? as usize; - let n_cc = read_u8(reader, 4)? as usize; - - // Mix-down presence + bodies. - let mono_mixdown_present = read_bit(reader)?; - let mono_mixdown_element_number = if mono_mixdown_present { - Some(read_u8(reader, 4)?) - } else { - None - }; - let stereo_mixdown_present = read_bit(reader)?; - let stereo_mixdown_element_number = if stereo_mixdown_present { - Some(read_u8(reader, 4)?) - } else { - None - }; - let matrix_mixdown_idx_present = read_bit(reader)?; - let matrix_mixdown = if matrix_mixdown_idx_present { - let idx = read_u8(reader, 2)?; - let pseudo = read_bit(reader)?; - Some((idx, pseudo)) - } else { - None - }; - - // Element lists. - let front_elements = read_element_selects(reader, n_front)?; - let side_elements = read_element_selects(reader, n_side)?; - let back_elements = read_element_selects(reader, n_back)?; - - let mut lfe_element_tag_selects = Vec::with_capacity(n_lfe); - for _ in 0..n_lfe { - lfe_element_tag_selects.push(read_u8(reader, 4)?); - } - let mut assoc_data_tag_selects = Vec::with_capacity(n_assoc); - for _ in 0..n_assoc { - assoc_data_tag_selects.push(read_u8(reader, 4)?); - } - let mut valid_cc_elements = Vec::with_capacity(n_cc); - for _ in 0..n_cc { - let is_ind_sw = read_bit(reader)?; - let tag_select = read_u8(reader, 4)?; - valid_cc_elements.push(CcElementSelect { - is_ind_sw, - tag_select, - }); - } - - // §4.4.1.1 Note 1: byte_alignment() relative to the PCE's - // origin reference. The "next byte boundary" is determined - // by the absolute reader position minus `origin_bit_offset` - // — when the offset is 0, this collapses to the standard - // `align_to_byte()`. - align_relative_to_origin(reader, origin_bit_offset)?; - - let comment_field_bytes = read_u8(reader, 8)? as usize; - let mut comment_field = Vec::with_capacity(comment_field_bytes); - for _ in 0..comment_field_bytes { - comment_field.push(read_u8(reader, 8)?); - } - - Ok(Pce { - element_instance_tag, - object_type, - sampling_frequency_index, - front_elements, - side_elements, - back_elements, - lfe_element_tag_selects, - assoc_data_tag_selects, - valid_cc_elements, - mono_mixdown_element_number, - stereo_mixdown_element_number, - matrix_mixdown, - comment_field, - }) - } - - /// Total channel count implied by this PCE — sums one channel - /// for each SCE entry and two channels for each CPE entry across - /// front/side/back lists, plus one channel per LFE entry. (CCEs - /// are coupling buses and do not contribute to the output - /// channel count.) - pub fn channel_count(&self) -> usize { - let count_list = |list: &[ElementSelect]| -> usize { - list.iter().map(|e| if e.is_cpe { 2 } else { 1 }).sum() - }; - count_list(&self.front_elements) - + count_list(&self.side_elements) - + count_list(&self.back_elements) - + self.lfe_element_tag_selects.len() - } - - /// Encode this PCE into `writer` per ISO/IEC 14496-3 §4.4.1.1 - /// Table 4.2 — the bit-exact inverse of [`Pce::parse`]. The - /// emitted layout matches the parser exactly: - /// - /// `element_instance_tag(4) + object_type(2) + - /// sampling_frequency_index(4) + num_front(4) + num_side(4) + - /// num_back(4) + num_lfe(2) + num_assoc(3) + num_valid_cc(4) + - /// mono_mixdown_present(1) [+ mono_mixdown_element_number(4)] + - /// stereo_mixdown_present(1) [+ stereo_mixdown_element_number(4)] + - /// matrix_mixdown_idx_present(1) [+ matrix_mixdown_idx(2) + - /// pseudo_surround_enable(1)] + front[i].is_cpe(1) + - /// front[i].tag_select(4) ... + side[...] + back[...] + - /// lfe[i].tag_select(4) + assoc[i].tag_select(4) + - /// cc[i].is_ind_sw(1) + cc[i].tag_select(4) + byte_alignment() + - /// comment_field_bytes(8) + comment_field bytes` - /// - /// `origin_bit_offset` controls the Table 4.2 Note 1 - /// `byte_alignment()` semantics — pass `0` for a standalone PCE - /// inside a `raw_data_block()` (align to the absolute byte - /// boundary of the writer), and the ASC origin bit-position for - /// a PCE inline in [`AudioSpecificConfig`](crate::asc::AudioSpecificConfig) - /// (align relative to the ASC origin). Note that when the writer - /// itself starts at bit 0 (the standalone case) the absolute and - /// the origin-relative alignments coincide. - /// - /// Returns [`Error::PceEncodeInvalid`] when any wire field - /// overflows its bit-width — see [`Error::PceEncodeInvalid`] for - /// the exhaustive list. - pub fn write(&self, writer: &mut BitWriter, origin_bit_offset: u64) -> Result<()> { - // ----- Header fields ----- - // 4-bit element_instance_tag, 2-bit object_type, 4-bit - // sampling_frequency_index. - if self.element_instance_tag > 0x0f - || self.object_type > 0x03 - || self.sampling_frequency_index > 0x0f - { - return Err(Error::PceEncodeInvalid); - } - - // ----- Element counts: validate against field widths first - // so a single overflow surfaces before any bits leak onto - // the wire. - if self.front_elements.len() > 0x0f - || self.side_elements.len() > 0x0f - || self.back_elements.len() > 0x0f - || self.lfe_element_tag_selects.len() > 0x03 - || self.assoc_data_tag_selects.len() > 0x07 - || self.valid_cc_elements.len() > 0x0f - { - return Err(Error::PceEncodeInvalid); - } - - // ----- Per-element tag_select field-width checks (4 bits - // each) and mix-down field-width checks. Doing the checks - // up-front avoids emitting a partial PCE on a downstream - // overflow. - for e in self - .front_elements - .iter() - .chain(self.side_elements.iter()) - .chain(self.back_elements.iter()) - { - if e.tag_select > 0x0f { - return Err(Error::PceEncodeInvalid); - } - } - for &t in self - .lfe_element_tag_selects - .iter() - .chain(self.assoc_data_tag_selects.iter()) - { - if t > 0x0f { - return Err(Error::PceEncodeInvalid); - } - } - for cc in &self.valid_cc_elements { - if cc.tag_select > 0x0f { - return Err(Error::PceEncodeInvalid); - } - } - if let Some(n) = self.mono_mixdown_element_number { - if n > 0x0f { - return Err(Error::PceEncodeInvalid); - } - } - if let Some(n) = self.stereo_mixdown_element_number { - if n > 0x0f { - return Err(Error::PceEncodeInvalid); - } - } - if let Some((idx, _)) = self.matrix_mixdown { - if idx > 0x03 { - return Err(Error::PceEncodeInvalid); - } - } - if self.comment_field.len() > 0xff { - return Err(Error::PceEncodeInvalid); - } - - // ----- Emit header ----- - writer.write_u32(self.element_instance_tag as u32, 4); - writer.write_u32(self.object_type as u32, 2); - writer.write_u32(self.sampling_frequency_index as u32, 4); - writer.write_u32(self.front_elements.len() as u32, 4); - writer.write_u32(self.side_elements.len() as u32, 4); - writer.write_u32(self.back_elements.len() as u32, 4); - writer.write_u32(self.lfe_element_tag_selects.len() as u32, 2); - writer.write_u32(self.assoc_data_tag_selects.len() as u32, 3); - writer.write_u32(self.valid_cc_elements.len() as u32, 4); - - // ----- Mix-down presence + bodies ----- - match self.mono_mixdown_element_number { - Some(n) => { - writer.write_bit(true); - writer.write_u32(n as u32, 4); - } - None => writer.write_bit(false), - } - match self.stereo_mixdown_element_number { - Some(n) => { - writer.write_bit(true); - writer.write_u32(n as u32, 4); - } - None => writer.write_bit(false), - } - match self.matrix_mixdown { - Some((idx, pseudo)) => { - writer.write_bit(true); - writer.write_u32(idx as u32, 2); - writer.write_bit(pseudo); - } - None => writer.write_bit(false), - } - - // ----- Element lists ----- - for e in &self.front_elements { - writer.write_bit(e.is_cpe); - writer.write_u32(e.tag_select as u32, 4); - } - for e in &self.side_elements { - writer.write_bit(e.is_cpe); - writer.write_u32(e.tag_select as u32, 4); - } - for e in &self.back_elements { - writer.write_bit(e.is_cpe); - writer.write_u32(e.tag_select as u32, 4); - } - for &t in &self.lfe_element_tag_selects { - writer.write_u32(t as u32, 4); - } - for &t in &self.assoc_data_tag_selects { - writer.write_u32(t as u32, 4); - } - for cc in &self.valid_cc_elements { - writer.write_bit(cc.is_ind_sw); - writer.write_u32(cc.tag_select as u32, 4); - } - - // ----- Table 4.2 Note 1 byte_alignment() — relative to the - // PCE's origin reference. The pad is `(8 - from_origin % 8) % - // 8` where `from_origin = writer.bit_position() - - // origin_bit_offset`. For a standalone PCE inside - // `raw_data_block()` the caller passes `origin_bit_offset = - // 0` and this collapses to absolute alignment; for an - // ASC-inline PCE the caller passes the ASC origin so the - // alignment is computed relative to the start of the ASC. - let cur = writer.bit_position(); - let from_origin = cur.saturating_sub(origin_bit_offset); - let pad = (8 - (from_origin % 8)) % 8; - if pad > 0 { - writer.write_u32(0, pad as u32); - } - - // ----- comment_field ----- - writer.write_u32(self.comment_field.len() as u32, 8); - for &b in &self.comment_field { - writer.write_byte(b); - } - Ok(()) - } -} - -fn read_element_selects(reader: &mut BitReader<'_>, n: usize) -> Result> { - let mut out = Vec::with_capacity(n); - for _ in 0..n { - let is_cpe = read_bit(reader)?; - let tag_select = read_u8(reader, 4)?; - out.push(ElementSelect { is_cpe, tag_select }); - } - Ok(out) -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} - -/// Align the reader to the next byte boundary measured from -/// `origin_bit_offset`. Equivalent to `align_to_byte()` when -/// `origin_bit_offset == 0`. -fn align_relative_to_origin(reader: &mut BitReader<'_>, origin_bit_offset: u64) -> Result<()> { - let cur = reader.bit_position(); - let from_origin = cur.saturating_sub(origin_bit_offset); - let pad = (8 - (from_origin % 8)) % 8; - if pad == 0 { - return Ok(()); - } - reader.skip(pad as u32).map_err(|_| Error::UnexpectedEnd)?; - Ok(()) -} diff --git a/crates/vendor/oxideav-aac/src/pcm.rs b/crates/vendor/oxideav-aac/src/pcm.rs deleted file mode 100644 index 4e87df3e..00000000 --- a/crates/vendor/oxideav-aac/src/pcm.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! §4.6.11 time-domain output → integer PCM rendering. -//! -//! The §4.6.11 filterbank ([`crate::filterbank`]) emits one channel's -//! reconstructed time signal as `f64` samples already scaled to the -//! 16-bit full-scale amplitude domain (the `2/N` IMDCT normalisation -//! plus the §4.6.2.3.3 scalefactor gain land the dequantised, windowed, -//! overlap-added output directly on the `±32768` axis). This module -//! turns that floating-point time signal into the integer-PCM -//! representation a sink consumes, and interleaves a frame's channels. -//! -//! Two operations live here, both fully spec-determined: -//! -//! 1. **Rounding to the nearest integer.** ISO/IEC 14496-3 §1.3 defines -//! the `NINT()` nearest-integer operator as *"Returns the nearest -//! integer value to the real-valued argument. Half-integer values are -//! rounded away from zero."* [`nint`] implements exactly that -//! (`floor(x + 0.5)` for `x ≥ 0`, `ceil(x - 0.5)` for `x < 0`), which -//! is the same tie-breaking rule the spec's `//` rounded-division and -//! every other `NINT`-quoting clause use. -//! -//! 2. **Saturation to the output word.** A 16-bit signed sink represents -//! `-32768 ..= 32767`; a sample whose magnitude overshoots that range -//! (possible only on a clipped / full-scale input) saturates to the -//! nearest representable extreme rather than wrapping. [`to_s16`] -//! clamps after rounding. -//! -//! The conversion is *the only* output-rendering step the crate applies: -//! there is no resampler, no dither, and no channel remap. The integer -//! samples are produced in the filterbank's own time order; the optional -//! [`interleave_s16`] helper packs a frame's per-channel buffers into the -//! element-order interleaved layout a multi-channel sink expects. -//! -//! ## Provenance -//! -//! Every constant and rule here is from ISO/IEC 14496-3 (the §1.3 -//! arithmetic-operator definitions and the §4.6.11 filterbank output -//! contract) staged under `docs/audio/aac/`. The full-scale `±32768` -//! amplitude domain is the filterbank's documented output scale (the -//! `2/N` IMDCT factor of [`crate::filterbank`]); this module adds only -//! the spec's `NINT()` rounding and the integer-word saturation. - -use crate::{Error, Result}; - -/// The most negative value a 16-bit signed PCM word can hold. -pub const S16_MIN: i32 = -32768; -/// The most positive value a 16-bit signed PCM word can hold. -pub const S16_MAX: i32 = 32767; - -/// ISO/IEC 14496-3 §1.3 `NINT()` — round a real value to the nearest -/// integer, with half-integers rounded **away from zero**. -/// -/// `NINT(2.5) == 3`, `NINT(-2.5) == -3`, `NINT(2.4) == 2`, -/// `NINT(-2.4) == -2`. A non-finite input (`NaN` / `±∞`) has no nearest -/// integer; it returns `0.0` so a downstream cast cannot trap (the -/// filterbank never emits non-finite output for a well-formed stream, -/// but a hostile bitstream must not be able to poison the PCM cast). -#[must_use] -pub fn nint(x: f64) -> f64 { - if !x.is_finite() { - return 0.0; - } - if x >= 0.0 { - (x + 0.5).floor() - } else { - (x - 0.5).ceil() - } -} - -/// Render one filterbank time-domain sample to a saturating 16-bit -/// signed PCM word. -/// -/// Applies the §1.3 [`nint`] rounding then clamps to -/// [`S16_MIN`]`..=`[`S16_MAX`]. The clamp is a no-op for the -/// well-below-full-scale output of a dequantised LC stream; it only -/// engages on a clipped / full-scale signal whose rounded magnitude -/// would overflow the 16-bit word. -#[must_use] -pub fn to_s16(sample: f64) -> i16 { - nint(sample).clamp(S16_MIN as f64, S16_MAX as f64) as i16 -} - -/// Render a whole channel's time signal to 16-bit PCM in place order, -/// returning a fresh `Vec` of the same length. -#[must_use] -pub fn channel_to_s16(samples: &[f64]) -> Vec { - samples.iter().copied().map(to_s16).collect() -} - -/// Interleave a frame's per-channel time signals into the element-order -/// interleaved 16-bit PCM layout a multi-channel sink consumes. -/// -/// `channels[c][n]` is channel `c`'s sample `n`; the output is -/// `out[n * num_channels + c] = to_s16(channels[c][n])`. Every channel -/// buffer must be the same length (the §4.6.11 per-frame sample count, -/// `1024` for the 1024-line transform family); a length disagreement is -/// rejected with [`Error::PcmInvalid`]. An empty channel list yields an -/// empty buffer. -pub fn interleave_s16(channels: &[Vec]) -> Result> { - if channels.is_empty() { - return Ok(Vec::new()); - } - let frame_len = channels[0].len(); - if channels.iter().any(|c| c.len() != frame_len) { - return Err(Error::PcmInvalid); - } - let num_channels = channels.len(); - let mut out = Vec::with_capacity(frame_len * num_channels); - for n in 0..frame_len { - for ch in channels { - out.push(to_s16(ch[n])); - } - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn nint_rounds_half_away_from_zero() { - // §1.3: half-integers round away from zero. - assert_eq!(nint(2.5), 3.0); - assert_eq!(nint(-2.5), -3.0); - assert_eq!(nint(0.5), 1.0); - assert_eq!(nint(-0.5), -1.0); - assert_eq!(nint(1.5), 2.0); - assert_eq!(nint(-1.5), -2.0); - } - - #[test] - fn nint_rounds_non_halves_to_nearest() { - assert_eq!(nint(2.4), 2.0); - assert_eq!(nint(2.6), 3.0); - assert_eq!(nint(-2.4), -2.0); - assert_eq!(nint(-2.6), -3.0); - assert_eq!(nint(0.0), 0.0); - assert_eq!(nint(-0.0), 0.0); - } - - #[test] - fn nint_non_finite_is_zero() { - assert_eq!(nint(f64::NAN), 0.0); - assert_eq!(nint(f64::INFINITY), 0.0); - assert_eq!(nint(f64::NEG_INFINITY), 0.0); - } - - #[test] - fn to_s16_saturates() { - assert_eq!(to_s16(0.0), 0); - assert_eq!(to_s16(100.4), 100); - assert_eq!(to_s16(100.5), 101); - assert_eq!(to_s16(-100.5), -101); - // Beyond full scale clamps, not wraps. - assert_eq!(to_s16(40000.0), S16_MAX as i16); - assert_eq!(to_s16(-40000.0), S16_MIN as i16); - // The exact extremes round-trip. - assert_eq!(to_s16(32767.0), 32767); - assert_eq!(to_s16(-32768.0), -32768); - // 32767.5 rounds away from zero to 32768 then clamps to 32767. - assert_eq!(to_s16(32767.5), 32767); - // -32768.5 rounds to -32769 then clamps to -32768. - assert_eq!(to_s16(-32768.5), -32768); - } - - #[test] - fn channel_to_s16_maps_each_sample() { - let got = channel_to_s16(&[0.0, 1.4, 1.5, -1.5, 50000.0]); - assert_eq!(got, vec![0, 1, 2, -2, S16_MAX as i16]); - } - - #[test] - fn interleave_two_channels() { - let l = vec![0.0, 10.0, 20.0]; - let r = vec![1.0, 11.0, 21.0]; - let got = interleave_s16(&[l, r]).unwrap(); - assert_eq!(got, vec![0, 1, 10, 11, 20, 21]); - } - - #[test] - fn interleave_single_channel_is_identity_order() { - let mono = vec![3.4, 3.5, -3.5]; - let got = interleave_s16(&[mono]).unwrap(); - assert_eq!(got, vec![3, 4, -4]); - } - - #[test] - fn interleave_empty_is_empty() { - assert!(interleave_s16(&[]).unwrap().is_empty()); - } - - #[test] - fn interleave_rejects_length_mismatch() { - let l = vec![0.0, 1.0]; - let r = vec![0.0, 1.0, 2.0]; - assert!(matches!(interleave_s16(&[l, r]), Err(Error::PcmInvalid))); - } -} diff --git a/crates/vendor/oxideav-aac/src/pns.rs b/crates/vendor/oxideav-aac/src/pns.rs deleted file mode 100644 index 742ceb79..00000000 --- a/crates/vendor/oxideav-aac/src/pns.rs +++ /dev/null @@ -1,701 +0,0 @@ -//! §4.6.13 Perceptual Noise Substitution (PNS) synthesis — ISO/IEC -//! 14496-3. -//! -//! PNS replaces the Huffman-coded / inverse-quantised spectrum of a -//! noise-like scalefactor band with a freshly generated random vector -//! scaled to a transmitted target energy. It is the third channel / -//! noise tool in the §4.6 decode chain (after M/S and before intensity -//! stereo), signalled by the pseudo codebook `NOISE_HCB` (13) in a -//! band's `sfb_cb`. Because no spectral coefficients are transmitted -//! for such a band (during Huffman decoding `NOISE_HCB` is treated -//! exactly like `ZERO_HCB`, §4.6.13.5), the in-band coefficients arrive -//! as silence and this tool fills them. -//! -//! ## §4.6.13.3 decoding process -//! -//! The energy of a noise band is carried by `noise_nrg[g][sfb]` — a -//! value coded *exactly like a scalefactor* (Huffman-DPCM, with the -//! first PNS band of the frame sent as a 9-bit literal) on its own DPCM -//! track seeded at `global_gain - NOISE_OFFSET - 256` -//! (`NOISE_OFFSET == 90`). That accumulation is the upstream job of -//! [`crate::scale_factor_data::accumulate`]; this module consumes the -//! absolute `noise_nrg[g][sfb]` it produces. -//! -//! The per-band synthesis (ISO/IEC 14496-3:2009 §4.6.13.3 pseudo code) -//! is: -//! -//! ```text -//! size = swb_offset[sfb+1] - swb_offset[sfb]; -//! gen_rand_vector(&spec[..], size); /* random vector */ -//! nrg = 0; for i in 0..size { nrg += spec[i]*spec[i]; } -//! sqrt_nrg = sqrt(nrg); -//! scale *= 2.0^(0.25 * noise_nrg[g][sfb]) / sqrt_nrg; -//! for i in 0..size { spec[i] *= scale; } -//! ``` -//! -//! The 2009 revision normalises by the **measured** energy of the -//! generated vector (`sqrt_nrg = sqrt(Σ spec²)`) rather than by an -//! assumed per-sample average energy `MEAN_NRG` (the 2001 form). This -//! removes the dependence on any particular generator's variance: the -//! band that comes out has L2 norm -//! -//! ```text -//! ‖spec‖₂ = sqrt(Σ (spec[i]·scale)²) -//! = scale · sqrt_nrg -//! = 2.0^(0.25 · noise_nrg[g][sfb]) -//! ``` -//! -//! exactly, independent of which random vector was drawn (as long as it -//! is non-zero). The target energy is therefore **spec-determined and -//! deterministic**; only the per-coefficient *phase* of the band is a -//! function of the generator, which the standard deliberately leaves -//! open ("a suitable random number generator can be realized using one -//! multiplication/accumulation per random value", §4.6.13.3). The -//! `2.0^(0.25·noise_nrg)` energy ladder is the same per-quarter-step -//! gain as the §4.6.2.3.3 scalefactor gain. -//! -//! ## Generator -//! -//! [`gen_rand_vector`] is the default generator: a 32-bit -//! multiply-accumulate LCG mapped to signed `f64` values in -//! `[-1.0, 1.0)`, one multiply-accumulate per coefficient as the spec -//! suggests. Because the final scaling normalises away the generator's -//! amplitude, only its *zero-sum-of-squares-avoidance* matters for -//! correctness (a band of length ≥ 1 from this generator always has a -//! non-zero sum of squares). [`apply_pns`] takes the generator as a -//! closure so a caller can substitute a different (e.g. bit-exact -//! reference) source without changing the synthesis maths. -//! -//! The staged clean-room analysis -//! (`docs/audio/aac/pns-gen-rand-vector.md`) pins down exactly how -//! far the standard constrains this tool: the band selection, the -//! noise-energy DPCM, the `2^(0.25·noise_nrg)` target energy, the -//! 2009 measured-energy normalisation, and the correlated-CPE -//! same-vector rule are all normative (and implemented here), while -//! the generator's recurrence, seed, word→coefficient mapping, and -//! state-threading order are **deliberately unspecified** — the spec -//! demands only signed values with a non-zero sum of squares, one -//! multiply-accumulate each. Byte-exact PCM against any *particular* -//! reference decoder's PNS output would require replicating that -//! decoder's exact generator and threading order, which no document -//! can pin; cross-decoder PNS validation is therefore an -//! energy-domain comparison by design (the fixtures-doc §8 check), -//! with the per-bin noise *phase* implementation-defined. The LCG -//! below realises the doc's example recurrence -//! (`state·1664525 + 1013904223`), one of the admissible family. -//! -//! ## §4.6.13.3 channel-pair correlation (`ms_used`) -//! -//! For a channel pair, if the **same** `(group, sfb)` is `NOISE_HCB` in -//! **both** channels and the band's `ms_used` bit is set (or -//! `ms_mask_present == 2`), the *same* random vector is used for both -//! channels (correlated noise); otherwise each channel draws its own -//! (independent noise). No M/S de-matrix is applied to such a band — -//! PNS and M/S are mutually exclusive (§4.6.13.5), so a set `ms_used` -//! bit on a both-channels-noise band selects the shared vector rather -//! than an M/S reconstruction. [`apply_pns_pair`] implements this. -//! -//! ## Decoder block order -//! -//! Per §4.6 the noise components are injected into the output spectrum -//! **prior to** the §4.6.9 TNS step (§4.6.13.5), on the de-interleaved, -//! window-major spectrum produced by -//! [`crate::decoded_spectrum::quant_to_spec`] -//! (`spec[w * window_len + k]`). The per-band coefficient extent -//! `swb_offset[sfb+1]-swb_offset[sfb]` and the -//! `(group, in-group window) → absolute window` mapping match the rest -//! of the pipeline. -//! -//! ## Scope -//! -//! This module is the §4.6.13.3 forward (`global_gain`-seeded) noise -//! synthesis only. The RVLC backward-DPCM noise-energy decode -//! (§4.6.13.3, error-resilient profiles) and the scalable-coder -//! integration (§4.6.13.6) are separate follow-ups; the `noise_nrg` -//! track itself is produced upstream by -//! [`crate::scale_factor_data::accumulate`]. - -use crate::ics_info::IcsInfo; -use crate::section_data::NOISE_HCB; -#[cfg(test)] -use crate::swb_offset::{long_window_offsets, LONG_WINDOW_LEN}; -use crate::{Error, Result}; - -/// §4.6.13.3 `is_noise(group,sfb)` — the noise-band predicate. -/// -/// `true` ⇔ the band's `sfb_cb` is `NOISE_HCB` (13); the band carries a -/// `noise_nrg` energy in place of a scalefactor and no spectral -/// coefficients, and is filled by this tool. -pub fn is_noise(cb: u8) -> bool { - cb == NOISE_HCB -} - -/// §4.6.13.3 `2.0^(0.25 * noise_nrg)` — the target L2 norm of a noise -/// band. -/// -/// The same per-quarter-step gain ladder as the §4.6.2.3.3 scalefactor -/// gain `2^(0.25·(sf−100))`; the absolute `noise_nrg[g][sfb]` plays the -/// role of a scalefactor for the noise energy. After -/// measured-energy normalisation a synthesised band has exactly this -/// L2 norm. -pub fn noise_target_norm(noise_nrg: i32) -> f64 { - 2.0f64.powf(0.25 * noise_nrg as f64) -} - -/// Default `gen_rand_vector(addr, size)` (§4.6.13.3): fill `out` with -/// `out.len()` signed pseudo-random values in `[-1.0, 1.0)` using one -/// multiply-accumulate per value. -/// -/// `state` is the generator's running 32-bit register; pass the same -/// `&mut state` across calls within a frame so independent bands draw -/// independent vectors. The amplitude is irrelevant to the final -/// output — [`apply_pns`] normalises by the measured energy — so this -/// only needs to produce a vector whose sum of squares is non-zero, -/// which it always does for a non-empty band. -/// -/// The recurrence is a 32-bit linear congruential step -/// (`state = state * 1664525 + 1013904223`, both wrapping) whose high -/// bits are mapped to a signed fraction. This is one of the "suitable -/// random number generators" the spec admits; callers needing a -/// different source pass their own closure to [`apply_pns`]. -pub fn gen_rand_vector(out: &mut [f64], state: &mut u32) { - for v in out.iter_mut() { - // One multiply-accumulate per random value (§4.6.13.3). - *state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); - // Map the 32-bit register to a signed fraction in [-1, 1). - // (state as i32) ranges over the full signed 32-bit interval; - // dividing by 2^31 yields [-1.0, 1.0). - *v = (*state as i32) as f64 / 2_147_483_648.0; - } -} - -/// Synthesise one PNS band in place from a pre-filled random vector. -/// -/// `band` is the generated random vector (length `size`); on return it -/// holds the energy-normalised noise band whose L2 norm is exactly -/// `2.0^(0.25 · noise_nrg)` (§4.6.13.3, 2009 measured-energy form). -/// -/// If the random vector is all-zero (sum of squares zero) the band -/// cannot be normalised; per §4.6.13.3 a suitable generator yields a -/// non-zero sum of squares, so this leaves an all-zero band untouched -/// (the only energy-preserving choice) rather than dividing by zero. -fn normalise_band(band: &mut [f64], noise_nrg: i32) { - let nrg: f64 = band.iter().map(|&x| x * x).sum(); - if nrg <= 0.0 { - return; - } - let sqrt_nrg = nrg.sqrt(); - let scale = noise_target_norm(noise_nrg) / sqrt_nrg; - for x in band.iter_mut() { - *x *= scale; - } -} - -/// A single channel's de-interleaved spectrum plus the per-band -/// codebooks and noise energies the §4.6.13.3 synthesis needs. -/// -/// `spec` is the window-major decoded spectrum -/// (`num_windows × window_len`) produced by -/// [`crate::decoded_spectrum::quant_to_spec`]; noise bands arrive as -/// silence and are overwritten in place. `sfb_cb[g][sfb]` selects which -/// bands are noise-coded; `noise_nrg[g][sfb]` is the absolute energy -/// (§4.6.13.3) for each noise band (consulted only where -/// `sfb_cb == NOISE_HCB`). -#[derive(Debug)] -pub struct PnsChannel<'a> { - /// Window-major channel spectrum; noise bands overwritten in place. - pub spec: &'a mut [f64], - /// Per-band `sfb_cb[g][sfb]`. - pub sfb_cb: &'a [Vec], - /// Absolute `noise_nrg[g][sfb]` (§4.6.13.3). - pub noise_nrg: &'a [Vec], -} - -/// Validate that a channel's spectrum / `sfb_cb` / `noise_nrg` shapes -/// agree with `ics_info`, returning the window geometry. -fn channel_geometry<'a>( - spec_len: usize, - sfb_cb: &[Vec], - noise_nrg: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, -) -> Result<(usize, &'a [u16])> { - let window_len = ics_info.window_len()?; - let offsets = ics_info.swb_offsets(fs_index)?; - let num_swb = offsets.len() - 1; - let num_windows = ics_info.num_windows as usize; - let num_groups = ics_info.num_window_groups as usize; - let max_sfb = ics_info.max_sfb as usize; - - if spec_len != num_windows * window_len { - return Err(Error::PnsInvalid); - } - if ics_info.window_group_length.len() != num_groups - || ics_info - .window_group_length - .iter() - .map(|&w| w as usize) - .sum::() - != num_windows - { - return Err(Error::PnsInvalid); - } - if max_sfb > num_swb { - return Err(Error::PnsInvalid); - } - if sfb_cb.len() != num_groups || noise_nrg.len() != num_groups { - return Err(Error::PnsInvalid); - } - for g in 0..num_groups { - if sfb_cb[g].len() < max_sfb || noise_nrg[g].len() < max_sfb { - return Err(Error::PnsInvalid); - } - } - Ok((window_len, offsets)) -} - -/// Apply the §4.6.13.3 noise substitution to one channel in place. -/// -/// For every `(group, sfb)` whose `sfb_cb` is `NOISE_HCB`, every window -/// of the group has its in-band coefficients replaced by a fresh random -/// vector (drawn via `rng`) scaled to L2 norm -/// `2.0^(0.25 · noise_nrg[g][sfb])`. Non-noise bands are left exactly -/// as they arrive. -/// -/// * `chan` — the channel spectrum, codebooks, and noise energies -/// ([`PnsChannel`]). -/// * `ics_info` — supplies `num_window_groups`, `window_group_length`, -/// `max_sfb`, and the window geometry. -/// * `fs_index` — `samplingFrequencyIndex`, selecting the `swb_offset` -/// table. -/// * `rng` — `gen_rand_vector(out)` fills `out` with a fresh random -/// vector; called once per `(group, window, noise sfb)`. Use a -/// stateful closure over [`gen_rand_vector`] for the default -/// generator. -/// -/// Returns [`Error::PnsInvalid`] if the buffer / `sfb_cb` / `noise_nrg` -/// shapes disagree with `ics_info` (see the variant docs). When no band -/// is noise-coded the spectrum is left untouched. -pub fn apply_pns( - chan: &mut PnsChannel<'_>, - ics_info: &IcsInfo, - fs_index: u8, - mut rng: F, -) -> Result<()> -where - F: FnMut(&mut [f64]), -{ - let PnsChannel { - spec, - sfb_cb, - noise_nrg, - } = chan; - - let (window_len, offsets) = - channel_geometry(spec.len(), sfb_cb, noise_nrg, ics_info, fs_index)?; - let num_groups = ics_info.num_window_groups as usize; - let max_sfb = ics_info.max_sfb as usize; - - let mut window_base = 0usize; - for g in 0..num_groups { - let wgl = ics_info.window_group_length[g] as usize; - for sfb in 0..max_sfb { - if !is_noise(sfb_cb[g][sfb]) { - continue; - } - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - let nrg = noise_nrg[g][sfb]; - for b in 0..wgl { - let base = (window_base + b) * window_len; - let band = &mut spec[base + start..base + end]; - rng(band); - normalise_band(band, nrg); - } - } - window_base += wgl; - } - - Ok(()) -} - -/// Apply the §4.6.13.3 noise substitution to a channel pair in place, -/// honouring the shared-random-vector correlation rule. -/// -/// Both channels share the `common_window` `ics_info`. For each -/// `(group, sfb)`: -/// -/// * If `NOISE_HCB` in **both** channels and the band's `ms_used` bit -/// is set (`ms_mask_present == true`, i.e. `ms_mask_present == 1`), -/// **or** `all_shared` is set (`ms_mask_present == 2`, "all bands -/// shared"), the **same** random vector is generated once and scaled -/// independently into each channel (correlated noise; no M/S -/// de-matrix — PNS and M/S are mutually exclusive, §4.6.13.5). -/// * Otherwise each noise band draws an independent vector. -/// -/// `rng(out)` fills `out` with a fresh random vector. -/// -/// * `left` / `right` — the two channels' spectra, codebooks, and noise -/// energies. -/// * `ms_mask_present` — `true` when a per-band `ms_used` mask is -/// present (`ms_mask_present == 1`). -/// * `all_shared` — `true` when `ms_mask_present == 2` (every band's -/// noise vector is shared); when set, `ms_used` is not consulted. -/// * `ms_used` — `ms_used[g][sfb]`; consulted only when `ms_mask_present` -/// is `true` and `all_shared` is `false`. Pass an empty slice -/// otherwise. -/// * `ics_info` / `fs_index` — shared window geometry and `swb_offset` -/// table. -/// -/// Returns [`Error::PnsInvalid`] on any shape mismatch. -#[allow(clippy::too_many_arguments)] -pub fn apply_pns_pair( - left: &mut PnsChannel<'_>, - right: &mut PnsChannel<'_>, - ms_mask_present: bool, - all_shared: bool, - ms_used: &[Vec], - ics_info: &IcsInfo, - fs_index: u8, - mut rng: F, -) -> Result<()> -where - F: FnMut(&mut [f64]), -{ - let (window_len, offsets) = channel_geometry( - left.spec.len(), - left.sfb_cb, - left.noise_nrg, - ics_info, - fs_index, - )?; - // Right channel must match the same geometry. - let (rwindow_len, _) = channel_geometry( - right.spec.len(), - right.sfb_cb, - right.noise_nrg, - ics_info, - fs_index, - )?; - if rwindow_len != window_len { - return Err(Error::PnsInvalid); - } - - let num_groups = ics_info.num_window_groups as usize; - let max_sfb = ics_info.max_sfb as usize; - - if ms_mask_present && !all_shared { - if ms_used.len() != num_groups { - return Err(Error::PnsInvalid); - } - for row in ms_used { - if row.len() < max_sfb { - return Err(Error::PnsInvalid); - } - } - } - - let mut window_base = 0usize; - for g in 0..num_groups { - let wgl = ics_info.window_group_length[g] as usize; - for sfb in 0..max_sfb { - let l_noise = is_noise(left.sfb_cb[g][sfb]); - let r_noise = is_noise(right.sfb_cb[g][sfb]); - if !l_noise && !r_noise { - continue; - } - let start = offsets[sfb] as usize; - let end = offsets[sfb + 1] as usize; - let size = end - start; - // Shared vector only when both channels are noise on this - // band AND the band signals correlation. The `ms_used` - // lookup is gated on `ms_mask_present` (and validated - // above), so the `.get()` chain only matters defensively. - let band_ms_used = ms_used - .get(g) - .and_then(|row| row.get(sfb)) - .copied() - .unwrap_or(false); - let shared = l_noise && r_noise && (all_shared || (ms_mask_present && band_ms_used)); - for b in 0..wgl { - let base = (window_base + b) * window_len; - if shared { - // One random vector, scaled independently into both - // channels (§4.6.13.3 correlated-noise path). - let mut vec = vec![0.0f64; size]; - rng(&mut vec); - let lband = &mut left.spec[base + start..base + end]; - lband.copy_from_slice(&vec); - normalise_band(lband, left.noise_nrg[g][sfb]); - let rband = &mut right.spec[base + start..base + end]; - rband.copy_from_slice(&vec); - normalise_band(rband, right.noise_nrg[g][sfb]); - } else { - if l_noise { - let lband = &mut left.spec[base + start..base + end]; - rng(lband); - normalise_band(lband, left.noise_nrg[g][sfb]); - } - if r_noise { - let rband = &mut right.spec[base + start..base + end]; - rng(rband); - normalise_band(rband, right.noise_nrg[g][sfb]); - } - } - } - } - window_base += wgl; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - use crate::section_data::{INTENSITY_HCB, ZERO_HCB}; - - const FS_48000: u8 = 3; - - /// Build a minimal long-window `IcsInfo` (one group of one window, - /// `max_sfb` bands) for synthesis tests at fs_index 3 (48 kHz). - fn long_ics(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[FS_48000 as usize], - } - } - - /// L2 norm of a slice. - fn norm(s: &[f64]) -> f64 { - s.iter().map(|&x| x * x).sum::().sqrt() - } - - #[test] - fn noise_target_norm_is_quarter_step_ladder() { - assert!((noise_target_norm(0) - 1.0).abs() < 1e-12); - // +4 in noise_nrg doubles the target norm (2^(0.25*4) = 2). - assert!((noise_target_norm(4) - 2.0).abs() < 1e-12); - assert!((noise_target_norm(-4) - 0.5).abs() < 1e-12); - } - - #[test] - fn gen_rand_vector_is_signed_and_nonzero_energy() { - let mut state = 1u32; - let mut v = vec![0.0f64; 16]; - gen_rand_vector(&mut v, &mut state); - // Values lie in [-1, 1) and the sum of squares is non-zero. - assert!(v.iter().all(|&x| (-1.0..1.0).contains(&x))); - assert!(v.iter().map(|&x| x * x).sum::() > 0.0); - // Signed: at least one negative and one positive over 16 draws. - assert!(v.iter().any(|&x| x < 0.0)); - assert!(v.iter().any(|&x| x > 0.0)); - } - - #[test] - fn synthesised_band_has_exact_target_norm() { - // fs_index 3 (48 kHz) long window, single noise band at sfb 0. - let fs = 3u8; - let ics = long_ics(1); - let offsets = long_window_offsets(fs).unwrap(); - let mut spec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let sfb_cb = vec![vec![NOISE_HCB]]; - let noise_nrg = vec![vec![8i32]]; // target norm = 2^(0.25*8) = 4.0 - let mut state = 12345u32; - let mut chan = PnsChannel { - spec: &mut spec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - apply_pns(&mut chan, &ics, fs, |out| gen_rand_vector(out, &mut state)).unwrap(); - let start = offsets[0] as usize; - let end = offsets[1] as usize; - let got = norm(&spec[start..end]); - assert!((got - 4.0).abs() < 1e-9, "band L2 norm {got} != target 4.0"); - // Coefficients outside the noise band are untouched (silent). - assert!(spec[end..].iter().all(|&x| x == 0.0)); - } - - #[test] - fn non_noise_bands_left_untouched() { - let fs = 3u8; - let ics = long_ics(2); - let offsets = long_window_offsets(fs).unwrap(); - let mut spec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - // Seed band 1 (a non-noise band) with a recognisable value. - let b1 = offsets[1] as usize; - spec[b1] = 7.5; - let sfb_cb = vec![vec![NOISE_HCB, ZERO_HCB]]; - let noise_nrg = vec![vec![0i32, 0i32]]; - let mut state = 1u32; - let mut chan = PnsChannel { - spec: &mut spec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - apply_pns(&mut chan, &ics, fs, |out| gen_rand_vector(out, &mut state)).unwrap(); - assert_eq!(spec[b1], 7.5, "non-noise band must be untouched"); - // The noise band (band 0) was filled. - assert!(norm(&spec[offsets[0] as usize..b1]) > 0.0); - } - - #[test] - fn shape_mismatch_is_rejected() { - let fs = 3u8; - let ics = long_ics(1); - let mut spec = vec![0.0f64; 10]; // wrong length - let sfb_cb = vec![vec![NOISE_HCB]]; - let noise_nrg = vec![vec![0i32]]; - let mut chan = PnsChannel { - spec: &mut spec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - let r = apply_pns(&mut chan, &ics, fs, |_| {}); - assert!(matches!(r, Err(Error::PnsInvalid))); - } - - #[test] - fn pair_shared_vector_correlates_noise() { - // Both channels noise at sfb 0 with the SAME noise_nrg and the - // ms_used bit set → shared random vector → the two bands are - // identical (correlated noise), since equal target norms scale - // the same source vector by the same factor. - let fs = 3u8; - let ics = long_ics(1); - let offsets = long_window_offsets(fs).unwrap(); - let mut lspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let mut rspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let sfb_cb = vec![vec![NOISE_HCB]]; - let noise_nrg = vec![vec![4i32]]; - let ms_used = vec![vec![true]]; - let mut state = 999u32; - { - let mut l = PnsChannel { - spec: &mut lspec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - let mut r = PnsChannel { - spec: &mut rspec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - apply_pns_pair(&mut l, &mut r, true, false, &ms_used, &ics, fs, |out| { - gen_rand_vector(out, &mut state) - }) - .unwrap(); - } - let start = offsets[0] as usize; - let end = offsets[1] as usize; - for i in start..end { - assert!( - (lspec[i] - rspec[i]).abs() < 1e-12, - "shared-vector bands must be identical at {i}" - ); - } - assert!((norm(&lspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); - } - - #[test] - fn pair_independent_when_mask_absent() { - // Both channels noise but ms_mask_present == false → independent - // vectors → the two bands differ (overwhelmingly likely for a - // 96-bin band; we assert they are not bit-identical). - let fs = 3u8; - let ics = long_ics(1); - let offsets = long_window_offsets(fs).unwrap(); - let mut lspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let mut rspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let sfb_cb = vec![vec![NOISE_HCB]]; - let noise_nrg = vec![vec![4i32]]; - let mut state = 7u32; - { - let mut l = PnsChannel { - spec: &mut lspec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - let mut r = PnsChannel { - spec: &mut rspec, - sfb_cb: &sfb_cb, - noise_nrg: &noise_nrg, - }; - apply_pns_pair(&mut l, &mut r, false, false, &[], &ics, fs, |out| { - gen_rand_vector(out, &mut state) - }) - .unwrap(); - } - let start = offsets[0] as usize; - let end = offsets[1] as usize; - let identical = (start..end).all(|i| lspec[i] == rspec[i]); - assert!(!identical, "independent draws must not be bit-identical"); - // Both still hit the exact target norm. - assert!((norm(&lspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); - assert!((norm(&rspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); - } - - #[test] - fn pair_one_channel_noise_ignores_ms_used() { - // Only the right channel is noise at sfb 0; ms_used is set but - // must be ignored (§4.6.13.3) — the left band stays silent and - // only the right is filled, independently. - let fs = 3u8; - let ics = long_ics(1); - let offsets = long_window_offsets(fs).unwrap(); - let mut lspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let mut rspec = vec![0.0f64; LONG_WINDOW_LEN as usize]; - let lcb = vec![vec![ZERO_HCB]]; - let rcb = vec![vec![NOISE_HCB]]; - let lnrg = vec![vec![0i32]]; - let rnrg = vec![vec![4i32]]; - let ms_used = vec![vec![true]]; - let mut state = 42u32; - { - let mut l = PnsChannel { - spec: &mut lspec, - sfb_cb: &lcb, - noise_nrg: &lnrg, - }; - let mut r = PnsChannel { - spec: &mut rspec, - sfb_cb: &rcb, - noise_nrg: &rnrg, - }; - apply_pns_pair(&mut l, &mut r, true, false, &ms_used, &ics, fs, |out| { - gen_rand_vector(out, &mut state) - }) - .unwrap(); - } - let start = offsets[0] as usize; - let end = offsets[1] as usize; - assert!( - lspec[start..end].iter().all(|&x| x == 0.0), - "non-noise left band must stay silent" - ); - assert!((norm(&rspec[start..end]) - noise_target_norm(4)).abs() < 1e-9); - } - - #[test] - fn is_noise_predicate() { - assert!(is_noise(NOISE_HCB)); - assert!(!is_noise(ZERO_HCB)); - assert!(!is_noise(INTENSITY_HCB)); - assert!(!is_noise(1)); - } -} diff --git a/crates/vendor/oxideav-aac/src/predictor.rs b/crates/vendor/oxideav-aac/src/predictor.rs deleted file mode 100644 index bc73f800..00000000 --- a/crates/vendor/oxideav-aac/src/predictor.rs +++ /dev/null @@ -1,752 +0,0 @@ -//! MPEG-2 frequency-domain prediction — ISO/IEC 14496-3 §4.6.6 -//! (carried over from ISO/IEC 13818-7). -//! -//! Frequency-domain prediction is the backward-adaptive intra-channel -//! predictor of the AAC **Main** object type. It exploits the -//! auto-correlation between the spectral components of consecutive -//! frames: for every MDCT line up to the §4.6.6.2 `PRED_SFB_MAX` limit -//! there is one second-order, backward-adaptive lattice predictor. The -//! predictor coefficients are derived from previously reconstructed -//! values on both encoder and decoder, so no coefficients are -//! transmitted — only the per-frame / per-sfb on/off side information -//! ([`crate::ics_info::PredictorData`], Table 4.6) controls whether the -//! reconstructed prediction error or the reconstructed spectral value is -//! carried. -//! -//! ## Scope of this module -//! -//! Prediction is only ever applied on the three long window sequences -//! (`ONLY_LONG_SEQUENCE`, `LONG_START_SEQUENCE`, `LONG_STOP_SEQUENCE`); -//! an `EIGHT_SHORT_SEQUENCE` disables prediction and resets every -//! predictor (§4.6.6.3.2.1 / §4.6.6.3.3). This module implements: -//! -//! * the §4.6.6.3.2.1 lattice `predict()` (estimate + LMS adaptation); -//! * the §4.6.6.3.2.3 `flt_round_inf()` 16-bit-float rounding used on -//! every stored state variable and on the predicted value; -//! * the §4.6.6.3.2.1 per-frame reconstruction loop -//! `x_rec = x_est + y_rec` on the predicted bands; -//! * the §4.6.6.3.3 predictor reset (cyclic group reset + short-block -//! reset-all), with the 30 reset groups of Table 4.97. -//! -//! The decode steps, transcribed from the §4.6.6.3.2.1 pseudo code: -//! -//! ```text -//! if (ONLY_LONG || LONG_START || LONG_STOP) { -//! for (sfb = 0; sfb < PRED_SFB_MAX; sfb++) { -//! for (c = swb[sfb]; c < swb[sfb+1]; c++) { -//! x_est[c] = predict(); // lattice estimate -//! if (predictor_data_present && prediction_used[sfb]) -//! x_rec[c] = x_est[c] + y_rec[c]; -//! else -//! x_rec[c] = y_rec[c]; -//! } -//! } -//! } else { -//! reset_all_predictors(); -//! } -//! ``` -//! -//! Each per-coefficient predictor is run **every** frame (whether or not -//! its band is active) so its coefficients keep tracking the signal -//! statistics (§4.6.6.3.2.1, "all the predictors are run all the time"). -//! The post-processing reset of the signalled group then follows -//! (§4.6.6.3.3, "after the normal predictor processing ... has been -//! carried out"). -//! -//! Per §4.6.6 the predicted value `x_est` is rounded to a 16-bit float -//! before use ([`flt_round_inf`]), the six saved state variables `r0, -//! r1, COR1, COR2, VAR1, VAR2` are stored as *truncated* 16-msb floats -//! ([`flt_trunc`]), and the `b / VAR_m` ratio is quantized through the -//! §4.6.6.3.2.4 `make_inv_tables()` lookup pair (7-bit-mantissa -//! nearest-even reciprocal). All three fixed-precision forms are -//! transcribed from the printed listings; the ISO/IEC 14496-26 -//! `am05_*` conformance vectors (AAC Main with long prediction runs) -//! are the empirical anchor. - -use crate::ics_info::{IcsInfo, PredictorData, WindowSequence, PRED_SFB_MAX}; -use crate::swb_offset::long_window_offsets; -use crate::Error; - -type Result = core::result::Result; - -/// §4.6.6.3.2.1 LMS adaptation time constant `α = 0.90625`. -pub const ALPHA: f32 = 0.90625; - -/// §4.6.6.3.2.1 attenuation factor `a = 0.953125`. -pub const A: f32 = 0.953125; - -/// §4.6.6.3.2.1 attenuation factor `b = 0.953125`. -pub const B: f32 = 0.953125; - -/// Number of cyclic reset groups (Table 4.97). Predictor `i` belongs to -/// reset group `(i mod 30) + 1` (the group numbers are 1-based and the -/// values `0` and `31` are reserved, §4.6.6.3.3). -pub const NUM_RESET_GROUPS: usize = 30; - -/// §4.6.6.3.2.3 — round a single-precision float toward infinity to a -/// 16-bit float (a 7-bit mantissa: the 16 most-significant bits of the -/// IEEE-754 storage word). -/// -/// This is the bit-exact transcription of the spec `flt_round_inf()` -/// pseudo code: the low 16 bits of the mantissa are discarded, and if -/// the most-significant discarded bit (`0x00008000`) was set, half an -/// lsb of the retained representation is added so the result rounds -/// toward (away from zero) infinity rather than truncating. The -/// add/subtract dance reproduces the spec's "add 1 lsb and elided one" -/// trick using only float arithmetic on the exponent/sign field. -pub fn flt_round_inf(pf: f32) -> f32 { - let bits = pf.to_bits(); - // Most-significant discarded mantissa bit. - let flg = bits & 0x0000_8000; - // Truncate to the 16 msb (clears the low 16 mantissa bits). - let truncated = bits & 0xffff_0000; - let mut result = f32::from_bits(truncated); - if flg != 0 { - // Build "1 lsb" of the 16-bit representation from the retained - // exponent + sign, then add it (carrying the elided leading - // one) and subtract the elided one again — exactly the spec's - // round-half-toward-infinity sequence. - let exp_sign = truncated & 0xff80_0000; - let one_lsb = exp_sign | 0x0001_0000; - result += f32::from_bits(one_lsb); - result -= f32::from_bits(exp_sign); - } - result -} - -/// §4.6.6.3.2.2 — truncate a single-precision float to its 16 most -/// significant storage bits (a 7-bit mantissa), the storage format of -/// the six saved predictor state variables ("saved as *truncated* -/// IEEE floating-point numbers" — truncation, not rounding). -#[inline] -pub fn flt_trunc(pf: f32) -> f32 { - f32::from_bits(pf.to_bits() & 0xffff_0000) -} - -/// §4.6.6.3.2.4 `flt_round_even()` — round to an 8-bit mantissa, -/// nearest-even, via the printed `frexp`-based listing. Used when -/// building the `b / VAR` inverse tables. -fn flt_round_even(pf: f32) -> f32 { - if pf == 0.0 { - return 0.0; - } - // frexp: pf = mant · 2^exp with mant in [0.5, 1). - let bits = pf.to_bits(); - let biased = ((bits >> 23) & 0xff) as i32; - let exp = biased - 126; - let scale = 2f32.powi(8 - exp); - let tmp = pf * scale; - let mut a = tmp as i64; - if (tmp - a as f32) >= 0.5 { - a += 1; - } - if (tmp - a as f32) == 0.5 { - a &= -2; - } - a as f32 / scale -} - -/// §4.6.6.3.2.4 `make_inv_tables()` — the two lookup tables through -/// which the `b / VAR_m` ratio is computed: `MNT_TABLE[m]` holds -/// `flt_round_even(b / (1.m))` for each 7-bit mantissa prefix, and -/// `EXP_TABLE[e]` holds `1 / 2^(e-127)` for exponent fields whose -/// value exceeds 1.0 (zero otherwise, exactly as the printed listing -/// guards it). `b_over_var` composes them at the state's stored -/// (truncated) precision. -fn mnt_table(i: usize) -> f32 { - let f = f32::from_bits(0x3f80_0000 + ((i as u32) << 16)); - flt_round_even(B / f) -} - -fn exp_table(i: usize) -> f32 { - let f = f32::from_bits((i as u32) << 23); - if f > 1.0 { - 1.0 / f - } else { - 0.0 - } -} - -/// `b / VAR` computed via the §4.6.6.3.2.4 table pair, keyed by the -/// truncated state's 7 mantissa msbs and its exponent field. -#[inline] -fn b_over_var(var: f32) -> f32 { - let bits = var.to_bits(); - let mant7 = ((bits >> 16) & 0x7f) as usize; - let exp = ((bits >> 23) & 0xff) as usize; - MNT_TABLE[mant7] * EXP_TABLE[exp] -} - -/// Precomputed §4.6.6.3.2.4 tables (see [`b_over_var`]). -static MNT_TABLE: std::sync::LazyLock<[f32; 128]> = - std::sync::LazyLock::new(|| core::array::from_fn(mnt_table)); -static EXP_TABLE: std::sync::LazyLock<[f32; 256]> = - std::sync::LazyLock::new(|| core::array::from_fn(exp_table)); - -/// State of one second-order backward-adaptive lattice predictor -/// (§4.6.6.3.2.1), i.e. one MDCT line. -/// -/// The six saved variables of §4.6.6.3.2.2 (`r0, r1, COR1, COR2, VAR1, -/// VAR2`) are stored as 16-bit-truncated floats. [`Self::new`] applies -/// the §4.6.6.3.3 initialisation `r0 = r1 = 0, COR1 = COR2 = 0, -/// VAR1 = VAR2 = 1`. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Predictor { - /// `r_q,0(n-1)` — first basic element's delayed register. - r0: f32, - /// `r_q,1(n-1)` — second basic element's delayed register. - r1: f32, - /// `COR1(n-1)` — first element's running correlation estimate. - cor1: f32, - /// `COR2(n-1)` — second element's running correlation estimate. - cor2: f32, - /// `VAR1(n-1)` — first element's running variance estimate. - var1: f32, - /// `VAR2(n-1)` — second element's running variance estimate. - var2: f32, -} - -impl Default for Predictor { - fn default() -> Self { - Self::new() - } -} - -impl Predictor { - /// §4.6.6.3.3 predictor initialisation: `r0 = r1 = 0, - /// COR1 = COR2 = 0, VAR1 = VAR2 = 1`. - pub fn new() -> Self { - Self { - r0: 0.0, - r1: 0.0, - cor1: 0.0, - cor2: 0.0, - var1: 1.0, - var2: 1.0, - } - } - - /// §4.6.6.3.3 reset — re-initialise to the start-of-decoding state. - pub fn reset(&mut self) { - *self = Self::new(); - } - - /// `b · k_m(n) = COR_m(n-1) · (b / VAR_m(n-1))` for `m = 1, 2` - /// (§4.6.6.3.2.1), with the `b / VAR` factor quantized through the - /// §4.6.6.3.2.4 table pair — the normative fixed-precision form - /// (`VAR_m` is initialised to `1` and never decays to `0`). - fn coefficients(&self) -> (f32, f32) { - ( - self.cor1 * b_over_var(self.var1), - self.cor2 * b_over_var(self.var2), - ) - } - - /// §4.6.6.3.2.1 `predict()` — form the estimate `x_est(n)` from the - /// current state, **without** advancing it. - /// - /// The two cascaded basic elements compute - /// `x_est,m(n) = b · k_m(n) · r_q,m-1(n-1)` and - /// `x_est(n) = x_est,1(n) + x_est,2(n)`, where `r_q,0(n-1) = r0` and - /// `r_q,1(n-1) = r1`. The result is rounded to a 16-bit float per - /// §4.6.6.3.2.2 before use. - pub fn predict(&self) -> f32 { - let (bk1, bk2) = self.coefficients(); - let x_est1 = bk1 * self.r0; - let x_est2 = bk2 * self.r1; - flt_round_inf(x_est1 + x_est2) - } - - /// §4.6.6.3.2.1 — advance the predictor by one frame given the - /// reconstructed spectral value `x_rec(n)` of this line, updating the - /// LMS correlation / variance estimates and the lattice registers. - /// - /// This realises the §4.6.6.3.2.1 recursion: - /// - /// ```text - /// e_q,0(n) = r_q,0(n) = x_rec(n) (for adaptation) - /// x_est,1(n) = b·k1(n)·r_q,0(n-1) - /// e_q,1(n) = e_q,0(n) − x_est,1(n) - /// r_q,1(n) = a·(r_q,0(n-1) − b·k1(n)·e_q,0(n)) - /// x_est,2(n) = b·k2(n)·r_q,1(n-1) - /// COR_m(n) = α·COR_m(n-1) + r_q,m-1(n-1)·e_q,m-1(n) - /// VAR_m(n) = α·VAR_m(n-1) + 0.5·(r_q,m-1²(n-1) + e_q,m-1²(n)) - /// r_q,0(n) = a·x_rec(n) - /// ``` - /// - /// Every stored variable is rounded to a 16-bit float (§4.6.6.3.2.2). - pub fn update(&mut self, x_rec: f32) { - // Only element 1's coefficient enters the lattice register - // update / second-element error; k2 only affects the estimate - // (computed in `predict`). `b·k1` uses the same §4.6.6.3.2.4 - // table-quantized `b / VAR` factor as the estimate path. - let bk1 = self.cor1 * b_over_var(self.var1); - - // Element 1: e_q,0(n) = r_q,0(n) = x_rec(n). - let e0 = x_rec; - let r0_prev = self.r0; - let r1_prev = self.r1; - - // x_est,1(n) = b·k1·r_q,0(n-1); e_q,1(n) = e_q,0(n) − x_est,1(n). - let x_est1 = bk1 * r0_prev; - let e1 = e0 - x_est1; - - // Adapt element 1: COR1, VAR1 use r_q,0(n-1) and e_q,0(n). - let cor1 = ALPHA * self.cor1 + r0_prev * e0; - let var1 = ALPHA * self.var1 + 0.5 * (r0_prev * r0_prev + e0 * e0); - - // Adapt element 2: COR2, VAR2 use r_q,1(n-1) and e_q,1(n). - let cor2 = ALPHA * self.cor2 + r1_prev * e1; - let var2 = ALPHA * self.var2 + 0.5 * (r1_prev * r1_prev + e1 * e1); - - // New lattice registers. - // r_q,1(n) = a·(r_q,0(n-1) − b·k1(n)·e_q,0(n)). - let r1_new = A * (r0_prev - bk1 * e0); - // r_q,0(n) = a·x_rec(n). - let r0_new = A * x_rec; - - // §4.6.6.3.2.2: the six saved state variables are stored as - // *truncated* 16-msb floats (truncation, not the round-to- - // infinity used for x_est). - self.r0 = flt_trunc(r0_new); - self.r1 = flt_trunc(r1_new); - self.cor1 = flt_trunc(cor1); - self.cor2 = flt_trunc(cor2); - self.var1 = flt_trunc(var1); - self.var2 = flt_trunc(var2); - } -} - -/// A per-channel bank of §4.6.6 frequency-domain predictors, one for -/// every MDCT line up to the §4.6.6.2 `PRED_SFB_MAX` coefficient limit. -/// -/// The bank lives for the whole channel decode (across frames), carrying -/// the backward-adaptive state. Construct one per channel with -/// [`PredictorBank::new`] and call [`PredictorBank::apply_long`] each -/// frame (§4.6.6.3.2.1) — including frames where prediction is off, so -/// the LMS coefficients keep adapting. -#[derive(Clone, Debug)] -pub struct PredictorBank { - /// One predictor per coefficient index `0 .. num_predictors`. - predictors: Vec, -} - -impl PredictorBank { - /// Build a fresh bank for the `fs_index` sampling-frequency index. - /// - /// The number of predictors is `swb_offset_long_window[fs_index] - /// [PRED_SFB_MAX[fs_index]]`, i.e. the first MDCT line **above** the - /// last predictable scalefactor band (§4.6.6.2 / Table 4.96). All - /// predictors start in the §4.6.6.3.3 initial state. - /// - /// Errors: the [`Error`] from [`long_window_offsets`] for a bad - /// `fs_index`, or [`Error::PredictorInvalid`] if the long-window - /// offset table is too short to cover `PRED_SFB_MAX`. - pub fn new(fs_index: u8) -> Result { - let offsets = long_window_offsets(fs_index)?; - let pred_sfb_max = PRED_SFB_MAX[fs_index as usize] as usize; - let num_predictors = offsets - .get(pred_sfb_max) - .copied() - .ok_or(Error::PredictorInvalid)? as usize; - Ok(Self { - predictors: vec![Predictor::new(); num_predictors], - }) - } - - /// Number of per-line predictors in the bank. - pub fn len(&self) -> usize { - self.predictors.len() - } - - /// Whether the bank carries no predictors. - pub fn is_empty(&self) -> bool { - self.predictors.is_empty() - } - - /// §4.6.6.3.3 — reset every predictor in the bank (the - /// `reset_all_predictors()` path taken on a short block). - pub fn reset_all(&mut self) { - for p in &mut self.predictors { - p.reset(); - } - } - - /// §4.6.6.3.3 — reset the predictors of one cyclic reset group. - /// - /// `group` is the 1-based `predictor_reset_group_number` (Table 4.97, - /// valid range `1 ..= 30`). Predictor `i` belongs to group - /// `(i mod 30) + 1`, so the members of group `g` are the lines - /// `g-1, g-1+30, g-1+60, …`. - /// - /// Errors: [`Error::PredictorInvalid`] if `group` is `0` or `> 30` - /// (the reserved values of §4.6.6.3.3). - pub fn reset_group(&mut self, group: u8) -> Result<()> { - if group == 0 || group as usize > NUM_RESET_GROUPS { - return Err(Error::PredictorInvalid); - } - let start = (group - 1) as usize; - let mut idx = start; - while idx < self.predictors.len() { - self.predictors[idx].reset(); - idx += NUM_RESET_GROUPS; - } - Ok(()) - } - - /// §4.6.6.3.2.1 — apply frequency-domain prediction to one channel's - /// reconstructed long-window spectrum in place, then advance and (if - /// signalled) reset the predictor bank. - /// - /// * `spec` — the decoded coefficients `y_rec` (the reconstructed - /// quantised prediction error or spectral value), modified to - /// `x_rec` on the predicted bands. Length must be at least the - /// bank's predictor count. - /// * `ics_info` — provides `window_sequence` (prediction only acts on - /// the three long sequences; a short sequence resets the whole bank - /// and leaves the spectrum untouched) and `max_sfb` (bands at or - /// above `max_sfb` carry `prediction_used = 0`). - /// * `pred` — the parsed §4.6.6.3.1 `predictor_data()` side info, or - /// `None` when `predictor_data_present == 0` (prediction off this - /// frame, but the bank is still run to keep adapting). - /// * `fs_index` — selects the §4.5.4 long-window scalefactor-band - /// offsets. - /// - /// Returns `true` if prediction modified the spectrum, `false` - /// otherwise (short block, or no active band). - /// - /// Errors: the [`Error`] from [`long_window_offsets`] for a bad - /// `fs_index`; [`Error::PredictorInvalid`] if `spec` is shorter than - /// the predictor bank or the reset-group number is reserved. - pub fn apply_long( - &mut self, - spec: &mut [f64], - ics_info: &IcsInfo, - pred: Option<&PredictorData>, - fs_index: u8, - ) -> Result { - // Short block: disable prediction and reset every predictor - // (§4.6.6.3.2.1 else-branch / §4.6.6.3.3). - if ics_info.window_sequence == WindowSequence::EightShort { - self.reset_all(); - return Ok(false); - } - - if spec.len() < self.predictors.len() { - return Err(Error::PredictorInvalid); - } - - // Family-aware long-window offsets (the §4.6.6 predictor is a - // long-window tool; the 960-line family shares every band - // start below the PRED_SFB_MAX region with the 1024 table, so - // the bank sizing from `new` stays valid). - let offsets = ics_info.swb_offsets(fs_index)?; - let pred_sfb_max = PRED_SFB_MAX[fs_index as usize] as usize; - let max_sfb = ics_info.max_sfb as usize; - - let mut modified = false; - let num_predictors = self.predictors.len(); - // §4.6.6.3.2.1 — run every predictor every frame; only the - // reconstruction differs by `prediction_used[sfb]`. - for sfb in 0..pred_sfb_max { - let fc = offsets[sfb] as usize; - let lc = (offsets[sfb + 1] as usize).min(num_predictors); - if fc >= lc { - continue; - } - // A band at/above max_sfb has prediction_used = 0 (the bits - // are not transmitted, §4.6.6.2). - let active = sfb < max_sfb - && pred.is_some_and(|p| p.prediction_used.get(sfb).copied().unwrap_or(false)); - for (p, y) in self.predictors[fc..lc] - .iter_mut() - .zip(spec[fc..lc].iter_mut()) - { - // §13.3.2.2 (ISO/IEC 13818-7): "The predicted value - // xest will be rounded to a 16-bit floating point - // representation prior to being used in ANY - // calculation" — the rounding applies to x_est - // itself, not to the x_est + y_rec sum. Rounding the - // sum instead leaves a small persistent bias on - // predicted bands (measured against the ISO/IEC - // 14496-26 am05_48 vector's reference waveform: the - // prediction-bearing channel pair decodes ~5e-3 - // err/sig with the sum-rounding form and ~1e-4 with - // this one). - let x_est = flt_round_inf(p.predict()); - let y_rec = *y as f32; - let x_rec = if active { - modified = true; - x_est + y_rec - } else { - y_rec - }; - *y = x_rec as f64; - p.update(x_rec); - } - } - - // §4.6.6.3.3 — the signalled group reset is applied *after* the - // normal per-frame processing. - if let Some(p) = pred { - if p.reset { - if let Some(group) = p.reset_group_number { - self.reset_group(group)?; - } - } - } - - Ok(modified) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::{WindowSequence, WindowShape}; - - /// Build a minimal long-window `IcsInfo` for predictor tests. - fn long_ics(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: 49, - } - } - - #[test] - fn flt_round_inf_clears_low_16_bits_when_no_rounding() { - // A value whose low 16 mantissa bits are already zero is a - // fixed point of the rounding. - let v = 1.5_f32; // 0x3FC00000, low 16 bits zero. - assert_eq!(flt_round_inf(v).to_bits() & 0x0000_ffff, 0); - assert_eq!(flt_round_inf(v), v); - } - - #[test] - fn flt_round_inf_result_always_has_zero_low_bits() { - for &v in &[0.0_f32, 1.0, -1.0, 3.5_f32, -2.6_f32, 1e-8, 1e8, 0.953125] { - let r = flt_round_inf(v); - assert_eq!( - r.to_bits() & 0x0000_ffff, - 0, - "flt_round_inf({v}) left low mantissa bits set" - ); - } - } - - #[test] - fn flt_round_inf_rounds_toward_infinity() { - // Construct a positive value with the round bit (0x8000) set and - // a larger magnitude in the remaining discarded bits: the result - // must be >= the truncation. - let bits = 1.0_f32.to_bits() | 0x0000_8001; - let v = f32::from_bits(bits); - let truncated = f32::from_bits(bits & 0xffff_0000); - let r = flt_round_inf(v); - assert!(r > truncated, "expected round-up: {r} vs trunc {truncated}"); - assert_eq!(r.to_bits() & 0x0000_ffff, 0); - } - - #[test] - fn fresh_predictor_predicts_zero() { - // With r0 = r1 = 0, the estimate is 0 regardless of COR/VAR. - let p = Predictor::new(); - assert_eq!(p.predict(), 0.0); - } - - #[test] - fn predictor_initial_state_matches_spec() { - let p = Predictor::new(); - assert_eq!(p.r0, 0.0); - assert_eq!(p.r1, 0.0); - assert_eq!(p.cor1, 0.0); - assert_eq!(p.cor2, 0.0); - assert_eq!(p.var1, 1.0); - assert_eq!(p.var2, 1.0); - } - - #[test] - fn update_then_reset_returns_to_initial() { - let mut p = Predictor::new(); - for _ in 0..16 { - p.update(0.7); - } - assert_ne!(p, Predictor::new()); - p.reset(); - assert_eq!(p, Predictor::new()); - } - - #[test] - fn update_advances_lattice_register() { - // After feeding x_rec, r0 should become flt_round_inf(a·x_rec). - let mut p = Predictor::new(); - let x = 2.0_f32; - p.update(x); - assert_eq!(p.r0, flt_round_inf(A * x)); - } - - #[test] - fn bank_size_covers_pred_sfb_max() { - // fs_index 4 (44100 Hz): PRED_SFB_MAX = 40, swb[40] = 672. - let bank = PredictorBank::new(4).unwrap(); - assert_eq!(bank.len(), 672); - assert!(!bank.is_empty()); - } - - #[test] - fn bank_size_24khz() { - // fs_index 6 (24000 Hz): PRED_SFB_MAX = 41, swb[41] = 652. - let bank = PredictorBank::new(6).unwrap(); - assert_eq!(bank.len(), 652); - } - - #[test] - fn reset_group_rejects_reserved_numbers() { - let mut bank = PredictorBank::new(4).unwrap(); - assert!(matches!(bank.reset_group(0), Err(Error::PredictorInvalid))); - assert!(matches!(bank.reset_group(31), Err(Error::PredictorInvalid))); - assert!(bank.reset_group(1).is_ok()); - assert!(bank.reset_group(30).is_ok()); - } - - #[test] - fn reset_group_only_touches_its_members() { - let mut bank = PredictorBank::new(4).unwrap(); - // Dirty every predictor. - for p in &mut bank.predictors { - p.update(0.5); - } - let before: Vec = bank.predictors.clone(); - bank.reset_group(1).unwrap(); - // Group 1 members are lines 0, 30, 60, … — those must be fresh, - // every other line unchanged. - for (i, p) in bank.predictors.iter().enumerate() { - if i % NUM_RESET_GROUPS == 0 { - assert_eq!(*p, Predictor::new(), "line {i} should be reset"); - } else { - assert_eq!(*p, before[i], "line {i} should be untouched"); - } - } - } - - #[test] - fn short_block_resets_and_leaves_spectrum_untouched() { - let mut bank = PredictorBank::new(4).unwrap(); - for p in &mut bank.predictors { - p.update(0.3); - } - let mut ics = long_ics(40); - ics.window_sequence = WindowSequence::EightShort; - let mut spec = vec![1.0_f64; 1024]; - let original = spec.clone(); - let modified = bank.apply_long(&mut spec, &ics, None, 4).unwrap(); - assert!(!modified); - assert_eq!(spec, original); - // Every predictor is back to the initial state. - for p in &bank.predictors { - assert_eq!(*p, Predictor::new()); - } - } - - #[test] - fn prediction_off_leaves_spectrum_but_advances_state() { - // predictor_data_present == 0: spectrum untouched, but predictors - // still run (so they adapt). With a fresh bank, x_est = 0 so the - // spectrum is unchanged either way; verify state advanced. - let mut bank = PredictorBank::new(4).unwrap(); - let ics = long_ics(40); - let mut spec = vec![2.0_f64; 1024]; - let original = spec.clone(); - let modified = bank.apply_long(&mut spec, &ics, None, 4).unwrap(); - assert!(!modified); - assert_eq!(spec, original, "prediction-off must not alter the spectrum"); - // Predictors over the active range advanced (r0 = a·x_rec). - assert_ne!(bank.predictors[0], Predictor::new()); - } - - #[test] - fn active_band_modifies_spectrum_on_second_frame() { - // Frame 1 primes the lattice; frame 2 produces a non-zero - // estimate that is added on the active band. - let mut bank = PredictorBank::new(4).unwrap(); - let mut ics = long_ics(40); - ics.predictor_data_present = true; - let pred = PredictorData { - reset: false, - reset_group_number: None, - // Enable prediction on sfb 0 only. - prediction_used: { - let mut v = vec![false; 40]; - v[0] = true; - v - }, - }; - // Prime the lattice over several frames so the LMS correlation - // and the delayed register r0 build up (a single frame leaves - // COR1 = r0_prev·e0 = 0 because r0_prev starts at zero). - for _ in 0..6 { - let mut spec = vec![0.0_f64; 1024]; - for (c, s) in spec.iter_mut().enumerate().take(8) { - *s = (c as f64) + 1.0; - } - bank.apply_long(&mut spec, &ics, Some(&pred), 4).unwrap(); - } - // Next frame: an active band should now add a non-zero estimate. - let mut spec2 = vec![1.0_f64; 1024]; - let y_rec = spec2.clone(); - let modified = bank.apply_long(&mut spec2, &ics, Some(&pred), 4).unwrap(); - assert!(modified); - // At least one coefficient in sfb 0 changed from its y_rec. - let band0_changed = (0..4).any(|c| spec2[c] != y_rec[c]); - assert!(band0_changed, "active band 0 spectrum did not change"); - } - - #[test] - fn reset_after_processing_clears_signalled_group() { - let mut bank = PredictorBank::new(4).unwrap(); - let mut ics = long_ics(40); - ics.predictor_data_present = true; - let pred = PredictorData { - reset: true, - reset_group_number: Some(1), - prediction_used: vec![true; 40], - }; - let mut spec = vec![3.0_f64; 1024]; - bank.apply_long(&mut spec, &ics, Some(&pred), 4).unwrap(); - // Group 1 lines were reset *after* processing, so they are fresh. - assert_eq!(bank.predictors[0], Predictor::new()); - assert_eq!(bank.predictors[NUM_RESET_GROUPS], Predictor::new()); - // A non-group-1 line still carries adapted state. - assert_ne!(bank.predictors[1], Predictor::new()); - } - - #[test] - fn spec_shorter_than_bank_is_rejected() { - let mut bank = PredictorBank::new(4).unwrap(); - let ics = long_ics(40); - let mut spec = vec![0.0_f64; 100]; - assert!(matches!( - bank.apply_long(&mut spec, &ics, None, 4), - Err(Error::PredictorInvalid) - )); - } - - #[test] - fn bad_fs_index_propagates_error() { - assert!(PredictorBank::new(13).is_err()); - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_data.rs b/crates/vendor/oxideav-aac/src/ps_data.rs deleted file mode 100644 index 078ecf17..00000000 --- a/crates/vendor/oxideav-aac/src/ps_data.rs +++ /dev/null @@ -1,705 +0,0 @@ -//! `ps_data()` — Parametric Stereo bitstream element, ISO/IEC -//! 14496-3:2009 §8.4.2 Tables 8.9–8.14 (+ §8.5.2 semantics). -//! -//! PS conveys the stereo image of an HE-AAC v2 stream as per-band -//! Inter-channel Intensity Differences (IID), Inter-channel -//! Coherences (ICC) and optional Inter-channel / Overall Phase -//! Differences (IPD/OPD), carried inside the SBR `sbr_extension()` -//! container (`bs_extension_id == EXTENSION_ID_PS`, Annex 8.A). -//! -//! ## Header persistence -//! -//! The one-bit `enable_ps_header` gates the configuration block -//! (`enable_iid` / `iid_mode` / `enable_icc` / `icc_mode` / -//! `enable_ext`); when clear, **the latest transmitted configuration -//! persists** (§8.5.2). [`PsData::parse`] therefore takes the previous -//! frame's [`PsConfig`] and returns `Ok(None)` for a headerless -//! element with no prior configuration — per §8.6.5.1 the decoder -//! outputs the mono signal in both channels until a decodable -//! `ps_data()` arrives. -//! -//! ## Differential decode -//! -//! IID/ICC/IPD/OPD parameters are DPCM-coded per envelope, either over -//! frequency (`*_dt[e] == 0`, band `b` relative to band `b-1`, the -//! first band relative to index 0) or over time (`*_dt[e] == 1`, -//! relative to the same band of envelope `e-1`, envelope 0 relative to -//! the previous frame's last envelope). [`PsData::resolve`] applies -//! the accumulation against a caller-threaded [`PsIndexState`] and -//! range-checks the result against the Table 8.24 / 8.27 index ranges -//! (IPD/OPD indices accumulate modulo 8 on the Table 8.31 phase -//! ladder, so they cannot leave their range). `num_env == 0` signals -//! that the previous parameters are held (§8.5.2 / Table 8.50–8.52); -//! `resolve` then produces no envelopes and leaves the state -//! untouched. -//! -//! All truth from ISO/IEC 14496-3:2009 subpart 8 staged under -//! `docs/audio/aac/`. - -use oxideav_core::bits::BitReader; - -use crate::ps_huffman::{ - ps_huff_dec, HUFF_ICC_DF, HUFF_ICC_DT, HUFF_IID_DF, HUFF_IID_DT, HUFF_IID_FINE_DF, - HUFF_IID_FINE_DT, HUFF_IPD_DF, HUFF_IPD_DT, HUFF_OPD_DF, HUFF_OPD_DT, -}; -use crate::{Error, Result}; - -/// `nr_iid_par_tab[iid_mode]` / `nr_icc_par_tab[icc_mode]` — Tables -/// 8.24 / 8.27 (modes 6 and 7 are reserved). -const NR_PAR_TAB: [usize; 6] = [10, 20, 34, 10, 20, 34]; - -/// `nr_ipdopd_par_tab[iid_mode]` — Table 8.24. -const NR_IPDOPD_PAR_TAB: [usize; 6] = [5, 11, 17, 5, 11, 17]; - -/// `num_env_tab[frame_class][num_env_idx]` — Table 8.29. -const NUM_ENV_TAB: [[usize; 4]; 2] = [[0, 1, 2, 4], [1, 2, 3, 4]]; - -/// The persistent `ps_data()` configuration (the `enable_ps_header` -/// block of Table 8.9): which parameters are transmitted and on which -/// band/quantization grid (Tables 8.24 / 8.27). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PsConfig { - /// `enable_iid`. - pub enable_iid: bool, - /// `iid_mode` (0..=5; 6/7 reserved). Meaningful when `enable_iid`. - pub iid_mode: u8, - /// `enable_icc`. - pub enable_icc: bool, - /// `icc_mode` (0..=5; 6/7 reserved). Meaningful when `enable_icc`. - pub icc_mode: u8, - /// `enable_ext` — whether the extension layer (IPD/OPD) may be - /// present. - pub enable_ext: bool, -} - -impl PsConfig { - /// Number of IID parameters per envelope (Table 8.24). - #[must_use] - pub fn nr_iid_par(&self) -> usize { - if self.enable_iid { - NR_PAR_TAB[usize::from(self.iid_mode)] - } else { - 0 - } - } - - /// Number of ICC parameters per envelope (Table 8.27). - #[must_use] - pub fn nr_icc_par(&self) -> usize { - if self.enable_icc { - NR_PAR_TAB[usize::from(self.icc_mode)] - } else { - 0 - } - } - - /// Number of IPD/OPD parameters per envelope (Table 8.24 — coupled - /// to the IID configuration). - #[must_use] - pub fn nr_ipdopd_par(&self) -> usize { - if self.enable_iid { - NR_IPDOPD_PAR_TAB[usize::from(self.iid_mode)] - } else { - 0 - } - } - - /// `iid_quant` — Table 8.24: modes 3..=5 use the fine (±15, - /// Table 8.26) grid, modes 0..=2 the default (±7, Table 8.25). - #[must_use] - pub fn iid_quant_fine(&self) -> bool { - self.iid_mode >= 3 - } - - /// The Table 8.24 IID index bound: 7 (default grid) or 15 (fine). - #[must_use] - pub fn iid_bound(&self) -> i32 { - if self.iid_quant_fine() { - 15 - } else { - 7 - } - } -} - -/// One parsed `ps_data()` element: the effective configuration plus -/// the raw (still differential) parameter deltas of each envelope. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PsData { - /// `enable_ps_header` — whether this element carried a fresh - /// configuration block. - pub header_present: bool, - /// The effective configuration (fresh or inherited). - pub config: PsConfig, - /// `frame_class` — `false` = FIX_BORDERS, `true` = VAR_BORDERS. - pub frame_class: bool, - /// `num_env` (Table 8.29). `0` = hold the previous parameters. - pub num_env: usize, - /// `border_position[e]` (5 bits each) when VAR_BORDERS. - pub border_position: Vec, - /// `iid_dt[e]` — time (`true`) vs frequency differential. - pub iid_dt: Vec, - /// Raw IID deltas per envelope (`nr_iid_par` each). - pub iid_deltas: Vec>, - /// `icc_dt[e]`. - pub icc_dt: Vec, - /// Raw ICC deltas per envelope (`nr_icc_par` each). - pub icc_deltas: Vec>, - /// `enable_ipdopd` (extension layer, Table 8.10); `false` when no - /// extension was present. - pub enable_ipdopd: bool, - /// `ipd_dt[e]`. - pub ipd_dt: Vec, - /// Raw IPD deltas per envelope (`nr_ipdopd_par` each). - pub ipd_deltas: Vec>, - /// `opd_dt[e]`. - pub opd_dt: Vec, - /// Raw OPD deltas per envelope. - pub opd_deltas: Vec>, -} - -impl PsData { - /// Parse one `ps_data()` element (Table 8.9). - /// - /// `prev_config` is the configuration in force from the last - /// element that carried `enable_ps_header == 1`. Returns - /// `Ok(None)` when the element carries no header and no previous - /// configuration exists (§8.6.5.1: output mono until then) — - /// the payload bits are consumed either way. - pub fn parse( - reader: &mut BitReader<'_>, - prev_config: Option<&PsConfig>, - ) -> Result> { - let header_present = read_flag(reader)?; - let config = if header_present { - let enable_iid = read_flag(reader)?; - let mut iid_mode = 0u8; - if enable_iid { - iid_mode = read(reader, 3)? as u8; - if iid_mode > 5 { - return Err(Error::PsDataInvalid); - } - } - let enable_icc = read_flag(reader)?; - let mut icc_mode = 0u8; - if enable_icc { - icc_mode = read(reader, 3)? as u8; - if icc_mode > 5 { - return Err(Error::PsDataInvalid); - } - } - let enable_ext = read_flag(reader)?; - PsConfig { - enable_iid, - iid_mode, - enable_icc, - icc_mode, - enable_ext, - } - } else { - match prev_config { - Some(c) => *c, - // §8.6.5.1: not yet decodable — a conformant stream - // starts with a header'd element; consume nothing more - // and signal "mono until a header arrives". - None => return Ok(None), - } - }; - - let frame_class = read_flag(reader)?; - let num_env_idx = read(reader, 2)? as usize; - let num_env = NUM_ENV_TAB[usize::from(frame_class)][num_env_idx]; - - let mut border_position = Vec::new(); - if frame_class { - for _ in 0..num_env { - border_position.push(read(reader, 5)? as u8); - } - } - - let nr_iid = config.nr_iid_par(); - let mut iid_dt = Vec::with_capacity(num_env); - let mut iid_deltas = Vec::with_capacity(num_env); - if config.enable_iid { - let fine = config.iid_quant_fine(); - for _ in 0..num_env { - let dt = read_flag(reader)?; - iid_dt.push(dt); - let table: &[(u8, u32)] = match (fine, dt) { - (false, false) => &HUFF_IID_DF, - (false, true) => &HUFF_IID_DT, - (true, false) => &HUFF_IID_FINE_DF, - (true, true) => &HUFF_IID_FINE_DT, - }; - let lav = if fine { 30 } else { 14 }; - let mut row = Vec::with_capacity(nr_iid); - for _ in 0..nr_iid { - row.push(ps_huff_dec(reader, table, lav)?); - } - iid_deltas.push(row); - } - } - - let nr_icc = config.nr_icc_par(); - let mut icc_dt = Vec::with_capacity(num_env); - let mut icc_deltas = Vec::with_capacity(num_env); - if config.enable_icc { - for _ in 0..num_env { - let dt = read_flag(reader)?; - icc_dt.push(dt); - let table: &[(u8, u32)] = if dt { &HUFF_ICC_DT } else { &HUFF_ICC_DF }; - let mut row = Vec::with_capacity(nr_icc); - for _ in 0..nr_icc { - row.push(ps_huff_dec(reader, table, 7)?); - } - icc_deltas.push(row); - } - } - - // Extension layer (Tables 8.9/8.10): byte-counted, id-tagged. - let mut enable_ipdopd = false; - let mut ipd_dt = Vec::new(); - let mut ipd_deltas = Vec::new(); - let mut opd_dt = Vec::new(); - let mut opd_deltas = Vec::new(); - if config.enable_ext { - let mut cnt = read(reader, 4)?; - if cnt == 15 { - cnt += read(reader, 8)?; - } - let mut num_bits_left = i64::from(8 * cnt); - let nr_ipdopd = config.nr_ipdopd_par(); - while num_bits_left > 7 { - let id = read(reader, 2)?; - num_bits_left -= 2; - if id == 0 { - // ps_extension(0): optional IPD/OPD + reserved bit. - let start = reader.bit_position(); - enable_ipdopd = read_flag(reader)?; - if enable_ipdopd { - for _ in 0..num_env { - let dt_i = read_flag(reader)?; - ipd_dt.push(dt_i); - let t: &[(u8, u32)] = if dt_i { &HUFF_IPD_DT } else { &HUFF_IPD_DF }; - let mut row = Vec::with_capacity(nr_ipdopd); - for _ in 0..nr_ipdopd { - row.push(ps_huff_dec(reader, t, 0)?); - } - ipd_deltas.push(row); - let dt_o = read_flag(reader)?; - opd_dt.push(dt_o); - let t: &[(u8, u32)] = if dt_o { &HUFF_OPD_DT } else { &HUFF_OPD_DF }; - let mut row = Vec::with_capacity(nr_ipdopd); - for _ in 0..nr_ipdopd { - row.push(ps_huff_dec(reader, t, 0)?); - } - opd_deltas.push(row); - } - } - let _reserved_ps = read_flag(reader)?; - num_bits_left -= (reader.bit_position() - start) as i64; - } else { - // Unknown extension id: the remaining block is fill. - skip_bits(reader, num_bits_left)?; - num_bits_left = 0; - } - } - if num_bits_left < 0 { - return Err(Error::PsDataInvalid); - } - // fill_bits. - skip_bits(reader, num_bits_left)?; - } - - Ok(Some(PsData { - header_present, - config, - frame_class, - num_env, - border_position, - iid_dt, - iid_deltas, - icc_dt, - icc_deltas, - enable_ipdopd, - ipd_dt, - ipd_deltas, - opd_dt, - opd_deltas, - })) - } -} - -/// Cross-frame differential state: the absolute parameter indices of -/// the previous frame's last envelope, plus the band counts they were -/// decoded at (a mode change forces frequency-differential coding on -/// the first envelope, §8.5.2). -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct PsIndexState { - /// Last-envelope absolute IID indices. - pub iid: Vec, - /// Last-envelope absolute ICC indices. - pub icc: Vec, - /// Last-envelope absolute IPD indices (0..8). - pub ipd: Vec, - /// Last-envelope absolute OPD indices (0..8). - pub opd: Vec, -} - -/// The resolved (absolute-index) parameters of one `ps_data()` -/// element: `num_env` rows per enabled parameter kind. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct PsIndices { - /// Absolute IID indices per envelope (Table 8.25/8.26 domain). - pub iid: Vec>, - /// Absolute ICC indices per envelope (Table 8.28 domain, 0..=7). - pub icc: Vec>, - /// Absolute IPD indices per envelope (Table 8.31 ladder, 0..8). - pub ipd: Vec>, - /// Absolute OPD indices per envelope. - pub opd: Vec>, -} - -impl PsData { - /// Resolve the differential deltas to absolute indices against - /// `state` (§8.5.2 `iid_par[e][b]` accumulation), updating `state` - /// to this element's last envelope. Time-differential envelope 0 - /// references the previous frame's last envelope; when the - /// previous state has a different parameter count (mode change — - /// the spec forces `*_dt[0] == 0` there) a zero history is used - /// for robustness. IID/ICC results are range-checked; IPD/OPD - /// accumulate modulo 8. - pub fn resolve(&self, state: &mut PsIndexState) -> Result { - let mut out = PsIndices::default(); - if self.num_env == 0 { - // Parameters held (§8.6.4.6.5); state unchanged. - return Ok(out); - } - let bound = self.config.iid_bound(); - out.iid = resolve_kind( - &self.iid_deltas, - &self.iid_dt, - &mut state.iid, - self.config.nr_iid_par(), - Some((-bound, bound)), - )?; - out.icc = resolve_kind( - &self.icc_deltas, - &self.icc_dt, - &mut state.icc, - self.config.nr_icc_par(), - Some((0, 7)), - )?; - if self.enable_ipdopd { - out.ipd = resolve_kind( - &self.ipd_deltas, - &self.ipd_dt, - &mut state.ipd, - self.config.nr_ipdopd_par(), - None, - )?; - out.opd = resolve_kind( - &self.opd_deltas, - &self.opd_dt, - &mut state.opd, - self.config.nr_ipdopd_par(), - None, - )?; - } else { - // §8.5.2: no IPD/OPD data → parameters are index 0. - state.ipd.clear(); - state.opd.clear(); - } - Ok(out) - } -} - -/// Accumulate one parameter kind's deltas to absolute indices. -/// `range = None` selects the modulo-8 phase accumulation (Table -/// 8.31); `Some((lo, hi))` the range-checked linear accumulation. -fn resolve_kind( - deltas: &[Vec], - dt: &[bool], - state: &mut Vec, - nr_par: usize, - range: Option<(i32, i32)>, -) -> Result>> { - if deltas.is_empty() { - // Parameter kind disabled this frame; reset its history so a - // later re-enable starts from the defaults (§8.5.2 index 0). - state.clear(); - return Ok(Vec::new()); - } - let mut rows: Vec> = Vec::with_capacity(deltas.len()); - for (e, row) in deltas.iter().enumerate() { - let mut abs = Vec::with_capacity(nr_par); - if dt[e] { - // Time differential: reference envelope e-1 (or the - // previous frame's last envelope; zeros on a mode change). - let prev_row: &[i32] = if e > 0 { - &rows[e - 1] - } else if state.len() == nr_par { - state - } else { - &[] - }; - for (b, &d) in row.iter().enumerate().take(nr_par) { - let prev = prev_row.get(b).copied().unwrap_or(0); - abs.push(accumulate(prev, d, range)?); - } - } else { - // Frequency differential: band b references band b-1, - // band 0 references index 0. - let mut prev = 0i32; - for &d in row { - prev = accumulate(prev, d, range)?; - abs.push(prev); - } - } - rows.push(abs); - } - *state = rows.last().cloned().unwrap_or_default(); - Ok(rows) -} - -#[inline] -fn accumulate(prev: i32, delta: i32, range: Option<(i32, i32)>) -> Result { - match range { - Some((lo, hi)) => { - let v = prev + delta; - if v < lo || v > hi { - return Err(Error::PsDataInvalid); - } - Ok(v) - } - None => Ok((prev + delta).rem_euclid(8)), - } -} - -#[inline] -fn read(reader: &mut BitReader<'_>, n: u32) -> Result { - reader.read_u32(n).map_err(|_| Error::PsDataInvalid) -} - -#[inline] -fn read_flag(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::PsDataInvalid) -} - -#[inline] -fn skip_bits(reader: &mut BitReader<'_>, mut n: i64) -> Result<()> { - while n > 0 { - let step = n.min(32) as u32; - read(reader, step)?; - n -= i64::from(step); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::BitWriter; - - /// Write the 1-bit codeword for delta 0 in the coarse IID (`0`), - /// ICC (`0`) tables. - fn write_zero_deltas(w: &mut BitWriter, n: usize) { - for _ in 0..n { - w.write_bit(false); - } - } - - /// Minimal header'd element: IID mode 0 (10 bands), ICC mode 0, - /// no ext, FIX_BORDERS, 1 envelope, all-zero freq deltas. - fn build_min() -> Vec { - let mut w = BitWriter::new(); - w.write_bit(true); // enable_ps_header - w.write_bit(true); // enable_iid - w.write_u32(0, 3); // iid_mode = 0 - w.write_bit(true); // enable_icc - w.write_u32(0, 3); // icc_mode = 0 - w.write_bit(false); // enable_ext - w.write_bit(false); // frame_class = FIX - w.write_u32(1, 2); // num_env_idx = 1 -> num_env = 1 - w.write_bit(false); // iid_dt[0] = freq - write_zero_deltas(&mut w, 10); - w.write_bit(false); // icc_dt[0] = freq - write_zero_deltas(&mut w, 10); - w.finish() - } - - #[test] - fn parses_minimal_headered_element() { - let bytes = build_min(); - let mut r = BitReader::new(&bytes); - let ps = PsData::parse(&mut r, None).unwrap().unwrap(); - assert!(ps.header_present); - assert!(ps.config.enable_iid); - assert_eq!(ps.config.nr_iid_par(), 10); - assert_eq!(ps.config.nr_icc_par(), 10); - assert!(!ps.config.iid_quant_fine()); - assert_eq!(ps.num_env, 1); - assert_eq!(ps.iid_deltas[0], vec![0; 10]); - assert_eq!(ps.icc_deltas[0], vec![0; 10]); - - let mut st = PsIndexState::default(); - let idx = ps.resolve(&mut st).unwrap(); - assert_eq!(idx.iid[0], vec![0; 10]); - assert_eq!(idx.icc[0], vec![0; 10]); - assert_eq!(st.iid, vec![0; 10]); - } - - #[test] - fn headerless_without_prior_config_is_mono_signal() { - let mut w = BitWriter::new(); - w.write_bit(false); // enable_ps_header = 0 - w.write_bit(false); - w.write_u32(0, 2); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(PsData::parse(&mut r, None).unwrap().is_none()); - } - - #[test] - fn headerless_inherits_previous_config() { - // First frame with header, then a headerless frame reusing it. - let bytes = build_min(); - let mut r = BitReader::new(&bytes); - let ps0 = PsData::parse(&mut r, None).unwrap().unwrap(); - - let mut w = BitWriter::new(); - w.write_bit(false); // enable_ps_header = 0 - w.write_bit(false); // frame_class - w.write_u32(1, 2); // num_env = 1 - w.write_bit(true); // iid_dt[0] = time - for _ in 0..10 { - w.write_bit(false); // coarse dt zero-delta codeword `0` - } - w.write_bit(true); // icc_dt[0] = time - for _ in 0..10 { - w.write_bit(false); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ps1 = PsData::parse(&mut r, Some(&ps0.config)).unwrap().unwrap(); - assert!(!ps1.header_present); - assert_eq!(ps1.config, ps0.config); - assert!(ps1.iid_dt[0]); - } - - /// Frequency-differential accumulation: deltas +1 per band ramp - /// the index; time-differential carries envelope-to-envelope. - #[test] - fn differential_accumulation_freq_then_time() { - let mut w = BitWriter::new(); - w.write_bit(true); // header - w.write_bit(true); // enable_iid - w.write_u32(0, 3); // iid_mode 0 - w.write_bit(false); // enable_icc = 0 - w.write_bit(false); // enable_ext = 0 - w.write_bit(false); // FIX - w.write_u32(2, 2); // num_env = 2 - // env 0: freq deltas +1 ×7 then -1 ×3 - // (coarse df: +1 = `100`, -1 = `101`). - w.write_bit(false); - for _ in 0..7 { - w.write_u32(0b100, 3); - } - for _ in 0..3 { - w.write_u32(0b101, 3); - } - // env 1: time deltas -1 ×10 (coarse dt: -1 = `10`). - w.write_bit(true); - for _ in 0..10 { - w.write_u32(0b10, 2); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ps = PsData::parse(&mut r, None).unwrap().unwrap(); - let mut st = PsIndexState::default(); - let idx = ps.resolve(&mut st).unwrap(); - // env 0 freq ramp: +1 ×7 then -1 ×3 → 1..7 then 6,5,4. - assert_eq!(idx.iid[0], vec![1, 2, 3, 4, 5, 6, 7, 6, 5, 4]); - // env 1 subtracts 1 per band from env 0. - assert_eq!(idx.iid[1], vec![0, 1, 2, 3, 4, 5, 6, 5, 4, 3]); - // State carries env 1 forward. - assert_eq!(st.iid, idx.iid[1]); - // ICC disabled: no rows, history cleared. - assert!(idx.icc.is_empty()); - assert!(st.icc.is_empty()); - } - - /// A frequency ramp that leaves the Table 8.24 index range is - /// rejected. - #[test] - fn out_of_range_iid_rejected() { - let mut w = BitWriter::new(); - w.write_bit(true); // header - w.write_bit(true); // enable_iid - w.write_u32(0, 3); // iid_mode 0 (bound ±7) - w.write_bit(false); // enable_icc - w.write_bit(false); // enable_ext - w.write_bit(false); // FIX - w.write_u32(1, 2); // num_env = 1 - w.write_bit(false); // freq - for _ in 0..10 { - w.write_u32(0b100, 3); // +1 each → crosses +7 at band 7 - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ps = PsData::parse(&mut r, None).unwrap().unwrap(); - let mut st = PsIndexState::default(); - assert!(matches!(ps.resolve(&mut st), Err(Error::PsDataInvalid))); - } - - /// VAR_BORDERS carries 5-bit border positions; the extension - /// layer decodes IPD/OPD with modulo-8 accumulation. - #[test] - fn var_borders_and_ipdopd_extension() { - let mut w = BitWriter::new(); - w.write_bit(true); // header - w.write_bit(true); // enable_iid - w.write_u32(0, 3); // iid_mode 0 → nr_ipdopd_par = 5 - w.write_bit(false); // enable_icc - w.write_bit(true); // enable_ext - w.write_bit(true); // frame_class = VAR - w.write_u32(0, 2); // num_env_idx 0 → num_env = 1 (VAR column) - w.write_u32(15, 5); // border_position[0] - w.write_bit(false); // iid_dt[0] = freq - for _ in 0..10 { - w.write_bit(false); // zero deltas - } - // Extension: ps_extension_size counts whole bytes. Body: - // id(2) + enable_ipdopd(1) + ipd_dt(1) + 5×ipd deltas + - // opd_dt(1) + 5×opd deltas + reserved(1) then fill. Zero - // phase deltas are the 1-bit codeword `1`. - let mut body = BitWriter::new(); - body.write_u32(0, 2); // ps_extension_id = 0 - body.write_bit(true); // enable_ipdopd - body.write_bit(false); // ipd_dt[0] = freq - for _ in 0..5 { - body.write_bit(true); // delta 0 - } - body.write_bit(false); // opd_dt[0] - for _ in 0..5 { - body.write_bit(true); - } - body.write_bit(false); // reserved_ps - let body_bytes = body.finish(); // padded to whole bytes = fill - w.write_u32(body_bytes.len() as u32, 4); // ps_extension_size - for &b in &body_bytes { - w.write_u32(u32::from(b), 8); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ps = PsData::parse(&mut r, None).unwrap().unwrap(); - assert!(ps.frame_class); - assert_eq!(ps.border_position, vec![15]); - assert!(ps.enable_ipdopd); - assert_eq!(ps.ipd_deltas[0], vec![0; 5]); - let mut st = PsIndexState::default(); - let idx = ps.resolve(&mut st).unwrap(); - assert_eq!(idx.ipd[0], vec![0; 5]); - assert_eq!(idx.opd[0], vec![0; 5]); - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_decoder.rs b/crates/vendor/oxideav-aac/src/ps_decoder.rs deleted file mode 100644 index 4a428cec..00000000 --- a/crates/vendor/oxideav-aac/src/ps_decoder.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! PS frame driver — ISO/IEC 14496-3:2009 Annex 8.A (combination of -//! the SBR tool with the parametric stereo tool). -//! -//! Composes the whole §8.6.4 chain per stereo frame: `ps_data()` -//! parse (with the persistent header configuration), differential -//! index resolution, the hybrid analysis of the Annex 8.A.3 `Xinput` -//! matrix (32 SBR slots + 6 look-ahead slots from `XLow`), -//! de-correlation with the per-frame partial reset above the -//! SBR-generated spectrum (`kmax = k_x + M + 7` hybrid channels for -//! 10/20 stereo bands, `+ 27` for 34 — the split-region offsets), the -//! §8.6.4.6 stereo mixing, and the hybrid synthesis back to two -//! 64-band QMF matrices ready for the final synthesis filterbanks. -//! -//! Per §8.6.5.1 the decoder stays *inactive* (mono output duplicated -//! by the caller) until the first `ps_data()` that carries -//! `enable_ps_header == 1` arrives; per Annex 8.A.3 a frame with no -//! `ps_data()` after activation holds the previous parameters, and a -//! *missing previous* `ps_data()` forces a full de-correlator reset. -//! Table 8.44 picks the stereo band count from the IID/ICC modes -//! (either at 34 bands → 34, else 20); a switch re-maps the retained -//! mixing coefficients (Table 8.47) and resets the hybrid / -//! de-correlator state. -//! -//! All truth from ISO/IEC 14496-3:2009 subpart 8 + Annex 8.A staged -//! under `docs/audio/aac/`. - -use oxideav_core::bits::BitReader; - -use crate::ps_data::{PsConfig, PsData, PsIndexState}; -use crate::ps_decorr::PsDecorr; -use crate::ps_hybrid::{synthesize, HybridConfig, PsHybrid}; -use crate::ps_stereo::PsStereo; -use crate::sbr_qmf::Complex; -use crate::Result; - -/// A stereo pair of 64-band QMF matrices (`NUM_QMF_SLOTS` slots). -pub type QmfPair = (Vec<[Complex; 64]>, Vec<[Complex; 64]>); - -/// The Annex 8.A PS decoder: one instance per SBR channel element. -#[derive(Debug)] -pub struct PsDecoder { - /// Persistent `enable_ps_header` configuration (§8.5.2). - config: Option, - /// Cross-frame differential-index state. - idx_state: PsIndexState, - hybrid: PsHybrid, - decorr: PsDecorr, - stereo: PsStereo, - /// Whether the previous frame carried a `ps_data()` element - /// (Annex 8.A.3 full-reset rule). - prev_frame_had_ps: bool, - /// Whether a decodable (header-carrying) `ps_data()` has arrived. - active: bool, -} - -impl Default for PsDecoder { - fn default() -> Self { - PsDecoder::new() - } -} - -impl PsDecoder { - /// A fresh, inactive PS decoder (20-band configuration until the - /// first header says otherwise). - #[must_use] - pub fn new() -> Self { - PsDecoder { - config: None, - idx_state: PsIndexState::default(), - hybrid: PsHybrid::new(HybridConfig::Bands1020), - decorr: PsDecorr::new(HybridConfig::Bands1020), - stereo: PsStereo::new(20), - prev_frame_had_ps: false, - active: false, - } - } - - /// Whether a decodable `ps_data()` has been received — before - /// this, the caller outputs the mono signal on both channels. - #[must_use] - pub fn active(&self) -> bool { - self.active - } - - /// Process one stereo frame. - /// - /// * `payload` — the raw `sbr_extension()` body bytes carrying - /// `ps_data()` (already stripped of the 2-bit extension id), or - /// `None` when this frame transmitted no PS data (parameters - /// hold). - /// * `x_input` — the Annex 8.A.3 `Xinput` matrix: - /// `NUM_QMF_SLOTS + LOOKAHEAD` slots of 64 QMF bands (the - /// look-ahead tail needs only the split bands populated). - /// * `kx_plus_m` — `k_x + M` (§4.6.18.3.2.2): the first QMF band - /// above the SBR-generated spectrum, for the per-frame partial - /// de-correlator reset (pass 32 for a pure-upsampled frame). - /// - /// Returns `Ok(None)` while inactive (§8.6.5.1 — the caller - /// duplicates the mono synthesis), otherwise the left/right QMF - /// matrices for two independent §4.6.18.4.2 synthesis banks. - pub fn process( - &mut self, - payload: Option<&[u8]>, - x_input: &[[Complex; 64]], - kx_plus_m: usize, - ) -> Result> { - // Parse (and activate on the first header'd element). - let parsed: Option = match payload { - Some(bytes) => { - let mut reader = BitReader::new(bytes); - PsData::parse(&mut reader, self.config.as_ref())? - } - None => None, - }; - if let Some(ps) = &parsed { - self.config = Some(ps.config); - self.active = true; - } - let Some(config) = self.config else { - // Not yet decodable: mono until a header arrives. - self.prev_frame_had_ps = payload.is_some(); - return Ok(None); - }; - if !self.active { - self.prev_frame_had_ps = payload.is_some(); - return Ok(None); - } - - // Table 8.44: 34 stereo bands iff either parameter kind runs - // on the 34-band grid; disabled kinds count as 20. - let bands34 = (config.enable_iid && config.iid_mode % 3 == 2) - || (config.enable_icc && config.icc_mode % 3 == 2); - let hcfg = if bands34 { - HybridConfig::Bands34 - } else { - HybridConfig::Bands1020 - }; - if hcfg != self.hybrid.config() { - // Table 8.47: instantaneous filterbank switch, coefficient - // re-map, de-correlator reset. - self.hybrid.reset(hcfg); - self.decorr = PsDecorr::new(hcfg); - self.stereo.switch_bands(if bands34 { 34 } else { 20 }); - } - - // Annex 8.A.3 resets: full when the previous frame had no - // ps_data(); otherwise partial above the SBR spectrum. - if !self.prev_frame_had_ps { - self.decorr.reset_bands(0); - } else { - let off = if bands34 { 27 } else { 7 }; - let kmax = (kx_plus_m + off).min(hcfg.nr_bands()); - self.decorr.reset_bands(kmax); - } - - // The hold element for a frame with no (new) parameters. - let ps = parsed.unwrap_or_else(|| hold_element(config)); - let idx = ps.resolve(&mut self.idx_state)?; - - // Hybrid analysis → de-correlation → stereo mixing → - // hybrid synthesis. - let s = self.hybrid.analyze(x_input)?; - let d = self.decorr.process(&s)?; - let (l, r) = self.stereo.process(&ps, &idx, hcfg, &s, &d)?; - let l_qmf = synthesize(hcfg, &l); - let r_qmf = synthesize(hcfg, &r); - - self.prev_frame_had_ps = payload.is_some(); - Ok(Some((l_qmf, r_qmf))) - } -} - -/// A `num_env == 0` element holding the previous parameters -/// (§8.6.4.6.5 / Table 8.50–8.52). -fn hold_element(config: PsConfig) -> PsData { - PsData { - header_present: false, - config, - frame_class: false, - num_env: 0, - border_position: Vec::new(), - iid_dt: Vec::new(), - iid_deltas: Vec::new(), - icc_dt: Vec::new(), - icc_deltas: Vec::new(), - enable_ipdopd: false, - ipd_dt: Vec::new(), - ipd_deltas: Vec::new(), - opd_dt: Vec::new(), - opd_deltas: Vec::new(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ps_hybrid::{LOOKAHEAD, NUM_QMF_SLOTS}; - use oxideav_core::bits::BitWriter; - - /// Build a header'd one-envelope ps_data payload: coarse IID with - /// a uniform index, ICC index 0 everywhere (freq differential). - fn payload(iid_idx: i32) -> Vec { - let mut w = BitWriter::new(); - w.write_bit(true); // enable_ps_header - w.write_bit(true); // enable_iid - w.write_u32(0, 3); // iid_mode 0 - w.write_bit(true); // enable_icc - w.write_u32(0, 3); // icc_mode 0 - w.write_bit(false); // enable_ext - w.write_bit(false); // FIX - w.write_u32(1, 2); // num_env = 1 - w.write_bit(false); // iid_dt = freq - let (len, code) = crate::ps_huffman::HUFF_IID_DF[(iid_idx + 14) as usize]; - w.write_u32(code, u32::from(len)); - let (l0, c0) = crate::ps_huffman::HUFF_IID_DF[14]; - for _ in 1..10 { - w.write_u32(c0, u32::from(l0)); - } - w.write_bit(false); // icc_dt = freq - let (li, ci) = crate::ps_huffman::HUFF_ICC_DF[7]; - for _ in 0..10 { - w.write_u32(ci, u32::from(li)); - } - w.finish() - } - - fn x_input_ones() -> Vec<[Complex; 64]> { - (0..NUM_QMF_SLOTS + LOOKAHEAD) - .map(|_| [Complex::new(1.0, 0.0); 64]) - .collect() - } - - /// Inactive until a header'd element arrives; then the stereo - /// output appears and a hold frame keeps producing it. - #[test] - fn activation_and_hold() { - let mut dec = PsDecoder::new(); - let x = x_input_ones(); - // No payload → inactive. - assert!(dec.process(None, &x, 32).unwrap().is_none()); - // Headerless payload with no prior config → still inactive. - let mut w = BitWriter::new(); - w.write_bit(false); // enable_ps_header = 0 - w.write_bit(false); // frame_class - w.write_u32(0, 2); // num_env_idx → num_env = 0 - let headerless = w.finish(); - assert!(dec.process(Some(&headerless), &x, 32).unwrap().is_none()); - // Header'd element → active, stereo out. - let p = payload(7); // +25 dB left - let out = dec.process(Some(&p), &x, 32).unwrap(); - let (l, r) = out.expect("active after header"); - assert_eq!(l.len(), NUM_QMF_SLOTS); - assert_eq!(r.len(), NUM_QMF_SLOTS); - // Hold frame (no payload) keeps producing stereo. - assert!(dec.process(None, &x, 32).unwrap().is_some()); - } - - /// A large positive IID tilts the energy to the left channel - /// (steady state, after a couple of frames of interpolation). - #[test] - fn iid_tilts_energy_left() { - let mut dec = PsDecoder::new(); - let x = x_input_ones(); - let p = payload(7); - let mut l_e = 0.0f64; - let mut r_e = 0.0f64; - for f in 0..4 { - let out = dec.process(Some(&p), &x, 32).unwrap().unwrap(); - if f >= 2 { - for n in 0..NUM_QMF_SLOTS { - for k in 0..64 { - l_e += out.0[n][k].norm_sqr(); - r_e += out.1[n][k].norm_sqr(); - } - } - } - } - // 25 dB IID → power ratio 10^2.5 ≈ 316; allow generous slack - // for the decorrelated component and filter transients. - assert!(l_e > 50.0 * r_e, "left {l_e} not dominant over right {r_e}"); - } - - /// IID 0 + ICC 1 reproduces the mono signal identically on both - /// channels in steady state (h11 = h12 = 1, h21 = h22 = 0). - #[test] - fn neutral_cues_give_dual_mono() { - let mut dec = PsDecoder::new(); - let x = x_input_ones(); - let p = payload(0); - let mut last = None; - for _ in 0..3 { - last = dec.process(Some(&p), &x, 32).unwrap(); - } - let (l, r) = last.unwrap(); - for n in 0..NUM_QMF_SLOTS { - for k in 0..64 { - let d = l[n][k] - r[n][k]; - assert!(d.norm_sqr() < 1e-20, "slot {n} band {k}"); - // And the mono signal passes through: DC input in - // every QMF band re-appears (the hybrid partition is - // exact). - } - } - let d = l[16][10] - Complex::new(1.0, 0.0); - assert!(d.norm_sqr() < 1e-18, "mono pass-through broken: {d:?}"); - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_decorr.rs b/crates/vendor/oxideav-aac/src/ps_decorr.rs deleted file mode 100644 index f0229468..00000000 --- a/crates/vendor/oxideav-aac/src/ps_decorr.rs +++ /dev/null @@ -1,519 +0,0 @@ -//! PS de-correlation — ISO/IEC 14496-3:2009 §8.6.4.5. -//! -//! The stereo reconstruction mixes the mono hybrid signal `s_k(n)` -//! with a de-correlated version `d_k(n)` of itself. Per §8.6.4.5.2 -//! the first `NR_ALLPASS_BANDS` hybrid channels run through a chain -//! of `NR_ALLPASS_LINKS = 3` complex all-pass sections behind a -//! 2-slot delay and a fractional-delay rotation: -//! -//! ```text -//! H_k(z) = z⁻² · φ_fract(k) · Π_m (Q(k,m)·z^(−d(m)) − a(m)·g(k)) -//! / (1 − a(m)·g(k)·Q(k,m)·z^(−d(m))) -//! ``` -//! -//! with `a(m) = {0.65143905753106, 0.56471812200776, 0.48954165955695}`, -//! `d(m) = {3, 4, 5}` (Table 8.39), the unit rotations -//! `Q(k,m) = exp(−iπ·q(m)·fcenter(k))` (`q = {0.43, 0.75, 0.347}`, -//! Table 8.42), `φ_fract(k) = exp(−iπ·q_φ·fcenter(k))` (`q_φ = 0.39`), -//! and the frequency-dependent decay -//! `g(k) = max(0, 1 − DECAY_SLOPE·(k − DECAY_CUTOFF))`. The centre -//! frequencies `fcenter(k)` come from Table 8.40 / 8.41 for the split -//! region and the closed forms `k + 1/2 − 7` / `k + 1/2 − 27` above -//! it. Bands `NR_ALLPASS_BANDS..` use a plain delay: 14 slots up to -//! `SHORT_DELAY_BAND`, 1 slot above. -//! -//! §8.6.4.5.3–5.4 duck the de-correlated signal at transients: the -//! per-stereo-band input power is peak-decayed -//! (`α = 0.76592833836465`, Table 8.43), both the power and the -//! peak-minus-power difference are smoothed with the one-pole -//! `H_smooth` (`a_smooth = 0.25`), and wherever -//! `γ·PSmoothPeakDecayDiff > PSmoothNrg` (`γ = 1.5`) the output is -//! scaled by their ratio. -//! -//! [`PsDecorr`] carries every filter/delay/detector state across -//! frames and exposes the Annex 8.A.3 resets: `reset_bands(kmax)` -//! zeroes the state of hybrid channels `k ≥ kmax` each stereo frame -//! (the region above the SBR-generated spectrum), and a full reset -//! covers the "no `ps_data()` in the previous frame" rule. -//! -//! All truth from ISO/IEC 14496-3:2009 §8.6.4.5 / Annex 8.A staged -//! under `docs/audio/aac/`. - -use crate::ps_hybrid::HybridConfig; -use crate::ps_map::parameter_map; -use crate::sbr_qmf::Complex; -use crate::{Error, Result}; - -/// `DECAY_SLOPE` (§8.6.4.5.1). -const DECAY_SLOPE: f64 = 0.05; - -/// `a(m)` — all-pass filter coefficients (Table 8.39). -const A: [f64; 3] = [0.65143905753106, 0.56471812200776, 0.48954165955695]; - -/// `d(m)` — all-pass link delays (Table 8.39). -const D: [usize; 3] = [3, 4, 5]; - -/// `q(m)` — fractional delay lengths (Table 8.42). -const Q_FRACT: [f64; 3] = [0.43, 0.75, 0.347]; - -/// `q_φ` — fractional delay constant (§8.6.4.5.2). -const Q_PHI: f64 = 0.39; - -/// Peak decay factor `α` (Table 8.43). -const PEAK_DECAY: f64 = 0.76592833836465; - -/// Smoothing coefficient `a_smooth` (§8.6.4.5.1). -const A_SMOOTH: f64 = 0.25; - -/// Transient impact factor `γ` (§8.6.4.5.3). -const GAMMA: f64 = 1.5; - -/// Long delay for the non-all-pass mid bands (§8.6.4.5.2). -const LONG_DELAY: usize = 14; - -/// Table 8.40 — `fcenter_20(k)` for the split region (k = 0..10). -const F_CENTER_20: [f64; 10] = [ - -3.0 / 8.0, - -1.0 / 8.0, - 1.0 / 8.0, - 3.0 / 8.0, - 5.0 / 8.0, - 7.0 / 8.0, - 5.0 / 4.0, - 7.0 / 4.0, - 9.0 / 4.0, - 11.0 / 4.0, -]; - -/// Table 8.41 — `fcenter_34(k)` for the split region (k = 0..32). -const F_CENTER_34: [f64; 32] = [ - 1.0 / 12.0, - 3.0 / 12.0, - 5.0 / 12.0, - 7.0 / 12.0, - 9.0 / 12.0, - 11.0 / 12.0, - 13.0 / 12.0, - 15.0 / 12.0, - 17.0 / 12.0, - -5.0 / 12.0, - -3.0 / 12.0, - -1.0 / 12.0, - 17.0 / 8.0, - 19.0 / 8.0, - 5.0 / 8.0, - 7.0 / 8.0, - 9.0 / 8.0, - 11.0 / 8.0, - 13.0 / 8.0, - 15.0 / 8.0, - 9.0 / 4.0, - 11.0 / 4.0, - 13.0 / 4.0, - 7.0 / 4.0, - 17.0 / 4.0, - 11.0 / 4.0, - 13.0 / 4.0, - 15.0 / 4.0, - 17.0 / 4.0, - 19.0 / 4.0, - 21.0 / 4.0, - 15.0 / 4.0, -]; - -/// The §8.6.4.5.1 configuration constants that depend on the stereo -/// band count. -#[derive(Debug, Clone, Copy)] -struct DecorrConsts { - nr_par_bands: usize, - nr_bands: usize, - decay_cutoff: usize, - nr_allpass_bands: usize, - short_delay_band: usize, -} - -fn consts(config: HybridConfig) -> DecorrConsts { - match config { - HybridConfig::Bands1020 => DecorrConsts { - nr_par_bands: 20, - nr_bands: 71, - decay_cutoff: 10, - nr_allpass_bands: 30, - short_delay_band: 42, - }, - HybridConfig::Bands34 => DecorrConsts { - nr_par_bands: 34, - nr_bands: 91, - decay_cutoff: 32, - nr_allpass_bands: 50, - short_delay_band: 62, - }, - } -} - -/// `fcenter(k)` for the all-pass region (§8.6.4.5.2). -fn f_center(config: HybridConfig, k: usize) -> f64 { - match config { - HybridConfig::Bands1020 => { - if k < F_CENTER_20.len() { - F_CENTER_20[k] - } else { - k as f64 + 0.5 - 7.0 - } - } - HybridConfig::Bands34 => { - if k < F_CENTER_34.len() { - F_CENTER_34[k] - } else { - k as f64 + 0.5 - 27.0 - } - } - } -} - -/// Per-band all-pass state: the z⁻² input delay plus one direct-form -/// ring per link (`w[n] = u[n] + a·g·Q·w[n−d]`, -/// `v[n] = Q·w[n−d] − a·g·w[n]`). -#[derive(Debug, Clone)] -struct AllpassState { - /// z⁻² input history (index 0 = one slot ago). - in2: [Complex; 2], - /// Ring buffers for the three links (lengths 3, 4, 5). - w: [Vec; 3], - /// Ring positions. - pos: [usize; 3], -} - -impl AllpassState { - fn new() -> Self { - AllpassState { - in2: [Complex::default(); 2], - w: [ - vec![Complex::default(); D[0]], - vec![Complex::default(); D[1]], - vec![Complex::default(); D[2]], - ], - pos: [0; 3], - } - } - - fn reset(&mut self) { - self.in2 = [Complex::default(); 2]; - for (w, d) in self.w.iter_mut().zip(D) { - w.iter_mut().for_each(|c| *c = Complex::default()); - debug_assert_eq!(w.len(), d); - } - self.pos = [0; 3]; - } -} - -/// The §8.6.4.5 de-correlator (one instance per PS decoder). -#[derive(Debug, Clone)] -pub struct PsDecorr { - config: HybridConfig, - /// All-pass state per band `k < NR_ALLPASS_BANDS`. - allpass: Vec, - /// Pre-computed `φ_fract(k)` per all-pass band. - phi_fract: Vec, - /// Pre-computed `Q(k,m)·1` per all-pass band and link. - q_fract: Vec<[Complex; 3]>, - /// `g_DecaySlope(k)` per all-pass band. - g_decay: Vec, - /// Delay lines for the non-all-pass bands (14 or 1 slots each). - delay: Vec>, - /// Ring positions for `delay`. - delay_pos: Vec, - /// Transient detector state per stereo band. - peak_decay_nrg: Vec, - smooth_nrg: Vec, - smooth_peak_diff: Vec, -} - -impl PsDecorr { - /// A fresh de-correlator for `config`. - #[must_use] - pub fn new(config: HybridConfig) -> Self { - let c = consts(config); - let mut phi_fract = Vec::with_capacity(c.nr_allpass_bands); - let mut q_fract = Vec::with_capacity(c.nr_allpass_bands); - let mut g_decay = Vec::with_capacity(c.nr_allpass_bands); - for k in 0..c.nr_allpass_bands { - let f = f_center(config, k); - let arg = -core::f64::consts::PI * Q_PHI * f; - let (s, co) = arg.sin_cos(); - phi_fract.push(Complex::new(co, s)); - let mut qs = [Complex::default(); 3]; - for (m, q) in qs.iter_mut().enumerate() { - let arg = -core::f64::consts::PI * Q_FRACT[m] * f; - let (s, co) = arg.sin_cos(); - *q = Complex::new(co, s); - } - q_fract.push(qs); - let g = if k > c.decay_cutoff { - (1.0 - DECAY_SLOPE * (k as f64 - c.decay_cutoff as f64)).max(0.0) - } else { - 1.0 - }; - g_decay.push(g); - } - let mut delay = Vec::with_capacity(c.nr_bands - c.nr_allpass_bands); - for k in c.nr_allpass_bands..c.nr_bands { - let d = if k < c.short_delay_band { - LONG_DELAY - } else { - 1 - }; - delay.push(vec![Complex::default(); d]); - } - PsDecorr { - config, - allpass: vec![AllpassState::new(); c.nr_allpass_bands], - phi_fract, - q_fract, - g_decay, - delay_pos: vec![0; c.nr_bands - c.nr_allpass_bands], - delay, - peak_decay_nrg: vec![0.0; c.nr_par_bands], - smooth_nrg: vec![0.0; c.nr_par_bands], - smooth_peak_diff: vec![0.0; c.nr_par_bands], - } - } - - /// Annex 8.A.3 partial reset: zero the filter state of hybrid - /// channels `k ≥ kmax` (the region above the SBR-generated - /// spectrum), or the whole bank with `kmax = 0` (the "no - /// `ps_data()` in the previous frame" full reset). - pub fn reset_bands(&mut self, kmax: usize) { - let c = consts(self.config); - for k in kmax..c.nr_allpass_bands { - self.allpass[k].reset(); - } - for k in kmax.max(c.nr_allpass_bands)..c.nr_bands { - let i = k - c.nr_allpass_bands; - self.delay[i] - .iter_mut() - .for_each(|v| *v = Complex::default()); - self.delay_pos[i] = 0; - } - } - - /// De-correlate one stereo frame of hybrid slots (each - /// `nr_bands()` wide). Returns `d_k(n)` with the transient - /// attenuation applied; all state advances. - pub fn process(&mut self, s: &[Vec]) -> Result>> { - let c = consts(self.config); - let b_k = parameter_map(self.config); - if s.iter().any(|row| row.len() != c.nr_bands) { - return Err(Error::PsDataInvalid); - } - let mut out = vec![vec![Complex::default(); c.nr_bands]; s.len()]; - for (n, row) in s.iter().enumerate() { - // §8.6.4.5.3 transient detection at this slot. - let mut p = vec![0.0f64; c.nr_par_bands]; - for (k, v) in row.iter().enumerate() { - p[usize::from(b_k[k])] += v.norm_sqr(); - } - let mut g_ratio = vec![1.0f64; c.nr_par_bands]; - for i in 0..c.nr_par_bands { - let peak = if PEAK_DECAY * self.peak_decay_nrg[i] < p[i] { - p[i] - } else { - PEAK_DECAY * self.peak_decay_nrg[i] - }; - self.peak_decay_nrg[i] = peak; - self.smooth_nrg[i] += A_SMOOTH * (p[i] - self.smooth_nrg[i]); - self.smooth_peak_diff[i] += A_SMOOTH * (peak - p[i] - self.smooth_peak_diff[i]); - if GAMMA * self.smooth_peak_diff[i] > self.smooth_nrg[i] { - g_ratio[i] = self.smooth_nrg[i] / (GAMMA * self.smooth_peak_diff[i]); - } - } - - // §8.6.4.5.2 all-pass chain for the low bands. - for k in 0..c.nr_allpass_bands { - let st = &mut self.allpass[k]; - // z⁻² then φ_fract rotation. - let delayed = st.in2[1]; - st.in2[1] = st.in2[0]; - st.in2[0] = row[k]; - let mut u = self.phi_fract[k] * delayed; - // Three all-pass links. - let g = self.g_decay[k]; - for m in 0..3 { - let coef = A[m] * g; - let q = self.q_fract[k][m]; - let pos = st.pos[m]; - let w_d = st.w[m][pos]; - // w[n] = u[n] + a·g·Q·w[n−d] - let w_n = u + q * w_d * coef; - // v[n] = Q·w[n−d] − a·g·w[n] - u = q * w_d - w_n * coef; - st.w[m][pos] = w_n; - st.pos[m] = (pos + 1) % D[m]; - } - out[n][k] = u * g_ratio[usize::from(b_k[k])]; - } - - // Plain delays above. - for k in c.nr_allpass_bands..c.nr_bands { - let i = k - c.nr_allpass_bands; - let pos = self.delay_pos[i]; - let v = self.delay[i][pos]; - self.delay[i][pos] = row[k]; - self.delay_pos[i] = (pos + 1) % self.delay[i].len(); - out[n][k] = v * g_ratio[usize::from(b_k[k])]; - } - } - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ps_hybrid::HybridConfig; - - fn noise_slot(seed: u64, n: usize, nb: usize) -> Vec { - (0..nb) - .map(|k| { - let mut h = seed - .wrapping_mul(6364136223846793005) - .wrapping_add((n * 128 + k) as u64); - h ^= h >> 33; - h = h.wrapping_mul(0xff51afd7ed558ccd); - h ^= h >> 33; - Complex::new( - (h & 0xFFFF) as f64 / 65535.0 - 0.5, - ((h >> 16) & 0xFFFF) as f64 / 65535.0 - 0.5, - ) - }) - .collect() - } - - /// The all-pass chain preserves energy per band in steady state - /// (stationary input keeps the transient ratio at 1, and each - /// section is unit-magnitude on the unit circle). - #[test] - fn allpass_preserves_energy_on_stationary_noise() { - let config = HybridConfig::Bands1020; - let mut dec = PsDecorr::new(config); - let nb = config.nr_bands(); - let mut in_e = vec![0.0f64; nb]; - let mut out_e = vec![0.0f64; nb]; - for f in 0..40 { - let s: Vec> = (0..32).map(|n| noise_slot(3, f * 32 + n, nb)).collect(); - let d = dec.process(&s).unwrap(); - if f >= 8 { - for n in 0..32 { - for k in 0..nb { - in_e[k] += s[n][k].norm_sqr(); - out_e[k] += d[n][k].norm_sqr(); - } - } - } - } - for k in 0..nb { - let ratio = out_e[k] / in_e[k]; - assert!( - (0.85..1.15).contains(&ratio), - "band {k}: energy ratio {ratio}" - ); - } - } - - /// The upper bands are pure delays: 14 slots in the mid region, - /// 1 slot at the top. - #[test] - fn upper_bands_are_pure_delays() { - let config = HybridConfig::Bands1020; - let mut dec = PsDecorr::new(config); - let nb = config.nr_bands(); - // Stationary-amplitude signal so the transient ratio stays 1: - // an impulse *train* in every band with period > delay would - // still trip the detector, so use a constant rotating phasor - // instead and check the delay relation on the waveform. - let mut frames: Vec>> = Vec::new(); - for f in 0..3 { - let s: Vec> = (0..32) - .map(|n| { - let t = (f * 32 + n) as f64; - (0..nb) - .map(|k| { - let arg = 0.1 * t + k as f64; - let (si, co) = arg.sin_cos(); - Complex::new(co, si) - }) - .collect() - }) - .collect(); - frames.push(s); - } - let mut all_in: Vec> = Vec::new(); - let mut all_out: Vec> = Vec::new(); - for s in &frames { - let d = dec.process(s).unwrap(); - all_in.extend_from_slice(s); - all_out.extend_from_slice(&d); - } - // Mid band k=35 (30..42): 14-slot delay. Top band k=50: 1. - for (k, delay) in [(35usize, 14usize), (50, 1)] { - for n in 40..96 { - let d = all_out[n][k] - all_in[n - delay][k]; - assert!( - d.norm_sqr() < 1e-20, - "band {k} slot {n}: not a {delay}-delay" - ); - } - } - } - - /// After a loud burst cuts to silence the peak tracker holds while - /// the smoothed power decays, so the de-correlated tail (still - /// flowing out of the 14-slot delay line) is ducked (G < 1). A - /// constant-level signal, by contrast, keeps `peak == P`, the - /// difference at zero, and G exactly 1 — the steady test above - /// already pins that via the exact delay identity. - #[test] - fn transient_tail_is_ducked() { - let config = HybridConfig::Bands1020; - let nb = config.nr_bands(); - let loud: Vec> = (0..32).map(|_| vec![Complex::new(1.0, 0.0); nb]).collect(); - let quiet: Vec> = (0..32).map(|_| vec![Complex::default(); nb]).collect(); - let mut dec = PsDecorr::new(config); - dec.process(&loud).unwrap(); - let d = dec.process(&quiet).unwrap(); - // Band 35 is a pure 14-slot delay (b(35) = 18): during the - // first 14 silence slots the delayed loud samples (|·| = 1) - // are still emerging, scaled by G(18, n). By slot 5 the - // recurrences (α peak decay vs a_smooth power decay, γ = 1.5) - // put G well under 0.8; at slot 0 G is still 1. - let first = d[0][35].norm_sqr(); - let later = d[5][35].norm_sqr(); - assert!((first - 1.0).abs() < 1e-12, "slot 0 should be unducked"); - assert!(later < 0.64, "slot 5 should be ducked: {later}"); - // And the duck deepens monotonically over the tail. - let even_later = d[10][35].norm_sqr(); - assert!(even_later < later); - } - - /// reset_bands zeroes the tail region state only. - #[test] - fn partial_reset_clears_upper_state() { - let config = HybridConfig::Bands1020; - let nb = config.nr_bands(); - let mut dec = PsDecorr::new(config); - let s: Vec> = (0..32).map(|n| noise_slot(9, n, nb)).collect(); - dec.process(&s).unwrap(); - dec.reset_bands(40); - let zeros: Vec> = (0..32).map(|_| vec![Complex::default(); nb]).collect(); - let d = dec.process(&zeros).unwrap(); - // Bands >= 40 were reset: zero input → zero output. - for (n, row) in d.iter().enumerate().take(14) { - for (k, v) in row.iter().enumerate().skip(40) { - assert_eq!(*v, Complex::default(), "slot {n} band {k}"); - } - } - // A low band still rings from its surviving state. - let rings = (0..8).any(|n| d[n][3].norm_sqr() > 0.0); - assert!(rings, "low-band state should survive a partial reset"); - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_huffman.rs b/crates/vendor/oxideav-aac/src/ps_huffman.rs deleted file mode 100644 index 50e1cec2..00000000 --- a/crates/vendor/oxideav-aac/src/ps_huffman.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Parametric Stereo Huffman codebooks + `ps_huff_dec()` — ISO/IEC -//! 14496-3:2009 Annex 8.B (Tables 8.B.17–8.B.21). -//! -//! The `ps_data()` element (§8.4.2 Table 8.9) entropy-codes its IID / -//! ICC / IPD / OPD parameters as DPCM deltas with ten canonical -//! Huffman codebooks, selected by parameter kind, quantization grid -//! (`iid_quant`, Table 8.24) and coding direction (time vs frequency -//! differential, the `*_dt[e]` flags): -//! -//! | parameter | grid | direction | table | -//! |-----------|--------|-----------|-------| -//! | IID | coarse | freq | [`HUFF_IID_DF`] (8.B.18) | -//! | IID | coarse | time | [`HUFF_IID_DT`] (8.B.18) | -//! | IID | fine | freq | [`HUFF_IID_FINE_DF`] (8.B.17) | -//! | IID | fine | time | [`HUFF_IID_FINE_DT`] (8.B.17) | -//! | ICC | — | freq | [`HUFF_ICC_DF`] (8.B.19) | -//! | ICC | — | time | [`HUFF_ICC_DT`] (8.B.19) | -//! | IPD | — | freq | [`HUFF_IPD_DF`] (8.B.20) | -//! | IPD | — | time | [`HUFF_IPD_DT`] (8.B.20) | -//! | OPD | — | freq | [`HUFF_OPD_DF`] (8.B.21) | -//! | OPD | — | time | [`HUFF_OPD_DT`] (8.B.21) | -//! -//! ## Codeword representation -//! -//! Same shape as [`crate::sbr_huffman`]: each table is `[(u8, u32); N]` -//! `(code_length_bits, codeword)` pairs indexed by the Huffman table -//! index, MSB-first prefix codes. [`ps_huff_dec`] accumulates bits and -//! returns `index - lav` (the signed delta). The IID/ICC tables carry -//! their LAV in the index layout (`LAV = (N-1)/2`); IPD/OPD deltas are -//! phase-index differences taken modulo 8 by the caller, so their -//! tables decode with `lav = 0`. -//! -//! ## Provenance -//! -//! All ten tables are transcribed from the normative codeword grids in -//! ISO/IEC 14496-3:2009 Annex 8.B staged under `docs/audio/aac/`. All -//! six IID/ICC tables were additionally cross-checked leaf-for-leaf -//! against the staged `docs/audio/aac/sbr-tables/ps-huffbook-*.csv` -//! decode-tree data at transcription time; every table satisfies the -//! complete-prefix-code invariant (Kraft sum exactly 1). - -use crate::{Error, Result}; - -/// Longest PS codeword across all Annex 8.B tables (`huff_iid_dt[0]` -/// reaches 20 bits). -pub const PS_HUFF_MAX_CODE_LEN: u32 = 20; - -/// `huff_iid_df[1]` — Table 8.B.17 (fine grid, frequency direction). -/// Index `i` decodes the delta `i - 30`. -pub const HUFF_IID_FINE_DF: [(u8, u32); 61] = [ - (18, 0b011111111010110100), // -30 - (18, 0b011111111010110101), // -29 - (18, 0b011111110101110110), // -28 - (18, 0b011111110101110111), // -27 - (18, 0b011111110101110100), // -26 - (18, 0b011111110101110101), // -25 - (18, 0b011111111010001010), // -24 - (18, 0b011111111010001011), // -23 - (18, 0b011111111010001000), // -22 - (17, 0b01111111010000000), // -21 - (18, 0b011111111010110110), // -20 - (17, 0b01111111010000010), // -19 - (17, 0b01111111010111000), // -18 - (16, 0b0111111101000010), // -17 - (16, 0b0111111110101110), // -16 - (15, 0b011111110101111), // -15 - (14, 0b01111111010001), // -14 - (14, 0b01111111101001), // -13 - (13, 0b0111111101001), // -12 - (12, 0b011111101010), // -11 - (12, 0b011111111011), // -10 - (11, 0b01111111011), // -9 - (10, 0b0111111011), // -8 - (10, 0b0111111111), // -7 - (8, 0b01111100), // -6 - (7, 0b0111100), // -5 - (6, 0b011100), // -4 - (5, 0b01100), // -3 - (4, 0b0000), // -2 - (3, 0b001), // -1 - (1, 0b1), // +0 - (3, 0b010), // +1 - (4, 0b0001), // +2 - (5, 0b01101), // +3 - (6, 0b011101), // +4 - (7, 0b0111101), // +5 - (8, 0b01111101), // +6 - (9, 0b011111100), // +7 - (10, 0b0111111100), // +8 - (11, 0b01111111100), // +9 - (11, 0b01111110100), // +10 - (12, 0b011111101011), // +11 - (13, 0b0111111101010), // +12 - (14, 0b01111111101010), // +13 - (14, 0b01111111010110), // +14 - (15, 0b011111111010000), // +15 - (16, 0b0111111110101111), // +16 - (16, 0b0111111101000011), // +17 - (17, 0b01111111010111001), // +18 - (17, 0b01111111010000011), // +19 - (18, 0b011111111010110111), // +20 - (17, 0b01111111010000001), // +21 - (18, 0b011111111010001001), // +22 - (18, 0b011111111010001110), // +23 - (18, 0b011111111010001111), // +24 - (18, 0b011111111010001100), // +25 - (18, 0b011111111010001101), // +26 - (18, 0b011111111010110010), // +27 - (18, 0b011111111010110011), // +28 - (18, 0b011111111010110000), // +29 - (18, 0b011111111010110001), // +30 -]; - -/// `huff_iid_dt[1]` — Table 8.B.17 (fine grid, time direction). -/// Index `i` decodes the delta `i - 30`. -pub const HUFF_IID_FINE_DT: [(u8, u32); 61] = [ - (16, 0b0100111011010100), // -30 - (16, 0b0100111011010101), // -29 - (16, 0b0100111011001110), // -28 - (16, 0b0100111011001111), // -27 - (16, 0b0100111011001100), // -26 - (16, 0b0100111011010110), // -25 - (16, 0b0100111011011000), // -24 - (16, 0b0100111101000110), // -23 - (16, 0b0100111101100000), // -22 - (15, 0b010011100011000), // -21 - (15, 0b010011100011001), // -20 - (15, 0b010011101100100), // -19 - (15, 0b010011101100101), // -18 - (15, 0b010011101101101), // -17 - (15, 0b010011110110001), // -16 - (14, 0b01001110110111), // -15 - (14, 0b01001111010110), // -14 - (13, 0b0100111000111), // -13 - (13, 0b0100111101001), // -12 - (13, 0b0100111101101), // -11 - (12, 0b010011101110), // -10 - (12, 0b010011110111), // -9 - (11, 0b01001111000), // -8 - (10, 0b0100111001), // -7 - (9, 0b010011010), // -6 - (9, 0b010011111), // -5 - (7, 0b0100000), // -4 - (6, 0b010001), // -3 - (5, 0b01010), // -2 - (3, 0b011), // -1 - (1, 0b1), // +0 - (2, 0b00), // +1 - (5, 0b01011), // +2 - (6, 0b010010), // +3 - (7, 0b0100001), // +4 - (8, 0b01001100), // +5 - (9, 0b010011011), // +6 - (10, 0b0100111010), // +7 - (11, 0b01001111001), // +8 - (11, 0b01001110000), // +9 - (12, 0b010011101111), // +10 - (12, 0b010011100010), // +11 - (13, 0b0100111101010), // +12 - (13, 0b0100111011000), // +13 - (14, 0b01001111010111), // +14 - (14, 0b01001111010000), // +15 - (15, 0b010011110110010), // +16 - (15, 0b010011110100010), // +17 - (15, 0b010011100011010), // +18 - (15, 0b010011100011011), // +19 - (16, 0b0100111101100110), // +20 - (16, 0b0100111101100111), // +21 - (16, 0b0100111101100001), // +22 - (16, 0b0100111101000111), // +23 - (16, 0b0100111011011001), // +24 - (16, 0b0100111011010111), // +25 - (16, 0b0100111011001101), // +26 - (16, 0b0100111011010010), // +27 - (16, 0b0100111011010011), // +28 - (16, 0b0100111011010000), // +29 - (16, 0b0100111011010001), // +30 -]; - -/// `huff_iid_df[0]` — Table 8.B.18 (coarse grid, frequency direction). -/// Index `i` decodes the delta `i - 14`. -pub const HUFF_IID_DF: [(u8, u32); 29] = [ - (17, 0b11111111111111011), // -14 - (17, 0b11111111111111100), // -13 - (17, 0b11111111111111101), // -12 - (17, 0b11111111111111010), // -11 - (16, 0b1111111111111100), // -10 - (15, 0b111111111111100), // -9 - (13, 0b1111111111101), // -8 - (10, 0b1111111110), // -7 - (9, 0b111111110), // -6 - (7, 0b1111110), // -5 - (6, 0b111100), // -4 - (5, 0b11101), // -3 - (4, 0b1101), // -2 - (3, 0b101), // -1 - (1, 0b0), // +0 - (3, 0b100), // +1 - (4, 0b1100), // +2 - (5, 0b11100), // +3 - (6, 0b111101), // +4 - (6, 0b111110), // +5 - (8, 0b11111110), // +6 - (11, 0b11111111110), // +7 - (13, 0b1111111111100), // +8 - (14, 0b11111111111100), // +9 - (14, 0b11111111111101), // +10 - (15, 0b111111111111101), // +11 - (17, 0b11111111111111110), // +12 - (18, 0b111111111111111110), // +13 - (18, 0b111111111111111111), // +14 -]; - -/// `huff_iid_dt[0]` — Table 8.B.18 (coarse grid, time direction). -/// Index `i` decodes the delta `i - 14`. -pub const HUFF_IID_DT: [(u8, u32); 29] = [ - (19, 0b1111111111111111001), // -14 - (19, 0b1111111111111111010), // -13 - (19, 0b1111111111111111011), // -12 - (20, 0b11111111111111111000), // -11 - (20, 0b11111111111111111001), // -10 - (20, 0b11111111111111111010), // -9 - (17, 0b11111111111111101), // -8 - (15, 0b111111111111110), // -7 - (12, 0b111111111110), // -6 - (10, 0b1111111110), // -5 - (8, 0b11111110), // -4 - (6, 0b111110), // -3 - (4, 0b1110), // -2 - (2, 0b10), // -1 - (1, 0b0), // +0 - (3, 0b110), // +1 - (5, 0b11110), // +2 - (7, 0b1111110), // +3 - (9, 0b111111110), // +4 - (11, 0b11111111110), // +5 - (13, 0b1111111111110), // +6 - (14, 0b11111111111110), // +7 - (17, 0b11111111111111100), // +8 - (19, 0b1111111111111111000), // +9 - (20, 0b11111111111111111011), // +10 - (20, 0b11111111111111111100), // +11 - (20, 0b11111111111111111101), // +12 - (20, 0b11111111111111111110), // +13 - (20, 0b11111111111111111111), // +14 -]; - -/// `huff_icc_df` — Table 8.B.19 (frequency direction). -/// Index `i` decodes the delta `i - 7`. -pub const HUFF_ICC_DF: [(u8, u32); 15] = [ - (14, 0b11111111111111), // -7 - (14, 0b11111111111110), // -6 - (12, 0b111111111110), // -5 - (10, 0b1111111110), // -4 - (7, 0b1111110), // -3 - (5, 0b11110), // -2 - (3, 0b110), // -1 - (1, 0b0), // +0 - (2, 0b10), // +1 - (4, 0b1110), // +2 - (6, 0b111110), // +3 - (8, 0b11111110), // +4 - (9, 0b111111110), // +5 - (11, 0b11111111110), // +6 - (13, 0b1111111111110), // +7 -]; - -/// `huff_icc_dt` — Table 8.B.19 (time direction). -/// Index `i` decodes the delta `i - 7`. -pub const HUFF_ICC_DT: [(u8, u32); 15] = [ - (14, 0b11111111111110), // -7 - (13, 0b1111111111110), // -6 - (11, 0b11111111110), // -5 - (9, 0b111111110), // -4 - (7, 0b1111110), // -3 - (5, 0b11110), // -2 - (3, 0b110), // -1 - (1, 0b0), // +0 - (2, 0b10), // +1 - (4, 0b1110), // +2 - (6, 0b111110), // +3 - (8, 0b11111110), // +4 - (10, 0b1111111110), // +5 - (12, 0b111111111110), // +6 - (14, 0b11111111111111), // +7 -]; - -/// `huff_ipd_df` — Table 8.B.20 (frequency direction). Decodes the -/// raw phase-index delta `0..8` (`lav = 0`). -pub const HUFF_IPD_DF: [(u8, u32); 8] = [ - (1, 0b1), // 0 - (3, 0b000), // 1 - (4, 0b0110), // 2 - (4, 0b0100), // 3 - (4, 0b0010), // 4 - (4, 0b0011), // 5 - (4, 0b0101), // 6 - (4, 0b0111), // 7 -]; - -/// `huff_ipd_dt` — Table 8.B.20 (time direction). Decodes the raw -/// phase-index delta `0..8` (`lav = 0`). -pub const HUFF_IPD_DT: [(u8, u32); 8] = [ - (1, 0b1), // 0 - (3, 0b010), // 1 - (4, 0b0010), // 2 - (5, 0b00011), // 3 - (5, 0b00010), // 4 - (4, 0b0000), // 5 - (4, 0b0011), // 6 - (3, 0b011), // 7 -]; - -/// `huff_opd_df` — Table 8.B.21 (frequency direction). Decodes the -/// raw phase-index delta `0..8` (`lav = 0`). -pub const HUFF_OPD_DF: [(u8, u32); 8] = [ - (1, 0b1), // 0 - (3, 0b001), // 1 - (4, 0b0110), // 2 - (4, 0b0100), // 3 - (5, 0b01111), // 4 - (5, 0b01110), // 5 - (4, 0b0101), // 6 - (3, 0b000), // 7 -]; - -/// `huff_opd_dt` — Table 8.B.21 (time direction). Decodes the raw -/// phase-index delta `0..8` (`lav = 0`). -pub const HUFF_OPD_DT: [(u8, u32); 8] = [ - (1, 0b1), // 0 - (3, 0b010), // 1 - (4, 0b0001), // 2 - (5, 0b00111), // 3 - (5, 0b00110), // 4 - (4, 0b0000), // 5 - (4, 0b0010), // 6 - (3, 0b011), // 7 -]; - -/// Decode one PS Huffman codeword from `reader` against `table`, -/// returning `index - lav` (the signed DPCM delta). -/// -/// Reads bits MSB-first, accumulating a codeword until it matches an -/// entry `(length, codeword)`. Returns [`Error::PsDataInvalid`] if no -/// codeword of length up to [`PS_HUFF_MAX_CODE_LEN`] matches (a -/// corrupt or truncated `ps_data()` payload). -pub fn ps_huff_dec( - reader: &mut oxideav_core::bits::BitReader<'_>, - table: &[(u8, u32)], - lav: i32, -) -> Result { - let mut codeword: u32 = 0; - let mut len: u32 = 0; - loop { - codeword = (codeword << 1) | reader.read_u32(1).map_err(|_| Error::PsDataInvalid)?; - len += 1; - for (idx, &(clen, ccode)) in table.iter().enumerate() { - if u32::from(clen) == len && ccode == codeword { - return Ok(idx as i32 - lav); - } - } - if len >= PS_HUFF_MAX_CODE_LEN { - return Err(Error::PsDataInvalid); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::{BitReader, BitWriter}; - - /// Every table: codewords fit their declared length, the code is - /// prefix-free, and it is *complete* (Kraft sum exactly 1) — the - /// invariants the Annex 8.B grids must satisfy. - fn check_table(table: &[(u8, u32)]) { - let mut kraft_num: u64 = 0; // sum of 2^(max_len - len) - for &(len, code) in table { - assert!(len >= 1 && u32::from(len) <= PS_HUFF_MAX_CODE_LEN); - assert!( - u64::from(code) < (1u64 << len), - "codeword 0x{code:08X} overflows its {len}-bit length" - ); - kraft_num += 1u64 << (PS_HUFF_MAX_CODE_LEN - u32::from(len)); - } - assert_eq!( - kraft_num, - 1u64 << PS_HUFF_MAX_CODE_LEN, - "code is not complete" - ); - for (a, &(la, ca)) in table.iter().enumerate() { - for (b, &(lb, cb)) in table.iter().enumerate() { - if a == b || lb < la { - continue; - } - assert!(cb >> (lb - la) != ca, "prefix conflict {a} vs {b}"); - } - } - } - - #[test] - fn all_tables_are_complete_prefix_codes() { - check_table(&HUFF_IID_FINE_DF); - check_table(&HUFF_IID_FINE_DT); - check_table(&HUFF_IID_DF); - check_table(&HUFF_IID_DT); - check_table(&HUFF_ICC_DF); - check_table(&HUFF_ICC_DT); - check_table(&HUFF_IPD_DF); - check_table(&HUFF_IPD_DT); - check_table(&HUFF_OPD_DF); - check_table(&HUFF_OPD_DT); - } - - /// Round-trip every index of every table through ps_huff_dec. - #[test] - fn every_codeword_decodes_to_its_index() { - let cases: [(&[(u8, u32)], i32); 10] = [ - (&HUFF_IID_FINE_DF, 30), - (&HUFF_IID_FINE_DT, 30), - (&HUFF_IID_DF, 14), - (&HUFF_IID_DT, 14), - (&HUFF_ICC_DF, 7), - (&HUFF_ICC_DT, 7), - (&HUFF_IPD_DF, 0), - (&HUFF_IPD_DT, 0), - (&HUFF_OPD_DF, 0), - (&HUFF_OPD_DT, 0), - ]; - for (table, lav) in cases { - for (idx, &(len, code)) in table.iter().enumerate() { - let mut w = BitWriter::new(); - w.write_u32(code, u32::from(len)); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let got = ps_huff_dec(&mut r, table, lav).unwrap(); - assert_eq!(got, idx as i32 - lav); - assert_eq!(r.bit_position(), u64::from(len)); - } - } - } - - /// The zero delta is always the 1-bit codeword `1` for IID/ICC - /// (Table 8.B.17–8.B.19 anchor `0 → 1`, except the coarse tables' - /// `0 → 0`) — pin the two anchors that differ. - #[test] - fn zero_delta_anchors() { - // Fine IID: delta 0 = codeword 1 (1 bit). - assert_eq!(HUFF_IID_FINE_DF[30], (1, 0b1)); - // Coarse IID: delta 0 = codeword 0 (1 bit). - assert_eq!(HUFF_IID_DF[14], (1, 0b0)); - // ICC: delta 0 = codeword 0 (1 bit). - assert_eq!(HUFF_ICC_DF[7], (1, 0b0)); - // IPD/OPD: delta 0 = codeword 1 (1 bit). - assert_eq!(HUFF_IPD_DF[0], (1, 0b1)); - assert_eq!(HUFF_OPD_DT[0], (1, 0b1)); - } - - /// A truncated payload (the reader running dry mid-codeword) - /// surfaces the parse error rather than spinning. - #[test] - fn unmatched_bits_error() { - // In HUFF_IID_DT the shortest all-ones codeword is 20 bits, so - // 8 one-bits cannot complete a codeword; the reader runs dry. - let bytes = [0xFFu8; 1]; - let mut r = BitReader::new(&bytes); - assert!(matches!( - ps_huff_dec(&mut r, &HUFF_IID_DT, 14), - Err(Error::PsDataInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_hybrid.rs b/crates/vendor/oxideav-aac/src/ps_hybrid.rs deleted file mode 100644 index 2f982320..00000000 --- a/crates/vendor/oxideav-aac/src/ps_hybrid.rs +++ /dev/null @@ -1,508 +0,0 @@ -//! PS hybrid filterbank — ISO/IEC 14496-3:2009 §8.6.4.3 / Annex 8.A.3. -//! -//! Parametric Stereo needs a finer frequency resolution at the bottom -//! of the spectrum than the 64-band QMF provides, so the lowest QMF -//! subbands are split further by 13-tap prototype filters (Tables -//! 8.36–8.38), producing the *hybrid* sub-subband domain: -//! -//! * **10/20 stereo bands** — QMF band 0 split by 8 (Type A, complex -//! modulated) with the outer sub-subband pairs merged to 6 channels, -//! QMF bands 1 and 2 split by 2 (Type B, cosine modulated); 71 -//! hybrid channels total (`6 + 2 + 2 + 61`). -//! * **34 stereo bands** — QMF band 0 split by 12, band 1 by 8, bands -//! 2–4 by 4 (all Type A); 91 hybrid channels (`12+8+4+4+4 + 59`). -//! -//! ```text -//! Type A: G_q^p[n] = g^p[n] · exp(j·2π/Q^p·(q+1/2)·(n−6)) -//! Type B: G_q^p[n] = g^p[n] · cos(2π·q/Q^p·(n−6)) -//! ``` -//! -//! The prototypes are linear-phase with a 6-slot delay; per Annex -//! 8.A.3 the SBR combination feeds the filterbank 6 *look-ahead* QMF -//! slots (`XLow` beyond the current frame), so the hybrid output is -//! time-aligned with the QMF input at **zero net delay**: the unsplit -//! bands pass straight through and the split bands consume the -//! look-ahead. Filtering is the convolution -//! `y[n] = Σ_m G[m] · x[n+6−m]`, needing 6 history slots per split -//! band which [`PsHybrid`] threads across frames. -//! -//! ## Channel ordering (Figures 8.20 / 8.22) -//! -//! For the 10/20 configuration QMF band 0's eight Type-A outputs `q` -//! (sub-subband centres `(q+1/2)·π/8`, `q ≥ 4` the negative-frequency -//! mirrors) merge and reorder to six hybrid channels: -//! `s0 = q6, s1 = q7, s2 = q0, s3 = q1, s4 = q2+q5, s5 = q3+q4`. -//! QMF band 1's two Type-B outputs land **swapped** (`s6 = q1, -//! s7 = q0` — odd QMF bands are spectrally inverted), band 2's in -//! order (`s8 = q0, s9 = q1`). The 34-band configuration keeps every -//! split output in filter order (Figure 8.22). -//! -//! The synthesis (§8.6.4.7 / Figures 8.21, 8.23) is a plain adder: -//! sub-subbands of a split QMF band sum back into that band. Because -//! each prototype's sub-filters sum to a pure 6-slot delay (the -//! Type-A modulation phases cancel off-centre, the Type-B prototypes -//! vanish at the surviving off-centre taps), analysis followed by -//! synthesis reconstructs the input exactly — pinned by the tests. -//! -//! All truth from ISO/IEC 14496-3:2009 §8.6.4.3 / Annex 8.A staged -//! under `docs/audio/aac/`. - -use crate::sbr_qmf::Complex; -use crate::{Error, Result}; - -/// QMF slots per PS stereo frame in the SBR combination -/// (`numQMFSlots = numTimeSlots · RATE`, Annex 8.A.3, 1024 framing). -pub const NUM_QMF_SLOTS: usize = 32; - -/// Look-ahead slots supplied by the SBR low-band buffer (Annex 8.A.3). -pub const LOOKAHEAD: usize = 6; - -/// Prototype filter length (§8.6.4.3). -const PROTO_LEN: usize = 13; - -/// Table 8.37 — `g⁰[n]`, `Q⁰ = 8` (10/20 stereo bands, QMF band 0). -const G0_Q8: [f64; PROTO_LEN] = [ - 0.00746082949812, - 0.02270420949825, - 0.04546865930473, - 0.07266113929591, - 0.09885108575264, - 0.11793710567217, - 0.125, - 0.11793710567217, - 0.09885108575264, - 0.07266113929591, - 0.04546865930473, - 0.02270420949825, - 0.00746082949812, -]; - -/// Table 8.37 — `g^{1,2}[n]`, `Q^{1,2} = 2` (10/20 bands, QMF 1–2). -const G12_Q2: [f64; PROTO_LEN] = [ - 0.0, - 0.01899487526049, - 0.0, - -0.07293139167538, - 0.0, - 0.30596630545168, - 0.5, - 0.30596630545168, - 0.0, - -0.07293139167538, - 0.0, - 0.01899487526049, - 0.0, -]; - -/// Table 8.38 — `g⁰[n]`, `Q⁰ = 12` (34 stereo bands, QMF band 0). -const G0_Q12: [f64; PROTO_LEN] = [ - 0.04081179924692, - 0.03812810994926, - 0.05144908135699, - 0.06399831151592, - 0.07428313801106, - 0.08100347892914, - 0.08333333333333, - 0.08100347892914, - 0.07428313801106, - 0.06399831151592, - 0.05144908135699, - 0.03812810994926, - 0.04081179924692, -]; - -/// Table 8.38 — `g¹[n]`, `Q¹ = 8` (34 bands, QMF band 1). -const G1_Q8: [f64; PROTO_LEN] = [ - 0.01565675600122, - 0.03752716391991, - 0.05417891378782, - 0.08417044116767, - 0.10307344158036, - 0.12222452249753, - 0.125, - 0.12222452249753, - 0.10307344158036, - 0.08417044116767, - 0.05417891378782, - 0.03752716391991, - 0.01565675600122, -]; - -/// Table 8.38 — `g^{2,3,4}[n]`, `Q^{2,3,4} = 4` (34 bands, QMF 2–4). -const G234_Q4: [f64; PROTO_LEN] = [ - -0.05908211155639, - -0.04871498374946, - 0.0, - 0.07778723915851, - 0.16486303567403, - 0.23279856662996, - 0.25, - 0.23279856662996, - 0.16486303567403, - 0.07778723915851, - 0.0, - -0.04871498374946, - -0.05908211155639, -]; - -/// The two §8.6.4.3 hybrid configurations. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HybridConfig { - /// 10 or 20 stereo bands: 71 hybrid channels, QMF bands 0–2 split. - Bands1020, - /// 34 stereo bands: 91 hybrid channels, QMF bands 0–4 split. - Bands34, -} - -impl HybridConfig { - /// `NR_BANDS` — hybrid channel count (§8.6.4.5.1). - #[must_use] - pub fn nr_bands(&self) -> usize { - match self { - HybridConfig::Bands1020 => 71, - HybridConfig::Bands34 => 91, - } - } - - /// Number of QMF bands that are split. - fn split_bands(&self) -> usize { - match self { - HybridConfig::Bands1020 => 3, - HybridConfig::Bands34 => 5, - } - } - - /// Split factor `Q^p` per split QMF band. - fn q(&self, p: usize) -> usize { - match self { - HybridConfig::Bands1020 => [8, 2, 2][p], - HybridConfig::Bands34 => [12, 8, 4, 4, 4][p], - } - } - - /// Prototype `g^p` per split QMF band. - fn proto(&self, p: usize) -> &'static [f64; PROTO_LEN] { - match self { - HybridConfig::Bands1020 => [&G0_Q8, &G12_Q2, &G12_Q2][p], - HybridConfig::Bands34 => [&G0_Q12, &G1_Q8, &G234_Q4, &G234_Q4, &G234_Q4][p], - } - } - - /// Whether split band `p` uses the Type-A (complex) modulation. - fn type_a(&self, p: usize) -> bool { - match self { - HybridConfig::Bands1020 => p == 0, - HybridConfig::Bands34 => true, - } - } -} - -/// One channel's hybrid analysis/synthesis state: the 6 history slots -/// per split QMF band that the 13-tap convolution reaches into before -/// the current frame. -#[derive(Debug, Clone)] -pub struct PsHybrid { - config: HybridConfig, - /// `history[p][j]` — the previous frame's QMF slots `26..32` for - /// split band `p` (`j = 0` is the oldest). - history: Vec<[Complex; LOOKAHEAD]>, -} - -impl PsHybrid { - /// A fresh filterbank for `config` (zero history). - #[must_use] - pub fn new(config: HybridConfig) -> Self { - PsHybrid { - config, - history: vec![[Complex::default(); LOOKAHEAD]; config.split_bands()], - } - } - - /// The active configuration. - #[must_use] - pub fn config(&self) -> HybridConfig { - self.config - } - - /// Switch configuration (a §8.6.4.6.1 stereo-band change resets - /// the filter state instantaneously). - pub fn reset(&mut self, config: HybridConfig) { - self.config = config; - self.history = vec![[Complex::default(); LOOKAHEAD]; config.split_bands()]; - } - - /// Hybrid analysis of one stereo frame. - /// - /// `x` is the Annex 8.A.3 `Xinput` matrix: at least - /// `NUM_QMF_SLOTS + LOOKAHEAD` slots of 64 QMF bands (the trailing - /// 6 slots only need bands `0..split_bands` populated). Returns - /// `NUM_QMF_SLOTS` slots of `nr_bands()` hybrid channels, and - /// advances the cross-frame history. - pub fn analyze(&mut self, x: &[[Complex; 64]]) -> Result>> { - if x.len() < NUM_QMF_SLOTS + LOOKAHEAD { - return Err(Error::PsDataInvalid); - } - let nb = self.config.nr_bands(); - let split = self.config.split_bands(); - let mut out = vec![vec![Complex::default(); nb]; NUM_QMF_SLOTS]; - - for p in 0..split { - // Extended buffer: 6 history slots + the frame + look-ahead. - let mut buf = [Complex::default(); LOOKAHEAD + NUM_QMF_SLOTS + LOOKAHEAD]; - buf[..LOOKAHEAD].copy_from_slice(&self.history[p]); - for (j, slot) in x.iter().enumerate().take(NUM_QMF_SLOTS + LOOKAHEAD) { - buf[LOOKAHEAD + j] = slot[p]; - } - let q_cnt = self.config.q(p); - let g = self.config.proto(p); - let type_a = self.config.type_a(p); - for q in 0..q_cnt { - // G_q[m] for m = 0..13. - let mut filt = [Complex::default(); PROTO_LEN]; - for (m, f) in filt.iter_mut().enumerate() { - let arg = if type_a { - 2.0 * core::f64::consts::PI / q_cnt as f64 - * (q as f64 + 0.5) - * (m as f64 - 6.0) - } else { - 2.0 * core::f64::consts::PI * q as f64 / q_cnt as f64 * (m as f64 - 6.0) - }; - let (s, c) = arg.sin_cos(); - *f = if type_a { - Complex::new(g[m] * c, g[m] * s) - } else { - Complex::new(g[m] * c, 0.0) - }; - } - for (n, row) in out.iter_mut().enumerate() { - // y[n] = Σ_m G[m]·x[n+6−m]; buf[j] = x[j−6]. - let mut acc = Complex::default(); - for (m, &f) in filt.iter().enumerate() { - acc += f * buf[n + 12 - m]; - } - accumulate_channel(&self.config, p, q, acc, row); - } - } - // Next frame's x[−6..0] are this frame's slots 26..32. - for j in 0..LOOKAHEAD { - self.history[p][j] = x[NUM_QMF_SLOTS - LOOKAHEAD + j][p]; - } - } - - // Unsplit QMF bands pass through at zero delay. - for (n, row) in out.iter_mut().enumerate() { - for k in split..64 { - row[hybrid_offset(&self.config) + k - split] = x[n][k]; - } - } - Ok(out) - } -} - -/// First hybrid channel index of the unsplit QMF region. -fn hybrid_offset(config: &HybridConfig) -> usize { - match config { - HybridConfig::Bands1020 => 10, - HybridConfig::Bands34 => 32, - } -} - -/// Route split-band filter output `q` of QMF band `p` into its hybrid -/// channel (Figures 8.20 / 8.22), merging where the 10/20 -/// configuration combines sub-subbands. -fn accumulate_channel(config: &HybridConfig, p: usize, q: usize, v: Complex, row: &mut [Complex]) { - match config { - HybridConfig::Bands1020 => match p { - 0 => { - // s0=q6, s1=q7, s2=q0, s3=q1, s4=q2+q5, s5=q3+q4. - let k = match q { - 6 => 0, - 7 => 1, - 0 => 2, - 1 => 3, - 2 | 5 => 4, - _ => 5, // 3 | 4 - }; - row[k] += v; - } - 1 => { - // Spectrally inverted odd QMF band: s6=q1, s7=q0. - row[if q == 0 { 7 } else { 6 }] += v; - } - _ => { - // Band 2 in order: s8=q0, s9=q1. - row[8 + q] += v; - } - }, - HybridConfig::Bands34 => { - // Figure 8.22: filter order, bands packed consecutively. - let base = [0usize, 12, 20, 24, 28][p]; - row[base + q] += v; - } - } -} - -/// Hybrid synthesis (§8.6.4.7): sum each split QMF band's sub-subbands -/// back into the band; copy the unsplit region. `rows` are -/// `nr_bands()`-wide hybrid slots; returns 64-band QMF slots. -#[must_use] -pub fn synthesize(config: HybridConfig, rows: &[Vec]) -> Vec<[Complex; 64]> { - let split = config.split_bands(); - let off = hybrid_offset(&config); - let mut out = Vec::with_capacity(rows.len()); - for row in rows { - let mut slot = [Complex::default(); 64]; - // Per-band sub-subband spans in the hybrid row. - let spans: &[(usize, usize)] = match config { - HybridConfig::Bands1020 => &[(0, 6), (6, 8), (8, 10)], - HybridConfig::Bands34 => &[(0, 12), (12, 20), (20, 24), (24, 28), (28, 32)], - }; - for (p, &(lo, hi)) in spans.iter().enumerate() { - for v in &row[lo..hi] { - slot[p] += *v; - } - } - for k in split..64 { - slot[k] = row[off + k - split]; - } - out.push(slot); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn frame_from(f: impl Fn(usize, usize) -> Complex) -> Vec<[Complex; 64]> { - (0..NUM_QMF_SLOTS + LOOKAHEAD) - .map(|n| { - let mut s = [Complex::default(); 64]; - for (k, cell) in s.iter_mut().enumerate() { - *cell = f(n, k); - } - s - }) - .collect() - } - - /// Deterministic pseudo-random complex signal. - fn noise(seed: u64) -> impl Fn(usize, usize) -> Complex { - move |n, k| { - let mut h = seed - .wrapping_mul(6364136223846793005) - .wrapping_add((n * 64 + k) as u64); - h ^= h >> 33; - h = h.wrapping_mul(0xff51afd7ed558ccd); - h ^= h >> 33; - let re = (h & 0xFFFF) as f64 / 65535.0 - 0.5; - let im = ((h >> 16) & 0xFFFF) as f64 / 65535.0 - 0.5; - Complex::new(re, im) - } - } - - /// Analysis followed by synthesis reconstructs the input exactly - /// (both configurations, across a frame boundary so the history - /// path is exercised). - #[test] - fn perfect_reconstruction_both_configs() { - for config in [HybridConfig::Bands1020, HybridConfig::Bands34] { - let mut fb = PsHybrid::new(config); - // Two consecutive frames of one continuous signal: frame f - // covers absolute slots 32f .. 32f+38. - for f in 0..3 { - let sig = noise(7); - let x = frame_from(|n, k| sig(32 * f + n, k)); - let hyb = fb.analyze(&x).unwrap(); - assert_eq!(hyb.len(), NUM_QMF_SLOTS); - assert_eq!(hyb[0].len(), config.nr_bands()); - let back = synthesize(config, &hyb); - // The split-band path reaches 6 slots into history, - // which is zero for the first frame's first slots — - // skip the warm-up region of frame 0. - let start = if f == 0 { LOOKAHEAD } else { 0 }; - for n in start..NUM_QMF_SLOTS { - for k in 0..64 { - let d = back[n][k] - x[n][k]; - assert!( - d.norm_sqr() < 1e-24, - "cfg {config:?} frame {f} slot {n} band {k}: {d:?}" - ); - } - } - } - } - } - - /// A complex exponential at the centre of QMF-band-0 sub-subband - /// `q = 0` (frequency π/8·(0+1/2) = π/16) concentrates in hybrid - /// channel `s2` of the 10/20 configuration — pinning the Figure - /// 8.20 reorder (positive low frequencies land on s2/s3, negative - /// on s1/s0). - #[test] - fn band0_positive_low_frequency_lands_on_s2() { - let mut fb = PsHybrid::new(HybridConfig::Bands1020); - let omega = core::f64::consts::PI / 16.0; - let x = frame_from(|n, k| { - if k == 0 { - let (s, c) = (omega * n as f64).sin_cos(); - Complex::new(c, s) - } else { - Complex::default() - } - }); - let hyb = fb.analyze(&x).unwrap(); - // Steady-state slot (history warm-up over). - let row = &hyb[20]; - let energies: Vec = (0..10).map(|k| row[k].norm_sqr()).collect(); - let max_k = (0..10) - .max_by(|&a, &b| energies[a].partial_cmp(&energies[b]).unwrap()) - .unwrap(); - assert_eq!(max_k, 2, "energies: {energies:?}"); - } - - /// The negative mirror (−π/16) lands on s1 (`q = 7`). - #[test] - fn band0_negative_low_frequency_lands_on_s1() { - let mut fb = PsHybrid::new(HybridConfig::Bands1020); - let omega = -core::f64::consts::PI / 16.0; - let x = frame_from(|n, k| { - if k == 0 { - let (s, c) = (omega * n as f64).sin_cos(); - Complex::new(c, s) - } else { - Complex::default() - } - }); - let hyb = fb.analyze(&x).unwrap(); - let row = &hyb[20]; - let energies: Vec = (0..10).map(|k| row[k].norm_sqr()).collect(); - let max_k = (0..10) - .max_by(|&a, &b| energies[a].partial_cmp(&energies[b]).unwrap()) - .unwrap(); - assert_eq!(max_k, 1, "energies: {energies:?}"); - } - - /// Unsplit bands pass through unchanged at zero delay. - #[test] - fn unsplit_bands_pass_through() { - let mut fb = PsHybrid::new(HybridConfig::Bands1020); - let sig = noise(11); - let x = frame_from(&sig); - let hyb = fb.analyze(&x).unwrap(); - for n in 0..NUM_QMF_SLOTS { - for k in 3..64 { - let d = hyb[n][10 + k - 3] - x[n][k]; - assert!(d.norm_sqr() < 1e-30); - } - } - } - - /// Short input is rejected. - #[test] - fn short_input_rejected() { - let mut fb = PsHybrid::new(HybridConfig::Bands34); - let x = vec![[Complex::default(); 64]; NUM_QMF_SLOTS]; - assert!(fb.analyze(&x).is_err()); - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_map.rs b/crates/vendor/oxideav-aac/src/ps_map.rs deleted file mode 100644 index 51ed0d6d..00000000 --- a/crates/vendor/oxideav-aac/src/ps_map.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! PS parameter-band maps — ISO/IEC 14496-3:2009 §8.6.4.6.1 -//! (Tables 8.45 / 8.46 / 8.48 / 8.49). -//! -//! The stereo cues are defined per *stereo band* `b` (20 or 34 of -//! them), while the signal lives in 71 or 91 *hybrid channels* `k`. -//! [`parameter_map`] is `b(k)` — which stereo band governs each hybrid -//! channel — and [`conjugate_flags`] marks the negative-frequency -//! sub-subbands whose mixing coefficients apply conjugated -//! (the `*`-marked rows of Tables 8.48 / 8.49). -//! -//! [`map_10_to_20`], [`MAP_20_TO_34`] and [`MAP_34_TO_20`] convert -//! parameter vectors between band counts (§8.6.4.6.1): 10→20 -//! duplicates every parameter; 20→34 and 34→20 follow Tables 8.45 and -//! 8.46, averaging in *ANSI-C integer arithmetic* on the index -//! representation (the same tables are reused with float arithmetic -//! for the `h`-coefficient hand-over when the stereo-band count -//! switches mid-stream). -//! -//! In the 34-band configuration `b(k)` is deliberately non-monotonic -//! over the split region: the short 13-tap sub-filters of QMF bands -//! 1–4 have pass-bands reaching into neighbouring QMF bands (e.g. -//! hybrid channel 14, the third sub-subband of QMF band 1, sits at -//! 5/8 of a QMF bandwidth — inside stereo band 4), exactly as the -//! Table 8.41 centre-frequency ladder describes. -//! -//! All truth from ISO/IEC 14496-3:2009 subpart 8 staged under -//! `docs/audio/aac/`. - -use crate::ps_hybrid::HybridConfig; - -/// Table 8.48 — `b(k)` for the 20-stereo-band configuration -/// (71 hybrid channels). -const B_K_20: [u8; 71] = [ - 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, // sub-QMF (k0/k1 conjugate) - 8, 9, 10, 11, 12, 13, // QMF 3..8 - 14, 14, // 9-10 - 15, 15, 15, // 11-13 - 16, 16, 16, 16, // 14-17 - 17, 17, 17, 17, 17, // 18-22 - 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, // 23-34 - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, // 35-63 -]; - -/// Table 8.49 — `b(k)` for the 34-stereo-band configuration -/// (91 hybrid channels). -const B_K_34: [u8; 91] = [ - 0, 1, 2, 3, 4, 5, 6, 6, 7, 2, 1, 0, // QMF band 0 (k9..k11 conjugate) - 10, 10, 4, 5, 6, 7, 8, 9, // QMF band 1 - 10, 11, 12, 9, // QMF band 2 - 14, 11, 12, 13, // QMF band 3 - 14, 15, 16, 13, // QMF band 4 - 16, // QMF 5 - 17, // 6 - 18, // 7 - 19, // 8 - 20, // 9 - 21, // 10 - 22, 22, // 11-12 - 23, 23, // 13-14 - 24, 24, // 15-16 - 25, 25, // 17-18 - 26, 26, // 19-20 - 27, 27, 27, // 21-23 - 28, 28, 28, // 24-26 - 29, 29, 29, // 27-29 - 30, 30, 30, // 30-32 - 31, 31, 31, 31, // 33-36 - 32, 32, 32, 32, // 37-40 - 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, - 33, // 41-63 -]; - -/// `b(k)` — stereo band per hybrid channel (Tables 8.48 / 8.49). -#[must_use] -pub fn parameter_map(config: HybridConfig) -> &'static [u8] { - match config { - HybridConfig::Bands1020 => &B_K_20, - HybridConfig::Bands34 => &B_K_34, - } -} - -/// The `*`-marked hybrid channels of Tables 8.48 / 8.49 — the -/// negative-frequency sub-subbands whose `h` coefficients apply -/// complex-conjugated when phase parameters are enabled. -#[must_use] -pub fn conjugate_flags(config: HybridConfig) -> &'static [usize] { - match config { - HybridConfig::Bands1020 => &[0, 1], - HybridConfig::Bands34 => &[9, 10, 11], - } -} - -/// §8.6.4.6.1 — map a 10-band parameter vector to 20 bands by -/// duplication (Table 8.45: `20idx_k ← 10idx_{k/2}`). -#[must_use] -pub fn map_10_to_20(v: &[i32]) -> Vec { - (0..20).map(|k| v[k / 2]).collect() -} - -/// Table 8.45 — 20→34 source per 34-band entry: `Single(i)` copies -/// `idx_i`, `Avg(i, j)` takes `(idx_i + idx_j) / 2` (integer -/// arithmetic on indices). -#[derive(Debug, Clone, Copy)] -pub enum MapSrc { - /// Copy one source band. - Single(usize), - /// Average two source bands. - Avg(usize, usize), - /// Average four source bands (only 34→20's `idx18`). - Avg4(usize, usize, usize, usize), - /// Weighted `(2·a + b)/3`. - W21(usize, usize), - /// Weighted `(a + 2·b)/3`. - W12(usize, usize), -} - -/// Table 8.45 — mapping from 20 to 34 parameters. -pub const MAP_20_TO_34: [MapSrc; 34] = [ - MapSrc::Single(0), - MapSrc::Avg(0, 1), - MapSrc::Single(1), - MapSrc::Single(2), - MapSrc::Avg(2, 3), - MapSrc::Single(3), - MapSrc::Single(4), - MapSrc::Single(4), - MapSrc::Single(5), - MapSrc::Single(5), - MapSrc::Single(6), - MapSrc::Single(7), - MapSrc::Single(8), - MapSrc::Single(8), - MapSrc::Single(9), - MapSrc::Single(9), - MapSrc::Single(10), - MapSrc::Single(11), - MapSrc::Single(12), - MapSrc::Single(13), - MapSrc::Single(14), - MapSrc::Single(14), - MapSrc::Single(15), - MapSrc::Single(15), - MapSrc::Single(16), - MapSrc::Single(16), - MapSrc::Single(17), - MapSrc::Single(17), - MapSrc::Single(18), - MapSrc::Single(18), - MapSrc::Single(18), - MapSrc::Single(18), - MapSrc::Single(19), - MapSrc::Single(19), -]; - -/// Table 8.46 — mapping from 34 down to 20 parameters. -pub const MAP_34_TO_20: [MapSrc; 20] = [ - MapSrc::W21(0, 1), - MapSrc::W12(1, 2), - MapSrc::W21(3, 4), - MapSrc::W12(4, 5), - MapSrc::Avg(6, 7), - MapSrc::Avg(8, 9), - MapSrc::Single(10), - MapSrc::Single(11), - MapSrc::Avg(12, 13), - MapSrc::Avg(14, 15), - MapSrc::Single(16), - MapSrc::Single(17), - MapSrc::Single(18), - MapSrc::Single(19), - MapSrc::Avg(20, 21), - MapSrc::Avg(22, 23), - MapSrc::Avg(24, 25), - MapSrc::Avg(26, 27), - MapSrc::Avg4(28, 29, 30, 31), - MapSrc::Avg(32, 33), -]; - -impl MapSrc { - /// Apply to an integer index vector (ANSI-C truncating division). - #[must_use] - pub fn apply_i32(&self, v: &[i32]) -> i32 { - match *self { - MapSrc::Single(i) => v[i], - MapSrc::Avg(i, j) => (v[i] + v[j]) / 2, - MapSrc::Avg4(i, j, k, l) => (v[i] + v[j] + v[k] + v[l]) / 4, - MapSrc::W21(i, j) => (2 * v[i] + v[j]) / 3, - MapSrc::W12(i, j) => (v[i] + 2 * v[j]) / 3, - } - } - - /// Apply to a float vector (the `h`-coefficient hand-over on a - /// stereo-band-count switch, §8.6.4.6.1). - #[must_use] - pub fn apply_f64(&self, v: &[f64]) -> f64 { - match *self { - MapSrc::Single(i) => v[i], - MapSrc::Avg(i, j) => (v[i] + v[j]) / 2.0, - MapSrc::Avg4(i, j, k, l) => (v[i] + v[j] + v[k] + v[l]) / 4.0, - MapSrc::W21(i, j) => (2.0 * v[i] + v[j]) / 3.0, - MapSrc::W12(i, j) => (v[i] + 2.0 * v[j]) / 3.0, - } - } -} - -/// Map an index vector of `n` parameters (10, 20 or 34) to the target -/// stereo-band count (20 or 34), per §8.6.4.6.1: 10→20 duplication, -/// 20→34 via Table 8.45, 34→20 via Table 8.46, 10→34 via 20. -#[must_use] -pub fn map_indices(v: &[i32], target: usize) -> Vec { - match (v.len(), target) { - (n, t) if n == t => v.to_vec(), - (10, 20) => map_10_to_20(v), - (20, 34) => MAP_20_TO_34.iter().map(|m| m.apply_i32(v)).collect(), - (10, 34) => { - let v20 = map_10_to_20(v); - MAP_20_TO_34.iter().map(|m| m.apply_i32(&v20)).collect() - } - (34, 20) => MAP_34_TO_20.iter().map(|m| m.apply_i32(v)).collect(), - // Shorter vectors (IPD/OPD's nr_ipdopd_par = 5/11/17) are - // handled by the caller; anything else passes through. - _ => v.to_vec(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn b_k_tables_are_consistent() { - assert_eq!(B_K_20.len(), 71); - assert_eq!(B_K_34.len(), 91); - assert!(B_K_20.iter().all(|&b| b < 20)); - assert!(B_K_34.iter().all(|&b| b < 34)); - // Every stereo band is hit at least once. - for b in 0..20u8 { - assert!(B_K_20.contains(&b), "20-band {b} unused"); - } - for b in 0..34u8 { - assert!(B_K_34.contains(&b), "34-band {b} unused"); - } - // The unsplit QMF region of the 20-band table: k=10..16 map - // QMF bands 3..9 one-to-one (Table 8.48 rows 10..15 + 16-17). - assert_eq!(&B_K_20[10..16], &[8, 9, 10, 11, 12, 13]); - // Table 8.49 spot rows: the QMF band 1 sub-subbands reach into - // stereo bands 4..10. - assert_eq!(&B_K_34[12..20], &[10, 10, 4, 5, 6, 7, 8, 9]); - } - - #[test] - fn index_mapping_round_trips_shape() { - let v10: Vec = (0..10).collect(); - let v20 = map_indices(&v10, 20); - assert_eq!( - v20, - vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9] - ); - let v34 = map_indices(&v20, 34); - assert_eq!(v34.len(), 34); - // Table 8.45 first rows: idx0, (idx0+idx1)/2, idx1, idx2, ... - assert_eq!(v34[0], 0); - assert_eq!(v34[1], (v20[0] + v20[1]) / 2); - assert_eq!(v34[2], v20[1]); - let back = map_indices(&v34, 20); - assert_eq!(back.len(), 20); - // A constant vector survives every mapping exactly. - let c34 = map_indices(&[5i32; 20], 34); - assert_eq!(c34, vec![5i32; 34]); - let c20 = map_indices(&[5i32; 34], 20); - assert_eq!(c20, vec![5i32; 20]); - } - - #[test] - fn ansi_c_integer_average_truncates_toward_zero() { - // (-3 + 2)/2 = -0 in C (truncation), not -1 (flooring). - let v = vec![-3i32, 2]; - assert_eq!(MapSrc::Avg(0, 1).apply_i32(&v), 0); - assert_eq!(MapSrc::W21(0, 1).apply_i32(&v), -1); // (-6+2)/3 - } -} diff --git a/crates/vendor/oxideav-aac/src/ps_stereo.rs b/crates/vendor/oxideav-aac/src/ps_stereo.rs deleted file mode 100644 index 74261e38..00000000 --- a/crates/vendor/oxideav-aac/src/ps_stereo.rs +++ /dev/null @@ -1,541 +0,0 @@ -//! PS stereo processing — ISO/IEC 14496-3:2009 §8.6.4.6. -//! -//! Converts the mono hybrid signal `s_k(n)` and its de-correlation -//! `d_k(n)` into left/right hybrid signals through the 2×2 mixing -//! -//! ```text -//! l_k(n) = H11(k,n)·s_k(n) + H21(k,n)·d_k(n) -//! r_k(n) = H12(k,n)·s_k(n) + H22(k,n)·d_k(n) -//! ``` -//! -//! Per parameter position (envelope border) the vectors `h11..h22` -//! are derived per stereo band from the dequantized cues: -//! -//! * IID: `c(b) = 10^(iid(b)/20)` on the Table 8.25 (default) or -//! 8.26 (fine) dB grid; -//! * ICC: `ρ(b)` on the Table 8.28 grid, driving **mixing procedure -//! Ra** (`icc_mode 0..2`: scale factors `c1 = √(2/(1+c²))`, -//! `c2 = √2·c/√(1+c²)`, rotation `α = ½·arccos(ρ)`, -//! `β = α·(c1−c2)/√2`) or **Rb** (`icc_mode 3..5`: `ρ` floored at -//! 0.05, `α = ½·arctan(2cρ/(c²−1))` with the `c = 1` and -//! modulo-π/2 corrections, `μ`/`γ` per §8.6.4.6.2.2); -//! * IPD/OPD (§8.6.4.6.3.2, when enabled): the three-position -//! smoothing `φ = ∠(¼e^(j·prev2) + ½e^(j·prev1) + e^(j·cur))` on -//! the Table 8.31 `π/4` ladder, applied as `e^(jφ1)` on `h11/h21` -//! and `e^(jφ2)` (`φ2 = φ_opd − φ_ipd`) on `h12/h22`; the -//! `*`-marked negative-frequency hybrid channels take the complex -//! conjugate. -//! -//! Between borders the four H matrices are linearly interpolated -//! (§8.6.4.6.4), the first region interpolating from the previous -//! frame's final coefficients (zeros on the very first frame), the -//! region after the last border holding. FIX_BORDERS positions are -//! `⌊32·(e+1)/num_env⌋ − 1`; VAR_BORDERS come from the bitstream. -//! `num_env == 0` holds the previous frame's coefficients for the -//! whole frame (§8.6.4.6.5). A stereo-band-count switch (Table 8.47) -//! re-maps the retained coefficients through Tables 8.45 / 8.46. -//! -//! All truth from ISO/IEC 14496-3:2009 §8.6.4.6 staged under -//! `docs/audio/aac/`. - -use crate::ps_data::{PsData, PsIndices}; -use crate::ps_hybrid::{HybridConfig, NUM_QMF_SLOTS}; -use crate::ps_map::{conjugate_flags, map_indices, parameter_map, MAP_20_TO_34, MAP_34_TO_20}; -use crate::sbr_qmf::Complex; -use crate::{Error, Result}; - -/// Table 8.25 — default IID quantization grid, dB, index −7..7. -const IID_DB_COARSE: [f64; 15] = [ - -25.0, -18.0, -14.0, -10.0, -7.0, -4.0, -2.0, 0.0, 2.0, 4.0, 7.0, 10.0, 14.0, 18.0, 25.0, -]; - -/// Table 8.26 — fine IID quantization grid, dB, index −15..15. -const IID_DB_FINE: [f64; 31] = [ - -50.0, -45.0, -40.0, -35.0, -30.0, -25.0, -22.0, -19.0, -16.0, -13.0, -10.0, -8.0, -6.0, -4.0, - -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 13.0, 16.0, 19.0, 22.0, 25.0, 30.0, 35.0, 40.0, 45.0, - 50.0, -]; - -/// Table 8.28 — ICC quantization grid `ρ`. -const ICC_RHO: [f64; 8] = [1.0, 0.937, 0.84118, 0.60092, 0.36764, 0.0, -0.589, -1.0]; - -/// One stereo band's mixing coefficients. -type H4 = [Complex; 4]; // h11, h12, h21, h22 - -/// A stereo pair of hybrid-domain frames. -pub type HybridPair = (Vec>, Vec>); - -/// §8.6.4.6 stereo processor: per-band coefficient state across -/// frames plus the IPD/OPD smoothing history. -#[derive(Debug, Clone)] -pub struct PsStereo { - /// Stereo band count in force (20 or 34). - n_bands: usize, - /// `H(·, n_{−1})` — coefficients at the previous frame's last - /// slot, per stereo band. - h_prev: Vec

, - /// IPD/OPD angle history: `[.., e−1]` and `[.., e]` positions - /// (radians), per stereo band. - ipd_hist: [Vec; 2], - opd_hist: [Vec; 2], -} - -impl PsStereo { - /// Fresh state (first frame interpolates from zero coefficients). - #[must_use] - pub fn new(n_bands: usize) -> Self { - PsStereo { - n_bands, - h_prev: vec![[Complex::default(); 4]; n_bands], - ipd_hist: [vec![0.0; n_bands], vec![0.0; n_bands]], - opd_hist: [vec![0.0; n_bands], vec![0.0; n_bands]], - } - } - - /// Table 8.47 — switch the stereo band count, re-mapping the - /// retained coefficients through Table 8.45 / 8.46 and resetting - /// the phase-smoothing history. - pub fn switch_bands(&mut self, n_bands: usize) { - if n_bands == self.n_bands { - return; - } - let map = |vals: Vec| -> Vec { - if n_bands == 34 { - MAP_20_TO_34.iter().map(|m| m.apply_f64(&vals)).collect() - } else { - MAP_34_TO_20.iter().map(|m| m.apply_f64(&vals)).collect() - } - }; - let mut new_h = vec![[Complex::default(); 4]; n_bands]; - for c in 0..4 { - let re: Vec = self.h_prev.iter().map(|h| h[c].re).collect(); - let im: Vec = self.h_prev.iter().map(|h| h[c].im).collect(); - let re = map(re); - let im = map(im); - for (b, h) in new_h.iter_mut().enumerate() { - h[c] = Complex::new(re[b], im[b]); - } - } - self.h_prev = new_h; - self.n_bands = n_bands; - self.ipd_hist = [vec![0.0; n_bands], vec![0.0; n_bands]]; - self.opd_hist = [vec![0.0; n_bands], vec![0.0; n_bands]]; - } - - /// The stereo band count in force. - #[must_use] - pub fn n_bands(&self) -> usize { - self.n_bands - } - - /// Process one stereo frame: mix `s` (mono hybrid) and `d` - /// (de-correlated hybrid) into `(l, r)` hybrid signals per the - /// resolved parameters. `config` must agree with `n_bands`. - pub fn process( - &mut self, - ps: &PsData, - idx: &PsIndices, - config: HybridConfig, - s: &[Vec], - d: &[Vec], - ) -> Result { - let nb = self.n_bands; - let expected = match config { - HybridConfig::Bands1020 => 20, - HybridConfig::Bands34 => 34, - }; - if expected != nb || s.len() != NUM_QMF_SLOTS || d.len() != NUM_QMF_SLOTS { - return Err(Error::PsDataInvalid); - } - let b_k = parameter_map(config); - let conj_k = conjugate_flags(config); - let nr_hyb = config.nr_bands(); - if s.iter().chain(d.iter()).any(|row| row.len() != nr_hyb) { - return Err(Error::PsDataInvalid); - } - - // Per-slot H matrices, per stereo band. - let mut h_slots = vec![vec![[Complex::default(); 4]; nb]; NUM_QMF_SLOTS]; - - if ps.num_env == 0 { - // §8.6.4.6.5: hold the previous coefficients all frame. - for slot in h_slots.iter_mut() { - slot.copy_from_slice(&self.h_prev); - } - } else { - // Envelope borders n_e. - let borders: Vec = if ps.frame_class { - ps.border_position - .iter() - .map(|&b| usize::from(b).min(NUM_QMF_SLOTS - 1)) - .collect() - } else { - (0..ps.num_env) - .map(|e| NUM_QMF_SLOTS * (e + 1) / ps.num_env - 1) - .collect() - }; - - let mut h_from = self.h_prev.clone(); - let mut n_from: isize = -1; // "border" behind slot 0 - for (e, &n_e) in borders.iter().enumerate() { - let h_to = self.envelope_h(ps, idx, e)?; - // §8.6.4.6.4: first region divides by n_0 with - // multiplier n; later regions by (n_e − n_{e−1}). - let (den, base) = if e == 0 { - (n_e.max(1) as f64, 0isize) - } else { - (((n_e as isize - n_from).max(1)) as f64, n_from) - }; - let lo = ((n_from + 1).max(0)) as usize; - let hi = n_e.min(NUM_QMF_SLOTS - 1); - for (n, slot) in h_slots.iter_mut().enumerate().take(hi + 1).skip(lo) { - let t = (n as isize - base) as f64 / den; - for (b, cell) in slot.iter_mut().enumerate() { - for c in 0..4 { - cell[c] = h_from[b][c] + (h_to[b][c] - h_from[b][c]) * t; - } - } - } - h_from = h_to; - n_from = n_e as isize; - } - // Region after the last border: hold. - let lo = ((n_from + 1).max(0)) as usize; - for slot in h_slots.iter_mut().skip(lo) { - slot.copy_from_slice(&h_from); - } - self.h_prev = h_from; - } - - // Mix. - let mut l = vec![vec![Complex::default(); nr_hyb]; NUM_QMF_SLOTS]; - let mut r = vec![vec![Complex::default(); nr_hyb]; NUM_QMF_SLOTS]; - for n in 0..NUM_QMF_SLOTS { - for k in 0..nr_hyb { - let b = usize::from(b_k[k]); - let mut h = h_slots[n][b]; - if conj_k.contains(&k) { - for c in h.iter_mut() { - *c = c.conj(); - } - } - l[n][k] = h[0] * s[n][k] + h[2] * d[n][k]; - r[n][k] = h[1] * s[n][k] + h[3] * d[n][k]; - } - } - Ok((l, r)) - } - - /// Derive `h11..h22` per stereo band for envelope `e` - /// (§8.6.4.6.2 + §8.6.4.6.3), advancing the phase history. - fn envelope_h(&mut self, ps: &PsData, idx: &PsIndices, e: usize) -> Result> { - let nb = self.n_bands; - - // Map the parameter vectors to the stereo band count; a - // disabled parameter kind is index 0 (§8.5.2 defaults). - let iid = match idx.iid.get(e) { - Some(v) => map_indices(v, nb), - None => vec![0; nb], - }; - let icc = match idx.icc.get(e) { - Some(v) => map_indices(v, nb), - None => vec![0; nb], - }; - if iid.len() != nb || icc.len() != nb { - return Err(Error::PsDataInvalid); - } - - let fine = ps.config.iid_quant_fine(); - let rb = ps.config.icc_mode >= 3; - - let mut out = vec![[Complex::default(); 4]; nb]; - for b in 0..nb { - let iid_db = if fine { - *IID_DB_FINE - .get((iid[b] + 15) as usize) - .ok_or(Error::PsDataInvalid)? - } else { - *IID_DB_COARSE - .get((iid[b] + 7) as usize) - .ok_or(Error::PsDataInvalid)? - }; - let c = 10f64.powf(iid_db / 20.0); - let rho = *ICC_RHO.get(icc[b] as usize).ok_or(Error::PsDataInvalid)?; - - let (h11, h12, h21, h22) = if rb { mix_rb(c, rho) } else { mix_ra(c, rho) }; - out[b] = [ - Complex::new(h11, 0.0), - Complex::new(h12, 0.0), - Complex::new(h21, 0.0), - Complex::new(h22, 0.0), - ]; - } - - if ps.enable_ipdopd { - // Zero-extended, band-count-mapped phase indices. - let nr = ps.config.nr_ipdopd_par(); - let native = if ps.config.iid_mode % 3 == 0 { - 10 - } else if ps.config.iid_mode % 3 == 1 { - 20 - } else { - 34 - }; - let extend = |v: Option<&Vec>| -> Vec { - let mut full = vec![0i32; native]; - if let Some(v) = v { - full[..nr.min(v.len())].copy_from_slice(&v[..nr.min(v.len())]); - } - map_indices(&full, nb) - .iter() - .map(|&i| f64::from(i) * core::f64::consts::FRAC_PI_4) - .collect() - }; - let ipd_cur = extend(idx.ipd.get(e)); - let opd_cur = extend(idx.opd.get(e)); - for b in 0..nb { - let sm = |h: &[Vec; 2], cur: f64| -> f64 { - let mut acc = Complex::default(); - for (w, ang) in [(0.25, h[0][b]), (0.5, h[1][b]), (1.0, cur)] { - let (si, co) = ang.sin_cos(); - acc += Complex::new(co * w, si * w); - } - acc.im.atan2(acc.re) - }; - let phi_opd = sm(&self.opd_hist, opd_cur[b]); - let phi_ipd = sm(&self.ipd_hist, ipd_cur[b]); - let phi1 = phi_opd; - let phi2 = phi_opd - phi_ipd; - let (s1, c1) = phi1.sin_cos(); - let (s2, c2) = phi2.sin_cos(); - let r1 = Complex::new(c1, s1); - let r2 = Complex::new(c2, s2); - out[b][0] = out[b][0] * r1; - out[b][2] = out[b][2] * r1; - out[b][1] = out[b][1] * r2; - out[b][3] = out[b][3] * r2; - } - // Advance the history. - self.ipd_hist[0] = core::mem::take(&mut self.ipd_hist[1]); - self.ipd_hist[1] = ipd_cur; - self.opd_hist[0] = core::mem::take(&mut self.opd_hist[1]); - self.opd_hist[1] = opd_cur; - } - Ok(out) - } -} - -/// §8.6.4.6.2.1 mixing procedure Ra. -fn mix_ra(c: f64, rho: f64) -> (f64, f64, f64, f64) { - let denom = (1.0 + c * c).sqrt(); - let c1 = core::f64::consts::SQRT_2 / denom; - let c2 = core::f64::consts::SQRT_2 * c / denom; - let alpha = 0.5 * rho.clamp(-1.0, 1.0).acos(); - let beta = alpha * (c1 - c2) / core::f64::consts::SQRT_2; - ( - (alpha + beta).cos() * c2, - (beta - alpha).cos() * c1, - (alpha + beta).sin() * c2, - (beta - alpha).sin() * c1, - ) -} - -/// §8.6.4.6.2.2 mixing procedure Rb. -fn mix_rb(c: f64, rho: f64) -> (f64, f64, f64, f64) { - let rho = rho.max(0.05); - let mut alpha = if (c - 1.0).abs() < 1e-12 { - core::f64::consts::FRAC_PI_4 - } else { - 0.5 * (2.0 * c * rho / (c * c - 1.0)).atan() - }; - // Modulo correction into [0, π/2). - alpha -= (alpha / core::f64::consts::FRAC_PI_2).floor() * core::f64::consts::FRAC_PI_2; - let mu = 1.0 + (4.0 * rho * rho - 4.0) / (c + 1.0 / c).powi(2); - let gamma = ((1.0 - mu) / (1.0 + mu)).max(0.0).sqrt().atan(); - let s2 = core::f64::consts::SQRT_2; - ( - s2 * alpha.cos() * gamma.cos(), - s2 * alpha.sin() * gamma.cos(), - -s2 * alpha.sin() * gamma.sin(), - s2 * alpha.cos() * gamma.sin(), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ps_data::PsIndexState; - use oxideav_core::bits::{BitReader, BitWriter}; - - /// Build a one-envelope FIX ps_data with the given uniform IID / - /// ICC index (coarse grid, 10 pars each) and resolve it. - fn ps_with(iid_idx: i32, icc_idx: i32) -> (PsData, PsIndices) { - let mut w = BitWriter::new(); - w.write_bit(true); // header - w.write_bit(true); // enable_iid - w.write_u32(0, 3); // iid_mode 0 - w.write_bit(true); // enable_icc - w.write_u32(0, 3); // icc_mode 0 - w.write_bit(false); // enable_ext - w.write_bit(false); // FIX - w.write_u32(1, 2); // num_env = 1 - w.write_bit(false); // iid freq - for b in 0..10 { - // First band carries the index, the rest delta 0. - let (len, code) = crate::ps_huffman::HUFF_IID_DF[(iid_idx + 14) as usize]; - if b == 0 { - w.write_u32(code, u32::from(len)); - } else { - let (l0, c0) = crate::ps_huffman::HUFF_IID_DF[14]; - w.write_u32(c0, u32::from(l0)); - } - } - w.write_bit(false); // icc freq - for b in 0..10 { - let (len, code) = crate::ps_huffman::HUFF_ICC_DF[(icc_idx + 7) as usize]; - if b == 0 { - w.write_u32(code, u32::from(len)); - } else { - let (l0, c0) = crate::ps_huffman::HUFF_ICC_DF[7]; - w.write_u32(c0, u32::from(l0)); - } - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ps = PsData::parse(&mut r, None).unwrap().unwrap(); - let mut st = PsIndexState::default(); - let idx = ps.resolve(&mut st).unwrap(); - (ps, idx) - } - - fn ones(nr: usize) -> Vec> { - (0..NUM_QMF_SLOTS) - .map(|_| vec![Complex::new(1.0, 0.0); nr]) - .collect() - } - - fn zeros(nr: usize) -> Vec> { - (0..NUM_QMF_SLOTS) - .map(|_| vec![Complex::default(); nr]) - .collect() - } - - /// ICC = 1 (index 0) makes α = β = 0: the mix is pure IID panning - /// `l = c2·s`, `r = c1·s`, `d` unused. Pin the exact §8.6.4.6.2.1 - /// scale factors at the last slot (interpolation complete). - #[test] - fn pure_iid_panning_matches_scale_factors() { - let config = HybridConfig::Bands1020; - let (ps, idx) = ps_with(7, 0); // +25 dB - let mut st = PsStereo::new(20); - let s = ones(config.nr_bands()); - let d = zeros(config.nr_bands()); - let (l, r) = st.process(&ps, &idx, config, &s, &d).unwrap(); - let c = 10f64.powf(25.0 / 20.0); - let c1 = core::f64::consts::SQRT_2 / (1.0 + c * c).sqrt(); - let c2 = c * c1; - // Hybrid channel 20 (stereo band 16), final slot: h fully - // interpolated to the envelope value. - let n = NUM_QMF_SLOTS - 1; - assert!((l[n][20].re - c2).abs() < 1e-12, "{} vs {c2}", l[n][20].re); - assert!((r[n][20].re - c1).abs() < 1e-12); - assert!(l[n][20].im.abs() < 1e-15 && r[n][20].im.abs() < 1e-15); - // Left is 25 dB louder. - let ratio = 20.0 * (l[n][20].re / r[n][20].re).log10(); - assert!((ratio - 25.0).abs() < 1e-9); - } - - /// ICC = −1 (index 7) with IID 0: α = π/2, the channels are the - /// anti-phase de-correlated pair `l = d`, `r = −d` (§8.6.4.6.2.1). - #[test] - fn full_anticorrelation_uses_decorrelated_signal() { - let config = HybridConfig::Bands1020; - let (ps, idx) = ps_with(0, 7); - let mut st = PsStereo::new(20); - let s = ones(config.nr_bands()); - let d = ones(config.nr_bands()); - let (l, r) = st.process(&ps, &idx, config, &s, &d).unwrap(); - let n = NUM_QMF_SLOTS - 1; - // h11 = cos(π/2) = 0, h21 = sin(π/2) = 1 → l = d. - assert!((l[n][20].re - 1.0).abs() < 1e-12); - // h12 = cos(−π/2) = 0, h22 = sin(−π/2) = −1 → r = −d. - assert!((r[n][20].re + 1.0).abs() < 1e-12); - } - - /// The first region interpolates from zero (fresh state) to the - /// envelope coefficients linearly in n/n_0. - #[test] - fn first_region_interpolates_from_zero() { - let config = HybridConfig::Bands1020; - let (ps, idx) = ps_with(0, 0); // IID 0 dB, ICC 1 → h11 = h12 = 1 - let mut st = PsStereo::new(20); - let s = ones(config.nr_bands()); - let d = zeros(config.nr_bands()); - let (l, _r) = st.process(&ps, &idx, config, &s, &d).unwrap(); - // num_env = 1, FIX → n_0 = 31; H(n) = n/31 · h. - for (n, row) in l.iter().enumerate() { - let expect = n as f64 / 31.0; - assert!( - (row[20].re - expect).abs() < 1e-12, - "slot {n}: {} vs {expect}", - row[20].re - ); - } - // A second identical frame is flat at the full value. - let (l2, _) = st.process(&ps, &idx, config, &s, &d).unwrap(); - for row in &l2 { - assert!((row[20].re - 1.0).abs() < 1e-12); - } - } - - /// num_env = 0 holds the previous coefficients for the whole - /// frame. - #[test] - fn zero_envelopes_hold_previous_coefficients() { - let config = HybridConfig::Bands1020; - let (ps, idx) = ps_with(7, 0); - let mut st = PsStereo::new(20); - let s = ones(config.nr_bands()); - let d = zeros(config.nr_bands()); - let _ = st.process(&ps, &idx, config, &s, &d).unwrap(); - // Hold frame: num_env = 0 (frame_class FIX, num_env_idx 0). - let mut hold = ps.clone(); - hold.num_env = 0; - let empty = PsIndices::default(); - let (l, r) = st.process(&hold, &empty, config, &s, &d).unwrap(); - let c = 10f64.powf(25.0 / 20.0); - let c1 = core::f64::consts::SQRT_2 / (1.0 + c * c).sqrt(); - for row in &l { - assert!((row[20].re - c * c1).abs() < 1e-12); - } - for row in &r { - assert!((row[20].re - c1).abs() < 1e-12); - } - } - - /// Rb at c = 1, ρ = 1: α = π/4, μ = 1, γ = 0 → an energy- - /// preserving 45° rotation of the mono signal (`h11 = h12 = 1`, - /// `h21 = h22 = 0`). - #[test] - fn rb_identity_point() { - let (h11, h12, h21, h22) = mix_rb(1.0, 1.0); - assert!((h11 - 1.0).abs() < 1e-12); - assert!((h12 - 1.0).abs() < 1e-12); - assert!(h21.abs() < 1e-12); - assert!(h22.abs() < 1e-12); - } - - /// Ra preserves total energy: |h11|² + |h12|² + |h21|² + |h22|² - /// = c1² + c2² = 2 for every cue combination. - #[test] - fn ra_energy_invariant() { - for iid in -7..=7 { - for &rho in &ICC_RHO { - let c = 10f64.powf(IID_DB_COARSE[(iid + 7) as usize] / 20.0); - let (a, b, x, y) = mix_ra(c, rho); - let e = a * a + b * b + x * x + y * y; - assert!((e - 2.0).abs() < 1e-12, "iid {iid} rho {rho}: {e}"); - } - } - } -} diff --git a/crates/vendor/oxideav-aac/src/pulse_data.rs b/crates/vendor/oxideav-aac/src/pulse_data.rs deleted file mode 100644 index e6292b0e..00000000 --- a/crates/vendor/oxideav-aac/src/pulse_data.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! `pulse_data()` parser + encoder primitive — ISO/IEC 14496-3 -//! §4.4.6.3 / Table 4.7. -//! -//! `pulse_data()` is the optional "pulse escape" tool inside -//! `individual_channel_stream()`: when the encoder finds it cheaper -//! to replace a small number (1..=4) of quantised spectral -//! coefficients with smaller ones plus a fix-up record than to spend -//! the bits on the literal escape codeword, it writes a `pulse_data()` -//! block that the decoder uses to restore the original amplitudes -//! after Huffman decoding. The pulse escape is dispatched by the -//! one-bit `pulse_data_present` flag immediately after -//! `scale_factor_data()` (Table 4.44 / Table 4.50). -//! -//! ## Wire layout (Table 4.7) -//! -//! ```text -//! pulse_data() { -//! number_pulse; 2 bits -//! pulse_start_sfb; 6 bits -//! for (i = 0; i < number_pulse + 1; i++) { -//! pulse_offset[i]; 5 bits -//! pulse_amp[i]; 4 bits -//! } -//! } -//! ``` -//! -//! Every field is fixed-width. The actual pulse count on the wire -//! is `number_pulse + 1` (so 1..=4 pulses, never zero), encoded in -//! 2 bits as `0..=3`. -//! -//! ## What this module covers -//! -//! * [`PulseData::parse`] — read a Table 4.7 block from a -//! [`BitReader`], surfacing the raw wire fields without applying -//! the §4.6.13 reconstruction (the spectral fix-up itself needs -//! `swb_offset_long_window[]` + the post-Huffman `x_quant` array, -//! neither of which exists in Phase 2 yet). -//! * [`PulseData::write`] — the inverse: serialise a [`PulseData`] -//! onto a [`BitWriter`] in bit-exact Table 4.7 form. Surfaces -//! field-overflow as [`Error::PulseDataEncodeInvalid`]. -//! -//! ## What this module does *not* cover -//! -//! * The §4.6.13 reconstruction loop (`k += -//! swb_offset[pulse_start_sfb]; k += pulse_offset[j]; x_quant[…] ±= -//! pulse_amp[j]`) is deferred until `swb_offset` tables land with -//! `spectral_data()`. -//! * The normative constraint that `pulse_data_present` *must* be 0 -//! when `window_sequence == EIGHT_SHORT_SEQUENCE` (§4.4.6.3 last -//! paragraph) is the responsibility of the dispatching -//! `individual_channel_stream()` (which has not landed yet); the -//! parser and writer here intentionally surface the literal Table 4.7 -//! bytes regardless of the surrounding window sequence so that -//! future round work has access to the raw decoded record. -//! * No validation against `swb_offset_long_window[fs_index]` — the -//! parser cannot tell whether `pulse_start_sfb` is in-range for a -//! given sample rate without the offset table; the encoder cannot -//! tell whether a pulse position lands inside the represented -//! coefficient grid. These are §4.6.13 reconstruction concerns, -//! not Table 4.7 wire-format concerns. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::{Error, Result}; - -/// Per-pulse `(offset, amp)` record. Both fields are unsigned. -/// -/// * `offset` — 5 bits. `pulse_offset[i]` per Table 4.7. Read by -/// the decoder as a delta added to the running coefficient index -/// `k` (initialised to `swb_offset[pulse_start_sfb]` before the -/// loop). -/// * `amp` — 4 bits. `pulse_amp[i]` per Table 4.7. Unsigned -/// magnitude added to (or subtracted from, depending on the sign -/// of the existing `x_quant` coefficient) the reconstructed -/// spectral coefficient. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Pulse { - /// `pulse_offset[i]` — 5-bit unsigned delta. - pub offset: u8, - /// `pulse_amp[i]` — 4-bit unsigned magnitude. - pub amp: u8, -} - -/// Width in bits of the wire `pulse_offset` field. ISO/IEC 14496-3 -/// Table 4.7. -pub const PULSE_OFFSET_BITS: u32 = 5; - -/// Width in bits of the wire `pulse_amp` field. ISO/IEC 14496-3 -/// Table 4.7. -pub const PULSE_AMP_BITS: u32 = 4; - -/// Maximum pulse count expressible in the 2-bit `number_pulse` -/// field. The wire value runs `0..=3`; the actual pulse count is -/// `number_pulse + 1`, so `MAX_PULSES == 4`. -pub const MAX_PULSES: usize = 4; - -/// Parsed `pulse_data()` block (Table 4.7). -/// -/// `pulses` always carries 1..=4 entries (since `number_pulse + 1 -/// >= 1`); this is enforced by the writer and produced by the -/// parser. An empty `pulses` vector is rejected by [`PulseData::write`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PulseData { - /// `pulse_start_sfb` — 6-bit index of the lowest scalefactor - /// band that carries a pulse fix-up. - pub pulse_start_sfb: u8, - /// `pulses[i]` — the `(offset, amp)` records, in wire order. - /// Length is in `1..=MAX_PULSES`; the wire `number_pulse` field - /// is `pulses.len() - 1`. - pub pulses: Vec, -} - -impl PulseData { - /// Parse a `pulse_data()` from `reader`. - /// - /// Returns [`Error::UnexpectedEnd`] on bit-reader underflow. - /// Never returns a structural-error variant because every field - /// of Table 4.7 is fixed-width and unconditionally well-formed - /// up to bit-position arithmetic. - pub fn parse(reader: &mut BitReader<'_>) -> Result { - let number_pulse = read_u8(reader, 2)?; - let pulse_start_sfb = read_u8(reader, 6)?; - let count = number_pulse as usize + 1; - let mut pulses = Vec::with_capacity(count); - for _ in 0..count { - let offset = read_u8(reader, PULSE_OFFSET_BITS)?; - let amp = read_u8(reader, PULSE_AMP_BITS)?; - pulses.push(Pulse { offset, amp }); - } - Ok(PulseData { - pulse_start_sfb, - pulses, - }) - } - - /// Wire `number_pulse` value (always `pulses.len() - 1`). - /// Returns `0` for an empty `pulses` vector, which is itself - /// rejected by [`PulseData::write`] — the accessor exists so the - /// writer doesn't have to inline the subtraction with a saturating - /// path of its own. - pub fn number_pulse(&self) -> u8 { - self.pulses.len().saturating_sub(1) as u8 - } - - /// Encode `pulse_data()` onto `writer`, the inverse of - /// [`PulseData::parse`]. - /// - /// The writer mirrors Table 4.7 verbatim — 2-bit `number_pulse` - /// (where the wire value is `pulses.len() - 1`), 6-bit - /// `pulse_start_sfb`, then `(5-bit pulse_offset + 4-bit - /// pulse_amp)` per entry. - /// - /// Returns [`Error::PulseDataEncodeInvalid`] if: - /// - /// * `pulses.is_empty()` — Table 4.7's loop bound is - /// `number_pulse + 1`, so the smallest legal pulse count is 1. - /// A zero-pulse block has no wire representation that round- - /// trips through [`PulseData::parse`]. - /// * `pulses.len() > MAX_PULSES` (the 2-bit field maxes at 4). - /// * `pulse_start_sfb > 0x3f` (6-bit field overflow). - /// * Any `Pulse::offset > 0x1f` (5-bit field overflow). - /// * Any `Pulse::amp > 0x0f` (4-bit field overflow). - pub fn write(&self, writer: &mut BitWriter) -> Result<()> { - if self.pulses.is_empty() || self.pulses.len() > MAX_PULSES { - return Err(Error::PulseDataEncodeInvalid); - } - if self.pulse_start_sfb > 0x3f { - return Err(Error::PulseDataEncodeInvalid); - } - for p in &self.pulses { - if p.offset > 0x1f || p.amp > 0x0f { - return Err(Error::PulseDataEncodeInvalid); - } - } - - let number_pulse = (self.pulses.len() - 1) as u32; - writer.write_u32(number_pulse, 2); - writer.write_u32(self.pulse_start_sfb as u32, 6); - for p in &self.pulses { - writer.write_u32(p.offset as u32, PULSE_OFFSET_BITS); - writer.write_u32(p.amp as u32, PULSE_AMP_BITS); - } - Ok(()) - } -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} diff --git a/crates/vendor/oxideav-aac/src/raw_data_block.rs b/crates/vendor/oxideav-aac/src/raw_data_block.rs deleted file mode 100644 index 727ee1c1..00000000 --- a/crates/vendor/oxideav-aac/src/raw_data_block.rs +++ /dev/null @@ -1,666 +0,0 @@ -//! `raw_data_block()` syntactic walker. -//! -//! ISO/IEC 14496-3 §4.4.2.1 defines `raw_data_block()` as a sequence -//! of *syntactic elements*, each prefixed by a 3-bit `id_syn_ele` -//! identifier (Table 4.71). The element types are: -//! -//! | id (binary) | id (decimal) | name | role | -//! |-------------|--------------|------|--------------------------------------------| -//! | `0b000` | 0 | SCE | single-channel element (mono) | -//! | `0b001` | 1 | CPE | channel-pair element | -//! | `0b010` | 2 | CCE | coupling channel element | -//! | `0b011` | 3 | LFE | low-frequency-effects element | -//! | `0b100` | 4 | DSE | data stream element | -//! | `0b101` | 5 | PCE | program config element | -//! | `0b110` | 6 | FIL | fill element (padding / extension payload) | -//! | `0b111` | 7 | END | block terminator | -//! -//! After the terminating `END`, ISO/IEC 14496-3 §4.4.2.1 requires the -//! decoder to byte-align the bit-reader before the next -//! `raw_data_block()` begins. The walker performs that alignment so -//! the next call after `END` resumes on a fresh byte boundary. -//! -//! ## Phase 1 scope -//! -//! This module is the **syntactic skeleton** — the walker emits an -//! [`Element`] per `id_syn_ele` it encounters and stops at `END`. -//! Per-element bodies are handled as follows: -//! -//! * **SCE / CPE / CCE / LFE**: the walker reads the mandatory 4-bit -//! `element_instance_tag` and then *stops body parsing*. The -//! consumer must advance the [`BitReader`](oxideav_core::bits::BitReader) -//! past the channel-element body itself; Phase 2 will absorb that -//! logic. The emitted [`Element::ChannelElement`] carries the -//! element kind and its tag. -//! * **FIL**: parsed as ISO/IEC 14496-3 §4.4.2.7 — 4-bit -//! `count`, optional 8-bit `esc_count` escape (when `count == 15`, -//! the real byte count is `count + esc_count − 1`), then *count* -//! bytes of `extension_payload` which are skipped without -//! interpretation. The emitted [`Element::Fill`] reports the byte -//! length skipped. -//! * **DSE**: parsed as ISO/IEC 14496-3 §4.4.2.5 — 4-bit -//! `element_instance_tag`, 1-bit `data_byte_align_flag`, 8-bit -//! `count`, optional 8-bit `esc_count`, byte-align (if flag set), -//! then *count* bytes of `data_stream_byte[]`. -//! * **PCE**: parsed via [`crate::pce::Pce::parse`] with an -//! `origin_bit_offset` of `0` (the standalone-in-`raw_data_block` -//! form has no enclosing ASC, so the Table 4.2 `byte_alignment()` -//! resolves to the absolute byte boundary). The walker emits -//! [`Element::ProgramConfig`] carrying the resolved -//! [`crate::pce::Pce`]. -//! * **END**: emits [`Element::End`] and byte-aligns the reader. -//! Subsequent calls return `None`. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::pce::Pce; -use crate::{Error, Result}; - -/// Syntactic element identifier — the 3-bit `id_syn_ele` field -/// defined in ISO/IEC 14496-3 Table 4.71. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum IdSynEle { - /// `0b000` — single-channel element. - Sce = 0, - /// `0b001` — channel-pair element. - Cpe = 1, - /// `0b010` — coupling channel element. - Cce = 2, - /// `0b011` — low-frequency-effects element. - Lfe = 3, - /// `0b100` — data stream element. - Dse = 4, - /// `0b101` — program config element. - Pce = 5, - /// `0b110` — fill element. - Fil = 6, - /// `0b111` — raw-data-block terminator. - End = 7, -} - -impl IdSynEle { - /// Map a 3-bit wire value (0..=7) to the corresponding variant. - pub fn from_bits(bits: u8) -> Self { - match bits & 0b111 { - 0 => IdSynEle::Sce, - 1 => IdSynEle::Cpe, - 2 => IdSynEle::Cce, - 3 => IdSynEle::Lfe, - 4 => IdSynEle::Dse, - 5 => IdSynEle::Pce, - 6 => IdSynEle::Fil, - _ => IdSynEle::End, - } - } - - /// Short upper-case name as used in the spec table and the - /// AAC_TRACE fixture corpus (`SCE`, `CPE`, `CCE`, `LFE`, `DSE`, - /// `PCE`, `FIL`, `END`). - pub fn name(self) -> &'static str { - match self { - IdSynEle::Sce => "SCE", - IdSynEle::Cpe => "CPE", - IdSynEle::Cce => "CCE", - IdSynEle::Lfe => "LFE", - IdSynEle::Dse => "DSE", - IdSynEle::Pce => "PCE", - IdSynEle::Fil => "FIL", - IdSynEle::End => "END", - } - } -} - -/// An event emitted by [`Walker::next_element`]. -/// -/// The walker emits exactly one event per `id_syn_ele` it consumes -/// and stops at `END`. For non-`End` events the bit-reader position -/// after the call reflects the bytes the walker itself consumed -/// (header + any per-element bookkeeping it parses); see the -/// per-variant docs for which bytes have been skipped. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Element { - /// SCE / CPE / CCE / LFE — channel element. The walker has - /// consumed the 3-bit `id_syn_ele` and the 4-bit - /// `element_instance_tag`. The channel-element body (`ics_info`, - /// section data, scale factors, spectral data, …) starts at the - /// current bit-reader position and is **not** parsed in Phase 1. - ChannelElement { - /// The channel element variant (`Sce`, `Cpe`, `Cce`, or - /// `Lfe`). - kind: IdSynEle, - /// The 4-bit `element_instance_tag` read from the wire. - element_instance_tag: u8, - }, - /// FIL — fill element. The walker has consumed the 3-bit - /// `id_syn_ele`, the 4-bit `count`, the optional 8-bit - /// `esc_count`, and the resulting *count* `extension_payload` - /// bytes. - Fill { - /// Total `extension_payload` bytes skipped (`count` after - /// optional escape expansion). - payload_bytes: u32, - }, - /// DSE — data stream element. The walker has consumed the - /// header (3-bit `id_syn_ele`, 4-bit `element_instance_tag`, - /// 1-bit `data_byte_align_flag`, 8-bit `count`, optional 8-bit - /// `esc_count`, optional byte-align) and the resulting *count* - /// `data_stream_byte[]` values. - Data { - /// The 4-bit `element_instance_tag` read from the wire. - element_instance_tag: u8, - /// `true` ⇔ a `data_byte_align_flag == 1` was processed and - /// the bit-reader was byte-aligned before the payload. - byte_align_flag: bool, - /// Total `data_stream_byte[]` bytes skipped (`count` after - /// optional escape expansion). - payload_bytes: u32, - }, - /// PCE — program config element. The walker has consumed the - /// 3-bit `id_syn_ele` and the entire PCE body per - /// [`Pce::parse`] (`origin_bit_offset = 0` — see - /// [`crate::pce`] for the standalone vs ASC-embedded handling - /// of the trailing `byte_alignment()`). - ProgramConfig(Pce), - /// END (`0b111`) — the raw-data-block terminator. The walker - /// has consumed the 3-bit `id_syn_ele` and byte-aligned the - /// bit-reader (ISO/IEC 14496-3 §4.4.2.1). - End, -} - -/// Walker over a `raw_data_block()` payload. -/// -/// Drive the walker by calling [`Walker::next_element`] in a loop -/// until it returns either an [`Element::End`] event or `None` -/// (input exhausted before reaching `END`). See the [module -/// docs](self) for the per-element body-skipping rules and what -/// the walker currently does not parse. -pub struct Walker<'a, 'b> { - reader: &'b mut BitReader<'a>, - finished: bool, -} - -impl<'a, 'b> core::fmt::Debug for Walker<'a, 'b> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Walker") - .field("finished", &self.finished) - .field("bit_position", &self.reader.bit_position()) - .finish() - } -} - -impl<'a, 'b> Walker<'a, 'b> { - /// Bind a walker to an existing [`BitReader`] positioned at the - /// first byte of a `raw_data_block()` payload. - pub fn new(reader: &'b mut BitReader<'a>) -> Self { - Self { - reader, - finished: false, - } - } - - /// Read the next syntactic element. Returns `Ok(Some(_))` for - /// every non-terminating element, `Ok(Some(Element::End))` once - /// (and the walker becomes `finished`), and `Ok(None)` for any - /// further calls after `End`. - /// - /// Errors out with [`Error::UnsupportedElementSkip`] when the - /// next `id_syn_ele` would require body parsing Phase 1 has - /// not landed yet. As of this round, PCE is fully parsed - /// (round 126) and FIL / DSE are skipped (round 121); only the - /// channel-element bodies (SCE/CPE/CCE/LFE) remain deferred, - /// and even those return [`Element::ChannelElement`] for the - /// header — the caller must advance the bit-reader past the - /// body itself if more than one element is needed in a single - /// `raw_data_block()`. - pub fn next_element(&mut self) -> Result> { - self.next_element_impl(true) - } - - /// [`Self::next_element`], except a FIL element's - /// `extension_payload` body is **left unconsumed**: the returned - /// [`Element::Fill`] reports the byte count and the bit-reader - /// stays at the first extension-payload bit, so the caller can - /// parse the Table 4.51 `extension_payload()` chain itself (e.g. - /// to route an `EXT_SBR_DATA` payload into the SBR decoder). The - /// caller **must** consume exactly `payload_bytes` bytes worth of - /// bits before the next call. - pub fn next_element_keep_fill(&mut self) -> Result> { - self.next_element_impl(false) - } - - fn next_element_impl(&mut self, consume_fill: bool) -> Result> { - if self.finished { - return Ok(None); - } - - let id_bits = self.reader.read_u32(3).map_err(|_| Error::UnexpectedEnd)? as u8; - let id = IdSynEle::from_bits(id_bits); - - match id { - IdSynEle::Sce | IdSynEle::Cpe | IdSynEle::Cce | IdSynEle::Lfe => { - let element_instance_tag = - self.reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; - Ok(Some(Element::ChannelElement { - kind: id, - element_instance_tag, - })) - } - IdSynEle::Fil => { - let payload_bytes = self.read_fill_count()?; - if consume_fill { - self.skip_bytes(payload_bytes)?; - } - Ok(Some(Element::Fill { payload_bytes })) - } - IdSynEle::Dse => { - let element_instance_tag = - self.reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)? as u8; - let byte_align_flag = self.reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let payload_bytes = self.read_data_count()?; - if byte_align_flag { - self.reader.align_to_byte(); - } - self.skip_bytes(payload_bytes)?; - Ok(Some(Element::Data { - element_instance_tag, - byte_align_flag, - payload_bytes, - })) - } - IdSynEle::Pce => { - // Standalone PCE inside a raw_data_block: align the - // PCE's byte_alignment() to the absolute byte - // boundary (origin_bit_offset == 0). The ASC-inline - // variant uses the surrounding ASC origin instead. - let pce = Pce::parse(self.reader, 0)?; - Ok(Some(Element::ProgramConfig(pce))) - } - IdSynEle::End => { - self.reader.align_to_byte(); - self.finished = true; - Ok(Some(Element::End)) - } - } - } - - /// `true` once an [`Element::End`] event has been returned. - pub fn is_finished(&self) -> bool { - self.finished - } - - /// Fill-element byte-count read per ISO/IEC 14496-3 §4.4.2.7. - /// 4-bit `count`; if `count == 15`, an 8-bit `esc_count` follows - /// and the resulting count is `count + esc_count − 1`. - fn read_fill_count(&mut self) -> Result { - let count = self.reader.read_u32(4).map_err(|_| Error::UnexpectedEnd)?; - if count == 15 { - let esc = self.reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)?; - // §4.4.2.7: `cnt = esc_count + 15 - 1`. - Ok(esc + 15 - 1) - } else { - Ok(count) - } - } - - /// Data-stream-element byte-count read per ISO/IEC 14496-3 - /// §4.4.2.5. 8-bit `count`; if `count == 255`, an 8-bit - /// `esc_count` follows and the resulting count is - /// `count + esc_count`. - fn read_data_count(&mut self) -> Result { - let count = self.reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)?; - if count == 255 { - let esc = self.reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)?; - Ok(count + esc) - } else { - Ok(count) - } - } - - /// Skip `n` whole bytes via the bit-reader. - fn skip_bytes(&mut self, n: u32) -> Result<()> { - // Multiplication is safe within u32 because §4.4.2.5 caps - // `count` at 2 × 255 = 510 and §4.4.2.7 caps `cnt` at - // 15 + 255 − 1 = 269, well below `u32::MAX / 8`. - let bits = n.saturating_mul(8); - self.reader.skip(bits).map_err(|_| Error::UnexpectedEnd) - } -} - -// =================================================================== -// raw_data_block() frame assembler — encoder primitive -// =================================================================== -// -// Round 160 lands the symmetric encoder side: a [`FrameAssembler`] -// that composes the existing typed writers into a complete -// `raw_data_block()` byte stream per ISO/IEC 14496-3 §4.4.2.1, the -// inverse of [`Walker`]. The assembler accepts: -// -// * [`FrameAssembler::push_channel_header`] — emits the 3-bit -// `id_syn_ele` (`SCE` / `CPE` / `CCE` / `LFE`) + 4-bit -// `element_instance_tag`. The channel-element *body* -// (`ics_info` → `section_data` → `scale_factor_data` → optional -// `pulse_data` / `tns_data` / `gain_control_data` → `spectral_data`) -// is not internalised yet; the caller is responsible for serialising -// it via the existing per-tool writers (`IcsInfo::write`, -// `SectionData::write`, `ScaleFactorData::write`, `PulseData::write`, -// `TnsData::write`, …). [`FrameAssembler::push_channel_body_bits`] -// appends a pre-serialised body as a bit-slice immediately after a -// channel header. -// -// * [`FrameAssembler::push_fill`] — emits a FIL element per §4.4.2.7, -// including the 8-bit `esc_count` escape when `payload_bytes >= 15` -// (resulting wire `count = 15` + `esc_count = payload_bytes - 15 + 1` -// — the inverse of `read_fill_count`'s `cnt = esc_count + 15 - 1`). -// -// * [`FrameAssembler::push_data`] — emits a DSE element per §4.4.2.5, -// honouring `data_byte_align_flag` (which, when set, byte-aligns -// *before* the payload bytes per §4.4.2.5) and the 8-bit `esc_count` -// escape when `payload_bytes >= 255` (resulting wire `count = 255` + -// `esc_count = payload_bytes - 255` — the inverse of -// `read_data_count`'s `cnt = count + esc_count`). -// -// * [`FrameAssembler::push_end`] — emits the 3-bit `END` terminator -// and byte-aligns to the next byte boundary per §4.4.2.1. -// -// PCE encoding is deferred — [`Pce`] has no `write` primitive yet, and -// adding one is a separate round's worth of work (Tables 4.4 / 4.5 -// front/side/back/lfe element selects, mono / stereo / matrix -// mix-down hints, comment field, plus the relative-origin -// `byte_alignment()` per Table 4.2 Note 1). -// -// The §4.4.2.1 normative constraint that exactly one `END` element -// terminates the block (and that no further elements may follow) is -// enforced by the type-state: [`FrameAssembler::push_end`] consumes -// `self` and returns the finished [`Vec`] (calling any other -// `push_*` after END is a compile-time error). - -/// Encoder-side frame assembler for `raw_data_block()` per ISO/IEC -/// 14496-3 §4.4.2.1 — the bit-exact inverse of [`Walker`]. -/// -/// Construct via [`FrameAssembler::new`] or -/// [`FrameAssembler::with_capacity`], push elements with the -/// `push_*` family in wire order, then finish with -/// [`FrameAssembler::push_end`] which consumes the assembler and -/// returns the byte-aligned frame. END is mandatory — dropping a -/// non-finished assembler discards the in-progress frame. -/// -/// ## Composition with the existing typed writers -/// -/// Channel-element *headers* are emitted by -/// [`FrameAssembler::push_channel_header`]. The channel-element -/// *body* — `ics_info` → `section_data` → `scale_factor_data` → -/// optional `pulse_data` / `tns_data` / `gain_control_data` → -/// `spectral_data` — has no single round-160 writer. Callers -/// serialise the body separately via the existing tool writers -/// ([`crate::ics_info::IcsInfo::write`], -/// [`crate::section_data::SectionData::write`], -/// [`crate::scale_factor_data::ScaleFactorData::write`], -/// [`crate::pulse_data::PulseData::write`], -/// [`crate::tns_data::TnsData::write`]) into an auxiliary -/// [`BitWriter`] and append the resulting bits to the frame via -/// [`FrameAssembler::push_channel_body_bits`]. This keeps the -/// frame-level concern (element ordering + sync + alignment + -/// fill/data escapes + END) separate from the channel-element-level -/// concern (per-tool bit layouts), which round 160 already covers -/// for everything except `gain_control_data` / `spectral_data`. -/// -/// ## Why this is `Phase 2`, not `Phase 1` -/// -/// The Phase 1 [`Walker`] *consumes* a `raw_data_block()` byte slice -/// produced by an external encoder (typically extracted from an ADTS -/// frame or an MP4 audio sample). Phase 2 adds the inverse — the -/// assembler that *produces* the byte slice that the Phase 1 walker -/// can read back. Together they form a complete §4.4.2.1 -/// parse / write cycle for every element type with a bit-exact -/// inverse already in the crate (channel headers, FIL, DSE, END). -pub struct FrameAssembler { - writer: BitWriter, -} - -impl core::fmt::Debug for FrameAssembler { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("FrameAssembler") - .field("bit_position", &self.writer.bit_position()) - .finish() - } -} - -impl Default for FrameAssembler { - fn default() -> Self { - Self::new() - } -} - -impl FrameAssembler { - /// Start a new, empty `raw_data_block()` assembler. - pub fn new() -> Self { - Self { - writer: BitWriter::new(), - } - } - - /// Start a new assembler whose underlying byte buffer is - /// pre-reserved for at least `cap` bytes. - pub fn with_capacity(cap: usize) -> Self { - Self { - writer: BitWriter::with_capacity(cap), - } - } - - /// Current bit position (relative to the start of the frame). - /// Useful for sizing channel-element bodies. - pub fn bit_position(&self) -> u64 { - self.writer.bit_position() - } - - /// Emit a channel-element header — the 3-bit `id_syn_ele` (one of - /// `SCE` / `CPE` / `CCE` / `LFE`) followed by the 4-bit - /// `element_instance_tag` per ISO/IEC 14496-3 §4.4.2.1. - /// - /// The channel-element body itself is the caller's responsibility - /// — see [`FrameAssembler::push_channel_body_bits`] for the - /// post-header append. - /// - /// Returns [`Error::RawDataBlockEncodeInvalid`] when: - /// - /// * `kind` is not one of `SCE` / `CPE` / `CCE` / `LFE` (this - /// helper is for channel elements only — use - /// [`FrameAssembler::push_fill`] / [`FrameAssembler::push_data`] - /// / [`FrameAssembler::push_end`] for the other element types, - /// each of which has its own bespoke wire layout). - /// * `element_instance_tag > 0x0f` (4-bit field overflow). - pub fn push_channel_header(&mut self, kind: IdSynEle, element_instance_tag: u8) -> Result<()> { - match kind { - IdSynEle::Sce | IdSynEle::Cpe | IdSynEle::Cce | IdSynEle::Lfe => {} - _ => return Err(Error::RawDataBlockEncodeInvalid), - } - if element_instance_tag > 0x0f { - return Err(Error::RawDataBlockEncodeInvalid); - } - self.writer.write_u32(kind as u32, 3); - self.writer.write_u32(element_instance_tag as u32, 4); - Ok(()) - } - - /// Append `bit_count` raw bits from `bits` (read MSB-first) to - /// the frame — the channel-element body that follows a - /// [`FrameAssembler::push_channel_header`]. - /// - /// `bits` is interpreted as an MSB-first packed bit-buffer (the - /// same byte layout [`BitWriter::finish`] / [`BitReader::new`] - /// already use throughout the crate). The low `(8 - bit_count % - /// 8) % 8` bits of the last byte are not consumed and may carry - /// arbitrary content. - /// - /// Returns [`Error::RawDataBlockEncodeInvalid`] when `bit_count` - /// exceeds `bits.len() * 8`. - pub fn push_channel_body_bits(&mut self, bits: &[u8], bit_count: u64) -> Result<()> { - if bit_count > (bits.len() as u64).saturating_mul(8) { - return Err(Error::RawDataBlockEncodeInvalid); - } - let mut remaining = bit_count; - let mut byte_idx = 0usize; - // Whole bytes first. - while remaining >= 8 { - self.writer.write_byte(bits[byte_idx]); - byte_idx += 1; - remaining -= 8; - } - // Trailing partial byte: take the high `remaining` bits of - // the next source byte. - if remaining > 0 { - let last = bits[byte_idx]; - let high = (last as u32) >> (8 - remaining); - self.writer.write_u32(high, remaining as u32); - } - Ok(()) - } - - /// Emit a FIL element per ISO/IEC 14496-3 §4.4.2.7 — the 3-bit - /// `id_syn_ele` (`0b110`), the 4-bit `count`, the optional 8-bit - /// `esc_count` escape (when `payload_bytes >= 15`), then the - /// `payload_bytes` of `extension_payload`. - /// - /// Escape arithmetic: the parser's `cnt = esc_count + 15 - 1` - /// (see [`Walker::read_fill_count`]) inverts to `esc_count = - /// payload_bytes - 15 + 1 = payload_bytes - 14`, so the largest - /// representable payload is `15 + 255 - 1 = 269` bytes. Larger - /// fill payloads must be split across multiple FIL elements (as - /// AAC's bit-reservoir code path does in practice for long fill - /// runs). - /// - /// Returns [`Error::RawDataBlockEncodeInvalid`] when: - /// - /// * `payload.len() > 269` (Table 4.57 + escape arithmetic - /// ceiling), or - /// * `payload.len()` exceeds the `bit_count` capacity of the - /// surrounding writer (in practice `u32::MAX`). - pub fn push_fill(&mut self, payload: &[u8]) -> Result<()> { - let n = payload.len(); - if n > 269 { - return Err(Error::RawDataBlockEncodeInvalid); - } - self.writer.write_u32(IdSynEle::Fil as u32, 3); - if n < 15 { - self.writer.write_u32(n as u32, 4); - } else { - self.writer.write_u32(15, 4); - // §4.4.2.7: parser reconstructs `cnt = esc_count + 15 - - // 1`. The inverse, given `cnt == n`, is - // `esc_count = n - 15 + 1 = n - 14`. The 8-bit - // `esc_count` field caps `n` at `15 + 255 - 1 = 269`, - // which we already rejected above when violated. - let esc = (n as u32) - 14; - self.writer.write_u32(esc, 8); - } - // Per §4.4.2.7 the payload is *not* required to be - // byte-aligned — `extension_payload()` is itself a - // bit-level item — but the walker treats it as `count` - // whole bytes, mirroring how every conforming encoder we - // care about ever emits it. The assembler therefore writes - // the payload as bytes too. - for &b in payload { - self.writer.write_byte(b); - } - Ok(()) - } - - /// Emit a DSE element per ISO/IEC 14496-3 §4.4.2.5 — the 3-bit - /// `id_syn_ele` (`0b100`), the 4-bit `element_instance_tag`, the - /// 1-bit `data_byte_align_flag`, the 8-bit `count`, the optional - /// 8-bit `esc_count` escape (when `payload_bytes >= 255`), - /// optionally byte-align (if the flag was set), then the - /// `payload_bytes` of `data_stream_byte[]`. - /// - /// Escape arithmetic: the parser's `cnt = count + esc_count` (see - /// [`Walker::read_data_count`]) inverts to `esc_count = - /// payload_bytes - 255`, so the largest representable payload is - /// `255 + 255 = 510` bytes. Larger data payloads must be split - /// across multiple DSE elements with the same `tag`. - /// - /// Returns [`Error::RawDataBlockEncodeInvalid`] when: - /// - /// * `element_instance_tag > 0x0f` (4-bit field overflow), or - /// * `payload.len() > 510` (the escape arithmetic ceiling above). - pub fn push_data( - &mut self, - element_instance_tag: u8, - byte_align_flag: bool, - payload: &[u8], - ) -> Result<()> { - if element_instance_tag > 0x0f { - return Err(Error::RawDataBlockEncodeInvalid); - } - let n = payload.len(); - if n > 510 { - return Err(Error::RawDataBlockEncodeInvalid); - } - self.writer.write_u32(IdSynEle::Dse as u32, 3); - self.writer.write_u32(element_instance_tag as u32, 4); - self.writer.write_bit(byte_align_flag); - if n < 255 { - self.writer.write_u32(n as u32, 8); - } else { - // §4.4.2.5: parser reconstructs `cnt = count + - // esc_count`. The inverse, given `cnt == n` and the - // escape trigger `count == 255`, is `esc_count = n - - // 255`. The 8-bit `esc_count` field caps `n` at - // `255 + 255 = 510`, which we already rejected above - // when violated. - self.writer.write_u32(255, 8); - let esc = (n as u32) - 255; - self.writer.write_u32(esc, 8); - } - if byte_align_flag { - self.writer.align_to_byte(); - } - for &b in payload { - self.writer.write_byte(b); - } - Ok(()) - } - - /// Emit a PCE element per ISO/IEC 14496-3 §4.4.1.1 / Table 4.2 - /// — the 3-bit `id_syn_ele` (`0b101`) followed by the full - /// `program_config_element()` body produced by [`Pce::write`]. - /// - /// The Table 4.2 Note 1 `byte_alignment()` call inside the PCE - /// body is *relative to the start of the PCE body* (i.e. the bit - /// position immediately after the 3-bit `id_syn_ele`). For the - /// standalone-in-`raw_data_block()` form the PCE-relative origin - /// is the parser's `origin_bit_offset = 0` (see [`Pce::parse`]) - /// — since [`Pce::write`] reproduces that exact arithmetic, this - /// helper simply passes `0` and the writer's own - /// `bit_position` becomes the alignment reference. Bit-exact - /// inverse of [`Walker::next_element`]'s - /// [`Element::ProgramConfig`] branch. - /// - /// Returns [`Error::PceEncodeInvalid`] propagated from - /// [`Pce::write`] when any wire field overflows its bit-width. - pub fn push_pce(&mut self, pce: &Pce) -> Result<()> { - self.writer.write_u32(IdSynEle::Pce as u32, 3); - // §4.4.1.1 Note 1: the Table 4.2 byte_alignment() is - // measured from the start of the PCE body, which is the - // current writer position *after* the id_syn_ele prefix. - // The Phase 1 standalone-in-raw_data_block parser hands - // origin_bit_offset = 0 to `Pce::parse`, which collapses to - // absolute byte alignment of the reader. The writer mirrors - // that exact collapse by passing 0 here — the alignment - // pad inside `Pce::write` will then align to the next - // absolute byte boundary of the underlying BitWriter. - pce.write(&mut self.writer, 0) - } - - /// Emit the terminating `END` element per ISO/IEC 14496-3 - /// §4.4.2.1 — the 3-bit `id_syn_ele` (`0b111`), then a pad-to- - /// byte-boundary that the [`Walker`] mirrors via - /// [`BitReader::align_to_byte`]. Consumes the assembler and - /// returns the finished byte buffer; the final byte is always - /// fully populated. - pub fn push_end(mut self) -> Vec { - self.writer.write_u32(IdSynEle::End as u32, 3); - self.writer.align_to_byte(); - self.writer.finish() - } -} diff --git a/crates/vendor/oxideav-aac/src/rvlc.rs b/crates/vendor/oxideav-aac/src/rvlc.rs deleted file mode 100644 index a881d5d1..00000000 --- a/crates/vendor/oxideav-aac/src/rvlc.rs +++ /dev/null @@ -1,407 +0,0 @@ -//! Reversible Variable Length Coding (RVLC) codebooks — ISO/IEC -//! 14496-3 §4.6.16.2 (error-resilient AAC scalefactor coding). -//! -//! RVLC is the error-resilient plug-in replacement for the §4.6.3 -//! noiseless coding of scalefactors. Instead of the Table 4.A.1 -//! Huffman codebook (codebook 12, indices `0..=120`, DPCM range -//! `-60..=+60`), the error-resilient `scale_factor_data()` branch -//! (Table 4.53) codes the scalefactor / intensity-position / -//! noise-energy DPCM deltas with the **RVLC codebook** (Table 4.166) -//! — a small *symmetric* (palindromic) prefix code covering only the -//! deltas `-7..=+7`. The value `±7` is the `ESC_FLAG`: it signals -//! that an escape magnitude (Huffman-coded with the separate RVLC-ESC -//! codebook, Table 4.168) is to be *added to +7* (positive ESC) or -//! *subtracted from -7* (negative ESC) to recover the true delta. -//! -//! Two properties make RVLC error-resilient, and both are exercised -//! by this module: -//! -//! 1. **Symmetry / reversibility.** Every Table 4.166 codeword is a -//! bit-palindrome, so the same codebook decodes a stream forwards -//! *and* backwards. The encoder transmits `rev_global_gain` (the -//! last scalefactor) and `length_of_rvlc_sf` (the bit length of -//! the RVLC part) so a decoder that hits a bit error mid-stream -//! can restart from the far end. This module provides the forward -//! primitive — the §4.6.2.3.2 note that "the decoding process of -//! the RVLC words is the same as for the Huffman codewords" -//! means a clean stream decodes identically forwards, so the -//! backward path is a recovery-only concern handled at the -//! `scale_factor_data` driver level. -//! 2. **Sparse code → error detection.** The 4-bit-deep code tree -//! has unused (asymmetric) leaves: Table 4.167 lists eight -//! *forbidden* codewords that a conforming encoder never emits. -//! Hitting one signals a bit error. [`rvlc_decode`] surfaces them -//! as [`Error::RvlcForbiddenCodeword`]. -//! -//! ## Provenance / cross-check -//! -//! The two codebooks are transcribed verbatim from the human-readable -//! ISO/IEC 14496-3:2009 normative tables: -//! -//! * [`RVLC_CB`] — Table 4.166 (`index`, `length`, `codeword`). -//! * [`RVLC_FORBIDDEN`] — Table 4.167 (asymmetric / forbidden -//! `length`, `codeword`). -//! * [`RVLC_ESC_CB`] — Table 4.168 (RVLC escape Huffman, 54 entries). -//! -//! Each was independently cross-validated against the packed -//! binary-tree node tables staged under -//! `docs/audio/aac/tables/rvlc-codewords-huff-tree.csv` and -//! `rvlc-escape-huff-tree.csv`: decoding the tree recovers exactly -//! the 15 Table 4.166 codewords (leaf − 7 == index) plus the 8 -//! Table 4.167 forbidden leaves, confirming both transcriptions. - -use oxideav_core::bits::BitReader; - -use crate::{Error, Result}; - -// ============================================================================= -// Table 4.166 — RVLC codebook -// ============================================================================= - -/// The `ESC_FLAG` magnitude. A decoded RVLC delta of `±7` does not -/// stand for the literal value `±7` when escapes are present; it -/// flags that an escape magnitude follows (§4.6.16.2.1). -pub const RVLC_ESC_FLAG: i8 = 7; - -/// Number of entries in the Table 4.166 RVLC codebook (deltas -/// `-7..=+7`, 15 entries). -pub const RVLC_CB_NUM_ENTRIES: usize = 15; - -/// Maximum Table 4.166 codeword length (9 bits — the `±6` codewords). -pub const RVLC_CB_MAX_LEN: u32 = 9; - -/// Table 4.166 — `(value, length_in_bits, codeword)` for the RVLC -/// codebook. `value` is the signed DPCM delta in `-7..=+7`; -/// `codeword` is right-aligned within the `u32` (MSB at bit -/// `length - 1`). Every codeword is a bit-palindrome (the symmetry -/// property §4.6.16.2.1 relies on). -const RVLC_CB: [(i8, u8, u32); RVLC_CB_NUM_ENTRIES] = [ - (-7, 7, 65), // 1000001 - (-6, 9, 257), // 100000001 - (-5, 8, 129), // 10000001 - (-4, 6, 33), // 100001 - (-3, 5, 17), // 10001 - (-2, 4, 9), // 1001 - (-1, 3, 5), // 101 - (0, 1, 0), // 0 - (1, 3, 7), // 111 - (2, 5, 27), // 11011 - (3, 6, 51), // 110011 - (4, 7, 107), // 1101011 - (5, 8, 195), // 11000011 - (6, 9, 427), // 110101011 - (7, 7, 99), // 1100011 -]; - -/// Table 4.167 — the eight *asymmetric* (forbidden) codewords as -/// `(length_in_bits, codeword)`. A conforming encoder never emits -/// these; a decode that lands on one signals a bit error -/// (§4.6.16.2.1 "some error detection is possible … because not all -/// nodes of the coding tree are used as codewords"). -const RVLC_FORBIDDEN: [(u8, u32); 8] = [ - (6, 50), // 110010 - (7, 96), // 1100000 - (9, 256), // 100000000 - (8, 194), // 11000010 - (7, 98), // 1100010 - (6, 52), // 110100 - (9, 426), // 110101010 - (8, 212), // 11010100 -]; - -/// Encode a signed RVLC delta in `-7..=+7` to its Table 4.166 -/// codeword. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u32` (MSB at bit `length - 1`). Out-of-range `value` -/// produces [`Error::RvlcEncodeInvalid`]. -/// -/// The inverse of [`rvlc_decode`]. -pub fn rvlc_encode(value: i8) -> Result<(u8, u32)> { - for &(v, len, cw) in &RVLC_CB { - if v == value { - return Ok((len, cw)); - } - } - Err(Error::RvlcEncodeInvalid) -} - -/// Decode one Table 4.166 RVLC codeword from `reader`, returning the -/// signed delta in `-7..=+7`. -/// -/// Read MSB-first one bit at a time and prefix-match against the -/// codebook. If the accumulated bit pattern matches one of the -/// Table 4.167 forbidden codewords, return -/// [`Error::RvlcForbiddenCodeword`] (an error-detection event, not a -/// reader fault). Returns [`Error::UnexpectedEnd`] on reader -/// underflow. -/// -/// A delta of `±7` is the `ESC_FLAG` (see [`RVLC_ESC_FLAG`]); the -/// caller decides whether an escape magnitude follows based on the -/// stream's `sf_escapes_present` flag. -pub fn rvlc_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=RVLC_CB_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for &(value, entry_len, entry_cw) in &RVLC_CB { - if u32::from(entry_len) == len && entry_cw == acc { - return Ok(value); - } - } - for &(f_len, f_cw) in &RVLC_FORBIDDEN { - if u32::from(f_len) == len && f_cw == acc { - return Err(Error::RvlcForbiddenCodeword); - } - } - } - // Every 9-bit prefix is either a valid codeword, a forbidden - // codeword, or a prefix of one of those; the RVLC tree is fully - // populated to depth 9, so a 9-bit walk always terminates in one - // of the two arms above. The guard keeps the return type `!`-free. - Err(Error::RvlcForbiddenCodeword) -} - -// ============================================================================= -// Table 4.168 — RVLC escape Huffman codebook -// ============================================================================= - -/// Number of entries in the Table 4.168 RVLC-ESC Huffman codebook -/// (54 entries, indices `0..=53`). -pub const RVLC_ESC_NUM_ENTRIES: usize = 54; - -/// Maximum Table 4.168 codeword length (20 bits). -pub const RVLC_ESC_MAX_LEN: u32 = 20; - -/// Table 4.168 — `(length_in_bits, codeword)` per escape index -/// `0..=53`. `codeword` is right-aligned in the `u32`. -/// -/// The escape *index* is the magnitude added to the `ESC_FLAG`: a -/// positive escape recovers `+7 + index`, a negative escape recovers -/// `-7 - index` (§4.6.16.2.1). Indices `0` and `1` (the two -/// shortest, 2-bit codewords) correspond to magnitudes 0 and 1. -const RVLC_ESC_CB: [(u8, u32); RVLC_ESC_NUM_ENTRIES] = [ - (2, 2), // 0 - (2, 0), // 1 - (3, 6), // 2 - (3, 2), // 3 - (4, 14), // 4 - (5, 31), // 5 - (5, 15), // 6 - (5, 13), // 7 - (6, 61), // 8 - (6, 29), // 9 - (6, 25), // 10 - (6, 24), // 11 - (7, 120), // 12 - (7, 56), // 13 - (8, 242), // 14 - (8, 114), // 15 - (9, 486), // 16 - (9, 230), // 17 - (10, 974), // 18 - (10, 463), // 19 - (11, 1950), // 20 - (11, 1951), // 21 - (11, 925), // 22 - (12, 1848), // 23 - (14, 7399), // 24 - (13, 3698), // 25 - (15, 14797), // 26 - (20, 473482), // 27 - (20, 473483), // 28 - (20, 473484), // 29 - (20, 473485), // 30 - (20, 473486), // 31 - (20, 473487), // 32 - (20, 473488), // 33 - (20, 473489), // 34 - (20, 473490), // 35 - (20, 473491), // 36 - (20, 473492), // 37 - (20, 473493), // 38 - (20, 473494), // 39 - (20, 473495), // 40 - (20, 473496), // 41 - (20, 473497), // 42 - (20, 473498), // 43 - (20, 473499), // 44 - (20, 473500), // 45 - (20, 473501), // 46 - (20, 473502), // 47 - (20, 473503), // 48 - (19, 236736), // 49 - (19, 236737), // 50 - (19, 236738), // 51 - (19, 236739), // 52 - (19, 236740), // 53 -]; - -/// Encode an RVLC escape magnitude (`0..=53`) to its Table 4.168 -/// Huffman codeword. -/// -/// Returns `(length_in_bits, codeword)` right-aligned in the `u32`. -/// An out-of-range magnitude produces [`Error::RvlcEncodeInvalid`]. -/// -/// The inverse of [`rvlc_esc_decode`]. -pub fn rvlc_esc_encode(magnitude: u8) -> Result<(u8, u32)> { - RVLC_ESC_CB - .get(magnitude as usize) - .copied() - .ok_or(Error::RvlcEncodeInvalid) -} - -/// Decode one Table 4.168 RVLC-ESC Huffman codeword from `reader`, -/// returning the escape magnitude index `0..=53`. -/// -/// Read MSB-first one bit at a time and prefix-match. Returns -/// [`Error::UnexpectedEnd`] on reader underflow and -/// [`Error::RvlcEscInvalid`] if the 20-bit walk matches no entry -/// (a bit error inside the escape part). -pub fn rvlc_esc_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=RVLC_ESC_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in RVLC_ESC_CB.iter().enumerate() { - if u32::from(entry_len) == len && entry_cw == acc { - return Ok(idx as u8); - } - } - } - Err(Error::RvlcEscInvalid) -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::BitWriter; - - fn encode_rvlc(value: i8) -> Vec { - let (len, cw) = rvlc_encode(value).unwrap(); - let mut w = BitWriter::new(); - w.write_u32(cw, u32::from(len)); - // Pad to a byte so the BitReader has whole bytes to read. - w.align_to_byte_zero(); - w.finish() - } - - #[test] - fn rvlc_codebook_roundtrips_every_value() { - for value in -7..=7 { - let bytes = encode_rvlc(value); - let mut r = BitReader::new(&bytes); - assert_eq!(rvlc_decode(&mut r).unwrap(), value, "value {value}"); - } - } - - #[test] - fn rvlc_codewords_are_palindromes() { - // The §4.6.16.2.1 symmetry property: every codeword reads the - // same forwards and backwards. This is what enables backward - // decoding of the RVLC part. - for &(value, len, cw) in &RVLC_CB { - let mut forward = 0u32; - for i in 0..len { - let bit = (cw >> i) & 1; - forward = (forward << 1) | bit; - } - assert_eq!(forward, cw, "value {value} codeword not a palindrome"); - } - } - - #[test] - fn rvlc_codebook_is_prefix_free() { - for &(_, li, ci) in &RVLC_CB { - for &(_, lj, cj) in &RVLC_CB { - if (li, ci) == (lj, cj) { - continue; - } - // ci is a prefix of cj iff the high `li` bits of cj - // (a `lj`-bit codeword) equal ci. - if li <= lj { - let shifted = cj >> (lj - li); - assert_ne!(shifted, ci, "({li},{ci}) is a prefix of ({lj},{cj})"); - } - } - } - } - - #[test] - fn forbidden_codewords_are_detected() { - for &(len, cw) in &RVLC_FORBIDDEN { - let mut w = BitWriter::new(); - w.write_u32(cw, u32::from(len)); - w.align_to_byte_zero(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!( - matches!(rvlc_decode(&mut r), Err(Error::RvlcForbiddenCodeword)), - "forbidden codeword ({len},{cw}) not detected" - ); - } - } - - #[test] - fn forbidden_codewords_disjoint_from_valid() { - for &(fl, fc) in &RVLC_FORBIDDEN { - for &(_, vl, vc) in &RVLC_CB { - assert!( - !(fl == vl && fc == vc), - "forbidden ({fl},{fc}) collides with a valid codeword" - ); - } - } - } - - #[test] - fn rvlc_esc_codebook_roundtrips_every_magnitude() { - for magnitude in 0u8..RVLC_ESC_NUM_ENTRIES as u8 { - let (len, cw) = rvlc_esc_encode(magnitude).unwrap(); - let mut w = BitWriter::new(); - w.write_u32(cw, u32::from(len)); - w.align_to_byte_zero(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert_eq!( - rvlc_esc_decode(&mut r).unwrap(), - magnitude, - "mag {magnitude}" - ); - } - } - - #[test] - fn rvlc_esc_codebook_is_prefix_free() { - for &(li, ci) in &RVLC_ESC_CB { - for &(lj, cj) in &RVLC_ESC_CB { - if (li, ci) == (lj, cj) { - continue; - } - if li <= lj { - let shifted = cj >> (lj - li); - assert_ne!(shifted, ci, "esc ({li},{ci}) is a prefix of ({lj},{cj})"); - } - } - } - } - - #[test] - fn rvlc_encode_rejects_out_of_range() { - assert!(matches!(rvlc_encode(8), Err(Error::RvlcEncodeInvalid))); - assert!(matches!(rvlc_encode(-8), Err(Error::RvlcEncodeInvalid))); - assert!(matches!( - rvlc_esc_encode(RVLC_ESC_NUM_ENTRIES as u8), - Err(Error::RvlcEncodeInvalid) - )); - } - - #[test] - fn esc_flag_is_seven() { - // Table 4.166 maps +7 and -7 to the shortest of the - // extreme magnitudes; the ESC_FLAG constant must agree. - assert_eq!(RVLC_ESC_FLAG, 7); - assert!(rvlc_encode(RVLC_ESC_FLAG).is_ok()); - assert!(rvlc_encode(-RVLC_ESC_FLAG).is_ok()); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_decoder.rs b/crates/vendor/oxideav-aac/src/sbr_decoder.rs deleted file mode 100644 index 3c6db4b1..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_decoder.rs +++ /dev/null @@ -1,1111 +0,0 @@ -//! SBR frame driver — ISO/IEC 14496-3 §4.6.18.5 "SBR tool overview". -//! -//! Composes the whole SBR back-end for one channel element (SCE or -//! CPE): the §4.6.18.4.1 analysis QMF of the core decoder output, the -//! `XLow` buffer with its `tHFGen = 8`-slot cross-frame history, the -//! §4.6.18.6 HF generator, the §4.6.18.7 envelope adjuster, the -//! §4.6.18.5 output matrix `X` assembly (the `lTemp` splice of the -//! previous frame's `Y'` against the current `XLow` / `Y`), and the -//! §4.6.18.4.2 64-band synthesis QMF producing `numTimeSlots·RATE·64 = -//! 2048` output samples per 1024-sample core frame (dual-rate SBR). -//! [`SbrDecoder::set_downsampled`] selects the §4.6.18.4.3 downsampled -//! output mode instead: the 32-channel synthesis bank keeps the output -//! at the core rate (1024 samples per frame), discarding the assembled -//! `X` subbands above the core Nyquist. -//! -//! [`SbrDecoder::process_frame`] drives a parsed -//! [`crate::sbr_extension::SbrExtensionData`]; -//! [`SbrDecoder::upsample_frame`] is the §4.6.18.5 "pure upsampling -//! without SBR processing" path used when a frame carries no SBR -//! payload, keeping the selected output rate and the QMF state -//! continuous. -//! -//! ## Provenance -//! -//! The buffer geometry (`tHFGen = 8`, `tHFAdj = 2`, `lf = -//! numTimeSlots·RATE = 32`), the `XLow` history splice, the `lTemp` -//! output splice, and the reset rules are from the §4.6.18.5 text and -//! Figure 4.47 of the staged spec. No part of this implementation is -//! derived from any external decoder. - -use crate::ps_decoder::PsDecoder; -use crate::ps_hybrid::LOOKAHEAD; -use crate::sbr_dequant::{dequant_coupled, dequant_single, DequantizedSbr}; -use crate::sbr_element::EXTENSION_ID_PS; -use crate::sbr_env_adjust::{adjust, EnvAdjustState, EnvParams}; -use crate::sbr_extension::SbrExtensionData; -use crate::sbr_freq_bands::{k0 as derive_k0, k2 as derive_k2, master_table, HiLoTables}; -use crate::sbr_header::SbrHeader; -use crate::sbr_hf_gen::{ - build_patches, chirp_factors, generate_hf, reflection_coefficient, Patches, T_HF_ADJ, T_HF_GEN, -}; -use crate::sbr_limiter::limiter_table; -use crate::sbr_lp::{aliasing_degree, deg_patched}; -use crate::sbr_qmf::{ - AnalysisQmf, Complex, DownsampledSynthesisQmf, RealAnalysisQmf, RealDownsampledSynthesisQmf, - RealSynthesisQmf, SynthesisQmf, -}; -use crate::sbr_reconstruct::{EnvelopeScalefactors, NoiseScalefactors}; -use crate::sbr_time_grid::derive_time_grid; -use crate::{Error, Result}; - -/// `numTimeSlots` for the 1024-sample core frame (§4.6.18.2.6). -pub const NUM_TIME_SLOTS: i32 = 16; - -/// `RATE = 2` (§4.6.18.2.5). -pub const RATE: i32 = 2; - -/// Slots per frame at the SBR rate (`lf = numTimeSlots · RATE`). -const LF: usize = (NUM_TIME_SLOTS * RATE) as usize; - -/// Total `XLow` / `XHigh` / `Y` columns (`lf + tHFGen`). -const COLS: usize = LF + T_HF_GEN; - -/// The synthesis filterbank of one output channel: the §4.6.18.4.2 -/// 64-band dual-rate bank, or the §4.6.18.4.3 32-channel downsampled -/// bank that keeps the output at the core rate (fed the first 32 -/// subbands of the assembled `X` matrix; the SBR content above the -/// core Nyquist is discarded by construction). -#[derive(Debug)] -enum SynthesisBank { - /// §4.6.18.4.2 — 64 output samples per slot (2× rate). - Dual(SynthesisQmf), - /// §4.6.18.4.3 — 32 output samples per slot (core rate). - Down(DownsampledSynthesisQmf), - /// §4.6.18.8.2.3 — the real-valued low-power dual-rate bank. - RealDual(RealSynthesisQmf), - /// §4.6.18.8.2.4 — the real-valued low-power core-rate bank. - RealDown(RealDownsampledSynthesisQmf), -} - -impl SynthesisBank { - fn new(downsampled: bool, low_power: bool) -> Self { - match (low_power, downsampled) { - (false, false) => SynthesisBank::Dual(SynthesisQmf::new()), - (false, true) => SynthesisBank::Down(DownsampledSynthesisQmf::new()), - (true, false) => SynthesisBank::RealDual(RealSynthesisQmf::new()), - (true, true) => SynthesisBank::RealDown(RealDownsampledSynthesisQmf::new()), - } - } - - /// Output samples per QMF slot (64 dual-rate, 32 downsampled). - fn samples_per_slot(&self) -> usize { - match self { - SynthesisBank::Dual(_) | SynthesisBank::RealDual(_) => 64, - SynthesisBank::Down(_) | SynthesisBank::RealDown(_) => 32, - } - } - - /// Synthesize one assembled `X` column, appending the slot's output - /// samples to `out`. The real (low-power) banks consume the real - /// parts — the LP signal path never populates the imaginary parts. - fn push_slot(&mut self, x: &[Complex; 64], out: &mut Vec) -> Result<()> { - match self { - SynthesisBank::Dual(s) => out.extend_from_slice(&s.push_slot(x)?), - SynthesisBank::Down(s) => out.extend_from_slice(&s.push_slot(&x[..32])?), - SynthesisBank::RealDual(s) => { - let mut re = [0.0f64; 64]; - for (r, c) in re.iter_mut().zip(x.iter()) { - *r = c.re; - } - out.extend_from_slice(&s.push_slot(&re)?); - } - SynthesisBank::RealDown(s) => { - let mut re = [0.0f64; 32]; - for (r, c) in re.iter_mut().zip(x.iter()) { - *r = c.re; - } - out.extend_from_slice(&s.push_slot(&re)?); - } - } - Ok(()) - } -} - -/// The analysis filterbank of one core channel: the §4.6.18.4.1 -/// complex bank, or the §4.6.18.8.2.2 real-valued low-power bank -/// (whose output rides the same `Complex` slots with zero imaginary -/// parts, so the HF generator and adjuster formulas apply unchanged). -#[derive(Debug)] -enum AnalysisBank { - Complex(AnalysisQmf), - Real(RealAnalysisQmf), -} - -impl AnalysisBank { - fn new(low_power: bool) -> Self { - if low_power { - AnalysisBank::Real(RealAnalysisQmf::new()) - } else { - AnalysisBank::Complex(AnalysisQmf::new()) - } - } - - fn push_slot(&mut self, samples: &[f64]) -> Result<[Complex; 32]> { - match self { - AnalysisBank::Complex(a) => a.push_slot(samples), - AnalysisBank::Real(a) => { - let w = a.push_slot(samples)?; - let mut out = [Complex::default(); 32]; - for (o, &r) in out.iter_mut().zip(w.iter()) { - o.re = r; - } - Ok(out) - } - } - } -} - -/// Per-channel cross-frame state. -#[derive(Debug)] -struct ChannelState { - analysis: AnalysisBank, - synthesis: SynthesisBank, - /// The previous frame's last `tHFGen` analysis slots (`W'`). - w_hist: Vec<[Complex; 32]>, - /// The previous frame's `Y` buffer (spec absolute columns). - y_prev: Vec<[Complex; 64]>, - /// `tE'(LE')` — the previous frame's trailing envelope border. - t_e_last_prev: i32, - /// The previous frame's `kx` / `M` (for the `lTemp` splice). - k_x_prev: i32, - m_prev: i32, - env_state: EnvAdjustState, - prev_invf: Vec, - prev_bw: Vec, - prev_env: Option, - prev_noise: Option, -} - -impl ChannelState { - fn new(downsampled: bool, low_power: bool) -> Self { - ChannelState { - analysis: AnalysisBank::new(low_power), - synthesis: SynthesisBank::new(downsampled, low_power), - w_hist: vec![[Complex::default(); 32]; T_HF_GEN], - y_prev: vec![[Complex::default(); 64]; COLS], - t_e_last_prev: NUM_TIME_SLOTS, - k_x_prev: 0, - m_prev: 0, - env_state: EnvAdjustState::new(), - prev_invf: Vec::new(), - prev_bw: Vec::new(), - prev_env: None, - prev_noise: None, - } - } - - /// Run the analysis QMF over one 1024-sample core frame and build - /// the `XLow` buffer: columns `0..tHFGen` are the previous frame's - /// trailing slots (`W'`), columns `tHFGen..` the current `W`. - fn analyze(&mut self, core: &[f64]) -> Result> { - if core.len() != 1024 { - return Err(Error::SbrQmfInvalid); - } - let mut x_low = Vec::with_capacity(COLS); - x_low.extend_from_slice(&self.w_hist); - for slot in 0..LF { - let w = self.analysis.push_slot(&core[slot * 32..(slot + 1) * 32])?; - x_low.push(w); - } - self.w_hist.clear(); - self.w_hist.extend_from_slice(&x_low[COLS - T_HF_GEN..]); - Ok(x_low) - } -} - -/// One SBR decoder per channel element (SCE: 1 channel, CPE: 2). -#[derive(Debug)] -pub struct SbrDecoder { - fs_sbr: u32, - header: Option, - bands: Option, - patches: Option, - f_table_lim: Vec, - /// §4.6.18.4.3 downsampled output mode: the synthesis runs the - /// 32-channel bank and every frame yields 1024 samples per channel - /// at the *core* rate instead of 2048 at `fs_sbr`. - downsampled: bool, - /// §4.6.18.8 low-power mode: real-valued filterbanks, ×2 energy - /// estimation, aliasing detection/reduction, modified sinusoid - /// injection. PS payloads are rejected ([`Error::SbrLowPowerPs`]). - low_power: bool, - /// `k0` of the active band setup (the first `fMaster` subband; - /// the §4.6.18.8.3 reflection coefficients cover `0 ≤ k < k0`). - k0: i32, - /// Set once the first frame is processed (mode switches are then - /// rejected — the QMF synthesis state is rate-specific). - started: bool, - channels: Vec, - /// Annex 8.A parametric stereo state, created when a - /// single-channel element first carries a PS extension. Holds the - /// PS decoder plus the second (right-channel) synthesis bank; the - /// channel's own bank renders the left channel. - ps: Option, -} - -/// PS decoder + right-channel synthesis bank (Annex 8.A). -#[derive(Debug)] -struct PsState { - dec: PsDecoder, - synthesis_r: SynthesisBank, -} - -impl SbrDecoder { - /// A fresh SBR decoder. `fs_sbr` is the SBR internal rate (twice - /// the core rate); `num_channels` is 1 (SCE) or 2 (CPE). - pub fn new(fs_sbr: u32, num_channels: usize) -> Result { - if num_channels == 0 || num_channels > 2 || fs_sbr == 0 { - return Err(Error::SbrFreqBandInvalid); - } - Ok(SbrDecoder { - fs_sbr, - header: None, - bands: None, - patches: None, - f_table_lim: Vec::new(), - downsampled: false, - low_power: false, - k0: 0, - started: false, - channels: (0..num_channels) - .map(|_| ChannelState::new(false, false)) - .collect(), - ps: None, - }) - } - - /// Select the §4.6.18.4.3 downsampled output mode: the SBR-processed - /// subband signals are synthesized through the 32-channel QMF bank, - /// so the output stays at the *core* coder rate (1024 samples per - /// channel per frame) instead of the dual `fs_sbr` rate. The SBR - /// range above the core Nyquist (assembled `X` subbands 32..64) is - /// discarded by construction; the reconstructed bands below it are - /// kept, so the mode is still an SBR decode, not a plain core decode. - /// - /// Must be selected before the first frame is processed — the QMF - /// synthesis history is rate-specific ([`Error::SbrQmfInvalid`] - /// otherwise). - pub fn set_downsampled(&mut self, downsampled: bool) -> Result<()> { - if self.started { - return Err(Error::SbrQmfInvalid); - } - if self.downsampled != downsampled { - self.downsampled = downsampled; - self.rebuild_banks(); - } - Ok(()) - } - - /// `true` ⇔ the §4.6.18.4.3 downsampled output mode is selected. - #[must_use] - pub fn is_downsampled(&self) -> bool { - self.downsampled - } - - /// Select the §4.6.18.8 low-power SBR mode: the whole signal path - /// runs on real-valued subband signals (the §4.6.18.8.2 real - /// filterbanks), the envelope adjuster applies the §4.6.18.8.4 - /// energy correction and §4.6.18.8.5 aliasing reduction / modified - /// sinusoid injection, and gain smoothing is disabled. Composable - /// with [`Self::set_downsampled`]. A PS payload on a low-power - /// decoder is rejected with [`Error::SbrLowPowerPs`] — the - /// subpart-8 tool needs the complex QMF domain. - /// - /// Must be selected before the first frame is processed - /// ([`Error::SbrQmfInvalid`] otherwise). - pub fn set_low_power(&mut self, low_power: bool) -> Result<()> { - if self.started { - return Err(Error::SbrQmfInvalid); - } - if self.low_power != low_power { - self.low_power = low_power; - self.rebuild_banks(); - } - Ok(()) - } - - /// `true` ⇔ the §4.6.18.8 low-power mode is selected. - #[must_use] - pub fn is_low_power(&self) -> bool { - self.low_power - } - - /// Re-instantiate every filterbank for the current mode pair - /// (only legal before the first frame). - fn rebuild_banks(&mut self) { - for ch in &mut self.channels { - ch.analysis = AnalysisBank::new(self.low_power); - ch.synthesis = SynthesisBank::new(self.downsampled, self.low_power); - } - if let Some(ps) = &mut self.ps { - ps.synthesis_r = SynthesisBank::new(self.downsampled, self.low_power); - } - } - - /// §4.6.18.5 pure upsampling: no SBR data for this frame — run the - /// analysis / synthesis pair with the high 32 bands zero, keeping - /// the output rate steady and the QMF state continuous. - /// - /// `core` holds one 1024-sample time signal per channel; returns - /// 2048 samples per channel (1024 in the §4.6.18.4.3 downsampled - /// mode). - pub fn upsample_frame(&mut self, core: &[&[f64]]) -> Result>> { - if core.len() != self.channels.len() { - return Err(Error::SbrQmfInvalid); - } - self.started = true; - let mut out = Vec::with_capacity(core.len()); - let n_ch = self.channels.len(); - for (ch, core_ch) in self.channels.iter_mut().zip(core.iter()) { - let x_low = ch.analyze(core_ch)?; - let mut x_cols: Vec<[Complex; 64]> = Vec::with_capacity(LF); - for l in 0..LF { - let mut x = [Complex::default(); 64]; - x[..32].copy_from_slice(&x_low[l + T_HF_ADJ]); - x_cols.push(x); - } - let sps = ch.synthesis.samples_per_slot(); - // A PS-active stream holds its stereo parameters over a - // frame without SBR/PS payload (Annex 8.A.3); the whole - // 32-band spectrum counts as SBR-covered for the partial - // reset. - let mut emitted = false; - if n_ch == 1 { - if let Some(ps) = self.ps.as_mut() { - let x_input = build_x_input(&x_cols, &x_low); - if let Some((lq, rq)) = ps.dec.process(None, &x_input, 32)? { - let mut pcm_l = Vec::with_capacity(LF * sps); - let mut pcm_r = Vec::with_capacity(LF * sps); - for l in 0..LF { - ch.synthesis.push_slot(&lq[l], &mut pcm_l)?; - ps.synthesis_r.push_slot(&rq[l], &mut pcm_r)?; - } - out.push(pcm_l); - out.push(pcm_r); - emitted = true; - } - } - } - if !emitted { - let mut pcm = Vec::with_capacity(LF * sps); - for x in &x_cols { - ch.synthesis.push_slot(x, &mut pcm)?; - } - out.push(pcm); - } - // No Y for this frame; the next frame's lTemp splice sees - // an empty previous envelope span. - ch.y_prev - .iter_mut() - .for_each(|c| *c = [Complex::default(); 64]); - ch.t_e_last_prev = NUM_TIME_SLOTS; - } - Ok(out) - } - - /// Decode one SBR frame: `ext` is the parsed `sbr_extension_data()` - /// for this element, `core` one 1024-sample signal per channel. - /// Returns 2048 samples per channel at the SBR rate (1024 per - /// channel at the core rate in the §4.6.18.4.3 downsampled mode). - pub fn process_frame( - &mut self, - ext: &SbrExtensionData, - core: &[&[f64]], - ) -> Result>> { - let n_ch = self.channels.len(); - if core.len() != n_ch || ext.element.channels.len() != n_ch { - return Err(Error::SbrFreqBandInvalid); - } - self.started = true; - - // §4.6.18.3.3 reset: first header, or a transmitted header that - // changes the band geometry. - let reset = match &self.header { - None => true, - Some(prev) => prev.band_geometry_changed(&ext.header), - }; - if reset { - let k0v = derive_k0(self.fs_sbr, ext.header.start_freq)?; - let k2v = derive_k2(self.fs_sbr, ext.header.stop_freq, k0v)?; - let f_master = master_table(k0v, k2v, ext.header.freq_scale, ext.header.alter_scale)?; - let bands = - HiLoTables::derive(&f_master, ext.header.xover_band, ext.header.noise_bands)?; - let patches = build_patches(&f_master, k0v, bands.k_x, bands.m, self.fs_sbr)?; - self.f_table_lim = limiter_table( - &bands, - &patches.borders(bands.k_x), - ext.header.limiter_bands, - )?; - self.bands = Some(bands); - self.patches = Some(patches); - self.k0 = k0v; - for ch in &mut self.channels { - ch.prev_invf.clear(); - ch.prev_bw.clear(); - ch.prev_env = None; - ch.prev_noise = None; - } - } - self.header = Some(ext.header); - let bands = self.bands.as_ref().ok_or(Error::SbrFreqBandInvalid)?; - let patches = self.patches.as_ref().ok_or(Error::SbrFreqBandInvalid)?; - - let coupling = ext.element.coupling; - - // Reconstruct the quantized scalefactors per transmitted - // channel, then dequantize (jointly for a coupled pair). - let mut recon: Vec<(EnvelopeScalefactors, NoiseScalefactors)> = Vec::with_capacity(n_ch); - for (c, sbr_ch) in ext.element.channels.iter().enumerate() { - let st = &self.channels[c]; - let env = EnvelopeScalefactors::reconstruct( - &sbr_ch.envelope, - &sbr_ch.grid, - &sbr_ch.dtdf, - bands, - coupling, - c == 1, - if reset { None } else { st.prev_env.as_ref() }, - )?; - let noise = NoiseScalefactors::reconstruct( - &sbr_ch.noise, - &sbr_ch.grid, - &sbr_ch.dtdf, - bands.n_q(), - coupling, - c == 1, - if reset { None } else { st.prev_noise.as_ref() }, - )?; - recon.push((env, noise)); - } - - let dequant: Vec = if coupling && n_ch == 2 { - let amp_res = effective_amp_res(&ext.header, &ext.element.channels[0].grid); - let (l, r) = - dequant_coupled(&recon[0].0, &recon[0].1, &recon[1].0, &recon[1].1, amp_res); - vec![l, r] - } else { - (0..n_ch) - .map(|c| { - let amp_res = effective_amp_res(&ext.header, &ext.element.channels[c].grid); - dequant_single(&recon[c].0, &recon[c].1, amp_res) - }) - .collect() - }; - - let mut out = Vec::with_capacity(n_ch); - for c in 0..n_ch { - let sbr_ch = &ext.element.channels[c]; - let grid = derive_time_grid(&sbr_ch.grid, NUM_TIME_SLOTS)?; - - // Coupling: the second channel transmits no sbr_invf() - // (Table 4.66) — it shares the first channel's - // inverse-filtering modes. - let invf_modes = if coupling && c == 1 { - &ext.element.channels[0].invf.invf_mode - } else { - &sbr_ch.invf.invf_mode - }; - - let ch = &mut self.channels[c]; - - // Chirp factors (per noise band). - let bw = chirp_factors(invf_modes, &ch.prev_invf, &ch.prev_bw); - - // Analysis + XLow (with tHFGen history). - let x_low = ch.analyze(core[c])?; - - // HF generation over the envelope span. - let l_range = (RATE * grid.t_e[0])..(RATE * grid.t_e[grid.t_e.len() - 1]); - let x_high = generate_hf(&x_low, patches, &bw, bands, l_range, LF)?; - - // §4.6.18.8.3 aliasing detection (low power): reflection - // coefficients over the low band, the Figure 4.53 degree - // walk, and the patch carry onto the SBR range. - let dp = if self.low_power { - let k0_cnt = usize::try_from(self.k0).map_err(|_| Error::SbrFreqBandInvalid)?; - let mut refl = Vec::with_capacity(k0_cnt); - for k in 0..k0_cnt.min(32) { - refl.push(reflection_coefficient(&x_low, k, LF)?); - } - let deg = aliasing_degree(&refl); - Some(deg_patched(°, patches, bands.k_x, bands.m)?) - } else { - None - }; - - // Envelope adjustment. - let freq_res: Vec = sbr_ch.grid.freq_res.clone(); - let params = EnvParams { - bands, - f_table_lim: &self.f_table_lim, - t_e: &grid.t_e, - t_q: &grid.t_q, - freq_res: &freq_res, - l_a: grid.l_a, - e_orig: &dequant[c].e_orig, - q_orig: &dequant[c].q_orig, - add_harmonic: &sbr_ch.add_harmonic, - interpol_freq: ext.header.interpol_freq, - smoothing_mode: ext.header.smoothing_mode, - limiter_gains: ext.header.limiter_gains, - reset, - low_power: self.low_power, - deg_patched: dp.as_deref(), - }; - let y = adjust(&x_high, ¶ms, &mut ch.env_state)?; - - // §4.6.18.5 X assembly. - let l_temp = (RATE * ch.t_e_last_prev - NUM_TIME_SLOTS * RATE).max(0) as usize; - let mut x_cols: Vec<[Complex; 64]> = Vec::with_capacity(LF); - for l in 0..LF { - let mut x = [Complex::default(); 64]; - let (kx_cur, m_cur, y_col) = if l < l_temp { - (ch.k_x_prev, ch.m_prev, &ch.y_prev[l + T_HF_ADJ + LF]) - } else { - (bands.k_x, bands.m, &y[l + T_HF_ADJ]) - }; - let kx_u = kx_cur.max(0) as usize; - for (k, cell) in x.iter_mut().enumerate().take(kx_u.min(32)) { - *cell = x_low[l + T_HF_ADJ][k]; - } - let hi = (kx_cur + m_cur).max(0) as usize; - // §4.6.18.8.5: the low-power sinusoid spill extends the - // Y range one subband above the SBR range (≤ 63)… - let hi = if self.low_power { - (hi + 1).min(64) - } else { - hi.min(64) - }; - if kx_u < hi { - x[kx_u..hi].copy_from_slice(&y_col[kx_u..hi]); - } - // …and adds Y(kx − 1) onto the lowband subband rather - // than replacing it. - if self.low_power && (1..=32).contains(&kx_u) { - x[kx_u - 1] += y_col[kx_u - 1]; - } - x_cols.push(x); - } - - // Annex 8.A: a single-channel element carrying an - // EXTENSION_ID_PS payload renders stereo through the PS - // tool (the element's own bank = left, the PS state's = - // right). Until the first decodable ps_data() the mono - // path below stays in effect. - let ps_payload = if n_ch == 1 { - ext.element - .extension - .as_ref() - .filter(|e| e.id == EXTENSION_ID_PS) - .map(|e| e.data.as_slice()) - } else { - None - }; - if ps_payload.is_some() && self.low_power { - // §4.6.18.8: the real-valued tool cannot host the - // complex-domain PS processing. - return Err(Error::SbrLowPowerPs); - } - if ps_payload.is_some() && self.ps.is_none() { - self.ps = Some(PsState { - dec: PsDecoder::new(), - synthesis_r: SynthesisBank::new(self.downsampled, self.low_power), - }); - } - let sps = ch.synthesis.samples_per_slot(); - let mut emitted = false; - if n_ch == 1 { - if let Some(ps) = self.ps.as_mut() { - let x_input = build_x_input(&x_cols, &x_low); - let kx_plus_m = (bands.k_x + bands.m).max(0) as usize; - if let Some((lq, rq)) = ps.dec.process(ps_payload, &x_input, kx_plus_m)? { - let mut pcm_l = Vec::with_capacity(LF * sps); - let mut pcm_r = Vec::with_capacity(LF * sps); - for l in 0..LF { - ch.synthesis.push_slot(&lq[l], &mut pcm_l)?; - ps.synthesis_r.push_slot(&rq[l], &mut pcm_r)?; - } - out.push(pcm_l); - out.push(pcm_r); - emitted = true; - } - } - } - if !emitted { - let mut pcm = Vec::with_capacity(LF * sps); - for x in &x_cols { - ch.synthesis.push_slot(x, &mut pcm)?; - } - out.push(pcm); - } - - // Thread cross-frame state. - ch.y_prev = y; - ch.t_e_last_prev = grid.t_e[grid.t_e.len() - 1]; - ch.k_x_prev = bands.k_x; - ch.m_prev = bands.m; - ch.prev_invf = invf_modes.clone(); - ch.prev_bw = bw; - let (env, noise) = recon[c].clone(); - ch.prev_env = Some(env); - ch.prev_noise = Some(noise); - } - Ok(out) - } -} - -/// Assemble the Annex 8.A.3 `Xinput` matrix: the 32 assembled `X` -/// columns followed by `LOOKAHEAD` slots taken from `XLow` beyond the -/// frame (`XLow(k, l + tHFAdj)`, `k < 5` — the split bands the hybrid -/// filterbank consumes ahead of time). -fn build_x_input(x_cols: &[[Complex; 64]], x_low: &[[Complex; 32]]) -> Vec<[Complex; 64]> { - let mut v = Vec::with_capacity(LF + LOOKAHEAD); - v.extend_from_slice(x_cols); - for l in LF..LF + LOOKAHEAD { - let mut col = [Complex::default(); 64]; - col[..5].copy_from_slice(&x_low[l + T_HF_ADJ][..5]); - v.push(col); - } - v -} - -/// The effective `bs_amp_res` after the single-envelope FIXFIX -/// override (§4.4.2.8 Table 4.69 Note). -fn effective_amp_res(header: &SbrHeader, grid: &crate::sbr_grid::SbrGrid) -> bool { - if grid.amp_res_override { - false - } else { - header.amp_res - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sbr_element::{SbrChannel, SbrElement}; - use crate::sbr_envelope::{SbrEnvelopeData, SbrNoiseData}; - use crate::sbr_grid::{FrameClass, SbrDtdf, SbrGrid, SbrInvf}; - - fn sine(freq: f64, n: usize, offset: usize) -> Vec { - (0..n) - .map(|t| (2.0 * core::f64::consts::PI * freq * (t + offset) as f64).sin()) - .collect() - } - - /// Pure upsampling reproduces a 2×-upsampled, delayed sine across - /// frame boundaries. - #[test] - fn upsample_frames_are_continuous() { - let mut dec = SbrDecoder::new(44_100, 1).unwrap(); - let freq = 0.02; - let mut out = Vec::new(); - for f in 0..4 { - let core = sine(freq, 1024, f * 1024); - let o = dec.upsample_frame(&[&core]).unwrap(); - assert_eq!(o[0].len(), 2048); - out.extend_from_slice(&o[0]); - } - // Steady-state fit against the ideal upsampled sine. - let ideal = |t: f64, d: f64| (2.0 * core::f64::consts::PI * freq * (t - d) / 2.0).sin(); - let mut best = f64::INFINITY; - for delay in 0..1500usize { - let mut err = 0.0; - let mut sig = 0.0; - for (t, &o) in out.iter().enumerate().skip(2500) { - let e = o - ideal(t as f64, delay as f64); - err += e * e; - sig += o * o; - } - best = best.min(err / sig.max(1e-30)); - } - assert!(best < 1e-4, "upsample error ratio {best}"); - } - - /// Build a minimal single-channel SBR extension: one FIXFIX - /// envelope, frequency-direction start values, flat noise floor. - fn synthetic_ext(fs_sbr: u32, env_start: i32, noise_q: i32) -> SbrExtensionData { - let header = SbrHeader { - amp_res: true, - start_freq: 5, - stop_freq: 3, - xover_band: 0, - reserved: 0, - header_extra_1: false, - header_extra_2: false, - freq_scale: 2, - alter_scale: true, - noise_bands: 2, - limiter_bands: 2, - limiter_gains: 2, - interpol_freq: true, - smoothing_mode: true, - }; - let bands = header.derive_bands(fs_sbr).unwrap(); - let n_high = bands.n_high(); - let n_q = bands.n_q(); - let grid = SbrGrid { - frame_class: FrameClass::FixFix, - num_env: 1, - num_noise: 1, - freq_res: vec![true], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: true, - }; - let dtdf = SbrDtdf { - df_env: vec![false], - df_noise: vec![false], - }; - let invf = SbrInvf { - invf_mode: vec![0; n_q], - }; - let mut env_row = vec![0i32; n_high]; - env_row[0] = env_start; - let envelope = SbrEnvelopeData { - data: vec![env_row], - }; - let noise = SbrNoiseData { - data: vec![{ - let mut r = vec![0i32; n_q]; - r[0] = noise_q; - r - }], - }; - SbrExtensionData { - crc: None, - crc_region: None, - header_present: true, - header, - element: SbrElement { - coupling: false, - channels: vec![SbrChannel { - grid, - dtdf, - invf, - envelope, - noise, - add_harmonic: vec![], - }], - extension: None, - }, - num_sbr_bits: 0, - } - } - - /// A full synthetic SBR frame produces finite 2048-sample output - /// with energy in the SBR band, and threads state across frames - /// (header reuse, no reset). - #[test] - fn synthetic_sbr_frame_produces_high_band() { - let fs_sbr = 44_100; - let ext = synthetic_ext(fs_sbr, 10, 6); - let mut dec = SbrDecoder::new(fs_sbr, 1).unwrap(); - // A mid-band core tone so the patch sources carry signal. - let freq = 0.11; - let mut all = Vec::new(); - for f in 0..3 { - let core = sine(freq, 1024, f * 1024); - let out = dec.process_frame(&ext, &[&core]).unwrap(); - assert_eq!(out.len(), 1); - assert_eq!(out[0].len(), 2048); - assert!(out[0].iter().all(|v| v.is_finite())); - all.extend_from_slice(&out[0]); - } - // The output must carry energy (base band at least). - let energy: f64 = all.iter().map(|v| v * v).sum(); - assert!(energy > 1.0, "energy {energy}"); - // Deterministic: a second decoder over the same input matches - // bit-exactly. - let mut dec2 = SbrDecoder::new(fs_sbr, 1).unwrap(); - let mut all2 = Vec::new(); - for f in 0..3 { - let core = sine(freq, 1024, f * 1024); - all2.extend_from_slice(&dec2.process_frame(&ext, &[&core]).unwrap()[0]); - } - assert_eq!(all, all2); - } - - /// The high band actually receives patched content: with a strong - /// envelope target the spectrum above kx·(fs/128) is non-silent, - /// and it scales with the envelope scalefactor. - #[test] - fn envelope_scalefactor_controls_high_band_level() { - let fs_sbr = 44_100; - let mut quiet = SbrDecoder::new(fs_sbr, 1).unwrap(); - let mut loud = SbrDecoder::new(fs_sbr, 1).unwrap(); - let ext_quiet = synthetic_ext(fs_sbr, 2, 10); - let ext_loud = synthetic_ext(fs_sbr, 12, 10); - let freq = 0.09; - let mut hi_q = 0.0f64; - let mut hi_l = 0.0f64; - for f in 0..3 { - let core = sine(freq, 1024, f * 1024); - let oq = quiet.process_frame(&ext_quiet, &[&core]).unwrap(); - let ol = loud.process_frame(&ext_loud, &[&core]).unwrap(); - if f > 0 { - // High-pass both outputs with a crude difference filter - // to weight the HF region, then compare energies. - for w in oq[0].windows(2) { - hi_q += (w[1] - w[0]) * (w[1] - w[0]); - } - for w in ol[0].windows(2) { - hi_l += (w[1] - w[0]) * (w[1] - w[0]); - } - } - } - assert!(hi_l > hi_q * 4.0, "loud {hi_l} vs quiet {hi_q}"); - } - - /// Downsampled pure upsampling is the identity at the core rate - /// (up to the analysis+synthesis delay), and each frame yields - /// 1024 samples. - #[test] - fn downsampled_upsample_is_identity_at_core_rate() { - let mut dec = SbrDecoder::new(44_100, 1).unwrap(); - dec.set_downsampled(true).unwrap(); - assert!(dec.is_downsampled()); - let freq = 0.02; - let mut input_all = Vec::new(); - let mut out = Vec::new(); - for f in 0..4 { - let core = sine(freq, 1024, f * 1024); - input_all.extend_from_slice(&core); - let o = dec.upsample_frame(&[&core]).unwrap(); - assert_eq!(o[0].len(), 1024); - out.extend_from_slice(&o[0]); - } - // Mode switches after the first frame are rejected. - assert!(dec.set_downsampled(false).is_err()); - let mut best = (f64::INFINITY, 0usize); - for delay in 0..1024usize { - let mut err = 0.0; - let mut sig = 0.0; - for t in 1500..out.len() { - if t < delay { - continue; - } - let e = out[t] - input_all[t - delay]; - err += e * e; - sig += out[t] * out[t]; - } - let ratio = err / sig.max(1e-30); - if ratio < best.0 { - best = (ratio, delay); - } - } - assert!( - best.0 < 1e-4, - "identity error ratio {} at {}", - best.0, - best.1 - ); - } - - /// With the whole SBR range inside the first 32 QMF bands, the - /// dual-rate output is band-limited below the core Nyquist, so the - /// downsampled decode must match a straight 2:1 decimation of the - /// dual-rate decode (same synthetic SBR frames, delay-searched). - #[test] - fn downsampled_matches_decimated_dual_rate() { - // The synthetic header at 44.1 kHz derives kx 14, M 15 — - // kx + M = 29 ≤ 32, so no SBR content crosses the core Nyquist. - let fs_sbr = 44_100; - let ext = synthetic_ext(fs_sbr, 8, 6); - let bands = ext.header.derive_bands(fs_sbr).unwrap(); - assert!( - bands.k_x + bands.m <= 32, - "test premise: SBR range within 32 bands (kx {} M {})", - bands.k_x, - bands.m - ); - let mut dual = SbrDecoder::new(fs_sbr, 1).unwrap(); - let mut down = SbrDecoder::new(fs_sbr, 1).unwrap(); - down.set_downsampled(true).unwrap(); - let freq = 0.055; - let mut out_dual = Vec::new(); - let mut out_down = Vec::new(); - for f in 0..6 { - let core = sine(freq, 1024, f * 1024); - out_dual.extend_from_slice(&dual.process_frame(&ext, &[&core]).unwrap()[0]); - let o = down.process_frame(&ext, &[&core]).unwrap(); - assert_eq!(o[0].len(), 1024); - out_down.extend_from_slice(&o[0]); - } - // out_down[n] ≈ out_dual[2n − d] for some fixed integer d - // (either parity): search d, then gate the steady-state error. - let mut best = (f64::INFINITY, 0usize); - for d in 0..1400usize { - let mut err = 0.0; - let mut sig = 0.0; - for (n, &od) in out_down.iter().enumerate().skip(1200) { - let idx = 2 * n; - if idx < d || idx - d >= out_dual.len() { - continue; - } - let e = od - out_dual[idx - d]; - err += e * e; - sig += od * od; - } - let ratio = err / sig.max(1e-30); - if ratio < best.0 { - best = (ratio, d); - } - } - assert!( - best.0 < 1e-3, - "decimation mismatch ratio {} at delay {}", - best.0, - best.1 - ); - } - - /// The §4.6.18.8 low-power mode reconstructs the same synthetic - /// SBR frame as the high-quality mode to a moderate tolerance - /// (the LP tool is a real-valued approximation), stays finite and - /// deterministic, and composes with the downsampled output. - #[test] - fn low_power_tracks_high_quality() { - let fs_sbr = 44_100; - let ext = synthetic_ext(fs_sbr, 8, 6); - let mut hq = SbrDecoder::new(fs_sbr, 1).unwrap(); - let mut lp = SbrDecoder::new(fs_sbr, 1).unwrap(); - lp.set_low_power(true).unwrap(); - assert!(lp.is_low_power()); - let freq = 0.055; - let mut out_hq = Vec::new(); - let mut out_lp = Vec::new(); - for f in 0..6 { - let core = sine(freq, 1024, f * 1024); - out_hq.extend_from_slice(&hq.process_frame(&ext, &[&core]).unwrap()[0]); - let o = lp.process_frame(&ext, &[&core]).unwrap(); - assert_eq!(o[0].len(), 2048); - assert!(o[0].iter().all(|v| v.is_finite())); - out_lp.extend_from_slice(&o[0]); - } - assert!(lp.set_low_power(false).is_err(), "mode locked after start"); - // Energy tracks the HQ reconstruction (the two banks share the - // prototype and delay). - let e_hq: f64 = out_hq.iter().skip(4096).map(|v| v * v).sum(); - let e_lp: f64 = out_lp.iter().skip(4096).map(|v| v * v).sum(); - assert!( - e_lp > 0.5 * e_hq && e_lp < 2.0 * e_hq, - "LP {e_lp} vs HQ {e_hq}" - ); - // The real-valued HF processing does not reproduce the complex - // path's subband phases, so the comparison is energy-domain: - // the core tone's amplitude (quadrature probe at the upsampled - // frequency) must match tightly, and the per-block energy - // envelope must track. - let probe = |x: &[f64]| -> f64 { - let w = 2.0 * core::f64::consts::PI * freq / 2.0; - let (mut cs, mut sn) = (0.0f64, 0.0f64); - let n0 = 4096; - for (t, &v) in x.iter().enumerate().skip(n0) { - cs += v * (w * t as f64).cos(); - sn += v * (w * t as f64).sin(); - } - let n = (x.len() - n0) as f64; - 2.0 / n * (cs * cs + sn * sn).sqrt() - }; - let (a_hq, a_lp) = (probe(&out_hq), probe(&out_lp)); - assert!( - (a_lp - a_hq).abs() < 0.05 * a_hq, - "core tone amplitude LP {a_lp} vs HQ {a_hq}" - ); - for (block_hq, block_lp) in out_hq - .chunks_exact(1024) - .zip(out_lp.chunks_exact(1024)) - .skip(4) - { - let e_h: f64 = block_hq.iter().map(|v| v * v).sum(); - let e_l: f64 = block_lp.iter().map(|v| v * v).sum(); - assert!( - e_l > 0.4 * e_h && e_l < 2.5 * e_h, - "block energy LP {e_l} vs HQ {e_h}" - ); - } - - // Determinism. - let mut lp2 = SbrDecoder::new(fs_sbr, 1).unwrap(); - lp2.set_low_power(true).unwrap(); - let mut out_lp2 = Vec::new(); - for f in 0..6 { - let core = sine(freq, 1024, f * 1024); - out_lp2.extend_from_slice(&lp2.process_frame(&ext, &[&core]).unwrap()[0]); - } - assert_eq!(out_lp, out_lp2); - - // LP + downsampled: 1024 samples per frame, finite. - let mut lpd = SbrDecoder::new(fs_sbr, 1).unwrap(); - lpd.set_low_power(true).unwrap(); - lpd.set_downsampled(true).unwrap(); - let core = sine(freq, 1024, 0); - let o = lpd.process_frame(&ext, &[&core]).unwrap(); - assert_eq!(o[0].len(), 1024); - assert!(o[0].iter().all(|v| v.is_finite())); - } - - /// Low-power pure upsampling is still the identity at 2× rate - /// (real analysis + real synthesis pair). - #[test] - fn low_power_upsample_is_identity() { - let mut dec = SbrDecoder::new(44_100, 1).unwrap(); - dec.set_low_power(true).unwrap(); - let freq = 0.02; - let mut out = Vec::new(); - for f in 0..4 { - let core = sine(freq, 1024, f * 1024); - let o = dec.upsample_frame(&[&core]).unwrap(); - assert_eq!(o[0].len(), 2048); - out.extend_from_slice(&o[0]); - } - let ideal = |t: f64, d: f64| (2.0 * core::f64::consts::PI * freq * (t - d) / 2.0).sin(); - let mut best = f64::INFINITY; - for delay in 0..1500usize { - let mut err = 0.0; - let mut sig = 0.0; - for (t, &o) in out.iter().enumerate().skip(2500) { - let e = o - ideal(t as f64, delay as f64); - err += e * e; - sig += o * o; - } - best = best.min(err / sig.max(1e-30)); - } - assert!(best < 1e-4, "LP upsample error ratio {best}"); - } - - /// A PS payload on a low-power decoder is rejected — the - /// subpart-8 tool needs the complex QMF domain. - #[test] - fn low_power_rejects_ps() { - use crate::sbr_element::SbrExtension; - let fs_sbr = 44_100; - let mut ext = synthetic_ext(fs_sbr, 8, 6); - ext.element.extension = Some(SbrExtension { - id: EXTENSION_ID_PS, - data: vec![0u8; 4], - }); - let mut lp = SbrDecoder::new(fs_sbr, 1).unwrap(); - lp.set_low_power(true).unwrap(); - let core = sine(0.05, 1024, 0); - assert!(matches!( - lp.process_frame(&ext, &[&core]), - Err(Error::SbrLowPowerPs) - )); - } - - /// A channel-count / buffer-length mismatch is rejected. - #[test] - fn shape_mismatches_rejected() { - let mut dec = SbrDecoder::new(44_100, 1).unwrap(); - let core = vec![0.0; 512]; - assert!(dec.upsample_frame(&[&core]).is_err()); - let ext = synthetic_ext(44_100, 0, 6); - let short = vec![0.0; 1024]; - assert!(dec.process_frame(&ext, &[&short[..], &short[..]]).is_err()); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_dequant.rs b/crates/vendor/oxideav-aac/src/sbr_dequant.rs deleted file mode 100644 index 88b559b1..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_dequant.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! SBR envelope / noise-floor dequantization — ISO/IEC 14496-3 -//! §4.6.18.3.5 "Dequantization and stereo decoding". -//! -//! Converts the reconstructed *quantized* scalefactors -//! ([`crate::sbr_reconstruct`]'s `E_Q(k,l)` / `Q(k,l)`) into the linear -//! energy values `EOrig(k,l)` / `QOrig(k,l)` the envelope adjuster -//! (§4.6.18.7) consumes: -//! -//! * Single channel (or an uncoupled pair, `bs_coupling == 0`): -//! `EOrig = 64 · 2^(E/a)` with `a = 2` for `bs_amp_res = 0` (1.5 dB -//! steps) and `a = 1` for `bs_amp_res = 1` (3.0 dB steps); -//! `QOrig = 2^(NOISE_FLOOR_OFFSET − Q)` with -//! `NOISE_FLOOR_OFFSET = 6` (§4.6.18.2.5). -//! * Coupled pair (`bs_coupling == 1`): channel 0 carries the -//! level average and channel 1 the pan ratio; -//! `panOffset = [24, 12]` (§4.6.18.2.6) recentres the ratio. The -//! left / right split divides the doubled average -//! `64·2^(E0/a + 1)` by `1 + 2^(±(panOffset − E1)/a)` (and the -//! noise analogue with `panOffset(1) = 12`), which preserves -//! `ELeft + ERight = 2 · (64·2^(E0/a))`. -//! -//! ## Provenance -//! -//! Every formula and constant is from the §4.6.18.3.5 text and the -//! §4.6.18.2.5 / §4.6.18.2.6 constant lists of the staged spec. No part -//! of this implementation is derived from any external decoder. - -use crate::sbr_reconstruct::{EnvelopeScalefactors, NoiseScalefactors}; - -/// `NOISE_FLOOR_OFFSET = 6` (§4.6.18.2.5). -pub const NOISE_FLOOR_OFFSET: f64 = 6.0; - -/// `panOffset = [24, 12]` indexed by `bs_amp_res` (§4.6.18.2.6). -#[inline] -#[must_use] -pub fn pan_offset(amp_res: bool) -> f64 { - if amp_res { - 12.0 - } else { - 24.0 - } -} - -/// The §4.6.18.3.5 amplitude-resolution divisor `a`: `2` for -/// `bs_amp_res = 0` (1.5 dB), `1` for `bs_amp_res = 1` (3.0 dB). -#[inline] -#[must_use] -pub fn amp_divisor(amp_res: bool) -> f64 { - if amp_res { - 1.0 - } else { - 2.0 - } -} - -/// Dequantized (linear-energy) envelope and noise-floor scalefactors -/// for one channel. -#[derive(Debug, Clone, PartialEq)] -pub struct DequantizedSbr { - /// `EOrig[l][k]` — linear envelope energies, one band vector per - /// envelope (band count follows the envelope's frequency - /// resolution). - pub e_orig: Vec>, - /// `QOrig[l][k]` — linear noise-floor energies, one `NQ`-band - /// vector per noise floor. - pub q_orig: Vec>, -} - -/// §4.6.18.3.5 single-channel dequantization: -/// `EOrig = 64·2^(E/a)`, `QOrig = 2^(NOISE_FLOOR_OFFSET − Q)`. -#[must_use] -pub fn dequant_single( - env: &EnvelopeScalefactors, - noise: &NoiseScalefactors, - amp_res: bool, -) -> DequantizedSbr { - let a = amp_divisor(amp_res); - let e_orig = env - .eq - .iter() - .map(|l| { - l.iter() - .map(|&e| 64.0 * (f64::from(e) / a).exp2()) - .collect() - }) - .collect(); - let q_orig = noise - .q - .iter() - .map(|l| { - l.iter() - .map(|&q| (NOISE_FLOOR_OFFSET - f64::from(q)).exp2()) - .collect() - }) - .collect(); - DequantizedSbr { e_orig, q_orig } -} - -/// §4.6.18.3.5 coupled-pair dequantization. -/// -/// `ch0` carries the level average (`E0` / `Q0`), `ch1` the pan ratio -/// (`E1` / `Q1`). Returns the `(left, right)` linear energies. -#[must_use] -pub fn dequant_coupled( - env0: &EnvelopeScalefactors, - noise0: &NoiseScalefactors, - env1: &EnvelopeScalefactors, - noise1: &NoiseScalefactors, - amp_res: bool, -) -> (DequantizedSbr, DequantizedSbr) { - let a = amp_divisor(amp_res); - let pan = pan_offset(amp_res); - - let mut left_e = Vec::with_capacity(env0.eq.len()); - let mut right_e = Vec::with_capacity(env0.eq.len()); - for (l0, l1) in env0.eq.iter().zip(env1.eq.iter()) { - let mut le = Vec::with_capacity(l0.len()); - let mut re = Vec::with_capacity(l0.len()); - for (&e0, &e1) in l0.iter().zip(l1.iter()) { - // 64·2^(E0/a + 1) split by the pan ratio. - let avg2 = 64.0 * (f64::from(e0) / a + 1.0).exp2(); - let ratio = ((pan - f64::from(e1)) / a).exp2(); - le.push(avg2 / (1.0 + ratio)); - re.push(avg2 / (1.0 + 1.0 / ratio)); - } - left_e.push(le); - right_e.push(re); - } - - // Noise floors always use panOffset(1) = 12 (§4.6.18.3.5: the - // noise formulas are written with panOffset(1) regardless of - // bs_amp_res). - let noise_pan = pan_offset(true); - let mut left_q = Vec::with_capacity(noise0.q.len()); - let mut right_q = Vec::with_capacity(noise0.q.len()); - for (l0, l1) in noise0.q.iter().zip(noise1.q.iter()) { - let mut lq = Vec::with_capacity(l0.len()); - let mut rq = Vec::with_capacity(l0.len()); - for (&q0, &q1) in l0.iter().zip(l1.iter()) { - let avg2 = (NOISE_FLOOR_OFFSET - f64::from(q0) + 1.0).exp2(); - let ratio = (noise_pan - f64::from(q1)).exp2(); - lq.push(avg2 / (1.0 + ratio)); - rq.push(avg2 / (1.0 + 1.0 / ratio)); - } - left_q.push(lq); - right_q.push(rq); - } - - ( - DequantizedSbr { - e_orig: left_e, - q_orig: left_q, - }, - DequantizedSbr { - e_orig: right_e, - q_orig: right_q, - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn env(eq: Vec>) -> EnvelopeScalefactors { - let n = eq.len(); - EnvelopeScalefactors { - eq, - freq_res: vec![true; n], - } - } - - fn noise(q: Vec>) -> NoiseScalefactors { - NoiseScalefactors { q } - } - - /// `EOrig = 64·2^(E/a)`: exact powers for both amplitude - /// resolutions. - #[test] - fn single_channel_envelope_powers() { - let e = env(vec![vec![0, 2, 4]]); - let q = noise(vec![vec![6]]); - // bs_amp_res = 1 → a = 1: 64·2^E. - let d = dequant_single(&e, &q, true); - assert_eq!(d.e_orig[0], vec![64.0, 256.0, 1024.0]); - // bs_amp_res = 0 → a = 2: 64·2^(E/2). - let d = dequant_single(&e, &q, false); - assert_eq!(d.e_orig[0], vec![64.0, 128.0, 256.0]); - } - - /// `QOrig = 2^(6 − Q)`: Q = 6 is unity, each +1 halves. - #[test] - fn single_channel_noise_powers() { - let e = env(vec![vec![0]]); - let q = noise(vec![vec![0, 6, 8]]); - let d = dequant_single(&e, &q, true); - assert_eq!(d.q_orig[0], vec![64.0, 1.0, 0.25]); - } - - /// A balanced pan (`E1 == panOffset`) splits the energy equally: - /// both channels get exactly the mono dequantization. - #[test] - fn coupled_balanced_pan_is_symmetric() { - for amp_res in [false, true] { - let e0 = env(vec![vec![4, 8]]); - let q0 = noise(vec![vec![3]]); - let e1 = env(vec![vec![ - pan_offset(amp_res) as i32, - pan_offset(amp_res) as i32, - ]]); - let q1 = noise(vec![vec![12]]); - let (l, r) = dequant_coupled(&e0, &q0, &e1, &q1, amp_res); - let mono = dequant_single(&e0, &q0, amp_res); - for k in 0..2 { - assert!((l.e_orig[0][k] - mono.e_orig[0][k]).abs() < 1e-12); - assert!((r.e_orig[0][k] - mono.e_orig[0][k]).abs() < 1e-12); - } - assert!((l.q_orig[0][0] - mono.q_orig[0][0]).abs() < 1e-12); - assert!((r.q_orig[0][0] - mono.q_orig[0][0]).abs() < 1e-12); - } - } - - /// The coupled split preserves the pair sum: - /// `ELeft + ERight = 2·(64·2^(E0/a))` for every pan value, and the - /// same for the noise floors. - #[test] - fn coupled_split_preserves_energy_sum() { - for amp_res in [false, true] { - for e1v in [0, 5, 11, 17, 24] { - let e0 = env(vec![vec![6]]); - let q0 = noise(vec![vec![4]]); - let e1 = env(vec![vec![e1v]]); - let q1 = noise(vec![vec![(e1v % 12) * 2]]); - let (l, r) = dequant_coupled(&e0, &q0, &e1, &q1, amp_res); - let mono = dequant_single(&e0, &q0, amp_res); - let sum = l.e_orig[0][0] + r.e_orig[0][0]; - assert!( - (sum - 2.0 * mono.e_orig[0][0]).abs() < 1e-9, - "amp_res {amp_res} pan {e1v}: {sum}" - ); - let qsum = l.q_orig[0][0] + r.q_orig[0][0]; - assert!((qsum - 2.0 * mono.q_orig[0][0]).abs() < 1e-9); - } - } - } - - /// A pan below the offset weights the left channel heavier (E1 - /// counts down from left-dominant to right-dominant). - #[test] - fn coupled_pan_direction() { - let e0 = env(vec![vec![6]]); - let q0 = noise(vec![vec![4]]); - let e1 = env(vec![vec![2]]); - let q1 = noise(vec![vec![2]]); - let (l, r) = dequant_coupled(&e0, &q0, &e1, &q1, true); - assert!(l.e_orig[0][0] < r.e_orig[0][0]); - assert!(l.q_orig[0][0] < r.q_orig[0][0]); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_element.rs b/crates/vendor/oxideav-aac/src/sbr_element.rs deleted file mode 100644 index e0ef6689..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_element.rs +++ /dev/null @@ -1,561 +0,0 @@ -//! SBR element framing — `sbr_single_channel_element()` / -//! `sbr_channel_pair_element()` and `sbr_sinusoidal_coding()` — -//! ISO/IEC 14496-3 §4.4.2.8, Tables 4.65, 4.66, 4.74. -//! -//! These wrappers tie the per-channel grid / dtdf / invf / envelope / -//! noise parses together into a whole SBR data element, in the exact -//! order the spec syntax tables prescribe: -//! -//! * `sbr_single_channel_element()` (Table 4.65): one optional -//! `bs_data_extra` reserved field, then `sbr_grid(0)`, `sbr_dtdf(0)`, -//! `sbr_invf(0)`, `sbr_envelope(0,0)`, `sbr_noise(0,0)`, the optional -//! `sbr_sinusoidal_coding(0)`, and the optional extended-data block. -//! * `sbr_channel_pair_element()` (Table 4.66): the two -//! coupling-dependent layouts. When `bs_coupling` is set, a single -//! shared grid drives both channels' envelopes / noise (with the -//! second channel coded in *balance* mode); otherwise each channel -//! carries its own grid. Either way the parse order is fixed by the -//! table. -//! -//! `sbr_sinusoidal_coding()` (Table 4.74) reads one -//! `bs_add_harmonic[ch][n]` flag per high-resolution band (`NHigh`). -//! -//! The element-level `bs_amp_res` may be forced to `0` by a -//! single-envelope FIXFIX grid ([`crate::sbr_grid::SbrGrid::amp_res_override`]); -//! this wrapper applies that override before decoding the envelopes so -//! the start-value widths and codebook selection match the spec's -//! in-order `bs_amp_res` mutation. -//! -//! The extended-data block (`bs_extended_data` … `sbr_extension`) is -//! recognized and its size is consumed, but the only standardized -//! `sbr_extension` payload (PS, `bs_extension_id == EXTENSION_ID_PS`) -//! is not yet decoded — its bits are skipped as fill so the element -//! parse stays byte-aligned. The raw extension bytes are surfaced for a -//! later PS pass. -//! -//! All of this is fixed-/variable-width syntax driven by the grid and -//! the band tables; the Huffman content lives in [`crate::sbr_huffman`]. - -use crate::sbr_envelope::{SbrEnvelopeData, SbrNoiseData}; -use crate::sbr_freq_bands::HiLoTables; -use crate::sbr_grid::{SbrDtdf, SbrGrid, SbrInvf}; -use crate::{Error, Result}; -use oxideav_core::bits::BitReader; - -/// `bs_extension_id` value that signals a Parametric Stereo payload -/// inside `sbr_extension()` (§4.4.2.8 / Table 4.A.x). PS itself is not -/// decoded here yet. -pub const EXTENSION_ID_PS: u8 = 2; - -/// One channel's fully-parsed SBR side info. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrChannel { - /// The time-frequency grid. - pub grid: SbrGrid, - /// The delta-direction flags. - pub dtdf: SbrDtdf, - /// The inverse-filtering modes (one per noise band). - pub invf: SbrInvf, - /// Raw envelope deltas. - pub envelope: SbrEnvelopeData, - /// Raw noise-floor deltas. - pub noise: SbrNoiseData, - /// `bs_add_harmonic[n]` — one flag per high-resolution band - /// (`NHigh`); empty when `bs_add_harmonic_flag` was clear. - pub add_harmonic: Vec, -} - -/// A parsed SBR data element (single channel or channel pair). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrElement { - /// `bs_coupling` (always `false` for a single channel element). - pub coupling: bool, - /// One or two channels of side info. - pub channels: Vec, - /// Raw bytes of an `sbr_extension()` payload, if `bs_extended_data` - /// was set. Reserved for a later PS decode; `None` when no extended - /// data was present. - pub extension: Option, -} - -/// Raw `sbr_extension()` content carried past this parse. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrExtension { - /// `bs_extension_id`. - pub id: u8, - /// The extension body bytes (everything after the 2-bit id, up to - /// the byte-aligned fill). - pub data: Vec, -} - -/// `sbr_sinusoidal_coding()` (Table 4.74): `NHigh` add-harmonic flags. -fn parse_sinusoidal(reader: &mut BitReader<'_>, n_high: usize) -> Result> { - let mut v = Vec::with_capacity(n_high); - for _ in 0..n_high { - v.push(read_flag(reader)?); - } - Ok(v) -} - -/// Parse one channel's `sbr_grid` → `sbr_dtdf` → `sbr_invf` block (the -/// shared prefix of both element types). -fn parse_grid_dtdf_invf( - reader: &mut BitReader<'_>, - n_q: usize, -) -> Result<(SbrGrid, SbrDtdf, SbrInvf)> { - let grid = SbrGrid::parse(reader)?; - let dtdf = SbrDtdf::parse(reader, grid.num_env, grid.num_noise)?; - let invf = SbrInvf::parse(reader, n_q)?; - Ok((grid, dtdf, invf)) -} - -impl SbrElement { - /// Parse `sbr_single_channel_element()` (Table 4.65). - /// - /// `bands` is the derived band table for the active header; `n_q` is - /// its noise-band count. `bs_amp_res` is the header amplitude - /// resolution (it may be overridden by a single-envelope FIXFIX - /// grid). - pub fn parse_single( - reader: &mut BitReader<'_>, - bands: &HiLoTables, - amp_res: bool, - ) -> Result { - let n_q = bands.n_q(); - // bs_data_extra (1 bit) → optional bs_reserved (4). - if read_flag(reader)? { - read(reader, 4)?; - } - - let (grid, dtdf, invf) = parse_grid_dtdf_invf(reader, n_q)?; - let eff_amp = amp_res && !grid.amp_res_override; - - let envelope = SbrEnvelopeData::parse(reader, &grid, &dtdf, bands, false, false, eff_amp)?; - let noise = SbrNoiseData::parse(reader, &grid, &dtdf, n_q, false, false, eff_amp)?; - - let add_harmonic = if read_flag(reader)? { - parse_sinusoidal(reader, bands.n_high())? - } else { - Vec::new() - }; - - let extension = parse_extended_data(reader)?; - - Ok(SbrElement { - coupling: false, - channels: vec![SbrChannel { - grid, - dtdf, - invf, - envelope, - noise, - add_harmonic, - }], - extension, - }) - } - - /// Parse `sbr_channel_pair_element()` (Table 4.66), both the coupled - /// and the independent layouts. - pub fn parse_pair( - reader: &mut BitReader<'_>, - bands: &HiLoTables, - amp_res: bool, - ) -> Result { - let n_q = bands.n_q(); - // bs_data_extra (1 bit) → two bs_reserved (4 each). - if read_flag(reader)? { - read(reader, 4)?; - read(reader, 4)?; - } - - let coupling = read_flag(reader)?; - - let channels = if coupling { - // Shared grid; second channel coded in balance mode. Parse - // order (Table 4.66, coupling): grid(0), dtdf(0), dtdf(1), - // invf(0). - let grid = SbrGrid::parse(reader)?; - let dtdf0 = SbrDtdf::parse(reader, grid.num_env, grid.num_noise)?; - let dtdf1 = SbrDtdf::parse(reader, grid.num_env, grid.num_noise)?; - let invf0 = SbrInvf::parse(reader, n_q)?; - let eff_amp = amp_res && !grid.amp_res_override; - - // Order (Table 4.66, coupling): env0, noise0, env1, noise1. - let env0 = SbrEnvelopeData::parse(reader, &grid, &dtdf0, bands, true, false, eff_amp)?; - let noise0 = SbrNoiseData::parse(reader, &grid, &dtdf0, n_q, true, false, eff_amp)?; - let env1 = SbrEnvelopeData::parse(reader, &grid, &dtdf1, bands, true, true, eff_amp)?; - let noise1 = SbrNoiseData::parse(reader, &grid, &dtdf1, n_q, true, true, eff_amp)?; - - let (h0, h1) = parse_pair_harmonics(reader, bands)?; - vec![ - SbrChannel { - grid: grid.clone(), - dtdf: dtdf0, - invf: invf0, - envelope: env0, - noise: noise0, - add_harmonic: h0, - }, - SbrChannel { - grid, - dtdf: dtdf1, - invf: SbrInvf { - invf_mode: Vec::new(), - }, - envelope: env1, - noise: noise1, - add_harmonic: h1, - }, - ] - } else { - // Independent grids per channel. - let grid0 = SbrGrid::parse(reader)?; - let grid1 = SbrGrid::parse(reader)?; - let dtdf0 = SbrDtdf::parse(reader, grid0.num_env, grid0.num_noise)?; - let dtdf1 = SbrDtdf::parse(reader, grid1.num_env, grid1.num_noise)?; - let invf0 = SbrInvf::parse(reader, n_q)?; - let invf1 = SbrInvf::parse(reader, n_q)?; - - let eff0 = amp_res && !grid0.amp_res_override; - let eff1 = amp_res && !grid1.amp_res_override; - - // Order (Table 4.66, no coupling): env0, env1, noise0, noise1. - let env0 = SbrEnvelopeData::parse(reader, &grid0, &dtdf0, bands, false, false, eff0)?; - let env1 = SbrEnvelopeData::parse(reader, &grid1, &dtdf1, bands, false, true, eff1)?; - let noise0 = SbrNoiseData::parse(reader, &grid0, &dtdf0, n_q, false, false, eff0)?; - let noise1 = SbrNoiseData::parse(reader, &grid1, &dtdf1, n_q, false, true, eff1)?; - - let (h0, h1) = parse_pair_harmonics(reader, bands)?; - vec![ - SbrChannel { - grid: grid0, - dtdf: dtdf0, - invf: invf0, - envelope: env0, - noise: noise0, - add_harmonic: h0, - }, - SbrChannel { - grid: grid1, - dtdf: dtdf1, - invf: invf1, - envelope: env1, - noise: noise1, - add_harmonic: h1, - }, - ] - }; - - let extension = parse_extended_data(reader)?; - - Ok(SbrElement { - coupling, - channels, - extension, - }) - } -} - -/// The two `bs_add_harmonic_flag[ch]` blocks of a channel pair -/// (Table 4.66): each optionally followed by an `sbr_sinusoidal_coding` -/// of `NHigh` flags. -fn parse_pair_harmonics( - reader: &mut BitReader<'_>, - bands: &HiLoTables, -) -> Result<(Vec, Vec)> { - let h0 = if read_flag(reader)? { - parse_sinusoidal(reader, bands.n_high())? - } else { - Vec::new() - }; - let h1 = if read_flag(reader)? { - parse_sinusoidal(reader, bands.n_high())? - } else { - Vec::new() - }; - Ok((h0, h1)) -} - -/// The shared `if (bs_extended_data) { … }` block (Tables 4.65 / 4.66). -/// -/// Reads `bs_extension_size` (4 bits, extended by `bs_esc_count` when -/// `== 15`), then for the duration of the block reads `bs_extension_id` -/// (2 bits) and captures the body as raw bytes. The standardized PS -/// payload is not decoded here; the bytes are returned for a later -/// pass. -fn parse_extended_data(reader: &mut BitReader<'_>) -> Result> { - if !read_flag(reader)? { - return Ok(None); - } - let mut cnt = read(reader, 4)?; - if cnt == 15 { - cnt += read(reader, 8)?; - } - let mut num_bits_left = (8 * cnt) as i64; - // The while-loop in the spec reads one bs_extension_id then hands - // the rest to sbr_extension(); we capture the first id and then - // *every remaining bit* of the block. The extension payload (e.g. - // ps_data(), Table 8.A.1) is a bitstream that is NOT byte-aligned - // within the block — its final sub-byte shares a byte with the - // bs_fill_bits — so a whole-byte capture would truncate up to 7 - // trailing payload bits. The re-packed buffer is zero-padded to a - // byte; the payload parser consumes exactly the bits it needs and - // ignores the rest as fill. - let mut id = 0u8; - let mut data: Vec = Vec::new(); - if num_bits_left > 7 { - id = read(reader, 2)? as u8; - num_bits_left -= 2; - let mut w = oxideav_core::bits::BitWriter::new(); - while num_bits_left >= 8 { - w.write_u32(read(reader, 8)?, 8); - num_bits_left -= 8; - } - if num_bits_left > 0 { - let n = num_bits_left as u32; - w.write_u32(read(reader, n)?, n); - num_bits_left = 0; - } - data = w.finish(); - } - // bs_fill_bits: consume the remaining (< 8) bits of a block too - // short to carry an id. - if num_bits_left > 0 { - read(reader, num_bits_left as u32)?; - } - Ok(Some(SbrExtension { id, data })) -} - -#[inline] -fn read(reader: &mut BitReader<'_>, n: u32) -> Result { - reader.read_u32(n).map_err(|_| Error::SbrGridInvalid) -} - -#[inline] -fn read_flag(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::SbrGridInvalid) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sbr_freq_bands::{k0, k2, master_table, HiLoTables}; - use crate::sbr_grid::FrameClass; - use crate::sbr_huffman::{env_tables, noise_tables, SbrHuffContext}; - use oxideav_core::bits::{BitReader, BitWriter}; - - fn bands_44100() -> HiLoTables { - let k0v = k0(88_200, 5).unwrap(); - let k2v = k2(88_200, 5, k0v).unwrap(); - let fm = master_table(k0v, k2v, 0, false).unwrap(); - HiLoTables::derive(&fm, 1, 2).unwrap() - } - - fn push_code(w: &mut BitWriter, table: &[(u8, u32)], idx: usize) { - let (len, code) = table[idx]; - w.write_u32(code, len as u32); - } - - /// Write a minimal single-channel SBR element: no data_extra, a - /// FIXFIX single high-res envelope (forces amp_res=0), freq-coded - /// envelope + noise, no sinusoidal, no extended data. - fn write_minimal_sce(bands: &HiLoTables) -> Vec { - let n_high = bands.n_high(); - let n_q = bands.n_q(); - let mut w = BitWriter::new(); - w.write_bit(false); // bs_data_extra - // sbr_grid: FIXFIX, 2^0 = 1 env, freq_res high. - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(0, 2); // 1 env - w.write_bit(true); // freq_res[0] = high - // sbr_dtdf: 1 env flag + 1 noise flag, both freq (0). - w.write_bit(false); // df_env[0] - w.write_bit(false); // df_noise[0] - // sbr_invf: n_q 2-bit modes. - for _ in 0..n_q { - w.write_u32(1, 2); - } - // sbr_envelope: amp_res override → false; level start = 7 bits. - let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - w.write_u32(33, 7); // start value - for i in 1..n_high { - push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); - } - // sbr_noise: 5-bit start + (n_q-1) f deltas. - let ((_nt, _ntl), (nf, nfl)) = noise_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - w.write_u32(10, 5); - for i in 1..n_q { - push_code(&mut w, nf, (i + nfl as usize) % nf.len()); - } - w.write_bit(false); // bs_add_harmonic_flag[0] - w.write_bit(false); // bs_extended_data - w.finish() - } - - #[test] - fn single_channel_element_round_trips_structure() { - let bands = bands_44100(); - let bytes = write_minimal_sce(&bands); - let mut r = BitReader::new(&bytes); - // Header amp_res = true, but the single-env FIXFIX overrides it. - let el = SbrElement::parse_single(&mut r, &bands, true).unwrap(); - assert!(!el.coupling); - assert_eq!(el.channels.len(), 1); - let ch = &el.channels[0]; - assert_eq!(ch.grid.frame_class, FrameClass::FixFix); - assert_eq!(ch.grid.num_env, 1); - assert!(ch.grid.amp_res_override); - assert_eq!(ch.envelope.data[0].len(), bands.n_high()); - assert_eq!(ch.envelope.data[0][0], 33); - assert_eq!(ch.noise.data[0].len(), bands.n_q()); - assert_eq!(ch.noise.data[0][0], 10); - assert!(ch.add_harmonic.is_empty()); - assert!(el.extension.is_none()); - assert_eq!(ch.invf.invf_mode.len(), bands.n_q()); - } - - #[test] - fn single_channel_with_sinusoidal_and_extension() { - let bands = bands_44100(); - let n_high = bands.n_high(); - let n_q = bands.n_q(); - let mut w = BitWriter::new(); - w.write_bit(false); // data_extra - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(0, 2); - w.write_bit(true); - w.write_bit(false); // df_env - w.write_bit(false); // df_noise - for _ in 0..n_q { - w.write_u32(0, 2); - } - let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - w.write_u32(20, 7); - for i in 1..n_high { - push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); - } - let ((_nt, _ntl), (nf, nfl)) = noise_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - w.write_u32(5, 5); - for i in 1..n_q { - push_code(&mut w, nf, (i + nfl as usize) % nf.len()); - } - // Sinusoidal: flag set, then n_high bools (alternating). - w.write_bit(true); - for n in 0..n_high { - w.write_bit(n % 2 == 0); - } - // Extended data: id = PS, one body byte 0xA5, then byte-align. - w.write_bit(true); // bs_extended_data - w.write_u32(1, 4); // bs_extension_size = 1 byte (8 bits) - // 8 bits = id(2) + 6 fill bits; body has no full byte. - w.write_u32(EXTENSION_ID_PS as u32, 2); - w.write_u32(0, 6); // fill - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let el = SbrElement::parse_single(&mut r, &bands, false).unwrap(); - let ch = &el.channels[0]; - assert_eq!(ch.add_harmonic.len(), n_high); - assert!(ch.add_harmonic[0]); - assert!(!ch.add_harmonic[1]); - let ext = el.extension.unwrap(); - assert_eq!(ext.id, EXTENSION_ID_PS); - } - - #[test] - fn channel_pair_independent_grids() { - let bands = bands_44100(); - let n_high = bands.n_high(); - let n_q = bands.n_q(); - let mut w = BitWriter::new(); - w.write_bit(false); // data_extra - w.write_bit(false); // bs_coupling = 0 (independent) - // grid0: FIXFIX 1 env high. - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(0, 2); - w.write_bit(true); - // grid1: FIXFIX 1 env high. - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(0, 2); - w.write_bit(true); - // dtdf0, dtdf1 (1 env + 1 noise each), all freq. - w.write_bit(false); - w.write_bit(false); - w.write_bit(false); - w.write_bit(false); - // invf0, invf1. - for _ in 0..n_q { - w.write_u32(2, 2); - } - for _ in 0..n_q { - w.write_u32(3, 2); - } - let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - let ((_nt, _ntl), (nf, nfl)) = noise_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - // env0, env1. - for &start in &[30u32, 40] { - w.write_u32(start, 7); - for i in 1..n_high { - push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); - } - } - // noise0, noise1. - for &start in &[8u32, 9] { - w.write_u32(start, 5); - for i in 1..n_q { - push_code(&mut w, nf, (i + nfl as usize) % nf.len()); - } - } - w.write_bit(false); // harmonic flag ch0 - w.write_bit(false); // harmonic flag ch1 - w.write_bit(false); // extended data - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let el = SbrElement::parse_pair(&mut r, &bands, false).unwrap(); - assert!(!el.coupling); - assert_eq!(el.channels.len(), 2); - assert_eq!(el.channels[0].envelope.data[0][0], 30); - assert_eq!(el.channels[1].envelope.data[0][0], 40); - assert_eq!(el.channels[0].noise.data[0][0], 8); - assert_eq!(el.channels[1].noise.data[0][0], 9); - assert_eq!(el.channels[0].invf.invf_mode, vec![2u8; n_q]); - assert_eq!(el.channels[1].invf.invf_mode, vec![3u8; n_q]); - } - - #[test] - fn truncated_element_errors() { - let bands = bands_44100(); - let bytes = [0u8; 0]; - let mut r = BitReader::new(&bytes); - assert!(matches!( - SbrElement::parse_single(&mut r, &bands, true), - Err(Error::SbrGridInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_env_adjust.rs b/crates/vendor/oxideav-aac/src/sbr_env_adjust.rs deleted file mode 100644 index a1396126..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_env_adjust.rs +++ /dev/null @@ -1,1029 +0,0 @@ -//! SBR HF adjustment (envelope adjuster) — ISO/IEC 14496-3 §4.6.18.7. -//! -//! Takes the HF-generated subband matrix `XHigh` and produces the -//! output matrix `Y` over the `M` SBR subbands starting at `kx`: -//! -//! * **Mapping** (§4.6.18.7.2) — `EOrigMapped` / `QMapped` to QMF -//! resolution, the `SIndexMapped` sinusoid placement (band middle, -//! `δStep` start gate against `lA` and the previous frame's -//! sinusoids) and the `SMapped` band flags. -//! * **Current envelope estimation** (§4.6.18.7.3) — `ECurr` by -//! squared-magnitude averaging, per subband (`bs_interpol_freq = 1`) -//! or per envelope band. -//! * **Additional-component levels** (§4.6.18.7.4) — `QM` / `SM` -//! (amplitude domain, i.e. with the square root of the energy -//! ratios). -//! * **Gain** (§4.6.18.7.5) — `G`, the limiter (`GMax` from the -//! `fTableLim` band ratios and `limGain`), the noise-level limit -//! `QM_Lim`, and the boost compensation `GBoost` capped at -//! `1.584893192`. -//! * **Assembly** (§4.6.18.7.6) — the `hSmooth` gain/noise smoothing -//! over `hSL` columns, `W1 = GFilt·XHigh`, the Table 4.A.91 noise -//! mix `W2`, and the `φsin` sinusoid injection with the -//! `(−1)^(m+kx)` imaginary alternation, producing `Y`. -//! -//! **Low-power mode** (§4.6.18.8, `EnvParams::low_power`): the energy -//! estimation carries the §4.6.18.8.4 factor 2 (real-valued subband -//! signals hold half the energy of the complex representation), gain -//! smoothing is disabled regardless of `bs_smoothing_mode`, the -//! §4.6.18.8.5 aliasing reduction re-computes the limiter/boost gains -//! over the Figure 4.54 groups (driven by the caller-supplied -//! `degPatched`), the Table 4.A.91 noise mix keeps only its real -//! part, and the sinusoid injection follows the §4.6.18.8.5 modified -//! equations — real-valued `ψm` with the `−0.00815·(−1)^(m+kx)` -//! neighbour correction, applied to the first 16 sinusoids per time -//! segment, spilling into subbands `kx − 1` and `kx + M`. -//! -//! Cross-frame state (`EnvAdjustState`) carries the previous frame's -//! last-envelope `SIndexMapped`, `lA` / `LE`, the `GTemp` / `QTemp` -//! smoothing tails, and the running `indexNoise` / `indexSine`. -//! -//! ## Provenance -//! -//! Every formula (including the square roots the §4.6.18.7.4–7.5 -//! equations carry) was read from the staged ISO/IEC 14496-3:2009 spec -//! PDF's typeset equations. No part of this implementation is derived -//! from any external decoder. - -use crate::sbr_freq_bands::HiLoTables; -use crate::sbr_hf_gen::T_HF_ADJ; -use crate::sbr_lp::{aliasing_reduction, gain_groups}; -use crate::sbr_noise_table::NOISE_TABLE; -use crate::sbr_qmf::Complex; -use crate::{Error, Result}; - -/// `limGain = [0.70795, 1.0, 1.41254, 1e10]` (§4.6.18.7.5). -pub const LIM_GAIN: [f64; 4] = [0.70795, 1.0, 1.41254, 1e10]; - -/// `ε0 = 1e-12` (§4.6.18.7.5). -pub const EPS0: f64 = 1e-12; - -/// `ε = 1` (§4.6.18.2.5) — the division-by-zero guard in the gain. -pub const EPS: f64 = 1.0; - -/// The `GBoost` cap `1.584893192` (§4.6.18.7.5). -pub const MAX_BOOST: f64 = 1.584893192; - -/// The `GMax` cap `10^5` (§4.6.18.7.5). -pub const G_MAX_CAP: f64 = 1e5; - -/// `hSmooth` — the §4.6.18.7.6 smoothing filter. -pub const H_SMOOTH: [f64; 5] = [ - 0.33333333333333, - 0.30150283239582, - 0.21816949906249, - 0.11516383427084, - 0.03183050093751, -]; - -/// `φRe,sin = [1, 0, −1, 0]`, `φIm,sin = [0, 1, 0, −1]` (§4.6.18.7.6). -pub const PHI_SIN: [(f64, f64); 4] = [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)]; - -/// Per-frame inputs to the envelope adjuster (one channel). -#[derive(Debug)] -pub struct EnvParams<'a> { - /// Derived frequency band tables (`M`, `kx`, high/low/noise). - pub bands: &'a HiLoTables, - /// The §4.6.18.3.2.3 limiter band table `fTableLim(0..=NL)`. - pub f_table_lim: &'a [i32], - /// Envelope time borders `tE(0..=LE)` (slots). - pub t_e: &'a [i32], - /// Noise-floor time borders `tQ(0..=LQ)` (slots). - pub t_q: &'a [i32], - /// Per-envelope frequency resolution `r(l)` (`true` = high). - pub freq_res: &'a [bool], - /// Table 4.176 `lA` (`-1` = none). - pub l_a: i32, - /// Dequantized envelope energies `EOrig[l][k]`. - pub e_orig: &'a [Vec], - /// Dequantized noise-floor energies `QOrig[l][k]`. - pub q_orig: &'a [Vec], - /// `bs_add_harmonic` flags (`NHigh` entries; empty = none). - pub add_harmonic: &'a [bool], - /// `bs_interpol_freq`. - pub interpol_freq: bool, - /// `bs_smoothing_mode` (`true` ⇒ `hSL = 0`). - pub smoothing_mode: bool, - /// `bs_limiter_gains` (`0..=3`, indexes [`LIM_GAIN`]). - pub limiter_gains: u8, - /// The §4.6.18.3.3 reset flag (header band geometry changed). - pub reset: bool, - /// §4.6.18.8 low-power mode: ×2 energy estimation, no gain - /// smoothing, aliasing reduction, real-only noise, and the - /// modified sinusoid injection. - pub low_power: bool, - /// The §4.6.18.8.3 `degPatched` (`kx`-relative, `M` entries) — - /// required when `low_power` is set. - pub deg_patched: Option<&'a [f64]>, -} - -/// Cross-frame envelope-adjuster state for one channel. -#[derive(Debug, Clone, Default)] -pub struct EnvAdjustState { - /// Previous frame's last-envelope `SIndexMapped` (per SBR subband, - /// `kx`-relative) plus its `kx`, for the `δStep` gate. - s_index_prev: Vec, - k_x_prev: i32, - /// Previous frame's `lA` and `LE` (for `lAPrev`). - l_a_prev_frame: i32, - l_e_prev: i32, - /// Previous frame's trailing `hSL` columns of `GTemp` / `QTemp`. - g_temp_tail: Vec>, - q_temp_tail: Vec>, - /// Running noise / sine phase indices. - index_noise: usize, - index_sine: usize, - started: bool, -} - -impl EnvAdjustState { - /// Fresh state (first frame / after a stream reset). - #[must_use] - pub fn new() -> Self { - Self::default() - } -} - -/// Run the §4.6.18.7 HF adjustment for one channel's SBR frame. -/// -/// `x_high` is the slot-major HF-generator output (spec absolute -/// columns, i.e. spec index `i + tHFAdj` is a direct column index). -/// Returns `Y` in the same layout, filled for the SBR range and the -/// frame's envelope span; other cells are zero. -pub fn adjust( - x_high: &[[Complex; 64]], - p: &EnvParams<'_>, - st: &mut EnvAdjustState, -) -> Result> { - let m_cnt = usize::try_from(p.bands.m).map_err(|_| Error::SbrFreqBandInvalid)?; - let k_x = p.bands.k_x; - let l_e = p - .t_e - .len() - .checked_sub(1) - .ok_or(Error::SbrFreqBandInvalid)?; - if l_e == 0 - || p.freq_res.len() != l_e - || p.e_orig.len() != l_e - || p.q_orig.len() + 1 != p.t_q.len() - || p.f_table_lim.len() < 2 - || usize::from(p.limiter_gains) >= LIM_GAIN.len() - { - return Err(Error::SbrFreqBandInvalid); - } - - let rate = 2i32; // RATE (§4.6.18.2.5) - let i0 = rate * p.t_e[0]; - let i_end = rate * p.t_e[l_e]; - let n_cols = usize::try_from(i_end - i0).map_err(|_| Error::SbrFreqBandInvalid)?; - if i0 < 0 - || usize::try_from(i_end).map_err(|_| Error::SbrFreqBandInvalid)? + T_HF_ADJ > x_high.len() - { - return Err(Error::SbrFreqBandInvalid); - } - - if p.reset || !st.started { - st.index_noise = 0; - st.index_sine = 0; - st.s_index_prev.clear(); - st.g_temp_tail.clear(); - st.q_temp_tail.clear(); - st.l_a_prev_frame = -1; - st.l_e_prev = 0; - st.started = true; - } - - // lAPrev: 0 if the previous frame's transient sat on its trailing - // border, else -1. - let l_a_prev = if st.l_a_prev_frame == st.l_e_prev { - 0i32 - } else { - -1 - }; - - // ---- §4.6.18.7.2 mapping ------------------------------------- - // Envelope band table per resolution. - let f_of = |high: bool| -> &Vec { - if high { - &p.bands.f_table_high - } else { - &p.bands.f_table_low - } - }; - // Band index of QMF subband `k` in border table `f`. - let band_of = |f: &[i32], k: i32| -> Result { - for i in 0..f.len() - 1 { - if f[i] <= k && k < f[i + 1] { - return Ok(i); - } - } - Err(Error::SbrFreqBandInvalid) - }; - - let mut e_map = vec![vec![0.0f64; m_cnt]; l_e]; // EOrigMapped[l][m] - let mut q_map = vec![vec![0.0f64; m_cnt]; l_e]; // QMapped[l][m] - let mut s_index = vec![vec![false; m_cnt]; l_e]; // SIndexMapped[l][m] - let mut s_map = vec![vec![false; m_cnt]; l_e]; // SMapped[l][m] - - let n_high = p.bands.n_high(); - for l in 0..l_e { - let f = f_of(p.freq_res[l]); - if p.e_orig[l].len() + 1 != f.len() { - return Err(Error::SbrFreqBandInvalid); - } - // k(l): the noise floor whose span contains envelope l. - let mut kq = None; - for q in 0..p.t_q.len() - 1 { - if p.t_q[q] <= p.t_e[l] && p.t_e[l + 1] <= p.t_q[q + 1] { - kq = Some(q); - break; - } - } - let kq = kq.ok_or(Error::SbrFreqBandInvalid)?; - if p.q_orig[kq].len() + 1 != p.bands.f_table_noise.len() { - return Err(Error::SbrFreqBandInvalid); - } - for m in 0..m_cnt { - let k = k_x + i32::try_from(m).map_err(|_| Error::SbrFreqBandInvalid)?; - e_map[l][m] = p.e_orig[l][band_of(f, k)?]; - q_map[l][m] = p.q_orig[kq][band_of(&p.bands.f_table_noise, k)?]; - } - - // SIndexMapped: sinusoid in the middle subband of each - // high-resolution band, gated by δStep. - if !p.add_harmonic.is_empty() { - if p.add_harmonic.len() != n_high { - return Err(Error::SbrFreqBandInvalid); - } - for (i, &on) in p.add_harmonic.iter().enumerate() { - if !on { - continue; - } - let mid = (p.bands.f_table_high[i + 1] + p.bands.f_table_high[i]) / 2; - let m_rel = mid - k_x; - if m_rel < 0 || m_rel as usize >= m_cnt { - continue; - } - // δStep: on from lA, or already ringing in the - // previous frame's last envelope. - let prev_on = { - let prev_rel = mid - st.k_x_prev; - prev_rel >= 0 - && st - .s_index_prev - .get(prev_rel as usize) - .copied() - .unwrap_or(false) - }; - if (l as i32) >= p.l_a || prev_on { - s_index[l][m_rel as usize] = true; - } - } - } - // SMapped: any sinusoid within the envelope band. - for i in 0..f.len() - 1 { - let any = ((f[i] - k_x).max(0)..(f[i + 1] - k_x).max(0)) - .any(|j| (j as usize) < m_cnt && s_index[l][j as usize]); - if any { - for j in (f[i] - k_x).max(0)..(f[i + 1] - k_x).max(0) { - if (j as usize) < m_cnt { - s_map[l][j as usize] = true; - } - } - } - } - } - - // ---- §4.6.18.7.3 current envelope ---------------------------- - // §4.6.18.8.4: the real-valued low-power signals carry half the - // energy of the complex representation — the estimation doubles. - let e_scale = if p.low_power { 2.0 } else { 1.0 }; - let mut e_curr = vec![vec![0.0f64; m_cnt]; l_e]; - for (l, e_curr_l) in e_curr.iter_mut().enumerate() { - let lo = (rate * p.t_e[l] + T_HF_ADJ as i32) as usize; - let hi = (rate * p.t_e[l + 1] + T_HF_ADJ as i32) as usize; - let width = (hi - lo) as f64; - if p.interpol_freq { - for (m, e) in e_curr_l.iter_mut().enumerate() { - let k = (k_x as usize) + m; - let sum: f64 = x_high[lo..hi].iter().map(|col| col[k].norm_sqr()).sum(); - *e = e_scale * sum / width; - } - } else { - let f = f_of(p.freq_res[l]); - for pband in 0..f.len() - 1 { - let kl = f[pband]; - let kh = f[pband + 1] - 1; - let mut sum = 0.0; - for j in kl..=kh { - sum += x_high[lo..hi] - .iter() - .map(|col| col[j as usize].norm_sqr()) - .sum::(); - } - let avg = e_scale * sum / (width * f64::from(kh - kl + 1)); - for j in kl..=kh { - let m_rel = j - k_x; - if m_rel >= 0 && (m_rel as usize) < m_cnt { - e_curr_l[m_rel as usize] = avg; - } - } - } - } - } - - // ---- §4.6.18.7.4 / 7.5 gain, limiter, boost ------------------ - let lim_gain = LIM_GAIN[usize::from(p.limiter_gains)]; - let n_l = p.f_table_lim.len() - 1; - - let mut g_lim_boost = vec![vec![0.0f64; m_cnt]; l_e]; - let mut q_m_lim_boost = vec![vec![0.0f64; m_cnt]; l_e]; - let mut s_m_boost = vec![vec![0.0f64; m_cnt]; l_e]; - - for l in 0..l_e { - let li = l as i32; - let delta_l = if li == p.l_a || li == l_a_prev { - 0.0 - } else { - 1.0 - }; - - // QM / SM (amplitude domain). - let mut q_m = vec![0.0f64; m_cnt]; - let mut s_m = vec![0.0f64; m_cnt]; - let mut g = vec![0.0f64; m_cnt]; - for m in 0..m_cnt { - let e_o = e_map[l][m]; - let q = q_map[l][m]; - q_m[m] = (e_o * q / (1.0 + q)).sqrt(); - s_m[m] = if s_index[l][m] { - (e_o / (1.0 + q)).sqrt() - } else { - 0.0 - }; - g[m] = if s_map[l][m] { - ((e_o / (EPS + e_curr[l][m])) * (q / (1.0 + q))).sqrt() - } else { - (e_o / ((EPS + e_curr[l][m]) * (1.0 + delta_l * q))).sqrt() - }; - } - - // Limiter-band maxima. - let mut g_max = vec![0.0f64; m_cnt]; - for k in 0..n_l { - let lo = (p.f_table_lim[k] - k_x).max(0) as usize; - let hi = ((p.f_table_lim[k + 1] - k_x).max(0) as usize).min(m_cnt); - let num: f64 = EPS0 + e_map[l][lo..hi].iter().sum::(); - let den: f64 = EPS0 + e_curr[l][lo..hi].iter().sum::(); - let gmax = ((num / den).sqrt() * lim_gain).min(G_MAX_CAP); - for gm in &mut g_max[lo..hi] { - *gm = gmax; - } - } - - // QM_Lim / GLim. - let mut q_m_lim = vec![0.0f64; m_cnt]; - let mut g_lim = vec![0.0f64; m_cnt]; - for m in 0..m_cnt { - q_m_lim[m] = if g[m] > 0.0 { - q_m[m].min(q_m[m] * g_max[m] / g[m]) - } else { - q_m[m] - }; - g_lim[m] = g[m].min(g_max[m]); - } - - // Boost per limiter band. - for k in 0..n_l { - let lo = (p.f_table_lim[k] - k_x).max(0) as usize; - let hi = ((p.f_table_lim[k + 1] - k_x).max(0) as usize).min(m_cnt); - let mut num = EPS0; - let mut den = EPS0; - for i in lo..hi { - num += e_map[l][i]; - let delta_s = if s_m[i] != 0.0 || li == p.l_a || li == l_a_prev { - 0.0 - } else { - 1.0 - }; - den += e_curr[l][i] * g_lim[i] * g_lim[i] - + s_m[i] * s_m[i] - + delta_s * q_m_lim[i] * q_m_lim[i]; - } - let boost = (num / den).sqrt().min(MAX_BOOST); - for i in lo..hi { - g_lim_boost[l][i] = g_lim[i] * boost; - q_m_lim_boost[l][i] = q_m_lim[i] * boost; - s_m_boost[l][i] = s_m[i] * boost; - } - } - } - - // ---- §4.6.18.8.5 aliasing reduction (low power) -------------- - // GA replaces GLimBoost in the assembly below. - if p.low_power { - let dp = p.deg_patched.ok_or(Error::SbrFreqBandInvalid)?; - if dp.len() != m_cnt { - return Err(Error::SbrFreqBandInvalid); - } - for l in 0..l_e { - let groups = gain_groups(dp, &s_map[l], k_x); - aliasing_reduction(&mut g_lim_boost[l], &e_curr[l], dp, &groups, k_x)?; - } - } - - // ---- §4.6.18.7.6 assembly ------------------------------------ - // §4.6.18.8.5: the low-power tool never smooths, regardless of - // bs_smoothing_mode. - let h_sl: usize = if p.smoothing_mode || p.low_power { - 0 - } else { - 4 - }; - - // GTemp / QTemp with the hSL-column prefix. - let mut g_temp = vec![vec![0.0f64; m_cnt]; n_cols + h_sl]; - let mut q_temp = vec![vec![0.0f64; m_cnt]; n_cols + h_sl]; - for j in 0..h_sl { - if st.g_temp_tail.len() == h_sl && st.g_temp_tail[j].len() == m_cnt { - g_temp[j].clone_from(&st.g_temp_tail[j]); - q_temp[j].clone_from(&st.q_temp_tail[j]); - } else { - // Reset (or first frame): prefix = first column values. - g_temp[j].clone_from(&g_lim_boost[0]); - q_temp[j].clone_from(&q_m_lim_boost[0]); - } - } - // Envelope of column i (spec index space i0..i_end). - let env_of = |i: i32| -> usize { - let mut l = l_e - 1; - for e in 0..l_e { - if i >= rate * p.t_e[e] && i < rate * p.t_e[e + 1] { - l = e; - break; - } - } - l - }; - for c in 0..n_cols { - let l = env_of(i0 + c as i32); - g_temp[c + h_sl].clone_from(&g_lim_boost[l]); - q_temp[c + h_sl].clone_from(&q_m_lim_boost[l]); - } - - // §4.6.18.8.5: the modified sinusoid equations apply to the first - // 16 sinusoids (in increasing frequency order) of every time - // segment; later sinusoids keep the original (real-part) term. - let lp_first16: Vec> = if p.low_power { - s_index - .iter() - .map(|row| { - let mut count = 0usize; - row.iter() - .map(|&on| { - if on { - count += 1; - count <= 16 - } else { - false - } - }) - .collect() - }) - .collect() - } else { - Vec::new() - }; - - let mut y = vec![[Complex::default(); 64]; x_high.len()]; - let mut f_index_noise = 0usize; - let mut f_index_sine = 0usize; - for c in 0..n_cols { - let i = i0 + c as i32; - let l = env_of(i); - let li = l as i32; - let col = (i + T_HF_ADJ as i32) as usize; - let smooth_gain = li != p.l_a && li != l_a_prev && h_sl != 0; - f_index_sine = (st.index_sine + c) % 4; - let (sin_re, sin_im) = PHI_SIN[f_index_sine]; - for m in 0..m_cnt { - let k = (k_x as usize) + m; - // GFilt. - let g_filt = if smooth_gain { - (0..=h_sl) - .map(|j| g_temp[c + h_sl - j][m] * H_SMOOTH[j]) - .sum::() - } else { - g_temp[c + h_sl][m] - }; - // QFilt: zero on transient envelopes and sinusoid bands. - let q_filt = if li == p.l_a || li == l_a_prev || s_m_boost[l][m] != 0.0 { - 0.0 - } else if h_sl != 0 { - (0..=h_sl) - .map(|j| q_temp[c + h_sl - j][m] * H_SMOOTH[j]) - .sum::() - } else { - q_temp[c + h_sl][m] - }; - - // W1 = GFilt · XHigh. - let w1 = x_high[col][k] * g_filt; - - // W2 = W1 + QFilt · V(fIndexNoise). The low-power tool - // ignores every imaginary part (§4.6.18.8.1). - f_index_noise = (st.index_noise + c * m_cnt + m + 1) % 512; - let (v_re, v_im) = NOISE_TABLE[f_index_noise]; - let mut out = if p.low_power { - Complex::new(w1.re + q_filt * v_re, 0.0) - } else { - Complex::new(w1.re + q_filt * v_re, w1.im + q_filt * v_im) - }; - - // Y = W2 + ψ (sinusoids; the low-power injection runs as - // a separate per-column pass below). - if !p.low_power && s_index[l][m] { - let s = s_m_boost[l][m]; - let alt = if (m + k_x as usize) % 2 == 1 { - -1.0 - } else { - 1.0 - }; - out.re += s * sin_re; - out.im += s * alt * sin_im; - } - y[col][k] = out; - } - - if p.low_power { - // §4.6.18.8.5 sinusoid injection: real-valued ψm with the - // −0.00815·(−1)^(m+kx) neighbour correction, over targets - // m ∈ −1..=M — spilling into the lowband subband kx − 1 - // and the subband kx + M just above the SBR range. - let phi_re_at = |off: i64| -> f64 { - let idx = (st.index_sine as i64 + c as i64 + off).rem_euclid(4) as usize; - PHI_SIN[idx].0 - }; - let f0 = phi_re_at(0); - let fm1 = phi_re_at(-1); - let fp1 = phi_re_at(1); - let first16 = &lp_first16[l]; - // ψRe of the (first-16) sinusoid in band m, else 0. - let s16 = |m: i64| -> f64 { - if m >= 0 && (m as usize) < m_cnt && first16[m as usize] { - s_m_boost[l][m as usize] - } else { - 0.0 - } - }; - for t in -1..=(m_cnt as i64) { - let band = i64::from(k_x) + t; - if !(0..64).contains(&band) { - continue; - } - let alt = if band.rem_euclid(2) == 1 { -1.0 } else { 1.0 }; - let psi = s16(t) * f0 - 0.00815 * alt * (s16(t - 1) * fm1 + s16(t + 1) * fp1); - if psi != 0.0 { - y[col][band as usize].re += psi; - } - } - // Sinusoids beyond the sixteenth keep the original term - // (real part only). - for (m, &on) in s_index[l].iter().enumerate() { - if on && !first16[m] { - y[col][(k_x as usize) + m].re += s_m_boost[l][m] * f0; - } - } - } - } - - // ---- thread cross-frame state -------------------------------- - st.index_noise = if n_cols > 0 { - f_index_noise - } else { - st.index_noise - }; - st.index_sine = if n_cols > 0 { - (f_index_sine + 1) % 4 - } else { - st.index_sine - }; - st.g_temp_tail = g_temp[n_cols..].to_vec(); - st.q_temp_tail = q_temp[n_cols..].to_vec(); - st.s_index_prev = s_index[l_e - 1].clone(); - st.k_x_prev = k_x; - st.l_a_prev_frame = p.l_a; - st.l_e_prev = l_e as i32; - - Ok(y) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn bands() -> HiLoTables { - HiLoTables { - f_table_high: vec![8, 10, 12, 14, 16], - f_table_low: vec![8, 12, 16], - f_table_noise: vec![8, 16], - m: 8, - k_x: 8, - } - } - - fn flat_x_high(amp: f64, cols: usize) -> Vec<[Complex; 64]> { - let mut x = vec![[Complex::default(); 64]; cols]; - for (ci, col) in x.iter_mut().enumerate() { - for (k, cell) in col.iter_mut().enumerate().take(16).skip(8) { - // A deterministic unit-magnitude phase pattern. - let ph = (ci * 7 + k) as f64 * 0.37; - *cell = Complex::new(amp * ph.cos(), amp * ph.sin()); - } - } - x - } - - #[allow(clippy::too_many_arguments)] - fn params<'a>( - b: &'a HiLoTables, - lim: &'a [i32], - t_e: &'a [i32], - t_q: &'a [i32], - freq_res: &'a [bool], - e_orig: &'a [Vec], - q_orig: &'a [Vec], - add: &'a [bool], - ) -> EnvParams<'a> { - EnvParams { - bands: b, - f_table_lim: lim, - t_e, - t_q, - freq_res, - l_a: -1, - e_orig, - q_orig, - add_harmonic: add, - interpol_freq: true, - smoothing_mode: true, - limiter_gains: 3, - reset: false, - low_power: false, - deg_patched: None, - } - } - - /// A flat XHigh with EOrig = G²·|X|² reproduces gain G on every - /// sample (no noise, no sinusoids, limiter wide open). - #[test] - fn flat_gain_reproduces_target_envelope() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let amp = 100.0; - let target_gain = 3.0; - // EOrig is an energy: G = sqrt(EOrig / (ε + |X|²)). - let e_target = target_gain * target_gain * (amp * amp + EPS); - let e_orig = vec![vec![e_target; 4]]; - let q_orig = vec![vec![0.0]]; - let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); - let x = flat_x_high(amp, 40); - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - // The boost ratio uses the raw energies (no ε), so the exact - // applied gain is target·sqrt((amp² + ε)/amp²); pin to 1e-3. - for c in 0..32usize { - let col = c + T_HF_ADJ; - for k in 8..16 { - let g = (y[col][k].norm_sqr() / x[col][k].norm_sqr()).sqrt(); - assert!( - (g - target_gain).abs() < 1e-3 * target_gain, - "col {col} k {k}: gain {g}" - ); - } - } - } - - /// Per-envelope gains switch exactly at the tE border. - #[test] - fn gain_switches_at_envelope_border() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 8, 16]; - let t_q = [0, 8, 16]; - let fr = [true, true]; - let amp = 50.0; - let e0 = 4.0 * (amp * amp + EPS); - let e1 = 25.0 * (amp * amp + EPS); - let e_orig = vec![vec![e0; 4], vec![e1; 4]]; - let q_orig = vec![vec![0.0], vec![0.0]]; - let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); - let x = flat_x_high(amp, 40); - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - // Slots 0..16 → gain 2; slots 16..32 → gain 5. - let g_at = |c: usize| { - let col = c + T_HF_ADJ; - (y[col][9].norm_sqr() / x[col][9].norm_sqr()).sqrt() - }; - assert!((g_at(3) - 2.0).abs() < 1e-2); - assert!((g_at(15) - 2.0).abs() < 1e-2); - assert!((g_at(16) - 5.0).abs() < 2e-2); - assert!((g_at(31) - 5.0).abs() < 2e-2); - } - - /// The limiter clamps a runaway per-subband gain to the - /// limiter-band average, and the boost compensates the band's - /// total energy (up to the 1.584893192 cap). - #[test] - fn limiter_clamps_and_boost_compensates() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - // Band 0 demands a huge gain (XHigh is tiny there), bands 1..4 - // are ordinary. limiter_gains = 1 → limGain = 1.0. - let amp = 10.0; - let mut x = flat_x_high(amp, 40); - for col in x.iter_mut() { - for cell in &mut col[8..10] { - *cell = *cell * 1e-6; - } - } - let e_orig = vec![vec![ - 400.0 * (amp * amp), - 400.0 * (amp * amp), - 400.0 * (amp * amp), - 400.0 * (amp * amp), - ]]; - let q_orig = vec![vec![0.0]]; - let mut p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); - p.limiter_gains = 1; - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - // Unclamped G in the dead band would be ≈ 2e7; the limiter-band - // average cap is far smaller, so the dead band's output stays - // bounded by GMax·|X| ≪ 1 with boost ≤ MAX_BOOST. - for c in 0..32usize { - let col = c + T_HF_ADJ; - assert!(y[col][8].norm_sqr() < 1.0); - // The healthy bands keep a finite, boosted gain. - assert!(y[col][12].norm_sqr().is_finite()); - } - } - - /// A pure noise band (XHigh = 0, QOrig ≫) synthesises Table 4.A.91 - /// noise at the QM level, and the running index threads across - /// frames. - #[test] - fn noise_floor_synthesis_and_index_threading() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let e_orig = vec![vec![64.0; 4]]; - let q_orig = vec![vec![1.0]]; // QMapped = 1 → QM = sqrt(64/2) - let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); - let x = vec![[Complex::default(); 64]; 40]; - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - // First sample: fIndexNoise = 0·8 + 0 + 1 = 1. - let qm = (64.0f64 * 1.0 / 2.0).sqrt(); - // Boost over the limiter band: num = Σ EOrig = 8·64, den = - // Σ QM² = 8·32 → GBoost = √2 (below the cap). - let expect = qm * 2.0f64.sqrt(); - let (v_re, v_im) = NOISE_TABLE[1]; - let got = y[T_HF_ADJ][8]; - assert!((got.re - expect * v_re).abs() < 1e-9, "{got:?}"); - assert!((got.im - expect * v_im).abs() < 1e-9); - // Last index this frame: (31·8 + 7 + 1) mod 512 = 256. - assert_eq!(st.index_noise, 256); - // Second frame continues from 256. - let y2 = adjust(&x, &p, &mut st).unwrap(); - let (v_re2, v_im2) = NOISE_TABLE[257]; - let got2 = y2[T_HF_ADJ][8]; - assert!((got2.re - expect * v_re2).abs() < 1e-9); - assert!((got2.im - expect * v_im2).abs() < 1e-9); - } - - /// An additional sinusoid lands in the middle subband of its - /// high-res band with the [1, 0, −1, 0] / (−1)^(m+kx) pattern and - /// the cross-frame indexSine advance. - #[test] - fn sinusoid_injection_pattern() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let e_orig = vec![vec![64.0; 4]]; - let q_orig = vec![vec![0.0]]; - // Harmonic in high band 1 → mid subband (10 + 12)/2 = 11. - let add = [false, true, false, false]; - let p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &add); - let x = vec![[Complex::default(); 64]; 40]; - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - let s = 64.0f64.sqrt() * MAX_BOOST; // SM boosted (ECurr = 0) - // m + kx = 11 (odd) → imaginary part sign-flipped. - // c = 0: φ = (1, 0); c = 1: φ = (0, 1) → im = −s. - let y0 = y[T_HF_ADJ][11]; - let y1 = y[T_HF_ADJ + 1][11]; - assert!((y0.re - s).abs() < 1e-9 && y0.im.abs() < 1e-12, "{y0:?}"); - assert!(y1.re.abs() < 1e-12 && (y1.im + s).abs() < 1e-9, "{y1:?}"); - // Other bands carry no sinusoid. - assert_eq!(y[T_HF_ADJ][9], Complex::default()); - // indexSine advances past the frame: (31 % 4 + 1) % 4 = 0. - assert_eq!(st.index_sine, 0); - // Next frame: still ringing (prev SIndexMapped carries over) - // even though l_a stays -1. - let y2 = adjust(&x, &p, &mut st).unwrap(); - assert!(y2[T_HF_ADJ][11].norm_sqr() > 0.0); - } - - /// Smoothing mode 0 (hSL = 4) filters a gain step across the - /// carry, and the second frame consumes the previous tail. - #[test] - fn smoothing_carries_across_frames() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let amp = 10.0; - let x = flat_x_high(amp, 40); - let e_lo = vec![vec![1.0 * (amp * amp + EPS); 4]]; - let e_hi = vec![vec![100.0 * (amp * amp + EPS); 4]]; - let q_orig = vec![vec![0.0]]; - let mut p1 = params(&b, &lim, &t_e, &t_q, &fr, &e_lo, &q_orig, &[]); - p1.smoothing_mode = false; - let mut st = EnvAdjustState::new(); - let _ = adjust(&x, &p1, &mut st).unwrap(); - assert_eq!(st.g_temp_tail.len(), 4); - // Second frame jumps to gain 10; the first output columns are - // still pulled down by the smoothing history (gain < 10). - let mut p2 = params(&b, &lim, &t_e, &t_q, &fr, &e_hi, &q_orig, &[]); - p2.smoothing_mode = false; - let y = adjust(&x, &p2, &mut st).unwrap(); - let g0 = (y[T_HF_ADJ][9].norm_sqr() / x[T_HF_ADJ][9].norm_sqr()).sqrt(); - let g_late = (y[T_HF_ADJ + 20][9].norm_sqr() / x[T_HF_ADJ + 20][9].norm_sqr()).sqrt(); - assert!(g0 < 6.0, "g0 = {g0}"); - assert!((g_late - 10.0).abs() < 0.1, "g_late = {g_late}"); - } - - /// Low-power mode requires `deg_patched`, doubles the energy - /// estimation (§4.6.18.8.4: with EOrig = G²·(2|X|² + ε) the flat - /// gain lands on G), and produces a purely real Y. - #[test] - fn low_power_energy_doubling_and_real_output() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let amp = 100.0; - let target_gain = 3.0; - let e_target = target_gain * target_gain * (2.0 * amp * amp + EPS); - let e_orig = vec![vec![e_target; 4]]; - let q_orig = vec![vec![0.0]]; - let dp = [0.0f64; 8]; - let mut p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &[]); - p.low_power = true; - // deg_patched is mandatory in low-power mode. - let x = flat_x_high(amp, 40); - let mut st = EnvAdjustState::new(); - assert!(adjust(&x, &p, &mut st).is_err()); - p.deg_patched = Some(&dp); - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - for c in 0..32usize { - let col = c + T_HF_ADJ; - for cell in &y[col][8..16] { - assert_eq!(cell.im, 0.0, "LP Y must be real"); - } - } - // Noise-free path: the applied gain is uniform on the real - // part; ECurr = 2·|X|², so G = √(EOrig/(ε + 2·amp²)) = 3. - let g = (y[T_HF_ADJ][12].re / x[T_HF_ADJ][12].re).abs(); - let expect = (e_target / (EPS + 2.0 * amp * amp)).sqrt(); - assert!( - (g - expect).abs() < 1e-3 * expect, - "LP gain {g} vs expected {expect}" - ); - } - - /// The §4.6.18.8.5 sinusoid injection: real-valued main term on - /// the φRe cycle, the −0.00815 neighbour corrections one subband - /// away (with the (−1)^band alternation), and no imaginary part. - #[test] - fn low_power_sinusoid_injection_pattern() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let e_orig = vec![vec![64.0; 4]]; - let q_orig = vec![vec![0.0]]; - // Harmonic in high band 1 → mid subband (10 + 12)/2 = 11. - let add = [false, true, false, false]; - let dp = [0.0f64; 8]; - let mut p = params(&b, &lim, &t_e, &t_q, &fr, &e_orig, &q_orig, &add); - p.low_power = true; - p.deg_patched = Some(&dp); - let x = vec![[Complex::default(); 64]; 40]; - let mut st = EnvAdjustState::new(); - let y = adjust(&x, &p, &mut st).unwrap(); - let s = 64.0f64.sqrt() * MAX_BOOST; // SM boosted (ECurr = 0) - - // c = 0: φRe(0) = 1 → main term s in band 11; φRe(±1) = 0 → - // no neighbour corrections. - assert!((y[T_HF_ADJ][11].re - s).abs() < 1e-9); - assert_eq!(y[T_HF_ADJ][11].im, 0.0); - assert_eq!(y[T_HF_ADJ][10].re, 0.0); - assert_eq!(y[T_HF_ADJ][12].re, 0.0); - - // c = 1: φRe(1) = 0 → no main term; band 10 sees the m+1 - // neighbour at i+1 (φRe(2) = −1): ψ = −0.00815·(+1)·(−s); - // band 12 sees the m−1 neighbour at i−1 (φRe(0) = 1): - // ψ = −0.00815·(+1)·(s). - let col1 = T_HF_ADJ + 1; - assert!(y[col1][11].re.abs() < 1e-12); - assert!( - (y[col1][10].re - 0.00815 * s).abs() < 1e-9, - "{}", - y[col1][10].re - ); - assert!( - (y[col1][12].re + 0.00815 * s).abs() < 1e-9, - "{}", - y[col1][12].re - ); - // Everything stays real. - for col in y.iter() { - for cell in col.iter() { - assert_eq!(cell.im, 0.0); - } - } - } - - /// LP mode never smooths: a gain step lands instantly even with - /// bs_smoothing_mode = 0, and the aliasing reduction equalizes a - /// full-degree group while preserving its output energy. - #[test] - fn low_power_no_smoothing_and_aliasing_reduction() { - let b = bands(); - let lim = [8, 16]; - let t_e = [0, 16]; - let t_q = [0, 16]; - let fr = [true]; - let amp = 10.0; - let x = flat_x_high(amp, 40); - let e_lo = vec![vec![2.0 * (amp * amp) + EPS; 4]]; - let e_hi = vec![vec![100.0 * (2.0 * (amp * amp) + EPS); 4]]; - let q_orig = vec![vec![0.0]]; - let dp = [0.0f64; 8]; - let mut p1 = params(&b, &lim, &t_e, &t_q, &fr, &e_lo, &q_orig, &[]); - p1.smoothing_mode = false; // requests smoothing… - p1.low_power = true; // …which LP overrides - p1.deg_patched = Some(&dp); - let mut st = EnvAdjustState::new(); - let _ = adjust(&x, &p1, &mut st).unwrap(); - // No smoothing tail is carried in LP mode. - assert!(st.g_temp_tail.is_empty()); - let mut p2 = params(&b, &lim, &t_e, &t_q, &fr, &e_hi, &q_orig, &[]); - p2.smoothing_mode = false; - p2.low_power = true; - p2.deg_patched = Some(&dp); - let y = adjust(&x, &p2, &mut st).unwrap(); - let g0 = (y[T_HF_ADJ][9].re / x[T_HF_ADJ][9].re).abs(); - assert!((g0 - 10.0).abs() < 0.5, "gain step not instant: {g0}"); - - // With a full-degree dp the group gains equalize but keep the - // envelope's output energy (checked via the flat spectrum). - let dp_full = [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]; - let e_skew = vec![vec![ - 1.0 * (2.0 * amp * amp + EPS), - 4.0 * (2.0 * amp * amp + EPS), - 9.0 * (2.0 * amp * amp + EPS), - 16.0 * (2.0 * amp * amp + EPS), - ]]; - let mut p3 = params(&b, &lim, &t_e, &t_q, &fr, &e_skew, &q_orig, &[]); - p3.low_power = true; - p3.deg_patched = Some(&dp_full); - let mut st3 = EnvAdjustState::new(); - let y3 = adjust(&x, &p3, &mut st3).unwrap(); - // Adjacent grouped subbands carry (near-)equal gains. - let g_at = |k: usize| (y3[T_HF_ADJ + 4][k].re / x[T_HF_ADJ + 4][k].re).abs(); - assert!( - (g_at(9) - g_at(10)).abs() < 1e-6 * g_at(9), - "grouped gains differ: {} vs {}", - g_at(9), - g_at(10) - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_envelope.rs b/crates/vendor/oxideav-aac/src/sbr_envelope.rs deleted file mode 100644 index 8d39c196..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_envelope.rs +++ /dev/null @@ -1,410 +0,0 @@ -//! `sbr_envelope()` / `sbr_noise()` raw decode — ISO/IEC 14496-3 -//! §4.4.2.8, Tables 4.72–4.73. -//! -//! These two elements carry the SBR spectral-envelope scalefactors and -//! noise-floor scalefactors as **delta values** (`bs_data_env` / -//! `bs_data_noise`). For each envelope (resp. noise floor) the delta -//! direction comes from `sbr_dtdf()` ([`crate::sbr_grid::SbrDtdf`]): -//! -//! * delta-in-**frequency** (`bs_df_* == 0`): the first band carries an -//! absolute *start value* read as a fixed-width field, and the -//! remaining bands are frequency-direction Huffman deltas (`f_huff`). -//! * delta-in-**time** (`bs_df_* == 1`): every band is a -//! time-direction Huffman delta (`t_huff`) relative to the -//! corresponding band of the previous envelope / noise floor. -//! -//! The start-value field widths (Table 4.72 / 4.73) depend on the -//! coupling / channel / amplitude-resolution context: -//! -//! | element | context | width | -//! |---------|---------|-------| -//! | envelope | coupling && ch, amp_res | 5 | -//! | envelope | coupling && ch, !amp_res | 6 | -//! | envelope | level, amp_res | 6 | -//! | envelope | level, !amp_res | 7 | -//! | noise | (any) | 5 | -//! -//! The per-envelope band count is `num_env_bands[bs_freq_res]` — the -//! high-resolution band count `NHigh` when the envelope's freq-res flag -//! is set, otherwise the low-resolution count `NLow` -//! ([`crate::sbr_freq_bands::HiLoTables`]). The noise band count is -//! `NQ` for every noise floor. -//! -//! This module produces the **raw** delta arrays exactly as written on -//! the wire; the §4.6.18.3.5 DPCM accumulation across bands / time and -//! the §4.6.18 dequantization to linear energies are downstream. - -use crate::sbr_grid::{SbrDtdf, SbrGrid}; -use crate::sbr_huffman::{env_tables, noise_tables, sbr_huff_dec, SbrHuffContext}; -use crate::{Error, Result}; -use oxideav_core::bits::BitReader; - -/// Raw `bs_data_env` for one channel: one delta vector per envelope. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrEnvelopeData { - /// `bs_data_env[env][band]` — the raw delta (or, at band 0 of a - /// frequency-coded envelope, the absolute start value). One inner - /// vector per envelope; its length is the envelope's band count. - pub data: Vec>, -} - -/// Raw `bs_data_noise` for one channel: one delta vector per noise -/// floor. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrNoiseData { - /// `bs_data_noise[noise][band]` — raw delta / start value. One - /// inner vector per noise floor; each has `NQ` entries. - pub data: Vec>, -} - -/// The number of envelope bands for a given freq-resolution flag: -/// `NHigh` (high res) or `NLow` (low res). -fn num_env_bands(bands: &crate::sbr_freq_bands::HiLoTables, high_res: bool) -> usize { - if high_res { - bands.n_high() - } else { - bands.n_low() - } -} - -impl SbrEnvelopeData { - /// Parse `sbr_envelope()` (Table 4.72) for one channel. - /// - /// * `grid` / `dtdf` are this channel's already-parsed grid and - /// delta-direction flags. - /// * `bands` supplies `NHigh` / `NLow` for the per-envelope band - /// counts. - /// * `coupling` is the element `bs_coupling`; `ch` is the channel - /// index within the element; `amp_res` is the *effective* - /// amplitude resolution (after any single-envelope FIXFIX - /// override). - pub fn parse( - reader: &mut BitReader<'_>, - grid: &SbrGrid, - dtdf: &SbrDtdf, - bands: &crate::sbr_freq_bands::HiLoTables, - coupling: bool, - ch: bool, - amp_res: bool, - ) -> Result { - let ctx = SbrHuffContext { - coupling, - ch, - amp_res, - }; - let ((t_huff, t_lav), (f_huff, f_lav)) = env_tables(ctx); - - // Start-value width per Table 4.72. - let start_bits = if coupling && ch { - if amp_res { - 5 - } else { - 6 - } - } else if amp_res { - 6 - } else { - 7 - }; - - let mut data = Vec::with_capacity(grid.num_env); - for env in 0..grid.num_env { - let n = num_env_bands(bands, grid.freq_res[env]); - let mut row = Vec::with_capacity(n); - if !dtdf.df_env[env] { - // Delta in frequency: band 0 is the absolute start - // value, bands 1.. are f_huff deltas. - let start = read(reader, start_bits)? as i32; - row.push(start); - for _ in 1..n { - row.push(sbr_huff_dec(reader, f_huff, f_lav)?); - } - } else { - // Delta in time: every band is a t_huff delta. - for _ in 0..n { - row.push(sbr_huff_dec(reader, t_huff, t_lav)?); - } - } - data.push(row); - } - Ok(SbrEnvelopeData { data }) - } -} - -impl SbrNoiseData { - /// Parse `sbr_noise()` (Table 4.73) for one channel. - /// - /// `num_noise_bands` is `NQ` - /// ([`crate::sbr_freq_bands::HiLoTables::n_q`]). The other - /// arguments mirror [`SbrEnvelopeData::parse`]; the noise start - /// value is always a 5-bit field (Table 4.73), regardless of - /// `amp_res`. - pub fn parse( - reader: &mut BitReader<'_>, - grid: &SbrGrid, - dtdf: &SbrDtdf, - num_noise_bands: usize, - coupling: bool, - ch: bool, - amp_res: bool, - ) -> Result { - let ctx = SbrHuffContext { - coupling, - ch, - amp_res, - }; - let ((t_huff, t_lav), (f_huff, f_lav)) = noise_tables(ctx); - - let mut data = Vec::with_capacity(grid.num_noise); - for noise in 0..grid.num_noise { - let mut row = Vec::with_capacity(num_noise_bands); - if !dtdf.df_noise[noise] { - // Delta in frequency: band 0 is a 5-bit absolute start - // value, bands 1.. are f_huff deltas. - let start = read(reader, 5)? as i32; - row.push(start); - for _ in 1..num_noise_bands { - row.push(sbr_huff_dec(reader, f_huff, f_lav)?); - } - } else { - for _ in 0..num_noise_bands { - row.push(sbr_huff_dec(reader, t_huff, t_lav)?); - } - } - data.push(row); - } - Ok(SbrNoiseData { data }) - } -} - -#[inline] -fn read(reader: &mut BitReader<'_>, n: u32) -> Result { - reader.read_u32(n).map_err(|_| Error::SbrHuffInvalid) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sbr_freq_bands::{k0, k2, master_table, HiLoTables}; - use crate::sbr_grid::FrameClass; - use oxideav_core::bits::{BitReader, BitWriter}; - - fn bands_44100() -> HiLoTables { - // The known-good 44.1 kHz linear geometry from sbr_freq_bands. - let k0v = k0(88_200, 5).unwrap(); - let k2v = k2(88_200, 5, k0v).unwrap(); - let fm = master_table(k0v, k2v, 0, false).unwrap(); - HiLoTables::derive(&fm, 1, 2).unwrap() - } - - /// Push the MSB-first codeword for one table entry into a writer. - fn push_code(w: &mut BitWriter, table: &[(u8, u32)], idx: usize) { - let (len, code) = table[idx]; - w.write_u32(code, len as u32); - } - - #[test] - fn envelope_freq_direction_one_env() { - let bands = bands_44100(); - // FIXFIX, single env, high-res freq, delta-in-frequency. - let grid = SbrGrid { - frame_class: FrameClass::FixFix, - num_env: 1, - num_noise: 1, - freq_res: vec![true], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: true, - }; - let dtdf = SbrDtdf { - df_env: vec![false], // frequency direction - df_noise: vec![false], - }; - // amp_res = false → level start width 7 bits. - let n = bands.n_high(); - let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - let mut w = BitWriter::new(); - w.write_u32(40, 7); // start value - // remaining n-1 bands: pick a few known indices. - let chosen: Vec = (1..n) - .map(|i| (i + f_lav as usize) % f_huff.len()) - .collect(); - for &idx in &chosen { - push_code(&mut w, f_huff, idx); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ev = SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, false, false, false).unwrap(); - assert_eq!(ev.data.len(), 1); - assert_eq!(ev.data[0].len(), n); - assert_eq!(ev.data[0][0], 40); - for (band, &idx) in chosen.iter().enumerate() { - assert_eq!(ev.data[0][band + 1], idx as i32 - f_lav); - } - } - - #[test] - fn envelope_time_direction() { - let bands = bands_44100(); - let grid = SbrGrid { - frame_class: FrameClass::FixFix, - num_env: 1, - num_noise: 1, - freq_res: vec![false], // low res - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: false, - }; - let dtdf = SbrDtdf { - df_env: vec![true], // time direction → no start value - df_noise: vec![false], - }; - let n = bands.n_low(); - let ((t_huff, t_lav), (_f, _fl)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - let mut w = BitWriter::new(); - let chosen: Vec = (0..n).map(|i| (i * 2 + 1) % t_huff.len()).collect(); - for &idx in &chosen { - push_code(&mut w, t_huff, idx); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ev = SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, false, false, false).unwrap(); - assert_eq!(ev.data[0].len(), n); - for (band, &idx) in chosen.iter().enumerate() { - assert_eq!(ev.data[0][band], idx as i32 - t_lav); - } - } - - #[test] - fn noise_freq_and_time() { - let bands = bands_44100(); - let nq = bands.n_q(); - let grid = SbrGrid { - frame_class: FrameClass::FixVar, - num_env: 2, - num_noise: 2, - freq_res: vec![true, true], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: false, - }; - let dtdf = SbrDtdf { - df_env: vec![false, false], - df_noise: vec![false, true], // floor 0 freq, floor 1 time - }; - let ((t_huff, t_lav), (f_huff, f_lav)) = noise_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - let mut w = BitWriter::new(); - // Floor 0: 5-bit start + (nq-1) f deltas. - w.write_u32(12, 5); - let f_chosen: Vec = (1..nq) - .map(|i| (i + f_lav as usize) % f_huff.len()) - .collect(); - for &idx in &f_chosen { - push_code(&mut w, f_huff, idx); - } - // Floor 1: nq t deltas. - let t_chosen: Vec = (0..nq) - .map(|i| (i + t_lav as usize) % t_huff.len()) - .collect(); - for &idx in &t_chosen { - push_code(&mut w, t_huff, idx); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let nd = SbrNoiseData::parse(&mut r, &grid, &dtdf, nq, false, false, false).unwrap(); - assert_eq!(nd.data.len(), 2); - assert_eq!(nd.data[0].len(), nq); - assert_eq!(nd.data[0][0], 12); - for (band, &idx) in f_chosen.iter().enumerate() { - assert_eq!(nd.data[0][band + 1], idx as i32 - f_lav); - } - for (band, &idx) in t_chosen.iter().enumerate() { - assert_eq!(nd.data[1][band], idx as i32 - t_lav); - } - } - - #[test] - fn coupling_balance_start_width() { - // Coupled second channel at 3.0 dB → balance start width 5. - let bands = bands_44100(); - let grid = SbrGrid { - frame_class: FrameClass::FixFix, - num_env: 1, - num_noise: 1, - freq_res: vec![true], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: false, - }; - let dtdf = SbrDtdf { - df_env: vec![false], - df_noise: vec![false], - }; - let n = bands.n_high(); - let ((_t, _tl), (f_huff, f_lav)) = env_tables(SbrHuffContext { - coupling: true, - ch: true, - amp_res: true, - }); - let mut w = BitWriter::new(); - w.write_u32(7, 5); // 5-bit balance start - for i in 1..n { - push_code(&mut w, f_huff, (i + f_lav as usize) % f_huff.len()); - } - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let ev = SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, true, true, true).unwrap(); - assert_eq!(ev.data[0][0], 7); - } - - #[test] - fn truncated_envelope_errors() { - let bands = bands_44100(); - let grid = SbrGrid { - frame_class: FrameClass::FixFix, - num_env: 1, - num_noise: 1, - freq_res: vec![true], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: false, - }; - let dtdf = SbrDtdf { - df_env: vec![false], - df_noise: vec![false], - }; - let bytes = [0u8; 0]; - let mut r = BitReader::new(&bytes); - assert!(matches!( - SbrEnvelopeData::parse(&mut r, &grid, &dtdf, &bands, false, false, false), - Err(Error::SbrHuffInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_extension.rs b/crates/vendor/oxideav-aac/src/sbr_extension.rs deleted file mode 100644 index 0129b473..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_extension.rs +++ /dev/null @@ -1,493 +0,0 @@ -//! `sbr_extension_data()` top-level walker — ISO/IEC 14496-3 §4.4.2.8 -//! Table 4.62. -//! -//! This is the glue between [`crate::extension_payload`] and the SBR -//! side-info parsers: it consumes a whole SBR extension payload from a -//! `fill_element()`'s `extension_payload()` body, in the exact spec -//! order: -//! -//! ```text -//! sbr_extension_data(id_aac, crc_flag) { -//! num_sbr_bits = 0; -//! if (crc_flag) { bs_sbr_crc_bits; 10 uimsbf num_sbr_bits += 10; } -//! // sbr_layer != SBR_STEREO_ENHANCE for a non-scalable core: -//! bs_header_flag; 1 uimsbf num_sbr_bits += 1; -//! if (bs_header_flag) num_sbr_bits += sbr_header(); -//! num_sbr_bits += sbr_data(id_aac, bs_amp_res); -//! num_align_bits = (8*cnt - 4 - num_sbr_bits) % 8; -//! bs_fill_bits; num_align_bits uimsbf -//! } -//! ``` -//! -//! `sbr_data(id_aac, bs_amp_res)` dispatches on the AAC element type the -//! SBR payload extends: an `ID_SCE` core element pairs with -//! `sbr_single_channel_element()` ([`SbrElement::parse_single`]), an -//! `ID_CPE` core element with `sbr_channel_pair_element()` -//! ([`SbrElement::parse_pair`]). The band tables both need are derived -//! from the active [`SbrHeader`] at the SBR *internal* sample rate -//! `fs_sbr` (twice the AAC core rate) via [`SbrHeader::derive_bands`]. -//! -//! ## Header reuse -//! -//! When `bs_header_flag == 0` the payload reuses the most recent -//! transmitted `sbr_header()`. The first SBR payload of a stream must -//! carry a header (`bs_header_flag == 1`); a clear flag with no prior -//! header is an ill-formed stream ([`Error::SbrFreqBandInvalid`]). The -//! caller threads the returned [`SbrExtensionData::header`] back in as -//! `prev_header` on the next payload so the reuse chain is continuous. -//! -//! ## Scope -//! -//! This decodes the SBR *bitstream* side info end to end (CRC field + -//! header + grid / dtdf / invf / envelope / noise / add-harmonic + -//! extended-data block). The SBR back-end DSP (dequantization to linear -//! energies, the QMF analysis / synthesis filterbanks, HF generation / -//! patching, the limiter, and the envelope adjustment that produces -//! up-sampled PCM) is **not** part of this walker — it keys off the -//! band tables and scalefactors this produces. The `bs_sbr_crc_bits` -//! value is captured along with its §4.4.2.8.1 coverage region (the -//! `num_sbr_bits − 10` payload bits after the CRC field); callers that -//! own the payload buffer verify it via -//! [`SbrExtensionData::verify_crc`] (the decode drivers do). -//! -//! ## Clean-room provenance -//! -//! The Table 4.62 syntax, the `num_align_bits = (8·cnt − 4 − -//! num_sbr_bits) % 8` fill computation, and the `sbr_data` dispatch on -//! `id_aac` are transcribed from ISO/IEC 14496-3:2009 §4.4.2.8 staged -//! under `docs/audio/aac/`. The non-scalable core fixes the helper -//! `sbr_layer` to `SBR_NOT_SCALABLE` (Table 4.62 Note 1), so the -//! `bs_header_flag` is always present. - -use oxideav_core::bits::BitReader; - -use crate::raw_data_block::IdSynEle; -use crate::sbr_element::SbrElement; -use crate::sbr_header::SbrHeader; -use crate::{Error, Result}; - -/// Field width of `bs_sbr_crc_bits` (Table 4.62). -pub const SBR_CRC_BITS: u32 = 10; - -/// A fully-parsed `sbr_extension_data()` payload (Table 4.62). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrExtensionData { - /// `bs_sbr_crc_bits` (10-bit) when `crc_flag` was set (the - /// `EXT_SBR_DATA_CRC` extension type); `None` for the plain - /// `EXT_SBR_DATA` type. Verify with [`Self::verify_crc`]. - pub crc: Option, - /// The protected bit range `[start, end)` — absolute positions in - /// the buffer the parsing [`BitReader`] was constructed over — - /// covering every `sbr_extension_data()` bit after the CRC field - /// up to the end of `sbr_data()` (the §4.4.2.8.1 coverage region, - /// `num_sbr_bits − 10` bits). `None` when no CRC was present. - pub crc_region: Option<(u64, u64)>, - /// `bs_header_flag` — whether this payload transmitted a fresh - /// `sbr_header()`. - pub header_present: bool, - /// The active SBR header for this payload: the freshly parsed one - /// when `header_present`, otherwise the reused `prev_header`. The - /// caller threads this forward as the next payload's `prev_header`. - pub header: SbrHeader, - /// The decoded SBR data element (single channel or channel pair), - /// dispatched on the core element's `id_aac`. - pub element: SbrElement, - /// The number of SBR side-info bits consumed before the trailing - /// `bs_fill_bits` (the spec's `num_sbr_bits`). Useful for callers - /// validating against the `extension_payload()` byte count. - pub num_sbr_bits: u64, -} - -impl SbrExtensionData { - /// Parse an `sbr_extension_data(id_aac, crc_flag)` payload (Table - /// 4.62) from `reader`, positioned at the first SBR bit (i.e. the - /// caller — [`crate::extension_payload`] — has already consumed the - /// 4-bit `extension_type`). - /// - /// * `id_aac` — the AAC core element this SBR payload extends: only - /// [`IdSynEle::Sce`] / [`IdSynEle::Cpe`] are valid (an SBR payload - /// only attaches to a channel element). Any other id is rejected - /// with [`Error::SbrFreqBandInvalid`]. - /// * `crc_flag` — `true` for the `EXT_SBR_DATA_CRC` extension type - /// (a 10-bit `bs_sbr_crc_bits` field precedes the header), `false` - /// for plain `EXT_SBR_DATA`. - /// * `fs_sbr` — the SBR *internal* sample rate (twice the AAC core - /// `samplingFrequencyIndex` rate). Drives [`SbrHeader::derive_bands`]. - /// * `cnt` — the `extension_payload()` byte count `cnt` (Table 4.51), - /// used to size the trailing `bs_fill_bits` alignment. Pass `None` - /// to skip the fill consumption (when the caller bounds the reader - /// itself); the fill is then left in the reader. - /// * `prev_header` — the most recent transmitted header for the reuse - /// path; `None` on the stream's first SBR payload. A clear - /// `bs_header_flag` with `prev_header == None` is ill-formed. - pub fn parse( - reader: &mut BitReader<'_>, - id_aac: IdSynEle, - crc_flag: bool, - fs_sbr: u32, - cnt: Option, - prev_header: Option, - ) -> Result { - let start = reader.bit_position(); - - let crc = if crc_flag { - Some(read(reader, SBR_CRC_BITS)? as u16) - } else { - None - }; - let region_start = reader.bit_position(); - - // Non-scalable core ⇒ sbr_layer == SBR_NOT_SCALABLE, so the - // bs_header_flag is always present (Table 4.62 Note 1). - let header_present = read_flag(reader)?; - Self::finish( - reader, - id_aac, - crc, - start, - region_start, - header_present, - prev_header, - fs_sbr, - cnt, - ) - } - - /// [`SbrExtensionData::parse`] for a caller that has already - /// consumed the `extension_type` nibble, the optional 10-bit CRC - /// field, **and** a set `bs_header_flag` (the pre-header probe in - /// [`crate::extension_payload::ExtensionPayload::parse_with_sbr`]). - /// `nibble_start` is the bit position of the `extension_type` - /// nibble, from which the CRC coverage region and the Table 4.62 - /// `num_sbr_bits` accounting are reconstructed. - #[allow(clippy::too_many_arguments)] - pub fn parse_after_prefix( - reader: &mut BitReader<'_>, - id_aac: IdSynEle, - crc: Option, - nibble_start: u64, - fs_sbr: u32, - cnt: Option, - prev_header: Option, - ) -> Result { - let start = nibble_start + 4; - let region_start = start - + if crc.is_some() { - u64::from(SBR_CRC_BITS) - } else { - 0 - }; - Self::finish( - reader, - id_aac, - crc, - start, - region_start, - true, - prev_header, - fs_sbr, - cnt, - ) - } - - /// Shared tail of the two parse entries: `sbr_header()` (when - /// present), band derivation, `sbr_data()`, and the Table 4.62 - /// `bs_fill_bits` alignment. - #[allow(clippy::too_many_arguments)] - fn finish( - reader: &mut BitReader<'_>, - id_aac: IdSynEle, - crc: Option, - start: u64, - region_start: u64, - header_present: bool, - prev_header: Option, - fs_sbr: u32, - cnt: Option, - ) -> Result { - let header = if header_present { - SbrHeader::parse(reader)? - } else { - // Reuse the previous transmitted header. A stream that - // opens with header-less SBR payloads is the §4.5.2.8.1 - // "upsampling and delay adjustment only" state — the - // parse_with_sbr caller intercepts that case before - // reaching here, so a missing header at this point is a - // caller-contract violation. - prev_header.ok_or(Error::SbrFreqBandInvalid)? - }; - - // sbr_data(id_aac, bs_amp_res): the band tables are derived from - // the active header at the SBR internal rate; the element type is - // selected by the core element id_aac. - let bands = header.derive_bands(fs_sbr)?; - let element = match id_aac { - IdSynEle::Sce => SbrElement::parse_single(reader, &bands, header.amp_res)?, - IdSynEle::Cpe => SbrElement::parse_pair(reader, &bands, header.amp_res)?, - _ => return Err(Error::SbrFreqBandInvalid), - }; - - let region_end = reader.bit_position(); - let num_sbr_bits = region_end - start; - - // num_align_bits = (8*cnt - 4 - num_sbr_bits) % 8. The `- 4` - // accounts for the extension_type nibble the caller already read; - // when cnt is known, consume the trailing bs_fill_bits so the - // reader lands on the next extension_payload element. - let mut crc_end = region_end; - if let Some(cnt) = cnt { - let total = u64::from(cnt) * 8; - let consumed = num_sbr_bits + 4; // + the extension_type nibble - if total < consumed { - return Err(Error::SbrFreqBandInvalid); - } - let align = (total - consumed) % 8; - if align > 0 { - read(reader, align as u32)?; - } - // §4.5.2.8.1: "The checksum shall be calculated covering - // the whole SBR data range including possible - // bs_fill_bits" — the coverage extends past the end of - // sbr_data() through the alignment padding to the end of - // the fill payload. Confirmed against the ISO/IEC - // 14496-26 `al_sbr_*` type-14 vectors, whose - // header-bearing payloads carry non-zero bs_fill_bits and - // only verify over the padded region. - // (`start` is 4 bits past the extension_type nibble; the - // grouping avoids u64 underflow when a caller parses a - // nibble-less buffer from position 0.) - crc_end = start + (total - 4); - } - - Ok(SbrExtensionData { - crc, - crc_region: crc.map(|_| (region_start, crc_end)), - header_present, - header, - element, - num_sbr_bits, - }) - } - - /// Verify the `bs_sbr_crc_bits` checksum against the §4.5.2.8.1 - /// coverage region (every payload bit after the CRC field to the - /// end of the fill payload — "the whole SBR data range including - /// possible bs_fill_bits"). - /// - /// `data` must be the same byte buffer the parsing [`BitReader`] - /// was constructed over ([`Self::crc_region`] holds absolute bit - /// positions into it). A payload without a CRC field (plain - /// `EXT_SBR_DATA`) verifies vacuously. Returns - /// [`Error::SbrCrcMismatch`] when the recomputed 10-bit `G10` - /// (zero-init) CRC disagrees with the transmitted value. - pub fn verify_crc(&self, data: &[u8]) -> Result<()> { - if let (Some(crc), Some((start, end))) = (self.crc, self.crc_region) { - if crate::adts_crc::sbr_crc(data, start, end) != crc { - return Err(Error::SbrCrcMismatch); - } - } - Ok(()) - } -} - -#[inline] -fn read(reader: &mut BitReader<'_>, n: u32) -> Result { - reader.read_u32(n).map_err(|_| Error::SbrFreqBandInvalid) -} - -#[inline] -fn read_flag(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::SbrFreqBandInvalid) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sbr_freq_bands::HiLoTables; - use crate::sbr_grid::FrameClass; - use crate::sbr_huffman::{env_tables, noise_tables, SbrHuffContext}; - use oxideav_core::bits::BitWriter; - - const FS_SBR: u32 = 88_200; // 44.1 kHz core, doubled. - - /// A header carrying explicit extra-1 params (freq_scale 0, - /// alter_scale false, noise_bands 2) so the derived band geometry is - /// deterministic; extra-2 absent. - fn write_header(w: &mut BitWriter, amp_res: bool) { - w.write_bit(amp_res); // bs_amp_res - w.write_u32(5, 4); // bs_start_freq - w.write_u32(0, 4); // bs_stop_freq - w.write_u32(1, 3); // bs_xover_band - w.write_u32(0, 2); // bs_reserved - w.write_bit(true); // bs_header_extra_1 - w.write_bit(false); // bs_header_extra_2 - w.write_u32(0, 2); // bs_freq_scale - w.write_bit(false); // bs_alter_scale - w.write_u32(2, 2); // bs_noise_bands - } - - /// The band tables a `write_header(_, _)`-built header derives. - fn header_bands() -> HiLoTables { - let mut w = BitWriter::new(); - write_header(&mut w, false); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let h = SbrHeader::parse(&mut r).unwrap(); - h.derive_bands(FS_SBR).unwrap() - } - - fn push_code(w: &mut BitWriter, table: &[(u8, u32)], idx: usize) { - let (len, code) = table[idx]; - w.write_u32(code, len as u32); - } - - /// Minimal single-channel SBR element body (FIXFIX single env, freq - /// deltas, no sinusoidal / extended data). Mirrors the - /// `sbr_element` test helper but inline so the band geometry comes - /// from the header we just wrote. - fn write_minimal_sce(w: &mut BitWriter, bands: &HiLoTables) { - let n_high = bands.n_high(); - let n_q = bands.n_q(); - w.write_bit(false); // bs_data_extra - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(0, 2); // 2^0 = 1 env - w.write_bit(true); // freq_res[0] high - w.write_bit(false); // df_env[0] - w.write_bit(false); // df_noise[0] - for _ in 0..n_q { - w.write_u32(1, 2); // invf modes - } - let (_, (f_huff, f_lav)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - w.write_u32(33, 7); // env start value (amp_res override → 7-bit) - for i in 1..n_high { - push_code(w, f_huff, (i + f_lav as usize) % f_huff.len()); - } - let (_, (nf, nfl)) = noise_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - w.write_u32(10, 5); // noise start - for i in 1..n_q { - push_code(w, nf, (i + nfl as usize) % nf.len()); - } - w.write_bit(false); // bs_add_harmonic_flag - w.write_bit(false); // bs_extended_data - } - - #[test] - fn parses_header_plus_single_channel() { - let bands = header_bands(); - let mut w = BitWriter::new(); - w.write_bit(true); // bs_header_flag - write_header(&mut w, true); - write_minimal_sce(&mut w, &bands); - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let sbr = - SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, None, None).unwrap(); - assert!(sbr.header_present); - assert!(sbr.crc.is_none()); - assert_eq!(sbr.header.start_freq, 5); - assert_eq!(sbr.header.freq_scale, 0); - assert!(!sbr.element.coupling); - assert_eq!(sbr.element.channels.len(), 1); - assert_eq!(sbr.element.channels[0].envelope.data[0][0], 33); - assert_eq!(sbr.element.channels[0].noise.data[0][0], 10); - } - - #[test] - fn crc_flag_reads_ten_bit_field() { - let bands = header_bands(); - let mut w = BitWriter::new(); - w.write_u32(0x2A5, SBR_CRC_BITS); // bs_sbr_crc_bits - w.write_bit(true); // bs_header_flag - write_header(&mut w, true); - write_minimal_sce(&mut w, &bands); - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let sbr = SbrExtensionData::parse(&mut r, IdSynEle::Sce, true, FS_SBR, None, None).unwrap(); - assert_eq!(sbr.crc, Some(0x2A5)); - assert!(sbr.header_present); - } - - #[test] - fn header_reuse_when_flag_clear() { - // A prior header is reused when bs_header_flag == 0. - let bands = header_bands(); - let prev = { - let mut w = BitWriter::new(); - write_header(&mut w, true); - let bytes = w.finish(); - SbrHeader::parse(&mut BitReader::new(&bytes)).unwrap() - }; - let mut w = BitWriter::new(); - w.write_bit(false); // bs_header_flag clear - write_minimal_sce(&mut w, &bands); - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let sbr = SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, None, Some(prev)) - .unwrap(); - assert!(!sbr.header_present); - assert_eq!(sbr.header, prev); - assert_eq!(sbr.element.channels.len(), 1); - } - - #[test] - fn header_clear_without_prior_is_error() { - let mut w = BitWriter::new(); - w.write_bit(false); // bs_header_flag clear, no prior header - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(matches!( - SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, None, None), - Err(Error::SbrFreqBandInvalid) - )); - } - - #[test] - fn non_channel_id_aac_is_rejected() { - let mut w = BitWriter::new(); - w.write_bit(true); - write_header(&mut w, true); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - assert!(matches!( - SbrExtensionData::parse(&mut r, IdSynEle::Lfe, false, FS_SBR, None, None), - Err(Error::SbrFreqBandInvalid) - )); - } - - #[test] - fn fill_bits_consumed_when_cnt_given() { - // Pad the payload to a known byte count and confirm the walker - // consumes the trailing bs_fill_bits so the reader is byte-aligned - // at `cnt` bytes (minus the extension_type nibble the caller owns). - let bands = header_bands(); - let mut w = BitWriter::new(); - w.write_bit(true); - write_header(&mut w, true); - write_minimal_sce(&mut w, &bands); - let mut body = w.finish(); - // cnt counts whole bytes of the extension_payload including its - // 4-bit type nibble; add two trailing fill bytes so there is a - // non-trivial bs_fill_bits to swallow and the reader has the bits. - let cnt = (body.len() + 2) as u32; - body.extend_from_slice(&[0u8, 0u8]); - let mut r = BitReader::new(&body); - let before = r.bit_position(); - let sbr = - SbrExtensionData::parse(&mut r, IdSynEle::Sce, false, FS_SBR, Some(cnt), None).unwrap(); - let consumed = r.bit_position() - before; - // Total consumed (+ the 4-bit type nibble) must be a multiple of 8. - assert_eq!((consumed + 4) % 8, 0); - assert_eq!(sbr.element.channels.len(), 1); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs b/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs deleted file mode 100644 index 507583ff..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_freq_bands.rs +++ /dev/null @@ -1,753 +0,0 @@ -//! SBR frequency band tables — ISO/IEC 14496-3 §4.6.18.3.2. -//! -//! Spectral Band Replication groups the QMF subbands in frequency by a -//! family of *frequency band tables*. Everything is derived from one -//! **master** table `fMaster`, which is in turn fixed by two QMF subband -//! boundaries — the low boundary `k0` and the high boundary `k2` — and -//! the header data elements `bs_freq_scale` / `bs_alter_scale`. -//! -//! This module implements the *static* (header-only) half of the band -//! setup, i.e. everything that does **not** depend on the §4.6.18.6 QMF -//! patching / high-frequency-generation back-end: -//! -//! * [`k0`] — §4.6.18.3.2.1 low boundary `k0 = startMin + -//! offset(bs_start_freq)`, with the per-`FsSBR` `offset` table and the -//! `startMin = NINT(c · 128 / FsSBR)` thresholds. -//! * [`k2`] — §4.6.18.3.2.1 high boundary, including the -//! `bs_stop_freq < 14` `stopDkSort` accumulation path and the -//! `bs_stop_freq == 14 / 15` `min(64, 2·k0)` / `min(64, 3·k0)` -//! shortcuts. -//! * [`master_table`] — §4.6.18.3.2.1 `fMaster` (Figure 4.39 for -//! `bs_freq_scale == 0`, Figure 4.40 for `bs_freq_scale > 0`). -//! * [`HiLoTables::derive`] — §4.6.18.3.2.2 `fTableHigh`, `fTableLow`, -//! `fTableNoise`, plus the `M` / `k_x` outputs every later SBR stage -//! keys off. -//! -//! ## Scope -//! -//! * The §4.6.18.3.2.3 limiter band table `fTableLim` is **not** here: -//! for `bs_limiter_bands > 0` it consumes the `patchBorders` / -//! `patchNumSubbands` produced by §4.6.18.6, which needs the QMF -//! patching back-end this crate does not have yet. The -//! `bs_limiter_bands == 0` single-band case -//! (`{fTableLow(0), fTableLow(NLow)}`) is trivially derivable from -//! [`HiLoTables`] and is left to the limiter pass. -//! * The actual envelope decode, noise-floor decode, and QMF synthesis -//! are downstream of these tables. -//! -//! ## Operators -//! -//! The spec's `INT()` truncates toward zero and `NINT()` rounds to the -//! nearest integer with halves away from zero (ISO/IEC 14496-3 §4.6.18, -//! reusing the §4 `INT` / `NINT` definitions). The arguments here are -//! always non-negative, so `INT` is a plain floor and the `NINT` helper -//! adds `0.5` before truncating. - -use crate::{Error, Result}; - -/// §4.6.18.3.2.1 nearest-integer operator (`NINT`): round to the nearest -/// integer, halves away from zero. All call sites in this module pass a -/// finite, non-negative argument. -#[inline] -fn nint(x: f64) -> i32 { - // Halves away from zero: for x >= 0 this is floor(x + 0.5); the sign - // branch keeps the helper correct for any finite input. - if x >= 0.0 { - (x + 0.5).floor() as i32 - } else { - (x - 0.5).ceil() as i32 - } -} - -/// §4 `INT` operator: truncation toward zero. The arguments in this -/// module are always non-negative, so this is a plain `floor`. -#[inline] -fn int_trunc(x: f64) -> i32 { - x.trunc() as i32 -} - -/// The `offset(bs_start_freq)` row for an `FsSBR` value, per the -/// §4.6.18.3.2.1 `offset` table. Returns `None` for an `FsSBR` outside -/// the tabulated set (the spec only defines rows for the standard SBR -/// internal sample rates). -fn offset_row(fs_sbr: u32) -> Option<&'static [i32; 16]> { - // FsSBR is twice the core sample rate; the table is keyed by the - // SBR internal rate directly. - const OFF_16: [i32; 16] = [-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]; - const OFF_22: [i32; 16] = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13]; - const OFF_24: [i32; 16] = [-5, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16]; - const OFF_32: [i32; 16] = [-6, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16]; - const OFF_44: [i32; 16] = [-4, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16, 20]; - const OFF_64: [i32; 16] = [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 16, 20, 24]; - - match fs_sbr { - 16000 => Some(&OFF_16), - 22050 => Some(&OFF_22), - 24000 => Some(&OFF_24), - 32000 => Some(&OFF_32), - // `44100 <= FsSBR <= 64000` shares one row. - 44100 | 48000 | 64000 => Some(&OFF_44), - // `FsSBR > 64000`. - 88200 | 96000 | 128000 | 176400 | 192000 => Some(&OFF_64), - _ => None, - } -} - -/// §4.6.18.3.2.1 `startMin = NINT(c · 128 / FsSBR)`, with the three -/// `c ∈ {3000, 4000, 5000}` bands keyed by `FsSBR`. -fn start_min(fs_sbr: u32) -> i32 { - let fs = fs_sbr as f64; - let c = if fs_sbr < 32000 { - 3000.0 - } else if fs_sbr < 64000 { - 4000.0 - } else { - 5000.0 - }; - nint(c * 128.0 / fs) -} - -/// §4.6.18.3.2.1 `stopMin = NINT(c · 128 / FsSBR)`, with the three -/// `c ∈ {6000, 8000, 10000}` bands keyed by `FsSBR`. -fn stop_min(fs_sbr: u32) -> i32 { - let fs = fs_sbr as f64; - let c = if fs_sbr < 32000 { - 6000.0 - } else if fs_sbr < 64000 { - 8000.0 - } else { - 10000.0 - }; - nint(c * 128.0 / fs) -} - -/// §4.6.18.3.2.1 low boundary `k0`. -/// -/// `k0 = startMin + offset(bs_start_freq)`. `bs_start_freq` is a 4-bit -/// header field (`0 ..= 15`); `fs_sbr` must be one of the tabulated SBR -/// internal sample rates. Returns [`Error::SbrFreqBandInvalid`] for an -/// out-of-range `bs_start_freq` or an unsupported `fs_sbr`. -pub fn k0(fs_sbr: u32, bs_start_freq: u8) -> Result { - let row = offset_row(fs_sbr).ok_or(Error::SbrFreqBandInvalid)?; - let idx = bs_start_freq as usize; - if idx >= row.len() { - return Err(Error::SbrFreqBandInvalid); - } - Ok(start_min(fs_sbr) + row[idx]) -} - -/// §4.6.18.3.2.1 high boundary `k2`. -/// -/// For `0 <= bs_stop_freq < 14` this is -/// `min(64, stopMin + Σ_{i Result { - if bs_stop_freq > 15 { - return Err(Error::SbrFreqBandInvalid); - } - let val = match bs_stop_freq { - 14 => (2 * k0_val).min(64), - 15 => (3 * k0_val).min(64), - _ => { - let stop_min_v = stop_min(fs_sbr); - if stop_min_v <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let ratio = 64.0 / stop_min_v as f64; - // stopDk(p), 0 <= p <= 12 -> 13 entries. - let mut stop_dk = [0i32; 13]; - for (p, slot) in stop_dk.iter_mut().enumerate() { - let hi = nint(stop_min_v as f64 * ratio.powf((p as f64 + 1.0) / 13.0)); - let lo = nint(stop_min_v as f64 * ratio.powf(p as f64 / 13.0)); - *slot = hi - lo; - } - stop_dk.sort_unstable(); - // stopMin + Σ_{i=0}^{bs_stop_freq-1} stopDkSort(i). - let mut acc = stop_min_v; - for &dk in stop_dk.iter().take(bs_stop_freq as usize) { - acc += dk; - } - acc.min(64) - } - }; - Ok(val) -} - -/// §4.6.18.3.2.1 master frequency band table `fMaster`. -/// -/// Implements Figure 4.39 (`bs_freq_scale == 0`) and Figure 4.40 -/// (`bs_freq_scale > 0`). The returned vector is `fMaster(0..=NMaster)`, -/// so `NMaster == len() - 1`. `fMaster` is only defined for `k2 > k0`; -/// `numBands > 0` and the §4.6.18.3.6 `vDk > 0` requirements are checked. -/// -/// * `bs_freq_scale ∈ {0, 1, 2, 3}` (0 = no warping/linear, -/// 1/2/3 select `bands ∈ {12, 10, 8}`). -/// * `bs_alter_scale ∈ {0, 1}`. -pub fn master_table( - k0_val: i32, - k2_val: i32, - bs_freq_scale: u8, - bs_alter_scale: bool, -) -> Result> { - if k2_val <= k0_val || bs_freq_scale > 3 { - return Err(Error::SbrFreqBandInvalid); - } - - if bs_freq_scale == 0 { - master_linear(k0_val, k2_val, bs_alter_scale) - } else { - master_warped(k0_val, k2_val, bs_freq_scale, bs_alter_scale) - } -} - -/// Figure 4.39 — `fMaster` for `bs_freq_scale == 0`. -fn master_linear(k0_val: i32, k2_val: i32, bs_alter_scale: bool) -> Result> { - let (dk, num_bands) = if !bs_alter_scale { - let dk = 1; - // numBands = 2 * INT( (k2 - k0) / (dk * 2) ) - ( - dk, - 2 * int_trunc((k2_val - k0_val) as f64 / (dk as f64 * 2.0)), - ) - } else { - let dk = 2; - // numBands = 2 * NINT( (k2 - k0) / (dk * 2) ) - (dk, 2 * nint((k2_val - k0_val) as f64 / (dk as f64 * 2.0))) - }; - if num_bands <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let num_bands = num_bands as usize; - - let mut v_dk = vec![dk; num_bands]; - let k2_achieved = k0_val + num_bands as i32 * dk; - let mut k2_diff = k2_val - k2_achieved; - - if k2_diff != 0 { - // incr / k start, then walk while k2Diff != 0. - let (incr, mut k): (i32, isize) = if k2_diff < 0 { - (1, 0) - } else { - (-1, num_bands as isize - 1) - }; - while k2_diff != 0 { - v_dk[k as usize] -= incr; - k += incr as isize; - k2_diff += incr; - } - } - - // fMaster(0) = k0; fMaster(k) = fMaster(k-1) + vDk[k-1]. - let mut f_master = Vec::with_capacity(num_bands + 1); - f_master.push(k0_val); - for &d in &v_dk { - // §4.6.18.3.6: numBands > 0 is checked above; the away-from-zero - // walk above can drive a vDk entry to 0 only on malformed input. - if d <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let next = *f_master.last().unwrap() + d; - f_master.push(next); - } - Ok(f_master) -} - -/// Figure 4.40 — `fMaster` for `bs_freq_scale > 0`. -fn master_warped( - k0_val: i32, - k2_val: i32, - bs_freq_scale: u8, - bs_alter_scale: bool, -) -> Result> { - // temp1 = {12, 10, 8}; bands = temp1[bs_freq_scale - 1]. - let bands = [12.0, 10.0, 8.0][(bs_freq_scale - 1) as usize]; - // temp2 = {1.0, 1.3}; warp = temp2[bs_alter_scale]. - let warp = if bs_alter_scale { 1.3 } else { 1.0 }; - - let (two_regions, k1) = if (k2_val as f64) / (k0_val as f64) > 2.2449 { - (true, 2 * k0_val) - } else { - (false, k2_val) - }; - - // Lower region. - let v_k0 = warped_region(k0_val, k1, bands, 1.0)?; - let num_bands0 = v_k0.len() - 1; - - if !two_regions { - return Ok(v_k0); - } - - // Upper region with warping. The §4.6.18.3.6 "min(vDk1) < max(vDk0)" - // smoothing step is part of warped_region_upper. - let max_v_dk0 = max_step(&v_k0); - let v_k1 = warped_region_upper(k1, k2_val, bands, warp, max_v_dk0)?; - let num_bands1 = v_k1.len() - 1; - - // fMaster: vk0[0..=numBands0] then vk1[1..=numBands1]. - let mut f_master = Vec::with_capacity(num_bands0 + num_bands1 + 1); - f_master.extend_from_slice(&v_k0); - f_master.extend_from_slice(&v_k1[1..]); - Ok(f_master) -} - -/// Largest forward step `vDk[k] = vk[k+1] - vk[k]` of a `vk` vector. -fn max_step(v_k: &[i32]) -> i32 { - v_k.windows(2).map(|w| w[1] - w[0]).max().unwrap_or(0) -} - -/// Figure 4.40 lower-region builder: produces `vk0` (or, for the -/// `twoRegions == 0` case, the whole `fMaster`). -/// -/// `numBands0 = 2 * NINT( bands * log(k1/k0) / (2 * log(2) * warp) )` -/// (the lower region always passes `warp = 1`), then -/// `vDk0[k] = NINT(k0 * (k1/k0)^((k+1)/numBands0)) − NINT(k0 * -/// (k1/k0)^(k/numBands0))`, sorted ascending, cumulatively summed from -/// `k0`. -fn warped_region(k_lo: i32, k_hi: i32, bands: f64, warp: f64) -> Result> { - let ratio = k_hi as f64 / k_lo as f64; - let num_bands = 2 * nint(bands * ratio.ln() / (2.0 * 2.0_f64.ln() * warp)); - if num_bands <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let num_bands = num_bands as usize; - - let mut v_dk = vec![0i32; num_bands]; - for (k, slot) in v_dk.iter_mut().enumerate() { - let hi = nint(k_lo as f64 * ratio.powf((k as f64 + 1.0) / num_bands as f64)); - let lo = nint(k_lo as f64 * ratio.powf(k as f64 / num_bands as f64)); - *slot = hi - lo; - } - v_dk.sort_unstable(); - - let mut v_k = Vec::with_capacity(num_bands + 1); - v_k.push(k_lo); - for &d in &v_dk { - // §4.6.18.3.6: vDk0(i) > 0 ∀ i. - if d <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let next = *v_k.last().unwrap() + d; - v_k.push(next); - } - Ok(v_k) -} - -/// Figure 4.40 upper-region builder with the `min(vDk1) < max(vDk0)` -/// smoothing branch. -fn warped_region_upper( - k1: i32, - k2_val: i32, - bands: f64, - warp: f64, - max_v_dk0: i32, -) -> Result> { - let ratio = k2_val as f64 / k1 as f64; - // numBands1 = 2 * NINT(bands * log(k2/k1) / (2 * log(2) * warp)) - let num_bands1 = 2 * nint(bands * ratio.ln() / (2.0 * 2.0_f64.ln() * warp)); - if num_bands1 <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let num_bands1 = num_bands1 as usize; - - let mut v_dk1 = vec![0i32; num_bands1]; - for (k, slot) in v_dk1.iter_mut().enumerate() { - let hi = nint(k1 as f64 * ratio.powf((k as f64 + 1.0) / num_bands1 as f64)); - let lo = nint(k1 as f64 * ratio.powf(k as f64 / num_bands1 as f64)); - *slot = hi - lo; - } - - // if min(vDk1) < max(vDk0): sort, then redistribute `change` from the - // largest to the smallest entry (capped at half the spread). - if v_dk1.iter().copied().min().unwrap_or(0) < max_v_dk0 { - v_dk1.sort_unstable(); - let mut change = max_v_dk0 - v_dk1[0]; - let half = int_trunc((v_dk1[num_bands1 - 1] - v_dk1[0]) as f64 / 2.0); - if change > half { - change = half; - } - v_dk1[0] += change; - v_dk1[num_bands1 - 1] -= change; - } - v_dk1.sort_unstable(); - - let mut v_k1 = Vec::with_capacity(num_bands1 + 1); - v_k1.push(k1); - for &d in &v_dk1 { - // §4.6.18.3.6: vDk1(i) > 0 ∀ i. - if d <= 0 { - return Err(Error::SbrFreqBandInvalid); - } - let next = *v_k1.last().unwrap() + d; - v_k1.push(next); - } - Ok(v_k1) -} - -/// §4.6.18.3.2.2 derived high / low / noise frequency band tables, plus -/// the `M` (number of QMF subbands covered by SBR) and `k_x` (first SBR -/// subband) outputs that the envelope / noise / patching stages key off. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HiLoTables { - /// `fTableHigh(0..=NHigh)` — high-resolution envelope band borders. - pub f_table_high: Vec, - /// `fTableLow(0..=NLow)` — low-resolution envelope band borders. - pub f_table_low: Vec, - /// `fTableNoise(0..=NQ)` — noise-floor band borders. - pub f_table_noise: Vec, - /// `M = fTableHigh(NHigh) − fTableHigh(0)` — number of QMF subbands - /// covered by SBR. - pub m: i32, - /// `k_x = fTableHigh(0)` — index of the first QMF subband in the SBR - /// range. - pub k_x: i32, -} - -impl HiLoTables { - /// `NHigh = len(fTableHigh) - 1`. - #[inline] - pub fn n_high(&self) -> usize { - self.f_table_high.len() - 1 - } - - /// `NLow = len(fTableLow) - 1`. - #[inline] - pub fn n_low(&self) -> usize { - self.f_table_low.len() - 1 - } - - /// `NQ = len(fTableNoise) - 1`. - #[inline] - pub fn n_q(&self) -> usize { - self.f_table_noise.len() - 1 - } - - /// §4.6.18.3.2.2 derive `fTableHigh` / `fTableLow` / `fTableNoise` - /// from a master table. - /// - /// `f_master` is `fMaster(0..=NMaster)` (i.e. [`master_table`]'s - /// output). `bs_xover_band` must satisfy `bs_xover_band < NMaster` - /// (§4.6.18.3.6). `bs_noise_bands ∈ {0, 1, 2, 3}`. - pub fn derive(f_master: &[i32], bs_xover_band: u8, bs_noise_bands: u8) -> Result { - if f_master.len() < 2 || bs_noise_bands > 3 { - return Err(Error::SbrFreqBandInvalid); - } - let n_master = f_master.len() - 1; - let xover = bs_xover_band as usize; - // bs_xover_band < NMaster (§4.6.18.3.6). - if xover >= n_master { - return Err(Error::SbrFreqBandInvalid); - } - - // NHigh = NMaster - bs_xover_band. - let n_high = n_master - xover; - // fTableHigh(k) = fMaster(k + bs_xover_band), 0 <= k <= NHigh. - let f_table_high: Vec = f_master[xover..=n_master].to_vec(); - debug_assert_eq!(f_table_high.len(), n_high + 1); - - // M = fTableHigh(NHigh) - fTableHigh(0); k_x = fTableHigh(0). - let k_x = f_table_high[0]; - let m = f_table_high[n_high] - k_x; - - // NLow = INT(NHigh/2) + (NHigh - 2*INT(NHigh/2)). - let half = n_high / 2; - let n_low = half + (n_high - 2 * half); - - // fTableLow(k) = fTableHigh(i(k)): - // i(0) = 0; i(k) = 2*k - ((1 - (-1)^NHigh)/2) for k != 0. - let parity = (1 - if n_high % 2 == 0 { 1 } else { -1 }) / 2; // 0 if NHigh even, 1 if odd - let mut f_table_low = Vec::with_capacity(n_low + 1); - for k in 0..=n_low { - let i_k = if k == 0 { - 0 - } else { - (2 * k as isize - parity as isize) as usize - }; - f_table_low.push(*f_table_high.get(i_k).ok_or(Error::SbrFreqBandInvalid)?); - } - - // NQ = max(1, NINT(bs_noise_bands * log2(k2/k_x))), where - // k2 == fTableLow(NLow) (the high boundary of the SBR range). - let k2_range = f_table_low[n_low]; - let n_q = if bs_noise_bands == 0 { - 1usize - } else { - let val = nint(bs_noise_bands as f64 * ((k2_range as f64 / k_x as f64).log2())); - val.max(1) as usize - }; - - // fTableNoise(0) = fTableLow(0); for k != 0: - // i(k) = i(k-1) + INT((NLow - i(k-1)) / (NQ + 1 - k)). - let mut f_table_noise = Vec::with_capacity(n_q + 1); - let mut i_prev: usize = 0; - f_table_noise.push(f_table_low[0]); - for k in 1..=n_q { - let denom = (n_q + 1 - k) as f64; - let step = int_trunc((n_low - i_prev) as f64 / denom); - i_prev += step as usize; - f_table_noise.push(*f_table_low.get(i_prev).ok_or(Error::SbrFreqBandInvalid)?); - } - - Ok(HiLoTables { - f_table_high, - f_table_low, - f_table_noise, - m, - k_x, - }) - } -} - -#[cfg(test)] -mod tests { - //! Truth is the ISO/IEC 14496-3 §4.6.18.3.2 closed-form algorithm - //! (Figures 4.39 / 4.40 and the §4.6.18.3.2.2 derivations). Each - //! expected value below is computed by hand from those formulas for - //! a specific `(FsSBR, bs_start_freq, bs_stop_freq, bs_freq_scale, - //! …)` parameter set; no external decoder is consulted. - - use super::*; - - #[test] - fn nint_rounds_half_away_from_zero() { - assert_eq!(nint(2.5), 3); - assert_eq!(nint(2.4), 2); - assert_eq!(nint(2.6), 3); - assert_eq!(nint(0.5), 1); - assert_eq!(nint(3.0), 3); - } - - #[test] - fn start_stop_min_44100() { - // 44.1 kHz core -> FsSBR = 88200 (> 64000): c = 5000 / 10000. - // startMin = NINT(5000 * 128 / 88200) = NINT(7.256...) = 7. - assert_eq!(start_min(88200), 7); - // stopMin = NINT(10000 * 128 / 88200) = NINT(14.51...) = 15. - assert_eq!(stop_min(88200), 15); - } - - #[test] - fn start_min_band_thresholds() { - // FsSBR < 32000 -> c = 3000. FsSBR = 24000: - // NINT(3000 * 128 / 24000) = NINT(16.0) = 16. - assert_eq!(start_min(24000), 16); - // 32000 <= FsSBR < 64000 -> c = 4000. FsSBR = 44100: - // NINT(4000 * 128 / 44100) = NINT(11.61...) = 12. - assert_eq!(start_min(44100), 12); - } - - #[test] - fn k0_24khz_start_freq_5() { - // FsSBR = 24000 -> startMin = 16, offset row OFF_24. - // offset(5) = 1 -> k0 = 17. - assert_eq!(k0(24000, 5).unwrap(), 17); - // offset(0) = -5 -> k0 = 11. - assert_eq!(k0(24000, 0).unwrap(), 11); - } - - #[test] - fn k0_rejects_bad_inputs() { - // bs_start_freq out of 0..=15 (would need a 5-bit field). - assert_eq!(k0(24000, 16), Err(Error::SbrFreqBandInvalid)); - // Unsupported FsSBR. - assert_eq!(k0(11025, 0), Err(Error::SbrFreqBandInvalid)); - } - - #[test] - fn k2_shortcuts() { - // bs_stop_freq == 14 -> min(64, 2*k0). - assert_eq!(k2(88200, 14, 10).unwrap(), 20); - assert_eq!(k2(88200, 14, 40).unwrap(), 64); // capped - // bs_stop_freq == 15 -> min(64, 3*k0). - assert_eq!(k2(88200, 15, 10).unwrap(), 30); - assert_eq!(k2(88200, 15, 30).unwrap(), 64); // capped - } - - #[test] - fn k2_accumulation_bs_stop_freq_0() { - // bs_stop_freq == 0 -> empty sum -> k2 = min(64, stopMin). - // FsSBR = 88200 -> stopMin = 15. - assert_eq!(k2(88200, 0, 7).unwrap(), 15); - } - - #[test] - fn k2_accumulation_is_monotone() { - // As bs_stop_freq grows, k2 is non-decreasing (stopDkSort >= 0 - // and the sum accumulates) and capped at 64. - let mut prev = k2(88200, 0, 7).unwrap(); - for bsf in 1..14 { - let cur = k2(88200, bsf, 7).unwrap(); - assert!(cur >= prev, "k2 dropped at bs_stop_freq={bsf}"); - assert!(cur <= 64); - prev = cur; - } - } - - #[test] - fn master_linear_simple() { - // bs_freq_scale = 0, bs_alter_scale = 0 -> dk = 1, every band - // width 1. k0 = 5, k2 = 13 -> numBands = 2*INT(8/2) = 8, - // k2Achieved = 13, k2Diff = 0 -> fMaster = 5..=13. - let fm = master_table(5, 13, 0, false).unwrap(); - assert_eq!(fm, vec![5, 6, 7, 8, 9, 10, 11, 12, 13]); - } - - #[test] - fn master_linear_with_remainder() { - // k0 = 5, k2 = 12 -> numBands = 2*INT(7/2) = 6, - // k2Achieved = 11, k2Diff = 1 > 0 -> incr = -1, k starts at 5: - // bump the last band by +1. fMaster spans 5..=12, 6 bands. - let fm = master_table(5, 12, 0, false).unwrap(); - assert_eq!(*fm.first().unwrap(), 5); - assert_eq!(*fm.last().unwrap(), 12); - assert_eq!(fm.len(), 7); // numBands + 1 - // Strictly increasing (all vDk > 0). - assert!(fm.windows(2).all(|w| w[1] > w[0])); - } - - #[test] - fn master_linear_alter_scale_dk2() { - // bs_alter_scale = 1 -> dk = 2. - // k0 = 4, k2 = 16 -> numBands = 2*NINT(12/4) = 6, - // k2Achieved = 4 + 6*2 = 16, k2Diff = 0 -> 6 bands of width 2. - let fm = master_table(4, 16, 0, true).unwrap(); - assert_eq!(fm, vec![4, 6, 8, 10, 12, 14, 16]); - } - - #[test] - fn master_rejects_k2_le_k0() { - assert_eq!( - master_table(20, 20, 0, false), - Err(Error::SbrFreqBandInvalid) - ); - assert_eq!( - master_table(20, 10, 1, false), - Err(Error::SbrFreqBandInvalid) - ); - } - - #[test] - fn master_warped_single_region_monotone() { - // k2/k0 = 28/14 = 2.0 <= 2.2449 -> single region. - // bs_freq_scale = 1 -> bands = 12. The §4.6.18.3.6 `vDk0(i) > 0` - // requirement holds for this range, so the table is well-defined, - // strictly increasing, and spans [k0, k2]. - let fm = master_table(14, 28, 1, false).unwrap(); - assert_eq!(*fm.first().unwrap(), 14); - assert_eq!(*fm.last().unwrap(), 28); - assert!(fm.windows(2).all(|w| w[1] > w[0])); - } - - #[test] - fn master_warped_two_region_monotone() { - // k2/k0 = 32/12 ≈ 2.667 > 2.2449 -> two regions, k1 = 2*k0 = 24. - // bs_freq_scale = 2 -> bands = 10 (the §4.6.18.3.6 `vDk > 0` - // requirement holds for both regions at this geometry). - let fm = master_table(12, 32, 2, false).unwrap(); - assert_eq!(*fm.first().unwrap(), 12); - assert_eq!(*fm.last().unwrap(), 32); - assert!(fm.windows(2).all(|w| w[1] > w[0])); - // Crossover region boundary k1 = 2*k0 = 24 must be a border. - assert!(fm.contains(&24)); - } - - #[test] - fn derive_high_low_noise_geometry() { - // Build a clean linear master, then derive. - // k0 = 5, k2 = 13 -> fMaster = 5..=13 (NMaster = 8). - let fm = master_table(5, 13, 0, false).unwrap(); - let t = HiLoTables::derive(&fm, 2, 2).unwrap(); - - // NHigh = NMaster - xover = 8 - 2 = 6. - assert_eq!(t.n_high(), 6); - // fTableHigh = fMaster[2..=8] = 7..=13. - assert_eq!(t.f_table_high, vec![7, 8, 9, 10, 11, 12, 13]); - // k_x = 7, M = 13 - 7 = 6. - assert_eq!(t.k_x, 7); - assert_eq!(t.m, 6); - - // NHigh = 6 (even): NLow = INT(6/2) + (6 - 2*3) = 3. - assert_eq!(t.n_low(), 3); - // parity = 0 (NHigh even): i(k) = 2k. fTableLow = high[0,2,4,6]. - assert_eq!(t.f_table_low, vec![7, 9, 11, 13]); - - // fTableLow(0) is always the first noise border; tables strictly - // increasing; last border == k2 of the range. - assert_eq!(t.f_table_noise[0], 7); - assert_eq!(*t.f_table_noise.last().unwrap(), 13); - assert!(t.f_table_noise.windows(2).all(|w| w[1] > w[0])); - } - - #[test] - fn derive_odd_nhigh_parity() { - // Force an odd NHigh. k0 = 5, k2 = 12 -> fMaster has 7 entries - // (NMaster = 6); xover = 1 -> NHigh = 5 (odd). - let fm = master_table(5, 12, 0, false).unwrap(); - let t = HiLoTables::derive(&fm, 1, 1).unwrap(); - assert_eq!(t.n_high(), 5); - // NHigh odd: NLow = INT(5/2) + (5 - 2*2) = 2 + 1 = 3. - assert_eq!(t.n_low(), 3); - // parity = 1: i(0)=0, i(k) = 2k - 1 -> high[0,1,3,5]. - let h = &t.f_table_high; - assert_eq!(t.f_table_low, vec![h[0], h[1], h[3], h[5]]); - } - - #[test] - fn derive_noise_bands_zero_single_band() { - let fm = master_table(5, 13, 0, false).unwrap(); - let t = HiLoTables::derive(&fm, 2, 0).unwrap(); - // bs_noise_bands == 0 -> NQ = 1 (two borders). - assert_eq!(t.n_q(), 1); - assert_eq!(t.f_table_noise.len(), 2); - assert_eq!(t.f_table_noise[0], t.f_table_low[0]); - assert_eq!( - *t.f_table_noise.last().unwrap(), - *t.f_table_low.last().unwrap() - ); - } - - #[test] - fn derive_rejects_xover_ge_nmaster() { - let fm = master_table(5, 13, 0, false).unwrap(); // NMaster = 8 - assert_eq!( - HiLoTables::derive(&fm, 8, 1), - Err(Error::SbrFreqBandInvalid) - ); - assert_eq!( - HiLoTables::derive(&fm, 9, 1), - Err(Error::SbrFreqBandInvalid) - ); - } - - #[test] - fn end_to_end_44100_typical() { - // An HE-AAC 44.1 kHz config wired end-to-end from FsSBR through - // the derived tables: - // FsSBR = 88200, bs_start_freq = 5, bs_stop_freq = 5, - // bs_freq_scale = 0 (linear), bs_alter_scale = 0, - // bs_xover_band = 1, bs_noise_bands = 2. - // Linear scale (bs_freq_scale == 0) is chosen here because it is - // well-defined for every k0/k2 pair; the warped scale is exercised - // by the dedicated single/two-region tests, which pick geometries - // that satisfy the §4.6.18.3.6 `vDk0(i) > 0` requirement. - let k0v = k0(88200, 5).unwrap(); - let k2v = k2(88200, 5, k0v).unwrap(); - assert!(k2v > k0v); - let fm = master_table(k0v, k2v, 0, false).unwrap(); - let t = HiLoTables::derive(&fm, 1, 2).unwrap(); - // k_x = fTableHigh(0) = fMaster(bs_xover_band); M spans from there - // to the top of the master table. The geometry is self-consistent. - assert_eq!(t.k_x, fm[1]); - assert_eq!(t.m, fm[fm.len() - 1] - fm[1]); - // §4.6.18.3.6: k_x <= 32 and k_x + M <= 64. - assert!(t.k_x <= 32); - assert!(t.k_x + t.m <= 64); - // Every derived table is strictly increasing. - assert!(t.f_table_high.windows(2).all(|w| w[1] > w[0])); - assert!(t.f_table_low.windows(2).all(|w| w[1] > w[0])); - assert!(t.f_table_noise.windows(2).all(|w| w[1] > w[0])); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_grid.rs b/crates/vendor/oxideav-aac/src/sbr_grid.rs deleted file mode 100644 index 5992b0ba..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_grid.rs +++ /dev/null @@ -1,464 +0,0 @@ -//! `sbr_grid()` / `sbr_dtdf()` / `sbr_invf()` — ISO/IEC 14496-3 -//! §4.4.2.8, Tables 4.69–4.71. -//! -//! The SBR time-frequency grid describes how a frame's QMF time slots -//! are partitioned into SBR *envelopes* and *noise floors*, and which -//! frequency resolution (high / low) each envelope uses. It is the -//! variable-length heart of an SBR data element: `sbr_envelope()` and -//! `sbr_noise()` are sized entirely by the grid (`bs_num_env` envelopes -//! and `bs_num_noise` noise floors). -//! -//! Four frame classes (Table 4.69) describe the slot layout: -//! -//! * `FIXFIX` (0) — a fixed number of equal-length envelopes -//! (`bs_num_env = 2^bs_num_env_raw`, the raw value being a 2-bit -//! field). A single envelope forces `bs_amp_res = 0`. All envelopes -//! share one transmitted frequency resolution. -//! * `FIXVAR` (1) — a fixed leading border plus a variable trailing -//! border list; envelopes are counted by `bs_num_rel_1 + 1` and the -//! frequency-resolution flags are transmitted in reverse order. -//! * `VARFIX` (2) — a variable leading border plus a fixed trailing -//! border; envelopes counted by `bs_num_rel_0 + 1`, freq-res in -//! forward order. -//! * `VARVAR` (3) — both borders variable; envelopes counted by -//! `bs_num_rel_0 + bs_num_rel_1 + 1`. -//! -//! For the variable classes the *envelope-count pointer* `bs_pointer` -//! is read as `ptr_bits = ceil(log2(bs_num_env + 1))` bits (Table 4.69 -//! Note 2: a true float log, not a truncated one). -//! -//! After the class-specific body, `bs_num_noise = (bs_num_env > 1) ? 2 -//! : 1`. -//! -//! `sbr_dtdf()` (Table 4.70) reads one delta-direction flag per -//! envelope (`bs_df_env`) and per noise floor (`bs_df_noise`): -//! `false` = delta in frequency (the first band is an absolute start -//! value), `true` = delta in time. -//! -//! `sbr_invf()` (Table 4.71) reads a 2-bit inverse-filtering mode per -//! noise band (`NQ`, taken from the derived noise band table). -//! -//! All three are fixed-/variable-width *syntax* only — no Huffman — so -//! they are fully recoverable from the spec tables. The actual border -//! reconstruction, envelope dequantization, and QMF synthesis are -//! downstream of this parse. - -use crate::{Error, Result}; -use oxideav_core::bits::BitReader; - -/// SBR frame class (`bs_frame_class`, Table 4.69 switch). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FrameClass { - /// `FIXFIX` (0) — fixed start, fixed stop; equal-length envelopes. - FixFix, - /// `FIXVAR` (1) — fixed start, variable stop. - FixVar, - /// `VARFIX` (2) — variable start, fixed stop. - VarFix, - /// `VARVAR` (3) — variable start, variable stop. - VarVar, -} - -impl FrameClass { - fn from_bits(v: u32) -> Self { - match v & 0b11 { - 0 => FrameClass::FixFix, - 1 => FrameClass::FixVar, - 2 => FrameClass::VarFix, - _ => FrameClass::VarVar, - } - } - - /// The 2-bit `bs_frame_class` wire value. - pub fn to_bits(self) -> u32 { - match self { - FrameClass::FixFix => 0, - FrameClass::FixVar => 1, - FrameClass::VarFix => 2, - FrameClass::VarVar => 3, - } - } -} - -/// The maximum number of SBR envelopes per frame (§4.6.18.3.6). Used to -/// bound the variable border lists so a corrupt grid cannot allocate -/// without limit. -pub const SBR_MAX_NUM_ENV: usize = 5; - -/// A parsed `sbr_grid()` (Table 4.69) for one channel. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrGrid { - /// `bs_frame_class`. - pub frame_class: FrameClass, - /// `bs_num_env[ch]` — number of envelopes in this frame. - pub num_env: usize, - /// `bs_num_noise[ch]` — number of noise floors (`1` or `2`). - pub num_noise: usize, - /// `bs_freq_res[ch][env]` — per-envelope frequency-resolution flag - /// (`true` = high resolution). Length is [`Self::num_env`]. - pub freq_res: Vec, - /// `bs_var_bord_0[ch]` — variable leading border (VARFIX / VARVAR), - /// else `0`. - pub var_bord_0: u8, - /// `bs_var_bord_1[ch]` — variable trailing border (FIXVAR / - /// VARVAR), else `0`. - pub var_bord_1: u8, - /// `bs_rel_bord_0[ch][..]` — relative leading borders (VARFIX / - /// VARVAR). Each element is the *raw* 2-bit value; the reconstructed - /// border is `2·raw + 2`. - pub rel_bord_0: Vec, - /// `bs_rel_bord_1[ch][..]` — relative trailing borders (FIXVAR / - /// VARVAR). Raw 2-bit values; reconstructed `2·raw + 2`. - pub rel_bord_1: Vec, - /// `bs_pointer[ch]` — the envelope-count pointer for the variable - /// classes (`0` for FIXFIX). - pub pointer: u32, - /// Whether this grid forced `bs_amp_res = 0` (single-envelope - /// FIXFIX). The caller applies this override to the element-level - /// `bs_amp_res`. - pub amp_res_override: bool, -} - -/// `ptr_bits = ceil(log2(num_env + 1))` (Table 4.69 Note 2: a true -/// float division / log, not a truncated one). For `num_env + 1` a -/// power of two this is exactly `log2`; otherwise it rounds up. -fn ptr_bits(num_env: usize) -> u32 { - let n = (num_env + 1) as u32; - // ceil(log2(n)): the position of the highest set bit, plus one if n - // is not itself a power of two. - if n <= 1 { - 0 - } else { - let floor_log2 = 31 - n.leading_zeros(); - if n.is_power_of_two() { - floor_log2 - } else { - floor_log2 + 1 - } - } -} - -impl SbrGrid { - /// Parse `sbr_grid()` (Table 4.69) for channel `ch` from `reader`. - /// - /// `num_env` is bounded by [`SBR_MAX_NUM_ENV`]; a value beyond it - /// (only reachable for a corrupt VARVAR grid) yields - /// [`Error::SbrGridInvalid`]. - pub fn parse(reader: &mut BitReader<'_>) -> Result { - let frame_class = FrameClass::from_bits(read(reader, 2)?); - let mut var_bord_0 = 0u8; - let mut var_bord_1 = 0u8; - let mut rel_bord_0: Vec = Vec::new(); - let mut rel_bord_1: Vec = Vec::new(); - let mut pointer = 0u32; - let mut amp_res_override = false; - - let (num_env, freq_res) = match frame_class { - FrameClass::FixFix => { - let raw = read(reader, 2)?; - let num_env = 1usize << raw; // bs_num_env = 2^tmp. - check_num_env(num_env)?; - if num_env == 1 { - amp_res_override = true; // bs_amp_res = 0. - } - let fr0 = read_flag(reader)?; - // All envelopes share bs_freq_res[ch][0]. - let freq_res = vec![fr0; num_env]; - (num_env, freq_res) - } - FrameClass::FixVar => { - var_bord_1 = read(reader, 2)? as u8; - let num_rel_1 = read(reader, 2)? as usize; - let num_env = num_rel_1 + 1; - check_num_env(num_env)?; - for _ in 0..num_env - 1 { - rel_bord_1.push(read(reader, 2)? as u8); - } - pointer = read(reader, ptr_bits(num_env))?; - // Frequency-resolution flags transmitted in reverse: - // bs_freq_res[ch][num_env - 1 - env]. - let mut freq_res = vec![false; num_env]; - for env in 0..num_env { - freq_res[num_env - 1 - env] = read_flag(reader)?; - } - (num_env, freq_res) - } - FrameClass::VarFix => { - var_bord_0 = read(reader, 2)? as u8; - let num_rel_0 = read(reader, 2)? as usize; - let num_env = num_rel_0 + 1; - check_num_env(num_env)?; - for _ in 0..num_env - 1 { - rel_bord_0.push(read(reader, 2)? as u8); - } - pointer = read(reader, ptr_bits(num_env))?; - // Forward order. - let mut freq_res = Vec::with_capacity(num_env); - for _ in 0..num_env { - freq_res.push(read_flag(reader)?); - } - (num_env, freq_res) - } - FrameClass::VarVar => { - var_bord_0 = read(reader, 2)? as u8; - var_bord_1 = read(reader, 2)? as u8; - let num_rel_0 = read(reader, 2)? as usize; - let num_rel_1 = read(reader, 2)? as usize; - let num_env = num_rel_0 + num_rel_1 + 1; - check_num_env(num_env)?; - for _ in 0..num_rel_0 { - rel_bord_0.push(read(reader, 2)? as u8); - } - for _ in 0..num_rel_1 { - rel_bord_1.push(read(reader, 2)? as u8); - } - pointer = read(reader, ptr_bits(num_env))?; - let mut freq_res = Vec::with_capacity(num_env); - for _ in 0..num_env { - freq_res.push(read_flag(reader)?); - } - (num_env, freq_res) - } - }; - - let num_noise = if num_env > 1 { 2 } else { 1 }; - - Ok(SbrGrid { - frame_class, - num_env, - num_noise, - freq_res, - var_bord_0, - var_bord_1, - rel_bord_0, - rel_bord_1, - pointer, - amp_res_override, - }) - } -} - -/// `sbr_dtdf()` (Table 4.70) — the delta-coding direction flags for a -/// channel's envelopes and noise floors. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrDtdf { - /// `bs_df_env[ch][env]` — `false` = delta in frequency (absolute - /// start band), `true` = delta in time. Length = `num_env`. - pub df_env: Vec, - /// `bs_df_noise[ch][noise]` — same convention. Length = `num_noise`. - pub df_noise: Vec, -} - -impl SbrDtdf { - /// Parse `sbr_dtdf()` (Table 4.70). `num_env` / `num_noise` come - /// from the channel's already-parsed [`SbrGrid`]. - pub fn parse(reader: &mut BitReader<'_>, num_env: usize, num_noise: usize) -> Result { - let mut df_env = Vec::with_capacity(num_env); - for _ in 0..num_env { - df_env.push(read_flag(reader)?); - } - let mut df_noise = Vec::with_capacity(num_noise); - for _ in 0..num_noise { - df_noise.push(read_flag(reader)?); - } - Ok(SbrDtdf { df_env, df_noise }) - } -} - -/// `sbr_invf()` (Table 4.71) — the 2-bit inverse-filtering mode per -/// noise band. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SbrInvf { - /// `bs_invf_mode[ch][n]` — one mode (0..=3) per noise band (`NQ`). - pub invf_mode: Vec, -} - -impl SbrInvf { - /// Parse `sbr_invf()` (Table 4.71). `num_noise_bands` is `NQ` from - /// the derived noise band table - /// ([`crate::sbr_freq_bands::HiLoTables::n_q`]). - pub fn parse(reader: &mut BitReader<'_>, num_noise_bands: usize) -> Result { - let mut invf_mode = Vec::with_capacity(num_noise_bands); - for _ in 0..num_noise_bands { - invf_mode.push(read(reader, 2)? as u8); - } - Ok(SbrInvf { invf_mode }) - } -} - -#[inline] -fn read(reader: &mut BitReader<'_>, n: u32) -> Result { - reader.read_u32(n).map_err(|_| Error::SbrGridInvalid) -} - -#[inline] -fn read_flag(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::SbrGridInvalid) -} - -#[inline] -fn check_num_env(num_env: usize) -> Result<()> { - if num_env == 0 || num_env > SBR_MAX_NUM_ENV { - Err(Error::SbrGridInvalid) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::BitWriter; - - #[test] - fn ptr_bits_matches_ceil_log2() { - // ceil(log2(n+1)) for n = num_env. - assert_eq!(ptr_bits(1), 1); // ceil(log2 2) = 1 - assert_eq!(ptr_bits(2), 2); // ceil(log2 3) = 2 - assert_eq!(ptr_bits(3), 2); // ceil(log2 4) = 2 - assert_eq!(ptr_bits(4), 3); // ceil(log2 5) = 3 - assert_eq!(ptr_bits(5), 3); // ceil(log2 6) = 3 - } - - #[test] - fn fixfix_single_env_forces_amp_res() { - let mut w = BitWriter::new(); - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(0, 2); // 2^0 = 1 envelope - w.write_bit(true); // freq_res[0] - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let g = SbrGrid::parse(&mut r).unwrap(); - assert_eq!(g.frame_class, FrameClass::FixFix); - assert_eq!(g.num_env, 1); - assert_eq!(g.num_noise, 1); - assert_eq!(g.freq_res, vec![true]); - assert!(g.amp_res_override); - } - - #[test] - fn fixfix_four_env_shares_freq_res() { - let mut w = BitWriter::new(); - w.write_u32(FrameClass::FixFix.to_bits(), 2); - w.write_u32(2, 2); // 2^2 = 4 envelopes - w.write_bit(false); // freq_res[0] shared by all - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let g = SbrGrid::parse(&mut r).unwrap(); - assert_eq!(g.num_env, 4); - assert_eq!(g.num_noise, 2); - assert_eq!(g.freq_res, vec![false; 4]); - assert!(!g.amp_res_override); - } - - #[test] - fn fixvar_reverses_freq_res() { - // num_rel_1 = 2 → num_env = 3. Frequency-resolution flags are - // transmitted as bs_freq_res[num_env-1-env]. - let mut w = BitWriter::new(); - w.write_u32(FrameClass::FixVar.to_bits(), 2); - w.write_u32(1, 2); // var_bord_1 - w.write_u32(2, 2); // num_rel_1 = 2 → num_env = 3 - w.write_u32(0, 2); // rel_bord_1[0] - w.write_u32(3, 2); // rel_bord_1[1] - // ptr_bits(3) = 2. - w.write_u32(1, 2); // pointer - // freq_res transmitted reversed: index 2, then 1, then 0. - w.write_bit(true); // -> freq_res[2] - w.write_bit(false); // -> freq_res[1] - w.write_bit(true); // -> freq_res[0] - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let g = SbrGrid::parse(&mut r).unwrap(); - assert_eq!(g.frame_class, FrameClass::FixVar); - assert_eq!(g.num_env, 3); - assert_eq!(g.var_bord_1, 1); - assert_eq!(g.rel_bord_1, vec![0, 3]); - assert_eq!(g.pointer, 1); - assert_eq!(g.freq_res, vec![true, false, true]); - } - - #[test] - fn varfix_forward_freq_res() { - let mut w = BitWriter::new(); - w.write_u32(FrameClass::VarFix.to_bits(), 2); - w.write_u32(2, 2); // var_bord_0 - w.write_u32(1, 2); // num_rel_0 = 1 → num_env = 2 - w.write_u32(3, 2); // rel_bord_0[0] - // ptr_bits(2) = 2. - w.write_u32(0, 2); // pointer - w.write_bit(false); // freq_res[0] - w.write_bit(true); // freq_res[1] - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let g = SbrGrid::parse(&mut r).unwrap(); - assert_eq!(g.frame_class, FrameClass::VarFix); - assert_eq!(g.num_env, 2); - assert_eq!(g.var_bord_0, 2); - assert_eq!(g.rel_bord_0, vec![3]); - assert_eq!(g.freq_res, vec![false, true]); - } - - #[test] - fn varvar_both_borders() { - let mut w = BitWriter::new(); - w.write_u32(FrameClass::VarVar.to_bits(), 2); - w.write_u32(1, 2); // var_bord_0 - w.write_u32(2, 2); // var_bord_1 - w.write_u32(1, 2); // num_rel_0 = 1 - w.write_u32(1, 2); // num_rel_1 = 1 → num_env = 3 - w.write_u32(0, 2); // rel_bord_0[0] - w.write_u32(3, 2); // rel_bord_1[0] - // ptr_bits(3) = 2. - w.write_u32(2, 2); // pointer - w.write_bit(true); - w.write_bit(false); - w.write_bit(true); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let g = SbrGrid::parse(&mut r).unwrap(); - assert_eq!(g.frame_class, FrameClass::VarVar); - assert_eq!(g.num_env, 3); - assert_eq!(g.num_noise, 2); - assert_eq!(g.var_bord_0, 1); - assert_eq!(g.var_bord_1, 2); - assert_eq!(g.rel_bord_0, vec![0]); - assert_eq!(g.rel_bord_1, vec![3]); - assert_eq!(g.pointer, 2); - assert_eq!(g.freq_res, vec![true, false, true]); - } - - #[test] - fn dtdf_reads_per_env_and_noise() { - let mut w = BitWriter::new(); - w.write_bit(true); // df_env[0] - w.write_bit(false); // df_env[1] - w.write_bit(true); // df_noise[0] - w.write_bit(false); // df_noise[1] - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let d = SbrDtdf::parse(&mut r, 2, 2).unwrap(); - assert_eq!(d.df_env, vec![true, false]); - assert_eq!(d.df_noise, vec![true, false]); - } - - #[test] - fn invf_reads_two_bits_per_band() { - let mut w = BitWriter::new(); - w.write_u32(0, 2); - w.write_u32(1, 2); - w.write_u32(2, 2); - w.write_u32(3, 2); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let inv = SbrInvf::parse(&mut r, 4).unwrap(); - assert_eq!(inv.invf_mode, vec![0, 1, 2, 3]); - } - - #[test] - fn truncated_grid_errors() { - let bytes = [0u8; 0]; - let mut r = BitReader::new(&bytes); - assert!(matches!(SbrGrid::parse(&mut r), Err(Error::SbrGridInvalid))); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_header.rs b/crates/vendor/oxideav-aac/src/sbr_header.rs deleted file mode 100644 index d074f904..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_header.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! `sbr_header()` parser — ISO/IEC 14496-3 §4.4.2.8, Table 4.63. -//! -//! The SBR header carries the static per-stream parameters that drive -//! the §4.6.18.3.2 frequency-band setup ([`crate::sbr_freq_bands`]): -//! the start / stop frequency indices, the crossover band, and the two -//! optional "extra" header blocks (`bs_header_extra_1` / -//! `bs_header_extra_2`). Per Table 4.63 Note 3, when an extra-header -//! flag is clear the underlying elements take their **default** values, -//! disregarding any previously transmitted value: -//! -//! | element | width | default (Tables 4.105–4.111) | -//! |---------|-------|------------------------------| -//! | `bs_freq_scale` | 2 | 2 (10 bands/octave) | -//! | `bs_alter_scale` | 1 | 1 (grouping / extra-wide) | -//! | `bs_noise_bands` | 2 | 2 (2 bands/octave) | -//! | `bs_limiter_bands` | 2 | 2 (2.0 bands/octave) | -//! | `bs_limiter_gains` | 2 | 2 (3 dB max gain) | -//! | `bs_interpol_freq` | 1 | 1 (interpolation on) | -//! | `bs_smoothing_mode`| 1 | 1 (smoothing off) | -//! -//! `bs_amp_res` (the envelope amplitude resolution: 0 = 1.5 dB, -//! 1 = 3.0 dB) is carried in the header but may be overridden to 0 by -//! `sbr_grid()` for a single-envelope `FIXFIX` frame — that override is -//! applied downstream, not here. -//! -//! The header is a fixed-width bit layout with no Huffman or -//! variable-length content, so it is fully recoverable from the spec -//! syntax table alone. - -use crate::{Error, Result}; -use oxideav_core::bits::BitReader; - -/// Default `bs_freq_scale` when `bs_header_extra_1 == 0` (Table 4.105: -/// 10 bands/octave). -pub const DEFAULT_FREQ_SCALE: u8 = 2; -/// Default `bs_alter_scale` when `bs_header_extra_1 == 0` (Table 4.106). -pub const DEFAULT_ALTER_SCALE: bool = true; -/// Default `bs_noise_bands` when `bs_header_extra_1 == 0` (Table 4.107: -/// 2 bands/octave). -pub const DEFAULT_NOISE_BANDS: u8 = 2; -/// Default `bs_limiter_bands` when `bs_header_extra_2 == 0` (Table -/// 4.108: 2.0 bands/octave). -pub const DEFAULT_LIMITER_BANDS: u8 = 2; -/// Default `bs_limiter_gains` when `bs_header_extra_2 == 0` (Table -/// 4.109: 3 dB max gain). -pub const DEFAULT_LIMITER_GAINS: u8 = 2; -/// Default `bs_interpol_freq` when `bs_header_extra_2 == 0` (Table -/// 4.110: interpolation on). -pub const DEFAULT_INTERPOL_FREQ: bool = true; -/// Default `bs_smoothing_mode` when `bs_header_extra_2 == 0` (Table -/// 4.111: smoothing off). -pub const DEFAULT_SMOOTHING_MODE: bool = true; - -/// A parsed `sbr_header()` (Table 4.63). -/// -/// The two `bs_header_extra_*` flags are recorded so a re-encoder can -/// reproduce the exact bit layout, but the underlying parameters are -/// already resolved to their effective values (the Table 4.63 Note 3 -/// defaults are filled in when an extra flag is clear). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SbrHeader { - /// `bs_amp_res` — envelope amplitude resolution (false = 1.5 dB, - /// true = 3.0 dB). May be forced to false by a single-envelope - /// `FIXFIX` grid downstream. - pub amp_res: bool, - /// `bs_start_freq` — 4-bit index into the §4.6.18.3.2.1 `offset` - /// table that sets the low QMF boundary `k0`. - pub start_freq: u8, - /// `bs_stop_freq` — 4-bit index that sets the high QMF boundary - /// `k2`. - pub stop_freq: u8, - /// `bs_xover_band` — 3-bit index into the master frequency table - /// where the SBR range begins (`bs_xover_band < NMaster`). - pub xover_band: u8, - /// `bs_reserved` — 2 reserved bits (kept for faithful re-encode). - pub reserved: u8, - /// `bs_header_extra_1` — whether the optional header part 1 was - /// transmitted. - pub header_extra_1: bool, - /// `bs_header_extra_2` — whether the optional header part 2 was - /// transmitted. - pub header_extra_2: bool, - /// `bs_freq_scale` — master-table warping selector (Table 4.105). - pub freq_scale: u8, - /// `bs_alter_scale` — master-table alteration flag (Table 4.106). - pub alter_scale: bool, - /// `bs_noise_bands` — noise-band density selector (Table 4.107). - pub noise_bands: u8, - /// `bs_limiter_bands` — limiter-band density selector (Table 4.108). - pub limiter_bands: u8, - /// `bs_limiter_gains` — limiter max-gain selector (Table 4.109). - pub limiter_gains: u8, - /// `bs_interpol_freq` — frequency-interpolation flag (Table 4.110). - pub interpol_freq: bool, - /// `bs_smoothing_mode` — smoothing flag (Table 4.111). - pub smoothing_mode: bool, -} - -impl SbrHeader { - /// Parse `sbr_header()` (Table 4.63) from `reader`, filling in the - /// Table 4.63 Note 3 default values for any extra-header block that - /// is not present. - /// - /// The `bs_reserved` field is read but not validated (Table 4.63 - /// leaves its value unconstrained). Returns [`Error::SbrHuffInvalid`] - /// only if `reader` runs out of bits — there is no Huffman content - /// here, so this maps the bit-exhaustion error onto the SBR error - /// surface. - pub fn parse(reader: &mut BitReader<'_>) -> Result { - let amp_res = read_bit(reader)?; - let start_freq = read_u8(reader, 4)?; - let stop_freq = read_u8(reader, 4)?; - let xover_band = read_u8(reader, 3)?; - let reserved = read_u8(reader, 2)?; - let header_extra_1 = read_bit(reader)?; - let header_extra_2 = read_bit(reader)?; - - let (freq_scale, alter_scale, noise_bands) = if header_extra_1 { - (read_u8(reader, 2)?, read_bit(reader)?, read_u8(reader, 2)?) - } else { - (DEFAULT_FREQ_SCALE, DEFAULT_ALTER_SCALE, DEFAULT_NOISE_BANDS) - }; - - let (limiter_bands, limiter_gains, interpol_freq, smoothing_mode) = if header_extra_2 { - ( - read_u8(reader, 2)?, - read_u8(reader, 2)?, - read_bit(reader)?, - read_bit(reader)?, - ) - } else { - ( - DEFAULT_LIMITER_BANDS, - DEFAULT_LIMITER_GAINS, - DEFAULT_INTERPOL_FREQ, - DEFAULT_SMOOTHING_MODE, - ) - }; - - Ok(SbrHeader { - amp_res, - start_freq, - stop_freq, - xover_band, - reserved, - header_extra_1, - header_extra_2, - freq_scale, - alter_scale, - noise_bands, - limiter_bands, - limiter_gains, - interpol_freq, - smoothing_mode, - }) - } - - /// Whether this header differs from `other` in any field that - /// affects the §4.6.18.3.2 frequency-band geometry - /// (`bs_start_freq`, `bs_stop_freq`, `bs_xover_band`, - /// `bs_freq_scale`, `bs_alter_scale`, `bs_noise_bands`). - /// - /// SBR decoders only need to recompute the master / derived band - /// tables when one of these "reset" parameters changes; the limiter - /// / interpolation / smoothing parameters do not alter the band - /// geometry. This mirrors the §4.6.18.3.3 header-change reset. - pub fn band_geometry_changed(&self, other: &SbrHeader) -> bool { - self.start_freq != other.start_freq - || self.stop_freq != other.stop_freq - || self.xover_band != other.xover_band - || self.freq_scale != other.freq_scale - || self.alter_scale != other.alter_scale - || self.noise_bands != other.noise_bands - } - - /// Compute the §4.6.18.3.2 derived frequency-band tables - /// ([`crate::sbr_freq_bands::HiLoTables`]) for this header at the - /// given SBR internal sample rate `fs_sbr` (twice the AAC core - /// rate). - /// - /// This chains [`crate::sbr_freq_bands::k0`] / - /// [`crate::sbr_freq_bands::k2`] / - /// [`crate::sbr_freq_bands::master_table`] / - /// [`crate::sbr_freq_bands::HiLoTables::derive`] with this header's - /// parameters; it returns [`Error::SbrFreqBandInvalid`] for any - /// §4.6.18.3.6-violating geometry. - pub fn derive_bands(&self, fs_sbr: u32) -> Result { - let k0 = crate::sbr_freq_bands::k0(fs_sbr, self.start_freq)?; - let k2 = crate::sbr_freq_bands::k2(fs_sbr, self.stop_freq, k0)?; - let f_master = - crate::sbr_freq_bands::master_table(k0, k2, self.freq_scale, self.alter_scale)?; - crate::sbr_freq_bands::HiLoTables::derive(&f_master, self.xover_band, self.noise_bands) - } -} - -#[inline] -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::SbrHuffInvalid) -} - -#[inline] -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - reader - .read_u32(n) - .map(|v| v as u8) - .map_err(|_| Error::SbrHuffInvalid) -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::BitWriter; - - /// Build a minimal header bitstream with both extra flags clear. - fn pack_minimal(amp_res: bool, start: u8, stop: u8, xover: u8, reserved: u8) -> Vec { - let mut w = BitWriter::new(); - w.write_bit(amp_res); - w.write_u32(start as u32, 4); - w.write_u32(stop as u32, 4); - w.write_u32(xover as u32, 3); - w.write_u32(reserved as u32, 2); - w.write_bit(false); // bs_header_extra_1 - w.write_bit(false); // bs_header_extra_2 - w.finish() - } - - #[test] - fn minimal_header_uses_defaults() { - let bytes = pack_minimal(true, 5, 6, 4, 0b10); - let mut r = BitReader::new(&bytes); - let h = SbrHeader::parse(&mut r).unwrap(); - assert!(h.amp_res); - assert_eq!(h.start_freq, 5); - assert_eq!(h.stop_freq, 6); - assert_eq!(h.xover_band, 4); - assert_eq!(h.reserved, 0b10); - assert!(!h.header_extra_1); - assert!(!h.header_extra_2); - // Table 4.63 Note 3 defaults. - assert_eq!(h.freq_scale, DEFAULT_FREQ_SCALE); - assert_eq!(h.alter_scale, DEFAULT_ALTER_SCALE); - assert_eq!(h.noise_bands, DEFAULT_NOISE_BANDS); - assert_eq!(h.limiter_bands, DEFAULT_LIMITER_BANDS); - assert_eq!(h.limiter_gains, DEFAULT_LIMITER_GAINS); - assert_eq!(h.interpol_freq, DEFAULT_INTERPOL_FREQ); - assert_eq!(h.smoothing_mode, DEFAULT_SMOOTHING_MODE); - } - - #[test] - fn full_header_round_trip_values() { - let mut w = BitWriter::new(); - w.write_bit(false); // amp_res - w.write_u32(3, 4); // start_freq - w.write_u32(9, 4); // stop_freq - w.write_u32(2, 3); // xover_band - w.write_u32(0, 2); // reserved - w.write_bit(true); // header_extra_1 - w.write_bit(true); // header_extra_2 - w.write_u32(1, 2); // freq_scale - w.write_bit(false); // alter_scale - w.write_u32(3, 2); // noise_bands - w.write_u32(0, 2); // limiter_bands - w.write_u32(1, 2); // limiter_gains - w.write_bit(false); // interpol_freq - w.write_bit(false); // smoothing_mode - let bytes = w.finish(); - - let mut r = BitReader::new(&bytes); - let h = SbrHeader::parse(&mut r).unwrap(); - assert!(!h.amp_res); - assert_eq!(h.start_freq, 3); - assert_eq!(h.stop_freq, 9); - assert_eq!(h.xover_band, 2); - assert!(h.header_extra_1); - assert!(h.header_extra_2); - assert_eq!(h.freq_scale, 1); - assert!(!h.alter_scale); - assert_eq!(h.noise_bands, 3); - assert_eq!(h.limiter_bands, 0); - assert_eq!(h.limiter_gains, 1); - assert!(!h.interpol_freq); - assert!(!h.smoothing_mode); - } - - #[test] - fn truncated_header_errors() { - let bytes = [0x00u8]; // 8 bits, not enough for the 16-bit fixed prefix - let mut r = BitReader::new(&bytes); - assert!(matches!( - SbrHeader::parse(&mut r), - Err(Error::SbrHuffInvalid) - )); - } - - #[test] - fn band_geometry_change_detection() { - let a = pack_minimal(true, 5, 6, 4, 0); - let mut ra = BitReader::new(&a); - let ha = SbrHeader::parse(&mut ra).unwrap(); - - // Same geometry → no change. - let mut rb = BitReader::new(&a); - let hb = SbrHeader::parse(&mut rb).unwrap(); - assert!(!ha.band_geometry_changed(&hb)); - - // Different start_freq → change. - let c = pack_minimal(true, 7, 6, 4, 0); - let mut rc = BitReader::new(&c); - let hc = SbrHeader::parse(&mut rc).unwrap(); - assert!(ha.band_geometry_changed(&hc)); - } - - #[test] - fn derive_bands_matches_freq_band_module() { - // A representative 44.1 kHz core → fs_sbr = 88200. Use a header - // with the linear master scale (bs_freq_scale = 0), which is - // well-defined for every k0/k2 pair, and the known-good - // §4.6.18.3.6 geometry from the sbr_freq_bands end-to-end test: - // bs_start_freq = 5, bs_stop_freq = 5, bs_xover_band = 1, - // bs_alter_scale = 0, bs_noise_bands = 2. - let fs_sbr = 88_200; - let mut w = BitWriter::new(); - w.write_bit(false); // amp_res - w.write_u32(5, 4); // start_freq - w.write_u32(5, 4); // stop_freq - w.write_u32(1, 3); // xover_band - w.write_u32(0, 2); // reserved - w.write_bit(true); // header_extra_1 (so freq_scale is explicit) - w.write_bit(false); // header_extra_2 - w.write_u32(0, 2); // freq_scale = 0 (linear) - w.write_bit(false); // alter_scale = 0 - w.write_u32(2, 2); // noise_bands = 2 - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let h = SbrHeader::parse(&mut r).unwrap(); - assert_eq!(h.freq_scale, 0); - let bands = h.derive_bands(fs_sbr).unwrap(); - - let k0 = crate::sbr_freq_bands::k0(fs_sbr, h.start_freq).unwrap(); - let k2 = crate::sbr_freq_bands::k2(fs_sbr, h.stop_freq, k0).unwrap(); - let fm = crate::sbr_freq_bands::master_table(k0, k2, h.freq_scale, h.alter_scale).unwrap(); - let direct = - crate::sbr_freq_bands::HiLoTables::derive(&fm, h.xover_band, h.noise_bands).unwrap(); - assert_eq!(bands.f_table_high, direct.f_table_high); - assert_eq!(bands.f_table_low, direct.f_table_low); - assert_eq!(bands.f_table_noise, direct.f_table_noise); - assert_eq!(bands.m, direct.m); - assert_eq!(bands.k_x, direct.k_x); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_hf_gen.rs b/crates/vendor/oxideav-aac/src/sbr_hf_gen.rs deleted file mode 100644 index 927f323f..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_hf_gen.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! SBR high-frequency generation — ISO/IEC 14496-3 §4.6.18.6. -//! -//! Builds the `XHigh` subband matrix from the analysis-filterbank -//! output `XLow`: -//! -//! * **Patch construction** (§4.6.18.6.3 / Figure 4.48) — the -//! `numPatches` / `patchStartSubband` / `patchNumSubbands` decision -//! that maps consecutive low-band source ranges onto the SBR range, -//! driven by `goalSb = NINT(2.048e6 / FsSBR)` and the `fMaster` -//! grid, with the trailing small-patch trim. -//! * **Inverse filtering** (§4.6.18.6.2) — the covariance-method -//! second-order linear prediction per low subband (`φk(i,j)` over -//! `numTimeSlots·RATE + 6` samples, `d(k)` with `εInv = 1e-6`, the -//! `α0(k)` / `α1(k)` solution, and the `|α| ≥ 4` reset), plus the -//! Table 4.175 `newBw` transition function and the `bwArray` chirp -//! blend (`0.75/0.25` attack, `0.90625/0.09375` decay, `< 0.015625` -//! flush to zero). -//! * **HF generator** (§4.6.18.6.3) — `XHigh(k, l + tHFAdj) = -//! XLow(p, …) + bw·α0(p)·XLow(p, l−1+…) + bw²·α1(p)·XLow(p, l−2+…)` -//! over the patch mapping, with the chirp factor selected by the -//! noise-floor band `g(k)`. -//! -//! Both `XLow` and `XHigh` are stored slot-major (`x[slot][band]`) -//! with the slot axis carrying the spec's absolute column index (the -//! `tHFGen`-slot history precedes the current frame, so spec index -//! `l + tHFAdj` is a direct column index). -//! -//! ## Provenance -//! -//! Every formula, constant, and branch is from the §4.6.18.6 text, -//! Table 4.175, and the Figure 4.48 flowchart of the staged spec. No -//! part of this implementation is derived from any external decoder. - -use crate::sbr_freq_bands::HiLoTables; -use crate::sbr_qmf::Complex; -use crate::{Error, Result}; - -/// `tHFAdj = 2` — the envelope-adjuster offset (§4.6.18.5). -pub const T_HF_ADJ: usize = 2; - -/// `tHFGen = 8` — the HF-generator offset (§4.6.18.5). -pub const T_HF_GEN: usize = 8; - -/// The §4.6.18.6.2 relaxation parameter `εInv`. -pub const EPS_INV: f64 = 1e-6; - -/// §4.6.18.3.6: `numPatches ≤ 5`. -pub const MAX_PATCHES: usize = 5; - -/// The §4.6.18.6.3 / Figure 4.48 patch layout. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Patches { - /// `patchStartSubband(i)` — first source QMF subband of patch `i`. - pub start: Vec, - /// `patchNumSubbands(i)` — subband count of patch `i`. - pub num: Vec, -} - -impl Patches { - /// `numPatches`. - #[inline] - #[must_use] - pub fn num_patches(&self) -> usize { - self.num.len() - } - - /// The §4.6.18.3.2.3 patch borders: `patchBorders(0) = kx`, - /// `patchBorders(k) = patchBorders(k-1) + patchNumSubbands(k-1)`. - #[must_use] - pub fn borders(&self, k_x: i32) -> Vec { - let mut b = Vec::with_capacity(self.num.len() + 1); - b.push(k_x); - for &n in &self.num { - b.push(b[b.len() - 1] + n as i32); - } - b - } -} - -/// Figure 4.48 — patch construction. -/// -/// `f_master` is the §4.6.18.3.2.1 master table (`fMaster(0..=NMaster)`), -/// `k0` its first subband, `k_x` / `m` the SBR range, and `fs_sbr` the -/// SBR internal rate driving `goalSb = NINT(2.048e6 / FsSBR)`. -pub fn build_patches(f_master: &[i32], k0: i32, k_x: i32, m: i32, fs_sbr: u32) -> Result { - if f_master.len() < 2 || fs_sbr == 0 { - return Err(Error::SbrFreqBandInvalid); - } - let n_master = f_master.len() - 1; - - let mut msb = k0; - let mut usb = k_x; - let mut start = Vec::new(); - let mut num = Vec::new(); - - // goalSb = NINT(2.048e6 / Fs). - let goal_sb = ((2.0 * 2.048e6 / f64::from(fs_sbr) + 1.0) / 2.0).floor() as i32; - // k: the first master index at/after goalSb (NMaster if goalSb is - // past the SBR stop border). - let mut k = if goal_sb < k_x + m { - let mut kk = 0usize; - for (i, &f) in f_master.iter().enumerate() { - if f < goal_sb { - kk = i + 1; - } else { - break; - } - } - kk - } else { - n_master - }; - - let mut sb; - let mut guard = 0usize; - loop { - guard += 1; - if guard > 64 { - return Err(Error::SbrFreqBandInvalid); - } - // Walk j downward from k until the patch source fits under the - // first master subband: sb <= k0 - 1 + msb - odd. - let mut j = k; - let odd = loop { - if j >= f_master.len() { - return Err(Error::SbrFreqBandInvalid); - } - sb = f_master[j]; - let odd = (sb - 2 + k0).rem_euclid(2); - if sb <= k0 - 1 + msb - odd { - break odd; - } - if j == 0 { - return Err(Error::SbrFreqBandInvalid); - } - j -= 1; - }; - - let n = (sb - usb).max(0); - let s = k0 - odd - n; - if n > 0 { - if s < 0 || start.len() >= MAX_PATCHES { - return Err(Error::SbrFreqBandInvalid); - } - start.push(s as usize); - num.push(n as usize); - usb = sb; - msb = sb; - } else { - msb = k_x; - } - - if f_master[k] - sb < 3 { - k = n_master; - } - if sb == k_x + m { - break; - } - } - - // Trailing small-patch trim: drop a final patch narrower than 3 - // subbands when more than one patch was built. - if num.len() > 1 && *num.last().unwrap() < 3 { - num.pop(); - start.pop(); - } - - Ok(Patches { start, num }) -} - -/// Table 4.175 — `newBw(bs_invf_mode´, bs_invf_mode)`. Row is the -/// previous frame's mode, column the current one (both `0..=3` for -/// Off / Low / Intermediate / Strong). -#[must_use] -pub fn new_bw(prev_mode: u8, cur_mode: u8) -> f64 { - const TABLE: [[f64; 4]; 4] = [ - [0.0, 0.6, 0.9, 0.98], - [0.6, 0.75, 0.9, 0.98], - [0.0, 0.75, 0.9, 0.98], - [0.0, 0.75, 0.9, 0.98], - ]; - TABLE[usize::from(prev_mode.min(3))][usize::from(cur_mode.min(3))] -} - -/// §4.6.18.6.2 chirp-factor update: one `bwArray` entry per noise -/// band. `prev_invf` / `prev_bw` are the previous SBR frame's values -/// (all zero for the first frame). -#[must_use] -pub fn chirp_factors(cur_invf: &[u8], prev_invf: &[u8], prev_bw: &[f64]) -> Vec { - cur_invf - .iter() - .enumerate() - .map(|(i, &cur)| { - let prev_mode = prev_invf.get(i).copied().unwrap_or(0); - let bw_prev = prev_bw.get(i).copied().unwrap_or(0.0); - let nb = new_bw(prev_mode, cur); - let temp = if nb < bw_prev { - 0.75 * nb + 0.25 * bw_prev - } else { - 0.90625 * nb + 0.09375 * bw_prev - }; - if temp < 0.015625 { - 0.0 - } else { - temp - } - }) - .collect() -} - -/// §4.6.18.6.2 covariance-method prediction coefficients -/// `(α0(k), α1(k))` for low subband `k`. -/// -/// `x_low` is slot-major with the spec's absolute column index (the -/// covariance windows over `n − i + tHFAdj` for -/// `0 ≤ n < n_slots_frame + 6`), so `x_low` must carry at least -/// `n_slots_frame + 6 + tHFAdj` columns. -pub fn prediction_coefficients( - x_low: &[[Complex; 32]], - k: usize, - n_slots_frame: usize, -) -> Result<(Complex, Complex)> { - if k >= 32 || x_low.len() < n_slots_frame + 6 + T_HF_ADJ { - return Err(Error::SbrFreqBandInvalid); - } - // φk(i, j) = Σ_n XLow(k, n - i + tHFAdj) · XLow*(k, n - j + tHFAdj). - let phi = |i: usize, j: usize| -> Complex { - let mut acc = Complex::default(); - for n in 0..(n_slots_frame + 6) { - let a = x_low[n + T_HF_ADJ - i][k]; - let b = x_low[n + T_HF_ADJ - j][k]; - acc += a * b.conj(); - } - acc - }; - let phi01 = phi(0, 1); - let phi02 = phi(0, 2); - let phi11 = phi(1, 1); - let phi12 = phi(1, 2); - let phi22 = phi(2, 2); - - // d(k) = φ(2,2)·φ(1,1) − |φ(1,2)|² / (1 + εInv). φ(1,1) / φ(2,2) - // are real by construction. - let d = phi22.re * phi11.re - phi12.norm_sqr() / (1.0 + EPS_INV); - - let alpha1 = if d != 0.0 { - let numer = phi01 * phi12 - phi02 * phi11.re; - Complex::new(numer.re / d, numer.im / d) - } else { - Complex::default() - }; - let alpha0 = if phi11.re != 0.0 { - let numer = phi01 + alpha1 * phi12.conj(); - Complex::new(-numer.re / phi11.re, -numer.im / phi11.re) - } else { - Complex::default() - }; - - // If either magnitude reaches 4, both coefficients reset to zero. - if alpha0.norm_sqr() >= 16.0 || alpha1.norm_sqr() >= 16.0 { - return Ok((Complex::default(), Complex::default())); - } - Ok((alpha0, alpha1)) -} - -/// §4.6.18.8.3 reflection coefficient for the low-power SBR aliasing -/// detection: `ref(k) = min(max(−φk(0,1)/φk(1,1), −1), 1)` when -/// `φk(1,1) ≠ 0`, else `0`, with the covariance sums of §4.6.18.6.2 -/// (over the same `numTimeSlots·RATE + 6` window). The low-power tool -/// operates on real-valued subband signals, so the real parts carry -/// the whole covariance. -pub fn reflection_coefficient( - x_low: &[[Complex; 32]], - k: usize, - n_slots_frame: usize, -) -> Result { - if k >= 32 || x_low.len() < n_slots_frame + 6 + T_HF_ADJ { - return Err(Error::SbrFreqBandInvalid); - } - let mut phi01 = 0.0f64; - let mut phi11 = 0.0f64; - for n in 0..(n_slots_frame + 6) { - let a = x_low[n + T_HF_ADJ][k].re; - let b = x_low[n + T_HF_ADJ - 1][k].re; - phi01 += a * b; - phi11 += b * b; - } - Ok(if phi11 != 0.0 { - (-phi01 / phi11).clamp(-1.0, 1.0) - } else { - 0.0 - }) -} - -/// §4.6.18.6.3 — generate `XHigh` from `XLow` over the patch mapping. -/// -/// * `x_low` — slot-major analysis output (spec absolute columns). -/// * `patches` — the Figure 4.48 layout. -/// * `bw_array` — the per-noise-band chirp factors. -/// * `bands` — the derived frequency tables (`fTableNoise`, `k_x`). -/// * `l_range` — the spec's `RATE·tE(0) .. RATE·tE(LE)` column range -/// (exclusive end, *before* the `tHFAdj` offset). -/// * `n_slots_frame` — `numTimeSlots · RATE` (covariance length). -/// -/// Returns `XHigh` with the same slot-major layout and column count as -/// `x_low` (bands outside the patched range stay zero). -pub fn generate_hf( - x_low: &[[Complex; 32]], - patches: &Patches, - bw_array: &[f64], - bands: &HiLoTables, - l_range: core::ops::Range, - n_slots_frame: usize, -) -> Result> { - let k_x = bands.k_x; - let mut x_high = vec![[Complex::default(); 64]; x_low.len()]; - - // α cache per source subband (a subband may feed several patches). - let mut alphas: [Option<(Complex, Complex)>; 32] = [None; 32]; - - // g(k): the noise band containing QMF subband k. - let g_of = |k: i32| -> Result { - let nb = &bands.f_table_noise; - for i in 0..nb.len() - 1 { - if nb[i] <= k && k < nb[i + 1] { - return Ok(i); - } - } - Err(Error::SbrFreqBandInvalid) - }; - - let mut k_off = 0usize; - for (i, (&p_start, &p_num)) in patches.start.iter().zip(patches.num.iter()).enumerate() { - let _ = i; - for x in 0..p_num { - let k = k_x as usize + x + k_off; - let p = p_start + x; - if k >= 64 || p >= 32 { - return Err(Error::SbrFreqBandInvalid); - } - let (a0, a1) = match alphas[p] { - Some(a) => a, - None => { - let a = prediction_coefficients(x_low, p, n_slots_frame)?; - alphas[p] = Some(a); - a - } - }; - let bw = *bw_array - .get(g_of(k as i32)?) - .ok_or(Error::SbrFreqBandInvalid)?; - let bw2 = bw * bw; - for l in l_range.clone() { - let c = usize::try_from(l).map_err(|_| Error::SbrFreqBandInvalid)? + T_HF_ADJ; - if c >= x_low.len() || c < 2 { - return Err(Error::SbrFreqBandInvalid); - } - x_high[c][k] = - x_low[c][p] + (a0 * bw) * x_low[c - 1][p] + (a1 * bw2) * x_low[c - 2][p]; - } - } - k_off += p_num; - } - Ok(x_high) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Table 4.175 spot values. - #[test] - fn new_bw_table() { - assert_eq!(new_bw(0, 0), 0.0); - assert_eq!(new_bw(0, 1), 0.6); - assert_eq!(new_bw(1, 0), 0.6); - assert_eq!(new_bw(1, 1), 0.75); - assert_eq!(new_bw(2, 0), 0.0); - assert_eq!(new_bw(2, 1), 0.75); - assert_eq!(new_bw(3, 3), 0.98); - assert_eq!(new_bw(0, 2), 0.9); - } - - /// Chirp blend: rising values take the 0.90625/0.09375 mix, - /// falling values the 0.75/0.25 mix, and tiny results flush to 0. - #[test] - fn chirp_blend_and_flush() { - // First frame: prev all zero. newBw(0, 3) = 0.98 rising: - // 0.90625·0.98 = 0.888125. - let bw = chirp_factors(&[3], &[0], &[0.0]); - assert!((bw[0] - 0.888125).abs() < 1e-12); - // Falling: newBw(3, 0) = 0.0 < prev 0.888125: - // 0.25·0.888125 = 0.22203125. - let bw2 = chirp_factors(&[0], &[3], &bw); - assert!((bw2[0] - 0.22203125).abs() < 1e-12); - // Repeated Off decays geometrically to below 0.015625 → 0. - let mut cur = bw2; - for _ in 0..4 { - cur = chirp_factors(&[0], &[0], &cur); - } - assert_eq!(cur[0], 0.0); - } - - /// §4.6.18.8.3 reflection coefficient: a constant subband signal - /// has φ(0,1) = φ(1,1) → ref = −1; an alternating-sign signal has - /// φ(0,1) = −φ(1,1) → ref = +1; silence → 0; and the clamp holds. - #[test] - fn reflection_coefficient_orientations() { - let n = 32usize; - let cols = n + 6 + T_HF_ADJ; - let mut x = vec![[Complex::default(); 32]; cols]; - for (c, col) in x.iter_mut().enumerate() { - col[3] = Complex::new(1.0, 0.0); // constant - col[4] = Complex::new(if c % 2 == 0 { 1.0 } else { -1.0 }, 0.0); // alternating - } - assert_eq!(reflection_coefficient(&x, 3, n).unwrap(), -1.0); - assert_eq!(reflection_coefficient(&x, 4, n).unwrap(), 1.0); - assert_eq!(reflection_coefficient(&x, 5, n).unwrap(), 0.0); - assert!(reflection_coefficient(&x, 32, n).is_err()); - } - - /// Figure 4.48 on a hand-walked geometry: fMaster = 8..=24 step 2, - /// k0 = kx = 8, M = 16, goalSb past the range. - #[test] - fn patch_construction_hand_walked() { - let f_master: Vec = (0..=8).map(|i| 8 + 2 * i).collect(); - // fs_sbr small enough that goalSb = NINT(2.048e6/fs) ≥ 24. - let p = build_patches(&f_master, 8, 8, 16, 85_000).unwrap(); - // Iter 1: sb = 14 → patch (start 2, num 6); - // iter 2: sb = 20 → patch (2, 6); iter 3: sb = 24 → (4, 4). - assert_eq!(p.start, vec![2, 2, 4]); - assert_eq!(p.num, vec![6, 6, 4]); - assert_eq!(p.borders(8), vec![8, 14, 20, 24]); - } - - /// The patch trim drops a trailing patch narrower than 3 subbands. - #[test] - fn patch_trim_drops_small_tail() { - // fMaster reaching kx + M = 22 with a final 2-wide step. - let f_master = vec![8, 10, 12, 14, 16, 20, 22]; - let p = build_patches(&f_master, 8, 8, 14, 85_000).unwrap(); - // Walk: msb=8,usb=8 → sb=14 (odd 0) num 6 start 2; - // then sb=20? 20 ≤ 7+14-0=21 → num 6 start 2; then sb=22: - // 22 ≤ 7+20-0=27 → num 2 start 6 → trimmed. - assert_eq!(p.num, vec![6, 6]); - assert_eq!(p.start, vec![2, 2]); - } - - /// Patch invariants on a spec-derived master table (44.1 kHz - /// HE-AAC geometry). - #[test] - fn patch_invariants_on_derived_master() { - let fs_sbr = 44_100; - let k0 = crate::sbr_freq_bands::k0(fs_sbr, 5).unwrap(); - let k2 = crate::sbr_freq_bands::k2(fs_sbr, 5, k0).unwrap(); - let fm = crate::sbr_freq_bands::master_table(k0, k2, 2, true).unwrap(); - let bands = HiLoTables::derive(&fm, 0, 2).unwrap(); - let p = build_patches(&fm, k0, bands.k_x, bands.m, fs_sbr).unwrap(); - assert!(p.num_patches() >= 1 && p.num_patches() <= MAX_PATCHES); - for (&s, &n) in p.start.iter().zip(p.num.iter()) { - assert!(n > 0); - // Source range lies below the first master subband. - assert!((s + n) as i32 <= k0); - } - // Borders start at kx and stay within kx + M. - let borders = p.borders(bands.k_x); - assert_eq!(borders[0], bands.k_x); - assert!(*borders.last().unwrap() <= bands.k_x + bands.m); - } - - /// Build a slot-major XLow whose band `k` carries an exact - /// second-order recursion `x[n] = a1·x[n-1] + a2·x[n-2]`. - fn ar2_xlow(k: usize, a1: Complex, a2: Complex, cols: usize) -> Vec<[Complex; 32]> { - let mut x = vec![[Complex::default(); 32]; cols]; - x[0][k] = Complex::new(1.0, 0.3); - x[1][k] = Complex::new(0.2, -0.5); - for n in 2..cols { - let v = a1 * x[n - 1][k] + a2 * x[n - 2][k]; - x[n][k] = v; - } - x - } - - /// The covariance method recovers an exact AR(2) recursion: - /// α0 = −a1, α1 = −a2. - #[test] - fn prediction_recovers_ar2() { - let a1 = Complex::new(0.9, 0.1); - let a2 = Complex::new(-0.5, 0.05); - let x = ar2_xlow(3, a1, a2, 40); - let (al0, al1) = prediction_coefficients(&x, 3, 32).unwrap(); - // The εInv = 1e-6 relaxation perturbs the exact solution by - // O(εInv), so the recovery is pinned to that scale. - assert!((al0 + a1).norm_sqr() < 1e-10, "{al0:?}"); - assert!((al1 + a2).norm_sqr() < 1e-10, "{al1:?}"); - } - - /// |α| ≥ 4 resets both coefficients. - #[test] - fn prediction_resets_large_coefficients() { - // An unstable recursion with |a1| > 4 forces the reset. - let a1 = Complex::new(4.5, 0.0); - let a2 = Complex::new(0.0, 0.0); - let mut x = vec![[Complex::default(); 32]; 40]; - x[0][0] = Complex::new(1e-6, 0.0); - for n in 1..40 { - let v = a1 * x[n - 1][0]; - x[n][0] = v; - } - let _ = a2; - let (al0, al1) = prediction_coefficients(&x, 0, 32).unwrap(); - assert_eq!(al0, Complex::default()); - assert_eq!(al1, Complex::default()); - } - - fn tiny_bands() -> HiLoTables { - HiLoTables { - f_table_high: vec![8, 12, 16], - f_table_low: vec![8, 16], - f_table_noise: vec![8, 16], - m: 8, - k_x: 8, - } - } - - /// bw = 0 copies the source band; bw = 1 on a perfectly - /// predictable source whitens it to (near) zero. - #[test] - fn generate_copies_and_whitens() { - let a1 = Complex::new(0.8, 0.2); - let a2 = Complex::new(-0.4, 0.0); - let x = ar2_xlow(2, a1, a2, 40); - let patches = Patches { - start: vec![2], - num: vec![8], - }; - let bands = tiny_bands(); - // bw = 0: XHigh(k) == XLow(p) on the generated range. Patch - // maps source 2..10 → 8..16; k = 8 comes from p = 2. - let hi = generate_hf(&x, &patches, &[0.0], &bands, 0..32, 32).unwrap(); - for l in 0..32usize { - let c = l + T_HF_ADJ; - assert_eq!(hi[c][8], x[c][2]); - } - // bw = 1: the inverse filter cancels the AR(2) recursion (to - // the O(εInv) accuracy of the relaxed covariance solution). - let hi = generate_hf(&x, &patches, &[1.0], &bands, 0..32, 32).unwrap(); - let sig: f64 = (0..32).map(|l| x[l + T_HF_ADJ][2].norm_sqr()).sum(); - let res: f64 = (0..32).map(|l| hi[l + T_HF_ADJ][8].norm_sqr()).sum(); - assert!(res < 1e-10 * sig, "residual {res} vs signal {sig}"); - // Un-patched bands stay zero. - for col in &hi { - assert_eq!(col[20], Complex::default()); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_huffman.rs b/crates/vendor/oxideav-aac/src/sbr_huffman.rs deleted file mode 100644 index a5c12497..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_huffman.rs +++ /dev/null @@ -1,1011 +0,0 @@ -//! SBR Huffman codebooks + `sbr_huff_dec()` — ISO/IEC 14496-3 -//! Annex 4.A.6.1 (Tables 4.A.78–4.A.88). -//! -//! Spectral Band Replication codes its envelope scalefactors and -//! noise-floor values as DPCM deltas entropy-coded with one of ten -//! canonical Huffman codebooks. The codebook is selected per the -//! §4.6.18.3 `sbr_envelope()` / `sbr_noise()` switch on the coupling -//! flag, the channel index, the amplitude resolution (`bs_amp_res`), -//! and the time/frequency direction (`bs_df_*`): -//! -//! | direction | amp_res | coupling | which | table | -//! |-----------|---------|----------|-------|-------| -//! | time | 0 (1.5 dB) | level | env | [`T_HUFFMAN_ENV_1_5DB`] | -//! | freq | 0 (1.5 dB) | level | env | [`F_HUFFMAN_ENV_1_5DB`] | -//! | time | 0 (1.5 dB) | balance | env | [`T_HUFFMAN_ENV_BAL_1_5DB`] | -//! | freq | 0 (1.5 dB) | balance | env | [`F_HUFFMAN_ENV_BAL_1_5DB`] | -//! | time | 1 (3.0 dB) | level | env | [`T_HUFFMAN_ENV_3_0DB`] | -//! | freq | 1 (3.0 dB) | level | env | [`F_HUFFMAN_ENV_3_0DB`] | -//! | time | 1 (3.0 dB) | balance | env | [`T_HUFFMAN_ENV_BAL_3_0DB`] | -//! | freq | 1 (3.0 dB) | balance | env | [`F_HUFFMAN_ENV_BAL_3_0DB`] | -//! | time | dc | level | noise | [`T_HUFFMAN_NOISE_3_0DB`] | -//! | time | dc | balance | noise | [`T_HUFFMAN_NOISE_BAL_3_0DB`] | -//! -//! Per Table 4.A.78 Note 2, the *frequency*-direction noise codebooks -//! `f_huffman_noise_3_0dB` / `f_huffman_noise_bal_3_0dB` are identical -//! to the 3.0 dB envelope freq codebooks `f_huffman_env_3_0dB` / -//! `f_huffman_env_bal_3_0dB`, so they are not duplicated here — the -//! [`noise_tables`] selector aliases them. -//! -//! ## Codeword representation -//! -//! Each table is `[(u8, u32); N]` indexed by the Huffman table index, -//! where the tuple is `(code_length_bits, codeword)`. Codewords are -//! MSB-first prefix codes (the most-significant of the `length` low -//! bits is read first). [`sbr_huff_dec`] reads one bit at a time, -//! accumulating MSB-first, and returns the first table index whose -//! `(length, codeword)` matches, with the table's largest-absolute- -//! value (LAV) subtracted so the result is the signed DPCM delta. -//! -//! ## Provenance -//! -//! All ten tables are transcribed directly from the normative -//! codeword grids in ISO/IEC 14496-3:2009 Annex 4.A (Tables 4.A.79 -//! through 4.A.88). Each table was validated for completeness (every -//! index 0..=2·LAV present), self-consistency (every codeword fits in -//! its declared bit length), and the prefix-free property (no codeword -//! is a prefix of another) at extraction time. - -use crate::{Error, Result}; - -/// `t_huffman_env_1_5dB` — ISO/IEC 14496-3 Table 4.A.79 (LAV = 60). -/// -/// 121 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 60`. -pub const T_HUFFMAN_ENV_1_5DB: [(u8, u32); 121] = [ - (18, 0x0003FFD6), - (18, 0x0003FFD7), - (18, 0x0003FFD8), - (18, 0x0003FFD9), - (18, 0x0003FFDA), - (18, 0x0003FFDB), - (19, 0x0007FFB8), - (19, 0x0007FFB9), - (19, 0x0007FFBA), - (19, 0x0007FFBB), - (19, 0x0007FFBC), - (19, 0x0007FFBD), - (19, 0x0007FFBE), - (19, 0x0007FFBF), - (19, 0x0007FFC0), - (19, 0x0007FFC1), - (19, 0x0007FFC2), - (19, 0x0007FFC3), - (19, 0x0007FFC4), - (19, 0x0007FFC5), - (19, 0x0007FFC6), - (19, 0x0007FFC7), - (19, 0x0007FFC8), - (19, 0x0007FFC9), - (19, 0x0007FFCA), - (19, 0x0007FFCB), - (19, 0x0007FFCC), - (19, 0x0007FFCD), - (19, 0x0007FFCE), - (19, 0x0007FFCF), - (19, 0x0007FFD0), - (19, 0x0007FFD1), - (19, 0x0007FFD2), - (19, 0x0007FFD3), - (17, 0x0001FFE6), - (18, 0x0003FFD4), - (16, 0x0000FFF0), - (17, 0x0001FFE9), - (18, 0x0003FFD5), - (17, 0x0001FFE7), - (16, 0x0000FFF1), - (16, 0x0000FFEC), - (16, 0x0000FFED), - (16, 0x0000FFEE), - (15, 0x00007FF4), - (14, 0x00003FF9), - (14, 0x00003FF7), - (13, 0x00001FFA), - (13, 0x00001FF9), - (12, 0x00000FFB), - (11, 0x000007FC), - (10, 0x000003FC), - (9, 0x000001FD), - (8, 0x000000FD), - (7, 0x0000007D), - (6, 0x0000003D), - (5, 0x0000001D), - (4, 0x0000000D), - (3, 0x00000005), - (2, 0x00000001), - (2, 0x00000000), - (3, 0x00000004), - (4, 0x0000000C), - (5, 0x0000001C), - (6, 0x0000003C), - (7, 0x0000007C), - (8, 0x000000FC), - (9, 0x000001FC), - (10, 0x000003FD), - (12, 0x00000FFA), - (13, 0x00001FF8), - (14, 0x00003FF6), - (14, 0x00003FF8), - (15, 0x00007FF5), - (16, 0x0000FFEF), - (17, 0x0001FFE8), - (16, 0x0000FFF2), - (19, 0x0007FFD4), - (19, 0x0007FFD5), - (19, 0x0007FFD6), - (19, 0x0007FFD7), - (19, 0x0007FFD8), - (19, 0x0007FFD9), - (19, 0x0007FFDA), - (19, 0x0007FFDB), - (19, 0x0007FFDC), - (19, 0x0007FFDD), - (19, 0x0007FFDE), - (19, 0x0007FFDF), - (19, 0x0007FFE0), - (19, 0x0007FFE1), - (19, 0x0007FFE2), - (19, 0x0007FFE3), - (19, 0x0007FFE4), - (19, 0x0007FFE5), - (19, 0x0007FFE6), - (19, 0x0007FFE7), - (19, 0x0007FFE8), - (19, 0x0007FFE9), - (19, 0x0007FFEA), - (19, 0x0007FFEB), - (19, 0x0007FFEC), - (19, 0x0007FFED), - (19, 0x0007FFEE), - (19, 0x0007FFEF), - (19, 0x0007FFF0), - (19, 0x0007FFF1), - (19, 0x0007FFF2), - (19, 0x0007FFF3), - (19, 0x0007FFF4), - (19, 0x0007FFF5), - (19, 0x0007FFF6), - (19, 0x0007FFF7), - (19, 0x0007FFF8), - (19, 0x0007FFF9), - (19, 0x0007FFFA), - (19, 0x0007FFFB), - (19, 0x0007FFFC), - (19, 0x0007FFFD), - (19, 0x0007FFFE), - (19, 0x0007FFFF), -]; - -/// `f_huffman_env_1_5dB` — ISO/IEC 14496-3 Table 4.A.80 (LAV = 60). -/// -/// 121 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 60`. -pub const F_HUFFMAN_ENV_1_5DB: [(u8, u32); 121] = [ - (19, 0x0007FFE7), - (19, 0x0007FFE8), - (20, 0x000FFFD2), - (20, 0x000FFFD3), - (20, 0x000FFFD4), - (20, 0x000FFFD5), - (20, 0x000FFFD6), - (20, 0x000FFFD7), - (20, 0x000FFFD8), - (19, 0x0007FFDA), - (20, 0x000FFFD9), - (20, 0x000FFFDA), - (20, 0x000FFFDB), - (20, 0x000FFFDC), - (19, 0x0007FFDB), - (20, 0x000FFFDD), - (19, 0x0007FFDC), - (19, 0x0007FFDD), - (20, 0x000FFFDE), - (18, 0x0003FFE4), - (20, 0x000FFFDF), - (20, 0x000FFFE0), - (20, 0x000FFFE1), - (19, 0x0007FFDE), - (20, 0x000FFFE2), - (20, 0x000FFFE3), - (20, 0x000FFFE4), - (19, 0x0007FFDF), - (20, 0x000FFFE5), - (19, 0x0007FFE0), - (18, 0x0003FFE8), - (19, 0x0007FFE1), - (18, 0x0003FFE0), - (18, 0x0003FFE9), - (17, 0x0001FFEF), - (18, 0x0003FFE5), - (17, 0x0001FFEC), - (17, 0x0001FFED), - (17, 0x0001FFEE), - (16, 0x0000FFF4), - (16, 0x0000FFF3), - (16, 0x0000FFF0), - (15, 0x00007FF7), - (15, 0x00007FF6), - (14, 0x00003FFA), - (13, 0x00001FFA), - (13, 0x00001FF9), - (12, 0x00000FFA), - (12, 0x00000FF8), - (11, 0x000007F9), - (10, 0x000003FB), - (9, 0x000001FC), - (9, 0x000001FA), - (8, 0x000000FB), - (7, 0x0000007C), - (6, 0x0000003C), - (5, 0x0000001C), - (4, 0x0000000C), - (3, 0x00000005), - (2, 0x00000001), - (2, 0x00000000), - (3, 0x00000004), - (4, 0x0000000D), - (5, 0x0000001D), - (6, 0x0000003D), - (8, 0x000000FA), - (8, 0x000000FC), - (9, 0x000001FB), - (10, 0x000003FA), - (11, 0x000007F8), - (11, 0x000007FA), - (11, 0x000007FB), - (12, 0x00000FF9), - (12, 0x00000FFB), - (13, 0x00001FF8), - (13, 0x00001FFB), - (14, 0x00003FF8), - (14, 0x00003FF9), - (16, 0x0000FFF1), - (16, 0x0000FFF2), - (17, 0x0001FFEA), - (17, 0x0001FFEB), - (18, 0x0003FFE1), - (18, 0x0003FFE2), - (18, 0x0003FFEA), - (18, 0x0003FFE3), - (18, 0x0003FFE6), - (18, 0x0003FFE7), - (18, 0x0003FFEB), - (20, 0x000FFFE6), - (19, 0x0007FFE2), - (20, 0x000FFFE7), - (20, 0x000FFFE8), - (20, 0x000FFFE9), - (20, 0x000FFFEA), - (20, 0x000FFFEB), - (20, 0x000FFFEC), - (19, 0x0007FFE3), - (20, 0x000FFFED), - (20, 0x000FFFEE), - (20, 0x000FFFEF), - (20, 0x000FFFF0), - (19, 0x0007FFE4), - (20, 0x000FFFF1), - (18, 0x0003FFEC), - (20, 0x000FFFF2), - (20, 0x000FFFF3), - (19, 0x0007FFE5), - (19, 0x0007FFE6), - (20, 0x000FFFF4), - (20, 0x000FFFF5), - (20, 0x000FFFF6), - (20, 0x000FFFF7), - (20, 0x000FFFF8), - (20, 0x000FFFF9), - (20, 0x000FFFFA), - (20, 0x000FFFFB), - (20, 0x000FFFFC), - (20, 0x000FFFFD), - (20, 0x000FFFFE), - (20, 0x000FFFFF), -]; - -/// `t_huffman_env_bal_1_5dB` — ISO/IEC 14496-3 Table 4.A.81 (LAV = 24). -/// -/// 49 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 24`. -pub const T_HUFFMAN_ENV_BAL_1_5DB: [(u8, u32); 49] = [ - (16, 0x0000FFE4), - (16, 0x0000FFE5), - (16, 0x0000FFE6), - (16, 0x0000FFE7), - (16, 0x0000FFE8), - (16, 0x0000FFE9), - (16, 0x0000FFEA), - (16, 0x0000FFEB), - (16, 0x0000FFEC), - (16, 0x0000FFED), - (16, 0x0000FFEE), - (16, 0x0000FFEF), - (16, 0x0000FFF0), - (16, 0x0000FFF1), - (16, 0x0000FFF2), - (16, 0x0000FFF3), - (16, 0x0000FFF4), - (16, 0x0000FFE2), - (12, 0x00000FFC), - (11, 0x000007FC), - (9, 0x000001FE), - (7, 0x0000007E), - (5, 0x0000001E), - (3, 0x00000006), - (1, 0x00000000), - (2, 0x00000002), - (4, 0x0000000E), - (6, 0x0000003E), - (8, 0x000000FE), - (11, 0x000007FD), - (12, 0x00000FFD), - (15, 0x00007FF0), - (16, 0x0000FFE3), - (16, 0x0000FFF5), - (16, 0x0000FFF6), - (16, 0x0000FFF7), - (16, 0x0000FFF8), - (16, 0x0000FFF9), - (16, 0x0000FFFA), - (17, 0x0001FFF6), - (17, 0x0001FFF7), - (17, 0x0001FFF8), - (17, 0x0001FFF9), - (17, 0x0001FFFA), - (17, 0x0001FFFB), - (17, 0x0001FFFC), - (17, 0x0001FFFD), - (17, 0x0001FFFE), - (17, 0x0001FFFF), -]; - -/// `f_huffman_env_bal_1_5dB` — ISO/IEC 14496-3 Table 4.A.82 (LAV = 24). -/// -/// 49 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 24`. -pub const F_HUFFMAN_ENV_BAL_1_5DB: [(u8, u32); 49] = [ - (18, 0x0003FFE2), - (18, 0x0003FFE3), - (18, 0x0003FFE4), - (18, 0x0003FFE5), - (18, 0x0003FFE6), - (18, 0x0003FFE7), - (18, 0x0003FFE8), - (18, 0x0003FFE9), - (18, 0x0003FFEA), - (18, 0x0003FFEB), - (18, 0x0003FFEC), - (18, 0x0003FFED), - (18, 0x0003FFEE), - (18, 0x0003FFEF), - (18, 0x0003FFF0), - (16, 0x0000FFF7), - (17, 0x0001FFF0), - (14, 0x00003FFC), - (11, 0x000007FE), - (11, 0x000007FC), - (8, 0x000000FE), - (7, 0x0000007E), - (4, 0x0000000E), - (2, 0x00000002), - (1, 0x00000000), - (3, 0x00000006), - (5, 0x0000001E), - (6, 0x0000003E), - (9, 0x000001FE), - (11, 0x000007FD), - (12, 0x00000FFE), - (15, 0x00007FFA), - (16, 0x0000FFF6), - (18, 0x0003FFF1), - (18, 0x0003FFF2), - (18, 0x0003FFF3), - (18, 0x0003FFF4), - (18, 0x0003FFF5), - (18, 0x0003FFF6), - (18, 0x0003FFF7), - (18, 0x0003FFF8), - (18, 0x0003FFF9), - (18, 0x0003FFFA), - (18, 0x0003FFFB), - (18, 0x0003FFFC), - (18, 0x0003FFFD), - (18, 0x0003FFFE), - (19, 0x0007FFFE), - (19, 0x0007FFFF), -]; - -/// `t_huffman_env_3_0dB` — ISO/IEC 14496-3 Table 4.A.83 (LAV = 31). -/// -/// 63 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 31`. -pub const T_HUFFMAN_ENV_3_0DB: [(u8, u32); 63] = [ - (18, 0x0003FFED), - (18, 0x0003FFEE), - (19, 0x0007FFDE), - (19, 0x0007FFDF), - (19, 0x0007FFE0), - (19, 0x0007FFE1), - (19, 0x0007FFE2), - (19, 0x0007FFE3), - (19, 0x0007FFE4), - (19, 0x0007FFE5), - (19, 0x0007FFE6), - (19, 0x0007FFE7), - (19, 0x0007FFE8), - (19, 0x0007FFE9), - (19, 0x0007FFEA), - (19, 0x0007FFEB), - (19, 0x0007FFEC), - (17, 0x0001FFF4), - (16, 0x0000FFF7), - (16, 0x0000FFF9), - (16, 0x0000FFF8), - (14, 0x00003FFB), - (14, 0x00003FFA), - (14, 0x00003FF8), - (13, 0x00001FFA), - (12, 0x00000FFC), - (11, 0x000007FC), - (8, 0x000000FE), - (6, 0x0000003E), - (4, 0x0000000E), - (2, 0x00000002), - (1, 0x00000000), - (3, 0x00000006), - (5, 0x0000001E), - (7, 0x0000007E), - (9, 0x000001FE), - (11, 0x000007FD), - (13, 0x00001FFB), - (14, 0x00003FF9), - (14, 0x00003FFC), - (15, 0x00007FFA), - (16, 0x0000FFF6), - (17, 0x0001FFF5), - (18, 0x0003FFEC), - (19, 0x0007FFED), - (19, 0x0007FFEE), - (19, 0x0007FFEF), - (19, 0x0007FFF0), - (19, 0x0007FFF1), - (19, 0x0007FFF2), - (19, 0x0007FFF3), - (19, 0x0007FFF4), - (19, 0x0007FFF5), - (19, 0x0007FFF6), - (19, 0x0007FFF7), - (19, 0x0007FFF8), - (19, 0x0007FFF9), - (19, 0x0007FFFA), - (19, 0x0007FFFB), - (19, 0x0007FFFC), - (19, 0x0007FFFD), - (19, 0x0007FFFE), - (19, 0x0007FFFF), -]; - -/// `f_huffman_env_3_0dB` — ISO/IEC 14496-3 Table 4.A.84 (LAV = 31). -/// -/// 63 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 31`. -pub const F_HUFFMAN_ENV_3_0DB: [(u8, u32); 63] = [ - (20, 0x000FFFF0), - (20, 0x000FFFF1), - (20, 0x000FFFF2), - (20, 0x000FFFF3), - (20, 0x000FFFF4), - (20, 0x000FFFF5), - (20, 0x000FFFF6), - (18, 0x0003FFF3), - (19, 0x0007FFF5), - (19, 0x0007FFEE), - (19, 0x0007FFEF), - (19, 0x0007FFF6), - (18, 0x0003FFF4), - (18, 0x0003FFF2), - (20, 0x000FFFF7), - (19, 0x0007FFF0), - (17, 0x0001FFF5), - (18, 0x0003FFF0), - (17, 0x0001FFF4), - (16, 0x0000FFF7), - (16, 0x0000FFF6), - (15, 0x00007FF8), - (14, 0x00003FFB), - (12, 0x00000FFD), - (11, 0x000007FD), - (10, 0x000003FD), - (9, 0x000001FD), - (8, 0x000000FD), - (6, 0x0000003E), - (4, 0x0000000E), - (2, 0x00000002), - (1, 0x00000000), - (3, 0x00000006), - (5, 0x0000001E), - (8, 0x000000FC), - (9, 0x000001FC), - (10, 0x000003FC), - (11, 0x000007FC), - (12, 0x00000FFC), - (13, 0x00001FFC), - (14, 0x00003FFA), - (15, 0x00007FF9), - (15, 0x00007FFA), - (16, 0x0000FFF8), - (16, 0x0000FFF9), - (17, 0x0001FFF6), - (17, 0x0001FFF7), - (18, 0x0003FFF5), - (18, 0x0003FFF6), - (18, 0x0003FFF1), - (20, 0x000FFFF8), - (19, 0x0007FFF1), - (19, 0x0007FFF2), - (19, 0x0007FFF3), - (20, 0x000FFFF9), - (19, 0x0007FFF7), - (19, 0x0007FFF4), - (20, 0x000FFFFA), - (20, 0x000FFFFB), - (20, 0x000FFFFC), - (20, 0x000FFFFD), - (20, 0x000FFFFE), - (20, 0x000FFFFF), -]; - -/// `t_huffman_env_bal_3_0dB` — ISO/IEC 14496-3 Table 4.A.85 (LAV = 12). -/// -/// 25 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 12`. -pub const T_HUFFMAN_ENV_BAL_3_0DB: [(u8, u32); 25] = [ - (13, 0x00001FF2), - (13, 0x00001FF3), - (13, 0x00001FF4), - (13, 0x00001FF5), - (13, 0x00001FF6), - (13, 0x00001FF7), - (13, 0x00001FF8), - (12, 0x00000FF8), - (8, 0x000000FE), - (7, 0x0000007E), - (4, 0x0000000E), - (3, 0x00000006), - (1, 0x00000000), - (2, 0x00000002), - (5, 0x0000001E), - (6, 0x0000003E), - (9, 0x000001FE), - (13, 0x00001FF9), - (13, 0x00001FFA), - (13, 0x00001FFB), - (13, 0x00001FFC), - (13, 0x00001FFD), - (13, 0x00001FFE), - (14, 0x00003FFE), - (14, 0x00003FFF), -]; - -/// `f_huffman_env_bal_3_0dB` — ISO/IEC 14496-3 Table 4.A.86 (LAV = 12). -/// -/// 25 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 12`. -pub const F_HUFFMAN_ENV_BAL_3_0DB: [(u8, u32); 25] = [ - (13, 0x00001FF7), - (13, 0x00001FF8), - (13, 0x00001FF9), - (13, 0x00001FFA), - (13, 0x00001FFB), - (14, 0x00003FF8), - (14, 0x00003FF9), - (11, 0x000007FC), - (8, 0x000000FE), - (7, 0x0000007E), - (4, 0x0000000E), - (2, 0x00000002), - (1, 0x00000000), - (3, 0x00000006), - (5, 0x0000001E), - (6, 0x0000003E), - (9, 0x000001FE), - (12, 0x00000FFA), - (13, 0x00001FF6), - (14, 0x00003FFA), - (14, 0x00003FFB), - (14, 0x00003FFC), - (14, 0x00003FFD), - (14, 0x00003FFE), - (14, 0x00003FFF), -]; - -/// `t_huffman_noise_3_0dB` — ISO/IEC 14496-3 Table 4.A.87 (LAV = 31). -/// -/// 63 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 31`. -pub const T_HUFFMAN_NOISE_3_0DB: [(u8, u32); 63] = [ - (13, 0x00001FCE), - (13, 0x00001FCF), - (13, 0x00001FD0), - (13, 0x00001FD1), - (13, 0x00001FD2), - (13, 0x00001FD3), - (13, 0x00001FD4), - (13, 0x00001FD5), - (13, 0x00001FD6), - (13, 0x00001FD7), - (13, 0x00001FD8), - (13, 0x00001FD9), - (13, 0x00001FDA), - (13, 0x00001FDB), - (13, 0x00001FDC), - (13, 0x00001FDD), - (13, 0x00001FDE), - (13, 0x00001FDF), - (13, 0x00001FE0), - (13, 0x00001FE1), - (13, 0x00001FE2), - (13, 0x00001FE3), - (13, 0x00001FE4), - (13, 0x00001FE5), - (13, 0x00001FE6), - (13, 0x00001FE7), - (11, 0x000007F2), - (8, 0x000000FD), - (6, 0x0000003E), - (4, 0x0000000E), - (3, 0x00000006), - (1, 0x00000000), - (2, 0x00000002), - (5, 0x0000001E), - (8, 0x000000FC), - (10, 0x000003F8), - (13, 0x00001FCC), - (13, 0x00001FE8), - (13, 0x00001FE9), - (13, 0x00001FEA), - (13, 0x00001FEB), - (13, 0x00001FEC), - (13, 0x00001FCD), - (13, 0x00001FED), - (13, 0x00001FEE), - (13, 0x00001FEF), - (13, 0x00001FF0), - (13, 0x00001FF1), - (13, 0x00001FF2), - (13, 0x00001FF3), - (13, 0x00001FF4), - (13, 0x00001FF5), - (13, 0x00001FF6), - (13, 0x00001FF7), - (13, 0x00001FF8), - (13, 0x00001FF9), - (13, 0x00001FFA), - (13, 0x00001FFB), - (13, 0x00001FFC), - (13, 0x00001FFD), - (13, 0x00001FFE), - (14, 0x00003FFE), - (14, 0x00003FFF), -]; - -/// `t_huffman_noise_bal_3_0dB` — ISO/IEC 14496-3 Table 4.A.88 (LAV = 12). -/// -/// 25 entries `(code_length_bits, codeword)` indexed by the Huffman -/// table index; the decoded value is `index - 12`. -pub const T_HUFFMAN_NOISE_BAL_3_0DB: [(u8, u32); 25] = [ - (8, 0x000000EC), - (8, 0x000000ED), - (8, 0x000000EE), - (8, 0x000000EF), - (8, 0x000000F0), - (8, 0x000000F1), - (8, 0x000000F2), - (8, 0x000000F3), - (8, 0x000000F4), - (8, 0x000000F5), - (5, 0x0000001C), - (2, 0x00000002), - (1, 0x00000000), - (3, 0x00000006), - (6, 0x0000003A), - (8, 0x000000F6), - (8, 0x000000F7), - (8, 0x000000F8), - (8, 0x000000F9), - (8, 0x000000FA), - (8, 0x000000FB), - (8, 0x000000FC), - (8, 0x000000FD), - (8, 0x000000FE), - (8, 0x000000FF), -]; - -/// Resolution / coupling context that picks an envelope or noise -/// codebook pair, per the §4.6.18.3 `sbr_envelope()` / `sbr_noise()` -/// table-selection pseudo-code. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SbrHuffContext { - /// `bs_coupling` — the channel pair is coupled (balance coding). - pub coupling: bool, - /// Channel index within the element (`0` or `1`); only relevant - /// when `coupling` is set (the second coupled channel carries the - /// balance values). - pub ch: bool, - /// `bs_amp_res` — `false` = 1.5 dB resolution, `true` = 3.0 dB. - pub amp_res: bool, -} - -/// One SBR Huffman codebook ready for [`sbr_huff_dec`]: the table -/// slice and its largest-absolute-value (`lav`) offset. -pub type SbrHuffCodebook = (&'static [(u8, u32)], i32); - -/// Returns the `(t_huff, f_huff)` envelope codebook pair for a given -/// `sbr_envelope()` context, per the §4.6.18.3 selection pseudo-code -/// (Table 4.72 surrounding text). `t_huff` is the time-direction -/// table, `f_huff` the frequency-direction table; each is returned as -/// `(slice, lav)`. -pub fn env_tables(ctx: SbrHuffContext) -> (SbrHuffCodebook, SbrHuffCodebook) { - // The balance tables are only ever selected for the *second* - // channel of a coupled pair; otherwise the level tables apply. - if ctx.coupling && ctx.ch { - if ctx.amp_res { - ( - (&T_HUFFMAN_ENV_BAL_3_0DB, 12), - (&F_HUFFMAN_ENV_BAL_3_0DB, 12), - ) - } else { - ( - (&T_HUFFMAN_ENV_BAL_1_5DB, 24), - (&F_HUFFMAN_ENV_BAL_1_5DB, 24), - ) - } - } else if ctx.amp_res { - ((&T_HUFFMAN_ENV_3_0DB, 31), (&F_HUFFMAN_ENV_3_0DB, 31)) - } else { - ((&T_HUFFMAN_ENV_1_5DB, 60), (&F_HUFFMAN_ENV_1_5DB, 60)) - } -} - -/// Returns the `(t_huff, f_huff)` noise codebook pair for a given -/// `sbr_noise()` context, per the §4.6.18.3 selection pseudo-code -/// (Table 4.73 surrounding text). Noise floors are always coded at the -/// 3.0 dB resolution (`bs_amp_res` is "don't care" for noise). Per -/// Table 4.A.78 Note 2 the frequency-direction noise codebooks reuse -/// the 3.0 dB *envelope* frequency codebooks. -pub fn noise_tables(ctx: SbrHuffContext) -> (SbrHuffCodebook, SbrHuffCodebook) { - if ctx.coupling && ctx.ch { - ( - (&T_HUFFMAN_NOISE_BAL_3_0DB, 12), - // f_huffman_noise_bal_3_0dB == f_huffman_env_bal_3_0dB. - (&F_HUFFMAN_ENV_BAL_3_0DB, 12), - ) - } else { - ( - (&T_HUFFMAN_NOISE_3_0DB, 31), - // f_huffman_noise_3_0dB == f_huffman_env_3_0dB. - (&F_HUFFMAN_ENV_3_0DB, 31), - ) - } -} - -/// The longest codeword across every SBR Huffman table is 20 bits -/// (`f_huffman_env_1_5dB` / `f_huffman_env_3_0dB`). `sbr_huff_dec` -/// refuses to read past this many bits without a match (a malformed -/// bitstream would otherwise loop until the reader runs dry). -pub const SBR_HUFF_MAX_CODE_LEN: u32 = 20; - -/// `sbr_huff_dec()` — ISO/IEC 14496-3 Annex 4.A.6.1. -/// -/// Reads bits MSB-first from `reader`, accumulating a codeword, until -/// it matches an entry `(length, codeword)` of `table`. Returns the -/// matching table index minus `lav`, i.e. the signed DPCM delta the -/// envelope / noise reconstruction adds to the running value. -/// -/// Returns [`Error::SbrHuffInvalid`] if no codeword of length up to -/// [`SBR_HUFF_MAX_CODE_LEN`] matches (a corrupt or truncated payload). -pub fn sbr_huff_dec( - reader: &mut oxideav_core::bits::BitReader<'_>, - table: &[(u8, u32)], - lav: i32, -) -> Result { - let mut codeword: u32 = 0; - let mut len: u32 = 0; - loop { - codeword = (codeword << 1) | reader.read_u32(1).map_err(|_| Error::SbrHuffInvalid)?; - len += 1; - for (idx, &(clen, ccode)) in table.iter().enumerate() { - if u32::from(clen) == len && ccode == codeword { - return Ok(idx as i32 - lav); - } - } - if len >= SBR_HUFF_MAX_CODE_LEN { - return Err(Error::SbrHuffInvalid); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use oxideav_core::bits::BitReader; - - /// Every table is complete, codewords fit their declared length, - /// and the table is prefix-free — the canonical-Huffman invariants - /// the spec grids must satisfy. - fn check_table(table: &[(u8, u32)]) { - for &(len, code) in table { - assert!(len >= 1 && len <= SBR_HUFF_MAX_CODE_LEN as u8); - // codeword fits in its declared bit length. - assert!( - code < (1u32 << len), - "codeword 0x{code:08X} overflows its {len}-bit length" - ); - } - // Prefix-free: no codeword is a prefix of another. With - // `lb >= la` (the shorter or equal code is `ca`), truncating - // the longer code `cb` to `la` bits must not equal `ca` — - // covering both the equal-length collision and the strict - // prefix case in one comparison. - for (a, &(la, ca)) in table.iter().enumerate() { - for (b, &(lb, cb)) in table.iter().enumerate() { - if a == b || lb < la { - continue; - } - let shifted = cb >> (lb - la); - assert!(shifted != ca, "prefix conflict between index {a} and {b}"); - } - } - } - - #[test] - fn all_tables_valid() { - check_table(&T_HUFFMAN_ENV_1_5DB); - check_table(&F_HUFFMAN_ENV_1_5DB); - check_table(&T_HUFFMAN_ENV_BAL_1_5DB); - check_table(&F_HUFFMAN_ENV_BAL_1_5DB); - check_table(&T_HUFFMAN_ENV_3_0DB); - check_table(&F_HUFFMAN_ENV_3_0DB); - check_table(&T_HUFFMAN_ENV_BAL_3_0DB); - check_table(&F_HUFFMAN_ENV_BAL_3_0DB); - check_table(&T_HUFFMAN_NOISE_3_0DB); - check_table(&T_HUFFMAN_NOISE_BAL_3_0DB); - } - - #[test] - fn table_sizes_match_lav() { - assert_eq!(T_HUFFMAN_ENV_1_5DB.len(), 121); - assert_eq!(F_HUFFMAN_ENV_1_5DB.len(), 121); - assert_eq!(T_HUFFMAN_ENV_BAL_1_5DB.len(), 49); - assert_eq!(F_HUFFMAN_ENV_BAL_1_5DB.len(), 49); - assert_eq!(T_HUFFMAN_ENV_3_0DB.len(), 63); - assert_eq!(F_HUFFMAN_ENV_3_0DB.len(), 63); - assert_eq!(T_HUFFMAN_ENV_BAL_3_0DB.len(), 25); - assert_eq!(F_HUFFMAN_ENV_BAL_3_0DB.len(), 25); - assert_eq!(T_HUFFMAN_NOISE_3_0DB.len(), 63); - assert_eq!(T_HUFFMAN_NOISE_BAL_3_0DB.len(), 25); - } - - /// Encode each codeword MSB-first into a byte buffer and confirm - /// `sbr_huff_dec` decodes back to `index - lav`. - fn roundtrip(table: &[(u8, u32)], lav: i32) { - for (idx, &(len, code)) in table.iter().enumerate() { - // Pack the codeword MSB-first, then pad to a byte so the - // reader has whole bytes to consume. - let mut bits: Vec = Vec::new(); - for b in (0..len).rev() { - bits.push(((code >> b) & 1) as u8); - } - let mut bytes = vec![0u8; len.div_ceil(8) as usize]; - for (i, &bit) in bits.iter().enumerate() { - if bit != 0 { - bytes[i / 8] |= 1 << (7 - (i % 8)); - } - } - let mut reader = BitReader::new(&bytes); - let got = sbr_huff_dec(&mut reader, table, lav).unwrap(); - assert_eq!(got, idx as i32 - lav, "table index {idx}"); - } - } - - #[test] - fn roundtrip_all() { - roundtrip(&T_HUFFMAN_ENV_1_5DB, 60); - roundtrip(&F_HUFFMAN_ENV_1_5DB, 60); - roundtrip(&T_HUFFMAN_ENV_BAL_1_5DB, 24); - roundtrip(&F_HUFFMAN_ENV_BAL_1_5DB, 24); - roundtrip(&T_HUFFMAN_ENV_3_0DB, 31); - roundtrip(&F_HUFFMAN_ENV_3_0DB, 31); - roundtrip(&T_HUFFMAN_ENV_BAL_3_0DB, 12); - roundtrip(&F_HUFFMAN_ENV_BAL_3_0DB, 12); - roundtrip(&T_HUFFMAN_NOISE_3_0DB, 31); - roundtrip(&T_HUFFMAN_NOISE_BAL_3_0DB, 12); - } - - #[test] - fn context_selectors() { - // Mono / level path picks the level tables. - let ((tt, tl), (ft, fl)) = env_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - assert_eq!(tl, 60); - assert_eq!(fl, 60); - assert_eq!(tt.len(), 121); - assert_eq!(ft.len(), 121); - - // Coupled second channel at 3.0 dB picks the balance tables. - let ((tt, tl), (_ft, _fl)) = env_tables(SbrHuffContext { - coupling: true, - ch: true, - amp_res: true, - }); - assert_eq!(tl, 12); - assert_eq!(tt.len(), 25); - - // Noise freq-direction reuses the 3.0 dB envelope freq table - // (Table 4.A.78 Note 2): same contents, same LAV. - let ((_nt, _nl), (nf, nfl)) = noise_tables(SbrHuffContext { - coupling: false, - ch: false, - amp_res: false, - }); - assert_eq!(nf, &F_HUFFMAN_ENV_3_0DB[..]); - assert_eq!(nfl, 31); - // Coupled noise balance freq-direction reuses the 3.0 dB - // envelope balance freq table. - let ((_nt, _nl), (nf, nfl)) = noise_tables(SbrHuffContext { - coupling: true, - ch: true, - amp_res: false, - }); - assert_eq!(nf, &F_HUFFMAN_ENV_BAL_3_0DB[..]); - assert_eq!(nfl, 12); - } - - #[test] - fn truncated_payload_errors() { - // An empty buffer can never complete a codeword — the first - // bit read fails and maps to SbrHuffInvalid rather than the raw - // bitreader error. - let bytes: [u8; 0] = []; - let mut reader = BitReader::new(&bytes); - assert!(matches!( - sbr_huff_dec(&mut reader, &T_HUFFMAN_ENV_1_5DB, 60), - Err(Error::SbrHuffInvalid) - )); - } - - /// The noise balance table's longest codeword followed by the - /// shortest exercises the bit-at-a-time accumulation past a byte - /// boundary. - #[test] - fn decode_across_byte_boundary() { - // f_huffman_env_1_5dB index 0 is an 18-bit codeword; decode it - // then immediately decode index 60's 2-bit codeword from the - // same stream. - let (l0, c0) = F_HUFFMAN_ENV_1_5DB[0]; - let (l1, c1) = F_HUFFMAN_ENV_1_5DB[60]; - let total = l0 as u32 + l1 as u32; - let combined = (u64::from(c0) << l1) | u64::from(c1); - let nbytes = total.div_ceil(8) as usize; - let mut bytes = vec![0u8; nbytes]; - for b in 0..total { - let bit = (combined >> (total - 1 - b)) & 1; - if bit != 0 { - bytes[(b / 8) as usize] |= 1 << (7 - (b % 8)); - } - } - let mut reader = BitReader::new(&bytes); - assert_eq!( - sbr_huff_dec(&mut reader, &F_HUFFMAN_ENV_1_5DB, 60).unwrap(), - -60 - ); - assert_eq!( - sbr_huff_dec(&mut reader, &F_HUFFMAN_ENV_1_5DB, 60).unwrap(), - 0 - ); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_limiter.rs b/crates/vendor/oxideav-aac/src/sbr_limiter.rs deleted file mode 100644 index f9ea0dfe..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_limiter.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! SBR limiter frequency band table — ISO/IEC 14496-3 §4.6.18.3.2.3 / -//! Figure 4.41. -//! -//! `fTableLim` partitions the SBR range into the bands over which the -//! §4.6.18.7.5 gain limiter averages: either exactly one band -//! (`bs_limiter_bands == 0`) or approximately 1.2 / 2 / 3 bands per -//! octave. The table is a subset of the union of `fTableLow` and the -//! §4.6.18.6 patch borders; the Figure 4.41 walk merges neighbours -//! closer than `0.49 / limBands` octaves, always preferring to keep a -//! patch border over an envelope border (both being patch borders -//! keeps both). -//! -//! ## Provenance -//! -//! The construction is the Figure 4.41 flowchart of the staged spec, -//! with the `limiterBandsPerOctave = {1.2, 2, 3}` selector. No part of -//! this implementation is derived from any external decoder. - -use crate::sbr_freq_bands::HiLoTables; -use crate::{Error, Result}; - -/// §4.6.18.3.2.3 / Figure 4.41 — build `fTableLim`. -/// -/// * `bands` — the derived frequency tables (`fTableLow`, `k_x`, `m`). -/// * `patch_borders` — the §4.6.18.6 patch borders -/// ([`crate::sbr_hf_gen::Patches::borders`], starting at `k_x`). -/// * `bs_limiter_bands` — the 2-bit header field (`0..=3`). -/// -/// Returns the border vector `fTableLim(0..=NL)`. -pub fn limiter_table( - bands: &HiLoTables, - patch_borders: &[i32], - bs_limiter_bands: u8, -) -> Result> { - let f_low = &bands.f_table_low; - if f_low.len() < 2 || bs_limiter_bands > 3 { - return Err(Error::SbrFreqBandInvalid); - } - - // bs_limiter_bands == 0: one band over the whole SBR range. - if bs_limiter_bands == 0 { - return Ok(vec![f_low[0], f_low[f_low.len() - 1]]); - } - - // limiterBandsPerOctave = {1.2, 2, 3}. - let lim_bands = [1.2f64, 2.0, 3.0][usize::from(bs_limiter_bands - 1)]; - - // limTable = fTableLow ∪ interior patch borders, sorted. - let num_patches = patch_borders.len().saturating_sub(1); - let mut lim_table: Vec = f_low.clone(); - if num_patches > 1 { - lim_table.extend_from_slice(&patch_borders[1..num_patches]); - } - lim_table.sort_unstable(); - - // nrLim = NLow + numPatches - 1 (the last index of limTable). - let mut k = 1usize; - while k < lim_table.len() { - if lim_table[k] < 1 || lim_table[k - 1] < 1 { - return Err(Error::SbrFreqBandInvalid); - } - let n_octaves = (f64::from(lim_table[k]) / f64::from(lim_table[k - 1])).log2(); - if n_octaves * lim_bands < 0.49 { - if lim_table[k] == lim_table[k - 1] { - // Duplicate border: drop one copy. - lim_table.remove(k); - } else if !patch_borders.contains(&lim_table[k]) { - // The upper border is droppable (an envelope border). - lim_table.remove(k); - } else if !patch_borders.contains(&lim_table[k - 1]) { - // The upper border is a patch border; drop the lower - // envelope border instead. - lim_table.remove(k - 1); - } else { - // Both are patch borders: keep both. - k += 1; - } - } else { - k += 1; - } - } - - Ok(lim_table) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn bands(f_low: Vec) -> HiLoTables { - let k_x = f_low[0]; - let m = f_low[f_low.len() - 1] - k_x; - HiLoTables { - f_table_high: f_low.clone(), - f_table_low: f_low, - f_table_noise: vec![k_x, k_x + m], - m, - k_x, - } - } - - /// bs_limiter_bands == 0 → exactly one band over the SBR range. - #[test] - fn zero_limiter_bands_is_one_band() { - let b = bands(vec![8, 12, 16, 20, 24]); - let t = limiter_table(&b, &[8, 16, 24], 0).unwrap(); - assert_eq!(t, vec![8, 24]); - } - - /// A single patch adds no interior borders: wide envelope bands - /// pass through untouched. - #[test] - fn single_patch_keeps_envelope_borders() { - let b = bands(vec![8, 12, 16, 20, 24]); - let t = limiter_table(&b, &[8, 24], 3).unwrap(); - assert_eq!(t, vec![8, 12, 16, 20, 24]); - } - - /// A patch border duplicating an envelope border collapses to one - /// entry. - #[test] - fn duplicate_border_removed() { - let b = bands(vec![8, 12, 16, 20, 24]); - // Interior patch border at 16 duplicates fLow's 16. - let t = limiter_table(&b, &[8, 16, 24], 3).unwrap(); - assert_eq!(t, vec![8, 12, 16, 20, 24]); - } - - /// A close pair drops the envelope border and keeps the patch - /// border. - #[test] - fn close_pair_keeps_patch_border() { - // fLow has 15 next to the interior patch border 16: - // log2(16/15)·3 ≈ 0.28 < 0.49 → merge, dropping 15. - let b = bands(vec![8, 12, 15, 20, 24]); - let t = limiter_table(&b, &[8, 16, 24], 3).unwrap(); - assert!(t.contains(&16) && !t.contains(&15), "{t:?}"); - // Borders stay sorted, spanning the SBR range. - assert_eq!(t.first(), Some(&8)); - assert_eq!(t.last(), Some(&24)); - assert!(t.windows(2).all(|w| w[0] < w[1])); - } - - /// A close envelope pair (no patch border involved) drops the - /// upper border. - #[test] - fn close_envelope_pair_drops_upper() { - // 20 and 21 are ~0.07 octaves apart → merged; neither is a - // patch border so the upper (21) goes. - let b = bands(vec![8, 14, 20, 21, 28]); - let t = limiter_table(&b, &[8, 28], 2).unwrap(); - assert_eq!(t, vec![8, 14, 20, 28]); - } - - /// The coarsest per-octave setting (1.2) merges more bands than - /// the finest (3). - #[test] - fn coarser_setting_merges_more() { - let b = bands(vec![8, 9, 10, 12, 14, 17, 20, 24]); - let pb = [8, 24]; - let t1 = limiter_table(&b, &pb, 1).unwrap(); - let t3 = limiter_table(&b, &pb, 3).unwrap(); - assert!(t1.len() <= t3.len(), "{t1:?} vs {t3:?}"); - for t in [&t1, &t3] { - assert_eq!(t.first(), Some(&8)); - assert_eq!(t.last(), Some(&24)); - assert!(t.windows(2).all(|w| w[0] < w[1])); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_lp.rs b/crates/vendor/oxideav-aac/src/sbr_lp.rs deleted file mode 100644 index 1dcb8455..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_lp.rs +++ /dev/null @@ -1,331 +0,0 @@ -//! Low-power SBR aliasing detection and reduction — ISO/IEC 14496-3 -//! §4.6.18.8.3 / §4.6.18.8.5. -//! -//! The low-power SBR tool processes real-valued subband signals, so -//! the per-subband gains applied by the §4.6.18.7 envelope adjuster -//! can introduce audible aliasing between adjacent QMF subbands. This -//! module implements the countermeasure: -//! -//! * **Aliasing degree** (§4.6.18.8.3 / Figure 4.53) — from the -//! per-subband reflection coefficients -//! [`crate::sbr_hf_gen::reflection_coefficient`], the `deg` vector -//! marking low-band subband pairs whose spectral orientation makes -//! gain steps alias. -//! * **Patched degree** — `degPatched`, the low-band degrees carried -//! onto the SBR range through the §4.6.18.6 patch mapping (zero at -//! every patch start and beyond the patch coverage). -//! * **Gain grouping** (Figure 4.54) — the per-envelope `FGroup` -//! start/stop index pairs bracketing runs of aliasing-prone, -//! sinusoid-free subbands. -//! * **Aliasing reduction** (§4.6.18.8.5) — the `GLimBoost → GA` gain -//! re-calculation: per group, a target gain from the group energies, -//! the `α(m)`-weighted blend, and the exact energy-restoring -//! normalization. -//! -//! ## Provenance -//! -//! Every formula and branch is from the §4.6.18.8.3 / §4.6.18.8.5 text -//! and the Figure 4.53 / 4.54 flowcharts of the staged spec. No part -//! of this implementation is derived from any external decoder. - -use crate::sbr_env_adjust::EPS0; -use crate::sbr_hf_gen::Patches; -use crate::{Error, Result}; - -/// §4.6.18.8.3 / Figure 4.53 — the aliasing degree `deg(k)` of every -/// low-band subband, from the reflection coefficients `ref(k)` -/// (`0 ≤ k < k0`). Entries 0 and 1 are always zero (the flowchart -/// starts at `k = 2` after forcing `ref(0) = 0`, `deg(1) = 0`). -#[must_use] -pub fn aliasing_degree(refl: &[f64]) -> Vec { - let k0 = refl.len(); - let mut deg = vec![0.0f64; k0]; - let mut refl = refl.to_vec(); - if !refl.is_empty() { - refl[0] = 0.0; - } - let mut k = 2usize; - while k < k0 { - deg[k] = 0.0; - // Even subbands alias on a negative reflection, odd subbands - // on a positive one; other orientations are alias-free. - let sign = if k % 2 == 0 && refl[k] < 0.0 { - 1.0 - } else if k % 2 == 1 && refl[k] > 0.0 { - -1.0 - } else { - k += 1; - continue; - }; - if sign * refl[k - 1] < 0.0 { - deg[k] = 1.0; - if sign * refl[k - 2] > 0.0 { - deg[k - 1] = 1.0 - refl[k - 1] * refl[k - 1]; - } - } else if sign * refl[k - 2] > 0.0 { - deg[k] = 1.0 - refl[k - 1] * refl[k - 1]; - } - k += 1; - } - deg -} - -/// §4.6.18.8.3 — `degPatched(k)` over the SBR range, `kx`-relative -/// (`m` entries): each patch carries the source subband's degree, the -/// first subband of every patch (`x == 0`) and the region beyond the -/// patch coverage are zero. -pub fn deg_patched(deg: &[f64], patches: &Patches, k_x: i32, m: i32) -> Result> { - let m_cnt = usize::try_from(m).map_err(|_| Error::SbrFreqBandInvalid)?; - if k_x < 0 { - return Err(Error::SbrFreqBandInvalid); - } - let mut dp = vec![0.0f64; m_cnt]; - let mut k_off = 0usize; - for (&start, &num) in patches.start.iter().zip(patches.num.iter()) { - for x in 0..num { - let rel = k_off + x; - if rel >= m_cnt { - break; - } - let p = start + x; - dp[rel] = if x == 0 { - 0.0 - } else { - deg.get(p).copied().ok_or(Error::SbrFreqBandInvalid)? - }; - } - k_off += num; - } - Ok(dp) -} - -/// Figure 4.54 — the gain groups of one SBR envelope: `(start, stop)` -/// absolute-QMF-subband pairs (`stop` exclusive), bracketing runs -/// where the *next* subband boundary is aliasing-prone -/// (`degPatched(k+1) ≠ 0`) and no sinusoid is mapped. -/// -/// `dp` is the `kx`-relative `degPatched` (length `M`), `s_mapped` the -/// envelope's `SMapped` row (length `M`). -#[must_use] -pub fn gain_groups(dp: &[f64], s_mapped: &[bool], k_x: i32) -> Vec<(usize, usize)> { - let m_cnt = dp.len().min(s_mapped.len()); - let kx = k_x.max(0) as usize; - let mut groups: Vec<(usize, usize)> = Vec::new(); - let mut open: Option = None; - // k walks kx .. kx + M − 1 (exclusive), exactly the flowchart loop. - for rel in 0..m_cnt.saturating_sub(1) { - let k = kx + rel; - if dp[rel + 1] != 0.0 && !s_mapped[rel] { - if open.is_none() { - open = Some(k); - } - } else if let Some(start) = open.take() { - // Close the group: past the current subband when it is - // sinusoid-free, before it otherwise. - let stop = if s_mapped[rel] { k } else { k + 1 }; - groups.push((start, stop)); - } - } - if let Some(start) = open { - groups.push((start, kx + m_cnt)); - } - groups -} - -/// §4.6.18.8.5 — recompute the limiter/boost gains `GLimBoost` of one -/// envelope into the aliasing-reduced `GA`, in place. -/// -/// `g` is the envelope's `GLimBoost` row and `e_curr` its `ECurr` row -/// (both `kx`-relative, length `M`); `dp` the `kx`-relative -/// `degPatched`; `groups` the Figure 4.54 gain groups (absolute -/// subband indices). Subbands outside every group keep `GLimBoost`. -pub fn aliasing_reduction( - g: &mut [f64], - e_curr: &[f64], - dp: &[f64], - groups: &[(usize, usize)], - k_x: i32, -) -> Result<()> { - let m_cnt = g.len(); - if e_curr.len() != m_cnt || dp.len() != m_cnt || k_x < 0 { - return Err(Error::SbrFreqBandInvalid); - } - let kx = k_x as usize; - for &(start, stop) in groups { - if start < kx || stop > kx + m_cnt || start >= stop { - return Err(Error::SbrFreqBandInvalid); - } - let lo = start - kx; - let hi = stop - kx; - // ETotal: the group energy the GLimBoost gains would produce. - let mut e_total = 0.0f64; - let mut e_curr_sum = 0.0f64; - for i in lo..hi { - e_total += g[i] * g[i] * e_curr[i]; - e_curr_sum += e_curr[i]; - } - // GTarget²: the group-equalized gain. - let g_target2 = e_total / (EPS0 + e_curr_sum); - // α(m)-weighted blend into G²ARtemp. - let mut g_ar2 = vec![0.0f64; hi - lo]; - for i in lo..hi { - let alpha = if i + 1 < m_cnt { - dp[i].max(dp[i + 1]) - } else { - dp[i] - }; - g_ar2[i - lo] = alpha * g_target2 + (1.0 - alpha) * g[i] * g[i]; - } - // Restore the exact group output energy. - let mut e_total_new = 0.0f64; - for (i, &ga2) in (lo..hi).zip(g_ar2.iter()) { - e_total_new += ga2 * e_curr[i]; - } - let scale2 = e_total / (EPS0 + e_total_new); - for (i, &ga2) in (lo..hi).zip(g_ar2.iter()) { - g[i] = (ga2 * scale2).sqrt(); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Figure 4.53 hand-walked: an even subband with a negative - /// reflection whose left neighbour also reflects negatively is a - /// full-degree alias pair, and a positive `ref(k−2)` marks the - /// neighbour too. - #[test] - fn aliasing_degree_even_subband_cases() { - // k = 2: sign = 1 (ref[2] < 0); sign·ref[1] < 0 → deg[2] = 1; - // sign·ref[0] forced 0 → no deg[1] update. - let deg = aliasing_degree(&[0.9, -0.6, -0.5, 0.0]); - assert_eq!(deg, vec![0.0, 0.0, 1.0, 0.0]); - - // k = 4: sign = 1; ref[3] = −0.4 < 0 → deg[4] = 1, and ref[2] - // = 0.5 > 0 marks the neighbour: deg[3] = 1 − 0.4² = 0.84 - // (k = 2 and k = 3 fire on neither orientation). - let deg = aliasing_degree(&[0.0, 0.0, 0.5, -0.4, -0.3]); - assert_eq!(deg[4], 1.0); - assert!((deg[3] - 0.84).abs() < 1e-12); - assert_eq!(°[..3], &[0.0, 0.0, 0.0]); - - // k = 2 with ref[1] < 0 and ref[0]... ref[0] is forced to 0 by - // the flowchart even when transmitted non-zero. - let deg = aliasing_degree(&[0.9, 0.2, -0.5, 0.0]); - assert_eq!(deg[2], 0.0, "no alias: sign·ref[1] > 0, ref[0] forced 0"); - } - - /// Figure 4.53 odd-subband orientation: positive reflection at an - /// odd `k` with a positive left neighbour (sign = −1 → - /// sign·ref[k−1] < 0) is a full-degree alias, and `ref(k−2) < 0` - /// marks the neighbour. - #[test] - fn aliasing_degree_odd_subband_cases() { - // k = 3: ref[3] > 0 → sign = −1; −ref[2] < 0 (ref[2] > 0) → - // deg[3] = 1; −ref[1] > 0 (ref[1] < 0) → deg[2] = 1 − ref[2]². - let deg = aliasing_degree(&[0.0, -0.8, 0.3, 0.7]); - assert_eq!(deg[3], 1.0); - assert!((deg[2] - (1.0 - 0.09)).abs() < 1e-12); - - // Odd-k else-branch: k = 2 fires first (ref[2] < 0, ref[1] < - // 0 → deg[2] = 1), then k = 3: −ref[2] = 0.3 ≥ 0 → else; - // −ref[1] = 0.6 > 0 → deg[3] = 1 − ref[2]² = 0.91. - let deg = aliasing_degree(&[0.0, -0.6, -0.3, 0.7]); - assert_eq!(deg[2], 1.0); - assert!((deg[3] - 0.91).abs() < 1e-12); - } - - /// `degPatched`: the source degrees ride the patch mapping, patch - /// starts and uncovered tail are zero. - #[test] - fn deg_patched_rides_patches() { - let patches = Patches { - start: vec![2, 4], - num: vec![3, 2], - }; - // deg over the low band 0..k0. - let deg = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]; - // M = 7: patches cover 5 subbands, tail of 2 stays zero. - let dp = deg_patched(°, &patches, 10, 7).unwrap(); - // Patch 0 (src 2..5): x=0 → 0, then deg[3], deg[4]. - // Patch 1 (src 4..6): x=0 → 0, then deg[5]. - assert_eq!(dp, vec![0.0, 0.3, 0.4, 0.0, 0.5, 0.0, 0.0]); - } - - /// Figure 4.54 hand-walk: a run of alias-prone boundaries opens a - /// group at its first subband and closes it past the last one; a - /// mapped sinusoid closes the group *before* the sinusoid subband; - /// a run reaching the loop end closes at `kx + M`. - #[test] - fn gain_groups_hand_walk() { - let kx = 8; - // dp[1], dp[2] non-zero → boundaries after subbands 0 and 1. - let dp = [0.0, 1.0, 0.5, 0.0, 0.0, 0.0]; - let sm = [false; 6]; - assert_eq!(gain_groups(&dp, &sm, kx), vec![(8, 11)]); - - // A sinusoid at rel 1 blocks the group from covering it: the - // open condition fails at rel 1, and the close lands at k - // (the sinusoid subband) rather than k + 1. - let sm = [false, true, false, false, false, false]; - let dp = [0.0, 1.0, 1.0, 1.0, 0.0, 0.0]; - assert_eq!(gain_groups(&dp, &sm, kx), vec![(8, 9), (10, 12)]); - - // A run whose alias boundaries reach the end of the SBR range - // closes at kx + M. - let sm = [false; 6]; - let dp = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0]; - assert_eq!(gain_groups(&dp, &sm, kx), vec![(11, 14)]); - } - - /// §4.6.18.8.5: the group output energy under GA equals the - /// GLimBoost energy exactly (the ETotal/ETotalNew normalization), - /// and a full-degree group equalizes the gains. - #[test] - fn aliasing_reduction_preserves_group_energy() { - let kx = 8; - let e_curr = [4.0, 1.0, 9.0, 2.0]; - let mut g = [3.0, 0.5, 1.0, 2.0]; - let dp = [0.0, 1.0, 1.0, 1.0]; - let groups = vec![(8usize, 12usize)]; - let e_before: f64 = g - .iter() - .zip(e_curr.iter()) - .map(|(gi, ei)| gi * gi * ei) - .sum(); - aliasing_reduction(&mut g, &e_curr, &dp, &groups, kx).unwrap(); - let e_after: f64 = g - .iter() - .zip(e_curr.iter()) - .map(|(gi, ei)| gi * gi * ei) - .sum(); - assert!( - (e_after - e_before).abs() < 1e-6 * e_before, - "group energy {e_after} vs {e_before}" - ); - // α = 1 on every interior subband → gains equalize to the - // target (the last subband blends with α = dp[3] = 1 too). - for w in g.windows(2) { - assert!((w[0] - w[1]).abs() < 1e-9, "gains not equalized: {g:?}"); - } - } - - /// Subbands outside every group keep their GLimBoost value. - #[test] - fn aliasing_reduction_leaves_ungrouped_gains() { - let kx = 0; - let e_curr = [1.0, 1.0, 1.0, 1.0]; - let mut g = [1.0, 2.0, 3.0, 4.0]; - let dp = [0.0, 0.6, 0.0, 0.0]; - let groups = vec![(0usize, 2usize)]; - aliasing_reduction(&mut g, &e_curr, &dp, &groups, kx).unwrap(); - assert_eq!(g[2], 3.0); - assert_eq!(g[3], 4.0); - // The grouped pair's energy is preserved. - let e = g[0] * g[0] + g[1] * g[1]; - assert!((e - 5.0).abs() < 1e-9, "group energy {e}"); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_noise_table.rs b/crates/vendor/oxideav-aac/src/sbr_noise_table.rs deleted file mode 100644 index c25a7728..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_noise_table.rs +++ /dev/null @@ -1,564 +0,0 @@ -//! SBR noise table `V` — ISO/IEC 14496-3 Table 4.A.91. -//! -//! The 512-entry complex pseudo-noise sequence -//! `(φRe,noise(i), φIm,noise(i))` that the §4.6.18.7.6 HF assembly adds -//! at the `QFilt` level, indexed by the running -//! `fIndexNoise = (indexNoise + (i − RATE·tE(0))·M + m + 1) mod 512`. -//! -//! Transcribed digit-for-digit from the Table 4.A.91 grid of the -//! staged ISO/IEC 14496-3:2009 spec PDF (`docs/audio/aac/`); the -//! staged `sbr-tables/sbr-random-phase.csv` data table cross-checks -//! every present value to 1e-9 (the CSV is missing one of the 1024 -//! scalars, so the spec grid is the authoritative source here). -//! -//! ## Provenance -//! -//! Numeric data only, from the staged spec PDF. No part of this table -//! or its indexing is derived from any external decoder -//! implementation. - -/// Table 4.A.91 — `V(0, i) + i·V(1, i)` as `(re, im)` pairs. -#[rustfmt::skip] -pub const NOISE_TABLE: [(f64, f64); 512] = [ - (-0.99948153278296, -0.59483417516607), - (0.97113454393991, -0.67528515225647), - (0.14130051758487, -0.95090983575689), - (-0.47005496701697, -0.37340549728647), - (0.80705063769351, 0.29653668284408), - (-0.38981478896926, 0.89572605717087), - (-0.01053049862020, -0.66959058036166), - (-0.91266367957293, -0.11522938140034), - (0.54840422910309, 0.75221367176302), - (0.40009252867955, -0.98929400334421), - (-0.99867974711855, -0.88147068645358), - (-0.95531076805040, 0.90908757154593), - (-0.45725933317144, -0.56716323646760), - (-0.72929675029275, -0.98008272727324), - (0.75622801399036, 0.20950329995549), - (0.07069442601050, -0.78247898470706), - (0.74496252926055, -0.91169004445807), - (-0.96440182703856, -0.94739918296622), - (0.30424629369539, -0.49438267012479), - (0.66565033746925, 0.64652935542491), - (0.91697008020594, 0.17514097332009), - (-0.70774918760427, 0.52548653416543), - (-0.70051415345560, -0.45340028808763), - (-0.99496513054797, -0.90071908066973), - (0.98164490790123, -0.77463155528697), - (-0.54671580548181, -0.02570928536004), - (-0.01689629065389, 0.00287506445732), - (-0.86110349531986, 0.42548583726477), - (-0.98892980586032, -0.87881132267556), - (0.51756627678691, 0.66926784710139), - (-0.99635026409640, -0.58107730574765), - (-0.99969370862163, 0.98369989360250), - (0.55266258627194, 0.59449057465591), - (0.34581177741673, 0.94879421061866), - (0.62664209577999, -0.74402970906471), - (-0.77149701404973, -0.33883658042801), - (-0.91592244254432, 0.03687901376713), - (-0.76285492357887, -0.91371867919124), - (0.79788337195331, -0.93180971199849), - (0.54473080610200, -0.11919206037186), - (-0.85639281671058, 0.42429854760451), - (-0.92882402971423, 0.27871809078609), - (-0.11708371046774, -0.99800843444966), - (0.21356749817493, -0.90716295627033), - (-0.76191692573909, 0.99768118356265), - (0.98111043100884, -0.95854459734407), - (-0.85913269895572, 0.95766566168880), - (-0.93307242253692, 0.49431757696466), - (0.30485754879632, -0.70540034357529), - (0.85289650925190, 0.46766131791044), - (0.91328082618125, -0.99839597361769), - (-0.05890199924154, 0.70741827819497), - (0.28398686150148, 0.34633555702188), - (0.95258164539612, -0.54893416026939), - (-0.78566324168507, -0.75568541079691), - (-0.95789495447877, -0.20423194696966), - (0.82411158711197, 0.96654618432562), - (-0.65185446735885, -0.88734990773289), - (-0.93643603134666, 0.99870790442385), - (0.91427159529618, -0.98290505544444), - (-0.70395684036886, 0.58796798221039), - (0.00563771969365, 0.61768196727244), - (0.89065051931895, 0.52783352697585), - (-0.68683707712762, 0.80806944710339), - (0.72165342518718, -0.69259857349564), - (-0.62928247730667, 0.13627037407335), - (0.29938434065514, -0.46051329682246), - (-0.91781958879280, -0.74012716684186), - (0.99298717043688, 0.40816610075661), - (0.82368298622748, -0.74036047190173), - (-0.98512833386833, -0.99972330709594), - (-0.95915368242257, -0.99237800466040), - (-0.21411126572790, -0.93424819052545), - (-0.68821476106884, -0.26892306315457), - (0.91851997982317, 0.09358228901785), - (-0.96062769559127, 0.36099095133739), - (0.51646184922287, -0.71373332873917), - (0.61130721139669, 0.46950141175917), - (0.47336129371299, -0.27333178296162), - (0.90998308703519, 0.96715662938132), - (0.44844799194357, 0.99211574628306), - (0.66614891079092, 0.96590176169121), - (0.74922239129237, -0.89879858826087), - (-0.99571588506485, 0.52785521494349), - (0.97401082477563, -0.16855870075190), - (0.72683747733879, -0.48060774432251), - (0.95432193457128, 0.68849603408441), - (-0.72962208425191, -0.76608443420917), - (-0.85359479233537, 0.88738125901579), - (-0.81412430338535, -0.97480768049637), - (-0.87930772356786, 0.74748307690436), - (-0.71573331064977, -0.98570608178923), - (0.83524300028228, 0.83702537075163), - (-0.48086065601423, -0.98848504923531), - (0.97139128574778, 0.80093621198236), - (0.51992825347895, 0.80247631400510), - (-0.00848591195325, -0.76670128000486), - (-0.70294374303036, 0.55359910445577), - (-0.95894428168140, -0.43265504344783), - (0.97079252950321, 0.09325857238682), - (-0.92404293670797, 0.85507704027855), - (-0.69506469500450, 0.98633412625459), - (0.26559203620024, 0.73314307966524), - (0.28038443336943, 0.14537913654427), - (-0.74138124825523, 0.99310339807762), - (-0.01752795995444, -0.82616635284178), - (-0.55126773094930, -0.98898543862153), - (0.97960898850996, -0.94021446752851), - (-0.99196309146936, 0.67019017358456), - (-0.67684928085260, 0.12631491649378), - (0.09140039465500, -0.20537731453108), - (-0.71658965751996, -0.97788200391224), - (0.81014640078925, 0.53722648362443), - (0.40616991671205, -0.26469008598449), - (-0.67680188682972, 0.94502052337695), - (0.86849774348749, -0.18333598647899), - (-0.99500381284851, -0.02634122068550), - (0.84329189340667, 0.10406957462213), - (-0.09215968531446, 0.69540012101253), - (0.99956173327206, -0.12358542001404), - (-0.79732779473535, -0.91582524736159), - (0.96349973642406, 0.96640458041000), - (-0.79942778496547, 0.64323902822857), - (-0.11566039853896, 0.28587846253726), - (-0.39922954514662, 0.94129601616966), - (0.99089197565987, -0.92062625581587), - (0.28631285179909, -0.91035047143603), - (-0.83302725605608, -0.67330410892084), - (0.95404443402072, 0.49162765398743), - (-0.06449863579434, 0.03250560813135), - (-0.99575054486311, 0.42389784469507), - (-0.65501142790847, 0.82546114655624), - (-0.81254441908887, -0.51627234660629), - (-0.99646369485481, 0.84490533520752), - (0.00287840603348, 0.64768261158166), - (0.70176989408455, -0.20453028573322), - (0.96361882270190, 0.40706967140989), - (-0.68883758192426, 0.91338958840772), - (-0.34875585502238, 0.71472290693300), - (0.91980081243087, 0.66507455644919), - (-0.99009048343881, 0.85868021604848), - (0.68865791458395, 0.55660316809678), - (-0.99484402129368, -0.20052559254934), - (0.94214511408023, -0.99696425367461), - (-0.67414626793544, 0.49548221180078), - (-0.47339353684664, -0.85904328834047), - (0.14323651387360, -0.94145598222488), - (-0.29268293575672, 0.05759224927952), - (0.43793861458754, -0.78904969892724), - (-0.36345126374441, 0.64874435357162), - (-0.08750604656825, 0.97686944362527), - (-0.96495267812511, -0.53960305946511), - (0.55526940659947, 0.78891523734774), - (0.73538215752630, 0.96452072373404), - (-0.30889773919437, -0.80664389776860), - (0.03574995626194, -0.97325616900959), - (0.98720684660488, 0.48409133691962), - (-0.81689296271203, -0.90827703628298), - (0.67866860118215, 0.81284503870856), - (-0.15808569732583, 0.85279555024382), - (0.80723395114371, -0.24717418514605), - (0.47788757329038, -0.46333147839295), - (0.96367554763201, 0.38486749303242), - (-0.99143875716818, -0.24945277239809), - (0.83081876925833, -0.94780851414763), - (-0.58753191905341, 0.01290772389163), - (0.95538108220960, -0.85557052096538), - (-0.96490920476211, -0.64020970923102), - (-0.97327101028521, 0.12378128133110), - (0.91400366022124, 0.57972471346930), - (-0.99925837363824, 0.71084847864067), - (-0.86875903507313, -0.20291699203564), - (-0.26240034795124, -0.68264554369108), - (-0.24664412953388, -0.87642273115183), - (0.02416275806869, 0.27192914288905), - (0.82068619590515, -0.85087787994476), - (0.88547373760759, -0.89636802901469), - (-0.18173078152226, -0.26152145156800), - (0.09355476558534, 0.54845123045604), - (-0.54668414224090, 0.95980774020221), - (0.37050990604091, -0.59910140383171), - (-0.70373594262891, 0.91227665827081), - (-0.34600785879594, -0.99441426144200), - (-0.68774481731008, -0.30238837956299), - (-0.26843291251234, 0.83115668004362), - (0.49072334613242, -0.45359708737775), - (0.38975993093975, 0.95515358099121), - (-0.97757125224150, 0.05305894580606), - (-0.17325552859616, -0.92770672250494), - (0.99948035025744, 0.58285545563426), - (-0.64946246527458, 0.68645507104960), - (-0.12016920576437, -0.57147322153312), - (-0.58947456517751, -0.34847132454388), - (-0.41815140454465, 0.16276422358861), - (0.99885650204884, 0.11136095490444), - (-0.56649614128386, -0.90494866361587), - (0.94138021032330, 0.35281916733018), - (-0.75725076534641, 0.53650549640587), - (0.20541973692630, -0.94435144369918), - (0.99980371023351, 0.79835913565599), - (0.29078277605775, 0.35393777921520), - (-0.62858772103030, 0.38765693387102), - (0.43440904467688, -0.98546330463232), - (-0.98298583762390, 0.21021524625209), - (0.19513029146934, -0.94239832251867), - (-0.95476662400101, 0.98364554179143), - (0.93379635304810, -0.70881994583682), - (-0.85235410573336, -0.08342347966410), - (-0.86425093011245, -0.45795025029466), - (0.38879779059045, 0.97274429344593), - (0.92045124735495, -0.62433652524220), - (0.89162532251878, 0.54950955570563), - (-0.36834336949252, 0.96458298020975), - (0.93891760988045, -0.89968353740388), - (0.99267657565094, -0.03757034316958), - (-0.94063471614176, 0.41332338538963), - (0.99740224117019, -0.16830494996370), - (-0.35899413170555, -0.46633226649613), - (0.05237237274947, -0.25640361602661), - (0.36703583957424, -0.38653265641875), - (0.91653180367913, -0.30587628726597), - (0.69000803499316, 0.90952171386132), - (-0.38658751133527, 0.99501571208985), - (-0.29250814029851, 0.37444994344615), - (-0.60182204677608, 0.86779651036123), - (-0.97418588163217, 0.96468523666475), - (0.88461574003963, 0.57508405276414), - (0.05198933055162, 0.21269661669964), - (-0.53499621979720, 0.97241553731237), - (-0.49429560226497, 0.98183865291903), - (-0.98935142339139, -0.40249159006933), - (-0.98081380091130, -0.72856895534041), - (-0.27338148835532, 0.99950922447209), - (0.06310802338302, -0.54539587529618), - (-0.20461677199539, -0.14209977628489), - (0.66223843141647, 0.72528579940326), - (-0.84764345483665, 0.02372316801261), - (-0.89039863483811, 0.88866581484602), - (0.95903308477986, 0.76744927173873), - (0.73504123909879, -0.03747203173192), - (-0.31744434966056, -0.36834111883652), - (-0.34110827591623, 0.40211222807691), - (0.47803883714199, -0.39423219786288), - (0.98299195879514, 0.01989791390047), - (-0.30963073129751, -0.18076720599336), - (0.99992588229018, -0.26281872094289), - (-0.93149731080767, -0.98313162570490), - (0.99923472302773, -0.80142993767554), - (-0.26024169633417, -0.75999759855752), - (-0.35712514743563, 0.19298963768574), - (-0.99899084509530, 0.74645156992493), - (0.86557171579452, 0.55593866696299), - (0.33408042438752, 0.86185953874709), - (0.99010736374716, 0.04602397576623), - (-0.66694269691195, -0.91643611810148), - (0.64016792079480, 0.15649530836856), - (0.99570534804836, 0.45844586038111), - (-0.63431466947340, 0.21079116459234), - (-0.07706847005931, -0.89581437101329), - (0.98590090577724, 0.88241721133981), - (0.80099335254678, -0.36851896710853), - (0.78368131392666, 0.45506999802597), - (0.08707806671691, 0.80938994918745), - (-0.86811883080712, 0.39347308654705), - (-0.39466529740375, -0.66809432114456), - (0.97875325649683, -0.72467840967746), - (-0.95038560288864, 0.89563219587625), - (0.17005239424212, 0.54683053962658), - (-0.76910792026848, -0.96226617549298), - (0.99743281016846, 0.42697157037567), - (0.95437383549973, 0.97002324109952), - (0.99578905365569, -0.54106826257356), - (0.28058259829990, -0.85361420634036), - (0.85256524470573, -0.64567607735589), - (-0.50608540105128, -0.65846015480300), - (-0.97210735183243, -0.23095213067791), - (0.95424048234441, -0.99240147091219), - (-0.96926570524023, 0.73775654896574), - (0.30872163214726, 0.41514960556126), - (-0.24523839572639, 0.63206633394807), - (-0.33813265086024, -0.38661779441897), - (-0.05826828420146, -0.06940774188029), - (-0.22898461455054, 0.97054853316316), - (-0.18509915019881, 0.47565762892084), - (-0.10488238045009, -0.87769947402394), - (-0.71886586182037, 0.78030982480538), - (0.99793873738654, 0.90041310491497), - (0.57563307626120, -0.91034337352097), - (0.28909646383717, 0.96307783970534), - (0.42188998312520, 0.48148651230437), - (0.93335049681047, -0.43537023883588), - (-0.97087374418267, 0.86636445711364), - (0.36722871286923, 0.65291654172961), - (-0.81093025665696, 0.08778370229363), - (-0.26240603062237, -0.92774095379098), - (0.83996497984604, 0.55839849139647), - (-0.99909615720225, -0.96024605713970), - (0.74649464155061, 0.12144893606462), - (-0.74774595569805, -0.26898062008959), - (0.95781667469567, -0.79047927052628), - (0.95472308713099, -0.08588776019550), - (0.48708332746299, 0.99999041579432), - (0.46332038247497, 0.10964126185063), - (-0.76497004940162, 0.89210929242238), - (0.57397389364339, 0.35289703373760), - (0.75374316974495, 0.96705214651335), - (-0.59174397685714, -0.89405370422752), - (0.75087906691890, -0.29612672982396), - (-0.98607857336230, 0.25034911730023), - (-0.40761056640505, -0.90045573444695), - (0.66929266740477, 0.98629493401748), - (-0.97463695257310, -0.00190223301301), - (0.90145509409859, 0.99781390365446), - (-0.87259289048043, 0.99233587353666), - (-0.91529461447692, -0.15698707534206), - (-0.03305738840705, -0.37205262859764), - (0.07223051368337, -0.88805001733626), - (0.99498012188353, 0.97094358113387), - (-0.74904939500519, 0.99985483641521), - (0.04585228574211, 0.99812337444082), - (-0.89054954257993, -0.31791913188064), - (-0.83782144651251, 0.97637632547466), - (0.33454804933804, -0.86231516800408), - (-0.99707579362824, 0.93237990079441), - (-0.22827527843994, 0.18874759397997), - (0.67248046289143, -0.03646211390569), - (-0.05146538187944, -0.92599700120679), - (0.99947295749905, 0.93625229707912), - (0.66951124390363, 0.98905825623893), - (-0.99602956559179, -0.44654715757688), - (0.82104905483590, 0.99540741724928), - (0.99186510988782, 0.72023001312947), - (-0.65284592392918, 0.52186723253637), - (0.93885443798188, -0.74895312615259), - (0.96735248738388, 0.90891816978629), - (-0.22225968841114, 0.57124029781228), - (-0.44132783753414, -0.92688840659280), - (-0.85694974219574, 0.88844532719844), - (0.91783042091762, -0.46356892383970), - (0.72556974415690, -0.99899555770747), - (-0.99711581834508, 0.58211560180426), - (0.77638976371966, 0.94321834873819), - (0.07717324253925, 0.58638399856595), - (-0.56049829194163, 0.82522301569036), - (0.98398893639988, 0.39467440420569), - (0.47546946844938, 0.68613044836811), - (0.65675089314631, 0.18331637134880), - (0.03273375457980, -0.74933109564108), - (-0.38684144784738, 0.51337349030406), - (-0.97346267944545, -0.96549364384098), - (-0.53282156061942, -0.91423265091354), - (0.99817310731176, 0.61133572482148), - (-0.50254500772635, -0.88829338134294), - (0.01995873238855, 0.85223515096765), - (0.99930381973804, 0.94578896296649), - (0.82907767600783, -0.06323442598128), - (-0.58660709669728, 0.96840773806582), - (-0.17573736667267, -0.48166920859485), - (0.83434292401346, -0.13023450646997), - (0.05946491307025, 0.20511047074866), - (0.81505484574602, -0.94685947861369), - (-0.44976380954860, 0.40894572671545), - (-0.89746474625671, 0.99846578838537), - (0.39677256130792, -0.74854668609359), - (-0.07588948563079, 0.74096214084170), - (0.76343198951445, 0.41746629422634), - (-0.74490104699626, 0.94725911744610), - (0.64880119792759, 0.41336660830571), - (0.62319537462542, -0.93098313552599), - (0.42215817594807, -0.07712787385208), - (0.02704554141885, -0.05417518053666), - (0.80001773566818, 0.91542195141039), - (-0.79351832348816, -0.36208897989136), - (0.63872359151636, 0.08128252493444), - (0.52890520960295, 0.60048872455592), - (0.74238552914587, 0.04491915291044), - (0.99096131449250, -0.19451182854402), - (-0.80412329643109, -0.88513818199457), - (-0.64612616129736, 0.72198674804544), - (0.11657770663191, -0.83662833815041), - (-0.95053182488101, -0.96939905138082), - (-0.62228872928622, 0.82767262846661), - (0.03004475787316, -0.99738896333384), - (-0.97987214341034, 0.36526129686425), - (-0.99986980746200, -0.36021610299715), - (0.89110648599879, -0.97894250343044), - (0.10407960510582, 0.77357793811619), - (0.95964737821728, -0.35435818285502), - (0.50843233159162, 0.96107691266205), - (0.17006334670615, -0.76854025314829), - (0.25872675063360, 0.99893303933816), - (-0.01115998681937, 0.98496019742444), - (-0.79598702973261, 0.97138411318894), - (-0.99264708948101, -0.99542822402536), - (-0.99829663752818, 0.01877138824311), - (-0.70801016548184, 0.33680685948117), - (-0.70467057786826, 0.93272777501857), - (0.99846021905254, -0.98725746254433), - (-0.63364968534650, -0.16473594423746), - (-0.16258217500792, -0.95939125400802), - (-0.43645594360633, -0.94805030113284), - (-0.99848471702976, 0.96245166923809), - (-0.16796458968998, -0.98987511890470), - (-0.87979225745213, -0.71725725041680), - (0.44183099021786, -0.93568974498761), - (0.93310180125532, -0.99913308068246), - (-0.93941931782002, -0.56409379640356), - (-0.88590003188677, 0.47624600491382), - (0.99971463703691, -0.83889954253462), - (-0.75376385639978, 0.00814643438625), - (0.93887685615875, -0.11284528204636), - (0.85126435782309, 0.52349251543547), - (0.39701421446381, 0.81779634174316), - (-0.37024464187437, -0.87071656222959), - (-0.36024828242896, 0.34655735648287), - (-0.93388812549209, -0.84476541096429), - (-0.65298804552119, -0.18439575450921), - (0.11960319006843, 0.99899346780168), - (0.94292565553160, 0.83163906518293), - (0.75081145286948, -0.35533223142265), - (0.56721979748394, -0.24076836414499), - (0.46857766746029, -0.30140233457198), - (0.97312313923635, -0.99548191630031), - (-0.38299976567017, 0.98516909715427), - (0.41025800019463, 0.02116736935734), - (0.09638062008048, 0.04411984381457), - (-0.85283249275397, 0.91475563922421), - (0.88866808958124, -0.99735267083226), - (-0.48202429536989, -0.96805608884164), - (0.27572582416567, 0.58634753335832), - (-0.65889129659168, 0.58835634138583), - (0.98838086953732, 0.99994349600236), - (-0.20651349620689, 0.54593044066355), - (-0.62126416356920, -0.59893681700392), - (0.20320105410437, -0.86879180355289), - (-0.97790548600584, 0.96290806999242), - (0.11112534735126, 0.21484763313301), - (-0.41368337314182, 0.28216837680365), - (0.24133038992960, 0.51294362630238), - (-0.66393410674885, -0.08249679629081), - (-0.53697829178752, -0.97649903936228), - (-0.97224737889348, 0.22081333579837), - (0.87392477144549, -0.12796173740361), - (0.19050361015753, 0.01602615387195), - (-0.46353441212724, -0.95249041539006), - (-0.07064096339021, -0.94479803205886), - (-0.92444085484466, -0.10457590187436), - (-0.83822593578728, -0.01695043208885), - (0.75214681811150, -0.99955681042665), - (-0.42102998829339, 0.99720941999394), - (-0.72094786237696, -0.35008961934255), - (0.78843311019251, 0.52851398958271), - (0.97394027897442, -0.26695944086561), - (0.99206463477946, -0.57010120849429), - (0.76789609461795, -0.76519356730966), - (-0.82002421836409, -0.73530179553767), - (0.81924990025724, 0.99698425250579), - (-0.26719850873357, 0.68903369776193), - (-0.43311260380975, 0.85321815947490), - (0.99194979673836, 0.91876249766422), - (-0.80692001248487, -0.32627540663214), - (0.43080003649976, -0.21919095636638), - (0.67709491937357, -0.95478075822906), - (0.56151770568316, -0.70693811747778), - (0.10831862810749, -0.08628837174592), - (0.91229417540436, -0.65987351408410), - (-0.48972893932274, 0.56289246362686), - (-0.89033658689697, -0.71656563987082), - (0.65269447475094, 0.65916004833932), - (0.67439478141121, -0.81684380846796), - (-0.47770832416973, -0.16789556203025), - (-0.99715979260878, -0.93565784007648), - (-0.90889593602546, 0.62034397054380), - (-0.06618622548177, -0.23812217221359), - (0.99430266919728, 0.18812555317553), - (0.97686402381843, -0.28664534366620), - (0.94813650221268, -0.97506640027128), - (-0.95434497492853, -0.79607978501983), - (-0.49104783137150, 0.32895214359663), - (0.99881175120751, 0.88993983831354), - (0.50449166760303, -0.85995072408434), - (0.47162891065108, -0.18680204049569), - (-0.62081581361840, 0.75000676218956), - (-0.43867015250812, 0.99998069244322), - (0.98630563232075, -0.53578899600662), - (-0.61510362277374, -0.89515019899997), - (-0.03841517601843, -0.69888815681179), - (-0.30102157304644, -0.07667808922205), - (0.41881284182683, 0.02188098922282), - (-0.86135454941237, 0.98947480909359), - (0.67226861393788, -0.13494389011014), - (-0.70737398842068, -0.76547349325992), - (0.94044946687963, 0.09026201157416), - (-0.82386352534327, 0.08924768823676), - (-0.32070666698656, 0.50143421908753), - (0.57593163224487, -0.98966422921509), - (-0.36326018419965, 0.07440243123228), - (0.99979044674350, -0.14130287347405), - (-0.92366023326932, -0.97979298068180), - (-0.44607178518598, -0.54233252016394), - (0.44226800932956, 0.71326756742752), - (0.03671907158312, 0.63606389366675), - (0.52175424682195, -0.85396826735705), - (-0.94701139690956, -0.01826348194255), - (-0.98759606946049, 0.82288714303073), - (0.87434794743625, 0.89399495655433), - (-0.93412041758744, 0.41374052024363), - (0.96063943315511, 0.93116709541280), - (0.97534253457837, 0.86150930812689), - (0.99642466504163, 0.70190043427512), - (-0.94705089665984, -0.29580042814306), - (0.91599807087376, -0.98147830385781), -]; - -#[cfg(test)] -mod tests { - use super::NOISE_TABLE; - - /// Table 4.A.91 spot values (first, last, and an interior row). - #[test] - fn spot_values() { - assert_eq!(NOISE_TABLE[0], (-0.99948153278296, -0.59483417516607)); - assert_eq!(NOISE_TABLE[1], (0.97113454393991, -0.67528515225647)); - assert_eq!(NOISE_TABLE[511], (0.91599807087376, -0.98147830385781)); - } - - /// The sequence is a bounded pseudo-noise table: every component - /// stays within (-1, 1] and the sequence is zero-mean to within a - /// few percent of full scale. - #[test] - fn bounded_and_roughly_zero_mean() { - let mut sum_re = 0.0; - let mut sum_im = 0.0; - for &(re, im) in NOISE_TABLE.iter() { - assert!(re.abs() <= 1.0 && im.abs() <= 1.0); - sum_re += re; - sum_im += im; - } - assert!((sum_re / 512.0).abs() < 0.05); - assert!((sum_im / 512.0).abs() < 0.05); - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_qmf.rs b/crates/vendor/oxideav-aac/src/sbr_qmf.rs deleted file mode 100644 index a1b6bedb..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_qmf.rs +++ /dev/null @@ -1,1005 +0,0 @@ -//! SBR QMF filterbanks — ISO/IEC 14496-3 §4.6.18.4. -//! -//! The complex-exponential-modulated filterbank pair of the SBR tool: -//! -//! * [`AnalysisQmf`] — §4.6.18.4.1 / Figure 4.42: splits the core -//! decoder's time-domain output into 32 complex-valued subband -//! signals (oversampled by two relative to a real QMF bank), one -//! 32-sample slot at a time. -//! * [`SynthesisQmf`] — §4.6.18.4.2 / Figure 4.43: recombines 64 -//! complex subbands into 64 real time-domain samples per slot (the -//! dual-rate output of the SBR tool). -//! * [`DownsampledSynthesisQmf`] — §4.6.18.4.3 / Figure 4.44: the -//! 32-channel variant that keeps the output at the core rate. -//! -//! The low-power SBR tool (§4.6.18.8) replaces the complex banks with -//! real-valued ones (§4.6.18.8.2): -//! -//! * [`RealAnalysisQmf`] — §4.6.18.8.2.2 / Figure 4.50: 32 real-valued, -//! critically sampled subband signals. -//! * [`RealSynthesisQmf`] — §4.6.18.8.2.3 / Figure 4.51: the 64-subband -//! real synthesis bank (dual-rate output). -//! * [`RealDownsampledSynthesisQmf`] — §4.6.18.8.2.4 / Figure 4.52: the -//! 32-channel real variant at the core rate. -//! -//! The 640-tap prototype window `c[i]` is Table 4.A.89, transcribed -//! from the staged ISO/IEC 14496-3:2009 spec PDF (`docs/audio/aac/`). -//! The table prints `c[639]` with nine decimals (`-0.000552528`); every -//! other entry carries ten. The transcription preserves the printed -//! digits verbatim, including the mirror structure -//! `|c[i]| == |c[640 - i]|` that the tests pin. -//! -//! ## Provenance -//! -//! Every constant and loop bound below comes from the §4.6.18.4 text -//! and the Figure 4.42 / 4.43 / 4.44 flowcharts of the staged spec. -//! No part of this implementation is derived from any external decoder. - -use crate::{Error, Result}; - -/// A complex number, as used by the SBR subband domain (§4.6.18.2.2: -/// the subband samples are complex-valued). -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Complex { - /// Real part. - pub re: f64, - /// Imaginary part. - pub im: f64, -} - -impl Complex { - /// `re + i·im`. - #[inline] - #[must_use] - pub fn new(re: f64, im: f64) -> Self { - Complex { re, im } - } - - /// The complex conjugate. - #[inline] - #[must_use] - pub fn conj(self) -> Self { - Complex { - re: self.re, - im: -self.im, - } - } - - /// Squared magnitude `re² + im²`. - #[inline] - #[must_use] - pub fn norm_sqr(self) -> f64 { - self.re * self.re + self.im * self.im - } -} - -impl core::ops::Add for Complex { - type Output = Complex; - #[inline] - fn add(self, rhs: Complex) -> Complex { - Complex::new(self.re + rhs.re, self.im + rhs.im) - } -} - -impl core::ops::Sub for Complex { - type Output = Complex; - #[inline] - fn sub(self, rhs: Complex) -> Complex { - Complex::new(self.re - rhs.re, self.im - rhs.im) - } -} - -impl core::ops::Mul for Complex { - type Output = Complex; - #[inline] - fn mul(self, rhs: Complex) -> Complex { - Complex::new( - self.re * rhs.re - self.im * rhs.im, - self.re * rhs.im + self.im * rhs.re, - ) - } -} - -impl core::ops::Mul for Complex { - type Output = Complex; - #[inline] - fn mul(self, rhs: f64) -> Complex { - Complex::new(self.re * rhs, self.im * rhs) - } -} - -impl core::ops::AddAssign for Complex { - #[inline] - fn add_assign(&mut self, rhs: Complex) { - self.re += rhs.re; - self.im += rhs.im; - } -} - -/// Table 4.A.89 — the 640 coefficients `c[i]` of the QMF bank window, -/// shared by the analysis and both synthesis filterbanks. -#[rustfmt::skip] -pub const QMF_WINDOW: [f64; 640] = [ - 0.0000000000, -0.0005525286, -0.0005617692, -0.0004947518, - -0.0004875227, -0.0004893791, -0.0005040714, -0.0005226564, - -0.0005466565, -0.0005677802, -0.0005870930, -0.0006132747, - -0.0006312493, -0.0006540333, -0.0006777690, -0.0006941614, - -0.0007157736, -0.0007255043, -0.0007440941, -0.0007490598, - -0.0007681371, -0.0007724848, -0.0007834332, -0.0007779869, - -0.0007803664, -0.0007801449, -0.0007757977, -0.0007630793, - -0.0007530001, -0.0007319357, -0.0007215391, -0.0006917937, - -0.0006650415, -0.0006341594, -0.0005946118, -0.0005564576, - -0.0005145572, -0.0004606325, -0.0004095121, -0.0003501175, - -0.0002896981, -0.0002098337, -0.0001446380, -0.0000617334, - 0.0000134949, 0.0001094383, 0.0002043017, 0.0002949531, - 0.0004026540, 0.0005107388, 0.0006239376, 0.0007458025, - 0.0008608443, 0.0009885988, 0.0011250155, 0.0012577884, - 0.0013902494, 0.0015443219, 0.0016868083, 0.0018348265, - 0.0019841140, 0.0021461583, 0.0023017254, 0.0024625616, - 0.0026201758, 0.0027870464, 0.0029469447, 0.0031125420, - 0.0032739613, 0.0034418874, 0.0036008268, 0.0037603922, - 0.0039207432, 0.0040819753, 0.0042264269, 0.0043730719, - 0.0045209852, 0.0046606460, 0.0047932560, 0.0049137603, - 0.0050393022, 0.0051407353, 0.0052461166, 0.0053471681, - 0.0054196775, 0.0054876040, 0.0055475714, 0.0055938023, - 0.0056220643, 0.0056455196, 0.0056389199, 0.0056266114, - 0.0055917128, 0.0055404363, 0.0054753783, 0.0053838975, - 0.0052715758, 0.0051382275, 0.0049839687, 0.0048109469, - 0.0046039530, 0.0043801861, 0.0041251642, 0.0038456408, - 0.0035401246, 0.0032091885, 0.0028446757, 0.0024508540, - 0.0020274176, 0.0015784682, 0.0010902329, 0.0005832264, - 0.0000276045, -0.0005464280, -0.0011568135, -0.0018039472, - -0.0024826723, -0.0031933778, -0.0039401124, -0.0047222596, - -0.0055337211, -0.0063792293, -0.0072615816, -0.0081798233, - -0.0091325329, -0.0101150215, -0.0111315548, -0.0121849995, - 0.0132718220, 0.0143904666, 0.0155405553, 0.0167324712, - 0.0179433381, 0.0191872431, 0.0204531793, 0.0217467550, - 0.0230680169, 0.0244160992, 0.0257875847, 0.0271859429, - 0.0286072173, 0.0300502657, 0.0315017608, 0.0329754081, - 0.0344620948, 0.0359697560, 0.0374812850, 0.0390053679, - 0.0405349170, 0.0420649094, 0.0436097542, 0.0451488405, - 0.0466843027, 0.0482165720, 0.0497385755, 0.0512556155, - 0.0527630746, 0.0542452768, 0.0557173648, 0.0571616450, - 0.0585915683, 0.0599837480, 0.0613455171, 0.0626857808, - 0.0639715898, 0.0652247106, 0.0664367512, 0.0676075985, - 0.0687043828, 0.0697630244, 0.0707628710, 0.0717002673, - 0.0725682583, 0.0733620255, 0.0741003642, 0.0747452558, - 0.0753137336, 0.0758008358, 0.0761992479, 0.0764992170, - 0.0767093490, 0.0768173975, 0.0768230011, 0.0767204924, - 0.0765050718, 0.0761748321, 0.0757305756, 0.0751576255, - 0.0744664394, 0.0736406005, 0.0726774642, 0.0715826364, - 0.0703533073, 0.0689664013, 0.0674525021, 0.0657690668, - 0.0639444805, 0.0619602779, 0.0598166570, 0.0575152691, - 0.0550460034, 0.0524093821, 0.0495978676, 0.0466303305, - 0.0434768782, 0.0401458278, 0.0366418116, 0.0329583930, - 0.0290824006, 0.0250307561, 0.0207997072, 0.0163701258, - 0.0117623832, 0.0069636862, 0.0019765601, -0.0032086896, - -0.0085711749, -0.0141288827, -0.0198834129, -0.0258227288, - -0.0319531274, -0.0382776572, -0.0447806821, -0.0514804176, - -0.0583705326, -0.0654409853, -0.0726943300, -0.0801372934, - -0.0877547536, -0.0955533352, -0.1035329531, -0.1116826931, - -0.1200077984, -0.1285002850, -0.1371551761, -0.1459766491, - -0.1549607071, -0.1640958855, -0.1733808172, -0.1828172548, - -0.1923966745, -0.2021250176, -0.2119735853, -0.2219652696, - -0.2320690870, -0.2423016884, -0.2526480309, -0.2631053299, - -0.2736634040, -0.2843214189, -0.2950716717, -0.3059098575, - -0.3168278913, -0.3278113727, -0.3388722693, -0.3499914122, - 0.3611589903, 0.3723795546, 0.3836350013, 0.3949211761, - 0.4062317676, 0.4175696896, 0.4289119920, 0.4402553754, - 0.4515996535, 0.4629308085, 0.4742453214, 0.4855253091, - 0.4967708254, 0.5079817500, 0.5191234970, 0.5302240895, - 0.5412553448, 0.5522051258, 0.5630789140, 0.5738524131, - 0.5845403235, 0.5951123086, 0.6055783538, 0.6159109932, - 0.6261242695, 0.6361980107, 0.6461269695, 0.6559016302, - 0.6655139880, 0.6749663190, 0.6842353293, 0.6933282376, - 0.7022388719, 0.7109410426, 0.7194462634, 0.7277448900, - 0.7358211758, 0.7436827863, 0.7513137456, 0.7587080760, - 0.7658674865, 0.7727780881, 0.7794287519, 0.7858353120, - 0.7919735841, 0.7978466413, 0.8034485751, 0.8087695004, - 0.8138191270, 0.8185776004, 0.8230419890, 0.8272275347, - 0.8311038457, 0.8346937361, 0.8379717337, 0.8409541392, - 0.8436238281, 0.8459818469, 0.8480315777, 0.8497805198, - 0.8511971524, 0.8523047035, 0.8531020949, 0.8535720573, - 0.8537385600, 0.8535720573, 0.8531020949, 0.8523047035, - 0.8511971524, 0.8497805198, 0.8480315777, 0.8459818469, - 0.8436238281, 0.8409541392, 0.8379717337, 0.8346937361, - 0.8311038457, 0.8272275347, 0.8230419890, 0.8185776004, - 0.8138191270, 0.8087695004, 0.8034485751, 0.7978466413, - 0.7919735841, 0.7858353120, 0.7794287519, 0.7727780881, - 0.7658674865, 0.7587080760, 0.7513137456, 0.7436827863, - 0.7358211758, 0.7277448900, 0.7194462634, 0.7109410426, - 0.7022388719, 0.6933282376, 0.6842353293, 0.6749663190, - 0.6655139880, 0.6559016302, 0.6461269695, 0.6361980107, - 0.6261242695, 0.6159109932, 0.6055783538, 0.5951123086, - 0.5845403235, 0.5738524131, 0.5630789140, 0.5522051258, - 0.5412553448, 0.5302240895, 0.5191234970, 0.5079817500, - 0.4967708254, 0.4855253091, 0.4742453214, 0.4629308085, - 0.4515996535, 0.4402553754, 0.4289119920, 0.4175696896, - 0.4062317676, 0.3949211761, 0.3836350013, 0.3723795546, - -0.3611589903, -0.3499914122, -0.3388722693, -0.3278113727, - -0.3168278913, -0.3059098575, -0.2950716717, -0.2843214189, - -0.2736634040, -0.2631053299, -0.2526480309, -0.2423016884, - -0.2320690870, -0.2219652696, -0.2119735853, -0.2021250176, - -0.1923966745, -0.1828172548, -0.1733808172, -0.1640958855, - -0.1549607071, -0.1459766491, -0.1371551761, -0.1285002850, - -0.1200077984, -0.1116826931, -0.1035329531, -0.0955533352, - -0.0877547536, -0.0801372934, -0.0726943300, -0.0654409853, - -0.0583705326, -0.0514804176, -0.0447806821, -0.0382776572, - -0.0319531274, -0.0258227288, -0.0198834129, -0.0141288827, - -0.0085711749, -0.0032086896, 0.0019765601, 0.0069636862, - 0.0117623832, 0.0163701258, 0.0207997072, 0.0250307561, - 0.0290824006, 0.0329583930, 0.0366418116, 0.0401458278, - 0.0434768782, 0.0466303305, 0.0495978676, 0.0524093821, - 0.0550460034, 0.0575152691, 0.0598166570, 0.0619602779, - 0.0639444805, 0.0657690668, 0.0674525021, 0.0689664013, - 0.0703533073, 0.0715826364, 0.0726774642, 0.0736406005, - 0.0744664394, 0.0751576255, 0.0757305756, 0.0761748321, - 0.0765050718, 0.0767204924, 0.0768230011, 0.0768173975, - 0.0767093490, 0.0764992170, 0.0761992479, 0.0758008358, - 0.0753137336, 0.0747452558, 0.0741003642, 0.0733620255, - 0.0725682583, 0.0717002673, 0.0707628710, 0.0697630244, - 0.0687043828, 0.0676075985, 0.0664367512, 0.0652247106, - 0.0639715898, 0.0626857808, 0.0613455171, 0.0599837480, - 0.0585915683, 0.0571616450, 0.0557173648, 0.0542452768, - 0.0527630746, 0.0512556155, 0.0497385755, 0.0482165720, - 0.0466843027, 0.0451488405, 0.0436097542, 0.0420649094, - 0.0405349170, 0.0390053679, 0.0374812850, 0.0359697560, - 0.0344620948, 0.0329754081, 0.0315017608, 0.0300502657, - 0.0286072173, 0.0271859429, 0.0257875847, 0.0244160992, - 0.0230680169, 0.0217467550, 0.0204531793, 0.0191872431, - 0.0179433381, 0.0167324712, 0.0155405553, 0.0143904666, - -0.0132718220, -0.0121849995, -0.0111315548, -0.0101150215, - -0.0091325329, -0.0081798233, -0.0072615816, -0.0063792293, - -0.0055337211, -0.0047222596, -0.0039401124, -0.0031933778, - -0.0024826723, -0.0018039472, -0.0011568135, -0.0005464280, - 0.0000276045, 0.0005832264, 0.0010902329, 0.0015784682, - 0.0020274176, 0.0024508540, 0.0028446757, 0.0032091885, - 0.0035401246, 0.0038456408, 0.0041251642, 0.0043801861, - 0.0046039530, 0.0048109469, 0.0049839687, 0.0051382275, - 0.0052715758, 0.0053838975, 0.0054753783, 0.0055404363, - 0.0055917128, 0.0056266114, 0.0056389199, 0.0056455196, - 0.0056220643, 0.0055938023, 0.0055475714, 0.0054876040, - 0.0054196775, 0.0053471681, 0.0052461166, 0.0051407353, - 0.0050393022, 0.0049137603, 0.0047932560, 0.0046606460, - 0.0045209852, 0.0043730719, 0.0042264269, 0.0040819753, - 0.0039207432, 0.0037603922, 0.0036008268, 0.0034418874, - 0.0032739613, 0.0031125420, 0.0029469447, 0.0027870464, - 0.0026201758, 0.0024625616, 0.0023017254, 0.0021461583, - 0.0019841140, 0.0018348265, 0.0016868083, 0.0015443219, - 0.0013902494, 0.0012577884, 0.0011250155, 0.0009885988, - 0.0008608443, 0.0007458025, 0.0006239376, 0.0005107388, - 0.0004026540, 0.0002949531, 0.0002043017, 0.0001094383, - 0.0000134949, -0.0000617334, -0.0001446380, -0.0002098337, - -0.0002896981, -0.0003501175, -0.0004095121, -0.0004606325, - -0.0005145572, -0.0005564576, -0.0005946118, -0.0006341594, - -0.0006650415, -0.0006917937, -0.0007215391, -0.0007319357, - -0.0007530001, -0.0007630793, -0.0007757977, -0.0007801449, - -0.0007803664, -0.0007779869, -0.0007834332, -0.0007724848, - -0.0007681371, -0.0007490598, -0.0007440941, -0.0007255043, - -0.0007157736, -0.0006941614, -0.0006777690, -0.0006540333, - -0.0006312493, -0.0006132747, -0.0005870930, -0.0005677802, - -0.0005466565, -0.0005226564, -0.0005040714, -0.0004893791, - -0.0004875227, -0.0004947518, -0.0005617692, -0.000552528, -]; - -/// §4.6.18.4.1 / Figure 4.42 — the 32-band complex analysis QMF bank. -/// -/// One instance carries the 320-sample input history `x` of one -/// channel; [`AnalysisQmf::push_slot`] consumes the next 32 time-domain -/// samples and produces the 32 complex subband samples `W[k][l]` of one -/// QMF slot. -#[derive(Debug, Clone)] -pub struct AnalysisQmf { - /// The Figure 4.42 input history; a higher index is an older sample. - x: Vec, - /// Precomputed modulation matrix - /// `2·exp(i·π/64·(k + 0.5)·(2n − 0.5))`, row-major `[k][n]`. - m: Vec, -} - -impl Default for AnalysisQmf { - fn default() -> Self { - Self::new() - } -} - -impl AnalysisQmf { - /// A fresh analysis bank with an all-zero history. - #[must_use] - pub fn new() -> Self { - let mut m = Vec::with_capacity(32 * 64); - for k in 0..32 { - for n in 0..64 { - let arg = core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 0.5); - m.push(Complex::new(2.0 * arg.cos(), 2.0 * arg.sin())); - } - } - AnalysisQmf { - x: vec![0.0; 320], - m, - } - } - - /// Run one Figure 4.42 loop: shift in 32 new time samples (oldest - /// first within `samples`) and return the 32 complex subband - /// samples `W[k]` for this slot. - pub fn push_slot(&mut self, samples: &[f64]) -> Result<[Complex; 32]> { - if samples.len() != 32 { - return Err(Error::SbrQmfInvalid); - } - // Shift the history by 32 (discarding the oldest 32) and store - // the new samples in positions 0..=31. Figure 4.42 fills - // `x[31] .. x[0]` from consecutive input samples, so the newest - // input sample lands at index 0 (a higher index is older). - self.x.copy_within(0..288, 32); - for (n, s) in samples.iter().enumerate() { - self.x[31 - n] = *s; - } - // z[n] = x[n] · c[2n]; u[n] = Σ_{j=0..=4} z[n + 64j]. - let mut u = [0.0f64; 64]; - for (n, un) in u.iter_mut().enumerate() { - let mut acc = 0.0; - for j in 0..5 { - let idx = n + j * 64; - acc += self.x[idx] * QMF_WINDOW[2 * idx]; - } - *un = acc; - } - // W[k] = Σ_n u[n] · 2·exp(i·π/64·(k + 0.5)(2n − 0.5)). - let mut w = [Complex::default(); 32]; - for (k, wk) in w.iter_mut().enumerate() { - let row = &self.m[k * 64..(k + 1) * 64]; - let mut acc = Complex::default(); - for (n, cell) in row.iter().enumerate() { - acc += *cell * u[n]; - } - *wk = acc; - } - Ok(w) - } -} - -/// §4.6.18.4.2 / Figure 4.43 — the 64-band real-output synthesis QMF -/// bank (dual-rate SBR output). -#[derive(Debug, Clone)] -pub struct SynthesisQmf { - /// The Figure 4.43 synthesis history `v`. - v: Vec, - /// Precomputed `exp(i·π/128·(k + 0.5)·(2n − 255)) / 64`, row-major - /// `[n][k]` (transposed for the inner sum over `k`). - n_mat: Vec, -} - -impl Default for SynthesisQmf { - fn default() -> Self { - Self::new() - } -} - -impl SynthesisQmf { - /// A fresh synthesis bank with an all-zero history. - #[must_use] - pub fn new() -> Self { - let mut n_mat = Vec::with_capacity(128 * 64); - for n in 0..128 { - for k in 0..64 { - let arg = - core::f64::consts::PI / 128.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 255.0); - n_mat.push(Complex::new(arg.cos() / 64.0, arg.sin() / 64.0)); - } - } - SynthesisQmf { - v: vec![0.0; 1280], - n_mat, - } - } - - /// Run one Figure 4.43 loop: consume the 64 complex subband samples - /// `X[k]` of one slot and return the 64 real output samples. - pub fn push_slot(&mut self, bands: &[Complex]) -> Result<[f64; 64]> { - if bands.len() != 64 { - return Err(Error::SbrQmfInvalid); - } - // Shift v by 128 (discard the oldest 128 samples). - self.v.copy_within(0..1152, 128); - // v[n] = Σ_k Real(X[k]/64 · exp(i·π/128·(k + 0.5)(2n − 255))). - for n in 0..128 { - let row = &self.n_mat[n * 64..(n + 1) * 64]; - let mut acc = 0.0; - for (k, cell) in row.iter().enumerate() { - let x = bands[k]; - acc += x.re * cell.re - x.im * cell.im; - } - self.v[n] = acc; - } - // Extract g from v, window by c, and sum the ten taps. - let mut out = [0.0f64; 64]; - for (k, o) in out.iter_mut().enumerate() { - let mut acc = 0.0; - for n in 0..5 { - // g[128n + k] = v[256n + k]; w = g·c. - acc += self.v[256 * n + k] * QMF_WINDOW[128 * n + k]; - // g[128n + 64 + k] = v[256n + 192 + k]. - acc += self.v[256 * n + 192 + k] * QMF_WINDOW[128 * n + 64 + k]; - } - *o = acc; - } - Ok(out) - } -} - -/// §4.6.18.4.3 / Figure 4.44 — the 32-channel downsampled synthesis QMF -/// bank (output at the core rate). -#[derive(Debug, Clone)] -pub struct DownsampledSynthesisQmf { - /// The Figure 4.44 synthesis history `v`. - v: Vec, - /// Precomputed `exp(i·π/64·(k + 0.5)·(2n − 127.5)) / 64`, row-major - /// `[n][k]`. - n_mat: Vec, -} - -impl Default for DownsampledSynthesisQmf { - fn default() -> Self { - Self::new() - } -} - -impl DownsampledSynthesisQmf { - /// A fresh downsampled synthesis bank with an all-zero history. - #[must_use] - pub fn new() -> Self { - let mut n_mat = Vec::with_capacity(64 * 32); - for n in 0..64 { - for k in 0..32 { - let arg = - core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 127.5); - n_mat.push(Complex::new(arg.cos() / 64.0, arg.sin() / 64.0)); - } - } - DownsampledSynthesisQmf { - v: vec![0.0; 640], - n_mat, - } - } - - /// Run one Figure 4.44 loop: consume the 32 complex subband samples - /// `X[k]` of one slot and return the 32 real output samples. - pub fn push_slot(&mut self, bands: &[Complex]) -> Result<[f64; 32]> { - if bands.len() != 32 { - return Err(Error::SbrQmfInvalid); - } - // Shift v by 64 (discard the oldest 64 samples). - self.v.copy_within(0..576, 64); - // v[n] = Σ_k Real(X[k]/64 · exp(i·π/64·(k + 0.5)(2n − 127.5))). - for n in 0..64 { - let row = &self.n_mat[n * 32..(n + 1) * 32]; - let mut acc = 0.0; - for (k, cell) in row.iter().enumerate() { - let x = bands[k]; - acc += x.re * cell.re - x.im * cell.im; - } - self.v[n] = acc; - } - // g extraction, every-other-coefficient windowing, ten-tap sum. - let mut out = [0.0f64; 32]; - for (k, o) in out.iter_mut().enumerate() { - let mut acc = 0.0; - for n in 0..5 { - // g[64n + k] = v[128n + k]; w[n] = g[n]·c[2n]. - acc += self.v[128 * n + k] * QMF_WINDOW[2 * (64 * n + k)]; - // g[64n + 32 + k] = v[128n + 96 + k]. - acc += self.v[128 * n + 96 + k] * QMF_WINDOW[2 * (64 * n + 32 + k)]; - } - *o = acc; - } - Ok(out) - } -} - -/// §4.6.18.8.2.2 / Figure 4.50 — the 32-band real-valued analysis QMF -/// bank of the low-power SBR tool (critically sampled). -#[derive(Debug, Clone)] -pub struct RealAnalysisQmf { - /// The Figure 4.50 input history; a higher index is an older sample. - x: Vec, - /// Precomputed modulation matrix - /// `2·cos(π/64·(k + 0.5)·(2n − 96))`, row-major `[k][n]`. - m: Vec, -} - -impl Default for RealAnalysisQmf { - fn default() -> Self { - Self::new() - } -} - -impl RealAnalysisQmf { - /// A fresh real-valued analysis bank with an all-zero history. - #[must_use] - pub fn new() -> Self { - let mut m = Vec::with_capacity(32 * 64); - for k in 0..32 { - for n in 0..64 { - let arg = core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 96.0); - m.push(2.0 * arg.cos()); - } - } - RealAnalysisQmf { - x: vec![0.0; 320], - m, - } - } - - /// Run one Figure 4.50 loop: shift in 32 new time samples (oldest - /// first within `samples`) and return the 32 real subband samples - /// `W[k]` for this slot. - pub fn push_slot(&mut self, samples: &[f64]) -> Result<[f64; 32]> { - if samples.len() != 32 { - return Err(Error::SbrQmfInvalid); - } - // As Figure 4.42: newest input sample lands at index 0. - self.x.copy_within(0..288, 32); - for (n, s) in samples.iter().enumerate() { - self.x[31 - n] = *s; - } - // z[n] = x[n] · c[2n]; u[n] = Σ_{j=0..=4} z[n + 64j]. - let mut u = [0.0f64; 64]; - for (n, un) in u.iter_mut().enumerate() { - let mut acc = 0.0; - for j in 0..5 { - let idx = n + j * 64; - acc += self.x[idx] * QMF_WINDOW[2 * idx]; - } - *un = acc; - } - // W[k] = Σ_n u[n] · 2·cos(π/64·(k + 0.5)(2n − 96)). - let mut w = [0.0f64; 32]; - for (k, wk) in w.iter_mut().enumerate() { - let row = &self.m[k * 64..(k + 1) * 64]; - let mut acc = 0.0; - for (n, cell) in row.iter().enumerate() { - acc += *cell * u[n]; - } - *wk = acc; - } - Ok(w) - } -} - -/// §4.6.18.8.2.3 / Figure 4.51 — the 64-subband real-valued synthesis -/// QMF bank (dual-rate low-power SBR output). -#[derive(Debug, Clone)] -pub struct RealSynthesisQmf { - /// The Figure 4.51 synthesis history `v`. - v: Vec, - /// Precomputed `cos(π/128·(k + 0.5)·(2n − 64)) / 32`, row-major - /// `[n][k]`. - n_mat: Vec, -} - -impl Default for RealSynthesisQmf { - fn default() -> Self { - Self::new() - } -} - -impl RealSynthesisQmf { - /// A fresh real synthesis bank with an all-zero history. - #[must_use] - pub fn new() -> Self { - let mut n_mat = Vec::with_capacity(128 * 64); - for n in 0..128 { - for k in 0..64 { - let arg = - core::f64::consts::PI / 128.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 64.0); - n_mat.push(arg.cos() / 32.0); - } - } - RealSynthesisQmf { - v: vec![0.0; 1280], - n_mat, - } - } - - /// Run one Figure 4.51 loop: consume the 64 real subband samples - /// `X[k]` of one slot and return the 64 real output samples. - pub fn push_slot(&mut self, bands: &[f64]) -> Result<[f64; 64]> { - if bands.len() != 64 { - return Err(Error::SbrQmfInvalid); - } - // Shift v by 128 (discard the oldest 128 samples). - self.v.copy_within(0..1152, 128); - // v[n] = Σ_k X[k]/32 · cos(π/128·(k + 0.5)(2n − 64)). - for n in 0..128 { - let row = &self.n_mat[n * 64..(n + 1) * 64]; - let mut acc = 0.0; - for (k, cell) in row.iter().enumerate() { - acc += bands[k] * *cell; - } - self.v[n] = acc; - } - // g extraction (as Figure 4.51), full-window multiply, ten-tap - // sum. - let mut out = [0.0f64; 64]; - for (k, o) in out.iter_mut().enumerate() { - let mut acc = 0.0; - for n in 0..5 { - // g[128n + k] = v[256n + k]; w = g·c. - acc += self.v[256 * n + k] * QMF_WINDOW[128 * n + k]; - // g[128n + 64 + k] = v[256n + 192 + k]. - acc += self.v[256 * n + 192 + k] * QMF_WINDOW[128 * n + 64 + k]; - } - *o = acc; - } - Ok(out) - } -} - -/// §4.6.18.8.2.4 / Figure 4.52 — the 32-channel downsampled real-valued -/// synthesis QMF bank (core-rate low-power SBR output). -#[derive(Debug, Clone)] -pub struct RealDownsampledSynthesisQmf { - /// The Figure 4.52 synthesis history `v`. - v: Vec, - /// Precomputed `cos(π/64·(k + 0.5)·(2n − 32)) / 32`, row-major - /// `[n][k]`. - n_mat: Vec, -} - -impl Default for RealDownsampledSynthesisQmf { - fn default() -> Self { - Self::new() - } -} - -impl RealDownsampledSynthesisQmf { - /// A fresh downsampled real synthesis bank with an all-zero history. - #[must_use] - pub fn new() -> Self { - let mut n_mat = Vec::with_capacity(64 * 32); - for n in 0..64 { - for k in 0..32 { - let arg = core::f64::consts::PI / 64.0 * (k as f64 + 0.5) * (2.0 * n as f64 - 32.0); - n_mat.push(arg.cos() / 32.0); - } - } - RealDownsampledSynthesisQmf { - v: vec![0.0; 640], - n_mat, - } - } - - /// Run one Figure 4.52 loop: consume the 32 real subband samples - /// `X[k]` of one slot and return the 32 real output samples. - pub fn push_slot(&mut self, bands: &[f64]) -> Result<[f64; 32]> { - if bands.len() != 32 { - return Err(Error::SbrQmfInvalid); - } - // Shift v by 64 (discard the oldest 64 samples). - self.v.copy_within(0..576, 64); - // v[n] = Σ_k X[k]/32 · cos(π/64·(k + 0.5)(2n − 32)). - for n in 0..64 { - let row = &self.n_mat[n * 32..(n + 1) * 32]; - let mut acc = 0.0; - for (k, cell) in row.iter().enumerate() { - acc += bands[k] * *cell; - } - self.v[n] = acc; - } - // g extraction (as Figure 4.52), every-other-coefficient - // windowing, ten-tap sum. - let mut out = [0.0f64; 32]; - for (k, o) in out.iter_mut().enumerate() { - let mut acc = 0.0; - for n in 0..5 { - // g[64n + k] = v[128n + k]; w[n] = g[n]·c[2n]. - acc += self.v[128 * n + k] * QMF_WINDOW[2 * (64 * n + k)]; - // g[64n + 32 + k] = v[128n + 96 + k]. - acc += self.v[128 * n + 96 + k] * QMF_WINDOW[2 * (64 * n + 32 + k)]; - } - *o = acc; - } - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Table 4.A.89 spot values, straight from the printed table. - #[test] - fn window_spot_values() { - assert_eq!(QMF_WINDOW[0], 0.0); - assert_eq!(QMF_WINDOW[1], -0.0005525286); - assert_eq!(QMF_WINDOW[128], 0.0132718220); - assert_eq!(QMF_WINDOW[320], 0.8537385600); - assert_eq!(QMF_WINDOW[512], -0.0132718220); - // The table prints c[639] with nine decimals. - assert_eq!(QMF_WINDOW[639], -0.000552528); - } - - /// The printed table mirrors around index 320: - /// `|c[i]| == |c[640 - i]|` for every interior index (the last - /// entry only to the table's own nine printed decimals). - #[test] - fn window_mirror_structure() { - for i in 1..320usize { - let a = QMF_WINDOW[i].abs(); - let b = QMF_WINDOW[640 - i].abs(); - assert!((a - b).abs() < 1e-9, "mirror mismatch at {i}: {a} vs {b}"); - } - } - - /// Silence in → silence out, and slot-length validation. - #[test] - fn analysis_silence_and_shape() { - let mut a = AnalysisQmf::new(); - assert!(matches!(a.push_slot(&[0.0; 16]), Err(Error::SbrQmfInvalid))); - for _ in 0..4 { - let w = a.push_slot(&[0.0; 32]).unwrap(); - assert!(w.iter().all(|c| c.re == 0.0 && c.im == 0.0)); - } - let mut s = SynthesisQmf::new(); - assert!(matches!( - s.push_slot(&[Complex::default(); 32]), - Err(Error::SbrQmfInvalid) - )); - let out = s.push_slot(&[Complex::default(); 64]).unwrap(); - assert!(out.iter().all(|&x| x == 0.0)); - } - - /// A pure low-frequency sine through analysis → 64-band synthesis - /// (upper 32 bands zero) reconstructs the 2×-upsampled sine to - /// within the filterbank's near-perfect-reconstruction bound. - #[test] - fn analysis_synthesis_upsamples_a_sine() { - let mut a = AnalysisQmf::new(); - let mut s = SynthesisQmf::new(); - let freq = 0.03; // cycles per input sample, well inside band 1 - let slots = 96; - let mut output = Vec::new(); - for slot in 0..slots { - let mut input = [0.0f64; 32]; - for (n, v) in input.iter_mut().enumerate() { - let t = (slot * 32 + n) as f64; - *v = (2.0 * core::f64::consts::PI * freq * t).sin(); - } - let w = a.push_slot(&input).unwrap(); - let mut x = [Complex::default(); 64]; - x[..32].copy_from_slice(&w); - output.extend_from_slice(&s.push_slot(&x).unwrap()); - } - // Search the analysis+synthesis delay (in output samples) by - // matching against the ideal upsampled sine, then measure the - // steady-state error. - let ideal = - |t: f64, delay: f64| (2.0 * core::f64::consts::PI * freq * (t - delay) / 2.0).sin(); - let mut best = (f64::INFINITY, 0usize); - for delay in 0..1200usize { - let mut err = 0.0; - let mut sig = 0.0; - for (t, &out) in output.iter().enumerate().skip(1400) { - let e = out - ideal(t as f64, delay as f64); - err += e * e; - sig += out * out; - } - let ratio = err / sig.max(1e-30); - if ratio < best.0 { - best = (ratio, delay); - } - } - assert!( - best.0 < 1e-4, - "reconstruction error ratio {} at delay {}", - best.0, - best.1 - ); - } - - /// The downsampled synthesis bank reconstructs the input at the - /// core rate (identity up to the filterbank delay). - #[test] - fn analysis_downsampled_synthesis_is_identity() { - let mut a = AnalysisQmf::new(); - let mut s = DownsampledSynthesisQmf::new(); - let freq = 0.04; - let slots = 96; - let mut input_all = Vec::new(); - let mut output = Vec::new(); - for slot in 0..slots { - let mut input = [0.0f64; 32]; - for (n, v) in input.iter_mut().enumerate() { - let t = (slot * 32 + n) as f64; - *v = (2.0 * core::f64::consts::PI * freq * t).sin() - + 0.5 * (2.0 * core::f64::consts::PI * 2.3 * freq * t).cos(); - } - input_all.extend_from_slice(&input); - let w = a.push_slot(&input).unwrap(); - output.extend_from_slice(&s.push_slot(&w).unwrap()); - } - let mut best = (f64::INFINITY, 0usize); - for delay in 0..640usize { - let mut err = 0.0; - let mut sig = 0.0; - for t in 800..output.len() { - if t < delay { - continue; - } - let e = output[t] - input_all[t - delay]; - err += e * e; - sig += output[t] * output[t]; - } - let ratio = err / sig.max(1e-30); - if ratio < best.0 { - best = (ratio, delay); - } - } - assert!( - best.0 < 1e-4, - "identity error ratio {} at delay {}", - best.0, - best.1 - ); - } - - /// The analysis bank is linear: analysis(a + b) == analysis(a) + - /// analysis(b) slot by slot. - #[test] - fn analysis_is_linear() { - let mut qa = AnalysisQmf::new(); - let mut qb = AnalysisQmf::new(); - let mut qs = AnalysisQmf::new(); - for slot in 0..8 { - let mut a = [0.0f64; 32]; - let mut b = [0.0f64; 32]; - let mut sum = [0.0f64; 32]; - for n in 0..32 { - let t = (slot * 32 + n) as f64; - a[n] = (0.11 * t).sin(); - b[n] = (0.031 * t + 1.0).cos(); - sum[n] = a[n] + b[n]; - } - let wa = qa.push_slot(&a).unwrap(); - let wb = qb.push_slot(&b).unwrap(); - let ws = qs.push_slot(&sum).unwrap(); - for k in 0..32 { - let d = ws[k] - (wa[k] + wb[k]); - assert!(d.norm_sqr() < 1e-18); - } - } - } - - /// The real-valued LP bank pair (§4.6.18.8.2.2 + §4.6.18.8.2.4) - /// reconstructs the input at the core rate: real-QMF aliasing - /// between adjacent subbands cancels in the matched synthesis. - #[test] - fn real_analysis_downsampled_synthesis_is_identity() { - let mut a = RealAnalysisQmf::new(); - let mut s = RealDownsampledSynthesisQmf::new(); - let freq = 0.037; - let slots = 96; - let mut input_all = Vec::new(); - let mut output = Vec::new(); - for slot in 0..slots { - let mut input = [0.0f64; 32]; - for (n, v) in input.iter_mut().enumerate() { - let t = (slot * 32 + n) as f64; - *v = (2.0 * core::f64::consts::PI * freq * t).sin() - + 0.5 * (2.0 * core::f64::consts::PI * 2.9 * freq * t).cos(); - } - input_all.extend_from_slice(&input); - let w = a.push_slot(&input).unwrap(); - output.extend_from_slice(&s.push_slot(&w).unwrap()); - } - let mut best = (f64::INFINITY, 0usize); - for delay in 0..640usize { - let mut err = 0.0; - let mut sig = 0.0; - for (t, &o) in output.iter().enumerate().skip(900) { - if t < delay { - continue; - } - let e = o - input_all[t - delay]; - err += e * e; - sig += o * o; - } - let ratio = err / sig.max(1e-30); - if ratio < best.0 { - best = (ratio, delay); - } - } - assert!( - best.0 < 1e-4, - "identity error ratio {} at delay {}", - best.0, - best.1 - ); - } - - /// Real analysis → 64-band real synthesis (top half zero) - /// reconstructs the 2×-upsampled input (§4.6.18.8.2.3). - #[test] - fn real_analysis_synthesis_upsamples_a_sine() { - let mut a = RealAnalysisQmf::new(); - let mut s = RealSynthesisQmf::new(); - let freq = 0.043; - let slots = 96; - let mut output = Vec::new(); - for slot in 0..slots { - let mut input = [0.0f64; 32]; - for (n, v) in input.iter_mut().enumerate() { - let t = (slot * 32 + n) as f64; - *v = (2.0 * core::f64::consts::PI * freq * t).sin(); - } - let w = a.push_slot(&input).unwrap(); - let mut x = [0.0f64; 64]; - x[..32].copy_from_slice(&w); - output.extend_from_slice(&s.push_slot(&x).unwrap()); - } - let ideal = - |t: f64, delay: f64| (2.0 * core::f64::consts::PI * freq * (t - delay) / 2.0).sin(); - let mut best = (f64::INFINITY, 0usize); - for delay in 0..1200usize { - let mut err = 0.0; - let mut sig = 0.0; - for (t, &out) in output.iter().enumerate().skip(1600) { - let e = out - ideal(t as f64, delay as f64); - err += e * e; - sig += out * out; - } - let ratio = err / sig.max(1e-30); - if ratio < best.0 { - best = (ratio, delay); - } - } - assert!( - best.0 < 1e-4, - "reconstruction error ratio {} at delay {}", - best.0, - best.1 - ); - } - - /// The real analysis output is the real part structure of the - /// complex bank only in aggregate — but silence and shape checks - /// hold exactly, and the bank is linear. - #[test] - fn real_banks_silence_shape_linearity() { - let mut a = RealAnalysisQmf::new(); - assert!(matches!(a.push_slot(&[0.0; 16]), Err(Error::SbrQmfInvalid))); - for _ in 0..4 { - let w = a.push_slot(&[0.0; 32]).unwrap(); - assert!(w.iter().all(|&c| c == 0.0)); - } - let mut s = RealSynthesisQmf::new(); - assert!(matches!(s.push_slot(&[0.0; 32]), Err(Error::SbrQmfInvalid))); - assert!(s.push_slot(&[0.0; 64]).unwrap().iter().all(|&x| x == 0.0)); - let mut d = RealDownsampledSynthesisQmf::new(); - assert!(matches!(d.push_slot(&[0.0; 64]), Err(Error::SbrQmfInvalid))); - assert!(d.push_slot(&[0.0; 32]).unwrap().iter().all(|&x| x == 0.0)); - - // Linearity. - let mut qa = RealAnalysisQmf::new(); - let mut qb = RealAnalysisQmf::new(); - let mut qs = RealAnalysisQmf::new(); - for slot in 0..8 { - let mut va = [0.0f64; 32]; - let mut vb = [0.0f64; 32]; - let mut sum = [0.0f64; 32]; - for n in 0..32 { - let t = (slot * 32 + n) as f64; - va[n] = (0.13 * t).sin(); - vb[n] = (0.029 * t + 0.4).cos(); - sum[n] = va[n] + vb[n]; - } - let wa = qa.push_slot(&va).unwrap(); - let wb = qb.push_slot(&vb).unwrap(); - let ws = qs.push_slot(&sum).unwrap(); - for k in 0..32 { - assert!((ws[k] - (wa[k] + wb[k])).abs() < 1e-9); - } - } - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_reconstruct.rs b/crates/vendor/oxideav-aac/src/sbr_reconstruct.rs deleted file mode 100644 index b41e8970..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_reconstruct.rs +++ /dev/null @@ -1,414 +0,0 @@ -//! SBR envelope / noise-floor DPCM reconstruction — ISO/IEC 14496-3 -//! §4.6.18.3.5. -//! -//! [`crate::sbr_envelope`] yields the **raw** transmitted values -//! `bs_data_env` / `bs_data_noise`, which are delta-coded (the spec's -//! `E_Delta(k,l)`). This module inverts the §4.6.18.3.5 delta coding to -//! recover the quantized scalefactors `E_Q(k,l)` (and the noise-floor -//! `Q(k,l)`). -//! -//! The spec defines `E_Delta` in terms of `E_Q`; inverting: -//! -//! * **frequency direction** (`bs_df_env(l) == 0`): -//! - `E_Q(0,l) = bs_data_env(0,l) / δ` -//! - `E_Q(k,l) = E_Q(k-1,l) + bs_data_env(k,l) / δ`, `k ≥ 1` -//! * **time direction** (`bs_df_env(l) == 1`): -//! - `E_Q(k,l) = g_E(k,l) + bs_data_env(k,l) / δ` -//! -//! where `δ = 0.5` for the second channel of a coupled pair (so the -//! transmitted balance values carry a factor of 2 — i.e. they must be -//! even, per §4.6.18.3.6) and `δ = 1` otherwise. In the integer -//! quantized domain the divide-by-δ is a multiply-by-`1/δ` (× 2 for the -//! coupled second channel); the transmitted values are even there, so -//! the result stays integral. -//! -//! `g_E(k,l)` is the "previous envelope, same band" reference for a -//! time delta: -//! -//! * for `l ≥ 1` it is `E_Q(k, l-1)` of the *current* frame, -//! * for `l == 0` it is `E'_Q(k, L'_E − 1)` — the last envelope of the -//! *previous* frame. -//! -//! When the frequency resolution of the reference envelope differs from -//! the current envelope (`r(l) ≠ g(l)`), the band index must be -//! re-mapped between the high- and low-resolution band tables via the -//! `i(k)` relation: -//! -//! * `r(l) = 1, g(l) = 0` (current high, ref low): for current -//! high-band `k`, the reference low-band `i` satisfies -//! `fTableLow(i) ≤ fTableHigh(k) < fTableLow(i+1)`. -//! * `r(l) = 0, g(l) = 1` (current low, ref high): for current -//! low-band `k`, the reference high-band `i` satisfies -//! `fTableHigh(i) = fTableLow(k)`. -//! -//! Noise floors follow the identical scheme over `NQ` bands, except a -//! noise floor is always at the (single) noise-band resolution, so no -//! resolution remap is ever needed. - -use crate::sbr_envelope::{SbrEnvelopeData, SbrNoiseData}; -use crate::sbr_freq_bands::HiLoTables; -use crate::sbr_grid::{SbrDtdf, SbrGrid}; -use crate::{Error, Result}; - -/// Reconstructed quantized envelope scalefactors `E_Q(k,l)` for one -/// channel: one band vector per envelope. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EnvelopeScalefactors { - /// `E_Q[l][k]` — the quantized envelope scalefactor for envelope - /// `l`, band `k`. - pub eq: Vec>, - /// Per-envelope frequency-resolution flag `r(l)` (copied from the - /// grid) — needed by the next frame for a cross-frame time delta. - pub freq_res: Vec, -} - -/// Reconstructed quantized noise-floor scalefactors `Q(k,l)` for one -/// channel: one `NQ`-band vector per noise floor. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NoiseScalefactors { - /// `Q[l][k]` — the quantized noise-floor scalefactor for noise - /// floor `l`, band `k`. - pub q: Vec>, -} - -/// The `1/δ` integer multiplier: `2` for the coupled second channel, -/// `1` otherwise. -#[inline] -fn inv_delta(coupling: bool, ch: bool) -> i32 { - if coupling && ch { - 2 - } else { - 1 - } -} - -/// `i(k)` for `r(l) = 1, g(l) = 0` (current high-res band `k` → ref -/// low-res band): the largest `i` with `fTableLow(i) ≤ fTableHigh(k)`. -fn high_to_low(bands: &HiLoTables, k: usize) -> usize { - let target = bands.f_table_high[k]; - let mut i = 0usize; - while i + 1 < bands.f_table_low.len() && bands.f_table_low[i + 1] <= target { - i += 1; - } - i -} - -/// `i(k)` for `r(l) = 0, g(l) = 1` (current low-res band `k` → ref -/// high-res band): the `i` with `fTableHigh(i) = fTableLow(k)`. -fn low_to_high(bands: &HiLoTables, k: usize) -> usize { - let target = bands.f_table_low[k]; - bands - .f_table_high - .iter() - .position(|&v| v == target) - .unwrap_or(0) -} - -/// Map a reference-envelope band array `prev` (at resolution -/// `prev_high`) onto the current envelope's band `k` (at resolution -/// `cur_high`), per the §4.6.18.3.5 `i(k)` relation. -fn ref_band(bands: &HiLoTables, prev: &[i32], cur_high: bool, prev_high: bool, k: usize) -> i32 { - let idx = if cur_high == prev_high { - k - } else if cur_high { - // r=1, g=0 - high_to_low(bands, k) - } else { - // r=0, g=1 - low_to_high(bands, k) - }; - prev.get(idx).copied().unwrap_or(0) -} - -impl EnvelopeScalefactors { - /// Reconstruct `E_Q(k,l)` from the raw `bs_data_env`. - /// - /// `prev` is the previous frame's reconstructed envelopes (its last - /// envelope is `g_E` for an `l == 0` time delta); pass `None` for - /// the first frame after a reset (in which case a time-coded first - /// envelope is treated as if the reference were all-zero, which the - /// §4.6.18.3.5 reset rule forbids on the wire anyway). - pub fn reconstruct( - env: &SbrEnvelopeData, - grid: &SbrGrid, - dtdf: &SbrDtdf, - bands: &HiLoTables, - coupling: bool, - ch: bool, - prev: Option<&EnvelopeScalefactors>, - ) -> Result { - let inv = inv_delta(coupling, ch); - let mut eq: Vec> = Vec::with_capacity(grid.num_env); - - for l in 0..grid.num_env { - let cur_high = grid.freq_res[l]; - let n = if cur_high { - bands.n_high() - } else { - bands.n_low() - }; - let raw = &env.data[l]; - if raw.len() != n { - return Err(Error::SbrGridInvalid); - } - let mut row = vec![0i32; n]; - - if !dtdf.df_env[l] { - // Frequency direction. - row[0] = raw[0] * inv; - for k in 1..n { - row[k] = row[k - 1] + raw[k] * inv; - } - } else { - // Time direction: reference is the previous envelope of - // this frame (l-1), or the last envelope of the previous - // frame for l == 0. - let (prev_row, prev_high): (Vec, bool) = if l >= 1 { - (eq[l - 1].clone(), grid.freq_res[l - 1]) - } else if let Some(p) = prev { - let last = p.eq.len().saturating_sub(1); - ( - p.eq.get(last).cloned().unwrap_or_default(), - *p.freq_res.get(last).unwrap_or(&cur_high), - ) - } else { - (vec![0i32; n], cur_high) - }; - for k in 0..n { - let g = ref_band(bands, &prev_row, cur_high, prev_high, k); - row[k] = g + raw[k] * inv; - } - } - eq.push(row); - } - - Ok(EnvelopeScalefactors { - eq, - freq_res: grid.freq_res.clone(), - }) - } -} - -impl NoiseScalefactors { - /// Reconstruct `Q(k,l)` from the raw `bs_data_noise` over `NQ` - /// bands. Noise floors share one resolution, so there is no - /// `i(k)` remap. - pub fn reconstruct( - noise: &SbrNoiseData, - grid: &SbrGrid, - dtdf: &SbrDtdf, - num_noise_bands: usize, - coupling: bool, - ch: bool, - prev: Option<&NoiseScalefactors>, - ) -> Result { - let inv = inv_delta(coupling, ch); - let mut q: Vec> = Vec::with_capacity(grid.num_noise); - - for l in 0..grid.num_noise { - let raw = &noise.data[l]; - if raw.len() != num_noise_bands { - return Err(Error::SbrGridInvalid); - } - let mut row = vec![0i32; num_noise_bands]; - if !dtdf.df_noise[l] { - row[0] = raw[0] * inv; - for k in 1..num_noise_bands { - row[k] = row[k - 1] + raw[k] * inv; - } - } else { - let prev_row: Vec = if l >= 1 { - q[l - 1].clone() - } else if let Some(p) = prev { - p.q.last() - .cloned() - .unwrap_or_else(|| vec![0i32; num_noise_bands]) - } else { - vec![0i32; num_noise_bands] - }; - for k in 0..num_noise_bands { - let g = prev_row.get(k).copied().unwrap_or(0); - row[k] = g + raw[k] * inv; - } - } - q.push(row); - } - - Ok(NoiseScalefactors { q }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::sbr_freq_bands::{k0, k2, master_table, HiLoTables}; - use crate::sbr_grid::FrameClass; - - fn bands_44100() -> HiLoTables { - let k0v = k0(88_200, 5).unwrap(); - let k2v = k2(88_200, 5, k0v).unwrap(); - let fm = master_table(k0v, k2v, 0, false).unwrap(); - HiLoTables::derive(&fm, 1, 2).unwrap() - } - - fn single_env_grid(high: bool) -> (SbrGrid, SbrDtdf) { - ( - SbrGrid { - frame_class: FrameClass::FixFix, - num_env: 1, - num_noise: 1, - freq_res: vec![high], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: false, - }, - SbrDtdf { - df_env: vec![false], - df_noise: vec![false], - }, - ) - } - - #[test] - fn freq_direction_accumulates() { - let bands = bands_44100(); - let (grid, dtdf) = single_env_grid(true); - let n = bands.n_high(); - // raw = [10, 1, 2, -1, ...] → cumulative sums. - let mut raw = vec![10i32]; - for k in 1..n { - raw.push(if k % 2 == 0 { 2 } else { -1 }); - } - let env = SbrEnvelopeData { - data: vec![raw.clone()], - }; - let rec = EnvelopeScalefactors::reconstruct(&env, &grid, &dtdf, &bands, false, false, None) - .unwrap(); - // Expected: cumulative sum. - let mut acc = 10; - assert_eq!(rec.eq[0][0], 10); - for (k, &delta) in raw.iter().enumerate().skip(1) { - acc += delta; - assert_eq!(rec.eq[0][k], acc); - } - } - - #[test] - fn coupled_second_channel_doubles_delta() { - let bands = bands_44100(); - let (grid, dtdf) = single_env_grid(true); - let n = bands.n_high(); - let mut raw = vec![4i32]; - raw.extend(std::iter::repeat_n(2, n - 1)); - let env = SbrEnvelopeData { data: vec![raw] }; - // coupling && ch → inv_delta = 2. - let rec = EnvelopeScalefactors::reconstruct(&env, &grid, &dtdf, &bands, true, true, None) - .unwrap(); - assert_eq!(rec.eq[0][0], 8); // 4 * 2 - assert_eq!(rec.eq[0][1], 12); // 8 + 2*2 - } - - #[test] - fn time_direction_uses_prev_envelope_in_frame() { - let bands = bands_44100(); - let n = bands.n_high(); - // Two high-res envelopes: env0 freq-coded, env1 time-coded. - let grid = SbrGrid { - frame_class: FrameClass::FixVar, - num_env: 2, - num_noise: 2, - freq_res: vec![true, true], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: false, - }; - let dtdf = SbrDtdf { - df_env: vec![false, true], // env1 is time-coded - df_noise: vec![false, false], - }; - let mut raw0 = vec![20i32]; // env0 start = 20, flat thereafter - raw0.extend(std::iter::repeat_n(0, n - 1)); - let raw1 = vec![1i32; n]; // env1 = env0 + 1 per band - let env = SbrEnvelopeData { - data: vec![raw0, raw1], - }; - let rec = EnvelopeScalefactors::reconstruct(&env, &grid, &dtdf, &bands, false, false, None) - .unwrap(); - for k in 0..n { - assert_eq!(rec.eq[0][k], 20); - assert_eq!(rec.eq[1][k], 21); // 20 + 1 - } - } - - #[test] - fn time_direction_cross_frame() { - let bands = bands_44100(); - let n = bands.n_high(); - let (grid, _) = single_env_grid(true); - // Previous frame: a single high-res envelope all = 30. - let prev = EnvelopeScalefactors { - eq: vec![vec![30i32; n]], - freq_res: vec![true], - }; - // Current frame: single time-coded envelope, deltas all +2. - let dtdf = SbrDtdf { - df_env: vec![true], - df_noise: vec![false], - }; - let env = SbrEnvelopeData { - data: vec![vec![2i32; n]], - }; - let rec = EnvelopeScalefactors::reconstruct( - &env, - &grid, - &dtdf, - &bands, - false, - false, - Some(&prev), - ) - .unwrap(); - for k in 0..n { - assert_eq!(rec.eq[0][k], 32); // 30 + 2 - } - } - - #[test] - fn resolution_remap_high_to_low_is_monotone() { - // high_to_low must be non-decreasing and in-range for every - // high-res band. - let bands = bands_44100(); - let mut prev = 0usize; - for k in 0..=bands.n_high() { - let i = high_to_low(&bands, k); - assert!(i < bands.f_table_low.len()); - assert!(i >= prev); - prev = i; - } - } - - #[test] - fn noise_reconstruct_accumulates() { - let bands = bands_44100(); - let nq = bands.n_q(); - let (grid, dtdf) = single_env_grid(true); - let raw = (0..nq) - .map(|k| if k == 0 { 5 } else { 1 }) - .collect::>(); - let noise = SbrNoiseData { data: vec![raw] }; - let rec = - NoiseScalefactors::reconstruct(&noise, &grid, &dtdf, nq, false, false, None).unwrap(); - let mut acc = 5; - assert_eq!(rec.q[0][0], 5); - for k in 1..nq { - acc += 1; - assert_eq!(rec.q[0][k], acc); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/sbr_time_grid.rs b/crates/vendor/oxideav-aac/src/sbr_time_grid.rs deleted file mode 100644 index de3140fd..00000000 --- a/crates/vendor/oxideav-aac/src/sbr_time_grid.rs +++ /dev/null @@ -1,340 +0,0 @@ -//! SBR time / frequency grid derivation — ISO/IEC 14496-3 §4.6.18.3.3. -//! -//! Turns a parsed [`crate::sbr_grid::SbrGrid`] into the envelope and -//! noise-floor time border vectors `tE(l)` / `tQ(l)` (in SBR time -//! slots) plus the `lA` "transient envelope" index of Table 4.176: -//! -//! * `absBordLead` / `absBordTrail` — the leading / trailing SBR frame -//! borders per frame class (`bs_var_bord_*` offsets for the variable -//! sides). -//! * `nRelLead` / `nRelTrail` and the relative-border vectors — -//! `NINT(numTimeSlots / LE)` uniform spacing for FIXFIX, the -//! reconstructed `2·bs_rel_bord + 2` values for the variable sides. -//! * `tQ` — one or two noise floors, the two-floor split at -//! `tE(middleBorder)` with `middleBorder` from Table 4.174. -//! * `lA` — Table 4.176 (`-1` when no transient envelope is -//! signalled), consumed by the §4.6.18.7.5 gain calculation. -//! -//! ## Provenance -//! -//! Every branch below is from the §4.6.18.3.3 text and Tables 4.174 / -//! 4.176 of the staged spec. No part of this implementation is derived -//! from any external decoder. - -use crate::sbr_grid::{FrameClass, SbrGrid}; -use crate::{Error, Result}; - -/// The derived §4.6.18.3.3 time grid for one channel's SBR frame. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TimeGrid { - /// `tE(0..=LE)` — envelope time borders in SBR time slots. The - /// start border of segment `l` is inclusive, the stop border - /// exclusive. - pub t_e: Vec, - /// `tQ(0..=LQ)` — noise-floor time borders (a subset of `t_e`). - pub t_q: Vec, - /// `lA` per Table 4.176: the envelope index where a newly started - /// sinusoid begins (and where the §4.6.18.7.5 `δ(l)` noise gate - /// opens); `-1` when none is signalled. - pub l_a: i32, -} - -/// Derive the §4.6.18.3.3 time grid from a parsed `sbr_grid()`. -/// -/// `num_time_slots` is the §4.6.18.2.6 `numTimeSlots` (16 for the -/// 1024-sample core frame this crate decodes). Border vectors that are -/// not strictly increasing, or that leave the -/// `[0, num_time_slots + 8]` range, are rejected with -/// [`Error::SbrGridInvalid`] (a malformed variable-border grid). -pub fn derive_time_grid(grid: &SbrGrid, num_time_slots: i32) -> Result { - let le = grid.num_env; - if le == 0 { - return Err(Error::SbrGridInvalid); - } - - // Leading / trailing absolute borders. - let abs_bord_lead = match grid.frame_class { - FrameClass::FixFix | FrameClass::FixVar => 0, - FrameClass::VarFix | FrameClass::VarVar => i32::from(grid.var_bord_0), - }; - let abs_bord_trail = match grid.frame_class { - FrameClass::FixFix | FrameClass::VarFix => num_time_slots, - FrameClass::FixVar | FrameClass::VarVar => i32::from(grid.var_bord_1) + num_time_slots, - }; - - // Relative-border counts. - let n_rel_lead = match grid.frame_class { - FrameClass::FixFix => le - 1, - FrameClass::FixVar => 0, - FrameClass::VarFix | FrameClass::VarVar => grid.rel_bord_0.len(), - }; - let n_rel_trail = match grid.frame_class { - FrameClass::FixFix | FrameClass::VarFix => 0, - FrameClass::FixVar | FrameClass::VarVar => grid.rel_bord_1.len(), - }; - if n_rel_lead + n_rel_trail + 1 != le { - return Err(Error::SbrGridInvalid); - } - - // relBordLead(l): FIXFIX splits the frame uniformly with - // NINT(numTimeSlots / LE); the variable classes carry - // 2·bs_rel_bord_0 + 2. - let rel_lead = |l: usize| -> i32 { - match grid.frame_class { - FrameClass::FixFix => nint_ratio(num_time_slots, le as i32), - _ => 2 * i32::from(grid.rel_bord_0[l]) + 2, - } - }; - // relBordTrail(l): 2·bs_rel_bord_1 + 2. - let rel_trail = |l: usize| -> i32 { 2 * i32::from(grid.rel_bord_1[l]) + 2 }; - - // tE(l). - let mut t_e = Vec::with_capacity(le + 1); - for l in 0..=le { - let border = if l == 0 { - abs_bord_lead - } else if l == le { - abs_bord_trail - } else if l <= n_rel_lead { - let mut b = abs_bord_lead; - for i in 0..l { - b += rel_lead(i); - } - b - } else { - let mut b = abs_bord_trail; - for i in 0..(le - l) { - b -= rel_trail(i); - } - b - }; - t_e.push(border); - } - - // §4.6.18.3.3 border sanity: strictly increasing, within the - // addressable slot range (the XLow / XHigh buffers extend - // tHFGen = 8 slots past the frame). - for w in t_e.windows(2) { - if w[1] <= w[0] { - return Err(Error::SbrGridInvalid); - } - } - if t_e[0] < 0 || t_e[le] > num_time_slots + 8 { - return Err(Error::SbrGridInvalid); - } - - // tQ: one floor spans the frame; two floors split at - // tE(middleBorder) (Table 4.174). - let t_q = if le == 1 { - vec![t_e[0], t_e[1]] - } else { - let middle = middle_border(grid.frame_class, grid.pointer, le)?; - if middle == 0 || middle >= le { - return Err(Error::SbrGridInvalid); - } - vec![t_e[0], t_e[middle], t_e[le]] - }; - if grid.num_noise != t_q.len() - 1 { - return Err(Error::SbrGridInvalid); - } - - // lA (Table 4.176). - let l_a = match grid.frame_class { - FrameClass::FixFix => -1, - FrameClass::FixVar | FrameClass::VarVar => { - if grid.pointer == 0 { - -1 - } else { - le as i32 + 1 - grid.pointer as i32 - } - } - FrameClass::VarFix => { - if grid.pointer > 1 { - grid.pointer as i32 - 1 - } else { - -1 - } - } - }; - - Ok(TimeGrid { t_e, t_q, l_a }) -} - -/// Table 4.174 — the `middleBorder` envelope index that splits the two -/// noise floors. -fn middle_border(class: FrameClass, pointer: u32, le: usize) -> Result { - let le_i = le as i32; - let v = match class { - FrameClass::FixFix => le_i / 2, - FrameClass::VarFix => match pointer { - 0 => 1, - 1 => le_i - 1, - _ => pointer as i32 - 1, - }, - FrameClass::FixVar | FrameClass::VarVar => match pointer { - 0 | 1 => le_i - 1, - _ => le_i + 1 - pointer as i32, - }, - }; - if v < 0 { - return Err(Error::SbrGridInvalid); - } - Ok(v as usize) -} - -/// §1.3 `NINT()` of the ratio `num / den` (round half away from zero; -/// both operands are positive here). -#[inline] -fn nint_ratio(num: i32, den: i32) -> i32 { - (2 * num + den) / (2 * den) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn fixfix(num_env: usize) -> SbrGrid { - SbrGrid { - frame_class: FrameClass::FixFix, - num_env, - num_noise: if num_env > 1 { 2 } else { 1 }, - freq_res: vec![true; num_env], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![], - pointer: 0, - amp_res_override: num_env == 1, - } - } - - /// FIXFIX splits the frame uniformly: LE ∈ {1, 2, 4} over 16 slots. - #[test] - fn fixfix_uniform_borders() { - assert_eq!(derive_time_grid(&fixfix(1), 16).unwrap().t_e, vec![0, 16]); - assert_eq!( - derive_time_grid(&fixfix(2), 16).unwrap().t_e, - vec![0, 8, 16] - ); - assert_eq!( - derive_time_grid(&fixfix(4), 16).unwrap().t_e, - vec![0, 4, 8, 12, 16] - ); - } - - /// FIXFIX noise floors: LE = 1 has one floor over the frame; LE > 1 - /// splits at tE(LE/2); lA is always -1. - #[test] - fn fixfix_noise_floors_and_la() { - let g1 = derive_time_grid(&fixfix(1), 16).unwrap(); - assert_eq!(g1.t_q, vec![0, 16]); - assert_eq!(g1.l_a, -1); - let g4 = derive_time_grid(&fixfix(4), 16).unwrap(); - assert_eq!(g4.t_q, vec![0, 8, 16]); - assert_eq!(g4.l_a, -1); - } - - /// FIXVAR counts envelopes back from the variable trailing border. - #[test] - fn fixvar_borders_from_trail() { - let grid = SbrGrid { - frame_class: FrameClass::FixVar, - num_env: 2, - num_noise: 2, - freq_res: vec![true; 2], - var_bord_0: 0, - var_bord_1: 3, - rel_bord_0: vec![], - rel_bord_1: vec![1], // reconstructed 2·1 + 2 = 4 - pointer: 0, - amp_res_override: false, - }; - let g = derive_time_grid(&grid, 16).unwrap(); - // absBordTrail = 3 + 16 = 19; tE(1) = 19 - 4 = 15. - assert_eq!(g.t_e, vec![0, 15, 19]); - // middleBorder (pointer = 0) = LE - 1 = 1. - assert_eq!(g.t_q, vec![0, 15, 19]); - assert_eq!(g.l_a, -1); - // pointer = 1 → lA = LE + 1 - 1 = 2. - let g = derive_time_grid(&SbrGrid { pointer: 1, ..grid }, 16).unwrap(); - assert_eq!(g.l_a, 2); - } - - /// VARFIX counts envelopes forward from the variable leading - /// border; lA fires only for pointer > 1. - #[test] - fn varfix_borders_from_lead() { - let grid = SbrGrid { - frame_class: FrameClass::VarFix, - num_env: 2, - num_noise: 2, - freq_res: vec![false; 2], - var_bord_0: 2, - var_bord_1: 0, - rel_bord_0: vec![0], // reconstructed 2 - rel_bord_1: vec![], - pointer: 2, - amp_res_override: false, - }; - let g = derive_time_grid(&grid, 16).unwrap(); - assert_eq!(g.t_e, vec![2, 4, 16]); - // middleBorder (pointer = 2) = pointer - 1 = 1. - assert_eq!(g.t_q, vec![2, 4, 16]); - // lA = pointer - 1 = 1. - assert_eq!(g.l_a, 1); - let g = derive_time_grid(&SbrGrid { pointer: 1, ..grid }, 16).unwrap(); - assert_eq!(g.l_a, -1); - } - - /// VARVAR mixes both variable sides. - #[test] - fn varvar_mixed_borders() { - let grid = SbrGrid { - frame_class: FrameClass::VarVar, - num_env: 3, - num_noise: 2, - freq_res: vec![true; 3], - var_bord_0: 1, - var_bord_1: 2, - rel_bord_0: vec![2], // 6 - rel_bord_1: vec![3], // 8 - pointer: 0, - amp_res_override: false, - }; - let g = derive_time_grid(&grid, 16).unwrap(); - // lead: 1, 1+6 = 7; trail: 18, 18-8 = 10. - assert_eq!(g.t_e, vec![1, 7, 10, 18]); - // middleBorder (pointer = 0) = LE - 1 = 2 → tQ splits at 10. - assert_eq!(g.t_q, vec![1, 10, 18]); - } - - /// Non-monotonic borders are rejected. - #[test] - fn non_monotonic_borders_rejected() { - let grid = SbrGrid { - frame_class: FrameClass::FixVar, - num_env: 2, - num_noise: 2, - freq_res: vec![true; 2], - var_bord_0: 0, - var_bord_1: 0, - rel_bord_0: vec![], - rel_bord_1: vec![3], // tE(1) = 16 - 8 = 8 … fine - pointer: 0, - amp_res_override: false, - }; - assert!(derive_time_grid(&grid, 16).is_ok()); - let bad = SbrGrid { - var_bord_1: 0, - rel_bord_1: vec![3, 3, 3], - num_env: 4, - freq_res: vec![true; 4], - ..grid - }; - // tE = [0, 16-24, …] — not increasing. - assert!(matches!( - derive_time_grid(&bad, 16), - Err(Error::SbrGridInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/scalable.rs b/crates/vendor/oxideav-aac/src/scalable.rs deleted file mode 100644 index 667fb6cb..00000000 --- a/crates/vendor/oxideav-aac/src/scalable.rs +++ /dev/null @@ -1,1541 +0,0 @@ -//! Scalable AAC — ISO/IEC 14496-3 §4.4.2.2 (Tables 4.13–4.18) syntax -//! and the §4.5.2.2 / §4.6.14.2 AAC-only layer-combination decode for -//! the AAC scalable (AOT 6) and ER AAC scalable (AOT 20) object types. -//! -//! ## Payload shape -//! -//! A scalable program is one `aac_scalable_main_element()` (ASME, -//! Table 4.13 — layer 0) plus up to seven -//! `aac_scalable_extension_element()`s (ASEE, Table 4.14 — layers -//! 1..8), each riding its own elementary stream / LATM layer. Every -//! element is a header followed by one `individual_channel_stream(1,1)` -//! per channel (the Table 4.50 `scale_flag == 1` form: no inline -//! `ics_info()`, no pulse / TNS / gain-control dispatch — see -//! [`IcsBody::parse_scale`]), a trailing `extension_payload()` loop and -//! `byte_alignment()`. -//! -//! * `aac_scalable_main_header()` (Table 4.15, the AAC-only branch — -//! `core_flag == 0`, `tvq_layer_present == 0`): `ics_reserved_bit`, -//! `window_sequence`, `window_shape`, `max_sfb` (+ -//! `scale_factor_grouping` on `EIGHT_SHORT_SEQUENCE`), the stereo -//! `ms_mask_present` / `ms_data()`, then per channel -//! `tns_data_present` / `tns_data()` and `ltp_data_present` / -//! `ltp_data()`. -//! * `aac_scalable_extension_header()` (Table 4.16): `max_sfb`, the -//! stereo `ms_mask_present` / `ms_data()` (Table 4.60 — transmitted -//! for the **additional** bands `last_max_sfb_ms..max_sfb` only, -//! §4.6.8.1.4), per-channel `tns_data_present` / `tns_data()` on the -//! *first stereo layer after mono layers* only (`mono_stereo_flag`, -//! §4.6.9.5), and per-channel `diff_control_data_lr()` (Table 4.18) -//! on every stereo layer of a mixed mono/stereo configuration. -//! -//! ## Layer combination (§4.5.2.2.4, Figure 4.4) -//! -//! The Scalable Inverse AAC Quantization module (SIAQ) adds the -//! dequantized spectra of all layers per output path; the per-band -//! tool interactions follow Tables 4.91–4.93: -//! -//! * mono→mono / stereo→stereo plain bands: **sum**; -//! * PNS bands: a lower layer's noise band survives only while every -//! higher layer decodes the band to all-zero (§4.6.13.6); a higher -//! layer's PNS **replaces** a lower PNS band; PNS on top of real -//! coefficients (and vice versa within a channel pair) is invalid; -//! * intensity bands: only the left/mid channel accumulates across -//! IS→IS layers, positions come from the highest layer; IS over a -//! plain stereo band (or plain over IS) replaces the band with the -//! highest layer's content per Table 4.92; -//! * at the mono→stereo transition the combined mono spectrum `M''` -//! enters M/S-coded bands as `M = M'' + M'` and L/R-coded bands via -//! the §4.6.14.2 FSS: `L/R += 2·M''` where the per-channel -//! `diff_control_lr` bit is `0` (untransmitted bands default to -//! `1`); a mono PNS band never crosses the transition (Table 4.93). -//! -//! M/S (§4.6.8.1.4: one cumulative mask across layers), intensity -//! (§4.6.8.2.3 — `invert_intensity() = +1` for the scalable AOT) and -//! PNS (§4.6.13.6 — `ms_used` still signals noise correlation) are -//! then applied on the combined spectra, followed by the §4.6.9.5 -//! serial TNS layout (Table 4.158: the first mono layer's filter data -//! serves the `M` region up to the highest mono `max_sfb`, the first -//! stereo layer's filters serve L / R; an L/R filter reaching below -//! the mono boundary overrides the M filter) and the §4.6.11 -//! filterbank. -//! -//! §4.6.7.5 LTP: prediction runs only on the lowest GA layer, its -//! reconstruction history is the time-domain output of the first -//! layer decoded **alone** — the driver keeps a parallel base-layer -//! synthesis chain for exactly that; intensity / PNS bands of the -//! base layer take precedence over prediction (§4.6.7.5 / §4.6.7.4.2). -//! -//! CELP-core (`dependsOnCoreCoder == 1`) and TwinVQ lower layers are -//! other subparts' codecs and are rejected -//! ([`Error::ScalableUnsupportedCore`]). - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::asc::AacResilienceFlags; -use crate::decoded_spectrum::quant_to_spec; -use crate::dequant::rescale_spectrum; -use crate::extension_payload::ExtensionPayload; -use crate::filterbank::Filterbank; -use crate::ics_body::IcsBody; -use crate::ics_info::{ - derive_window_grouping_family, parse_ltp_data, write_ltp_data, IcsInfo, LtpData, - WindowSequence, WindowShape, -}; -use crate::intensity_stereo::{apply_intensity_stereo, IntensityPairSpectra}; -use crate::ltp::LtpState; -use crate::ms_stereo::{apply_ms_stereo, ChannelPairSpectra, MsMaskPresent}; -use crate::pns::{apply_pns, apply_pns_pair, gen_rand_vector, PnsChannel}; -use crate::scale_factor_data::{accumulate, AbsoluteScaleFactors}; -use crate::section_data::{INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB}; -use crate::spectral_data::SpectralData; -use crate::swb_offset::FrameFamily; -use crate::tns_data::TnsData; -use crate::tns_frame::{tns_analysis_frame_ics, tns_decode_frame_ics}; -use crate::{Error, Result}; - -/// Maximum number of coding layers (§4.5.2.2.4: one AAC main layer -/// plus up to 7 AAC extension layers). -pub const MAX_LAYERS: usize = 8; - -/// Static configuration of a scalable program, resolved from the -/// per-layer `AudioSpecificConfig`s. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ScalableConfig { - /// `audioObjectType` — 6 (AAC scalable) or 20 (ER AAC scalable). - pub aot: u8, - /// Table 1.18 `samplingFrequencyIndex` (all layers share it in - /// the AAC-only combinations — §4.5.2.2.4 runs one filterbank). - pub fs_index: u8, - /// Resolved sampling rate in Hz. - pub sample_rate: u32, - /// §4.5.1.1 frame-length family — `Lc1024` or `Lc960` - /// (`frameLengthFlag`); the LD families are not scalable shapes. - pub family: FrameFamily, - /// The ASC resilience triplet for AOT 20; all-false for AOT 6. - pub resilience: AacResilienceFlags, - /// `this_layer_stereo` per layer, in layer order (§4.5.2.2.1.1). - /// Derived from each layer's `channelConfiguration` (1 or 2). - pub layer_stereo: Vec, -} - -impl ScalableConfig { - /// Validate the §4.5.2.2 shape: 1..=8 layers, no mono layer after - /// a stereo layer (Table 4.87), a non-LD family, a scalable AOT. - pub fn validate(&self) -> Result<()> { - if self.aot != 6 && self.aot != 20 { - return Err(Error::ScalableInvalid); - } - if self.layer_stereo.is_empty() || self.layer_stereo.len() > MAX_LAYERS { - return Err(Error::ScalableInvalid); - } - if self.family.is_ld() { - return Err(Error::ScalableInvalid); - } - // Table 4.87: AAC mono may feed mono or stereo; AAC stereo - // feeds stereo only. - let mut seen_stereo = false; - for &s in &self.layer_stereo { - if seen_stereo && !s { - return Err(Error::ScalableInvalid); - } - seen_stereo |= s; - } - Ok(()) - } - - /// `mono_layer_flag` (§4.5.2.2.1.1): any mono layer present. - pub fn mono_layer_flag(&self) -> bool { - self.layer_stereo.iter().any(|&s| !s) - } - - /// Index of the first stereo layer, if any. - pub fn first_stereo_layer(&self) -> Option { - self.layer_stereo.iter().position(|&s| s) - } - - /// `mono_stereo_flag` for layer `lay` (§4.5.2.2.1.1): at least one - /// mono layer exists and `lay` is the first stereo layer. - pub fn mono_stereo_flag(&self, lay: usize) -> bool { - self.mono_layer_flag() && self.first_stereo_layer() == Some(lay) - } - - /// Build a [`ScalableConfig`] from the per-layer - /// `AudioSpecificConfig`s of a LATM program (§1.7.3: one layer per - /// `streamID[prog][lay]`, in layer order). - /// - /// Shape rules enforced here: every layer carries the same - /// scalable AOT (6 / 20), the same `samplingFrequencyIndex` and - /// the same `frameLengthFlag`; each `channelConfiguration` is 1 - /// (mono) or 2 (stereo); a `dependsOnCoreCoder == 1` layer (CELP - /// core, §4.5.2.2.5) is rejected with - /// [`Error::ScalableUnsupportedCore`]; the AOT-20 resilience - /// triplet comes from the first layer and must match on every - /// layer. - pub fn from_layer_ascs(ascs: &[&crate::asc::AudioSpecificConfig]) -> Result { - let first = ascs.first().ok_or(Error::ScalableInvalid)?; - if first.aot != 6 && first.aot != 20 { - return Err(Error::ScalableInvalid); - } - let family = FrameFamily::from_aot_and_flag( - first.aot, - first.ga_body.frame_length == crate::asc::FrameLength::Long960, - ); - let resilience = |asc: &crate::asc::AudioSpecificConfig| { - asc.ga_body - .extension_body - .as_ref() - .and_then(|ext| ext.resilience) - .unwrap_or_default() - }; - let res0 = resilience(first); - let mut layer_stereo = Vec::with_capacity(ascs.len()); - for asc in ascs { - if asc.aot != first.aot - || asc.sampling_frequency_index != first.sampling_frequency_index - || asc.ga_body.frame_length != first.ga_body.frame_length - || resilience(asc) != res0 - { - return Err(Error::ScalableInvalid); - } - if asc.ga_body.depends_on_core_coder { - return Err(Error::ScalableUnsupportedCore); - } - layer_stereo.push(match asc.channel_configuration { - 1 => false, - 2 => true, - _ => return Err(Error::ScalableInvalid), - }); - } - let cfg = ScalableConfig { - aot: first.aot, - fs_index: first.sampling_frequency_index, - sample_rate: first.sample_rate, - family, - resilience: res0, - layer_stereo, - }; - cfg.validate()?; - Ok(cfg) - } - - /// Number of output channels (2 iff any layer is stereo). - pub fn output_channels(&self) -> usize { - if self.layer_stereo.iter().any(|&s| s) { - 2 - } else { - 1 - } - } - - fn channels_of_layer(&self, lay: usize) -> usize { - if self.layer_stereo[lay] { - 2 - } else { - 1 - } - } -} - -/// One channel of one layer: the `individual_channel_stream(1,1)` -/// body plus its decoded spectrum. -#[derive(Debug, Clone)] -pub struct ScalableChannel { - /// The Table 4.50 `scale_flag == 1` body - /// ([`IcsBody::parse_scale`]). - pub body: IcsBody, - /// The channel's quantized spectrum — from `spectral_data()` or, - /// for AOT 20 with `aacSpectralDataResilienceFlag`, from the - /// §4.6.16.3 `reordered_spectral_data()` payload. - pub spectral: SpectralData, -} - -/// One parsed layer of a scalable frame (main or extension element). -#[derive(Debug, Clone)] -pub struct ScalableLayer { - /// Per-layer geometry: the main header's `window_sequence` / - /// `window_shape` / grouping with **this layer's** `max_sfb`. - pub ics: IcsInfo, - /// `ms_mask_present` for a stereo layer ([`MsMaskPresent::AllZeros`] - /// for mono layers, whose headers carry no mask). - pub ms_mask_present: MsMaskPresent, - /// The layer's newly transmitted `ms_used` rows (Table 4.60): - /// `num_window_groups` rows covering `last_max_sfb_ms..max_sfb`. - /// Empty unless `ms_mask_present == 1`. - pub ms_used_new: Vec>, - /// Per-channel `tns_data()`; populated only on layers whose header - /// carries TNS bits (the main layer; the `mono_stereo_flag` - /// extension layer). - pub tns: Vec>, - /// Per-channel `ltp_data()` (main layer only, §4.6.7.5). - pub ltp: Vec>, - /// Per-channel long-window `diff_control_lr` bits in transmission - /// order (Table 4.18: bands `last_max_sfb_ms..min(last_mono_max_sfb, - /// max_sfb)` whose cumulative `ms_used` is clear). Empty when the - /// header carries none. - pub diff_lr_long: Vec>, - /// Per-channel short-window `diff_control_lr[win][0]` bits (first - /// stereo layer only). `None` when absent. - pub diff_lr_short: Vec>, - /// The per-channel ICS bodies + spectra. - pub channels: Vec, -} - -/// A fully parsed scalable frame: every layer element plus the -/// cumulative cross-layer tables. -#[derive(Debug, Clone)] -pub struct ScalableFrame { - /// The per-layer parsed elements, in layer order. - pub layers: Vec, - /// Cumulative `ms_used[g][sfb]` over `max_total_sfb` bands - /// (§4.6.8.1.4 — one mask across all layers, each layer - /// transmitting only its additional bands). - pub ms_used: Vec>, - /// Cumulative per-channel long-window `diff_control_lr[sfb]` - /// (§4.6.14.2.1; `None` = untransmitted = `1`). - pub diff_lr_long: [Vec>; 2], - /// Per-channel short-window `diff_control_lr[win][0]` (first - /// stereo layer; `None` when the frame is long-window or has no - /// stereo transition). - pub diff_lr_short: [Option<[bool; 8]>; 2], - /// Highest `max_sfb` across all layers. - pub max_total_sfb: u8, - /// Highest `max_sfb` across the mono layers (`0` when none). - pub max_mono_sfb: u8, -} - -impl ScalableFrame { - /// Parse one frame's per-layer payloads (one byte buffer per - /// layer, layer 0 first) into the element structures. - pub fn parse(cfg: &ScalableConfig, payloads: &[&[u8]]) -> Result { - cfg.validate()?; - if payloads.len() != cfg.layer_stereo.len() { - return Err(Error::ScalableInvalid); - } - - let mut layers: Vec = Vec::with_capacity(payloads.len()); - // Base geometry from the main header (window sequence / shape / - // grouping are frame-global; only max_sfb varies per layer). - let mut base_ics: Option = None; - let mut ms_used: Vec> = Vec::new(); - let mut diff_lr_long: [Vec>; 2] = [Vec::new(), Vec::new()]; - let mut diff_lr_short: [Option<[bool; 8]>; 2] = [None, None]; - let mut last_max_sfb_ms: u8 = 0; // previous *stereo* layer's max_sfb - let mut max_mono_sfb: u8 = 0; - let mut max_total_sfb: u8 = 0; - - for (lay, payload) in payloads.iter().enumerate() { - let stereo = cfg.layer_stereo[lay]; - let n_ch = cfg.channels_of_layer(lay); - let mut reader = BitReader::new(payload); - - let (ics, ms_mask_present, ms_used_new, tns, ltp, dl_long, dl_short); - if lay == 0 { - // ---- Table 4.15 aac_scalable_main_header() (AAC-only). - let ics_reserved_bit = read_bit(&mut reader)?; - let ws = WindowSequence::from_bits(read_u8(&mut reader, 2)?); - let shape = WindowShape::from_bit(read_bit(&mut reader)?); - let (msfb, sfg) = if ws.is_eight_short() { - let m = read_u8(&mut reader, 4)?; - let g = read_u8(&mut reader, 7)?; - (m, Some(g)) - } else { - (read_u8(&mut reader, 6)?, None) - }; - let (num_windows, num_window_groups, window_group_length, num_swb) = - derive_window_grouping_family(cfg.family, ws, sfg, cfg.fs_index)?; - let info = IcsInfo { - family: cfg.family, - ics_reserved_bit, - window_sequence: ws, - window_shape: shape, - max_sfb: msfb, - scale_factor_grouping: sfg, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows, - num_window_groups, - window_group_length, - num_swb, - }; - if msfb > num_swb { - return Err(Error::ScalableInvalid); - } - let groups = usize::from(num_window_groups); - ms_used = vec![Vec::new(); groups]; - - let (mask, new_rows) = if stereo { - parse_ms_data(&mut reader, groups, 0, msfb)? - } else { - (MsMaskPresent::AllZeros, Vec::new()) - }; - merge_ms_rows(&mut ms_used, mask, &new_rows, 0, msfb); - - // Note: `mono_stereo_flag` cannot be set on the main - // layer of an AAC-only configuration (a stereo main - // layer means no mono layer exists), so the - // `tns_channel_mono_layer` bit never occurs here. - let mut tns_v: Vec> = Vec::with_capacity(n_ch); - let mut ltp_v: Vec> = Vec::with_capacity(n_ch); - for _ch in 0..n_ch { - // Table 4.15 per-channel loop: TNS then (AAC-only - // branch) LTP. - if read_bit(&mut reader)? { - tns_v.push(Some(TnsData::parse(&mut reader, ws)?)); - } else { - tns_v.push(None); - } - if read_bit(&mut reader)? { - ltp_v.push(Some(parse_ltp_data(&mut reader, cfg.aot, ws, msfb)?)); - } else { - ltp_v.push(None); - } - } - ics = info; - ms_mask_present = mask; - ms_used_new = new_rows; - tns = tns_v; - ltp = ltp_v; - dl_long = Vec::new(); - dl_short = vec![None; n_ch]; - base_ics = Some(ics.clone()); - } else { - // ---- Table 4.16 aac_scalable_extension_header(). - let base = base_ics.as_ref().ok_or(Error::ScalableInvalid)?; - let ws = base.window_sequence; - let msfb = if ws.is_eight_short() { - read_u8(&mut reader, 4)? - } else { - read_u8(&mut reader, 6)? - }; - if msfb > base.num_swb { - return Err(Error::ScalableInvalid); - } - let groups = usize::from(base.num_window_groups); - let (mask, new_rows) = if stereo { - parse_ms_data(&mut reader, groups, last_max_sfb_ms, msfb)? - } else { - (MsMaskPresent::AllZeros, Vec::new()) - }; - merge_ms_rows(&mut ms_used, mask, &new_rows, last_max_sfb_ms, msfb); - - let tns_v: Vec> = if cfg.mono_stereo_flag(lay) { - let mut v = Vec::with_capacity(2); - for _ch in 0..2 { - if read_bit(&mut reader)? { - v.push(Some(TnsData::parse(&mut reader, ws)?)); - } else { - v.push(None); - } - } - v - } else { - vec![None; n_ch] - }; - - // Table 4.18 diff_control_data_lr(), one per channel. - let mut dl_long_v: Vec> = Vec::new(); - let mut dl_short_v: Vec> = vec![None; n_ch]; - if cfg.mono_layer_flag() && stereo { - for ch in 0..2usize { - if ws != WindowSequence::EightShort { - let hi = core::cmp::min(max_mono_sfb, msfb); - let mut bits = Vec::new(); - for sfb in last_max_sfb_ms..hi { - let on = ms_used - .first() - .and_then(|row| row.get(usize::from(sfb))) - .copied() - .unwrap_or(false); - if !on { - let b = read_bit(&mut reader)?; - bits.push(b); - if usize::from(sfb) >= diff_lr_long[ch].len() { - diff_lr_long[ch].resize(usize::from(sfb) + 1, None); - } - diff_lr_long[ch][usize::from(sfb)] = Some(b); - } - } - dl_long_v.push(bits); - } else { - dl_long_v.push(Vec::new()); - if last_max_sfb_ms == 0 { - // Only in the first stereo layer. - let mut w = [false; 8]; - for slot in w.iter_mut() { - *slot = read_bit(&mut reader)?; - } - dl_short_v[ch] = Some(w); - diff_lr_short[ch] = Some(w); - } - } - } - } - - let mut info = base.clone(); - info.max_sfb = msfb; - ics = info; - ms_mask_present = mask; - ms_used_new = new_rows; - tns = tns_v; - ltp = vec![None; n_ch]; - dl_long = dl_long_v; - dl_short = dl_short_v; - } - - // ---- Per-channel individual_channel_stream(1, 1). - let mut channels: Vec = Vec::with_capacity(n_ch); - for _ch in 0..n_ch { - let body = IcsBody::parse_scale(&mut reader, &ics, cfg.resilience)?; - let spectral = if cfg.resilience.spectral_data { - let (len_reordered, len_longest) = body - .reordered_spectral_lengths - .ok_or(Error::ScalableInvalid)?; - let len = crate::hcr::clamp_reordered_length(len_reordered, stereo); - let mut buf = vec![0u8; usize::from(len).div_ceil(8)]; - for i in 0..usize::from(len) { - if read_bit(&mut reader)? { - buf[i / 8] |= 0x80 >> (i % 8); - } - } - crate::hcr_decode::decode_reordered_spectral_data( - &buf, - len, - len_longest, - &ics, - &body.section_data, - cfg.fs_index, - )? - } else { - SpectralData::parse(&mut reader, &ics, &body.section_data, cfg.fs_index)? - }; - channels.push(ScalableChannel { body, spectral }); - } - - // ---- Trailing extension_payload() loop + byte_alignment(). - let total_bits = (payload.len() as u64) * 8; - let mut cnt = (total_bits.saturating_sub(reader.bit_position())) / 8; - while cnt >= 1 { - let p = ExtensionPayload::parse(&mut reader, cnt as u32)?; - let used = u64::from(p.byte_length()); - if used == 0 || used > cnt { - return Err(Error::ScalableInvalid); - } - cnt -= used; - } - if reader.bit_position() > total_bits { - return Err(Error::ScalableInvalid); - } - - // ---- Cumulative bookkeeping. - if stereo { - last_max_sfb_ms = ics.max_sfb; - } else { - max_mono_sfb = core::cmp::max(max_mono_sfb, ics.max_sfb); - } - max_total_sfb = core::cmp::max(max_total_sfb, ics.max_sfb); - - layers.push(ScalableLayer { - ics, - ms_mask_present, - ms_used_new, - tns, - ltp, - diff_lr_long: dl_long, - diff_lr_short: dl_short, - channels, - }); - } - - // Pad the cumulative mask rows to max_total_sfb. - for row in &mut ms_used { - if row.len() < usize::from(max_total_sfb) { - row.resize(usize::from(max_total_sfb), false); - } - } - - Ok(ScalableFrame { - layers, - ms_used, - diff_lr_long, - diff_lr_short, - max_total_sfb, - max_mono_sfb, - }) - } - - /// Re-emit the frame as one byte-aligned payload per layer — the - /// bit-exact inverse of [`ScalableFrame::parse`] (no trailing - /// extension payloads are emitted). - pub fn write(&self, cfg: &ScalableConfig) -> Result>> { - cfg.validate()?; - if self.layers.len() != cfg.layer_stereo.len() { - return Err(Error::ScalableInvalid); - } - let mut out = Vec::with_capacity(self.layers.len()); - let mut last_max_sfb_ms: u8 = 0; - let mut max_mono_sfb: u8 = 0; - for (lay, layer) in self.layers.iter().enumerate() { - let stereo = cfg.layer_stereo[lay]; - let n_ch = cfg.channels_of_layer(lay); - if layer.channels.len() != n_ch { - return Err(Error::ScalableInvalid); - } - let mut w = BitWriter::new(); - let ics = &layer.ics; - if lay == 0 { - w.write_bit(ics.ics_reserved_bit); - w.write_u32(u32::from(ics.window_sequence as u8), 2); - w.write_bit(matches!(ics.window_shape, WindowShape::Kbd)); - if ics.window_sequence.is_eight_short() { - w.write_u32(u32::from(ics.max_sfb), 4); - w.write_u32( - u32::from(ics.scale_factor_grouping.ok_or(Error::ScalableInvalid)?), - 7, - ); - } else { - w.write_u32(u32::from(ics.max_sfb), 6); - } - if stereo { - write_ms_data( - &mut w, - layer.ms_mask_present, - &layer.ms_used_new, - 0, - ics.max_sfb, - )?; - } - for ch in 0..n_ch { - let tns = layer.tns.get(ch).ok_or(Error::ScalableInvalid)?; - w.write_bit(tns.is_some()); - if let Some(t) = tns { - t.write(&mut w, ics.window_sequence)?; - } - let ltp = layer.ltp.get(ch).ok_or(Error::ScalableInvalid)?; - w.write_bit(ltp.is_some()); - if let Some(l) = ltp { - write_ltp_data(&mut w, l, cfg.aot, ics.window_sequence, ics.max_sfb)?; - } - } - } else { - if ics.window_sequence.is_eight_short() { - w.write_u32(u32::from(ics.max_sfb), 4); - } else { - w.write_u32(u32::from(ics.max_sfb), 6); - } - if stereo { - write_ms_data( - &mut w, - layer.ms_mask_present, - &layer.ms_used_new, - last_max_sfb_ms, - ics.max_sfb, - )?; - } - if cfg.mono_stereo_flag(lay) { - for ch in 0..2usize { - let tns = layer.tns.get(ch).ok_or(Error::ScalableInvalid)?; - w.write_bit(tns.is_some()); - if let Some(t) = tns { - t.write(&mut w, ics.window_sequence)?; - } - } - } - if cfg.mono_layer_flag() && stereo { - for ch in 0..2usize { - if ics.window_sequence != WindowSequence::EightShort { - let bits = layer.diff_lr_long.get(ch).ok_or(Error::ScalableInvalid)?; - let mut it = bits.iter(); - let hi = core::cmp::min(max_mono_sfb, ics.max_sfb); - for sfb in last_max_sfb_ms..hi { - let on = self - .ms_used - .first() - .and_then(|row| row.get(usize::from(sfb))) - .copied() - .unwrap_or(false); - if !on { - w.write_bit(*it.next().ok_or(Error::ScalableInvalid)?); - } - } - if it.next().is_some() { - return Err(Error::ScalableInvalid); - } - } else if last_max_sfb_ms == 0 { - let bits = layer - .diff_lr_short - .get(ch) - .and_then(|b| *b) - .ok_or(Error::ScalableInvalid)?; - for b in bits { - w.write_bit(b); - } - } - } - } - } - - for chan in &layer.channels { - chan.body.write_scale(&mut w, ics, cfg.resilience)?; - if cfg.resilience.spectral_data { - let (buf, len, _longest) = crate::hcr_decode::encode_reordered_spectral_data( - &chan.spectral, - ics, - &chan.body.section_data, - cfg.fs_index, - )?; - // The body writer emitted the stored length fields; - // they must match the re-encoded payload. - let (stored_len, _stored_longest) = chan - .body - .reordered_spectral_lengths - .ok_or(Error::ScalableInvalid)?; - if stored_len != len { - return Err(Error::ScalableInvalid); - } - for i in 0..usize::from(len) { - w.write_bit(buf[i / 8] & (0x80 >> (i % 8)) != 0); - } - } else { - chan.spectral - .write(&mut w, ics, &chan.body.section_data, cfg.fs_index)?; - } - } - // byte_alignment() - let pos = w.bit_position(); - for _ in 0..((8 - (pos % 8)) % 8) { - w.write_bit(false); - } - out.push(w.finish()); - - if stereo { - last_max_sfb_ms = ics.max_sfb; - } else { - max_mono_sfb = core::cmp::max(max_mono_sfb, ics.max_sfb); - } - } - Ok(out) - } -} - -/// Parse a stereo layer's `ms_mask_present` + Table 4.60 `ms_data()` -/// covering bands `lo..hi` (the §4.6.8.1.4 incremental range). -fn parse_ms_data( - reader: &mut BitReader<'_>, - groups: usize, - lo: u8, - hi: u8, -) -> Result<(MsMaskPresent, Vec>)> { - let bits = read_u8(reader, 2)?; - // §4.6.8.1.2: `11` is reserved. - let mask = MsMaskPresent::from_bits(bits).map_err(|_| Error::ScalableInvalid)?; - let mut rows = Vec::new(); - if mask == MsMaskPresent::Mask { - for _g in 0..groups { - let mut row = Vec::new(); - for _sfb in lo..hi { - row.push(read_bit(reader)?); - } - rows.push(row); - } - } - Ok((mask, rows)) -} - -/// Emit `ms_mask_present` + the incremental `ms_data()` rows. -fn write_ms_data( - w: &mut BitWriter, - mask: MsMaskPresent, - rows: &[Vec], - lo: u8, - hi: u8, -) -> Result<()> { - w.write_u32(u32::from(mask.to_bits()), 2); - if mask == MsMaskPresent::Mask { - let span = usize::from(hi.saturating_sub(lo)); - for row in rows { - if row.len() != span { - return Err(Error::ScalableInvalid); - } - for &b in row { - w.write_bit(b); - } - } - } - Ok(()) -} - -/// Fold a layer's transmitted mask into the cumulative `ms_used` -/// (§4.6.8.1.4). `AllOnes` sets the whole incremental range; `Mask` -/// scatters the transmitted rows; `AllZeros` leaves the range clear. -fn merge_ms_rows( - ms_used: &mut [Vec], - mask: MsMaskPresent, - rows: &[Vec], - lo: u8, - hi: u8, -) { - for (g, row) in ms_used.iter_mut().enumerate() { - if row.len() < usize::from(hi) { - row.resize(usize::from(hi), false); - } - for sfb in lo..hi { - let v = match mask { - MsMaskPresent::AllZeros => false, - MsMaskPresent::AllOnes => true, - MsMaskPresent::Mask => rows - .get(g) - .and_then(|r| r.get(usize::from(sfb - lo))) - .copied() - .unwrap_or(false), - }; - if v { - row[usize::from(sfb)] = true; - } - } - } -} - -fn read_bit(reader: &mut BitReader<'_>) -> Result { - reader.read_bit().map_err(|_| Error::UnexpectedEnd) -} - -fn read_u8(reader: &mut BitReader<'_>, bits: u32) -> Result { - Ok(reader.read_u32(bits).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -// --------------------------------------------------------------------------- -// Layer combination + decode driver (§4.5.2.2.4 / §4.6.14.2 / §4.6.9.5) -// --------------------------------------------------------------------------- - -/// Per-band combination state across stereo layers (Tables 4.92/4.93). -#[derive(Debug, Clone, Copy, Default)] -struct BandState { - covered_l: bool, - covered_r: bool, - noise_l: Option, - noise_r: Option, - /// `(in_phase, is_position)` — set while the band is - /// intensity-coded; the position comes from the highest IS layer. - intensity: Option<(bool, i32)>, -} - -/// The window-major slices of one `(g, sfb)` band. -fn band_slices(ics: &IcsInfo, fs: u8, g: usize, sfb: usize) -> Result> { - let window_len = ics.window_len()?; - let offsets = ics.swb_offsets(fs)?; - let lo = *offsets.get(sfb).ok_or(Error::ScalableInvalid)? as usize; - let hi = *offsets.get(sfb + 1).ok_or(Error::ScalableInvalid)? as usize; - let mut window_base = 0usize; - let mut out = Vec::new(); - for (gg, &wgl) in ics.window_group_length.iter().enumerate() { - if gg == g { - for b in 0..usize::from(wgl) { - let base = (window_base + b) * window_len; - out.push((base + lo, base + hi)); - } - return Ok(out); - } - window_base += usize::from(wgl); - } - Err(Error::ScalableInvalid) -} - -/// `true` iff every coefficient of the band is exactly zero in `spec` -/// (§4.6.13.6 "all spectral coefficients … are decoded to zero"). -fn band_is_zero(spec: &[f64], slices: &[(usize, usize)]) -> bool { - slices - .iter() - .all(|&(a, b)| spec[a..b].iter().all(|&v| v == 0.0)) -} - -fn add_band(dst: &mut [f64], src: &[f64], slices: &[(usize, usize)], gain: f64) { - for &(a, b) in slices { - for i in a..b { - dst[i] += gain * src[i]; - } - } -} - -fn copy_band(dst: &mut [f64], src: &[f64], slices: &[(usize, usize)]) { - for &(a, b) in slices { - dst[a..b].copy_from_slice(&src[a..b]); - } -} - -fn zero_band(dst: &mut [f64], slices: &[(usize, usize)]) { - for &(a, b) in slices { - for v in &mut dst[a..b] { - *v = 0.0; - } - } -} - -/// One reconstructed layer: window-major dequantized spectra plus the -/// per-channel band tables. -struct LayerRecon { - /// `[channel]` window-major spectra. - specs: Vec>, - /// `[channel]` band-indexed `noise_nrg[g][sfb]`. - noise: Vec>>, - /// Right-channel band-indexed `is_pos[g][sfb]` (stereo layers). - is_pos: Option>>, -} - -/// §4.6.9.5: the lowest sfb any of this `tns_data()`'s filters -/// reaches (the filters run downward from `max_sfb`), minimised over -/// windows. Used for the Table 4.158 serial-filter override rule. -fn tns_lower_boundary(tns: &TnsData, max_sfb: u8) -> u8 { - let mut lowest = max_sfb; - for w in &tns.windows { - let total: u32 = w.filters.iter().map(|f| u32::from(f.length)).sum(); - let bottom = u32::from(max_sfb).saturating_sub(total) as u8; - lowest = core::cmp::min(lowest, bottom); - } - lowest -} - -/// Spectral-domain output of the layer-combination pipeline: one -/// combined spectrum per output channel, ready for the filterbank. -struct CombinedSpectra { - chans: Vec>, -} - -/// Stateful decoder for one scalable program (§4.5.2.2). -/// -/// Feed one payload per layer per frame ([`ScalableDecoder::decode_frame`]); -/// the per-channel §4.6.11 overlap-add tails and the §4.6.7.5 -/// base-layer LTP history persist across frames. -#[derive(Debug)] -pub struct ScalableDecoder { - cfg: ScalableConfig, - /// Output-path filterbanks (1 or 2). - out_fbs: Vec, - /// Base-layer filterbanks for the §4.6.7.5 LTP history (used only - /// when more than one layer is configured). - base_fbs: Vec, - /// §4.6.7.5 base-layer LTP reconstruction state per base channel. - base_ltp: Vec, - /// §4.6.13.3 generator state for the output run. - pns_state: u32, - /// Independent generator state for the base-layer history run. - base_pns_state: u32, -} - -impl ScalableDecoder { - /// Build a decoder for the given configuration. - pub fn new(cfg: ScalableConfig) -> Result { - cfg.validate()?; - if cfg.aot == 6 - && (cfg.resilience.section_data - || cfg.resilience.scalefactor_data - || cfg.resilience.spectral_data) - { - return Err(Error::ScalableInvalid); - } - let n_out = cfg.output_channels(); - let n_base = cfg.channels_of_layer(0); - Ok(ScalableDecoder { - out_fbs: (0..n_out) - .map(|_| Filterbank::new_family(cfg.family)) - .collect(), - base_fbs: (0..n_base) - .map(|_| Filterbank::new_family(cfg.family)) - .collect(), - base_ltp: (0..n_base) - .map(|_| LtpState::new_family(cfg.family)) - .collect(), - pns_state: 0x0001_2345, - base_pns_state: 0x0001_2345, - cfg, - }) - } - - /// The static configuration. - pub fn config(&self) -> &ScalableConfig { - &self.cfg - } - - /// Decode one frame (one payload per layer, layer 0 first) to - /// interleaved 16-bit PCM. - pub fn decode_frame(&mut self, payloads: &[&[u8]]) -> Result { - let chans = self.decode_frame_channels(payloads)?; - let pcm = crate::pcm::interleave_s16(&chans)?; - Ok(crate::decode::DecodedFrame { - pcm, - channels: chans.len(), - sample_rate: self.cfg.sample_rate, - }) - } - - /// Decode one frame to per-channel `f64` time signals (`L, R` or - /// mono), each `family.frame_len()` samples. - pub fn decode_frame_channels(&mut self, payloads: &[&[u8]]) -> Result>> { - let frame = ScalableFrame::parse(&self.cfg, payloads)?; - let fs = self.cfg.fs_index; - - // ---- Per-layer reconstruction (SIAQ inverse quantisation). - let mut recon: Vec = Vec::with_capacity(frame.layers.len()); - for layer in &frame.layers { - let mut specs = Vec::new(); - let mut noise = Vec::new(); - let mut abs_all: Vec = Vec::new(); - for chan in &layer.channels { - let abs = accumulate( - &chan.body.scale_factor_data, - &chan.body.section_data.sfb_cb, - chan.body.global_gain, - )?; - let rescaled = rescale_spectrum( - &chan.spectral, - &abs, - &chan.body.section_data.sfb_cb, - &layer.ics, - fs, - )?; - let spec = quant_to_spec(&rescaled, &layer.ics, fs)?; - noise.push(crate::element_decode::noise_nrg_table( - &abs, - &chan.body.section_data.sfb_cb, - usize::from(layer.ics.max_sfb), - )?); - specs.push(spec); - abs_all.push(abs); - } - let is_pos = if layer.channels.len() == 2 { - Some(crate::element_decode::is_pos_table( - &abs_all[1], - &layer.channels[1].body.section_data.sfb_cb, - usize::from(layer.ics.max_sfb), - )?) - } else { - None - }; - recon.push(LayerRecon { - specs, - noise, - is_pos, - }); - } - - // ---- §4.6.7.5 base-layer LTP (prediction on layer 0 only; - // IS / PNS bands of the base layer take precedence). - let single_layer = frame.layers.len() == 1; - { - let layer0 = &frame.layers[0]; - let n_base = layer0.channels.len(); - for ch in 0..n_base { - if let Some(ltp) = &layer0.ltp[ch] { - let mut masked = ltp.clone(); - let sfb_cb_own = &layer0.channels[ch].body.section_data.sfb_cb; - let sfb_cb_right = &layer0.channels[n_base - 1].body.section_data.sfb_cb; - for (sfb, used) in masked.long_used.iter_mut().enumerate() { - let noise_band = sfb_cb_own - .first() - .and_then(|row| row.get(sfb)) - .is_some_and(|&cb| cb == NOISE_HCB); - let is_band = n_base == 2 - && sfb_cb_right - .first() - .and_then(|row| row.get(sfb)) - .is_some_and(|&cb| cb == INTENSITY_HCB || cb == INTENSITY_HCB2); - if noise_band || is_band { - *used = false; - } - } - let fb = if single_layer { - &self.out_fbs[ch] - } else { - &self.base_fbs[ch] - }; - let prev_shape = fb.prev_shape(); - let tns = layer0.tns[ch].clone(); - let ics0 = &layer0.ics; - let aot = self.cfg.aot; - let spec0 = &mut recon[0].specs[ch]; - self.base_ltp[ch].apply_long_with_analysis( - spec0, - ics0, - &masked, - prev_shape, - fs, - |x_est| { - if let Some(tns) = &tns { - tns_analysis_frame_ics(x_est, tns, ics0, aot, fs)?; - } - Ok(()) - }, - )?; - } - } - } - - // ---- Full combination run → output channels. - let n_layers = frame.layers.len(); - let mut pns_state = self.pns_state; - let combined = combine_layers(&self.cfg, &frame, &recon, n_layers, &mut pns_state)?; - self.pns_state = pns_state; - let mut out: Vec> = Vec::with_capacity(combined.chans.len()); - for (ch, spec) in combined.chans.iter().enumerate() { - out.push(self.out_fbs[ch].synthesize(spec, &frame.layers[0].ics)?); - } - - // ---- §4.6.7.5 LTP history: the time-domain output of the - // first GA layer decoded alone. - if single_layer { - for (ch, o) in out.iter().enumerate() { - let tail = self.out_fbs[ch].aliased_tail().to_vec(); - self.base_ltp[ch].push_frame(o, &tail); - } - } else { - let mut base_pns = self.base_pns_state; - let base = combine_layers(&self.cfg, &frame, &recon, 1, &mut base_pns)?; - self.base_pns_state = base_pns; - for (ch, spec) in base.chans.iter().enumerate() { - let o = self.base_fbs[ch].synthesize(spec, &frame.layers[0].ics)?; - let tail = self.base_fbs[ch].aliased_tail().to_vec(); - self.base_ltp[ch].push_frame(&o, &tail); - } - } - Ok(out) - } -} - -/// Run the §4.5.2.2.4 layer combination over the first `n_layers` -/// layers: SIAQ accumulation with the Table 4.91–4.93 per-band rules, -/// the §4.6.14.2 FSS mono→stereo merge, cumulative M/S (§4.6.8.1.4), -/// intensity (§4.6.8.2.3), PNS (§4.6.13.6) and the §4.6.9.5 serial -/// TNS. Returns the combined spectra ready for the filterbank. -fn combine_layers( - cfg: &ScalableConfig, - frame: &ScalableFrame, - recon: &[LayerRecon], - n_layers: usize, - pns_state: &mut u32, -) -> Result { - let fs = cfg.fs_index; - let base_ics = &frame.layers[0].ics; - let window_len = base_ics.window_len()?; - let num_windows = usize::from(base_ics.num_windows); - let spec_len = num_windows * window_len; - let num_groups = usize::from(base_ics.num_window_groups); - - // Coverage bounds inside this sub-run. - let stereo_present = (0..n_layers).any(|l| cfg.layer_stereo[l]); - let max_mono: u8 = (0..n_layers) - .filter(|&l| !cfg.layer_stereo[l]) - .map(|l| frame.layers[l].ics.max_sfb) - .max() - .unwrap_or(0); - let max_total: u8 = (0..n_layers) - .map(|l| frame.layers[l].ics.max_sfb) - .max() - .unwrap_or(0); - - // The synthetic geometry every final band op runs under. - let mut ics_total = base_ics.clone(); - ics_total.max_sfb = max_total; - - // Precompute band slices. - let mut slices: Vec>> = Vec::with_capacity(num_groups); - for g in 0..num_groups { - let mut per_sfb = Vec::with_capacity(usize::from(max_total)); - for sfb in 0..usize::from(max_total) { - per_sfb.push(band_slices(&ics_total, fs, g, sfb)?); - } - slices.push(per_sfb); - } - - // ---- Stage 1: mono prefix (Table 4.91). - let mut m_acc = vec![0.0f64; spec_len]; - let mut m_noise: Vec>> = vec![vec![None; usize::from(max_total)]; num_groups]; - let mut m_covered: Vec> = vec![vec![false; usize::from(max_total)]; num_groups]; - for (l, rec) in recon.iter().enumerate().take(n_layers) { - if cfg.layer_stereo[l] { - continue; - } - let layer = &frame.layers[l]; - let spec = &rec.specs[0]; - let sfb_cb = &layer.channels[0].body.section_data.sfb_cb; - for g in 0..num_groups { - for sfb in 0..usize::from(layer.ics.max_sfb) { - let cb = sfb_cb[g][sfb]; - let sl = &slices[g][sfb]; - if cb == NOISE_HCB { - if m_covered[g][sfb] && m_noise[g][sfb].is_none() { - // Table 4.91: No Tool → PNS is invalid. - return Err(Error::ScalableLayerCombination); - } - // First coverage or PNS → PNS (layer N+1 wins). - m_noise[g][sfb] = Some(rec.noise[0][g][sfb]); - } else { - if m_noise[g][sfb].is_some() && !band_is_zero(spec, sl) { - // §4.6.13.6: non-zero higher-layer content - // cancels the noise substitution. - m_noise[g][sfb] = None; - } - add_band(&mut m_acc, spec, sl, 1.0); - } - m_covered[g][sfb] = true; - } - } - } - - // ---- Stage 2: stereo layers (Table 4.92). - let mut l_acc = vec![0.0f64; spec_len]; - let mut r_acc = vec![0.0f64; spec_len]; - let mut st: Vec> = - vec![vec![BandState::default(); usize::from(max_total)]; num_groups]; - for (l, rec) in recon.iter().enumerate().take(n_layers) { - if !cfg.layer_stereo[l] { - continue; - } - let layer = &frame.layers[l]; - let (lspec, rspec) = (&rec.specs[0], &rec.specs[1]); - let lcb_t = &layer.channels[0].body.section_data.sfb_cb; - let rcb_t = &layer.channels[1].body.section_data.sfb_cb; - for g in 0..num_groups { - for sfb in 0..usize::from(layer.ics.max_sfb) { - let sl = &slices[g][sfb]; - let lcb = lcb_t[g][sfb]; - let rcb = rcb_t[g][sfb]; - let s = &mut st[g][sfb]; - let is_band = rcb == INTENSITY_HCB || rcb == INTENSITY_HCB2; - if is_band { - let pos = rec.is_pos.as_ref().map(|t| t[g][sfb]).unwrap_or(0); - let in_phase = rcb == INTENSITY_HCB; - if s.intensity.is_some() { - // IS → IS: sum the M/L channel, take the - // positions from layer N+1. - add_band(&mut l_acc, lspec, sl, 1.0); - } else if s.noise_l.is_some() || s.noise_r.is_some() { - // PNS → IS: layer N+1 only. - s.noise_l = None; - s.noise_r = None; - copy_band(&mut l_acc, lspec, sl); - zero_band(&mut r_acc, sl); - } else if s.covered_l || s.covered_r { - // No Tool / MS → IS: invalid (Table 4.92). - return Err(Error::ScalableLayerCombination); - } else { - copy_band(&mut l_acc, lspec, sl); - } - s.intensity = Some((in_phase, pos)); - s.covered_l = true; - s.covered_r = true; - continue; - } - if s.intensity.is_some() { - if lcb == NOISE_HCB || rcb == NOISE_HCB { - // IS → PNS: invalid (Table 4.92). - return Err(Error::ScalableLayerCombination); - } - // IS → No Tool / MS: layer N+1 only. - s.intensity = None; - copy_band(&mut l_acc, lspec, sl); - copy_band(&mut r_acc, rspec, sl); - s.covered_l = true; - s.covered_r = true; - continue; - } - // Per-channel plain / noise handling. - let ms_band = frame - .ms_used - .get(g) - .and_then(|row| row.get(sfb)) - .copied() - .unwrap_or(false); - let l_zero = band_is_zero(lspec, sl); - let r_zero = band_is_zero(rspec, sl); - // Table 4.93: a plain-coded mono band cannot turn - // into a stereo PNS band (No Tool → PNS is invalid). - let mono_plain = m_covered[g][sfb] && m_noise[g][sfb].is_none(); - // Left channel. - if lcb == NOISE_HCB { - if s.covered_l && s.noise_l.is_none() { - return Err(Error::ScalableLayerCombination); - } - if !s.covered_l && mono_plain { - return Err(Error::ScalableLayerCombination); - } - s.noise_l = Some(rec.noise[0][g][sfb]); - } else { - if s.noise_l.is_some() { - let cancels = if ms_band { - !(l_zero && r_zero) - } else { - !l_zero - }; - if cancels { - s.noise_l = None; - } - } - add_band(&mut l_acc, lspec, sl, 1.0); - } - s.covered_l = true; - // Right channel. - if rcb == NOISE_HCB { - if s.covered_r && s.noise_r.is_none() { - return Err(Error::ScalableLayerCombination); - } - if !s.covered_r && mono_plain { - return Err(Error::ScalableLayerCombination); - } - s.noise_r = Some(rec.noise[1][g][sfb]); - } else { - if s.noise_r.is_some() { - let cancels = if ms_band { - !(l_zero && r_zero) - } else { - !r_zero - }; - if cancels { - s.noise_r = None; - } - } - add_band(&mut r_acc, rspec, sl, 1.0); - } - s.covered_r = true; - } - } - } - - if !stereo_present { - // ---- Mono-only output: PNS, then serial TNS (M source). - let mut sfb_cb: Vec> = vec![vec![1u8; usize::from(max_total)]; num_groups]; - let mut noise_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; - for g in 0..num_groups { - for sfb in 0..usize::from(max_total) { - if let Some(nrg) = m_noise[g][sfb] { - sfb_cb[g][sfb] = NOISE_HCB; - noise_tab[g][sfb] = nrg; - } - } - } - { - let mut chan = PnsChannel { - spec: &mut m_acc, - sfb_cb: &sfb_cb, - noise_nrg: &noise_tab, - }; - apply_pns(&mut chan, &ics_total, fs, |out| { - gen_rand_vector(out, pns_state) - })?; - } - // First mono layer's TNS serves the M output (Table 4.158). - let first_mono = (0..n_layers).find(|&l| !cfg.layer_stereo[l]); - if let Some(l0) = first_mono { - if let Some(tns) = frame.layers[l0].tns.first().and_then(|t| t.as_ref()) { - tns_decode_frame_ics(&mut m_acc, tns, &frame.layers[l0].ics, cfg.aot, fs)?; - } - } - return Ok(CombinedSpectra { chans: vec![m_acc] }); - } - - // ---- Stage 3: mono → stereo merge (Table 4.93 + §4.6.14.2.1). - let has_mono = (0..n_layers).any(|l| !cfg.layer_stereo[l]); - if has_mono { - let short = base_ics.window_sequence.is_eight_short(); - if !short { - for g in 0..num_groups { - for sfb in 0..usize::from(max_mono) { - let s = &st[g][sfb]; - if s.intensity.is_some() || s.noise_l.is_some() || s.noise_r.is_some() { - // Mono content never crosses into an IS / PNS - // band (Table 4.93). - continue; - } - if m_noise[g][sfb].is_some() { - // A mono PNS band never crosses the transition. - continue; - } - let sl = &slices[g][sfb]; - let ms_band = frame.ms_used[g].get(sfb).copied().unwrap_or(false); - if ms_band { - // M = M'' + M' (§4.5.2.2.4). - add_band(&mut l_acc, &m_acc, sl, 1.0); - } else { - // §4.6.14.2.1 FSS: `+ 2·M''` where the bit is 0. - if frame.diff_lr_long[0].get(sfb).copied().flatten() == Some(false) { - add_band(&mut l_acc, &m_acc, sl, 2.0); - } - if frame.diff_lr_long[1].get(sfb).copied().flatten() == Some(false) { - add_band(&mut r_acc, &m_acc, sl, 2.0); - } - } - } - } - } else { - // §4.6.14.2.1 short windows: diff_control_lr[win][0] - // covers every band up to the mono coverage per window. - let offsets = ics_total.swb_offsets(fs)?; - let hi_coef = usize::from(offsets[usize::from(max_mono)]); - let mut window_of_group: Vec = Vec::with_capacity(num_windows); - for (g, &wgl) in base_ics.window_group_length.iter().enumerate() { - for _ in 0..wgl { - window_of_group.push(g); - } - } - for w in 0..num_windows { - let g = window_of_group[w]; - let base = w * window_len; - for sfb in 0..usize::from(max_mono) { - let s = &st[g][sfb]; - if s.intensity.is_some() || s.noise_l.is_some() || s.noise_r.is_some() { - continue; - } - if m_noise[g][sfb].is_some() { - continue; - } - let a = base + usize::from(offsets[sfb]); - let b = base + core::cmp::min(usize::from(offsets[sfb + 1]), hi_coef); - let ms_band = frame.ms_used[g].get(sfb).copied().unwrap_or(false); - if ms_band { - for i in a..b { - l_acc[i] += m_acc[i]; - } - } else { - if frame.diff_lr_short[0].map(|bits| bits[w]) == Some(false) { - for i in a..b { - l_acc[i] += 2.0 * m_acc[i]; - } - } - if frame.diff_lr_short[1].map(|bits| bits[w]) == Some(false) { - for i in a..b { - r_acc[i] += 2.0 * m_acc[i]; - } - } - } - } - } - } - } - - // ---- Stage 4: synthetic band tables → M/S → IS → PNS. - let mut synth_l: Vec> = vec![vec![1u8; usize::from(max_total)]; num_groups]; - let mut synth_r: Vec> = vec![vec![1u8; usize::from(max_total)]; num_groups]; - let mut noise_l_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; - let mut noise_r_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; - let mut is_pos_tab: Vec> = vec![vec![0i32; usize::from(max_total)]; num_groups]; - for g in 0..num_groups { - for sfb in 0..usize::from(max_total) { - let s = &st[g][sfb]; - if let Some((in_phase, pos)) = s.intensity { - synth_r[g][sfb] = if in_phase { - INTENSITY_HCB - } else { - INTENSITY_HCB2 - }; - is_pos_tab[g][sfb] = pos; - continue; - } - if let Some(nrg) = s.noise_l { - synth_l[g][sfb] = NOISE_HCB; - noise_l_tab[g][sfb] = nrg; - } - if let Some(nrg) = s.noise_r { - synth_r[g][sfb] = NOISE_HCB; - noise_r_tab[g][sfb] = nrg; - } - } - } - - { - let mut pair = ChannelPairSpectra { - left: &mut l_acc, - right: &mut r_acc, - left_sfb_cb: &synth_l, - right_sfb_cb: &synth_r, - }; - apply_ms_stereo( - &mut pair, - MsMaskPresent::Mask, - &frame.ms_used, - &ics_total, - fs, - )?; - } - { - let mut pair = IntensityPairSpectra { - left: &l_acc, - right: &mut r_acc, - right_sfb_cb: &synth_r, - is_pos: &is_pos_tab, - }; - // §4.6.8.2.3: invert_intensity() == +1 for the scalable AOT, - // so the ms_used phase-reversal branch is disabled. - apply_intensity_stereo(&mut pair, false, &[], &ics_total, fs)?; - } - { - let mut left = PnsChannel { - spec: &mut l_acc, - sfb_cb: &synth_l, - noise_nrg: &noise_l_tab, - }; - let mut right = PnsChannel { - spec: &mut r_acc, - sfb_cb: &synth_r, - noise_nrg: &noise_r_tab, - }; - // §4.6.13.6: the cumulative ms_used still signals noise - // correlation across the channel pair. - apply_pns_pair( - &mut left, - &mut right, - true, - false, - &frame.ms_used, - &ics_total, - fs, - |out| gen_rand_vector(out, pns_state), - )?; - } - - // ---- Stage 5: §4.6.9.5 serial TNS (Table 4.158). - let first_mono = (0..n_layers).find(|&l| !cfg.layer_stereo[l]); - let first_stereo = (0..n_layers).find(|&l| cfg.layer_stereo[l]); - let tns_m: Option<(&TnsData, &IcsInfo)> = first_mono.and_then(|l| { - frame.layers[l] - .tns - .first() - .and_then(|t| t.as_ref()) - .map(|t| (t, &frame.layers[l].ics)) - }); - for (ch, acc) in [&mut l_acc, &mut r_acc].into_iter().enumerate() { - let tns_ch: Option<(&TnsData, &IcsInfo)> = first_stereo.and_then(|l| { - frame.layers[l] - .tns - .get(ch) - .and_then(|t| t.as_ref()) - .map(|t| (t, &frame.layers[l].ics)) - }); - match (tns_ch, tns_m) { - (Some((t, ics)), Some((tm, ics_m))) => { - // Serial L/M (R/M) layout: the M filter first (it - // covers the low bands, stopping at the highest mono - // max_sfb), then the channel filter — unless the - // channel filter reaches below the mono boundary, in - // which case the M filter is skipped. - if tns_lower_boundary(t, ics.max_sfb) >= max_mono { - tns_decode_frame_ics(acc, tm, ics_m, cfg.aot, fs)?; - } - tns_decode_frame_ics(acc, t, ics, cfg.aot, fs)?; - } - (Some((t, ics)), None) => { - tns_decode_frame_ics(acc, t, ics, cfg.aot, fs)?; - } - (None, Some((tm, ics_m))) => { - tns_decode_frame_ics(acc, tm, ics_m, cfg.aot, fs)?; - } - (None, None) => {} - } - } - - Ok(CombinedSpectra { - chans: vec![l_acc, r_acc], - }) -} diff --git a/crates/vendor/oxideav-aac/src/scale_factor_data.rs b/crates/vendor/oxideav-aac/src/scale_factor_data.rs deleted file mode 100644 index d247ae9d..00000000 --- a/crates/vendor/oxideav-aac/src/scale_factor_data.rs +++ /dev/null @@ -1,1579 +0,0 @@ -//! `scale_factor_data()` parser + encoder primitive — ISO/IEC 14496-3 -//! §4.4.6 / Table 4.53 (non-resilient branch) plus §4.6.3 / Table 4.A.1 -//! ("Scalefactor Huffman Codebook" — codebook 12). -//! -//! `scale_factor_data()` is the third tool inside -//! `individual_channel_stream()` (after `global_gain` and -//! `section_data()`, before `pulse_data_present` / -//! `pulse_data()`). For every `(g, sfb)` whose -//! [`section_data`](crate::section_data) classifier picked a non-zero -//! codebook, this tool emits one differentially-coded value (a DPCM -//! delta in the range `-60..=+60`) using the 121-entry Table 4.A.1 -//! Huffman codebook. The exception is the **first** Perceptual Noise -//! Substitution (PNS) band of the frame, whose energy delta is sent -//! as a literal 9-bit signed value — every subsequent PNS band falls -//! back to the Huffman path. -//! -//! ## Wire layout (Table 4.53, non-resilient branch) -//! -//! ```text -//! scale_factor_data() { -//! noise_pcm_flag = 1 -//! for (g = 0; g < num_window_groups; g++) { -//! for (sfb = 0; sfb < max_sfb; sfb++) { -//! if (sfb_cb[g][sfb] != ZERO_HCB) { -//! if (is_intensity(g, sfb)) { -//! hcod_sf[dpcm_is_position[g][sfb]]; 1..19 bits -//! } else if (is_noise(g, sfb)) { -//! if (noise_pcm_flag) { -//! noise_pcm_flag = 0 -//! dpcm_noise_nrg[g][sfb]; 9 bits (PCM) -//! } else { -//! hcod_sf[dpcm_noise_nrg[g][sfb]]; 1..19 bits -//! } -//! } else { -//! hcod_sf[dpcm_sf[g][sfb]]; 1..19 bits -//! } -//! } -//! } -//! } -//! } -//! ``` -//! -//! Three observations the parser and writer both rely on: -//! -//! 1. The outer `(g, sfb)` traversal is **driven by** -//! [`section_data::SectionData::sfb_cb`](crate::section_data::SectionData::sfb_cb) -//! — the parser must already know which bands carry a value before -//! it can decide between "skip", "Huffman value", or "9-bit PCM -//! energy". The wire stream carries no per-band header that would -//! let it self-synchronise. -//! 2. The DPCM range is `-60..=+60` (Table 4.150). The Huffman -//! codebook (Table 4.A.1) has 121 entries indexed `0..=120`; an -//! `index_offset` of `-60` recovers the signed delta. The codeword -//! for index 60 (delta 0) is the single bit `0`. -//! 3. `noise_pcm_flag` is **frame-scoped** (not group-scoped): it -//! starts at `1` at the top of `scale_factor_data()` and clears the -//! first time a PNS band is emitted, regardless of which window -//! group or scalefactor band that is. -//! -//! ## What this module covers -//! -//! * [`ScaleFactorData::parse`] — read a non-resilient Table 4.53 -//! block given the surrounding `sfb_cb[g][sfb]` map. Surfaces the -//! raw transmitted `dpcm_sf` / `dpcm_is_position` deltas and the -//! `dpcm_noise_nrg` magnitudes verbatim. -//! * [`ScaleFactorData::write`] — the inverse: serialise a -//! [`ScaleFactorData`] bit-for-bit. Surfaces caller-side structural -//! bugs (delta out of range, PCM energy out of range, missing / -//! surplus per-band entry versus the `sfb_cb` map) as -//! [`Error::ScaleFactorDataEncodeInvalid`]. -//! * [`accumulate`] — the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM -//! accumulator (decoder side). Runs the three independent tracks -//! forward: spectrum scalefactors (`last_sf = global_gain`), -//! intensity stereo positions (`last_is = 0`), and PNS noise -//! energies (`last_nrg = global_gain - NOISE_OFFSET - 256`). -//! Returns absolute `(sf, is_pos, noise_nrg)` per band. -//! * [`differentiate`] — the symmetric inverse (encoder side). Takes -//! absolute per-band quantities from rate-allocation and produces -//! the [`ScaleFactorData`] the bit-exact writer expects. Validates -//! that every spectrum / intensity / PNS-subsequent delta fits -//! Table 4.150's `-60..=+60`, and that the first PNS band's -//! seed fits the 9-bit `uimsbf` Table 4.53 field. -//! * [`hcod_sf_encode`] / [`hcod_sf_decode`] — public Table 4.A.1 -//! accessors for callers (Auditor harnesses, fixture cross-checks) -//! that need the codebook directly without going through the full -//! `scale_factor_data()` driver. -//! -//! ## Three-track DPCM (spec ambiguity, resolved per §4.6.8 / §4.6.13) -//! -//! The §4.6.2.3.2 illustrative pseudocode declares **one** accumulator -//! `last_sf = global_gain` and lumps PNS (`NOISE_HCB`) bands into it -//! alongside spectrum bands. This pseudocode predates MPEG-4's PNS -//! feature (it is identical in 13818-7 §11.3.2 where no PNS exists) -//! and conflicts with the surrounding §4.6.8.1.4 + §4.6.13 wording, -//! which states explicitly that "differential decoding is done -//! separately between scalefactors, intensity stereo positions and -//! noise energies" with each track having its own running register -//! and its own initial-condition seed. -//! -//! This module implements the three-track interpretation: -//! intensity bands seed at `last_is = 0`, PNS bands seed at -//! `last_nrg = global_gain - NOISE_OFFSET - 256` (with the first -//! PNS band's 9-bit literal added directly to `last_nrg`), spectrum -//! bands seed at `last_sf = global_gain`. The §4.6.2.3.2 pseudocode's -//! single-track form is not used because the §4.6.8 / §4.6.13 -//! prose-level requirement of independence cannot be honoured under -//! a single track that mixes spectrum and PNS deltas. -//! -//! ## What this module does *not* cover -//! -//! * The §4.4.6 error-resilient branch (`aacScalefactorDataResilienceFlag -//! == 1` → RVLC with `rev_global_gain`, `length_of_rvlc_sf`, -//! `sf_concealment`, `length_of_rvlc_escapes`, etc.) — the in-memory -//! structure here is the non-resilient flavour. ER AAC-LD / scalable -//! profiles that flip the resilience flag will need a sibling -//! `scale_factor_data_rvlc()` module. -//! * The §4.6.2.3.3 / §4.6.8 / §4.6.13 reconstruction steps that -//! actually *consume* the absolute values: `get_scale_factor_gain -//! = 2^(0.25 * (sf - SF_OFFSET))`, the IS rescaling sign-flip per -//! `ms_used`, and the PNS random-vector energy rescaling. Those -//! are per-AOT IMDCT back-end concerns that need spectral-context -//! state this module does not own. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::ics_info::WindowSequence; -use crate::section_data::{INTENSITY_HCB, INTENSITY_HCB2, NOISE_HCB, ZERO_HCB}; -use crate::{Error, Result}; - -// ============================================================================= -// Table 4.A.1 — Scalefactor Huffman Codebook (codebook 12) -// ============================================================================= -// -// Per Table 4.150, the codebook covers indices 0..=120 with -// `index_offset = -60`, producing DPCM values in `-60..=+60`. The -// table is reproduced verbatim from ISO/IEC 14496-3 §4.A.1 / Table -// 4.A.1 with every length / codeword cross-checked against the -// 13818-7 §11.3.2 / Table 11.3 listing (the two specifications carry -// the same table for backwards bitstream compatibility). -// -// Format: `(length_in_bits, codeword_value)`. Codewords are stored -// right-aligned (the MSB of the wire codeword sits at bit -// `length - 1`), exactly as the Table 4.A.1 hexadecimal column -// presents them. - -/// `index_offset` for the scalefactor codebook per Table 4.150 -/// (`-60`, surfaced as a signed type because the DPCM range is -/// `-60..=+60`). -pub const SF_INDEX_OFFSET: i8 = -60; - -/// `dpcm_noise_nrg` PCM seed width — Table 4.53 `dpcm_noise_nrg` -/// row (9 bits, `uimsbf` in the spec which the §4.6.13 decoder -/// re-interprets as a signed 9-bit delta). -pub const NOISE_PCM_BITS: u32 = 9; - -/// Number of entries in Table 4.A.1 (`121`, indices `0..=120`). -pub const HCOD_SF_NUM_ENTRIES: usize = 121; - -/// Maximum codeword length emitted by Table 4.A.1 (19 bits). -pub const HCOD_SF_MAX_LEN: u32 = 19; - -/// Table 4.A.1 — `(length_in_bits, codeword)` per index `0..=120`. -/// -/// Codewords are right-aligned within the `u32`. To emit one bit-for- -/// bit, write `codeword` as `length` bits MSB-first. -const HCOD_SF: [(u8, u32); HCOD_SF_NUM_ENTRIES] = [ - (18, 0x3ffe8), // 0 - (18, 0x3ffe6), // 1 - (18, 0x3ffe7), // 2 - (18, 0x3ffe5), // 3 - (19, 0x7fff5), // 4 - (19, 0x7fff1), // 5 - (19, 0x7ffed), // 6 - (19, 0x7fff6), // 7 - (19, 0x7ffee), // 8 - (19, 0x7ffef), // 9 - (19, 0x7fff0), // 10 - (19, 0x7fffc), // 11 - (19, 0x7fffd), // 12 - (19, 0x7ffff), // 13 - (19, 0x7fffe), // 14 - (19, 0x7fff7), // 15 - (19, 0x7fff8), // 16 - (19, 0x7fffb), // 17 - (19, 0x7fff9), // 18 - (18, 0x3ffe4), // 19 - (19, 0x7fffa), // 20 - (18, 0x3ffe3), // 21 - (17, 0x1ffef), // 22 - (17, 0x1fff0), // 23 - (16, 0x0fff5), // 24 - (17, 0x1ffee), // 25 - (16, 0x0fff2), // 26 - (16, 0x0fff3), // 27 - (16, 0x0fff4), // 28 - (16, 0x0fff1), // 29 - (15, 0x07ff6), // 30 - (15, 0x07ff7), // 31 - (14, 0x03ff9), // 32 - (14, 0x03ff5), // 33 - (14, 0x03ff7), // 34 - (14, 0x03ff3), // 35 - (14, 0x03ff6), // 36 - (14, 0x03ff2), // 37 - (13, 0x01ff7), // 38 - (13, 0x01ff5), // 39 - (12, 0x00ff9), // 40 - (12, 0x00ff7), // 41 - (12, 0x00ff6), // 42 - (11, 0x007f9), // 43 - (12, 0x00ff4), // 44 - (11, 0x007f8), // 45 - (10, 0x003f9), // 46 - (10, 0x003f7), // 47 - (10, 0x003f5), // 48 - (9, 0x001f8), // 49 - (9, 0x001f7), // 50 - (8, 0x000fa), // 51 - (8, 0x000f8), // 52 - (8, 0x000f6), // 53 - (7, 0x00079), // 54 - (6, 0x0003a), // 55 - (6, 0x00038), // 56 - (5, 0x0001a), // 57 - (4, 0x0000b), // 58 - (3, 0x00004), // 59 - (1, 0x00000), // 60 — delta 0, single bit `0` - (4, 0x0000a), // 61 - (4, 0x0000c), // 62 - (5, 0x0001b), // 63 - (6, 0x00039), // 64 - (6, 0x0003b), // 65 - (7, 0x00078), // 66 - (7, 0x0007a), // 67 - (8, 0x000f7), // 68 - (8, 0x000f9), // 69 - (9, 0x001f6), // 70 - (9, 0x001f9), // 71 - (10, 0x003f4), // 72 - (10, 0x003f6), // 73 - (10, 0x003f8), // 74 - (11, 0x007f5), // 75 - (11, 0x007f4), // 76 - (11, 0x007f6), // 77 - (11, 0x007f7), // 78 - (12, 0x00ff5), // 79 - (12, 0x00ff8), // 80 - (13, 0x01ff4), // 81 - (13, 0x01ff6), // 82 - (13, 0x01ff8), // 83 - (14, 0x03ff8), // 84 - (14, 0x03ff4), // 85 - (16, 0x0fff0), // 86 - (15, 0x07ff4), // 87 - (16, 0x0fff6), // 88 - (15, 0x07ff5), // 89 - (18, 0x3ffe2), // 90 - (19, 0x7ffd9), // 91 - (19, 0x7ffda), // 92 - (19, 0x7ffdb), // 93 - (19, 0x7ffdc), // 94 - (19, 0x7ffdd), // 95 - (19, 0x7ffde), // 96 - (19, 0x7ffd8), // 97 - (19, 0x7ffd2), // 98 - (19, 0x7ffd3), // 99 - (19, 0x7ffd4), // 100 - (19, 0x7ffd5), // 101 - (19, 0x7ffd6), // 102 - (19, 0x7fff2), // 103 - (19, 0x7ffdf), // 104 - (19, 0x7ffe7), // 105 - (19, 0x7ffe8), // 106 - (19, 0x7ffe9), // 107 - (19, 0x7ffea), // 108 - (19, 0x7ffeb), // 109 - (19, 0x7ffe6), // 110 - (19, 0x7ffe0), // 111 - (19, 0x7ffe1), // 112 - (19, 0x7ffe2), // 113 - (19, 0x7ffe3), // 114 - (19, 0x7ffe4), // 115 - (19, 0x7ffe5), // 116 - (19, 0x7ffd7), // 117 - (19, 0x7ffec), // 118 - (19, 0x7fff4), // 119 - (19, 0x7fff3), // 120 -]; - -/// Encode a signed DPCM delta in `-60..=+60` to the wire Huffman -/// codeword for Table 4.A.1. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u32` (MSB at bit `length - 1`). Out-of-range `dpcm` -/// produces [`Error::ScaleFactorDataEncodeInvalid`]. -/// -/// The inverse of [`hcod_sf_decode`]. -pub fn hcod_sf_encode(dpcm: i8) -> Result<(u8, u32)> { - let idx = (dpcm as i32) - (SF_INDEX_OFFSET as i32); - if !(0..HCOD_SF_NUM_ENTRIES as i32).contains(&idx) { - return Err(Error::ScaleFactorDataEncodeInvalid); - } - Ok(HCOD_SF[idx as usize]) -} - -/// Decode one Table 4.A.1 Huffman codeword from `reader`, returning -/// the signed DPCM delta in `-60..=+60`. -/// -/// The decoder is a straight prefix-match: read one bit at a time, -/// look it up in a flat table. The table is small (121 entries, max -/// length 19 bits) so a single linear scan per bit-extend is -/// sufficient and avoids the cost / complexity of a multi-level -/// lookup acceleration table. Returns [`Error::UnexpectedEnd`] on -/// reader underflow. -/// -/// The codebook is a **complete** prefix code (Kraft equality: -/// `Σ 2^(19-L_i) = 2^19`), so every fully-read 19-bit sequence is -/// guaranteed to match some entry — the bottom of the loop is -/// unreachable provided `reader` produces 19 bits without -/// underflowing. A purely-defensive `unreachable!()` guards the -/// loop fall-through; it has been verified at compile-time as -/// dead code by the [`hcod_sf_decode_is_complete`](#) regression -/// test that exhaustively walks all `2^19` 19-bit prefixes. -pub fn hcod_sf_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD_SF_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - // Linear scan: cost is bounded by HCOD_SF_NUM_ENTRIES * 19. - for (idx, &(entry_len, entry_cw)) in HCOD_SF.iter().enumerate() { - if u32::from(entry_len) == len && entry_cw == acc { - return Ok((idx as i8) + SF_INDEX_OFFSET); - } - } - } - // Unreachable: the codebook is a complete prefix code over - // 19 bits (Kraft equality = 524288), so the inner loop must - // hit for at least one `len <= 19`. The guard is here so the - // compiler doesn't infer a non-`!` return path. - unreachable!("HCOD_SF is a complete 19-bit prefix code; the 19-bit walk must match"); -} - -// ============================================================================= -// Per-band record -// ============================================================================= - -/// One transmitted per-band record. -/// -/// The variant is selected by [`crate::section_data::SectionData::sfb_cb`]: -/// `Dpcm` for ordinary spectrum books (1..=11, plus PNS book 13 -/// after the first), `Intensity` for books 14 / 15, `NoisePcm` for -/// the **first** PNS band of the frame. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ScaleFactorEntry { - /// `hcod_sf[dpcm_sf[g][sfb]]` — Huffman DPCM delta for a band - /// whose codebook is a non-zero spectrum book (1..=11). - Dpcm(i8), - /// `hcod_sf[dpcm_is_position[g][sfb]]` — Huffman DPCM delta for - /// an intensity-stereo band (codebook 14 or 15). - Intensity(i8), - /// `dpcm_noise_nrg[g][sfb]` 9-bit PCM seed — emitted **only** - /// for the first PNS band (codebook 13) of the frame. The value - /// is the raw 9-bit wire bits (the §4.6.13 reconstruction - /// converts the unsigned wire pattern to a signed `-256..=+255` - /// energy delta). - NoisePcm(u16), - /// `hcod_sf[dpcm_noise_nrg[g][sfb]]` — Huffman DPCM delta for a - /// PNS band after the first. - NoiseDpcm(i8), -} - -/// Parsed `scale_factor_data()` payload (non-resilient branch). -/// -/// `entries` is grouped per window group: `entries[g][i]` is the -/// `i`-th transmitted per-band record for group `g`, in wire -/// (low-frequency-first) order. The mapping back to scalefactor -/// bands is recovered by walking -/// [`SectionData::sfb_cb`](crate::section_data::SectionData::sfb_cb) -/// and skipping `ZERO_HCB` bands — the same walk the parser -/// performed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ScaleFactorData { - /// `entries[g]` — the per-band records of window group `g` in - /// wire order. `entries.len()` equals `sfb_cb.len()` - /// (`num_window_groups`). - pub entries: Vec>, -} - -impl ScaleFactorData { - /// Parse a non-resilient `scale_factor_data()` from `reader`. - /// - /// * `reader` — positioned at the first bit of the - /// `scale_factor_data()` block (immediately after - /// `section_data()`). - /// * `sfb_cb` — the per-`(g, sfb)` codebook map produced by - /// [`section_data::SectionData::parse`](crate::section_data::SectionData::parse). - /// Outer length is `num_window_groups`; each inner slice is - /// `max_sfb` entries. - /// - /// Returns [`Error::UnexpectedEnd`] on reader underflow. The - /// codebook is a complete 19-bit prefix code so a fully-read - /// Huffman value is guaranteed to match an entry. - pub fn parse(reader: &mut BitReader<'_>, sfb_cb: &[Vec]) -> Result { - let mut noise_pcm_flag = true; - let mut entries: Vec> = Vec::with_capacity(sfb_cb.len()); - for group in sfb_cb { - let mut group_entries: Vec = Vec::new(); - for &cb in group { - if cb == ZERO_HCB { - continue; - } - let entry = if is_intensity(cb) { - let dpcm = hcod_sf_decode(reader)?; - ScaleFactorEntry::Intensity(dpcm) - } else if is_noise(cb) { - if noise_pcm_flag { - noise_pcm_flag = false; - let pcm = reader - .read_u32(NOISE_PCM_BITS) - .map_err(|_| Error::UnexpectedEnd)? - as u16; - ScaleFactorEntry::NoisePcm(pcm) - } else { - let dpcm = hcod_sf_decode(reader)?; - ScaleFactorEntry::NoiseDpcm(dpcm) - } - } else { - let dpcm = hcod_sf_decode(reader)?; - ScaleFactorEntry::Dpcm(dpcm) - }; - group_entries.push(entry); - } - entries.push(group_entries); - } - Ok(ScaleFactorData { entries }) - } - - /// Encode `scale_factor_data()` onto `writer`, the inverse of - /// [`ScaleFactorData::parse`]. - /// - /// * `writer` — receives the bit-exact Table 4.53 stream. - /// * `sfb_cb` — the same codebook map the matching parse call - /// would receive. Drives the variant the writer expects at - /// each band. - /// - /// Returns [`Error::ScaleFactorDataEncodeInvalid`] if: - /// - /// * `self.entries.len()` does not equal `sfb_cb.len()`. - /// * A group's `entries` count does not match the number of - /// non-zero-codebook bands in the matching `sfb_cb` group. - /// * The variant at index `i` does not match the codebook - /// classification of the `i`-th non-zero band - /// (e.g. [`ScaleFactorEntry::Intensity`] paired with a - /// spectrum book, or [`ScaleFactorEntry::NoisePcm`] paired - /// with a non-PNS band, or — for the second PNS band onward — - /// [`ScaleFactorEntry::NoisePcm`] re-used after - /// `noise_pcm_flag` has cleared). - /// * A `Dpcm` / `Intensity` / `NoiseDpcm` delta falls outside - /// `-60..=+60`. - /// * A `NoisePcm` value exceeds the 9-bit field cap - /// (`> 0x1ff`). - pub fn write(&self, writer: &mut BitWriter, sfb_cb: &[Vec]) -> Result<()> { - if self.entries.len() != sfb_cb.len() { - return Err(Error::ScaleFactorDataEncodeInvalid); - } - let mut noise_pcm_flag = true; - for (group_entries, group_cb) in self.entries.iter().zip(sfb_cb.iter()) { - // Walk both in lockstep: the entries list and the - // non-zero subsequence of sfb_cb must match position-by- - // position. Surfacing a mismatch is the same error - // regardless of cause (length vs variant mismatch). - let mut entry_iter = group_entries.iter(); - for &cb in group_cb { - if cb == ZERO_HCB { - continue; - } - let entry = entry_iter - .next() - .ok_or(Error::ScaleFactorDataEncodeInvalid)?; - match (entry, cb) { - (ScaleFactorEntry::Intensity(dpcm), cb) if is_intensity(cb) => { - let (len, cw) = hcod_sf_encode(*dpcm)?; - writer.write_u32(cw, u32::from(len)); - } - (ScaleFactorEntry::NoisePcm(pcm), cb) if is_noise(cb) => { - if !noise_pcm_flag { - // PNS seed already consumed earlier; - // a second NoisePcm is wire-illegal. - return Err(Error::ScaleFactorDataEncodeInvalid); - } - if u32::from(*pcm) >= (1u32 << NOISE_PCM_BITS) { - return Err(Error::ScaleFactorDataEncodeInvalid); - } - noise_pcm_flag = false; - writer.write_u32(u32::from(*pcm), NOISE_PCM_BITS); - } - (ScaleFactorEntry::NoiseDpcm(dpcm), cb) if is_noise(cb) => { - if noise_pcm_flag { - // First PNS band of the frame must use - // the 9-bit PCM seed, not the Huffman - // delta — caller skipped the seed. - return Err(Error::ScaleFactorDataEncodeInvalid); - } - let (len, cw) = hcod_sf_encode(*dpcm)?; - writer.write_u32(cw, u32::from(len)); - } - (ScaleFactorEntry::Dpcm(dpcm), cb) if !is_intensity(cb) && !is_noise(cb) => { - let (len, cw) = hcod_sf_encode(*dpcm)?; - writer.write_u32(cw, u32::from(len)); - } - _ => return Err(Error::ScaleFactorDataEncodeInvalid), - } - } - // Extra entries beyond the non-zero codebook subsequence - // would silently shift the wire layout — reject. - if entry_iter.next().is_some() { - return Err(Error::ScaleFactorDataEncodeInvalid); - } - } - Ok(()) - } -} - -// ============================================================================= -// §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM accumulators -// ============================================================================= -// -// `scale_factor_data()` transmits *differential* values. Recovering the -// absolute per-band quantities the per-AOT IMDCT / intensity-stereo / -// PNS back-ends consume requires accumulating the DPCM deltas against -// initial-condition seeds. There are **three** independent tracks: -// -// 1. **Spectrum scalefactors** (codebooks 1..=11): per ISO/IEC 14496-3 -// §4.6.2.3.2 / ISO/IEC 13818-7 §11.3.2, accumulator initial value -// `last_sf = global_gain`; per-band `sf[g][sfb] = dpcm_sf + -// last_sf; last_sf = sf[g][sfb]`. Range `0..=255` (clause note; -// the 13818-7 wording matches). -// -// 2. **Intensity stereo positions** (codebooks 14, 15): per -// §4.6.8.1.4, initial `last_is = 0`; per-band `is_pos[g][sfb] = -// dpcm_is_position + last_is; last_is = is_pos[g][sfb]`. The -// §4.6.8.1.4 text is explicit that intensity-position differential -// decoding is "done separately" from the scalefactor track, with -// the seed starting at zero rather than `global_gain`. -// -// 3. **PNS noise energies** (codebook 13): per §4.6.13, initial -// `last_nrg = global_gain - NOISE_OFFSET - 256` (`NOISE_OFFSET == -// 90`); the first PNS band carries a 9-bit `uimsbf` literal -// `dpcm_noise_nrg` (added to `last_nrg` directly), each -// subsequent PNS band carries a Huffman delta in `-60..=+60`. -// Per-band `noise_nrg[g][sfb] = dpcm_noise_nrg + last_nrg; -// last_nrg = noise_nrg[g][sfb]`. The §4.6.13 text is explicit -// that PNS energies are "done separately" from both other tracks. -// -// The three-track presentation in §4.6.8 / §4.6.13 takes precedence -// over the §4.6.2.3.2 illustrative pseudocode (which predates PNS -// in 13818-7 and conflates the spectrum + PNS tracks under a single -// `last_sf` register). The "done separately" wording in §4.6.8.1.4 -// and §4.6.13 is unambiguous; this crate honours it. -// -// `accumulate(sfd, sfb_cb, global_gain)` runs all three tracks -// forward (decoder side) to recover absolute `(sf, is_pos, -// noise_nrg)`. `differentiate(abs, sfb_cb, global_gain)` is its -// inverse (encoder side, fed by the rate-allocation stage's -// absolute-value output). - -/// `NOISE_OFFSET` per §4.6.13 — added to the PNS energy seed to -/// position the running `last_nrg` register relative to -/// `global_gain`. -pub const NOISE_OFFSET: i32 = 90; - -/// One absolute per-band record, the result of running the §4.6.2.3.2 -/// / §4.6.8.1.4 / §4.6.13 DPCM accumulators forward over a -/// [`ScaleFactorData`] together with `global_gain`. -/// -/// The variant matches the [`ScaleFactorEntry`] variant of the -/// corresponding transmitted record but carries the absolute value -/// the per-AOT back-end consumes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AbsoluteScaleFactorEntry { - /// Absolute spectrum-band scalefactor `sf[g][sfb] ∈ 0..=255` — - /// the gain applied to the spectral coefficients of this - /// scalefactor band per §4.6.2.3.3. - Sf(u8), - /// Absolute intensity stereo position `is_pos[g][sfb] ∈ - /// -60..=+60` accumulated — the value the §4.6.8.2 IS decoder - /// consumes. The track seeds at 0 and accumulates `-60..=+60` - /// deltas, so the absolute value's reachable range is in - /// principle unbounded; conforming streams keep it within the - /// signed 8-bit window. - IsPos(i16), - /// Absolute noise energy `noise_nrg[g][sfb]` — the value the - /// §4.6.13 noise-substitution back-end consumes. Tracked as - /// `i32` because the seed is `global_gain - NOISE_OFFSET - 256` - /// (which can be negative for small `global_gain`) and the - /// running accumulator may dip negative before the first PNS - /// band lands a positive 9-bit delta. - NoiseNrg(i32), -} - -/// Absolute per-band quantities recovered by running the §4.6.2.3.2 -/// / §4.6.8.1.4 / §4.6.13 DPCM accumulators forward over a -/// [`ScaleFactorData`]. -/// -/// Outer length equals `sfb_cb.len()` (`num_window_groups`); inner -/// `entries[g]` length matches the `entries[g]` of the source -/// [`ScaleFactorData`] (the non-`ZERO_HCB` band count of the -/// matching `sfb_cb[g]`). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AbsoluteScaleFactors { - /// `entries[g]` — the per-band absolute records of window group - /// `g` in wire (low-frequency-first) order. Variant order - /// follows the per-band codebook classification in the matching - /// `sfb_cb[g]`, skipping `ZERO_HCB` bands. - pub entries: Vec>, -} - -/// Run the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM accumulators -/// forward over `sfd` to recover absolute scalefactors, intensity -/// stereo positions, and PNS noise energies (decoder side). -/// -/// * `sfd` — the transmitted DPCM record set returned by -/// [`ScaleFactorData::parse`]. -/// * `sfb_cb` — the per-`(g, sfb)` codebook map produced by -/// [`crate::section_data::SectionData::parse`]. -/// * `global_gain` — the 8-bit `global_gain` element transmitted -/// immediately before `section_data()` in -/// `individual_channel_stream()`. -/// -/// Returns [`Error::ScaleFactorAccumulatorInvalid`] if the -/// per-group entry layout in `sfd` does not match the non-`ZERO_HCB` -/// codebook classification of the matching `sfb_cb` group, or if a -/// Sf-track running value escapes the `0..=255` spec range (Note -/// after §4.6.2.3.2 pseudocode). -pub fn accumulate( - sfd: &ScaleFactorData, - sfb_cb: &[Vec], - global_gain: u8, -) -> Result { - if sfd.entries.len() != sfb_cb.len() { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - let mut last_sf: i32 = i32::from(global_gain); - let mut last_is: i32 = 0; - let mut last_nrg: i32 = i32::from(global_gain) - NOISE_OFFSET - 256; - let mut noise_pcm_flag = true; - let mut out: Vec> = Vec::with_capacity(sfb_cb.len()); - for (group_entries, group_cb) in sfd.entries.iter().zip(sfb_cb.iter()) { - let mut entry_iter = group_entries.iter(); - let mut group_out: Vec = Vec::new(); - for &cb in group_cb { - if cb == ZERO_HCB { - continue; - } - let entry = entry_iter - .next() - .ok_or(Error::ScaleFactorAccumulatorInvalid)?; - let abs_entry = match (entry, cb) { - (ScaleFactorEntry::Intensity(dpcm), cb) if is_intensity(cb) => { - last_is += i32::from(*dpcm); - AbsoluteScaleFactorEntry::IsPos(last_is as i16) - } - (ScaleFactorEntry::NoisePcm(pcm), cb) if is_noise(cb) => { - if !noise_pcm_flag { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - noise_pcm_flag = false; - last_nrg += i32::from(*pcm); - AbsoluteScaleFactorEntry::NoiseNrg(last_nrg) - } - (ScaleFactorEntry::NoiseDpcm(dpcm), cb) if is_noise(cb) => { - if noise_pcm_flag { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - last_nrg += i32::from(*dpcm); - AbsoluteScaleFactorEntry::NoiseNrg(last_nrg) - } - (ScaleFactorEntry::Dpcm(dpcm), cb) if !is_intensity(cb) && !is_noise(cb) => { - last_sf += i32::from(*dpcm); - if !(0..=255).contains(&last_sf) { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - AbsoluteScaleFactorEntry::Sf(last_sf as u8) - } - _ => return Err(Error::ScaleFactorAccumulatorInvalid), - }; - group_out.push(abs_entry); - } - if entry_iter.next().is_some() { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - out.push(group_out); - } - Ok(AbsoluteScaleFactors { entries: out }) -} - -/// Run the §4.6.2.3.2 / §4.6.8.1.4 / §4.6.13 DPCM accumulators -/// backward (encoder side): convert absolute per-band quantities -/// produced by rate-allocation into the transmitted DPCM record set -/// the bit-exact `scale_factor_data()` writer expects. -/// -/// This is the symmetric inverse of [`accumulate`]: -/// `accumulate(differentiate(abs, sfb_cb, gg)?, sfb_cb, gg) == abs` -/// on every well-formed input. -/// -/// * `abs` — the absolute per-band records from rate-allocation -/// (`Sf` for spectrum bands, `IsPos` for intensity bands, -/// `NoiseNrg` for PNS bands). -/// * `sfb_cb` — per-band codebook map from `section_data()`. -/// * `global_gain` — the 8-bit element the wire stream carries -/// immediately before `section_data()` (a free parameter the -/// encoder picks; conforming choice is the first spectrum band's -/// absolute `sf` to make the first delta `0`). -/// -/// Returns [`Error::ScaleFactorAccumulatorInvalid`] if outer / inner -/// shape disagrees with `sfb_cb`, if an entry variant does not match -/// its band's codebook, if a spectrum / intensity / PNS-subsequent -/// delta `cur - prev` falls outside Table 4.150's `-60..=+60`, or -/// if the first PNS band's initial `dpcm_noise_nrg` magnitude does -/// not fit the 9-bit `uimsbf` Table 4.53 field (`0..=511`). -pub fn differentiate( - abs: &AbsoluteScaleFactors, - sfb_cb: &[Vec], - global_gain: u8, -) -> Result { - if abs.entries.len() != sfb_cb.len() { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - let mut last_sf: i32 = i32::from(global_gain); - let mut last_is: i32 = 0; - let mut last_nrg: i32 = i32::from(global_gain) - NOISE_OFFSET - 256; - let mut noise_pcm_flag = true; - let mut out: Vec> = Vec::with_capacity(sfb_cb.len()); - for (group_abs, group_cb) in abs.entries.iter().zip(sfb_cb.iter()) { - let mut abs_iter = group_abs.iter(); - let mut group_out: Vec = Vec::new(); - for &cb in group_cb { - if cb == ZERO_HCB { - continue; - } - let abs_entry = abs_iter - .next() - .ok_or(Error::ScaleFactorAccumulatorInvalid)?; - let entry = match (abs_entry, cb) { - (AbsoluteScaleFactorEntry::IsPos(cur), cb) if is_intensity(cb) => { - let delta = i32::from(*cur) - last_is; - if !(-60..=60).contains(&delta) { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - last_is = i32::from(*cur); - ScaleFactorEntry::Intensity(delta as i8) - } - (AbsoluteScaleFactorEntry::NoiseNrg(cur), cb) if is_noise(cb) => { - if noise_pcm_flag { - let delta = *cur - last_nrg; - if !(0..=511).contains(&delta) { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - noise_pcm_flag = false; - last_nrg = *cur; - ScaleFactorEntry::NoisePcm(delta as u16) - } else { - let delta = *cur - last_nrg; - if !(-60..=60).contains(&delta) { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - last_nrg = *cur; - ScaleFactorEntry::NoiseDpcm(delta as i8) - } - } - (AbsoluteScaleFactorEntry::Sf(cur), cb) if !is_intensity(cb) && !is_noise(cb) => { - let delta = i32::from(*cur) - last_sf; - if !(-60..=60).contains(&delta) { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - last_sf = i32::from(*cur); - ScaleFactorEntry::Dpcm(delta as i8) - } - _ => return Err(Error::ScaleFactorAccumulatorInvalid), - }; - group_out.push(entry); - } - if abs_iter.next().is_some() { - return Err(Error::ScaleFactorAccumulatorInvalid); - } - out.push(group_out); - } - Ok(ScaleFactorData { entries: out }) -} - -// ============================================================================= -// Error-resilient `scale_factor_data()` — Table 4.53 RVLC branch (§4.6.16.2) -// ============================================================================= -// -// When the GASpecificConfig sets `aacScalefactorDataResilienceFlag == 1`, -// `scale_factor_data()` takes the RVLC branch: the Table 4.A.1 Huffman -// codebook is replaced by the Table 4.166 symmetric RVLC codebook (see -// [`crate::rvlc`]) and three extra wire fields wrap the band loop so a -// decoder can recover from bit errors by decoding backwards: -// -// * `sf_concealment` (1 bit) — concealment hint, decode-irrelevant -// for an error-free stream (§4.6.16.2.2). -// * `rev_global_gain` (8 bits) — the *last* scalefactor, the start -// value for backward DPCM decoding. -// * `length_of_rvlc_sf` (11 bits if `EIGHT_SHORT_SEQUENCE` else 9) — -// the bit length of the RVLC part (the band loop + the optional -// `dpcm_is_last_position`), used to seek to the backward start. -// * `sf_escapes_present` (1 bit) + `length_of_rvlc_escapes` (8 bits) -// — the optional escape sub-stream, present iff any band's RVLC -// delta reached the ESC_FLAG (`±7`). -// * `dpcm_is_last_position` (RVLC, present iff intensity used) — the -// symmetric backward seed for the intensity-position track. -// * `dpcm_noise_last_position` (9 bits, present iff PNS used) — the -// symmetric backward seed for the PNS-energy track. -// -// Forward decoding is the focus here. Per §4.6.2.3.2, "the decoding -// process of the RVLC words is the same as for the Huffman -// codewords" — so once the RVLC deltas are recovered (and folded with -// their escapes), the *same* [`accumulate`] three-track DPCM forward -// pass reconstructs the absolute scalefactors. The `rev_global_gain` -// / `dpcm_*_last_position` seeds and the `length_of_*` fields are the -// backward-recovery scaffolding; this module surfaces them verbatim -// (and validates the two length fields against the bits actually -// consumed, an in-band conformance check) so a future recovery path -// can use them, but forward decode keys off `global_gain` exactly as -// the non-resilient branch does. -// -// Escape folding (§4.6.16.2.1): a base RVLC delta of `+7` means the -// true delta is `+7 + esc`; a base delta of `-7` means `-7 - esc`, -// where `esc` is the Table 4.168 escape magnitude. The escapes are a -// *separate pass* over the same band walk, after the whole RVLC part. - -/// A parsed error-resilient `scale_factor_data()` block — Table 4.53 -/// RVLC branch (`aacScalefactorDataResilienceFlag == 1`). -/// -/// `data` carries the *reconstructed* per-band DPCM records (RVLC base -/// delta with any escape already folded in), so it feeds [`accumulate`] -/// unchanged. The remaining fields are the §4.6.16.2 backward-decoding -/// scaffolding, surfaced verbatim. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ErScaleFactorData { - /// `sf_concealment` (1 bit) — concealment hint; not needed to - /// decode an error-free stream. - pub sf_concealment: bool, - /// `rev_global_gain` (8 bits) — last scalefactor, the backward - /// DPCM start value. - pub rev_global_gain: u8, - /// The reconstructed per-band records (escapes folded in), - /// identical in shape to the non-resilient - /// [`ScaleFactorData`] so [`accumulate`] consumes it directly. - pub data: ScaleFactorData, - /// `dpcm_is_last_position` — backward seed for the intensity - /// track. `Some` iff at least one intensity band was present. - pub dpcm_is_last_position: Option, - /// `dpcm_noise_last_position` (9-bit `uimsbf`) — backward seed - /// for the PNS track. `Some` iff at least one PNS band was - /// present. - pub dpcm_noise_last_position: Option, -} - -/// `length_of_rvlc_sf` field width — 11 bits for -/// `EIGHT_SHORT_SEQUENCE`, 9 bits otherwise (§4.6.16.2.2). -fn length_of_rvlc_sf_bits(window_sequence: WindowSequence) -> u32 { - if window_sequence.is_eight_short() { - 11 - } else { - 9 - } -} - -/// `length_of_rvlc_escapes` field width — always 8 bits -/// (§4.6.16.2.2). -const LENGTH_OF_RVLC_ESCAPES_BITS: u32 = 8; - -/// Fold a Table 4.168 escape magnitude into a base RVLC `±ESC_FLAG` -/// delta (§4.6.16.2.1): a positive base recovers `+7 + esc`, a -/// negative base recovers `-7 - esc`. Both extremes stay within the -/// `-60..=+60` DPCM range, so the result fits `i8`. -fn fold_escape(base: i8, esc_magnitude: u8) -> i8 { - if base >= 0 { - crate::rvlc::RVLC_ESC_FLAG + esc_magnitude as i8 - } else { - -crate::rvlc::RVLC_ESC_FLAG - esc_magnitude as i8 - } -} - -impl ErScaleFactorData { - /// Parse an error-resilient `scale_factor_data()` from `reader` - /// (Table 4.53, RVLC branch). - /// - /// * `reader` — positioned at the first bit of the block (the - /// `sf_concealment` flag). - /// * `sfb_cb` — the per-`(g, sfb)` codebook map from - /// [`section_data`](crate::section_data). - /// * `window_sequence` — selects the `length_of_rvlc_sf` field - /// width (11 vs 9 bits). - /// - /// The two `length_of_*` fields are validated against the bits - /// actually consumed; a mismatch surfaces - /// [`Error::RvlcScaleFactorDataInvalid`] (an in-band conformance - /// check). A forbidden RVLC codeword surfaces - /// [`Error::RvlcForbiddenCodeword`]; reader underflow surfaces - /// [`Error::UnexpectedEnd`]. - pub fn parse( - reader: &mut BitReader<'_>, - sfb_cb: &[Vec], - window_sequence: WindowSequence, - ) -> Result { - let sf_concealment = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)? != 0; - let rev_global_gain = reader.read_u32(8).map_err(|_| Error::UnexpectedEnd)? as u8; - let len_rvlc_sf = u64::from( - reader - .read_u32(length_of_rvlc_sf_bits(window_sequence)) - .map_err(|_| Error::UnexpectedEnd)?, - ); - - // ---- RVLC part (Table 4.53): base deltas + ESC bookkeeping. - let rvlc_start = reader.bit_position(); - let mut intensity_used = false; - let mut noise_used = false; - // base[g] mirrors entries[g]; esc_band flags which records - // need an escape fold in the second pass. - let mut base: Vec> = Vec::with_capacity(sfb_cb.len()); - // `(group, index_within_group)` of each record that is at - // ESC_FLAG and must read an escape (in band-walk order). The - // first-PNS-PCM record is never escaped. - let mut esc_records: Vec<(usize, usize)> = Vec::new(); - for (g, group) in sfb_cb.iter().enumerate() { - let mut group_entries: Vec = Vec::new(); - for &cb in group { - if cb == ZERO_HCB { - continue; - } - let idx_in_group = group_entries.len(); - let entry = if is_intensity(cb) { - intensity_used = true; - let d = crate::rvlc::rvlc_decode(reader)?; - if d.abs() == crate::rvlc::RVLC_ESC_FLAG { - esc_records.push((g, idx_in_group)); - } - ScaleFactorEntry::Intensity(d) - } else if is_noise(cb) { - if !noise_used { - noise_used = true; - let pcm = reader - .read_u32(NOISE_PCM_BITS) - .map_err(|_| Error::UnexpectedEnd)? - as u16; - ScaleFactorEntry::NoisePcm(pcm) - } else { - let d = crate::rvlc::rvlc_decode(reader)?; - if d.abs() == crate::rvlc::RVLC_ESC_FLAG { - esc_records.push((g, idx_in_group)); - } - ScaleFactorEntry::NoiseDpcm(d) - } - } else { - let d = crate::rvlc::rvlc_decode(reader)?; - if d.abs() == crate::rvlc::RVLC_ESC_FLAG { - esc_records.push((g, idx_in_group)); - } - ScaleFactorEntry::Dpcm(d) - }; - group_entries.push(entry); - } - base.push(group_entries); - } - - // `dpcm_is_last_position` (RVLC) closes the RVLC part if any - // intensity band was present. - let mut is_last_base: Option = None; - if intensity_used { - let d = crate::rvlc::rvlc_decode(reader)?; - is_last_base = Some(d); - } - - // The RVLC part length must match the transmitted field. - let rvlc_consumed = reader.bit_position() - rvlc_start; - if rvlc_consumed != len_rvlc_sf { - return Err(Error::RvlcScaleFactorDataInvalid); - } - - // ---- Escape part (Table 4.53): optional second pass. - let sf_escapes_present = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)? != 0; - let mut is_last_esc: Option = None; - if sf_escapes_present { - let len_escapes = u64::from( - reader - .read_u32(LENGTH_OF_RVLC_ESCAPES_BITS) - .map_err(|_| Error::UnexpectedEnd)?, - ); - let esc_start = reader.bit_position(); - // Read one escape per recorded ESC_FLAG band, in walk order, - // and fold it into the base delta. - for &(g, i) in &esc_records { - let mag = crate::rvlc::rvlc_esc_decode(reader)?; - let folded = match base[g][i] { - ScaleFactorEntry::Dpcm(d) => ScaleFactorEntry::Dpcm(fold_escape(d, mag)), - ScaleFactorEntry::Intensity(d) => { - ScaleFactorEntry::Intensity(fold_escape(d, mag)) - } - ScaleFactorEntry::NoiseDpcm(d) => { - ScaleFactorEntry::NoiseDpcm(fold_escape(d, mag)) - } - // A NoisePcm record is never recorded as an escape. - ScaleFactorEntry::NoisePcm(_) => { - return Err(Error::RvlcScaleFactorDataInvalid); - } - }; - base[g][i] = folded; - } - // `dpcm_is_last_position` escape closes the escape part. - if let Some(d) = is_last_base { - if d.abs() == crate::rvlc::RVLC_ESC_FLAG { - let mag = crate::rvlc::rvlc_esc_decode(reader)?; - is_last_esc = Some(mag); - } - } - let esc_consumed = reader.bit_position() - esc_start; - if esc_consumed != len_escapes { - return Err(Error::RvlcScaleFactorDataInvalid); - } - } - - // ---- PNS backward seed. - // - // Table 4.53 resets `noise_used = 0` immediately before - // `sf_escapes_present` and re-derives it inside the escape - // loop's `if (!noise_used)` arm. The terminal - // `if (noise_used) dpcm_noise_last_position` therefore fires - // *only* when both a PNS band is present **and** - // `sf_escapes_present == 1` (the escape loop — and its - // `noise_used = 1` — is wholly inside `if (sf_escapes_present)`). - // A PNS frame with no escapes carries no `dpcm_noise_last_position`. - let dpcm_noise_last_position = if noise_used && sf_escapes_present { - Some( - reader - .read_u32(NOISE_PCM_BITS) - .map_err(|_| Error::UnexpectedEnd)? as u16, - ) - } else { - None - }; - - // Fold the intensity-last backward seed (with its escape). - let dpcm_is_last_position = is_last_base.map(|d| { - let folded = match is_last_esc { - Some(mag) => fold_escape(d, mag), - None => d, - }; - i16::from(folded) - }); - - Ok(ErScaleFactorData { - sf_concealment, - rev_global_gain, - data: ScaleFactorData { entries: base }, - dpcm_is_last_position, - dpcm_noise_last_position, - }) - } - - /// Encode an error-resilient `scale_factor_data()` onto `writer`, - /// the inverse of [`ErScaleFactorData::parse`]. - /// - /// The records in `self.data` carry the *final* DPCM deltas - /// (escapes already folded). The writer re-splits each delta whose - /// magnitude exceeds `±6` into a base `±7` RVLC codeword plus a - /// Table 4.168 escape magnitude, regenerates the `length_of_*` - /// fields from the bits emitted, and sets `sf_escapes_present` - /// when any escape is needed. - /// - /// Returns [`Error::RvlcScaleFactorDataInvalid`] on a structural - /// mismatch (record/codebook shape, escape magnitude out of the - /// Table 4.168 domain, or a backward-seed field overflow). - pub fn write( - &self, - writer: &mut BitWriter, - sfb_cb: &[Vec], - window_sequence: WindowSequence, - ) -> Result<()> { - if self.data.entries.len() != sfb_cb.len() { - return Err(Error::RvlcScaleFactorDataInvalid); - } - - writer.write_u32(u32::from(self.sf_concealment), 1); - writer.write_u32(u32::from(self.rev_global_gain), 8); - - // Build the RVLC part into a scratch writer first so its bit - // length is known for `length_of_rvlc_sf`. Collect the escape - // magnitudes (in walk order) for the second pass. - let mut rvlc_part = BitWriter::new(); - let mut escapes: Vec = Vec::new(); - let mut intensity_used = false; - let mut noise_used = false; - - for (group_entries, group_cb) in self.data.entries.iter().zip(sfb_cb.iter()) { - let mut entry_iter = group_entries.iter(); - for &cb in group_cb { - if cb == ZERO_HCB { - continue; - } - let entry = entry_iter.next().ok_or(Error::RvlcScaleFactorDataInvalid)?; - match (entry, cb) { - (ScaleFactorEntry::Intensity(d), cb) if is_intensity(cb) => { - intensity_used = true; - write_rvlc_delta(&mut rvlc_part, *d, &mut escapes)?; - } - (ScaleFactorEntry::NoisePcm(pcm), cb) if is_noise(cb) => { - if noise_used { - return Err(Error::RvlcScaleFactorDataInvalid); - } - if u32::from(*pcm) >= (1u32 << NOISE_PCM_BITS) { - return Err(Error::RvlcScaleFactorDataInvalid); - } - noise_used = true; - rvlc_part.write_u32(u32::from(*pcm), NOISE_PCM_BITS); - } - (ScaleFactorEntry::NoiseDpcm(d), cb) if is_noise(cb) => { - if !noise_used { - return Err(Error::RvlcScaleFactorDataInvalid); - } - write_rvlc_delta(&mut rvlc_part, *d, &mut escapes)?; - } - (ScaleFactorEntry::Dpcm(d), cb) if !is_intensity(cb) && !is_noise(cb) => { - write_rvlc_delta(&mut rvlc_part, *d, &mut escapes)?; - } - _ => return Err(Error::RvlcScaleFactorDataInvalid), - } - } - if entry_iter.next().is_some() { - return Err(Error::RvlcScaleFactorDataInvalid); - } - } - - // `dpcm_is_last_position` (RVLC) closes the RVLC part. - let mut is_last_escape: Option = None; - match (intensity_used, self.dpcm_is_last_position) { - (true, Some(d)) => { - let d8 = i8::try_from(d).map_err(|_| Error::RvlcScaleFactorDataInvalid)?; - let mut tail: Vec = Vec::new(); - write_rvlc_delta(&mut rvlc_part, d8, &mut tail)?; - is_last_escape = tail.into_iter().next(); - } - (true, None) | (false, Some(_)) => { - // Intensity presence must agree with the seed presence. - return Err(Error::RvlcScaleFactorDataInvalid); - } - (false, None) => {} - } - - let len_rvlc_sf = rvlc_part.bit_position(); - let field_bits = length_of_rvlc_sf_bits(window_sequence); - if len_rvlc_sf >= (1u64 << field_bits) { - return Err(Error::RvlcScaleFactorDataInvalid); - } - writer.write_u32(len_rvlc_sf as u32, field_bits); - append_bits(writer, len_rvlc_sf, &rvlc_part.finish()); - - // ---- Escape part. - let any_escape = !escapes.is_empty() || is_last_escape.is_some(); - writer.write_u32(u32::from(any_escape), 1); - if any_escape { - let mut esc_part = BitWriter::new(); - for &mag in &escapes { - let (len, cw) = crate::rvlc::rvlc_esc_encode(mag)?; - esc_part.write_u32(cw, u32::from(len)); - } - if let Some(mag) = is_last_escape { - let (len, cw) = crate::rvlc::rvlc_esc_encode(mag)?; - esc_part.write_u32(cw, u32::from(len)); - } - let len_escapes = esc_part.bit_position(); - if len_escapes >= (1u64 << LENGTH_OF_RVLC_ESCAPES_BITS) { - return Err(Error::RvlcScaleFactorDataInvalid); - } - writer.write_u32(len_escapes as u32, LENGTH_OF_RVLC_ESCAPES_BITS); - append_bits(writer, len_escapes, &esc_part.finish()); - } - - // ---- PNS backward seed. - // - // Per Table 4.53 the terminal `dpcm_noise_last_position` is - // present only when a PNS band exists **and** - // `sf_escapes_present == 1` (the spec re-derives `noise_used` - // inside the escape loop, which only runs when escapes are - // present). So the seed must be `Some` exactly when - // `noise_used && any_escape`, and `None` otherwise — any other - // combination cannot be represented on the wire. - let expect_noise_seed = noise_used && any_escape; - match (expect_noise_seed, self.dpcm_noise_last_position) { - (true, Some(pcm)) => { - if u32::from(pcm) >= (1u32 << NOISE_PCM_BITS) { - return Err(Error::RvlcScaleFactorDataInvalid); - } - writer.write_u32(u32::from(pcm), NOISE_PCM_BITS); - } - (false, None) => {} - _ => return Err(Error::RvlcScaleFactorDataInvalid), - } - Ok(()) - } -} - -/// Split a final DPCM delta into an RVLC base codeword plus (if the -/// magnitude exceeds `±6`) an escape magnitude pushed onto `escapes`. -/// The base RVLC codeword is emitted onto `part`. -fn write_rvlc_delta(part: &mut BitWriter, delta: i8, escapes: &mut Vec) -> Result<()> { - let flag = crate::rvlc::RVLC_ESC_FLAG; // 7 - if delta.abs() < flag { - // Fits the RVLC codebook directly (no escape, magnitude ≤ 6). - let (len, cw) = crate::rvlc::rvlc_encode(delta)?; - part.write_u32(cw, u32::from(len)); - } else { - // Magnitude ≥ 7: base codeword is the signed ESC_FLAG, the - // remainder is the escape magnitude. - let (base, mag) = if delta >= 0 { - (flag, (delta - flag) as u8) - } else { - (-flag, (-delta - flag) as u8) - }; - let (len, cw) = crate::rvlc::rvlc_encode(base)?; - part.write_u32(cw, u32::from(len)); - if mag as usize >= crate::rvlc::RVLC_ESC_NUM_ENTRIES { - return Err(Error::RvlcScaleFactorDataInvalid); - } - escapes.push(mag); - } - Ok(()) -} - -/// Append the first `total` bits of `bytes` (a zero-padded -/// `BitWriter::finish()` output) to `dst`, MSB-first, preserving bit -/// position. The trailing zero pad bits past `total` are ignored. -fn append_bits(dst: &mut BitWriter, total: u64, bytes: &[u8]) { - let mut remaining = total; - let mut byte_idx = 0usize; - while remaining >= 8 { - dst.write_u32(u32::from(bytes[byte_idx]), 8); - byte_idx += 1; - remaining -= 8; - } - if remaining > 0 { - // The final partial byte is MSB-aligned in the finished buffer. - let last = bytes[byte_idx]; - let value = u32::from(last) >> (8 - remaining); - dst.write_u32(value, remaining as u32); - } -} - -/// Internal: `cb` is an intensity codebook (14 or 15). -fn is_intensity(cb: u8) -> bool { - cb == INTENSITY_HCB || cb == INTENSITY_HCB2 -} - -/// Internal: `cb` is the PNS codebook (13). -fn is_noise(cb: u8) -> bool { - cb == NOISE_HCB -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Spot-check a handful of Table 4.A.1 rows the way the spec - /// presents them: `index 60 → 1 bit, codeword 0`; `index 59 → - /// 3 bits, codeword 4`; `index 61 → 4 bits, codeword 0xa`. - #[test] - fn hcod_sf_table_known_rows() { - assert_eq!(HCOD_SF[60], (1, 0x0)); - assert_eq!(HCOD_SF[59], (3, 0x4)); - assert_eq!(HCOD_SF[61], (4, 0xa)); - assert_eq!(HCOD_SF[0], (18, 0x3ffe8)); - assert_eq!(HCOD_SF[120], (19, 0x7fff3)); - } - - /// The codebook is prefix-free (no codeword is a prefix of any - /// other). Verified once here as a regression guard against typos - /// in the Table 4.A.1 transcription above. - #[test] - fn hcod_sf_table_is_prefix_free() { - for (i, &(li, vi)) in HCOD_SF.iter().enumerate() { - for (j, &(lj, vj)) in HCOD_SF.iter().enumerate() { - if i == j || lj < li { - continue; - } - let lo = u32::from(lj - li); - let prefix = vj >> lo; - assert_ne!( - prefix, vi, - "entry {} (L={}, v={:x}) is prefix of entry {} (L={}, v={:x})", - i, li, vi, j, lj, vj - ); - } - } - } - - /// `index_offset = -60`: encoding `dpcm = 0` selects index 60, - /// the single-bit `0` codeword. - #[test] - fn encode_dpcm_zero_is_single_bit() { - let (len, cw) = hcod_sf_encode(0).unwrap(); - assert_eq!(len, 1); - assert_eq!(cw, 0); - } - - /// Boundary values: `-60` and `+60` are the endpoints of the - /// DPCM range; anything outside is rejected. - #[test] - fn encode_dpcm_boundaries() { - assert!(hcod_sf_encode(-60).is_ok()); - assert!(hcod_sf_encode(60).is_ok()); - assert_eq!( - hcod_sf_encode(-61), - Err(Error::ScaleFactorDataEncodeInvalid) - ); - assert_eq!(hcod_sf_encode(61), Err(Error::ScaleFactorDataEncodeInvalid)); - } - - /// Every entry of the table round-trips: encode then decode - /// recovers the original DPCM value. - #[test] - fn hcod_sf_roundtrip_every_entry() { - for dpcm in -60i8..=60 { - let (len, cw) = hcod_sf_encode(dpcm).unwrap(); - let mut bw = BitWriter::new(); - bw.write_u32(cw, u32::from(len)); - let bits_written = bw.bit_position(); - let buf = bw.finish(); - let mut br = BitReader::new(&buf); - let recovered = hcod_sf_decode(&mut br).unwrap(); - assert_eq!(recovered, dpcm); - // Reader must consume exactly `len` bits. - assert_eq!(br.bit_position(), bits_written); - } - } - - // ------------------------------------------------------------------------- - // Error-resilient (RVLC) `scale_factor_data()` — Table 4.53 / §4.6.16.2 - // ------------------------------------------------------------------------- - - /// Round-trip an ER block (no intensity / no PNS, all spectrum - /// bands within the RVLC ±6 range — no escapes) and confirm the - /// writer regenerates exactly what the parser read back. - #[test] - fn er_roundtrip_spectrum_only_no_escapes() { - // Two groups, codebook 2 (spectrum) on every band. - let sfb_cb = vec![vec![2u8, 2, 2], vec![2u8, 2]]; - let block = ErScaleFactorData { - sf_concealment: true, - rev_global_gain: 137, - data: ScaleFactorData { - entries: vec![ - vec![ - ScaleFactorEntry::Dpcm(0), - ScaleFactorEntry::Dpcm(3), - ScaleFactorEntry::Dpcm(-5), - ], - vec![ScaleFactorEntry::Dpcm(6), ScaleFactorEntry::Dpcm(-6)], - ], - }, - dpcm_is_last_position: None, - dpcm_noise_last_position: None, - }; - let mut w = BitWriter::new(); - block - .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) - .unwrap(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong).unwrap(); - assert_eq!(parsed, block); - } - - /// A delta whose magnitude exceeds ±6 must round-trip through the - /// base-`±7` + escape split (§4.6.16.2.1) and set - /// `sf_escapes_present`. - #[test] - fn er_roundtrip_with_escapes() { - let sfb_cb = vec![vec![3u8, 3, 3]]; - let block = ErScaleFactorData { - sf_concealment: false, - rev_global_gain: 200, - data: ScaleFactorData { - entries: vec![vec![ - ScaleFactorEntry::Dpcm(7), // +7 + 0 escape - ScaleFactorEntry::Dpcm(-20), // -7 - 13 escape - ScaleFactorEntry::Dpcm(60), // +7 + 53 escape (max) - ]], - }, - dpcm_is_last_position: None, - dpcm_noise_last_position: None, - }; - let mut w = BitWriter::new(); - block - .write(&mut w, &sfb_cb, WindowSequence::EightShort) - .unwrap(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::EightShort).unwrap(); - assert_eq!(parsed, block); - } - - /// An ER block with both intensity and PNS bands round-trips, - /// exercising `dpcm_is_last_position`, the first-PNS 9-bit PCM - /// seed, a subsequent PNS RVLC delta, and `dpcm_noise_last_position`. - /// The Table 4.53 terminal `dpcm_noise_last_position` is present - /// only when `sf_escapes_present == 1`, so this block carries an - /// escape (`NoiseDpcm(10)` → base +7 + magnitude 3). - #[test] - fn er_roundtrip_intensity_and_pns() { - // band codebooks: spectrum(2), intensity(15), pns(13), pns(13). - let sfb_cb = vec![vec![2u8, INTENSITY_HCB2, NOISE_HCB, NOISE_HCB]]; - let block = ErScaleFactorData { - sf_concealment: true, - rev_global_gain: 100, - data: ScaleFactorData { - entries: vec![vec![ - ScaleFactorEntry::Dpcm(2), - ScaleFactorEntry::Intensity(-3), - ScaleFactorEntry::NoisePcm(0x1a5), // 9-bit PCM seed - ScaleFactorEntry::NoiseDpcm(10), // escape → sf_escapes_present - ]], - }, - dpcm_is_last_position: Some(5), - dpcm_noise_last_position: Some(0x0c2), - }; - let mut w = BitWriter::new(); - block - .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) - .unwrap(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong).unwrap(); - assert_eq!(parsed, block); - } - - /// A PNS frame whose deltas all fit the RVLC ±6 range emits no - /// escapes (`sf_escapes_present == 0`), so per Table 4.53 the - /// terminal `dpcm_noise_last_position` is **absent** — the parser - /// recovers `None` for it. The writer rejects a `Some` seed in - /// that escapeless case as unrepresentable. - #[test] - fn er_pns_without_escapes_has_no_noise_seed() { - let sfb_cb = vec![vec![NOISE_HCB, NOISE_HCB]]; - let block = ErScaleFactorData { - sf_concealment: false, - rev_global_gain: 80, - data: ScaleFactorData { - entries: vec![vec![ - ScaleFactorEntry::NoisePcm(0x010), - ScaleFactorEntry::NoiseDpcm(3), // within ±6 → no escape - ]], - }, - dpcm_is_last_position: None, - dpcm_noise_last_position: None, - }; - let mut w = BitWriter::new(); - block - .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) - .unwrap(); - let bytes = w.finish(); - let mut r = BitReader::new(&bytes); - let parsed = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong).unwrap(); - assert_eq!(parsed, block); - assert_eq!(parsed.dpcm_noise_last_position, None); - - // A Some seed in the escapeless case is unrepresentable. - let bad = ErScaleFactorData { - dpcm_noise_last_position: Some(0x0aa), - ..block - }; - let mut bw = BitWriter::new(); - assert!(matches!( - bad.write(&mut bw, &sfb_cb, WindowSequence::OnlyLong), - Err(Error::RvlcScaleFactorDataInvalid) - )); - } - - /// The headline §4.6.2.3.2 equivalence: an RVLC-coded scalefactor - /// stream and the Huffman-coded stream carrying the *same* DPCM - /// deltas accumulate to identical absolute scalefactors. This is - /// what "the decoding process of the RVLC words is the same as - /// for the Huffman codewords" means in practice. - #[test] - fn er_forward_decode_matches_huffman_path() { - let sfb_cb = vec![vec![2u8, 2, 2, 2]]; - let global_gain = 120u8; - // Identical DPCM records for both paths. - let entries = vec![vec![ - ScaleFactorEntry::Dpcm(0), - ScaleFactorEntry::Dpcm(5), - ScaleFactorEntry::Dpcm(-30), // forces an escape on the RVLC side - ScaleFactorEntry::Dpcm(2), - ]]; - let sfd = ScaleFactorData { - entries: entries.clone(), - }; - - // Huffman path: write + parse + accumulate. - let mut hw = BitWriter::new(); - sfd.write(&mut hw, &sfb_cb).unwrap(); - let hbytes = hw.finish(); - let mut hr = BitReader::new(&hbytes); - let hsfd = ScaleFactorData::parse(&mut hr, &sfb_cb).unwrap(); - let habs = accumulate(&hsfd, &sfb_cb, global_gain).unwrap(); - - // RVLC path: write + parse the ER block, then accumulate the - // reconstructed records with the SAME global_gain. - let er = ErScaleFactorData { - sf_concealment: false, - rev_global_gain: 0, - data: ScaleFactorData { entries }, - dpcm_is_last_position: None, - dpcm_noise_last_position: None, - }; - let mut ew = BitWriter::new(); - er.write(&mut ew, &sfb_cb, WindowSequence::OnlyLong) - .unwrap(); - let ebytes = ew.finish(); - let mut er_reader = BitReader::new(&ebytes); - let parsed_er = - ErScaleFactorData::parse(&mut er_reader, &sfb_cb, WindowSequence::OnlyLong).unwrap(); - let eabs = accumulate(&parsed_er.data, &sfb_cb, global_gain).unwrap(); - - assert_eq!(habs, eabs, "RVLC forward decode must equal Huffman path"); - } - - /// A corrupted RVLC bit pattern that lands on a Table 4.167 - /// forbidden codeword surfaces the in-band error-detection event. - #[test] - fn er_forbidden_codeword_is_detected() { - // Forbidden codeword (6 bits, 0b110010) followed by padding. - let mut w = BitWriter::new(); - w.write_u32(0, 1); // sf_concealment - w.write_u32(0, 8); // rev_global_gain - w.write_u32(6, 9); // length_of_rvlc_sf == 6 bits - w.write_u32(0b110010, 6); // the forbidden codeword - let bytes = w.finish(); - let sfb_cb = vec![vec![2u8]]; - let mut r = BitReader::new(&bytes); - assert!(matches!( - ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong), - Err(Error::RvlcForbiddenCodeword) - )); - } - - /// A `length_of_rvlc_sf` that disagrees with the bits actually - /// consumed is an in-band conformance failure. - #[test] - fn er_length_mismatch_rejected() { - // Build a valid block then corrupt the length field. - let sfb_cb = vec![vec![2u8, 2]]; - let block = ErScaleFactorData { - sf_concealment: false, - rev_global_gain: 50, - data: ScaleFactorData { - entries: vec![vec![ScaleFactorEntry::Dpcm(1), ScaleFactorEntry::Dpcm(-1)]], - }, - dpcm_is_last_position: None, - dpcm_noise_last_position: None, - }; - let mut w = BitWriter::new(); - block - .write(&mut w, &sfb_cb, WindowSequence::OnlyLong) - .unwrap(); - let mut bytes = w.finish(); - // The length_of_rvlc_sf field sits at bit offset 9 (after the - // 1-bit sf_concealment + 8-bit rev_global_gain), 9 bits wide. - // Flip its low bit to desync the consumed-bit check. - // bits 9..18 → spans bytes 1 (bits 1..8) and 2 (bits 0..1). - bytes[2] ^= 0x40; // flip a bit inside the length field region - let mut r = BitReader::new(&bytes); - // Either a length mismatch or a forbidden codeword — both are - // valid in-band rejections of the corrupted stream. - let res = ErScaleFactorData::parse(&mut r, &sfb_cb, WindowSequence::OnlyLong); - assert!(res.is_err(), "corrupted length field must be rejected"); - } -} diff --git a/crates/vendor/oxideav-aac/src/section_data.rs b/crates/vendor/oxideav-aac/src/section_data.rs deleted file mode 100644 index 51b97cbf..00000000 --- a/crates/vendor/oxideav-aac/src/section_data.rs +++ /dev/null @@ -1,631 +0,0 @@ -//! `section_data()` parser — ISO/IEC 14496-3 §4.4.6 / ISO/IEC -//! 13818-7 §6.3 Table 17. -//! -//! `section_data()` is the second tool inside -//! `individual_channel_stream()` (after `global_gain` and -//! `ics_info()`, before `scale_factor_data()`). It assigns one -//! Huffman codebook (`sect_cb`) to each *run* of scalefactor bands -//! (a "section") within each window group, using run-length coding -//! with an escape mechanism for sections longer than the field can -//! hold in one increment. -//! -//! This parser depends only on values already produced by -//! [`crate::ics_info::IcsInfo`]: -//! -//! * `num_window_groups` — the outer loop bound. -//! * `max_sfb` — the inner loop terminator (`while (k < max_sfb)`). -//! * `window_sequence == EIGHT_SHORT_SEQUENCE` — selects the -//! 3-bit (`sect_esc_val = 7`) versus 5-bit (`sect_esc_val = 31`) -//! `sect_len_incr` field width. -//! -//! Crucially it carries **no Huffman codebook of its own**: every -//! field is fixed-width (`sect_cb` is 4 bits, `sect_len_incr` is -//! 3 or 5 bits), so the parser is a pure bit-walker. The Huffman -//! codebooks the `sect_cb` values *select* (the spectrum books 1-11 -//! plus the scalefactor book) are consumed by later tools -//! (`scale_factor_data()`, `spectral_data()`), not here. -//! -//! ## Run-length escape coding (Table 17) -//! -//! For each window group `g`, starting at scalefactor band `k = 0`: -//! -//! 1. Read `sect_cb[g][i]` (4 bits). -//! 2. Set `sect_len = 0`. Read `sect_len_incr` (3 or 5 bits). -//! While the value read equals `sect_esc_val`, add `sect_esc_val` -//! to `sect_len` and read the next `sect_len_incr`. When a -//! non-escape value is read, add it to `sect_len` and stop. -//! 3. The section covers bands `[k, k + sect_len)`. Record -//! `sect_start[g][i] = k`, `sect_end[g][i] = k + sect_len`, and -//! `sfb_cb[g][sfb] = sect_cb[g][i]` for every band in the run. -//! 4. Advance `k += sect_len`, `i += 1`. Repeat while `k < max_sfb`. -//! -//! `num_sec[g]` is the final value of `i` for the group. -//! -//! ## What is *not* in this round -//! -//! * No Huffman decode. The codebook indices are surfaced verbatim; -//! the spectrum / scalefactor decoders consume them later. -//! * No `is_intensity()` / PNS classification. The -//! [`Codebook`] enum exposes the semantic role of each value -//! (`Intensity`, `IntensityInPhase`, `Noise`, `Esc`, …) for the -//! benefit of `scale_factor_data()` / `spectral_data()`, but -//! `section_data()` itself only records the raw `u8`. -//! * No validation that `sfb_cb` is fully populated to `max_sfb` in -//! pathological streams — the parser surfaces a -//! [`Error::SectionDataOverrun`] when a section would extend past -//! `max_sfb` (which a conforming encoder never emits) and -//! otherwise trusts the run lengths. -//! -//! ## Encode side (Phase 2: first writer primitive) -//! -//! [`SectionData::write`] is the inverse of [`SectionData::parse`]: -//! given the same `window_sequence` / `num_window_groups` / `max_sfb` -//! context the parser was invoked with, it emits the bit-exact -//! Table 17 syntax that the parser reads back. This is the AAC -//! crate's first encoder primitive — a bounded syntax-element -//! writer with no Huffman tables of its own, so the surface lives -//! entirely in the fixed-width `sect_cb` / `sect_len_incr` field -//! pair. -//! -//! The encode-side rule for the §6.3 escape is the inverse of the -//! decode-side accumulation: -//! -//! 1. While the remaining `sect_len` is **greater than or equal to** -//! `sect_esc_val`, emit a `sect_len_incr` of `sect_esc_val` and -//! subtract `sect_esc_val` from the remaining length. The -//! "greater than or equal to" boundary is what forces a trailing -//! non-escape `sect_len_incr == 0` after a length that lands -//! exactly on a multiple of `sect_esc_val` — the parser loop -//! keeps reading while `incr == sect_esc_val`, so the writer -//! must terminate the run with a non-escape value (which can be -//! zero) so the parser sees a `break` condition. -//! 2. Emit the residual `sect_len` (which is now strictly less than -//! `sect_esc_val`) as a single non-escape `sect_len_incr`. -//! -//! [`SectionData::write`] validates that the supplied sections form -//! a contiguous run `0 → max_sfb` per group and that every -//! `sect_cb` and `sect_len` fits the wire field; encoder bugs upstream -//! that violate either invariant surface as -//! [`Error::SectionDataEncodeInvalid`]. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::ics_info::WindowSequence; -use crate::{Error, Result}; - -/// `ZERO_HCB` — section carries neither scalefactor nor spectral -/// data; the band is silent. ISO/IEC 13818-7 §9.2.2 / §11.3.2. -pub const ZERO_HCB: u8 = 0; - -/// `FIRST_PAIR_HCB` — the first codebook whose dimension is 2 -/// (a 2-tuple); books `< FIRST_PAIR_HCB` are 4-tuple (QUAD) books. -/// ISO/IEC 13818-7 §9.2.2. -pub const FIRST_PAIR_HCB: u8 = 5; - -/// `ESC_HCB` — the spectrum escape codebook (book 11). Values whose -/// magnitude reaches the LAV use the §9.3 escape sequence for the -/// actual coefficient. ISO/IEC 13818-7 §9.2.2. -pub const ESC_HCB: u8 = 11; - -/// `NOISE_HCB` — Perceptual Noise Substitution codebook (value 13). -/// An MPEG-4 extension (ISO/IEC 14496-3; the base ISO/IEC 13818-7 -/// Table 59 marks value 13 *reserved* and adds PNS in its Annex B -/// Table B.1 extended `scale_factor_data()`). When a band's -/// `sfb_cb == NOISE_HCB` the band is noise-filled and its -/// "scalefactor" position carries the PNS energy delta instead. -pub const NOISE_HCB: u8 = 13; - -/// `INTENSITY_HCB2` — out-of-phase intensity-stereo codebook -/// (value 14). ISO/IEC 13818-7 §9.2.2 / Table 59. -pub const INTENSITY_HCB2: u8 = 14; - -/// `INTENSITY_HCB` — in-phase intensity-stereo codebook (value 15). -/// ISO/IEC 13818-7 §9.2.2 / Table 59. -pub const INTENSITY_HCB: u8 = 15; - -/// Semantic classification of a 4-bit `sect_cb` value, per ISO/IEC -/// 13818-7 Table 59 (extended by the MPEG-4 PNS codebook 13). -/// -/// `section_data()` records the raw `u8` in [`Section::codebook`]; -/// this enum is a *view* over that value so downstream tools -/// (`scale_factor_data()` for the `is_intensity` / PNS branch, -/// `spectral_data()` for the dimension / signed / escape branch) -/// can dispatch without re-deriving the classification. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Codebook { - /// `0` — `ZERO_HCB`: silent band, no scalefactor, no spectrum. - Zero, - /// `1..=4` — 4-tuple (QUAD) spectrum book. `signed` is `false` - /// for books 1-2 (`unsigned_cb == 0`) and `true` for 3-4. - Quad { - /// Codebook number (1..=4). - number: u8, - /// `true` ⇔ the book is *unsigned* (`unsigned_cb[i] == 1`). - unsigned: bool, - }, - /// `5..=10` — 2-tuple (PAIR) spectrum book. - Pair { - /// Codebook number (5..=10). - number: u8, - /// `true` ⇔ the book is *unsigned* (`unsigned_cb[i] == 1`). - unsigned: bool, - }, - /// `11` — `ESC_HCB`: 2-tuple unsigned escape book. - Esc, - /// `12` — reserved (ISO/IEC 13818-7 Table 59). - Reserved12, - /// `13` — `NOISE_HCB`: Perceptual Noise Substitution (MPEG-4). - Noise, - /// `14` — `INTENSITY_HCB2`: out-of-phase intensity stereo. - IntensityOutOfPhase, - /// `15` — `INTENSITY_HCB`: in-phase intensity stereo. - IntensityInPhase, -} - -impl Codebook { - /// Classify a raw 4-bit `sect_cb` value (0..=15). - /// - /// `unsigned_cb[]` per ISO/IEC 13818-7 Table 59: books 1, 2 are - /// signed (`unsigned == false`); books 3, 4, 5*, 6*, 7, 8, 9, - /// 10, 11 are unsigned. (*Books 5 and 6 are 2-tuple signed in - /// Table 59 — see the per-number mapping below.) - pub fn from_value(value: u8) -> Self { - match value & 0x0f { - 0 => Codebook::Zero, - // QUAD books (dimension 4): 1, 2 signed; 3, 4 unsigned. - n @ 1..=4 => Codebook::Quad { - number: n, - unsigned: matches!(n, 3 | 4), - }, - // PAIR books (dimension 2): 5, 6 signed; 7, 8, 9, 10 - // unsigned. - n @ 5..=10 => Codebook::Pair { - number: n, - unsigned: matches!(n, 7..=10), - }, - 11 => Codebook::Esc, - 12 => Codebook::Reserved12, - 13 => Codebook::Noise, - 14 => Codebook::IntensityOutOfPhase, - 15 => Codebook::IntensityInPhase, - _ => unreachable!("masked to 0..=15"), - } - } - - /// `true` ⇔ this codebook is an intensity-stereo book - /// (`INTENSITY_HCB` or `INTENSITY_HCB2`). Mirrors the spec - /// `is_intensity()` helper used by `scale_factor_data()`. - pub fn is_intensity(self) -> bool { - matches!( - self, - Codebook::IntensityInPhase | Codebook::IntensityOutOfPhase - ) - } - - /// `true` ⇔ this is the PNS noise codebook (`NOISE_HCB`). - pub fn is_noise(self) -> bool { - matches!(self, Codebook::Noise) - } - - /// `true` ⇔ this is `ZERO_HCB` (band carries no data). - pub fn is_zero(self) -> bool { - matches!(self, Codebook::Zero) - } -} - -/// One contiguous run of scalefactor bands sharing a codebook, as -/// produced by Table 17. `start`/`end` are scalefactor-band indices -/// (`end` is one past the last band, matching `sect_end`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Section { - /// `sect_cb[g][i]` — the raw 4-bit codebook value for this run. - pub codebook: u8, - /// `sect_start[g][i]` — first scalefactor band in the section. - pub start: u8, - /// `sect_end[g][i]` — one past the last band (`start + - /// sect_len`). - pub end: u8, -} - -impl Section { - /// Length of the section in scalefactor bands (`sect_len`). - pub fn len(self) -> u8 { - self.end - self.start - } - - /// `true` ⇔ the section spans zero bands. A conforming encoder - /// never emits a zero-length section, but the accessor is - /// provided so the `clippy::len_without_is_empty` lint is - /// satisfied and callers can defensively check. - pub fn is_empty(self) -> bool { - self.end == self.start - } - - /// Semantic [`Codebook`] classification of [`Self::codebook`]. - pub fn codebook_kind(self) -> Codebook { - Codebook::from_value(self.codebook) - } -} - -/// Parsed `section_data()` for one `individual_channel_stream()`. -/// -/// The per-group section lists plus the flattened `sfb_cb[g][sfb]` -/// map are surfaced; `scale_factor_data()` (next round) consumes -/// `sfb_cb` to decide which bands carry a transmitted scalefactor. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SectionData { - /// `sect[g]` — the ordered sections of window group `g`. The - /// outer index runs `0..num_window_groups`; `sect[g].len()` is - /// `num_sec[g]`. - pub sections: Vec>, - /// `sfb_cb[g][sfb]` — the codebook assigned to scalefactor band - /// `sfb` of group `g`, for `sfb in 0..max_sfb`. Flattened per - /// group; the outer index runs `0..num_window_groups`. - pub sfb_cb: Vec>, -} - -impl SectionData { - /// Parse a `section_data()` from the bit-reader. - /// - /// * `reader` — positioned immediately after `ics_info()` (well, - /// after `global_gain` + `ics_info()` in the full ICS, but - /// `section_data()` starts right where the caller leaves the - /// reader). - /// * `window_sequence` — from the surrounding `ics_info()`; - /// selects the 3-bit vs 5-bit `sect_len_incr` field. - /// * `num_window_groups` — from the surrounding `ics_info()` - /// derivations (`1` for long sequences). - /// * `max_sfb` — from the surrounding `ics_info()`. - /// - /// Returns [`Error::SectionDataOverrun`] if a section run would - /// extend past `max_sfb` (non-conforming stream), and - /// [`Error::UnexpectedEnd`] on bit-reader underflow. - pub fn parse( - reader: &mut BitReader<'_>, - window_sequence: WindowSequence, - num_window_groups: u8, - max_sfb: u8, - ) -> Result { - // Table 17: sect_esc_val and sect_len_incr field width. - let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { - ((1u32 << 3) - 1, 3u32) // 7, 3-bit field - } else { - ((1u32 << 5) - 1, 5u32) // 31, 5-bit field - }; - - let mut sections: Vec> = Vec::with_capacity(num_window_groups as usize); - let mut sfb_cb: Vec> = Vec::with_capacity(num_window_groups as usize); - - for _g in 0..num_window_groups { - let mut group_sections: Vec
= Vec::new(); - let mut group_sfb_cb: Vec = vec![ZERO_HCB; max_sfb as usize]; - - let mut k: u32 = 0; - let max = max_sfb as u32; - while k < max { - let sect_cb = read_u8(reader, 4)?; - - // sect_len accumulation with escape coding. - let mut sect_len: u32 = 0; - loop { - let incr = reader - .read_u32(len_bits) - .map_err(|_| Error::UnexpectedEnd)?; - if incr == sect_esc_val { - sect_len += sect_esc_val; - // Re-read another sect_len_incr. - continue; - } - sect_len += incr; - break; - } - - let start = k; - let end = k + sect_len; - if end > max { - return Err(Error::SectionDataOverrun); - } - for sfb in start..end { - group_sfb_cb[sfb as usize] = sect_cb; - } - group_sections.push(Section { - codebook: sect_cb, - start: start as u8, - end: end as u8, - }); - k = end; - } - - sections.push(group_sections); - sfb_cb.push(group_sfb_cb); - } - - Ok(SectionData { sections, sfb_cb }) - } - - /// Parse the error-resilient `section_data()` branch - /// (`aacSectionDataResilienceFlag == 1`, Table 4.52). - /// - /// Two differences from the non-resilient [`SectionData::parse`]: - /// - /// * `sect_cb[g][i]` is read as a **5-bit** field (so it can carry - /// the §4.6.16.4 virtual codebooks 16..=31, the per-band VCB11 - /// range derived from `ESC_HCB`) rather than 4 bits. - /// * The `sect_len_incr` escape loop only runs when - /// `sect_cb < 11 || (sect_cb > 11 && sect_cb < 16)`; for - /// `sect_cb == 11` (`ESC_HCB`) or `sect_cb >= 16` (a virtual - /// codebook) the section length is fixed at `sect_len_incr = 1` - /// (one band) with no field on the wire. This is the Table 4.52 - /// `else { sect_len_incr = 1; }` branch. - /// - /// The recovered `sfb_cb[g][sfb]` therefore carries the raw 5-bit - /// `sect_cb` value (which may exceed `0x0f`); downstream tools that - /// only understand the base §4.A.1 books must map a virtual `>= 16` - /// codebook back onto `ESC_HCB` before dispatching — the value is - /// preserved here so that mapping can stay one layer up. - /// - /// Returns [`Error::SectionDataOverrun`] on a run past `max_sfb` - /// and [`Error::UnexpectedEnd`] on bit-reader underflow. - pub fn parse_er( - reader: &mut BitReader<'_>, - window_sequence: WindowSequence, - num_window_groups: u8, - max_sfb: u8, - ) -> Result { - // Table 4.52: sect_esc_val / sect_len_incr field width are the - // same as the non-resilient branch; only sect_cb widens to 5 - // bits and the escape loop is gated by the codebook value. - let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { - ((1u32 << 3) - 1, 3u32) - } else { - ((1u32 << 5) - 1, 5u32) - }; - - let mut sections: Vec> = Vec::with_capacity(num_window_groups as usize); - let mut sfb_cb: Vec> = Vec::with_capacity(num_window_groups as usize); - - for _g in 0..num_window_groups { - let mut group_sections: Vec
= Vec::new(); - let mut group_sfb_cb: Vec = vec![ZERO_HCB; max_sfb as usize]; - - let mut k: u32 = 0; - let max = max_sfb as u32; - while k < max { - let sect_cb = read_u8(reader, 5)?; - - let mut sect_len: u32 = 0; - if er_uses_escape_coding(sect_cb) { - loop { - let incr = reader - .read_u32(len_bits) - .map_err(|_| Error::UnexpectedEnd)?; - if incr == sect_esc_val { - sect_len += sect_esc_val; - continue; - } - sect_len += incr; - break; - } - } else { - // Table 4.52 `else { sect_len_incr = 1; }` — one band, - // no field on the wire. - sect_len = 1; - } - - let start = k; - let end = k + sect_len; - if end > max { - return Err(Error::SectionDataOverrun); - } - for sfb in start..end { - group_sfb_cb[sfb as usize] = sect_cb; - } - group_sections.push(Section { - codebook: sect_cb, - start: start as u8, - end: end as u8, - }); - k = end; - } - - sections.push(group_sections); - sfb_cb.push(group_sfb_cb); - } - - Ok(SectionData { sections, sfb_cb }) - } - - /// Encode the error-resilient `section_data()` branch, the inverse - /// of [`SectionData::parse_er`]. - /// - /// `sect_cb` is emitted as a 5-bit field; the `sect_len_incr` - /// escape sequence is emitted only for codebooks that use escape - /// coding (`< 11`, or `12..=15`). A `sect_cb == 11` / `>= 16` - /// section must span exactly one band (the Table 4.52 fixed - /// `sect_len_incr = 1`); a longer such section is rejected with - /// [`Error::SectionDataEncodeInvalid`]. - pub fn write_er( - &self, - writer: &mut BitWriter, - window_sequence: WindowSequence, - max_sfb: u8, - ) -> Result<()> { - let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { - (7u32, 3u32) - } else { - (31u32, 5u32) - }; - - for group_sections in &self.sections { - if group_sections.is_empty() { - if max_sfb != 0 { - return Err(Error::SectionDataEncodeInvalid); - } - continue; - } - if group_sections[0].start != 0 { - return Err(Error::SectionDataEncodeInvalid); - } - for w in group_sections.windows(2) { - if w[0].end != w[1].start { - return Err(Error::SectionDataEncodeInvalid); - } - } - if group_sections.last().unwrap().end != max_sfb { - return Err(Error::SectionDataEncodeInvalid); - } - - for section in group_sections { - // sect_cb is 5 bits in the ER branch. - if section.codebook > 0x1f { - return Err(Error::SectionDataEncodeInvalid); - } - let sect_len = section.len() as u32; - if sect_len == 0 { - return Err(Error::SectionDataEncodeInvalid); - } - - writer.write_u32(section.codebook as u32, 5); - - if er_uses_escape_coding(section.codebook) { - let mut remaining = sect_len; - while remaining >= sect_esc_val { - writer.write_u32(sect_esc_val, len_bits); - remaining -= sect_esc_val; - } - writer.write_u32(remaining, len_bits); - } else { - // Fixed sect_len_incr = 1 — the section must be a - // single band and carries no length field. - if sect_len != 1 { - return Err(Error::SectionDataEncodeInvalid); - } - } - } - } - - Ok(()) - } - - /// `num_sec[g]` — number of sections in window group `g`. - /// Returns `0` for an out-of-range group index. - pub fn num_sec(&self, group: usize) -> usize { - self.sections.get(group).map_or(0, Vec::len) - } - - /// Encode `section_data()` onto `writer`, inverse of - /// [`SectionData::parse`]. - /// - /// * `writer` — receives the bit-exact Table 17 stream. The - /// writer position advances by `4 + (3|5) × (n_increments)` bits - /// per section (per the chosen `sect_esc_val` branch). - /// * `window_sequence` — must match the value the surrounding - /// `ics_info()` carries; selects 3-bit / 5-bit `sect_len_incr`. - /// * `max_sfb` — the band count the parser will be told. Every - /// per-group section list must cover bands `[0, max_sfb)` - /// exactly without gaps or overlaps. - /// - /// Returns [`Error::SectionDataEncodeInvalid`] if: - /// - /// * `self.sections.len()` doesn't equal the implicit - /// `num_window_groups` (taken from `self.sections.len()`). - /// `num_window_groups` itself isn't a parameter — it's read - /// off `self.sections` so a caller who constructed - /// [`SectionData`] in-memory cannot accidentally desync. - /// * Any group's section list isn't contiguous from band `0` - /// to band `max_sfb` (start of first section != 0; end of - /// last section != `max_sfb`; or section `[i].end != - /// sections[i+1].start`). - /// * A `sect_cb` exceeds the 4-bit field width. - /// * A `sect_len` of `0` appears (a conforming encoder never - /// emits empty sections, and the §6.3 escape can't terminate - /// a zero-length run with the parser's `break` semantics). - pub fn write( - &self, - writer: &mut BitWriter, - window_sequence: WindowSequence, - max_sfb: u8, - ) -> Result<()> { - let (sect_esc_val, len_bits) = if window_sequence.is_eight_short() { - (7u32, 3u32) // (1 << 3) - 1, 3-bit field - } else { - (31u32, 5u32) // (1 << 5) - 1, 5-bit field - }; - - for group_sections in &self.sections { - // Empty section list is only valid when max_sfb == 0: - // the parser's `while k < max_sfb` loop never enters. - if group_sections.is_empty() { - if max_sfb != 0 { - return Err(Error::SectionDataEncodeInvalid); - } - continue; - } - - // Contiguity: first section starts at 0, sections chain - // end[i] == start[i+1], last ends at max_sfb. - if group_sections[0].start != 0 { - return Err(Error::SectionDataEncodeInvalid); - } - for w in group_sections.windows(2) { - if w[0].end != w[1].start { - return Err(Error::SectionDataEncodeInvalid); - } - } - if group_sections.last().unwrap().end != max_sfb { - return Err(Error::SectionDataEncodeInvalid); - } - - for section in group_sections { - // sect_cb is 4 bits; reject any out-of-range value. - if section.codebook > 0x0f { - return Err(Error::SectionDataEncodeInvalid); - } - let sect_len = section.len() as u32; - // A conforming encoder never emits a zero-length - // section; the §6.3 termination relies on a non- - // escape final increment, and the parser's outer - // `while k < max_sfb` would then re-enter the loop - // expecting another sect_cb. Reject up front. - if sect_len == 0 { - return Err(Error::SectionDataEncodeInvalid); - } - - writer.write_u32(section.codebook as u32, 4); - - // §6.3 escape: while remaining >= sect_esc_val, - // emit sect_esc_val and subtract. The trailing - // non-escape increment (which is in [0, sect_esc_val) - // by construction) terminates the run. This is what - // forces a literal `0` after a length that's an - // exact multiple of sect_esc_val (e.g. sect_len=31 - // long branch → emit 31, then 0). - let mut remaining = sect_len; - while remaining >= sect_esc_val { - writer.write_u32(sect_esc_val, len_bits); - remaining -= sect_esc_val; - } - writer.write_u32(remaining, len_bits); - } - } - - Ok(()) - } -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} - -/// Table 4.52 escape-coding gate for the error-resilient -/// `section_data()` branch. -/// -/// Escape coding (`sect_len_incr` loop) runs when -/// `sect_cb < 11 || (sect_cb > 11 && sect_cb < 16)`. For -/// `sect_cb == 11` (`ESC_HCB`) and `sect_cb >= 16` (the §4.6.16.4 -/// virtual codebooks) the spec fixes `sect_len_incr = 1` and emits no -/// length field, so the section spans exactly one band. -fn er_uses_escape_coding(sect_cb: u8) -> bool { - sect_cb < 11 || (sect_cb > 11 && sect_cb < 16) -} diff --git a/crates/vendor/oxideav-aac/src/spectral_codebook.rs b/crates/vendor/oxideav-aac/src/spectral_codebook.rs deleted file mode 100644 index 24d71b3d..00000000 --- a/crates/vendor/oxideav-aac/src/spectral_codebook.rs +++ /dev/null @@ -1,543 +0,0 @@ -//! Spectrum Huffman codebook parameters and the §4.6.3.3 index → -//! n-tuple translation. -//! -//! ISO/IEC 14496-3 §4.6.3 / Table 4.95 enumerates the AAC spectrum -//! Huffman codebooks. Each codebook is identified by a number `i ∈ -//! 0..=11` (plus the §4.6.3.1 non-spectral books 12..=15 and the -//! ISO/IEC 14496-3 Annex 4.6.3.3 extension books 16..=31) and carries -//! four parameters used by the §4.6.3.3 spectrum-translation -//! pseudocode: -//! -//! | column | meaning | -//! |----------------|---------| -//! | `unsigned_cb` | `0` ⇔ codeword indices encode a signed centred range `-LAV..=+LAV`; `1` ⇔ unsigned `0..=LAV` with explicit sign bits | -//! | `dimension` | `2` (PAIR books) or `4` (QUAD books) — number of spectral coefficients per codeword | -//! | `LAV` | largest absolute value the book can represent directly (without the ESC sequence) | -//! | spec table | which `Table 4.A.x` lists the Huffman codes (not consumed by this module — see "Scope" below) | -//! -//! ## What this module covers -//! -//! * The [`Table495Row`] struct — the four normative columns of -//! Table 4.95 for one codebook number. -//! * The [`TABLE_4_95`] static — the row for every codebook number -//! in `0..=31`, sourced from ISO/IEC 14496-3:2001(E) §4.6.3.1 -//! Table 4.95. Rows for `12` (reserved), `13` (PNS), `14` -//! (out-of-phase intensity), and `15` (in-phase intensity) carry -//! `None` for `unsigned_cb`, `dimension`, and `lav` — those four -//! indices do not carry spectral data so the §4.6.3.3 translation -//! does not apply. -//! * [`table_4_95`] — a safe accessor that returns the row for a -//! given codebook number (0..=31). -//! * [`decode_index_to_tuple`] — the §4.6.3.3 pseudocode that -//! translates a Huffman codeword index `idx` (the first column of -//! Table 4.A.2 through Table 4.A.12) into a `dim`-tuple of -//! quantised spectral coefficients. For unsigned books, the -//! returned tuple carries non-negative magnitudes whose signs are -//! restored by the per-coefficient sign bits that follow the -//! codeword on the wire. -//! * [`encode_tuple_to_index`] — the inverse of -//! `decode_index_to_tuple`. Given a `dim`-tuple of quantised -//! coefficients valid for the codebook (i.e. respecting the -//! `signed`/`unsigned` convention and the LAV cap), returns the -//! matching codeword index that an encoder would emit before the -//! Huffman compression layer. -//! * [`apply_sign_bits`] — folds the per-non-zero-coefficient sign -//! bits from §4.6.3.3 onto an unsigned-codebook decoded tuple. -//! * [`derive_sign_bits`] — the inverse: extracts the sign bits an -//! encoder must emit for an unsigned-codebook signed tuple. -//! * [`decode_esc_value`] — the §4.6.3.3 escape sequence for -//! codebook 11 (`ESC_HCB`). Given an `escape_prefix` length (the -//! run of 1-bits before the separator 0) and the `(N + 4)`-bit -//! `escape_word`, returns the absolute magnitude -//! `2^(N + 4) + escape_word`. -//! * [`encode_esc_value`] — the inverse: given an absolute magnitude -//! `>= LAV = 16`, returns the `(prefix_len, escape_word_bits, -//! escape_word)` triple an encoder must emit. -//! * [`MAX_QUANT`] = `8191` — the maximum absolute amplitude any -//! spectrum codebook 11 can represent, per §4.6.1.3. -//! -//! ## What this module does *not* cover -//! -//! * The Huffman tables themselves (Tables 4.A.2 through 4.A.12 + -//! the AAC-LD / ER variants). Those translate a codeword -//! *bit-pattern* into the `idx` consumed by [`decode_index_to_tuple`]. -//! The Huffman trees are a separate clean-room transcription that -//! will land in a follow-up round. -//! * The §4.4.6 `spectral_data()` wire walker — the function that -//! loops over scalefactor bands and dispatches per-band onto the -//! appropriate codebook. That walker will sit on top of this -//! module and the (forthcoming) Huffman tables. -//! * Codebooks 16..=31 — the Table 4.95 tail (rows 16..=31, all -//! reusing Table 4.A.12 with different ESC thresholds) are -//! surfaced in [`TABLE_4_95`] for completeness but the §4.6.3.3 -//! index translation for these books needs the ESC threshold -//! plumbed through the ESC sequence; the parser-facing accessors -//! in this round handle the standard `0..=11` range and reject -//! `12..=31` with [`Error::SpectralCodebookOutOfRange`]. The -//! per-row LAV value already differs for `16..=31` because each -//! row carries its own ESC threshold; the row data is correct, -//! only the wire decoder is unwired. - -use crate::section_data::Codebook; -use crate::{Error, Result}; - -/// Maximum absolute amplitude for a quantised spectral coefficient -/// (`x_quant`). ISO/IEC 14496-3 §4.6.1.3. -pub const MAX_QUANT: i32 = 8191; - -/// One row of ISO/IEC 14496-3 Table 4.95 (Spectrum Huffman codebook -/// parameters). Carries the `unsigned_cb`, dimension, and LAV -/// columns; the "Codebook listed in Table" column is encoded as a -/// `Some(table_index)` (e.g. `Some(2)` for Table 4.A.2) when the -/// row references a Huffman codebook listing, and `None` for the -/// non-spectral books (0 / 12..=15). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Table495Row { - /// Column 2 — `unsigned_cb[i]`. `None` for non-spectral books - /// (0 / 12..=15). - pub unsigned: Option, - /// Column 3 — dimension (`2` or `4`). `None` for non-spectral - /// books. - pub dimension: Option, - /// Column 4 — Largest Absolute Value the codebook can encode - /// directly. For codebook `0` the value is `0` (the band carries - /// no data so the maximum encoded magnitude is trivially 0). For - /// `12..=15` the column is `None`. For `11` and `16..=31` the - /// row carries the LAV after the ESC sequence is consumed; the - /// in-band LAV (15) is fixed by the Huffman codebook shape — see - /// [`Self::esc_threshold`] for the per-row ESC value. - pub lav: Option, - /// Column 4 trailing parenthesis — the per-row ESC threshold. - /// `Some(8191)` for codebook 11, `Some(15)` for codebook 16 - /// (the "w/o ESC" row — the threshold is the in-band cap), and - /// `Some(31)..=Some(2047)` for codebooks 17..=31. `None` for - /// codebooks 1..=10 (no ESC sequence — the LAV is fully covered - /// by the in-band Huffman table) and for the non-spectral books. - pub esc_threshold: Option, - /// Column 5 — the Table 4.A.x number that lists the Huffman - /// codes. `Some(2)..=Some(12)` for codebooks 1..=11; the - /// extension books 16..=31 all reuse Table 4.A.12 so the value - /// is `Some(12)` for each of those. `None` for codebook 0 and - /// 12..=15. - pub huffman_table: Option, -} - -impl Table495Row { - /// `true` ⇔ the row carries `unsigned_cb == 1`. Convenience - /// accessor that defaults to `false` for non-spectral books - /// (where the column is `None`). - pub fn is_unsigned(self) -> bool { - matches!(self.unsigned, Some(true)) - } - - /// `true` ⇔ the codebook carries an ESC sequence (codebook 11 - /// and the extension books 16..=31). - pub fn has_esc(self) -> bool { - self.esc_threshold.is_some() - } -} - -/// Helper to build a row for a spectral codebook (`1..=11` and -/// `16..=31`). -const fn spec_row( - unsigned: bool, - dimension: u8, - lav: u32, - esc: Option, - table: u8, -) -> Table495Row { - Table495Row { - unsigned: Some(unsigned), - dimension: Some(dimension), - lav: Some(lav), - esc_threshold: esc, - huffman_table: Some(table), - } -} - -/// Helper for non-spectral rows (`0`, `12..=15`). -const fn nonspec_row() -> Table495Row { - Table495Row { - unsigned: None, - dimension: None, - lav: None, - esc_threshold: None, - huffman_table: None, - } -} - -/// ISO/IEC 14496-3 §4.6.3.1 Table 4.95 — Spectrum Huffman codebook -/// parameters. Index by codebook number (`0..=31`). -/// -/// Cross-check with ISO/IEC 14496-3:2001(E) page 113. Row-by-row: -/// -/// | i | unsigned | dim | LAV | ESC | table | -/// |----|----------|-----|-----|-----|-------| -/// | 0 | — | — | 0 | — | — | -/// | 1 | 0 | 4 | 1 | — | 4.A.2 | -/// | 2 | 0 | 4 | 1 | — | 4.A.3 | -/// | 3 | 1 | 4 | 2 | — | 4.A.4 | -/// | 4 | 1 | 4 | 2 | — | 4.A.5 | -/// | 5 | 0 | 2 | 4 | — | 4.A.6 | -/// | 6 | 0 | 2 | 4 | — | 4.A.7 | -/// | 7 | 1 | 2 | 7 | — | 4.A.8 | -/// | 8 | 1 | 2 | 7 | — | 4.A.9 | -/// | 9 | 1 | 2 | 12 | — | 4.A.10| -/// | 10 | 1 | 2 | 12 | — | 4.A.11| -/// | 11 | 1 | 2 | 16 | 8191| 4.A.12| -/// | 12 | — | — | — | — | reserved | -/// | 13 | — | — | — | — | PNS | -/// | 14 | — | — | — | — | intensity out-of-phase | -/// | 15 | — | — | — | — | intensity in-phase | -/// | 16 | 1 | 2 | 16 | 15 | 4.A.12 | -/// | 17 | 1 | 2 | 16 | 31 | 4.A.12 | -/// | 18 | 1 | 2 | 16 | 47 | 4.A.12 | -/// | 19 | 1 | 2 | 16 | 63 | 4.A.12 | -/// | 20 | 1 | 2 | 16 | 95 | 4.A.12 | -/// | 21 | 1 | 2 | 16 | 127 | 4.A.12 | -/// | 22 | 1 | 2 | 16 | 159 | 4.A.12 | -/// | 23 | 1 | 2 | 16 | 191 | 4.A.12 | -/// | 24 | 1 | 2 | 16 | 223 | 4.A.12 | -/// | 25 | 1 | 2 | 16 | 255 | 4.A.12 | -/// | 26 | 1 | 2 | 16 | 319 | 4.A.12 | -/// | 27 | 1 | 2 | 16 | 383 | 4.A.12 | -/// | 28 | 1 | 2 | 16 | 511 | 4.A.12 | -/// | 29 | 1 | 2 | 16 | 767 | 4.A.12 | -/// | 30 | 1 | 2 | 16 | 1023| 4.A.12 | -/// | 31 | 1 | 2 | 16 | 2047| 4.A.12 | -pub const TABLE_4_95: [Table495Row; 32] = [ - // 0: ZERO_HCB - Table495Row { - unsigned: None, - dimension: None, - lav: Some(0), - esc_threshold: None, - huffman_table: None, - }, - // 1..=4 (QUAD) - spec_row(false, 4, 1, None, 2), - spec_row(false, 4, 1, None, 3), - spec_row(true, 4, 2, None, 4), - spec_row(true, 4, 2, None, 5), - // 5..=10 (PAIR) - spec_row(false, 2, 4, None, 6), - spec_row(false, 2, 4, None, 7), - spec_row(true, 2, 7, None, 8), - spec_row(true, 2, 7, None, 9), - spec_row(true, 2, 12, None, 10), - spec_row(true, 2, 12, None, 11), - // 11: ESC - spec_row(true, 2, 16, Some(8191), 12), - // 12..=15: non-spectral - nonspec_row(), - nonspec_row(), - nonspec_row(), - nonspec_row(), - // 16: w/o ESC 15 (ESC threshold equals in-band LAV — the row - // exists but the ESC sequence is never invoked because the LAV - // cap is also 15). - spec_row(true, 2, 16, Some(15), 12), - // 17..=31: ESC books with increasing thresholds - spec_row(true, 2, 16, Some(31), 12), - spec_row(true, 2, 16, Some(47), 12), - spec_row(true, 2, 16, Some(63), 12), - spec_row(true, 2, 16, Some(95), 12), - spec_row(true, 2, 16, Some(127), 12), - spec_row(true, 2, 16, Some(159), 12), - spec_row(true, 2, 16, Some(191), 12), - spec_row(true, 2, 16, Some(223), 12), - spec_row(true, 2, 16, Some(255), 12), - spec_row(true, 2, 16, Some(319), 12), - spec_row(true, 2, 16, Some(383), 12), - spec_row(true, 2, 16, Some(511), 12), - spec_row(true, 2, 16, Some(767), 12), - spec_row(true, 2, 16, Some(1023), 12), - spec_row(true, 2, 16, Some(2047), 12), -]; - -/// Safe accessor for [`TABLE_4_95`]. Returns -/// [`Error::SpectralCodebookOutOfRange`] for `codebook > 31`. -pub fn table_4_95(codebook: u8) -> Result { - if (codebook as usize) >= TABLE_4_95.len() { - return Err(Error::SpectralCodebookOutOfRange(codebook)); - } - Ok(TABLE_4_95[codebook as usize]) -} - -/// Translate a Huffman codeword index `idx` to a `dim`-tuple of -/// quantised spectral coefficients, per ISO/IEC 14496-3 §4.6.3.3. -/// -/// The output buffer is the first `dim` entries of the returned -/// fixed-size array; the unused trailing entries are zero. For -/// `dim == 2` the meaningful entries are `[y, z]`; for `dim == 4` -/// they are `[w, x, y, z]`. The spec ordering is preserved -/// (low-frequency first within the n-tuple). -/// -/// `codebook` must be one of: -/// -/// * `1..=11` — standard spectrum books. The full §4.6.3.3 path is -/// exercised; ESC handling for `11` is not performed *inside* this -/// call (the caller dispatches on [`Table495Row::has_esc`] and -/// invokes [`decode_esc_value`] for each coefficient at the LAV -/// cap). -/// * `0` is rejected with [`Error::SpectralCodebookHasNoTuple`] -/// because the band carries no spectrum data. -/// * `12..=15` are rejected with -/// [`Error::SpectralCodebookHasNoTuple`] (non-spectral books). -/// * `16..=31` are accepted for the pseudocode mechanics but with -/// the same ESC-handling caveat as `11`. -/// -/// An out-of-range `idx` (which can only happen when the caller's -/// Huffman tree is incoherent — a conforming Huffman decoder always -/// emits an in-range index) surfaces as -/// [`Error::SpectralCodebookIndexOutOfRange`]. The legal range is -/// `0..mod^dim` where `mod = lav + 1` (unsigned) or `2 * lav + 1` -/// (signed). -pub fn decode_index_to_tuple(codebook: u8, idx: u32) -> Result<[i32; 4]> { - let row = table_4_95(codebook)?; - let dim = row - .dimension - .ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; - let lav = row.lav.ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; - let unsigned = row.is_unsigned(); - - let (modulus, offset) = if unsigned { - (lav as i64 + 1, 0i64) - } else { - (2 * lav as i64 + 1, lav as i64) - }; - - // Range check: idx must be < modulus^dim. - let mut max = 1i64; - for _ in 0..dim { - max = max.saturating_mul(modulus); - } - if (idx as i64) >= max { - return Err(Error::SpectralCodebookIndexOutOfRange(codebook)); - } - - let mut out = [0i32; 4]; - let mut remaining = idx as i64; - if dim == 4 { - // §4.6.3.3 pseudocode: - // w = INT(idx / mod^3) - off - // x = INT(idx / mod^2) - off (after removing the w slice) - // y = INT(idx / mod^1) - off (after removing the x slice) - // z = idx - off (the leftover scaled by mod^0) - let m2 = modulus * modulus; - let m3 = m2 * modulus; - let w = remaining / m3 - offset; - remaining -= (w + offset) * m3; - let x = remaining / m2 - offset; - remaining -= (x + offset) * m2; - let y = remaining / modulus - offset; - remaining -= (y + offset) * modulus; - let z = remaining - offset; - out[0] = w as i32; - out[1] = x as i32; - out[2] = y as i32; - out[3] = z as i32; - } else { - // dim == 2: only y and z (in the lower two slots). - let y = remaining / modulus - offset; - remaining -= (y + offset) * modulus; - let z = remaining - offset; - out[0] = y as i32; - out[1] = z as i32; - } - Ok(out) -} - -/// Inverse of [`decode_index_to_tuple`]: given a `dim`-tuple of -/// quantised coefficients, returns the codeword index that maps to -/// it under the §4.6.3.3 translation. -/// -/// `tuple` is the first `dim` entries of the input slice (`[w, x, -/// y, z]` for `dim == 4`, `[y, z]` for `dim == 2`); the unused -/// trailing entries are ignored. For unsigned codebooks every entry -/// must be in `0..=lav`; for signed codebooks every entry must be in -/// `-lav..=+lav`. Any value outside the valid range surfaces as -/// [`Error::SpectralCodebookTupleOutOfRange`]. -pub fn encode_tuple_to_index(codebook: u8, tuple: &[i32]) -> Result { - let row = table_4_95(codebook)?; - let dim = row - .dimension - .ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; - let lav = row.lav.ok_or(Error::SpectralCodebookHasNoTuple(codebook))?; - let unsigned = row.is_unsigned(); - - if tuple.len() < dim as usize { - return Err(Error::SpectralCodebookTupleOutOfRange(codebook)); - } - - let (modulus, offset) = if unsigned { - (lav as i64 + 1, 0i64) - } else { - (2 * lav as i64 + 1, lav as i64) - }; - let lav_i = lav as i32; - - let mut acc: i64 = 0; - for &v in tuple.iter().take(dim as usize) { - let valid = if unsigned { - (0..=lav_i).contains(&v) - } else { - (-lav_i..=lav_i).contains(&v) - }; - if !valid { - return Err(Error::SpectralCodebookTupleOutOfRange(codebook)); - } - acc = acc * modulus + (v as i64 + offset); - } - Ok(acc as u32) -} - -/// Apply the §4.6.3.3 sign-bit fix-up to an unsigned-codebook -/// decoded tuple. -/// -/// On the wire, an unsigned codebook (codebooks 3, 4, 7, 8, 9, 10, -/// 11, 16..=31) emits non-negative magnitudes; the actual sign of -/// each *non-zero* coefficient is carried in a separate sign bit -/// that immediately follows the Huffman codeword. The bit ordering -/// matches the spec's "lower frequency first" rule: for a QUAD book, -/// the sign for `w` (if `w != 0`) is first, then `x`, then `y`, -/// then `z`; for a PAIR book the order is `y`, then `z`. -/// -/// `signs` must contain exactly one bit per non-zero coefficient in -/// `tuple`, in the spec-defined order. A `1` bit makes the -/// coefficient negative; a `0` leaves it positive. -/// -/// On signed codebooks this is a no-op (signed books already carry -/// their sign in the codeword index). The caller is expected to -/// guard on [`Table495Row::is_unsigned`]; if invoked on a signed -/// codebook the function returns the input unchanged. -/// -/// Returns [`Error::SpectralCodebookSignBitsMismatch`] when -/// `signs.len()` disagrees with the count of non-zero coefficients -/// in the unsigned-codebook tuple. -pub fn apply_sign_bits(codebook: u8, mut tuple: [i32; 4], signs: &[bool]) -> Result<[i32; 4]> { - let row = table_4_95(codebook)?; - if !row.is_unsigned() { - // Signed codebooks already carry the sign in the codeword - // index; this is a no-op. We still accept `signs.is_empty()` - // and reject any non-empty `signs` to keep the API symmetric - // — a caller that incorrectly sent sign bits for a signed - // codebook is a bug worth surfacing. - if !signs.is_empty() { - return Err(Error::SpectralCodebookSignBitsMismatch(codebook)); - } - return Ok(tuple); - } - let dim = row - .dimension - .ok_or(Error::SpectralCodebookHasNoTuple(codebook))? as usize; - let nonzero = tuple.iter().take(dim).filter(|&&v| v != 0).count(); - if signs.len() != nonzero { - return Err(Error::SpectralCodebookSignBitsMismatch(codebook)); - } - let mut sign_it = signs.iter(); - for entry in tuple.iter_mut().take(dim) { - if *entry != 0 { - let neg = *sign_it.next().expect("count match"); - if neg { - *entry = -*entry; - } - } - } - Ok(tuple) -} - -/// Inverse of [`apply_sign_bits`]: given a signed tuple decoded -/// from an unsigned codebook, returns the sign-bit sequence the -/// encoder must emit (one bit per non-zero coefficient, low-to-high -/// frequency). -/// -/// On signed codebooks returns an empty sign-bit vector. -pub fn derive_sign_bits(codebook: u8, tuple: &[i32]) -> Result> { - let row = table_4_95(codebook)?; - let dim = row - .dimension - .ok_or(Error::SpectralCodebookHasNoTuple(codebook))? as usize; - if tuple.len() < dim { - return Err(Error::SpectralCodebookTupleOutOfRange(codebook)); - } - if !row.is_unsigned() { - return Ok(Vec::new()); - } - let mut bits = Vec::with_capacity(dim); - for &v in tuple.iter().take(dim) { - if v != 0 { - bits.push(v < 0); - } - } - Ok(bits) -} - -/// Decode a §4.6.3.3 ESC sequence to its absolute magnitude. -/// -/// The ESC sequence is emitted whenever a codebook-11 Huffman -/// codeword decodes to a 2-tuple coefficient at the in-band cap -/// (magnitude `16`). It consists of: -/// -/// 1. `escape_prefix` — a run of `N` consecutive `1` bits. -/// 2. `escape_separator` — a single `0` bit. -/// 3. `escape_word` — `N + 4` bits, big-endian, carrying the -/// unsigned word value. -/// -/// The decoded absolute magnitude is `2^(N + 4) + escape_word`. -/// -/// `prefix_len` must be in `0..=24`. §4.6.2 caps the *encoder-side* -/// magnitude at [`MAX_QUANT`] (8191, i.e. `N ≤ 8`), but the decode -/// side deliberately accepts larger escape codes: the normative -/// ISO/IEC 14496-26 ER AAC LD conformance vectors transmit escapes -/// far past the cap (`er_ad1103np_22_ep0` AU 508 carries magnitude -/// 9283 at `N == 9`; `er_ad1103np_24_ep0` AU 1551 carries 783 966 at -/// `N == 15`), and their reference waveforms require the value to be -/// decoded, not rejected. The `> 24` bound keeps a hostile all-ones -/// prefix run from consuming unbounded input (and the u32 magnitude -/// in `i32` range) while admitting every observed conformance -/// magnitude with headroom. `escape_word` must fit `(N + 4)` bits. -/// Out-of-range arguments surface as -/// [`Error::SpectralCodebookEscOutOfRange`]. -pub fn decode_esc_value(prefix_len: u32, escape_word: u32) -> Result { - if prefix_len > 24 { - return Err(Error::SpectralCodebookEscOutOfRange); - } - let word_bits = prefix_len + 4; - if escape_word >= (1u32 << word_bits) { - return Err(Error::SpectralCodebookEscOutOfRange); - } - Ok((1u32 << word_bits) + escape_word) -} - -/// Inverse of [`decode_esc_value`]: given an absolute magnitude -/// `>= 16` (the ESC threshold for codebook 11), returns the -/// `(prefix_len, escape_word)` pair the encoder must emit. -/// -/// The mapping is `prefix_len = floor(log2(value)) - 4` and -/// `escape_word = value - 2^(prefix_len + 4)`. Values in -/// `0..=15` cannot be ESC-encoded (they are in-band) and surface as -/// [`Error::SpectralCodebookEscOutOfRange`]. Values greater than -/// [`MAX_QUANT`] also surface there. -pub fn encode_esc_value(value: u32) -> Result<(u32, u32)> { - if value < 16 || value as i32 > MAX_QUANT { - return Err(Error::SpectralCodebookEscOutOfRange); - } - // floor(log2(value)) — value is in 16..=8191, so log2 is in - // 4..=12, and prefix_len = log2 - 4 is in 0..=8. - let log = 31 - value.leading_zeros(); - let prefix_len = log - 4; - let escape_word = value - (1u32 << (prefix_len + 4)); - Ok((prefix_len, escape_word)) -} - -/// Bridge to the existing [`Codebook`] enum: classifies a `sect_cb` -/// value (`0..=15`) into a semantic category. The wire-form -/// `sect_cb` field is 4 bits in the standard branch and 5 bits in -/// the ER-AAC resilience branch (Table 17), so [`Codebook`] only -/// covers `0..=15`; this re-export is a convenience so callers can -/// reach the existing classifier without importing -/// [`crate::section_data`] directly. -pub fn classify(sect_cb: u8) -> Codebook { - Codebook::from_value(sect_cb) -} diff --git a/crates/vendor/oxideav-aac/src/spectral_data.rs b/crates/vendor/oxideav-aac/src/spectral_data.rs deleted file mode 100644 index 20e13e36..00000000 --- a/crates/vendor/oxideav-aac/src/spectral_data.rs +++ /dev/null @@ -1,927 +0,0 @@ -//! `spectral_data()` wire walker — ISO/IEC 14496-3 Table 4.56. -//! -//! This module is the §4.4.6 driver that the round-259 README named -//! as the next step after the Codebook 1..=11 table set completed: -//! it loops over the window groups and sections established by -//! [`crate::ics_info`] / [`crate::section_data`] and recovers the -//! quantised spectral coefficients `x_quant` by dispatching, per -//! section, onto the [`crate::spectrum_huffman`] codeword decoders -//! and the [`crate::spectral_codebook`] index/sign/ESC translation -//! helpers. -//! -//! ## Table 4.56 layout -//! -//! ```text -//! spectral_data() { -//! for (g = 0; g < num_window_groups; g++) { -//! for (i = 0; i < num_sec[g]; i++) { -//! if (sect_cb[g][i] != ZERO_HCB && -//! sect_cb[g][i] != NOISE_HCB && -//! sect_cb[g][i] != INTENSITY_HCB && -//! sect_cb[g][i] != INTENSITY_HCB2) { -//! for (k = sect_sfb_offset[g][sect_start[g][i]]; -//! k < sect_sfb_offset[g][sect_end[g][i]];) { -//! if (sect_cb[g][i] < FIRST_PAIR_HCB) { -//! hcod[sect_cb[g][i]][w][x][y][z]; // 1..16 vlclbf -//! if (unsigned_cb[sect_cb[g][i]]) -//! quad_sign_bits; // 0..4 bslbf -//! k += QUAD_LEN; -//! } else { -//! hcod[sect_cb[g][i]][y][z]; // 1..15 vlclbf -//! if (unsigned_cb[sect_cb[g][i]]) -//! pair_sign_bits; // 0..2 bslbf -//! k += PAIR_LEN; -//! if (sect_cb[g][i] == ESC_HCB) { -//! if (y == ESC_FLAG) hcod_esc_y; // 5..21 vlclbf -//! if (z == ESC_FLAG) hcod_esc_z; // 5..21 vlclbf -//! } -//! } -//! } -//! } -//! } -//! } -//! } -//! ``` -//! -//! ## `sect_sfb_offset` — §4.5.2.3.4 -//! -//! The loop bounds come from the per-group coefficient offsets -//! `sect_sfb_offset[g][sfb]` derived in §4.5.2.3.4: -//! -//! * For the three long window sequences (`num_window_groups == 1`, -//! `window_group_length[0] == 1`) the offsets are simply -//! `swb_offset_long_window[fs_index][sfb]` for -//! `sfb ∈ 0..=max_sfb`. -//! * For `EIGHT_SHORT_SEQUENCE` each group `g` spans -//! `window_group_length[g]` grouped short windows whose spectral -//! data is interleaved scalefactor-band by scalefactor-band -//! (§4.5.2.3.5), so each *virtual* scalefactor band is -//! `window_group_length[g]` times the Table 4.130-family -//! scalefactor-window-band width: -//! `sect_sfb_offset[g][i+1] = sect_sfb_offset[g][i] + -//! (swb_offset_short[i+1] − swb_offset_short[i]) × -//! window_group_length[g]`. -//! -//! [`sect_sfb_offset`] exposes that derivation so follow-up tools -//! (the §4.6.3.3 `quant_to_spec()` deinterleaver, intensity / PNS -//! reconstruction) can reuse it. -//! -//! ## Coefficient storage -//! -//! [`SpectralData::x_quant`] holds one buffer per window group, in -//! the §4.5.2.3.5 *transmission* order: groups sequential, and -//! within a group the coefficients of all grouped short windows -//! interleaved per scalefactor band ("virtual" scalefactor bands). -//! Each group buffer is allocated at the full group span — -//! `window_group_length[g] × 128` for `EIGHT_SHORT_SEQUENCE`, -//! `1024` otherwise — with the bands above `max_sfb` (and every -//! `ZERO_HCB` / `NOISE_HCB` / intensity band) left at `0`, matching -//! the §4.5.2.3 "all spectral data associated with Huffman codebook -//! zero are omitted [and zeroed]" rule. De-interleaving into the -//! `spec[w][k]` window-major layout consumed by TNS / the filterbank -//! (the §4.6.3.3 `quant_to_spec()` pseudocode) is a follow-up tool. -//! -//! ## §4.6.3.3 per-codeword translation -//! -//! * The Huffman codeword index is translated to the n-tuple via -//! [`crate::spectral_codebook::decode_index_to_tuple`]. -//! * For unsigned codebooks (3, 4, 7..=11) the -//! `quad_sign_bits` / `pair_sign_bits` field follows the codeword -//! — one bit per non-zero coefficient, low frequency first, `1` = -//! negative — applied via -//! [`crate::spectral_codebook::apply_sign_bits`]. -//! * For the ESC codebook (11) a decoded magnitude of `16` -//! (`ESC_FLAG`) is not a literal value: a `hcod_esc_y` / -//! `hcod_esc_z` escape sequence follows the sign bits (in `y`, -//! `z` order) — an `escape_prefix` of `N` ones, a zero -//! `escape_separator`, and an `(N + 4)`-bit `escape_word` — -//! decoding to `2^(N+4) + escape_word` via -//! [`crate::spectral_codebook::decode_esc_value`], with the sign -//! carried by the already-parsed sign bit. §4.6.1.3 caps the -//! magnitude at `MAX_QUANT` (8191), so `N ≤ 8` on a conforming -//! stream. -//! -//! [`SpectralData::write`] is the symmetric encoder: it re-derives -//! the codeword index via -//! [`crate::spectral_codebook::encode_tuple_to_index`] (clamping -//! ESC-book magnitudes ≥ 16 to the in-band `ESC_FLAG`), emits the -//! sign bits via [`crate::spectral_codebook::derive_sign_bits`], and -//! appends the escape sequences via -//! [`crate::spectral_codebook::encode_esc_value`], producing a -//! bit-exact inverse of [`SpectralData::parse`]. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::ics_info::IcsInfo; -use crate::section_data::{Codebook, Section, SectionData}; -use crate::spectral_codebook::{ - apply_sign_bits, decode_esc_value, decode_index_to_tuple, derive_sign_bits, encode_esc_value, - encode_tuple_to_index, table_4_95, MAX_QUANT, -}; -use crate::spectrum_huffman::{ - hcod10_decode, hcod10_write, hcod11_decode, hcod11_write, hcod1_decode, hcod1_write, - hcod2_decode, hcod2_write, hcod3_decode, hcod3_write, hcod4_decode, hcod4_write, hcod5_decode, - hcod5_write, hcod6_decode, hcod6_write, hcod7_decode, hcod7_write, hcod8_decode, hcod8_write, - hcod9_decode, hcod9_write, -}; -#[cfg(test)] -use crate::swb_offset::{long_window_offsets, short_window_offsets}; -use crate::{Error, Result}; - -/// `QUAD_LEN` — coefficients per codeword for the dim-4 books -/// (1..=4), per Table 4.56 / Table 4.151. -pub const QUAD_LEN: usize = 4; - -/// `PAIR_LEN` — coefficients per codeword for the dim-2 books -/// (5..=11), per Table 4.56 / Table 4.151. -pub const PAIR_LEN: usize = 2; - -/// `ESC_FLAG` — the in-band ESC-book magnitude (16) that signals a -/// following `hcod_esc_y` / `hcod_esc_z` escape sequence -/// (§4.6.3.3). -pub const ESC_FLAG: i32 = 16; - -/// Derive `sect_sfb_offset[g][sfb]` (`sfb ∈ 0..=max_sfb`) per the -/// §4.5.2.3.4 pseudocode — the offset of the first coefficient of -/// each (virtual) scalefactor band within window group `g`'s -/// interleaved coefficient stream. -/// -/// Returns one `max_sfb + 1`-entry offset vector per window group. -/// For the long window sequences this is a single group mirroring -/// `swb_offset_long_window[fs_index]`; for `EIGHT_SHORT_SEQUENCE` -/// each group scales the Table 4.130-family band widths by -/// `window_group_length[g]`. -/// -/// Errors: -/// -/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] — `fs_index` -/// outside the `0..=11` SWB-table range. -/// * [`Error::SpectralDataInvalid`] — `max_sfb` exceeds the -/// `num_swb` of the active window sequence (the §4.5.2.3.4 loops -/// index `swb_offset[max_sfb]`, which only exists up to -/// `num_swb`). -pub fn sect_sfb_offset(ics_info: &IcsInfo, fs_index: u8) -> Result>> { - let max_sfb = ics_info.max_sfb as usize; - if ics_info.window_sequence.is_eight_short() { - let swb = ics_info.swb_offsets(fs_index)?; - // `swb` has num_swb + 1 entries; band widths exist for - // sfb < num_swb only. - if max_sfb + 1 > swb.len() { - return Err(Error::SpectralDataInvalid); - } - let mut per_group = Vec::with_capacity(ics_info.num_window_groups as usize); - for g in 0..ics_info.num_window_groups as usize { - let wgl = u32::from(ics_info.window_group_length[g]); - let mut offsets = Vec::with_capacity(max_sfb + 1); - let mut offset = 0u32; - offsets.push(offset); - for i in 0..max_sfb { - let width = u32::from(swb[i + 1] - swb[i]) * wgl; - offset += width; - offsets.push(offset); - } - per_group.push(offsets); - } - Ok(per_group) - } else { - let swb = ics_info.swb_offsets(fs_index)?; - if max_sfb + 1 > swb.len() { - return Err(Error::SpectralDataInvalid); - } - let offsets = swb[..=max_sfb].iter().map(|&o| u32::from(o)).collect(); - Ok(vec![offsets]) - } -} - -/// Quantised spectral coefficients recovered from (or destined for) -/// a Table 4.56 `spectral_data()` block. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SpectralData { - /// `x_quant`, one buffer per window group, in the §4.5.2.3.5 - /// transmission (interleaved) order. `x_quant[g].len() == - /// window_group_length[g] × 128` for `EIGHT_SHORT_SEQUENCE`, - /// `1024` otherwise; bands at or above `max_sfb` and bands whose - /// section codebook carries no spectrum (`ZERO_HCB`, - /// `NOISE_HCB`, intensity) are `0`. - pub x_quant: Vec>, -} - -impl SpectralData { - /// Length of one window group's coefficient buffer. - fn group_len(ics_info: &IcsInfo, g: usize) -> usize { - // The parser rejects LD + EIGHT_SHORT, so window_len() cannot - // fail on a parsed ics_info; fall back to the family frame - // length defensively. - let window_len = ics_info - .window_len() - .unwrap_or_else(|_| ics_info.family.frame_len()); - if ics_info.window_sequence.is_eight_short() { - ics_info.window_group_length[g] as usize * window_len - } else { - window_len - } - } - - /// Parse a Table 4.56 `spectral_data()` block. - /// - /// * `reader` — positioned at the first `spectral_data()` bit - /// (the position [`crate::ics_body::IcsBody`] surfaces as - /// `spectral_data_bit_offset`). - /// * `ics_info` — the channel's parsed `ics_info()` (drives - /// `num_window_groups` / `window_group_length` / - /// `window_sequence`). - /// * `section_data` — the channel's parsed `section_data()` - /// (drives the per-section codebook dispatch and the - /// `sect_start` / `sect_end` loop bounds). - /// * `fs_index` — `samplingFrequencyIndex` selecting the - /// Table 4.129-family `swb_offset` tables. - /// - /// Errors: - /// - /// * [`Error::UnexpectedEnd`] — bit-reader underflow inside a - /// codeword, sign-bit field, or escape sequence. - /// * [`Error::SpectralDataInvalid`] — structural violations: see - /// [`sect_sfb_offset`], a `section_data` group count that - /// disagrees with `ics_info`, a section carrying the reserved - /// codebook 12, or a section span that is not a whole number - /// of n-tuples. - /// * [`Error::SpectralCodebookEscOutOfRange`] — an escape - /// sequence whose decoded magnitude exceeds `MAX_QUANT` - /// (8191) per §4.6.1.3. - pub fn parse( - reader: &mut BitReader<'_>, - ics_info: &IcsInfo, - section_data: &SectionData, - fs_index: u8, - ) -> Result { - let offsets = sect_sfb_offset(ics_info, fs_index)?; - let num_groups = ics_info.num_window_groups as usize; - if section_data.sections.len() != num_groups { - return Err(Error::SpectralDataInvalid); - } - - let mut x_quant = Vec::with_capacity(num_groups); - for (g, group_offsets) in offsets.iter().enumerate() { - let mut buf = vec![0i32; Self::group_len(ics_info, g)]; - for sec in §ion_data.sections[g] { - let (cb, dim) = match section_codebook(sec)? { - Some(pair) => pair, - None => continue, - }; - let start = group_offsets[sec.start as usize] as usize; - let end = group_offsets[sec.end as usize] as usize; - debug_assert!(end <= buf.len(), "offsets bounded by group span"); - let mut k = start; - while k < end { - if k + dim > end { - return Err(Error::SpectralDataInvalid); - } - let idx = decode_codeword(reader, cb)?; - let tuple = decode_index_to_tuple(cb, idx)?; - let tuple = read_and_apply_signs(reader, cb, dim, tuple)?; - for (j, &v) in tuple.iter().take(dim).enumerate() { - buf[k + j] = if cb == 11 && v.abs() == ESC_FLAG { - // §4.6.3.3: escape sequences follow the - // sign bits, in y then z order; the sign - // bit already parsed applies to the - // escaped magnitude. - let mag = read_escape_sequence(reader)? as i32; - if v < 0 { - -mag - } else { - mag - } - } else { - v - }; - } - k += dim; - } - } - x_quant.push(buf); - } - Ok(SpectralData { x_quant }) - } - - /// Write a Table 4.56 `spectral_data()` block — the bit-exact - /// inverse of [`SpectralData::parse`] under the same `ics_info` - /// / `section_data` / `fs_index`. - /// - /// Errors: - /// - /// * [`Error::SpectralDataInvalid`] — same structural checks as - /// the parser. - /// * [`Error::SpectralDataEncodeInvalid`] — group buffer count - /// or lengths disagreeing with the `ics_info` grouping, or a - /// non-zero coefficient in a band that transmits no spectrum - /// (`ZERO_HCB` / `NOISE_HCB` / intensity sections, or at and - /// above `max_sfb`). - /// * [`Error::SpectralCodebookTupleOutOfRange`] — a coefficient - /// magnitude exceeding the section codebook's LAV (for the - /// ESC book, propagated as - /// [`Error::SpectralCodebookEscOutOfRange`] above - /// `MAX_QUANT`). - pub fn write( - &self, - writer: &mut BitWriter, - ics_info: &IcsInfo, - section_data: &SectionData, - fs_index: u8, - ) -> Result<()> { - let offsets = sect_sfb_offset(ics_info, fs_index)?; - let num_groups = ics_info.num_window_groups as usize; - if section_data.sections.len() != num_groups { - return Err(Error::SpectralDataInvalid); - } - if self.x_quant.len() != num_groups { - return Err(Error::SpectralDataEncodeInvalid); - } - - for (g, group_offsets) in offsets.iter().enumerate() { - let buf = &self.x_quant[g]; - if buf.len() != Self::group_len(ics_info, g) { - return Err(Error::SpectralDataEncodeInvalid); - } - // Bands that transmit no spectrum must hold zeros: - // everything not covered by a spectrum-carrying section. - let mut covered = vec![false; buf.len()]; - for sec in §ion_data.sections[g] { - if section_codebook(sec)?.is_none() { - continue; - } - let start = group_offsets[sec.start as usize] as usize; - let end = group_offsets[sec.end as usize] as usize; - covered[start..end].fill(true); - } - if buf.iter().zip(covered.iter()).any(|(&v, &c)| v != 0 && !c) { - return Err(Error::SpectralDataEncodeInvalid); - } - - for sec in §ion_data.sections[g] { - let (cb, dim) = match section_codebook(sec)? { - Some(pair) => pair, - None => continue, - }; - let start = group_offsets[sec.start as usize] as usize; - let end = group_offsets[sec.end as usize] as usize; - let mut k = start; - while k < end { - if k + dim > end { - return Err(Error::SpectralDataInvalid); - } - write_tuple(writer, cb, dim, &buf[k..k + dim])?; - k += dim; - } - } - } - Ok(()) - } -} - -/// Classify a section for the Table 4.56 dispatch: `Ok(None)` for -/// the codebooks that transmit no spectral data (`ZERO_HCB`, -/// `NOISE_HCB`, `INTENSITY_HCB`, `INTENSITY_HCB2`), -/// `Ok(Some((cb, dim)))` for the spectrum books 1..=11, and -/// [`Error::SpectralDataInvalid`] for the reserved codebook 12 -/// (which the Table 4.56 condition does not exclude but which has -/// no Huffman table to dispatch onto). -fn section_codebook(sec: &Section) -> Result> { - match sec.codebook_kind() { - Codebook::Zero - | Codebook::Noise - | Codebook::IntensityInPhase - | Codebook::IntensityOutOfPhase => Ok(None), - Codebook::Quad { number, .. } => Ok(Some((number, QUAD_LEN))), - Codebook::Pair { number, .. } => Ok(Some((number, PAIR_LEN))), - Codebook::Esc => Ok(Some((11, PAIR_LEN))), - Codebook::Reserved12 => Err(Error::SpectralDataInvalid), - } -} - -/// Dispatch one `hcod[cb]` codeword decode onto the per-book -/// decoder (Tables 4.A.2 … 4.A.12). -pub(crate) fn decode_codeword(reader: &mut BitReader<'_>, cb: u8) -> Result { - match cb { - 1 => hcod1_decode(reader), - 2 => hcod2_decode(reader), - 3 => hcod3_decode(reader), - 4 => hcod4_decode(reader), - 5 => hcod5_decode(reader), - 6 => hcod6_decode(reader), - 7 => hcod7_decode(reader), - 8 => hcod8_decode(reader), - 9 => hcod9_decode(reader), - 10 => hcod10_decode(reader), - 11 => hcod11_decode(reader), - _ => Err(Error::SpectralDataInvalid), - } -} - -/// Dispatch one `hcod[cb]` codeword write onto the per-book writer. -fn write_codeword(writer: &mut BitWriter, cb: u8, idx: u32) -> Result<()> { - match cb { - 1 => hcod1_write(writer, idx), - 2 => hcod2_write(writer, idx), - 3 => hcod3_write(writer, idx), - 4 => hcod4_write(writer, idx), - 5 => hcod5_write(writer, idx), - 6 => hcod6_write(writer, idx), - 7 => hcod7_write(writer, idx), - 8 => hcod8_write(writer, idx), - 9 => hcod9_write(writer, idx), - 10 => hcod10_write(writer, idx), - 11 => hcod11_write(writer, idx), - _ => Err(Error::SpectralDataInvalid), - } -} - -/// For unsigned codebooks, read the `quad_sign_bits` / -/// `pair_sign_bits` field (one bit per non-zero coefficient, low -/// frequency first, `1` = negative) and apply it to the magnitude -/// tuple per §4.6.3.3. Signed codebooks pass through unchanged. -pub(crate) fn read_and_apply_signs( - reader: &mut BitReader<'_>, - cb: u8, - dim: usize, - tuple: [i32; 4], -) -> Result<[i32; 4]> { - let row = table_4_95(cb)?; - if !row.is_unsigned() { - return Ok(tuple); - } - let nonzero = tuple.iter().take(dim).filter(|&&v| v != 0).count(); - let mut signs = Vec::with_capacity(nonzero); - for _ in 0..nonzero { - signs.push(reader.read_bit().map_err(|_| Error::UnexpectedEnd)?); - } - apply_sign_bits(cb, tuple, &signs) -} - -/// Read one `hcod_esc_y` / `hcod_esc_z` escape sequence per -/// §4.6.3.3: an `escape_prefix` of `N` ones, a zero -/// `escape_separator`, and an `(N + 4)`-bit `escape_word`, decoding -/// to `2^(N+4) + escape_word`. §4.6.2 caps the *encoded* magnitude -/// at `MAX_QUANT` (`N ≤ 8`), but the decoder accepts up to `N == 24` -/// — the ISO/IEC 14496-26 ER AAC LD conformance vectors transmit -/// escapes up to `N == 15` (magnitude 783 966) whose reference -/// waveforms require the decoded value (see -/// [`crate::spectral_codebook::decode_esc_value`]); a longer prefix -/// run is rejected without consuming further bits. -pub(crate) fn read_escape_sequence(reader: &mut BitReader<'_>) -> Result { - let mut prefix_len = 0u32; - while reader.read_bit().map_err(|_| Error::UnexpectedEnd)? { - prefix_len += 1; - if prefix_len > 24 { - return Err(Error::SpectralCodebookEscOutOfRange); - } - } - let escape_word = reader - .read_u32(prefix_len + 4) - .map_err(|_| Error::UnexpectedEnd)?; - decode_esc_value(prefix_len, escape_word) -} - -/// Write one n-tuple: the Huffman codeword, the sign bits (unsigned -/// books), and the escape sequences (ESC book, magnitudes ≥ 16). -pub(crate) fn write_tuple( - writer: &mut BitWriter, - cb: u8, - dim: usize, - coeffs: &[i32], -) -> Result<()> { - // Build the in-band tuple: for the ESC book, magnitudes >= 16 - // are clamped to the ESC_FLAG (signed, so the sign survives for - // derive_sign_bits); §4.6.1.3 bounds the true magnitude at - // MAX_QUANT. - let mut tuple = [0i32; 4]; - for (slot, &v) in tuple.iter_mut().zip(coeffs.iter()) { - if cb == 11 && v.abs() >= ESC_FLAG { - if v.abs() > MAX_QUANT { - return Err(Error::SpectralCodebookEscOutOfRange); - } - *slot = v.signum() * ESC_FLAG; - } else { - *slot = v; - } - } - - let row = table_4_95(cb)?; - let index_tuple: Vec = if row.is_unsigned() { - tuple.iter().take(dim).map(|v| v.abs()).collect() - } else { - tuple[..dim].to_vec() - }; - let idx = encode_tuple_to_index(cb, &index_tuple)?; - write_codeword(writer, cb, idx)?; - - if row.is_unsigned() { - for neg in derive_sign_bits(cb, &tuple[..dim])? { - writer.write_bit(neg); - } - } - - if cb == 11 { - for (&clamped, &v) in tuple.iter().zip(coeffs.iter()).take(dim) { - if clamped.abs() == ESC_FLAG { - let (prefix_len, escape_word) = encode_esc_value(v.unsigned_abs())?; - for _ in 0..prefix_len { - writer.write_bit(true); - } - writer.write_bit(false); - writer.write_u32(escape_word, prefix_len + 4); - } - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::WindowSequence; - use crate::section_data::ZERO_HCB; - - /// Build a long-window IcsInfo for fs_index 4 (44.1 kHz) with - /// the given max_sfb. - fn long_ics_info(max_sfb: u8) -> IcsInfo { - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::OnlyLong, - window_shape: crate::ics_info::WindowShape::Sine, - max_sfb, - scale_factor_grouping: None, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 1, - num_window_groups: 1, - window_group_length: vec![1], - num_swb: crate::ics_info::NUM_SWB_LONG_WINDOW[4], - } - } - - /// Build an EIGHT_SHORT IcsInfo for fs_index 4 with the given - /// grouping. - fn short_ics_info(max_sfb: u8, window_group_length: Vec) -> IcsInfo { - let num_window_groups = window_group_length.len() as u8; - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: WindowSequence::EightShort, - window_shape: crate::ics_info::WindowShape::Sine, - max_sfb, - scale_factor_grouping: Some(0), - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: 8, - num_window_groups, - window_group_length, - num_swb: crate::ics_info::NUM_SWB_SHORT_WINDOW[4], - } - } - - fn one_section(num_groups: usize, codebook: u8, max_sfb: u8) -> SectionData { - let sections = (0..num_groups) - .map(|_| { - vec![Section { - codebook, - start: 0, - end: max_sfb, - }] - }) - .collect::>(); - let sfb_cb = (0..num_groups) - .map(|_| vec![codebook; max_sfb as usize]) - .collect::>(); - SectionData { sections, sfb_cb } - } - - fn round_trip( - data: &SpectralData, - ics_info: &IcsInfo, - section_data: &SectionData, - fs_index: u8, - ) -> SpectralData { - let mut writer = BitWriter::new(); - data.write(&mut writer, ics_info, section_data, fs_index) - .expect("write"); - let bytes = writer.finish(); - let mut reader = BitReader::new(&bytes); - SpectralData::parse(&mut reader, ics_info, section_data, fs_index).expect("parse") - } - - #[test] - fn sect_sfb_offset_long_mirrors_swb_table() { - let info = long_ics_info(10); - let offsets = sect_sfb_offset(&info, 4).expect("offsets"); - assert_eq!(offsets.len(), 1); - let swb = long_window_offsets(4).expect("table"); - assert_eq!(offsets[0].len(), 11); - for (i, &o) in offsets[0].iter().enumerate() { - assert_eq!(o, u32::from(swb[i])); - } - } - - #[test] - fn sect_sfb_offset_short_scales_by_group_length() { - // Grouping 5 + 3: each virtual band is wgl × the Table - // 4.130 band width. - let info = short_ics_info(4, vec![5, 3]); - let offsets = sect_sfb_offset(&info, 4).expect("offsets"); - assert_eq!(offsets.len(), 2); - let swb = short_window_offsets(4).expect("table"); - for (g, wgl) in [(0usize, 5u32), (1, 3)] { - for i in 0..4 { - let width = u32::from(swb[i + 1] - swb[i]) * wgl; - assert_eq!(offsets[g][i + 1] - offsets[g][i], width); - } - } - } - - #[test] - fn sect_sfb_offset_rejects_max_sfb_above_num_swb() { - let mut info = long_ics_info(50); - info.max_sfb = 50; // num_swb for fs 4 long is 49. - assert!(matches!( - sect_sfb_offset(&info, 4), - Err(Error::SpectralDataInvalid) - )); - } - - #[test] - fn all_zero_sections_consume_no_bits() { - let info = long_ics_info(10); - let sd = one_section(1, ZERO_HCB, 10); - let mut reader = BitReader::new(&[0xff, 0xff]); - let parsed = SpectralData::parse(&mut reader, &info, &sd, 4).expect("parse"); - assert_eq!(reader.bit_position(), 0); - assert_eq!(parsed.x_quant.len(), 1); - assert_eq!(parsed.x_quant[0].len(), 1024); - assert!(parsed.x_quant[0].iter().all(|&v| v == 0)); - } - - #[test] - fn quad_signed_book_round_trip() { - let info = long_ics_info(2); - let sd = one_section(1, 1, 2); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - // fs 4 long bands 0..2 cover coefficients 0..8. - data.x_quant[0][..8].copy_from_slice(&[1, -1, 0, 1, -1, 0, 0, 1]); - assert_eq!(round_trip(&data, &info, &sd, 4), data); - } - - #[test] - fn unsigned_pair_book_round_trip_with_signs() { - let info = long_ics_info(2); - let sd = one_section(1, 7, 2); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - data.x_quant[0][..8].copy_from_slice(&[7, -7, 0, 3, -1, 2, 0, -5]); - assert_eq!(round_trip(&data, &info, &sd, 4), data); - } - - #[test] - fn esc_book_round_trip_with_escapes() { - let info = long_ics_info(2); - let sd = one_section(1, 11, 2); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - // In-band, half-ESC, full-ESC, extreme magnitudes. - data.x_quant[0][..8].copy_from_slice(&[15, -15, 16, -16, 8191, -8191, 0, 100]); - assert_eq!(round_trip(&data, &info, &sd, 4), data); - } - - #[test] - fn esc_magnitude_16_uses_escape_sequence_00000() { - // §4.6.3.3 worked example: an escape_sequence of 00000 - // decodes as 16. Pin the wire layout for the tuple (16, 0): - // index 16*17+0 = 272 → 9-bit 0x1c2, one sign bit (0), then - // prefix-less escape 0 0000. - let info = long_ics_info(1); - let sd = one_section(1, 11, 1); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - data.x_quant[0][..4].copy_from_slice(&[16, 0, 0, 0]); - let mut writer = BitWriter::new(); - data.write(&mut writer, &info, &sd, 4).expect("write"); - // Band 0 at fs 4 long spans 4 coefficients = 2 pair tuples: - // (16, 0) then (0, 0). Codeword 0x1c2 (9 bits), sign 0, - // escape 00000 (5 bits), then (0,0) codeword 0b0000 (4 - // bits). Total 9 + 1 + 5 + 4 = 19 bits. - assert_eq!(writer.bit_position(), 19); - let bytes = writer.finish(); - let mut reader = BitReader::new(&bytes); - let parsed = SpectralData::parse(&mut reader, &info, &sd, 4).expect("parse"); - assert_eq!(reader.bit_position(), 19); - assert_eq!(parsed, data); - } - - #[test] - fn short_grouped_round_trip() { - // Two groups (5 + 3 windows); codebook 2 (signed quad) over - // 4 virtual bands per group. - let info = short_ics_info(4, vec![5, 3]); - let sd = one_section(2, 2, 4); - let offsets = sect_sfb_offset(&info, 4).expect("offsets"); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 5 * 128], vec![0i32; 3 * 128]], - }; - for (g, group_offsets) in offsets.iter().enumerate() { - let end = group_offsets[4] as usize; - for k in 0..end { - data.x_quant[g][k] = match k % 3 { - 0 => 1, - 1 => -1, - _ => 0, - }; - } - } - assert_eq!(round_trip(&data, &info, &sd, 4), data); - } - - #[test] - fn parse_rejects_reserved_codebook_12() { - let info = long_ics_info(2); - let sd = one_section(1, 12, 2); - let mut reader = BitReader::new(&[0x00; 8]); - assert!(matches!( - SpectralData::parse(&mut reader, &info, &sd, 4), - Err(Error::SpectralDataInvalid) - )); - } - - #[test] - fn parse_rejects_group_count_mismatch() { - let info = long_ics_info(2); - let sd = one_section(2, 1, 2); // two groups vs long's one - let mut reader = BitReader::new(&[0x00; 8]); - assert!(matches!( - SpectralData::parse(&mut reader, &info, &sd, 4), - Err(Error::SpectralDataInvalid) - )); - } - - #[test] - fn parse_rejects_truncated_codeword() { - let info = long_ics_info(2); - let sd = one_section(1, 9, 2); - // Codebook 9 max codeword is 15 bits; an all-ones byte is a - // prefix of longer codewords, so a 1-byte buffer underflows. - let mut reader = BitReader::new(&[0xff]); - assert!(matches!( - SpectralData::parse(&mut reader, &info, &sd, 4), - Err(Error::UnexpectedEnd) - )); - } - - #[test] - fn write_rejects_nonzero_outside_sections() { - let info = long_ics_info(2); - let sd = one_section(1, ZERO_HCB, 2); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - data.x_quant[0][0] = 1; - let mut writer = BitWriter::new(); - assert!(matches!( - data.write(&mut writer, &info, &sd, 4), - Err(Error::SpectralDataEncodeInvalid) - )); - } - - #[test] - fn write_rejects_nonzero_above_max_sfb() { - let info = long_ics_info(2); - let sd = one_section(1, 1, 2); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - data.x_quant[0][1023] = 1; - let mut writer = BitWriter::new(); - assert!(matches!( - data.write(&mut writer, &info, &sd, 4), - Err(Error::SpectralDataEncodeInvalid) - )); - } - - #[test] - fn write_rejects_magnitude_above_lav() { - let info = long_ics_info(2); - let sd = one_section(1, 1, 2); // codebook 1, LAV 1 - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - data.x_quant[0][0] = 2; - let mut writer = BitWriter::new(); - assert!(matches!( - data.write(&mut writer, &info, &sd, 4), - Err(Error::SpectralCodebookTupleOutOfRange(1)) - )); - } - - #[test] - fn write_rejects_esc_magnitude_above_max_quant() { - let info = long_ics_info(2); - let sd = one_section(1, 11, 2); - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - data.x_quant[0][0] = MAX_QUANT + 1; - let mut writer = BitWriter::new(); - assert!(matches!( - data.write(&mut writer, &info, &sd, 4), - Err(Error::SpectralCodebookEscOutOfRange) - )); - } - - #[test] - fn write_rejects_wrong_group_buffer_length() { - let info = long_ics_info(2); - let sd = one_section(1, 1, 2); - let data = SpectralData { - x_quant: vec![vec![0i32; 512]], - }; - let mut writer = BitWriter::new(); - assert!(matches!( - data.write(&mut writer, &info, &sd, 4), - Err(Error::SpectralDataEncodeInvalid) - )); - } - - #[test] - fn escape_prefix_run_past_24_rejected() { - // A run of >24 ones exceeds the decoder-side tolerance bound - // (the ISO conformance vectors reach N == 15; the cap guards - // hostile all-ones input). - let mut reader = BitReader::new(&[0xff, 0xff, 0xff, 0xff]); - assert!(matches!( - read_escape_sequence(&mut reader), - Err(Error::SpectralCodebookEscOutOfRange) - )); - } - - #[test] - fn escape_sequence_examples_from_spec() { - // §4.6.3.3: 00000 → 16, 01111 → 31, 1000000 → 32, - // 1011111 → 63. - for (bits, len, expect) in [ - (0b00000u32, 5u32, 16u32), - (0b01111, 5, 31), - (0b1000000, 7, 32), - (0b1011111, 7, 63), - ] { - let mut writer = BitWriter::new(); - writer.write_u32(bits, len); - let bytes = writer.finish(); - let mut reader = BitReader::new(&bytes); - assert_eq!(read_escape_sequence(&mut reader).expect("esc"), expect); - assert_eq!(reader.bit_position(), u64::from(len)); - } - } - - #[test] - fn multi_section_mixed_codebooks_round_trip() { - // Bands 0..2 on book 1 (quad), 2..4 zero, 4..6 on book 11. - let info = long_ics_info(6); - let sections = vec![vec![ - Section { - codebook: 1, - start: 0, - end: 2, - }, - Section { - codebook: ZERO_HCB, - start: 2, - end: 4, - }, - Section { - codebook: 11, - start: 4, - end: 6, - }, - ]]; - let sfb_cb = vec![vec![1, 1, ZERO_HCB, ZERO_HCB, 11, 11]]; - let sd = SectionData { sections, sfb_cb }; - let mut data = SpectralData { - x_quant: vec![vec![0i32; 1024]], - }; - // fs 4 long: bands are 4 wide here, so 0..8 book 1, 8..16 - // zero, 16..24 book 11. - data.x_quant[0][..8].copy_from_slice(&[1, 0, -1, 0, 0, 1, 1, -1]); - data.x_quant[0][16..24].copy_from_slice(&[20, -3, 0, 0, 1000, -16, 15, 0]); - assert_eq!(round_trip(&data, &info, &sd, 4), data); - } -} diff --git a/crates/vendor/oxideav-aac/src/spectrum_huffman.rs b/crates/vendor/oxideav-aac/src/spectrum_huffman.rs deleted file mode 100644 index 09d96306..00000000 --- a/crates/vendor/oxideav-aac/src/spectrum_huffman.rs +++ /dev/null @@ -1,5593 +0,0 @@ -//! Spectrum Huffman codebook **wire** layer — ISO/IEC 14496-3 -//! §4.6.3 + Annex 4.A (Tables 4.A.2 … 4.A.12). -//! -//! Round 213 landed [`crate::spectral_codebook`] — the §4.6.3.3 index ↔ -//! n-tuple translation, the §4.6.3 sign-bit fix-up, and the codebook-11 -//! ESC sequence. That module does **not** carry the Huffman codeword -//! tables themselves: it operates on the *index* the wire bitstream -//! decodes to, leaving the codeword ↔ index mapping for this module -//! to own. -//! -//! Round 219 landed the first of the eleven spectrum Huffman -//! codebooks — **Table 4.A.2, "Spectrum Huffman Codebook 1"**. Round -//! 226 added the second — **Table 4.A.3, "Spectrum Huffman Codebook -//! 2"**. Round 231 added the third — **Table 4.A.4, "Spectrum Huffman -//! Codebook 3"**. Round 234 added the fourth — **Table 4.A.5, "Spectrum -//! Huffman Codebook 4"**. Round 238 added the fifth — **Table 4.A.6, -//! "Spectrum Huffman Codebook 5"** — the first **pair** (`dim = 2`) -//! book and the first book to widen its codewords to 13 bits. Round -//! 241 adds the sixth — **Table 4.A.7, "Spectrum Huffman Codebook -//! 6"** — the second pair book, sharing the Codebook 5 Table 4.95 -//! row shape (`signed`, `dim = 2`, `LAV = 4`) but tightening the -//! codeword ceiling back down to 11 bits. Round 244 adds the -//! seventh — **Table 4.A.8, "Spectrum Huffman Codebook 7"** — the -//! first **unsigned pair** book (Table 4.95 row 7: `unsigned_cb = 1`, -//! `dim = 2`, `LAV = 7`), widening the per-coefficient magnitude -//! range to `0..=7` and parking the §4.6.3.3 zero-tuple `(0, 0)` at -//! index 0 with a single-bit `0` codeword. Round 250 adds the -//! eighth — **Table 4.A.9, "Spectrum Huffman Codebook 8"** — the -//! second **unsigned pair** book, sharing the Codebook 7 Table 4.95 -//! row shape (`unsigned_cb = 1`, `dim = 2`, `LAV = 7` → 64 entries -//! indexed `0..=63`) but tightening the codeword ceiling down to -//! 10 bits and migrating the shortest codeword off the §4.6.3.3 -//! zero-tuple `(0, 0)` at index 0 (which now carries a 5-bit -//! `0b01110`) onto the interior tuple `(1, 1)` at index 9 (which -//! carries the 3-bit `0b000`). Round 253 adds the ninth — **Table -//! 4.A.10, "Spectrum Huffman Codebook 9"** — the first -//! **expanded-LAV unsigned pair** book (Table 4.95 row 9: -//! `unsigned_cb = 1`, `dim = 2`, `LAV = 12`), exercising the -//! §4.6.3.3 universe expansion to a `(12 + 1)^2 = 13^2 = 169`-entry -//! lattice indexed `0..=168` with each `(y, z)` coefficient in -//! `0..=12`. Codebook 9 parks the §4.6.3.3 zero-tuple `(0, 0)` at -//! index 0 with a single-bit `0` codeword (matching the head- -//! placement of Codebook 7) and pins the far corner `(12, 12)` at -//! index 168 with a 15-bit `0x7fff` — the widest codeword among -//! the non-ESC spectrum books. -//! Codebooks 1 and 2 share the same Table 4.95 -//! row shape (`signed`, `dim = 4`, `LAV = 1` → `3^4 = 81` entries -//! indexed `0..=80`); Codebooks 3 and 4 share the unsigned dim-4 -//! shape (Table 4.95 rows 3 and 4 both: `unsigned_cb = 1`, `dim = 4`, -//! `LAV = 2` → `3^4 = 81` entries indexed `0..=80`, with sign bits -//! following the Huffman codeword for every non-zero coefficient per -//! §4.6.3.3); Codebooks 5 and 6 share the signed pair shape -//! (Table 4.95 rows 5 and 6 both: `unsigned_cb = 0`, `dim = 2`, -//! `LAV = 4` → `(2 * 4 + 1)^2 = 9^2 = 81` entries indexed `0..=80`, -//! each tuple coefficient in `-4..=+4`, signed-book so no sign-bit -//! suffix is required after the codeword). Codebook 7 is the first -//! unsigned pair book (Table 4.95 row 7: `unsigned_cb = 1`, `dim = 2`, -//! `LAV = 7` → `(7 + 1)^2 = 8^2 = 64` entries indexed `0..=63`, each -//! tuple coefficient in `0..=7`, sign-bit suffix follows the codeword -//! for each non-zero coefficient per §4.6.3.3); Codebook 8 shares -//! the same unsigned dim-2 LAV-7 shape (Table 4.95 row 8 column-for- -//! column matches row 7 except for the `Codebook listed in Table` -//! cell pointing at Table 4.A.9). Codebook 9 (Table 4.95 row 9) -//! widens the per-coefficient ceiling to `LAV = 12` — the §4.6.3.3 -//! universe grows from `8 × 8 = 64` to `13 × 13 = 169` entries — -//! making it the largest of the non-ESC spectrum books. -//! Round 255 adds the tenth — **Table 4.A.11, "Spectrum Huffman -//! Codebook 10"** — the second **expanded-LAV unsigned pair** book -//! (Table 4.95 row 10: `unsigned_cb = 1`, `dim = 2`, `LAV = 12` → -//! 169 entries indexed `0..=168`, the same `13 × 13` universe -//! Codebook 9 covers). Codebook 10 trades Codebook 9's -//! zero-tuple-at-the-1-bit-head distribution for a flatter codeword -//! profile: the zero-tuple `(0, 0)` at index 0 now carries a 6-bit -//! `0b100010` (`0x22`), the shortest slot (4 bits, codeword `0b0000`) -//! migrates onto the interior `(1, 1)` tuple at index 14, and the -//! codeword ceiling pulls down from Codebook 9's 15 bits to **12 -//! bits** — matching the head-displacement pattern Codebook 8 uses -//! relative to Codebook 7 (one row lifted from the 1-bit slot, -//! shortest codeword moved off the zero-tuple) but at the wider -//! `LAV = 12` universe. -//! Round 259 adds the eleventh — **Table 4.A.12, "Spectrum Huffman -//! Codebook 11"** — the only **ESC** spectrum book (Table 4.95 -//! row 11: `unsigned_cb = 1`, `dim = 2`, `LAV = 16` with an ESC -//! threshold of `8191` — the §4.6.1.3 `x_quant` ceiling). The -//! §4.6.3.3 in-band universe widens to a `17 × 17 = 289`-entry -//! lattice indexed `0..=288` with each `(y, z)` coefficient in -//! `0..=16`; a coefficient value of `16` in either slot is the -//! §4.6.3.3 `escape_flag` whose actual magnitude is reconstructed -//! from the `escape_sequence` (`escape_prefix` of N `1`s, a `0` -//! `escape_separator`, and an `(N + 4)`-bit `escape_word`) bridged -//! by [`crate::spectral_codebook::decode_esc_value`] / -//! [`crate::spectral_codebook::encode_esc_value`] — both already -//! landed in round 213, separate from the Huffman codeword this -//! module carries. Codebook 11 parks the zero-tuple `(0, 0)` at -//! index 0 with the shortest 4-bit codeword `0b0000`, shares that -//! 4-bit floor with the interior `(1, 1)` pair at index 18 (the -//! second 4-bit slot, codeword `0b0001`), pins the half-ESC tuples -//! `(0, 16)` and `(16, 0)` to 10-bit `0x38e` (index 16) and 9-bit -//! `0x1c2` (index 272), and parks the full-ESC corner `(16, 16)` -//! at index 288 with the surprisingly short 5-bit `0b00100` -//! (`0x04`) — the wire layout extends with two sign bits and two -//! escape sequences for that corner, so the Huffman codeword -//! itself stays short. The codeword ceiling matches Codebook 10's -//! 12 bits — exactly six rows reach it (indices 12, 14, 15, 255, -//! 269, 270) — because Codebook 11 pushes its tail distribution -//! out of the Huffman table and into the §4.6.3 ESC sequence. -//! With Codebook 11 the per-codebook AAC spectrum Huffman tables -//! are complete (Tables 4.A.2 through 4.A.12 all land in this -//! module); the next step is the §4.4.6 `spectral_data()` wire -//! walker that loops over scalefactor bands and dispatches per-band -//! onto the codebook chosen by `section_data()`. -//! -//! ## Codebook 1 invariants (Table 4.A.2) -//! -//! | property | value | source | -//! |------------------------|-----------|------------------------------| -//! | dimension | 4 | Table 4.95 row 1, column 3 | -//! | `unsigned_cb` | 0 (signed)| Table 4.95 row 1, column 2 | -//! | LAV | 1 | Table 4.95 row 1, column 4 | -//! | entry count | `3^4 = 81`| `(2 * 1 + 1)^4` per §4.6.3.3 | -//! | maximum codeword length| 11 bits | Table 4.A.2 column 2 maximum | -//! | shortest codeword | 1 bit | Table 4.A.2 row 40 (index 40)| -//! | shortest codeword value| `0` | Table 4.A.2 row 40 | -//! | Kraft equality | 2048 = 2¹¹| see [`hcod1_is_complete`] | -//! -//! Index 40 is `(w, x, y, z) = (0, 0, 0, 0)` per §4.6.3.3 — the -//! zero-tuple gets the single-bit codeword because zero-tuples are -//! the modal spectrum n-tuple in any non-silent frame. -//! -//! ## Wire representation in memory -//! -//! Codewords are stored right-aligned within a `u16`: the MSB of the -//! wire codeword sits at bit `length − 1`, the LSB at bit `0`. To emit -//! bit-for-bit, [`hcod1_encode`] returns `(length, codeword)` and the -//! caller passes them straight to -//! [`oxideav_core::bits::BitWriter::write_u32`]. -//! -//! ## Codebook 2 invariants (Table 4.A.3) -//! -//! | property | value | source | -//! |------------------------|-----------|------------------------------| -//! | dimension | 4 | Table 4.95 row 2, column 3 | -//! | `unsigned_cb` | 0 (signed)| Table 4.95 row 2, column 2 | -//! | LAV | 1 | Table 4.95 row 2, column 4 | -//! | entry count | `3^4 = 81`| `(2 * 1 + 1)^4` per §4.6.3.3 | -//! | maximum codeword length| 9 bits | Table 4.A.3 column 2 maximum | -//! | shortest codeword | 3 bits | Table 4.A.3 row 40 (index 40)| -//! | shortest codeword value| `0` | Table 4.A.3 row 40 | -//! | Kraft equality | 512 = 2⁹ | see [`hcod2_is_complete`] | -//! -//! Codebook 2 covers the same `3^4 = 81` signed 4-tuple universe as -//! Codebook 1, with each coefficient in `(-1, 0, +1)`. The encoder -//! chooses between the two books per-section based on -//! `section_data()`'s `sect_cb` field; the choice reflects which book -//! gives the shorter overall bit count for the section's tuple -//! statistics. Index 40 is `(w, x, y, z) = (0, 0, 0, 0)` in both -//! books; in Codebook 2 it carries the 3-bit codeword `0b000` -//! (vs the single bit `0` in Codebook 1). -//! -//! ## Codebook 3 invariants (Table 4.A.4) -//! -//! | property | value | source | -//! |------------------------|------------|------------------------------| -//! | dimension | 4 | Table 4.95 row 3, column 3 | -//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 3, column 2 | -//! | LAV | 2 | Table 4.95 row 3, column 4 | -//! | entry count | `3^4 = 81` | `(2 + 1)^4` per §4.6.3.3 | -//! | maximum codeword length| 16 bits | Table 4.A.4 column 2 maximum | -//! | shortest codeword | 1 bit | Table 4.A.4 row 0 (index 0) | -//! | shortest codeword value| `0` | Table 4.A.4 row 0 | -//! | Kraft equality | 65536 = 2¹⁶| see [`hcod3_is_complete`] | -//! -//! Codebook 3 is the first *unsigned* spectrum book: the Huffman -//! codeword conveys the magnitude n-tuple (each coefficient in -//! `0..=LAV = 0..=2`) and each non-zero coefficient is followed by a -//! single sign bit per §4.6.3.3 (the sign bits travel in -//! low-frequency-first order: `w`, `x`, `y`, `z`). The zero-tuple -//! `(0, 0, 0, 0)` is at *index 0* (not 40 as in the signed books) -//! because the unsigned modulus-3 polynomial puts all-zero at the -//! origin; it carries the single bit codeword `0`. The §4.6.3.3 -//! sign-bit suffix is exposed by -//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -//! and is *not* part of the Huffman codeword itself — this module's -//! `hcod3_encode` / `hcod3_decode` cover the codeword only. -//! -//! ## Codebook 4 invariants (Table 4.A.5) -//! -//! | property | value | source | -//! |------------------------|------------|------------------------------| -//! | dimension | 4 | Table 4.95 row 4, column 3 | -//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 4, column 2 | -//! | LAV | 2 | Table 4.95 row 4, column 4 | -//! | entry count | `3^4 = 81` | `(2 + 1)^4` per §4.6.3.3 | -//! | maximum codeword length| 12 bits | Table 4.A.5 column 2 maximum | -//! | shortest codeword | 4 bits | Table 4.A.5 row 40 (index 40)| -//! | shortest codeword value| `0` | Table 4.A.5 row 40 | -//! | Kraft equality | 4096 = 2¹² | see [`hcod4_is_complete`] | -//! -//! Codebook 4 shares Codebook 3's unsigned dim-4 LAV-2 tuple universe -//! (Table 4.95 row 4 is identical to row 3 except for the `Codebook -//! listed in Table` column) but uses a different per-row Huffman -//! length tuning for a different encoder target-statistics. Where -//! Codebook 3 puts the zero-tuple at index 0 with a single-bit -//! codeword and lets the magnitude-2 tuples climb to a 16-bit -//! maximum, Codebook 4 puts the zero-tuple at the *same* §4.6.3.3 -//! polynomial position (index 0 maps the unsigned `(0, 0, 0, 0)` -//! tuple via the `((w*3 + x)*3 + y)*3 + z` evaluation with no offset) -//! — but the codeword assignment lifts the zero-tuple to a 4-bit -//! codeword (`0b0111`) and parks the *shortest* codeword (4 bits -//! `0b0000`) at **index 40** instead. The maximum codeword length is -//! **12 bits** (vs 16 for Codebook 3), and two distinct rows reach -//! that length: index 62 (`0xfff`) and index 74 (`0xffe`). The -//! shorter overall code length distribution makes Codebook 4 a -//! better fit for sections whose magnitude statistics are flatter -//! across the `(0, 0, 0, 0) .. (2, 2, 2, 2)` range than Codebook 3's -//! zero-heavy target. The §4.6.3.3 sign-bit suffix is again exposed -//! by [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) -//! / [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -//! and is *not* part of the Huffman codeword itself — this module's -//! `hcod4_encode` / `hcod4_decode` cover the codeword only. -//! -//! ## Codebook 5 invariants (Table 4.A.6) -//! -//! | property | value | source | -//! |------------------------|------------|------------------------------| -//! | dimension | 2 (pair) | Table 4.95 row 5, column 3 | -//! | `unsigned_cb` | 0 (signed) | Table 4.95 row 5, column 2 | -//! | LAV | 4 | Table 4.95 row 5, column 4 | -//! | entry count | `9^2 = 81` | `(2 * 4 + 1)^2` per §4.6.3.3 | -//! | maximum codeword length| 13 bits | Table 4.A.6 column 2 maximum | -//! | shortest codeword | 1 bit | Table 4.A.6 row 40 (index 40)| -//! | shortest codeword value| `0` | Table 4.A.6 row 40 | -//! | Kraft equality | 8192 = 2¹³ | see [`hcod5_is_complete`] | -//! -//! Codebook 5 is the first **pair** book — the §4.6.3.3 translation -//! consumes two coefficients per Huffman codeword (`(y, z)`) rather -//! than four (`(w, x, y, z)`) — and the first book to widen the -//! per-coefficient quantised range to `-4..=+4` (LAV = 4). The pair -//! universe stays at 81 entries because `(2 * 4 + 1)^2 = 9^2 = 81` -//! coincides with the dim-4 LAV-1 / LAV-2 universes of Codebooks -//! 1..=4. Index 40 carries the §4.6.3.3 zero-tuple `(0, 0)` — the -//! `(modulus = 9, offset = 4)` polynomial evaluation puts the -//! origin at the centre of the index range, not at the edges as in -//! the unsigned books (Codebooks 3 and 4 placed `(0, 0, 0, 0)` at -//! index 0). The shortest codeword (1 bit `0`) parks at index 40 -//! — the same zero-tuple position as Codebook 1 (whose dim-4 origin -//! also lands at the row-40 centre via the same signed-book -//! polynomial). The maximum codeword length is **13 bits** — one -//! more than Codebook 4's 12-bit ceiling and three less than -//! Codebook 3's 16-bit reach — and exactly four rows occupy the -//! 13-bit ceiling: indices 0, 8, 72, and 80 (the four corners -//! `(-4, -4)`, `(-4, +4)`, `(+4, -4)`, `(+4, +4)` of the -//! `9 × 9` signed pair lattice). Because Codebook 5 is **signed**, -//! the §4.6.3.3 sign-bit suffix is *not* emitted after the -//! codeword — every coefficient's sign is baked into the index -//! itself via the `offset = LAV = 4` shift. -//! -//! ## Codebook 6 invariants (Table 4.A.7) -//! -//! | property | value | source | -//! |------------------------|------------|------------------------------| -//! | dimension | 2 (pair) | Table 4.95 row 6, column 3 | -//! | `unsigned_cb` | 0 (signed) | Table 4.95 row 6, column 2 | -//! | LAV | 4 | Table 4.95 row 6, column 4 | -//! | entry count | `9^2 = 81` | `(2 * 4 + 1)^2` per §4.6.3.3 | -//! | maximum codeword length| 11 bits | Table 4.A.7 column 2 maximum | -//! | shortest codeword | 4 bits | Table 4.A.7 row 40 (index 40)| -//! | shortest codeword value| `0` | Table 4.A.7 row 40 | -//! | Kraft equality | 2048 = 2¹¹| see [`hcod6_is_complete`] | -//! -//! ## Codebook 7 invariants (Table 4.A.8) -//! -//! | property | value | source | -//! |------------------------|------------|------------------------------| -//! | dimension | 2 (pair) | Table 4.95 row 7, column 3 | -//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 7, column 2 | -//! | LAV | 7 | Table 4.95 row 7, column 4 | -//! | entry count | `8^2 = 64` | `(7 + 1)^2` per §4.6.3.3 | -//! | maximum codeword length| 12 bits | Table 4.A.8 column 2 maximum | -//! | shortest codeword | 1 bit | Table 4.A.8 row 0 (index 0) | -//! | shortest codeword value| `0` | Table 4.A.8 row 0 | -//! | Kraft equality | 4096 = 2¹²| see [`hcod7_is_complete`] | -//! -//! Codebook 7 is the first **unsigned pair** spectrum book — the -//! §4.6.3.3 translation consumes two coefficients per Huffman codeword -//! (`(y, z)`) with each coefficient in `0..=LAV = 0..=7`. The pair -//! universe has `(7 + 1)^2 = 64` entries indexed `0..=63`, a notable -//! drop from the 81-entry universe of Codebooks 1..=6 — the higher -//! per-coefficient ceiling (LAV = 7 vs LAV = 1, 2, 4 in the earlier -//! books) trades dimensionality for range. Like the unsigned dim-4 -//! books (Codebooks 3 and 4) the zero-tuple sits at *index 0* (not -//! 40 as in the signed books); the unsigned polynomial -//! `idx = y * (LAV + 1) + z = y * 8 + z` puts all-zero at the origin -//! and the maximum tuple `(7, 7)` at index 63. The single-bit -//! codeword `0` parks at index 0 — the same shortest-codeword position -//! as Codebook 3. The §4.6.3.3 sign-bit suffix applies after every -//! non-zero coefficient (the sign bits travel in low-frequency-first -//! order: `y`, `z`); the suffix is exposed by -//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -//! and is *not* part of the Huffman codeword itself — this module's -//! `hcod7_encode` / `hcod7_decode` cover the codeword only. -//! -//! ## Codebook 8 invariants (Table 4.A.9) -//! -//! | property | value | source | -//! |------------------------|------------|------------------------------| -//! | dimension | 2 (pair) | Table 4.95 row 8, column 3 | -//! | `unsigned_cb` | 1 (unsigned)| Table 4.95 row 8, column 2 | -//! | LAV | 7 | Table 4.95 row 8, column 4 | -//! | entry count | `8^2 = 64` | `(7 + 1)^2` per §4.6.3.3 | -//! | maximum codeword length| 10 bits | Table 4.A.9 column 2 maximum | -//! | shortest codeword | 3 bits | Table 4.A.9 row 9 (index 9) | -//! | shortest codeword value| `0` | Table 4.A.9 row 9 | -//! | Kraft equality | 1024 = 2¹⁰| see [`hcod8_is_complete`] | -//! -//! Codebook 8 shares Codebook 7's unsigned pair tuple universe -//! (Table 4.95 row 8 is identical to row 7 except for the `Codebook -//! listed in Table` column) but uses a different per-row Huffman -//! length tuning. Where Codebook 7 pins the §4.6.3.3 zero-tuple -//! `(0, 0)` to index 0 with the single-bit codeword `0` and lets -//! the upper-right quadrant of the lattice climb to a 12-bit -//! ceiling, Codebook 8 lifts the zero-tuple at index 0 to a 5-bit -//! `0b01110` and migrates the shortest codeword (3 bits `0b000`) to -//! **index 9** — the unsigned-polynomial position of the interior -//! tuple `(y, z) = (1, 1)` (`idx = 1 * 8 + 1 = 9`). The maximum -//! codeword length is **10 bits**; exactly four rows reach the -//! ceiling: indices 7 (`0x3fe`), 47 (`0x3fc`), 56 (`0x3fd`), and -//! 63 (`0x3ff`) — the rarest pair magnitudes (one or two -//! coefficients at the LAV cap). The flatter, lower-ceiling -//! codeword distribution makes Codebook 8 a better fit for sections -//! whose magnitude statistics put weight on the `(1, 1)` interior -//! rather than the `(0, 0)` zero-tuple corner Codebook 7 -//! optimises. Because Codebook 8 is unsigned, the §4.6.3.3 sign-bit -//! suffix follows the Huffman codeword on the wire — one sign bit -//! per non-zero coefficient, low-frequency-first — and is exposed -//! by [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) -//! / [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -//! and is *not* part of the Huffman codeword itself — this module's -//! `hcod8_encode` / `hcod8_decode` cover the codeword only. -//! -//! ## Codebook 9 invariants (Table 4.A.10) -//! -//! | property | value | source | -//! |------------------------|---------------|-------------------------------| -//! | dimension | 2 (pair) | Table 4.95 row 9, column 3 | -//! | `unsigned_cb` | 1 (unsigned) | Table 4.95 row 9, column 2 | -//! | LAV | 12 | Table 4.95 row 9, column 4 | -//! | entry count | `13^2 = 169` | `(12 + 1)^2` per §4.6.3.3 | -//! | maximum codeword length| 15 bits | Table 4.A.10 column 2 maximum | -//! | shortest codeword | 1 bit | Table 4.A.10 row 0 (index 0) | -//! | shortest codeword value| `0` | Table 4.A.10 row 0 | -//! | Kraft equality | 32768 = 2¹⁵ | see [`hcod9_is_complete`] | -//! -//! Codebook 9 is the first **expanded-LAV pair** spectrum book — it -//! steps away from the `8 × 8` unsigned pair lattice Codebooks 7 and -//! 8 share and widens the per-coefficient ceiling from `7` to `12`, -//! producing a `13 × 13 = 169`-entry universe indexed `0..=168` with -//! each `(y, z)` coefficient in `0..=12`. The §4.6.3.3 unsigned -//! polynomial `idx = y * (LAV + 1) + z = y * 13 + z` parks the -//! zero-tuple `(0, 0)` at index 0 — the same head placement -//! Codebook 7 uses — and pins the maximum tuple `(12, 12)` at index -//! 168 (the far corner of the `13 × 13` unsigned lattice). The -//! single-bit codeword `0` lives at index 0, matching the -//! shortest-slot placement Codebook 7 also uses for its zero-tuple. -//! The maximum codeword length is **15 bits** — a 5-bit jump up -//! from Codebook 8's 10-bit ceiling and the widest non-ESC spectrum -//! codeword in the entire Annex 4.A book set — reflecting the -//! `169 / 64 ≈ 2.6×` universe expansion that widens the -//! distribution's tail. Exactly four rows reach the 15-bit ceiling: -//! indices 142 (`0x7ffc`), 154 (`0x7ffd`), 155 (`0x7ffe`), and 168 -//! (`0x7fff`) — the rarest pair magnitudes, sitting near the -//! `LAV = 12` cap. The table is a **complete** 15-bit prefix code -//! (Kraft equality `Σ 2^(15 − L) = 32768 = 2¹⁵`), exhaustively -//! verified by walking every 15-bit prefix and asserting each maps -//! to exactly one entry. Because Codebook 9 is unsigned, the -//! §4.6.3.3 sign-bit suffix follows the Huffman codeword on the -//! wire — one sign bit per non-zero coefficient, low-frequency- -//! first — and is exposed by -//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -//! and is *not* part of the Huffman codeword itself — this module's -//! `hcod9_encode` / `hcod9_decode` cover the codeword only. -//! -//! ## Codebook 10 invariants (Table 4.A.11) -//! -//! | property | value | source | -//! |------------------------|---------------|-------------------------------| -//! | dimension | 2 (pair) | Table 4.95 row 10, column 3 | -//! | `unsigned_cb` | 1 (unsigned) | Table 4.95 row 10, column 2 | -//! | LAV | 12 | Table 4.95 row 10, column 4 | -//! | entry count | `13^2 = 169` | `(12 + 1)^2` per §4.6.3.3 | -//! | maximum codeword length| 12 bits | Table 4.A.11 column 2 maximum | -//! | shortest codeword | 4 bits | Table 4.A.11 row 14 (index 14)| -//! | shortest codeword value| `0` | Table 4.A.11 row 14 | -//! | Kraft equality | 4096 = 2¹² | see [`hcod10_is_complete`] | -//! -//! Codebook 10 shares Codebook 9's expanded-LAV unsigned pair tuple -//! universe (Table 4.95 row 10 is identical to row 9 except for the -//! `Codebook listed in Table` column pointing at Table 4.A.11) but -//! uses a different per-row Huffman length tuning for a different -//! encoder target-statistics. Where Codebook 9 parks the §4.6.3.3 -//! zero-tuple `(0, 0)` at index 0 with the single-bit `0` codeword -//! and lets the four rarest pair magnitudes climb to a 15-bit -//! ceiling, Codebook 10 keeps the zero-tuple at index 0 (the -//! §4.6.3.3 polynomial position is fixed by the tuple) but its -//! codeword swells to 6 bits (`0x22`), the shortest 4-bit slot -//! migrates onto the interior `(1, 1)` tuple at index 14 with -//! codeword `0b0000`, and the codeword ceiling pulls down to -//! **12 bits**. Exactly three rows reach the 4-bit floor (indices -//! 14, 15, 27 with codewords `0x0`, `0x1`, `0x2`) and exactly eight -//! rows reach the 12-bit ceiling (indices 12, 129, 142, 155, 165, -//! 166, 167, 168 with codewords `0xffd`, `0xffa`, `0xff9`, `0xffb`, -//! `0xff8`, `0xffe`, `0xffc`, `0xfff`) — the four corners and four -//! near-edges of the `13 × 13` unsigned lattice. The flatter, -//! pull-down distribution makes Codebook 10 a better fit for -//! sections whose magnitude statistics put more weight in the -//! `(1..=4, 1..=4)` interior than Codebook 9's -//! more-zero-tuple-heavy target. The encoder chooses between the -//! two books per-section via `section_data()`'s `sect_cb` field; -//! the §4.6.3.3 sign-bit suffix follows the Huffman codeword on the -//! wire — one sign bit per non-zero coefficient, low-frequency- -//! first — and is exposed by -//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -//! [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -//! and is *not* part of the Huffman codeword itself — this module's -//! `hcod10_encode` / `hcod10_decode` cover the codeword only. -//! -//! Codebook 6 shares Codebook 5's signed pair tuple universe -//! (Table 4.95 row 6 is identical to row 5 except for the `Codebook -//! listed in Table` column) but uses a different per-row Huffman -//! length tuning. Where Codebook 5 parks the single bit `0` at -//! index 40 and lets the four lattice corners reach a 13-bit -//! ceiling, Codebook 6 lifts the zero-tuple at index 40 to a 4-bit -//! `0b0000` and pulls the ceiling back to **11 bits**. Exactly four -//! rows reach the 11-bit ceiling: indices 0 (`0x7fe`), 8 (`0x7fd`), -//! 72 (`0x7ff`), and 80 (`0x7fc`) — the four `(±4, ±4)` corners of -//! the `9 × 9` signed pair lattice, the same four corner positions -//! Codebook 5 also pinned to its 13-bit ceiling. The shorter, -//! flatter codeword distribution makes Codebook 6 a better fit -//! for sections whose magnitude statistics put more weight in the -//! `(±1, ±1) .. (±3, ±3)` interior than Codebook 5's -//! more-zero-tuple-heavy target. The encoder chooses between the -//! two books per-section via `section_data()`'s `sect_cb` field; -//! the §4.6.3.3 sign bits remain inside the index for both books -//! because both are signed (`unsigned_cb = 0`). -//! -//! * The §4.6.3.3 index → n-tuple translation. That sits in -//! [`crate::spectral_codebook::decode_index_to_tuple`] / -//! [`crate::spectral_codebook::encode_tuple_to_index`]. -//! * The ESC sequence (codebook 11 and the extension books 16..=31). -//! That sits in [`crate::spectral_codebook::decode_esc_value`] / -//! [`crate::spectral_codebook::encode_esc_value`]. -//! * The §4.6.3 sign-bit suffix for unsigned codebooks. Codebook 1 -//! is *signed* so no sign bits follow the codeword; the -//! [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) -//! path is exercised by unsigned codebooks (3, 4, 7..=11, 16..=31). -//! * The `spectral_data()` driver that loops over scalefactor bands -//! and dispatches per-band onto the codebook chosen by -//! `section_data()`. That driver will land once codebooks 2..=11 -//! are in place. - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::{Error, Result}; - -// ============================================================================= -// Table 4.A.2 — Spectrum Huffman Codebook 1 -// ============================================================================= -// -// 81 entries, indices 0..=80. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the wire -// codeword at bit `length − 1`). Reproduced verbatim from ISO/IEC -// 14496-3:2001(E) §4.A.1 Table 4.A.2 (page 193). -// -// The codebook is a complete prefix code: Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹. -// This is exhaustively verified at compile time by the -// `hcod1_is_complete` regression test (which walks every 11-bit -// prefix and asserts each maps to exactly one index). - -/// Number of entries in Table 4.A.2 (`81`, indices `0..=80`). -pub const HCOD1_NUM_ENTRIES: usize = 81; - -/// Maximum codeword length emitted by Table 4.A.2 (11 bits). -pub const HCOD1_MAX_LEN: u32 = 11; - -/// Table 4.A.2 — `(length_in_bits, codeword)` per index `0..=80`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD1: [(u8, u16); HCOD1_NUM_ENTRIES] = [ - (11, 0x7f8), // 0 - (9, 0x1f1), // 1 - (11, 0x7fd), // 2 - (10, 0x3f5), // 3 - (7, 0x68), // 4 - (10, 0x3f0), // 5 - (11, 0x7f7), // 6 - (9, 0x1ec), // 7 - (11, 0x7f5), // 8 - (10, 0x3f1), // 9 - (7, 0x72), // 10 - (10, 0x3f4), // 11 - (7, 0x74), // 12 - (5, 0x11), // 13 - (7, 0x76), // 14 - (9, 0x1eb), // 15 - (7, 0x6c), // 16 - (10, 0x3f6), // 17 - (11, 0x7fc), // 18 - (9, 0x1e1), // 19 - (11, 0x7f1), // 20 - (9, 0x1f0), // 21 - (7, 0x61), // 22 - (9, 0x1f6), // 23 - (11, 0x7f2), // 24 - (9, 0x1ea), // 25 - (11, 0x7fb), // 26 - (9, 0x1f2), // 27 - (7, 0x69), // 28 - (9, 0x1ed), // 29 - (7, 0x77), // 30 - (5, 0x17), // 31 - (7, 0x6f), // 32 - (9, 0x1e6), // 33 - (7, 0x64), // 34 - (9, 0x1e5), // 35 - (7, 0x67), // 36 - (5, 0x15), // 37 - (7, 0x62), // 38 - (5, 0x12), // 39 - (1, 0x000), // 40 — zero-tuple, single bit `0` - (5, 0x14), // 41 - (7, 0x65), // 42 - (5, 0x16), // 43 - (7, 0x6d), // 44 - (9, 0x1e9), // 45 - (7, 0x63), // 46 - (9, 0x1e4), // 47 - (7, 0x6b), // 48 - (5, 0x13), // 49 - (7, 0x71), // 50 - (9, 0x1e3), // 51 - (7, 0x70), // 52 - (9, 0x1f3), // 53 - (11, 0x7fe), // 54 - (9, 0x1e7), // 55 - (11, 0x7f3), // 56 - (9, 0x1ef), // 57 - (7, 0x60), // 58 - (9, 0x1ee), // 59 - (11, 0x7f0), // 60 - (9, 0x1e2), // 61 - (11, 0x7fa), // 62 - (10, 0x3f3), // 63 - (7, 0x6a), // 64 - (9, 0x1e8), // 65 - (7, 0x75), // 66 - (5, 0x10), // 67 - (7, 0x73), // 68 - (9, 0x1f4), // 69 - (7, 0x6e), // 70 - (10, 0x3f7), // 71 - (11, 0x7f6), // 72 - (9, 0x1e0), // 73 - (11, 0x7f9), // 74 - (10, 0x3f2), // 75 - (7, 0x66), // 76 - (9, 0x1f5), // 77 - (11, 0x7ff), // 78 - (9, 0x1f7), // 79 - (11, 0x7f4), // 80 -]; - -/// Encode a Codebook 1 codeword index (`0..=80`) to the wire Huffman -/// codeword from Table 4.A.2. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=80` (the 81-entry `3^4` enumeration of every legal -/// signed 4-tuple with each coefficient in `-1..=+1`). -/// -/// The inverse of [`hcod1_decode`]. -pub fn hcod1_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD1 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(1))?; - Ok(*entry) -} - -/// Decode one Codebook 1 Huffman codeword from `reader`, returning -/// the codeword index in `0..=80`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 81-entry table. The table is -/// small (max codeword length 11 bits, 81 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 11 bits (Kraft -/// equality `Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹`), so any 11-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 11 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod1_is_complete` regression test that exhaustively -/// walks all `2¹¹` 11-bit prefixes. -pub fn hcod1_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD1_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD1.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD1 is a complete 11-bit prefix code. The - // `hcod1_is_complete` regression test verifies every 11-bit - // prefix maps to exactly one entry. - unreachable!("HCOD1 is a complete 11-bit prefix code; the 11-bit walk must match"); -} - -/// Write a Codebook 1 codeword to `writer` by index. -/// -/// Convenience over `hcod1_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. -pub fn hcod1_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod1_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.3 — Spectrum Huffman Codebook 2 -// ============================================================================= -// -// 81 entries, indices 0..=80. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the wire -// codeword at bit `length − 1`). Transcribed verbatim from ISO/IEC -// 14496-3:2001(E) §4.A.1 Table 4.A.3 (page 194). -// -// The codebook is a complete prefix code: Σᵢ 2^(9 − Lᵢ) = 512 = 2⁹. -// This is exhaustively verified by the `hcod2_is_complete` regression -// test (which walks every 9-bit prefix and asserts each maps to -// exactly one index). -// -// The signed-tuple universe is identical to Codebook 1's (3^4 = 81 -// signed 4-tuples with each element in `-1..=+1`); the §4.6.3.3 index -// translation in [`crate::spectral_codebook`] is reused as-is. - -/// Number of entries in Table 4.A.3 (`81`, indices `0..=80`). -pub const HCOD2_NUM_ENTRIES: usize = 81; - -/// Maximum codeword length emitted by Table 4.A.3 (9 bits). -pub const HCOD2_MAX_LEN: u32 = 9; - -/// Table 4.A.3 — `(length_in_bits, codeword)` per index `0..=80`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD2: [(u8, u16); HCOD2_NUM_ENTRIES] = [ - (9, 0x1f3), // 0 - (7, 0x6f), // 1 - (9, 0x1fd), // 2 - (8, 0xeb), // 3 - (6, 0x23), // 4 - (8, 0xea), // 5 - (9, 0x1f7), // 6 - (8, 0xe8), // 7 - (9, 0x1fa), // 8 - (8, 0xf2), // 9 - (6, 0x2d), // 10 - (7, 0x70), // 11 - (6, 0x20), // 12 - (5, 0x06), // 13 - (6, 0x2b), // 14 - (7, 0x6e), // 15 - (6, 0x28), // 16 - (8, 0xe9), // 17 - (9, 0x1f9), // 18 - (7, 0x66), // 19 - (8, 0xf8), // 20 - (8, 0xe7), // 21 - (6, 0x1b), // 22 - (8, 0xf1), // 23 - (9, 0x1f4), // 24 - (7, 0x6b), // 25 - (9, 0x1f5), // 26 - (8, 0xec), // 27 - (6, 0x2a), // 28 - (7, 0x6c), // 29 - (6, 0x2c), // 30 - (5, 0x0a), // 31 - (6, 0x27), // 32 - (7, 0x67), // 33 - (6, 0x1a), // 34 - (8, 0xf5), // 35 - (6, 0x24), // 36 - (5, 0x08), // 37 - (6, 0x1f), // 38 - (5, 0x09), // 39 - (3, 0x000), // 40 — zero-tuple, 3-bit codeword `0` - (5, 0x07), // 41 - (6, 0x1d), // 42 - (5, 0x0b), // 43 - (6, 0x30), // 44 - (8, 0xef), // 45 - (6, 0x1c), // 46 - (7, 0x64), // 47 - (6, 0x1e), // 48 - (5, 0x0c), // 49 - (6, 0x29), // 50 - (8, 0xf3), // 51 - (6, 0x2f), // 52 - (8, 0xf0), // 53 - (9, 0x1fc), // 54 - (7, 0x71), // 55 - (9, 0x1f2), // 56 - (8, 0xf4), // 57 - (6, 0x21), // 58 - (8, 0xe6), // 59 - (8, 0xf7), // 60 - (7, 0x68), // 61 - (9, 0x1f8), // 62 - (8, 0xee), // 63 - (6, 0x22), // 64 - (7, 0x65), // 65 - (6, 0x31), // 66 - (4, 0x02), // 67 - (6, 0x26), // 68 - (8, 0xed), // 69 - (6, 0x25), // 70 - (7, 0x6a), // 71 - (9, 0x1fb), // 72 - (7, 0x72), // 73 - (9, 0x1fe), // 74 - (7, 0x69), // 75 - (6, 0x2e), // 76 - (8, 0xf6), // 77 - (9, 0x1ff), // 78 - (7, 0x6d), // 79 - (9, 0x1f6), // 80 -]; - -/// Encode a Codebook 2 codeword index (`0..=80`) to the wire Huffman -/// codeword from Table 4.A.3. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the -/// codebook number `2`; the legal range is `0..=80` (the 81-entry -/// `3^4` enumeration of every legal signed 4-tuple with each -/// coefficient in `-1..=+1` — the same universe as Codebook 1). -/// -/// The inverse of [`hcod2_decode`]. -pub fn hcod2_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD2 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(2))?; - Ok(*entry) -} - -/// Decode one Codebook 2 Huffman codeword from `reader`, returning -/// the codeword index in `0..=80`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 81-entry table. The table is -/// small (max codeword length 9 bits, 81 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 9 bits (Kraft -/// equality `Σᵢ 2^(9 − Lᵢ) = 512 = 2⁹`), so any 9-bit prefix fully -/// read from `reader` is guaranteed to match exactly one entry — the -/// bottom of the loop is unreachable when `reader` produces 9 bits -/// without underflowing. A purely defensive `unreachable!()` guards -/// the loop fall-through; it is verified dead by the -/// `hcod2_is_complete` regression test that exhaustively walks all -/// `2⁹` 9-bit prefixes. -pub fn hcod2_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD2_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD2.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD2 is a complete 9-bit prefix code. The - // `hcod2_is_complete` regression test verifies every 9-bit - // prefix maps to exactly one entry. - unreachable!("HCOD2 is a complete 9-bit prefix code; the 9-bit walk must match"); -} - -/// Write a Codebook 2 codeword to `writer` by index. -/// -/// Convenience over `hcod2_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. -pub fn hcod2_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod2_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.4 — Spectrum Huffman Codebook 3 -// ============================================================================= -// -// 81 entries, indices 0..=80. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the -// wire codeword at bit `length − 1`). Transcribed verbatim from -// ISO/IEC 14496-3:2009(E) §4.A.1 Table 4.A.4. -// -// The codebook is a complete prefix code: Σᵢ 2^(16 − Lᵢ) = 65536 = 2¹⁶. -// This is exhaustively verified by the `hcod3_is_complete` regression -// test (which walks every 16-bit prefix and asserts each maps to -// exactly one index). -// -// Codebook 3 is the first *unsigned* spectrum book: each tuple -// coefficient is a non-negative magnitude in `0..=LAV = 0..=2`, and -// the §4.6.3.3 sign-bit suffix carries the sign of each non-zero -// coefficient outside the Huffman codeword. The §4.6.3.3 index ↔ -// 4-tuple translation lives in -// [`crate::spectral_codebook::decode_index_to_tuple`] / -// [`crate::spectral_codebook::encode_tuple_to_index`]; the sign-bit -// suffix lives in -// [`crate::spectral_codebook::apply_sign_bits`] / -// [`crate::spectral_codebook::derive_sign_bits`]. - -/// Number of entries in Table 4.A.4 (`81`, indices `0..=80`). -pub const HCOD3_NUM_ENTRIES: usize = 81; - -/// Maximum codeword length emitted by Table 4.A.4 (16 bits). -pub const HCOD3_MAX_LEN: u32 = 16; - -/// Table 4.A.4 — `(length_in_bits, codeword)` per index `0..=80`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD3: [(u8, u16); HCOD3_NUM_ENTRIES] = [ - (1, 0x0000), // 0 — zero-tuple, single bit `0` - (4, 0x0009), // 1 - (8, 0x00ef), // 2 - (4, 0x000b), // 3 - (5, 0x0019), // 4 - (8, 0x00f0), // 5 - (9, 0x01eb), // 6 - (9, 0x01e6), // 7 - (10, 0x03f2), // 8 - (4, 0x000a), // 9 - (6, 0x0035), // 10 - (9, 0x01ef), // 11 - (6, 0x0034), // 12 - (6, 0x0037), // 13 - (9, 0x01e9), // 14 - (9, 0x01ed), // 15 - (9, 0x01e7), // 16 - (10, 0x03f3), // 17 - (9, 0x01ee), // 18 - (10, 0x03ed), // 19 - (13, 0x1ffa), // 20 - (9, 0x01ec), // 21 - (9, 0x01f2), // 22 - (11, 0x07f9), // 23 - (11, 0x07f8), // 24 - (10, 0x03f8), // 25 - (12, 0x0ff8), // 26 - (4, 0x0008), // 27 - (6, 0x0038), // 28 - (10, 0x03f6), // 29 - (6, 0x0036), // 30 - (7, 0x0075), // 31 - (10, 0x03f1), // 32 - (10, 0x03eb), // 33 - (10, 0x03ec), // 34 - (12, 0x0ff4), // 35 - (5, 0x0018), // 36 - (7, 0x0076), // 37 - (11, 0x07f4), // 38 - (6, 0x0039), // 39 - (7, 0x0074), // 40 - (10, 0x03ef), // 41 - (9, 0x01f3), // 42 - (9, 0x01f4), // 43 - (11, 0x07f6), // 44 - (9, 0x01e8), // 45 - (10, 0x03ea), // 46 - (13, 0x1ffc), // 47 - (8, 0x00f2), // 48 - (9, 0x01f1), // 49 - (12, 0x0ffb), // 50 - (10, 0x03f5), // 51 - (11, 0x07f3), // 52 - (12, 0x0ffc), // 53 - (8, 0x00ee), // 54 - (10, 0x03f7), // 55 - (15, 0x7ffe), // 56 - (9, 0x01f0), // 57 - (11, 0x07f5), // 58 - (15, 0x7ffd), // 59 - (13, 0x1ffb), // 60 - (14, 0x3ffa), // 61 - (16, 0xffff), // 62 - (8, 0x00f1), // 63 - (10, 0x03f0), // 64 - (14, 0x3ffc), // 65 - (9, 0x01ea), // 66 - (10, 0x03ee), // 67 - (14, 0x3ffb), // 68 - (12, 0x0ff6), // 69 - (12, 0x0ffa), // 70 - (15, 0x7ffc), // 71 - (11, 0x07f2), // 72 - (12, 0x0ff5), // 73 - (16, 0xfffe), // 74 - (10, 0x03f4), // 75 - (11, 0x07f7), // 76 - (15, 0x7ffb), // 77 - (12, 0x0ff7), // 78 - (12, 0x0ff9), // 79 - (15, 0x7ffa), // 80 -]; - -/// Encode a Codebook 3 codeword index (`0..=80`) to the wire Huffman -/// codeword from Table 4.A.4. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the -/// codebook number `3`; the legal range is `0..=80` (the 81-entry -/// `3^4` enumeration of every legal unsigned 4-tuple with each -/// coefficient in `0..=LAV = 0..=2`). -/// -/// The inverse of [`hcod3_decode`]. The sign-bit suffix for each -/// non-zero coefficient is *not* part of the returned codeword — the -/// caller emits sign bits separately per -/// [`crate::spectral_codebook::derive_sign_bits`]. -pub fn hcod3_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD3 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(3))?; - Ok(*entry) -} - -/// Decode one Codebook 3 Huffman codeword from `reader`, returning -/// the codeword index in `0..=80`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 81-entry table. The table is -/// small (max codeword length 16 bits, 81 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 16 bits (Kraft -/// equality `Σᵢ 2^(16 − Lᵢ) = 65536 = 2¹⁶`), so any 16-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 16 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod3_is_complete` regression test that exhaustively -/// walks all `2¹⁶` 16-bit prefixes. -/// -/// The sign-bit suffix for non-zero coefficients is *not* consumed -/// here — the caller pairs the returned index with the §4.6.3.3 -/// translation and then reads exactly one sign bit per non-zero -/// coefficient in low-frequency-first order. -pub fn hcod3_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD3_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD3.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD3 is a complete 16-bit prefix code. The - // `hcod3_is_complete` regression test verifies every 16-bit - // prefix maps to exactly one entry. - unreachable!("HCOD3 is a complete 16-bit prefix code; the 16-bit walk must match"); -} - -/// Write a Codebook 3 codeword to `writer` by index. -/// -/// Convenience over `hcod3_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. The -/// caller is responsible for emitting the §4.6.3.3 sign bits for -/// every non-zero coefficient after this call. -pub fn hcod3_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod3_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.5 — Spectrum Huffman Codebook 4 -// ============================================================================= -// -// 81 entries, indices 0..=80. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the -// wire codeword at bit `length − 1`). Transcribed verbatim from -// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.5. -// -// The codebook is a complete prefix code: Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹². -// This is exhaustively verified by the `hcod4_is_complete` regression -// test (which walks every 12-bit prefix and asserts each maps to -// exactly one index). -// -// Codebook 4 shares Codebook 3's unsigned dim-4 LAV-2 tuple universe -// (Table 4.95 row 4 = row 3 except for the source-table column); -// the §4.6.3.3 index ↔ 4-tuple translation in -// [`crate::spectral_codebook`] is reused as-is. The §4.6.3.3 sign-bit -// suffix lives in [`crate::spectral_codebook::apply_sign_bits`] / -// [`crate::spectral_codebook::derive_sign_bits`]. - -/// Number of entries in Table 4.A.5 (`81`, indices `0..=80`). -pub const HCOD4_NUM_ENTRIES: usize = 81; - -/// Maximum codeword length emitted by Table 4.A.5 (12 bits). -pub const HCOD4_MAX_LEN: u32 = 12; - -/// Table 4.A.5 — `(length_in_bits, codeword)` per index `0..=80`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD4: [(u8, u16); HCOD4_NUM_ENTRIES] = [ - (4, 0x007), // 0 - (5, 0x016), // 1 - (8, 0x0f6), // 2 - (5, 0x018), // 3 - (4, 0x008), // 4 - (8, 0x0ef), // 5 - (9, 0x1ef), // 6 - (8, 0x0f3), // 7 - (11, 0x7f8), // 8 - (5, 0x019), // 9 - (5, 0x017), // 10 - (8, 0x0ed), // 11 - (5, 0x015), // 12 - (4, 0x001), // 13 - (8, 0x0e2), // 14 - (8, 0x0f0), // 15 - (7, 0x070), // 16 - (10, 0x3f0), // 17 - (9, 0x1ee), // 18 - (8, 0x0f1), // 19 - (11, 0x7fa), // 20 - (8, 0x0ee), // 21 - (8, 0x0e4), // 22 - (10, 0x3f2), // 23 - (11, 0x7f6), // 24 - (10, 0x3ef), // 25 - (11, 0x7fd), // 26 - (4, 0x005), // 27 - (5, 0x014), // 28 - (8, 0x0f2), // 29 - (4, 0x009), // 30 - (4, 0x004), // 31 - (8, 0x0e5), // 32 - (8, 0x0f4), // 33 - (8, 0x0e8), // 34 - (10, 0x3f4), // 35 - (4, 0x006), // 36 - (4, 0x002), // 37 - (8, 0x0e7), // 38 - (4, 0x003), // 39 - (4, 0x000), // 40 — shortest codeword in Codebook 4 - (7, 0x06b), // 41 - (8, 0x0e3), // 42 - (7, 0x069), // 43 - (9, 0x1f3), // 44 - (8, 0x0eb), // 45 - (8, 0x0e6), // 46 - (10, 0x3f6), // 47 - (7, 0x06e), // 48 - (7, 0x06a), // 49 - (9, 0x1f4), // 50 - (10, 0x3ec), // 51 - (9, 0x1f0), // 52 - (10, 0x3f9), // 53 - (8, 0x0f5), // 54 - (8, 0x0ec), // 55 - (11, 0x7fb), // 56 - (8, 0x0ea), // 57 - (7, 0x06f), // 58 - (10, 0x3f7), // 59 - (11, 0x7f9), // 60 - (10, 0x3f3), // 61 - (12, 0xfff), // 62 - (8, 0x0e9), // 63 - (7, 0x06d), // 64 - (10, 0x3f8), // 65 - (7, 0x06c), // 66 - (7, 0x068), // 67 - (9, 0x1f5), // 68 - (10, 0x3ee), // 69 - (9, 0x1f2), // 70 - (11, 0x7f4), // 71 - (11, 0x7f7), // 72 - (10, 0x3f1), // 73 - (12, 0xffe), // 74 - (10, 0x3ed), // 75 - (9, 0x1f1), // 76 - (11, 0x7f5), // 77 - (11, 0x7fe), // 78 - (10, 0x3f5), // 79 - (11, 0x7fc), // 80 -]; - -/// Encode a Codebook 4 codeword index (`0..=80`) to the wire Huffman -/// codeword from Table 4.A.5. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the -/// codebook number `4`; the legal range is `0..=80` (the 81-entry -/// `3^4` enumeration of every legal unsigned 4-tuple with each -/// coefficient in `0..=LAV = 0..=2` — the same universe as Codebook -/// 3). -/// -/// The inverse of [`hcod4_decode`]. The sign-bit suffix for each -/// non-zero coefficient is *not* part of the returned codeword — the -/// caller emits sign bits separately per -/// [`crate::spectral_codebook::derive_sign_bits`]. -pub fn hcod4_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD4 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(4))?; - Ok(*entry) -} - -/// Decode one Codebook 4 Huffman codeword from `reader`, returning -/// the codeword index in `0..=80`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 81-entry table. The table is -/// small (max codeword length 12 bits, 81 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 12 bits (Kraft -/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 12 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod4_is_complete` regression test that exhaustively -/// walks all `2¹²` 12-bit prefixes. -/// -/// The sign-bit suffix for non-zero coefficients is *not* consumed -/// here — the caller pairs the returned index with the §4.6.3.3 -/// translation and then reads exactly one sign bit per non-zero -/// coefficient in low-frequency-first order. -pub fn hcod4_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD4_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD4.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD4 is a complete 12-bit prefix code. The - // `hcod4_is_complete` regression test verifies every 12-bit - // prefix maps to exactly one entry. - unreachable!("HCOD4 is a complete 12-bit prefix code; the 12-bit walk must match"); -} - -/// Write a Codebook 4 codeword to `writer` by index. -/// -/// Convenience over `hcod4_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. The -/// caller is responsible for emitting the §4.6.3.3 sign bits for -/// every non-zero coefficient after this call. -pub fn hcod4_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod4_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.6 — Spectrum Huffman Codebook 5 -// ============================================================================= -// -// 81 entries, indices 0..=80. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the -// wire codeword at bit `length − 1`). Transcribed verbatim from -// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.6. -// -// The codebook is a complete prefix code: Σᵢ 2^(13 − Lᵢ) = 8192 = 2¹³. -// This is exhaustively verified by the `hcod5_is_complete` regression -// test (which walks every 13-bit prefix and asserts each maps to -// exactly one index). -// -// Codebook 5 is the first **pair** spectrum book (Table 4.95 row 5: -// `unsigned_cb = 0`, `dim = 2`, `LAV = 4`). Per §4.6.3.3 the -// index↔tuple translation evaluates `idx = (y + LAV) * 9 + (z + LAV)` -// so the signed pair lattice spans `(-4, -4) .. (+4, +4)` and the -// zero-tuple `(0, 0)` lands at the centre row index 40. The -// [`crate::spectral_codebook`] §4.6.3.3 dispatcher already handles -// the dim=2 path; this module owns only the codeword wire layer. -// Because Codebook 5 is signed, no sign-bit suffix follows the -// codeword on the wire — the index alone fully specifies the -// signed pair. - -/// Number of entries in Table 4.A.6 (`81`, indices `0..=80`). -pub const HCOD5_NUM_ENTRIES: usize = 81; - -/// Maximum codeword length emitted by Table 4.A.6 (13 bits). -pub const HCOD5_MAX_LEN: u32 = 13; - -/// Table 4.A.6 — `(length_in_bits, codeword)` per index `0..=80`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD5: [(u8, u16); HCOD5_NUM_ENTRIES] = [ - (13, 0x1fff), // 0 — (y, z) = (-4, -4); one of the four 13-bit corners - (12, 0xff7), // 1 - (11, 0x7f4), // 2 - (11, 0x7e8), // 3 - (10, 0x3f1), // 4 - (11, 0x7ee), // 5 - (11, 0x7f9), // 6 - (12, 0xff8), // 7 - (13, 0x1ffd), // 8 — (y, z) = (-4, +4); 13-bit corner - (12, 0xffd), // 9 - (11, 0x7f1), // 10 - (10, 0x3e8), // 11 - (9, 0x1e8), // 12 - (8, 0xf0), // 13 - (9, 0x1ec), // 14 - (10, 0x3ee), // 15 - (11, 0x7f2), // 16 - (12, 0xffa), // 17 - (12, 0xff4), // 18 - (10, 0x3ef), // 19 - (9, 0x1f2), // 20 - (8, 0xe8), // 21 - (7, 0x70), // 22 - (8, 0xec), // 23 - (9, 0x1f0), // 24 - (10, 0x3ea), // 25 - (11, 0x7f3), // 26 - (11, 0x7eb), // 27 - (9, 0x1eb), // 28 - (8, 0xea), // 29 - (5, 0x1a), // 30 - (4, 0x8), // 31 - (5, 0x19), // 32 - (8, 0xee), // 33 - (9, 0x1ef), // 34 - (11, 0x7ed), // 35 - (10, 0x3f0), // 36 - (8, 0xf2), // 37 - (7, 0x73), // 38 - (4, 0xb), // 39 - (1, 0x0), // 40 — (y, z) = (0, 0); single-bit zero codeword - (4, 0xa), // 41 - (7, 0x71), // 42 - (8, 0xf3), // 43 - (11, 0x7e9), // 44 - (11, 0x7ef), // 45 - (9, 0x1ee), // 46 - (8, 0xef), // 47 - (5, 0x18), // 48 - (4, 0x9), // 49 - (5, 0x1b), // 50 - (8, 0xeb), // 51 - (9, 0x1e9), // 52 - (11, 0x7ec), // 53 - (11, 0x7f6), // 54 - (10, 0x3eb), // 55 - (9, 0x1f3), // 56 - (8, 0xed), // 57 - (7, 0x72), // 58 - (8, 0xe9), // 59 - (9, 0x1f1), // 60 - (10, 0x3ed), // 61 - (11, 0x7f7), // 62 - (12, 0xff6), // 63 - (11, 0x7f0), // 64 - (10, 0x3e9), // 65 - (9, 0x1ed), // 66 - (8, 0xf1), // 67 - (9, 0x1ea), // 68 - (10, 0x3ec), // 69 - (11, 0x7f8), // 70 - (12, 0xff9), // 71 - (13, 0x1ffc), // 72 — (y, z) = (+4, -4); 13-bit corner - (12, 0xffc), // 73 - (12, 0xff5), // 74 - (11, 0x7ea), // 75 - (10, 0x3f3), // 76 - (10, 0x3f2), // 77 - (11, 0x7f5), // 78 - (12, 0xffb), // 79 - (13, 0x1ffe), // 80 — (y, z) = (+4, +4); 13-bit corner -]; - -/// Encode a Codebook 5 codeword index (`0..=80`) to the wire Huffman -/// codeword from Table 4.A.6. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`] carrying the -/// codebook number `5`; the legal range is `0..=80` (the 81-entry -/// `9^2` enumeration of every legal signed 2-tuple with each -/// coefficient in `-LAV..=+LAV = -4..=+4`). -/// -/// The inverse of [`hcod5_decode`]. Because Codebook 5 is signed, -/// no sign-bit suffix follows the codeword on the wire — the -/// `offset = LAV = 4` shift inside the §4.6.3.3 translation already -/// encodes every coefficient's sign into the index. -pub fn hcod5_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD5 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(5))?; - Ok(*entry) -} - -/// Decode one Codebook 5 Huffman codeword from `reader`, returning -/// the codeword index in `0..=80`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 81-entry table. The table is -/// small (max codeword length 13 bits, 81 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 13 bits (Kraft -/// equality `Σᵢ 2^(13 − Lᵢ) = 8192 = 2¹³`), so any 13-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 13 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod5_is_complete` regression test that exhaustively -/// walks all `2¹³` 13-bit prefixes. -/// -/// No sign-bit suffix is read here — Codebook 5 is signed, so every -/// coefficient's sign is already baked into the index via the -/// `offset = LAV = 4` §4.6.3.3 polynomial. -pub fn hcod5_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD5_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD5.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD5 is a complete 13-bit prefix code. The - // `hcod5_is_complete` regression test verifies every 13-bit - // prefix maps to exactly one entry. - unreachable!("HCOD5 is a complete 13-bit prefix code; the 13-bit walk must match"); -} - -/// Write a Codebook 5 codeword to `writer` by index. -/// -/// Convenience over `hcod5_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. No -/// sign bits follow on the wire (Codebook 5 is signed). -pub fn hcod5_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod5_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.7 — Spectrum Huffman Codebook 6 -// ============================================================================= -// -// 81 entries, indices 0..=80. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the wire -// codeword at bit `length − 1`). Transcribed verbatim from ISO/IEC -// 14496-3:2001(E) §4.A.1 Table 4.A.7. -// -// The codebook is a complete prefix code: Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹. -// This is exhaustively verified by the `hcod6_is_complete` regression -// test (which walks every 11-bit prefix and asserts each maps to -// exactly one index). -// -// Codebook 6 is the second signed pair spectrum book (Table 4.95 row 6: -// `unsigned_cb = 0`, `dim = 2`, `LAV = 4` → `9^2 = 81` entries, each -// coefficient in `-4..=+4`). The §4.6.3.3 polynomial places the -// zero-tuple `(0, 0)` at the centre of the index range (index 40); -// the four `(±4, ±4)` lattice corners sit at indices 0, 8, 72, 80. -// Because Codebook 6 is signed, no sign-bit suffix follows the -// codeword on the wire. - -/// Number of entries in Table 4.A.7 (`81`, indices `0..=80`). -pub const HCOD6_NUM_ENTRIES: usize = 81; - -/// Maximum codeword length emitted by Table 4.A.7 (11 bits). -pub const HCOD6_MAX_LEN: u32 = 11; - -/// Table 4.A.7 — `(length_in_bits, codeword)` per index `0..=80`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD6: [(u8, u16); HCOD6_NUM_ENTRIES] = [ - (11, 0x7fe), // 0 — (y, z) = (-4, -4) - (10, 0x3fd), // 1 - (9, 0x1f1), // 2 - (9, 0x1eb), // 3 - (9, 0x1f4), // 4 - (9, 0x1ea), // 5 - (9, 0x1f0), // 6 - (10, 0x3fc), // 7 - (11, 0x7fd), // 8 — (y, z) = (-4, +4) - (10, 0x3f6), // 9 - (9, 0x1e5), // 10 - (8, 0xea), // 11 - (7, 0x6c), // 12 - (7, 0x71), // 13 - (7, 0x68), // 14 - (8, 0xf0), // 15 - (9, 0x1e6), // 16 - (10, 0x3f7), // 17 - (9, 0x1f3), // 18 - (8, 0xef), // 19 - (6, 0x32), // 20 - (6, 0x27), // 21 - (6, 0x28), // 22 - (6, 0x26), // 23 - (6, 0x31), // 24 - (8, 0xeb), // 25 - (9, 0x1f7), // 26 - (9, 0x1e8), // 27 - (7, 0x6f), // 28 - (6, 0x2e), // 29 - (4, 0x8), // 30 - (4, 0x4), // 31 - (4, 0x6), // 32 - (6, 0x29), // 33 - (7, 0x6b), // 34 - (9, 0x1ee), // 35 - (9, 0x1ef), // 36 - (7, 0x72), // 37 - (6, 0x2d), // 38 - (4, 0x2), // 39 - (4, 0x0), // 40 — zero-tuple (y, z) = (0, 0), 4-bit `0b0000` - (4, 0x3), // 41 - (6, 0x2f), // 42 - (7, 0x73), // 43 - (9, 0x1fa), // 44 - (9, 0x1e7), // 45 - (7, 0x6e), // 46 - (6, 0x2b), // 47 - (4, 0x7), // 48 - (4, 0x1), // 49 - (4, 0x5), // 50 - (6, 0x2c), // 51 - (7, 0x6d), // 52 - (9, 0x1ec), // 53 - (9, 0x1f9), // 54 - (8, 0xee), // 55 - (6, 0x30), // 56 - (6, 0x24), // 57 - (6, 0x2a), // 58 - (6, 0x25), // 59 - (6, 0x33), // 60 - (8, 0xec), // 61 - (9, 0x1f2), // 62 - (10, 0x3f8), // 63 - (9, 0x1e4), // 64 - (8, 0xed), // 65 - (7, 0x6a), // 66 - (7, 0x70), // 67 - (7, 0x69), // 68 - (7, 0x74), // 69 - (8, 0xf1), // 70 - (10, 0x3fa), // 71 - (11, 0x7ff), // 72 — (y, z) = (+4, -4) - (10, 0x3f9), // 73 - (9, 0x1f6), // 74 - (9, 0x1ed), // 75 - (9, 0x1f8), // 76 - (9, 0x1e9), // 77 - (9, 0x1f5), // 78 - (10, 0x3fb), // 79 - (11, 0x7fc), // 80 — (y, z) = (+4, +4) -]; - -/// Encode a Codebook 6 codeword index (`0..=80`) to the wire Huffman -/// codeword from Table 4.A.7. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=80` (the 81-entry `9^2` enumeration of every legal -/// signed pair with each coefficient in `-4..=+4`). -/// -/// The inverse of [`hcod6_decode`]. Because Codebook 6 is signed, -/// each tuple coefficient's sign is already encoded in the index via -/// the §4.6.3.3 `offset = LAV = 4` shift — no sign-bit suffix is -/// emitted after the codeword. -pub fn hcod6_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD6 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(6))?; - Ok(*entry) -} - -/// Decode one Codebook 6 Huffman codeword from `reader`, returning -/// the codeword index in `0..=80`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 81-entry table. The table is -/// small (max codeword length 11 bits, 81 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 11 bits (Kraft -/// equality `Σᵢ 2^(11 − Lᵢ) = 2048 = 2¹¹`), so any 11-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 11 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod6_is_complete` regression test that exhaustively -/// walks all `2¹¹` 11-bit prefixes. -/// -/// No sign-bit suffix is read here — Codebook 6 is signed, so every -/// `(y, z)` pair carries its sign inside the §4.6.3.3 index via the -/// `offset = LAV = 4` shift. -pub fn hcod6_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD6_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD6.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD6 is a complete 11-bit prefix code. The - // `hcod6_is_complete` regression test verifies every 11-bit - // prefix maps to exactly one entry. - unreachable!("HCOD6 is a complete 11-bit prefix code; the 11-bit walk must match"); -} - -/// Write a Codebook 6 codeword to `writer` by index. -/// -/// Convenience over `hcod6_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 80`. No -/// sign bits follow on the wire (Codebook 6 is signed). -pub fn hcod6_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod6_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.8 — Spectrum Huffman Codebook 7 -// ============================================================================= -// -// 64 entries, indices 0..=63. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the -// wire codeword at bit `length − 1`). Transcribed verbatim from -// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.8. -// -// The codebook is a complete prefix code: Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹². -// This is exhaustively verified by the `hcod7_is_complete` regression -// test (which walks every 12-bit prefix and asserts each maps to -// exactly one entry). -// -// Codebook 7 is the first unsigned pair spectrum book (Table 4.95 row 7: -// `unsigned_cb = 1`, `dim = 2`, `LAV = 7` → `8^2 = 64` entries, each -// coefficient in `0..=7`). The §4.6.3.3 polynomial -// `idx = y * (LAV + 1) + z = y * 8 + z` places the zero-tuple `(0, 0)` -// at index 0 (the origin of the unsigned dim-2 lattice) and the maximum -// tuple `(7, 7)` at index 63 (the far corner). Because Codebook 7 is -// unsigned, a sign-bit suffix follows the Huffman codeword for every -// non-zero coefficient per §4.6.3.3 — the suffix is delivered by -// `crate::spectral_codebook::apply_sign_bits` / -// `crate::spectral_codebook::derive_sign_bits`, separate from the -// Huffman codeword carried here. - -/// Number of entries in Table 4.A.8 (`64`, indices `0..=63`). -pub const HCOD7_NUM_ENTRIES: usize = 64; - -/// Maximum codeword length emitted by Table 4.A.8 (12 bits). -pub const HCOD7_MAX_LEN: u32 = 12; - -/// Table 4.A.8 — `(length_in_bits, codeword)` per index `0..=63`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD7: [(u8, u16); HCOD7_NUM_ENTRIES] = [ - (1, 0x000), // 0 — zero-tuple (y, z) = (0, 0), 1-bit `0` - (3, 0x005), // 1 - (6, 0x037), // 2 - (7, 0x074), // 3 - (8, 0x0f2), // 4 - (9, 0x1eb), // 5 - (10, 0x3ed), // 6 - (11, 0x7f7), // 7 - (3, 0x004), // 8 - (4, 0x00c), // 9 - (6, 0x035), // 10 - (7, 0x071), // 11 - (8, 0x0ec), // 12 - (8, 0x0ee), // 13 - (9, 0x1ee), // 14 - (9, 0x1f5), // 15 - (6, 0x036), // 16 - (6, 0x034), // 17 - (7, 0x072), // 18 - (8, 0x0ea), // 19 - (8, 0x0f1), // 20 - (9, 0x1e9), // 21 - (9, 0x1f3), // 22 - (10, 0x3f5), // 23 - (7, 0x073), // 24 - (7, 0x070), // 25 - (8, 0x0eb), // 26 - (8, 0x0f0), // 27 - (9, 0x1f1), // 28 - (9, 0x1f0), // 29 - (10, 0x3ec), // 30 - (10, 0x3fa), // 31 - (8, 0x0f3), // 32 - (8, 0x0ed), // 33 - (9, 0x1e8), // 34 - (9, 0x1ef), // 35 - (10, 0x3ef), // 36 - (10, 0x3f1), // 37 - (10, 0x3f9), // 38 - (11, 0x7fb), // 39 - (9, 0x1ed), // 40 - (8, 0x0ef), // 41 - (9, 0x1ea), // 42 - (9, 0x1f2), // 43 - (10, 0x3f3), // 44 - (10, 0x3f8), // 45 - (11, 0x7f9), // 46 - (11, 0x7fc), // 47 - (10, 0x3ee), // 48 - (9, 0x1ec), // 49 - (9, 0x1f4), // 50 - (10, 0x3f4), // 51 - (10, 0x3f7), // 52 - (11, 0x7f8), // 53 - (12, 0xffd), // 54 - (12, 0xffe), // 55 - (11, 0x7f6), // 56 - (10, 0x3f0), // 57 - (10, 0x3f2), // 58 - (10, 0x3f6), // 59 - (11, 0x7fa), // 60 - (11, 0x7fd), // 61 - (12, 0xffc), // 62 - (12, 0xfff), // 63 — far corner (y, z) = (7, 7) -]; - -/// Encode a Codebook 7 codeword index (`0..=63`) to the wire Huffman -/// codeword from Table 4.A.8. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=63` (the 64-entry `8^2` enumeration of every legal -/// unsigned pair with each coefficient in `0..=7`). -/// -/// The inverse of [`hcod7_decode`]. Because Codebook 7 is unsigned, -/// callers transmit one sign bit after the codeword for each non-zero -/// coefficient via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried -/// here. -pub fn hcod7_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD7 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(7))?; - Ok(*entry) -} - -/// Decode one Codebook 7 Huffman codeword from `reader`, returning -/// the codeword index in `0..=63`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 64-entry table. The table is -/// small (max codeword length 12 bits, 64 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 12 bits (Kraft -/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 12 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod7_is_complete` regression test that exhaustively -/// walks all `2¹²` 12-bit prefixes. -/// -/// The §4.6.3.3 sign-bit suffix lies outside this routine — for -/// unsigned Codebook 7 the caller consumes one sign bit per non-zero -/// coefficient after the Huffman codeword via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). -pub fn hcod7_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD7_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD7.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD7 is a complete 12-bit prefix code. The - // `hcod7_is_complete` regression test verifies every 12-bit - // prefix maps to exactly one entry. - unreachable!("HCOD7 is a complete 12-bit prefix code; the 12-bit walk must match"); -} - -/// Write a Codebook 7 codeword to `writer` by index. -/// -/// Convenience over `hcod7_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 63`. The -/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one -/// suffix bit per non-zero coefficient, low-frequency-first). -pub fn hcod7_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod7_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ============================================================================= -// Table 4.A.9 — Spectrum Huffman Codebook 8 -// ============================================================================= -// -// 64 entries, indices 0..=63. Each row is `(length_in_bits, -// codeword)` with `codeword` right-aligned in a `u16` (MSB of the -// wire codeword at bit `length − 1`). Transcribed verbatim from -// ISO/IEC 14496-3:2001(E) §4.A.1 Table 4.A.9 (page 198). -// -// The codebook is a complete prefix code: Σᵢ 2^(10 − Lᵢ) = 1024 = 2¹⁰. -// This is exhaustively verified by the `hcod8_is_complete` regression -// test (which walks every 10-bit prefix and asserts each maps to -// exactly one entry). -// -// Codebook 8 is the second unsigned pair spectrum book — it shares -// Codebook 7's Table 4.95 row shape (row 8 column-for-column matches -// row 7 except for the `Codebook listed in Table` cell pointing at -// Table 4.A.9): `unsigned_cb = 1`, `dim = 2`, `LAV = 7` → `(7 + 1)^2 -// = 8^2 = 64` entries, each coefficient in `0..=7`. The §4.6.3.3 -// unsigned polynomial `idx = y * (LAV + 1) + z = y * 8 + z` places -// the zero-tuple `(0, 0)` at index 0, the interior `(1, 1)` at -// index 9, and the far corner `(7, 7)` at index 63 — the same head -// and far-corner placements Codebook 7 also uses for its unsigned -// dim-2 universe. The Huffman-length tuning differs: Codebook 8 -// lifts the zero-tuple off the single-bit codeword (now 5 bits at -// index 0) and migrates the shortest codeword (3 bits `0b000`) to -// the interior tuple `(1, 1)` at index 9. The maximum codeword -// length is 10 bits; exactly four rows reach the ceiling -// (indices 7, 47, 56, 63). -// -// Because Codebook 8 is unsigned, a sign-bit suffix follows the -// Huffman codeword for every non-zero coefficient per §4.6.3.3 — -// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` -// / `crate::spectral_codebook::derive_sign_bits`, separate from the -// Huffman codeword carried here. - -/// Number of entries in Table 4.A.9 (`64`, indices `0..=63`). -pub const HCOD8_NUM_ENTRIES: usize = 64; - -/// Maximum codeword length emitted by Table 4.A.9 (10 bits). -pub const HCOD8_MAX_LEN: u32 = 10; - -/// Table 4.A.9 — `(length_in_bits, codeword)` per index `0..=63`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD8: [(u8, u16); HCOD8_NUM_ENTRIES] = [ - (5, 0x00e), // 0 — zero-tuple (y, z) = (0, 0) - (4, 0x005), // 1 - (5, 0x010), // 2 - (6, 0x030), // 3 - (7, 0x06f), // 4 - (8, 0x0f1), // 5 - (9, 0x1fa), // 6 - (10, 0x3fe), // 7 - (4, 0x003), // 8 - (3, 0x000), // 9 — interior (y, z) = (1, 1), shortest 3-bit `0` - (4, 0x004), // 10 - (5, 0x012), // 11 - (6, 0x02c), // 12 - (7, 0x06a), // 13 - (7, 0x075), // 14 - (8, 0x0f8), // 15 - (5, 0x00f), // 16 - (4, 0x002), // 17 - (4, 0x006), // 18 - (5, 0x014), // 19 - (6, 0x02e), // 20 - (7, 0x069), // 21 - (7, 0x072), // 22 - (8, 0x0f5), // 23 - (6, 0x02f), // 24 - (5, 0x011), // 25 - (5, 0x013), // 26 - (6, 0x02a), // 27 - (6, 0x032), // 28 - (7, 0x06c), // 29 - (8, 0x0ec), // 30 - (8, 0x0fa), // 31 - (7, 0x071), // 32 - (6, 0x02b), // 33 - (6, 0x02d), // 34 - (6, 0x031), // 35 - (7, 0x06d), // 36 - (7, 0x070), // 37 - (8, 0x0f2), // 38 - (9, 0x1f9), // 39 - (8, 0x0ef), // 40 - (7, 0x068), // 41 - (6, 0x033), // 42 - (7, 0x06b), // 43 - (7, 0x06e), // 44 - (8, 0x0ee), // 45 - (8, 0x0f9), // 46 - (10, 0x3fc), // 47 - (9, 0x1f8), // 48 - (7, 0x074), // 49 - (7, 0x073), // 50 - (8, 0x0ed), // 51 - (8, 0x0f0), // 52 - (8, 0x0f6), // 53 - (9, 0x1f6), // 54 - (9, 0x1fd), // 55 - (10, 0x3fd), // 56 - (8, 0x0f3), // 57 - (8, 0x0f4), // 58 - (8, 0x0f7), // 59 - (9, 0x1f7), // 60 - (9, 0x1fb), // 61 - (9, 0x1fc), // 62 - (10, 0x3ff), // 63 — far corner (y, z) = (7, 7) -]; - -/// Encode a Codebook 8 codeword index (`0..=63`) to the wire Huffman -/// codeword from Table 4.A.9. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=63` (the 64-entry `8^2` enumeration of every legal -/// unsigned pair with each coefficient in `0..=7`). -/// -/// The inverse of [`hcod8_decode`]. Because Codebook 8 is unsigned, -/// callers transmit one sign bit after the codeword for each non-zero -/// coefficient via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried -/// here. -pub fn hcod8_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD8 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(8))?; - Ok(*entry) -} - -/// Decode one Codebook 8 Huffman codeword from `reader`, returning -/// the codeword index in `0..=63`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 64-entry table. The table is -/// small (max codeword length 10 bits, 64 entries) so a single -/// linear scan per bit-extend is cheaper than the storage and -/// build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 10 bits (Kraft -/// equality `Σᵢ 2^(10 − Lᵢ) = 1024 = 2¹⁰`), so any 10-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 10 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod8_is_complete` regression test that exhaustively -/// walks all `2¹⁰` 10-bit prefixes. -/// -/// The §4.6.3.3 sign-bit suffix lies outside this routine — for -/// unsigned Codebook 8 the caller consumes one sign bit per non-zero -/// coefficient after the Huffman codeword via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). -pub fn hcod8_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD8_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD8.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD8 is a complete 10-bit prefix code. The - // `hcod8_is_complete` regression test verifies every 10-bit - // prefix maps to exactly one entry. - unreachable!("HCOD8 is a complete 10-bit prefix code; the 10-bit walk must match"); -} - -/// Write a Codebook 8 codeword to `writer` by index. -/// -/// Convenience over `hcod8_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 63`. The -/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one -/// suffix bit per non-zero coefficient, low-frequency-first). -pub fn hcod8_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod8_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ===================================================================== -// Codebook 9 — Table 4.A.10 -// ===================================================================== -// -// Codebook 9 is the first expanded-LAV unsigned pair spectrum book — -// Table 4.95 row 9 declares `unsigned_cb = 1`, `dim = 2`, `LAV = 12`, -// so the §4.6.3.3 universe shifts to `(12 + 1)^2 = 13^2 = 169` -// entries indexed `0..=168` with each `(y, z)` coefficient in -// `0..=12`. That is a substantial step up from Codebooks 7 and 8's -// shared `8 × 8 = 64`-entry unsigned pair lattice — the `169 / 64 ≈ -// 2.6×` universe expansion widens the distribution's tail and lifts -// the codeword ceiling from Codebook 8's 10 bits to **15 bits**, the -// widest non-ESC codeword in the entire Annex 4.A book set. The -// §4.6.3.3 unsigned polynomial `idx = y * (LAV + 1) + z = y * 13 + z` -// places the zero-tuple `(0, 0)` at index 0 and the maximum tuple -// `(12, 12)` at index 168 (`12 * 13 + 12 = 168`). The single-bit -// codeword `0` parks at index 0 — the same shortest-codeword head -// placement Codebook 7 uses for its zero-tuple. Exactly four rows -// reach the 15-bit ceiling (indices 142, 154, 155, 168) — the -// rarest pair magnitudes near the `LAV = 12` cap. -// -// Because Codebook 9 is unsigned, a sign-bit suffix follows the -// Huffman codeword for every non-zero coefficient per §4.6.3.3 — -// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` -// / `crate::spectral_codebook::derive_sign_bits`, separate from the -// Huffman codeword carried here. - -/// Number of entries in Table 4.A.10 (`169`, indices `0..=168`). -pub const HCOD9_NUM_ENTRIES: usize = 169; - -/// Maximum codeword length emitted by Table 4.A.10 (15 bits). -pub const HCOD9_MAX_LEN: u32 = 15; - -/// Table 4.A.10 — `(length_in_bits, codeword)` per index `0..=168`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD9: [(u8, u16); HCOD9_NUM_ENTRIES] = [ - (1, 0x0000), // 0 — zero-tuple (y, z) = (0, 0), shortest 1-bit `0` - (3, 0x0005), // 1 - (6, 0x0037), // 2 - (8, 0x00e7), // 3 - (9, 0x01de), // 4 - (10, 0x03ce), // 5 - (10, 0x03d9), // 6 - (11, 0x07c8), // 7 - (11, 0x07cd), // 8 - (12, 0x0fc8), // 9 - (12, 0x0fdd), // 10 - (13, 0x1fe4), // 11 - (13, 0x1fec), // 12 - (3, 0x0004), // 13 - (4, 0x000c), // 14 — interior (y, z) = (1, 1) - (6, 0x0035), // 15 - (7, 0x0072), // 16 - (8, 0x00ea), // 17 - (8, 0x00ed), // 18 - (9, 0x01e2), // 19 - (10, 0x03d1), // 20 - (10, 0x03d3), // 21 - (10, 0x03e0), // 22 - (11, 0x07d8), // 23 - (12, 0x0fcf), // 24 - (12, 0x0fd5), // 25 - (6, 0x0036), // 26 - (6, 0x0034), // 27 - (7, 0x0071), // 28 - (8, 0x00e8), // 29 - (8, 0x00ec), // 30 - (9, 0x01e1), // 31 - (10, 0x03cf), // 32 - (10, 0x03dd), // 33 - (10, 0x03db), // 34 - (11, 0x07d0), // 35 - (12, 0x0fc7), // 36 - (12, 0x0fd4), // 37 - (12, 0x0fe4), // 38 - (8, 0x00e6), // 39 - (7, 0x0070), // 40 - (8, 0x00e9), // 41 - (9, 0x01dd), // 42 - (9, 0x01e3), // 43 - (10, 0x03d2), // 44 - (10, 0x03dc), // 45 - (11, 0x07cc), // 46 - (11, 0x07ca), // 47 - (11, 0x07de), // 48 - (12, 0x0fd8), // 49 - (12, 0x0fea), // 50 - (13, 0x1fdb), // 51 - (9, 0x01df), // 52 - (8, 0x00eb), // 53 - (9, 0x01dc), // 54 - (9, 0x01e6), // 55 - (10, 0x03d5), // 56 - (10, 0x03de), // 57 - (11, 0x07cb), // 58 - (11, 0x07dd), // 59 - (11, 0x07dc), // 60 - (12, 0x0fcd), // 61 - (12, 0x0fe2), // 62 - (12, 0x0fe7), // 63 - (13, 0x1fe1), // 64 - (10, 0x03d0), // 65 - (9, 0x01e0), // 66 - (9, 0x01e4), // 67 - (10, 0x03d6), // 68 - (11, 0x07c5), // 69 - (11, 0x07d1), // 70 - (11, 0x07db), // 71 - (12, 0x0fd2), // 72 - (11, 0x07e0), // 73 - (12, 0x0fd9), // 74 - (12, 0x0feb), // 75 - (13, 0x1fe3), // 76 - (13, 0x1fe9), // 77 - (11, 0x07c4), // 78 - (9, 0x01e5), // 79 - (10, 0x03d7), // 80 - (11, 0x07c6), // 81 - (11, 0x07cf), // 82 - (11, 0x07da), // 83 - (12, 0x0fcb), // 84 - (12, 0x0fda), // 85 - (12, 0x0fe3), // 86 - (12, 0x0fe9), // 87 - (13, 0x1fe6), // 88 - (13, 0x1ff3), // 89 - (13, 0x1ff7), // 90 - (11, 0x07d3), // 91 - (10, 0x03d8), // 92 - (10, 0x03e1), // 93 - (11, 0x07d4), // 94 - (11, 0x07d9), // 95 - (12, 0x0fd3), // 96 - (12, 0x0fde), // 97 - (13, 0x1fdd), // 98 - (13, 0x1fd9), // 99 - (13, 0x1fe2), // 100 - (13, 0x1fea), // 101 - (13, 0x1ff1), // 102 - (13, 0x1ff6), // 103 - (11, 0x07d2), // 104 - (10, 0x03d4), // 105 - (10, 0x03da), // 106 - (11, 0x07c7), // 107 - (11, 0x07d7), // 108 - (11, 0x07e2), // 109 - (12, 0x0fce), // 110 - (12, 0x0fdb), // 111 - (13, 0x1fd8), // 112 - (13, 0x1fee), // 113 - (14, 0x3ff0), // 114 - (13, 0x1ff4), // 115 - (14, 0x3ff2), // 116 - (11, 0x07e1), // 117 - (10, 0x03df), // 118 - (11, 0x07c9), // 119 - (11, 0x07d6), // 120 - (12, 0x0fca), // 121 - (12, 0x0fd0), // 122 - (12, 0x0fe5), // 123 - (12, 0x0fe6), // 124 - (13, 0x1feb), // 125 - (13, 0x1fef), // 126 - (14, 0x3ff3), // 127 - (14, 0x3ff4), // 128 - (14, 0x3ff5), // 129 - (12, 0x0fe0), // 130 - (11, 0x07ce), // 131 - (11, 0x07d5), // 132 - (12, 0x0fc6), // 133 - (12, 0x0fd1), // 134 - (12, 0x0fe1), // 135 - (13, 0x1fe0), // 136 - (13, 0x1fe8), // 137 - (13, 0x1ff0), // 138 - (14, 0x3ff1), // 139 - (14, 0x3ff8), // 140 - (14, 0x3ff6), // 141 - (15, 0x7ffc), // 142 - (12, 0x0fe8), // 143 - (11, 0x07df), // 144 - (12, 0x0fc9), // 145 - (12, 0x0fd7), // 146 - (12, 0x0fdc), // 147 - (13, 0x1fdc), // 148 - (13, 0x1fdf), // 149 - (13, 0x1fed), // 150 - (13, 0x1ff5), // 151 - (14, 0x3ff9), // 152 - (14, 0x3ffb), // 153 - (15, 0x7ffd), // 154 - (15, 0x7ffe), // 155 - (13, 0x1fe7), // 156 - (12, 0x0fcc), // 157 - (12, 0x0fd6), // 158 - (12, 0x0fdf), // 159 - (13, 0x1fde), // 160 - (13, 0x1fda), // 161 - (13, 0x1fe5), // 162 - (13, 0x1ff2), // 163 - (14, 0x3ffa), // 164 - (14, 0x3ff7), // 165 - (14, 0x3ffc), // 166 - (14, 0x3ffd), // 167 - (15, 0x7fff), // 168 — far corner (y, z) = (12, 12) -]; - -/// Encode a Codebook 9 codeword index (`0..=168`) to the wire Huffman -/// codeword from Table 4.A.10. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=168` (the 169-entry `13^2` enumeration of every -/// legal unsigned pair with each coefficient in `0..=12`). -/// -/// The inverse of [`hcod9_decode`]. Because Codebook 9 is unsigned, -/// callers transmit one sign bit after the codeword for each non-zero -/// coefficient via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried -/// here. -pub fn hcod9_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD9 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(9))?; - Ok(*entry) -} - -/// Decode one Codebook 9 Huffman codeword from `reader`, returning -/// the codeword index in `0..=168`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 169-entry table. The table is -/// small enough (max codeword length 15 bits, 169 entries) that a -/// single linear scan per bit-extend is cheaper than the storage -/// and build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 15 bits (Kraft -/// equality `Σᵢ 2^(15 − Lᵢ) = 32768 = 2¹⁵`), so any 15-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 15 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod9_is_complete` regression test that exhaustively -/// walks all `2¹⁵` 15-bit prefixes. -/// -/// The §4.6.3.3 sign-bit suffix lies outside this routine — for -/// unsigned Codebook 9 the caller consumes one sign bit per non-zero -/// coefficient after the Huffman codeword via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). -pub fn hcod9_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD9_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD9.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD9 is a complete 15-bit prefix code. The - // `hcod9_is_complete` regression test verifies every 15-bit - // prefix maps to exactly one entry. - unreachable!("HCOD9 is a complete 15-bit prefix code; the 15-bit walk must match"); -} - -/// Write a Codebook 9 codeword to `writer` by index. -/// -/// Convenience over `hcod9_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 168`. The -/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one -/// suffix bit per non-zero coefficient, low-frequency-first). -pub fn hcod9_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod9_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ===================================================================== -// Codebook 10 — Table 4.A.11 -// ===================================================================== -// -// Codebook 10 is the second expanded-LAV unsigned pair spectrum book — -// Table 4.95 row 10 mirrors Codebook 9's row 9 column-for-column -// (`unsigned_cb = 1`, `dim = 2`, `LAV = 12`) so the §4.6.3.3 universe -// is the same `13 × 13 = 169`-entry lattice indexed `0..=168` with -// each `(y, z)` coefficient in `0..=12`. The §4.6.3.3 unsigned -// polynomial `idx = y * (LAV + 1) + z = y * 13 + z` places the -// zero-tuple `(0, 0)` at index 0 and the maximum tuple `(12, 12)` at -// index 168 (`12 * 13 + 12 = 168`). Where Codebook 9 parks the -// single-bit codeword `0` on the zero-tuple at index 0, Codebook 10 -// lifts the zero-tuple to a 6-bit `0b100010` (`0x22`) and migrates -// the shortest codeword (4 bits) onto the interior `(1, 1)` at -// index 14 with codeword `0b0000` — the same head-displacement -// pattern Codebook 8 uses to relocate its shortest slot off the -// zero-tuple. Exactly three rows reach the 4-bit floor (indices -// 14, 15, 27 with codewords `0x0`, `0x1`, `0x2`), reflecting an -// encoder target whose magnitude statistics are denser around -// `(±1, ±1) .. (±2, ±2)` than Codebook 9's zero-heavy distribution. -// The maximum codeword length is **12 bits** — a 3-bit pull-down -// from Codebook 9's 15-bit ceiling — and exactly eight rows reach -// that 12-bit ceiling (indices 12, 129, 142, 155, 165, 166, 167, -// 168 with codewords `0xffd, 0xffa, 0xff9, 0xffb, 0xff8, 0xffe, -// 0xffc, 0xfff`), the rarest pair magnitudes near the `LAV = 12` -// cap. -// -// Because Codebook 10 is unsigned, a sign-bit suffix follows the -// Huffman codeword for every non-zero coefficient per §4.6.3.3 — -// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` -// / `crate::spectral_codebook::derive_sign_bits`, separate from the -// Huffman codeword carried here. - -/// Number of entries in Table 4.A.11 (`169`, indices `0..=168`). -pub const HCOD10_NUM_ENTRIES: usize = 169; - -/// Maximum codeword length emitted by Table 4.A.11 (12 bits). -pub const HCOD10_MAX_LEN: u32 = 12; - -/// Table 4.A.11 — `(length_in_bits, codeword)` per index `0..=168`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -const HCOD10: [(u8, u16); HCOD10_NUM_ENTRIES] = [ - (6, 0x0022), // 0 — zero-tuple (y, z) = (0, 0) - (5, 0x0008), // 1 - (6, 0x001d), // 2 - (6, 0x0026), // 3 - (7, 0x005f), // 4 - (8, 0x00d3), // 5 - (9, 0x01cf), // 6 - (10, 0x03d0), // 7 - (10, 0x03d7), // 8 - (10, 0x03ed), // 9 - (11, 0x07f0), // 10 - (11, 0x07f6), // 11 - (12, 0x0ffd), // 12 - (5, 0x0007), // 13 - (4, 0x0000), // 14 — interior (y, z) = (1, 1), shortest 4-bit `0b0000` - (4, 0x0001), // 15 - (5, 0x0009), // 16 - (6, 0x0020), // 17 - (7, 0x0054), // 18 - (7, 0x0060), // 19 - (8, 0x00d5), // 20 - (8, 0x00dc), // 21 - (9, 0x01d4), // 22 - (10, 0x03cd), // 23 - (10, 0x03de), // 24 - (11, 0x07e7), // 25 - (6, 0x001c), // 26 - (4, 0x0002), // 27 - (5, 0x0006), // 28 - (5, 0x000c), // 29 - (6, 0x001e), // 30 - (6, 0x0028), // 31 - (7, 0x005b), // 32 - (8, 0x00cd), // 33 - (8, 0x00d9), // 34 - (9, 0x01ce), // 35 - (9, 0x01dc), // 36 - (10, 0x03d9), // 37 - (10, 0x03f1), // 38 - (6, 0x0025), // 39 - (5, 0x000b), // 40 - (5, 0x000a), // 41 - (5, 0x000d), // 42 - (6, 0x0024), // 43 - (7, 0x0057), // 44 - (7, 0x0061), // 45 - (8, 0x00cc), // 46 - (8, 0x00dd), // 47 - (9, 0x01cc), // 48 - (9, 0x01de), // 49 - (10, 0x03d3), // 50 - (10, 0x03e7), // 51 - (7, 0x005d), // 52 - (6, 0x0021), // 53 - (6, 0x001f), // 54 - (6, 0x0023), // 55 - (6, 0x0027), // 56 - (7, 0x0059), // 57 - (7, 0x0064), // 58 - (8, 0x00d8), // 59 - (8, 0x00df), // 60 - (9, 0x01d2), // 61 - (9, 0x01e2), // 62 - (10, 0x03dd), // 63 - (10, 0x03ee), // 64 - (8, 0x00d1), // 65 - (7, 0x0055), // 66 - (6, 0x0029), // 67 - (7, 0x0056), // 68 - (7, 0x0058), // 69 - (7, 0x0062), // 70 - (8, 0x00ce), // 71 - (8, 0x00e0), // 72 - (8, 0x00e2), // 73 - (9, 0x01da), // 74 - (10, 0x03d4), // 75 - (10, 0x03e3), // 76 - (11, 0x07eb), // 77 - (9, 0x01c9), // 78 - (7, 0x005e), // 79 - (7, 0x005a), // 80 - (7, 0x005c), // 81 - (7, 0x0063), // 82 - (8, 0x00ca), // 83 - (8, 0x00da), // 84 - (9, 0x01c7), // 85 - (9, 0x01ca), // 86 - (9, 0x01e0), // 87 - (10, 0x03db), // 88 - (10, 0x03e8), // 89 - (11, 0x07ec), // 90 - (9, 0x01e3), // 91 - (8, 0x00d2), // 92 - (8, 0x00cb), // 93 - (8, 0x00d0), // 94 - (8, 0x00d7), // 95 - (8, 0x00db), // 96 - (9, 0x01c6), // 97 - (9, 0x01d5), // 98 - (9, 0x01d8), // 99 - (10, 0x03ca), // 100 - (10, 0x03da), // 101 - (11, 0x07ea), // 102 - (11, 0x07f1), // 103 - (9, 0x01e1), // 104 - (8, 0x00d4), // 105 - (8, 0x00cf), // 106 - (8, 0x00d6), // 107 - (8, 0x00de), // 108 - (8, 0x00e1), // 109 - (9, 0x01d0), // 110 - (9, 0x01d6), // 111 - (10, 0x03d1), // 112 - (10, 0x03d5), // 113 - (10, 0x03f2), // 114 - (11, 0x07ee), // 115 - (11, 0x07fb), // 116 - (10, 0x03e9), // 117 - (9, 0x01cd), // 118 - (9, 0x01c8), // 119 - (9, 0x01cb), // 120 - (9, 0x01d1), // 121 - (9, 0x01d7), // 122 - (9, 0x01df), // 123 - (10, 0x03cf), // 124 - (10, 0x03e0), // 125 - (10, 0x03ef), // 126 - (11, 0x07e6), // 127 - (11, 0x07f8), // 128 - (12, 0x0ffa), // 129 - (10, 0x03eb), // 130 - (9, 0x01dd), // 131 - (9, 0x01d3), // 132 - (9, 0x01d9), // 133 - (9, 0x01db), // 134 - (10, 0x03d2), // 135 - (10, 0x03cc), // 136 - (10, 0x03dc), // 137 - (10, 0x03ea), // 138 - (11, 0x07ed), // 139 - (11, 0x07f3), // 140 - (11, 0x07f9), // 141 - (12, 0x0ff9), // 142 - (11, 0x07f2), // 143 - (10, 0x03ce), // 144 - (9, 0x01e4), // 145 - (10, 0x03cb), // 146 - (10, 0x03d8), // 147 - (10, 0x03d6), // 148 - (10, 0x03e2), // 149 - (10, 0x03e5), // 150 - (11, 0x07e8), // 151 - (11, 0x07f4), // 152 - (11, 0x07f5), // 153 - (11, 0x07f7), // 154 - (12, 0x0ffb), // 155 - (11, 0x07fa), // 156 - (10, 0x03ec), // 157 - (10, 0x03df), // 158 - (10, 0x03e1), // 159 - (10, 0x03e4), // 160 - (10, 0x03e6), // 161 - (10, 0x03f0), // 162 - (11, 0x07e9), // 163 - (11, 0x07ef), // 164 - (12, 0x0ff8), // 165 - (12, 0x0ffe), // 166 - (12, 0x0ffc), // 167 - (12, 0x0fff), // 168 — far corner (y, z) = (12, 12) -]; - -/// Encode a Codebook 10 codeword index (`0..=168`) to the wire Huffman -/// codeword from Table 4.A.11. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=168` (the 169-entry `13^2` enumeration of every -/// legal unsigned pair with each coefficient in `0..=12`). -/// -/// The inverse of [`hcod10_decode`]. Because Codebook 10 is unsigned, -/// callers transmit one sign bit after the codeword for each non-zero -/// coefficient via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried -/// here. -pub fn hcod10_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD10 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(10))?; - Ok(*entry) -} - -/// Decode one Codebook 10 Huffman codeword from `reader`, returning -/// the codeword index in `0..=168`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 169-entry table. The table is -/// small enough (max codeword length 12 bits, 169 entries) that a -/// single linear scan per bit-extend is cheaper than the storage -/// and build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 12 bits (Kraft -/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 12 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod10_is_complete` regression test that -/// exhaustively walks all `2¹²` 12-bit prefixes. -/// -/// The §4.6.3.3 sign-bit suffix lies outside this routine — for -/// unsigned Codebook 10 the caller consumes one sign bit per -/// non-zero coefficient after the Huffman codeword via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits). -pub fn hcod10_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD10_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD10.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD10 is a complete 12-bit prefix code. The - // `hcod10_is_complete` regression test verifies every 12-bit - // prefix maps to exactly one entry. - unreachable!("HCOD10 is a complete 12-bit prefix code; the 12-bit walk must match"); -} - -/// Write a Codebook 10 codeword to `writer` by index. -/// -/// Convenience over `hcod10_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 168`. The -/// §4.6.3.3 sign-bit suffix is the caller's responsibility (one -/// suffix bit per non-zero coefficient, low-frequency-first). -pub fn hcod10_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod10_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -// ===================================================================== -// Codebook 11 — Table 4.A.12 -// ===================================================================== -// -// Codebook 11 is the only AAC spectrum book that carries an **escape -// (ESC) sequence**. Table 4.95 row 11 declares `unsigned_cb = 1`, -// `dim = 2`, `LAV = 16` and an ESC threshold of `8191` — the §4.6.1.3 -// `x_quant` ceiling. The in-band Huffman universe is therefore the -// `(LAV + 1)^dim = 17^2 = 289`-entry lattice indexed `0..=288` with -// each `(y, z)` coefficient in `0..=16`. A coefficient value of `16` -// in either `y` or `z` is **not** a literal 16: per §4.6.3.3 it is the -// `escape_flag` that signals an `escape_sequence` follows the -// Huffman codeword (and any sign-bit suffix). The -// `escape_sequence` is a unary `escape_prefix` of N `1` bits, a -// `0` `escape_separator`, and an `(N + 4)`-bit unsigned `escape_word`, -// whose reconstructed magnitude is `2^(N + 4) + escape_word`. The -// ESC bridge sits in [`crate::spectral_codebook::decode_esc_value`] -// / [`crate::spectral_codebook::encode_esc_value`] and is **not** -// part of the Huffman codeword carried here; this module's -// `hcod11_encode` / `hcod11_decode` cover the codeword only. -// -// The §4.6.3.3 unsigned polynomial `idx = y * (LAV + 1) + z = y * 17 -// + z` parks the zero-tuple `(0, 0)` at index 0 with the 4-bit -// codeword `0b0000` — the shortest slot. The interior pair `(1, 1)` -// lives at index `1 * 17 + 1 = 18` and shares the 4-bit floor with -// the zero-tuple (codeword `0b0001`). The far corner `(16, 16)` — -// both coefficients flagged as ESC — lives at index `16 * 17 + 16 = -// 288` with the 5-bit `0b00100` (`0x04`); the `(0, 16)` half-ESC -// tuple lives at index 16 with the 10-bit `0x38e`; the `(16, 0)` -// half-ESC tuple lives at index `16 * 17 = 272` with the 9-bit -// `0x1c2`. The maximum codeword length is **12 bits** — matching -// Codebook 10's ceiling — and exactly six rows reach that 12-bit -// ceiling (indices 12, 14, 15, 255, 269, 270 with codewords -// `0xffb`, `0xffa`, `0xffe`, `0xffd`, `0xffc`, `0xfff`). Exactly two -// rows reach the 4-bit floor: indices 0 and 18 (the zero-tuple and -// the interior `(1, 1)` pair). The codeword-length histogram is -// `{4: 2, 5: 6, 6: 7, 7: 16, 8: 59, 9: 55, 10: 95, 11: 43, 12: 6}`. -// -// Because Codebook 11 is unsigned, a sign-bit suffix follows the -// Huffman codeword for every non-zero coefficient per §4.6.3.3 — -// the suffix is delivered by `crate::spectral_codebook::apply_sign_bits` -// / `crate::spectral_codebook::derive_sign_bits`, separate from the -// Huffman codeword carried here. The §4.6.3.3 wire layout for an -// in-band coefficient pair is: `` then `0..=2` -// sign bits (one per non-zero coefficient). When `y` or `z` is at -// the ESC threshold (`= 16`), the wire layout extends with the -// `escape_sequence` bridge per §4.6.3 (handled outside this -// module). - -/// Number of entries in Table 4.A.12 (`289`, indices `0..=288`). -pub const HCOD11_NUM_ENTRIES: usize = 289; - -/// Maximum codeword length emitted by Table 4.A.12 (12 bits). -pub const HCOD11_MAX_LEN: u32 = 12; - -/// Table 4.A.12 — `(length_in_bits, codeword)` per index `0..=288`. -/// -/// Codewords are right-aligned within the `u16`. To emit one -/// bit-for-bit, write `codeword` as `length` bits MSB-first. -/// -/// A coefficient value of `16` in either slot of the decoded -/// `(y, z)` pair is the §4.6.3.3 `escape_flag` — the actual -/// magnitude is reconstructed by the -/// [`crate::spectral_codebook::decode_esc_value`] bridge from the -/// `escape_sequence` that follows the Huffman codeword (and any -/// sign-bit suffix) on the wire. -const HCOD11: [(u8, u16); HCOD11_NUM_ENTRIES] = [ - (4, 0x0000), // 0 — zero-tuple (y, z) = (0, 0) - (5, 0x0006), // 1 - (6, 0x0019), // 2 - (7, 0x003d), // 3 - (8, 0x009c), // 4 - (8, 0x00c6), // 5 - (9, 0x01a7), // 6 - (10, 0x0390), // 7 - (10, 0x03c2), // 8 - (10, 0x03df), // 9 - (11, 0x07e6), // 10 - (11, 0x07f3), // 11 - (12, 0x0ffb), // 12 - (11, 0x07ec), // 13 - (12, 0x0ffa), // 14 - (12, 0x0ffe), // 15 - (10, 0x038e), // 16 — (y, z) = (0, 16) — z at ESC threshold - (5, 0x0005), // 17 - (4, 0x0001), // 18 — interior (y, z) = (1, 1), shortest 4-bit `0b0000` - (5, 0x0008), // 19 - (6, 0x0014), // 20 - (7, 0x0037), // 21 - (7, 0x0042), // 22 - (8, 0x0092), // 23 - (8, 0x00af), // 24 - (9, 0x0191), // 25 - (9, 0x01a5), // 26 - (9, 0x01b5), // 27 - (10, 0x039e), // 28 - (10, 0x03c0), // 29 - (10, 0x03a2), // 30 - (10, 0x03cd), // 31 - (11, 0x07d6), // 32 - (8, 0x00ae), // 33 - (6, 0x0017), // 34 - (5, 0x0007), // 35 - (5, 0x0009), // 36 - (6, 0x0018), // 37 - (7, 0x0039), // 38 - (7, 0x0040), // 39 - (8, 0x008e), // 40 - (8, 0x00a3), // 41 - (8, 0x00b8), // 42 - (9, 0x0199), // 43 - (9, 0x01ac), // 44 - (9, 0x01c1), // 45 - (10, 0x03b1), // 46 - (10, 0x0396), // 47 - (10, 0x03be), // 48 - (10, 0x03ca), // 49 - (8, 0x009d), // 50 - (7, 0x003c), // 51 - (6, 0x0015), // 52 - (6, 0x0016), // 53 - (6, 0x001a), // 54 - (7, 0x003b), // 55 - (7, 0x0044), // 56 - (8, 0x0091), // 57 - (8, 0x00a5), // 58 - (8, 0x00be), // 59 - (9, 0x0196), // 60 - (9, 0x01ae), // 61 - (9, 0x01b9), // 62 - (10, 0x03a1), // 63 - (10, 0x0391), // 64 - (10, 0x03a5), // 65 - (10, 0x03d5), // 66 - (8, 0x0094), // 67 - (8, 0x009a), // 68 - (7, 0x0036), // 69 - (7, 0x0038), // 70 - (7, 0x003a), // 71 - (7, 0x0041), // 72 - (8, 0x008c), // 73 - (8, 0x009b), // 74 - (8, 0x00b0), // 75 - (8, 0x00c3), // 76 - (9, 0x019e), // 77 - (9, 0x01ab), // 78 - (9, 0x01bc), // 79 - (10, 0x039f), // 80 - (10, 0x038f), // 81 - (10, 0x03a9), // 82 - (10, 0x03cf), // 83 - (8, 0x0093), // 84 - (8, 0x00bf), // 85 - (7, 0x003e), // 86 - (7, 0x003f), // 87 - (7, 0x0043), // 88 - (7, 0x0045), // 89 - (8, 0x009e), // 90 - (8, 0x00a7), // 91 - (8, 0x00b9), // 92 - (9, 0x0194), // 93 - (9, 0x01a2), // 94 - (9, 0x01ba), // 95 - (9, 0x01c3), // 96 - (10, 0x03a6), // 97 - (10, 0x03a7), // 98 - (10, 0x03bb), // 99 - (10, 0x03d4), // 100 - (8, 0x009f), // 101 - (9, 0x01a0), // 102 - (8, 0x008f), // 103 - (8, 0x008d), // 104 - (8, 0x0090), // 105 - (8, 0x0098), // 106 - (8, 0x00a6), // 107 - (8, 0x00b6), // 108 - (8, 0x00c4), // 109 - (9, 0x019f), // 110 - (9, 0x01af), // 111 - (9, 0x01bf), // 112 - (10, 0x0399), // 113 - (10, 0x03bf), // 114 - (10, 0x03b4), // 115 - (10, 0x03c9), // 116 - (10, 0x03e7), // 117 - (8, 0x00a8), // 118 - (9, 0x01b6), // 119 - (8, 0x00ab), // 120 - (8, 0x00a4), // 121 - (8, 0x00aa), // 122 - (8, 0x00b2), // 123 - (8, 0x00c2), // 124 - (8, 0x00c5), // 125 - (9, 0x0198), // 126 - (9, 0x01a4), // 127 - (9, 0x01b8), // 128 - (10, 0x038c), // 129 - (10, 0x03a4), // 130 - (10, 0x03c4), // 131 - (10, 0x03c6), // 132 - (10, 0x03dd), // 133 - (10, 0x03e8), // 134 - (8, 0x00ad), // 135 - (10, 0x03af), // 136 - (9, 0x0192), // 137 - (8, 0x00bd), // 138 - (8, 0x00bc), // 139 - (9, 0x018e), // 140 - (9, 0x0197), // 141 - (9, 0x019a), // 142 - (9, 0x01a3), // 143 - (9, 0x01b1), // 144 - (10, 0x038d), // 145 - (10, 0x0398), // 146 - (10, 0x03b7), // 147 - (10, 0x03d3), // 148 - (10, 0x03d1), // 149 - (10, 0x03db), // 150 - (11, 0x07dd), // 151 - (8, 0x00b4), // 152 - (10, 0x03de), // 153 - (9, 0x01a9), // 154 - (9, 0x019b), // 155 - (9, 0x019c), // 156 - (9, 0x01a1), // 157 - (9, 0x01aa), // 158 - (9, 0x01ad), // 159 - (9, 0x01b3), // 160 - (10, 0x038b), // 161 - (10, 0x03b2), // 162 - (10, 0x03b8), // 163 - (10, 0x03ce), // 164 - (10, 0x03e1), // 165 - (10, 0x03e0), // 166 - (11, 0x07d2), // 167 - (11, 0x07e5), // 168 - (8, 0x00b7), // 169 - (11, 0x07e3), // 170 - (9, 0x01bb), // 171 - (9, 0x01a8), // 172 - (9, 0x01a6), // 173 - (9, 0x01b0), // 174 - (9, 0x01b2), // 175 - (9, 0x01b7), // 176 - (10, 0x039b), // 177 - (10, 0x039a), // 178 - (10, 0x03ba), // 179 - (10, 0x03b5), // 180 - (10, 0x03d6), // 181 - (11, 0x07d7), // 182 - (10, 0x03e4), // 183 - (11, 0x07d8), // 184 - (11, 0x07ea), // 185 - (8, 0x00ba), // 186 - (11, 0x07e8), // 187 - (10, 0x03a0), // 188 - (9, 0x01bd), // 189 - (9, 0x01b4), // 190 - (10, 0x038a), // 191 - (9, 0x01c4), // 192 - (10, 0x0392), // 193 - (10, 0x03aa), // 194 - (10, 0x03b0), // 195 - (10, 0x03bc), // 196 - (10, 0x03d7), // 197 - (11, 0x07d4), // 198 - (11, 0x07dc), // 199 - (11, 0x07db), // 200 - (11, 0x07d5), // 201 - (11, 0x07f0), // 202 - (8, 0x00c1), // 203 - (11, 0x07fb), // 204 - (10, 0x03c8), // 205 - (10, 0x03a3), // 206 - (10, 0x0395), // 207 - (10, 0x039d), // 208 - (10, 0x03ac), // 209 - (10, 0x03ae), // 210 - (10, 0x03c5), // 211 - (10, 0x03d8), // 212 - (10, 0x03e2), // 213 - (10, 0x03e6), // 214 - (11, 0x07e4), // 215 - (11, 0x07e7), // 216 - (11, 0x07e0), // 217 - (11, 0x07e9), // 218 - (11, 0x07f7), // 219 - (9, 0x0190), // 220 - (11, 0x07f2), // 221 - (10, 0x0393), // 222 - (9, 0x01be), // 223 - (9, 0x01c0), // 224 - (10, 0x0394), // 225 - (10, 0x0397), // 226 - (10, 0x03ad), // 227 - (10, 0x03c3), // 228 - (10, 0x03c1), // 229 - (10, 0x03d2), // 230 - (11, 0x07da), // 231 - (11, 0x07d9), // 232 - (11, 0x07df), // 233 - (11, 0x07eb), // 234 - (11, 0x07f4), // 235 - (11, 0x07fa), // 236 - (9, 0x0195), // 237 - (11, 0x07f8), // 238 - (10, 0x03bd), // 239 - (10, 0x039c), // 240 - (10, 0x03ab), // 241 - (10, 0x03a8), // 242 - (10, 0x03b3), // 243 - (10, 0x03b9), // 244 - (10, 0x03d0), // 245 - (10, 0x03e3), // 246 - (10, 0x03e5), // 247 - (11, 0x07e2), // 248 - (11, 0x07de), // 249 - (11, 0x07ed), // 250 - (11, 0x07f1), // 251 - (11, 0x07f9), // 252 - (11, 0x07fc), // 253 - (9, 0x0193), // 254 - (12, 0x0ffd), // 255 - (10, 0x03dc), // 256 - (10, 0x03b6), // 257 - (10, 0x03c7), // 258 - (10, 0x03cc), // 259 - (10, 0x03cb), // 260 - (10, 0x03d9), // 261 - (10, 0x03da), // 262 - (11, 0x07d3), // 263 - (11, 0x07e1), // 264 - (11, 0x07ee), // 265 - (11, 0x07ef), // 266 - (11, 0x07f5), // 267 - (11, 0x07f6), // 268 - (12, 0x0ffc), // 269 - (12, 0x0fff), // 270 - (9, 0x019d), // 271 - (9, 0x01c2), // 272 — (y, z) = (16, 0) — y at ESC threshold - (8, 0x00b5), // 273 - (8, 0x00a1), // 274 - (8, 0x0096), // 275 - (8, 0x0097), // 276 - (8, 0x0095), // 277 - (8, 0x0099), // 278 - (8, 0x00a0), // 279 - (8, 0x00a2), // 280 - (8, 0x00ac), // 281 - (8, 0x00a9), // 282 - (8, 0x00b1), // 283 - (8, 0x00b3), // 284 - (8, 0x00bb), // 285 - (8, 0x00c0), // 286 - (9, 0x018f), // 287 - (5, 0x0004), // 288 — far corner (y, z) = (16, 16) — both at ESC threshold -]; - -/// Encode a Codebook 11 codeword index (`0..=288`) to the wire Huffman -/// codeword from Table 4.A.12. -/// -/// Returns `(length_in_bits, codeword)` with `codeword` right-aligned -/// in the `u16` (MSB at bit `length − 1`). Out-of-range `idx` -/// produces [`Error::SpectralCodebookIndexOutOfRange`]; the legal -/// range is `0..=288` (the 289-entry `17^2` enumeration of every -/// legal unsigned pair with each coefficient in `0..=16` where `16` -/// is the §4.6.3.3 escape flag). -/// -/// The inverse of [`hcod11_decode`]. Because Codebook 11 is unsigned, -/// callers transmit one sign bit after the codeword for each non-zero -/// coefficient via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) / -/// [`derive_sign_bits`](crate::spectral_codebook::derive_sign_bits) -/// — the §4.6.3.3 suffix sits outside the Huffman codeword carried -/// here. When either coefficient is `16`, the ESC sequence from -/// [`encode_esc_value`](crate::spectral_codebook::encode_esc_value) -/// follows the sign-bit suffix; the ESC bridge is also outside this -/// module. -pub fn hcod11_encode(idx: u32) -> Result<(u8, u16)> { - let entry = HCOD11 - .get(idx as usize) - .ok_or(Error::SpectralCodebookIndexOutOfRange(11))?; - Ok(*entry) -} - -/// Decode one Codebook 11 Huffman codeword from `reader`, returning -/// the codeword index in `0..=288`. -/// -/// The decoder is a straight prefix-match: read one bit at a time -/// (MSB-first), look it up in a flat 289-entry table. The table is -/// small enough (max codeword length 12 bits, 289 entries) that a -/// single linear scan per bit-extend is cheaper than the storage -/// and build-time cost of a multi-level lookup acceleration table. -/// Returns [`Error::UnexpectedEnd`] on reader underflow. -/// -/// The codebook is a **complete** prefix code over 12 bits (Kraft -/// equality `Σᵢ 2^(12 − Lᵢ) = 4096 = 2¹²`), so any 12-bit prefix -/// fully read from `reader` is guaranteed to match exactly one -/// entry — the bottom of the loop is unreachable when `reader` -/// produces 12 bits without underflowing. A purely defensive -/// `unreachable!()` guards the loop fall-through; it is verified -/// dead by the `hcod11_is_complete` regression test that -/// exhaustively walks all `2¹²` 12-bit prefixes. -/// -/// The §4.6.3.3 sign-bit suffix and the ESC sequence (when either -/// coefficient is `16`) lie outside this routine — for unsigned -/// Codebook 11 the caller consumes one sign bit per non-zero -/// coefficient after the Huffman codeword via -/// [`apply_sign_bits`](crate::spectral_codebook::apply_sign_bits) -/// and dispatches onto the -/// [`decode_esc_value`](crate::spectral_codebook::decode_esc_value) -/// bridge when the §4.6.3.3 index translation surfaces a `16` in -/// either slot. -pub fn hcod11_decode(reader: &mut BitReader<'_>) -> Result { - let mut acc: u32 = 0; - for len in 1..=HCOD11_MAX_LEN { - let bit = reader.read_u32(1).map_err(|_| Error::UnexpectedEnd)?; - acc = (acc << 1) | bit; - for (idx, &(entry_len, entry_cw)) in HCOD11.iter().enumerate() { - if u32::from(entry_len) == len && u32::from(entry_cw) == acc { - return Ok(idx as u32); - } - } - } - // Unreachable: HCOD11 is a complete 12-bit prefix code. The - // `hcod11_is_complete` regression test verifies every 12-bit - // prefix maps to exactly one entry. - unreachable!("HCOD11 is a complete 12-bit prefix code; the 12-bit walk must match"); -} - -/// Write a Codebook 11 codeword to `writer` by index. -/// -/// Convenience over `hcod11_encode` + manual `write_u32`. Returns -/// [`Error::SpectralCodebookIndexOutOfRange`] for `idx > 288`. The -/// §4.6.3.3 sign-bit suffix and the ESC sequence are the caller's -/// responsibility — the suffix is one bit per non-zero coefficient -/// emitted low-frequency-first, and the ESC sequence is appended -/// after the sign bits for each coefficient whose value reaches the -/// `16` flag. -pub fn hcod11_write(writer: &mut BitWriter, idx: u32) -> Result<()> { - let (len, cw) = hcod11_encode(idx)?; - writer.write_u32(u32::from(cw), u32::from(len)); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - // ------------------------------------------------------------------- - // Table-shape invariants - // ------------------------------------------------------------------- - - #[test] - fn hcod1_has_exactly_81_entries() { - // 3^4 = 81 (signed LAV=1 → mod = 2*1+1 = 3, dim = 4). - assert_eq!(HCOD1.len(), HCOD1_NUM_ENTRIES); - assert_eq!(HCOD1_NUM_ENTRIES, 81); - } - - #[test] - fn hcod1_max_length_is_11_bits() { - let max = HCOD1.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD1_MAX_LEN); - assert_eq!(HCOD1_MAX_LEN, 11); - } - - #[test] - fn hcod1_min_length_is_one_bit_at_index_40() { - // The zero-tuple (w, x, y, z) = (0, 0, 0, 0) at index 40 - // gets the single bit `0`. Every other index has length >= 5. - for (idx, &(len, cw)) in HCOD1.iter().enumerate() { - if idx == 40 { - assert_eq!(len, 1, "index 40 must be 1-bit"); - assert_eq!(cw, 0, "index 40 codeword must be `0`"); - } else { - assert!( - len >= 5, - "every non-zero-tuple index must have length >= 5; idx={} len={}", - idx, - len - ); - } - } - } - - #[test] - fn hcod1_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD1.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - // ------------------------------------------------------------------- - // Kraft equality / completeness - // ------------------------------------------------------------------- - - #[test] - fn hcod1_kraft_sum_is_two_to_the_eleven() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD1_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD1 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 2048); - } - - #[test] - fn hcod1_is_complete() { - // Walk every 11-bit prefix, decode it via the same path the - // production decoder uses, and confirm every prefix yields - // exactly one entry. Bonus: confirm the decoded index round- - // trips back to the same codeword via `hcod1_encode`. - for prefix in 0u32..(1u32 << HCOD1_MAX_LEN) { - let bytes = [(prefix >> 3) as u8, ((prefix & 0x7) << 5) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod1_decode(&mut br).expect("11-bit prefix must decode"); - let (len, cw) = hcod1_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD1_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - // ------------------------------------------------------------------- - // Encoder API - // ------------------------------------------------------------------- - - #[test] - fn encode_zero_tuple_is_single_zero_bit() { - // Index 40 = the zero 4-tuple → 1-bit `0` codeword. - let (len, cw) = hcod1_encode(40).unwrap(); - assert_eq!(len, 1); - assert_eq!(cw, 0); - } - - #[test] - fn encode_first_entry_matches_table() { - // Spec PDF Table 4.A.2 row 0: length 11, codeword 0x7f8. - let (len, cw) = hcod1_encode(0).unwrap(); - assert_eq!(len, 11); - assert_eq!(cw, 0x7f8); - } - - #[test] - fn encode_last_entry_matches_table() { - // Spec PDF Table 4.A.2 row 80: length 11, codeword 0x7f4. - let (len, cw) = hcod1_encode(80).unwrap(); - assert_eq!(len, 11); - assert_eq!(cw, 0x7f4); - } - - #[test] - fn encode_rejects_out_of_range_index() { - assert!(matches!( - hcod1_encode(81), - Err(Error::SpectralCodebookIndexOutOfRange(1)) - )); - assert!(matches!( - hcod1_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(1)) - )); - } - - // ------------------------------------------------------------------- - // Decoder API - // ------------------------------------------------------------------- - - #[test] - fn decode_single_zero_bit_yields_index_40() { - // One byte starting with `0` followed by anything → idx 40. - let bytes = [0b0111_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod1_decode(&mut br).unwrap(); - assert_eq!(idx, 40); - // Only one bit consumed; the remaining 7 are untouched. - assert_eq!(br.bit_position(), 1); - } - - #[test] - fn decode_first_entry_round_trip() { - // Index 0 → length 11, codeword 0x7f8 = 0b111_1111_1000. - // Pack into 2 bytes left-aligned: 0xff, 0x00. - let bytes = [0xff, 0x00]; - let mut br = BitReader::new(&bytes); - let idx = hcod1_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - assert_eq!(br.bit_position(), 11); - } - - #[test] - fn decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod1_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - // ------------------------------------------------------------------- - // Writer API - // ------------------------------------------------------------------- - - #[test] - fn write_then_decode_round_trips_every_index() { - for idx in 0..HCOD1_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod1_write(&mut w, idx).unwrap(); - // Pad to byte boundary if needed so BitReader can consume. - let (len, _) = hcod1_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod1_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod1_write(&mut w, 81), - Err(Error::SpectralCodebookIndexOutOfRange(1)) - )); - } - - // ------------------------------------------------------------------- - // Codebook 2 — Table 4.A.3 - // ------------------------------------------------------------------- - - #[test] - fn hcod2_has_exactly_81_entries() { - // 3^4 = 81 (signed LAV=1 → mod = 2*1+1 = 3, dim = 4) — same - // tuple universe as Codebook 1. - assert_eq!(HCOD2.len(), HCOD2_NUM_ENTRIES); - assert_eq!(HCOD2_NUM_ENTRIES, 81); - } - - #[test] - fn hcod2_max_length_is_9_bits() { - let max = HCOD2.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD2_MAX_LEN); - assert_eq!(HCOD2_MAX_LEN, 9); - } - - #[test] - fn hcod2_min_length_is_three_bits_at_index_40() { - // The zero-tuple (w, x, y, z) = (0, 0, 0, 0) at index 40 - // gets a 3-bit codeword `0b000` (vs the 1-bit `0` of - // Codebook 1). Every other index has length >= 4. - for (idx, &(len, cw)) in HCOD2.iter().enumerate() { - if idx == 40 { - assert_eq!(len, 3, "index 40 must be 3-bit"); - assert_eq!(cw, 0, "index 40 codeword must be `0`"); - } else { - assert!( - len >= 4, - "every non-zero-tuple index must have length >= 4; idx={} len={}", - idx, - len - ); - } - } - } - - #[test] - fn hcod2_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD2.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod2_kraft_sum_is_two_to_the_nine() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD2_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD2 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 512); - } - - #[test] - fn hcod2_is_complete() { - // Walk every 9-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod2_encode`. - for prefix in 0u32..(1u32 << HCOD2_MAX_LEN) { - // Pack `prefix` (9 bits) left-aligned into two bytes: - // [bits 8..1] [bit 0 << 7 | rest]. - let bytes = [(prefix >> 1) as u8, ((prefix & 0x1) << 7) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod2_decode(&mut br).expect("9-bit prefix must decode"); - let (len, cw) = hcod2_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD2_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn encode_zero_tuple_is_three_zero_bits_in_codebook_2() { - // Index 40 = the zero 4-tuple → 3-bit `000` codeword. - let (len, cw) = hcod2_encode(40).unwrap(); - assert_eq!(len, 3); - assert_eq!(cw, 0); - } - - #[test] - fn hcod2_encode_first_entry_matches_table() { - // Spec PDF Table 4.A.3 row 0: length 9, codeword 0x1f3. - let (len, cw) = hcod2_encode(0).unwrap(); - assert_eq!(len, 9); - assert_eq!(cw, 0x1f3); - } - - #[test] - fn hcod2_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.3 row 80: length 9, codeword 0x1f6. - let (len, cw) = hcod2_encode(80).unwrap(); - assert_eq!(len, 9); - assert_eq!(cw, 0x1f6); - } - - #[test] - fn hcod2_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod2_encode(81), - Err(Error::SpectralCodebookIndexOutOfRange(2)) - )); - assert!(matches!( - hcod2_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(2)) - )); - } - - #[test] - fn hcod2_decode_three_zero_bits_yields_index_40() { - // Three leading `0` bits → idx 40. Remaining 5 bits untouched. - let bytes = [0b0001_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod2_decode(&mut br).unwrap(); - assert_eq!(idx, 40); - assert_eq!(br.bit_position(), 3); - } - - #[test] - fn hcod2_decode_first_entry_round_trip() { - // Index 0 → length 9, codeword 0x1f3 = 0b1_1111_0011. - // Pack into 2 bytes left-aligned: 0xf9, 0x80. - // 0x1f3 << 7 = 0xf980 (16-bit big-endian). - let bytes = [0xf9, 0x80]; - let mut br = BitReader::new(&bytes); - let idx = hcod2_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - assert_eq!(br.bit_position(), 9); - } - - #[test] - fn hcod2_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod2_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod2_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD2_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod2_write(&mut w, idx).unwrap(); - let (len, _) = hcod2_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod2_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod2_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod2_write(&mut w, 81), - Err(Error::SpectralCodebookIndexOutOfRange(2)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebooks 1 and 2 share the same tuple universe but - // never share a codeword for the same index (different lengths - // and codewords for index 40 — 1 bit `0` vs 3 bits `0b000`). - // ------------------------------------------------------------------- - - #[test] - fn codebook_1_and_2_disagree_on_zero_tuple_codeword_length() { - let (l1, _) = hcod1_encode(40).unwrap(); - let (l2, _) = hcod2_encode(40).unwrap(); - // Both books carry the zero-tuple at index 40 but use - // different codeword lengths: 1 bit for Codebook 1, 3 bits - // for Codebook 2. - assert_eq!(l1, 1); - assert_eq!(l2, 3); - assert_ne!(l1, l2); - } - - // ------------------------------------------------------------------- - // Codebook 3 — Table 4.A.4 - // ------------------------------------------------------------------- - - #[test] - fn hcod3_has_exactly_81_entries() { - // 3^4 = 81 (unsigned LAV=2 → mod = lav+1 = 3, dim = 4). - assert_eq!(HCOD3.len(), HCOD3_NUM_ENTRIES); - assert_eq!(HCOD3_NUM_ENTRIES, 81); - } - - #[test] - fn hcod3_max_length_is_16_bits() { - let max = HCOD3.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD3_MAX_LEN); - assert_eq!(HCOD3_MAX_LEN, 16); - } - - #[test] - fn hcod3_min_length_is_one_bit_at_index_0() { - // Unsigned books put the all-zero magnitude n-tuple at - // index 0 (vs index 40 for the signed books); it carries the - // single bit `0`. Every other index has length >= 4. - for (idx, &(len, cw)) in HCOD3.iter().enumerate() { - if idx == 0 { - assert_eq!(len, 1, "index 0 must be 1-bit"); - assert_eq!(cw, 0, "index 0 codeword must be `0`"); - } else { - assert!( - len >= 4, - "every non-zero-tuple index must have length >= 4; idx={} len={}", - idx, - len - ); - } - } - } - - #[test] - fn hcod3_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD3.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod3_kraft_sum_is_two_to_the_sixteen() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD3_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD3 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 65536); - } - - #[test] - fn hcod3_is_complete() { - // Walk every 16-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod3_encode`. - for prefix in 0u32..(1u32 << HCOD3_MAX_LEN) { - // `prefix` already fits in 16 bits: pack left-aligned - // into two bytes (high byte first). - let bytes = [(prefix >> 8) as u8, (prefix & 0xff) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod3_decode(&mut br).expect("16-bit prefix must decode"); - let (len, cw) = hcod3_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD3_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#06x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod3_encode_zero_tuple_is_single_zero_bit() { - // Index 0 = the zero 4-tuple `(0, 0, 0, 0)` in the unsigned - // book → 1-bit `0` codeword. - let (len, cw) = hcod3_encode(0).unwrap(); - assert_eq!(len, 1); - assert_eq!(cw, 0); - } - - #[test] - fn hcod3_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.4 row 80: length 15, codeword 0x7ffa. - let (len, cw) = hcod3_encode(80).unwrap(); - assert_eq!(len, 15); - assert_eq!(cw, 0x7ffa); - } - - #[test] - fn hcod3_encode_index_62_is_the_only_full_16_bit_codeword_0xffff() { - // Spec PDF Table 4.A.4 row 62: length 16, codeword 0xffff - // (the all-ones 16-bit pattern). Verify by spot-check that - // this is the unique row with codeword 0xffff. - let (len, cw) = hcod3_encode(62).unwrap(); - assert_eq!(len, 16); - assert_eq!(cw, 0xffff); - let count_matching = HCOD3.iter().filter(|&&(_, c)| c == 0xffff).count(); - assert_eq!(count_matching, 1); - } - - #[test] - fn hcod3_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod3_encode(81), - Err(Error::SpectralCodebookIndexOutOfRange(3)) - )); - assert!(matches!( - hcod3_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(3)) - )); - } - - #[test] - fn hcod3_decode_single_zero_bit_yields_index_0() { - // Leading `0` bit → idx 0 (the unsigned book's zero-tuple). - let bytes = [0b0111_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod3_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - // Only one bit consumed; the remaining 7 are untouched. - assert_eq!(br.bit_position(), 1); - } - - #[test] - fn hcod3_decode_full_16_bit_codeword_round_trips() { - // Index 62 → length 16, codeword 0xffff. Pack as two bytes. - let bytes = [0xff, 0xff]; - let mut br = BitReader::new(&bytes); - let idx = hcod3_decode(&mut br).unwrap(); - assert_eq!(idx, 62); - assert_eq!(br.bit_position(), 16); - } - - #[test] - fn hcod3_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod3_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod3_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD3_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod3_write(&mut w, idx).unwrap(); - let (len, _) = hcod3_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod3_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod3_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod3_write(&mut w, 81), - Err(Error::SpectralCodebookIndexOutOfRange(3)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebook 3 zero-tuple sits at a different index - // than Codebooks 1 / 2 because unsigned books use a different - // index origin from signed books. - // ------------------------------------------------------------------- - - #[test] - fn codebook_3_zero_tuple_lives_at_index_zero_not_forty() { - // The zero magnitude 4-tuple `(0, 0, 0, 0)`: - // - signed book (mod = 3, offset = LAV = 1): polynomial - // evaluates to (0+1)*27 + (0+1)*9 + (0+1)*3 + (0+1) = 40. - // - unsigned book (mod = 3, offset = 0): polynomial - // evaluates to (0)*27 + (0)*9 + (0)*3 + (0) = 0. - // So the zero-tuple lives at index 40 in HCOD1 / HCOD2 and - // at index 0 in HCOD3. Both still carry a 1-bit codeword in - // their respective books (Codebook 1 + 3); Codebook 2 trades - // the 1-bit zero-tuple for a 3-bit one to free up the short - // codes for the non-zero tuples its target statistics prefer. - let (l1, cw1) = hcod1_encode(40).unwrap(); - let (l3, cw3) = hcod3_encode(0).unwrap(); - assert_eq!(l1, 1); - assert_eq!(cw1, 0); - assert_eq!(l3, 1); - assert_eq!(cw3, 0); - } - - // ------------------------------------------------------------------- - // Codebook 4 — Table 4.A.5 - // ------------------------------------------------------------------- - - #[test] - fn hcod4_has_exactly_81_entries() { - // 3^4 = 81 (unsigned LAV=2 → mod = lav+1 = 3, dim = 4) — same - // tuple universe as Codebook 3. - assert_eq!(HCOD4.len(), HCOD4_NUM_ENTRIES); - assert_eq!(HCOD4_NUM_ENTRIES, 81); - } - - #[test] - fn hcod4_max_length_is_12_bits() { - let max = HCOD4.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD4_MAX_LEN); - assert_eq!(HCOD4_MAX_LEN, 12); - } - - #[test] - fn hcod4_min_length_is_four_bits_at_index_40() { - // The shortest codeword in Codebook 4 is 4 bits, parked at - // index 40 with the all-zero pattern `0b0000`. Every other - // index has length >= 4 (Codebook 4's distribution has a - // dense 4-bit head: indices 0, 4, 13, 27, 30, 31, 36, 37, 39, - // 40 all share length 4). - let (len_40, cw_40) = (HCOD4[40].0, HCOD4[40].1); - assert_eq!(len_40, 4, "index 40 must be 4-bit"); - assert_eq!(cw_40, 0, "index 40 codeword must be `0b0000`"); - for (idx, &(len, _)) in HCOD4.iter().enumerate() { - assert!( - len >= 4, - "every index must have length >= 4; idx={} len={}", - idx, - len - ); - } - } - - #[test] - fn hcod4_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD4.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod4_kraft_sum_is_two_to_the_twelve() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD4_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD4 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 4096); - } - - #[test] - fn hcod4_is_complete() { - // Walk every 12-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod4_encode`. - for prefix in 0u32..(1u32 << HCOD4_MAX_LEN) { - // Pack `prefix` (12 bits) left-aligned into two bytes: - // high byte = bits 11..4, low byte = (bits 3..0) << 4. - let bytes = [(prefix >> 4) as u8, ((prefix & 0xf) << 4) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod4_decode(&mut br).expect("12-bit prefix must decode"); - let (len, cw) = hcod4_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD4_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod4_encode_index_40_is_4_bit_zero_codeword() { - // Spec PDF Table 4.A.5 row 40: length 4, codeword 0 (the - // shortest codeword in the table). - let (len, cw) = hcod4_encode(40).unwrap(); - assert_eq!(len, 4); - assert_eq!(cw, 0); - } - - #[test] - fn hcod4_encode_first_entry_matches_table() { - // Spec PDF Table 4.A.5 row 0: length 4, codeword 0x7. - let (len, cw) = hcod4_encode(0).unwrap(); - assert_eq!(len, 4); - assert_eq!(cw, 0x7); - } - - #[test] - fn hcod4_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.5 row 80: length 11, codeword 0x7fc. - let (len, cw) = hcod4_encode(80).unwrap(); - assert_eq!(len, 11); - assert_eq!(cw, 0x7fc); - } - - #[test] - fn hcod4_encode_indices_62_and_74_are_the_full_12_bit_codewords() { - // Spec PDF Table 4.A.5 row 62: length 12, codeword 0xfff. - // Spec PDF Table 4.A.5 row 74: length 12, codeword 0xffe. - // These are the only two 12-bit rows in Codebook 4. - let (len_62, cw_62) = hcod4_encode(62).unwrap(); - assert_eq!((len_62, cw_62), (12, 0xfff)); - let (len_74, cw_74) = hcod4_encode(74).unwrap(); - assert_eq!((len_74, cw_74), (12, 0xffe)); - let count_12_bit = HCOD4.iter().filter(|&&(l, _)| l == 12).count(); - assert_eq!(count_12_bit, 2); - } - - #[test] - fn hcod4_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod4_encode(81), - Err(Error::SpectralCodebookIndexOutOfRange(4)) - )); - assert!(matches!( - hcod4_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(4)) - )); - } - - #[test] - fn hcod4_decode_four_zero_bits_yields_index_40() { - // Leading `0b0000` → idx 40 (Codebook 4's shortest codeword). - // Remaining 4 bits of the byte untouched. - let bytes = [0b0000_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod4_decode(&mut br).unwrap(); - assert_eq!(idx, 40); - assert_eq!(br.bit_position(), 4); - } - - #[test] - fn hcod4_decode_full_12_bit_codeword_round_trips_index_62() { - // Index 62 → length 12, codeword 0xfff = 0b1111_1111_1111. - // Pack left-aligned into 2 bytes: 0xff, 0xf0. - let bytes = [0xff, 0xf0]; - let mut br = BitReader::new(&bytes); - let idx = hcod4_decode(&mut br).unwrap(); - assert_eq!(idx, 62); - assert_eq!(br.bit_position(), 12); - } - - #[test] - fn hcod4_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod4_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod4_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD4_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod4_write(&mut w, idx).unwrap(); - let (len, _) = hcod4_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod4_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod4_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod4_write(&mut w, 81), - Err(Error::SpectralCodebookIndexOutOfRange(4)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebook 3 and Codebook 4 share the unsigned dim-4 - // LAV-2 tuple universe (same Table 4.95 row shape) but assign - // different codewords for the same tuple — Codebook 3 gives the - // zero-tuple the single-bit codeword `0`; Codebook 4 lifts it to - // a 4-bit `0b0111` and parks the 4-bit `0b0000` shortest at - // index 40 instead. - // ------------------------------------------------------------------- - - #[test] - fn codebook_3_and_4_disagree_on_zero_tuple_codeword() { - let (l3, cw3) = hcod3_encode(0).unwrap(); - let (l4, cw4) = hcod4_encode(0).unwrap(); - assert_eq!((l3, cw3), (1, 0)); - assert_eq!((l4, cw4), (4, 0x7)); - // Codebook 4's shortest codeword sits at a different index - // (40) with a different value (`0b0000`). - let (l40, cw40) = hcod4_encode(40).unwrap(); - assert_eq!((l40, cw40), (4, 0)); - } - - // ------------------------------------------------------------------- - // Codebook 5 (Table 4.A.6) — signed dim-2 LAV-4 pair book - // ------------------------------------------------------------------- - - #[test] - fn hcod5_has_exactly_81_entries() { - // 9^2 = 81 (signed LAV=4 → mod = 2*4+1 = 9, dim = 2). - assert_eq!(HCOD5.len(), HCOD5_NUM_ENTRIES); - assert_eq!(HCOD5_NUM_ENTRIES, 81); - } - - #[test] - fn hcod5_max_length_is_13_bits() { - let max = HCOD5.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD5_MAX_LEN); - assert_eq!(HCOD5_MAX_LEN, 13); - } - - #[test] - fn hcod5_min_length_is_one_bit_at_index_40() { - // The shortest codeword in Codebook 5 is the single bit `0` - // at index 40 — the §4.6.3.3 zero-tuple `(0, 0)` for a - // signed pair book with LAV = 4 lands at the centre of the - // index range, not at the edges. - let (len_40, cw_40) = (HCOD5[40].0, HCOD5[40].1); - assert_eq!(len_40, 1, "index 40 must be 1-bit"); - assert_eq!(cw_40, 0, "index 40 codeword must be `0`"); - let count_1_bit = HCOD5.iter().filter(|&&(l, _)| l == 1).count(); - assert_eq!(count_1_bit, 1, "exactly one 1-bit codeword"); - } - - #[test] - fn hcod5_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD5.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod5_kraft_sum_is_two_to_the_thirteen() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD5_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD5 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 8192); - } - - #[test] - fn hcod5_is_complete() { - // Walk every 13-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod5_encode`. - for prefix in 0u32..(1u32 << HCOD5_MAX_LEN) { - // Pack `prefix` (13 bits) left-aligned into two bytes: - // high byte = bits 12..5, low byte = (bits 4..0) << 3. - let bytes = [(prefix >> 5) as u8, ((prefix & 0x1f) << 3) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod5_decode(&mut br).expect("13-bit prefix must decode"); - let (len, cw) = hcod5_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD5_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#06x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod5_encode_index_40_is_single_zero_bit() { - // Spec PDF Table 4.A.6 row 40: length 1, codeword 0 — the - // §4.6.3.3 zero-tuple `(0, 0)`. - let (len, cw) = hcod5_encode(40).unwrap(); - assert_eq!(len, 1); - assert_eq!(cw, 0); - } - - #[test] - fn hcod5_encode_first_entry_matches_table() { - // Spec PDF Table 4.A.6 row 0: length 13, codeword 0x1fff — - // the lower-left corner `(-4, -4)` of the signed pair lattice. - let (len, cw) = hcod5_encode(0).unwrap(); - assert_eq!(len, 13); - assert_eq!(cw, 0x1fff); - } - - #[test] - fn hcod5_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.6 row 80: length 13, codeword 0x1ffe — - // the upper-right corner `(+4, +4)` of the signed pair lattice. - let (len, cw) = hcod5_encode(80).unwrap(); - assert_eq!(len, 13); - assert_eq!(cw, 0x1ffe); - } - - #[test] - fn hcod5_encode_four_13_bit_rows_are_the_lattice_corners() { - // The four 13-bit codewords sit at indices 0, 8, 72, 80 — the - // four `(±4, ±4)` corners of the signed `9 × 9` pair lattice. - let expected = [ - (0u32, 0x1fffu16), // (-4, -4) - (8u32, 0x1ffdu16), // (-4, +4) - (72u32, 0x1ffcu16), // (+4, -4) - (80u32, 0x1ffeu16), // (+4, +4) - ]; - let observed: Vec<_> = HCOD5 - .iter() - .enumerate() - .filter_map(|(i, &(l, cw))| if l == 13 { Some((i as u32, cw)) } else { None }) - .collect(); - assert_eq!(observed.len(), 4); - for (e, o) in expected.iter().zip(observed.iter()) { - assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); - } - } - - #[test] - fn hcod5_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod5_encode(81), - Err(Error::SpectralCodebookIndexOutOfRange(5)) - )); - assert!(matches!( - hcod5_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(5)) - )); - } - - #[test] - fn hcod5_decode_single_zero_bit_yields_index_40() { - // Leading bit `0` → idx 40 (the zero-tuple `(0, 0)`). - // Remaining 7 bits of the byte untouched. - let bytes = [0b0111_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod5_decode(&mut br).unwrap(); - assert_eq!(idx, 40); - assert_eq!(br.bit_position(), 1); - } - - #[test] - fn hcod5_decode_full_13_bit_codeword_round_trips_index_0() { - // Index 0 → length 13, codeword 0x1fff = 0b1_1111_1111_1111. - // Pack left-aligned into 2 bytes: 0xff, 0xf8. - let bytes = [0xff, 0xf8]; - let mut br = BitReader::new(&bytes); - let idx = hcod5_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - assert_eq!(br.bit_position(), 13); - } - - #[test] - fn hcod5_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod5_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod5_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD5_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod5_write(&mut w, idx).unwrap(); - let (len, _) = hcod5_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod5_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod5_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod5_write(&mut w, 81), - Err(Error::SpectralCodebookIndexOutOfRange(5)) - )); - } - - // ------------------------------------------------------------------- - // Codebook 6 (Table 4.A.7) — signed dim-2 LAV-4 pair book - // ------------------------------------------------------------------- - - #[test] - fn hcod6_has_exactly_81_entries() { - // 9^2 = 81 (signed LAV=4 → mod = 2*4+1 = 9, dim = 2) — same - // tuple universe as Codebook 5. - assert_eq!(HCOD6.len(), HCOD6_NUM_ENTRIES); - assert_eq!(HCOD6_NUM_ENTRIES, 81); - } - - #[test] - fn hcod6_max_length_is_11_bits() { - let max = HCOD6.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD6_MAX_LEN); - assert_eq!(HCOD6_MAX_LEN, 11); - } - - #[test] - fn hcod6_min_length_is_four_bits_at_index_40() { - // The shortest codeword in Codebook 6 is 4 bits, parked at - // index 40 (the §4.6.3.3 zero-tuple `(0, 0)` for a signed - // pair book with LAV=4) with the all-zero pattern `0b0000`. - // Every other index has length >= 4 (Codebook 6's - // distribution has a dense 4-bit head: indices 30, 31, 32, - // 39, 40, 41, 48, 49, 50 all share length 4). - let (len_40, cw_40) = (HCOD6[40].0, HCOD6[40].1); - assert_eq!(len_40, 4, "index 40 must be 4-bit"); - assert_eq!(cw_40, 0, "index 40 codeword must be `0b0000`"); - for (idx, &(len, _)) in HCOD6.iter().enumerate() { - assert!( - len >= 4, - "every index must have length >= 4; idx={} len={}", - idx, - len - ); - } - } - - #[test] - fn hcod6_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD6.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod6_kraft_sum_is_two_to_the_eleven() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD6_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD6 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 2048); - } - - #[test] - fn hcod6_is_complete() { - // Walk every 11-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod6_encode`. - for prefix in 0u32..(1u32 << HCOD6_MAX_LEN) { - // Pack `prefix` (11 bits) left-aligned into two bytes: - // high byte = bits 10..3, low byte = (bits 2..0) << 5. - let bytes = [(prefix >> 3) as u8, ((prefix & 0x7) << 5) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod6_decode(&mut br).expect("11-bit prefix must decode"); - let (len, cw) = hcod6_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD6_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod6_encode_index_40_is_4_bit_zero_codeword() { - // Spec PDF Table 4.A.7 row 40: length 4, codeword 0 — the - // §4.6.3.3 zero-tuple `(0, 0)`. - let (len, cw) = hcod6_encode(40).unwrap(); - assert_eq!(len, 4); - assert_eq!(cw, 0); - } - - #[test] - fn hcod6_encode_first_entry_matches_table() { - // Spec PDF Table 4.A.7 row 0: length 11, codeword 0x7fe — - // the lower-left corner `(-4, -4)` of the signed pair lattice. - let (len, cw) = hcod6_encode(0).unwrap(); - assert_eq!(len, 11); - assert_eq!(cw, 0x7fe); - } - - #[test] - fn hcod6_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.7 row 80: length 11, codeword 0x7fc — - // the upper-right corner `(+4, +4)` of the signed pair lattice. - let (len, cw) = hcod6_encode(80).unwrap(); - assert_eq!(len, 11); - assert_eq!(cw, 0x7fc); - } - - #[test] - fn hcod6_encode_four_11_bit_rows_are_the_lattice_corners() { - // The four 11-bit codewords sit at indices 0, 8, 72, 80 — the - // four `(±4, ±4)` corners of the signed `9 × 9` pair lattice. - let expected = [ - (0u32, 0x7feu16), // (-4, -4) - (8u32, 0x7fdu16), // (-4, +4) - (72u32, 0x7ffu16), // (+4, -4) - (80u32, 0x7fcu16), // (+4, +4) - ]; - let observed: Vec<_> = HCOD6 - .iter() - .enumerate() - .filter_map(|(i, &(l, cw))| if l == 11 { Some((i as u32, cw)) } else { None }) - .collect(); - assert_eq!(observed.len(), 4); - for (e, o) in expected.iter().zip(observed.iter()) { - assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); - } - } - - #[test] - fn hcod6_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod6_encode(81), - Err(Error::SpectralCodebookIndexOutOfRange(6)) - )); - assert!(matches!( - hcod6_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(6)) - )); - } - - #[test] - fn hcod6_decode_four_zero_bits_yields_index_40() { - // Leading `0b0000` → idx 40 (the zero-tuple `(0, 0)`). - // Remaining 4 bits of the byte untouched. - let bytes = [0b0000_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod6_decode(&mut br).unwrap(); - assert_eq!(idx, 40); - assert_eq!(br.bit_position(), 4); - } - - #[test] - fn hcod6_decode_full_11_bit_codeword_round_trips_index_72() { - // Index 72 → length 11, codeword 0x7ff = 0b111_1111_1111. - // Pack left-aligned into 2 bytes: 0xff, 0xe0. - let bytes = [0xff, 0xe0]; - let mut br = BitReader::new(&bytes); - let idx = hcod6_decode(&mut br).unwrap(); - assert_eq!(idx, 72); - assert_eq!(br.bit_position(), 11); - } - - #[test] - fn hcod6_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod6_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod6_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD6_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod6_write(&mut w, idx).unwrap(); - let (len, _) = hcod6_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod6_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod6_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod6_write(&mut w, 81), - Err(Error::SpectralCodebookIndexOutOfRange(6)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebooks 5 and 6 share the signed pair tuple - // universe (Table 4.95 rows 5 and 6 are identical except for the - // `Codebook listed in Table` column) but assign different codewords - // for the same tuple — Codebook 5 gives the zero-tuple the single- - // bit codeword `0`; Codebook 6 lifts it to a 4-bit `0b0000` and - // pulls the ceiling back from 13 down to 11 bits. - // ------------------------------------------------------------------- - - #[test] - fn codebook_5_and_6_disagree_on_zero_tuple_codeword() { - let (l5, cw5) = hcod5_encode(40).unwrap(); - let (l6, cw6) = hcod6_encode(40).unwrap(); - assert_eq!((l5, cw5), (1, 0)); - assert_eq!((l6, cw6), (4, 0)); - } - - #[test] - fn codebook_5_and_6_agree_on_lattice_corner_indices() { - // Both books pin the four (±4, ±4) lattice corners to their - // respective maximum-length codewords — Codebook 5 at 13 bits, - // Codebook 6 at 11 bits — but at the same four index positions. - let corners: Vec = [0, 8, 72, 80].to_vec(); - let cb5_max_idx: Vec = HCOD5 - .iter() - .enumerate() - .filter_map(|(i, &(l, _))| { - if u32::from(l) == HCOD5_MAX_LEN { - Some(i) - } else { - None - } - }) - .collect(); - let cb6_max_idx: Vec = HCOD6 - .iter() - .enumerate() - .filter_map(|(i, &(l, _))| { - if u32::from(l) == HCOD6_MAX_LEN { - Some(i) - } else { - None - } - }) - .collect(); - assert_eq!(cb5_max_idx, corners); - assert_eq!(cb6_max_idx, corners); - } - - // ------------------------------------------------------------------- - // Codebook 7 (Table 4.A.8): unsigned pair, dim=2, LAV=7, - // 64 entries indexed 0..=63 (8^2 lattice). Zero-tuple `(0, 0)` at - // index 0 carries the single-bit codeword `0`. Maximum codeword - // length 12 bits. Complete prefix code: Kraft sum = 4096 = 2^12. - // ------------------------------------------------------------------- - - #[test] - fn hcod7_has_exactly_64_entries() { - // 8^2 = 64 (unsigned LAV=7 → mod = 7+1 = 8, dim = 2). - assert_eq!(HCOD7.len(), HCOD7_NUM_ENTRIES); - assert_eq!(HCOD7_NUM_ENTRIES, 64); - } - - #[test] - fn hcod7_max_length_is_12_bits() { - let max = HCOD7.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD7_MAX_LEN); - assert_eq!(HCOD7_MAX_LEN, 12); - } - - #[test] - fn hcod7_min_length_is_one_bit_at_index_0() { - // The shortest codeword in Codebook 7 is 1 bit, parked at - // index 0 (the §4.6.3.3 zero-tuple `(0, 0)` for an unsigned - // pair book with LAV=7) with the codeword `0`. Index 0 is the - // only 1-bit entry; every other index has length >= 3. - let (len_0, cw_0) = (HCOD7[0].0, HCOD7[0].1); - assert_eq!(len_0, 1, "index 0 must be 1-bit"); - assert_eq!(cw_0, 0, "index 0 codeword must be `0`"); - let single_bit_entries: usize = HCOD7.iter().filter(|&&(len, _)| len == 1).count(); - assert_eq!(single_bit_entries, 1, "exactly one 1-bit codeword"); - for (idx, &(len, _)) in HCOD7.iter().enumerate().skip(1) { - assert!( - len >= 3, - "every index > 0 must have length >= 3; idx={} len={}", - idx, - len - ); - } - } - - #[test] - fn hcod7_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD7.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod7_kraft_sum_is_two_to_the_twelve() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD7_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD7 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 4096); - } - - #[test] - fn hcod7_is_complete() { - // Walk every 12-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod7_encode`. - for prefix in 0u32..(1u32 << HCOD7_MAX_LEN) { - // Pack `prefix` (12 bits) left-aligned into two bytes: - // high byte = bits 11..4, low byte = (bits 3..0) << 4. - let bytes = [(prefix >> 4) as u8, ((prefix & 0xf) << 4) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod7_decode(&mut br).expect("12-bit prefix must decode"); - let (len, cw) = hcod7_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD7_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod7_encode_index_0_is_1_bit_zero_codeword() { - // Spec PDF Table 4.A.8 row 0: length 1, codeword 0 — the - // §4.6.3.3 zero-tuple `(0, 0)`. - let (len, cw) = hcod7_encode(0).unwrap(); - assert_eq!(len, 1); - assert_eq!(cw, 0); - } - - #[test] - fn hcod7_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.8 row 63: length 12, codeword 0xfff — - // the far corner `(7, 7)` of the unsigned `8 × 8` pair lattice. - let (len, cw) = hcod7_encode(63).unwrap(); - assert_eq!(len, 12); - assert_eq!(cw, 0xfff); - } - - #[test] - fn hcod7_encode_four_12_bit_rows_match_table() { - // Exactly four rows reach the 12-bit ceiling in Table 4.A.8: - // indices 54, 55, 62, 63 with codewords ffd, ffe, ffc, fff. - let expected = [ - (54u32, 0xffdu16), - (55u32, 0xffeu16), - (62u32, 0xffcu16), - (63u32, 0xfffu16), - ]; - let observed: Vec<_> = HCOD7 - .iter() - .enumerate() - .filter_map(|(i, &(l, cw))| if l == 12 { Some((i as u32, cw)) } else { None }) - .collect(); - assert_eq!(observed.len(), 4); - for (e, o) in expected.iter().zip(observed.iter()) { - assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); - } - } - - #[test] - fn hcod7_encode_index_8_is_first_y1_row() { - // Index 8 = (y, z) = (1, 0) via `y * 8 + z`. Table 4.A.8 row - // 8: length 3, codeword 4. - let (len, cw) = hcod7_encode(8).unwrap(); - assert_eq!(len, 3); - assert_eq!(cw, 4); - } - - #[test] - fn hcod7_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod7_encode(64), - Err(Error::SpectralCodebookIndexOutOfRange(7)) - )); - assert!(matches!( - hcod7_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(7)) - )); - } - - #[test] - fn hcod7_decode_single_zero_bit_yields_index_0() { - // Leading `0` → idx 0 (the zero-tuple `(0, 0)`). Remaining 7 - // bits of the byte untouched. - let bytes = [0b0111_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod7_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - assert_eq!(br.bit_position(), 1); - } - - #[test] - fn hcod7_decode_full_12_bit_codeword_round_trips_index_63() { - // Index 63 → length 12, codeword 0xfff = 0b1111_1111_1111. - // Pack left-aligned into 2 bytes: 0xff, 0xf0. - let bytes = [0xff, 0xf0]; - let mut br = BitReader::new(&bytes); - let idx = hcod7_decode(&mut br).unwrap(); - assert_eq!(idx, 63); - assert_eq!(br.bit_position(), 12); - } - - #[test] - fn hcod7_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod7_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod7_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD7_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod7_write(&mut w, idx).unwrap(); - let (len, _) = hcod7_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod7_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod7_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod7_write(&mut w, 64), - Err(Error::SpectralCodebookIndexOutOfRange(7)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebook 7 is the first unsigned **pair** book. - // It shares Codebook 3's "zero-tuple at index 0 with the shortest - // codeword" placement (both are unsigned books with the §4.6.3.3 - // polynomial origin at index 0) but at dim=2 vs dim=4, and with - // a 12-bit ceiling vs Codebook 3's 16-bit ceiling. - // ------------------------------------------------------------------- - - #[test] - fn codebook_3_and_7_both_park_zero_tuple_at_index_0() { - // Both unsigned books map the §4.6.3.3 origin to index 0 and - // hand it the shortest available codeword (length 1, value 0). - let (l3, cw3) = hcod3_encode(0).unwrap(); - let (l7, cw7) = hcod7_encode(0).unwrap(); - assert_eq!((l3, cw3), (1, 0)); - assert_eq!((l7, cw7), (1, 0)); - } - - #[test] - fn codebook_7_entry_count_is_64_vs_81_for_dim4_books() { - // Dim-4 unsigned (HCB3/HCB4 with LAV=2): (2+1)^4 = 81. - // Dim-2 unsigned (HCB7 with LAV=7): (7+1)^2 = 64. The - // dim-2 → dim-4 split affects the §4.6.3.3 universe size. - assert_eq!(HCOD3.len(), 81); - assert_eq!(HCOD4.len(), 81); - assert_eq!(HCOD7.len(), 64); - } - - // ------------------------------------------------------------------- - // Codebook 8 (Table 4.A.9): unsigned pair, dim=2, LAV=7, - // 64 entries indexed 0..=63 (8^2 lattice). The §4.6.3.3 zero-tuple - // `(0, 0)` at index 0 carries a 5-bit `0b01110` (not the shortest); - // the shortest 3-bit codeword `0` parks at index 9 (= (1, 1)). - // Maximum codeword length 10 bits. Complete prefix code: Kraft - // sum = 1024 = 2^10. - // ------------------------------------------------------------------- - - #[test] - fn hcod8_has_exactly_64_entries() { - // 8^2 = 64 (unsigned LAV=7 → mod = 7+1 = 8, dim = 2). Shares - // the universe size with Codebook 7. - assert_eq!(HCOD8.len(), HCOD8_NUM_ENTRIES); - assert_eq!(HCOD8_NUM_ENTRIES, 64); - } - - #[test] - fn hcod8_max_length_is_10_bits() { - let max = HCOD8.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD8_MAX_LEN); - assert_eq!(HCOD8_MAX_LEN, 10); - } - - #[test] - fn hcod8_min_length_is_three_bits_at_index_9() { - // The shortest codeword in Codebook 8 is 3 bits, parked at - // index 9 (the §4.6.3.3 interior tuple `(y, z) = (1, 1)` for - // an unsigned pair book with LAV=7) with the codeword `0`. - // Index 9 is the only 3-bit entry; every other index has - // length >= 4. - let (len_9, cw_9) = (HCOD8[9].0, HCOD8[9].1); - assert_eq!(len_9, 3, "index 9 must be 3-bit"); - assert_eq!(cw_9, 0, "index 9 codeword must be `0`"); - let three_bit_entries: usize = HCOD8.iter().filter(|&&(len, _)| len == 3).count(); - assert_eq!(three_bit_entries, 1, "exactly one 3-bit codeword"); - for (idx, &(len, _)) in HCOD8.iter().enumerate() { - if idx == 9 { - continue; - } - assert!( - len >= 4, - "every index != 9 must have length >= 4; idx={} len={}", - idx, - len - ); - } - } - - #[test] - fn hcod8_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD8.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod8_kraft_sum_is_two_to_the_ten() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD8_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD8 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 1024); - } - - #[test] - fn hcod8_is_complete() { - // Walk every 10-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod8_encode`. - for prefix in 0u32..(1u32 << HCOD8_MAX_LEN) { - // Pack `prefix` (10 bits) left-aligned into two bytes: - // high byte = bits 9..2, low byte = (bits 1..0) << 6. - let bytes = [(prefix >> 2) as u8, ((prefix & 0x3) << 6) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod8_decode(&mut br).expect("10-bit prefix must decode"); - let (len, cw) = hcod8_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` bits - // of `prefix`. - let lead = prefix >> (HCOD8_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod8_encode_index_0_is_5_bit_zero_tuple_codeword() { - // Spec PDF Table 4.A.9 row 0: length 5, codeword 0xe — the - // §4.6.3.3 zero-tuple `(0, 0)` lifted off the shortest slot. - let (len, cw) = hcod8_encode(0).unwrap(); - assert_eq!(len, 5); - assert_eq!(cw, 0xe); - } - - #[test] - fn hcod8_encode_index_9_is_3_bit_zero_codeword() { - // Spec PDF Table 4.A.9 row 9: length 3, codeword 0 — the - // shortest codeword, parked on `(1, 1)` (= y * 8 + z = 9). - let (len, cw) = hcod8_encode(9).unwrap(); - assert_eq!(len, 3); - assert_eq!(cw, 0); - } - - #[test] - fn hcod8_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.9 row 63: length 10, codeword 0x3ff — - // the far corner `(7, 7)` of the unsigned `8 × 8` pair lattice. - let (len, cw) = hcod8_encode(63).unwrap(); - assert_eq!(len, 10); - assert_eq!(cw, 0x3ff); - } - - #[test] - fn hcod8_encode_four_10_bit_rows_match_table() { - // Exactly four rows reach the 10-bit ceiling in Table 4.A.9: - // indices 7, 47, 56, 63 with codewords 3fe, 3fc, 3fd, 3ff. - let expected = [ - (7u32, 0x3feu16), - (47u32, 0x3fcu16), - (56u32, 0x3fdu16), - (63u32, 0x3ffu16), - ]; - let observed: Vec<_> = HCOD8 - .iter() - .enumerate() - .filter_map(|(i, &(l, cw))| if l == 10 { Some((i as u32, cw)) } else { None }) - .collect(); - assert_eq!(observed.len(), 4); - for (e, o) in expected.iter().zip(observed.iter()) { - assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); - } - } - - #[test] - fn hcod8_encode_index_8_is_first_y1_row() { - // Index 8 = (y, z) = (1, 0) via `y * 8 + z`. Table 4.A.9 row - // 8: length 4, codeword 0x3. - let (len, cw) = hcod8_encode(8).unwrap(); - assert_eq!(len, 4); - assert_eq!(cw, 0x3); - } - - #[test] - fn hcod8_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod8_encode(64), - Err(Error::SpectralCodebookIndexOutOfRange(8)) - )); - assert!(matches!( - hcod8_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(8)) - )); - } - - #[test] - fn hcod8_decode_three_zero_bits_yields_index_9() { - // Leading `0b000` → idx 9 (the interior tuple `(1, 1)`). - // Remaining 5 bits of the byte untouched. - let bytes = [0b0001_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod8_decode(&mut br).unwrap(); - assert_eq!(idx, 9); - assert_eq!(br.bit_position(), 3); - } - - #[test] - fn hcod8_decode_full_10_bit_codeword_round_trips_index_63() { - // Index 63 → length 10, codeword 0x3ff = 0b1111_1111_11. - // Pack left-aligned into 2 bytes: 0xff, 0xc0. - let bytes = [0xff, 0xc0]; - let mut br = BitReader::new(&bytes); - let idx = hcod8_decode(&mut br).unwrap(); - assert_eq!(idx, 63); - assert_eq!(br.bit_position(), 10); - } - - #[test] - fn hcod8_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod8_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod8_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD8_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod8_write(&mut w, idx).unwrap(); - let (len, _) = hcod8_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod8_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod8_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod8_write(&mut w, 64), - Err(Error::SpectralCodebookIndexOutOfRange(8)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebooks 7 and 8 share the unsigned pair tuple - // universe (Table 4.95 rows 7 and 8 are identical except for the - // `Codebook listed in Table` column) but assign different codewords - // for the same `(y, z)` tuple. Where Codebook 7 pins the zero-tuple - // to the 1-bit slot, Codebook 8 lifts it to a 5-bit codeword and - // hands the 3-bit shortest-codeword slot to the `(1, 1)` interior. - // ------------------------------------------------------------------- - - #[test] - fn codebook_7_and_8_share_universe_size_but_disagree_on_shortest_slot() { - assert_eq!(HCOD7_NUM_ENTRIES, HCOD8_NUM_ENTRIES); - assert_eq!(HCOD7_NUM_ENTRIES, 64); - // Codebook 7: zero-tuple at index 0 takes the 1-bit slot. - let (l7_0, _) = hcod7_encode(0).unwrap(); - assert_eq!(l7_0, 1); - // Codebook 8: zero-tuple at index 0 takes 5 bits; the 3-bit - // shortest slot lives on the (1, 1) interior at index 9. - let (l8_0, _) = hcod8_encode(0).unwrap(); - let (l8_9, cw8_9) = hcod8_encode(9).unwrap(); - assert_eq!(l8_0, 5); - assert_eq!((l8_9, cw8_9), (3, 0)); - } - - #[test] - fn codebook_8_far_corner_matches_codebook_7_far_corner_index() { - // Both unsigned dim-2 LAV-7 books park `(7, 7)` at index 63 - // (the §4.6.3.3 unsigned polynomial puts the far corner at - // the highest index). Only the codeword length / value - // differs: Codebook 7 → 12-bit 0xfff; Codebook 8 → 10-bit 0x3ff. - let (l7, cw7) = hcod7_encode(63).unwrap(); - let (l8, cw8) = hcod8_encode(63).unwrap(); - assert_eq!((l7, cw7), (12, 0xfff)); - assert_eq!((l8, cw8), (10, 0x3ff)); - } - - // ------------------------------------------------------------------- - // Codebook 9 — Table 4.A.10 - // ------------------------------------------------------------------- - - #[test] - fn hcod9_has_exactly_169_entries() { - // 13^2 = 169 (unsigned LAV=12 → mod = lav+1 = 13, dim = 2). - assert_eq!(HCOD9.len(), HCOD9_NUM_ENTRIES); - assert_eq!(HCOD9_NUM_ENTRIES, 169); - } - - #[test] - fn hcod9_max_length_is_15_bits() { - let max = HCOD9.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD9_MAX_LEN); - assert_eq!(HCOD9_MAX_LEN, 15); - } - - #[test] - fn hcod9_min_length_is_one_bit_at_index_0() { - // Unsigned books put the all-zero magnitude pair tuple at - // index 0; Codebook 9 carries it as the single bit `0`. - // Every other index has length >= 3. - for (idx, &(len, cw)) in HCOD9.iter().enumerate() { - if idx == 0 { - assert_eq!(len, 1, "index 0 must be 1-bit"); - assert_eq!(cw, 0, "index 0 codeword must be `0`"); - } else { - assert!( - len >= 3, - "every non-zero index must have length >= 3; idx={} len={}", - idx, - len - ); - } - } - } - - #[test] - fn hcod9_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD9.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod9_kraft_sum_is_two_to_the_fifteen() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD9_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD9 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 32768); - } - - #[test] - fn hcod9_is_complete() { - // Walk every 15-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod9_encode`. - for prefix in 0u32..(1u32 << HCOD9_MAX_LEN) { - // Pack `prefix` (15 bits) left-aligned into two bytes: - // high byte = bits 14..7, low byte = (bits 6..0) << 1. - let bytes = [(prefix >> 7) as u8, ((prefix & 0x7f) << 1) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod9_decode(&mut br).expect("15-bit prefix must decode"); - let (len, cw) = hcod9_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` - // bits of `prefix`. - let lead = prefix >> (HCOD9_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#06x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod9_encode_index_0_is_one_bit_zero_codeword() { - // Spec PDF Table 4.A.10 row 0: length 1, codeword 0 — the - // §4.6.3.3 zero-tuple `(0, 0)` carries the shortest possible - // codeword. - let (len, cw) = hcod9_encode(0).unwrap(); - assert_eq!(len, 1); - assert_eq!(cw, 0); - } - - #[test] - fn hcod9_encode_first_few_rows_match_spec() { - // Spec PDF Table 4.A.10 spot checks: indices 1, 13, 14. - // Row 1: length 3, codeword 0x5; row 13: length 3, codeword - // 0x4 (the only other 3-bit row); row 14: length 4, - // codeword 0xc (interior `(y, z) = (1, 1)` since `idx = - // 1 * 13 + 1 = 14`). - assert_eq!(hcod9_encode(1).unwrap(), (3, 0x5)); - assert_eq!(hcod9_encode(13).unwrap(), (3, 0x4)); - assert_eq!(hcod9_encode(14).unwrap(), (4, 0xc)); - } - - #[test] - fn hcod9_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.10 row 168: length 15, codeword 0x7fff - // — the far corner `(12, 12)` of the unsigned `13 × 13` - // pair lattice. - let (len, cw) = hcod9_encode(168).unwrap(); - assert_eq!(len, 15); - assert_eq!(cw, 0x7fff); - } - - #[test] - fn hcod9_encode_four_15_bit_rows_match_table() { - // Exactly four rows reach the 15-bit ceiling in Table 4.A.10: - // indices 142, 154, 155, 168 with codewords 7ffc, 7ffd, - // 7ffe, 7fff. - let expected = [ - (142u32, 0x7ffcu16), - (154u32, 0x7ffdu16), - (155u32, 0x7ffeu16), - (168u32, 0x7fffu16), - ]; - let observed: Vec<_> = HCOD9 - .iter() - .enumerate() - .filter_map(|(i, &(l, cw))| if l == 15 { Some((i as u32, cw)) } else { None }) - .collect(); - assert_eq!(observed.len(), 4); - for (e, o) in expected.iter().zip(observed.iter()) { - assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); - } - } - - #[test] - fn hcod9_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod9_encode(169), - Err(Error::SpectralCodebookIndexOutOfRange(9)) - )); - assert!(matches!( - hcod9_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(9)) - )); - } - - #[test] - fn hcod9_decode_single_zero_bit_yields_index_0() { - // Leading `0` → idx 0 (the zero-tuple). - // Remaining 7 bits of the byte untouched. - let bytes = [0b0111_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod9_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - assert_eq!(br.bit_position(), 1); - } - - #[test] - fn hcod9_decode_full_15_bit_codeword_round_trips_index_168() { - // Index 168 → length 15, codeword 0x7fff = 0b111_1111_1111_1111. - // Pack left-aligned into 2 bytes: 0xff, 0xfe. - let bytes = [0xff, 0xfe]; - let mut br = BitReader::new(&bytes); - let idx = hcod9_decode(&mut br).unwrap(); - assert_eq!(idx, 168); - assert_eq!(br.bit_position(), 15); - } - - #[test] - fn hcod9_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod9_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod9_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD9_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod9_write(&mut w, idx).unwrap(); - let (len, _) = hcod9_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod9_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod9_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod9_write(&mut w, 169), - Err(Error::SpectralCodebookIndexOutOfRange(9)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebook 9 expands the unsigned pair universe. - // Codebooks 7 and 8 share the `8 × 8 = 64`-entry `LAV = 7` - // lattice; Codebook 9 widens the per-coefficient ceiling to - // `LAV = 12`, producing the `13 × 13 = 169`-entry lattice and - // lifting the codeword ceiling from 10 (HCOD8) to 15 bits. - // ------------------------------------------------------------------- - - #[test] - fn codebook_9_universe_size_grows_to_169_from_codebook_8_64() { - assert_eq!(HCOD7_NUM_ENTRIES, 64); - assert_eq!(HCOD8_NUM_ENTRIES, 64); - assert_eq!(HCOD9_NUM_ENTRIES, 169); - // 169 / 64 ≈ 2.64 — the §4.6.3.3 universe more than doubles. - const { assert!(HCOD9_NUM_ENTRIES > 2 * HCOD8_NUM_ENTRIES) }; - } - - #[test] - fn codebook_9_zero_tuple_shares_codebook_7_head_placement() { - // Both Codebook 7 and Codebook 9 are unsigned pair books - // that park the §4.6.3.3 zero-tuple `(0, 0)` at index 0 with - // the single-bit `0` codeword — the shortest-possible slot. - // Codebook 8 lifts the zero-tuple off the 1-bit slot (it - // becomes 5 bits at index 0 and the 3-bit shortest moves to - // the (1, 1) interior at index 9). - let (l7_0, cw7_0) = hcod7_encode(0).unwrap(); - let (l9_0, cw9_0) = hcod9_encode(0).unwrap(); - assert_eq!((l7_0, cw7_0), (1, 0)); - assert_eq!((l9_0, cw9_0), (1, 0)); - } - - #[test] - fn codebook_9_far_corner_index_matches_lav_12_polynomial() { - // The §4.6.3.3 unsigned polynomial puts the max pair tuple - // `(LAV, LAV)` at index `LAV * (LAV + 1) + LAV`. For - // Codebook 9 with `LAV = 12` that's `12 * 13 + 12 = 168`, - // which carries the 15-bit codeword `0x7fff` — the widest - // codeword in any non-ESC spectrum book. - let (l9, cw9) = hcod9_encode(168).unwrap(); - assert_eq!((l9, cw9), (15, 0x7fff)); - // Compare to Codebook 8's far corner (LAV = 7) at index - // 63: that's only 10 bits wide. - let (l8, cw8) = hcod8_encode(63).unwrap(); - assert_eq!((l8, cw8), (10, 0x3ff)); - // Codebook 9's ceiling is 5 bits wider than Codebook 8's. - assert_eq!(HCOD9_MAX_LEN - HCOD8_MAX_LEN, 5); - } - - // ------------------------------------------------------------------- - // Codebook 10 — Table 4.A.11 - // ------------------------------------------------------------------- - - #[test] - fn hcod10_has_exactly_169_entries() { - // 13^2 = 169 (unsigned LAV=12 → mod = lav+1 = 13, dim = 2). - assert_eq!(HCOD10.len(), HCOD10_NUM_ENTRIES); - assert_eq!(HCOD10_NUM_ENTRIES, 169); - } - - #[test] - fn hcod10_max_length_is_12_bits() { - let max = HCOD10.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD10_MAX_LEN); - assert_eq!(HCOD10_MAX_LEN, 12); - } - - #[test] - fn hcod10_min_length_is_four_bits_at_interior_tuple_index_14() { - // Codebook 10 lifts the zero-tuple off the shortest slot (it - // sits at 6 bits at index 0) and parks the 4-bit shortest - // codeword on the interior `(1, 1)` tuple at index 14, the - // same head-displacement pattern Codebook 8 uses. - // Exactly three rows reach 4 bits: indices 14, 15, 27. - let mut four_bit_indices = Vec::new(); - for (idx, &(len, _)) in HCOD10.iter().enumerate() { - if len == 4 { - four_bit_indices.push(idx); - } - assert!( - len >= 4, - "every row must have length >= 4; idx={} len={}", - idx, - len - ); - } - assert_eq!(four_bit_indices, vec![14, 15, 27]); - } - - #[test] - fn hcod10_zero_tuple_lives_at_index_0_with_six_bit_codeword() { - // Codebook 10 places the §4.6.3.3 zero-tuple `(0, 0)` at - // index 0 via the unsigned polynomial idx = 0 * 13 + 0 = 0, - // but the Huffman row carries a 6-bit `0b100010` (`0x22`) - // codeword — not the 1-bit `0` that Codebook 9 uses. - let (len, cw) = HCOD10[0]; - assert_eq!(len, 6); - assert_eq!(cw, 0x22); - } - - #[test] - fn hcod10_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD10.iter().enumerate() { - let max = if len == 0 { 0 } else { (1u32 << len) - 1 }; - assert!( - u32::from(cw) <= max, - "idx={}: codeword {:#x} does not fit {} bits", - idx, - cw, - len - ); - } - } - - #[test] - fn hcod10_kraft_sum_is_two_to_the_twelve() { - // Σᵢ 2^(L_max − Lᵢ) must equal 2^L_max for a complete code. - let lmax = HCOD10_MAX_LEN; - let mut sum: u64 = 0; - for &(len, _) in &HCOD10 { - sum += 1u64 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u64 << lmax, "Kraft equality failed"); - assert_eq!(sum, 4096); - } - - #[test] - fn hcod10_is_complete() { - // Walk every 12-bit prefix, decode it via the production - // decoder, and confirm every prefix yields exactly one entry. - // Bonus: confirm the decoded index round-trips back to the - // same codeword via `hcod10_encode`. - for prefix in 0u32..(1u32 << HCOD10_MAX_LEN) { - // Pack `prefix` (12 bits) left-aligned into two bytes: - // high byte = bits 11..4, low byte = (bits 3..0) << 4. - let bytes = [(prefix >> 4) as u8, ((prefix & 0xf) << 4) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod10_decode(&mut br).expect("12-bit prefix must decode"); - let (len, cw) = hcod10_encode(idx).expect("decoded index must round-trip"); - // The decoded codeword should match the leading `len` - // bits of `prefix`. - let lead = prefix >> (HCOD10_MAX_LEN - u32::from(len)); - assert_eq!( - u32::from(cw), - lead, - "round-trip prefix={:#05x} idx={} len={} cw={:#x}", - prefix, - idx, - len, - cw - ); - } - } - - #[test] - fn hcod10_encode_index_0_is_six_bit_codeword_0x22() { - // Spec PDF Table 4.A.11 row 0: length 6, codeword 0x22 — the - // §4.6.3.3 zero-tuple `(0, 0)` does NOT carry the shortest - // possible codeword in Codebook 10. - let (len, cw) = hcod10_encode(0).unwrap(); - assert_eq!(len, 6); - assert_eq!(cw, 0x22); - } - - #[test] - fn hcod10_encode_shortest_codewords_match_spec() { - // Spec PDF Table 4.A.11 spot checks: the three 4-bit rows are - // indices 14, 15, 27 with codewords 0, 1, 2. - assert_eq!(hcod10_encode(14).unwrap(), (4, 0x0)); - assert_eq!(hcod10_encode(15).unwrap(), (4, 0x1)); - assert_eq!(hcod10_encode(27).unwrap(), (4, 0x2)); - } - - #[test] - fn hcod10_encode_last_entry_matches_table() { - // Spec PDF Table 4.A.11 row 168: length 12, codeword 0xfff — - // the far corner `(12, 12)` of the unsigned `13 × 13` pair - // lattice. - let (len, cw) = hcod10_encode(168).unwrap(); - assert_eq!(len, 12); - assert_eq!(cw, 0xfff); - } - - #[test] - fn hcod10_encode_eight_12_bit_rows_match_table() { - // Exactly eight rows reach the 12-bit ceiling in - // Table 4.A.11. Their indices and codewords are pinned here. - let expected = [ - (12u32, 0x0ffdu16), - (129u32, 0x0ffau16), - (142u32, 0x0ff9u16), - (155u32, 0x0ffbu16), - (165u32, 0x0ff8u16), - (166u32, 0x0ffeu16), - (167u32, 0x0ffcu16), - (168u32, 0x0fffu16), - ]; - let observed: Vec<_> = HCOD10 - .iter() - .enumerate() - .filter_map(|(i, &(l, cw))| if l == 12 { Some((i as u32, cw)) } else { None }) - .collect(); - assert_eq!(observed.len(), 8); - for (e, o) in expected.iter().zip(observed.iter()) { - assert_eq!(*e, *o, "expected {:?} got {:?}", e, o); - } - } - - #[test] - fn hcod10_encode_rejects_out_of_range_index() { - assert!(matches!( - hcod10_encode(169), - Err(Error::SpectralCodebookIndexOutOfRange(10)) - )); - assert!(matches!( - hcod10_encode(0xffff_ffff), - Err(Error::SpectralCodebookIndexOutOfRange(10)) - )); - } - - #[test] - fn hcod10_decode_four_bit_zero_codeword_yields_index_14() { - // Index 14 → length 4, codeword 0 = 0b0000. Pack - // left-aligned in a single byte: top 4 bits = 0, bottom 4 - // bits arbitrary. - let bytes = [0b0000_1111u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod10_decode(&mut br).unwrap(); - assert_eq!(idx, 14); - assert_eq!(br.bit_position(), 4); - } - - #[test] - fn hcod10_decode_full_12_bit_codeword_round_trips_index_168() { - // Index 168 → length 12, codeword 0xfff = 0b1111_1111_1111. - // Pack left-aligned: high byte = 0xff (bits 11..4), low byte - // = (0xf << 4) = 0xf0 (bits 3..0 in the top of the low byte). - let bytes = [0xff, 0xf0]; - let mut br = BitReader::new(&bytes); - let idx = hcod10_decode(&mut br).unwrap(); - assert_eq!(idx, 168); - assert_eq!(br.bit_position(), 12); - } - - #[test] - fn hcod10_decode_propagates_unexpected_end() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - assert_eq!(hcod10_decode(&mut br), Err(Error::UnexpectedEnd)); - } - - #[test] - fn hcod10_write_then_decode_round_trips_every_index() { - for idx in 0..HCOD10_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod10_write(&mut w, idx).unwrap(); - let (len, _) = hcod10_encode(idx).unwrap(); - let mut w2 = w; - let pad = (8 - (u32::from(len) % 8)) % 8; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let decoded = hcod10_decode(&mut br).unwrap(); - assert_eq!( - decoded, idx, - "round-trip mismatch at idx={} (encoded as {} bits)", - idx, len - ); - } - } - - #[test] - fn hcod10_write_rejects_out_of_range_index() { - let mut w = BitWriter::new(); - assert!(matches!( - hcod10_write(&mut w, 169), - Err(Error::SpectralCodebookIndexOutOfRange(10)) - )); - } - - // ------------------------------------------------------------------- - // Cross-check: Codebooks 9 and 10 share the unsigned dim-2 LAV-12 - // universe (169 entries each) but differ in codeword distribution. - // Codebook 9's ceiling is 15 bits with the zero-tuple at the - // 1-bit head; Codebook 10's ceiling pulls down to 12 bits and - // lifts the zero-tuple off the head — the shortest 4-bit slot - // sits on the interior `(1, 1)` tuple at index 14. - // ------------------------------------------------------------------- - - #[test] - fn codebook_10_matches_codebook_9_universe_size() { - assert_eq!(HCOD9_NUM_ENTRIES, 169); - assert_eq!(HCOD10_NUM_ENTRIES, 169); - } - - #[test] - fn codebook_10_ceiling_is_3_bits_below_codebook_9() { - // Codebook 9's ceiling is 15 bits; Codebook 10's ceiling - // is 12 bits — a 3-bit pull-down reflecting the flatter - // codeword distribution targeted by Codebook 10's - // encoder-statistics tuning. - assert_eq!(HCOD9_MAX_LEN, 15); - assert_eq!(HCOD10_MAX_LEN, 12); - assert_eq!(HCOD9_MAX_LEN - HCOD10_MAX_LEN, 3); - } - - #[test] - fn codebook_10_lifts_zero_tuple_off_codebook_9_head_placement() { - // Codebook 9 parks the §4.6.3.3 zero-tuple at index 0 with - // the 1-bit `0` codeword (shortest possible slot). Codebook - // 10 keeps the zero-tuple at index 0 (the §4.6.3.3 polynomial - // index is fixed by the tuple, not the codebook) but the - // codeword swells to 6 bits — the shortest 4-bit slot - // migrates onto the interior `(1, 1)` tuple at index 14. - let (l9_0, cw9_0) = hcod9_encode(0).unwrap(); - let (l10_0, cw10_0) = hcod10_encode(0).unwrap(); - let (l10_14, cw10_14) = hcod10_encode(14).unwrap(); - assert_eq!((l9_0, cw9_0), (1, 0)); - assert_eq!((l10_0, cw10_0), (6, 0x22)); - assert_eq!((l10_14, cw10_14), (4, 0)); - } - - #[test] - fn codebook_10_far_corner_matches_codebook_9_far_corner_index() { - // Both codebooks share LAV = 12, so the §4.6.3.3 unsigned - // polynomial parks `(12, 12)` at index 12 * 13 + 12 = 168. - // Codeword shapes differ: Codebook 9 → 15-bit 0x7fff; - // Codebook 10 → 12-bit 0xfff. - let (l9, cw9) = hcod9_encode(168).unwrap(); - let (l10, cw10) = hcod10_encode(168).unwrap(); - assert_eq!((l9, cw9), (15, 0x7fff)); - assert_eq!((l10, cw10), (12, 0xfff)); - } - - // ------------------------------------------------------------------- - // Codebook 11 invariants (Table 4.A.12) - // ------------------------------------------------------------------- - - #[test] - fn hcod11_has_exactly_289_entries() { - // 17^2 = 289 (unsigned LAV=16 → mod = 17, dim = 2). - assert_eq!(HCOD11.len(), HCOD11_NUM_ENTRIES); - assert_eq!(HCOD11_NUM_ENTRIES, 289); - } - - #[test] - fn hcod11_max_length_is_12_bits() { - let max = HCOD11.iter().map(|&(len, _)| len).max().unwrap(); - assert_eq!(u32::from(max), HCOD11_MAX_LEN); - assert_eq!(HCOD11_MAX_LEN, 12); - } - - #[test] - fn hcod11_min_length_is_four_bits_at_zero_tuple_and_interior_pair() { - // The 4-bit floor is shared by exactly two rows: index 0 - // (the zero-tuple (0, 0)) and index 18 (the interior (1, 1) - // pair, since 1 * 17 + 1 = 18). The zero-tuple carries - // 0b0000 and (1, 1) carries 0b0001. - let mut min: u32 = u32::MAX; - let mut min_indices: Vec = Vec::new(); - for (idx, &(len, _)) in HCOD11.iter().enumerate() { - let l = u32::from(len); - if l < min { - min = l; - min_indices.clear(); - min_indices.push(idx); - } else if l == min { - min_indices.push(idx); - } - } - assert_eq!(min, 4); - assert_eq!(min_indices, vec![0, 18]); - } - - #[test] - fn hcod11_zero_tuple_lives_at_index_0_with_four_bit_codeword() { - // The §4.6.3.3 unsigned polynomial idx = y * 17 + z places - // the zero-tuple (0, 0) at index 0; Codebook 11 hands it - // the shortest 4-bit codeword 0b0000. - let (len, cw) = HCOD11[0]; - assert_eq!(len, 4); - assert_eq!(cw, 0x0000); - } - - #[test] - fn hcod11_interior_one_one_tuple_lives_at_index_18_with_four_bit_codeword() { - // 1 * 17 + 1 = 18 → the second 4-bit slot, codeword 0b0001. - let (len, cw) = HCOD11[18]; - assert_eq!(len, 4); - assert_eq!(cw, 0x0001); - } - - #[test] - fn hcod11_far_corner_lives_at_index_288_with_five_bit_codeword() { - // (16, 16) at 16 * 17 + 16 = 288 — both coefficients flagged - // as ESC. Codebook 11 spends only 5 bits on this far corner - // (codeword 0b00100), keeping the in-band codeword short - // because the wire layout extends with two escape sequences - // and (where the magnitudes are non-zero) two sign bits. - let (len, cw) = HCOD11[288]; - assert_eq!(len, 5); - assert_eq!(cw, 0x0004); - } - - #[test] - fn hcod11_codewords_fit_their_declared_length() { - for (idx, &(len, cw)) in HCOD11.iter().enumerate() { - assert!( - u32::from(cw) < (1u32 << u32::from(len)), - "row {idx}: codeword 0x{cw:x} >= 2^{len}", - ); - } - } - - #[test] - fn hcod11_kraft_sum_is_two_to_the_twelve() { - // Σ 2^(L_max - L) = 2^L_max ⇔ complete prefix code. - let lmax = HCOD11_MAX_LEN; - let mut sum: u32 = 0; - for &(len, _) in &HCOD11 { - sum += 1u32 << (lmax - u32::from(len)); - } - assert_eq!(sum, 1u32 << lmax); - assert_eq!(sum, 4096); - } - - #[test] - fn hcod11_is_complete() { - // Exhaustively walk every 12-bit prefix and verify each - // matches exactly one entry. This is the strongest - // possible check that the table is a complete prefix code - // and that `hcod11_decode`'s `unreachable!()` is dead. - for prefix in 0u32..(1u32 << HCOD11_MAX_LEN) { - let bytes = [((prefix >> 4) & 0xff) as u8, ((prefix & 0xf) << 4) as u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod11_decode(&mut br).expect("12-bit prefix must decode"); - let (len, cw) = hcod11_encode(idx).expect("decoded index must round-trip"); - // The decoded prefix must match the leading `len` bits - // of our 12-bit walk. - let lead = prefix >> (HCOD11_MAX_LEN - u32::from(len)); - assert_eq!( - lead, - u32::from(cw), - "prefix 0b{prefix:012b} decoded idx={idx} → codeword ({len}, 0x{cw:x})", - ); - } - } - - #[test] - fn hcod11_twelve_bit_ceiling_hits_exactly_six_indices() { - // Indices 12, 14, 15, 255, 269, 270 are the only rows whose - // codeword length reaches the 12-bit ceiling. - let ceiling: Vec = HCOD11 - .iter() - .enumerate() - .filter_map(|(i, &(len, _))| if len == 12 { Some(i) } else { None }) - .collect(); - assert_eq!(ceiling, vec![12, 14, 15, 255, 269, 270]); - assert_eq!(HCOD11[12], (12, 0x0ffb)); - assert_eq!(HCOD11[14], (12, 0x0ffa)); - assert_eq!(HCOD11[15], (12, 0x0ffe)); - assert_eq!(HCOD11[255], (12, 0x0ffd)); - assert_eq!(HCOD11[269], (12, 0x0ffc)); - assert_eq!(HCOD11[270], (12, 0x0fff)); - } - - #[test] - fn hcod11_half_esc_rows_match_spec() { - // Index 16 corresponds to (y, z) = (0, 16), index 272 to - // (16, 0). Both are half-ESC tuples — exactly one - // coefficient at the §4.6.3.3 escape flag. - assert_eq!(HCOD11[16], (10, 0x038e)); - assert_eq!(HCOD11[272], (9, 0x01c2)); - } - - #[test] - fn hcod11_encode_rejects_out_of_range_indices() { - for bad in [289u32, 290, 300, 1000, u32::MAX] { - assert!(matches!( - hcod11_encode(bad), - Err(Error::SpectralCodebookIndexOutOfRange(11)) - )); - } - } - - #[test] - fn hcod11_write_rejects_out_of_range_indices() { - let mut w = BitWriter::new(); - for bad in [289u32, 1000, u32::MAX] { - assert!(matches!( - hcod11_write(&mut w, bad), - Err(Error::SpectralCodebookIndexOutOfRange(11)) - )); - } - } - - #[test] - fn hcod11_decode_index_0_zero_bits() { - // Index 0 → 4-bit `0`. Padding to a byte boundary with zeros - // keeps the wire byte at 0x00. - let bytes = [0x00u8]; - let mut br = BitReader::new(&bytes); - let idx = hcod11_decode(&mut br).unwrap(); - assert_eq!(idx, 0); - assert_eq!(br.bit_position(), 4u64); - } - - #[test] - fn hcod11_decode_index_270_full_12_bit_far_codeword() { - // Index 270 → 12-bit 0xfff packed left-aligned: high byte = - // 0xff (bits 11..4), low byte = (0xf << 4) = 0xf0 (bits - // 3..0 in the high nibble of the low byte). - let bytes = [0xffu8, 0xf0]; - let mut br = BitReader::new(&bytes); - let idx = hcod11_decode(&mut br).unwrap(); - assert_eq!(idx, 270); - assert_eq!(br.bit_position(), 12u64); - } - - #[test] - fn hcod11_writer_round_trip_pins_every_index() { - // Writer → reader round-trip for every legal index. Each - // index must produce the exact bit-stream the encode - // function claims, and decode must recover the original - // index using exactly `len` bits. - for idx in 0..HCOD11_NUM_ENTRIES as u32 { - let mut w = BitWriter::new(); - hcod11_write(&mut w, idx).unwrap(); - let (len, _) = hcod11_encode(idx).unwrap(); - let pad = (8 - (u32::from(len) % 8)) % 8; - let mut w2 = w; - if pad > 0 { - w2.write_u32(0, pad); - } - let bytes = w2.into_bytes(); - let mut br = BitReader::new(&bytes); - let got = hcod11_decode(&mut br).unwrap(); - assert_eq!(got, idx, "round-trip mismatch at idx={idx}"); - assert_eq!( - br.bit_position(), - u64::from(len), - "bit consumption mismatch at idx={idx}", - ); - } - } - - #[test] - fn hcod11_decoder_returns_unexpected_end_on_truncation() { - let bytes: [u8; 0] = []; - let mut br = BitReader::new(&bytes); - let err = hcod11_decode(&mut br).unwrap_err(); - assert_eq!(err, Error::UnexpectedEnd); - } - - #[test] - fn hcod11_max_len_constant_matches_table_data() { - let mut observed_max = 0u32; - for idx in 0..HCOD11_NUM_ENTRIES as u32 { - let (len, _) = hcod11_encode(idx).unwrap(); - observed_max = observed_max.max(u32::from(len)); - } - assert_eq!(observed_max, HCOD11_MAX_LEN); - } - - #[test] - fn hcod11_ceiling_matches_codebook_10_ceiling() { - // Codebook 10 caps at 12 bits; Codebook 11 also caps at 12 - // bits — the universe widens (169 → 289 entries) but the - // codeword ceiling stays the same because the ESC sequence - // soaks up the tail-distribution rather than spending - // longer Huffman codewords on it. - assert_eq!(HCOD10_MAX_LEN, HCOD11_MAX_LEN); - assert_eq!(HCOD11_MAX_LEN, 12); - } - - #[test] - fn hcod11_universe_is_69_entries_wider_than_codebook_10() { - // 289 - 169 = 120 extra rows = (17 + 17 - 1) extra entries - // along the ESC border `y == 16 || z == 16`. - assert_eq!(HCOD11_NUM_ENTRIES - HCOD10_NUM_ENTRIES, 120); - assert_eq!(HCOD11_NUM_ENTRIES, 289); - assert_eq!(HCOD10_NUM_ENTRIES, 169); - } -} diff --git a/crates/vendor/oxideav-aac/src/ssr.rs b/crates/vendor/oxideav-aac/src/ssr.rs deleted file mode 100644 index ea0d30e8..00000000 --- a/crates/vendor/oxideav-aac/src/ssr.rs +++ /dev/null @@ -1,549 +0,0 @@ -//! SSR per-channel gain-control + IPQF back-end driver (ISO/IEC -//! 14496-3 §4.6.12). -//! -//! [`SsrGainControl`] composes the four-band gain-control state -//! ([`crate::gain_control::GainBandState`]) and the IPQF synthesizer -//! ([`crate::ipqf::Ipqf`]) into one persistent per-channel pipeline. -//! Per frame it consumes the four per-band IMDCT outputs `U_{W,B}` plus -//! the `gain_control_data()` side info and returns the reconstructed -//! PCM time signal `AS(n)`: -//! -//! ```text -//! for each PQF band B in 0..4: -//! V_B = GainBandState[B].window_overlap(ladder[B], U_B, seq) §4.6.12.3.3 -//! AS = IPQF.synthesize([V_0, V_1, V_2, V_3]) §4.6.12.3.4 -//! ``` -//! -//! ## Front half -//! -//! [`SsrGainControl`] runs the §4.6.12.3.3–4 *back half* of the SSR -//! tool: the per-band gain windowing/overlap and the IPQF synthesis, -//! from caller-supplied non-overlapped `U_{W,B}` columns. The -//! §4.6.12.1 *front half* — splitting the transmitted spectrum into -//! the four PQF-band coefficient columns, the even-band spectral -//! reversal, and the per-band 256-line (long) / 32-line (short) -//! IMDCTs + windows — lives in [`crate::ssr_filterbank`]; -//! [`SsrChannelDecoder`] chains the two into the complete -//! spectrum → PCM pipeline. -//! -//! ## Provenance -//! -//! Composes the §4.6.12.1 front half ([`crate::ssr_filterbank`]) and -//! the §4.6.12.3.1–4 stages implemented in [`crate::gain_control`] and -//! [`crate::ipqf`]; no new tables. No external SSR implementation was -//! consulted — the full-pipeline tests below validate against the -//! Annex C.2.1.1 analysis PQF and the §4.6.11 TDAC property. - -use crate::gain_control::{band_record, GainBandState}; -use crate::gain_control_data::GainControlData; -use crate::ics_info::{IcsInfo, WindowSequence}; -use crate::ipqf::{Ipqf, NUM_BANDS}; -use crate::ssr_filterbank::SsrSynthesis; -use crate::Result; - -/// One channel's persistent SSR gain-control + IPQF state: the four -/// per-band [`GainBandState`] carries plus the streaming [`Ipqf`]. -#[derive(Debug, Clone)] -pub struct SsrGainControl { - /// Per-PQF-band gain-control cross-frame state (`PFMD` / `PT`). - bands: [GainBandState; NUM_BANDS], - /// The streaming IPQF synthesizer (cross-frame band history). - ipqf: Ipqf, -} - -impl Default for SsrGainControl { - fn default() -> Self { - Self::new() - } -} - -impl SsrGainControl { - /// A fresh per-channel SSR pipeline with the §4.6.12 spec initial - /// state (`PFMD ≡ 1.0`, `PT ≡ 0.0`, zero IPQF history). - #[must_use] - pub fn new() -> Self { - SsrGainControl { - bands: core::array::from_fn(|_| GainBandState::new()), - ipqf: Ipqf::new(), - } - } - - /// Reconstruct one frame of PCM `AS(n)` from the four per-band IMDCT - /// outputs and the frame's gain-control side info. - /// - /// * `u` — the four non-overlapped per-band IMDCT outputs - /// `U_{W,B}`. `u[B]` is the band-`B` column: a single 512-sample - /// window for the long sequences, or eight 64-sample windows - /// concatenated for `EIGHT_SHORT_SEQUENCE`. - /// * `gcd` — the decoded `gain_control_data()` (`None` ⇒ no gain - /// control active this frame, every band runs `T = U`). - /// * `seq` — the frame's `window_sequence`. - /// - /// Returns `NUM_BANDS · |V_B|` PCM samples (`4 · 256 = 1024` for the - /// steady `ONLY_LONG` / `EIGHT_SHORT` case). - #[must_use] - pub fn decode_frame( - &mut self, - u: &[Vec; NUM_BANDS], - gcd: Option<&GainControlData>, - seq: WindowSequence, - ) -> Vec { - // §4.6.12.3.3 — per-band gain windowing + overlap → V_B. - let mut v: [Vec; NUM_BANDS] = core::array::from_fn(|_| Vec::new()); - for (b, slot) in v.iter_mut().enumerate() { - // Spec band index is 1..=3 for gain-controlled bands; PQF - // band 0 never carries a ladder (§4.6.12.3.3 `B == 0`). - let ladder = gcd.and_then(|g| band_record(g, b)); - *slot = self.bands[b].window_overlap(ladder, &u[b], seq); - } - - // §4.6.12.3.4 — IPQF synthesis. All four V_B share the same - // per-frame length by construction. - let len = v[0].len(); - debug_assert!(v.iter().all(|vb| vb.len() == len)); - let band_refs: [&[f64]; NUM_BANDS] = core::array::from_fn(|b| v[b].as_slice()); - self.ipqf.synthesize(&band_refs, len) - } -} - -/// One channel's *complete* §4.6.12 SSR reconstruction pipeline: the -/// §4.6.12.1 front-half filterbank ([`SsrSynthesis`] — band split, -/// even-band reversal, per-band 256/32-line IMDCTs + windows) chained -/// into the §4.6.12.3 gain-control + IPQF back end -/// ([`SsrGainControl`]). -/// -/// This is the SSR (AOT 3) replacement for the per-channel §4.6.11 -/// [`crate::filterbank::Filterbank`]: it consumes the same decoded -/// 1024-line spectrum (window-major for `EIGHT_SHORT_SEQUENCE`, after -/// TNS) and produces the frame's PCM time signal `AS(n)`. -#[derive(Debug, Clone, Default)] -pub struct SsrChannelDecoder { - /// §4.6.12.1 front half (carries the previous block's - /// `window_shape`). - synth: SsrSynthesis, - /// §4.6.12.3 back half (carries `PFMD` / `PT` / IPQF history). - gain: SsrGainControl, -} - -impl SsrChannelDecoder { - /// A fresh SSR channel pipeline with the spec initial state. - #[must_use] - pub fn new() -> Self { - SsrChannelDecoder::default() - } - - /// Decode one frame: 1024-line spectrum (+ this frame's - /// `gain_control_data()`, if any) → PCM `AS(n)`. - /// - /// The output length follows the §4.6.12.3.3 band fragment length - /// times the four-band IPQF interpolation: 1024 samples for - /// `ONLY_LONG` / `EIGHT_SHORT`, 1472 for `LONG_START`, 576 for - /// `LONG_STOP` (a `START`/`STOP` pair still totals 2048, so stream - /// timing is preserved). - pub fn decode_frame( - &mut self, - spec: &[f64], - ics_info: &IcsInfo, - gcd: Option<&GainControlData>, - ) -> Result> { - let u = self.synth.windowed_bands(spec, ics_info)?; - Ok(self.gain.decode_frame(&u, gcd, ics_info.window_sequence)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Four bands of constant-zero U give silence out. - #[test] - fn zero_bands_give_silence() { - let mut ssr = SsrGainControl::new(); - let u: [Vec; NUM_BANDS] = core::array::from_fn(|_| vec![0.0f64; 512]); - let pcm = ssr.decode_frame(&u, None, WindowSequence::OnlyLong); - assert_eq!(pcm.len(), 1024); - assert!(pcm.iter().all(|&x| x == 0.0)); - } - - /// A steady ONLY_LONG stream produces 1024 PCM samples per frame and - /// the pipeline is finite + deterministic. - #[test] - fn only_long_frame_is_1024_pcm() { - let mut ssr = SsrGainControl::new(); - let u: [Vec; NUM_BANDS] = - core::array::from_fn(|b| (0..512).map(|j| ((b * 512 + j) as f64) * 1e-3).collect()); - let pcm0 = ssr.decode_frame(&u, None, WindowSequence::OnlyLong); - assert_eq!(pcm0.len(), 1024); - assert!(pcm0.iter().all(|x| x.is_finite())); - // A second identical frame also yields 1024 and threads state. - let pcm1 = ssr.decode_frame(&u, None, WindowSequence::OnlyLong); - assert_eq!(pcm1.len(), 1024); - // The first and second frames differ (the overlap tail carries). - assert!(pcm0 != pcm1); - } - - /// Gain control with `max_band == 0` (the bare 2-bit field, no - /// ladders) is the identity: same PCM as `None`. - #[test] - fn max_band_zero_matches_no_gain() { - let u: [Vec; NUM_BANDS] = core::array::from_fn(|b| { - (0..512) - .map(|j| ((b + 1) as f64 * (j as f64 + 1.0)).sin()) - .collect() - }); - let gcd = GainControlData { - max_band: 0, - bands: Vec::new(), - }; - let mut a = SsrGainControl::new(); - let mut b = SsrGainControl::new(); - let pa = a.decode_frame(&u, Some(&gcd), WindowSequence::OnlyLong); - let pb = b.decode_frame(&u, None, WindowSequence::OnlyLong); - assert_eq!(pa.len(), pb.len()); - for (x, y) in pa.iter().zip(pb.iter()) { - assert!((x - y).abs() < 1e-12); - } - } - - /// EIGHT_SHORT bands (eight 64-sample windows each) also reconstruct - /// 1024 PCM samples per frame. - #[test] - fn eight_short_frame_is_1024_pcm() { - let mut ssr = SsrGainControl::new(); - let u: [Vec; NUM_BANDS] = - core::array::from_fn(|_| (0..512).map(|j| (j as f64 * 0.01).cos()).collect()); - let pcm = ssr.decode_frame(&u, None, WindowSequence::EightShort); - assert_eq!(pcm.len(), 1024); - assert!(pcm.iter().all(|x| x.is_finite())); - } -} - -/// Full-pipeline round-trip tests: the Annex C.2.1.1 analysis PQF + -/// the §4.6.11.3.2 (quarter-scale) analysis windows + forward MDCTs -/// mirror the encoder; [`SsrChannelDecoder`] must reconstruct the -/// input within the PQF pair's near-perfect-reconstruction bound. -#[cfg(test)] -mod round_trip_tests { - use super::*; - use crate::filterbank::{forward_mdct, long_sequence_window_n, short_window_n}; - use crate::gain_control::{band_record, pfmd_len, BandGainFunction}; - use crate::gain_control_data::{GainAdjust, GainBand, GainWindow}; - use crate::ics_info::{IcsInfo, WindowShape}; - use crate::ssr_filterbank::pqf_test_support::{pqf_analysis, PQF_CASCADE_DELAY}; - use crate::ssr_filterbank::{SSR_LONG_TRANSFORM, SSR_SHORT_TRANSFORM}; - use core::f64::consts::PI; - - /// A minimal [`IcsInfo`] carrying just what the SSR pipeline reads. - fn ics(shape: WindowShape, seq: WindowSequence) -> IcsInfo { - let short = seq == WindowSequence::EightShort; - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: seq, - window_shape: shape, - max_sfb: 0, - scale_factor_grouping: if short { Some(0) } else { None }, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: if short { 8 } else { 1 }, - num_window_groups: if short { 8 } else { 1 }, - window_group_length: if short { vec![1; 8] } else { vec![1] }, - num_swb: 0, - } - } - - /// A broadband deterministic test signal exciting all four PQF - /// bands: four tones (one per band quarter) plus a slow envelope. - fn test_signal(len: usize) -> Vec { - (0..len) - .map(|n| { - let t = n as f64; - let env = 0.6 + 0.4 * (2.0 * PI * t / 3000.0).sin(); - env * ((0.05 * t).sin() - + 0.7 * (0.9 * t).sin() - + 0.5 * (1.8 * t).sin() - + 0.4 * (2.9 * t).sin()) - }) - .collect() - } - - /// Encoder-mirror state: per-band position of the next frame's - /// window origin (in band samples) plus the previous block's - /// window shape and the per-band `PFMD` gain threading. - struct MirrorEncoder { - /// Absolute band-sample position `P_f` where this frame's `V` - /// starts. - p: usize, - prev_shape: Option, - /// Per-band `PFMD` carry for the encoder-side GMF (256 - /// entries, prefix-read like the decoder's). - pfmd: [Vec; NUM_BANDS], - } - - impl MirrorEncoder { - fn new() -> Self { - MirrorEncoder { - p: 0, - prev_shape: None, - pfmd: core::array::from_fn(|_| vec![1.0f64; 256]), - } - } - - /// Encode one frame: window the four band signals at the - /// §4.6.12.3.3-mirror positions, apply the §4.6.12.3.2 `GMF` - /// (identity when `gcd` is `None`), forward-MDCT each band, - /// reverse the even (0-based 1 and 3) bands and assemble the - /// 1024-line spectrum. Advances the band position by the - /// frame's `V` length. - fn encode_frame( - &mut self, - bands: &[Vec; NUM_BANDS], - seq: WindowSequence, - shape: WindowShape, - gcd: Option<&GainControlData>, - ) -> Vec { - let left = self.prev_shape.unwrap_or(shape); - let mut spec = vec![0.0f64; 1024]; - - // Per-band GMF (1/AD) for this frame, threading PFMD the - // same way the decoder does. - let gmf: [Vec>; NUM_BANDS] = core::array::from_fn(|b| { - let record = gcd.and_then(|g| band_record(g, b)); - let f = match record { - Some(rec) => { - BandGainFunction::reconstruct(rec, seq, &self.pfmd[b][..pfmd_len(seq)]) - } - None => BandGainFunction::identity(seq), - }; - self.pfmd[b][..f.pfmd_next.len()].copy_from_slice(&f.pfmd_next); - f.ad.iter() - .map(|w| w.iter().map(|&a| 1.0 / a).collect()) - .collect() - }); - - match seq { - WindowSequence::EightShort => { - // Window w over band samples [p + 32w, p + 32w + 64). - for w in 0..8 { - let win = short_window_n(SSR_SHORT_TRANSFORM, w, left, shape); - for (b, band) in bands.iter().enumerate() { - let z: Vec = (0..SSR_SHORT_TRANSFORM) - .map(|n| band[self.p + 32 * w + n] * gmf[b][w][n] * win[n]) - .collect(); - let mut coeffs = forward_mdct(&z, SSR_SHORT_TRANSFORM); - if b % 2 == 1 { - coeffs.reverse(); - } - spec[128 * w + 32 * b..128 * w + 32 * b + 32].copy_from_slice(&coeffs); - } - } - self.p += 256; - } - _ => { - // Long window over [p, p+512) (LONG_STOP: the - // window origin sits 112 band samples *before* the - // frame's V start, mirroring §4.6.12.3.3). - let origin = match seq { - WindowSequence::LongStop => self.p - 112, - _ => self.p, - }; - let win = long_sequence_window_n( - SSR_LONG_TRANSFORM, - SSR_SHORT_TRANSFORM, - seq, - left, - shape, - ) - .unwrap(); - for (b, band) in bands.iter().enumerate() { - let z: Vec = (0..SSR_LONG_TRANSFORM) - .map(|n| band[origin + n] * gmf[b][0][n] * win[n]) - .collect(); - let mut coeffs = forward_mdct(&z, SSR_LONG_TRANSFORM); - if b % 2 == 1 { - coeffs.reverse(); - } - spec[256 * b..256 * b + 256].copy_from_slice(&coeffs); - } - self.p += match seq { - WindowSequence::OnlyLong => 256, - WindowSequence::LongStart => 368, - WindowSequence::LongStop => 144, - WindowSequence::EightShort => unreachable!(), - }; - } - } - self.prev_shape = Some(shape); - spec - } - } - - /// Round-trip error-to-signal RMS of `y` (decoder output) against - /// `x` delayed by the PQF cascade, over `[skip, n)`. - fn err_ratio(x: &[f64], y: &[f64], skip: usize) -> f64 { - let n = y.len().min(x.len().saturating_sub(PQF_CASCADE_DELAY)); - let (mut err, mut sig) = (0.0f64, 0.0f64); - for i in skip..n { - // y(i) reconstructs x(i - delay): compare shifted. - let d = y[i] - x[i - PQF_CASCADE_DELAY]; - err += d * d; - sig += x[i - PQF_CASCADE_DELAY] * x[i - PQF_CASCADE_DELAY]; - } - (err / sig).sqrt() - } - - /// Steady `ONLY_LONG` frames round-trip through the complete - /// §4.6.12 pipeline within the PQF pair's reconstruction bound, - /// for both window shapes. - #[test] - fn full_pipeline_round_trips_only_long() { - let frames = 20usize; - let x = test_signal(4 * 256 * (frames + 3)); - let bands = pqf_analysis(&x); - for shape in [WindowShape::Sine, WindowShape::Kbd] { - let mut enc = MirrorEncoder::new(); - let mut dec = SsrChannelDecoder::new(); - let info = ics(shape, WindowSequence::OnlyLong); - let mut y = Vec::new(); - for _ in 0..frames { - let spec = enc.encode_frame(&bands, WindowSequence::OnlyLong, shape, None); - y.extend(dec.decode_frame(&spec, &info, None).unwrap()); - } - assert_eq!(y.len(), 1024 * frames); - let ratio = err_ratio(&x, &y, 2048); - assert!(ratio < 1e-3, "{shape:?} round-trip err/sig = {ratio}"); - } - } - - /// A full window-sequence transition chain (`ONLY_LONG → - /// LONG_START → EIGHT_SHORT ×2 → LONG_STOP → ONLY_LONG`) - /// round-trips, with the §4.6.12.3.3 variable per-frame output - /// lengths (1024 / 1472 / 1024 / 576) preserving stream timing. - #[test] - fn full_pipeline_round_trips_window_transitions() { - use WindowSequence::{EightShort, LongStart, LongStop, OnlyLong}; - let chain = [ - OnlyLong, OnlyLong, OnlyLong, LongStart, EightShort, EightShort, LongStop, OnlyLong, - OnlyLong, LongStart, EightShort, LongStop, OnlyLong, OnlyLong, - ]; - let x = test_signal(4 * 256 * (chain.len() + 3)); - let bands = pqf_analysis(&x); - let mut enc = MirrorEncoder::new(); - let mut dec = SsrChannelDecoder::new(); - let mut y = Vec::new(); - let mut expect_len = 0usize; - for &seq in &chain { - let spec = enc.encode_frame(&bands, seq, WindowShape::Sine, None); - let out = dec - .decode_frame(&spec, &ics(WindowShape::Sine, seq), None) - .unwrap(); - expect_len += match seq { - OnlyLong | EightShort => 1024, - LongStart => 1472, - LongStop => 576, - }; - y.extend(out); - } - assert_eq!(y.len(), expect_len); - let ratio = err_ratio(&x, &y, 2048); - assert!( - ratio < 1e-3, - "transition-chain round-trip err/sig = {ratio}" - ); - } - - /// Gain ladders cancel end to end: the encoder applies the - /// §4.6.12.3.2 `GMF`, the decoder its inverse `AD`, and the - /// round-trip stays close to the input — while decoding the same - /// stream *without* the gain data leaves the gain modification in - /// the output (large error). Pins the orientation of the whole - /// §4.6.12.3 gain path against the front half. - #[test] - fn gain_ladders_cancel_in_round_trip() { - let frames = 16usize; - let x = test_signal(4 * 256 * (frames + 3)); - let bands = pqf_analysis(&x); - - // Ladders on bands 1..=3 (spec 2nd..4th), one gain change per - // window: modest ±1-exponent steps at varied positions. - let gcd = GainControlData { - max_band: 3, - bands: vec![ - GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 5, // AdjLev = 1 ⇒ ALEV = 2. - aloccode: 4, // ALOC = 32. - }], - }], - }, - GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 3, // AdjLev = −1 ⇒ ALEV = 1/2. - aloccode: 12, // ALOC = 96. - }], - }], - }, - GainBand { - windows: vec![GainWindow { - adjustments: vec![GainAdjust { - alevcode: 6, // AdjLev = 2 ⇒ ALEV = 4. - aloccode: 20, // ALOC = 160. - }], - }], - }, - ], - }; - - let mut enc = MirrorEncoder::new(); - let mut dec = SsrChannelDecoder::new(); - let mut dec_plain = SsrChannelDecoder::new(); - let info = ics(WindowShape::Sine, WindowSequence::OnlyLong); - let mut y = Vec::new(); - let mut y_plain = Vec::new(); - for _ in 0..frames { - let spec = enc.encode_frame( - &bands, - WindowSequence::OnlyLong, - WindowShape::Sine, - Some(&gcd), - ); - y.extend(dec.decode_frame(&spec, &info, Some(&gcd)).unwrap()); - y_plain.extend(dec_plain.decode_frame(&spec, &info, None).unwrap()); - } - let ratio = err_ratio(&x, &y, 2048); - // Gain steps re-introduce a little aliasing at the transition - // ramps (the §4.6.12.3.2 Inter() ramp bounds it); the - // compensated round trip must stay small… - assert!(ratio < 0.02, "gain-compensated err/sig = {ratio}"); - // …while dropping the gain data leaves the modification in. - let ratio_plain = err_ratio(&x, &y_plain, 2048); - assert!( - ratio_plain > 5.0 * ratio, - "uncompensated err/sig = {ratio_plain} vs compensated {ratio}" - ); - } - - /// Per-sequence output lengths of [`SsrChannelDecoder`]. - #[test] - fn decode_frame_output_lengths() { - let spec = vec![0.5f64; 1024]; - let mut dec = SsrChannelDecoder::new(); - for (seq, len) in [ - (WindowSequence::OnlyLong, 1024), - (WindowSequence::LongStart, 1472), - (WindowSequence::EightShort, 1024), - (WindowSequence::LongStop, 576), - ] { - let out = dec - .decode_frame(&spec, &ics(WindowShape::Sine, seq), None) - .unwrap(); - assert_eq!(out.len(), len, "{seq:?}"); - } - } -} diff --git a/crates/vendor/oxideav-aac/src/ssr_filterbank.rs b/crates/vendor/oxideav-aac/src/ssr_filterbank.rs deleted file mode 100644 index 4e184556..00000000 --- a/crates/vendor/oxideav-aac/src/ssr_filterbank.rs +++ /dev/null @@ -1,454 +0,0 @@ -//! SSR front-half filterbank — ISO/IEC 14496-3 §4.6.12.1 (matching -//! ISO/IEC 13818-7 §16.1): the spectrum → PQF-band de-interleave, the -//! even-band spectral reversal, and the per-band 256/32-line IMDCTs -//! with the quarter-scale §4.6.11.3.2 windows. -//! -//! When the gain control tool is active (the SSR object type, AOT 3), -//! the §4.6.11 filterbank configuration changes (§4.6.12.1): -//! -//! * the IMDCT is 256 lines instead of 1024 (one per PQF band) for the -//! long window sequences, and 32 lines instead of 128 (eight per -//! band) for `EIGHT_SHORT_SEQUENCE`; -//! * "the filter bank tool outputs a total of 2048 non-overlapped -//! values per frame" — four bands × 512 windowed samples, handed to -//! the §4.6.12.3.3 gain-control windowing/overlap stage as -//! `U_{W,B}(j)`; -//! * "the order of the MDCT coefficients in each even PQF band must be -//! reversed … exchanging the higher frequency MDCT coefficients with -//! the lower frequency MDCT coefficients". -//! -//! ## The spectrum → band arrangement -//! -//! The PQF splits the input into "four equal width frequency bands" -//! (Annex C.2.1.1), band `B` covering the `B`-th quarter of the -//! spectrum in ascending frequency (its modulator is centred on -//! `(2B+1)π/8`). The transmitted spectrum keeps the ordinary -//! ascending-frequency coefficient order (the §4.5.2.3 scalefactor-band -//! machinery runs on it unchanged), so band `B`'s 256 (long) / 32 -//! (short, per window) coefficient column is the contiguous quarter -//! `spec[256·B ..][..256]` / `spec[128·w + 32·B ..][..32]`. -//! -//! ## Which bands are "even" -//! -//! The §4.6.12.2 definitions count IPQF bands ordinally — `max_band` -//! is defined over "the 2nd / 3rd / 4th IPQF band" — so the "even PQF -//! band[s]" whose coefficients are reversed are the 2nd and 4th, i.e. -//! 0-based bands 1 and 3. This is also forced by the filterbank -//! mathematics: decimating band `B` by four spectrally inverts the -//! odd-indexed (0-based) bands, so exactly those bands need the -//! reversal for the assembled spectrum to be frequency-ascending. The -//! `tone_lands_at_its_spectral_bin` test pins this against the Annex -//! C.2.1.1 analysis PQF: a pure tone encoded through the PQF → MDCT → -//! reversal chain peaks at its global spectral bin only under this -//! convention (bands 1 and 3 mirror without it). -//! -//! ## Provenance -//! -//! Transform sizes, output layout and the reversal rule are the -//! §4.6.12.1 / §16.1 prose; the window geometry is §4.6.11.3.2 -//! evaluated at the `(512, 64)` family with the KBD windows pinned -//! against Tables 4.A.13 / 4.A.14; the validation PQF is the Annex -//! C.2.1.1 formula. All from the spec PDFs staged under -//! `docs/audio/aac/`. No external SSR implementation was consulted. - -use crate::filterbank::{imdct, long_sequence_window_n, short_window_n}; -use crate::ics_info::{IcsInfo, WindowSequence, WindowShape}; -use crate::ipqf::NUM_BANDS; -use crate::Error; - -type Result = core::result::Result; - -/// The SSR per-band long transform length (§4.6.12.1: 256 lines → -/// `N = 512`). -pub const SSR_LONG_TRANSFORM: usize = 512; -/// The SSR per-band short transform length (§4.6.12.1: 32 lines → -/// `N = 64`). -pub const SSR_SHORT_TRANSFORM: usize = 64; -/// Spectral lines per band for the long window sequences. -pub const BAND_LINES_LONG: usize = SSR_LONG_TRANSFORM / 2; // 256 -/// Spectral lines per band per short window. -pub const BAND_LINES_SHORT: usize = SSR_SHORT_TRANSFORM / 2; // 32 -/// Short windows in an `EIGHT_SHORT_SEQUENCE`. -const NUM_SHORT_WINDOWS: usize = 8; -/// Non-overlapped windowed samples each band contributes per frame -/// (§4.6.12.1: `4 × 512 = 2048` total). -pub const BAND_SAMPLES_PER_FRAME: usize = SSR_LONG_TRANSFORM; - -/// §4.6.12.1 — split the frame's 1024 decoded spectral coefficients -/// into the four PQF-band coefficient columns, applying the even-band -/// (0-based 1 and 3, see the module notes) spectral reversal. -/// -/// * Long sequences: `spec` is the 1024-line frequency-ascending -/// spectrum; band `B`'s column is `spec[256·B ..][..256]`, reversed -/// for bands 1 and 3. -/// * `EIGHT_SHORT_SEQUENCE`: `spec` is window-major (window `w` at -/// `spec[128·w ..][..128]`); band `B`'s column concatenates the -/// eight per-window quarters `spec[128·w + 32·B ..][..32]` (each -/// reversed for bands 1 and 3), so it is itself window-major. -/// -/// Errors with [`Error::FilterbankInvalid`] if `spec` is not 1024 -/// coefficients. -pub fn split_bands(spec: &[f64], seq: WindowSequence) -> Result<[Vec; NUM_BANDS]> { - if spec.len() != NUM_BANDS * BAND_LINES_LONG { - return Err(Error::FilterbankInvalid); - } - let mut bands: [Vec; NUM_BANDS] = - core::array::from_fn(|_| Vec::with_capacity(BAND_LINES_LONG)); - match seq { - WindowSequence::EightShort => { - for w in 0..NUM_SHORT_WINDOWS { - let win = - &spec[w * (NUM_BANDS * BAND_LINES_SHORT)..][..NUM_BANDS * BAND_LINES_SHORT]; - for (b, band) in bands.iter_mut().enumerate() { - let col = &win[b * BAND_LINES_SHORT..][..BAND_LINES_SHORT]; - if b % 2 == 1 { - band.extend(col.iter().rev()); - } else { - band.extend_from_slice(col); - } - } - } - } - _ => { - for (b, band) in bands.iter_mut().enumerate() { - let col = &spec[b * BAND_LINES_LONG..][..BAND_LINES_LONG]; - if b % 2 == 1 { - band.extend(col.iter().rev()); - } else { - band.extend_from_slice(col); - } - } - } - } - Ok(bands) -} - -/// The stateful SSR front-half synthesis for one channel: the -/// §4.6.12.1 band split + per-band IMDCT + quarter-scale §4.6.11.3.2 -/// windowing, producing the non-overlapped `U_{W,B}(j)` columns the -/// §4.6.12.3.3 gain-control stage consumes. -/// -/// Carries the previous block's `window_shape` across frames (the left -/// half of every window inherits it, §4.6.11.3.2 — the SSR family -/// keeps the standard inheritance rule). -#[derive(Debug, Clone, Default)] -pub struct SsrSynthesis { - /// `window_shape` of the previous block; `None` before the first - /// frame (the first block uses its own shape for both halves). - prev_shape: Option, -} - -impl SsrSynthesis { - /// A fresh front half with no previous-block shape. - #[must_use] - pub fn new() -> Self { - SsrSynthesis::default() - } - - /// Produce the four per-band non-overlapped windowed columns - /// `U_{W,B}` for one frame. - /// - /// `spec` is the frame's decoded 1024-line spectrum (window-major - /// for `EIGHT_SHORT_SEQUENCE`). Each returned column holds - /// [`BAND_SAMPLES_PER_FRAME`] (512) samples: a single windowed - /// 512-sample block for the long sequences, or eight windowed - /// 64-sample blocks concatenated window-major for - /// `EIGHT_SHORT_SEQUENCE` — exactly the `u` layout - /// [`crate::gain_control::GainBandState::window_overlap`] expects. - pub fn windowed_bands( - &mut self, - spec: &[f64], - ics_info: &IcsInfo, - ) -> Result<[Vec; NUM_BANDS]> { - let left_shape = self.prev_shape.unwrap_or(ics_info.window_shape); - let right_shape = ics_info.window_shape; - let seq = ics_info.window_sequence; - let cols = split_bands(spec, seq)?; - - let mut out: [Vec; NUM_BANDS] = core::array::from_fn(|_| Vec::new()); - match seq { - WindowSequence::EightShort => { - // Eight per-band 32-line IMDCTs, each windowed with the - // 64-sample short window (window 0's left half inherits - // the previous block's shape). No intra-sequence - // overlap-add here: §4.6.12.3.3 performs it after the - // gain is applied. - for (band, col) in out.iter_mut().zip(cols.iter()) { - let mut u = Vec::with_capacity(BAND_SAMPLES_PER_FRAME); - for w in 0..NUM_SHORT_WINDOWS { - let lines = &col[w * BAND_LINES_SHORT..][..BAND_LINES_SHORT]; - let x = imdct(lines, SSR_SHORT_TRANSFORM); - let win = short_window_n(SSR_SHORT_TRANSFORM, w, left_shape, right_shape); - u.extend(x.iter().zip(win.iter()).map(|(&xv, &wv)| xv * wv)); - } - band.extend_from_slice(&u); - } - } - _ => { - let win = long_sequence_window_n( - SSR_LONG_TRANSFORM, - SSR_SHORT_TRANSFORM, - seq, - left_shape, - right_shape, - )?; - for (band, col) in out.iter_mut().zip(cols.iter()) { - let x = imdct(col, SSR_LONG_TRANSFORM); - band.extend(x.iter().zip(win.iter()).map(|(&xv, &wv)| xv * wv)); - } - } - } - - self.prev_shape = Some(right_shape); - Ok(out) - } -} - -/// Test-side mirror of the encoder PQF (Annex C.2.1.1), shared by the -/// front-half tests here and the full round-trip tests in -/// [`crate::ssr`]. -#[cfg(test)] -pub(crate) mod pqf_test_support { - use super::NUM_BANDS; - use crate::ipqf::{prototype, PROTO_LEN}; - use core::f64::consts::PI; - - /// Annex C.2.1.1 — the encoder-side PQF analysis coefficients - /// `h_i(n) = (1/4)·cos((2i+1)(2n+5)π/16)·Q(n)`, `0 ≤ n ≤ 95`, - /// with `Q` the Table 4.110 prototype (test-side mirror of the - /// §4.6.12.3.4 IPQF). - pub(crate) fn analysis_coefs() -> [[f64; PROTO_LEN]; NUM_BANDS] { - let q = prototype(); - core::array::from_fn(|i| { - core::array::from_fn(|n| { - 0.25 * ((2.0 * i as f64 + 1.0) * (2.0 * n as f64 + 5.0) * PI / 16.0).cos() * q[n] - }) - }) - } - - /// Critically-sampled PQF analysis: band sample - /// `X_B(m) = Σ_n h_B(n)·x(4m + 3 − n)` — each band sample consumes - /// one block of four new input samples (the `+3` reads up to the - /// newest sample of block `m`; the resulting analysis+synthesis - /// cascade delay is [`PQF_CASCADE_DELAY`] full-rate samples). - pub(crate) fn pqf_analysis(x: &[f64]) -> [Vec; NUM_BANDS] { - let h = analysis_coefs(); - let m_len = x.len() / NUM_BANDS; - core::array::from_fn(|b| { - (0..m_len) - .map(|m| { - let mut acc = 0.0f64; - for (n, &hn) in h[b].iter().enumerate() { - let idx = 4 * m as isize + 3 - n as isize; - if idx >= 0 { - if let Some(&xv) = x.get(idx as usize) { - acc += hn * xv; - } - } - } - acc - }) - .collect() - }) - } - - /// Full-rate delay of the Annex C.2.1.1 analysis → §4.6.12.3.4 - /// synthesis cascade with the `+3` analysis alignment (measured by - /// the near-perfect-reconstruction test). - pub(crate) const PQF_CASCADE_DELAY: usize = 92; -} - -#[cfg(test)] -mod tests { - use super::pqf_test_support::{pqf_analysis, PQF_CASCADE_DELAY}; - use super::*; - use crate::filterbank::forward_mdct; - use crate::ipqf::Ipqf; - use core::f64::consts::PI; - - /// The analysis PQF and the IPQF are a near-perfect-reconstruction - /// pair: white input round-trips within the prototype's stopband - /// leakage (measured ≈ 2.9e-4 err/sig) at a flat 92-sample delay. - #[test] - fn pqf_ipqf_cascade_is_near_perfect_reconstruction() { - // Deterministic pseudo-random input. - let mut state = 0x1234_5678u32; - let mut rnd = || { - state = state.wrapping_mul(1664525).wrapping_add(1013904223); - (state >> 8) as f64 / (1u32 << 24) as f64 - 0.5 - }; - let x: Vec = (0..4000).map(|_| rnd()).collect(); - let bands = pqf_analysis(&x); - let refs: [&[f64]; NUM_BANDS] = core::array::from_fn(|b| bands[b].as_slice()); - let mut ipqf = Ipqf::new(); - let y = ipqf.synthesize(&refs, bands[0].len()); - - let (mut err, mut sig) = (0.0f64, 0.0f64); - for n in 500..2500 { - let d = y[n + PQF_CASCADE_DELAY] - x[n]; - err += d * d; - sig += x[n] * x[n]; - } - let ratio = (err / sig).sqrt(); - assert!(ratio < 1e-3, "cascade err/sig = {ratio}"); - // Discriminator: a wrong delay is nowhere near. - let mut err_bad = 0.0f64; - for n in 500..2500 { - let d = y[n + PQF_CASCADE_DELAY + 4] - x[n]; - err_bad += d * d; - } - assert!((err_bad / sig).sqrt() > 0.1); - } - - /// §4.6.12.1 — a pure tone at global spectral bin `k`, encoded - /// through the Annex C.2.1.1 PQF → per-band windowed MDCT → - /// even-band reversal → contiguous quarters, peaks at bin `k`. - /// Without the reversal, the band-1 / band-3 tones mirror inside - /// their quarter — this pins both the split arrangement and the - /// reversal convention (0-based bands 1 and 3). - #[test] - fn tone_lands_at_its_spectral_bin() { - let win: Vec = (0..SSR_LONG_TRANSFORM) - .map(|n| (PI / SSR_LONG_TRANSFORM as f64 * (n as f64 + 0.5)).sin()) - .collect(); - // One tone per PQF band. - for &k_target in &[100usize, 300, 550, 800] { - let f = (k_target as f64 + 0.5) * PI / 1024.0; - let x: Vec = (0..8192).map(|n| (f * n as f64).sin()).collect(); - let bands = pqf_analysis(&x); - - // Steady ONLY_LONG frame over band samples [768, 1280). - let mut spec = vec![0.0f64; 1024]; - let mut spec_unreversed = vec![0.0f64; 1024]; - for b in 0..NUM_BANDS { - let z: Vec = (0..SSR_LONG_TRANSFORM) - .map(|n| bands[b][768 + n] * win[n]) - .collect(); - let mut coeffs = forward_mdct(&z, SSR_LONG_TRANSFORM); - spec_unreversed[256 * b..256 * b + 256].copy_from_slice(&coeffs); - if b % 2 == 1 { - coeffs.reverse(); - } - spec[256 * b..256 * b + 256].copy_from_slice(&coeffs); - } - let peak = |s: &[f64]| { - (0..s.len()) - .max_by(|&a, &b| s[a].abs().partial_cmp(&s[b].abs()).unwrap()) - .unwrap() - }; - let got = peak(&spec); - assert!( - got.abs_diff(k_target) <= 2, - "tone k={k_target} peaked at {got}" - ); - let got_unrev = peak(&spec_unreversed); - if k_target / 256 % 2 == 1 { - // Bands 1 and 3 mirror without the reversal. - let band = k_target / 256; - let mirrored = 256 * band + (255 - (k_target - 256 * band)); - assert!( - got_unrev.abs_diff(mirrored) <= 2, - "unreversed tone k={k_target} peaked at {got_unrev}, expected ≈{mirrored}" - ); - } - } - } - - /// `split_bands` long layout: contiguous ascending quarters, bands - /// 1 and 3 reversed. - #[test] - fn split_bands_long_layout() { - let spec: Vec = (0..1024).map(|i| i as f64).collect(); - let bands = split_bands(&spec, WindowSequence::OnlyLong).unwrap(); - for (b, band) in bands.iter().enumerate() { - assert_eq!(band.len(), 256); - if b % 2 == 0 { - assert_eq!(band[0], (256 * b) as f64); - assert_eq!(band[255], (256 * b + 255) as f64); - } else { - assert_eq!(band[0], (256 * b + 255) as f64); - assert_eq!(band[255], (256 * b) as f64); - } - } - } - - /// `split_bands` short layout: per short window, per-band 32-line - /// quarters (window-major columns), bands 1 and 3 reversed within - /// each window. - #[test] - fn split_bands_short_layout() { - let spec: Vec = (0..1024).map(|i| i as f64).collect(); - let bands = split_bands(&spec, WindowSequence::EightShort).unwrap(); - for (b, band) in bands.iter().enumerate() { - assert_eq!(band.len(), 256); - for w in 0..8 { - let base = (128 * w + 32 * b) as f64; - if b % 2 == 0 { - assert_eq!(band[32 * w], base); - assert_eq!(band[32 * w + 31], base + 31.0); - } else { - assert_eq!(band[32 * w], base + 31.0); - assert_eq!(band[32 * w + 31], base); - } - } - } - } - - /// Bad spectrum length is rejected. - #[test] - fn split_bands_rejects_bad_length() { - assert!(split_bands(&[0.0; 512], WindowSequence::OnlyLong).is_err()); - } - - /// A minimal [`IcsInfo`] for the front-half tests. - fn test_ics_info(shape: WindowShape, seq: WindowSequence) -> IcsInfo { - let short = seq == WindowSequence::EightShort; - IcsInfo { - family: crate::swb_offset::FrameFamily::Lc1024, - ics_reserved_bit: false, - window_sequence: seq, - window_shape: shape, - max_sfb: 0, - scale_factor_grouping: if short { Some(0) } else { None }, - predictor_data_present: false, - predictor_data: None, - ltp_data_present: false, - ltp_data: None, - ltp_data_present_pair: None, - ltp_data_pair: None, - num_windows: if short { 8 } else { 1 }, - num_window_groups: if short { 8 } else { 1 }, - window_group_length: if short { vec![1; 8] } else { vec![1] }, - num_swb: 0, - } - } - - /// `windowed_bands` output geometry: four 512-sample columns for - /// every window sequence, and the long-start column goes silent - /// after the §4.6.11.3.2 zero region (scaled: `[400, 512)`). - #[test] - fn windowed_bands_geometry() { - let spec = vec![1.0f64; 1024]; - for seq in [ - WindowSequence::OnlyLong, - WindowSequence::LongStart, - WindowSequence::EightShort, - WindowSequence::LongStop, - ] { - let mut synth = SsrSynthesis::new(); - let info = test_ics_info(WindowShape::Sine, seq); - let u = synth.windowed_bands(&spec, &info).unwrap(); - for band in &u { - assert_eq!(band.len(), BAND_SAMPLES_PER_FRAME); - assert!(band.iter().all(|v| v.is_finite())); - } - if seq == WindowSequence::LongStart { - for band in &u { - for &v in &band[400..] { - assert_eq!(v, 0.0, "LONG_START zero region"); - } - } - } - } - } -} diff --git a/crates/vendor/oxideav-aac/src/swb_offset.rs b/crates/vendor/oxideav-aac/src/swb_offset.rs deleted file mode 100644 index 7eb868a0..00000000 --- a/crates/vendor/oxideav-aac/src/swb_offset.rs +++ /dev/null @@ -1,1418 +0,0 @@ -//! Scalefactor-band offset tables — ISO/IEC 14496-3 §4.5.4.1 / Tables -//! 4.129–4.141. -//! -//! Each `swb_offset_long_window[fs_index]` / `swb_offset_short_window[fs_index]` -//! table lists the *index of the lowest spectral coefficient* of each -//! scalefactor band, plus a trailing sentinel at the spectrum length -//! (1024 for long, 128 for short). The per-band width is therefore -//! `offset[i + 1] - offset[i]`, and the total entry count is -//! `num_swb + 1`. -//! -//! ## What this module covers -//! -//! * [`SWB_OFFSET_LONG_WINDOW`] — 13-entry lookup of long-window -//! offset slices, keyed by `samplingFrequencyIndex` (Table 1.18). -//! Slots `0..=11` cover the 12 sampling rates that have defined -//! SWB tables; slot `12` (7350 Hz) is an empty slice (no SWB -//! table is defined). Sourced from Tables 4.129 (44.1 / 48 kHz, -//! fs 3/4), 4.131 (32 kHz, fs 5), 4.132 (8 kHz, fs 11), 4.134 -//! (11.025 / 12 / 16 kHz, fs 8/9/10), 4.136 (22.05 / 24 kHz, fs 6/7), -//! 4.138 (64 kHz, fs 2), 4.140 (88.2 / 96 kHz, fs 0/1). -//! * [`SWB_OFFSET_SHORT_WINDOW`] — 13-entry lookup of 128-line -//! short-window offset slices (same fs-index layout as the long -//! table). Sourced from Tables 4.130 (32 / 44.1 / 48 kHz, -//! fs 3/4/5), 4.133 (8 kHz, fs 11), 4.135 (11.025 / 12 / 16 kHz, -//! fs 8/9/10), 4.137 (22.05 / 24 kHz, fs 6/7), 4.139 (64 kHz, -//! fs 2), 4.141 (88.2 / 96 kHz, fs 0/1). -//! * [`long_window_offsets`] / [`short_window_offsets`] — safe -//! bounds-checked accessors. -//! * [`apply_pulse_data`] — the §4.6.13 pulse-escape reconstruction -//! loop. Given a quantised long-window spectrum `x_quant` and a -//! parsed [`crate::pulse_data::PulseData`] block, applies the -//! per-pulse offset / amplitude fix-up in place. -//! -//! * [`FrameFamily`] + [`long_window_offsets_family`] / -//! [`short_window_offsets_family`] — the §4.5.1.1 frame-length -//! families: the 960/120-line variant (`frameLengthFlag == 1`, the -//! bracketed "values for 1920 / 240" columns of Tables 4.129–4.141) -//! and the ER AAC LD 512/480-line variants (§4.6.17.2.1, Tables -//! 4.142–4.147 with the §4.5.1.1 nearest-defined-table rule for -//! rates those tables omit). -//! -//! ## What this module does *not* cover -//! -//! * `sampling_frequency_index == 12` (7350 Hz) has no -//! scalefactor-band table in the spec; accessors return -//! [`Error::IcsInfoUnsupportedSampleRateIndex`] for that index. -//! * The 24-bit explicit-rate escape (`samplingFrequencyIndex -//! == 0xf`) does not select an SWB table directly — the caller must -//! resolve the explicit rate to the nearest standard index before -//! invoking these accessors. - -use crate::pulse_data::PulseData; -use crate::{Error, Result}; - -/// Total number of spectral coefficients in a long-window frame -/// (1024). The sentinel of every long-window table equals this value. -pub const LONG_WINDOW_LEN: u16 = 1024; - -/// Total number of spectral coefficients in a short-window frame -/// (128). The sentinel of every short-window table equals this value. -pub const SHORT_WINDOW_LEN: u16 = 128; - -/// `swb_offset_long_window[3]` / `swb_offset_long_window[4]` — Table -/// 4.129 (44.1 and 48 kHz, 49 SWB). 50 entries (49 bands + sentinel). -const SWB_OFFSET_LONG_44100_48000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, - 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, - 736, 768, 800, 832, 864, 896, 928, 1024, -]; - -/// `swb_offset_long_window[5]` — Table 4.131 (32 kHz, 51 SWB). 52 -/// entries. -const SWB_OFFSET_LONG_32000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, - 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, - 736, 768, 800, 832, 864, 896, 928, 960, 992, 1024, -]; - -/// `swb_offset_long_window[11]` — Table 4.132 (8 kHz, 40 SWB). 41 -/// entries. -const SWB_OFFSET_LONG_8000: &[u16] = &[ - 0, 12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 172, 188, 204, 220, 236, 252, 268, - 288, 308, 328, 348, 372, 396, 420, 448, 476, 508, 544, 580, 620, 664, 712, 764, 820, 880, 944, - 1024, -]; - -/// `swb_offset_long_window[8]` / `[9]` / `[10]` — Table 4.134 -/// (11.025, 12 and 16 kHz, 43 SWB). 44 entries. -const SWB_OFFSET_LONG_11025_12000_16000: &[u16] = &[ - 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 100, 112, 124, 136, 148, 160, 172, 184, 196, 212, - 228, 244, 260, 280, 300, 320, 344, 368, 396, 424, 456, 492, 532, 572, 616, 664, 716, 772, 832, - 896, 960, 1024, -]; - -/// `swb_offset_long_window[6]` / `[7]` — Table 4.136 (22.05 and 24 kHz, -/// 47 SWB). 48 entries. -const SWB_OFFSET_LONG_22050_24000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, 136, - 148, 160, 172, 188, 204, 220, 240, 260, 284, 308, 336, 364, 396, 432, 468, 508, 552, 600, 652, - 704, 768, 832, 896, 960, 1024, -]; - -/// `swb_offset_long_window[2]` — Table 4.138 (64 kHz, 47 SWB). 48 -/// entries. -const SWB_OFFSET_LONG_64000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 100, 112, 124, 140, - 156, 172, 192, 216, 240, 268, 304, 344, 384, 424, 464, 504, 544, 584, 624, 664, 704, 744, 784, - 824, 864, 904, 944, 984, 1024, -]; - -/// `swb_offset_long_window[0]` / `[1]` — Table 4.140 (88.2 and 96 kHz, -/// 41 SWB). 42 entries. -const SWB_OFFSET_LONG_88200_96000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, - 144, 156, 172, 188, 212, 240, 276, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, -]; - -/// `swb_offset_short_window[3]` / `[4]` / `[5]` — Table 4.130 -/// (32, 44.1, 48 kHz, 14 SWB). 15 entries. -const SWB_OFFSET_SHORT_32000_44100_48000: &[u16] = - &[0, 4, 8, 12, 16, 20, 28, 36, 44, 56, 68, 80, 96, 112, 128]; - -/// `swb_offset_short_window[11]` — Table 4.133 (8 kHz, 15 SWB). 16 -/// entries. -const SWB_OFFSET_SHORT_8000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 60, 72, 88, 108, 128, -]; - -/// `swb_offset_short_window[8]` / `[9]` / `[10]` — Table 4.135 -/// (11.025, 12, 16 kHz, 15 SWB). 16 entries. -const SWB_OFFSET_SHORT_11025_12000_16000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 60, 72, 88, 108, 128, -]; - -/// `swb_offset_short_window[6]` / `[7]` — Table 4.137 (22.05, 24 kHz, -/// 15 SWB). 16 entries. -const SWB_OFFSET_SHORT_22050_24000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 64, 76, 92, 108, 128, -]; - -/// `swb_offset_short_window[2]` — Table 4.139 (64 kHz, 12 SWB). 13 -/// entries. -const SWB_OFFSET_SHORT_64000: &[u16] = &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 128]; - -/// `swb_offset_short_window[0]` / `[1]` — Table 4.141 (88.2, 96 kHz, -/// 12 SWB). 13 entries. -const SWB_OFFSET_SHORT_88200_96000: &[u16] = &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 128]; - -/// `swb_offset_long_window` keyed by `samplingFrequencyIndex` -/// (ISO/IEC 14496-3 Table 1.18). Slot `12` (7350 Hz) carries an empty -/// slice — no SWB table is defined for that rate. -/// -/// Each slot is `num_swb + 1` entries long (the trailing entry is the -/// spectrum-length sentinel `1024`). -pub const SWB_OFFSET_LONG_WINDOW: [&[u16]; 13] = [ - SWB_OFFSET_LONG_88200_96000, // 0 = 96 kHz - SWB_OFFSET_LONG_88200_96000, // 1 = 88.2 kHz - SWB_OFFSET_LONG_64000, // 2 = 64 kHz - SWB_OFFSET_LONG_44100_48000, // 3 = 48 kHz - SWB_OFFSET_LONG_44100_48000, // 4 = 44.1 kHz - SWB_OFFSET_LONG_32000, // 5 = 32 kHz - SWB_OFFSET_LONG_22050_24000, // 6 = 24 kHz - SWB_OFFSET_LONG_22050_24000, // 7 = 22.05 kHz - SWB_OFFSET_LONG_11025_12000_16000, // 8 = 16 kHz - SWB_OFFSET_LONG_11025_12000_16000, // 9 = 12 kHz - SWB_OFFSET_LONG_11025_12000_16000, // 10 = 11.025 kHz - SWB_OFFSET_LONG_8000, // 11 = 8 kHz - &[], // 12 = 7350 Hz (no SWB table) -]; - -/// `swb_offset_short_window` keyed by `samplingFrequencyIndex` -/// (ISO/IEC 14496-3 Table 1.18). Slot `12` (7350 Hz) carries an -/// empty slice. -/// -/// Each slot is `num_swb + 1` entries long (the trailing entry is the -/// short-spectrum-length sentinel `128`). -pub const SWB_OFFSET_SHORT_WINDOW: [&[u16]; 13] = [ - SWB_OFFSET_SHORT_88200_96000, // 0 = 96 kHz - SWB_OFFSET_SHORT_88200_96000, // 1 = 88.2 kHz - SWB_OFFSET_SHORT_64000, // 2 = 64 kHz - SWB_OFFSET_SHORT_32000_44100_48000, // 3 = 48 kHz - SWB_OFFSET_SHORT_32000_44100_48000, // 4 = 44.1 kHz - SWB_OFFSET_SHORT_32000_44100_48000, // 5 = 32 kHz - SWB_OFFSET_SHORT_22050_24000, // 6 = 24 kHz - SWB_OFFSET_SHORT_22050_24000, // 7 = 22.05 kHz - SWB_OFFSET_SHORT_11025_12000_16000, // 8 = 16 kHz - SWB_OFFSET_SHORT_11025_12000_16000, // 9 = 12 kHz - SWB_OFFSET_SHORT_11025_12000_16000, // 10 = 11.025 kHz - SWB_OFFSET_SHORT_8000, // 11 = 8 kHz - &[], // 12 = 7350 Hz (no SWB table) -]; - -/// Look up `swb_offset_long_window[fs_index]`. -/// -/// Returns the slice of `num_swb + 1` per-band lowest-coefficient -/// indices (with the trailing `1024` sentinel) for the requested -/// `samplingFrequencyIndex`. -/// -/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] if `fs_index` -/// is outside `0..=11`. Index 12 (7350 Hz) has no defined long-window -/// SWB table. -pub fn long_window_offsets(fs_index: u8) -> Result<&'static [u16]> { - let idx = fs_index as usize; - if idx >= SWB_OFFSET_LONG_WINDOW.len() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - let slice = SWB_OFFSET_LONG_WINDOW[idx]; - if slice.is_empty() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - Ok(slice) -} - -/// Look up `swb_offset_short_window[fs_index]`. -/// -/// Returns the slice of `num_swb + 1` per-band lowest-coefficient -/// indices (with the trailing `128` sentinel) for the requested -/// `samplingFrequencyIndex`. -/// -/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] if `fs_index` -/// is outside `0..=11`. Index 12 (7350 Hz) has no defined short-window -/// SWB table. -pub fn short_window_offsets(fs_index: u8) -> Result<&'static [u16]> { - let idx = fs_index as usize; - if idx >= SWB_OFFSET_SHORT_WINDOW.len() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - let slice = SWB_OFFSET_SHORT_WINDOW[idx]; - if slice.is_empty() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - Ok(slice) -} - -// --------------------------------------------------------------------------- -// Frame-length families — §4.5.1.1 `frameLengthFlag` / §4.6.17.2.1. -// --------------------------------------------------------------------------- - -/// The four spectral-line frame families a General-Audio payload can -/// select — ISO/IEC 14496-3 §4.5.1.1 (`frameLengthFlag`) and -/// §4.6.17.2.1 (the ER AAC LD frame sizes). -/// -/// * For every GA AOT except AAC SSR and ER AAC LD, -/// `frameLengthFlag == 0` selects the 1024/128-line IMDCT family -/// and `frameLengthFlag == 1` the 960/120-line family. -/// * For ER AAC LD (AOT 23), `frameLengthFlag == 0` selects a single -/// 512-line IMDCT and `frameLengthFlag == 1` a single 480-line -/// IMDCT; there is no block switching (§4.6.17.2.2), hence no -/// short-window geometry at all. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum FrameFamily { - /// 1024 spectral lines per long frame, 128 per short window - /// (`frameLengthFlag == 0`, all GA AOTs except SSR / LD). - #[default] - Lc1024, - /// 960 spectral lines per long frame, 120 per short window - /// (`frameLengthFlag == 1`). - Lc960, - /// ER AAC LD, 512 spectral lines (`frameLengthFlag == 0`); - /// long-only. - Ld512, - /// ER AAC LD, 480 spectral lines (`frameLengthFlag == 1`); - /// long-only. - Ld480, -} - -impl FrameFamily { - /// Resolve the family from the stream's `audioObjectType` and - /// `frameLengthFlag` per §4.5.1.1. - pub fn from_aot_and_flag(aot: u8, frame_length_flag: bool) -> Self { - match (aot == 23, frame_length_flag) { - (false, false) => FrameFamily::Lc1024, - (false, true) => FrameFamily::Lc960, - (true, false) => FrameFamily::Ld512, - (true, true) => FrameFamily::Ld480, - } - } - - /// Spectral lines per long window == PCM samples per frame per - /// channel (1024 / 960 / 512 / 480). - pub fn frame_len(self) -> usize { - match self { - FrameFamily::Lc1024 => 1024, - FrameFamily::Lc960 => 960, - FrameFamily::Ld512 => 512, - FrameFamily::Ld480 => 480, - } - } - - /// `N_l` — the long IMDCT transform length (`2 × frame_len`): - /// 2048 / 1920 / 1024 / 960. - pub fn long_transform_len(self) -> usize { - 2 * self.frame_len() - } - - /// Spectral lines per short window (128 / 120), or [`None`] for - /// the long-only LD families (§4.6.17.2.2 — no block switching). - pub fn short_window_len(self) -> Option { - match self { - FrameFamily::Lc1024 => Some(128), - FrameFamily::Lc960 => Some(120), - FrameFamily::Ld512 | FrameFamily::Ld480 => None, - } - } - - /// `N_s` — the short IMDCT transform length (256 / 240), or - /// [`None`] for the LD families. - pub fn short_transform_len(self) -> Option { - self.short_window_len().map(|w| 2 * w) - } - - /// `true` for the ER AAC LD families (§4.6.17): long-only frames, - /// low-overlap window in place of KBD, LD LTP lag semantics. - pub fn is_ld(self) -> bool { - matches!(self, FrameFamily::Ld512 | FrameFamily::Ld480) - } -} - -// --------------------------------------------------------------------------- -// 960/120-line tables — the bracketed "values for 1920 / 240" columns -// of Tables 4.129–4.141. -// --------------------------------------------------------------------------- -// -// Each long table prints the 1920-transform variant as bracketed -// values on the shared rows: the band starts are identical to the -// 2048-transform column and only the tail changes — the sentinel -// becomes 960 and any offsets at or above 960 are dropped (`(-)`). -// Each short table only re-brackets the sentinel (`128 (120)`). - -/// `swb_offset_long_window[3]` / `[4]` for the 960-line family — -/// Table 4.129 bracketed column (44.1 / 48 kHz, 49 SWB). 50 entries. -const SWB_OFFSET_LONG_960_44100_48000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, - 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, - 736, 768, 800, 832, 864, 896, 928, 960, -]; - -/// `swb_offset_long_window[5]` for the 960-line family — Table 4.131 -/// bracketed column (32 kHz; the 992 / 1024 rows are `(-)`, so 49 -/// SWB). 50 entries. -const SWB_OFFSET_LONG_960_32000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 88, 96, 108, 120, 132, 144, 160, - 176, 196, 216, 240, 264, 292, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, - 736, 768, 800, 832, 864, 896, 928, 960, -]; - -/// `swb_offset_long_window[11]` for the 960-line family — Table 4.132 -/// bracketed column (8 kHz, 40 SWB). 41 entries. -const SWB_OFFSET_LONG_960_8000: &[u16] = &[ - 0, 12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 172, 188, 204, 220, 236, 252, 268, - 288, 308, 328, 348, 372, 396, 420, 448, 476, 508, 544, 580, 620, 664, 712, 764, 820, 880, 944, - 960, -]; - -/// `swb_offset_long_window[8]` / `[9]` / `[10]` for the 960-line -/// family — Table 4.134 bracketed column (11.025 / 12 / 16 kHz; the -/// 1024 row is `(-)`, so 42 SWB). 43 entries. -const SWB_OFFSET_LONG_960_11025_12000_16000: &[u16] = &[ - 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 100, 112, 124, 136, 148, 160, 172, 184, 196, 212, - 228, 244, 260, 280, 300, 320, 344, 368, 396, 424, 456, 492, 532, 572, 616, 664, 716, 772, 832, - 896, 960, -]; - -/// `swb_offset_long_window[6]` / `[7]` for the 960-line family — -/// Table 4.136 bracketed column (22.05 / 24 kHz; the 1024 row is -/// `(-)`, so 46 SWB). 47 entries. -const SWB_OFFSET_LONG_960_22050_24000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, 136, - 148, 160, 172, 188, 204, 220, 240, 260, 284, 308, 336, 364, 396, 432, 468, 508, 552, 600, 652, - 704, 768, 832, 896, 960, -]; - -/// `swb_offset_long_window[2]` for the 960-line family — Table 4.138 -/// bracketed column (64 kHz, `num_swb 47 (46)`: the 984 row brackets -/// to 960 and the 1024 row is `(-)`). 47 entries. -const SWB_OFFSET_LONG_960_64000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 100, 112, 124, 140, - 156, 172, 192, 216, 240, 268, 304, 344, 384, 424, 464, 504, 544, 584, 624, 664, 704, 744, 784, - 824, 864, 904, 944, 960, -]; - -/// `swb_offset_long_window[0]` / `[1]` for the 960-line family — -/// Table 4.140 bracketed column (88.2 / 96 kHz; the 1024 row is -/// `(-)`, so 40 SWB). 41 entries. -const SWB_OFFSET_LONG_960_88200_96000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, - 144, 156, 172, 188, 212, 240, 276, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, -]; - -/// Table 4.130 bracketed column — 120-line short window at 32 / 44.1 / -/// 48 kHz (14 SWB). 15 entries. -const SWB_OFFSET_SHORT_120_32000_44100_48000: &[u16] = - &[0, 4, 8, 12, 16, 20, 28, 36, 44, 56, 68, 80, 96, 112, 120]; - -/// Table 4.133 bracketed column — 120-line short window at 8 kHz -/// (15 SWB). 16 entries. -const SWB_OFFSET_SHORT_120_8000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 60, 72, 88, 108, 120, -]; - -/// Table 4.135 bracketed column — 120-line short window at 11.025 / -/// 12 / 16 kHz (15 SWB). 16 entries. -const SWB_OFFSET_SHORT_120_11025_12000_16000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 60, 72, 88, 108, 120, -]; - -/// Table 4.137 bracketed column — 120-line short window at 22.05 / -/// 24 kHz (15 SWB). 16 entries. -const SWB_OFFSET_SHORT_120_22050_24000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 36, 44, 52, 64, 76, 92, 108, 120, -]; - -/// Table 4.139 bracketed column — 120-line short window at 64 kHz -/// (12 SWB). 13 entries. -const SWB_OFFSET_SHORT_120_64000: &[u16] = &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 120]; - -/// Table 4.141 bracketed column — 120-line short window at 88.2 / -/// 96 kHz (12 SWB). 13 entries. -const SWB_OFFSET_SHORT_120_88200_96000: &[u16] = - &[0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 92, 120]; - -/// 960-line long-window offset tables keyed by -/// `samplingFrequencyIndex` (the bracketed Tables 4.129–4.140 -/// columns). Same slot layout as [`SWB_OFFSET_LONG_WINDOW`]. -pub const SWB_OFFSET_LONG_WINDOW_960: [&[u16]; 13] = [ - SWB_OFFSET_LONG_960_88200_96000, // 0 = 96 kHz - SWB_OFFSET_LONG_960_88200_96000, // 1 = 88.2 kHz - SWB_OFFSET_LONG_960_64000, // 2 = 64 kHz - SWB_OFFSET_LONG_960_44100_48000, // 3 = 48 kHz - SWB_OFFSET_LONG_960_44100_48000, // 4 = 44.1 kHz - SWB_OFFSET_LONG_960_32000, // 5 = 32 kHz - SWB_OFFSET_LONG_960_22050_24000, // 6 = 24 kHz - SWB_OFFSET_LONG_960_22050_24000, // 7 = 22.05 kHz - SWB_OFFSET_LONG_960_11025_12000_16000, // 8 = 16 kHz - SWB_OFFSET_LONG_960_11025_12000_16000, // 9 = 12 kHz - SWB_OFFSET_LONG_960_11025_12000_16000, // 10 = 11.025 kHz - SWB_OFFSET_LONG_960_8000, // 11 = 8 kHz - &[], // 12 = 7350 Hz (no SWB table) -]; - -/// 120-line short-window offset tables keyed by -/// `samplingFrequencyIndex` (the bracketed Tables 4.130–4.141 -/// columns). Same slot layout as [`SWB_OFFSET_SHORT_WINDOW`]. -pub const SWB_OFFSET_SHORT_WINDOW_120: [&[u16]; 13] = [ - SWB_OFFSET_SHORT_120_88200_96000, // 0 = 96 kHz - SWB_OFFSET_SHORT_120_88200_96000, // 1 = 88.2 kHz - SWB_OFFSET_SHORT_120_64000, // 2 = 64 kHz - SWB_OFFSET_SHORT_120_32000_44100_48000, // 3 = 48 kHz - SWB_OFFSET_SHORT_120_32000_44100_48000, // 4 = 44.1 kHz - SWB_OFFSET_SHORT_120_32000_44100_48000, // 5 = 32 kHz - SWB_OFFSET_SHORT_120_22050_24000, // 6 = 24 kHz - SWB_OFFSET_SHORT_120_22050_24000, // 7 = 22.05 kHz - SWB_OFFSET_SHORT_120_11025_12000_16000, // 8 = 16 kHz - SWB_OFFSET_SHORT_120_11025_12000_16000, // 9 = 12 kHz - SWB_OFFSET_SHORT_120_11025_12000_16000, // 10 = 11.025 kHz - SWB_OFFSET_SHORT_120_8000, // 11 = 8 kHz - &[], // 12 = 7350 Hz (no SWB table) -]; - -// --------------------------------------------------------------------------- -// ER AAC LD tables — §4.5.4 Tables 4.142–4.147 (window lengths 960 -// and 1024, i.e. LD frame sizes 480 and 512). -// --------------------------------------------------------------------------- - -/// Table 4.143 — LD 512-line frame at 44.1 / 48 kHz (36 SWB). 37 -/// entries. -const SWB_OFFSET_LD_512_44100_48000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 68, 76, 84, 92, 100, 112, 124, - 136, 148, 164, 184, 208, 236, 268, 300, 332, 364, 396, 428, 460, 512, -]; - -/// Table 4.145 — LD 512-line frame at 32 kHz (37 SWB). 38 entries. -const SWB_OFFSET_LD_512_32000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, - 144, 160, 176, 192, 212, 236, 260, 288, 320, 352, 384, 416, 448, 480, 512, -]; - -/// Table 4.147 — LD 512-line frame at 22.05 / 24 kHz (31 SWB). 32 -/// entries. -const SWB_OFFSET_LD_512_22050_24000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 80, 92, 104, 120, 140, 164, 192, 224, - 256, 288, 320, 352, 384, 416, 448, 480, 512, -]; - -/// Table 4.142 — LD 480-line frame at 44.1 / 48 kHz (35 SWB). 36 -/// entries. -const SWB_OFFSET_LD_480_44100_48000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 108, 120, 132, - 144, 156, 172, 188, 212, 240, 272, 304, 336, 368, 400, 432, 480, -]; - -/// Table 4.144 — LD 480-line frame at 32 kHz (37 SWB). 38 entries. -const SWB_OFFSET_LD_480_32000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 124, - 136, 148, 164, 180, 200, 224, 256, 288, 320, 352, 384, 416, 448, 480, -]; - -/// Table 4.146 — LD 480-line frame at 22.05 / 24 kHz (30 SWB). 31 -/// entries. -const SWB_OFFSET_LD_480_22050_24000: &[u16] = &[ - 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 52, 60, 68, 80, 92, 104, 120, 140, 164, 192, 224, - 256, 288, 320, 352, 384, 416, 448, 480, -]; - -/// Map a `samplingFrequencyIndex` onto the LD table column that -/// covers it. -/// -/// Tables 4.142–4.147 only define the 48 / 44.1 / 32 / 24 / 22.05 kHz -/// rates. Per §4.5.1.1 ("if in a certain sampling frequency dependent -/// table a sampling frequency stated in the right column of Table -/// 4.82 is not defined, the nearest defined table shall be used"), -/// every higher rate resolves to the 48 kHz table (48 000 is the -/// nearest defined rate for 96 / 88.2 / 64 kHz) and every lower rate -/// to the 22.05 kHz table (22 050 is the nearest defined rate for -/// 16 / 12 / 11.025 / 8 kHz). -fn ld_table_slot(fs_index: u8) -> Result { - match fs_index { - 0..=4 => Ok(0), // 96 / 88.2 / 64 / 48 / 44.1 kHz → 44.1/48 table - 5 => Ok(1), // 32 kHz - 6..=11 => Ok(2), // 24 / 22.05 kHz + nearest-rule lower rates - other => Err(Error::IcsInfoUnsupportedSampleRateIndex(other)), - } -} - -/// LD 512-line tables in [`ld_table_slot`] order. -const SWB_OFFSET_LD_512: [&[u16]; 3] = [ - SWB_OFFSET_LD_512_44100_48000, - SWB_OFFSET_LD_512_32000, - SWB_OFFSET_LD_512_22050_24000, -]; - -/// LD 480-line tables in [`ld_table_slot`] order. -const SWB_OFFSET_LD_480: [&[u16]; 3] = [ - SWB_OFFSET_LD_480_44100_48000, - SWB_OFFSET_LD_480_32000, - SWB_OFFSET_LD_480_22050_24000, -]; - -/// Family-aware `swb_offset_long_window[fs_index]` lookup. -/// -/// Dispatches on the [`FrameFamily`]: `Lc1024` reads the Tables -/// 4.129–4.140 primary columns (== [`long_window_offsets`]), `Lc960` -/// their bracketed 1920-transform columns, and the LD families the -/// dedicated Tables 4.142–4.147 (with the §4.5.1.1 nearest-defined- -/// table rule for rates those tables omit). -pub fn long_window_offsets_family(family: FrameFamily, fs_index: u8) -> Result<&'static [u16]> { - match family { - FrameFamily::Lc1024 => long_window_offsets(fs_index), - FrameFamily::Lc960 => { - let idx = fs_index as usize; - let slice = SWB_OFFSET_LONG_WINDOW_960 - .get(idx) - .copied() - .unwrap_or(&[][..]); - if slice.is_empty() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - Ok(slice) - } - FrameFamily::Ld512 => Ok(SWB_OFFSET_LD_512[ld_table_slot(fs_index)?]), - FrameFamily::Ld480 => Ok(SWB_OFFSET_LD_480[ld_table_slot(fs_index)?]), - } -} - -/// Family-aware `swb_offset_short_window[fs_index]` lookup. -/// -/// `Lc1024` reads the Tables 4.130–4.141 primary columns -/// (== [`short_window_offsets`]), `Lc960` their bracketed -/// 240-transform columns. The LD families have no short windows at -/// all (§4.6.17.2.2 — no block switching), so the lookup itself is -/// invalid and surfaces [`Error::LdShortWindow`]. -pub fn short_window_offsets_family(family: FrameFamily, fs_index: u8) -> Result<&'static [u16]> { - match family { - FrameFamily::Lc1024 => short_window_offsets(fs_index), - FrameFamily::Lc960 => { - let idx = fs_index as usize; - let slice = SWB_OFFSET_SHORT_WINDOW_120 - .get(idx) - .copied() - .unwrap_or(&[][..]); - if slice.is_empty() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - Ok(slice) - } - FrameFamily::Ld512 | FrameFamily::Ld480 => Err(Error::LdShortWindow), - } -} - -/// Apply the §4.6.13 pulse-escape reconstruction to a long-window -/// quantised spectrum. -/// -/// The decoder pseudocode in ISO/IEC 14496-3 §4.6.13 is: -/// -/// ```text -/// if (pulse_data_present) { -/// k = swb_offset_long_window[fs_index][pulse_start_sfb]; -/// for (i = 0; i < number_pulse + 1; i++) { -/// k += pulse_offset[i]; -/// if (x_quant[k] > 0) -/// x_quant[k] += pulse_amp[i]; -/// else -/// x_quant[k] -= pulse_amp[i]; -/// } -/// } -/// ``` -/// -/// `x_quant` is the per-coefficient quantised spectrum from -/// `spectral_data()`; pulse fix-ups overwrite the residual the encoder -/// shaved off the literal escape codeword. -/// -/// ## Inputs -/// -/// * `x_quant` — `&mut [i32]`, length must be at least -/// [`LONG_WINDOW_LEN`] (1024). Note: §4.4.6.3 normatively forbids -/// `pulse_data_present` on `EIGHT_SHORT_SEQUENCE` frames, so the -/// only window-sequence context this loop runs on is long -/// (long / long_start / long_stop). The short-window spectrum is -/// never touched. -/// * `fs_index` — `samplingFrequencyIndex` (Table 1.18, 0..=11). Selects -/// `swb_offset_long_window[fs_index]`. -/// * `pulse_data` — the parsed [`PulseData`] block. `pulses` must be in -/// `1..=4`; `pulse_start_sfb` must be in -/// `0..long_window_offsets(fs_index).len() - 1` (i.e. addressable -/// without going past the last real band). -/// -/// ## Errors -/// -/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] if `fs_index` has no -/// long-window SWB table. -/// * [`Error::PulseDataEncodeInvalid`] if: -/// * `pulse_data.pulses.is_empty()` or `> 4` (Table 4.7 cap), -/// * `pulse_data.pulse_start_sfb` indexes past the last real -/// scalefactor band (`>= long_offsets.len() - 1`), -/// * the running coefficient index `k` reaches or exceeds -/// [`LONG_WINDOW_LEN`] (the per-pulse offset accumulation runs off -/// the end of the spectrum). All three checks correspond to -/// conditions a conforming AAC encoder will never produce — this -/// surfaces malformed bitstreams or caller bugs. -/// * `x_quant.len() < LONG_WINDOW_LEN` panics in debug, saturates the -/// slice length in release. (The caller is expected to pass a -/// correctly-sized buffer; misuse here is a programming error, not -/// a wire-format violation.) -pub fn apply_pulse_data(x_quant: &mut [i32], fs_index: u8, pulse_data: &PulseData) -> Result<()> { - apply_pulse_data_family(x_quant, FrameFamily::Lc1024, fs_index, pulse_data) -} - -/// [`apply_pulse_data`] generalized to any [`FrameFamily`]: the band -/// start `k` is read from the family's own long-window table and the -/// running index is bounded by the family's long spectrum length. -pub fn apply_pulse_data_family( - x_quant: &mut [i32], - family: FrameFamily, - fs_index: u8, - pulse_data: &PulseData, -) -> Result<()> { - // A corrupted stream can pair a pulse_data_present flag with a - // group buffer shorter than the family frame length (e.g. a - // flipped window_sequence bit) — reject rather than assert. - if x_quant.len() < family.frame_len() { - return Err(Error::PulseDataEncodeInvalid); - } - - if pulse_data.pulses.is_empty() || pulse_data.pulses.len() > crate::pulse_data::MAX_PULSES { - return Err(Error::PulseDataEncodeInvalid); - } - - let offsets = long_window_offsets_family(family, fs_index)?; - let start_sfb = pulse_data.pulse_start_sfb as usize; - // Last entry of the offsets slice is the sentinel; bands are - // addressable at indices 0..offsets.len() - 1. - if start_sfb >= offsets.len() - 1 { - return Err(Error::PulseDataEncodeInvalid); - } - - let mut k = offsets[start_sfb] as usize; - let len = x_quant.len().min(family.frame_len()); - for pulse in &pulse_data.pulses { - k += pulse.offset as usize; - if k >= len { - return Err(Error::PulseDataEncodeInvalid); - } - let amp = pulse.amp as i32; - if x_quant[k] > 0 { - x_quant[k] += amp; - } else { - x_quant[k] -= amp; - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn num_swb_long_window() -> [u8; 12] { - // Mirror NUM_SWB_LONG_WINDOW in src/ics_info.rs without - // referencing it from outside the module under test. - [41, 41, 47, 49, 49, 51, 47, 47, 43, 43, 43, 40] - } - - fn num_swb_short_window() -> [u8; 12] { - [12, 12, 12, 14, 14, 14, 15, 15, 15, 15, 15, 15] - } - - #[test] - fn long_offset_lengths_match_num_swb() { - let counts = num_swb_long_window(); - for fs_index in 0..12_u8 { - let offsets = long_window_offsets(fs_index).unwrap(); - assert_eq!( - offsets.len(), - counts[fs_index as usize] as usize + 1, - "fs_index {} long-window offset table length", - fs_index, - ); - } - } - - #[test] - fn short_offset_lengths_match_num_swb() { - let counts = num_swb_short_window(); - for fs_index in 0..12_u8 { - let offsets = short_window_offsets(fs_index).unwrap(); - assert_eq!( - offsets.len(), - counts[fs_index as usize] as usize + 1, - "fs_index {} short-window offset table length", - fs_index, - ); - } - } - - #[test] - fn long_tables_start_at_zero_and_end_at_1024() { - for fs_index in 0..12_u8 { - let offsets = long_window_offsets(fs_index).unwrap(); - assert_eq!(offsets[0], 0, "fs_index {} first offset", fs_index); - assert_eq!( - *offsets.last().unwrap(), - LONG_WINDOW_LEN, - "fs_index {} sentinel", - fs_index - ); - } - } - - #[test] - fn short_tables_start_at_zero_and_end_at_128() { - for fs_index in 0..12_u8 { - let offsets = short_window_offsets(fs_index).unwrap(); - assert_eq!(offsets[0], 0, "fs_index {} first offset", fs_index); - assert_eq!( - *offsets.last().unwrap(), - SHORT_WINDOW_LEN, - "fs_index {} sentinel", - fs_index - ); - } - } - - #[test] - fn long_offsets_are_strictly_monotonic() { - for fs_index in 0..12_u8 { - let offsets = long_window_offsets(fs_index).unwrap(); - for w in offsets.windows(2) { - assert!( - w[0] < w[1], - "fs_index {} non-monotonic at {} -> {}", - fs_index, - w[0], - w[1] - ); - } - } - } - - #[test] - fn short_offsets_are_strictly_monotonic() { - for fs_index in 0..12_u8 { - let offsets = short_window_offsets(fs_index).unwrap(); - for w in offsets.windows(2) { - assert!( - w[0] < w[1], - "fs_index {} non-monotonic at {} -> {}", - fs_index, - w[0], - w[1] - ); - } - } - } - - #[test] - fn fs_index_7350_returns_unsupported() { - assert!(matches!( - long_window_offsets(12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - assert!(matches!( - short_window_offsets(12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - } - - #[test] - fn fs_index_out_of_range_returns_unsupported() { - assert!(matches!( - long_window_offsets(13), - Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) - )); - assert!(matches!( - long_window_offsets(15), - Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) - )); - assert!(matches!( - short_window_offsets(15), - Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) - )); - } - - #[test] - fn table_4_129_spot_check_48k() { - // Table 4.129 — 44.1 / 48 kHz long window. 50 entries. - let offsets = long_window_offsets(3).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[1], 4); - assert_eq!(offsets[10], 40); - assert_eq!(offsets[11], 48); - assert_eq!(offsets[24], 196); - assert_eq!(offsets[49], 1024); - assert_eq!(offsets.len(), 50); - } - - #[test] - fn table_4_131_spot_check_32k() { - // Table 4.131 — 32 kHz long window. 52 entries. - let offsets = long_window_offsets(5).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[50], 992); - assert_eq!(offsets[51], 1024); - assert_eq!(offsets.len(), 52); - } - - #[test] - fn table_4_132_spot_check_8k() { - // Table 4.132 — 8 kHz long window. 41 entries. - let offsets = long_window_offsets(11).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[1], 12); - assert_eq!(offsets[20], 268); - assert_eq!(offsets[40], 1024); - assert_eq!(offsets.len(), 41); - } - - #[test] - fn table_4_134_spot_check_16k() { - // Table 4.134 — 11.025 / 12 / 16 kHz long window. 44 entries. - let offsets = long_window_offsets(8).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[1], 8); - assert_eq!(offsets[21], 212); - assert_eq!(offsets[22], 228); - assert_eq!(offsets[43], 1024); - assert_eq!(offsets.len(), 44); - // Same table also covers 12 kHz (fs 9) and 11.025 kHz (fs 10). - assert_eq!(long_window_offsets(9).unwrap(), offsets); - assert_eq!(long_window_offsets(10).unwrap(), offsets); - } - - #[test] - fn table_4_136_spot_check_24k() { - // Table 4.136 — 22.05 / 24 kHz long window. 48 entries. - let offsets = long_window_offsets(6).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[11], 44); - assert_eq!(offsets[12], 52); - assert_eq!(offsets[23], 148); - assert_eq!(offsets[24], 160); - assert_eq!(offsets[47], 1024); - assert_eq!(offsets.len(), 48); - assert_eq!(long_window_offsets(7).unwrap(), offsets); - } - - #[test] - fn table_4_138_spot_check_64k() { - // Table 4.138 — 64 kHz long window. 48 entries. - let offsets = long_window_offsets(2).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[14], 56); - assert_eq!(offsets[15], 64); - assert_eq!(offsets[22], 140); - assert_eq!(offsets[46], 984); - assert_eq!(offsets[47], 1024); - assert_eq!(offsets.len(), 48); - } - - #[test] - fn table_4_140_spot_check_96k() { - // Table 4.140 — 88.2 / 96 kHz long window. 42 entries. - let offsets = long_window_offsets(0).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[20], 108); - assert_eq!(offsets[21], 120); - assert_eq!(offsets[41], 1024); - assert_eq!(offsets.len(), 42); - assert_eq!(long_window_offsets(1).unwrap(), offsets); - } - - #[test] - fn table_4_130_spot_check_48k_short() { - // Table 4.130 — 32 / 44.1 / 48 kHz short window. 15 entries. - let offsets = short_window_offsets(3).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[5], 20); - assert_eq!(offsets[6], 28); - assert_eq!(offsets[14], 128); - assert_eq!(offsets.len(), 15); - // Shared with 44.1 and 32 kHz. - assert_eq!(short_window_offsets(4).unwrap(), offsets); - assert_eq!(short_window_offsets(5).unwrap(), offsets); - } - - #[test] - fn table_4_133_spot_check_8k_short() { - // Table 4.133 — 8 kHz short window. 16 entries. - let offsets = short_window_offsets(11).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[7], 28); - assert_eq!(offsets[8], 36); - assert_eq!(offsets[15], 128); - assert_eq!(offsets.len(), 16); - } - - #[test] - fn table_4_135_spot_check_16k_short() { - // Table 4.135 — 11.025 / 12 / 16 kHz short window. 16 entries. - let offsets = short_window_offsets(8).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[8], 32); - assert_eq!(offsets[9], 40); - assert_eq!(offsets[15], 128); - assert_eq!(offsets.len(), 16); - assert_eq!(short_window_offsets(9).unwrap(), offsets); - assert_eq!(short_window_offsets(10).unwrap(), offsets); - } - - #[test] - fn table_4_137_spot_check_24k_short() { - // Table 4.137 — 22.05 / 24 kHz short window. 16 entries. - let offsets = short_window_offsets(6).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[7], 28); - assert_eq!(offsets[8], 36); - assert_eq!(offsets[11], 64); - assert_eq!(offsets[15], 128); - assert_eq!(offsets.len(), 16); - assert_eq!(short_window_offsets(7).unwrap(), offsets); - } - - #[test] - fn table_4_139_spot_check_64k_short() { - // Table 4.139 — 64 kHz short window. 13 entries. - let offsets = short_window_offsets(2).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[6], 24); - assert_eq!(offsets[7], 32); - assert_eq!(offsets[11], 92); - assert_eq!(offsets[12], 128); - assert_eq!(offsets.len(), 13); - } - - #[test] - fn table_4_141_spot_check_96k_short() { - // Table 4.141 — 88.2 / 96 kHz short window. 13 entries. - let offsets = short_window_offsets(0).unwrap(); - assert_eq!(offsets[0], 0); - assert_eq!(offsets[7], 32); - assert_eq!(offsets[12], 128); - assert_eq!(offsets.len(), 13); - assert_eq!(short_window_offsets(1).unwrap(), offsets); - } - - #[test] - fn apply_pulse_data_single_positive_pulse_48k() { - use crate::pulse_data::{Pulse, PulseData}; - // 48 kHz long, swb_offset_long[3] = 12, then a single pulse - // with offset=5 (k = 12 + 5 = 17), amp=3, on a positive - // x_quant: x_quant[17] += 3. - let mut x_quant = vec![0_i32; 1024]; - x_quant[17] = 7; - let pd = PulseData { - pulse_start_sfb: 3, - pulses: vec![Pulse { offset: 5, amp: 3 }], - }; - apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); - assert_eq!(x_quant[17], 10); - } - - #[test] - fn apply_pulse_data_single_negative_pulse_48k() { - use crate::pulse_data::{Pulse, PulseData}; - // x_quant <= 0 (incl. 0): amp is subtracted. - let mut x_quant = vec![0_i32; 1024]; - x_quant[17] = -7; - let pd = PulseData { - pulse_start_sfb: 3, - pulses: vec![Pulse { offset: 5, amp: 3 }], - }; - apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); - assert_eq!(x_quant[17], -10); - } - - #[test] - fn apply_pulse_data_zero_coefficient_subtracts_amp() { - use crate::pulse_data::{Pulse, PulseData}; - // Zero is not > 0, so it falls into the else branch and amp - // is subtracted (matching the §4.6.13 pseudocode). - let mut x_quant = vec![0_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 0, - pulses: vec![Pulse { offset: 1, amp: 4 }], - }; - apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); - assert_eq!(x_quant[1], -4); - } - - #[test] - fn apply_pulse_data_four_pulses_accumulate_k() { - use crate::pulse_data::{Pulse, PulseData}; - // 48 kHz long, swb_offset_long[10] = 40. Four pulses with - // offsets 1/2/3/4 land at k = 41, 43, 46, 50. All four target - // coefficients are set positive so each is incremented by its - // amplitude. - let mut x_quant = vec![1_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 10, - pulses: vec![ - Pulse { offset: 1, amp: 1 }, - Pulse { offset: 2, amp: 2 }, - Pulse { offset: 3, amp: 3 }, - Pulse { offset: 4, amp: 4 }, - ], - }; - apply_pulse_data(&mut x_quant, 3, &pd).unwrap(); - assert_eq!(x_quant[41], 2); - assert_eq!(x_quant[43], 3); - assert_eq!(x_quant[46], 4); - assert_eq!(x_quant[50], 5); - } - - #[test] - fn apply_pulse_data_overrun_rejected() { - use crate::pulse_data::{Pulse, PulseData}; - // Pulse offset that drives k past 1024 is rejected. - let mut x_quant = vec![0_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 48, // 48 kHz, swb_offset_long[48] = 928 - pulses: vec![Pulse { offset: 31, amp: 0 }; 4], // 928 + 4*31 = 1052 - }; - assert!(matches!( - apply_pulse_data(&mut x_quant, 3, &pd), - Err(Error::PulseDataEncodeInvalid) - )); - } - - #[test] - fn apply_pulse_data_start_sfb_past_last_band_rejected() { - use crate::pulse_data::{Pulse, PulseData}; - // 48 kHz long has 49 SWB; addressable band indices are 0..=48. - let mut x_quant = vec![0_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 49, - pulses: vec![Pulse { offset: 1, amp: 1 }], - }; - assert!(matches!( - apply_pulse_data(&mut x_quant, 3, &pd), - Err(Error::PulseDataEncodeInvalid) - )); - } - - #[test] - fn apply_pulse_data_empty_pulses_rejected() { - use crate::pulse_data::PulseData; - let mut x_quant = vec![0_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 0, - pulses: vec![], - }; - assert!(matches!( - apply_pulse_data(&mut x_quant, 3, &pd), - Err(Error::PulseDataEncodeInvalid) - )); - } - - #[test] - fn apply_pulse_data_too_many_pulses_rejected() { - use crate::pulse_data::{Pulse, PulseData}; - let mut x_quant = vec![0_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 0, - pulses: vec![Pulse { offset: 1, amp: 1 }; 5], - }; - assert!(matches!( - apply_pulse_data(&mut x_quant, 3, &pd), - Err(Error::PulseDataEncodeInvalid) - )); - } - - #[test] - fn apply_pulse_data_unsupported_fs_index_rejected() { - use crate::pulse_data::{Pulse, PulseData}; - let mut x_quant = vec![0_i32; 1024]; - let pd = PulseData { - pulse_start_sfb: 0, - pulses: vec![Pulse { offset: 1, amp: 1 }], - }; - assert!(matches!( - apply_pulse_data(&mut x_quant, 12, &pd), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - } - - #[test] - fn long_widths_match_num_swb_sums() { - // sum of per-band widths must equal LONG_WINDOW_LEN for every - // table. - for fs_index in 0..12_u8 { - let offsets = long_window_offsets(fs_index).unwrap(); - let total: u32 = offsets.windows(2).map(|w| (w[1] - w[0]) as u32).sum(); - assert_eq!(total, LONG_WINDOW_LEN as u32); - } - } - - #[test] - fn short_widths_match_num_swb_sums() { - for fs_index in 0..12_u8 { - let offsets = short_window_offsets(fs_index).unwrap(); - let total: u32 = offsets.windows(2).map(|w| (w[1] - w[0]) as u32).sum(); - assert_eq!(total, SHORT_WINDOW_LEN as u32); - } - } - - // -- FrameFamily geometry ------------------------------------------------ - - #[test] - fn family_resolution_follows_4_5_1_1() { - assert_eq!( - FrameFamily::from_aot_and_flag(2, false), - FrameFamily::Lc1024 - ); - assert_eq!(FrameFamily::from_aot_and_flag(2, true), FrameFamily::Lc960); - assert_eq!(FrameFamily::from_aot_and_flag(17, true), FrameFamily::Lc960); - assert_eq!( - FrameFamily::from_aot_and_flag(23, false), - FrameFamily::Ld512 - ); - assert_eq!(FrameFamily::from_aot_and_flag(23, true), FrameFamily::Ld480); - } - - #[test] - fn family_lengths() { - assert_eq!(FrameFamily::Lc1024.frame_len(), 1024); - assert_eq!(FrameFamily::Lc1024.long_transform_len(), 2048); - assert_eq!(FrameFamily::Lc1024.short_window_len(), Some(128)); - assert_eq!(FrameFamily::Lc1024.short_transform_len(), Some(256)); - assert_eq!(FrameFamily::Lc960.frame_len(), 960); - assert_eq!(FrameFamily::Lc960.long_transform_len(), 1920); - assert_eq!(FrameFamily::Lc960.short_window_len(), Some(120)); - assert_eq!(FrameFamily::Lc960.short_transform_len(), Some(240)); - assert_eq!(FrameFamily::Ld512.frame_len(), 512); - assert_eq!(FrameFamily::Ld512.long_transform_len(), 1024); - assert_eq!(FrameFamily::Ld512.short_window_len(), None); - assert_eq!(FrameFamily::Ld480.frame_len(), 480); - assert_eq!(FrameFamily::Ld480.long_transform_len(), 960); - assert_eq!(FrameFamily::Ld480.short_window_len(), None); - assert!(FrameFamily::Ld512.is_ld()); - assert!(FrameFamily::Ld480.is_ld()); - assert!(!FrameFamily::Lc1024.is_ld()); - assert!(!FrameFamily::Lc960.is_ld()); - } - - #[test] - fn lc1024_family_lookup_matches_legacy_accessors() { - for fs_index in 0..12_u8 { - assert_eq!( - long_window_offsets_family(FrameFamily::Lc1024, fs_index).unwrap(), - long_window_offsets(fs_index).unwrap() - ); - assert_eq!( - short_window_offsets_family(FrameFamily::Lc1024, fs_index).unwrap(), - short_window_offsets(fs_index).unwrap() - ); - } - } - - #[test] - fn lc960_long_tables_are_the_bracketed_columns() { - // Tables 4.129–4.140: the 1920-transform column shares every - // band start with the 2048 column; the sentinel becomes 960 - // and any offsets >= 960 are dropped. So each 960 table must - // be a strict prefix of its 1024 sibling with the sentinel - // replaced by 960. - for fs_index in 0..12_u8 { - let long1024 = long_window_offsets(fs_index).unwrap(); - let long960 = long_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); - let n = long960.len(); - assert_eq!(*long960.last().unwrap(), 960, "fs {} sentinel", fs_index); - assert_eq!( - &long960[..n - 1], - &long1024[..n - 1], - "fs {} shared band starts", - fs_index - ); - // Everything dropped from the 1024 table must be >= 960. - for &off in &long1024[n - 1..] { - assert!(off >= 960, "fs {} dropped offset {}", fs_index, off); - } - // Strictly monotonic, starts at zero. - assert_eq!(long960[0], 0); - for w in long960.windows(2) { - assert!(w[0] < w[1], "fs {} non-monotonic", fs_index); - } - } - } - - #[test] - fn lc960_expected_num_swb() { - // Bracket-derived band counts: 44.1/48 keeps all 49 bands - // (only the sentinel shrinks); 32 kHz drops from 51 to 49; - // 64 kHz prints `47 (46)` in Table 4.138; the rest drop - // exactly the bands whose start would be >= 960. - let expected: [usize; 12] = [40, 40, 46, 49, 49, 49, 46, 46, 42, 42, 42, 40]; - for fs_index in 0..12_u8 { - let long960 = long_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); - assert_eq!( - long960.len() - 1, - expected[fs_index as usize], - "fs {} num_swb", - fs_index - ); - } - } - - #[test] - fn lc960_short_tables_only_rescale_the_sentinel() { - for fs_index in 0..12_u8 { - let short128 = short_window_offsets(fs_index).unwrap(); - let short120 = short_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); - assert_eq!(short120.len(), short128.len(), "fs {}", fs_index); - let n = short120.len(); - assert_eq!(&short120[..n - 1], &short128[..n - 1]); - assert_eq!(short120[n - 1], 120); - for w in short120.windows(2) { - assert!(w[0] < w[1], "fs {} non-monotonic", fs_index); - } - } - } - - #[test] - fn ld_tables_match_spec_counts_and_sentinels() { - // Table 4.143 / 4.145 / 4.147 — LD 512: 36 / 37 / 31 SWB. - for (fs, num) in [(3u8, 36usize), (4, 36), (5, 37), (6, 31), (7, 31)] { - let t = long_window_offsets_family(FrameFamily::Ld512, fs).unwrap(); - assert_eq!(t.len() - 1, num, "LD512 fs {}", fs); - assert_eq!(t[0], 0); - assert_eq!(*t.last().unwrap(), 512); - for w in t.windows(2) { - assert!(w[0] < w[1]); - } - } - // Table 4.142 / 4.144 / 4.146 — LD 480: 35 / 37 / 30 SWB. - for (fs, num) in [(3u8, 35usize), (4, 35), (5, 37), (6, 30), (7, 30)] { - let t = long_window_offsets_family(FrameFamily::Ld480, fs).unwrap(); - assert_eq!(t.len() - 1, num, "LD480 fs {}", fs); - assert_eq!(t[0], 0); - assert_eq!(*t.last().unwrap(), 480); - for w in t.windows(2) { - assert!(w[0] < w[1]); - } - } - } - - #[test] - fn ld_512_spot_checks() { - // Table 4.143 spot rows: swb 16 -> 68, swb 21 -> 112, - // swb 27 -> 208, swb 35 -> 460. - let t = long_window_offsets_family(FrameFamily::Ld512, 3).unwrap(); - assert_eq!(t[16], 68); - assert_eq!(t[21], 112); - assert_eq!(t[27], 208); - assert_eq!(t[35], 460); - // Table 4.145 spot rows: swb 15 -> 64, swb 20 -> 108, - // swb 30 -> 288, swb 36 -> 480. - let t = long_window_offsets_family(FrameFamily::Ld512, 5).unwrap(); - assert_eq!(t[15], 64); - assert_eq!(t[20], 108); - assert_eq!(t[30], 288); - assert_eq!(t[36], 480); - // Table 4.147 spot rows: swb 12 -> 52, swb 18 -> 120, - // swb 25 -> 320, swb 30 -> 480. - let t = long_window_offsets_family(FrameFamily::Ld512, 6).unwrap(); - assert_eq!(t[12], 52); - assert_eq!(t[18], 120); - assert_eq!(t[25], 320); - assert_eq!(t[30], 480); - } - - #[test] - fn ld_480_spot_checks() { - // Table 4.142 spot rows: swb 15 -> 64, swb 20 -> 108, - // swb 27 -> 212, swb 34 -> 432. - let t = long_window_offsets_family(FrameFamily::Ld480, 4).unwrap(); - assert_eq!(t[15], 64); - assert_eq!(t[20], 108); - assert_eq!(t[27], 212); - assert_eq!(t[34], 432); - // Table 4.144 spot rows: swb 17 -> 72, swb 23 -> 124, - // swb 29 -> 224, swb 36 -> 448. - let t = long_window_offsets_family(FrameFamily::Ld480, 5).unwrap(); - assert_eq!(t[17], 72); - assert_eq!(t[23], 124); - assert_eq!(t[29], 224); - assert_eq!(t[36], 448); - // Table 4.146 spot rows: swb 12 -> 52, swb 16 -> 92, - // swb 22 -> 224, swb 29 -> 448. - let t = long_window_offsets_family(FrameFamily::Ld480, 7).unwrap(); - assert_eq!(t[12], 52); - assert_eq!(t[16], 92); - assert_eq!(t[22], 224); - assert_eq!(t[29], 448); - } - - #[test] - fn ld_nearest_defined_table_rule() { - // §4.5.1.1: rates the LD tables omit resolve to the nearest - // defined rate — 96/88.2/64 kHz to the 48 kHz table, 16 kHz - // and below to the 22.05 kHz table. - let t48 = long_window_offsets_family(FrameFamily::Ld512, 3).unwrap(); - for fs in [0u8, 1, 2, 4] { - assert_eq!( - long_window_offsets_family(FrameFamily::Ld512, fs).unwrap(), - t48 - ); - } - let t22 = long_window_offsets_family(FrameFamily::Ld512, 7).unwrap(); - for fs in [6u8, 8, 9, 10, 11] { - assert_eq!( - long_window_offsets_family(FrameFamily::Ld512, fs).unwrap(), - t22 - ); - } - assert!(matches!( - long_window_offsets_family(FrameFamily::Ld512, 12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - } - - #[test] - fn ld_short_lookup_is_rejected() { - assert!(matches!( - short_window_offsets_family(FrameFamily::Ld512, 3), - Err(Error::LdShortWindow) - )); - assert!(matches!( - short_window_offsets_family(FrameFamily::Ld480, 3), - Err(Error::LdShortWindow) - )); - } - - #[test] - fn family_widths_sum_to_family_lengths() { - for family in [FrameFamily::Lc960, FrameFamily::Ld512, FrameFamily::Ld480] { - for fs_index in 0..12_u8 { - let long = long_window_offsets_family(family, fs_index).unwrap(); - assert_eq!( - *long.last().unwrap() as usize, - family.frame_len(), - "{:?} fs {} long sentinel", - family, - fs_index - ); - } - } - for fs_index in 0..12_u8 { - let short = short_window_offsets_family(FrameFamily::Lc960, fs_index).unwrap(); - assert_eq!(*short.last().unwrap(), 120); - } - } - - #[test] - fn apply_pulse_data_family_uses_family_bounds() { - use crate::pulse_data::{Pulse, PulseData}; - // LD512 at 48 kHz: swb_offset[35] == 460 is the last band. - // A pulse landing at 460 + 40 = 500 stays inside the 512-line - // spectrum, while the same pulse under a 1024-line check - // would also pass — so also verify the overrun at >= 512. - let mut x_quant = vec![1_i32; 512]; - let pd = PulseData { - pulse_start_sfb: 35, - pulses: vec![Pulse { offset: 31, amp: 2 }], - }; - apply_pulse_data_family(&mut x_quant, FrameFamily::Ld512, 3, &pd).unwrap(); - assert_eq!(x_quant[491], 3); - - let mut x_quant = vec![1_i32; 512]; - let pd = PulseData { - pulse_start_sfb: 35, - pulses: vec![Pulse { offset: 31, amp: 2 }; 2], // 460+62 = 522 >= 512 - }; - assert!(matches!( - apply_pulse_data_family(&mut x_quant, FrameFamily::Ld512, 3, &pd), - Err(Error::PulseDataEncodeInvalid) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/tns_coef.rs b/crates/vendor/oxideav-aac/src/tns_coef.rs deleted file mode 100644 index b2542358..00000000 --- a/crates/vendor/oxideav-aac/src/tns_coef.rs +++ /dev/null @@ -1,1239 +0,0 @@ -//! TNS coefficient inverse-quantisation and LPC step-up — ISO/IEC -//! 14496-3 §4.6.9.3 (`tns_decode_coef` pseudo-code) plus the -//! ISO/IEC 14496-3:2001 §C.6 encoder-side quantisation companion. -//! -//! Temporal Noise Shaping carries one all-pole filter per scalefactor -//! region. The wire `coef[i]` slots produced by [`crate::tns_data`] -//! hold the per-coefficient *quantised reflection (PARCOR)* index in -//! `coef_bits` (2..=4) of unsigned magnitude with the high bit acting -//! as a sign flag (signed-magnitude / two's-complement padding). The -//! decoder reconstructs the floating-point reflection coefficient -//! `rq[i]` by: -//! -//! 1. Sign-extending the truncated wire value to a normal signed int. -//! 2. Inverse-quantising with `sin(index / iqfac)` where `iqfac` -//! depends on the sign of `index` (`iqfac` for non-negative, -//! `iqfac_m` for negative — the half-bit offset matches the -//! encoder's rounded quantisation). -//! 3. Running the §4.6.9.3 *conversion-to-LPC* step-up loop that -//! converts the order-`order` PARCOR array into an order-`order` -//! direct-form LPC `a[]` vector with `a[0] = 1`. -//! -//! The encoder side (§C.6) inverts steps (1) and (2): given the -//! floating-point reflection coefficients computed by Levinson-Durbin, -//! quantise via `NINT(arcsin(r) * iqfac)`, where `iqfac` again branches -//! on the sign of `r`. The step-up loop is unchanged — both encoder and -//! decoder run it to derive the same `a[]` array that drives the -//! `tns_ar_filter()` / inverse FIR pass. -//! -//! ## §4.6.9.3 pseudocode (transcribed for cross-check) -//! -//! ```text -//! tns_decode_coef( order, coef_res_bits, coef_compress, coef[], a[] ) -//! { -//! sgn_mask[] = { 0x2, 0x4, 0x8 }; -//! neg_mask[] = { ~0x3, ~0x7, ~0xf }; -//! -//! coef_res2 = coef_res_bits - coef_compress; -//! s_mask = sgn_mask[ coef_res2 - 2 ]; -//! n_mask = neg_mask[ coef_res2 - 2 ]; -//! -//! for (i = 0; i < order; i++) -//! tmp[i] = (coef[i] & s_mask) ? (coef[i] | n_mask) : coef[i]; -//! -//! iqfac = ((1 << (coef_res_bits-1)) - 0.5) / (π/2.0); -//! iqfac_m = ((1 << (coef_res_bits-1)) + 0.5) / (π/2.0); -//! for (i = 0; i < order; i++) { -//! tmp2[i] = sin( tmp[i] / ((tmp[i] >= 0) ? iqfac : iqfac_m) ); -//! } -//! -//! a[0] = 1; -//! for (m = 1; m <= order; m++) { -//! for (i = 1; i < m; i++) -//! b[i] = a[i] + tmp2[m-1] * a[m-i]; -//! for (i = 1; i < m; i++) -//! a[i] = b[i]; -//! a[m] = tmp2[m-1]; -//! } -//! } -//! ``` -//! -//! The `sgn_mask` / `neg_mask` pair encode the two's-complement -//! sign-extension of a `coef_res2`-bit field (`coef_res2 ∈ {2, 3, 4}`). -//! `s_mask` is `1 << (coef_res2 - 1)` (the MSB of the truncated field) -//! and `n_mask` is `~((1 << coef_res2) - 1)` (the bits that need to be -//! filled with 1 to extend a negative value into a normal signed int). -//! -//! ## What this module covers -//! -//! * [`iqfac`] / [`iqfac_m`] — the §4.6.9.3 quantiser scale factors -//! `((1 << (n-1)) ± 0.5) / (π/2)`. Exposed as standalone helpers so -//! the §C.6 encoder path can re-use the same constants. -//! * [`sign_extend_coef`] — inverse of `(coef & s_mask) ? coef | -//! n_mask : coef`. Takes a wire `coef` (held in the low `coef_res2` -//! bits as transmitted) and a `coef_res2 ∈ {2, 3, 4}` field width; -//! returns the matching signed integer. -//! * [`tns_decode_coef`] — the full §4.6.9.3 path: wire `coef[]` → -//! floating-point `tmp2[]` (the inverse-quantised PARCOR -//! coefficients). -//! * [`tns_encode_coef`] — the §C.6 inverse: floating-point reflection -//! coefficients → wire `coef[]` values ready for [`crate::tns_data`]. -//! * [`lpc_step_up`] — the §4.6.9.3 *conversion-to-LPC* loop. Takes a -//! slice of inverse-quantised PARCOR `tmp2[]` values and returns the -//! `order + 1` direct-form LPC `a[]` vector with `a[0] = 1.0`. -//! * [`tns_decode_coef_to_lpc`] — convenience wrapper that runs -//! [`tns_decode_coef`] followed by [`lpc_step_up`]; the -//! reconstruction loop will call this once per `(window, filter)` -//! pair. -//! * [`tns_ar_filter`] — the §4.6.9.3 `tns_ar_filter()` all-pole IIR -//! pass. Operates in place over a strided region of the dequantised -//! spectrum (`start` / `size` / `inc`) driven by the `lpc[]` array -//! from [`lpc_step_up`]. Filter state is zero-seeded per -//! invocation, exactly as the spec mandates. -//! -//! ## What this module does *not* cover -//! -//! * The §4.6.9 `tns_decode_frame()` orchestration that dispatches -//! `tns_decode_coef_to_lpc` / `tns_ar_filter` per filter per window. -//! That orchestration is the responsibility of the eventual -//! `individual_channel_stream()` reconstruction driver. -//! * The §4.6.17.3.4 ER AAC LD `int_tns_decode_coef()` integer -//! variant. The LD path uses a fixed-point arithmetic surface that -//! we do not need until the AAC LD reconstruction path is wired. -//! * The §C.6 Levinson-Durbin / autocorrelation reflection-coefficient -//! derivation. The encoder gets floating-point PARCOR coefficients -//! from some upstream LPC estimator (a standard speech-coding -//! procedure); this module accepts the already-derived `r[]` array -//! and quantises it. -//! -//! ## Numerical contract -//! -//! Both `iqfac` and `iqfac_m` are exactly representable as `f64` for -//! every legal `coef_res_bits ∈ {3, 4}`: -//! -//! | coef_res_bits | iqfac (≈) | iqfac_m (≈) | -//! |---------------|-------------------------|-------------------------| -//! | 3 | `3.5 / (π/2) ≈ 2.228...`| `4.5 / (π/2) ≈ 2.864...`| -//! | 4 | `7.5 / (π/2) ≈ 4.774...`| `8.5 / (π/2) ≈ 5.411...`| -//! -//! The encoder's `NINT(arcsin(r) * iqfac)` rounding is implemented via -//! `f64::round` (round-half-away-from-zero, matching the spec's `NINT` -//! convention). A reflection coefficient `r = 0.0` quantises to -//! `index = 0` (the `iqfac` branch is taken because `r >= 0`); the -//! decoder then reconstructs `sin(0 / iqfac) = 0.0`. Likewise the -//! sentinel `r = 1.0` rounds to the field maximum (`6` for -//! `coef_res2 = 3`, `7` for `coef_res2 = 4` after `coef_compress = 0`) -//! and `r = -1.0` rounds to the field minimum (`-7` / `-8`); both -//! recover via `sin(±π/2) = ±1.0` to within IEEE-754 round-off -//! (`|round-trip error| < 1e-15`). All in-range PARCOR values quantise -//! cleanly without saturation; an out-of-range `r` (`|r| > 1.0`) is -//! rejected by [`tns_encode_coef`] with -//! [`Error::TnsCoefOutOfRange`] because `arcsin` is undefined there. -//! -//! The signed-magnitude wire fold preserves round-trip: every legal -//! sign-extended index `i` in `[-(1 << (coef_res2-1)), -//! (1 << (coef_res2-1)) - 1]` maps back to a unique `coef_res2`-bit -//! pattern. The step-up loop is exact (no quantisation), so the same -//! quantised PARCOR array always yields bit-identical LPC coefficients. - -use core::f64::consts::PI; - -use crate::{Error, Result}; - -/// Half-π. Cached so the [`iqfac`] / [`iqfac_m`] arithmetic matches -/// the spec's literal `π/2.0` division. -const HALF_PI: f64 = PI / 2.0; - -/// `iqfac` per §4.6.9.3. Branches on a *non-negative* index / PARCOR -/// value. Defined as `((1 << (coef_res_bits-1)) - 0.5) / (π/2)`. -/// -/// Returns [`Error::TnsCoefOutOfRange`] when `coef_res_bits` lies -/// outside `3..=4` (the legal `coef_res[w] + 3` values per -/// §4.6.9.3 and §C.6, where `coef_res[w] ∈ {0, 1}` is the wire flag). -pub fn iqfac(coef_res_bits: u32) -> Result { - if !(3..=4).contains(&coef_res_bits) { - return Err(Error::TnsCoefOutOfRange); - } - let scale = (1u32 << (coef_res_bits - 1)) as f64 - 0.5; - Ok(scale / HALF_PI) -} - -/// `iqfac_m` per §4.6.9.3. Branches on a *negative* index / PARCOR -/// value. Defined as `((1 << (coef_res_bits-1)) + 0.5) / (π/2)`. -/// -/// Errors as [`iqfac`]. -pub fn iqfac_m(coef_res_bits: u32) -> Result { - if !(3..=4).contains(&coef_res_bits) { - return Err(Error::TnsCoefOutOfRange); - } - let scale = (1u32 << (coef_res_bits - 1)) as f64 + 0.5; - Ok(scale / HALF_PI) -} - -/// Sign-extend a wire `coef` value held in the low `coef_res2` bits -/// into a normal signed integer. -/// -/// `coef_res2 = coef_res_bits - coef_compress` per §4.6.9.3 and is -/// always in `{2, 3, 4}`. The spec's `sgn_mask = 1 << -/// (coef_res2 - 1)` selects the MSB of the truncated field; if that -/// bit is set, the spec ORs in `neg_mask = ~((1 << coef_res2) - 1)` -/// to fill the upper bits with 1 (two's-complement sign extension). -/// -/// Returns [`Error::TnsCoefOutOfRange`] when `coef_res2` lies outside -/// `2..=4` or when `coef` does not fit in `coef_res2` bits. -pub fn sign_extend_coef(coef: u32, coef_res2: u32) -> Result { - if !(2..=4).contains(&coef_res2) { - return Err(Error::TnsCoefOutOfRange); - } - let field_mask = (1u32 << coef_res2) - 1; - if coef & !field_mask != 0 { - return Err(Error::TnsCoefOutOfRange); - } - let sgn_mask = 1u32 << (coef_res2 - 1); - if coef & sgn_mask != 0 { - // Negative — OR with the bits above the field. - let neg_mask = !field_mask; - Ok((coef | neg_mask) as i32) - } else { - Ok(coef as i32) - } -} - -/// Inverse of [`sign_extend_coef`]: pack a signed integer back into a -/// `coef_res2`-bit wire field. Used by the encoder to emit the wire -/// `coef[i]` slot. -/// -/// Returns [`Error::TnsCoefOutOfRange`] when `coef_res2` is outside -/// `2..=4`, or when `value` is outside the field-representable range -/// `-(1 << (coef_res2-1))..=(1 << (coef_res2-1)) - 1`. -pub fn pack_coef(value: i32, coef_res2: u32) -> Result { - if !(2..=4).contains(&coef_res2) { - return Err(Error::TnsCoefOutOfRange); - } - let half = 1i32 << (coef_res2 - 1); - if !(-half..half).contains(&value) { - return Err(Error::TnsCoefOutOfRange); - } - let field_mask = (1u32 << coef_res2) - 1; - Ok((value as u32) & field_mask) -} - -/// Run §4.6.9.3 `tns_decode_coef`: sign-extend the wire `coef[]`, -/// then inverse-quantise via `sin(tmp[i] / iqfac_branch)` to recover -/// the floating-point PARCOR (reflection-coefficient) array. -/// -/// * `coef_res_bits` is the spec's `coef_res[w] + 3`, i.e. either -/// `3` (`coef_res = 0`) or `4` (`coef_res = 1`). -/// * `coef_compress` is the per-filter flag (0 or 1). The §4.6.9.3 -/// field width on the wire is `coef_res2 = coef_res_bits - -/// coef_compress` bits per coefficient. -/// * `coef` is the per-coefficient wire slice produced by -/// [`crate::tns_data::TnsData::parse`], length `order`. Every entry -/// must fit in `coef_res2` bits (the parser already enforces this, -/// so a runtime overflow here means the caller fabricated an -/// in-memory [`crate::tns_data::TnsFilter`]). -/// -/// The returned `Vec` has the same length as `coef` and contains -/// the §4.6.9.3 `tmp2[]` array (PARCOR coefficients in `[-1, 1]`). -/// -/// Returns [`Error::TnsCoefOutOfRange`] on invalid `coef_res_bits` -/// (`!= 3 && != 4`), `coef_compress > 1`, or a `coef[i]` that does -/// not fit `coef_res2` bits. -pub fn tns_decode_coef(coef_res_bits: u32, coef_compress: u32, coef: &[u32]) -> Result> { - if coef_compress > 1 { - return Err(Error::TnsCoefOutOfRange); - } - let coef_res2 = coef_res_bits - .checked_sub(coef_compress) - .ok_or(Error::TnsCoefOutOfRange)?; - let iq = iqfac(coef_res_bits)?; - let iq_m = iqfac_m(coef_res_bits)?; - - let mut out = Vec::with_capacity(coef.len()); - for &c in coef { - let signed = sign_extend_coef(c, coef_res2)?; - let divisor = if signed >= 0 { iq } else { iq_m }; - out.push((signed as f64 / divisor).sin()); - } - Ok(out) -} - -/// Encoder-side inverse of [`tns_decode_coef`] per §C.6: quantise a -/// PARCOR reflection-coefficient array `r[]` into the wire `coef[]` -/// slots [`crate::tns_data::TnsFilter::coef`] consumes. -/// -/// The quantisation rule is `index = NINT(arcsin(r) * iqfac_branch)`, -/// where the `iqfac_branch` selector is keyed on the *sign of `r`* -/// (not the index): non-negative `r` uses [`iqfac`], strictly -/// negative `r` uses [`iqfac_m`]. After rounding, the encoder clamps -/// `index` to the `coef_res2`-bit signed-magnitude range -/// `-(1 << (coef_res2-1))..=(1 << (coef_res2-1)) - 1` and folds it -/// through [`pack_coef`]. -/// -/// Returns [`Error::TnsCoefOutOfRange`] on invalid `coef_res_bits` / -/// `coef_compress`, or on a `|r| > 1.0` value (`arcsin` is undefined -/// outside `[-1, 1]`). -pub fn tns_encode_coef(coef_res_bits: u32, coef_compress: u32, r: &[f64]) -> Result> { - if coef_compress > 1 { - return Err(Error::TnsCoefOutOfRange); - } - let coef_res2 = coef_res_bits - .checked_sub(coef_compress) - .ok_or(Error::TnsCoefOutOfRange)?; - let iq = iqfac(coef_res_bits)?; - let iq_m = iqfac_m(coef_res_bits)?; - let half = 1i32 << (coef_res2 - 1); - let max_idx = half - 1; - let min_idx = -half; - - let mut out = Vec::with_capacity(r.len()); - for &value in r { - if !(-1.0..=1.0).contains(&value) { - return Err(Error::TnsCoefOutOfRange); - } - let scale = if value >= 0.0 { iq } else { iq_m }; - // NINT = round-half-away-from-zero; f64::round matches this. - let raw = (value.asin() * scale).round() as i32; - let clamped = raw.clamp(min_idx, max_idx); - out.push(pack_coef(clamped, coef_res2)?); - } - Ok(out) -} - -/// §4.6.9.3 *conversion to LPC coefficients* — the "step-up procedure" -/// that converts an order-`N` PARCOR array `tmp2[]` (output of -/// [`tns_decode_coef`]) into the order-`N` direct-form LPC vector -/// `a[]` of length `N + 1` with `a[0] = 1.0`. -/// -/// The loop is: -/// -/// ```text -/// a[0] = 1 -/// for (m = 1; m <= order; m++) { -/// for (i = 1; i < m; i++) -/// b[i] = a[i] + tmp2[m-1] * a[m-i]; -/// for (i = 1; i < m; i++) -/// a[i] = b[i]; -/// a[m] = tmp2[m-1]; -/// } -/// ``` -/// -/// `parcor.len()` is the filter order; the returned vector has -/// `parcor.len() + 1` entries. An empty `parcor` slice produces the -/// degenerate `[1.0]` (no filtering — every filter with `order == 0` -/// is skipped by the §4.6.9.3 outer loop). -pub fn lpc_step_up(parcor: &[f64]) -> Vec { - let order = parcor.len(); - // `a` is the running LPC coefficient array. Length is `order + 1` - // throughout; only the first `m + 1` slots are meaningful at the - // start of iteration `m` (the remainder is zeroed and overwritten - // by later iterations). - let mut a = vec![0.0_f64; order + 1]; - a[0] = 1.0; - // Scratch `b[]` matches the spec's pseudocode literally. Allocated - // once and reused across iterations; only the low `m` slots are - // consulted per `m`. - let mut b = vec![0.0_f64; order + 1]; - for m in 1..=order { - let k = parcor[m - 1]; - for i in 1..m { - b[i] = a[i] + k * a[m - i]; - } - // Copy the m-1 newly-derived `b[1..m]` slots back into a; - // clippy prefers `copy_from_slice` here over a manual loop. - a[1..m].copy_from_slice(&b[1..m]); - a[m] = k; - } - a -} - -/// Convenience wrapper that runs [`tns_decode_coef`] then -/// [`lpc_step_up`] in one call. -/// -/// Returns the `order + 1` LPC `a[]` vector (`a[0] = 1.0`) the -/// §4.6.9.3 `tns_ar_filter()` loop consumes. -pub fn tns_decode_coef_to_lpc( - coef_res_bits: u32, - coef_compress: u32, - coef: &[u32], -) -> Result> { - let parcor = tns_decode_coef(coef_res_bits, coef_compress, coef)?; - Ok(lpc_step_up(&parcor)) -} - -/// §4.6.9.3 `tns_ar_filter()` — the simple all-pole (auto-regressive) -/// IIR filter that TNS slides across a strided region of the -/// dequantised MDCT spectrum, in place. -/// -/// The §4.6.9.3 pseudocode defines the filter by the recurrence -/// -/// ```text -/// y(n) = x(n) - lpc[1]*y(n-1) - ... - lpc[order]*y(n-order) -/// ``` -/// -/// with these spec-mandated properties: -/// -/// * the filter state (`y(n-1) .. y(n-order)`) is **initialised to -/// zero** at every invocation; -/// * the output overwrites the input (**in-place operation**); -/// * `size` samples are processed, stepping to the next sample by the -/// index increment `inc` (`+1` upward, `−1` downward). -/// -/// `lpc` is the direct-form `a[]` array produced by [`lpc_step_up`] / -/// [`tns_decode_coef_to_lpc`]: `lpc[0] == 1.0` and `lpc[1..=order]` -/// are the predictor taps. The filter order is `lpc.len() - 1`; a -/// `lpc` of length 1 (order 0) leaves the spectrum untouched. -/// -/// `spectrum` is the full per-window coefficient buffer. `start` is -/// the index of the first sample to process — for an upward filter -/// (`inc = 1`) this is the §4.6.9.3 `start = swb_offset[bottom]`; for -/// a downward filter (`inc = -1`) the §4.6.9.3 `tns_decode_frame` -/// outer loop has already set `start = end - 1`, so the same `start` -/// argument is the top of the region and the walk proceeds toward -/// lower indices. -/// -/// The recurrence is evaluated literally: because the output is -/// written over the input and the filter reads back its own previous -/// *outputs* (`y`), the per-tap history is a small ring of the last -/// `order` produced samples, seeded with zeros. -/// -/// Returns [`Error::TnsCoefOutOfRange`] when: -/// -/// * `lpc` is empty (no `a[0]`), -/// * `inc` is neither `+1` nor `-1`, -/// * the strided walk of `size` samples starting at `start` with step -/// `inc` would leave the bounds of `spectrum` (an out-of-range -/// `start`/`size`/`inc` triple the caller fabricated; the -/// §4.6.9.3 `size = end - start <= 0` guard and the `swb_offset` -/// clamping in `tns_decode_frame` keep legitimate callers in range). -pub fn tns_ar_filter( - spectrum: &mut [f64], - start: usize, - size: usize, - inc: i32, - lpc: &[f64], -) -> Result<()> { - if lpc.is_empty() { - return Err(Error::TnsCoefOutOfRange); - } - if inc != 1 && inc != -1 { - return Err(Error::TnsCoefOutOfRange); - } - let order = lpc.len() - 1; - if size == 0 || order == 0 { - // Nothing to shape: an order-0 filter (`lpc == [1.0]`) is the - // identity, and a zero-length region is a no-op. Still - // bounds-check the (degenerate) walk so a bad `start` is - // rejected consistently. - if size > 0 { - walk_bounds_check(spectrum.len(), start, size, inc)?; - } - return Ok(()); - } - - walk_bounds_check(spectrum.len(), start, size, inc)?; - - // Filter-state ring: the last `order` *output* samples y(n-1) .. - // y(n-order). Index `0` is the most recent output; the ring is - // shifted by one each iteration. Seeded with zeros per §4.6.9.3. - let mut history = vec![0.0_f64; order]; - - let mut idx = start as isize; - for _ in 0..size { - let x = spectrum[idx as usize]; - // y(n) = x(n) - Σ_{k=1..order} lpc[k] * y(n-k) - let mut y = x; - for k in 1..=order { - y -= lpc[k] * history[k - 1]; - } - spectrum[idx as usize] = y; - // Shift the history ring: y becomes the new y(n-1). - for k in (1..order).rev() { - history[k] = history[k - 1]; - } - history[0] = y; - idx += inc as isize; - } - Ok(()) -} - -/// §4.6.7.4.1 TNS **analysis** filter — the all-zero (moving-average, -/// FIR) inverse of the §4.6.9.3 [`tns_ar_filter`] all-pole synthesis -/// filter, applied in place over a strided region. -/// -/// Figure 4.30 puts an additional TNS analysis filter in the LTP loop: -/// because TNS is applied to a *reconstructed* spectrum, the -/// LTP-predicted spectrum `X_est` has to be pushed through the same -/// noise-shaping the residual carries before it can be added to the -/// transmitted residual `Y_rec` (which sits in the pre-synthesis, -/// noise-shaped domain). That forward filter is the exact inverse of -/// the synthesis recurrence: where [`tns_ar_filter`] computes -/// -/// ```text -/// y(n) = x(n) - Σ_{k=1..order} lpc[k] * y(n-k) (all-pole) -/// ``` -/// -/// the analysis filter computes -/// -/// ```text -/// y(n) = x(n) + Σ_{k=1..order} lpc[k] * x(n-k) (all-zero) -/// ``` -/// -/// reading back its own *inputs* (`x`) rather than its outputs. Running -/// the analysis filter and then the synthesis filter over the same -/// region with the same `lpc` is the identity, which is the §4.6.7.4.1 -/// requirement: the analysis step in the LTP loop is undone by the -/// §4.6.9 TNS synthesis step that follows the `X_est + Y_rec` add. -/// -/// Argument and error semantics mirror [`tns_ar_filter`] exactly: the -/// filter state is seeded with zeros at every invocation, the output -/// overwrites the input in place, and `size` samples are processed -/// stepping by `inc ∈ {-1, +1}`. `lpc[0]` is the implicit `1.0`; -/// `lpc[1..=order]` are the predictor taps. An order-0 filter -/// (`lpc == [1.0]`) is the identity. -pub fn tns_ma_filter( - spectrum: &mut [f64], - start: usize, - size: usize, - inc: i32, - lpc: &[f64], -) -> Result<()> { - if lpc.is_empty() { - return Err(Error::TnsCoefOutOfRange); - } - if inc != 1 && inc != -1 { - return Err(Error::TnsCoefOutOfRange); - } - let order = lpc.len() - 1; - if size == 0 || order == 0 { - if size > 0 { - walk_bounds_check(spectrum.len(), start, size, inc)?; - } - return Ok(()); - } - - walk_bounds_check(spectrum.len(), start, size, inc)?; - - // Filter-state ring: the last `order` *input* samples x(n-1) .. - // x(n-order). Index `0` is the most recent input. Seeded with zeros, - // matching the all-pole filter's zero-initialised state so the two - // are mutual inverses over the region. - let mut history = vec![0.0_f64; order]; - - let mut idx = start as isize; - for _ in 0..size { - let x = spectrum[idx as usize]; - // y(n) = x(n) + Σ_{k=1..order} lpc[k] * x(n-k) - let mut y = x; - for k in 1..=order { - y += lpc[k] * history[k - 1]; - } - spectrum[idx as usize] = y; - // Shift the history ring: x becomes the new x(n-1). - for k in (1..order).rev() { - history[k] = history[k - 1]; - } - history[0] = x; - idx += inc as isize; - } - Ok(()) -} - -/// Bounds-check the §4.6.9.3 strided walk: `size` samples starting at -/// `start`, stepping by `inc ∈ {-1, +1}`, must all land inside a -/// buffer of `len` elements. Returns [`Error::TnsCoefOutOfRange`] -/// otherwise. -fn walk_bounds_check(len: usize, start: usize, size: usize, inc: i32) -> Result<()> { - if start >= len { - return Err(Error::TnsCoefOutOfRange); - } - // Last visited index = start + (size-1)*inc. Validate it stays in - // `0..len` without overflowing. - let span = (size - 1) as isize; - let last = start as isize + span * inc as isize; - if last < 0 || last >= len as isize { - return Err(Error::TnsCoefOutOfRange); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - // ---------- iqfac / iqfac_m ---------- - - #[test] - fn iqfac_matches_spec_formula_for_legal_widths() { - // coef_res_bits = 3 (coef_res = 0): scale = 4 - 0.5 = 3.5 - let want3 = 3.5_f64 / HALF_PI; - let want4 = 7.5_f64 / HALF_PI; - assert!((iqfac(3).unwrap() - want3).abs() < 1e-15); - assert!((iqfac(4).unwrap() - want4).abs() < 1e-15); - } - - #[test] - fn iqfac_m_matches_spec_formula_for_legal_widths() { - let want3 = 4.5_f64 / HALF_PI; - let want4 = 8.5_f64 / HALF_PI; - assert!((iqfac_m(3).unwrap() - want3).abs() < 1e-15); - assert!((iqfac_m(4).unwrap() - want4).abs() < 1e-15); - } - - #[test] - fn iqfac_rejects_widths_outside_3_to_4() { - assert!(matches!(iqfac(0), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(iqfac(2), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(iqfac(5), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(iqfac_m(0), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(iqfac_m(2), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(iqfac_m(5), Err(Error::TnsCoefOutOfRange))); - } - - #[test] - fn iqfac_m_is_always_greater_than_iqfac() { - // The +0.5 vs -0.5 offset guarantees `iqfac_m > iqfac` for - // every coef_res_bits — this is what biases the round-to-zero - // of negative reflection coefficients toward the next-larger - // magnitude (so they don't underflow toward zero). - for n in [3, 4] { - assert!(iqfac_m(n).unwrap() > iqfac(n).unwrap()); - } - } - - // ---------- sign extension ---------- - - #[test] - fn sign_extend_4bit_covers_signed_range() { - // coef_res2 = 4 ⇒ signed range -8..=7. Walk every wire pattern. - let expected: [i32; 16] = [0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1]; - for wire in 0_u32..16 { - assert_eq!( - sign_extend_coef(wire, 4).unwrap(), - expected[wire as usize], - "wire {wire:04b}", - ); - } - } - - #[test] - fn sign_extend_3bit_covers_signed_range() { - // coef_res2 = 3 ⇒ signed range -4..=3. - let expected: [i32; 8] = [0, 1, 2, 3, -4, -3, -2, -1]; - for wire in 0_u32..8 { - assert_eq!(sign_extend_coef(wire, 3).unwrap(), expected[wire as usize]); - } - } - - #[test] - fn sign_extend_2bit_covers_signed_range() { - // coef_res2 = 2 ⇒ signed range -2..=1. - let expected: [i32; 4] = [0, 1, -2, -1]; - for wire in 0_u32..4 { - assert_eq!(sign_extend_coef(wire, 2).unwrap(), expected[wire as usize]); - } - } - - #[test] - fn sign_extend_rejects_out_of_range_field_width() { - assert!(matches!( - sign_extend_coef(0, 1), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - sign_extend_coef(0, 5), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn sign_extend_rejects_wire_value_that_overflows_field() { - // 4-bit field: a wire value of 16 (0b10000) doesn't fit. - assert!(matches!( - sign_extend_coef(16, 4), - Err(Error::TnsCoefOutOfRange) - )); - // 2-bit field: 4 doesn't fit. - assert!(matches!( - sign_extend_coef(4, 2), - Err(Error::TnsCoefOutOfRange) - )); - } - - // ---------- pack_coef (encoder-side) ---------- - - #[test] - fn pack_coef_round_trips_through_sign_extend_4bit() { - for value in -8_i32..=7 { - let packed = pack_coef(value, 4).unwrap(); - assert_eq!(sign_extend_coef(packed, 4).unwrap(), value); - } - } - - #[test] - fn pack_coef_round_trips_through_sign_extend_3bit() { - for value in -4_i32..=3 { - let packed = pack_coef(value, 3).unwrap(); - assert_eq!(sign_extend_coef(packed, 3).unwrap(), value); - } - } - - #[test] - fn pack_coef_round_trips_through_sign_extend_2bit() { - for value in -2_i32..=1 { - let packed = pack_coef(value, 2).unwrap(); - assert_eq!(sign_extend_coef(packed, 2).unwrap(), value); - } - } - - #[test] - fn pack_coef_rejects_out_of_field_value() { - // 4-bit signed range is -8..=7; 8 and -9 reject. - assert!(matches!(pack_coef(8, 4), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(pack_coef(-9, 4), Err(Error::TnsCoefOutOfRange))); - // 2-bit signed range is -2..=1; 2 and -3 reject. - assert!(matches!(pack_coef(2, 2), Err(Error::TnsCoefOutOfRange))); - assert!(matches!(pack_coef(-3, 2), Err(Error::TnsCoefOutOfRange))); - } - - // ---------- tns_decode_coef ---------- - - #[test] - fn decode_zero_wire_yields_zero_parcor() { - let parcor = tns_decode_coef(4, 0, &[0, 0, 0]).unwrap(); - assert_eq!(parcor.len(), 3); - for v in parcor { - assert!(v.abs() < 1e-15); - } - } - - #[test] - fn decode_field_extrema_yield_near_unity_magnitudes() { - // coef_res_bits=4, coef_compress=0 ⇒ coef_res2=4 ⇒ signed range - // -8..=7. The extreme positive index is 7; it should decode to - // sin(7 / iqfac) = sin(7 / (7.5 / (π/2))) ≈ sin(0.4666... · π/2). - // The extreme negative index is -8 ⇒ sin(-8 / iqfac_m). - let pos = tns_decode_coef(4, 0, &[7]).unwrap()[0]; - let neg = tns_decode_coef(4, 0, &[8]).unwrap()[0]; // wire 8 = -8 after sign-extend - let want_pos = (7.0_f64 / (7.5 / HALF_PI)).sin(); - let want_neg = (-8.0_f64 / (8.5 / HALF_PI)).sin(); - assert!((pos - want_pos).abs() < 1e-15); - assert!((neg - want_neg).abs() < 1e-15); - // Both magnitudes are in [-1, 1] — PARCOR coefficient validity. - assert!(pos.abs() <= 1.0); - assert!(neg.abs() <= 1.0); - } - - #[test] - fn decode_negative_branch_uses_iqfac_m() { - // Wire value 0xF in a 4-bit field sign-extends to -1. - // Decoded value must use iqfac_m (not iqfac): sin(-1 / iqfac_m). - let got = tns_decode_coef(4, 0, &[0xF]).unwrap()[0]; - let want = (-1.0_f64 / iqfac_m(4).unwrap()).sin(); - assert!((got - want).abs() < 1e-15); - } - - #[test] - fn decode_3bit_branch_uses_coef_res_bits_3() { - // coef_res_bits = 3 always — coef_compress doesn't change the - // iqfac arithmetic (only coef_res2 changes for sign extension). - let got_long = tns_decode_coef(3, 0, &[1]).unwrap()[0]; - let want_long = (1.0_f64 / iqfac(3).unwrap()).sin(); - assert!((got_long - want_long).abs() < 1e-15); - // coef_compress = 1 ⇒ coef_res2 = 2; signed range -2..=1. - // Wire 1 ⇒ +1 after sign-extend, then sin(1 / iqfac(3)). - let got_short = tns_decode_coef(3, 1, &[1]).unwrap()[0]; - assert!((got_short - want_long).abs() < 1e-15); - } - - #[test] - fn decode_rejects_oversized_wire_value_for_compress_path() { - // coef_res_bits=4, coef_compress=1 ⇒ coef_res2=3 (signed -4..=3); - // wire 8 (0b1000) does not fit a 3-bit field. - assert!(matches!( - tns_decode_coef(4, 1, &[8]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn decode_rejects_invalid_coef_res_bits() { - assert!(matches!( - tns_decode_coef(5, 0, &[0]), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - tns_decode_coef(2, 0, &[0]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn decode_rejects_invalid_coef_compress() { - assert!(matches!( - tns_decode_coef(4, 2, &[0]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn decode_empty_input_yields_empty_output() { - let parcor = tns_decode_coef(4, 0, &[]).unwrap(); - assert!(parcor.is_empty()); - } - - // ---------- tns_encode_coef ---------- - - #[test] - fn encode_zero_parcor_yields_zero_wire() { - let wire = tns_encode_coef(4, 0, &[0.0, 0.0, 0.0]).unwrap(); - assert_eq!(wire, vec![0, 0, 0]); - } - - #[test] - fn encode_unity_parcor_saturates_to_field_max() { - // r = 1.0 ⇒ arcsin = π/2; index = round(π/2 * iqfac(4)) = - // round(π/2 * (7.5 / (π/2))) = round(7.5) = 8, clamped to 7 - // (the 4-bit signed field maximum). Sign-extends back to +7. - let wire = tns_encode_coef(4, 0, &[1.0]).unwrap(); - assert_eq!(wire, vec![7]); - // r = -1.0 ⇒ arcsin = -π/2; index = round(-π/2 * iqfac_m(4)) = - // round(-π/2 * (8.5 / (π/2))) = round(-8.5) = -9, clamped to - // -8 (the 4-bit signed field minimum). Wire pattern is - // 0b1000 = 8. - let wire_neg = tns_encode_coef(4, 0, &[-1.0]).unwrap(); - assert_eq!(wire_neg, vec![8]); - } - - #[test] - fn encode_rejects_parcor_outside_minus_one_to_plus_one() { - assert!(matches!( - tns_encode_coef(4, 0, &[1.0001]), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - tns_encode_coef(4, 0, &[-1.0001]), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - tns_encode_coef(4, 0, &[f64::NAN]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn encode_rejects_invalid_coef_res_bits() { - assert!(matches!( - tns_encode_coef(5, 0, &[0.5]), - Err(Error::TnsCoefOutOfRange) - )); - } - - // ---------- round-trip ---------- - - #[test] - fn round_trip_every_4bit_wire_value_through_decode_then_encode() { - // For every 4-bit wire input, decode to PARCOR then re-encode - // and confirm we land on the same wire pattern. This is the - // fundamental invariant that §C.6 NINT(arcsin(sin(x))) returns - // the input integer when the magnitude is in-range. - for wire in 0_u32..16 { - let parcor = tns_decode_coef(4, 0, &[wire]).unwrap(); - let back = tns_encode_coef(4, 0, &parcor).unwrap(); - assert_eq!(back, vec![wire], "wire {wire:04b} round-trip"); - } - } - - #[test] - fn round_trip_every_3bit_wire_value_through_decode_then_encode() { - for wire in 0_u32..8 { - let parcor = tns_decode_coef(3, 0, &[wire]).unwrap(); - let back = tns_encode_coef(3, 0, &parcor).unwrap(); - assert_eq!(back, vec![wire], "wire {wire:03b} round-trip"); - } - } - - #[test] - fn round_trip_with_coef_compress_for_both_res_settings() { - // coef_res_bits = 4, coef_compress = 1 ⇒ coef_res2 = 3. - // Sign-extension uses the 3-bit field but iqfac/iqfac_m use - // coef_res_bits = 4. Confirm round-trip lands on the same - // 3-bit wire pattern. - for wire in 0_u32..8 { - let parcor = tns_decode_coef(4, 1, &[wire]).unwrap(); - let back = tns_encode_coef(4, 1, &parcor).unwrap(); - assert_eq!(back, vec![wire], "coef_res=1 compress=1 wire {wire:03b}"); - } - // coef_res_bits = 3, coef_compress = 1 ⇒ coef_res2 = 2. - for wire in 0_u32..4 { - let parcor = tns_decode_coef(3, 1, &[wire]).unwrap(); - let back = tns_encode_coef(3, 1, &parcor).unwrap(); - assert_eq!(back, vec![wire], "coef_res=0 compress=1 wire {wire:02b}"); - } - } - - // ---------- lpc_step_up ---------- - - #[test] - fn step_up_zero_order_returns_unit_a() { - let a = lpc_step_up(&[]); - assert_eq!(a, vec![1.0]); - } - - #[test] - fn step_up_first_order_matches_hand_arithmetic() { - // order = 1: a[0] = 1, a[1] = k. No inner-loop iterations. - let a = lpc_step_up(&[0.5]); - assert_eq!(a, vec![1.0, 0.5]); - } - - #[test] - fn step_up_second_order_matches_hand_arithmetic() { - // order = 2 with parcor [k1, k2]: - // m=1: a = [1, k1] - // m=2: b[1] = a[1] + k2 * a[1] = k1 * (1 + k2) - // a[1] = b[1]; a[2] = k2 - // ⇒ a = [1, k1*(1 + k2), k2] - let (k1, k2) = (0.3, 0.4); - let a = lpc_step_up(&[k1, k2]); - assert_eq!(a.len(), 3); - assert!((a[0] - 1.0).abs() < 1e-15); - assert!((a[1] - k1 * (1.0 + k2)).abs() < 1e-15); - assert!((a[2] - k2).abs() < 1e-15); - } - - #[test] - fn step_up_third_order_matches_hand_arithmetic() { - // order = 3: - // m=1: a = [1, k1, 0, 0] - // m=2: a = [1, k1*(1+k2), k2, 0] - // m=3: b[1] = a[1] + k3 * a[2] = k1*(1+k2) + k3*k2 - // b[2] = a[2] + k3 * a[1] = k2 + k3*k1*(1+k2) - // a[3] = k3 - let (k1, k2, k3) = (0.2, 0.3, -0.4); - let a = lpc_step_up(&[k1, k2, k3]); - let want = [ - 1.0, - k1 * (1.0 + k2) + k3 * k2, - k2 + k3 * k1 * (1.0 + k2), - k3, - ]; - for i in 0..4 { - assert!( - (a[i] - want[i]).abs() < 1e-15, - "i={i} got {} want {}", - a[i], - want[i], - ); - } - } - - #[test] - fn step_up_a0_always_one() { - // a[0] = 1 for every PARCOR sequence — invariant of the - // step-up loop init. - for parcor in [ - vec![0.5], - vec![-0.5], - vec![0.1, -0.2], - vec![0.3, -0.4, 0.5, -0.6, 0.7, -0.8, 0.9, -0.95], - ] { - let a = lpc_step_up(&parcor); - assert_eq!(a.len(), parcor.len() + 1); - assert!((a[0] - 1.0).abs() < 1e-15); - } - } - - #[test] - fn step_up_last_coefficient_is_last_parcor() { - // The §4.6.9.3 loop's final iteration sets a[m] = tmp2[m-1] at - // m = order. So a[order] must equal parcor[order-1] for every - // order. (The intermediate a[i] entries pick up the cross - // terms.) - for parcor in [vec![0.3], vec![0.3, -0.5], vec![0.1, 0.2, 0.3, 0.4]] { - let a = lpc_step_up(&parcor); - let last_idx = parcor.len(); - assert_eq!(a[last_idx], *parcor.last().unwrap()); - } - } - - // ---------- combined wrapper ---------- - - #[test] - fn decode_to_lpc_combines_decode_and_step_up() { - let wire = [3, 5, 0xF]; // mixed positive / negative - let parcor = tns_decode_coef(4, 0, &wire).unwrap(); - let want = lpc_step_up(&parcor); - let got = tns_decode_coef_to_lpc(4, 0, &wire).unwrap(); - assert_eq!(got, want); - } - - #[test] - fn decode_to_lpc_propagates_decode_errors() { - assert!(matches!( - tns_decode_coef_to_lpc(5, 0, &[0]), - Err(Error::TnsCoefOutOfRange) - )); - } - - // ---------- tns_ar_filter ---------- - - /// Reference implementation of the §4.6.9.3 recurrence written the - /// straightforward (non-ring-buffer) way, for cross-checking the - /// production `tns_ar_filter`. Operates on a contiguous copy. - fn ref_ar_filter(x: &[f64], lpc: &[f64]) -> Vec { - let order = lpc.len() - 1; - let mut y = vec![0.0_f64; x.len()]; - for n in 0..x.len() { - let mut acc = x[n]; - for k in 1..=order { - if n >= k { - acc -= lpc[k] * y[n - k]; - } - } - y[n] = acc; - } - y - } - - #[test] - fn ar_filter_order0_is_identity() { - let mut spec = [1.0, 2.0, 3.0, 4.0]; - let before = spec; - // lpc = [1.0] ⇒ order 0. - tns_ar_filter(&mut spec, 0, 4, 1, &[1.0]).unwrap(); - assert_eq!(spec, before); - } - - #[test] - fn ar_filter_order1_matches_recurrence_upward() { - // y(n) = x(n) - lpc[1]*y(n-1). - let lpc = [1.0, 0.5]; - let x = [1.0, 0.0, 0.0, 0.0, 0.0]; - let want = ref_ar_filter(&x, &lpc); - let mut spec = x; - tns_ar_filter(&mut spec, 0, 5, 1, &lpc).unwrap(); - for (g, w) in spec.iter().zip(want.iter()) { - assert!((g - w).abs() < 1e-12, "got {g} want {w}"); - } - // Hand-check: unit impulse through y(n)+0.5 y(n-1) = x gives - // y = 1, -0.5, 0.25, -0.125, 0.0625. - let hand = [1.0, -0.5, 0.25, -0.125, 0.0625]; - for (g, h) in spec.iter().zip(hand.iter()) { - assert!((g - h).abs() < 1e-12); - } - } - - #[test] - fn ar_filter_order3_matches_reference() { - let lpc = [1.0, -0.4, 0.2, 0.1]; - let x = [0.7, -1.3, 2.1, 0.0, -0.5, 1.1, 0.9, -0.2]; - let want = ref_ar_filter(&x, &lpc); - let mut spec = x; - tns_ar_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); - for (g, w) in spec.iter().zip(want.iter()) { - assert!((g - w).abs() < 1e-12, "got {g} want {w}"); - } - } - - #[test] - fn ar_filter_downward_walks_high_to_low() { - // direction = 1 ⇒ inc = -1, start = end - 1. The §4.6.9.3 - // filter then processes the region top-to-bottom. Cross-check - // by reversing the region, filtering forward, and reversing - // back. - let lpc = [1.0, 0.3, -0.15]; - let region = [0.5, -0.2, 0.9, 1.4, -0.7]; - // Place region inside a larger buffer with sentinel padding to - // confirm only the targeted span is touched. - let mut spec = vec![100.0, 0.5, -0.2, 0.9, 1.4, -0.7, 200.0]; - let start = 5; // end-1, where end = 6 (one past last region idx) - let size = 5; - tns_ar_filter(&mut spec, start, size, -1, &lpc).unwrap(); - - // Reference: process region in reverse order (high→low). - let mut rev: Vec = region.iter().rev().copied().collect(); - let want_rev = ref_ar_filter(&rev, &lpc); - rev.copy_from_slice(&want_rev); - let want: Vec = rev.into_iter().rev().collect(); - - assert_eq!(spec[0], 100.0, "lower sentinel untouched"); - assert_eq!(spec[6], 200.0, "upper sentinel untouched"); - for (i, w) in want.iter().enumerate() { - assert!( - (spec[1 + i] - w).abs() < 1e-12, - "idx {i}: {} vs {w}", - spec[1 + i] - ); - } - } - - #[test] - fn ar_filter_only_touches_targeted_region() { - let lpc = [1.0, 0.5]; - let mut spec = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; - // Filter only indices 2..=4 (size 3, upward). - tns_ar_filter(&mut spec, 2, 3, 1, &lpc).unwrap(); - assert_eq!(spec[0], 1.0); - assert_eq!(spec[1], 2.0); - assert_eq!(spec[5], 6.0); - // Region recomputed independently. - let want = ref_ar_filter(&[3.0, 4.0, 5.0], &lpc); - for i in 0..3 { - assert!((spec[2 + i] - want[i]).abs() < 1e-12); - } - } - - #[test] - fn ar_filter_zero_size_is_noop() { - let mut spec = [1.0, 2.0, 3.0]; - let before = spec; - tns_ar_filter(&mut spec, 0, 0, 1, &[1.0, 0.5]).unwrap(); - assert_eq!(spec, before); - } - - #[test] - fn ar_filter_rejects_empty_lpc() { - let mut spec = [1.0, 2.0]; - assert!(matches!( - tns_ar_filter(&mut spec, 0, 2, 1, &[]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn ar_filter_rejects_bad_inc() { - let mut spec = [1.0, 2.0]; - assert!(matches!( - tns_ar_filter(&mut spec, 0, 2, 0, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - tns_ar_filter(&mut spec, 0, 2, 2, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn ar_filter_rejects_out_of_bounds_walk() { - let mut spec = [1.0, 2.0, 3.0]; - // start in range but size overruns the top. - assert!(matches!( - tns_ar_filter(&mut spec, 1, 5, 1, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - // downward walk underruns below 0. - assert!(matches!( - tns_ar_filter(&mut spec, 1, 3, -1, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - // start past the end. - assert!(matches!( - tns_ar_filter(&mut spec, 3, 1, 1, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - } - - #[test] - fn ar_filter_end_to_end_from_wire_coef() { - // Decode a wire TNS filter to LPC, then shape a spectrum. - // Confirms the lpc_step_up output drives tns_ar_filter without - // any glue. coef_res_bits = 4, coef_compress = 0, order 2. - let wire = [3_u32, 0xE]; // one positive, one negative reflection - let lpc = tns_decode_coef_to_lpc(4, 0, &wire).unwrap(); - assert_eq!(lpc.len(), 3); - assert_eq!(lpc[0], 1.0); - let x = [0.3, -0.9, 1.2, 0.4, -0.6, 0.1]; - let want = ref_ar_filter(&x, &lpc); - let mut spec = x; - tns_ar_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); - for (g, w) in spec.iter().zip(want.iter()) { - assert!((g - w).abs() < 1e-12); - } - } - - // ---------- tns_ma_filter (analysis / all-zero) ---------- - - /// Reference all-zero (analysis) filter for an upward, in-order - /// region: y(n) = x(n) + Σ lpc[k]·x(n-k), zero-seeded history. - fn ref_ma_filter(x: &[f64], lpc: &[f64]) -> Vec { - let order = lpc.len() - 1; - let mut y = vec![0.0; x.len()]; - for n in 0..x.len() { - let mut acc = x[n]; - for k in 1..=order { - if n >= k { - acc += lpc[k] * x[n - k]; - } - } - y[n] = acc; - } - y - } - - #[test] - fn ma_filter_order_zero_is_identity() { - let mut spec = [0.3, -0.9, 1.2, 0.4]; - let before = spec; - let n = spec.len(); - tns_ma_filter(&mut spec, 0, n, 1, &[1.0]).unwrap(); - assert_eq!(spec, before); - } - - #[test] - fn ma_filter_matches_reference_upward() { - let lpc = [1.0, 0.5, -0.25]; - let x = [0.3, -0.9, 1.2, 0.4, -0.6, 0.1]; - let want = ref_ma_filter(&x, &lpc); - let mut spec = x; - tns_ma_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); - for (g, w) in spec.iter().zip(want.iter()) { - assert!((g - w).abs() < 1e-12, "got {g} want {w}"); - } - } - - #[test] - fn ma_then_ar_is_identity() { - // §4.6.7.4.1: the analysis filter followed by the synthesis - // filter (same region, same lpc) reconstructs the input exactly. - let lpc = tns_decode_coef_to_lpc(4, 0, &[3, 0xE]).unwrap(); - let x = [0.7, -0.2, 1.1, -1.3, 0.05, 0.9, -0.4]; - let mut spec = x; - tns_ma_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); - tns_ar_filter(&mut spec, 0, x.len(), 1, &lpc).unwrap(); - for (g, w) in spec.iter().zip(x.iter()) { - assert!((g - w).abs() < 1e-12, "ma∘ar not identity: {g} vs {w}"); - } - } - - #[test] - fn ma_then_ar_is_identity_downward() { - // Same inverse relationship for the downward (direction=1) walk. - let lpc = [1.0, -0.4, 0.2]; - let x = [0.7, -0.2, 1.1, -1.3, 0.05]; - let mut spec = x; - let end = x.len(); - tns_ma_filter(&mut spec, end - 1, end, -1, &lpc).unwrap(); - tns_ar_filter(&mut spec, end - 1, end, -1, &lpc).unwrap(); - for (g, w) in spec.iter().zip(x.iter()) { - assert!((g - w).abs() < 1e-12); - } - } - - #[test] - fn ma_filter_rejects_bad_args() { - let mut spec = [1.0, 2.0, 3.0]; - assert!(matches!( - tns_ma_filter(&mut spec, 0, 1, 2, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - tns_ma_filter(&mut spec, 1, 5, 1, &[1.0, 0.5]), - Err(Error::TnsCoefOutOfRange) - )); - assert!(matches!( - tns_ma_filter(&mut spec, 0, 1, 1, &[]), - Err(Error::TnsCoefOutOfRange) - )); - } -} diff --git a/crates/vendor/oxideav-aac/src/tns_data.rs b/crates/vendor/oxideav-aac/src/tns_data.rs deleted file mode 100644 index 1c289adc..00000000 --- a/crates/vendor/oxideav-aac/src/tns_data.rs +++ /dev/null @@ -1,480 +0,0 @@ -//! `tns_data()` parser + encoder primitive — ISO/IEC 14496-3 -//! §4.4.6 / Table 4.54 (syntax) and §4.6.9 / Table 4.155 (field-size -//! switching). -//! -//! Temporal Noise Shaping is an in-MDCT prediction tool that shapes -//! the temporal envelope of quantisation noise inside each transform -//! window. The encoder emits one or more all-pole filters per -//! window, each covering a contiguous range of scalefactor bands. -//! The decoder reverses the filtering after Huffman decoding but -//! before IMDCT. `tns_data()` is the wire record of those filters. -//! It rides inside an `individual_channel_stream()` between -//! `pulse_data()` and `gain_control_data()` / `spectral_data()`, -//! gated by the dispatching `tns_data_present` flag (Tables 4.44 / -//! 4.50). -//! -//! ## Wire layout (Table 4.54) -//! -//! ```text -//! tns_data() { -//! for (w = 0; w < num_windows; w++) { -//! n_filt[w]; 1..2 bits (Table 4.155) -//! if (n_filt[w]) -//! coef_res[w]; 1 bit -//! for (filt = 0; filt < n_filt[w]; filt++) { -//! length[w][filt]; 4 or 6 bits (Table 4.155) -//! order[w][filt]; 3 or 5 bits (Table 4.155) -//! if (order[w][filt]) { -//! direction[w][filt]; 1 bit -//! coef_compress[w][filt]; 1 bit -//! for (i = 0; i < order[w][filt]; i++) -//! coef[w][filt][i]; 2..4 bits (see below) -//! } -//! } -//! } -//! } -//! ``` -//! -//! Two `window_sequence`-dependent field-width pairs control the -//! per-window dispatch (§4.6.9.2 Table 4.155): -//! -//! | name | EIGHT_SHORT (128-line) | other window sizes | -//! |-----------|-------------------------|--------------------| -//! | `n_filt` | 1 bit | 2 bits | -//! | `length` | 4 bits | 6 bits | -//! | `order` | 3 bits | 5 bits | -//! -//! Per-filter `coef[i]` width is determined by `coef_res[w]` and -//! `coef_compress[w][filt]` per §4.6.9.3 `tns_decode_coef`: -//! -//! ```text -//! coef_res_bits = coef_res[w] ? 4 : 3 -//! coef_bits = coef_res_bits - coef_compress[w][filt] -//! ∈ {2, 3, 4} -//! ``` -//! -//! `coef_res` is **only** present on the wire when at least one -//! filter is emitted for the window (`n_filt[w] > 0`); zero-filter -//! windows simply skip the bit. -//! -//! `num_windows` is supplied by the surrounding `ics_info()`: `8` for -//! `EIGHT_SHORT_SEQUENCE`, `1` for every other window sequence -//! (§4.5.2.3.4). -//! -//! ## What this module covers -//! -//! * [`TnsData::parse`] — read a Table 4.54 block from a -//! [`BitReader`], surfacing every wire field literally. Per-filter -//! `coef[]` widths are computed from the freshly-read `coef_res` -//! and `coef_compress` flags exactly as §4.6.9.3 prescribes. -//! * [`TnsData::write`] — the inverse: serialise a [`TnsData`] onto -//! a [`BitWriter`] in bit-exact Table 4.54 form. Surfaces caller- -//! side structural bugs (field overflow, length mismatch between -//! `order` and the `coef` slice, out-of-range `coef` value) as -//! [`Error::TnsDataEncodeInvalid`]. -//! -//! ## What this module does *not* cover -//! -//! * The §4.6.9.3 `tns_decode_coef` LPC reconstruction (signed-magnitude -//! conversion, `iqfac` arcsine inverse-quantisation, Levinson-style -//! conversion to LPC coefficients) is **not** performed here — it -//! needs a floating-point or fixed-point spectral context that -//! arrives with the per-AOT IMDCT back-end. -//! * The §4.6.9.3 `tns_ar_filter` all-pole filtering pass over the -//! spectrum is similarly deferred. -//! * The §4.6.9.4 `TNS_MAX_ORDER` and `TNS_MAX_BANDS` clamp tables -//! (Tables 4.156 / 4.157) are not consulted by the wire encoder -//! or parser. The parser surfaces the literal wire `order` / -//! `length` regardless of whether they exceed the AOT-and-sample- -//! rate-dependent caps; the decoder's reconstruction loop is the -//! layer that applies `min(order, TNS_MAX_ORDER)` and -//! `min(bands, TNS_MAX_BANDS, max_sfb)`. -//! * The normative constraint that `tns_data_present == 0` for the -//! ER AAC LD `gain_control_data` path (Table 4.50) is the -//! responsibility of the dispatching `individual_channel_stream()` -//! (which has not landed yet). - -use oxideav_core::bits::{BitReader, BitWriter}; - -use crate::ics_info::WindowSequence; -use crate::swb_offset::FrameFamily; -use crate::{Error, Result}; - -/// One TNS noise-shaping filter inside a single transform window. -/// -/// Fields are the literal Table 4.54 wire values. The `coef` slot -/// holds the unsigned magnitudes as transmitted (each entry occupies -/// `coef_bits` per §4.6.9.3 — `coef_res_bits − coef_compress`); -/// signed-magnitude conversion is performed by `tns_decode_coef()` -/// at decode time and is *not* applied here. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TnsFilter { - /// `length[w][filt]` — number of scalefactor bands covered by this - /// filter (4 bits on `EIGHT_SHORT_SEQUENCE`, 6 bits otherwise). - pub length: u8, - /// `order[w][filt]` — all-pole filter order (3 bits on - /// `EIGHT_SHORT_SEQUENCE`, 5 bits otherwise). When `order == 0` - /// no `direction` / `coef_compress` / `coef[]` are emitted. - pub order: u8, - /// `direction[w][filt]` — slide direction across the spectrum: - /// `false` = upward, `true` = downward. Absent on the wire when - /// `order == 0`; the [`TnsData::parse`] caller-side default is - /// `false` in that case. - pub direction: bool, - /// `coef_compress[w][filt]` — when `true` the MSB of every - /// transmitted coefficient is omitted, shrinking each `coef[i]` - /// from `coef_res_bits` to `coef_res_bits − 1`. Absent on the - /// wire when `order == 0`. - pub coef_compress: bool, - /// `coef[w][filt][i]` for `i in 0..order` — unsigned magnitudes - /// as transmitted. Length **must** equal `order`. Each entry - /// is in `0..(1 << coef_bits)` where - /// `coef_bits = (3 + coef_res as u32) − coef_compress as u32`. - pub coef: Vec, -} - -/// Per-window TNS payload. Always carries `coef_res` even when -/// `filters.is_empty()`; the [`TnsData::write`] code path omits the -/// wire bit in that case but the field is meaningful for callers -/// that round-trip a structurally identical block. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TnsWindow { - /// `coef_res[w]` — `false` selects a 3-bit `coef_res_bits`, - /// `true` selects a 4-bit `coef_res_bits` per §4.6.9.3. When - /// `filters.is_empty()` the bit is **not** transmitted; both the - /// parser and the writer treat the stored value as a don't-care - /// in that case. - pub coef_res: bool, - /// The filters for this window, in wire order. `n_filt[w]` on - /// the wire is `filters.len()` and is capped by the field width - /// (1 bit on `EIGHT_SHORT_SEQUENCE`, 2 bits otherwise → 0..=1 - /// vs 0..=3). - pub filters: Vec, -} - -/// Parsed `tns_data()` block (Table 4.54). -/// -/// `windows` always carries exactly `num_windows` entries (8 for -/// `EIGHT_SHORT_SEQUENCE`, 1 otherwise). The [`TnsData::write`] code -/// path validates this against the surrounding [`WindowSequence`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TnsData { - /// One entry per transform window. Length must match the - /// surrounding [`WindowSequence`]: 8 for `EightShort`, 1 - /// otherwise. - pub windows: Vec, -} - -/// `n_filt` field width for `EIGHT_SHORT_SEQUENCE` per Table 4.155. -pub const N_FILT_BITS_SHORT: u32 = 1; -/// `n_filt` field width for any non-`EIGHT_SHORT_SEQUENCE` per Table -/// 4.155. -pub const N_FILT_BITS_LONG: u32 = 2; -/// `length` field width for `EIGHT_SHORT_SEQUENCE` per Table 4.155. -pub const LENGTH_BITS_SHORT: u32 = 4; -/// `length` field width for any non-`EIGHT_SHORT_SEQUENCE` per Table -/// 4.155. -pub const LENGTH_BITS_LONG: u32 = 6; -/// `order` field width for `EIGHT_SHORT_SEQUENCE` per Table 4.155. -pub const ORDER_BITS_SHORT: u32 = 3; -/// `order` field width for any non-`EIGHT_SHORT_SEQUENCE` per Table -/// 4.155. -pub const ORDER_BITS_LONG: u32 = 5; -/// `coef_res` field width per Table 4.54. -pub const COEF_RES_BITS: u32 = 1; -/// `direction` field width per Table 4.54. -pub const DIRECTION_BITS: u32 = 1; -/// `coef_compress` field width per Table 4.54. -pub const COEF_COMPRESS_BITS: u32 = 1; - -/// `(n_filt_bits, length_bits, order_bits)` triple for the given -/// `window_sequence`. The selection rule is §4.6.9.2 Table 4.155 — -/// the 128-line `EIGHT_SHORT_SEQUENCE` shrinks every field by one -/// or two bits versus the other window sizes. -pub fn field_widths(seq: WindowSequence) -> (u32, u32, u32) { - if seq.is_eight_short() { - (N_FILT_BITS_SHORT, LENGTH_BITS_SHORT, ORDER_BITS_SHORT) - } else { - (N_FILT_BITS_LONG, LENGTH_BITS_LONG, ORDER_BITS_LONG) - } -} - -/// `(n_filt_bits, length_bits, order_bits)` triple for the given -/// frame family and `window_sequence`. -/// -/// For the ER AAC LD families (§4.6.17, 512/480-line long-only -/// frames) the normative ISO/IEC 14496-26 conformance bitstreams -/// transmit the *reduced* Table 4.155 column — `n_filt` in **1 bit** -/// — even though the literal table keying (window size ≠ 128) selects -/// the 2-bit column. The resolution is corpus-empirical: across all -/// 173 `er_ad*_ep0` conformance vectors, every TNS-bearing access -/// unit parses with the 1-bit width and the 2-bit reading -/// desynchronises `spectral_data()` (792 hard failures of 2 017 TNS -/// records). `length` / `order` take the rest of the same reduced -/// column (4 / 3 bits); the corpus never transmits either field -/// (`n_filt == 0` throughout), so those two widths follow the only -/// hypothesis with a consistent selection mechanism. See -/// `docs/audio/aac/er-ld-tns-divergence.md` §0 (resolution of issue -/// #292). -/// -/// Every non-LD family keeps the literal Table 4.155 dispatch of -/// [`field_widths`]. -pub fn field_widths_family(family: FrameFamily, seq: WindowSequence) -> (u32, u32, u32) { - if family.is_ld() { - (N_FILT_BITS_SHORT, LENGTH_BITS_SHORT, ORDER_BITS_SHORT) - } else { - field_widths(seq) - } -} - -/// `num_windows` for the given `window_sequence` per §4.5.2.3.4: -/// `8` for `EIGHT_SHORT_SEQUENCE`, `1` otherwise. -pub fn num_windows(seq: WindowSequence) -> usize { - if seq.is_eight_short() { - 8 - } else { - 1 - } -} - -/// Per-filter `coef_bits` width per §4.6.9.3: -/// -/// ```text -/// coef_res_bits = 3 + (coef_res ? 1 : 0) -/// coef_bits = coef_res_bits - (coef_compress ? 1 : 0) -/// ``` -/// -/// Result is in `{2, 3, 4}`. -pub fn coef_bits(coef_res: bool, coef_compress: bool) -> u32 { - let coef_res_bits = 3 + u32::from(coef_res); - coef_res_bits - u32::from(coef_compress) -} - -impl TnsData { - /// Parse a `tns_data()` from `reader`, given the surrounding - /// `window_sequence` (which selects the per-window field widths - /// and `num_windows`). - /// - /// Returns [`Error::UnexpectedEnd`] on bit-reader underflow. - /// Returns [`Error::TnsDataEncodeInvalid`] when a `coef[i]` is - /// large enough to indicate a parser/spec mismatch (which cannot - /// happen for a conforming stream — every `coef[i]` is bounded - /// by its field width — but the check guards round-trip - /// invariants for hostile inputs). - pub fn parse(reader: &mut BitReader<'_>, window_sequence: WindowSequence) -> Result { - Self::parse_family(reader, FrameFamily::Lc1024, window_sequence) - } - - /// [`TnsData::parse`] under an explicit §4.5.1.1 frame family. - /// - /// The family selects the per-window field widths via - /// [`field_widths_family`]: the ER AAC LD families read the - /// reduced 1 / 4 / 3-bit column (the corpus-resolved AOT-23 wire, - /// `docs/audio/aac/er-ld-tns-divergence.md` §0), every other - /// family follows the literal Table 4.155 `window_sequence` - /// dispatch. - pub fn parse_family( - reader: &mut BitReader<'_>, - family: FrameFamily, - window_sequence: WindowSequence, - ) -> Result { - Self::parse_widths( - reader, - field_widths_family(family, window_sequence), - window_sequence, - ) - } - - /// [`TnsData::parse`] under an **explicit** - /// `(n_filt_bits, length_bits, order_bits)` width triple. - /// - /// This is the configurability hook - /// `docs/audio/aac/er-ld-tns-divergence.md` §0.6 recommends: the - /// LD `n_filt` width is corpus-settled at 1 bit, but the LD - /// `length` / `order` widths are only *preferred* at 4 / 3 (the - /// rest of the reduced Table 4.155 column) — the ISO/IEC 14496-26 - /// corpus transmits `n_filt == 0` in every LD TNS record, so it - /// cannot discriminate 4 / 3 from 6 / 5. A caller confronted with - /// evidence for a mixed wire (e.g. 1 / 6 / 5) can drive this entry - /// point directly instead of forking [`Self::parse_family`]'s - /// dispatch. Each width must be `1..=8` - /// ([`Error::TnsDataEncodeInvalid`] otherwise — the widths are - /// caller configuration, not wire data). - pub fn parse_widths( - reader: &mut BitReader<'_>, - widths: (u32, u32, u32), - window_sequence: WindowSequence, - ) -> Result { - let (n_filt_bits, length_bits, order_bits) = widths; - if !widths_valid(widths) { - return Err(Error::TnsDataEncodeInvalid); - } - let nw = num_windows(window_sequence); - let mut windows = Vec::with_capacity(nw); - for _ in 0..nw { - let n_filt = read_u8(reader, n_filt_bits)?; - let coef_res = if n_filt > 0 { - reader.read_bit().map_err(|_| Error::UnexpectedEnd)? - } else { - false - }; - let mut filters = Vec::with_capacity(n_filt as usize); - for _ in 0..n_filt { - let length = read_u8(reader, length_bits)?; - let order = read_u8(reader, order_bits)?; - let (direction, coef_compress, coef) = if order > 0 { - let direction = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let coef_compress = reader.read_bit().map_err(|_| Error::UnexpectedEnd)?; - let bits = coef_bits(coef_res, coef_compress); - let mut coef = Vec::with_capacity(order as usize); - for _ in 0..order { - coef.push(read_u8(reader, bits)?); - } - (direction, coef_compress, coef) - } else { - (false, false, Vec::new()) - }; - filters.push(TnsFilter { - length, - order, - direction, - coef_compress, - coef, - }); - } - windows.push(TnsWindow { coef_res, filters }); - } - Ok(TnsData { windows }) - } - - /// Encode `tns_data()` onto `writer`, the inverse of - /// [`TnsData::parse`]. - /// - /// Returns [`Error::TnsDataEncodeInvalid`] if: - /// - /// * `windows.len()` differs from [`num_windows`] for - /// `window_sequence` (1 for long sequences, 8 for - /// `EIGHT_SHORT_SEQUENCE`). - /// * `filters.len()` exceeds the `n_filt` field cap - /// (`(1 << n_filt_bits) - 1`) — 1 on `EIGHT_SHORT_SEQUENCE`, - /// 3 otherwise. - /// * Any `length` exceeds the `length` field cap - /// (`(1 << length_bits) - 1`) — 15 on `EIGHT_SHORT_SEQUENCE`, - /// 63 otherwise. - /// * Any `order` exceeds the `order` field cap — 7 on - /// `EIGHT_SHORT_SEQUENCE`, 31 otherwise. - /// * A filter's `coef.len()` differs from its `order`. - /// * A filter's `coef[i]` exceeds the `(1 << coef_bits) - 1` - /// field cap (where `coef_bits = (3 + coef_res) - coef_compress`). - /// * A filter has populated `direction` / `coef_compress` / - /// `coef` slots while `order == 0` (those fields are not - /// transmitted on the wire and a non-default value would not - /// round-trip). - pub fn write(&self, writer: &mut BitWriter, window_sequence: WindowSequence) -> Result<()> { - self.write_family(writer, FrameFamily::Lc1024, window_sequence) - } - - /// [`TnsData::write`] under an explicit §4.5.1.1 frame family — - /// the bit-exact inverse of [`TnsData::parse_family`]. The LD - /// families emit the reduced 1 / 4 / 3-bit widths, capping - /// `filters.len()` at 1, `length` at 15 and `order` at 7 per - /// window. - pub fn write_family( - &self, - writer: &mut BitWriter, - family: FrameFamily, - window_sequence: WindowSequence, - ) -> Result<()> { - self.write_widths( - writer, - field_widths_family(family, window_sequence), - window_sequence, - ) - } - - /// [`TnsData::write`] under an **explicit** - /// `(n_filt_bits, length_bits, order_bits)` width triple — the - /// bit-exact inverse of [`TnsData::parse_widths`] (see there for - /// why the widths are caller-configurable). Field caps derive from - /// the given widths; each width must be `1..=8`. - pub fn write_widths( - &self, - writer: &mut BitWriter, - widths: (u32, u32, u32), - window_sequence: WindowSequence, - ) -> Result<()> { - let (n_filt_bits, length_bits, order_bits) = widths; - if !widths_valid(widths) { - return Err(Error::TnsDataEncodeInvalid); - } - let nw = num_windows(window_sequence); - if self.windows.len() != nw { - return Err(Error::TnsDataEncodeInvalid); - } - let n_filt_max = (1u32 << n_filt_bits) - 1; - let length_max = (1u32 << length_bits) - 1; - let order_max = (1u32 << order_bits) - 1; - for w in &self.windows { - if (w.filters.len() as u32) > n_filt_max { - return Err(Error::TnsDataEncodeInvalid); - } - for f in &w.filters { - if u32::from(f.length) > length_max || u32::from(f.order) > order_max { - return Err(Error::TnsDataEncodeInvalid); - } - if f.order as usize != f.coef.len() { - return Err(Error::TnsDataEncodeInvalid); - } - if f.order == 0 && (f.direction || f.coef_compress) { - // Non-default direction/compress would silently be - // dropped on the wire (the spec emits neither field - // when order == 0); reject to keep round-trip - // identity. - return Err(Error::TnsDataEncodeInvalid); - } - if f.order > 0 { - let bits = coef_bits(w.coef_res, f.coef_compress); - let coef_max = (1u32 << bits) - 1; - for c in &f.coef { - if u32::from(*c) > coef_max { - return Err(Error::TnsDataEncodeInvalid); - } - } - } - } - } - - for w in &self.windows { - writer.write_u32(w.filters.len() as u32, n_filt_bits); - if !w.filters.is_empty() { - writer.write_bit(w.coef_res); - } - for f in &w.filters { - writer.write_u32(u32::from(f.length), length_bits); - writer.write_u32(u32::from(f.order), order_bits); - if f.order > 0 { - writer.write_bit(f.direction); - writer.write_bit(f.coef_compress); - let bits = coef_bits(w.coef_res, f.coef_compress); - for c in &f.coef { - writer.write_u32(u32::from(*c), bits); - } - } - } - } - Ok(()) - } -} - -/// A caller-supplied width triple is sane when every field fits the -/// `u8`-backed record (`1..=8` bits). -fn widths_valid((n_filt_bits, length_bits, order_bits): (u32, u32, u32)) -> bool { - (1..=8).contains(&n_filt_bits) - && (1..=8).contains(&length_bits) - && (1..=8).contains(&order_bits) -} - -fn read_u8(reader: &mut BitReader<'_>, n: u32) -> Result { - debug_assert!(n <= 8); - Ok(reader.read_u32(n).map_err(|_| Error::UnexpectedEnd)? as u8) -} diff --git a/crates/vendor/oxideav-aac/src/tns_frame.rs b/crates/vendor/oxideav-aac/src/tns_frame.rs deleted file mode 100644 index 16564a37..00000000 --- a/crates/vendor/oxideav-aac/src/tns_frame.rs +++ /dev/null @@ -1,1035 +0,0 @@ -//! §4.6.9.3 `tns_decode_frame()` — per-frame Temporal Noise Shaping -//! orchestration. -//! -//! This module chains the three TNS building blocks that previous -//! rounds landed into the spec's outer per-frame loop: -//! -//! * [`crate::tns_data`] — the Table 4.54 wire parser that yields the -//! per-window `n_filt` / `coef_res` and per-filter `length` / -//! `order` / `direction` / `coef_compress` / `coef[]` fields; -//! * [`crate::tns_coef::tns_decode_coef_to_lpc`] — the §4.6.9.3 -//! `tns_decode_coef()` inverse-quantisation + conversion-to-LPC -//! step-up; -//! * [`crate::tns_coef::tns_ar_filter`] — the §4.6.9.3 -//! `tns_ar_filter()` all-pole IIR pass over a strided spectral -//! region. -//! -//! The orchestration follows the §4.6.9.3 pseudocode: -//! -//! ```text -//! tns_decode_frame() -//! { -//! for (w = 0; w < num_windows; w++) { -//! bottom = num_swb; -//! for (f = 0; f < n_filt[w]; f++) { -//! top = bottom; -//! bottom = max( top - length[w][f], 0 ); -//! tns_order = min( order[w][f], TNS_MAX_ORDER ); -//! if (!tns_order) continue; -//! tns_decode_coef( tns_order, coef_res[w]+3, -//! coef_compress[w][f], coef[w][f], lpc[] ); -//! start = swb_offset[min(bottom, TNS_MAX_BANDS, max_sfb)]; -//! end = swb_offset[min(top, TNS_MAX_BANDS, max_sfb)]; -//! if ((size = end - start) <= 0) continue; -//! if (direction[w][f]) { inc = -1; start = end - 1; } -//! else { inc = 1; } -//! tns_ar_filter( &spec[w][start], size, inc, lpc[], tns_order ); -//! } -//! } -//! } -//! ``` -//! -//! Filter regions are sliced top-down: the first transmitted filter -//! covers the topmost `length[w][0]` scalefactor bands (counting down -//! from `num_swb`), the next filter covers the `length[w][1]` bands -//! immediately below, and so on, with `bottom` clamped at band 0. The -//! band → coefficient-index mapping goes through the -//! [`crate::swb_offset`] tables, with each lookup index clamped by the -//! three-way `min(band, TNS_MAX_BANDS, max_sfb)` -//! ([`crate::tns_max::clamp_tns_band`]); the filter order is clamped -//! by `TNS_MAX_ORDER` ([`crate::tns_max::clamp_tns_order`]). Both -//! caps are object-type-dependent (Tables 4.102 / 4.103). -//! -//! Scope: the canonical 1024-line long / 8 × 128-line short frames -//! that the [`crate::swb_offset`] tables cover. The ER AAC LD -//! 480/512-line frames (Tables 4.119 / 4.120 band caps, dedicated -//! `swb_offset` tables) remain deferred until the LD reconstruction -//! path is wired, matching the standing `int_tns_decode_coef()` -//! deferral. - -use crate::ics_info::IcsInfo; -use crate::ics_info::WindowSequence; -#[cfg(test)] -use crate::swb_offset::{long_window_offsets, short_window_offsets}; -use crate::swb_offset::{long_window_offsets_family, short_window_offsets_family, FrameFamily}; -use crate::tns_coef::{tns_ar_filter, tns_decode_coef_to_lpc, tns_ma_filter}; -use crate::tns_data::{num_windows, TnsData}; -use crate::tns_max::{clamp_tns_band_family, clamp_tns_order}; -use crate::{Error, Result}; - -/// Apply §4.6.9.3 `tns_decode_frame()` to one channel's dequantised -/// spectrum, in place. -/// -/// ## Inputs -/// -/// * `spec` — the channel's full-frame coefficient buffer, windows -/// concatenated in order: `num_windows × window_len` samples, i.e. -/// `8 × 128 = 1024` for `EIGHT_SHORT_SEQUENCE` and `1 × 1024` -/// otherwise. Window `w` occupies -/// `spec[w * window_len .. (w + 1) * window_len]` (the pseudocode's -/// `spec[w][..]`). -/// * `tns` — the parsed [`TnsData`] block for this channel. Its -/// window count must match `window_sequence` (which -/// [`TnsData::parse`] guarantees when called under the same -/// sequence). -/// * `window_sequence` — the surrounding `ics_info()` window -/// sequence; selects `num_windows`, `window_len`, the -/// [`crate::swb_offset`] table, and the short/long columns of -/// Tables 4.102 / 4.103. -/// * `max_sfb` — the surrounding `ics_info()` field; third operand of -/// the §4.6.9.3 band clamp. -/// * `aot` — `audioObjectType` (Table 1.17); selects the -/// `TNS_MAX_ORDER` row and the PQF / non-PQF `TNS_MAX_BANDS` -/// columns. -/// * `fs_index` — `samplingFrequencyIndex` (Table 1.18, `0..=11`); -/// selects the `swb_offset` table and the `TNS_MAX_BANDS` row. -/// -/// ## Errors -/// -/// * [`Error::TnsFrameInvalid`] — `spec.len()` is not -/// `num_windows × window_len`; `tns.windows.len()` disagrees with -/// `window_sequence`; or a filter's `coef` vector is shorter than -/// its `TNS_MAX_ORDER`-clamped `tns_order` (a fabricated -/// structure — the wire parser always emits `coef.len() == order`). -/// * [`Error::IcsInfoUnsupportedSampleRateIndex`] — `fs_index` has no -/// `swb_offset` / `TNS_MAX_BANDS` entry (`>= 12`). -/// * [`Error::TnsCoefOutOfRange`] — a wire `coef[i]` magnitude does -/// not fit `coef_res2 = coef_res_bits − coef_compress` bits -/// (propagated from [`tns_decode_coef_to_lpc`]). -/// -/// An order-0 filter and an empty (clamped-away) region are -/// well-defined no-ops per the pseudocode's `continue` arms; a -/// `TnsData` with no filters at all leaves `spec` untouched. -pub fn tns_decode_frame( - spec: &mut [f64], - tns: &TnsData, - window_sequence: WindowSequence, - max_sfb: u8, - aot: u8, - fs_index: u8, -) -> Result<()> { - tns_frame_filter( - spec, - tns, - FrameFamily::Lc1024, - window_sequence, - max_sfb, - aot, - fs_index, - TnsFilterKind::Synthesis, - ) -} - -/// [`tns_decode_frame`] driven by a parsed [`IcsInfo`] — the frame's -/// §4.5.1.1 family, `window_sequence` and `max_sfb` all come from the -/// side info, so the 960 / LD geometries (window lengths, family SWB -/// tables and the §4.6.17.2.5 LD `TNS_MAX_BANDS`) are selected -/// consistently with the rest of the channel decode. -pub fn tns_decode_frame_ics( - spec: &mut [f64], - tns: &TnsData, - ics_info: &IcsInfo, - aot: u8, - fs_index: u8, -) -> Result<()> { - tns_frame_filter( - spec, - tns, - ics_info.family, - ics_info.window_sequence, - ics_info.max_sfb, - aot, - fs_index, - TnsFilterKind::Synthesis, - ) -} - -/// §4.6.7.4.1 TNS **analysis** pass — the same per-window / per-filter -/// region walk as [`tns_decode_frame`], but applying the all-zero -/// [`tns_ma_filter`] (the inverse of the §4.6.9.3 all-pole synthesis -/// filter) instead. -/// -/// Figure 4.30 requires this forward filter inside the LTP loop: the -/// LTP-predicted spectrum `X_est = MDCT(x_est)` must be moved into the -/// noise-shaped residual domain (the domain the transmitted `Y_rec` -/// lives in, *before* TNS synthesis) so that `X_rec = X_est + Y_rec` -/// adds like-for-like. The subsequent §4.6.9 TNS synthesis pass over -/// `X_rec` then undoes the analysis on the LTP contribution while -/// shaping the residual, exactly as the all-pole filter inverts the -/// all-zero one over a shared region. -/// -/// Inputs, scope and errors mirror [`tns_decode_frame`]; the only -/// difference is the filter polarity. When `tns` carries no filters -/// (or only order-0 / empty-region filters) the spectrum is untouched, -/// so a channel without TNS needs no analysis pass. -pub fn tns_analysis_frame( - spec: &mut [f64], - tns: &TnsData, - window_sequence: WindowSequence, - max_sfb: u8, - aot: u8, - fs_index: u8, -) -> Result<()> { - tns_frame_filter( - spec, - tns, - FrameFamily::Lc1024, - window_sequence, - max_sfb, - aot, - fs_index, - TnsFilterKind::Analysis, - ) -} - -/// [`tns_analysis_frame`] driven by a parsed [`IcsInfo`] (see -/// [`tns_decode_frame_ics`] for the family selection). -pub fn tns_analysis_frame_ics( - spec: &mut [f64], - tns: &TnsData, - ics_info: &IcsInfo, - aot: u8, - fs_index: u8, -) -> Result<()> { - tns_frame_filter( - spec, - tns, - ics_info.family, - ics_info.window_sequence, - ics_info.max_sfb, - aot, - fs_index, - TnsFilterKind::Analysis, - ) -} - -/// Which TNS filter polarity [`tns_frame_filter`] applies over each -/// region: the §4.6.9.3 all-pole synthesis filter (the normal decode -/// path) or the §4.6.7.4.1 all-zero analysis filter (the LTP loop). -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum TnsFilterKind { - /// All-pole [`tns_ar_filter`] — §4.6.9.3 decode. - Synthesis, - /// All-zero [`tns_ma_filter`] — §4.6.7.4.1 LTP-loop analysis. - Analysis, -} - -/// Shared §4.6.9.3 region walk for both TNS polarities. Identical band -/// clamping, coefficient decode and region selection; only the final -/// per-region filter call differs (`kind`). -#[allow(clippy::too_many_arguments)] -fn tns_frame_filter( - spec: &mut [f64], - tns: &TnsData, - family: FrameFamily, - window_sequence: WindowSequence, - max_sfb: u8, - aot: u8, - fs_index: u8, - kind: TnsFilterKind, -) -> Result<()> { - let windows = num_windows(window_sequence); - let (window_len, offsets) = if window_sequence.is_eight_short() { - ( - family.short_window_len().ok_or(Error::LdShortWindow)?, - short_window_offsets_family(family, fs_index)?, - ) - } else { - ( - family.frame_len(), - long_window_offsets_family(family, fs_index)?, - ) - }; - - if tns.windows.len() != windows { - return Err(Error::TnsFrameInvalid); - } - if spec.len() != windows * window_len { - return Err(Error::TnsFrameInvalid); - } - - // `num_swb + 1` entries per swb_offset table; the top band index - // (the pseudocode's initial `bottom = num_swb`) is the sentinel - // slot, so every clamped lookup below stays in bounds. - let num_swb = offsets.len() - 1; - - for (w, tns_window) in tns.windows.iter().enumerate() { - let coef_res_bits = 3 + u32::from(tns_window.coef_res); - let window_spec = &mut spec[w * window_len..(w + 1) * window_len]; - - let mut bottom = num_swb; - for filter in &tns_window.filters { - let top = bottom; - bottom = top.saturating_sub(filter.length as usize); - - let tns_order = clamp_tns_order(filter.order, aot, window_sequence, fs_index)? as usize; - if tns_order == 0 { - continue; - } - if filter.coef.len() < tns_order { - return Err(Error::TnsFrameInvalid); - } - - // tns_decode_coef( tns_order, coef_res[w]+3, - // coef_compress[w][f], coef[w][f], lpc[] ) - // — only the first `tns_order` transmitted magnitudes - // participate when the wire `order` exceeded the cap. - let coef: Vec = filter.coef[..tns_order] - .iter() - .map(|&c| u32::from(c)) - .collect(); - let lpc = - tns_decode_coef_to_lpc(coef_res_bits, u32::from(filter.coef_compress), &coef)?; - - // Band indices are at most `num_swb` (bottom/top start - // there and only decrease), so the u8 narrowing is exact: - // every standard table has num_swb <= 51. - let start_band = clamp_tns_band_family( - bottom as u8, - max_sfb, - family, - aot, - window_sequence, - fs_index, - )?; - let end_band = - clamp_tns_band_family(top as u8, max_sfb, family, aot, window_sequence, fs_index)?; - let start = offsets[start_band as usize] as usize; - let end = offsets[end_band as usize] as usize; - if end <= start { - continue; - } - let size = end - start; - - let (filter_start, inc) = if filter.direction { - (end - 1, -1) - } else { - (start, 1) - }; - match kind { - TnsFilterKind::Synthesis => { - tns_ar_filter(window_spec, filter_start, size, inc, &lpc)?; - } - TnsFilterKind::Analysis => { - tns_ma_filter(window_spec, filter_start, size, inc, &lpc)?; - } - } - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tns_data::{TnsFilter, TnsWindow}; - use crate::tns_max::{tns_max_bands, tns_max_order, AOT_AAC_LC, AOT_AAC_MAIN}; - - /// 48 kHz — long-window table has 49 SWBs (sentinel 1024), short - /// has 14 (sentinel 128). - const FS_48K: u8 = 3; - - fn ramp(len: usize) -> Vec { - (0..len).map(|i| (i % 97) as f64 * 0.25 - 12.0).collect() - } - - fn long_window(filters: Vec, coef_res: bool) -> TnsData { - TnsData { - windows: vec![TnsWindow { coef_res, filters }], - } - } - - fn no_filter_window() -> TnsWindow { - TnsWindow { - coef_res: false, - filters: vec![], - } - } - - // ===== no-op paths ===== - - #[test] - fn empty_tns_data_leaves_spectrum_untouched() { - let mut spec = ramp(1024); - let want = spec.clone(); - let tns = long_window(vec![], false); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - #[test] - fn order_zero_filter_is_a_no_op() { - let mut spec = ramp(1024); - let want = spec.clone(); - let tns = long_window( - vec![TnsFilter { - length: 49, - order: 0, - direction: false, - coef_compress: false, - coef: vec![], - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - #[test] - fn zero_length_region_is_a_no_op() { - // length = 0 → bottom == top → end == start → `continue` arm. - let mut spec = ramp(1024); - let want = spec.clone(); - let tns = long_window( - vec![TnsFilter { - length: 0, - order: 2, - direction: false, - coef_compress: false, - coef: vec![1, 2], - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - // ===== single-filter long window: composition equivalence ===== - - /// The orchestrator must produce exactly the manual composition - /// `tns_decode_coef_to_lpc` + `tns_ar_filter` over the region the - /// §4.6.9.3 band arithmetic selects. - #[test] - fn single_upward_filter_matches_manual_composition() { - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; // 49 - let length = 10_u8; - let coef: Vec = vec![1, 7, 2]; // 3-bit wire magnitudes - let order = coef.len() as u8; - - let mut spec = ramp(1024); - let mut want = spec.clone(); - - // Manual composition. max_sfb = num_swb and TNS_MAX_BANDS for - // LC long @48k >= 40, so top clamps to min(49, cap, 49) and - // bottom to min(39, cap, 49). - let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - let top = num_swb.min(cap).min(num_swb); - let bottom = (num_swb - length as usize).min(cap).min(num_swb); - let start = offsets[bottom] as usize; - let end = offsets[top] as usize; - let coef_u32: Vec = coef.iter().map(|&c| u32::from(c)).collect(); - let lpc = tns_decode_coef_to_lpc(3, 0, &coef_u32).unwrap(); - tns_ar_filter(&mut want, start, end - start, 1, &lpc).unwrap(); - - let tns = long_window( - vec![TnsFilter { - length, - order, - direction: false, - coef_compress: false, - coef, - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - // The filter genuinely changed something inside the region. - assert_ne!(spec[start..end], ramp(1024)[start..end]); - } - - #[test] - fn downward_filter_matches_manual_composition() { - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; - let length = 8_u8; - let coef: Vec = vec![3, 14, 9]; // 4-bit wire magnitudes - let order = coef.len() as u8; - - let mut spec = ramp(1024); - let mut want = spec.clone(); - - let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - let top = num_swb.min(cap); - let bottom = (num_swb - length as usize).min(cap); - let start = offsets[bottom] as usize; - let end = offsets[top] as usize; - let coef_u32: Vec = coef.iter().map(|&c| u32::from(c)).collect(); - let lpc = tns_decode_coef_to_lpc(4, 0, &coef_u32).unwrap(); - // direction = 1 → inc = -1, start = end - 1. - tns_ar_filter(&mut want, end - 1, end - start, -1, &lpc).unwrap(); - - let tns = long_window( - vec![TnsFilter { - length, - order, - direction: true, - coef_compress: false, - coef, - }], - true, // coef_res = 1 → coef_res_bits = 4 - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - // ===== region slicing ===== - - #[test] - fn filter_region_counts_down_from_top_band_and_leaves_rest_untouched() { - // LC long @48 kHz: TNS_MAX_BANDS = 40 < num_swb = 49, so the - // §4.6.9.3 three-way min clamps both region ends. length = 15 - // → bottom = 34, top = 49→40: the live region is - // swb_offset[34]..swb_offset[40]; bands 40..49 are clamped - // away entirely. - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; - let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - assert_eq!(cap, 40); - let length = 15_u8; - let bottom = num_swb - length as usize; // 34, below the cap - let start = offsets[bottom] as usize; - let end = offsets[cap] as usize; - - let mut spec = ramp(1024); - let before = spec.clone(); - let tns = long_window( - vec![TnsFilter { - length, - order: 1, - direction: false, - coef_compress: false, - coef: vec![2], - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - // Everything below swb_offset[bottom] is untouched. - assert_eq!(spec[..start], before[..start]); - // Everything above the TNS_MAX_BANDS clamp is untouched too. - assert_eq!(spec[end..], before[end..]); - // The surviving clamped region was genuinely filtered. - assert_ne!(spec[start..end], before[start..end]); - } - - #[test] - fn second_filter_covers_bands_below_the_first() { - // Two filters: f0 covers the top 14 bands, f1 the 7 bands - // below them. Verify against a manual two-pass composition. - // With TNS_MAX_BANDS = 40 < num_swb = 49 the f0 region top - // clamps to band 40 while its bottom (35) survives, and the - // f1 region (28..35) lies entirely below the cap. - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; - let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - let (len0, len1) = (14_u8, 7_u8); - - let mut spec = ramp(1024); - let mut want = spec.clone(); - - let top0 = num_swb; - let bottom0 = top0 - len0 as usize; - let lpc0 = tns_decode_coef_to_lpc(3, 0, &[4]).unwrap(); - let s0 = offsets[bottom0.min(cap)] as usize; - let e0 = offsets[top0.min(cap)] as usize; - tns_ar_filter(&mut want, s0, e0 - s0, 1, &lpc0).unwrap(); - - let top1 = bottom0; - let bottom1 = top1 - len1 as usize; - let lpc1 = tns_decode_coef_to_lpc(3, 0, &[7, 1]).unwrap(); - let s1 = offsets[bottom1.min(cap)] as usize; - let e1 = offsets[top1.min(cap)] as usize; - tns_ar_filter(&mut want, e1 - 1, e1 - s1, -1, &lpc1).unwrap(); - - let tns = long_window( - vec![ - TnsFilter { - length: len0, - order: 1, - direction: false, - coef_compress: false, - coef: vec![4], - }, - TnsFilter { - length: len1, - order: 2, - direction: true, - coef_compress: false, - coef: vec![7, 1], - }, - ], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - // The two regions are disjoint and both genuinely filtered. - assert!(s1 < e1 && e1 == s0 && s0 < e0); - } - - #[test] - fn length_overrun_saturates_bottom_at_band_zero() { - // length = 63 (max 6-bit wire value) > num_swb → bottom = 0, - // region = whole clamped spectrum. - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; - let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - - let mut spec = ramp(1024); - let mut want = spec.clone(); - let lpc = tns_decode_coef_to_lpc(3, 0, &[5]).unwrap(); - let end = offsets[num_swb.min(cap)] as usize; - tns_ar_filter(&mut want, 0, end, 1, &lpc).unwrap(); - - let tns = long_window( - vec![TnsFilter { - length: 63, - order: 1, - direction: false, - coef_compress: false, - coef: vec![5], - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - // ===== clamps ===== - - #[test] - fn max_sfb_clamps_the_filter_region_top() { - // max_sfb = 20 → end = swb_offset[20]; coefficients above it - // must stay untouched even though the filter nominally covers - // the top 30 bands. - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; - let max_sfb = 20_u8; - let end = offsets[max_sfb as usize] as usize; - - let mut spec = ramp(1024); - let before = spec.clone(); - let tns = long_window( - vec![TnsFilter { - length: 30, - order: 1, - direction: false, - coef_compress: false, - coef: vec![6], - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - max_sfb, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec[end..], before[end..]); - // bottom = 49 - 30 = 19 < max_sfb → a 1-band region survives - // the clamp and is filtered. - let start = offsets[(num_swb - 30).min(max_sfb as usize)] as usize; - assert_ne!(spec[start..end], before[start..end]); - } - - #[test] - fn fully_clamped_region_is_a_no_op() { - // bottom = 49 - 5 = 44 > max_sfb = 10 → both ends clamp to - // swb_offset[10] → size = 0 → continue. - let mut spec = ramp(1024); - let want = spec.clone(); - let tns = long_window( - vec![TnsFilter { - length: 5, - order: 1, - direction: false, - coef_compress: false, - coef: vec![3], - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 10, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - #[test] - fn wire_order_is_clamped_by_tns_max_order() { - // AOT LC long → TNS_MAX_ORDER = 12. A wire order of 15 must - // use only the first 12 transmitted magnitudes. - let cap = tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - assert_eq!(cap, 12); - let coef: Vec = (0..15).map(|i| (i % 8) as u8).collect(); - - let offsets = long_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; - let band_cap = - tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, FS_48K).unwrap() as usize; - let length = 12_u8; - let bottom = num_swb - length as usize; - let start = offsets[bottom.min(band_cap)] as usize; - let end = offsets[num_swb.min(band_cap)] as usize; - - let mut spec = ramp(1024); - let mut want = spec.clone(); - let coef_u32: Vec = coef[..cap].iter().map(|&c| u32::from(c)).collect(); - let lpc = tns_decode_coef_to_lpc(3, 0, &coef_u32).unwrap(); - assert_eq!(lpc.len(), cap + 1); - tns_ar_filter(&mut want, start, end - start, 1, &lpc).unwrap(); - - let tns = long_window( - vec![TnsFilter { - length, - order: 15, - direction: false, - coef_compress: false, - coef, - }], - false, - ); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, want); - } - - #[test] - fn aac_main_long_window_allows_order_up_to_20() { - // Same wire order 15, but AOT Main (TNS_MAX_ORDER = 20 long): - // all 15 magnitudes participate, so the output differs from - // the LC-clamped run. - let coef: Vec = (0..15).map(|i| ((i * 3) % 8) as u8).collect(); - let mk = |aot: u8| { - let mut spec = ramp(1024); - let tns = long_window( - vec![TnsFilter { - length: 12, - order: 15, - direction: false, - coef_compress: false, - coef: coef.clone(), - }], - false, - ); - tns_decode_frame(&mut spec, &tns, WindowSequence::OnlyLong, 49, aot, FS_48K).unwrap(); - spec - }; - assert_ne!(mk(AOT_AAC_MAIN), mk(AOT_AAC_LC)); - } - - // ===== short windows ===== - - #[test] - fn short_sequence_filters_only_the_targeted_window() { - // 8 × 128 frame; a single filter on window 3 must leave the - // other 7 windows byte-identical. - let offsets = short_window_offsets(FS_48K).unwrap(); - let num_swb = offsets.len() - 1; // 14 - let mut windows: Vec = (0..8).map(|_| no_filter_window()).collect(); - windows[3] = TnsWindow { - coef_res: false, - filters: vec![TnsFilter { - length: num_swb as u8, - order: 2, - direction: false, - coef_compress: false, - coef: vec![1, 6], - }], - }; - let tns = TnsData { windows }; - - let mut spec = ramp(1024); - let before = spec.clone(); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::EightShort, - num_swb as u8, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec[..3 * 128], before[..3 * 128]); - assert_eq!(spec[4 * 128..], before[4 * 128..]); - assert_ne!(spec[3 * 128..4 * 128], before[3 * 128..4 * 128]); - - // And window 3 matches the manual composition on its slice. - let cap = tns_max_bands(AOT_AAC_LC, WindowSequence::EightShort, FS_48K).unwrap() as usize; - let end = offsets[num_swb.min(cap)] as usize; - let lpc = tns_decode_coef_to_lpc(3, 0, &[1, 6]).unwrap(); - let mut want_w3 = before[3 * 128..4 * 128].to_vec(); - tns_ar_filter(&mut want_w3, 0, end, 1, &lpc).unwrap(); - assert_eq!(spec[3 * 128..4 * 128], want_w3); - } - - // ===== validation ===== - - #[test] - fn rejects_spectrum_length_mismatch() { - let mut spec = ramp(512); - let tns = long_window(vec![], false); - assert!(matches!( - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K - ), - Err(Error::TnsFrameInvalid) - )); - } - - #[test] - fn rejects_window_count_mismatch() { - // 1 TnsWindow under EIGHT_SHORT_SEQUENCE (needs 8). - let mut spec = ramp(1024); - let tns = long_window(vec![], false); - assert!(matches!( - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::EightShort, - 14, - AOT_AAC_LC, - FS_48K - ), - Err(Error::TnsFrameInvalid) - )); - } - - #[test] - fn rejects_coef_shorter_than_clamped_order() { - let mut spec = ramp(1024); - let tns = long_window( - vec![TnsFilter { - length: 10, - order: 3, - direction: false, - coef_compress: false, - coef: vec![1], // < clamped order 3 - }], - false, - ); - assert!(matches!( - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K - ), - Err(Error::TnsFrameInvalid) - )); - } - - #[test] - fn rejects_unsupported_fs_index() { - let mut spec = ramp(1024); - let tns = long_window(vec![], false); - assert!(matches!( - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - 12 - ), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - } - - #[test] - fn propagates_coef_out_of_range_from_decode() { - // coef_compress = 1 with coef_res = 0 → coef_res2 = 2 bits; - // a magnitude of 4 overflows the field. - let mut spec = ramp(1024); - let tns = long_window( - vec![TnsFilter { - length: 10, - order: 1, - direction: false, - coef_compress: true, - coef: vec![4], - }], - false, - ); - assert!(matches!( - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K - ), - Err(Error::TnsCoefOutOfRange) - )); - } - - // ===== §4.6.7.4.1 analysis pass ===== - - #[test] - fn analysis_then_synthesis_is_identity() { - // The LTP-loop analysis filter (all-zero) followed by the §4.6.9 - // synthesis filter (all-pole), over the same frame, reconstructs - // the spectrum exactly — the §4.6.7.4.1 invariant that lets the - // single TNS synthesis pass after the LTP add undo the analysis - // on X_est while shaping the residual. - let tns = long_window( - vec![ - TnsFilter { - length: 12, - order: 3, - direction: false, - coef_compress: false, - coef: vec![1, 7, 2], - }, - TnsFilter { - length: 8, - order: 2, - direction: true, - coef_compress: false, - coef: vec![6, 3], - }, - ], - false, - ); - let original = ramp(1024); - let mut spec = original.clone(); - tns_analysis_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - // The analysis pass actually changed the spectrum. - assert_ne!(spec, original); - tns_decode_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - for (g, w) in spec.iter().zip(original.iter()) { - assert!((g - w).abs() < 1e-9, "analysis∘synthesis drift: {g} vs {w}"); - } - } - - #[test] - fn analysis_no_filters_is_noop() { - let tns = TnsData { - windows: vec![no_filter_window()], - }; - let original = ramp(1024); - let mut spec = original.clone(); - tns_analysis_frame( - &mut spec, - &tns, - WindowSequence::OnlyLong, - 49, - AOT_AAC_LC, - FS_48K, - ) - .unwrap(); - assert_eq!(spec, original); - } -} diff --git a/crates/vendor/oxideav-aac/src/tns_max.rs b/crates/vendor/oxideav-aac/src/tns_max.rs deleted file mode 100644 index db07fcc7..00000000 --- a/crates/vendor/oxideav-aac/src/tns_max.rs +++ /dev/null @@ -1,750 +0,0 @@ -//! Maximum TNS filter order and bandwidth lookup tables — -//! ISO/IEC 14496-3 §4.6.9.4 Tables 4.102 / 4.103 (general AAC) -//! and §4.6.17.2.5 Tables 4.119 / 4.120 (AAC LD). -//! -//! ## What this module covers -//! -//! TNS (Temporal Noise Shaping) places caps on two per-filter wire -//! quantities that the decoder must clamp during reconstruction -//! (the dispatching `tns_data()` parser, [`crate::tns_data`], -//! surfaces wire values *literally* — clamping happens here): -//! -//! * `TNS_MAX_ORDER` (Table 4.102) — the upper bound for -//! `order[w][filt]` as a function of audio-object type, window -//! sequence, and whether the surrounding stream's sampling rate -//! exceeds 32 kHz. -//! * `TNS_MAX_BANDS` (Table 4.103) — the upper bound for the -//! `bottom` and `top` band indices a TNS filter touches, as a -//! function of audio-object type and `samplingFrequencyIndex`. -//! Two AOT families dispatch differently: AOT 3 (AAC SSR) uses the -//! polyphase-quadrature-filterbank columns; every other GA AOT -//! uses the non-PQF columns. -//! * `TNS_MAX_BANDS` for AAC LD (Tables 4.119 / 4.120) — a separate -//! pair of tables keyed by the AAC LD frame size (480 vs 512 -//! samples) and sampling rate, used by AOT 23 (ER AAC LD). -//! -//! ## How the spec applies these caps -//! -//! Per §4.6.9.3 the TNS reconstruction loop clamps the wire `order` -//! and the per-filter band range with: -//! -//! ```text -//! tns_order = min(order[w][f], TNS_MAX_ORDER); -//! start = swb_offset[min(bottom, TNS_MAX_BANDS, max_sfb)]; -//! end = swb_offset[min(top, TNS_MAX_BANDS, max_sfb)]; -//! ``` -//! -//! [`tns_max_order`] and [`tns_max_bands`] return those caps; the -//! [`clamp_tns_order`] / [`clamp_tns_band`] helpers fold the -//! `min` chain into one call so the eventual reconstruction layer -//! consumes them without re-deriving the dispatch from the AOT. -//! -//! ## AOT dispatch -//! -//! The Table 4.102 row map: -//! -//! | AOT | name | row | -//! |-----------|-----------------------|--------------------| -//! | 1 | AAC Main | first row | -//! | 2 | AAC LC | second row | -//! | 3 | AAC SSR | third row | -//! | 4, 17, 19, 20, 21, 22, 23 | other GA + ER variants using TNS | fourth row ("other AOT using TNS") | -//! -//! AOT 6 (AAC Scalable) and AOT 7 (TwinVQ) are GA dispatch targets -//! per [`crate::asc::GA_AOTS`] but do not use the AAC TNS surface -//! verbatim — AOT 6 wraps an inner AAC layer (which picks its own -//! row), and AOT 7 is a different frequency-domain codec entirely. -//! The accessor surfaces them as the "other" row when invoked, since -//! the field-width dispatch in [`crate::tns_data`] does not gate on -//! AOT in any case. -//! -//! The Table 4.103 column map: -//! -//! | AOT | columns used | -//! |------|---------------------------------------| -//! | 1, 2, 4, 6, 7, 17, 19, 20, 21, 22 | columns 1 (long) / 2 (short) — "without PQF filterbank" | -//! | 3 | columns 3 (long) / 4 (short) — "with PQF filterbank" | -//! -//! AOT 23 (ER AAC LD) does **not** use Table 4.103 at all; its -//! `TNS_MAX_BANDS` cap comes from the §4.6.17.2.5 LD-specific tables -//! [`TNS_MAX_BANDS_LD_480`] / [`TNS_MAX_BANDS_LD_512`] keyed by the -//! AAC LD frame size (480 vs 512 samples). The crate's frame-size -//! tracking is the responsibility of the dispatching -//! `individual_channel_stream()` layer (not landed yet); the -//! accessor here exposes both tables as a stand-alone surface so -//! the eventual LD reconstruction loop can pick the right one. -//! -//! ## What this module does *not* cover -//! -//! * No wire-format I/O. The clamps are decoder-side reconstruction -//! constraints; the literal `length` / `order` values are still -//! written and read by [`crate::tns_data`] without clamping. -//! * No actual TNS LPC reconstruction. That belongs in the -//! per-AOT IMDCT back-end (not yet present in this crate). -//! * No ER AAC ELD (AOT 39) TNS cap. ELD uses its own MDCT length -//! (480 / 512 like AAC LD) and an ELD-specific reconstruction -//! path; the spec subclause for that cap lives in §4.6.20 and is -//! deferred until ELD-specific machinery lands. -//! * No xHE-AAC / USAC (AOT 42) caps. USAC's TNS is governed by -//! ISO/IEC 23003-3 which is out of scope for this crate. - -use crate::ics_info::WindowSequence; -use crate::{Error, Result}; - -/// AOT 1 — AAC Main. -pub const AOT_AAC_MAIN: u8 = 1; -/// AOT 2 — AAC LC (Low Complexity). -pub const AOT_AAC_LC: u8 = 2; -/// AOT 3 — AAC SSR (Scalable Sampling Rate). Uses the PQF-filterbank -/// columns of Table 4.103. -pub const AOT_AAC_SSR: u8 = 3; -/// AOT 4 — AAC LTP (Long-Term Prediction). -pub const AOT_AAC_LTP: u8 = 4; -/// AOT 23 — ER AAC LD (Low Delay). Uses the §4.6.17.2.5 LD-specific -/// `TNS_MAX_BANDS` tables, not Table 4.103. -pub const AOT_ER_AAC_LD: u8 = 23; - -/// Sample-rate index threshold for the "short window / long window -/// >32 kHz / long window ≤32 kHz" partition in Table 4.102. -/// -/// `samplingFrequencyIndex` 0..=4 cover 96000 / 88200 / 64000 / -/// 48000 / 44100 Hz — all > 32 kHz. Index 5 is exactly 32 kHz which -/// the table's `<= 32kHz` column also covers. Indices 6..=11 cover -/// 24000 / 22050 / 16000 / 12000 / 11025 / 8000 Hz — all ≤ 32 kHz. -/// Index 12 (7350 Hz) is also ≤ 32 kHz. -const FS_INDEX_FIRST_LE_32K: u8 = 5; - -/// `TNS_MAX_BANDS` lookup for AOTs that use the "without PQF -/// filterbank" columns of Table 4.103 with **long** windows. Indexed -/// by `samplingFrequencyIndex` 0..=11 — slot 12 (7350 Hz) is not -/// covered by the table. -const TNS_MAX_BANDS_LONG_NON_PQF: [u8; 12] = [31, 31, 34, 40, 42, 51, 46, 46, 42, 42, 42, 39]; - -/// `TNS_MAX_BANDS` lookup for AOTs that use the "without PQF -/// filterbank" columns of Table 4.103 with **short** windows. -const TNS_MAX_BANDS_SHORT_NON_PQF: [u8; 12] = [9, 9, 10, 14, 14, 14, 14, 14, 14, 14, 14, 14]; - -/// `TNS_MAX_BANDS` lookup for AOT 3 (AAC SSR) — the "with PQF -/// filterbank" columns of Table 4.103 — with **long** windows. -const TNS_MAX_BANDS_LONG_PQF: [u8; 12] = [28, 28, 27, 26, 26, 26, 29, 29, 23, 23, 23, 19]; - -/// `TNS_MAX_BANDS` lookup for AOT 3 (AAC SSR) — the "with PQF -/// filterbank" columns of Table 4.103 — with **short** windows. -const TNS_MAX_BANDS_SHORT_PQF: [u8; 12] = [7, 7, 7, 6, 6, 6, 7, 7, 8, 8, 8, 7]; - -/// `TNS_MAX_BANDS` for the AAC LD coder when the frame is 480 -/// samples per ISO/IEC 14496-3 Table 4.119. Indexed by -/// `samplingFrequencyIndex` 0..=11; entries marked `None` mean the -/// rate is not covered by the table (Table 4.119 only specifies -/// 48000, 44100, 32000, 24000, 22050 Hz — fs indices 3, 4, 5, 6, 7). -pub const TNS_MAX_BANDS_LD_480: [Option; 12] = [ - None, // 0 = 96000 - None, // 1 = 88200 - None, // 2 = 64000 - Some(31), // 3 = 48000 - Some(32), // 4 = 44100 - Some(37), // 5 = 32000 - Some(30), // 6 = 24000 - Some(30), // 7 = 22050 - None, // 8 = 16000 - None, // 9 = 12000 - None, // 10 = 11025 - None, // 11 = 8000 -]; - -/// `TNS_MAX_BANDS` for the AAC LD coder when the frame is 512 -/// samples per ISO/IEC 14496-3 Table 4.120. Indexed by -/// `samplingFrequencyIndex` 0..=11. -pub const TNS_MAX_BANDS_LD_512: [Option; 12] = [ - None, // 0 = 96000 - None, // 1 = 88200 - None, // 2 = 64000 - Some(31), // 3 = 48000 - Some(32), // 4 = 44100 - Some(37), // 5 = 32000 - Some(31), // 6 = 24000 - Some(31), // 7 = 22050 - None, // 8 = 16000 - None, // 9 = 12000 - None, // 10 = 11025 - None, // 11 = 8000 -]; - -/// Look up `TNS_MAX_ORDER` per ISO/IEC 14496-3 Table 4.102. -/// -/// `aot` is the `audioObjectType` value driving the stream -/// (1 = Main, 2 = LC, 3 = SSR; all other AOTs fall into the -/// "other AOT using TNS" row of the table). `window_sequence` is -/// the per-frame `ics_info()` value; `EIGHT_SHORT_SEQUENCE` -/// dispatches the `short windows` column, every other sequence -/// dispatches one of the two `long windows` columns. `fs_index` is -/// `samplingFrequencyIndex` (Table 1.18) and partitions the long- -/// window dispatch between `> 32 kHz` (fs 0..=4) and `<= 32 kHz` -/// (fs 5..=12) per the Table 4.102 header. -/// -/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when -/// `fs_index >= 13`. -pub fn tns_max_order(aot: u8, window_sequence: WindowSequence, fs_index: u8) -> Result { - if fs_index >= 13 { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - - if window_sequence.is_eight_short() { - // Every AOT row collapses to 7 for short windows. - return Ok(7); - } - - let above_32k = fs_index < FS_INDEX_FIRST_LE_32K; - Ok(match aot { - AOT_AAC_MAIN => 20, - AOT_AAC_LC => 12, - AOT_AAC_SSR => 12, - _ => { - if above_32k { - 20 - } else { - 12 - } - } - }) -} - -/// Look up `TNS_MAX_BANDS` per ISO/IEC 14496-3 Table 4.103. -/// -/// `aot` selects the table column pair: AOT 3 (AAC SSR) uses the -/// "with PQF filterbank" columns; every other AOT uses the -/// "without PQF filterbank" columns. `window_sequence` distinguishes -/// the long-window column (every sequence except -/// `EIGHT_SHORT_SEQUENCE`) from the short-window column. -/// -/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when -/// `fs_index >= 12` (Table 4.103 does not cover fs 12 = 7350 Hz). -/// -/// For AOT 23 (ER AAC LD) this accessor returns the non-PQF Table -/// 4.103 entry as a syntactic fallback; callers in an LD stream -/// should use [`tns_max_bands_ld_480`] or [`tns_max_bands_ld_512`] -/// directly per the LD frame size in [`crate::asc`]. -pub fn tns_max_bands(aot: u8, window_sequence: WindowSequence, fs_index: u8) -> Result { - let idx = fs_index as usize; - if idx >= TNS_MAX_BANDS_LONG_NON_PQF.len() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - - let table = match (aot, window_sequence.is_eight_short()) { - (AOT_AAC_SSR, false) => &TNS_MAX_BANDS_LONG_PQF, - (AOT_AAC_SSR, true) => &TNS_MAX_BANDS_SHORT_PQF, - (_, false) => &TNS_MAX_BANDS_LONG_NON_PQF, - (_, true) => &TNS_MAX_BANDS_SHORT_NON_PQF, - }; - Ok(table[idx]) -} - -/// [`tns_max_bands`] under an explicit §4.5.1.1 frame-length family. -/// -/// * `Lc1024` / `Lc960` read Table 4.157 (its values are per sampling -/// rate, not per frame length; the §4.6.9.3 three-way `min` with -/// `max_sfb` keeps any 960-family band-count difference in bounds). -/// * `Ld512` / `Ld480` read the §4.6.17.2.5 LD tables (Tables 4.173 / -/// 4.172), with the §4.5.1.1 nearest-defined-table rule for the -/// rates those tables omit: 96 / 88.2 / 64 kHz resolve to the -/// 48 kHz entry, 16 kHz and below to the 22.05 kHz entry. -pub fn tns_max_bands_family( - family: crate::swb_offset::FrameFamily, - aot: u8, - window_sequence: WindowSequence, - fs_index: u8, -) -> Result { - use crate::swb_offset::FrameFamily; - match family { - FrameFamily::Lc1024 | FrameFamily::Lc960 => tns_max_bands(aot, window_sequence, fs_index), - FrameFamily::Ld512 | FrameFamily::Ld480 => { - if window_sequence.is_eight_short() { - return Err(Error::LdShortWindow); - } - // §4.5.1.1 nearest-defined-table rule (the LD tables only - // cover fs 3..=7). - let slot = match fs_index { - 0..=3 => 3, - 4..=7 => fs_index, - 8..=11 => 7, - other => return Err(Error::IcsInfoUnsupportedSampleRateIndex(other)), - }; - if family == FrameFamily::Ld512 { - tns_max_bands_ld_512(slot) - } else { - tns_max_bands_ld_480(slot) - } - } - } -} - -/// Look up `TNS_MAX_BANDS` for an AAC LD stream with a 480-sample -/// frame, per ISO/IEC 14496-3 Table 4.119. -/// -/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when -/// `fs_index >= 12`, and the same error when `fs_index` lies in the -/// table's covered range (0..=11) but the entry is `None` (i.e. the -/// sampling rate is not one of the five LD rates 48 / 44.1 / 32 / 24 / -/// 22.05 kHz). -pub fn tns_max_bands_ld_480(fs_index: u8) -> Result { - let idx = fs_index as usize; - if idx >= TNS_MAX_BANDS_LD_480.len() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - TNS_MAX_BANDS_LD_480[idx].ok_or(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)) -} - -/// Look up `TNS_MAX_BANDS` for an AAC LD stream with a 512-sample -/// frame, per ISO/IEC 14496-3 Table 4.120. -/// -/// Returns [`Error::IcsInfoUnsupportedSampleRateIndex`] when -/// `fs_index >= 12`, and the same error when the table entry is -/// `None` (Table 4.120 only covers fs indices 3, 4, 5, 6, 7). -pub fn tns_max_bands_ld_512(fs_index: u8) -> Result { - let idx = fs_index as usize; - if idx >= TNS_MAX_BANDS_LD_512.len() { - return Err(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)); - } - TNS_MAX_BANDS_LD_512[idx].ok_or(Error::IcsInfoUnsupportedSampleRateIndex(fs_index)) -} - -/// Clamp a raw `order[w][filt]` wire value by `TNS_MAX_ORDER` per -/// §4.6.9.3: -/// -/// ```text -/// tns_order = min(order[w][f], TNS_MAX_ORDER); -/// ``` -/// -/// Returns the clamped order, or -/// [`Error::IcsInfoUnsupportedSampleRateIndex`] when the cap lookup -/// rejects `fs_index`. -pub fn clamp_tns_order( - order: u8, - aot: u8, - window_sequence: WindowSequence, - fs_index: u8, -) -> Result { - let cap = tns_max_order(aot, window_sequence, fs_index)?; - Ok(order.min(cap)) -} - -/// Clamp a TNS filter band-index (the `bottom` or `top` operand of -/// the swb_offset lookup) by `min(band, TNS_MAX_BANDS, max_sfb)` per -/// §4.6.9.3: -/// -/// ```text -/// start = swb_offset[min(bottom, TNS_MAX_BANDS, max_sfb)]; -/// end = swb_offset[min(top, TNS_MAX_BANDS, max_sfb)]; -/// ``` -/// -/// `max_sfb` is the surrounding `ics_info()` field. Returns the -/// three-way `min`. Errors mirror [`tns_max_bands`]. -pub fn clamp_tns_band( - band: u8, - max_sfb: u8, - aot: u8, - window_sequence: WindowSequence, - fs_index: u8, -) -> Result { - let cap = tns_max_bands(aot, window_sequence, fs_index)?; - Ok(band.min(cap).min(max_sfb)) -} - -/// [`clamp_tns_band`] under an explicit §4.5.1.1 frame-length family -/// (the `TNS_MAX_BANDS` operand comes from [`tns_max_bands_family`]). -pub fn clamp_tns_band_family( - band: u8, - max_sfb: u8, - family: crate::swb_offset::FrameFamily, - aot: u8, - window_sequence: WindowSequence, - fs_index: u8, -) -> Result { - let cap = tns_max_bands_family(family, aot, window_sequence, fs_index)?; - Ok(band.min(cap).min(max_sfb)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ics_info::WindowSequence; - - // ===== Table 4.102 — TNS_MAX_ORDER ===== - - #[test] - fn order_short_window_is_7_for_every_aot() { - // Every row of Table 4.102 collapses to 7 in the short-window - // column. Cover the four AOTs the table calls out by name - // plus a representative ER AOT (17 = ER AAC LC). - for aot in [AOT_AAC_MAIN, AOT_AAC_LC, AOT_AAC_SSR, AOT_AAC_LTP, 17] { - for fs in 0..=12_u8 { - assert_eq!( - tns_max_order(aot, WindowSequence::EightShort, fs).unwrap(), - 7, - "AOT {aot} fs {fs} short windows", - ); - } - } - } - - #[test] - fn order_aac_main_long_window_is_20_for_all_rates() { - for fs in 0..=12_u8 { - for ws in [ - WindowSequence::OnlyLong, - WindowSequence::LongStart, - WindowSequence::LongStop, - ] { - assert_eq!(tns_max_order(AOT_AAC_MAIN, ws, fs).unwrap(), 20); - } - } - } - - #[test] - fn order_aac_lc_long_window_is_12_for_all_rates() { - for fs in 0..=12_u8 { - assert_eq!( - tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, fs).unwrap(), - 12, - ); - } - } - - #[test] - fn order_aac_ssr_long_window_is_12_for_all_rates() { - for fs in 0..=12_u8 { - assert_eq!( - tns_max_order(AOT_AAC_SSR, WindowSequence::OnlyLong, fs).unwrap(), - 12, - ); - } - } - - #[test] - fn order_other_aot_long_window_splits_at_32k_threshold() { - // "other AOT using TNS": > 32 kHz → 20, ≤ 32 kHz → 12. - // fs indices 0..=4 (96/88.2/64/48/44.1 kHz) take the high - // column; 5..=12 (32 / 24 / 22.05 / 16 / 12 / 11.025 / 8 / - // 7.35 kHz) take the low column. - for aot in [AOT_AAC_LTP, 17, 19, 20, 21, 22, 23] { - for fs in 0..=4_u8 { - assert_eq!( - tns_max_order(aot, WindowSequence::OnlyLong, fs).unwrap(), - 20, - "AOT {aot} fs {fs} long > 32 kHz", - ); - } - for fs in 5..=12_u8 { - assert_eq!( - tns_max_order(aot, WindowSequence::OnlyLong, fs).unwrap(), - 12, - "AOT {aot} fs {fs} long <= 32 kHz", - ); - } - } - } - - #[test] - fn order_rejects_out_of_range_fs_index() { - assert!(matches!( - tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, 13), - Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) - )); - assert!(matches!( - tns_max_order(AOT_AAC_LC, WindowSequence::OnlyLong, 15), - Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) - )); - } - - // ===== Table 4.103 — TNS_MAX_BANDS ===== - - #[test] - fn bands_long_non_pqf_matches_table_row_by_row() { - // Each row of Table 4.103, column 1 ("without PQF filterbank, - // long windows"), per the Table 1.18 fs-index ordering. - let expected: [(u8, u8); 12] = [ - (0, 31), // 96000 - (1, 31), // 88200 - (2, 34), // 64000 - (3, 40), // 48000 - (4, 42), // 44100 - (5, 51), // 32000 - (6, 46), // 24000 - (7, 46), // 22050 - (8, 42), // 16000 - (9, 42), // 12000 - (10, 42), // 11025 - (11, 39), // 8000 - ]; - for (fs, expected_bands) in expected { - assert_eq!( - tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, fs).unwrap(), - expected_bands, - "fs {fs} long non-PQF", - ); - } - } - - #[test] - fn bands_short_non_pqf_matches_table_row_by_row() { - let expected: [(u8, u8); 12] = [ - (0, 9), - (1, 9), - (2, 10), - (3, 14), - (4, 14), - (5, 14), - (6, 14), - (7, 14), - (8, 14), - (9, 14), - (10, 14), - (11, 14), - ]; - for (fs, expected_bands) in expected { - assert_eq!( - tns_max_bands(AOT_AAC_LC, WindowSequence::EightShort, fs).unwrap(), - expected_bands, - "fs {fs} short non-PQF", - ); - } - } - - #[test] - fn bands_long_pqf_aac_ssr_matches_table_row_by_row() { - let expected: [(u8, u8); 12] = [ - (0, 28), - (1, 28), - (2, 27), - (3, 26), - (4, 26), - (5, 26), - (6, 29), - (7, 29), - (8, 23), - (9, 23), - (10, 23), - (11, 19), - ]; - for (fs, expected_bands) in expected { - assert_eq!( - tns_max_bands(AOT_AAC_SSR, WindowSequence::OnlyLong, fs).unwrap(), - expected_bands, - "fs {fs} long PQF", - ); - } - } - - #[test] - fn bands_short_pqf_aac_ssr_matches_table_row_by_row() { - let expected: [(u8, u8); 12] = [ - (0, 7), - (1, 7), - (2, 7), - (3, 6), - (4, 6), - (5, 6), - (6, 7), - (7, 7), - (8, 8), - (9, 8), - (10, 8), - (11, 7), - ]; - for (fs, expected_bands) in expected { - assert_eq!( - tns_max_bands(AOT_AAC_SSR, WindowSequence::EightShort, fs).unwrap(), - expected_bands, - "fs {fs} short PQF", - ); - } - } - - #[test] - fn bands_dispatches_long_start_and_stop_to_long_column() { - // §4.6.9.4 contrast is short vs long; LongStart and LongStop - // are long-window sequences (the analysis transform produces a - // 1024-line spectrum just like OnlyLong), so they must use the - // long-windows column. - for ws in [WindowSequence::LongStart, WindowSequence::LongStop] { - assert_eq!(tns_max_bands(AOT_AAC_LC, ws, 4).unwrap(), 42); - assert_eq!(tns_max_bands(AOT_AAC_SSR, ws, 4).unwrap(), 26); - } - } - - #[test] - fn bands_rejects_fs_12_and_above() { - // Table 4.103 does not list 7350 Hz (fs 12) as a row. - assert!(matches!( - tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, 12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - assert!(matches!( - tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, 13), - Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) - )); - assert!(matches!( - tns_max_bands(AOT_AAC_LC, WindowSequence::OnlyLong, 15), - Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) - )); - } - - #[test] - fn bands_aot_other_treats_as_non_pqf() { - // AOT 17 / 19 / 20 / 21 / 22 / 23 (ER variants) are *not* - // SSR, so they take the non-PQF columns identical to AOT 2. - for aot in [AOT_AAC_LTP, 17, 19, 20, 21, 22, 23] { - assert_eq!( - tns_max_bands(aot, WindowSequence::OnlyLong, 4).unwrap(), - 42, - "AOT {aot} long non-PQF", - ); - assert_eq!( - tns_max_bands(aot, WindowSequence::EightShort, 4).unwrap(), - 14, - "AOT {aot} short non-PQF", - ); - } - } - - // ===== Tables 4.119 / 4.120 — AAC LD ===== - - #[test] - fn ld_480_matches_table_4_119_row_by_row() { - assert_eq!(tns_max_bands_ld_480(3).unwrap(), 31); // 48000 - assert_eq!(tns_max_bands_ld_480(4).unwrap(), 32); // 44100 - assert_eq!(tns_max_bands_ld_480(5).unwrap(), 37); // 32000 - assert_eq!(tns_max_bands_ld_480(6).unwrap(), 30); // 24000 - assert_eq!(tns_max_bands_ld_480(7).unwrap(), 30); // 22050 - } - - #[test] - fn ld_512_matches_table_4_120_row_by_row() { - assert_eq!(tns_max_bands_ld_512(3).unwrap(), 31); // 48000 - assert_eq!(tns_max_bands_ld_512(4).unwrap(), 32); // 44100 - assert_eq!(tns_max_bands_ld_512(5).unwrap(), 37); // 32000 - // The 512-sample row for 24 kHz / 22.05 kHz is 31 (one - // higher than the 480 row); this is the row-by-row - // contrast that justifies the two tables existing. - assert_eq!(tns_max_bands_ld_512(6).unwrap(), 31); // 24000 - assert_eq!(tns_max_bands_ld_512(7).unwrap(), 31); // 22050 - } - - #[test] - fn ld_480_rejects_uncovered_rates() { - // Table 4.119 covers fs 3..=7 only. Every other slot is None. - for fs in [0_u8, 1, 2, 8, 9, 10, 11] { - assert!(matches!( - tns_max_bands_ld_480(fs), - Err(Error::IcsInfoUnsupportedSampleRateIndex(_)) - )); - } - } - - #[test] - fn ld_512_rejects_uncovered_rates() { - for fs in [0_u8, 1, 2, 8, 9, 10, 11] { - assert!(matches!( - tns_max_bands_ld_512(fs), - Err(Error::IcsInfoUnsupportedSampleRateIndex(_)) - )); - } - } - - #[test] - fn ld_accessors_reject_out_of_range_fs_index() { - assert!(matches!( - tns_max_bands_ld_480(12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - assert!(matches!( - tns_max_bands_ld_480(15), - Err(Error::IcsInfoUnsupportedSampleRateIndex(15)) - )); - assert!(matches!( - tns_max_bands_ld_512(13), - Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) - )); - } - - // ===== Clamp helpers ===== - - #[test] - fn clamp_order_floors_to_cap() { - // AAC LC at 48 kHz long: cap is 12. A wire order of 20 - // (decoder MUST clamp per §4.6.9.3) becomes 12. - assert_eq!( - clamp_tns_order(20, AOT_AAC_LC, WindowSequence::OnlyLong, 3).unwrap(), - 12, - ); - // Wire order under the cap is returned unchanged. - assert_eq!( - clamp_tns_order(5, AOT_AAC_LC, WindowSequence::OnlyLong, 3).unwrap(), - 5, - ); - // Equal-to-cap order is preserved (not clamped to one less). - assert_eq!( - clamp_tns_order(12, AOT_AAC_LC, WindowSequence::OnlyLong, 3).unwrap(), - 12, - ); - // Short windows always cap at 7 regardless of AOT. - assert_eq!( - clamp_tns_order(31, AOT_AAC_MAIN, WindowSequence::EightShort, 3).unwrap(), - 7, - ); - } - - #[test] - fn clamp_order_propagates_fs_error() { - assert!(matches!( - clamp_tns_order(5, AOT_AAC_LC, WindowSequence::OnlyLong, 13), - Err(Error::IcsInfoUnsupportedSampleRateIndex(13)) - )); - } - - #[test] - fn clamp_band_takes_three_way_min() { - // AAC LC at 44.1 kHz long: TNS_MAX_BANDS = 42. With - // max_sfb = 49 (per Table 4.129) and a wire band of 50, the - // three-way min is 42 (the TNS_MAX_BANDS cap wins). - assert_eq!( - clamp_tns_band(50, 49, AOT_AAC_LC, WindowSequence::OnlyLong, 4).unwrap(), - 42, - ); - // With max_sfb = 30 the second min collapses to 30 (the - // ics_info `max_sfb` cap wins). - assert_eq!( - clamp_tns_band(50, 30, AOT_AAC_LC, WindowSequence::OnlyLong, 4).unwrap(), - 30, - ); - // With a wire band under both caps, the band itself wins. - assert_eq!( - clamp_tns_band(10, 49, AOT_AAC_LC, WindowSequence::OnlyLong, 4).unwrap(), - 10, - ); - } - - #[test] - fn clamp_band_propagates_fs_error() { - assert!(matches!( - clamp_tns_band(5, 49, AOT_AAC_LC, WindowSequence::OnlyLong, 12), - Err(Error::IcsInfoUnsupportedSampleRateIndex(12)) - )); - } - - // ===== Sanity: table lengths cover fs 0..=11 ===== - - #[test] - fn every_non_ld_table_has_12_entries() { - assert_eq!(TNS_MAX_BANDS_LONG_NON_PQF.len(), 12); - assert_eq!(TNS_MAX_BANDS_SHORT_NON_PQF.len(), 12); - assert_eq!(TNS_MAX_BANDS_LONG_PQF.len(), 12); - assert_eq!(TNS_MAX_BANDS_SHORT_PQF.len(), 12); - } - - #[test] - fn every_ld_table_has_12_entries() { - assert_eq!(TNS_MAX_BANDS_LD_480.len(), 12); - assert_eq!(TNS_MAX_BANDS_LD_512.len(), 12); - } -} diff --git a/crates/vuio-bench/Cargo.toml b/crates/vuio-bench/Cargo.toml index 52d45e2b..482c80d8 100644 --- a/crates/vuio-bench/Cargo.toml +++ b/crates/vuio-bench/Cargo.toml @@ -16,6 +16,10 @@ publish = false name = "vuio-bench" path = "src/main.rs" +[[bin]] +name = "aac-bench" +path = "src/aac_bench.rs" + [dependencies] anyhow = "1.0" clap = { version = "4.6", features = ["derive"] } @@ -25,4 +29,8 @@ tokio = { version = "1.53", features = ["rt-multi-thread", "macros"] } # because it opens every internal module and carries no stability promise. This # crate is `publish = false` and exists only to drive the database from the # inside, which is the same category as core's own dev-dependency on itself. -vuio-core = { path = "../vuio-core", version = "0.0.45", features = ["unstable-internals"] } +vuio-core = { path = "../vuio-core", version = "0.0.45", features = ["unstable-internals", "transcode-aac", "transcode-ac3", "transcode-dts"] } +oxideav-ac3 = { path = "../vendor/oxideav-ac3" } +oxideav-dts = { path = "../vendor/oxideav-dts" } +oxideav-core = { path = "../vendor/oxideav-core" } +xaac-rs = "0.2" diff --git a/crates/vuio-bench/src/aac_bench.rs b/crates/vuio-bench/src/aac_bench.rs new file mode 100644 index 00000000..93dc93d7 --- /dev/null +++ b/crates/vuio-bench/src/aac_bench.rs @@ -0,0 +1,504 @@ +//! Benchmark for audio decoders and encoders in VuIO: +//! - AC-3 Decoder (oxideav-ac3) +//! - E-AC-3 Decoder (oxideav-ac3) +//! - DTS Decoder (oxideav-dts) +//! - AAC Encoder (xaac-rs vs Apple Native vs oxideav-aac) + +use std::time::{Duration, Instant}; +use vuio_core::media::transcode::{PcmDecoder, TranscodeCodec}; + +const AC3_FIXTURE: &[u8] = include_bytes!("../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); +const DTS_FIXTURE: &[u8] = include_bytes!("../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); + +fn generate_sine_wave(sample_rate: u32, channels: u16, duration_secs: f64) -> Vec { + let total_samples = (sample_rate as f64 * duration_secs) as usize; + let mut pcm = Vec::with_capacity(total_samples * channels as usize); + let freq = 440.0; + for i in 0..total_samples { + let t = i as f64 / sample_rate as f64; + let val = (t * freq * 2.0 * std::f64::consts::PI).sin(); + let sample = (val * 30000.0) as i16; + for _ in 0..channels { + pcm.push(sample); + } + } + pcm +} + +fn pcm_i16_to_u8_le(pcm: &[i16]) -> Vec { + let mut bytes = Vec::with_capacity(pcm.len() * 2); + for &s in pcm { + bytes.extend_from_slice(&s.to_le_bytes()); + } + bytes +} + +// ========================================================================= +// DECODE BENCHMARKS: AC-3, E-AC-3, DTS +// ========================================================================= + +/// Benchmark AC-3 Decoding +fn bench_ac3_decode(iterations: usize) -> (Duration, f64, usize) { + let frame_len = 768; // 48kHz 192kbps stereo AC-3 frame + let num_frames_in_fixture = AC3_FIXTURE.len() / frame_len; + let first_frame = &AC3_FIXTURE[..frame_len]; + + let start = Instant::now(); + let (mut decoder, first_pcm) = PcmDecoder::open(TranscodeCodec::Ac3, 48000, Some(2), first_frame) + .expect("open AC-3 decoder"); + + let mut total_pcm_bytes = first_pcm.len(); + let mut total_samples = 1536; // first frame samples + + for _ in 0..iterations { + for f in 0..num_frames_in_fixture { + let frame = &AC3_FIXTURE[f * frame_len..(f + 1) * frame_len]; + let pcm = decoder.decode_or_silence(frame, Some(1536)); + total_pcm_bytes += pcm.len(); + total_samples += 1536; + } + } + + let elapsed = start.elapsed(); + let audio_duration_secs = total_samples as f64 / 48000.0; + (elapsed, audio_duration_secs, total_pcm_bytes) +} + +/// Benchmark E-AC-3 (Dolby Digital Plus) Decoding +fn bench_eac3_decode(iterations: usize) -> (Duration, f64, usize) { + // Generate a compliant E-AC-3 bitstream via oxideav-ac3's EAC-3 encoder + use oxideav_core::{AudioFrame, CodecId, CodecParameters, Frame, SampleFormat}; + + let sample_rate = 48000; + let channels = 2; + let mut params = CodecParameters::audio(CodecId::new("eac3")); + params.sample_rate = Some(sample_rate); + params.channels = Some(channels); + params.sample_format = Some(SampleFormat::S16); + + let mut enc = oxideav_ac3::eac3::make_encoder(¶ms).expect("init Eac3Encoder"); + let test_pcm = generate_sine_wave(sample_rate, channels, 1.0); + let test_bytes = pcm_i16_to_u8_le(&test_pcm); + + let frame = Frame::Audio(AudioFrame { + samples: 1536, + pts: None, + data: vec![test_bytes[..1536 * 4].to_vec()], + }); + enc.send_frame(&frame).expect("send frame to Eac3Encoder"); + enc.flush().expect("flush Eac3Encoder"); + + let mut eac3_packets = Vec::new(); + while let Ok(pkt) = enc.receive_packet() { + eac3_packets.push(pkt.data); + } + assert!(!eac3_packets.is_empty(), "E-AC-3 encoder produced no packets"); + + let first_frame = &eac3_packets[0]; + let start = Instant::now(); + let (mut decoder, first_pcm) = PcmDecoder::open(TranscodeCodec::Eac3, sample_rate, Some(channels), first_frame) + .expect("open E-AC-3 decoder"); + + let mut total_pcm_bytes = first_pcm.len(); + let mut total_samples = 1536; + + for _ in 0..iterations { + for pkt in &eac3_packets { + let pcm = decoder.decode_or_silence(pkt, Some(1536)); + total_pcm_bytes += pcm.len(); + total_samples += 1536; + } + } + + let elapsed = start.elapsed(); + let audio_duration_secs = total_samples as f64 / sample_rate as f64; + (elapsed, audio_duration_secs, total_pcm_bytes) +} + +/// Benchmark DTS Decoding +fn bench_dts_decode(iterations: usize) -> (Duration, f64, usize) { + // dts_5_frames.bin carries 5 real DTS 5.1/stereo frames + // Read frame lengths from DTS frame headers (syncword 0x7FFE8001) + let mut frames = Vec::new(); + let mut offset = 0; + while offset + 10 <= DTS_FIXTURE.len() { + if DTS_FIXTURE[offset..offset + 4] == [0x7F, 0xFE, 0x80, 0x01] { + let fsize = (((DTS_FIXTURE[offset + 5] as usize & 0x03) << 12) + | ((DTS_FIXTURE[offset + 6] as usize) << 4) + | ((DTS_FIXTURE[offset + 7] as usize & 0xF0) >> 4)) + + 1; + if offset + fsize <= DTS_FIXTURE.len() { + frames.push(&DTS_FIXTURE[offset..offset + fsize]); + offset += fsize; + continue; + } + } + offset += 1; + } + assert!(!frames.is_empty(), "No DTS frames found in fixture"); + + let first_frame = frames[0]; + let start = Instant::now(); + let (mut decoder, first_pcm) = PcmDecoder::open(TranscodeCodec::Dts, 48000, Some(2), first_frame) + .expect("open DTS decoder"); + + let mut total_pcm_bytes = first_pcm.len(); + let mut total_samples = 512; + + for _ in 0..iterations { + for frame in &frames { + let pcm = decoder.decode_or_silence(frame, Some(512)); + total_pcm_bytes += pcm.len(); + total_samples += 512; + } + } + + let elapsed = start.elapsed(); + let audio_duration_secs = total_samples as f64 / 48000.0; + (elapsed, audio_duration_secs, total_pcm_bytes) +} + +// ========================================================================= +// ENCODE BENCHMARKS +// ========================================================================= + +fn bench_vuio_aac(pcm_bytes: &[u8], sample_rate: u32, channels: u16) -> (Duration, usize) { + let start = Instant::now(); + let mut encoder = vuio_core::media::transcode::AacEncoder::new(sample_rate, channels) + .expect("failed to init VuIO AAC encoder"); + + let chunk_size = 1024 * channels as usize * 2; + let mut out_len = 0; + for chunk in pcm_bytes.chunks(chunk_size) { + let adts = encoder.push(chunk).expect("VuIO AAC encode error"); + out_len += adts.len(); + } + let tail = encoder.finish(); + out_len += tail.len(); + let elapsed = start.elapsed(); + (elapsed, out_len) +} + +#[cfg(target_os = "macos")] +#[allow(non_snake_case, non_upper_case_globals)] +mod apple_native { + use std::ffi::c_void; + + #[repr(C)] + #[derive(Debug, Clone, Copy, Default)] + struct AudioStreamBasicDescription { + mSampleRate: f64, + mFormatID: u32, + mFormatFlags: u32, + mBytesPerPacket: u32, + mFramesPerPacket: u32, + mBytesPerFrame: u32, + mChannelsPerFrame: u32, + mBitsPerChannel: u32, + mReserved: u32, + } + + const kAudioFormatLinearPCM: u32 = 0x6c70636d; + const kAudioFormatMPEG4AAC: u32 = 0x61616320; + const kAudioFormatFlagIsSignedInteger: u32 = 1 << 2; + const kAudioFormatFlagIsPacked: u32 = 1 << 3; + + #[repr(C)] + struct AudioBuffer { + mNumberChannels: u32, + mDataByteSize: u32, + mData: *mut c_void, + } + + #[repr(C)] + struct AudioBufferList { + mNumberBuffers: u32, + mBuffers: [AudioBuffer; 1], + } + + #[repr(C)] + #[derive(Debug, Default, Clone, Copy)] + struct AudioStreamPacketDescription { + mStartOffset: i64, + mVariableFramesInPacket: u32, + mDataByteSize: u32, + } + + type AudioConverterRef = *mut c_void; + type OSStatus = i32; + + type AudioConverterComplexInputDataProc = unsafe extern "C" fn( + inAudioConverter: AudioConverterRef, + ioNumberDataPackets: *mut u32, + ioData: *mut AudioBufferList, + outDataPacketDescription: *mut *mut AudioStreamPacketDescription, + inUserData: *mut c_void, + ) -> OSStatus; + + #[link(name = "AudioToolbox", kind = "framework")] + extern "C" { + fn AudioConverterNew( + inSourceFormat: *const AudioStreamBasicDescription, + inDestinationFormat: *const AudioStreamBasicDescription, + outAudioConverter: *mut AudioConverterRef, + ) -> OSStatus; + + fn AudioConverterDispose(inAudioConverter: AudioConverterRef) -> OSStatus; + + fn AudioConverterFillComplexBuffer( + inAudioConverter: AudioConverterRef, + inInputDataProc: AudioConverterComplexInputDataProc, + inInputDataProcUserData: *mut c_void, + ioOutputDataPacketSize: *mut u32, + outOutputData: *mut AudioBufferList, + outPacketDescription: *mut AudioStreamPacketDescription, + ) -> OSStatus; + } + + struct InputContext<'a> { + pcm_bytes: &'a [u8], + pos: usize, + bytes_per_packet: usize, + } + + unsafe extern "C" fn input_data_proc( + _in_converter: AudioConverterRef, + io_num_packets: *mut u32, + io_data: *mut AudioBufferList, + _out_packet_desc: *mut *mut AudioStreamPacketDescription, + in_user_data: *mut c_void, + ) -> OSStatus { + let ctx = &mut *(in_user_data as *mut InputContext); + let requested_packets = *io_num_packets as usize; + let available_bytes = ctx.pcm_bytes.len().saturating_sub(ctx.pos); + let available_packets = available_bytes / ctx.bytes_per_packet; + let packets_to_give = requested_packets.min(available_packets); + + if packets_to_give == 0 { + *io_num_packets = 0; + return 0; + } + + let bytes_to_give = packets_to_give * ctx.bytes_per_packet; + let ptr = ctx.pcm_bytes[ctx.pos..].as_ptr() as *mut c_void; + ctx.pos += bytes_to_give; + + *io_num_packets = packets_to_give as u32; + (*io_data).mNumberBuffers = 1; + (*io_data).mBuffers[0].mNumberChannels = 2; + (*io_data).mBuffers[0].mDataByteSize = bytes_to_give as u32; + (*io_data).mBuffers[0].mData = ptr; + + 0 + } + + pub fn bench_apple(pcm_bytes: &[u8], sample_rate: u32, channels: u16) -> (std::time::Duration, usize) { + let start = std::time::Instant::now(); + + let in_format = AudioStreamBasicDescription { + mSampleRate: sample_rate as f64, + mFormatID: kAudioFormatLinearPCM, + mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, + mBytesPerPacket: (channels * 2) as u32, + mFramesPerPacket: 1, + mBytesPerFrame: (channels * 2) as u32, + mChannelsPerFrame: channels as u32, + mBitsPerChannel: 16, + mReserved: 0, + }; + + let out_format = AudioStreamBasicDescription { + mSampleRate: sample_rate as f64, + mFormatID: kAudioFormatMPEG4AAC, + mFormatFlags: 0, + mBytesPerPacket: 0, + mFramesPerPacket: 1024, + mBytesPerFrame: 0, + mChannelsPerFrame: channels as u32, + mBitsPerChannel: 0, + mReserved: 0, + }; + + let mut converter: AudioConverterRef = std::ptr::null_mut(); + let status = unsafe { AudioConverterNew(&in_format, &out_format, &mut converter) }; + assert_eq!(status, 0, "AudioConverterNew failed: {status}"); + + let mut ctx = InputContext { + pcm_bytes, + pos: 0, + bytes_per_packet: (channels * 2) as usize, + }; + + let mut out_len = 0; + let mut out_buf = vec![0u8; 8192]; + let mut packet_descs = vec![AudioStreamPacketDescription::default(); 16]; + + loop { + let mut num_packets: u32 = 16; + let mut buffer_list = AudioBufferList { + mNumberBuffers: 1, + mBuffers: [AudioBuffer { + mNumberChannels: channels as u32, + mDataByteSize: out_buf.len() as u32, + mData: out_buf.as_mut_ptr() as *mut c_void, + }], + }; + + let res = unsafe { + AudioConverterFillComplexBuffer( + converter, + input_data_proc, + &mut ctx as *mut _ as *mut c_void, + &mut num_packets, + &mut buffer_list, + packet_descs.as_mut_ptr(), + ) + }; + + if num_packets == 0 || res != 0 { + break; + } + + for i in 0..num_packets as usize { + out_len += packet_descs[i].mDataByteSize as usize; + } + } + + unsafe { AudioConverterDispose(converter) }; + let elapsed = start.elapsed(); + (elapsed, out_len) + } +} + +fn bench_xaac(pcm_bytes: &[u8], sample_rate: u32, channels: u16) -> Result<(Duration, usize), String> { + let start = Instant::now(); + use xaac_rs::{Encoder, EncoderConfig, OutputFormat, Profile}; + + let mut config = EncoderConfig::default(); + config.profile = Profile::AacLc; + config.sample_rate = sample_rate; + config.channels = channels; + config.bitrate = 128_000; + config.output_format = OutputFormat::Adts; + + let mut encoder = Encoder::new(config).map_err(|e| format!("{e:?}"))?; + let frame_bytes = encoder.input_frame_bytes(); + + let mut out_len = 0; + for chunk in pcm_bytes.chunks(frame_bytes) { + if chunk.len() == frame_bytes { + let encoded = encoder.encode_pcm_bytes(chunk).map_err(|e| format!("{e:?}"))?; + out_len += encoded.data.len(); + } else { + let encoded = encoder.encode_pcm_bytes_with_padding(chunk).map_err(|e| format!("{e:?}"))?; + out_len += encoded.packet.data.len(); + } + } + + let elapsed = start.elapsed(); + Ok((elapsed, out_len)) +} + +fn main() { + println!("================================================================================"); + println!("AUDIO DECODER SPEED BENCHMARKS (RELEASE MODE)"); + println!("================================================================================"); + + // 1. AC-3 Decode Benchmark + { + print!("Benchmarking AC-3 Decoder (oxideav-ac3)... "); + let iterations = 1000; // ~512 seconds of audio + let (dur, audio_secs, out_bytes) = bench_ac3_decode(iterations); + let speed = audio_secs / dur.as_secs_f64(); + println!("DONE\n Decoded Audio: {:.2} seconds ({:.2} MB PCM)\n Time Taken: {:.3} ms\n Speedup: {:.1}x real-time\n Throughput: {:.2} MB/s\n", + audio_secs, + out_bytes as f64 / (1024.0 * 1024.0), + dur.as_secs_f64() * 1000.0, + speed, + (out_bytes as f64 / (1024.0 * 1024.0)) / dur.as_secs_f64() + ); + } + + // 2. E-AC-3 Decode Benchmark + { + print!("Benchmarking E-AC-3 Decoder (oxideav-ac3)... "); + let iterations = 5000; // ~160 seconds of audio + let (dur, audio_secs, out_bytes) = bench_eac3_decode(iterations); + let speed = audio_secs / dur.as_secs_f64(); + println!("DONE\n Decoded Audio: {:.2} seconds ({:.2} MB PCM)\n Time Taken: {:.3} ms\n Speedup: {:.1}x real-time\n Throughput: {:.2} MB/s\n", + audio_secs, + out_bytes as f64 / (1024.0 * 1024.0), + dur.as_secs_f64() * 1000.0, + speed, + (out_bytes as f64 / (1024.0 * 1024.0)) / dur.as_secs_f64() + ); + } + + // 3. DTS Decode Benchmark + { + print!("Benchmarking DTS Decoder (oxideav-dts)... "); + let iterations = 3000; // ~160 seconds of audio + let (dur, audio_secs, out_bytes) = bench_dts_decode(iterations); + let speed = audio_secs / dur.as_secs_f64(); + println!("DONE\n Decoded Audio: {:.2} seconds ({:.2} MB PCM)\n Time Taken: {:.3} ms\n Speedup: {:.1}x real-time\n Throughput: {:.2} MB/s\n", + audio_secs, + out_bytes as f64 / (1024.0 * 1024.0), + dur.as_secs_f64() * 1000.0, + speed, + (out_bytes as f64 / (1024.0 * 1024.0)) / dur.as_secs_f64() + ); + } + + println!("================================================================================"); + println!("AAC ENCODER BENCHMARKS (RELEASE MODE)"); + println!("================================================================================"); + + let sample_rate = 48000; + let channels = 2; + let duration_secs = 60.0; + let pcm = generate_sine_wave(sample_rate, channels, duration_secs); + let pcm_bytes = pcm_i16_to_u8_le(&pcm); + + #[cfg(target_os = "macos")] + { + print!("Benchmarking Apple Native (AudioToolbox)... "); + let (dur, out_bytes) = apple_native::bench_apple(&pcm_bytes, sample_rate, channels); + let speed = duration_secs / dur.as_secs_f64(); + println!("DONE\n Time: {:.3} ms\n Speedup: {:.1}x real-time\n Output: {} bytes ({:.1} kbps)\n", + dur.as_secs_f64() * 1000.0, + speed, + out_bytes, + (out_bytes as f64 * 8.0) / (duration_secs * 1000.0) + ); + } + + { + print!("Benchmarking xaac-rs (libxaac)... "); + match bench_xaac(&pcm_bytes, sample_rate, channels) { + Ok((dur, out_bytes)) => { + let speed = duration_secs / dur.as_secs_f64(); + println!("DONE\n Time: {:.3} ms\n Speedup: {:.1}x real-time\n Output: {} bytes ({:.1} kbps)\n", + dur.as_secs_f64() * 1000.0, + speed, + out_bytes, + (out_bytes as f64 * 8.0) / (duration_secs * 1000.0) + ); + } + Err(e) => { + println!("FAILED: {}\n", e); + } + } + } + + { + print!("Benchmarking VuIO AacEncoder (libxaac)... "); + let (dur, out_bytes) = bench_vuio_aac(&pcm_bytes, sample_rate, channels); + let speed = duration_secs / dur.as_secs_f64(); + println!("DONE\n Time: {:.3} ms\n Speedup: {:.1}x real-time\n Output: {} bytes ({:.1} kbps)\n", + dur.as_secs_f64() * 1000.0, + speed, + out_bytes, + (out_bytes as f64 * 8.0) / (duration_secs * 1000.0) + ); + } +} diff --git a/crates/vuio-core/Cargo.toml b/crates/vuio-core/Cargo.toml index 14dec055..27325ed3 100644 --- a/crates/vuio-core/Cargo.toml +++ b/crates/vuio-core/Cargo.toml @@ -130,7 +130,7 @@ transcode-dts = ["transcode", "dep:oxideav-dts"] # The AAC-LC encoder, for `[transcode] audio_format = "aac"`. LPCM is the # default output and needs no encoder, so this is separable — but it is also # what the HLS audio path will re-encode into, so it ships on. -transcode-aac = ["transcode", "dep:oxideav-aac"] +transcode-aac = ["transcode", "dep:xaac-rs"] unstable-internals = [] [dependencies] @@ -193,7 +193,7 @@ hex = { version = "0.4", optional = true } oxideav-core = { path = "../vendor/oxideav-core", optional = true } oxideav-ac3 = { path = "../vendor/oxideav-ac3", optional = true } oxideav-dts = { path = "../vendor/oxideav-dts", optional = true } -oxideav-aac = { path = "../vendor/oxideav-aac", optional = true } +xaac-rs = { version = "0.2", optional = true } rusqlite = { version = "0.40.2", features = ["bundled", "collation"] } # Only the `mediainfo` feature uses this, and only to talk to public metadata APIs # over TLS. `rustls-no-provider` rather than a provider-selecting feature because diff --git a/crates/vuio-core/src/media/transcode/aac.rs b/crates/vuio-core/src/media/transcode/aac.rs index fbb6d3cd..84393fcf 100644 --- a/crates/vuio-core/src/media/transcode/aac.rs +++ b/crates/vuio-core/src/media/transcode/aac.rs @@ -15,7 +15,9 @@ use anyhow::{Context, Result}; /// One AAC-LC encoder bound to a stream's shape. pub struct AacEncoder { - inner: Box, + inner: xaac_rs::Encoder, + frame_bytes: usize, + buffer: Vec, channels: u16, sample_rate: u32, } @@ -23,25 +25,31 @@ pub struct AacEncoder { impl AacEncoder { /// Build an encoder producing `channels` at `sample_rate`. /// - /// The bitrate is the vendored encoder's default of 64 kbps per channel — - /// the conventional AAC-LC "good quality" operating point, and around a - /// tenth of the LPCM the same audio would cost. Left unconfigurable - /// deliberately: it is one more knob whose wrong setting is audible, and - /// nothing about this path benefits from tuning it. + /// The bitrate is 64 kbps per channel — the conventional AAC-LC "good quality" + /// operating point, and around a tenth of the LPCM the same audio would cost. pub fn new(sample_rate: u32, channels: u16) -> Result { - use oxideav_core::{CodecId, CodecParameters, SampleFormat}; + if !matches!(channels, 1 | 2 | 3 | 4 | 5 | 6 | 8) { + anyhow::bail!("unsupported channel count for AAC: {channels}"); + } - let mut params = CodecParameters::audio(CodecId::new("aac")); - params.sample_rate = Some(sample_rate); - params.channels = Some(channels); - params.sample_format = Some(SampleFormat::S16); + let config = xaac_rs::EncoderConfig { + profile: xaac_rs::Profile::AacLc, + sample_rate, + channels, + bitrate: 64_000 * u32::from(channels), + output_format: xaac_rs::OutputFormat::Adts, + ..Default::default() + }; - let inner = oxideav_aac::codec_encoder::make_encoder(¶ms) - .map_err(|e| anyhow::anyhow!("AAC encoder: {e}")) + let inner = xaac_rs::Encoder::new(config) + .map_err(|e| anyhow::anyhow!("AAC encoder: {e:?}")) .context("configuring the AAC encoder")?; + let frame_bytes = inner.input_frame_bytes(); Ok(Self { inner, + frame_bytes, + buffer: Vec::with_capacity(frame_bytes * 2), channels, sample_rate, }) @@ -49,32 +57,32 @@ impl AacEncoder { /// Feed interleaved S16 and collect whatever ADTS frames come out. /// - /// The encoder buffers to its own 1024-sample frame length, so a call may - /// well produce nothing; that is normal, not an error. + /// The encoder buffers to its own frame length (typically 1024 samples per channel), + /// so a call may well produce nothing; that is normal, not an error. pub fn push(&mut self, pcm: &[u8]) -> Result> { - use oxideav_core::{AudioFrame, Frame}; - - let samples = pcm.len() / (self.channels as usize * 2); - if samples == 0 { - return Ok(Vec::new()); + self.buffer.extend_from_slice(pcm); + let mut out = Vec::new(); + while self.buffer.len() >= self.frame_bytes { + let chunk: Vec = self.buffer.drain(..self.frame_bytes).collect(); + let encoded = self + .inner + .encode_pcm_bytes(&chunk) + .map_err(|e| anyhow::anyhow!("AAC encode: {e:?}"))?; + out.extend_from_slice(&encoded.data); } - let frame = Frame::Audio(AudioFrame { - samples: samples as u32, - pts: None, - data: vec![pcm.to_vec()], - }); - self.inner - .send_frame(&frame) - .map_err(|e| anyhow::anyhow!("AAC encode: {e}"))?; - Ok(self.drain()) + Ok(out) } - /// Flush the encoder's lookahead and overlap, ending the stream cleanly. + /// Flush the encoder's lookahead and trailing buffer, ending the stream cleanly. pub fn finish(&mut self) -> Vec { - if self.inner.flush().is_err() { - return Vec::new(); + let mut out = Vec::new(); + if !self.buffer.is_empty() { + let chunk = std::mem::take(&mut self.buffer); + if let Ok(encoded) = self.inner.encode_pcm_bytes_with_padding(&chunk) { + out.extend_from_slice(&encoded.packet.data); + } } - self.drain() + out } /// Sample rate the encoder was configured for. @@ -82,14 +90,9 @@ impl AacEncoder { self.sample_rate } - fn drain(&mut self) -> Vec { - let mut out = Vec::new(); - // `receive_packet` returns `NeedMore` once drained, which is the normal - // exit rather than a failure. - while let Ok(packet) = self.inner.receive_packet() { - out.extend_from_slice(&packet.data); - } - out + /// Number of channels the encoder was configured for. + pub fn channels(&self) -> u16 { + self.channels } } diff --git a/scripts/vendor-oxideav.sh b/scripts/vendor-oxideav.sh index 6691cde4..3e7baaa4 100755 --- a/scripts/vendor-oxideav.sh +++ b/scripts/vendor-oxideav.sh @@ -37,9 +37,8 @@ SELF="$ROOT/scripts/vendor-oxideav.sh" PIN_oxideav_core=defa866dffdd224424d75ac7a38be868723395a5 PIN_oxideav_ac3=8acf106d50d58f359946c086d1b393a060eaf5f6 PIN_oxideav_dts=528203ed608223c5137843009054e05920af5c50 -PIN_oxideav_aac=719f1f594aef3465ecf2d718685bc27f8e797423 -CRATES="oxideav-core oxideav-ac3 oxideav-dts oxideav-aac" +CRATES="oxideav-core oxideav-ac3 oxideav-dts" UPDATE=0 [ "${1:-}" = "--update" ] && UPDATE=1 From 9a771d4c95eb3fc3ae0f031ae4d623fc7ba56c8d Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 18:48:12 +0300 Subject: [PATCH 13/38] audio decoder works --- crates/vuio-core/src/media/remux/hls.rs | 91 ++++++++++++++++++- .../vuio-core/src/media/remux/mkv_demuxer.rs | 6 +- .../src/media/transcode/rendition.rs | 4 +- crates/vuio-core/src/web/remux_streaming.rs | 3 +- .../vuio-core/src/web/ui/js/video-player.js | 43 ++++++--- crates/vuio-core/src/web/video_streaming.rs | 16 +++- .../dist/_app/immutable/nodes/0.CESpnOw-.js | 2 +- 7 files changed, 142 insertions(+), 23 deletions(-) diff --git a/crates/vuio-core/src/media/remux/hls.rs b/crates/vuio-core/src/media/remux/hls.rs index 7875a002..85ce58e0 100644 --- a/crates/vuio-core/src/media/remux/hls.rs +++ b/crates/vuio-core/src/media/remux/hls.rs @@ -33,10 +33,7 @@ impl HlsGenerator { if !audio_tracks.is_empty() { for (idx, track) in audio_tracks.iter().enumerate() { - let name = track - .name - .clone() - .unwrap_or_else(|| format!("Audio Track {}", idx + 1)); + let name = format_audio_track_name(track, idx); let lang = track.language.as_deref().unwrap_or("und"); let is_default = if idx == 0 { "YES" } else { "NO" }; @@ -133,6 +130,92 @@ fn aac_codec_string(extra_data: &[u8]) -> String { format!("mp4a.40.{}", audio_object_type) } +fn language_name(code: &str) -> &'static str { + match code.to_ascii_lowercase().as_str() { + "eng" | "en" => "English", + "spa" | "es" => "Spanish", + "fra" | "fre" | "fr" => "French", + "deu" | "ger" | "de" => "German", + "ita" | "it" => "Italian", + "jpn" | "ja" => "Japanese", + "rus" | "ru" => "Russian", + "zho" | "chi" | "zh" => "Chinese", + "kor" | "ko" => "Korean", + "por" | "pt" => "Portuguese", + "hin" | "hi" => "Hindi", + "ara" | "ar" => "Arabic", + "pol" | "pl" => "Polish", + "ukr" | "uk" => "Ukrainian", + "vie" | "vi" => "Vietnamese", + "tur" | "tr" => "Turkish", + "nld" | "dut" | "nl" => "Dutch", + "swe" | "sv" => "Swedish", + "nor" | "no" => "Norwegian", + "dan" | "da" => "Danish", + "fin" | "fi" => "Finnish", + "ces" | "cze" | "cs" => "Czech", + "hun" | "hu" => "Hungarian", + "ron" | "rum" | "ro" => "Romanian", + "ell" | "gre" | "el" => "Greek", + "heb" | "he" => "Hebrew", + "tha" | "th" => "Thai", + _ => "", + } +} + +pub fn format_audio_track_name(track: &TrackInfo, idx: usize) -> String { + if let Some(ref name) = track.name { + let trimmed = name.trim(); + if !trimmed.is_empty() + && !trimmed.eq_ignore_ascii_case("und") + && !trimmed.starts_with("Audio Track") + { + return trimmed.to_string(); + } + } + + let lang_code = track.language.as_deref().unwrap_or(""); + let lang_display = if !lang_code.is_empty() && lang_code != "und" { + let name = language_name(lang_code); + if !name.is_empty() { + name + } else { + lang_code + } + } else { + "" + }; + + let channels_str = match track.channels { + Some(6) => "5.1", + Some(8) => "7.1", + Some(2) => "Stereo", + Some(1) => "Mono", + Some(c) if c > 2 => return format!("Track {} ({}ch {})", idx + 1, c, track.codec), + _ => "", + }; + + let main_label = if !lang_display.is_empty() { + lang_display.to_string() + } else { + format!("Audio Track {}", idx + 1) + }; + + let mut details = Vec::new(); + if !channels_str.is_empty() { + details.push(channels_str); + } + if !track.codec.is_empty() { + details.push(&track.codec); + } + + if !details.is_empty() { + format!("{} ({})", main_label, details.join(" ")) + } else { + main_label + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/vuio-core/src/media/remux/mkv_demuxer.rs b/crates/vuio-core/src/media/remux/mkv_demuxer.rs index 9d515fe6..de9e3a63 100644 --- a/crates/vuio-core/src/media/remux/mkv_demuxer.rs +++ b/crates/vuio-core/src/media/remux/mkv_demuxer.rs @@ -322,11 +322,15 @@ impl MkvDemuxer { hint.with_extension(ext); } + let mut meta_opts = MetadataOptions::default(); + meta_opts.limit_tag_bytes = symphonia::core::common::Limit::Maximum(0); + meta_opts.limit_visual_bytes = symphonia::core::common::Limit::Maximum(0); + let mut format = symphonia::default::get_probe().probe( &hint, stream, FormatOptions::default(), - MetadataOptions::default(), + meta_opts, )?; let track_time_base = format diff --git a/crates/vuio-core/src/media/transcode/rendition.rs b/crates/vuio-core/src/media/transcode/rendition.rs index d2c8a187..247619dd 100644 --- a/crates/vuio-core/src/media/transcode/rendition.rs +++ b/crates/vuio-core/src/media/transcode/rendition.rs @@ -43,6 +43,7 @@ pub fn reencode_to_aac( sample_rate: u32, channels: u16, track_id: u32, + nominal_start_pts: Option, ) -> Result> { let Some(first) = packets.first() else { return Ok(Vec::new()); @@ -65,7 +66,8 @@ pub fn reencode_to_aac( // that already starts at the beginning of the film, where there is no // earlier timeline to move onto — the residual lag there is one frame, // twenty-one milliseconds at 48 kHz. - let mut dts = first.pts.saturating_sub(AAC_FRAME_SAMPLES); + let base = nominal_start_pts.unwrap_or(first.pts); + let mut dts = base.saturating_sub(AAC_FRAME_SAMPLES); let mut out = Vec::new(); for payload in super::adts_payloads(&adts) { out.push(MediaPacket { diff --git a/crates/vuio-core/src/web/remux_streaming.rs b/crates/vuio-core/src/web/remux_streaming.rs index 8a9f0e16..1d9018fb 100644 --- a/crates/vuio-core/src/web/remux_streaming.rs +++ b/crates/vuio-core/src/web/remux_streaming.rs @@ -201,6 +201,7 @@ fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> V let out_track = rendition_track(track); let timescale = Fmp4Writer::timescale_for(&out_track); let start_secs = seq as f64 * SEGMENT_DURATION_SECS as f64; + let nominal_decode_time = (start_secs * timescale as f64).round() as u64; let packets = MkvDemuxer::extract_track_packets( path, @@ -220,6 +221,7 @@ fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> V out_track.sample_rate.unwrap_or(48_000), DECODED_CHANNELS, out_track.id, + Some(nominal_decode_time), ) .unwrap_or_default(), None => packets, @@ -229,7 +231,6 @@ fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> V // base decode time comes from the packets themselves (`build_segment` takes it from // the first one's decode timestamp). The nominal `seq`-derived position is only a // fallback for a segment that came back empty. - let nominal_decode_time = (start_secs * timescale as f64).round() as u64; Fmp4Writer::build_segment(seq + 1, &out_track, nominal_decode_time, &packets) } diff --git a/crates/vuio-core/src/web/ui/js/video-player.js b/crates/vuio-core/src/web/ui/js/video-player.js index 5ee30277..98aac2ed 100644 --- a/crates/vuio-core/src/web/ui/js/video-player.js +++ b/crates/vuio-core/src/web/ui/js/video-player.js @@ -152,23 +152,34 @@ async function attachHlsSource(video, url, file) { } // Populates the audio-track `,1),xe=e(`
Cover
`),Se=e(` `,1);function Q(e,t){j(t,!0);let c,f=_(()=>q.activeAudioItem?.cat===`radio`||q.activeAudioItem?.mime===`audio/radio`||q.activeAudioItem?.ext===`m3u`||q.activeAudioItem?.ext===`pls`||q.activeAudioItem?.path?.toLowerCase().includes(`/radio/`)||q.activeAudioItem?.path?.toLowerCase().includes(`\\radio\\`));o(()=>{if(q.activeAudioItem&&c){let e=q.activeAudioItem.stream_url??H(q.activeAudioItem.id),t=e.startsWith(`http`)?e:window.location.origin+e;c.src!==t&&(c.src=e,q.isPlayingAudio&&c.play().catch(()=>{}))}else!q.activeAudioItem&&c&&c.src&&(c.pause(),c.removeAttribute(`src`),c.load())}),o(()=>{c&&(q.isPlayingAudio&&c.paused?c.play().catch(()=>{}):!q.isPlayingAudio&&!c.paused&&c.pause())});function v(){c&&(q.audioProgress=c.currentTime,q.audioDuration=c.duration||0)}function T(e){let t=e.target,n=parseFloat(t.value);c&&(c.currentTime=n,q.audioProgress=n)}function E(e){let t=e.target,n=parseFloat(t.value);q.volume=n,c&&(c.volume=n,c.muted=n===0)}function k(){q.isMuted=!q.isMuted,c&&(c.muted=q.isMuted)}function A(e){if(!e||isNaN(e))return`0:00`;let t=Math.floor(e/60),n=Math.floor(e%60);return`${t}:${n<10?`0`:``}${n}`}var M=Se(),N=a(M);b(N,e=>c=e,()=>c);var P=l(N,2),F=e=>{var t=xe(),o=g(t),c=g(o),_=l(c,2),v=g(_),b=g(v,!0);w(v);var x=l(v,2),j=g(x),M=l(j),N=e=>{var t=d();h(()=>n(t,`— ${q.activeAudioItem.album??``}`)),p(e,t)};r(M,e=>{q.activeAudioItem.album&&e(N)}),w(x),w(_),w(o);var P=l(o,2),F=g(P),L=g(F);pe(g(L),{size:16}),w(L);var R=l(L,2),te=g(R),ne=e=>{de(e,{size:20})},z=e=>{ee(e,{size:20})};r(te,e=>{q.isPlayingAudio?e(ne):e(z,-1)}),w(R);var B=l(R,2),V=g(B);ae(V,{size:16}),w(B),w(F);var H=l(F,2),re=g(H),ie=e=>{var t=Z(),r=a(t),i=g(r);G(i,{size:12}),O(),w(r);var o=l(r,2),s=g(o,!0);w(o),h(e=>n(s,e),[()=>A(q.audioProgress)]),p(e,t)},U=e=>{var t=be(),r=a(t),i=g(r,!0);w(r);var o=l(r,2);S(o);var s=l(o,2),c=g(s,!0);w(s),h((e,t)=>{n(i,e),D(o,`max`,q.audioDuration||100),C(o,q.audioProgress),n(c,t)},[()=>A(q.audioProgress),()=>A(q.audioDuration)]),m(`input`,o,T),p(e,t)};r(re,e=>{u(f)?e(ie):e(U,-1)}),w(H),w(P);var oe=l(P,2),W=g(oe),K=g(W),ce=e=>{_e(e,{size:16})},J=e=>{he(e,{size:16})};r(K,e=>{q.isMuted||q.volume===0?e(ce):e(J,-1)}),w(W);var Y=l(W,2);S(Y);var X=l(Y,2),le=g(X);se(le,{size:16}),w(X),w(oe),w(t),h(e=>{D(c,`src`,e),n(b,q.activeAudioItem.title||q.activeAudioItem.name),n(j,`${(q.activeAudioItem.artist||(u(f)?`Internet Radio`:`Unknown Artist`))??``} `),L.disabled=u(f),B.disabled=u(f),C(Y,q.volume),y(X,1,`btn btn-secondary btn-icon icon-sm ${q.isQueueOpen?`active`:``}`,`svelte-o0g1vk`)},[()=>I(q.activeAudioItem.id)]),i(`error`,c,e=>{e.target.src=`data:image/svg+xml,`}),s(c),m(`click`,L,()=>q.prevAudio()),m(`click`,R,()=>q.toggleAudioPlay()),m(`click`,B,()=>q.nextAudio()),m(`click`,W,k),m(`input`,Y,E),m(`click`,X,()=>q.toggleQueue()),p(e,t)};r(P,e=>{q.activeAudioItem&&e(F)}),i(`timeupdate`,N,v),i(`ended`,N,()=>q.nextAudio()),p(e,M),x()}f([`click`,`input`]);var Ce=e(`

Container Format Unsupported for In-Browser Playback

File format cannot be demuxed directly inside - your web browser. You can download the raw media file below.

`),$=e(``,2),we=e(`
`);function Te(e,t){j(t,!0);let i=new Set([`avi`,`wmv`,`flv`,`mpg`,`mpeg`]),s=v(null),d=null,f=null,y=v(!1),S=_(()=>q.activeVideoItem);o(()=>{if(u(S)&&u(s)&&q.isVideoOpen){let e=(u(S).ext||``).toLowerCase();if(i.has(e)){c(y,!0);return}c(y,!1),C()}return()=>{E()}});async function C(){if(!u(S)||!u(s)||typeof window>`u`)return;E();let[e,t]=await Promise.all([N(()=>import(`../chunks/BySXlKnG.js`),[],import.meta.url),N(()=>import(`../chunks/DFbhk10t.js`),[],import.meta.url)]),n=e.default||e,r=t.default||t,i=H(u(S).id),a=(u(S).ext||``).toLowerCase()===`mkv`||i.endsWith(`.m3u8`),o=a?L(u(S).id):i;if(u(S).subs){let e=document.createElement(`track`);e.kind=`subtitles`,e.srclang=`en`,e.label=`English`,e.src=P(u(S).id),e.default=!0,u(s).appendChild(e)}if(a){if(r.isSupported())f=new r({enableWorker:!0}),f.loadSource(o),f.attachMedia(u(s));else if(u(s).canPlayType(`application/vnd.apple.mpegurl`))u(s).src=o;else{c(y,!0);return}}else u(s).src=o;u(s).ontimeupdate=()=>{u(S)&&u(s)&&u(s).currentTime>2&&F.updateProgress(u(S).id,Math.floor(u(s).currentTime))},d=new n(u(s),{iconUrl:`/assets/plyr.svg`,blankVideo:`/assets/blank.mp4`,controls:[`play-large`,`play`,`progress`,`current-time`,`duration`,`mute`,`volume`,`captions`,`settings`,`pip`,`airplay`,`fullscreen`],settings:[`captions`,`speed`],speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2]},keyboard:{focused:!0,global:!0},captions:{active:!!u(S).subs,update:!0},seekTime:10});let l=d.play();l&&typeof l.catch==`function`&&l.catch(()=>{})}function E(){d&&=(d.destroy(),null),f&&=(f.destroy(),null)}function k(){E(),q.closeVideo()}var A=T(),M=a(A),I=e=>{var t=we(),i=g(t),a=g(i),o=g(a),d=g(o,!0);w(o);var f=l(o,2);ye(g(f),{size:18}),w(f),w(a);var _=l(a,2),v=g(_),x=e=>{var t=Ce(),r=g(t);V(r,{size:48,class:`text-amber`});var i=l(r,4),a=l(g(i)),o=g(a);w(a),O(),w(i);var s=l(i,2),c=g(s);Y(c,{size:18});var d=l(c);w(s),w(t),h((e,t)=>{n(o,`.${e??``}`),D(s,`href`,t),D(s,`download`,u(S).name),n(d,` Download ${u(S).name??``}`)},[()=>u(S).ext.toUpperCase(),()=>H(u(S).id)]),p(e,t)},C=e=>{var t=$();b(t,e=>c(s,e),()=>u(s)),p(e,t)};r(v,e=>{u(y)?e(x):e(C,-1)}),w(_),w(i),w(t),h(()=>n(d,u(S).info_title||u(S).title||u(S).name)),m(`click`,f,k),p(e,t)};r(M,e=>{q.isVideoOpen&&u(S)&&e(I)}),p(e,A),x()}f([`click`]);var Ee=e(`
Selected Media
`),De=e(`

Searching for DLNA, UPnP, or Chromecast devices on your network...

`),Oe=e(``),ke=e(`
`),Ae=e(`

Active Casting Session

Casting to

`),je=e(``);function Me(e,t){j(t,!0);let i=v(!1);async function o(e){U.targetMedia&&(c(i,!0),await U.startCast(e,U.targetMedia),c(i,!1))}var s=T(),f=a(s),_=e=>{var t=je(),a=g(t),s=g(a),c=g(s),f=g(c);ie(f,{size:22,class:`text-cyan`}),O(2),w(c);var _=l(c,2);ye(g(_),{size:18}),w(_),w(s);var v=l(s,2),y=g(v),b=e=>{var t=Ee(),r=l(g(t),2),i=g(r,!0);w(r),w(t),h(()=>n(i,U.targetMedia.info_title||U.targetMedia.title||U.targetMedia.name)),p(e,t)};r(y,e=>{U.targetMedia&&e(b)});var x=l(y,2),S=l(g(x),2),C=e=>{var t=De(),n=g(t);G(n,{size:32,class:`text-muted`});var r=l(n,4);w(t),m(`click`,r,()=>U.loadRenderers()),p(e,t)},T=e=>{var t=ke();M(t,21,()=>U.renderers,e=>e.id,(e,t)=>{var a=Oe(),s=g(a);W(s,{size:24,class:`text-cyan`});var c=l(s,2),f=g(c),_=g(f,!0);w(f);var v=l(f,2),y=g(v);w(v),w(c);var b=l(c,2),x=g(b),S=e=>{re(e,{size:14,class:`spinner`})},C=e=>{var t=d(`Cast Now`);p(e,t)};r(x,e=>{u(i)?e(S):e(C,-1)}),w(b),w(a),h(()=>{n(_,u(t).name),n(y,`${u(t).device_type??``} • ${u(t).ip??``}`)}),m(`click`,a,()=>o(u(t))),p(e,a)}),w(t),p(e,t)};r(S,e=>{U.renderers.length===0?e(C):e(T,-1)}),w(x);var E=l(x,2),D=e=>{var t=Ae(),r=l(g(t),2),i=l(g(r)),a=g(i,!0);w(i),w(r);var o=l(r,2),s=g(o),c=g(s);ee(c,{size:18}),w(s);var u=l(s,2);de(g(u),{size:18}),w(u);var d=l(u,2),f=g(d);R(f,{size:18}),w(d),w(o),w(t),h(()=>n(a,U.activeRenderer.name)),m(`click`,s,()=>U.control(`play`)),m(`click`,u,()=>U.control(`pause`)),m(`click`,d,()=>U.control(`stop`)),p(e,t)};r(E,e=>{U.isCasting&&U.activeRenderer&&e(D)}),w(v),w(a),w(t),m(`click`,_,()=>U.closeCastModal()),p(e,t)};r(f,e=>{U.isCastModalOpen&&e(_)}),p(e,s),x()}f([`click`]);var Ne=e(` Subtitles Available`),Pe=e(`

`),Fe=e(`

Overview / Synopsis

`),Ie=e(``),Le=e(``);function Re(e,t){j(t,!0);let o=_(()=>K.selectedItem);function c(e){if(!e)return`Unknown`;let t=Math.floor(e/3600),n=Math.floor(e%3600/60);return t>0?`${t}h ${n}m`:`${n}m`}function f(){K.selectItem(null)}function v(){u(o)&&(f(),u(o).cat===`audio`||u(o).cat===`radio`?q.playAudio(u(o),K.visibleFiles):q.openVideo(u(o)))}function y(){u(o)&&(f(),U.openCastModal(u(o)))}var b=T(),S=a(b),C=e=>{var t=Le(),a=g(t),_=g(a);ye(g(_),{size:18}),w(_);var b=l(_,2),x=g(b),S=l(x,4),C=g(S),T=l(C,2),E=g(T),k=g(E),A=g(k,!0);w(k);var j=l(k,2),M=g(j,!0);w(j);var N=l(j,2),P=e=>{var t=Ne(),n=g(t);B(n,{size:12}),O(),w(t),p(e,t)};r(N,e=>{u(o).subs&&e(P)}),w(E);var F=l(E,2),L=g(F,!0);w(F);var R=l(F,2),te=e=>{var t=Pe(),i=g(t),a=l(i),s=e=>{var t=d();h(()=>n(t,`— ${u(o).album??``}`)),p(e,t)};r(a,e=>{u(o).album&&e(s)}),w(t),h(()=>n(i,`${u(o).artist??``} `)),p(e,t)};r(R,e=>{u(o).artist&&e(te)}),w(T),w(S),w(b);var z=l(b,2),V=g(z),re=e=>{var t=Fe(),r=l(g(t),2),i=g(r,!0);w(r),w(t),h(()=>n(i,u(o).info_overview)),p(e,t)};r(V,e=>{u(o).info_overview&&e(re)});var U=l(V,2),ae=l(g(U),2),G=g(ae),K=g(G);oe(K,{size:16,class:`meta-icon`});var se=l(K,2),q=l(g(se),2),ce=g(q,!0);w(q),w(se),w(G);var J=l(G,2),X=g(J);ne(X,{size:16,class:`meta-icon`});var ue=l(X,2),de=l(g(ue),2),fe=g(de,!0);w(de),w(ue),w(J);var pe=l(J,2),me=g(pe);le(me,{size:16,class:`meta-icon`});var he=l(me,2),ge=l(g(he),2),_e=g(ge,!0);w(ge),w(he),w(pe),w(ae),w(U);var ve=l(U,2),Z=g(ve),be=g(Z);ee(be,{size:18,fill:`currentColor`}),O(),w(Z);var xe=l(Z,2),Se=e=>{var t=Ie(),n=g(t);W(n,{size:18}),O(),w(t),m(`click`,t,v),p(e,t)};r(xe,e=>{u(o).cat===`video`&&e(Se)});var Q=l(xe,2),Ce=g(Q);ie(Ce,{size:18}),O(),w(Q);var $=l(Q,2);Y(g($),{size:18}),O(),w($),w(ve),w(z),w(a),w(t),h((e,t,r,i,a,s)=>{D(x,`src`,e),D(C,`src`,t),D(C,`alt`,u(o).name),n(A,r),n(M,i),n(L,u(o).info_title||u(o).title||u(o).name),n(ce,a),n(fe,u(o).size_str),n(_e,u(o).mime),D($,`href`,s),D($,`download`,u(o).name)},[()=>I(u(o).id),()=>I(u(o).id),()=>u(o).cat.toUpperCase(),()=>u(o).ext.toUpperCase(),()=>c(u(o).dur),()=>H(u(o).id)]),m(`click`,_,f),i(`error`,x,e=>{e.target.style.display=`none`}),s(x),i(`error`,C,e=>{e.target.src=`data:image/svg+xml,`}),s(C),m(`click`,Z,v),m(`click`,Q,y),p(e,t)};r(S,e=>{u(o)&&e(C)}),p(e,b),x()}f([`click`]);var ze=e(``);function Be(e,t){j(t,!0);let i=_(()=>K.selectedItem),o=_(()=>u(i)?.cat===`image`);function s(){K.selectItem(null)}var c=T(),d=a(c),f=e=>{var t=ze(),r=g(t),a=g(r),o=g(a),c=g(o);te(c,{size:20,class:`text-cyan`});var d=l(c,2),f=g(d,!0);w(d);var _=l(d,2),v=g(_);w(_),w(o);var y=l(o,2),b=g(y);Y(g(b),{size:18}),w(b);var x=l(b,2);ye(g(x),{size:18}),w(x),w(y),w(a);var S=l(a,2),C=g(S);w(S),w(r),w(t),h((e,t,r)=>{n(f,u(i).info_title||u(i).title||u(i).name),n(v,`${e??``} • ${u(i).size_str??``}`),D(b,`href`,t),D(b,`download`,u(i).name),D(C,`src`,r),D(C,`alt`,u(i).name)},[()=>u(i).ext.toUpperCase(),()=>H(u(i).id),()=>H(u(i).id)]),m(`click`,x,s),p(e,t)};r(d,e=>{u(i)&&u(o)&&e(f)}),p(e,c),x()}f([`click`]);var Ve=e(`
`);function He(e,n){var r=Ve(),i=g(r);t(i,()=>n.children);var a=l(i,2);Q(a,{});var o=l(a,2);Te(o,{});var s=l(o,2);Me(s,{});var c=l(s,2);Re(c,{}),Be(l(c,2),{}),w(r),p(e,r)}export{He as component,ce as universal}; \ No newline at end of file + your web browser. You can download the raw media file below.

`),$=e(``,2),we=e(`
`);function Te(e,t){j(t,!0);let i=new Set([`avi`,`wmv`,`flv`,`mpg`,`mpeg`]),s=v(null),d=null,f=null,y=v(!1),S=_(()=>q.activeVideoItem);o(()=>{if(u(S)&&u(s)&&q.isVideoOpen){let e=(u(S).ext||``).toLowerCase();if(i.has(e)){c(y,!0);return}c(y,!1),C()}return()=>{E()}});async function C(){if(!u(S)||!u(s)||typeof window>`u`)return;E();let[e,t]=await Promise.all([N(()=>import(`../chunks/BySXlKnG.js`),[],import.meta.url),N(()=>import(`../chunks/DFbhk10t.js`),[],import.meta.url)]),n=e.default||e,r=t.default||t,i=H(u(S).id),a=(u(S).ext||``).toLowerCase()===`mkv`||i.endsWith(`.m3u8`),o=a?L(u(S).id):i;if(u(S).subs){let e=document.createElement(`track`);e.kind=`subtitles`,e.srclang=`en`,e.label=`English`,e.src=P(u(S).id),e.default=!0,u(s).appendChild(e)}if(a){if(r.isSupported()){f=new r({enableWorker:!0});let ut=()=>{try{let tr=f.audioTracks||[];let h=document.querySelector('.video-modal-header');if(h&&tr.length>1){let s=h.querySelector('.vuio-audio-sel');if(!s){s=document.createElement('select');s.className='vuio-audio-sel';s.style.cssText='margin-left:auto;margin-right:1rem;background:rgba(15,23,42,0.85);color:#f8fafc;border:1px solid rgba(255,255,255,0.15);border-radius:6px;padding:0.25rem 0.6rem;font-size:0.82rem;font-weight:500;cursor:pointer;outline:none;max-width:50%;box-shadow:0 2px 8px rgba(0,0,0,0.3);';h.insertBefore(s,h.querySelector('button'));s.onchange=()=>{if(f)f.audioTrack=Number(s.value);};}s.replaceChildren(...tr.map((t,i)=>{let o=document.createElement('option');o.value=String(i);o.textContent=t.name||t.lang||('Audio Track '+(i+1));o.selected=(i===f.audioTrack);return o;}));}}catch(e){}};f.on(r.Events.MANIFEST_PARSED,ut);f.on(r.Events.AUDIO_TRACKS_UPDATED,ut);f.on(r.Events.AUDIO_TRACK_SWITCHED,(e,d)=>{let s=document.querySelector('.vuio-audio-sel');if(s&&typeof d.id==='number')s.value=String(d.id);});f.loadSource(o);f.attachMedia(u(s));}else if(u(s).canPlayType(`application/vnd.apple.mpegurl`))u(s).src=o;else{c(y,!0);return}}else u(s).src=o;u(s).ontimeupdate=()=>{u(S)&&u(s)&&u(s).currentTime>2&&F.updateProgress(u(S).id,Math.floor(u(s).currentTime))},d=new n(u(s),{iconUrl:`/assets/plyr.svg`,blankVideo:`/assets/blank.mp4`,controls:[`play-large`,`play`,`progress`,`current-time`,`duration`,`mute`,`volume`,`captions`,`settings`,`pip`,`airplay`,`fullscreen`],settings:[`captions`,`speed`],speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2]},keyboard:{focused:!0,global:!0},captions:{active:!!u(S).subs,update:!0},seekTime:10});let l=d.play();l&&typeof l.catch==`function`&&l.catch(()=>{})}function E(){d&&=(d.destroy(),null),f&&=(f.destroy(),null)}function k(){E(),q.closeVideo()}var A=T(),M=a(A),I=e=>{var t=we(),i=g(t),a=g(i),o=g(a),d=g(o,!0);w(o);var f=l(o,2);ye(g(f),{size:18}),w(f),w(a);var _=l(a,2),v=g(_),x=e=>{var t=Ce(),r=g(t);V(r,{size:48,class:`text-amber`});var i=l(r,4),a=l(g(i)),o=g(a);w(a),O(),w(i);var s=l(i,2),c=g(s);Y(c,{size:18});var d=l(c);w(s),w(t),h((e,t)=>{n(o,`.${e??``}`),D(s,`href`,t),D(s,`download`,u(S).name),n(d,` Download ${u(S).name??``}`)},[()=>u(S).ext.toUpperCase(),()=>H(u(S).id)]),p(e,t)},C=e=>{var t=$();b(t,e=>c(s,e),()=>u(s)),p(e,t)};r(v,e=>{u(y)?e(x):e(C,-1)}),w(_),w(i),w(t),h(()=>n(d,u(S).info_title||u(S).title||u(S).name)),m(`click`,f,k),p(e,t)};r(M,e=>{q.isVideoOpen&&u(S)&&e(I)}),p(e,A),x()}f([`click`]);var Ee=e(`
Selected Media
`),De=e(`

Searching for DLNA, UPnP, or Chromecast devices on your network...

`),Oe=e(``),ke=e(`
`),Ae=e(`

Active Casting Session

Casting to

`),je=e(``);function Me(e,t){j(t,!0);let i=v(!1);async function o(e){U.targetMedia&&(c(i,!0),await U.startCast(e,U.targetMedia),c(i,!1))}var s=T(),f=a(s),_=e=>{var t=je(),a=g(t),s=g(a),c=g(s),f=g(c);ie(f,{size:22,class:`text-cyan`}),O(2),w(c);var _=l(c,2);ye(g(_),{size:18}),w(_),w(s);var v=l(s,2),y=g(v),b=e=>{var t=Ee(),r=l(g(t),2),i=g(r,!0);w(r),w(t),h(()=>n(i,U.targetMedia.info_title||U.targetMedia.title||U.targetMedia.name)),p(e,t)};r(y,e=>{U.targetMedia&&e(b)});var x=l(y,2),S=l(g(x),2),C=e=>{var t=De(),n=g(t);G(n,{size:32,class:`text-muted`});var r=l(n,4);w(t),m(`click`,r,()=>U.loadRenderers()),p(e,t)},T=e=>{var t=ke();M(t,21,()=>U.renderers,e=>e.id,(e,t)=>{var a=Oe(),s=g(a);W(s,{size:24,class:`text-cyan`});var c=l(s,2),f=g(c),_=g(f,!0);w(f);var v=l(f,2),y=g(v);w(v),w(c);var b=l(c,2),x=g(b),S=e=>{re(e,{size:14,class:`spinner`})},C=e=>{var t=d(`Cast Now`);p(e,t)};r(x,e=>{u(i)?e(S):e(C,-1)}),w(b),w(a),h(()=>{n(_,u(t).name),n(y,`${u(t).device_type??``} • ${u(t).ip??``}`)}),m(`click`,a,()=>o(u(t))),p(e,a)}),w(t),p(e,t)};r(S,e=>{U.renderers.length===0?e(C):e(T,-1)}),w(x);var E=l(x,2),D=e=>{var t=Ae(),r=l(g(t),2),i=l(g(r)),a=g(i,!0);w(i),w(r);var o=l(r,2),s=g(o),c=g(s);ee(c,{size:18}),w(s);var u=l(s,2);de(g(u),{size:18}),w(u);var d=l(u,2),f=g(d);R(f,{size:18}),w(d),w(o),w(t),h(()=>n(a,U.activeRenderer.name)),m(`click`,s,()=>U.control(`play`)),m(`click`,u,()=>U.control(`pause`)),m(`click`,d,()=>U.control(`stop`)),p(e,t)};r(E,e=>{U.isCasting&&U.activeRenderer&&e(D)}),w(v),w(a),w(t),m(`click`,_,()=>U.closeCastModal()),p(e,t)};r(f,e=>{U.isCastModalOpen&&e(_)}),p(e,s),x()}f([`click`]);var Ne=e(` Subtitles Available`),Pe=e(`

`),Fe=e(`

Overview / Synopsis

`),Ie=e(``),Le=e(``);function Re(e,t){j(t,!0);let o=_(()=>K.selectedItem);function c(e){if(!e)return`Unknown`;let t=Math.floor(e/3600),n=Math.floor(e%3600/60);return t>0?`${t}h ${n}m`:`${n}m`}function f(){K.selectItem(null)}function v(){u(o)&&(f(),u(o).cat===`audio`||u(o).cat===`radio`?q.playAudio(u(o),K.visibleFiles):q.openVideo(u(o)))}function y(){u(o)&&(f(),U.openCastModal(u(o)))}var b=T(),S=a(b),C=e=>{var t=Le(),a=g(t),_=g(a);ye(g(_),{size:18}),w(_);var b=l(_,2),x=g(b),S=l(x,4),C=g(S),T=l(C,2),E=g(T),k=g(E),A=g(k,!0);w(k);var j=l(k,2),M=g(j,!0);w(j);var N=l(j,2),P=e=>{var t=Ne(),n=g(t);B(n,{size:12}),O(),w(t),p(e,t)};r(N,e=>{u(o).subs&&e(P)}),w(E);var F=l(E,2),L=g(F,!0);w(F);var R=l(F,2),te=e=>{var t=Pe(),i=g(t),a=l(i),s=e=>{var t=d();h(()=>n(t,`— ${u(o).album??``}`)),p(e,t)};r(a,e=>{u(o).album&&e(s)}),w(t),h(()=>n(i,`${u(o).artist??``} `)),p(e,t)};r(R,e=>{u(o).artist&&e(te)}),w(T),w(S),w(b);var z=l(b,2),V=g(z),re=e=>{var t=Fe(),r=l(g(t),2),i=g(r,!0);w(r),w(t),h(()=>n(i,u(o).info_overview)),p(e,t)};r(V,e=>{u(o).info_overview&&e(re)});var U=l(V,2),ae=l(g(U),2),G=g(ae),K=g(G);oe(K,{size:16,class:`meta-icon`});var se=l(K,2),q=l(g(se),2),ce=g(q,!0);w(q),w(se),w(G);var J=l(G,2),X=g(J);ne(X,{size:16,class:`meta-icon`});var ue=l(X,2),de=l(g(ue),2),fe=g(de,!0);w(de),w(ue),w(J);var pe=l(J,2),me=g(pe);le(me,{size:16,class:`meta-icon`});var he=l(me,2),ge=l(g(he),2),_e=g(ge,!0);w(ge),w(he),w(pe),w(ae),w(U);var ve=l(U,2),Z=g(ve),be=g(Z);ee(be,{size:18,fill:`currentColor`}),O(),w(Z);var xe=l(Z,2),Se=e=>{var t=Ie(),n=g(t);W(n,{size:18}),O(),w(t),m(`click`,t,v),p(e,t)};r(xe,e=>{u(o).cat===`video`&&e(Se)});var Q=l(xe,2),Ce=g(Q);ie(Ce,{size:18}),O(),w(Q);var $=l(Q,2);Y(g($),{size:18}),O(),w($),w(ve),w(z),w(a),w(t),h((e,t,r,i,a,s)=>{D(x,`src`,e),D(C,`src`,t),D(C,`alt`,u(o).name),n(A,r),n(M,i),n(L,u(o).info_title||u(o).title||u(o).name),n(ce,a),n(fe,u(o).size_str),n(_e,u(o).mime),D($,`href`,s),D($,`download`,u(o).name)},[()=>I(u(o).id),()=>I(u(o).id),()=>u(o).cat.toUpperCase(),()=>u(o).ext.toUpperCase(),()=>c(u(o).dur),()=>H(u(o).id)]),m(`click`,_,f),i(`error`,x,e=>{e.target.style.display=`none`}),s(x),i(`error`,C,e=>{e.target.src=`data:image/svg+xml,`}),s(C),m(`click`,Z,v),m(`click`,Q,y),p(e,t)};r(S,e=>{u(o)&&e(C)}),p(e,b),x()}f([`click`]);var ze=e(``);function Be(e,t){j(t,!0);let i=_(()=>K.selectedItem),o=_(()=>u(i)?.cat===`image`);function s(){K.selectItem(null)}var c=T(),d=a(c),f=e=>{var t=ze(),r=g(t),a=g(r),o=g(a),c=g(o);te(c,{size:20,class:`text-cyan`});var d=l(c,2),f=g(d,!0);w(d);var _=l(d,2),v=g(_);w(_),w(o);var y=l(o,2),b=g(y);Y(g(b),{size:18}),w(b);var x=l(b,2);ye(g(x),{size:18}),w(x),w(y),w(a);var S=l(a,2),C=g(S);w(S),w(r),w(t),h((e,t,r)=>{n(f,u(i).info_title||u(i).title||u(i).name),n(v,`${e??``} • ${u(i).size_str??``}`),D(b,`href`,t),D(b,`download`,u(i).name),D(C,`src`,r),D(C,`alt`,u(i).name)},[()=>u(i).ext.toUpperCase(),()=>H(u(i).id),()=>H(u(i).id)]),m(`click`,x,s),p(e,t)};r(d,e=>{u(i)&&u(o)&&e(f)}),p(e,c),x()}f([`click`]);var Ve=e(`
`);function He(e,n){var r=Ve(),i=g(r);t(i,()=>n.children);var a=l(i,2);Q(a,{});var o=l(a,2);Te(o,{});var s=l(o,2);Me(s,{});var c=l(s,2);Re(c,{}),Be(l(c,2),{}),w(r),p(e,r)}export{He as component,ce as universal}; \ No newline at end of file From d037259d04c998afc2ce54221d6d2c972f43f537 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 19:02:22 +0300 Subject: [PATCH 14/38] audio fix --- Cargo.toml | 32 +- crates/vuio-bench/src/aac_bench.rs | 50 ++- .../vuio-core/src/media/remux/mkv_demuxer.rs | 54 ++- crates/vuio-core/src/media/transcode/aac.rs | 10 +- .../src/media/transcode/rendition.rs | 6 +- crates/vuio-core/src/web/remux_streaming.rs | 26 +- .../vuio-core/tests/film_transcode_tests.rs | 9 +- phas4plan.txt | 333 ------------------ 8 files changed, 130 insertions(+), 390 deletions(-) delete mode 100644 phas4plan.txt diff --git a/Cargo.toml b/Cargo.toml index 94d52f33..30ac22ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,14 +32,36 @@ default-members = [ ] resolver = "2" -# The vendored codecs are the only CPU-bound code in the tree. Optimizing those -# packages alone leaves `cargo build` on the code actually being worked on as fast as it was. +# Optimizing codecs, encoders, and demuxers in dev profile gives fast real-time +# audio decoding and encoding without requiring release rebuilds. [profile.dev.package.oxideav-core] -opt-level = 2 +opt-level = 3 [profile.dev.package.oxideav-ac3] -opt-level = 2 +opt-level = 3 [profile.dev.package.oxideav-dts] -opt-level = 2 +opt-level = 3 +[profile.dev.package.xaac-rs] +opt-level = 3 +[profile.dev.package.libxaac-sys] +opt-level = 3 +[profile.dev.package.symphonia] +opt-level = 3 +[profile.dev.package.symphonia-core] +opt-level = 3 +[profile.dev.package.symphonia-format-mkv] +opt-level = 3 +[profile.dev.package.symphonia-format-riff] +opt-level = 3 +[profile.dev.package.symphonia-format-isomp4] +opt-level = 3 +[profile.dev.package.symphonia-bundle-flac] +opt-level = 3 +[profile.dev.package.symphonia-bundle-mp3] +opt-level = 3 +[profile.dev.package.symphonia-codec-aac] +opt-level = 3 +[profile.dev.package.symphonia-codec-pcm] +opt-level = 3 [profile.release] opt-level = 3 diff --git a/crates/vuio-bench/src/aac_bench.rs b/crates/vuio-bench/src/aac_bench.rs index 93dc93d7..eb343ff8 100644 --- a/crates/vuio-bench/src/aac_bench.rs +++ b/crates/vuio-bench/src/aac_bench.rs @@ -7,8 +7,10 @@ use std::time::{Duration, Instant}; use vuio_core::media::transcode::{PcmDecoder, TranscodeCodec}; -const AC3_FIXTURE: &[u8] = include_bytes!("../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); -const DTS_FIXTURE: &[u8] = include_bytes!("../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); +const AC3_FIXTURE: &[u8] = + include_bytes!("../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); +const DTS_FIXTURE: &[u8] = + include_bytes!("../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); fn generate_sine_wave(sample_rate: u32, channels: u16, duration_secs: f64) -> Vec { let total_samples = (sample_rate as f64 * duration_secs) as usize; @@ -44,8 +46,9 @@ fn bench_ac3_decode(iterations: usize) -> (Duration, f64, usize) { let first_frame = &AC3_FIXTURE[..frame_len]; let start = Instant::now(); - let (mut decoder, first_pcm) = PcmDecoder::open(TranscodeCodec::Ac3, 48000, Some(2), first_frame) - .expect("open AC-3 decoder"); + let (mut decoder, first_pcm) = + PcmDecoder::open(TranscodeCodec::Ac3, 48000, Some(2), first_frame) + .expect("open AC-3 decoder"); let mut total_pcm_bytes = first_pcm.len(); let mut total_samples = 1536; // first frame samples @@ -92,12 +95,20 @@ fn bench_eac3_decode(iterations: usize) -> (Duration, f64, usize) { while let Ok(pkt) = enc.receive_packet() { eac3_packets.push(pkt.data); } - assert!(!eac3_packets.is_empty(), "E-AC-3 encoder produced no packets"); + assert!( + !eac3_packets.is_empty(), + "E-AC-3 encoder produced no packets" + ); let first_frame = &eac3_packets[0]; let start = Instant::now(); - let (mut decoder, first_pcm) = PcmDecoder::open(TranscodeCodec::Eac3, sample_rate, Some(channels), first_frame) - .expect("open E-AC-3 decoder"); + let (mut decoder, first_pcm) = PcmDecoder::open( + TranscodeCodec::Eac3, + sample_rate, + Some(channels), + first_frame, + ) + .expect("open E-AC-3 decoder"); let mut total_pcm_bytes = first_pcm.len(); let mut total_samples = 1536; @@ -139,8 +150,9 @@ fn bench_dts_decode(iterations: usize) -> (Duration, f64, usize) { let first_frame = frames[0]; let start = Instant::now(); - let (mut decoder, first_pcm) = PcmDecoder::open(TranscodeCodec::Dts, 48000, Some(2), first_frame) - .expect("open DTS decoder"); + let (mut decoder, first_pcm) = + PcmDecoder::open(TranscodeCodec::Dts, 48000, Some(2), first_frame) + .expect("open DTS decoder"); let mut total_pcm_bytes = first_pcm.len(); let mut total_samples = 512; @@ -292,7 +304,11 @@ mod apple_native { 0 } - pub fn bench_apple(pcm_bytes: &[u8], sample_rate: u32, channels: u16) -> (std::time::Duration, usize) { + pub fn bench_apple( + pcm_bytes: &[u8], + sample_rate: u32, + channels: u16, + ) -> (std::time::Duration, usize) { let start = std::time::Instant::now(); let in_format = AudioStreamBasicDescription { @@ -370,7 +386,11 @@ mod apple_native { } } -fn bench_xaac(pcm_bytes: &[u8], sample_rate: u32, channels: u16) -> Result<(Duration, usize), String> { +fn bench_xaac( + pcm_bytes: &[u8], + sample_rate: u32, + channels: u16, +) -> Result<(Duration, usize), String> { let start = Instant::now(); use xaac_rs::{Encoder, EncoderConfig, OutputFormat, Profile}; @@ -387,10 +407,14 @@ fn bench_xaac(pcm_bytes: &[u8], sample_rate: u32, channels: u16) -> Result<(Dura let mut out_len = 0; for chunk in pcm_bytes.chunks(frame_bytes) { if chunk.len() == frame_bytes { - let encoded = encoder.encode_pcm_bytes(chunk).map_err(|e| format!("{e:?}"))?; + let encoded = encoder + .encode_pcm_bytes(chunk) + .map_err(|e| format!("{e:?}"))?; out_len += encoded.data.len(); } else { - let encoded = encoder.encode_pcm_bytes_with_padding(chunk).map_err(|e| format!("{e:?}"))?; + let encoded = encoder + .encode_pcm_bytes_with_padding(chunk) + .map_err(|e| format!("{e:?}"))?; out_len += encoded.packet.data.len(); } } diff --git a/crates/vuio-core/src/media/remux/mkv_demuxer.rs b/crates/vuio-core/src/media/remux/mkv_demuxer.rs index de9e3a63..4c26d688 100644 --- a/crates/vuio-core/src/media/remux/mkv_demuxer.rs +++ b/crates/vuio-core/src/media/remux/mkv_demuxer.rs @@ -359,8 +359,11 @@ impl MkvDemuxer { }, ); + let start_ticks = + (start_secs.max(0.0) * output_timescale as f64).round() as u64; let target_ticks = (target_duration_secs.max(0.0) * output_timescale as f64).round() as u64; + let is_video = matches!(codec, TrackCodec::Avc | TrackCodec::Hevc); let mut packets = Vec::new(); let mut accumulated_ticks: u64 = 0; @@ -392,26 +395,39 @@ impl MkvDemuxer { let dts = rescale(packet.dts.get()); let dur = rescale(packet.dur.get() as i64); let is_keyframe = packet_is_keyframe(&packet.data, codec); - // Guarantee the segment opens on a random-access point even where - // the container's cue index is sparse enough that the seek above - // landed mid-GOP. Dropping these leading frames loses nothing: they - // depend on references the player would not have when starting here, - // and the previous segment already covers their span. - if packets.is_empty() && !is_keyframe { - continue; - } - // Matroska stores no per-block duration: a `SimpleBlock` is a - // timestamp and a payload, and symphonia can only report a - // duration where the track declares `DefaultDuration` or the - // codec implies one. Accumulating durations alone therefore - // runs to the packet ceiling on any track that declares - // neither — which is a segment holding the whole film. The - // elapsed presentation time is the check that does not depend - // on the container being generous. - let elapsed = pts.saturating_sub(*first_pts.get_or_insert(pts)); - if elapsed >= target_ticks && !packets.is_empty() { - break; + + if is_video { + // Guarantee the segment opens on a random-access point even where + // the container's cue index is sparse enough that the seek above + // landed mid-GOP. Dropping these leading frames loses nothing: they + // depend on references the player would not have when starting here, + // and the previous segment already covers their span. + if packets.is_empty() && !is_keyframe { + continue; + } + // Matroska stores no per-block duration: a `SimpleBlock` is a + // timestamp and a payload, and symphonia can only report a + // duration where the track declares `DefaultDuration` or the + // codec implies one. Accumulating durations alone therefore + // runs to the packet ceiling on any track that declares + // neither — which is a segment holding the whole film. The + // elapsed presentation time is the check that does not depend + // on the container being generous. + let elapsed = pts.saturating_sub(*first_pts.get_or_insert(pts)); + if elapsed >= target_ticks && !packets.is_empty() { + break; + } + } else { + // For audio tracks: discard packets that belong before this segment's start time + // so coarse seeking does not replay packets from the previous segment. + if (dur > 0 && pts + dur <= start_ticks) || (dur == 0 && pts < start_ticks) { + continue; + } + if pts >= start_ticks + target_ticks && !packets.is_empty() { + break; + } } + accumulated_ticks += dur; packets.push(MediaPacket { track_id: packet.track_id, diff --git a/crates/vuio-core/src/media/transcode/aac.rs b/crates/vuio-core/src/media/transcode/aac.rs index 84393fcf..392091a4 100644 --- a/crates/vuio-core/src/media/transcode/aac.rs +++ b/crates/vuio-core/src/media/transcode/aac.rs @@ -32,11 +32,19 @@ impl AacEncoder { anyhow::bail!("unsupported channel count for AAC: {channels}"); } + let bitrate = match channels { + 1 => 96_000, + 2 => 192_000, + 6 => 384_000, + 8 => 512_000, + _ => 96_000 * u32::from(channels), + }; + let config = xaac_rs::EncoderConfig { profile: xaac_rs::Profile::AacLc, sample_rate, channels, - bitrate: 64_000 * u32::from(channels), + bitrate, output_format: xaac_rs::OutputFormat::Adts, ..Default::default() }; diff --git a/crates/vuio-core/src/media/transcode/rendition.rs b/crates/vuio-core/src/media/transcode/rendition.rs index 247619dd..8c0e2f10 100644 --- a/crates/vuio-core/src/media/transcode/rendition.rs +++ b/crates/vuio-core/src/media/transcode/rendition.rs @@ -62,12 +62,8 @@ pub fn reencode_to_aac( } adts.extend_from_slice(&encoder.finish()); - // One frame early, to cancel the encoder's delay. Clamped at zero for a run - // that already starts at the beginning of the film, where there is no - // earlier timeline to move onto — the residual lag there is one frame, - // twenty-one milliseconds at 48 kHz. let base = nominal_start_pts.unwrap_or(first.pts); - let mut dts = base.saturating_sub(AAC_FRAME_SAMPLES); + let mut dts = base; let mut out = Vec::new(); for payload in super::adts_payloads(&adts) { out.push(MediaPacket { diff --git a/crates/vuio-core/src/web/remux_streaming.rs b/crates/vuio-core/src/web/remux_streaming.rs index 1d9018fb..ef6bbe0f 100644 --- a/crates/vuio-core/src/web/remux_streaming.rs +++ b/crates/vuio-core/src/web/remux_streaming.rs @@ -200,8 +200,24 @@ pub async fn serve_hls_audio_init_segment( fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> Vec { let out_track = rendition_track(track); let timescale = Fmp4Writer::timescale_for(&out_track); - let start_secs = seq as f64 * SEGMENT_DURATION_SECS as f64; - let nominal_decode_time = (start_secs * timescale as f64).round() as u64; + let is_reencoded_audio = track.codec_kind.transcode_codec().is_some(); + + // For re-encoded audio (AC-3/DTS -> AAC), align segment durations to integer AAC frames (1024 samples) + // so no segment ends on a fractional frame or requires zero-padding silence. + let (start_secs, target_duration_secs, nominal_decode_time) = if is_reencoded_audio { + let sample_rate = out_track.sample_rate.unwrap_or(48_000); + let frames_per_seg = + ((SEGMENT_DURATION_SECS as f64 * sample_rate as f64) / 1024.0).round() as u64; + let seg_samples = frames_per_seg * 1024; + let start_sample = seq as u64 * seg_samples; + let start_s = start_sample as f64 / sample_rate as f64; + let dur_s = seg_samples as f64 / sample_rate as f64; + (start_s, dur_s, start_sample) + } else { + let start_s = seq as f64 * SEGMENT_DURATION_SECS as f64; + let nominal = (start_s * timescale as f64).round() as u64; + (start_s, SEGMENT_DURATION_SECS as f64, nominal) + }; let packets = MkvDemuxer::extract_track_packets( path, @@ -209,7 +225,7 @@ fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> V track.codec_kind, timescale, start_secs, - SEGMENT_DURATION_SECS as f64, + target_duration_secs, ) .unwrap_or_default(); @@ -227,10 +243,6 @@ fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> V None => packets, }; - // Seeking lands at (or before) `start_secs`, not exactly on it, so the fragment's - // base decode time comes from the packets themselves (`build_segment` takes it from - // the first one's decode timestamp). The nominal `seq`-derived position is only a - // fallback for a segment that came back empty. Fmp4Writer::build_segment(seq + 1, &out_track, nominal_decode_time, &packets) } diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs index 12e710cf..d97ddad5 100644 --- a/crates/vuio-core/tests/film_transcode_tests.rs +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -481,15 +481,10 @@ async fn an_audio_segment_carries_real_re_encoded_aac() { "ADTS framing leaked into an MP4 sample" ); - // The second segment begins four seconds in, one AAC frame early to cancel - // the encoder's delay. + // The second segment begins four seconds in, matching the AAC frame-aligned decode time. let tfdt = find_box(&segment, "tfdt").expect("a tfdt"); let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); - assert_eq!( - base, - 4 * 48_000 - 1024, - "the run must sit one frame before the segment boundary" - ); + assert_eq!(base, 188 * 1024, "the run sits at the segment boundary"); } #[tokio::test] diff --git a/phas4plan.txt b/phas4plan.txt deleted file mode 100644 index debd18a5..00000000 --- a/phas4plan.txt +++ /dev/null @@ -1,333 +0,0 @@ -================================================================================ -PHASE 4 — AC-3 / E-AC-3 / DTS INSIDE FILMS -Video files with undecodable audio tracks, for TVs that play them silently -================================================================================ - -CONTEXT - -Phases 1-3 (branch feat/transcode-ac3-dts, 6 commits) shipped the decode core -and made it reachable for *elementary* streams: a standalone .ac3/.eac3/.dts -file is indexed, offered as a second , and served as LPCM or AAC. - -That is the foundation, not the payoff. The common shape of the problem is a -film — Movie.mkv with an AC-3 or DTS track — where the TV shows the picture and -produces no sound. Phase 4 is what actually fixes that. - -The seam is already marked in the code. media/remux/mkv_demuxer.rs:14-27: - - /// This is a passthrough remuxer, not a transcoder: everything here is real - /// content Symphonia can identify (E-AC-3/AC-3/DTS/TrueHD audio, VP9/AV1 - /// video, ...), but with no decoder/encoder in the pipeline those tracks are - /// marked `Unsupported` and left out of what gets offered to the browser - /// rather than shipped as a broken stream. - pub enum TrackCodec { Avc, Hevc, Aac, #[default] Unsupported } - -There is now a decoder in the pipeline. That comment is the work order. - - --------------------------------------------------------------------------------- -WHAT ALREADY EXISTS AND IS REUSED --------------------------------------------------------------------------------- - -From phases 1-3 (all shared, no changes needed): - - media/transcode/mod.rs TranscodeCodec, make_decoder(), is_decodable() - media/transcode/pcm.rs PcmDecoder — packets in, interleaved S16 out - media/transcode/aac.rs AacEncoder — S16 in, ADTS AAC-LC out - media/transcode/session.rs TranscodeState: plan cache + concurrency ceiling - web/mod.rs item_needs_transcode(codec, mime, filename) - — already consults MediaFileView::codec() FIRST, - which is exactly what a container track needs - web/mod.rs transcode_advert() -> Option - web/xml/rendering.rs TranscodeAdvert::write / write_didl - config [transcode] enabled/audio_format/prefer/max_concurrent - -Already in the repo, from the existing browser remux path: - - media/remux/mkv_demuxer.rs MkvDemuxer::inspect(), extract_track_packets() - media/remux/fmp4_writer.rs Fmp4Writer::build_segment() — 849 lines, - writes avcC / hvcC / mp4a+esds (:225-365) - media/remux/hls.rs master + media playlist generation - web/remux_streaming.rs /media/{id}/hls/* handlers (199 lines) - web/radio.rs:141-171 the async_stream chunked-body pattern - -NOT reused, and why: - - oxideav-mkv / oxideav-mp4 muxers: oxideav's Muxer trait requires - Write + Seek, so it cannot write into an HTTP body. Fmp4Writer already - produces fragmented MP4, which needs neither seek nor a known length. - - oxideav-mpegts: demuxer only. There is no TS muxer in the family. - => Phase 4 vendors NOTHING NEW. All four crates are already in crates/vendor. - - --------------------------------------------------------------------------------- -STEP 1 — THE DATABASE PREREQUISITE (do this first; everything blocks on it) --------------------------------------------------------------------------------- - -Deciding "does this film need a decoded alternative?" must be a DB read. Probing -files while rendering a browse page is not an option — a folder of 400 films -would open 400 files per Browse request. - -Two existing gaps make that impossible today: - -1. platform/filesystem/metadata.rs:184-204 derives stream.codec from - - symphonia::default::get_codecs().get_audio_decoder(audio.codec) - .map(|registered| registered.codec.info.short_name.to_owned()) - - which returns None for any codec symphonia cannot DECODE. So an AC-3 track - stores a NULL codec even though symphonia identified it perfectly well. - => Add a CodecId -> &'static str fallback map covering at least - CODEC_ID_AC3 -> "ac3", CODEC_ID_EAC3 -> "eac3", CODEC_ID_DCA -> "dca", - CODEC_ID_TRUEHD -> "truehd". Note truehd is recorded but NOT decodable — - TranscodeCodec::from_stored_codec already returns None for it, and must - keep doing so. - -2. media/scanner.rs:737-739 only probes audio files: - - if media_file.mime_type.starts_with("audio/") { - let _ = crate::platform::filesystem::extract_audio_metadata(...).await; - } - - Video files get no stream info at all. - => Extend to video/*, capturing the audio track's codec id only. This is a - header probe, not a decode. Measure the added scan time on a real library - and record it in the commit message — this is the one place phase 4 makes - every user pay something. - -3. Bump TAGS_VERSION (metadata.rs:40) so existing libraries re-probe on the next - scan rather than sitting on NULL codecs forever. - -Test: a scanned MKV with an AC-3 track has codec == "ac3" in media_files, and -item_needs_transcode() returns true for it without opening the file. - -Once this lands, the advertising from phase 3 starts firing for films with -no further change — which is why the URL it points at must exist first. Sequence -Step 1 and Step 3 in the same PR, or gate on TranscodeCodec::is_decodable(). - - --------------------------------------------------------------------------------- -STEP 2 — TEACH THE DEMUXER ABOUT THE THREE CODECS --------------------------------------------------------------------------------- - -media/remux/mkv_demuxer.rs: - - - Extend TrackCodec: Avc | Hevc | Aac | Ac3 | Eac3 | Dts | Unsupported. - Add the variants UNGATED. The enum is Serialize/Deserialize and the variants - are additive; a build with no decoder should still NAME the track ("AC-3, - cannot be decoded by this build") rather than reporting Unsupported, which - is a worse diagnostic and a worse log line. - - - Audio classification (~:166-180) currently: - - let (codec_kind, codec_name) = if a.codec == CODEC_ID_AAC { - (TrackCodec::Aac, "AAC") - } else { - (TrackCodec::Unsupported, "Audio") - }; - - becomes a match over CODEC_ID_AAC / AC3 / EAC3 / DCA. - - - extra_data is currently captured only for AAC. AC-3/DTS need none for - decoding (the decoders read the bitstream headers), so leave that as is — - but the RE-ENCODED AAC track needs its own AudioSpecificConfig, which comes - from the encoder's output_params, not from the source. - - - browser_audio_tracks (~:68-83) stops dropping these tracks when the matching - decoder feature is compiled in. Gate on TranscodeCodec::is_decodable() so a - build without transcode-dts still drops DTS rather than offering a track it - cannot produce. - -media/remux/hls.rs:8-14 and its test at :190-204 - (test_master_playlist_excludes_unsupported_audio_codecs) asserts the OLD - behaviour. It must be rewritten, not deleted: the new contract is "excluded - when this build cannot decode it", which is still worth a test. - - --------------------------------------------------------------------------------- -STEP 3 — A PACKET SOURCE THAT ISN'T AN ELEMENTARY STREAM --------------------------------------------------------------------------------- - -Phase 1-3's FrameIndex/AudioPlan walk sync words in a raw stream. A container -track's packets come from symphonia instead. Generalise: - - media/transcode/source.rs (new) - - /// Where compressed audio frames come from. - enum PacketSource { - /// A raw .ac3/.eac3/.dts file, framed by walking sync words. - Elementary(FrameIndex), - /// One track inside a container symphonia can demux. - Container { track_id: u32, /* symphonia reader */ }, - } - -Keep AudioPlan as the thing that resolves total samples / sample rate / channels -before any bytes are sent — that contract is what makes the LPCM resource -seekable and must not be weakened. For a container: - - - Total samples: prefer symphonia's track n_frames when the container declares - it (MKV usually does via Duration + TimestampScale; MP4 via stts). If absent, - fall back to a demux-only counting pass — same cost class as the elementary - header walk, and it is cached in TranscodeState either way. - - IMPORTANT: if total samples cannot be determined, the resource must degrade - to chunked with DLNA.ORG_OP=00, never guess a Content-Length. A wrong - Content-Length is a truncated download; an absent one is only a lost seek. - -The audio-only resources from phase 2 (/transcode/audio.wav|.aac) then work for -films too, and codec_for() in web/transcode_streaming.rs gains a DB-codec branch -alongside its MIME/extension branches. That alone is worth shipping: it fixes -"play the film's soundtrack on a hi-fi", and it exercises the container path -before the video work lands on top of it. - - --------------------------------------------------------------------------------- -STEP 4 — THE BROWSER PATH (HLS) --------------------------------------------------------------------------------- - -The smaller half, and the one with a working harness around it already. - -web/remux_streaming.rs:151-180, build_segment_response(): - - Add a decode+re-encode variant for audio segments whose track is - Ac3/Eac3/Dts: extract_track_packets -> PcmDecoder -> AacEncoder -> - Fmp4Writer::build_segment with the encoder's own AudioSpecificConfig. - - Requires transcode-aac (already a default feature). - - - MOVE SEGMENT BUILDING TO spawn_blocking. It currently runs synchronously on - the request task, which is survivable for a byte copy and is not for a - decode+encode of a 4-second segment (SEGMENT_DURATION_SECS, :26). This is a - prerequisite, not a nicety: without it one seeking client stalls the whole - runtime's worker. - - - Take a TranscodeState permit per segment build, so HLS and the DLNA path - share one concurrency ceiling rather than each having their own. - - - Segment caching becomes worth it here in a way it was not for a copy: the - same segment is rebuilt on every seek and every re-buffer. Key on - (file id, track, segment index) and reuse the existing Cache-Control. - -web/ui/js/video-player.js:54 already routes .mkv to /hls/master.m3u8, so the -browser side needs no change once the audio track stops being dropped. - - --------------------------------------------------------------------------------- -STEP 5 — THE DLNA PATH (progressive fMP4) — THE ACTUAL DELIVERABLE --------------------------------------------------------------------------------- - -New route, beside the phase-2 ones: - - GET|HEAD /media/{id}/transcode/video.mp4 - -Shape: - - Fragmented MP4: one init segment (ftyp + moov with the video track's avcC or - hvcC copied verbatim, plus an mp4a track from the AAC encoder's params), - then a continuous moof/mdat stream. - - Video is PASSTHROUGH. Nothing is re-encoded — only the audio track is - decoded and re-encoded. This is what keeps the CPU cost bounded and the - picture bit-identical. - - Chunked body via async_stream (web/radio.rs:141-171 is the proven pattern), - fed from a spawn_blocking producer over a bounded channel so a slow TV - applies backpressure instead of buffering a film into RAM. - - Headers: no Content-Length, no Accept-Ranges, - contentFeatures.dlna.org: DLNA.ORG_OP=00;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=... - OP=00 because there is no seeking. Do not claim otherwise. - - advertising: extend TranscodeAdvert so a video item advertises -video/mp4 -> transcode/video.mp4 while an audio item keeps audio.wav/audio.aac. -The selection belongs in web/mod.rs::transcode_advert() next to the existing -audio_format switch, so both DIDL writers stay feature-blind. - -Track selection for multi-audio films: transcode the track the container marks -DEFAULT; failing that the first audio track. Do not try to be clever about -language — a wrong guess is worse than a predictable one. If it turns out to -matter, the follow-up is one per audio track, which the DIDL already -supports (phase 3 proved multiple render fine). - - --------------------------------------------------------------------------------- -RISKS, HONESTLY --------------------------------------------------------------------------------- - -1. DO TVs ACTUALLY PLAY PROGRESSIVE fMP4 OVER DLNA? This is the largest product - risk in the whole feature and it is not answerable from the code. Support is - uneven and brand-specific. VERIFY THIS EARLY — before building Step 5 — with - a hand-built fMP4 served from a stub route to a real television. If it fails - broadly, the fallback is a progressive MPEG-TS mux, which nothing in the tree - provides and which would mean writing one (~600-900 lines) or vendoring a - fifth crate. Knowing this costs an afternoon; discovering it after Step 5 - costs the step. - -2. No seeking on the video resource. A film you cannot scrub is a real - regression in user experience versus direct play. Partial mitigation: keep - the original first (prefer = "original" default), so a TV that CAN - play AC-3 keeps its seekable direct-play resource and only the ones that - cannot fall back to the stream. Full mitigation is time-seek - (DLNA.ORG_OP=01 + TimeSeekRange.dlna.org), which is a follow-up: the - demuxer can seek by timestamp and the decoder can be primed the same way - phase 2 primes for byte ranges. - -3. CPU. Video passthrough keeps this bounded, but a 5.1 DTS track decoded and - re-encoded in real time on a Raspberry Pi is not free. max_concurrent - already exists and already refuses rather than queues. Measure on the - slowest target that matters before defaulting anything on. - -4. No real test fixtures exist. test-media/movie1.mkv is a 26-byte stub. Phase - 1-3 solved this for audio by using the vendored crates' own fixtures and by - synthesizing with oxideav-ac3's ENCODER. For phase 4 the same trick extends: - build a small MKV at test time from a synthesized H.264 or MPEG-1 video - track plus an AC-3 track encoded by oxideav-ac3. Prefer that over committing - a binary film clip. - -5. The unit tests will lie to you. Phase 1-3 learned this the hard way: every - integration test injected MediaFile rows directly into the database and so - passed against a scanner that indexed nothing, and a range test that seeked - to frame 1 masked a real decoder-state bug. FOR PHASE 4, DRIVE A REAL SERVER - WITH A REAL FILE BEFORE BELIEVING ANY OF IT. - - --------------------------------------------------------------------------------- -SUGGESTED PR SPLIT --------------------------------------------------------------------------------- - -PR A Step 1 + Step 2 + Step 3. - DB codec identification, demuxer awareness, container packet source. - Ships a working feature on its own: a film's soundtrack becomes playable - as audio.wav/audio.aac. Low risk, fully testable, no new dependencies. - -PR B Step 4. HLS audio for the browser player, plus the spawn_blocking and - caching fixes to build_segment_response that it forces. - -PR C Step 5. The progressive video resource. GATE THIS ON RISK 1 BEING - ANSWERED FIRST. - - --------------------------------------------------------------------------------- -VERIFICATION (all of it self-run, no manual steps) --------------------------------------------------------------------------------- - - cargo build - cargo check -p vuio-core --no-default-features - cargo check -p vuio-core --no-default-features --features transcode - cargo check -p vuio-core --no-default-features --features transcode-ac3 - cargo check -p vuio-core --no-default-features --features transcode-dts - cargo check -p vuio-core --no-default-features --features transcode-aac - cargo test --workspace - - Integration, in crates/vuio-core/tests/ following the oneshot pattern in - web_ui_integration_tests.rs:1-130: - - a scanned MKV with an AC-3 track records codec "ac3" and advertises two - elements - - the same MKV with the feature off advertises exactly one - - a build without transcode-dts drops a DTS track from the HLS master - playlist rather than offering a broken one - - the video.mp4 resource returns a parseable ftyp+moov followed by at least - two moof/mdat pairs, with the video track's avcC byte-identical to the - source's - - Live, against ./target/debug/vuio with CONTAINER=1 VUIO_MEDIA_DIRS=... and - the server killed in the same command: - - Browse as a Samsung UA, confirm both elements - - curl the video.mp4 resource, confirm it parses and carries both tracks - - confirm /media/{id} direct play is byte-identical to the source file - - Last, before committing: cargo fmt and cargo clippy, scoped to the - first-party crates (-p vuio-core -p vuio-cli -p vuio-cast -p vuio-web) so the - vendored tree under crates/vendor is not linted. From f2c16bdb475faf2a6e171049d56ac1549f1879aa Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 22:36:23 +0300 Subject: [PATCH 15/38] audio fix --- .../vuio-core/src/media/remux/mkv_demuxer.rs | 16 +- crates/vuio-core/src/media/transcode/dts.rs | 325 ++++++++++++++++++ crates/vuio-core/src/media/transcode/mod.rs | 35 +- crates/vuio-core/src/media/transcode/pcm.rs | 105 ++++-- .../src/media/transcode/rendition.rs | 285 +++++++++++++-- crates/vuio-core/src/media/transcode/video.rs | 102 +++++- crates/vuio-core/src/web/remux_streaming.rs | 101 ++++-- .../vuio-core/tests/film_transcode_tests.rs | 59 +++- 8 files changed, 897 insertions(+), 131 deletions(-) create mode 100644 crates/vuio-core/src/media/transcode/dts.rs diff --git a/crates/vuio-core/src/media/remux/mkv_demuxer.rs b/crates/vuio-core/src/media/remux/mkv_demuxer.rs index 4c26d688..18332fa3 100644 --- a/crates/vuio-core/src/media/remux/mkv_demuxer.rs +++ b/crates/vuio-core/src/media/remux/mkv_demuxer.rs @@ -401,7 +401,11 @@ impl MkvDemuxer { // the container's cue index is sparse enough that the seek above // landed mid-GOP. Dropping these leading frames loses nothing: they // depend on references the player would not have when starting here, - // and the previous segment already covers their span. + // and the previous segment already covers their span. Refusing the + // frames *before* `start_ticks` instead would be the opposite + // trade: on a film whose keyframes are further apart than a + // segment, the segment would open at the next one and leave a hole + // where the picture should be. if packets.is_empty() && !is_keyframe { continue; } @@ -418,9 +422,13 @@ impl MkvDemuxer { break; } } else { - // For audio tracks: discard packets that belong before this segment's start time - // so coarse seeking does not replay packets from the previous segment. - if (dur > 0 && pts + dur <= start_ticks) || (dur == 0 && pts < start_ticks) { + // Audio is partitioned strictly by the packet's own start, so + // every packet lands in exactly one segment and consecutive + // segments meet without overlapping. A caller that needs samples + // from before its segment — the re-encode does, to prime an + // encoder — asks for an earlier `start_secs` rather than being + // handed a packet twice. + if pts < start_ticks { continue; } if pts >= start_ticks + target_ticks && !packets.is_empty() { diff --git a/crates/vuio-core/src/media/transcode/dts.rs b/crates/vuio-core/src/media/transcode/dts.rs new file mode 100644 index 00000000..ee013f68 --- /dev/null +++ b/crates/vuio-core/src/media/transcode/dts.rs @@ -0,0 +1,325 @@ +//! Driving the vendored DTS decoder at a scale `i32` can actually hold. +//! +//! `oxideav-dts` reconstructs the Core profile faithfully — its per-channel PCM +//! is shape-identical to a reference decode, Pearson 1.000000 across every +//! channel of a real film — but two things about the *numbers* it hands back +//! make its `Decoder` trait impl unusable as it stands. +//! +//! The first is the output scale. §C.2.5 ends at +//! `naCh[nChIndex++] = int(rScale * raZ[i])`, and the specification does not fix +//! `rScale`: it is whatever brings a particular implementation's filterbank +//! output up to integer full scale. The crate's [`output_r_scale`] returns the +//! derivation for an implementation whose `raZ` is unit-normalised — +//! `2^(PCMR_bits - 1)`, so 2^23 for the 24-bit-sourced films that make up most +//! of a library — but its own `raZ` is not unit-normalised: a full-scale sample +//! leaves the filterbank at [`FULL_SCALE_RA_Z`], not at 1.0. Multiplying the two +//! together overflows `i32` by a factor of 180, and `as i32` saturates rather +//! than wrapping, so every sample above about -45 dBFS comes back as +//! `i32::MAX`. The result is a square wave — which is exactly what a television +//! played when the film's DTS track was selected. +//! +//! So this drives [`CoreStreamDecoder`] directly with the frame header's `PCMR` +//! forced to the 16-bit code. `rScale` is then 2^15, full scale lands at +//! `2^30 · √2`, and `i32` has √2 of headroom over it — enough that no +//! inter-sample overshoot can saturate. The scaling back down to S16 happens +//! here, in `f64`, where it costs nothing. Nothing else in the decode reads +//! `PCMR`; it feeds `output_r_scale` and no other call site. +//! +//! The second is the channel layout: the planes come back in the source's own +//! AMODE order (5.1 is `C, L, R, Ls, Rs`, not the `L, R, C, LFE, Ls, Rs` an +//! MP4 would use), and there are up to six of them where a browser tab wants +//! two. AC-3 carries the §7.8 downmix coefficients so its decoder can fold to +//! stereo itself; DTS Core does not, so the fold is [`fold_for`] below — the +//! ITU-R BS.775 coefficients, normalised the same way the AC-3 decoder +//! normalises its own, so selecting one audio track or the other does not move +//! the volume. +//! +//! [`output_r_scale`]: oxideav_dts::DtsFrameHeader::output_r_scale + +use anyhow::{anyhow, bail, Context, Result}; +use oxideav_dts::{AmodeArrangement, CoreStreamDecoder, DtsFrameHeader, FourteenBitByteOrder}; + +/// The value `raZ` reaches at full scale, where §C.2.5 assumes 1.0. +/// +/// Measured, not derived: a reference decode of a real 5.1 film lines up with +/// this crate's output at exactly this ratio on all five primary channels, and +/// the vendored 5-frame fixture agrees to seven digits. It is `2^15 · √2`, +/// which is suggestive of a filterbank normalisation missing upstream, but the +/// number is load-bearing whatever its provenance, so it is stated as what it +/// is: what this decoder's filterbank puts out for a full-scale sample. +const FULL_SCALE_RA_Z: f64 = 46_340.950_011_841_18; + +/// The `PCMR` code for 16-bit source PCM (ETSI TS 102 114 §5.3.1 Table 5-17). +const PCMR_16_BIT: u8 = 0b000; + +/// The `rScale` that code resolves to, and therefore the one every frame is +/// decoded at here. +const R_SCALE: f64 = 32_768.0; + +/// Scale from the decoder's `i32` output to S16: full scale arrives at +/// `R_SCALE · FULL_SCALE_RA_Z` and has to leave at 32768. +const TO_S16: f64 = 32_768.0 / (R_SCALE * FULL_SCALE_RA_Z); + +/// -3 dB: the ITU-R BS.775 coefficient for folding a centre or surround channel +/// into both halves of a stereo pair. +const FOLD: f64 = std::f64::consts::FRAC_1_SQRT_2; + +/// Bytes per sample per channel in the output. +const BYTES_PER_SAMPLE: usize = 2; + +/// One half of a fold: which planes it draws on, and at what weight. +type Taps = Vec<(usize, f64)>; + +/// One DTS Core stream, decoded and folded to at most two channels. +/// +/// Holds the §C.2.5 filter tail across frames — a DTS elementary stream's QMF +/// filter is continuous, and restarting it per frame injects a warm-up +/// transient at every frame boundary. +pub struct DtsDecoder { + stream: Option, + /// Channels the fold emits. Two unless the caller asked for one. + channels: u16, +} + +impl DtsDecoder { + /// A decoder emitting `want_channels`, which is honoured for one and two and + /// otherwise taken as two — every caller in this crate asks for stereo, and + /// a caller that wants more widens the fold's output itself. + pub fn new(want_channels: Option) -> Self { + Self { + stream: None, + channels: if want_channels == Some(1) { 1 } else { 2 }, + } + } + + /// Decode one frame into interleaved S16, with its sample count per channel. + pub fn decode(&mut self, frame: &[u8]) -> Result<(Vec, u32)> { + // Both 14-bit container byte orders carry the same logical bitstream as + // the raw-16-bit forms, packed 14 payload bits per 16-bit word, and the + // raw-16-bit parser is what says so: it refuses a 14-bit sync by name. + // Unpack those into the domain the reconstruction operates on and + // decode them through the identical chain. + let (unpacked, mut header) = match oxideav_dts::parse_frame_header(frame) { + Ok(header) => (None, header), + Err(oxideav_dts::Error::UnsupportedFourteenBit) => { + let packed = oxideav_dts::parse_frame_header_14bit(frame) + .map_err(|e| anyhow!("DTS 14-bit header: {e}"))?; + let order = FourteenBitByteOrder::from_sync(packed.sync_word_encoding) + .ok_or_else(|| anyhow!("DTS: a 14-bit sync with no container byte order"))?; + let bytes = oxideav_dts::unpack_14bit_to_16bit(frame, order) + .map_err(|e| anyhow!("DTS 14-bit unpack: {e}"))?; + let header = oxideav_dts::parse_frame_header(&bytes) + .map_err(|e| anyhow!("DTS header: {e}"))?; + (Some(bytes), header) + } + Err(e) => bail!("DTS header: {e}"), + }; + let bytes = unpacked.as_deref().unwrap_or(frame); + + // See the module comment: the declared resolution's `rScale` saturates + // `i32` against this decoder's filterbank output, and the 16-bit code's + // does not. + header.source_pcm_resolution_index = PCMR_16_BIT; + + let channels = primary_channels(bytes, &header)?; + let mut stream = match self.stream.take() { + // A stream's channel count is constant in practice; restarting the + // filter for a new layout is what the vendored driver does too. + Some(stream) if stream.channel_count() == channels => stream, + _ => CoreStreamDecoder::new(channels), + }; + + let planes = stream.decode_frame(bytes, &header); + // The LFE plane comes back through a separate accessor and on a + // different scale from the primary channels. The fold drops it, as a + // stereo downmix conventionally does, so it is never read — but it must + // still be taken, or it accumulates into the next frame's. + let _ = stream.take_last_lfe_pcm(); + self.stream = Some(stream); + let planes = planes.map_err(|e| anyhow!("DTS decode: {e:?}"))?; + + let samples = planes.first().map_or(0, Vec::len); + Ok(( + self.fold(&planes, header.amode_arrangement(), samples), + samples as u32, + )) + } + + /// Fold the frame's planes into interleaved S16 at `self.channels`. + fn fold(&self, planes: &[Vec], arrangement: AmodeArrangement, samples: usize) -> Vec { + let (left, right) = fold_for(arrangement, planes.len()); + let left_gain = normalise(&left); + let right_gain = normalise(&right); + let mono = self.channels == 1; + + let mut out = Vec::with_capacity(samples * self.channels as usize * BYTES_PER_SAMPLE); + for n in 0..samples { + let mix = |taps: &[(usize, f64)], gain: f64| -> f64 { + taps.iter() + .map(|(plane, weight)| { + weight * planes[*plane].get(n).copied().unwrap_or(0) as f64 + }) + .sum::() + * gain + }; + let l = mix(&left, left_gain); + if mono { + out.extend_from_slice(&to_s16((l + mix(&right, right_gain)) * 0.5).to_le_bytes()); + } else { + out.extend_from_slice(&to_s16(l).to_le_bytes()); + out.extend_from_slice(&to_s16(mix(&right, right_gain)).to_le_bytes()); + } + } + out + } +} + +/// Scale one summed sample to S16, clamping rather than wrapping. +fn to_s16(value: f64) -> i16 { + (value * TO_S16).clamp(-32_768.0, 32_767.0) as i16 +} + +/// The gain that keeps a fold's coefficients summing to unity. +/// +/// This is the same clip guard the AC-3 decoder applies to its own §7.8 +/// downmix — measured at `1 / (1 + 2/√2)` against a reference decode of the +/// same film's AC-3 track — so a viewer switching between a film's AC-3 and DTS +/// renditions hears the same level rather than an 8 dB jump. +fn normalise(taps: &[(usize, f64)]) -> f64 { + let sum: f64 = taps.iter().map(|(_, weight)| weight.abs()).sum(); + if sum > 0.0 { + 1.0 / sum + } else { + 0.0 + } +} + +/// Which planes make up each half of the stereo fold, and at what weight. +/// +/// The plane order is the arrangement's own (ETSI TS 102 114 §5.3.1 Table 5-4): +/// `AMODE 9`, the one essentially every 5.1 film carries, delivers +/// `C, L, R, Ls, Rs` — centre first — which is why this is a table and not an +/// index arithmetic. Weights are the ITU-R BS.775 fold; [`normalise`] applies +/// the clip guard afterwards, so a layout with nothing to fold in (plain +/// stereo) is passed through at unity rather than attenuated. +/// +/// `planes` is the trailing fallback for the arrangements above `AMODE 9`, +/// which pair their channels left-then-right across the layout and do not +/// appear in circulation; taking the even planes as left and the odd as right +/// is an approximation of them, not a reading of Table 5-4. +fn fold_for(arrangement: AmodeArrangement, planes: usize) -> (Taps, Taps) { + use AmodeArrangement as A; + match arrangement { + // A single channel, heard from both speakers. + A::Mono => (vec![(0, 1.0)], vec![(0, 1.0)]), + // Two independent channels, or an already-folded pair: straight across. + A::DualMono | A::Stereo | A::LtRt if planes >= 2 => { + (vec![(0, 1.0)], vec![(1, 1.0)]) + } + // Sum and difference: L = (S+D)/2, R = (S-D)/2. + A::SumDifference if planes >= 2 => { + (vec![(0, 0.5), (1, 0.5)], vec![(0, 0.5), (1, -0.5)]) + } + // C, L, R. + A::ClR if planes >= 3 => (vec![(1, 1.0), (0, FOLD)], vec![(2, 1.0), (0, FOLD)]), + // L, R, S — one shared surround into both halves. + A::LrS if planes >= 3 => (vec![(0, 1.0), (2, FOLD)], vec![(1, 1.0), (2, FOLD)]), + // C, L, R, S. + A::ClRS if planes >= 4 => ( + vec![(1, 1.0), (0, FOLD), (3, FOLD)], + vec![(2, 1.0), (0, FOLD), (3, FOLD)], + ), + // L, R, Ls, Rs. + A::LrSlSr if planes >= 4 => ( + vec![(0, 1.0), (2, FOLD)], + vec![(1, 1.0), (3, FOLD)], + ), + // C, L, R, Ls, Rs — 5.1 without its LFE, and the layout that matters. + A::ClRSlSr if planes >= 5 => ( + vec![(1, 1.0), (0, FOLD), (3, FOLD)], + vec![(2, 1.0), (0, FOLD), (4, FOLD)], + ), + _ => { + if planes >= 2 { + ( + (0..planes).step_by(2).map(|i| (i, 1.0)).collect(), + (1..planes).step_by(2).map(|i| (i, 1.0)).collect(), + ) + } else { + (vec![(0, 1.0)], vec![(0, 1.0)]) + } + } + } +} + +/// The frame's §5.3.2 primary-channel count (`nPCHS`), which sizes the filter. +/// +/// Read from the audio coding header rather than from `AMODE`, because that is +/// where the reconstruction itself reads it: a filter sized from a disagreeing +/// count decodes into the wrong number of planes. +fn primary_channels(bytes: &[u8], header: &DtsFrameHeader) -> Result { + let header_bits = header.header_bit_length() as usize; + let (coding, _) = + oxideav_dts::decode_audio_coding_header_at(bytes, header_bits, header.crc_present) + .map_err(|e| anyhow!("{e}")) + .context("reading the DTS audio coding header")?; + Ok(coding.n_pchs) +} + +#[cfg(test)] +mod tests { + use super::*; + + const FIXTURE: &[u8] = include_bytes!("../../../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); + + /// The fixture is 2-channel, so each frame's planes fold straight across. + #[test] + fn the_fixture_decodes_to_stereo_at_a_sane_level() { + let mut decoder = DtsDecoder::new(Some(2)); + // Frame boundaries: the fixture is five 1024-byte frames. + let mut peak = 0i32; + let mut frames = 0; + for frame in FIXTURE.chunks_exact(1024) { + let (pcm, samples) = decoder.decode(frame).expect("a fixture frame decodes"); + assert_eq!(pcm.len(), samples as usize * 2 * BYTES_PER_SAMPLE); + for pair in pcm.as_chunks::<2>().0 { + peak = peak.max(i16::from_le_bytes(*pair).unsigned_abs() as i32); + } + frames += 1; + } + assert_eq!(frames, 5); + // The whole point: full scale is 32768, and before the `PCMR` override + // this railed at it on every sample. A real signal sits below it. + assert!( + (1_000..32_000).contains(&peak), + "peak {peak} is either silence or the saturation this exists to prevent" + ); + } + + /// A fold with nothing to fold in must not be quieter than its source. + #[test] + fn a_stereo_layout_is_passed_through_at_unity() { + let (left, right) = fold_for(AmodeArrangement::Stereo, 2); + assert_eq!(normalise(&left), 1.0); + assert_eq!(normalise(&right), 1.0); + } + + /// The 5.1 fold is the ITU one, normalised to unity gain. + #[test] + fn the_five_one_fold_takes_l_c_and_ls_into_the_left_half() { + let (left, right) = fold_for(AmodeArrangement::ClRSlSr, 5); + assert_eq!(left, vec![(1, 1.0), (0, FOLD), (3, FOLD)]); + assert_eq!(right, vec![(2, 1.0), (0, FOLD), (4, FOLD)]); + // 1 / (1 + 2/√2), the AC-3 decoder's own clip guard. + assert!((normalise(&left) - 0.414_213_562_373).abs() < 1e-9); + } + + /// Sum/difference recovers the pair rather than folding it. + #[test] + fn sum_difference_is_undone_rather_than_mixed() { + let (left, right) = fold_for(AmodeArrangement::SumDifference, 2); + assert_eq!(left, vec![(0, 0.5), (1, 0.5)]); + assert_eq!(right, vec![(0, 0.5), (1, -0.5)]); + assert_eq!(normalise(&left), 1.0); + } +} diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index 1e2f7339..0ad42aaa 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -15,6 +15,8 @@ #[cfg(feature = "transcode-aac")] mod aac; +#[cfg(feature = "transcode-dts")] +mod dts; mod frames; mod pcm; mod plan; @@ -40,7 +42,9 @@ pub use pcm::PcmDecoder; pub use plan::{AudioPlan, Seeked}; #[cfg(all(feature = "transcode-aac", feature = "demux"))] #[allow(unused_imports)] -pub use rendition::{fit_channels, reencode_to_aac, AAC_FRAME_SAMPLES}; +pub use rendition::{ + fit_channels, reencode_to_aac, run_anchor, AacWindow, AAC_FRAME_SAMPLES, ENCODER_DELAY, +}; pub use session::{IndexKey, SegmentKey, TranscodeState}; #[cfg(all(feature = "transcode-aac", feature = "casting"))] pub use video::ProgressiveStream; @@ -105,23 +109,24 @@ impl TranscodeCodec { } } -/// Build a decoder for `codec`, or `None` when this build cannot decode it. +/// Build a trait-object decoder for `codec`. /// -/// `want_channels` is passed through to the decoder rather than applied -/// afterwards: AC-3 carries the §7.8 downmix coefficients in the bitstream, so -/// asking the decoder for two channels produces the mix the encoder intended, -/// which a naive channel-summing downmix outside the decoder would not. +/// AC-3 and E-AC-3 only. `want_channels` is passed through to the decoder +/// rather than applied afterwards: AC-3 carries the §7.8 downmix coefficients +/// in the bitstream, so asking the decoder for two channels produces the mix +/// the encoder intended, which a naive channel-summing downmix outside the +/// decoder would not. DTS does not come through here at all — the vendored +/// decoder's trait impl scales its output by an `rScale` that saturates `i32` +/// on most real films, so [`dts`] drives the reconstruction underneath it +/// instead. #[cfg(feature = "transcode")] -#[cfg_attr( - not(any(feature = "transcode-ac3", feature = "transcode-dts")), - allow(unused_variables) -)] +#[cfg_attr(not(feature = "transcode-ac3"), allow(unused_variables))] pub(crate) fn make_decoder( codec: TranscodeCodec, sample_rate: u32, want_channels: Option, ) -> anyhow::Result> { - #[cfg(any(feature = "transcode-ac3", feature = "transcode-dts"))] + #[cfg(feature = "transcode-ac3")] use oxideav_core::{CodecId, CodecParameters, SampleFormat}; match codec { @@ -141,14 +146,6 @@ pub(crate) fn make_decoder( } .map_err(|e| anyhow::anyhow!("AC-3 decoder: {e}")) } - #[cfg(feature = "transcode-dts")] - TranscodeCodec::Dts => { - let mut params = CodecParameters::audio(CodecId::new("dts")); - params.sample_rate = Some(sample_rate); - params.channels = want_channels; - params.sample_format = Some(SampleFormat::S16); - oxideav_dts::make_decoder(¶ms).map_err(|e| anyhow::anyhow!("DTS decoder: {e}")) - } #[allow(unreachable_patterns)] other => anyhow::bail!( "this build of vuio-core was compiled without a decoder for {}", diff --git a/crates/vuio-core/src/media/transcode/pcm.rs b/crates/vuio-core/src/media/transcode/pcm.rs index f1891d76..154da218 100644 --- a/crates/vuio-core/src/media/transcode/pcm.rs +++ b/crates/vuio-core/src/media/transcode/pcm.rs @@ -1,12 +1,21 @@ //! Turning compressed frames into interleaved little-endian S16. //! -//! The vendored decoders emit exactly that layout in `AudioFrame::data[0]` when -//! asked for [`SampleFormat::S16`], so this is a thin driver over the push/pull -//! `Decoder` trait rather than any DSP of its own — the one substantive choice -//! is asking the decoder for the channel count we want instead of mixing down -//! afterwards. AC-3 carries the §7.8 downmix coefficients in the bitstream, so -//! the decoder's own two-channel output is the mix the encoder intended; summing -//! channels outside it would throw that away and clip besides. +//! The AC-3 decoder emits exactly that layout in `AudioFrame::data[0]` when +//! asked for [`SampleFormat::S16`], so for those two codecs this is a thin +//! driver over the push/pull `Decoder` trait rather than any DSP of its own — +//! the one substantive choice is asking the decoder for the channel count we +//! want instead of mixing down afterwards. AC-3 carries the §7.8 downmix +//! coefficients in the bitstream, so the decoder's own two-channel output is +//! the mix the encoder intended; summing channels outside it would throw that +//! away and clip besides. +//! +//! DTS does not offer that, and the vendored decoder's `Decoder` impl cannot be +//! used at all — see [`super::dts`], which is the driver for it. What both +//! paths share is this type's contract: one compressed frame in, interleaved +//! S16 at a fixed channel count out, and a frame that fails costing exactly its +//! own duration in silence. +//! +//! [`SampleFormat::S16`]: oxideav_core::SampleFormat::S16 use anyhow::{bail, Context, Result}; @@ -17,12 +26,23 @@ const BYTES_PER_SAMPLE: usize = 2; /// A decoder bound to one stream, with its output shape resolved. pub struct PcmDecoder { - inner: Box, + inner: Inner, codec: TranscodeCodec, sample_rate: u32, channels: u16, } +/// The two shapes a decode takes. +enum Inner { + /// AC-3 and E-AC-3, through the vendored `Decoder` trait, which hands back + /// interleaved S16 already folded to the channel count it was asked for. + Trait(Box), + /// DTS, through [`super::dts`] — the trait impl for it cannot be driven at + /// a scale `i32` holds, so this crate drives the reconstruction itself. + #[cfg(feature = "transcode-dts")] + Dts(super::dts::DtsDecoder), +} + impl PcmDecoder { /// Open a decoder for `codec` and resolve its output shape from `first_frame`. /// @@ -37,7 +57,11 @@ impl PcmDecoder { want_channels: Option, first_frame: &[u8], ) -> Result<(Self, Vec)> { - let inner = super::make_decoder(codec, sample_rate, want_channels)?; + let inner = match codec { + #[cfg(feature = "transcode-dts")] + TranscodeCodec::Dts => Inner::Dts(super::dts::DtsDecoder::new(want_channels)), + _ => Inner::Trait(super::make_decoder(codec, sample_rate, want_channels)?), + }; let mut me = Self { inner, codec, @@ -102,27 +126,34 @@ impl PcmDecoder { /// Feed one frame and collect everything it produces. fn decode_measured(&mut self, frame: &[u8]) -> Result<(Vec, u32)> { - use oxideav_core::{Frame, Packet, TimeBase}; - - let packet = Packet::new(0, TimeBase::new(1, self.sample_rate as i64), frame.to_vec()); - self.inner - .send_packet(&packet) - .map_err(|e| anyhow::anyhow!("{}: {e}", self.codec.as_str())) - .context("feeding a frame to the decoder")?; - - let mut out = Vec::new(); - let mut samples = 0u32; - // `receive_frame` returns `NeedMore` once the packet is drained, which is - // the normal exit, not an error. - while let Ok(frame) = self.inner.receive_frame() { - if let Frame::Audio(af) = frame { - if let Some(plane) = af.data.first() { - out.extend_from_slice(plane); + match &mut self.inner { + #[cfg(feature = "transcode-dts")] + Inner::Dts(decoder) => decoder.decode(frame), + Inner::Trait(inner) => { + use oxideav_core::{Frame, Packet, TimeBase}; + + let packet = + Packet::new(0, TimeBase::new(1, self.sample_rate as i64), frame.to_vec()); + inner + .send_packet(&packet) + .map_err(|e| anyhow::anyhow!("{}: {e}", self.codec.as_str())) + .context("feeding a frame to the decoder")?; + + let mut out = Vec::new(); + let mut samples = 0u32; + // `receive_frame` returns `NeedMore` once the packet is drained, + // which is the normal exit, not an error. + while let Ok(frame) = inner.receive_frame() { + if let Frame::Audio(af) = frame { + if let Some(plane) = af.data.first() { + out.extend_from_slice(plane); + } + samples += af.samples; + } } - samples += af.samples; + Ok((out, samples)) } } - Ok((out, samples)) } } @@ -207,11 +238,27 @@ mod tests { ); } + /// Both ways a DTS decode goes wrong, in one assertion. + /// + /// Below the floor it is silence, which is what a decoder that refused + /// every frame produces. At the ceiling it is the square wave the vendored + /// decoder's own `rScale` derivation produces on any real film — the whole + /// reason [`super::super::dts`] exists — and which a bare "is it louder + /// than silence" check waves through. #[cfg(feature = "transcode-dts")] #[test] - fn dts_decodes_to_audible_audio() { + fn dts_decodes_to_audible_audio_rather_than_to_a_railed_one() { let (pcm, _, _) = decode_all(TranscodeCodec::Dts, DTS_FIXTURE); - assert!(rms(&pcm) > 10.0, "decoded RMS {} is silence", rms(&pcm)); + let level = rms(&pcm); + assert!(level > 10.0, "decoded RMS {level} is silence"); + assert!(level < 16_000.0, "decoded RMS {level} is a saturated decode"); + let railed = pcm + .as_chunks::<2>() + .0 + .iter() + .filter(|c| i16::from_le_bytes(**c).unsigned_abs() >= 32_767) + .count(); + assert_eq!(railed, 0, "{railed} samples came back clipped to full scale"); } /// A corrupt frame must cost its own duration in silence and nothing more, diff --git a/crates/vuio-core/src/media/transcode/rendition.rs b/crates/vuio-core/src/media/transcode/rendition.rs index 8c0e2f10..bc360b7b 100644 --- a/crates/vuio-core/src/media/transcode/rendition.rs +++ b/crates/vuio-core/src/media/transcode/rendition.rs @@ -6,66 +6,167 @@ //! decoded and re-encoded to exist at all. What differs between them is //! framing and headers; what happens to the samples is here. //! -//! The one number worth understanding is the encoder's delay. Its MDCT window -//! spans the previous hop and the current one, so the frame emitted for input -//! samples `[n, n+1024)` is only fully reconstructed once the *next* frame has -//! been overlap-added — a decoder's output therefore trails its input by -//! exactly one frame. Placing the run 1024 samples earlier on the decode -//! timeline cancels that, and is the difference between lip-sync and a -//! twenty-one millisecond lag. +//! Two numbers govern all of it. +//! +//! The first is [`ENCODER_DELAY`]. The encoder's MDCT window spans the previous +//! hop and the current one, so what a decoder reconstructs trails what the +//! encoder was fed — measured end to end at 1600 samples, constant across every +//! rate and channel count the encoder accepts. Placing the run that many +//! samples earlier on the decode timeline is the difference between lip-sync +//! and a thirty-three millisecond lag. +//! +//! The second is that an AAC frame is 1024 samples and a segment is four +//! seconds, and 192000 is not a multiple of 1024. A segment that simply encodes +//! its own four seconds and starts the run at its nominal decode time therefore +//! runs 512 samples past where the next segment begins, and the two collide in +//! the player's source buffer — once every four seconds, for the length of the +//! film. So a segment does not own a duration here; it owns a stretch of the +//! film-wide frame grid ([`AacWindow`]), and consecutive windows meet exactly +//! because each starts where the arithmetic says the previous one stopped. use anyhow::Result; use super::{AacEncoder, PcmDecoder, TranscodeCodec}; use crate::media::remux::MediaPacket; -/// Samples per AAC-LC frame, and therefore the encoder's delay. +/// Samples per AAC-LC frame. pub const AAC_FRAME_SAMPLES: u64 = 1024; +/// Samples by which a decoder's output trails what the encoder was fed. +/// +/// Measured, not assumed: an impulse train through this encoder and back out of +/// a reference decoder comes back 1600 samples late, at 32, 44.1 and 48 kHz and +/// in mono and stereo alike. It is not a multiple of the frame length, which is +/// why cancelling it is done by moving the *input* window rather than by +/// shifting whole frames. +pub const ENCODER_DELAY: u64 = 1600; + +/// Frames fed to the encoder before the window's first, and then discarded. +/// +/// Enough to cover [`ENCODER_DELAY`] and leave the first kept frame's MDCT +/// window filled with real audio rather than with the silence a cold encoder +/// starts from — without which every segment boundary would carry the encoder's +/// warm-up transient. +const PREROLL_FRAMES: u64 = 4; + /// Bytes per sample per channel in the decoder's output. const BYTES_PER_SAMPLE: usize = 2; -/// Decode `packets` and re-encode them as AAC, as samples for an MP4 track. +/// The stretch of the film-wide AAC frame grid one segment owns. +/// +/// Frames sit at multiples of [`AAC_FRAME_SAMPLES`] measured from the start of +/// the film, never from the start of the segment. That is the whole trick: two +/// segments computed independently, in different requests, on different +/// threads, cannot overlap or leave a gap, because neither of them is choosing +/// where its frames go. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AacWindow { + first_frame: u64, + frames: u64, +} + +impl AacWindow { + /// The window covering `[start_sample, end_sample)` of the film. + /// + /// Rounding both ends up to a frame boundary is what makes the windows + /// tile: this window's end is the next one's start, by construction. + pub fn covering(start_sample: u64, end_sample: u64) -> Self { + let first_frame = start_sample.div_ceil(AAC_FRAME_SAMPLES); + Self { + first_frame, + frames: end_sample + .div_ceil(AAC_FRAME_SAMPLES) + .saturating_sub(first_frame), + } + } + + /// Decode time of the window's first sample. + pub fn start_sample(&self) -> u64 { + self.first_frame * AAC_FRAME_SAMPLES + } + + /// The source samples the encoder has to be fed to produce this window: + /// where to start on the film's timeline, and how many. + /// + /// The start is signed because the film's first window asks for audio from + /// before the film — [`reencode_to_aac`] answers that with silence, which is + /// what the encoder would have warmed up on anyway. + pub fn source_span(&self) -> (i64, u64) { + let from = self.start_sample() as i64 + ENCODER_DELAY as i64 + - (PREROLL_FRAMES * AAC_FRAME_SAMPLES) as i64; + (from, (PREROLL_FRAMES + self.frames) * AAC_FRAME_SAMPLES) + } +} + +/// Decode `packets` and re-encode `window` of them as AAC, as MP4 samples. /// /// `packets` must be one track's packets in order, with timestamps already in /// the output timescale — which for an audio track is its sample rate, so a -/// timestamp *is* a sample index. `channels` is what the output track declares, -/// and what the samples are made to match: a mono source asked to be stereo is -/// widened here rather than being allowed to contradict the `esds` box that has -/// already gone out in the init segment. +/// timestamp *is* a sample index, and between them they say where the decoded +/// run sits on the film's timeline (see [`run_anchor`]). They must cover +/// [`AacWindow::source_span`]; anything they do not reach, at either end, is +/// silence. `channels` is what +/// the output track declares, and what the samples are made to match: a mono +/// source asked to be stereo is widened here rather than being allowed to +/// contradict the `esds` box that has already gone out in the init segment. /// -/// The returned packets carry `pts == dts` and a duration of one AAC frame. -/// Audio has no reordering, so there is nothing for a composition offset to -/// express. +/// Exactly the window's frames come back, at exactly the window's decode times, +/// whatever the source's own framing was: the encoder is fed a run positioned +/// by absolute sample index, and its warm-up frames are dropped rather than +/// shipped. The returned packets carry `pts == dts` and a duration of one AAC +/// frame — audio has no reordering, so there is nothing for a composition +/// offset to express. pub fn reencode_to_aac( codec: TranscodeCodec, packets: &[MediaPacket], sample_rate: u32, channels: u16, track_id: u32, - nominal_start_pts: Option, + window: AacWindow, ) -> Result> { let Some(first) = packets.first() else { return Ok(Vec::new()); }; - let (mut decoder, primed) = - PcmDecoder::open(codec, sample_rate, Some(channels), &first.data)?; + let (mut decoder, primed) = PcmDecoder::open(codec, sample_rate, Some(channels), &first.data)?; let decoded_channels = decoder.channels(); - let mut encoder = AacEncoder::new(sample_rate, channels)?; + let source_frame_bytes = decoded_channels as usize * BYTES_PER_SAMPLE; - let mut adts = encoder.push(&fit_channels(&primed, decoded_channels, channels))?; + // One contiguous run of PCM, and every packet's own account of where that + // run begins. Each frame contributes exactly the sample count its own + // header promised, so a frame that fails to decode costs its own duration + // and does not shift everything after it off the timeline. + let mut pcm = pad_to( + primed, + super::frames::frame_samples(codec, &first.data), + decoded_channels, + ); + let mut anchors = vec![first.pts as i64]; for packet in &packets[1..] { + anchors.push(packet.pts as i64 - (pcm.len() / source_frame_bytes) as i64); let expect = super::frames::frame_samples(codec, &packet.data); - let pcm = decoder.decode_or_silence(&packet.data, expect); - adts.extend_from_slice(&encoder.push(&fit_channels(&pcm, decoded_channels, channels))?); + pcm.extend_from_slice(&decoder.decode_or_silence(&packet.data, expect)); } + let pcm = fit_channels(&pcm, decoded_channels, channels); + + let (from, len) = window.source_span(); + let mut encoder = AacEncoder::new(sample_rate, channels)?; + let mut adts = encoder.push(&sample_range( + &pcm, + run_anchor(&mut anchors), + from, + len, + channels as usize * BYTES_PER_SAMPLE, + ))?; adts.extend_from_slice(&encoder.finish()); - let base = nominal_start_pts.unwrap_or(first.pts); - let mut dts = base; + let mut dts = window.start_sample(); let mut out = Vec::new(); - for payload in super::adts_payloads(&adts) { + for payload in super::adts_payloads(&adts) + .into_iter() + .skip(PREROLL_FRAMES as usize) + .take(window.frames as usize) + { out.push(MediaPacket { track_id, pts: dts, @@ -81,6 +182,67 @@ pub fn reencode_to_aac( Ok(out) } +/// Where the decoded run begins on the film's timeline. +/// +/// Not simply the first packet's timestamp. Matroska stores block timestamps in +/// milliseconds, and a muxer writing 512-sample DTS frames — ten and two thirds +/// milliseconds each — has to round every one of them. On real files the result +/// wanders as much as seventy-five milliseconds either side of where the audio +/// actually is, while the frames themselves stay perfectly contiguous. A +/// segment anchored on one such timestamp is placed that far out, and both of +/// its joins are heard as a jump. +/// +/// So every packet is asked the same question instead — "if this run is +/// contiguous, where does it start?" — and the answer taken from the upper part +/// of the spread, because the noise is one-sided: rounding a timestamp down and +/// a muxer running behind both push an estimate low, and nothing pushes it +/// high. The maximum would be the estimate the rounding alone implies, but one +/// spurious timestamp would then set the answer, so this stops short of it. +/// +/// A track whose timestamps are exact — AC-3 at 1536 samples a frame divides +/// into milliseconds, and every packet then agrees — is unaffected: all the +/// estimates are the same number, and every quantile of them is that number. +pub fn run_anchor(estimates: &mut [i64]) -> i64 { + estimates.sort_unstable(); + estimates[(estimates.len() - 1) * ANCHOR_QUANTILE.0 / ANCHOR_QUANTILE.1] +} + +/// The quantile [`run_anchor`] reads the anchor off at, as a fraction. +/// +/// Measured against a sequential read of a real DTS track: at three quarters +/// the estimate is out by thirteen samples on average and never by more than +/// twenty-two milliseconds, where the first timestamp alone averaged six +/// hundred and eighty and reached seventy-five. +const ANCHOR_QUANTILE: (usize, usize) = (3, 4); + +/// Pad one decoded frame out to the sample count its header declared. +fn pad_to(mut pcm: Vec, samples: Option, channels: u16) -> Vec { + if let Some(samples) = samples { + pcm.resize(samples as usize * channels as usize * BYTES_PER_SAMPLE, 0); + } + pcm +} + +/// `len` sample frames of `pcm` from absolute position `from`, where `pcm` +/// itself begins at absolute position `pcm_start`. +/// +/// Silence stands in for anything outside what `pcm` holds. That is not a +/// failure case: the film's first window reaches back before the film to prime +/// the encoder, and its last reaches past the end for the same reason. +fn sample_range(pcm: &[u8], pcm_start: i64, from: i64, len: u64, frame_bytes: usize) -> Vec { + let mut out = vec![0u8; len as usize * frame_bytes]; + let available = (pcm.len() / frame_bytes) as i64; + let begin = from.max(pcm_start); + let end = (from + len as i64).min(pcm_start + available); + if end > begin { + let src = (begin - pcm_start) as usize * frame_bytes; + let at = (begin - from) as usize * frame_bytes; + let take = (end - begin) as usize * frame_bytes; + out[at..at + take].copy_from_slice(&pcm[src..src + take]); + } + out +} + /// Fit interleaved S16 with `have` channels into `want` channels. /// /// The decoder is asked for the channel count the output declares and normally @@ -114,6 +276,77 @@ pub fn fit_channels(pcm: &[u8], have: u16, want: u16) -> Vec { mod tests { use super::*; + /// The arithmetic the whole segmented path rests on: run the windows a + /// player would actually request and check that they lay end to end. + #[test] + fn consecutive_windows_meet_without_a_gap_or_an_overlap() { + const RATE: u64 = 48_000; + const SEGMENT: u64 = 4 * RATE; + // 192000 is 187.5 frames, so every other boundary falls mid-frame — + // which is the case a naive "encode my own four seconds" gets wrong. + let mut expected = 0; + for seq in 0..64u64 { + let window = AacWindow::covering(seq * SEGMENT, (seq + 1) * SEGMENT); + assert_eq!(window.start_sample(), expected, "window {seq} does not open where {} closed", seq.saturating_sub(1)); + expected = window.start_sample() + window.frames * AAC_FRAME_SAMPLES; + } + // And they keep time: sixty-four segments of four seconds, to within + // the frame the grid rounds by. + assert!(expected.abs_diff(64 * SEGMENT) < AAC_FRAME_SAMPLES); + } + + /// The encoder is fed from before the window and past it, or its first kept + /// frame opens on silence and its last is never finished. + #[test] + fn the_source_span_brackets_the_window_it_produces() { + let window = AacWindow::covering(4 * 48_000, 8 * 48_000); + let (from, len) = window.source_span(); + assert!(from < window.start_sample() as i64, "no pre-roll"); + let ends_at = from + len as i64; + let window_ends = (window.start_sample() + window.frames * AAC_FRAME_SAMPLES) as i64; + assert_eq!( + ends_at - window_ends, + ENCODER_DELAY as i64, + "the run must reach exactly the encoder's delay past its last output sample" + ); + } + + /// The film's first window reaches back before the film, and its last past + /// the end. Neither is an error, and neither may shift what is there. + #[test] + fn a_span_reaching_outside_the_decoded_run_is_padded_with_silence() { + // Four stereo sample frames, at absolute position 100. + let pcm: Vec = (1i16..=8).flat_map(|v| v.to_le_bytes()).collect(); + let out = sample_range(&pcm, 100, 98, 8, 4); + let values: Vec = out + .as_chunks::<2>() + .0 + .iter() + .map(|c| i16::from_le_bytes(*c)) + .collect(); + assert_eq!( + values, + vec![0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0], + "the run has to sit at its own offset inside the padding" + ); + } + + /// A track whose timestamps are exact is left exactly where they put it. + #[test] + fn an_unjittered_run_anchors_on_its_own_timestamps() { + let mut anchors = vec![7_680; 128]; + assert_eq!(run_anchor(&mut anchors), 7_680); + } + + /// One timestamp seventy-five milliseconds out of place must not move the + /// run, which is the defect a browser hears as a jump at both its joins. + #[test] + fn a_stray_timestamp_does_not_move_the_run() { + let mut anchors = vec![7_680; 128]; + anchors[0] -= 3_616; + assert_eq!(run_anchor(&mut anchors), 7_680); + } + #[test] fn mono_is_widened_by_duplication_and_the_frame_count_is_kept() { let mono: Vec = [1i16, 2, 3] diff --git a/crates/vuio-core/src/media/transcode/video.rs b/crates/vuio-core/src/media/transcode/video.rs index 0a827f01..486c958a 100644 --- a/crates/vuio-core/src/media/transcode/video.rs +++ b/crates/vuio-core/src/media/transcode/video.rs @@ -82,10 +82,29 @@ struct AudioDecode { decoder: PcmDecoder, encoder: AacEncoder, decoded_channels: u16, - /// Decode time of the next AAC frame, in samples. + /// Decode time of the next AAC frame, in samples. `None` until enough of + /// the track has been seen to say where the run belongs. next_dts: Option, + /// Each packet's own account of where the run starts. Read once, by + /// [`super::run_anchor`], and then dropped. + anchors: Vec, + /// Samples decoded so far, which is what each estimate is measured against. + decoded: u64, + /// Frames encoded before the run could be placed. + held: Vec>, } +/// Packets to hear from before placing the run. +/// +/// A single Matroska timestamp is only good to a millisecond, and on a track +/// whose frames are not a whole number of them it can be seventy-five out — +/// enough to lose lip-sync for the length of the film, because a progressive +/// stream anchors once and never again. A hundred of them settle it. Two +/// seconds of video accumulate before the first fragment is written, which is +/// more audio packets than this for every codec here, so nothing is delayed by +/// the wait. +const ANCHOR_PACKETS: usize = 96; + impl ProgressiveStream { /// Open `path` positioned at `start_secs`, ready to emit fragments. /// @@ -240,33 +259,35 @@ impl ProgressiveStream { let decode = match audio.decode.as_mut() { Some(decode) => decode, None => { - let (decoder, primed) = + let (decoder, mut primed) = PcmDecoder::open(codec, sample_rate, Some(DECODED_CHANNELS), data)?; let decoded_channels = decoder.channels(); let encoder = AacEncoder::new(sample_rate, DECODED_CHANNELS)?; + // The probe frame contributes the sample count its own header + // declared, like every frame after it, so the running position + // the estimates are measured against stays true. + if let Some(samples) = super::frames::frame_samples(codec, data) { + primed.resize(samples as usize * decoded_channels as usize * 2, 0); + } audio.decode = Some(AudioDecode { codec, decoder, encoder, decoded_channels, - // One frame early, cancelling the encoder's delay: its MDCT - // window spans the previous hop and this one, so a decoder's - // output trails its input by exactly one frame. - next_dts: Some(ticks.saturating_sub(super::AAC_FRAME_SAMPLES)), + next_dts: None, + anchors: Vec::new(), + decoded: 0, + held: Vec::new(), }); let decode = audio.decode.as_mut().unwrap(); - let pcm = super::fit_channels(&primed, decoded_channels, DECODED_CHANNELS); - let adts = decode.encoder.push(&pcm)?; - push_aac(&mut audio.sink, decode, &adts); + decode.take(&mut audio.sink, ticks, primed)?; return Ok(()); } }; let expect = super::frames::frame_samples(decode.codec, data); let pcm = decode.decoder.decode_or_silence(data, expect); - let pcm = super::fit_channels(&pcm, decode.decoded_channels, DECODED_CHANNELS); - let adts = decode.encoder.push(&pcm)?; - push_aac(&mut audio.sink, decode, &adts); + decode.take(&mut audio.sink, ticks, pcm)?; Ok(()) } @@ -278,11 +299,19 @@ impl ProgressiveStream { return; }; let tail = decode.encoder.finish(); - push_aac(&mut audio.sink, decode, &tail); + push_aac(&mut audio.sink, decode, &tail, true); } /// Wrap whatever both tracks hold into one fragment. fn emit(&mut self) -> Option> { + // A fragment about to be written cannot wait for more packets before + // deciding where the audio run sits, so this is where the decision is + // forced if it has not been made already. + if let Some(audio) = self.audio.as_mut() { + if let Some(decode) = audio.decode.as_mut() { + push_aac(&mut audio.sink, decode, &[], true); + } + } let video_packets = self.video.take(); let audio_packets = self .audio @@ -313,19 +342,56 @@ impl ProgressiveStream { } } -/// Append the AAC frames in `adts` to the audio track's pending run. -fn push_aac(sink: &mut TrackSink, decode: &mut AudioDecode, adts: &[u8]) { +/// Hold the AAC frames in `adts`, and hand over everything held once the run's +/// place on the film's timeline is settled. +/// +/// `settle` decides that with however many packets have been seen so far, +/// because the caller is about to write a fragment and cannot wait. +fn push_aac(sink: &mut TrackSink, decode: &mut AudioDecode, adts: &[u8], settle: bool) { for payload in super::adts_payloads(adts) { - let dts = decode.next_dts.unwrap_or(0); + decode.held.push(payload.to_vec()); + } + if decode.next_dts.is_none() { + if !settle && decode.anchors.len() < ANCHOR_PACKETS { + return; + } + if decode.anchors.is_empty() { + return; + } + let mut anchors = std::mem::take(&mut decode.anchors); + // Placed early by exactly the encoder's delay, which is what a decoder's + // output trails its input by. Nothing here has to land on a frame + // boundary — this is one continuous run, not a tile of a grid other + // requests also write to — so the shift is the measured sample count + // rather than a rounded number of frames. + let anchor = super::run_anchor(&mut anchors).saturating_sub(super::ENCODER_DELAY as i64); + decode.next_dts = Some(anchor.max(0) as u64); + } + let mut dts = decode.next_dts.unwrap_or(0); + for payload in decode.held.drain(..) { sink.pending.push(MediaPacket { track_id: sink.track.id, pts: dts, dts, duration: super::AAC_FRAME_SAMPLES, is_keyframe: true, - data: payload.to_vec(), + data: payload, }); - decode.next_dts = Some(dts + super::AAC_FRAME_SAMPLES); + dts += super::AAC_FRAME_SAMPLES; + } + decode.next_dts = Some(dts); +} + +impl AudioDecode { + /// Take one frame's decoded PCM: note where the packet says the run starts, + /// widen the samples to the output's channel count, and encode them. + fn take(&mut self, sink: &mut TrackSink, ticks: u64, pcm: Vec) -> Result<()> { + self.anchors.push(ticks as i64 - self.decoded as i64); + self.decoded += (pcm.len() / (self.decoded_channels as usize * 2)) as u64; + let pcm = super::fit_channels(&pcm, self.decoded_channels, DECODED_CHANNELS); + let adts = self.encoder.push(&pcm)?; + push_aac(sink, self, &adts, false); + Ok(()) } } diff --git a/crates/vuio-core/src/web/remux_streaming.rs b/crates/vuio-core/src/web/remux_streaming.rs index ef6bbe0f..7cec8359 100644 --- a/crates/vuio-core/src/web/remux_streaming.rs +++ b/crates/vuio-core/src/web/remux_streaming.rs @@ -200,24 +200,13 @@ pub async fn serve_hls_audio_init_segment( fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> Vec { let out_track = rendition_track(track); let timescale = Fmp4Writer::timescale_for(&out_track); - let is_reencoded_audio = track.codec_kind.transcode_codec().is_some(); - - // For re-encoded audio (AC-3/DTS -> AAC), align segment durations to integer AAC frames (1024 samples) - // so no segment ends on a fractional frame or requires zero-padding silence. - let (start_secs, target_duration_secs, nominal_decode_time) = if is_reencoded_audio { - let sample_rate = out_track.sample_rate.unwrap_or(48_000); - let frames_per_seg = - ((SEGMENT_DURATION_SECS as f64 * sample_rate as f64) / 1024.0).round() as u64; - let seg_samples = frames_per_seg * 1024; - let start_sample = seq as u64 * seg_samples; - let start_s = start_sample as f64 / sample_rate as f64; - let dur_s = seg_samples as f64 / sample_rate as f64; - (start_s, dur_s, start_sample) - } else { - let start_s = seq as f64 * SEGMENT_DURATION_SECS as f64; - let nominal = (start_s * timescale as f64).round() as u64; - (start_s, SEGMENT_DURATION_SECS as f64, nominal) - }; + let start_secs = seq as f64 * SEGMENT_DURATION_SECS as f64; + let nominal_decode_time = (start_secs * timescale as f64).round() as u64; + + #[cfg(all(feature = "transcode-aac", feature = "demux"))] + if let Some(codec) = track.codec_kind.transcode_codec() { + return build_reencoded_segment(path, track, &out_track, codec, seq, timescale); + } let packets = MkvDemuxer::extract_track_packets( path, @@ -225,27 +214,73 @@ fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> V track.codec_kind, timescale, start_secs, - target_duration_secs, + SEGMENT_DURATION_SECS as f64, ) .unwrap_or_default(); - #[cfg(all(feature = "transcode-aac", feature = "demux"))] - let packets = match track.codec_kind.transcode_codec() { - Some(codec) => crate::media::transcode::reencode_to_aac( - codec, - &packets, - out_track.sample_rate.unwrap_or(48_000), - DECODED_CHANNELS, - out_track.id, - Some(nominal_decode_time), - ) - .unwrap_or_default(), - None => packets, - }; - Fmp4Writer::build_segment(seq + 1, &out_track, nominal_decode_time, &packets) } +/// The longest a source frame of any codec this decodes can be, in samples. +/// +/// DTS Core's `NBLKS` tops out at 127, for `(127 + 1) * 32` samples; AC-3 and +/// E-AC-3 are 1536. Only used as a guard band on the demuxer request below, so +/// generous is free and short is a lost frame of pre-roll. +#[cfg(all(feature = "transcode-aac", feature = "demux"))] +const LONGEST_SOURCE_FRAME: i64 = 4096; + +/// One segment of an audio track that has to be decoded and re-encoded. +/// +/// Different from the passthrough path in what it asks the demuxer for: not +/// this segment's four seconds, but the span the encoder has to be *fed* to +/// produce this segment's frames — which reaches back before the segment +/// begins, to cancel the encoder's delay and to warm its MDCT window up on real +/// audio, and forward past where it ends, because that same delay means the +/// last frame is not finished until samples beyond it have been seen. The +/// re-encode positions what it gets by absolute sample index, so the guard band +/// on the request costs a little decoding and nothing else. +#[cfg(all(feature = "transcode-aac", feature = "demux"))] +fn build_reencoded_segment( + path: &std::path::Path, + track: &TrackInfo, + out_track: &TrackInfo, + codec: crate::media::transcode::TranscodeCodec, + seq: u32, + timescale: u32, +) -> Vec { + use crate::media::transcode::AacWindow; + + let rate = out_track.sample_rate.unwrap_or(48_000) as u64; + let segment_samples = SEGMENT_DURATION_SECS as u64 * rate; + let window = AacWindow::covering(seq as u64 * segment_samples, (seq as u64 + 1) * segment_samples); + + let (from, len) = window.source_span(); + let request_from = (from - LONGEST_SOURCE_FRAME).max(0); + let request_len = (from + len as i64 - request_from).max(0); + + let packets = MkvDemuxer::extract_track_packets( + path, + track.id, + track.codec_kind, + timescale, + request_from as f64 / timescale as f64, + request_len as f64 / timescale as f64, + ) + .unwrap_or_default(); + + let packets = crate::media::transcode::reencode_to_aac( + codec, + &packets, + rate as u32, + DECODED_CHANNELS, + out_track.id, + window, + ) + .unwrap_or_default(); + + Fmp4Writer::build_segment(seq + 1, out_track, window.start_sample(), &packets) +} + /// Build (or recall) one segment and wrap it in a response. /// /// Three things happen here that did not when this path only copied bytes. The diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs index d97ddad5..f39e75d9 100644 --- a/crates/vuio-core/tests/film_transcode_tests.rs +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -481,10 +481,65 @@ async fn an_audio_segment_carries_real_re_encoded_aac() { "ADTS framing leaked into an MP4 sample" ); - // The second segment begins four seconds in, matching the AAC frame-aligned decode time. + // Not at its nominal four seconds: an AAC frame is 1024 samples, four + // seconds of 48 kHz is 187.5 of them, and a segment opens on the film-wide + // frame grid rather than half way through a frame. let tfdt = find_box(&segment, "tfdt").expect("a tfdt"); let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); - assert_eq!(base, 188 * 1024, "the run sits at the segment boundary"); + assert_eq!(base, 188 * 1024, "the run opens on the frame grid"); +} + +/// The defect a browser hears as a tick every four seconds. +/// +/// Each segment is built by its own request, on its own thread, from nothing +/// but its sequence number — so if they disagree by even one frame about where +/// they sit, the player's source buffer resolves the collision by throwing +/// samples away. What is asserted is the only thing that rules that out: each +/// segment opens exactly where the previous one's last sample ended, and the +/// run of them keeps time with the wall clock rather than drifting a fraction +/// of a frame per segment. +#[tokio::test] +async fn consecutive_audio_segments_meet_without_a_gap_or_an_overlap() { + const FRAME: u64 = 1024; + let (_temp, state, id) = scanned_film(24.0).await; + + let mut opens_at: Option = None; + let mut first_open = 0u64; + let segments = 5u32; + for seq in 0..segments { + let (status, _, segment) = get( + &state, + &format!("/media/{id}/hls/audio/0/segment/{seq}"), + Method::GET, + None, + ) + .await; + assert_eq!(status, StatusCode::OK, "segment {seq}"); + + let tfdt = find_box(&segment, "tfdt").expect("a tfdt"); + let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); + let trun = find_box(&segment, "trun").expect("a trun"); + let samples = u64::from(u32::from_be_bytes(trun[4..8].try_into().unwrap())); + + match opens_at { + None => first_open = base, + Some(expected) => assert_eq!( + base, + expected, + "segment {seq} opens at {base}, but segment {} ended at {expected}", + seq - 1 + ), + } + opens_at = Some(base + samples * FRAME); + } + + // Four seconds a segment on average, to within the frame the grid rounds by. + let covered = opens_at.unwrap() - first_open; + let nominal = u64::from(segments) * 4 * 48_000; + assert!( + covered.abs_diff(nominal) < FRAME, + "{segments} segments covered {covered} samples where four seconds each is {nominal}" + ); } #[tokio::test] From 59366ac7da477cee5672ff4819c73564c6cb75cb Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 23:45:19 +0300 Subject: [PATCH 16/38] audio fix 2 --- crates/vuio-core/src/media/remux/hls.rs | 116 +++-- crates/vuio-core/src/media/remux/mkv_cues.rs | 493 ++++++++++++++++++ .../vuio-core/src/media/remux/mkv_demuxer.rs | 85 ++- crates/vuio-core/src/media/remux/mod.rs | 1 + crates/vuio-core/src/web/remux_streaming.rs | 275 ++++++++-- .../vuio-core/tests/film_transcode_tests.rs | 56 +- crates/vuio-core/tests/zz_scratch_diag.rs | 145 ++++++ 7 files changed, 1045 insertions(+), 126 deletions(-) create mode 100644 crates/vuio-core/src/media/remux/mkv_cues.rs create mode 100644 crates/vuio-core/tests/zz_scratch_diag.rs diff --git a/crates/vuio-core/src/media/remux/hls.rs b/crates/vuio-core/src/media/remux/hls.rs index 85ce58e0..1f5535af 100644 --- a/crates/vuio-core/src/media/remux/hls.rs +++ b/crates/vuio-core/src/media/remux/hls.rs @@ -13,7 +13,16 @@ impl HlsGenerator { /// build compiled without the matching decoder — or a codec nothing here decodes, /// TrueHD being the one that turns up in real libraries — drops the rendition /// rather than offering one that would arrive silent. - pub fn build_master_playlist(_media_id: &str, tracks: &[TrackInfo]) -> String { + /// + /// `independent_segments` says whether every segment really does open on a + /// keyframe. Claiming it when it is not true tells a player it may start + /// decoding at any segment boundary, which it then does, to a blank picture + /// until the next keyframe arrives. + pub fn build_master_playlist( + _media_id: &str, + tracks: &[TrackInfo], + independent_segments: bool, + ) -> String { let Some(video_track) = browser_video_track(tracks) else { // No browser-playable video track: a variant-less master playlist fails // hls.js's manifest parse, which routes the player to its existing @@ -27,7 +36,10 @@ impl HlsGenerator { let mut playlist = String::new(); playlist.push_str("#EXTM3U\n"); playlist.push_str("#EXT-X-VERSION:6\n"); - playlist.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n\n"); + if independent_segments { + playlist.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n"); + } + playlist.push('\n'); let video_codec = video_codec_string(video_track); @@ -66,43 +78,57 @@ impl HlsGenerator { /// /// Every rendition's init segment and segments live alongside its own playlist /// (`init.mp4`, `segment/{n}`), so this is identical for video and audio callers. - pub fn build_media_playlist(total_duration_secs: f64, segment_duration_secs: u32) -> String { - let segment_duration_secs = segment_duration_secs.max(1); - let total_duration_secs = total_duration_secs.max(0.0); - let segment_count = (total_duration_secs / segment_duration_secs as f64) + /// + /// `boundaries` is where each segment starts followed by where the last one + /// ends — one more value than there are segments. They are not assumed to be + /// evenly spaced, because for a film they are not: a segment opens on a + /// keyframe, and a Blu-ray remux puts one every eight to twelve seconds. What + /// a playlist may never do is state a duration the segment does not have. A + /// player builds its whole timeline out of these numbers, and one that + /// disagrees with the media by even a fraction stalls on the difference. + pub fn build_media_playlist(boundaries: &[f64]) -> String { + let target = boundaries + .windows(2) + .map(|w| w[1] - w[0]) + .fold(0.0f64, f64::max) .ceil() - .max(1.0) as usize; + .max(1.0) as u64; let mut playlist = String::new(); playlist.push_str("#EXTM3U\n"); playlist.push_str("#EXT-X-VERSION:6\n"); - playlist.push_str(&format!( - "#EXT-X-TARGETDURATION:{}\n", - segment_duration_secs - )); + playlist.push_str(&format!("#EXT-X-TARGETDURATION:{target}\n")); playlist.push_str("#EXT-X-MEDIA-SEQUENCE:0\n"); playlist.push_str("#EXT-X-PLAYLIST-TYPE:VOD\n"); playlist.push_str("#EXT-X-MAP:URI=\"init.mp4\"\n\n"); - let mut remaining = total_duration_secs; - for i in 0..segment_count { - // Every segment is `segment_duration_secs` except the last, which is - // whatever real time is left — declaring the nominal duration for a - // shorter final segment drifts the reported vs. actual timeline and can - // make players stall waiting for content that was never coming. - let this_duration = if i + 1 == segment_count { - remaining.max(0.0) - } else { - segment_duration_secs as f64 - }; - remaining -= this_duration; - - playlist.push_str(&format!("#EXTINF:{:.3},\nsegment/{}\n", this_duration, i)); + for (i, pair) in boundaries.windows(2).enumerate() { + playlist.push_str(&format!( + "#EXTINF:{:.3},\nsegment/{}\n", + (pair[1] - pair[0]).max(0.0), + i + )); } playlist.push_str("#EXT-X-ENDLIST\n"); playlist } + + /// An evenly spaced set of boundaries, for a film whose keyframes are not + /// known. + /// + /// The last segment is whatever real time is left rather than another whole + /// one: declaring the nominal duration for a short final segment drifts the + /// reported timeline against the real one and can leave a player waiting for + /// content that was never coming. + pub fn uniform_boundaries(total_duration_secs: f64, segment_duration_secs: u32) -> Vec { + let step = f64::from(segment_duration_secs.max(1)); + let total = total_duration_secs.max(0.0); + let count = (total / step).ceil().max(1.0) as usize; + let mut boundaries: Vec = (0..count).map(|i| i as f64 * step).collect(); + boundaries.push(total.max(boundaries.last().copied().unwrap_or(0.0))); + boundaries + } } /// Derive an `avc1.PPCCLL`/generic `hvc1...` CODECS string. hls.js/MSE only use this for @@ -269,7 +295,7 @@ mod tests { audio_track(3, TrackCodec::Aac, "Spanish", "spa"), ]; - let master = HlsGenerator::build_master_playlist("test-id", &tracks); + let master = HlsGenerator::build_master_playlist("test-id", &tracks, true); assert!(master.contains("#EXT-X-MEDIA:TYPE=AUDIO")); assert!(master.contains("NAME=\"English\"")); assert!(master.contains("NAME=\"Spanish\"")); @@ -286,7 +312,7 @@ mod tests { audio_track(2, TrackCodec::Unsupported, "TrueHD Atmos", "eng"), ]; - let master = HlsGenerator::build_master_playlist("test-id", &tracks); + let master = HlsGenerator::build_master_playlist("test-id", &tracks, true); assert!(!master.contains("#EXT-X-MEDIA:TYPE=AUDIO")); assert!(!master.contains("AUDIO=\"audio\"")); assert!(master.contains("video/index.m3u8")); @@ -302,7 +328,7 @@ mod tests { video_track(TrackCodec::Avc), audio_track(2, TrackCodec::Ac3, "5.1 English", "eng"), ]; - let master = HlsGenerator::build_master_playlist("test-id", &tracks); + let master = HlsGenerator::build_master_playlist("test-id", &tracks, true); if TrackCodec::Ac3.is_playable() { assert!(master.contains("#EXT-X-MEDIA:TYPE=AUDIO")); @@ -321,7 +347,7 @@ mod tests { video_track(TrackCodec::Avc), audio_track(2, TrackCodec::Dts, "DTS", "eng"), ]; - let master = HlsGenerator::build_master_playlist("test-id", &tracks); + let master = HlsGenerator::build_master_playlist("test-id", &tracks, true); assert_eq!( master.contains("#EXT-X-MEDIA:TYPE=AUDIO"), TrackCodec::Dts.is_playable(), @@ -332,7 +358,7 @@ mod tests { #[test] fn test_master_playlist_no_supported_video_is_variant_less() { let tracks = vec![video_track(TrackCodec::Unsupported)]; - let master = HlsGenerator::build_master_playlist("test-id", &tracks); + let master = HlsGenerator::build_master_playlist("test-id", &tracks, true); assert!(!master.contains("#EXT-X-STREAM-INF")); } @@ -340,7 +366,8 @@ mod tests { fn test_media_playlist_final_segment_uses_real_remainder() { // 10 seconds of content at a 4-second target: 4, 4, then a 2-second remainder — // not another 4-second entry that overruns the real content. - let playlist = HlsGenerator::build_media_playlist(10.0, 4); + let playlist = + HlsGenerator::build_media_playlist(&HlsGenerator::uniform_boundaries(10.0, 4)); assert!(playlist.contains("#EXTINF:4.000,\nsegment/0\n")); assert!(playlist.contains("#EXTINF:4.000,\nsegment/1\n")); assert!(playlist.contains("#EXTINF:2.000,\nsegment/2\n")); @@ -348,6 +375,33 @@ mod tests { assert!(playlist.contains("#EXT-X-MAP:URI=\"init.mp4\"")); } + /// A film's keyframes are not four seconds apart, and a playlist that says + /// they are is the defect: the player builds its timeline from these + /// numbers, seeks to where they say the next segment begins, and stalls on + /// finding something else there. + #[test] + fn test_media_playlist_states_the_real_length_of_uneven_segments() { + let playlist = HlsGenerator::build_media_playlist(&[0.0, 12.429, 22.814, 33.242]); + assert!(playlist.contains("#EXTINF:12.429,\nsegment/0\n"), "{playlist}"); + assert!(playlist.contains("#EXTINF:10.385,\nsegment/1\n"), "{playlist}"); + assert!(playlist.contains("#EXTINF:10.428,\nsegment/2\n"), "{playlist}"); + assert!(!playlist.contains("segment/3")); + // The target duration has to cover the longest of them, or a player is + // entitled to treat the playlist as malformed. + assert!(playlist.contains("#EXT-X-TARGETDURATION:13\n"), "{playlist}"); + } + + /// The tag is a promise that a player may start decoding at any segment. It + /// is only true when the boundaries came from the film's own keyframes. + #[test] + fn test_master_playlist_only_claims_independent_segments_when_they_are() { + let tracks = vec![video_track(TrackCodec::Avc)]; + assert!(HlsGenerator::build_master_playlist("id", &tracks, true) + .contains("#EXT-X-INDEPENDENT-SEGMENTS")); + assert!(!HlsGenerator::build_master_playlist("id", &tracks, false) + .contains("#EXT-X-INDEPENDENT-SEGMENTS")); + } + #[test] fn test_video_codec_string_uses_real_avcc_bytes() { let track = video_track(TrackCodec::Avc); diff --git a/crates/vuio-core/src/media/remux/mkv_cues.rs b/crates/vuio-core/src/media/remux/mkv_cues.rs new file mode 100644 index 00000000..47bfabfa --- /dev/null +++ b/crates/vuio-core/src/media/remux/mkv_cues.rs @@ -0,0 +1,493 @@ +//! Reading a Matroska's own index, to find out where its keyframes are. +//! +//! An HLS segment has to open on a random-access point, or a player that starts +//! there has nothing to decode the first picture against. Films do not oblige by +//! putting one every four seconds: a Blu-ray remux runs eight to twelve seconds +//! between them, so a four-second grid asks for boundaries that mostly are not +//! keyframes, and a segmenter that rounds forward to the next one hands the +//! player a stretch of film several segments further on than it asked for. The +//! picture stops within a few seconds of pressing play. +//! +//! The fix needs the keyframe times, and the file already knows them: that is +//! precisely what the `Cues` element is. This reads it — a few hundred kilobytes +//! at a position the `SeekHead` gives directly — rather than demuxing the track +//! to find out, which for a thirty-gigabyte film means reading thirty gigabytes. +//! +//! Symphonia parses the same element internally and seeks by it, but exposes +//! neither the cue list nor a seek that lands on one (its `Coarse` seek resolves +//! to the nearest *block*, which is how the segmenter came to be asking for +//! non-keyframe boundaries in the first place). So the element is read here. It +//! is a flat list, and nothing below cares about the rest of the container. + +use anyhow::{Context, Result}; +use std::fs::File; +use std::io::{BufReader, Read, Seek, SeekFrom}; +use std::path::Path; + +/// EBML element ids, as the four documented classes of variable-length integer +/// with their length marker left in place — which is how they appear on the +/// wire, and why they are written a byte at a time here. +mod id { + pub const EBML: u32 = 0x1A_45_DF_A3; + pub const SEGMENT: u32 = 0x18_53_80_67; + pub const SEEK_HEAD: u32 = 0x11_4D_9B_74; + pub const SEEK: u32 = 0x4D_BB; + pub const SEEK_ID: u32 = 0x53_AB; + pub const SEEK_POSITION: u32 = 0x53_AC; + pub const INFO: u32 = 0x15_49_A9_66; + pub const TIMESTAMP_SCALE: u32 = 0x2A_D7_B1; + pub const CUES: u32 = 0x1C_53_BB_6B; + pub const CUE_POINT: u32 = 0xBB; + pub const CUE_TIME: u32 = 0xB3; + pub const CUE_TRACK_POSITIONS: u32 = 0xB7; + pub const CUE_TRACK: u32 = 0xF7; +} + +/// The default `TimestampScale`, in nanoseconds per tick: Matroska's cue and +/// cluster timestamps are in these, and almost every file uses milliseconds. +const DEFAULT_TIMESTAMP_SCALE: u64 = 1_000_000; + +/// Top-level elements to walk past while looking for `Cues` without a +/// `SeekHead` to follow. +/// +/// A film is a few thousand clusters, and walking them costs one header read +/// each — cheap, but not unbounded: a file whose element sizes are nonsense +/// must not turn a playlist request into a scan of the whole disk. +const MAX_TOP_LEVEL_ELEMENTS: usize = 100_000; + +/// The most of one element this will read into memory. A two-and-a-half hour +/// film indexes itself in a few hundred kilobytes; this is room for an order of +/// magnitude more without letting a corrupt size become an allocation. +const MAX_ELEMENT_BYTES: u64 = 64 * 1024 * 1024; + +/// The times, in milliseconds, at which `track_number` has a cue point. +/// +/// For a video track those are its keyframes — that is what a cue point is for. +/// An empty result is not an error: plenty of files carry no index, or index +/// only some of their tracks, and the caller falls back to a fixed grid. +pub fn cue_times_ms(path: &Path, track_number: u64) -> Result> { + let file = File::open(path) + .with_context(|| format!("opening {} to read its cue index", path.display()))?; + let mut reader = Reader { + end: file.metadata().map(|m| m.len()).unwrap_or(u64::MAX), + inner: BufReader::with_capacity(64 * 1024, file), + }; + + let segment = reader.find_segment()?; + let (scale, cues) = reader.find_cues(&segment)?; + let Some(cues) = cues else { + return Ok(Vec::new()); + }; + + // One contiguous read, then all of the walking happens in memory. Seeking + // per element instead would be a few tens of thousands of seeks on a film, + // each throwing away the read buffer it just filled. + let body = reader.read_body(&cues)?; + let mut times = cue_times(&body, track_number); + // Cue points are meant to be written in order, and are not always. Anything + // downstream treats these as a timeline, so make them one. + times.sort_unstable(); + times.dedup(); + // A tick is a millisecond in every file anyone ships, but the header is + // allowed to say otherwise and then the numbers mean something else. + if scale != DEFAULT_TIMESTAMP_SCALE { + for time in &mut times { + *time = time.saturating_mul(scale) / 1_000_000; + } + } + Ok(times) +} + +/// Every cue point in `cues` belonging to `track_number`, in Matroska ticks. +fn cue_times(cues: &[u8], track_number: u64) -> Vec { + let mut times = Vec::new(); + let mut points = Cursor::new(cues); + while let Some((id, point)) = points.read_element() { + if id != id::CUE_POINT { + continue; + } + let mut time: Option = None; + let mut wanted = false; + let mut fields = Cursor::new(point); + while let Some((field, body)) = fields.read_element() { + match field { + id::CUE_TIME => time = Some(uint(body)), + // One cue point can index several tracks at the same instant, + // so this is a search rather than a read. + id::CUE_TRACK_POSITIONS => { + let mut positions = Cursor::new(body); + while let Some((inner, at)) = positions.read_element() { + if inner == id::CUE_TRACK && uint(at) == track_number { + wanted = true; + } + } + } + _ => {} + } + } + if let (Some(time), true) = (time, wanted) { + times.push(time); + } + } + times +} + +/// An EBML element's body read as an unsigned integer, big-endian. +fn uint(bytes: &[u8]) -> u64 { + bytes + .iter() + .take(8) + .fold(0u64, |acc, b| (acc << 8) | u64::from(*b)) +} + +/// A walk over the elements of one element's body, in memory. +struct Cursor<'a> { + bytes: &'a [u8], + at: usize, +} + +impl<'a> Cursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, at: 0 } + } + + /// Read one variable-length integer, returning it and its width. + /// + /// `keep_marker` distinguishes the two uses EBML puts these to: an element + /// id is the bytes as they stand, marker included, while a size is the + /// value with the marker stripped. + fn read_vint(&mut self, keep_marker: bool) -> Option<(u64, u32)> { + let lead = *self.bytes.get(self.at)?; + if lead == 0 { + // Five bytes or more: legal for a size, never used, and not worth + // carrying a slow path for. + return None; + } + let width = lead.leading_zeros() + 1; + // At the widest, every bit of the first byte is marker. + let value_bits = if width >= 8 { 0 } else { 0xFFu8 >> width }; + let mut value = if keep_marker { + u64::from(lead) + } else { + u64::from(lead & value_bits) + }; + for offset in 1..width as usize { + value = (value << 8) | u64::from(*self.bytes.get(self.at + offset)?); + } + self.at += width as usize; + Some((value, width)) + } + + /// The next element's id and body, or `None` at the end — or on anything + /// malformed, because a truncated index is a missing index rather than a + /// reason to fail a request. + fn read_element(&mut self) -> Option<(u32, &'a [u8])> { + let (id, _) = self.read_vint(true)?; + let (size, _) = self.read_vint(false)?; + let size = usize::try_from(size).ok()?; + let end = self.at.checked_add(size)?.min(self.bytes.len()); + let body = self.bytes.get(self.at..end)?; + self.at = end; + Some((u32::try_from(id).ok()?, body)) + } +} + +/// A `Segment` element's extent, which every `SeekHead` position is relative to. +struct Span { + start: u64, + end: u64, +} + +struct Reader { + inner: BufReader, + /// Length of the file, and so the end of any element that declares itself + /// unknown-length (which a Segment written by a live muxer does). + end: u64, +} + +impl Reader { + fn position(&mut self) -> Result { + Ok(self.inner.stream_position()?) + } + + fn seek_to(&mut self, at: u64) -> Result<()> { + self.inner.seek(SeekFrom::Start(at))?; + Ok(()) + } + + /// Read one variable-length integer, returning it and its width. + /// + /// `keep_marker` distinguishes the two uses EBML puts these to: an element + /// id is the bytes as they stand, marker included, while a size is the + /// value with the marker stripped. + fn read_vint(&mut self, keep_marker: bool) -> Result> { + let mut first = [0u8; 1]; + if self.inner.read_exact(&mut first).is_err() { + return Ok(None); + } + let lead = first[0]; + if lead == 0 { + // Five bytes or more: legal for a size, never used, and not worth + // carrying a slow path for. + return Ok(None); + } + let width = lead.leading_zeros() + 1; + // At the widest, every bit of the first byte is marker. + let value_bits = if width >= 8 { 0 } else { 0xFFu8 >> width }; + let mut value = if keep_marker { + u64::from(lead) + } else { + u64::from(lead & value_bits) + }; + for _ in 1..width { + let mut next = [0u8; 1]; + if self.inner.read_exact(&mut next).is_err() { + return Ok(None); + } + value = (value << 8) | u64::from(next[0]); + } + Ok(Some((value, width))) + } + + /// Read an element header: its id, and the extent of its body. + /// + /// `None` at the end of the enclosing element, or on anything malformed — + /// a truncated index is a missing index, not a reason to fail a request. + fn read_header(&mut self, limit: u64) -> Result> { + if self.position()? >= limit { + return Ok(None); + } + let Some((id, _)) = self.read_vint(true)? else { + return Ok(None); + }; + let Some((size, width)) = self.read_vint(false)? else { + return Ok(None); + }; + let body = self.position()?; + // A size whose every value bit is set means "unknown", which only a + // Segment or a Cluster uses and which then runs to the end of its + // parent. + let unknown = size == (1u64 << (7 * width)) - 1; + let end = if unknown { + limit + } else { + body.saturating_add(size).min(limit) + }; + Ok(Some((u32::try_from(id).unwrap_or(0), Span { start: body, end }))) + } + + /// Read an element's body into memory. + /// + /// Capped, because the size is a number in the file and a corrupt one must + /// not become an allocation. A film's index is a few hundred kilobytes. + fn read_body(&mut self, span: &Span) -> Result> { + let len = (span.end.saturating_sub(span.start)).min(MAX_ELEMENT_BYTES) as usize; + let mut body = vec![0u8; len]; + self.seek_to(span.start)?; + self.inner.read_exact(&mut body)?; + Ok(body) + } + + /// Find the `Segment`, which everything else lives inside. + fn find_segment(&mut self) -> Result { + self.seek_to(0)?; + let file_end = self.end; + while let Some((id, span)) = self.read_header(file_end)? { + match id { + id::SEGMENT => return Ok(span), + // The EBML header, and anything else preceding the Segment. + id::EBML => self.seek_to(span.end)?, + _ => self.seek_to(span.end)?, + } + } + anyhow::bail!("no Matroska Segment element") + } + + /// Locate `Cues` and read the `TimestampScale`, following the `SeekHead` + /// where there is one and walking the top level where there is not. + fn find_cues(&mut self, segment: &Span) -> Result<(u64, Option)> { + let mut scale = DEFAULT_TIMESTAMP_SCALE; + let mut pointer: Option = None; + let mut info_seen = false; + + self.seek_to(segment.start)?; + let mut seen = 0usize; + while let Some((id, span)) = self.read_header(segment.end)? { + seen += 1; + if seen > MAX_TOP_LEVEL_ELEMENTS { + break; + } + match id { + id::SEEK_HEAD => pointer = self.read_seek_head(&span, segment)?.or(pointer), + id::INFO => { + info_seen = true; + scale = self.read_timestamp_scale(&span)?.unwrap_or(scale); + } + id::CUES => return Ok((scale, Some(span))), + _ => {} + } + // Following the pointer is the whole point of a `SeekHead`: it + // turns a walk over every cluster of a thirty-gigabyte film into + // two reads. Not taken before `Info`, because the scale those cue + // times are in is only known once it has been read — and `Info` + // always precedes the clusters. + if info_seen { + if let Some(at) = pointer.take() { + if let Some(cues) = self.cues_at(at, segment)? { + return Ok((scale, Some(cues))); + } + // The pointer did not lead to `Cues`, so it is worth + // nothing; carry on walking from where the walk had got to. + } + } + self.seek_to(span.end)?; + } + Ok((scale, None)) + } + + /// The element at `at`, if that is where `Cues` turns out to be. + fn cues_at(&mut self, at: u64, segment: &Span) -> Result> { + if at <= segment.start || at >= segment.end { + return Ok(None); + } + self.seek_to(at)?; + Ok(match self.read_header(segment.end)? { + Some((id::CUES, span)) => Some(span), + _ => None, + }) + } + + /// The position a `SeekHead` gives for `Cues`, as an absolute file offset. + fn read_seek_head(&mut self, span: &Span, segment: &Span) -> Result> { + let body = self.read_body(span)?; + let mut entries = Cursor::new(&body); + let mut found = None; + while let Some((id, entry)) = entries.read_element() { + if id != id::SEEK { + continue; + } + let mut fields = Cursor::new(entry); + let mut target: Option = None; + let mut at: Option = None; + while let Some((field, value)) = fields.read_element() { + match field { + id::SEEK_ID => target = u32::try_from(uint(value)).ok(), + id::SEEK_POSITION => at = Some(uint(value)), + _ => {} + } + } + if target == Some(id::CUES) { + // Seek positions are relative to the Segment's first byte. + found = at.map(|at| segment.start.saturating_add(at)); + } + } + Ok(found) + } + + /// The `TimestampScale` inside an `Info` element. + fn read_timestamp_scale(&mut self, span: &Span) -> Result> { + let body = self.read_body(span)?; + let mut fields = Cursor::new(&body); + let mut scale = None; + while let Some((id, value)) = fields.read_element() { + if id == id::TIMESTAMP_SCALE { + scale = Some(uint(value)); + } + } + Ok(scale.filter(|s| *s > 0)) + } + +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Write an EBML element: id (already marker-bearing), then an eight-byte + /// size, then the body. + fn element(id: u32, body: &[u8]) -> Vec { + let mut out = Vec::new(); + let id_bytes = id.to_be_bytes(); + let lead = id_bytes.iter().position(|b| *b != 0).unwrap_or(3); + out.extend_from_slice(&id_bytes[lead..]); + // The eight-byte size form: 0x01 then seven length bytes. + out.push(0x01); + out.extend_from_slice(&(body.len() as u64).to_be_bytes()[1..]); + out.extend_from_slice(body); + out + } + + fn uint(id: u32, value: u64) -> Vec { + element(id, &value.to_be_bytes()) + } + + /// A minimal but real Matroska skeleton: a header, then a Segment holding + /// Info, a Cues list, and nothing else that matters here. + fn skeleton(scale: u64, points: &[(u64, u64)]) -> Vec { + let mut cues = Vec::new(); + for (time, track) in points { + let mut point = uint(id::CUE_TIME, *time); + point.extend_from_slice(&element( + id::CUE_TRACK_POSITIONS, + &uint(id::CUE_TRACK, *track), + )); + cues.extend_from_slice(&element(id::CUE_POINT, &point)); + } + let mut segment = element(id::INFO, &uint(id::TIMESTAMP_SCALE, scale)); + segment.extend_from_slice(&element(id::CUES, &cues)); + + let mut file = element(id::EBML, &[0u8; 4]); + file.extend_from_slice(&element(id::SEGMENT, &segment)); + file + } + + fn read(bytes: &[u8], track: u64) -> Vec { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("Film.mkv"); + std::fs::write(&path, bytes).unwrap(); + cue_times_ms(&path, track).unwrap() + } + + #[test] + fn a_tracks_own_cue_points_come_back_in_milliseconds() { + let file = skeleton(1_000_000, &[(0, 1), (2_002, 1), (12_429, 1), (22_814, 1)]); + assert_eq!(read(&file, 1), vec![0, 2_002, 12_429, 22_814]); + } + + /// A film indexes its subtitle tracks too, and those cue points are not + /// keyframes of anything the segmenter is cutting. + #[test] + fn another_tracks_cue_points_are_not_this_ones() { + let file = skeleton(1_000_000, &[(0, 1), (500, 7), (2_002, 1), (900, 7)]); + assert_eq!(read(&file, 1), vec![0, 2_002]); + assert_eq!(read(&file, 7), vec![500, 900]); + assert!(read(&file, 3).is_empty()); + } + + /// The scale is a header field, not a constant, and the times mean nothing + /// without it. + #[test] + fn a_non_default_timestamp_scale_is_applied() { + // Ten-microsecond ticks: a hundred of them to the millisecond. + let file = skeleton(10_000, &[(0, 1), (200_200, 1), (1_242_900, 1)]); + assert_eq!(read(&file, 1), vec![0, 2_002, 12_429]); + } + + /// Plenty of files carry no index at all. That is a fixed grid's problem to + /// solve, not an error to fail a playlist request with. + #[test] + fn a_file_with_no_cues_reads_as_no_cues_rather_than_failing() { + let mut file = element(id::EBML, &[0u8; 4]); + file.extend_from_slice(&element( + id::SEGMENT, + &element(id::INFO, &uint(id::TIMESTAMP_SCALE, 1_000_000)), + )); + assert!(read(&file, 1).is_empty()); + } + + #[test] + fn cue_points_written_out_of_order_still_read_as_a_timeline() { + let file = skeleton(1_000_000, &[(12_429, 1), (0, 1), (2_002, 1), (2_002, 1)]); + assert_eq!(read(&file, 1), vec![0, 2_002, 12_429]); + } +} diff --git a/crates/vuio-core/src/media/remux/mkv_demuxer.rs b/crates/vuio-core/src/media/remux/mkv_demuxer.rs index 18332fa3..20f048b2 100644 --- a/crates/vuio-core/src/media/remux/mkv_demuxer.rs +++ b/crates/vuio-core/src/media/remux/mkv_demuxer.rs @@ -308,6 +308,10 @@ impl MkvDemuxer { // A safety valve, not a tuning knob: guards against runaway loops if a track's // packets never accumulate to `target_duration_secs` (e.g. a corrupt duration). const MAX_PACKETS_PER_SEGMENT: usize = 4096; + // How far before the requested time to aim the seek. One Matroska tick: + // enough to land at or before the block asked for, and short enough that + // nothing else is read to get there. + const SEEK_BACKOFF_SECS: f64 = 0.001; use symphonia::core::formats::probe::Hint; use symphonia::core::formats::{FormatOptions, SeekMode, SeekTo}; use symphonia::core::io::MediaSourceStream; @@ -350,7 +354,14 @@ impl MkvDemuxer { // Coarse lands on the container's own cue point (a keyframe) at or before the // requested time, where Accurate lands on the nearest sample — which is usually // mid-GOP, and produced segments a player could not start on. - let target_time = Time::try_from_secs_f64(start_secs.max(0.0)).unwrap_or(Time::ZERO); + // Backed off by a hair, because symphonia's seek lands on the first + // block whose presentation time is at or *after* the request, and asking + // for exactly a keyframe's own timestamp is answered with the block + // after it — half a second of film, gone, at the start of every segment. + // Asking a millisecond earlier lands one frame before the keyframe + // instead, which the filter below then drops. + let target_time = Time::try_from_secs_f64((start_secs - SEEK_BACKOFF_SECS).max(0.0)) + .unwrap_or(Time::ZERO); let _ = format.seek( SeekMode::Coarse, SeekTo::Time { @@ -363,13 +374,9 @@ impl MkvDemuxer { (start_secs.max(0.0) * output_timescale as f64).round() as u64; let target_ticks = (target_duration_secs.max(0.0) * output_timescale as f64).round() as u64; - let is_video = matches!(codec, TrackCodec::Avc | TrackCodec::Hevc); let mut packets = Vec::new(); let mut accumulated_ticks: u64 = 0; - // Where the segment began on the presentation timeline, for the elapsed - // check below. - let mut first_pts: Option = None; loop { if packets.len() >= MAX_PACKETS_PER_SEGMENT { @@ -396,44 +403,36 @@ impl MkvDemuxer { let dur = rescale(packet.dur.get() as i64); let is_keyframe = packet_is_keyframe(&packet.data, codec); - if is_video { - // Guarantee the segment opens on a random-access point even where - // the container's cue index is sparse enough that the seek above - // landed mid-GOP. Dropping these leading frames loses nothing: they - // depend on references the player would not have when starting here, - // and the previous segment already covers their span. Refusing the - // frames *before* `start_ticks` instead would be the opposite - // trade: on a film whose keyframes are further apart than a - // segment, the segment would open at the next one and leave a hole - // where the picture should be. - if packets.is_empty() && !is_keyframe { - continue; - } - // Matroska stores no per-block duration: a `SimpleBlock` is a - // timestamp and a payload, and symphonia can only report a - // duration where the track declares `DefaultDuration` or the - // codec implies one. Accumulating durations alone therefore - // runs to the packet ceiling on any track that declares - // neither — which is a segment holding the whole film. The - // elapsed presentation time is the check that does not depend - // on the container being generous. - let elapsed = pts.saturating_sub(*first_pts.get_or_insert(pts)); - if elapsed >= target_ticks && !packets.is_empty() { - break; - } - } else { - // Audio is partitioned strictly by the packet's own start, so - // every packet lands in exactly one segment and consecutive - // segments meet without overlapping. A caller that needs samples - // from before its segment — the re-encode does, to prime an - // encoder — asks for an earlier `start_secs` rather than being - // handed a packet twice. - if pts < start_ticks { - continue; - } - if pts >= start_ticks + target_ticks && !packets.is_empty() { - break; - } + // Every track is partitioned strictly by the packet's own + // presentation time, so a packet lands in exactly one segment + // and consecutive segments meet without a gap or an overlap. + // Note what this deliberately does *not* do: round the start + // forward to a keyframe. A film's keyframes are eight to + // twelve seconds apart, so rounding hands back a stretch of + // film several segments further on than the caller asked for, + // and the player's timeline and the media stop describing the + // same thing. Opening on a keyframe is real, and is the + // caller's to arrange by asking for a range that starts on + // one — which is what `web::remux_streaming::segmentation` + // reads the container's cue index to do. + // + // A caller that needs samples from before its segment — the + // audio re-encode does, to prime an encoder — asks for an + // earlier `start_secs` rather than being handed a packet that + // also belongs to its neighbour. + if pts < start_ticks { + continue; + } + // Matroska stores no per-block duration: a `SimpleBlock` is a + // timestamp and a payload, and symphonia can only report a + // duration where the track declares `DefaultDuration` or the + // codec implies one. Accumulating durations alone therefore + // runs to the packet ceiling on any track that declares + // neither — which is a segment holding the whole film. The + // presentation time is the check that does not depend on the + // container being generous. + if pts >= start_ticks + target_ticks && !packets.is_empty() { + break; } accumulated_ticks += dur; diff --git a/crates/vuio-core/src/media/remux/mod.rs b/crates/vuio-core/src/media/remux/mod.rs index 87091fb0..dbed2668 100644 --- a/crates/vuio-core/src/media/remux/mod.rs +++ b/crates/vuio-core/src/media/remux/mod.rs @@ -1,5 +1,6 @@ pub mod fmp4_writer; pub mod hls; +pub mod mkv_cues; pub mod mkv_demuxer; pub use fmp4_writer::*; diff --git a/crates/vuio-core/src/web/remux_streaming.rs b/crates/vuio-core/src/web/remux_streaming.rs index 7cec8359..71de8f6f 100644 --- a/crates/vuio-core/src/web/remux_streaming.rs +++ b/crates/vuio-core/src/web/remux_streaming.rs @@ -19,8 +19,9 @@ use crate::{ database::DatabaseManager, error::AppError, media::remux::{ + mkv_cues, mkv_demuxer::{browser_audio_tracks, browser_video_track, FileInfo, TrackInfo}, - Fmp4Writer, HlsGenerator, MkvDemuxer, + Fmp4Writer, HlsGenerator, MediaPacket, MkvDemuxer, }, state::AppState, }; @@ -32,9 +33,100 @@ use axum::{ use std::path::PathBuf; use tracing::error; -/// Default segment duration target in seconds. +/// Shortest a segment may be, in seconds. +/// +/// A target rather than a length: segments open on the film's own keyframes, so +/// what this actually controls is how many of them get run together. A film with +/// a keyframe every ten seconds has ten-second segments. const SEGMENT_DURATION_SECS: u32 = 4; +/// How one film's segments are laid out on its timeline. +struct Segmentation { + /// Where each segment starts, followed by where the last one ends: one more + /// value than there are segments. + boundaries: Vec, + /// Whether every boundary is a keyframe, and so whether a player may start + /// decoding at any of them. + independent: bool, +} + +impl Segmentation { + /// The half-open range segment `seq` covers, or `None` past the end. + fn range(&self, seq: u32) -> Option<(f64, f64)> { + let at = seq as usize; + match (self.boundaries.get(at), self.boundaries.get(at + 1)) { + (Some(start), Some(end)) if end > start => Some((*start, *end)), + _ => None, + } + } +} + +/// Where to cut this film, from the keyframes its container indexes. +/// +/// An HLS segment has to open on a keyframe, and a film does not have one every +/// four seconds: a Blu-ray remux runs eight to twelve seconds between them. Cut +/// on a fixed grid and each segment has to round forward to the next keyframe, +/// which lands it several segments further into the film than the player asked +/// for — the picture stops within a few seconds of pressing play, which is the +/// defect this exists to prevent. So the grid comes from the film: its own cue +/// index says where the keyframes are, and consecutive ones closer together +/// than [`SEGMENT_DURATION_SECS`] are run into one segment so that a short-GOP +/// file does not produce thousands of tiny ones. +/// +/// A file that indexes nothing falls back to the fixed grid, which is what it +/// always was — right for the web-encoded files that keep their keyframes close +/// together, and honestly declared as not independently startable. +fn segmentation(path: &std::path::Path, info: &FileInfo) -> Segmentation { + let duration = info.duration_secs.unwrap_or(0.0).max(0.0); + let keyframes = browser_video_track(&info.tracks) + .filter(|_| duration > 0.0) + .map(|video| mkv_cues::cue_times_ms(path, u64::from(video.id)).unwrap_or_default()) + .unwrap_or_default(); + + match keyframe_boundaries(&keyframes, duration, f64::from(SEGMENT_DURATION_SECS)) { + Some(boundaries) => Segmentation { + boundaries, + independent: true, + }, + None => Segmentation { + boundaries: HlsGenerator::uniform_boundaries(duration, SEGMENT_DURATION_SECS), + independent: false, + }, + } +} + +/// Turn a film's keyframe times into segment boundaries, or `None` if they will +/// not make a usable set. +/// +/// Two rules, and they pull against each other. A keyframe closer to the +/// previous boundary than `target` is passed over, so a file that puts one every +/// second does not produce a playlist of thousands of one-second segments. And +/// the run-up to the end is not cut at all: a final segment shorter than the +/// target is the classic runt that players handle badly, so the last real +/// keyframe before it is skipped and the segment before absorbs the remainder — +/// at most twice the target long. +fn keyframe_boundaries(keyframes_ms: &[u64], duration: f64, target: f64) -> Option> { + if duration <= 0.0 || keyframes_ms.is_empty() { + return None; + } + // The film starts at zero whatever its first keyframe says, or whatever + // precedes that keyframe would be in no segment at all. + let mut boundaries = vec![0.0f64]; + for time in keyframes_ms.iter().map(|ms| *ms as f64 / 1000.0) { + if time >= duration - target { + break; + } + if time - boundaries[boundaries.len() - 1] >= target { + boundaries.push(time); + } + } + if boundaries.len() < 2 { + return None; + } + boundaries.push(duration); + Some(boundaries) +} + /// Resolve `{id}` to a file path and probe its tracks/duration. A probe failure (e.g. /// the file went away) is treated as "no browser-playable tracks" rather than a hard /// error, so callers fall through to their normal "unsupported" handling. @@ -65,8 +157,9 @@ pub async fn serve_hls_master( State(state): State>, Path(id): Path, ) -> Result { - let (_path, info) = load_file_info(&state, &id).await?; - let master_playlist = HlsGenerator::build_master_playlist(&id, &info.tracks); + let (path, info) = load_file_info(&state, &id).await?; + let master_playlist = + HlsGenerator::build_master_playlist(&id, &info.tracks, segmentation(&path, &info).independent); Ok(( [ @@ -82,11 +175,10 @@ pub async fn serve_hls_video_playlist( State(state): State>, Path(id): Path, ) -> Result { - let (_path, info) = load_file_info(&state, &id).await?; + let (path, info) = load_file_info(&state, &id).await?; browser_video_track(&info.tracks).ok_or(AppError::NotFound)?; - let playlist = - HlsGenerator::build_media_playlist(info.duration_secs.unwrap_or(0.0), SEGMENT_DURATION_SECS); + let playlist = HlsGenerator::build_media_playlist(&segmentation(&path, &info).boundaries); Ok(( [ @@ -102,14 +194,14 @@ pub async fn serve_hls_audio_playlist( State(state): State>, Path((id, audio_idx)): Path<(String, usize)>, ) -> Result { - let (_path, info) = load_file_info(&state, &id).await?; + let (path, info) = load_file_info(&state, &id).await?; browser_audio_tracks(&info.tracks) .get(audio_idx) .ok_or(AppError::NotFound)?; - // Every rendition of the same file shares the same overall timeline. - let playlist = - HlsGenerator::build_media_playlist(info.duration_secs.unwrap_or(0.0), SEGMENT_DURATION_SECS); + // Every rendition of the same file is cut at the same places, so that the + // player's audio and video timelines describe the same film. + let playlist = HlsGenerator::build_media_playlist(&segmentation(&path, &info).boundaries); Ok(( [ @@ -189,38 +281,74 @@ pub async fn serve_hls_audio_init_segment( Ok(init_segment_response(track)) } -/// Extract packets for `track` starting at `seq * SEGMENT_DURATION_SECS` and mux them -/// into an fMP4 segment (`moof` + `mdat`). Shared by the video- and audio-segment -/// routes — they differ only in which track they resolve `{id}`/`{idx}` to. +/// Extract the packets `track` contributes to `[start_secs, end_secs)` and mux +/// them into an fMP4 segment (`moof` + `mdat`). Shared by the video- and +/// audio-segment routes — they differ only in which track they resolve +/// `{id}`/`{idx}` to. +/// +/// The range comes from [`segmentation`] rather than from the sequence number, +/// so the segment covers what the playlist said it would. A segment that +/// silently covers something else is the whole defect: a player's timeline is +/// built from the playlist, and media that disagrees with it stalls playback +/// rather than correcting it. /// /// Blocking. Demuxing was always file I/O and parsing; a decoded rendition adds -/// a decode and an encode of four seconds of audio on top. Callers run it under +/// a decode and an encode of the segment's audio on top. Callers run it under /// `spawn_blocking`, without which one seeking browser takes a runtime worker /// out of service for the duration. -fn build_segment_bytes(path: &std::path::Path, track: &TrackInfo, seq: u32) -> Vec { +fn build_segment_bytes( + path: &std::path::Path, + track: &TrackInfo, + seq: u32, + start_secs: f64, + end_secs: f64, +) -> Vec { let out_track = rendition_track(track); let timescale = Fmp4Writer::timescale_for(&out_track); - let start_secs = seq as f64 * SEGMENT_DURATION_SECS as f64; - let nominal_decode_time = (start_secs * timescale as f64).round() as u64; #[cfg(all(feature = "transcode-aac", feature = "demux"))] if let Some(codec) = track.codec_kind.transcode_codec() { - return build_reencoded_segment(path, track, &out_track, codec, seq, timescale); + return build_reencoded_segment( + path, track, &out_track, codec, seq, timescale, start_secs, end_secs, + ); } - let packets = MkvDemuxer::extract_track_packets( + let mut packets = MkvDemuxer::extract_track_packets( path, track.id, track.codec_kind, timescale, start_secs, - SEGMENT_DURATION_SECS as f64, + end_secs - start_secs, ) .unwrap_or_default(); + let nominal_decode_time = (start_secs * timescale as f64).round() as u64; + close_the_segment(&mut packets, (end_secs * timescale as f64).round() as u64); Fmp4Writer::build_segment(seq + 1, &out_track, nominal_decode_time, &packets) } +/// Stretch the last sample to where the segment ends. +/// +/// Every other sample's duration is the gap to the next one's decode time, but +/// the last has no successor and falls back to whatever duration the container +/// declared for it. On a 23.976 fps film stored with millisecond timestamps +/// that is 41 ms where the real gap to the next segment's first frame is 42, +/// which leaves a one-millisecond hole in the player's video buffer at every +/// single segment join. A hole is a hole: the player finds no picture at the +/// playhead, nudges over it, and does that once per segment for the length of +/// the film — until the nudges run out and playback stops. +/// +/// The segment's own end is the honest duration for that sample, and it makes +/// the samples cover exactly what the playlist promised. +fn close_the_segment(packets: &mut [MediaPacket], end_ticks: u64) { + if let Some(last) = packets.last_mut() { + if let Some(remaining) = end_ticks.checked_sub(last.dts).filter(|d| *d > 0) { + last.duration = remaining; + } + } +} + /// The longest a source frame of any codec this decodes can be, in samples. /// /// DTS Core's `NBLKS` tops out at 127, for `(127 + 1) * 32` samples; AC-3 and @@ -231,15 +359,16 @@ const LONGEST_SOURCE_FRAME: i64 = 4096; /// One segment of an audio track that has to be decoded and re-encoded. /// -/// Different from the passthrough path in what it asks the demuxer for: not -/// this segment's four seconds, but the span the encoder has to be *fed* to -/// produce this segment's frames — which reaches back before the segment -/// begins, to cancel the encoder's delay and to warm its MDCT window up on real -/// audio, and forward past where it ends, because that same delay means the -/// last frame is not finished until samples beyond it have been seen. The -/// re-encode positions what it gets by absolute sample index, so the guard band -/// on the request costs a little decoding and nothing else. +/// Different from the passthrough path in what it asks the demuxer for: not the +/// segment's own span, but the span the encoder has to be *fed* to produce this +/// segment's frames — which reaches back before the segment begins, to cancel +/// the encoder's delay and to warm its MDCT window up on real audio, and +/// forward past where it ends, because that same delay means the last frame is +/// not finished until samples beyond it have been seen. The re-encode positions +/// what it gets by absolute sample index, so the guard band on the request +/// costs a little decoding and nothing else. #[cfg(all(feature = "transcode-aac", feature = "demux"))] +#[allow(clippy::too_many_arguments)] fn build_reencoded_segment( path: &std::path::Path, track: &TrackInfo, @@ -247,12 +376,16 @@ fn build_reencoded_segment( codec: crate::media::transcode::TranscodeCodec, seq: u32, timescale: u32, + start_secs: f64, + end_secs: f64, ) -> Vec { use crate::media::transcode::AacWindow; let rate = out_track.sample_rate.unwrap_or(48_000) as u64; - let segment_samples = SEGMENT_DURATION_SECS as u64 * rate; - let window = AacWindow::covering(seq as u64 * segment_samples, (seq as u64 + 1) * segment_samples); + let window = AacWindow::covering( + (start_secs * rate as f64).round() as u64, + (end_secs * rate as f64).round() as u64, + ); let (from, len) = window.source_span(); let request_from = (from - LONGEST_SOURCE_FRAME).max(0); @@ -287,8 +420,8 @@ fn build_reencoded_segment( /// build runs on a blocking thread. It takes a transcoding permit, from the same /// pool the DLNA path draws on, so the two share one CPU ceiling rather than /// each keeping its own. And the result is cached: a scrub or a re-buffer asks -/// for the same segment again, and rebuilding it means decoding those four -/// seconds again. +/// for the same segment again, and rebuilding it means decoding those seconds +/// again. #[cfg_attr(not(feature = "transcode"), allow(unused_variables))] async fn segment_response( state: &AppState, @@ -296,6 +429,7 @@ async fn segment_response( path: &std::path::Path, track: &TrackInfo, seq: u32, + range: (f64, f64), ) -> Result { let headers = [ (header::CONTENT_TYPE, "video/mp4"), @@ -335,8 +469,9 @@ async fn segment_response( let owned_path = path.to_path_buf(); let owned_track = track.clone(); + let (start_secs, end_secs) = range; let bytes = tokio::task::spawn_blocking(move || { - build_segment_bytes(&owned_path, &owned_track, seq) + build_segment_bytes(&owned_path, &owned_track, seq, start_secs, end_secs) }) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("segment builder panicked: {e}")))?; @@ -359,7 +494,13 @@ pub async fn serve_hls_video_segment( let video_track = browser_video_track(&info.tracks) .ok_or(AppError::NotFound)? .clone(); - segment_response(&state, file_id, &path, &video_track, seq).await + // A request past the last segment is a request for something the playlist + // never offered, which is a 404 rather than an empty segment a player would + // append and then wait on. + let range = segmentation(&path, &info) + .range(seq) + .ok_or(AppError::NotFound)?; + segment_response(&state, file_id, &path, &video_track, seq, range).await } pub async fn serve_hls_audio_segment( @@ -374,5 +515,67 @@ pub async fn serve_hls_audio_segment( .copied() .ok_or(AppError::NotFound)? .clone(); - segment_response(&state, file_id, &path, &track, seq).await + let range = segmentation(&path, &info) + .range(seq) + .ok_or(AppError::NotFound)?; + segment_response(&state, file_id, &path, &track, seq, range).await +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A film's keyframes, as a Blu-ray remux actually spaces them. + const GOP: &[u64] = &[0, 2_002, 12_429, 22_814, 33_242, 43_627, 54_012, 64_398]; + + #[test] + fn boundaries_land_on_keyframes_and_skip_the_ones_too_close_together() { + let boundaries = keyframe_boundaries(GOP, 74.0, 4.0).expect("a usable set"); + // 2.002 is passed over: two seconds is not a segment. + assert_eq!( + boundaries, + vec![0.0, 12.429, 22.814, 33.242, 43.627, 54.012, 64.398, 74.0] + ); + } + + /// Every segment has to meet the next, or the player's timeline and the + /// media stop describing the same film — which is heard as a stall. + #[test] + fn boundaries_tile_the_whole_film() { + let boundaries = keyframe_boundaries(GOP, 74.0, 4.0).expect("a usable set"); + assert_eq!(boundaries.first(), Some(&0.0)); + assert_eq!(boundaries.last(), Some(&74.0)); + assert!(boundaries.windows(2).all(|w| w[1] > w[0])); + } + + /// A runt final segment is worse than a long one, so the last keyframe + /// before the end is passed over rather than cut on. + #[test] + fn the_film_does_not_end_on_a_fragment_of_a_segment() { + // A keyframe 0.4s before the end would leave a 0.4s final segment. + let keyframes = &[0u64, 12_429, 22_814, 23_600]; + let boundaries = keyframe_boundaries(keyframes, 24.0, 4.0).expect("a usable set"); + assert_eq!(boundaries, vec![0.0, 12.429, 24.0]); + } + + /// A film that indexes nothing, or indexes only its own first frame, has no + /// keyframe grid to offer and falls back to the fixed one. + #[test] + fn a_film_with_nothing_to_cut_on_declines_rather_than_inventing_boundaries() { + assert!(keyframe_boundaries(&[], 100.0, 4.0).is_none()); + assert!(keyframe_boundaries(&[0], 100.0, 4.0).is_none()); + // No duration means no last boundary, and so no segments. + assert!(keyframe_boundaries(GOP, 0.0, 4.0).is_none()); + } + + /// Short GOPs must not become thousands of tiny segments. + #[test] + fn a_keyframe_every_second_still_makes_segments_of_the_target_length() { + let keyframes: Vec = (0..60).map(|i| i * 1_000).collect(); + let boundaries = keyframe_boundaries(&keyframes, 60.0, 4.0).expect("a usable set"); + assert_eq!( + boundaries, + vec![0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0, 28.0, 32.0, 36.0, 40.0, 44.0, 48.0, 52.0, 60.0] + ); + } } diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs index f39e75d9..e3737e90 100644 --- a/crates/vuio-core/tests/film_transcode_tests.rs +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -489,24 +489,42 @@ async fn an_audio_segment_carries_real_re_encoded_aac() { assert_eq!(base, 188 * 1024, "the run opens on the frame grid"); } -/// The defect a browser hears as a tick every four seconds. +/// The two defects a browser shows as playback stopping a few seconds in. /// -/// Each segment is built by its own request, on its own thread, from nothing -/// but its sequence number — so if they disagree by even one frame about where -/// they sit, the player's source buffer resolves the collision by throwing -/// samples away. What is asserted is the only thing that rules that out: each -/// segment opens exactly where the previous one's last sample ended, and the -/// run of them keeps time with the wall clock rather than drifting a fraction -/// of a frame per segment. +/// A player builds its whole timeline out of the playlist's `EXTINF` durations +/// and then fetches segments expecting to find exactly that. If a segment +/// overruns the next one's start the source buffer resolves the collision by +/// throwing samples away; if it covers something else entirely — which is what +/// rounding a segment's start forward to the next keyframe does on a film whose +/// keyframes are ten seconds apart — the buffer never reaches the playhead and +/// playback stops. So this asserts the one property that rules both out: the +/// segments tile the timeline the playlist described, exactly, with nothing +/// between them and nothing on top of each other. #[tokio::test] -async fn consecutive_audio_segments_meet_without_a_gap_or_an_overlap() { +async fn the_segments_tile_the_timeline_the_playlist_promised() { const FRAME: u64 = 1024; + const RATE: f64 = 48_000.0; let (_temp, state, id) = scanned_film(24.0).await; + let (status, _, body) = get( + &state, + &format!("/media/{id}/hls/audio/0/index.m3u8"), + Method::GET, + None, + ) + .await; + assert_eq!(status, StatusCode::OK); + let playlist = String::from_utf8(body).expect("a playlist is text"); + let promised: Vec = playlist + .lines() + .filter_map(|line| line.strip_prefix("#EXTINF:")) + .filter_map(|value| value.trim_end_matches(',').parse().ok()) + .collect(); + assert!(!promised.is_empty(), "no segments offered:\n{playlist}"); + let mut opens_at: Option = None; let mut first_open = 0u64; - let segments = 5u32; - for seq in 0..segments { + for seq in 0..promised.len() { let (status, _, segment) = get( &state, &format!("/media/{id}/hls/audio/0/segment/{seq}"), @@ -514,7 +532,12 @@ async fn consecutive_audio_segments_meet_without_a_gap_or_an_overlap() { None, ) .await; - assert_eq!(status, StatusCode::OK, "segment {seq}"); + assert_eq!( + status, + StatusCode::OK, + "segment {seq} of {}", + promised.len() + ); let tfdt = find_box(&segment, "tfdt").expect("a tfdt"); let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); @@ -533,12 +556,13 @@ async fn consecutive_audio_segments_meet_without_a_gap_or_an_overlap() { opens_at = Some(base + samples * FRAME); } - // Four seconds a segment on average, to within the frame the grid rounds by. + // And the timeline they cover is the one the playlist described, to within + // the frame the AAC grid rounds by. let covered = opens_at.unwrap() - first_open; - let nominal = u64::from(segments) * 4 * 48_000; + let promised_samples = (promised.iter().sum::() * RATE).round() as u64; assert!( - covered.abs_diff(nominal) < FRAME, - "{segments} segments covered {covered} samples where four seconds each is {nominal}" + covered.abs_diff(promised_samples) < FRAME, + "the segments carry {covered} samples where the playlist promised {promised_samples}" ); } diff --git a/crates/vuio-core/tests/zz_scratch_diag.rs b/crates/vuio-core/tests/zz_scratch_diag.rs new file mode 100644 index 00000000..296450db --- /dev/null +++ b/crates/vuio-core/tests/zz_scratch_diag.rs @@ -0,0 +1,145 @@ +//! Temporary diagnostic harness. Not part of the suite; deleted before commit. +#![cfg(all(feature = "transcode-dts", feature = "casting"))] + +mod common; + +use axum::http::{Method, Request, StatusCode}; +use axum::extract::ConnectInfo; +use std::sync::Arc; +use tower::ServiceExt; +use vuio_core::database::MediaRepository; + +async fn get( + state: &vuio_core::state::AppState, + uri: &str, +) -> (StatusCode, Vec) { + let request = Request::builder() + .method(Method::GET) + .uri(uri) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(axum::body::Body::empty()) + .unwrap(); + let router = vuio_core::web::create_router(state.clone(), vuio_core::web::Surface::Primary); + let response = router.oneshot(request).await.unwrap(); + let status = response.status(); + let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); + (status, body.to_vec()) +} + +fn boxes(data: &[u8]) -> Vec<(String, &[u8])> { + let mut out = Vec::new(); + let mut pos = 0usize; + while pos + 8 <= data.len() { + let size = u32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as usize; + let name = String::from_utf8_lossy(&data[pos + 4..pos + 8]).into_owned(); + if size < 8 || pos + size > data.len() { break; } + out.push((name, &data[pos + 8..pos + size])); + pos += size; + } + out +} + +fn find_box<'a>(data: &'a [u8], name: &str) -> Option<&'a [u8]> { + const CONTAINERS: &[&str] = &["moov", "trak", "mdia", "minf", "stbl", "stsd", "mvex", "moof", "traf", "avc1", "hvc1", "mp4a"]; + for (found, body) in boxes(data) { + if found == name { return Some(body); } + if CONTAINERS.contains(&found.as_str()) { + let inner = match found.as_str() { + "stsd" => 8, + "avc1" | "hvc1" => 78, + "mp4a" => 28, + _ => 0, + }; + if body.len() > inner { + if let Some(hit) = find_box(&body[inner..], name) { return Some(hit); } + } + } + } + None +} + +/// trun sample durations, summed: what the segment really covers on the timeline. +fn trun_span(segment: &[u8]) -> (u64, u64, u32) { + let tfdt = find_box(segment, "tfdt").expect("tfdt"); + let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); + let trun = find_box(segment, "trun").expect("trun"); + let flags = u32::from_be_bytes([0, trun[1], trun[2], trun[3]]); + let count = u32::from_be_bytes(trun[4..8].try_into().unwrap()); + let mut at = 8usize; + if flags & 0x000001 != 0 { at += 4; } // data-offset + if flags & 0x000004 != 0 { at += 4; } // first-sample-flags + let per = ((flags & 0x000100 != 0) as usize + + (flags & 0x000200 != 0) as usize + + (flags & 0x000400 != 0) as usize + + (flags & 0x000800 != 0) as usize) * 4; + let mut total = 0u64; + for i in 0..count as usize { + let off = at + i * per; + if flags & 0x000100 != 0 && off + 4 <= trun.len() { + total += u64::from(u32::from_be_bytes(trun[off..off + 4].try_into().unwrap())); + } + } + (base, total, count) +} + +#[tokio::test] +async fn diag_real_router() { + let Some(film) = std::env::var_os("DIAG_IN").map(std::path::PathBuf::from) else { return }; + let segs: usize = std::env::var("DIAG_SEGS").ok().and_then(|v| v.parse().ok()).unwrap_or(12); + + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + let link = root.join("Film.mkv"); + std::os::unix::fs::symlink(&film, &link).unwrap(); + + let state = common::state_over(temp.path(), &root).await; + let files = common::scan_into(&state).await; + let entry = files.iter().find(|f| f.filename == "Film.mkv").expect("scanned"); + let id = entry.id.unwrap(); + eprintln!("scanned as id {id}"); + + let (status, body) = get(&state, &format!("/media/{id}/hls/master.m3u8")).await; + eprintln!("master {status}:\n{}", String::from_utf8_lossy(&body)); + + for rendition in ["video", "audio/0", "audio/1"] { + let (status, body) = get(&state, &format!("/media/{id}/hls/{rendition}/index.m3u8")).await; + let text = String::from_utf8_lossy(&body).into_owned(); + let extinf: Vec = text.lines() + .filter_map(|l| l.strip_prefix("#EXTINF:")) + .filter_map(|v| v.trim_end_matches(',').parse().ok()) + .collect(); + let mx = extinf.iter().cloned().fold(0.0f64, f64::max); + let mn = extinf.iter().cloned().fold(f64::MAX, f64::min); + let target = text.lines().find(|l| l.starts_with("#EXT-X-TARGETDURATION")).unwrap_or("(none)"); + eprintln!("--- {rendition} playlist {status}: {} segments, total {:.3}s, min {mn:.3} max {mx:.3}, {target}", + extinf.len(), extinf.iter().sum::()); + let long: Vec<(usize, f64)> = extinf.iter().cloned().enumerate().filter(|(_, d)| *d > 20.0).collect(); + eprintln!(" segments over 20s: {} {:?}", long.len(), long.iter().take(10).collect::>()); + + let (status, init) = get(&state, &format!("/media/{id}/hls/{rendition}/init.mp4")).await; + eprintln!(" init {status}, {} bytes", init.len()); + + let mut expect_base: Option = None; + let timescale: u64 = if rendition == "video" { 90_000 } else { 48_000 }; + for seq in 0..segs.min(extinf.len()) { + let (status, seg) = get(&state, &format!("/media/{id}/hls/{rendition}/segment/{seq}")).await; + if status != StatusCode::OK { + eprintln!(" seg {seq}: {status} !!"); + continue; + } + let (base, span, count) = trun_span(&seg); + let promised = extinf[seq]; + let actual = span as f64 / timescale as f64; + let gap = expect_base.map(|e| base as i64 - e as i64); + eprintln!( + " seg {seq:2}: {} bytes, {count} samples, tfdt={base} ({:.3}s) covers {actual:.3}s vs EXTINF {promised:.3}s, joint gap {:?}", + seg.len(), base as f64 / timescale as f64, gap + ); + expect_base = Some(base + span); + } + } + let _ = Arc::strong_count(&state.database); +} From b2104855bd16d7d606d66a0142897eac4e04f697 Mon Sep 17 00:00:00 2001 From: vyrti Date: Mon, 24 Aug 2026 23:59:42 +0300 Subject: [PATCH 17/38] fmt --- crates/vuio-core/tests/zz_scratch_diag.rs | 145 ---------------------- plan.txt | 11 ++ 2 files changed, 11 insertions(+), 145 deletions(-) delete mode 100644 crates/vuio-core/tests/zz_scratch_diag.rs create mode 100644 plan.txt diff --git a/crates/vuio-core/tests/zz_scratch_diag.rs b/crates/vuio-core/tests/zz_scratch_diag.rs deleted file mode 100644 index 296450db..00000000 --- a/crates/vuio-core/tests/zz_scratch_diag.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Temporary diagnostic harness. Not part of the suite; deleted before commit. -#![cfg(all(feature = "transcode-dts", feature = "casting"))] - -mod common; - -use axum::http::{Method, Request, StatusCode}; -use axum::extract::ConnectInfo; -use std::sync::Arc; -use tower::ServiceExt; -use vuio_core::database::MediaRepository; - -async fn get( - state: &vuio_core::state::AppState, - uri: &str, -) -> (StatusCode, Vec) { - let request = Request::builder() - .method(Method::GET) - .uri(uri) - .extension(ConnectInfo::( - "127.0.0.1:50000".parse().unwrap(), - )) - .body(axum::body::Body::empty()) - .unwrap(); - let router = vuio_core::web::create_router(state.clone(), vuio_core::web::Surface::Primary); - let response = router.oneshot(request).await.unwrap(); - let status = response.status(); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - (status, body.to_vec()) -} - -fn boxes(data: &[u8]) -> Vec<(String, &[u8])> { - let mut out = Vec::new(); - let mut pos = 0usize; - while pos + 8 <= data.len() { - let size = u32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as usize; - let name = String::from_utf8_lossy(&data[pos + 4..pos + 8]).into_owned(); - if size < 8 || pos + size > data.len() { break; } - out.push((name, &data[pos + 8..pos + size])); - pos += size; - } - out -} - -fn find_box<'a>(data: &'a [u8], name: &str) -> Option<&'a [u8]> { - const CONTAINERS: &[&str] = &["moov", "trak", "mdia", "minf", "stbl", "stsd", "mvex", "moof", "traf", "avc1", "hvc1", "mp4a"]; - for (found, body) in boxes(data) { - if found == name { return Some(body); } - if CONTAINERS.contains(&found.as_str()) { - let inner = match found.as_str() { - "stsd" => 8, - "avc1" | "hvc1" => 78, - "mp4a" => 28, - _ => 0, - }; - if body.len() > inner { - if let Some(hit) = find_box(&body[inner..], name) { return Some(hit); } - } - } - } - None -} - -/// trun sample durations, summed: what the segment really covers on the timeline. -fn trun_span(segment: &[u8]) -> (u64, u64, u32) { - let tfdt = find_box(segment, "tfdt").expect("tfdt"); - let base = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()); - let trun = find_box(segment, "trun").expect("trun"); - let flags = u32::from_be_bytes([0, trun[1], trun[2], trun[3]]); - let count = u32::from_be_bytes(trun[4..8].try_into().unwrap()); - let mut at = 8usize; - if flags & 0x000001 != 0 { at += 4; } // data-offset - if flags & 0x000004 != 0 { at += 4; } // first-sample-flags - let per = ((flags & 0x000100 != 0) as usize - + (flags & 0x000200 != 0) as usize - + (flags & 0x000400 != 0) as usize - + (flags & 0x000800 != 0) as usize) * 4; - let mut total = 0u64; - for i in 0..count as usize { - let off = at + i * per; - if flags & 0x000100 != 0 && off + 4 <= trun.len() { - total += u64::from(u32::from_be_bytes(trun[off..off + 4].try_into().unwrap())); - } - } - (base, total, count) -} - -#[tokio::test] -async fn diag_real_router() { - let Some(film) = std::env::var_os("DIAG_IN").map(std::path::PathBuf::from) else { return }; - let segs: usize = std::env::var("DIAG_SEGS").ok().and_then(|v| v.parse().ok()).unwrap_or(12); - - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("media"); - std::fs::create_dir_all(&root).unwrap(); - let link = root.join("Film.mkv"); - std::os::unix::fs::symlink(&film, &link).unwrap(); - - let state = common::state_over(temp.path(), &root).await; - let files = common::scan_into(&state).await; - let entry = files.iter().find(|f| f.filename == "Film.mkv").expect("scanned"); - let id = entry.id.unwrap(); - eprintln!("scanned as id {id}"); - - let (status, body) = get(&state, &format!("/media/{id}/hls/master.m3u8")).await; - eprintln!("master {status}:\n{}", String::from_utf8_lossy(&body)); - - for rendition in ["video", "audio/0", "audio/1"] { - let (status, body) = get(&state, &format!("/media/{id}/hls/{rendition}/index.m3u8")).await; - let text = String::from_utf8_lossy(&body).into_owned(); - let extinf: Vec = text.lines() - .filter_map(|l| l.strip_prefix("#EXTINF:")) - .filter_map(|v| v.trim_end_matches(',').parse().ok()) - .collect(); - let mx = extinf.iter().cloned().fold(0.0f64, f64::max); - let mn = extinf.iter().cloned().fold(f64::MAX, f64::min); - let target = text.lines().find(|l| l.starts_with("#EXT-X-TARGETDURATION")).unwrap_or("(none)"); - eprintln!("--- {rendition} playlist {status}: {} segments, total {:.3}s, min {mn:.3} max {mx:.3}, {target}", - extinf.len(), extinf.iter().sum::()); - let long: Vec<(usize, f64)> = extinf.iter().cloned().enumerate().filter(|(_, d)| *d > 20.0).collect(); - eprintln!(" segments over 20s: {} {:?}", long.len(), long.iter().take(10).collect::>()); - - let (status, init) = get(&state, &format!("/media/{id}/hls/{rendition}/init.mp4")).await; - eprintln!(" init {status}, {} bytes", init.len()); - - let mut expect_base: Option = None; - let timescale: u64 = if rendition == "video" { 90_000 } else { 48_000 }; - for seq in 0..segs.min(extinf.len()) { - let (status, seg) = get(&state, &format!("/media/{id}/hls/{rendition}/segment/{seq}")).await; - if status != StatusCode::OK { - eprintln!(" seg {seq}: {status} !!"); - continue; - } - let (base, span, count) = trun_span(&seg); - let promised = extinf[seq]; - let actual = span as f64 / timescale as f64; - let gap = expect_base.map(|e| base as i64 - e as i64); - eprintln!( - " seg {seq:2}: {} bytes, {count} samples, tfdt={base} ({:.3}s) covers {actual:.3}s vs EXTINF {promised:.3}s, joint gap {:?}", - seg.len(), base as f64 / timescale as f64, gap - ); - expect_base = Some(base + span); - } - } - let _ = Arc::strong_count(&state.database); -} diff --git a/plan.txt b/plan.txt new file mode 100644 index 00000000..f065618d --- /dev/null +++ b/plan.txt @@ -0,0 +1,11 @@ +How VuIO Autodetects TVs (via User-Agent & Client Profiles) +VuIO inspects the client's UPnP headers and User-Agent: + +Samsung Tizen TV: SEC_HHP_[TV] Samsung ... +LG webOS TV: LGE_DLNA_SDK ... or LG Player / webOS +Sony Bravia: X-AV-Client-Info: ... BRAVIA ... +We can make client profile handling automatic in rendering.rs: + +For Sony / PC / VLC: Keep the original MKV 1st (direct lossless DTS passthrough without using server CPU). +For Samsung & LG webOS (known no-DTS): Automatically promote the transcoded MP4 to 1st only when the media file contains DTS. +Would you like VuIO to automatically promote the transcoded stream to 1st for LG and Samsung client profiles while keeping the original file 1st for Sony and standard DLNA clients? \ No newline at end of file From 509eac57f8d427a979307d556eb222658a884621 Mon Sep 17 00:00:00 2001 From: vyrti Date: Tue, 25 Aug 2026 03:10:07 +0300 Subject: [PATCH 18/38] u --- config.example.toml | 8 +- crates/vuio-core/src/config/loading.rs | 25 +- crates/vuio-core/src/config/mod.rs | 2 +- crates/vuio-core/src/config/model.rs | 67 ++-- .../vuio-core/src/media/remux/fmp4_writer.rs | 180 ++++++++- crates/vuio-core/src/media/transcode/video.rs | 145 ++++--- .../src/platform/filesystem/metadata.rs | 34 +- crates/vuio-core/src/web/admin.rs | 12 +- crates/vuio-core/src/web/mod.rs | 8 +- crates/vuio-core/src/web/video_streaming.rs | 184 ++++++--- crates/vuio-core/src/web/xml/browse.rs | 82 ++-- crates/vuio-core/src/web/xml/rendering.rs | 101 ++--- crates/vuio-core/tests/common/mod.rs | 6 + .../vuio-core/tests/film_transcode_tests.rs | 366 ++++++++++++++++-- .../tests/transcode_integration_tests.rs | 14 +- 15 files changed, 939 insertions(+), 295 deletions(-) diff --git a/config.example.toml b/config.example.toml index 3ddf0895..aa4ec5cd 100644 --- a/config.example.toml +++ b/config.example.toml @@ -138,9 +138,11 @@ enabled = true # untouched, the soundtrack re-encoded — and is seekable by time rather than by # byte, which works the same whatever its audio codec was. audio_format = "lpcm" -# Which version is listed first, for a TV that takes what it is given rather -# than choosing. Switch to "transcoded" only if a TV still plays silently. -prefer = "original" +# Operating mode for transcode advertisement: +# enabled transcode is available; original stream listed first, transcode as alternative +# forced transcode is forced; only transcoded stream is offered so every TV plays it +# disabled transcode is disabled +mode = "enabled" # Decodes allowed at once, shared between TVs and the browser player. Past this # a further request is refused rather than queued, so the streams already # playing keep up. diff --git a/crates/vuio-core/src/config/loading.rs b/crates/vuio-core/src/config/loading.rs index 6104f10e..9e11c47a 100644 --- a/crates/vuio-core/src/config/loading.rs +++ b/crates/vuio-core/src/config/loading.rs @@ -233,18 +233,25 @@ impl AppConfig { require_auth: env_flag("VUIO_MCP_REQUIRE_AUTH").unwrap_or(false), }, transcode: TranscodeConfig { - enabled: env_flag("VUIO_TRANSCODE_ENABLED").unwrap_or(true), - // An unrecognised value falls back to the default rather than - // refusing to start: this is a container that may have been - // handed a typo through a compose file, and a media server that - // will not boot is worse than one that plays LPCM. - audio_format: std::env::var("VUIO_TRANSCODE_AUDIO_FORMAT") + enabled: env_flag("VUIO_TRANSCODE_ENABLED").unwrap_or_else(|| { + if let Ok(v) = std::env::var("VUIO_TRANSCODE") { + !matches!( + v.trim().to_ascii_lowercase().as_str(), + "disabled" | "disable" | "off" | "false" + ) + } else { + true + } + }), + mode: std::env::var("VUIO_TRANSCODE") + .or_else(|_| std::env::var("VUIO_TRANSCODE_MODE")) + .or_else(|_| std::env::var("VUIO_TRANSCODE_PREFER")) .ok() - .and_then(|v| TranscodeAudioFormat::parse(&v)) + .and_then(|v| TranscodeMode::parse(&v)) .unwrap_or_default(), - prefer: std::env::var("VUIO_TRANSCODE_PREFER") + audio_format: std::env::var("VUIO_TRANSCODE_AUDIO_FORMAT") .ok() - .and_then(|v| TranscodePreference::parse(&v)) + .and_then(|v| TranscodeAudioFormat::parse(&v)) .unwrap_or_default(), max_concurrent: std::env::var("VUIO_TRANSCODE_MAX_CONCURRENT") .ok() diff --git a/crates/vuio-core/src/config/mod.rs b/crates/vuio-core/src/config/mod.rs index 2a45c8e8..3974eff2 100644 --- a/crates/vuio-core/src/config/mod.rs +++ b/crates/vuio-core/src/config/mod.rs @@ -21,7 +21,7 @@ use model::{ pub use model::{ AppConfig, ConfigOverrides, DatabaseConfig, ManagementConfig, McpConfig, MediaConfig, MediaInfoConfig, MonitoredDirectoryConfig, NetworkConfig, NetworkInterfaceConfig, ServerConfig, - TranscodeAudioFormat, TranscodeConfig, TranscodePreference, ValidationMode, WebUiConfig, + TranscodeAudioFormat, TranscodeConfig, TranscodeMode, ValidationMode, WebUiConfig, }; use crate::platform::config::PlatformConfig; diff --git a/crates/vuio-core/src/config/model.rs b/crates/vuio-core/src/config/model.rs index eecce994..c07817d9 100644 --- a/crates/vuio-core/src/config/model.rs +++ b/crates/vuio-core/src/config/model.rs @@ -137,6 +137,22 @@ impl ConfigOverrides { }) .collect(); } + if let Ok(v) = std::env::var("VUIO_TRANSCODE") + .or_else(|_| std::env::var("VUIO_TRANSCODE_MODE")) + .or_else(|_| std::env::var("VUIO_TRANSCODE_PREFER")) + { + if let Some(mode) = TranscodeMode::parse(&v) { + config.transcode.mode = mode; + if mode == TranscodeMode::Disabled { + config.transcode.enabled = false; + } + } + } + if let Ok(v) = std::env::var("VUIO_TRANSCODE_AUDIO_FORMAT") { + if let Some(fmt) = TranscodeAudioFormat::parse(&v) { + config.transcode.audio_format = fmt; + } + } } /// The settings this forces, as dotted config keys and the value in force, so a @@ -232,18 +248,13 @@ pub struct TranscodeConfig { /// Offer a decoded resource beside the original for AC-3/E-AC-3/DTS items. #[serde(default = "default_true")] pub enabled: bool, + /// Operating mode: enabled (default/auto), forced (transcode always listed first / primary), or disabled. + #[serde(default, alias = "prefer")] + pub mode: TranscodeMode, /// What the decoded resource is delivered as. #[serde(default)] pub audio_format: TranscodeAudioFormat, - /// Which resource is listed first in the DIDL response. - #[serde(default)] - pub prefer: TranscodePreference, /// Ceiling on simultaneous transcode sessions. - /// - /// Decoding is the only CPU-bound work this server does, and a shared folder - /// can be opened by every TV in the house at once. Past this, a request is - /// refused rather than joining a queue that would starve the ones already - /// playing. #[serde(default = "default_transcode_max_concurrent")] pub max_concurrent: usize, } @@ -252,8 +263,8 @@ impl Default for TranscodeConfig { fn default() -> Self { Self { enabled: true, + mode: TranscodeMode::default(), audio_format: TranscodeAudioFormat::default(), - prefer: TranscodePreference::default(), max_concurrent: default_transcode_max_concurrent(), } } @@ -303,32 +314,29 @@ impl TranscodeAudioFormat { } } -/// Which of an item's two resources is listed first. +/// Operating mode for transcode advertisement and delivery. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub enum TranscodePreference { - /// The original file first, the decoded resource second. - /// - /// The default, because it is the choice that cannot make anything worse. A - /// renderer that matches on protocolInfo picks whichever it can actually - /// play; one that blindly takes the first resource behaves exactly as it did - /// before this feature existed. +pub enum TranscodeMode { + /// Transcoding is enabled and offered alongside the original format. #[default] - Original, - /// The decoded resource first. - /// - /// For a renderer that takes the first resource without checking and cannot - /// play the original — it plays silently otherwise, and no amount of correct - /// protocolInfo will change its mind. - Transcoded, + #[serde(alias = "original", alias = "auto", alias = "true")] + Enabled, + /// Transcoding is forced: transcoded streams are listed first so any TV is forced to play the transcoded version. + #[serde(alias = "transcoded", alias = "force", alias = "always")] + Forced, + /// Transcoding is disabled. + #[serde(alias = "disable", alias = "off", alias = "false")] + Disabled, } -impl TranscodePreference { +impl TranscodeMode { /// Parse an environment-variable or TOML value, case- and space-insensitively. pub fn parse(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { - "original" | "source" => Some(Self::Original), - "transcoded" | "decoded" => Some(Self::Transcoded), + "enabled" | "enable" | "auto" | "true" | "original" => Some(Self::Enabled), + "forced" | "force" | "always" | "transcoded" => Some(Self::Forced), + "disabled" | "disable" | "off" | "false" => Some(Self::Disabled), _ => None, } } @@ -336,8 +344,9 @@ impl TranscodePreference { /// The value as it is written in a config file. pub fn as_str(self) -> &'static str { match self { - Self::Original => "original", - Self::Transcoded => "transcoded", + Self::Enabled => "enabled", + Self::Forced => "forced", + Self::Disabled => "disabled", } } } diff --git a/crates/vuio-core/src/media/remux/fmp4_writer.rs b/crates/vuio-core/src/media/remux/fmp4_writer.rs index 3e755246..a60e4989 100644 --- a/crates/vuio-core/src/media/remux/fmp4_writer.rs +++ b/crates/vuio-core/src/media/remux/fmp4_writer.rs @@ -83,9 +83,28 @@ impl Fmp4Writer { mvhd.extend_from_slice(&next_track_id.to_be_bytes()); moov_body.extend_from_slice(&Self::wrap_box(&mvhd)); - // 2. trak (Track Atom), one per track - for track in tracks { - moov_body.extend_from_slice(&Self::build_trak(track, movie_duration)); + // 2. trak (Track Atom), one per track. + // + // Every audio track goes into one alternate group, and only the first of + // them is enabled. Both halves matter and they say different things. The + // group says the soundtracks are alternatives: a player reading a file + // whose audio tracks are all in group zero is entitled to render them + // all at once, which for a film with three soundtracks is three + // soundtracks played over each other. The enabled bit says which one to + // start with, and exactly one track in a group should carry it — three + // tracks all claiming to be the default is a choice the renderer then + // makes for itself. Both match what every muxer writes. + let leading_audio = tracks + .iter() + .position(|track| track.track_kind == TrackKind::Audio); + for (index, track) in tracks.iter().enumerate() { + let audio = track.track_kind == TrackKind::Audio; + moov_body.extend_from_slice(&Self::build_trak( + track, + movie_duration, + u16::from(audio), + !audio || Some(index) == leading_audio, + )); } // 3. mvex (Movie Extends Atom) @@ -109,14 +128,23 @@ impl Fmp4Writer { } } - fn build_trak(track: &TrackInfo, movie_duration: u32) -> Vec { + fn build_trak( + track: &TrackInfo, + movie_duration: u32, + alternate_group: u16, + enabled: bool, + ) -> Vec { let mut trak_body = Vec::new(); // tkhd (Track Header) let mut tkhd = Vec::new(); tkhd.extend_from_slice(b"tkhd"); tkhd.push(0); // version - tkhd.extend_from_slice(&[0, 0, 7]); // flags = enabled + in_movie + in_preview + // flags = in_movie + in_preview, and enabled for a track a renderer + // should play without being asked. Clearing the enabled bit does not + // hide an alternate — it is how a group says "selectable, but not the + // one to start with". + tkhd.extend_from_slice(&[0, 0, if enabled { 0x7 } else { 0x6 }]); tkhd.extend_from_slice(&[0; 4]); // creation_time tkhd.extend_from_slice(&[0; 4]); // modification_time tkhd.extend_from_slice(&track.id.to_be_bytes()); // track_id @@ -124,7 +152,7 @@ impl Fmp4Writer { tkhd.extend_from_slice(&movie_duration.to_be_bytes()); // duration, mvhd units tkhd.extend_from_slice(&[0; 8]); // reserved tkhd.extend_from_slice(&[0; 2]); // layer - tkhd.extend_from_slice(&[0; 2]); // alternate_group + tkhd.extend_from_slice(&alternate_group.to_be_bytes()); let vol = if track.track_kind == TrackKind::Audio { 0x0100u16 } else { @@ -168,16 +196,28 @@ impl Fmp4Writer { mdhd.extend_from_slice(&[0; 4]); // modification_time mdhd.extend_from_slice(×cale.to_be_bytes()); mdhd.extend_from_slice(&[0; 4]); // duration - mdhd.extend_from_slice(&[0x55, 0xc4]); // lang: "und" + mdhd.extend_from_slice(&packed_language(track.language.as_deref())); mdhd.extend_from_slice(&[0; 2]); // pre_defined mdia_body.extend_from_slice(&Self::wrap_box(&mdhd)); // hdlr (Handler Reference Atom) - let (handler_type, name) = match track.track_kind { + // + // The name field is nominally a description of the handler, and that is + // what it holds for a track with nothing better to say. But it is also + // where a good many renderers read the label they put beside a track in + // an audio menu, so a track the container named — "Commentary", + // "Director's cut" — carries that name here instead. + let (handler_type, default_name) = match track.track_kind { TrackKind::Video => (b"vide", "VideoHandler"), TrackKind::Audio => (b"soun", "SoundHandler"), TrackKind::Other => (b"hint", "HintHandler"), }; + let name = track + .name + .as_deref() + .map(str::trim) + .filter(|name| !name.is_empty()) + .unwrap_or(default_name); let mut hdlr = Vec::new(); hdlr.extend_from_slice(b"hdlr"); hdlr.extend_from_slice(&[0; 4]); // version + flags @@ -740,10 +780,134 @@ impl Fmp4Writer { } } +/// A track's language, as `mdhd` carries it. +/// +/// ISO-BMFF packs three lowercase ISO-639-2/T letters into fifteen bits, each +/// letter held as its distance from `0x60` — one below `a`, not `a` itself, +/// which is why the "und" every muxer writes for an unknown language comes out +/// as `0x55c4` and not a value fifty-nine lower. That is also what +/// anything unrecognisable resolves to here: a television offered a soundtrack +/// labelled with a language it cannot parse tends to hide the track, where an +/// honest "undetermined" leaves it listed. +/// +/// Matroska stores the two-letter ISO-639-1 forms in some files and the +/// three-letter ones in others, and appends a country suffix (`pt-BR`) in a few. +/// Only the three-letter form fits the field, so that is the one taken; the rest +/// is undetermined rather than guessed at. +fn packed_language(language: Option<&str>) -> [u8; 2] { + /// `und`, the value for a language not stated. + const UNDETERMINED: [u8; 2] = [0x55, 0xc4]; + + let Some(language) = language else { + return UNDETERMINED; + }; + let code = language.trim().split(['-', '_']).next().unwrap_or_default(); + let bytes = code.as_bytes(); + if bytes.len() != 3 || !bytes.iter().all(|b| b.is_ascii_alphabetic()) { + return UNDETERMINED; + } + let packed = bytes.iter().fold(0u16, |packed, byte| { + (packed << 5) | u16::from(byte.to_ascii_lowercase() - 0x60) + }); + packed.to_be_bytes() +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn a_language_is_packed_as_three_five_bit_letters() { + // The value every muxer writes for a language it does not know, and the + // one every reader recognises. + assert_eq!(packed_language(None), [0x55, 0xc4]); + assert_eq!(packed_language(Some("und")), [0x55, 0xc4]); + // 'e'-0x60 = 5, 'n'-0x60 = 14, 'g'-0x60 = 7, in fifteen bits. + let eng: u16 = (5 << 10) | (14 << 5) | 7; + assert_eq!(packed_language(Some("eng")), eng.to_be_bytes()); + assert_eq!(packed_language(Some("ENG")), packed_language(Some("eng"))); + } + + /// Only the three-letter form fits the field. Anything else is undetermined + /// rather than guessed at — a television offered a soundtrack labelled with + /// a language it cannot parse tends to hide the track entirely. + #[test] + fn a_language_that_does_not_fit_the_field_is_undetermined() { + for input in ["en", "english", "", " ", "e1g", "日本語"] { + assert_eq!( + packed_language(Some(input)), + [0x55, 0xc4], + "{input:?} must not be packed into fifteen bits" + ); + } + // A country suffix is dropped rather than taken as part of the code. + assert_eq!(packed_language(Some("por-BR")), packed_language(Some("por"))); + } + + fn track_of(id: u32, kind: TrackKind) -> TrackInfo { + TrackInfo { + id, + track_kind: kind, + codec: "AAC".into(), + codec_kind: TrackCodec::Aac, + language: None, + name: None, + sample_rate: Some(48_000), + channels: Some(2), + width: None, + height: None, + is_default: true, + extra_data: vec![0x11, 0x90], + } + } + + /// Every `tkhd` in a `moov`, as (flags, alternate_group), in track order. + fn track_headers(moov: &[u8]) -> Vec<(u32, u16)> { + let mut out = Vec::new(); + let mut from = 0; + while let Some(at) = moov[from..].windows(4).position(|w| w == b"tkhd") { + // tkhd body: version(1) + flags(3) + creation(4) + modification(4) + // + track_id(4) + reserved(4) + duration(4) + reserved(8) + layer(2). + let body = from + at + 4; + let flags = u32::from_be_bytes([0, moov[body + 1], moov[body + 2], moov[body + 3]]); + let group = u16::from_be_bytes(moov[body + 34..body + 36].try_into().unwrap()); + out.push((flags, group)); + from = body; + } + out + } + + /// Audio tracks must be declared alternatives of each other, or a player is + /// entitled to render every one of them at once — and exactly one of them + /// carries the enabled bit, or three tracks all claim to be the default. + /// + /// The values are ffmpeg's, checked against a file it muxed: the alternates + /// differ from the leading track in the enabled bit and nothing else. + #[test] + fn only_the_leading_soundtrack_is_enabled_and_they_share_a_group() { + let tracks = [ + track_of(1, TrackKind::Video), + track_of(2, TrackKind::Audio), + track_of(3, TrackKind::Audio), + track_of(4, TrackKind::Audio), + ]; + let refs: Vec<&TrackInfo> = tracks.iter().collect(); + assert_eq!( + track_headers(&Fmp4Writer::build_moov_for(&refs, None)), + vec![(0x7, 0), (0x7, 1), (0x6, 1), (0x6, 1)] + ); + } + + /// A lone soundtrack is the leading one, so the single-track path a browser + /// rendition takes is unaffected by any of that. + #[test] + fn a_single_track_moov_is_enabled_whatever_it_carries() { + for kind in [TrackKind::Audio, TrackKind::Video] { + let moov = Fmp4Writer::build_moov(&track_of(1, kind)); + assert_eq!(track_headers(&moov)[0].0, 0x7, "{kind:?}"); + } + } + #[test] fn test_ftyp_box_magic() { let ftyp = Fmp4Writer::build_ftyp(); diff --git a/crates/vuio-core/src/media/transcode/video.rs b/crates/vuio-core/src/media/transcode/video.rs index 486c958a..f8d6e005 100644 --- a/crates/vuio-core/src/media/transcode/video.rs +++ b/crates/vuio-core/src/media/transcode/video.rs @@ -3,10 +3,20 @@ //! The deliverable of phase 4. A television that cannot decode AC-3 or DTS shows //! the picture and produces no sound; what it is offered instead is this — the //! same file, in fragmented MP4, with the video track copied out bit for bit and -//! only the audio track decoded and re-encoded as AAC. The picture is never -//! touched, which is what keeps the CPU cost proportional to the soundtrack +//! every audio track decoded and re-encoded as AAC. The picture is never +//! touched, which is what keeps the CPU cost proportional to the soundtracks //! rather than to the film. //! +//! Every soundtrack, not the one we guessed at. A television switches audio +//! track inside its own demuxer, on bytes it already holds — no second request +//! is made and nothing about the switch reaches this server, so a track it can +//! be switched to has to be in the body before it asks. Carrying them all is +//! what makes the audio button work; the alternative is guessing which one the +//! viewer wanted and being wrong for every film with a commentary. It is +//! affordable because the picture is passthrough either way: one decode and +//! re-encode chain measures at about a hundredth of a core, so a film with four +//! soundtracks costs four hundredths rather than twice anything. +//! //! Nothing about the output is written twice or seeked back into, because a //! fragmented MP4 has no index to fix up at the end: an init segment describing //! both tracks, then `moof`/`mdat` pairs forever. That is what lets it go @@ -46,7 +56,10 @@ const DECODED_CHANNELS: u16 = 2; pub struct ProgressiveStream { format: Box, video: TrackSink, - audio: Option, + /// Every soundtrack being carried, in the order they are written into the + /// `moov` — which is the order a renderer that takes the first audio track + /// without asking will read them in, so the caller puts the default first. + audio: Vec, /// Total length of the film, for `mehd`. duration_secs: Option, sequence: u32, @@ -109,11 +122,14 @@ impl ProgressiveStream { /// Open `path` positioned at `start_secs`, ready to emit fragments. /// /// `video` and `audio` come from the same probe the caller used to decide - /// this resource exists at all, so nothing here re-inspects the file. + /// this resource exists at all, so nothing here re-inspects the file. Order + /// in `audio` is preserved into the output, and a track this build cannot + /// produce is dropped rather than carried: better a film with two working + /// soundtracks than one with a third that plays noise. pub fn open( path: &Path, video: &TrackInfo, - audio: Option<&TrackInfo>, + audio: &[TrackInfo], start_secs: f64, duration_secs: Option, ) -> Result { @@ -137,22 +153,22 @@ impl ProgressiveStream { } let video_tb = track_time_base(format.as_ref(), video.id); - // An audio track this build cannot produce is no audio track: better a - // silent film with a picture that plays than a stream carrying samples - // the renderer will read as noise. - let audio = audio.and_then(|track| { - let codec = track.codec_kind.transcode_codec(); - if codec.is_none() && track.codec_kind != TrackCodec::Aac { - return None; - } - let audio_tb = track_time_base(format.as_ref(), track.id); - Some(AudioSink { - source_id: track.id, - codec, - sink: TrackSink::new(aac_track(track), audio_tb), - decode: None, + let audio: Vec = audio + .iter() + .filter_map(|track| { + let codec = track.codec_kind.transcode_codec(); + if codec.is_none() && track.codec_kind != TrackCodec::Aac { + return None; + } + let audio_tb = track_time_base(format.as_ref(), track.id); + Some(AudioSink { + source_id: track.id, + codec, + sink: TrackSink::new(aac_track(track), audio_tb), + decode: None, + }) }) - }); + .collect(); Ok(Self { format, @@ -165,12 +181,10 @@ impl ProgressiveStream { }) } - /// `ftyp` + `moov`: the init segment describing both tracks. + /// `ftyp` + `moov`: the init segment describing every track. pub fn init_segment(&self) -> Vec { let mut tracks: Vec<&TrackInfo> = vec![&self.video.track]; - if let Some(audio) = &self.audio { - tracks.push(&audio.sink.track); - } + tracks.extend(self.audio.iter().map(|audio| &audio.sink.track)); let duration_ms = self .duration_secs .filter(|d| *d > 0.0) @@ -239,14 +253,19 @@ impl ProgressiveStream { } } - /// Feed one audio packet, if it belongs to the track being carried. + /// Feed one audio packet to whichever carried track it belongs to. + /// + /// A packet from a track that is not being carried — TrueHD, or a codec no + /// decoder here handles — finds no sink and is dropped, which is the whole + /// of what "not carried" means. fn take_audio(&mut self, track_id: u32, pts: i64, data: &[u8]) -> Result<()> { - let Some(audio) = self.audio.as_mut() else { + let Some(audio) = self + .audio + .iter_mut() + .find(|audio| audio.source_id == track_id) + else { return Ok(()); }; - if track_id != audio.source_id { - return Ok(()); - } let sample_rate = audio.sink.track.sample_rate.unwrap_or(48_000); let ticks = audio.sink.rescale(pts, sample_rate); @@ -292,33 +311,34 @@ impl ProgressiveStream { } fn flush_audio(&mut self) { - let Some(audio) = self.audio.as_mut() else { - return; - }; - let Some(decode) = audio.decode.as_mut() else { - return; - }; - let tail = decode.encoder.finish(); - push_aac(&mut audio.sink, decode, &tail, true); + for audio in &mut self.audio { + let Some(decode) = audio.decode.as_mut() else { + continue; + }; + let tail = decode.encoder.finish(); + push_aac(&mut audio.sink, decode, &tail, true); + } } - /// Wrap whatever both tracks hold into one fragment. + /// Wrap whatever every track holds into one fragment. fn emit(&mut self) -> Option> { // A fragment about to be written cannot wait for more packets before - // deciding where the audio run sits, so this is where the decision is - // forced if it has not been made already. - if let Some(audio) = self.audio.as_mut() { + // deciding where each audio run sits, so this is where the decision is + // forced if it has not been made already. Every track settles its own: + // they are separate runs off separate decoders and nothing about one + // says where another belongs. + for audio in &mut self.audio { if let Some(decode) = audio.decode.as_mut() { push_aac(&mut audio.sink, decode, &[], true); } } let video_packets = self.video.take(); - let audio_packets = self + let audio_packets: Vec> = self .audio - .as_mut() + .iter_mut() .map(|audio| audio.sink.take()) - .unwrap_or_default(); - if video_packets.is_empty() && audio_packets.is_empty() { + .collect(); + if video_packets.is_empty() && audio_packets.iter().all(Vec::is_empty) { return None; } @@ -326,8 +346,8 @@ impl ProgressiveStream { let mut tracks: Vec<(&TrackInfo, &[MediaPacket])> = vec![(&self.video.track, &video_packets)]; let mut fallbacks = vec![self.video.next_decode_time]; - if let Some(audio) = &self.audio { - tracks.push((&audio.sink.track, &audio_packets)); + for (audio, packets) in self.audio.iter().zip(&audio_packets) { + tracks.push((&audio.sink.track, packets)); fallbacks.push(audio.sink.next_decode_time); } let fragment = Fmp4Writer::build_multi_track_segment(self.sequence, &tracks, &fallbacks); @@ -335,8 +355,8 @@ impl ProgressiveStream { // Remember where each track's timeline reached, so a fragment a track // contributes nothing to still declares a sane base decode time. self.video.advance(&video_packets); - if let Some(audio) = self.audio.as_mut() { - audio.sink.advance(&audio_packets); + for (audio, packets) in self.audio.iter_mut().zip(&audio_packets) { + audio.sink.advance(packets); } Some(fragment) } @@ -461,8 +481,12 @@ impl TrackSink { /// encoder's channel count rather than the source's 5.1. A track that is already /// AAC keeps everything it had, including the config the container carried. fn aac_track(track: &TrackInfo) -> TrackInfo { + let name = track.name.clone().or_else(|| Some(source_label(track))); if track.codec_kind == TrackCodec::Aac { - return track.clone(); + return TrackInfo { + name, + ..track.clone() + }; } let sample_rate = track.sample_rate.unwrap_or(48_000); TrackInfo { @@ -471,10 +495,31 @@ fn aac_track(track: &TrackInfo) -> TrackInfo { channels: Some(DECODED_CHANNELS as u8), extra_data: super::audio_specific_config(sample_rate, DECODED_CHANNELS), track_kind: TrackKind::Audio, + name, ..track.clone() } } +/// What to call this track in a renderer's audio menu. +/// +/// Every carried track leaves here as stereo AAC, so naming them after what they +/// have become would print the same three words three times. What tells a film's +/// soundtracks apart is what they arrived as — the main mix in DTS 5.1, the +/// commentary in AC-3 stereo — so that is what the label states. Matroska's own +/// track name would be better still, and is preferred when there is one; there +/// is not, today, because the demuxer this reads from does not surface it. +fn source_label(track: &TrackInfo) -> String { + let layout = match track.channels { + Some(1) => "Mono".to_string(), + Some(2) => "Stereo".to_string(), + Some(6) => "5.1".to_string(), + Some(8) => "7.1".to_string(), + Some(count) => format!("{count}ch"), + None => return track.codec.clone(), + }; + format!("{} {layout}", track.codec) +} + fn track_time_base( format: &dyn symphonia::core::formats::FormatReader, track_id: u32, diff --git a/crates/vuio-core/src/platform/filesystem/metadata.rs b/crates/vuio-core/src/platform/filesystem/metadata.rs index 685a4070..f1f0fd95 100644 --- a/crates/vuio-core/src/platform/filesystem/metadata.rs +++ b/crates/vuio-core/src/platform/filesystem/metadata.rs @@ -245,12 +245,23 @@ fn probe_metadata(path: &Path) -> anyhow::Result { } } + // The container's own duration (e.g. Matroska's Segment > Info > Duration or MP4 mvhd) + let media_info = format.media_info(); + if let (Some(time_base), Some(duration)) = (media_info.time_base, media_info.duration) { + if let Some(t) = time_base.calc_time(symphonia::core::units::Timestamp::new(duration.get() as i64)) { + let secs = t.as_secs_f64(); + if secs > 0.0 { + probed.duration = Some(Duration::from_secs_f64(secs)); + } + } + } + // Stream properties come off the default audio track. A container with no // audio track still has usable tags, so this is not an error. if let Some(track) = format.default_track(TrackType::Audio) { let num_frames = track.num_frames; if let Some(audio) = track.codec_params.as_ref().and_then(|params| params.audio()) { - probed.stream.codec = audio_codec_short_name(audio.codec); + probed.stream.codec = audio_codec_short_name(audio.codec).map(|c| c.to_string()); probed.stream.sample_rate = audio.sample_rate; probed.stream.channels = audio .channels @@ -261,12 +272,29 @@ fn probe_metadata(path: &Path) -> anyhow::Result { .or(audio.bits_per_coded_sample) .map(|bits| bits as u16); - if let (Some(frames), Some(rate)) = (num_frames, audio.sample_rate.filter(|r| *r > 0)) { - probed.duration = Some(Duration::from_secs_f64(frames as f64 / f64::from(rate))); + if probed.duration.is_none() { + if let (Some(frames), Some(rate)) = (num_frames, audio.sample_rate.filter(|r| *r > 0)) { + probed.duration = Some(Duration::from_secs_f64(frames as f64 / f64::from(rate))); + } } } } + // If any audio track in the container is DTS, record it as DTS so that DTS transcoding + // can be properly applied for the film. + let has_dts = format.tracks().iter().any(|t| { + t.track_type() == Some(TrackType::Audio) + && t.codec_params + .as_ref() + .and_then(|p| p.audio()) + .and_then(|a| audio_codec_short_name(a.codec)) + .map(|c| c == "dca" || c == "dts") + .unwrap_or(false) + }); + if has_dts { + probed.stream.codec = Some("dts".to_string()); + } + // A file can carry more than one revision — ID3v2 at the head and APEv2 at // the tail, say. Draining the log oldest-first and letting later writes win // keeps every tag while still preferring the newest revision. diff --git a/crates/vuio-core/src/web/admin.rs b/crates/vuio-core/src/web/admin.rs index 87c2c373..64e7bf3a 100644 --- a/crates/vuio-core/src/web/admin.rs +++ b/crates/vuio-core/src/web/admin.rs @@ -446,18 +446,16 @@ const TRANSCODE_FIELDS: &[FieldSpec] = &[ ), noted( optional( - "transcode.prefer", - "List first", + "transcode.mode", + "Transcode Mode", FieldKind::Enum { - options: &["original", "transcoded"], + options: &["enabled", "forced", "disabled"], free_form: false, }, Impact::Live, - "Which of the two versions is listed first for a TV that takes whichever it is \ - given rather than choosing.", + "Operating mode: enabled (auto/standard), forced (transcoded stream listed first for all TVs), or disabled.", ), - "Leave on \u{201c}original\u{201d} unless a TV plays these films silently: that is the \ - symptom of one that takes the first version without checking whether it can decode it.", + "Use \u{201c}forced\u{201d} to force all TVs (even those with native DTS support) to play the transcoded AAC stream.", ), noted( optional( diff --git a/crates/vuio-core/src/web/mod.rs b/crates/vuio-core/src/web/mod.rs index 15dc83dc..0ba573e5 100644 --- a/crates/vuio-core/src/web/mod.rs +++ b/crates/vuio-core/src/web/mod.rs @@ -88,9 +88,9 @@ pub(crate) fn transcode_advert( } #[cfg(feature = "transcode")] { - use crate::config::{TranscodeAudioFormat, TranscodePreference}; + use crate::config::{TranscodeAudioFormat, TranscodeMode}; let config = state.current_config(); - if !config.transcode.enabled { + if !config.transcode.enabled || config.transcode.mode == TranscodeMode::Disabled { return None; } Some(xml::TranscodeAdvert { @@ -121,14 +121,14 @@ pub(crate) fn transcode_advert( video: Some(xml::AdvertResource { mime: "video/mp4", path: "transcode/video.mp4", - op: "01", + op: "10", }), // With no remuxer or no encoder there is nothing to offer a film. // Offering it `audio.wav` instead would replace a silent film with // no film at all. #[cfg(not(all(feature = "transcode-aac", feature = "casting")))] video: None, - first: config.transcode.prefer == TranscodePreference::Transcoded, + first: config.transcode.mode == TranscodeMode::Forced, }) } } diff --git a/crates/vuio-core/src/web/video_streaming.rs b/crates/vuio-core/src/web/video_streaming.rs index b13ee585..e6d4b879 100644 --- a/crates/vuio-core/src/web/video_streaming.rs +++ b/crates/vuio-core/src/web/video_streaming.rs @@ -38,7 +38,7 @@ use axum::{ }; use tracing::{debug, warn}; -use crate::media::remux::{browser_audio_tracks, browser_video_track, FileInfo, MkvDemuxer, TrackInfo, TrackKind}; +use crate::media::remux::{browser_audio_tracks, browser_video_track, FileInfo, MkvDemuxer, TrackInfo}; use crate::media::transcode::ProgressiveStream; use crate::{database::DatabaseManager, error::AppError, state::AppState}; @@ -57,7 +57,12 @@ const PIPELINE_DEPTH: usize = 2; /// seeked and is therefore set per resource rather than shared. const DLNA_FLAGS: &str = "DLNA.ORG_FLAGS=01700000000000000000000000000000"; -/// `?t=` for seeking, `?audio_track=` for selecting audio track. +/// `?t=` for seeking, `?audio_track=` to carry one track alone. +/// +/// The index is into [`browser_audio_tracks`], the same order the HLS renditions +/// use. Omitted — which is how a television reaches this, since the DIDL never +/// writes the parameter — every playable track is carried and the renderer +/// chooses between them itself. #[derive(serde::Deserialize, Default)] pub struct VideoQuery { t: Option, @@ -76,23 +81,20 @@ pub async fn serve_transcoded_video( let video = browser_video_track(&info.tracks) .ok_or(AppError::NotFound)? .clone(); - let audio = if let Some(idx) = query.audio_track { - let playable = browser_audio_tracks(&info.tracks); - playable - .get(idx) - .copied() - .cloned() - .or_else(|| default_audio_track(&info.tracks).cloned()) - } else { - default_audio_track(&info.tracks).cloned() - }; + let audio = audio_tracks(&info.tracks, query.audio_track); let duration = info.duration_secs.filter(|d| *d > 0.0); - let requested = headers + // A time seek asked for in a header is a range request and is answered as + // one. `?t=` is not: it is how the browser player and a test name a starting + // point, and it gets a plain 200 — a 206 nobody asked for is a response to a + // range request that was never made. + let seek_header = headers .get("TimeSeekRange.dlna.org") + .or_else(|| headers.get("timeseekrange.dlna.org")) + .or_else(|| headers.get(header::RANGE)) .and_then(|value| value.to_str().ok()) - .and_then(parse_npt_start) - .or(query.t); + .and_then(parse_npt_start); + let requested = seek_header.or(query.t); // A seek past the end is clamped rather than refused: a renderer that has // drifted a little past a film's declared duration should see the last // moment of it, not an error. @@ -101,8 +103,17 @@ pub async fn serve_transcoded_video( .max(0.0) .min(duration.map(|d| (d - 0.1).max(0.0)).unwrap_or(f64::MAX)); + tracing::debug!( + "transcoded video: id={id}, file={filename}, start={start:.3}s, \ + requested={requested:?}, audio={:?}", + audio + .iter() + .map(|track| (track.id, track.language.as_deref(), track.name.as_deref())) + .collect::>() + ); + let mut response = Response::builder() - .status(if requested.is_some() { + .status(if seek_header.is_some() { StatusCode::PARTIAL_CONTENT } else { StatusCode::OK @@ -110,24 +121,38 @@ pub async fn serve_transcoded_video( .header(header::CONTENT_TYPE, "video/mp4") .header(header::CACHE_CONTROL, "no-cache") .header("transferMode.dlna.org", "Streaming") - // OP=01: time seek yes, byte seek no. Saying otherwise would be worse - // than saying nothing — a renderer that byte-seeks a resource which - // cannot honour it stops playing rather than falling back. + // `DLNA.ORG_OP=ab` is two independent answers: `a` is whether + // `TimeSeekRange.dlna.org` is honoured and `b` is whether byte ranges + // are. So this is time seek yes, byte seek no — and the order matters + // more than anything else in this header, because `01` says the + // opposite. A renderer told it may byte-seek a resource with no length + // and no `Accept-Ranges` sends `Range: bytes=` for every scrub, gets the + // film from the beginning each time, and concludes the file cannot be + // seeked at all. .header( "contentFeatures.dlna.org", - format!("DLNA.ORG_OP=01;DLNA.ORG_CI=1;{DLNA_FLAGS}"), + format!("DLNA.ORG_OP=10;DLNA.ORG_CI=1;{DLNA_FLAGS}"), ); if let Some(duration) = duration { // A renderer with no length to divide has nothing else to draw a scrub // bar from. `mehd` in the init segment says the same thing; this is for // the ones that read headers and not boxes. response = response.header("X-Content-Duration", format!("{duration:.3}")); - if requested.is_some() { + // The range actually being answered, which DLNA asks for in reply to a + // request that named one — and only then, since it is an answer rather + // than an announcement. + if seek_header.is_some() { response = response.header( "TimeSeekRange.dlna.org", format!("npt={start:.3}-{duration:.3}/{duration:.3}"), ); } + // Not a DLNA header — DLNA's own `availableSeekRange.dlna.org` belongs + // to limited-operation content, which this is not. This is the + // PlayStation spelling of the same statement, read by a handful of + // renderers and ignored by the rest, and what it states is true: the + // whole film is reachable, from the first moment to the last. + response = response.header("X-AvailableSeekRange", format!("1 npt=0.000-{duration:.3}")); } if method == Method::HEAD { @@ -151,7 +176,7 @@ pub async fn serve_transcoded_video( fn fmp4_body( path: std::path::PathBuf, video: TrackInfo, - audio: Option, + audio: Vec, start: f64, duration: Option, permit: tokio::sync::OwnedSemaphorePermit, @@ -160,14 +185,13 @@ fn fmp4_body( tokio::task::spawn_blocking(move || { let _permit = permit; - let mut stream = - match ProgressiveStream::open(&path, &video, audio.as_ref(), start, duration) { - Ok(stream) => stream, - Err(e) => { - let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); - return; - } - }; + let mut stream = match ProgressiveStream::open(&path, &video, &audio, start, duration) { + Ok(stream) => stream, + Err(e) => { + let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); + return; + } + }; if tx .blocking_send(Ok(bytes::Bytes::from(stream.init_segment()))) @@ -212,18 +236,30 @@ async fn resolve( Ok((file.path, file.filename, info)) } -/// Which audio track to carry. +/// Which of a film's soundtracks to carry, and in what order. /// -/// The one the container marks default, then the first this build can produce. -/// Deliberately not clever about language: a wrong guess is worse than a -/// predictable one, and the fix if it turns out to matter is one `` per -/// audio track, which the DIDL already supports. -fn default_audio_track(tracks: &[TrackInfo]) -> Option<&TrackInfo> { - let playable = |t: &&TrackInfo| t.track_kind == TrackKind::Audio && t.codec_kind.is_playable(); - tracks - .iter() - .find(|t| playable(t) && t.is_default) - .or_else(|| tracks.iter().find(playable)) +/// All of them, because a television switches audio track inside its own +/// demuxer and never tells this server it did — so a track it can be switched +/// to has to already be in the body. The one the container marks default is put +/// first, which is what a renderer that takes the leading audio track without +/// asking will play; the rest follow in container order so a viewer reading down +/// the audio menu sees them as the file lists them. +/// +/// `only` restricts the answer to one track, by index into +/// [`browser_audio_tracks`] — the same order the HLS renditions are numbered in. +/// That is for the browser player and for narrowing down a report of a bad +/// track; a television never sends it. +fn audio_tracks(tracks: &[TrackInfo], only: Option) -> Vec { + let playable = browser_audio_tracks(tracks); + if let Some(index) = only { + return playable.get(index).copied().cloned().into_iter().collect(); + } + let default = playable.iter().position(|track| track.is_default); + let mut ordered: Vec = playable.into_iter().cloned().collect(); + if let Some(index) = default { + ordered[..=index].rotate_right(1); + } + ordered } fn busy(state: &AppState, filename: &str) -> Response { @@ -266,7 +302,7 @@ pub(crate) fn parse_npt_start(header: &str) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::media::remux::TrackCodec; + use crate::media::remux::{TrackCodec, TrackKind}; fn audio(id: u32, codec_kind: TrackCodec, is_default: bool) -> TrackInfo { TrackInfo { @@ -285,42 +321,84 @@ mod tests { } } + fn ids(tracks: &[TrackInfo], only: Option) -> Vec { + audio_tracks(tracks, only).iter().map(|t| t.id).collect() + } + + /// Every soundtrack is carried, because the television switches between + /// them without asking — and the default leads, because a renderer that + /// takes the first one instead must still get the right one. #[test] - fn a_multi_audio_film_carries_the_track_the_container_marks_default() { + fn every_playable_track_is_carried_with_the_default_leading() { let tracks = vec![ audio(2, TrackCodec::Ac3, false), audio(3, TrackCodec::Ac3, true), audio(4, TrackCodec::Ac3, false), ]; - assert_eq!(default_audio_track(&tracks).map(|t| t.id), Some(3)); + let expected: Vec = if TrackCodec::Ac3.is_playable() { + vec![3, 2, 4] + } else { + vec![] + }; + assert_eq!(ids(&tracks, None), expected); } + /// Moving the default to the front must not reshuffle anything else: the + /// audio menu should read in the order the file lists them. #[test] - fn with_no_default_marked_the_first_playable_track_is_carried() { + fn the_tracks_behind_the_default_keep_their_container_order() { + let tracks = vec![ + audio(2, TrackCodec::Aac, false), + audio(3, TrackCodec::Aac, false), + audio(4, TrackCodec::Aac, true), + audio(5, TrackCodec::Aac, false), + ]; + assert_eq!(ids(&tracks, None), vec![4, 2, 3, 5]); + } + + #[test] + fn with_no_default_marked_the_container_order_stands() { // The first track here is TrueHD: named, and decoded by nothing // vendored. Carrying it would produce a stream of noise. let tracks = vec![ audio(2, TrackCodec::Unsupported, false), audio(3, TrackCodec::Ac3, false), ]; - let expected = if TrackCodec::Ac3.is_playable() { - Some(3) + let expected: Vec = if TrackCodec::Ac3.is_playable() { + vec![3] } else { - None + vec![] }; - assert_eq!(default_audio_track(&tracks).map(|t| t.id), expected); + assert_eq!(ids(&tracks, None), expected); } - /// A default-marked track this build cannot produce must not win over one it - /// can: the point of the preference is which track to carry, not whether to - /// carry a broken one. + /// A default-marked track this build cannot produce must not take the lead + /// from one it can: the point of the preference is which track plays first, + /// not whether to lead with a broken one. #[test] fn a_default_track_this_build_cannot_decode_is_passed_over() { let tracks = vec![ audio(2, TrackCodec::Unsupported, true), audio(3, TrackCodec::Aac, false), + audio(4, TrackCodec::Aac, false), + ]; + assert_eq!(ids(&tracks, None), vec![3, 4]); + } + + /// `?audio_track=` indexes the playable tracks, not the container's, which + /// is what makes it mean the same track as the HLS rendition of that number. + #[test] + fn an_explicit_index_carries_that_track_alone() { + let tracks = vec![ + audio(2, TrackCodec::Unsupported, false), + audio(3, TrackCodec::Aac, false), + audio(4, TrackCodec::Aac, true), ]; - assert_eq!(default_audio_track(&tracks).map(|t| t.id), Some(3)); + assert_eq!(ids(&tracks, Some(0)), vec![3]); + assert_eq!(ids(&tracks, Some(1)), vec![4]); + // Out of range is no track rather than a fallback to the default: a + // renderer asking for a track that is not there has a bug worth seeing. + assert!(ids(&tracks, Some(2)).is_empty()); } #[test] diff --git a/crates/vuio-core/src/web/xml/browse.rs b/crates/vuio-core/src/web/xml/browse.rs index 57110e7b..ce9489ec 100644 --- a/crates/vuio-core/src/web/xml/browse.rs +++ b/crates/vuio-core/src/web/xml/browse.rs @@ -253,7 +253,19 @@ pub async fn generate_browse_response( !file.mime_type.starts_with("video/") || crate::web::item_can_remux_video(file.stream.video_codec.as_deref()) }); - if let Some(advert) = transcoded.filter(|a| a.first) { + let is_dts = !file.mime_type.starts_with("video/") + || file + .stream + .codec + .as_deref() + .map(|c| c.trim().to_ascii_lowercase()) + .map(|c| c == "dca" || c == "dts" || c == "a_dts") + .unwrap_or_else(|| { + file.mime_type.contains("dts") + || file.filename.to_ascii_lowercase().ends_with(".dts") + }); + let is_forced = transcoded.as_ref().is_some_and(|a| a.first && is_dts); + if let Some(advert) = transcoded.filter(|a| a.first && is_dts) { let _ = advert.write_didl( &mut didl, server_ip, @@ -264,48 +276,50 @@ pub async fn generate_browse_response( ); } - let _ = write!( - &mut didl, - r#"http://{}:{}/media/{}"#, - server_ip, - state.http_binding.port(), - file_id - ); + if let Some(secs) = duration_secs { + let _ = write!(&mut didl, r#" duration="{}""#, format_duration(secs)); + } - if let Some(advert) = transcoded.filter(|a| !a.first) { - let _ = advert.write_didl( + if (client == crate::web::client::DlnaClientProfile::LgTv + || client == crate::web::client::DlnaClientProfile::PanasonicTv) + && has_srt + { + let _ = write!( + &mut didl, + r#" pv:subtitleFileUri="http://{}:{}/media/{}/subtitle" pv:subtitleFileType="SRT""#, + server_ip, + state.http_binding.port(), + file_id + ); + } + + let _ = write!( &mut didl, + r#">http://{}:{}/media/{}"#, server_ip, state.http_binding.port(), - file_id, - &file.mime_type, - duration_secs, + file_id ); + + if let Some(advert) = transcoded.filter(|a| !a.first) { + let _ = advert.write_didl( + &mut didl, + server_ip, + state.http_binding.port(), + file_id, + &file.mime_type, + duration_secs, + ); + } } if client == crate::web::client::DlnaClientProfile::LgTv && has_srt { diff --git a/crates/vuio-core/src/web/xml/rendering.rs b/crates/vuio-core/src/web/xml/rendering.rs index 63d325f3..93c730f3 100644 --- a/crates/vuio-core/src/web/xml/rendering.rs +++ b/crates/vuio-core/src/web/xml/rendering.rs @@ -502,63 +502,74 @@ pub(super) fn write_media_view( .filter(|_| { !mime.starts_with("video/") || crate::web::item_can_remux_video(file.video_codec()) }); + let is_dts = !mime.starts_with("video/") + || file + .codec() + .map(|c| c.trim().to_ascii_lowercase()) + .map(|c| c == "dca" || c == "dts" || c == "a_dts") + .unwrap_or_else(|| { + mime.contains("dts") || file.filename().to_ascii_lowercase().ends_with(".dts") + }); + let is_forced = transcoded.as_ref().is_some_and(|a| a.first && is_dts); let item_duration = file.duration_secs().map(|value| value as u64); - if let Some(advert) = transcoded.filter(|a| a.first) { + if let Some(advert) = transcoded.filter(|a| a.first && is_dts) { advert.write(output, context, file_id, mime, item_duration)?; } - write!( - output, - r#" 0) { + write!(output, r#" bitrate="{}""#, bits_per_second / 8)?; + } + if let Some(sample_rate) = file.sample_rate().filter(|rate| *rate > 0) { + write!(output, r#" sampleFrequency="{sample_rate}""#)?; + } + if let Some(channels) = file.channels().filter(|count| *count > 0) { + write!(output, r#" nrAudioChannels="{channels}""#)?; + } + if let Some(bits) = file.bits_per_sample().filter(|bits| *bits > 0) { + write!(output, r#" bitsPerSample="{bits}""#)?; + } + } + if matches!( + context.client, + crate::web::client::DlnaClientProfile::LgTv + | crate::web::client::DlnaClientProfile::PanasonicTv + ) && has_srt + { write!( output, - r#" duration="{:02}:{:02}:{:02}""#, - seconds / 3600, - (seconds % 3600) / 60, - seconds % 60 + r#" pv:subtitleFileUri="http://{}:{}/media/{}/subtitle" pv:subtitleFileType="SRT""#, + context.server_ip, context.server_port, file_id )?; } - } - if !is_radio { - // Renderers use these to decide whether they can play a track before - // fetching a byte of it. Note that DLNA's `res@bitrate` is *bytes* per - // second, not bits, which is the usual thing to get wrong. - if let Some(bits_per_second) = file.bit_rate().filter(|rate| *rate > 0) { - write!(output, r#" bitrate="{}""#, bits_per_second / 8)?; - } - if let Some(sample_rate) = file.sample_rate().filter(|rate| *rate > 0) { - write!(output, r#" sampleFrequency="{sample_rate}""#)?; - } - if let Some(channels) = file.channels().filter(|count| *count > 0) { - write!(output, r#" nrAudioChannels="{channels}""#)?; - } - if let Some(bits) = file.bits_per_sample().filter(|bits| *bits > 0) { - write!(output, r#" bitsPerSample="{bits}""#)?; - } - } - if matches!( - context.client, - crate::web::client::DlnaClientProfile::LgTv - | crate::web::client::DlnaClientProfile::PanasonicTv - ) && has_srt - { write!( output, - r#" pv:subtitleFileUri="http://{}:{}/media/{}/subtitle" pv:subtitleFileType="SRT""#, + ">http://{}:{}/media/{}", context.server_ip, context.server_port, file_id )?; - } - write!( - output, - ">http://{}:{}/media/{}", - context.server_ip, context.server_port, file_id - )?; - if let Some(advert) = transcoded.filter(|a| !a.first) { - advert.write(output, context, file_id, mime, item_duration)?; + if let Some(advert) = transcoded.filter(|a| !a.first) { + advert.write(output, context, file_id, mime, item_duration)?; + } } if context.client == crate::web::client::DlnaClientProfile::LgTv && has_srt { write!( diff --git a/crates/vuio-core/tests/common/mod.rs b/crates/vuio-core/tests/common/mod.rs index e97165e9..d7aa3027 100644 --- a/crates/vuio-core/tests/common/mod.rs +++ b/crates/vuio-core/tests/common/mod.rs @@ -54,6 +54,9 @@ pub struct Track { pub all_keyframes: bool, /// Whether the container marks this the default track of its kind. pub is_default: bool, + /// Matroska `Language`, an ISO-639-2 code. `None` writes no element, which + /// is how a muxer says nothing rather than saying "und". + pub language: Option<&'static str>, } pub enum TrackKind { @@ -97,6 +100,9 @@ pub fn build_mkv(tracks: &[Track], duration_ms: f64) -> Vec { }, )); // TrackType entry.extend(uint_el(0x88, u64::from(track.is_default))); // FlagDefault + if let Some(language) = track.language { + entry.extend(str_el(0x22B59C, language)); // Language + } entry.extend(str_el(0x86, track.codec_id)); // CodecID if !track.codec_private.is_empty() { entry.extend(bin_el(0x63A2, &track.codec_private)); // CodecPrivate diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs index e3737e90..03723bde 100644 --- a/crates/vuio-core/tests/film_transcode_tests.rs +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -59,6 +59,7 @@ pub fn film(seconds: f64) -> Vec { samples: video_samples, all_keyframes: false, is_default: true, + language: None, }, Track { number: 2, @@ -71,6 +72,7 @@ pub fn film(seconds: f64) -> Vec { samples: audio_samples, all_keyframes: true, is_default: true, + language: Some("eng"), }, ], seconds * 1000.0, @@ -649,7 +651,7 @@ async fn the_remuxed_film_is_a_parseable_fmp4_with_both_tracks() { let features = headers["contentFeatures.dlna.org"].to_str().unwrap(); assert!(features.contains("DLNA.ORG_CI=1"), "{features}"); assert!( - features.contains("DLNA.ORG_OP=01"), + features.contains("DLNA.ORG_OP=10"), "time seek yes, byte seek no: {features}" ); assert!( @@ -761,6 +763,45 @@ async fn a_time_seek_starts_the_film_where_it_was_asked_to() { ); } +/// A `206` is an answer to a range request. `?t=` is not one — it is how the +/// browser player and these tests name a starting point — so it gets a plain +/// `200`, and no `TimeSeekRange.dlna.org` stating a range nobody asked about. +#[tokio::test] +async fn only_a_range_request_is_answered_as_partial_content() { + let (_temp, state, id) = scanned_film(12.0).await; + + let (status, headers, _) = video_mp4(&state, id, Method::GET, Some("npt=6.000-")).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert!(headers.contains_key("TimeSeekRange.dlna.org")); + + let response = create_router(state.clone(), Surface::Primary) + .oneshot( + Request::builder() + .method(Method::GET) + .uri(format!("/media/{id}/transcode/video.mp4?t=6.0")) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert!( + !response.headers().contains_key("TimeSeekRange.dlna.org"), + "the header is a reply to a request that named a range" + ); + + // And the seek still happened — the point is the status, not the position. + let body = axum::body::to_bytes(response.into_body(), 256 * 1024 * 1024) + .await + .unwrap(); + let tfdt = find_box(&body, "tfdt").expect("a tfdt in the first fragment"); + let seconds = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()) as f64 / 90_000.0; + assert!((5.0..=6.1).contains(&seconds), "started at {seconds:.3}s"); +} + #[tokio::test] async fn the_same_seek_expressed_as_a_clock_time_lands_in_the_same_place() { let (_temp, state, id) = scanned_film(12.0).await; @@ -782,52 +823,276 @@ async fn a_seek_past_the_end_is_clamped_rather_than_refused() { assert_eq!(&top[..2], &["ftyp", "moov"]); } -/// A film with several soundtracks: the one the container marks default is the -/// one carried. The output track keeps the source's track number, so the `tkhd` -/// in the init segment says which was chosen. +/// The reason every soundtrack is carried rather than one: a television switches +/// audio track inside its own demuxer, on bytes it already holds. No second +/// request is made, and nothing about the switch reaches this server — so a +/// track it can be switched to has to be in the body before it asks. #[tokio::test] -async fn a_multi_audio_film_carries_the_track_the_container_marks_default() { - for default_track in [2u64, 3] { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("media"); - std::fs::create_dir_all(&root).unwrap(); - std::fs::write(root.join("Film.mkv"), multi_audio_film(6.0, default_track)).unwrap(); - - let state = common::state_over(temp.path(), &root).await; - let id = common::scan_into(&state) - .await - .iter() - .find(|f| f.filename == "Film.mkv") - .unwrap() - .id - .unwrap(); +async fn a_multi_audio_film_carries_every_soundtrack() { + let (_temp, state, id) = multi_audio_state(6.0, 2).await; + let (status, _, body) = video_mp4(&state, id, Method::GET, None).await; + assert_eq!(status, StatusCode::OK); - let (status, _, body) = video_mp4(&state, id, Method::GET, None).await; - assert_eq!(status, StatusCode::OK); + assert_eq!( + audio_track_ids(&body), + vec![2, 3, 4], + "all three soundtracks must be in the container, not the one we guessed at" + ); + let moov = find_moov(&body); + let traks = boxes(moov).iter().filter(|(n, _)| n == "trak").count(); + assert_eq!(traks, 4, "one video track and three audio tracks"); +} - let moov = boxes(&body) - .into_iter() - .find(|(name, _)| name == "moov") - .unwrap() - .1; - let audio_trak = boxes(moov) - .into_iter() - .filter(|(name, _)| name == "trak") - .find(|(_, body)| find_box(body, "mp4a").is_some()) - .expect("an audio track") - .1; - let tkhd = find_box(audio_trak, "tkhd").expect("a tkhd"); - // version(1) + flags(3) + creation(4) + modification(4), then track_id. - let track_id = u32::from_be_bytes(tkhd[12..16].try_into().unwrap()); +/// The default leads, because a renderer that takes the first audio track +/// without asking must still get the one the container nominated. +#[tokio::test] +async fn the_default_soundtrack_is_written_first() { + for (default_track, expected) in [ + (2u64, vec![2, 3, 4]), + (3, vec![3, 2, 4]), + (4, vec![4, 2, 3]), + ] { + let (_temp, state, id) = multi_audio_state(6.0, default_track).await; + let (_, _, body) = video_mp4(&state, id, Method::GET, None).await; assert_eq!( - u64::from(track_id), - default_track, - "the default-marked track must be the one carried" + audio_track_ids(&body), + expected, + "with track {default_track} marked default" ); } } -/// A film with two AC-3 soundtracks, one of which the container marks default. +/// Audio tracks all in one alternate group, and only the first of them enabled. +/// A player reading a file whose audio tracks are all in group zero is entitled +/// to render them all at once — three soundtracks over each other — and three +/// tracks all claiming to be the default is a choice it then makes for itself. +#[tokio::test] +async fn the_soundtracks_are_declared_alternatives_of_each_other() { + let (_temp, state, id) = multi_audio_state(6.0, 2).await; + let (_, _, body) = video_mp4(&state, id, Method::GET, None).await; + + let headers: Vec<(&str, u8, u16)> = traks_by_kind(&body) + .into_iter() + .map(|(kind, trak)| { + let tkhd = find_box(trak, "tkhd").expect("a tkhd"); + // version(1) + flags(3) + creation(4) + modification(4) + track_id(4) + // + reserved(4) + duration(4) + reserved(8) + layer(2). + ( + kind, + tkhd[3], + u16::from_be_bytes(tkhd[34..36].try_into().unwrap()), + ) + }) + .collect(); + + assert_eq!( + headers, + vec![ + ("video", 0x7, 0), + ("audio", 0x7, 1), + ("audio", 0x6, 1), + ("audio", 0x6, 1), + ], + "(kind, tkhd flags, alternate_group) per track" + ); +} + +/// What a television prints beside each entry in its audio menu. Without these +/// three soundtracks are three identical lines. +#[tokio::test] +async fn each_soundtrack_carries_its_language_and_a_label() { + let (_temp, state, id) = multi_audio_state(6.0, 2).await; + let (_, _, body) = video_mp4(&state, id, Method::GET, None).await; + + let mut seen = Vec::new(); + for (kind, trak) in traks_by_kind(&body) { + if kind != "audio" { + continue; + } + let mdhd = find_box(trak, "mdhd").expect("an mdhd"); + // version(1) + flags(3) + creation(4) + modification(4) + timescale(4) + // + duration(4), then the packed language. + let packed = u16::from_be_bytes(mdhd[20..22].try_into().unwrap()); + let unpack = |shift: u16| (((packed >> shift) & 0x1f) as u8 + 0x60) as char; + let language: String = [unpack(10), unpack(5), unpack(0)].into_iter().collect(); + + let hdlr = find_box(trak, "hdlr").expect("an hdlr"); + // version+flags(4) + pre_defined(4) + handler_type(4) + reserved(12). + let name = String::from_utf8_lossy(&hdlr[24..]) + .trim_end_matches('\0') + .to_string(); + seen.push((language, name)); + } + + assert_eq!( + seen, + vec![ + ("eng".to_string(), "AC-3 Stereo".to_string()), + ("fra".to_string(), "AC-3 Stereo".to_string()), + ("deu".to_string(), "AC-3 Stereo".to_string()), + ], + "each track must state the language it is in and what it arrived as" + ); +} + +/// The one path that still carries a single track, for the browser player and +/// for narrowing down a report of a bad soundtrack. +#[tokio::test] +async fn an_explicit_audio_track_index_carries_that_one_alone() { + let (_temp, state, id) = multi_audio_state(6.0, 2).await; + let response = create_router(state.clone(), Surface::Primary) + .oneshot( + Request::builder() + .method(Method::GET) + .uri(format!("/media/{id}/transcode/video.mp4?audio_track=1")) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let body = axum::body::to_bytes(response.into_body(), 256 * 1024 * 1024) + .await + .unwrap() + .to_vec(); + + assert_eq!( + audio_track_ids(&body), + vec![3], + "index 1 of the playable tracks is the film's second soundtrack" + ); +} + +/// Present in the `moov` is not the same as audible. Each soundtrack runs off +/// its own decoder and its own encoder, and a routing mistake would leave two of +/// them declared in the init segment and empty for the length of the film — +/// which a television shows as three entries, two of them silent. +#[tokio::test] +async fn every_soundtrack_carries_samples_through_the_whole_film() { + let (_temp, state, id) = multi_audio_state(6.0, 2).await; + let (_, _, body) = video_mp4(&state, id, Method::GET, None).await; + + let mut samples: std::collections::BTreeMap = Default::default(); + let mut fragments = 0; + for (name, moof) in boxes(&body) { + if name != "moof" { + continue; + } + fragments += 1; + for (name, traf) in boxes(moof) { + if name != "traf" { + continue; + } + let mut track_id = None; + for (name, contents) in boxes(traf) { + // tfhd: version(1) + flags(3), then track_ID. + if name == "tfhd" { + track_id = Some(u32::from_be_bytes(contents[4..8].try_into().unwrap())); + } + // trun: version(1) + flags(3), then sample_count. + if name == "trun" { + let count = u32::from_be_bytes(contents[4..8].try_into().unwrap()); + *samples + .entry(track_id.expect("a tfhd before the trun")) + .or_default() += count; + } + } + } + } + + assert!( + fragments >= 2, + "expected several fragments, got {fragments}" + ); + // Six seconds of 48 kHz audio is 281 AAC frames of 1024 samples, and each + // track should carry very nearly all of them — the first fragment or two + // are lost to the wait for the video's first keyframe. + for track_id in [2u32, 3, 4] { + let count = samples.get(&track_id).copied().unwrap_or(0); + assert!( + (200..=290).contains(&count), + "track {track_id} carried {count} AAC frames across the film: {samples:?}" + ); + } + let video = samples.get(&1).copied().unwrap_or(0); + assert!( + (140..=155).contains(&video), + "the picture must still come through: {video} frames of a 25 fps six-second film" + ); +} + +/// A film with several soundtracks, scanned and served. +async fn multi_audio_state( + seconds: f64, + default_track: u64, +) -> (tempfile::TempDir, vuio_core::state::AppState, i64) { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write( + root.join("Film.mkv"), + multi_audio_film(seconds, default_track), + ) + .unwrap(); + + let state = common::state_over(temp.path(), &root).await; + let id = common::scan_into(&state) + .await + .iter() + .find(|f| f.filename == "Film.mkv") + .unwrap() + .id + .unwrap(); + (temp, state, id) +} + +fn find_moov(body: &[u8]) -> &[u8] { + boxes(body) + .into_iter() + .find(|(name, _)| name == "moov") + .expect("a moov") + .1 +} + +/// Each `trak` in the init segment, tagged by what it carries, in `moov` order. +fn traks_by_kind(body: &[u8]) -> Vec<(&'static str, &[u8])> { + boxes(find_moov(body)) + .into_iter() + .filter(|(name, _)| name == "trak") + .map(|(_, trak)| { + let kind = if find_box(trak, "mp4a").is_some() { + "audio" + } else { + "video" + }; + (kind, trak) + }) + .collect() +} + +/// The `tkhd` track ids of the audio tracks, in the order they are written. +/// +/// The output track keeps the source's track number — one track in, one track +/// out — so this says which of the film's soundtracks went where. +fn audio_track_ids(body: &[u8]) -> Vec { + traks_by_kind(body) + .into_iter() + .filter(|(kind, _)| *kind == "audio") + .map(|(_, trak)| { + let tkhd = find_box(trak, "tkhd").expect("a tkhd"); + // version(1) + flags(3) + creation(4) + modification(4), then track_id. + u32::from_be_bytes(tkhd[12..16].try_into().unwrap()) + }) + .collect() +} + +/// A film with three soundtracks, one of which the container marks default. +/// +/// Three rather than two, and in three languages, so that "the default leads" +/// and "the rest keep container order" are distinguishable assertions rather +/// than the same one. fn multi_audio_film(seconds: f64, default_track: u64) -> Vec { let frames: Vec<&[u8]> = AC3.chunks_exact(AC3_FRAME_LEN).collect(); let audio_count = (seconds * 1000.0 / AC3_FRAME_MS).round() as usize; @@ -851,7 +1116,7 @@ fn multi_audio_film(seconds: f64, default_track: u64) -> Vec { }) .collect(); - let audio = |number: u64, offset: usize| Track { + let audio = |number: u64, offset: usize, language: &'static str| Track { number, codec_id: "A_AC3", codec_private: Vec::new(), @@ -862,6 +1127,7 @@ fn multi_audio_film(seconds: f64, default_track: u64) -> Vec { samples: audio_samples(offset), all_keyframes: true, is_default: number == default_track, + language: Some(language), }; build_mkv( @@ -877,9 +1143,11 @@ fn multi_audio_film(seconds: f64, default_track: u64) -> Vec { samples: video_samples, all_keyframes: false, is_default: true, + language: None, }, - audio(2, 0), - audio(3, 1), + audio(2, 0, "eng"), + audio(3, 1, "fra"), + audio(4, 2, "deu"), ], seconds * 1000.0, ) @@ -899,7 +1167,7 @@ async fn a_film_advertises_the_remuxed_film_and_not_its_soundtrack() { "offering a film's soundtrack in place of the film would lose the picture:\n{didl}" ); assert!( - didl.contains("DLNA.ORG_OP=01;DLNA.ORG_CI=1"), + didl.contains("DLNA.ORG_OP=10;DLNA.ORG_CI=1"), "the advertised operations must be the ones the resource honours:\n{didl}" ); // The original stays, and stays first by default, so a television that can @@ -1050,3 +1318,15 @@ fn dump_long_fixture() { std::fs::write(&out, film(7200.0)).unwrap(); eprintln!("wrote {out}"); } + +/// Not a test: writes the multi-audio remux out so an external demuxer can be +/// pointed at it. `cargo test --all-features dump_multi_audio -- --ignored` +#[tokio::test] +#[ignore] +async fn dump_multi_audio() { + let (_temp, state, id) = multi_audio_state(6.0, 3).await; + let (_, _, body) = video_mp4(&state, id, Method::GET, None).await; + let out = std::env::var("VUIO_DUMP").unwrap_or_else(|_| "/tmp/multi_audio.mp4".into()); + std::fs::write(&out, &body).unwrap(); + eprintln!("wrote {} bytes to {out}", body.len()); +} diff --git a/crates/vuio-core/tests/transcode_integration_tests.rs b/crates/vuio-core/tests/transcode_integration_tests.rs index 4bc9c279..837920d7 100644 --- a/crates/vuio-core/tests/transcode_integration_tests.rs +++ b/crates/vuio-core/tests/transcode_integration_tests.rs @@ -537,20 +537,22 @@ async fn the_original_is_listed_first_by_default() { } #[tokio::test] -async fn prefer_transcoded_puts_the_decoded_resource_first() { +async fn forced_transcode_serves_the_decoded_resource_exclusively() { let (_temp, mut state, _) = library().await; let mut config = (*state.config).clone(); - config.transcode.prefer = vuio_core::config::TranscodePreference::Transcoded; + config.transcode.mode = vuio_core::config::TranscodeMode::Forced; let config = Arc::new(config); state.config = config.clone(); state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); let didl = browse_audio(&state).await; - let original = didl.find("audio/ac3").expect("original res"); - let decoded = didl.find("audio/vnd.wave").expect("decoded res"); assert!( - decoded < original, - "the decoded resource must come first when asked for:\n{didl}" + didl.find("audio/vnd.wave").is_some(), + "the decoded resource must be present:\n{didl}" + ); + assert!( + didl.find("audio/ac3").is_none(), + "the original unsupported resource must be omitted in forced mode:\n{didl}" ); } From eb6d94cf4ac6e6ddfb9e1b7deecafff676c02082 Mon Sep 17 00:00:00 2001 From: vyrti Date: Tue, 25 Aug 2026 15:28:09 +0300 Subject: [PATCH 19/38] bugs --- config.example.toml | 14 +- crates/vuio-core/src/config/model.rs | 27 +- crates/vuio-core/src/config/template.toml | 15 +- .../vuio-core/src/media/remux/ac3_config.rs | 274 ++++ crates/vuio-core/src/media/remux/annexb.rs | 283 ++++ .../vuio-core/src/media/remux/fmp4_writer.rs | 63 +- .../vuio-core/src/media/remux/mkv_demuxer.rs | 35 + crates/vuio-core/src/media/remux/mod.rs | 9 + crates/vuio-core/src/media/remux/ts_writer.rs | 452 ++++++ crates/vuio-core/src/media/transcode/aac.rs | 23 +- .../vuio-core/src/media/transcode/frames.rs | 1 + crates/vuio-core/src/media/transcode/mod.rs | 28 + .../vuio-core/src/media/transcode/session.rs | 42 + crates/vuio-core/src/media/transcode/ts.rs | 1178 ++++++++++++++++ crates/vuio-core/src/media/transcode/video.rs | 191 ++- crates/vuio-core/src/web/client.rs | 38 + crates/vuio-core/src/web/mod.rs | 129 +- crates/vuio-core/src/web/streaming.rs | 8 + crates/vuio-core/src/web/ts_streaming.rs | 544 ++++++++ crates/vuio-core/src/web/video_streaming.rs | 332 +++-- crates/vuio-core/src/web/xml/browse.rs | 28 +- crates/vuio-core/src/web/xml/rendering.rs | 94 +- .../vuio-core/tests/film_transcode_tests.rs | 1214 ++++++++++++++++- 23 files changed, 4798 insertions(+), 224 deletions(-) create mode 100644 crates/vuio-core/src/media/remux/ac3_config.rs create mode 100644 crates/vuio-core/src/media/remux/annexb.rs create mode 100644 crates/vuio-core/src/media/remux/ts_writer.rs create mode 100644 crates/vuio-core/src/media/transcode/ts.rs create mode 100644 crates/vuio-core/src/web/ts_streaming.rs diff --git a/config.example.toml b/config.example.toml index aa4ec5cd..1dd8418c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -143,7 +143,13 @@ audio_format = "lpcm" # forced transcode is forced; only transcoded stream is offered so every TV plays it # disabled transcode is disabled mode = "enabled" -# Decodes allowed at once, shared between TVs and the browser player. Past this -# a further request is refused rather than queued, so the streams already -# playing keep up. -max_concurrent = 2 +# Sessions allowed at once, shared between TVs and the browser player. Past this +# a further request is refused rather than queued. +# +# Set high on purpose. A television opens three connections within a second of +# pressing play — one to play on, one to read the end of the file, and one to +# play on again — so a low ceiling refuses the one it meant to play on and the +# film never starts. The work is cheap besides: the picture is passed through +# and so is Dolby, and only a DTS soundtrack is really decoded. Lower it only if +# you have a genuine reason to cap the CPU. +max_concurrent = 100 diff --git a/crates/vuio-core/src/config/model.rs b/crates/vuio-core/src/config/model.rs index c07817d9..8451b07f 100644 --- a/crates/vuio-core/src/config/model.rs +++ b/crates/vuio-core/src/config/model.rs @@ -63,11 +63,26 @@ pub(super) fn default_true() -> bool { true } -/// Two at once: enough that a second TV starting a film does not get a refusal, -/// low enough that a small box is not asked to run four decoders and serve the -/// library at the same time. +/// High enough not to refuse anything a household actually does. +/// +/// This began as a tuning figure — two, on the reasoning that a small box should +/// not run four decoders while serving the library. What that missed is that a +/// renderer does not open one connection per film. A television opens three +/// within a second of pressing play: one to play on, one to read the end of the +/// file, and one to play on again. Against a ceiling of two, the third is +/// refused, and what the viewer sees is a film that will not start. +/// +/// It also over-charged for the work. The picture is passed through and so is +/// Dolby, so most sessions are a remux costing almost nothing; only a DTS +/// soundtrack is really decoded, at about a hundredth of a core per track. A low +/// ceiling was mostly refusing work that was nearly free. +/// +/// So this is no longer a tuning knob but a guard against something having gone +/// wrong — a renderer looping on a failure, or a crawler. Anyone with a genuine +/// reason to cap the CPU can still set it, and zero is rejected outright by +/// [`crate::config::validation`] because it would refuse every transcode. pub(super) fn default_transcode_max_concurrent() -> usize { - 2 + 100 } pub(super) fn default_web_ui_port() -> u16 { @@ -255,6 +270,10 @@ pub struct TranscodeConfig { #[serde(default)] pub audio_format: TranscodeAudioFormat, /// Ceiling on simultaneous transcode sessions. + /// + /// A guard against a renderer looping on a failure rather than a tuning + /// figure — see [`default_transcode_max_concurrent`] for why it is set where + /// it is, and what a television does that a low ceiling breaks. #[serde(default = "default_transcode_max_concurrent")] pub max_concurrent: usize, } diff --git a/crates/vuio-core/src/config/template.toml b/crates/vuio-core/src/config/template.toml index 93c235b7..fce651a0 100644 --- a/crates/vuio-core/src/config/template.toml +++ b/crates/vuio-core/src/config/template.toml @@ -95,12 +95,15 @@ enabled = true # lpcm uncompressed, seekable, about 1.5 Mbps # aac about a tenth of that, lossy, and cannot be scrubbed audio_format = "lpcm" -# Which version is listed first, for a TV that takes what it is given rather -# than choosing. Switch to "transcoded" only if a TV still plays silently. -prefer = "original" -# Decodes allowed at once. Past this a further request is refused rather than -# queued, so the streams already playing keep up. -max_concurrent = 2 +# What is offered, and in what order. +# enabled the original is listed first, the decoded version beside it +# forced the decoded version leads, and for DTS it is the only one offered +# disabled nothing is decoded +mode = "enabled" +# Sessions allowed at once. Past this a further request is refused rather than +# queued. Set high on purpose: a television opens several connections within a +# second of pressing play, and a low ceiling refuses the one it meant to play on. +max_concurrent = 100 # Platform-specific notes: # PLACEHOLDER_PLATFORM_NOTES diff --git a/crates/vuio-core/src/media/remux/ac3_config.rs b/crates/vuio-core/src/media/remux/ac3_config.rs new file mode 100644 index 00000000..43ac6436 --- /dev/null +++ b/crates/vuio-core/src/media/remux/ac3_config.rs @@ -0,0 +1,274 @@ +//! The configuration records an MP4 needs to carry AC-3 or E-AC-3 untouched. +//! +//! A television that can decode Dolby Digital should be handed Dolby Digital, +//! not a stereo AAC downmix of it: passing the track through costs no CPU and +//! keeps the 5.1 that re-encoding would throw away. What stands in the way is +//! that an `mp4a` sample entry cannot describe AC-3 — ISO-BMFF carries these in +//! `ac-3` and `ec-3` entries instead, each holding a small record that restates +//! what the bitstream's own headers already say (ETSI TS 102 366 Annex F). +//! +//! Matroska stores no `CodecPrivate` for either codec, because the syncframe is +//! self-describing. So the record is built here, from the first frame of the +//! track, by reading the same header fields the decoder would. +//! +//! Deliberately free of the vendored decoders and of `transcode-ac3`. Passing a +//! track through requires no decoder, and a build compiled without one should +//! still be able to hand a television the audio it could already play. + +/// Sample rates an `fscod` of 0, 1 or 2 selects (A/52 Table 5.6). +const RATES: [u32; 3] = [48_000, 44_100, 32_000]; + +/// Channels each `acmod` describes, before `lfeon` (A/52 Table 5.8). +const ACMOD_CHANNELS: [u8; 8] = [2, 1, 2, 3, 3, 4, 4, 5]; + +/// What one AC-3 or E-AC-3 track needs stated in its sample entry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ac3Config { + /// The `dac3` or `dec3` box body, ready to wrap. + pub record: Vec, + /// Sample rate the syncframe declares, in Hz. + pub sample_rate: u32, + /// Channels the syncframe declares, the LFE included. + pub channels: u8, +} + +/// Read an AC-3 syncframe and build the `dac3` record describing it. +/// +/// `None` for anything that is not a base AC-3 syncframe, which is the caller's +/// signal to fall back to decoding the track rather than to write a sample entry +/// describing something it has not actually understood. +pub fn parse_ac3(frame: &[u8]) -> Option { + // syncword(16) crc1(16) fscod(2) frmsizecod(6) bsid(5) bsmod(3) acmod(3) + if frame.len() < 7 || frame[0] != 0x0B || frame[1] != 0x77 { + return None; + } + let fscod = frame[4] >> 6; + let frmsizecod = frame[4] & 0x3F; + let bsid = frame[5] >> 3; + let bsmod = frame[5] & 0x07; + let acmod = frame[6] >> 5; + if fscod > 2 || bsid > 8 { + return None; + } + + // The mix-level fields between `acmod` and `lfeon` are present or absent + // depending on `acmod` itself, so the one bit we want moves (A/52 §5.3.2). + let mut bit = 6 * 8 + 3; + if acmod & 0x01 != 0 && acmod != 0x01 { + bit += 2; // cmixlev + } + if acmod & 0x04 != 0 { + bit += 2; // surmixlev + } + if acmod == 0x02 { + bit += 2; // dsurmod + } + let lfeon = read_bit(frame, bit)?; + + // `bit_rate_code` is the upper five bits of `frmsizecod`; the sixth selects + // between the two frame sizes a 44.1 kHz stream alternates between, which + // says nothing about the rate itself. + let bit_rate_code = frmsizecod >> 1; + + let mut record = Vec::with_capacity(3); + let mut writer = BitWriter::default(); + writer.push(u32::from(fscod), 2); + writer.push(u32::from(bsid), 5); + writer.push(u32::from(bsmod), 3); + writer.push(u32::from(acmod), 3); + writer.push(u32::from(lfeon), 1); + writer.push(u32::from(bit_rate_code), 5); + writer.push(0, 5); // reserved + record.extend_from_slice(&writer.finish()); + + Some(Ac3Config { + record, + sample_rate: RATES[fscod as usize], + channels: ACMOD_CHANNELS[acmod as usize] + lfeon, + }) +} + +/// Read an E-AC-3 syncframe and build the `dec3` record describing it. +/// +/// One independent substream is described, which is what all but a handful of +/// files carry. `bsmod` is reported as zero — "complete main" — because Annex E +/// buries it behind a run of variable-length fields that would have to be +/// walked to reach it, and every renderer treats the field as advisory. +pub fn parse_eac3(frame: &[u8]) -> Option { + // syncword(16) strmtyp(2) substreamid(3) frmsiz(11) + // fscod(2) numblkscod(2) acmod(3) lfeon(1) bsid(5) … + if frame.len() < 6 || frame[0] != 0x0B || frame[1] != 0x77 { + return None; + } + let bsid = frame[5] >> 3; + if !(9..=16).contains(&bsid) { + return None; + } + let strmtyp = frame[2] >> 6; + // Type 1 is a dependent substream: it cannot open a track, because what it + // carries are extra channels for an independent one somewhere before it. + if strmtyp == 1 { + return None; + } + let frmsiz = (u32::from(frame[2] & 0x07) << 8) | u32::from(frame[3]); + let frame_bytes = (frmsiz + 1) * 2; + + let fscod = frame[4] >> 6; + let numblkscod = (frame[4] >> 4) & 0x03; + let acmod = (frame[4] >> 1) & 0x07; + let lfeon = frame[4] & 0x01; + + // `fscod == 3` is a half-rate stream: `fscod2` replaces the block count, + // which is then six by definition (§E.2.3.1.4). + let (sample_rate, blocks) = if fscod == 3 { + let fscod2 = numblkscod; + if fscod2 > 2 { + return None; + } + (RATES[fscod2 as usize] / 2, 6u32) + } else { + (RATES[fscod as usize], [1u32, 2, 3, 6][numblkscod as usize]) + }; + + // `data_rate` is stated in kbit/s, and a syncframe says everything needed to + // work it out: this many bytes covering this many blocks of 256 samples. + let frame_secs = f64::from(blocks * 256) / f64::from(sample_rate); + let data_rate = ((f64::from(frame_bytes) * 8.0 / frame_secs) / 1000.0).round() as u32; + + let mut writer = BitWriter::default(); + writer.push(data_rate.min(0x1FFF), 13); + writer.push(0, 3); // num_ind_sub, as "one substream" less one + writer.push(u32::from(fscod.min(3)), 2); + writer.push(u32::from(bsid), 5); + writer.push(0, 1); // reserved + writer.push(0, 1); // asvc + writer.push(0, 3); // bsmod + writer.push(u32::from(acmod), 3); + writer.push(u32::from(lfeon), 1); + writer.push(0, 3); // reserved + writer.push(0, 4); // num_dep_sub + writer.push(0, 1); // reserved, in place of chan_loc + + Some(Ac3Config { + record: writer.finish(), + sample_rate, + channels: ACMOD_CHANNELS[acmod as usize] + lfeon, + }) +} + +/// One bit of `data`, counted from the first bit of the first byte. +fn read_bit(data: &[u8], bit: usize) -> Option { + let byte = data.get(bit / 8)?; + Some((byte >> (7 - bit % 8)) & 1) +} + +/// Big-endian bit packer, for records whose fields do not fall on byte edges. +#[derive(Default)] +struct BitWriter { + out: Vec, + partial: u8, + used: u32, +} + +impl BitWriter { + fn push(&mut self, value: u32, bits: u32) { + for shift in (0..bits).rev() { + let bit = ((value >> shift) & 1) as u8; + self.partial = (self.partial << 1) | bit; + self.used += 1; + if self.used == 8 { + self.out.push(self.partial); + self.partial = 0; + self.used = 0; + } + } + } + + /// The packed bytes, the last one padded with zeroes to a byte edge. + fn finish(mut self) -> Vec { + if self.used > 0 { + self.out.push(self.partial << (8 - self.used)); + } + self.out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The vendored conformance stream: 48 kHz stereo, no LFE. + #[cfg(feature = "transcode-ac3")] + const AC3: &[u8] = + include_bytes!("../../../../vendor/oxideav-ac3/tests/fixtures/sine440_stereo.ac3"); + + #[cfg(feature = "transcode-ac3")] + #[test] + fn a_real_ac3_frame_yields_a_three_byte_record() { + let config = parse_ac3(AC3).expect("the fixture is a base AC-3 syncframe"); + assert_eq!(config.record.len(), 3, "dac3 is exactly 24 bits"); + assert_eq!(config.sample_rate, 48_000); + assert_eq!(config.channels, 2, "stereo, and the fixture has no LFE"); + + // The record is fscod(2) bsid(5) bsmod(3) acmod(3) lfeon(1) + // bit_rate_code(5) reserved(5), so the first byte is fscod, bsid, and + // the leading bit of bsmod. + assert_eq!(config.record[0] >> 6, 0, "48 kHz is fscod 0"); + let bsid = (config.record[0] >> 1) & 0x1F; + assert_eq!(bsid, 8, "the fixture is bsid 8, plain AC-3"); + let acmod = ((config.record[1] >> 3) & 0x07) as usize; + assert_eq!(ACMOD_CHANNELS[acmod], 2, "acmod {acmod} is not stereo"); + assert_eq!((config.record[1] >> 2) & 1, 0, "no LFE in the fixture"); + } + + #[test] + fn anything_that_is_not_a_syncframe_is_declined() { + assert!(parse_ac3(&[]).is_none()); + assert!(parse_ac3(&[0x0B, 0x77]).is_none(), "too short to read"); + assert!(parse_ac3(&[0xFF; 32]).is_none(), "no syncword"); + assert!(parse_eac3(&[0xFF; 32]).is_none()); + } + + /// A dependent substream carries extra channels for an independent one, so + /// it cannot open a track of its own. + #[test] + fn a_dependent_substream_does_not_describe_a_track() { + let mut frame = [0u8; 16]; + frame[0] = 0x0B; + frame[1] = 0x77; + frame[2] = 0x40; // strmtyp = 1 + frame[5] = 16 << 3; // bsid = 16, in the E-AC-3 range + assert!(parse_eac3(&frame).is_none()); + } + + /// Hand-built E-AC-3 header: 48 kHz, 6 blocks, 3/2 plus LFE. + #[test] + fn an_eac3_header_is_described_as_five_point_one() { + let mut frame = [0u8; 16]; + frame[0] = 0x0B; + frame[1] = 0x77; + // strmtyp = 0, substreamid = 0, frmsiz = 0x2FF → 1536 bytes. + frame[2] = 0x02; + frame[3] = 0xFF; + // fscod = 0 (48 kHz), numblkscod = 3 (6 blocks), acmod = 7, lfeon = 1. + frame[4] = (3 << 4) | (7 << 1) | 1; + frame[5] = 16 << 3; // bsid = 16 + + let config = parse_eac3(&frame).expect("a valid independent substream"); + assert_eq!(config.sample_rate, 48_000); + assert_eq!(config.channels, 6, "3/2 is five channels, plus LFE"); + assert_eq!(config.record.len(), 5, "dec3 for one substream is 5 bytes"); + // data_rate occupies the leading 13 bits: 1536 bytes over 32 ms. + let data_rate = (u32::from(config.record[0]) << 5) | (u32::from(config.record[1]) >> 3); + assert_eq!(data_rate, 384, "1536 bytes per 32 ms is 384 kbit/s"); + } + + #[test] + fn the_bit_writer_packs_big_endian_and_pads_the_tail() { + let mut writer = BitWriter::default(); + writer.push(0b101, 3); + writer.push(0b11, 2); + // 101 then 11, left-packed and zero-padded to a byte. + assert_eq!(writer.finish(), vec![0b1011_1000]); + } +} diff --git a/crates/vuio-core/src/media/remux/annexb.rs b/crates/vuio-core/src/media/remux/annexb.rs new file mode 100644 index 00000000..fcfc787a --- /dev/null +++ b/crates/vuio-core/src/media/remux/annexb.rs @@ -0,0 +1,283 @@ +//! Turning length-prefixed video back into the byte stream a transport carries. +//! +//! The same picture is framed two ways. Inside MP4 and Matroska each NAL unit is +//! preceded by its length, and the parameter sets that describe the whole +//! sequence — SPS and PPS for H.264, plus VPS for HEVC — are held once in the +//! container's own decoder configuration record. A transport stream has no such +//! record and no lengths: NAL units are separated by start codes, and the +//! parameter sets travel in the stream itself. +//! +//! That difference is not a formality. A transport stream is meant to be joined +//! part way through, so a decoder arriving at a random-access point has to find +//! everything it needs *there* rather than in a header it never saw. Which is +//! why the parameter sets are written again before every keyframe here, and why +//! a stream that carried them only once would play from the beginning and show +//! nothing at all to a set that seeked. + +use super::mkv_demuxer::TrackCodec; + +/// The four-byte start code, used at the head of every access unit and before +/// each parameter set. +const START_CODE: [u8; 4] = [0, 0, 0, 1]; + +/// An H.264 access unit delimiter: `primary_pic_type` 7, which says nothing +/// about the slices that follow and is what every muxer writes. +const AVC_DELIMITER: [u8; 6] = [0, 0, 0, 1, 0x09, 0xF0]; + +/// The HEVC one. Same job, and two bytes of NAL header rather than one. +const HEVC_DELIMITER: [u8; 7] = [0, 0, 0, 1, 0x46, 0x01, 0x50]; + +/// The parameter sets a decoder needs before it can decode anything, already in +/// Annex B form and ready to be written before each keyframe. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ParameterSets(Vec); + +impl ParameterSets { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Read them out of a container's decoder configuration record. + /// + /// `None` for a record this does not understand, which the caller should + /// treat as a track it cannot carry rather than one to carry without them. + pub fn parse(codec: TrackCodec, extra_data: &[u8]) -> Option { + match codec { + TrackCodec::Avc => parse_avcc(extra_data), + TrackCodec::Hevc => parse_hvcc(extra_data), + _ => None, + } + } +} + +/// The `AVCDecoderConfigurationRecord`'s SPS and PPS, in stream order. +fn parse_avcc(record: &[u8]) -> Option { + // version(1) profile(1) compat(1) level(1) lengthSizeMinusOne(1) numSPS(1) + if record.len() < 6 { + return None; + } + let mut out = Vec::new(); + let mut at = 5; + for _ in 0..2 { + // SPS then PPS: the counts are stored the same way, five bits for the + // first and a whole byte for the second, and the low five bits are the + // count in both. + let count = record.get(at)? & 0x1F; + at += 1; + for _ in 0..count { + let len = usize::from(u16::from_be_bytes(record.get(at..at + 2)?.try_into().ok()?)); + at += 2; + out.extend_from_slice(&START_CODE); + out.extend_from_slice(record.get(at..at + len)?); + at += len; + } + } + (!out.is_empty()).then_some(ParameterSets(out)) +} + +/// The `HEVCDecoderConfigurationRecord`'s VPS, SPS and PPS. +/// +/// Laid out as a count of arrays, each naming its NAL type and holding however +/// many units of it — so unlike AVC the parameter sets are not at a fixed +/// offset and the arrays have to be walked. +fn parse_hvcc(record: &[u8]) -> Option { + const ARRAY_COUNT_AT: usize = 22; + if record.len() <= ARRAY_COUNT_AT { + return None; + } + let mut out = Vec::new(); + let mut at = ARRAY_COUNT_AT + 1; + for _ in 0..record[ARRAY_COUNT_AT] { + // array_completeness(1) reserved(1) NAL_unit_type(6), then the count. + at += 1; + let count = u16::from_be_bytes(record.get(at..at + 2)?.try_into().ok()?); + at += 2; + for _ in 0..count { + let len = usize::from(u16::from_be_bytes(record.get(at..at + 2)?.try_into().ok()?)); + at += 2; + out.extend_from_slice(&START_CODE); + out.extend_from_slice(record.get(at..at + len)?); + at += len; + } + } + (!out.is_empty()).then_some(ParameterSets(out)) +} + +/// Rewrite one length-prefixed access unit as an Annex B one. +/// +/// Three things go in front of the picture, in this order. +/// +/// An access unit delimiter, first, unless the unit already opens with one. A +/// container has frame boundaries of its own — a Matroska block is one access +/// unit and says so — and a transport stream has none: the boundary is exactly +/// this NAL unit and nothing else. Every muxer in the field inserts one for that +/// reason, ffmpeg included, and a decoder that relies on it shows nothing at all +/// without it. +/// +/// Then `parameter_sets`, when this unit is a random-access point, so that a +/// decoder joining the stream here has them. They are deliberately not written +/// otherwise: repeating them on every frame would cost a few per cent of the +/// bitrate to say something that has not changed. +/// +/// A malformed unit stops the conversion where it went wrong rather than +/// failing: what has been recovered so far is a valid, if short, access unit, +/// and one damaged frame should cost one frame. +pub fn to_annexb( + sample: &[u8], + parameter_sets: &ParameterSets, + keyframe: bool, + codec: TrackCodec, +) -> Vec { + let delimiter: &[u8] = match codec { + TrackCodec::Hevc => &HEVC_DELIMITER, + _ => &AVC_DELIMITER, + }; + let mut out = Vec::with_capacity(sample.len() + parameter_sets.0.len() + 24); + if !opens_with_a_delimiter(sample, codec) { + out.extend_from_slice(delimiter); + } + if keyframe { + out.extend_from_slice(¶meter_sets.0); + } + let mut at = 0; + while at + 4 <= sample.len() { + let len = u32::from_be_bytes(sample[at..at + 4].try_into().unwrap()) as usize; + at += 4; + if len == 0 || at + len > sample.len() { + break; + } + out.extend_from_slice(&START_CODE); + out.extend_from_slice(&sample[at..at + len]); + at += len; + } + out +} + +/// Whether the encoder already wrote a delimiter, in which case writing a second +/// one is not belt and braces but a malformed access unit. +fn opens_with_a_delimiter(sample: &[u8], codec: TrackCodec) -> bool { + // The length prefix, then as much of the NAL header as the codec spends on + // its type: one byte for AVC, two for HEVC. + let Some(header) = sample.get(4..6) else { + return false; + }; + match codec { + TrackCodec::Hevc => (header[0] >> 1) & 0x3F == 35, + _ => header[0] & 0x1F == 9, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A minimal `avcC`: one SPS of three bytes, one PPS of two. + fn avcc() -> Vec { + vec![ + 0x01, 0x64, 0x00, 0x28, 0xFF, // version, profile, compat, level, lengthSize + 0xE1, 0x00, 0x03, 0x67, 0x64, 0x28, // one SPS + 0x01, 0x00, 0x02, 0x68, 0xEE, // one PPS + ] + } + + #[test] + fn the_parameter_sets_come_out_of_the_record_in_stream_order() { + let sets = ParameterSets::parse(TrackCodec::Avc, &avcc()).expect("a valid avcC"); + assert_eq!( + sets.as_bytes(), + &[0, 0, 0, 1, 0x67, 0x64, 0x28, 0, 0, 0, 1, 0x68, 0xEE], + "SPS then PPS, each behind a start code" + ); + } + + #[test] + fn a_record_that_does_not_parse_is_declined_rather_than_half_read() { + assert!(ParameterSets::parse(TrackCodec::Avc, &[]).is_none()); + assert!(ParameterSets::parse(TrackCodec::Avc, &[0x01, 0x64]).is_none()); + // A count that runs off the end of the record. + assert!(ParameterSets::parse(TrackCodec::Avc, &[0x01, 0, 0, 0, 0xFF, 0xE1, 0xFF, 0xFF]).is_none()); + assert!(ParameterSets::parse(TrackCodec::Aac, &avcc()).is_none()); + } + + #[test] + fn lengths_become_start_codes() { + let sets = ParameterSets::default(); + // Two NAL units, of three and two bytes. + let sample = [0, 0, 0, 3, 0x41, 0x9A, 0x02, 0, 0, 0, 2, 0x41, 0x9B]; + assert_eq!( + to_annexb(&sample, &sets, false, TrackCodec::Avc), + vec![ + 0, 0, 0, 1, 0x09, 0xF0, // the delimiter this unit lacked + 0, 0, 0, 1, 0x41, 0x9A, 0x02, // + 0, 0, 0, 1, 0x41, 0x9B + ] + ); + } + + /// A transport stream marks access unit boundaries with a delimiter and + /// nothing else, so every unit has to carry one — and exactly one. + #[test] + fn each_access_unit_opens_with_a_delimiter_and_never_two() { + let sets = ParameterSets::default(); + + // NAL type 9 is the delimiter; an encoder that wrote its own keeps it. + let already = [0, 0, 0, 2, 0x09, 0xF0, 0, 0, 0, 2, 0x41, 0x9B]; + let out = to_annexb(&already, &sets, false, TrackCodec::Avc); + assert_eq!( + out, + vec![0, 0, 0, 1, 0x09, 0xF0, 0, 0, 0, 1, 0x41, 0x9B], + "a second delimiter would be a malformed access unit" + ); + + // HEVC spends two bytes on the NAL header and numbers the delimiter 35. + let hevc = [0, 0, 0, 2, 0x26, 0x01]; + let out = to_annexb(&hevc, &sets, false, TrackCodec::Hevc); + assert_eq!(&out[..7], &HEVC_DELIMITER, "and gets the HEVC spelling of it"); + + let hevc_already = [0, 0, 0, 3, 0x46, 0x01, 0x50]; + let out = to_annexb(&hevc_already, &sets, false, TrackCodec::Hevc); + assert_eq!(out, vec![0, 0, 0, 1, 0x46, 0x01, 0x50]); + } + + /// The whole reason a transport stream can be joined part way through: a + /// decoder arriving at a keyframe finds the parameter sets there. + #[test] + fn a_keyframe_carries_the_parameter_sets_and_other_frames_do_not() { + let sets = ParameterSets::parse(TrackCodec::Avc, &avcc()).unwrap(); + let sample = [0, 0, 0, 2, 0x65, 0x88]; + + let key = to_annexb(&sample, &sets, true, TrackCodec::Avc); + assert!( + key.starts_with(&AVC_DELIMITER), + "the delimiter comes before them, as every muxer writes it" + ); + assert!( + key[AVC_DELIMITER.len()..].starts_with(sets.as_bytes()), + "a keyframe leads with them" + ); + assert_eq!(key.len(), AVC_DELIMITER.len() + sets.as_bytes().len() + 6); + + let inter = to_annexb(&sample, &sets, false, TrackCodec::Avc); + assert_eq!( + inter, + vec![0, 0, 0, 1, 0x09, 0xF0, 0, 0, 0, 1, 0x65, 0x88], + "and nothing else repeats them" + ); + } + + #[test] + fn a_damaged_unit_costs_only_itself() { + let sets = ParameterSets::default(); + // A good unit, then a length reaching past the end of the sample. + let sample = [0, 0, 0, 2, 0x41, 0x9A, 0x00, 0x00, 0xFF, 0xFF, 0x41]; + assert_eq!( + to_annexb(&sample, &sets, false, TrackCodec::Avc), + vec![0, 0, 0, 1, 0x09, 0xF0, 0, 0, 0, 1, 0x41, 0x9A], + "what was recovered is still a valid access unit" + ); + } +} diff --git a/crates/vuio-core/src/media/remux/fmp4_writer.rs b/crates/vuio-core/src/media/remux/fmp4_writer.rs index a60e4989..3d294c10 100644 --- a/crates/vuio-core/src/media/remux/fmp4_writer.rs +++ b/crates/vuio-core/src/media/remux/fmp4_writer.rs @@ -283,6 +283,21 @@ impl Fmp4Writer { Self::wrap_box(&minf) } + /// The `AudioSampleEntry` preamble every audio codec shares, up to the point + /// where its own configuration box begins. + fn audio_sample_entry(fourcc: &[u8; 4], track: &TrackInfo) -> Vec { + let mut sample_entry = Vec::new(); + sample_entry.extend_from_slice(fourcc); + sample_entry.extend_from_slice(&[0; 6]); // reserved + sample_entry.extend_from_slice(&(1u16).to_be_bytes()); // data_reference_index + sample_entry.extend_from_slice(&[0; 8]); // reserved + sample_entry.extend_from_slice(&(track.channels.unwrap_or(2) as u16).to_be_bytes()); + sample_entry.extend_from_slice(&(16u16).to_be_bytes()); // sample_size = 16 + sample_entry.extend_from_slice(&[0; 4]); // pre_defined + reserved + sample_entry.extend_from_slice(&(track.sample_rate.unwrap_or(44100) << 16).to_be_bytes()); + sample_entry + } + fn build_stbl(track: &TrackInfo) -> Vec { let mut stbl_body = Vec::new(); @@ -332,28 +347,32 @@ impl Fmp4Writer { } Self::wrap_box(&sample_entry) } - // Everything else gets an `mp4a` entry. The three decoded codecs are - // here only for exhaustiveness: a track of theirs reaches this writer - // already restated as AAC (see `aac_track`), because what the fragment - // will carry is the re-encoded stream, not the source. - TrackCodec::Aac - | TrackCodec::Ac3 - | TrackCodec::Eac3 - | TrackCodec::Dts - | TrackCodec::Unsupported => { - let mut sample_entry = Vec::new(); - sample_entry.extend_from_slice(b"mp4a"); - sample_entry.extend_from_slice(&[0; 6]); // reserved - sample_entry.extend_from_slice(&(1u16).to_be_bytes()); // data_reference_index - sample_entry.extend_from_slice(&[0; 8]); // reserved - sample_entry - .extend_from_slice(&(track.channels.unwrap_or(2) as u16).to_be_bytes()); - sample_entry.extend_from_slice(&(16u16).to_be_bytes()); // sample_size = 16 - sample_entry.extend_from_slice(&[0; 4]); // pre_defined + reserved - sample_entry.extend_from_slice( - &(track.sample_rate.unwrap_or(44100) << 16).to_be_bytes(), - ); - + // Dolby, passed through. An `mp4a` entry cannot describe AC-3, so + // ISO-BMFF gives these their own sample entries, each carrying the + // record built from the bitstream's first syncframe (see + // `super::ac3_config`). A track reaching here as `Ac3` or `Eac3` is + // one being carried untouched; one that had to be decoded arrives + // restated as `Aac` and takes the branch below. + TrackCodec::Ac3 | TrackCodec::Eac3 => { + let (fourcc, config_box): (&[u8; 4], &[u8; 4]) = + if track.codec_kind == TrackCodec::Eac3 { + (b"ec-3", b"dec3") + } else { + (b"ac-3", b"dac3") + }; + let mut sample_entry = Self::audio_sample_entry(fourcc, track); + let mut config = Vec::with_capacity(4 + track.extra_data.len()); + config.extend_from_slice(config_box); + config.extend_from_slice(&track.extra_data); + sample_entry.extend_from_slice(&Self::wrap_box(&config)); + Self::wrap_box(&sample_entry) + } + // Everything else gets an `mp4a` entry. `Dts` is here only for + // exhaustiveness: a DTS track reaches this writer already restated + // as AAC, because what the fragment will carry is the re-encoded + // stream and not the source. + TrackCodec::Aac | TrackCodec::Dts | TrackCodec::Unsupported => { + let mut sample_entry = Self::audio_sample_entry(b"mp4a", track); if !track.extra_data.is_empty() { sample_entry .extend_from_slice(&Self::wrap_box(&Self::build_esds(&track.extra_data))); diff --git a/crates/vuio-core/src/media/remux/mkv_demuxer.rs b/crates/vuio-core/src/media/remux/mkv_demuxer.rs index 20f048b2..ecfab28b 100644 --- a/crates/vuio-core/src/media/remux/mkv_demuxer.rs +++ b/crates/vuio-core/src/media/remux/mkv_demuxer.rs @@ -70,6 +70,31 @@ impl TrackCodec { Self::Unsupported => false, } } + + /// Whether a progressive MP4 built for a television can carry this track — + /// either as the bitstream it already is, or decoded into one that can be. + /// + /// Wider than [`TrackCodec::is_playable`], which asks the same question on a + /// browser's behalf. A browser's media source cannot take AC-3 under any + /// circumstances, so for the HLS path a Dolby track is only ever reachable + /// by decoding it. A television is the one device that usually can: it is + /// what Dolby Digital was designed for, and handing it a stereo AAC downmix + /// of a 5.1 track would throw away the surround it was about to play. So + /// AC-3 and E-AC-3 are carried whatever this build can decode — passing a + /// track through needs no decoder at all — and only DTS, which televisions + /// commonly do lack, has to be re-encoded to be heard. + pub fn plays_on_a_television(self) -> bool { + match self { + Self::Avc | Self::Hevc | Self::Aac | Self::Ac3 | Self::Eac3 => true, + #[cfg(feature = "transcode")] + Self::Dts => self + .transcode_codec() + .is_some_and(|codec| codec.is_decodable() && cfg!(feature = "transcode-aac")), + #[cfg(not(feature = "transcode"))] + Self::Dts => false, + Self::Unsupported => false, + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -137,6 +162,16 @@ pub fn browser_audio_tracks(tracks: &[TrackInfo]) -> Vec<&TrackInfo> { .collect() } +/// Audio tracks a progressive MP4 for a television can carry, in container +/// order. Wider than [`browser_audio_tracks`] by exactly the Dolby codecs a +/// television plays for itself — see [`TrackCodec::plays_on_a_television`]. +pub fn television_audio_tracks(tracks: &[TrackInfo]) -> Vec<&TrackInfo> { + tracks + .iter() + .filter(|t| t.track_kind == TrackKind::Audio && t.codec_kind.plays_on_a_television()) + .collect() +} + pub struct MkvDemuxer; impl MkvDemuxer { diff --git a/crates/vuio-core/src/media/remux/mod.rs b/crates/vuio-core/src/media/remux/mod.rs index dbed2668..b228660d 100644 --- a/crates/vuio-core/src/media/remux/mod.rs +++ b/crates/vuio-core/src/media/remux/mod.rs @@ -1,8 +1,17 @@ +pub mod ac3_config; +pub mod annexb; pub mod fmp4_writer; pub mod hls; pub mod mkv_cues; pub mod mkv_demuxer; +pub mod ts_writer; +#[allow(unused_imports)] +pub use ac3_config::{parse_ac3, parse_eac3, Ac3Config}; +pub use annexb::{to_annexb, ParameterSets}; pub use fmp4_writer::*; pub use hls::*; pub use mkv_demuxer::*; +pub use ts_writer::{ + PesTiming, TsMuxer, TsStreamSpec, FIRST_ES_PID, TS_CLOCK_HZ, TS_PACKET_LEN, +}; diff --git a/crates/vuio-core/src/media/remux/ts_writer.rs b/crates/vuio-core/src/media/remux/ts_writer.rs new file mode 100644 index 00000000..b501cb81 --- /dev/null +++ b/crates/vuio-core/src/media/remux/ts_writer.rs @@ -0,0 +1,452 @@ +//! MPEG-2 transport stream muxing, for televisions that seek by byte. +//! +//! Fragmented MP4 is the right answer for a browser and the wrong one for a +//! television. A set that scrubs a file asks for a byte offset, and a byte +//! offset into a stream produced on demand names nothing stable — answer it +//! positionally and the renderer splices two generations of the stream together +//! and decodes the join as noise. +//! +//! Transport stream is the format that does not care. It is a flat run of +//! 188-byte packets, each opening with a sync byte, and the tables describing +//! the programme are repeated forever rather than written once at the front. A +//! decoder handed the middle of one finds the next sync byte, waits for the next +//! PAT and PMT, waits for the next random-access point, and plays. That is what +//! it was designed for — it is a broadcast format, and a viewer turning a +//! television on mid-programme is the ordinary case, not the exceptional one. +//! +//! Which is why every DLNA server transcodes to this and not to MP4, and why +//! seeking a transcoded film works on hardware where the same film in fMP4 will +//! not scrub at all. +//! +//! What this module owns is the packet layer: the tables, the PES wrapping, the +//! continuity counters and the clock. What goes *into* the packets — Annex B +//! conversion, which audio is passed through and which is re-encoded — belongs +//! to [`crate::media::transcode::TsStream`]. + +use super::mkv_demuxer::TrackCodec; + +/// Bytes in a transport packet. Fixed by the standard, and the reason a decoder +/// can find its footing anywhere in the stream. +pub const TS_PACKET_LEN: usize = 188; + +/// Packets carrying nothing, used as padding. +const NULL_PID: u16 = 0x1FFF; +/// The programme association table always lives here. +const PAT_PID: u16 = 0x0000; +/// Where this muxer puts the programme map table. +pub const PMT_PID: u16 = 0x1000; +/// PID of the first elementary stream; later ones follow it. +pub const FIRST_ES_PID: u16 = 0x0100; +/// The one programme this muxer describes. +const PROGRAM_NUMBER: u16 = 1; + +/// Ticks per second of the PTS/DTS clock. +pub const TS_CLOCK_HZ: u64 = 90_000; + +/// One elementary stream, as the tables and the PES headers need it described. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TsStreamSpec { + pub pid: u16, + /// The PMT's `stream_type`. + pub stream_type: u8, + /// The PES `stream_id`: which range of the MPEG-1 stream map this belongs + /// to. Video takes `0xE0`, MPEG audio `0xC0`, and everything carried as + /// private data — which is where AC-3 lives — takes `0xBD`. + pub stream_id: u8, + /// Descriptors for this stream's PMT entry, already encoded. + pub descriptors: Vec, +} + +impl TsStreamSpec { + /// How `codec` is described in a transport stream, or `None` for one that + /// has no place in it. + pub fn for_codec(codec: TrackCodec, pid: u16) -> Option { + // A registration descriptor naming the format is what an ATSC decoder + // looks for to confirm a privately-carried stream is really Dolby; + // `stream_type` alone is enough for most, and both together is what + // every muxer in the field writes. + let registration = |tag: &[u8; 4]| { + let mut out = vec![0x05, 4]; + out.extend_from_slice(tag); + out + }; + let (stream_type, stream_id, descriptors) = match codec { + TrackCodec::Avc => (0x1B, 0xE0, Vec::new()), + TrackCodec::Hevc => (0x24, 0xE0, Vec::new()), + TrackCodec::Aac => (0x0F, 0xC0, Vec::new()), + TrackCodec::Ac3 => (0x81, 0xBD, registration(b"AC-3")), + TrackCodec::Eac3 => (0x87, 0xBD, registration(b"EAC3")), + TrackCodec::Dts | TrackCodec::Unsupported => return None, + }; + Some(Self { + pid, + stream_type, + stream_id, + descriptors, + }) + } +} + +/// When one access unit is decoded and shown, and what a decoder arriving at it +/// should be told. +/// +/// Grouped because every field is a property of the same instant, and a PES +/// writer taking four bare integers is one transposed pair away from telling a +/// decoder to show a frame before it decodes it. +#[derive(Debug, Clone, Copy, Default)] +pub struct PesTiming { + pub pts: u64, + /// Only written when it differs from `pts`, which is the convention and + /// saves five bytes on every audio frame and every unreordered video one. + pub dts: Option, + /// Whether a decoder may start here. + pub random_access: bool, + /// Puts the programme clock in this packet's adaptation field. A decoder + /// needs it regularly to run its own clock against. + pub pcr: Option, +} + +/// Packet-level state: what each PID's continuity counter is up to. +/// +/// A decoder uses that counter to notice dropped packets, so it has to advance +/// by exactly one per packet carrying payload, per PID, and wrap at sixteen. +#[derive(Default)] +pub struct TsMuxer { + continuity: std::collections::HashMap, +} + +impl TsMuxer { + pub fn new() -> Self { + Self::default() + } + + /// The programme association table: one programme, and where its map is. + pub fn pat(&mut self) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&PROGRAM_NUMBER.to_be_bytes()); + body.extend_from_slice(&(0xE000 | PMT_PID).to_be_bytes()); + let section = section(0x00, PROGRAM_NUMBER, &body); + self.table_packet(PAT_PID, §ion) + } + + /// The programme map table: every elementary stream, and which PID carries + /// the clock. + pub fn pmt(&mut self, pcr_pid: u16, streams: &[TsStreamSpec]) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&(0xE000 | pcr_pid).to_be_bytes()); + body.extend_from_slice(&(0xF000u16).to_be_bytes()); // program_info_length = 0 + for stream in streams { + body.push(stream.stream_type); + body.extend_from_slice(&(0xE000 | stream.pid).to_be_bytes()); + body.extend_from_slice(&(0xF000 | stream.descriptors.len() as u16).to_be_bytes()); + body.extend_from_slice(&stream.descriptors); + } + let section = section(0x02, PROGRAM_NUMBER, &body); + self.table_packet(PMT_PID, §ion) + } + + /// Wrap one access unit as a PES packet and split it across TS packets. + pub fn pes( + &mut self, + out: &mut Vec, + spec: &TsStreamSpec, + payload: &[u8], + timing: &PesTiming, + ) { + let &PesTiming { + pts, + dts, + random_access, + pcr, + } = timing; + let dts = dts.filter(|dts| *dts != pts); + let mut header = Vec::with_capacity(19); + header.extend_from_slice(&[0x00, 0x00, 0x01, spec.stream_id]); + + let stamps = if dts.is_some() { 10 } else { 5 }; + // A video PES may be longer than the field can express, and zero is the + // defined escape for "runs until the next one starts". Audio always + // fits, and stating it lets a decoder validate the frame. + let pes_len = 3 + stamps + payload.len(); + let declared = if spec.stream_id == 0xE0 || pes_len > 0xFFFF { + 0 + } else { + pes_len as u16 + }; + header.extend_from_slice(&declared.to_be_bytes()); + // '10', not scrambled, not priority, data-aligned. + header.push(0b1000_0100); + header.push(if dts.is_some() { 0b1100_0000 } else { 0b1000_0000 }); + header.push(stamps as u8); + if let Some(dts) = dts { + header.extend_from_slice(×tamp(0b0011, pts)); + header.extend_from_slice(×tamp(0b0001, dts)); + } else { + header.extend_from_slice(×tamp(0b0010, pts)); + } + + let mut body = header; + body.extend_from_slice(payload); + self.packetize(out, spec.pid, &body, random_access, pcr); + } + + /// A packet carrying nothing, which is how a transport stream is padded. + pub fn null(out: &mut Vec) { + let mut packet = [0xFFu8; TS_PACKET_LEN]; + packet[0] = 0x47; + packet[1] = (NULL_PID >> 8) as u8; + packet[2] = (NULL_PID & 0xFF) as u8; + packet[3] = 0x10; // payload only, continuity is not counted for null PIDs + out.extend_from_slice(&packet); + } + + /// Split `body` across as many packets as it needs. + /// + /// The first carries the payload-unit-start flag, and the last is padded out + /// with an adaptation field rather than being left short — a transport + /// packet is 188 bytes whether or not there is that much to say. + fn packetize( + &mut self, + out: &mut Vec, + pid: u16, + body: &[u8], + random_access: bool, + pcr: Option, + ) { + let mut offset = 0; + let mut first = true; + while offset < body.len() { + let mut adaptation = Vec::new(); + if first && (random_access || pcr.is_some()) { + let mut flags = 0u8; + if random_access { + flags |= 0b0100_0000; + } + if pcr.is_some() { + flags |= 0b0001_0000; + } + adaptation.push(flags); + if let Some(pcr) = pcr { + // 33 bits of 90 kHz base, six reserved bits, then a 9-bit + // 27 MHz extension this muxer leaves at zero. + let base = pcr; + adaptation.push((base >> 25) as u8); + adaptation.push((base >> 17) as u8); + adaptation.push((base >> 9) as u8); + adaptation.push((base >> 1) as u8); + adaptation.push((((base & 1) as u8) << 7) | 0x7E); + adaptation.push(0); + } + } + + // What is left for payload once the header and any adaptation field + // are accounted for. An adaptation field costs its own length byte. + let remaining = body.len() - offset; + let overhead = 4 + if adaptation.is_empty() { + 0 + } else { + 1 + adaptation.len() + }; + let mut payload = (TS_PACKET_LEN - overhead).min(remaining); + // A short tail is padded by growing the adaptation field, which is + // the only place a transport packet has room to waste. + let mut stuffing = TS_PACKET_LEN - overhead - payload; + if stuffing > 0 && adaptation.is_empty() { + // An adaptation field has to exist before it can stuff, and its + // own length byte eats one of the bytes being made up for. + adaptation.push(0); + stuffing = stuffing.saturating_sub(2); + payload = (TS_PACKET_LEN - 4 - 1 - adaptation.len() - stuffing).min(remaining); + } + adaptation.extend(std::iter::repeat_n(0xFFu8, stuffing)); + + let counter = self.continuity.entry(pid).or_insert(0); + let mut packet = Vec::with_capacity(TS_PACKET_LEN); + packet.push(0x47); + packet.push(((u16::from(first) << 6) | (pid >> 8)) as u8); + packet.push((pid & 0xFF) as u8); + let control = if adaptation.is_empty() { 0b01 } else { 0b11 }; + packet.push((control << 4) | (*counter & 0x0F)); + *counter = counter.wrapping_add(1) & 0x0F; + if !adaptation.is_empty() { + packet.push(adaptation.len() as u8); + packet.extend_from_slice(&adaptation); + } + packet.extend_from_slice(&body[offset..offset + payload]); + debug_assert_eq!(packet.len(), TS_PACKET_LEN, "a transport packet is 188 bytes"); + out.extend_from_slice(&packet); + + offset += payload; + first = false; + } + } + + /// One table section, in one packet. Every section this muxer writes is far + /// short of a packet, so none of them ever has to be continued. + fn table_packet(&mut self, pid: u16, section: &[u8]) -> Vec { + let mut packet = Vec::with_capacity(TS_PACKET_LEN); + packet.push(0x47); + packet.push((0x40 | (pid >> 8)) as u8); // payload unit start + packet.push((pid & 0xFF) as u8); + let counter = self.continuity.entry(pid).or_insert(0); + packet.push(0x10 | (*counter & 0x0F)); + *counter = counter.wrapping_add(1) & 0x0F; + packet.push(0); // pointer_field: the section starts immediately + packet.extend_from_slice(section); + packet.resize(TS_PACKET_LEN, 0xFF); + packet + } +} + +/// Wrap a table body in its section header and CRC. +fn section(table_id: u8, id_extension: u16, body: &[u8]) -> Vec { + // table_id_extension(2) + version/current(1) + section/last(2) + body + CRC(4) + let section_length = 5 + body.len() + 4; + let mut out = Vec::with_capacity(3 + section_length); + out.push(table_id); + // syntax indicator set, '0', two reserved bits, then the length. + out.extend_from_slice(&(0xB000 | section_length as u16).to_be_bytes()); + out.extend_from_slice(&id_extension.to_be_bytes()); + out.push(0xC1); // reserved, version 0, current + out.push(0x00); // section_number + out.push(0x00); // last_section_number + out.extend_from_slice(body); + let crc = mpeg_crc32(&out); + out.extend_from_slice(&crc.to_be_bytes()); + out +} + +/// A PTS or DTS, in the five bytes a PES header spends on one. +/// +/// Thirty-three bits, broken into three runs and interleaved with marker bits +/// that are always one — so that a parser scanning for a start code can never +/// mistake a timestamp for the beginning of a packet. +fn timestamp(prefix: u8, value: u64) -> [u8; 5] { + let value = value & 0x1_FFFF_FFFF; + [ + (prefix << 4) | (((value >> 30) & 0x07) as u8) << 1 | 1, + ((value >> 22) & 0xFF) as u8, + ((((value >> 15) & 0x7F) as u8) << 1) | 1, + ((value >> 7) & 0xFF) as u8, + (((value & 0x7F) as u8) << 1) | 1, + ] +} + +/// The CRC every MPEG-2 section ends with: the standard 32-bit polynomial, most +/// significant bit first, starting from all ones and not inverted at the end. +fn mpeg_crc32(data: &[u8]) -> u32 { + let mut crc = 0xFFFF_FFFFu32; + for byte in data { + crc ^= u32::from(*byte) << 24; + for _ in 0..8 { + crc = if crc & 0x8000_0000 != 0 { + (crc << 1) ^ 0x04C1_1DB7 + } else { + crc << 1 + }; + } + } + crc +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The value every reference implementation produces for this input, and the + /// one a television checks each table against before believing it. + #[test] + fn the_section_crc_matches_the_mpeg_polynomial() { + // `0x0376E6E7` over "123456789" is the published check value for + // CRC-32/MPEG-2, which is what pins the polynomial, the all-ones start + // and the absence of any reflection or final inversion. + assert_eq!(mpeg_crc32(b"123456789"), 0x0376_E6E7); + assert_eq!(mpeg_crc32(&[0x00]), 0x4E08_BFB4); + } + + #[test] + fn a_timestamp_carries_its_marker_bits_and_survives_a_round_trip() { + let value = 0x1_2345_6789u64; + let encoded = timestamp(0b0010, value); + assert_eq!(encoded[0] >> 4, 0b0010, "the prefix names which stamp it is"); + for (index, byte) in encoded.iter().enumerate() { + if index % 2 == 0 { + assert_eq!(byte & 1, 1, "byte {index} must end in a marker bit"); + } + } + let decoded = (u64::from(encoded[0] & 0x0E) << 29) + | (u64::from(encoded[1]) << 22) + | (u64::from(encoded[2] & 0xFE) << 14) + | (u64::from(encoded[3]) << 7) + | (u64::from(encoded[4]) >> 1); + assert_eq!(decoded, value); + } + + #[test] + fn every_packet_is_a_hundred_and_eighty_eight_bytes_starting_with_a_sync() { + let mut muxer = TsMuxer::new(); + let spec = TsStreamSpec::for_codec(TrackCodec::Avc, FIRST_ES_PID).unwrap(); + let mut out = muxer.pat(); + out.extend_from_slice(&muxer.pmt(spec.pid, std::slice::from_ref(&spec))); + // A payload deliberately not a multiple of anything, so the tail has to + // be stuffed. + muxer.pes( + &mut out, + &spec, + &vec![0xABu8; 1000], + &PesTiming { + pts: 90_000, + dts: None, + random_access: true, + pcr: Some(90_000), + }, + ); + TsMuxer::null(&mut out); + + assert_eq!(out.len() % TS_PACKET_LEN, 0, "packets must tile the stream"); + for packet in out.chunks(TS_PACKET_LEN) { + assert_eq!(packet[0], 0x47, "every packet opens with a sync byte"); + } + } + + /// A decoder uses this to notice a dropped packet, so it has to advance by + /// exactly one per packet on a PID and wrap at sixteen. + #[test] + fn continuity_counters_advance_once_per_packet_and_wrap() { + let mut muxer = TsMuxer::new(); + let spec = TsStreamSpec::for_codec(TrackCodec::Aac, FIRST_ES_PID).unwrap(); + let mut out = Vec::new(); + for frame in 0..20 { + muxer.pes( + &mut out, + &spec, + &[0u8; 20], + &PesTiming { + pts: 90_000 * frame, + random_access: true, + ..Default::default() + }, + ); + } + let counters: Vec = out + .chunks(TS_PACKET_LEN) + .map(|packet| packet[3] & 0x0F) + .collect(); + assert_eq!(counters.len(), 20, "each frame fits in one packet"); + for (index, counter) in counters.iter().enumerate() { + assert_eq!(*counter, (index % 16) as u8, "at packet {index}"); + } + } + + #[test] + fn dolby_is_carried_as_private_data_with_a_registration_descriptor() { + let ac3 = TsStreamSpec::for_codec(TrackCodec::Ac3, FIRST_ES_PID).unwrap(); + assert_eq!(ac3.stream_type, 0x81); + assert_eq!(ac3.stream_id, 0xBD, "AC-3 rides in private_stream_1"); + assert_eq!(ac3.descriptors, vec![0x05, 4, b'A', b'C', b'-', b'3']); + let eac3 = TsStreamSpec::for_codec(TrackCodec::Eac3, FIRST_ES_PID).unwrap(); + assert_eq!(eac3.stream_type, 0x87); + // DTS has no place here: it reaches this muxer re-encoded as AAC. + assert!(TsStreamSpec::for_codec(TrackCodec::Dts, FIRST_ES_PID).is_none()); + } +} diff --git a/crates/vuio-core/src/media/transcode/aac.rs b/crates/vuio-core/src/media/transcode/aac.rs index 392091a4..5c47a751 100644 --- a/crates/vuio-core/src/media/transcode/aac.rs +++ b/crates/vuio-core/src/media/transcode/aac.rs @@ -23,6 +23,21 @@ pub struct AacEncoder { } impl AacEncoder { + /// What an encoder for `channels` will run at, in bits per second. + /// + /// Public because the transport-stream handler commits to a length before + /// any of this audio exists, and the re-encoded soundtracks are the part of + /// that length it knows exactly rather than estimates. + pub fn bitrate_for(channels: u16) -> u32 { + match channels { + 1 => 96_000, + 2 => 192_000, + 6 => 384_000, + 8 => 512_000, + _ => 96_000 * u32::from(channels), + } + } + /// Build an encoder producing `channels` at `sample_rate`. /// /// The bitrate is 64 kbps per channel — the conventional AAC-LC "good quality" @@ -32,13 +47,7 @@ impl AacEncoder { anyhow::bail!("unsupported channel count for AAC: {channels}"); } - let bitrate = match channels { - 1 => 96_000, - 2 => 192_000, - 6 => 384_000, - 8 => 512_000, - _ => 96_000 * u32::from(channels), - }; + let bitrate = Self::bitrate_for(channels); let config = xaac_rs::EncoderConfig { profile: xaac_rs::Profile::AacLc, diff --git a/crates/vuio-core/src/media/transcode/frames.rs b/crates/vuio-core/src/media/transcode/frames.rs index c81b80e1..d987463e 100644 --- a/crates/vuio-core/src/media/transcode/frames.rs +++ b/crates/vuio-core/src/media/transcode/frames.rs @@ -398,3 +398,4 @@ mod tests { assert!(FrameIndex::build(TranscodeCodec::Ac3, &mut &junk[..]).is_err()); } } + diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index 0ad42aaa..cd522d89 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -23,6 +23,8 @@ mod plan; #[cfg(all(feature = "transcode-aac", feature = "demux"))] mod rendition; #[cfg(all(feature = "transcode-aac", feature = "casting"))] +mod ts; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] mod video; mod session; mod source; @@ -47,12 +49,38 @@ pub use rendition::{ }; pub use session::{IndexKey, SegmentKey, TranscodeState}; #[cfg(all(feature = "transcode-aac", feature = "casting"))] +#[allow(unused_imports)] +pub use ts::{ + audio_disposition, measure_track_rates, promised_ts_length, AudioDisposition, TrackRate, + TrackRates, TsStream, +}; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] pub use video::ProgressiveStream; #[allow(unused_imports)] pub use source::{PacketSource, PcmStream}; #[allow(unused_imports)] pub use wav::{wav_header, WAV_HEADER_LEN}; +/// The instant to ask the demuxer for, given the instant that was requested. +/// +/// A hair earlier. A coarse seek lands at or just *past* the point it is given, +/// and the caller then has to wait for a random-access point — so asking for the +/// exact time of a keyframe arrives immediately after it and costs a whole group +/// of pictures, which on a film with five-second groups is five seconds later +/// than the viewer dragged to. Backing off by less than a frame lands on that +/// keyframe instead. +/// +/// It does not make the seek exact. Between keyframes there is nothing to land +/// on, so a request that falls mid-group still opens at the next one; what this +/// removes is the whole group lost when the request was already on the mark. +#[cfg(feature = "casting")] +pub(crate) fn seek_target(requested_secs: f64) -> f64 { + /// Shorter than a frame at any rate a film is shot at, and longer than the + /// rounding a container's own timestamps carry. + const BACK_OFF: f64 = 0.005; + (requested_secs - BACK_OFF).max(0.0) +} + /// An audio codec VuIO can decode but many renderers cannot play. /// /// Deliberately not "every codec symphonia knows": this is the set that is both diff --git a/crates/vuio-core/src/media/transcode/session.rs b/crates/vuio-core/src/media/transcode/session.rs index 5c0d6208..f64cd350 100644 --- a/crates/vuio-core/src/media/transcode/session.rs +++ b/crates/vuio-core/src/media/transcode/session.rs @@ -15,6 +15,8 @@ use std::sync::Arc; use tokio::sync::{Mutex, Semaphore}; use super::AudioPlan; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] +use super::TrackRates; /// How many indexes to keep. A two-hour AC-3 track indexes to roughly 3 MB, so /// this is single-digit megabytes for a household's worth of open streams. @@ -57,14 +59,34 @@ pub struct SegmentKey { const MAX_CACHED_SEGMENTS: usize = 24; const MAX_CACHED_SEGMENT_BYTES: usize = 48 * 1024 * 1024; +/// How many films' soundtrack measurements to keep. +/// +/// Two numbers per soundtrack, so this is bytes rather than megabytes and the +/// only reason there is a ceiling at all is that a library is not a session. +const MAX_CACHED_RATES: usize = 64; + /// Shared transcoding state, held by `AppState`. #[derive(Debug)] pub struct TranscodeState { cache: Mutex, segments: Mutex, + rates: Mutex, permits: Arc, } +/// What each film's tracks were measured to cost. +/// +/// Worth caching for the same reason the index is: a renderer opens a film with +/// a `HEAD`, then a `GET`, then a range request per scrub, and every one of them +/// has to state the same promised length or the byte offsets stop meaning the +/// same instants. Measuring once is both cheaper and the only way the answer is +/// guaranteed to be identical each time. +#[derive(Debug, Default)] +struct RateCache { + entries: HashMap>, + order: Vec, +} + #[derive(Debug, Default)] struct SegmentCache { entries: HashMap, @@ -97,6 +119,7 @@ impl TranscodeState { Self { cache: Mutex::new(Cache::default()), segments: Mutex::new(SegmentCache::default()), + rates: Mutex::new(RateCache::default()), permits: Arc::new(Semaphore::new(max_concurrent.max(1))), } } @@ -127,6 +150,25 @@ impl TranscodeState { } } + /// What `key`'s soundtracks were measured to cost, if they have been. + #[cfg(all(feature = "transcode-aac", feature = "casting"))] + pub async fn cached_rates(&self, key: &IndexKey) -> Option> { + self.rates.lock().await.entries.get(key).cloned() + } + + /// Remember one film's measurement, evicting the oldest if full. + #[cfg(all(feature = "transcode-aac", feature = "casting"))] + pub async fn remember_rates(&self, key: IndexKey, rates: Arc) { + let mut cache = self.rates.lock().await; + if cache.entries.insert(key, rates).is_none() { + cache.order.push(key); + while cache.order.len() > MAX_CACHED_RATES { + let oldest = cache.order.remove(0); + cache.entries.remove(&oldest); + } + } + } + /// The bytes of segment `key`, if it was built recently. pub async fn cached_segment(&self, key: &SegmentKey) -> Option { self.segments.lock().await.entries.get(key).cloned() diff --git a/crates/vuio-core/src/media/transcode/ts.rs b/crates/vuio-core/src/media/transcode/ts.rs new file mode 100644 index 00000000..8a03682e --- /dev/null +++ b/crates/vuio-core/src/media/transcode/ts.rs @@ -0,0 +1,1178 @@ +//! A film remuxed into a transport stream, for a television that seeks by byte. +//! +//! The same work as [`super::ProgressiveStream`] — picture copied through, DTS +//! decoded and re-encoded, Dolby passed through — poured into a different +//! container, and the container is the whole point. A fragmented MP4 has one +//! header at the front describing everything that follows, so a renderer that +//! jumps to a byte offset lands in the middle of a structure it has no way to +//! interpret. A transport stream has no front: the tables are repeated +//! throughout, every packet begins with a sync byte, and the parameter sets a +//! decoder needs travel beside each keyframe. Land anywhere and it recovers. +//! +//! That is what makes a scrub bar work on hardware that scrubs by byte, which is +//! most televisions. See [`crate::media::remux::ts_writer`] for the packet layer. +//! +//! ## Why this batches +//! +//! Matroska stores presentation timestamps in decode order and no decode +//! timestamps at all; a transport stream needs both, because a decoder has to be +//! told when to decode a frame it will not display yet. Recovering the decode +//! timeline needs a run of frames to sort, not a single one — so packets are +//! gathered a second or so at a time, the timeline is worked out over the batch, +//! and the batch is then written out interleaved by decode time. Which is also +//! what a transport stream wants: audio and video arriving together, in the +//! order a decoder will want them, rather than in track-sized runs. + +use anyhow::Result; +use std::path::Path; + +use super::{AacEncoder, PcmDecoder, TranscodeCodec}; +use crate::media::remux::{ + derive_decode_timestamps, packet_is_keyframe, rescale_ticks, to_annexb, ParameterSets, + PesTiming, TrackCodec, TrackInfo, TsMuxer, TsStreamSpec, FIRST_ES_PID, TS_CLOCK_HZ, + TS_PACKET_LEN, +}; + +/// Seconds of film gathered before a batch is written. +/// +/// Long enough that the decode timeline can be recovered across any reordering a +/// real encoder produces, short enough that a renderer starts promptly and a +/// dropped connection wastes little. +const BATCH_SECS: f64 = 1.0; + +/// Channels the re-encoded audio track carries. +const DECODED_CHANNELS: u16 = 2; + +/// Packets to hear from before placing a re-encoded run on the timeline. +/// See [`super::run_anchor`] for what the spread is and why the first +/// timestamp alone will not do. +const ANCHOR_PACKETS: usize = 96; + +/// How far the presentation clock runs ahead of the programme clock. +/// +/// A decoder starts its own clock from the PCR it is given and shows each +/// picture when that clock reaches the picture's PTS. Write the two equal and +/// every frame is due the instant it arrives, so the decoder has no buffer at +/// all and the first jitter starves it — which on a television is a film that +/// stutters, or one that never starts. Every muxer in the field leaves a gap +/// here; this is ffmpeg's, near enough, and it is what the renderer spends +/// filling its buffer before the first frame is due. +const CLOCK_HEADROOM: u64 = TS_CLOCK_HZ / 2; + +/// How often the programme tables are repeated, in output clock ticks. +/// +/// A decoder that joins the stream anywhere can interpret nothing until it has +/// seen a PAT and a PMT, so the wait for the next pair is the floor on how long +/// a seek takes to produce a picture. A tenth of a second is what the broadcast +/// profiles require and costs two packets to honour. +const TABLE_INTERVAL: u64 = TS_CLOCK_HZ / 10; + +/// Packets to read while looking for the first frame of every soundtrack. +/// +/// Bounded twice over, because the two things that go wrong are different: a +/// film whose tracks are interleaved normally shows all of them within a +/// fraction of a second, and one that never shows a track at all should cost +/// something small and bounded to give up on. The byte bound is the one that +/// matters on a film with a large picture, where a thousand packets could be a +/// hundred megabytes. +const PRIME_PACKETS: usize = 1024; +const PRIME_BYTES: usize = 8 * 1024 * 1024; + +/// A packet read during priming, waiting to be replayed. +type PrimedPacket = (u32, i64, Vec); + +/// What becomes of one soundtrack on its way into a transport stream. +/// +/// Asked in two places that must agree — [`TsStream::open`], which carries the +/// track, and the handler that commits to a length before the muxer has +/// produced a byte — so it is answered once, here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AudioDisposition { + /// Carried exactly as it arrived. A television decodes Dolby and AAC for + /// itself, and copying costs nothing and keeps the 5.1 a stereo re-encode + /// would throw away. + Passthrough, + /// Decoded and re-encoded as stereo AAC, which is the whole point of this + /// path: a set with no DTS licence plays the picture and nothing else. + Reencoded, + /// Neither possible. Better a film with two working soundtracks than one + /// with a third that plays noise. + Dropped, +} + +/// How `track` reaches the output, and what it is described as when it gets +/// there. +pub fn audio_disposition(track: &TrackInfo) -> AudioDisposition { + match track.codec_kind { + TrackCodec::Aac | TrackCodec::Ac3 | TrackCodec::Eac3 => AudioDisposition::Passthrough, + other => match other.transcode_codec() { + Some(codec) if codec.is_decodable() && cfg!(feature = "transcode-aac") => { + AudioDisposition::Reencoded + } + _ => AudioDisposition::Dropped, + }, + } +} + +/// What one track costs the film it is in, and how often it costs it. +/// +/// Measured from the file rather than assumed from the codec, because the +/// assumption is the thing that goes wrong. AC-3 runs anywhere from 192 to 640 +/// kilobits, DTS from 754 to 1509 for its core and several times that with the +/// lossless extension on top — so a table keyed on codec and channel count is +/// out by a factor of three on real films, in whichever direction happens to be +/// wrong for the film in front of it. +/// +/// The frame rate is here for a less obvious reason. A transport stream charges +/// by the packet, and the last packet of every frame is padded out to 188 bytes +/// whatever is left over — so what a track costs to carry depends on how large +/// its frames are, not only on how many bits a second they add up to. On 768-byte +/// AC-3 frames that padding is a fifth of the track. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TrackRate { + pub id: u32, + /// Bits a second this track occupies in the source. + pub bits_per_second: u64, + /// Access units a second. + pub frames_per_second: f64, +} + +/// Every track of one film, measured. +/// +/// What makes measuring cheap is that the soundtracks these films carry are all +/// constant bitrate, and frame rates do not change. Half a second of a film says +/// what two hours of it will cost. +#[derive(Debug, Default, Clone)] +pub struct TrackRates(Vec); + +impl TrackRates { + /// What track `id` was measured at, if it was reached. + pub fn get(&self, id: u32) -> Option { + self.0.iter().find(|rate| rate.id == id).copied() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Seconds of each track to hear before its rate is settled. +/// +/// A DTS frame is eleven milliseconds and an AC-3 frame thirty-two, so half a +/// second is fifteen frames at worst — far more than a constant bitrate needs +/// to reveal itself, and short enough that the read stays inside the film's +/// first couple of megabytes. +const MEASURE_SECS: f64 = 0.5; +const MEASURE_FRAMES: usize = 8; +const MEASURE_PACKETS: usize = 4096; +const MEASURE_BYTES: usize = 12 * 1024 * 1024; + +/// Measure what each of `tracks` costs the source. +/// +/// Reads from the head of the film, which for constant-bitrate audio and a +/// fixed frame rate is representative of all of it, and gives up on whatever has +/// not appeared within a bounded read — a track measured as nothing falls back +/// to its codec's nominal shape at the call site. +pub fn measure_track_rates(path: &Path, tracks: &[TrackInfo]) -> Result { + /// One track's accumulating account of itself. + #[derive(Default)] + struct Meter { + first: Option, + last: i64, + /// Bytes of the frames wholly inside `[first, last)`. + bytes: u64, + /// The most recent frame's, which has no span behind it yet. + carry: u64, + frames: usize, + } + + if tracks.is_empty() { + return Ok(TrackRates::default()); + } + let mut format = super::source::open_format(path)?; + let mut meters: std::collections::HashMap = tracks + .iter() + .map(|track| (track.id, Meter::default())) + .collect(); + let bases: std::collections::HashMap> = tracks + .iter() + .map(|track| (track.id, track_time_base(format.as_ref(), track.id))) + .collect(); + + let span = |id: &u32, meter: &Meter| -> Option { + let base = (*bases.get(id)?)?; + let at = |ticks: i64| { + base.calc_time(symphonia::core::units::Timestamp::new(ticks)) + .map(|time| time.as_secs_f64()) + }; + Some(at(meter.last)? - at(meter.first?)?) + }; + + let mut read = 0usize; + for _ in 0..MEASURE_PACKETS { + if read >= MEASURE_BYTES { + break; + } + let Ok(Some(packet)) = format.next_packet() else { + break; + }; + let (id, pts, len) = (packet.track_id, packet.pts.get(), packet.data.len()); + read += len; + let Some(meter) = meters.get_mut(&id) else { + continue; + }; + meter.bytes += meter.carry; + meter.carry = len as u64; + meter.first.get_or_insert(pts); + meter.last = pts; + meter.frames += 1; + + // Every track heard from for long enough: nothing further to learn. + if meters.iter().all(|(id, meter)| { + meter.frames >= MEASURE_FRAMES + && span(id, meter).is_some_and(|span| span >= MEASURE_SECS) + }) { + break; + } + } + + let mut rates: Vec = meters + .iter() + .filter_map(|(id, meter)| { + let span = span(id, meter)?; + // Too little of it heard to say anything, which is not an error: the + // caller has a nominal shape to fall back on. + if meter.frames < MEASURE_FRAMES || span <= 0.0 { + return None; + } + Some(TrackRate { + id: *id, + bits_per_second: (meter.bytes as f64 * 8.0 / span) as u64, + frames_per_second: (meter.frames - 1) as f64 / span, + }) + }) + .collect(); + rates.sort_unstable_by_key(|rate| rate.id); + Ok(TrackRates(rates)) +} + +/// What a track of this codec and shape usually looks like, for one +/// [`measure_track_rates`] could not reach. +/// +/// Every bitrate here is at the low end of what the codec is used at, and that +/// is deliberate rather than sloppy. A soundtrack's rate is subtracted from the +/// file's own to leave the picture's, so guessing one *small* leaves the picture +/// looking large, which leaves the promised length long, which costs padding. +/// Guessing it large does the opposite and cuts off the end of the film. +fn nominal_rate(track: &TrackInfo) -> TrackRate { + use crate::media::remux::TrackKind; + + let sample_rate = f64::from(track.sample_rate.unwrap_or(48_000)); + let surround = track.channels.unwrap_or(2) >= 6; + let (bits, frames) = match track.codec_kind { + TrackCodec::Ac3 => (if surround { 384_000 } else { 192_000 }, sample_rate / 1536.0), + TrackCodec::Eac3 => (if surround { 384_000 } else { 128_000 }, sample_rate / 1536.0), + TrackCodec::Dts => (if surround { 768_000 } else { 384_000 }, sample_rate / 512.0), + TrackCodec::Aac => ( + 64_000 * u64::from(track.channels.unwrap_or(2)), + sample_rate / 1024.0, + ), + // Video, and anything the demuxer would not name — including TrueHD, + // which is not carried. Claiming no bitrate for an unnamed soundtrack + // leaves its bytes attributed to the picture, which is the safe + // direction. + _ => ( + 0, + if track.track_kind == TrackKind::Video { + 24.0 + } else { + sample_rate / 1536.0 + }, + ), + }; + TrackRate { + id: track.id, + bits_per_second: bits, + frames_per_second: frames, + } +} + +/// What carrying `bits` a second in frames of `fps` costs in a transport stream. +/// +/// Not a percentage. A transport stream is a run of 188-byte packets and the +/// last one of every frame is stuffed out to 188 whatever is left over, so the +/// cost is a step function of the frame size: two per cent on a 130-kilobyte +/// picture, twenty-two on a 768-byte AC-3 frame. A flat multiplier that is right +/// for one is badly wrong for the other, and being wrong low here is the film's +/// last minutes cut off. +fn transport_cost(bits: u64, fps: f64) -> u64 { + /// A PES header carrying both timestamps, which is what this muxer writes. + const PES_HEADER: f64 = 19.0; + /// Payload in a packet: 188 less the four-byte header, less a two-byte + /// adaptation field for the flags the first packet of a frame carries. + const PAYLOAD: f64 = 182.0; + + // `is_finite` first, so a track measured as NaN falls back rather than + // multiplying its way into the promise. + if bits == 0 || !fps.is_finite() || fps <= 0.0 { + return bits; + } + let per_frame = bits as f64 / (8.0 * fps) + PES_HEADER; + let packets = (per_frame / PAYLOAD).ceil(); + (packets * TS_PACKET_LEN as f64 * 8.0 * fps) as u64 +} + +/// The bits a second the transport stream itself will run at. +/// +/// Which is the number the whole seek mechanism rests on. A byte offset into +/// this resource is read as a fraction of its promised length, so the promise +/// being an honest account of what the stream weighs is what makes an offset +/// mean the moment the viewer dragged to. Promise three times the truth — which +/// assuming the output weighs what the source did does, on a film carrying five +/// DTS soundtracks that leave as stereo AAC — and every byte offset names a +/// moment three times too far along. +/// +/// `tracks` is every track the file has, because what the picture costs is what +/// is left of the file once its soundtracks are accounted for. `carried` is the +/// subset this response will actually write. +fn stream_bitrate( + source_size: u64, + duration_secs: f64, + tracks: &[TrackInfo], + carried: &[TrackInfo], + rates: &TrackRates, +) -> u64 { + use crate::media::remux::TrackKind; + + /// The programme tables, at [`TABLE_INTERVAL`]: two packets, ten times a + /// second. + const TABLE_BITS: u64 = 20 * TS_PACKET_LEN as u64 * 8; + + let rate_of = |track: &TrackInfo| rates.get(track.id).unwrap_or_else(|| nominal_rate(track)); + + let source_bits = (source_size as f64 * 8.0 / duration_secs) as u64; + let source_audio: u64 = tracks + .iter() + .filter(|track| track.track_kind == TrackKind::Audio) + .map(|track| rate_of(track).bits_per_second) + .sum(); + // A film whose soundtracks appear to outweigh it has been measured badly, + // or is mostly soundtrack. Either way the picture is not nothing. + let video_bits = source_bits + .saturating_sub(source_audio) + .max(source_bits / 20); + let video_fps = tracks + .iter() + .find(|track| track.track_kind == TrackKind::Video) + .map(|track| rate_of(track).frames_per_second) + .unwrap_or(24.0); + + let carried_bits: u64 = carried + .iter() + .map(|track| { + let rate = rate_of(track); + match audio_disposition(track) { + AudioDisposition::Passthrough => { + transport_cost(rate.bits_per_second, rate.frames_per_second) + } + AudioDisposition::Reencoded => { + let sample_rate = f64::from(track.sample_rate.unwrap_or(48_000)); + transport_cost( + u64::from(AacEncoder::bitrate_for(DECODED_CHANNELS)), + sample_rate / f64::from(super::AAC_FRAME_SAMPLES as u32), + ) + } + AudioDisposition::Dropped => 0, + } + }) + .sum(); + + transport_cost(video_bits, video_fps) + carried_bits + TABLE_BITS +} + +/// The length a transport stream of this film commits to, in bytes. +/// +/// Leaning long, and the lean is the only part that is not an estimate. The +/// response is made exactly this length whatever the muxer produces: short of it +/// is padding a renderer skips, over it is the film's last seconds cut off. So +/// the margin buys the second outcome off with a little of the first. +pub fn promised_ts_length( + source_size: u64, + duration_secs: f64, + tracks: &[TrackInfo], + carried: &[TrackInfo], + rates: &TrackRates, +) -> u64 { + /// Slack over the estimate. Small, because with the tracks measured and the + /// packet cost counted rather than guessed the estimate lands within a few + /// per cent, and every point of it is bytes a renderer fetches and throws + /// away. It covers what is left: the parameter sets beside every keyframe, + /// and a picture whose average bitrate the file's own size understates. + const MARGIN: f64 = 1.10; + /// Nothing shorter, so a very short film is not trimmed by the tables. + const FLOOR: u64 = 1 << 18; + + let bits = stream_bitrate(source_size, duration_secs, tracks, carried, rates); + let bytes = (bits as f64 * duration_secs * MARGIN / 8.0) as u64; + let aligned = (bytes.max(FLOOR) / TS_PACKET_LEN as u64) * TS_PACKET_LEN as u64; + aligned.max(TS_PACKET_LEN as u64) +} + +/// One track being carried, and what has to happen to its packets. +struct Stream { + spec: TsStreamSpec, + source_id: u32, + time_base: Option, + /// `None` for a track passed through as it stands. + codec: Option, + /// Set only for video: what to write before each keyframe. + parameter_sets: Option, + /// The rate a re-encoded track runs at, from the container's declaration. + sample_rate: u32, + decode: Option, + /// Access units waiting for the batch to be written. + pending: Vec, +} + +/// The state of one track's decode-and-re-encode chain. +struct Decode { + codec: TranscodeCodec, + decoder: PcmDecoder, + encoder: AacEncoder, + decoded_channels: u16, + sample_rate: u32, + /// Where the re-encoded run sits, in samples. `None` until enough packets + /// have been seen to say. + next_pts: Option, + anchors: Vec, + decoded: u64, + /// Frames encoded before the run could be placed. + held: Vec>, +} + +/// One access unit, ready to become a PES packet once its decode time is known. +struct Unit { + pts: u64, + dts: u64, + keyframe: bool, + data: Vec, +} + +/// A film being rewritten as a transport stream. +pub struct TsStream { + format: Box, + muxer: TsMuxer, + /// Video first, then every soundtrack in the order the caller gave them. + streams: Vec, + specs: Vec, + video_pid: u16, + video_id: u32, + video_codec: TrackCodec, + /// Packets read while priming the decoders, replayed ahead of anything + /// further from the demuxer. See [`prime_decoders`]. + primed: std::collections::VecDeque, + /// Presentation time of the batch's first picture, in the output clock. + batch_start: Option, + started: bool, + finished: bool, +} + +impl TsStream { + /// Open `path` positioned at `start_secs`, ready to emit packets. + /// + /// A track this build can neither pass through nor produce is dropped rather + /// than carried, exactly as in the MP4 path: better a film with two working + /// soundtracks than one with a third that plays noise. + pub fn open( + path: &Path, + video: &TrackInfo, + audio: &[TrackInfo], + start_secs: f64, + ) -> Result { + use symphonia::core::formats::{SeekMode, SeekTo}; + use symphonia::core::units::Time; + + let mut format = super::source::open_format(path)?; + if start_secs > 0.0 { + // Coarse, not Accurate: a stream has to open on a random-access + // point or the renderer has no reference frame to decode the first + // picture against. + let _ = format.seek( + SeekMode::Coarse, + SeekTo::Time { + time: Time::try_from_secs_f64(super::seek_target(start_secs)) + .unwrap_or(Time::ZERO), + track_id: Some(video.id), + }, + ); + } + + let parameter_sets = ParameterSets::parse(video.codec_kind, &video.extra_data) + .ok_or_else(|| anyhow::anyhow!("no parameter sets for the video track"))?; + let mut next_pid = FIRST_ES_PID; + let mut streams = Vec::new(); + streams.push(Stream { + spec: TsStreamSpec::for_codec(video.codec_kind, next_pid) + .ok_or_else(|| anyhow::anyhow!("{} has no transport mapping", video.codec))?, + source_id: video.id, + time_base: track_time_base(format.as_ref(), video.id), + codec: None, + parameter_sets: Some(parameter_sets), + sample_rate: 0, + decode: None, + pending: Vec::new(), + }); + + for track in audio { + next_pid += 1; + // Dolby and AAC ride as they are; anything else has to become AAC + // first, and is described as AAC from here on. + let (carried, codec) = match audio_disposition(track) { + AudioDisposition::Passthrough => (track.codec_kind, None), + AudioDisposition::Reencoded => { + (TrackCodec::Aac, track.codec_kind.transcode_codec()) + } + AudioDisposition::Dropped => continue, + }; + let Some(spec) = TsStreamSpec::for_codec(carried, next_pid) else { + continue; + }; + streams.push(Stream { + spec, + source_id: track.id, + time_base: track_time_base(format.as_ref(), track.id), + codec, + parameter_sets: None, + sample_rate: track.sample_rate.unwrap_or(48_000), + decode: None, + pending: Vec::new(), + }); + } + + // Everything the programme map is about to promise, proved before it + // promises it. + let primed = prime_decoders(format.as_mut(), &mut streams); + + let specs = streams.iter().map(|s| s.spec.clone()).collect(); + Ok(Self { + format, + muxer: TsMuxer::new(), + video_pid: streams[0].spec.pid, + video_id: video.id, + video_codec: if streams[0].spec.stream_type == 0x24 { + TrackCodec::Hevc + } else { + TrackCodec::Avc + }, + streams, + specs, + primed, + batch_start: None, + started: false, + finished: false, + }) + } + + /// The next run of transport packets, or `None` at the end of the film. + pub fn next_chunk(&mut self) -> Option> { + if self.finished { + return None; + } + let batch_ticks = (BATCH_SECS * TS_CLOCK_HZ as f64) as u64; + + loop { + let Some((id, pts, data)) = self.next_source_packet() else { + self.finished = true; + self.flush_encoders(); + return self.emit(); + }; + + if id == self.video_id { + let keyframe = packet_is_keyframe(&data, self.video_codec); + // Nothing before the first random-access point is decodable. + if !self.started && !keyframe { + continue; + } + self.started = true; + let ticks = self.rescale(0, pts); + let elapsed = ticks.saturating_sub(*self.batch_start.get_or_insert(ticks)); + // Batches break on keyframes, so every one opens with everything + // a decoder needs to start there. + if keyframe && elapsed >= batch_ticks && !self.streams[0].pending.is_empty() { + let chunk = self.emit(); + self.batch_start = Some(ticks); + self.push_video(ticks, data, keyframe); + if chunk.is_some() { + return chunk; + } + continue; + } + self.push_video(ticks, data, keyframe); + continue; + } + + // Audio ahead of the first picture is dropped: a renderer given + // sound before it has a frame has nothing to synchronise against. + if !self.started { + continue; + } + if let Err(error) = self.take_audio(id, pts, &data) { + tracing::debug!(%error, "dropping an audio packet that would not re-encode"); + } + } + } + + /// The next packet from the film: what priming read, then the demuxer. + fn next_source_packet(&mut self) -> Option { + if let Some(packet) = self.primed.pop_front() { + return Some(packet); + } + match self.format.next_packet() { + Ok(Some(packet)) => Some(( + packet.track_id, + packet.pts.get(), + packet.data.to_vec(), + )), + _ => None, + } + } + + fn rescale(&self, index: usize, ticks: i64) -> u64 { + match self.streams[index].time_base { + Some(time_base) => rescale_ticks(ticks, time_base, TS_CLOCK_HZ as u32), + None => ticks.max(0) as u64, + } + } + + fn push_video(&mut self, ticks: u64, data: Vec, keyframe: bool) { + let sets = self.streams[0] + .parameter_sets + .clone() + .unwrap_or_default(); + let codec = self.video_codec; + self.streams[0].pending.push(Unit { + pts: ticks, + dts: ticks, + keyframe, + data: to_annexb(&data, &sets, keyframe, codec), + }); + } + + /// Feed one audio packet to whichever carried track it belongs to. + fn take_audio(&mut self, track_id: u32, pts: i64, data: &[u8]) -> Result<()> { + let Some(index) = self + .streams + .iter() + .position(|stream| stream.source_id == track_id && stream.spec.pid != self.video_pid) + else { + return Ok(()); + }; + let ticks = self.rescale(index, pts); + let stream = &mut self.streams[index]; + + let Some(codec) = stream.codec else { + // Passed through: the container's frame is the access unit. + stream.pending.push(Unit { + pts: ticks, + dts: ticks, + keyframe: true, + data: data.to_vec(), + }); + return Ok(()); + }; + + let sample_rate = stream.sample_rate; + let decode = match stream.decode.as_mut() { + Some(decode) => decode, + None => { + let (decoder, mut primed) = + PcmDecoder::open(codec, sample_rate, Some(DECODED_CHANNELS), data)?; + let decoded_channels = decoder.channels(); + let encoder = AacEncoder::new(sample_rate, DECODED_CHANNELS)?; + if let Some(samples) = super::frames::frame_samples(codec, data) { + primed.resize(samples as usize * decoded_channels as usize * 2, 0); + } + stream.decode = Some(Decode { + codec, + decoder, + encoder, + decoded_channels, + sample_rate, + next_pts: None, + anchors: Vec::new(), + decoded: 0, + held: Vec::new(), + }); + let decode = stream.decode.as_mut().unwrap(); + take_decoded(&mut stream.pending, decode, ticks, primed)?; + return Ok(()); + } + }; + let expect = super::frames::frame_samples(decode.codec, data); + let pcm = decode.decoder.decode_or_silence(data, expect); + take_decoded(&mut stream.pending, decode, ticks, pcm)?; + Ok(()) + } + + fn flush_encoders(&mut self) { + for stream in &mut self.streams { + if let Some(decode) = stream.decode.as_mut() { + let tail = decode.encoder.finish(); + place_frames(&mut stream.pending, decode, &tail, true); + } + } + } + + /// Write everything held as transport packets, interleaved by decode time. + fn emit(&mut self) -> Option> { + // A batch about to be written cannot wait for more packets before + // deciding where a re-encoded run sits, so this is where it is forced. + for stream in &mut self.streams { + if let Some(decode) = stream.decode.as_mut() { + place_frames(&mut stream.pending, decode, &[], true); + } + } + + // Matroska stores presentation timestamps in decode order and no decode + // timestamps; sorting the batch's presentation times recovers the decode + // timeline exactly, because each frame is decoded once and shown once. + let mut video = std::mem::take(&mut self.streams[0].pending); + let mut times: Vec = video + .iter() + .map(|unit| crate::media::remux::MediaPacket { + track_id: 0, + pts: unit.pts, + dts: unit.dts, + duration: 0, + is_keyframe: unit.keyframe, + data: Vec::new(), + }) + .collect(); + derive_decode_timestamps(&mut times); + for (unit, derived) in video.iter_mut().zip(×) { + unit.dts = derived.dts; + } + + // (decode time, stream index, unit) + let mut ordered: Vec<(u64, usize, Unit)> = + video.drain(..).map(|unit| (unit.dts, 0, unit)).collect(); + for index in 1..self.streams.len() { + for unit in self.streams[index].pending.drain(..) { + ordered.push((unit.dts, index, unit)); + } + } + if ordered.is_empty() { + return None; + } + ordered.sort_by_key(|(dts, index, _)| (*dts, *index)); + + // The tables lead every batch, and are then repeated inside it. + // Repeating them is what lets a decoder that joined part way through + // learn what the programme contains without having seen the beginning + // of it — so the gap between one pair and the next is the floor on how + // long a seek takes to show a picture, and a batch is far too long to + // make a viewer wait. + let mut out = self.muxer.pat(); + out.extend_from_slice(&self.muxer.pmt(self.video_pid, &self.specs)); + let mut tables_at = ordered[0].0; + + for (_, index, unit) in ordered { + let spec = self.streams[index].spec.clone(); + let is_video = index == 0; + if unit.dts.saturating_sub(tables_at) >= TABLE_INTERVAL { + tables_at = unit.dts; + out.extend_from_slice(&self.muxer.pat()); + out.extend_from_slice(&self.muxer.pmt(self.video_pid, &self.specs)); + } + // The programme clock rides on the video track, at every picture — + // a decoder needs it far more often than the hundred milliseconds + // the standard allows between one and the next. It is deliberately + // the *unshifted* decode time: the presentation stamps run + // `CLOCK_HEADROOM` ahead of it, and that gap is the renderer's + // buffer. + self.muxer.pes( + &mut out, + &spec, + &unit.data, + &PesTiming { + pts: unit.pts + CLOCK_HEADROOM, + dts: Some(unit.dts + CLOCK_HEADROOM), + random_access: unit.keyframe, + pcr: is_video.then_some(unit.dts), + }, + ); + } + Some(out) + } +} + +/// Read far enough ahead to prove every soundtrack in `streams` can produce +/// packets, and drop the ones that cannot. +/// +/// A stream declared in the programme map and then silent forever is worse than +/// one that was never declared. A renderer reads the map, sees a PID it is +/// expecting audio on, and waits for it — so a DTS track whose decoder will not +/// open, or a track the container lists but never writes a block for, does not +/// cost that soundtrack. It costs the film. +/// +/// Two ways a track fails that, and both are checked here. It may never appear +/// in the stream at all, which the read below finds; or its decoder may refuse +/// its first frame, which is found by opening one on that frame and throwing it +/// away. The decode chain is then built again, lazily, on the same frame when it +/// is replayed — one frame decoded twice at the start of a film, against a +/// television that would otherwise sit on a black screen. +/// +/// Every packet read on the way is handed back to be replayed rather than +/// dropped: one of them is the keyframe the film has to start on. +fn prime_decoders( + format: &mut dyn symphonia::core::formats::FormatReader, + streams: &mut Vec, +) -> std::collections::VecDeque { + let mut primed = std::collections::VecDeque::new(); + let wanted: Vec = streams[1..].iter().map(|stream| stream.source_id).collect(); + if wanted.is_empty() { + return primed; + } + + let mut first_frames: std::collections::HashMap> = + std::collections::HashMap::new(); + let mut read = 0usize; + for _ in 0..PRIME_PACKETS { + if read >= PRIME_BYTES { + break; + } + let Ok(Some(packet)) = format.next_packet() else { + break; + }; + let (id, pts, data) = (packet.track_id, packet.pts.get(), packet.data.to_vec()); + read += data.len(); + if wanted.contains(&id) { + first_frames.entry(id).or_insert_with(|| data.clone()); + } + primed.push_back((id, pts, data)); + if wanted.iter().all(|id| first_frames.contains_key(id)) { + break; + } + } + + streams.retain(|stream| { + if stream.spec.pid == FIRST_ES_PID { + return true; + } + let Some(frame) = first_frames.get(&stream.source_id) else { + tracing::warn!( + track = stream.source_id, + "dropping a soundtrack that carries no packets, rather than \ + declaring a stream a renderer would wait on forever" + ); + return false; + }; + let Some(codec) = stream.codec else { + return true; + }; + match decode_chain_opens(codec, stream.sample_rate, frame) { + Ok(()) => true, + Err(error) => { + tracing::warn!( + track = stream.source_id, + "dropping a soundtrack whose decoder will not open: {error:#}" + ); + false + } + } + }); + primed +} + +/// Whether both halves of a re-encode can be built for this track. +fn decode_chain_opens(codec: TranscodeCodec, sample_rate: u32, frame: &[u8]) -> Result<()> { + let (decoder, _) = PcmDecoder::open(codec, sample_rate, Some(DECODED_CHANNELS), frame)?; + AacEncoder::new(sample_rate, DECODED_CHANNELS)?; + drop(decoder); + Ok(()) +} + +/// Take one frame's decoded PCM and encode it. +fn take_decoded(pending: &mut Vec, decode: &mut Decode, ticks: u64, pcm: Vec) -> Result<()> { + // Where this run starts, if it is contiguous — asked of every packet, so + // that one rounded container timestamp cannot place the whole run. + let samples = (ticks as i128 * i64::from(decode.sample_rate) as i128 + / TS_CLOCK_HZ as i128) as i64; + decode.anchors.push(samples - decode.decoded as i64); + decode.decoded += (pcm.len() / (decode.decoded_channels as usize * 2)) as u64; + let pcm = super::fit_channels(&pcm, decode.decoded_channels, DECODED_CHANNELS); + let adts = decode.encoder.push(&pcm)?; + place_frames(pending, decode, &adts, false); + Ok(()) +} + +/// Hold the encoder's ADTS frames until the run's place is settled, then queue +/// them as access units. +/// +/// The ADTS headers stay on, unlike the MP4 path which strips them: a transport +/// stream carries AAC exactly as the encoder framed it, because there is no +/// sample entry alongside to repeat what the header says. +fn place_frames(pending: &mut Vec, decode: &mut Decode, adts: &[u8], settle: bool) { + for frame in adts_frames(adts) { + decode.held.push(frame.to_vec()); + } + if decode.next_pts.is_none() { + if !settle && decode.anchors.len() < ANCHOR_PACKETS { + return; + } + if decode.anchors.is_empty() { + return; + } + let mut anchors = std::mem::take(&mut decode.anchors); + // Placed early by the encoder's own delay, which is what a decoder's + // output trails its input by. + let anchor = + super::run_anchor(&mut anchors).saturating_sub(super::ENCODER_DELAY as i64); + decode.next_pts = Some(anchor.max(0) as u64); + } + let mut samples = decode.next_pts.unwrap_or(0); + for frame in decode.held.drain(..) { + let ticks = samples * TS_CLOCK_HZ / u64::from(decode.sample_rate); + pending.push(Unit { + pts: ticks, + dts: ticks, + keyframe: true, + data: frame, + }); + samples += super::AAC_FRAME_SAMPLES; + } + decode.next_pts = Some(samples); +} + +/// Whole ADTS frames, headers included. +fn adts_frames(stream: &[u8]) -> Vec<&[u8]> { + let mut frames = Vec::new(); + let mut pos = 0usize; + while pos + 7 <= stream.len() { + let header = &stream[pos..]; + if header[0] != 0xFF || (header[1] & 0xF0) != 0xF0 { + break; + } + let len = (((u32::from(header[3]) & 0x03) << 11) + | (u32::from(header[4]) << 3) + | (u32::from(header[5]) >> 5)) as usize; + if len < 7 || pos + len > stream.len() { + break; + } + frames.push(&stream[pos..pos + len]); + pos += len; + } + frames +} + +fn track_time_base( + format: &dyn symphonia::core::formats::FormatReader, + track_id: u32, +) -> Option { + format + .tracks() + .iter() + .find(|t| t.id == track_id) + .and_then(|t| t.time_base) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::media::remux::TrackKind; + + fn track(id: u32, kind: TrackKind, codec: TrackCodec, channels: u8) -> TrackInfo { + TrackInfo { + id, + track_kind: kind, + codec: format!("{codec:?}"), + codec_kind: codec, + language: None, + name: None, + sample_rate: Some(48_000), + channels: Some(channels), + width: None, + height: None, + is_default: id == 1, + extra_data: Vec::new(), + } + } + + fn rate(id: u32, bits: u64, fps: f64) -> TrackRate { + TrackRate { + id, + bits_per_second: bits, + frames_per_second: fps, + } + } + + /// The case the whole estimate exists for, and the one no fixture in this + /// repository is big enough to reproduce. + /// + /// A film carrying five DTS soundtracks, which between them are most of what + /// it weighs. Every one of them leaves as stereo AAC at an eighth of the + /// rate, so the stream is a third of the file — and promising the file's own + /// size names every byte offset three times too far along, which is a scrub + /// bar that lands nowhere near where it was dragged to. + #[test] + fn five_dts_soundtracks_do_not_reach_the_renderer_weighing_what_they_did() { + let duration = 120.0; + let source = 124_000_000u64; + + let mut tracks = vec![track(1, TrackKind::Video, TrackCodec::Avc, 0)]; + tracks.extend((2..7).map(|id| track(id, TrackKind::Audio, TrackCodec::Dts, 6))); + let carried: Vec = tracks[1..].to_vec(); + + // Measured: the picture at a megabit and a half, each soundtrack near + // the DTS core rate in eleven-millisecond frames. + let mut measured = vec![rate(1, 0, 24.0)]; + measured.extend((2..7).map(|id| rate(id, 1_360_000, 93.75))); + let rates = TrackRates(measured); + + let promised = promised_ts_length(source, duration, &tracks, &carried, &rates); + assert!( + promised < source / 2, + "five soundtracks shrank eightfold and the promise did not: {promised} \ + against a {source}-byte source" + ); + + // Five stereo AAC tracks, a megabit and a half of picture and the + // programme tables, over two minutes — reached without reference to the + // source's size, which is the whole point. + let expected = (1_470_000.0 * 1.03 + 5.0 * 211_500.0 + 30_080.0) * duration / 8.0; + assert!( + (promised as f64) > expected && (promised as f64) < expected * 1.2, + "{promised} is not the {expected} this stream actually weighs" + ); + } + + /// The estimate leans long on purpose: short of the promise is padding a + /// renderer skips, over it is the film's last minutes cut off. + #[test] + fn the_picture_always_fits_inside_the_promise() { + const GIB: u64 = 1 << 30; + let duration = 7200.0; + + let mut tracks = vec![track(1, TrackKind::Video, TrackCodec::Avc, 0)]; + tracks.extend((2..7).map(|id| track(id, TrackKind::Audio, TrackCodec::Dts, 6))); + let carried: Vec = tracks[1..].to_vec(); + let mut measured = vec![rate(1, 0, 24.0)]; + measured.extend((2..7).map(|id| rate(id, 1_509_000, 93.75))); + let rates = TrackRates(measured); + + let source = 30 * GIB; + let promised = promised_ts_length(source, duration, &tracks, &carried, &rates); + // The picture is the file less its five soundtracks, and it passes + // through untouched, so every byte of it has to fit. + let picture = source - (5 * 1_509_000 * 7200 / 8); + assert!( + promised > picture, + "{promised} does not leave room for {picture} bytes of picture" + ); + assert!( + promised < source + source / 16, + "and should still be under what the source's own size would promise" + ); + } + + /// A soundtrack that is passed through costs what it always cost, so a film + /// of Dolby leaves at roughly the weight it arrived — which is the case the + /// old source-sized promise got right and this must not get wrong. + #[test] + fn a_film_that_only_passes_its_dolby_through_is_promised_what_it_weighs() { + let duration = 3600.0; + let tracks = vec![ + track(1, TrackKind::Video, TrackCodec::Avc, 0), + track(2, TrackKind::Audio, TrackCodec::Ac3, 6), + ]; + let carried = vec![tracks[1].clone()]; + let rates = TrackRates(vec![rate(1, 0, 24.0), rate(2, 640_000, 31.25)]); + + let source = 4 * (1u64 << 30); + let promised = promised_ts_length(source, duration, &tracks, &carried, &rates); + assert!( + promised > source && promised < source * 6 / 5, + "a passthrough film should be promised its own size and a little over, \ + not {promised} against {source}" + ); + } + + /// Frames small enough that the packet they are stuffed into costs more than + /// they do. A flat percentage overhead is badly wrong here, and wrong low — + /// which is the film's last minutes cut off rather than a little padding. + #[test] + fn the_packet_cost_is_counted_rather_than_guessed_at() { + // 192 kbps in 768-byte AC-3 frames: 782 bytes of PES, which is five + // packets of 188 rather than the four and a bit a percentage would give. + let cost = transport_cost(192_000, 31.25); + assert_eq!(cost, (5.0 * 188.0 * 8.0 * 31.25) as u64); + assert!( + cost > 192_000 * 6 / 5, + "a fifth of this track is stuffing, and {cost} does not show it" + ); + + // A 130-kilobyte picture pays for its header and almost nothing else. + let cost = transport_cost(25_000_000, 24.0); + assert!( + cost > 25_000_000 && cost < 25_000_000 * 21 / 20, + "a large frame should cost a couple of per cent, not {cost}" + ); + + // Nothing to say about a track with no rate or no frames. + assert_eq!(transport_cost(0, 24.0), 0); + assert_eq!(transport_cost(500, 0.0), 500); + } + + /// The disposition decides both what the muxer does and what the handler + /// promises, so the two cannot be allowed to answer it differently. + #[test] + fn dolby_and_aac_ride_as_they_are_and_dts_is_re_encoded() { + assert_eq!( + audio_disposition(&track(1, TrackKind::Audio, TrackCodec::Ac3, 6)), + AudioDisposition::Passthrough + ); + assert_eq!( + audio_disposition(&track(1, TrackKind::Audio, TrackCodec::Eac3, 6)), + AudioDisposition::Passthrough + ); + assert_eq!( + audio_disposition(&track(1, TrackKind::Audio, TrackCodec::Aac, 2)), + AudioDisposition::Passthrough + ); + assert_eq!( + audio_disposition(&track(1, TrackKind::Audio, TrackCodec::Unsupported, 2)), + AudioDisposition::Dropped, + "TrueHD and anything else unnamed has no way into a transport stream" + ); + let dts = audio_disposition(&track(1, TrackKind::Audio, TrackCodec::Dts, 6)); + assert_eq!( + dts, + if cfg!(all(feature = "transcode-dts", feature = "transcode-aac")) { + AudioDisposition::Reencoded + } else { + AudioDisposition::Dropped + } + ); + } + + #[test] + fn adts_frames_are_returned_whole_unlike_the_mp4_paths_payloads() { + // Two frames of seven-byte headers and one byte of payload each. + let mut stream = Vec::new(); + for _ in 0..2 { + stream.extend_from_slice(&[0xFF, 0xF1, 0x4C, 0x80, 0x01, 0x1F, 0xFC, 0xAA]); + } + let frames = adts_frames(&stream); + assert_eq!(frames.len(), 2); + for frame in frames { + assert_eq!(frame.len(), 8, "the header stays on"); + assert_eq!(frame[0], 0xFF, "and the frame opens with its syncword"); + } + } + + #[test] + fn a_truncated_frame_is_not_returned() { + let stream = [0xFF, 0xF1, 0x4C, 0x80, 0x7F, 0xFF, 0xFC]; + assert!(adts_frames(&stream).is_empty(), "a frame running past the end"); + } +} diff --git a/crates/vuio-core/src/media/transcode/video.rs b/crates/vuio-core/src/media/transcode/video.rs index f8d6e005..bfdc2822 100644 --- a/crates/vuio-core/src/media/transcode/video.rs +++ b/crates/vuio-core/src/media/transcode/video.rs @@ -60,6 +60,9 @@ pub struct ProgressiveStream { /// `moov` — which is the order a renderer that takes the first audio track /// without asking will read them in, so the caller puts the default first. audio: Vec, + /// Packets read before the `moov` could be written, replayed ahead of + /// anything further from the demuxer. See [`prime_dolby_tracks`]. + primed: std::collections::VecDeque, /// Total length of the film, for `mehd`. duration_secs: Option, sequence: u32, @@ -146,25 +149,25 @@ impl ProgressiveStream { let _ = format.seek( SeekMode::Coarse, SeekTo::Time { - time: Time::try_from_secs_f64(start_secs).unwrap_or(Time::ZERO), + time: Time::try_from_secs_f64(super::seek_target(start_secs)) + .unwrap_or(Time::ZERO), track_id: Some(video.id), }, ); } let video_tb = track_time_base(format.as_ref(), video.id); + let (primed, first_frames) = prime_dolby_tracks(format.as_mut(), audio); + let audio: Vec = audio .iter() .filter_map(|track| { - let codec = track.codec_kind.transcode_codec(); - if codec.is_none() && track.codec_kind != TrackCodec::Aac { - return None; - } let audio_tb = track_time_base(format.as_ref(), track.id); + let (out, codec) = output_track(track, first_frames.get(&track.id))?; Some(AudioSink { source_id: track.id, codec, - sink: TrackSink::new(aac_track(track), audio_tb), + sink: TrackSink::new(out, audio_tb), decode: None, }) }) @@ -174,6 +177,7 @@ impl ProgressiveStream { format, video: TrackSink::new(video.clone(), video_tb), audio, + primed, duration_secs, sequence: 0, started: false, @@ -203,19 +207,25 @@ impl ProgressiveStream { let fragment_ticks = (FRAGMENT_SECS * f64::from(VIDEO_TIMESCALE)).round() as u64; loop { - let packet = match self.format.next_packet() { - Ok(Some(packet)) => packet, - _ => { - // End of the film: flush the encoder's tail and emit whatever - // is held, then stop. - self.finished = true; - self.flush_audio(); - return self.emit(); - } + // Whatever priming read ahead of the `moov` comes first, in the + // order it was read, so the timeline the demuxer handed over is the + // timeline that gets written. + let next = match self.primed.pop_front() { + Some(packet) => Some(packet), + None => match self.format.next_packet() { + Ok(Some(packet)) => { + Some((packet.track_id, packet.pts.get(), packet.data.to_vec())) + } + _ => None, + }, + }; + let Some((track_id, pts, data)) = next else { + // End of the film: flush the encoder's tail and emit whatever + // is held, then stop. + self.finished = true; + self.flush_audio(); + return self.emit(); }; - let track_id = packet.track_id; - let pts = packet.pts.get(); - let data = packet.data.to_vec(); if track_id == self.video.track.id { let ticks = self.video.rescale(pts, VIDEO_TIMESCALE); @@ -270,7 +280,9 @@ impl ProgressiveStream { let ticks = audio.sink.rescale(pts, sample_rate); let Some(codec) = audio.codec else { - // Already AAC. The container's frames are MP4 samples as they stand. + // Passed through: AAC, or Dolby a television decodes for itself. + // Every one of these codecs frames its own bitstream, so a + // container packet is already exactly one MP4 sample. audio.sink.push(ticks, data.to_vec(), true); return Ok(()); }; @@ -474,30 +486,133 @@ impl TrackSink { } } -/// The audio track as it will be written into the output. +/// One packet held back from the demuxer during priming. +type PrimedPacket = (u32, i64, Vec); + +/// Packets to read while looking for the first frame of each Dolby track. +/// +/// A film interleaves its tracks, so the first frame of the last soundtrack +/// arrives within a fragment or so of the first. This only has to be larger +/// than that, and small enough that a file with a track that never appears +/// costs nothing much to give up on. +const PRIME_PACKETS: usize = 512; + +/// Read far enough ahead to describe every Dolby track being passed through. +/// +/// An `ac-3` sample entry needs a record that only the bitstream carries — +/// Matroska stores no `CodecPrivate` for AC-3, because the syncframe already +/// says everything — and the init segment naming that entry goes out before the +/// first fragment. So the first frame of each such track is read here, ahead of +/// the `moov`, and every packet read on the way is handed back to be replayed +/// rather than dropped. +/// +/// Reads nothing at all for a film with no Dolby track to pass through. +fn prime_dolby_tracks( + format: &mut dyn symphonia::core::formats::FormatReader, + audio: &[TrackInfo], +) -> ( + std::collections::VecDeque, + std::collections::HashMap>, +) { + let mut primed = std::collections::VecDeque::new(); + let mut first_frames = std::collections::HashMap::new(); + + let wanted: Vec = audio + .iter() + .filter(|track| matches!(track.codec_kind, TrackCodec::Ac3 | TrackCodec::Eac3)) + .map(|track| track.id) + .collect(); + if wanted.is_empty() { + return (primed, first_frames); + } + + for _ in 0..PRIME_PACKETS { + let Ok(Some(packet)) = format.next_packet() else { + break; + }; + let (id, pts, data) = (packet.track_id, packet.pts.get(), packet.data.to_vec()); + if wanted.contains(&id) { + first_frames.entry(id).or_insert_with(|| data.clone()); + } + primed.push_back((id, pts, data)); + if wanted.iter().all(|id| first_frames.contains_key(id)) { + break; + } + } + (primed, first_frames) +} + +/// The audio track as it will be written into the output, and what has to +/// happen to its packets on the way. /// -/// A decoded track is restated as the AAC it becomes: an `mp4a` sample entry -/// whose `esds` carries the encoder's own `AudioSpecificConfig`, at the -/// encoder's channel count rather than the source's 5.1. A track that is already -/// AAC keeps everything it had, including the config the container carried. -fn aac_track(track: &TrackInfo) -> TrackInfo { +/// Three outcomes, and the returned codec is which one. `None` is passthrough: +/// AAC, and AC-3 or E-AC-3 whose first frame described itself, all of which a +/// television plays as they stand — carrying them costs no CPU and keeps the +/// 5.1 that a stereo re-encode would throw away. `Some(codec)` is a track that +/// has to be decoded and re-encoded to be heard at all, which after this change +/// means DTS and nothing else. And `None` for the whole track — dropping it — +/// is for one this build can neither pass through nor produce: better a film +/// with two working soundtracks than one with a third that plays noise. +fn output_track( + track: &TrackInfo, + first_frame: Option<&Vec>, +) -> Option<(TrackInfo, Option)> { let name = track.name.clone().or_else(|| Some(source_label(track))); + if track.codec_kind == TrackCodec::Aac { - return TrackInfo { - name, - ..track.clone() - }; + return Some(( + TrackInfo { + name, + ..track.clone() + }, + None, + )); } - let sample_rate = track.sample_rate.unwrap_or(48_000); - TrackInfo { - codec: format!("{} → AAC", track.codec), - codec_kind: TrackCodec::Aac, - channels: Some(DECODED_CHANNELS as u8), - extra_data: super::audio_specific_config(sample_rate, DECODED_CHANNELS), - track_kind: TrackKind::Audio, - name, - ..track.clone() + + // Dolby, passed through if its own first frame will describe it. A frame + // that does not parse is not passed through on the strength of the + // container's word: an `ac-3` entry whose record was guessed at is a track + // a renderer will try to decode and fail on. + if matches!(track.codec_kind, TrackCodec::Ac3 | TrackCodec::Eac3) { + let parsed = first_frame.and_then(|frame| match track.codec_kind { + TrackCodec::Eac3 => crate::media::remux::parse_eac3(frame), + _ => crate::media::remux::parse_ac3(frame), + }); + if let Some(config) = parsed { + return Some(( + TrackInfo { + // The syncframe outranks the container on both counts: it is + // what the decoder will actually be handed. + sample_rate: Some(config.sample_rate), + channels: Some(config.channels), + extra_data: config.record, + name, + ..track.clone() + }, + None, + )); + } + } + + // Anything left has to be decoded, and can only be carried if this build + // has both halves of that. + let codec = track.codec_kind.transcode_codec()?; + if !codec.is_decodable() || !cfg!(feature = "transcode-aac") { + return None; } + let sample_rate = track.sample_rate.unwrap_or(48_000); + Some(( + TrackInfo { + codec: format!("{} → AAC", track.codec), + codec_kind: TrackCodec::Aac, + channels: Some(DECODED_CHANNELS as u8), + extra_data: super::audio_specific_config(sample_rate, DECODED_CHANNELS), + track_kind: TrackKind::Audio, + name, + ..track.clone() + }, + Some(codec), + )) } /// What to call this track in a renderer's audio menu. diff --git a/crates/vuio-core/src/web/client.rs b/crates/vuio-core/src/web/client.rs index d2942002..267755a7 100644 --- a/crates/vuio-core/src/web/client.rs +++ b/crates/vuio-core/src/web/client.rs @@ -79,6 +79,44 @@ tokio::task_local! { pub static CURRENT_CLIENT: DlnaClientProfile; } +/// One line saying what a renderer asked for, and how it asked. +/// +/// A television that will not seek is nearly always a disagreement about one of +/// two things: which `` it actually fetched, and which seek mechanism it +/// used to scrub. Neither is visible from this end unless it is written down — +/// the DIDL offers two resources and says both are seekable, and what the set +/// arrives at is its own business until it makes a request. So this records the +/// request as it came in: who sent it, what they asked for, and every header +/// that bears on seeking, including the ones we do not honour. A `Range: bytes=` +/// on a resource advertised as time-seek-only is not a bug in the renderer; it +/// is the answer to why the scrub bar does nothing. +pub fn log_renderer_request( + resource: &str, + method: &axum::http::Method, + headers: &HeaderMap, +) -> DlnaClientProfile { + let profile = detect_client(headers); + let header = |name: &str| { + headers + .get(name) + .and_then(|value| value.to_str().ok()) + .unwrap_or("-") + .to_string() + }; + tracing::info!( + target: "vuio::renderer", + "{method} {resource} | profile={profile:?} | ua={:?} | av-client={:?} | \ + TimeSeekRange={:?} | Range={:?} | getcontentFeatures={:?} | transferMode={:?}", + header("user-agent"), + header("x-av-client-info"), + header("timeseekrange.dlna.org"), + header("range"), + header("getcontentfeatures.dlna.org"), + header("transfermode.dlna.org"), + ); + profile +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/vuio-core/src/web/mod.rs b/crates/vuio-core/src/web/mod.rs index 0ba573e5..40fcc803 100644 --- a/crates/vuio-core/src/web/mod.rs +++ b/crates/vuio-core/src/web/mod.rs @@ -20,6 +20,8 @@ pub mod subtitles; #[cfg(feature = "transcode")] pub mod transcode_streaming; #[cfg(all(feature = "transcode-aac", feature = "casting"))] +pub mod ts_streaming; +#[cfg(all(feature = "transcode-aac", feature = "casting"))] pub mod video_streaming; #[cfg(feature = "dashboard")] pub mod ui; @@ -27,6 +29,82 @@ pub mod xml; use crate::{database::DatabaseManager, state::AppState}; +/// The length a transcoded film commits to when nothing better is known. +/// +/// Built from the source's own size, because the picture is the bulk of both and +/// passes through untouched. What that ignores is what the soundtracks do, and +/// on a film carrying several of them in DTS it is the whole answer: a quarter +/// of the file leaves re-encoded at an eighth of the rate, so the stream weighs +/// nothing like the source and this number is out by a factor of three. +/// +/// Which is why the transport stream does not use it. That resource is the one a +/// television seeks by byte, so its promise has to be an honest account of what +/// it produces, and [`crate::media::transcode::promised_ts_length`] builds one +/// from the film's own measured tracks. This remains for the two cases with +/// nothing to measure against: the fragmented MP4, which the browser player +/// fetches whole and never seeks into by byte, and a film whose container +/// declares no duration, where there is no way to turn bytes into instants at +/// all. +/// +/// Rounded up, and that is the part that matters wherever it is used. The +/// response is made to be exactly this long whatever the muxer produces, so the +/// estimate being wrong is not the risk — the risk is which way. Short of the +/// promise is padding the renderer skips; over the promise is the film's last +/// seconds cut off. So the number leans high. +pub(crate) fn promised_transcode_length(source_size: u64) -> u64 { + /// What to promise for a file the index has no size for. Zero would + /// advertise an empty resource, which renderers decline to open at all. + const FALLBACK: u64 = 1 << 30; + /// Slack over the source, against a source whose own overhead is unusually + /// light — a sixteenth, and never less than this many bytes, which is what + /// keeps a very short film from being trimmed by its fragment headers. + const FLOOR: u64 = 256 * 1024; + + if source_size == 0 { + return align_to_packets(FALLBACK); + } + align_to_packets(source_size + (source_size / 16).max(FLOOR)) +} + +/// Round a length down to a whole number of transport packets. +/// +/// One of the containers this number can describe is a transport stream, which +/// is a run of 188-byte packets and nothing else. A promise that is not a whole +/// number of them ends in a fragment of a packet — filler no decoder can read as +/// anything, sitting where a decoder reading to the end would look. The MP4 path +/// is indifferent to the alignment, so one aligned number is true of either. +fn align_to_packets(length: u64) -> u64 { + const TS_PACKET: u64 = crate::media::remux::TS_PACKET_LEN as u64; + (length / TS_PACKET) * TS_PACKET +} + +/// Whether a byte range is a renderer sizing the resource up rather than seeking +/// into it. +/// +/// Renderers read the end of a file before they play it — sixteen bytes for an +/// MP4 reader looking for a `moov`, a few hundred kilobytes for a transport +/// stream reader looking for a last timestamp. Both land in the padding that +/// makes the promised length true, so both can be answered from it directly: +/// no transcode slot, no muxing, and the bytes are the truth because padding is +/// the one part of these resources whose contents are known without producing +/// them. +/// +/// Getting that wrong in the expensive direction is what stops a film playing. +/// A probe answered by muxing holds a slot for as long as it takes to seek into +/// a thirty-gigabyte file, and a television that opens three connections at once +/// against two slots then has its actual playback request refused outright. +/// +/// The threshold scales, because "near the end" means something different for a +/// two-minute clip than for a three-hour film, and it is deliberately small: a +/// genuine seek this close to the end lands in the last second or two of the +/// film, where answering with padding costs nothing anyone would notice. +pub(crate) fn is_probe_tail(first_byte: u64, promised: u64) -> bool { + const SMALLEST: u64 = 64 * 1024; + const LARGEST: u64 = 8 * 1024 * 1024; + let tail = (promised / 128).clamp(SMALLEST, LARGEST); + promised.saturating_sub(first_byte) <= tail +} + /// Whether this item is one a renderer may be unable to play unaided. /// /// True only when the codec is AC-3, E-AC-3 or DTS *and* this build can decode @@ -104,6 +182,10 @@ pub(crate) fn transcode_advert( // Constant-bitrate PCM: a byte offset divides straight back // into a sample, so this is a real seek. op: "11", + // The exact length is the decoded sample count, which only + // the plan knows; the streaming handler states it per + // response rather than the DIDL stating it up front. + sized: false, }, TranscodeAudioFormat::Aac => xml::AdvertResource { mime: "audio/aac", @@ -111,17 +193,34 @@ pub(crate) fn transcode_advert( // A lossy re-encode has no length until it exists, so there // is nothing to seek within. op: "00", + sized: false, }, }, // A film is offered the film, not its soundtrack: the same picture, - // with an audio track the renderer can actually decode. Time seek - // only — see `web::video_streaming` for why byte seek is not on - // offer and why time seek is enough. + // with audio the renderer can actually decode. + // + // As a transport stream, not the MP4 next door, because this is what + // a television is being handed and a television scrubs by byte. A + // byte offset into a fragmented MP4 produced on demand names nothing + // stable; a transport stream resynchronises wherever it is joined, + // which is what makes `DLNA.ORG_OP=11` here an honest claim rather + // than a corrupt one. The MP4 remains for the browser player, which + // fetches whole responses and never needs it. See + // `web::ts_streaming`. #[cfg(all(feature = "transcode-aac", feature = "casting"))] video: Some(xml::AdvertResource { - mime: "video/mp4", - path: "transcode/video.mp4", - op: "10", + mime: "video/mpeg", + path: "transcode/video.ts", + op: "11", + // No size, and this is the one place the DIDL cannot honestly + // state one. The length of the produced stream turns on what + // each of the film's soundtracks costs it and which of them + // leave re-encoded — facts that live in the file, not in the + // index, and reading them here would mean opening every film in + // a folder to render one browse response. The streaming handler + // does know, states it as a `Content-Length`, and makes it true; + // a `size` guessed from the source would only contradict it. + sized: false, }), // With no remuxer or no encoder there is nothing to offer a film. // Offering it `audio.wav` instead would replace a silent film with @@ -362,11 +461,19 @@ pub fn create_router( // The film itself, remuxed with its audio decoded. Needs the demuxer as // well as the encoder, which is why it rides on `casting` too. #[cfg(all(feature = "transcode-aac", feature = "casting"))] - let router = router.route( - "/media/{id}/transcode/video.mp4", - get(video_streaming::serve_transcoded_video::) - .head(video_streaming::serve_transcoded_video::), - ); + let router = router + .route( + "/media/{id}/transcode/video.mp4", + get(video_streaming::serve_transcoded_video::) + .head(video_streaming::serve_transcoded_video::), + ) + // The same film as a transport stream, which is the one a television can + // seek. See `web::ts_streaming`. + .route( + "/media/{id}/transcode/video.ts", + get(ts_streaming::serve_transcoded_ts::) + .head(ts_streaming::serve_transcoded_ts::), + ); #[cfg(feature = "casting")] let router = router diff --git a/crates/vuio-core/src/web/streaming.rs b/crates/vuio-core/src/web/streaming.rs index 1739f247..8425d659 100644 --- a/crates/vuio-core/src/web/streaming.rs +++ b/crates/vuio-core/src/web/streaming.rs @@ -71,6 +71,14 @@ pub async fn serve_media( ) -> Result { let start_time = Instant::now(); + // Only the opening request of a playback, not every range request that + // follows it — what this is for is seeing which resource a renderer chose + // and how it seeks, and one line per scrub says that without one line per + // buffer refill. + if !headers.contains_key(header::RANGE) { + crate::web::client::log_renderer_request(&format!("/media/{id}"), &method, &headers); + } + let file_id = media_id_from_path_segment(&id).ok_or_else(|| { state.web_metrics.record_error(); AppError::NotFound diff --git a/crates/vuio-core/src/web/ts_streaming.rs b/crates/vuio-core/src/web/ts_streaming.rs new file mode 100644 index 00000000..fe5cac5f --- /dev/null +++ b/crates/vuio-core/src/web/ts_streaming.rs @@ -0,0 +1,544 @@ +//! A film served as a transport stream, which is the one a television can seek. +//! +//! The `video.mp4` next door is the right answer for the browser player, where +//! the client fetches whole responses and starts each one over. It is the wrong +//! answer for a television, which scrubs by asking for a byte offset — and a +//! byte offset into a fragmented MP4 produced on demand names nothing stable, so +//! answering it positionally hands the renderer the middle of a structure it +//! cannot interpret. +//! +//! A transport stream is built for exactly this. Sync bytes every 188 bytes, the +//! programme tables repeated throughout rather than written once, and the +//! parameter sets a decoder needs travelling beside every keyframe. Land at an +//! arbitrary offset and it finds its footing: the next sync byte, the next +//! tables, the next random-access point, and it plays. That is why a broadcast +//! format is what every DLNA server transcodes to, and why this resource can +//! honestly say `DLNA.ORG_OP=11`. +//! +//! ## What a byte offset means here +//! +//! A fraction. The response commits to a length, and an offset is read as that +//! fraction of the film, then produced from the keyframe at or before it. It is +//! not exact the way seeking a stored file is: a film whose bitrate varies a lot +//! will land seconds away from where the scrub bar said. It is close enough to +//! watch, which is the thing that was not previously true at all. +//! +//! Which makes the promised length the load-bearing number in the whole +//! mechanism, and it is one this resource has to state before a byte of it +//! exists. Promising the source file's own size — the obvious guess, since the +//! picture passes through untouched — is wrong by a factor of three on the films +//! this path exists for: five DTS soundtracks at a megabit and a half each leave +//! as stereo AAC at a fifth of that, so two thirds of the promise is padding and +//! every byte offset names a moment two thirds too far along. So the promise is +//! built instead from what the output will really weigh, with each soundtrack's +//! cost measured off the file rather than assumed from its codec. See +//! [`crate::media::transcode::promised_ts_length`]. +//! +//! The body is then made to be exactly that length, padded with null packets — +//! transport stream's own defined filler — so the transfer completes rather than +//! coming up short. + +use axum::{ + body::Body, + extract::{Path, Query, State}, + http::{header, HeaderMap, Method, StatusCode}, + response::{IntoResponse, Response}, +}; +use tracing::{debug, warn}; + +use crate::media::remux::{browser_video_track, MkvDemuxer, TrackInfo, TS_PACKET_LEN}; +use crate::media::transcode::{ + audio_disposition, measure_track_rates, promised_ts_length, AudioDisposition, IndexKey, + TrackRates, TsStream, +}; +use crate::{database::DatabaseManager, error::AppError, state::AppState}; + +use std::sync::Arc; + +use super::streaming::media_id_from_path_segment; +use super::video_streaming::{audio_tracks, parse_npt_start}; + +/// How many chunks may sit between the muxer and the socket. +const PIPELINE_DEPTH: usize = 2; + +/// DLNA flags: streaming and background transfer modes, connection stalling, +/// and the DLNA 1.5 marker. +const DLNA_FLAGS: &str = "DLNA.ORG_FLAGS=01700000000000000000000000000000"; + +/// `?t=` for seeking, `?audio_track=` to carry one track alone. +#[derive(serde::Deserialize, Default)] +pub struct TsQuery { + t: Option, + audio_track: Option, +} + +/// `GET`/`HEAD /media/{id}/transcode/video.ts`. +pub async fn serve_transcoded_ts( + State(state): State>, + Path(id): Path, + method: Method, + Query(query): Query, + headers: HeaderMap, +) -> Result { + super::client::log_renderer_request( + &format!("/media/{id}/transcode/video.ts"), + &method, + &headers, + ); + let (file_id, path, size, filename, info) = resolve(&state, &id).await?; + let video = browser_video_track(&info.tracks) + .ok_or(AppError::NotFound)? + .clone(); + let audio = audio_tracks(&info.tracks, query.audio_track); + let duration = info.duration_secs.filter(|d| *d > 0.0); + let promised = match duration { + Some(duration) => { + let rates = track_rates(&state, file_id, &path, &info.tracks).await; + promised_ts_length(size, duration, &info.tracks, &audio, &rates) + } + // No duration is no way to turn bytes into instants, so there is nothing + // better than the source's own size to promise. + None => super::promised_transcode_length(size), + }; + + // Either mechanism, and both mean the same thing here. A time seek names the + // instant; a byte offset names the fraction of the promised length that the + // scrub bar was dragged to, which is the same instant expressed the only way + // a set that scrubs by byte knows how to express it. + let time_seek = header_value(&headers, "timeseekrange.dlna.org").and_then(parse_npt_start); + let byte_seek = header_value(&headers, "range").and_then(parse_byte_start); + let requested = time_seek + .or_else(|| { + byte_seek + .zip(duration) + .map(|(offset, duration)| duration * offset as f64 / promised as f64) + }) + .or(query.t); + let start = requested + .unwrap_or(0.0) + .max(0.0) + .min(duration.map(|d| (d - 0.1).max(0.0)).unwrap_or(f64::MAX)); + + let is_range = byte_seek.is_some() || time_seek.is_some(); + // Where the response sits in the promised byte space. A byte seek already + // said; a time seek has to be converted so that the `Content-Range` and the + // length agree with the bar the renderer drew. + let first_byte = match byte_seek { + Some(offset) => offset.min(promised.saturating_sub(1)), + None => match duration { + Some(duration) if time_seek.is_some() && duration > 0.0 => { + ((promised as f64 * start / duration) as u64).min(promised.saturating_sub(1)) + } + _ => 0, + }, + }; + + // A renderer sizing the resource up rather than seeking into it. Answered + // from the padding, which costs nothing and takes no transcode slot — the + // point being that muxing an answer here holds a slot for as long as it + // takes to seek into a thirty-gigabyte film, and the set has already opened + // the connection it actually wants to play on. + if byte_seek.is_some() && super::is_probe_tail(first_byte, promised) { + return padding_tail(first_byte, promised); + } + + let deliver = ((promised - first_byte) / TS_PACKET_LEN as u64) * TS_PACKET_LEN as u64; + if deliver == 0 { + // A range starting inside the stream's final packet. There is no whole + // packet left to produce, but the question is a perfectly ordinary one — + // a client sizing up the file reads its last handful of bytes — and + // refusing it as unsatisfiable is reported as a transfer error rather + // than shrugged off. So it is answered from the final packet itself, + // which is padding, and whose bytes are therefore known exactly. + return final_packet_response(first_byte, promised); + } + + // Codecs, not just track numbers. Which codec a track is in decides whether + // it is passed through or decoded, and a stream that will not play is + // nearly always one whose codec was not what the container claimed or not + // one this build handles — neither of which is visible from a track id. + debug!( + "transcoded ts: id={id}, file={filename}, start={start:.3}s, byte={first_byte}, \ + promised={promised}, video={} ({:?}), audio=[{}]", + video.codec, + video.codec_kind, + audio + .iter() + .map(|track| format!( + "{}:{} {:?} {}ch {}", + track.id, + track.language.as_deref().unwrap_or("und"), + track.codec_kind, + track.channels.unwrap_or(0), + match audio_disposition(track) { + AudioDisposition::Passthrough => "passthrough", + AudioDisposition::Reencoded => "re-encoded", + AudioDisposition::Dropped => "dropped", + } + )) + .collect::>() + .join(", ") + ); + + let mut response = Response::builder() + .status(if is_range { + StatusCode::PARTIAL_CONTENT + } else { + StatusCode::OK + }) + .header(header::CONTENT_TYPE, "video/mpeg") + .header(header::CACHE_CONTROL, "no-cache") + .header(header::ACCEPT_RANGES, "bytes") + .header("transferMode.dlna.org", "Streaming") + // Both, and both are honest: a transport stream resynchronises wherever + // it is joined, so an approximate byte offset is a usable seek rather + // than a corrupt one. + .header( + "contentFeatures.dlna.org", + format!("DLNA.ORG_OP=11;DLNA.ORG_CI=1;{DLNA_FLAGS}"), + ) + // Whole packets, which for a range starting part way through means + // ending a little short of the promise. HTTP allows a server to answer + // with less of a range than was asked for, and a decoder handed a + // fragment of a packet can do nothing with it. + .header(header::CONTENT_LENGTH, deliver); + if is_range { + response = response.header( + header::CONTENT_RANGE, + format!("bytes {first_byte}-{}/{promised}", first_byte + deliver - 1), + ); + } + if let Some(duration) = duration { + response = response.header("X-Content-Duration", format!("{duration:.3}")); + if is_range { + response = response.header( + "TimeSeekRange.dlna.org", + format!("npt={start:.3}-{duration:.3}/{duration:.3}"), + ); + } + response = response.header("X-AvailableSeekRange", format!("1 npt=0.000-{duration:.3}")); + } + + if method == Method::HEAD { + return Ok(response.body(Body::empty())?); + } + + let Some(permit) = state.transcode.try_acquire() else { + return Ok(busy(&state, &filename)); + }; + + Ok(response.body(ts_body(path, video, audio, start, deliver, permit))?) +} + +/// Mux the film on a blocking thread, handing packets over a bounded channel. +/// +/// Exactly `deliver` bytes reach the socket. Short output is padded with null +/// packets, which is transport stream's own filler and what a decoder is already +/// built to skip; long output is cut at the promise, on a packet boundary so +/// that what arrives is never half a packet. +fn ts_body( + path: std::path::PathBuf, + video: TrackInfo, + audio: Vec, + start: f64, + deliver: u64, + permit: tokio::sync::OwnedSemaphorePermit, +) -> Body { + let (tx, rx) = tokio::sync::mpsc::channel::>(PIPELINE_DEPTH); + + tokio::task::spawn_blocking(move || { + let _permit = permit; + let mut sent: u64 = 0; + let opened = std::time::Instant::now(); + let mut stream = match TsStream::open(&path, &video, &audio, start) { + Ok(stream) => stream, + Err(error) => { + // Loud, because the renderer's only symptom is a film that will + // not start: the failure goes down the body as a broken + // transfer and is otherwise invisible from either end. + warn!("cannot open {} as a transport stream: {error:#}", path.display()); + let _ = tx.blocking_send(Err(std::io::Error::other(error.to_string()))); + return; + } + }; + + let mut chunks = 0usize; + while let Some(chunk) = stream.next_chunk() { + if chunks == 0 { + // How long a renderer waited before its first byte, which is + // the other way this fails: a set that gives up before the + // first group of pictures has been muxed shows the same nothing + // as one that was handed something it could not decode. + debug!( + "first chunk for {}: {} bytes after {:.2}s", + path.display(), + chunk.len(), + opened.elapsed().as_secs_f64() + ); + } + chunks += 1; + if !send_capped(&tx, &mut sent, deliver, chunk) { + debug!("{} stopped after {chunks} chunks, {sent} bytes", path.display()); + return; + } + } + debug!( + "{} finished: {chunks} chunks, {sent} of {deliver} bytes before padding", + path.display() + ); + while sent < deliver { + let filler = null_packets(deliver - sent); + if !send_capped(&tx, &mut sent, deliver, filler) { + return; + } + } + }); + + Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) +} + +/// Hand over `chunk`, trimmed to a whole number of packets within what is left. +fn send_capped( + tx: &tokio::sync::mpsc::Sender>, + sent: &mut u64, + deliver: u64, + mut chunk: Vec, +) -> bool { + let room = deliver.saturating_sub(*sent); + if room == 0 { + return false; + } + if chunk.len() as u64 > room { + // On a packet boundary: half a transport packet is not something a + // decoder can do anything with, and the promise is met by padding. + chunk.truncate((room as usize / TS_PACKET_LEN) * TS_PACKET_LEN); + } + *sent += chunk.len() as u64; + if chunk.is_empty() { + return false; + } + tx.blocking_send(Ok(bytes::Bytes::from(chunk))).is_ok() && *sent < deliver +} + +/// Filler occupying up to `remaining` bytes, as whole null packets. +/// +/// A remainder too small to hold one goes out as zeroes, which sit past the last +/// packet any decoder will look at. +fn null_packets(remaining: u64) -> Vec { + const CHUNK: u64 = 64 * 1024; + let len = remaining.min(CHUNK); + let packets = len as usize / TS_PACKET_LEN; + if packets == 0 { + return vec![0u8; remaining as usize]; + } + let mut out = Vec::with_capacity(packets * TS_PACKET_LEN); + for _ in 0..packets { + crate::media::remux::TsMuxer::null(&mut out); + } + out +} + +/// Answer a range lying inside the stream's last packet, without producing +/// anything. +fn final_packet_response(first: u64, promised: u64) -> Result { + padding_tail(first, promised) +} + +/// Serve `[first, promised)` from the stream's padding. +/// +/// The stream is padded out to its promised length with null packets, and a null +/// packet is the same 188 bytes every time — so these are the exact bytes a +/// complete read would end with, aligned the same way, rather than a convenient +/// substitute for them. Nothing is demuxed, nothing is decoded, and no transcode +/// slot is taken. +fn padding_tail(first: u64, promised: u64) -> Result { + /// Filler generated at a time, so a renderer asking for a large stretch of + /// padding does not become a large allocation. + const CHUNK: u64 = 64 * 1024; + + let length = promised.saturating_sub(first); + if length == 0 { + return Ok(Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, format!("bytes */{promised}")) + .body(Body::empty())?); + } + // Where `first` falls inside a packet, so the bytes line up with the ones a + // full read would have delivered at this offset. + let phase = (first % TS_PACKET_LEN as u64) as usize; + let mut packet = Vec::new(); + crate::media::remux::TsMuxer::null(&mut packet); + + let zeroes = futures_util::stream::unfold((length, phase), |(left, phase)| async move { + if left == 0 { + return None; + } + let mut packet = Vec::new(); + crate::media::remux::TsMuxer::null(&mut packet); + let mut out = Vec::with_capacity(CHUNK as usize + TS_PACKET_LEN); + out.extend_from_slice(&packet[phase..]); + while (out.len() as u64) < CHUNK.min(left) { + out.extend_from_slice(&packet); + } + let take = (left.min(out.len() as u64)) as usize; + out.truncate(take); + Some(( + Ok::<_, std::io::Error>(bytes::Bytes::from(out)), + (left - take as u64, 0), + )) + }); + + Ok(Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header(header::CONTENT_TYPE, "video/mpeg") + .header(header::CONTENT_LENGTH, length) + .header( + header::CONTENT_RANGE, + format!("bytes {first}-{}/{promised}", promised - 1), + ) + .body(Body::from_stream(zeroes))?) +} + +fn header_value<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { + headers.get(name).and_then(|value| value.to_str().ok()) +} + +/// The first byte a `Range` header asks for. +/// +/// Only the offset matters: this resource is produced from that point to the end +/// whatever end the header named, which HTTP allows a server to do. +fn parse_byte_start(header: &str) -> Option { + let range = header.trim().strip_prefix("bytes=")?; + let start = range.split('-').next()?.trim(); + if start.is_empty() { + return None; + } + start.parse::().ok() +} + +type Resolved = ( + i64, + std::path::PathBuf, + u64, + String, + crate::media::remux::FileInfo, +); + +async fn resolve(state: &AppState, id: &str) -> Result { + let Some(file_id) = media_id_from_path_segment(id) else { + return Err(AppError::NotFound); + }; + if !state.current_config().transcode.enabled { + return Err(AppError::NotFound); + } + let file = state + .database + .get_file_location_by_id(file_id) + .await? + .ok_or(AppError::NotFound)?; + let info = MkvDemuxer::inspect(&file.path).map_err(|error| { + debug!("cannot inspect {} for remuxing: {error}", file.filename); + AppError::NotFound + })?; + Ok((file_id, file.path, file.size, file.filename, info)) +} + +/// What this film's tracks cost it, measured once and then remembered. +/// +/// Every response for one film has to state the same promised length — a +/// `HEAD`, the `GET` after it and each scrub all divide byte offsets by it, and +/// two different answers are two different films as far as the renderer's scrub +/// bar is concerned. Caching is therefore not only the cheap thing but the +/// correct one. +/// +/// A measurement that cannot be taken is not an error. Each track then falls +/// back to its codec's nominal shape, which is what the estimate did before +/// there was anything to measure. +async fn track_rates( + state: &AppState, + file_id: i64, + path: &std::path::Path, + tracks: &[TrackInfo], +) -> Arc { + let key = match tokio::fs::metadata(path).await { + Ok(metadata) => IndexKey { + id: file_id, + size: metadata.len(), + modified: metadata + .modified() + .ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + .unwrap_or(0), + }, + Err(_) => return Arc::new(TrackRates::default()), + }; + if let Some(rates) = state.transcode.cached_rates(&key).await { + return rates; + } + + let owned = path.to_path_buf(); + let wanted = tracks.to_vec(); + let measured = tokio::task::spawn_blocking(move || measure_track_rates(&owned, &wanted)) + .await + .ok() + .and_then(|result| result.ok()) + .unwrap_or_default(); + if measured.is_empty() { + debug!( + "no track of {} could be measured; falling back to nominal rates", + path.display() + ); + } + let measured = Arc::new(measured); + state.transcode.remember_rates(key, measured.clone()).await; + measured +} + +fn busy(state: &AppState, filename: &str) -> Response { + warn!( + "refusing to remux {}: all {} transcode slots are in use", + filename, + state.current_config().transcode.max_concurrent + ); + ( + StatusCode::SERVICE_UNAVAILABLE, + [(header::RETRY_AFTER, "5")], + "All transcoding slots are in use.", + ) + .into_response() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_range_names_its_first_byte() { + assert_eq!(parse_byte_start("bytes=1024-"), Some(1024)); + assert_eq!(parse_byte_start("bytes=1024-2048"), Some(1024)); + assert_eq!(parse_byte_start("bytes=0-"), Some(0)); + // A suffix range needs a real length to resolve against. + assert_eq!(parse_byte_start("bytes=-500"), None); + assert_eq!(parse_byte_start("npt=10-"), None); + } + + /// Padding is whole packets, because a decoder reading the tail should find + /// the format it was promised rather than a partial one. + #[test] + fn padding_is_made_of_whole_null_packets() { + let filler = null_packets(1000); + assert_eq!(filler.len() % TS_PACKET_LEN, 0); + assert_eq!(filler.len(), 5 * TS_PACKET_LEN, "1000 bytes holds five"); + for packet in filler.chunks(TS_PACKET_LEN) { + assert_eq!(packet[0], 0x47); + // PID 0x1FFF is the null packet. + assert_eq!( + (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]), + 0x1FFF + ); + } + // A remainder too small for a packet still fills the promise exactly. + assert_eq!(null_packets(100).len(), 100); + } +} diff --git a/crates/vuio-core/src/web/video_streaming.rs b/crates/vuio-core/src/web/video_streaming.rs index e6d4b879..d97e4cb5 100644 --- a/crates/vuio-core/src/web/video_streaming.rs +++ b/crates/vuio-core/src/web/video_streaming.rs @@ -1,34 +1,42 @@ -//! A film served as fragmented MP4, with the soundtrack decoded on the way past. +//! A film served as fragmented MP4, with only the audio a television cannot +//! decode re-encoded on the way past. //! -//! What phase 4 is for. The television plays the picture and the AC-3 or DTS -//! track produces nothing, so it is offered a second resource: the same film, -//! the same video bitstream copied through untouched, and an AAC audio track -//! made by decoding the original and re-encoding it. +//! What phase 4 is for. The television plays the picture and the soundtrack +//! produces nothing, so it is offered a second resource: the same film, the same +//! video bitstream copied through untouched, and audio it can actually play. +//! AC-3 and E-AC-3 are handed over as they are — a television is what Dolby +//! Digital was built for, and re-encoding a 5.1 track into stereo AAC would +//! throw away the surround it was about to play. Only DTS, which televisions +//! commonly do lack, is decoded and re-encoded. //! -//! ## Why this resource has no `Content-Length` +//! ## Why this resource's length is an estimate //! -//! It does not exist until it is produced, and unlike the LPCM resource next -//! door its length cannot be worked out in advance. Video passthrough is -//! predictable only if every sample's size is known, which for Matroska means -//! reading the whole film; the AAC half is not predictable at all, because a -//! lossy encoder's frame sizes depend on the audio. So the body is chunked and -//! its length is unstated. A guessed `Content-Length` would be far worse: a -//! renderer that is promised bytes it never receives reports a failed transfer, -//! where an unstated length costs only the byte-seek nobody could honour anyway. +//! It does not exist until it is produced, and its length cannot be worked out +//! in advance: video passthrough is only predictable if every sample size is +//! known, which for Matroska means reading the whole film, and a lossy +//! re-encode is not predictable at all. But a renderer that is told nothing +//! about the length mostly declines to draw a scrub bar, and one that is told +//! byte seeking is unavailable mostly declines to seek — which is the whole +//! feature, gone, in exchange for a technically truthful header. //! -//! ## How it is seekable regardless +//! So a length is stated: the source file's, which is close because the picture +//! is the bulk of both and passes through untouched. What makes that safe is +//! that the body is then made to be exactly that long, whatever the muxer +//! actually produces — short output is padded with `free` boxes, which ISO-BMFF +//! defines as skippable filler, and long output is cut at the promise. Every +//! response therefore delivers precisely the bytes it committed to, which is the +//! part a renderer actually checks. //! -//! By time. `TimeSeekRange.dlna.org` is the DLNA mechanism for exactly this -//! case, and `DLNA.ORG_OP=01` is how a renderer is told to use it: byte seeking -//! unsupported, time seeking supported. A seek is a fresh response built from -//! the same film at a different point — the demuxer seeks by timestamp to the -//! keyframe at or before the request, and the fragments that follow carry the -//! real timeline, so the renderer's position display stays true. +//! ## How seeking works //! -//! Because the seek is by time rather than by byte, it works the same for every -//! audio codec: nothing in it depends on the audio being predictable in size, or -//! on it being passed through rather than re-encoded. A film with an AC-3 track -//! and a film with an AAC one are scrubbed identically. +//! Both ways, because renderers disagree about which to use. A +//! `TimeSeekRange.dlna.org` names an instant and is answered exactly: the +//! demuxer seeks to the keyframe at or before it. A `Range: bytes=` names an +//! offset into a file that does not really exist, so it is read as a fraction of +//! the promised length and turned back into an instant — which lands within a +//! few seconds on a film of roughly even bitrate, and further out on a very +//! variable one. Neither is exact in the way seeking a stored file is. Both are +//! close enough to scrub with, which nothing at all was not. use axum::{ body::Body, @@ -38,7 +46,9 @@ use axum::{ }; use tracing::{debug, warn}; -use crate::media::remux::{browser_audio_tracks, browser_video_track, FileInfo, MkvDemuxer, TrackInfo}; +use crate::media::remux::{ + browser_video_track, television_audio_tracks, FileInfo, MkvDemuxer, TrackInfo, +}; use crate::media::transcode::ProgressiveStream; use crate::{database::DatabaseManager, error::AppError, state::AppState}; @@ -59,10 +69,10 @@ const DLNA_FLAGS: &str = "DLNA.ORG_FLAGS=01700000000000000000000000000000"; /// `?t=` for seeking, `?audio_track=` to carry one track alone. /// -/// The index is into [`browser_audio_tracks`], the same order the HLS renditions -/// use. Omitted — which is how a television reaches this, since the DIDL never -/// writes the parameter — every playable track is carried and the renderer -/// chooses between them itself. +/// The index is into [`television_audio_tracks`], in container order. Omitted — +/// which is how a television reaches this, since the DIDL never writes the +/// parameter — every carryable track is included and the renderer chooses +/// between them itself. #[derive(serde::Deserialize, Default)] pub struct VideoQuery { t: Option, @@ -77,24 +87,52 @@ pub async fn serve_transcoded_video( Query(query): Query, headers: HeaderMap, ) -> Result { - let (path, filename, info) = resolve(&state, &id).await?; + crate::web::client::log_renderer_request( + &format!("/media/{id}/transcode/video.mp4"), + &method, + &headers, + ); + let (path, size, filename, info) = resolve(&state, &id).await?; let video = browser_video_track(&info.tracks) .ok_or(AppError::NotFound)? .clone(); let audio = audio_tracks(&info.tracks, query.audio_track); let duration = info.duration_secs.filter(|d| *d > 0.0); - // A time seek asked for in a header is a range request and is answered as - // one. `?t=` is not: it is how the browser player and a test name a starting - // point, and it gets a plain 200 — a 206 nobody asked for is a response to a - // range request that was never made. - let seek_header = headers - .get("TimeSeekRange.dlna.org") - .or_else(|| headers.get("timeseekrange.dlna.org")) - .or_else(|| headers.get(header::RANGE)) - .and_then(|value| value.to_str().ok()) - .and_then(parse_npt_start); - let requested = seek_header.or(query.t); + // The length this response commits to, and which its body is then made to + // be exactly. See the module documentation for why it is the source's. + let promised = crate::web::promised_transcode_length(size); + + // A renderer given a length probes the end of it before it plays. An MP4 + // reader looks there for the `moov` a progressive file carries at its tail, + // and an Android television does it as sixteen bytes off the end — which + // this resource can answer exactly, because what is really at the end of it + // is the padding that makes the promised length true, and padding is + // zeroes. Answering it by producing the film would take a transcode slot + // and thirty gigabytes of work to hand back sixteen bytes of nothing, and + // answering it with the whole film from the start — which is what ignoring + // the range does — is a reply the renderer cannot make sense of at all, so + // it gives up before playing a frame. + if let Some((first, last)) = header_value(&headers, "range").and_then(parse_byte_range) { + if first >= size && first < promised { + return padding_response(first, last, promised); + } + } + + // Time seeks only, and this is the hard-won part. A byte offset into this + // resource does not mean anything stable: the bytes are produced on demand, + // so offset X is whatever the muxer happened to emit that time round. Answer + // a range request positionally and the client splices two different + // generations of the stream together and decodes noise — and it is worse + // than that, because a client told `Accept-Ranges: bytes` will seek while + // merely *parsing*, so even straight playback comes apart. A time seek has + // none of that: it is a whole new response the renderer knows to start over + // on, which is exactly what a fresh `ftyp`/`moov` needs it to do. + let time_seek = header_value(&headers, "timeseekrange.dlna.org").and_then(parse_npt_start); + // A `Range` that arrives anyway is answered from the beginning rather than + // positionally. HTTP allows a server to ignore a range, and a stream from + // the wrong place is worse than a stream from the start. + let requested = time_seek.or(query.t); // A seek past the end is clamped rather than refused: a renderer that has // drifted a little past a film's declared duration should see the last // moment of it, not an error. @@ -103,9 +141,19 @@ pub async fn serve_transcoded_video( .max(0.0) .min(duration.map(|d| (d - 0.1).max(0.0)).unwrap_or(f64::MAX)); + // Where this response sits in the promised byte space. Not a position a + // renderer may ask for — only the answer to "how much is left", which is + // what a scrub bar needs once it has seeked. + let first_byte = match duration { + Some(duration) if time_seek.is_some() && duration > 0.0 => { + ((promised as f64 * start / duration) as u64).min(promised.saturating_sub(1)) + } + _ => 0, + }; + tracing::debug!( "transcoded video: id={id}, file={filename}, start={start:.3}s, \ - requested={requested:?}, audio={:?}", + promised={promised}, audio={:?}", audio .iter() .map(|track| (track.id, track.language.as_deref(), track.name.as_deref())) @@ -113,45 +161,42 @@ pub async fn serve_transcoded_video( ); let mut response = Response::builder() - .status(if seek_header.is_some() { + .status(if time_seek.is_some() { StatusCode::PARTIAL_CONTENT } else { StatusCode::OK }) .header(header::CONTENT_TYPE, "video/mp4") .header(header::CACHE_CONTROL, "no-cache") + // Explicitly not `bytes`. Saying nothing here is not enough: some + // clients probe with a range request and take a positional-looking + // answer as proof, so the refusal is stated. + .header(header::ACCEPT_RANGES, "none") .header("transferMode.dlna.org", "Streaming") // `DLNA.ORG_OP=ab` is two independent answers: `a` is whether - // `TimeSeekRange.dlna.org` is honoured and `b` is whether byte ranges - // are. So this is time seek yes, byte seek no — and the order matters - // more than anything else in this header, because `01` says the - // opposite. A renderer told it may byte-seek a resource with no length - // and no `Accept-Ranges` sends `Range: bytes=` for every scrub, gets the - // film from the beginning each time, and concludes the file cannot be - // seeked at all. + // `TimeSeekRange.dlna.org` is honoured, `b` whether byte ranges are. + // Time yes, bytes no — see above for why claiming bytes breaks even + // playback, never mind seeking. .header( "contentFeatures.dlna.org", format!("DLNA.ORG_OP=10;DLNA.ORG_CI=1;{DLNA_FLAGS}"), - ); + ) + // A length, even though the resource is produced on demand: it is what a + // renderer divides to draw a scrub bar, and one given none mostly draws + // nothing and refuses to seek at all. The body is made to be exactly + // this long — see `fmp4_body`. + .header(header::CONTENT_LENGTH, promised - first_byte); if let Some(duration) = duration { - // A renderer with no length to divide has nothing else to draw a scrub - // bar from. `mehd` in the init segment says the same thing; this is for - // the ones that read headers and not boxes. response = response.header("X-Content-Duration", format!("{duration:.3}")); // The range actually being answered, which DLNA asks for in reply to a // request that named one — and only then, since it is an answer rather // than an announcement. - if seek_header.is_some() { + if time_seek.is_some() { response = response.header( "TimeSeekRange.dlna.org", format!("npt={start:.3}-{duration:.3}/{duration:.3}"), ); } - // Not a DLNA header — DLNA's own `availableSeekRange.dlna.org` belongs - // to limited-operation content, which this is not. This is the - // PlayStation spelling of the same statement, read by a handful of - // renderers and ignored by the rest, and what it states is true: the - // whole film is reachable, from the first moment to the last. response = response.header("X-AvailableSeekRange", format!("1 npt=0.000-{duration:.3}")); } @@ -166,11 +211,20 @@ pub async fn serve_transcoded_video( return Ok(busy(&state, &filename)); }; - Ok(response.body(fmp4_body(path, video, audio, start, duration, permit))?) + let deliver = promised - first_byte; + Ok(response.body(fmp4_body(path, video, audio, start, duration, deliver, permit))?) } /// Mux the film on a blocking thread, handing fragments over a bounded channel. /// +/// Exactly `deliver` bytes reach the socket, whatever the muxer produces. That +/// is the price of having promised a length for something that does not exist +/// yet, and it is paid at the tail: output that falls short is padded out with +/// `free` boxes, which ISO-BMFF defines as filler a reader skips, and output +/// that runs over is cut at the promise. Both happen after the film's last +/// picture has gone out, so what is padded or lost is the end of a stream the +/// renderer has already finished watching. +/// /// The permit rides along and is released when the body is dropped — which is /// also what happens when a television disconnects, or seeks, mid-film. fn fmp4_body( @@ -179,31 +233,37 @@ fn fmp4_body( audio: Vec, start: f64, duration: Option, + deliver: u64, permit: tokio::sync::OwnedSemaphorePermit, ) -> Body { let (tx, rx) = tokio::sync::mpsc::channel::>(PIPELINE_DEPTH); tokio::task::spawn_blocking(move || { let _permit = permit; + let mut sent: u64 = 0; let mut stream = match ProgressiveStream::open(&path, &video, &audio, start, duration) { Ok(stream) => stream, - Err(e) => { - let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string()))); + Err(error) => { + warn!("cannot open {} for remuxing: {error:#}", path.display()); + let _ = tx.blocking_send(Err(std::io::Error::other(error.to_string()))); return; } }; - if tx - .blocking_send(Ok(bytes::Bytes::from(stream.init_segment()))) - .is_err() - { - return; + if send_capped(&tx, &mut sent, deliver, stream.init_segment()) { + while let Some(fragment) = stream.next_fragment() { + if !send_capped(&tx, &mut sent, deliver, fragment) { + return; + } + } } - while let Some(fragment) = stream.next_fragment() { - if tx - .blocking_send(Ok(bytes::Bytes::from(fragment))) - .is_err() - { + + // The film is over and the promise is not met. Fill the difference with + // skippable boxes rather than leaving the transfer short, which is what + // a renderer reports as a failed download. + while sent < deliver { + let remaining = deliver - sent; + if !send_capped(&tx, &mut sent, deliver, free_box(remaining)) { return; } } @@ -212,11 +272,52 @@ fn fmp4_body( Body::from_stream(tokio_stream::wrappers::ReceiverStream::new(rx)) } +/// Hand over `chunk`, trimmed to whatever is left of the promised length. +/// +/// `false` means stop, for either of the two reasons there are: the quota is +/// met, or the renderer has gone away. +fn send_capped( + tx: &tokio::sync::mpsc::Sender>, + sent: &mut u64, + deliver: u64, + mut chunk: Vec, +) -> bool { + let room = deliver.saturating_sub(*sent); + if room == 0 { + return false; + } + if chunk.len() as u64 > room { + chunk.truncate(room as usize); + } + *sent += chunk.len() as u64; + tx.blocking_send(Ok(bytes::Bytes::from(chunk))).is_ok() && *sent < deliver +} + +/// Filler occupying up to `remaining` bytes. +/// +/// A `free` box is ISO-BMFF's own "ignore this": a length, the tag, and nothing +/// that means anything. Capped so a long tail is sent as several boxes rather +/// than one allocation the size of the shortfall, and a remainder too small to +/// hold even a box header goes out as plain zeroes — which sit past the last +/// box a reader will ever look at. +fn free_box(remaining: u64) -> Vec { + const CHUNK: u64 = 1 << 20; + if remaining < 8 { + return vec![0u8; remaining as usize]; + } + let len = remaining.min(CHUNK); + let mut out = Vec::with_capacity(len as usize); + out.extend_from_slice(&(len as u32).to_be_bytes()); + out.extend_from_slice(b"free"); + out.resize(len as usize, 0); + out +} + /// Look the item up and confirm it is a film with a track worth decoding. async fn resolve( state: &AppState, id: &str, -) -> Result<(std::path::PathBuf, String, FileInfo), AppError> { +) -> Result<(std::path::PathBuf, u64, String, FileInfo), AppError> { let Some(file_id) = media_id_from_path_segment(id) else { return Err(AppError::NotFound); }; @@ -233,7 +334,7 @@ async fn resolve( debug!("cannot inspect {} for remuxing: {error}", file.filename); AppError::NotFound })?; - Ok((file.path, file.filename, info)) + Ok((file.path, file.size, file.filename, info)) } /// Which of a film's soundtracks to carry, and in what order. @@ -246,11 +347,13 @@ async fn resolve( /// the audio menu sees them as the file lists them. /// /// `only` restricts the answer to one track, by index into -/// [`browser_audio_tracks`] — the same order the HLS renditions are numbered in. -/// That is for the browser player and for narrowing down a report of a bad -/// track; a television never sends it. -fn audio_tracks(tracks: &[TrackInfo], only: Option) -> Vec { - let playable = browser_audio_tracks(tracks); +/// [`television_audio_tracks`] — the tracks this resource can carry, in +/// container order. That is for narrowing down a report of a bad soundtrack; a +/// television never sends it. Note that it indexes this resource's own list, +/// which is wider than the HLS renditions': a browser cannot take Dolby at all, +/// so its numbering skips tracks that appear here. +pub(crate) fn audio_tracks(tracks: &[TrackInfo], only: Option) -> Vec { + let playable = television_audio_tracks(tracks); if let Some(index) = only { return playable.get(index).copied().cloned().into_iter().collect(); } @@ -276,6 +379,73 @@ fn busy(state: &AppState, filename: &str) -> Response { .into_response() } +/// One header, by its lowercase name. +fn header_value<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { + headers.get(name).and_then(|value| value.to_str().ok()) +} + +/// The bounds a `Range: bytes=` header names, as an inclusive pair. +/// +/// Only the single-range form, which is all any renderer sends. A suffix range +/// (`bytes=-500`) is declined rather than resolved: it means "the last 500 +/// bytes", and answering it would commit to the promised length being where the +/// content really ends. +fn parse_byte_range(header: &str) -> Option<(u64, Option)> { + let spec = header.trim().strip_prefix("bytes=")?; + if spec.contains(',') { + return None; + } + let (first, last) = spec.split_once('-')?; + let first = first.trim().parse::().ok()?; + let last = match last.trim() { + "" => None, + value => Some(value.parse::().ok()?), + }; + Some((first, last)) +} + +/// Answer a range that falls inside the padding, without producing anything. +/// +/// The bytes really are zeroes, so this is not a fiction — it is the one part of +/// this resource whose contents are known without muxing a frame. It takes no +/// transcode slot, because there is nothing here to transcode. +fn padding_response( + first: u64, + last: Option, + promised: u64, +) -> Result { + /// Zeroes handed over at a time, so a renderer asking for a large stretch of + /// padding does not become a large allocation. + const CHUNK: u64 = 64 * 1024; + + let last = last.unwrap_or(promised - 1).min(promised - 1); + if last < first { + return Ok(Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, format!("bytes */{promised}")) + .body(Body::empty())?); + } + let length = last - first + 1; + let zeroes = futures_util::stream::unfold(length, |left| async move { + if left == 0 { + return None; + } + let take = left.min(CHUNK); + let chunk = bytes::Bytes::from(vec![0u8; take as usize]); + Some((Ok::<_, std::io::Error>(chunk), left - take)) + }); + + Ok(Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header(header::CONTENT_TYPE, "video/mp4") + .header(header::CONTENT_LENGTH, length) + .header( + header::CONTENT_RANGE, + format!("bytes {first}-{last}/{promised}"), + ) + .body(Body::from_stream(zeroes))?) +} + /// The start time of a `TimeSeekRange.dlna.org` header, in seconds. /// /// Two spellings are legal and both turn up: decimal seconds (`npt=120.5-`) and diff --git a/crates/vuio-core/src/web/xml/browse.rs b/crates/vuio-core/src/web/xml/browse.rs index ce9489ec..848e0173 100644 --- a/crates/vuio-core/src/web/xml/browse.rs +++ b/crates/vuio-core/src/web/xml/browse.rs @@ -264,15 +264,26 @@ pub async fn generate_browse_response( file.mime_type.contains("dts") || file.filename.to_ascii_lowercase().ends_with(".dts") }); + // Whether to hide the original, not whether to offer the decoded + // resource. The decoded resource is offered either way — a renderer + // that cannot play the original and is shown nothing else plays + // nothing at all, which is the failure this whole path exists to + // prevent. What `is_dts` decides is only whether the original is + // worth leaving beside it: a television that cannot license DTS is + // certain not to have it, where AC-3 is common enough that hiding + // the original would take away a working choice. let is_forced = transcoded.as_ref().is_some_and(|a| a.first && is_dts); - if let Some(advert) = transcoded.filter(|a| a.first && is_dts) { + if let Some(advert) = transcoded.filter(|a| a.first) { let _ = advert.write_didl( &mut didl, server_ip, state.http_binding.port(), - file_id, - &file.mime_type, - duration_secs, + &AdvertItem { + file_id, + mime: &file.mime_type, + duration: duration_secs, + source_size: file.size, + }, ); } @@ -315,9 +326,12 @@ pub async fn generate_browse_response( &mut didl, server_ip, state.http_binding.port(), - file_id, - &file.mime_type, - duration_secs, + &AdvertItem { + file_id, + mime: &file.mime_type, + duration: duration_secs, + source_size: file.size, + }, ); } } diff --git a/crates/vuio-core/src/web/xml/rendering.rs b/crates/vuio-core/src/web/xml/rendering.rs index 93c730f3..3bf872a0 100644 --- a/crates/vuio-core/src/web/xml/rendering.rs +++ b/crates/vuio-core/src/web/xml/rendering.rs @@ -162,10 +162,38 @@ pub struct AdvertResource { /// Stated per resource, and it differs: constant-bitrate LPCM divides a byte /// offset straight back into a sample, so it is `11`; a re-encoded AAC /// stream has no length to seek within, so it is `00`; a remuxed film is - /// `01`, time seek only. A resource that claims an operation it cannot - /// perform is worse than one that claims none — a renderer which byte-seeks - /// and gets nothing usable stops playing rather than falling back. + /// `10`, time seek only, because a byte offset into a stream produced on + /// demand does not name a fixed place in it. pub op: &'static str, + /// Whether the `` states a `size`. + /// + /// None of them do, and for three different reasons. Decoded LPCM has an + /// exact length that only its plan knows; re-encoded AAC has no length at + /// all until it exists; and the remuxed film has one the streaming handler + /// works out from the file's soundtracks and then makes true by padding the + /// body — which a browse response must not open the file to learn. A `size` + /// here would be a second, worse answer to a question the response itself + /// answers exactly. + /// + /// Kept as a field rather than deleted because it is the shape of the + /// question, and a resource with a length known up front would state it. + pub sized: bool, +} + +/// The item a decoded alternative is being written for. +/// +/// Grouped rather than passed loose because every one of these is a property of +/// the same row, and a `` writer taking seven positional arguments is one +/// transposed pair away from advertising a film's duration as its size. +#[derive(Clone, Copy, Debug)] +pub(crate) struct AdvertItem<'a> { + pub file_id: i64, + pub mime: &'a str, + pub duration: Option, + /// The stored file's size. Not written into any `` today — see + /// [`AdvertResource::sized`] — and kept because it is what a resource with a + /// length known from the index would be sized from. + pub source_size: u64, } /// How a decoded alternative resource should be advertised. @@ -198,26 +226,17 @@ impl TranscodeAdvert { } } - /// Write the decoded alternative for `file_id`. + /// Write the decoded alternative for `item`. fn write( &self, output: &mut W, context: &BrowseRenderContext, - file_id: i64, - mime: &str, - duration: Option, + item: &AdvertItem<'_>, ) -> std::fmt::Result { - self.write_didl( - output, - &context.server_ip, - context.server_port, - file_id, - mime, - duration, - ) + self.write_didl(output, &context.server_ip, context.server_port, item) } - /// The same, for the fallback writer, which carries its parts loose rather + /// The same, for the fallback writer, which carries its address loose rather /// than in a context. /// /// `DLNA.ORG_CI=1` is the conversion indicator: a renderer matching on @@ -228,10 +247,14 @@ impl TranscodeAdvert { output: &mut W, server_ip: &str, server_port: u16, - file_id: i64, - mime: &str, - duration: Option, + item: &AdvertItem<'_>, ) -> std::fmt::Result { + let &AdvertItem { + file_id, + mime, + duration, + source_size, + } = item; let Some(resource) = self.resource_for(mime) else { return Ok(()); }; @@ -240,6 +263,13 @@ impl TranscodeAdvert { r#"( .unwrap_or_else(|| { mime.contains("dts") || file.filename().to_ascii_lowercase().ends_with(".dts") }); + // See `browse.rs`: this decides whether to hide the original, not whether to + // offer the decoded resource. The decoded resource is offered either way. let is_forced = transcoded.as_ref().is_some_and(|a| a.first && is_dts); let item_duration = file.duration_secs().map(|value| value as u64); - if let Some(advert) = transcoded.filter(|a| a.first && is_dts) { - advert.write(output, context, file_id, mime, item_duration)?; + if let Some(advert) = transcoded.filter(|a| a.first) { + advert.write( + output, + context, + &AdvertItem { + file_id, + mime, + duration: item_duration, + source_size: file.size(), + }, + )?; } if !is_forced { @@ -568,7 +609,16 @@ pub(super) fn write_media_view( context.server_ip, context.server_port, file_id )?; if let Some(advert) = transcoded.filter(|a| !a.first) { - advert.write(output, context, file_id, mime, item_duration)?; + advert.write( + output, + context, + &AdvertItem { + file_id, + mime, + duration: item_duration, + source_size: file.size(), + }, + )?; } } if context.client == crate::web::client::DlnaClientProfile::LgTv && has_srt { diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs index 03723bde..ca0ea0e7 100644 --- a/crates/vuio-core/tests/film_transcode_tests.rs +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -356,7 +356,7 @@ fn boxes(data: &[u8]) -> Vec<(String, &[u8])> { fn find_box<'a>(data: &'a [u8], name: &str) -> Option<&'a [u8]> { const CONTAINERS: &[&str] = &[ "moov", "trak", "mdia", "minf", "stbl", "stsd", "mvex", "moof", "traf", "avc1", "hvc1", - "mp4a", + "mp4a", "ac-3", "ec-3", ]; for (found, body) in boxes(data) { if found == name { @@ -368,7 +368,9 @@ fn find_box<'a>(data: &'a [u8], name: &str) -> Option<&'a [u8]> { let inner = match found.as_str() { "stsd" => &body[8..], "avc1" | "hvc1" => &body[78..], - "mp4a" => &body[28..], + // Every audio sample entry shares the same 28-byte preamble + // before its own configuration box. + "mp4a" | "ac-3" | "ec-3" => &body[28..], _ => body, }; if let Some(hit) = find_box(inner, name) { @@ -652,20 +654,40 @@ async fn the_remuxed_film_is_a_parseable_fmp4_with_both_tracks() { assert!(features.contains("DLNA.ORG_CI=1"), "{features}"); assert!( features.contains("DLNA.ORG_OP=10"), - "time seek yes, byte seek no: {features}" + "time seek yes, byte seek no — a byte offset into a stream produced on demand does not name a fixed place in it: {features}" ); - assert!( - !headers.contains_key(header::ACCEPT_RANGES), - "claiming byte ranges and then refusing them is worse than never claiming" + assert_eq!( + headers[header::ACCEPT_RANGES], + "none", + "stated rather than merely omitted: a client that probes with a range request and gets a positional-looking answer concludes ranges work, then seeks while parsing and splices two generations of the stream" ); - assert!( - !headers.contains_key(header::CONTENT_LENGTH), - "the length of this resource is not knowable before it exists" + // The length is a promise the body is then made to keep exactly. + let promised: usize = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + assert_eq!( + body.len(), + promised, + "the body must be exactly as long as the response promised" ); let top: Vec = boxes(&body).into_iter().map(|(name, _)| name).collect(); assert_eq!(&top[..2], &["ftyp", "moov"], "an init segment comes first"); - let fragments = top[2..].chunks(2).collect::>(); + // Then moof/mdat pairs, and then however many `free` boxes it takes to + // reach the length the response promised. The padding is what lets this + // resource state a length at all, and a renderer skips it — mostly without + // fetching it, since it stops at the duration the `moov` declared. + let padding = top + .iter() + .position(|name| name == "free") + .unwrap_or(top.len()); + assert!( + top[padding..].iter().all(|name| name == "free"), + "nothing may follow the padding: {top:?}" + ); + let fragments = top[2..padding].chunks(2).collect::>(); assert!( fragments.len() >= 2, "expected several moof/mdat pairs, got {top:?}" @@ -673,6 +695,7 @@ async fn the_remuxed_film_is_a_parseable_fmp4_with_both_tracks() { for pair in &fragments { assert_eq!(pair, &["moof", "mdat"], "in {top:?}"); } + assert!(padding < top.len(), "this film should be padded: {top:?}"); // Two tracks, and the picture must arrive as the picture: the source's own // decoder configuration record, byte for byte. That is what "passthrough" @@ -689,9 +712,20 @@ async fn the_remuxed_film_is_a_parseable_fmp4_with_both_tracks() { Some(AVCC), "the video track is copied, not re-encoded" ); + // AC-3 is handed over as AC-3: a television is what Dolby Digital was + // built for, and a stereo AAC downmix would throw away the 5.1 it plays. + assert!( + find_box(&body, "ac-3").is_some(), + "the AC-3 track must be passed through, not re-encoded" + ); + assert_eq!( + find_box(&body, "dac3").map(<[u8]>::len), + Some(3), + "an ac-3 sample entry is nothing without the record describing it" + ); assert!( - find_box(&body, "mp4a").is_some(), - "the AC-3 track must arrive as AAC — nothing else would play" + find_box(&body, "mp4a").is_none(), + "nothing here needed re-encoding" ); // `mehd` is what a fragmented file carries its total duration in, and what a // renderer with no Content-Length draws a scrub bar from. @@ -710,6 +744,17 @@ async fn a_head_describes_the_film_without_decoding_it() { assert_eq!(status, StatusCode::OK); assert!(body.is_empty()); assert_eq!(headers[header::CONTENT_TYPE], "video/mp4"); + // A renderer probes with HEAD before it plays, and a resource with no + // length — or worse, a length of zero — is one it mostly declines to draw a + // scrub bar for. The promise made here is the one a GET then keeps. + let promised = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .to_string(); + assert_ne!(promised, "0", "HEAD must not describe an empty resource"); + let (_, get_headers, get_body) = video_mp4(&state, id, Method::GET, None).await; + assert_eq!(get_headers[header::CONTENT_LENGTH], promised); + assert_eq!(get_body.len().to_string(), promised); assert_eq!( headers["X-Content-Duration"] .to_str() @@ -763,6 +808,116 @@ async fn a_time_seek_starts_the_film_where_it_was_asked_to() { ); } +/// What an Android television does before it plays anything: read sixteen bytes +/// off the end of the file, looking for the `moov` a progressive MP4 carries at +/// its tail. +/// +/// Answering that by producing the film would take a transcode slot and, on a +/// thirty-gigabyte remux, an enormous amount of work to hand back sixteen bytes. +/// Answering it with the whole film from the beginning — which is what ignoring +/// the range does — is a reply the set cannot make sense of, and it gives up +/// before playing a frame. So it is answered from the padding, which is the one +/// part of this resource whose contents are known without muxing anything. +#[tokio::test] +async fn the_tail_a_renderer_probes_is_answered_without_producing_the_film() { + let (_temp, state, id) = scanned_film(8.0).await; + + // The length the resource commits to, which is what the renderer counts + // back from. + let (_, headers, whole) = video_mp4(&state, id, Method::GET, None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + let first = promised - 16; + let response = create_router(state.clone(), Surface::Primary) + .oneshot( + Request::builder() + .method(Method::GET) + .uri(format!("/media/{id}/transcode/video.mp4")) + .header(header::RANGE, format!("bytes={first}-")) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + assert_eq!( + response.headers()[header::CONTENT_RANGE].to_str().unwrap(), + format!("bytes {first}-{}/{promised}", promised - 1) + ); + let body = axum::body::to_bytes(response.into_body(), 1 << 20) + .await + .unwrap(); + assert_eq!(body.len(), 16); + // And it is the truth rather than a convenient reply: these are the bytes a + // whole download really ends with. + assert_eq!( + &body[..], + &whole[whole.len() - 16..], + "the padding answer must match what the stream actually ends with" + ); +} + +/// The defect that made three rounds of seek fixes pointless: a byte offset into +/// this resource does not name a fixed place in it, because the bytes are +/// produced on demand and offset X is whatever the muxer emitted that time. +/// +/// Answering a `Range` positionally therefore hands the client a fresh +/// `ftyp`/`moov` where it expected the continuation of what it was already +/// reading, and it decodes the join as noise. Worse, a client told +/// `Accept-Ranges: bytes` seeks while merely *parsing*, so straight playback +/// comes apart too — the stream is corrupt before anyone touches a scrub bar. +/// +/// So a range is answered from the beginning, which HTTP explicitly allows, and +/// the refusal is advertised rather than left to be discovered. +#[tokio::test] +async fn a_byte_range_is_answered_from_the_beginning_rather_than_positionally() { + let (_temp, state, id) = scanned_film(12.0).await; + + let response = create_router(state.clone(), Surface::Primary) + .oneshot( + Request::builder() + .method(Method::GET) + .uri(format!("/media/{id}/transcode/video.mp4")) + .header(header::RANGE, "bytes=100000-") + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!( + response.status(), + StatusCode::OK, + "a range this resource cannot honour must not be answered as though it had been" + ); + assert!( + !response.headers().contains_key(header::CONTENT_RANGE), + "no positional claim may be made about a stream produced on demand" + ); + + let body = axum::body::to_bytes(response.into_body(), 256 * 1024 * 1024) + .await + .unwrap(); + assert_eq!(&body[4..8], b"ftyp", "a whole stream, from its start"); + let tfdt = find_box(&body, "tfdt").expect("a tfdt in the first fragment"); + let seconds = u64::from_be_bytes(tfdt[4..12].try_into().unwrap()) as f64 / 90_000.0; + assert!( + seconds < 0.5, + "the range was honoured positionally after all: started at {seconds:.3}s" + ); +} + /// A `206` is an answer to a range request. `?t=` is not one — it is how the /// browser player and these tests name a starting point — so it gets a plain /// `200`, and no `TimeSeekRange.dlna.org` stating a range nobody asked about. @@ -1006,14 +1161,15 @@ async fn every_soundtrack_carries_samples_through_the_whole_film() { fragments >= 2, "expected several fragments, got {fragments}" ); - // Six seconds of 48 kHz audio is 281 AAC frames of 1024 samples, and each - // track should carry very nearly all of them — the first fragment or two - // are lost to the wait for the video's first keyframe. + // These are AC-3 syncframes now, passed through rather than re-encoded: + // 1536 samples each, so six seconds of 48 kHz is 187 or 188 of them, and + // each track should carry very nearly all of them — the first fragment or + // two are lost to the wait for the video's first keyframe. for track_id in [2u32, 3, 4] { let count = samples.get(&track_id).copied().unwrap_or(0); assert!( - (200..=290).contains(&count), - "track {track_id} carried {count} AAC frames across the film: {samples:?}" + (170..=190).contains(&count), + "track {track_id} carried {count} frames across the film: {samples:?}" ); } let video = samples.get(&1).copied().unwrap_or(0); @@ -1062,7 +1218,12 @@ fn traks_by_kind(body: &[u8]) -> Vec<(&'static str, &[u8])> { .into_iter() .filter(|(name, _)| name == "trak") .map(|(_, trak)| { - let kind = if find_box(trak, "mp4a").is_some() { + // From the `hdlr` handler type, not the sample entry: which sample + // entry an audio track carries is the thing under test — `mp4a` for + // one re-encoded, `ac-3` or `ec-3` for one passed through. + let hdlr = find_box(trak, "hdlr").expect("a hdlr"); + // version+flags(4) + pre_defined(4), then the handler type. + let kind = if &hdlr[8..12] == b"soun" { "audio" } else { "video" @@ -1159,7 +1320,7 @@ async fn a_film_advertises_the_remuxed_film_and_not_its_soundtrack() { let didl = browse_didl(&state).await; assert!( - didl.contains(&format!("/media/{id}/transcode/video.mp4")), + didl.contains(&format!("/media/{id}/transcode/video.ts")), "a film's alternative is the film:\n{didl}" ); assert!( @@ -1167,15 +1328,15 @@ async fn a_film_advertises_the_remuxed_film_and_not_its_soundtrack() { "offering a film's soundtrack in place of the film would lose the picture:\n{didl}" ); assert!( - didl.contains("DLNA.ORG_OP=10;DLNA.ORG_CI=1"), - "the advertised operations must be the ones the resource honours:\n{didl}" + didl.contains("DLNA.ORG_OP=11;DLNA.ORG_CI=1"), + "a transport stream resynchronises wherever it is joined, so both seek modes are honest here:\n{didl}" ); // The original stays, and stays first by default, so a television that can // decode AC-3 keeps its byte-seekable direct-play resource. let original = didl .find(&format!("/media/{id}")) .unwrap_or_else(|| panic!("no direct-play resource in:\n{didl}")); - let transcoded = didl.find("transcode/video.mp4").unwrap(); + let transcoded = didl.find("transcode/video.ts").unwrap(); assert!( original < transcoded, "the original is listed first:\n{didl}" @@ -1234,6 +1395,44 @@ async fn with_the_feature_off_a_film_has_exactly_one_resource() { assert!(!didl.contains("transcode/"), "{didl}"); } +/// `mode = "forced"` must still offer the decoded resource for a film whose +/// audio is not DTS. +/// +/// The forcing rule is about whether to *hide* the original, and only DTS earns +/// that — a television that cannot license DTS certainly has no decoder for it, +/// where AC-3 is common enough that hiding the original would take away a +/// working choice. Gating the decoded resource itself on the same test left an +/// AC-3 film in forced mode with no decoded resource at all, which is the exact +/// case this whole path exists for. +#[tokio::test] +async fn a_forced_film_still_offers_the_transcode_when_its_audio_is_not_dts() { + let (_temp, mut state, _) = scanned_film(4.0).await; + let mut config = (*state.config).clone(); + config.transcode.mode = vuio_core::config::TranscodeMode::Forced; + let config = Arc::new(config); + state.config = config.clone(); + state.live_config = Arc::new(vuio_core::state::LiveConfig::new(config)); + + let didl = browse_didl(&state).await; + assert!( + didl.contains("transcode/video.ts"), + "an AC-3 film in forced mode must still be offered the remux:\n{didl}" + ); + // And the original stays beside it, because AC-3 is not DTS. + assert!( + didl.contains("video/x-matroska") || didl.contains("video/x-mkv"), + "the original must not be hidden for AC-3:\n{didl}" + ); + // Forced means the decoded resource leads. `DLNA.ORG_CI` is what separates + // them: 1 is converted, 0 is the file as it is stored. + let converted = didl.find("DLNA.ORG_CI=1").expect("the decoded resource"); + let stored = didl.find("DLNA.ORG_CI=0").expect("the original"); + assert!( + converted < stored, + "the forced resource must be listed first:\n{didl}" + ); +} + /// Browse the root folder as a television would, and return the DIDL. async fn browse_didl(state: &vuio_core::state::AppState) -> String { let body = r#" @@ -1311,6 +1510,96 @@ fn dump_fixture() { eprintln!("wrote {out}"); } +/// Not a test: prints what this server would commit to for a film on disk, so a +/// real library can be checked without a television in the room. +/// `VUIO_FILM=/path/to/Film.mkv cargo test --all-features describe_a_real_film -- --ignored --nocapture` +#[test] +#[ignore] +fn describe_a_real_film() { + use vuio_core::media::remux::{ + browser_video_track, television_audio_tracks, MkvDemuxer, TrackKind, + }; + use vuio_core::media::transcode::{audio_disposition, measure_track_rates, promised_ts_length}; + + let path = std::env::var("VUIO_FILM").expect("set VUIO_FILM to the film to describe"); + let path = std::path::PathBuf::from(path); + let size = std::fs::metadata(&path).unwrap().len(); + + let probed = std::time::Instant::now(); + let info = MkvDemuxer::inspect(&path).expect("inspect the film"); + let duration = info.duration_secs.unwrap_or(0.0); + eprintln!( + "{}\n {size} bytes, {duration:.1}s, inspected in {:.2}s", + path.display(), + probed.elapsed().as_secs_f64() + ); + + let video = browser_video_track(&info.tracks); + match video { + Some(track) => eprintln!(" video: {} ({:?})", track.codec, track.codec_kind), + None => eprintln!(" video: NONE this build can copy through — no .ts is offered"), + } + + let measured = std::time::Instant::now(); + let rates = measure_track_rates(&path, &info.tracks).expect("measure the film"); + eprintln!(" measured in {:.2}s", measured.elapsed().as_secs_f64()); + + for track in &info.tracks { + let rate = rates.get(track.id); + let shape = match rate { + Some(rate) => format!( + "{:.0} kbps, {:.2} fps", + rate.bits_per_second as f64 / 1000.0, + rate.frames_per_second + ), + None => "not measured".to_string(), + }; + let role = if track.track_kind == TrackKind::Audio { + format!("{:?}", audio_disposition(track)) + } else { + format!("{:?}", track.track_kind) + }; + eprintln!( + " track {} {:?} {}ch lang={} — {role}, {shape}", + track.id, + track.codec_kind, + track.channels.unwrap_or(0), + track.language.as_deref().unwrap_or("und") + ); + } + + let carried: Vec<_> = television_audio_tracks(&info.tracks) + .into_iter() + .cloned() + .collect(); + let promised = promised_ts_length(size, duration, &info.tracks, &carried, &rates); + let old = size + (size / 16); + eprintln!( + " promised {promised} bytes ({:.1}% of source, {:.2} Mbps)\n \ + the source-sized promise would have been {old} ({:.1}%), so a byte offset \ + would have named a moment {:.2}x too far along", + promised as f64 * 100.0 / size as f64, + promised as f64 * 8.0 / duration / 1e6, + old as f64 * 100.0 / size as f64, + old as f64 / promised as f64, + ); +} + +/// Not a test: writes a DTS film out so a real server can be pointed at it. +/// `cargo test --all-features dump_dts_fixture -- --ignored` +#[cfg(feature = "transcode-dts")] +#[test] +#[ignore] +fn dump_dts_fixture() { + let out = std::env::var("VUIO_FIXTURE_OUT").unwrap_or_else(|_| "/tmp/vuio-dts.mkv".into()); + let seconds: f64 = std::env::var("VUIO_FIXTURE_SECS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(60.0); + std::fs::write(&out, dts_film(seconds)).unwrap(); + eprintln!("wrote {out}"); +} + #[test] #[ignore] fn dump_long_fixture() { @@ -1330,3 +1619,884 @@ async fn dump_multi_audio() { std::fs::write(&out, &body).unwrap(); eprintln!("wrote {} bytes to {out}", body.len()); } + +// ── Step 6: the transport stream a television can seek ──────────────────── + +async fn video_ts( + state: &vuio_core::state::AppState, + id: i64, + query: &str, + range: Option<&str>, +) -> (StatusCode, axum::http::HeaderMap, Vec) { + let mut builder = Request::builder() + .method(Method::GET) + .uri(format!("/media/{id}/transcode/video.ts{query}")) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )); + if let Some(range) = range { + builder = builder.header(header::RANGE, range); + } + let response = create_router(state.clone(), Surface::Primary) + .oneshot(builder.body(Body::empty()).unwrap()) + .await + .unwrap(); + let status = response.status(); + let headers = response.headers().clone(); + let body = axum::body::to_bytes(response.into_body(), 256 * 1024 * 1024) + .await + .unwrap() + .to_vec(); + (status, headers, body) +} + +const TS_PACKET: usize = 188; + +/// Every packet in a transport stream begins with a sync byte, and that is the +/// whole reason a decoder can join one part way through. +fn assert_well_formed_ts(body: &[u8]) { + assert_eq!(body.len() % TS_PACKET, 0, "packets must tile the stream"); + for (index, packet) in body.chunks(TS_PACKET).enumerate() { + assert_eq!(packet[0], 0x47, "packet {index} has no sync byte"); + } +} + +/// PIDs that carry a payload, in order of first appearance. +fn pids(body: &[u8]) -> Vec { + let mut seen = Vec::new(); + for packet in body.chunks(TS_PACKET) { + let pid = (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]); + if !seen.contains(&pid) { + seen.push(pid); + } + } + seen +} + +#[tokio::test] +async fn a_film_is_served_as_a_well_formed_transport_stream() { + let (_temp, state, id) = scanned_film(8.0).await; + let (status, headers, body) = video_ts(&state, id, "", None).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "video/mpeg"); + assert_eq!(headers[header::ACCEPT_RANGES], "bytes"); + let features = headers["contentFeatures.dlna.org"].to_str().unwrap(); + assert!( + features.contains("DLNA.ORG_OP=11"), + "a transport stream resynchronises wherever it is joined, so byte seeking is an honest claim here: {features}" + ); + assert_well_formed_ts(&body); + + // The tables come first, because a decoder that joined here has to be told + // what the programme contains before anything else means anything. + let seen = pids(&body); + assert_eq!(seen[0], 0x0000, "the programme association table leads"); + assert_eq!(seen[1], 0x1000, "then the programme map"); + assert!( + seen.contains(&0x0100) && seen.contains(&0x0101), + "a video stream and a soundtrack: {seen:?}" + ); + + // And the body is exactly the length it promised. + let promised: usize = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + assert_eq!(body.len(), promised); +} + +/// The tables are repeated rather than written once, which is the difference +/// between a stream that can be joined and one that cannot. +#[tokio::test] +async fn the_programme_tables_repeat_throughout_the_stream() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, _, body) = video_ts(&state, id, "", None).await; + + let pat_packets = body + .chunks(TS_PACKET) + .filter(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x0000) + .count(); + assert!( + pat_packets >= 4, + "an eight-second film should carry the tables several times over, not once at the front: found {pat_packets}" + ); +} + +/// What the whole transport stream exists for: a byte offset is a usable seek. +#[tokio::test] +async fn a_byte_offset_seeks_to_that_fraction_of_the_film() { + let (_temp, state, id) = scanned_film(12.0).await; + let (_, headers, whole) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + let half = promised / 2; + let (status, headers, body) = video_ts(&state, id, "", Some(&format!("bytes={half}-"))).await; + + // Whole packets, so a range whose first byte falls inside one ends a little + // short of the promise rather than delivering a fragment of a packet. HTTP + // lets a server answer an open-ended range with less of it than was asked + // for, and a decoder handed 40 bytes of a packet can do nothing with them. + let expected = ((promised - half) / TS_PACKET as u64) * TS_PACKET as u64; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_eq!( + headers[header::CONTENT_RANGE].to_str().unwrap(), + format!("bytes {half}-{}/{promised}", half + expected - 1) + ); + assert_eq!(body.len() as u64, expected, "exactly what was promised"); + assert_well_formed_ts(&body); + // The tables lead this response too — without them a decoder joining here + // would have nothing to interpret the packets against. + assert_eq!(pids(&body)[0], 0x0000); + assert!( + body.len() < whole.len(), + "seeking to the middle produced as much as the whole film" + ); +} + +#[tokio::test] +async fn a_head_describes_the_transport_stream_without_producing_it() { + let (_temp, state, id) = scanned_film(8.0).await; + let response = create_router(state.clone(), Surface::Primary) + .oneshot( + Request::builder() + .method(Method::HEAD) + .uri(format!("/media/{id}/transcode/video.ts")) + .extension(ConnectInfo::( + "127.0.0.1:50000".parse().unwrap(), + )) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.headers()[header::CONTENT_TYPE], "video/mpeg"); + let promised = response.headers()[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .to_string(); + assert_ne!(promised, "0"); + + let (_, get_headers, get_body) = video_ts(&state, id, "", None).await; + assert_eq!(get_headers[header::CONTENT_LENGTH], promised); + assert_eq!(get_body.len().to_string(), promised); +} + +/// A client sizing the file up reads its last handful of bytes — fewer than one +/// packet, so there is no whole packet left to produce. Refusing that as +/// unsatisfiable is reported as a transfer error rather than shrugged off, and +/// the film never starts. +#[tokio::test] +async fn a_range_inside_the_final_packet_is_answered_from_it() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, headers, whole) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + // Forty bytes into the last packet, which is where a reader counting back + // from the end lands. + let first = promised - 148; + let (status, headers, body) = video_ts(&state, id, "", Some(&format!("bytes={first}-"))).await; + + assert_eq!(status, StatusCode::PARTIAL_CONTENT, "not 416"); + assert_eq!(body.len(), 148); + assert_eq!( + headers[header::CONTENT_RANGE].to_str().unwrap(), + format!("bytes {first}-{}/{promised}", promised - 1) + ); + // And it is the truth: these are the bytes a complete read really ends with. + assert_eq!( + &body[..], + &whole[whole.len() - 148..], + "the tail answer must match what the stream actually ends with" + ); +} + +/// The `video.mp4` next door stays, because the browser player fetches whole +/// responses and never needs a byte offset — it is only a television that does. +#[tokio::test] +async fn the_mp4_resource_remains_beside_the_transport_stream() { + let (_temp, state, id) = scanned_film(4.0).await; + let (status, headers, _) = video_mp4(&state, id, Method::GET, None).await; + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "video/mp4"); + + let (status, headers, _) = video_ts(&state, id, "", None).await; + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "video/mpeg"); +} + +/// The failure that stopped a film playing at all, and the least obvious one. +/// +/// A television opens three connections in under a second: one to play on, one +/// to read the end of the file, and one to play on again. Answering the middle +/// one by muxing means seeking into a thirty-gigabyte film and holding a +/// transcode slot for as long as that takes — and with two slots, the request +/// the set actually wanted to play on is refused outright. +/// +/// A probe produces nothing, so it should cost nothing. +#[tokio::test] +async fn a_tail_probe_is_answered_even_with_every_transcode_slot_taken() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, headers, whole) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + // Every slot busy, as it is when a set has connections already open. + let mut held = Vec::new(); + while let Some(permit) = state.transcode.try_acquire() { + held.push(permit); + } + assert!(!held.is_empty(), "there is at least one slot to exhaust"); + + // Producing the film is refused, which is the point of the limit. + let (status, _, _) = video_ts(&state, id, "", None).await; + assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE); + + // Reading its tail is not, because that produces nothing. + let first = promised - 4096; + let (status, headers, body) = video_ts(&state, id, "", Some(&format!("bytes={first}-"))).await; + assert_eq!( + status, + StatusCode::PARTIAL_CONTENT, + "a probe must not queue behind the films being played" + ); + assert_eq!(body.len(), 4096); + assert_eq!( + headers[header::CONTENT_RANGE].to_str().unwrap(), + format!("bytes {first}-{}/{promised}", promised - 1) + ); + // And it is still the truth, aligned the way a full read would deliver it. + assert_eq!( + &body[..], + &whole[whole.len() - 4096..], + "the padding answer must match what the stream actually ends with" + ); +} + +/// The elementary-stream PIDs the programme map declares. +/// +/// Parsed rather than inferred from what appears in the body, because the point +/// of the assertions below is the difference between the two: a PID a renderer +/// is told to expect audio on and never receives any is a renderer that waits +/// forever. +fn declared_pids(body: &[u8]) -> Vec { + let pmt = body + .chunks(TS_PACKET) + .find(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x1000) + .expect("the stream carries a programme map"); + // header(4) pointer_field(1), then the section. + let section = &pmt[5..]; + assert_eq!(section[0], 0x02, "table_id 2 is the programme map"); + let length = usize::from(u16::from_be_bytes([section[1] & 0x0F, section[2]])); + // table_id_extension(2) version(1) section(2) pcr_pid(2) program_info(2) + let info_length = usize::from(u16::from_be_bytes([section[10] & 0x0F, section[11]])); + let mut at = 12 + info_length; + // The section runs to `length` past the third byte, less its four-byte CRC. + let end = 3 + length - 4; + let mut pids = Vec::new(); + while at + 5 <= end { + pids.push(u16::from_be_bytes([ + section[at + 1] & 0x1F, + section[at + 2], + ])); + let descriptors = usize::from(u16::from_be_bytes([ + section[at + 3] & 0x0F, + section[at + 4], + ])); + at += 5 + descriptors; + } + pids +} + +/// Where one packet's payload begins, and the programme clock it carries. +fn payload_and_pcr(packet: &[u8]) -> (usize, Option) { + let control = (packet[3] >> 4) & 0b11; + if control & 0b10 == 0 { + return (4, None); + } + let length = usize::from(packet[4]); + let payload = 5 + length; + if length == 0 { + return (payload, None); + } + if packet[5] & 0x10 == 0 { + return (payload, None); + } + let base = (u64::from(packet[6]) << 25) + | (u64::from(packet[7]) << 17) + | (u64::from(packet[8]) << 9) + | (u64::from(packet[9]) << 1) + | u64::from(packet[10] >> 7); + (payload, Some(base)) +} + +/// The presentation timestamp in a PES header starting at `payload`. +fn pes_pts(payload: &[u8]) -> Option { + if payload.get(..3)? != [0x00, 0x00, 0x01] { + return None; + } + if payload[7] >> 6 == 0 { + return None; + } + let stamp = payload.get(9..14)?; + Some( + (u64::from(stamp[0] & 0x0E) << 29) + | (u64::from(stamp[1]) << 22) + | (u64::from(stamp[2] & 0xFE) << 14) + | (u64::from(stamp[3]) << 7) + | (u64::from(stamp[4]) >> 1), + ) +} + +/// A decoder starts its clock from the PCR and shows each picture when that +/// clock reaches the picture's PTS. Write the two equal and every frame is due +/// the instant it arrives, so there is no buffer at all and the first jitter +/// starves the renderer — which after a seek, where the decoder starts from +/// nothing, is exactly when it can least afford it. +#[tokio::test] +async fn the_presentation_clock_runs_ahead_of_the_programme_clock() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, _, body) = video_ts(&state, id, "", None).await; + + let mut checked = 0; + for packet in body.chunks(TS_PACKET) { + let pid = (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]); + if pid != 0x0100 || packet[1] & 0x40 == 0 { + continue; + } + let (payload, Some(pcr)) = payload_and_pcr(packet) else { + continue; + }; + let Some(pts) = pes_pts(&packet[payload..]) else { + continue; + }; + assert!( + pts > pcr, + "a picture due at {pts} was clocked as arriving at {pcr}, so the renderer \ + has no buffer to fill" + ); + // Half a second of ninety-kilohertz ticks, and not so much more that the + // film takes noticeably longer to start. + assert_eq!(pts - pcr, 45_000, "the headroom is a fixed half second"); + checked += 1; + } + assert!(checked > 20, "only {checked} pictures carried a clock"); +} + +/// A decoder that joins the stream anywhere can interpret nothing until it has +/// seen a PAT and a PMT, so the wait for the next pair is the floor on how long +/// a seek takes to produce a picture. +#[tokio::test] +async fn the_tables_repeat_often_enough_to_join_the_stream_quickly() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, _, body) = video_ts(&state, id, "", None).await; + + let pmts = body + .chunks(TS_PACKET) + .filter(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x1000) + .count(); + // Ten a second is what the broadcast profiles ask for; eight seconds of film + // should therefore carry something near eighty, and certainly not eight. + assert!( + pmts >= 60, + "{pmts} programme maps in eight seconds of film is a decoder waiting a \ + second to learn what it has joined" + ); +} + +/// A transport stream marks access unit boundaries with a delimiter and nothing +/// else — a container's own frame boundaries do not survive the conversion — so +/// every muxer in the field writes one, and a decoder that relies on it shows +/// nothing at all without it. +#[tokio::test] +async fn every_picture_opens_with_an_access_unit_delimiter() { + let (_temp, state, id) = scanned_film(4.0).await; + let (_, _, body) = video_ts(&state, id, "", None).await; + + let mut checked = 0; + for packet in body.chunks(TS_PACKET) { + let pid = (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]); + if pid != 0x0100 || packet[1] & 0x40 == 0 { + continue; + } + let (payload, _) = payload_and_pcr(packet); + let pes = &packet[payload..]; + // start codes(3) stream_id(1) length(2) flags(2) header_len(1), then the + // timestamps the header declared. + let unit = &pes[9 + usize::from(pes[8])..]; + assert_eq!( + &unit[..5], + &[0, 0, 0, 1, 0x09], + "an access unit that does not open with a delimiter" + ); + checked += 1; + } + assert!(checked > 10, "only {checked} access units were examined"); +} + +// ── The codec this whole path exists for ────────────────────────────────── +// +// AC-3 is passed through: a television decodes it for itself, so a film with an +// AC-3 soundtrack never reaches a decoder here and exercises none of the work +// below. DTS is the one no television will play and the one every stage of this +// has to get right — measured, decoded, re-encoded, and declared in the +// programme map only once its decoder has been proved to open. + +/// The vendored DTS conformance fixture: 48 kHz, 1024-byte frames of 512 +/// samples each, which is 768 kbps in eleven-millisecond frames. +const DTS: &[u8] = include_bytes!("../../vendor/oxideav-dts/tests/fixtures/dts_5_frames.bin"); +const DTS_FRAME_LEN: usize = 1024; +const DTS_FRAME_MS: f64 = 512.0 / 48.0; + +/// A film whose only soundtrack is DTS, which is the case a television shows +/// the picture of and plays nothing. +#[cfg(feature = "transcode-dts")] +fn dts_film(seconds: f64) -> Vec { + let frames: Vec<&[u8]> = DTS.chunks_exact(DTS_FRAME_LEN).collect(); + let audio_count = (seconds * 1000.0 / DTS_FRAME_MS).round() as usize; + let audio_samples: Vec<(u64, Vec)> = (0..audio_count) + .map(|i| { + ( + (i as f64 * DTS_FRAME_MS).round() as u64, + frames[i % frames.len()].to_vec(), + ) + }) + .collect(); + + let video_count = (seconds * 25.0).round() as usize; + let video_samples: Vec<(u64, Vec)> = (0..video_count) + .map(|i| { + ( + (i as f64 * 40.0).round() as u64, + video_sample(i % 25 == 0, 96, i as u8), + ) + }) + .collect(); + + build_mkv( + &[ + Track { + number: 1, + codec_id: "V_MPEG4/ISO/AVC", + codec_private: AVCC.to_vec(), + kind: TrackKind::Video { + width: 640, + height: 360, + }, + samples: video_samples, + all_keyframes: false, + is_default: true, + language: None, + }, + Track { + number: 2, + codec_id: "A_DTS", + codec_private: Vec::new(), + kind: TrackKind::Audio { + sample_rate: 48_000.0, + channels: 6, + }, + samples: audio_samples, + all_keyframes: true, + is_default: true, + language: Some("eng"), + }, + ], + seconds * 1000.0, + ) +} + +#[cfg(feature = "transcode-dts")] +async fn scanned_dts_film(seconds: f64) -> (tempfile::TempDir, vuio_core::state::AppState, i64) { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(root.join("Film.mkv"), dts_film(seconds)).unwrap(); + let state = common::state_over(temp.path(), &root).await; + let id = common::scan_into(&state) + .await + .iter() + .find(|f| f.filename == "Film.mkv") + .unwrap() + .id + .unwrap(); + (temp, state, id) +} + +/// Whole ADTS frames on `pid`, reassembled across the packets they are split +/// over — which is how a television's demuxer will see them. +fn elementary_stream(body: &[u8], pid: u16) -> Vec { + let mut out = Vec::new(); + for packet in body.chunks(TS_PACKET) { + let this = (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]); + if this != pid { + continue; + } + let (payload, _) = payload_and_pcr(packet); + let start = packet[1] & 0x40 != 0; + if start { + // Past the PES header and the timestamps it declared. + let pes = &packet[payload..]; + out.extend_from_slice(&pes[9 + usize::from(pes[8])..]); + } else { + out.extend_from_slice(&packet[payload..]); + } + } + out +} + +/// The presentation timestamp of every access unit on `pid`, in stream order. +fn pes_timestamps(body: &[u8], pid: u16) -> Vec { + body.chunks(TS_PACKET) + .filter(|packet| { + (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == pid + && packet[1] & 0x40 != 0 + }) + .filter_map(|packet| { + let (payload, _) = payload_and_pcr(packet); + pes_pts(&packet[payload..]) + }) + .collect() +} + +/// The deliverable: a DTS soundtrack no television can decode arrives as AAC it +/// can, in a transport stream it can seek. +#[cfg(feature = "transcode-dts")] +#[tokio::test] +async fn a_dts_film_reaches_the_television_as_aac() { + let (_temp, state, id) = scanned_dts_film(8.0).await; + let (status, headers, body) = video_ts(&state, id, "", None).await; + + assert_eq!(status, StatusCode::OK); + assert_eq!(headers[header::CONTENT_TYPE], "video/mpeg"); + assert_well_formed_ts(&body); + + // The programme map declares the picture and one soundtrack, and the + // soundtrack is AAC — not the DTS the container held, which is the point. + let declared = declared_pids(&body); + assert_eq!(declared, vec![0x0100, 0x0101], "{declared:?}"); + let pmt = body + .chunks(TS_PACKET) + .find(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x1000) + .unwrap(); + let section = &pmt[5..]; + let info_length = usize::from(u16::from_be_bytes([section[10] & 0x0F, section[11]])); + assert_eq!( + section[12 + info_length], + 0x1B, + "the picture is declared as H.264" + ); + assert_eq!( + section[12 + info_length + 5], + 0x0F, + "and the soundtrack as AAC, whatever it arrived as" + ); + + // And what arrives on that PID really is AAC: ADTS frames, each one a + // syncword and a length that walks to the next. + let audio = elementary_stream(&body, 0x0101); + assert!(!audio.is_empty(), "the soundtrack PID carried nothing"); + let mut at = 0usize; + let mut frames = 0usize; + while at + 7 <= audio.len() { + assert_eq!(audio[at], 0xFF, "frame {frames} has no ADTS syncword"); + assert_eq!(audio[at + 1] & 0xF0, 0xF0); + let len = ((usize::from(audio[at + 3]) & 0x03) << 11) + | (usize::from(audio[at + 4]) << 3) + | (usize::from(audio[at + 5]) >> 5); + assert!(len >= 7, "frame {frames} declares {len} bytes"); + at += len; + frames += 1; + } + // Eight seconds of 1024-sample frames at 48 kHz is around 375 of them. + // Eight seconds of 1024-sample frames at 48 kHz is 375 of them, and every + // one is accounted for: a decode that drops frames shortens the soundtrack + // against the picture for the rest of the film. + assert_eq!(frames, 375, "eight seconds of AAC is 375 frames"); + + // And they are placed across the whole film rather than bunched at its + // start, which is the failure mode of a re-encoded run anchored wrongly: + // the sound plays, and plays in the wrong place. + let stamps = pes_timestamps(&body, 0x0101); + let (first, last) = (stamps[0], *stamps.last().unwrap()); + assert!( + first < 45_000 + 90_000 / 2, + "the soundtrack starts {first} ticks in, half a second past the headroom" + ); + let span = last - first; + // Eight seconds less the final frame, in ninety-kilohertz ticks. + assert!( + span.abs_diff(718_080) < 9_000, + "the soundtrack spans {span} ticks of an eight-second film" + ); +} + +/// The estimate on the codec it was written for. A DTS soundtrack leaves at a +/// quarter of the rate it arrived at, so a promise built from the source's own +/// size is mostly padding — and every byte offset then names a moment far later +/// than the viewer dragged to. +#[cfg(feature = "transcode-dts")] +#[tokio::test] +async fn the_promise_follows_a_dts_soundtrack_down_to_what_it_becomes() { + let (_temp, state, id) = scanned_dts_film(12.0).await; + let (_, headers, body) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + assert_eq!(body.len() as u64, promised); + + let padding = body + .chunks(TS_PACKET) + .filter(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x1FFF) + .count() as u64 + * TS_PACKET as u64; + let film = promised - padding; + assert!( + padding * 4 < film, + "{padding} bytes of the {promised} promised are filler against {film} of film" + ); + + // The source's own size is what the promise used to be, and on this film it + // is far more than the stream weighs: 768 kbps of DTS leaves as 192 of AAC. + let source = std::fs::metadata(_temp.path().join("media").join("Film.mkv")) + .unwrap() + .len(); + assert!( + promised < source, + "the soundtrack shrank fourfold and the promise did not: {promised} against \ + a {source}-byte source" + ); +} + +/// Seeking is the thing being fixed, and it has to work on the codec that needs +/// the decoder — where the muxer opens mid-film and every soundtrack frame has +/// to be decoded and re-encoded from a standing start. +#[cfg(feature = "transcode-dts")] +#[tokio::test] +async fn a_dts_film_seeks_by_byte_and_still_carries_its_soundtrack() { + let (_temp, state, id) = scanned_dts_film(12.0).await; + let (_, headers, _) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + let half = promised / 2; + let (status, _, body) = video_ts(&state, id, "", Some(&format!("bytes={half}-"))).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_well_formed_ts(&body); + assert_eq!(pids(&body)[0], 0x0000, "the tables lead the response"); + + let audio = elementary_stream(&body, 0x0101); + assert!( + audio.len() > 1024, + "a seek into the middle of a DTS film produced {} bytes of soundtrack", + audio.len() + ); + assert_eq!(audio[0], 0xFF, "and it is still framed AAC"); + + // The picture is there too, and starts on a random-access point — otherwise + // the renderer has no reference frame to decode the first picture against. + let opens_on_a_keyframe = body + .chunks(TS_PACKET) + .filter(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x0100) + .find(|packet| packet[1] & 0x40 != 0) + .map(|packet| (packet[3] >> 4) & 0b10 != 0 && packet[5] & 0x40 != 0) + .unwrap_or(false); + assert!( + opens_on_a_keyframe, + "the first picture is not marked random-access" + ); +} + +/// A film with a soundtrack that cannot produce a single packet: one declared in +/// the container and never written, and one whose frames no decoder will open. +fn film_with_broken_soundtracks(seconds: f64) -> Vec { + let frames: Vec<&[u8]> = AC3.chunks_exact(AC3_FRAME_LEN).collect(); + let audio_count = (seconds * 1000.0 / AC3_FRAME_MS).round() as usize; + let stamp = |i: usize| (i as f64 * AC3_FRAME_MS).round() as u64; + + let video_count = (seconds * 25.0).round() as usize; + let video_samples: Vec<(u64, Vec)> = (0..video_count) + .map(|i| { + ( + (i as f64 * 40.0).round() as u64, + video_sample(i % 25 == 0, 96, i as u8), + ) + }) + .collect(); + + build_mkv( + &[ + Track { + number: 1, + codec_id: "V_MPEG4/ISO/AVC", + codec_private: AVCC.to_vec(), + kind: TrackKind::Video { + width: 640, + height: 360, + }, + samples: video_samples, + all_keyframes: false, + is_default: true, + language: None, + }, + // The one that works. + Track { + number: 2, + codec_id: "A_AC3", + codec_private: Vec::new(), + kind: TrackKind::Audio { + sample_rate: 48_000.0, + channels: 2, + }, + samples: (0..audio_count) + .map(|i| (stamp(i), frames[i % frames.len()].to_vec())) + .collect(), + all_keyframes: true, + is_default: true, + language: Some("eng"), + }, + // Declared, and never written a block. + Track { + number: 3, + codec_id: "A_AC3", + codec_private: Vec::new(), + kind: TrackKind::Audio { + sample_rate: 48_000.0, + channels: 2, + }, + samples: Vec::new(), + all_keyframes: true, + is_default: false, + language: Some("fra"), + }, + // Written, and nothing will decode it. + Track { + number: 4, + codec_id: "A_DTS", + codec_private: Vec::new(), + kind: TrackKind::Audio { + sample_rate: 48_000.0, + channels: 6, + }, + samples: (0..audio_count) + .map(|i| (stamp(i), vec![0xA5u8; 1024])) + .collect(), + all_keyframes: true, + is_default: false, + language: Some("deu"), + }, + ], + seconds * 1000.0, + ) +} + +/// A stream declared in the programme map and then silent forever is worse than +/// one that was never declared: the renderer reads the map, sees a PID it is +/// expecting audio on, and waits for it. So the map promises nothing that has +/// not been proved first. +#[tokio::test] +async fn a_soundtrack_that_cannot_produce_packets_is_never_declared() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("media"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(root.join("Film.mkv"), film_with_broken_soundtracks(8.0)).unwrap(); + let state = common::state_over(temp.path(), &root).await; + let id = common::scan_into(&state) + .await + .iter() + .find(|f| f.filename == "Film.mkv") + .unwrap() + .id + .unwrap(); + + let (status, _, body) = video_ts(&state, id, "", None).await; + assert_eq!(status, StatusCode::OK); + assert_well_formed_ts(&body); + + let declared = declared_pids(&body); + assert_eq!( + declared, + vec![0x0100, 0x0101], + "the picture and the one soundtrack that works, and nothing else: {declared:?}" + ); + + // And what it declared really does arrive. + let carried = pids(&body); + for pid in &declared { + assert!( + carried.contains(pid), + "PID {pid:#x} was promised and never sent" + ); + } +} + +/// The number the whole seek mechanism rests on. +/// +/// A byte offset into this resource is read as a fraction of its promised +/// length, so the promise being an honest account of what the stream weighs is +/// what makes an offset mean the moment the viewer dragged to. Promising the +/// source file's own size overstated it by a factor of three on the films this +/// path exists for, and every byte offset then named a moment three times too +/// far along. +#[tokio::test] +async fn the_promised_length_is_an_honest_account_of_the_stream() { + let (_temp, state, id) = scanned_film(12.0).await; + let (_, headers, body) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + assert_eq!(body.len() as u64, promised); + + let padding = body + .chunks(TS_PACKET) + .filter(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x1FFF) + .count() as u64 + * TS_PACKET as u64; + let film = promised - padding; + assert!( + padding * 4 < film, + "{padding} bytes of the {promised} promised are filler, against {film} of \ + film — the promise is not describing what is actually produced" + ); +} + +/// A seek near the end is a seek, not a probe: it still produces film. +#[tokio::test] +async fn a_genuine_seek_is_not_mistaken_for_a_probe() { + let (_temp, state, id) = scanned_film(12.0).await; + let (_, headers, _) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + let half = promised / 2; + let (_, _, body) = video_ts(&state, id, "", Some(&format!("bytes={half}-"))).await; + // Padding is null packets on PID 0x1FFF; film is not. + let carries_film = body.chunks(TS_PACKET).any(|packet| { + let pid = (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]); + pid != 0x1FFF + }); + assert!(carries_film, "a seek to the middle produced only padding"); +} From 937aac56c0c3dc758c6f58f992e4cf02b90af874 Mon Sep 17 00:00:00 2001 From: vyrti Date: Tue, 25 Aug 2026 16:03:22 +0300 Subject: [PATCH 20/38] transcoding works --- crates/vuio-core/src/media/transcode/ts.rs | 34 ++- crates/vuio-core/src/web/ts_streaming.rs | 41 +++- .../vuio-core/tests/film_transcode_tests.rs | 121 ++++++++++ log.log | 212 ++++++++++++++++++ 4 files changed, 396 insertions(+), 12 deletions(-) create mode 100644 log.log diff --git a/crates/vuio-core/src/media/transcode/ts.rs b/crates/vuio-core/src/media/transcode/ts.rs index 8a03682e..682899c1 100644 --- a/crates/vuio-core/src/media/transcode/ts.rs +++ b/crates/vuio-core/src/media/transcode/ts.rs @@ -38,7 +38,15 @@ use crate::media::remux::{ /// Long enough that the decode timeline can be recovered across any reordering a /// real encoder produces, short enough that a renderer starts promptly and a /// dropped connection wastes little. -const BATCH_SECS: f64 = 1.0; +/// +/// It is the floor on how long a renderer waits for its first byte, and that +/// turns out to matter far more than it looks. A television seeking a transport +/// stream binary-searches it: twenty-odd ranged requests, each read only far +/// enough to find one clock value, each converging on the instant the viewer +/// dragged to. Every one of them pays this. At a second and a half a probe the +/// search takes half a minute and the set gives up; at a fifth of that it is a +/// seek. +const BATCH_SECS: f64 = 0.4; /// Channels the re-encoded audio track carries. const DECODED_CHANNELS: u16 = 2; @@ -473,6 +481,9 @@ pub struct TsStream { primed: std::collections::VecDeque, /// Presentation time of the batch's first picture, in the output clock. batch_start: Option, + /// The latest presentation time held in the batch, which is what says + /// whether it can be cut here. See [`TsStream::next_chunk`]. + batch_max_pts: u64, started: bool, finished: bool, } @@ -568,6 +579,7 @@ impl TsStream { specs, primed, batch_start: None, + batch_max_pts: 0, started: false, finished: false, }) @@ -596,11 +608,24 @@ impl TsStream { self.started = true; let ticks = self.rescale(0, pts); let elapsed = ticks.saturating_sub(*self.batch_start.get_or_insert(ticks)); - // Batches break on keyframes, so every one opens with everything - // a decoder needs to start there. - if keyframe && elapsed >= batch_ticks && !self.streams[0].pending.is_empty() { + // Where the batch may be cut. Not only at a keyframe: a film + // with five-second groups of pictures would then hand a renderer + // five seconds of decoded soundtrack before its first byte, and + // a set binary-searching for a seek point pays that twenty times + // over. + // + // Anywhere the reordering is closed will do, and this frame + // being displayed after everything already held is exactly that: + // the batch is then a complete run in both orders, so sorting it + // recovers its decode times without reference to what follows, + // and the next batch's times all fall after this one's. A + // keyframe satisfies it too, which is why the old rule worked. + let closed = ticks > self.batch_max_pts; + if (keyframe || closed) && elapsed >= batch_ticks && !self.streams[0].pending.is_empty() + { let chunk = self.emit(); self.batch_start = Some(ticks); + self.batch_max_pts = 0; self.push_video(ticks, data, keyframe); if chunk.is_some() { return chunk; @@ -650,6 +675,7 @@ impl TsStream { .clone() .unwrap_or_default(); let codec = self.video_codec; + self.batch_max_pts = self.batch_max_pts.max(ticks); self.streams[0].pending.push(Unit { pts: ticks, dts: ticks, diff --git a/crates/vuio-core/src/web/ts_streaming.rs b/crates/vuio-core/src/web/ts_streaming.rs index fe5cac5f..ae83880f 100644 --- a/crates/vuio-core/src/web/ts_streaming.rs +++ b/crates/vuio-core/src/web/ts_streaming.rs @@ -133,14 +133,32 @@ pub async fn serve_transcoded_ts( }, }; - // A renderer sizing the resource up rather than seeking into it. Answered - // from the padding, which costs nothing and takes no transcode slot — the - // point being that muxing an answer here holds a slot for as long as it - // takes to seek into a thirty-gigabyte film, and the set has already opened - // the connection it actually wants to play on. - if byte_seek.is_some() && super::is_probe_tail(first_byte, promised) { - return padding_tail(first_byte, promised); - } + // A renderer reading the end of the resource rather than seeking into it. + // + // It has to be given the *film* there, not filler. A television works out + // how long a transport stream is by reading its last hundred kilobytes or so + // and taking the newest timestamp it finds — there is no header to ask, a + // transport stream not having one. Answer that read with null packets and + // the set learns nothing: no duration, so no scrub bar, so no seeking, while + // the picture and every soundtrack play perfectly. Which is the exact shape + // of the fault reported against this resource. + // + // So it is produced like any other seek. But not from the very last instant: + // a stream can only open on a random-access point, and the offset a duration + // probe names is past the film's final keyframe, which produces nothing at + // all — filler again, by a different route. It is pulled back far enough to + // be sure of landing on one, which costs the set a fractionally short + // duration and buys it a duration at all. + let is_probe = byte_seek.is_some() && super::is_probe_tail(first_byte, promised); + let start = match (is_probe, duration) { + (true, Some(duration)) => { + // Comfortably more than any group of pictures, and small against + // any film — a fifth of a per cent of a feature. + let backoff = (duration * 0.01).clamp(2.0, 20.0); + (duration - backoff).max(0.0).min(start) + } + _ => start, + }; let deliver = ((promised - first_byte) / TS_PACKET_LEN as u64) * TS_PACKET_LEN as u64; if deliver == 0 { @@ -224,6 +242,13 @@ pub async fn serve_transcoded_ts( } let Some(permit) = state.transcode.try_acquire() else { + // Nothing left to produce this with. A renderer sizing the resource up + // rather than playing it can still be told something true — the padding + // is the one part of this resource whose bytes are known without + // producing them — and that beats refusing the request outright. + if is_probe { + return padding_tail(first_byte, promised); + } return Ok(busy(&state, &filename)); }; diff --git a/crates/vuio-core/tests/film_transcode_tests.rs b/crates/vuio-core/tests/film_transcode_tests.rs index ca0ea0e7..a81b8cc1 100644 --- a/crates/vuio-core/tests/film_transcode_tests.rs +++ b/crates/vuio-core/tests/film_transcode_tests.rs @@ -2480,6 +2480,127 @@ async fn the_promised_length_is_an_honest_account_of_the_stream() { ); } +/// How a television learns how long a transport stream is, and the reason a film +/// can play perfectly and still have no scrub bar. +/// +/// There is no header to ask — a transport stream does not have one. So a set +/// reads the last hundred kilobytes or so of the resource and takes the newest +/// timestamp it finds; `last - first` is the duration, and until it has one it +/// publishes no seek map at all. Answering that read with null packets teaches +/// it nothing: the picture plays, every soundtrack plays, and the film is +/// unseekable and of unknown length. +/// +/// So the end of the resource has to carry the end of the film. +#[tokio::test] +async fn the_last_bytes_carry_the_films_final_timestamps() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, headers, _) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + // The last few kilobytes, which is where a duration reader looks. + let first = promised - 4096; + let (status, _, body) = video_ts(&state, id, "", Some(&format!("bytes={first}-"))).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT); + assert_well_formed_ts(&body); + + let filler = body + .chunks(TS_PACKET) + .filter(|packet| (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]) == 0x1FFF) + .count(); + assert!( + filler * 2 < body.len() / TS_PACKET, + "the tail is mostly null packets, which name no instant at all" + ); + + let clocks: Vec = body + .chunks(TS_PACKET) + .filter_map(|packet| payload_and_pcr(packet).1) + .map(|pcr| pcr as f64 / 90_000.0) + .collect(); + assert!( + !clocks.is_empty(), + "nothing in the last {} bytes says what time it is", + body.len() + ); + let newest = clocks.iter().cloned().fold(f64::MIN, f64::max); + assert!( + newest > 6.0, + "the last bytes of an eight-second film are stamped {newest:.2}s, so a set \ + reading them would think the film that long" + ); +} + +/// With nothing left to produce it with, the tail falls back to padding rather +/// than being refused: a renderer sizing the resource up gets an answer, and the +/// streams already playing keep their slots. +#[tokio::test] +async fn a_tail_probe_still_answers_when_every_slot_is_taken() { + let (_temp, state, id) = scanned_film(8.0).await; + let (_, headers, _) = video_ts(&state, id, "", None).await; + let promised: u64 = headers[header::CONTENT_LENGTH] + .to_str() + .unwrap() + .parse() + .unwrap(); + + let mut held = Vec::new(); + while let Some(permit) = state.transcode.try_acquire() { + held.push(permit); + } + let first = promised - 4096; + let (status, _, body) = video_ts(&state, id, "", Some(&format!("bytes={first}-"))).await; + assert_eq!(status, StatusCode::PARTIAL_CONTENT, "not 503"); + assert_eq!(body.len(), 4096); +} + +/// The decode timeline has to be recovered without reference to what follows, +/// because a batch is written before the next one is read. +/// +/// Batches are cut wherever the reordering is closed rather than only at +/// keyframes — a film with ten-second groups of pictures would otherwise hand a +/// renderer ten seconds of decoded soundtrack before its first byte. The cut is +/// only safe where every frame held is displayed before the next one read; cut +/// through a reorder group instead and the batch after it opens with a frame +/// due *earlier* than the one that closed the batch before, which a decoder +/// reads as time running backwards. +#[tokio::test] +async fn decode_times_never_run_backwards_across_a_batch_seam() { + let (_temp, state, id) = scanned_film(12.0).await; + let (_, _, body) = video_ts(&state, id, "", None).await; + + let mut previous: Option = None; + let mut seen = 0usize; + for packet in body.chunks(TS_PACKET) { + let pid = (u16::from(packet[1] & 0x1F) << 8) | u16::from(packet[2]); + if pid != 0x0100 || packet[1] & 0x40 == 0 { + continue; + } + let (payload, Some(pcr)) = payload_and_pcr(packet) else { + continue; + }; + if let Some(previous) = previous { + assert!( + pcr >= previous, + "the programme clock went from {previous} back to {pcr} at picture {seen}" + ); + } + previous = Some(pcr); + // And the picture is never due before it is decoded. + if let Some(pts) = pes_pts(&packet[payload..]) { + assert!( + pts >= pcr, + "picture {seen} is due at {pts}, clocked at {pcr}" + ); + } + seen += 1; + } + assert!(seen > 100, "only {seen} pictures were examined"); +} + /// A seek near the end is a seek, not a probe: it still produces film. #[tokio::test] async fn a_genuine_seek_is_not_mistaken_for_a_probe() { diff --git a/log.log b/log.log new file mode 100644 index 00000000..c6ede2c9 --- /dev/null +++ b/log.log @@ -0,0 +1,212 @@ +2026-08-25T16:00:39.102956+03:00 INFO ThreadId(09) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:39.12762+03:00 DEBUG ThreadId(04) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=0.000s, byte=0, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:39.248789+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 912176 bytes after 0.12s +2026-08-25T16:00:39.385723+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=27026464332-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:39.389394+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=10123.936s, byte=27026464332, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:39.39573+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 6476788 bytes +2026-08-25T16:00:39.518729+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 161304 bytes after 0.13s +2026-08-25T16:00:39.672238+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 3 chunks, 282000 bytes +2026-08-25T16:00:39.679632+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:39.683814+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=0.000s, byte=0, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:39.778305+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 912176 bytes after 0.09s +2026-08-25T16:00:49.469715+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 133 chunks, 145101032 bytes +2026-08-25T16:00:49.526933+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=200052150-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:49.531847+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=75.085s, byte=200052150, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:49.654446+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1175940 bytes after 0.12s +2026-08-25T16:00:49.787273+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=326731686-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:49.790639+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=122.632s, byte=326731686, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:49.814099+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 3 chunks, 3936532 bytes +2026-08-25T16:00:49.890051+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 731508 bytes after 0.10s +2026-08-25T16:00:50.031898+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=453411252-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:50.035302+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=170.179s, byte=453411252, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:50.05145+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 3 chunks, 2646852 bytes +2026-08-25T16:00:50.154892+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1395148 bytes after 0.12s +2026-08-25T16:00:50.284325+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=478747160-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:50.285313+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 3 chunks, 5092732 bytes +2026-08-25T16:00:50.287615+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=179.688s, byte=478747160, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:50.392105+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 717972 bytes after 0.10s +2026-08-25T16:00:50.568999+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=489655284-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:50.572357+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=183.782s, byte=489655284, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:50.590926+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3060264 bytes +2026-08-25T16:00:50.672832+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 717972 bytes after 0.10s +2026-08-25T16:00:50.846013+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=500559042-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:50.849388+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=187.875s, byte=500559042, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:50.870121+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3060264 bytes +2026-08-25T16:00:50.958125+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.11s +2026-08-25T16:00:51.120166+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=494026432-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:51.123522+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=185.423s, byte=494026432, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:51.143173+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:51.228013+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.10s +2026-08-25T16:00:51.40404+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=491511402-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:51.407402+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.479s, byte=491511402, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:51.410425+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:51.506869+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 717972 bytes after 0.10s +2026-08-25T16:00:51.680834+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492652922-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:51.684219+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.907s, byte=492652922, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:51.70343+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3060264 bytes +2026-08-25T16:00:51.788763+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.10s +2026-08-25T16:00:51.952724+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492124126-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:51.956089+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.709s, byte=492124126, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:51.972624+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:52.055514+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 717972 bytes after 0.10s +2026-08-25T16:00:52.233479+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492500954-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:52.236836+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.850s, byte=492500954, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:52.252803+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3060264 bytes +2026-08-25T16:00:52.341089+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.10s +2026-08-25T16:00:52.500906+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492442446-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:52.504249+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.828s, byte=492442446, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:52.525114+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:52.608834+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.10s +2026-08-25T16:00:52.770868+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492419922-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:52.774205+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.820s, byte=492419922, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:52.792859+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:52.87849+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.10s +2026-08-25T16:00:53.038291+03:00 INFO ThreadId(04) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492411249-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:53.041675+03:00 DEBUG ThreadId(04) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.816s, byte=492411249, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:53.062642+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:53.146089+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.10s +2026-08-25T16:00:53.312155+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492407911-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:53.315511+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.815s, byte=492407911, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:53.329604+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:53.421351+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.11s +2026-08-25T16:00:53.58572+03:00 INFO ThreadId(04) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492406625-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:53.589098+03:00 DEBUG ThreadId(04) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.815s, byte=492406625, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:53.653258+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:53.770598+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.18s +2026-08-25T16:00:53.938324+03:00 INFO ThreadId(04) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=492406126-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:53.941647+03:00 DEBUG ThreadId(04) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=184.815s, byte=492406126, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:53.954059+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3374412 bytes +2026-08-25T16:00:54.046741+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 767604 bytes after 0.11s +2026-08-25T16:00:56.234339+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 36 chunks, 37044084 bytes +2026-08-25T16:00:56.308279+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=651127624-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:56.313615+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=244.387s, byte=651127624, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:56.413257+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 790164 bytes after 0.10s +2026-08-25T16:00:56.580364+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 4126224 bytes +2026-08-25T16:00:56.596928+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=682245027-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:56.600866+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=256.067s, byte=682245027, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:56.704546+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1146424 bytes after 0.10s +2026-08-25T16:00:56.875546+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=679618292-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:56.878933+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=255.081s, byte=679618292, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:56.913707+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 4825960 bytes +2026-08-25T16:00:56.986808+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1146424 bytes after 0.11s +2026-08-25T16:00:57.168467+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=677215301-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:57.171846+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=254.179s, byte=677215301, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:57.196758+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 4825960 bytes +2026-08-25T16:00:57.27294+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1146424 bytes after 0.10s +2026-08-25T16:00:57.447422+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=675016995-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:57.450801+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=253.354s, byte=675016995, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:57.482096+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 4825960 bytes +2026-08-25T16:00:57.553316+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1146424 bytes after 0.10s +2026-08-25T16:00:57.731427+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=673005940-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:57.734788+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=252.599s, byte=673005940, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:57.762209+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 4825960 bytes +2026-08-25T16:00:57.840782+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1146424 bytes after 0.11s +2026-08-25T16:00:58.027995+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=671166186-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:58.031358+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=251.908s, byte=671166186, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:58.049009+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 4825960 bytes +2026-08-25T16:00:58.14025+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 1146424 bytes after 0.11s +2026-08-25T16:00:58.267624+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 3 chunks, 3437204 bytes +2026-08-25T16:00:58.274784+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2779344132-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:58.278676+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1043.170s, byte=2779344132, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:58.375888+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 567008 bytes after 0.10s +2026-08-25T16:00:58.549406+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2914345201-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:58.55277+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1093.840s, byte=2914345201, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:58.558469+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2957804 bytes +2026-08-25T16:00:58.6659+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:00:58.836765+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2900915370-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:58.840089+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1088.799s, byte=2900915370, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:58.853991+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:00:58.931985+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 768356 bytes after 0.09s +2026-08-25T16:00:59.107285+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3608472 bytes +2026-08-25T16:00:59.114067+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2910173001-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:59.118059+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1092.274s, byte=2910173001, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:59.227435+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:00:59.395348+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2907324705-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:59.398722+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1091.205s, byte=2907324705, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:59.414671+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:00:59.509868+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:00:59.674422+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2905380219-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:59.677793+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1090.475s, byte=2905380219, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:59.697498+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:00:59.786955+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:00:59.953424+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2904052748-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:00:59.956779+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.977s, byte=2904052748, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:00:59.974133+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:00.067626+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:00.234929+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2903146504-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:00.238282+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.637s, byte=2903146504, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:00.255376+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:00.403782+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.17s +2026-08-25T16:01:00.575879+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902527826-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:00.579253+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.404s, byte=2902527826, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:00.593132+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:00.668002+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 768356 bytes after 0.09s +2026-08-25T16:01:00.841107+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 3608472 bytes +2026-08-25T16:01:00.842138+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2903039482-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:00.845355+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.596s, byte=2903039482, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:00.953955+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:01.119824+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902966421-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:01.123161+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.569s, byte=2902966421, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:01.14322+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:01.233215+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:01.401911+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902916543-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:01.405376+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.550s, byte=2902916543, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:01.420748+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:01.515813+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:01.684372+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902882492-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:01.687707+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.538s, byte=2902882492, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:01.702745+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:01.798631+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:01.963433+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902859246-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:01.966958+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.529s, byte=2902859246, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:01.98759+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:02.078346+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:02.247309+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902843376-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:02.250687+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.523s, byte=2902843376, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:02.266106+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:02.361837+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:02.533562+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902832542-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:02.536885+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.519s, byte=2902832542, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:02.549287+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:02.646365+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:02.810744+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902825146-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:02.814082+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.516s, byte=2902825146, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:02.833949+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:02.924432+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:03.086271+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902820097-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:03.089635+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.514s, byte=2902820097, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:03.111872+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:03.199985+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:03.367761+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902816649-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:03.371106+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.513s, byte=2902816649, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:03.387911+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:03.48204+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:03.647719+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902814296-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:03.651079+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.512s, byte=2902814296, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:03.67274+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:03.76219+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:03.925464+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902812690-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:03.92881+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.511s, byte=2902812690, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:03.949339+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:04.038096+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:04.207419+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902811594-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:04.210773+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.511s, byte=2902811594, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:04.230012+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:04.32102+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:04.486172+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902810845-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:04.48954+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.511s, byte=2902810845, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:04.508553+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:04.599363+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:04.76817+03:00 INFO ThreadId(06) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902810334-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:04.771532+03:00 DEBUG ThreadId(06) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.510s, byte=2902810334, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:04.786959+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:04.881063+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +2026-08-25T16:01:05.045297+03:00 INFO ThreadId(03) vuio::renderer: crates/vuio-core/src/web/client.rs:106: GET /media/358/transcode/video.ts | profile=SonyBravia | ua="Dalvik/2.1.0 (Linux; U; Android 12; BRAVIA 4K VH22 Build/STT2.230505.001.S136)" | av-client="-" | TimeSeekRange="-" | Range="bytes=2902809826-" | getcontentFeatures="-" | transferMode="-" +2026-08-25T16:01:05.048696+03:00 DEBUG ThreadId(03) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:178: transcoded ts: id=358, file=Интерстеллар.IMAX.1080p. Ton.mkv, start=1089.510s, byte=2902809826, promised=27026746332, video=H.264 (Avc), audio=[2:rus Ac3 6ch passthrough, 3:rus Dts 6ch re-encoded, 4:rus Dts 6ch re-encoded, 5:rus Dts 6ch re-encoded, 6:eng Dts 6ch re-encoded] +2026-08-25T16:01:05.068557+03:00 DEBUG ThreadId(40) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 4 chunks, 2822444 bytes +2026-08-25T16:01:05.159507+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:297: first chunk for /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv: 842992 bytes after 0.11s +^C2026-08-25T16:01:32.905518+03:00 WARN ThreadId(06) vuio_core::lifecycle::media_service::monitoring: crates/vuio-core/src/lifecycle/media/monitoring.rs:236: File system event handler stopped +2026-08-25T16:01:32.9058+03:00 ERROR ThreadId(04) vuio_core::lifecycle::runner: crates/vuio-core/src/lifecycle/runner.rs:488: critical service task panicked: task 25 was cancelled +2026-08-25T16:01:32.906871+03:00 WARN ThreadId(04) vuio_core::lifecycle::runner: crates/vuio-core/src/lifecycle/runner.rs:511: Service join failed during shutdown: task 26 was cancelled +2026-08-25T16:01:32.906976+03:00 WARN ThreadId(04) vuio_core::lifecycle::runner: crates/vuio-core/src/lifecycle/runner.rs:511: Service join failed during shutdown: task 22 was cancelled +2026-08-25T16:01:32.906971+03:00 DEBUG ThreadId(39) vuio_core::web::ts_streaming: crates/vuio-core/src/web/ts_streaming.rs:306: /Users/alex/Downloads/Интерстеллар.IMAX.1080p. Ton.mkv stopped after 156 chunks, 179015104 bytes \ No newline at end of file From cdef2d8e6669931bc9bdfbf5b4fea1ca5e6b4a01 Mon Sep 17 00:00:00 2001 From: vyrti Date: Tue, 25 Aug 2026 17:11:27 +0300 Subject: [PATCH 21/38] transcoding fix --- crates/vuio-core/src/media/transcode/mod.rs | 2 +- crates/vuio-core/src/media/transcode/pcm.rs | 18 ++ .../vuio-core/src/media/transcode/session.rs | 69 +++++++ crates/vuio-core/src/media/transcode/ts.rs | 172 +++++++++++++----- crates/vuio-core/src/web/ts_streaming.rs | 132 ++++++++++++-- 5 files changed, 323 insertions(+), 70 deletions(-) diff --git a/crates/vuio-core/src/media/transcode/mod.rs b/crates/vuio-core/src/media/transcode/mod.rs index cd522d89..fb2dd621 100644 --- a/crates/vuio-core/src/media/transcode/mod.rs +++ b/crates/vuio-core/src/media/transcode/mod.rs @@ -47,7 +47,7 @@ pub use plan::{AudioPlan, Seeked}; pub use rendition::{ fit_channels, reencode_to_aac, run_anchor, AacWindow, AAC_FRAME_SAMPLES, ENCODER_DELAY, }; -pub use session::{IndexKey, SegmentKey, TranscodeState}; +pub use session::{ChunkKey, IndexKey, SegmentKey, TranscodeState}; #[cfg(all(feature = "transcode-aac", feature = "casting"))] #[allow(unused_imports)] pub use ts::{ diff --git a/crates/vuio-core/src/media/transcode/pcm.rs b/crates/vuio-core/src/media/transcode/pcm.rs index 154da218..f135421b 100644 --- a/crates/vuio-core/src/media/transcode/pcm.rs +++ b/crates/vuio-core/src/media/transcode/pcm.rs @@ -276,3 +276,21 @@ mod tests { assert!(out.iter().all(|&b| b == 0)); } } + +#[cfg(test)] +mod thread_safety { + /// The transport-stream muxer decodes a film's soundtracks side by side — + /// on a film with four DTS tracks that is most of what a renderer waits + /// through before its first byte. It can only do that while a decoder can + /// cross a thread boundary. + /// + /// The AAC encoder deliberately cannot: it wraps a C library holding raw + /// pointers, and stays on the muxing thread. So this is asserted of the + /// decoder alone, and it is a real constraint rather than a formality — + /// `media::transcode::ts::TsStream::decode_held` stops compiling without it. + #[test] + fn a_decoder_can_move_between_threads() { + fn require_send() {} + require_send::(); + } +} diff --git a/crates/vuio-core/src/media/transcode/session.rs b/crates/vuio-core/src/media/transcode/session.rs index f64cd350..f0a1323c 100644 --- a/crates/vuio-core/src/media/transcode/session.rs +++ b/crates/vuio-core/src/media/transcode/session.rs @@ -59,6 +59,34 @@ pub struct SegmentKey { const MAX_CACHED_SEGMENTS: usize = 24; const MAX_CACHED_SEGMENT_BYTES: usize = 48 * 1024 * 1024; +/// Identifies the first run of packets a seeked response opens with. +/// +/// Worth remembering because a television seeking a transport stream +/// binary-searches it — twenty-odd ranged requests, each read only far enough to +/// find one clock value — and a coarse seek snaps every byte offset inside one +/// group of pictures back to the same random-access point. On a film with +/// ten-second groups that is most of the search asking, in different words, for +/// bytes that have already been produced: twenty-nine requests, eight distinct +/// answers, measured. +/// +/// The soundtracks are part of the key because `?audio_track=` produces a +/// different stream from the same instant. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ChunkKey { + pub file: IndexKey, + /// Where the demuxer landed, in the source's own units. + pub origin: u64, + /// Which soundtracks the response carries. + pub tracks: u64, +} + +/// How many opening runs to keep, and how much memory they may occupy. +/// +/// One is a fraction of a second of film, so single-digit megabytes each on a +/// high-bitrate feature — a count alone would bound the wrong thing. +const MAX_CACHED_CHUNKS: usize = 32; +const MAX_CACHED_CHUNK_BYTES: usize = 64 * 1024 * 1024; + /// How many films' soundtrack measurements to keep. /// /// Two numbers per soundtrack, so this is bytes rather than megabytes and the @@ -71,9 +99,19 @@ pub struct TranscodeState { cache: Mutex, segments: Mutex, rates: Mutex, + /// A plain lock, not the async one the others use: this is read and written + /// from the blocking thread that does the muxing, which cannot await. + chunks: std::sync::Mutex, permits: Arc, } +#[derive(Debug, Default)] +struct ChunkCache { + entries: HashMap>>, + order: Vec, + bytes: usize, +} + /// What each film's tracks were measured to cost. /// /// Worth caching for the same reason the index is: a renderer opens a film with @@ -120,6 +158,7 @@ impl TranscodeState { cache: Mutex::new(Cache::default()), segments: Mutex::new(SegmentCache::default()), rates: Mutex::new(RateCache::default()), + chunks: std::sync::Mutex::new(ChunkCache::default()), permits: Arc::new(Semaphore::new(max_concurrent.max(1))), } } @@ -169,6 +208,36 @@ impl TranscodeState { } } + /// The opening run of packets for `key`, if it was produced recently. + /// + /// Synchronous, because the caller is the blocking thread doing the muxing. + /// A poisoned lock is treated as a miss: the worst that costs is producing + /// the run again. + pub fn cached_chunk(&self, key: &ChunkKey) -> Option>> { + self.chunks.lock().ok()?.entries.get(key).cloned() + } + + /// Remember an opening run, evicting oldest-first past either ceiling. + pub fn remember_chunk(&self, key: ChunkKey, chunk: Arc>) { + let Ok(mut cache) = self.chunks.lock() else { + return; + }; + let len = chunk.len(); + if len > MAX_CACHED_CHUNK_BYTES { + return; + } + if cache.entries.insert(key, chunk).is_none() { + cache.order.push(key); + cache.bytes += len; + while cache.order.len() > MAX_CACHED_CHUNKS || cache.bytes > MAX_CACHED_CHUNK_BYTES { + let oldest = cache.order.remove(0); + if let Some(gone) = cache.entries.remove(&oldest) { + cache.bytes = cache.bytes.saturating_sub(gone.len()); + } + } + } + } + /// The bytes of segment `key`, if it was built recently. pub async fn cached_segment(&self, key: &SegmentKey) -> Option { self.segments.lock().await.entries.get(key).cloned() diff --git a/crates/vuio-core/src/media/transcode/ts.rs b/crates/vuio-core/src/media/transcode/ts.rs index 682899c1..6a44624c 100644 --- a/crates/vuio-core/src/media/transcode/ts.rs +++ b/crates/vuio-core/src/media/transcode/ts.rs @@ -437,15 +437,33 @@ struct Stream { parameter_sets: Option, /// The rate a re-encoded track runs at, from the container's declaration. sample_rate: u32, - decode: Option, + /// The decoder for a re-encoded track, kept apart from the rest of its + /// chain because it is the half that can be run beside the other tracks'. + /// See [`TsStream::decode_held`]. + decoder: Option, + /// Everything downstream of the decoder. + encode: Option, + /// Packets read but not yet decoded, with the instant each arrived at. + /// + /// Held rather than decoded where they are read, so that every soundtrack + /// can be decoded at once when the batch is written. A film with four DTS + /// tracks decodes four of them, and doing that one after another on the + /// muxing thread is most of what a renderer waits through before its first + /// byte. + held: Vec<(u64, Vec)>, + /// What the decoder produced, waiting for the encoder. + pcm: Vec<(u64, Vec)>, /// Access units waiting for the batch to be written. pending: Vec, } -/// The state of one track's decode-and-re-encode chain. -struct Decode { - codec: TranscodeCodec, - decoder: PcmDecoder, +/// The state of one track's re-encoding, downstream of its decoder. +/// +/// The decoder is not here. It lives on the [`Stream`] so that the decoding of +/// every soundtrack can be run side by side, which the rest of this cannot be: +/// the AAC encoder wraps a C library holding raw pointers and does not cross a +/// thread boundary at all. +struct Encode { encoder: AacEncoder, decoded_channels: u16, sample_rate: u32, @@ -479,6 +497,13 @@ pub struct TsStream { /// Packets read while priming the decoders, replayed ahead of anything /// further from the demuxer. See [`prime_decoders`]. primed: std::collections::VecDeque, + /// Where the demuxer actually landed, in the source's own units. + /// + /// Not where it was asked to land. A coarse seek snaps back to the nearest + /// random-access point, so every byte offset inside one group of pictures + /// opens the same stream and produces the same first batch — which is what + /// makes that batch worth remembering. See `web::ts_streaming`. + origin: u64, /// Presentation time of the batch's first picture, in the output clock. batch_start: Option, /// The latest presentation time held in the batch, which is what says @@ -504,11 +529,12 @@ impl TsStream { use symphonia::core::units::Time; let mut format = super::source::open_format(path)?; + let mut origin = 0u64; if start_secs > 0.0 { // Coarse, not Accurate: a stream has to open on a random-access // point or the renderer has no reference frame to decode the first // picture against. - let _ = format.seek( + let seeked = format.seek( SeekMode::Coarse, SeekTo::Time { time: Time::try_from_secs_f64(super::seek_target(start_secs)) @@ -516,6 +542,9 @@ impl TsStream { track_id: Some(video.id), }, ); + if let Ok(seeked) = seeked { + origin = seeked.actual_ts.get().max(0) as u64; + } } let parameter_sets = ParameterSets::parse(video.codec_kind, &video.extra_data) @@ -530,7 +559,10 @@ impl TsStream { codec: None, parameter_sets: Some(parameter_sets), sample_rate: 0, - decode: None, + decoder: None, + encode: None, + held: Vec::new(), + pcm: Vec::new(), pending: Vec::new(), }); @@ -555,7 +587,10 @@ impl TsStream { codec, parameter_sets: None, sample_rate: track.sample_rate.unwrap_or(48_000), - decode: None, + decoder: None, + encode: None, + held: Vec::new(), + pcm: Vec::new(), pending: Vec::new(), }); } @@ -578,6 +613,7 @@ impl TsStream { streams, specs, primed, + origin, batch_start: None, batch_max_pts: 0, started: false, @@ -585,6 +621,12 @@ impl TsStream { }) } + /// Where the demuxer landed, which is the same for every byte offset inside + /// one group of pictures. + pub fn origin(&self) -> u64 { + self.origin + } + /// The next run of transport packets, or `None` at the end of the film. pub fn next_chunk(&mut self) -> Option> { if self.finished { @@ -595,6 +637,7 @@ impl TsStream { loop { let Some((id, pts, data)) = self.next_source_packet() else { self.finished = true; + self.decode_held(); self.flush_encoders(); return self.emit(); }; @@ -696,8 +739,9 @@ impl TsStream { let ticks = self.rescale(index, pts); let stream = &mut self.streams[index]; - let Some(codec) = stream.codec else { - // Passed through: the container's frame is the access unit. + if stream.codec.is_none() { + // Passed through: the container's frame is the access unit, and + // there is no work to put off. stream.pending.push(Unit { pts: ticks, dts: ticks, @@ -705,44 +749,55 @@ impl TsStream { data: data.to_vec(), }); return Ok(()); - }; + } + stream.held.push((ticks, data.to_vec())); + Ok(()) + } - let sample_rate = stream.sample_rate; - let decode = match stream.decode.as_mut() { - Some(decode) => decode, - None => { - let (decoder, mut primed) = - PcmDecoder::open(codec, sample_rate, Some(DECODED_CHANNELS), data)?; - let decoded_channels = decoder.channels(); - let encoder = AacEncoder::new(sample_rate, DECODED_CHANNELS)?; - if let Some(samples) = super::frames::frame_samples(codec, data) { - primed.resize(samples as usize * decoded_channels as usize * 2, 0); + /// Turn every held packet into access units, decoding all the soundtracks + /// at once. + /// + /// Two phases, and the split is forced by what can cross a thread boundary. + /// A decoder owns nothing shared and is `Send`, so one thread per soundtrack + /// makes the wait the slowest track rather than the sum of them all — which + /// on a film with four DTS soundtracks is most of the wait. The AAC encoder + /// wraps a C library holding raw pointers, is not `Send`, and stays here. + fn decode_held(&mut self) { + std::thread::scope(|scope| { + for stream in self.streams.iter_mut().skip(1) { + if stream.held.is_empty() { + continue; } - stream.decode = Some(Decode { - codec, - decoder, - encoder, - decoded_channels, - sample_rate, - next_pts: None, - anchors: Vec::new(), - decoded: 0, - held: Vec::new(), + let (Some(codec), Some(decoder)) = (stream.codec, stream.decoder.as_mut()) else { + continue; + }; + let held = std::mem::take(&mut stream.held); + let pcm = &mut stream.pcm; + scope.spawn(move || { + for (ticks, frame) in held { + let expect = super::frames::frame_samples(codec, &frame); + pcm.push((ticks, decoder.decode_or_silence(&frame, expect))); + } }); - let decode = stream.decode.as_mut().unwrap(); - take_decoded(&mut stream.pending, decode, ticks, primed)?; - return Ok(()); } - }; - let expect = super::frames::frame_samples(decode.codec, data); - let pcm = decode.decoder.decode_or_silence(data, expect); - take_decoded(&mut stream.pending, decode, ticks, pcm)?; - Ok(()) + }); + + for stream in self.streams.iter_mut().skip(1) { + let Some(encode) = stream.encode.as_mut() else { + stream.pcm.clear(); + continue; + }; + for (ticks, pcm) in std::mem::take(&mut stream.pcm) { + if let Err(error) = take_decoded(&mut stream.pending, encode, ticks, pcm) { + tracing::debug!(%error, "dropping an audio packet that would not re-encode"); + } + } + } } fn flush_encoders(&mut self) { for stream in &mut self.streams { - if let Some(decode) = stream.decode.as_mut() { + if let Some(decode) = stream.encode.as_mut() { let tail = decode.encoder.finish(); place_frames(&mut stream.pending, decode, &tail, true); } @@ -751,10 +806,11 @@ impl TsStream { /// Write everything held as transport packets, interleaved by decode time. fn emit(&mut self) -> Option> { + self.decode_held(); // A batch about to be written cannot wait for more packets before // deciding where a re-encoded run sits, so this is where it is forced. for stream in &mut self.streams { - if let Some(decode) = stream.decode.as_mut() { + if let Some(decode) = stream.encode.as_mut() { place_frames(&mut stream.pending, decode, &[], true); } } @@ -881,7 +937,7 @@ fn prime_decoders( } } - streams.retain(|stream| { + streams.retain_mut(|stream| { if stream.spec.pid == FIRST_ES_PID { return true; } @@ -896,7 +952,7 @@ fn prime_decoders( let Some(codec) = stream.codec else { return true; }; - match decode_chain_opens(codec, stream.sample_rate, frame) { + match open_chain(stream, codec, frame) { Ok(()) => true, Err(error) => { tracing::warn!( @@ -910,16 +966,34 @@ fn prime_decoders( primed } -/// Whether both halves of a re-encode can be built for this track. -fn decode_chain_opens(codec: TranscodeCodec, sample_rate: u32, frame: &[u8]) -> Result<()> { +/// Build both halves of one track's re-encoding, on its first frame. +/// +/// Kept rather than proved and thrown away: the frame is decoded once here to +/// find out whether the chain works and what shape its output is, and the +/// decoder that did it is the one the film then runs through. The frame itself +/// is decoded again when the priming read is replayed, which for these codecs — +/// where every frame stands alone — costs one frame and keeps the packet path +/// with no special case in it. +fn open_chain(stream: &mut Stream, codec: TranscodeCodec, frame: &[u8]) -> Result<()> { + let sample_rate = stream.sample_rate; let (decoder, _) = PcmDecoder::open(codec, sample_rate, Some(DECODED_CHANNELS), frame)?; - AacEncoder::new(sample_rate, DECODED_CHANNELS)?; - drop(decoder); + let decoded_channels = decoder.channels(); + let encoder = AacEncoder::new(sample_rate, DECODED_CHANNELS)?; + stream.decoder = Some(decoder); + stream.encode = Some(Encode { + encoder, + decoded_channels, + sample_rate, + next_pts: None, + anchors: Vec::new(), + decoded: 0, + held: Vec::new(), + }); Ok(()) } /// Take one frame's decoded PCM and encode it. -fn take_decoded(pending: &mut Vec, decode: &mut Decode, ticks: u64, pcm: Vec) -> Result<()> { +fn take_decoded(pending: &mut Vec, decode: &mut Encode, ticks: u64, pcm: Vec) -> Result<()> { // Where this run starts, if it is contiguous — asked of every packet, so // that one rounded container timestamp cannot place the whole run. let samples = (ticks as i128 * i64::from(decode.sample_rate) as i128 @@ -938,7 +1012,7 @@ fn take_decoded(pending: &mut Vec, decode: &mut Decode, ticks: u64, pcm: V /// The ADTS headers stay on, unlike the MP4 path which strips them: a transport /// stream carries AAC exactly as the encoder framed it, because there is no /// sample entry alongside to repeat what the header says. -fn place_frames(pending: &mut Vec, decode: &mut Decode, adts: &[u8], settle: bool) { +fn place_frames(pending: &mut Vec, decode: &mut Encode, adts: &[u8], settle: bool) { for frame in adts_frames(adts) { decode.held.push(frame.to_vec()); } diff --git a/crates/vuio-core/src/web/ts_streaming.rs b/crates/vuio-core/src/web/ts_streaming.rs index ae83880f..18d7d97b 100644 --- a/crates/vuio-core/src/web/ts_streaming.rs +++ b/crates/vuio-core/src/web/ts_streaming.rs @@ -48,8 +48,8 @@ use tracing::{debug, warn}; use crate::media::remux::{browser_video_track, MkvDemuxer, TrackInfo, TS_PACKET_LEN}; use crate::media::transcode::{ - audio_disposition, measure_track_rates, promised_ts_length, AudioDisposition, IndexKey, - TrackRates, TsStream, + audio_disposition, measure_track_rates, promised_ts_length, AudioDisposition, ChunkKey, + IndexKey, TrackRates, TranscodeState, TsStream, }; use crate::{database::DatabaseManager, error::AppError, state::AppState}; @@ -85,7 +85,9 @@ pub async fn serve_transcoded_ts( &method, &headers, ); + let began = std::time::Instant::now(); let (file_id, path, size, filename, info) = resolve(&state, &id).await?; + let inspected = began.elapsed(); let video = browser_video_track(&info.tracks) .ok_or(AppError::NotFound)? .clone(); @@ -177,7 +179,9 @@ pub async fn serve_transcoded_ts( // one this build handles — neither of which is visible from a track id. debug!( "transcoded ts: id={id}, file={filename}, start={start:.3}s, byte={first_byte}, \ - promised={promised}, video={} ({:?}), audio=[{}]", + promised={promised}, inspect={:.0}ms, plan={:.0}ms, video={} ({:?}), audio=[{}]", + inspected.as_secs_f64() * 1000.0, + (began.elapsed() - inspected).as_secs_f64() * 1000.0, video.codec, video.codec_kind, audio @@ -252,7 +256,15 @@ pub async fn serve_transcoded_ts( return Ok(busy(&state, &filename)); }; - Ok(response.body(ts_body(path, video, audio, start, deliver, permit))?) + // Everything the opening run of packets depends on except where the seek + // lands, which only the demuxer can say. See [`ts_body`]. + let cache = file_key(file_id, &path) + .await + .map(|file| (state.transcode.clone(), file, track_signature(&audio))); + + Ok(response.body(ts_body( + path, video, audio, start, deliver, permit, cache, + ))?) } /// Mux the film on a blocking thread, handing packets over a bounded channel. @@ -261,6 +273,7 @@ pub async fn serve_transcoded_ts( /// packets, which is transport stream's own filler and what a decoder is already /// built to skip; long output is cut at the promise, on a packet boundary so /// that what arrives is never half a packet. +#[allow(clippy::too_many_arguments)] fn ts_body( path: std::path::PathBuf, video: TrackInfo, @@ -268,6 +281,7 @@ fn ts_body( start: f64, deliver: u64, permit: tokio::sync::OwnedSemaphorePermit, + cache: Option<(Arc, IndexKey, u64)>, ) -> Body { let (tx, rx) = tokio::sync::mpsc::channel::>(PIPELINE_DEPTH); @@ -275,7 +289,14 @@ fn ts_body( let _permit = permit; let mut sent: u64 = 0; let opened = std::time::Instant::now(); - let mut stream = match TsStream::open(&path, &video, &audio, start) { + let stream = TsStream::open(&path, &video, &audio, start); + debug!( + "opened {} at {start:.3}s in {:.0}ms", + path.display(), + opened.elapsed().as_secs_f64() * 1000.0 + ); + let muxing = std::time::Instant::now(); + let mut stream = match stream { Ok(stream) => stream, Err(error) => { // Loud, because the renderer's only symptom is a film that will @@ -287,20 +308,76 @@ fn ts_body( } }; + // The run this response opens with, which every byte offset inside the + // same group of pictures produces identically. A television seeking + // this film asks for twenty-odd of them and reads a few kilobytes of + // each, so most of a search is the same answer over again. + let key = cache.as_ref().map(|(_, file, tracks)| ChunkKey { + file: *file, + origin: stream.origin(), + tracks: *tracks, + }); + let mut replay = false; + if let (Some((state, ..)), Some(key)) = (cache.as_ref(), key) { + if let Some(ready) = state.cached_chunk(&key) { + debug!( + "opening run for {} at origin {} served from memory in {:.0}ms", + path.display(), + key.origin, + opened.elapsed().as_secs_f64() * 1000.0 + ); + if !send_capped(&tx, &mut sent, deliver, ready.to_vec()) { + return; + } + // Produced again only if the renderer wants more than the run + // it just got — a seek probe reads a little and goes away. + if tx.is_closed() { + return; + } + replay = true; + } + } + let mut chunks = 0usize; - while let Some(chunk) = stream.next_chunk() { + loop { + // Nothing to produce this for. A renderer seeking a film opens a + // connection per probe and abandons it after a few kilobytes, so + // without this each one leaves a muxer decoding four soundtracks + // into a socket that closed — twenty-seven of them during one seek. + if tx.is_closed() { + debug!( + "{} abandoned after {chunks} chunks, {sent} bytes", + path.display() + ); + return; + } + let Some(chunk) = stream.next_chunk() else { + break; + }; + if replay { + // The muxer's own first run, which is the one already sent. + replay = false; + chunks += 1; + continue; + } if chunks == 0 { // How long a renderer waited before its first byte, which is // the other way this fails: a set that gives up before the // first group of pictures has been muxed shows the same nothing // as one that was handed something it could not decode. debug!( - "first chunk for {}: {} bytes after {:.2}s", + "first chunk for {}: {} bytes after {:.0}ms of muxing ({:.0}ms total)", path.display(), chunk.len(), - opened.elapsed().as_secs_f64() + muxing.elapsed().as_secs_f64() * 1000.0, + opened.elapsed().as_secs_f64() * 1000.0 ); } + if chunks == 0 { + if let (Some((state, ..)), Some(key)) = (cache.as_ref(), key) { + state.remember_chunk(key, Arc::new(chunk.clone())); + } + } chunks += 1; if !send_capped(&tx, &mut sent, deliver, chunk) { debug!("{} stopped after {chunks} chunks, {sent} bytes", path.display()); @@ -424,6 +501,31 @@ fn padding_tail(first: u64, promised: u64) -> Result { .body(Body::from_stream(zeroes))?) } +/// Which soundtracks a response carries, as one number. +/// +/// `?audio_track=` produces a different stream from the same instant, so the +/// remembered opening run has to be told apart by it. +fn track_signature(audio: &[TrackInfo]) -> u64 { + audio.iter().fold(0u64, |signature, track| { + signature.wrapping_mul(31).wrapping_add(u64::from(track.id)) + }) +} + +/// The file's identity, as the caches key on it. +async fn file_key(file_id: i64, path: &std::path::Path) -> Option { + let metadata = tokio::fs::metadata(path).await.ok()?; + Some(IndexKey { + id: file_id, + size: metadata.len(), + modified: metadata + .modified() + .ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + .unwrap_or(0), + }) +} + fn header_value<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { headers.get(name).and_then(|value| value.to_str().ok()) } @@ -485,18 +587,8 @@ async fn track_rates( path: &std::path::Path, tracks: &[TrackInfo], ) -> Arc { - let key = match tokio::fs::metadata(path).await { - Ok(metadata) => IndexKey { - id: file_id, - size: metadata.len(), - modified: metadata - .modified() - .ok() - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|d| d.as_secs() as i64) - .unwrap_or(0), - }, - Err(_) => return Arc::new(TrackRates::default()), + let Some(key) = file_key(file_id, path).await else { + return Arc::new(TrackRates::default()); }; if let Some(rates) = state.transcode.cached_rates(&key).await { return rates; From 15d00500105a3cbcc779a8002adc3e53555c8ad9 Mon Sep 17 00:00:00 2001 From: vyrti Date: Tue, 25 Aug 2026 17:42:56 +0300 Subject: [PATCH 22/38] clippy --- .github/workflows/ci.yml | 8 +- Cargo.toml | 6 + crates/vendor/libxaac-sys/.cargo-ok | 1 + .../vendor/libxaac-sys/.cargo_vcs_info.json | 6 + crates/vendor/libxaac-sys/Cargo.toml | 70 + crates/vendor/libxaac-sys/Cargo.toml.orig | 38 + crates/vendor/libxaac-sys/README.md | 55 + crates/vendor/libxaac-sys/build.rs | 195 + crates/vendor/libxaac-sys/examples/sample.rs | 183 + .../libxaac/.github/workflows/cifuzz.yml | 27 + .../libxaac/.github/workflows/cmake.yml | 23 + crates/vendor/libxaac-sys/libxaac/Android.bp | 523 + .../vendor/libxaac-sys/libxaac/CMakeLists.txt | 29 + crates/vendor/libxaac-sys/libxaac/LICENSE | 191 + crates/vendor/libxaac-sys/libxaac/METADATA | 3 + .../libxaac/MODULE_LICENSE_APACHE2 | 0 crates/vendor/libxaac-sys/libxaac/NOTICE | 19 + crates/vendor/libxaac-sys/libxaac/OWNERS | 3 + .../vendor/libxaac-sys/libxaac/PREUPLOAD.cfg | 2 + .../libxaac-sys/libxaac/README.experimental | 5 + crates/vendor/libxaac-sys/libxaac/README.md | 102 + .../vendor/libxaac-sys/libxaac/README_dec.md | 245 + .../vendor/libxaac-sys/libxaac/README_enc.md | 123 + .../libxaac-sys/libxaac/README_enc_drc.md | 250 + .../cmake/toolchains/aarch32_toolchain.cmake | 11 + .../cmake/toolchains/aarch64_toolchain.cmake | 19 + .../cmake/toolchains/x86_toolchain.cmake | 18 + .../libxaac-sys/libxaac/cmake/utils.cmake | 135 + .../libxaac-sys/libxaac/common/common.cmake | 9 + .../libxaac/common/ixheaac_basic_op.h | 27 + .../libxaac/common/ixheaac_basic_ops.h | 137 + .../libxaac/common/ixheaac_basic_ops16.h | 237 + .../libxaac/common/ixheaac_basic_ops32.h | 436 + .../libxaac/common/ixheaac_basic_ops40.h | 255 + .../libxaac/common/ixheaac_basic_ops_arr.h | 56 + .../libxaac/common/ixheaac_constants.h | 91 + .../libxaac/common/ixheaac_error_standards.h | 27 + .../libxaac/common/ixheaac_esbr_fft.c | 1209 ++ .../libxaac/common/ixheaac_esbr_rom.c | 4678 +++++ .../libxaac/common/ixheaac_esbr_rom.h | 83 + .../common/ixheaac_fft_ifft_32x32_rom.c | 1210 ++ .../libxaac/common/ixheaac_fft_ifft_rom.h | 31 + .../libxaac/common/ixheaac_sbr_const.h | 232 + .../libxaac/common/ixheaac_type_def.h | 90 + .../decoder/armv7/ia_xheaacd_mps_mulshift.s | 46 + .../ia_xheaacd_mps_reoder_mulshift_acc.s | 240 + .../armv7/ixheaacd_aac_ld_dec_rearrange.s | 50 + .../decoder/armv7/ixheaacd_apply_rot.s | 229 + .../decoder/armv7/ixheaacd_apply_scale_fac.s | 147 + .../decoder/armv7/ixheaacd_auto_corr.s | 155 + .../decoder/armv7/ixheaacd_autocorr_st2.s | 401 + .../decoder/armv7/ixheaacd_calc_post_twid.s | 109 + .../decoder/armv7/ixheaacd_calc_pre_twid.s | 107 + .../armv7/ixheaacd_calcmaxspectralline.s | 82 + .../decoder/armv7/ixheaacd_complex_fft_p2.s | 809 + .../decoder/armv7/ixheaacd_complex_ifft_p2.s | 809 + .../armv7/ixheaacd_conv_ergtoamplitude.s | 132 + .../armv7/ixheaacd_conv_ergtoamplitudelp.s | 148 + .../decoder/armv7/ixheaacd_cos_sin_mod.s | 472 + .../libxaac/decoder/armv7/ixheaacd_dct3_32.s | 507 + .../decoder/armv7/ixheaacd_dec_DCT2_64_asm.s | 523 + .../decoder/armv7/ixheaacd_decorr_filter2.s | 1071 ++ .../ixheaacd_eld_decoder_sbr_pre_twiddle.s | 67 + .../armv7/ixheaacd_enery_calc_per_subband.s | 158 + .../armv7/ixheaacd_esbr_cos_sin_mod_loop1.s | 173 + .../armv7/ixheaacd_esbr_cos_sin_mod_loop2.s | 181 + .../armv7/ixheaacd_esbr_fwd_modulation.s | 112 + .../armv7/ixheaacd_esbr_qmfsyn64_winadd.s | 410 + .../decoder/armv7/ixheaacd_esbr_radix4bfly.s | 155 + .../armv7/ixheaacd_expsubbandsamples.s | 113 + .../decoder/armv7/ixheaacd_ffr_divide16.s | 49 + .../decoder/armv7/ixheaacd_fft32x32_ld.s | 860 + .../armv7/ixheaacd_fft32x32_ld2_armv7.s | 376 + .../decoder/armv7/ixheaacd_fft_15_ld.s | 529 + .../decoder/armv7/ixheaacd_fft_armv7.c | 89 + .../ixheaacd_function_selector_arm_non_neon.c | 186 + .../armv7/ixheaacd_function_selector_armv7.c | 251 + .../decoder/armv7/ixheaacd_fwd_modulation.s | 134 + .../armv7/ixheaacd_harm_idx_zerotwolp.s | 109 + .../decoder/armv7/ixheaacd_imdct_using_fft.s | 825 + .../decoder/armv7/ixheaacd_inv_dit_fft_8pt.s | 163 + .../libxaac/decoder/armv7/ixheaacd_lap1.s | 113 + .../armv7/ixheaacd_mps_complex_fft_64_asm.s | 691 + .../armv7/ixheaacd_mps_synt_out_calc.s | 55 + .../ixheaacd_mps_synt_post_fft_twiddle.s | 65 + .../armv7/ixheaacd_mps_synt_post_twiddle.s | 60 + .../armv7/ixheaacd_mps_synt_pre_twiddle.s | 60 + .../libxaac/decoder/armv7/ixheaacd_no_lap1.s | 91 + .../decoder/armv7/ixheaacd_overlap_add1.s | 264 + .../decoder/armv7/ixheaacd_overlap_add2.s | 268 + .../armv7/ixheaacd_post_radix_compute2.s | 145 + .../armv7/ixheaacd_post_radix_compute4.s | 139 + .../decoder/armv7/ixheaacd_post_twiddle.s | 545 + .../armv7/ixheaacd_post_twiddle_overlap.s | 1186 ++ .../armv7/ixheaacd_pre_twiddle_compute.s | 388 + .../decoder/armv7/ixheaacd_qmf_dec_armv7.c | 490 + .../decoder/armv7/ixheaacd_radix4_bfly.s | 150 + .../armv7/ixheaacd_rescale_subbandsamples.s | 205 + .../armv7/ixheaacd_sbr_imdct_using_fft.s | 856 + .../armv7/ixheaacd_sbr_qmfanal32_winadds.s | 266 + .../ixheaacd_sbr_qmfanal32_winadds_eld.s | 245 + .../armv7/ixheaacd_sbr_qmfsyn64_winadd.s | 380 + .../decoder/armv7/ixheaacd_shiftrountine.s | 106 + .../ixheaacd_shiftrountine_with_rnd_eld.s | 93 + .../armv7/ixheaacd_shiftrountine_with_round.s | 112 + .../ixheaacd_shiftrountine_with_round_hq.s | 76 + .../armv7/ixheaacd_tns_ar_filter_fixed.s | 553 + .../ixheaacd_tns_ar_filter_fixed_32x16.s | 272 + .../armv7/ixheaacd_tns_parcor2lpc_32x16.s | 122 + .../decoder/armv7/libxaacdec_armv7.cmake | 67 + .../armv8/ixheaacd_apply_scale_factors.s | 166 + .../armv8/ixheaacd_calcmaxspectralline.s | 82 + .../armv8/ixheaacd_cos_sin_mod_loop1.s | 231 + .../armv8/ixheaacd_cos_sin_mod_loop2.s | 213 + .../armv8/ixheaacd_fft32x32_ld2_armv8.s | 555 + .../armv8/ixheaacd_function_selector_armv8.c | 250 + .../decoder/armv8/ixheaacd_imdct_using_fft.s | 819 + .../decoder/armv8/ixheaacd_inv_dit_fft_8pt.s | 174 + .../libxaac/decoder/armv8/ixheaacd_no_lap1.s | 112 + .../decoder/armv8/ixheaacd_overlap_add1.s | 301 + .../decoder/armv8/ixheaacd_overlap_add2.s | 305 + .../decoder/armv8/ixheaacd_post_twiddle.s | 713 + .../armv8/ixheaacd_post_twiddle_overlap.s | 1878 ++ .../armv8/ixheaacd_postradixcompute4.s | 148 + .../decoder/armv8/ixheaacd_pre_twiddle.s | 512 + .../decoder/armv8/ixheaacd_qmf_dec_armv8.c | 1490 ++ .../armv8/ixheaacd_sbr_imdct_using_fft.s | 777 + .../armv8/ixheaacd_sbr_qmf_analysis32_neon.s | 341 + .../armv8/ixheaacd_sbr_qmfsyn64_winadd.s | 403 + .../armv8/ixheaacd_shiftrountine_with_round.s | 73 + .../ixheaacd_shiftrountine_with_round_eld.s | 79 + .../decoder/armv8/libxaacdec_armv8.cmake | 28 + .../decoder/drc_src/impd_apicmd_standards.h | 107 + .../libxaac/decoder/drc_src/impd_drc_api.c | 618 + .../decoder/drc_src/impd_drc_api_defs.h | 49 + .../decoder/drc_src/impd_drc_api_struct_def.h | 141 + .../decoder/drc_src/impd_drc_bitbuffer.c | 218 + .../decoder/drc_src/impd_drc_bitbuffer.h | 46 + .../drc_src/impd_drc_bitstream_dec_api.h | 43 + .../libxaac/decoder/drc_src/impd_drc_common.h | 259 + .../decoder/drc_src/impd_drc_config_params.h | 52 + .../libxaac/decoder/drc_src/impd_drc_dec.c | 368 + .../libxaac/decoder/drc_src/impd_drc_dec.h | 47 + .../decoder/drc_src/impd_drc_definitions.h | 34 + .../drc_src/impd_drc_dynamic_payload.c | 1458 ++ .../libxaac/decoder/drc_src/impd_drc_eq.c | 1347 ++ .../libxaac/decoder/drc_src/impd_drc_eq.h | 184 + .../decoder/drc_src/impd_drc_error_codes.h | 56 + .../drc_src/impd_drc_extr_delta_coded_info.c | 92 + .../drc_src/impd_drc_extr_delta_coded_info.h | 87 + .../decoder/drc_src/impd_drc_filter_bank.c | 431 + .../decoder/drc_src/impd_drc_filter_bank.h | 149 + .../decoder/drc_src/impd_drc_gain_dec.c | 790 + .../decoder/drc_src/impd_drc_gain_dec.h | 94 + .../decoder/drc_src/impd_drc_gain_decoder.c | 450 + .../decoder/drc_src/impd_drc_gain_decoder.h | 86 + .../decoder/drc_src/impd_drc_hashdefines.h | 29 + .../libxaac/decoder/drc_src/impd_drc_init.c | 588 + .../decoder/drc_src/impd_drc_interface.h | 138 + .../drc_src/impd_drc_interface_decoder.c | 161 + .../drc_src/impd_drc_loudness_control.c | 906 + .../drc_src/impd_drc_loudness_control.h | 93 + .../drc_src/impd_drc_main_td_process.c | 380 + .../decoder/drc_src/impd_drc_multi_band.h | 63 + .../decoder/drc_src/impd_drc_multiband.c | 166 + .../decoder/drc_src/impd_drc_parametric_dec.c | 1109 ++ .../libxaac/decoder/drc_src/impd_drc_parser.h | 105 + .../drc_src/impd_drc_parser_interface.h | 61 + .../decoder/drc_src/impd_drc_peak_limiter.c | 181 + .../decoder/drc_src/impd_drc_peak_limiter.h | 53 + .../drc_src/impd_drc_peak_limiter_struct.h | 45 + .../decoder/drc_src/impd_drc_process.c | 331 + .../decoder/drc_src/impd_drc_process_audio.h | 77 + .../decoder/drc_src/impd_drc_qmf_filter.h | 36 + .../libxaac/decoder/drc_src/impd_drc_rom.c | 1144 ++ .../libxaac/decoder/drc_src/impd_drc_rom.h | 162 + .../drc_src/impd_drc_sel_proc_drc_set_sel.h | 178 + .../drc_src/impd_drc_selection_process.c | 1119 ++ .../drc_src/impd_drc_selection_process.h | 179 + ...d_drc_selection_process_drcset_selection.c | 1559 ++ .../drc_src/impd_drc_selection_process_init.c | 537 + .../decoder/drc_src/impd_drc_shape_filter.c | 308 + .../decoder/drc_src/impd_drc_static_payload.c | 2501 +++ .../libxaac/decoder/drc_src/impd_drc_struct.h | 688 + .../drc_src/impd_drc_uni_bitstream_dec_api.h | 51 + .../decoder/drc_src/impd_drc_uni_dec.h | 40 + .../libxaac/decoder/drc_src/impd_drc_uni_eq.h | 48 + .../decoder/drc_src/impd_drc_uni_gain_dec.h | 114 + .../decoder/drc_src/impd_drc_uni_interface.h | 116 + .../decoder/drc_src/impd_drc_uni_loud_eq.h | 30 + .../decoder/drc_src/impd_drc_uni_multi_band.h | 66 + .../decoder/drc_src/impd_drc_uni_parser.h | 98 + .../drc_src/impd_drc_uni_process_audio.h | 54 + .../impd_drc_uni_sel_proc_drc_set_sel.h | 140 + .../drc_src/impd_drc_uni_sel_proc_init.h | 44 + .../impd_drc_uni_sel_proc_loudness_control.h | 83 + .../drc_src/impd_drc_uni_shape_filter.h | 23 + .../decoder/drc_src/impd_drc_uni_tables.h | 91 + .../decoder/drc_src/impd_error_handler.h | 91 + .../decoder/drc_src/impd_error_standards.h | 40 + .../decoder/drc_src/impd_memory_standards.h | 108 + .../decoder/drc_src/impd_parametric_drc_dec.h | 191 + .../libxaac/decoder/drc_src/impd_type_def.h | 112 + .../decoder/drc_src/libxaacdec_drc.cmake | 27 + .../ixheaacd_function_selector_generic.c | 252 + .../generic/ixheaacd_qmf_dec_generic.c | 2016 +++ .../libxaac/decoder/ixheaacd_Windowing.c | 112 + .../libxaac/decoder/ixheaacd_aac_config.h | 114 + .../libxaac/decoder/ixheaacd_aac_ec.c | 457 + .../libxaac/decoder/ixheaacd_aac_imdct.c | 3449 ++++ .../libxaac/decoder/ixheaacd_aac_imdct.h | 180 + .../libxaac/decoder/ixheaacd_aac_rom.c | 3045 ++++ .../libxaac/decoder/ixheaacd_aac_rom.h | 204 + .../libxaac/decoder/ixheaacd_aac_tns.c | 448 + .../libxaac/decoder/ixheaacd_aacdec.h | 90 + .../libxaac/decoder/ixheaacd_aacdecoder.c | 1147 ++ .../libxaac/decoder/ixheaacd_aacpluscheck.c | 236 + .../libxaac/decoder/ixheaacd_acelp_bitparse.c | 594 + .../libxaac/decoder/ixheaacd_acelp_com.h | 68 + .../libxaac/decoder/ixheaacd_acelp_decode.c | 685 + .../libxaac/decoder/ixheaacd_acelp_info.h | 63 + .../libxaac/decoder/ixheaacd_acelp_mdct.c | 243 + .../libxaac/decoder/ixheaacd_acelp_tools.c | 203 + .../libxaac/decoder/ixheaacd_adts.h | 46 + .../libxaac/decoder/ixheaacd_adts_crc_check.c | 230 + .../libxaac/decoder/ixheaacd_adts_crc_check.h | 31 + .../libxaac/decoder/ixheaacd_api.c | 3788 ++++ .../libxaac/decoder/ixheaacd_api_defs.h | 39 + .../decoder/ixheaacd_apicmd_standards.h | 85 + .../libxaac/decoder/ixheaacd_arith_dec.c | 2154 +++ .../libxaac/decoder/ixheaacd_arith_dec.h | 35 + .../libxaac/decoder/ixheaacd_audioobjtypes.h | 66 + .../libxaac/decoder/ixheaacd_avq_dec.c | 312 + .../libxaac/decoder/ixheaacd_avq_rom.c | 1227 ++ .../libxaac/decoder/ixheaacd_basic_funcs.c | 196 + .../libxaac/decoder/ixheaacd_basic_funcs.h | 53 + .../libxaac/decoder/ixheaacd_basic_ops.c | 657 + .../libxaac/decoder/ixheaacd_bit_extract.h | 83 + .../libxaac/decoder/ixheaacd_bitbuffer.c | 328 + .../libxaac/decoder/ixheaacd_bitbuffer.h | 138 + .../libxaac/decoder/ixheaacd_block.c | 1363 ++ .../libxaac/decoder/ixheaacd_block.h | 198 + .../libxaac/decoder/ixheaacd_channel.c | 1236 ++ .../libxaac/decoder/ixheaacd_channel.h | 64 + .../libxaac/decoder/ixheaacd_channelinfo.h | 342 + .../libxaac/decoder/ixheaacd_cnst.h | 124 + .../decoder/ixheaacd_common_initfuncs.c | 191 + .../libxaac/decoder/ixheaacd_common_lpfuncs.c | 398 + .../libxaac/decoder/ixheaacd_common_rom.c | 354 + .../libxaac/decoder/ixheaacd_common_rom.h | 47 + .../libxaac/decoder/ixheaacd_config.h | 724 + .../libxaac/decoder/ixheaacd_create.c | 715 + .../libxaac/decoder/ixheaacd_create.h | 48 + .../libxaac/decoder/ixheaacd_dec_main.h | 31 + .../libxaac/decoder/ixheaacd_decode_main.c | 656 + .../libxaac/decoder/ixheaacd_defines.h | 52 + .../libxaac/decoder/ixheaacd_definitions.h | 53 + .../decoder/ixheaacd_drc_data_struct.h | 93 + .../libxaac/decoder/ixheaacd_drc_dec.h | 44 + .../libxaac/decoder/ixheaacd_drc_freq_dec.c | 1106 ++ .../libxaac/decoder/ixheaacd_dsp_fft32x32s.c | 117 + .../libxaac/decoder/ixheaacd_dsp_fft32x32s.h | 41 + .../libxaac-sys/libxaac/decoder/ixheaacd_ec.h | 45 + .../libxaac/decoder/ixheaacd_ec_defines.h | 47 + .../libxaac/decoder/ixheaacd_ec_rom.c | 32 + .../libxaac/decoder/ixheaacd_ec_rom.h | 28 + .../libxaac/decoder/ixheaacd_ec_struct_def.h | 55 + .../libxaac/decoder/ixheaacd_env_calc.c | 1899 ++ .../libxaac/decoder/ixheaacd_env_calc.h | 183 + .../libxaac/decoder/ixheaacd_env_dec.c | 923 + .../libxaac/decoder/ixheaacd_env_dec.h | 72 + .../libxaac/decoder/ixheaacd_env_extr.c | 1962 ++ .../libxaac/decoder/ixheaacd_env_extr.h | 186 + .../libxaac/decoder/ixheaacd_env_extr_part.h | 112 + .../libxaac/decoder/ixheaacd_error_codes.h | 139 + .../libxaac/decoder/ixheaacd_error_handler.h | 60 + .../decoder/ixheaacd_error_standards.h | 27 + .../libxaac/decoder/ixheaacd_esbr_envcal.c | 1093 ++ .../libxaac/decoder/ixheaacd_esbr_polyphase.c | 338 + .../libxaac/decoder/ixheaacd_ext_ch_ele.c | 1069 ++ .../libxaac/decoder/ixheaacd_fft.c | 2672 +++ .../libxaac/decoder/ixheaacd_fft_ifft_32x32.c | 1587 ++ .../libxaac/decoder/ixheaacd_freq_sca.c | 713 + .../libxaac/decoder/ixheaacd_freq_sca.h | 32 + .../libxaac/decoder/ixheaacd_func_def.h | 97 + .../decoder/ixheaacd_function_selector.h | 225 + .../libxaac/decoder/ixheaacd_fwd_alias_cnx.c | 199 + .../libxaac/decoder/ixheaacd_hbe_dft_trans.c | 941 + .../libxaac/decoder/ixheaacd_hbe_trans.c | 1606 ++ .../libxaac/decoder/ixheaacd_hcr.h | 44 + .../libxaac/decoder/ixheaacd_headerdecode.c | 1192 ++ .../libxaac/decoder/ixheaacd_headerdecode.h | 76 + .../decoder/ixheaacd_huff_code_reorder.c | 1906 ++ .../libxaac/decoder/ixheaacd_huff_tools.c | 96 + .../libxaac/decoder/ixheaacd_hufftables.c | 80 + .../libxaac/decoder/ixheaacd_hybrid.c | 285 + .../libxaac/decoder/ixheaacd_hybrid.h | 66 + .../libxaac/decoder/ixheaacd_imdct.c | 654 + .../libxaac/decoder/ixheaacd_info.h | 108 + .../libxaac/decoder/ixheaacd_init_config.c | 694 + .../libxaac/decoder/ixheaacd_initfuncs.c | 582 + .../libxaac/decoder/ixheaacd_interface.h | 109 + .../libxaac/decoder/ixheaacd_intrinsics.h | 26 + .../libxaac/decoder/ixheaacd_latmdemux.c | 320 + .../libxaac/decoder/ixheaacd_latmdemux.h | 65 + .../libxaac/decoder/ixheaacd_ld_mps_config.c | 325 + .../libxaac/decoder/ixheaacd_ld_mps_dec.c | 212 + .../libxaac/decoder/ixheaacd_ld_mps_dec.h | 39 + .../libxaac/decoder/ixheaacd_longblock.c | 306 + .../libxaac/decoder/ixheaacd_lpc.c | 828 + .../libxaac/decoder/ixheaacd_lpc_dec.c | 279 + .../libxaac/decoder/ixheaacd_lpfuncs.c | 1208 ++ .../libxaac/decoder/ixheaacd_lpp_tran.c | 1258 ++ .../libxaac/decoder/ixheaacd_lpp_tran.h | 110 + .../libxaac/decoder/ixheaacd_lt_predict.c | 531 + .../libxaac/decoder/ixheaacd_lt_predict.h | 59 + .../libxaac/decoder/ixheaacd_main.h | 276 + .../decoder/ixheaacd_memory_standards.h | 114 + .../libxaac/decoder/ixheaacd_mps_aac_struct.h | 94 + .../decoder/ixheaacd_mps_apply_common.c | 112 + .../decoder/ixheaacd_mps_apply_common.h | 28 + .../libxaac/decoder/ixheaacd_mps_apply_m1.c | 261 + .../libxaac/decoder/ixheaacd_mps_apply_m2.c | 498 + .../libxaac/decoder/ixheaacd_mps_basic_op.h | 393 + .../libxaac/decoder/ixheaacd_mps_bitdec.c | 2792 +++ .../libxaac/decoder/ixheaacd_mps_bitdec.h | 80 + .../libxaac/decoder/ixheaacd_mps_blind.c | 433 + .../libxaac/decoder/ixheaacd_mps_blind.h | 29 + .../decoder/ixheaacd_mps_calc_m1m2_common.c | 942 + .../decoder/ixheaacd_mps_calc_m1m2_common.h | 52 + .../decoder/ixheaacd_mps_calc_m1m2_emm.c | 193 + .../ixheaacd_mps_calc_m1m2_tree_515x.c | 529 + .../ixheaacd_mps_calc_m1m2_tree_51sx.c | 330 + .../ixheaacd_mps_calc_m1m2_tree_52xx.c | 643 + .../ixheaacd_mps_calc_m1m2_tree_727x.c | 897 + .../ixheaacd_mps_calc_m1m2_tree_757x.c | 240 + .../ixheaacd_mps_calc_m1m2_tree_config.h | 35 + .../libxaac/decoder/ixheaacd_mps_dec.c | 2139 +++ .../libxaac/decoder/ixheaacd_mps_dec.h | 1031 ++ .../libxaac/decoder/ixheaacd_mps_decor.h | 81 + .../libxaac/decoder/ixheaacd_mps_decorr.c | 1070 ++ .../libxaac/decoder/ixheaacd_mps_defines.h | 33 + .../libxaac/decoder/ixheaacd_mps_get_index.c | 61 + .../libxaac/decoder/ixheaacd_mps_get_index.h | 27 + .../libxaac/decoder/ixheaacd_mps_huff_tab.h | 95 + .../libxaac/decoder/ixheaacd_mps_hybfilter.h | 74 + .../decoder/ixheaacd_mps_hybrid_filt.c | 1232 ++ .../libxaac/decoder/ixheaacd_mps_initfuncs.c | 1416 ++ .../libxaac/decoder/ixheaacd_mps_interface.h | 40 + .../libxaac/decoder/ixheaacd_mps_m1m2.h | 33 + .../decoder/ixheaacd_mps_m1m2_common.c | 281 + .../libxaac/decoder/ixheaacd_mps_macro_def.h | 216 + .../libxaac/decoder/ixheaacd_mps_mdct_2_qmf.c | 1888 ++ .../libxaac/decoder/ixheaacd_mps_mdct_2_qmf.h | 65 + .../libxaac/decoder/ixheaacd_mps_nlc_dec.h | 61 + .../libxaac/decoder/ixheaacd_mps_parse.c | 1501 ++ .../libxaac/decoder/ixheaacd_mps_poly_filt.c | 391 + .../libxaac/decoder/ixheaacd_mps_polyphase.c | 1180 ++ .../libxaac/decoder/ixheaacd_mps_polyphase.h | 31 + .../libxaac/decoder/ixheaacd_mps_pre_mix.c | 1395 ++ .../libxaac/decoder/ixheaacd_mps_process.c | 380 + .../libxaac/decoder/ixheaacd_mps_process.h | 37 + .../libxaac/decoder/ixheaacd_mps_res.h | 34 + .../libxaac/decoder/ixheaacd_mps_res_block.c | 1070 ++ .../libxaac/decoder/ixheaacd_mps_res_block.h | 56 + .../decoder/ixheaacd_mps_res_channel.c | 393 + .../decoder/ixheaacd_mps_res_channel.h | 30 + .../decoder/ixheaacd_mps_res_channel_info.c | 97 + .../decoder/ixheaacd_mps_res_channelinfo.h | 33 + .../decoder/ixheaacd_mps_res_huffman.h | 59 + .../decoder/ixheaacd_mps_res_longblock.c | 233 + .../decoder/ixheaacd_mps_res_pns_js_thumb.c | 175 + .../decoder/ixheaacd_mps_res_pulsedata.c | 54 + .../decoder/ixheaacd_mps_res_pulsedata.h | 27 + .../libxaac/decoder/ixheaacd_mps_res_rom.h | 89 + .../libxaac/decoder/ixheaacd_mps_res_tns.c | 152 + .../libxaac/decoder/ixheaacd_mps_res_tns.h | 39 + .../decoder/ixheaacd_mps_reshape_bb_env.c | 594 + .../decoder/ixheaacd_mps_reshape_bb_env.h | 40 + .../libxaac/decoder/ixheaacd_mps_rom.c | 8286 +++++++++ .../libxaac/decoder/ixheaacd_mps_smoothing.c | 726 + .../libxaac/decoder/ixheaacd_mps_smoothing.h | 30 + .../libxaac/decoder/ixheaacd_mps_struct_def.h | 397 + .../libxaac/decoder/ixheaacd_mps_tables.h | 36 + .../decoder/ixheaacd_mps_temp_process.c | 1474 ++ .../decoder/ixheaacd_mps_temp_reshape.c | 211 + .../libxaac/decoder/ixheaacd_mps_tonality.c | 436 + .../libxaac/decoder/ixheaacd_mps_tonality.h | 27 + .../libxaac/decoder/ixheaacd_mps_tp_process.h | 38 + .../libxaac/decoder/ixheaacd_multichannel.c | 422 + .../libxaac/decoder/ixheaacd_multichannel.h | 39 + .../libxaac/decoder/ixheaacd_peak_limiter.c | 333 + .../ixheaacd_peak_limiter_struct_def.h | 50 + .../libxaac/decoder/ixheaacd_pns.h | 52 + .../libxaac/decoder/ixheaacd_pns_js_thumb.c | 515 + .../libxaac/decoder/ixheaacd_pred_vec_block.c | 239 + .../libxaac/decoder/ixheaacd_process.c | 606 + .../libxaac/decoder/ixheaacd_process.h | 29 + .../libxaac/decoder/ixheaacd_ps_bitdec.c | 283 + .../libxaac/decoder/ixheaacd_ps_bitdec.h | 34 + .../libxaac/decoder/ixheaacd_ps_dec.c | 991 + .../libxaac/decoder/ixheaacd_ps_dec.h | 368 + .../libxaac/decoder/ixheaacd_ps_dec_flt.c | 1224 ++ .../libxaac/decoder/ixheaacd_pulsedata.h | 41 + .../libxaac/decoder/ixheaacd_pvc_dec.h | 55 + .../libxaac/decoder/ixheaacd_pvc_rom.c | 616 + .../libxaac/decoder/ixheaacd_pvc_rom.h | 53 + .../libxaac/decoder/ixheaacd_qmf_dec.c | 1129 ++ .../libxaac/decoder/ixheaacd_qmf_dec.h | 242 + .../libxaac/decoder/ixheaacd_qmf_poly.h | 44 + .../libxaac/decoder/ixheaacd_rev_vlc.c | 1755 ++ .../libxaac/decoder/ixheaacd_rom.c | 4455 +++++ .../libxaac/decoder/ixheaacd_rvlc.h | 21 + .../libxaac/decoder/ixheaacd_sbr_common.h | 25 + .../libxaac/decoder/ixheaacd_sbr_crc.c | 97 + .../libxaac/decoder/ixheaacd_sbr_crc.h | 30 + .../libxaac/decoder/ixheaacd_sbr_dec.c | 1546 ++ .../libxaac/decoder/ixheaacd_sbr_dec.h | 322 + .../libxaac/decoder/ixheaacd_sbr_payload.h | 36 + .../libxaac/decoder/ixheaacd_sbr_rom.c | 3727 ++++ .../libxaac/decoder/ixheaacd_sbr_rom.h | 250 + .../libxaac/decoder/ixheaacd_sbr_scale.h | 33 + .../decoder/ixheaacd_sbrdec_initfuncs.c | 1278 ++ .../libxaac/decoder/ixheaacd_sbrdec_lpfuncs.c | 1359 ++ .../libxaac/decoder/ixheaacd_sbrdecoder.c | 1337 ++ .../libxaac/decoder/ixheaacd_sbrdecoder.h | 94 + .../libxaac/decoder/ixheaacd_sbrdecsettings.h | 87 + .../libxaac/decoder/ixheaacd_sbrqmftrans.h | 46 + .../libxaac/decoder/ixheaacd_spectrum_dec.c | 466 + .../libxaac/decoder/ixheaacd_stereo.c | 236 + .../libxaac/decoder/ixheaacd_stereo.h | 35 + .../libxaac/decoder/ixheaacd_struct.h | 63 + .../libxaac/decoder/ixheaacd_struct_def.h | 339 + .../libxaac/decoder/ixheaacd_tcx_fwd_alcnx.c | 453 + .../libxaac/decoder/ixheaacd_tcx_fwd_mdct.c | 255 + .../libxaac/decoder/ixheaacd_td_mdct.h | 31 + .../libxaac/decoder/ixheaacd_thumb_ps_dec.c | 181 + .../libxaac/decoder/ixheaacd_tns.c | 314 + .../libxaac/decoder/ixheaacd_tns.h | 87 + .../libxaac/decoder/ixheaacd_tns_usac.h | 49 + .../libxaac/decoder/ixheaacd_type_def.h | 90 + .../libxaac/decoder/ixheaacd_usac_ec.c | 642 + .../libxaac/decoder/ixheaacd_vec_baisc_ops.h | 64 + .../libxaac/decoder/ixheaacd_ver_number.h | 26 + .../libxaac/decoder/ixheaacd_windows.h | 61 + .../libxaac/decoder/libxaacdec.cmake | 139 + .../x86/ixheaacd_function_selector_x86.c | 249 + .../libxaac/decoder/x86/libxaacdec_x86.cmake | 4 + .../ixheaacd_function_selector_x86_64.c | 249 + .../decoder/x86_64/libxaacdec_x86_64.cmake | 4 + .../libxaac/docs/Api_flowchart_dec.png | Bin 0 -> 1677112 bytes .../libxaac/docs/Api_flowchart_enc.png | Bin 0 -> 91771 bytes .../libxaac/docs/LIBXAAC-Enc-API.pdf | Bin 0 -> 539652 bytes .../libxaac/docs/LIBXAAC-Enc-GSG.pdf | Bin 0 -> 338745 bytes .../libxaac/docs/libxaac_block_diagram.jpg | Bin 0 -> 381851 bytes .../libxaac/encoder/drc_src/impd_drc_api.c | 844 + .../libxaac/encoder/drc_src/impd_drc_api.h | 54 + .../encoder/drc_src/impd_drc_common_enc.h | 90 + .../libxaac/encoder/drc_src/impd_drc_enc.c | 339 + .../libxaac/encoder/drc_src/impd_drc_enc.h | 47 + .../drc_src/impd_drc_gain_calculator.c | 535 + .../encoder/drc_src/impd_drc_gain_enc.c | 1024 ++ .../encoder/drc_src/impd_drc_gain_enc.h | 211 + .../libxaac/encoder/drc_src/impd_drc_mux.c | 3374 ++++ .../libxaac/encoder/drc_src/impd_drc_mux.h | 35 + .../encoder/drc_src/impd_drc_struct_def.h | 51 + .../libxaac/encoder/drc_src/impd_drc_tables.c | 214 + .../libxaac/encoder/drc_src/impd_drc_tables.h | 62 + .../encoder/drc_src/impd_drc_uni_drc.h | 628 + .../encoder/drc_src/impd_drc_uni_drc_eq.c | 1439 ++ .../encoder/drc_src/impd_drc_uni_drc_eq.h | 172 + .../drc_src/impd_drc_uni_drc_filter_bank.c | 184 + .../drc_src/impd_drc_uni_drc_filter_bank.h | 39 + .../encoder/drc_src/libxaacenc_drc.cmake | 12 + .../libxaac/encoder/iusace_acelp_enc.c | 439 + .../libxaac/encoder/iusace_acelp_rom.c | 213 + .../libxaac/encoder/iusace_acelp_tools.c | 1082 ++ .../libxaac/encoder/iusace_arith_enc.c | 433 + .../libxaac/encoder/iusace_arith_enc.h | 47 + .../libxaac/encoder/iusace_avq_enc.c | 343 + .../libxaac/encoder/iusace_avq_enc.h | 38 + .../libxaac/encoder/iusace_avq_rom.c | 674 + .../libxaac/encoder/iusace_basic_ops_flt.h | 25 + .../libxaac/encoder/iusace_bitbuffer.c | 132 + .../libxaac/encoder/iusace_bitbuffer.h | 42 + .../libxaac/encoder/iusace_block_switch.c | 282 + .../libxaac/encoder/iusace_block_switch.h | 27 + .../encoder/iusace_block_switch_const.h | 45 + .../encoder/iusace_block_switch_struct_def.h | 39 + .../libxaac-sys/libxaac/encoder/iusace_cnst.h | 214 + .../libxaac/encoder/iusace_config.h | 350 + .../libxaac/encoder/iusace_enc_fac.c | 537 + .../libxaac/encoder/iusace_enc_main.c | 1472 ++ .../libxaac/encoder/iusace_esbr_inter_tes.c | 793 + .../libxaac/encoder/iusace_esbr_inter_tes.h | 115 + .../libxaac/encoder/iusace_esbr_pvc.c | 426 + .../libxaac/encoder/iusace_esbr_pvc.h | 131 + .../libxaac/encoder/iusace_esbr_pvc_rom.c | 251 + .../libxaac/encoder/iusace_esbr_rom.c | 32 + .../libxaac/encoder/iusace_esbr_rom.h | 24 + .../libxaac/encoder/iusace_fd_enc.h | 27 + .../libxaac/encoder/iusace_fd_fac.c | 332 + .../libxaac/encoder/iusace_fd_qc_adjthr.h | 109 + .../libxaac/encoder/iusace_fd_qc_util.h | 74 + .../libxaac/encoder/iusace_fd_quant.h | 35 + .../libxaac-sys/libxaac/encoder/iusace_fft.c | 1586 ++ .../libxaac-sys/libxaac/encoder/iusace_fft.h | 41 + .../libxaac/encoder/iusace_func_prototypes.h | 80 + .../libxaac-sys/libxaac/encoder/iusace_lpc.c | 216 + .../libxaac/encoder/iusace_lpc_avq.c | 372 + .../libxaac-sys/libxaac/encoder/iusace_lpd.h | 67 + .../libxaac/encoder/iusace_lpd_enc.c | 850 + .../libxaac/encoder/iusace_lpd_rom.c | 417 + .../libxaac/encoder/iusace_lpd_rom.h | 39 + .../libxaac/encoder/iusace_lpd_utils.c | 593 + .../libxaac-sys/libxaac/encoder/iusace_main.h | 132 + .../libxaac-sys/libxaac/encoder/iusace_ms.c | 142 + .../libxaac-sys/libxaac/encoder/iusace_ms.h | 35 + .../libxaac/encoder/iusace_psy_mod.c | 275 + .../libxaac/encoder/iusace_psy_mod.h | 150 + .../libxaac/encoder/iusace_psy_rom.c | 218 + .../libxaac/encoder/iusace_psy_utils.c | 510 + .../libxaac/encoder/iusace_psy_utils.h | 49 + .../libxaac-sys/libxaac/encoder/iusace_rom.c | 15083 ++++++++++++++++ .../libxaac-sys/libxaac/encoder/iusace_rom.h | 130 + .../encoder/iusace_signal_classifier.h | 152 + .../libxaac/encoder/iusace_tcx_enc.c | 844 + .../libxaac/encoder/iusace_tcx_mdct.c | 186 + .../libxaac/encoder/iusace_tcx_mdct.h | 29 + .../libxaac/encoder/iusace_tns_usac.c | 536 + .../libxaac/encoder/iusace_tns_usac.h | 88 + .../libxaac/encoder/iusace_type_def.h | 88 + .../libxaac/encoder/iusace_windowing.c | 181 + .../libxaac/encoder/iusace_windowing.h | 34 + .../libxaac/encoder/iusace_write_bitstream.c | 712 + .../libxaac/encoder/iusace_write_bitstream.h | 62 + .../libxaac/encoder/ixheaace_aac_constants.h | 120 + .../encoder/ixheaace_adjust_threshold.c | 991 + .../encoder/ixheaace_adjust_threshold.h | 54 + .../encoder/ixheaace_adjust_threshold_data.h | 62 + .../libxaac/encoder/ixheaace_api.c | 3921 ++++ .../libxaac/encoder/ixheaace_api.h | 166 + .../libxaac/encoder/ixheaace_api_defs.h | 47 + .../libxaac/encoder/ixheaace_asc_write.c | 701 + .../libxaac/encoder/ixheaace_asc_write.h | 48 + .../libxaac/encoder/ixheaace_basic_ops.c | 65 + .../libxaac/encoder/ixheaace_bitbuffer.c | 214 + .../libxaac/encoder/ixheaace_bitbuffer.h | 67 + .../libxaac/encoder/ixheaace_bitbuffer_hp.c | 96 + .../libxaac/encoder/ixheaace_bits_count.c | 948 + .../libxaac/encoder/ixheaace_bits_count.h | 103 + .../libxaac/encoder/ixheaace_block_switch.c | 266 + .../libxaac/encoder/ixheaace_block_switch.h | 78 + .../encoder/ixheaace_calc_ms_band_energy.c | 89 + .../encoder/ixheaace_calc_ms_band_energy.h | 32 + .../libxaac/encoder/ixheaace_channel_map.c | 211 + .../libxaac/encoder/ixheaace_channel_map.h | 29 + .../libxaac/encoder/ixheaace_common_rom.c | 1269 ++ .../libxaac/encoder/ixheaace_common_rom.h | 54 + .../libxaac/encoder/ixheaace_common_utils.h | 42 + .../libxaac/encoder/ixheaace_config.h | 50 + .../libxaac/encoder/ixheaace_config_params.h | 59 + .../libxaac/encoder/ixheaace_constants.h | 43 + .../libxaac/encoder/ixheaace_cplx_pred.c | 523 + .../libxaac/encoder/ixheaace_cplx_pred.h | 26 + .../libxaac/encoder/ixheaace_definitions.h | 32 + .../libxaac/encoder/ixheaace_dynamic_bits.c | 598 + .../libxaac/encoder/ixheaace_dynamic_bits.h | 71 + .../libxaac/encoder/ixheaace_enc_init.c | 518 + .../libxaac/encoder/ixheaace_enc_main.c | 237 + .../libxaac/encoder/ixheaace_enc_main.h | 116 + .../libxaac/encoder/ixheaace_env_bit.h | 43 + .../libxaac/encoder/ixheaace_error_codes.h | 222 + .../libxaac/encoder/ixheaace_error_handler.h | 66 + .../libxaac/encoder/ixheaace_fd_enc.c | 127 + .../libxaac/encoder/ixheaace_fd_mdct.c | 215 + .../libxaac/encoder/ixheaace_fd_qc_adjthr.c | 1709 ++ .../libxaac/encoder/ixheaace_fd_qc_util.c | 164 + .../libxaac/encoder/ixheaace_fd_quant.c | 679 + .../libxaac/encoder/ixheaace_fft.c | 2485 +++ .../libxaac/encoder/ixheaace_fft.h | 82 + .../libxaac/encoder/ixheaace_group_data.c | 191 + .../libxaac/encoder/ixheaace_group_data.h | 31 + .../libxaac/encoder/ixheaace_huffman_rom.c | 45 + .../libxaac/encoder/ixheaace_hybrid.c | 370 + .../libxaac/encoder/ixheaace_hybrid_init.c | 65 + .../libxaac/encoder/ixheaace_interface.c | 93 + .../libxaac/encoder/ixheaace_interface.h | 71 + .../encoder/ixheaace_loudness_measurement.c | 400 + .../encoder/ixheaace_loudness_measurement.h | 99 + .../libxaac/encoder/ixheaace_mdct_480.c | 489 + .../encoder/ixheaace_memory_standards.h | 41 + .../libxaac/encoder/ixheaace_mps_bitstream.c | 1031 ++ .../libxaac/encoder/ixheaace_mps_bitstream.h | 122 + .../libxaac/encoder/ixheaace_mps_buf.h | 28 + .../encoder/ixheaace_mps_common_define.h | 44 + .../libxaac/encoder/ixheaace_mps_common_fix.h | 26 + .../libxaac/encoder/ixheaace_mps_dct.c | 211 + .../libxaac/encoder/ixheaace_mps_dct.h | 24 + .../libxaac/encoder/ixheaace_mps_defines.h | 189 + .../libxaac/encoder/ixheaace_mps_delay.c | 129 + .../libxaac/encoder/ixheaace_mps_delay.h | 54 + .../encoder/ixheaace_mps_dmx_tdom_enh.c | 215 + .../encoder/ixheaace_mps_dmx_tdom_enh.h | 47 + .../libxaac/encoder/ixheaace_mps_enc.c | 1391 ++ .../libxaac/encoder/ixheaace_mps_enc.h | 58 + .../libxaac/encoder/ixheaace_mps_filter.c | 61 + .../libxaac/encoder/ixheaace_mps_filter.h | 34 + .../encoder/ixheaace_mps_frame_windowing.c | 301 + .../encoder/ixheaace_mps_frame_windowing.h | 75 + .../libxaac/encoder/ixheaace_mps_huff_tab.c | 1385 ++ .../libxaac/encoder/ixheaace_mps_huff_tab.h | 141 + .../encoder/ixheaace_mps_hybrid_filter.c | 1000 + .../libxaac/encoder/ixheaace_mps_lib.h | 58 + .../encoder/ixheaace_mps_main_structure.h | 54 + .../libxaac/encoder/ixheaace_mps_memory.h | 52 + .../libxaac/encoder/ixheaace_mps_nlc_enc.c | 1914 ++ .../libxaac/encoder/ixheaace_mps_nlc_enc.h | 32 + .../encoder/ixheaace_mps_onset_detect.c | 148 + .../encoder/ixheaace_mps_onset_detect.h | 54 + .../encoder/ixheaace_mps_param_extract.c | 344 + .../encoder/ixheaace_mps_param_extract.h | 83 + .../libxaac/encoder/ixheaace_mps_polyphase.c | 1023 ++ .../libxaac/encoder/ixheaace_mps_qmf.c | 164 + .../libxaac/encoder/ixheaace_mps_qmf.h | 43 + .../libxaac/encoder/ixheaace_mps_rom.c | 254 + .../libxaac/encoder/ixheaace_mps_rom.h | 71 + .../encoder/ixheaace_mps_sac_hybfilter.h | 39 + .../encoder/ixheaace_mps_sac_nlc_enc.h | 27 + .../encoder/ixheaace_mps_sac_polyphase.h | 34 + .../encoder/ixheaace_mps_spatial_bitstream.h | 166 + .../encoder/ixheaace_mps_static_gain.c | 74 + .../encoder/ixheaace_mps_static_gain.h | 41 + .../libxaac/encoder/ixheaace_mps_struct_def.h | 34 + .../libxaac/encoder/ixheaace_mps_structure.h | 120 + .../libxaac/encoder/ixheaace_mps_tools_rom.c | 496 + .../libxaac/encoder/ixheaace_mps_tools_rom.h | 29 + .../libxaac/encoder/ixheaace_mps_tree.c | 188 + .../libxaac/encoder/ixheaace_mps_tree.h | 90 + .../encoder/ixheaace_mps_vector_functions.c | 80 + .../encoder/ixheaace_mps_vector_functions.h | 32 + .../libxaac/encoder/ixheaace_ms_stereo.c | 137 + .../libxaac/encoder/ixheaace_ms_stereo.h | 30 + .../libxaac-sys/libxaac/encoder/ixheaace_nf.c | 178 + .../libxaac-sys/libxaac/encoder/ixheaace_nf.h | 26 + .../libxaac/encoder/ixheaace_ps_bitenc.c | 312 + .../libxaac/encoder/ixheaace_ps_enc.c | 671 + .../libxaac/encoder/ixheaace_ps_enc_init.c | 210 + .../encoder/ixheaace_psy_configuration.c | 432 + .../encoder/ixheaace_psy_configuration.h | 83 + .../libxaac/encoder/ixheaace_psy_const.h | 106 + .../libxaac/encoder/ixheaace_psy_data.h | 44 + .../libxaac/encoder/ixheaace_psy_mod.c | 671 + .../libxaac/encoder/ixheaace_psy_mod.h | 49 + .../libxaac/encoder/ixheaace_psy_utils.c | 60 + .../libxaac/encoder/ixheaace_psy_utils.h | 26 + .../encoder/ixheaace_psy_utils_spreading.c | 58 + .../encoder/ixheaace_psy_utils_spreading.h | 25 + .../libxaac/encoder/ixheaace_qc_data.h | 157 + .../libxaac/encoder/ixheaace_qc_main_hp.c | 280 + .../libxaac/encoder/ixheaace_qc_util.c | 518 + .../libxaac/encoder/ixheaace_qc_util.h | 62 + .../libxaac/encoder/ixheaace_quant.c | 54 + .../libxaac/encoder/ixheaace_quant.h | 25 + .../libxaac/encoder/ixheaace_radix2_fft.c | 175 + .../libxaac/encoder/ixheaace_resampler.c | 523 + .../libxaac/encoder/ixheaace_resampler.h | 127 + .../libxaac/encoder/ixheaace_resampler_init.c | 86 + .../libxaac/encoder/ixheaace_rom.c | 3964 ++++ .../libxaac/encoder/ixheaace_rom.h | 212 + .../libxaac/encoder/ixheaace_sbr.h | 151 + .../libxaac/encoder/ixheaace_sbr_cmondata.h | 36 + .../encoder/ixheaace_sbr_code_envelope.c | 192 + .../encoder/ixheaace_sbr_code_envelope.h | 81 + .../encoder/ixheaace_sbr_code_envelope_lp.c | 243 + .../libxaac/encoder/ixheaace_sbr_crc.c | 126 + .../libxaac/encoder/ixheaace_sbr_crc.h | 47 + .../libxaac/encoder/ixheaace_sbr_def.h | 212 + .../libxaac/encoder/ixheaace_sbr_enc_struct.h | 77 + .../libxaac/encoder/ixheaace_sbr_env_est.c | 2948 +++ .../libxaac/encoder/ixheaace_sbr_env_est.h | 59 + .../encoder/ixheaace_sbr_env_est_init.c | 169 + .../encoder/ixheaace_sbr_frame_info_gen.c | 1151 ++ .../encoder/ixheaace_sbr_frame_info_gen.h | 103 + .../encoder/ixheaace_sbr_freq_scaling.c | 692 + .../encoder/ixheaace_sbr_freq_scaling.h | 45 + .../libxaac/encoder/ixheaace_sbr_hbe.h | 252 + .../encoder/ixheaace_sbr_hbe_dft_trans.c | 1007 ++ .../libxaac/encoder/ixheaace_sbr_hbe_fft.h | 56 + .../encoder/ixheaace_sbr_hbe_fft_ifft_32x32.c | 1679 ++ .../encoder/ixheaace_sbr_hbe_polyphase.c | 329 + .../libxaac/encoder/ixheaace_sbr_hbe_trans.c | 1589 ++ .../libxaac/encoder/ixheaace_sbr_header.h | 55 + .../libxaac/encoder/ixheaace_sbr_hybrid.h | 58 + .../ixheaace_sbr_inv_filtering_estimation.c | 248 + .../ixheaace_sbr_inv_filtering_estimation.h | 56 + .../libxaac/encoder/ixheaace_sbr_main.c | 1097 ++ .../libxaac/encoder/ixheaace_sbr_main.h | 147 + .../libxaac/encoder/ixheaace_sbr_misc.c | 78 + .../libxaac/encoder/ixheaace_sbr_misc.h | 30 + .../ixheaace_sbr_missing_harmonics_det.c | 747 + .../ixheaace_sbr_missing_harmonics_det.h | 86 + .../encoder/ixheaace_sbr_noise_floor_est.c | 323 + .../encoder/ixheaace_sbr_noise_floor_est.h | 55 + .../libxaac/encoder/ixheaace_sbr_ps_bitenc.h | 33 + .../libxaac/encoder/ixheaace_sbr_ps_enc.h | 101 + .../libxaac/encoder/ixheaace_sbr_qmf_enc.c | 1356 ++ .../libxaac/encoder/ixheaace_sbr_qmf_enc.h | 82 + .../encoder/ixheaace_sbr_qmf_enc_init.c | 87 + .../libxaac/encoder/ixheaace_sbr_rom.c | 1939 ++ .../libxaac/encoder/ixheaace_sbr_rom.h | 224 + .../libxaac/encoder/ixheaace_sbr_ton_corr.c | 329 + .../libxaac/encoder/ixheaace_sbr_ton_corr.h | 101 + .../encoder/ixheaace_sbr_ton_corr_hp.c | 190 + .../libxaac/encoder/ixheaace_sbr_tran_det.c | 279 + .../libxaac/encoder/ixheaace_sbr_tran_det.h | 81 + .../encoder/ixheaace_sbr_tran_det_hp.c | 333 + .../encoder/ixheaace_sbr_write_bitstream.c | 1399 ++ .../encoder/ixheaace_sbr_write_bitstream.h | 49 + .../libxaac/encoder/ixheaace_sf_estimation.c | 782 + .../libxaac/encoder/ixheaace_sf_estimation.h | 35 + .../encoder/ixheaace_signal_classifier.c | 1076 ++ .../encoder/ixheaace_signal_classifier_rom.c | 1518 ++ .../libxaac/encoder/ixheaace_static_bits.c | 207 + .../libxaac/encoder/ixheaace_static_bits.h | 26 + .../libxaac/encoder/ixheaace_stereo_preproc.c | 256 + .../libxaac/encoder/ixheaace_stereo_preproc.h | 64 + .../libxaac/encoder/ixheaace_struct_def.h | 128 + .../libxaac/encoder/ixheaace_tns.c | 397 + .../libxaac/encoder/ixheaace_tns.h | 102 + .../libxaac/encoder/ixheaace_tns_func.h | 71 + .../libxaac/encoder/ixheaace_tns_hp.c | 245 + .../libxaac/encoder/ixheaace_tns_init.c | 240 + .../libxaac/encoder/ixheaace_tns_params.c | 148 + .../libxaac/encoder/ixheaace_tns_params.h | 53 + .../libxaac/encoder/ixheaace_version_number.h | 23 + .../encoder/ixheaace_write_adts_adif.c | 345 + .../encoder/ixheaace_write_adts_adif.h | 27 + .../encoder/ixheaace_write_bitstream.c | 828 + .../encoder/ixheaace_write_bitstream.h | 41 + .../libxaac/encoder/libxaacenc.cmake | 140 + .../libxaac-sys/libxaac/fuzzer/Android.bp | 44 + .../libxaac-sys/libxaac/fuzzer/README.md | 66 + .../libxaac-sys/libxaac/fuzzer/ossfuzz.sh | 39 + .../libxaac/fuzzer/xaac_dec_fuzzer.cmake | 7 + .../libxaac/fuzzer/xaac_dec_fuzzer.cpp | 897 + .../libxaac/fuzzer/xaac_dec_fuzzer.dict | 2 + .../libxaac/fuzzer/xaac_enc_fuzzer.cmake | 6 + .../libxaac/fuzzer/xaac_enc_fuzzer.cpp | 879 + .../libxaac/fuzzer/xaac_enc_fuzzer.dict | 2 + .../libxaac-sys/libxaac/test/Android.bp | 71 + .../test/decoder/impd_drc_config_params.h | 63 + .../libxaac/test/decoder/ixheaacd_error.c | 370 + .../libxaac/test/decoder/ixheaacd_fileifc.c | 178 + .../libxaac/test/decoder/ixheaacd_fileifc.h | 81 + .../libxaac/test/decoder/ixheaacd_main.c | 2510 +++ .../test/decoder/ixheaacd_metadata_read.c | 179 + .../test/decoder/ixheaacd_metadata_read.h | 54 + .../libxaac/test/decoder/xaacdec.cmake | 27 + .../test/encoder/impd_drc_config_params.txt | 144 + .../test/encoder/impd_drc_user_config.c | 658 + .../test/encoder/impd_drc_user_config.h | 26 + .../libxaac/test/encoder/ixheaace_error.c | 408 + .../libxaac/test/encoder/ixheaace_testbench.c | 1898 ++ .../libxaac/test/encoder/paramfilesimple.txt | 28 + .../libxaac/test/encoder/sine_2ch.wav | Bin 0 -> 1922844 bytes .../libxaac/test/encoder/xaacenc.cmake | 29 + crates/vendor/libxaac-sys/src/lib.rs | 5 + crates/vendor/libxaac-sys/wrapper.h | 17 + crates/vendor/xaac-rs/.cargo-ok | 1 + crates/vendor/xaac-rs/.cargo_vcs_info.json | 7 + crates/vendor/xaac-rs/Cargo.toml | 83 + crates/vendor/xaac-rs/Cargo.toml.orig | 40 + crates/vendor/xaac-rs/README.md | 172 + .../xaac-rs/examples/convert_wav_to_aac.rs | 187 + .../vendor/xaac-rs/examples/decode_stream.rs | 75 + crates/vendor/xaac-rs/examples/file_info.rs | 129 + .../python/__pycache__/_wav.cpython-312.pyc | Bin 0 -> 3597 bytes .../python/__pycache__/_wav.cpython-314.pyc | Bin 0 -> 4108 bytes .../convert_wav_to_aac.cpython-312.pyc | Bin 0 -> 3433 bytes .../__pycache__/decode_stream.cpython-312.pyc | Bin 0 -> 3287 bytes .../__pycache__/file_info.cpython-312.pyc | Bin 0 -> 5331 bytes crates/vendor/xaac-rs/examples/python/_wav.py | 110 + .../examples/python/convert_wav_to_aac.py | 68 + .../xaac-rs/examples/python/decode_stream.py | 75 + .../xaac-rs/examples/python/file_info.py | 114 + crates/vendor/xaac-rs/src/decoder.rs | 1465 ++ crates/vendor/xaac-rs/src/encoder.rs | 543 + crates/vendor/xaac-rs/src/error.rs | 63 + crates/vendor/xaac-rs/src/ffi.rs | 44 + crates/vendor/xaac-rs/src/lib.rs | 19 + crates/vendor/xaac-rs/src/python.rs | 1258 ++ crates/vendor/xaac-rs/src/util.rs | 112 + crates/vendor/xaac-rs/tests/codec.rs | 93 + .../src/database/playlist_formats.rs | 1 + .../vuio-core/src/media/remux/mkv_demuxer.rs | 1 + crates/vuio-core/src/media/remux/mod.rs | 5 + crates/vuio-core/src/media/scanner.rs | 1 + crates/vuio-core/src/media/transcode/ac3.rs | 147 + crates/vuio-core/src/media/transcode/dts.rs | 2 +- crates/vuio-core/src/media/transcode/mod.rs | 6 + .../vuio-core/src/media/transcode/session.rs | 3 + .../vuio-core/src/media/transcode/source.rs | 1 + crates/vuio-core/src/media/transcode/ts.rs | 150 +- crates/vuio-core/src/platform/mod.rs | 1 + .../vuio-core/tests/film_transcode_tests.rs | 28 +- log.log | 212 - plan.txt | 11 - 808 files changed, 311296 insertions(+), 250 deletions(-) create mode 100644 crates/vendor/libxaac-sys/.cargo-ok create mode 100644 crates/vendor/libxaac-sys/.cargo_vcs_info.json create mode 100644 crates/vendor/libxaac-sys/Cargo.toml create mode 100644 crates/vendor/libxaac-sys/Cargo.toml.orig create mode 100644 crates/vendor/libxaac-sys/README.md create mode 100644 crates/vendor/libxaac-sys/build.rs create mode 100644 crates/vendor/libxaac-sys/examples/sample.rs create mode 100644 crates/vendor/libxaac-sys/libxaac/.github/workflows/cifuzz.yml create mode 100644 crates/vendor/libxaac-sys/libxaac/.github/workflows/cmake.yml create mode 100644 crates/vendor/libxaac-sys/libxaac/Android.bp create mode 100644 crates/vendor/libxaac-sys/libxaac/CMakeLists.txt create mode 100644 crates/vendor/libxaac-sys/libxaac/LICENSE create mode 100644 crates/vendor/libxaac-sys/libxaac/METADATA create mode 100644 crates/vendor/libxaac-sys/libxaac/MODULE_LICENSE_APACHE2 create mode 100644 crates/vendor/libxaac-sys/libxaac/NOTICE create mode 100644 crates/vendor/libxaac-sys/libxaac/OWNERS create mode 100644 crates/vendor/libxaac-sys/libxaac/PREUPLOAD.cfg create mode 100644 crates/vendor/libxaac-sys/libxaac/README.experimental create mode 100644 crates/vendor/libxaac-sys/libxaac/README.md create mode 100644 crates/vendor/libxaac-sys/libxaac/README_dec.md create mode 100644 crates/vendor/libxaac-sys/libxaac/README_enc.md create mode 100644 crates/vendor/libxaac-sys/libxaac/README_enc_drc.md create mode 100644 crates/vendor/libxaac-sys/libxaac/cmake/toolchains/aarch32_toolchain.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/cmake/toolchains/aarch64_toolchain.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/cmake/toolchains/x86_toolchain.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/cmake/utils.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/common/common.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_basic_op.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_basic_ops.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_basic_ops16.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_basic_ops32.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_basic_ops40.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_basic_ops_arr.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_constants.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_error_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_esbr_fft.c create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_esbr_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_esbr_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_fft_ifft_32x32_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_fft_ifft_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_sbr_const.h create mode 100644 crates/vendor/libxaac-sys/libxaac/common/ixheaac_type_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ia_xheaacd_mps_mulshift.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ia_xheaacd_mps_reoder_mulshift_acc.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_aac_ld_dec_rearrange.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_apply_rot.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_apply_scale_fac.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_auto_corr.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_autocorr_st2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_calc_post_twid.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_calc_pre_twid.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_calcmaxspectralline.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_complex_fft_p2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_complex_ifft_p2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_conv_ergtoamplitude.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_conv_ergtoamplitudelp.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_cos_sin_mod.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_dct3_32.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_dec_DCT2_64_asm.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_decorr_filter2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_eld_decoder_sbr_pre_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_enery_calc_per_subband.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_esbr_cos_sin_mod_loop1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_esbr_cos_sin_mod_loop2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_esbr_fwd_modulation.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_esbr_qmfsyn64_winadd.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_esbr_radix4bfly.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_expsubbandsamples.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_ffr_divide16.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_fft32x32_ld.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_fft32x32_ld2_armv7.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_fft_15_ld.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_fft_armv7.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_function_selector_arm_non_neon.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_function_selector_armv7.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_fwd_modulation.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_harm_idx_zerotwolp.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_imdct_using_fft.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_inv_dit_fft_8pt.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_lap1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_mps_complex_fft_64_asm.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_mps_synt_out_calc.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_mps_synt_post_fft_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_mps_synt_post_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_mps_synt_pre_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_no_lap1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_overlap_add1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_overlap_add2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_post_radix_compute2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_post_radix_compute4.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_post_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_post_twiddle_overlap.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_pre_twiddle_compute.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_qmf_dec_armv7.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_radix4_bfly.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_rescale_subbandsamples.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_sbr_imdct_using_fft.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_sbr_qmfanal32_winadds.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_sbr_qmfanal32_winadds_eld.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_sbr_qmfsyn64_winadd.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_shiftrountine.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_shiftrountine_with_rnd_eld.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_shiftrountine_with_round.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_shiftrountine_with_round_hq.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_tns_ar_filter_fixed.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_tns_ar_filter_fixed_32x16.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/ixheaacd_tns_parcor2lpc_32x16.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv7/libxaacdec_armv7.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_apply_scale_factors.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_calcmaxspectralline.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_cos_sin_mod_loop1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_cos_sin_mod_loop2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_fft32x32_ld2_armv8.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_function_selector_armv8.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_imdct_using_fft.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_inv_dit_fft_8pt.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_no_lap1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_overlap_add1.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_overlap_add2.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_post_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_post_twiddle_overlap.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_postradixcompute4.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_pre_twiddle.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_qmf_dec_armv8.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_sbr_imdct_using_fft.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_sbr_qmf_analysis32_neon.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_sbr_qmfsyn64_winadd.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_shiftrountine_with_round.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/ixheaacd_shiftrountine_with_round_eld.s create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/armv8/libxaacdec_armv8.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_apicmd_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_api.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_api_defs.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_api_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_bitbuffer.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_bitbuffer.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_bitstream_dec_api.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_common.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_config_params.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_definitions.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_dynamic_payload.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_eq.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_eq.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_error_codes.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_extr_delta_coded_info.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_extr_delta_coded_info.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_filter_bank.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_filter_bank.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_gain_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_gain_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_gain_decoder.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_gain_decoder.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_hashdefines.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_interface.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_interface_decoder.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_loudness_control.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_loudness_control.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_main_td_process.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_multi_band.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_multiband.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_parametric_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_parser.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_parser_interface.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_peak_limiter.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_peak_limiter.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_peak_limiter_struct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_process.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_process_audio.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_qmf_filter.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_sel_proc_drc_set_sel.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_selection_process.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_selection_process.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_selection_process_drcset_selection.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_selection_process_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_shape_filter.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_static_payload.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_struct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_bitstream_dec_api.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_eq.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_gain_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_interface.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_loud_eq.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_multi_band.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_parser.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_process_audio.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_sel_proc_drc_set_sel.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_sel_proc_init.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_sel_proc_loudness_control.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_shape_filter.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_drc_uni_tables.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_error_handler.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_error_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_memory_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_parametric_drc_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/impd_type_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/drc_src/libxaacdec_drc.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/generic/ixheaacd_function_selector_generic.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/generic/ixheaacd_qmf_dec_generic.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_Windowing.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_config.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_ec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_imdct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_imdct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aac_tns.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aacdec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aacdecoder.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_aacpluscheck.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_acelp_bitparse.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_acelp_com.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_acelp_decode.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_acelp_info.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_acelp_mdct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_acelp_tools.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_adts.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_adts_crc_check.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_adts_crc_check.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_api.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_api_defs.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_apicmd_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_arith_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_arith_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_audioobjtypes.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_avq_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_avq_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_basic_funcs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_basic_funcs.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_basic_ops.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_bit_extract.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_bitbuffer.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_bitbuffer.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_block.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_block.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_channel.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_channel.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_channelinfo.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_cnst.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_common_initfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_common_lpfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_common_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_common_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_config.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_create.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_create.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_dec_main.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_decode_main.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_defines.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_definitions.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_drc_data_struct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_drc_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_drc_freq_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_dsp_fft32x32s.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_dsp_fft32x32s.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ec_defines.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ec_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ec_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ec_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_calc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_calc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_extr.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_extr.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_env_extr_part.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_error_codes.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_error_handler.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_error_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_esbr_envcal.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_esbr_polyphase.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ext_ch_ele.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_fft.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_fft_ifft_32x32.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_freq_sca.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_freq_sca.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_func_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_function_selector.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_fwd_alias_cnx.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_hbe_dft_trans.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_hbe_trans.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_hcr.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_headerdecode.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_headerdecode.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_huff_code_reorder.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_huff_tools.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_hufftables.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_hybrid.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_hybrid.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_imdct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_info.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_init_config.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_initfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_interface.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_intrinsics.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_latmdemux.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_latmdemux.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ld_mps_config.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ld_mps_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ld_mps_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_longblock.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lpc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lpc_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lpfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lpp_tran.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lpp_tran.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lt_predict.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_lt_predict.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_main.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_memory_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_aac_struct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_apply_common.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_apply_common.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_apply_m1.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_apply_m2.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_basic_op.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_bitdec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_bitdec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_blind.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_blind.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_common.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_common.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_emm.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_tree_515x.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_tree_51sx.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_tree_52xx.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_tree_727x.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_tree_757x.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_calc_m1m2_tree_config.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_decor.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_decorr.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_defines.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_get_index.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_get_index.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_huff_tab.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_hybfilter.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_hybrid_filt.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_initfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_interface.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_m1m2.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_m1m2_common.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_macro_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_mdct_2_qmf.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_mdct_2_qmf.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_nlc_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_parse.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_poly_filt.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_polyphase.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_polyphase.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_pre_mix.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_process.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_process.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_block.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_block.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_channel.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_channel.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_channel_info.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_channelinfo.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_huffman.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_longblock.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_pns_js_thumb.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_pulsedata.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_pulsedata.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_tns.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_res_tns.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_reshape_bb_env.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_reshape_bb_env.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_smoothing.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_smoothing.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_tables.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_temp_process.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_temp_reshape.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_tonality.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_tonality.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_mps_tp_process.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_multichannel.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_multichannel.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_peak_limiter.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_peak_limiter_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pns.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pns_js_thumb.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pred_vec_block.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_process.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_process.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ps_bitdec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ps_bitdec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ps_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ps_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ps_dec_flt.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pulsedata.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pvc_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pvc_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_pvc_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_qmf_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_qmf_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_qmf_poly.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_rev_vlc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_rvlc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_common.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_crc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_crc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_dec.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_payload.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbr_scale.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbrdec_initfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbrdec_lpfuncs.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbrdecoder.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbrdecoder.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbrdecsettings.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_sbrqmftrans.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_spectrum_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_stereo.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_stereo.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_struct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_tcx_fwd_alcnx.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_tcx_fwd_mdct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_td_mdct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_thumb_ps_dec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_tns.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_tns.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_tns_usac.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_type_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_usac_ec.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_vec_baisc_ops.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_ver_number.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/ixheaacd_windows.h create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/libxaacdec.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/x86/ixheaacd_function_selector_x86.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/x86/libxaacdec_x86.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/x86_64/ixheaacd_function_selector_x86_64.c create mode 100644 crates/vendor/libxaac-sys/libxaac/decoder/x86_64/libxaacdec_x86_64.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/docs/Api_flowchart_dec.png create mode 100644 crates/vendor/libxaac-sys/libxaac/docs/Api_flowchart_enc.png create mode 100644 crates/vendor/libxaac-sys/libxaac/docs/LIBXAAC-Enc-API.pdf create mode 100644 crates/vendor/libxaac-sys/libxaac/docs/LIBXAAC-Enc-GSG.pdf create mode 100644 crates/vendor/libxaac-sys/libxaac/docs/libxaac_block_diagram.jpg create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_api.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_api.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_common_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_gain_calculator.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_gain_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_gain_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_mux.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_mux.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_tables.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_tables.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_uni_drc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_uni_drc_eq.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_uni_drc_eq.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_uni_drc_filter_bank.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/impd_drc_uni_drc_filter_bank.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/drc_src/libxaacenc_drc.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_acelp_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_acelp_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_acelp_tools.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_arith_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_arith_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_avq_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_avq_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_avq_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_basic_ops_flt.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_bitbuffer.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_bitbuffer.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_block_switch.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_block_switch.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_block_switch_const.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_block_switch_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_cnst.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_config.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_enc_fac.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_enc_main.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_inter_tes.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_inter_tes.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_pvc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_pvc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_pvc_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_esbr_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fd_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fd_fac.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fd_qc_adjthr.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fd_qc_util.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fd_quant.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fft.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_fft.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_func_prototypes.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpc_avq.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpd.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpd_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpd_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpd_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_lpd_utils.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_main.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_ms.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_ms.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_psy_mod.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_psy_mod.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_psy_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_psy_utils.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_psy_utils.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_signal_classifier.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_tcx_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_tcx_mdct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_tcx_mdct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_tns_usac.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_tns_usac.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_type_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_windowing.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_windowing.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_write_bitstream.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/iusace_write_bitstream.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_aac_constants.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_adjust_threshold.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_adjust_threshold.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_adjust_threshold_data.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_api.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_api.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_api_defs.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_asc_write.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_asc_write.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_basic_ops.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_bitbuffer.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_bitbuffer.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_bitbuffer_hp.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_bits_count.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_bits_count.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_block_switch.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_block_switch.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_calc_ms_band_energy.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_calc_ms_band_energy.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_channel_map.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_channel_map.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_common_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_common_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_common_utils.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_config.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_config_params.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_constants.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_cplx_pred.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_cplx_pred.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_definitions.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_dynamic_bits.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_dynamic_bits.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_enc_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_enc_main.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_enc_main.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_env_bit.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_error_codes.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_error_handler.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fd_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fd_mdct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fd_qc_adjthr.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fd_qc_util.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fd_quant.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fft.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_fft.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_group_data.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_group_data.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_huffman_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_hybrid.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_hybrid_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_interface.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_interface.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_loudness_measurement.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_loudness_measurement.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mdct_480.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_memory_standards.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_bitstream.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_bitstream.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_buf.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_common_define.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_common_fix.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_dct.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_dct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_defines.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_delay.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_delay.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_dmx_tdom_enh.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_dmx_tdom_enh.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_filter.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_filter.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_frame_windowing.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_frame_windowing.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_huff_tab.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_huff_tab.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_hybrid_filter.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_lib.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_main_structure.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_memory.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_nlc_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_nlc_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_onset_detect.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_onset_detect.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_param_extract.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_param_extract.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_polyphase.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_qmf.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_qmf.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_sac_hybfilter.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_sac_nlc_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_sac_polyphase.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_spatial_bitstream.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_static_gain.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_static_gain.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_structure.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_tools_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_tools_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_tree.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_tree.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_vector_functions.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_mps_vector_functions.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_ms_stereo.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_ms_stereo.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_nf.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_nf.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_ps_bitenc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_ps_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_ps_enc_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_configuration.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_configuration.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_const.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_data.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_mod.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_mod.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_utils.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_utils.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_utils_spreading.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_psy_utils_spreading.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_qc_data.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_qc_main_hp.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_qc_util.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_qc_util.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_quant.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_quant.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_radix2_fft.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_resampler.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_resampler.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_resampler_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_cmondata.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_code_envelope.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_code_envelope.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_code_envelope_lp.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_crc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_crc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_enc_struct.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_env_est.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_env_est.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_env_est_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_frame_info_gen.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_frame_info_gen.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_freq_scaling.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_freq_scaling.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hbe.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hbe_dft_trans.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hbe_fft.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hbe_fft_ifft_32x32.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hbe_polyphase.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hbe_trans.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_header.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_hybrid.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_inv_filtering_estimation.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_inv_filtering_estimation.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_main.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_main.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_misc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_misc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_missing_harmonics_det.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_missing_harmonics_det.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_noise_floor_est.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_noise_floor_est.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_ps_bitenc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_ps_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_qmf_enc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_qmf_enc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_qmf_enc_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_rom.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_ton_corr.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_ton_corr.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_ton_corr_hp.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_tran_det.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_tran_det.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_tran_det_hp.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_write_bitstream.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sbr_write_bitstream.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sf_estimation.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_sf_estimation.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_signal_classifier.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_signal_classifier_rom.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_static_bits.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_static_bits.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_stereo_preproc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_stereo_preproc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_struct_def.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns_func.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns_hp.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns_init.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns_params.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_tns_params.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_version_number.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_write_adts_adif.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_write_adts_adif.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_write_bitstream.c create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/ixheaace_write_bitstream.h create mode 100644 crates/vendor/libxaac-sys/libxaac/encoder/libxaacenc.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/Android.bp create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/README.md create mode 100755 crates/vendor/libxaac-sys/libxaac/fuzzer/ossfuzz.sh create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/xaac_dec_fuzzer.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/xaac_dec_fuzzer.cpp create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/xaac_dec_fuzzer.dict create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/xaac_enc_fuzzer.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/xaac_enc_fuzzer.cpp create mode 100644 crates/vendor/libxaac-sys/libxaac/fuzzer/xaac_enc_fuzzer.dict create mode 100644 crates/vendor/libxaac-sys/libxaac/test/Android.bp create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/impd_drc_config_params.h create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/ixheaacd_error.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/ixheaacd_fileifc.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/ixheaacd_fileifc.h create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/ixheaacd_main.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/ixheaacd_metadata_read.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/ixheaacd_metadata_read.h create mode 100644 crates/vendor/libxaac-sys/libxaac/test/decoder/xaacdec.cmake create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/impd_drc_config_params.txt create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/impd_drc_user_config.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/impd_drc_user_config.h create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/ixheaace_error.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/ixheaace_testbench.c create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/paramfilesimple.txt create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/sine_2ch.wav create mode 100644 crates/vendor/libxaac-sys/libxaac/test/encoder/xaacenc.cmake create mode 100644 crates/vendor/libxaac-sys/src/lib.rs create mode 100644 crates/vendor/libxaac-sys/wrapper.h create mode 100644 crates/vendor/xaac-rs/.cargo-ok create mode 100644 crates/vendor/xaac-rs/.cargo_vcs_info.json create mode 100644 crates/vendor/xaac-rs/Cargo.toml create mode 100644 crates/vendor/xaac-rs/Cargo.toml.orig create mode 100644 crates/vendor/xaac-rs/README.md create mode 100644 crates/vendor/xaac-rs/examples/convert_wav_to_aac.rs create mode 100644 crates/vendor/xaac-rs/examples/decode_stream.rs create mode 100644 crates/vendor/xaac-rs/examples/file_info.rs create mode 100644 crates/vendor/xaac-rs/examples/python/__pycache__/_wav.cpython-312.pyc create mode 100644 crates/vendor/xaac-rs/examples/python/__pycache__/_wav.cpython-314.pyc create mode 100644 crates/vendor/xaac-rs/examples/python/__pycache__/convert_wav_to_aac.cpython-312.pyc create mode 100644 crates/vendor/xaac-rs/examples/python/__pycache__/decode_stream.cpython-312.pyc create mode 100644 crates/vendor/xaac-rs/examples/python/__pycache__/file_info.cpython-312.pyc create mode 100644 crates/vendor/xaac-rs/examples/python/_wav.py create mode 100644 crates/vendor/xaac-rs/examples/python/convert_wav_to_aac.py create mode 100644 crates/vendor/xaac-rs/examples/python/decode_stream.py create mode 100644 crates/vendor/xaac-rs/examples/python/file_info.py create mode 100644 crates/vendor/xaac-rs/src/decoder.rs create mode 100644 crates/vendor/xaac-rs/src/encoder.rs create mode 100644 crates/vendor/xaac-rs/src/error.rs create mode 100644 crates/vendor/xaac-rs/src/ffi.rs create mode 100644 crates/vendor/xaac-rs/src/lib.rs create mode 100644 crates/vendor/xaac-rs/src/python.rs create mode 100644 crates/vendor/xaac-rs/src/util.rs create mode 100644 crates/vendor/xaac-rs/tests/codec.rs create mode 100644 crates/vuio-core/src/media/transcode/ac3.rs delete mode 100644 log.log delete mode 100644 plan.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5147a3f9..33ea6814 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -393,7 +393,7 @@ jobs: mem: "6144" cache-after-prepare: true prepare: | - pkg install -y curl bash git + pkg install -y curl bash git cmake curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal run: | set -e @@ -431,7 +431,7 @@ jobs: mem: "6144" cache-after-prepare: true prepare: | - pkg install -y curl bash git rust + pkg install -y curl bash git rust cmake run: | set -e rustc -vV @@ -552,7 +552,7 @@ jobs: mem: "6144" cache-after-prepare: true prepare: | - pkg install -y curl bash git + pkg install -y curl bash git cmake curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal run: | set -e @@ -623,7 +623,7 @@ jobs: mem: "6144" cache-after-prepare: true prepare: | - pkg install -y curl bash git rust + pkg install -y curl bash git rust cmake run: | set -e rustc -vV diff --git a/Cargo.toml b/Cargo.toml index 30ac22ae..110fda41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ members = [ "crates/vendor/oxideav-core", "crates/vendor/oxideav-ac3", "crates/vendor/oxideav-dts", + "crates/vendor/libxaac-sys", + "crates/vendor/xaac-rs", ] # `vuio-bench` is deliberately not a default member. Cargo unifies features across @@ -32,6 +34,10 @@ default-members = [ ] resolver = "2" +[patch.crates-io] +libxaac-sys = { path = "crates/vendor/libxaac-sys" } +xaac-rs = { path = "crates/vendor/xaac-rs" } + # Optimizing codecs, encoders, and demuxers in dev profile gives fast real-time # audio decoding and encoding without requiring release rebuilds. [profile.dev.package.oxideav-core] diff --git a/crates/vendor/libxaac-sys/.cargo-ok b/crates/vendor/libxaac-sys/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/crates/vendor/libxaac-sys/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/crates/vendor/libxaac-sys/.cargo_vcs_info.json b/crates/vendor/libxaac-sys/.cargo_vcs_info.json new file mode 100644 index 00000000..a69bb372 --- /dev/null +++ b/crates/vendor/libxaac-sys/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "df357b887a1aa96398f9eb319931ee51cf3377fe" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/crates/vendor/libxaac-sys/Cargo.toml b/crates/vendor/libxaac-sys/Cargo.toml new file mode 100644 index 00000000..ffb0441a --- /dev/null +++ b/crates/vendor/libxaac-sys/Cargo.toml @@ -0,0 +1,70 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.85" +name = "libxaac-sys" +version = "0.1.0" +authors = ["Yehor Smoliakov "] +build = "build.rs" +links = "xaac" +include = [ + "Cargo.toml", + "README.md", + "build.rs", + "wrapper.h", + "examples/**", + "src/**", + "libxaac/**", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Rust FFI bindings to the libxaac AAC/xHE-AAC encoder and decoder library" +documentation = "https://docs.rs/libxaac-sys" +readme = "README.md" +keywords = [ + "aac", + "xhe-aac", + "ffi", + "bindings", + "audio", +] +categories = [ + "external-ffi-bindings", + "multimedia::audio", +] +license = "Apache-2.0" + +[features] +bundled = [] +default = [ + "bundled", + "static", +] +dynamic = [] +static = [] + +[lib] +name = "libxaac_sys" +path = "src/lib.rs" + +[[example]] +name = "sample" +path = "examples/sample.rs" + +[dependencies] + +[build-dependencies.bindgen] +version = "0.72" diff --git a/crates/vendor/libxaac-sys/Cargo.toml.orig b/crates/vendor/libxaac-sys/Cargo.toml.orig new file mode 100644 index 00000000..e8c9d57b --- /dev/null +++ b/crates/vendor/libxaac-sys/Cargo.toml.orig @@ -0,0 +1,38 @@ +[package] +name = "libxaac-sys" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +authors = ["Yehor Smoliakov "] +description = "Rust FFI bindings to the libxaac AAC/xHE-AAC encoder and decoder library" +readme = "README.md" +license = "Apache-2.0" +documentation = "https://docs.rs/libxaac-sys" +keywords = ["aac", "xhe-aac", "ffi", "bindings", "audio"] +categories = ["external-ffi-bindings", "multimedia::audio"] +links = "xaac" +include = [ + "Cargo.toml", + "README.md", + "build.rs", + "wrapper.h", + "examples/**", + "src/**", + "libxaac/**", +] + +[features] +default = ["bundled", "static"] +bundled = [] +static = [] +dynamic = [] + +[dependencies] + +[build-dependencies] +bindgen = "0.72" + +[profile.release] +lto = true +codegen-units = 1 +strip = "symbols" diff --git a/crates/vendor/libxaac-sys/README.md b/crates/vendor/libxaac-sys/README.md new file mode 100644 index 00000000..d5006908 --- /dev/null +++ b/crates/vendor/libxaac-sys/README.md @@ -0,0 +1,55 @@ +# libxaac-sys + +Rust FFI bindings for the vendored `libxaac` C library. + +This crate exposes low-level bindings to the `libxaac` encoder and decoder APIs. +By default it builds the bundled upstream sources and does not require a system +installation of `libxaac`. + +Upstream project: + +## Features + +- `bundled`: + Build the vendored `libxaac` sources with CMake. Enabled by default. +- `static`: + Prefer static linking. Enabled by default. +- `dynamic`: + Prefer dynamic linking when using a system-provided `libxaac`. + +`static` and `dynamic` are mutually exclusive. + +## Linking Modes + +Default: + +```toml +[dependencies] +libxaac-sys = "0.1" +``` + +Bundled static build: + +```toml +[dependencies] +libxaac-sys = { version = "0.1", features = ["bundled", "static"] } +``` + +System dynamic linking: + +```toml +[dependencies] +libxaac-sys = { version = "0.1", default-features = false, features = ["dynamic"] } +``` + +System static linking: + +```toml +[dependencies] +libxaac-sys = { version = "0.1", default-features = false, features = ["static"] } +``` + +## License + +This crate is licensed under Apache-2.0. The vendored upstream `libxaac` +sources are included under their Apache-2.0 license in `libxaac/LICENSE`. diff --git a/crates/vendor/libxaac-sys/build.rs b/crates/vendor/libxaac-sys/build.rs new file mode 100644 index 00000000..60ba8e94 --- /dev/null +++ b/crates/vendor/libxaac-sys/build.rs @@ -0,0 +1,195 @@ +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=wrapper.h"); + println!("cargo:rerun-if-changed=libxaac"); + + let bundled = env::var_os("CARGO_FEATURE_BUNDLED").is_some(); + let prefer_static = env::var_os("CARGO_FEATURE_STATIC").is_some(); + let prefer_dynamic = env::var_os("CARGO_FEATURE_DYNAMIC").is_some(); + + assert!( + !(prefer_static && prefer_dynamic), + "`static` and `dynamic` features are mutually exclusive" + ); + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir")); + let source_dir = manifest_dir.join("libxaac"); + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR")); + + if bundled { + let processor = cmake_processor(); + let build_dir = out_dir.join("cmake-build"); + + if build_dir.exists() { + std::fs::remove_dir_all(&build_dir) + .unwrap_or_else(|err| panic!("failed to clean {}: {err}", build_dir.display())); + } + + run(Command::new("cmake") + .arg("-S") + .arg(&source_dir) + .arg("-B") + .arg(&build_dir) + .arg(format!("-DCMAKE_SYSTEM_PROCESSOR={processor}")) + .arg("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")); + let config_type = if env::var("PROFILE").unwrap_or_default() == "release" { + "Release" + } else { + "Debug" + }; + + run(Command::new("cmake") + .arg("--build") + .arg(&build_dir) + .arg("--config") + .arg(config_type) + .arg("--target") + .arg("libxaacenc") + .arg("libxaacdec")); + + let find_lib = |base_name: &str| -> (PathBuf, String) { + let candidates = [ + format!("lib{base_name}.a"), + format!("{base_name}.a"), + format!("lib{base_name}.lib"), + format!("{base_name}.lib"), + ]; + for candidate in &candidates { + if let Some(p) = find_file(&build_dir, candidate) { + let stem = p.file_stem().unwrap().to_str().unwrap().to_string(); + return (p, stem); + } + } + panic!( + "failed to locate library for {base_name} under {}", + build_dir.display() + ); + }; + + let (enc_path, enc_stem) = find_lib("xaacenc"); + let (dec_path, dec_stem) = find_lib("xaacdec"); + + let enc_dir = enc_path + .parent() + .expect("static library missing parent directory"); + let dec_dir = dec_path + .parent() + .expect("static library missing parent directory"); + + println!("cargo:rustc-link-search=native={}", enc_dir.display()); + if dec_dir != enc_dir { + println!("cargo:rustc-link-search=native={}", dec_dir.display()); + } + + let is_msvc = env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc"); + let (enc_link, dec_link) = if is_msvc { + (enc_stem, dec_stem) + } else { + ( + enc_stem + .strip_prefix("lib") + .unwrap_or(&enc_stem) + .to_string(), + dec_stem + .strip_prefix("lib") + .unwrap_or(&dec_stem) + .to_string(), + ) + }; + + let link_kind = if bundled || prefer_static { + "static" + } else { + "dylib" + }; + + println!("cargo:rustc-link-lib={link_kind}={enc_link}"); + println!("cargo:rustc-link-lib={link_kind}={dec_link}"); + } + + if bundled && prefer_dynamic { + println!( + "cargo:warning=`dynamic` requested with `bundled`, but vendored libxaac only builds static libraries; using static linking" + ); + } + + if !bundled { + let link_kind = if prefer_static { "static" } else { "dylib" }; + + println!("cargo:rustc-link-lib={link_kind}=xaacenc"); + println!("cargo:rustc-link-lib={link_kind}=xaacdec"); + } + + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os != "windows" { + println!("cargo:rustc-link-lib=m"); + } + + let bindings = bindgen::Builder::default() + .header(manifest_dir.join("wrapper.h").display().to_string()) + .clang_arg(format!("-I{}", manifest_dir.display())) + .clang_arg(format!("-I{}", source_dir.join("common").display())) + .clang_arg(format!("-I{}", source_dir.join("decoder").display())) + .clang_arg(format!( + "-I{}", + source_dir.join("decoder/drc_src").display() + )) + .clang_arg(format!("-I{}", source_dir.join("encoder").display())) + .clang_arg(format!( + "-I{}", + source_dir.join("encoder/drc_src").display() + )) + .allowlist_function("ixheaace_(get_lib_id_strings|create|process|delete)") + .allowlist_function("ixheaacd_(get_lib_id_strings|dec_api|dec_main)") + .allowlist_function("ia_drc_dec_api") + .allowlist_type("ixheaace_.*") + .allowlist_type("ia_(mem_info_struct|lib_info_struct)") + .allowlist_var("(IA|IXHEAACE|AOT|DEFAULT_MEM_ALIGN_8).*") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("failed to generate bindings"); + + bindings + .write_to_file(out_dir.join("bindings.rs")) + .expect("failed to write bindings"); +} + +fn cmake_processor() -> &'static str { + match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { + Ok("x86_64") => "x86_64", + Ok("x86") => "i686", + Ok("aarch64") => "aarch64", + Ok("arm") => "aarch32", + Ok(other) => panic!("unsupported target arch: {other}"), + Err(_) => panic!("missing CARGO_CFG_TARGET_ARCH"), + } +} + +fn find_file(dir: &Path, file_name: &str) -> Option { + if !dir.exists() { + return None; + } + + let entries = std::fs::read_dir(dir).ok()?; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if let Some(found) = find_file(&path, file_name) { + return Some(found); + } + } else if path.file_name().and_then(|name| name.to_str()) == Some(file_name) { + return Some(path); + } + } + + None +} + +fn run(command: &mut Command) { + let status = command.status().expect("failed to spawn command"); + assert!(status.success(), "command failed with status {status}"); +} diff --git a/crates/vendor/libxaac-sys/examples/sample.rs b/crates/vendor/libxaac-sys/examples/sample.rs new file mode 100644 index 00000000..425adc28 --- /dev/null +++ b/crates/vendor/libxaac-sys/examples/sample.rs @@ -0,0 +1,183 @@ +use std::alloc::{Layout, alloc_zeroed, dealloc}; +use std::ffi::{CStr, c_void}; +use std::mem::{align_of, size_of, zeroed}; +use std::ptr; + +use libxaac_sys::{ + AOT_AAC_LC, DEFAULT_MEM_ALIGN_8, IA_API_CMD_GET_API_SIZE, IA_MEMTYPE_INPUT, IA_MEMTYPE_OUTPUT, + ia_lib_info_struct, ixheaacd_dec_api, ixheaacd_get_lib_id_strings, ixheaace_delete, + ixheaace_get_lib_id_strings, ixheaace_process, ixheaace_user_config_struct, ixheaace_version, +}; + +fn main() { + print_library_versions(); + print_decoder_api_size(); + + let (mut config, pcm_input_size) = build_encoder_config(); + + let create_status = unsafe { + libxaac_sys::ixheaace_create( + (&mut config.input_config as *mut _) as *mut c_void, + (&mut config.output_config as *mut _) as *mut c_void, + ) + }; + assert_eq!(create_status, 0, "ixheaace_create failed: {create_status}"); + + let process_obj = config.output_config.pv_ia_process_api_obj; + let in_buf = config.output_config.mem_info_table[IA_MEMTYPE_INPUT as usize].mem_ptr as *mut u8; + let out_buf = + config.output_config.mem_info_table[IA_MEMTYPE_OUTPUT as usize].mem_ptr as *const u8; + + unsafe { + ptr::write_bytes(in_buf, 0, pcm_input_size); + } + + let asc_len = config.output_config.i_out_bytes as usize; + println!("encoder input frame bytes: {pcm_input_size}"); + if asc_len != 0 { + println!("audio specific config bytes: {:02x?}", unsafe { + std::slice::from_raw_parts(out_buf, asc_len) + }); + } + + let process_status = unsafe { + ixheaace_process( + process_obj, + (&mut config.input_config as *mut _) as *mut c_void, + (&mut config.output_config as *mut _) as *mut c_void, + ) + }; + assert_eq!( + process_status, 0, + "ixheaace_process failed: {process_status}" + ); + + let encoded_len = config.output_config.i_out_bytes as usize; + let encoded = unsafe { std::slice::from_raw_parts(out_buf, encoded_len) }; + println!("encoded silent AAC frame bytes: {encoded_len}"); + println!( + "first frame prefix: {:02x?}", + &encoded[..encoded.len().min(16)] + ); + + let delete_status = + unsafe { ixheaace_delete((&mut config.output_config as *mut _) as *mut c_void) }; + assert_eq!(delete_status, 0, "ixheaace_delete failed: {delete_status}"); +} + +fn build_encoder_config() -> (ixheaace_user_config_struct, usize) { + let mut config: ixheaace_user_config_struct = unsafe { zeroed() }; + + config.output_config.malloc_xheaace = Some(xaac_alloc); + config.output_config.free_xheaace = Some(xaac_free); + + config.input_config.ui_pcm_wd_sz = 16; + config.input_config.i_bitrate = 48_000; + config.input_config.frame_length = 1024; + config.input_config.aot = AOT_AAC_LC as i32; + config.input_config.i_channels = 2; + config.input_config.i_samp_freq = 44_100; + config.input_config.i_native_samp_freq = 44_100; + config.input_config.i_mps_tree_config = -1; + config.input_config.i_use_mps = 0; + config.input_config.i_use_adts = 0; + config.input_config.i_use_es = 1; + config.input_config.random_access_interval = 0; + + config.input_config.aac_config.sample_rate = 44_100; + config.input_config.aac_config.bitrate = 48_000; + config.input_config.aac_config.num_channels_in = 2; + config.input_config.aac_config.num_channels_out = 2; + config.input_config.aac_config.use_tns = 1; + config.input_config.aac_config.bitreservoir_size = 768; + config.input_config.aac_config.length = 1024 * 2 * 2; + + let pcm_input_size = config.input_config.frame_length as usize + * config.input_config.i_channels as usize + * (config.input_config.ui_pcm_wd_sz as usize / 8); + + (config, pcm_input_size) +} + +fn print_library_versions() { + let mut enc_version: ixheaace_version = unsafe { zeroed() }; + let mut dec_version = unsafe { zeroed::() }; + + let enc_status = + unsafe { ixheaace_get_lib_id_strings((&mut enc_version as *mut _) as *mut c_void) }; + assert_eq!( + enc_status, 0, + "ixheaace_get_lib_id_strings failed: {enc_status}" + ); + unsafe { ixheaacd_get_lib_id_strings((&mut dec_version as *mut _) as *mut c_void) }; + + println!( + "encoder: {} {}", + c_string(enc_version.p_lib_name), + c_string(enc_version.p_version_num) + ); + println!( + "decoder: {} {}", + c_string(dec_version.p_lib_name), + c_string(dec_version.p_version_num) + ); +} + +fn print_decoder_api_size() { + let mut api_size = 0u32; + let status = unsafe { + ixheaacd_dec_api( + ptr::null_mut(), + IA_API_CMD_GET_API_SIZE as i32, + 0, + (&mut api_size as *mut _) as *mut c_void, + ) + }; + assert_eq!(status, 0, "ixheaacd_dec_api(GET_API_SIZE) failed: {status}"); + println!("decoder api object size: {api_size} bytes"); +} + +fn c_string(ptr: *mut i8) -> String { + if ptr.is_null() { + return "".to_owned(); + } + + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() +} + +unsafe extern "C" fn xaac_alloc(size: u32, alignment: u32) -> *mut c_void { + let requested_align = alignment.max(DEFAULT_MEM_ALIGN_8).next_power_of_two() as usize; + let header_words = 2 * size_of::(); + let total_size = size as usize + requested_align + header_words; + let layout = Layout::from_size_align(total_size, align_of::()).unwrap(); + let base = unsafe { alloc_zeroed(layout) }; + if base.is_null() { + return ptr::null_mut(); + } + + let aligned_addr = + (base as usize + header_words + requested_align - 1) & !(requested_align - 1); + let aligned = aligned_addr as *mut u8; + let meta = unsafe { aligned.cast::().sub(2) }; + unsafe { + meta.write(base as usize); + meta.add(1).write(total_size); + } + aligned.cast() +} + +unsafe extern "C" fn xaac_free(ptr: *mut c_void) { + if ptr.is_null() { + return; + } + + let meta = unsafe { (ptr as *mut usize).sub(2) }; + let base = unsafe { meta.read() } as *mut u8; + let total_size = unsafe { meta.add(1).read() }; + let layout = Layout::from_size_align(total_size, align_of::()).unwrap(); + unsafe { + dealloc(base, layout); + } +} diff --git a/crates/vendor/libxaac-sys/libxaac/.github/workflows/cifuzz.yml b/crates/vendor/libxaac-sys/libxaac/.github/workflows/cifuzz.yml new file mode 100644 index 00000000..a7f489ad --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/.github/workflows/cifuzz.yml @@ -0,0 +1,27 @@ +name: CIFuzz +on: [pull_request] +jobs: + Fuzzing: + runs-on: ubuntu-latest + strategy: + matrix: + sanitizer: [address, memory] + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'libxaac' + language: c++ + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'libxaac' + language: c++ + fuzz-seconds: 600 + - name: Upload Crash + uses: actions/upload-artifact@v4 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/crates/vendor/libxaac-sys/libxaac/.github/workflows/cmake.yml b/crates/vendor/libxaac-sys/libxaac/.github/workflows/cmake.yml new file mode 100644 index 00000000..0ec6d70b --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/.github/workflows/cmake.yml @@ -0,0 +1,23 @@ +name: CMake + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/out -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + + - name: Build + run: cmake --build ${{github.workspace}}/out --config ${{env.BUILD_TYPE}} diff --git a/crates/vendor/libxaac-sys/libxaac/Android.bp b/crates/vendor/libxaac-sys/libxaac/Android.bp new file mode 100644 index 00000000..99f361d0 --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/Android.bp @@ -0,0 +1,523 @@ +package { + default_applicable_licenses: ["external_libxaac_license"], +} + +// Added automatically by a large-scale-change +// +// large-scale-change included anything that looked like it might be a license +// text as a license_text. e.g. LICENSE, NOTICE, COPYING etc. +// +// Please consider removing redundant or irrelevant files from 'license_text:'. +// See: http://go/android-license-faq +license { + name: "external_libxaac_license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-Apache-2.0", + ], + license_text: [ + "LICENSE", + "NOTICE", + ], +} + +cc_library_static { + name: "libxaacdec", + + vendor_available: true, + host_supported: true, + cflags: [ + "-O3", + "-DLOUDNESS_LEVELING_SUPPORT", + ], + + export_include_dirs: [ + "common", + "decoder", + "decoder/drc_src", + ], + + srcs: [ + "common/ixheaac_esbr_fft.c", + "common/ixheaac_esbr_rom.c", + "common/ixheaac_fft_ifft_32x32_rom.c", + "decoder/ixheaacd_aacdecoder.c", + "decoder/ixheaacd_aacpluscheck.c", + "decoder/ixheaacd_aac_ec.c", + "decoder/ixheaacd_aac_imdct.c", + "decoder/ixheaacd_aac_rom.c", + "decoder/ixheaacd_aac_tns.c", + "decoder/ixheaacd_acelp_bitparse.c", + "decoder/ixheaacd_acelp_decode.c", + "decoder/ixheaacd_acelp_mdct.c", + "decoder/ixheaacd_acelp_tools.c", + "decoder/ixheaacd_adts_crc_check.c", + "decoder/ixheaacd_api.c", + "decoder/ixheaacd_arith_dec.c", + "decoder/ixheaacd_avq_dec.c", + "decoder/ixheaacd_avq_rom.c", + "decoder/ixheaacd_basic_funcs.c", + "decoder/ixheaacd_basic_ops.c", + "decoder/ixheaacd_bitbuffer.c", + "decoder/ixheaacd_block.c", + "decoder/ixheaacd_channel.c", + "decoder/ixheaacd_common_initfuncs.c", + "decoder/ixheaacd_common_lpfuncs.c", + "decoder/ixheaacd_common_rom.c", + "decoder/ixheaacd_create.c", + "decoder/ixheaacd_decode_main.c", + "decoder/ixheaacd_drc_freq_dec.c", + "decoder/ixheaacd_dsp_fft32x32s.c", + "decoder/ixheaacd_ec_rom.c", + "decoder/ixheaacd_env_calc.c", + "decoder/ixheaacd_env_dec.c", + "decoder/ixheaacd_env_extr.c", + "decoder/ixheaacd_esbr_envcal.c", + "decoder/ixheaacd_esbr_polyphase.c", + "decoder/ixheaacd_ext_ch_ele.c", + "decoder/ixheaacd_fft.c", + "decoder/ixheaacd_fft_ifft_32x32.c", + "decoder/ixheaacd_freq_sca.c", + "decoder/ixheaacd_fwd_alias_cnx.c", + "decoder/ixheaacd_hbe_dft_trans.c", + "decoder/ixheaacd_hbe_trans.c", + "decoder/ixheaacd_headerdecode.c", + "decoder/ixheaacd_hufftables.c", + "decoder/ixheaacd_huff_code_reorder.c", + "decoder/ixheaacd_huff_tools.c", + "decoder/ixheaacd_hybrid.c", + "decoder/ixheaacd_imdct.c", + "decoder/ixheaacd_initfuncs.c", + "decoder/ixheaacd_init_config.c", + "decoder/ixheaacd_latmdemux.c", + "decoder/ixheaacd_ld_mps_config.c", + "decoder/ixheaacd_ld_mps_dec.c", + "decoder/ixheaacd_longblock.c", + "decoder/ixheaacd_lpc.c", + "decoder/ixheaacd_lpc_dec.c", + "decoder/ixheaacd_lpfuncs.c", + "decoder/ixheaacd_lpp_tran.c", + "decoder/ixheaacd_lt_predict.c", + "decoder/ixheaacd_mps_apply_common.c", + "decoder/ixheaacd_mps_apply_m1.c", + "decoder/ixheaacd_mps_apply_m2.c", + "decoder/ixheaacd_mps_bitdec.c", + "decoder/ixheaacd_mps_blind.c", + "decoder/ixheaacd_mps_calc_m1m2_common.c", + "decoder/ixheaacd_mps_calc_m1m2_emm.c", + "decoder/ixheaacd_mps_calc_m1m2_tree_515x.c", + "decoder/ixheaacd_mps_calc_m1m2_tree_51sx.c", + "decoder/ixheaacd_mps_calc_m1m2_tree_52xx.c", + "decoder/ixheaacd_mps_calc_m1m2_tree_727x.c", + "decoder/ixheaacd_mps_calc_m1m2_tree_757x.c", + "decoder/ixheaacd_mps_dec.c", + "decoder/ixheaacd_mps_decorr.c", + "decoder/ixheaacd_mps_get_index.c", + "decoder/ixheaacd_mps_hybrid_filt.c", + "decoder/ixheaacd_mps_initfuncs.c", + "decoder/ixheaacd_mps_m1m2_common.c", + "decoder/ixheaacd_mps_mdct_2_qmf.c", + "decoder/ixheaacd_mps_parse.c", + "decoder/ixheaacd_mps_polyphase.c", + "decoder/ixheaacd_mps_poly_filt.c", + "decoder/ixheaacd_mps_pre_mix.c", + "decoder/ixheaacd_mps_process.c", + "decoder/ixheaacd_mps_reshape_bb_env.c", + "decoder/ixheaacd_mps_res_block.c", + "decoder/ixheaacd_mps_res_channel.c", + "decoder/ixheaacd_mps_res_channel_info.c", + "decoder/ixheaacd_mps_res_longblock.c", + "decoder/ixheaacd_mps_res_pns_js_thumb.c", + "decoder/ixheaacd_mps_res_pulsedata.c", + "decoder/ixheaacd_mps_res_tns.c", + "decoder/ixheaacd_mps_rom.c", + "decoder/ixheaacd_mps_smoothing.c", + "decoder/ixheaacd_mps_temp_process.c", + "decoder/ixheaacd_mps_temp_reshape.c", + "decoder/ixheaacd_mps_tonality.c", + "decoder/ixheaacd_multichannel.c", + "decoder/ixheaacd_peak_limiter.c", + "decoder/ixheaacd_pns_js_thumb.c", + "decoder/ixheaacd_pred_vec_block.c", + "decoder/ixheaacd_process.c", + "decoder/ixheaacd_ps_bitdec.c", + "decoder/ixheaacd_ps_dec.c", + "decoder/ixheaacd_ps_dec_flt.c", + "decoder/ixheaacd_pvc_rom.c", + "decoder/ixheaacd_qmf_dec.c", + "decoder/ixheaacd_rev_vlc.c", + "decoder/ixheaacd_rom.c", + "decoder/ixheaacd_sbrdecoder.c", + "decoder/ixheaacd_sbrdec_initfuncs.c", + "decoder/ixheaacd_sbrdec_lpfuncs.c", + "decoder/ixheaacd_sbr_crc.c", + "decoder/ixheaacd_sbr_dec.c", + "decoder/ixheaacd_sbr_rom.c", + "decoder/ixheaacd_spectrum_dec.c", + "decoder/ixheaacd_stereo.c", + "decoder/ixheaacd_tcx_fwd_alcnx.c", + "decoder/ixheaacd_tcx_fwd_mdct.c", + "decoder/ixheaacd_thumb_ps_dec.c", + "decoder/ixheaacd_tns.c", + "decoder/ixheaacd_usac_ec.c", + "decoder/ixheaacd_Windowing.c", + "decoder/drc_src/impd_drc_api.c", + "decoder/drc_src/impd_drc_bitbuffer.c", + "decoder/drc_src/impd_drc_dec.c", + "decoder/drc_src/impd_drc_dynamic_payload.c", + "decoder/drc_src/impd_drc_eq.c", + "decoder/drc_src/impd_drc_extr_delta_coded_info.c", + "decoder/drc_src/impd_drc_filter_bank.c", + "decoder/drc_src/impd_drc_gain_dec.c", + "decoder/drc_src/impd_drc_gain_decoder.c", + "decoder/drc_src/impd_drc_init.c", + "decoder/drc_src/impd_drc_interface_decoder.c", + "decoder/drc_src/impd_drc_loudness_control.c", + "decoder/drc_src/impd_drc_main_td_process.c", + "decoder/drc_src/impd_drc_multiband.c", + "decoder/drc_src/impd_drc_parametric_dec.c", + "decoder/drc_src/impd_drc_peak_limiter.c", + "decoder/drc_src/impd_drc_process.c", + "decoder/drc_src/impd_drc_rom.c", + "decoder/drc_src/impd_drc_selection_process.c", + "decoder/drc_src/impd_drc_selection_process_drcset_selection.c", + "decoder/drc_src/impd_drc_selection_process_init.c", + "decoder/drc_src/impd_drc_shape_filter.c", + "decoder/drc_src/impd_drc_static_payload.c", + ], + + sanitize: { + misc_undefined: [ + "unsigned-integer-overflow", + "signed-integer-overflow", + "bounds", + ], + cfi: true, + config: { + cfi_assembly_support: true, + }, + }, + + arch: { + arm: { + cflags: [ + ], + + local_include_dirs: [ + "decoder/armv7", + ], + + srcs: [ + "decoder/generic/ixheaacd_function_selector_generic.c", + "decoder/generic/ixheaacd_qmf_dec_generic.c", + "decoder/armv7/ixheaacd_fft_armv7.c", + "decoder/armv7/ixheaacd_function_selector_armv7.c", + "decoder/armv7/ixheaacd_qmf_dec_armv7.c", + "decoder/armv7/ixheaacd_aac_ld_dec_rearrange.s", + "decoder/armv7/ixheaacd_apply_rot.s", + "decoder/armv7/ixheaacd_apply_scale_fac.s", + "decoder/armv7/ixheaacd_autocorr_st2.s", + "decoder/armv7/ixheaacd_auto_corr.s", + "decoder/armv7/ixheaacd_calcmaxspectralline.s", + "decoder/armv7/ixheaacd_calc_post_twid.s", + "decoder/armv7/ixheaacd_calc_pre_twid.s", + "decoder/armv7/ixheaacd_complex_fft_p2.s", + "decoder/armv7/ixheaacd_complex_ifft_p2.s", + "decoder/armv7/ixheaacd_conv_ergtoamplitude.s", + "decoder/armv7/ixheaacd_conv_ergtoamplitudelp.s", + "decoder/armv7/ixheaacd_cos_sin_mod.s", + "decoder/armv7/ixheaacd_dct3_32.s", + "decoder/armv7/ixheaacd_decorr_filter2.s", + "decoder/armv7/ixheaacd_dec_DCT2_64_asm.s", + "decoder/armv7/ixheaacd_eld_decoder_sbr_pre_twiddle.s", + "decoder/armv7/ixheaacd_enery_calc_per_subband.s", + "decoder/armv7/ixheaacd_esbr_cos_sin_mod_loop1.s", + "decoder/armv7/ixheaacd_esbr_cos_sin_mod_loop2.s", + "decoder/armv7/ixheaacd_esbr_fwd_modulation.s", + "decoder/armv7/ixheaacd_esbr_qmfsyn64_winadd.s", + "decoder/armv7/ixheaacd_esbr_radix4bfly.s", + "decoder/armv7/ixheaacd_expsubbandsamples.s", + "decoder/armv7/ixheaacd_ffr_divide16.s", + "decoder/armv7/ixheaacd_fft32x32_ld2_armv7.s", + "decoder/armv7/ixheaacd_fft_15_ld.s", + "decoder/armv7/ixheaacd_fwd_modulation.s", + "decoder/armv7/ixheaacd_harm_idx_zerotwolp.s", + "decoder/armv7/ixheaacd_imdct_using_fft.s", + "decoder/armv7/ixheaacd_inv_dit_fft_8pt.s", + "decoder/armv7/ixheaacd_lap1.s", + "decoder/armv7/ixheaacd_mps_complex_fft_64_asm.s", + "decoder/armv7/ixheaacd_mps_synt_out_calc.s", + "decoder/armv7/ixheaacd_mps_synt_post_fft_twiddle.s", + "decoder/armv7/ixheaacd_mps_synt_post_twiddle.s", + "decoder/armv7/ixheaacd_mps_synt_pre_twiddle.s", + "decoder/armv7/ixheaacd_no_lap1.s", + "decoder/armv7/ixheaacd_overlap_add1.s", + "decoder/armv7/ixheaacd_overlap_add2.s", + "decoder/armv7/ixheaacd_post_radix_compute2.s", + "decoder/armv7/ixheaacd_post_radix_compute4.s", + "decoder/armv7/ixheaacd_post_twiddle.s", + "decoder/armv7/ixheaacd_post_twiddle_overlap.s", + "decoder/armv7/ixheaacd_pre_twiddle_compute.s", + "decoder/armv7/ixheaacd_radix4_bfly.s", + "decoder/armv7/ixheaacd_rescale_subbandsamples.s", + "decoder/armv7/ixheaacd_sbr_imdct_using_fft.s", + "decoder/armv7/ixheaacd_sbr_qmfanal32_winadds.s", + "decoder/armv7/ixheaacd_sbr_qmfanal32_winadds_eld.s", + "decoder/armv7/ixheaacd_sbr_qmfsyn64_winadd.s", + "decoder/armv7/ixheaacd_shiftrountine.s", + "decoder/armv7/ixheaacd_shiftrountine_with_rnd_eld.s", + "decoder/armv7/ixheaacd_shiftrountine_with_round.s", + "decoder/armv7/ixheaacd_shiftrountine_with_round_hq.s", + "decoder/armv7/ixheaacd_tns_ar_filter_fixed.s", + "decoder/armv7/ixheaacd_tns_ar_filter_fixed_32x16.s", + "decoder/armv7/ixheaacd_tns_parcor2lpc_32x16.s", + ], + }, + + arm64: { + cflags: [ + ], + + local_include_dirs: [ + "decoder/armv8", + ], + + srcs: [ + "decoder/armv8/ixheaacd_function_selector_armv8.c", + "decoder/armv8/ixheaacd_qmf_dec_armv8.c", + "decoder/armv8/ixheaacd_apply_scale_factors.s", + "decoder/armv8/ixheaacd_calcmaxspectralline.s", + "decoder/armv8/ixheaacd_cos_sin_mod_loop1.s", + "decoder/armv8/ixheaacd_cos_sin_mod_loop2.s", + "decoder/armv8/ixheaacd_fft32x32_ld2_armv8.s", + "decoder/armv8/ixheaacd_imdct_using_fft.s", + "decoder/armv8/ixheaacd_inv_dit_fft_8pt.s", + "decoder/armv8/ixheaacd_no_lap1.s", + "decoder/armv8/ixheaacd_overlap_add1.s", + "decoder/armv8/ixheaacd_overlap_add2.s", + "decoder/armv8/ixheaacd_postradixcompute4.s", + "decoder/armv8/ixheaacd_post_twiddle.s", + "decoder/armv8/ixheaacd_post_twiddle_overlap.s", + "decoder/armv8/ixheaacd_pre_twiddle.s", + "decoder/armv8/ixheaacd_sbr_imdct_using_fft.s", + "decoder/armv8/ixheaacd_sbr_qmfsyn64_winadd.s", + "decoder/armv8/ixheaacd_sbr_qmf_analysis32_neon.s", + "decoder/armv8/ixheaacd_shiftrountine_with_round.s", + "decoder/armv8/ixheaacd_shiftrountine_with_round_eld.s", + ], + }, + + x86: { + cflags: [ + ], + + local_include_dirs: [ + ], + + srcs: [ + "decoder/generic/ixheaacd_qmf_dec_generic.c", + "decoder/x86/ixheaacd_function_selector_x86.c", + ], + }, + + x86_64: { + cflags: [ + ], + + local_include_dirs: [ + ], + + srcs: [ + "decoder/generic/ixheaacd_qmf_dec_generic.c", + "decoder/x86_64/ixheaacd_function_selector_x86_64.c", + ], + }, + + riscv64: { + cflags: [ + ], + + local_include_dirs: [ + ], + + srcs: [ + "decoder/generic/ixheaacd_qmf_dec_generic.c", + "decoder/generic/ixheaacd_function_selector_generic.c", + ], + }, + }, +} + +cc_library_static { + name: "libxaacenc", + + vendor_available: true, + host_supported: true, + cflags: [ + "-O3", + "-DLOUDNESS_LEVELING_SUPPORT", + ], + + export_include_dirs: [ + "common", + "encoder", + "encoder/drc_src", + ], + + srcs: [ + "common/ixheaac_esbr_fft.c", + "common/ixheaac_esbr_rom.c", + "common/ixheaac_fft_ifft_32x32_rom.c", + "encoder/iusace_acelp_enc.c", + "encoder/iusace_acelp_rom.c", + "encoder/iusace_acelp_tools.c", + "encoder/iusace_arith_enc.c", + "encoder/iusace_avq_enc.c", + "encoder/iusace_avq_rom.c", + "encoder/iusace_block_switch.c", + "encoder/iusace_bitbuffer.c", + "encoder/iusace_enc_fac.c", + "encoder/iusace_enc_main.c", + "encoder/iusace_esbr_inter_tes.c", + "encoder/iusace_esbr_pvc.c", + "encoder/iusace_esbr_pvc_rom.c", + "encoder/iusace_esbr_rom.c", + "encoder/iusace_fd_fac.c", + "encoder/iusace_fft.c", + "encoder/iusace_lpc.c", + "encoder/iusace_lpc_avq.c", + "encoder/iusace_lpd_enc.c", + "encoder/iusace_lpd_rom.c", + "encoder/iusace_lpd_utils.c", + "encoder/iusace_ms.c", + "encoder/iusace_psy_rom.c", + "encoder/iusace_psy_mod.c", + "encoder/iusace_psy_utils.c", + "encoder/iusace_rom.c", + "encoder/iusace_tcx_enc.c", + "encoder/iusace_tcx_mdct.c", + "encoder/iusace_tns_usac.c", + "encoder/iusace_windowing.c", + "encoder/iusace_write_bitstream.c", + "encoder/ixheaace_adjust_threshold.c", + "encoder/ixheaace_api.c", + "encoder/ixheaace_asc_write.c", + "encoder/ixheaace_basic_ops.c", + "encoder/ixheaace_bitbuffer.c", + "encoder/ixheaace_bitbuffer_hp.c", + "encoder/ixheaace_bits_count.c", + "encoder/ixheaace_block_switch.c", + "encoder/ixheaace_calc_ms_band_energy.c", + "encoder/ixheaace_channel_map.c", + "encoder/ixheaace_common_rom.c", + "encoder/ixheaace_cplx_pred.c", + "encoder/ixheaace_dynamic_bits.c", + "encoder/ixheaace_enc_init.c", + "encoder/ixheaace_enc_main.c", + "encoder/ixheaace_fd_enc.c", + "encoder/ixheaace_fd_mdct.c", + "encoder/ixheaace_fd_qc_adjthr.c", + "encoder/ixheaace_fd_qc_util.c", + "encoder/ixheaace_fd_quant.c", + "encoder/ixheaace_fft.c", + "encoder/ixheaace_group_data.c", + "encoder/ixheaace_huffman_rom.c", + "encoder/ixheaace_hybrid.c", + "encoder/ixheaace_hybrid_init.c", + "encoder/ixheaace_interface.c", + "encoder/ixheaace_loudness_measurement.c", + "encoder/ixheaace_mdct_480.c", + "encoder/ixheaace_mps_bitstream.c", + "encoder/ixheaace_mps_dct.c", + "encoder/ixheaace_mps_delay.c", + "encoder/ixheaace_mps_dmx_tdom_enh.c", + "encoder/ixheaace_mps_enc.c", + "encoder/ixheaace_mps_filter.c", + "encoder/ixheaace_mps_frame_windowing.c", + "encoder/ixheaace_mps_huff_tab.c", + "encoder/ixheaace_mps_hybrid_filter.c", + "encoder/ixheaace_mps_nlc_enc.c", + "encoder/ixheaace_mps_onset_detect.c", + "encoder/ixheaace_mps_param_extract.c", + "encoder/ixheaace_mps_polyphase.c", + "encoder/ixheaace_mps_qmf.c", + "encoder/ixheaace_mps_rom.c", + "encoder/ixheaace_mps_static_gain.c", + "encoder/ixheaace_mps_tools_rom.c", + "encoder/ixheaace_mps_tree.c", + "encoder/ixheaace_mps_vector_functions.c", + "encoder/ixheaace_ms_stereo.c", + "encoder/ixheaace_nf.c", + "encoder/ixheaace_ps_bitenc.c", + "encoder/ixheaace_ps_enc.c", + "encoder/ixheaace_ps_enc_init.c", + "encoder/ixheaace_psy_configuration.c", + "encoder/ixheaace_psy_mod.c", + "encoder/ixheaace_psy_utils.c", + "encoder/ixheaace_psy_utils_spreading.c", + "encoder/ixheaace_qc_main_hp.c", + "encoder/ixheaace_qc_util.c", + "encoder/ixheaace_quant.c", + "encoder/ixheaace_radix2_fft.c", + "encoder/ixheaace_resampler.c", + "encoder/ixheaace_resampler_init.c", + "encoder/ixheaace_rom.c", + "encoder/ixheaace_sbr_code_envelope.c", + "encoder/ixheaace_sbr_code_envelope_lp.c", + "encoder/ixheaace_sbr_crc.c", + "encoder/ixheaace_sbr_env_est.c", + "encoder/ixheaace_sbr_env_est_init.c", + "encoder/ixheaace_sbr_frame_info_gen.c", + "encoder/ixheaace_sbr_freq_scaling.c", + "encoder/ixheaace_sbr_hbe_dft_trans.c", + "encoder/ixheaace_sbr_hbe_fft_ifft_32x32.c", + "encoder/ixheaace_sbr_hbe_polyphase.c", + "encoder/ixheaace_sbr_hbe_trans.c", + "encoder/ixheaace_sbr_inv_filtering_estimation.c", + "encoder/ixheaace_sbr_main.c", + "encoder/ixheaace_sbr_misc.c", + "encoder/ixheaace_sbr_missing_harmonics_det.c", + "encoder/ixheaace_sbr_noise_floor_est.c", + "encoder/ixheaace_sbr_qmf_enc.c", + "encoder/ixheaace_sbr_qmf_enc_init.c", + "encoder/ixheaace_sbr_rom.c", + "encoder/ixheaace_sbr_ton_corr.c", + "encoder/ixheaace_sbr_ton_corr_hp.c", + "encoder/ixheaace_sbr_tran_det.c", + "encoder/ixheaace_sbr_tran_det_hp.c", + "encoder/ixheaace_sbr_write_bitstream.c", + "encoder/ixheaace_sf_estimation.c", + "encoder/ixheaace_signal_classifier.c", + "encoder/ixheaace_signal_classifier_rom.c", + "encoder/ixheaace_static_bits.c", + "encoder/ixheaace_stereo_preproc.c", + "encoder/ixheaace_tns.c", + "encoder/ixheaace_tns_hp.c", + "encoder/ixheaace_tns_init.c", + "encoder/ixheaace_tns_params.c", + "encoder/ixheaace_write_adts_adif.c", + "encoder/ixheaace_write_bitstream.c", + "encoder/drc_src/impd_drc_api.c", + "encoder/drc_src/impd_drc_enc.c", + "encoder/drc_src/impd_drc_gain_calculator.c", + "encoder/drc_src/impd_drc_gain_enc.c", + "encoder/drc_src/impd_drc_mux.c", + "encoder/drc_src/impd_drc_tables.c", + "encoder/drc_src/impd_drc_uni_drc_eq.c", + "encoder/drc_src/impd_drc_uni_drc_filter_bank.c", + ], + + sanitize: { + misc_undefined: [ + "unsigned-integer-overflow", + "signed-integer-overflow", + "bounds", + ], + cfi: true, + config: { + cfi_assembly_support: true, + }, + }, +} + +subdirs = ["test"] diff --git a/crates/vendor/libxaac-sys/libxaac/CMakeLists.txt b/crates/vendor/libxaac-sys/libxaac/CMakeLists.txt new file mode 100644 index 00000000..82539b79 --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.5.1) +project(libxaac C CXX) +enable_language(ASM) + +option(BUILD64 "Build for 64 bit" OFF) +set(XAAC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}") +set(XAAC_CONFIG_DIR "${CMAKE_CURRENT_BINARY_DIR}") +find_package(Threads REQUIRED) + +set(CMAKE_STATIC_LIBRARY_PREFIX "") + +include("${XAAC_ROOT}/cmake/utils.cmake") + +libxaac_add_compile_options() +libxaac_add_definitions() + +if(NOT COMPILER_HAS_SANITIZER) + libxaac_set_link_libraries() +endif() + +include("${XAAC_ROOT}/common/common.cmake") +include("${XAAC_ROOT}/decoder/libxaacdec.cmake") +include("${XAAC_ROOT}/test/decoder/xaacdec.cmake") +include("${XAAC_ROOT}/fuzzer/xaac_dec_fuzzer.cmake") + +include("${XAAC_ROOT}/encoder/libxaacenc.cmake") +include("${XAAC_ROOT}/test/encoder/xaacenc.cmake") +include("${XAAC_ROOT}/fuzzer/xaac_enc_fuzzer.cmake") + diff --git a/crates/vendor/libxaac-sys/libxaac/LICENSE b/crates/vendor/libxaac-sys/libxaac/LICENSE new file mode 100644 index 00000000..edcd8c28 --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright (c) 2018, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/vendor/libxaac-sys/libxaac/METADATA b/crates/vendor/libxaac-sys/libxaac/METADATA new file mode 100644 index 00000000..d97975ca --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/METADATA @@ -0,0 +1,3 @@ +third_party { + license_type: NOTICE +} diff --git a/crates/vendor/libxaac-sys/libxaac/MODULE_LICENSE_APACHE2 b/crates/vendor/libxaac-sys/libxaac/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b diff --git a/crates/vendor/libxaac-sys/libxaac/NOTICE b/crates/vendor/libxaac-sys/libxaac/NOTICE new file mode 100644 index 00000000..75ec98bc --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/NOTICE @@ -0,0 +1,19 @@ +/****************************************************************************** + * * + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ***************************************************************************** + * Originally developed and contributed by Ittiam Systems Pvt. Ltd, Bangalore +*/ diff --git a/crates/vendor/libxaac-sys/libxaac/OWNERS b/crates/vendor/libxaac-sys/libxaac/OWNERS new file mode 100644 index 00000000..528d32c4 --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/OWNERS @@ -0,0 +1,3 @@ +# owners for external/libxaac +include platform/frameworks/av:/media/janitors/codec_OWNERS +essick@google.com diff --git a/crates/vendor/libxaac-sys/libxaac/PREUPLOAD.cfg b/crates/vendor/libxaac-sys/libxaac/PREUPLOAD.cfg new file mode 100644 index 00000000..ecf8b8ef --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/PREUPLOAD.cfg @@ -0,0 +1,2 @@ +[Hook Scripts] +mainline_hook = ${REPO_ROOT}/frameworks/av/tools/mainline_hook_project.sh diff --git a/crates/vendor/libxaac-sys/libxaac/README.experimental b/crates/vendor/libxaac-sys/libxaac/README.experimental new file mode 100644 index 00000000..515d685d --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/README.experimental @@ -0,0 +1,5 @@ +This xaac codec (external/xaac) is experimental; it is not yet intended +to be used on production devices. + +This codec should not be configured into any production Android device +that is intended to be shipped. diff --git a/crates/vendor/libxaac-sys/libxaac/README.md b/crates/vendor/libxaac-sys/libxaac/README.md new file mode 100644 index 00000000..d1224b70 --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/README.md @@ -0,0 +1,102 @@ +# Introduction to libxaac + +Extended HE-AAC, the latest innovation member of the MPEG AAC codec family, is ideally suited for adaptive bit rate streaming and digital radio applications. Extended HE-AAC bridges the gap between speech and audio coding and ensures consistent high-quality audio for all signal types, including speech, music, and mixed material. It is the required audio codec for DRM (Digital Radio Mondiale). When it comes to coding, the codec is incredibly effective, generating high-quality audio for music and speech at bitrates as low as 6 kbit/s for mono and 12 kbit/s for stereo services. By switching to extremely low bitrate streams, Extended HE-AAC streaming apps and streaming radio players can provide uninterrupted playback even during very congested network conditions. + +As the Extended High Efficiency AAC Profile is a logical evolution of the MPEG Audio's popular AAC Family profiles, the codec supports AAC-LC, HE-AACv1 (AAC+) and HE-AACv2 (eAAC+) audio object type encoding. The bitrate that was saved with AAC family tools can be used to enhance video quality. Extended HE-AAC is a well-liked option for a number of applications since it is a strong and effective audio codec that provides high-quality audio at low bitrates. + +![Architecture](docs/libxaac_block_diagram.jpg) + +One of the key features of libxaac (refer to above image) is that it has support for AAC-LD (Low Delay), AAC-ELD (Enhanced Low Delay), and AAC-ELDv2 (Enhanced Low Delay version 2) modes. AAC-LD mode provides low latency encoding, making it suitable for applications such as interactive communication and live audio streaming. It helps to reduce the delay in the encoding process to improve the real-time performance of the system. AAC-ELD mode improves the low-delay performance of HE-AAC by reducing the coding delay while maintaining high audio quality. It was observed that minimum delay it can achieve is 15ms. In order to achieve low delay coding scheme and low bitrate, it uses the Low Delay SBR tool. AAC-ELDv2 is the most advanced version of AAC-based low delay coding. It provides an enhanced version of AAC-ELD, which provides even lower coding delay and higher audio quality. + +MPEG-D USAC, also known as Unified Speech and Audio Coding, is designed to provide high-quality audio coding at low bit rates. MPEG-D USAC combines advanced audio coding techniques with state-of-the-art speech coding algorithms to achieve significant compression gains while maintaining perceptual audio quality. The standard supports a wide range of audio content, including music, speech, and mixed audio, making it versatile for different use cases. With its ability to deliver high-fidelity audio at reduced bit rates, MPEG-D USAC plays a crucial role in optimizing bandwidth usage and enhancing the user experience in the digital audio domain. + +Overall, libxaac, with support for AAC-LD, AAC-ELD, and AAC-ELDv2 modes, is a versatile audio coding technology that can be used for a wide range of applications, such as broadcasting, streaming, and teleconferencing which requires high-quality audio compression with minimal delay. + +Also, the libxaac supports MPEG-D DRC (Dynamic Range Control) for the Extended HE-AAC profile in both encoder and decoder. MPEG-D DRC offers a bitrate efficient representation of dynamically compressed versions of an audio signal. This is achieved by adding a low-bitrate DRC metadata stream to the audio signal. DRC includes dedicated sections for metadata-based loudness leveling, clipping prevention, ducking, and for generating a fade-in and fade-out to supplement the main dynamic range compression functionality. The DRC effects available at the DRC decoder are generated at the DRC encoder side. At the DRC decoder side, the audio signal may be played back without applying DRC, or an appropriate DRC effect is selected and applied based on the given playback scenario. It offers flexible solutions to efficiently support the widespread demand for technologies such as loudness normalization and dynamic range compression for various playback scenarios. + +#### Note +* The operating points for MPEG-D USAC (along with MPEG-D DRC) in libxaac encoder is currently restricted to 64 kbps and 96 kbps. It is recommended to use the encoder at these operating points only. The support shall be extended to other operating points soon. +* Further Quality enhancements for AAC-ELD and AAC-ELDv2 modes may be pushed as quality assessment is in progress. + +# Building the libxaac decoder and encoder + +## Building for AOSP +* Makefile for building the libxaac decoder and encoder library is provided in root(`libxaac/`) folder. +* Makefile for building the libxaac decoder and encoder testbench is provided in `test` folder. +* Build the library followed by the application using the below commands: +Go to root directory +``` +$ mm +``` + +## Using CMake +Users can also use cmake to build for `x86`, `x86_64`, `armv7`, `armv8` and Windows (MSVS project) platforms. + +### Creating MSVS project files +To create MSVS project files for the libxaac decoder and encoder from cmake, run the following commands: +``` +Go to the root directory(libxaac/). +Create a new folder in the project root directory and move to the newly created folder. + +$ cd +$ mkdir bin +$ cd bin +$ cmake -G "Visual Studio 15 2017" .. +``` + +Above command will create Win32 version of MSVS workspace +To create MSVS project files for Win64 version from cmake, run the following commands: +``` +$ mkdir cmake_build +$ cd cmake_build +$ cmake -G "Visual Studio 15 2017 Win64" .. +``` +The above command creates MSVS 2017 project files. If the version is different, modify the generator name accordingly. + +### Building for native platforms +Run the following commands to build the libxaac decoder and encoder for native platform: +``` +Go to the root directory(libxaac/). +Create a new folder in the project root directory and move to the newly created folder. + +$ cd +$ mkdir bin +$ cd bin +$ cmake .. +$ make +``` + +### Cross-compiler based builds +Ensure to edit the file cmake/toolchains/*_toolchain.cmake to set proper paths in host for corresponding platforms. + +### Building for x86_32 on a x86_64 Linux machine +``` +$ cd +$ mkdir build +$ cd build +$ cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/x86_toolchain.cmake +$ make +``` + +### Building for aarch32/aarch64 +Update 'CMAKE_C_COMPILER', 'CMAKE_CXX_COMPILER', 'CMAKE_C_COMPILER_AR', and 'CMAKE_CXX_COMPILER_AR' in CMAKE_TOOLCHAIN_FILE passed below +``` +$ cd +$ mkdir build +$ cd build +``` + +### For aarch64 +``` +$ cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/aarch64_toolchain.cmake +$ make +``` + +### For aarch32 +``` +$ cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/aarch32_toolchain.cmake +$ make +``` + +### For API and testbench usage of decoder, please refer [`README_dec.md`](README_dec.md) +### For API and testbench usage of encoder, please refer [`README_enc.md`](README_enc.md) diff --git a/crates/vendor/libxaac-sys/libxaac/README_dec.md b/crates/vendor/libxaac-sys/libxaac/README_dec.md new file mode 100644 index 00000000..3db93207 --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/README_dec.md @@ -0,0 +1,245 @@ + + +# Introduction to libxaac decoder APIs + +## Decoder APIs + +A single API is used to get and set configurations and execute the decode thread, based on command index passed. +* ia_xheaacd_dec_api + +| **API Command** | **API Sub Command** | **Description** | +|------|------|------| +|IA_API_CMD_GET_LIB_ID_STRINGS | IA_CMD_TYPE_LIB_NAME | Gets the decoder library name | +|IA_API_CMD_GET_LIB_ID_STRINGS | IA_CMD_TYPE_LIB_VERSION | Gets the decoder version | +|IA_API_CMD_GET_API_SIZE | 0 | Gets the memory requirements size of the API | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_API_PRE_CONFIG_PARAMS | Sets the configuration parameters of the libxaac decoder to default values | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_API_POST_CONFIG_PARAMS | Sets the attributes(size, priority, alignment) of all memory types required by the application onto the memory structure | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_PROCESS | Search for the valid header, does header decode to get the parameters and initializes state and configuration structure | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_DONE_QUERY | Checks if the initialization process has completed | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_PCM_WDSZ | Sets the bit width of the output PCM samples. The value has to be 16 | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_SAMP_FREQ | Sets the core AAC sampling frequency for RAW header decoding | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_EFFECT_TYPE | Sets the value of DRC effect type | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_TARGET_LOUDNESS | Sets the value of DRC target loudness | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DOWNMIX | Sets the parameter whether the output needs to be down-mix to mono(1) or not(0) | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_TOSTEREO | Sets the flag to disable interleave mono to stereo | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DSAMPLE | Sets the parameter whether the output needs to be downsampled(1) or not(0) | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_ISMP4 | Sets the flag to 0 or 1 to indicate whether given test vector is an mp4 file or not | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_MAX_CHANNEL | Sets the maximum number of channels present. Its maximum value is 2 for stereo library and 8 for multichannel library | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_COUP_CHANNEL | Sets the number of coupling channels to be used for coupling. It can take values from 0 to 16. This command is supported only if the library has multichannel support | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DOWNMIX_STEREO | Sets the flag of downmixing n number of channels to stereo. Can be 0 or 1. This command is supported only if the library has multichannel support | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_DISABLE_SYNC | Sets the flag of ADTS syncing or not ADTS syncing as 0 or 1 | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_AUTO_SBR_UPSAMPLE | Sets the parameter auto SBR upsample to 0 or 1. Used in case of stream changing from SBR present to SBR not present | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_CUT | Sets the value of DRC cut factor | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_BOOST | Sets the value of DRC boost factor | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_HEAVY_COMP | Sets the parameter to either enable/disable DRC heavy compression | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_FRAMESIZE | Sets the parameter whether decoder should decode for frame length 480 or 512 | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_LD_TESTING | Sets the flag to enable LD testing in decoder | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_HQ_ESBR | Sets the flag to enable/disable high quality eSBR | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_PS_ENABLE | Sets the flag to indicate the presence of PS data in eSBR bit stream | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_PEAK_LIMITER | Sets the flag to disable/enable peak limiter | +|IA_API_CMD_SET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_FRAMELENGTH_FLAG | Sets to flag to indicate whether decoder should decode for frame length 960 or 1024 | +|IA_API_CMD_SET_CONFIG_PARAM | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_TARGET_LEVEL | Sets the value of DRC target level | +|IA_API_CMD_SET_CONFIG_PARAM | IA_XHEAAC_DEC_CONFIG_ERROR_CONCEALMENT | Sets the flag to disable/enable error concealment | +|IA_API_CMD_SET_CONFIG_PARAM | IA_XHEAAC_DEC_CONFIG_PARAM_ESBR | Sets the flag to disable/enable eSBR | +|IA_API_CMD_GET_N_MEMTABS | 0 | Gets the number of memory types | +|IA_API_CMD_GET_N_TABLES | 0 | Gets the number of tables | +|IA_API_CMD_GET_MEM_INFO_SIZE | 0 | Gets the size of the memory type being referred to by the index | +|IA_API_CMD_GET_MEM_INFO_ALIGNMENT | 0 | Gets the alignment information of the memory-type being referred to by the index | +|IA_API_CMD_GET_MEM_INFO_TYPE | 0 | Gets the type of memory being referred to by the index | +|IA_API_CMD_SET_MEM_PTR | 0 | Sets the pointer to the memory being referred to by the index to the input value | +|IA_API_CMD_GET_TABLE_INFO_SIZE | 0 | Gets the size of the memory type being referred to by the index | +|IA_API_CMD_GET_TABLE_INFO_ALIGNMENT | 0 | Gets the alignment information of the memory-type being referred to by the index | +|IA_API_CMD_GET_TABLE_PTR | 0 | Gets the address of the current location of the table | +|IA_API_CMD_SET_TABLE_PTR | 0 | Sets the relocated table address | +|IA_API_CMD_GET_MEMTABS_SIZE | 0 | Gets the size of the memory structures | +|IA_API_CMD_SET_MEMTABS_PTR | 0 | Sets the memory structure pointer in the library to the allocated value | +|IA_API_CMD_INPUT_OVER | 0 | Signals the end of bit-stream to the library | +|IA_API_CMD_SET_INPUT_BYTES | 0 | Sets the number of bytes available in the input buffer for initialization | +|IA_API_CMD_GET_CURIDX_INPUT_BUF | 0 | Gets the number of input buffer bytes consumed by the last initialization | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_PCM_WDSZ | Gets the output PCM word size | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_SAMP_FREQ | Gets the sampling frequency | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_NUM_CHANNELS | Gets the output number of channels | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_CHANNEL_MASK | Gets the channel mask | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_CHANNEL_MODE | Gets the channel mode. (Mono or PS/Stereo/Dual-mono) | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_SBR_MODE | Gets the SBR mode (Present/ Not Present) | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_EFFECT_TYPE | Gets the value of DRC effect type | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_TARGET_LOUDNESS | Gets the value of DRC target loudness | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_LOUD_NORM | Gets the value of DRC loudness normalization level | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_PARAM_AOT | Gets the value of audio object type | +|IA_API_CMD_GET_CONFIG_PARAM | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_LOUDNESS_LEVELING | Gets the value of loudness leveling flag | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_EXT_ELE_PTR | Gets the extension element pointer | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_EXT_ELE_BUF_SIZES | Gets the extension element buffer sizes | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_NUM_ELE | Gets the number of configuration elements | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_NUM_CONFIG_EXT | Gets the number of extension elements | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_GAIN_PAYLOAD_LEN | Gets the gain payload length | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_GAIN_PAYLOAD_BUF | Gets the gain payload buffer | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_CONFIG_GET_NUM_PRE_ROLL_FRAMES | Gets the number of preroll frames | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_DRC_IS_CONFIG_CHANGED | Gets the value of DRC config change flag | +|IA_API_CMD_GET_CONFIG_PARAM | IA_ENHAACPLUS_DEC_DRC_APPLY_CROSSFADE | Gets the value of DRC crossfade flag | +|IA_API_CMD_EXECUTE | IA_CMD_TYPE_DO_EXECUTE | Executes the decode thread | +|IA_API_CMD_EXECUTE | IA_CMD_TYPE_DONE_QUERY | Checks if the end of decode has been reached | +|IA_API_CMD_GET_OUTPUT_BYTES | 0 | Gets the number of bytes output by the decoder in the last frame | + +### Aliases for some of the macros exist as XHEAAC + +| **Macro** | **Alias** | +|------|------| +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_PCM_WDSZ | IA_XHEAAC_DEC_CONFIG_PARAM_PCM_WDSZ | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_SAMP_FREQ | IA_XHEAAC_DEC_CONFIG_PARAM_SAMP_FREQ | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_NUM_CHANNELS | IA_XHEAAC_DEC_CONFIG_PARAM_NUM_CHANNELS | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_CHANNEL_MASK | IA_XHEAAC_DEC_CONFIG_PARAM_CHANNEL_MASK | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_CHANNEL_MODE | IA_XHEAAC_DEC_CONFIG_PARAM_CHANNEL_MODE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_SBR_MODE | IA_XHEAAC_DEC_CONFIG_PARAM_SBR_MODE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_EFFECT_TYPE | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_EFFECT_TYPE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_TARGET_LOUDNESS | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_TARGET_LOUDNESS | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_LOUD_NORM | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_LOUD_NORM | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DOWNMIX | IA_XHEAAC_DEC_CONFIG_PARAM_DOWNMIX | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_TOSTEREO | IA_XHEAAC_DEC_CONFIG_PARAM_TOSTEREO | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DSAMPLE | IA_XHEAAC_DEC_CONFIG_PARAM_DSAMPLE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_ISMP4 | IA_XHEAAC_DEC_CONFIG_PARAM_MP4FLAG | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_MAX_CHANNEL | IA_XHEAAC_DEC_CONFIG_PARAM_MAX_CHANNEL | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_COUP_CHANNEL | IA_XHEAAC_DEC_CONFIG_PARAM_COUP_CHANNEL | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DOWNMIX_STEREO | IA_XHEAAC_DEC_CONFIG_PARAM_DOWNMIX_STEREO | +|IA_ENHAACPLUS_DEC_CONFIG_DISABLE_SYNC | IA_XHEAAC_DEC_CONFIG_DISABLE_SYNC | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_AUTO_SBR_UPSAMPLE | IA_XHEAAC_DEC_CONFIG_PARAM_AUTO_SBR_UPSAMPLE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_CUT | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_CUT | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_BOOST | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_BOOST | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_DRC_HEAVY_COMP | IA_XHEAAC_DEC_CONFIG_PARAM_DRC_HEAVY_COMP | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_FRAMESIZE | IA_XHEAAC_DEC_CONFIG_PARAM_FRAMESIZE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_LD_TESTING | IA_XHEAAC_DEC_CONFIG_PARAM_LD_TESTING | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_HQ_ESBR | IA_XHEAAC_DEC_CONFIG_PARAM_HQ_ESBR | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_PS_ENABLE | IA_XHEAAC_DEC_CONFIG_PARAM_PS_ENABLE | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_AOT | IA_XHEAAC_DEC_CONFIG_PARAM_AOT | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_PEAK_LIMITER | IA_XHEAAC_DEC_CONFIG_PARAM_PEAK_LIMITER | +|IA_ENHAACPLUS_DEC_CONFIG_PARAM_FRAMELENGTH_FLAG | IA_XHEAAC_DEC_CONFIG_PARAM_FRAMELENGTH_FLAG | + +## DRC APIs + +A single API is used to get and set configurations and execute the decode thread, based on command index passed. +* ia_drc_dec_api + +| **API Command** | **API Sub Command** | **Description** | +|------|------|------| +|IA_API_CMD_GET_API_SIZE | 0 | Gets the memory requirements size of the API | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_API_PRE_CONFIG_PARAMS | Sets the configuration parameters of the libxaac decoder to default values | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_API_POST_CONFIG_PARAMS | Sets the attributes(size, priority, alignment) of all memory types required by the application onto the memory structure | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_PROCESS | Search for the valid header, does header decode to get the parameters and initializes state and configuration structure | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_CPY_BSF_BUFF | Sets the bitstream split format buffer | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_CPY_IC_BSF_BUFF | Sets the configuration bitstream split format buffer | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_CPY_IL_BSF_BUFF | Sets the loudness bitstream split format buffer | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_CPY_IN_BSF_BUFF | Sets the interface bitstream split format buffer | +|IA_API_CMD_INIT | IA_CMD_TYPE_INIT_SET_BUFF_PTR | Sets the input buffer pointer | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_SAMP_FREQ | Sets the sampling frequency of the input stream/data | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_NUM_CHANNELS | Sets the number of channels in the input stream/data | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_PCM_WDSZ | Sets the PCM word size of the input data | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_BITS_FORMAT | Sets the bit stream format | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_INT_PRESENT | Sets the DRC decoders interface present flag to 1 or 0 | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_FRAME_SIZE | Sets the frame size | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_DRC_EFFECT_TYPE | Sets the value of DRC effect type | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_DRC_TARGET_LOUDNESS | Sets the value of DRC target loudness | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_DRC_LOUD_NORM | Sets the value of DRC loudness normalization level | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_APPLY_CROSSFADE | Sets the value of DRC crossfade flag | +|IA_API_CMD_SET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_CONFIG_CHANGED | Sets the value of DRC config change flag | +|IA_API_CMD_GET_MEM_INFO_SIZE | 0 | Gets the size of the memory type being referred to by the index | +|IA_API_CMD_GET_MEMTABS_SIZE | 0 | Gets the size of the memory structures | +|IA_API_CMD_SET_MEMTABS_PTR | 0 | Sets the memory structure pointer in the library to the allocated value | +|IA_API_CMD_GET_N_MEMTABS | 0 | Gets the number of memory types | +|IA_API_CMD_SET_INPUT_BYTES | 0 | Sets the number of bytes available in the input buffer for initialization | +|IA_API_CMD_GET_MEM_INFO_ALIGNMENT | 0 | Gets the alignment information of the memory-type being referred to by the index | +|IA_API_CMD_GET_MEM_INFO_TYPE | 0 | Gets the type of memory being referred to by the index | +|IA_API_CMD_SET_MEM_PTR | 0 | Sets the pointer to the memory being referred to by the index to the input value | +|IA_API_CMD_SET_INPUT_BYTES_BS | 0 | Sets the number of bytes to be processed in bitstream split format | +|IA_API_CMD_SET_INPUT_BYTES_IC_BS | 0 | Sets the number of bytes to be processed in configuration bitstream split format | +|IA_API_CMD_SET_INPUT_BYTES_IL_BS | 0 | Sets the number of bytes to be processed in loudness bitstream split format | +|IA_API_CMD_GET_CONFIG_PARAM | IA_DRC_DEC_CONFIG_PARAM_NUM_CHANNELS | Gets the output number of channels | +|IA_API_CMD_EXECUTE | IA_CMD_TYPE_DO_EXECUTE | Executes the decode thread | + +## Flowchart of calling sequence + +![API Flowchart](docs/Api_flowchart_dec.png) + +# Running the libxaac decoder + +The libxaac decoder can be run by providing command-line parameters(CLI options) directly or by providing a parameter file as a command line argument. + +Command line usage : +``` + -ifile: -imeta: -ofile: [options] + +[options] can be, +[-mp4:] +[-pcmsz:] +[-dmix:] +[-esbr_hq:] +[-esbr_ps:] +[-tostereo:] +[-dsample:] +[-drc_cut_fac:] +[-drc_boost_fac:] +[-drc_target_level:] +[-drc_heavy_comp:] +[-effect:] +[-target_loudness:] +[-nosync:] +[-sbrup:] +[-flflag:} +[-fs:] +[-maxchannel:] +[-coupchannel:] +[-downmix:] +[-fs480:] +[-ld_testing:] +[-peak_limiter_off:] +[-err_conceal:] +[-esbr:] +[-loudness_leveling:] + +where, + is the input AAC-LC/HE-AACv1/HE-AACv2/AAC-LD/AAC-ELD/AAC-ELDv2/USAC file name. + is a text file which contains metadata. To be given when -mp4:1 is enabled. + is the output file name. + is a flag that should be set to 1 when passing raw stream along with meta data text file. + is the bits per sample info. value can be 16 or 24. + is to enable/disable always mono output. Default 1. + is to enable/disable high quality eSBR. Default 0. + is to indicate eSBR with PS. Default 0. + is to enable/disable always interleaved to stereo output. Default 1. + is to enable/disable down-sampled SBR output. Default auto identification from header. + is to set DRC cut factor value. Default value is 1. + is to set DRC boost factor. Default value is 1. + is to set DRC target reference level. Default value is 108. + is to enable/disable DRC heavy compression. Default value is 0. + is to set DRC effect type. Default value is 0. + is to set target loudness level. Default value is -24. + is to disable the ADTS/ADIF sync search i.e when enabled the decoder expects the header to be at the start of input buffer. Default 0. + is to enable(1) or disable(0) auto SBR upsample in case of stream changing from SBR present to SBR not present. Default 1. + is flag for decoding framelength of 1024 or 960. 1 to decode 960 frame length, 0 to decode 1024 frame length. + Frame length value in the GA header will override this option. Default 0. + is to indicate the core AAC sample rate for a RAW stream. If this is specified no other file format headers are searched for. + is the number of maxiumum channels the input may have. Default is 6 for multichannel libraries and 2 for stereo libraries. + is element instance tag of independent coupling channel to be mixed. Default is 0. + is flag for Downmix. Give 1 to get stereo (downmix) output. Default is 0. + is to indicate ld frame size. 0 is for 512 frame length, 1 is for 480 frame length. Default value is 512 (0). + is to enable/disable ld decoder testing. Default value is 0. + is to enable/disable peak limiter. Default value is 0. + is to enable/disable error concealment. Default value is 0. + is to enable/disable eSBR. Default value is 1. + is to enable / disable loudness leveling. Default value is 1. + +``` +Note: `loudness_leveling_flag` is applicable only if `LOUDNESS_LEVELING_SUPPORT` macro is enabled. +Sample CLI: +``` + -ifile:in_file.aac -ofile:out_file.wav -pcmsz:16 +``` + +# Validating the libxaac decoder + +Conformance testing for AAC/HE-AACv1/HE-AACv2 mainly involves comparing +decoder under test output with the ISO and 3GPP reference decoded output. + +Testing for USAC is done using encoded streams generated using ISO USAC +reference encoder. The output generated by libxaac USAC decoder is +compared against the output generated by ISO USAC decoder for 16-bit +conformance on the respective(ARMv7, ARMv8, X86_32, X86_64) platforms. + diff --git a/crates/vendor/libxaac-sys/libxaac/README_enc.md b/crates/vendor/libxaac-sys/libxaac/README_enc.md new file mode 100644 index 00000000..b80b1e2b --- /dev/null +++ b/crates/vendor/libxaac-sys/libxaac/README_enc.md @@ -0,0 +1,123 @@ +# Introduction libxaac encoder APIs + +# Encoder APIs + +| **API Call** | **Description** | +|------|------| +|ixheaace_get_lib_id_strings| Gets the encoder library name and version number details | +|ixheaace_create| Sets the encoder configuration parameters, gets the memory requirements and allocates required memory | +|ixheaace_process| Encodes the input frame data | +|ixheaace_delete| Frees the allocated memories for the encoder | + +## Flowchart of calling sequence +![API Flowchart](docs/Api_flowchart_enc.png) + +# Audio Object Types(AOT) +| **AOT** | **Description** | +|------|------| +|2|AAC-LC| +|5|HE-AACv1| +|23|AAC-LD| +|29|HE-AACv2| +|39|AAC-ELD| +|42|USAC| + +# Running the libxaac encoder +The libxaac encoder can be run by providing command-line parameters(CLI options) directly or by providing a parameter file as a command line argument. +The reference paramfile is placed in `encoder\test` directory(paramfilesimple.txt) +The configuration file for DRC is placed in `encoder\test` directory(impd_drc_config_params.txt). Please make sure this file is placed in the same directory as the executable. + +# Command line usage : +``` + -ifile: -ofile: [options] +(or) + -paramfile: +[options] can be, +[-br:] +[-mps:] +[-adts:] +[-tns:] +[-nf:] +[-cmpx_pred:] +[-framesize:] +[-aot:] +[-esbr:] +[-full_bandwidth:] +[-max_out_buffer_per_ch:] +[-tree_cfg:] +[-usac:] +[-ccfl_idx:] +[-pvc_enc:] +[-harmonic_sbr:] +[-esbr_hq:] +[-drc:] +[-inter_tes_enc:] +[-rap:] +[-stream_id:] + +where, + is the parameter file with multiple commands + is the input 16-bit WAV or PCM file name + is the output ADTS/ES file name + is the bit-rate in bits per second. Default value is 48000. + Valid values are 0 (disable MPS) and 1 (enable MPS). Default is 0. + Valid values are 0 ( No ADTS header) and 1 ( generate ADTS header). Default is 0. + Valid values are 0 (disable TNS) and 1 (enable TNS). Default is 1. + controls usage of noise filling in encoding. Default 0. + controls usage of complex prediction in encoding. Default is 0. + is the framesize to be used. + For AOT 23, 39 (LD core coder profiles) valid values are 480 and 512. Default is 512. + For AOT 2, 5, 29 (LC core coder profiles) valid values are 960 and 1024. Default is 1024. + For AOT 42 (USAC profile) valid values are 768 and 1024. Default is 1024. + is the Audio object type. + 2 for AAC-LC + 5 for HE-AACv1(Legacy SBR) + 23 for AAC-LD + 29 for HE-AACv2 + 39 for AAC-ELD + 42 for USAC + Default is 2 for AAC-LC. + Valid values are 0 (disable eSBR) and 1 (enable eSBR). Default is 0 for HE-AACv1 profile (legacy SBR) and 1 for USAC profile. + Enable use of full bandwidth of input. Valid values are 0(disable full bandwidth) and 1(enable full bandwidth). Default is 0. + is the maximum size of bit reservoir to be used. Valid values are from -1 to 6144. -1 will omit use of bit reservoir. Default is 384. + MPS tree config + 0 for '212' + 1 for '5151' + 2 for '5152' + 3 for '525' + Default 0 for stereo input and 1 for 6ch input. + USAC encoding mode to be chose + 0 for 'usac_switched' + 1 for 'usac_fd' + 2 for 'usac_td' + Default 'usac_fd' + is the core coder framelength index for USAC encoder. + Valid values are 0, 1, 2, 3, 4. eSBR enabling is implicit + 0 - Core coder framelength of USAC is 768 and eSBR is disabled + 1 - Core coder framelength of USAC is 1024 and eSBR is disabled + 2 - Core coder framelength of USAC is 768 and eSBR ratio 8:3 + 3 - Core coder framelength of USAC is 1024 and eSBR ratio 2:1 + 4 - Core coder framelength of USAC is 1024 and eSBR ratio 4:1 + Valid values are 0 (disable PVC encoding) and 1 (enable PVC encoding). Default is 0. + Valid values are 0 (disable harmonic SBR) and 1 (enable harmonic SBR). Default is 0. + Valid values are 0 (disable high quality eSBR) and 1 (enable high quality eSBR). Default is 0. + Valid values are 0 (disable DRC encoding) and 1 (enable DRC encoding). Default is 0. + Valid values are 0 (disable inter-TES encoding) and 1 (enable inter-TES encoding). Default is 0. + is the time interval between audio preroll frames in ms. It is applicable only for AOT 42. Valid values are -1 (Audio preroll sent only at beginning of file) and greater than 1000 ms. Default is -1. + It is the stream id used to uniquely identify configuration of a stream within a set of associated streams. It is applicable only for AOT 42. Valid values are 0 to 65535. Any value outside this range is type-casted to a value of unsigned short type. Default is 0. + is used to discard algorithmic delay from the decoded file. It is applicable only for AOT 42. Valid values are 0 and 1. Default is 1." + +``` +Sample CLI: +``` +-ifile:input_file.wav -ofile:out_file.aac -br: –aot: